python TCP Socket的粘包和分包的处理详解


Posted in Python onFebruary 09, 2018

概述

在进行TCP Socket开发时,都需要处理数据包粘包和分包的情况。本文详细讲解解决该问题的步骤。使用的语言是Python。实际上解决该问题很简单,在应用层下,定义一个协议:消息头部+消息长度+消息正文即可。

那什么是粘包和分包呢?

关于分包和粘包

粘包:发送方发送两个字符串”hello”+”world”,接收方却一次性接收到了”helloworld”。

分包:发送方发送字符串”helloworld”,接收方却接收到了两个字符串”hello”和”world”。

虽然socket环境有以上问题,但是TCP传输数据能保证几点:

  • 顺序不变。例如发送方发送hello,接收方也一定顺序接收到hello,这个是TCP协议承诺的,因此这点成为我们解决分包、黏包问题的关键。
  • 分割的包中间不会插入其他数据。

因此如果要使用socket通信,就一定要自己定义一份协议。目前最常用的协议标准是:消息头部(包头)+消息长度+消息正文

TCP为什么会分包

TCP是以段(Segment)为单位发送数据的,建立TCP链接后,有一个最大消息长度(MSS)。如果应用层数据包超过MSS,就会把应用层数据包拆分,分成两个段来发送。这个时候接收端的应用层就要拼接这两个TCP包,才能正确处理数据。

相关的,路由器有一个MTU( 最大传输单元),一般是1500字节,除去IP头部20字节,留给TCP的就只有MTU-20字节。所以一般TCP的MSS为MTU-20=1460字节。

当应用层数据超过1460字节时,TCP会分多个数据包来发送。

扩展阅读

TCP的RFC定义MSS的默认值是536,这是因为 RFC 791里说了任何一个IP设备都得最少接收576尺寸的大小(实际上来说576是拨号的网络的MTU,而576减去IP头的20个字节就是536)。
TCP为什么会粘包

有时候,TCP为了提高网络的利用率,会使用一个叫做Nagle的算法。该算法是指,发送端即使有要发送的数据,如果很少的话,会延迟发送。如果应用层给TCP传送数据很快的话,就会把两个应用层数据包“粘”在一起,TCP最后只发一个TCP数据包给接收端。

开发环境

  • Python版本:3.5.1
  • 操作系统:Windows 10 x64

消息头部(包含消息长度)

消息头部不一定只能是一个字节比如0xAA什么的,也可以包含协议版本号,指令等,当然也可以把消息长度合并到消息头部里,唯一的要求是包头长度要固定的,包体则可变长。下面是我自定义的一个包头:

版本号(ver) 消息长度(bodySize) 指令(cmd)

版本号,消息长度,指令数据类型都是无符号32位整型变量,于是这个消息长度固定为4×3=12字节。在Python由于没有类型定义,所以一般是使用struct模块生成包头。示例:

import struct
import json

ver = 1
body = json.dumps(dict(hello="world"))
print(body) # {"hello": "world"}
cmd = 101
header = [ver, body.__len__(), cmd]
headPack = struct.pack("!3I", *header)
print(headPack) # b'\x00\x00\x00\x01\x00\x00\x00\x12\x00\x00\x00e'

关于用自定义结束符分割数据包

有的人会想用自定义的结束符分割每一个数据包,这样传输数据包时就不需要指定长度甚至也不需要包头了。但是如果这样做,网络传输性能损失非常大,因为每一读取一个字节都要做一次if判断是否是结束符。所以建议还是选择消息头部+消息长度+消息正文这种方式。

而且,使用自定义结束符的时候,如果消息正文中出现这个符号,就会把后面的数据截止,这个时候还需要处理符号转义,类比于\r\n的反斜杠。所以非常不建议使用结束符分割数据包。

消息正文

消息正文的数据格式可以使用Json格式,这里一般是用来存放独特信息的数据。在下面代码中,我使用{"hello","world"}数据来测试。在Python使用json模块来生成json数据

Python示例

下面使用Python代码展示如何处理TCP Socket的粘包和分包。核心在于用一个FIFO队列接收缓冲区dataBuffer和一个小while循环来判断。

具体流程是这样的:把从socket读取出来的数据放到dataBuffer后面(入队),然后进入小循环,如果dataBuffer内容长度小于消息长度(bodySize),则跳出小循环继续接收;大于消息长度,则从缓冲区读取包头并获取包体的长度,再判断整个缓冲区是否大于消息头部+消息长度,如果小于则跳出小循环继续接收,如果大于则读取包体的内容,然后处理数据,最后再把这次的消息头部和消息正文从dataBuffer删掉(出队)。

下面用Markdown画了一个流程图。

