一、 粘包
1. 粘包现象
基于tcp协议的socket,客户端一次接受不完,下一次继续接受(如果间隔时间相对过长,后续的数据会与之前剩余的数据黏在一起),send数据时,连续的发送少量的数据(时间间隔很短),这些数据会积压在一起发送出去.
2. 粘包现象
- 接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
- 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包)
3. 粘包产生的原因
- TCP协议是基于数据流的,无论底层是怎样分段分片的,TCP协议不会根据消息的界限传输数据,而是把构成整条消息的数据段排序完成后呈现到内核缓冲区,等到发送方的缓冲区有足够的数据后才发送一个TCP段
- 当socket的传输的数据大于接收的数据时,多余的数据会被放置在缓冲区,接收方的recv在下一次接收数据时,它会继续接受之前放置在缓冲的数据,而不是发送端最新发送过来的数据
4. 缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
这些I/O缓冲区特性可整理如下:
1.I/O缓冲区在每个TCP套接字中单独存在;
2.I/O缓冲区在创建套接字时自动生成;
3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
4.关闭套接字将丢失输入缓冲区中的数据。
二、粘包的解决方案
1. low版
服务端
import socket
import subprocess
import struct
server_side = socket.socket()
server_side.bind(("127.0.0.1", 8848))
server_side.listen(5)
while 1:
conn, addr = server_side.accept()
while 1:
try:
cmd = conn.recv(1024)
if cmd.decode("utf-8") == 'q':
break
obj = subprocess.Popen(cmd.decode("utf-8"),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
result = obj.stdout.read() + obj.stderr.read()
# 制作报头
total_size = len(result)
# 将不固定长度的int数据类型的报头,转化成固定长度的4bytes
total_size_bytes = struct.pack("i", total_size)
# 发送报头
conn.send(total_size_bytes)
# 发送数据
conn.send(result)
except ConnectionResetError:
break
conn.close()
server_side.close()
客户端
import socket
import struct
client_side = socket.socket()
client_side.connect(("127.0.0.1", 8848))
while 1:
to_send = input("请输入你要执行的命令:")
if to_send.upper() == "Q":
client_side.send("q".encode("utf-8"))
break
client_side.send(to_send.encode("utf-8"))
# 接收报头
head_bytes = client_side.recv(4)
# 反解报头
total_size = struct.unpack("i", head_bytes)[0]
# 循环接收数据
data = b""
while len(data) < total_size:
data += client_side.recv(1024)
print(data.decode("gbk"))
client_side.close()
2. 旗舰版
服务端
import socket
import subprocess
import json
import struct
phone = socket.socket()
phone.bind(("127.0.0.1", 8848))
phone.listen(5)
conn, addr = phone.accept()
while 1:
try:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode("utf-8"),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
result = obj.stdout.read() + obj.stderr.read()
result = result.decode("gbk").encode("utf-8")
# 1.制作报头
head_dict = {"md5": "df",
"file_name": "新建文件夹",
"file_size": len(result)}
# 2. 将报头字典转化成json字符串
head_dict_json = json.dumps(head_dict)
# 3. 将json字符串转化成bytes
head_dict_json_bytes = head_dict_json.encode("utf-8")
# 4. 获取报头的长度
head_len = len(head_dict_json_bytes)
# 5. 将报头长度转化成固定的4个bytes
head_len_bytes = struct.pack("i", head_len)
# 6. 发送固定的4个字节
conn.send(head_len_bytes)
# 7. 发送报头
conn.send(head_dict_json_bytes)
# 8. 发送原数据
conn.send(result)
except ConnectionResetError:
break
conn.close()
phone.close()
客户端
import socket
import struct
import json
phone = socket.socket()
phone.connect(("127.0.0.1", 8848))
while 1:
cmd = input("请输入指令")
phone.send(cmd.encode("utf-8"))
# 1. 接收报头长度
head_len_bytes = phone.recv(4)
# 2. 将报头数字转化成int类型
head_len = struct.unpack("i", head_len_bytes)[0]
# 3. 接收bytes类型的报头字典
head_dict_json_bytes = phone.recv(head_len)
# 4. 将bytes类型的字典转化成json字符串
head_dict_json = head_dict_json_bytes.decode("utf-8")
# 5. 将json字符串转化成字典
head_dict = json.loads(head_dict_json)
# 6. 循环接收原数据
total_data = b''
while len(total_data) < head_dict["file_size"]:
total_data += phone.recv(1024)
print(total_data.decode("utf-8"))
phone.close