swoole算是nodejs在php中的一種實現,異步響應請求,性能超強
1 安裝准備
1.1 安裝swoole前必須保證系統已經安裝了下列軟件
php-5.3.10 或更高版本
gcc-4.4 或更高版本
make
autoconf
pcre (centos系統可以執行命令:yum install pcre-devel)
1.2 下載並解壓
下載地址 https://github.com/swoole/swoole-src/releases
進入頁面后選擇download鏈接下的tar.gz的壓縮包
下載源代碼包后,解壓
tar xzvf xxx.tar.gz
在終端進入源碼目錄,執行下面的命令進行編譯和安裝
cd swoole
phpize
./configure --enable-swoole-debug
make
sudo make install
編譯參數根據自己的需求選擇,詳情參看官方文檔。
1.3 編譯安裝成功后,修改php.ini
在php.ini中加入 extension=swoole.so
通過在命令行使用 php-m查看,是否安裝了swoole
注意:如通重新編譯的話需要 make clean
2 構建Swoole基本實例
2.1 tcp服務器實例
(來自w3cschool教程https://www.w3cschool.cn/swoole/bnte1qcd.html)
服務端代碼:Server.php
<?php
// Server
class Server
{
private $serv;
public function __construct() {
$this->serv = new swoole_server("0.0.0.0", 9501);
$this->serv->set(array(
'worker_num' => 8,
'daemonize' => false,
'max_request' => 10000,
'dispatch_mode' => 2,
'debug_mode'=> 1
));
$this->serv->on('Start', array($this, 'onStart'));
$this->serv->on('Connect', array($this, 'onConnect'));
$this->serv->on('Receive', array($this, 'onReceive'));
$this->serv->on('Close', array($this, 'onClose'));
$this->serv->start();
}
public function onStart( $serv ) {
echo "Start\n";
}
public function onConnect( $serv, $fd, $from_id ) {
$serv->send( $fd, "Hello {$fd}!" );
}
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
echo "Get Message From Client {$fd}:{$data}\n";
}
public function onClose( $serv, $fd, $from_id ) {
echo "Client {$fd} close connection\n";
}
}
// 啟動服務器
$server = new Server();
從代碼中可以看出,創建一個swoole_server基本分三步:
- 通過構造函數創建swoole_server對象
- 調用set函數設置swoole_server的相關配置選項
- 調用on函數設置相關回調函數 關於set配置選項以及on回調函數的具體說明,請參考我整理的swoole文檔( 配置選項)
這里只給出簡單介紹。onStart回調在server運行前被調用,onConnect在有新客戶端連接過來時被調用,onReceive函數在有數據發送到server時被調用,onClose在有客戶端斷開連接時被調用。 這里就可以大概看出如何使用swoole:在onConnect處監聽新的連接;在onReceive處接收數據並處理,然后可以調用send函數將處理結果發送出去;在onClose處處理客戶端下線的事件。
客戶端的代碼:Client.php
<?php
class Client
{
private $client;
public function __construct() {
$this->client = new swoole_client(SWOOLE_SOCK_TCP);
}
public function connect() {
if( !$this->client->connect("127.0.0.1", 9501 , 1) ) {
echo "Error: {$fp->errMsg}[{$fp->errCode}]\n";
}
$message = $this->client->recv();
echo "Get Message From Server:{$message}\n";
fwrite(STDOUT, "請輸入消息:");
$msg = trim(fgets(STDIN));
$this->client->send( $msg );
}
}
$client = new Client();
$client->connect();
這里,通過swoole_client創建一個基於TCP的客戶端實例,並調用connect函數向指定的IP及端口發起連接請求。隨后即可通過recv()和send()兩個函數來接收和發送請求。需要注意的是,這里我使用了默認的同步阻塞客戶端,因此recv和send操作都會產生網絡阻塞。
使用方法
進入到文件目錄,在窗口1先啟動php Serve.php,然后再開一個窗口(窗口2)啟動php Client.php
窗口1內容:
# root @ WENGINE in /data/learnSwoole [9:24:57] C:130
$ php Server.php
Start
Get Message From Client 1:ceshi1
Client 1 close connection
窗口2內容:
# root @ WENGINE in /data/learnSwoole [9:23:07]
$ php Client.php
Get Message From Server:Hello 1!
請輸入消息:ceshi1
2.2 web服務器
服務端代碼 http_server.php
$http = new swoole_http_server("0.0.0.0", 9501);
$http->on('request', function ($request, $response) {
var_dump($request->get, $request->post);
$response->header("Content-Type", "text/html; charset=utf-8");
$response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");
});
$http->start();
Http服務器只需要關注請求響應即可,所以只需要監聽一個onRequest事件。當有新的Http請求進入就會觸發此事件。事件回調函數有2個參數,一個是$request對象,包含了請求的相關信息,如GET/POST請求的數據。
另外一個是response對象,對request的響應可以通過操作response對象來完成。$response->end()方法表示輸出一段HTML內容,並結束此請求。
● 0.0.0.0 表示監聽所有IP地址,一台服務器可能同時有多個IP,如127.0.0.1本地回環IP、192.168.1.100局域網IP、210.127.20.2 外網IP,這里也可以單獨指定監聽一個IP
● 9501 監聽的端口,如果被占用程序會拋出致命錯誤,中斷執行。
2.3 WebSocket服務器
服務端程序代碼 ws_server.php
//創建websocket服務器對象,監聽0.0.0.0:9502端口
$ws = new swoole_websocket_server("0.0.0.0", 9502);
//監聽WebSocket連接打開事件
$ws->on('open', function ($ws, $request) {
var_dump($request->fd, $request->get, $request->server);
$ws->push($request->fd, "hello, welcome\n");
});
//監聽WebSocket消息事件
$ws->on('message', function ($ws, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
});
//監聽WebSocket連接關閉事件
$ws->on('close', function ($ws, $fd) {
echo "client-{$fd} is closed\n";
});
$ws->start();
WebSocket服務器是建立在Http服務器之上的長連接服務器,客戶端首先會發送一個Http的請求與服務器進行握手。握手成功后會觸發onOpen事件,表示連接已就緒,onOpen函數中可以得到$request對象,包含了Http握手的相關信息,如GET參數、Cookie、Http頭信息等。
建立連接后客戶端與服務器端就可以雙向通信了。
● 客戶端向服務器端發送信息時,服務器端觸發onMessage事件回調
● 服務器端可以調用$server->push()向某個客戶端(使用$fd標識符)發送消息
● 服務器端可以設置onHandShake事件回調來手工處理WebSocket握手
運行程序
客戶端的代碼
可以使用Chrome瀏覽器進行測試,JS代碼為:
var wsServer = 'ws://127.0.0.1:9502';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
console.log("Connected to WebSocket server.");
};
websocket.onclose = function (evt) {
console.log("Disconnected");
};
websocket.onmessage = function (evt) {
console.log('Retrieved data from server: ' + evt.data);
};
websocket.onerror = function (evt, e) {
console.log('Error occured: ' + evt.data);
};
- 不能直接使用swoole_client與websocket服務器通信,swoole_client是TCP客戶端
- 必須實現WebSocket協議才能和WebSocket服務器通信,可以使用swoole/framework提供的PHP WebSocket客戶端
- WebSocket服務器除了提供WebSocket功能之外,實際上也可以處理Http長連接。只需要增加onRequest事件監聽即可實現Comet方案Http長輪詢。
關於onRequest回調
swoole_websocket_server 繼承自 swoole_http_server
- 設置了onRequest回調,websocket服務器也可以同時作為http服務器
- 未設置onRequest回調,websocket服務器收到http請求后會返回http 400錯誤頁面
- 如果想通過接收http觸發所有websocket的推送,需要注意作用域的問題,面向過程請使用“global”對swoole_websocket_server進行引用,面向對象可以把swoole_websocket_server設置成一個成員屬性
可以創建更多的服務器 參照官方文檔嘗試
https://wiki.swoole.com/wiki/page/475.html
3 使用laravel5.5實現的前后台通信實例
主要思路是使用php artisan 自建命令控制服務端,使用HTML5的websocket實現客戶端功能
服務端:app/Console/Commands/Websocket.php內容
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use swoole_http_request;
use swoole_http_response;
use swoole_websocket_server;
class WebSocket extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'websocket
{cmd=start : can use start|stop|status|restart}
{--daemon : set to run in daemonize mode}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'swoole server control';
/**
* server
*
* @var swoole_websocket_server
*/
private $server;
/**
* * TYPE_ADMIN
* */
const TYPE_ADMIN = 0X00;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* 處理不同command信息
*
* @return mixed
*/
public function handle()
{
$command = $this->argument('cmd');
$option = $this->option('daemon');
switch ($command) {
case 'start':
$this->initWs($option);
break;
case 'stop':
$res = $this->sendAdminRequest($command);
if ($res){
$this->info('stop the server successfully');
} else {
$this->info('the server is not running');
}
break;
case 'status':
$res = $this->sendAdminRequest($command);
if ($res){
$this->info($res);
} else {
$this->info('the server is not running');
}
break;
case 'restart':
$res = $this->sendAdminRequest($command);
if ($res){
$this->info('restart the server successfully');
} else {
$this->info('the server is not running');
}
break;
default:
$this->info('請按照下面格式輸入命令:php artisan websocket {start|stop|status|restart}');
break;
}
}
//初始化服務端
public function initWs($daemonize = false)
{
if ($daemonize) {
$this->info('Starting Websocke server in daemon mode...');
} else {
$this->info('Starting Websocke server in interactive mode...');
}
$server = new swoole_websocket_server('0.0.0.0', 9501);
$server->set([
'daemonize' => $daemonize,
'log_file' => '/var/log/websocket.log'
]);
$server->on('close', function ($server, $fd) {
$this->info('close websocket server');
});
$server->on('open', function (swoole_websocket_server $server, $request) {
$this->info('websocket open');
});
$server->on('open', [$this, 'onOpen']);
$server->on('close', [$this, 'onClose']);
$server->on('message', [$this, 'onMessage']);
$server->on('request', [$this, 'onRequest']);
$this->server = $server;
$this->server->start();
}
public function onOpen(swoole_websocket_server $server, $request)
{
$this->info('websocket open');
}
public function onClose($server, $fd)
{
$this->info('close websocket server');
}
public function onMessage(swoole_websocket_server $server, $frame)
{
$this->info($frame->data);
$data = json_decode($frame->data, true);
//對data進行邏輯處理
$reply = '發送的信息是:' . $data['message'];
$response = [
'status' => true,
'data' => $reply
];
$server->push($frame->fd, json_encode($response));
}
//websocket客戶端同樣支持http協議
public function onRequest(swoole_http_request $request, swoole_http_response $response)
{
if ($request->post['type'] == self::TYPE_ADMIN) {
$ret = json_encode($this->commandHandle($request->post['content']));
return $response->end($ret);
}
}
//操作命名
public function commandHandle($command) {
if ($command == 'status') {
$this->info('handle status');
return $this->server->stats();
}
if ($command == 'restart') {
$this->info('handle restart');
return $this->server->reload();
}
if ($command == 'stop') {
$this->info('handle stop');
return $this->server->stop() && $this->server->shutdown();
}
return 'Unknown Command';
}
//發送http請求
public function sendAdminRequest($content) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1:9501");
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Expect:']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'type' => self::TYPE_ADMIN,
'content' => $content
]);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}
客戶端內容
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>websocket client</title>
</head>
<body>
<div>
<input id="message-content" type="text" name="message" />
<button onclick="sendMessage()">發送消息</button>
</div>
</body>
<script>
var wsServer = 'ws://115.159.81.46:9501';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
console.log("Connected to WebSocket server.");
};
websocket.onclose = function (evt) {
console.log("Disconnected");
};
websocket.onmessage = function (evt) {
console.log('從服務器接收到json信息: ' + evt.data);
alert('服務器返回信息:' + JSON.parse(evt.data).data);
};
websocket.onerror = function (evt, e) {
console.log('Error occured: ' + evt.data);
};
function sendMessage(){
var content = document.getElementById('message-content').value;
var data = {
message : content,
}
websocket.send(JSON.stringify(data));
};
</script>
</html>
啟動websocket服務器
進入系統根目錄,
php artisan websocket [--daemon] //是否使用daemon模式
php artisan websocket start|stop|status|restart //默認是start