Socket的粘包和分包的处理

TCP Socket的粘包和分包的处理

概述

在进行TCP socket开发时,都需要处理数据包粘包和分包的情况。解决方法在应用层下,定义一个协议:消息头部+消息长度+消息正文即可。

只有TCP有粘包问题,而UDP永远不会粘包。socket收发消息原理:

服务端可以1kb,1kb的向客户端发送数据,客户端的应用程序可以在缓存当中2kb,2kb地取走数据,当然也可以更多,或更少。也就是说,应用程序看到的数据是个整体。或者说是一个流。一条消息有多少个字节对应用程序是可见的,tcp协议是面向流的协议,这就是它容易粘包的问题原因。

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一个消息要提取多少字节的数据所造成的。

此外,发送方引起的粘包是由tcp协议本身造成的,tcp为提高传输效率,发送方往往收集到足够多的数据才发送一个tcp段。若连续几次需要发送的数据都很少,通常tcp会根据nagle优化算法 ,把这些数据合成一个tcp段后发送出去,这样接收方就收到了粘包数据。

粘包情况一:发送端要等缓冲区满才发送出去(即发送的数量少且时间间隔短,会合并到一起),造成粘包

1、服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from socket import *

server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)

conn,client_addr=server.accept()

res1=conn.recv(1024)
print('第一次:',res1)
res2=conn.recv(1024)
print('第二次:',res2)

conn.close()
server.close()

2、客户端

1
2
3
4
5
6
7
8
from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))

client.send(b'hello')
client.send(b'world')
client.close()

先启动服务端,后再启动客户端,服务端得到的结果为:

1
2
3
[root@seafile:~]# python3 server.py 
第一次: b'helloworld'
第二次: b''

可以看出客户端是发了两次数据而服务端一次就接收完了。

粘包情况二:客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候是从缓冲区拿上次遗留的数据,产生粘包。

情况一的,客户端不变,服务端略作修改,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from socket import *

server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)

conn,client_addr=server.accept()

res1=conn.recv(2)
print('第一次:',res1)
res2=conn.recv(3)
print('第二次:',res2)

conn.close()
server.close()

先启动服务端,后再启动客户端,服务端得到的结果为:

1
2
3
[root@seafile:~]# python3 server.py 
第一次: b'hel'
第二次: b'lowor'

现在我们来想一下如何处理粘包的方法。粘包的问题根源是接收端不知发送端将要传送的字节流,所以我们要让发送端在发送数据前,把要发送的字节流总大小让接收端知晓,然后接收端来个循环将其全部接收。这种方法比较低级,存在的问题:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

刚才上面 在发送消息之前需先发送消息长度给对端,还必须要等对端返回一个ready收消息的确认,不等到对端确认就直接发消息的话,还是会产生粘包问题(承载消息长度的那条消息和消息本身粘在一起)。 有没有优化的好办法么?

思考一个问题,为什么不能在发送了消息长度(称为消息头head吧)给对端后,立刻发消息内容(称为body吧),是因为怕head 和body 粘在一起,所以通过等对端返回确认来把两条消息中断开。

可不可以直接发head + body,但又能让对端区分出哪个是head,哪个是body呢?我靠、我靠,感觉智商要涨了。

想到了,把head设置成定长的呀,这样对端只要收消息时,先固定收定长的数据,head里写好,后面还有多少是属于这条消息的数据,然后直接写个循环收下来不就完了嘛!唉呀妈呀,我真机智。

可是、可是如何制作定长的消息头呢?假设你有2条消息要发送,第一条消息长度是 3000个字节,第2条消息是200字节。如果消息头只包含消息长度的话,那两个消息的消息头分别是

1
len(msg1) = 4000 = 4字节 len(msg2) = 200 = 3字节

你的服务端如何完整的收到这个消息头呢?是recv(3)还是recv(4)服务器端怎么知道?

目前比较合理的处理方法是:为字节流加上一个报头,将这个报头做成字典,字典里包含将要发送的真实数据详细信息。将这个字典JSON序列化,然后用struck将序列化后的数据长度打包成4个字节(4个字节完全够用)

发送时:先发报头长度,再编码报头内容后发送,最后发真实数据。

接收时:用struct取出报头长度,然后根据长度取出报头,解码,反序列化。最后从反序列化的结果中取出待取数据的详细信息,然后取真实的数据内容。

来看实现的代码:

1、服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from socket import *

import subprocess
import struct
import json

server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn,client_addr=server.accept()
print(conn,client_addr)#(连接对象,客户端的ip和端口)
while True:
try:
cmd=conn.recv(1024)
obj=subprocess.Popen(
cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout=obj.stdout.read()
stderr=obj.stderr.read()
#1、制作报头
header_dic={
'total_size':len(stdout)+len(stderr),
'md5':'dgdsfsdfdsdfsfewrewge',
'file_name':'a.txt'
}
header_json=json.dumps(header_dic)
header_bytes=header_json.encode('utf-8')
#2、先发送报头的长度
header_size=len(header_bytes)
conn.send(struct.pack('i',header_size))
#3、发送报头
conn.send(header_bytes)
#4、发送真实的数据
conn.send(stdout)
conn.send(stderr)

except ConnectionResetError:
break
conn.close()
server.close()

2、客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from socket import *
import json
import struct

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))

while True:
cmd=input(">>:").strip()
if not cmd:continue
client.send(cmd.encode('utf-8'))

#1、接收报文头的长度
header_size=struct.unpack('i',client.recv(4))[0]
#2、接收报文
header_bytes=client.recv(header_size)

#3、解析报文
header_json=header_bytes.decode('utf-8')
header_dic=json.loads(header_json)
print(header_dic)

#4、获取真实数据的长度
totol_size=header_dic['total_size']

#5、获取数据
recv_size=0
res=b''
while recv_size<totol_size:
recv_date=client.recv(1024)
res+=recv_date
recv_size+=len(recv_date)

print(res.decode('gbk'))
client.close()

补充struct模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import struct
#struct是用来将整型的数字转成固定长度的bytes.
import json

header_dic={
'total_size':32322,
'md5':'gdssfsfsdfsf',
'filename':'a.txt'

}
#1、将报头字典序列化。
header_json=json.dumps(header_dic)
#2、将序列后的字典转成字节
header_bytes=header_json.encode('utf-8')
#3、获取序列的字字典转成字节的个数
header_size=len(header_bytes)
print(header_size)
#4、将这个个数转成固字长度的字节表示
obj=struct.pack('i',header_size)
print(obj,len(obj))
#、这个固定长度的字节经过反转后是一个元组。
res=struct.unpack('i',obj)
#、通过按索取值就可等到报头字典长度。
header_size=res[0]

原文链接:https://blog.csdn.net/miaoqinian/article/details/80020291