Python 第三十章 粘包现象

粘包现象

'''
什么是粘包?
有缓存区,缓存区里放响应的字节,满了后,将剩下的存起来
等下次再接受信息的时候,先将上次剩下的输出
再次接受信息的时候,将上次没有输出的信息输出
    在TCP协议中,发送方发送的若干包数据到接收方,接收时粘成一包
从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

为什么设置缓冲区?
断网后数据会丢失
下载数据断网后再联网做个缓冲控制速度均衡
1、暂时存储一些数据
2、网络有波动,保证数据的收发稳定和均速
缺点:造成了粘包现象之一

什么情况下出现粘包
1、send:数据过大,大过对方recv的上限时,对方第二次recv时,会接收上一次没有recv完的剩余数据
2、send:连续短暂的send多次(传送数据量较小),统一存到了recv缓存区;再统一发送出去
多少算小

展示一些收发的内容
只要send就要转成bytes类型

 如何解决粘包现象
 服务端发一次数据:10000字节
 客户端接受数据时,循环接收,每次最多接收1024个字节,直到将10000所有的字节全部接收完毕,
 将接收的数据拼接在一起,最后解码

 1、遇到的问题:recv的次数无法确定
    发送总具体数据之前,先给一个总数据的长度,然后再发总数据
    客户端先接收一个长度,再循环recv控制循环的条件就是只要接收的数据小于总长度,就一直循环
 2、遇到的问题:
    总数据的长度转成字节
    要解决将不固定长度的int类型转成固定的bytes字节,且还能翻转回来
    要将total_size int类型转成bytes类型才能发送
    
    服务端:
    conn.send(total_size)
    conn.send(result)
    total_size int 类型
    
    客户端:
    total_size_bytes = phone.recv(4)
    total_size
    data = b''
    while len(data) < total_size:
        data = data + phone.recv(1024)
    将total_size int 类型转化成bytes类型才可以发送
    将不固定长度的int类型转化成固定长度的bytes并且可以翻转回来
    387 --> str(387)'387' -->bytes b'387' 长度 3bytes
    4185 --> str(4185)'4185' --> bytes b'387' 长度  4bytes

什么时候用bytes类型:网络传输,文件存储时
'''

low 版解决方案

解决思路

# 解决思路
# import struct
# # 制作报头pack 将一个数据类型10000,转成等4个字节的长度的bytes类型i
# ret = struct.pack('i',10000)
# # 输出报头 报头的类型 报头的长度
# print(ret,type(ret),len(ret))
#
# # 反解报头unpack 将报头按照每4个字节的长度进行反解 索引位置取第一个
# ret1 = struct.unpack('i',ret)[0]
# # 输出反解的报头 报头的类型
# print(ret1,type(ret1))
#
# # 总数据
# s1 = 'jgijrhgie总数据'
# # 将总数据encode 转成字节型
# b1 = s1.encode('utf-8')
# # 输出字节型的总数据 总数据的长度
# print(b1,len(b1))

旗舰版解决方案

server端

# 第一种粘包,send的数据超过recv的最大限度,第二次recv时会接收上次recv的剩余数据

# 一个聊完了继续往下接客户
# 导入socket模块
import socket # socket 一组接口
# 导入subprocess模块
import subprocess # subprocess 远程命令
# 导入结构模块
import struct # struct 转化各种数据类型

# 买电话 实例化一个phone对象 socket.socket方法
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# socket.socket()方法 默认可以不写 默认就是基于TCP协议的socket  基于网络的是tcp  基于文件的是upd
# socket.AF_INET,socket.SOCK_STREAM 是基于TCP协议的socket

# 绑定电话卡 bind方法 绑定本地IP地址和端口
phone.bind(('127.0.0.1',8847)) # 元组的形式
# 127.0.0.1本地回环地址,只能自己连,别的都不能连

# 开机监听 listen方法
phone.listen(2)
# 参数可以默认不写  写了之后允许连接有限制 多余的会进行等待
# listen:允许2个人同时连接,先跟一个人聊,聊完了再跟另一个人聊 剩下的链接可以链接也可以等待
# 实际有3个,有一个在于服务端建立链接


print('等待连接')
# while 循环接收链接
while 1:
# 开启循环链接模式,同时开3个,先连接第一个人,第一个结束后,去找下一个人
# 3个人同时发送消息,先接受第一个人的,等第一个退出后,直接链接下一个人


    # 阻塞 等待客户端链接服务端,阻塞状态中 accent方法
    conn,addr = phone.accept()
    # conn 双向通信管理,连接作用
    # addr 客户端的ip地址和端口号

    # 输出 conn通道信息和addrIP地址和端口
    print(f'链接来了:{conn,addr}')
