施磊老师基于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);
}
相关推荐
BingoGo1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
Sinclair3 天前
简单几步,安卓手机秒变服务器,安装 CMS 程序
android·服务器
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
Rockbean4 天前
用40行代码搭建自己的无服务器OCR
服务器·python·deepseek
茶杯梦轩4 天前
CompletableFuture 在 项目实战 中 创建异步任务 的核心优势及使用场景
服务器·后端·面试
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel