TCP、UDP数据读写基础

如何使用 C++ 进行 TCP 和 UDP 数据读写的基础知识

一、通用数据读写基础(TCP/UDP 均需掌握)

无论使用何种协议,网络数据读写都需处理字节序转换、数据序列化和错误处理,这是跨平台通信的基础。

1. 字节序转换(解决主机与网络字节序差异)

网络协议规定使用大端字节序(网络字节序),主机字节序可能为大端或小端(如 x86 为小端),需通过以下函数转换:

  • uint16_t htons(uint16_t hostshort):主机→网络字节序(16 位,如端口号)

  • uint32_t htonl(uint32_t hostlong):主机→网络字节序(32 位,如 IPv4 地址)

  • uint16_t ntohs(uint16_t netshort):网络→主机字节序(16 位)

  • uint32_t ntohl(uint32_t netlong):网络→主机字节序(32 位)

进阶场景

  • 64 位数据:POSIX 提供htobe64/be64toh(大端)、htole64/le64toh(小端),Windows 需手动实现。

  • 浮点数:无标准转换函数,需通过指针转换为字节数组传输(依赖 IEEE 754 兼容)。

示例

1
2
3
int port = 8080;
sockaddr_in addr;
addr.sin_port = htons(port); // 端口必须用网络字节序

2. 数据序列化与缓冲区管理

网络传输的是字节流,需通过缓冲区处理,结构化数据需序列化以避免对齐和字节序问题。

核心函数

  • memcpy(void *dest, const void *src, size_t n):拷贝字节(用于序列化)

  • memset(void *s, int c, size_t n):初始化缓冲区(如清空接收缓冲区)

  • strlen(const char *s):计算字符串长度(用于发送字符串)

序列化方案

  • 手动序列化:拆分结构体成员,转换字节序后写入缓冲区:

    1
    2
    3
    4
    5
    6
    struct Msg { int len; short type; };
    char buf[6];
    int net_len = htonl(msg.len);
    short net_type = htons(msg.type);
    memcpy(buf, &net_len, 4);
    memcpy(buf+4, &net_type, 2);
  • 协议库:Protobuf、FlatBuffers 等自动处理对齐和字节序。

缓冲区策略

  • 动态缓冲区:使用vector<char>避免栈溢出,适合接收未知长度数据。

  • 分散 - 聚集 IO:通过iovecsendmsg/recvmsg减少内存拷贝:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct iovec iov[2];  // 分散发送消息头和消息体
    iov[0].iov_base = &header;
    iov[0].iov_len = sizeof(header);
    iov[1].iov_base = data;
    iov[1].iov_len = data_len;

    struct msghdr msg;
    msg.msg_iov = iov;
    msg.msg_iovlen = 2;
    sendmsg(sockfd, &msg, 0);

3. 通用错误处理

