网络编程之粘包问题
粘包问题
只有TCP有粘包现象,UDP永远不会粘包
什么是粘包
存在于客户端接收数据时,不能一次性收取全部缓冲区中的数据.当下一次再有数据来时,缓冲区中剩余的数据会和新的数据'粘连'在一起.这就是粘包现象。
## 什么是粘包? 存在于TCP/IP协议中数据粘连在一起。 ## socket中造成粘包现象的原因是什么? 客户端不能一次性接收完缓冲区里的所有数据,服务端接受客户端发送数据时,由于缓冲区没有存满,回先把数据'聚合'在客户端的缓冲区中,当客户端缓存区存满之后,在发给服务端。 ## 哪些情况会发生粘包现象? 1. 客户端多次send少量数据, 会在客户端send输出缓冲区堆积, 当缓冲区堆积到一定程度 .会一次性把缓冲区的数据发给服务端造成粘包现象 2. 客户端接收服务端发送的数据,会在客户端的recv输入缓存堆积,由于每次接收数据有限,当第二次服务端把数据发送过来时数据会堆积在客户端的输入缓冲区.造成粘包 3. 发送频率过快:发送方发送数据的速度过快,接收方可能无法及时接收数据,从而导致多个数据包合并在一起。 ## 解决粘包问题的方案有哪些? 1. 消息长度标识:发送方在每个数据包前添加消息长度信息,接收方根据消息长度来分割数据包,这样可以确保接收方正确地解析每个数据包。 2. 分隔符:发送方在数据包之间添加一种特殊的分隔符,接收方通过分隔符来切分数据包。 3. 应用层协议:通过在应用层定义协议,如固定长度的数据包格式、以JSON或XML格式打包数据等,可以避免粘包问题。
产生粘包现象:
### 客户端 import socket client=socket.socket() client.connect(('127.0.0.1',7777)) while 1: ui=input('请输入有命令:>>').strip() client.send(ui.encode('utf-8')) # 向服务端发送指令 ###***重点 # 客户端 send完之后, 就会执行recv() 等待接收数据 # 第一次 输入:dir指令 能够一次性接收完服务端返回的数据 # 第二次 输入:ipconfig指令 不能一次性接收完服务端返回的数据,部分残余数据放在缓冲区中 # 第三次 输入:dir指令时 由于上一次的返回的数据还没全部取完,新的指令数据还未到达缓冲区. # 总结: 如果缓冲区承载的数据过大时,每次只接收1024字节,当新的指令执行完,返回回来的数据会'怼'到缓冲区,造成数据粘连现象. 这就是粘包 ser_data=client.recv(1024) # 接收到服务端发送的字节码 print(ser_data.decode('utf-8')) # 解码 输出 client.close() ### 服务端 import socket import subprocess # 执行 系统cmd指令 server=socket.socket() server.bind(("127.0.0.1",7777)) # 绑定IP和端口 server.listen(5) while 1: conn, addr=server.accept() # 等待接收连接 print(conn,addr) while 1: try: cmd=conn.recv(1024) # 接收客户端发送过来的数据 cmd=cmd.decode('utf-8') # 将客户端发送过来的字节进行解码 # 执行客户端发来的系统命令 obj=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # 这是执行系统命令之后产生正确结果或错误结果, # 字节形式 result=(obj.stdout.read()+obj.stderr.read()).decode('gbk') conn.send(result.encode('utf-8') ) # 向客户端发送结果,字节形式 except Exception : break conn.close() server.close()
而基于UDP的命令执行程序是不存在粘包问题的:
服务端
import socket import subprocess ip_port = ('127.0.0.1', 9003) bufsize = 1024 udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_server.bind(ip_port) while True: # 收消息 cmd, addr = udp_server.recvfrom(bufsize) print('用户命令----->', cmd,addr) # 逻辑处理 res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE, stdin=subprocess.PIPE, stdout=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() # 发消息 udp_server.sendto(stdout + stderr, addr) udp_server.close()
客户端
from socket import * import time ip_port = ('127.0.0.1', 9003) bufsize = 1024 udp_client = socket(AF_INET, SOCK_DGRAM) while True: msg = input('>>: ').strip() if len(msg) == 0: continue udp_client.sendto(msg.encode('utf-8'), ip_port) data, addr = udp_client.recvfrom(bufsize) print(data.decode('utf-8'), end='')
- 当我们启动udp服务端后,由udp客户端向服务端发送两条数据
- 但是在udp服务端只接收到了一条数据
- 这是因为 udp 是报式协议,传送数据过程中会将数据打包直接发走,不会对数据进行拼接操作(没有Nagle算法)
解决粘包问题
方式一(recv工作原理):
recv 工作原理:
- 能够接收来自socket缓冲区的字节数据;
- 当缓冲区没有数据可以读取时,recv会一直处于阻塞状态,知道缓冲区至少有一个字节数据可取,或者客户端关闭;
- 关闭远程端并读取所有数据后,再recv会返回字符串。
简易版解决粘包:
### 客户端 import socket client=socket.socket() client.connect(('127.0.0.1',9989)) while 1: ui=input('请输入指令:>>>').strip() if len(ui)<0:continue if ui.upper()=='Q':break client.send(ui.encode('utf-8')) message_size=int(client.recv(1024).decode('utf-8')) client.send(b'recv_ready') recv_size=0 data=b'' while recv_size<message_size: data+=client.recv(1024) recv_size+=len(data) print(data.decode('utf-8')) ### 服务端 import socket import subprocess server=socket.socket() server.bind(('127.0.0.1',9989)) server.listen(5) while 1: conn,addr=server.accept() print(conn,addr) while 1: cmd=conn.recv(1024).decode('utf-8') obj=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result=(obj.stdout.read()+obj.stderr.read()).decode('gbk').encode('utf-8') conn.send(str(len(result)).encode('utf-8')) data=conn.recv(1024).decode('utf-8') if data=='recv_ready': conn.sendall(result) conn.close() server.close()
方式二(使用struct模块):
利用 struct
模块将传输过去的数据的总长度 打包 + 到头部进行发送
工作原理:
import struct # 'i' 的取值范围 -2147483648 <= number <= 2147483647 # i 模式 等长四位 , 将一个数字转化成等长度的bytes类型。 res=struct.pack('i',123456) print(res,len(res)) ### 通过unpack反解回来 data=struct.unpack('i',res) print(data) ### 但是通过struct 处理不能处理太大 ret = struct.pack('q', 4323241232132324) print(ret, type(ret), len(ret)) # 报错
lowB版解决粘包问题:
### 服务端 import socket import subprocess import struct phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1', 8080)) phone.listen(5) while 1: conn, client_addr = phone.accept() print(client_addr) while 1: try: cmd = conn.recv(1024) ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) correct_msg = ret.stdout.read() error_msg = ret.stderr.read() # 1 制作固定报头 total_size = len(correct_msg) + len(error_msg) header = struct.pack('i', total_size) # 2 发送报头 conn.send(header) # 发送真实数据: conn.send(correct_msg) conn.send(error_msg) except ConnectionResetError: break conn.close() phone.close() # 但是low版本有问题: # 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。 # 2,通过struct模块直接数据处理,不能处理太大。 ### 客户端 import socket import struct phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) while 1: cmd = input('>>>').strip() if not cmd: continue phone.send(cmd.encode('utf-8')) # 1,接收固定报头 header = phone.recv(4) # 2,解析报头 print(struct.unpack('i', header)) # 这是一个元组 (xxx,) total_size = struct.unpack('i', header)[0] # 取第一个元素 # 3,根据报头信息,接收真实数据 recv_size = 0 res = b'' while recv_size < total_size: recv_data = phone.recv(1024) res += recv_data recv_size += len(recv_data) print(res.decode('gbk')) phone.close()
方式三(再加上json):
工作原理:
### 步骤解释 # 报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节 #发送时: 1.先发报头长度 2.再编码报头内容然后发送 3.最后发真实内容 #接收时: 1.先手报头长度,用struct取出来 2.根据取出的长度收取报头内容,然后解码,反序列化 3.从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
懂哥版解决粘包问题:
### 客户端 import socket import struct import json client = socket.socket() # 实例化 socket对象 client.connect(('127.0.0.1', 8898)) # socket 连接IP和端口 while 1: ui = input('请输入指令:>>').strip() # 1. 发送 用户输入 指令 client.send(ui.encode('utf-8')) #2. 接收 struct 封装的头 字节形式 , 接收4个字节 head head = client.recv(4) # 3. struct反解 ,获得自定义报头的长度 dic_length = struct.unpack('i', head)[0] #4. 接收自定义字典报头内容 字节形式 head_dic = client.recv(int(dic_length)) #5. 反序列化自定义字典,先解码在反序列化 dic = json.loads(head_dic.decode('utf-8')) #6. 得到 真实内容的长度 content_length = dic['size'] # 7 设置一个字节变量 用于接收真实数据 content = b'' # 8 设置一个客户端接收长度 recv_size = 0 # 9 当客户端接收长度 小于 源数据长度,一直接收 while recv_size < content_length: # 累加 真实数据,以字节形式 content += client.recv(1024) # 累加 客户端接收的长度 recv_size += len(content) # 接收完毕,解码内容 print(content.decode('utf-8')) client.close() ### 服务端 import socket import struct import json import time import subprocess server = socket.socket() # 实例化 socket对象 server.bind(('127.0.0.1', 8898)) # 绑定ip和端口 server.listen(5) # 邦迪监听连接 while 1: conn, addr = server.accept() # 等待连接 print(conn, addr) # 打印连接,和连接ip while 1: try: # 1. 接收客户端发送的 指令 ,字节形式 cmd = conn.recv(1024) # 2. 执行指令,得到结果 , 指令是字符形式(需要解码) obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) #3.得到结果字节 ,转换成utf-8的字节格式 result = (obj.stdout.read() + obj.stderr.read()).decode('gbk').encode('utf-8') #4. 获得真实数据的字节长度 total_res_bytes = len(result) #5. 自定制字典报头 head_dic = { 'time': time.localtime(time.time()), 'size': total_res_bytes, # 字节长度 'MD5': 'XXXXX', 'file_name': '婚前视频', } # 6. 序列化字典 ,并将其转换成字节形式 head_dic_bytes = json.dumps(head_dic).encode('utf-8') #7. 使用 struct 封装报头字典head_dic_bytes ,固定长度(4个字节) # 封装成字节,发送给客户端,还是按照字节取出来. head = struct.pack('i', len(head_dic_bytes)) # 8 , 先将固定头发送给客户端 conn.send(head) # 9 . 再将自定制报头发送给客户端 conn.send(head_dic_bytes) # 10. 最后将真实结果发送给客户端 conn.send(result) ### 这里就是拼接字节 # 格式: 固定头 + 自定义报头 +真实数据 except Exception: break conn.close() server.close()
【补充】struct模块
struct.pack()
是Python内置模块struct
中的一个函数- 它的作用是将指定的数据按照指定的格式进行打包,并将打包后的结果转换成一个字节序列(byte string)
- 可以用于在网络上传输或者储存于文件中。
struct.pack(fmt, v1, v2, ...)
- 其中,
fmt
为格式字符串,指定了需要打包的数据的格式,后面的v1
,v2
,...则是需要打包的数据。 - 这些数据会按照
fmt
的格式被编码成二进制的字节串,并返回这个字节串。
- 其中,
fmt
的常用格式符如下:x
--- 填充字节c
--- char类型,占1字节b
--- signed char类型,占1字节B
--- unsigned char类型,占1字节h
--- short类型,占2字节H
--- unsigned short类型,占2字节i
--- int类型,占4字节I
--- unsigned int类型,占4字节l
--- long类型,占4字节(32位机器上)或者8字节(64位机器上)L
--- unsigned long类型,占4字节(32位机器上)或者8字节(64位机器上)q
--- long long类型,占8字节Q
--- unsigned long long类型,占8字节f
--- float类型,占4字节d
--- double类型,占8字节s
--- char[]类型,占指定字节个数,需要用数字指定长度p
--- char[]类型,跟s
一样,但通常用来表示字符串?
--- bool类型,占1字节
- 具体的格式化规则可以在Python文档中查看(链接)。
import json import struct # 为避免粘包,必须自定制报头 # 1T数据,文件路径和md5值 header = {'file_size': 1073741824000, 'file_name': '/a/b/c/d/e/a.txt', 'md5': '8f6fbf8347faa4924a76856701edb0f3'} # 为了该报头能传送,需要序列化并且转为bytes # 序列化并转成bytes,用于传输 head_bytes = bytes(json.dumps(header), encoding='utf-8') # 为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 # 这4个字节里只包含了一个数字,该数字是报头的长度 head_len_bytes = struct.pack('i', len(head_bytes)) print(f"这是原本的数据 :>>>> {header}") print(f"这是json序列化后的数据 :>>>> {head_bytes}") print(f"这是压缩后的数据 :>>>> {head_len_bytes}") # 这是原本的数据 :>>>> {'file_size': 1073741824000, 'file_name': '/a/b/c/d/e/a.txt', 'md5': '8f6fbf8347faa4924a76856701edb0f3'} # 这是json序列化后的数据 :>>>> b'{"file_size": 1073741824000, "file_name": "/a/b/c/d/e/a.txt", "md5": "8f6fbf8347faa4924a76856701edb0f3"}' # 这是压缩后的数据 :>>>> b'h\x00\x00\x00'
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!