粘包发生的场景
当应用程序使用TCP协议发送数据时,由于TCP是基于流式的数据协议,会将数据像水流一样粘在一起,当接收方的数据容量小于发送的数据时,如果不指定接收的数据长度,就会将所有的数据混合在一起,让接收的数据发生混乱。
如:
# 服务端代码:
# coding=utf-8
import subprocess
import socket
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 在链接异常终止后,再次启动会复用之前的IP端口,防止资源没有释放而产生地址冲突
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(('127.0.0.1',8080)) # 绑定的IP和端口
server.listen(5) # 参数表示最大可以挂起的连接数
while True: # 循环建立链接
conn,client_addr=server.accept() # 客户端的链接信息
while True: # 循环收发消息
try:
client_data=conn.recv(1024) # 表示最大收取的消息
res = subprocess.Popen(client_data.decode('utf-8'), # 将接收的命令交给shell执行
shell=True, # 并将返回的错误输出和标准输出输出到管道
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout=res.stdout.read()
stderr=res.stderr.read()
if not client_data: break # 如果收到的消息为空就跳出循环(主要针对在Linux系统上,客户端意外断开,
conn.send(stdout) # Linux的服务端出现无穷循环收空包的情况)
conn.send(stderr)
except ConnectionResetError: # 在 Windows系统上,客户端意外断开服务端会出现ConnectionResetError的异常
break
conn.close() # 关闭链接
server.close()
客户端:
import socket
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
send_data=input(">>: ").strip()
if not send_data: continue # 禁止输入空,防止死锁
client.send(send_data.encode('utf-8')) # 发送的文件为bytes类型
server_data=client.recv(1024)
print(server_data.decode('gbk')) # 在windows上,系统命令的返回结果为GBK格式
client.close()
上面的代码在Windows平台上执行tasklist
后再执行其它命令就会出现粘包现象,如下是执行结果:
>>: tasklist
映像名称 PID 会话名 会话# 内存使用
========================= ======== ================ =========== ============
System Idle Process 0 Services 0 4 K
System 4 Services 0 7,840 K
smss.exe 420 Services 0 424 K
csrss.exe 608 Services 0 1,384 K
wininit.exe 704 Services 0 2,112 K
services.exe 832 Services 0 4,576 K
lsass.exe 840 Services 0 10,804 K
svchost.exe 928 Services 0 10,888 K
svchost.exe 992 Services 0 6,836 K
svchost.exe 724 Services 0 15,076 K
svchost.exe 892 Services 0 87,968 K
svchost.
>>:
>>: dir
exe 1160 Services 0 30,812 K
svchost.exe 1280 Services 0 13,680 K
svchost.exe 1288 Services 0 16,064 K
svchost.exe 1296 Services 0 2,960 K
igfxCUIService.exe 1464 Services 0 3,696 K
DisplayLinkManager.exe 1744 Services 0 4,028 K
svchost.exe 2172 Services 0 17,288 K
ZhuDongFangYu.exe 2188 Services 0 7,660 K
svchost.exe 2296 Services 0 2,660 K
spoolsv.exe 2596 Services 0 9,756 K
dasHost.exe 2984 Services 0 6,864 K
ibtsiva.exe 2124 Services 0 1,420 K
capiws.exe 1976 Services 0 7,464 K
openvpnserv.exe
>>:
再次输入的命令后,会依然取出上次命令没有取完的结果(由于我们指定了接受收数据最大为固定的1024字节)。
TCP协议在传输数据的时候,为了提高效率,会启用Nagle算法,将多个较小,且间隔时间很短的两个数据包合并在一起发送,于是就会出现如下粘包现象:
# 服务端
# coding=utf-8
from socket import *
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8081))
server.listen(5)
conn,client_addr=server.accept()
data=conn.recv(10)
print(data)
data1=conn.recv(10)
print(data1)
conn.close()
server.close()
# 客户端
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8081))
client.send('Hello'.encode('utf8'))
client.send('World'.encode('utf8'))
client.close()
运行得到的结果为:
b'HelloWorld'
b''
无论是哪一种 情况,只要在收的时候指定长度,就可以避免此问题。
粘包问题的解决方案
如果知道每次服务端发送的数据长度,按照长指定的长度取数据就不会出现这种情况,对于过长的数据可以循环去取。可以按照如下方式:
- 数据的传输可以像TCP的传输模式一样,定制一个固定长度的报头,在报头中指定数据的长度,和其它信息,这样在接收端就可以根据固定长度的报头解析出后面数据的长度信息等内容。
- 如果使用一个固定长度的报头(一般使用struct模块),该模块可以把数字转成固定长度的bytes
- 但是,有时候如果传输比较大的数据,如好几百G的文件,在报头指定的数据长度和其它信息会超过固定长度(struct在
i
类型的情况下,取值的范围是 -2147483648 <= number <= 2147483647,也就是最大只能标识2Gb的文件长度),这就需要采用再次迭代的思想,在传输报头前先传输一个固定长度的报头长度描述信息(这个定义的报头信息一般很小,不会超过1Kb),接收端根据此固定长度的信息按指定的长度接收报头信息,再根据报头中的信息接收数据信息,实现传输的控制。
struct的应用示例:
import struct
res=struct.pack('i', 2147483647)
print(type(res),res,len(res))
res=struct.pack('i', 2)
print(type(res),res,len(res))
# 输出:
<class 'bytes'> b'\xff\xff\xff\x7f' 4
<class 'bytes'> b'\x02\x00\x00\x00' 4
# 可以看出,无论数据是'2147483647'还是'2',最终都转化为了4个字节长度
优化后的传输代码:
# 服务端
# coding=utf-8
import subprocess
import socket
import struct
import json
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 在链接异常终止后,再次启动会复用之前的IP端口,防止资源没有释放而产生地址冲突
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定的IP和端口
server.bind(('127.0.0.1', 8080))
# 参数5表示最大可以挂起的连接数
server.listen(5)
# 循环建立链接
while True:
# 客户端的链接信息
conn, client_addr = server.accept()
# 循环收发消息
while True:
try:
# 表示最大收取的消息
client_data = conn.recv(1024)
# 将接收的命令交给shell执行,并将返回的错误输出和标准输出输出到管道
res = subprocess.Popen(client_data.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = res.stdout.read()
stderr = res.stderr.read()
total_size = len(stdout) + len(stderr)
# 自定义报头信息
header = {'total_size': total_size, 'MD5': '123456', 'msg_type': 'cmd_res'}
# 将字典转化为json格式后才能被反解
header_json = json.dumps(header)
# 将json转为bytes用于传输
header_json_bytes = bytes(header_json, encoding='utf-8')
# 将header_json_bytes打包为固定的4个字节长度
header_size = struct.pack('i', len(header_json_bytes))
# 如果收到的消息为空就跳出循环(主要针对在Linux系统上,客户端意外断开,Linux的服务端出现无穷循环收空包的情况)
if not client_data: break
# 发送头长度信息,为4个字节
conn.send(header_size)
# 发送头信息
conn.send(header_json_bytes)
conn.send(stdout)
conn.send(stderr)
# 在 Windows系统上,客户端意外断开服务端会出现ConnectionResetError的异常
except ConnectionResetError:
break
conn.close() # 关闭链接
server.close()
# 客户端
import socket
import struct
import json
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
send_data=input(">>: ").strip()
if not send_data: continue # 禁止输入空,防止死锁
client.send(send_data.encode('utf-8')) # 发送的文件为bytes类型
header_size=client.recv(4)
header_json_lens=struct.unpack('i',header_size)[0]
header_json_bytes=client.recv(header_json_lens)
header_json=json.loads(header_json_bytes.decode('utf-8'))
total_size=header_json['total_size']
file_MD5=header_json['MD5']
print(file_MD5)
data_size=0
server_data=b''
while total_size > data_size:
server_data+=client.recv(1024)
data_size=len(server_data)
print(server_data.decode('gbk')) # 在windows上,系统命令的返回结果为GBK格式
client.close()
FTP小示例
# ftp-server.py
import subprocess
import socket
import struct
import json
import os
import hashlib
# 上传下载文件,通过read的方式读取bytes格式的文件.
ftp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 在链接异常终止后,再次启动会复用之前的IP端口,防止资源没有释放而产生地址冲突
ftp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定的IP和端口
ftp_server.bind(('127.0.0.1', 8080))
# 参数5表示最大可以挂起的连接数
ftp_server.listen(5)
Base_Dir = "D:\\temp\\"
# 循环建立链接
while True:
# 客户端的链接信息
conn, client_addr = ftp_server.accept()
# 循环收发消息
while True:
try:
# 从服务器下载文件到客户端
client_data = conn.recv(1024)
if not client_data: break
method = client_data.decode('utf-8').split()[0]
print(method)
filename = client_data.decode('utf-8').split()[1]
print(filename)
filename_path = Base_Dir + filename
print(filename_path)
if method == 'get': # 生成MD5
if not os.path.exists(filename_path):
conn.send("0000".encode('utf-8'))
continue
total_size = os.path.getsize(filename_path)
m = hashlib.md5()
with open(filename_path,'rb') as f:
for line in f:
m.update(line)
MD5 = m.hexdigest()
header = {'total_size': total_size, 'MD5': MD5, 'filename': filename}
header_json = json.dumps(header)
header_json_bytes = bytes(header_json, encoding='utf-8')
header_size = struct.pack('i', len(header_json_bytes))
conn.send(header_size)
conn.send(header_json_bytes)
with open(filename_path,'rb') as f1:
for line1 in f1:
conn.send(line1)
if method == 'upload':
header_size = conn.recv(4)
header_json_lens = struct.unpack('i', header_size)[0]
print(header_json_lens)
header_json_bytes = conn.recv(header_json_lens)
header_json = json.loads(header_json_bytes.decode('utf-8'))
total_size = header_json['total_size']
file_MD5 = header_json['MD5']
filename = header_json['filename']
filename_path = Base_Dir + filename
print(file_MD5)
data_size = 0
server_data = b''
with open(filename_path, 'ab') as f:
while total_size > data_size:
server_data = conn.recv(1024)
f.write(server_data)
data_size += len(server_data)
except ConnectionResetError:
break
conn.close() # 关闭链接
ftp_server.close()
# ftp-client.py
import socket
import struct
import json
import hashlib
import os
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
send_data = input(">>: ").strip()
if not send_data: continue # 禁止输入空,防止死锁
if send_data.upper() == 'Q': break
method = send_data.split()[0]
file_name = send_data.split()[1]
if method == 'get':
client.send(send_data.encode('utf-8')) # 发送的文件为bytes类型
header_size=client.recv(4)
if header_size.decode('utf-8') == '0000':
print("FTP Server上不存在此文件!")
continue
header_json_lens=struct.unpack('i',header_size)[0]
print(header_json_lens)
header_json_bytes=client.recv(header_json_lens)
header_json=json.loads(header_json_bytes.decode('utf-8'))
total_size=header_json['total_size']
file_MD5 = header_json['MD5']
filename = header_json['filename']
print(file_MD5)
data_size=0
server_data=b''
with open(filename,'ab') as f:
while total_size > data_size:
server_data = client.recv(1024)
f.write(server_data)
data_size += len(server_data)
elif method == 'upload':
if not os.path.exists(file_name):
print("文件不存在!")
continue
client.send(send_data.encode('utf-8')) # 发送的文件为bytes类型
total_size = os.path.getsize(file_name)
m = hashlib.md5()
with open(file_name, 'rb') as f:
for line in f:
m.update(line)
MD5 = m.hexdigest()
header = {'total_size': total_size, 'MD5': MD5, 'filename': file_name}
header_json = json.dumps(header)
header_json_bytes = bytes(header_json, encoding='utf-8')
header_size = struct.pack('i', len(header_json_bytes))
client.send(header_size)
client.send(header_json_bytes)
with open(file_name, 'rb') as f1:
for line1 in f1:
client.send(line1)
else:
print("没有此方法!")
continue
client.close()
网友评论