TCP协议粘包问题的解决
不知名菜鸟
爱分享,爱生活,爱工作。
Official Account
基于TCP协议的数据传输在运行的时候会发生粘包问题
服务端
import socket
import subprocess
HOST = '0.0.0.0' # 服务端绑定0.0.0.0的IP 放行所有的客户端请求
PORT = 8080 # 0-65535 1024前被系统保留使用
BUFSIZE = 1024
ADDR = (HOST, PORT)
# STREAM 流式协议 TCP协议 DGRAM 数据报协议 UDP协议
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(ADDR) # 将服务端的IP和地址绑定到套接字
server.listen(5) # 监听链接 定义半链接池大小
print('服务端启动,监听地址:%s :%s' % (ADDR))
while True: # 链接循环 不停的接受请求 一直提供服务
# 等待客户端请求
conn, addr = server.accept() # 接受客户端链接
while True: # 通信循环 不停的收发消息
# send 和recv都会发起系统调用
try:
data = conn.recv(BUFSIZE)
if not data:
break
res = subprocess.Popen(data.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout_data = res.stdout.read()
stderr_data = res.stderr.read()
print(len(stderr_data) + len(stdout_data), addr)
conn.send(stdout_data)
conn.send(stderr_data)
except Exception:
# 针对Windows系统
break
conn.close() # (必选操作)关闭客户端套字节 回收资源的操作
客户端
import socket
HOST = '39.108.102.14' # 绑定服务器公网IP
PORT = 8080 # 0-65535 1024前被系统保留使用
BUFSIZE = 8096
ADDR = (HOST, PORT)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ADDR)
while True: # 开启通信循环 不停的收发消息
cmd = input('cmd>>>').strip()
if not cmd: # 不让客户端发送空消息
continue
client.send(cmd.encode('utf-8'))
res_data = client.recv(BUFSIZE)
if not res_data:
break
print(res_data.decode('gbk')) # Windows采用gbk Unix采用utf-8
client.close() # 关闭客户端 必要的选择
基于UDP远程执行命令
在运行时永远不会发生粘包
服务端
from socket import *
import subprocess
HOST = '127.0.0.1'
PORT = 8080
BUFSIZE = 1024
ADDR = (HOST, PORT)
server = socket(AF_INET, SOCK_DGRAM)
server.bind(ADDR)
while True:
cmd, addr = server.recvfrom(BUFSIZE)
if not cmd:
break
print('命令为>>>', cmd, 'from', addr)
res = subprocess.Popen(cmd.decode('utf-8'),
shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout_msg = res.stdout.read()
stderr_msg = res.stderr.read()
server.sendto(stdout_msg, addr)
server.sendto(stderr_msg, addr)
server.close()
客户端
from socket import *
HOST = '127.0.0.1'
PORT = 8080
BUFSIZE = 1024
ADDR = (HOST, PORT)
client = socket(AF_INET, SOCK_DGRAM)
while True:
cmd = input('>>>').strip()
if not cmd:
continue
client.sendto(cmd.encode('utf-8'), ADDR)
data, addr = client.recvfrom(BUFSIZE)
if not data:
break
print(data.decode('gbk')) # Windows
client.close()
粘包问题的解决
由于接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
发生粘包的情况
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,会合到一起,产生粘包)
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
解决粘包问题思路
先收固定长度的头信息并解析出数据的总大小:total_size
recv_size=0,循环接收,每接收一次,recv_size+=接收的长度
直到recv_size=total_size,结束循环
struct模块
可以把一个类型,如int类型,转成固定长度的bytes
>>>import struct
>>>x = struct.pack('i', 6666)
>>>x
>>>b'\n\x1a\x00\x00'
>>>len(x)
>>>4 # 长度为4字节
>>>struct.unpack('i', x)[0]
>>>6666
定义报头
先发送返回内容的长度信息,再发送真实的返回信息给客户端
服务端
import socket
import subprocess
import struct
HOST = '127.0.0.1'
PORT = 8080 # 0-65535 1024前被系统保留使用
BUFSIZE = 1024
ADDR = (HOST, PORT)
# STREAM 流式协议 TCP协议 DGRAM 数据报协议 UDP协议
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(ADDR) # 将服务端的IP和地址绑定到套接字
server.listen(5) # 监听链接 定义半链接池大小
print('服务端启动,监听地址:%s :%s' % (ADDR))
while True: # 链接循环 不停的接受请求 一直提供服务
# 等待客户端请求
conn, addr = server.accept() # 接受客户端链接
while True: # 通信循环 不停的收发消息
# send 和recv都会发起系统调用
try:
data = conn.recv(BUFSIZE)
if not data:
break
res = subprocess.Popen(data.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout_data = res.stdout.read()
stderr_data = res.stderr.read()
print(len(stderr_data) + len(stdout_data), addr)
total_size = len(stdout_data) + len(stderr_data)
# 先发头--->是对数据的描述信息
header = struct.pack('i', total_size)
conn.send(header)
# 再发真实的信息
conn.send(stdout_data)
conn.send(stderr_data)
except Exception:
# 针对Windows系统
break
conn.close() # (必选操作)关闭客户端套字节 回收资源的操作
客户端
import socket
import struct
HOST = '127.0.0.1'
PORT = 8080 # 0-65535 1024前被系统保留使用
BUFSIZE = 8096
ADDR = (HOST, PORT)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ADDR)
while True: # 开启通信循环 不停的收发消息
cmd = input('cmd>>>').strip()
if not cmd: # 不让客户端发送空消息
continue
client.send(cmd.encode('utf-8'))
# 服务端知道数据的总大小
header = client.recv(4) # 接收4字节的大小
total_size = struct.unpack('i', header)[0] # 取小元组的0索引就是数据的大小
recv_size = 0
while recv_size < total_size:
recv_data = client.recv(BUFSIZE)
recv_size += len(recv_data)
print(recv_data.decode('gbk'), end='')
else:
print()
client.close() # 关闭客户端 必要的选择
最终解决方案
可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。
发送时
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时
先接收报头长度,用struct模块的unpack方法取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容。
服务端
import socket
import subprocess
import struct
import json
HOST = '127.0.0.1'
PORT = 8080 # 0-65535 1024前被系统保留使用
BUFSIZE = 1024
ADDR = (HOST, PORT)
# STREAM 流式协议 TCP协议 DGRAM 数据报协议 UDP协议
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(ADDR) # 将服务端的IP和地址绑定到套接字
server.listen(5) # 监听链接 定义半链接池大小
print('服务端启动,监听地址:%s :%s' % (ADDR))
while True: # 链接循环 不停的接受请求 一直提供服务
# 等待客户端请求
conn, addr = server.accept() # 接受客户端链接
while True: # 通信循环 不停的收发消息
# send 和recv都会发起系统调用
try:
data = conn.recv(BUFSIZE)
if not data:
break
res = subprocess.Popen(data.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout_data = res.stdout.read()
stderr_data = res.stderr.read()
total_size = len(stdout_data) + len(stderr_data)
print(len(stderr_data) + len(stdout_data), addr)
# 制作报头字典
header_dic = {
"total_size": total_size
}
json_header_str = json.dumps(header_dic)
json_header_str_bytes = json_header_str.encode('utf-8')
# 先发头--->是对数据的描述信息
# 发送头的长度信息
conn.send(struct.pack('i', len(json_header_str_bytes)))
# 发头信息
conn.send(json_header_str_bytes)
# 再发真实的信息
conn.send(stdout_data)
conn.send(stderr_data)
except Exception:
# 针对Windows系统
break
conn.close() # (必选操作)关闭客户端套字节 回收资源的操作
客户端
import socket
import struct
import json
HOST = '127.0.0.1'
PORT = 8080 # 0-65535 1024前被系统保留使用
BUFSIZE = 8096
ADDR = (HOST, PORT)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ADDR)
while True: # 开启通信循环 不停的收发消息
cmd = input('cmd>>>').strip()
if not cmd: # 不让客户端发送空消息
continue
client.send(cmd.encode('utf-8'))
# 接收端
# 先接收4字节,提取头信息的长度
header_len = struct.unpack('i', client.recv(4))[0]
# 接收头信息并解析
json_header_str_bytes = client.recv(header_len)
json_header_str = json_header_str_bytes.decode('utf-8')
header_dic = json.loads(json_header_str)
print('报头信息:', header_dic)
total_size = header_dic['total_size']
recv_size = 0
while recv_size < total_size:
recv_data = client.recv(BUFSIZE)
recv_size += len(recv_data)
print(recv_data.decode('gbk'), end='')
else:
print()
client.close() # 关闭客户端 必要的选择
***************************************************************
完