• 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()
    
    
  • 相关阅读:
    将博客搬至CSDN
    U盘启动盘 安装双系统 详细教程
    vmware安装linux6.3
    hadoop学习之路
    linux重定向总结:如何将shell命令的输出信息自动输出到文件中保存
    AVRO讲解
    MapReduce 工作原理
    lucene索引存储原理
    ES数据库系统
    分流器设备与交换机设备的区别
  • 原文地址:https://www.cnblogs.com/zhangshan33/p/11360808.html
Copyright © 2020-2023  润新知