python中TCP协议中的粘包问题
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.在服务器端先收到命令,打开子进程,然后计算返回的数据的长度
2.先利用struct模块将数据长度转成固定4个字节传给客户端
3.再向客户端发送真实的数据。
客户端(两次接收):
1.第一次只接受4个字节,因为长度数据就是4个字节。这样防止了数据粘包。解码得到长度数据
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协议中的粘包问题的更多相关文章
- tcp协议传输方法&粘包问题
socket实现客户端和服务端 tcp协议可以用socket模块实现服务端可客户端的交互 # 服务端 import socket #生成一个socket对象 soc = socket.socket(s ...
- Python网络编程(2)-粘包现象及socketserver模块实现TCP并发
1. 基于Tcp的远程调用命令实现 很多人应该都使用过Xshell工具,这是一个远程连接工具,通过上面的知识,就可以模拟出Xshell远程连接服务器并调用命令的功能. Tcp服务端代码如下: impo ...
- 从TCP三次握手说起–浅析TCP协议中的疑难杂症(2)
版权声明:本文由黄日成原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/108 来源:腾云阁 https://www.qclo ...
- TCP协议中的SO_LINGER选项
TCP协议中的SO_LINGER选项 SO_LINGER选项用来设置延迟关闭的时间,等待套接字发送缓冲区中的数据发送完成.没有设置该选项时,在调用close()后,在发送完FIN后会立即进行一些清理工 ...
- TCP 协议中的 Window Size与吞吐量
原地址:http://blog.sina.com.cn/s/blog_c5c2d6690102wpxl.html TCP协议中影响实际业务流量的参数很多,这里主要分析一下窗口的影响. TCP窗口目的 ...
- TCP协议中的三次握手和四次挥手(图解)【转】
建立TCP需要三次握手才能建立,而断开连接则需要四次握手.整个过程如下图所示: 先来看看如何建立连接的. [更新于2017.01.04 ]该部分内容配图有误,请大家见谅,正确的配图如下,错误配图也不删 ...
- 客户端验证、tcp协议中多个客户端的同时在线
一.客户端验证 当在一个局域网内需要验证是否为合法的客户端连接时,我们需要写代码进行验证. Server端 import os import hmac import socket def auth(c ...
- Python之路 - 网络编程之粘包
Python之路 - 网络编程之粘包 粘包
- python网络编程:TCP通讯模板、粘包及解决方案、自定义报头
一.TCP通讯模板 二.远程CMD程序 三.解决粘包问题 四.解决粘包问题2 一.TCP通讯模板 TCP客户端 import socket c = socket.socket() # 连接服务器 c. ...
随机推荐
- 005-docker启动设置环境变量
https://blog.csdn.net/wsbgmofo/article/details/79173920
- 20175202 《Java程序设计》第八周学习总结
20175202 2018-2019-2 <Java程序设计>第八周学习总结 教材知识点总结 1.泛型: 主要目的是可以建立具有类型安全的集合框架,如链表.散列映射等数据结构. 泛型类的声 ...
- 单机版Kubernetes集群(一)
环境:CentOS Linux release 7.4.1708 (Core) 单机版Kubernetes集群的效果,如图: 1)JSP页面通过JDBC直接访问Mysql数据库并展示:这里只是为了 ...
- Btrace 拦截构造函数,同名函数
拦截方法: 1.普通方法 @OnMethod(clazz="", method="") 2.构造函数@OnMethod(claszz="" ...
- 浏览器端使用javascript调用腾讯翻译api
最近在学习的小玩意,发现腾讯的文档十分坑爹,里面有很多错误的指示. 不过腾讯的机器翻译还是很牛的,我觉得翻译水准比谷歌好很多. 腾讯的机器翻译貌似在试用阶段,不收费,用QQ或微信登录即可申请使用. 首 ...
- Notepad2用法说明
Notepad2用法说明:1.替换系统记事本.bat和恢复系统记事本.bat可以替换.回复系统记事本.2.查看→默认字体,编程可用Consolas,字号四号.3.查看→自定义方案,Identifier ...
- Android Studio Gradle依赖冲突
版本冲突 Gradle提供了两种解决版本冲突的策略:Newest和Fail.默认策略是Newest,配置Fail模式: configurations.all { resolutionStrategy. ...
- vscode之常用快捷键
原文章地址: vscode: Visual Studio Code 常用快捷键 官方快捷键说明:Key Bindings for Visual Studio Code 主命令框 F1 或 Ctrl+S ...
- 通用mapper版+SpringBoot+MyBatis框架+mysql数据库的整合
转:https://blog.csdn.net/qq_35153200/article/details/79538440 开发环境: 开发工具:Intellij IDEA 2017.2.3 JDK : ...
- HTML如何实现斜体字
HTML实现斜体字的标签为<i>标签,用来实现字体倾斜,写法如下: 字体斜体:<i>内容</i> 案例:正常 斜体 当文字加入i标签以后字体就会成为斜体