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

news2025/2/2 5:00:37

[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 socket

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(10)
data2 = conn.recv(10)

print('第一次------>', data1.decode('utf-8'))
print('第二次------>', data2.decode('utf-8'))

conn.close()

客户端:

import socket

ip_port = ('127.0.0.1',8080)
info_size = 1024

client = 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 socket

ip_port = ('127.0.0.1',8080)
info_size = 1024

client = 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 struct

ip_port = ('127.0.0.1',8080)
cmd_size = 8096

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)

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:
            break
    conn.close()
server.close()

客户端:

import socket
import struct

ip_port = ('127.0.0.1',8080)
info_size = 1024

client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
client.connect(ip_port)

while True:
    # 1、发命令
    cmd = input('>>: ').strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))

    # 2、拿到执行命令的结果,并打印
    # 第一步: 先收报头
    header = client.recv(4)

    # 第二步: 从报头中解析出对真实数据的描述信息(数据的长度)
    total_size = struct.unpack('i', header)[0]

    # 第三步: 接收真实的数据
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = client.recv(info_size)
        recv_data += res
        recv_size += len(res)  # 计算真实的接收长度,如果以后增加打印进度条的时候就可以精确无误的表示

    print(recv_data.decode('gbk'))

client.close()

代码输出如下:

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

三、终极版本

服务器端:

import socket
import subprocess
import struct
import json

ip_port = ('127.0.0.1',8080)
cmd_size = 8096

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)

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:
            break
    conn.close()
server.close()

客户端:

import socket
import struct
import json

ip_port = ('127.0.0.1',8080)
info_size = 1024

client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
client.connect(ip_port)

while True:
    # 1、发命令
    cmd = input('>>: ').strip()
    if not cmd:continue
    client.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 = 0
    recv_data = b''
    while recv_size < total_size:
        res = client.recv(info_size)
        recv_data += res
        recv_size += len(res)  # 计算真实的接收长度,如果以后增加打印进度条的时候就可以精确无误的表示

    print(recv_data.decode('gbk'))

client.close()

代码输出如下:

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

origin如何在已经画好的图上修改数据且不改变原图像的画风和格式

例如我现在的.opju文件长这样 现在我换了数据集&#xff0c;我想修改这两个图表里对应的算法里的数据&#xff0c;但是我还想保留这图像现在的形式&#xff0c;可以尝试像下面这样做&#xff1a; 右击第一个图&#xff0c;出现下面&#xff0c;选择Book[sheet1] 选择工作簿 出…

5.3.2 软件设计原则

文章目录 抽象模块化信息隐蔽与独立性衡量 软件设计原则&#xff1a;抽象、模块化、信息隐蔽。 抽象 抽象是抽出事物本质的共同特性。过程抽象是指将一个明确定义功能的操作当作单个实体看待。数据抽象是对数据的类型、操作、取值范围进行定义&#xff0c;然后通过这些操作对数…

【ArcGIS遇上Python】批量提取多波段影像至单个波段

本案例基于ArcGIS python,将landsat影像的7个波段影像数据,批量提取至单个波段。 相关阅读:【ArcGIS微课1000例】0141:提取多波段影像中的单个波段 文章目录 一、数据准备二、效果比对二、python批处理1. 编写python代码2. 运行代码一、数据准备 实验数据及完整的python位…

Spring Security(maven项目) 3.0.2.9版本 --- 改

前言&#xff1a; 通过实践而发现真理&#xff0c;又通过实践而证实真理和发展真理。从感性认识而能动地发展到理性认识&#xff0c;又从理性认识而能动地指导革命实践&#xff0c;改造主观世界和客观世界。实践、认识、再实践、再认识&#xff0c;这种形式&#xff0c;循环往…

仿真设计|基于51单片机的温度与烟雾报警系统

目录 具体实现功能 设计介绍 51单片机简介 资料内容 仿真实现&#xff08;protues8.7&#xff09; 程序&#xff08;Keil5&#xff09; 全部内容 资料获取 具体实现功能 &#xff08;1&#xff09;LCD1602实时监测及显示温度值和烟雾浓度值&#xff1b; &#xff08;2…

深入剖析 CSRF 漏洞:原理、危害案例与防护

目录 前言 漏洞介绍 漏洞原理 产生条件 产生的危害 靶场练习 post 请求csrf案例 防御措施 验证请求来源 设置 SameSite 属性 双重提交 Cookie 结语 前言 在网络安全领域&#xff0c;各类漏洞层出不穷&#xff0c;时刻威胁着用户的隐私与数据安全。跨站请求伪造&…

buuuctf_秘密文件

题目&#xff1a; 应该是分析流量包了&#xff0c;用wireshark打开 我追踪http流未果&#xff0c;分析下ftp流 追踪流看看 用户 “ctf” 使用密码 “ctf” 登录。 PORT命令用于为后续操作设置数据连接。 LIST命令用于列出 FTP 服务器上目录的内容&#xff0c;但在此日志中未…

课程设计|结构力学

课 程 设 计 第一部分 &#xff08;结构力学&#xff09; 2、两种结构在静力等效荷载作用下&#xff0c;内力有哪些不同&#xff1f;&#xff08;分析比较&#xff09; 1/2 1 1 1 1 1 1/2 1/4 11(1/2) 1/4 图1求解过程及结果&#xff1a; 轴力图&#xff1a; 内力计算 单位&…

