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


Posted in Python onMarch 22, 2019

TCP协议中的粘包问题

1.粘包现象

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

#服务端
import socket
import subprocess
sever = 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()
   break
sever.close()
​
​
​
#客户端
import socket
client = 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 struct
s = 123456789
res = struct.pack('i', s)
print(res)
​
res2 = struct.unpack('i', res)
print(res2)
print(res2[0])

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

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

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

#服务器端
import socket
import subprocess
import struct
sever = 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 socket
import 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 socket
import subprocess
import datetime
import json
import struct
sever = 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 socket
import struct
import json
client = 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实现的一个简单LRU cache
Sep 26 Python
使用Python编写一个简单的tic-tac-toe游戏的教程
Apr 16 Python
浅谈python对象数据的读写权限
Sep 12 Python
Python图形绘制操作之正弦曲线实现方法分析
Dec 25 Python
TensorFlow入门使用 tf.train.Saver()保存模型
Apr 24 Python
python 遍历目录(包括子目录)下所有文件的实例
Jul 11 Python
python3爬虫学习之数据存储txt的案例详解
Apr 24 Python
用uWSGI和Nginx部署Flask项目的方法示例
May 05 Python
python 使用turtule绘制递归图形(螺旋、二叉树、谢尔宾斯基三角形)
May 30 Python
浅析pandas 数据结构中的DataFrame
Oct 12 Python
Python使用QQ邮箱发送邮件实例与QQ邮箱设置详解
Feb 18 Python
Python面向对象程序设计之类和对象、实例变量、类变量用法分析
Mar 23 Python
Python JSON格式数据的提取和保存的实现
Mar 22 #Python
Python3.4解释器用法简单示例
Mar 22 #Python
Python常见数字运算操作实例小结
Mar 22 #Python
详解python校验SQL脚本命名规则
Mar 22 #Python
pymongo中group by的操作方法教程
Mar 22 #Python
Python常用特殊方法实例总结
Mar 22 #Python
pymongo中聚合查询的使用方法
Mar 22 #Python
You might like
escape unescape的php下的实现方法
2007/04/27 PHP
PHP 时间转换Unix时间戳代码
2010/01/22 PHP
解析php安全性问题中的:Null 字符问题
2013/06/21 PHP
php中替换字符串中的空格为逗号','的方法
2014/06/09 PHP
PHP在网页中动态生成PDF文件详细教程
2014/07/05 PHP
JS面向对象、prototype、call()、apply()
2009/05/14 Javascript
基于jquery实现的鼠标滑过按钮改变背景图片
2011/07/15 Javascript
jquery.Jwin.js 基于jquery的弹出层插件代码
2012/05/23 Javascript
选择器中含有空格在使用示例及注意事项
2013/07/31 Javascript
js中如何复制一个对象并获取其所有属性和属性对应的值
2013/10/24 Javascript
关于Javascript作用域链的八点总结
2013/12/06 Javascript
jQuery获得指定元素坐标的方法
2015/04/14 Javascript
chrome不支持form.submit的解决方案
2015/04/28 Javascript
javascript实现图片延迟加载方法汇总(三种方法)
2015/08/27 Javascript
Node.js插件安装图文教程
2016/05/06 Javascript
getElementById().innerHTML与getElementById().value的区别
2016/10/27 Javascript
JS 对java返回的json格式的数据处理方法
2016/12/05 Javascript
mui 打开新窗口的方式总结及注意事项
2017/08/20 Javascript
Javascript Promise用法详解
2018/05/10 Javascript
对mac下nodejs 更新到最新版本的最新方法(推荐)
2018/05/17 NodeJs
ES6基础之默认参数值
2019/02/21 Javascript
基于vue、react实现倒计时效果
2019/08/26 Javascript
微信小程序商品详情页底部弹出框
2019/11/22 Javascript
[01:04:49]KG vs LGD 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/16 DOTA
python通过pip更新所有已安装的包实现方法
2017/05/19 Python
浅谈python可视化包Bokeh
2018/02/07 Python
Python中模块(Module)和包(Package)的区别详解
2019/08/07 Python
python常用数据重复项处理方法
2019/11/22 Python
解决pycharm编辑区显示yaml文件层级结构遇中文乱码问题
2020/04/27 Python
html5使用window.postMessage进行跨域实现数据交互的一次实战
2021/02/24 HTML / CSS
工业自动化专业毕业生推荐信
2013/11/18 职场文书
优秀团员自我评价范文
2014/04/23 职场文书
导游词之西江千户苗寨
2019/12/24 职场文书
html+css实现文字折叠特效实例
2021/06/02 HTML / CSS
Vue中插槽slot的使用方法与应用场景详析
2021/06/08 Vue.js
Python-OpenCV教程之图像的位运算详解
2021/06/21 Python