C Windows下套接字send/recv缓冲区正确收发实现咨询
你的现有实现存在多处TCP编程典型错误,完全无法保证收发正确性,具体问题和修正方案如下:
一、核心问题说明
首先明确TCP是字节流协议,不存在天然的包边界:send()和recv()每次调用都可能只处理部分字节,哪怕是4字节的固定长度头,也可能被拆成多次收发,或者和后续 payload 合并成一次接收,你的代码完全没有处理这些场景。
1. 客户端发送逻辑错误
- 发送4字节长度头时仅调用一次
send(),出现SOCKET_ERROR仅打印日志就继续执行;即使没有报错,也可能只发送了1/2/3字节,接收端拿到的长度头完全错误。 - payload发送循环逻辑致命错误:每次
send()都从totalBuffer起始位置开始,传入的发送长度是总长度sizeBuffer,而非从已发送偏移位置发送剩余字节。比如总长度1000字节,第一次只发了300字节,第二次调用会从头开始重发1000字节,最终接收端会拿到大量重复的错乱数据。 - 没有处理
send()返回0(连接断开)的分支,出错后仍继续累加totalSent,会导致逻辑死循环或者越界。
2. 服务端接收逻辑错误
- 和发送端问题一致:4字节长度头仅调用一次
recv(),既没有处理收不全的场景,也没有判断连接断开、报错的分支。 - payload接收循环逻辑错误:每次
recv()都往buffer起始位置写入,传入的接收长度是总长度sizeBuffer,没有按已接收字节做偏移,也没有限制本次接收的最大长度为剩余未收字节,会直接覆盖已收到的数据,甚至造成缓冲区溢出。 - 用
strcat()拼接网络数据完全错误:strcat()依赖\0判断字符串结尾,网络传输的是二进制字节流,既不保证存在\0,如果传输内容本身包含\0还会出现截断;且你没有初始化totalBuffer首字节为\0,第一次调用strcat()就会从内存随机位置开始追加,直接造成内存越界。 - 循环内printf打印内容重叠的原因很简单:你每次
recv()拿到的片段没有手动补\0结尾,printf("%s")会一直向后读内存直到碰到随机出现的\0,自然会带上旧缓冲区的内容,出现重叠错乱。 - 每次接收后用
bzero清空整个缓冲区完全没有必要,只会浪费性能,只要正确维护写入偏移和接收长度即可。
二、长度字段必须做完整性校验
TCP不保证单次调用收发的字节数和传入参数一致,哪怕是固定4字节的长度头,也可能被拆成多次接收(比如第一次收1字节,第二次收3字节),如果直接把不完整的4字节转成uint32类型,得到的长度值完全错误,后续接收逻辑会出现内存越界、死循环等严重问题。
除此之外还必须校验长度值的合法性,比如设置一个最大允许的包长度,避免恶意构造的超大长度包导致内存溢出、OOM等问题。
三、正确实现示例
1. 通用可靠发送函数
先实现一个保证发完指定长度才返回的工具函数,长度头和payload都用这个函数发送:
// 阻塞socket下可靠发送len字节数据 // 返回值: 0=发送成功, -1=发送出错, 1=连接正常断开 int send_all(SOCKET sock, const char* buf, int len) { int total_sent = 0; int sent; while (total_sent < len) { sent = send(sock, buf + total_sent, len - total_sent, 0); if (sent == SOCKET_ERROR) { int err = WSAGetLastError(); printf("send failed, error code: %d\n", err); return -1; } if (sent == 0) { return 1; // 对端关闭连接 } total_sent += sent; } return 0; }
客户端发送逻辑简化为:
uint32_t num = htonl(sizeBuffer); int res = send_all(ClientSocket, (const char*)&num, sizeof(uint32_t)); if (res != 0) { closesocket(ClientSocket); WSACleanup(); return 1; } res = send_all(ClientSocket, totalBuffer, sizeBuffer); if (res != 0) { closesocket(ClientSocket); WSACleanup(); return 1; }
2. 通用可靠接收函数
同理实现收满指定长度才返回的工具函数:
// 阻塞socket下可靠接收len字节数据 // 返回值: 0=接收成功, -1=接收出错, 1=连接正常断开 int recv_all(SOCKET sock, char* buf, int len) { int total_recv = 0; int recved; while (total_recv < len) { recved = recv(sock, buf + total_recv, len - total_recv, 0); if (recved == SOCKET_ERROR) { int err = WSAGetLastError(); printf("recv failed, error code: %d\n", err); return -1; } if (recved == 0) { return 1; // 对端关闭连接 } total_recv += recved; } return 0; }
服务端接收逻辑简化为:
#define MAX_ALLOWED_PKG_SIZE 1024*1024 // 限制最大包为1MB,根据业务调整 char b[sizeof(uint32_t)]; int r = recv_all(s, b, sizeof(uint32_t)); if (r != 0) { closesocket(s); return 1; } uint32_t sizeBuffer = ntohl_ch(b); // 校验长度合法性 if (sizeBuffer == 0 || sizeBuffer > MAX_ALLOWED_PKG_SIZE) { printf("invalid package size: %u\n", sizeBuffer); closesocket(s); return 1; } // 分配缓冲区,多留1字节放字符串结束符 char* totalBuffer = (char*)malloc(sizeBuffer + 1); if (totalBuffer == NULL) { closesocket(s); return 1; } r = recv_all(s, totalBuffer, sizeBuffer); if (r != 0) { free(totalBuffer); closesocket(s); return 1; } totalBuffer[sizeBuffer] = '\0'; // 手动补结束符,避免打印乱码 printf("recv content: %s\n", totalBuffer); free(totalBuffer);
四、额外注意事项
- 如果你使用非阻塞socket,需要在上述函数中额外处理
WSAEWOULDBLOCK错误,配合select/IOCP等IO复用机制等待socket可写/可读后再继续收发。 - 不要用
strcat/strcpy等处理C字符串的函数操作网络字节流,这类函数依赖\0判断边界,和二进制字节流的特性完全不匹配,必然会出现内存错误或者数据截断。 - 如果需要在循环中打印接收进度,不要直接用
%s打印未完成的缓冲区,可以传入本次接收的长度,用printf("%.*s", received, buffer + totalReceived - received)的方式打印指定长度的内容,避免读越界。
内容的提问来源于stack exchange,提问作者Y K
相关产品推荐
相关产品推荐

