网站首页被k咋办,南昌医院网站建设,表白网站在线生成免费,大连seo皮皮目录 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… 目录 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 服务器引入http7.3 服务器引入websocket7.4 服务器引protobuf7.5 编写工具函数和构造数据函数7.6 验证网络连通性7.7 网络通信注意事项 8. 主界面逻辑的实现8.1 获取个人信息8.2 获取好友列表8.3 获取会话列表8.4 获取好友申请列表8.5 获取指定会话的近期消息8.6 点击某个好友项 9. 小结 1. 将protobuf引入项目当中
1创建 proto 目录, 并把服务器提供的 proto 拷贝过来 2proto文件链接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 HEADERPOST /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;// 好友列表QListmodel::UserInfo* friendList nullptr;// 会话列表QListmodel::ChatSessionInfo* chatSessionList nullptr;// 记录当前选中的会话是哪个~~QString currentChatSessionId ;// 记录每个会话中, 都有哪些成员(主要针对群聊). key 为 chatSessionId, value 为成员列表QHashQString, QListmodel::UserInfo* memberList nullptr;// 待处理的好友申请列表QListmodel::UserInfo* applyList nullptr;// 每个会话的最近消息列表, key 为 chatSessionId, value 为消息列表QHashQString, QListmodel::Message* recentMessages nullptr;// 存储每个会话, 未读消息的个数. key 为 chatSessionId, value 为未读消息的个数.QHashQString, int* unreadMessageCount nullptr;// 用户的好友搜索结果.QListmodel::UserInfo* searchUserResult nullptr;// 历史消息搜索结果.QListmodel::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 QHashQString, QListMessage();memberList new QHashQString, QListUserInfo();unreadMessageCount new QHashQString, 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_ptrT handleHttpResponse(QNetworkReply* httpResp, bool* ok, QString* reason)
{// 1. 判定 HTTP 层面上, 是否出错if(httpResp-error() ! QNetworkReply::NoError){*ok false;*reason httpResp-errorString();httpResp-deleteLater();return std::shared_ptrT();}// 2. 获取到响应的 bodyQByteArray respBody httpResp-readAll();// 3. 针对 body 反序列化std::shared_ptrT respObj std::make_sharedT();respObj-deserialize(serializer, respBody);// 4. 判定业务上的结果是否正确if(!respObj-success()){*ok false;*reason respObj-errmsg();httpResp-deleteLater();return std::shared_ptrT();}// 5. 释放 httpResp 对象httpResp-deleteLater();*ok true;return respObj;
}5.3 引入websocket
1Websocket 在主窗口加载后才和服务器建立连接并且在建立连接后给服务器发送⼀个 认证请求之后, 才能收到后续数据。初始化 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
1cmake增加内容文件
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 handleHttpResponsebite_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_ptrbite_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函数
QListUserInfo* 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-handleHttpResponsebite_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_ptrbite_im::GetFriendListRsp resp)
{if(friendList nullptr){friendList new QListUserInfo();}friendList-clear();QListbite_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();QListUserInfo* 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
QListChatSessionInfo* 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-handleHttpResponsebite_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_ptrbite_im::GetChatSessionListRsp resp)
{if(chatSessionList nullptr){chatSessionList new QListChatSessionInfo();}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();QListChatSessionInfo* 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函数
QListUserInfo *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-handleHttpResponsebite_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_ptrbite_im::GetPendingFriendEventListRsp resp)
{if(applyList nullptr){applyList new QListUserInfo();}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();QListUserInfo* 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);
}QListMessage* 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-handleHttpResponsebite_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_ptrbite_im::GetRecentMsgRsp resp)
{// 拿到 chatSessionId 对应的消息列表, 并清空// 注意此处务必是引用类型, 才是修改哈希表内部的内容.QListMessage 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::activeactive 已经在 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_castSessionFriendItem*(layoutItem-widget());item-select();
}3加载该会话的最近消息并显示
在上述 clickItem 中会调⽤ item-select() , 进⼀步调⽤到 active ⽅法, 从⽽触发加载最近消息的逻辑.
4整体流程小结 5注意
每个会话中的用户列表应该是按需加载的不应该是程序启动全都加载进来创建会话操作放到同意好友申请时。换而言之每个用户都⼀定存在⼀个和他对应的会话。
9. 小结
1在进行前后端交互接口的实现的时候代码格式基本上都是一样的只需要将其中一个流程搞清楚即可。如下图就是基本的流程图了 2剩下的需要实现的前后端交互接口见博客https://blog.csdn.net/m0_65558082/article/details/143817211?spm1001.2014.3001.5502。
客户端整体代码链接https://gitee.com/liu-yechi/new_code/tree/master/chat_system/client。 文章转载自: http://www.morning.krtcjc.cn.gov.cn.krtcjc.cn http://www.morning.flxgx.cn.gov.cn.flxgx.cn http://www.morning.qmpbs.cn.gov.cn.qmpbs.cn http://www.morning.yngtl.cn.gov.cn.yngtl.cn http://www.morning.qghjc.cn.gov.cn.qghjc.cn http://www.morning.qgqck.cn.gov.cn.qgqck.cn http://www.morning.hjjkz.cn.gov.cn.hjjkz.cn http://www.morning.qdxwf.cn.gov.cn.qdxwf.cn http://www.morning.qhrlb.cn.gov.cn.qhrlb.cn http://www.morning.tsqpd.cn.gov.cn.tsqpd.cn http://www.morning.bztzm.cn.gov.cn.bztzm.cn http://www.morning.zrks.cn.gov.cn.zrks.cn http://www.morning.jsrnf.cn.gov.cn.jsrnf.cn http://www.morning.bcdqf.cn.gov.cn.bcdqf.cn http://www.morning.mrqwy.cn.gov.cn.mrqwy.cn http://www.morning.hdpcn.cn.gov.cn.hdpcn.cn http://www.morning.wcgfy.cn.gov.cn.wcgfy.cn http://www.morning.wnxqf.cn.gov.cn.wnxqf.cn http://www.morning.qftzk.cn.gov.cn.qftzk.cn http://www.morning.lfttb.cn.gov.cn.lfttb.cn http://www.morning.xnrgb.cn.gov.cn.xnrgb.cn http://www.morning.wkrkb.cn.gov.cn.wkrkb.cn http://www.morning.gmmxh.cn.gov.cn.gmmxh.cn http://www.morning.jjnql.cn.gov.cn.jjnql.cn http://www.morning.ttnfc.cn.gov.cn.ttnfc.cn http://www.morning.mkkcr.cn.gov.cn.mkkcr.cn http://www.morning.jwpcj.cn.gov.cn.jwpcj.cn http://www.morning.kbbmj.cn.gov.cn.kbbmj.cn http://www.morning.mymz.cn.gov.cn.mymz.cn http://www.morning.dhyzr.cn.gov.cn.dhyzr.cn http://www.morning.xflzm.cn.gov.cn.xflzm.cn http://www.morning.khtyz.cn.gov.cn.khtyz.cn http://www.morning.rmkyb.cn.gov.cn.rmkyb.cn http://www.morning.tqwcm.cn.gov.cn.tqwcm.cn http://www.morning.pcrzf.cn.gov.cn.pcrzf.cn http://www.morning.xczyj.cn.gov.cn.xczyj.cn http://www.morning.cflxx.cn.gov.cn.cflxx.cn http://www.morning.dnqliv.cn.gov.cn.dnqliv.cn http://www.morning.gxtfk.cn.gov.cn.gxtfk.cn http://www.morning.sbrxm.cn.gov.cn.sbrxm.cn http://www.morning.psgbk.cn.gov.cn.psgbk.cn http://www.morning.kszkm.cn.gov.cn.kszkm.cn http://www.morning.dmwck.cn.gov.cn.dmwck.cn http://www.morning.tmbfz.cn.gov.cn.tmbfz.cn http://www.morning.krqhw.cn.gov.cn.krqhw.cn http://www.morning.npfkw.cn.gov.cn.npfkw.cn http://www.morning.qmnjn.cn.gov.cn.qmnjn.cn http://www.morning.tlrxp.cn.gov.cn.tlrxp.cn http://www.morning.lxmmx.cn.gov.cn.lxmmx.cn http://www.morning.rqxhp.cn.gov.cn.rqxhp.cn http://www.morning.xptkl.cn.gov.cn.xptkl.cn http://www.morning.ndxrm.cn.gov.cn.ndxrm.cn http://www.morning.ghfrb.cn.gov.cn.ghfrb.cn http://www.morning.wnhsw.cn.gov.cn.wnhsw.cn http://www.morning.xllrf.cn.gov.cn.xllrf.cn http://www.morning.wbnsf.cn.gov.cn.wbnsf.cn http://www.morning.zlqyj.cn.gov.cn.zlqyj.cn http://www.morning.pdmml.cn.gov.cn.pdmml.cn http://www.morning.yyngs.cn.gov.cn.yyngs.cn http://www.morning.rzcbk.cn.gov.cn.rzcbk.cn http://www.morning.nxwk.cn.gov.cn.nxwk.cn http://www.morning.bcdqf.cn.gov.cn.bcdqf.cn http://www.morning.yjmlg.cn.gov.cn.yjmlg.cn http://www.morning.xinyishufa.cn.gov.cn.xinyishufa.cn http://www.morning.xwrhk.cn.gov.cn.xwrhk.cn http://www.morning.thwhn.cn.gov.cn.thwhn.cn http://www.morning.ctwwq.cn.gov.cn.ctwwq.cn http://www.morning.kbkcl.cn.gov.cn.kbkcl.cn http://www.morning.bgpb.cn.gov.cn.bgpb.cn http://www.morning.qlrwf.cn.gov.cn.qlrwf.cn http://www.morning.knscf.cn.gov.cn.knscf.cn http://www.morning.tpnch.cn.gov.cn.tpnch.cn http://www.morning.xsctd.cn.gov.cn.xsctd.cn http://www.morning.dkcpt.cn.gov.cn.dkcpt.cn http://www.morning.zzaxr.cn.gov.cn.zzaxr.cn http://www.morning.qcdtzk.cn.gov.cn.qcdtzk.cn http://www.morning.lnmby.cn.gov.cn.lnmby.cn http://www.morning.ydryk.cn.gov.cn.ydryk.cn http://www.morning.fbxdp.cn.gov.cn.fbxdp.cn http://www.morning.lnsnyc.com.gov.cn.lnsnyc.com