C++中TCP/IP按約定報文協議接收資料完成拼包
有段時間沒有更新部落格了,近來比較忙,沒有顧上寫部落格。終於完成了一個大任務,有時間回顧一下這段時間的成果。這篇部落格,先介紹和總結一下很久前的工作。TCP/IP接收資料拼包。由於時間太長很多東西記不清楚了,請見諒。
任務是某裝置通過WIFI以TCP/IP的協議傳送影象資料,資料按照規定的報文協議接收資料。
報文內容分為控制域(8個位元組)與資料域(不定長),報文的啟動字元為0628H佔兩個位元組,接下來兩個位元組是報文長度(除去控制域本身之外的所有位元組長度,因此加上啟動字元在內的完整的報文為報文長度的值+8個位元組)。控制域後面4個位元組預留。資料域前2個位元組為資料型別,接下來2個位元組為資料內容長度,接下來2個位元組為幀型別,接下來2個位元組標誌位標示是否有後續幀,然後是真正的影象資料內容(由於影象資料內容很大,一幀報文資料可能發不完,因此分多幀傳送,後續幀標誌位就標誌某一幀影象資料是否發完)。
然後約定,所有資料型別按小端對齊(低位元組在前。當然也可以約定大端對齊,高位元組在前。)
好的,協議定好,接下來就開始傳送資料和接收資料把。不過,傳送資料的工作不在我這邊,我只負責接收。不過,傳送資料的工作跟接收資料的工作可以互相參考一下。整理一下我的任務,我需要通過TCP接收發過來的資料,識別出啟動字元和報文長度,然後按報文長度的值接收報文。接收完一幀報文後,開始解包操作。由於一幀完整的影象可能分多幀報文發,所以,解包的時候需要注意是否有後續幀,資料是否完整了。
好,網上如何TCP接收資料的程式碼很多,先上程式碼。
#include <stdio.h> #include<sys/socket.h> #include<arpa/inet.h> //inet_addr #include<netdb.h> //hostent #include <sys/types.h> #include <assert.h> #include<string.h> //strcpy #include<map> #include<vector> #include<fstream> #include<unistd.h> using namespace std; #pragma pack(push, 1) static int stepSize=0; void handleDataUint(char *dataUnit, int size) { //得到資料之後,在這裡進行拼包或者進行下一步處理等操作 } int main(int argc, char **argv) { int socket_desc,rcv_size; int err=-1; socklen_t optlen; struct sockaddr_in server;//定義伺服器的相關引數 char server_reply[5000]; //Create socket //下面的AF_INET也可以用PF_INET。AF_INET主要是用於網際網路地址,而 PF_INET 是協議相關,通常是sockets和埠 socket_desc = socket(AF_INET , SOCK_STREAM , 0);//第二個引數是套介面的型別:SOCK_STREAM或SOCK_DGRAM。第三個引數設定為0。 if (socket_desc == -1) { printf("Could not create socket"); } rcv_size = 4*640000; /* 接收緩衝區大小為4*640K */ optlen = sizeof(rcv_size); err = setsockopt(socket_desc,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen);//設定套接字,返回值為-1時則設定失敗 if(err<0){ printf("設定接收緩衝區大小錯誤\n"); } server.sin_addr.s_addr = inet_addr("192.168.10.2");//伺服器IP地址 server.sin_family = AF_INET;//對應與socket,也可選PF_INET server.sin_port = htons( 52404 );//埠號 if (connect(socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0)//用建立的socket嘗試同設定好的伺服器connect { perror("connect error:"); return 1; } printf("Connected"); char recbuff[800000];//接收的資料快取大小,這是我自己設定的區域,為了存放報文 unsigned long buffsize; int packetCount = 0; buffsize=0;//該值標示當前快取資料的大小,及下一幀資料存放的地址 while (1) { int readSize = recv(socket_desc, server_reply , sizeof(server_reply) , 0);//從伺服器接收資料,其中第三個引數為單次接收的資料大小 //同上面的rcv_size區分開,上面的rcv_size是TCP/IP的機理,傳輸過程中,資料會暫時先儲存在rcv_size裡. //然後你recv再從rcv_size這個緩衝裡取你設定的sizeof(serve_reply)的資料。其中readSize為recv的返回值,該值返回你實際接收到的資料大小,這點要注意。 //接收到的資料放在server_reply[5000]裡面 if(readSize < 0) {//實際接收到的資料為負,表示接收出錯 printf("recv failed"); //should shut down connection and reconnect! assert(false); return 1; } else if (readSize == 0) {//表示無資料傳輸,接收到資料為0 printf("readSize=0"); break; } ++packetCount;//接收到包的次數加1 memcpy(recbuff + buffsize, server_reply, readSize);//memcpy為記憶體拷貝函式,將該次接收到的server_reply的資料拷貝到recbuff裡 //其中+buffsize,從recbuff頭地址+buffsize的地址開始拷貝(第一次buffsize=0,及從頭開始拷貝)拷貝的大小為readSize,即recv實際接收到的資料大小。 buffsize += readSize;//buffsize=buffsize+readSize,此時buffsize指向該次拷貝的資料大小的下一位。 const int packetHeadSize = 8;//定義控制域的資料大小為8個位元組 static int expectedPacketSize = -1;//定義期望得到的資料包大小為-1,以此判斷本次接收到的資料是否是資料頭部 if (expectedPacketSize == -1 && buffsize >= packetHeadSize) {//接收到的資料比8個位元組大,即包含了控制域及資料域,則進行下一步分析 //start token must be 0x0628, otherwise reconnect! unsigned char t0 = recbuff[0];//取出接收資料的前兩個位元組 unsigned char t1 = recbuff[1]; assert(t0 == 0x28 && t1 == 0x06);//判斷是否為0628H,是否符合啟動字元條件,注意小端對齊 if (!(t0 == 0x28 && t1 == 0x06))//如果不是0628H,則表示該次資料有誤 { return 1; } //find packet length!! unsigned short len = 0;//定義報文長度 memcpy(&len, recbuff + 2, 2);//將接收資料的下第三第四個位元組付給len,根據協議第三第四個位元組儲存的是該幀報文的長度 expectedPacketSize = len + packetHeadSize;//則期望得到的資料包大小為報文長度加控制域長度 } //get one whole packet! if (expectedPacketSize != -1 && buffsize >= expectedPacketSize) {//當期望得到的資料包大小不是-1 // 並且recbuff裡接收到的資料大小已經大於所需要的資料大小,如果接收到的資料小於完整報文的長度,則繼續接收 // //下面為接收到的完整的一幀報文,定義了一個解包函式負責解包,從快取資料的第9個位元組開始取,取完整資料域長度的資料,即只取資料域的內容 handleDataUint(recbuff + packetHeadSize, expectedPacketSize -packetHeadSize); //下面的memmove函式是記憶體移動函式,將下一幀報文移動到recbuff的起始處,覆蓋掉已經取出的資料 memmove(recbuff, recbuff + expectedPacketSize, buffsize - expectedPacketSize); buffsize -= expectedPacketSize; expectedPacketSize = -1; } } return 0; } #pragma pack(pop)
其中,涉及到快取區的資料處理,主要是memcpy,memmove等函式的使用,且buffsize,expectedPacketSize等資料大小的使用。buffsize不僅可以表示目前recbuff快取區已經存入的資料大小,而且還表徵了下一幀要存放的資料地址。而expectedPacketSize=-1可以用於判斷某次recv接收到的資料是否完畢,是否含有報文的開頭,不等於-1的時候又可以表示期望獲得的完整一幀報文的資料大小。
由於TCP/IP的限制,一幀很大的報文需要分次多次傳送,這樣就需要將多次傳送過來的包進行拼包處理,已避免粘包等情況。
以上是我用C++的拼包的程式碼。希望有用哈~
時間太長,很多別的東西記不住了,就先這樣吧,我得去忙了。