3. 粘包现象演示:用 Python socket 模拟客户端连续发送数据,服务端接收时数据粘连

好,咱们直接动手。光讲理论不过瘾,这一节我带你用 Python 写个 demo,亲眼看看粘包到底长什么样。

你想想看,TCP 是个流式协议,数据就像水管里的水,没有明确的边界。客户端连续发了几条消息,服务端一收,可能全粘在一起了。这就是粘包。

3.1 先搭个最简单的服务端

我习惯先写服务端,再写客户端。这样调试起来方便。

# server.py
import socket

def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(('127.0.0.1', 8888))
    server_socket.listen(5)
    print('[服务端] 启动,监听 127.0.0.1:8888')

    conn, addr = server_socket.accept()
    print(f'[服务端] 收到客户端连接:{addr}')

    # 只收一次,看看能收到多少
    data = conn.recv(1024)
    print(f'[服务端] 一次 recv 收到的数据:{data}')
    print(f'[服务端] 数据长度:{len(data)} 字节')

    conn.close()
    server_socket.close()

if __name__ == '__main__':
    start_server()

这段代码很简单。服务端只调了一次 recv(1024),然后就把收到的数据打印出来。注意,这里我故意只收一次,就是为了看粘包效果。

3.2 再写个疯狂发送的客户端

客户端这边,我连续发送 3 条消息,每条之间不加任何延迟。

# client.py
import socket

def start_client():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(('127.0.0.1', 8888))

    messages = [b'Hello', b'World', b'Python']
    for msg in messages:
        client_socket.send(msg)
        print(f'[客户端] 发送:{msg}')

    client_socket.close()

if __name__ == '__main__':
    start_client()

先启动服务端,再启动客户端。看看输出。

服务端输出:

[服务端] 启动,监听 127.0.0.1:8888
[服务端] 收到客户端连接:('127.0.0.1', 54321)
[服务端] 一次 recv 收到的数据:b'HelloWorldPython'
[服务端] 数据长度:15 字节

看到了吗?客户端明明发了 3 条独立的消息:b'Hello'、b'World'、b'Python'。服务端一次 recv 全收进来了,变成了 b'HelloWorldPython'。3 条消息粘成了一整块。

这就是粘包。说白了,TCP 不关心你应用层怎么划分消息,它只保证字节流按顺序到达。

3.3 为什么会出现粘包?

我当年刚接触网络编程时,也纳闷过:为什么 TCP 不帮我把消息分开?

原因其实不复杂。TCP 是面向字节流的协议,它内部有缓冲区。发送端可能攒了一批数据才发出去,接收端也可能一次读走多个消息。具体来说,粘包有 3 个常见原因:

原因 说明
Nagle 算法 TCP 默认开启 Nagle 算法,会把多个小包合并成一个大的发送,提高网络利用率
接收端缓冲区 服务端 recv 缓冲区足够大,一次读取了多个消息
发送频率过高 客户端连续 send,数据在传输层被合并

嗯,这里要注意:粘包不是 TCP 的 bug,而是它的特性。UDP 就不会粘包,因为它是面向消息的,每个 sendto 对应一个独立的报文。

3.4 换个角度:看看数据是怎么流动的

我画了一张图,帮你理解数据从客户端到服务端的完整路径。

粘包现象:数据流动示意图 客户端 send(b'Hello') send(b'World') send(b'Python') TCP 传输层 Nagle 算法合并 缓冲区聚合 → 粘包形成 服务端 recv(1024) 收到: HelloWorldPython 关键点 • 客户端发送 3 条独立消息:Hello / World / Python • TCP 传输层可能合并这些消息,形成连续字节流 • 服务端一次 recv 收到全部数据,无法区分原始边界 • 这就是粘包:多条消息在接收端粘连在一起

从图里能看得很清楚。客户端发了 3 条消息,经过 TCP 传输层时被合并了,服务端一次全收走。你想想看,如果服务端不知道每条消息的长度,它怎么把 HelloWorldPython 拆回 3 条?

3.5 再深入一点:粘包 vs 拆包

我遇到过不少新手把粘包和拆包搞混。其实它们是两个不同的概念:

概念 定义 示例
粘包 多个消息在接收端合并成一个 收到 b'HelloWorld' 而不是 b'Hello' + b'World'
拆包 一个消息被拆分成多次接收 发送 b'HelloWorld',第一次 recv 收到 b'Hello',第二次收到 b'World'

拆包通常发生在消息体很大时,TCP 会分片传输。而粘包则多发生在小消息频繁发送时。两种现象都需要我们在应用层处理。

避坑指南: 我曾经在一个即时通讯项目里,客户端每输入一个字符就 send 一次,服务端那边收到的全是乱序粘在一起的字符串。后来加了消息头和长度字段才解决。所以,千万别指望 TCP 帮你维护消息边界。

3.6 怎么验证粘包?试试调整缓冲区

你可以做个实验。把服务端的 recv 缓冲区改小,比如改成 recv(5):

# 服务端修改 recv 大小
data = conn.recv(5)
print(f'收到:{data}')

再跑一次,你可能会看到:

[服务端] 收到:b'Hello'

只收到了 5 个字节。剩下的 b'WorldPython' 还在缓冲区里,下次 recv 才能读到。这就是拆包。但如果你把 recv 改回 1024,粘包又出现了。

所以,粘包和拆包是 TCP 的一体两面。你无法完全避免它们,只能通过应用层协议来正确处理。

3.7 小结一下

这一节我们用 Python 亲手演示了粘包现象。核心就一句话:TCP 是流式协议,不维护消息边界。客户端连续发送多条消息,服务端可能一次全收走,导致数据粘连。

下一节我们会讨论怎么解决这个问题。常见的方案有:固定长度、特殊分隔符、消息头+长度字段。每种方案都有适用场景,到时候我会结合项目经验给你讲透。

注意: 本节的 demo 只是为了演示粘包,生产环境千万别这么写。实际项目中必须设计消息边界协议,否则数据解析会出大问题。