馮建文 董 劍
(杭州電子科技大學 浙江 杭州 310018)
近年來,伴隨著計算機的普及和網絡技術的不斷發展,互聯網提供的服務在人們的生活中越來越不可或缺。TCP/IP協議是當前互聯網中最主要的通信協議標準,是國際互聯網絡的基礎,TCP協議是一種面向連接的、可靠的、以字節流方式進行傳輸的協議[1]。由于面向字節流的協議是無邊界的,在傳輸過程中,不保留數據的邊界信息,這樣就可能出現以下問題:當發送方連續進行發送操作時,接收方在一次接收操作中,可能會同時接收到發送方多次發送的數據;在接收端也可能一次無法完成所有數據的接收操作[2]。在客戶端和服務端通信時,如果數據之間沒有邊界,那么服務器端無法確定需要經過幾次接收操作才能完成一次數據交換。所以,需要設計應用層通信協議,對面向字節流的數據進行邊界識別,來保證數據正確發送和接收。而往往在實現自己需要的特定功能時,對數據的安全性、靈活性等方面會有較高的要求,http、ftp、smtp等已知協議可能難以滿足需求,因此需要設計并實現自定義應用層協議。本文提出的自定義應用層協議的方法可適用于大部分應用程序的設計,實驗結果證明此方法可以保證數據的準確性和實時性,并且代碼靈活性高,針對性強。
網絡協議是為進行數據傳輸而制定的標準。發送方將特定信息封裝到請求中發送給對方;接收方接收到來自發送方的信息后,按照相應協議解析,從而獲取對方發送過來的原始信息。
通信協議包括三個要素:
(1) 語法:規定了信息的結構和格式;
(2) 語義:表明信息要表達的內容;
(3) 同步:規則通信內容和通信時間。
TCP協議在不同領域的應用程序研發中被應用,當前互聯網上進行2臺計算機之間數據傳輸的主要方式就是應用了TCP協議[3]。在TCP協議中,通信雙方分為客戶端和服務器端,由于TCP是面向連接的,所以作為服務器端需要等待客戶端的連接申請,連接成功后客戶端和服務器端就可以互相通信,傳輸數據。客戶端和服務器端通過套接字(socket)這種通信機制可以在網絡中通信。
圖1中展示了TCP客戶端與服務器端進行通信時套接字函數的調用流程。

圖1 TCP客戶端/服務器端的套接字函數調用流程
服務器首先啟動,然后監聽客戶的連接。當收到客戶的請求時進行判斷,如果客戶連接成功,則雙方可以進行數據的發送與接收,直到客戶關閉客戶端的連接,服務器也關閉相應的服務器端的連接,然后等待新的客戶連接。
TCP協議是以流的形式傳輸,在TCP流傳輸的過程中,由于面向字節流的協議是沒有邊界的,可能會出現分包與黏包的現象。因此,需要自定義應用層協議對數據進行處理。
分包是指接收方只接收了部分數據包。IP分片、傳輸過程中丟失部分數據、接收緩沖區太小等都可能產生分包。
黏包是指發送方連續發送若干包數據,接收方接收后,后一包數據的頭緊接著前一包數據的尾,無法分辨出每個數據包的界限。由于TCP協議面向連接的機制,客戶端與服務器端會維持一個連接,數據在連接不斷開的情況下,會不停地向服務器端發送數據包,可能產生黏包;當發送的網絡數據包太小時,TCP協議本身會啟用Nagle算法將多個較小的數據包合并再發送。收到數據時服務器端可能由于無法確定數據包是否是客戶端自己分開發送的而產生黏包。
由于遠程實驗系統自定義應用層協議是基于TCP的,應用層無法得知數據是否完全接收完畢,為了使接收方能正確理解發送方需要發送的數據,一般有三種方法:
(1) 雙方約定一個固定的長度。發送方每次發送這一固定長度的數據,接收方每次都接收這么長,就不會造成偏差。這樣完成的系統缺乏可擴展性和靈活性,而且會增加網絡的負擔,無論每次發送的有效數據是多大,都要按照定長的數據長度進行發送。
(2) 在數據的最后設置分隔符。接收方接收到分隔符就說明一次發送完成。這樣對數據內容有要求,如果數據內容中含有分隔符,會造成一系列的錯誤。
(3) 在每個發送操作前加上數據包的長度。使用這種方法在接收方接收數據時,收到這一長度的數據量就算是一次接收完成。但是這種方法發送一次數據需要雙方進行兩次交互,分別發送長度和數據,加大了CPU的負荷,而且缺乏安全性。雖然TCP協議中有校驗和,但是不同層次的校驗覆蓋范圍不一致,因此自定義應用層協議中需要增加校驗和這一字段,進一步提高數據的完整性。
好的應用層協議一般具有以下特點:
(1) 高效。快速打包解包減少對CPU的占用。
(2) 簡單、易于人的理解。
(3) 易于擴展的。對可預知的變更,有足夠的彈性用于擴展。
(4) 容易兼容的。協議更新后,仍然可以使用新協議對舊協議發出的報文進行解析。
封包技術就是在發送時對數據包進行處理,將包處理成協議頭和包體。協議頭是大小固定的結構體,其中有成員變量表示包體長度、包類型等,通過協議頭中的內容可以判定接收方收到的數據包是否完整。
發送時通過封包技術將協議頭和數據內容組成一個數據包,其中協議頭中有包類型、包長度、校驗和等。接收方先讀取協議頭,根據協議頭中的數據長度循環接收數據,直到接收到的數據大小等于協議頭中的數據長度字段,此時接收完全。然后可以根據協議頭中的包類型等字段,使用相應的協議進行解包。由于TCP協議三次握手機制,可以保證數據從發送緩沖區到接收緩沖區是有序無誤的,而應用程序從緩沖區讀入的時候,無法完全保證數據安全性,所以應用上層還是要做TCP Sokcet的數據校驗。設計的通信協議如圖2所示。

圖2 通信協議設計
(1) 協議頭版本:便于后期更新、維護。
(2) 數據包類型:可以指定數據包的作用,便于解析數據部分的內容。
(3) 數據包長度:指的是數據包的總長度。
(4) CS校驗:TCP校驗無法覆蓋到應用進程與TCP協議棧間的信息交互錯誤。遠程實驗系統對數據的可靠性要求較高,因此自定義應用層協議中必須包含數據的完整性校驗。
(5) 預留:預留一塊空間,便于后期增加內容,提高協議的可擴展性和兼容性。
該自定義應用層協議工作時的處理機制如圖3所示。

圖3 自定義應用層協議服務器端數據傳輸流程圖
首先,服務器啟動,然后監聽客戶的連接。當收到客戶端發來的connect()請求后建立連接,接著Recv()函數接收客戶端發送的數據包,先對固定協議頭大小的數據使用協議頭進行解析,然后根據協議頭中的pktType、totalLen等字段使用相應的協議進行解析,發送對應的結果,接著繼續接收下一個數據包直到收到客戶端的Close()請求關閉連接。
遠程實驗系統由客戶端、服務器端和ARM客戶端三個模塊組成,其整體結構如圖4所示。

圖4 遠程實驗系統結構圖
(1) PC客戶端 給用戶提供實驗接口,引導用戶進行實驗,并將實驗數據形象地展現給客戶。
(2) 服務器端 負責對用戶數據、實驗數據進行管理,對數據進行解析或者封裝,是PC客戶端和ARM客戶端交互的橋梁。
(3) ARM客戶端 ARM客戶端對FPGA實驗平臺進行動態配置,采集實驗數據并將數據最終傳輸到客戶端顯示。
根據遠程實驗系統的結構,可以將協議頭部分定義為一個結構體,數據部分定義為一個結構體并且包含協議頭部分。不同包類型的結構如表1所示。

