侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

详解python中TCP协议中的粘包问题

Python  /  管理员 发布于 7年前   144

TCP协议中的粘包问题

1.粘包现象

基于TCP实现一个简易远程cmd功能

#服务端import socketimport subprocesssever = socket.socket()sever.bind(('127.0.0.1', 33521))sever.listen()while True: client, address = sever.accept() while True:  try:   cmd = client.recv(1024).decode('utf-8')   p1 = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr= subprocess.PIPE)   data = p1.stdout.read()   err_data = p1.stderr.read()   client.send(data)   client.send(err_data)  except ConnectionResetError:   print('connect broken')   client.close()   breaksever.close()​​​#客户端import socketclient = socket.socket()client.connect(('127.0.0.1', 33521))while True: cmd = input('请输入指令(Q\q退出)>>:').strip().lower() if cmd == 'q':  break client.send(cmd.encode('utf-8')) data = client.recv(1024) print(data.decode('gbk'))client.close()

上述是基于TCP协议的远程cmd简单功能,在运行时会发生粘包。

2、什么是粘包?

只有TCP会发生粘包现象,UDP协议永远不会发生粘包;

TCP:(transport control protocol,传输控制协议)流式协议。在socket中TCP协议是按照字节数进行数据的收发,数据的发送方发出的数据往往接收方不知道数据到底长度是多长,而TCP协议由于本身为了提高传输的效率,发送方往往需要收集到足够的数据才会进行发送。使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

UDP:(user datagram protocol,用户数据报协议)数据报协议。在socket中udp协议收发数据是以数据报为单位,服务端和客户端收发数据是以一个单位,所以不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

TCP协议不会丢失数据,UDP协议会丢失数据。

udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

3、什么情况下会发生粘包?

1.由于TCP协议的优化算法,当单个数据包较小的时候,会等到缓冲区满才会发生数据包前后数据叠加在一起的情况。然后取的时候就分不清了到底是哪段数据,这是第一种粘包。

2.当发送的单个数据包较大超过缓冲区时,收数据方一次就只能取一部分的数据,下次再收数据方再收数据将会延续上次为接收数据。这是第二种粘包。

粘包的本质问题就是接收方不知道发送数据方一次到底发送了多少数据,解决问题的方向也是从控制数据长度着手,也就是如何设置缓冲区的问题

4、如何解决粘包问题?

解决问题思路:上述已经明确粘包的产生是因为接收数据时不知道数据的具体长度。所以我们应该先发送一段数据表明我们发送的数据长度,那么就不会产生数据没有发送或者没有收取完全的情况。

1.struct 模块(结构体)

struct模块的功能可以将python中的数据类型转换成C语言中的结构体(bytes类型)

import structs = 123456789res = struct.pack('i', s)print(res)​res2 = struct.unpack('i', res)print(res2)print(res2[0])

2.粘包的解决方案基本版

既然我们拿到了一个可以固定长度的办法,那么应用struct模块,可以固定长度了。

为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

#服务器端import socketimport subprocessimport structsever = socket.socket()sever.bind(('127.0.0.1', 33520))sever.listen()while True: client, address = sever.accept() while True:  try:   cmd = client.recv(1024).decode('utf-8')   #利用子进程模块启动程序   p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)   #管道输出的信息有正确和错误的   data = p.stdout.read()   err_data = p.stderr.read()   #先将数据的长度发送给客户端   length = len(data)+len(err_data)   #利用struct模块将数据的长度信息转化成固定的字节   len_data = struct.pack('i', length)   #以下将信息传输给客户端   #1.数据的长度   client.send(len_data)   #2.正确的数据   client.send(data)   #2.错误管道的数据   client.send(err_data)  except Exception as e:   client.close()   print('连接中断。。。。')   break   ​#客户端   import socketimport struct​client = socket.socket()client.connect(('127.0.0.1', 33520))while True: cmd = input('请输入指令>>:').strip().encode('utf-8') client.send(cmd) #1.先接收传过来数据的长度是多少,我们通过struct模块固定了字节长度为4 length = client.recv(4) #将struct的字节再转回去整型数字 len_data = struct.unpack('i', length) print(len_data) len_data = len_data[0] print('数据长度为%s:' % len_data)​ all_data = b'' recv_size = 0 #2.接收真实的数据 #循环接收直到接收到数据的长度等于数据的真实长度(总长度) while recv_size < len_data:  data = client.recv(1024)  recv_size += len(data)  all_data += data​ print('接收长度%s' % recv_size) print(all_data.decode('gbk'))

#总结:

