I'm Sham
一个在通往码农道路上走走停停的行政文员

PHP Swoole实现简易聊天室,附加小程序端连接websocket简易代码

 Sham为了实现小程序中福利票券核销扫码时,用户能实时更新票券被核销的信息,所以学习使用websocket,以下通过搭建一个简单聊天室来记录备忘。

用到的工具:
PHP Swoole拓展  |  PHP Redis拓展  | Redis 7

一、 首先安装上述必要工具(下面是以宝塔面板中操作为例)

1. 给PHP安装Swoole和Redis拓展:

找到PHP软件,进入“设置”,找到“安装拓展”,找到redis和Swoole4,点右面的安装,等待安装结束

这里要注意系统默认的php版本是哪个,那么就安装哪个版本对应的拓展,查看php版本命令:

php -v

 2. 安装Redis软件

如果用的宝塔面板,直接搜索Redis,然后安装等待结束即可

 二、下面就开始创建websocket服务器文件”wss_server.php”

具体看代码后面注释,主要注意点:

  • Swoole用户连接后会生成唯一的fd(int格式的),需要缓存用户数据
  • 这里通过Redis来缓存websocket连接用户的数据,然后再后面调取
    这里Sham用的Redis的哈希表数据,通过hSet存储,然后hGet获取,
  • 这段代码是用于聊天室功能,用户发送消息后,会推送给发送者以外的其他所有在线用户。
  • 里面user_id,message需要和后面的chat.php对应
<?php
    
    use Swoole\WebSocket\Server;
    use Swoole\WebSocket\Frame;
    
    // 创建 Redis 客户端实例
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379); //端口号见Redis软件设置,默认是6379
    $redis->auth('xxxx');  //这里如果redis设置里密码,则增加这项

    // 创建 WebSocket 服务器
    $ws = new Server("0.0.0.0", 8040);
    
    // 设置服务器配置
    $ws->set([
        'heartbeat_check_interval' => 60,
        'heartbeat_idle_time' => 600,
    ]);
    
    // 监听 Worker 启动事件
    $ws->on('WorkerStart', function (Server $server, int $workerId) {
    // 可以在这里做一些初始化工作
    });
    
    // 监听连接事件
    $ws->on('open', function (Server $server, $request) use (&$redis) {
        // 当新用户连接时,记录用户信息
        $user_id = $request->get['user_id'] ?? null;
        $fd = $request->fd;
        if ($user_id) {
            // 将用户 ID 和 fd 存储到 Redis 哈希表
            //可以理解为类似数组中,online_users表示数组名称,$request->fd 表示key值,$user_id表示value值
            $redis->hSet('online_users', $request->fd, $user_id);
            //在服务器后端显示哪个用户上线了
            $msgs = [
                "from_user" => $user_id,    //显示消息是来自哪个用户
                "messages"=>"我上线啦!"
            ];
            //给所有用户广播谁上线了
            broadcast($server,$redis, json_encode($msgs),$request->fd);
        //var_dump($redis);
        } else {
            //echo "未提供用户 ID,fd: $fd\n";
        }
    });
    
    // 监听消息事件
    $ws->on('message', function (Server $server, Frame $frame) use (&$redis) {
        //将收到的数据进行转换
        $data = json_decode($frame->data, true);
        if (isset($data['message'])) {
            $msgs = [
                //从哈希表中获取用户
                "from_user" => $redis->hGet('online_users',$frame->fd),     //显示消息是来自哪个用户
                "messages"=>$data['message']
            ];
             // 当用户发送消息时,广播给所有在线用户
            broadcast($server,$redis, json_encode($msgs), $frame->fd);
        }
    });
    
    // 监听关闭事件
    $ws->on('close', function (Server $server, $fd) use (&$redis) {
        // 当用户断开连接时,从在线用户列表中移除该用户
        $msgs = [
            "from_user" => $redis->hGet('online_users',$fd),         //显示消息是来自哪个用户
            "messages"=>"我下线啦!"
        ];
        //从redis的哈希表中删除用户
        $redis->hDel('online_users', $fd);
         // 用户离线时,广播给所有在线用户
        broadcast($server,$redis, json_encode($msgs),$fd);
    });
    
    
    // 广播消息给所有在线用户
    function broadcast(Server $server,$redis, $msg, $excludeFd = null) {
        // 从redis的哈希表中获取所有在线用户的 FD
        $onlineUsers = $redis->hKeys('online_users');
        // 遍历在线用户并发送消息
        foreach ($onlineUsers as $fds) {
            //判断不是当前用户,同时用户在线时推送信息
            if ($fds != $excludeFd && $server->isEstablished($fds)) {
                //给对应fds用户发送消息
                $server->push($fds, $msg);
            }
        }
    }
    // 启动服务器
    $ws->start();

三、创建完”wss_server.php”,然后我们需要通过php命令来运行它

进入ssh或者创建定时任务执行shell都可以,运行如下命令,假设”wss_server.php”文件存放在”www/wwwroot”目录下

php /www/wwwroot/wss_server.php

如果没有报错等信息,则表示运行成功了

四、下面创建前端的chat.php文件

具体看代码后面注释,主要注意点:

  1. 为了防止因为空闲而导致与websocket服务器断开连接,需要设置心跳和重连(见代码)
  2. 这里因为是简易版,所有通过网址传参来通过user_id定于用户名,以区分用户,可修改其他方式,比如登录来获取
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>简易聊天By websocket</title>
        <style>
            #dialogue{
                width:600px;
                height:500px;
                background:#eee;
                padding:10px 15px;
                overflow-y: scroll;
                border:5px solid #ccc;
            }
            .show-right{
                text-align: right;
            }
            .show-left{
                text-align: left;
            }
            #content{
                width:600px;
                height:100px;
                border:5px solid #ccc;
                margin-top:20px;
                font-size:16px;
                display: block;
            }
        </style>
    </head>
    <body>
        <h1>简易聊天By websocket</h1>
        
        <div id="dialogue"></div>
        <textarea id="content" placeholder="在此输入信息"></textarea>
        <button onclick="sendMessage()">发送信息</button>
        
        <script>
            let ws;
            let heartBeatTimer; // 心跳计时器
            let reconnectTimer; // 重连计时器
            let HEARTBEAT_INTERVAL = 15000; // 心跳间隔时间(15秒)
            let RECONNECT_INTERVAL = 5000; // 重连间隔时间(5秒)
            let url = new URL(window.location.href);
            // 使用URLSearchParams获取查询参数
            let params = new URLSearchParams(url.search);
            // 获取特定的参数值
            let userid = params.get('userid');
            //执行连接服务器
            connectWebSocket();
            
            function connectWebSocket() {
                // 创建 WebSocket 连接
                //这里需要改成自己的ws地址,同时带上参数user_id(这个与上面server中的一致)
                //如果要使用wss,可以通过带SSL证书的域名通过反代到websoket地址即可
                ws = new WebSocket('ws://xxx.com?user_id='+userid);  //
                var dialogue = document.getElementById('dialogue')
                
                //监听websocket打开
                ws.onopen = () => {
                    console.log('WebSocket 连接成功');
                    dialogue.innerHTML += '<p>已连接服务器!</p>'; //输出信息
                    startHeartbeat(); // 开始心跳
                };
                //监听收到websocket发来的信息时
                ws.onmessage = (event) => {
                    console.log('收到消息:', event.data);
                    var data = JSON.parse(event.data); //这里要注意data是要为json字符串,否则会报错
                    dialogue.innerHTML += "<strong>"+data['from_user']+"</strong>: "+data['messages']+"</p>" ; 
                };
                //监听当websocket断开时
                ws.onclose = () => {
                    console.log('WebSocket 连接已关闭');
                    dialogue.innerHTML += '<p style="color:red">与服务器断开了!重新连接中……</p>' ; 
                    stopHeartbeat(); // 停止心跳
                    reconnectWebSocket(); // 尝试重连
                };
            }
            
            //发送消息
            function sendMessage() {
                //这里将信息组合成json对象
                const messages = {
                    //to_user_id: userid,  //这个如果是发给指定用户的话可以设置,然后服务器逻辑里加以判断,然后不广播,而是只给指定用户
                    message: document.getElementById('content').value  // 消息内容
                };
                //给websocket服务器发送信息,这里将json对象转成json字符串
                ws.send(JSON.stringify(messages));
                console.log('发送消息:', messages);
                //将自己发送的信息显示在右侧,与别人发送的分开来
                dialogue.innerHTML += `<p class="show-right">${document.getElementById('content').value}: <strong>你</strong></p>`; 
                //清空textarea内容,方便再次输入
                document.getElementById('content').value=""
            }
            

            // 开始心跳
            function startHeartbeat() {
                stopHeartbeat(); // 防止多次启动
                //根据前面设置的时间来定时循环给服务器发信息,防止空闲导致断开连接
                heartBeatTimer = setInterval(() => {
                    //当已经启动ws并连接状态时
                    if (ws && ws.readyState === ws.OPEN) {
                        //给websocket服务器发送信息
                        ws.send({
                            data: JSON.stringify({ type: 'ping' }), // 发送心跳包
                            success() {
                                console.log('心跳包已发送');
                            },
                            fail(err) {
                                console.error('心跳包发送失败:', err);
                                reconnectWebSocket(); // 如果发送失败,尝试重连
                            }
                        });
                    }else{
                        console.log('心跳包未启动')
                    }
                }, HEARTBEAT_INTERVAL);
            }
            
            // 停止心跳
            function stopHeartbeat() {
                if (heartBeatTimer) {
                    clearInterval(heartBeatTimer);
                    heartBeatTimer = null;
                }
            }
            
            // 尝试重连
            function reconnectWebSocket() {
                if (reconnectTimer) return; // 防止重复重连
                //根据前面设置的时间,延迟几秒重新连接服务器
                reconnectTimer = setTimeout(() => {
                    console.log('尝试重新连接 WebSocket');
                    connectWebSocket(); // 重新建立连接
                    reconnectTimer = null; // 重置重连计时器
                }, RECONNECT_INTERVAL);
            }
        </script>
        
    </body>
</html>

五、完成上面的文件,通过http://aa.com/chat.php?userid=xxx来访问上面的前端,其中xxx表示用户名,如果显示“已连接服务器”,则表示已经成功了,然后就可以发给别人来小小的聊下天啦。

注意:这个人员&聊天数据都是暂存的,关掉前端 或者重启websocket服务端都会导致数据重置清空。

写在结尾:

初次结束websocket和swoole,目前只能实现简单信息发送,暂时能满足小程序中需求,后续用到更多功能的时候再来学习。

附:小程序端连接websocket代码(需要将上面的服务端调整为只发给指定fd用户才行)

let use_socket=true; //这个是用来判断是否需要连接websocket的
let socket; // WebSocket 连接对象
let heartBeatTimer; // 心跳计时器
let reconnectTimer; // 重连计时器
const HEARTBEAT_INTERVAL = 15000; // 心跳间隔时间(15秒)
const RECONNECT_INTERVAL = 1000; // 重连间隔时间(5秒)

  Page({
  data: {
  },

  // 初始化 WebSocket 连接
    connectWebSocket() {
    var that =this;
    //定义socket来给后面使用
    socket = wx.connectSocket({
      url: 'wss://xxx.com?&user_id='+user_info[0].user_id, // 替换为你的 WebSocket 服务器地址,小程序需要通过wss连接,可以通过前面提到的ssl证书域名反代访问
      success() {
        console.log('WebSocket 连接成功');
      },
      fail(err) {
        console.error('WebSocket 连接失败:', err);
        that.reconnectWebSocket(); // 尝试重连
      }
    });

    // 监听 WebSocket 连接成功
    socket.onOpen(() => {
      var that=this;
      console.log('WebSocket 已打开');
      that.startHeartbeat(); // 开始心跳
    });

    // 监听 WebSocket 连接关闭
    socket.onClose(() => {
      var that=this;
      console.log('WebSocket 已关闭');
      //当断开后任允许连接的话,则重连(这里Sham是发现不加判断的话,退出当前页之后还是会自动重连socket)
      if(use_socket){
        that.stopHeartbeat(); // 停止心跳
        that.reconnectWebSocket(); // 尝试重连
      }
    });

    // 监听 WebSocket 错误
    socket.onError((err) => {
      var that=this;
      console.error('WebSocket 错误:', err);
      that.reconnectWebSocket(); // 尝试重连
    });

    // 监听 WebSocket 消息
    socket.onMessage((message) => {
      var datas = JSON.parse(message.data);
      //console.log(message)  //保险点打印数据来核对是否符合要求
      var that = this;
      //这里要确保发送的消息是json字符串格式的,不然会报错
      var wss_res = JSON.parse(datas.messages);
      if(wss_res.id !=null && wss_res.status !=null){
        //这里以福利票券为例
        var welfare_list = that.data.welfare_list;
        //通过filter来筛选出指定id的票券
        var show_qrcode_welfare = welfare_list.filter(item => item.id == wss_res.id)
        var msg = show_qrcode_welfare[0].welfare_name
        //弹出提醒对应票券已经被扫码核销
        wx.showToast({
          title: msg+' - 已被扫码!',
          icon:'none',
          duration:2500
        })
        //刷新数据(会从服务器重新获取数据)
        setTimeout(function () {
          that.onShow()
        }, 2500)

      }
      console.log(JSON.parse(message.data));
      // 处理收到的消息
    });
  },

  // 开始心跳
  startHeartbeat() {
    var that=this;
    that.stopHeartbeat(); // 防止多次启动
    heartBeatTimer = setInterval(() => {
      if (socket && socket.readyState === socket.OPEN) {
        socket.send({
          data: JSON.stringify({ type: 'ping' }), // 发送心跳包
          success() {
            console.log('心跳包已发送');
          },
          fail(err) {
            console.error('心跳包发送失败:', err);
            that.reconnectWebSocket(); // 如果发送失败,尝试重连
          }
        });
      }
    }, HEARTBEAT_INTERVAL);
  },

  // 停止心跳
  stopHeartbeat() {
    if (heartBeatTimer) {
      clearInterval(heartBeatTimer);
      heartBeatTimer = null;
    }
  },

  // 尝试重连
  reconnectWebSocket() {
    var that=this;
    if (reconnectTimer) return; // 防止重复重连
    reconnectTimer = setTimeout(() => {
      console.log('尝试重新连接 WebSocket');
      that.connectWebSocket(); // 重新建立连接
      reconnectTimer = null; // 重置重连计时器
    }, RECONNECT_INTERVAL);
  },

  /**
  * 生命周期函数--监听页面加载
  */
  onLoad(options) {
    var that = this;
    that.connectWebSocket();
  }
  },

  /**
  * 生命周期函数--监听页面初次渲染完成
  */
  onReady() {

  },

  /**
  * 生命周期函数--监听页面显示
  */
  onShow() {
    var that = this;
    //页面显示后就执行心跳
    that.startHeartbeat();

  /**
  * 生命周期函数--监听页面隐藏
  */
  onHide() {
    //页面隐藏时停止心跳并设置不再连接socket
    that.stopHeartbeat();
    clearInterval(heartBeatTimer);
    heartBeatTimer = null;
    use_socket=false
    wx.closeSocket({
      success() {
        console.log('WebSocket连接关闭成功');
      },
      fail(error) {
        console.error('WebSocket连接关闭失败', error);
      }
    });
  },

  /**
  * 生命周期函数--监听页面卸载
  */
  onUnload() {
    //页面销毁时停止心跳并设置不再连接socket
    var that=this;
    that.stopHeartbeat();
    clearInterval(heartBeatTimer);
    heartBeatTimer = null;
    use_socket=false
    wx.closeSocket({
      success() {
        console.log('WebSocket连接关闭成功');
      },
      fail(error) {
        onsole.error('WebSocket连接关闭失败', error);
      }
    });
  },

  /**
  * 页面相关事件处理函数--监听用户下拉动作
  */
  onPullDownRefresh() {

  },

  /**
  * 页面上拉触底事件的处理函数
  */
  onReachBottom() {

  },

  /**
  * 用户点击右上角分享

  onShareAppMessage() {

  }*/
})

赞(0) 赏杯咖啡!
未经允许不得转载:Sham@双目瞿 » PHP Swoole实现简易聊天室,附加小程序端连接websocket简易代码

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

如果你觉得文章好,请赏1杯速溶咖啡给Sham吧!

微信扫一扫打赏