表1 包類型結構圖
協議頭設計:
typedef struct PacketHeader
{
unsigned short version;
//協議頭版本號
unsigned short pktType;
//數據包類型
unsigned int totalLen;
//數據包長度
unsigned int checkSum;
//CS校驗
char reverse[24];
//預留
}PacketHeader;
以用戶登錄數據包為例,其數據包結構如下:
typedef struct ClientLoginPacket
{
PacketHeader header;
char userName[16];
//用戶名
char pwd[16];
//用戶密碼
}ClientLoginPacket;
以配置文件包為例,其數據包結構如下:
typedef struct FileDataPacket
{
PacketHeader header;
char filePath[32];
//文件路徑
int fileLen;
//文件總長度
int len;
//本次發送的數據包中,數據的長度
char data[2048];
//本次發送的文件內容
int id;
//客戶端id
} FileDataPacket;
登錄數據傳輸流程圖如圖5所示。

圖5 登錄數據傳輸流程圖
首先啟動服務器端,調用bind()和listen()這兩個函數,然后等待連接。當客戶端調用connect()函數連接成功后發送數據,當服務器端接收到來自客戶端的數據時,對數據進行處理,代碼如下:
while(pIoContext->m_nRecvLen>=PKT_HEADER_LEN)
{
PacketHeader *header
=(PacketHeader*)pIoContext->m_szRecvPkt;
if(pIoContext->m_nRecvLen >=header->totalLen)
{
pIOCPModel->_DoRecv(pHandleContext, pIoContext);
memcpy(pIoContext->m_szRecvPkt,
pIoContext->m_szRecvPkt+header->totalLen,
pIoContext->m_nRecvLen-header->totalLen);
pIoContext->m_nRecvLen-=header->totalLen;
}
else
break;
}
其中m_szRecvPkt是一個緩沖區,保存已收到的數據內容,m_nRecvLen是已收到的數據長度。代碼表示收到消息后,檢測收到的數據長度是否大于一個協議頭的長度,如果小于一個協議頭的長度,那么表示數據包沒有接收完成,繼續接收,否則使用數據協議頭對數據進行解析。再檢測數據協議頭中數據長度字段的大小,如果收到的數據長度大于協議頭中數據長度字段totalLen的長度,說明登錄數據包接收完成,否則,還沒有接收完,需要繼續接收。
完全接收到數據后對數據進行處理的代碼如下:
PacketHeader*header=
(PacketHeader*)pIoContext->m_szRecvPkt;
switch (header->pktType==CLIENT_LOGIN_PACKET)
{
ClientLoginPacket*clientLoginPacket=
(ClientLoginPacket*)pIoContext->m_szRecvPkt;
}
根據數據協議頭中的數據包類型字段pktType確定數據包是登錄數據包,然后使用登錄數據包對收到的數據進行解析,然后對其數據內容進行判斷,符合條件則登錄成功,向客戶端發送登錄成功消息,否則登錄失敗。
通過多線程的方式,啟動多個線程并發發送不同的文件,查看服務器端接收文件的情況,如表2所示,所有測試包的正確性為100%。

表2 測試結果表
表2中的登錄數據包和實驗數據包平均包過小,平均用時接近0 ms。由表2可知,對于大批量文件的傳輸,本文方法解決了由數據量過大或者網絡延遲過高造成的分包和黏包問題,保證了數據傳輸的準確性。
通過在遠程實驗系統中使用改進的應用層協議,數據傳輸提高了準確性、實時性。從實驗結果可以看到,使用這種改進的應用層協議使得打包解包更加快捷、準確,減少了CPU的占用;從程序代碼來看,結構清晰、易于理解,便于數據解析;由于數據協議頭中有版本號字段和預留字段,使得協議具有更好的擴展性和兼容性。
本文提出的改進的應用層協議的設計方法具有普遍性,對于不同情況的應用程序,經過修改均適用。