微服务即时通讯系统的实现(客户端)----(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 网络通信注意事项
- 不能使用两个 Qt Creator 分别启动服务器和客户端。后启动的程序 qDebug 会失效。提示:“无法获取调试输出”。
- websocket 客户端代码要编写完整,再连接服务器。否则会直接崩溃,而没有任何具体提示。
- ⼀定要确保 websocket 的 connected 信号触发之后,才能 sendTextMessage。否则不会有任何提示,但是消息发送不成功。Qt 这⼀套信号槽,用起来和 Node.js 非常相似的。时刻注意 “异步” 的问题。
- 每次更新完 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。
相关文章:

微服务即时通讯系统的实现(客户端)----(2)
目录 1. 将protobuf引入项目当中2. 前后端交互接口定义2.1 核心PB类2.2 HTTP接口定义2.3 websocket接口定义 3. 核心数据结构和PB之间的转换4. 设计数据中心DataCenter类5. 网络通信5.1 定义NetClient类5.2 引入HTTP5.3 引入websocket 6. 小结7. 搭建测试服务器7.1 创建项目7.2…...
QT使用libssh2库实现sftp文件传输
本篇文章通过用户名和密码来连接服务器端,通过密匙连接服务器端可以参考另外一篇文章: https://blog.csdn.net/u012372584/article/details/143826199?sharetype=blogdetail&sharerId=143826199&sharerefer=PC&sharesource=u012372584&spm=1011.2480.3001.…...

【Linux】进程的优先级
进程的优先级 一.概念二.修改优先级的方法三.进程切换的大致原理:四.上下文数据的保存位置: 一.概念 cpu资源分配的先后顺序,就是指进程的优先权(priority)。 优先权高的进程有优先执行权利。配置进程优先权对多任务环…...

python实现十进制转换二进制,tkinter界面
目录 需求 效果 代码实现 代码解释 需求 python实现十进制转换二进制 效果 代码实现 import tkinter as tk from tkinter import messageboxdef convert_to_binary():try:# 获取输入框中的十进制数decimal_number int(entry.get())# 转换为二进制binary_number bin(de…...

电子应用设计方案-12:智能窗帘系统方案设计
一、系统概述 本设计方案旨在打造便捷、高效的全自动智能窗帘系统。 二、硬件选择 1. 电机:选用低噪音、扭矩合适的智能电机,根据窗帘尺寸和重量确定电机功率,确保能平稳拉动窗帘。 2. 轨道:选择坚固、顺滑的铝合金轨道&…...
力扣 回文链表-234
回文链表-234 const int N 1e55; int a[N];//定义一个整形的全局数组作为辅助数组存储链表反转前的值 class Solution { /*本题的解题思路是先将链表中每个值存储到辅助数组a中,然后反转链表, 最后,反转后链表的值和没反转之前的值…...
采样率22050,那么CHUNK_SIZE 一次传输的音频数据大小设置多少合适?unity接收后出现卡顿的问题的思路
在采样率为22050的情况下,选择合适的 CHUNK_SIZE 主要取决于 Unity 接收和处理音频数据的效率。以下是设置 CHUNK_SIZE 的一些建议: 计算 CHUNK_SIZE:音频的传输数据量可以通过公式 CHUNK_SIZE 采样率 * 传输间隔秒数 * 每样本字节数 * 声道…...

网络初识--Java
一、网络通信基础 1.IP地址 IP地址主要⽤于标识⽹络主机、其他⽹络设备(如路由器)的⽹络地址。简单说,IP地址⽤于定位主 机的⽹络地址。 就像我们发送快递⼀样,需要知道对⽅的收货地址,快递员才能将包裹送到⽬的地。…...

K8S单节点部署及集群部署
1.Minikube搭建单节点K8S 前置条件:安装docker,注意版本兼容问题 # 配置docker源 wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.repos.d/docker-ce.repo# 安装docker环境依赖 yum install -y yum-utils device-m…...

GPIO相关的寄存器(重要)
目录 一、GPIO相关寄存器概述 二、整体介绍 三、详细介绍 1、端口配置低寄存器(GPIOx_CRL)(xA...E) 2、端口配置高寄存器(GPIOx_CRH)(xA...E) 3、端口输入数据寄存器ÿ…...
OpenCV基础
1. 基础入门:OpenCV概念与安装 a. OpenCV简介 OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉库,广泛应用于图像和视频处理、计算机视觉、机器学习等领域。 b. 安装OpenCV Python安装: pip in…...

两行命令搭建深度学习环境(Docker/torch2.5.1+cu118/命令行美化+插件),含完整的 Docker 安装步骤
深度学习环境的配置过于繁琐,所以我制作了两个基础的镜像,希望可以帮助大家节省时间,你可以选择其中一种进行安装,版本说明: base 版本基于 pytorch/pytorch:2.5.1-cuda11.8-cudnn9-devel,默认 python 版本…...

Redis做分布式锁
(一)为什么要有分布式锁以及本质 在一个分布式的系统中,会涉及到多个客户端访问同一个公共资源的问题,这时候我们就需要通过锁来做互斥控制,来避免类似于线程安全的问题 因为我们学过的sychronized只能对线程加锁&…...
lambdaQueryWrapper详细解释
LambdaQueryWrapper 是 MyBatis Plus 提供的一个强大的查询条件构建工具,它允许你使用 Lambda 表达式来构建查询条件,从而使代码更加简洁和易读。下面详细介绍 LambdaQueryWrapper 的使用方法及其底层原理。 什么是 LambdaQueryWrapper? La…...

【工控】线扫相机小结 第三篇
海康软件更新 目前使用的是 MVS_STD_4.3.2_240705.exe ,最新的已经到4.4了。 一个大的变动 在上一篇中我们提到一个问题: 需要注意的是,我们必须先设置 TriggerSelector 是 “FrameBurstStart” 还是 “LineStart” 再设置TriggerMode 是 …...

golang中的init函数
程序的初始化和执行都起始于 main 包。如果 main 包还导入了其它的包,那么就会在编译时将它们依次 导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt 包,但 它只会被导入一次&#x…...

理解和选择Vue的组件风格:组合式API与选项式API详解
目录 前言1. Vue 的两种组件风格概述1.1 选项式 API:直观且分块清晰1.2 组合式 API:灵活且逻辑集中 2. 深入理解组合式 API 的特点2.1 响应式变量与函数式编程2.2 逻辑组织更清晰2.3 更好的代码复用 3. 应用场景分析:如何选择 API 风格3.1 适…...

Java基础——高级技术
1. 单元测试 就是针对最小的功能单元(方法),编写测试代码对其进行正确性测试。 1.1. Junit单元测试框架 可以用来对方法进行测试,他是第三方公司开源出来的(很多开发工具都已经集成了Junit框架,如IDEA&a…...
什么是SSL VPN?其中的协议结构是怎样的?
定义:SSL VPN是以SSL协议为安全基础的VPN远程接入技术,移动办公人员使用SSL VPN可以安全、方便的接入企业内网,访问企业内网资源,提高工作效率。 SSL(Security Socket Layer)是一个安全协议,为…...
程序员高频率面试题-整理篇
Redis 除了做缓存,还能做什么? 分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。 限流:一般是通过 Redis Lua 脚本的方式来实现限流。 消息队列&#x…...

大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...

算法:模拟
1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) 遍历字符串:通过外层循环逐一检查每个字符。遇到 ? 时处理: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: 与…...

(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...
libfmt: 现代C++的格式化工具库介绍与酷炫功能
libfmt: 现代C的格式化工具库介绍与酷炫功能 libfmt 是一个开源的C格式化库,提供了高效、安全的文本格式化功能,是C20中引入的std::format的基础实现。它比传统的printf和iostream更安全、更灵活、性能更好。 基本介绍 主要特点 类型安全:…...

解析“道作为序位生成器”的核心原理
解析“道作为序位生成器”的核心原理 以下完整展开道函数的零点调控机制,重点解析"道作为序位生成器"的核心原理与实现框架: 一、道函数的零点调控机制 1. 道作为序位生成器 道在认知坐标系$(x_{\text{物}}, y_{\text{意}}, z_{\text{文}}…...
2025年低延迟业务DDoS防护全攻略:高可用架构与实战方案
一、延迟敏感行业面临的DDoS攻击新挑战 2025年,金融交易、实时竞技游戏、工业物联网等低延迟业务成为DDoS攻击的首要目标。攻击呈现三大特征: AI驱动的自适应攻击:攻击流量模拟真实用户行为,差异率低至0.5%,传统规则引…...