python----网络编程之解决远程命令程序的粘包问题

远程执行命令程序开发

上一篇我们实现了server与client端的聊天程序,这一篇我们实现一个远程执行命令的程序.

我们用到subprocess模块.

res = subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdout=subprocess.PIPE)

注意的是:命令结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在客户端接收需要用GBK解码,且只能从管道里读一次结果.

服务端:

import socket
import subprocess

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

phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

phone.bind(('127.0.0.1', 8083))  # 0-65535:0-1024个操作系统使用,1024以后随便用

phone.listen(5)

print('staring...')
while True:
    conn, client_addr = phone.accept()  # 接收链接对象
    print(client_addr)

    # 5.收 发消息
    while True:  # 通信循环
        try:
            # 1.收命令
            cmd = conn.recv(1024)  # 1.单位:bytes 2.1024代表最大接受1024个bytes
            # if not data:break  # 适用于linux操作系统  如果没有接收,break 客户端强制关闭
            print('客户端的数据', cmd)

            # 2.执行命令,拿到结果
            obj = subprocess.Popen(cmd.decode('gbk'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()

            # 3.把命令的结果返回个客户端
            print(len(stdout)+len(stderr))
            conn.send(stdout+stderr)  # 是一个可以优化的点

        except ConnectionResetError:  # 适用于windows系统
            print('客户端强制关闭')
            break

    conn.close()

phone.close()
View Code

客户端:

import socket


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

phone.connect(('127.0.0.1', 8083))

# 3.发 收消息
while True:
    # 1.发命令
    cmd = input('>>>:').strip()  # msg = ''  输入空
    if not cmd:continue  # 不能输入空
    phone.send(cmd.encode('gbk'))  # phont.send(b'')

    # 2.拿到命令的结果,并打印
    data = phone.recv(1024)  # 1024是一个坑
    print(data.decode('gbk'))

# 4.关闭
phone.close()
View Code

windows系统尝试  dir  tasklist 命令,拿到了正确的结果!!

但是在多次尝试后是有问题的.我们输入dir拿到正确的结果,然后我们输入tasklist命令,再输入dir....结果是什么样的呢???dir拿错结果了!!

是因为,tasklist命令的结果比较长,但客户端只recv(1024), 可结果比1024长呀,那怎么办,只好在服务器端的IO缓冲区里把客户端还没收走的暂时存下来,等客户端下次再来收,所以当客户端第2次调用recv(1024)就会首先把上次没收完的数据先收下来,再收dir命令的结果。

这个现象叫做粘包,就是指两次结果粘到一起了。它的发生主要是因为socket缓冲区导致的,看下图:

你的程序实际上无权直接操作网卡的,你操作网卡都是通过操作系统给用户程序暴露出来的接口,那每次你的程序要给远程发数据时,其实是先把数据从用户态copy到内核态,这样的操作是耗资源和时间的,频繁的在内核态和用户态之前交换数据势必会导致发送效率降低, 因此socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方。若连续几次需要send的数据都很少,通常TCP socket 会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。  

  (1)发送方原因

  我们知道,TCP默认会使用Nagle算法。而Nagle算法主要做两件事:

  1.只有上一个分组得到确认,才会发送下一个分组;

  2.收集多个小分组,在一个确认到来时一起发送。

  所以,正是Nagle算法造成了发送方有可能造成粘包现象。

  (2)接收方原因

  TCP接收到分组时,并不会立刻送至应用层处理,或者说,应用层并不一定会立即处理;实际上,TCP将收到的分组保存至接收缓存里,然后应用程序主动从缓存里读收到的分组。这样一来,如果TCP接收分组的速度大于应用程序读分组的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包。

粘包现象只存在TCP,UDP不存在粘包现象

如何处理粘包现象

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据

服务端:

import json
import socket
import struct
import subprocess

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

phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

phone.bind(('127.0.0.1', 8083))

phone.listen(5)

print('staring...')
while True:
    conn, client_addr = phone.accept()  # 接收链接对象
    print(client_addr)

    while True:
        try:
            cmd = conn.recv(1024)
            print('客户端的数据', cmd)

            obj = subprocess.Popen(cmd.decode('gbk'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 第一步:把报头(固定长度)发给客户端
            header_dic = {
                'filename': 'a.txt',
                'md5': 'xxx',
                'total_size': len(stdout)+len(stderr)
            }
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode('gbk')

            # 第二步:先发送报头的长度
            conn.send(struct.pack('i', len(header_bytes)))

            # 第三步:再发送报头
            conn.send(header_bytes)

            # 第四步:再发送真实的数据
            conn.send(stdout+stderr)

        except ConnectionResetError:
            print('客户端强制关闭')
            break

    conn.close()

phone.close()
View Code

客户端:

import json
import socket
import struct

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

phone.connect(('127.0.0.1', 8083))

while True:
    cmd = input('>>>:').strip()
    if not cmd:continue
    phone.send(cmd.encode('utf-8'))

    # 第一步:先收报头长度
    obj = phone.recv(4)
    header_size = struct.unpack('i', obj)[0]

    # 第二步:再收报头
    header_bytes = phone.recv(header_size)

    # 第三步:从报头中解析出对帧数数据的描述信息
    header_json = header_bytes.decode('gbk')
    header_dic = json.loads(header_json)
    print(header_dic)
    total_size = header_dic['total_size']

    # 第四步:接收真实的数据
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = phone.recv(1024)
        recv_data += res
        recv_size += len(res)
        print(recv_data.decode('gbk'))

phone.close()
View Code

猜你喜欢

转载自www.cnblogs.com/cnike/p/10726930.html