文章目录
一、socket粘包问题
-
什么是粘包:粘包指的是数据和数据之间没有明确的分界线,导致不能正确读取数据
应用程序无法直接操作硬件,应用程序想要发送数据则必须将数据交给操作系统,而操作系统需要需要同时为所有应用程序提供数据传输服务,也就意味着,操作系统不可能立马就能将应用程序的数据发送出去,就需要为应用程序提供一个缓冲区,用于临时存放数据,具体流程如下:
反送方:
当应用程序调用send函数时,应用程序会将数据从应用程序拷贝到操作系统缓存,再由操作系统从缓冲区读取数据并发送出去
接收方:
对方计算机收到数据也是操作系统先收到,至于应用程序何时处理这些数据,操作系统并不清楚,所以同样需要将数据先存储到操作系统的缓冲区中,当应用程序调用recv时,实际上是从操作系统缓冲区中将数据拷贝到应用程序的过程
上述过程对于TCP与UDP都是相同的不同之处在于:
UDP:
UDP在收发数据时是基于数据包的,即一个包一个包的发送,包与包之间有着明确的分界,到达对方操作系统缓冲区后也是一个一个独立的数据包,接收方从操作系统缓冲区中将数据包拷贝到应用程序
这种方式存在的问题:
- 发送方发送的数据长度每个操作系统会有不同的限制,数据超过限制则无法发送
- 接收方接收数据时如果应用程序的提供的缓存容量小于数据包的长度将造成数据丢失,而缓冲区大小不可能无限大
TCP:
当我们需要传输较大的数据,或需要保证数据完整性时,最简单的方式就是使用TCP协议了,与UDP不同的是,TCP增加了一套校验规则来保证数据的完整性,会将超过TCP包最大长度的数据拆分为多个TCP包,并在传输数据时为每一个TCP数据包指定一个顺序号,接收方在收到TCP数据包后按照顺序将数据包进行重组,重组后的数据全都是二进制数据,且每次收到的二进制数据之间没有明显的分界
基于这种工作机制TCP在三种情况下会发送粘包问题
- 当单个数据包较小时接收方可能一次性读取了多个包的数据
- 当整体数据较大时接收方可能一次仅读取了一个包的一部分内容
- 另外TCP协议为了提高效率,增加了一种优化机制,会将数据较小且发送间隔较短的数据合并发送,该机制也会导致发送方将两个数据包粘在一起发送
基础解决方案:
首先明确只有TCP会出现粘包问题,之所以粘包是因为接收方不知道一次该接收的数据长度,那如何才能让接收方知道数据的长度呢?
解决方案:在发送数据前先发送数据长度
cmd 服务端:
import socket
import subprocess
import struct
server = socket.socket()
server.bind(("127.0.0.1",9090))
server.listen()
while True:
client,addr = server.accept()
while True:
try:
#接收客户端命令
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
# data与err_data都是采用的系统编码,windows是GBK
data = p.stdout.read()
err_data = p.stderr.read()
print("数据长度:%s" % (len(data) + len(err_data)))
#计算数据长度
length = len(data) + len(err_data)
#将int类型的长度转成字节
len_data = struct.pack("i",length)
# 先发送长度,在发真实数据有可能长度数据和真实数据黏在一起,而接收方不知道长度数据的字节数 导致黏包
# 解决的方案就是 长度信息占的字节数固定死 整数 转成一个固定长度字节
# 先发送长度给客户端
client.send(len_data)
# 再发送数据给客户端
client.send(data)
client.send(err_data)
except ConnectionResetError:
client.close()
print("连接中断......")
break
cmd 客户端:
import socket
import struct
c = socket.socket()
c.connect(("127.0.0.1",9090))
while True:
cmd = input(">>:").strip()
c.send(cmd.encode("utf-8"))
# 先接收长度,长度固定为4个字节
length = c.recv(4)
# 转换为整型
len_data = struct.unpack("i",length)[0]
print("数据长度为%s" % len_data)
# 存储已接收数据
all_data = b""
# 已接收长度
rcv_size = 0
# 循环接收直到接收到的长度等于总长度
while rcv_size < len_data:
data = c.recv(1024)
rcv_size += len(data)
all_data += data
print("接收长度%s" % rcv_size)
print(all_data.decode("gbk"))
上述方案已经完美解决了粘包问题,但是扩展性不高,例如我们要实现文件上传下载,不光要传输文件数据,还需要传输文件名字,md5值等等,如何能实现呢?
解决方案:
发送端:
- 先将所有的额外信息打包到一个头中
- 然后先发送头部数据
- 最后发送真实数据
接收端:
- 接收固定长度的头部长度数据
- 根据长度数据获取头部数据
- 根据头部数据获取真实数据
cmd 服务端:
# 要求:不仅返回命令的结果 还要返回执行命令的时间 执行时间:2018/12/26
import socket
import subprocess
import struct
import datetime
import json
server = socket.socket()
server.bind(("127.0.0.1",9090))
server.listen()
while True:
client,addr = server.accept()
while True:
try:
# 接收命令
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
# data与err_data都是采用的系统编码,windows是GBK
data = p.stdout.read()
err_data = p.stderr.read()
print("数据长度:%s" % (len(data) + len(err_data)))
# 计算真实数据长度
length = len(data) + len(err_data)
# 在发送数据之前发送额外的信息
#t = "{执行时间:%s 真实数据长度:%s" % (datetime.datetime.now(),length)
# 把要发送的数据先存到字典中
t = {}
t["time"] = str(datetime.datetime.now())
t["size"] = length
t["filename"] = "a.mp4"
t_json = json.dumps(t) # 得到json格式字符串
t_data = t_json.encode("utf-8") # 将json转成了字节
t_length = struct.pack("i",len(t_data))
# 1.先发送额外信息的长度
client.send(t_length)
# 2.发送额外信息
client.send(t_data)
# 3.发送真实数据
client.send(data)
client.send(err_data)
except ConnectionResetError:
client.close()
print("连接中断......")
break
# 1.发送了真实数据长度 2.发送了额外信息长度 3.发送额外信息 4.发送真实数据
cmd 客户端:
import socket
import struct
import socket
import json
c = socket.socket()
c.connect(("127.0.0.1",9090))
while True:
cmd = input(">>>:")
if not cmd:
print("命令不能为空")
continue
c.send(cmd.encode("utf-8"))
# 1.接收的是额外信息的长度
length = c.recv(4)
len_data = struct.unpack("i",length)[0] # 转换为整型
# 2.接收额外信息
t_data = c.recv(len_data)
print(t_data.decode("utf-8"))
json_dic = json.loads(t_data.decode("utf-8"))
print("执行时间:%s" % json_dic["time"])
data_size = json_dic["size"] # 得到数据长度
all_data = b"" # 存储已接收数据
rcv_size = 0 # 已接收长度
# 接收真实数据
# 循环接收 直到 接收到的长度等于总长度
while rcv_size < data_size:
data = c.recv(1024)
rcv_size += len(data)
all_data += data
print("接收长度%s" % rcv_size)
print(all_data.decode("gbk"))
二、文件上传下载
服务端:
import socket
import struct
import json
server = socket.socket()
server.bind(("127.0.0.1",9090))
server.listen()
client,addr = server.accept()
f = open("接收到的文件",mode="wb")
head_len = client.recv(4)
json_len = struct.unpack("i",head_len)[0]
json_str = client.recv(json_len).decode("utf-8")
head = json.loads(json_str)
print(head)
recv_size = 0
while recv_size < head["size"]:
data = client.recv(1024)
f.write(data)
recv_size += len(data)
print("接收完成...")
客户端:
import socket
import os
import json
import struct
c = socket.socket()
c.connect(("127.0.0.1",9090))
filepath= r"F:\测试.mp4"
f = open(filepath,mode="rb")
# 在发送数据前先发送报头
head = {"size":os.path.getsize(filepath),"filename":"回顾.mp4"}
json_data = json.dumps(head).encode("utf-8")
json_len = struct.pack("i",len(json_data))
c.send(json_len) # 发长度
c.send(json_data) # 发报头
# 发数据
while True:
data = f.read(1024)
if not data:
break
# 发送给服务器
c.send(data)
print("上传完成...")