施磊老师基于muduo网络库的集群聊天服务器(七)

文章目录

数据表字符集问题

支持中文和英文

为什么使用 utf8mb4

  1. 支持完整的 Unicode 字符
    • utf8mb4 可以存储几乎所有语言的字符,支持 中文日文韩文,以及 Emoji 和符号。
  2. utf8 更加可靠
    • utf8 只支持最多三个字节的字符(不支持一些 4 字节字符,如 Emoji),而 utf8mb4 支持最多四个字节的字符。

查看数据库 所有表的 字符集

mysql 复制代码
SELECT 
    TABLE_NAME,
    TABLE_COLLATION
FROM 
    information_schema.TABLES
WHERE 
    TABLE_SCHEMA = '你的数据库名';

单独修改某个表

mysql 复制代码
ALTER TABLE 表名 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

批量修改字符集--修改某个字符集的所有表

c++ 复制代码
SELECT 
    CONCAT('ALTER TABLE ', TABLE_NAME, ' CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;') 
FROM 
    information_schema.TABLES
WHERE 
    TABLE_SCHEMA = '你的数据库名'
    AND TABLE_COLLATION LIKE 'latin1%';

推荐 查看整个表, 再单独修改

客户端群组功能

创建群组

c++ 复制代码
// creategroup函数
void creategroup(int clientfd, string msg)
{
    int idx = msg.find(":"); // 查找第一个:的位置
    if (idx == string::npos) // 没有找到  ==-1->不建议用
    {
        cout << "creategroup command: group name is invalid!" << endl;
        return;
    }
    string groupname = msg.substr(0, idx);                        // 截取群组名称
    string groupdesc = msg.substr(idx + 1, msg.size() - idx - 1); // 截取群组描述

    json js;
    js["msgid"] = CREATE_GROUP_MSG;   // 创建群组消息
    js["id"] = g_currentUser.getId(); // 当前登录用户id
    js["groupname"] = groupname;      // 群组名称
    js["groupdesc"] = groupdesc;      // 群组描述

    string request = js.dump();                                                // json转字符串  序列化
    int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据
    if (len < 0)
    {
        cerr << "send creategroup msg error: " << request << endl;
    }
}

添加群组

c++ 复制代码
// addgroup函数
void addgroup(int clientfd, string msg)
{
    int groupid = atoi(msg.c_str()); // 转成整型

    json js;
    js["msgid"] = ADD_GROUP_MSG;      // 添加群组消息
    js["id"] = g_currentUser.getId(); // 当前登录用户id
    js["groupid"] = groupid;          // 群组id

    string request = js.dump();                                                // json转字符串  序列化
    int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据
    if (len < 0)
    {
        cerr << "send addgroup msg error: " << request << endl;
    }
}

群组聊天

c++ 复制代码
// groupchat函数
void groupchat(int clientfd, string msg)
{
    int idx = msg.find(":"); // 查找第一个:的位置
    if (idx == string::npos) // 没有找到  ==-1->不建议用
    {
        cout << "groupchat command: group id is invalid!" << endl;
        return;
    }
    int groupid = atoi(msg.substr(0, idx).c_str());             // 截取群组id
    string message = msg.substr(idx + 1, msg.size() - idx - 1); // 截取聊天信息

    json js;
    js["msgid"] = GROUP_CHAT_MSG;         // 群组聊天消息
    js["id"] = g_currentUser.getId();     // 当前登录用户id
    js["name"] = g_currentUser.getName(); // 当前登录用户姓名
    js["groupid"] = groupid;              // 群组id  -- 字段要对应服务器那边的
    js["msg"] = message;                  // 聊天信息
    js["time"] = getCurrentTime();        // 时间

    // 发送聊天请求
    string request = js.dump();                                                // json转字符串  序列化
    int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据
    if (len < 0)
    {
        cerr << "send groupchat msg error: " << request << endl;
    }
}

接收在线群组消息

区分群组消息和个人消息

readTaskHandler函数

