粘包的原理与解决

粘包现象

须知:只有TCP有粘包现象,UDP永远不会粘包
粘包不一定会发生
如果发生了:1.可能是在客户端已经粘了
      2.客户端没有粘,可能是在服务端粘了
所谓粘包问题主要还是因为接收方不知道消息之间的界限 还有系统缓存区的问题 时间差的原因,不知道一次性提取多少字节的数据所造成的。

socket收发消息的原理

img

什么是缓冲区

输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:1024字节=1k

![1565859303401](file:///C:/Users/86131/AppData/Roaming/Typora/typora-user-images/1565859303401.png)

缓冲区的作用?

存储少量数据
如果你的网络出现短暂的异常或者波动,接收数据就会出现短暂的中断,影响你的下载或者上传的效率.
但是 凡是都是双刃剑,缓冲区解决了上传下载的传输效率的问题,带来了黏包问题.

为什么出现粘包?

第一种.连续短暂的send多次(数据量很少),你的数据会统一发送出去,
(不可控)
第二种: send的数据过大,大于对方recv的上限时,对方第2次recv时,会接收上一次没有recv完的剩余的
1,接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)** recv会产生黏包(如果recv接受的数据量(1024)小于发送的数据量,第一次只能接收规定的数据量1024,第二次接收剩余的数据量)
2,发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包)**send 也可能发生粘包现象.(连续send少量的数据发到输出缓冲区,由于缓冲区的机制,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络)

解决粘包现象

错误实例:

1. 可以扩大recv的上限. recv(10240000000000000000000000000) 不是解决这个问题的根本原因, 8g, 10G,这些都会直接放在内存中.
2. 故意延长recv的时间. sleep 这样会非常影响效率.

recv工作原理

源码解释:
Receive up to buffersize bytes from the socket.
接收来自socket缓冲区的字节数据,
For the optional flags argument, see the Unix manual.
对于这些设置的参数,可以查看Unix手册。
When no data is available, block untilat least one byte is available or until the remote end is closed.
当缓冲区没有数据可取时,recv会一直处于阻塞状态,直到缓冲区至少有一个字节数据可取,或者远程端关闭。
When the remote end is closed and all data is read, return the empty string.
关闭远程端并读取所有数据后,返回空字符串。

low版解决粘包现象

server

# 原因# 1. 粘包第一种: send的数据过大,大于对方recv的上限时,对方第二次recv时,会接收上一次没有recv完的剩余的数据。
# 解决方式 服务端 发送时记录字节长度 在将不固定第字节长度 利用模块转化为固定4个长度bytes
# 先发送4个字节过去 在发送原内容
# 客户端 先接受固定4个字节 在利用固定的4个字节bytes利用模块转回原来的字节个数(int)
# 利用while循环接收

# 利用的模块
# import struct
# # 将一个数字转化成等长度的bytes类型。
# ret = struct.pack('i', 180000000)
# # print(ret, type(ret), len(ret))
#
# # 通过unpack反解回来
# ret1 = struct.unpack('i',ret)[0]
# # print(ret1)
# print(ret1, type(ret1))
# import struct
# import subprocess
# obj = subprocess.Popen('dir1',
#                        shell=True,
#                        stdout=subprocess.PIPE,
#                        stderr=subprocess.PIPE,
#                        )
# print(obj.stdout.read().decode('gbk'))  # 正确命令
# print(obj.stderr.read().decode('gbk'))  # 错误命令

import struct
import subprocess
import socket
phone=socket.socket()
phone.bind(('127.0.0.1',6666))
phone.listen(5)
while 1:
    coon, addr = phone.accept()
    while 1:
        try:
            from_client_data=coon.recv(1024)
            print(f'来自{addr}消息{from_client_data.decode("utf-8")}')
            obj = subprocess.Popen(from_client_data.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE,
                                   )
            to_client_data=(obj.stderr.read().decode('gbk')+obj.stdout.read().decode('gbk')).encode('utf-8')#总字节数
            total_size=len(to_client_data)#总字节数长度
            # print(total_size)
            # 将一个数字转化成等长度的bytes类型。
            size = struct.pack('i', total_size)
            coon.send(size)#发送固定4个字节
            coon.send(to_client_data)#发送总数据
        except Exception:
            break
    coon.close()
phone.close()

client

