tty之ssh的基本实现原理
前言
linux服务器用户直接使用终端与服务器交互, 用户可以执行各种命令, 如果电脑上装的是linux无桌面版本, 那么点击开机键, 不久就能见到屏幕上提示登陆的信息, 用户登陆后能进行各种操作, 这个黑色的命令行界面就是终端。
此外, 桌面版本的linux用户虽然没有直接使用终端, 却能够用类似konsole的终端模拟器与终端交互。此外我们在本地的linux终端上输入ssh 对端服务器的ip
, 能够连接到远端服务器, 也能与终端进行交互。这两种场景都离不开伪终端的作用
阅读本文可以知道伪终端的基本概念, 知道如何实现一个终端模拟器, 知道如何实现一个ssh或者screen那样的软件, 下面是前置知识, 务必阅读。
要阅读这篇文章, 还需要对linux的session, signal, shell有一定的了解。
终端模拟器的简要图景
终端模拟器是模拟终端(命令行模式下真正的终端, 这种终端是一种硬件支持, 如果我们没有安装桌面, 我们访问的用户接口就叫终端)的一种软件, 它可以用多种多样的桌面应用框架实现界面, 比如qt, electron等。实现终端模拟器需要使用内核提供的pty设备, 这种设备可以用来实现终端模拟器或ssh, screen之类的软件。
pty就是伪终端的意思, 我们按下CTRL-ALT-F2
切换的终端是真正的终端。而比如我们在桌面上打开的konsole是一个经典的终端模拟器。
要创建一个终端模拟器, 需要两个进程, 父进程负责渲染界面, 子进程负责执行程序(使用exec命令)。下面的demo可以看出父子进程的职能:
1 |
|
我们可以认为, 一个终端模拟器的父进程需要从主设备文件中读, 用读出来的内容渲染界面(比如用一些如qt的桌面应用框架画图), 捕获用户按键, 将内容写入主设备文件(简而言之, 终端模拟器就是对master进行io操作, 包括read
, write
, ioctl
, 并画界面)。
子进程需要借助打开的从设备文件(成为伪终端的控制终端), 用dup
系统调用分别获得stdin(fd=0), stdout(fd=1)和stderr(fd=2), 并且执行exec
系统调用进入shell。
可以总结: 终端其实就是个cs架构的”服务器”, 终端模拟器只是读写master设备, 然后绘画界面, 将键盘事件写入到master设备做通信罢了。
我们也可以看到, 子进程通过拷贝(dup
)从设备文件得到了stdin
, stdout
, stderr
。难道它们其实是一个东西? 我们平时写c语言代码, 可是认为它们有着完全不同的意义。下面来验证一下, 看看是否能向stdin
写入内容:
1 |
|
最终结果很让人惊讶, 确实能写!那么可以说stdin
, stdout
, stderr
其实代指同一个设备文件对象。但为了符合标准, 将其分为了三个不同的fd, 暗含不同的逻辑含义。我们平时可能不会注意到这点, 0
, 1
, 2
代表同一个设备确实让我耳目一新。
终端的种类
现在打开你的终端模拟器, 比如konsole, 输入echo $TERM
, 可以得到以下输出:
1 | [root@mycom markity]# echo $TERM |
现在按CTRL-ALT-F2
, 在命令行模式下做同样的操作:
1 | [root@mycom markity]# echo $TERM |
我们来解释这些输出:
- 终端有不同的种类, 例如
xterm-256color
,vt100
,linux
- 不同种类的终端拥有不同的能力
- 某种终端所拥有的能力通过
/usr/share/terminfo
的数据库查询
自终端诞生以来, 兼容性就需要被考虑, 因此有数据库存储了各种各样终端的”能力”。我们桌面上终端模拟器的终端类型是xterm-256color
, 它有很多linux
终端没有的能力。比如我们可以支持鼠标, 这就是终端模拟器的拓展。
当终端模拟器大小改变?
当我们使用终端模拟器时, 我们可以改变终端模拟器的窗口大小, 然而vim似乎能正常获悉窗口大小改变, 做出响应式布局, 这是什么原理?
先来阐述步骤, 终端模拟器改变大小时, 父进程将做出以下操作:
1 | ioctl(master, TIOCSWINSZ, &win_size_struct); |
内核收到后, 将给伪终端的前台进程发送SIGWINCH, 此时vim就能获悉终端大小改变, 然后做出响应了。
实现ssh的简要图景:
我们知道, ssh分为客户端和服务端。服务器运行sshd这个守护进程, 我们在本地的终端运行ssh这个命令就能连接到远端, 随后输入用户名和密码就能像操纵本地终端一样操作远端linux服务器了。
现在不考虑加密, 只考虑简单实现, 我们来捋顺一下步骤
其中客户端要有下面的步骤:
0.将自身终端直接设置成raw mode(此时line discipline不发送任何信号, 也不会有行缓冲, 用户输入的字符立即被程序接收, 相当于用户的每个按键产生的字节都被内核原封不动地写入0号描述符, 我们可以从0读出字节序列)。并且设立SIGWINCH的信号handler, 在接收到信号的时候, 调用ioctl
, 命令用TIOCGWINSZ
就能获得新的宽高。
1.与服务端建立tcp连接, 发送自身终端的宽高
2.然后不断从socket读, 写入文件描述符0(其实0,1,2是一个东西, 写入0,1,2没区别的), 不断从文件描述符0读, 写入socket(这里要多路复用)。当SIGWINCH响应时, 也应该读出新的宽高, 发送给服务端。这里发送给服务器的数据包类型分两种, 一种是WINCH事件, 一种是用户正常读写, 要把包分成两种类型通知服务器
其中服务端要有以下步骤:
1.监听tcp连接, 接收客户端发来的宽高, 并初始化pty设备
2.开启子进程, 把stdin
, stdout
, stderr
安排好。调用exec执行/bin/bash
3.父进程负责从socket读, 写入0文件描述符, 且不断从0文件描述符读, 写入socket(此处多路复用)。父进程需要分析包的类型, 如果是WINCH事件, 那么ioctl(master, TIOCSWINSZ, &win_size_struct);
。如果是正常写入, 那么write
给master文件描述符
简要代码实现
点击此处, 这是一个go版本的简易实现, 能够响应窗口大小改变。