c++ 复制代码
// 解析json数据
json response = json::parse(buffer);   // 反序列化 字符串转json
if (response["msgid"] == ONE_CHAT_MSG) // 一对一聊天消息
{
    cout << response["time"].get<string>() << "[" << response["id"] << "] " << response["name"].get<string>() << " said: " << response["msg"].get<string>() << endl;
    continue;
}

if (response["msgid"] == GROUP_CHAT_MSG) // 群组聊天消息
{
    cout << "群消息-->[" << response["groupid"] << "] " << response["time"].get<string>() << "[" << response["id"] << "] " << response["name"].get<string>() << " said: " << response["msg"].get<string>() << endl;
}

接收离线群组消息

同样进行区分

main

c++ 复制代码
// 处理离线消息
if (response.contains("offlinemsg")) // 判断是否包含字段, 跟好点,  而不是看 是不是空
{
    vector<string> offlinemsg = response["offlinemsg"]; // 类型是vector<string>, 不是vector<User>,  根据服务器业务,存的是js.dump() 字符串
    for (auto &msg : offlinemsg)
    {
        json js = json::parse(msg); // 反序列化
        // 时间+fromid+fromname+msg-----详看笔记 一对一聊天发送的格式

        // 分一下 个人离线和群组离线
        if (js["msgid"] == ONE_CHAT_MSG)
        {
            cout << js["time"].get<string>() << "[" << js["id"].get<int>() << "] " << js["name"].get<string>() << " said: " << js["msg"].get<string>() << endl;
        }
        if (js["msgid"] == GROUP_CHAT_MSG) // 群组聊天消息
        {
            cout << "群消息-->[" << js["groupid"] << "] " << js["time"].get<string>() << "[" << js["id"] << "] " << js["name"].get<string>() << " said: " << js["msg"].get<string>() << endl;
        }
    }
}

补充服务器事件处理器

服务器业务---> 没有加 群组相关的 事件处理器

补充服务器查询群组列表

登录成功--查询群组列表

c++ 复制代码
// 查询群组列表
vector<Group> groupVec = _groupModel.queryGroups(id);
if (!groupVec.empty())
{
    vector<string> vec;
    for (auto &groupl : groupVec)
    {
        json js ;
        js["id"] = groupl.getId();
        js["groupname"] = groupl.getName();
        js["groupdesc"] = groupl.getDesc();

        vector<string> usersvec;
        for (auto &user : groupl.getUsers())
        {
            json js;
            js["id"]=user.getId();
            js["name"]=user.getName();
            js["state"]=user.getState();
            js["role"] = user.getRole();
            usersvec.push_back(js.dump());
        }
        js["users"] = usersvec;
        vec.push_back(js.dump());
    }
    response["groups"] = vec; // 群组列表
}

问题解决

服务器部分 groupmodel.cpp 有一些逻辑问题, 导致读不到用户, 提前返回了, 已进行修改

测试

自行测试--- 至此,功能都正常

目前报错总结

目前了解的:

数据库不支持 中文-----英文正常

数据库本身问题----把语句 先在mysql命令行试一下, 看情况处理

json在gdb不能直接看----在代码上添加 临时量 存储 js.dump(), 进行查看

由于客户端 和 服务器 和 mysql分离, 所以 有时候 可以分步测试, 看mysql 有没有, 再去排除问题

目前为止最恶心的错误

c++ 复制代码
terminate called after throwing an instance of 'nlohmann::json_abi_v3_12_0::detail::type_error'
what():  [json.exception.type_error.302] type must be string, but is null
Aborted (core dumped)
    
 

这个错误, 基本就是 json 变量名写错了

c++ 复制代码
type must be string, but is null: 你试图像字符串一样使用一个字段(如 j["name"].get<std::string>()),但 j["name"] 实际是 null,无法转换成字符串。

下面这段, cout里面是 js. 不是response.

c++ 复制代码
// 处理离线消息


// 分一下 个人离线和群组离线
if (js["msgid"] == ONE_CHAT_MSG)
{
    cout << js["time"].get<string>() << "[" << js["id"].get<int>() << "] " << js["name"].get<string>() << " said: " << js["msg"].get<string>() << endl;
}
if (js["msgid"] == GROUP_CHAT_MSG) // 群组聊天消息
{
    cout << "群消息-->[" << js["groupid"] << "] " << js["time"].get<string>() << "[" << js["id"] << "] " << js["name"].get<string>() << " said: " << js["msg"].get<string>() << endl;
}

