詳解Swoole TCP流數(shù)據(jù)邊界問(wèn)題解決方案
1. 數(shù)據(jù)發(fā)送過(guò)程
首先由客戶端將數(shù)據(jù)發(fā)往緩沖區(qū) (服務(wù)端并不是直接收到的), 對(duì)于客戶端來(lái)說(shuō),這次的數(shù)據(jù)即是發(fā)送成功了, 對(duì)于服務(wù)端是否真正的收到他是不知道的, 然后再由服務(wù)端從緩沖區(qū)中讀取數(shù)據(jù)。圖解:

2. 什么是數(shù)據(jù)邊界
因?yàn)?TCP 是流式傳輸,對(duì)于服務(wù)端來(lái)說(shuō)并不知道此時(shí)在緩沖區(qū)內(nèi)的數(shù)據(jù)是一次請(qǐng)求還是兩次請(qǐng)求的,所以在服務(wù)端接收數(shù)據(jù)時(shí)需要根據(jù)指定字符或約定長(zhǎng)度來(lái)對(duì)數(shù)據(jù)進(jìn)行分包,這個(gè)分包的標(biāo)志即是數(shù)據(jù)邊界。否則可能會(huì)出現(xiàn)一次讀取兩條或多條數(shù)據(jù),造成讀取、解析數(shù)據(jù)出錯(cuò)。

2.1 代碼演示
可以用代碼實(shí)現(xiàn)一下,假設(shè)客戶端死循環(huán)往緩沖區(qū)不停輸入 “1”,即相當(dāng)于每次的報(bào)文內(nèi)容都是 1, 那么在服務(wù)端讀取時(shí)收到的數(shù)據(jù)就是隨機(jī)長(zhǎng)度的。
客戶端代碼:
$client = new Swoole\Client(SWOOLE_SOCK_TCP);
if ($client->connect('127.0.0.1', 9501, -1)) {
while(true) {
$client->send(1);
}
}
$client->close();
服務(wù)端代碼:
$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){
});
運(yùn)行結(jié)果

可以看到運(yùn)行結(jié)果,服務(wù)端獲取到的數(shù)據(jù)完全是隨機(jī)的,有長(zhǎng)有短,那么接下來(lái)我們說(shuō)下如何解決這個(gè)問(wèn)題。
3.EOF 解決方案
第一種解決方案類似于我們 http 請(qǐng)求頭的分隔符,在每次發(fā)送的數(shù)據(jù)包結(jié)尾處使用 \r\n (可以配置) 來(lái)結(jié)尾, 當(dāng)服務(wù)端從緩沖區(qū)中讀取數(shù)據(jù), 根據(jù)指定字符來(lái)分割數(shù)據(jù)包,EOF 有兩種配置方案:
3.1 open_eof_check
首先放出配置方式:
$server->set([
'open_eof_check' => true,
'package_eof' => "\r\n"
]);
這種配置方式會(huì)對(duì)客戶端發(fā)來(lái)的數(shù)據(jù)包進(jìn)行檢測(cè), 當(dāng)發(fā)現(xiàn)結(jié)尾是 \r\n 時(shí),才會(huì)投遞給 worker 進(jìn)程, 也就是我們的 onReceive 回調(diào),否則會(huì)一直拼接數(shù)據(jù)包,直到超出緩沖區(qū)或者超時(shí)才終止。 但此方法有一個(gè)問(wèn)題是可能會(huì)一次性收到多個(gè)數(shù)據(jù)包,因?yàn)樗菑臄?shù)據(jù)包的結(jié)尾處來(lái)進(jìn)行檢查的,在數(shù)據(jù)內(nèi)容中存在 \r\n 時(shí)程序并不會(huì)發(fā)現(xiàn),需要我們自己在應(yīng)用代碼中再次使用 \r\n 來(lái)拆分?jǐn)?shù)據(jù)包。
客戶端運(yù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();
服務(wù)端代碼
$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();
運(yùn)行結(jié)果

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

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

以上通信協(xié)議的設(shè)計(jì)中,包頭長(zhǎng)度為 4 個(gè)整型,16 字節(jié),length 長(zhǎng)度值在第 3 個(gè)整型處。因此 package_length_offset 設(shè)置為 8,0-3 字節(jié)為 type,4-7 字節(jié)為 uid,8-11 字節(jié)為 length,12-15 字節(jié)為 serid。
下面來(lái)說(shuō)一下代碼實(shí)現(xiàn):
客戶端代碼:
$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();
服務(wù)端代碼:
$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); // 源數(shù)據(jù)
$tmp = unpack("Ntype/Nuid/Nlength", $data);
$unpacking = unpack("Ntype/Nuid/Nlength/Nserid/a{$tmp['length']}body", $data);
var_dump($unpacking); // 解包后數(shù)據(jù)
$server->send($fd, " Server Receive Data: ". $unpacking['body']);
});
$serv->on('close', function($server){
});
$serv->start();
客戶端運(yùn)行結(jié)果