import socket
import struct
phone=socket.socket()
phone.connect(('127.0.0.1',6666))
while 1:
    to_cerver_data=input('????')
    if not to_cerver_data:
        # 服务端如果接受到了空的内容,服务端就会一直阻塞中,所以无论哪一端发送内容时,都不能为空发送
        print('发送内容不能为空')
        continue
    phone.send(to_cerver_data.encode('utf-8'))
    from_server_size=phone.recv(4)#接收固定4个字节
    from_server_total= struct.unpack('i',from_server_size)[0] # 反解报头
    tatal=b''
    while len(tatal)<from_server_total:
        tatal += phone.recv(1024)
    print(tatal.decode('utf-8'))

phone.close()

旗舰版解决黏包

优点

1.高大上版:自定制报头
2.高大上版:可以解决文件过大的问题.

server端

import socket
import struct
import json
# 1. 创建socket对象(买手机) 左边是 右边是
# phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone = socket.socket() # 可以默认不写
# 连接服务器ip地址与端口 左边服务器IP地址 右边端口
phone.connect(('127.0.0.1', 9999))
# 发消息
while 1:
    try:
        to_server = input('>>>').strip()
        if to_server.upper() == 'Q':
            phone.send('q'.encode('utf-8'))#发消息
            break
        phone.send(to_server.encode('utf-8'))#send 发送

        # 第1步 先读前4个 此时读的是字典长度
        head_4 = phone.recv(4)
        #第2步 将固定4个byres 转化为字典byes原长度数字 此时是数字类型
        ret1 = struct.unpack('i',head_4)[0]
        ##第3步 读取 字典长度byres
        head_dic_len = phone.recv(ret1)
        #第4步将字典 byres解码 转化为 特殊的字符串
        head_dic_json=head_dic_len.decode('utf-8')
        #第5步将 json转化为字典
        head_dic=json.loads(head_dic_json)
        #第6步获取 字典有用的信息 比如文件byres 长度
        place_len=head_dic['file_size']
        #第7步读取 原文件
        place = phone.recv(place_len)
        print(place.decode('utf-8'))
    except ConnectionResetError:
        print('对方服务器崩了。。。。')
        break
# 关机
phone.close()#关闭手机
#ipconfig

![1563352971686](file:///C:/Users/86131/AppData/Roaming/Typora/typora-user-images/1563352971686.png)

client端

#第一步制作报头 head 头  #第2步将字典转为json字符串 #第3步 将json字符串 转为 byes类型 #第4步将json的byres 长度转为固定4个 byres #第5步 发送 固定4个byres total_size_byres   #第6步 发送字典byres 给客服端  # 第7步 发送原内容 给客服端

import socket#建立通讯
import subpr1111ocess#远程返回
import struct#将该模块可以把一个类型,如数字,转成固定长度的bytes
import json
phone = socket.socket()#买手机
phone.bind(('127.0.0.1', 9999))#插卡
phone.listen(5)#开放监听
# 4. 接收连接
print('start')
conn, addr = phone.accept()
while 1:
    try:
        cmd = conn.recv(1024) #  接收发送端的消息
        obj = subprocess.Popen(cmd.decode('utf-8'),
                               shell=True,
                               stdout=subprocess.PIPE,#正确命令
                               stderr=subprocess.PIPE,#错误命令
                               )
        #返回数据  由于有黏包现象 客户端接收不了这么多
        result = obj.stdout.read() + obj.stderr.read()#gbk 格式
        result = result.decode('gbk').encode('utf-8')#解码成utf-8
        #第一步制作报头 head 头
        head_dict={'file_size':len(result)}
        #第2步将字典转为json字符串
        head_dict_json = json.dumps(head_dict)
        #第3步 将json字符串 转为 byes类型
        head_dict_byres=head_dict_json.encode('utf-8')
        #第4步将json的byres 长度转为固定4个 byres
        total_size_byres=struct.pack('i',len(head_dict_byres))
        #第5步 发送 固定4个byres total_size_byres
        conn.send(total_size_byres)
        #第6步 发送字典byres 给客服端
        conn.send(head_dict_byres)
        # 第7步 发送原内容 给客服端
        conn.send(result)
    except ConnectionResetError:
        break
conn.close()
phone.close()

原文地址:https://www.cnblogs.com/saoqiang/p/12388422.html