一般來說,使用套接字進行網(wǎng)絡編程時,默認使用linux內(nèi)核提供的網(wǎng)絡服務。但是,現(xiàn)在我們自己在用戶空間構(gòu)建了一個tcp協(xié)議棧,并且讓它為其他應用程序提供網(wǎng)絡服務,這勢必要求我們自己實現(xiàn)一套新的套接字接口,并且提供給其他應用程序指定使用。
但是,我們并不希望把該tcp協(xié)議棧封裝成動態(tài)庫的形式,因為這樣一來,應用程序的編譯是必須要把庫一起連接進去的。那么原生網(wǎng)絡編程開發(fā)的程序就不能基于我們tcp協(xié)議棧來運行了。
一種較好的設計思路是,把tcp協(xié)議棧剝離出來作為一個獨立的組件來運行,然后通過一個中間件,把網(wǎng)絡程序與tcp協(xié)議棧協(xié)同工作起來。這個中間件的主要工作就是負責偷龍換鳳,也就是把網(wǎng)絡程序中的內(nèi)核網(wǎng)絡服務轉(zhuǎn)換成獨立運行的tcp協(xié)議棧的網(wǎng)絡服務。
核心思路:網(wǎng)絡程序(curl)+自定義套接字庫(liblevel.so)+tcp協(xié)議棧(level-ip),如下圖:
curl小工具
curl是一種命令行工具,作用是發(fā)出網(wǎng)絡請求,然后得到和提取數(shù)據(jù),顯示在"標準輸出"(stdout)上面。我們直接在curl命令后加上網(wǎng)址和端口,就可以看到網(wǎng)頁源碼。比如抓取www.sina.com網(wǎng)址:
curl www.sina.com 80
下面我們來看一個curl工具的簡易實現(xiàn),如下圖:
第3行:判斷目標主機名是否合法
第11行:判斷目標端口好是否合法
第16行:完成主機名到地址解析
第21行:使用socket申請一個套接字描述符
第23行:使用connect函數(shù)發(fā)起tcp連接
第30行:按照http 1.1協(xié)議來填充要發(fā)送的內(nèi)容,此處為http協(xié)議的get請求
第33行:調(diào)用write來發(fā)送網(wǎng)絡數(shù)據(jù)
第41行:在while循環(huán)中重復接收服務器返回來的網(wǎng)頁數(shù)據(jù),并且打印在當前控制臺終端上。
這是標準的網(wǎng)絡應用程序,使用gcc命令編譯后即可運行。
gcc curl.c -o curl
liblevelip.so庫
level-ip腳本
liblevelip.so庫重新封裝了常用的socket套接字,并借助socket原生的本地套接字接口來與tcp協(xié)議棧(level-ip)進行數(shù)據(jù)通信。以后在curl程序使用socket套接字時,優(yōu)先使用該庫的服務接口,而不是內(nèi)核的網(wǎng)絡服務。這是通過level-ip這個shell腳本完成的,具體命令如下:
./level-ip curl www.sina.com 80
我們來分析一下level-ip這個shell腳本的原理,如下圖:
第1行:執(zhí)行該shell腳本由/bin/sh程序來執(zhí)行。
第3行:指定腳本如果發(fā)生錯誤,或者遇到不存在的變量就報錯,并停止執(zhí)行。
第5行:保存腳本的第一個參數(shù)到prog變量中。
第6行:去掉一個參數(shù),即原來的1,2,依此類推。
第8行:LD_PRELOAD是一個環(huán)境變量,其指定的動態(tài)庫加載等級最高。@表示第二個參數(shù)之后的全部參數(shù)。
綜上所述,我們就可以確定curl程序是優(yōu)先加載liblevelip.so庫來使用了,通過這種打樁技術(shù),我們可以在加載階段替換部分系統(tǒng)函數(shù)的調(diào)用,比如我們常用的socket接口。
liblevelip.c文件
liblevelip.so庫由liblevellip.c文件編譯而來,該文件在tools文件夾中,我們逐步來分析一下這個c文件。
__libc_start_main函數(shù)
首先是__libc_start_main函數(shù),該函數(shù)原本是glibc庫里面的函數(shù),curl程序里面的main函數(shù)就是從這里開始被調(diào)用。但是我們在liblevelip.c里面實現(xiàn)了這個函數(shù),并且liblevelip.so的庫加載順序優(yōu)先于glibc的動態(tài)庫加載。因此在執(zhí)行curl程序中的main函數(shù)之前,此函數(shù)先被執(zhí)行。如下圖:
第3行:dlsym函數(shù)里的第一個參數(shù)為RTLD_NEXT,這意味著我們將從其他動態(tài)庫去加載__libc_start_main函數(shù)符號(比如glibc庫),然后把函數(shù)句柄賦值給__start_main變量。
第7~22行:從glibc庫中加載一部分linux系統(tǒng)原生提供的系統(tǒng)調(diào)用接口。因為我們的網(wǎng)絡服務還是要依賴于一些更底層的系統(tǒng)調(diào)用接口的。
第24行:初始化一個鏈表節(jié)點lvlip_socks。
第26行:調(diào)用glibc的原生__libc_start_main接口。
socket函數(shù)
接下來就是liblevelip.so對外提供的第一個網(wǎng)絡編程接口--socket函數(shù)了。該函數(shù)實現(xiàn)如下:
第3~5行:檢查網(wǎng)絡通信協(xié)議族是否為tcp協(xié)議,如果不是tcp協(xié)議,則調(diào)用內(nèi)核提供的網(wǎng)絡服務。
第9行:借助tcp本地套接字接口,與tcp協(xié)議棧建立連接,用于通信的本地文件為/tmp/lvlip.socket
第11行:申請一個lvlip_sock類型的buff用于管理socket信息。結(jié)構(gòu)體類型如下:
struct lvlip_sock { struct list_head list; int lvlfd; /* For Level-IP IPC */ int fd;};
list成員變量為鏈表結(jié)點
lvlfd記錄與tcp協(xié)議棧通信的網(wǎng)絡文件描述符
fd記錄tcp協(xié)議棧的返回狀態(tài)發(fā)送socket消息給tcp協(xié)議棧第12行:記錄與tcp協(xié)議棧通信的網(wǎng)絡文件描述符到sock->lvlfd第13行:把這次的網(wǎng)絡通信消息加入lvlip_socks鏈表中第14行:網(wǎng)絡通信消息數(shù)量加1第16行:獲取當前線程的pid號
第17~18行:申請ipc_msg+ipc_socket結(jié)構(gòu)體長度的buff,用于發(fā)送詳細的socket信息到tcp協(xié)議棧。結(jié)構(gòu)體定義如下:
struct ipc_msg { uint16_t type; pid_t pid; uint8_t data[];} __attribute__((packed));
type:記錄此次socket信息的具體類型
pid:記錄請求網(wǎng)絡服務的進程pid號
data:存放具體的通信內(nèi)容
struct ipc_socket { int domain; int type; int protocol;} __attribute__((packed));
實際上就是soket函數(shù)的三個參數(shù)。
第23~29行:把ipc_socket作為通信的具體內(nèi)容填充到ipc_msg的data區(qū)域中去
第31行:調(diào)用transmit_lvlip()函數(shù)真正給tcp協(xié)議棧發(fā)送消息,并且等待協(xié)議棧的數(shù)據(jù)回復。
此處我們就把liblevelip.so中的socket函數(shù)給剖析清楚了,其他諸如close、connect、write、read、send、sendto、recv、此處我們就把liblevelip.so中的socket函數(shù)給剖析清楚了,其他諸如close、connect、write、read、send、sendto、recv、此處我們就把liblevelip.so中的socket函數(shù)給剖析清楚了,其他諸如close、connect、write、read、send、sendto、recv、recvfrom、poll、select等函數(shù),原理都是一樣的,此處不再展開分析。
tcp協(xié)議棧(level-ip)
用戶空間的level-ip協(xié)議棧,在運行之初,就已經(jīng)在main函數(shù)里面創(chuàng)建了一系列線程。如下圖:
其中第9行,在run_threads()函數(shù)里創(chuàng)建了一系列線程,如下圖:
在這里,我們重點關(guān)注第5行創(chuàng)建的start_ipc_listener線程
該線程的實現(xiàn)如下:
第5行:指定tcp本地通信的路徑文件為"/tmp/lvlip.socket",與我們前面liblevelip.so庫的本地通信文件一致,這就說明它們之間確實是通過tco本地通信接口來通信的
第10行:調(diào)用socket接口開始進行tcp本地通信
第24行:調(diào)用bind函數(shù)綁定本地通信路徑
第31行:調(diào)用listen函數(shù)監(jiān)聽指定端口,等待liblevelip.so庫發(fā)起連接
第46行:如果liblevelip.so庫發(fā)起連接,則調(diào)用accept函數(shù)準備開始收發(fā)信息。
第54行:每監(jiān)聽到一個新的連接,新創(chuàng)建一個socket_ipc_open函數(shù)來進行數(shù)據(jù)的具體收發(fā)。
socket_ipc_open函數(shù)主要是負責通信信息的讀取,然后根據(jù)通信消息的類型不同,來進一步調(diào)用具體的處理函數(shù),其實現(xiàn)如下:
第7行:調(diào)用read函數(shù)進行數(shù)據(jù)的讀取
第8行:調(diào)用具體指令的回調(diào)信息
demux_ipc_socket_call的函數(shù)非常簡單,實現(xiàn)如下:
前面我們在liblevelip.so庫中調(diào)用socket()函數(shù)的時候,發(fā)送的消息類型為IPC_SOCKET,所以在此處我們進一步分析ipc_socket()這個函數(shù)。
它的具體實現(xiàn)如下:
我們重點是關(guān)注第7行的_socket函數(shù),該函數(shù)就是tcp協(xié)議棧的核心接口之一了,它是整個tcp協(xié)議棧的真正入口,我們以后再來專門分析這個接口。然后第9行,ipc_write函數(shù)負責把tcp協(xié)議棧的處理結(jié)果返回給liblevelip.so庫,代碼較為簡單,此處不再分析。
原文標題:Linux系統(tǒng)中間件的巧妙實現(xiàn)--以用戶空間的tcp協(xié)議棧為例
文章出處:【微信公眾號:FPGA之家】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
責任編輯:haq
-
Linux
+關(guān)注
關(guān)注
87文章
11456瀏覽量
212760
原文標題:Linux系統(tǒng)中間件的巧妙實現(xiàn)--以用戶空間的tcp協(xié)議棧為例
文章出處:【微信號:zhuyandz,微信公眾號:FPGA之家】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
51+單片機TCP-IP+協(xié)議棧ZLIP源碼
ID讀卡器TCP協(xié)議QT小程序開發(fā)

百問FB網(wǎng)絡編程 - 網(wǎng)絡編程簡介
飛凌嵌入式ElfBoard ELF 1板卡-網(wǎng)絡編程示例之網(wǎng)絡基礎(chǔ)知識
什么是socket編程 socket與tcp/ip協(xié)議的關(guān)系
TCP協(xié)議是什么
飛凌嵌入式ElfBoard ELF 1板卡-常見網(wǎng)絡服務搭建之SSH服務搭建
飛凌嵌入式ElfBoard ELF 1板卡-常見網(wǎng)絡服務搭建之SSH服務搭建
Linux網(wǎng)絡協(xié)議棧的實現(xiàn)

串口服務器和TCP/IP協(xié)議棧是什么關(guān)系
嵌入式學習-常見的shell命令之網(wǎng)絡相關(guān)命令
常見的shell命令之網(wǎng)絡相關(guān)命令
一文了解TCP/IP協(xié)議

評論