[Python学习日记-79] socket 开发中的粘包现象(解决模拟 SSH 远程执行命令代码中的粘包问题)

[Python学习日记-79] socket 开发中的粘包现象(解决模拟 SSH 远程执行命令代码中的粘包问题)

简介

粘包问题底层原理分析

粘包问题的解决

简介

        在Python学习日记-78我们留下了两个问题,一个是服务器端 send() 中使用加号的问题,另一个是收的 recv() 中接收长度导致的粘包现象。

        上图就是粘包现象,就是指两次结果粘到一起了,它的发生主要是因为 socket 缓冲区导致的,粘包对于用户体验造成的影响是比较大,难度也相对较高,所以本篇的主角就是粘包现象,我们一起来看看有什么办法可以解决这个难搞的现象。

粘包问题底层原理分析

         在了解什么是粘包之前我们必须知道一个前提,那就是粘包现象只会出现在 TCP 身上,而 UDP 是永远不会粘包的,要知道是什么原因我们要先掌握一个 socket 收发消息的原理先,下图为 sokcet 收发消息的原理图

         在发送端和接收端之间怎么样为一条消息呢?可以认为一次 send() 和 recv() 就是一条消息,但要知道你的程序实际上无权直接操作网卡的,你操作网卡都是通过操作系统给用户程序暴露出来的接口,那每次你的程序要给远程发数据时,其实是先把数据从用户态复制到内核态,这样的操作是耗资源和时间的,频繁的在内核态和用户态之前交换数据势必会导致发送效率降低,因此 socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方(send() 的字节流是先放入应用程序所在计算机的缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用 sendall() 就会循环调用 send(),数据不会丢失),所以这条消息无论底层是如何分段分片的传输层协议都会把构成整条消息的数据段排序完成后才呈现在内核缓冲区,所以到达了缓冲区其实都是一条完整的消息,关键就在与传输协议 TCP 和 UDP 的传输方式不一样,导致两者的特性各不相同。

        TCP 协议(流式协议)传输消息时发送端可能会一次性发送 1KB 的数据,而接收端可能会以 2KB、3KB、6KB、3Bytes 的形式来提取收到的数据,也就是说接收端所看到的数据是一个流(stream),即面向流的通信是无消息保护边界的协议,所以客户端是不能一下子看到一条消息是有多少字节的,例如基于 TCP 的套接字客户端往服务器端上传文件,发送时文件内容是按照一段一段的字节流发送的,在服务器端接收到后根本不知道该文件的字节流从何处开始,在何处结束。TCP 为提高传输效率,发送方往往要收集到足够多的数据后才发送一个 TCP 段,如果连续几次需要发送的数据都很少,通常 TCP 会根据优化算法(Nagle 算法)把这些数据合成一个 TCP 段后一次发送出去,当发送端缓冲区的长度大于网卡的 MTU 时会出现拆包情况的发生,届时 TCP 会将这次发送的数据拆成几个数据包发送出去,这样更加加重了 TCP 传输数据的粘包问题,这就是 TCP 为什么容易发生粘包问题的原因。但 TCP 的数据不会丢,在上一次传输没有收完的包,下次还会接收,发送端会在收到 ack 时才会清除缓冲区内容,所以数据是可靠传输的,缺点就是会粘包。

        UDP 协议传输消息是必须以消息为单位提取数据的,不能一次提取任意字节的数据,即面向消息的通信是有消息保护边界的,它也不会使用块的合并优化算法来进行优化,并且由于 UDP 支持的是一对多的模式,所以接收端的 skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的 UDP 包,在每个 UDP 包中就有了消息头(消息来源地址,端口等信息),对于接收端来说就容易进行区分处理了,所以 UDP 协议传输消息永远不可能出现粘包现象。但 UDP 的 recvfrom() 是阻塞的,一个 recvfrom(x) 必须对唯一一个 sendinto(y),收完了 x 个字节的数据就算完成,若是 y>x 那么 y-x 的数据就会丢失,这意味着 UDP 根本不会粘包,但是会丢数据,并不可靠。

        总的来说,所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

