粘包问题
【一】概要
- 粘包问题是在网络通信中常见的一种情况,它指的是发送方发送的多个小数据包在传输过程中被接收方一次性接收,导致数据粘在一起,难以正确解析。粘包问题通常出现在基于流的传输协议(如TCP)中,因为这些协议将数据视为一串字节流而不是消息。
【二】常用方法
| sock.recv(10240) |
| |
| """到底分多少次接收完所有的数据?""" |
| 发送的总数据量字节大小 / 每次接收的最大字节数 = 总次数 |
| |
| import struct |
| res=struct.pack('i', 10240) |
| obj=len(res) |
| |
| struct.unpack('i', obj)[0] |
| |
| 在发送数据的时候,多发送四个字节的大小,这个四个字节代表了数据的总大小,客户端接收数据的时候,先接收4个字节、sock.recv(4), 对这四个字节解包、得到总数据大小,然后在接收这个四个字节之后的数据才是真实数据, |
【三】详解
【1】粘包问题
- 粘包问题常见于TCP流式协议中,而不常见于UDP报式协议,是因为报式协议将数据打包成一组数据
【1.1】流式协议粘包问题
| '''流式协议-服务端''' |
| import socket |
| |
| |
| server = socket.socket() |
| server.bind(('127.0.0.1', 8080)) |
| server.listen() |
| |
| conn, addr = server.accept() |
| print(conn.recv(1024)) |
| print(conn.recv(5)) |
| print(conn.recv(5)) |
| conn.close() |
| ''' |
| b'hellohellohello' |
| b'' |
| b'' |
| ''' |
| '''流式协议-客户端''' |
| import socket |
| |
| client = socket.socket() |
| |
| client.connect(('127.0.0.1', 8080)) |
| '''流式协议,短时间,客户端可以分多次发送数据给服务端,服务端一次性接收完数据''' |
| client.send(b'hello') |
| client.send(b'hello') |
| client.send(b'hello') |
| client.close() |
【1.2】解决方法一(根据长度设置接收的字节)
- 当你知道,每一条数据的字节长度,可以根据长度设置接收的字节数,但当传输视频数据或者文件大小较大的数据时,就不太现实了
- 理论上,是可以通过字节数来接收较大的数据的,但耗时耗力且容错低,一旦1个字节出问题,整个数据将无法正常接收
| '''流式协议-服务端''' |
| import socket |
| |
| |
| server = socket.socket() |
| server.bind(('127.0.0.1', 8080)) |
| server.listen() |
| |
| conn, addr = server.accept() |
| print(conn.recv(5)) |
| print(conn.recv(5)) |
| print(conn.recv(5)) |
| conn.close() |
| ''' |
| b'hello' |
| b'hello' |
| b'hello' |
| ''' |
| '''流式协议-客户端''' |
| import socket |
| |
| client = socket.socket() |
| |
| client.connect(('127.0.0.1', 8080)) |
| client.send(b'hello') |
| client.send(b'hello') |
| client.send(b'hello') |
| client.close() |
【1.3】解决办法二(通过struct模块)
【1.3.1】struct模块
- 通过
struct
模块,你可以将数据打包成二进制流或从二进制流中解包出数据。
常用方法
pack(format, v1, v2, ...)
:
- 将给定的数据根据指定的格式(format)打包成二进制数据。
- 例如:
struct.pack('Ihf', 42, 3.14, 2.718)
将会把一个整数、一个单精度浮点数和一个双精度浮点数打包成二进制数据。
unpack(format, buffer)
:
- 从二进制数据中解包出数据,返回一个包含解包数据的元组。
- 例如:
struct.unpack('Ihf', b'\x2a\x00\x00\x00\x8f\xc2r@z\xe1z@')
将解包出对应的整数、单精度浮点数和双精度浮点数。
calcsize(format)
:
- 返回一个字符串格式的大小,表示给定格式的 struct 所需的字节数。
- 格式化字符:
【1.3.2】解决思路
-
sturct.pack('i',int)
:打包出来的二进制数据,固定四个字节长度
-
在发送数据的时候,先将struct打包的四个字节出入,这个四个字节代表了数据的总大小,客户端接收数据的时候,先接收4个字节、sock.recv(4), 对这四个字节解包、得到总数据大小,然后在接收这个四个字节之后的数据才是真实数据,
| '''服务端''' |
| import struct |
| import socket |
| |
| server = socket.socket() |
| server.bind(('127.0.0.1', 8080)) |
| server.listen() |
| |
| conn, addr = server.accept() |
| |
| first_msg = conn.recv(1024) |
| print(first_msg.decode('utf8')) |
| |
| |
| send_msg = '红红火火恍恍惚惚红红火火恍恍惚惚会后悔' |
| |
| send_msg = send_msg.encode('utf8') |
| |
| pack = struct.pack('i', len(send_msg)) |
| |
| |
| conn.send(pack) |
| |
| conn.send(send_msg) |
| |
| conn.close() |
| '''客户端''' |
| import struct |
| import socket |
| |
| client = socket.socket() |
| client.connect(('127.0.0.1', 8080)) |
| |
| client.send(b'client') |
| |
| pack = client.recv(4) |
| |
| size_pack = struct.unpack('i', pack) |
| print(size_pack) |
| |
| |
| count = 0 |
| data = b'' |
| |
| while count < size_pack[0]: |
| txt = client.recv(5) |
| |
| |
| count += len(txt) |
| |
| data += txt |
| |
| print(data.decode('utf8')) |
| |
| client.close() |
【1.3.3】常见情况小练习(客户端下载服务端的数据)
| import json |
| import socket |
| import struct |
| import os |
| |
| IP = '127.0.0.1' |
| PORT = 8090 |
| |
| |
| server = socket.socket() |
| |
| server.bind((IP, PORT)) |
| |
| server.listen(2) |
| |
| BASE_DIR = r'D:\Files\Python全栈28期\教师笔记\day35_37_网络编程\day37\视频' |
| file_list = os.listdir(BASE_DIR) |
| file_dict = dict(enumerate(file_list, 1)) |
| send_file_list = '\n'.join(file_list) |
| conn, addr = server.accept() |
| conn.send(send_file_list.encode('utf8')) |
| while True: |
| msg = conn.recv(1024) |
| msg = msg.decode('utf8') |
| print(msg) |
| if msg == 'q': |
| conn.close() |
| break |
| if '【video_choice】' in msg: |
| msg = msg[14:] |
| if not (int(msg) in file_dict if msg.isdigit() else False): |
| conn.send('【Number_False】编号有误!'.encode('utf8')) |
| continue |
| choice_video = file_dict[int(msg)] |
| video_path = os.path.join(BASE_DIR, choice_video) |
| with open(video_path, 'rb') as f: |
| video_data = f.read() |
| data_dict = { |
| 'file_name': choice_video, |
| 'data_len': len(video_data), |
| 'BASE_DIR': BASE_DIR |
| } |
| data_json = json.dumps(data_dict) |
| data_bytes = data_json.encode('utf8') |
| data_pack = struct.pack('i', len(data_bytes)) |
| conn.send(data_pack) |
| conn.send(data_bytes) |
| conn.send(video_data) |
| |
| server.close() |
| import json |
| import os.path |
| import socket |
| import struct |
| |
| IP = '127.0.0.1' |
| PORT = 8090 |
| client = socket.socket() |
| client.connect((IP, PORT)) |
| |
| |
| while True: |
| msg = client.recv(1024) |
| msg = msg.decode('utf8') |
| print(f"服务端反馈信息:\n{msg}") |
| if "Number_False" in msg: |
| video_choice = input("输入有误!请重新输入视频编号:").strip() |
| video_choice = '【video_choice】' + video_choice |
| if len(msg) == 4 and isinstance(msg, bytes): |
| data_pack = client.recv(4) |
| recv_size = struct.unpack('i', data_pack)[0] |
| recv_start = 0 |
| data_json = '' |
| video_data = '' |
| while recv_start < recv_size: |
| msg_from_server = client.recv(1024) |
| recv_size += len(msg_from_server) |
| msg_from_server = msg_from_server.decode('utf8') |
| data_json = json.loads(msg_from_server) |
| break |
| while recv_size < data_json['data_len']: |
| msg_from_server = client.recv(1024) |
| recv_size += len(msg_from_server) |
| print(msg_from_server, end='') |
| video_choice = input(f"请输入拷贝的视频文件名【默认为{data_json['file_name']}_new】:").strip() |
| if len(video_choice) == 0: |
| video_choice = f"{data_json['file_name']}_new" |
| new_path = os.path.join(data_json['BASE_DIR'], video_choice, '.mp4') |
| with open(new_path, 'ab') as fp: |
| fp.write(msg_from_server) |
| true_msg = f"文件已拷贝,请在{new_path} 查看!" |
| client.send(true_msg.encode('utf8')) |
| break |
| elif "File_False" in msg: |
| video_choice = input("输入有误!请重新输入拷贝的视频文件名:").strip() |
| video_choice = '【filename】' + video_choice |
| else: |
| video_choice = input("请输入视频编号:").strip() |
| video_choice = '【video_choice】' + video_choice |
| client.send(video_choice.encode('utf8')) |
【2】为什么UDP不会出现粘包问题
- UDP并没有像TCP那样的字节流概念,它是基于数据报(Datagram)的,每个UDP报文都是一个独立的数据包,没有先后顺序的概念。
| '''服务端''' |
| import socket |
| |
| |
| IP = '127.0.0.1' |
| PORT = 8080 |
| |
| |
| server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| |
| |
| server.bind((IP, PORT)) |
| |
| |
| data, addr = server.recvfrom(1024) |
| print(data) |
| |
| |
| with open('【8.0】进程锁(互斥锁).mp4', 'rb') as f: |
| data = f.read() |
| server.sendto(data, addr) |
| '''超过大小,直接报错''' |
| |
| |
| |
| server.close() |
| '''客户端''' |
| import socket |
| |
| |
| IP = '127.0.0.1' |
| PORT = 8080 |
| |
| client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| |
| client.sendto(b'client', (IP, PORT)) |
| |
| data, addr = client.recvfrom(1024) |
| |
| with open('copy.mp4','wb') as f: |
| f.write(data) |
| |
| client.close() |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了