黏包现象

黏包现象

我们先来看一个案例:

server端:

server = socket.socket()
server.bind(('127.0.0.1', 8081))
server.listen(5)

sock, addr = server.accept()
data1 = sock.recv(1024)
print(data1)
data2 = sock.recv(1024)
print(data2)
data3 = sock.recv(1024)
print(data3)

sock.close()
server.close()

client端:

import socket

client = socket.socket()
client.connect(('127.0.0.1', 8081))

client.send(b'hello kevin')
client.send(b'jason say hei')
client.send(b'jerry say goodbye')

服务端收到的结果如下:

上述现象称之为黏包现象:

  1.服务端连续执行三次recv
  2.客户端连续执行三次send

  结果:服务端一次性接收到了客户端三次的消息,该现象称为"黏包现象"。

注意:只有TCP有粘包现象,UDP永远不会粘包

 

黏包现象产生的原因:

1.接收方不知道消息之间的界限,不知道一次性提取多少字节的数据
2.TCP也称为流式协议:数据像水流一样绵绵不绝没有间隔(TCP会针对数据量较小且发送间隔较短的多条数据一次性合并打包发送)

黏包的解决方案

 避免黏包现象的核心思路\关键点:
       如何明确即将接收的数据具体有多大

  如何将长度变化的数据全部制作成固定长度的数据?

接下来我们介绍一个模块:

struct模块

该模块可以把一个类型,如数字,转成固定长度的bytes

struct.pack('i',1111111111111)

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

 

struct模块作用:将数据打包成固定的长度

import struct

info = b'hello big baby'
print(len(info))  # 数据真实的长度(bytes)  14
res = struct.pack('i', len(info))  # 将数据打包成固定的长度 i是固定的打包模式
print(len(res))  # 打包之后长度为(bytes)   4           报头

real_len = struct.unpack('i', res)
print(real_len)  # (14,)             根据固定长度的报头 解析出真实数据的长度


desc
= b'hello my baby I will take you to play big ball' print(len(desc)) # 数据真实的长度(bytes) 46 res1 = struct.pack('i', len(desc)) print(len(res1)) # 打包之后长度为(bytes) 4 报头 real_len1 = struct.unpack('i', res1) print(real_len1) # (46,) 根据固定长度的报头 解析出真实数据的长度

解决黏包问题初级版本

客户端
  1. 将真实数据转成bytes类型并计算长度
  2. 利用struct模块将真实长度制作一个固定长度的报头
  3. 将固定长度的报头先发送给服务端,服务端只需要在recv括号内填写固定长度的报头数字即可
  4. 然后再发送真实数据

服务端
  1. 服务端先接收固定长度的报头
  2. 利用struct模块反向解析出真实数据长度
  3. recv接收真实数据长度即可

但是上述方案也存在一些问题:

问题1struct模块无法打包数据量较大的数据,就算换更大的模式也不行

res = struct.pack('i', 12313213123)
print(res)    # struct.error: argument out of range

问题2:报头能否传递更多的信息,比如要传递一个电影,电影大小、电影名称、电影评价、电影简介

解决黏包问题的终极方案

  字典作为报头打包:效果更好,数字更小

  客户端
    1. 制作真实数据的信息字典(数据长度、数据简介、数据名称)
    2. 利用struct模块制作字典的报头
    3. 发送固定长度的报头(解析出来是字典的长度)
    4. 发送字典数据
    5. 发送真实数据
  服务端
    1.接收固定长度的字典报头
    2.解析出字典的长度并接收
    3.通过字典获取到真实数据的各项信息
    4.接收真实数据长度

黏包问题代码实操

client端:

import socket
import os
import struct
import json

client = socket.socket()
client.connect(('127.0.0.1', 8081))

'''任何文件都是下列思路 图片 视频 文本 ...'''
# 1.获取真实数据大小
file_size = os.path.getsize(r'/Users/jiboyuan/PycharmProjects/day36/xx老师合集.txt')
# 2.制作真实数据的字典数据
data_dict = {
    'file_name': '有你好看.txt',
    'file_size': file_size,
    'file_desc': '内容很长 准备好吃喝 我觉得营养快线挺好喝',
    'file_info': '这是我的私人珍藏'
}
# 3.制作字典报头
data_dict_bytes = json.dumps(data_dict).encode('utf8')
data_dict_len = struct.pack('i', len(data_dict_bytes))
# 4.发送字典报头
client.send(data_dict_len)  # 报头本身也是bytes类型 我们在看的时候用len长度是4
# 5.发送字典
client.send(data_dict_bytes)
# 6.最后发送真实数据
with open(r'/Users/jiboyuan/PycharmProjects/day36/xx老师合集.txt', 'rb') as f:
    for line in f:  # 一行行发送 和直接一起发效果一样 因为TCP流式协议的特性
        client.send(line)
import time
time.sleep(10)

server端:

import socket
import struct
import json


server = socket.socket()
server.bind(('127.0.0.1', 8081))
server.listen(5)

sock, addr = server.accept()
# 1.接收固定长度的字典报头
data_dict_head = sock.recv(4)
# 2.根据报头解析出字典数据的长度
data_dict_len = struct.unpack('i', data_dict_head)[0]
# 3.接收字典数据
data_dict_bytes = sock.recv(data_dict_len)
data_dict = json.loads(data_dict_bytes)  # 自动解码再反序列化
# 4.获取真实数据的各项信息
total_size = data_dict.get('file_size')
with open(data_dict.get('file_name'), 'wb') as f:
    f.write(sock.recv(total_size))
接收真实数据的时候,如果数据量非常大,recv括号内直接填写该数据量,不太合适,我们可以每次接收一点点,反正知道总长度
total_size = data_dict.get('file_size')
recv_size = 0
with open(data_dict.get('file_name'), 'wb') as f:
    while recv_size < total_size:
        data = sock.recv(1024)
        f.write(data)
        recv_size += len(data)
        print(recv_size)

 

posted @ 2022-11-17 15:57  莫~慌  阅读(103)  评论(0编辑  收藏  举报