socket網絡編程(五)——粘包拆包問題

今天和你們講一下socket網絡編程中粘包和拆包的問題。編程

一、出現粘包拆包的緣由

假設一個這樣的場景,客戶端要利用send()函數發送字符「asd」到服務端,連續發送3次,可是服務端休眠10秒以後再去緩衝池中接收。那麼請問10秒以後服務端從緩衝區接收到的信息是「asd」仍是「asdasdasd」呢?若是你們有去作實驗的話,能夠知道服務端收到的是「asdasdasd」,爲何會這樣呢?按正常的話,服務端收到的應該是「asd」,剩下的兩個asd要不就是收不到要不就是下次循環收到,怎麼會一次性收到「asdasdasd」呢?若是要說罪魁禍首的話就是那個休眠10秒,致使數據粘包了!緩存

服務端代碼:服務器

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
 
int main()
{
    //建立套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //初始化socket元素
    struct sockaddr_in server_addr;
    int server_len = sizeof(server_addr);
    memset(&server_addr, 0, server_len);
 
    server_addr.sin_family = AF_INET;
    //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用這個寫法也能夠
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(39002);
 
    //綁定文件描述符和服務器的ip和端口號
    int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len);
    if (m_bindfd < 0)
    {
        ERR_EXIT("bind ip and port fail");
    }
 
    //進入監聽狀態,等待用戶發起請求
    int m_listenfd = listen(m_sockfd, 20);
    if (m_listenfd < 0)
    {
        ERR_EXIT("listen client fail");
    }
 
    //定義客戶端的套接字,這裏返回一個新的套接字,後面通訊時,就用這個m_connfd進行通訊
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
    
  //這裏休眠了10秒
  sleep(10);
 
    //接收客戶端數據
    char buffer[BUF_SIZE];
    recv(m_connfd, buffer, sizeof(buffer)-1, 0);
    printf("server recv:%s\n", buffer);
    strcat(buffer, "+ACK");
    send(m_connfd, buffer, strlen(buffer), 0);
 
    //關閉套接字
    close(m_connfd);
    close(m_sockfd);
 
    return 0;
}

 

客戶端代碼:網絡

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
 
int main()
{
    //建立套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //服務器的ip爲本地,端口號
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("81.68.140.74");
    server_addr.sin_port = htons(39002);
 
    //向服務器發送鏈接請求
    int m_connectfd = connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (m_connectfd < 0)
    {
        ERR_EXIT("connect server fail");
    }
    //發送並接收數據
    char buffer[BUF_SIZE] = "asd";
    int datasize = strlen(buffer);
    send(m_sockfd, buffer, datasize, 0);
    send(m_sockfd, buffer, datasize, 0);
    send(m_sockfd, buffer, datasize, 0);
    recv(m_sockfd, buffer, sizeof(buffer)-1, 0);
    printf("client recv:%s\n", buffer);
 
    //斷開鏈接
    close(m_sockfd);
 
    return 0;
}

 

以上代碼在Linux平臺上運行以後就會出現粘包現象,你們能夠把以上代碼複製去驗證看看。socket

二、粘包拆包的幾種狀況

這個問題在socket網絡編程中很是的常見,數據不只會粘包,還會被拆包,就是一段數據被拆成兩部分。那麼拆包、粘包問題產生的緣由都有哪些呢函數

  • 要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包。
  • 待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。
  • 要發送的數據小於TCP發送緩衝區的大小,TCP將屢次寫入緩衝區的數據一次發送出去,將會發生粘包。
  • 接收數據端的應用層沒有及時讀取接收緩衝區中的數據,將發生粘包。

而數據之因此會發送粘包拆包的根本緣由是TCP的數據包是流的方式傳輸的,就像水流同樣,沒有一個分界的東西。spa

 

三、處理粘包拆包的方法

處理拆包、粘包問題的方法:3d

那麼最關鍵的就是咱們該怎麼處理粘包拆包問題呢?由於這個問題在socket沒法很好的處理,因此必需要在應用層上面處理,因此就須要要求你們在封裝網絡通訊接口的時候要本身實現粘包拆包的處理方法。解決問題的關鍵在於如何給每一個數據包添加邊界信息,經常使用的方法有以下幾個:code

  • 能夠在數據包之間設置邊界,如添加特殊符號,這樣,接收端經過這個邊界就能夠將不一樣的數據包拆分開。
  • 發送端將每一個數據包封裝爲固定長度(不夠的能夠經過補0填充),這樣接收端每次從接收緩衝區中讀取固定長度的數據就天然而然的把每一個數據包拆分開來。
  • 發送端給每一個數據包添加包首部,首部中應該至少包含數據包的長度,這樣接收端在接收到數據後,經過讀取包首部的長度字段,便知道每個數據包的實際長度了。

第1種和第2種方法都會存在一些偏差,沒有辦法很好處理好粘包拆包,因此通常的方法都是採用第3種。如下我先給出代碼,而後再結合代碼分析第3種粘包拆包的處理方式。server

3.一、服務端代碼

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
 
#include "protocol.h"
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
 