跟李沐学AI:视频生成类论文精读(Movie Gen、HunyuanVideo)

Movie Gen&#xff1a;A Cast of Media Foundation Models 简介 Movie Gen是Meta公司提出的一系列内容生成模型&#xff0c;包含了 3.2.1 预训练数据 Movie Gen采用大约 100M 的视频-文本对和 1B 的图片-文本对进行预训练。 图片-文本对的预训练流程与Meta提出的 Emu: Enh…

keil5如何添加.h 和.c文件,以及如何添加文件夹

1.简介 在hal库的编程中我们一般会生成如下的几个文件夹&#xff0c;在这几个文件夹内存储着各种外设所需要的函数接口.h文件&#xff0c;和实现函数具体功能的.c文件&#xff0c;但是有时我们想要创建自己的文件夹并在这些文件夹下面创造.h .c文件来实现某些功能&#xff0c;…

2025-1-28-sklearn学习(47) (48) 万家灯火亮年至,一声烟花开新来。

文章目录 sklearn学习(47) & (48)sklearn学习(47) 把它们放在一起47.1 模型管道化47.2 用特征面进行人脸识别47.3 开放性问题: 股票市场结构 sklearn学习(48) 寻求帮助48.1 项目邮件列表48.2 机器学习从业者的 Q&A 社区 sklearn学习(47) & (48) 文章参考网站&…

DeepSeek 云端部署,释放无限 AI 潜力!

1.简介 目前&#xff0c;OpenAI、Anthropic、Google 等公司的大型语言模型&#xff08;LLM&#xff09;已广泛应用于商业和私人领域。自 ChatGPT 推出以来&#xff0c;与 AI 的对话变得司空见惯&#xff0c;对我而言没有 LLM 几乎无法工作。 国产模型「DeepSeek-R1」的性能与…

【Qt5】声明之后快速跳转

我在网上看到的方法是ctrlL&#xff08;&#xff1f;不是很清楚&#xff0c;因为我跳转不成功&#xff01;&#xff09; 另外一种就是鼠标点击跳转的。 首先&#xff0c;声明私有成员函数 此时&#xff0c;一般步骤应该是在构造函数里面继续&#xff0c;写函数的框架什么的。于…

flowable expression和json字符串中的双引号内容

前言 最近做项目&#xff0c;发现了一批特殊的数据&#xff0c;即特殊字符"&#xff0c;本身输入双引号也不是什么特殊的字符&#xff0c;毕竟在存储时就是正常字符&#xff0c;只不过在编码的时候需要转义&#xff0c;转义符是\&#xff0c;然而转义符\也是特殊字符&…

新一代搜索引擎,是 ES 的15倍?

Manticore Search介绍 Manticore Search 是一个使用 C 开发的高性能搜索引擎&#xff0c;创建于 2017 年&#xff0c;其前身是 Sphinx Search 。Manticore Search 充分利用了 Sphinx&#xff0c;显着改进了它的功能&#xff0c;修复了数百个错误&#xff0c;几乎完全重写了代码…

事务01之事务机制

事务机制 文章目录 事务机制一&#xff1a;ACID1&#xff1a;什么是ACID2&#xff1a;MySQL是如何实现ACID的 二&#xff1a;MySQL事务机制综述1&#xff1a;手动管理事务2&#xff1a;事务回滚点3&#xff1a;事务问题和隔离机制&#xff08;面试&#xff09;3.1&#xff1a;事…

NX/UG二次开发—CAM—快速查找程序参数名称

使用UF_PARAM_XXX读取或设置参数时,会发现程序中有一个INT类型参数param_index,这个就是对应程序中的参数,比如读取程序余量,则param_index = UF_PARAM_STOCK_PART,读取程序的加工坐标系则param_index = UF_PARAM_MCS等等。 你需要读取什么参数,只要只能在uf_param_indic…

X86路由搭配rtl8367s交换机

x86软路由&#xff0c;买双网口就好。或者单网口主板&#xff0c;外加一个pcie千兆。 华硕h81主板戴尔i350-T2双千兆&#xff0c;做bridge下载&#xff0c;速度忽高忽低。 今天交换机到货&#xff0c;poe供电&#xff0c;还是网管&#xff0c;支持Qvlan及IGMP Snooping&#xf…

【LLM-agent】(task1)简单客服和阅卷智能体

note 一个完整的agent有模型 (Model)、工具 (Tools)、编排层 (Orchestration Layer)一个好的结构化 Prompt 模板&#xff0c;某种意义上是构建了一个好的全局思维链。 如 LangGPT 中展示的模板设计时就考虑了如下思维链&#xff1a;Role (角色) -> Profile&#xff08;角色…

ZZNUOJ(C/C++)基础练习1021——1030(详解版)

目录 1021 : 三数求大值 C语言版 C版 代码逻辑解释 1022 : 三整数排序 C语言版 C版 代码逻辑解释 补充 &#xff08;C语言版&#xff0c;三目运算&#xff09;C类似 代码逻辑解释 1023 : 大小写转换 C语言版 C版 1024 : 计算字母序号 C语言版 C版 代码逻辑总结…