以下两种情况会发生粘包:

1、发送端需要等缓冲区满才发送出去,从而造成粘包(发送数据时间间隔很短,而且数据量很小,会合到一起产生粘包)

服务器端:

import socketip_port = ('127.0.0.1',8080)server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(ip_port)
server.listen(5)conn,client_addr = server.accept()data1 = conn.recv(10)
data2 = conn.recv(10)print('第一次------>', data1.decode('utf-8'))
print('第二次------>', data2.decode('utf-8'))conn.close()

客户端:

import socketip_port = ('127.0.0.1',8080)
info_size = 1024client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
client.connect(ip_port)client.send('hello'.encode('utf-8'))
client.send('jove'.encode('utf-8'))

代码输出如下:

2、接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

服务器端:

import socket
import time
ip_port = ('127.0.0.1',8080)server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(ip_port)
server.listen(5)conn,client_addr = server.accept()data1 = conn.recv(2)    # 第一次没接收完整
data2 = conn.recv(10)   # 第二次接收的时候会先取出旧的数据,然后再取新的print('第一次------>', data1.decode('utf-8'))
time.sleep(1)
print('第二次------>', data2.decode('utf-8'))conn.close()

客户端:

import socketip_port = ('127.0.0.1',8080)
info_size = 1024client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
client.connect(ip_port)client.send('hello'.encode('utf-8'))
client.send('jove'.encode('utf-8'))

代码输出如下: 

粘包问题的解决

一、struct 模块

        解决粘包问题的关键就是要何如提前告诉接收端我发送的信息长度,我们的解决办法就是为真正的数据封装一个固定长度的报头,然后让接收端按照固定长度来接受该报头从而获取到我接受数据的长度大小,而 struct 模块就是用于数据的打包和解包。

        通过 struct 模块,可以将 Python 中的数据类型(如整数、浮点数等)转换为指定的二进制格式,或者将二进制数据解包成相应的 Python 对象。该模块提供了一些函数来执行这些转换,包括 pack()、unpack()、pack_into()、unpack_from() 等。其中,pack() 函数用于将数据打包为二进制字符串,unpack() 函数用于将二进制数据解包为 Python 对象。struct 模块定义了一些格式字符用于表示数据的布局、对齐方式和字节顺序。常用的格式字符包括:'i'(有符号整数)、'l'(有符号长整数)、'q'(有符号的长长整数)、'f'(浮点数)、's'(字符串)、'c'(单个字符)等。

代码演示:

import struct# 发送端打包,可以一次打包两个不同类型的数据,一个数据长度为4,两个数据长度为8,如此类推
res = struct.pack('if',12888,3.14)  # 'i' == int 'f' == float
print(res,type(res),len(res))# 接收端固定长度接收,client.recv(4)
obj = struct.unpack('if',res)
print(obj)    # 解包后是一个元组
print(obj[0])# res = struct.pack('i',12888888888)  # 'i'会超过范围报错

代码输出如下:

二、简单版本

服务器端:

import socket
import subprocess
import structip_port = ('127.0.0.1',8080)
cmd_size = 8096server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(ip_port)
server.listen(5)print('starting...')
while True:  # 链接循环conn, client_addr = server.accept()print(client_addr)while True:  # 通讯循环try:# 1、收命令cmd = conn.recv(cmd_size)   # 8096个字节的命令已经很好的保证了命令可以完整接收if not cmd: break# 2、执行命令,拿到结果obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)stdout = obj.stdout.read()stderr = obj.stderr.read()# 3、把命令的结果返回给客户端# 第一步: 制作固定长度的报头total_size = len(stdout) + len(stderr)header = struct.pack('i', total_size)# 第二步: 把报头(固定长度)发送给客户端conn.send(header)# 第三步: 再发送真实的数据conn.send(stdout)  # 这里不使用 +(加号) TCP/IP也会把两个包粘到一起conn.send(stderr)except ConnectionResetError:breakconn.close()
server.close()

客户端:

import socket
import structip_port = ('127.0.0.1',8080)
info_size = 1024client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
client.connect(ip_port)while True:# 1、发命令cmd = input('>>: ').strip()if not cmd:continueclient.send(cmd.encode('utf-8'))# 2、拿到执行命令的结果,并打印# 第一步: 先收报头header = client.recv(4)# 第二步: 从报头中解析出对真实数据的描述信息(数据的长度)total_size = struct.unpack('i', header)[0]# 第三步: 接收真实的数据recv_size = 0recv_data = b''while recv_size < total_size:res = client.recv(info_size)recv_data += resrecv_size += len(res)  # 计算真实的接收长度,如果以后增加打印进度条的时候就可以精确无误的表示print(recv_data.decode('gbk'))client.close()

代码输出如下:

        很明显已经没有粘包现象了,虽然解决了粘包的问题,但是还是存在包头信息过少的问题,例如我想客户端接收到数据后验证一下数据的完整性,那目前就无法完成这一功能了,并且打包的数据长度还会受到数据格式的限制,而在终极版当中这一切将会得到解决。

三、终极版本

服务器端:

import socket
import subprocess
import struct
import jsonip_port = ('127.0.0.1',8080)
cmd_size = 8096server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(ip_port)
server.listen(5)print('starting...')
while True:  # 链接循环conn, client_addr = server.accept()print(client_addr)while True:  # 通讯循环try:# 1、收命令cmd = conn.recv(cmd_size)   # 8096个字节的命令已经很好的保证了命令可以完整接收if not cmd: break# 2、执行命令,拿到结果obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)stdout = obj.stdout.read()stderr = obj.stderr.read()# 3、把命令的结果返回给客户端# 第一步: 制作报头header_dic = {  # 使用字典,解决了报头信息少的问题'filename': 'a.txt','md5': 'xxxxdxxx','total_size': len(stdout) + len(stderr)}header_json = json.dumps(header_dic)header_bytes = header_json.encode('utf-8')# 第二步: 先发送报头长度conn.send(struct.pack('i',len(header_bytes)))  # 字典的bytes的长度很小,'i'已经足够使用了# 第三步: 再发报头conn.send(header_bytes)# 第四步: 再发送真实的数据conn.send(stdout)  # 这里不使用+ TCP/IP也会把两个包粘到一起conn.send(stderr)except ConnectionResetError:breakconn.close()
server.close()

客户端:

import socket
import struct
import jsonip_port = ('127.0.0.1',8080)
info_size = 1024client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
client.connect(ip_port)while True:# 1、发命令cmd = input('>>: ').strip()if not cmd:continueclient.send(cmd.encode('utf-8'))# 2、拿到执行命令的结果,并打印# 第一步: 先收报头的长度obj = client.recv(4)header_size = struct.unpack('i',obj)[0]# 第二步: 再收报头header_bytes = client.recv(header_size)# 第三步: 从报头中解析出对真实数据的描述信息header_json = header_bytes.decode('utf-8')header_dic = json.loads(header_json)total_size = header_dic['total_size']# 第四步: 接收真实的数据recv_size = 0recv_data = b''while recv_size < total_size:res = client.recv(info_size)recv_data += resrecv_size += len(res)  # 计算真实的接收长度,如果以后增加打印进度条的时候就可以精确无误的表示print(recv_data.decode('gbk'))client.close()

代码输出如下:

        终极版当中报头使用了字典的形式,并且用 json 模块进行格式化,然后再用 struct 模块进行打包,这样报头就能包含更多的数据,从而实现更多的功能了,并且打包时不会再受到数据格式的限制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/7130.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【落羽的落羽 数据结构篇】算法复杂度

