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文件
具体看代码后面注释,主要注意点:
- 为了防止因为空闲而导致与websocket服务器断开连接,需要设置心跳和重连(见代码)
- 这里因为是简易版,所有通过网址传参来通过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() {
}*/
})
最新评论
感谢大佬,非常棒的自学资料,向大佬学习!
您的行政服务小程序V2正好我们有需求,能否给个联系方式沟通下呢?谢谢!
想咨询楼主
牛逼的楼主 感谢分享 学习学习
public function UpdateDomainRecord($ip)这里会报错
学习学习
新生进来学习
目前正在找食堂报餐的小程序,看了下评论发现楼主真的是行政文员,真的太牛逼,让我不得不敬佩!我会一直关注着您的