• 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
  • 相关阅读:
    Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
    IOC的优点是什么?
    《精益软件开发管理之道》阅读笔记02
    每日日报18
    每日日报17
    ecplise中没有Java Application的解决办法
    HTML+CSS+div 制作简单的登录界面
    HTML+CSS:通过li标签制作导航条
    每日日报16
    用Python爬取最新疫情数据(使用PyCharm)
  • 原文地址:https://www.cnblogs.com/cnike/p/10726930.html
Copyright © 2020-2023  润新知