1. 程式人生 > >【Python】Tcp Socket處理粘包與分包問題

【Python】Tcp Socket處理粘包與分包問題

測試環境

  • win10
  • python3.6

粘包和分包

  • 粘包:傳送方傳送兩個字串”hello”+”world”,接收方卻一次性接收到了”helloworld”
  • 分包:傳送方傳送字串”helloworld”,接收方卻接收到了兩個字串”hello”和”world”

解決方案

  • 自定義傳輸協議:訊息頭 + 訊息體
  • 其中訊息頭定長,且包含訊息體的長度

具體操作

  • 關鍵:一個FIFO佇列作為資料緩衝區,用於接收資料和判斷
  • 流程:
    • 把從socket接收到的資料,push進佇列
    • 判斷資料的長度是否大於訊息頭(自定義長度)的長度,如果成立則繼續下一步,否則跳出迴圈繼續接收資料
    • 如果當前資料長度大於訊息頭,則讀取訊息頭裡訊息體的長度,判斷當前資料長度是否大於訊息頭+訊息體的長度
    • 如果當前資料大於訊息頭+訊息體的長度,則處理資料,然後pop出佇列

程式碼實現

  • server.py
import socket
import struct

HOST = ''
PORT = 1234

# FIFO訊息佇列
dataBuffer = bytes()

# 自定義訊息頭的長度
headerSize = 12

# 定義資料包的個數
sn = 0

# 正文資料處理
def dataHandle(headPack, body):
    global sn
    sn += 1
print(f"第{sn}個數據包") print(body.decode()) print("\n") if __name__ == '__main__': with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((HOST, PORT)) s.listen(1) conn, addr = s.accept() with conn: print('Connected by', addr) while
True: data = conn.recv(1024) if data: # 把資料存入緩衝區,類似於push資料 dataBuffer += data while True: if len(dataBuffer) < headerSize: print("資料包(%s Byte)小於訊息頭部長度,跳出小迴圈" % len(dataBuffer)) break # 讀取包頭 # struct中:!代表Network order,3I代表3個unsigned int資料 headPack = struct.unpack('!3I', dataBuffer[:headerSize]) bodySize = headPack[1] # 分包情況處理,跳出函式繼續接收資料 if len(dataBuffer) < headerSize+bodySize : print("資料包(%s Byte)不完整(總共%s Byte),跳出小迴圈" % (len(dataBuffer), headerSize+bodySize)) break # 讀取訊息正文的內容 body = dataBuffer[headerSize:headerSize+bodySize] # 資料處理 dataHandle(headPack, body) # 資料出列 dataBuffer = dataBuffer[headerSize+bodySize:] # 獲取下一個資料包,類似於把資料pop出
  • client.py
import socket
import time
import struct
import json

host = "localhost"
port = 1234

ADDR = (host, port)

if __name__ == '__main__':
    client = socket.socket()
    client.connect(ADDR)

    # 正常資料包定義
    ver = 1
    body = json.dumps(dict(hello="world"))
    print(body)
    cmd = 101
    header = [ver, body.__len__(), cmd]
    headPack = struct.pack("!3I", *header)
    sendData1 = headPack+body.encode()

    # 分包資料定義
    ver = 2
    body = json.dumps(dict(hello="world2"))
    print(body)
    cmd = 102
    header = [ver, body.__len__(), cmd]
    headPack = struct.pack("!3I", *header)
    sendData2_1 = headPack+body[:2].encode()
    sendData2_2 = body[2:].encode()

    # 粘包資料定義
    ver = 3
    body1 = json.dumps(dict(hello="world3"))
    print(body1)
    cmd = 103
    header = [ver, body1.__len__(), cmd]
    headPack1 = struct.pack("!3I", *header)

    ver = 4
    body2 = json.dumps(dict(hello="world4"))
    print(body2)
    cmd = 104
    header = [ver, body2.__len__(), cmd]
    headPack2 = struct.pack("!3I", *header)

    sendData3 = headPack1+body1.encode()+headPack2+body2.encode()


    # 正常資料包
    client.send(sendData1)
    time.sleep(3)

    # 分包測試
    client.send(sendData2_1)
    time.sleep(0.2)
    client.send(sendData2_2)
    time.sleep(3)

    # 粘包測試
    client.send(sendData3)
    time.sleep(3)
    client.close()

效果

Connected by ('127.0.0.1', 29771)
第1個數據包
ver:1, bodySize:18, cmd:101
{"hello": "world"}

資料包(0 Byte)小於訊息頭部長度,跳出小迴圈
資料包(14 Byte)不完整(總共31 Byte),跳出小迴圈
第2個數據包
ver:2, bodySize:19, cmd:102
{"hello": "world2"}

資料包(0 Byte)小於訊息頭部長度,跳出小迴圈
第3個數據包
ver:3, bodySize:19, cmd:103
{"hello": "world3"}

第4個數據包
ver:4, bodySize:19, cmd:104
{"hello": "world4"}