0.注册账户用于跨服务器测试
0.1 注册账户
注册完用户头像丢失,查看一下客户端代码
由于找头文件每次都很不方便,代码太多,所以新建筛选器来管理项目

最终加载头像的属于基础窗口的chatwidget,直接去该筛选器下找到对应的头文件和cpp文件

初始化头像是从用户下加载的用户数据

查看该用户数据是何时设置的


在处理登录响应时已经设置了


查看注册时客户端的代码

GateServer服务器的代码,Mysql正确写入了

查看聊天服务器的代码
首先再次登录就没有任何问题
查看日志发现,服务器发来的数据是空的,

代码写多了,才知道原来指针也是有值传递的,我下意识的以为指针都是引用,哈哈

改成这样,然后采用默认的赋值运算符即可

已解决
1.跨服务器通讯
继承编译生成好的GRPC服务,重载一下函数,然后自定义一个客户端,通过创建和chatserver的连接,来调用接口通过连接和chatserver进行通信

1.1 启动GRPC服务
chatserver服务器启动监听客户端连接和grpc服务
cpp
int main()
{
auto pool = AsioIOServicePool::GetInstance();
boost::asio::io_context io_ct;
boost::asio::signal_set signals(io_ct, SIGINT, SIGTERM);
// 监听客户端连接
const int port = get<int>(ServerStatic::ParseConfig("SelfServer", "Port"));
unsigned short port_ushort = static_cast<unsigned short>(port);
shared_ptr<CServer> pServer = make_shared<CServer>(io_ct, port_ushort);
LogicSystem::GetInstance()->SetServer(pServer);
pServer->StartAccept();
// 启动grpc服务
const int rpcPort = get<int>(ServerStatic::ParseConfig("SelfServer", "RPCPort"));
string IP = "localhost:" + to_string(rpcPort);
ChatServiceImpl service;
grpc::ServerBuilder builder;
// 监听端口并添加要监听的服务
builder.AddListeningPort(IP, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
// 启动服务
unique_ptr<grpc::Server> server(builder.BuildAndStart());
// 创建单独的线程处理grpc服务
thread grpcThread([&server]()
{
cout << "grpcThread start" << endl;
server->Wait();
});
signals.async_wait([&io_ct, pool, &server](auto, auto)
{
server->Shutdown();
io_ct.stop();
pool->Stop();
});
io_ct.run();
grpcThread.join();
return 0;
}
1.2 跨服务器申请添加好友
1.chatserver1服务器处理客户端发来的添加好友申请

向chatserver2发送添加好友请求

2.chatserver2处理添加好友请求
获取申请者和授权者的信息然后往客户端发送请求即可

1.3 跨服务器授权好友申请
chatserver1服务器处理请求,然后查询申请者所在的服务器是否是chatserver1,如果是chatserver2则通过grpc请求向chatserver2进行通信,chatserver2收到通信后,获取申请者的会话,然后将授权结果返回给申请者
cpp
message AuthFriendRequest {
int32 fromUid = 1;
int32 toUid = 2;
}

gapc客户端发送请求

grpc服务器处理授权好友请求
cpp
Status ChatServiceImpl::NotifyAuthFriend(ServerContext* context, const AuthFriendRequest* request, AuthFriendResponse* response)
{
// 查找用户是否在本服务器
int apply_uid = request->touid(); // 获取申请者UID
int auth_uid = request->fromuid(); // 获取授权者UID
shared_ptr<CSession> pSession = UserMgr::GetInstance()->GetSession(apply_uid); // 获取与申请者的会话
// 返回响应
ConnectionRAII raii([request, response]()
{
response->set_error(ErrorCodes::SUCCESS);
response->set_fromuid(request->fromuid());
response->set_touid(request->touid());
});
// 用户不在线,该会话为空
if (pSession == nullptr)
return Status::OK;
// 用户在线,发送通知给对方
Json::Value jsonMsg;
jsonMsg["error"] = ErrorCodes::SUCCESS;
jsonMsg["auth_uid"] = auth_uid; // 授权者UID
// 获取授权者的基本信息
string baseKey = USER_BASE_INFO + to_string(auth_uid);
// shared_ptr<UserInfo> pUserInfo(new UserInfo); // 先调用UserInfo的构造函数再调用shared_ptr的构造以传参接管裸指针
shared_ptr<UserInfo> pUserInfo = make_shared<UserInfo>(); // 直接使用make_shared构造shared_ptr
bool bInfo = GetBaseInfo(baseKey, auth_uid, pUserInfo);
if (bInfo)
{
jsonMsg["auth_name"] = pUserInfo->name;
jsonMsg["auth_nick"] = pUserInfo->nick;
jsonMsg["auth_icon"] = pUserInfo->icon;
jsonMsg["auth_sex"] = pUserInfo->sex;
jsonMsg["auth_desc"] = pUserInfo->desc;
}
else
{
jsonMsg["error"] = ErrorCodes::UID_INVALID;
}
string result = jsonMsg.toStyledString();
// 向该在线用户发送好友申请信息
pSession->Send(result, MSG_IDS::ID_NOTIFY_AUTH_FRIEND_REQUEST);
return Status::OK;
}
1.4 跨服务器通讯
客户端发送消息
从编辑器中获取拆解的文本

假如全是文本,那么msgList的大小就是1
如果是文本 + 图片
大小为2
cpp
// 遍历消息列表,创建对应的消息气泡并添加到聊天视图中
for (int i = 0; i < msgList.size(); i++)
{
// 限制文本长度
if (msgList[i].MsgContent.length() > MAX_TEXT_LEN)
continue;
QString type = msgList[i].MsgType;
ChatItemBase* item = new ChatItemBase(role); // 聊天窗口
item->SetUserName(userName);
item->SetUserIcon(QPixmap(userIcon));
QWidget* bubble = nullptr; // 根据消息类型来创建气泡类型
// 生成消息ID
QUuid uuid = QUuid::createUuid();
QString uuidStr = uuid.toString();
if (type == MSG_TYPE_TEXT)
{
bubble = new TextBubble(role, msgList[i].MsgContent); // 文本气泡
// 如果文本长度超过限制,则发送给服务器
if (textSize + msgList[i].MsgContent.length() > MAX_TEXT_LEN)
{
SendMsgToTcp(textObj, textArray, textSize, userInfo->_uid);
}
textSize += msgList[i].MsgContent.length();
QJsonObject obj;
obj["content"] = msgList[i].MsgContent;
obj["msgId"] = uuidStr; // 标识消息的ID
obj["type"] = type;
textArray.append(obj); // 添加到文本数组中
QSharedPointer<TextChatData> chatMsg(new TextChatData(uuidStr, type, obj["content"].toString(),
userInfo->_uid, _userInfo->_uid));
emit SigAppendChatMsgToChatUserWnd(chatMsg); // 将与该用户的聊天信息添加到对应的聊天小窗口中
}
else if (type == MSG_TYPE_IMAGE)
{
QByteArray base64String = msgList[i].Msg_Image_Data.toUtf8(); // 先转为 UTF-8 QByteArray
QByteArray imgData = QByteArray::fromBase64(base64String); // 这才是真正的图片二进制数据
bubble = new PictureBubble(role, msgList[i].MsgPicture, imgData);
QJsonObject obj;
obj["content"] = msgList[i].Msg_Image_Data;
obj["msgId"] = uuidStr;
obj["type"] = type;
textArray.append(obj);
QSharedPointer<TextChatData> chatMsg(new TextChatData(uuidStr, type, obj["content"].toString(),
userInfo->_uid, _userInfo->_uid));
emit SigAppendChatMsgToChatUserWnd(chatMsg); // 将与该用户的聊天信息添加到对应的聊天小窗口中
}
else if (type == MSG_TYPE_FILE)
{
}
// 添加到聊天窗口中
if (bubble)
{
item->SetWidget(bubble);
ui.chatView->AppendChatItem(item);
}
}
最后将循环中,创建的json数组QJsonArray,添加到最后要打包的格式里面

如下是最终打包发送的Json格式
cpp
{
"fromuid": "发送者ID",
"touid": "接收者ID",
"textArray": [
{
"content": "消息内容1",
"msgId": "唯一ID1",
"type": "消息类型1"
},
{
"content": "消息内容2",
"msgId": "唯一ID2",
"type": "消息类型2"
},
...
]
}
单服务器解析客户端发送的文本信息
就是把数据拿出来,然后装到C++的Json格式中而已,多了一个Error,当然这是针对于在同一个服务器上而言
cpp
// 1.解析客户端发来的聊天信息
reader.parse(msg_data, jsonResult);
int uid = jsonResult["fromuid"].asInt();
int toUid = jsonResult["touid"].asInt();
cout << "Deal Chat Text Msg fromUid is " << uid << " toUid is " << toUid << "\n";
// 聊天信息
const Json::Value arrays = jsonResult["textArray"];
jsonReturn["error"] = ErrorCodes::SUCCESS;
jsonReturn["senderUid"] = uid;
jsonReturn["receiverUid"] = toUid;
jsonReturn["textArray"] = arrays;
跨服务器解析客户端发送的文本信息,并根据通讯协议打包新的消息体发送给另一个服务器
需要通过自己设定的协议,来组装成对应的包,通过grpc发送给另一个聊天服务器(chatserver2)
这里的repeated表示的是这个包的数组,类似于vector<TextChatData>
cpp
message TextChatMsgRequest {
int32 fromUid = 1;
int32 toUid = 2;
repeated TextChatData textMsgs = 3;
}
message TextChatData{
string msgId = 1;
string msgContent = 2;
string msgType = 3;
}
将json转换成TextChatMsgRequest,这个请求的数据格式如上,类似于上面说的Json格式
cpp
// 5.如果不在一个服务器上,向被聊天者所在的服务器发送聊天信息
TextChatMsgRequest textMsgRequest;
textMsgRequest.set_fromuid(uid);
textMsgRequest.set_touid(toUid);
for (const auto& text : arrays)
{
auto content = text["content"].asString();
string msgId = text["msgId"].asString();
string msgType = text["type"].asString();
cout << "Deal Chat Text Msg content is " << content << " msgId is " << msgId << "\n";
TextChatData* textMsg = textMsgRequest.add_textmsgs();
textMsg->set_msgid(msgId);
textMsg->set_msgcontent(content);
textMsg->set_msgtype(msgType);
}
ChatGrpcClient::GetInstance()->NotifyTextChatMsg(toIpValue, textMsgRequest, jsonReturn);
grpc服务器处理文本请求
跟之前客户端打包Json一样,都是发送者ID + 接收者ID + 文本数组的形式
cpp
// 用户在线,发送通知给对方
Json::Value jsonMsg;
jsonMsg["error"] = ErrorCodes::SUCCESS;
jsonMsg["senderUid"] = request->fromuid();
jsonMsg["receiverUid"] = toUid;
// 将聊天数据整合成Json嵌套的数据
Json::Value textArray;
for (auto& msg : request->textmsgs())
{
Json::Value element;
element["content"] = msg.msgcontent();
element["msgId"] = msg.msgid();
element["type"] = msg.msgtype();
textArray.append(element);
}
jsonMsg["textArray"] = textArray;
string result = jsonMsg.toStyledString();
// 向该在线用户发送聊天信息请求
pSession->Send(result, MSG_IDS::ID_NOTIFY_TEXT_CHAT_MSG_REQUSET);
接收者客户端解析服务器发来的Json
将Json解析成自定义的数据结构,并发送出去,交给ChatWidget类去处理
cpp
/* 客户端(接受文本)处理服务器发送的文本请求 */
_handlers.insert(ReqID::ID_NOTIFY_TEXT_CHAT_MSG_REQUEST, [this](ReqID id, int len, QByteArray data)
{
QJsonObject jsonObj;
bool ret = PraseJsonData(jsonObj, "NotifyChatMsgRequest", id, len, data);
if (!ret)
{
return;
}
qDebug() << QString::fromLocal8Bit("收到文本消息");
// 判断消息类型
QSharedPointer<TextChatMsg> chatMsg(new TextChatMsg(jsonObj["senderUid"].toInt(), jsonObj["receiverUid"].toInt(),
jsonObj["textArray"].toArray()));
emit SigTextChatMsg(chatMsg);
});
ChatMsg结构体的格式跟打包发送和接受的消息格式一摸一样
发送者ID + 接收者ID + 文本数组
cpp
/* 聊天信息(多条信息) */
struct TextChatMsg
{
TextChatMsg(int fromuid, int touid, QJsonArray arrays) :
_from_uid(fromuid), _to_uid(touid) {
for (auto msg_data : arrays)
{
auto msg_obj = msg_data.toObject();
auto content = msg_obj["content"].toString();
auto msgid = msg_obj["msgId"].toString();
auto type = msg_obj["type"].toString();
QSharedPointer<TextChatData> pMsg(new TextChatData(msgid, type, content, fromuid, touid));
_chat_msgs.push_back(pMsg);
}
}
int _to_uid;
int _from_uid;
QVector<QSharedPointer<TextChatData>> _chat_msgs;
};
在ChatWidget类下,先去通过解析的ChatMsg解析获取发送者ID,然后找到这个聊天窗口,并更新聊天信息
cpp
void ChatWidget::SlotTextChatMsg(QSharedPointer<TextChatMsg> msg)
{
// 查找发送者的聊天项,通过该聊天项找到聊天窗口
auto it = _chatItemMap.find(msg->_from_uid);
if (it != _chatItemMap.end())
{
qDebug() << "Set Chat Item Msg, uid is " << msg->_from_uid;
ui.chatUserList->insertItem(0, it.value()); // 选中该聊天项
ui.chatUserList->setCurrentItem(it.value()); // 选中该聊天项
QWidget* widget = ui.chatUserList->itemWidget(it.value());
ChatUserWnd* chatUserWnd = qobject_cast<ChatUserWnd*>(widget);
if(!chatUserWnd)
return;
chatUserWnd->UpdateLastMsgs(msg->_chat_msgs); // 该聊天窗口更新最后一条信息
UpdateChatMsg(msg->_chat_msgs); // 更新当前聊天界面信息
UserMgr::Instance()->AppendFriendChatMsgs(msg->_from_uid, msg->_chat_msgs); // 存储和当前用户的聊天信息
return;
}
// 如果没有找到,创建新的聊天小窗口
ChatUserWnd* chatUserWnd = new ChatUserWnd; // 聊天小窗口
// 获取好友信息
QSharedPointer<FriendInfo> friendInfo = UserMgr::Instance()->GetFriendByUid(msg->_from_uid);
chatUserWnd->SetInfo(friendInfo);
QListWidgetItem* item = new QListWidgetItem; // 新建聊天项
item->setSizeHint(chatUserWnd->sizeHint());
chatUserWnd->UpdateLastMsgs(msg->_chat_msgs); // 该聊天窗口更新最后一条信息
UserMgr::Instance()->AppendFriendChatMsgs(msg->_from_uid, msg->_chat_msgs); // 存储和当前用户的聊天信息
ui.chatUserList->insertItem(0, item); // 插入到聊天列表的最前面
ui.chatUserList->setItemWidget(item, chatUserWnd); // 必须添加项的容器,告诉列表如何显示这个容器
_chatItemMap[msg->_from_uid] = item; // 保存聊天项的映射关系
}
更新聊天信息
整体的逻辑如下,遍历文本数组,然后判断消息是自己发送的还是别人发送的,生成对应的消息气泡,如果是文本就生成文本气泡,如果是图片就生成图片气泡

cpp
void ChatWidget::UpdateChatMsg(QVector<QSharedPointer<TextChatData>> chatMsgs)
{
for (auto chatMsg : chatMsgs)
{
if (chatMsg->_from_uid != _curChatUid)
{
break;
}
ui.chatPage->AppendChatMsg(chatMsg);
}
}
void ChatPage::AppendChatMsg(QSharedPointer<TextChatData> chatMsg)
{
auto selfInfo = UserMgr::Instance()->GetUserInfo(); // 获取该客户端的信息
ChatRole role;
if (chatMsg->_from_uid == selfInfo->_uid)
{
role = ChatRole::Self;
ChatItemBase* pChatItem = new ChatItemBase(role); // 创建自己的消息气泡
pChatItem->SetUserName(selfInfo->_name);
pChatItem->SetUserIcon(QPixmap(selfInfo->_icon));
SpawnBubble(pChatItem, role, chatMsg->_msg_type, chatMsg->_msg_content); // 创建气泡
}
else
{
role = ChatRole::Other;
ChatItemBase* pChatItem = new ChatItemBase(role); // 创建对方的消息气泡
QSharedPointer<FriendInfo> otherInfo = UserMgr::Instance()->GetFriendByUid(chatMsg->_from_uid);
if (otherInfo == nullptr)
{
qDebug() << "Can't find friend info by uid:" << chatMsg->_from_uid;
return;
}
pChatItem->SetUserName(otherInfo->_name);
pChatItem->SetUserIcon(QPixmap(otherInfo->_icon));
SpawnBubble(pChatItem, role, chatMsg->_msg_type, chatMsg->_msg_content); // 创建气泡
}
}
void ChatPage::SpawnBubble(ChatItemBase* pChatItem, ChatRole role, QString msgType, QString msgContent)
{
QWidget* pBubble = nullptr;
qDebug() << "msg Type:" << msgType;
if (msgType == MSG_TYPE_TEXT)
{
pBubble = new TextBubble(role, msgContent); // 创建文本气泡
pChatItem->SetWidget(pBubble); // 插入消息内容
ui.chatView->AppendChatItem(pChatItem); // 添加到聊天视图中
}
else if (msgType == MSG_TYPE_IMAGE)
{
QByteArray base64String = msgContent.toUtf8(); // 先转为 UTF-8 QByteArray
QByteArray imgData = QByteArray::fromBase64(base64String); // 这才是真正的图片二进制数据
QImage img;
img.loadFromData(imgData);
QPixmap pixmap = QPixmap::fromImage(img);
pBubble = new PictureBubble(role, pixmap, imgData); // 创建图片气泡
pChatItem->SetWidget(pBubble); // 插入消息内容
ui.chatView->AppendChatItem(pChatItem); // 添加到聊天视图中
}
}
2. Navicat 设置唯一键

右键设计表

点击索引

先选中字段,将字段下的申请者UID和授权者UID选中,设置索引类型为唯一,即可
