概述

在进行TCP Socket开发时,都需要处理数据包粘包和分包的情况。本文详细讲解解决该问题的步骤。使用的语言是Python。

那什么是粘包和分包呢?

粘包:发送方发送两个字符串”hello”+”world”,接收方却一次性接收到了”helloworld”。

分包:发送方发送字符串”helloworld”,接收方却接收到了两个字符串”hello”和”world”。

虽然socket环境有以上问题,但是TCP传输数据能保证几点:

- 顺序不变。例如发送发送方发送hello,接收方也一定顺序受到hello,这个是TCP协议承诺的,因此这点成为我们解决分包、黏包问题的关键。

- 分割的包中间不会插入其他数据。

因此如果要使用socket通信,就一定要自己定义一份协议。目前最常用的协议标准是:包头+包体

包头的自定义

包头一般会包含协议版本号,指令,包体长度等数据,并且包头长度是固定的,包体是可变长的。下面是我自定义的一个包头:

版本号(ver)

包体长度(bodySize)

指令(cmd)

版本号,包体长度,指令数据类型都是无符号32位整型变量,于是这个包头长度固定为4×3=12字节。在Python由于没有类型定义,所以一般是使用struct模块生成包头。示例:

ver = 1
body = json.dumps(dict(hello="world"))
print(body)
cmd = 101
header = [ver, body.__len__(), cmd]
headPack = struct.pack("!3I", *header)

关于用自定义结束符分割数据包

有的人会想用自定义的结束符分割每一个数据包,这样传输数据包时就不需要指定长度甚至也不需要包头了。但是如果这样做,网络传输性能损失非常大,因为每一读取一个字节都要做一次if判断是否是结束符。所以建议还是选择包头+包体这种方式。

包体

包体的数据格式可以使用Json格式,这里一般是用来存放独特信息的数据。在下面代码中,我使用”{“hello”,”world”}”数据来测试。在Python使用json模块来生成json数据

Python示例

下面使用Python代码展示如何处理TCP Socket的粘包和分包。核心在于用一个接收缓冲区dataBuffer和一个小while循环来判断。

具体流程是这样的:把从socket读取出来的数据推入到dataBuffer后面(push),然后进入小循环,如果dataBuffer内容长度小于包头长度(bodySize),则跳出小循环继续接收;大于包头长度,则从缓冲区读取包头并获取包体的长度,再判断整个缓冲区是否大于包头+包体长度,如果小于则跳出小循环继续接收,如果大于则读取包体的内容,然后处理数据,最后再把这次的包头和包体从dataBuffer弹出(pop)。

下面用Markdown画了一个流程图。

Created with Raphaël 2.1.0

开始

等待数据到达

把数据push缓冲区

缓冲区小于

包头长度?

读取包头的内容

缓冲区小于包头

和包体的长度?

读取包体的内容

处理数据

从缓冲区pop数据

yes

no

yes

no

服务器端代码

