OnTheSSH(版本1.6以后)的SSH终端,是重新设计的,面向Web编程、账号密码安全可控、用户行为可记录的SSH终端,下面以一个使用场景来描述详细:
在一个云服务器租赁中心,主要业务是出租云Linux服务器给用户。这里有两个用户user a 和 user b,其中user a租用了Server 1和Server 2,user b租用了Server 3和Server 4。基本的安全要求是user a只能访问自己租用的服务器,而不能访问user b租用的服务器,反之亦然。其次云服务提供方出于安全考虑,需要定期(频繁地)更新服务器密码,同时希望能记录用户登录服务器的日志和在用户在SSH终端中输入的每一条指令。
使用传统的SSH终端,无法满足此场景需求,因为会遇到两个难题:
- 租赁中心更新服务器密码时,需要将密码告知给租赁用户,告知过程难免遇到密码泄露问题,同时频繁的更新密码会引起用户的不满,而用户更希望自己来修改密码。另外出于安全考虑,服务器密码通常比较复杂,用户登录时容易输入出错,也会引起用户的不满。
- SSH终端和Linux服务器之间的信息传输是加密的,无法通过侦听、截获等手段,记录用户的登录和使用过程。(SSH传输协议有足够的安全性,无法在防火墙、交换机等设备上截获信息)

OnTheSSH(版本1.6以后)的SSH终端,就是为实现此需求重新设计的。如上图所示,架构上支持在终端和服务器之间嵌入一个“接口机”,此接口机面向SSH终端是服务器,面向Linux Server是客户端,不仅可以记录用户登录和使用SSH命令的日志,还维护着一张密码映射表,主要的改动如下:
- 用户终端登录时,并不使用Server的密码(出于安全考虑,用户永远不知道Server的密码)。例如user a使用密码1234登录,接口机收到密码1234后,根据映射表转换为Server的真实密码,再以SSH协议登录到Server。
- Server密码可以随时进行修改,只需同步到密码映射表中,这种改动对用户使用是无感知的,用户还可以用原来的密码1234。
- SSH终端和Server之间所有的通讯,经过接口机转发,这样在接口机上可以方便的记录用户日志,可详细到用户输入的每一条命令。
OnTheSSH(版本1.6以后)的SSH终端作为独立的程序时,他的结构是这样的:

程序分为两部分,一部分是Qt语言编写的界面模块部分,另一部分是Rust语言编写的核心模块部分,核心模块作为动态链接库和界面模块组成应用程序。核心模块做了“足够”的封装,足以让界面模块得到以下改变:
- SSH终端的交互模式由原来的双向交互改为单向交互,让界面模块适合于HTTP方式交互,方便将终端嵌入到网页中运行。
- 将SSH终端原来复杂的控制序列(xterm control sequences),转换为类似于html的标签格式,极大简化了界面编程,并适合用JavaScript来实现终端界面。
- 屏蔽了SSH原来的默认缓冲区、备用缓冲区、键盘应用程序模式、光标键应用程序模式等难于理解的概念,让界面编程习惯更倾向于Web编程。
核心模块的设计考虑了二次开发,界面模块和核心模块是松耦合的。经过简易封装可为核心模块添加Socket接口或http接口。应对前面的场景需求,可以搭建以下架构:

