Python基础之网络编程:4、黏包及解决办法
Python基础之网络并发编程
一、黏包现象
1、什么是黏包
黏包是指,当我们基于TCP协议,客户端可服务端进行数据传输时,会自动将多个小部份的数据打包成一个大的数据进行发送,例如,在客户端给服务端发送数据时,我们分开发送了 ABC , 123 这两段信息,按照常理来说,客户端收到的消息也应该两段分开的消息,而服务端实际收到的消息是 ABC123,可是这并不是我们想要的结果,因为在实际中,我们收到这种消息后很难将多条连接在一起的消息区分开来
代码表现
1、客户端
# 导入socket
import socket
# 产生socket对象
server = socket.socket()
# 连接主机地址
server.connect(('192.168.1.188', 8899))
# 向主机发送信息
server.send(b'hello tom')
server.send(b'hello jason')
server.send(b'hello jerry')
2、服务端
# 产生socket对象
server = socket.socket()
# 设置主机IP地址
server.bind(('192.168.1.188', 8899))
# 设置半连接池
server.listen(5)
# 获取客户端接入
sock, addre = server.accept()
sock = sock.recv(1024).decode('utf8')
# 打印客户端信息
print(sock)
---------------------------------------------------------------
hello tomhello jasonhello jerry
2、产生黏包的原因
- 1、在使用socket的模块,接收消息时 需要设置 recv 也就是接收的字节数,可是我们并不知道对方发送来的消息具体大小是多少,所以就会默认接收recv设置的字节以内的多段的消息
- 2、TCP也称为流式协议:数据像水流一样绵绵不绝没有间隔(TCP会针对数据量较小且发送间隔较短的多条数据一次性合并打包发送)
3、如何避免黏包
核心思路:
1、明确即将接收的消息具体有多大
2、针对消息的大小设置recv的字节数
解决办法:
1、借助struct模块
二、struct模块
1、模块简介
可以将一组数据打包为固定的大小,也可将打包的数据进行解包
2、常用方法
1、struct
功能:导入模块
1、pack('模式', '待打包的数据')
功能:将一组数据按照指定模式进行打包,打包后的数据为Bytes类型
2、unpack(bytes_data)
功能:解析打包后的数据
3、常见格式
格式 | python类型 | 标准尺寸 |
---|---|---|
c | 1个字节长度 | 1 |
ns | n个字符 | n |
i | 整数 | 4 |
f | 浮点 | 4 |
三、黏包问题实战
1、解决思路
上述,我们了解了产生黏包的原因,并且知道了大致解决思路,下面我们将针对黏包问题指定详细解决办法
第一步:
分别在客户端和服务端导入struct模块,通过了解struct模块,我们知道在pack方法的‘i’模式下,会固定加打包的数据大小压缩为4个字节
第二步:
产生一个‘字典’类型,在字典内存入我们需要发送的目标文件详细信息,并将目标文件的大小存入键值对,将信息设置为键值对格式
第三步:
使用‘len’方法,获得字典编码后的字节个数,将字典的个数用'struct'模块的下'pack'方法进行打包,接收端将'recv'设置为4个字节,并接收打包后的‘字典字节个数’,将‘recv’设置为字典的大小,接收字典后,获取字典内目标文件大小,再将‘recv’设置为目标文件大小,进行接收目标文件
第四步:
将打包的'字典'使用'unpack'方法解析,并读取字典信息,获取到需要接收的数据详细大小,在将'recv'设置为需要接收的文件大小,进行接收数据
2、代码实战
模拟客户端向服务端发送信息,将存有数据的'txt'格式文本发送给服务端
1、服务端
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)
2、客户端
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)
四、UDP协议
1、UDP协议特点
UDP协议是面向无连接的通讯协议,UDP是不可靠协议,发送方所发送的数据对方不一定能够接收到
2、代码实现
服务端:
import socket
# UDP客户端、创建一个服务器端的Socket
socket_server = socket(AF_INET, SOCK_DGRAM)
# 2、定义服务器端的ip地址和端口号,元组形式
host_port = ('192.168.108.43', 8090)
# 3、服务器端的Socket来绑定地址和端口,只有绑定了地址和端口,才能称为服务器的Socket
socket_server.bind(host_port)
# 4、接收客户端发送过来的数据,每次接收1kb的数据,如果一直没收到数据会阻塞
# 收到的每一个数据报,里面是一个元组,第一个值是数据内容,第二个值是源地址
data = socket_server.recvfrom(1024)
# 解码
print(data[0].decode('utf-8'))
print(data)
# 5、关闭套接字、释放资源
socket_server.close()
服务器端的结构:
(1) 使用函数socket(),生成套接字描述符;
(2) 通过host_post 结构设置服务器地址和监听端口;
(3)使用bind()函数绑定监听端口,将套接字文件描述符和地址类型变(host_post) 进行绑定;
(4)接收客户端的数据,使用recvfrom()函数接收客户端的网络数据;
(5)关闭套接字,使用close()函数释放资源;
客户端:
impo
# 客户端发送一个请求也需要端口,端口是随机分配的
# 创建一个UDP协议的套接字,然后发送一条数据到网络上的另外一个进程
# UDP客户端、创建套接字
client_socket = socket(AF_INET, SOCK_DGRAM) # SOCK_DGRAM:UDP协议
# 2、定义一个接收消息的目标,8080是一个目标服务器的端口,127.0.0.1是目标服务器地址
server_host_port = ('192.168.108.43', 8080)
# 3、准备即将发送的数据,encode表示按照一种编码格式把数据变成字节数组bytes
# 数据一定是字节数据才能发送
datas = input('请输入:').encode('utf-8')
# 4、发送数据,标识一个进程是通过ip+端口+协议
client_socket.sendto(datas, server_host_port)
print('发送完成')
# 5、关闭套接字,其实就是释放了系统资源
client_socket.close()