Laravel + swoole + redis 實現一對一聊天

拼湊出整個銀河發表於2022-06-28

最終效果如下?(筆記本螢幕比較小?)

前言

練手專案,程式碼目前只完成了聊天部分,還有其他部分沒有寫完,有不完善的地方請多多指教。?

實現功能

  • 會話列表
    • 未讀
    • 閱讀
  • 聊天
    • 文字訊息
    • 圖片訊息
    • 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 協議》,轉載必須註明作者和本文連結

相關文章