目錄
- 1. 數據發送過程
- 2. 什么是數據邊界
- 3.EOF 解決方案
- 3.1 open_eof_check
- 3.2 open_eof_split
- 3.3 open_eof_check 和 open_eof_split 差異
- 4. 固定包頭 + 包體解決方案
- 5. 總結
- 6. 擴展知識
1. 數據發送過程
首先由客戶端將數據發往緩沖區 (服務端并不是直接收到的), 對于客戶端來說,這次的數據即是發送成功了, 對于服務端是否真正的收到他是不知道的, 然后再由服務端從緩沖區中讀取數據。圖解:

2. 什么是數據邊界
因為 TCP 是流式傳輸,對于服務端來說并不知道此時在緩沖區內的數據是一次請求還是兩次請求的,所以在服務端接收數據時需要根據指定字符或約定長度來對數據進行分包,這個分包的標志即是數據邊界。否則可能會出現一次讀取兩條或多條數據,造成讀取、解析數據出錯。

2.1 代碼演示
可以用代碼實現一下,假設客戶端死循環往緩沖區不停輸入 “1”,即相當于每次的報文內容都是 1, 那么在服務端讀取時收到的數據就是隨機長度的。
客戶端代碼:
$client = new Swoole\Client(SWOOLE_SOCK_TCP);
if ($client->connect('127.0.0.1', 9501, -1)) {
while(true) {
$client->send(1);
}
}
$client->close();
服務端代碼:
$server = new Swoole\Server('127.0.0.1', 9501);
$server->on('connect', function($server, $fd){
echo "client : ".$fd." connect";
});
$server->on('receive', function($server, $fd, $from_id, $data){
echo "receive:". $data.PHP_EOL;
});
$server->on('close', function($server){
});
運行結果

可以看到運行結果,服務端獲取到的數據完全是隨機的,有長有短,那么接下來我們說下如何解決這個問題。
3.EOF 解決方案
第一種解決方案類似于我們 http 請求頭的分隔符,在每次發送的數據包結尾處使用 \r\n (可以配置) 來結尾, 當服務端從緩沖區中讀取數據, 根據指定字符來分割數據包,EOF 有兩種配置方案:
3.1 open_eof_check
首先放出配置方式:
$server->set([
'open_eof_check' => true,
'package_eof' => "\r\n"
]);
這種配置方式會對客戶端發來的數據包進行檢測, 當發現結尾是 \r\n 時,才會投遞給 worker 進程, 也就是我們的 onReceive 回調,否則會一直拼接數據包,直到超出緩沖區或者超時才終止。 但此方法有一個問題是可能會一次性收到多個數據包,因為他是從數據包的結尾處來進行檢查的,在數據內容中存在 \r\n 時程序并不會發現,需要我們自己在應用代碼中再次使用 \r\n 來拆分數據包。
客戶端運行代碼
$client = new Swoole\Client(SWOOLE_SOCK_TCP);
if ($client->connect('127.0.0.1', 9501, -1)) {
while(true) {
$send2 = "Hello World \r\n";
$client->send($send2);
}
}
$client->close();
服務端代碼
$server = new Swoole\Server('127.0.0.1', 9501);
$server->set([
'open_eof_check' => true,
'package_eof' => "\r\n"
]);
$server->on('connect', function($server, $fd){
echo "client : ".$fd." connect";
});
$server->on('receive', function($server, $fd, $from_id, $data){
echo "receive:". $data;
});
$server->on('close', function($server){
});
$server->start();
運行結果

3.2 open_eof_split
配置方式:
$server->set([
'open_eof_split' => true,
'package_eof' => "\r\n"
]);
這種配置方式,服務端會對客戶端發來的數據逐個字符進行檢查,遇到 \r\n 就發送給 worker 進程,可以有效實現分包,但缺點是性能比較差。
運行結果:可以看到每次接收到一個 Hello World(代碼我就不貼了, 只把服務端 set 配置改一下, 其他都一樣)