服务器端:

  1. 1.在服务器端先收到命令,打开子进程,然后计算返回的数据的长度
  2. 2.先利用struct模块将数据长度转成固定4个字节传给客户端
  3. 3.再向客户端发送真实的数据。

 客户端(两次接收):

  1. 1.第一次只接受4个字节,因为长度数据就是4个字节。这样防止了数据粘包。解码得到长度数据
  2. 2.第二次循环接收真实数据,拼接真实数据完成解码读取数据。

很显然,如果仅仅只是这样肯定无法满足在实际生产中一些需求。那么该怎么修改?

我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个字节足够用了)

我们可以将自定义的报头设置成这种这种格式。

发送时:

1先发报头长度

2再编码报头内容然后发送

3最后发真实内容

接收时:

1先收报头长度,用struct取出来

2根据取出的长度收取报头内容,然后解码,反序列化

3从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

#服务器端import socketimport subprocessimport datetimeimport jsonimport structsever = socket.socket()sever.bind(('127.0.0.1', 33520))sever.listen()while True: client, address = sever.accept() while True:  try:   cmd = client.recv(1024).decode('utf-8')   #启动子进程   p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)   #得到子进程运行的数据   data = p.stdout.read() #子进程运行正确的输出管道数据,数据读出来后是字节   err_data = p.stderr.read() #子进程运行错误的输出管道数据   #计算数据的总长度   length = len(data) + len(err_data)   print('数据总长度:%s' % length)​   #先需要发送报头信息,以下为创建报头信息(至第一次发送)​​   #需要添加时间信息   time_info = datetime.datetime.now()   #设置一个字典将一些额外的信息和长度信息放进去然后json序列化,报头字典   masthead = {}   #将时间数据放入报头字典中   masthead['time'] = str(time_info) #时间格式不能被json序列化,所以将其转化为字符串形式   masthead['length'] = length​   #将报头字典json序列化   json_masthead = json.dumps(masthead)   #得到json格式的报头   # 将json格式的报头编码成字节形式   masthead_data = json_masthead.encode('utf-8')   #利用struct将报头编码的字节的长度转成固定的字节(4个字节)   masthead_length = struct.pack('i', len(masthead_data))​​   #1.发送报头的长度(第一次发送)   client.send(masthead_length)   #2.发送报头信息(第二次发送)   client.send(masthead_data)   #3.发送真实数据(第三次发送)   client.send(data)   client.send(err_data)  except ConnectionResetError:   print('客户端断开连接。。。')   client.close()   break         #客户端import socketimport structimport jsonclient = socket.socket()client.connect(('127.0.0.1', 33520))while True: cmd = input('请输入cmd指令(Q\q退出)>>:').strip() if cmd == 'q':  break​ #发送CMD指令至服务器 client.send(cmd.encode('utf-8'))​​ #1.第一次接收,接收报头信息的长度,由于struct模块固定长度为4字节,括号内直接填4 len_masthead = client.recv(4) #利用struct反解报头长度,由于是元组形式,取值得到整型数字masthead_length masthead_length = struct.unpack('i', len_masthead)[0]​​ #2.第二次接收,接收报头信息,接收长度为报头长度masthead_length 被编码成字节形式的json格式的字典, # 解字符编码得到json格式的字典masthead_data masthead_data = client.recv(masthead_length).decode('utf-8') #得到报头字典masthead masthead = json.loads(masthead_data) print('执行时间%s' % masthead['time']) #通过报头字典得到数据长度 data_length = masthead['length']​ #3.第三次接收,接收真实数据,真实数据长度为data_length # data = client.recv(data_length) #有可能真实数据长度太大会撑爆内存。 #所以循环读取数据 all_data = b'' length = 0 #循环直到长度大于等于数据长度 while length < data_length:  data = client.recv(1024)  length += len(data)  all_data += data print('数据的总长度:%s' % data_length)​ #我的电脑是Windows系统,所以用gbk解码系统发出的信息 print(all_data.decode('gbk'))

总结:

1.TCP协议中,会产生粘包现象。粘包现象产生本质就是读取数据长度未知。

2.解决粘包现象本质就是处理读取数据长度。

3.报头的作用就是解决数据传输过程中数据长度怎么计算传达和传输其他额外信息的。

以上所述是小编给大家介绍的python中TCP协议中的粘包问题详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对站的支持!


  • 上一条:
    详解用Python练习画个美队盾牌
    下一条:
    Python JSON格式数据的提取和保存的实现
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在python语言中Flask框架的学习及简单功能示例(0个评论)
    • 在Python语言中实现GUI全屏倒计时代码示例(0个评论)
    • Python + zipfile库实现zip文件解压自动化脚本示例(0个评论)
    • python爬虫BeautifulSoup快速抓取网站图片(1个评论)
    • vscode 配置 python3开发环境的方法(0个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2018-04
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2022-01
    • 2023-07
    • 2023-10
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客