远程执行命令程序开发
上一篇我们实现了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()
客户端:
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()
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()
客户端:
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()