苑曉芳,劉志廣
(中國電子科技集團公司第五十四研究所,河北石家莊050081)
隨著國產Linux操作系統的不斷應用,原來在Windows平臺下的信息系統也逐漸轉向了Linux平臺。在信息系統中非常重要的一個部分就是數據的共享,該功能通常由數據流轉軟件實現,而數據傳輸部分是該軟件一個重要的底層功能支撐。根據對組件的多客戶并發應用、保證數據傳輸的可靠性和時效性的要求特點,設計實現了一個在Linux系統下基于TCP協議的傳輸組件。
傳輸組件是一個對等傳輸,所以在使用時,要先初始化本地的傳輸服務端和客戶端。
(1)初始化組件服務端
建立組件服務端的套接字,對其進行參數設置,然后將本地服務端口號綁定到套接字。
啟動監聽線程:該線程是為等待接收新客戶端的連接請求。
啟動接收線程:該線程是為接收已連接客戶端發送的數據。
(2)初始化組件客戶端
啟動發送線程:該應用線程是為處理客戶端提交發送的數據。
(1)數據發送接收的準備過程
數據流轉軟件調用組件發送接口進行數據發送。組件客戶端首先調用socket()函數創建新的通信套接字,通過connect()函數和目的地址服務端建立連接。連接建立成功后,向服務端發送本地的監聽服務端口號。客戶端將建立成功的連接套接字存放到套接字列表。建立新的發送數據結構,將待發送內容和數據類型添加到結構中,并將該數據結構添加到待發送數據列表。
服務端在監聽端口調用accept()函數成功后,返回一個新的套接字表示與客戶端的連接。調用recv()函數接收客戶端發來的服務端口號。將返回的套接字和操作實例用map表進行保存,并將返回的套接字保存到套接字列表中。
客戶端發送線程通過select()函數,遍歷發送套接字列表,查找出已提交數據的套接字,然后從線程池取出線程用于對該目的地址進行數據發送。
(2)數據發送接收的具體過程
首先從發送數據列表中取出待發送數據,將當前發送狀態置為NONE_SNDST,然后構建發送請求數據包,該數據包中包含數據請求標志、數據編號和待發送數據長度;(其中,待發送數據長度是重要的數據項,可以保證服務端接收數據時對即將接收到的數據長度的控制。)然后將發送狀態置為WAITSNDACK_SNDST。
服務端的接收線程通過使用select()函數,對多個套接字描述符進行一定時間的等待,判定描述符是否有新數據到來。當有數據到來時,通過線程池啟動的線程接收數據,對數據進行處理。先將接收狀態置為NONE_RECVST,此時接收的數據是客戶端發送的數據請求。對該請求進行處理,如果是數據發送請求,得到數據長度;如果是文件發送請求,得到文件名和文件長度。將發送狀態置為READY_RECVST,構建發送請求響應包,包中包括響應標志和發送請求中的數據編號并發送請求響應。發送成功后,將接收狀態置為RECVING_RECVST。
客戶端接收到請求響應數據包,對響應進行驗證,驗證正確后,發送狀態轉為READY_SNDST。每次讀取一定數量字節的數據,進行循環發送,直到發送數據的字節數等于待發送數據的字節數,表明此次數據完全發送結束,將發送狀態置為SNDED_SNDST。
對等服務端循環接收客戶端發送的數據。如果是內存數據,接收直到已接收數據的字節數大于等于預期的接收字節數,回調組件的接收數據接口函數,將接收到的完整數據通過回調接收函數發送給應用程序;如果是文件數據,先在當前目錄創建該文件,然后將接收到的文件數據內容寫入文件,直到寫入的字節數等于預期接收的文件字節數。將接收狀態置為RECVED_RECVST,此次數據接收完成。
對等傳輸組件的工作原理如圖1所示。

圖1 傳輸組件的工作原理
傳輸組件在客戶和服務之間交互過程的狀態控制,提高了組件傳輸的可靠性。
Socket的讀寫模型有3種:阻塞方式、非阻塞方式和 IO 多路復用[1-4]。
阻塞方式:應用進程調用read()或recv()函數時,若套接字緩沖區沒有可讀或可寫的數據時,則函數將被阻塞,進程進入睡眠等待狀態,直到有數據到來,進程才被喚醒。阻塞式IO模型的結構簡單,但是進程的效率低,因為應用進程不能及時處理多個套接字的情況。
非阻塞方式:當進程調用read()或recv()函數時,若套接字緩沖區沒有數據或有部分數據都會立即返回。非阻塞方式相比較阻塞方式雖然進程效率有些提高,但因為每個套接字都要讀取多次,所以在多個套接字的情況下,需要對每個套接字進行輪詢而占用大量的CPU時間。
IO多路復用:IO復用可以在進程睡眠的狀態下實現對多個套接字的檢測,任何一個套接字上發生事件時,由linux系統去喚醒進程,這樣就能節省大量的CPU時間。
在該傳輸組件中,客戶端的發送線程和服務端的接收線程是通過使用socket的IO多路復用模型,來實現對數據的發送和接收的。其實現方式是通過select()函數:函數定義形式為:
Int select(int maxfd,fd_set*read_set,fd_set*write_set,fd_set except_set,const struct timeval*timeout);
其中,maxfd是需要檢測的文件描述符最大值加 1,read_set、write_set和 except_set參數分別對應于需要檢測的可讀描述符集,可寫描述符集和異常描述符集,timeout是select函數的最大等待時間。
以發送端的發送線程為例:循環發送套接字列表,并依次調用FD_SET(*iterSock,&sockSet)函數將每個套接字描述符*iterSock在sockSet中的相應位開放,并通過

}來記錄maxsock的最大值。調用INT iRes=select(maxsock,NULL,&sockSet,NULL,&tval),如果iRes>0,則在 sockSet集合中有一個或多個 socket可寫數據。再次循環socket發送列表,并通過FD_ISSET((*iterSock),&sockSet))函數確定某個socket達到了寫條件,然后對該socket中的數據進行發送操作。
Socket的IO多路復用綜合了阻塞式與非阻塞式輸入輸出的優點,提高了傳輸組件的進程效率,增加了數據處理的時效性。
傳輸組件的客戶端會向多個不同地址的客戶端發送數據,服務端也會接收多個不同地址客戶端發來的數據。在這種多用戶并發的情況下,為了能夠及時響應并處理多個不同端地址的數據,需要使用多個線程。傳統的多線程服務模型是當新數據請求到來時,就創建新線程,線程執行完任務后退出。盡管創建和銷毀線程是輕量級的,但過于頻繁的話,就會給服務造成很大的負擔[5-7]。為了解決上述問題,傳輸組件實現并使用了自適應的線程池。
線程池可以統一任務接口,通過線程復用將創建和銷毀線程的開銷分攤到多個任務上。線程池的實現包括4部分:線程池管理器、工作線程、任務接口和任務隊列。
線程池管理器:用于創建和管理池中的工作線程,并將工作任務按需分配到不同的線程中;
工作線程:執行實際任務的線程;
任務接口:執行任務函數和參數的地址;
隊列:保存待執行任務的列表和工作線程的列表。
以客戶端發送數據為例:當向新的客戶端發送數據時,發送客戶端將發送任務添加到線程池管理器的任務隊列,線程池管理器找出空閑線程后,將該任務分配到線程中去執行。需要注意的是,為了保證數據的按序發送,需要對添加的每個任務設置是否完成標志,來保證每條數據按序正常的發送。以客戶端為例,傳輸組件通過線程池執行任務的代碼如下:

TCP是面向連接的并且提供了一個可靠的字節流信道,數據流可以通過這個信道在兩個終端系統之間流動。由于TCP協議的傳輸特點,為了提高效率,TCP在發送方會將多個待發送的數據包一起發送。發送方發送的若干包數據到達接收方接收時粘成一包,即多個數據包首尾相連,稱為粘包[8,9]。
通常解決粘包現象的方法是:接收方創建一個預處理線程,對接收到的數據包進行預處理,將粘包分開。
本傳輸組件因為是對等傳輸,所以可以通過在客戶端和服務端之間的傳輸過程中加入控制機制,來避免對粘包的單獨處理。采取的措施如下:
①在每一次數據傳輸之前,都會先進行一個發送數據請求和返回請求響應的交互過程。該交互過程主要是在發送方和接收方之間確定即將傳輸數據的大小,這樣接收方就可以在數據傳送之前確定本次應該接收的數據的確切大小;
②第1次數據傳輸結束之后,會將發送狀態置為發送完畢,第2條數據就會開始發送數據請求。而由于TCP的傳輸機制,會將第1條的全部或部分數據和第2條的數據請求包粘在一起進行發送。因為發送方對于第2條數據沒有收到請求響應之前,不會發送真正數據內容,所以粘在后面的數據部分只可能是第2個數據包的數據請求。接收方先基于第1條數據請求中攜帶的數據長度,從粘包數據中截取真正的數據內容,然后將剩下的數據作為第2包的數據請求報進行驗證處理。
這種處理TCP粘包的方式比連續接收數據,并啟動線程進行單獨處理粘包數據的方式效率更高。
傳輸組件為了使上層應用能夠更靈活地進行數據傳輸和得到更多的數據傳輸信息,組件提供了較為豐富的傳輸接口和回調接口[10-12]。
傳輸接口:提供了對數據和文件的異步和同步發送。如異步發送數據接口:
virtual LONG SendData(const BYTE* pBuf,const ULONG ulSndLen,const PeerAddress& peeraddr,const ULONG uiBlockNotifySize,UINT & uiSnd-Num)=0;
其中,const BYTE*pBuf為指向待發送數據的指針;const ULONG ulSndLen為待發送數據長度;const PeerAddress&peeraddr為待發送數據的目的地址,其中PeerAddress為自定義的地址結構,包括端口號和IP地址等;const ULONG uiBlockNotifySize是設置已傳輸數據多大時通知給應用;UINT&uiSnd-Num是數據編號,可以為空。組件還提供了傳輸文件的異步接口SendFile,可同時發送數據和文件的異步接口SendDataAndFiles。
回調接口:組件通過回調接口可以接收文件或數據,還可以接收在傳輸過程中接收數據或傳輸數據的大小等。如接收數據回調接口:
virtual LONG OnRecvData(const BYTE*pData,const ULONG ulLen,const DataTime& tmDataRecv,const PeerAddress& peeraddr)=0;//接收數據
其中,const BYTE*pData為傳入的接收到的數據指針,const ULONG ulLen是接收到的數據長度,const DataTime&tmDataRecv是數據接收的時間,const PeerAddress&peeraddr是數據的發送地址。
組件還提供了接收文件的回調接口OnRecv-File,當前接收數據大小的回調接口OnRecvingData-Size,當前發送文件數據的大小 OnFileTransSize,發送結果OnSendResult。還有連接斷開OnDisconnect函數,OnConnError連接錯誤等函數。通過這些回調函數接口,應用進程可以獲得更多的傳輸狀態,發送結果等信息。
傳輸組件的功能是為信息系統中的數據流轉軟件提供及時和可靠的數據傳輸,并且在多用戶并發的情況下也能夠滿足。針對這一特性,在測試階段為傳輸組件設計了多種性能測試。測試內容主要是針對多用戶并發、大數據塊和大數據量等不同條件下,組件傳輸數據的時效性和可靠性。經過大量的測試實驗后,表明該傳輸組件可以為數據流轉軟件提供及時和可靠的傳輸服務。
Linux下基于TCP的傳輸組件,可以為數據流轉軟件和其他上層應用軟件提供底層的數據和文件傳輸,是信息系統中重要支撐功能;同時,數據傳輸的穩定性、可靠性和及時性,更是上層應用所關心的,所以性能成為了評價傳輸組件優劣非常關鍵的因素。
本文中所探討的組件實現通過在組件中應用多種技術,提高了組件的傳輸性能。首先在TCP傳輸協議上增加自控機制提高了數據傳輸的穩定性和可靠性,也提高了粘包數據的處理效率;采用socket的多路復用與線程池技術,提高了組件傳輸數據的及時性;豐富的傳輸接口和回調接口可以滿足不同上層應用軟件的要求,也提供了更全面的傳輸信息。
[1] 天夜創作室.Linux網絡編程技術[M].北京:人民郵電出版社,2001.
[2] 宋敬彬,孫海濱.Linux網絡編程[M].北京:清華大學出版社,2010.
[3] 張斌.Linux網絡編程[M].北京:清華大學出版社,2000.
[4] 陳娟,趙振平.TCP/IP高效編程[M].北京:人民郵電出版社,2011.
[5] 王雷,王子淘.基于Linux的Socket網絡編程的性能優化[J].電子設計工程,2009,17(9):101 -103.
[6] 唐富強,于鴻洋,張萍.Linux下通用線程池的改進與實現[J].計算機工程與應用,2012 48(28):77-83.
[7] 郭東升,田秀華.Linux環境下基于socket的網絡通信[J].軟件導刊,2009,8(1):116 -118.
[8] 李慧霸,田甜,彭宇行,等.網絡程序設計中的并發復雜性[J].軟件學報,2011,22(1):132 -148.
[9] 劉新強,曾兵義.用線程池解決服務器并發請求的方案設計[J].現代電子技術,2011,34(15):141 -143.
[10]崔弘珂.一種空間環境下的TCP傳輸技術研究[J].無線電通信技術,2011,37(4):21 -24.
[11]宋廣怡.超寬帶高速數據傳輸技術研究[J].無線電工程,2014,44(5):23 -25
[12]李志高,周音,孫學斌,等.認知無線電網絡中一種改進的傳輸層協議[J].無線電工程,2011,41(10):1 -3.