python TCP Socket的粘包和分包的處理詳解
概述
在進(jìn)行TCP Socket開發(fā)時,都需要處理數(shù)據(jù)包粘包和分包的情況。本文詳細(xì)講解解決該問題的步驟。使用的語言是Python。實際上解決該問題很簡單,在應(yīng)用層下,定義一個協(xié)議:消息頭部+消息長度+消息正文即可。
那什么是粘包和分包呢?
關(guān)于分包和粘包
粘包:發(fā)送方發(fā)送兩個字符串”hello”+”world”,接收方卻一次性接收到了”helloworld”。
分包:發(fā)送方發(fā)送字符串”helloworld”,接收方卻接收到了兩個字符串”hello”和”world”。
雖然socket環(huán)境有以上問題,但是TCP傳輸數(shù)據(jù)能保證幾點:
- 順序不變。例如發(fā)送方發(fā)送hello,接收方也一定順序接收到hello,這個是TCP協(xié)議承諾的,因此這點成為我們解決分包、黏包問題的關(guān)鍵。
- 分割的包中間不會插入其他數(shù)據(jù)。
因此如果要使用socket通信,就一定要自己定義一份協(xié)議。目前最常用的協(xié)議標(biāo)準(zhǔn)是:消息頭部(包頭)+消息長度+消息正文
TCP為什么會分包
TCP是以段(Segment)為單位發(fā)送數(shù)據(jù)的,建立TCP鏈接后,有一個最大消息長度(MSS)。如果應(yīng)用層數(shù)據(jù)包超過MSS,就會把應(yīng)用層數(shù)據(jù)包拆分,分成兩個段來發(fā)送。這個時候接收端的應(yīng)用層就要拼接這兩個TCP包,才能正確處理數(shù)據(jù)。
相關(guān)的,路由器有一個MTU( 最大傳輸單元),一般是1500字節(jié),除去IP頭部20字節(jié),留給TCP的就只有MTU-20字節(jié)。所以一般TCP的MSS為MTU-20=1460字節(jié)。
當(dāng)應(yīng)用層數(shù)據(jù)超過1460字節(jié)時,TCP會分多個數(shù)據(jù)包來發(fā)送。
擴(kuò)展閱讀
TCP的RFC定義MSS的默認(rèn)值是536,這是因為 RFC 791里說了任何一個IP設(shè)備都得最少接收576尺寸的大?。▽嶋H上來說576是撥號的網(wǎng)絡(luò)的MTU,而576減去IP頭的20個字節(jié)就是536)。
TCP為什么會粘包
有時候,TCP為了提高網(wǎng)絡(luò)的利用率,會使用一個叫做Nagle的算法。該算法是指,發(fā)送端即使有要發(fā)送的數(shù)據(jù),如果很少的話,會延遲發(fā)送。如果應(yīng)用層給TCP傳送數(shù)據(jù)很快的話,就會把兩個應(yīng)用層數(shù)據(jù)包“粘”在一起,TCP最后只發(fā)一個TCP數(shù)據(jù)包給接收端。
開發(fā)環(huán)境
- Python版本:3.5.1
- 操作系統(tǒng):Windows 10 x64
消息頭部(包含消息長度)
消息頭部不一定只能是一個字節(jié)比如0xAA什么的,也可以包含協(xié)議版本號,指令等,當(dāng)然也可以把消息長度合并到消息頭部里,唯一的要求是包頭長度要固定的,包體則可變長。下面是我自定義的一個包頭:
| 版本號(ver) | 消息長度(bodySize) | 指令(cmd) |
|---|
版本號,消息長度,指令數(shù)據(jù)類型都是無符號32位整型變量,于是這個消息長度固定為4×3=12字節(jié)。在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'
關(guān)于用自定義結(jié)束符分割數(shù)據(jù)包
有的人會想用自定義的結(jié)束符分割每一個數(shù)據(jù)包,這樣傳輸數(shù)據(jù)包時就不需要指定長度甚至也不需要包頭了。但是如果這樣做,網(wǎng)絡(luò)傳輸性能損失非常大,因為每一讀取一個字節(jié)都要做一次if判斷是否是結(jié)束符。所以建議還是選擇消息頭部+消息長度+消息正文這種方式。
而且,使用自定義結(jié)束符的時候,如果消息正文中出現(xiàn)這個符號,就會把后面的數(shù)據(jù)截止,這個時候還需要處理符號轉(zhuǎn)義,類比于\r\n的反斜杠。所以非常不建議使用結(jié)束符分割數(shù)據(jù)包。
消息正文
消息正文的數(shù)據(jù)格式可以使用Json格式,這里一般是用來存放獨特信息的數(shù)據(jù)。在下面代碼中,我使用{"hello","world"}數(shù)據(jù)來測試。在Python使用json模塊來生成json數(shù)據(jù)
Python示例
下面使用Python代碼展示如何處理TCP Socket的粘包和分包。核心在于用一個FIFO隊列接收緩沖區(qū)dataBuffer和一個小while循環(huán)來判斷。
具體流程是這樣的:把從socket讀取出來的數(shù)據(jù)放到dataBuffer后面(入隊),然后進(jìn)入小循環(huán),如果dataBuffer內(nèi)容長度小于消息長度(bodySize),則跳出小循環(huán)繼續(xù)接收;大于消息長度,則從緩沖區(qū)讀取包頭并獲取包體的長度,再判斷整個緩沖區(qū)是否大于消息頭部+消息長度,如果小于則跳出小循環(huán)繼續(xù)接收,如果大于則讀取包體的內(nèi)容,然后處理數(shù)據(jù),最后再把這次的消息頭部和消息正文從dataBuffer刪掉(出隊)。
下面用Markdown畫了一個流程圖。

