天天看點

c語言自定義tcp協定實作socket通信

    一般的tcp協定示例,大家給出的demo都是類似一個helloworld的示例,簡單羅列了socket建立,建立連接配接,發送資料,關閉連接配接的過程,實際上tcp通信确實也就這麼多内容。但是,在實際的開發中,我們用tcp通信,肯定不會隻是發送一句簡單的“你好”。

    實際應用中,我們需要自定義一個協定,也就是protocol,然後與服務端約定網絡位元組序,最後雙方都能根據協定實作資料編碼與解碼即可。

    自定義協定,沒有固定的格式,沒有嚴格的資料類型限制,隻要雙方都認可就行了。因為通信的雙方都需要編解碼,不存在隻有一方需要編碼或者解碼,或者說我用協定發資料,你回複資料就用一個"ok"或者200就搞定了,既然是協定,也就是雙方都要遵守,tcp是雙向通信,兩邊都需要編解碼。

    這裡給出一個簡單的demo,說明tcp通信如何通過自定義協定來實作。

    自定義協定一般重點在于發送方,因為這是資料源頭,是以這裡隻給出一個client,服務端我用一個netcat的工具來模拟,這裡主要看資料的定義,以及最終形成的網絡位元組序。

    c語言預設采用的是小端序表示的網絡位元組序。這個會跟解碼有關,一般而言,我們資料類型都遵守大端序,尤其是在java語言中,位元組序預設就是大端序,這個順序跟我們的認知是相符合的,也就是高位在前,低位在後,比如0x0001,大端序就表示的是1,這也符合我們的認知習慣,尤其是對于十進制非常熟悉的人來說。

    而小端序恰好相反,比如0x0001,實際上他表示卻是0x0100,高位在後,低位在前,不符合我們的認知習慣。

    當然,雖然說大端序,小端序是相反的,但是并不是說我們在表示的時候就是相反的,剛才舉的示例都是在轉換之後的表現,而且隻有數字類型才會表現這種情況,我們通常表示數字都不會直接使用這種十六進制來表示,但是在協定的表示中,為了與位數對齊,會使用0x0001來表示short類型的1。

    定義協定格式如下所示:

字段 資料類型 備注
flag short 辨別
version short 版本
type short 類型
reserved short 保留位
length int 資料長度
payload char[] 資料體
checksum char 校驗位

    前面說過,協定沒有固定的格式,也沒有嚴格的資料類型限制,根據需要定義。但是我是基于以下幾點來做的考慮:

    1、協定最好有個明顯的起始标志

    2、一般有個操作類型,就是表示這條封包是幹什麼的,不可能什麼東西都在資料體中描述。

    3、封包定義可能為了将來的考慮,設定一個保留位,以免考慮不周,将來需要新增字段整個協定就廢了。

    4、封包中一個長度來表示資料體的長度,這個可以使用定長,但是定長多長合适也是一個問題,将來可能有更大的資料進來也說不定,設定太大了,很多時候都是預設值,增加了網絡傳輸的壓力,設定太小了,将來擴充沒法玩。

    5、資料體是真實的封包内容,加密或者不加密都可以,使用json還是其他格式也可以。

    6、校驗位,用來校驗封包的正确性,一般有意義,但是如果所有封包都有問題,那隻能說傳輸太不可靠了,網絡有問題或者程式哪裡有bug。雖說是校驗位,但是不做強制要求,本示例就沒有指派,預設0x00。

    因為在c語言中定義結構體,存在一個位對齊的問題,是以本協定前面的幾個字段都使用short,統一起來,最後一位使用char。

    上面說了這麼多,都是個人在實際中的總結,下面直接show me the code:

  main.cpp

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef struct _pktdata{
    uint16_t flag;
    uint16_t version;
    uint16_t type;
    uint16_t reserved;
    uint32_t length;
    uint16_t payload[1024];
    uint8_t checksum;
}pktdata;
void build(pktdata *data){
    data->flag = (0x5aa5);
    data->version = (0x0001);
    data->type = (0x0001);
    data->reserved = (0xffff);
    const char* payload = "{\"name\":\"buejee\",\"age\":18}";
    memcpy(data->payload,payload,strlen(payload));
    data->length = strlen(payload);
    data->checksum = (0x00);
}
int main()
{
    int sockfd,ret;
    sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == sockfd){
        printf("create socket error : (%d)\n",errno);
        return 0;
    }
    printf("socket start.\n");
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(6666);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
    if(-1 == ret){
        printf("connect error : (%d)\n",errno);
        return 0;
    }
    for(int i=0;i<10;i++){
        char sendData[1024];
        pktdata pd;
        build(&pd);
        int len = pd.length+12;
        memcpy(sendData,(pktdata *)&pd,len);
        send(sockfd,sendData,len+1,0);
        sleep(3);
    }
    close(sockfd);
    printf("done.\n");
    return 0;
}
           

    這段代碼是在linux或者mac下運作的,擴充名是cpp,其實都是c的東西,這個不重要。因為平台之間的差别太大,windows下很多api都發生了改變,包括引用的頭檔案都有很大差别。這裡千萬不要在windows下去試。

    這段代碼隻是表示了client如何自定義協定發送給服務端的,是以我們要運作,需要先開啟一個服務端。

    我這裡在mac下,可以使用netcat這個指令,也是brew install netcat安裝的,linux下面也有這個指令。

    啟動一個tcp server并開啟輸出,這裡使用輸出可以很明顯的看到最終接收的十六進制數。

