一、粘包
1.1 粘包现象只有在tcp协议中会出现,在udp协议中永远不会出现
tcp协议是面向流的协议,这也是容易产生粘包问题的原因
所谓粘包问题只要你还是因为接受方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的
此外,发送方引起的粘包是由于TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。如果连续几次需要发送的数据都很少,通常TCP会根据优化算法将这些数据合成一个TCP段后一次发送出去,这样接受方就收到了粘包数据
TCP是面向连接,面向流的,提供高可靠性服务。收发两端都要有一一成对的socket,
因此,发送端为了将多个发往接受端的包,更有效的发到对方,使用了Nagle算法,
将多次间隔较小、数据量小的数据 合并成一个大的数据库块,然后进行封包。
这样,接受端就难于分辨出来了,必须提供科学的拆包机制。
即面向流的通信是无消息保护边界的。
UDP协议是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,
由于 UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用的是
链式的结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)
这样,对于接收端来说,就容易区分了。
即面向消息的通信是有消息保护边界的。
TCP是基于数据的,于是发送的消息不能为空,
这就需要在客户端和服务端都添加空消息处理机制防止程序卡住;
而UDP是基于数据报的,即便数据内容为空,UDP协议也会帮助封装上消息头
1.2 两种粘包的情况
① 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据很小,会合到一起,产生粘包)
② 接受方不及时接受缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只接收了一小部分,服务端下次再收的时候还是从缓冲区拿走上次遗留的数据,产生粘包)
1.3 拆包发生的情况
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去
补充:send(字节流)、 recv(1024) 、sendall
recv(1024)指的是从缓存里一次拿出1024个字节的数据
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么剩余数据丢失,用sendall就会循环调用send,数据不会丢失
二、解决粘包问题的方案
我们可以将报头做成字典,字典里面包含将要发送的真实数据的详细信息,使用json模块序列化字典后转成bytes类型,然后使用 struct模块将bytes数据的长度打包成4个字节("i"模式)
发送时:
先发送报头长度(struct打包后的数据)
再编码报头内容(bytes类型)然后发送
最后将真实的数据内容(转成bytes类型)发送出去
接收时:
先收报头长度,用struct.unpack取出数值(报头的字节个数)
根据取出来的长度接收指定长度的字节数(即取到报头内容),然后解码、反序列化
从反序列化的结果中取出数据的详细数据(字典),再根据字典中的信息(真实数据的长度)取指定长度的字节解码得到真实的数据
示例:连接服务端,发送指定让服务端执行,并将执行的结果返回
服务端:
import struct,json
import subprocess
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, client_addr = server.accept()
while True:
cmd = conn.recv(1024)
if len(cmd) == 0: break
print(f"cmd : {cmd.decode('utf-8')}")
obj = subprocess.Popen(cmd.decode('utf-8'),
shell = True,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE)
res1 = obj.stdout.read()
res2 = obj.stderr.read()
res = (res1 if res1 else res2)
header = {'data_size': len(res)}
header_json = json.dumps(header)
header_json_bytes = header_json.encode('utf-8')
conn.send(struct.pack('i', len(header_json_bytes))) # 发送报头长度
conn.send(header_json_bytes) # 发送报头内容(bytes类型)
conn.send(res.encode('utf-8')) # 发送真实的内容
conn.close()
客户端:
import json, struct
from socket import *
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'))
header = client.recv(4)
header_json_length = struct.unpack('i', header)[0]
header_json = recv(header_json_length)
data_length = header_json['data_size']
recv_size = 0
data_recv = b''
while recv_size < data_length:
data = client.recv(1024)
data_recv += data
recv_size += len(data)
print(recv_data.decode('gbk')) # windows默认返回gbk编码的结果