服務(wù)器端代碼
# 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個數(shù)據(jù)包" % 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:
# 把數(shù)據(jù)存入緩沖區(qū),類似于push數(shù)據(jù)
dataBuffer += data
while True:
if len(dataBuffer) < headerSize:
print("數(shù)據(jù)包(%s Byte)小于消息頭部長度,跳出小循環(huán)" % len(dataBuffer))
break
# 讀取包頭
# struct中:!代表Network order,3I代表3個unsigned int數(shù)據(jù)
headPack = struct.unpack('!3I', dataBuffer[:headerSize])
bodySize = headPack[1]
# 分包情況處理,跳出函數(shù)繼續(xù)接收數(shù)據(jù)
if len(dataBuffer) < headerSize+bodySize :
print("數(shù)據(jù)包(%s Byte)不完整(總共%s Byte),跳出小循環(huán)" % (len(dataBuffer), headerSize+bodySize))
break
# 讀取消息正文的內(nèi)容
body = dataBuffer[headerSize:headerSize+bodySize]
# 數(shù)據(jù)處理
dataHandle(headPack, body)
# 粘包情況的處理
dataBuffer = dataBuffer[headerSize+bodySize:] # 獲取下一個數(shù)據(jù)包,類似于把數(shù)據(jù)pop出
測試服務(wù)器端的客戶端代碼
下面附上測試粘包和分包的客戶端代碼
# 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)
# 正常數(shù)據(jù)包定義
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()
# 分包數(shù)據(jù)定義
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()
# 粘包數(shù)據(jù)定義
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()
# 正常數(shù)據(jù)包
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()
服務(wù)器端打印結(jié)果
下面是測試出來的打印結(jié)果,可見接收方已經(jīng)完美的處理粘包和分包問題了。
Connected by ('127.0.0.1', 23297)
第1個數(shù)據(jù)包
ver:1, bodySize:18, cmd:101
{"hello": "world"}
數(shù)據(jù)包(0 Byte)小于包頭長度,跳出小循環(huán)
數(shù)據(jù)包(14 Byte)不完整(總共31 Byte),跳出小循環(huán)
第2個數(shù)據(jù)包
ver:2, bodySize:19, cmd:102
{"hello": "world2"}
數(shù)據(jù)包(0 Byte)小于包頭長度,跳出小循環(huán)
第3個數(shù)據(jù)包
ver:3, bodySize:19, cmd:103
{"hello": "world3"}
第4個數(shù)據(jù)包
ver:4, bodySize:19, cmd:104
{"hello": "world4"}
在框架下處理粘包和分包
其實無論是使用阻塞還是異步socket開發(fā)框架,框架本身都會提供一個接收數(shù)據(jù)的方法提供給開發(fā)者,一般來說開發(fā)者都要覆寫這個方法。下面是在Twidted開發(fā)框架處理粘包和分包的示例,只上核心程序:
# 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數(shù)據(jù)
headPack = struct.unpack('!3I', self._data_buffer[:headerSize])
# 獲取消息正文長度
bodySize = headPack[1]
# 分包情況處理
if len(self._data_buffer) < headerSize+bodySize :
return
# 讀取消息正文的內(nèi)容
body = self._data_buffer[headerSize:headerSize+bodySize]
# 處理數(shù)據(jù)
self.dataHandle(headPack, body)
# 粘包情況的處理
self._data_buffer = self._data_buffer[headerSize+bodySize:]
總結(jié)
以上就是本文關(guān)于python TCP Socket的粘包和分包的處理詳解的全部內(nèi)容,希望對大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。感謝朋友們對本站的支持!
相關(guān)文章
Python 匹配任意字符(包括換行符)的正則表達(dá)式寫法
Python 正則表達(dá)式匹配任意字符(包括換行符)的寫法2009-10-10
利用Python腳本寫端口掃描器socket,python-nmap
這篇文章主要介紹了利用Python腳本寫端口掃描器socket,python-nmap,文章圍繞主題展開詳細(xì)介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-07-07
Pandas sample隨機(jī)抽樣的實現(xiàn)
隨機(jī)抽樣,是統(tǒng)計學(xué)中常用的一種方法,本文主要介紹了Pandas sample隨機(jī)抽樣的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-06-06
Jupyter Notebook 遠(yuǎn)程訪問配置詳解
這篇文章主要介紹了Jupyter Notebook 遠(yuǎn)程訪問配置詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
python使用openpyxl實現(xiàn)對excel表格相對路徑的超鏈接的創(chuàng)建方式
這篇文章主要介紹了python使用openpyxl實現(xiàn)對excel表格相對路徑的超鏈接的創(chuàng)建方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
opencv+playwright滑動驗證碼的實現(xiàn)
滑動驗證碼是常見的驗證碼之一,本文主要介紹了opencv+playwright滑動驗證碼的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-11-11

