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)