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

客户端开发开始

客户端

客户端首页面功能

客户端这边的主要任务之一就是组织并构造 JSON 字符串,这些字符串代表了要发送给服务器的命令和数据。

为何不逐行开发?

  • 客户端不需要高并发(仅需处理用户输入和服务器响应),无需像服务端那样处理多线程竞争、IO复用等复杂问题。

  • 协议驱动开发:客户端只需按服务端定义的JSON协议收发数据(服务端要什么,客户端就发什么;服务端返回什么,客户端就解析什么),业务逻辑简单

  • 模块化直接讲解:因后续需重点讲解集群改造(如Redis发布订阅、跨服务器通信),客户端代码采用模块化展示,核心分为:

    • 网络通信层(TCP连接、数据收发)
    • 协议解析层(JSON序列化/反序列化)
    • 业务逻辑层(登录/注册/退出等)

客户端CMake

src/CMakeLists.txt

# 加载子目录  src   既然进去, 就有 CMakeLists.txt
add_subdirectory(server)

# 加载子目录
add_subdirectory(client)

src/client/CMakeLists.txt

# 所有源文件
aux_source_directory(. SRC_LIST)

# 生成可执行
add_executable(Chatserver ${SRC_LIST})

# 链接库  -- 仅有两个线程, 读取和写入
target_link_libraries(Chatserver pthread )

代码搭配知识补充–有很多漏的

客户端main-登录,注册,退出

  • 无高并发需求:客户端只需处理单一用户的输入输出,无需考虑服务端级别的并发问题(如EPOLL、线程池)。
  • 从业务角度: 客户端 需要做的 就是 登录, 注册, 退出

因此, 直接使用 一个简单的 网络连接即可------基于linux的tcp

c++11版本

提高效率:

用户登录成功后,服务器会将该用户的相关信息一次性返回,包括:

  • 当前登录用户的信息(如用户名、用户ID);
  • 用户的好友列表;
  • 用户的群组列表;
  • 显示登录用户信息;
  • 离线消息记录。

客户端在接收到这些信息后,会将其本地保存,方便用户随时查看,无需再次向服务器请求,从而提高整体交互效率。

从后往前写业务, 退出最简单, 注册次之, 登录及登录后最麻烦

#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <ctime>
using namespace std;

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "json.hpp"
using json = nlohmann::json;

#include "user.hpp"
#include "public.hpp"
#include "group.hpp"

// 记录当前系统登录的用户信息
User g_currentUser;
// 记录当前登录用户的好友列表
vector<User> g_currentUserFriendsList;
// 记录当前登录用户的群组列表
vector<Group> g_currentUserGroupsList;
// 显示当前登录用户的基本信息
void showCurrentUserInfo();

// 接收线程----一共两个线程, 接收和发送, 是并行的   --- main主线程用于发送
void readTaskHandler(int clientfd);
// 获取系统时间(聊天信息添加时间信息)
string getCurrentTime();
// 主聊天页面程序
void mainMenu();

// 聊天客户端程序实现, main线程用作发送线程, 子线程用作接受线程