# Python Version:3.5.1
import socket
import struct
HOST = ''
PORT = 1234
dataBuffer = bytes()
headerSize = 12
sn = 0
def dataHandle(headPack, body):
global sn
sn += 1
print("第%s个数据包" % sn)
print("ver:%s, bodySize:%s, cmd:%s" % headPack)
print(body.decode())
print("")
if __name__ == '__main__':
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen(1)
conn, addr = s.accept()
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
if data:
# 把数据存入缓冲区
dataBuffer += data
while True:
if len(dataBuffer) < headerSize:
print("数据包(%s Byte)小于包头长度,跳出小循环" % len(dataBuffer))
break
# 读取包头
# struct中:!代表Network order,3I代表3个unsigned int数据
headPack = struct.unpack('!3I', dataBuffer[:headerSize])
bodySize = headPack[1]
# 分包情况处理,跳出函数继续接收数据
if len(dataBuffer) < headerSize+bodySize :
print("数据包(%s Byte)不完整(总共%s Byte),跳出小循环" % (len(dataBuffer), headerSize+bodySize))
break
# 读取包体的内容
body = dataBuffer[headerSize:headerSize+bodySize]
dataHandle(headPack, body)
# 粘包情况的处理
dataBuffer = dataBuffer[headerSize+bodySize:]
测试服务器端的客户端代码
下面附上测试粘包和分包的客户端代码:
# Python Version:3.5.1
import socket
import time
import struct
import json
host = "localhost"
port = 1234
ADDR = (host, port)
if __name__ == '__main__':
client = socket.socket()
client.connect(ADDR)
# 正常数据包
ver = 1
body = json.dumps(dict(hello="world"))
print(body)
cmd = 101
header = [ver, body.__len__(), cmd]
headPack = struct.pack("!3I", *header)
sendData1 = headPack+body.encode()
# 分包测试
ver = 2
body = json.dumps(dict(hello="world2"))
print(body)
cmd = 102
header = [ver, body.__len__(), cmd]
headPack = struct.pack("!3I", *header)
sendData2_1 = headPack+body[:2].encode()
sendData2_2 = body[2:].encode()
# 粘包测试
ver = 3
body1 = json.dumps(dict(hello="world3"))
print(body1)
cmd = 103
header = [ver, body1.__len__(), cmd]
headPack1 = struct.pack("!3I", *header)
ver = 4
body2 = json.dumps(dict(hello="world4"))
print(body2)
cmd = 104
header = [ver, body2.__len__(), cmd]
headPack2 = struct.pack("!3I", *header)
sendData3 = headPack1+body1.encode()+headPack2+body2.encode()
# 正常数据包
client.send(sendData1)
time.sleep(3)
# 分包测试
client.send(sendData2_1)
time.sleep(0.2)
client.send(sendData2_2)
time.sleep(3)
# 粘包测试
client.send(sendData3)
time.sleep(3)
client.close()

服务器端打印结果

下面是测试出来的打印结果,可见接收方已经完美的处理粘包和分包问题了。

Connected by (‘127.0.0.1’, 23297)

第1个数据包

ver:1, bodySize:18, cmd:101

{“hello”: “world”}

数据包(0 Byte)小于包头长度,跳出小循环

数据包(14 Byte)不完整(总共31 Byte),跳出小循环

第2个数据包

ver:2, bodySize:19, cmd:102

{“hello”: “world2”}

数据包(0 Byte)小于包头长度,跳出小循环

第3个数据包

ver:3, bodySize:19, cmd:103

{“hello”: “world3”}

第4个数据包

ver:4, bodySize:19, cmd:104

{“hello”: “world4”}

数据包(0 Byte)小于包头长度,跳出小循环

在框架下处理粘包和分包

其实无论是使用阻塞还是异步socket开发框架,框架本身都会提供一个接收数据的方法提供给开发者,一般来说开发者都要覆写这个方法。下面是Twidted开发框架处理粘包和分包的示例,只上核心程序:

# Twiested
class MyProtocol(Protocol):
_data_buffer = bytes()
# 代码省略
def dataReceived(self, data):
"""Called whenever data is received."""
self._data_buffer += data
headerSize = 12
while True:
if len(self._data_buffer) < headerSize:
return
# 读取包头
# struct中:!代表Network order,3I代表3个unsigned int数据
headPack = struct.unpack('!3I', self._data_buffer[:headerSize])
# 获取包体长度
bodySize = headPack[1]
# 分包情况处理
if len(self._data_buffer) < headerSize+bodySize :
return
# 读取包体的内容
body = self._data_buffer[headerSize:headerSize+bodySize]
# 处理数据
self.dataHandle(headPack, body)
# 粘包情况的处理
self._data_buffer = self._data_buffer[headerSize+bodySize:]

后话

处理粘包和分包的C语言版本有时间再补充。