目录
PHP Swoole拓展 | PHP Redis拓展 | Redis 7
二、创建websocket服务器文件"wss_server.php"
附:小程序端连接websocket代码(需要将上面的服务端调整为只发给指定fd用户才行)
Sham为了实现小程序中福利票券核销扫码时,用户能实时更新票券被核销的信息,所以学习使用websocket,以下通过搭建一个简单聊天室来记录备忘。
用到的工具:
PHP Swoole拓展 | PHP Redis拓展 | Redis 7
找到PHP软件,进入“设置”,找到“安装拓展”,找到redis和Swoole4,点右面的安装,等待安装结束
这里要注意系统默认的php版本是哪个,那么就安装哪个版本对应的拓展,查看php版本命令:
php -v
如果用的宝塔面板,直接搜索Redis,然后安装等待结束即可
具体看代码后面注释,主要注意点:
- Swoole用户连接后会生成唯一的fd(int格式的),需要缓存用户数据
- 这里通过Redis来缓存websocket连接用户的数据,然后再后面调取
这里Sham用的Redis的哈希表数据,通过hSet存储,然后hGet获取,- 这段代码是用于聊天室功能,用户发送消息后,会推送给发送者以外的其他所有在线用户。
- 里面user_id,message需要和后面的chat.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();
进入ssh或者创建定时任务执行shell都可以,运行如下命令,假设"wss_server.php"文件存放在"www/wwwroot"目录下
php /www/wwwroot/wss_server.php
如果没有报错等信息,则表示运行成功了
具体看代码后面注释,主要注意点:
- 为了防止因为空闲而导致与websocket服务器断开连接,需要设置心跳和重连(见代码)
- 这里因为是简易版,所有通过网址传参来通过user_id定于用户名,以区分用户,可修改其他方式,比如登录来获取
- "en">
-
- "UTF-8">
- "X-UA-Compatible" content="IE=edge">
- "viewport" content="width=device-width, initial-scale=1.0">
-
简易聊天By websocket -
- #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;
- }
-
-
-
-
简易聊天By websocket
-
- "dialogue">
-
-
-
-
- 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 += '
已连接服务器!
'; //输出信息 - startHeartbeat(); // 开始心跳
- };
- //监听收到websocket发来的信息时
- ws.onmessage = (event) => {
- console.log('收到消息:', event.data);
- var data = JSON.parse(event.data); //这里要注意data是要为json字符串,否则会报错
- dialogue.innerHTML += ""+data['from_user']+": "+data['messages']+"" ;
- };
- //监听当websocket断开时
- ws.onclose = () => {
- console.log('WebSocket 连接已关闭');
- dialogue.innerHTML += '
与服务器断开了!重新连接中……
' ; - 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 += `
class="show-right">${document.getElementById('content').value}: 你
`; - //清空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);
- }
-
-
-
完成上面的文件,通过http://aa.com/chat.php?userid=xxx来访问上面的前端,其中xxx表示用户名,如果显示“已连接服务器”,则表示已经成功了,然后就可以发给别人来小小的聊下天啦。
注意:这个人员&聊天数据都是暂存的,关掉前端 或者重启websocket服务端都会导致数据重置清空。
写在结尾:
初次结束websocket和swoole,目前只能实现简单信息发送,暂时能满足小程序中需求,后续用到更多功能的时候再来学习。
- 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() {
- }*/
- })