粘包现象

Posted ipython-201806

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了粘包现象相关的知识,希望对你有一定的参考价值。

一、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‘
x00x00x00‘
b = b
x00x00x00
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端

 

以上是关于粘包现象的主要内容,如果未能解决你的问题,请参考以下文章

socket网络编程:粘包现象以及解决方法(代码完善)

网络编程基础之粘包现象

详解啥是 TCP 粘包和拆包现象并演示 Netty 是如何解决的

透过现象看本质,我找到了Netty粘包与半包的这几种解决方案。

Netty进阶——粘包与半包(现象分析)

Netty进阶——粘包与半包(现象分析)