int main(int argc, char **argv)
{
    if (argc < 3)
    {
        cerr << "command invalid example ./bin/chatserver 127.0.0.1 6000" << endl;
        exit(-1);
    }

    // 解析通过命令行参数传递的ip和port
    char *ip = argv[1];
    uint16_t port = atoi(argv[2]);

    // 创建socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);

    if (clientfd < 0)
    {
        cerr << "socket error" << endl;
        exit(-1);
    }

    // 设置服务器地址结构
    // sockaddr_in serverAddr;
    sockaddr_in server;
    memset(&server, 0, sizeof(sockaddr_in)); // 清空结构体, 确保没有脏数据

    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = inet_addr(ip);

    // 连接服务器
    if (connect(clientfd, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        cerr << "connect error" << endl;
        close(clientfd);
        exit(-1);
    }

    // main线程用于发送数据
    for (;;)
    {
        // 显示登录首页面 登录, 注册, 退出
        cout << "========================================" << endl;
        cout << "1. login" << endl;
        cout << "2. register" << endl;
        cout << "3. exit" << endl;
        cout << "========================================" << endl;
        cout << "please input your choice: ";
        int choice;
        cin >> choice;
        cin.get(); // 清空输入缓冲区

        switch (choice)
        {

        // # 3
        case 1: // 登录  根据业务, 需要id与密码
        {
            int id = 0;
            char password[50] = {0};
            cout << "please input your id: ";
            cin >> id;
            cin.get(); // 清空输入缓冲区
            cout << "please input your password: ";
            cin.getline(password, 50); // 读取一行, 包括空格 cin和scanf不能读空格

            // 组装json数据
            json js;
            js["msgid"] = LOGIN_MSG; // 登录消息
            js["id"] = id;
            js["password"] = password;
            // 发送登录请求
            string request = js.dump();                                                // json转字符串  序列化
            int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据
            if (len < 0)
            {
                cerr << "send login msg error: " << request << endl;
                cerr << "connect error" << endl;
            }
            else
            {
                char buffer[1024] = {0};               // 接收服务器返回的数据
                len = recv(clientfd, buffer, 1024, 0); // 接收数据
                if (len < 0)
                {
                    cerr << "recv error" << endl;
                }
                else
                {
                    // 解析json数据
                    json response = json::parse(buffer); // 反序列化 字符串转json
                    string tmp = response.dump(); // json转字符串  序列化
                    if (response["errno"] == 0)
                    { // 根据业务代码处理 1.登录成功返回 2.好友列表 3.群组列表 4.离线消息
                        cout << "login success" << endl;
                        // 客户端记录登录用户信息
                        g_currentUser.setId(response["id"]);
                        g_currentUser.setName(response["name"]);

                        // if(response["friends"].is_null())
                        // 处理好友列表
                        // if (response["friends"].contains("friends")) // 判断是否包含字段, 跟好点,  而不是看 是不是空
                        if(response.contains("friends"))
                        {
                            vector<string> friends = response["friends"]; // 类型是vector<string>, 不是vector<User>,  根据服务器业务,存的是js.dump() 字符串
                            g_currentUserFriendsList.clear();
                            for (auto &friendUser : friends)
                            {
                                json js = json::parse(friendUser); // 反序列化
                                User user;
                                user.setId(js["id"]);
                                user.setName(js["name"]);
                                user.setState(js["state"]);
                                g_currentUserFriendsList.push_back(user);
                            }
                            for (auto &friendUser : g_currentUserFriendsList)
                            {
                                cout << "friendid: " << friendUser.getId() << " name: " << friendUser.getName() << " state: " << friendUser.getState() << endl;
                            }
                        }
                        else
                        {
                            cout << "friends list is empty" << endl;
                        }

                        // 处理群组列表
                        if (response.contains("groups")) // 判断是否包含字段, 跟好点,  而不是看 是不是空
                        {
                            vector<string> groups = response["groups"]; // 类型是vector<string>, 不是vector<User>,  根据服务器业务,存的是js.dump() 字符串
                            g_currentUserGroupsList.clear();
                            for (auto &groupl : groups)
                            {
                                json js = json::parse(groupl); // 反序列化
                                Group group;
                                group.setId(js["id"]);
                                group.setName(js["groupname"]);
                                group.setDesc(js["groupdesc"]);

                                // 处理群组成员列表
                                vector<string> users = js["users"];
                                for (auto &userl : users)
                                {
                                    json js = json::parse(userl); // 反序列化
                                    GroupUser user;
                                    user.setId(js["id"]);
                                    user.setName(js["name"]);
                                    user.setState(js["state"]);
                                    user.setRole(js["role"]);
                                    group.getUsers().push_back(user);
                                }
                                g_currentUserGroupsList.push_back(group);
                            }
                            for(auto &group : g_currentUserGroupsList)
                            {
                                cout << "groupid: " << group.getId() << " name: " << group.getName() << " desc: " << group.getDesc() << endl;
                                for (auto &groupUser : group.getUsers())
                                {
                                    cout << "group user id: " << groupUser.getId() << " name: " << groupUser.getName() << " state: " << groupUser.getState() << " role: " << groupUser.getRole() << endl;
                                }
                            }
                        }
                        else
                        {
                            cout << "groups list is empty" << endl;
                        }

                        // 显示当前登录用户的基本信息---包含好友列表和群组列表
                        showCurrentUserInfo();

                        // 处理离线消息
                        if (response["offlinemsg"].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-----详看笔记 一对一聊天发送的格式
                                cout << js["time"] << "[" << js["id"] << "] " << js["name"] << "said : " << js["msg"] << endl;
                            }
                        }
                        else
                        {
                            cout << "offlinemsg list is empty" << endl;
                        }

                        // 登录成功, 启动接收线程
                        std::thread readTask(readTaskHandler, clientfd); // thread 支持跨平台
                        readTask.detach();                               // 分离线程, 让其独立运行, 不阻塞主线程

                        // 主线程继续执行, 进入聊天菜单页面
                        mainMenu(clientfd);
                    }
                    // else if(response["errno"] == 1)
                    // {
                    //     // 用户不存在
                    //     cout << "login failed, error: " << response["errmsg"] << endl;
                    // }
                    // else if(response["errno"] == 2)
                    // {
                    //     // 重复登录
                    //     cout << "login failed, error: " << response["errmsg"] << endl;
                    // }
                    // else if(response["errno"] == 3)
                    // {
                    //     // 密码错误
                    //     cout << "login failed, error: " << response["errmsg"] << endl;
                    // }
                    else // 不分那么细, 服务器已经确定错误信息了
                    {
                        // 登录失败
                        cout << "login failed, error: " << response["errmsg"] << endl;
                        break;
                    }
                }
            }
        }
        break;
        // # 2
        case 2: // 注册
        {
            char name[50]; // 比string更好, 因为string会有内存分配, 还可以限制长度
            char password[50];
            cout << "please input your name: ";
            cin.getline(name, 50); // 读取一行, 包括空格 cin和scanf不能读空格
            cout << "please input your password: ";
            cin.getline(password, 50);

            // 组装json数据
            json js;
            js["msgid"] = REG_MSG; // 注册消息
            js["name"] = name;
            js["password"] = password;

            // 发送注册请求
            string request = js.dump(); // json转字符串  序列化

            // int len = send(clientfd, request.c_str(), request.size(), 0); // 发送数据
            // 第二个参数必须这么写, 因为规定是const void*类型, 不能直接传入string类型
            int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据 这样的+1, 是加了 \0, strlen不算这个
            if (len < 0)
            {
                cerr << "send error" << request << endl;
            }
            else
            {
                char buffer[1024] = {0};               // 接收服务器返回的数据
                len = recv(clientfd, buffer, 1024, 0); // 接收数据
                if (len < 0)
                {
                    cerr << "recv error" << endl;
                }
                else
                {
                    // 解析json数据
                    json response = json::parse(buffer); // 反序列化 字符串转json
                    if (response["errno"] == 0)
                    { // 根据业务代码处理
                        cout << "register success, userid: " << response["id"] << " do not forget it!" << endl;
                    }
                    else
                    {
                        // 注册失败
                        cout << "register failed, error: " << name << " is already exit!" << endl;
                    }
                }
            }
        }
        break;
        // # 1
        case 3: // 这个最简单 quit业务
        {
            cout << "exit system" << endl;
            close(clientfd);
            exit(0);
        }
        default:
        {
            cout << "input error" << endl;
            break;
        }
        }
    }
    return 0;
}


