實戰swoole【聊天室】

前言:瞭解概念以後就應該練練手啦,否則就是巨嬰php

有收穫的話請加顆小星星,沒有收穫的話能夠 反對 沒有幫助 舉報三連html

準備工做

  • 須要先看初識swoole【上】,瞭解基本的服務端WebSocket使用
  • js WebSocket客戶端簡單使用

使用

# 命令行1
php src/websocket/run.php
# 命令行2
cd public && php -S localhost:8000
# 客戶端,多開幾個查看效果
訪問http://localhost:8000/

複製代碼

WebSocket

官方示例git

$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();
複製代碼

詳解:github

  • swoole_websocket_server 繼承自 swoole_http_serverweb

    • 設置了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);redis

    • 當WebSocket客戶端與服務器創建鏈接並完成握手後會回調此函數。
    • $req 是一個Http請求對象,包含了客戶端發來的握手請求信息
    • onOpen事件函數中能夠調用push向客戶端發送數據或者調用close關閉鏈接
    • onOpen事件回調是可選的
  • function onMessage(swoole_websocket_server $server, swoole_websocket_frame $frame)json

    • 當服務器收到來自客戶端的數據幀時會回調此函數。
    • $frame 是swoole_websocket_frame對象,包含了客戶端發來的數據幀信息
    • onMessage回調必須被設置,未設置服務器將沒法啓動
    • 客戶端發送的ping幀不會觸發onMessage,底層會自動回覆pong包
  • swoole_websocket_frame 屬性bootstrap

    • $frame->fd,客戶端的socket id,使用$server->push推送數據時須要用到
    • $frame->data,數據內容,能夠是文本內容也能夠是二進制數據,能夠經過opcode的值來判斷
    • $frame->opcode,WebSocket的OpCode類型,能夠參考WebSocket協議標準文檔
    • $frame->finish, 表示數據幀是否完整,一個WebSocket請求可能會分紅多個數據幀進行發送(底層已經實現了自動合併數據幀,如今不用擔憂接收到的數據幀不完整)

聊天室服務端示例

目錄結構:bash

  • config
    • socket.php
  • src
    • websocket
      • Config.php
      • run.php
      • WebSocketServer.php 內存表版本
      • WsRedisServer.php redis版本

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上找完整的項目進行深刻學習

參考

相關文章
相關標籤/搜索