1. 获取当前用户的个人信息
1.1 前后端逻辑分析(主界面功能)
主界面上所有的前后端交互逻辑相同,分析到加载会话列表后其余功能仅实现。
核心逻辑总结
异步请求-响应模型
- 客户端发起请求,向服务器发送包含会话ID的请求
- 服务端处理请求并响应,根据的会话ID查找用户信息,并返回结果
- 客户端处理响应,解析服务器的响应并更新UI显式界面
前后端交互逻辑概述
前端流程
- **MainWidget::initData:**前端入口函数,主要就是负责初始化数据,与此同时客户端向DataCenter发送异步请求以获取个人信息
- **DataCenter::getMyselfAsync:**向NetClient发送请求,NetClient负责与服务器进行通信
- **NetClient::getMyself:**这个函数则是通过HTTP请求向服务器发送用户登录会话ID(loginSessionId)来获取个人信息
后端流程
- 服务器的HttpServer收到来自NetClient的请求后,然后**解析请求数据(**也就是会话ID)
- HttpServer::getUserInfo:服务器根据会话ID,查找用户信息并构建响应,然后将用户信息返回给客户端
客户端处理响应
- 服务器返回的响应信息由NetClient接收,然后通过DataCenter,最后在MainWidget中展示用户的头像信息
代码逻辑实现分析(前后端交互逻辑)
前端发起请求
MainWidget中通过初始化信号槽函数中,使用connect函数 建立了一个连接,当DataCenter::getMyselfDone信号 发出后,会执行一个回调函数 ,这个回调函数的作用就是获取用户信息(myself) 然以后设置头像。
**dataCenter->getMyselfAsync():**异步请求,也就是调用该函数进一步通过网络底层获取用户信息。
cpp
connect(dataCenter, &DataCenter::getMyselfDone, this, [=]() {
const auto* myself = dataCenter->getMyself();
this->userAvatar->setIcon(myself->avatar);
});
dataCenter->getMyselfAsync();
异步请求逻辑
在数据核心类中,调用getMyselfAsync(()函数,然后又会调用NetClient中的getMyselef函数,然后该函数会将登陆的sessionId传递给NetClient,最后由NetClient来处理网络通信
cpp
void DataCenter::getMyselfAsync() const {
netClient.getMyself(loginSessionId);
}
前后端通信的实现(NetClient与服务端)
- **构造请求:**使用proto构建请求体,该处使用的是GetUserInfoReq协议消息
- **发送请求:**sendHttpRequest方法,通过传递请求路劲和请求体,通过http客户端post请求,然后返回一个HTTP响应
- **处理响应:**在httpResp请求完成后,netClient会接收服务端返回的响应信息,通过httpleHttpResponse方法解析响应,解析成功后,将用户信息传递给DataCenter ,同时发出getMyselfDone信号,最后通知MainWidget更新UI
cpp
void NetClient::getMyself(const QString &loginSessionId) {
// 1. 构造请求体
bite_im::GetUserInfoReq req;
req.setRequestId(makeRequestId());
req.setSessionId(loginSessionId);
// 2. 发送 HTTP 请求
QByteArray body = req.serialize(&serializer);
LOG() << "[获取个人信息] requestId=" << req.requestId() << ", sessionId=" << loginSessionId;
QNetworkReply* httpResp = this->sendHttpRequest("/service/user/get_user_info", body);
// 3. 处理 HTTP 响应
connect(httpResp, &QNetworkReply::finished, this, [=]() {
auto userInfoRsp = this->handleHttpResponse<bite_im::GetUserInfoRsp>(httpResp);
if (!userInfoRsp) {
return;
}
// b) 设置 DataCenter 中的用户信息
dataCenter->resetMyself(userInfoRsp);
// c) 发出信号通知获取完成
emit dataCenter->getMyselfDone();
});
}
代码实现逻辑(服务端处理请求)
- **解析请求:**服务端首先解析客户端传过来的请求体,然后根据sessionID查找用户信息
- **构建响应:**通过构建UserInfo对象,填充用户的详细信息,将这些信息装入响应体GetUserInfoRsp,设置success为true就表明操作成功
- **返回响应:**将序列化后的响应体返回给客户端,客户端收到这个响应后会解析数据,然后更新页面
cpp
QHttpServerResponse HttpServer::getUserInfo(const QHttpServerRequest &req) {
// 解析请求
bite_im::GetUserInfoReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 获取用户信息] requestId=" << pbReq.requestId() << ", sessionId=" << pbReq.sessionId();
// 根据 sessionId 获取用户信息
bite_im::UserInfo userInfo;
userInfo.setUserId("1234");
userInfo.setNickname("张三");
userInfo.setDescription("这是个性签名");
userInfo.setPhone("18612345678");
userInfo.setAvatar(loadImageToByteArray(":/image/defaultAvatar.png"));
// 构造响应
bite_im::GetUserInfoRsp pbRsp;
pbRsp.setSuccess(true);
pbRsp.setUserInfo(userInfo);
// 序列化响应体并发送
QByteArray body = pbRsp.serialize(&serializer);
return QHttpServerResponse(body, QHttpServerResponse::StatusCode::Ok);
}
代码实现逻辑(客户端处理响应)
客户端接收到服务端返回的响应后,NetClient然后进行相应的处理
- **解析响应:**首先通过handleHttpResponse解析其中的HTTP响应体,生成GetUserInfoRsp对象
- **更新数据:**更新核心数据类中的数据
- **发出信号:**最后发出getMyselfDone信号,通知前端MainWidget数据获取完成,从而实现更新用户界面
cpp
auto userInfoRsp = this->handleHttpResponse<bite_im::GetUserInfoRsp>(httpResp);
if (!userInfoRsp) {
return;
}
// b) 设置 DataCenter 中的数据
dataCenter->resetMyself(userInfoRsp);
// c) 发出信号通知获取完成
emit dataCenter->getMyselfDone();
不同文件交互梳理
类似于外卖系统中的订单处理
- **客户端下单:**用户提交外卖订单,这也就相当于MainWidget发起了获取用户信息的请求
- **订单传递给平台:**订单会进入外卖平台的处理系统,相对于DataCenter调用NetClient来发起网络请求
- **平台传递订单给餐厅:**外卖平台将订单信息发送给餐厅,类似于NetClient发送HTTP请求给服务器
- **餐厅处理并回传订单状态:**此时餐厅已经准备食品,然后将订单完成状态返回给平台,这也就是服务器返回用户信息给客户端
- **平台通知客户端:**外卖平台将订单完成状态通知用户,客户端根据订单的状态更新用户的UI界面,这也就对应这UI更新用户信息
架构实现分析
该模块功能的实现使用MVC架构,也就是将数据、试图、控制逻辑三者进行分离,从而使得代码结构清晰模块化。具体来说就是Model负责数据管理、View负责UI展示、Controller负责业务逻辑的处理。
客户端
当网络请求发生的时候,打开主窗口,然后可以获取用户头像以及个人信息,然后头像显示到主窗口上,个人信息显示到个人资料上。
构建HTTP请求
获取和重置用户信息(在核心数据类中实现)
请求处理封装成模版类型
网络通信内部实现逻辑
获取个人信息逻辑
- 主窗口启动,关联信号槽,发起请求
- 构造HTTP请求,然后处理响应
- 响应处理完成后保存到中心数据类中,最后告知响应已经处理完成
测试服务器
分析HTTP服务器处理流程
该测试代码的任务就是负责处理"获取用户信息"请求,然后返回一个Protobuf序列化响应,使用QHttpServerRequest解析客户端请求,返回一个带有用户信息的QHttpServerResponse响应
- **请求解析:**从HTTP请求的body中提取数据,然后使用Protobuf反序列化成为GetUserInFoReq对象,提取请求中包含的用户信息
- **构建响应:**生成包含用户信息的GetUserInfoRsp响应性响应,并将其序列化后作为HTTP响应的body
- **发送响应:**使用protobuf序列化后的数据构建HTTP响应,设置响应头,并将其发送给客户端
服务器正常功能测试
功能实现分析
根据前后端接口编写客户端(界面---dataCenter---netClient---服务器---反显示后---调用datacenter---给出一个信号---界面)---编写服务器---测试
2. 加载好友列表
实现目标与实现思路分析
- **实现目标:**从服务器上获取好友列表然后显示到客户端界面上
- 实现方式
- 从核心数据类中获取好友列表数据
- 判定数据核心类中是否已经有数据
- 如果有数据则直接加载本地数据
- 如果没有数据则需要从服务器中获取数据
- 更新数据内容到客户端上
- 如果内存中有数据的话,则直接将数据从内存中拿取出显示即可
- 总体逻辑
- 内存读取:首先从本地缓存DataCenter中获取好友数据,如果缓存中有则直接加载到界面
- 网络请求:缓存中没有数据则需要进行网络请求,向服务器获取好友列表
- 数据存储:服务器返回数据后,先调用数据中心类的接口,清空原有数据列表中的数据,然后将新的好友列表加载到内存中缓存,方便后续的快速读取
- 界面更新:网络请求完成后,通过信号的方式通知主界面,然后根据获取的新好友列表更新界面
本地文件加载逻辑实现
网络请求获取好友列表数据
向服务器发送好友列表请求
- Protobuf构造GetFriendListenReq请求消息,设置RequestId 和 SessionId
- 向服务器发起请求,并指定URL(预先约定好)
- connect()函数负责等待服务器的响应到来,然后接收数据
- 响应到达后,解析响应,获取friendListResp,也就是好友列表响应
网络请求成功后将好友列表存储在核心数据类中
测试服务器逻辑
- 注册路由
- 监听指定的URL地址,当请求到达的时候,则调用getChatSessionList函数进行处理
- 解析请求
- 将客户端的请求体反序列化为缓冲区的Protobuf对象,同时通过日志记录
- 构造响应
- 初始化响应对象,然后设置请求ID、成功状态、错误信息
- 发送响应
- 构建所有会话信息后,服务器将响应序列化发送给客户端
前后端总体实现逻辑总结
3. 加载会话列表
逻辑分析
4. 加载好友申请列表
实现逻辑汇总
5. 加载会话的最近消息
初始化逻辑
该功能的实现并非在程序启动的时候,而是当用户点击某个会话的时候才会触发,下面从选择好友开始梳理其逻辑
逻辑梳理
服务器处理
客户端处理服务器响应
细节问题处理
保证滚动条每次直接到达的末尾位置
6. 处理点击好友列表项
功能分析
点击好友列表后
- 切换到会话列表
- 选中对应的会话,根据点击好友的userid和会话列表中的userid匹配
- 消息展示区中,加载出对应会话的最近消息
逻辑整理