PyQt5

PyQt5实现文件传输程序(六):TCP粘包问题

在测试程序运行时,出现了客户端向服务器发送的两次数据粘在了一起的现象(“粘包”),为了解决这一问题,查阅了一些资料,发现粘包现象在TCP通信当中普遍存在,所以写一篇文章来介绍一下这一问题以及解决方法。

一、什么时候会出现TCP粘包

  1. 如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。关闭连接主要要双方都发送close连接(参考tcp关闭协议)。如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself",然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。
  2. 如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
  3. 如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
    1)"hello give me sth abour yourself"
    2)"Don't give me sth abour yourself"
    那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hello give me sth abour yourselfDon't give me sth abour yourself" 这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。

二、TCP粘包的原因

在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows 网络编程)
1. 发送端需要等缓冲区满才发送出去,造成粘包
2. 接收方不及时接收缓冲区的包,造成多个包接收

三、如何解决TCP粘包问题

为了避免粘包现象,可采取以下几种措施。一是对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;二是对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;三是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。
下面主要介绍一种通过定义数据结构解决粘包问题的方法。

四、Python粘包问题解决方法:定义数据结构

定义数据结构来解决粘包问题的基本思路如下:首先每次发送都发送一个定长的报头长度(本例中为4字节),报头中包含操作命令,如果命令较为简单(如接收消息、注册、登录等)也可以包含消息、密码、账号等信息,但若是操作要求传输的数据较大(如传输文件),则在报头中则只写明接下来要接收的数据的长度。接收端接收到报头长度后根据报头长度接收报头,读取操作类型,并根据操作类型和报头中的数据长度接收剩下的数据。
解决这一问题主要使用了Python的struct模块,主要的方法是struct.packstruct.unpack

  • struct.pack用于将Python的值根据格式符,转换为字符串(因为Python中没有字节(Byte)类型,可以把这里的字符串理解为字节流,或字节数组)。
  • struct.unpack做的工作刚好与struct.pack相反,用于将字节流转换成python数据类型。它的函数原型为:struct.unpack(fmt, string),该函数返回一个元组。

根据上述思路,重写发送和接收函数。

# 将要发送的信息转化为报头发送
def ensend(self, c, data):
    # 制作报头
    head_json = json.dumps(data)  # json 序列化
    head_bytes = head_json.encode('utf-8')  # 要发送需要转换成字节数据

    # 发送报头的长度
    head_len = len(head_bytes)
    c.send(struct.pack('i', head_len))

    # 发送报头
    c.send(head_bytes)

# 将收到的信息转化为报头
def deread(self, c):
    # 接收报头长度
    head_struct = c.recv(4)
    head_len = struct.unpack('i', head_struct)[0]

    # 接收报头
    head_bytes = c.recv(head_len)
    head_json = head_bytes.decode('utf-8')
    head_dic = json.loads(head_json)

    return head_dic