> 環境說明: 系統:Ubuntu14.04 (安裝教程包括CentOS6.5)
> PHP版本:PHP-5.5.10
> swoole版本:1.7.8-alpha
## **1.為什么要提供自定義協議**
熟悉TCP通信的朋友都會知道,TCP是一個流式協議。客戶端向服務器發送的一段數據,可能并不會被服務器一次就完整的收到;客戶端向服務器發送的多段數據,可能服務器一次就收到了全部的數據。而實際應用中,我們希望在服務器端能一次接收**一段完整的**數據,不多也不少。傳統的TCP服務器中,往往需要由程序員維護一個緩存區,先將讀到的數據放進緩存區中,然后再通過預先設定好的協議內容,來區分一段完整數據的開頭、長度和結尾,并將一段完整的數據交給邏輯部分處理。這就是自定義協議的功能。
而在Swoole中,已經在底層實現了一個數據緩存區,并內置好了幾個常用的協議類型,直接在底層做好了數據的拆分,保證了在onReceive回調函數中,一定能收到一個(或數個)完整的數據段。數據緩存區的大小可以通過配置選項[package_max_length](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#19package_max_length)來控制。下面我就將講解如何使用這些內置協議。
## **2.EOF標記型協議**
第一個比較常用的協議就是EOF標記協議。協議的內容是通過規定一個一定不會出現在正常數據中的字符或者字符串,用這個來標記一段完整數據的結尾。這樣,只要發現這個結尾,就可以認定之前的數據已經結束,可以開始接收一個新的數據段了。
在Swoole中,可以通過[open_eof_check](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#13open_eof_check)和[package_eof](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#14package_eof)兩個配置項來開啟。其中,[open_eof_check](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#13open_eof_check)指定開啟了EOF檢測,[package_eof](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#14package_eof)指定了具體的EOF標記。通過這兩個選項,Swoole底層就會自動根據EOF標記來緩存和拆分收到的數據包。示例如下:
~~~
$this->serv->set(array(
'package_max_length' => 8192,
'open_eof_check'=> true,
'package_eof' => "\r\n"
));
~~~
就這樣,swoole就已經開啟了EOF標記協議的解析。那么讓我們來測試一下效果:
服務器這邊:
~~~
// Server
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
echo "Get Message From Client {$fd}:{$data}\n";
}
~~~
客戶端這邊:
~~~
$msg_eof = "This is a Msg\r\n";
$i = 0;
while( $i < 100 ) {
$this->client->send( $msg_eof );
$i ++;
}
~~~
然后運行一下,你會發現:哎不對啊,為什么還是一次收到了好多數據啊!
這是因為,在Swoole中,采用的不是遍歷識別的方法,而只是簡單的檢查每一次接收到的數據的末尾是不是定義好的EOF標記。因此,在開啟EOF檢測后,onReceive回調中還是可能會一次收到多個數據包。
這要怎么辦?你會發現,雖然是多個數據包,但是實際上收到的是N個完整的數據片段,那就只需要根據EOF把每個包再拆出來,一個個處理就好啦。
修改后的服務器端代碼如下:
~~~
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
$data_list = explode("\r\n", $data);
foreach ($data_list as $msg) {
if( !empty($msg) ) {
echo "Get Message From Client {$fd}:{$msg}\n";
}
}
}
~~~
再次運行,妥了~
[點此查看完整實例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05/swoole_eof_server.php)
另外,如果擔心這樣運行多段數據會長時間占用Worker,可以采用把數據+fd轉發給Task進程的做法。如何轉發請讀者自己嘗試實現。
## **3.固定包頭類型協議**
固定包頭協議是在實際應用中最常用的協議。該協議的內容是規定一個固定長度的包頭,在包頭的固定位置有一個指定好的字段存放了后續數據的實際長度。這樣,服務器端可以先讀取固定長度的數據,從中提取出長度,然后再讀取指定長度的數據,即可獲取一段完整的數據。
在Swoole中,同樣提供了固定包頭的協議格式。需要注意的是,Swoole只允許二進制形式的包頭,因此,需要使用pack、unpack來打包、解包。
通過設置[open_length_check](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#15open_length_check)選項,即可打開固定包頭協議解析功能。此外還有[package_length_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#16package_length_offset),[package_body_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#17package_body_offset)和[package_length_type](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#18package_length_type)三個配置項用于控制解析功能。[package_length_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#16package_length_offset)規定了包頭中第幾個字節開始是長度字段,[package_body_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#17package_body_offset)規定了包頭的長度,[package_length_type](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#18package_length_type)規定了長度字段的類型。
具體設置如下:
~~~
$this->serv->set(array(
'package_max_length' => 8192,
'open_length_check'=> true,
'package_length_offset' => 0,
'package_body_offset' => 4,
'package_length_type' => 'N'
));
~~~
具體如何設置這些參數請參考[文檔](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md)
OK,廢話不多講,直接上實例:
服務器端:
~~~
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
$length = unpack("N" , $data)[1];
echo "Length = {$length}\n";
$msg = substr($data,-$length);
echo "Get Message From Client {$fd}:{$msg}\n";
}
~~~
客戶端:
~~~
$msg_length = pack("N" , strlen($msg_normal) ). $msg_normal;
$i = 0;
while( $i < 100 ) {
$this->client->send( $msg_length );
$i ++;
}
~~~
直接運行,Perfect!
[點此查看完整實例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05/swoole_length_check_server.php)
[點此查看其他相關源碼](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05)
## **4.特別篇:Http協議-Swoole內置的http_server**
從Swoole-1.7.7-stable開始,Swoole在內部封裝并實現了一個Http服務器。是的,沒錯,再也不用在PHP層緩存和解析http協議了,Swoole直接內置Http服務器了。
創建一個swoole_http_server的代碼如下:
~~~
$http = new swoole_http_server("127.0.0.1", 9501);
$http->on('request', function (swoole_http_request $request, swoole_http_response $response) {
$response->end("<h1>Hello Swoole.</h1>");
});
$http->start();
~~~
只需創建一個swoole_http_server對象并設置onRequest回調函數,即可實現一個http服務器。
在onRequest回調中,共有兩個參數。參數`$request`存放了來自客戶端的請求,包括Http請求的頭部信息、Http請求相關的服務器信息、Http請求的GET和POST參數以及HTTP請求攜帶的COOKIE信息。參數`$response`用于發送數據給客戶端,可以通過該參數設置HTTP響應的Header信息、cookie信息和狀態碼。
此外,swoole_http_server還提供WebSocket功能,使用此功能需要設置onMessage回調函數,如下:
~~~
$http_server->on('message', function(swoole_http_request $request, swoole_http_response $response) {
echo $request->message;
$response->message(json_encode(array("data1", "data2")));
})
~~~
通過`$request->message`獲取WebSocket發送來的消息,再通過`$response->message()`回復消息即可。
[點此查看完整實例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05/swoole_http_server.php)
[點此查看swoole_http_server相關文檔](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/14.swoole_http_server.md)
(最后做個小廣告,經過嘗試,已經初步將php的Yaf框架移植到了swoole_http_server上,經過測試,swoole-yaf的性能遠遠超過了nginx+php-fpm+yaf的性能。
項目地址:[https://github.com/LinkedDestiny/swoole-yaf](https://github.com/LinkedDestiny/swoole-yaf)
我將繼續不斷完善這個項目,力爭能夠真正用于線上項目)