客户端用户注销功能

用户登出:

1. 结束mainMenu

2. 结束对应的 接受线程

1. 用户注销功能的设计与实现

区分了 quit-->正常注销 和 客户端异常-->ctrl+c

  • 正常注销 vs. 异常退出
    • 正常注销 :客户端主动发送 LOGOUT 消息,服务端收到后:
      • connectionMap 移除该用户的连接(需加锁保证线程安全)。
      • 更新数据库,将该用户状态设为 offline
    • 异常退出 (如客户端强制关闭):
      • 服务端检测到连接断开,触发 closeException 处理。
      • 由于无法直接获取用户ID,需遍历 connectionMap 比对连接来删除。
  • 关键点
    • 正常注销比异常退出更高效,因为能直接通过 userID 定位用户连接。
    • 服务端需要保证对共享资源(如 connectionMap)的线程安全操作。

2. 客户端状态管理与循环控制

我们希望, 在正常退出后, 会回到主菜单页面!

之前的代码, 在登录后会进入 菜单, 进入死循环, 可以一直 发送消息

  • 全局变量控制页面跳转
    • 引入 isMainMenuRunning(布尔值)控制主菜单循环:
      • 登录成功 → true(进入主菜单循环)。
      • 注销 → false(退出循环,返回登录页面)。
    • 避免因循环阻塞导致无法返回登录界面。
  • 关键点
    • 通过状态变量而非死循环控制流程,使代码更清晰、可维护。
    • 注销后需重置用户数据(如好友列表、群组列表),防止重复加载。

3. 数据重复加载问题与解决方案

  • 问题
    • 用户注销后重新登录时,未清空旧数据,导致好友/群组列表重复累积。
  • 解决方案
    • 每次登录时先清空容器 (如 friendList.clear()),再加载新数据。
    • 确保数据唯一性,避免重复存储。

4. 接收线程的单例控制-重点

  • 问题
    • 多次登录会重复启动接收线程,导致多个线程同时监听消息,引发混乱。
  • 解决方案
    • 使用静态变量 确保线程只启动一次:
      • 首次登录时启动线程,后续登录直接复用。
      • 避免资源浪费和线程竞争。

巧用静态局部变量

静态局部变量(static 局部变量)是 C/C++ 中的一种局部变量,其特点是:

  1. 作用域:与普通的局部变量一样,静态局部变量的作用域仅限于函数内部。它只能在声明它的函数内使用,外部无法访问。
  2. 生命周期:与普通的局部变量不同,静态局部变量的生命周期是整个程序运行期间。它在程序开始时就被分配内存,并在程序结束时销毁,而普通局部变量则是在函数调用时创建,在函数调用结束时销毁。
  3. 初始化 :静态局部变量只会在第一次进入该函数时进行初始化,之后不会再次初始化。如果没有显式初始化,静态局部变量会被默认初始化为零(对于基本数据类型)。
c++ 复制代码
#include <iostream>

void counterFunction() {
    static int counter = 0; // 静态局部变量
    counter++;
    std::cout << "Counter: " << counter << std::endl;
}

int main() {
    counterFunction(); // 输出 Counter: 1
    counterFunction(); // 输出 Counter: 2
    counterFunction(); // 输出 Counter: 3
    return 0;
}

老师这节课进行了一个小总结

5. 架构设计思想

  • 分层解耦
    • 网络层:仅负责数据传输(如收发JSON消息)。
    • 业务层:处理具体逻辑(如注销、更新状态),不直接操作数据库。
    • 数据层 :通过ORM(如UserGroup类)封装数据库操作,业务层无需关心SQL细节。
  • 关键优势
    • 代码可维护性高,各层职责清晰。
    • 扩展性强(如未来支持集群化,只需修改服务端,客户端无感知)。

