探索TCP:分包与粘包解析

目录

1.分包

2.粘包

3.处理分包和粘包

4.代码

4.1基本概念

4.2示例环境

4.3发送方代码

4.4接收方代码

4.4.1水平触发(LT)模式

4.4.2边缘触发(ET)模式

4.4.5注意点


TCP的分包和粘包是指在TCP通信过程中,由于TCP是面向流的协议,发送方发送的数据可以被分割成多个报文段(分包)或多个小的数据包可能会被合并到一个报文段中(粘包)发送到接收方。这两种现象在实际的网络通信中非常常见,理解它们对于正确处理TCP数据流非常重要。

1.分包

分包是指发送方发送的数据包在网络传输过程中被拆分成了多个较小的包。这可能发生在以下情况下:

  • 发送的数据量超过了单个TCP报文段的最大传输单元(MTU),因此数据被拆分。
  • 由于网络设备(如路由器或交换机)的限制,较大的数据包需要被拆分以适应网络传输。

2.粘包

粘包是指发送方发送的多个小数据包被合并成一个较大的TCP报文段发送。这可能发生在以下情况下:

  • 发送方发送了多个小的数据包,但TCP为了提高网络传输效率,将它们合并到一个报文段中发送。
  • 接收方的接收缓冲区中收到的数据是一个连续的流,可能包含多个逻辑上的数据包,这些数据没有明确的边界标志。

3.处理分包和粘包

为了正确处理TCP的分包和粘包问题,通常需要在应用层进行数据的边界识别和重新组装。常见的解决方法有:

  1. 定长消息:每个消息的长度是固定的,接收方只需要按照固定的长度读取数据即可。
  2. 消息头:在消息开始处添加一个定长的消息头,消息头中包含整个消息的长度信息,接收方可以根据消息头来确定消息的边界。
  3. 分隔符:在每个消息之间使用特殊的分隔符,例如换行符。接收方通过检测分隔符来确定消息的边界。

4.代码

在C/C++中处理TCP的分包和粘包问题,通常涉及到编写网络通信代码,使用套接字(socket)来发送和接收数据。下面我将通过一个简单的例子,说明如何在C/C++中处理TCP粘包问题。

4.1基本概念

在TCP通信中,由于TCP是面向流的协议,不保留消息边界,所以发送的多个包可能在接收时被看作一个连续的流。因此,确保数据的完整性通常需要在应用层处理分包和粘包问题。

4.2示例环境
  • 发送方:负责发送两条消息,每条消息包含一个头部(消息长度)和一个消息体。
  • 接收方:读取数据流,根据头部信息确定每条消息的长度,然后根据长度提取完整的消息。
4.3发送方代码

发送方将发送两个消息,每个消息包括一个前缀长度,后面跟着具体的消息内容。

cpp 复制代码
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

void sendMessage(int socket, const char* message) {
    uint32_t len = strlen(message);
    uint32_t networkLen = htonl(len);  // 转换为网络字节序

    // 发送消息长度
    send(socket, &networkLen, sizeof(networkLen), 0);
    // 发送消息体
    send(socket, message, len, 0);
}

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv_addr;

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    sendMessage(sockfd, "Hello");
    sendMessage(sockfd, "World");

    close(sockfd);
    return 0;
}

在网络编程中,ET(边缘触发)和LT(水平触发)是两种常见的事件通知机制,用于非阻塞IO操作。这些机制在Linux系统中常通过epoll来实现。下面我将给出示例代码,分别展示如何在接收方使用ETLT模式处理TCP数据。

4.4接收方代码
4.4.1水平触发(LT)模式

水平触发(Level Triggered)是默认的工作方式,如果文件描述符准备好了,它会一直通知你,直到你做了相应的操作。

LT 模式中,每次 epoll_wait 调用返回时,程序只需处理当前可用的数据。它不需要一次性读取所有数据,因为如果有剩余数据,epoll 会在下一次循环中再次通知该文件描述符的事件,直到所有数据被处理完毕。

cpp 复制代码
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <unistd.h>

void lt_epoll(int listen_sock) {
    int epfd = epoll_create(1);
    struct epoll_event ev, events[10];

    ev.events = EPOLLIN; // 监听输入事件
    ev.data.fd = listen_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);

    while (true) {
        int nfds = epoll_wait(epfd, events, 10, -1);
        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == listen_sock) {
                struct sockaddr_in client_addr;
                socklen_t client_addr_len = sizeof(client_addr);
                int conn_sock = accept(listen_sock, (struct sockaddr *)&client_addr, &client_addr_len);
                char buffer[1024];
                ssize_t nread = read(conn_sock, buffer, sizeof(buffer));
                if (nread > 0) {
                    buffer[nread] = '\0';
                    std::cout << "Received: " << buffer << std::endl;
                }
                close(conn_sock);
            }
        }
    }
    close(epfd);
}

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv_addr;

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = INADDR_ANY;

    bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    listen(sockfd, 10);
    lt_epoll(sockfd);
    close(sockfd);
    return 0;
}
4.4.2边缘触发(ET)模式

边缘触发(Edge Triggered)是一种更高效的方式,只有在状态发生变化时,即文件描述符从未准备好变为准备好时,会通知一次。

ET 模式下,接收方需要在每次 epoll_wait 返回时尽可能多地读取数据,直到返回 EAGAIN 错误,表明当前没有更多数据可读。这种模式要求程序更加精确地控制数据的读取过程,以避免漏掉任何数据,因为如果有数据未被读取完毕,epoll 不会再次通知你这一事件。

cpp 复制代码
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>

void setNonBlocking(int sock) {
    int opts;
    opts = fcntl(sock, F_GETFL);
    if (opts < 0) {
        perror("fcntl(F_GETFL)");
        exit(EXIT_FAILURE);
    }
    opts = (opts | O_NONBLOCK);
    if (fcntl(sock, F_SETFL, opts) < 0) {
        perror("fcntl(F_SETFL)");
        exit(EXIT_FAILURE);
    }
}

void et_epoll(int listen_sock) {
    int epfd = epoll_create(1);
    struct epoll_event ev, events[10];

    ev.events = EPOLLIN | EPOLLET; // 监听输入事件,边缘触发
    ev.data.fd = listen_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);

    while (true) {
        int nfds = epoll_wait(epfd, events, 10, -1);
        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == listen_sock) {
                struct sockaddr_in client_addr;
                socklen_t client_addr_len = sizeof(client_addr);
                int conn_sock = accept(listen_sock, (struct sockaddr *)&client_addr, &client_addr_len);
                setNonBlocking(conn_sock);
                while (true) {
                    char buffer[1024];
                    ssize_t nread = read(conn_sock, buffer, sizeof(buffer));
                    if (nread > 0) {
                        buffer[nread] = '\0';
                        std::cout << "Received: " << buffer << std::endl;
                    } else if (nread == 0 || (nread < 0 && errno != EAGAIN)) {
                        close(conn_sock);
                        break;
                    }
                }
            }
        }
    }
    close(epfd);
}

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv_addr;

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = INADDR_ANY;

    bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    listen(sockfd, 10);
    setNonBlocking(sockfd);
    et_epoll(sockfd);
    close(sockfd);
    return 0;
}
4.4.5注意点
  • LT模式中,每次epoll_wait返回时,如果文件描述符有事件,它会继续通知直到处理完毕。
  • ET模式中,你必须循环处理数据直到EAGAIN返回,因为新事件只在状态变化时通知一次。
  • setNonBlocking函数设置套接字为非阻塞模式,这对ET模式尤为重要,以确保不会因为阻塞调用而停滞。
相关推荐
车载诊断技术18 分钟前
电子电气架构 --- 什么是EPS?
网络·人工智能·安全·架构·汽车·需求分析
KevinRay_23 分钟前
Python超能力:高级技巧让你的代码飞起来
网络·人工智能·python·lambda表达式·列表推导式·python高级技巧
2301_819287121 小时前
ce第六次作业
linux·运维·服务器·网络
CIb0la1 小时前
GitLab 停止为中国区用户提供 GitLab.com 账号服务
运维·网络·程序人生
Black_mario2 小时前
链原生 Web3 AI 网络 Chainbase 推出 AVS 主网, 拓展 EigenLayer AVS 应用场景
网络·人工智能·web3
Aileen_0v02 小时前
【AI驱动的数据结构:包装类的艺术与科学】
linux·数据结构·人工智能·笔记·网络协议·tcp/ip·whisper
中科岩创3 小时前
中科岩创边坡自动化监测解决方案
大数据·网络·物联网
花鱼白羊3 小时前
TCP Vegas拥塞控制算法——baseRtt 和 minRtt的区别
服务器·网络协议·tcp/ip
brrdg_sefg4 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全