# while 循环输入连接
    while 1:
        # try exception 异常处理
        # try 正常情况的代码
        try:
            # from_client_data 来自客户端的消息
            # 接收到客户端的消息 recv方法 缓存区
            from_client_data = conn.recv(1024)
            # conn.recv(1024)  通道里面的缓冲区最多接收1024个字节
            # 1024 最多接收1024个字节

            # if判断 来自客户端的信息是Q
            if from_client_data.upper() == b'Q':
            # upper() 给服务端的消息转化成大写
            # b'Q' 通过b方法将Q转成字节型

                # 输出正常退出
                print('客户端正常退出聊天了')
                # 终止循环
                break

            obj = subprocess.Popen(from_client_data.decode('utf-8'),

                                   shell = True, # shell 命令解释器,相当于调用cmd 执行指定的命令
                                   stdout = subprocess.PIPE, # 正确的命令 丢到管道中
                                   stderr = subprocess.PIPE, # 错误的命令 丢到另一个管道中
                                   )
            # obj = subprocess.Popen() obj对象 = subprocess模块.Popen方法
            # from_client_data 来自客户端的信息
            # decode('utf-8') 解码成utf-8类型
            # windows操作系统默认编码是gbk编码

            print(obj) # <subprocess.Popen object at 0x1030de2e8> 得到对象的地址
            # result 同时输入正确的方法和错误的方法
            result = obj.stdout.read() + obj.stderr.read()
            # obj.stdout.read() 正确的管道
            # obj.stderr.read() 错误的管道
            print(result) # 得到所有管道的信息
            # 获取到总字节数
            total_size = len(result)
            # total_size总字节数 len方法(result)得到的管道信息
            print(f'总字节数:{total_size}')

            # 1.制作固定长度的报头 struct.pack方法
            head_bytes = struct.pack('i',total_size)
            # head_bytes报头数 实例化对象
            # struct.pack struct模块 pack制作方法
            # ('i',total_size) i固定长度4 将总长度转换成每四个字节的长度,制作一个报头
            print(head_bytes) # b'xb4	x00x00' 4个字节

            # 2.发送固定长度的报头 send方法
            conn.send(head_bytes) # head_bytes报头

            # 3. 发送总数据 send方法
            conn.send(result) # result 总数据
        # except 异常处理
        except ConnectionAbortedError:
        # ConnectionAbortedError 错误类型

            # 输出 客户端断了
            print('客户端链接中断了')
            # 终止while输入循环
            break

    # 退出链接循环 关闭双向通信连接
    conn.close()

# 挂断电话 关闭连接
phone.close()

客户端

# 客户端发一个q可以正常退出,且不能输入空
# 导入socket模块
import socket  # 一组接口
# 导入struct模块
import struct

# 买电话 实例化一个phone对象
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 默认就是基于TCP协议的socket  AF_INET,socket.SOCK_STREAM基于网络的

# 拨打电话 connect方法 连接服务器ip地址
phone.connect(('127.0.0.1', 8847))
# 127.0.0.1 服务端的ip地址
# 8848 端口是客户端随机分配的

# 循环接收,发送消息
while 1:
    # to_server_data 给服务端的消息
    to_server_data = input('客户端输入(输入q或者Q退出):').strip().encode('utf-8')
    # 发送空字符串服务端会阻塞 加个if判断,不能为空
    # 同时开多个,提示'输入'表示已经链接

    # if 判读 发给服务端的消息不为空
    if not to_server_data:
    # 服务端如果接受到了空的内容,服务端就会一直阻塞中,无论哪一方发送消息时,都不能为空
    # 必须一个recv 一个send

        # 输出提示
        print('输入的内容不能为空')
        # 继续判断是不是为空
        continue
    # send方法 发送消息
    phone.send(to_server_data)

    # if 判读发送给服务端的消息是不是q
    if to_server_data.upper() == b'Q':
    # upper() 给服务端的消息转化成大写
    # b'Q' 通过b方法将Q转成字节型

        # 是Q就终止循环
        break
    # 1.接收报头recv 按照每4个字节的长度接收报头 得到head_bytes报头
    head_bytes = phone.recv(4)

    # 2.反解报头unpack 按照每4个字节的长度反解报头 得到total_size总长度
    total_size = struct.unpack('i',head_bytes)[0]

    # 3.接收总数据
    total_data = b''
    # total_data 总数据是字节型
    while len(total_data) < total_size:
        total_data += phone.recv(1024)
    # while循环
    # len(total_data)总数据的长度 < total_size总长度
    # total_data总数据 += 1024个接收到的数据

    # 输出 总数据的长度
    print(len(total_data))
    # 输出总数据转成utf-8类型
    print(total_data.decode('utf-8'))

