C语言TCP流拆分消息的正确实现与多消息传输故障排查
C语言TCP套接字连续收发阻塞问题排查与修复
问题背景
开发基于C语言的客户端-服务端TCP数据交互项目时,已知TCP为字节流协议,单次send调用提交的数据可能需要多次recv调用才能完整读取,因此最初实现了如下循环收发的基础函数:
初始发送函数
void send_message(int clientSocket, char message [], int length ){ int sent = 0; while(sent < length){ sent += send(clientSocket, message + sent , length - sent , 0); } }
初始接收函数
void receive_message(int clientSocket, char message [], int length){ int received = 0; while(received < length){ received += recv(clientSocket, message + received , length - received , 0); } }
单条消息传输场景下上述函数运行正常,但连续发送多条固定长度消息时,发送端可正常退出发送循环(表面代表所有字节已提交到套接字发送缓冲区),接收端却无法完成全部数据接收,会在某次recv调用时持续返回0字节导致程序阻塞卡死。
初始测试代码
客户端初始代码
#include<stdlib.h> #include<string.h> #include<inttypes.h> #include <arpa/inet.h> #include <netinet/in.h> #include<sys/socket.h> void send_message(int clientSocket, char message [], int length ){ int sent = 0; while(sent < length){ sent += send(clientSocket, message + sent , length - sent , 0); } } void receive_message(int clientSocket, char message [], int length){ int received = 0; while(received < length){ received += recv(clientSocket, message + received , length - received , 0); } } int main(){ //初始化套接字 struct sockaddr_in serverAddr; socklen_t addr_size; int clientSocket = socket(AF_INET, SOCK_STREAM, 0); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(7899); //将SERVER_IP_ADDRESS替换为实际服务端地址 inet_pton(AF_INET,"SERVER_IP_ADDRESS9999",&serverAddr.sin_addr); memset(serverAddr.sin_zero, '\0', sizeof serverAddr.sin_zero); addr_size = sizeof serverAddr; connect(clientSocket, (struct sockaddr *) &serverAddr, addr_size); // 测试逻辑 char message [3000]= ""; memset(message, 0,3000); for(int i = 0 ; i< 20; i ++){ //填充测试字符串 for(int j=0; j<2999; j++){ message[j] = 'A' + i; } send_message(clientSocket, message, 3000); printf("i = %d \n %s\n",i, message); } }
服务端初始代码
#include <stdio.h> #include<stdlib.h> #include<string.h> #include<inttypes.h> #include <netinet/in.h> #include<sys/socket.h> void send_message(int clientSocket, char message [], int length ){ int sent = 0; while(sent < length){ sent += send(clientSocket, message + sent , length - sent , 0); } } void receive_message(int clientSocket, char message [], int length){ int received = 0; while(received < length){ received += recv(clientSocket, message + received , length - received , 0); } } int main(){ //初始化套接字 int welcomeSocket, clientSocket; struct sockaddr_in serverAddr; struct sockaddr_storage serverStorage; socklen_t addr_size; welcomeSocket = socket(AF_INET, SOCK_STREAM, 0); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(7899); serverAddr.sin_addr.s_addr = INADDR_ANY; memset(serverAddr.sin_zero, '\0', sizeof serverAddr.sin_zero); bind(welcomeSocket, (struct sockaddr *) &serverAddr, sizeof(serverAddr)); if (listen(welcomeSocket,1)==0) printf("Listening\n"); else printf("Error\n"); addr_size = sizeof serverStorage; //接收到达的客户端连接 clientSocket = accept(welcomeSocket, (struct sockaddr *) &serverStorage, &addr_size); printf("accepted"); // 测试逻辑 char message [3000]= ""; memset(message, 0,3000); for(int i = 0 ; i< 20; i ++){ receive_message(clientSocket, message, 3000); printf("i:= %d\n %s\n",i, message); } }
异常现象汇总
- 发送端在每条消息发送间隔增加短暂等待后,程序可正常运行,初步怀疑为短时间发送字节量过大导致套接字过载
- 同机部署(本地环回通信)时代码运行正常,跨机器远程部署时稳定复现接收阻塞问题
- 发送端循环内增加
printf打印语句时程序无异常,移除打印语句后服务端立即复现接收卡住的问题
根因分析
- 原代码未处理套接字接口的异常返回:
recv返回值为0时代表对端已正常关闭连接,返回-1时代表出现网络错误,原逻辑直接将返回值累加到接收字节计数中,一旦连接提前断开,返回0会导致接收计数永远无法达到预期长度,循环无限执行表现为程序卡死。 - 原客户端发送完所有数据后未执行优雅关闭流程,进程直接退出会触发连接异常中断,发送缓冲区中未传输完成的数据可能被丢弃。本地环回场景下网络传输延迟极低、缓冲区充足,几乎不会出现数据未传完连接就断开的情况;跨网络场景下受Nagle算法、链路延迟、设备缓冲区调度影响,高速连续发送时很容易出现连接提前断开的情况,增加sleep、打印语句本质是放缓了发送速率,给数据传输留出了足够时间,因此会掩盖真实问题。
修复方案
核心修复逻辑共两点:
- 为
recv(生产环境还需包含send)增加返回值校验,返回值小于等于0时立即打印错误信息、跳出循环,避免无限阻塞 - 所有数据发送完成后调用
shutdown执行优雅套接字关闭,确保发送缓冲区的所有数据完成传输后再断开连接
修复后服务端代码
#include <stdio.h> #include<stdlib.h> #include<string.h> #include<inttypes.h> #include <netinet/in.h> #include<sys/socket.h> void receive_message(int clientSocket, char message [], int length){ int received = 0; int r =0; while(received < length){ r = recv(clientSocket, message + received , length - received , 0); if( r<= 0){ printf("!\n\nERROR- skip \n\n\n!!!!!!!!"); break; }else{ received += r; } } } int main(){ //初始化套接字 int welcomeSocket, clientSocket; struct sockaddr_in serverAddr; struct sockaddr_storage serverStorage; socklen_t addr_size; welcomeSocket = socket(AF_INET, SOCK_STREAM, 0); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(7899); serverAddr.sin_addr.s_addr = INADDR_ANY; memset(serverAddr.sin_zero, '\0', sizeof serverAddr.sin_zero); bind(welcomeSocket, (struct sockaddr *) &serverAddr, sizeof(serverAddr)); if (listen(welcomeSocket,1)==0) printf("Listening\n"); else printf("Error\n"); addr_size = sizeof serverStorage; //接收到达的客户端连接 clientSocket = accept(welcomeSocket, (struct sockaddr *) &serverStorage, &addr_size); printf("accepted"); // 测试逻辑 char message [3000]= ""; memset(message, 0,3000); for(int i = 0 ; i< 30; i ++){ receive_message(clientSocket, message, 3000); printf("i:= %d\n %s\n",i, message); } }
修复后客户端代码
#include <stdio.h> #include<stdlib.h> #include<string.h> #include<inttypes.h> #include <arpa/inet.h> #include <netinet/in.h> #include<sys/socket.h> void send_message(int clientSocket, char message [], int length ){ int sent = 0; while(sent < length){ sent += send(clientSocket, message + sent , length - sent , 0); } } int main(){ //初始化套接字 struct sockaddr_in serverAddr; socklen_t addr_size; int clientSocket = socket(AF_INET, SOCK_STREAM, 0); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(7899); //将SERVER_IP_ADDRESS替换为实际服务端地址 inet_pton(AF_INET,"15.161.236.166",&serverAddr.sin_addr); memset(serverAddr.sin_zero, '\0', sizeof serverAddr.sin_zero); addr_size = sizeof serverAddr; connect(clientSocket, (struct sockaddr *) &serverAddr, addr_size); // 测试逻辑 printf("Start\n"); char message [3000]= ""; memset(message, 0,3000); for(int i = 0 ; i< 30; i ++){ //填充测试字符串 for(int j=0; j<2999; j++){ message[j] = 'A' + (i % 26);//(rand() %20) ; } message[4] = 0; send_message(clientSocket, message, 3000); } shutdown(clientSocket, 2); }
内容的提问来源于stack exchange,提问作者R_marche
相关产品推荐
相关产品推荐