void readTaskHandler(int clientfd)
{
    
}

void mainMenu()
{
    
}

一定要根据之前的 服务器业务, 写这个首页面功能

群组有问题

在服务器登录业务不分, 没有返回 群组字段信息

测试

每种情况都测试一下, 各种错误也试试, 不然会漏掉错误

客户端好友添加与聊天功能

表驱动设计:

  • 表驱动(commandMap + commandHandlerMap)

    • 解耦:将命令的定义、帮助文本、处理逻辑分离,新增命令只需扩展映射表。
    • 用户友好help命令动态展示所有功能,降低学习成本。
  • commandMap:存储命令名称和帮助文本(如"chat" : "一对一聊天,格式:chat:friendID:message")。

  • commandHandlerMap:关联命令名称和处理函数(如"chat"绑定到chatHandler函数)。

  • 优势:符合开闭原则,新增命令只需扩展映射表,无需修改主逻辑。

commandMap

commandMap 是一个 命令-格式说明 的映射表,用于 向用户清晰展示所有可用命令及其正确输入格式,帮助用户快速上手并减少输入错误。

// 系统支持的客户端命令列表
unordered_map<string, string> commandList = {
    {"help", "显示所有支持的命令, 格式help"},
    {"chat", "一对一聊天, 格式chat:friend:msg"},
    {"addfriend", "添加好友, 格式addfriend:friendid"},
    {"creategroup", "创建群组, 格式creategroup:groupname:groupdesc"},
    {"addgroup", "添加群组, 格式addgroup:groupid"},
    {"groupchat", "群组聊天, 格式groupchat:groupid:msg"},
    {"loginout/quit", "退出系统/注销, 格式quit"}
};