# 关闭电话 关闭连接
phone.close()

纯代码

服务端:
import socket
import subprocess
import struct

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

phone.bind(('127.0.0.1', 8848))

phone.listen(5)

while 1:
    conn, client_addr = phone.accept()
    print(client_addr)

    while 1:
        try:
            cmd = conn.recv(4)
            ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            correct_msg = ret.stdout.read()
            error_msg = ret.stderr.read()

            # 1 制作固定报头
            total_size = len(correct_msg) + len(error_msg)
            header = struct.pack('i', total_size)

            # 2 发送报头
            conn.send(header)

            # 3、发送总数据:
            conn.send(correct_msg)
            conn.send(error_msg)
        except ConnectionResetError:
            break

    conn.close()
phone.close()

客户端:
import socket
import struct

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

phone.connect(('127.0.0.1', 8848))

while 1:
    cmd = input('>>>').strip()
    if not cmd: continue
    phone.send(cmd.encode('utf-8'))

    # 1,接收固定报头
    header = phone.recv(4)

    # 2,反解报头
    total_size = struct.unpack('i', header)[0]

    # 3,根据报头信息,接收总数据
    recv_size = 0
    res = b''

    while recv_size < total_size:
        recv_data = phone.recv(4)
        res += recv_data
        recv_size += len(recv_data)

    print(res.decode('utf-8'))

phone.close()

# 但是low版本有问题:
# 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。
# 2,通过struct模块直接数据处理,不能处理太大。
# 为解决这些问题,需使用旗舰版

服务端

# 第一种粘包,send的数据超过recv的最大限度,第二次recv时会接收上次recv的剩余数据

# 一个聊完了继续往下接客户
# 导入socket模块
import socket # socket 一组接口
# 导入subprocess模块
import subprocess # subprocess 远程命令
# 导入结构模块
import struct # struct 转化各种数据类型
# 导入文件写入模块
import json

# 买电话 实例化一个phone对象 socket.socket方法
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# socket.socket()方法 默认可以不写 默认就是基于TCP协议的socket  基于网络的是tcp  基于文件的是upd
# socket.AF_INET,socket.SOCK_STREAM 是基于TCP协议的socket

# 绑定电话卡 bind方法 绑定本地IP地址和端口
phone.bind(('127.0.0.1',8847)) # 元组的形式
# 绑定地址(host,port)到套接字,在AF_INET下,以元组(host,port)的形式表示地址
# 127.0.0.1本地回环地址,只能自己连,别的都不能连

# 开机监听 listen方法
phone.listen(2)
# 开启TCP监听,backing指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分程序设为5即可
# 参数可以默认不写  写了之后允许连接有限制 多余的会进行等待
# listen:允许2个人同时连接,先跟一个人聊,聊完了再跟另一个人聊 剩下的链接可以链接也可以等待
# 实际有3个,有一个在于服务端建立链接

print('等待连接')
# while 循环接收链接 多个用户同时连接(不是并发,是排队连接)
while 1:
# 开启循环链接模式,同时开3个,先连接第一个人,第一个结束后,去找下一个人
# 3个人同时发送消息,先接受第一个人的,等第一个退出后,直接链接下一个人


    # 阻塞 等待客户端链接服务端,阻塞状态中 accent方法
    conn,addr = phone.accept()
    # 被动接收TCP客户端链接,阻塞式等待连接的到来,会有两个值
    # conn 双向通信管理,连接作用
    # addr 客户端的ip地址和端口号

    # 输出 conn通道信息和addrIP地址和端口
    print(f'链接来了:{conn,addr}')
