馮建文 董 劍
(杭州電子科技大學 浙江 杭州 310018)
近年來,伴隨著計算機的普及和網(wǎng)絡技術(shù)的不斷發(fā)展,互聯(lián)網(wǎng)提供的服務在人們的生活中越來越不可或缺。TCP/IP協(xié)議是當前互聯(lián)網(wǎng)中最主要的通信協(xié)議標準,是國際互聯(lián)網(wǎng)絡的基礎,TCP協(xié)議是一種面向連接的、可靠的、以字節(jié)流方式進行傳輸?shù)膮f(xié)議[1]。由于面向字節(jié)流的協(xié)議是無邊界的,在傳輸過程中,不保留數(shù)據(jù)的邊界信息,這樣就可能出現(xiàn)以下問題:當發(fā)送方連續(xù)進行發(fā)送操作時,接收方在一次接收操作中,可能會同時接收到發(fā)送方多次發(fā)送的數(shù)據(jù);在接收端也可能一次無法完成所有數(shù)據(jù)的接收操作[2]。在客戶端和服務端通信時,如果數(shù)據(jù)之間沒有邊界,那么服務器端無法確定需要經(jīng)過幾次接收操作才能完成一次數(shù)據(jù)交換。所以,需要設計應用層通信協(xié)議,對面向字節(jié)流的數(shù)據(jù)進行邊界識別,來保證數(shù)據(jù)正確發(fā)送和接收。而往往在實現(xiàn)自己需要的特定功能時,對數(shù)據(jù)的安全性、靈活性等方面會有較高的要求,http、ftp、smtp等已知協(xié)議可能難以滿足需求,因此需要設計并實現(xiàn)自定義應用層協(xié)議。本文提出的自定義應用層協(xié)議的方法可適用于大部分應用程序的設計,實驗結(jié)果證明此方法可以保證數(shù)據(jù)的準確性和實時性,并且代碼靈活性高,針對性強。
網(wǎng)絡協(xié)議是為進行數(shù)據(jù)傳輸而制定的標準。發(fā)送方將特定信息封裝到請求中發(fā)送給對方;接收方接收到來自發(fā)送方的信息后,按照相應協(xié)議解析,從而獲取對方發(fā)送過來的原始信息。
通信協(xié)議包括三個要素:
(1) 語法:規(guī)定了信息的結(jié)構(gòu)和格式;
(2) 語義:表明信息要表達的內(nèi)容;
(3) 同步:規(guī)則通信內(nèi)容和通信時間。
TCP協(xié)議在不同領域的應用程序研發(fā)中被應用,當前互聯(lián)網(wǎng)上進行2臺計算機之間數(shù)據(jù)傳輸?shù)闹饕绞骄褪菓昧薚CP協(xié)議[3]。在TCP協(xié)議中,通信雙方分為客戶端和服務器端,由于TCP是面向連接的,所以作為服務器端需要等待客戶端的連接申請,連接成功后客戶端和服務器端就可以互相通信,傳輸數(shù)據(jù)??蛻舳撕头掌鞫送ㄟ^套接字(socket)這種通信機制可以在網(wǎng)絡中通信。
圖1中展示了TCP客戶端與服務器端進行通信時套接字函數(shù)的調(diào)用流程。
圖1 TCP客戶端/服務器端的套接字函數(shù)調(diào)用流程
服務器首先啟動,然后監(jiān)聽客戶的連接。當收到客戶的請求時進行判斷,如果客戶連接成功,則雙方可以進行數(shù)據(jù)的發(fā)送與接收,直到客戶關閉客戶端的連接,服務器也關閉相應的服務器端的連接,然后等待新的客戶連接。
TCP協(xié)議是以流的形式傳輸,在TCP流傳輸?shù)倪^程中,由于面向字節(jié)流的協(xié)議是沒有邊界的,可能會出現(xiàn)分包與黏包的現(xiàn)象。因此,需要自定義應用層協(xié)議對數(shù)據(jù)進行處理。
分包是指接收方只接收了部分數(shù)據(jù)包。IP分片、傳輸過程中丟失部分數(shù)據(jù)、接收緩沖區(qū)太小等都可能產(chǎn)生分包。
黏包是指發(fā)送方連續(xù)發(fā)送若干包數(shù)據(jù),接收方接收后,后一包數(shù)據(jù)的頭緊接著前一包數(shù)據(jù)的尾,無法分辨出每個數(shù)據(jù)包的界限。由于TCP協(xié)議面向連接的機制,客戶端與服務器端會維持一個連接,數(shù)據(jù)在連接不斷開的情況下,會不停地向服務器端發(fā)送數(shù)據(jù)包,可能產(chǎn)生黏包;當發(fā)送的網(wǎng)絡數(shù)據(jù)包太小時,TCP協(xié)議本身會啟用Nagle算法將多個較小的數(shù)據(jù)包合并再發(fā)送。收到數(shù)據(jù)時服務器端可能由于無法確定數(shù)據(jù)包是否是客戶端自己分開發(fā)送的而產(chǎn)生黏包。
由于遠程實驗系統(tǒng)自定義應用層協(xié)議是基于TCP的,應用層無法得知數(shù)據(jù)是否完全接收完畢,為了使接收方能正確理解發(fā)送方需要發(fā)送的數(shù)據(jù),一般有三種方法:
(1) 雙方約定一個固定的長度。發(fā)送方每次發(fā)送這一固定長度的數(shù)據(jù),接收方每次都接收這么長,就不會造成偏差。這樣完成的系統(tǒng)缺乏可擴展性和靈活性,而且會增加網(wǎng)絡的負擔,無論每次發(fā)送的有效數(shù)據(jù)是多大,都要按照定長的數(shù)據(jù)長度進行發(fā)送。
(2) 在數(shù)據(jù)的最后設置分隔符。接收方接收到分隔符就說明一次發(fā)送完成。這樣對數(shù)據(jù)內(nèi)容有要求,如果數(shù)據(jù)內(nèi)容中含有分隔符,會造成一系列的錯誤。
(3) 在每個發(fā)送操作前加上數(shù)據(jù)包的長度。使用這種方法在接收方接收數(shù)據(jù)時,收到這一長度的數(shù)據(jù)量就算是一次接收完成。但是這種方法發(fā)送一次數(shù)據(jù)需要雙方進行兩次交互,分別發(fā)送長度和數(shù)據(jù),加大了CPU的負荷,而且缺乏安全性。雖然TCP協(xié)議中有校驗和,但是不同層次的校驗覆蓋范圍不一致,因此自定義應用層協(xié)議中需要增加校驗和這一字段,進一步提高數(shù)據(jù)的完整性。
好的應用層協(xié)議一般具有以下特點:
(1) 高效。快速打包解包減少對CPU的占用。
(2) 簡單、易于人的理解。
(3) 易于擴展的。對可預知的變更,有足夠的彈性用于擴展。
(4) 容易兼容的。協(xié)議更新后,仍然可以使用新協(xié)議對舊協(xié)議發(fā)出的報文進行解析。
封包技術(shù)就是在發(fā)送時對數(shù)據(jù)包進行處理,將包處理成協(xié)議頭和包體。協(xié)議頭是大小固定的結(jié)構(gòu)體,其中有成員變量表示包體長度、包類型等,通過協(xié)議頭中的內(nèi)容可以判定接收方收到的數(shù)據(jù)包是否完整。
發(fā)送時通過封包技術(shù)將協(xié)議頭和數(shù)據(jù)內(nèi)容組成一個數(shù)據(jù)包,其中協(xié)議頭中有包類型、包長度、校驗和等。接收方先讀取協(xié)議頭,根據(jù)協(xié)議頭中的數(shù)據(jù)長度循環(huán)接收數(shù)據(jù),直到接收到的數(shù)據(jù)大小等于協(xié)議頭中的數(shù)據(jù)長度字段,此時接收完全。然后可以根據(jù)協(xié)議頭中的包類型等字段,使用相應的協(xié)議進行解包。由于TCP協(xié)議三次握手機制,可以保證數(shù)據(jù)從發(fā)送緩沖區(qū)到接收緩沖區(qū)是有序無誤的,而應用程序從緩沖區(qū)讀入的時候,無法完全保證數(shù)據(jù)安全性,所以應用上層還是要做TCP Sokcet的數(shù)據(jù)校驗。設計的通信協(xié)議如圖2所示。
圖2 通信協(xié)議設計
(1) 協(xié)議頭版本:便于后期更新、維護。
(2) 數(shù)據(jù)包類型:可以指定數(shù)據(jù)包的作用,便于解析數(shù)據(jù)部分的內(nèi)容。
(3) 數(shù)據(jù)包長度:指的是數(shù)據(jù)包的總長度。
(4) CS校驗:TCP校驗無法覆蓋到應用進程與TCP協(xié)議棧間的信息交互錯誤。遠程實驗系統(tǒng)對數(shù)據(jù)的可靠性要求較高,因此自定義應用層協(xié)議中必須包含數(shù)據(jù)的完整性校驗。
(5) 預留:預留一塊空間,便于后期增加內(nèi)容,提高協(xié)議的可擴展性和兼容性。
該自定義應用層協(xié)議工作時的處理機制如圖3所示。
圖3 自定義應用層協(xié)議服務器端數(shù)據(jù)傳輸流程圖
首先,服務器啟動,然后監(jiān)聽客戶的連接。當收到客戶端發(fā)來的connect()請求后建立連接,接著Recv()函數(shù)接收客戶端發(fā)送的數(shù)據(jù)包,先對固定協(xié)議頭大小的數(shù)據(jù)使用協(xié)議頭進行解析,然后根據(jù)協(xié)議頭中的pktType、totalLen等字段使用相應的協(xié)議進行解析,發(fā)送對應的結(jié)果,接著繼續(xù)接收下一個數(shù)據(jù)包直到收到客戶端的Close()請求關閉連接。
遠程實驗系統(tǒng)由客戶端、服務器端和ARM客戶端三個模塊組成,其整體結(jié)構(gòu)如圖4所示。
圖4 遠程實驗系統(tǒng)結(jié)構(gòu)圖
(1) PC客戶端 給用戶提供實驗接口,引導用戶進行實驗,并將實驗數(shù)據(jù)形象地展現(xiàn)給客戶。
(2) 服務器端 負責對用戶數(shù)據(jù)、實驗數(shù)據(jù)進行管理,對數(shù)據(jù)進行解析或者封裝,是PC客戶端和ARM客戶端交互的橋梁。
(3) ARM客戶端 ARM客戶端對FPGA實驗平臺進行動態(tài)配置,采集實驗數(shù)據(jù)并將數(shù)據(jù)最終傳輸?shù)娇蛻舳孙@示。
根據(jù)遠程實驗系統(tǒng)的結(jié)構(gòu),可以將協(xié)議頭部分定義為一個結(jié)構(gòu)體,數(shù)據(jù)部分定義為一個結(jié)構(gòu)體并且包含協(xié)議頭部分。不同包類型的結(jié)構(gòu)如表1所示。
表1 包類型結(jié)構(gòu)圖
協(xié)議頭設計:
typedef struct PacketHeader
{
unsigned short version;
//協(xié)議頭版本號
unsigned short pktType;
//數(shù)據(jù)包類型
unsigned int totalLen;
//數(shù)據(jù)包長度
unsigned int checkSum;
//CS校驗
char reverse[24];
//預留
}PacketHeader;
以用戶登錄數(shù)據(jù)包為例,其數(shù)據(jù)包結(jié)構(gòu)如下:
typedef struct ClientLoginPacket
{
PacketHeader header;
char userName[16];
//用戶名
char pwd[16];
//用戶密碼
}ClientLoginPacket;
以配置文件包為例,其數(shù)據(jù)包結(jié)構(gòu)如下:
typedef struct FileDataPacket
{
PacketHeader header;
char filePath[32];
//文件路徑
int fileLen;
//文件總長度
int len;
//本次發(fā)送的數(shù)據(jù)包中,數(shù)據(jù)的長度
char data[2048];
//本次發(fā)送的文件內(nèi)容
int id;
//客戶端id
} FileDataPacket;
登錄數(shù)據(jù)傳輸流程圖如圖5所示。
圖5 登錄數(shù)據(jù)傳輸流程圖
首先啟動服務器端,調(diào)用bind()和listen()這兩個函數(shù),然后等待連接。當客戶端調(diào)用connect()函數(shù)連接成功后發(fā)送數(shù)據(jù),當服務器端接收到來自客戶端的數(shù)據(jù)時,對數(shù)據(jù)進行處理,代碼如下:
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是一個緩沖區(qū),保存已收到的數(shù)據(jù)內(nèi)容,m_nRecvLen是已收到的數(shù)據(jù)長度。代碼表示收到消息后,檢測收到的數(shù)據(jù)長度是否大于一個協(xié)議頭的長度,如果小于一個協(xié)議頭的長度,那么表示數(shù)據(jù)包沒有接收完成,繼續(xù)接收,否則使用數(shù)據(jù)協(xié)議頭對數(shù)據(jù)進行解析。再檢測數(shù)據(jù)協(xié)議頭中數(shù)據(jù)長度字段的大小,如果收到的數(shù)據(jù)長度大于協(xié)議頭中數(shù)據(jù)長度字段totalLen的長度,說明登錄數(shù)據(jù)包接收完成,否則,還沒有接收完,需要繼續(xù)接收。
完全接收到數(shù)據(jù)后對數(shù)據(jù)進行處理的代碼如下:
PacketHeader*header=
(PacketHeader*)pIoContext->m_szRecvPkt;
switch (header->pktType==CLIENT_LOGIN_PACKET)
{
ClientLoginPacket*clientLoginPacket=
(ClientLoginPacket*)pIoContext->m_szRecvPkt;
}
根據(jù)數(shù)據(jù)協(xié)議頭中的數(shù)據(jù)包類型字段pktType確定數(shù)據(jù)包是登錄數(shù)據(jù)包,然后使用登錄數(shù)據(jù)包對收到的數(shù)據(jù)進行解析,然后對其數(shù)據(jù)內(nèi)容進行判斷,符合條件則登錄成功,向客戶端發(fā)送登錄成功消息,否則登錄失敗。
通過多線程的方式,啟動多個線程并發(fā)發(fā)送不同的文件,查看服務器端接收文件的情況,如表2所示,所有測試包的正確性為100%。
表2 測試結(jié)果表
表2中的登錄數(shù)據(jù)包和實驗數(shù)據(jù)包平均包過小,平均用時接近0 ms。由表2可知,對于大批量文件的傳輸,本文方法解決了由數(shù)據(jù)量過大或者網(wǎng)絡延遲過高造成的分包和黏包問題,保證了數(shù)據(jù)傳輸?shù)臏蚀_性。
通過在遠程實驗系統(tǒng)中使用改進的應用層協(xié)議,數(shù)據(jù)傳輸提高了準確性、實時性。從實驗結(jié)果可以看到,使用這種改進的應用層協(xié)議使得打包解包更加快捷、準確,減少了CPU的占用;從程序代碼來看,結(jié)構(gòu)清晰、易于理解,便于數(shù)據(jù)解析;由于數(shù)據(jù)協(xié)議頭中有版本號字段和預留字段,使得協(xié)議具有更好的擴展性和兼容性。
本文提出的改進的應用層協(xié)議的設計方法具有普遍性,對于不同情況的應用程序,經(jīng)過修改均適用。