socket网络编程:粘包现象以及解决方法(代码完善)
Posted py-xiaoqiang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了socket网络编程:粘包现象以及解决方法(代码完善)相关的知识,希望对你有一定的参考价值。
粘包现象
.recv(1024)坑:当传送来的数据超过1024bytes的时候,因为recv只能一次接受1024byte,传输管道就会积压数据 → 下次recv会继续接收积压的数据 → 这回导致本次send的处理结果可能返回的是上次的结果内容的一部分,
粘包现象:TCP协议是流数据协议(传送的是一个整体的流数据),即一条消息对应多少bytes是不可见的。 这导致接收方接受数据的时候不知道怎么断句,使得数据混乱。这就是所谓的粘包现象
注意:只有TCP有粘包现象,UDP永远不会粘包
粘包详解send 和 recv详解
首先我们强调一下:
应用软件不能直接操作硬件,必须借由OS来调用硬件。所以应用软件的send和recv都是针对os的数据传输(应用层)。
其他底层的网络协议的实现都是由os完全接管的。
- 粘包总结:
无论send还是recv都是针对本地os的内存进行数据交换而不是直接跟目标服务器进行数据交换
注意:不是一次send就只能对应一次recv,可以任意次对应任意次
- recv,send处理:
- recv:
- wait data:os等待数据 耗时长(需要一直等待)
- copy data:从os内存拷贝到数据到应用软件
- send:
- send data:从应用软件拷贝数据到os内存
- 粘包:
- 接收取数据时,一个包数据过大,使数据有一部分残留在了内存里。
- 接收取数据时,把多个包的数据当成一个包(Nagle导致的)接收了。
- recv:
TCP流传输使用Nagle优化算法把包合并起来发送:这是粘包的一个原因
Nagle算法:把数据量小并且时间间隔比较短的包合成一个包发送
- 解决方法:
- 明确知道包的bytes数的话,就可以避免粘包了 → 事先告诉接收端包的信息就可以了
解决粘包问题
我们用ssh模拟的程序说明:
我么虽然可以recv的参数设定的很大,但是这也是固定值,最大值也不会超过内存的空间,所以也不是个很好的方案
- 那么我们分析下怎么做:
- 发送信息的是对方,所以发送数据的时候
- 把数据长度发送给对方
- 再把数据发送给对方
- 发送信息的是对方,所以发送数据的时候
server.py
import socket
import subprocess
import struct
import locale
ssh_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssh_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ssh_server.bind(('127.0.0.1', 8080))
ssh_server.listen(3)
while True:
conn, addr = ssh_server.accept()
while True:
try:
cmd = conn.recv(1024).decode('utf-8')
if not cmd: break
print("excute cmd:", cmd)
obj = subprocess.Popen(cmd, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stderr.read()
os_locale = locale.getdefaultlocale()
os_encode = os_locale[1]
print(stdout.decode(os_encode))
# 第一步:生成数据的报头(报头:固定长度):需要用到struct模块
# 我们首先需要把发送数据的描述信息发送给客户端,但是客户端并不知道这个包是干什么的,而且会和数据的包粘包
# 所以我们把这段数据作为数据的报头发送
total_size = len(stdout) + len(stderr)
header = struct.pack('i', total_size) # 打包报头,打包返回的数据是一个固定长度(4bytes)的bytes类型
# struct.pack(模式,报头数据),注意:'i'模式int范围:正负2*10^9之内(因为只有4bytes)
# 另外有'l'模式:长整型。但是这个模式报头为8bytes。也有范围限制
# 第二部:发送报头
conn.send(header) # 因为是bytes类型所以可以直接发送
# 把命令结果返回给客户端
# conn.send(stdout+stderr) # 这里之前用+连接一起发送,但其实粘包也可以一起发送
conn.send(stdout) # 连续发送可以粘包
conn.send(stderr) # 与上一行粘包成一个数据包了
except ConnectionResetError as e:
print(e)
break
conn.close()
ssh_server.close()
client.py
import socket
import struct
ssh_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssh_client.connect(('127.0.0.1', 8080))
while True:
# 发命令
cmd = input(">>>>:").strip()
if not cmd:continue
ssh_client.send(cmd.encode('utf-8'))
# 第一步:先收报头
header = ssh_client.recv(4) # 因为报头固定4byte 所以我们收4
# 第二部:从报头解析出数据的描述(数据的长度)
total_size = struct.unpack('i', header)[0] # 解包报头,返回一个元组类型,这里我们第一个元素是长度
print(total_size) # 打印一下报头内容
# 第三部:接受真实收据
recv_size = 0
recv_data = b''
while recv_size < total_size:
res = ssh_client.recv(1024)
recv_data += res
recv_size += len(res)
print(recv_data.decode('cp932')) # 因为粘包问题解决了,这里我们直接打印
ssh_client.close()
以上是关于socket网络编程:粘包现象以及解决方法(代码完善)的主要内容,如果未能解决你的问题,请参考以下文章