文章目录 一、数据结构和算法简介二、算法复杂度1. 时间复杂度2. 空间复杂度 一、数据结构和算法简介 数据结构是计算机存储、组织数据的方式&#xff0c;指相互之间存在一种或多种特定关系的数据元素的集合。没有一种单一的数据结构对所有用途都有用&#xff0c;所以我们要学…

22_解析XML配置文件_List列表

解析XML文件 需要先 1.【加载XML文件】 而 【加载XML】文件有两种方式 【第一种 —— 使用Unity资源系统加载文件】 TextAsset xml Resources.Load<TextAsset>(filePath); XmlDocument doc new XmlDocument(); doc.LoadXml(xml.text); 【第二种 —— 在C#文件IO…

第十五届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组

第十五届的题目在规定时间内做出了前5道&#xff0c;还有2道找时间再磨一磨。现在把做的一些思路总结如下&#xff1a; 题1&#xff1a;握手问题 问题描述 小蓝组织了一场算法交流会议&#xff0c;总共有 50人参加了本次会议。在会议上&#xff0c;大家进行了握手交流。按照惯例…

联想电脑怎么设置u盘启动_联想电脑设置u盘启动方法(支持新旧机型)

有很多网友问联想电脑怎么设置u盘启动&#xff0c;联想电脑设置u盘启动的方法有两种&#xff0c;一是通过bios进行设置。二是通过快捷方式启动进入u盘启动。但需要注意有两种引导模式是&#xff0c;一种是uefi引导&#xff0c;一种是传统的leacy引导&#xff0c;所以需要注意制…

GitHub Actions 使用需谨慎:深度剖析其痛点与替代方案

在持续集成与持续部署&#xff08;CI/CD&#xff09;领域&#xff0c;GitHub Actions 曾是众多开发者的热门选择&#xff0c;但如今&#xff0c;其弊端逐渐显现&#xff0c;让不少人在使用前不得不深思熟虑。 团队由大约 15 名工程师组成&#xff0c;采用基于主干的开发方式&am…

Leetcode-两数相加

给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数都不会以 0 …

MySQL安装教程

一、下载 点开下面的链接&#xff1a;下载地址 点击Download 就可以下载对应的安装包了, 安装包如下: 二、解压 下载完成后我们得到的是一个压缩包&#xff0c;将其解压&#xff0c;我们就可以得到MySQL 8.0.34 的软件本体了(就是一个文件夹)&#xff0c;我们可以把它放在你想…

BGP分解实验·11——路由聚合与条件性通告(3)

续接上&#xff08;2&#xff09;的实验。其拓扑如下&#xff1a; 路由聚合的负向也就是拆分&#xff0c;在有双出口的情况下&#xff0c;在多出口做流量分担是优选方法之一。 BGP可以根据指定来源而聚合路由&#xff0c;在产生该聚合路由的范围内的条目注入到本地BGP表后再向…

INCOSE需求编写指南-第1部分:介绍

第1部分&#xff1a;介绍Section 1: Introduction 1.1 目的和范围 Purpose and Scope 本指南专门介绍如何在系统工程背景下以文本形式表达需求和要求陈述。其目的是将现有标准&#xff08;如 ISO/IEC/IEEE 29148&#xff09;中的建议以及作者、主要贡献者和审稿员的最佳实践结…

基于神经网络的视频编码NNVC(1):帧内预测

在H.266/VVC发布后&#xff0c;基于传统编码框架提升压缩率越来越难&#xff0c;随着深度学习的发展&#xff0c;研究人员开始尝试将神经网络引入编码器。为此&#xff0c;JVET工作组在2020年成立AHG11小组来专门进行基于神经网络的视频编码的研究。 为了方便研究&#xff0c;工…

深入探究分布式日志系统 Graylog:架构、部署与优化

