注:只有TCP有粘包问题,而UDP永远不会粘包
TCP(transport control protocol,传输控制协议): 面向连接的,面向流的,提供高可靠性服务。收发两端都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议): 是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
1. 粘包产生的原因
简单来说,粘包问题主要是因为接收方不知道消息之间的界限,不知道一个消息要提取多少字节的数据所造成的。
发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发出去,这样接收方就收到了粘包数据。
2. 粘包情况
粘包情况一: 发送端要等缓冲区满才发送出去,如果每次send的数据量少且时间间隔短,就会合到一块去,造成粘包。
服务端代码:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8000))
s.listen(5)
conn, addr = s.accept()
rec1 = conn.recv(1024)
rec2 = conn.recv(1024)
print("第一条信息:", str(rec1, 'utf8'))
print("第二条信息:", str(rec2, 'utf8'))
conn.close()
s.close()
客户端代码:
import socket
c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
c.connect(('127.0.0.1', 8000))
c.send(b'hello')
c.send(b'world')
c.close()
服务端打印的信息是:
第一条信息:helloworld
第二条信息:
粘包情况二: 客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候不是从缓冲区拿上次遗留的数据,产生粘包。
服务端代码:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8000))
s.listen(5)
conn, addr = s.accept()
rec1 = conn.recv(2)
rec2 = conn.recv(3)
print("第一条信息:", str(rec1, 'utf8'))
print("第二条信息:", str(rec2, 'utf8'))
conn.close()
s.close()
客户端代码不变,服务端打印的信息为:
第一条信息:hel
第二条信息:lo
3. 解决粘包问题
目前比较合理的处理方法是:
为字节流加上一个报头,将这个报告做成字典,字典里包含将要发送的真实数据详细信息。
将这个字典JSON序列化,然后用struck将序列化后的数据长度打包成4个字节(4个字节完全够用)
注:struct是用来将整型的数字转成固定长度的bytes
服务端代码:
import socket
import struct
import json
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
conn, addr = server.accept()
while True:
try:
client_cmd = str(conn.recv(1024), 'utf8')
if client_cmd == 'exit':
break
send_data1 = input('输入发送的数据1>>>')
send_data2 = input('输入发送的数据2>>>')
#制作报头
header_dic = {
"send_data1" : len(send_data1),
"send_data2" : len(send_data2),
}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('utf-8')
#用struct将报头长度转成固定的4字节长度
header_size = len(header_bytes)
conn.send(struct.pack('i', header_size))
#发送报头
conn.send(header_bytes)
#发送数据
conn.send(bytes(send_data1, "utf8"))
conn.send(bytes(send_data2, "utf8"))
except ConnectionResetError:
break
conn.close()
server.close()
客户端代码:
import socket
import json
import struct
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8000))
while True:
cmd = input('>>:').strip()
if cmd == 'exit':
break
client.send(bytes(cmd,'utf8'))
#接收报文长度
header_size = struct.unpack('i', client.recv(4))[0]
#接收报文
header_bytes = client.recv(header_size)
#解析报文
header_json = header_bytes.decode('utf-8')
header_dic = json.loads(header_json)
#获取真实数据的长度
data1_len = header_dic['send_data1']
data2_len = header_dic['send_data2']
#获取数据
rec_data1 = client.recv(data1_len)
rec_data2 = client.recv(data2_len)
print(str(rec_data1, 'utf8'))
print(str(rec_data2, 'utf8'))
client.close()
注:先运行服务端,再运行客户端。在客户端输入除"exit"以外的任何内容,服务端都可以开始输入要传送的消息。
本例服务端连续回复2条消息,为了测试粘包问题是否真正解决。