代码编织梦想

解决思路

        粘包问题是由于TCP协议底层优化算法Nagle算法造成的。我们可以在发送数据包之前,先告诉接收方我们发送的数据量有多大,接收方就可以精确接收一个数据包或者对一个数据包进行多次接收,这样不仅能够享受到Nagle算法带来的便利,也能够有效解决粘包问题。

伪代码实现解决粘包

1、问题:一次性发送的数据长度未知,接收方不方便接收。

2、解决思路:发送方先说明发送的数据有多长,接收方再接收。

3、不足:告诉接收方的信息(我要发送的数据有多长)不是固定长度的,不符合协议规范。

服务端

    # 第一步:把数据长度发送给客户端
    total_size = len(stdout)+len(stdeer)
    conn.send(str(total_size).encode("utf-8"))
    # 第二步:再发送真实的数据
    conn.send(stdout)
    conn.send(stdeer)

客户端

    # 协议 = 报头 + 数据
    # 第一步:先收报头
    total_size = 10241
    # 第二步:接收真实的数据
    recv_size = 0
    recv_data = b""
    while recv_size < total_size:
        res = phone.recv(1024)
        recv_data += res
        recv_size += len(res)
    print(recv_data.decode("utf-8"))

解决粘包问题简单版本

1、问题:告诉接收方的信息(我要发送的数据有多长)不是固定长度的,不符合协议规范。

2、解决办法:将关于数据长度的信息打包到报头里面,这需要用到一个模块:struct。其中struct.pack("i",int)方法可以将整数(数据长度)打包成定长字节类型

  • 参数一:i表示打包的数据是整型,打包后返回值是一个定长为4的字节类型。
  • 参数二:int表示想打包的一个整数类型(不管数据长度多少,打包后长度为4)。

解包用struct.unpack()方法。

3、代码实现 

服务端

        # 3、把命令的结果返回给客户端
        # 第一步:制作固定长度的报头。
        total_size = len(stdout) + len(stdeer)
        header = struct.pack("i",total_size)
        # 第二步:把报头(固定长度)发送给客户端
        conn.send(header)
        # 第二步:再发送真实的数据
        conn.send(stdout)
        conn.send(stdeer)

客户端

    # 2、拿到命令的结果,并打印
    # 第一步:先收报头
    header = phone.recv(4)
    # 第二步:从报头中解析出对真实数据的描述信息(数据的长度)
    total_size = struct.unpack("i",header)[0]
    # 第三步:接收真实的数据
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = phone.recv(1024)
        recv_data += res
        recv_size += len(res)
    print(recv_data.decode("gbk"))

4、还存在的问题:

  1. 报头信息少;还可能有文件名等其他文本信息。
  2. 打包数据可能很大;对于struct模块中的pack方法来说,如果将数据打包成整型(i),当传入第二个参数数值很大时,打包成整型将会报错。

解决粘包问题终极版本

1、解决思路:选用字典类型作为报头,考虑到网络传输数据需要将字典转换为字节类型,同时传输后还要将字节数据解析为字典(序列化),因此调用json模块。

2、传输过程中的数据类型变化:

字典类型的报头——>转换为json对象,为str类型(序列化)——>编码,转换为字节类型——>解码,为json对象——>反序列化,变成字典类型。

        # 字典类型的数据头
        header_dic = {
            "filename": "a.txt",
            "md5": "xxxdxxx",
            'total_size': len(stdout)+len(stdeer)
        }
        # 将数据头转换为json对象,是str类型
        header_json = json.dumps(header_dic)
        # json对象转换为字节类型
        header_bytes = header_json.encode('utf-8')

3、还有一个关键点,引入字典类型后,此时报头不是定长,不符合传输规范。那么,我们可以利用struct.pack()方法将字典报头打包成定长,接收方就可以通过解析包得到报头包含的信息,再去接收数据。

conn.send(struct.pack("i",len(header_bytes)))

优化后的完整代码

服务端

