Laravel + swoole + redis 实现一对一聊天
最终效果如下👇(笔记本屏幕比较小😂)
前言
练手项目,代码目前只完成了聊天部分,还有其他部分没有写完,有不完善的地方请多多指教。😅
实现功能
- 会话列表
- 未读
- 阅读
- 聊天
- 文字消息
- 图片消息
- emoji
待实现功能
- 发送失败
- 发送失败标记
- 重新发送
- 撤回
- 聊天记录定时保存到数据库
Redis数据
Key 类型 描述 值 laravel_database_(user_id) String 存储client id,以用户的id为key。发送消息时,根据id取出client id然后发送消息。 laravel_database_online_(user_id) String 存储当前时间,五分钟后过期,用于判断用户是否在线。可以在发送心跳的时候更新这个值,close时清除该值。 laravel_database_logs_(会话id) List 以dialog_id为key,存储聊天记录。 laravel_database_logs_id_(会话id) Hash 记录msg id在list中的索引(index)值 laravel_database_msg_group_(user_id) Hash 以用户id为key,记录该用户的会话列表 - laravel_database_msg_group_(user_id)
{ "dialog_id": "2349643881_2535180514", # 两个用户的唯一会话id, 用于聊天记录的查询 "body": "你好 # 最后一条消息内容,根据msg_type将展示不同的内容 "created_at": "2022-06-28 22:06:39", # 最后一条消息的时间 "no_read": 0, # 未读的数量 "msg_type": 1, # 消息类型,1是文字消息,2是图片 "model": { # 聊天对象 "uuid": 2535180514, "name": "Sheldon Zboncak", "avatar": "avatar" } }
- laravel_database_logs_(会话id)
{ "class": "admin", "function": "say", "from_id": 2349643881, # 发送者的id "from_model": "admin", # 发送者的模型 "from": { # 发送者的信息 "uuid": 2349643881, "name": "Admin", "avatar": "avatar" }, "to_id": 2535180514, # 接收者的id "to_model": "user", # 接收者的模型 "to": { # 接收者的信息 "uuid": 2535180514, "name": "Sheldon Zboncak", "avatar": "avatar" }, "body": "/uploads/images/201710/14/1/ZqM7iaP4CR.png", # 消息内容, "msg_type": 2, # 消息类型 "uuid": "43n2657oxv60000000", # 前端生成的消息的唯一id "has_read": false, # 是否阅读 "read_at": "", # 阅读时间 "dialog_id": "2349643881_2535180514", # 会话id "created_at": "2022-06-28 22:06:39" # 创建时间 }
SwooleHandler
class SwooleHandler
{
public $chatCache;
public function __construct()
{
$this->chatCache = new ChatCache();
}
public function onMessage(Server $server, Frame $frame)
{
$content = $frame->data;
$content = json_decode($content);
$myFD = $frame->fd;
switch ($content->function) {
# 前端每十秒发送一次心跳,接受到心跳后,更新在线时间
case 'ping': #{"class":"user","function":"ping","user":{"id":1376688201,"name":"Admin"}}
$uuid = $content->user->id;
$server->push($myFD, Message::ping());
$this->chatCache->setOnline($uuid);
break;
# 前端在onopen时发送user信息,后台将user id与fa绑定,更新redis中的数据(未关闭旧设备连接),并更新在线时间
case 'login': #{"class":"user","function":"login","user":{"id":1376688201,"name":"Admin"}}
$uuid = $content->user->id;
$this->chatCache->bindUuid($uuid, $myFD);
$server->bind($myFD, $uuid);
$server->push($myFD, Message::login());
$this->chatCache->setOnline($uuid);
break;
# 根据user_id获取用户是否在线
case 'is_online': #{"class":"user","function":"is_online","user":{"id":1376688201,"name":"Admin"}}
$uuid = $content->user->uuid;
$status = $this->chatCache->isOnline($uuid);
$server->push($myFD, Message::isOnline($status));
break;
# 当用户处于对应的聊天页面时,或正在与该用户聊天时。前端将收到的消息立即发送回来,后端接收到后,根据msg的uuid更新list中的has_read和read_at两个字段
case 'read':#{"class":"user","function":"read","from_id":1420677271,"to_id":2455347791,"uuid":"81b7a11b-a3e7-463a-af0b-dc0bf64883c6"}
$dialogId = $content->dialog_id;
if ($content->uuid) {
# 根据会话id与msg的uuid获取该条记录的索引
$index = $this->chatCache->getLogId($dialogId, $content->uuid);
# 根据索引获取聊天记录
$log = $this->chatCache->getOneLogByIndex($dialogId, $index);
# 更新聊天记录
$log = json_decode($log);
$log->has_read = true;
$log->read_at = now()->toDateTimeString();
$this->chatCache->setLogByIndex($dialogId, $index, $log);
# 更新会话列表
$group = $this->chatCache->getOneOfMsgGroup($content->to_id, $content->from_id);
$group = json_decode($group);
$group->no_read = 0;
$this->chatCache->setMsgGroup($content->to_id, $content->from_id, $group);
}
break;
# 点击会话列表时,阅读所有的未读消息。前端会先根据no_read字段的值判断是否需要发送readAll消息
case 'readAll':#{"class":"user","function":"read","dialog_id":"254856121_1376688201","body":"aaaaaaaaaaaaaaaaa","created_at":"2022-06-17 15:59:15","no_read":0,"model":{"uuid":254856121,"name":"Admin"}}
$dialogId = $content->dialog_id;
$noRead = $content->no_read;
$uuid = $content->model->uuid;
# 判断是否存在未读消息
if ($noRead) {
# 根据no_read数量获取对应数量的记录
$logs = $this->chatCache->getSomeLogsBy($dialogId, -1, $noRead);
foreach ($logs as $log) {
$log = json_decode($log);
if ($log->from_id == $content->model->uuid) {
$index = $this->chatCache->getLogId($dialogId, $log->uuid);
$log->has_read = true;
$log->read_at = now()->toDateTimeString();
$this->chatCache->setLogByIndex($dialogId, $index, $log);
}
}
$myUuid = str_replace('_', '', $dialogId);
$myUuid = str_replace($uuid, '', $myUuid);
$group = $this->chatCache->getOneOfMsgGroup($myUuid, $uuid);
$group = json_decode($group);
$group->no_read = 0;
$this->chatCache->setMsgGroup($myUuid, $uuid, $group);
}
break;
# 发送消息。根据收,发人的id生成唯一会话id,处理数据存入redis list中,添加或更新发送者和接收者的会话列表
case 'say':#{"class":"user","function":"say","from":{"id":1376688201,"name":"Admin"},"from_id":1376688201,"from_model":"user","to":{"id":254856121,"name":"Admin"},"to_id":254856121,"to_model":"lawyer","body":"aaaaaaaaaaaaaaaaa","uuid":"b580320d-1b76-4df9-87a4-0c6e200e9910","msg_type":1,"created_at":""}
$dialogId = get_dialog_id($content->from_id, $content->to_id);
$uuid = $content->from_id;
$data = Message::say($content, $dialogId);
$num = $this->chatCache->setLogBy($dialogId, $data);
$this->chatCache->setLogId($dialogId, $content->uuid, $num - 1);
$group = [
'dialog_id' => $dialogId,
'body' => $content->body,
'created_at' => now()->toDateTimeString(),
'no_read' => 0,
'msg_type' => $content->msg_type,
'model' => [
'uuid' => $content->to_id,
'name' => $content->to->name,
'avatar' => $content->to->avatar,
],
];
$this->chatCache->setMsgGroup($uuid, $content->to_id, $group);
if ($this->chatCache->groupIsExists($content->to_id, $uuid)) {
$toGroup = $this->chatCache->getOneOfMsgGroup($content->to_id, $uuid);
$toGroup = json_decode($toGroup);
} else {
$toGroup = (object)[
'dialog_id' => $dialogId,
'body' => $content->body,
'created_at' => now()->toDateTimeString(),
'no_read' => 0,
'msg_type' => $content->msg_type,
'model' => [
'uuid' => $content->from_id,
'name' => $content->from->name,
'avatar' => $content->from->avatar,
],
];
}
$toGroup->body = $group['body'];
$toGroup->created_at = $group['created_at'];
$toGroup->no_read += 1;
$this->chatCache->setMsgGroup($content->to_id, $uuid, $toGroup);
$toFd = $this->chatCache->getFdBy($content->to_id);
if ($toFd && $server->isEstablished($toFd)) {
$server->push($toFd, json_encode($data));
}
$data['function'] = 'said';
$server->push($myFD, json_encode($data));
break;
case 'logs':#{"class":"user","function":"logs","dialog_id":"254856121_1376688201","page":"1","per_page":"10"}
$cacheData = $this->chatCache->getPaginateLogsBy($content->dialog_id, $content->page, $content->per_page);
$logs = [];
foreach ($cacheData['data'] as $datum) {
$logs[] = json_decode($datum);
}
$server->push($myFD, Message::logs($logs));
break;
}
}
}
ChatCache
class ChatCache
{
public $cache;
public function __construct()
{
$this->cache = Redis::connection();
}
public function bindUuid($uuid, $fd)
{
$this->cache->set($uuid, $fd);
}
public function getFdBy($uuid)
{
return $this->cache->get($uuid);
}
public function setOnline($uuid)
{
$key = 'online_' . $uuid;
$this->cache->setex($key, 5 * 60, now()->toDateTimeString());
}
public function setOffline($uuid)
{
$key = 'online_' . $uuid;
$this->cache->del($key);
}
public function isOnline($uuid)
{
$key = 'online_' . $uuid;
return $this->cache->exists($key);
}
public function setLogBy($dialogId, $data)
{
$key = 'logs_' . $dialogId;
return $this->cache->lpush($key, json_encode($data));
}
public function setLogByIndex($dialogId, $index, $data)
{
$key = 'logs_' . $dialogId;
return $this->cache->lset($key, $index, json_encode($data));
}
public function getPaginateLogsBy($dialogId, $page = 1, $perPage = 15)
{
$total = $this->getLogLen($dialogId);
$pageCount = ceil($total / $perPage);
$start = ($page - 1) * $perPage;
$end = $start + $perPage - 1;
$data = $this->getSomeLogsBy($dialogId, $start, $end);
return [
'data' => $data,
'meta' => [
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'last_page' => $pageCount,
],
];
}
public function getOneLogByIndex($dialogId, $index)
{
$key = 'logs_' . $dialogId;
return $this->cache->lindex($key, $index);
}
public function getSomeLogsBy($dialogId, $start, $stop)
{
$key = 'logs_' . $dialogId;
return $this->cache->lrange($key, $start, $stop);
}
public function getAllLogBy($dialogId)
{
$key = 'logs_' . $dialogId;
return $this->cache->lrange($key, 0, -1);
}
public function getLogLen($dialogId)
{
$key = 'logs_' . $dialogId;
return $this->cache->llen($key);
}
public function setLogId($dialogId, $msgId, $index)
{
$key = 'logs_id_' . $dialogId;
$this->cache->hset($key, $msgId, $index);
}
public function getLogId($dialogId, $msgId)
{
$key = 'logs_id_' . $dialogId;
return $this->cache->hget($key, $msgId);
}
public function setMsgGroup($uuid, $toUuid, $data)
{
$key = 'msg_group_' . $uuid;
$this->cache->hset($key, $toUuid, json_encode($data));
}
public function getMsgGroup($uuid)
{
$key = 'msg_group_' . $uuid;
return $this->cache->hgetall($key);
}
public function getOneOfMsgGroup($uuid, $toUuid)
{
$key = 'msg_group_' . $uuid;
return $this->cache->hget($key, $toUuid);
}
public function groupIsExists($uuid, $toUuid)
{
$key = 'msg_group_' . $uuid;
return $this->cache->hexists($key, $toUuid);
}
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2年前 自动加精
不错,不过使用框架事半功倍啊,可以参考这个 博客:从 0 开始打造聊天室,搞定 Laravel 实时通信 —— 准备工作 和 广播系统《Laravel 9 中文文档》
方便全系统开源到github吗?
赞 赞 赞
希望开源就更好了