如上图所示:终端界面程序,可以封装为普通的应用程序,也可嵌入到网页中。网络通讯可以采用TCP/Socket,也可以是HTTP/HTTPS。前面场景中论述的密码映射和用户日志功能,可在Socket API或HTTP API中实现。核心模块在设计上考虑了接口的可封装性,因此Socket API和HTTP API的实现,对于稍有能力的编程人员,都不是难事。
HTTP API也可使用我编写的另一个开源项目Module Proxy来实现。(https://gitee.com/dyf029/module-proxy)
类似html标签的界面描述
核心模块做了大量的转换工作,让SSH终端界面部分开发容易了很多,最重要的改变是界面描述由原来复杂的(xterm control sequences)改为标签结构(和html非常相似),下图是一个登录Ubuntu后的界面信息:

页面信息源码如下:
<head><seq>0</seq><rows>28</rows><cols>118</cols><cxy>27;16</cxy><cs>1</cs><bell>0</bell></head>
<body>Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-60-generic x86_64)<br/><br/> * Documentation: https://help.ubuntu.com<br/> * Management: https://landscape.canonical.com<br/> * Support: https://ubuntu.com/pro<br/><br/>Expanded Security Maintenance for Applications is not enabled.<br/><br/>88 updates can be applied immediately.<br/>4 of these updates are standard security updates.<br/>To see these additional updates run: apt list --upgradable<br/><br/>2 additional security updates can be applied with ESM Apps.<br/>Learn more about enabling ESM Apps service at https://ubuntu.com/esm<br/><br/>New release '24.04.2 LTS' available.<br/>Run 'do-release-upgrade' to upgrade to it.<br/><br/>Last login: Wed Jun 11 03:23:11 2025 from 192.168.152.1<br/><b><fc#4><bc#11>dyf@dyf-virtual-machine</bc></fc></b>:<b><fc#6><bc#11>~</bc></fc></b>$ <br/></body>
页面的基本结构由<head>部分和<body>部分组成,<head>有以下标签:
<head>
<seq>0</seq>
<rows>28</rows>
<cols>118</cols>
<cxy>27;16<cxy>
<cs>1</cs>
<bell>0</bell>
</head>
<head>中各标签的含义如下:
- <seq> – 视窗序列,这是个重要概念,后面会详细讲述。
- <rows> – 此页面的行数。这里的28表示终端窗口有28行文字高度。
- <cols> – 此页面的列数。这里的118表示终端窗口有118列文字宽度。
- <cxy> – 光标的行列位置(取值范围: 0;0 ~ cols;rows)。这里的 27;16 表示光标在第28列第17行。
- <cs> – 光标是否显示,1表示显示光标,0表示不显示光标。当终端执行一些命令时(如top)光标是不显示的。
- <bell> – 是否响铃,1响铃,0不响铃。响铃通常是SSH终端的告警提醒,比如vim编辑时,如果想把光标从行首向左移动时,会响铃提示。
<body>中各标签的含义如下(除了<br/>其他标签都是成对出现):
- <br/> – 表示换行
- <b> – 粗体。示例: 这是<b>一个粗体</b>片段
- <u> – 下划线。示例: 这是<u>一个下划线</u>片段
- <fc> – 前景色。示例:这是<fc#3>一个字符颜色</fc>片段,#3是颜色编码(在不同的配色方案下颜色编码代表的颜色是不同的)
- <bc> – 前景色。示例:这是<bc#11>一个字符底色</bc>片段,#11是颜色编码
- <f> – 闪烁。示例:这是<f>一个字符闪烁</f>片段
- 转义符 – 因为是标签描述,如果文字中出现 ‘<‘或’>’字符,核心模块将他们转为’<’或’>’,这和html的转换是一样的。
适合于HTTP方式的交互
在执行ping或top等命令时会不断输出新的内容,这种由服务器生成的新内容要实时反馈到终端界面上,但http协议并不支持这种“推”的交互方式,因此核心模块还负责将双向的通讯模式转换为适合http协议的一问一答的通讯模式。
在核心模块内部维护着界面缓存,Linux服务器产生的最新的界面信息先保存在缓存中,当终端界面请求信息时再返回给终端界面,见下图:

一问一答符合http的通讯方式,但是要及时的反馈界面内容的变化,一问一答的频率要高,这种高频率的轮询可能会造成大量的“垃圾”信息传输,因为多数情况下页面内容并没有变化。<seq>标签就是用来解决这个问题的:在核心模块中会记录这个seq值的变化,每当buf中页面内容变化时seq值就会增加。
界面终端向核心模块发送请求时,会附带seq参数,如果请求参数seq与核心模块记录的seq一致时,表示界面无变化,值不同表示界面有新内容。核心模块返回的信息中包含最新的seq,界面终端会记录seq的更新,下次请求时使用更新后的seq参数。
如果请求的seq与核心模块的一样(界面内容没有变化),核心模块只返回一个简单的结构信息:
<head><seq>1</seq></head>
此时返回的界面信息不含<body>,<head>中也只有一个<seq>标签,这样的结构可以降低轮询时对网络传输的压力。
核心模块提供的重要接口
1)创建session会话
- 参数: Linux服务器地址、SSH服务的端口、账号、密码
- 返回:session id
- 说明:此接口用来创建核心模块与Linux服务器之间的SSH安全连接,并进行用户登录的验证。如果执行成功,返回代表此SSH会话的session id,失败则返回错误信息。
2)创建shell通道
- 参数:session id、窗口字符宽度、窗口字符高度
- 返回:通道号(channel_no)
- 说明:在SSH协议中当session会话创建后,可在此会话中创建若干通道(channel),每个通道可以运行一种应用,这里是创建shell应用通道,参数包含终端窗口的宽度和高度。
3)关闭shell通道
- 参数:session id、通道号
- 说明:关闭shell通道
4)关闭session
- 参数:session id
- 说明:关闭session会话
5)输入
- 参数:session id、通道号、键盘输入的文字或指令
- 说明:用户在SSH终端程序中输入的信息,包括:键盘输入的文字、操作光标的控制、ctrl+Key等功能键信号
6)输出
- 参数:session id、通道号
- 返回:<head>和<body>构建的标签结构的页面信息。
- 说明:此接口需要终端界面程序不断的调用(轮询)。
7)窗口滚动
- 参数:session id、通道号、滚动行数
- 说明:控制窗口视图在核心模块缓冲区上的移动。
8)窗口大小变化
- 参数:session id、通道号、窗口字符宽度、窗口字符宽度
- 说明:终端窗口大小改变、或字体字号的改变,都会造成窗口字符宽度和高度的变化。核心模块需要按新的窗口尺寸,重新计算窗口视图的内容显示。
开发终端窗口程序的注意事项
核心模块封装了大量的逻辑细节,让终端窗口程序的开发容易了很多,但SSH终端毕竟有其复杂性,开发时仍需要注意:
1)因为平时使用SSH shell的习惯,输入光标和显示混在一起,很多人以为整个shell窗口就是一个大的文本编辑框,这是一个错误的理解。SSH shell程序我们把它称为伪终端,而终端的历史要上溯到上世纪七八十年代,不管是终端还是伪终端,重要的一点:输入和输出是分开的,如下图:

左图是传统的终端,键盘发出的信息在终端中经过转换,直接发送给计算机主机;屏幕接收主机发出的信息并显示。注意关键点:键盘输出并不直接打印在屏幕上。右图是SSH伪终端,同样键盘输入信息直接发送到远端Linux主机,屏幕的显示也全是从远端主机发来的信息。
因此屏幕显示是只读的,虽然看上去有个输入光标在屏幕上闪烁,但实际上无论文字怎么显示,光标在哪闪烁,都是从远端主机发来的信息和指令控制的。
2)屏幕的文字输出和布局,是由核心模块计算出来的,终端窗口的文字宽度和文字高度是计算的必要参数。宽度和高度受终端的窗口尺寸、选择的字体、字号的影响,因此终端窗口的改变,如鼠标拖动令窗口尺寸变化、或更改字体字号,都要及时通知核心模块窗口宽度和高度的变化。这里的窗口宽度和高度,并不是像素宽度和像素高度,终端窗口程序需要计算当前的窗口尺寸下,能容纳下多少行多少列当前的字符。
展示文字时还要注意的:1. 选用等宽字体,2. 每个英文或数字字符占用1列,汉字等东亚文字每个字占用2列。
占用2列的字符有哪些?怎样判断?请参考OnTheSSH Terminal V1.6 (Qt源码) shell/util.cpp
3)键盘产生的输入文字,按ACSII或Unicode码发送到核心模块,这些文字在编程上不用特殊考虑。但SSH需要的一些控制指令,如光标移动、Home、End、Backspace等功能键,Ctrl+Key等功能键,在SSH shell中有特殊的指令编码。
指令编码可参考OnTheSSH Terminal V1.6 (Qt源码) shell/inputkeys.cpp,实际上SSH shell的控制序列,对这些功能键的编码更为复杂,核心模块封装并简化了这些编码。
还有一个地方需要注意:Tab键在shell中是命令补全的指令,但在窗口程序中一般用来切换焦点,编程时需要屏蔽焦点切换功能,只发送 ‘\t’ 编码。