首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

TCP 封包/解包核心原理與簡易實現:解決黏包、半包問題

     編輯:關於C語言

前言:為什麼 TCP 需要應用層封包?

TCP 是位元組流協議,無訊息邊界。send 多次調用的數據可能合併為一個網絡包到達(黏包),一次 send 的大數據可能被分片多次 recv 到達(半包)。應用層必須自定義協議框架,還原完整業務訊息。

一、通用封包結構設計

// 固定長度包頭 (8 bytes 示例)
struct PacketHeader {
    uint16_t opcode;      // 操作碼:業務指令標識
    uint32_t length;      // 整包總長度(含包頭),網絡位序
    uint16_t reserved;    // 擴展/校驗/版本
};

// 完整封包
struct Packet {
    PacketHeader header;
    uint8_t body[];       // 變長業務數據
};

關鍵點:

  • length 必須包含包頭自身,便於接收端一次性判斷完整性。
  • 所有整數字段採用網絡位序(htons/htonl),跨平台兼容。
  • opcode 設計預留擴展位(高 8 位模塊,低 8 位命令)。

二、發送端封包邏輯

bool SendPacket(SOCKET sock, uint16_t opcode, const void* data, uint32_t dataLen) {
    const uint32_t headerSize = sizeof(PacketHeader);
    uint32_t totalLen = headerSize + dataLen;
    if (totalLen > MAX_PACKET_SIZE) return false;

    // 分配發送緩衝
    uint8_t* buf = (uint8_t*)malloc(totalLen);
    PacketHeader* hdr = (PacketHeader*)buf;
    hdr->opcode = htons(opcode);
    hdr->length = htonl(totalLen);
    hdr->reserved = 0;
    if (dataLen > 0) memcpy(buf + headerSize, data, dataLen);

    // 發送(處理發送緩衝區滿/非阻塞場景需循環 send)
    int sent = send(sock, (char*)buf, totalLen, 0);
    free(buf);
    return sent == (int)totalLen;
}

三、接收端解包狀態機(核心難點)

維護每連接一個動態接收緩衝區,循環處理:

  1. 追加數據:recv 到的數據 append 到緩衝區尾部。
  2. 嘗試解析:
    • 若緩衝區 sizeof(PacketHeader),返回等待更多數據。
    • 解析包頭獲取 totalLen(網絡序轉主機序)。
    • 若緩衝區 totalLen,返回等待(半包)。
    • 完整包:提取 body 分發業務邏輯,從緩衝區移除該包數據(memmove 或環形緩衝區)。
    • 循環步驟 2,直到緩衝區不足一個包頭或不足一個完整包。

四、簡易客戶端/服務端框架代碼

服務端

void ServerLoop(SOCKET listenSock) {
    SOCKET client = accept(listenSock, NULL, NULL);
    uint8_t recvBuf[RECV_BUF_SIZE];
    uint32_t dataLen = 0;

    while (true) {
        int r = recv(client, (char*)recvBuf + dataLen, RECV_BUF_SIZE - dataLen, 0);
        if (r <= 0) break;  // 斷開/錯誤
        dataLen += r;

        // 解包循環
        while (dataLen >= sizeof(PacketHeader)) {
            PacketHeader* hdr = (PacketHeader*)recvBuf;
            uint32_t pktLen = ntohl(hdr->length);
            if (dataLen < pktLen) break;  // 半包

            // 完整包到達
            uint16_t opcode = ntohs(hdr->opcode);
            uint8_t* body = recvBuf + sizeof(PacketHeader);
            uint32_t bodyLen = pktLen - sizeof(PacketHeader);
            HandleBusiness(opcode, body, bodyLen);  // 業務分發

            // 移除已處理包,前移剩餘數據
            dataLen -= pktLen;
            if (dataLen > 0) memmove(recvBuf, recvBuf + pktLen, dataLen);
        }
    }
    closesocket(client);
}

客戶端

void ClientLoop(SOCKET sock) {
    // 發送 5 個測試包
    for (int i = 0; i < 5; ++i) {
        TestMsg msg = { .index = i };
        strcpy(msg.text, "[1]Hello[2]World[3]!");
        SendPacket(sock, OP_TEST, &msg, sizeof(msg));
        Sleep(100);
    }
    // 接收回應邏輯同服務端解包循環...
}

五、工程化增強建議

  • 環形緩衝區:避免 memmove 開銷,O(1) 入隊出隊。
  • 長度校驗上限:防止惡意超大包導致 OOM(如 MAX_PACKET_SIZE = 1MB)。
  • 心跳/超時機制:檢測僵死連接。
  • 非阻塞 + I/O 多路復用:select/poll/epoll/IOCP 支撐高併發。
  • 序列化框架:Protobuf/FlatBuffers/MessagePack 替代手工 memcpy。

總結

TCP 封包核心在於「長度前綴 + 緩衝區狀態機」。理解黏包/半包本質,掌握動態緩衝區解析邏輯,是網絡程式設計的入門必修課。生產環境建議成熟框架,但手寫簡易版是深入理解的最佳途徑。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved