一,單線程 - shell交互
def chan_recv(chan): data = chan.recv(1024) # 收1024數據 sys.stdout.write(data.decode()) # 輸出 sys.stdout.flush() if __name__ == '__main__': ssh = paramiko.SSHClient() ssh.load_system_host_keys() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect('10.10.10.2', port=22, username='cisco', password='cisco', timeout=3) # 3秒超時 channel = ssh.invoke_shell() chan_recv(channel) # 開始前先收一下數據 while True: # 監聽輸入 d = input() if d == 'quit': # 如果輸入quit,就退出 break channel.send(d + '\n') chan_recv(channel) channel.close() ssh.close()
問題:接收數據時的不規則性,chan.recv(1024)每次只收1024數據:
1,如果發送方的數據大於1024,就導致一次就取不完,需要分多次取
2,粘包問題: 即使發送方數據小於1024,但是如果去緩存取數據的時候數據還沒到達,也會導致一次取不完;而且也可能會取到下一次命令的返回數據,即如果交互多次,此時輸入命令和拿到的結果無法一一對應,
以上代碼在執行時,獲取不到預期結果時,多敲幾個回車就會出結果
解決方法:
1,第二個粘包問題可以通過sleep粗暴解決
2,如果想把兩個問題同時解決,主要有三個方法:
a)發送實際數據前,server端先發數據大小,client端持續接收,並且最后一次不收1024,而收實際大小,但是像paramiko這種server端無法改造的不適用(老男孩python socket編程就是這種解決思路)
b)明確結尾標示符,即做回顯判斷,每輸入一條命令,都接收到“結尾標示符”為止,參考“python paramiko自動登錄網絡設備抓取配置信息”
c)雙線程,主線程做輸入,子線程持續不斷接收
二,雙線程 - shell交互
def chan_recv(chan): while True: data = chan.recv(1024) # data是收到的數據,每次收1024 if not data: # 客戶端輸入了斷開socket的命令(例如exit),會導致子線程循環結束 break sys.stdout.write(data.decode()) sys.stdout.flush() if __name__ == '__main__': ssh = paramiko.SSHClient() ssh.load_system_host_keys() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect('10.10.10.2', port=22, username='cisco', password='cisco', timeout=3) # 3秒超時 channel = ssh.invoke_shell() # 這里如果不設置daemon進程,即使主線程關閉退出,子線程也不會結束 writer = threading.Thread(target=chan_recv, args=(channel,), daemon=True) writer.start() while True: d = input() if d == 'quit': break channel.send(d + '\n') # writer.join() # 這里不用join了,線程已經設置為守護線程,主線程結束就會自動關閉守護線程了 channel.close() ssh.close()
備注:
1,可以看到相比單線程-shell交互,雙線程版的子線程可以通過while循環接收server,也就不用去關心粘包、一次收1024能不能收完這類問題了
2,server端可能會有交互要求輸入,例如server端可能進行了分屏,返回--More--,要求輸入空格后,才繼續顯示,此時需要先取消分屏
3,輸入一條命令,界面上會顯示兩遍,第一遍是自己客戶端輸入的,第二遍是server端的回顯
三,雙線程 - 執行預先定義的命令
本例是對“python paramiko自動登錄網絡設備抓取配置信息”的改進,無需事先確定回顯內容
import paramiko import threading class MyThread(threading.Thread): def __init__(self, func, args=()): super(MyThread, self).__init__() self.func = func self.args = args def run(self): self.result = self.func(*self.args) def get_result(self): try: return self.result except Exception: return None def chan_recv(chan): resp = '' while True: data = chan.recv(1024) if not data: break resp += data.decode() return resp def shell(commands, host, port, username, password, timeout=3): ssh = paramiko.SSHClient() ssh.load_system_host_keys() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(host, port=port, username=username, password=password, timeout=timeout) channel = ssh.invoke_shell() writer = MyThread(chan_recv, args=(channel,)) writer.start() for cmd in commands: channel.send(cmd) writer.join() channel.close() ssh.close() return writer.get_result() if __name__ == '__main__': cmds = ['enable\n', 'cisco\n', 'terminal length 0\n', 'show ip int br\n', 'show run\n', 'show version\n', 'show inventory\n', 'sh cdp nei\n', 'conf t\n', 'router ospf 110\n', 'network 10.10.10.2 0.0.0.0 area 0\n', 'end\n', 'exit\n'] res = shell(cmds, '10.10.10.2', '22', 'cisco', 'cisco') print(res)
執行時,有時會遇到EOFerror報錯,需要在接收數據的時候做下改造:
while True: try: data = channel.recv(1024) if not data: break resp += data.decode() except EOFError: pass
四,單線程 - 執行預先定義的命令
其實執行預先定義的命令,不存在交互,無需另起線程,可以用串行方式,先把命令發過去,再不停接收數據即可。
import paramiko import os def shell(commands, host, port, username, password, timeout=3): ssh = paramiko.SSHClient() ssh.load_system_host_keys() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(host, port=port, username=username, password=password, timeout=timeout) channel = ssh.invoke_shell() for cmd in commands: channel.send(cmd) resp = '' while True: try: data = channel.recv(1024) # 如果對方沒有發東西過來,就會一直阻塞在recv if not data: # 如果發過來是b'',退出 break resp += data.decode() except EOFError: # 如果發過來了其他終止符,導致EOF pass # 只能用pass,不能用break channel.close() ssh.close() return resp if __name__ == '__main__': cmds = ['enable\n', 'cisco\n', 'terminal length 0\n', 'show ip int br\n', 'show run\n', 'show version\n', 'show inventory\n', 'sh cdp nei\n', 'conf t\n', 'router ospf 110\n', 'network 10.10.10.2 0.0.0.0 area 0\n', 'end\n', 'exit\n'] res = shell(cmds, '10.10.10.2', '22', 'cisco', 'cisco') print(res)
總結:
結束的判斷主要有三種方法:(參考https://www.cnblogs.com/litaozijin/p/6624029.html)
1,服務器端預先發送數據長度,每次接收時判斷(老男孩python課件中socket編程也是這個方法)
2,結尾標示符
3,終止socket
收數據時的方法有兩種:
1,單線程
2,雙線程:主線程發送命令,子線程負責持續接收