我一直对Terminal发生了什么感到困惑。但在上周,我使用xterm.js在浏览器中显示了一个交互式终端,我终于想到要问一个相当基本的问题:当你在终端中按下键盘上的一个键时(例如删除,或Escape,或a),发送了哪些字节?像往常一样,我们将通过做一些实验并看看会发生什么来回答这个问题:)远程终端是非常古老的技术首先,我想说使用xterm.js在浏览器中显示一个终端可能看起来就像一个新事物,但事实并非如此。在70年代,计算机价格昂贵。因此,一个组织的许多员工将共享一台计算机,并且每个人都可以有自己的“终端”来连接到该计算机。例如,这是70年代或80年代的VT100终端的照片。这看起来像一台计算机(它有点大!),但它不是-它只是显示实际计算机发送的任何信息。DECVT100终端当然,在70年代,他们并没有为此使用Websockets,但是来回发送信息的方式与当时几乎相同。(照片中的终端来自西雅图的LivingComputerMuseum,我曾经去那里在一个非常古老的Unix系统上编写FizzBu??zzined,所以我可能真的用过那台机器或它的兄弟姐妹之一!我真的希望LivingComputerMuseum又开张了,能玩老式电脑真是太爽了。)发了什么消息?显然,如果您想连接到远程计算机(使用ssh或xterm.js和Websockets或其他),则需要在客户端和服务器之间发送一些信息。具体来说:客户端需要发送用户输入的键盘信息(比如ls-l)。服务器需要告诉客户端要在屏幕上显示什么。让我们看一下在浏览器中运行远程终端的真实程序,看看来回发送了哪些信息!我们将使用goterm进行实验我在GitHub上找到了一个名为goterm的小程序,它运行一个Go服务器并允许您使用xterm.js在浏览器中与终端交互。这个程序非常不安全,但它很简单易学。自从它上次更新是6年前以来,我将它与最新的xterm.js一起使用。然后我添加了一些日志语句来打印出每次通过WebSocket发送/接收的字节数。让我们来看看在几个不同的终端交互过程中的发送和接收!示例:ls首先,让我们运行ls。这是我在xterm.js终端上看到的:~:/play$lsfile~:/play$这是发送和接收的内容:(在我的代码中,我每次记录客户端发送的字节数:sent:[bytes],每次从服务器接收到的字节:recv:[bytes])sent:"l"recv:"l"sent:"s"recv:"s"sent:"\r"recv:"\r\n\x1b[?2004l\r"recv:"file\r\n"recv:"\x1b[~:/play$"我注意到这个输出中有3件事:回声:客户端发送一个l,然后立即收到发回的l。我认为这里的重点是客户端真的很笨——它不知道当我输入l时,我希望l被回显到屏幕上。服务器进程必须明确告知它才能显示它。换行:当我按回车键时,它发送一个\r'(回车)符号而不是\n'(换行)。转义序列:\x1b是一个ASCII转义字符,所以\x1b[?2004h告诉终端要显示什么或其他什么。我认为这是一系列颜色,但我不确定。稍后我们将更详细地讨论转义序列。好吧,现在让我们做一些稍微复杂一点的事情。示例:Ctrl+C接下来,让我们看看使用Ctrl+C中断进程时会发生什么。这是我在终端中看到的:~:/play$cat^C~:/play$这是客户端发送和接收的内容。发送:“c”接收:“c”发送:“a”接收:“a”发送:“t”接收:“t”发送:“\r”接收:“\r\n\x1b[?2004l\r"sent:"\x03"recv:"^C"recv:"\r\n"recv:"\x1b[?2004h"recv:"~:/play$"当我按下Ctrl+C,客户端发送\x03。如果我查找ASCII表,\x03是“文本结束”,这似乎是合理的。我觉得这真的很酷,因为我对Ctrl+C的工作原理有点困惑——很高兴知道它只是发送一个\x03字符。我相信当我们按Ctrl+C时cat被中断的原因是服务器端的Linux内核接收到这个\x03字符并识别它意味着“中断”,然后发送一个SIGINT到拥有伪-的进程组终端。所以它是在内核中处理的,而不是在用户空间中处理的。示例:Ctrl+D让我们尝试完全相同的操作,只是使用Ctrl+D。这是我在终端中看到的:~:/play$cat~:/play$这是发送和接收的内容:sent:"c"recv:"c"sent:"a"recv:"a"sent:"t"recv:"t"sent:"\r"recv:"\r\n\x1b[?2004l\r"sent:"\x04"recv:"\x1b[?2004h"recv:"~:/play$"它与Ctrl+C非常相似,但发送的是\x04而不是\x03。非常好!\x04对应ASCII“传输结束”。Ctrl+其他字母呢?接下来我开始想知道——如果我发送Ctrl+e,会发送哪些字节?原来它只是字母表中字母的编号,就像这样。Ctrl+a=>1Ctrl+b=>2Ctrl+c=>3Ctrl+d=>4...Ctrl+z=>26此外,Ctrl+Shift+b与Ctrl+b具有完全相同的效果(它写入0x2)。键盘上的其他键呢?它们的映射如下:Tab->0x9(和Ctrl+I一样,因为I是第9个字母)Escape->\x1bBackspace->\x7f首页->\x1b[H结束->\x1b[F打印屏幕->\x1b\x5b\x31\x3b\x35\x41插入->\x1b\x5b\x32\x7e删除->\x1b\x5b\x33\x7e我的元密钥没有'根本不起作用,Alt呢?从我的实验(和一些搜索)来看,似乎Alt和Escape实际上是同一件事,除了按下Alt本身不会向终端发送任何字符,而按下Escape本身会。所以:alt+d=>\x1bd(其他字母相同)alt+shift+d=>\x1bD(其他字母相同)等等让我们看看另一个例子!示例:nano以下是我在运行文本编辑器nano时发送和接收的内容:recv:"\r\x1b[~:/play$"sent:"n"[[??]byte{0x6e}]recv:"n”发送:“a”[[]byte{0x61}]recv:“a”发送:“n”[[]byte{0x6e}]recv:“n”发送:“o”[[]byte{0x6f}]recv:"o"sent:"\r"[[]byte{0xd}]recv:"\r\n\x1b[?2004l\r"recv:"\x1b[?2004h"recv:"\x1b[?1049h\x1b[22;0;0t\x1b[1;16r\x1b(B\x1b[m\x1b[4l\x1b[?7h\x1b[39;49m\x1b[?1h\x1b=\x1b[?1h\x1b=\x1b[?25l"recv:"\x1b[39;49m\x1b(B\x1b[m\x1b[H\x1b[2J"recv:"\x1b(B\x1b[0;7mGNUnano6.2\x1b[44bNewBuffer\x1b[53b\x1b[1;123H\x1b(B\x1b[m\x1b[14;38H\x1b(B\x1b[0;7m[欢迎使用nano。获取基本帮助,输入Ctrl+G。]\x1b(B\x1b[m\r\x1b[15d\x1b(B\x1b[0;7m^G\x1b(B\x1b[mHelp\x1b[15;16H\x1b(B\x1b[0;7m^O\x1b(B\x1b[m写出\x1b(B\x1b[0;7m^W\x1b(B\x1b[m\x1b(B\x1b[0;7m^K\x1b(B\x1b[mCut\x1b[15;61H"您可以从用户界面看到一些文本,例如“GNUnano6.2”,而这些\x1b[27m东西是转义序列说说转义序列吧!ANSI转义序列上面这些nano发给客户端的\x1b[东西叫做“转义”序列”或“转义码”。这是因为它们都以“转义”字符\x1b开头。它们可以改变光标的位置,使文本变成粗体或加下划线,改变颜色等。维基百科描述一些历史,如果你有兴趣,可以去看看。一个简单的例子:如果你运行echo-e'\e[0;31mhi\e[0mthere'它会打印“hithere”,其中“hi”是红色和“那里”是黑色。此页面上有一些颜色和格式的参考转义码示例。我认为有几种不同的转义码标准,但我的理解是人们在Unix上使用的最常见的转义码集来自VT100(博文顶部图片中的那个旧终端),在过去的40年里并没有真正改变。转义码是你的终端得到mes的原因sed,如果你把一些二进制数据放到你的屏幕上——通常你会不小心打印出一堆随机转义码,这会弄乱你的终端——如果你把足够多的二进制数据放到你的终端里,那肯定是一个0x1b字节在那里。手动输入转义序列?在前面的章节中,我们讨论了Home键是如何映射到\x1b[H.这3个字节是Escape+[+H(因为Escape是\x1b)。如果我在xterm.js终端中手动键入Escape,然后[,然后是H,我将出现在该行的开头,就像我按下Home键一样。我注意到这在我计算机上的Fishshell中不起作用——如果我键入Escape然后[它只打印[而不是让我继续转义序列。我问过我的朋友Jesse,他写了一堆Rust终端代码Jesse告诉我很多程序都实现了转义码的超时——如果你在最短时间内没有按下另一个键,它就认为它实际上不是任何更长的是转义码。显然,这可以在Fishshell中使用fish_escape_delay_ms进行配置,因此我运行了setfish_escape_delay_ms1000并且能够手动输入转义码。效果很好!终端编码有点奇怪我想在这里暂停一下,我认为你按下的键映射到字节的方式很奇怪。例如,如果我们今天从头开始编写击键代码,我们可能不会让Ctrl+a做与Ctrl+Shift+a完全相同的事情。Alt是与Escape相同的控制序列(如颜色/移动光标)使用与Escape键相同的字节,因此您需要依靠时间来确定它是一个控制序列还是用户只是想按Escape。但是所有这些都是在70年代或80年代或其他什么时候设计的,然后为了向后兼容而需要永远保持不变,所以这就是我们得到的:笑脸:改变终端中的窗口大小并不是你的全部通过来回发送字节来完成。例如,当终端调整大小时,我们必须以不同的方式告诉Linux窗口大小发生了变化。这是在goterm中执行此操作的Go代码如下所示:syscall.Syscall(syscall.SYS_IOCTL,tty.Fd(),syscall.TIOCSWINSZ,uintptr(unsafe.Pointer(&resizeMessage)),)这是使用ioctl系统调用。我对ioctl的理解是,它是一个系统调用,可以处理一些其他系统调用未涵盖的随机内容,我猜通常与IO相关。syscall.TIOCSWINSZ是一个整数常量,它告诉ioctl在这种情况下我们希望它做什么(更改终端窗口大小)。这也是xterm的工作方式。在本文中,我们一直在谈论远程终端,其中客户端和服务器位于不同的计算机上。但实际上,如果您使用的是像xterm这样的终端仿真器,那么所有这些都以完全相同的方式工作,只是很难注意到,因为字节不是通过网络连接发送的。文章到此结束。关于终端肯定还有很多东西需要学习(我们可以更多地讨论颜色、原始模式与熟化模式、Unicode支持或Linux伪终端界面),但我会在这里停下来,因为现在是晚上10点,这篇文章有点长,我认为我的大脑今天无法处理有关终端的更多新信息。