python TCP Socket的粘包和分包的处理详解

服务器端代码

# Python Version:3.5.1
import socket
import struct

HOST = ''
PORT = 1234

dataBuffer = bytes()
headerSize = 12

sn = 0
def dataHandle(headPack, body):
  global sn
  sn += 1
  print("第%s个数据包" % sn)
  print("ver:%s, bodySize:%s, cmd:%s" % headPack)
  print(body.decode())
  print("")

if __name__ == '__main__':
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen(1)
    conn, addr = s.accept()
    with conn:
      print('Connected by', addr)
      while True:
        data = conn.recv(1024)
        if data:
          # 把数据存入缓冲区,类似于push数据
          dataBuffer += data
          while True:
            if len(dataBuffer) < headerSize:
              print("数据包(%s Byte)小于消息头部长度,跳出小循环" % len(dataBuffer))
              break

            # 读取包头
            # struct中:!代表Network order,3I代表3个unsigned int数据
            headPack = struct.unpack('!3I', dataBuffer[:headerSize])
            bodySize = headPack[1]

            # 分包情况处理,跳出函数继续接收数据
            if len(dataBuffer) < headerSize+bodySize :
              print("数据包(%s Byte)不完整(总共%s Byte),跳出小循环" % (len(dataBuffer), headerSize+bodySize))
              break
            # 读取消息正文的内容
            body = dataBuffer[headerSize:headerSize+bodySize]

            # 数据处理
            dataHandle(headPack, body)

            # 粘包情况的处理
            dataBuffer = dataBuffer[headerSize+bodySize:] # 获取下一个数据包,类似于把数据pop出

测试服务器端的客户端代码

下面附上测试粘包和分包的客户端代码

# Python Version:3.5.1
import socket
import time
import struct
import json

host = "localhost"
port = 1234

ADDR = (host, port)

if __name__ == '__main__':
  client = socket.socket()
  client.connect(ADDR)

  # 正常数据包定义
  ver = 1
  body = json.dumps(dict(hello="world"))
  print(body)
  cmd = 101
  header = [ver, body.__len__(), cmd]
  headPack = struct.pack("!3I", *header)
  sendData1 = headPack+body.encode()

  # 分包数据定义
  ver = 2
  body = json.dumps(dict(hello="world2"))
  print(body)
  cmd = 102
  header = [ver, body.__len__(), cmd]
  headPack = struct.pack("!3I", *header)
  sendData2_1 = headPack+body[:2].encode()
  sendData2_2 = body[2:].encode()

  # 粘包数据定义
  ver = 3
  body1 = json.dumps(dict(hello="world3"))
  print(body1)
  cmd = 103
  header = [ver, body1.__len__(), cmd]
  headPack1 = struct.pack("!3I", *header)

  ver = 4
  body2 = json.dumps(dict(hello="world4"))
  print(body2)
  cmd = 104
  header = [ver, body2.__len__(), cmd]
  headPack2 = struct.pack("!3I", *header)

  sendData3 = headPack1+body1.encode()+headPack2+body2.encode()


  # 正常数据包
  client.send(sendData1)
  time.sleep(3)

  # 分包测试
  client.send(sendData2_1)
  time.sleep(0.2)
  client.send(sendData2_2)
  time.sleep(3)

  # 粘包测试
  client.send(sendData3)
  time.sleep(3)
  client.close()

服务器端打印结果

下面是测试出来的打印结果,可见接收方已经完美的处理粘包和分包问题了。

Connected by ('127.0.0.1', 23297)
第1个数据包
ver:1, bodySize:18, cmd:101
{"hello": "world"}

数据包(0 Byte)小于包头长度,跳出小循环
数据包(14 Byte)不完整(总共31 Byte),跳出小循环
第2个数据包
ver:2, bodySize:19, cmd:102
{"hello": "world2"}

数据包(0 Byte)小于包头长度,跳出小循环
第3个数据包
ver:3, bodySize:19, cmd:103
{"hello": "world3"}

第4个数据包
ver:4, bodySize:19, cmd:104
{"hello": "world4"}

在框架下处理粘包和分包

其实无论是使用阻塞还是异步socket开发框架,框架本身都会提供一个接收数据的方法提供给开发者,一般来说开发者都要覆写这个方法。下面是在Twidted开发框架处理粘包和分包的示例,只上核心程序:

