微服务即时通讯系统的实现(客户端)----(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.mzph.cn/news/886936.shtml

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

相关文章

凸函数与深度学习调参

问题1&#xff1a;如何区分凸问题和凹问题&#xff1f; 问题2&#xff1a;深度学习如何区分调参&#xff1f;

使用可视化工具kafkatool连接docker的kafka集群,查看消息内容和offset

1、下载kafkatool 下载地址Offset Explorer&#xff0c;下载对应系统的offset explorer 下载完&#xff0c;傻瓜安装即可&#xff08;建议放D盘&#xff09;&#xff0c;在开始菜单输入offset找到该应用打开 打开 2、连接kafka 点击File > add new connection Bootstrap…

关于Java使用ueditor上传图片的一些总结

1.如何配置ueditor让上传的图片到项目之外&#xff1f; 因为图片上传到web项目中,重新部署项目可能会丢失图片。 解决方法&#xff1a;下载ueditor.1.1.2.jar. 地址&#xff1a;ueditor-1.1.2项目源码及jar包.zip 链接: https://pan.baidu.com/s/1Bhumfw8OX16n0MTO9ur73g 提…

React可以做全栈开发吗

React可以做全栈开发吗? 答案是肯定的&#xff0c;而且还比较完美 React可以用于全栈开发&#xff0c;以下是具体的介绍&#xff1a; 前端部分 构建用户界面 React是一个用于构建用户界面的JavaScript库&#xff0c;它通过组件化的方式让开发者能够高效地创建交互式的UI。例…

【前端学习笔记】Javascript学习二(运算符、数组、函数)

一、运算符 运算符&#xff08;operator&#xff09;也被称为操作符&#xff0c;是用于实现赋值、比较和执行算数运算等功能的符号。 JavaScript中常用的运算符有&#xff1a; 算数运算符、递增和递减运算符、比较运算符、逻辑运算符、赋值运算符 算数运算符&#xff1a; 、-…

Redis五大基本类型——List列表命令详解(命令用法详解+思维导图详解)

目录 一、List列表类型介绍 二、常见命令 1、LPUSH 2、LPUSHX 3、RPUSH 4、RPUSHX 5、LRANGE 6、LPOP 7、RPOP 8、LREM 9、LSET 10、LINDEX 11、LINSERT 12、LLEN 13、阻塞版本命令 BLPOP BRPOP 三、命令小结 相关内容&#xff1a; Redis五大基本类型——Ha…

快速入门消息队列MQ、RabbitMQ

目录 一、MQ简介 1.同步调用 2.异步调用 3.技术选型 二、RabbitMQ 1.安装 2.控制台的使用说明 2.1交换机 2.2队列​编辑 2.3绑定关系 3.AMQP 3.1快速入门 3.2WorkQueues模型 3.3交换机 3.3.1 Fanout交换机 3.3.2 Direct交换机 3.3.3 Topic交换机 3.4 声明交换机…

Spark SQL大数据分析快速上手-完全分布模式安装

【图书介绍】《Spark SQL大数据分析快速上手》-CSDN博客 《Spark SQL大数据分析快速上手》【摘要 书评 试读】- 京东图书 大数据与数据分析_夏天又到了的博客-CSDN博客 Hadoop完全分布式环境搭建步骤-CSDN博客,前置环境安装参看此博文 完全分布模式也叫集群模式。将Spark目…

《现代网络技术》读书笔记:NFV功能

本文部分内容来源于《现代网络技术&#xff1a;SDN,NFV,QoE、物联网和云计算&#xff1a;SDN,NFV,QoE,IoT,andcloud》 NFV基础设施 NFV体系结构的核心是资源与功能集合&#xff0c;也为称为NFV基础设施(NFVI)。NFVI包括以下三个域&#xff1a; 计算域&#xff1a;提供商用的大…

MySQL数据库2——SQL语句

一.SQL基础 1.SQL通用语法 1.SQL语句可以单行或多行书写&#xff0c;以分号结尾。2.SOL语句可以使用空格/缩进来增强语句的可读性。3.MySQL数据库的SQL语句不区分大小写&#xff0c;关键字建议使用大写 注释&#xff1a; 单行注释&#xff1a;-- 注释内容或#注释内容(MySQL…

会员等级经验问题

问题描述 会员从一级完成任务升级到二级以后&#xff0c;一级显示还差经验&#xff0c;这里差的其实是二级到三级的经验&#xff0c;如下图所示 修复方法 1、前端需要修改&#xff1a; 路径&#xff1a;/pages/users/user_vip/index.vue 方便复制&#xff1a; v-if"i…

【Apache Paimon】-- 6 -- 清理过期数据

目录 1、简要介绍 2、操作方式和步骤 2.1、调整快照文件过期时间 2.2、设置分区过期时间 2.2.1、举例1 2.2.2、举例2 2.3、清理废弃文件 3、参考 1、简要介绍 清理 paimon &#xff08;表&#xff09;过期数据可以释放存储空间&#xff0c;优化资源利用并提升系统运行效…

Spring Boot整合Kafka,实现单条消费和批量消费,示例教程

如何安装Kafka&#xff0c;可以参考docker搭载Kafka集群&#xff0c;一个文件搞定&#xff0c;超简单&#xff0c;亲试可行-CSDN博客 1、在pom.xml中加入依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-sta…

django基于Python的农产品销售系统的设计与实现

摘 要 随着现代人们的快速发展&#xff0c;农产品销售系统已成为农产品的需求。该平台采用Python技术和django搭建系统框架&#xff0c;后台使用MySQL数据库进行信息管理&#xff1b;通过个人中心、用户管理、商家管理、产品类型管理、农产品管理、系统管理、订单管理等功能&a…

项目-摄像

树莓派摄像头使用方法 Camera教程 https://www.raspi.cc/index.php?cread&id53&page1 nanopc-t4 ​https://www.raspi.cc/index.php?cread&id53&page1 摄像头型号 Raspberry Pi Camera Rev 1.3 检测故障 dmesg | grep -i mipi piNanoPC-T4:~$ dmesg | …

Facebook商城号封号的原因是什么?

Facebook商城作为一个重要的销售平台&#xff0c;不仅为商家提供了巨大的市场机会&#xff0c;也带来了一系列需要警惕的风险&#xff0c;其中包括账号被封的风险。本文将从环境异常、频繁操作和违规行为三个主要方面深入探讨&#xff0c;解析导致Facebook商城账号被封禁的具体…

聊一聊Elasticsearch的索引分片的恢复机制

1、什么是索引分片的恢复&#xff1f; 所谓索引分片的恢复指的是在某些条件下&#xff0c;索引分片丢失&#xff0c;ES会把某索引的分片复制一份来得到该分片副本的过程。 2、触发分片恢复的场景有哪些&#xff1f; 分片的分配 当集群中节点的数量发生变化&#xff0c;或者配…

字符串的基本操作(C语言版)

一、实验内容&#xff1a; 采用顺序结构存储串&#xff0c;编写一个函数substring(strl,str2)&#xff0c;用于判定str2是否为strl的子串&#xff1b;编写一个函数&#xff0c;实现在两个已知字符串中找出所有非空最长公共子串的长度和最长公共子串的个数&#xff1b; ①字符…

一些任务调度的概念杂谈

任务调度 1.什么是调度任务 依赖&#xff1a;依赖管理是整个DAG调度的核心。调度依赖包括依赖策略和依赖区间。 依赖分为任务依赖和作业依赖&#xff0c;任务依赖是DAG任务本身的依赖关系&#xff0c;作业依赖是根据任务依赖每天的作业产生的。两者在数据存储模型上有所不同…

解决 npm xxx was blocked, reason: xx bad guy, steal env and delete files

问题复现 今天一位朋友说&#xff0c;vue2的老项目安装不老依赖&#xff0c;报错内容如下&#xff1a; npm install 451 Unavailable For Legal Reasons - GET https://registry.npmmirror.com/vab-count - [UNAVAILABLE_FOR_LEGAL_REASONS] vab-count was blocked, reas…