Learning-Python【29】:网络编程之粘包

粘包问题

  上一篇博客遗留了一个问题,在接收的最大字节数设置为 1024 时,当接收的结果大于1024,再执行下一条命令时还是会返回上一条命令未执行完成的结果。这就是粘包问题。

  因为TCP协议又叫流式协议,每次发送给客户端的数据实际上是发送到客户端所在操作系统的缓存上,客户端就是一个应用程序,需要通过操作系统才能调到缓存内的数据,而缓存的空间是一个队列,有 “先进先出” 的思想,当第一次的 tasklist 数据未接收完,第二次又来一个 dir 的数据时,只能等第一次先全部接收完成才会接收后面的。

  有一个解决方法是每次在接收数据时,都将数据的完整结果全部接收,这样就不会出现粘包现象。那该怎么样才能全部接收呢?有人说将接收的最大字节数设置大点不就能接收 tasklist 的全部执行结果了吗?这样做确实可以,但如果是文件的上传下载呢?客户端执行下载命令,服务端将下载的结果发送给客户端,客户端再接收,文件的大小是超过 GB、TB 的,那最大字节数该设置多大?其实设置再大也没有意义,因为客户端接收数据是通过自己操作系统的缓存空间接收的,缓存空间的大小不可能比自己计算机的物理内存还大,就算和物理内存一样大,假设物理内存是 8G,那你也只能一次收到 8GB 的数据,当发送的数据超过 8G 呢?

  TCP协议为了优化传输效率,而导致了粘包问题。客户端和服务端之间是基于网络收发数据,网络的 I/O 是越少越好,TCP有一种 Nagle 算法,是将多次时间间隔较短且数据量小的数据,合并成一个大的数据块,然后进行封包,这样,尽可能多的降低 I/O,从而提升程序的运行效率。但是接收端很难分辨出来,这就导致了粘包问题。

总结粘包问题:

粘包不一定会发生

如果发生了:1)可能是在客户端已经粘了

      2)客户端没有粘,可能是在服务端粘了

 客户端粘包:发送数据时间间隔很短,数据量很小,TCP优化算法会当做一个包发出去,产生粘包

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)

conn, client_addr = server.accept()

data1 = conn.recv(1024)
print("第一次收: ", data1)

data2 = conn.recv(1024)
print("第二次收: ",data2)

data3 = conn.recv(1024)
print("第三次收: ",data3)
服务端
from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

# TCP协议会将数据量较小且时间间隔较短的数据合并成一个数据报发送
client.send(b'hello')
client.send(b'world')
client.send(b'qiu')
客户端
第一次收:  b'helloworldqiu'
第二次收:  b''
第三次收:  b''
运行结果

服务端粘包:客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)

conn, client_addr = server.accept()

data1 = conn.recv(1)
print("第一次收: ", data1)

data2 = conn.recv(2)
print("第二次收: ",data2)

data3 = conn.recv(1024)
print("第三次收: ",data3)
服务端
from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

client.send(b'hello')
client.send(b'world')
client.send(b'qiu')
客户端
第一次收:  b'h'
第二次收:  b'el'
第三次收:  b'loworldqiu'
运行结果

粘包问题的解决思路

  问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是发送端在发送数据前,发一个头文件包,里面包含每次要发送数据的长度,构成一个总长度,然后接收端用循环接收完所有数据,但是长度是整型,发送的数据是字节,所以要将整型转成字节类型再发送。

struct 模块

使用 struct 模块可以用于将 Python 的 int 类型转换为 bytes 类型

struct 模块中最重要的三个函数是pack(), unpack(), calcsize()

pack(fmt, v1, v2, ...):按照给定的格式 (fmt),把数据封装成字符串(实际上是类似于 C 语言中结构体的字节流)

unpack(fmt, string):按照给定的格式 (fmt) 解析字节流 string,返回解析出来的 tuple

calcsize(fmt):计算给定的格式 (fmt) 占用多少字节的内存

struct 中支持的格式如下表

import struct

obj = struct.pack('i', 1231)
print(obj)
print(len(obj))     # C语言中int类型占4个字节

res = struct.unpack("i", obj)
print(res)
print(res[0])

