基于swoole的websocket聊天室设计实现

本文介绍如何利用php的swoole扩展来实现一个基于websocket的聊天室。重点在于利用swoole_table进行进程间数据共享,管理用户连接,并在处理订阅和取消订阅操作时确保线程安全。
摘要由CSDN通过智能技术生成

php中就不能不知道swoole这个扩展了,有了这个扩展很多不可能就变成了可能。

借助于swoole提供的websocket机制,实现一个websocket服务器其实非常简单,我们只需要关注如何正确的管理用户链接以及状态。

实现要点:

  • 使用swoole_table 在进程间共享数据,用来存储房间号中的fd列表。
  • 处理订阅以及取消订阅的时候要加锁。(涉及到fd列表的反序列化)

我们一起来看看完整的实现

<?php
/**
 * websocket 客户端
 */
date_default_timezone_set('Asia/Shanghai');

class Server
{
    private $_server;
    private $_lock;
    private $_table;

    private $_config = [];


    public function __construct($config)
    {
        $this->_config = $config;
        $this->_lock = new \Swoole\Lock(SWOOLE_MUTEX);
        $this->_server = new \Swoole\Websocket\Server($this->_config['ws']['host'], $this->_config['ws']['port']);
        $this->_table = new \Swoole\Table($config['ws']['max_channel']);
        $this->_table->column('fds', \Swoole\Table::TYPE_STRING, $config['ws']['max_fds'] * 2);// fds 以逗号分隔
        $this->_table->create();
    }

    public function run()
    {
        $this->_server->on('start', [$this, 'onStart']);
        $this->_server->on('open', [$this, 'onOpen']);
        $this->_server->on('message', [$this, 'onMessage']);
        $this->_server->on('request', [$this, 'onRequest']);
        $this->_server->on('close', [$this, 'onClose']);
        $this->_server->set($this->_config['websocket_server']);
        $this->_server->start();
    }


    /**
     * 打开socket链接回调
     */
    public function onOpen($ws, $frame)
    {
        // 不是websocket链接 有可能是常规的http
        if (!$this->is_webscoket($frame->fd)) {
            return;
        }
        $this->broadcast('internal.open', json_encode(['fd' => $frame->fd]));
        $this->log('info', "user open the connection", "fd=". $frame->fd);
    }

    /**
     * 程序启动回调
     */
    public function onStart($server)
    {
        $this->log('info', "websocket start success", "address=ws://". $this->_config['ws']['host'] . ":" . $this->_config['ws']['port']);
    }
    
    /**
     * 收到ws消息的回调 {"event": "", "channel": "", "cid":0}
     */
    public function onMessage($server, $frame)
    {
        $req = json_decode($frame->data, true);
        if (empty($req)) {
            $this->push($frame->fd, $this->pack($req, [], -1, 'empty request'));
            return;
        }
        if (!isset($req['event'])) {
            $this->push($frame->fd, $this->pack($req, [], -1, 'event is required'));
            return;
        }
        // 长连接 心跳
        if ($req['event'] == 'heartbeat') {
            $this->push($frame->fd, $this->pack($req));
            return;
        }
        // 长连接 状态查询
        if ($req['event'] == 'status') {
            $this->push($frame->fd, $this->handle_status([]));
            return;
        }
        // 长连接订阅
        if ($req['event'] == 'sub') {
            if (!isset($req['channel'])) {
                $this->push($frame->fd, $this->pack($req, [], -1, 'channel is required'));
                return;
            }
            $this->sub($req['channel'], $frame->fd);
            $this->push($frame->fd, $this->pack($req));
            return;
        }
        // 长连接取消订阅
        if ($req['event'] == 'unsub') {
            if (!isset($req['channel'])) {
                $this->push($frame->fd, $this->pack($req, [], -1, 'channel is required'));
                return;
            }
            $this->unsub($req['channel'], $frame->fd);
            $this->push($frame->fd, $this->pack($req));
            return;
        }
        // 长连接广播
        if ($req['event'] == 'broadcast') {
            if (!isset($req['channel'])) {
                $this->push($frame->fd, $this->pack($req, [], -1, 'channel is required'));
                return;
            }
            if (!isset($req['data'])) {
                $this->push($frame->fd, $this->pack($req, [], -1, 'data is required'));
                return;
            }
            $this->push($frame->fd, $this->handle_broadcast(['channel' => $req['channel'], 'data' => $req['data']]));
            return;
        }
        // 长连接推送
        if ($req['event'] == 'push') {
            if (!isset($req['channel'])) {
                $this->push($frame->fd, $this->pack($req, [], -1, 'channel is required'));
                return;
            }
            if (!isset($req['data'])) {
                $this->push($frame->fd, $this->pack($req, [], -1, 'data is required'));
                return;
            }
            if (!isset($req['fd'])) {
                $this->push($frame->fd, $this->pack($req, [], -1, 'fd is required'));
                return;
            }
            $this->push($frame->fd, $this->handle_push(['channel' => $req['channel'], 'data' => $req['data'],'fd' => $req['fd']]));
            return;
        }
        $this->push($frame->fd, $this->pack($req, [], -1, 'invalid event name'));
    }

