基于 Swoole 开发实时在线聊天室(十二):加入和退出聊天室房间功能实现
今天我们接着上篇教程,继续介绍用户进入聊天室房间后触发进入房间事件,以及退出房间时触发退出房间事件的 Websocket 服务端实现。
进入房间
我们来看进入房间的实现,在 routes/websocket.php
中新增如下代码:
WebsocketProxy::on('room', function (WebSocket $websocket, $data) {
if (!empty($data['api_token']) && ($user = User::where('api_token', $data['api_token'])->first())) {
// 从请求数据中获取房间ID
if (empty($data['roomid'])) {
return;
}
$roomId = $data['roomid'];
// 重置用户与fd关联
Redis::command('hset', ['socket_id', $user->id, $websocket->getSender()]);
// 将该房间下用户未读消息清零
$count = Count::where('user_id', $user->id)->where('room_id', $roomId)->first();
$count->count = 0;
$count->save();
// 将用户加入指定房间
$room = Count::$ROOMLIST[$roomId];
$websocket->join($room);
// 打印日志
Log::info($user->name . '进入房间:' . $room);
// 更新在线用户信息
$roomUsersKey = 'online_users_' . $room;
$onelineUsers = Cache::get($roomUsersKey);
$user->src = $user->avatar;
if ($onelineUsers) {
$onelineUsers[$user->id] = $user;
Cache::forever($roomUsersKey, $onelineUsers);
} else {
$onelineUsers = [
$user->id => $user
];
Cache::forever($roomUsersKey, $onelineUsers);
}
// 广播消息给房间内所有用户
$websocket->to($room)->emit('room', $onelineUsers);
} else {
$websocket->emit('login', '登录后才能进入聊天室');
}
});
首先还是要确保用户已经登录(有人提议说用户认证实现可以通过中间件来处理,是的,后面我会统一优化下),然后要确保客户端传递过来了房间号,否则无法与指定聊天室房间关联。
接下来,我们通过 Redis 的 HSET
数据结构保存用户 ID 与 Websocket 连接的关联,以便后续可以通过用户 ID 获取对应的 Websocket 连接,以便给它发送未读消息计数:
Redis::command('hset', ['socket_id', $user->id, $websocket->getSender()]);
然后将本聊天室房间内该用户未读消息设置为 0,并通过 App\Services\WebSocket\Websocket
的 join
方法将其加入这个房间:
$room = Count::$ROOMLIST[$roomId];
$websocket->join($room);
这里的房间默认是基于 Redis
作为存储媒介的,你可以在 config/laravels.php
将其调整为 SwooleTable
(目前只支持这两种驱动):
'websocket' => [
'enable' => true,
'handler' => \App\Services\WebSocket\WebSocketHandler::class,
...
'drivers' => [
'default' => 'redis',
'table' => \App\Services\Websocket\Rooms\TableRoom::class,
'redis' => \App\Services\Websocket\Rooms\RedisRoom::class,
'settings' => [
'table' => [
...
],
'redis' => [
'server' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
'persistent' => true,
],
'options' => [
//
],
'prefix' => 'swoole:',
],
],
],
再然后,我们更新当前房间内在线用户信息,必将其保存到缓存里面,最后通过如下代码将在线用户信息广播给当前房间内的所有用户(包括自己):
$websocket->to($room)->emit('room', $onelineUsers);
这样,客户端在聊天室房间页面 resources/js/pages/Chat.vue
里面就可以通过如下代码接收服务端发送数据:
socket.on('room', function (obj) {
that.$store.commit('setUsers', obj);
});
这是进入房间的实现代码,下面我们再来看退出房间。
退出房间
退出房间是在客户端点击聊天室页面左上角返回按钮时触发的:
goback () {
const obj = {
name: this.userid,
roomid: this.roomid,
api_token: this.auth_token,
};
socket.emit('roomout', obj);
...
},
同样包含了用户信息,房间信息和认证信息,在服务端我们可以通过在 中编写如下路由来响应客户端 roomout
请求:
WebsocketProxy::on('roomout', function (WebSocket $websocket, $data) {
if (!empty($data['api_token']) && ($user = User::where('api_token', $data['api_token'])->first())) {
if (empty($data['roomid'])) {
return;
}
$roomId = $data['roomid'];
$room = Count::$ROOMLIST[$roomId];
// 更新在线用户信息
$roomUsersKey = 'online_users_' . $room;
$onelineUsers = Cache::get($roomUsersKey);
if (!empty($onelineUsers[$user->id])) {
unset($onelineUsers[$user->id]);
Cache::forever($roomUsersKey, $onelineUsers);
}
$websocket->to($room)->emit('roomout', $onelineUsers);
Log::info($user->name . '退出房间: ' . $room);
$websocket->leave([$room]);
} else {
$websocket->emit('login', '登录后才能进入聊天室');
}
});
和进入房间一样,我们需要确保用户已认证并且房间号不为空,然后更新在线用户信息(将当前用户剔除),最后通过如下代码离开房间并将更新后的用户信息广播给客户端所有在线用户(包括自己):
$websocket->to($room)->emit('roomout', $onelineUsers);
$websocket->leave([$room]);
在客户端聊天室页面 resources/js/pages/Chat.vue
中,可以通过如下代码接收 Websocket 服务端 roomout
响应:
socket.on('roomout', function (obj) {
that.$store.commit('setUsers', obj);
});
断开连接
除了用户在聊天室点击返回按钮外,当 Websocket 连接断开时,对应连接上的用户也要退出所有房间,进入离线状态。目前来看断开连接和退出房间的逻辑是一样的,因此可以共用同一套代码,我们编写 disconnect
路由并重构 roomout
路由实现代码如下:
WebsocketProxy::on('roomout', function (WebSocket $websocket, $data) {
roomout($websocket, $data);
});
WebsocketProxy::on('disconnect', function (WebSocket $websocket, $data) {
roomout($websocket, $data);
});
function roomout(WebSocket $websocket, $data) {
if (!empty($data['api_token']) && ($user = User::where('api_token', $data['api_token'])->first())) {
if (empty($data['roomid'])) {
return;
}
$roomId = $data['roomid'];
$room = Count::$ROOMLIST[$roomId];
// 更新在线用户信息
$roomUsersKey = 'online_users_' . $room;
$onelineUsers = Cache::get($roomUsersKey);
if (!empty($onelineUsers[$user->id])) {
unset($onelineUsers[$user->id]);
Cache::forever($roomUsersKey, $onelineUsers);
}
$websocket->to($room)->emit('roomout', $onelineUsers);
Log::info($user->name . '退出房间: ' . $room);
$websocket->leave([$room]);
} else {
$websocket->emit('login', '登录后才能进入聊天室');
}
}
客户端代码不用做任何调整,下面我们来测试用户进入房间和退出房间事件。
流程测试
为了便于演示用户进入和退出房间在线用户数的变化,我们新开一个浏览器(比如 Firefox)访问聊天室应用并注册一个新用户 test@xueyuanjun.com
,然后在 Chrome 浏览器中进入房间 1:
此时聊天室只有一个用户,然后我们在数据表 counts
中初始化新注册用户(ID=2)未读消息记录(后面可以将这个初始化流程自动处理):
然后在 Firefox 浏览器中也登录并进入房间 1:
可以看到聊天室房间 1 有两个用户了,再回到 Chrome 浏览器聊天室界面,在未刷新页面的情况下可以看到也已经变成两个在线用户了:
这是因为 Websocket 服务器发送广播消息时,推送给了 room1
频道(房间1)的所有客户端。
接下来我们在 Firefox 浏览器退出房间 1,然后在 Chrome 浏览器中可以看到在线用户数又变成 1 个了:
如果你在开发者面板通过 WS
标签查看 Websocket 通信记录的话,会看到在 Firefox 浏览器中加入和退出房间会在 Chrome 浏览器中收到对应的广播消息,反之亦然:
以上就是加入和退出聊天室房间的 Websocket 通信前后端交互介绍,下篇教程,我们将正式开始编写消息发送实现代码。
4 Comments
这里有一个错误 $roomId = $data['roomid']; $room = Count::$ROOMLIST[$roomId]; $roomId 直接用就可以了,这样获取一下$room就不是$room_id了,或者把model里的数组改成 [1 => 1, 2 => 2]
没问题 因为我之间创建房间的时候就是使用 room1、room2 这样的room id,并不是数字1、2
谢谢学院君提供的教程,下的新版的已经改过了,是我没仔细看
学院君,这篇教程中有个问题请教一下: WebsocketProxy::on('room', function (WebSocket $websocket, $data) { if (!empty($data['api_token']) && ($user = User::where('api_token', $data['api_token'])->first())) { //$data中并没有api_token这个值,后续代码我看你是通过 $userId = $websocket->getUserId(); 来进行登录房间的用户验证的。如果是基于$data来验证的话,如何给$data赋予api_token呢? 谢谢。