6. 集群化扩展的铺垫

  • 客户端无感知
    • 无论服务端是单机还是集群,客户端只需发送请求,不关心后端如何负载均衡。
  • 服务端集群化
    • 后续可通过Nginx实现负载均衡,水平扩展服务器性能。在很短的时间内 提升服务器并发能力
    • 需解决共享状态问题(如用户连接信息需集中管理,如用Redis)。

错误

  1. main 的 for循环 最后 不要return, 不然 注销后, 主线程就结束了

  2. 老师的 课中, 出现正常注销后, 再次登录, 会打印两边信息, 是因为 那几个变量是 全局的, 每次重新登录后变量没有清空, 之前的信息还在

    c++ 复制代码
    g_currentUserGroupsList.clear();
    // vector.clear()清空一下

代码

include/public.hpp

c++ 复制代码
LOGINOUT_MSG, // 登录成功

include/server/chatservice.hpp

c++ 复制代码
// 处理注销业务
    void loginout(const TcpConnectionPtr &conn, json &js, Timestamp time);

src/server/chatservice.cpp

c++ 复制代码
_msghandlermap.insert({LOGINOUT_MSG, std::bind(&ChatService::loginout, this, _1, _2, _3)});




// 处理注销业务
void ChatService::loginout(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
    int userid = js["id"].get<int>();
    {
        lock_guard<mutex> lock(_connMutex);
        auto it = _userConnMap.find(userid);
        if(it != _userConnMap.end())
        {
            _userConnMap.erase(it);
        }
    }

    // 更新用户状态
    User user(userid, "","", "offline");
    // user.setId(userid);
    // user.setState("offline");
    _usermodel.updateState(user); // 仅需要id和状态, 剩下的具体 由函数在数据库完成 
}

src/client/main.cpp

c++ 复制代码
// 控制聊天页面--注销需要退出聊天页面
bool isMainMenuRunning = false;
c++ 复制代码
// 登录成功, 启动接收线程----只要客户端 不完全退出, 就只启动一次!
static int threadnum = 0;
if (threadnum == 0)
{
    std::thread readTask(readTaskHandler, clientfd); // thread 支持跨平台
    readTask.detach();
}

// 分离线程, 让其独立运行, 不阻塞主线程

// 主线程继续执行, 进入聊天菜单页面
isMainMenuRunning = true;
mainMenu(clientfd);
c++ 复制代码
// 删除 return 0;
c++ 复制代码
// 主页面聊天程序
void mainMenu(int clientfd)
{
    help();
    // for (;;)
    while (isMainMenuRunning)
    {...}
}
c++ 复制代码
// quit函数
void quit(int clientfd, string msg)
{

    json js;
    js["msgid"] = LOGINOUT_MSG;       // 注销消息
    js["id"] = g_currentUser.getId(); // 当前登录用户id

    string request = js.dump();                                                // json转字符串  序列化
    int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据
    if (len < 0)
    {
        cerr << "send quit msg error: " << request << endl;
    }
    isMainMenuRunning = false; // 退出聊天页面
    // close(clientfd);  放在服务端处理
    // exit(0);
}
相关推荐
子非衣10 分钟前
Windows云主机远程连接提示“出现了内部错误”
服务器·windows
布尼卡1 小时前
mac brew 无法找到php7.2 如何安装php7.2
php·mac
Gazer_S1 小时前
【HTTP/2:信息高速公路的革命】
网络·网络协议·http
lLinkl1 小时前
项目笔记2:post请求是什么,还有什么请求
服务器·网络协议·http
ALe要立志成为web糕手1 小时前
[BJDCTF2020]EzPHP
web安全·网络安全·php·ctf
李匠20241 小时前
C++ RPC以及cmake
网络·c++·网络协议·rpc
科技小E1 小时前
EasyRTC音视频实时通话嵌入式SDK,打造社交娱乐低延迟实时互动的新体验
大数据·网络
珹洺1 小时前
Linux操作系统从入门到实战(三)Linux基础指令(上)
linux·运维·服务器
再睡一夏就好1 小时前
Linux常见工具如yum、vim、gcc、gdb的基本使用,以及编译过程和动静态链接的区别
linux·服务器·c语言·c++·笔记
归寻太乙2 小时前
Linux环境变量
linux·运维·服务器