目录
我们的客户端终究是需要和我们的服务器进行通信连接的。
我们知道,我们客户端往这个服务器发送请求的时候其实是分成下面两种情况的
- 客户端主动向服务器发起信息获取请求:使用HTTP协议
- 服务器向客户端推送消息:使用WebSocket协议
那么我们就势必需要使用到下面3个组件
cpp
// http 客户端
QNetworkAccessManager httpClient;
// websocket 客户端
QWebSocket websocketClient;
// 序列化器
QProtobufSerializer serializer;
那么我们从服务端获取的数据存放到哪里呢?
我们其实是存放到我们的数据中心里面啊。这个数据中心其实是我们自己写的
cpp
model::DataCenter* dataCenter;
好了,那么现在我们就聚齐了我们客户端的通信连接类所必须的一些东西
cpp
namespace network {
class NetClient : public QObject
{
Q_OBJECT
private:
// 定义重要常量. ip 都暂时使用本地的环回 ip. 端口号约定成 8000 和 8001
const QString HTTP_URL = "http://106.75.144.240:8000";
const QString WEBSOCKET_URL = "ws://106.75.144.240:8001/ws";
public:
NetClient(model::DataCenter* dataCenter);
private:
model::DataCenter* dataCenter;
// http 客户端
QNetworkAccessManager httpClient;
// websocket 客户端
QWebSocket websocketClient;
// 序列化器
QProtobufSerializer serializer;
signals:
};
} // end network
#endif // NETCLIENT_H
有了这几个成员,我们才能写出下面这些函数
一.WebSocket模块
1.1.连接建立成功
我们将连接建立成功的回调函数设置为一个身份认证函数
cpp
// 1. 准备好所有需要的信号槽
connect(&websocketClient, &QWebSocket::connected, this, [=]() {
LOG() << "websocket 连接成功!";
// 不要忘记! 在 websocket 连接成功之后, 发送身份认证消息!
sendAuth();
});
那么sendAuth();里面干了什么
我们先不着急,我们先来思考一个问题:我们websocket是用于什么时候?
很清楚了,其实就是服务器对客户端进行消息推送的时候。那么推送的是什么消息呢?
-
好友添加申请通知
当有人向你发送好友申请时,你会收到这条通知。通知里包含了申请人的用户信息(如昵称、头像、ID 等)。
-
好友添加处理结果通知
当你处理别人的好友申请(同意或拒绝)后,对方会收到这条通知。通知里会标明是否同意 ,以及处理人(也就是你)的用户信息。
-
新聊天会话创建通知
当你被拉入一个新的聊天会话(比如新建的群聊或单聊窗口)时,你会收到这条通知。里面包含了新建会话的详细信息(会话ID、类型、名称、成员列表等)。
-
新消息通知
当你在某个聊天会话中收到一条新消息时,服务端会推送这条通知。里面包含了消息的完整内容(发送者、时间、文本或附件等)。
-
好友移除通知
当你的某位好友将你从好友列表中删除时,你会收到这条通知。它只包含被删除好友的用户ID,你可以据此更新本地的好友列表。
那么我们可以发现,这些都是登录后的操作,那么在我们的设计中,登录之后的操作都是需要携带一个叫用户ID的东西,没有这个东西,我们根本都不知道是去哪个用户那里操作,只有带上了这个东西,我们才能去进行这些操作,但是,我们发起登录请求的时候,服务器只是会给客户端返回一个session_id,这个session_id和这个用户ID是一一对应的,这两个东西是存在redis数据库里的,那么我们就可以拿着这个session_id去向Redis服务器查询,这个步骤我将它设计在了这个网关子服务里面,网关子服务去Redis查询到了这个用户ID,就会将这个uid填充进我们的请求里面,再发起RPC调用。
cpp
void NetClient::sendAuth()
{
IMS::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();
}
此外,这个session_id其实还有别的用处,我们可以去看看网关子服务那边的,这个网关子服务去拿这个session_id去向Redis服务器查询对应的uid的时候,如果说Redis里面没有这个键值对,那么就说明这个用户就还没有进登录。这个就是我们简易的身份验证环节。
重温一下我们网关子服务的websocket的设计