netcat -lp 6666 -o bin.out
           

    接着在ide中運作上面的程式,其實這個單檔案程式可以直接g++編譯,然後運作可執行程式。

    我在程式中設定預設發送10次消息,然後停止。是以我們看服務端的輸出檔案:

Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.       
           

    這裡模拟了一個真實的自定義協定發送資料,并最終接收到資料。

    我們在實際的開發中,看到的tcp協定封包資料大部分都是這個樣子的,涉及到很多位元組轉數字,位元組轉字元串,位元組轉十六進制。 

    需要注意的是小端序,大端序。上面的程式預設傳輸的網絡位元組序數字類型就是小端序。比如flag=(0x5aa5),最終傳輸變成了A55A,version=(0x0001),傳過來就是0100,這裡特别要注意的是length這個字段,1A 00 00 00,這個很明顯就是小端序,十進制的26在正常的位元組序中,肯定是00 00 00 1A。這個數字還關系到後面取資料體的内容,是以如果端序弄反了,取到的就是一個天文數字,最終影響到協定解析。

    這裡看到的服務端接收的封包裡面正好是10條分開的封包,是因為我在send調用之後,sleep讓程式睡眠了3秒,是以接收端資料是斷斷續續的,沒有連在一起,如果不睡眠,用戶端一下子發送完成,服務端接收的就是10條全部合在一起的封包。

Received 390 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00 A5  5A 01 00 01  00 FF FF 1A  e":18}..Z.......
00000030  00 00 00 7B  22 6E 61 6D  65 22 3A 22  62 75 65 6A  ...{"name":"buej
00000040  65 65 22 2C  22 61 67 65  22 3A 31 38  7D 00 A5 5A  ee","age":18}..Z
00000050  01 00 01 00  FF FF 1A 00  00 00 7B 22  6E 61 6D 65  ..........{"name
00000060  22 3A 22 62  75 65 6A 65  65 22 2C 22  61 67 65 22  ":"buejee","age"
00000070  3A 31 38 7D  00 A5 5A 01  00 01 00 FF  FF 1A 00 00  :18}..Z.........
00000080  00 7B 22 6E  61 6D 65 22  3A 22 62 75  65 6A 65 65  .{"name":"buejee
00000090  22 2C 22 61  67 65 22 3A  31 38 7D 00  A5 5A 01 00  ","age":18}..Z..
000000A0  01 00 FF FF  1A 00 00 00  7B 22 6E 61  6D 65 22 3A  ........{"name":
000000B0  22 62 75 65  6A 65 65 22  2C 22 61 67  65 22 3A 31  "buejee","age":1
000000C0  38 7D 00 A5  5A 01 00 01  00 FF FF 1A  00 00 00 7B  8}..Z..........{
000000D0  22 6E 61 6D  65 22 3A 22  62 75 65 6A  65 65 22 2C  "name":"buejee",
000000E0  22 61 67 65  22 3A 31 38  7D 00 A5 5A  01 00 01 00  "age":18}..Z....
000000F0  FF FF 1A 00  00 00 7B 22  6E 61 6D 65  22 3A 22 62  ......{"name":"b
00000100  75 65 6A 65  65 22 2C 22  61 67 65 22  3A 31 38 7D  uejee","age":18}
00000110  00 A5 5A 01  00 01 00 FF  FF 1A 00 00  00 7B 22 6E  ..Z..........{"n
00000120  61 6D 65 22  3A 22 62 75  65 6A 65 65  22 2C 22 61  ame":"buejee","a
00000130  67 65 22 3A  31 38 7D 00  A5 5A 01 00  01 00 FF FF  ge":18}..Z......
00000140  1A 00 00 00  7B 22 6E 61  6D 65 22 3A  22 62 75 65  ....{"name":"bue
00000150  6A 65 65 22  2C 22 61 67  65 22 3A 31  38 7D 00 A5  jee","age":18}..
00000160  5A 01 00 01  00 FF FF 1A  00 00 00 7B  22 6E 61 6D  Z..........{"nam
00000170  65 22 3A 22  62 75 65 6A  65 65 22 2C  22 61 67 65  e":"buejee","age
00000180  22 3A 31 38  7D 00                                  ":18}.          
           

    這裡也引出了一個問題,就是資料傳輸 接收端存在的拆包問題。簡單粗暴的解決辦法就是間隔時間段發送,但是實際中這個辦法不管用,因為會有很多用戶端不同時間發送大量的資料,是以間隔發送不能從根本上解決拆包問題。 

繼續閱讀