網絡編程之粘包

粘包問題算法

  上一篇博客遺留了一個問題,在接收的最大字節數設置爲 1024 時,當接收的結果大於1024,再執行下一條命令時仍是會返回上一條命令未執行完成的結果。這就是粘包問題。shell

  由於TCP協議又叫流式協議,每次發送給客戶端的數據其實是發送到客戶端所在操做系統的緩存上,客戶端就是一個應用程序,須要經過操做系統才能調到緩存內的數據,而緩存的空間是一個隊列,有 「先進先出」 的思想,當第一次的 tasklist 數據未接收完,第二次又來一個 dir 的數據時,只能等第一次先所有接收完成纔會接收後面的。json

  有一個解決方法是每次在接收數據時,都將數據的完整結果所有接收,這樣就不會出現粘包現象。那該怎麼樣才能所有接收呢?有人說將接收的最大字節數設置大點不就能接收 tasklist 的所有執行結果了嗎?這樣作確實能夠,但若是是文件的上傳下載呢?客戶端執行下載命令,服務端將下載的結果發送給客戶端,客戶端再接收,文件的大小是超過 GB、TB 的,那最大字節數該設置多大?其實設置再大也沒有意義,由於客戶端接收數據是經過本身操做系統的緩存空間接收的,緩存空間的大小不可能比本身計算機的物理內存還大,就算和物理內存同樣大,假設物理內存是 8G,那你也只能一次收到 8GB 的數據,當發送的數據超過 8G 呢?緩存

  TCP協議爲了優化傳輸效率,而致使了粘包問題。客戶端和服務端之間是基於網絡收發數據,網絡的 I/O 是越少越好,TCP有一種 Nagle 算法,是將屢次時間間隔較短且數據量小的數據,合併成一個大的數據塊,而後進行封包,這樣,儘量多的下降 I/O,從而提高程序的運行效率。可是接收端很難分辨出來,這就致使了粘包問題。網絡

總結粘包問題:ssh

粘包不必定會發生socket

若是發生了:1)多是在客戶端已經粘了ide

      2)客戶端沒有粘,多是在服務端粘了函數

 客戶端粘包:發送數據時間間隔很短,數據量很小,TCP優化算法會當作一個包發出去,產生粘包優化

from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) conn, client_addr = server.accept() data1 = conn.recv(1024) print("第一次收: ", data1) data2 = conn.recv(1024) print("第二次收: ",data2) data3 = conn.recv(1024) print("第三次收: ",data3)
服務端
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # TCP協議會將數據量較小且時間間隔較短的數據合併成一個數據報發送
client.send(b'hello') client.send(b'world') client.send(b'qiu')
客戶端
第一次收:  b'helloworldqiu' 第二次收: b'' 第三次收: b''
運行結果

服務端粘包:客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包

from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) conn, client_addr = server.accept() data1 = conn.recv(1) print("第一次收: ", data1) data2 = conn.recv(2) print("第二次收: ",data2) data3 = conn.recv(1024) print("第三次收: ",data3)
服務端
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) client.send(b'hello') client.send(b'world') client.send(b'qiu')
客戶端
第一次收:  b'h' 第二次收: b'el' 第三次收: b'loworldqiu'
運行結果

粘包問題的解決思路

  問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是發送端在發送數據前,發一個頭文件包,裏面包含每次要發送數據的長度,構成一個總長度,而後接收端用循環接收完全部數據,可是長度是整型,發送的數據是字節,因此要將整型轉成字節類型再發送。

struct 模塊

使用 struct 模塊能夠用於將 Python 的 int 類型轉換爲 bytes 類型

struct 模塊中最重要的三個函數是pack(), unpack(), calcsize()

pack(fmt, v1, v2, ...):按照給定的格式 (fmt),把數據封裝成字符串(其實是相似於 C 語言中結構體的字節流)

unpack(fmt, string):按照給定的格式 (fmt) 解析字節流 string,返回解析出來的 tuple

calcsize(fmt):計算給定的格式 (fmt) 佔用多少字節的內存

struct 中支持的格式以下表