绑定的函数都如下
cpp
// 第一部分:这些都是设置给websocketpp服务器的回调函数
void onOpen(websocketpp::connection_hdl hdl)
{
LOG_DEBUG("websocket长连接建立成功 {}", (size_t)_ws_server.get_con_from_hdl(hdl).get());
}
// 长连接一断开,就说明用户下线了
void onClose(websocketpp::connection_hdl hdl)
{
// 长连接断开时做的清理工作
// 0. 通过连接对象,获取对应的用户ID与登录会话ID
auto conn = _ws_server.get_con_from_hdl(hdl);
std::string uid, ssid;
bool ret = _connections->client(conn, uid, ssid);
if (ret == false)
{
LOG_WARN("长连接断开,未找到长连接对应的客户端信息!");
return;
}
// 1. 移除登录会话信息
_redis_session->remove(ssid);
// 2. 移除登录状态信息
_redis_status->remove(uid);
// 3. 移除长连接管理数据
_connections->remove(conn);
LOG_DEBUG("{} {} {} 长连接断开,清理缓存数据!", ssid, uid, (size_t)conn.get());
}
// 保持连接活跃的函数,通过定期发送 Ping 帧来维持 WebSocket 连接
void keepAlive(server_t::connection_ptr conn)
{
// 检查连接指针是否有效,并且连接状态是否为打开状态
if (!conn || conn->get_state() != websocketpp::session::state::value::open)
{
// 如果连接无效或状态不是打开状态,记录调试日志并直接返回,不再继续保活
LOG_DEBUG("非正常连接状态,结束连接保活");
return;
}
// 向对端发送一个空的 Ping 帧,用于探测连接是否仍然存活
conn->ping("");
// 设置一个定时器,60 秒后再次执行本函数,形成周期性的保活检查
_ws_server.set_timer(60000, std::bind(&GatewayServer::keepAlive, this, conn));
}
// 消息到达处理回调函数
void onMessage(websocketpp::connection_hdl hdl, server_t::message_ptr msg)
{
/*客户端并不是只使用 HTTP 来给服务端发消息。
在这个网关实现中,WebSocket 连接建立后,客户端会主动发送第一条认证消息(ClientAuthenticationReq),这就是通过 WebSocket 发送的。
客户端除了这里使用了websocket来给服务端发送消息,后面就再也没有使用websocket给服务端发消息了,都是通过HTTP来发送消息*/
// 收到第一条消息后,根据消息中的会话ID进行身份识别,将客户端长连接添加管理
// 1. 取出长连接对应的连接对象
auto conn = _ws_server.get_con_from_hdl(hdl);
// 2. 针对消息内容进行反序列化 -- ClientAuthenticationReq -- 提取登录会话ID
ClientAuthenticationReq request; // ClientAuthenticationReq存在于gateway.proto里面
bool ret = request.ParseFromString(msg->get_payload());
if (ret == false)
{
LOG_ERROR("长连接身份识别失败:正文反序列化失败!");
_ws_server.close(hdl, websocketpp::close::status::unsupported_data, "正文反序列化失败!"); // 关闭websocket长连接
return;
}
// 3. 在会话信息缓存中,查找会话信息
std::string ssid = request.session_id();
auto uid = _redis_session->uid(ssid);
// 4. 会话信息不存在则关闭连接
if (!uid)
{
LOG_ERROR("长连接身份识别失败:未找到会话信息 {}!", ssid);
_ws_server.close(hdl, websocketpp::close::status::unsupported_data, "未找到会话信息!"); // 关闭websocket长连接
return;
}
// 5. 会话信息存在,则添加长连接管理
_connections->insert(conn, *uid, ssid);
LOG_DEBUG("新增长连接管理:{}-{}-{}", ssid, *uid, (size_t)conn.get());
keepAlive(conn); // 这个函数会保证我们这个连接不会断开,形成长连接
}
里面最核心的就是消息到达处理回调函数。
客户端通过websocket发来了这个认证消息,我们服务器
- 拿着客户端的请求里面的session_id去Redis服务器查询对应的uid
- 将这个websocket管理起来
- 隔一段事件就去ping一下客户端,保持websocket长连接
此外,这个连接处理回调函数也挺好理解的
- 如果长连接断了,就说明客户端下线了,我们就去Redis数据库里面清理在线状态,session_id与uid这个键值对,然后把websocket长连接删除掉
1.2.收到二进制消息
我们的消息都是通过protobuf来进行序列化后来传输的,所以都是二进制的数据。
等会,我们需要先思考一个问题,服务器给客户端推送消息,会推送哪些消息?
-
好友添加申请通知
当有人向你发送好友申请时,你会收到这条通知。通知里包含了申请人的用户信息(如昵称、头像、ID 等)。
-
好友添加处理结果通知
当你处理别人的好友申请(同意或拒绝)后,对方会收到这条通知。通知里会标明是否同意 ,以及处理人(也就是你)的用户信息。
-
新聊天会话创建通知
当你被拉入一个新的聊天会话(比如新建的群聊或单聊窗口)时,你会收到这条通知。里面包含了新建会话的详细信息(会话ID、类型、名称、成员列表等)。
-
新消息通知
当你在某个聊天会话中收到一条新消息时,服务端会推送这条通知。里面包含了消息的完整内容(发送者、时间、文本或附件等)。
-
好友移除通知
当你的某位好友将你从好友列表中删除时,你会收到这条通知。它只包含被删除好友的用户ID,你可以据此更新本地的好友列表。
这些我们都已经在这个notify.proto里面已经定义好了
cpp
syntax = "proto3";
package IMS;
import "base.proto";
option cc_generic_services = true;
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;//消息信息
}
}
我们的消息都是通过protobuf来进行序列化后来传输的,所以都是二进制的数据。
cpp
connect(&websocketClient, &QWebSocket::binaryMessageReceived, this, [=](const QByteArray& byteArray) {
LOG() << "websocket 收到二进制消息!" << byteArray.length();
IMS::NotifyMessage notifyMessage;
notifyMessage.deserialize(&serializer, byteArray);
handleWsResponse(notifyMessage);
});
我们将二进制数据交给了handleWsResponse来进行处理,我们其实想一想就能明白,这里面干了什么?
首先肯定是需要先区分一下各种消息的类型了,然后根据不同类型的消息再进行不同的处理
cpp
void NetClient::handleWsResponse(const IMS::NotifyMessage ¬ifyMessage)
{
if (notifyMessage.notifyType() == IMS::NotifyTypeGadget::NotifyType::CHAT_MESSAGE_NOTIFY) {
// 收到消息
// 1. 把 pb 中的 MessageInfo 转成客户端自己的 Message
Message message;
message.load(notifyMessage.newMessageInfo().messageInfo());
// 2. 针对自己的 message 做进一步的处理
handleWsMessage(message);
} else if (notifyMessage.notifyType() == IMS::NotifyTypeGadget::NotifyType::CHAT_SESSION_CREATE_NOTIFY) {
// 创建新的会话通知
ChatSessionInfo chatSessionInfo;
chatSessionInfo.load(notifyMessage.newChatSessionInfo().chatSessionInfo());
handleWsSessionCreate(chatSessionInfo);
} else if (notifyMessage.notifyType() == IMS::NotifyTypeGadget::NotifyType::FRIEND_ADD_APPLY_NOTIFY) {
// 添加好友申请通知
UserInfo userInfo;
userInfo.load(notifyMessage.friendAddApply().userInfo());
handleWsAddFriendApply(userInfo);
} else if (notifyMessage.notifyType() == IMS::NotifyTypeGadget::NotifyType::FRIEND_ADD_PROCESS_NOTIFY) {
// 添加好友申请的处理结果通知
UserInfo userInfo;
userInfo.load(notifyMessage.friendProcessResult().userInfo());
bool agree = notifyMessage.friendProcessResult().agree();
handleWsAddFriendProcess(userInfo, agree);
} else if (notifyMessage.notifyType() == IMS::NotifyTypeGadget::NotifyType::FRIEND_REMOVE_NOTIFY) {
// 删除好友通知
const QString& userId = notifyMessage.friendRemove().userId();
handleWsRemoveFriend(userId);
}
}
我们看到这里调用了这个load成员函数,大家别忘了,这个是我们在data.h里面定义好的。
那么针对服务器推送过来的不同类型的消息,这里是将它们分别封装成了函数
cpp
void NetClient::handleWsRemoveFriend(const QString &userId)
{
// 1. 删除数据. DataCenter 好友列表的数据
dataCenter->removeFriend(userId);
// 2. 通知界面变化. 更新 好友列表 / 会话列表
emit dataCenter->deleteFriendDone();
}
void NetClient::handleWsAddFriendApply(const model::UserInfo &userInfo)
{
// 1. DataCenter 中有一个 好友申请列表. 需要把这个数据添加到好友申请列表中
QList<UserInfo>* applyList = dataCenter->getApplyList();
if (applyList == nullptr) {
LOG() << "客户端没有加载到好友申请列表!";
return;
}
// 把新的元素放到列表前面
applyList->push_front(userInfo);
// 2. 通知界面更新.
emit dataCenter->receiveFriendApplyDone();
}
void NetClient::handleWsAddFriendProcess(const model::UserInfo &userInfo, bool agree)
{
if (agree) {
// 对方同意了你的好友申请
QList<UserInfo>* friendList = dataCenter->getFriendList();
if (friendList == nullptr) {
LOG() << "客户端没有加载好友列表";
return;
}
friendList->push_front(userInfo);
// 同时也更新一下界面
emit dataCenter->receiveFriendProcessDone(userInfo.nickname, agree);
} else {
// 对方未同意好友申请
emit dataCenter->receiveFriendProcessDone(userInfo.nickname, agree);
}
}
void NetClient::handleWsSessionCreate(const model::ChatSessionInfo& chatSessionInfo) {
// 把这个 ChatSessionInfo 添加到会话列表中即可
QList<ChatSessionInfo>* chatSessionList = dataCenter->getChatSessionList();
if (chatSessionList == nullptr) {
LOG() << "客户端没有加载会话列表";
return;
}
// 新的元素添加到列表头部.
chatSessionList->push_front(chatSessionInfo);
// 发送一个信号, 通知界面更新
emit dataCenter->receiveSessionCreateDone();
}
可以看到,都是清一色的干2件事情
- 去数据中心更新数据
- 发送一个特定的信号
那么为什么发送这个信号呢?其实本质还是想要实现实时去刷新我们的显示界面,某个组件在接受到自己绑定的那个信号之后,就能里面对界面进行刷新。
1.3.最终的初始化函数
最后,我们终于能给出这个websocket的初始化函数了
cpp
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();
IMS::NotifyMessage notifyMessage;
notifyMessage.deserialize(&serializer, byteArray);
handleWsResponse(notifyMessage);
});
// 2. 和服务器真正建立连接
websocketClient.open(WEBSOCKET_URL);
}
至于我没有提到的,都是非常简单的。
二.HTTP模块
首先,我们需要先封装出2个常用的函数出来
cpp
QString NetClient::makeRequestId()
{
// 基本要求, 确保每个请求的 id 都是不重复(唯一的)
// 通过 UUID 来实现上述效果.
return "R" + QUuid::createUuid().toString().sliced(25, 12);
}
// 通过这个函数, 把发送 HTTP 请求操作封装一下.
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请求的发送都基于这2个函数来
接下来的所有函数都是需要和数据中心进行对接的,换句话说,接下来的接口都是向服务器发起HTTP获取请求,然后将获取的数据填充进数据中心,然后发送一个信号,然后对应的组件会提取给这个信号配置槽函数,等到这里的信号一发出,就会对窗口画面进行实时更新
cpp
void NetClient::getMyself(const QString &loginSessionId)
{
// 1. 构造出 HTTP 请求 body 部分
IMS::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<IMS::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();
});
}
这样子就形成了一个闭环。
至于后面的那些,我不想去接着写了,其实都是一个样的。