import socket
import subprocess
import struct
import json

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",9910))
phone.listen(5)
print("starting...")
while True:
    conn,client_addr = phone.accept()
    print(client_addr)
    while True:
        # 1、收命令
        cmd = conn.recv(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()
        stdeer = obj.stderr.read()

        # 3、把命令的结果返回给客户端
        # 第一步:制作固定长度的报头。
        header_dic = {
            "filename": "a.txt",
            "md5": "xxxdxxx",
            'total_size': len(stdout)+len(stdeer)
        }
        header_json = json.dumps(header_dic)
        header_bytes = header_json.encode('utf-8')

        # 第二步:先发送报头的长度
        conn.send(struct.pack("i",len(header_bytes)))

        # 第三步:再发报头
        conn.send(header_bytes)

        # 第四步:再发送真实的数据
        conn.send(stdout)
        conn.send(stdeer)

    conn.close()
phone.close()

客户端

import socket
import struct
import json

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",9910))

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

    # 2、拿到命令的结果,并打印
    # 第一步:先收报头的长度
    obj = phone.recv(4)
    header_size = struct.unpack('i',obj)[0]

    # 第二步:再收报头
    header_bytes = phone.recv(header_size)

    # 第三步:从报头中解析出对真实数据的描述信息
    header_json = header_bytes.decode("utf-8")
    header_dic = json.loads(header_json)
    print(header_dic)
    total_size = header_dic["total_size"]
    print(total_size,type(total_size))

    # 第三步:接收真实的数据
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = phone.recv(1024)
        recv_data += res
        recv_size += len(res)
    print(recv_data.decode("gbk"))

phone.close()

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/hold_on_qlc/article/details/128462129

网络编程面向字节流—粘包问题_hxingwei的博客-爱代码爱编程

粘包:通俗的讲,在我们买包子的时候,我们可以看到蒸笼中,每个包子之间都隔开了空间,或者是用纸把每个包子都隔离开了,不让它们粘在一起。如果让两个包子之间无缝隙的在粘在一起。当你买包子时,老板把一个包子装起来,另个一个包子的皮就

python网络编程之——tcp粘包&udp丢包_weixin_40432363的博客-爱代码爱编程_python通过scp会丢包

一、tcp粘包问题产生的原因: 发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供

socket网络编程(五)——粘包拆包问题-爱代码爱编程

目录 1、出现粘包拆包的原因 2、粘包拆包的几种情况 3、处理粘包拆包的方法 3.1、服务端代码 3.2、客户端代码 3.3、公用的部分 今天和大家讲一下socket网络编程中粘包和拆包的问题。 1、出现粘包拆包的原因 假设一个这样的场景,客户端要利用send()函数发送字符“asd”到服务端,连续发送3次,但是服务端休眠10秒之后再去缓

Netty编程(七)—— 粘包半包(一)-爱代码爱编程

Netty编程(七)—— 粘包半包 文章目录 Netty编程(七)—— 粘包半包粘包现象半包现象现象分析粘包半包本质 粘包现象 以下面这个例子来说一下什么是粘包,下面这个是一个客户端的代码,它的任务很简单,就是向服务端连发十次0到15(十进制的字节00到0f): public class StudyClient { static

Netty编程(八)—— 粘包半包(二)-爱代码爱编程

Netty编程(八)—— 粘包半包(二) 文章目录 Netty编程(八)—— 粘包半包(二)短链接定长解码器行解码器长度字段解码器参数解析参数图解例子 在上一篇博客《Netty编程(七)—— 粘包半包(一)》中介绍了一下什么是粘包和半包,这篇博客将继续介绍Netty如何处理粘包半包问题。 短链接 短链接的思路是客户端每次向服务器发送数据以

Python网络编程——基于tcp协议实现远程执行命令、udp协议没有粘包问题、解决粘包问题、socketserver模块的基本使用(基于tcp协议、基于udp协议的使用)-爱代码爱编程

文章目录 基于tcp协议实现远程执行命令udp协议没有粘包问题解决粘包问题解决粘包问题(终极版)socketserver模块的基本使用基于tcp协议的使用基于udp协议的使用 基于tcp协议实现远程执行命令 服务端 # 服务端应该满足两个特点: # 1、一直对外提供服务 # 2、并发地服务多个客户端 import subprocess fr

网络编程学习(5)—— 基于SOCKET模拟SSH远程执行命令后出现的粘包问题及解决方案-爱代码爱编程

网络编程学习(5)—— 基于SOCKET模拟SSH远程执行命令后出现的粘包问题及解决方案 粘包问题:问题出现粘包问题:分析粘包问题:文字版解决方案粘包问题:struct 模块补充粘包问题:简单代码解决粘包问题:终极代码解决 粘包问题:问题出现 前面我们已经实现了利用socket模拟远程执行命令,但是我们在代码运行中很有可能会遇到这样的问题,如

网络编程学习——TCP-爱代码爱编程

网络编程 TCP 一、确认应答ACK 序号和确认序号 二、超时重传 如果ACK丢了就会导致接收方收到一样的数据,TCP会自动在接收缓冲区里根据序号进行重传,保证接收方不收到重复数据 三、连接管理 三次握手:建立连接 为什么要三次握手?为了可以投石问路,确认当前网络环境是通畅的,可以进行可靠传输 四次挥手:断开连接四、滑动窗口 1. 滑

c++面试最常见问题(四)——网络编程部分_小无欢的博客-爱代码爱编程

系列文章目录 #C++面试最常见问题(一) #C++面试最常见问题(二) #C++面试最常见问题(三) 文章目录 系列文章目录一、OSI七层模型是什么?每层主要完成什么任务?二、简述三次握手和四次挥手三、TCP和UDP是什么,它们之间有什么联系和区别?四、TCP、UDP使用场景五、TCP如何维护可靠的通信方式?六、TCP中三次握手可以变为两次握

nx 系统环境 python3.6 部署 ppocr 报错记录-爱代码爱编程

NX 系统环境 python3.6 部署 PPOCR 报错记录 前言(这环境,就硬配) 问:为什么要用系统环境,不用 conda?答:因为 conda 的 ARM 端 python 最低只支持 3.7,而 paddlep

python学习基础笔记六十二——反射2-爱代码爱编程

1、 isinstanace(obj, cls)   # 检查是否obj是否是类cls的对象: class Foo(object): pass obj = Foo() print(isinstance(obj, Foo)) 结果返回:True。 issubclass(sub, super)    # 检查sub类是否是 super

diffusion model(一): 公式推导详解-爱代码爱编程

本文目录 前言Diffusion的前向过程Diffusion的反向过程 前言 本文一共分为三大部分,这是第一部分 Diffusion model(一): 公式推导详解 Diffusion mode

吴恩达《机器学习》——logistic多分类与神经网络-爱代码爱编程

Logistic多分类与神经网络 1. MINIST数据集与Logistic多分类MINIST简介数据集可视化 Logistic如何实现多分类?One-Hot向量 Python实现 2.