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

目录

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

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

(2)会话信息:

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

(3)消息信息:

//消息类型
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;//消息IDstring chat_session_id = 2;//消息所属聊天会话IDint64 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)请求响应基本格式:

//通信接口统一采用POST请求实现,正文采用protobuf协议进行组织
/*  HTTP HEADER:POST /service/xxxxxContent-Type: application/x-protobufContent-Length: 123xxxxxx-------------------------------------------------------HTTP/1.1 200 OK Content-Type: application/x-protobufContent-Length: 123xxxxxxxxxx
*/

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

//在客户端与网关服务器的通信中,使用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)身份认证:

/*消息推送使用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)消息推送。当前存在五种消息推送:

  • 申请好友通知。
  • 好友申请处理通知 (同意/拒绝)。
  • 创建消息会话通知。
  • 收到消息通知。
  • 删除好友通知。
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的数据转化函数:(类里面的成员变量没有写出来):

//
/// 用户信息
//
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:// 通过这个方法生成唯一的 messageIdstatic 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类来管理所有客户端需要的数据。这是一个单例类:

class DataCenter : public QObject
{Q_OBJECT
public:static DataCenter* getInstance();~DataCenter();private:DataCenter();static DataCenter* instance;// 列出 DataCenter 中要组织管理的所有的数据// 当前客户端登录到服务器对应的登录会话 idQString 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;// 短信验证码的验证 idQString currentVerifyCodeId = "";// 让 DataCenter 持有 NetClient 实例.network::NetClient netClient;public:// 初始化数据文件void initDataFile();// 存储数据到文件中void saveDataFile();// 从数据文件中加载数据到内存void loadDataFile();signals:
};

(2)具体实现:

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 和 未读消息信息:

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 的指针。

class NetClient : public QObject
{Q_OBJECTprivate:// 定义重要常量. ip 都暂时使用本地的环回 ip. 端口号约定成 8000 和 8001const 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);// 生成请求 idstatic 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)进行网络测试:

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;}// 获取到响应的 bodyQByteArray body = httpResp->readAll();LOG() << "响应内容: " << body;httpResp->deleteLater();});
}

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

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. 获取到响应的 bodyQByteArray 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:

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)初始化身份信息:

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 消息推送的逻辑:

void NetClient::handleWsResponse(const bite_im::NotifyMessage& notifyMessage)
{if(notifyMessage.notifyType() == bite_im::NotifyTypeGadget::NotifyType::CHAT_MESSAGE_NOTIFY){// 收到消息// 1. 把 pb 中的 MessageInfo 转成客户端自己的 Messagemodel::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), 但是使用图形界面更合适⼀些。尤其是后面构造⼀些测试数据,图形界面更方便进行操作。比如在界面上提供不同的按钮,按下不同按钮就可以给客户端推送不同的数据:

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类来实现此功能:

class HttpServer : public QObject
{Q_OBJECTpublic:static HttpServer* getInstance();// 通过这个函数, 针对 HTTP Server 进行初始化 (绑定端口, 配置路由....)bool init();private:static HttpServer* instance;HttpServer() {}QHttpServer httpServer;QProtobufSerializer serializer;signals:
};

(2)具体实现:

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类来实现此功能:

class WebsocketServer : public QObject
{Q_OBJECTprivate:static WebsocketServer* instance;WebsocketServer() : websocketServer("websocket server", QWebSocketServer::NonSecureMode) {}QWebSocketServer websocketServer;QProtobufSerializer serializer;public:static WebsocketServer* getInstance();bool init();int messageIndex = 0;signals:
};

(2)具体实现:

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 中的 acceptQWebSocket* 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增加内容文件:

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。从
target_link_libraries(ChatServerMock Qt${QT_VERSION_MAJOR}::Core Qt6::Network Qt6::WebSockets)
  • 修改为:
target_link_libraries(ChatServerMock PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt6::Network Qt6::WebSockets)

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

(1)工具函数:

// 读写文件操作.
// 从指定文件中, 读取所有的二进制内容. 得到一个 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)构造数据函数:

// 生成默认的 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 , 添加网络测试代码:

// 测试⽹络联通
#if TEST_NETWORKnetwork::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槽函数:
connect(dataCenter, &DataCenter::getMyselfDone, this, [=]() 
{// 从 DataCenter 中拿到响应结果的 myself, 把里面的头像取出来, 显示到界面上.const auto* myself = dataCenter->getMyself();this->userAvatar->setIcon(myself->avatar);
});dataCenter->getMyselfAsync();
  • 编写 DataCenter::getMyselfAsync函数:
void DataCenter::getMyselfAsync()
{netClient.getMyself(loginSessionId);
}
  • 编写NetClient::getMyself函数以及接口定义:
//个⼈信息获取-这个只⽤于获取当前登录⽤⼾的信息
// 客⼾端传递的时候只需要填充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函数:
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信号:
signals:// 获取个⼈信息完成void getMyselfDone();

(3)服务器处理请求:

  • 编写 HttpServer::init 注册路由:
httpServer.route("/service/user/get_user_info", [=](const QHttpServerRequest& req) 
{return this->getUserInfo(req);
});
  • 实现处理函数:
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添加槽函数:

/// 获取好友列表

loadFriendList();
  • 具体实现loadFriendList函数:
// 加载好友列表
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函数:

QList<UserInfo>* DataCenter::getFriendList()
{return friendList;
}
void DataCenter::getFriendListAsync()
{netClient.getFriendList(loginSessionId);
}
  • 实现 NetClient::getFriendList函数:
// 接⼝定义
//--------------------------------------
//好友列表获取
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 构造 bodybite_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函数:
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 信号:
void getFriendListDone();
  • 实现 MainWidget::updateFriendList函数:
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 注册路由:
httpServer.route("/service/friend/get_friend_list", [=](constQHttpServerRequest& req) 
{return this->getFriendList(req);
});
  • 实现处理函数:
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槽函数:

/// 获取会话列表

loadSessionList();
  • 具体实现loadSessionList()函数:
// 加载会话列表
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:
QList<ChatSessionInfo>* DataCenter::getChatSessionList()
{return chatSessionList;
}
void DataCenter::getChatSessionListAsync()
{netClient.getChatSessionList(loginSessionId);
}
  • 编写 NetClient以及接口定义:
//--------------------------------------
//会话列表获取
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 构造 bodybite_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函数:
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 信号:
// 获取会话列表完成
void getChatSessionListDone();
  • 实现 MainWidget::updateChatSessionList函数:
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 注册路由
httpServer.route("/service/friend/get_chat_session_list", [=](constQHttpServerRequest& req) 
{return this->getChatSessionList(req);
});
  • 实现处理函数:
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槽函数:
loadApplyList();
  • 具体实现loadApplyList()函数:
// 加载好友申请列表
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函数:
QList<UserInfo> *DataCenter::getApplyList()
{return applyList;
}void DataCenter::getApplyListAsync()
{netClient.getApplyList(loginSessionId);
}
  • 实现 NetClient::getApplyList和接口定义:
//获取待处理的,申请⾃⼰好友的信息列表
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 构造 bodybite_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函数:
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 信号:
void getApplyListDone();
  • 实现 MainWidget::updateApplyList函数:
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.route("/service/friend/get_pending_friend_events", [=](constQHttpServerRequest& req) 
{return this->getApplyList(req);
});
  • 实现处理函数:
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 中已经通过多态的方式调用到了。只要用户点击,就能触发这个逻辑:
void SessionItem::active()
{// 点击之后, 要加载会话的历史消息列表LOG() << "点击 SessionItem 触发的逻辑! chatSessionId=" << chatSessionId;// 加载会话历史消息, 即会涉及到当前内存的数据操作, 又会涉及到网络通信, 还涉及到界面的变更.MainWidget* mainWidget = MainWidget::getInstance();mainWidget->loadRecentMessage(chatSessionId);// TODO 后续在这⾥添加针对未读消息的处理.
}
  • 编写 MainWidget::loadRecentMessages函数:
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当中的对应函数:
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和接口定义:
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 构造请求 bodybite_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函数:
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 信号:
// 获取近期消息完成
void getRecentMsgListDone(const QString& chatSessionId); // 更新UI
void getRecentMsgListDoneNoUI(const QString& chatSessionId); // 不更新 UI
  • 实现 MainWidget::updateRecentMessages函数:
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:
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:
void DataCenter::setCurrentChatSessionId(const QString &chatSessionId)
{this->currentChatSessionId = chatSessionId;
}const QString& DataCenter::getCurrentChatSessionId()
{return this->currentChatSessionId;
}

(3)服务器处理请求:

  • 注册路由:
httpServer.route("/service/message_storage/get_recent", [=](constQHttpServerRequest& req) 
{return this->getRecent(req);
});
  • 实现处理函数:
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 方法中通过多态的方式调用到了:
void FriendItem::active()
{LOG() << "FriendItem active. userId=" << userId;// 切换到当前会话. 如果没有就创建会话MainWidget* mainWidget = MainWidget::getInstance();mainWidget->switchToSession(userId);
}

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

  • 实现 MainWidget::switchSession函数:
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:
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函数将选中好友置顶:
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函数:
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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/473231.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot集成itext导出PDF

添加依赖 <!-- PDF导出 --><dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.11</version></dependency><dependency><groupId>com.itextpdf</groupId>&l…

[ACTF2020]Upload 1--详细解析

信息收集 题目告诉我们是一道upload&#xff0c;也就是文件上传漏洞题目。 进入界面&#xff0c;是一个灯泡&#xff0c;将鼠标放在图标上就会出现文件上传的相应位置&#xff1a; 思路 文件上传漏洞&#xff0c;先看看有没有前端校验。 在js源码中找到了前端校验&#xff…

针对股票评论的情感分类器

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;编程探索专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年11月16日13点39分 神秘男子影, 秘而不宣藏。 泣意深不见, 男子自持重, 子夜独自沉。 论文链接 点击开启你的论文编程之旅…

大数据-226 离线数仓 - Flume 优化配置 自定义拦截器 拦截原理 了 拦截器实现 Java

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇开始了&#xff01; 目前开始更新 MyBatis&#xff0c;一起深入浅出&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff0…

【工具插件类教学】在 Unity 中使用 iTextSharp 实现 PDF 文件生成与导出

目录 一、准备工作 1. 安装 iTextSharp 2. 准备资源文件 二、创建 ExportPDFTool 脚本 1、初始化 PDF 文件,设置字体 2、添加标题、内容、表格和图片 三、使用工具类生成 PDF 四、源码地址 在 Unity 项目中,我们有时会需要生成带有文本、表格和图片的 PDF 文件,以便…

【Node.js】使用 Node.js 需要了解多少 JavaScript?

在现代开发中&#xff0c;Node.js 已经成为了构建高性能、可扩展的服务器端应用的必备工具。作为一个基于 JavaScript 的运行时环境&#xff0c;Node.js 使得开发者能够使用同一种语言来编写前后端代码&#xff0c;这在全栈开发中尤为重要。那么&#xff0c;使用 Node.js 开发时…

GRE做题笔记(零散的个人经验)

locomotive机车By 1813, the Luddite resistance had all but vanished. all but表示“几乎完全”的程度&#xff0c;或者表示排除piston活塞attributed to 归因于how a sportsperson accounted for their own experience of stress 运动员如何解释自己的压力经历 &#xff0c;…

Android OpenGL ES详解——实例化

目录 一、实例化 1、背景 2、概念 实例化、实例数量 gl_InstanceID 应用举例 二、实例化数组 1、概念 2、应用举例 三、应用举例——小行星带 1、不使用实例化 2、使用实例化 四、总结 一、实例化 1、背景 假如你有一个有许多模型的场景&#xff0c;而这些模型的…

Python3.11.9+selenium,选择证书用多线程+键盘enter解决

Python3.11.9+selenium,选择证书用多线程+键盘enter解决 1、遇到问题:弹出证书选择,无法点击确定 import pyautogui pyautogui.press(enter) 键盘enter也无法点击 2、解决办法:用多线程解决同时执行click链接和Enter点击证书的确定 1、点击操作 # # 通过文本链接文本…

[Android]相关属性功能的裁剪

1.将home界面的search bar 移除 /src/com/android/launcher3/graphics/LauncherPreviewRenderer.java // Add first page QSBif (FeatureFlags.QSB_ON_FIRST_SCREEN) {CellLayout firstScreen mWorkspaceScreens.get(FIRST_SCREEN_ID);View qsb mHomeElementInflater.infla…

linux笔记(防火墙)

一、概述 防火墙的作用 在 Linux 系统中&#xff0c;防火墙用于控制进出系统的网络流量&#xff0c;基于预定义的安全规则允许或拒绝数据包&#xff0c;从而保护系统免受未经授权的访问、恶意攻击&#xff0c;并确保网络服务的安全运行。 二、常见的 Linux 防火墙软件 iptabl…

WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇

WebRTC视频 01 - 视频采集整体架构 WebRTC视频 02 - 视频采集类 VideoCaptureModule [WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇]&#xff08;本文&#xff09; WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇 WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇 一、前…

高光谱深度学习调研

综述 高光谱深度学习只有小综述&#xff0c;没有大综述。小综述里面场景分类、目标检测的综述比较多。 Wang C, Liu B, Liu L, et al. A review of deep learning used in the hyperspectral image analysis for agriculture[J]. Artificial Intelligence Review, 2021, 54(7)…

计算机视觉 1-8章 (硕士)

文章目录 零、前言1.先行课程&#xff1a;python、深度学习、数字图像处理2.查文献3.环境安装 第一章&#xff1a;概论1.计算机视觉的概念2.机器学习 第二章&#xff1a;图像处理相关基础1.图像的概念2.图像处理3.滤波器4.卷积神经网络CNN5.图像的多层表示&#xff1a;图像金字…

24-Ingest Pipeline Painless Script

将文档中的tags字段按照逗号&#xff08;,&#xff09;分隔符进行分割。 同时为文档&#xff0c;增加一个字段。blog查看量 DELETE tech_blogs#Blog数据&#xff0c;包含3个字段&#xff0c;tags用逗号间隔 PUT tech_blogs/_doc/1 {"title":"Introducing big …

【ubuntu18.04】vm虚拟机复制粘贴键不能用-最后无奈换版本

我是ubuntu16版本的 之前费老大劲安装的vmware tools结果不能用 我又卸载掉&#xff0c;安装了open-vm-tools 首先删除VMware tools sudo vmware-uninstall-tools.pl sudo rm -rf /usr/lib/vmware-tools sudo apt-get autoremove open-vm-tools --purge再下载open-vm-tools s…

Android Mobile Network Settings | APN 菜单加载异常

问题 从log看是有创建APN对应的Controller&#xff08;功能逻辑是ok的&#xff09;&#xff0c;但是Mobile Network Settings无法显示&#xff08;UI异常&#xff09;。 相关术语&#xff1a; GSM&#xff08;Global System for Mobile Communications&#xff09; 全球移动…

AndroidStudio-Activity的生命周期

一、Avtivity的启动和结束 从当前页面跳到新页面&#xff0c;跳转代码如下&#xff1a; startActivity(new Intent(源页面.this&#xff0c;目标页面.class))&#xff1b; 从当前页面回到上一个页面&#xff0c;相当于关闭当前页面&#xff0c;返回代码如下&#xff1a; finis…

python机器人Agent编程——多Agent框架的底层逻辑(上)

目录 一、前言二、两个核心概念2.1 Routines&#xff08;1&#xff09;清晰的Prompt&#xff08;2&#xff09;工具调用json schema自动生成&#xff08;3&#xff09;解析模型的toolcall指令&#xff08;4&#xff09;单Agent的循环决策与输出 PS.扩展阅读ps1.六自由度机器人相…

SOP搭建:企业标准化操作程序构建与实施指南

一、引言 在当今充满竞争的商业领域&#xff0c;实现企业运营的标准化、高效化和高质量化是提升企业市场竞争力的关键所在。标准操作程序&#xff08;SOP&#xff09;作为一种至关重要的管理工具&#xff0c;能够清晰地阐述业务流程&#xff0c;规范操作行为&#xff0c;并促进…