    /**
    * http 链接回调 可以通过curl来rpc
    */
    public function onRequest($request, $response)
    {
        $task = json_decode($request->rawContent(), true);
        if (empty($task)) {
            $response->end($this->pack([], [], -1, 'empty request'));
            return;
        }
        if (!isset($task['method'])) {
            $response->end($this->pack([], [], -1, 'method required'));
            return;
        }
        if (!isset($task['params'])) {
            $response->end($this->pack([], [], -1, 'params required'));
            return;
        }
        // 广播消息
        if ($task['method'] == 'broadcast') {
            $ret = $this->handle_broadcast($task['params']);
            $response->end($ret);
        // 单推
        } elseif ($task['method'] == 'push') {
            $ret = $this->handle_push($task['params']);
            $response->end($ret);
        // 状态查询
        } elseif ($task['method'] == 'status') {
            $ret = $this->handle_status($task['params']);
            $response->end($ret);
        // 未知请求
        } else {
            $response->end($this->pack([], [], -1, 'unknow method'));
        }
    }
    /**
     *  处理多推
     */
    private function handle_broadcast($task)
    {
        if (!isset($task['channel'])) {
            return $this->pack([], [], -1, 'params channel required');
        }
        if (!isset($task['data'])) {
            return $this->pack([], [], -1, 'params data required');
        }
        $channel = $task['channel'];
        $data = $task['data'];
        $this->broadcast($channel, $data);
        $this->log("info", "handle_broadcast", "task=" . json_encode($task));
        return $this->pack([]);
    }

    /**
     *  处理单推
     */
    private function handle_push($task)
    {
        if (!isset($task['channel'])) {
            return $this->pack([], [], -1, 'params channel required');
        }
        if (!isset($task['fd'])) {
            return $this->pack([], [], -1, 'params fd required');
        }
        if (!isset($task['data'])) {
            return $this->pack([], [], -1, 'params data required');
        }
        $channel = $task['channel'];
        if (!$this->_table->exist($channel)) {
            return;
        }
        $data = $task['data'];
        $fd = $task['fd'];
        $raw_msg = $this->pack(['channel' => $channel, 'event' => 'push'], $data);
        $this->push($fd, $raw_msg);
        $this->log("info", "handle_push", "task=" . json_encode($task));
        return $this->pack([]);
    }

    /**
     *  处理状态查询
     */
    private function handle_status($task)
    {
        $status = [];
        foreach ($this->_table as $channel => $fds) {
            $status[$channel] = $fds['fds'];
        }
        $this->log("info", "handle_status", "task=" . json_encode($task));
        return $this->pack([], $status);
    }

    /**
    * 关闭ws链接回调
    */
    public function onClose($server, $fd)
    {
        // 不是websocket链接
        if (!$this->is_webscoket($fd)) {
            return;
        }
        foreach ($this->_table as $channel => $data) {
            $fds = explode(',', $data['fds']);
            if (array_search($fd, $fds) === false) {
                continue;
            }
            $this->unsub($channel, $fd);
        }
        $this->broadcast('internal.close', json_encode(['fd' => $fd]));
        $this->log("info", "user close the connection", "fd=$fd");
    }


    /**
     * 判断当前连接是否是websocket
     */
    private function is_webscoket($fd)
    {
        $info = $this->_server->connection_info($fd);
        if (empty($info) || !isset($info['websocket_status'])) {
            return false;
        }
        return $info['websocket_status'] != 0;
    }

    /**
     * 返回ws响应包
     */
    private function pack($req, $data = [], $code = 0, $message = '')
    {
        $res = [];
        $res['cid'] = 0;
        if (isset($req['cid'])) {
            $res['cid'] = $req['cid'];
        }
        $res['code'] = $code;
        if (isset($req['event'])) {
            $res['event'] = $req['event'];
        }
        if (isset($req['channel'])) {
            $res['channel'] = $req['channel'];
        }
        $res['message'] = $message;
        $res['data'] = $data;
        return json_encode($res);
    }

    /**
     * 指定fd推送消息
     */
    private function push($fd, $msg)
    {
        if ($this->_server->exist($fd)) {
            $this->_server->push($fd, $msg);
        } else {
            $this->log('error', "push failed fd is not exists", "fd=$fd");
        }
    }

    /**
     * 按照频道广播
     */
    private function broadcast($channel, $msg)
    {
        if (!$this->_table->exist($channel)) {
            return;
        }
        $fds = explode(",", $this->_table->get($channel, 'fds'));
        if (empty($fds)) {
            return;
        }
        $raw = $this->pack(['channel' => $channel, 'event' => 'push'], $msg);
        foreach ($fds as $fd) {
            $this->push($fd, $raw);
        }
    }

    
    /**
     * 订阅频道回调
     */
    private function sub($channel, $fd)
    {
        try {
            $this->_lock->lock();// 以下操作有竞态 需加锁
            $fds = [];
            if ($this->_table->exist($channel)) {
                $fds = explode(',', trim($this->_table->get($channel, 'fds')));
            }
            if (array_search($fd, $fds) != false) {
                return;
            }
            $fds[] = $fd;
            $this->_table->set($channel, ['fds' => trim(implode(',', $fds))]);
            $this->broadcast('internal.sub', json_encode(['channel' => $channel, 'fd' => $fd]));
            $this->log(__FUNCTION__, "info", "fd $fd sub channel $channel");
        } finally {
            $this->_lock->unlock(); // 确保解锁
        }
    }

    /**
     * 取消订阅频道回调
     */
    private function unsub($channel, $fd)
    {
        try {
            $this->_lock->lock(); // 以下操作有竞态 需加锁
            $fds = [];
            if ($this->_table->exist($channel)) {
                $fds = explode(',', trim($this->_table->get($channel, 'fds')));
            }
            $index = array_search($fd, $fds);
            if ($index === false) {
                return;
            }
            array_splice($fds, $index, 1);
            if (empty($fds)) {
                $this->_table->del($channel);
            } else {
                $this->_table->set($channel, ['fds' => trim(implode(',', $fds))]);
            }
            $this->broadcast('internal.unsub', json_encode(['channel' => $channel, 'fd' => $fd]));
            $this->log(__FUNCTION__, "info", "fd $fd unsub channel $channel");
        } finally {
            $this->_lock->unlock(); // 确保解锁
        }
    }

    /**
     * 记录日志
     */
    private function log()
    {
        $args = func_get_args();
        $msg = implode(",", $args);
        echo date('[Y-m-d H:i:s]') . "," . $msg . "\n";
    }
};

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值