解决思路
粘包问题是由于TCP协议底层优化算法Nagle算法造成的。我们可以在发送数据包之前,先告诉接收方我们发送的数据量有多大,接收方就可以精确接收一个数据包或者对一个数据包进行多次接收,这样不仅能够享受到Nagle算法带来的便利,也能够有效解决粘包问题。
伪代码实现解决粘包
1、问题:一次性发送的数据长度未知,接收方不方便接收。
2、解决思路:发送方先说明发送的数据有多长,接收方再接收。
3、不足:告诉接收方的信息(我要发送的数据有多长)不是固定长度的,不符合协议规范。
服务端
# 第一步:把数据长度发送给客户端
total_size = len(stdout)+len(stdeer)
conn.send(str(total_size).encode("utf-8"))
# 第二步:再发送真实的数据
conn.send(stdout)
conn.send(stdeer)
客户端
# 协议 = 报头 + 数据
# 第一步:先收报头
total_size = 10241
# 第二步:接收真实的数据
recv_size = 0
recv_data = b""
while recv_size < total_size:
res = phone.recv(1024)
recv_data += res
recv_size += len(res)
print(recv_data.decode("utf-8"))
解决粘包问题简单版本
1、问题:告诉接收方的信息(我要发送的数据有多长)不是固定长度的,不符合协议规范。
2、解决办法:将关于数据长度的信息打包到报头里面,这需要用到一个模块:struct。其中struct.pack(“i”,int)方法可以将整数(数据长度)打包成定长的字节类型。
- 参数一:i表示打包的数据是整型,打包后返回值是一个定长为4的字节类型。
- 参数二:int表示想打包的一个整数类型(不管数据长度多少,打包后长度为4)。
解包用struct.unpack()方法。
3、代码实现
服务端
# 3、把命令的结果返回给客户端
# 第一步:制作固定长度的报头。
total_size = len(stdout) + len(stdeer)
header = struct.pack("i",total_size)
# 第二步:把报头(固定长度)发送给客户端
conn.send(header)
# 第二步:再发送真实的数据
conn.send(stdout)
conn.send(stdeer)
客户端
# 2、拿到命令的结果,并打印
# 第一步:先收报头
header = phone.recv(4)
# 第二步:从报头中解析出对真实数据的描述信息(数据的长度)
total_size = struct.unpack("i",header)[0]
# 第三步:接收真实的数据
recv_size = 0
recv_data = b''
while recv_size < total_size:
res = phone.recv(1024)
recv_data += res
recv_size += len(res)
print(recv_data.decode("gbk"))
4、还存在的问题:
- 报头信息少;还可能有文件名等其他文本信息。
- 打包数据可能很大;对于struct模块中的pack方法来说,如果将数据打包成整型(i),当传入第二个参数数值很大时,打包成整型将会报错。
解决粘包问题终极版本
1、解决思路:选用字典类型作为报头,考虑到网络传输数据需要将字典转换为字节类型,同时传输后还要将字节数据解析为字典(序列化),因此调用json模块。
2、传输过程中的数据类型变化:
字典类型的报头——>转换为json对象,为str类型(序列化)——>编码,转换为字节类型——>解码,为json对象——>反序列化,变成字典类型。
# 字典类型的数据头
header_dic = {
"filename": "a.txt",
"md5": "xxxdxxx",
'total_size': len(stdout)+len(stdeer)
}
# 将数据头转换为json对象,是str类型
header_json = json.dumps(header_dic)
# json对象转换为字节类型
header_bytes = header_json.encode('utf-8')
3、还有一个关键点,引入字典类型后,此时报头不是定长,不符合传输规范。那么,我们可以利用struct.pack()方法将字典报头打包成定长,接收方就可以通过解析包得到报头包含的信息,再去接收数据。
conn.send(struct.pack("i",len(header_bytes)))
优化后的完整代码
服务端
import socket
import subprocess
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",9910))
phone.listen(5)
print("starting...")
while True:
conn,client_addr = phone.accept()
print(client_addr)
while True:
# 1、收命令
cmd = conn.recv(8096)
if not cmd:break
# 2、执行命令,拿到结果
obj = subprocess.Popen(cmd.decode("utf-8"), shell=True,
stdout=subprocess.PIPE, # 正确结果丢到这个“管道”
stderr=subprocess.PIPE) # 错误结果丢入这个“管道”
stdout = obj.stdout.read()
stdeer = obj.stderr.read()
# 3、把命令的结果返回给客户端
# 第一步:制作固定长度的报头。
header_dic = {
"filename": "a.txt",
"md5": "xxxdxxx",
'total_size': len(stdout)+len(stdeer)
}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('utf-8')
# 第二步:先发送报头的长度
conn.send(struct.pack("i",len(header_bytes)))
# 第三步:再发报头
conn.send(header_bytes)
# 第四步:再发送真实的数据
conn.send(stdout)
conn.send(stdeer)
conn.close()
phone.close()
客户端
import socket
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",9910))
while True:
# 1、发命令
cmd = input(">>:").strip()
if not cmd:continue
phone.send(cmd.encode("utf-8"))
# 2、拿到命令的结果,并打印
# 第一步:先收报头的长度
obj = phone.recv(4)
header_size = struct.unpack('i',obj)[0]
# 第二步:再收报头
header_bytes = phone.recv(header_size)
# 第三步:从报头中解析出对真实数据的描述信息
header_json = header_bytes.decode("utf-8")
header_dic = json.loads(header_json)
print(header_dic)
total_size = header_dic["total_size"]
print(total_size,type(total_size))
# 第三步:接收真实的数据
recv_size = 0
recv_data = b''
while recv_size < total_size:
res = phone.recv(1024)
recv_data += res
recv_size += len(res)
print(recv_data.decode("gbk"))
phone.close()
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/122897.html