# 执行结果
b'\xcf\x04\x00\x00'
4
(1231,)
1231
struct模块

 模拟ssh实现远程执行命令(解决粘包问题简单版)

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_addr = server.accept()

    # 通信循环
    while True:
        try:
            cmd = conn.recv(1024)  # cmd = b'dir'
            # 针对Linux系统
            if len(cmd) == 0:
                break
            # 命令的执行结果
            obj = subprocess.Popen(cmd.decode("utf-8"),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 1. 先制作固定长度的报头
            header = struct.pack("i", len(stdout) + len(stderr))
            # 2. 再发送报头
            conn.send(header)
            # 3. 最后发送真实的数据
            conn.send(stdout)
            conn.send(stderr)

        except ConnectionResetError:
            break

    conn.close()
server.close()
服务端
from socket import *
import struct

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

# 通信循环
while True:
    cmd = input("请输入: ").strip()
    if len(cmd) == "0":
        continue
    client.send(cmd.encode("utf-8"))
    # 1. 先收报头, 从报头里解出数据的长度
    header = client.recv(4)
    total_size = struct.unpack("i", header)[0]
    # 2. 接收真正的数据
    cmd_res = b""
    # 接收数据的长度初始值为0
    recv_size = 0
    # 当接收的数据长度小于报头长度
    while recv_size < total_size:
        data = client.recv(1024)
        recv_size += len(data)
        cmd_res += data

    print(cmd_res.decode("gbk"))

client.close()
客户端

这样写有一个限制,我在 struct 模块中设置的是 i 格式,只能用于传输较小的字节数,且此时报头里只包含数据长度信息,如果是上传下载文件,还可能包含文件名、文件大小、文件的 md5 值等其它信息,那这种方法就不适用了

可以考虑将报头设置成一个字典,包含相关的信息,然后将字典序列化成 JSON 格式发送,在接收方反序列化还能得到字典格式,且可以设置字典里的文件大小很大,但 JSON 的长度却很小

import json

header_dic = {
    "filename": "a.txt",
    "md5": "DASHJH423465CSA",
    "total_size":456165446511564651351456413514543543
}

header_json = json.dumps(header_dic)
print(len(header_json))

# 运行
99
报头字典序列化成JSON的长度

模拟ssh实现远程执行命令(解决粘包问题终极版)

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_addr = server.accept()

    # 通信循环
    while True:
        try:
            cmd = conn.recv(1024)  # cmd = b'dir'
            # 针对Linux系统
            if len(cmd) == 0:
                break
            # 命令的执行结果
            obj = subprocess.Popen(cmd.decode("utf-8"),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 1. 先制作报头
            header_dic = {
                "filename": "a.txt",
                "md5": "DASHJH423465CSA",
                "total_size": len(stdout) + len(stderr)
            }
            # 将报头序列化成json格式的字符串
            header_json = json.dumps(header_dic)
            # json格式的字符串转成bytes类型
            header_bytes = header_json.encode("utf-8")

            # 2. 先发送4个bytes(包含报头的长度)
            conn.send(struct.pack("i", len(header_bytes)))

            # 3. 发送报头
            conn.send(header_bytes)

            # 4. 最后发送真实的数据
            conn.send(stdout)
            conn.send(stderr)

        except ConnectionResetError:
            break

    conn.close()
server.close()
服务端
from socket import *
import struct
import json

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

# 通信循环
while True:
    cmd = input("请输入: ").strip()
    if len(cmd) == "0":
        continue
    client.send(cmd.encode("utf-8"))
    # 1. 先收4个bytes, 解出报头长度
    header_size = struct.unpack("i", client.recv(4))[0]

    # 2. 再接收报头, 拿到head_dic
    header_bytes = client.recv(header_size)
    header_json = header_bytes.decode("utf-8")
    head_dic = json.loads(header_json)
    print(head_dic)
    total_size = head_dic["total_size"]

    # 3. 接收真正的数据
    cmd_res = b""
    # 接收数据的长度初始值为0
    recv_size = 0
    # 当接收的数据长度小于报头长度
    while recv_size < total_size:
        data = client.recv(1024)
        recv_size += len(data)
        cmd_res += data

    print(cmd_res.decode("gbk"))

client.close()
客户端

猜你喜欢

转载自www.cnblogs.com/qiuxirufeng/p/9910118.html