网络函数失败时返回 - 1,需通过错误码定位问题:

  • Linux:errno(如EINTRECONNRESET

  • Windows:WSAGetLastError()(如WSAECONNRESET

常见错误:

  • EINTR:操作被信号中断(需重试)

  • EWOULDBLOCK:非阻塞模式下无数据(需等待)

  • ECONNRESET:连接被对方重置

  • ENOTCONN:TCP 未连接或 UDP 未指定目标

二、TCP 数据读写(面向连接的字节流)

TCP 是可靠的面向连接协议,数据无边界,需通过应用层协议界定消息边界。

1. 核心读写函数

  • 发送:ssize_t send(int sockfd, const void *buf, size_t len, int flags)

    • 参数:sockfd(已连接的 TCP socket)、缓冲区、长度、标志(通常 0)。
    • 返回值:实际发送字节数(可能小于len);失败返回 - 1。
    • 标志:MSG_NOSIGNAL(Linux):避免对方关闭时触发 SIGPIPE。
  • 接收:ssize_t recv(int sockfd, void *buf, size_t len, int flags)

    • 参数:sockfd、接收缓冲区、长度、标志(通常 0)。
    • 返回值:实际接收字节数;0 表示对方正常关闭;-1 表示错误。
    • 标志:MSG_PEEK(预览数据不移除)。

MSG_OOB

  • 发送 / 接收带外数据(Out-of-Band data),用于传输紧急数据。带外数据具有更高的优先级,可绕过常规数据流的排队机制。

MSG_PEEK

  • 仅适用于 recv():查看缓冲区中的数据,但不会将其从缓冲区中移除(即数据仍然可以被后续的 recv() 调用读取)。

  • send() 无效。

MSG_WAITALL

  • 仅适用于 recv():要求函数阻塞直到接收到 len 参数指定的全部数据(除非发生错误或连接关闭)。

  • 注意:即使设置了此标志,也可能因信号中断、连接关闭等原因导致实际接收的数据少于 len

仅适用于 send() 的标志

MSG_DONTROUTE

  • 告知内核不使用路由表,直接将数据发送到本地网络上的主机(仅适用于 UDP 或原始套接字)。

MSG_NOSIGNAL

  • 当连接已关闭时,发送数据不会产生 SIGPIPE 信号(默认情况下会产生该信号导致程序终止)。

2. 关键特性与实现

  • 字节流无边界:需应用层定义消息格式,常见方案:

    1. **固定长度:**累计读取到指定长度后处理:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      ssize_t recv_fixed(int sockfd, char *buf, size_t fixed_len) {
      size_t total = 0;
      while (total < fixed_len) {
      ssize_t n = recv(sockfd, buf + total, fixed_len - total, 0);
      if (n <= 0) return n; // 错误或连接关闭
      total += n;
      }
      return total;
      }
    2. **长度前缀:**先读长度字段,再读对应数据:

      1
      2
      3
      4
      5
      6
      7
      8
      std::vector<char> recv_with_header(int sockfd) {
      uint32_t net_len;
      if (recv_fixed(sockfd, (char*)&net_len, 4) != 4) return {};
      size_t len = ntohl(net_len);
      std::vector<char> data(len);
      recv_fixed(sockfd, data.data(), len);
      return data;
      }
    3. 分隔符:如\r\n(HTTP 头部方式)。

  • 循环读写send/recv可能不一次性处理全部数据,需循环调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ssize_t send_all(int sockfd, const void *buf, size_t len) {
    size_t total_sent = 0;
    while (total_sent < len) {
    ssize_t sent = send(sockfd, (char*)buf + total_sent, len - total_sent, 0);
    if (sent == -1) return -1;
    total_sent += sent;
    }
    return total_sent;
    }

3. 连接异常处理

  • 半关闭:对方shutdown(SHUT_WR)后,recv返回 0(可继续发送数据)。

  • 重置连接recv返回 - 1 且errno=ECONNRESET,需关闭 socket 并重连。

  • 超时设置: 通过SO_RCVTIMEO/SO_SNDTIMEO设置读写超时:

    1
    2
    struct timeval timeout = {5, 0};  // 5秒超时
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

三、UDP 数据读写(无连接的数据包)

UDP 是不可靠的无连接协议,数据有边界(一次发送对应一次接收),无需建立连接。

1. 核心读写函数

  • 发送:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)

    • 参数:sockfd(UDP socket)、缓冲区、长度、标志、目标地址及长度。
    • 返回值:成功返回发送字节数(等于len);失败返回 - 1。
  • 接收:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)

    • 参数:sockfd、接收缓冲区、长度、标志、发送方地址(输出)及长度(输入输出)。
    • 返回值:接收字节数(≤len);失败返回 - 1。

2. 关键特性与实现

  • 数据报边界:一次sendto的数据可被recvfrom完整接收(缓冲区足够时)。

  • 长度限制:超过 MTU(通常 1500 字节)会分片,建议单包≤1472 字节(1500-20IP 头 - 8UDP 头)。

  • 截断检测:MSG_TRUNC标志可检测数据是否被截断:

    1
    2
    3
    socklen_t addrlen = sizeof(addr);
    ssize_t n = recvfrom(sockfd, buf, len, MSG_TRUNC, &addr, &addrlen);
    if (n == len && (flags & MSG_TRUNC)) { /* 数据被截断 */ }

3. 连接化 UDP(Connected UDP)

通过connect绑定固定目标地址后,可使用send/recv简化操作(仍为 UDP 特性):

1
2
3
4
struct sockaddr_in dest_addr;
// 初始化目标地址...
connect(udp_sockfd, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
send(udp_sockfd, buf, len, 0); // 无需重复指定地址

4. 可靠性增强

UDP 本身不可靠,需应用层补充机制:

  • 序号与确认(重传超时未确认的包)

  • 校验和(检测数据损坏)

  • 流量控制(避免接收方溢出)

四、高级函数与性能优化

  • ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags):支持分散发送(多个缓冲区数据合并发送),适合发送复杂数据结构。

  • ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags):支持聚集接收(将数据分散到多个缓冲区),常用于接收带辅助数据(如控制信息)的数据包。

  • ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)(Linux):零拷贝发送文件,直接从文件描述符in_fd发送到socketout_fd),减少用户态与内核态拷贝,效率极高。

性能优化

  • TCP:禁用 Nagle 算法(TCP_NODELAY)减少延迟;增大缓冲区(SO_SNDBUF/SO_RCVBUF)提高吞吐量。

  • UDP:避免碎片化;批量发送减少系统调用。

  • IO 模型:高并发场景使用非阻塞 IO+epoll(Linux)/kqueue(BSD)。

五、跨平台兼容性

  • Windows 需初始化套接字库(WSAStartup),Linux 无需。

  • 错误处理:Windows 用WSAGetLastError(),Linux 用errno

  • 关闭 socket:Windows 用closesocket,Linux 用close

兼容代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef _WIN32
#include <winsock2.h>
#define close closesocket
#else
#include <unistd.h>
#include <errno.h>
#endif

int get_last_error() {
#ifdef _WIN32
return WSAGetLastError();
#else
return errno;
#endif
}
  • TCP:用send/recv,面向连接,需处理字节流无边界问题,适合可靠传输(如 HTTP)。

  • UDP:用sendto/recvfrom,无连接,保留数据报边界,适合实时场景(如视频流)。

  • 通用基础:字节序转换、序列化、缓冲区管理和错误处理是核心前提。