一、什么是粘包:
粘包指的是数据与数据之间没有明确的分界线,导致程序不能正确读取数据。
TCP或UDP协议下,程序要将收发的数据交由操作系统处理,操作系统会设立缓冲区, 用于收发各个程序的数据
UDP(用户数据报协议): 是无连接的、面向消息的,面向消息的通信是有信息保护边界的。 基于数据包收发数据,数据包之间相互独立。 存在的问题: 数据包过大时,受制于发送方系统限制,数据包可能无法发送。 受制于接收方的系统缓存大小,当数据包超过限制时,数据包会丢失
TCP(传输控制协议): 可以传输较大数据并保证完整性, 是面向流的、面向连接的,但面向流的通信是无信息保护边界的。
两者区别: TCP是基于数据流的,收发的消息不能为空,客户端和服务器端都要添加空消息的处理机制,防止程序卡死 UDP是基于数据报的,即使输入空内容,也不是空消息,UDP协议会封装消息头。
二、粘包为什么会发生:
粘包只会发生在TCP下: 1、当单个数据包较小时,接收方可能一次读取多个包的数据 2、当整体数据较大时接收方可能一次仅读取一个包的一部分内容 3、另外TCP协议为了提高效率,增加了一种优化机制,会将数据较小且发送间隔较短的数据合并发送, 该机制也会导致发送方将两个数据包粘在一起发送
粘包问题产生的根本原因是接收方无法预知接受到的数据长度
三、如何解决粘包
自定义报头。
具体思路:
发送端: 1.先将所有的额外信息打包到一个头中 2.然后先发送头部数据 3.最后发送真实数据
接收端: 1.接收固定长度的头部长度数据 2.根据长度数据获取头部数据 3.根据头部数据获取真实数据
代码如下:
# 服务器端
import socket
import subprocess
import struct
import json
serve = socket.socket()
serve.bind(("127.2.1.1",9898))
serve.listen()
while True:
client,addr = serve.accept()
while True:
try:
cmd = client.recv(1024).decode("utf-8")
if not cmd:continue # 接受命令不能为空
p = subprocess.Popen(cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=-1
) # subprocess调取子进程
data = p.stdout.read()
err_data = p.stderr.read()
length = len(data)+len(err_data) # 得到数据长度
head = {"size":length,"xxx":"yyy"} # 自定义报头
head_data = json.dumps(head).encode("utf_8") # 序列化报头
len_head_data = struct.pack("i",len(head_data)) # 以元组的形式打包,struct是bytes类型
client.send(len_head_data) # 先发送报头长度,此处信息长度固定,i模式为4
client.send(head_data) #发送报头
client.send(data) #发送数据,正常输出和错误输出一般只会发出一个
client.send(err_data)
except ConnectionResetError:
client.close()
break
serve.close()
# 客户端
import socket
import json
import struct
client = socket.socket()
client.connect(("127.2.1.1",9898))
while True:
msg = input(">>>:").strip()
if not msg:continue # 命令不能为空
client.send(msg.encode("utf-8")) # 发送命令,编码为utf-8
data = client.recv(4) # 先接收4个bytes长度的数据,得到报头的长度
head_length = struct.unpack("i",data)[0] #收到的是一个元组,报头的长度
head_data = client.recv(head_length).decode("utf-8") #再次接收长度为报头的数据,获取报头
head = json.loads(head_data) # 反序列化报头
data_length = head["size"] # 获取字典中报头的长度
size = 0
res = b"" #预设空的bytes变量
while size < data_length:
real_data = client.recv(1024)
size += len(real_data)
res += real_data # 变量完整相加,即得到所需数据
print(res.decode("gbk")) # win平台下,解码为gbk
client.close()
四、简单的视频上传和下载
1、应该使用TCP协议,因为必须保证文件的完整性
2、上传与下载都是bytes类型,而视频也正好是bytes类型
3、逻辑思路
自定义报头 ---》发送文件名----》文件大小-----》MD5值
读取文件数据,发送给对方
服务器端代码:
import socket
import struct
import json
server = socket.socket()
server.bind(("127.0.0.1",6767))
server.listen()
client,addr = server.accept()
f = open("用户上传文件","wb") # 创建一个新的文件,注意此时会放到程序运行的当前文件夹下,且没有后缀
head_len = client.recv(4) # 接收固定长度的报头长度数据
json_len = struct.unpack("i",head_len)[0] # 解码,获得元组eg:(43,),43即为报头的长度
json_str = client.recv(json_len).decode("utf-8") # 获取报头信息并解码
head = json.loads(json_str) # 反序列化,得到真正的报头信息
recv_size = 0
while recv_size < head["size"]: # 获取文件大小
data = client.recv(1024)
f.write(data) # 没有关闭状态下,会继续写入而不是覆盖
recv_size += len(data)
print("接收完成")
f.close() # 关闭文件
serve.close() # 关闭程序
import json
import struct
import socket
import os
client = socket.socket()
client.connect(("127.0.0.1",6767))
filepath = r"C:UserswangtDocumentsday27 6 操作系统与进程发展史.mp4" # 获取文件路径
f = open(filepath,"rb") # 只读模式打开文件
head = {"size":os.path.getsize(filepath),"filename":"操作系统与进程发展史.mp4"} # 关注报头获取文件大小的方式
json_data = json.dumps(head).encode("utf-8") # 报头序列化,用utf-8编码
json_len = struct.pack("i",len(json_data)) # 报头长度打包
client.send(json_len) # 发送报头长度
client.send(json_data) # 发送报头数据
while True:
data = f.read(1024) # 循环读取文件内容,每次读取1024bytes
if not data:
break
client.send(data)
print("上传完成")
f.close()
client.close()