# while 循环输入连接 客户端与服务端多次对话,客户端多次执行循环
    while 1:
        # try exception 异常处理
        # try 正常情况的代码
        try:
            # from_client_data 来自客户端的消息
            # 接收到客户端的消息 recv方法 缓存区
            from_client_data = conn.recv(1024)
            # conn.recv(1024)  通道里面的缓冲区最多接收1024个字节
            # 1024 最多接收1024个字节

            # if判断 来自客户端的信息是Q
            if from_client_data.upper() == b'Q':
            # upper() 给服务端的消息转化成大写
            # b'Q' 通过b方法将Q转成字节型

                # 输出正常退出
                print('客户端正常退出聊天了')
                # 终止循环
                break

            obj = subprocess.Popen(from_client_data.decode('utf-8'),

                                   shell = True, # shell 命令解释器,相当于调用cmd 执行指定的命令
                                   stdout = subprocess.PIPE, # 正确的命令 丢到管道中
                                   stderr = subprocess.PIPE, # 错误的命令 丢到另一个管道中
                                   )
            # obj = subprocess.Popen() obj对象 = subprocess模块.Popen方法
            # from_client_data 来自客户端的信息
            # decode('utf-8') 解码成utf-8类型
            # windows操作系统默认编码是gbk编码

            print(obj) # <subprocess.Popen object at 0x1030de2e8> 得到对象的地址
            # result 将字节相加 同时输入正确的输出和错误的输出
            result = obj.stdout.read() + obj.stderr.read()
            # obj.stdout.read() 正确的管道
            # obj.stderr.read() 错误的管道
            # 正确的输出时,错误输出为空,相当于0+1 没有什么区别

            print(result) # 得到所有管道的信息
            # 获取到总字节数
            total_size = len(result)
            # total_size总字节数 len方法(result)得到的管道信息
            print(f'总字节数:{total_size}')

            # 1.自定义报头
            head_dic = {
                'file_name' : 'test1',
                'md5' : 248354756,
                'total_size' : total_size,
            }

            # 2.字典形式的报头转成json类型
            head_dic_json = json.dumps(head_dic)

            # 3.json形式的报头转成bytes类型
            head_dic_json_bytes = head_dic_json.encode('utf-8')

            # 4.获取bytes类型的总长度
            len_head_dic_json_bytes = len(head_dic_json_bytes)

            # 5.转成4个字节的固定长度
            four_head_bytes = struct.pack('i',len_head_dic_json_bytes)

            # 6.发送4个字节的固定长度
            conn.send(four_head_bytes)

            # 7.发送bytes类型的报头
            conn.send(head_dic_json_bytes)

            # 8.发送总数据
            conn.send(result)
        # except 异常处理
        except ConnectionAbortedError:
        # ConnectionAbortedError 错误类型

            # 输出 客户端断了
            print('客户端链接中断了')
            # 终止while输入循环
            break

    # 退出链接循环 关闭双向通信连接
    conn.close()

# 挂断电话 关闭连接
phone.close()

# 但是low版本有问题:
# 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。
# 2,通过struct模块直接数据处理,不能处理太大。


# 解决思路
# import struct
# # 制作报头pack 将一个数据类型10000,转成等4个字节的长度的bytes类型i
# ret = struct.pack('i',10000)
# # 输出报头 报头的类型 报头的长度
# print(ret,type(ret),len(ret))
#
# # 反解报头unpack 将报头按照每4个字节的长度进行反解 索引位置取第一个
# ret1 = struct.unpack('i',ret)[0]
# # 输出反解的报头 报头的类型
# print(ret1,type(ret1))
#
# # 总数据
# s1 = 'jgijrhgie总数据'
# # 将总数据encode 转成字节型
# b1 = s1.encode('utf-8')
# # 输出字节型的总数据 总数据的长度
# print(b1,len(b1))

客户端

# 客户端发一个q可以正常退出,且不能输入空
# 导入socket模块
import socket  # 一组接口
# 导入struct模块
import struct
# 导入json模块
import json

# 买电话 实例化一个phone对象
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 默认就是基于TCP协议的socket  AF_INET,socket.SOCK_STREAM基于网络的

# 拨打电话 connect方法 连接服务器ip地址
phone.connect(('127.0.0.1', 8847))
# 主动初始化TCP服务连接,address的格式为元组,如果连接出错,返回socket.error错误
# 127.0.0.1 服务端的ip地址
# 8848 端口是客户端随机分配的

