闲暇之余大概的看了一下 socketio.js 发现这个比传统的websocket更加方便、更加稳定;

但是socketio.js的官方服务端是node,当然文档中提供的有各种语言的服务端代码,但独独少了PHP,因为就PHP本身来说对socket的支持不算友好,但是现在有了swoole的加持,我们也可以提供很好的服务;

如果需要对socketio.js有更多的了解 可以查看下面链接:

https://socket.io/docs/v4/

对比js 中的 websocket 与 socketio.js

  • 传统的websocket 需要自己写心跳检测机制、而socketio有自带的心跳检测,只需要简单的配置就可以实现心跳机制;
  • socketio 有自带的断线重连机制;
  • socketio 可以自定义事件,只要服务端支持即可,所有的代码不用放在message处理了,使代码更好管理、更有层次;
  • 对平台兼容良好

总之 socketio 用起来确实比js 的websocket 更加方便、更加稳定;

这里简单说一嘴 其实thinkphp-swoole 扩展也对socketio提供了支持,我没有具体的看过,简单来说我觉得太过于复杂了,
人们对于未知的东西是不可控的,所以我决定自己写一个,以常人最简单的思维去实现,大家都能看得懂,方便后期维护和修改以及扩展,总之以最朴素的代码,最简洁的逻辑和方式来实现,不宜过度的封装,不宜使用什么trigger,event之类高大上的东西,以普通程序猿的常用思维来干一套!

本文以thinkphp6 + swoole来实现对socketio.js 的支持;下面我们来看一下效果

2022-06-24 15.49.29.gif

实现的场景是一对多私聊,对于公聊就更加简单了,不用维护uid与fd关系,这里就不写了,还有一个是群聊(房间),基础实现了实现房间也很简单;

  • 首先在appconfig 里面 增加一个swoole.php 配置,主要对端口以及swoole的一些基础设置,增加一个redis来绑定uid与fd的关系;
<?php
return [
    'websocket' => [
        'chat' => [
            'host' => '0.0.0.0',
            'port' => '9509',
            'process_name' => 'chart',
            'socket_config' => [
                'open_websocket_pong_frame' => true,
                'heartbeat_idle_time' => 60,
                'heartbeat_check_interval' => 30,
                'open_websocket_ping_frame' => true,
                'reactor_num' => 2,
                'worker_num' => 2,
                'task_worker_num' => 1,
                'daemonize' => false,
                //'log_file' => '/www/wwwlogs/run.log',
                //'pid_file' => '/www/wwwlogs/server.pid',
                'reload_async' => false,
                'dispatch_mode' => 5
            ],
            'ping_interval' => 5000,//实现对socket.io 心跳配置
            'ping_timeout' => 6000,//实现对socket.io 心跳超时配置
            //这里实现对socket.io事件回调类配置,需要自定义
            'event' => [
                'PrivateChat' => \server\swoole\chat\PrivateChat::class, //私聊
                'room' => \server\swoole\chat\Room::class //群聊
            ],
            'redis' => [
                'host' => '127.0.0.1',
                'port' => '6379',
                'timeout' => 5,
                'pass' => ''
            ],
            'room' => [
                'prefix_room_id' => 'chat_'
            ]
        ]
    ]
];
  • 实现swoole websocket 事件回调,对于不同websocket服务都可以调用它,只需要通过不同的name进行区分即可,使用call_user_func_array函数,对不同websocket服务进行事件自定义;
<?php

namespace server\swoole;

use server\Single;
use think\facade\Config;

class SwooleWebsocketServer
{
    use Single;

    public $ws = null;

    public $name;

    public $config = [];

    public function __construct($name = 'default')
    {
        $this->name = $name;

        $this->config = Config::load('swoole', 'config')['websocket'][$name];

        $this->ws = new \Swoole\WebSocket\Server($this->config['host'], $this->config['port']);

        $this->ws->set($this->config['socket_config']);

        if (isset($this->config['process_name']) && !empty($this->config['process_name'])) {
            swoole_set_process_name($this->config['process_name']);
        }
    }


    public function onConnect()
    {
        $this->ws->on('connect', call_user_func_array(["server\\swoole\\{$this->name}\\Event", "connect"], [$this->config]));
    }

    public function onOpen()
    {
        $this->ws->on('open', call_user_func_array(["server\\swoole\\{$this->name}\\Event", "open"], [$this->config]));
    }

    public function onMessage()
    {
        $this->ws->on('message', call_user_func_array(["server\\swoole\\{$this->name}\\Event", 'message'],[$this->config]));
    }


    public function onClose()
    {
        $this->ws->on('close', call_user_func_array(["server\\swoole\\{$this->name}\\Event", 'close'],[$this->config]));
    }

    public function onTask()
    {
        $this->ws->on('task', call_user_func_array(["server\\swoole\\{$this->name}\\Event", 'task'],[]));
    }

    public function onStart()
    {
        $this->ws->start();
    }


    public function start()
    {
        $this->onOpen();
        $this->onConnect();
        $this->onMessage();
        $this->onClose();
        $this->onTask();
        $this->onStart();
    }
}
  • 对事件的实现的trait类,不同websocket服务都可以引用该类或者重写该类
