C / S —— 黏包现象(及解决方案)、struct模块

C / S —— 黏包现象(及解决方案)、struct模块

一、黏包现象

1、黏包现象的产生

'''
粘包现象产生的原因
	
	黏包现象产生的原因
	1.不知道每次的数据到底多大
	2.TCP也称为流式协议:数据像水流一样绵绵不绝没有间隔(TCP会针对数据量较小且发送间隔较短的多条数据一次性合并打包发送)
	TCP中有一个Negal算法,用途是这样的:通信两端有很多小的数据包要发送,虽然传送的数据很少,但是流程一点没少,也需要TCP的各种确认,校验。这样小的数据包如果很多,会造成网络资源很大的浪费,Negal算法做了这样一件事,当来了一个很小的数据包,我不急于发送这个包,而是等来了更多的包,将这些小包组合成大包之后一并发送,不就提高了网络传输的效率的嘛。这个想法收到了很好的效果,但是我们想一下,如果是分属于两个不同页面的包,被合并在了一起,这就是粘包问题。粘包问题只存在于TCP中,不存在于UDP
 
避免黏包现象的核心思路\关键点
	如何明确即将接收的数据具体有多大
	
ps:如何将长度变化的数据全部制作成固定长度的数据
'''

2、TCP协议和UDP协议传输消息时之间的区别

'''
	TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
	UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。
'''

3、黏包问题的解决方案

3.1struct模块

# coding:utf-8
# 导入struct模块
import struct
info = b'hi beautiful gril!'
print(len(info))

# 将数据打包成固定的长度,i时固定的打包模式
res = struct.pack('i', len(info))

# 输出的长度为4 时固定的 单位时bytes 报头
print(len(res))  # 4

# 根据固定的报头,解析出真实的数据长度
real_len = struct.unpack('i', res)
print(real_len)  # (18,) 输出的结果是一个元组

3.2解决粘包问题的简单思路

"""
解决黏包问题初次版本
    客户端
        1.将真实数据转成bytes类型并计算长度
        2.利用struct模块将真实长度制作一个固定长度的报头
        3.将固定长度的报头先发送给服务端 服务端只需要在recv括号内填写固定长度的报头数字即可
        4.然后再发送真实数据
    
    服务端
        1.服务端先接收固定长度的报头
        2.利用struct模块反向解析出真实数据长度
        3.recv接收真实数据长度即可
"""

3.3简单方案



3.4简单方案的问题

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


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

3.5解决粘包问题的完整思路

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

3.6完整方案

3.6.1服务端
'''终极解决方案:字典作为报头打包 效果更好 数字更小'''
# coding:utf-8
import socket
import struct
import json

# 建立socket对象
server = socket.socket()

# 绑定一个固定的地址(服务器必备的条件)
server.bind(('127.0.0.1', 8088))

# 半连接池,设置最大的连接数
server.listen(8)

# 等待接收客户端的数据和地址
sock, addr = server.accept()

# 接收固定长度的字典报头
data_dict_head = sock.recv(4)

# 根据报头解析出字典数据的长度
data_dict_len = struct.unpack('i', data_dict_head)[0]

# 接收字典数据
data_dict_bytes = sock.recv(data_dict_len)

# 自动解码在进行反序列化
data_dict = json.loads(data_dict_bytes)

# 获取真实数据的各项信息
total_size = data_dict.get('file_size')

recv_size = 0

with open(data_dict.get('file_name'), 'wb') as f:
    # f.write(sock.recv(total_size))
    '''
    接收真实数据的时候,如果数据量非常大,
    recv括号内直接填写该数据,不合适,
    我们可以每次接收一点点,反正知道总长度
    '''
    while recv_size < total_size:
        data = sock.recv(1024)
        f.write(data)
        recv_size += len(data)
        print(recv_size)

3.6.2 client端
# coding:utf-8
import socket
import os
import struct
import json

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

# 任何文件都是下列的思路包括但不仅限于(图片、视频、文本...)
# 获取文件的真实数据大小
file_size = os.path.getsize(r'D:\pycharm\Blog\网络编程\a.txt')

# 制作真实数据的数据字典
data_dict = {
    'file_name': 'knowledge.mp4',
    'file_size': file_size,
    'file_desc': 'it\'s a mp4',
    'file_info': 'give you a big baby'
}

# 制作字典报头
data_dict_bytes = json.dumps(data_dict).encode('utf')
data_dict_len = struct.pack('i', len(data_dict_bytes))

# 发送字典报头
# 报头的本身也是bytes类型,我们在看的时候长度永远为4
client.send(data_dict_len)
client.send(data_dict_bytes)

# 最后发送真实数据
with open(r'D:\pycharm\Blog\网络编程\a.txt', 'rb') as f:
    for line in f:
        client.send(line)
    import time
    time.sleep(10)

posted @ 2022-11-17 20:05  负剑远游行归来仍少年  阅读(122)  评论(0)    收藏  举报