Socket连续发送消息粘包:C++服务端与Python客户端问题排查
解决TCP Socket消息合并问题
问题描述
我用C++编写了Socket服务端,Python编写了客户端,开发聊天机器人时遇到异常:服务端连续向客户端发送两条消息,Python客户端接收到的内容被合并显示,无法按预期分条展示。
Python客户端代码
import socket IP = "127.0.0.1" PORT = 4456 ADDR = (IP,PORT) SIZE = 1024 FORMAT = "utf-8" if __name__ == "__main__": print("This is client!!!!!") client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(ADDR) data = client.recv(SIZE).decode(FORMAT) print(f"CONNECTED TO THE SERVER WITH MESSAGE {data}") print(data[-1]) while(True): data = client.recv(SIZE).decode(FORMAT) print(f"[SERVER] {data}") if(data[-1] == "r"): break meg = input('press Y/N: ') client.send(meg.encode(FORMAT)) while(True): meg = input('To sever:') client.send(meg.encode(FORMAT)) data = client.recv(SIZE).decode(FORMAT) print(f"[SERVER] {data}") if(meg == "Done"): client.close() break
C++服务端代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string> #include <iostream> #define PORT 4456 using namespace std; int main(){ //Variables and structures int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t addr_size; char buffer[1024]; string inp; // Server socket server_fd = socket(AF_INET, SOCK_STREAM, 0); //create the server socket server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); listen(server_fd, 5); printf("[LISTENING] Port number: %d\n", PORT); client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_size); printf("[CONNECTED] New Connection\n"); strcpy(buffer, "Hello, this is a test message..."); send(client_fd, buffer, strlen(buffer), 0); while(1){ memset(buffer, '\0', sizeof(buffer)); strcpy(buffer, "Please press Y/N"); send(client_fd, buffer, strlen(buffer), 0); memset(buffer, '\0', sizeof(buffer)); recv(client_fd, buffer, 1024, 0); if(strcmp(buffer,"Y") == 0){ memset(buffer, '\0', sizeof(buffer)); strcpy(buffer, "Readyrrrrr"); send(client_fd, buffer, strlen(buffer), 0); break; } } while(1){ memset(buffer, '\0', sizeof(buffer)); recv(client_fd, buffer, 1024, 0); printf("[CLIENT]: %s\n", buffer); if(strcmp(buffer,"Done") == 0){ inp = "Done signal received"; strcpy(buffer, inp.c_str()); send(client_fd, buffer, strlen(buffer), 0); close(client_fd); printf("[DISCONNECTED] Process Done \n"); break; } cout<< "To client: "; getline(cin, inp); cout<<inp<<endl; strcpy(buffer, inp.c_str()); send(client_fd, buffer, strlen(buffer), 0); printf("[CONTINUE].....\n"); } return 0; }
实际输出
CONNECTED TO THE SERVER WITH MESSAGE Hello, this is a test message...Please press Y/N N
预期输出
CONNECTED TO THE SERVER WITH MESSAGE Hello, this is a test message... . [SERVER] Please press Y/N Please press Y/N:
问题根源
TCP是流式协议,它仅传输连续的字节流,不会为应用层划分消息边界。服务端连续发送的两条短消息会被操作系统的TCP缓冲区通过Nagle算法合并,导致客户端一次recv调用就读取到多条消息的内容。
解决方案
必须在应用层定义消息边界,以下是三种可靠的实现方式:
1. 特殊分隔符标记消息结束
在每条消息末尾添加固定分隔符(如换行符\n),客户端读取时按分隔符拆分内容,确保每次处理一条完整消息。
修改后服务端代码(关键部分)
// 第一条消息末尾加换行符 strcpy(buffer, "Hello, this is a test message...\n"); send(client_fd, buffer, strlen(buffer), 0); // 询问消息末尾加换行符 strcpy(buffer, "Please press Y/N\n"); send(client_fd, buffer, strlen(buffer), 0); // 结束消息末尾加换行符 strcpy(buffer, "Readyrrrrr\n"); send(client_fd, buffer, strlen(buffer), 0);
修改后客户端代码
import socket IP = "127.0.0.1" PORT = 4456 ADDR = (IP,PORT) SIZE = 1024 FORMAT = "utf-8" if __name__ == "__main__": print("This is client!!!!!") client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(ADDR) # 维护接收缓冲区,用于拼接不完整的消息 recv_buffer = "" def get_complete_message(): global recv_buffer # 循环读取直到找到分隔符 while '\n' not in recv_buffer: chunk = client.recv(SIZE).decode(FORMAT) if not chunk: return None # 连接断开 recv_buffer += chunk # 拆分出第一条完整消息,剩余内容留到下次处理 msg, recv_buffer = recv_buffer.split('\n', 1) return msg # 处理初始连接消息 data = get_complete_message() print(f"CONNECTED TO THE SERVER WITH MESSAGE {data}") print(data[-1]) while(True): data = get_complete_message() if not data: break print(f"[SERVER] {data}") if(data[-1] == "r"): break meg = input('press Y/N: ') client.send(meg.encode(FORMAT)) while(True): meg = input('To sever:') client.send(meg.encode(FORMAT)) if(meg == "Done"): client.close() break data = get_complete_message() if not data: break print(f"[SERVER] {data}")
2. 先发送消息长度,再发送内容
服务端先发送消息的字节长度(用固定长度的整数,如4字节),客户端先读取长度,再读取对应长度的字节内容,精准获取每条消息。
修改后服务端代码(关键部分)
// 封装带长度的消息发送函数 void send_with_length(int sock, const string& msg) { // 将长度转换为网络字节序(大端) uint32_t msg_len = htonl(msg.size()); send(sock, &msg_len, sizeof(msg_len), 0); // 发送实际消息内容 send(sock, msg.c_str(), msg.size(), 0); } // 使用示例 send_with_length(client_fd, "Hello, this is a test message..."); send_with_length(client_fd, "Please press Y/N"); send_with_length(client_fd, "Readyrrrrr");
修改后客户端代码
import socket import struct IP = "127.0.0.1" PORT = 4456 ADDR = (IP,PORT) SIZE = 1024 FORMAT = "utf-8" if __name__ == "__main__": print("This is client!!!!!") client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(ADDR) def get_complete_message(): # 先读取4字节的消息长度(网络字节序转主机字节序) len_data = client.recv(4) if not len_data: return None msg_len = struct.unpack('!I', len_data)[0] # 读取对应长度的消息内容 msg_bytes = b'' while len(msg_bytes) < msg_len: chunk = client.recv(min(msg_len - len(msg_bytes), SIZE)) if not chunk: return None msg_bytes += chunk return msg_bytes.decode(FORMAT) # 处理初始连接消息 data = get_complete_message() print(f"CONNECTED TO THE SERVER WITH MESSAGE {data}") print(data[-1]) while(True): data = get_complete_message() if not data: break print(f"[SERVER] {data}") if(data[-1] == "r"): break meg = input('press Y/N: ') client.send(meg.encode(FORMAT)) while(True): meg = input('To sever:') client.send(meg.encode(FORMAT)) if(meg == "Done"): client.close() break data = get_complete_message() if not data: break print(f"[SERVER] {data}")
3. 禁用Nagle算法(不推荐)
通过设置TCP_NODELAY选项禁用Nagle算法,减少小消息合并的概率,但无法从根本上解决流式协议的边界问题(网络拥塞时仍可能合并),仅作为临时缓解手段。
服务端设置
int flag = 1; setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag));
客户端设置
client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
内容的提问来源于stack exchange,提问作者Aom Sank
相关产品推荐
相关产品推荐