import struct obj = struct.pack('i', 1231) print(obj) print(len(obj))     # C語言中int類型佔4個字節
 res = struct.unpack("i", obj) print(res) print(res[0]) # 執行結果
b'\xcf\x04\x00\x00'
4 (1231,) 1231
struct模塊

 模擬ssh實現遠程執行命令(解決粘包問題簡單版)

from socket import *
import subprocess import struct server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) # 鏈接循環
while True: conn, client_addr = server.accept() # 通訊循環
    while True: try: cmd = conn.recv(1024)  # cmd = b'dir'
            # 針對Linux系統
            if len(cmd) == 0: break
            # 命令的執行結果
            obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() # 1. 先製做固定長度的報頭
            header = struct.pack("i", len(stdout) + len(stderr)) # 2. 再發送報頭
 conn.send(header) # 3. 最後發送真實的數據
 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
服務端
from socket import *
import struct client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通訊循環
while True: cmd = input("請輸入: ").strip() if len(cmd) == "0": continue client.send(cmd.encode("utf-8")) # 1. 先收報頭, 從報頭裏解出數據的長度
    header = client.recv(4) total_size = struct.unpack("i", header)[0] # 2. 接收真正的數據
    cmd_res = b""
    # 接收數據的長度初始值爲0
    recv_size = 0 # 當接收的數據長度小於報頭長度
    while recv_size < total_size: data = client.recv(1024) recv_size += len(data) cmd_res += data print(cmd_res.decode("gbk")) client.close()
客戶端

這樣寫有一個限制,我在 struct 模塊中設置的是 i 格式,只能用於傳輸較小的字節數,且此時報頭裏只包含數據長度信息,若是是上傳下載文件,還可能包含文件名、文件大小、文件的 md5 值等其它信息,那這種方法就不適用了

能夠考慮將報頭設置成一個字典,包含相關的信息,而後將字典序列化成 JSON 格式發送,在接收方反序列化還能獲得字典格式,且能夠設置字典裏的文件大小很大,但 JSON 的長度卻很小

import json header_dic = { "filename": "a.txt", "md5": "DASHJH423465CSA", "total_size":456165446511564651351456413514543543 } header_json = json.dumps(header_dic) print(len(header_json)) # 運行
99
報頭字典序列化成JSON的長度

模擬ssh實現遠程執行命令(解決粘包問題終極版)

from socket import *
import subprocess import struct import json server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) # 鏈接循環
while True: conn, client_addr = server.accept() # 通訊循環
    while True: try: cmd = conn.recv(1024)  # cmd = b'dir'
            # 針對Linux系統
            if len(cmd) == 0: break
            # 命令的執行結果
            obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() # 1. 先製做報頭
            header_dic = { "filename": "a.txt", "md5": "DASHJH423465CSA", "total_size": len(stdout) + len(stderr) } # 將報頭序列化成json格式的字符串
            header_json = json.dumps(header_dic) # json格式的字符串轉成bytes類型
            header_bytes = header_json.encode("utf-8") # 2. 先發送4個bytes(包含報頭的長度)
            conn.send(struct.pack("i", len(header_bytes))) # 3. 發送報頭
 conn.send(header_bytes) # 4. 最後發送真實的數據
 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
服務端
from socket import *
import struct import json client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通訊循環
while True: cmd = input("請輸入: ").strip() if len(cmd) == "0": continue client.send(cmd.encode("utf-8")) # 1. 先收4個bytes, 解出報頭長度
    header_size = struct.unpack("i", client.recv(4))[0] # 2. 再接收報頭, 拿到head_dic
    header_bytes = client.recv(header_size) header_json = header_bytes.decode("utf-8") head_dic = json.loads(header_json) print(head_dic) total_size = head_dic["total_size"] # 3. 接收真正的數據
    cmd_res = b""
    # 接收數據的長度初始值爲0
    recv_size = 0 # 當接收的數據長度小於報頭長度
    while recv_size < total_size: data = client.recv(1024) recv_size += len(data) cmd_res += data print(cmd_res.decode("gbk")) client.close()
客戶端
相關文章
相關標籤/搜索