远程终端服务的简单实现
大家可能见过类似这样嵌入到网页中的终端,可以在页面上与远程服务器交互,就像 ssh 到远程服务器一样。实现这样一个基于 web 的终端,具有跨平台、易审计、限制用户行为等优点。
本文将介绍如何构建一个最简单的 web 远程终端服务程序。
首先明确几个相关概念:
终端
终端是一种字符型输入输出设备,通过它用户才能与计算机进行 IO。在 linux 系统中,终端设备文件一般位于 /dev/ 下。
每打开一个终端,就会产生一个新的 tty 设备文件。使用命令 tty 可以查看当前使用的终端设备。
终端大致分为:
串行端口终端( /dev/ttySX )。是使用计算机串行端口连接的终端,串行端口所对应的设备名称是/dev/ttyS0、/dev/ttyS1 等等。 控制台终端( /dev/ttyn, /dev/console )。通常在 Linux 系统中,把计算机显示器称为控制台终端,与之相连的设备文件有:tty0, tty1, tty2 等。 控制终端( /dev/tty )。并不面向设备,而是面向进程组的,在 Linux 系统中,一个控制终端控制一个会话。通常情况下,用户通过终端输入的指令经由shell解释和执行,从而与系统内核进行交互。
系统启动以后,在指定的波特率上打开串行端口终端(ttyS0), 并将 STDIN、 STDOUT、STDERR 都绑定到该设备上,然后启动 login 程序等待用户完成登陆 。若用户登陆成功,则启动一个 shell 程序为用户服务,这样用户就拥有一个 shell 终端了。
伪终端
对于远程网络用户来说,上节描述的 Terminal 登录过程并不适用,网络用户既不能远程使用串行端口设备,也不能远程控制显示器设备。因此需要创建一个虚拟的终端设备为其服务 —— 伪终端。
伪终端,顾名思义,不是真正的终端,不能操作某个物理设备。它是虚拟的终端驱动设备,用来模拟串行终端的行为。
当使用 ssh、telnet 等程序连接到某台服务器上时进行操作时,底层使用的就是伪终端技术。
伪终端是成对的逻辑终端设备,分为“主设备”(master)和“从设备”(slave),例如/dev/ptyp3和/dev/ttyp3。
其中,“从设备”提供了与真正终端无异的接口,可以与系统进行 IO,规范终端行输入。; 而“主设备”与管道文件类似,可以进行读写操作。往“主设备”写入的数据会传输到“从设备”,而“从设备”从系统获取到的数据也会同样的传输到“主设备”。因此,也可以说,伪终端是一个双向管道。
上面已经介绍过,想要与系统进行交互,除了有终端设备,还需要 shell 程序。两者结合才能完成用户的指令。
因此,一个远程终端服务程序由两个部分构成:伪终端和 shell 进程。通常构建如下:
1 创建伪终端设备。 2 fork 创建子进程,并将该子进程的标准输入、输出和错误输出均 dup 为伪终端的"从设备"。 3 在子进程中 exec 执行 /bin/bash 命令,启动 shell 进程。由于上一步的操作,该子进程(也就是 shell 进程)的 stdin、stdout 和 stderr 已与伪终端进行了绑定。如此,shell 子进程的输出、输出、错误输出均是通过伪终端的“从设备”进行的。经过上述操作,可以说这个子进程就是一个“终端进程“了:既能够完成终端的输入输出操作,又能解释执行用户输入与系统内核交互。
由于伪终端“双向管道”的特性:对伪终端“主设备”的写操作,将传输到“从设备”,也就是传输给”终端进程“;而”终端进程“执行命令后的输出,将通过“从设备”传输返回至“主设备”。如此一来,对 ”终端进程“ 的 IO 操作完全可以通过操作伪终端的“主设备”来完成。
对“主设备”进行读写操作,就等同于在对一个终端 shell 进行操作。因此,如果在父进程中将该伪终端“主设备”与网络 socket 绑定,就能够实现远程终端操作了。(当然也可以将该“主设备”与其他文件描述符绑定,例如与另一进程通信的管道 fd 绑定等等,这些就取决于功能需求了)
数据传输可见下图:
下面给出实现一个 Remote Terminal 服务的关键代码。
主干框架
代码逻辑与上一节所描述的实现流程一致。
int startShell(int socketFd) // socketFd 为已连接状态可进行数据 IO 的 socket 描述符 int master = -1; int slave = -1; // 捕获子进程退出的信息,处理函数为 wait4child if (signal(SIGCHLD, wait4child) == SIG_ERR) oops("signal error", 0); // 创建伪终端,得到 “主从设备” 文件描述符: master, slave if(OpenSystemPtmx( master, slave) 0) oops("open OpenSystemPtmx error", errno); // 创建子进程 int pid = fork(); if(pid == 0) /* 子进程处理逻辑:将值为 0、1、2 的 fd 都变成伪终端“从设备” slave 的复制品。也就是说子进程的 stdin、stdout、stderr 都指向了 slave */ close(master); setsid(); dup2(slave, 0); dup2(slave, 1); dup2(slave, 2); // 执行 shell execlp("sh", NULL); else if(pid 0) close(master); close(slave); oops("fork err", 0); else // 主进程处理逻辑 int ret = 0; while(ret == 0) // 将从伪终端“主设备” master 读到的数据 echo 到 socket fd ret = echoData(master, socketFd); // 将从 socket fd 读到的数据 echo 到伪终端“主设备” master ret = echoData(socketFd, master); return ret;
创建伪终端
下面给出创建伪终端设备所需的最简单的代码。当然,还可以添加更复杂的代码来实现更多终端设置,例如屏蔽回显等等。
int OpenSystemPtmx(int *pMaster, int *pSlave) int master = open("/dev/ptmx", O_RDWR | O_NOCTTY); if (master == -1) return -1; if (grantpt(master) == -1) return -1; if (unlockpt(master) == -1) return -1; char* slaveName = ptsname(master); if (slaveName == NULL) return -1; int slave = open(slaveName, O_RDWR | O_NOCTTY); if (slave == -1) return -1; *pMaster = master; *pSlave = slave; return 0;
子进程退出处理逻辑
子进程就是 shell 进程。在 shell 中输入 exit 将会退出该进程,为了保证主进程的正常退出,这里在捕获到子进程的退出信号后,直接退出。
void wait4child(int signo) int status; while(waitpid(-1, status, WNOHANG) exit(1);
数据处理
这里给出的只是最简单的示例代码,同步且阻塞的读写。可以看到,在主干代码中,是先从 master echo 数据到 socket的。这是因为 shell 程序启动后,会立即有数据输出到 stdout,也就是 master 了。
例如下图中的输出: sh-3.2$
下面代码的实现是同步阻塞的读写,建议使用更高效的方式,例如 IO 复用等。
// 从 inFd 读取数据,并写入到 outFd int echoData(int inFd, int outFd) char buffer[MAX_SIZE]; bzero(buffer, MAX_SIZE); int nred = read(inFd, buffer, MAX_SIZE); if (nred = 0) return -1; int nwrite = write(outFd, buffer, nred); if (nwrite = 0) return -1; return 0;
1 终端默认是具有回显功能的,且终端是字符设备
Remote Terminal 在用户展示层需要格外注意,因为从 socket 写入到 master 的数据,socket 还会从 master 中读到。
因此 Remote Termial 最简单省事的实现是 在显示层捕获用户输入的每一个字符,并立即通过网络传输该单个字符 。这种方式,保留了 Terminal 最原始的功能,并不用处理回显等设置。(当然你也可以采用行数据网络传输的方式,只是要 care more ^.^)
注: Linux 系统中有 stty 命令,用于查看和更改终端行设置。stty -echo 命令会关闭回显,通常用于输入密码等场景。当然,也有相关的接口来实现屏蔽回显的功能。
2 终端操作通常是 IO 密集的,尤其是上述的单字符传输方式
上述代码中 echoData 的实现(同步阻塞 IO),最好改成 IO 复用的方式。可以使用select、poll、epoll 等框架, 监听 master fd 和 socket fd,提高 IO 效率。
3 开源组件
term.js 有完整的 web terminal 示例,同时提供了可参考的 terminal 前端库; termlib 是一个具有配色、text wrapping、远程通信等功能的Javascript库。
编程实用工具大全(前后端皆可用,不来瞅瞅?) 1.Snipaste 2.命名神器codelf 3.渐变色神器 4.CSS阴影效果神器 5.数据结构可视化 6.Buttons 7.CSS在线设计按钮 8.颜色码转换工具 9.HTML颜色代码 10.HTTP 状态代码 11.Iconfont 矢量图标库 12.JSON字符串格式化 13.数据库大全
相关文章
- socket实现一个简单的echo服务
- Netty实现丢弃服务协议(Netty4.X学习一)
- windows服务控制类
- 基于Thrift实现跨语言服务
- javaWeb服务详解(含源代码,测试通过,注释) ——Emp的Service层
- EDAS 4.0 助力企业一站式实现微服务架构转型与 K8s 容器化升级
- Asp.net(C#) windows 服务{用于实现计划任务,事件监控等}
- node.js实现国标GB28181流媒体点播(即实时预览)服务解决方案
- 微服务实现事务一致性实例
- Haproxy--脚本实现不重启haproxy服务上下线server
- 通过S3协议实现通用的文件存储服务中间件
- SAP ABAP OData 服务嵌套创建功能的实现步骤(Create Deep)试读版
- SAP ABAP OData 服务如何支持 $filter (过滤)操作试读版
- 云端的ABAP Restful服务开发
- Hybris Enterprise Commerce Platform 服务层的设计与实现
- Windows 服务卸载之后 重新安装提示 “指定的服务已标记为删除”
- Atitit 微服务 分布式 区别 微服务的判断标准 目录 1.1. 区别 微服务侧重于微小服务进程隔离级别,分布式侧重于机器隔离1 2. 微服务是一种架构, 。多微才叫微?1 2.1. 微服务
- 使用 HTTP PUT, PATCH 以及 MERGE 请求消费 SAP ABAP OData 服务修改操作的实现及其区别试读版
- SAP Gateway Service Builder 里的 OData 服务实现方式
- 家政服务小程序实战教程13-接入客服
- 【项目实战】基于Tomcat服务器实现Debug模式下服务不用重启的指引设置
- 【华为机试真题 Python实现】服务失效判断
- 两种方式实现转发代理访问内网NFS 服务—— 筑梦之路
- 33. 如何找出 SAP Fiori Launchpad 里点击 tile 之后,读取业务数据调用的是哪个 SAP 后台系统的 OData 服务
- 【微服务笔记10】微服务组件之Hystrix实现服务降级和服务熔断
- Logstash数据处理服务将采集的不同日志数据存储到不同的ES索引库(四)