1. 程式人生 > >C++中TCP/IP按約定報文協議接收資料完成拼包

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++的拼包的程式碼。希望有用哈~

時間太長,很多別的東西記不住了,就先這樣吧,我得去忙了。