网络编程笔记(3)——解决黏包问题
内容目录:
解决黏包问题:
- 提前发送文件的大小
- 使用struct模块
- 定制报文
内容详细:
1.发送之前先发文件大小
优点:
- 确定了到底要接收多大的数据
- 要在文件中配置一个配置项:每次recv文件的大小,buffer = 4096(建议使用这个)
- 当我们要发送大数据时,明确告知接收方要发送多大的数据,以便接收方能准确的接收到所有的数据
- 多用在文件传输的过程中:
- 大文件的传输,一定是按照字节读,每次读固定大小的字节
- 传输的过程中,一边读一边传;接收端:一边收一边写入文件
缺点:
- 多了一次接收交互
注意:
- send 或 sendto超过一定范围的时候都会报错(设备默认的范围大小)
- 写文件传输的时候必须考虑程序的内存管理
# server端:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
while True:
cmd = input('>>>')
if cmd == 'q':
conn.send(b'q')
break
conn.send(cmd.encode('gbk'))
num = conn.recv(1024).decode('utf-8') #接收到了client端发过来的字节总数
conn.send(b'ok')
res = conn.recv(int(num)).decode('gbk') #获取了接收的总字节数,设置进要接收的值里面,就可以动态的感知要发送的数据了
print(res)
conn.close()
sk.close()
#Client端:
import socket
import subprocess
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
cmd = sk.recv(1024).decode('gbk') #因为Windows传输都为GBK编码的
if cmd == 'q':
break
res = subprocess.Popen(cmd,shell=True, #PIPE为管道/队列,只能取一次,取完就结束了
stdout=subprocess.PIPE, #stdout为命令的输出
stderr=subprocess.PIPE) #stderr为命令错误的提示信息
std_out = res.stdout.read() #从队列中取出命令
std_err = res.stderr.read() #从队列中取出错误命令信息
sk.send(str(len(std_out)+len(std_err)).encode('utf-8')) #计算要发送的字节总数并发送过去
sk.recv(4096) #一般不要超过这个数字,否则会超过设备内存
sk.send(std_out)
sk.send(std_err)
sk.close()
2.使用struct模块
-
什么是固定长度的bytes
-
为什么要固定长度bytes?
- 因为发送数据前,先发数据的长度,接收方接收长度。如果固定长度的话就省略了一步收发数据长度的次数
#发送端需要借助struct模块 import struct ret = struct.pack('i',2048) #'i'代表int,就是即将要把一个数字转换成固定长度的bytes类型 print(ret) #接收端: num = struct.unpack('i',ret) #此时num为元组tuple类型 print(num[0]) #获取数据长度2048 #这样接收方只需要接收固定的长度就可以获取整个数据的长度了
# 使用struct模块收发信息 # Server端: import socket import struct sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.listen() conn,addr = sk.accept() while True: cmd = input('>>>') if cmd == 'q': conn.send(b'q') break conn.send(cmd.encode('gbk')) num = conn.recv(4) #接收到了client端发过来的4个字节数(二进制),不管多大始终是4个字节 num = struct.unpack('i',num)[0] #把4个字节数二进制unpack,得到总字节数:num res = conn.recv(int(num)).decode('gbk') #获取了接收的总字节数,设置进要接收的值里面,就可以动态的感知要发送的数据了 print(res) conn.close() sk.close() # Client端: import socket import struct import subprocess sk = socket.socket() sk.connect(('127.0.0.1',8080)) while True: cmd = sk.recv(1024).decode('gbk') #因为Windows传输都为GBK编码的 if cmd == 'q': break res = subprocess.Popen(cmd,shell=True, #PIPE为管道/队列,只能取一次,取完就结束了 stdout=subprocess.PIPE, #stdout为命令的输出 stderr=subprocess.PIPE) #stderr为命令错误的提示信息 std_out = res.stdout.read() #从队列中取出命令 std_err = res.stderr.read() #从队列中取出错误命令信息 len_num = len(std_out) + len(std_err)#计算要发送的字节总数并发送过去 num_by = struct.pack('i',len_num) #使用模块pack进去总字节数,发送过去的始终是4个字节 sk.send(num_by) #发送了4个字节 sk.send(std_out) sk.send(std_err) sk.close()
3.定制报文报头
-
我们在网络上传输的所有数据,都叫数据包
-
数据包里的所有数据,都叫报文
-
报文里不只有发送的数据,还有ip地址,MAC地址,端口号
-
所有的报文都有报头
-
可以自定义报文头
head = {'filename':'test','filesize':409600,'filetype':'txt','filepath':r'\user\bin'} # 先发送报头的长度 #接收4个字节的报头长度 # send(head) #根据这4个字节获取报头 # send(file)发送数据文件 #从报头中获取filesize,然后设置大小接收文件
-
例子:上传文件
# Server端: # 实现一个大文件的上传或下载 # 配置文件,IP地址,端口号 import socket import struct import json sk = socket.socket() sk.bind(('127.0.0.1',8090)) sk.listen() buffer = 1024 #注意,如果设置过大会设备缓存会出错,导致文件接收不完整 conn,addr = sk.accept() #接收 head_len = conn.recv(4) head_len = struct.unpack('i',head_len)[0] json_head = conn.recv(head_len).decode('utf-8') head = json.loads(json_head) filesize = head['filesize'] with open(head['filename'],'wb')as f: while filesize: if filesize >= buffer: content = conn.recv(buffer) f.write(content) filesize -= buffer else: content = conn.recv(filesize) f.write(content) filesize = 0 print('====>',len(content)) print(filesize) conn.close() sk.close() print(head['filesize']) # Client端: # 发送端 import os import json import struct import socket sk = socket.socket() sk.connect(('127.0.0.1',8090)) buffer = 1024 # 发送文件 head = {'filename':'02 python fullstack s9day47-web框架的本质.mp4', 'filepath':r'D:\硬盘文件\老男孩python\Python全栈9期(第02部分):并发编程+数据库+前端\3. 前端开发\day47', 'filesize':None} filepath = os.path.join(head['filepath'],head['filename']) filesize = os.path.getsize(filepath) head['filesize'] = filesize json_head = json.dumps(head) #字典转成了字符串 bytes_head = json_head.encode('utf-8') #字符串转bytes #计算head的长度 head_len = len(bytes_head) #报头的长度 pack_len = struct.pack('i',head_len) sk.send(pack_len) #先发报头的长度 sk.send(bytes_head) #再发送bytes类型的报头 with open(filepath,'rb')as f: while filesize: if filesize >= buffer: content = f.read(buffer) #每次读出来的内容 sk.send(content) filesize -= buffer else: content = f.read(filesize) sk.send(content) filesize = 0 sk.close()