項目中用到了websocket長鏈接, 記錄下結合swoole如何實現這個功能
項目中之所以要用websocket主要是想實現用户在回收設備上掃碼投遞瓶子之後,將投遞的瓶子數據推送到用户小程序端進行同步展示, 這樣用户在設備上投遞完瓶子後, 在小程序上就能同時看到相應變化, 給用户一個更好的使用體驗
面向過程風格代碼
//引入redis
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('***'); //redis密碼
$redis->select(15);//選擇使用的redis庫
//創建websocket服務端
$server = new Swoole\Server('0.0.0.0', 9502, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$server->set(array(
'ssl_cert_file' => __DIR__.'/config/fullchain.pem',
'ssl_key_file' => __DIR__.'/config/privkey.pem',
'ssl_verify_peer' => false,
'ssl_allow_self_signed' => true,
'log_file' => __DIR__ . '/cert/' . date('Ymd') . '.log',
));
//監聽WebSocket連接打開事件。
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
$returnData = ['fid' => $request->fd];
$ws->push($request->fd, json_encode($returnData));
});
//監聽WebSocket消息事件。
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
$data = json_decode($frame->data, true);
//用redis存儲小程序用户id與wesocket的鏈接標識fd的對應關係
if (isset($data['uid']) && $data['uid']) {
$oldFid = $redis->hGet('user:links', $data['uid']);
if ($oldFid != $frame->fd) {
$redis->hSet('user:links', $data['uid'], $frame->fd);
}
$returnData = [
'uid' => $data['uid'],
'fid' => $frame->fd
];
//給對應鏈接推送個消息
$ws->push($frame->fd, json_encode($returnData));
}
});
//監聽WebSocket連接關閉事件。
$server->on('close', function ($server, $fd) {
echo "client {$fd} closed\n";
});
//設置onRequest回調,WebSocket\Server 也可以同時作為 HTTP 服務器,
//這樣就可以通過接收HTTP請求來觸發webSocket的推送, 這樣就可以在程序中主動觸發推送了
$server->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
global $server;//調用外部的server
$post = $request->post ?: [];//獲取post請求傳遞的數據
if (isset($post['fd']) && $ws->isEstablished(intval($request->post['fd']))) {
$ws->push($request->post['fd'], $request->post['message']);
}
//可以通過$server->connections 遍歷所有websocket連接用户的fd,給所有用户推送
});
$server->start();
面向對象風格代碼
//聲明一個WebSocketServer 服務類
class WebSocketServer
{
public $server;
public function __construct()
{
$this->server = new Swoole\WebSocket\Server("0.0.0.0", 9502);
$this->server->on('open', function (Swoole\WebSocket\Server $server, $request) {
echo "server: handshake success with fd{$request->fd}\n";
});
$this->server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
$server->push($frame->fd, "this is server");
});
$this->server->on('close', function ($ser, $fd) {
echo "client {$fd} closed\n";
});
$this->server->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
// 接收http請求從get獲取message參數的值,給用户推送
// $this->server->connections 遍歷所有websocket連接用户的fd,給所有用户推送
foreach ($this->server->connections as $fd) {
// 需要先判斷是否是正確的websocket連接,否則有可能會push失敗
if ($this->server->isEstablished($fd)) {
$this->server->push($fd, $request->get['message']);
}
}
});
$this->server->start();
}
}
//實例化這個服務類,這樣就啓動了websocket服務了
new WebSocketServer();
上面分別用了"面向過程風格"與"面向對象風格"演示瞭如何通過swoole創建一websocket服務端, 有下面幾點需要注意的:
- 通過設置onRequest回調, 在創建websocket服務端的時候同時內部也起了一個http服務器,監聽9502端口, 這樣在程序中如果想給某個用户主動推送數據的時候, 就可以藉助這個9502端口的http服務, 發送post請求, 參數傳遞要發送請求的websocket鏈接標識fd以及要傳遞信息, 這樣就可以主動在程序中向用户推送數據了
- Swoole\Http\Request $request的post屬性中存儲的是HTTP請求的POST參數,格式為數組
//post屬性
Swoole\Http\Request->post: array
//示例
echo $request->post['hello'];
// 獲取所有POST參數
var_dump($request->post);
//get屬性
//存的是HTTP請求的GET參數, 相當於PHP中的$_GET, 格式為數組
Swoole\Http\Request->get: array
// 如:index.php?hello=123
echo $request->get['hello'];
// 獲取所有GET參數
var_dump($request->get);
- 講幾個配置項
-
ssl_cert_file / ssl_key_file
設置SSL隧道加密, 設置值為一個文件名字符串, 指定cert證書和key私鑰的路徑信息.
wss應用中, 發起WebSocket連接的頁面必須使用HTTPS, 且HTTPS應用瀏覽器必須信任證書才能瀏覽網頁, 瀏覽器如果不信任sll證書將無法使用wss. -
log_file
指定Swoole錯誤日誌文件. 在 Swoole 運行期發生的異常信息會記錄到這個文件中,默認會打印到屏幕。開啓守護進程模式後 (daemonize => true),標準輸出將會被重定向到 log_file。在 PHP 代碼中 echo/var_dump/print 等打印到屏幕的內容會寫入到 log_file 文件。
-
debug_mode 調試模式
設置日誌模式為 debug 調試模式,只有編譯時開啓了 --enable-debug 才有作用。
$server->set([ 'debug_mode' => true ]) -
daemonize 守護進程化(默認false)
設置 daemonize => true 時,程序將轉入後台作為守護進程運行。長時間運行的服務器端程序必須啓用此項。如果不啓用守護進程,當 ssh 終端退出後,程序將被終止運行。
- 啓用守護進程後,標準輸入和輸出會被衝定向到log_file.
- 如果沒有設置log_file, 將重定向到/dev/null, 所有打印屏幕的信息都會被丟棄
- 使用supervisord或者systemd管理Swoole服務的時候, 請勿設置daemonize=true. 主要因為兩者機制不同
實際測試看看效果
在寶塔面板中通過supervisord進行守護進程管理:
守護進程啓動後,就可以進行websocket鏈接了
連接成功之後就可以給websocket服務端發送消息了
如果這個時候想測試通過服務器給某個用户推送數據,就可以藉助啓動的HTTP服務
// 寫一個測試方法,
public function testPush()
{
$userId = $this->request->param('user_id',3075);
$redis = RedisService::getInstance();
$redis->select(15);
$fd = $redis->hGet('user:links', $userId);
if (!$fd) {
return frontReturn(0, 'websockt未連接');
}
$domain = 'https://***.demo-domain.com';
$port = 9502;
$listNew = ['test' => 'data'];
Http::post("{$domain}:{$port}", [
'fd' => $fd,
'message' => json_encode(['list_new' => $listNew])
]);
return frontReturn(1, 'ok',['list_new' => $listNew]);
}
然後請求接口如下:
接口請求完成之後,再看剛剛的websocket連接界面,就會收到一條新的消息: