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