文章目录 一、Graylog简介二、Graylog原理架构三、日志系统对比四、Graylog部署传统部署MongoDB部署OS或者ES部署Garylog部署容器化部署 五、配置详情六、优化网络和 REST APIMongoDB 七、升级八、监控九、常见问题及处理 一、Graylog简介 Graylog是一个简单易用、功能较全面的…

寒假1.23

题解 web&#xff1a;[极客大挑战 2019]Secret File&#xff08;文件包含漏洞&#xff09; 打开链接是一个普通的文字界面 查看一下源代码 发现一个链接&#xff0c;点进去看看 再点一次看看&#xff0c;没什么用 仔细看&#xff0c;有一个问题&#xff0c;当点击./action.ph…

ORB-SLAM2源码学习:Initializer.cc⑧: Initializer::CheckRT检验三角化结果

前言 ORB-SLAM2源码学习&#xff1a;Initializer.cc⑦: Initializer::Triangulate特征点对的三角化_cv::svd::compute-CSDN博客 经过上面的三角化我们成功得到了三维点&#xff0c;但是经过三角化成功的三维点并不一定是有效的&#xff0c;需要筛选才能作为初始化地图点。 …

微信小程序1.1 微信小程序介绍

1.1 微信小程序介绍 内容提要 1.1 什么是微信小程序 1.2 微信小程序的功能 1.3 微信小程序使用场景 1.4 微信小程序能取代App吗 1.5 微信小程序的发展历程 1.6微信小程序带来的机会

STM32 GPIO配置 点亮LED灯

本次是基于STM32F407ZET6做一个GPIO配置&#xff0c;实现点灯实验。 新建文件 LED.c、LED.h文件&#xff0c;将其封装到Driver文件中。 双击Driver文件将LED.c添加进来 编写头文件&#xff0c;这里注意需要将Driver头文件声明一下。 在LED.c、main.c里面引入头文件LED.h LED初…

vulnhub靶场【kioptrix-3】靶机

前言 靶机&#xff1a;kioptrix-3&#xff0c;IP地址为192.168.1.74 攻击&#xff1a;kali&#xff0c;IP地址为192.168.1.16 都采用虚拟机&#xff0c;网卡为桥接模式 文章中涉及的靶机&#xff0c;来源于vulnhub官网&#xff0c;想要下载&#xff0c;可自行访问官网下载&a…

Cloudflare通过代理服务器绕过 CORS 限制:原理、实现场景解析

第一部分&#xff1a;问题背景 1.1 错误现象复现 // 浏览器控制台报错示例 Access to fetch at https://chat.qwenlm.ai/api/v1/files/ from origin https://ocr.doublefenzhuan.me has been blocked by CORS policy: Response to preflight request doesnt pass access con…

VMware虚拟机安装Linux系统(openKylin)

首先打开VMware Workstation&#xff0c;点击创建新的虚拟机。 进入虚拟机引导界面后&#xff0c;选择“典型”选项&#xff0c;点击下一步&#xff1b; 选择“稍后安装操作系统”&#xff0c;点击下一步&#xff1b; 客户机操作系统选择“Linux”&#xff0c;版本选择Ubuntu 6…

RabbitMQ 多种安装模式

文章目录 前言一、Windows 安装 RabbitMq1、版本关系2、Erlang2.1、下载安装 Erlang 23.12.2、配置 Erlang 环境变量 3、RabbitMQ3.1、下载安装 RabbitMQ 3.8.93.2、环境变量3.3、启动RabbitMQ 管理插件3.3、RabbitMQ3.4、注意事项 二、安装docker1、更新系统包&#xff1a;2、…

【Java-数据结构】Java 链表面试题上 “最后一公里”:解决复杂链表问题的致胜法宝

我的个人主页 我的专栏&#xff1a;Java-数据结构&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;点赞❤ 收藏❤ 引言&#xff1a; Java链表&#xff0c;看似简单的链式结构&#xff0c;却蕴含着诸多有趣的特性与奥秘&#xff0c;等待我们去挖掘。它就像一…