远程终端服务的简单实现

大家可能见过类似这样嵌入到网页中的终端,可以在页面上与远程服务器交互,就像 ssh 到远程服务器一样。实现这样一个基于 web 的终端,具有跨平台、易审计、限制用户行为等优点。

本文将介绍如何构建一个最简单的 web 远程终端服务程序。

1. 基本概念

首先明确几个相关概念:

终端

终端是一种字符型输入输出设备,通过它用户才能与计算机进行 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,规范终端行输入。; 而“主设备”与管道文件类似,可以进行读写操作。往“主设备”写入的数据会传输到“从设备”,而“从设备”从系统获取到的数据也会同样的传输到“主设备”。因此,也可以说,伪终端是一个双向管道。

2. 构建远程终端服务

上面已经介绍过,想要与系统进行交互,除了有终端设备,还需要 shell 程序。两者结合才能完成用户的指令。

因此,一个远程终端服务程序由两个部分构成:伪终端和 shell 进程。通常构建如下:

  • 1 创建伪终端设备。
  • 2 fork 创建子进程,并将该子进程的标准输入、输出和错误输出均 dup 为伪终端的"从设备"。
  • 3 在子进程中 exec 执行 /bin/bash 命令,启动 shell 进程。由于上一步的操作,该子进程(也就是 shell 进程)的 stdin、stdout 和 stderr 已与伪终端进行了绑定。如此,shell 子进程的输出、输出、错误输出均是通过伪终端的“从设备”进行的。

经过上述操作,可以说这个子进程就是一个“终端进程“了:既能够完成终端的输入输出操作,又能解释执行用户输入与系统内核交互。

由于伪终端“双向管道”的特性:对伪终端“主设备”的写操作,将传输到“从设备”,也就是传输给”终端进程“;而”终端进程“执行命令后的输出,将通过“从设备”传输返回至“主设备”。如此一来,对 ”终端进程“ 的 IO 操作完全可以通过操作伪终端的“主设备”来完成。

对“主设备”进行读写操作,就等同于在对一个终端 shell 进行操作。因此,如果在父进程中将该伪终端“主设备”与网络 socket 绑定,就能够实现远程终端操作了。(当然也可以将该“主设备”与其他文件描述符绑定,例如与另一进程通信的管道 fd 绑定等等,这些就取决于功能需求了)

数据传输可见下图:

3. 代码实现

下面给出实现一个 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) > 0);
    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;
}

4. Tips

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库。
时间: 2024-10-24 20:17:34

远程终端服务的简单实现的相关文章

WebSphere Integration Developer指导教程 第3部分 构建面向服务的简单应用程

WebSphere Integration Developer指导教程 第3部分 构建面向服务的简单应用程序 引言 本系列的前一篇文章讨论了 WebSphere Integration Developer 的基本概念及其提供的一套工具.在该文中,我们还了解了使用面向服务的体系结构构建订单处理应用程序 (OrderProcessing) 的主要步骤.您可能此时尚未读过上一篇文章,或者已经读过了,但却记不清所有细节.不用担心,在深入了解如何实现应用程序的服务前,我们将简要进行一下回顾,从而为进行一步

golang(4):编写socket服务,简单支持命令

本文的原文连接是: http://blog.csdn.net/freewebsys/article/details/46881213 转载请必须注明出处! 1,socket服务 使用golang开发socket服务还是非常简单的. socket的库都封装好了. 参考文档: https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/08.1.md 2,简单例子 package main import (

一起谈.NET技术,使用WCF实现SOA面向服务编程——简单的WCF开发实例

前面为大家介绍过WCF的特点,现在再讲解一下WCF基础概念. 在WCF里,各个Application之间的通信是由EndPoint来实现的,EndPoint是WCF实现通信的核心要素.一个WCF Service可由多个EndPoint集合组成,每个EndPoint只能有一种绑定,就是说EndPoint就是通信的入口,客户端和服务端通过 EndPoint交换信息. <service name = " " >< endpoint address = ""

使用W“.NET技术”CF实现SOA面向服务编程——简单的WCF开发实例

前面为大家介绍过WCF的特点,现在再讲解一下WCF基础概念. 在WCF里,各个Application之间的通信是由EndPoint来实现的,EndPoint是WCF实现通信的核心要素.一个WCF Service可由多个EndPoint集合组成,每个EndPoint只能有一种绑定,就是说EndPoint就是通信的入口,客户端和服务端通过 EndPoint交换信息. <service name = " " >< endpoint address = ""

使用WC“.NET研究”F实现SOA面向服务编程——简单的WCF开发实例

前面为大家介绍过WCF的特点,现在再讲解一下WCF基础概念. 在WCF里,各个Application之间的通信是由EndPoint来实现的,EndPoint是WCF实现通信的核心要素.一个WCF Service可由多个EndPoint集合组成,每个EndPoint只能有一种绑定,就是说EndPoint就是通信的入口,客户端和服务端通过 EndPoint交换信息. <service name = " " >< endpoint address = ""

构建插件式的应用程序框架(八)-视图服务的简单实现

我在前一篇文章里提到,对于停靠工具栏或者是视图最好是不要将实例放到词典中,而是将工具栏或者视图的类型放到词典中,因为视图类型会经常的被重用,并且会经常被关闭或者再打开.当实例被关闭后,资源就被释放了,对于实例的管理就会比较麻烦,所以我们分为两步走.在插件被加载的时候,我们只注册类型,在应用程序运行的时候,我们通过某种途径来实例化他. 我修改的以前的例子,主要突出本次演示的功能.这次的例子实现的功能是通过插件扩展应用程序处理不同文件的能力.在原始的应用程序中,我们可以通过File菜单的Open,只

远程终端3389管理及安全防范技术_病毒查杀

远程终端服务是微软Windows Server系列中的一大特色,由于其简洁.维护及使用方便等特点深受广大用户及其黑客的喜爱,而远程终端服务往往运行在一些有重要程序的服务器上;如果由于远程终端服务的配置和管理不当,往往会带来巨大的经济损失. 一. 远程终端服务技术介绍 Windows 2003 Server中的Windows Terminal Services(WTS)又称为远程终端服务(Remote Terminal Services)或者俗称为3389,是在Windows NT中最先使用的一种

nfs服务简单配置手记

原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://koumm.blog.51cto.com/703525/1581693 nfs服务在实际工作中配置十分频繁,这里简要记录下配置过程.默认情况下nfs服务相关软件包已经安装好,只是nfs服务不是开机自启动.只需要配置nfs服务器开机自启动即可.其它portmap等服务都是开机自启的. 1. 服务端简单要配置过程如下 (1) 启动nfs服务 # chkconfig nfs on   

[翻译]利用C#获取终端服务(Terminal Services)会话的闲置时间

[翻译]利用C#获取终端服务(Terminal Services)会话的闲置时间 作者:Tuuzed(土仔)   发表于:2008年2月29日版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明.http://www.cppblog.com/tuuzed/archive/2008/02/29/43424.html    原著:Guy Teverovsky 翻译:土仔Tuuzed原文出处:Querying TS session idle time with C#原文U