<?php

namespace server\swoole;

use server\swoole\socketIo\Packet;
use server\swoole\socketIo\Parser;

trait  Handle
{
    /**
     * @param array $config
     * @return \Closure
     *
     */
    public static function connect(array  $config) : \Closure
    {
        return function ($server,$fd) use ($config)
        {

        };
    }

    /**
     * @param array $config
     * @return \Closure
     *
     */
    public static function open(array $config = []): \Closure
    {
        return function ($server, $request) use ($config) {

            $payload = json_encode([
                'sid' => base64_encode(uniqid()),
                'upgrades' => [],
                'pingInterval' => $config['ping_interval'],
                'pingTimeout' => $config['ping_timeout'],
            ]);

            $initPayload = Packet::OPEN . $payload;
            $connectPayload = Packet::MESSAGE . Packet::CONNECT;
            $server->push($request->fd, $initPayload);
            $server->push($request->fd, $connectPayload);

        };
    }

    /**
     * @param array $config
     * @return \Closure
     *
     */
    public static function message(array $config = []): \Closure
    {
        return function ($server, $frame) use ($config) {

            $payload = Parser::decode($frame);

            if (Parser::execute($server, $frame)) {
                return;
            }
            ['event' => $event] = $payload;
            return call_user_func_array([$config['event'][$event], 'send'], [$server, $frame,$config]);
        };
    }


    /**
     * @param array $config
     * @return \Closure
     */
    public static function close($config = []) : \Closure
    {
        return function ($ws, $fd) use ($config) {
            echo "client-{$fd} is closed\n";
        };
    }

    /**
     * @return \Closure
     */
    public static function task() : \Closure
    {
        return function ($ws, $task_id, $reactor_id, $data) {

        };
    }
}

这里说一嘴,open 方法非常重要,主要就是实现 upgrade#I/websocket.发送noop消息,之前hana住的轮询请求返回,如果忽略会导致链接不成功

fcfaaf51f3deb48fcf1002b3421d3d202df57869.png

message 方法 call_user_func_array 函数主要实现对socket.io的事件回调处理,也就是我们config配置里面,根据事件名称调用不同的处理类

其他类都是针对于 socket.io 的收发消息进行转码和编码,由于文字有限,这里就不一一列出来了。放在源代码里面了,感兴趣的朋友可以自己去看下;

  • 利用`thinkphp启动服务,appcommand下建立一个 Chat.php 文件

<?php

namespace app\command;

use server\swoole\SwooleWebsocketServer;
use think\console\Command;
use think\console\Input;
use think\console\Output;

class Chat extends Command
{
    protected function configure()
    {
        $this->setName('chat')->setDescription('The chart');
    }

    protected function execute(Input $input, Output $output)
    {
        SwooleWebsocketServer::getInstance('chat')->start();
    }
}
  • 执行命令
php think chat
  • h5页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/element/plus/css/index.css">
</head>
<body>

<div >

    <div style="width: 100%; height: 200px; border: 1px solid #cccc77; border-radius: 10px; padding: 10px; margin: 10px">

    </div>

    <div style="margin-top: 10px;">
        <el-input type="textarea" v-model="message"></el-input>
    </div>

    <div>

        <el-button type="danger" @click="handleSend">发送消息</el-button>
    </div>

</div>

</body>
</html>

<script src="/static/js/socket.io-2.0.0.js"></script>
<script src="/static/vue/3.0.11/vue.global.js"></script>
<script src="/static/element/plus/js/index.full.js"></script>

<script>
    const App = {
        data:function () {
            return {
                socket:{},
                message:""
            };
        },
        methods:{
            initSocket(){
                this.socket = io('127.0.0.1:9509?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjgsImFjY291bnQiOiJ0YW5ndGFuZyIsImludml0ZV9jb2RlIjoiemJxMmRwIiwiZXhwIjoxNjU2MTM5MDgzfQ.-gJo4Q1mPdZAMSFPf25vUNqbEYM8eqfRlE36ZSNYbSk', {
                    path: '/ws',
                    transports: ['websocket']
                });

                this.socket.on("error", (error) => {
                    console.log(error)
                });

                this.socket.on('PrivateChat',async (aa) => {
                    console.log(aa)
                })
            },
            handleSend(){
                this.socket.emit("PrivateChat", {sender:8,to:-1,text:this.message}, (response) => {
                    console.log(response); // "got it"
                });
            },
        },
        mounted:function () {
            this.initSocket()
        }
    };
    const app = Vue.createApp(App);
    app.use(ElementPlus, { size: 'small', zIndex: 3000 });
    app.mount("#app");
</script>

总结一下:总之这里就做一个简单的介绍,源代码请看这里:

https://gitee.com/phpbloger/think-element/tree/master/extend/server/swoole

由于我demo中写了用户鉴权之类的东西,如果你们需要也可以自己定义实现一下,对于后台展示的聊天界面,由于只写了一个简单的demo,也没上传了,如果自己需要可以下方留言;