# 循环接收,发送消息
while 1:
    # to_server_data 给服务端的消息
    to_server_data = input('客户端输入(输入q或者Q退出):').strip().encode('utf-8')
    # 发送空字符串服务端会阻塞 加个if判断,不能为空
    # 同时开多个,提示'输入'表示已经链接

    # if 判读 发给服务端的消息不为空
    if not to_server_data:
    # 服务端如果接受到了空的内容,服务端就会一直阻塞中,无论哪一方发送消息时,都不能为空
    # 必须一个recv 一个send

        # 输出提示
        print('输入的内容不能为空')
        # 继续判断是不是为空
        continue
    # send方法 发送消息
    phone.send(to_server_data)

    # if 判读发送给服务端的消息是不是q
    if to_server_data.upper() == b'Q':
    # upper() 给服务端的消息转化成大写
    # b'Q' 通过b方法将Q转成字节型

        # 是Q就终止循环
        break
    # 1.接收4个字节的报头
    head_bytes = phone.recv(4)

    # 2.获取bytes类型字典的总字节数
    len_head_dic_json_bytes = struct.unpack('i',head_bytes)[0]

    # 3.接收bytes形式的dic数据
    head_dic_json_bytes = phone.recv(len_head_dic_json_bytes)

    # 4.转成json类型的dic数据
    head_dic_json = head_dic_json_bytes.decode('utf-8')

    # 5.反解成字典格式的报头
    head_dic = json.loads(head_dic_json)

    # total_data总字节数 b是字节型 初始化为''
    total_data = b''
    # while循环 接收到的总字节数小于总长度 则一直循环
    while len(total_data) < head_dic['total_size']:
        total_data += phone.recv(1024)
    # len(total_data)总字节数 < total_size总长度
    # total_data总数据每次进行添加 += 1024个接收到的数据

    # 输出 总数据的长度
    print(len(total_data))
    # 输出总数据转成utf-8类型
    print(total_data.decode('utf-8'))

# 关闭电话 关闭连接
phone.close()

纯代码

# FTP 应用层自定义协议
'''
1. 高大上版: 自定制报头
dic = {'filename': XX, 'md5': 654654676576776, 'total_size': 26743}
2. 高大上版:可以解决文件过大的问题.
'''
服务端:
# import struct

# ret = struct.pack('Q',21321432423544354365563543543543)
# print(ret)

import socket
import subprocess
import struct
import json
phone = socket.socket()

phone.bind(('127.0.0.1',8848))

phone.listen(2)
# listen: 2 允许有两个客户端加到半链接池,超过两个则会报错

while 1:
    conn,addr = phone.accept()  # 等待客户端链接我,阻塞状态中
    # print(f'链接来了: {conn,addr}')

    while 1:
        try:

            from_client_data = conn.recv(1024)  # 接收命令


            if from_client_data.upper() == b'Q':
                print('客户端正常退出聊天了')
                break

            obj = subprocess.Popen(from_client_data.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE,

                                   )
            result = obj.stdout.read() + obj.stderr.read()
            total_size = len(result)

            # 1. 自定义报头
            head_dic = {
                'file_name': 'test1',
                'md5': 6567657678678,
                'total_size': total_size,

            }
            # 2. json形式的报头
            head_dic_json = json.dumps(head_dic)

            # 3. bytes形式报头
            head_dic_json_bytes = head_dic_json.encode('utf-8')

            # 4. 获取bytes形式的报头的总字节数
            len_head_dic_json_bytes = len(head_dic_json_bytes)

            # 5. 将不固定的int总字节数变成固定长度的4个字节
            four_head_bytes = struct.pack('i',len_head_dic_json_bytes)

            # 6. 发送固定的4个字节
            conn.send(four_head_bytes)

            # 7. 发送报头数据
            conn.send(head_dic_json_bytes)

            # 8. 发送总数据
            conn.send(result)

        except ConnectionResetError:
            print('客户端链接中断了')
            break
    conn.close()
phone.close()
客户端:
import socket
import struct
import json
phone = socket.socket()

phone.connect(('127.0.0.1',8848))
while 1:
    to_server_data = input('>>>输入q或者Q退出').strip().encode('utf-8')
    if not to_server_data:
        # 服务端如果接受到了空的内容,服务端就会一直阻塞中,所以无论哪一端发送内容时,都不能为空发送
        print('发送内容不能为空')
        continue
    phone.send(to_server_data)
    if to_server_data.upper() == b'Q':
        break

    # 1. 接收固定长度的4个字节
    head_bytes = phone.recv(4)

    # 2. 将报头反解回int类型
    len_head_dic_json_bytes = struct.unpack('i',head_bytes)[0] # 获取到数据总长度

    # 3.接收bytes类型的字典的总字节数
    head_dic_json_bytes = phone.recv(len_head_dic_json_bytes)

    # 4.将bytes类型解码成json类型
    head_dic_json = head_dic_json_bytes.decode('utf-8')

    # 5.将json类型转换成字典形式的报头
    head_dic=json.loads(head_dic_json)
    '''
    head_dic = {
                'file_name': 'test1',
                'md5': 6567657678678,
                'total_size': total_size,

            }
    '''
    total_data = b''
    while len(total_data) < head_dic['total_size']:
        total_data += phone.recv(1024)

    # print(len(total_data))
    print(total_data.decode('gbk'))

phone.close()

原文地址:https://www.cnblogs.com/zhangshan33/p/11360808.html