今日内容:
1、流式协议=》粘包问题(***)
解决方案:自定义应用层协议
注意:
head+data
head的长度必须固定
2、基于udp协议的套接字通信
先启动客户端是否可以???
服务端是否需要listen???
udp协议应用场景
3、socketserver模块
==========================================================================================
打印socket对象 得到的 不是地址 因为pyhon在str__中进行了 优化,显示的是一些配置信息
accept()请求建立之后,会返回了两个值
这里是函数的返回值 ,而不是解压赋值
conn 和 sever=socket(AF_INET,SOCK_STREAM)
都是相同套接字对象
sever是用来和 服务端进行通信的
但是conn是用来和客户端进行通信的
阻塞: 像这些函数的功能都是接收数据,如果数据没到,就在原地一直等
有数据过来就不阻塞了
input accept recv 都会阻塞(得到运行条件后,就继续恢复运行)
accept接收到数据就会继续运行
所有的I/O操作,都会引起阻塞
阻塞问题----实例剖析:
代码段
while n<10:
print(f'当前是第{n+1}次,取数据------------------------')
data = client.recv(128)
header += data
print(data.decode('utf-8'))
n+=1
print('嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤')
这段代码的执行结果为:
当前是第1次,取数据------------------------
东风夜放花千树。更吹落、星如雨。
宝马雕车香满路。凤箫声动,玉壶光
当前是第2次,取数据------------------------
转,一夜鱼龙舞。
蛾儿雪柳黄金缕。笑语盈盈暗香去。
当前是第3次,取数据------------------------
众里寻他千百度。蓦然回首,那人却在,灯火阑珊处。
当前是第4次,取数据------------------------
没有打印出剩下次数的 当前是第X次取数据 这个测试内容的原因:
因为:当缓存中没有数据的时候,程序就被阻塞了,不会再执行下面的代码
而当前次咨询就没有办法结束
解决 无法跳出阻塞 的方法:
1、测试着玩儿,不能用于实际应用中
指定 取出数据的大小,一次性将所有 缓存接收到的所有数据 取出来
这种方式显然有很大缺陷:
如果要进行传送的文件很大,而缓存的大小是有限制的,所以此种方法只适用于自己测试着玩儿,不能实际应用
2、只能是通过 报头,
首先指定报头的大小,取出反馈数据中的报头,
然后根据报头中指出的,真正数据的大小,
把 current_size < total_size 作为判断条件,
从而实现 [将本次接收过来的所有数据,精准的取出]
流式协议的特点
客户端发送了一个空 可以正常发出去
服务端接收不到空,因为对服务端而言,收到一个 空 和没收到任何数据没有差别
可以在客户端 进行过滤,如果是空 则重新输入
服务端只有收到东西之后 才能 作出响应
{
由于需要减少取数据的次数,从而减少I/O操作引起的延时
所以收到的数据,都按照先来后到的顺序放在缓存中,
但是,取数据时 无法知晓 到底应该取多少长度的数据
}
引入 货车取货送货 辅佐理解 [客户端和服务端的 发数据 和 从缓存取数据]:
须知: 固定班货车:满和不满 都发车
recv(1024):这里的1024就是货车一个车厢的最大装载量
这里的1024,指的是 存放 收到数据 的缓存大小为1024字节
发数据 :相当于 往货车上装货
取数据 :相当于 从货车上取货
如果发送的内容>1024 ,例如为 1025
取出1024个后
还有1个在自己的缓存中
1、装货时:
在装车的时间内,几次发送的内容都装在一辆货车上
连续两次快速send发送内容 达到了 拼接字符串 的运送效果,
并且不会引起一个完整的数据分成两次发送
(而且拼接字符串也存在效率问题)
装不下的部分,放到下一班货车上
2、取货时:
只有有货时,才取出货物
而且只能按照 到货的先后顺序 取货
要喝新水时,老水没喝干净,就继续喝缓存中的老水
流式采用的算法是Nagele,Nagle 的目标是减少网络I/0
前一辆货车上的货,都取完时,才能去除下一辆车上的货
粘包问题: 一次未能取出本次的所有数据,导致 两次操作的数据 粘在 一起:
默认每次取数据的时候,只取出一个车厢的全部数据 即1024个字节,
如果传过来的数据太多,多余的数据会留在后面的车厢里,
待下一次操作时,取出来的首先是上一次操作的数据
引入报头的必要性: (解决粘包问题),一次取出本次的所有数据
原始版 报头: (报头中内容是 序列化的数字)
因此发送方发数据时,必须要告诉接收方,本次发送的数据的总长度
用 从头开始的固定长度字节(这个长度 收发双方必须事先约定好) 告知 接收方 真正的数据的长度,从而实现接收所有发过来的数据
这个 从头开始的固定长度字节,就叫 报头
加强版 报头: (报头中的内容是 序列化的字典)
通过,事先约定 从头开始的固定长度字节 表示报头的长度,
报头中放的是 pickle序列化的字典,在字典中指明,后面真正的数据的长度
len方法:
len(bytes型数据): 得到的是字节数
len(python数据类型): 得到的是字符的个数
bytes类型应用点:
在文件中存储的只能是bytes类型,
python的数据类型如果想要存储在文件中,首先要进行编码,转换成对应编码规则的bytes类型
两个主机之间进行通信的数据格式 也 必须是bytes类型
重要示例:
# 注意这里面total_size表示的数值是一个天文数字,地球上任何一个文件大小都打不到,然而存储这个字典的大小也才400个byte,所以这个字典可以表示任何一个文件
header_dic = {
"total_size": 12322222222222222222222222222222221111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222,
"file_name": 'timg',
"md5": "982345793457894573249579784534899"
}
# 1、
# 通过pickle方式, 得到的header_file文件大小仅为 300 Byte,更小!
with open('header_file', 'wb') as f:
# pickle得到的直接就是Bytes类型,所以直接进行存储就行
f.write(pickle.dumps(header_dic))
f.flush()
# 2、
# 通过json方式, 得到的header文件大小也不过是 500 Byte
with open('header_file','wb') as f:
# 由于json得到的是字符串,所以欲存储,需先进行编码转成Bytes类型
f.write(json.dumps(header_dic).encode('utf-8'))
f.flush()
pickle模块常用的方法有:dumps、loads、dump、load
带s
1、pickle.dumps(obj) — 把 obj 对象序列化后以 bytes 对象返回,不写入文件
2、pickle.loads(bytes_object) — 从 bytes 对象中读取一个反序列化对象,并返回其重组后的对象
不带s, 涉及文件操作
3、pickle.dump( python_obj , f ) — 序列化对象,并将结果数据流写入到文件对象中
4、python_data = pickle.load(f) — 反序列化对象,将文件中的数据解析为一个Python对象
强调: json同理,只不过json操作的对象是 json格式的字符串
json的 dumps 和 loads:
json.dumps()
将python数据类型 ---转换为---> json格式的str字符串
json.loads()
将json格式的str字符串 ---反解成---> python数据类型
pickle的 dumps 和 loads:
dumps(python数据类型的数据)
将python数据类型 ---转换成---> 序列化的bytes类型
loads(bytes类型的数据)
将序列化的bytes类型 ---反解成---> python的数据类型
struct模块的 unpack方法 和 pack方法 # 数据 ①序列化的Bytes类型的整数 / ②python中int类型的整数
1、pack方法: 发数据时用到, 将python的int ---转换为---> 为固定字节数的bytes类型
struct.pack(模式, int型整数)
# 将int类型的整数 ---序列化成--> bytes类型
2、unpack: 收数据时用到, 将固定个数的bytes类型 ---转换为---> python的int
struct.unpack(模式, 序列化的bytes)
# 将序列化的bytes类型的数 ---转换称----> int类型的整数
例1:
struct.pack('i',header_size)
例2:
total_size = struct.unpack('i',header)[0]
header: 其就是一个序列化的bytes类型的整数(这个整数表示的就是 真正的数据的长度)
total_size: 就是真正数据的长度(int类型)
参数'i': 指定 数据的类型 ,从而限定长度的范围
[0] :表示就是报头的长度
得到的数据 直接是bytes类型
引入 加强版报头 收发数据的过程:
1.发
1、先发报头的长度
2、再发报头
报头中内容: 文件长度、文件名字、
3、再发 真正的数据
2.收 (按照发的顺序收)
1、先发报头的长度
2、再发报头
3、再发 真正的数据
传送的数据内容分两种:
1、命令(字符串):
C、S收/发的是命令的长度
2、文件:
get(下载) 文件在服务端上的路径
服务端以读的方式,发送文件
input(上传) 文件在客户端上的路径
客户端以写的方式,写入文件
os.isfile: 判断是否是文件,如果是文件夹 则为False
传输文件时,通过不关闭文件的情况下,可以直接在后面进行 追加(关闭,则指针会到起始位置,进行的操作操作是 覆写)
使用subprocess时, 不要忘记把管道中的内容read一下读出来
obj = subprocess.Popen(cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out = obj.stdout.read()
err = obj.stderr.read()
报头的必须注意点:
不管是使用 ①简单的报头(报头内容: int型整数, 记录真正信息的长度大小) or ②复杂的报头(报头内容: dict型字典, 记录文件名、文件长度大小、md5值等信息)
收发双方(客户端 与 服务端)都必须约定 一个统一的 报头的长度大小,双方都必须要遵守,
因此便有了协议用于规范收发双方, 如果没有协议规范收发双方, 粘包问题就无法根治
收发时注意:
1、在一段内容(字符串)的传输中,发送方报头中记录的长度 应该是 这段内容序列化后的 byte的个数
收取方收数据时, 判断 是否收完要接收的所有数据 的条件也应该是: 当前接收的byte的个数 < 真正内容的byte的个数
2、在文件传输中,同理
======================================> 代码 <===========================================
————————————————————————上节课代码————————————————————————
客户端:
from socket import *
client = socket(AF_INET, SOCK_STREAM)
# print(client)
client.connect(('127.0.0.1', 8080))
while True:
cmd = input(">>: ").strip()
if len(cmd) == 0:
continue
client.send(cmd.encode('utf-8'))
print('=======>')
data = client.recv(1024)
print('111111')
print(data.decode('utf-8'))
client.close()
服务端:
from socket import *
# AF_INET: 指定本套接字是基于网络的套接字,
# SOCK_STREAM:
server = socket(AF_INET, SOCK_STREAM)
# print(server),打印sever可以看到这个本应该是地址,不过此处是python优化后的结果
# 127.0.0.1 本地换回地址:只有本机自己能访问;隔离了外部的网络环境,多用于测试
server.bind(('127.0.0.1', 8080))
server.listen(5)
# 从半连接池中取出请求,建立连接(服务端 到 客户端 的桥)
while True:
conn, client_addr = server.accept()
print(conn)
print(client_addr)
while True:
try:
data = conn.recv(1024)
conn.send(data.upper())
except Exception:
break
conn.close()
server.close()
———————————————————————— 简单的报头(报头内容: 只是单纯的长度大小) --解决远程执行命令程序中的 粘包问题————————————————————————
客户端:
import struct
from socket import *
client = socket(AF_INET, SOCK_STREAM)
# print(client)
client.connect(('127.0.0.1', 8888))
while True:
cmd = input('请输入指令:').strip()
if len(cmd) == 0:
continue
client.send(pickle.dumps(cmd))
'''报头的长度为4,其记录真正数据的长度
所以,要先取出报头'''
'''最严谨时,还要考虑下列情况:
在网络不顺畅的 情况下
表示 报头的部分 可能会在两次的货车中
此时 要先把报头的长度的字节 收集完
得到长度后,再收报头指定长度的实际数据
网络通畅时,则不会发生报头在两个货车的情况'''
# 先收数据的长度
严谨的 获取简单报头的写法
n = 0
header = b''
while n < 4:
data = client.recv(1)
header += data
n += len(data)
'''
# 一般的(不严谨) 获取简单报头的写法
header=client.recv(4)
total_size = struct.unpack('i', header)[0]
# 收真正的数据
recv_size = 0
recv_data = b''
while recv_size < total_size:
part_data = client.recv(1024)
recv_data += data
recv_size += len(part_data)
print(res.decode('gbk'))
client.close()
服务端:
import subprocess
import struct
from socket import *
server = socket(AF_INET, SOCK_STREAM)
# print(server)
server.bind(('127.0.0.1', 8888))
server.listen(5)
while True:
conn, client_addr = server.accept()
print(conn)
print('当前客户端地址:',client_addr)
while True:
try:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# 要read()一下,把管道中的内容读出来
# 这个是 正确结果的输出内容
stdout = obj.stdout.read()
# 这个是 错误结果的输出内容
stderr = obj.stdout.read()
total_size = len(stdout) + len(stderr)
# 先发送数据的长度
conn.send(struct.pack('i',total_size))
# 再发送真正的数据
conn.send(stdout)
conn.send(stderr)
except Exception:
break
conn.close()
server.close()
————————————————————————制作简单的报头————————————————————————
import struct
from socket import *
client = socket(AF_INET, SOCK_STREAM)
# print(client)
client.connect(('127.0.0.1', 8082))
while True:
cmd = input(">>: ").strip() # get 文件路径
if len(cmd) == 0:
continue
client.send(cmd.encode('utf-8'))
# 先收数据的长度
n = 0
header = b''
while n < 8:
data = client.recv(1)
header += data
n += len(data)
total_size = struct.unpack('q', header)[0]
print(total_size)
# 再收真正的数据
recv_size = 0
with open('aaa.jpg', mode='wb') as f:
while recv_size < total_size:
part_data = client.recv(1024)
f.write(data)
recv_size += len(part_data)
client.close()
服务端:
import subprocess
import os
import struct
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8082))
server.listen(5)
while True:
conn, client_addr = server.accept()
print(conn)
print(client_addr)
while True:
try:
msg = conn.recv(1024).decode('utf-8')
cmd,file_path=msg.split()
if cmd == "get":
# 先发送报头
total_size=os.path.getsize(file_path)
conn.send(struct.pack('q',total_size))
# 再发送文件
with open(f'{file_path}}', mode='rb') as f:
for line in f:
conn.send(line)
except Exception:
break
conn.close()
server.close()
————————————————————————定制复杂的报头(通过字典) --实现文件的上传下载 ————————————————————————
单独拎出来的 报头:
header_dic = {
'file_name': "嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤嘤",
'md5': "12322222222222222222222222222222221111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222",
'total_size': 12322222222222222222222222222222221111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
}
pickle_size = len( pickle.dumps(header_dic) )
json_size = len( json.dumps(header_dic) )
print('pickle_size:', pickle_size) # 800 Byte
print('json_size:', json_size) # 1200 Byte
# 可以看到, 平均1kb便可存储 很多的文件信息
客户端:
import struct
import json
from socket import *
client = socket(AF_INET, SOCK_STREAM)
# print(client)
client.connect(('127.0.0.1', 8082))
while True:
cmd = input(">>: ").strip() # get 文件路径
if len(cmd) == 0:
continue
client.send(cmd.encode('utf-8'))
# 1、先接收报头的长度
res=client.recv(4)
header_size=struct.unpack('i',res)[0]
# 2、再接收报头
header_json_bytes=client.recv(header_size)
header_json=header_json_bytes.decode('utf-8')
header_dic=json.loads(header_json)
print(header_dic)
# 3、最后接收真实的数据
total_size=header_dic['total_size']
filename=header_dic['file_name']
recv_size = 0
with open(r"D:\python全栈15期\day32\代码\03 定制复杂的报头\版本2\download\%s" %filename, mode='wb') as f:
while recv_size < total_size:
part_data = client.recv(1024)
f.write(part_data)
recv_size += len(data)
client.close()
服务端:
import subprocess
import os
import struct
import json
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8888))
server.listen(5)
while True:
conn, client_addr = server.accept()
print(client_addr)
while True:
try:
msg = conn.recv(1024).decode('utf-8')
# 客户端发送的信息 格式为: get/input 文件路径
cmd, file_path = msg.split() # file_path 为服务端上的文件路径
if cmd == "get":
# 一、制作报头
# 1、编辑 报头字典中的内容
header_dic = {
"total_size": os.path.getsize(file_path), # 12322222222222222222222222222222221111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
"file_name": 'timg',
"md5": "12312222222222222222222222222222222222222222222222222222231231232132131232311"
}
# 2、将报头序列化得到 序列化的报头
# 转换成 json格式的字符串
header_json = json.dumps(header_dic)
# 将json格式的字符串 编码成对应编码 的bytes类型
header_json_bytes = header_json.encode('utf-8')
# 二、发送数据
# 由于当前的 报头 是变长的,长度不固定
# 1、所以先发送 序列化的 报头的长度
# 上面的这个字典的大小也才 400+ 个字节,所以'i'模式 足够容纳下它了
header_size = len(header_json_bytes) # 447
conn.send(struct.pack('i', header_size))
# 2、再发送 序列化的报头
conn.send(header_json_bytes)
# 3、最后发送文件中的 真实的数据
with open(r'%s' % file_path, mode='rb') as f:
for line in f:
conn.send(line)
except Exception:
break
conn.close()
server.close()
8-14-粘包问题、(通过简单报头)解决粘包问题、定制复杂报头
最新推荐文章于 2022-12-02 14:10:05 发布