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

news2024/9/20 17:02:01

目录

用到的工具:

PHP Swoole拓展  |  PHP Redis拓展  | Redis 7

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

给PHP安装Swoole和Redis拓展:

安装Redis软件

 二、创建websocket服务器文件"wss_server.php"

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

 三、通过php命令来运行服务端文件

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

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

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


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

用到的工具:

PHP Swoole拓展  |  PHP Redis拓展  | Redis 7

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

给PHP安装Swoole和Redis拓展:

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

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

php -v

 安装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() {

  }*/
})

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2140476.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

node.js+Koa框架+MySQL实现注册登录

完整视频展示&#xff1a;https://item.taobao.com/item.htm?ftt&id831092436619&spma21dvs.23580594.0.0.52de2c1bg9gTfM 效果展示&#xff1a; 一、项目介绍 本项目是基于node.jsKoamysql的注册登录的项目,主要是给才学习node.js和Koa框架的萌新才写的。 二、项目…

java数据结构----图

图的存储结构: 代码实现 public class Graph {// 标记顶点数目private int V;// 标记边数目private int E;// 邻接表private Queue<Integer>[] adj;public Graph(int v) {V v;this.E 0;this.adj new Queue[v];for (int i 0; i < adj.length; i) {adj[i] new Queu…

C++的类与对象中(主讲默认成员函数)

目录 1.类的默认成员函数 2.构造函数 1.全缺省构造函数 2.第7点中的对自定义类型的成员变量构造&#xff08;调用编译器自动生成的默认构造函数&#xff09; 3.析构函数 4.拷贝构造函数 5.运算符重载 1.概念 2.赋值运算符重载 6.const成员函数 1.类的默认成员函数 默…

微服务——网关路由(Spring Cloud Gateway)

网关路由 1.什么是网关 网关又称网间连接器、协议转换器&#xff0c;是在网络层以上实现网络互连的复杂设备&#xff0c;主要用于两个高层协议不同的网络之间的互连。网关就是网络的关口。数据在网络间传输&#xff0c;从一个网络传输到另一网络时就需要经过网关来做数据的路由…

【深度智能】:迈向高级时代的人工智能全景指南

​ ​ 前几天偶然发现了一个超棒的人工智能学习网站&#xff0c;内容通俗易懂&#xff0c;讲解风趣幽默&#xff0c;简直让人欲罢不能。忍不住分享给大家&#xff0c;人工智能立刻跳转&#xff0c;开启你的AI学习之旅吧&#xff01; 第一阶段&#xff1a;基础知识 1. 计算机科…

人脸防伪检测系统源码分享

人脸防伪检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

【Python基础】Python 装饰器(优雅的代码增强工具)

本文收录于 《Python编程入门》专栏&#xff0c;从零基础开始&#xff0c;分享一些Python编程基础知识&#xff0c;欢迎关注&#xff0c;谢谢&#xff01; 文章目录 一、前言二、装饰器基础三、语法糖 四、带参数的装饰器五、多层装饰器六、总结 一、前言 在Python编程的世界里…

【手撕】快排-分治

1. 颜色分类 . - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/sort-colors/description/ 代码 class Solution {public…

Neo4j入门案例:三星堆

创建一个关于三星堆的知识图谱可以是一个非常有趣的项目&#xff0c;它可以帮助理解如何使用Neo4j来存储和查询复杂的关系数据。三星堆文化以其独特的青铜器、金器和其他文物而闻名&#xff0c;这为我们提供了一个丰富的历史背景来构建知识图谱。 数据模型定义 实体类型&#…

数据安全查询-—SAAS本地化及未来之窗行业应用跨平台架构

一、数据库安全查询 默认数据库查询是不区分大小写的&#xff0c;这样无法区分Mm&#xff0c;Admin&#xff0c;admin 二、thinkphp 区分大小写 $condition "binary appdhj_sn {$应用sn}"; 三、原始mysql select * from TableA where binary columnA aaa; 四、…

mtk7628 网口灯问题

板子上电插入网线到网口&#xff0c;只有wan口灯会亮&#xff0c;插入lan口灯不会亮。对比了ok的代码&#xff0c;先对比设备树&#xff0c;未看到网口相关的GPIO。 mt7628an_WMD-7688A-12816.dts mt7628an_hilink_hlk-7628n.dts 继续查看网口相关代码&#xff0c;加打印&…

Android应用程序启动源码分析

文章目录 Android应用程序启动源码分析一、启动流程二、Launcher通知AndroidOS(用户点击图标)2.1 Activity.java2.2 Instrumentation.java2.3 ActivityTaskManagerService.java2.4 ActivityStarter.java2.5 RootWindowContainer.java2.5.1 Task.java2.5.2 TaskFragment.java 2.…

基于python+django+vue的个性化餐饮管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于pythondjangovueMySQL的视…

勇于尝试,永远行动 - 《洛克菲勒写给儿子的38封信》读书笔记

两倍速听过好几遍的书&#xff0c;洛克菲勒的思想和志向&#xff0c;配得上他的成就。 “在尝试中寻找突破&#xff0c;在行动中成就自我。”这是洛克菲勒写给儿子的箴言&#xff0c;也是他一生的真实写照。在这38封信中&#xff0c;他不仅分享了自己的工作心得&#xff0c;更…

Docker安装mysql安装nginx安装Redis

Docker安装mysql 下载镜像 docker pull mysql:8.0注意,使用此方法安装镜像需要提前配置镜像源,详情看之前的文章 安装 docker run -d -p 3306:3306 \ --name mysql \ --restartalways \ --privilegedtrue \ -e TZAsia/Shanghai \ -e MYSQL_ROOT_PASSWORDroot \ mysql:8.0进…

【重学 MySQL】二十九、函数的理解

【重学 MySQL】二十九、函数的理解 什么是函数不同 DBMS 函数的差异函数名称和参数功能实现数据类型支持性能和优化兼容性和可移植性 MySQL 的内置函数及分类单行函数多行函数&#xff08;聚合函数&#xff09;使用注意事项 什么是函数 函数&#xff08;Function&#xff09;在…

【PHP代码审计】 PHP环境搭建

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 | 每天学会一个渗透测试工具 安装phpstudy 泥菩萨-CSDN博客 安装vscode 直接去官网下载安装包&#xff0c;然后双击安装即可。官网地址&#xff1a;htt…

24/9/15 kaggle数字识别器

这次做的是数字识别器&#xff0c;借鉴了大佬的文章Kaggle竞赛实战系列&#xff08;一&#xff09;&#xff1a;手写数字识别器&#xff08;Digit Recognizer&#xff09;得分99.53&#xff05;、99.91%和100%_手写数字识别实现 digist-CSDN博客 加入了自己对大佬里面步骤的解…

多模态大语言模型综述(中)-算法实用指南

本文是Multimodal Large Language Models: A Survey的译文之算法实用指南部分。 上&#xff1a;摘要、概念与技术要点实用指南中&#xff1a;算法实用指南(本文)下: 任务的实用指南(应用)、挑战等 原始信息 标题: Multimodal Large Language Models: A Survey译文: 多模态大…

Conda和pip 清空缓存

conda clean --all然后选y&#xff0c;如下图&#xff1a; pip cache purge # 清除所有缓存