嘘~ 正在从服务器偷取页面 . . .

网络通信编程学习(5)—— 基于SOCKET模拟SSH远程执行命令后出现的粘包问题及解决方案


粘包问题:问题出现

前面我们已经实现了利用socket模拟远程执行命令,但是我们在代码运行中很有可能会遇到这样的问题,如下图。

客户端运行结果

我们发现命令 ipconfig命令的结果长度显然已经超过了 1024 phone.recv(1024),而这样的结果就是该命令的结果分成两部分发送,导致 xxx命令的结果在下一个命令发送后才能收到,可以说因为结果超过了 1024 而造成了堵塞。

上述情况我们称之为 粘包现象

粘包问题:分析

需要注意的是,粘包问题的本质并不是信息发送的堵塞,只是表现比较像

实际上粘包问题是由 socket 底层算法造成的——就简单来说就是在传输信息时底层算法会把要 send的信息打包在一起发送,而不管他们是否应该被打包在一起发送(即把前后两次 send 时间间隔短,字节长度短的信息打包在一起发送

文字描述似乎不太好理解,我们可以简单写一段代码进行分析。

服务端

# coding=gbk
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind(("127.0.0.1"))

server.listen(5)

conn, addr = server.accept()

res = conn.recv(1024)
res1 = conn.recv(1024)

print("第一次收到的信息是:", res)
print("第二次收到的信息是:", res1)

conn.close()
server.close()

客户端

# coding=gbk
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect(("127.0.0.1", 8080))

client.send("hello".encode("utf-8"))
client.send(" world!".encode("utf-8"))

client.close()

在运行前,我们可以会认为运行结果是这样的

第一次收到的信息是:hello
第二次收到的信息是: world!

但实际上我们的运行结果是这样的

这其实就是粘包问题。在我们所举的例子里要想解决这个问题是比较简单的,比如说将 conn.recv(1024)改成conn.recv(5)就可以了。

实际上这就是我们解决粘包问题的方法。

粘包问题:文字版解决方案

通过上面的分析我们了解到要想解决粘包问题的关键是知道传递给我们的消息的长度到底是多长。

所以我们的解决方法是
发消息端
第一步、编码信息的长度并发送
接收消息端
第一步、接收编码并解码获得信息长度数据
第二步、循环使用 conn.recv(1024) 直到信息全部接收完毕
注意、最好不要使用 conn.recv(真正信息的长度) 因为这意味着接收消息一方需要开‘信息长度’的缓存,如果长度过长,很有可能开不出这样大小的缓存

但这个方案也有一个问题,就是接收消息的一方在接受信息的时候怎么知道是长度是几位数?即使用的代码是 conn.recv(4) 还是 conn.recv(5) 或是别的长度

面对这个问题,我们的解决方案是采用独特的编码方式,将长度——无论是几位数——都编码成固定长度(即报头),发送给接收端。而这就需要用到struct模块了。

粘包问题:struct 模块补充

代码演示

# coding=gbk
import struct

res = struct.pack("i", 1230000) # i 表示整型,编码后的长度固定为 4
res1 = struct.pack("i", 123)
print(res, type(res))
print(len(res) == len(res1) == 4)
print("\n")

# 但 i 对编码前的数字大小有要求
# res2 = struct.pack("i", 12300000000) # 报错

# q 表示 long 型,数字范围比 i 大,但也有范围限制
# 编码后的长度固定为 8
res3 = struct.pack("q", 12300000000)
print(len(res3) == 8)

# 解码
data = struct.unpack("i", res)[0]
print(data)

data1 = struct.unpack("q", res3)[0]
print(data1)

运行结果

粘包问题:简单代码解决

分析到这里我们就可以试着解决粘包问题了

服务端

# coding=gbk
import socket
import struct
import subprocess

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind(("127.0.0.1", 8080))

server.listen(5)

while True: # 连接循环
    conn, addr = server.accept()
    print(addr)
    while True: # 通讯循环
        try:
            # 收到消息
            cmd = conn.recv(8096)
            if not cmd:
                break
            print("收到客户端的消息是:", cmd)
            # 执行命令
            obj = subprocess.Popen(cmd.decode("gbk"), shell=True,
                                   stdout = subprocess.PIPE,
                                   stderr = subprocess.PIPE)
            # 获取执行命令的结果
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 对结果长度形成报文,并发送
            total_size = struct.pack("i", len(stdout)+len(stderr)) # 形成固定的长度为 4 的报文
            conn.send(total_size)

            # 发送最终结果
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()

serve.close()

客户端

# coding=gbk
import socket
import struct

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect(("127.0.0.1", 8080))

while True: # 通信循环
    # 发送命令
    cmd = input(">>: ").strip()
    if not cmd:
        continue
    client.send(cmd.encode("utf-8"))

    # 接收报文并解码
    total_size = struct.unpack("i", client.recv(4))[0]

    # 循环直到接收到完整的结果
    res_size = 0
    res = b""
    while res_size < total_size:
        data = client.recv(1024)
        res += data
        res_size += len(data)

    print(res.decode("gbk"))

client.close()

运行结果

服务端

客户端

粘包问题:终极代码解决

到目前我们似乎完美解决了粘包问题,但果真如此吗?如果我们要传递的消息长度超过了可以编码的长度又该怎么办?

所以我们应该要有一个通用的解决方案,即终结解决方案。
发消息端
第一步、计算信息的长度,为防止此数字不能编码,将数字放进字典
第二步、将字典转化为字符串,计算字符串的长度并编码发送
接收消息端
第一步、获得字典字符串长度编码并解码
第二步、循环接收完整字典字符串,将字符串转换成字典,获得真正信息的长度
第三步、循环接收真正信息

服务端

# coding=gbk
import struct
import socket
import json
import subprocess

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind(("127.0.0.1", 8080))

server.listen(5)

while True: # 连接循环
    conn, addr = server.accept()
    print(addr)
    while True: # 通信循环
        try:
            cmd = conn.recv(8096)
            if not cmd: # 若接收的命令为空
                break
            # 执行命令
            obj = subprocess.Popen(cmd.decode("gbk"), shell=True,
                                   stdout = subprocess.PIPE,
                                   stderr = subprocess.PIPE)
            # 获取命令执行结果
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 报头字典
            header_dict = {
                "name": cmd.decode("utf-8")+".txt",
                "dict_size": len(stderr) + len(stdout)
            }
            # 字典转化为字符串
            header_bytes = json.dumps(header_dict)
            # 字符串长度编码并发送
            header = struct.pack("q", len(header_bytes))
            conn.send(header)
            # 发送字典字符串
            conn.send(header_bytes.encode("utf-8"))
            # 发送真实消息
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()

server.close()

客户端

# coding=gbk
import socket
import struct
import json

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect(("127.0.0.1", 8080))

while True: # 通信循环
    # 输入命令并发送
    cmd = input(">>: ").strip()
    if not cmd:
        continue
    client.send(cmd.encode("utf-8"))
    # 接收字典长度编码并解码
    dic_total_size = struct.unpack("q", client.recv(8))[0]
    # 接收字典字符串
    dic_size = 0
    dic_bytes = b""
    while dic_size < dic_total_size:
        data = client.recv(1024)
        dic_bytes += data
        dic_size += len(data)
    # 将字典字符串转换为字典
    header_dic = json.loads(dic_bytes.decode("utf-8"))
    print(header_dic)
    # 获取真实信息长度
    total_size = header_dic["dict_size"]
    # 获取真实信息
    res_size = 0
    res = b""
    while res_size < total_size:
        data = client.recv(1024)
        res += data
        res_size += len(data)
    print(res.decode("gbk"))

client.close()

运行结果


文章作者: New Ass
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 New Ass !
  目录