遠端終端服務的簡單實現
大家可能見過類似這樣嵌入到網頁中的終端,可以在頁面上與遠端伺服器互動,就像 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庫。
相關文章
- 《遠端控制》-服務端實現(一)服務端
- Go實現ssh執行遠端命令及遠端終端Go
- 如何在命令列下遠端安裝終端服務命令列
- bbossaop遠端服務介紹-遠端服務呼叫例項
- php+nginx實現最簡單的遠端呼叫rpc(微服務)PHPNginxRPC微服務
- Java review--NIO例項:實現服務端和客戶端的簡單通訊JavaView服務端客戶端
- 利用tirpc庫實現簡單的客戶端和服務端RPC客戶端服務端
- bbossaop遠端服務介紹-遠端服務id定義規則
- 01 . Go語言實現SSH遠端終端及WebSocketGoWeb
- 實現服務端和客戶端的實時雙向資料傳輸-WebSocket簡單瞭解服務端客戶端Web
- 如此簡單遠端漏洞掃描實現雲安全
- 實現SSR服務端渲染服務端
- spring 的遠端服務是?Spring
- 基於 WebSocket 的 PPT 遠端控制器簡單實現Web
- TCP通訊客戶端和服務端簡單程式碼實現TCP客戶端服務端
- Socket最簡單的客戶端與服務端通訊-Java客戶端服務端Java
- bbossaop遠端服務介紹-點對點遠端服務呼叫和組播服務呼叫的區別
- golang實現tcp客戶端服務端程式GolangTCP客戶端服務端
- React 服務端渲染實現 Gank 移動端React服務端
- 本地除錯遠端服務除錯
- Modbus RTU(Remote Terminal Unit 遠端終端單元)REM
- 實現客戶端與服務端的HTTP通訊客戶端服務端HTTP
- SHA-256加密簡單例項(客戶端、服務端)加密單例客戶端服務端
- windows socket簡單使用--實現客戶端連結服務端併傳送和接收資料Windows客戶端服務端
- 實現ssr服務端渲染demo服務端
- Android實現Thrift服務端與客戶端Android服務端客戶端
- [譯]React在服務端渲染的實現React服務端
- [譯] React 在服務端渲染的實現React服務端
- dubbo 遠端服務無法呼叫
- Windows遠端連線Docker服務WindowsDocker
- Java的oauth2.0 服務端與客戶端的實現JavaOAuth服務端客戶端
- C# 之 服務端獲取遠端資源C#服務端
- NAS教程丨如何透過DDNS實現SMB服務的遠端訪問?DNS
- React + Koa 實現服務端渲染(SSR)React服務端
- 簡單簡易實現伺服器遠端登陸傳送簡訊提示伺服器
- 關於Vue服務端渲染(nuxt)的簡單學習Vue服務端UX
- socket實現簡單ssh服務
- spring cloud feign實現遠端呼叫服務傳輸檔案SpringCloud