如何使用 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 | int port = 8080; |
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
6struct 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:通过
iovec和sendmsg/recvmsg减少内存拷贝:1
2
3
4
5
6
7
8
9
10struct 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(如EINTR、ECONNRESET) -
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
2
3
4
5
6
7
8
9ssize_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;
} -
**长度前缀:**先读长度字段,再读对应数据:
1
2
3
4
5
6
7
8std::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;
} -
分隔符:如
\r\n(HTTP 头部方式)。
-
-
循环读写:
send/recv可能不一次性处理全部数据,需循环调用:1
2
3
4
5
6
7
8
9ssize_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
2struct 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
3socklen_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 | struct sockaddr_in dest_addr; |
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发送到socket(out_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 |
|
-
TCP:用
send/recv,面向连接,需处理字节流无边界问题,适合可靠传输(如 HTTP)。 -
UDP:用
sendto/recvfrom,无连接,保留数据报边界,适合实时场景(如视频流)。 -
通用基础:字节序转换、序列化、缓冲区管理和错误处理是核心前提。