commandHandlerMap

commandHandlerMap 是一个 命令-处理函数 的映射表,用于 将用户输入的命令动态绑定到对应的业务逻辑。它的核心价值在于:

  • 解耦输入解析与业务逻辑:分离“用户输入的是什么”和“该输入如何被执行”。
  • 统一管理所有命令的执行入口:避免冗长的 if-elseswitch-case 分支判断。
// 注册系统支持的客户端命令处理
unordered_map<string, function<void(int, string)>> commandHandlerMap = {
    {"help", help},
    {"chat",chat},  // 一对一聊天
    {"addfriend", addfriend}, // 添加朋友
    {"creategroup", creategroup}, // 创建群组
    {"addgroup", addgroup}, // 添加群组
    {"groupchat", groupchat}, // 群组聊天
    {"quit", quit}
    
};

为什么都是int, string

  1. intclientFd
    • 代表 Socket 文件描述符,所有网络操作(如发送聊天消息、添加好友)必须通过这个 int 标识符与服务器通信。它是 客户端与服务器交互的唯一通道,因此必须传递给每个命令处理函数。
  2. string(用户输入)
    • 用户输入的命令参数–commandMap格式(如好友ID、消息内容)都是 字符串形式,方便解析和序列化成 JSON 发送。
  3. 统一接口
    • 所有命令处理函数保持 相同参数类型int, string),便于映射到 commandHandlerMap,实现 标准化调用

添加好友和聊天功能

// 接收线程---实时就收 服务器返回的数据--包括别人发来的聊天消息
void readTaskHandler(int clientfd)
{
    for (;;)
    {
        char buffer[1024] = {0};                   // 接收服务器返回的数据
        int len = recv(clientfd, buffer, 1024, 0); // 接收数据
        if (len < 0)                               // ==-1
        {
            cerr << "recv error" << endl;
            close(clientfd);
            exit(-1);
        }
        else if (len == 0) // 服务器关闭连接
        {
            cout << "server close" << endl;
            close(clientfd);
            exit(-1);
        }

        // 解析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;
        }
        // else if (response["msgid"] == GROUP_CHAT_MSG) // 群组聊天消息
        // {
        //     cout<<response["time"].get<string>()<<"["<<response["id"]<<"] "<<response["name"].get<string>()<<" said: "<<response["msg"].get<string>()<<endl;
        // }
    }
}

// handler合集
// 显示帮助信息
void help(int clientfd, string msg);

// 一对一聊天
void chat(int clientfd, string msg);

// 添加好友
void addfriend(int clientfd, string msg);

// 创建群组
void creategroup(int clientfd, string msg);

// 添加群组
void addgroup(int clientfd, string msg);

// 群组聊天
void groupchat(int clientfd, string msg);
// 退出系统
void quit(int clientfd, string msg);


// 系统支持的客户端命令列表
unordered_map<string, string> commandList = {
    {"help", "显示所有支持的命令, 格式help"},
    {"chat", "一对一聊天, 格式chat:friend:msg"},
    {"addfriend", "添加好友, 格式addfriend:friendid"},
    {"creategroup", "创建群组, 格式creategroup:groupname:groupdesc"},
    {"addgroup", "添加群组, 格式addgroup:groupid"},
    {"groupchat", "群组聊天, 格式groupchat:groupid:msg"},
    {"loginout/quit", "退出系统/注销, 格式quit"}
};



// 注册系统支持的客户端命令处理
unordered_map<string, function<void(int, string)>> commandHandlerMap = {
    {"help", help},
    {"chat",chat},  // 一对一聊天
    {"addfriend", addfriend}, // 添加朋友
    {"creategroup", creategroup}, // 创建群组
    {"addgroup", addgroup}, // 添加群组
    {"groupchat", groupchat}, // 群组聊天
    {"loginout/quit", quit}
    
};

// 主页面聊天程序 
void mainMenu(int clientfd)
{
    help();
    for(;;)
    {
        // 截取输入的 格式
        char buffer[1024] = {0}; // 用户输入的命令
        cin.getline(buffer, 1024); // 读取一行, 包括空格 cin和scanf不能读空格
        string commandbuf(buffer); // 转成string类型
        string command; // 存储实际命令
        int idx = commandbuf.find(":"); // 查找第一个:的位置
        
        if(idx == string::npos) // 没有找到  ==-1->不建议用
        {
            command = commandbuf; // 直接赋值
        }
        else
        {
            command = commandbuf.substr(0, idx); // 截取业务命令---"chat"
        }

        auto it = commandHandlerMap.find(command); // 查找命令
        if(it != commandHandlerMap.end()) // 找到命令
        {
            // 调用相应命令的事件处理函数, mainMenu 对修改封闭, 添加功能不需要修改函数
            it->second(clientfd, commandbuf.substr(idx+1, commandbuf.size()-idx-1)); // 调用对应的处理函数  出入剩下的字符串
            // 调用对应的处理函数  出入剩下的字符串
        }
        else
        {
            cout << "command invalid" << endl;
        }
    }


}


// help函数
void help(int clientfd=0, string msg="")
{
    cout << "====================command list====================" << endl;
    for (auto &command : commandList)
    {
        cout << command.first << " : " << command.second << endl;
    }
    cout << "=====================================================" << endl;
}

// addfriend函数
void addfriend(int clientfd, string msg)
{
    int friendid = atoi(msg.c_str()); // 转成整型
    json js;
    js["msgid"] = ADD_FRIEND_MSG; // 添加好友消息
    js["id"] = g_currentUser.getId(); // 当前登录用户id
    js["friendid"] = friendid;

    // 发送添加好友请求
    string request = js.dump(); // json转字符串  序列化
    int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据
    if (len < 0)
    {
        cerr << "send friendid is error  ===>  " << request << endl;
    }
}


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

    json js;
    js["msgid"] = ONE_CHAT_MSG; // 一对一聊天消息
    js["id"] = g_currentUser.getId(); // 当前登录用户id
    js["name"] = g_currentUser.getName(); // 当前登录用户姓名
    js["to"] = friendid; // 好友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 chat msg error: "<< request << endl;
    }
}

测试

自行测试

没有限制必须是 好友才能聊

错误解决

  1. commandHandlerMap 里面的string 必须 是和 commandMap 里面 给的 命令格式一致

    我的错误:

    // commandHandlerMap
    "loginout/quit"
    
    // commandMap
    给的 quit
    
  2. json 包含函数

    if(response.contains("friends"))
    
    // 错误写法如下
    if(response["friends"].contains("friends"))
    

friend表问题

这个表好像不是 联合主键, 出现了 重复, 修改成联合主键即可

mysql注入–(额外辉)

数据表问题

每个表的主键, 联合主键, 无主键, 设置一定要正确, 什么允许重复, 什么不允许重复, 要搞明白, 不然业务 会出错或者不完整

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值