微服务即时通讯系统的实现(客户端)----(2)

目录

  • [1. 将protobuf引入项目当中](#1. 将protobuf引入项目当中)
  • [2. 前后端交互接口定义](#2. 前后端交互接口定义)
    • [2.1 核心PB类](#2.1 核心PB类)
    • [2.2 HTTP接口定义](#2.2 HTTP接口定义)
    • [2.3 websocket接口定义](#2.3 websocket接口定义)
  • [3. 核心数据结构和PB之间的转换](#3. 核心数据结构和PB之间的转换)
  • [4. 设计数据中心DataCenter类](#4. 设计数据中心DataCenter类)
  • [5. 网络通信](#5. 网络通信)
    • [5.1 定义NetClient类](#5.1 定义NetClient类)
    • [5.2 引入HTTP](#5.2 引入HTTP)
    • [5.3 引入websocket](#5.3 引入websocket)
  • [6. 小结](#6. 小结)
  • [7. 搭建测试服务器](#7. 搭建测试服务器)
    • [7.1 创建项目](#7.1 创建项目)
    • [7.2 服务器引入http](#7.2 服务器引入http)
    • [7.3 服务器引入websocket](#7.3 服务器引入websocket)
    • [7.4 服务器引protobuf](#7.4 服务器引protobuf)
    • [7.5 编写工具函数和构造数据函数](#7.5 编写工具函数和构造数据函数)
    • [7.6 验证网络连通性](#7.6 验证网络连通性)
    • [7.7 网络通信注意事项](#7.7 网络通信注意事项)
  • [8. 主界面逻辑的实现](#8. 主界面逻辑的实现)
    • [8.1 获取个人信息](#8.1 获取个人信息)
    • [8.2 获取好友列表](#8.2 获取好友列表)
    • [8.3 获取会话列表](#8.3 获取会话列表)
    • [8.4 获取好友申请列表](#8.4 获取好友申请列表)
    • [8.5 获取指定会话的近期消息](#8.5 获取指定会话的近期消息)
    • [8.6 点击某个好友项](#8.6 点击某个好友项)
  • [9. 小结](#9. 小结)

1. 将protobuf引入项目当中

(1)创建 proto 目录, 并把服务器提供的 proto 拷贝过来:

(2)proto文件链接: https://gitee.com/liu-yechi/new_code/tree/master/chat_system/client/ChatClient/proto

2. 前后端交互接口定义

2.1 核心PB类

(1)用户信息:

cpp 复制代码
//用户信息结构
message UserInfo {
    string user_id = 1;//用户ID
    string nickname = 2;//昵称
    string description = 3;//个人签名/描述
    string phone = 4; //绑定手机号
    bytes  avatar = 5;//头像照片,文件内容使用二进制
}

(2)会话信息:

cpp 复制代码
//聊天会话信息
message ChatSessionInfo {
    optional string single_chat_friend_id = 1;//群聊会话不需要设置,单聊会话设置为对方ID
    string chat_session_id = 2; //会话ID
    string chat_session_name = 3;//会话名称git 
    optional MessageInfo prev_message = 4;//会话上一条消息,新建的会话没有最新消息
    optional bytes avatar = 5;//会话头像 --群聊会话不需要,直接由前端固定渲染,单聊就是对方的头像
}

(3)消息信息:

cpp 复制代码
//消息类型
enum MessageType {
    STRING = 0;
    IMAGE = 1;
    FILE = 2;
    SPEECH = 3;
}
message StringMessageInfo {
    string content = 1;//文字聊天内容
}
message ImageMessageInfo {
    optional string file_id = 1;//图片文件id,客户端发送的时候不用设置,由transmit服务器进行设置后交给storage的时候设置
    optional bytes image_content = 2;//图片数据,在ES中存储消息的时候只要id不要文件数据, 服务端转发的时候需要原样转发
}
message FileMessageInfo {
    optional string file_id = 1;//文件id,客户端发送的时候不用设置
    int64 file_size = 2;//文件大小
    string file_name = 3;//文件名称
    optional bytes file_contents = 4;//文件数据,在ES中存储消息的时候只要id和元信息,不要文件数据, 服务端转发的时候也不需要填充
}
message SpeechMessageInfo {
    optional string file_id = 1;//语音文件id,客户端发送的时候不用设置
    optional bytes file_contents = 2;//文件数据,在ES中存储消息的时候只要id不要文件数据, 服务端转发的时候也不需要填充
}
message MessageContent {
    MessageType message_type = 1; //消息类型
    oneof msg_content {
        StringMessageInfo string_message = 2;//文字消息
        FileMessageInfo file_message = 3;//文件消息
        SpeechMessageInfo speech_message = 4;//语音消息
        ImageMessageInfo image_message = 5;//图片消息
    };
}
//消息结构
message MessageInfo {
    string message_id = 1;//消息ID
    string chat_session_id = 2;//消息所属聊天会话ID
    int64 timestamp = 3;//消息产生时间
    UserInfo sender = 4;//消息发送者信息
    MessageContent message = 5;
}

message Message {
    string request_id = 1;
    MessageInfo message = 2;
}

message FileDownloadData {
    string file_id = 1;
    bytes file_content = 2;
}

message FileUploadData {
    string file_name = 1;
    int64 file_size = 2;
    bytes file_content = 3;
}

2.2 HTTP接口定义

(1)请求响应基本格式:

cpp 复制代码
//通信接口统一采用POST请求实现,正文采用protobuf协议进行组织
/*  
    HTTP HEADER:
    POST /service/xxxxx
    Content-Type: application/x-protobuf
    Content-Length: 123

    xxxxxx

    -------------------------------------------------------

    HTTP/1.1 200 OK 
    Content-Type: application/x-protobuf
    Content-Length: 123

    xxxxxxxxxx
*/

(2)约定路径:每个接口都提供对应的请求响应的 proto 对象:

cpp 复制代码
//在客户端与网关服务器的通信中,使用HTTP协议进行通信
//  通信时采用POST请求作为请求方法
//  通信时,正文采用protobuf作为正文协议格式,具体内容字段以前边各个文件中定义的字段格式为准
/*  以下是HTTP请求的功能与接口路径对应关系:
    SERVICE HTTP PATH:
    {
        获取随机验证码                  /service/user/get_random_verify_code
        获取短信验证码                  /service/user/get_phone_verify_code
        用户名密码注册                  /service/user/username_register
        用户名密码登录                  /service/user/username_login
        手机号码注册                    /service/user/phone_register
        手机号码登录                    /service/user/phone_login
        获取个人信息                    /service/user/get_user_info
        修改头像                        /service/user/set_avatar
        修改昵称                        /service/user/set_nickname
        修改签名                        /service/user/set_description
        修改绑定手机                    /service/user/set_phone

        获取好友列表                    /service/friend/get_friend_list
        获取好友信息                    /service/friend/get_friend_info
        发送好友申请                    /service/friend/add_friend_apply
        好友申请处理                    /service/friend/add_friend_process
        删除好友                        /service/friend/remove_friend
        搜索用户                        /service/friend/search_friend
        获取指定用户的消息会话列表       /service/friend/get_chat_session_list
        创建消息会话                    /service/friend/create_chat_session
        获取消息会话成员列表             /service/friend/get_chat_session_member
        获取待处理好友申请事件列表       /service/friend/get_pending_friend_events

        获取历史消息/离线消息列表        /service/message_storage/get_history
        获取最近N条消息列表             /service/message_storage/get_recent
        搜索历史消息                    /service/message_storage/search_history
        
        发送消息                        /service/message_transmit/new_message

        获取单个文件数据                /service/file/get_single_file
        获取多个文件数据                /service/file/get_multi_file
        发送单个文件                    /service/file/put_single_file
        发送多个文件                    /service/file/put_multi_file

        语音转文字                     /service/speech/recognition
    }
    
*/

2.3 websocket接口定义

(1)身份认证:

cpp 复制代码
/*
    消息推送使用websocket长连接进行
    websocket长连接转换请求:ws://host:ip/ws
    长连建立以后,需要客户端给服务器发送一个身份验证信息
*/
message ClientAuthenticationReq {
    string request_id = 1;
    string session_id = 2;
}
message ClientAuthenticationRsp {
    string request_id = 1;
    bool success = 2;
    string errmsg = 3;
}

(2)消息推送。当前存在五种消息推送:

  • 申请好友通知。
  • 好友申请处理通知 (同意/拒绝)。
  • 创建消息会话通知。
  • 收到消息通知。
  • 删除好友通知。
cpp 复制代码
enum NotifyType {
    FRIEND_ADD_APPLY_NOTIFY = 0;
    FRIEND_ADD_PROCESS_NOTIFY = 1;
    CHAT_SESSION_CREATE_NOTIFY = 2;
    CHAT_MESSAGE_NOTIFY = 3;
    FRIEND_REMOVE_NOTIFY = 4;
}

message NotifyFriendAddApply {
    UserInfo user_info = 1;  //申请人信息
}
message NotifyFriendAddProcess {
    bool agree = 1;
    UserInfo user_info = 2;  //处理人信息
}
message NotifyFriendRemove {
    string user_id = 1; //删除自己的用户ID
}
message NotifyNewChatSession {
    ChatSessionInfo chat_session_info = 1; //新建会话信息
}
message NotifyNewMessage {
    MessageInfo message_info = 1; //新消息
}


message NotifyMessage {
    optional string notify_event_id = 1;//通知事件操作id(有则填无则忽略)
    NotifyType notify_type = 2;//通知事件类型
    oneof notify_remarks {      //事件备注信息
        NotifyFriendAddApply friend_add_apply = 3;
        NotifyFriendAddProcess friend_process_result = 4;
        NotifyFriendRemove friend_remove = 7;
        NotifyNewChatSession new_chat_session_info = 5;//会话信息
        NotifyNewMessage new_message_info = 6;//消息信息
    }
}

3. 核心数据结构和PB之间的转换

(1)以下是protobuf数据和QString的数据转化函数:(类里面的成员变量没有写出来):

cpp 复制代码
//
/// 用户信息
//
class UserInfo
{
public:
    // 该类的成员变量没有写出来。。。

    // 从 protobuffer 的 UserInfo 对象, 转成当前代码的 UserInfo 对象
    void load(const bite_im::UserInfo& userInfo)
    {
        this->userId = userInfo.userId();
        this->nickname = userInfo.nickname();
        this->description = userInfo.description();
        this->phone = userInfo.phone();
        if(userInfo.avatar().isEmpty())
        {
            // 使用默认头像即可
            this->avatar = QIcon(":/resource/image/defaultAvatar.png");
        }
        else
        {
            this->avatar = makeIcon(userInfo.avatar());
        }
    }
};

//
/// 消息信息
//
enum MessageType
{
    TEXT_TYPE,		// 文本消息
    IMAGE_TYPE, 	// 图片消息
    FILE_TYPE, 		// 文件消息
    SPEECH_TYPE 	// 语音消息
};

class Message
{
public:
    // 该类的成员变量没有写出来。。。

    // 此处 extraInfo 目前只是在消息类型为文件消息时, 作为 "文件名" 补充.
    static Message makeMessage(MessageType messageType, const QString& chatSessionId,
                               const UserInfo& sender, const QByteArray& content,
                               const QString& extraInfo)
    {
        if(messageType == TEXT_TYPE)
        {
            return makeTextMessage(chatSessionId, sender, content);
        }
        else if(messageType == IMAGE_TYPE)
        {
            return makeImageMessage(chatSessionId, sender, content);
        }
        else if(messageType == FILE_TYPE)
        {
            return makeFileMessage(chatSessionId, sender, content, extraInfo);
        }
        else if(messageType == SPEECH_TYPE)
        {
            return makeSpeechMessage(chatSessionId, sender, content);
        }
        else
        {
            // 触发了未知的消息类型
            return Message();
        }
    }

    void load(const bite_im::MessageInfo& messageInfo)
    {
        this->messageId = messageInfo.messageId();
        this->chatSessionId = messageInfo.chatSessionId();
        this->time = formatTime(messageInfo.timestamp());
        this->sender.load(messageInfo.sender());

        // 设置消息类型
        auto type = messageInfo.message().messageType();
        if(type == bite_im::MessageTypeGadget::MessageType::STRING)
        {
            this->messageType = TEXT_TYPE;
            this->content = messageInfo.message().stringMessage().content().toUtf8();
        }
        else if(type == bite_im::MessageTypeGadget::MessageType::IMAGE)
        {
            this->messageType = IMAGE_TYPE;
            if(messageInfo.message().imageMessage().hasImageContent())
            {
                this->content = messageInfo.message().imageMessage().imageContent();
            }

            if(messageInfo.message().imageMessage().hasFileId())
            {
                this->fileId = messageInfo.message().imageMessage().fileId();
            }
        }
        else if(type == bite_im::MessageTypeGadget::MessageType::FILE)
        {
            this->messageType = FILE_TYPE;
            if(messageInfo.message().fileMessage().hasFileContents())
            {
                this->content = messageInfo.message().fileMessage().fileContents();
            }

            if(messageInfo.message().fileMessage().hasFileId())
            {
                this->fileId = messageInfo.message().fileMessage().fileId();
            }

            this->fileName = messageInfo.message().fileMessage().fileName();
        }
        else if(type == bite_im::MessageTypeGadget::MessageType::SPEECH)
        {
            this->messageType = SPEECH_TYPE;
            if(messageInfo.message().speechMessage().hasFileContents())
            {
                this->content = messageInfo.message().speechMessage().fileContents();
            }

            if(messageInfo.message().speechMessage().hasFileId())
            {
                this->fileId = messageInfo.message().speechMessage().fileId();
            }
        }
        else
        {
            // 错误的类型, 啥都不做了, 只是打印一个日志
            LOG() << "非法的消息类型! type=" << type;
        }
    }


private:
    // 通过这个方法生成唯一的 messageId
    static QString makeId()
    {
        return "M" + QUuid::createUuid().toString().sliced(25, 12);
    }

    static Message makeTextMessage(const QString& chatSessionId,
                                   const UserInfo& sender, const QByteArray& content)
    {
        Message message;
        message.messageId = makeId();
        message.chatSessionId = chatSessionId;
        message.messageType = TEXT_TYPE;
        message.content = content;
        message.sender = sender;
        message.time = formatTime(getTime()); // 生成一个格式化时间

        // 对于文本消息来说, 这俩属性不使用, 设为 ""
        message.fileId = "";
        message.fileName = "";

        return message;
    }

    static Message makeImageMessage(const QString& chatSessionId,
                                   const UserInfo& sender, const QByteArray& content)
    {
        Message message;
        message.messageId = makeId();
        message.chatSessionId = chatSessionId;
        message.messageType = IMAGE_TYPE;
        message.content = content;
        message.sender = sender;
        message.time = formatTime(getTime()); // 生成一个格式化时间

        // fileId 后续使用的时候再进一步设置
        message.fileId = "";
        // fileName 不使用, 直接设为 ""
        message.fileName = "";
        return message;
    }

    static Message makeFileMessage(const QString& chatSessionId, const UserInfo& sender,
                                   const QByteArray& content, const QString& fileName)
    {
        Message message;
        message.messageId = makeId();
        message.chatSessionId = chatSessionId;
        message.messageType = FILE_TYPE;
        message.content = content;
        message.sender = sender;
        message.time = formatTime(getTime()); // 生成一个格式化时间

        // fileId 后续使用的时候进一步设置
        message.fileId = "";
        message.fileName = fileName;

        return message;
    }

    static Message makeSpeechMessage(const QString& chatSessionId,
                                   const UserInfo& sender, const QByteArray& content)
    {
        Message message;
        message.messageId = makeId();
        message.chatSessionId = chatSessionId;
        message.messageType = SPEECH_TYPE;
        message.content = content;
        message.sender = sender;
        message.time = formatTime(getTime()); // 生成一个格式化时间

        // fileId 后续使用的时候进一步设置
        message.fileId = "";
        // fileName 不使用, 直接设为 ""
        message.fileName = "";

        return message;
    }
};

//
/// 会话信息
//
class ChatSessionInfo
{
public:
   	// 该类的成员变量没有写出来。。。

    void load(const bite_im::ChatSessionInfo& chatSessionInfo)
    {
        this->chatSessionId = chatSessionInfo.chatSessionId();
        this->chatSessionName = chatSessionInfo.chatSessionName();
        if(chatSessionInfo.hasSingleChatFriendId())
        {
            this->userId = chatSessionInfo.singleChatFriendId();
        }

        if(chatSessionInfo.hasPrevMessage())
        {
            lastMessage.load(chatSessionInfo.prevMessage());
        }

        if(chatSessionInfo.hasAvatar() && !chatSessionInfo.avatar().isEmpty())
        {
            // 已经有头像了, 直接设置这个头像
            this->avatar = makeIcon(chatSessionInfo.avatar());
        }
        else
        {
            // 如果没有头像, 则根据当前会话是单聊还是群聊, 使用不同的默认头像.
            if(userId != "")
            {
                // 单聊
                this->avatar = QIcon(":/resource/image/defaultAvatar.png");
            }
            else
            {
                // 群聊
                this->avatar = QIcon(":/resource/image/groupAvatar.png");
            }
        }
    }
};

4. 设计数据中心DataCenter类

(1)在model文件夹当中创建datacenter.h的头文件,并且在该头文件当中创建DataCenter类来管理所有客户端需要的数据。这是一个单例类:

cpp 复制代码
class DataCenter : public QObject
{
    Q_OBJECT
public:
    static DataCenter* getInstance();

    ~DataCenter();

private:
    DataCenter();
    static DataCenter* instance;

    // 列出 DataCenter 中要组织管理的所有的数据

    // 当前客户端登录到服务器对应的登录会话 id
    QString loginSessionId = "";

    // 当前的用户信息
    model::UserInfo* myself = nullptr;

    // 好友列表
    QList<model::UserInfo>* friendList = nullptr;

    // 会话列表
    QList<model::ChatSessionInfo>* chatSessionList = nullptr;
    // 记录当前选中的会话是哪个~~
    QString currentChatSessionId = "";
    // 记录每个会话中, 都有哪些成员(主要针对群聊). key 为 chatSessionId, value 为成员列表
    QHash<QString, QList<model::UserInfo>>* memberList = nullptr;

    // 待处理的好友申请列表
    QList<model::UserInfo>* applyList = nullptr;

    // 每个会话的最近消息列表, key 为 chatSessionId, value 为消息列表
    QHash<QString, QList<model::Message>>* recentMessages = nullptr;

    // 存储每个会话, 未读消息的个数. key 为 chatSessionId, value 为未读消息的个数.
    QHash<QString, int>* unreadMessageCount = nullptr;

    // 用户的好友搜索结果.
    QList<model::UserInfo>* searchUserResult = nullptr;

    // 历史消息搜索结果.
    QList<model::Message>* searchMessageResult = nullptr;

    // 短信验证码的验证 id
    QString currentVerifyCodeId = "";

    // 让 DataCenter 持有 NetClient 实例.
    network::NetClient netClient;

public:
    // 初始化数据文件
    void initDataFile();
    // 存储数据到文件中
    void saveDataFile();
    // 从数据文件中加载数据到内存
    void loadDataFile();

signals:
};

(2)具体实现:

cpp 复制代码
DataCenter* DataCenter::instance = nullptr;

DataCenter* DataCenter::getInstance()
{
    if(instance == nullptr)
    {
        instance = new DataCenter();
    }

    return instance;
}

DataCenter::DataCenter()
    :netClient(this)
{
    // 此处只是把这几个 hash 类型的属性 new 出实例. 其他的 QList 类型的属性, 都暂时不实例化.
    // 主要是为了使用 nullptr 表示 "非法状态"
    // 对于 hash 来说, 不关心整个 QHash 是否是 nullptr, 而是关心, 某个 key 对应的 value 是否存在~~
    // 通过 key 是否存在, 也能表示该值是否有效.
    recentMessages = new QHash<QString, QList<Message>>();
    memberList = new QHash<QString, QList<UserInfo>>();
    unreadMessageCount = new QHash<QString, int>();
}

DataCenter::~DataCenter()
{
    // 释放所有的成员
    // 此处不必判定 nullptr, 直接 delete 即可!
    // C++ 标准中明确规定, 针对 nullptr 进行 delete, 是合法行为, 不会有任何副作用.
    delete myself;
    delete friendList;
    delete chatSessionList;
    delete memberList;
    delete applyList;
    delete recentMessages;
    delete unreadMessageCount;
    delete searchUserResult;
    delete searchMessageResult;
}

NetClient 的实现后续完成。

(3)数据持久化:使用文件存储 sessionId 和 未读消息信息:

cpp 复制代码
void DataCenter::initDataFile()
{
    // 构造出文件的路径, 使用 appData 存储文件
    QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
    QString filePath = basePath + "/ChatClient.json";
    LOG() << "filePath=" << filePath;

    QDir dir;
    if(!dir.exists(basePath))
    {
        dir.mkpath(basePath);
    }

    // 构造好文件路径之后, 把文件创建出来.
    // 写方式打开, 并且写入初始内容
    QFile file(filePath);
    if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        LOG() << "打开文件失败!" << file.errorString();
        return;
    }

    // 打开成功, 写入初始内容.
    QString data = "{\n\n}";
    file.write(data.toUtf8());
    file.close();
}

void DataCenter::saveDataFile()
{
    QString filePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/ChatClient.json";

    QFile file(filePath);
    if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        LOG() << "打开文件失败!" << file.errorString();
        return;
    }

    // 按照 json 格式来写入数据.
    // 这个对象就可以当做 map 一样来使用.
    QJsonObject jsonObj;
    jsonObj["loginSessionId"] = loginSessionId;

    QJsonObject jsonUnread;
    for(auto it = unreadMessageCount->begin(); it != unreadMessageCount->end(); ++it)
    {
        // 注意 Qt 的迭代器使用细节和 STL 略有差别. 此处不是使用 first / second 的方式
        jsonUnread[it.key()] = it.value();
    }

    jsonObj["unread"] = jsonUnread;

    // 把 json 写入文件了
    QJsonDocument jsonDoc(jsonObj);
    QString s = jsonDoc.toJson();
    file.write(s.toUtf8());

    // 关闭文件
    file.close();
}

void DataCenter::loadDataFile()
{
    // 确保在加载之前, 先针对文件进行初始化操作.
    QString filePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/ChatClient.json";

    // 判定文件是否存在, 不存在则初始化, 并创建出新的空白的 json 文件
    QFileInfo fileInfo(filePath);
    if(!fileInfo.exists())
    {
        initDataFile();
    }

    QFile file(filePath);
    if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        LOG() << "打开文件失败!" << file.errorString();
        return;
    }

    // 读取到文件内容, 解析为 JSON 对象
    QJsonDocument jsonDoc = QJsonDocument::fromJson(file.readAll());
    if(jsonDoc.isNull())
    {
        LOG() << "解析 JSON 文件失败! JSON 文件格式有错误!";
        file.close();
        return;
    }

    QJsonObject jsonObj = jsonDoc.object();
    this->loginSessionId = jsonObj["loginSessionId"].toString();

    this->unreadMessageCount->clear();
    QJsonObject jsonUnread = jsonObj["unread"].toObject();
    for(auto it = jsonUnread.begin(); it != jsonUnread.end(); ++it)
    {
        this->unreadMessageCount->insert(it.key(), it.value().toInt());
    }

    file.close();
}

void DataCenter::clearUnread(const QString& chatSessionId)
{
    (*unreadMessageCount)[chatSessionId] = 0;

    // 手动保存一下结果到文件中.
    saveDataFile();
}

未读消息的实现放到后面完成。

5. 网络通信

5.1 定义NetClient类

(1)创建network文件夹,在创建netclient.h头文件,在此头文件创建 NetClient 类来管理所有的和服务器通信的内容。NetClient 内部又分成 httpClient 和 websocketClient 两个部分。DataCenter 中会持有 NetClient 的指针。

cpp 复制代码
class NetClient : public QObject
{
    Q_OBJECT

private:
    // 定义重要常量. ip 都暂时使用本地的环回 ip. 端口号约定成 8000 和 8001
    const QString HTTP_URL = "http://127.0.0.1:8000";
    const QString WEBSOCKET_URL = "ws://127.0.0.1:8001/ws";

public:
    NetClient(model::DataCenter* dataCenter);

	// 生成请求 id
    static QString makeRequestId();

    // 封装发送请求的逻辑
    QNetworkReply* sendHttpRequest(const QString& apiPath, const QByteArray& body);

private:
    model::DataCenter* dataCenter;

    QNetworkAccessManager httpClient;   // http 客户端

    QWebSocket websocketClient;         // websocket 客户端

    QProtobufSerializer serializer;     // 序列化器

signals:
};

5.2 引入HTTP

(1)进行网络测试:

cpp 复制代码
void NetClient::ping()
{
    QNetworkRequest httpReq;
    httpReq.setUrl(QUrl(HTTP_URL + "/ping"));

    QNetworkReply* httpResp = httpClient.get(httpReq);
    connect(httpResp, &QNetworkReply::finished, this, [=]()
    {
        // 这里面, 说明响应已经回来了.
        if(httpResp->error() != QNetworkReply::NoError)
        {
            // 请求失败!
            LOG() << "HTTP 请求失败! " << httpResp->errorString();
            httpResp->deleteLater();
            return;
        }

        // 获取到响应的 body
        QByteArray body = httpResp->readAll();
        LOG() << "响应内容: " << body;
        httpResp->deleteLater();
    });
}

(2)封装构造 HTTP 请求和处理响应以及请求id:

cpp 复制代码
QString NetClient::makeRequestId()
{
    // 基本要求, 确保每个请求的 id 都是不重复(唯一的)
    // 通过 UUID 来实现上述效果.
    return "R" + QUuid::createUuid().toString().sliced(25, 12);
}

// 通过这个函数, 把发送 HTTP 请求操作封装一下.
// apiPath 应该要以 / 开头
QNetworkReply* NetClient::sendHttpRequest(const QString &apiPath, const QByteArray &body)
{
    QNetworkRequest httpReq;
    httpReq.setUrl(QUrl(HTTP_URL + apiPath));
    httpReq.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-protobuf");

    QNetworkReply* httpResp = httpClient.post(httpReq, body);
    return httpResp;
}

// 封装处理响应的逻辑(包括判定 HTTP 正确性, 反序列化, 判定业务上的正确性)
// 由于不同的 api, 返回的 pb 对象结构, 不同, 为了让一个函数能处理多种不同类型, 需要使用 模板.
// 通过输出型参数, 表示这次操作是成功还是失败, 以及失败的原因.
template <typename T>
std::shared_ptr<T> handleHttpResponse(QNetworkReply* httpResp, bool* ok, QString* reason)
{
    // 1. 判定 HTTP 层面上, 是否出错
    if(httpResp->error() != QNetworkReply::NoError)
    {
        *ok = false;
        *reason = httpResp->errorString();
        httpResp->deleteLater();
        return std::shared_ptr<T>();
    }

    // 2. 获取到响应的 body
    QByteArray respBody = httpResp->readAll();

    // 3. 针对 body 反序列化
    std::shared_ptr<T> respObj = std::make_shared<T>();
    respObj->deserialize(&serializer, respBody);

    // 4. 判定业务上的结果是否正确
    if(!respObj->success())
    {
        *ok = false;
        *reason = respObj->errmsg();
       	httpResp->deleteLater();
        return std::shared_ptr<T>();
    }

    // 5. 释放 httpResp 对象
    httpResp->deleteLater();
    *ok = true;
    return respObj;
}

5.3 引入websocket

(1)Websocket 在主窗口加载后,才和服务器建立连接,并且在建立连接后给服务器发送⼀个 认证请求之后, 才能收到后续数据。初始化 websocket:

cpp 复制代码
void NetClient::initWebsocket()
{
    // 1. 准备好所有需要的信号槽
    connect(&websocketClient, &QWebSocket::connected, this, [=]()
    {
        LOG() << "websocket 连接成功!";
        // 不要忘记! 在 websocket 连接成功之后, 发送身份认证消息!
        sendAuth();
    });

    connect(&websocketClient, &QWebSocket::disconnected, this, [=]()
    {
        LOG() << "websocket 连接断开!";
    });

    connect(&websocketClient, &QWebSocket::errorOccurred, this, [=](QAbstractSocket::SocketError error)
    {
        LOG() << "websocket 连接出错!" << error;
    });

    connect(&websocketClient, &QWebSocket::textMessageReceived, this, [=](const QString& message)
    {
        LOG() << "websocket 收到文本消息!" << message;
    });

    connect(&websocketClient, &QWebSocket::binaryMessageReceived, this, [=](const QByteArray& byteArray)
    {
        LOG() << "websocket 收到二进制消息!" << byteArray.length();
        bite_im::NotifyMessage notifyMessage;
        notifyMessage.deserialize(&serializer, byteArray);
        handleWsResponse(notifyMessage);
    });

    // 2. 和服务器真正建立连接
    websocketClient.open(WEBSOCKET_URL);
}

(2)初始化身份信息:

cpp 复制代码
void NetClient::sendAuth()
{
    bite_im::ClientAuthenticationReq req;
    req.setRequestId(makeRequestId());
    req.setSessionId(dataCenter->getLoginSessionId());
    QByteArray body = req.serialize(&serializer);
    websocketClient.sendBinaryMessage(body);
    LOG() << "[WS身份认证] requestId=" << req.requestId() << ", loginSessionId=" << req.sessionId();
}

(3)搭建 websocket 消息推送的逻辑:

cpp 复制代码
void NetClient::handleWsResponse(const bite_im::NotifyMessage& notifyMessage)
{
    if(notifyMessage.notifyType() == bite_im::NotifyTypeGadget::NotifyType::CHAT_MESSAGE_NOTIFY)
    {
        // 收到消息
        // 1. 把 pb 中的 MessageInfo 转成客户端自己的 Message
        model::Message message;
        message.load(notifyMessage.newMessageInfo().messageInfo());
        // 2. 针对自己的 message 做进一步的处理
        handleWsMessage(message);
    }
    else if(notifyMessage.notifyType() == bite_im::NotifyTypeGadget::NotifyType::CHAT_SESSION_CREATE_NOTIFY)
    {
        // 创建新的会话通知
        model::ChatSessionInfo chatSessionInfo;
        chatSessionInfo.load(notifyMessage.newChatSessionInfo().chatSessionInfo());
        handleWsSessionCreate(chatSessionInfo);
    }
    else if(notifyMessage.notifyType() == bite_im::NotifyTypeGadget::NotifyType::FRIEND_ADD_APPLY_NOTIFY)
    {
        // 添加好友申请通知
        model::UserInfo userInfo;
        userInfo.load(notifyMessage.friendAddApply().userInfo());
        handleWsAddFriendApply(userInfo);
    }
    else if(notifyMessage.notifyType() == bite_im::NotifyTypeGadget::NotifyType::FRIEND_ADD_PROCESS_NOTIFY)
    {
        // 添加好友申请的处理结果通知
        model::UserInfo userInfo;
        userInfo.load(notifyMessage.friendProcessResult().userInfo());
        bool agree = notifyMessage.friendProcessResult().agree();
        handleWsAddFriendProcess(userInfo, agree);
    }
    else if(notifyMessage.notifyType() == bite_im::NotifyTypeGadget::NotifyType::FRIEND_REMOVE_NOTIFY)
    {
        // 删除好友通知
        const QString& userId = notifyMessage.friendRemove().userId();
        handleWsRemoveFriend(userId);
    }
}

(4)针对上述每种消息的处理实现,后续再进⼀步完成。

6. 小结

(1)三个层次关系:


NetClient从网络拿到数据,只交给DataCenter通过网络收到的数据,DataCenter负责发送信号给 MainWidget,从而异步通知界面更新。

7. 搭建测试服务器

7.1 创建项目

(1)基于 CMake 创建 Qt 项目。虽然使用控制台项目也可以(创建成 Qt Core Application), 但是使用图形界面更合适⼀些。尤其是后面构造⼀些测试数据,图形界面更方便进行操作。比如在界面上提供不同的按钮,按下不同按钮就可以给客户端推送不同的数据:

cpp 复制代码
cmake_minimum_required(VERSION 3.16)

find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets HttpServer WebSockets Protobuf)

file(GLOB PB_FILES "../ChatClient/proto/*.proto")

qt_add_protobuf(ChatServerMock PROTO_FILES ${PB_FILES})

target_link_libraries(ChatServerMock PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt6::HttpServer Qt6::WebSockets Qt6::Protobuf)

7.2 服务器引入http

(1)创建HttpServer类来实现此功能:

cpp 复制代码
class HttpServer : public QObject
{
    Q_OBJECT

public:
    static HttpServer* getInstance();

    // 通过这个函数, 针对 HTTP Server 进行初始化 (绑定端口, 配置路由....)
    bool init();

private:
    static HttpServer* instance;
    HttpServer() {}

    QHttpServer httpServer;
    QProtobufSerializer serializer;

signals:
};

(2)具体实现:

cpp 复制代码
HttpServer* HttpServer::instance = nullptr;

HttpServer* HttpServer::getInstance()
{
    if(instance == nullptr)
    {
        instance = new HttpServer();
    }

    return instance;
}

bool HttpServer::init()
{
	// 返回的值是 int, 表示成功绑定的端口号的数值.
    int ret = httpServer.listen(QHostAddress::Any, 8000);
    
	// 配置路由
    httpServer.route("/ping", [](const QHttpServerRequest& req)
    {
        (void) req;
        qDebug() << "[http] 收到 ping 请求";
        return "pong";
    });
    
	return ret == 8000;
}

7.3 服务器引入websocket

(1)创建WebsocketServer类来实现此功能:

cpp 复制代码
class WebsocketServer : public QObject
{
    Q_OBJECT

private:
    static WebsocketServer* instance;
    WebsocketServer() : websocketServer("websocket server", QWebSocketServer::NonSecureMode) {}

    QWebSocketServer websocketServer;
    QProtobufSerializer serializer;

public:
    static WebsocketServer* getInstance();

    bool init();

    int messageIndex = 0;


signals:
};

(2)具体实现:

cpp 复制代码
WebsocketServer* WebsocketServer::instance = nullptr;

WebsocketServer *WebsocketServer::getInstance()
{
    if (instance == nullptr)
    {
        instance = new WebsocketServer();
    }

    return instance;
}

// 针对 websocket 服务器进行初始化操作
bool WebsocketServer::init()
{
    // 1. 连接信号槽
    connect(&websocketServer, &QWebSocketServer::newConnection, this, [=]()
    {
        // 连接建立成功之后.
        qDebug() << "[websocket] 连接建立成功!";

        // 获取到用来通信的 socket 对象. nextPendingConnection 类似于 原生 socket 中的 accept
        QWebSocket* socket = websocketServer.nextPendingConnection();

        // 针对这个 socket 对象, 进行剩余信号的处理
        connect(socket, &QWebSocket::disconnected, this, [=]()
        {
            qDebug() << "[websocket] 连接断开!";
            
        });

        connect(socket, &QWebSocket::errorOccurred, this, [=](QAbstractSocket::SocketError error)
        {
            qDebug() << "[websocket] 连接出错! " << error;
        });

        connect(socket, &QWebSocket::textMessageReceived, this, [=](const QString& message)
        {
            qDebug() << "[websocket] 收到文本数据! message=" << message;
        });

        connect(socket, &QWebSocket::binaryMessageReceived, this, [=](const QByteArray& byteArray)
        {
            qDebug() << "[websocket] 收到二进制数据! " << byteArray.length();
        });
        
    });


    // 2. 绑定端口, 启动服务
    bool ok = websocketServer.listen(QHostAddress::Any, 8001);
    return ok;
}

7.4 服务器引protobuf

(1)cmake增加内容文件:

cpp 复制代码
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets HttpServer WebSockets Protobuf)
file(GLOB PB_FILES "../ChatClient/proto/*.proto")

直接从ChatClient项目中引入proto文件。

(2)如果出现下列报错:

  • 则给 target_link_libraries 引入 PRIVATE。从
cpp 复制代码
target_link_libraries(ChatServerMock Qt${QT_VERSION_MAJOR}::Core Qt6::Network Qt6::WebSockets)
  • 修改为:
cpp 复制代码
target_link_libraries(ChatServerMock PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt6::Network Qt6::WebSockets)

7.5 编写工具函数和构造数据函数

(1)工具函数:

cpp 复制代码
// 读写文件操作.
// 从指定文件中, 读取所有的二进制内容. 得到一个 QByteArray
static inline QByteArray loadFileToByteArray(const QString& path) {
    QFile file(path);
    bool ok = file.open(QFile::ReadOnly);
    if (!ok) {
        LOG() << "文件打开失败!";
        return QByteArray();
    }
    QByteArray content = file.readAll();
    file.close();
    return content;
}

// 把 QByteArray 中的内容, 写入到某个指定文件里
static inline void writeByteArrayToFile(const QString& path, const QByteArray& content) {
    QFile file(path);
    bool ok = file.open(QFile::WriteOnly);
    if (!ok) {
        LOG() << "文件打开失败!";
        return;
    }
    file.write(content);
    file.flush();
    file.close();
}

(2)构造数据函数:

cpp 复制代码
// 生成默认的 UserInfo 对象
bite_im::UserInfo makeUserInfo(int index, const QByteArray& avatar)
{
    bite_im::UserInfo userInfo;
    userInfo.setUserId(QString::number(1000 + index));
    userInfo.setNickname("张三" + QString::number(index));
    userInfo.setDescription("个性签名" + QString::number(index));
    userInfo.setPhone("18612345678");
    userInfo.setAvatar(avatar);
    return userInfo;
}

bite_im::MessageInfo makeTextMessageInfo(int index, const QString& chatSessionId, const QByteArray& avatar)
{
    bite_im::MessageInfo messageInfo;
    messageInfo.setMessageId(QString::number(3000 + index));
    messageInfo.setChatSessionId(chatSessionId);
    messageInfo.setTimestamp(getTime());
    messageInfo.setSender(makeUserInfo(index, avatar));

    bite_im::StringMessageInfo stringMessageInfo;
    stringMessageInfo.setContent("这是一条消息内容" + QString::number(index));

    bite_im::MessageContent messageContent;
    messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::STRING);
    messageContent.setStringMessage(stringMessageInfo);
    messageInfo.setMessage(messageContent);
    return messageInfo;
}

bite_im::MessageInfo makeImageMessageInfo(int index, const QString& chatSessionId, const QByteArray& avatar)
{
    bite_im::MessageInfo messageInfo;
    messageInfo.setMessageId(QString::number(3000 + index));
    messageInfo.setChatSessionId(chatSessionId);
    messageInfo.setTimestamp(getTime());
    messageInfo.setSender(makeUserInfo(index, avatar));

    bite_im::ImageMessageInfo imageMessageInfo;
    imageMessageInfo.setFileId("testImage");
    // 真实服务器推送的消息数据里, 本身也就不带图片的正文. 只是 fileId.
    // 需要通过 fileId, 二次发起请求, 通过 getSingleFile 接口来获取到内容.
    // imageMessageInfo.setImageContent();

    bite_im::MessageContent messageContent;
    messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::IMAGE);
    messageContent.setImageMessage(imageMessageInfo);
    messageInfo.setMessage(messageContent);
    return messageInfo;
}

bite_im::MessageInfo makeFileMessageInfo(int index, const QString& chatSessionId, const QByteArray& avatar)
{
    bite_im::MessageInfo messageInfo;
    messageInfo.setMessageId(QString::number(3000 + index));
    messageInfo.setChatSessionId(chatSessionId);
    messageInfo.setTimestamp(getTime());
    messageInfo.setSender(makeUserInfo(index, avatar));

    bite_im::FileMessageInfo fileMessageInfo;
    fileMessageInfo.setFileId("testFile");
    // 真实服务器推送的消息数据里, 本身也就不带图片的正文. 只是 fileId.
    // 需要通过 fileId, 二次发起请求, 通过 getSingleFile 接口来获取到内容.
    fileMessageInfo.setFileName("test.txt");
    // 此处文件大小, 无法设置. 由于 fileSize 属性, 不是 optional , 此处先设置一个 0 进来
    fileMessageInfo.setFileSize(0);

    bite_im::MessageContent messageContent;
    messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::FILE);
    messageContent.setFileMessage(fileMessageInfo);
    messageInfo.setMessage(messageContent);
    return messageInfo;
}

bite_im::MessageInfo makeSpeechMessageInfo(int index, const QString& chatSessionId, const QByteArray& avatar)
{
    bite_im::MessageInfo messageInfo;
    messageInfo.setMessageId(QString::number(3000 + index));
    messageInfo.setChatSessionId(chatSessionId);
    messageInfo.setTimestamp(getTime());
    messageInfo.setSender(makeUserInfo(index, avatar));

    bite_im::SpeechMessageInfo speechMessageInfo;
    // 真实服务器推送的消息数据里, 本身也就不带图片的正文. 只是 fileId.
    // 需要通过 fileId, 二次发起请求, 通过 getSingleFile 接口来获取到内容.
    speechMessageInfo.setFileId("testSpeech");

    bite_im::MessageContent messageContent;
    messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::SPEECH);
    messageContent.setSpeechMessage(speechMessageInfo);
    messageInfo.setMessage(messageContent);
    return messageInfo;
}

7.6 验证网络连通性

(1)修改客户端的 main.cpp , 添加网络测试代码:

cpp 复制代码
// 测试⽹络联通
#if TEST_NETWORK
 	network::NetClient netClient(nullptr);
 	netClient.ping();
#endif

运行客户端, 连接测试服务器,并验证是否 HTTP / Websocket网络能连通。

7.7 网络通信注意事项

  1. 不能使用两个 Qt Creator 分别启动服务器和客户端。后启动的程序 qDebug 会失效。提示:"无法获取调试输出"。
  2. websocket 客户端代码要编写完整,再连接服务器。否则会直接崩溃,而没有任何具体提示。
  3. ⼀定要确保 websocket 的 connected 信号触发之后,才能 sendTextMessage。否则不会有任何提示,但是消息发送不成功。Qt 这⼀套信号槽,用起来和 Node.js 非常相似的。时刻注意 "异步" 的问题。
  4. 每次更新完 PB,⼀定要记得服务器和客户端都需要重新编译运行!!否则程序会出现不可预期的错误。

8. 主界面逻辑的实现

8.1 获取个人信息

(1)客户端发送请求:

  • 在MainWidget::initSignalSlot函数当中添加获取信息的信号除力getMyselfDone槽函数:
cpp 复制代码
connect(dataCenter, &DataCenter::getMyselfDone, this, [=]() 
{
	// 从 DataCenter 中拿到响应结果的 myself, 把里面的头像取出来, 显示到界面上.
 	const auto* myself = dataCenter->getMyself();
 	this->userAvatar->setIcon(myself->avatar);
});

dataCenter->getMyselfAsync();
  • 编写 DataCenter::getMyselfAsync函数:
cpp 复制代码
void DataCenter::getMyselfAsync()
{
 	netClient.getMyself(loginSessionId);
}
  • 编写NetClient::getMyself函数以及接口定义:
cpp 复制代码
//个⼈信息获取-这个只⽤于获取当前登录⽤⼾的信息
// 客⼾端传递的时候只需要填充session_id即可
//其他个⼈/好友信息的获取在好友操作中完成
message GetUserInfoReq {
 	string request_id = 1;
 	optional string user_id = 2;
 	optional string session_id = 3;
}

message GetUserInfoRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
	UserInfo user_info = 4;
}

// 具体实现:
void NetClient::getMyself(const QString& loginSessionId)
{
    // 1. 构造出 HTTP 请求 body 部分
    bite_im::GetUserInfoReq req;
    req.setRequestId(makeRequestId());
    req.setSessionId(loginSessionId);
    QByteArray body = req.serialize(&serializer);
    LOG() << "[获取个人信息] 发送请求 requestId=" << req.requestId() << ", loginSessionId=" << loginSessionId;

    // 2. 构造出 HTTP 请求, 并发送出去.
    QNetworkReply* httpResp = sendHttpRequest("/service/user/get_user_info", body);

    // 3. 通过信号槽, 获取到当前的响应. finished 信号表示响应已经返回到客户端了.
    connect(httpResp, &QNetworkReply::finished, this, [=]()
    {
        // a) 先处理响应对象
        bool ok = false;
        QString reason;
        auto resp = handleHttpResponse<bite_im::GetUserInfoRsp>(httpResp, &ok, &reason);

        // b) 判定响应是否正确
        if (!ok)
        {
            LOG() << "[获取个人信息] 出错! requestId=" << req.requestId() << "reason=" << reason;
            return;
        }

        // c) 把结果保存在 DataCenter 中
        dataCenter->resetMyself(resp);

        // d) 通知调用逻辑, 响应已经处理完了. 仍然通过信号槽, 通知.
        emit dataCenter->getMyselfDone();

        // e) 打印日志.
        LOG() << "[获取个人信息] 处理响应 requestId=" << req.requestId();
    });
}

(2)客户端处理响应:

  • 实现 DataCenter::resetMyself函数:
cpp 复制代码
void DataCenter::resetMyself(std::shared_ptr<bite_im::GetUserInfoRsp> resp)
{
    if(myself == nullptr)
    {
        myself = new UserInfo();
    }

    const bite_im::UserInfo userInfo = resp->userInfo();
    myself->load(userInfo);
}
  • 定义DataCenter信号:
cpp 复制代码
signals:
 	// 获取个⼈信息完成
 	void getMyselfDone();

(3)服务器处理请求:

  • 编写 HttpServer::init 注册路由:
cpp 复制代码
httpServer.route("/service/user/get_user_info", [=](const QHttpServerRequest& req) 
{
 	return this->getUserInfo(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::getUserInfo(const QHttpServerRequest& req)
{
    // 解析请求, 把 req 的 body 取出来, 并且通过 pb 进行反序列化
    bite_im::GetUserInfoReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 获取用户信息] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId();

    // 构造响应数据
    bite_im::GetUserInfoRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");

    bite_im::UserInfo userInfo;
    userInfo.setUserId("1029");    // 调整自己的用户 id, 和返回的消息列表的内容匹配上
    userInfo.setNickname("张三");
    userInfo.setDescription("这是个性签名");
    userInfo.setPhone("18612345678");
    userInfo.setAvatar(loadFileToByteArray(":/resource/image/groupAvatar.png"));
    pbResp.setUserInfo(userInfo);

    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应数据
    QHttpServerResponse httpResp(body, QHttpServerResponse::StatusCode::Ok);
    httpResp.setHeader("Content-Type", "application/x-protobuf");
    return httpResp;
}

(4)整体流程小结:

8.2 获取好友列表

(1)客户端发送请求:

  • 在MainWidget::initSignalSlot添加槽函数:
cpp 复制代码
/// 获取好友列表

loadFriendList();
  • 具体实现loadFriendList函数:
cpp 复制代码
// 加载好友列表
void MainWidget::loadFriendList()
{
    // 好友列表数据是在 DataCenter 中存储的
    // 首先需要判定 DataCenter 中是否已经有数据了. 如果有数据, 直接加载本地的数据.
    // 如果没有数据, 从服务器获取
    DataCenter* dataCenter = DataCenter::getInstance();
    if(dataCenter->getFriendList() != nullptr)
    {
        // 从内存这个列表中加载数据
        updateFriendList();
    }
    else
    {
        // 通过网络来加载数据
        connect(dataCenter, &DataCenter::getFriendListDone, this, &MainWidget::updateFriendList, Qt::UniqueConnection);
        dataCenter->getFriendListAsync();
    }
}
  • 注意:

    • loadFriendList 不仅仅会在初始化时调用,也会在后续切换标签页时调用。
    • 多次 connect 虽然不会报错,但是会导致槽函数被⼀个信号触发多次。
    • 可以在 connect 的时候使用 Qt::UniqueConnection 参数(第五个参数),避免触发多次的情况。
  • 实现 DataCenter 中的 getFriendList和getFriendListAsync函数:

cpp 复制代码
QList<UserInfo>* DataCenter::getFriendList()
{
 	return friendList;
}
void DataCenter::getFriendListAsync()
{
 	netClient.getFriendList(loginSessionId);
}
  • 实现 NetClient::getFriendList函数:
cpp 复制代码
// 接⼝定义
//--------------------------------------
//好友列表获取
message GetFriendListReq {
 	string request_id = 1;
 	optional string user_id = 2;
 	optional string session_id = 3;
}

message GetFriendListRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
 	repeated UserInfo friend_list = 4;
}

// 代码实现
void NetClient::getFriendList(const QString& loginSessionId)
{
    // 1. 通过 protobuf 构造 body
    bite_im::GetFriendListReq req;
    req.setRequestId(makeRequestId());
    req.setSessionId(loginSessionId);
    QByteArray body = req.serialize(&serializer);
    LOG() << "[获取好友列表] 发送请求 requestId=" << req.requestId() << ", loginSessionId=" << loginSessionId;

    // 2. 发送 HTTP 请求
    QNetworkReply* httpResp = this->sendHttpRequest("/service/friend/get_friend_list", body);


    // 3. 处理响应
    connect(httpResp, &QNetworkReply::finished, this, [=]()
    {
        // a) 先处理响应对象
        bool ok = false;
        QString reason;
        auto friendListResp = this->handleHttpResponse<bite_im::GetFriendListRsp>(httpResp, &ok, &reason);

        // b) 判定响应是否正确
        if(!ok)
        {
            LOG() << "[获取好友列表] 失败! requestId=" << req.requestId() << ", reason=" << reason;
            return;
        }

        // c) 把结果保存在 DataCenter 中
        dataCenter->resetFriendList(friendListResp);

        // d) 发送信号, 通知界面, 当前这个操作完成了.
        emit dataCenter->getFriendListDone();

        // e) 打印日志.
        LOG() << "[获取好友列表] 处理响应 requestId=" << req.requestId();
    });
}

(2)客户端处理响应:

  • 编写 DataCenter::resetFriendList函数:
cpp 复制代码
void DataCenter::resetFriendList(std::shared_ptr<bite_im::GetFriendListRsp> resp)
{
    if(friendList == nullptr)
    {
        friendList = new QList<UserInfo>();
    }

    friendList->clear();
    QList<bite_im::UserInfo>& friendListPB = resp->friendList();
    for(auto& f : friendListPB)
    {
        UserInfo userinfo;
        userinfo.load(f);
        friendList->push_back(userinfo);
    }
}
  • 定义 DataCenter 信号:
cpp 复制代码
void getFriendListDone();
  • 实现 MainWidget::updateFriendList函数:
cpp 复制代码
void MainWidget::updateFriendList()
{
    if(activeTab != FRIEND_LIST)
    {
        // 当前的标签页不是好友列表, 就不渲染任何数据到界面上
        return;
    }

    DataCenter* dataCenter = DataCenter::getInstance();
    QList<UserInfo>* friendList = dataCenter->getFriendList();

    // 清空一下之前界面上的数据.
    sessionFriendArea->clear();

    // 遍历好友列表, 添加到界面上
    for (const auto& f : *friendList)
    {
        sessionFriendArea->addItem(FriendItemType, f.userId, f.avatar, f.nickname, f.description);
    }
}

(3)服务器处理请求:

  • 编写 HttpServer::init 注册路由:
cpp 复制代码
httpServer.route("/service/friend/get_friend_list", [=](constQHttpServerRequest& req) 
{
 	return this->getFriendList(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::getFriendList(const QHttpServerRequest& req)
{
    // 解析请求, 把 req 的 body 拿出来.
    bite_im::GetFriendListReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 获取好友列表] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId();

    // 构造响应
    bite_im::GetFriendListRsp pbRsp;
    pbRsp.setRequestId(pbReq.requestId());
    pbRsp.setSuccess(true);
    pbRsp.setErrmsg("");

    // 从文件读取数据操作, 其实是比较耗时的. (读取硬盘)
    // 耗时操作如果放在循环内部, 就会使整个的响应处理时间, 更长.
    QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");
    for(int i = 0; i < 20; i++)
    {
        bite_im::UserInfo userInfo = makeUserInfo(i, avatar);
        pbRsp.friendList().push_back(userInfo);
    }

    // 进行序列化
    QByteArray body = pbRsp.serialize(&serializer);

    // 构造成 HTTP 响应对象
    QHttpServerResponse httpResp(body, QHttpServerResponse::StatusCode::Ok);
    httpResp.setHeader("Content-Type", "application/x-protobuf");
    return httpResp;
}

(4)整体流程小结:

8.3 获取会话列表

(1)客户端发送请求:

  • 编写 MainWidget::init槽函数:
cpp 复制代码
/// 获取会话列表

loadSessionList();
  • 具体实现loadSessionList()函数:
cpp 复制代码
// 加载会话列表
void MainWidget::loadSessionList()
{
    // 先判定会话列表数据是否在本地 (DataCenter) 中存在. 如果本地存在, 直接构造界面内容.
    // 如果本地不存在, 则从服务器获取数据.
    DataCenter* dataCenter = DataCenter::getInstance();
    if(dataCenter->getFriendList() != nullptr)
    {
        // 从内存这个列表中加载数据
        updateChatSessionList();
    }
    else
    {
        // 从网络加载数据
        connect(dataCenter, &DataCenter::getChatSessionListDone, this, &MainWidget::updateChatSessionList, Qt::UniqueConnection);
        dataCenter->getChatSessionListAsync();
    }
}
  • 编写 DataCenter:
cpp 复制代码
QList<ChatSessionInfo>* DataCenter::getChatSessionList()
{
 	return chatSessionList;
}
void DataCenter::getChatSessionListAsync()
{
 	netClient.getChatSessionList(loginSessionId);
}
  • 编写 NetClient以及接口定义:
cpp 复制代码
//--------------------------------------
//会话列表获取
message GetChatSessionListReq {
 	string request_id = 1;
 	optional string session_id = 2;
 	optional string user_id = 3;
}

message GetChatSessionListRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
 	repeated ChatSessionInfo chat_session_info_list = 4;
}

// 函数实现
void NetClient::getChatSessionList(const QString& loginSessionId)
{
    // 1. 通过 protobuf 构造 body
    bite_im::GetChatSessionListReq req;
    req.setRequestId(makeRequestId());
    req.setSessionId(loginSessionId);
    QByteArray body = req.serialize(&serializer);
    LOG() << "[获取会话列表] 发送请求 requestId=" << req.requestId() << ", loginSessionId=" << loginSessionId;

    // 2. 发送 HTTP 请求
    QNetworkReply* resp = this->sendHttpRequest("/service/friend/get_chat_session_list", body);

    // 3. 针对响应进行处理
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::GetChatSessionListRsp>(resp, &ok, &reason);

        // b) 判定响应是否正确
        if (!ok)
        {
            LOG() << "[获取会话列表] 失败! reason=" << reason;
            return;
        }

        // c) 把得到的数据, 写入到 DataCenter 里
        dataCenter->resetChatSessionList(pbResp);

        // d) 通知调用者, 此处响应处理完毕
        emit dataCenter->getChatSessionListDone();

        // e) 打印日志
        LOG() << "[获取会话列表] 处理响应完毕! requestId=" << pbResp->requestId();
    });
}

(2)客户端处理响应:

  • 实现DataCenter::resetChatSessionList函数:
cpp 复制代码
void DataCenter::resetChatSessionList(std::shared_ptr<bite_im::GetChatSessionListRsp> resp)
{
    if(chatSessionList == nullptr)
    {
        chatSessionList = new QList<ChatSessionInfo>();
    }

    chatSessionList->clear();
    auto& chatSessionListPB = resp->chatSessionInfoList();
    for (auto& c : chatSessionListPB)
    {
        ChatSessionInfo chatSessionInfo;
        chatSessionInfo.load(c);
        chatSessionList->push_back(chatSessionInfo);
    }
}
  • 定义 DataCenter 信号:
cpp 复制代码
// 获取会话列表完成
void getChatSessionListDone();
  • 实现 MainWidget::updateChatSessionList函数:
cpp 复制代码
void MainWidget::updateChatSessionList()
{
    if(activeTab != SESSION_LIST)
    {
        // 当前的标签页不是好友列表, 就不渲染任何数据到界面上
        return;
    }

    DataCenter* dataCenter = DataCenter::getInstance();
    QList<ChatSessionInfo>* chatSessionList = dataCenter->getChatSessionList();

    sessionFriendArea->clear();
    // 遍历好友列表, 添加到界面上
    for (const auto& c : *chatSessionList)
    {
        if(c.lastMessage.messageType == TEXT_TYPE)
        {
            sessionFriendArea->addItem(SessionItemType, c.chatSessionId, c.avatar, c.chatSessionName, c.lastMessage.content);
        }
        else if(c.lastMessage.messageType == IMAGE_TYPE)
        {
            sessionFriendArea->addItem(SessionItemType, c.chatSessionId, c.avatar, c.chatSessionName, "[图片]");
        }
        else if(c.lastMessage.messageType == FILE_TYPE)
        {
            sessionFriendArea->addItem(SessionItemType, c.chatSessionId, c.avatar, c.chatSessionName, "[文件]");
        }
        else if(c.lastMessage.messageType == SPEECH_TYPE)
        {
            sessionFriendArea->addItem(SessionItemType, c.chatSessionId, c.avatar, c.chatSessionName, "[语音]");
        }
        else
        {
            LOG() << "错误的消息类型! messageType=" << c.lastMessage.messageType;
        }
    }
}

(3)服务器处理请求:

  • 编写 HttpServer::init 注册路由
cpp 复制代码
httpServer.route("/service/friend/get_chat_session_list", [=](constQHttpServerRequest& req) 
{
	return this->getChatSessionList(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::getChatSessionList(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::GetChatSessionListReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 获取会话列表] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId();

    // 构造响应
    bite_im::GetChatSessionListRsp pbRsp;
    pbRsp.setRequestId(pbReq.requestId());
    pbRsp.setSuccess(true);
    pbRsp.setErrmsg("");

    QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");

    // 构造若干个单聊会话
    for (int i = 0; i < 30; ++i)
    {
        bite_im::ChatSessionInfo chatSessionInfo;
        chatSessionInfo.setChatSessionId(QString::number(2000 + i));
        chatSessionInfo.setChatSessionName("会话" + QString::number(i));
        chatSessionInfo.setSingleChatFriendId(QString::number(1000 + i));
        chatSessionInfo.setAvatar(avatar);
        bite_im::MessageInfo messageInfo = makeTextMessageInfo(i, chatSessionInfo.chatSessionId(), avatar);

        chatSessionInfo.setPrevMessage(messageInfo);

        pbRsp.chatSessionInfoList().push_back(chatSessionInfo);
    }

    // 序列化响应
    QByteArray body = pbRsp.serialize(&serializer);
    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

(4)整体流程小结:

8.4 获取好友申请列表

(1)客户端发送请求:

  • 添加MainWidget::initSignalSlot槽函数:
cpp 复制代码
loadApplyList();
  • 具体实现loadApplyList()函数:
cpp 复制代码
// 加载好友申请列表
void MainWidget::loadApplyList()
{
    // 好友申请列表在 DataCenter 中存储的
    // 首先判定 DataCenter 本地是否已经有数据了. 如果有, 直接加载到界面上.
    // 如果没有则需要从服务器获取
    DataCenter* dataCenter = DataCenter::getInstance();
    if(dataCenter->getApplyList() != nullptr)
    {
        // 本地有数据, 直接加载
        updateApplyList();
    }
    else
    {
        // 本地没有数据, 通过网络加载
        connect(dataCenter, &DataCenter::getApplyListDone, this, &MainWidget::updateApplyList, Qt::UniqueConnection);
        dataCenter->getApplyListAsync();
    }
}
  • 实现 getApplyList 和 getApplyListAsync函数:
cpp 复制代码
QList<UserInfo> *DataCenter::getApplyList()
{
 	return applyList;
}

void DataCenter::getApplyListAsync()
{
 	netClient.getApplyList(loginSessionId);
}
  • 实现 NetClient::getApplyList和接口定义:
cpp 复制代码
//获取待处理的,申请⾃⼰好友的信息列表
message GetPendingFriendEventListReq {
 	string request_id = 1;
 	optional string session_id = 2;
 	optional string user_id = 3;
}

message FriendEvent {
 	string event_id = 1;
 	UserInfo sender = 3;
}

message GetPendingFriendEventListRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
 	repeated FriendEvent event = 4;
}

// 函数实现
void NetClient::getApplyList(const QString& loginSessionId)
{
    // 1. 通过 protobuf 构造 body
    bite_im::GetPendingFriendEventListReq req;
    req.setRequestId(makeRequestId());
    req.setSessionId(loginSessionId);
    QByteArray body = req.serialize(&serializer);
    LOG() << "[获取好友申请列表] 发送请求 requestId=" << req.requestId() << ", loginSessionId=" << loginSessionId;

    QNetworkReply* resp = sendHttpRequest("/service/friend/get_pending_friend_events", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::GetPendingFriendEventListRsp>(resp, &ok, &reason);

        // b) 判定结果是否出错
        if(!ok)
        {
            LOG() << "[获取好友申请列表] 失败! reason=" << reason;
            return;
        }

        // c) 拿到的数据, 写入到 DataCenter 中
        dataCenter->resetApplyList(pbResp);

        // d) 通知界面, 处理完毕
        emit dataCenter->getApplyListDone();

        // e) 打印日志
        LOG() << "[获取好友申请列表] 处理响应完成! requestId=" << req.requestId();
    });
}

(2)客户端处理响应:

  • 实现 DataCenter::resetApplyList函数:
cpp 复制代码
void DataCenter::resetApplyList(std::shared_ptr<bite_im::GetPendingFriendEventListRsp> resp)
{
    if(applyList == nullptr)
    {
        applyList = new QList<UserInfo>();
    }

    applyList->clear();
    auto& eventList = resp->event();
    for (auto& event : eventList)
    {
        UserInfo userInfo;
        userInfo.load(event.sender());
        applyList->push_back(userInfo);
    }
}
  • 定义 DataCenter 信号:
cpp 复制代码
void getApplyListDone();
  • 实现 MainWidget::updateApplyList函数:
cpp 复制代码
void MainWidget::updateFriendList()
{
    if(activeTab != FRIEND_LIST)
    {
        // 当前的标签页不是好友列表, 就不渲染任何数据到界面上
        return;
    }

    DataCenter* dataCenter = DataCenter::getInstance();
    QList<UserInfo>* friendList = dataCenter->getFriendList();

    // 清空一下之前界面上的数据.
    sessionFriendArea->clear();

    // 遍历好友列表, 添加到界面上
    for (const auto& f : *friendList)
    {
        sessionFriendArea->addItem(FriendItemType, f.userId, f.avatar, f.nickname, f.description);
    }
}

(3)服务器逻辑实现:

  • 注册路由:
cpp 复制代码
httpServer.route("/service/friend/get_pending_friend_events", [=](constQHttpServerRequest& req) 
{
 	return this->getApplyList(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::getApplyList(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::GetPendingFriendEventListReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 获取好友申请列表] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId();

    // 构造响应
    bite_im::GetPendingFriendEventListRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");

    // 循环构造出 event 对象, 构造出整个结果数组
    QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");
    for (int i = 0; i < 5; ++i)
    {
        bite_im::FriendEvent friendEvent;
        friendEvent.setEventId("");	// 此处不再使用这个 eventId, 直接设为 ""
        friendEvent.setSender(makeUserInfo(i, avatar));

        pbResp.event().push_back(friendEvent);
    }

    // 序列化成字节数组
    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应对象
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

(4)整体流程小结:

8.5 获取指定会话的近期消息

(1)点击会话列表中的列表项,获取该会话的最后 N 个历史消息,并展示到界面上。客户端发送请求:

  • 编写 SessionItem::active函数:
  • 此处的 active 在 select 中已经通过多态的方式调用到了。只要用户点击,就能触发这个逻辑:
cpp 复制代码
void SessionItem::active()
{
    // 点击之后, 要加载会话的历史消息列表
    LOG() << "点击 SessionItem 触发的逻辑! chatSessionId=" << chatSessionId;

    // 加载会话历史消息, 即会涉及到当前内存的数据操作, 又会涉及到网络通信, 还涉及到界面的变更.
    MainWidget* mainWidget = MainWidget::getInstance();
    mainWidget->loadRecentMessage(chatSessionId);

    // TODO 后续在这⾥添加针对未读消息的处理.
}
  • 编写 MainWidget::loadRecentMessages函数:
cpp 复制代码
void MainWidget::loadRecentMessage(const QString& chatSessionId)
{
    // 也是先判定, 本地内存中是否已经有对应的消息列表数据.
    // 有的话直接显示到界面上. 没有的话从网络获取.
    DataCenter* dataCenter = DataCenter::getInstance();

    if(dataCenter->getRecentMessageList(chatSessionId) != nullptr)
    {
        // 拿着本地数据更新界面
        updateRecentMessage(chatSessionId);
    }
    else
    {
        // 本地没有数据, 从网络加载
        connect(dataCenter, &DataCenter::getRecentMessageListDone, this, &MainWidget::updateRecentMessage, Qt::UniqueConnection);
        dataCenter->getRecentMessageListAsync(chatSessionId, true);
    }
}
  • 编写 DataCenter当中的对应函数:
cpp 复制代码
void DataCenter::getRecentMessageListAsync(const QString& chatSessionId, bool updateUI)
{
    netClient.getRecentMessageList(loginSessionId, chatSessionId, updateUI);
}

QList<Message>* DataCenter::getRecentMessageList(const QString& chatSessionId)
{
    if(!recentMessages->contains(chatSessionId))
    {
        return nullptr;
    }

    return &(*recentMessages)[chatSessionId];
}
  • 编写 NetClient和接口定义:
cpp 复制代码
message GetRecentMsgReq {
 	string request_id = 1;
 	string chat_session_id = 2;
 	int64 msg_count = 3;
 	optional int64 cur_time = 4;//⽤于扩展获取指定时间前的n条消息
 	optional string user_id = 5;
 	optional string session_id = 6;
}
message GetRecentMsgRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
 	repeated MessageInfo msg_list = 4;
}

// 函数实现
void NetClient::getRecentMessageList(const QString& loginSessionId, const QString& chatSessionId, bool updateUI)
{
    // 1. 通过 protobuf 构造请求 body
    bite_im::GetRecentMsgReq req;
    req.setRequestId(makeRequestId());
    req.setChatSessionId(chatSessionId);
    req.setMsgCount(50);	// 此处固定获取最近 50 条记录
    req.setSessionId(loginSessionId);
    QByteArray body = req.serialize(&serializer);
    LOG() << "[获取最近消息] 发送请求 requestId=" << req.requestId() << ", loginSessionId=" << loginSessionId << ", chatSessionId=" << chatSessionId;

    // 2. 发送 http 请求
    QNetworkReply* resp = this->sendHttpRequest("/service/message_storage/get_recent", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应, 反序列化
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::GetRecentMsgRsp>(resp, &ok, &reason);

        // b) 判定响应是否出错
        if(!ok)
        {
            LOG() << "[获取最近消息] 失败! reason=" << reason;
            return;
        }

        // c) 把拿到的数据, 设置到 DataCenter 中
        dataCenter->resetRecentMessageList(chatSessionId, pbResp);

        // d) 发送信号, 告知界面进行更新
        if (updateUI)
        {
            emit dataCenter->getRecentMessageListDone(chatSessionId);
        }
        else
        {
            emit dataCenter->getRecentMessageListDoneNoUI(chatSessionId);
        }

    });
}

(2)客户端处理响应:

  • 实现 DataCenter::resetRecentMsgList函数:
cpp 复制代码
void DataCenter::resetRecentMessageList(const QString& chatSessionId, std::shared_ptr<bite_im::GetRecentMsgRsp> resp)
{
    // 拿到 chatSessionId 对应的消息列表, 并清空
    // 注意此处务必是引用类型, 才是修改哈希表内部的内容.
    QList<Message>& messageList = (*recentMessages)[chatSessionId];
    messageList.clear();

    for(auto& m : resp->msgList())
    {
        Message message;
        message.load(m);

        messageList.push_back(message);
    }
}
  • 定义 DataCenter 信号:
cpp 复制代码
// 获取近期消息完成
void getRecentMsgListDone(const QString& chatSessionId); // 更新UI
void getRecentMsgListDoneNoUI(const QString& chatSessionId); // 不更新 UI
  • 实现 MainWidget::updateRecentMessages函数:
cpp 复制代码
void MainWidget::updateRecentMessage(const QString& chatSessionId)
{
    // 1. 拿到该会话的最近消息列表
    DataCenter* dataCenter = DataCenter::getInstance();
    auto* recentMessageList = dataCenter->getRecentMessageList(chatSessionId);

    // 2. 清空原有界面上显示的消息列表
    messageShowArea->clear();

    // 3. 根据当前拿到的消息列表, 显示到界面上
    //    此处把数据显示到界面上, 可以使用头插, 也可以使用尾插.
    //    这里打算使用头插的方式来进行实现.
    //    主要因为消息列表来说, 用户首先看到的, 应该是 "最近" 的消息, 也就是 "末尾" 的消息.
    for(int i = recentMessageList->size() - 1; i >= 0; --i)
    {
        const Message& message = recentMessageList->at(i);
        bool isLeft = message.sender.userId != dataCenter->getMyself()->userId;
        messageShowArea->addFrontMessage(isLeft, message);
    }

    // 4. 设置会话标题
    ChatSessionInfo* chatSessionInfo = dataCenter->findChatSessionById(chatSessionId);
    if(chatSessionInfo != nullptr)
    {
        // 把会话名称显示到界面上.
        sessionTitleLabel->setText(chatSessionInfo->chatSessionName);
    }

    // 5. 保存当前选中的会话是哪个.
    dataCenter->setCurrentChatSessionId(chatSessionId);

    // 6. 自动把滚动条, 滚动到末尾
    messageShowArea->scrollToEnd();
}
  • 实现 DataCenter::findChatSessionById函数方便找到对应的会话id:
cpp 复制代码
ChatSessionInfo* DataCenter::findChatSessionById(const QString& chatSessionId)
{
    if(chatSessionList == nullptr)
    {
        return nullptr;
    }

    for(auto& info : *chatSessionList)
    {
        if (info.chatSessionId == chatSessionId)
        {
            return &info;
        }
    }

    return nullptr;
}
  • 实现 DataCenter::setCurrentChatSessionId 和DataCenter::getCurrentChatSessionId方便设置会话id和获取会话id:
cpp 复制代码
void DataCenter::setCurrentChatSessionId(const QString &chatSessionId)
{
    this->currentChatSessionId = chatSessionId;
}

const QString& DataCenter::getCurrentChatSessionId()
{
    return this->currentChatSessionId;
}

(3)服务器处理请求:

  • 注册路由:
cpp 复制代码
httpServer.route("/service/message_storage/get_recent", [=](constQHttpServerRequest& req) 
{
 	return this->getRecent(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::getRecent(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::GetRecentMsgReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 获取最近消息列表] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", chatSessionId=" << pbReq.chatSessionId();

    // 构造响应
    bite_im::GetRecentMsgRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");

    QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");

    for(int i = 0; i < 30; ++i)
    {
        bite_im::MessageInfo messageInfo = makeTextMessageInfo(i, "2000", avatar);
        pbResp.msgList().push_back(messageInfo);
    }

    // 序列化
    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应对象
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

(4)整体流程小结:

8.6 点击某个好友项

(1)切换到会话列表:

  • 编写 FriendItem::active:
  • active 已经在 select 方法中通过多态的方式调用到了:
cpp 复制代码
void FriendItem::active()
{
 	LOG() << "FriendItem active. userId=" << userId;
 	// 切换到当前会话. 如果没有就创建会话
 	MainWidget* mainWidget = MainWidget::getInstance();
 	mainWidget->switchToSession(userId);
}

(2)该会话置顶并被选中:

  • 实现 MainWidget::switchSession函数:
cpp 复制代码
void MainWidget::switchSession(const QString& userId)
{
    // 1. 在会话列表中, 先找到对应的会话元素
    DataCenter* dataCenter = DataCenter::getInstance();
    ChatSessionInfo* chatSessionInfo = dataCenter->findChatSessionByUserId(userId);

    if(chatSessionInfo == nullptr)
    {
        // 正常来说, 每个好友, 都会有一个对应的会话(哪怕从来没说过话).
        // 添加好友的时候, 就创建出来的会话.
        LOG() << "[严重错误] 当前选中的好友, 对应的会话不存在!";
        return;
    }

    // 2. 把选中的会话置顶, 把这个会话信息放到整个会话列表的第一个位置.
    //    后续在界面显示的时候, 就是按照列表的顺序, 从前往后显示的.
    dataCenter->topChatSessionInfo(*chatSessionInfo);

    // 3. 切换到会话列表标签页
    switchTabToSession();

    // 4. 加载这个会话对应的历史消息. 刚刚做了一个 "置顶操作" , 被选中的好友对应的会话, 在会话列表的最前头, 也就是 0 号下标.
    sessionFriendArea->clickItem(0);
}

switchTabToSession已经在前⾯实现过了。

  • 实现 DataCenter::findChatSessionByUserId函数方便找到用户id:
cpp 复制代码
ChatSessionInfo* DataCenter::findChatSessionByUserId(const QString& userId)
{
    if(chatSessionList == nullptr)
    {
        return nullptr;
    }

    for(auto& info : *chatSessionList)
    {
        if (info.userId == userId)
        {
            return &info;
        }
    }

    return nullptr;
}
  • 实现 DataCenter::topChatSessionInfo函数将选中好友置顶:
cpp 复制代码
void DataCenter::topChatSessionInfo(const ChatSessionInfo &chatSessionInfo)
{
    if(chatSessionList == nullptr)
    {
        return;
    }

    // 1. 把这个元素从列表中找到
    auto iter = chatSessionList->begin();
    for(; iter != chatSessionList->end(); ++iter)
    {
        if(iter->chatSessionId == chatSessionInfo.chatSessionId)
        {
            break;
        }
    }

    if(iter == chatSessionList->end())
    {
        // 上面的循环没有找到匹配的元素, 直接返回. 正常来说, 不会走这个逻辑的.
        return;
    }

    // 2. 把这个元素备份一下, 然后删除
    ChatSessionInfo backup = chatSessionInfo;
    chatSessionList->erase(iter);

    // 3. 把备份的元素, 插入到头部
    chatSessionList->push_front(backup);
}
  • 实现 SessionFriendArea::clickItem函数:
cpp 复制代码
void SessionFriendArea::clickItem(int index)
{
    if(index < 0 || index >= container->layout()->count())
    {
        LOG() << "点击元素的下标超出范围! index=" << index;
        return;
    }

    QLayoutItem* layoutItem = container->layout()->itemAt(index);
    if(layoutItem == nullptr || layoutItem->widget() == nullptr)
    {
        LOG() << "指定的元素不存在! index=" << index;
        return;
    }

    SessionFriendItem* item = dynamic_cast<SessionFriendItem*>(layoutItem->widget());
    item->select();
}

(3)加载该会话的最近消息并显示:

  • 在上述 clickItem 中会调⽤ item->select() , 进⼀步调⽤到 active ⽅法, 从⽽触发加载最近消息的逻辑.

(4)整体流程小结:

(5)注意:

  • 每个会话中的用户列表,应该是按需加载的,不应该是程序启动全都加载进来!!
  • 创建会话操作放到同意好友申请时。换而言之每个用户都⼀定存在⼀个和他对应的会话。

9. 小结

(1)在进行前后端交互接口的实现的时候代码格式基本上都是一样的,只需要将其中一个流程搞清楚即可。如下图就是基本的流程图了:

(2)剩下的需要实现的前后端交互接口见博客:https://blog.csdn.net/m0_65558082/article/details/143817211?spm=1001.2014.3001.5502。

客户端整体代码链接https://gitee.com/liu-yechi/new_code/tree/master/chat_system/client。

相关推荐
Lee川3 小时前
深度拆解:基于面向对象思维的“就地编辑”组件全模块解析
javascript·架构
勤劳打代码3 小时前
Flutter 架构日记 — 状态管理
flutter·架构·前端框架
AI攻城狮8 小时前
OpenClaw 里 TAVILY_API_KEY 明明写在 ~/.bashrc,为什么还是失效?一次完整排查与修复
人工智能·云原生·aigc
子兮曰9 小时前
后端字段又改了?我撸了一个 BFF 数据适配器,从此再也不怕接口“屎山”!
前端·javascript·架构
卓卓不是桌桌11 小时前
如何优雅地处理 iframe 跨域通信?这是我的开源方案
javascript·架构
Qlly11 小时前
DDD 架构为什么适合 MCP Server 开发?
人工智能·后端·架构
阿里云云原生1 天前
零配置部署顶级模型!函数计算一键解锁 Qwen3.5
云原生
AI攻城狮1 天前
Kimi Bot + OpenClaw 完整配置指南:5 步实现本地 AI Agent 集成
人工智能·云原生·aigc
用户881586910911 天前
AI Agent 协作系统架构设计与实践
架构
鹏北海1 天前
Qiankun 微前端实战踩坑历程
前端·架构