欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

粘包现象

程序员文章站 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)
server端

 

粘包现象
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"))
client端

结果:

粘包现象

     报错原因为客户端设置接收的数大小小于消息包的大小,所以报错。

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())
server端

客户端:

粘包现象
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"))
client端

 结果:

粘包现象
>>>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()
server端

 客户端:

粘包现象
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()
client端

结果:

粘包现象

     如果两次发送有一定的时间间隔,那么就不会出现这种粘包情况。

三、粘包的解决方案

    产生粘包现象的根源在于接收端不知道发送端要传送的字节流长度。

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("客户端长度信息没有收到")
server端

客户端:

粘包现象
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"))
client端

结果:

粘包现象
请输入系统指令>>>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     
 char string of length 1  1  
 char integer  1  (3)
 unsigned char integer   1  (3)
?  _bool  bool  1  (1)
h  short  integer  2  (3)
 unsigned short  integer  2  (3)
 int  integer  4  (3)
 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)
server端

 客户端:

粘包现象
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"))
client端