Stories

Detail Return Return

項目實戰用swoole啓websocket服務 - Stories Detail

項目中用到了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進行守護進程管理:

image.png

守護進程啓動後,就可以進行websocket鏈接了

image.png

連接成功之後就可以給websocket服務端發送消息了

image.png

如果這個時候想測試通過服務器給某個用户推送數據,就可以藉助啓動的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]);
}

然後請求接口如下:

image.png

接口請求完成之後,再看剛剛的websocket連接界面,就會收到一條新的消息:

image.png

user avatar greatsql Avatar bytebase Avatar guangmingleiluodetouyingyi_bccdlf Avatar
Favorites 3 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.