粘包
先基于tcp协议写一个远程执行命令的程序:
客户端:
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
msg = input('请输入命令:>>>').strip()
if len(msg) == 0:
continue
if msg == 'quit':
break
client.send(msg.encode('utf-8'))
cmd_res = client.recv(1024)
print(cmd_res.decode('gbk'))
client.close()
服务端:
from socket import *
import subprocess
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
# 服务端应该做两件事
# 第一件事:循环地从板连接池中取出链接请求与其建立双向链接,拿到链接对象
while True:
conn, client_address = server.accept()
# 第二件事:拿到链接对象,与其进行通信循环
while True:
try:
cmd = conn.recv(1024)
if len(cmd) == 0:
break
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout_res = obj.stout.read()
stderr_res = obj.stderr.read()
conn.send(stdout_res+stderr_res)
except Exception:
break
conn.close()
在上述代码运行过程有可能会出现粘包的现象。
何为粘包现象
TCP协议是面向流的协议,在数据传输过程,数据就像流水一样,从服务端发送至客户端的缓存中,服务端可能会一次性发送大量的数据,而客户端一次只能接收一定数量的数据,就会导致客户端的缓存中还留有服务端一次性发送的数据,还未被接收的数据,然后服务端再发送数据,就导致上次未接收完的数据与新发送的数据首尾相连(TCP协议是面向流的协议,数据都是连在一起的),就导致了粘包现象。
两种情况下会发生粘包。
1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包
2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
所以最开始的代码,就会因为客户端发送命令,服务端接收命令,并将命令的执行结果发送给客户端,而有些命令的结果会过长,导致客户端接收不完全,导致数据遗留在了缓存区,形成了粘包现象
解决粘包问题
解决粘包问题思路:
1、先收固定长度的头,解析出数据的描述信息,包括;数据的总大小total_size
2、recv_size=0,循环接收,每接收一次,recv_size+=接收的长度
3、直到recv_size=total_size
客户端:
from socket import *
import struct
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
msg = input('请输入命令:>>>').strip()
if len(msg) == 0:
continue
if msg == 'quit':
break
header = client.recv(4)
total_size = struct.unpack('i', header)[0]
recv_size = 0
while recv_size < total_size:
recv_data = client.recv(1024)
recv_size += len(recv_data)
print(recv_data.decode('utf-8'))
client.send(msg.encode('utf-8'))
cmd_res = client.recv(1024)
print(cmd_res.decode('utf-8'))
client.close()
服务端:
from socket import *
import subprocess
import struct
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
# 服务端应该做两件事
# 第一件事:循环地从板连接池中取出链接请求与其建立双向链接,拿到链接对象
while True:
conn, client_address = server.accept()
# 第二件事:拿到链接对象,与其进行通信循环
while True:
try:
cmd = conn.recv(1024)
if len(cmd) == 0:
break
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout_res = obj.stout.read()
stderr_res = obj.stderr.read()
total_size = len(stderr_res) + len(stdout_res)
# 1、先发头信息(固定长度的bytes):对数据描述信息
# int->固定长度的bytes
header = struct('i', total_size)
conn.send(header)
# 2、再发送真实的数据
conn.send(stdout_res)
conn.send(stderr_res)
except Exception:
break
conn.close()
通常情况下,头部的信息不仅仅是包含数据的大小,而应该会有更多的信息,例如,文件名字等等
解决方法之一:
将头部信息写入字典
优化之后:
客户端:
import json
from socket import *
import struct
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
msg = input('请输入命令:>>>').strip()
if len(msg) == 0:
continue
if msg == 'quit':
break
# 接收端,先接收4个字节,从中提取接下来要接收的头的长度
header_size = client.recv(4)
header_len = struct.unpack('i', header_size)[0]
# 提取到了头部长度,就可以接收头部信息字典
json_str_bytes = client.recv(header_len)
# 将bytes格式转化了json,再转化为utf-8
json_str = json_str_bytes.decode('utf-8')
header_dic = json.loads(json_str)
total_size = header_dic['total_size']
recv_size = 0
while recv_size < total_size:
recv_data = client.recv(1024)
recv_size += len(recv_data)
print(recv_data.decode('utf-8'))
client.send(msg.encode('utf-8'))
cmd_res = client.recv(1024)
print(cmd_res.decode('utf-8'))
client.close()
服务端:
from socket import *
import subprocess
import struct
import json
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, client_address = server.accept()
while True:
try:
cmd = conn.recv(1024)
if len(cmd) == 0:
break
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout_res = obj.stdout.read()
stderr_res = obj.stderr.read()
total_size = len(stderr_res) + len(stdout_res)
# 头部信息
header_dic = {
'filename': 'a.txt',
'total_size': 123545687,
'md5': 'asd789a8s7cv46x57a8w'
}
# 将头部信息写成json格式,可以方便其他平台进行接收
json_str = json.dumps(header_dic)
json_str_bytes = json_str.encode('utf-8')
# 将带有头部信息的字典转成固定大小,方便客户端解析,提取头部信息
header_size = struct.pack('i', len(json_str_bytes))
conn.send(header_size)
conn.send(json_str_bytes)
# 再发送真实的数据
conn.send(stdout_res)
conn.send(stderr_res)
except Exception:
break
conn.close()
socketserver模块
基于tcp协议
解决多并发问题:
服务端:
import socketserver
class MyRequestHandle(socketserver.BaseRequestHandler):
def handle(self):
print(self.request) # 如果是tcp协议,相当于self.request=>conn
print(self.client_address)
while True:
try:
msg = self.request.recv(1024)
if len(msg) == 0:
break
self.request.send(msg.upper())
except Exception:
break
self.request.close()
# 服务端应该做两件事情
# 第一件事:循环地从半连接池中取出链接请求与其建立双向链接,拿到链接对象
s = socketserver.ThreadingTCPServer(('127.0.0.1', 8099), MyRequestHandle)
s.serve_forever()
# 等同于
# while True:
# conn,client_address = server.accept()
# 启动一个线程(conn,client_address)
# 第二件事:拿到链接对象,与其进行通信循环====》hanle方法
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8099))
while True:
msg = input('请输入你的信息:').strip()
if len(msg) == 0:
continue
client.send(msg.encode('utf-8'))
res = client.recv(1024)
print(res.decode('utf-8'))
基于udp协议
服务端:
class MyRequestHanlde(socketserver.BaseRequestHandler):
def handle(self):
client_data = self.request[0]
server = self.request[1]
client_address = self.client_address
print('客户端发来的数据:%s' % client_data)
server.sendto(client_data.upper(), client_address)
s = socketserver.ThreadingUDPServer(('127.0.0.1', 8090), MyRequestHanlde)
s.serve_forever()
客户端:
client = socket(AF_INET, SOCK_DGRAM)
client.connect(('127.0.0.1', 8090))
while True:
msg = input('请输入你的信息:').strip()
if len(msg) == 0:
continue
client.send(msg.encode('utf-8'))
res = client.recv(1024)
print(res.decode('utf-8'))