實戰swoole【聊天室】
前言:瞭解概念之後就應該練練手啦,不然就是巨嬰
有收穫的話請加顆小星星 ,沒有收穫的話可以反對 沒有幫助 舉報 三連
- ofollow,noindex">程式碼倉庫
- 實戰swoole【聊天室】
- 線上體驗
準備工作
- 需要先看初識swoole【上】,瞭解基本的服務端Socket/">WebSocket使用
- js WebSocket客戶端簡單使用
使用
# 命令列1 php src/websocket/run.php # 命令列2 cd public && php -S localhost:8000 # 客戶端,多開幾個檢視效果 訪問http://localhost:8000/ 複製程式碼
WebSocket
官方示例
$server = new swoole_websocket_server("0.0.0.0", 9501); $server->on('open', function (swoole_websocket_server $server, $request) { echo "server: handshake success with fd{$request->fd}\n"; }); $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"); }); $server->on('close', function ($ser, $fd) { echo "client {$fd} closed\n"; }); $server->on('request', function (swoole_http_request $request, swoole_http_response $response) { global $server;//呼叫外部的server // $server->connections 遍歷所有websocket連線使用者的fd,給所有使用者推送 foreach ($server->connections as $fd) { $server->push($fd, $request->get['message']); } }); $server->start(); 複製程式碼
詳解:
-
swoole_websocket_server 繼承自 swoole_http_server
- 設定了onRequest回撥,websocket伺服器也可以同時作為http伺服器
- 未設定onRequest回撥,websocket伺服器收到http請求後會返回http 400錯誤頁面
- 如果想通過接收http觸發所有websocket的推送,需要注意作用域的問題,面向過程請使用global對swoole_websocket_server進行引用,面向物件可以把swoole_websocket_server設定成一個成員屬性
-
function onOpen(swoole_websocket_server
$svr
, swoole_http_request$req
);- 當WebSocket客戶端與伺服器建立連線並完成握手後會回撥此函式。
- $req 是一個Http請求物件,包含了客戶端發來的握手請求資訊
- onOpen事件函式中可以呼叫push向客戶端傳送資料或者呼叫close關閉連線
- onOpen事件回撥是可選的
-
function onMessage(swoole_websocket_server
$server
, swoole_websocket_frame$frame
)- 當伺服器收到來自客戶端的資料幀時會回撥此函式。
- $frame 是swoole_websocket_frame物件,包含了客戶端發來的資料幀資訊
- onMessage回撥必須被設定 ,未設定伺服器將無法啟動
- 客戶端傳送的ping幀不會觸發onMessage,底層會自動回覆pong包
-
swoole_websocket_frame 屬性
-
$frame->fd
,客戶端的socket id,使用$server->push
推送資料時需要用到 -
$frame->data
,資料內容,可以是文字內容也可以是二進位制資料,可以通過opcode的值來判斷 -
$frame->opcode
,WebSocket的OpCode型別,可以參考WebSocket協議標準文件 -
$frame->finish
, 表示資料幀是否完整,一個WebSocket請求可能會分成多個數據幀進行傳送(底層已經實現了自動合併資料幀,現在不用擔心接收到的資料幀不完整)
-
聊天室服務端示例
目錄結構:
-
config
- socket.php
-
src
-
websocket
- Config.php
- run.php
- WebSocketServer.php 記憶體表版本
- WsRedisServer.php redis版本
-
websocket
WebSocketServer.php 記憶體表版本
<?php namespace App\WebSocket; class WebSocketServer { private $config; private $table; private $server; public function __construct() { // 記憶體表 實現程序間共享資料,也可以使用redis替代 $this->createTable(); // 例項化配置 $this->config = Config::getInstance(); } public function run() { $this->server = new \swoole_websocket_server( $this->config['socket']['host'], $this->config['socket']['port'] ); $this->server->on('open', [$this, 'open']); $this->server->on('message', [$this, 'message']); $this->server->on('close', [$this, 'close']); $this->server->start(); } public function open(\swoole_websocket_server $server, \swoole_http_request $request) { $user = [ 'fd' => $request->fd, 'name' => $this->config['socket']['name'][array_rand($this->config['socket']['name'])] . $request->fd, 'avatar' => $this->config['socket']['avatar'][array_rand($this->config['socket']['avatar'])] ]; // 放入記憶體表 $this->table->set($request->fd, $user); $server->push($request->fd, json_encode( array_merge(['user' => $user], ['all' => $this->allUser()], ['type' => 'openSuccess']) ) ); } private function allUser() { $users = []; foreach ($this->table as $row) { $users[] = $row; } return $users; } public function message(\swoole_websocket_server $server, \swoole_websocket_frame $frame) { $this->pushMessage($server, $frame->data, 'message', $frame->fd); } /** * 推送訊息 * * @param \swoole_websocket_server $server * @param string $message * @param string $type * @param int $fd */ private function pushMessage(\swoole_websocket_server $server, string $message, string $type, int $fd) { $message = htmlspecialchars($message); $datetime = date('Y-m-d H:i:s', time()); $user = $this->table->get($fd); foreach ($this->table as $item) { // 自己不用傳送 if ($item['fd'] == $fd) { continue; } $server->push($item['fd'], json_encode([ 'type' => $type, 'message' => $message, 'datetime' => $datetime, 'user' => $user ])); } } /** * 客戶端關閉的時候 * * @param \swoole_websocket_server $server * @param int $fd */ public function close(\swoole_websocket_server $server, int $fd) { $user = $this->table->get($fd); $this->pushMessage($server, "{$user['name']}離開聊天室", 'close', $fd); $this->table->del($fd); } /** * 建立記憶體表 */ private function createTable() { $this->table = new \swoole_table(1024); $this->table->column('fd', \swoole_table::TYPE_INT); $this->table->column('name', \swoole_table::TYPE_STRING, 255); $this->table->column('avatar', \swoole_table::TYPE_STRING, 255); $this->table->create(); } } 複製程式碼
WsRedisServer.php redis版本
<?php namespace App\WebSocket; use Predis\Client; /** * 使用redis代替table,並存儲歷史聊天記錄 * * Class WsRedisServer * @package App\WebSocket */ class WsRedisServer { private $config; private $server; private $client; private $key = "socket:user"; public function __construct() { // 例項化配置 $this->config = Config::getInstance(); // redis $this->initRedis(); // 初始化,主要是服務端自己關閉不會清空redis foreach ($this->allUser() as $item) { $this->client->hdel("{$this->key}:{$item['fd']}", ['fd', 'name', 'avatar']); } } public function run() { $this->server = new \swoole_websocket_server( $this->config['socket']['host'], $this->config['socket']['port'] ); $this->server->on('open', [$this, 'open']); $this->server->on('message', [$this, 'message']); $this->server->on('close', [$this, 'close']); $this->server->start(); } public function open(\swoole_websocket_server $server, \swoole_http_request $request) { $user = [ 'fd' => $request->fd, 'name' => $this->config['socket']['name'][array_rand($this->config['socket']['name'])] . $request->fd, 'avatar' => $this->config['socket']['avatar'][array_rand($this->config['socket']['avatar'])] ]; // 放入redis $this->client->hmset("{$this->key}:{$user['fd']}", $user); // 給每個人推送,包括自己 foreach ($this->allUser() as $item) { $server->push($item['fd'], json_encode([ 'user' => $user, 'all' => $this->allUser(), 'type' => 'openSuccess' ])); } } private function allUser() { $users = []; $keys = $this->client->keys("{$this->key}:*"); // 所有的key foreach ($keys as $k => $item) { $users[$k]['fd'] = $this->client->hget($item, 'fd'); $users[$k]['name'] = $this->client->hget($item, 'name'); $users[$k]['avatar'] = $this->client->hget($item, 'avatar'); } return $users; } public function message(\swoole_websocket_server $server, \swoole_websocket_frame $frame) { $this->pushMessage($server, $frame->data, 'message', $frame->fd); } /** * 推送訊息 * * @param \swoole_websocket_server $server * @param string $message * @param string $type * @param int $fd */ private function pushMessage(\swoole_websocket_server $server, string $message, string $type, int $fd) { $message = htmlspecialchars($message); $datetime = date('Y-m-d H:i:s', time()); $user['fd'] = $this->client->hget("{$this->key}:{$fd}", 'fd'); $user['name'] = $this->client->hget("{$this->key}:{$fd}", 'name'); $user['avatar'] = $this->client->hget("{$this->key}:{$fd}", 'avatar'); foreach ($this->allUser() as $item) { // 自己不用傳送 if ($item['fd'] == $fd) { continue; } $is_push = $server->push($item['fd'], json_encode([ 'type' => $type, 'message' => $message, 'datetime' => $datetime, 'user' => $user ])); // 刪除失敗的推送 if (!$is_push) { $this->client->hdel("{$this->key}:{$item['fd']}", ['fd', 'name', 'avatar']); } } } /** * 客戶端關閉的時候 * * @param \swoole_websocket_server $server * @param int $fd */ public function close(\swoole_websocket_server $server, int $fd) { $user['fd'] = $this->client->hget("{$this->key}:{$fd}", 'fd'); $user['name'] = $this->client->hget("{$this->key}:{$fd}", 'name'); $user['avatar'] = $this->client->hget("{$this->key}:{$fd}", 'avatar'); $this->pushMessage($server, "{$user['name']}離開聊天室", 'close', $fd); $this->client->hdel("{$this->key}:{$fd}", ['fd', 'name', 'avatar']); } /** * 初始化redis */ private function initRedis() { $this->client = new Client([ 'scheme' => $this->config['socket']['redis']['scheme'], 'host' => $this->config['socket']['redis']['host'], 'port' => $this->config['socket']['redis']['port'], ]); } } 複製程式碼
config.php
<?php namespace App\WebSocket; class Config implements \ArrayAccess { private $path; private $config; private static $instance; public function __construct() { $this->path = __DIR__ . '/../../config/'; } // 單例模式 public static function getInstance() { if (!self::$instance) { self::$instance = new self(); } return self::$instance; } public function offsetSet($offset, $value) { // 閹割 } public function offsetGet($offset) { if (empty($this->config)) { $this->config[$offset] = require $this->path . $offset . ".php"; } return $this->config[$offset]; } public function offsetExists($offset) { return isset($this->config[$offset]); } public function offsetUnset($offset) { // 閹割 } // 禁止克隆 final private function __clone(){} } 複製程式碼
config/socket.php
<?php return [ 'host' => '0.0.0.0', 'port' => 9501, 'redis' => [ 'scheme' => 'tcp', 'host' => '0.0.0.0', 'port' => 6380 ], 'avatar' => [ './images/avatar/1.jpg', './images/avatar/2.jpg', './images/avatar/3.jpg', './images/avatar/4.jpg', './images/avatar/5.jpg', './images/avatar/6.jpg' ], 'name' => [ '科比', '庫裡', 'KD', 'KG', '喬丹', '鄧肯', '格林', '湯普森', '伊戈達拉', '麥迪', '艾弗森', '卡哇伊', '保羅' ] ]; 複製程式碼
run.php
<?php require __DIR__ . '/../bootstrap.php'; $server = new App\WebSocket\WebSocketServer(); $server->run(); 複製程式碼
總結
完整示例:聊天室
學完後發現生活中所謂的聊天室其實也不過如此,當然這只是簡單的demo,很多功能都沒有實現,想進一步學習的話可以去github上找完整的專案進行深入學習