# Twiested
class MyProtocol(Protocol):
  _data_buffer = bytes()

  # 代码省略

  def dataReceived(self, data):
    """Called whenever data is received."""
    self._data_buffer += data
    headerSize = 12

    while True:
      if len(self._data_buffer) < headerSize:
        return

      # 读取消息头部
      # struct中:!代表Network order,3I代表3个unsigned int数据
      headPack = struct.unpack('!3I', self._data_buffer[:headerSize])
      # 获取消息正文长度
      bodySize = headPack[1]

      # 分包情况处理
      if len(self._data_buffer) < headerSize+bodySize :
        return

      # 读取消息正文的内容
      body = self._data_buffer[headerSize:headerSize+bodySize]
      # 处理数据
      self.dataHandle(headPack, body)
      # 粘包情况的处理
      self._data_buffer = self._data_buffer[headerSize+bodySize:]

总结

以上就是本文关于python TCP Socket的粘包和分包的处理详解的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

Python 相关文章推荐
Python多线程编程(一):threading模块综述
Apr 05 Python
python装饰器与递归算法详解
Feb 18 Python
python实现各进制转换的总结大全
Jun 18 Python
python内置数据类型之列表操作
Nov 12 Python
利用python GDAL库读写geotiff格式的遥感影像方法
Nov 29 Python
Python 图像处理: 生成二维高斯分布蒙版的实例
Jul 04 Python
Django model select的多种用法详解
Jul 16 Python
python用match()函数爬数据方法详解
Jul 23 Python
Python爬虫解析网页的4种方式实例及原理解析
Dec 30 Python
Python求平面内点到直线距离的实现
Jan 19 Python
详解python metaclass(元类)
Aug 13 Python
浅谈Python项目的服务器部署
Apr 25 Python
python实现Adapter模式实例代码
Feb 09 #Python
python实现Decorator模式实例代码
Feb 09 #Python
Python多线程扫描端口代码示例
Feb 09 #Python
Python编程实现从字典中提取子集的方法分析
Feb 09 #Python
python tensorflow学习之识别单张图片的实现的示例
Feb 09 #Python
python删除服务器文件代码示例
Feb 09 #Python
详解Python使用tensorflow入门指南
Feb 09 #Python
You might like
php笔记之:php函数range() round()和list()的使用说明
2013/04/26 PHP
PHP程序级守护进程的实现与优化的使用概述
2013/05/02 PHP
解决PHP超大文件下载,断点续传下载的方法详解
2013/06/06 PHP
PHP 生成N个不重复的随机数
2015/01/21 PHP
codeigniter实现get分页的方法
2015/07/10 PHP
php array_walk 对数组中的每个元素应用用户自定义函数详解
2016/11/18 PHP
简单解决微信文章图片防盗链问题
2016/12/17 PHP
Thinkphp通过一个入口文件如何区分移动端和PC端
2017/04/18 PHP
PHP实现分布式memcache设置web集群session同步的方法
2018/04/10 PHP
php高性能日志系统 seaslog 的安装与使用方法分析
2020/02/29 PHP
jquery $(document).ready() 与window.onload的区别
2009/12/28 Javascript
javascript管中窥豹 形参与实参浅析
2011/12/17 Javascript
js 对小数加法精度处理示例说明
2013/12/27 Javascript
jQuery中andSelf()方法用法实例
2015/01/08 Javascript
Javascript核心读书有感之类型、值和变量
2015/02/11 Javascript
JavaScript仿支付宝密码输入框
2015/12/29 Javascript
详解React-Todos入门例子
2016/11/08 Javascript
详解node HTTP请求客户端 - Request
2017/05/05 Javascript
深入理解Vue router的部分高级用法
2018/08/15 Javascript
vue如何限制只能输入正负数及小数
2019/07/04 Javascript
原生JavaScript实现贪吃蛇游戏
2020/11/04 Javascript
Python中操作符重载用法分析
2016/04/29 Python
Python之自动获取公网IP的实例讲解
2017/10/01 Python
快速解决pandas.read_csv()乱码的问题
2018/06/15 Python
Django 开发调试工具 Django-debug-toolbar使用详解
2019/07/23 Python
运用PyTorch动手搭建一个共享单车预测器
2019/08/06 Python
scrapy在python爬虫中搭建出错的解决方法
2020/11/22 Python
html5 浏览器支持 如何让所有的浏览器都支持HTML5标签样式
2012/12/07 HTML / CSS
Nayomi官网:沙特阿拉伯王国睡衣和内衣品牌
2020/12/19 全球购物
夜大自我鉴定
2013/10/31 职场文书
酒店执行总经理岗位职责
2013/12/15 职场文书
乡镇民主生活会发言材料
2014/10/20 职场文书
2016年会开场白台词
2015/06/01 职场文书
小学生家长意见
2015/06/03 职场文书
5分钟教你docker安装启动redis全教程(全新方式)
2021/05/29 Redis
使用pandas生成/读取csv文件的方法实例
2021/07/09 Python