基於swoole的websocket服務實現狀態同步
基礎流程圖
效果展示圖
介面展示
終端展示
前端實現程式碼
<html>
<head>
<title>WebSocket</title>
<style>
h2, hr, ul, li {
margin: 0;
padding: 0;
}
hr {
width: 200px;
margin-top: 5px;
margin-bottom: 5px;
}
#main li {
list-style: none;
}
#main li span {
display: inline-block;
width: 50px;
}
</style>
</head>
<!-- 初始化載入子節點 -->
<body onload="initialize();">
<h2>WebSocket</h2>
<hr>
<div>
<ul id="main">
<!-- 待渲染子節點列表 -->
</ul>
</div>
<hr>
<button onclick="connWebsocket()">傳送</button>
</body>
<script>
// 初始化頁面載入函式
function initialize() {
for (let i = 0; i < 10; i++) {
// 獲取主節點
let ulNode = window.document.getElementById('main')
// 生成隨機id
let id = makeRandomNumber(5)
// 建立子節點
liNode = window.document.createElement('li')
liNode.innerHTML = `<span>${id}</span><span></span><span>未開始</span>`
liNode.firstElementChild.setAttribute('style', 'color:#363636;')
liNode.lastElementChild.setAttribute('style', 'color:#363636;')
// 應用子節點
ulNode.appendChild(liNode)
}
}
// 連線websocket伺服器
function connWebsocket() {
// 所有的liNodes置為`進行中`
let liNodes = window.document.getElementById('main').children
// 待傳送訊息資料
let allData = []
for (let i = 0; i < liNodes.length; i++) {
let firstNode = liNodes[i].firstElementChild
let lastNode = liNodes[i].lastElementChild
// 渲染`進行中`狀態樣式
firstNode.setAttribute('style', 'color:#FF6347;')
lastNode.setAttribute('style', 'color:#FF6347;')
lastNode.textContent = '進行中'
allData.push({
'id': firstNode.textContent,
'status': lastNode.textContent,
})
}
// 進行weksocket通訊服務
let ws = new WebSocket("ws://localhost:9999");
ws.onopen = function(evt) {
console.log('connection start')
// 開啟連線就傳送訊息
ws.send(JSON.stringify(allData))
};
// 已完成數量
let completeNum = 0
ws.onmessage = function(evt) {
// 異常或錯誤處理
try {
var obj = JSON.parse(evt.data);
} catch (e) {
console.error(e)
return
}
if (!obj.id || !obj.status) {
console.error(`property is not undefined.`)
return
}
// 找到`id`對應節點
let nodeList = [...liNodes]
let liNode = nodeList.find((node) => {
return node.firstElementChild.textContent == obj.id
})
if (!liNode) {
console.error(`li node is not found.`)
return;
}
// 重新渲染介面,並自增已完成數量
completeNum++
liNode.lastElementChild.textContent = obj.status
liNode.lastElementChild.setAttribute('style', 'color:#008B8B;')
liNode.firstElementChild.setAttribute('style', 'color:#008B8B;')
// 如果完成數和節點數相等,主動斷開連線
if (completeNum === liNodes.length) {
ws.close()
}
};
ws.onclose = function(evt) {
console.log("connection close");
}
}
// 製作整型隨機數
function makeRandomNumber(digit = 6) {
if (digit < 1 || digit > 10) {
throw new RangeError('位數不能小於1且不能大於10')
}
const min = Math.pow(10, digit - 1)
const max = Math.pow(10, digit) - 1
let val = Math.floor(Math.random() * max + 1)
while (val < min) {
val = Math.floor(Math.random() * max + 1)
}
return val
}
</script>
</html>
後端程式碼實現
1. 檢視swoole擴充套件版本
> php --ri swoole
swoole
Swoole => enabled
Author => Swoole Team <team@swoole.com>
Version => 4.5.11
Built => Feb 21 2022 14:53:00
coroutine => enabled
kqueue => enabled
rwlock => enabled
pcre => enabled
zlib => 1.2.11
brotli => E16777225/D16777225
async_redis => enabled
Directive => Local Value => Master Value
swoole.enable_coroutine => On => On
swoole.enable_library => On => On
swoole.enable_preemptive_scheduler => Off => Off
swoole.display_errors => On => On
swoole.use_shortname => On => On
swoole.unixsock_buffer_size => 262144 => 262144
注意:以上演示 swoole 擴充套件版本為 4.5+
2. 建立自定義websocket服務類
<?php
namespace App\Handlers\Websockets;
use Swoole\Coroutine;
class CoServer
{
public function __construct()
{
// 開啟一鍵協程化
\Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
}
public function start()
{
// 非同步風格websocket伺服器
$ws = new \Swoole\WebSocket\Server('127.0.0.1', 9999);
$ws->on('Open', function ($ws, $request) {
$this->log('連線成功', $request->fd);
});
$ws->on('Message', function ($ws, $frame) {
$fid = $frame->fd ?? 0;
$data = json_decode($frame->data, true);
if (!$data || !is_array($data)) {
$this->log('訊息格式錯誤!', $fid);
return;
}
$this->log('伺服器已接收訊息.', $fid);
// 協程間通訊,類似 go 的 sync.WaitGroup
$wg = new \Swoole\Coroutine\WaitGroup();
// 建立請求遠端協程任務
$wg->add();
Coroutine::create(function () use($wg, $data, $fid) {
$this->log('已傳送訊息到遠端.', $fid);
// 協程 http 客戶端
$client = new \Swoole\Coroutine\Http\Client('127.0.0.1', 9101);
$client->post('/remote/message', [
'data' => $data,
'fid' => $fid,
]);
$client->close();
$wg->done();
});
// 建立監聽訊息協程任務
$wg->add();
Coroutine::create(function () use ($wg, $ws, $fid) {
$this->log('開始監聽訊息.', $fid);
// 協程 redis 客戶端
$redis = new \Swoole\Coroutine\Redis();
$redis->connect('127.0.0.1', 6379);
if ($redis->subscribe(['ws:fid:'.$fid])) {
while ($msg = $redis->recv()) {
list($type, $name, $cont) = $msg;
if ($type == 'message' && $name == 'ws:fid:'.$fid) {
$ws->push($fid, $cont);
$this->log('訊息已回覆', $fid);
}
}
}
$redis->close();
$wg->done();
});
$wg->wait();
// 處理完主動斷開連線
$ws->close($fid);
});
$ws->on('close', function ($server, $fid) {
echo "client {$fid} closed\n";
});
$ws->start();
}
protected function log($msg, $fd = 0)
{
if ($fd) {
echo sprintf('[%s]: %d -> %s'.PHP_EOL, date('H:i:s'), $fd, $msg);
} else {
echo sprintf('[%s]: %s'.PHP_EOL, date('H:i:s'), $msg);
}
}
}
3. 模擬遠端請求介面方法
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
class RemoteController extends Controller
{
// 對應路由:/remote/message POST
public function message(Request $request)
{
$data = $request->input('data');
$fid = $request->input('fid');
Log::info('請求引數: '.json_encode($request->all()));
while (true) {
if (empty($data)) {
break;
}
usleep(500000);
foreach ($data as $key => $val) {
if (random_int(0, 100) > 80) {
$val['status'] = '已完成';
Log::info('獲取訊息中...');
Redis::connection()->publish('ws:fid:'.$fid, json_encode($val));
unset($data[$key]);
}
}
}
return response()->json([
'status' => 0,
'msg' => 'success',
'data' => null,
]);
}
}
4. 建立命令執行websocket服務
<?php
namespace App\Console\Commands;
use App\Handlers\Websockets\CoServer;
use Illuminate\Console\Command;
use Ratchet\Http\HttpServer;
use Ratchet\Server\IoServer;
use Ratchet\WebSocket\WsServer;
class WebsocketServer extends Command
{
protected $signature = 'ws:start {server}';
protected $description = 'Command description';
const SERVER_HTTP = 'http';
const SERVER_CO = 'co';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$allowServers = [self::SERVER_HTTP, self::SERVER_CO];
$inputServer = $this->argument('server');
if (!in_array($inputServer, $allowServers)) {
$this->error('伺服器型別錯誤');
return;
}
switch ($inputServer) {
case self::SERVER_HTTP:
$this->handleHttp();
break;
case self::SERVER_CO:
$this->handleCo();
break;
default:
$this->error('伺服器型別不存在');
}
}
protected function handleHttp()
{
// $server = IoServer::factory(
// new HttpServer(
// new WsServer(
// new \App\Handlers\Websockets\HttpServer()
// )
// )
// , 9999);
// $server->run();
}
protected function handleCo()
{
$server = new CoServer();
$server->start();
}
}
5. 執行websocket服務
> php artisan ws:start co
後續擴充套件
許可權校驗
超時機制
資料同步
配置引數
……
本作品採用《CC 協議》,轉載必須註明作者和本文連結