int main()
{
    //建立套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //初始化socket元素
    struct sockaddr_in server_addr;
    int server_len = sizeof(server_addr);
    memset(&server_addr, 0, server_len);
 
    server_addr.sin_family = AF_INET;
    //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用這個寫法也能夠
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(39002);
 
    //綁定文件描述符和服務器的ip和端口號
    int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len);
    if (m_bindfd < 0)
    {
        ERR_EXIT("bind ip and port fail");
    }
 
    //進入監聽狀態,等待用戶發起請求
    int m_listenfd = listen(m_sockfd, 20);
    if (m_listenfd < 0)
    {
        ERR_EXIT("listen client fail");
    }
 
    //定義客戶端的套接字,這裏返回一個新的套接字,後面通訊時,就用這個m_connfd進行通訊
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
 
    //接收客戶端數據
    char recv_buffer[10000]; //接收數據的buffer
    memset(recv_buffer, 0, sizeof(recv_buffer)); //初始化接收buffer
 
    while (1)
    {
        if (m_connfd < 0)
        {
            m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
            printf("client accept success again!!!\n");
        }
 
        //休眠10秒纔能有粘包現象出現
        sleep(10);
 
        int nrecvsize = 0;      //一次接收到的數據大小
        int sum_recvsize = 0; //總共收到的數據大小
        int packersize;          //數據包長度
 
        int disconn = false;
 
        //先從緩存池取出包頭
        while (sum_recvsize != sizeof(NetPacketHeader))
        {
            nrecvsize = recv(m_connfd, recv_buffer + sum_recvsize, sizeof(NetPacketHeader) - sum_recvsize, 0);
            if (nrecvsize == 0)
            {
                close(m_connfd);
                m_connfd = -1;
                printf("client lose connection!!!\n");
                disconn = true;
                break;
            }
            sum_recvsize += nrecvsize;
        }
 
        if (disconn)
        {
            continue;
        }
 
        NetPacketHeader *phead = (NetPacketHeader *)recv_buffer;
        packersize = phead->wDataSize;                     //客戶端發過來的數據包長度(包含包頭)
 
        //從緩衝池中取出數據(不包含包頭)
        while (sum_recvsize != packersize)
        {
            nrecvsize = recv(m_connfd, recv_buffer + sum_recvsize, packersize - sum_recvsize, 0);
            if (nrecvsize == 0)
            {
                close(m_connfd);
                m_connfd = -1;
                printf("client lose connection!!!\n");
                disconn = true;
                break;
            }
            else if (nrecvsize < 0)
            {
                ERR_EXIT("recv fail");
            }
            printf("server recv:%s, size:%d\n", recv_buffer + sum_recvsize, nrecvsize);
 
            sum_recvsize += nrecvsize;
        }
        if (disconn)
        {
            continue;
        }
    }
 
    //關閉套接字
    close(m_connfd);
    close(m_sockfd);
 
    return 0;
}

 

3.二、客戶端代碼

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#include "protocol.h"
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
 
 
int main()
{
    //建立套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //服務器的ip爲本地,端口號
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("81.68.140.74");
    server_addr.sin_port = htons(39002);
 
    //向服務器發送鏈接請求
    if (connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        ERR_EXIT("connect server fail");
    }
 
    //發送並接收數據
    char data_buffer[BUF_SIZE] = "asd";
    int datasize = strlen(data_buffer);
 
    NetPacket send_packet;                                             //數據包
    send_packet.Header.wDataSize = datasize + sizeof(NetPacketHeader); //數據包大小
 
    memcpy(send_packet.Data, data_buffer, datasize); //數據拷貝
 
    send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0);
    send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0);
    send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0);
 
    //斷開鏈接
    close(m_sockfd);
 
    return 0;
}

 

3.三、公用的部分

//protocol.h
 
#ifndef _PROTOCOL_H
#define _PROTOCOL_H
 
#define NET_PACKET_DATA_SIZE 5000
 
/// 網絡數據包包頭
struct NetPacketHeader
{
    unsigned short wDataSize; ///< 數據包大小,包含包頭的長度和數據長度
};
 
/// 網絡數據包
struct NetPacket
{
    NetPacketHeader Header;                   /// 包頭
    unsigned char Data[NET_PACKET_DATA_SIZE]; /// 數據
};
 
 
#endif

 

首先定義一個新的文件protocol.h,主要是客戶端和服務端共用的部分,包含數據包和包頭的結構體定義。

而後客戶端發送的時候記得發送數據體的長度是數據加包頭的長度。

而在接收端的代碼則稍微要花點心思了。首先接收端須要分兩次來從緩衝池中接收數據,先取出長度爲包頭的數據,而後去取數據體的部分的時候必定要記得每次從緩衝區取數據的偏移量。

這樣子就能夠正確的處理好粘包拆包的問題了。固然從服務端向客戶端發送數據的話,二者則是顛倒過來,這裏就不在說明了。最後但願你們能夠從這邊文章得到一點收穫,有什麼疑問歡迎在下方評論說明。

更多精彩內容,請關注同名公衆:一點筆記alittle

相關文章
相關標籤/搜索