粘包问题
粘包问题
(1)粘包问题介绍
-
粘包问题是在计算机网络中,特别是在使用面向流的传输协议(例如TCP)时经常遇到的一种情况。它主要涉及到数据在传输过程中的组织和接收问题。
-
当使用TCP协议进行数据传输时,发送方往往会将要传输的数据切分成小的数据块,并通过网络发送给接收方。然而,底层的网络传输通常并不保证接收方能够按照发送方的数据块边界精确地接收数据。这就可能导致多个小数据包在传输过程中被合并成一个大的数据块,这种情况被称为粘包问题。
(2)解决办法
- 粘包问题可能会导致接收方难以正确解析和处理接收到的数据,因为数据的边界变得模糊不清。为了解决粘包问题,通常采用以下一些方法:
- 消息定界符: 在数据包中使用特殊字符或序列作为消息的结束标志,接收方根据这个标志来切分数据包。
- 消息长度固定: 在每个数据包中包含消息的固定长度信息,接收方根据长度来切分数据包。
- 使用消息头: 在数据包的开头添加一个消息头,包含消息的长度等信息,接收方根据消息头来切分数据包。
TCP协议解决粘包问题基础
- 利用
struct
模块将传输过去的数据的总长度 打包 + 到头部进行发送
(1)服务端模版
from socket import socket import subprocess import struct # 创建套接字 server = socket() # 绑定地址和端口 server.bind(('127.0.0.1', 8090)) # 监听连接 server.listen(5) while True: # 接受客户端连接 conn, client_addr = server.accept() while True: # 接收客户端发送的命令 cmd_from_client = conn.recv(1024) # 如果接收到的数据长度为0,说明客户端已经断开连接,跳出循环 if len(cmd_from_client) == 0: break # 在子进程中执行客户端发送的命令 msg_server = subprocess.Popen(cmd_from_client.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # 读取执行成功的结果(stdout)和执行失败的结果(stderr) true_msg = msg_server.stdout.read() # 读取到执行成功的结果,是二进制数据类型 false_msg = msg_server.stderr.read() # 读取到执行失败的结果,是二进制数据类型 # 计算总结果的大小 total_size_from_server = len(true_msg) + len(false_msg) # 将总大小打包成4字节的二进制数据,并发送给客户端 total_size_from_server_pack = struct.pack('i', total_size_from_server) conn.send(total_size_from_server_pack) # 将执行成功的结果和执行失败的结果发送给客户端 conn.send(true_msg) conn.send(false_msg) # 关闭与客户端的连接 conn.close()
(2)客户端模版
from socket import socket import struct # 创建套接字 client = socket() # 连接到服务器 client.connect(('127.0.0.1', 8090)) while True: # 输入要发送的信息 msg = input('请输入信息:>>>').strip() # 如果消息为空,继续输入 if not msg: continue # 将消息编码成UTF-8并发送给服务器 msg = msg.encode('utf-8') client.send(msg) # 接收服务器发送的总结果大小,4字节的二进制数据 recv_total_size_msg = client.recv(4) # 解包得到总结果大小 recv_total_size = struct.unpack('i', recv_total_size_msg)[0] # 初始化已接收的数据大小 recv_size = 0 # 循环接收数据直到达到总结果大小 while recv_size < recv_total_size: # 接收服务器发送的部分结果,每次最多接收1024字节 msg_from_server = client.recv(1024) # 更新已接收的数据大小 recv_size += len(msg_from_server) # 将接收到的二进制数据解码成GBK编码的字符串并打印 msg_from_server = msg_from_server.decode('gbk') print(msg_from_server, end='') else: print('命令结束') # 关闭与服务器的连接 client.close()
TCP协议解决粘包问题进阶
- 通过json模式,模版修改参数直接套用
(1)服务端模版
from socket import socket import subprocess import struct import json # 创建套接字 server = socket() # 绑定地址和端口 server.bind(('127.0.0.1', 8085)) # 监听连接 server.listen(5) while True: # 接受客户端连接 conn, client_addr = server.accept() while True: # 接收客户端发送的命令 cmd_from_client = conn.recv(1024) print(f'来自客户端的消息:>>> {cmd_from_client.decode("utf-8")}') # 如果接收到的数据长度为0,说明客户端已经断开连接,跳出循环 if len(cmd_from_client) == 0: break # 在子进程中执行客户端发送的命令 msg_server = subprocess.Popen(cmd_from_client.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # 读取执行成功的结果(stdout)和执行失败的结果(stderr) true_msg = msg_server.stdout.read() # 读取到执行成功的结果,是二进制数据类型 false_msg = msg_server.stderr.read() # 读取到执行失败的结果,是二进制数据类型 # 计算总结果的大小 total_size_from_server = len(true_msg) + len(false_msg) # 构建消息头的字典 headers_dict = { 'file_name': 'a.txt', # 文件名 'total_size': total_size_from_server, # 总大小 'md5': 'md5' # MD5校验值 } # 将消息头转为JSON格式的字符串 json_data_str = json.dumps(headers_dict) # 将JSON字符串转为字节数据 json_data_bytes = json_data_str.encode('utf-8') # 将JSON数据的长度打包成4字节的二进制数据,并发送给客户端 json_data_size_pack = struct.pack('i', len(json_data_bytes)) conn.send(json_data_size_pack) # 发送JSON数据给客户端 conn.send(json_data_bytes) # 发送执行成功的结果和执行失败的结果给客户端 conn.send(true_msg) conn.send(false_msg) # 关闭与客户端的连接 conn.close()
(2)客户端模版
import json from socket import socket import struct # 创建套接字 client = socket() # 连接到服务器 client.connect(('127.0.0.1', 8085)) while True: # 输入要发送的信息 msg = input('请输入命令:>>>').strip() # 如果消息为空,继续输入 if not msg: print('消息不能为空') continue # 将消息编码成UTF-8并发送给服务器 msg = msg.encode('utf-8') client.send(msg) # 接收服务器发送的JSON数据大小,4字节的二进制数据 json_data_size_unpack = client.recv(4) # 解包得到JSON数据的大小 json_data_size = struct.unpack('i', json_data_size_unpack)[0] # 接收服务器发送的JSON数据,根据JSON数据大小 json_data_bytes = client.recv(json_data_size) # 将JSON数据解码成字符串 header_dict_str = json_data_bytes.decode('utf-8') # 将JSON字符串转为字典 header_dict = json.loads(header_dict_str) # 获取总结果的大小 recv_total_size = header_dict['total_size'] # 初始化已接收的数据大小 recv_size = 0 # 循环接收数据直到达到总结果大小 while recv_size < recv_total_size: # 接收服务器发送的部分结果,每次最多接收1024字节 msg_from_server = client.recv(1024) # 更新已接收的数据大小 recv_size += len(msg_from_server) # 将接收到的二进制数据解码成GBK编码的字符串并打印 msg_from_server = msg_from_server.decode('gbk') print(msg_from_server) else: print('命令结束') # 关闭与服务器的连接 client.close()
struct模块补充
struct
模块是 Python 标准库中的一个模块,用于处理 C 语言中的结构体(struct)和二进制数据的转换。它提供了一组函数,可以将二进制数据与 Python 数据类型进行相互转换。
以下是 struct
模块的主要函数:
-
struct.pack(format, v1, v2, ...)
:- 将数据按照指定的格式
format
打包成一个二进制字符串。v1
,v2
, ... 是要打包的数据。
- 将数据按照指定的格式
-
struct.unpack(format, string)
:- 将二进制字符串按照指定的格式
format
解包,返回一个包含解包后数据的元组。
- 将二进制字符串按照指定的格式
-
struct.calcsize(format)
:- 返回按照指定格式
format
打包的字符串的长度(字节数)。
- 返回按照指定格式
这些函数中的 format
参数是一个字符串,它指定了数据的布局和编码方式。常用的格式化字符包括:
'i'
: 表示一个有符号整数(4字节)。'I'
: 表示一个无符号整数(4字节)。'h'
: 表示一个有符号短整数(2字节)。'H'
: 表示一个无符号短整数(2字节)。'f'
: 表示一个单精度浮点数(4字节)。'd'
: 表示一个双精度浮点数(8字节)。
以下是一个简单的例子,演示了 struct.pack
和 struct.unpack
的基本用法:
import struct # 将数据打包成二进制字符串 packed_data = struct.pack('i', 1) # 打印打包后的二进制数据 print("Packed Data:", packed_data) # Packed Data: b'\x01\x00\x00\x00' # 将二进制字符串解包成数据 unpacked_data = struct.unpack('i', packed_data) # 打印解包后的数据 print("Unpacked Data:", unpacked_data) # Unpacked Data: (1,)
这个例子中,'i'
指定了打包和解包的格式,为一个有符号整数。
(1)补充
header
: 定义了一个头部字典,包含文件大小和文件名等信息。head_bytes
: 将头部字典通过json.dumps
转换为JSON格式的字符串,然后通过bytes
函数转换为字节串,使用UTF-8编码。head_len_bytes
: 获取头部字节串的长度,然后使用struct.pack
将长度打包成4字节的二进制数据。print
语句: 打印原始数据、JSON序列化后的数据和压缩后的数据。
import json import struct # 定义一个头部字典 header = {'file_size': 1073741824000, 'file_name': 'a.txt'} # 将头部字典转换为字节串 head_bytes = bytes(json.dumps(header), encoding='utf-8') # 获取头部字节串的长度,并将长度打包成4字节的二进制数据 head_len_bytes = struct.pack('i', len(head_bytes)) # 打印原始数据 print(f"这是原本的数据 :>>>> {header}") # 这是原本的数据 :>>>> {'file_size': 1073741824000, 'file_name': 'a.txt'} # 打印JSON序列化后的数据 print(f"这是JSON序列化后的数据 :>>>> {head_bytes}") # 这是JSON序列化后的数据 :>>>> b'{"file_size": 1073741824000, "file_name": "a.txt"}' # 打印压缩后的数据(头部字节串的长度) print(f"这是压缩后的数据 :>>>> {head_len_bytes}") # 这是压缩后的数据 :>>>> b'2\x00\x00\x00'
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通