3.3 open_eof_check 和 open_eof_split 差異
open_eof_check 只檢查接收數據的末尾是否為 EOF,因此它的性能最好,幾乎沒有消耗
open_eof_check 無法解決多個數據包合并的問題,比如同時發送兩條帶有 EOF 的數據,底層可能會一次全部返回
open_eof_split 會從左到右對數據進行逐字節對比,查找數據中的 EOF 進行分包,性能較差。但是每次只會返回一個數據包
4. 固定包頭 + 包體解決方案
引用一段官方文檔的描述:
包長檢測提供了固定包頭 + 包體這種格式協議的解析。啟用后,可以保證 Worker 進程 onReceive 每次都會收到一個完整的數據包。
長度檢測協議,只需要計算一次長度,數據處理僅進行指針偏移,性能非常高,推薦使用。
可見官方是推薦使用這種方式的,就是配置比其他方案要復雜一些, 首先貼一下配置:
$server->set([
// 打開包長檢測特性
'package_length_check' => true,
// 包頭中某個字段作為包長度的值,底層支持了 10 種長度類型。可參考 pack() 方法
'package_length_type' => 'N',
// length 長度值在包頭的第幾個字節。
'package_length_offset' => 8,
// 從第幾個字節開始計算長度,一般有 2 種情況:
//length 的值包含了整個包(包頭 + 包體),package_body_offset 為 0
//包頭長度為 N 字節,length 的值不包含包頭,僅包含包體,package_body_offset 設置為 N
'package_body_offset' => 16,
// 設置最大數據包尺寸,單位為字節
'package_max_length' => 81920
]);
下面是一個數據包結構例子,可以很好的體現了字段含義。

以上通信協議的設計中,包頭長度為 4 個整型,16 字節,length 長度值在第 3 個整型處。因此 package_length_offset 設置為 8,0-3 字節為 type,4-7 字節為 uid,8-11 字節為 length,12-15 字節為 serid。
下面來說一下代碼實現:
客戶端代碼:
$client = new Swoole\Client(SWOOLE_SOCK_TCP);
$data = "123456789012345678901234567890";
$type = 0x30;
$uid = 0x123;
$length = strlen($data);
$serid = 0x15;
$head = pack("N4", $type, $uid, $length, $serid);
$body = pack("a{$length}", $data);
$message = $head.$body;
if ($client->connect('127.0.0.1', 9502, -1)) {
$client->send($message);
echo $client->recv();
}
$client->close();
服務端代碼:
$serv = new Swoole\Server('127.0.0.1', 9502);
$serv->set([
'open_length_check' => true,
'package_max_length' => 81920,
'package_length_type' => 'N',
'package_length_offset' => 8,
'package_body_offset' => 16,
]);
$serv->on('connect', function($server, $fd){
echo $fd. " Connect !".PHP_EOL;
});
$serv->on('receive', function($server, $fd, $from_id, $data){
var_dump($data); // 源數據
$tmp = unpack("Ntype/Nuid/Nlength", $data);
$unpacking = unpack("Ntype/Nuid/Nlength/Nserid/a{$tmp['length']}body", $data);
var_dump($unpacking); // 解包后數據
$server->send($fd, " Server Receive Data: ". $unpacking['body']);
});
$serv->on('close', function($server){
});
$serv->start();
客戶端運行結果

服務端運行結果

可以看到 客戶端成功的把發送的數據回顯, 服務端也打印出了接收到的所有數據, 其中有些字段在發送時是 16 進制的, 所以服務端在接收到之后需要進行進制轉換, 我這里沒有進行轉換, 所以顯示的數據是 10 進制的。
5. 總結
通過對比可以看出使用固定包頭 + 包體的方式是效率最高的一種, 因為他是按照固定長度去讀取的。期間專門去了解了 pack 函數的使用方法,但也不確定這么寫到底對不對,如果有其他了解的仁兄可以慷慨解答一下,網上相關資料有點少,官方文檔上也只給出了幾個字段的釋義。
6. 擴展知識
6.1 字節序
計算機硬件有兩種儲存數據的方式:大端字節序(big endian)和小端字節序(little endian)。
舉例來說,數值 0x2211 使用兩個字節儲存:高位字節是 0x22,低位字節是 0x11。
- 大端字節序:高位字節在前,低位字節在后,這是人類讀寫數值的方法。
- 小端字節序:低位字節在前,高位字節在后,即以 0x1122 形式儲存。
這個前和后指的是內存地址,計算機處理字節時是不知道高低字節之分的,它只知道按順序讀取字節,先讀第一個字節,再讀第二個字節。
例如: 0x1234567 的讀取順序:

以上就是詳解Swoole TCP流數據邊界問題解決方案的詳細內容,更多關于Swoole TCP流數據邊界問題解決方案的資料請關注腳本之家其它相關文章!
您可能感興趣的文章:- 詳解PHP Swoole與TCP三次握手
- Swoole擴展的6種模式深入詳解
- php中Swoole的熱更新實現代碼實例
- swoole鎖的機制代碼實例講解
- windows系統php環境安裝swoole具體步驟
- linux系統虛擬主機開啟支持Swoole Loader擴展的方法
- Swoole源碼中如何查詢Websocket的連接問題詳解
- 在Windows系統上安裝Cygwin搭建Swoole測試環境的圖文教程
- php使用goto實現自動重啟swoole、reactphp、workerman服務的代碼
- Centos7安裝swoole擴展操作示例