socket--接受大數據


一、簡單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('發送完成')
View Code

  客戶端:

# -*- 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有時會報錯
View Code

  運行結果:

  

  我們運行兩個命令都是正常的,看起來不錯,能實現命令的輸入和結果的輸出

  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('發送完成')
View Code

  客戶端:

# -*- 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('數據接收完畢!')
View Code

  結果:

  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,

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM