粘包现象
程序员文章站
2022-06-08 20:39:44
一、subprocess 注:如果是Windows,那么res.stdout.read()读出的是GBK编码的信息,在接收端需要用GBK解码且只能从管道里读一次结果,PIPE称为管道。 二、粘包现象 1. TCP会粘包,UDP永远不会粘包 发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两 ......
一、subprocess
import subprocess cmd = input("请输入指令>>>") res = subprocess.popen( cmd, # 字符串指令:"dir", "ipconfig"等 shell=true, # 使用shell,就相当于使用cmd窗口 stderr=subprocess.pipe, # 标准错误输出,凡是输入错误指令,错误指令输出的报错信息就会被它拿到 stdout=subprocess.pipe, # 标准输出,正确指令的输出结果被它拿到 ) print(res.stdout.read()) print(res.stderr.read())
注:如果是windows,那么res.stdout.read()读出的是gbk编码的信息,在接收端需要用gbk解码且只能从管道里读一次结果,pipe称为管道。
二、粘包现象
1. tcp会粘包,udp永远不会粘包
发送端可以是一k一k地发送数据,而接收端的应用程序可以两k两k地提走数据,当然也有可能一次提走3k或6k数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此tcp协议是面向流的协议,这也是容易出现粘包问题的原因。而udp是面向消息的协议,每个udp段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和tcp是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,tcp协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。 例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束 所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。 此外,发送方引起的粘包是由tcp协议本身造成的,tcp为提高传输效率,发送方往往要收集到足够多的数据后才发送一个tcp段。若连续几次需要send的数据都很少,通常tcp会根据优化算法把这些数据合成一个tcp段后一次发送出去,这样接收方就收到了粘包数据。 1.tcp(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 2.udp(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于udp支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的udp包,在每个udp包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 3.tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略 udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠 tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包
2. udp是面向包的,不存在粘包现象
import socket import subprocess server = socket.socket(type=socket.sock_dgram) ip_port = ("127.0.0.1", 8001) server.bind(ip_port) while 1: cmd, addr = server.recvfrom(1024) cmd = cmd.decode("utf-8") if cmd == "q": break res = subprocess.popen( cmd, shell=true, stdout=subprocess.pipe, stderr=subprocess.pipe, ) server.sendto(res.stdout.read(), addr)
import socket client = socket.socket(type=socket.sock_dgram) ip_port = ("127.0.0.1", 8001) cmd = input(">>>").strip().encode("utf-8") client.sendto(cmd, ip_port) msg, addr = client.recvfrom(1024) print(msg.decode("gbk"))
结果:
报错原因为客户端设置接收的数大小小于消息包的大小,所以报错。
3. tcp粘包现象
1) 第一种:接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再接收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
服务端:
import socket import subprocess server = socket.socket() ip_port = ("127.0.0.1", 8001) server.bind(ip_port) server.listen() conn, addr = server.accept() while 1: cmd = conn.recv(1024).decode("utf-8") res = subprocess.popen( cmd, shell=true, stdout=subprocess.pipe, stderr=subprocess.pipe, ) conn.send(res.stdout.read())
客户端:
import socket client = socket.socket() ip_port = ("127.0.0.1", 8001) client.connect(ip_port) while 1: cmd = input(">>>").strip().encode("utf-8") client.send(cmd) msg = client.recv(1024) print(msg.decode("gbk"))
结果:
>>>ipconfig windows ip 配置 无线局域网适配器 本地连接* 2: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 dns 后缀 . . . . . . . : 以太网适配器 以太网: 连接特定的 dns 后缀 . . . . . . . : 本地链接 ipv6 地址. . . . . . . . : fe80::642e:112d:ce7:7ce4%14 ipv4 地址 . . . . . . . . . . . . : 192.168.12.55 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.12.254 以太网适配器 vmware network adapter vmnet1: 连接特定的 dns 后缀 . . . . . . . : 本地链接 ipv6 地址. . . . . . . . : fe80::89c5:67be:1c8f:1493%18 ipv4 地址 . . . . . . . . . . . . : 192.168.75.1 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 以太网适配器 vmware network adapter vmnet8: 连接特定的 dns 后缀 . . . . . . . : 本地链接 ipv6 地址. . . . . . . . : fe80::7d7e:c23:b290:1420%12 ipv4 地址 . . . . . . . . . . . . : 192.168.174.1 子网掩码 . . . . . . . . . . . . : 255.255.255.0 >>>dir 默认网关. . . . . . . . . . . . . : 无线局域网适配器 wlan: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 dns 后缀 . . . . . . . : 隧道适配器 本地连接* 11: 连接特定的 dns 后缀 . . . . . . . : ipv6 地址 . . . . . . . . . . . . : 2001:0:9d38:953c:8be:267a:3f57:f3c8 本地链接 ipv6 地址. . . . . . . . : fe80::8be:267a:3f57:f3c8%5 默认网关. . . . . . . . . . . . . : ::
由结果可知,在输入dir命令时,打印的还是上次ipconfig的数据,由此产生粘包
2) 第二种:发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起发送,产生粘包)
服务端:
import socket server = socket.socket() ip_port = ("127.0.0.1", 8001) server.bind(ip_port) server.listen() conn, addr = server.accept() from_client_msg1 = conn.recv(1024).decode("utf-8") from_client_msg2 = conn.recv(1024).decode("utf-8") print("from_client_msg1>>>", from_client_msg1) print("from_client_msg2>>>", from_client_msg2) conn.close() server.close()
客户端:
import socket client = socket.socket() server_ip_port = ("127.0.0.1", 8001) client.connect(server_ip_port) client.send(b"11") client.send(b"22") client.close()
结果:
如果两次发送有一定的时间间隔,那么就不会出现这种粘包情况。
三、粘包的解决方案
产生粘包现象的根源在于接收端不知道发送端要传送的字节流长度。
1. 方案一:
发送端发送数据之前,先将数据长度让接收端知晓,接收端根据总长度利用循环完成消息的接收。
服务端:
import socket import subprocess server = socket.socket() ip_port = ("127.0.0.1", 8001) server.bind(ip_port) server.listen() conn, addr = server.accept() while 1: from_client_cmd = conn.recv(1024).decode("utf-8") sub_obj = subprocess.popen( from_client_cmd, shell=true, stdout=subprocess.pipe, stderr=subprocess.pipe, ) cmd_res = sub_obj.stdout.read() conn.send(str(len(cmd_res)).encode("utf-8")) # 将数据长度发送给接收方 client_stutas = conn.recv(1024).decode("utf-8") # 接收接收方确认结果 if client_stutas == "ok": # 如果确认结果为"ok",则发送数据 conn.send(cmd_res) else: print("客户端长度信息没有收到")
客户端:
import socket client = socket.socket() server_ip_port = ("127.0.0.1", 8001) client.connect(server_ip_port) while 1: client_cmd = input("请输入系统指令>>>").strip().encode("utf-8") client.send(client_cmd) # 发送指令 from_server_datalen = client.recv(1024).decode("utf-8") # 接收数据长度信息 client.send(b"ok") # 发送确认信息 from_server_result = client.recv(int(from_server_datalen)) print(from_server_result.decode("gbk"))
结果:
请输入系统指令>>>ipconfig -all windows ip 配置 主机名 . . . . . . . . . . . . . : desktop-1e4joli 主 dns 后缀 . . . . . . . . . . . : 节点类型 . . . . . . . . . . . . : 混合 ip 路由已启用 . . . . . . . . . . : 否 wins 代理已启用 . . . . . . . . . : 否 无线局域网适配器 本地连接* 2: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 dns 后缀 . . . . . . . : 描述. . . . . . . . . . . . . . . : microsoft wi-fi direct virtual adapter 物理地址. . . . . . . . . . . . . : 16-6d-57-0c-4d-7e dhcp 已启用 . . . . . . . . . . . : 是 自动配置已启用. . . . . . . . . . : 是 以太网适配器 以太网: 连接特定的 dns 后缀 . . . . . . . : 描述. . . . . . . . . . . . . . . : realtek pcie gbe family controller 物理地址. . . . . . . . . . . . . : f0-de-f1-df-a4-fb dhcp 已启用 . . . . . . . . . . . : 是 自动配置已启用. . . . . . . . . . : 是 本地链接 ipv6 地址. . . . . . . . : fe80::642e:112d:ce7:7ce4%14(首选) ipv4 地址 . . . . . . . . . . . . : 192.168.12.55(首选) 子网掩码 . . . . . . . . . . . . : 255.255.255.0 获得租约的时间 . . . . . . . . . : 2018年11月22日 18:43:32 租约过期的时间 . . . . . . . . . : 2018年11月24日 14:41:01 默认网关. . . . . . . . . . . . . : 192.168.12.254 dhcp 服务器 . . . . . . . . . . . : 192.168.12.254 dhcpv6 iaid . . . . . . . . . . . : 217112305 dhcpv6 客户端 duid . . . . . . . : 00-01-00-01-23-50-f6-63-f0-de-f1-df-a4-fb dns 服务器 . . . . . . . . . . . : 202.96.134.33 202.96.128.86 tcpip 上的 netbios . . . . . . . : 已启用 以太网适配器 vmware network adapter vmnet1: 连接特定的 dns 后缀 . . . . . . . : 描述. . . . . . . . . . . . . . . : vmware virtual ethernet adapter for vmnet1 物理地址. . . . . . . . . . . . . : 00-50-56-c0-00-01 dhcp 已启用 . . . . . . . . . . . : 是 自动配置已启用. . . . . . . . . . : 是 本地链接 ipv6 地址. . . . . . . . : fe80::89c5:67be:1c8f:1493%18(首选) ipv4 地址 . . . . . . . . . . . . : 192.168.75.1(首选) 子网掩码 . . . . . . . . . . . . : 255.255.255.0 获得租约的时间 . . . . . . . . . : 2018年11月23日 14:40:54 租约过期的时间 . . . . . . . . . : 2018年11月23日 18:10:54 默认网关. . . . . . . . . . . . . : dhcp 服务器 . . . . . . . . . . . : 192.168.75.254 dhcpv6 iaid . . . . . . . . . . . : 587223126 dhcpv6 客户端 duid . . . . . . . : 00-01-00-01-23-50-f6-63-f0-de-f1-df-a4-fb dns 服务器 . . . . . . . . . . . : fec0:0:0:ffff::1%1 fec0:0:0:ffff::2%1 fec0:0:0:ffff::3%1 tcpip 上的 netbios . . . . . . . : 已启用 以太网适配器 vmware network adapter vmnet8: 连接特定的 dns 后缀 . . . . . . . : 描述. . . . . . . . . . . . . . . : vmware virtual ethernet adapter for vmnet8 物理地址. . . . . . . . . . . . . : 00-50-56-c0-00-08 dhcp 已启用 . . . . . . . . . . . : 是 自动配置已启用. . . . . . . . . . : 是 本地链接 ipv6 地址. . . . . . . . : fe80::7d7e:c23:b290:1420%12(首选) ipv4 地址 . . . . . . . . . . . . : 192.168.174.1(首选) 子网掩码 . . . . . . . . . . . . : 255.255.255.0 获得租约的时间 . . . . . . . . . : 2018年11月23日 14:40:53 租约过期的时间 . . . . . . . . . : 2018年11月23日 18:10:54 默认网关. . . . . . . . . . . . . : dhcp 服务器 . . . . . . . . . . . : 192.168.174.254 dhcpv6 iaid . . . . . . . . . . . : 604000342 dhcpv6 客户端 duid . . . . . . . : 00-01-00-01-23-50-f6-63-f0-de-f1-df-a4-fb dns 服务器 . . . . . . . . . . . : fec0:0:0:ffff::1%1 fec0:0:0:ffff::2%1 fec0:0:0:ffff::3%1 主 wins 服务器 . . . . . . . . . : 192.168.174.2 tcpip 上的 netbios . . . . . . . : 已启用 无线局域网适配器 wlan: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 dns 后缀 . . . . . . . : 描述. . . . . . . . . . . . . . . : qualcomm atheros ar9285 wireless network adapter 物理地址. . . . . . . . . . . . . : 44-6d-57-0c-4d-7e dhcp 已启用 . . . . . . . . . . . : 是 自动配置已启用. . . . . . . . . . : 是 隧道适配器 本地连接* 11: 连接特定的 dns 后缀 . . . . . . . : 描述. . . . . . . . . . . . . . . : microsoft teredo tunneling adapter 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-e0 dhcp 已启用 . . . . . . . . . . . : 否 自动配置已启用. . . . . . . . . . : 是 ipv6 地址 . . . . . . . . . . . . : 2001:0:9d38:953c:8be:267a:3f57:f3c8(首选) 本地链接 ipv6 地址. . . . . . . . : fe80::8be:267a:3f57:f3c8%5(首选) 默认网关. . . . . . . . . . . . . : :: dhcpv6 iaid . . . . . . . . . . . : 83886080 dhcpv6 客户端 duid . . . . . . . : 00-01-00-01-23-50-f6-63-f0-de-f1-df-a4-fb tcpip 上的 netbios . . . . . . . : 已禁用 请输入系统指令>>>dir 驱动器 e 中的卷没有标签。 卷的序列号是 a473-0aca e:\python_个人\day 028 粘包现象\第二种 的目录 2018/11/23 17:51 <dir> . 2018/11/23 17:51 <dir> .. 2018/11/23 17:51 500 客户端.py 2018/11/23 17:47 810 服务端.py 2 个文件 1,310 字节 2 个目录 123,297,423,360 可用字节 请输入系统指令>>>
此时,数据之间就不存在粘包现象了。
2. 方案二:
通过struck模块将需要发送的内容的长度进行打包成一个4字节长度的数据发送给接收端,接收端只要取出前4个字节,然后对这个字节的数据进行解包,拿到你要发送的内容的长度,然后通过这个长度来继续接收我们实际要发送的内容。
1. struct模块
对python基本类型值与用python字符串格式表示的c struct类型间的转化。
format | c type | python type | standard size | notes |
x | pad type | no value | ||
c | char | string of length 1 | 1 | |
b | char | integer | 1 | (3) |
b | unsigned char | integer | 1 | (3) |
? | _bool | bool | 1 | (1) |
h | short | integer | 2 | (3) |
h | unsigned short | integer | 2 | (3) |
i | int | integer | 4 | (3) |
i | unsigned int | integer | 4 | (3) |
l | long | integer | 4 | (3) |
l | unsigned long | integer | 4 | (3) |
q | long long | integer | 8 | (2),(3) |
q | unsigned long long | integer | 8 | (2),(3) |
f | float | float | 4 | (4) |
d | double | float | 8 | (4) |
s | char[] | string | ||
p | char[] | string | ||
p | void * | integer | (5),(3) |
- pack(format, value):打包,对于int类型,只能打包[-2147483648,2147483647]范围内数据,否则报错struct.error错误。对于int类型,打包后得到是4字节的bytes类型
- unpack(format, string):解包,将bytes类型转化为format对应的类型,返回的结果是元组
import struct a = 10 print(struct.pack("i", a)) # b'\n\x00\x00\x00' b = b'\n\x00\x00\x00' print(struct.unpack("i", b)) # (10,)
服务端:
import socket import subprocess import struct server = socket.socket() ip_port = ("127.0.0.1", 8001) server.bind(ip_port) # 绑定端口 server.listen() # 监听 conn, addr = server.accept() # 等待连接 while 1: from_client_cmd = conn.recv(1024).decode("utf-8") # 接收消息 sub_obj = subprocess.popen( from_client_cmd, shell=true, stdout=subprocess.pipe, stderr=subprocess.pipe, ) cmd_res = sub_obj.stdout.read() # 得到的是bytes类型 data_len = len(cmd_res) # 数据长度 print(data_len) # 打印长度 # 将真实数据长度打包成4个字节的数据 struct_data_len = struct.pack("i", data_len) # 将长度信息和数据内容发送给客户端 conn.send(struct_data_len + cmd_res)
客户端:
import socket import struct client = socket.socket() server_ip_port = ("127.0.0.1", 8001) client.connect(server_ip_port) # 连接 while 1: client_cmd = input("请输入系统指令>>>").strip() client.send(client_cmd.encode("utf-8")) # 发送消息 # 先接收4个字节,得到数据内容长度 recv_data_len = client.recv(4) # 将4个字节长度的数据,解包成后面真实数据的长度 real_data_len = struct.unpack("i", recv_data_len)[0] print(real_data_len) # 打印长度 server_result = client.recv(real_data_len) # 接收数据内容 print(server_result.decode("gbk"))
下一篇: WEB端自适应尺寸的方法