006---粘包现象分析以及解决粘包问题
粘包
什么是粘包?
须知:只有TCP有粘包现象、UDP永远不会粘包。
socket收发消息的原理
模拟ssh远程执行的命令
# 服务端
import subprocess, socket
sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8011))
sk.listen(5)
print('starting...1')
while True:
conn, addr = sk.accept()
while True:
try:
# 收命令
cmd = conn.recv(1024)
if not cmd:
break
print('客户端发来的数据', cmd.decode('utf-8'))
# 执行命令,拿到结果
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
res = obj.stdout.read()
res_error = obj.stderr.read()
if not res and not res_error:
conn.send('呵呵呵'.encode('gbk'))
# 返回执行命令的结果给客户端
conn.send(res + res_error) # 可以优化
print(len(res) + len(res_error))
except ConnectionResetError:
break
conn.close()
sk.close()
# 客户端
import socket
sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sk.connect(('127.0.0.1', 8011))
while True:
# 发命令
cmd = input('输入你的cmd:').strip()
if not cmd: continue
sk.send(cmd.encode('utf-8'))
# 接收执行命令的结果
data = sk.recv(1024)
print(data.decode('gbk'))
sk.close()
分析
正常的cmd命令ipconfig
应该显示完全,可我们的tcp协议模拟的服务器和客户端并没有显示完全。
发送端可以是1k1k的发送数据,而接收端可以2k2k的提取数据,甚至10k10k的提取。也就是说应用程序看到的数据是一个流。
只有tcp有粘包现象,udp永远不会有粘包现象。
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
分类:
- 发送端粘包:需要等到缓冲区满了一定字节的数据才发给服务端。造成粘包。发送时间的间隔很短,数据很小,合到一起就粘包了。
# 服务端
import socket
sk = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
sk.bind(('127.0.0.1',8011))
sk.listen(5)
conn,addr = sk.accept()
data1 = conn.recv(1024)
print('第一次',data1)
data2 = conn.recv(1024)
print('第二次',data2)
# 客户端
import socket
client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
client.connect(('127.0.0.1',8011))
client.send('hello'.encode('utf-8'))
client.send('p1111'.encode('utf-8'))
client.send('p1111'.encode('utf-8'))
client.send('p1111'.encode('utf-8'))
- 接收方粘包(之前的模拟的cmd命令):接收的字节数小于发送方发送的字节数。所以,接收方的缓冲区有积压数据,下次接收就是直接取已残留的数据。
解决粘包问题
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。
struct模块
该模块可以把一个类型,如数组转化为固定长度的bytes。
服务端
import subprocess
import socket
sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8005))
sk.listen(5)
print('starting...1')
while True:
conn, addr = sk.accept()
while True:
try:
cmd = conn.recv(1024)
if not cmd:
break
print('客户端发来的命令:', cmd.decode('utf-8'))
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
res = obj.stdout.read()
res_error = obj.stderr.read()
if not res and not res_error:
conn.send('呵呵呵'.encode('gbk'))
length = len(res + res_error)
print('给客户端发送的执行命令结果的长度:', length)
import struct
data_length = struct.pack('i', length)
conn.send(data_length)
conn.send(res + res_error)
except ConnectionResetError:
break
conn.close()
sk.close()
客户端
import socket
sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sk.connect(('127.0.0.1', 8005))
while True:
# 发命令
cmd = input('输入你的cmd:').strip()
if not cmd: continue
sk.send(cmd.encode('utf-8'))
import struct
data_length = sk.recv(4)
length = struct.unpack('i', data_length)[0]
print('客户端接收服务端发来的数据长度:', length)
recv_size = 0
recv_msg = b''
while recv_size < length:
recv_msg += sk.recv(1024)
recv_size = len(recv_msg)
print('执行结果为:', recv_msg.decode('gbk'))
sk.close()