You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 23:30:53