【Python】TCP Socket的粘包和分包的處理


Reference: http://blog.csdn.net/yannanxiu/article/details/52096465

 

 

概述

在進行TCP Socket開發時,都需要處理數據包粘包和分包的情況。本文詳細講解解決該問題的步驟。使用的語言是Python。實際上解決該問題很簡單,在應用層下,定義一個協議:消息頭部+消息長度+消息正文即可。

那什么是粘包和分包呢?

關於分包和粘包

粘包:發送方發送兩個字符串”hello”+”world”,接收方卻一次性接收到了”helloworld”。

分包:發送方發送字符串”helloworld”,接收方卻接收到了兩個字符串”hello”和”world”。

雖然socket環境有以上問題,但是TCP傳輸數據能保證幾點:

  • 順序不變。例如發送方發送hello,接收方也一定順序接收到hello,這個是TCP協議承諾的,因此這點成為我們解決分包、黏包問題的關鍵。
  • 分割的包中間不會插入其他數據。

因此如果要使用socket通信,就一定要自己定義一份協議。目前最常用的協議標准是:消息頭部(包頭)+消息長度+消息正文

TCP為什么會分包

TCP是以段(Segment)為單位發送數據的,建立TCP鏈接后,有一個最大消息長度(MSS)。如果應用層數據包超過MSS,就會把應用層數據包拆分,分成兩個段來發送。這個時候接收端的應用層就要拼接這兩個TCP包,才能正確處理數據。

相關的,路由器有一個MTU( 最大傳輸單元),一般是1500字節,除去IP頭部20字節,留給TCP的就只有MTU-20字節。所以一般TCP的MSS為MTU-20=1460字節。

當應用層數據超過1460字節時,TCP會分多個數據包來發送。

擴展閱讀 
TCP的RFC定義MSS的默認值是536,這是因為 RFC 791里說了任何一個IP設備都得最少接收576尺寸的大小(實際上來說576是撥號的網絡的MTU,而576減去IP頭的20個字節就是536)。

TCP為什么會粘包

有時候,TCP為了提高網絡的利用率,會使用一個叫做Nagle的算法。該算法是指,發送端即使有要發送的數據,如果很少的話,會延遲發送。如果應用層給TCP傳送數據很快的話,就會把兩個應用層數據包“粘”在一起,TCP最后只發一個TCP數據包給接收端。

開發環境

  • Python版本:3.5.1
  • 操作系統:Windows 10 x64

消息頭部(包含消息長度)

消息頭部不一定只能是一個字節比如0xAA什么的,也可以包含協議版本號,指令等,當然也可以把消息長度合並到消息頭部里,唯一的要求是包頭長度要固定的,包體則可變長。下面是我自定義的一個包頭

版本號(ver) 消息長度(bodySize) 指令(cmd)

版本號,消息長度,指令數據類型都是無符號32位整型變量,於是這個消息長度固定為4×3=12字節。在Python由於沒有類型定義,所以一般是使用struct模塊生成包頭。示例:

import struct
import json

ver = 1
body = json.dumps(dict(hello="world"))
print(body) # {"hello": "world"} cmd = 101 header = [ver, body.__len__(), cmd] headPack = struct.pack("!3I", *header) print(headPack) # b'\x00\x00\x00\x01\x00\x00\x00\x12\x00\x00\x00e'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

關於用自定義結束符分割數據包

有的人會想用自定義的結束符分割每一個數據包,這樣傳輸數據包時就不需要指定長度甚至也不需要包頭了。但是如果這樣做,網絡傳輸性能損失非常大,因為每一讀取一個字節都要做一次if判斷是否是結束符。所以建議還是選擇消息頭部+消息長度+消息正文這種方式。

而且,使用自定義結束符的時候,如果消息正文中出現這個符號,就會把后面的數據截止,這個時候還需要處理符號轉義,類比於\r\n的反斜杠。所以非常不建議使用結束符分割數據包。

消息正文

消息正文的數據格式可以使用Json格式,這里一般是用來存放獨特信息的數據。在下面代碼中,我使用{"hello","world"}數據來測試。在Python使用json模塊來生成json數據

Python示例

下面使用Python代碼展示如何處理TCP Socket的粘包和分包。核心在於用一個FIFO隊列接收緩沖區dataBuffer和一個小while循環來判斷。

具體流程是這樣的:把從socket讀取出來的數據放到dataBuffer后面(入隊),然后進入小循環,如果dataBuffer內容長度小於消息長度(bodySize),則跳出小循環繼續接收;大於消息長度,則從緩沖區讀取包頭並獲取包體的長度,再判斷整個緩沖區是否大於消息頭部+消息長度,如果小於則跳出小循環繼續接收,如果大於則讀取包體的內容,然后處理數據,最后再把這次的消息頭部和消息正文從dataBuffer刪掉(出隊)。

下面用Markdown畫了一個流程圖。

開始等待數據到達把數據push緩沖區緩沖區小於消息長度?讀取消息頭部的內容緩沖區小於消息頭部和消息正文長度?讀取消息正文的內容處理數據從緩沖區pop數據yesnoyesno

服務器端代碼

# Python Version:3.5.1 import socket import struct HOST = '' PORT = 1234 dataBuffer = bytes() headerSize = 12 sn = 0 def dataHandle(headPack, body): global sn sn += 1 print("第%s個數據包" % sn) print("ver:%s, bodySize:%s, cmd:%s" % headPack) print(body.decode()) print("") 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出
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

測試服務器端的客戶端代碼

下面附上測試粘包和分包的客戶端代碼:

# Python Version:3.5.1 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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

服務器端打印結果

下面是測試出來的打印結果,可見接收方已經完美的處理粘包和分包問題了。

Connected by ('127.0.0.1', 23297) 第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"} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在框架下處理粘包和分包

其實無論是使用阻塞還是異步socket開發框架,框架本身都會提供一個接收數據的方法提供給開發者,一般來說開發者都要覆寫這個方法。下面是在Twidted開發框架處理粘包和分包的示例,只上核心程序:

# Twiested class MyProtocol(Protocol): _data_buffer = bytes() # 代碼省略 def dataReceived(self, data): """Called whenever data is received.""" self._data_buffer += data headerSize = 12 while True: if len(self._data_buffer) < headerSize: return # 讀取消息頭部 # struct中:!代表Network order,3I代表3個unsigned int數據 headPack = struct.unpack('!3I', self._data_buffer[:headerSize]) # 獲取消息正文長度 bodySize = headPack[1] # 分包情況處理 if len(self._data_buffer) < headerSize+bodySize : return # 讀取消息正文的內容 body = self._data_buffer[headerSize:headerSize+bodySize] # 處理數據 self.dataHandle(headPack, body) # 粘包情況的處理 self._data_buffer = self._data_buffer[headerSize+bodySize:]


免責聲明!

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



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