一、簡單ssh功能
1.1 實現功能
在前面的一篇博客中,我們已經實現了一個簡單的類似Linux服務器ssh功能的小程序,可以輸入系統命令來返回命令運行結果,今天我們也以此開始,看看socket如何來接受大量數據。
服務端:

# -*- coding: UTF-8 -*- import os import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP/IP協議, tcp ,如果不填寫就是默認這個 server.bind(('localhost', 9999)) server.listen() while True: # 可以接受多個客戶端 conn, addr = server.accept() while True: data = conn.recv(1024) if not data: # 防止當接受的客戶端數據為空時,程序卡掉 print('client has lost...') break print('執行命令:', data.decode()) cmd_res = os.popen(data.decode()).read() if len(cmd_res) == 0: print('command not found') else: # 發送數據 conn.send('{}'.format(cmd_res).encode('utf-8')) print('發送完成')
客戶端:

# -*- coding: UTF-8 -*- import socket client = socket.socket() client.connect(('localhost', 9999)) while True: cmd = input('>>:').strip() # 判斷是否發送空數據,如果是就重新發送 if len(cmd) == 0: continue else: client.send(cmd.encode('utf-8')) receive_data = client.recv(1024) # 接受的數據是bytes類型 print(receive_data.decode('utf-8', 'ignore')) # 不加ignore在windows有時會報錯
運行結果:
我們運行兩個命令都是正常的,看起來不錯,能實現命令的輸入和結果的輸出
1.2 出現的問題
我們接着往下看,會出現什么問題?既然是接收大量數據,那就返回的數據量大一些
看上去也沒有問題,但是我們接下來繼續執行命令
顯然出錯了,命令dir返回的不是目錄里面的信息,而是上一次ipconfig/all的部分信息,why?
二、Socket 緩沖區
2.1 什么是socket緩沖區
每個 socket 被創建后,都會分配兩個緩沖區,輸入緩沖區和輸出緩沖區。
write()/send() 並不立即向網絡中傳輸數據,而是先將數據寫入緩沖區中,再由TCP協議將數據從緩沖區發送到目標機器。一旦將數據寫入到緩沖區,函數就可以成功返回,不管它們有沒有到達目標機器,也不管它們何時被發送到網絡,這些都是TCP協議負責的事情。
TCP協議獨立於 write()/send() 函數,數據有可能剛被寫入緩沖區就發送到網絡,也可能在緩沖區中不斷積壓,多次寫入的數據被一次性發送到網絡,這取決於當時的網絡情況、當前線程是否空閑等諸多因素,不由程序員控制。
2.2 緩沖區收發數據
對於TCP套接字(默認情況下),當使用 write()/send() 發送數據時:
1) 首先會檢查緩沖區,如果緩沖區的可用空間長度小於要發送的數據,那么 write()/send() 會被阻塞(暫停執行),直到緩沖區中的數據被發送到目標機器,騰出足夠的空間,才喚醒 write()/send() 函數繼續寫入數據。
2) 如果TCP協議正在向網絡發送數據,那么輸出緩沖區會被鎖定,不允許寫入,write()/send() 也會被阻塞,直到數據發送完畢緩沖區解鎖,write()/send() 才會被喚醒。
3) 如果要寫入的數據大於緩沖區的最大長度,那么將分批寫入。
4) 直到所有數據被寫入緩沖區 write()/send() 才能返回。
當使用 read()/recv() 讀取數據時:
1) 首先會檢查緩沖區,如果緩沖區中有數據,那么就讀取,否則函數會被阻塞,直到網絡上有數據到來。
2) 如果要讀取的數據長度小於緩沖區中的數據長度,那么就不能一次性將緩沖區中的所有數據讀出,剩余數據將不斷積壓,直到有 read()/recv() 函數再次讀取。
3) 直到讀取到數據后 read()/recv() 函數才會返回,否則就一直被阻塞。
這就是TCP套接字的阻塞模式。所謂阻塞,就是上一步動作沒有完成,下一步動作將暫停,直到上一步動作完成后才能繼續,以保持同步性。
三、如何接受大量數據
3.1 解決思路
因為緩沖區的存在,我們在傳輸大量數據時不能一下子全部傳輸完畢!事實上和接受和發送數據量,即send(1024)/recv(1024)關系不大。並不是我們將這兩個值設置的很大和可以解決問題了。因為socket每次接收和發送都有最大數據量限制的,畢竟網絡帶寬也是有限的呀,不能一次發太多,發送的數據最大量的限制 就是緩沖區能緩存的數據的最大量,這個緩沖區的最大值在不同的系統上是不一樣的,不過官方的建議是不超過8k,也就是8192。
那么我們就只能從另一個角度來思考了,也就是說我要來判斷一下,一個命令執行后,它返回的數據到底有沒有完全傳輸完畢,如果沒有,那么就繼續傳輸,直到傳完為止。
3.2 解決方法
簡單的方法就是對比接收和傳輸數據量的大小,如果接收的數據量等於發送的數據量,不就是傳完了么?
so,代碼如下:
服務端:

# -*- coding: UTF-8 -*- import os import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP/IP協議, tcp ,如果不填寫就是默認這個 server.bind(('localhost', 9999)) server.listen() while True: # 可以接受多個客戶端 conn, addr = server.accept() while True: data = conn.recv(1024) if not data: # 防止當接受的客戶端數據為空時,程序卡掉 print('client has lost...') break print('執行命令:', data.decode()) cmd_res = os.popen(data.decode()).read() if len(cmd_res) == 0: print('command not found') else: # 服務端先將數據大小發送給客戶端,用於對比 cmd_res_length = len(cmd_res) # int類型 # print(len('{}'.format(cmd_res).encode('utf-8'))) # 不先對數據編碼的話,會出現中文長度統計不符。用下面注釋的方法傳輸的數據,長度比實際短 conn.send(str(len('{}'.format(cmd_res).encode('utf-8'))).encode('utf-8')) # conn.send(str(cmd_res_length).encode('utf-8')) # 發送數據 conn.send('{}'.format(cmd_res).encode('utf-8')) print('發送完成')
客戶端:

# -*- coding: UTF-8 -*- import socket client = socket.socket() client.connect(('localhost', 9999)) while True: cmd = input('>>:').strip() # 判斷是否發送空數據,如果是就重新發送 if len(cmd) == 0: continue else: client.send(cmd.encode('utf-8')) data_size = client.recv(1024) # 接收服務端發送的數據大小 print(data_size) data_length = int(data_size.decode()) print('返回數據大小:', data_length) # 定義已接收數據大小為0 received_length = 0 # 定義已接收數據為0 received_data = b'' while received_length < data_length: r_data = client.recv(1024) # 接受的數據是bytes類型 received_length += len(r_data) received_data += r_data else: print('接收數據大小:', received_length) print(received_data.decode('utf-8', 'ignore')) # 不加ignore在windows有時會報錯 print('數據接收完畢!')
結果:
Look,問題解決了
中間很長不放了
四、剩余一些問題
4.1 數據長度不一致的問題
剛才提到當字符串有中文時直接用len()函數,可能會得到不一樣的長度,如下例:
# ipconfig/all 中的一段內容 data = '以太網適配器 VMware Network Adapter VMnet1:' # 不轉換成utf-8格式計算長度 length = len(data) print(length) # 先轉碼在計算長度 utf_length = len(data.encode('utf-8')) print(utf_length) # 輸出 37 49
注:長度會比不編碼時長 中文字符個數 * 2,