专业编程基础技术教程

网站首页 > 基础教程 正文

C++网络编程之解决粘包问题

ccvgpt 2025-01-15 11:14:31 基础教程 2 ℃

在网络通信中,经常会遇到“粘包”(Packet Coalescing)或者叫“数据包合并”现象。最主要的原因TCP协议是一种基于流的传输层协议,数据是没有边界的,或者说不保证数据包的边界与应用层消息的边界完全一致。当发送端连续发送多个小的数据包时,由于TCP协议为了提高网络效率可能会将这些小的数据包合并成一个大的数据块后再发送出去,或者接收端接收到多个较小的数据包后没有立即交付给应用层,而是暂存在缓冲区中,等到积累到一定数量或者达到某个条件时一次性交付给上层应用,这就导致了接收端应用看到的是连续的一段数据,而不是预期的一个个独立的数据包。

前面两篇文章中程序,当客户端连续不断给服务端发送消息,服务端收到的消息就不一定是“Received message: Hello, Server!”,如下修改代码:

C++网络编程之解决粘包问题

客户端:

  for (int i = 0; i < 10; i++) {
    // 发送消息到服务器
    if (send(client_sock, message.c_str(), message.length(), 0) < 0) {
      std::cerr << "Failed to send data to the server." << std::endl;
      close(client_sock);
      return -1;
    }
    std::cout << "Message sent: " << message << std::endl;
  }

服务端:

    while (true) {
      // 接收客户端发送的数据
      ssize_t n = recv(client_sock, buffer, sizeof(buffer), 0);
      if (n < 0) {
        std::cerr << "Failed to receive data from client." << std::endl;
        close(client_sock);
        break;
      } else if (n == 0) { // 对方关闭
        std::cout << "Recieve 0, break.\n";
        close(client_sock);
        break;
      }
      buffer[n] = '\0';  // 添加字符串结束符

      std::cout << "Received message: " << buffer << std::endl;
    }

测试结果可以看到服务端打印的次数和服务端发送的次数不一致。

解决TCP粘包问题通常需要在应用层设计合适的协议来确保消息的边界清晰:

  • 定长消息:每个消息都固定长度,这样接收端每次接收指定长度的数据即可区分出一个完整的消息。
  • 消息头+消息体结构:在消息前添加头部,头部包含消息体的长度信息,接收端先读取头部,根据头部信息解析出消息体的实际长度,然后读取相应长度的内容作为一条完整的消息。
  • 分隔符标识:在每条消息之间使用特定的分隔符,例如换行符、特殊字符序列等,接收端通过识别分隔符来划分不同消息。

不同系统可根据自身需求来使用不同的方式解决粘包问题,实际业务系统应用中大多是采用消息头+消息体结构的方式来传送消息的,比如常见的http协议就是采用这种方式。

我们下面使用消息头+消息体结构的例子来说明这种情况,定义如下消息头:

struct message_head {
    int magic_num;     // magic number
    int message_id;    // 消息唯一标识符
    int message_type;  // 消息类型
    int content_len;   // 内容长度
};

以上,我们定义了一个非常简单的消息头。magic_num是系统自己约定的号码,当系统收到消息时先检查这个值是不是系统约定的值,如果不是就直接丢弃报文,甚至可以断开连接,因为此连接发送了错误的消息,留着也没什么用了;message_id作为消息的唯一标识,可以作为全链路追溯的标识,应答也使用这个标识,这样请求和应答就可以关联起来,特别有中间转发系统,这个标识可以作为转发通道依据;message_type表示消息类型,系统可以根据类型来处理消息内容;content_len表示内容长度,即消息头后跟着的消息体的长度。

我们把前两篇文章里的客户端发送消息和服务端接收消息的代码改动一下,如下:

增加一个公共头文件common.h,只声明一个消息头类型:

#pragma once

struct message_head {
    int magic_num;     // magic number
    int message_id;    // 消息唯一标识符
    int message_type;  // 消息类型
    int content_len;   // 内容长度
};

#define MAGIC_NUM 30030001

客户端发送消息部分做如下修改:

  message_head h;
  h.magic_num = MAGIC_NUM;
  h.message_id = 109900001;
  h.message_type = 1001;
  h.content_len = message.size();

  for (int i = 0; i < 10; i++) {
    // 发送消息头
    if (send(client_sock, &h, sizeof(message_head), 0) < 0) {
      std::cerr << "Failed to send message_head to the server." << std::endl;
      close(client_sock);
      return -1;
    }

    // 发送消息到服务器
    if (send(client_sock, message.c_str(), message.length(), 0) < 0) {
      std::cerr << "Failed to send data to the server." << std::endl;
      close(client_sock);
      return -1;
    }
    std::cout << "Message sent: " << message << std::endl;

    h.message_id++;
  }

服务端接收的代码做如下修改:

  while (true) {
    // 接受新的连接请求
    if ((client_sock = accept(server_sock, (struct sockaddr*)&client_addr,
                              &addr_len)) < 0) {
      std::cerr << "Failed to accept connection." << std::endl;
      continue;
    }

    std::cout << "Accepted connection from: " << inet_ntoa(client_addr.sin_addr)
              << ":" << ntohs(client_addr.sin_port) << std::endl;

    message_head h;

    while (true) {
      ssize_t n = recv(client_sock, &h, sizeof(message_head), 0);  // 先读取消息头
      if (n < 0) {
        std::cerr << "Failed to receive data from client." << std::endl;
        close(client_sock);
        break;
      } else if (n == 0) { // 对方关闭
        std::cout << "Recieve 0, break.\n";
        close(client_sock);
        break;
      }

      if (n != sizeof(message_head)) {
        std::cerr << "Message head is not complete.\n";
        close(client_sock);
        break;
      }

      if (h.magic_num != MAGIC_NUM) {
        std::cerr << "Magic number is not correct.\n";
        close(client_sock);
        break;
      }

      // 打印消息头
      printf("magic_num: %d, message_id: %d, message_type: %d, content_len: %d\n",
             h.magic_num, h.message_id, h.message_type, h.content_len);

      char *buff = (char *) calloc(h.content_len, sizeof(char));

      // 接收客户端发送的消息体数据
      n = recv(client_sock, buff, h.content_len, 0);
      if (n < 0) {
        std::cerr << "Failed to receive data from client." << std::endl;
        close(client_sock);
        break;
      } else if (n == 0) { // 对方关闭
        std::cout << "Recieve 0, break.\n";
        close(client_sock);
        break;
      }
      buff[n] = '\0';  // 添加字符串结束符

      std::cout << "Received message: " << buff << std::endl;

      free(buff);
    }

    // 关闭客户端连接
    close(client_sock);
  }

如此这般,服务端就可以正确的处理消息了。这里只是提供了最简单的最直白的发送接收消息,平铺直叙,这里面部分功能可以直接封装起来,为了方便讲解和阅读,我就不封装,读者有兴趣请自行封装。而且例子里没有提供更多的异常处理代码?主要是消息读取不完整时怎么处理,这些功能也会在后面的文章里填坑。

至此,粘包是什么以及怎么处理我们就搞定了。

最近发表
标签列表