Laravel + swoole + redis 实现一对一聊天

最终效果如下👇(笔记本屏幕比较小😂)

前言

练手项目,代码目前只完成了聊天部分,还有其他部分没有写完,有不完善的地方请多多指教。😅

实现功能

  • 会话列表
    • 未读
    • 阅读
  • 聊天
    • 文字消息
    • 图片消息
    • emoji

待实现功能

  • 发送失败
    • 发送失败标记
    • 重新发送
  • 撤回
  • 聊天记录定时保存到数据库

    Redis数据

    Key 类型 描述
    laravel_database_(user_id) String 存储client id,以用户的id为key。发送消息时,根据id取出client id然后发送消息。 Laravel + swoole + redis 实现一对一聊天
    laravel_database_online_(user_id) String 存储当前时间,五分钟后过期,用于判断用户是否在线。可以在发送心跳的时候更新这个值,close时清除该值。 Laravel + swoole + redis 实现一对一聊天
    laravel_database_logs_(会话id) List 以dialog_id为key,存储聊天记录。 Laravel + swoole + redis 实现一对一聊天
    laravel_database_logs_id_(会话id) Hash 记录msg id在list中的索引(index)值 Laravel + swoole + redis 实现一对一聊天
    laravel_database_msg_group_(user_id) Hash 以用户id为key,记录该用户的会话列表 Laravel + swoole + redis 实现一对一聊天
  • 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 协议》,转载必须注明作者和本文链接
本帖由系统于 1年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 7
1年前 评论
拼凑出整个银河 (楼主) 1年前

方便全系统开源到github吗?

1年前 评论
拼凑出整个银河 (楼主) 1年前

赞 赞 赞

1年前 评论
拼凑出整个银河 (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!