如今来写一个远程执行命令的程序,一个socket client端在windows端发送指令,一个socket server端在Linux端执行命令并返回结果给客户端。python
执行命令的话,确定是用subprocess模块,但要注意:算法
res = subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdout=subprocess.PIPE)
命令结果的编码是以当前所在的系统为准的,若是是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端须要用GBK解码,且只能从管道里读一次结果。shell
ssh server编程
import socket import subprocess ip_port = ('127.0.0.1', 8080) tcp_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) while True: conn, addr = tcp_socket_server.accept() print('客户端', addr) while True: cmd = conn.recv(1024) if len(cmd) == 0: break print("recv cmd",cmd) res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() print("res length",len(stdout)) conn.send(stderr) conn.send(stdout)
ssh clientjson
import socket ip_port = ('127.0.0.1', 8080) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) res = s.connect_ex(ip_port) while True: msg = input('>>: ').strip() if len(msg) == 0: continue if msg == 'quit': break s.send(msg.encode('utf-8')) act_res = s.recv(1024) print(act_res.decode('utf-8'), end='')
尝试执行ls、pwd命令,你惊喜的发现,拿到了正确的结果!windows
but 莫开心太早,此时执行一个结果比较长的命令,好比top -bn 1, 你发现依然能够拿到结果,但若是再执行一条df -h的话,就发现,你拿到并非df命令的结果,而是上一条top命令的部分结果。为啥捏?服务器
这是由于,top命令的结果比较长,但客户端只recv(1024), 可结果比1024长呀,那怎么办,只好在服务器端的IO缓冲区里把客户端还没收走的暂时存下来,等客户端下次再来收,因此当客户端第2次调用recv(1024)就会首先把上次没收完的数据先收下来,再收df命令的结果。网络
那怎么解决呢? 有些人可能会想到说,直接把recv(1024)改大不就行了,改为5000\10000或whatever。but这么干的话,并不能解决实际问题,由于你不可能提早知道对方返回的结果数据大下,不管你改为多大,对方的结果都有可能比你设置的大,另外这个recv并非真的能够随便改特别大的,有关部门建议的不要超过8192,再大反而会出现影响收发速度和不稳定的状况。ssh
同志们,这个现象叫作粘包,就是指两次结果粘到一块儿了。它的发生主要是由于socket缓冲区致使的,来看一下。socket
你的程序实际上无权直接操做网卡,操做网卡都是经过操做系统给用户程序暴露出来的接口,那每次你的程序要给远程发数据时,实际上是先把数据从用户态copy到内核态,这样的操做是耗资源和时间的,频繁地在内核态和用户态之间交换数据势必会致使发送效率下降,所以socket为提升传输效率,发送方每每要收集到足够多的数据后才会发送一次数据给对方。若连续几回须要send的数据都不多,一般TCP socket会根据优化算法把这些数据合成一个TCP段后一次发生出去,这样接收方就收到了粘包数据。
粘包现象只存在于TCP中,Not UDP
仍是看上图,发送端能够是一K一K地发送数据,而接收端的应用程序能够两K两K地提走数据,固然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个总体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,所以TCP协议是面向流的协议,这也是容易出现粘包问题的缘由。而UDP是面向消息的协议,每一个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不一样的。怎样定义消息呢?能够认为对方一次性write/send的数据为一个消息,须要明白的是当对方send一条信息的时候,不管底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈如今内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束。
所谓粘包问题主要仍是由于接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所形成的。
总结
一、TCP(transport control protocol,传输控制协议)是面向链接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,所以,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将屡次间隔较小且数据量小的数据,合并成一个大的数据块,而后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通讯是无消息保护边界的。
二、UDP(user datagram protocol,用户数据报协议)是无链接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 因为UDP支持的是一对多的模式,因此接收端的skbuff(套接字缓冲区)采用了链式结构来记录每个到达的UDP包,在每一个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来讲,就容易进行区分处理了。 即面向消息的通讯是有消息保护边界的。
三、tcp是基于数据流的,因而收发的消息不能为空,这就须要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即使是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。
上面说了,udp不存在粘包问题,咱们看一下实例
udp server
import socket import subprocess ip_port = ('127.0.0.1', 9003) bufsize = 1024 udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_server.bind(ip_port) while True: # 收消息 cmd, addr = udp_server.recvfrom(bufsize) print('用户命令----->', cmd,addr) # 逻辑处理 res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE, stdin=subprocess.PIPE, stdout=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() # 发消息 udp_server.sendto(stdout + stderr, addr) udp_server.close()
udp client
from socket import * import time ip_port = ('127.0.0.1', 9003) bufsize = 1024 udp_client = socket(AF_INET, SOCK_DGRAM) while True: msg = input('>>: ').strip() if len(msg) == 0: continue udp_client.sendto(msg.encode('utf-8'), ip_port) data, addr = udp_client.recvfrom(bufsize) print(data.decode('utf-8'), end='')
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,因此解决粘包的方法就是围绕,如何让发送端在发送数据前,把本身将要发送的字节流总大小让接收端知晓,而后接收端来一个死循环接收完全部数据。
一、普通青年版
server端
import socket,subprocess ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn,addr=s.accept() print('客户端',addr) while True: msg=conn.recv(1024) if not msg:break res=subprocess.Popen(msg.decode('utf-8'),shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err=res.stderr.read() if err: ret=err else: ret=res.stdout.read() data_length=len(ret) conn.send(str(data_length).encode('utf-8')) data=conn.recv(1024).decode('utf-8') if data == 'recv_ready': conn.sendall(ret) conn.close()
client端
import socket,time s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) length=int(s.recv(1024).decode('utf-8')) s.send('recv_ready'.encode('utf-8')) send_size=0 recv_size=0 data=b'' while recv_size < length: data+=s.recv(1024) recv_size+=len(data) #为何不直接写1024? print(data.decode('utf-8'))
为什么上面的代码很low?程序的运行速度远快于网络传输速度,因此在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
刚才上面 在发送消息以前需先发送消息长度给对端,还必需要等对端返回一个ready收消息的确认,不等到对端确认就直接发消息的话,仍是会产生粘包问题(承载消息长度的那条消息和消息自己粘在一块儿)。 有没有优化的好办法么?
二、文艺青年版(一)
思考一个问题,为何不能在发送了消息长度(称为消息头head吧)给对端后,马上发消息内容(称为body吧),是由于怕head 和body 粘在一块儿,因此经过等对端返回确认来把两条消息中断开。
可不能够直接发head + body,但又能让对端区分出哪一个是head,哪一个是body呢?
把head设置成定长的,这样对端只要收消息时,先固定收定长的数据,head里写好,后面还有多少是属于这条消息的数据,而后直接写个循环收下来不就完了嘛!
但是、但是如何制做定长的消息头呢?假设你有2条消息要发送,第一条消息长度是 3000个字节,第2条消息是200字节。若是消息头只包含消息长度的话,那两个消息的消息头分别是
len(msg1) = 4000 = 4字节 len(msg2) = 200 = 3字节
你的服务端如何完整的收到这个消息头呢?是recv(3)仍是recv(4)服务器端怎么知道?用尽我全部知识,我只能想到拼接字符串的办法了,打比方就是设置消息头固定100字节长,不够的拿空字符串去拼接。
server端
import socket,json import subprocess ip_port = ('127.0.0.1', 8080) tcp_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_socket_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #一行代码搞定,写在bind以前 tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) def pack_msg_header(header,size): bytes_header = bytes(json.dumps(header),encoding="utf-8") fill_up_size = size - len(bytes_header) print("need to fill up ",fill_up_size) header['fill'] = header['fill'].zfill(fill_up_size) print("new header",header) bytes_new_header = bytes(bytes(json.dumps(header),encoding="utf-8")) return bytes_new_header while True: conn, addr = tcp_socket_server.accept() print('客户端', addr) while True: cmd = conn.recv(1024) if len(cmd) == 0: break print("recv cmd",cmd) res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() print("res length",len(stdout)) msg_header = { 'length':len(stdout + stderr), 'fill':'' } packed_header = pack_msg_header(msg_header,100) print("packed header size",packed_header,len(packed_header)) conn.send(packed_header) conn.send(stdout + stderr)
client端
import socket import json ip_port = ('127.0.0.1', 8080) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) res = s.connect_ex(ip_port) while True: msg = input('>>: ').strip() if len(msg) == 0: continue if msg == 'quit': break s.send(msg.encode('utf-8')) response_msg_header = s.recv(100).decode("utf-8") response_msg_header_data = json.loads(response_msg_header) msg_size = response_msg_header_data['length'] res = s.recv(msg_size) print("received res size ",len(res)) print(res.decode('utf-8'), end='')
三、文艺青年版(二)
为字节流加上自定义固定长度报头也能够借助于第三方模块struct,用法为
import json,struct #假设经过客户端上传1T:1073741824000的文件a.txt #为避免粘包,必须自定制报头 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值 #为了该报头能传送,须要序列化而且转为bytes head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度 #客户端开始发送 conn.send(head_len_bytes) #先发报头的长度,4个bytes conn.send(head_bytes) #再发报头的字节格式 conn.sendall(文件内容) #而后发真实内容的字节格式 #服务端开始接收 head_len_bytes=s.recv(4) #先收报头4个bytes,获得报头长度的字节格式 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式 header=json.loads(json.dumps(header)) #提取报头 #最后根据报头的内容提取真实的数据,好比 real_data_len=s.recv(header['file_size']) s.recv(real_data_len)
使用struct模块实现方法以下:
server端
import socket,struct,json import subprocess phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break print('cmd: %s' %cmd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() print(err) if err: back_msg=err else: back_msg=res.stdout.read() headers={'data_size':len(back_msg)} head_json=json.dumps(headers) head_json_bytes=bytes(head_json,encoding='utf-8') conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度 conn.send(head_json_bytes) #再发报头 conn.sendall(back_msg) #在发真实的内容 conn.close()
client端
from socket import * import struct,json ip_port=('127.0.0.1',8080) client=socket(AF_INET,SOCK_STREAM) client.connect(ip_port) while True: cmd=input('>>: ') if not cmd:continue client.send(bytes(cmd,encoding='utf-8')) head=client.recv(4) #先收4个bytes,这里4个bytes里包含了报头的长度 head_json_len=struct.unpack('i',head)[0] #解出报头的长度 head_json=json.loads(client.recv(head_json_len).decode('utf-8')) #拿到报头 data_len=head_json['data_size'] #取出报头内包含的信息 #开始收数据 recv_size=0 recv_data=b'' while recv_size < data_len: recv_data+=client.recv(1024) recv_size=len(recv_data) print(recv_data.decode('utf-8')) #print(recv_data.decode('gbk')) #windows默认gbk编码