服務(wù)端運(yùn)行結(jié)果

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

以上就是詳解Swoole TCP流數(shù)據(jù)邊界問(wèn)題解決方案的詳細(xì)內(nèi)容,更多關(guān)于Swoole TCP流數(shù)據(jù)邊界問(wèn)題解決方案的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 詳解PHP Swoole與TCP三次握手
- Swoole擴(kuò)展的6種模式深入詳解
- php中Swoole的熱更新實(shí)現(xiàn)代碼實(shí)例
- swoole鎖的機(jī)制代碼實(shí)例講解
- windows系統(tǒng)php環(huán)境安裝swoole具體步驟
- linux系統(tǒng)虛擬主機(jī)開(kāi)啟支持Swoole Loader擴(kuò)展的方法
- Swoole源碼中如何查詢Websocket的連接問(wèn)題詳解
- 在Windows系統(tǒng)上安裝Cygwin搭建Swoole測(cè)試環(huán)境的圖文教程
- php使用goto實(shí)現(xiàn)自動(dòng)重啟swoole、reactphp、workerman服務(wù)的代碼
- Centos7安裝swoole擴(kuò)展操作示例
相關(guān)文章
如何在smarty中增加類似foreach的功能自動(dòng)加載數(shù)據(jù)
本篇文章是對(duì)在smarty中增加類似foreach的功能自動(dòng)加載數(shù)據(jù)進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06
php實(shí)現(xiàn)QQ空間獲取當(dāng)前用戶的用戶名并生成圖片
這篇文章主要介紹了php實(shí)現(xiàn)QQ空間獲取當(dāng)前用戶的用戶名并生成圖片的方法,涉及php針對(duì)QQ空間及接口調(diào)用的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07
PHP JSAPI調(diào)支付API實(shí)現(xiàn)微信支付功能詳解
本人最近做了微信支付開(kāi)發(fā),是第一次接觸,其中走了很多彎路,遇到的問(wèn)題也很多。為了讓和我一樣的新人不再遇到類似的問(wèn)題,我把我的開(kāi)發(fā)步驟和問(wèn)題寫(xiě)出來(lái)以供參考,這篇文章主要介紹了PHP JSAPI調(diào)支付API實(shí)現(xiàn)微信支付功能2022-11-11
php expects parameter 1 to be resource, array given 錯(cuò)誤
從名字可以看出來(lái)這是說(shuō)你傳的參數(shù)有問(wèn)題,也就是說(shuō),你在定義傳遞參數(shù)的時(shí)候 例如 mysql_query($query,$result),這兩個(gè)參數(shù),你只用了$query這個(gè)參數(shù),那么$result這個(gè)參數(shù),沒(méi)有使用,就會(huì)報(bào)這個(gè)錯(cuò)誤2011-03-03
自己寫(xiě)了一個(gè)php檢測(cè)文件編碼的函數(shù)
關(guān)于文件編碼的檢測(cè),百度一下一大把都是,但是確實(shí)沒(méi)有能用的,下面是自己寫(xiě)了一個(gè)檢測(cè)函數(shù),希望對(duì)大家有所幫助2014-04-04
用PHP讀取和編寫(xiě)XML DOM的實(shí)現(xiàn)代碼
有許多技術(shù)可用于用 PHP 讀取和編寫(xiě) XML。本文提供了三種方法讀取 XML:使用 DOM 庫(kù)、使用 SAX 解析器和使用正則表達(dá)式。還介紹了使用 DOM 和 PHP 文本模板編寫(xiě) XML。2011-02-02
php字符串函數(shù)學(xué)習(xí)之substr()
這篇文章主要介紹了php字符串函數(shù)學(xué)習(xí)之substr(),本文講解了它的定義和用法、參數(shù)描述、提示和注釋以及多個(gè)使用示例 ,需要的朋友可以參考下2015-03-03

