websocket結合實際項目的實踐
swoole官方websocket服務端介紹
項目通過swoole在local的9505端口上啓動一個websocket的服務端進行服務監聽, 然後使用nginx做websocket的代理, 將所有location匹配到/ws的請求都代理到websocket上
通過swoole啓動websocket服務端:
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select($selectDb);
//創建WebSocket Server對象,監聽127.0.0.1:9502端口。
$server = new Swoole\Websocket\Server('127.0.0.1', 9505);
$server->on('open', function($server, $req) {
echo "connection open: {$req->fd}\n";
});
$server->on('message', function($server, $frame) {
echo "received message: {$frame->data}\n";
$ws->push($frame->fd, 'server receive a message!');
$data = json_decode($frame->data, true);
if (!empty($data['deviceId'])) { //記錄設備連接
$ws->flag_key = 'device';//給ws增加標記類型
$ws->flag_value = $data['deviceId']; //給ws增加對應的key值
$oldFid = $redis->hGet('device:links', $data['deviceId']);
if ($oldFid != $frame->fd) {
$redis->hSet('device:links', $data['deviceId'], $frame->fd);
}
}
});
$server->on('close', function($server, $fd) {
$flagKey = $ws->flag_key ?? '';
$flagValue = $ws->flag_value ?? '';
//如果連接時候設置了相應redis的值斷開的時候要刪除
if ($flagKey && $flagValue) {
$redis->hDel($flagKey . ':links', $flagValue);
}
echo "connection close: {$fd}\n";
});
// 設置onRequest 回調,WebSocket\Server 也可以同時作為 HTTP 服務器
$ws->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) use ($ws) {
$post = $request->post ?: [];
echo 'request:' . json_encode($post, JSON_UNESCAPED_UNICODE);
if (isset($post['fd']) && $ws->isEstablished(intval($request->post['fd']))) {
$ws->push($request->post['fd'], $request->post['message']);
}
// 重要:需要給客户端返回響應,否則客户端會一直等待
$response->end('ok');
});
$server->start();
websoketServer.php只是啓動了本機9505端口的監聽
nginx中websocket代理設置
server {
# 同時監聽 80 端口(HTTP/ws)和 443 端口(HTTPS/wss)
listen 80;
listen 443 ssl;
server_name your_domain.com;
# SSL 證書配置(僅對 443 端口生效)
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 處理 WebSocket 代理(同時支持 ws 和 wss)
location /ws {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 轉發到後端同一個 ws 服務
proxy_pass http://127.0.0.1:9505;
}
}
通過nginx代理的方式並不需要向外部暴露websocket的端口
nginx默認代理設置proxy_read_timeout
注意nginx默認會斷開超過60s(默認)沒有接收到後端服務(這裏是swoole WebSocket服務)發送消息的連接:
之所以會出現上面的情況, websocket連接成功後,在沒有消息傳遞的情況下默認超過60s連接就被nginx斷開了. 是因為nginx默認代理設置的proxy_read_timeout為60s.
在 WebSocket 代理場景下,這意味着:
- 若在默認的超時時間內,Nginx 沒有從 Swoole 服務端接收到任何數據(包括客户端發送的心跳包被服務端處理後返回的響應),Nginx 會主動斷開與 Swoole 服務端的連接,進而導致客户端的 WebSocket 連接也被斷開 。
通過nginx代理websocket時注意:
- nginx默認代理設置的proxy_read_timeout=60s, 所以websocket客户端需要需要設置小於60s的定時心跳,並且服務器需要響應心跳包, 否則連接還是會被nginx斷開, 因為proxy_read_timeout 檢測的是後端服務的響應,
進行websocket連接建立
通過進程守護管理器啓動websocket服務
服務端啓動成功後客户端就可以進行websoket鏈接建立了:
- 鏈接方式1: ws://domain.com/ws
- 鏈接方式2: wss://domain.com/ws
- 鏈接方式3: http://domain.com/ws
注意這裏雖然使用的是 http://domain.com/ws 建立鏈接,實際發送的請求header中有Upgrade: websocket,且請求location匹配到了/ws這條規則,因此才能正常建立websocket鏈接, 有點兒類似於js中使用new websocket('http://domain.com/wx')
![]()
如果在瀏覽器地址欄中輸入與上面同樣的鏈接地址這種情況就不能建立websocket鏈接(因為header中沒有Upgrade: websocket)
異常鏈接方式:
注意: 直接通過ws://domain.com或者wss://domain.com的方式是不能建立websocket鏈接的,因為匹配不到nginx配置中的/ws這條規則:
![]()
通過http請求主動觸發websocket推送
寫一個http方法主動觸發websocket推送如下所示:
public function rebootTest()
{
$deviceId = $this->request->param('device_id', 'daoheng');
$redis = RedisService::getInstance();
if ($fid = $redis->hGet('device:links', $deviceId)) {
$message = ['advertisement' => 1];
$result = Http::post('http://localhost:9505', [
'fd' => $fid,
'message' => json_encode($message),
]);
return frontReturn(1, 'ok', [$fid, $result]);
}
return frontReturn(1, 'no_connect');
}
首先建立websocket鏈接,併發送一個消息
觸發接口請求:
websocket收到相應推送: