一丶模拟远程SSH执行命令
模拟一个SSH“远程”执行命令并获取命令结果的一个程序:
1、在C/S架构下,当客户端与服务器建立连接(这里以TCP为例)后,二者可以不断的进行数据交互。SSH远程可以实现的效果是客户端输入命令可以在服务器中执行并且可以将结果返回给客户端。但是需要注意的一点事:客户端的“命令”在计算机看来仅仅是“字符串”而已,而真正需要执行的“命令”必须是操作系统能够识别的!也就是说,真正“执行命令”与“返回结果”的地方仍然是服务器端。在客户端我们只是“显示”出了一个这样的知执行假象而已。
那么,这样的一个SSH远程执行程序的具体流程是怎样的呢?
下图是这样的一个简单过程:
简单的过程说明:
对于客户端来讲,用户首先输入了str类型的命令command,然后程序将这个str类型的字符串encode成能够在网络中传输的bytes类型的数据并发送给服务器;服务器接收到以后将其重新解码为str,然后在本段“执行”这段代码生成str类型的结果,接着再进行编码传给客户端,客户端接收到以后解码为人能识别的str类型最终输出到屏幕上。
2、这个过程有两个大问题:一个是服务器端是如何进行“代码执行”的,另一个就是客户端与服务器端str与bytes格式数据的编解码问题。
2.1、对于第一个问题,我们利用subprocess模块下的Popen方法可以将str类型的“虚假命令”转换为操作系统能够识别的“真是命令”:
import subprocess cmd = input('>>>:') obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) print(obj.stdout.read().decode('gbk')) print(obj.stderr.read().decode('gbk'))
这里有几点需要说明:
(1)可以把obj看成是一个管道,它一次性的、毫无保留将所有执行结果都拿到,再一次取值时里面没有了数据,这就像一个“管道”一样,里面的数据全部取完后就“无所保留”了。
(2)关于输出结果解码的问题:subprocess模块下的Popen方法在不同的操作系统下结果的编码方式不同:linux下为utf-8模式,windows下为gbk模式。由于本例是在windows操作系统下进行的,所以我们在输出是要进行gbk的方式解码才能看到结果。
(3)最终的结果包含正确的信息stdout与错误的结果stderr(当用户输入一个不存在的命令时产生)。
这里需要注意的一点是:本例是在windows操作系统下执行的,所以result的编码方式要以gbk模式进行,这样才不会在客户端中产生乱码。
程序的代码以及运行结果为:
import socket import subprocess server_whw = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server_whw.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) server_whw.bind(('127.0.0.1',8008)) server_whw.listen(5) print('Starting......') conn,client_addr = server_whw.accept() while 1: try: cmd = conn.recv(8096) #将从客户端传来的命令信息进行处理 obj = subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) #将结果返回给客户端 stdout = obj.stdout.read().decode('gbk') stderr = obj.stderr.read().decode('gbk') std1 = stdout.encode('gbk') std2 = stderr.encode('gbk') conn.send(std1) conn.send(std2) except ConnectionResetError: print('连接断开') break conn.close() server_whw.close() Server_ssh
import socket client_whw = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client_whw.connect(('127.0.0.1',8008)) while 1: cmd = input('请输入命令:') if not cmd: continue client_whw.send(cmd.encode('utf-8')) data = client_whw.recv(1024) print('命令结果: ',data.decode('gbk')) # client_whw.close()
程序出现黏包问题~
二丶黏包
- 成因:
- server 若有很大的数据流返回给client,而client在recv的时候却指定了每次最大1024字节(server也指定了1024 ,但几乎没有cmd能超过1024)
- 即第二次socket取回的是第一次返回数据的残留数据
- 解决思路1:保证传输一次,就传完所有数据
- recv 范围:1024-8192
- 以太网最大包传输 1500,千兆网卡,所以每次报文最大1500,即recv范围调大也没有用,超过1500,则会切割成多个报文发送
- 解决思路2: ACK 校验
- server传输前发送总共数据的长度,client不断循环接受,每次收1024并计数,当长度相等时,停止接受
实例:
import socket import subprocess sk = socket.socket() sk.bind(('127.0.0.1', 9999,)) sk.listen(5) while True: conn, address = sk.accept() conn.send(bytes('welcome to my ssh server'.center(40, '*'), encoding='utf-8')) while True: try: # 接收 cmd cmd = conn.recv(1024).decode() if len(cmd) == 0: raise Exception('链路状态改变,client go to close()') # 获取cmd执行结果 obj = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout_data, stderror_data = obj.communicate() # 本身执行结果就为字节,只不过windows上为GBK的字节,linux上为UTF-8的字节 if len(stdout_data) == 0: send_data = stderror_data # 若标准输出管道中为空,则命令执行错误,应该将标准错误管道数据返回给用户 else: send_data = stdout_data # 若标准错误管道中为空,则命令执行正确,应该将标准输出管道数据返回给用户 # 发送len(data) conn.send(bytes('begin:{}'.format(len(send_data)), encoding='utf-8')) # 接收 ACK, 开始发送数据 ack = conn.recv(1024).decode() if len(ack) == 0: raise Exception('链路状态改变,ack 没有接收到') if ack.startswith('start'): conn.send(send_data) except Exception as ex: print(ex) break conn.close()
import socket sk = socket.socket() sk.connect(('127.0.0.1', 9999,)) welcome_msg = sk.recv(1024).decode() print(welcome_msg) while True: cmd = input('>> ').strip() if len(cmd) == 0: continue if cmd == 'exit': break sk.send(bytes(cmd, encoding='utf-8')) # 接收 len(data) data_msg = sk.recv(1024).decode() print('send: {}'.format(data_msg)) # 打印总共需要接收的数据大小 # 发送ACK,记录len(data) if data_msg.startswith('begin'): sk.send(bytes('start', encoding='utf-8')) data_size= int(data_msg.split(':')[-1]) # 循环接收 data msg = b'' recv_size = 0 while recv_size < data_size: recv_data = sk.recv(1024) msg += recv_data recv_size += len(recv_data) else: print('recive: {}'.format(recv_size)) # 打印最终接收了多少数据大小 # 显示给client,windows上为gbk,linux上为utf-8 print(msg.decode('gbk')) sk.close()
三丶解包,压包
了解c语言的人,一定会知道struct结构体在c语言中的作用,它定义了一种结构,里面包含不同类型的数据(int,char,bool等等),方便对某一结构对象进行处理。而在网络通信当中,大多传递的数据是以二进制流(binary data)存在的。当传递字符串时,不必担心太多的问题,而当传递诸如int、char之类的基本数据的时候,就需要有一种机制将某些特定的结构体类型打包成二进制流的字符串然后再网络传输,而接收端也应该可以通过某种机制进行解包还原出原始的结构体数据。python中的struct模块就提供了这样的机制,该模块的主要作用就是对python基本类型值与用python字符串格式表示的C struct类型间的转化(This module performs conversions between Python values and C structs represented as Python strings.)。stuct模块提供了很简单的几个函数
1、基本的pack和unpack
struct提供用format specifier方式对数据进行打包和解包(Packing and Unpacking)。例如:
import struct import binascii values = (1, 'abc', 2.7) s = struct.Struct('I3sf') packed_data = s.pack(*values) unpacked_data = s.unpack(packed_data) print 'Original values:', values print 'Format string :', s.format print 'Uses :', s.size, 'bytes' print 'Packed Value :', binascii.hexlify(packed_data) print 'Unpacked Type :', type(unpacked_data), ' Value:', unpacked_data
输出:
Original values: (1, 'abc', 2.7)
Format string : I3sf
Uses : 12 bytes
Packed Value : 0100000061626300cdcc2c40
Unpacked Type : <type 'tuple'> Value: (1, 'abc', 2.700000047683716)
代码中,首先定义了一个元组数据,包含int、string、float三种数据类型,然后定义了struct对象,并制定了format‘I3sf’,I 表示int,3s表示三个字符长度的字符串,f 表示 float。最后通过struct的pack和unpack进行打包和解包。通过输出结果可以发现,value被pack之后,转化为了一段二进制字节串,而unpack可以把该字节串再转换回一个元组,但是值得注意的是对于float的精度发生了改变,这是由一些比如操作系统等客观因素所决定的。打包之后的数据所占用的字节数与C语言中的struct十分相似。定义format可以参照官方api提供的对照表:
2、字节顺序
另一方面,打包的后的字节顺序默认上是由操作系统的决定的,当然struct模块也提供了自定义字节顺序的功能,可以指定大端存储、小端存储等特定的字节顺序,对于底层通信的字节顺序是十分重要的,不同的字节顺序和存储方式也会导致字节大小的不同。在format字符串前面加上特定的符号即可以表示不同的字节顺序存储方式,例如采用小端存储 s = struct.Struct(‘<I3sf’)就可以了。官方api library 也提供了相应的对照列表: