C++项目——集群聊天服务器项目(十三)客户端登录、注册、退出业务

截止到上节,我们已将服务器端主要代码介绍完毕,由于不可能一直手动输入信息,所以我们还需编写客户端代码,进行双向通信。

客户端不要求高并发,因此我们这里不使用muduo网络库的TcpClient类编写,仅采用C++自带的Thread类实现读写多线程啦。

一、CMakeLists.txt

在src目录下的CMakeLists.txt添加客户端的子目录

add_subdirectory(server)
add_subdirectory(client)

在src/client下创建CMakeLists.txt,指定可执行文件、所用源文件、依赖的库文件

# 定义了一个SRC_LIST变量,包含了该目录下所有的源文件
aux_source_directory(. SRC_LIST)

# 指定生成可执行文件
add_executable(ChatClient ${SRC_LIST})
# 指定可执行文件链接时需要依赖的库文件
target_link_libraries(ChatClient pthread)

二、客户端主程序

首先定义全局变量、对象等,分别用于标识当前登录用户信息、好友列表、群组列表信息

// 全局变量、对象
User g_currentUser;                   // 记录当前系统登录的用户信息
vector<User> g_currentUserFriendList; // 记录当前登录用户的好友列表信息
vector<Group> g_currentUserGroupList; // 记录当前登录用户的群组列表信息

// 信号量
sem_t rwsem;                         // 用于读写线程之间的通信

/*atomic原子变量是一种多线程编程中常用的同步机制,它能够确保对共享变量的操作在执行时不会被其他线程的操作干扰
原子变量它具有类似于普通变量的操作,但是这些操作都是原子级别的,即要么全部完成,要么全部未完成。*/

atomic_bool g_isLoginSuccess{false}; // 记录登录状态   

信号量用于主线程与子线程,即读写线程间通信

bool类型的原子变量标识用户登录撞他,是多线程下常用同步机制

2.1 显示登录成功用户的基本信息

将登录成功用户的基本信息、好友信息、群组信息打印显示,以便用户可以与好友、群组聊天通信

// 显示当前登录成功用户的基本信息
void showCurrentUserData()
{
    cout << "======================login user======================" << endl;
    cout << "current login user -> id:" << g_currentUser.getId() << " name:" << g_currentUser.getName() << endl;
    cout << "----------------------friend list---------------------" << endl;
    if (!g_currentUserFriendList.empty())
    {
        for (User &user : g_currentUserFriendList)
        {
            cout << user.getId() << " " << user.getName() << " " << user.getState() << endl;
        }
    }
    cout << endl;
    cout << "----------------------group list----------------------" << endl;
    if (!g_currentUserGroupList.empty())
    {
        for (Group &group : g_currentUserGroupList)
        {
            cout << group.getId() << " " << group.getName() << " " << group.getDesc() << endl;
            for (GroupUser &user : group.getUsers())
            {
                cout << user.getId() << " " << user.getName() << " " << user.getState()
                     << " " << user.getRole() << endl;
            }
            cout << endl;
        }
    }
    cout << "======================================================" << endl;
}

2.2 获取系统时间

用户发送的消息会带有时间信息,因此我们记录发送信息的时间

// 获取系统时间(聊天信息需要添加时间信息)
string getCurrentTime()
{
    auto tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    struct tm *ptm = localtime(&tt);
    char date[60] = {0};
    sprintf(date, "%d-%02d-%02d %02d:%02d:%02d",
            (int)ptm->tm_year + 1900, (int)ptm->tm_mon + 1, (int)ptm->tm_mday,
            (int)ptm->tm_hour, (int)ptm->tm_min, (int)ptm->tm_sec);
    return std::string(date);
}

2.3 主线程main

主线程负责聊天客户端的实现,用作多线程中的发送线程,其中创建子线程用于接收服务端带来的响应

主线程流程如下:

(1)解析命令行参数获取ip和port

(2)创建客户端的socket,填写客户端需要连接的服务器的ip和port

(3)将客户端与服务器进行连接

(4)连接成功后,初始化读写线程信号量,启动接收子线程,分离子线程

(5)定义死循环函数,显示首页主菜单,即登录、注册、退出业务,并读取输入客户端输入选项

<1>登录功能

读取客户端输入id和密码,使用json序列化发送给服务器端后等待线程同步信号量,子线程完成登录业务后会通知主线程

子线程接收到服务器端响应信息后,反序列化为json对象,获取msgid,如果为登录响应信息,调用处理登录响应的业务逻辑

登录业务逻辑函数:登录成功后,从服务器端返回的响应中获取当前登录的用户信息、好友列表信息、群组列表信息,并插入全局变量中用于登录页面显示。同时将用户个人聊天、群组聊天离线信息进行显示。登录业务逻辑执行完成后,记录当前登录状态为true。

登录业务逻辑函数如下:

// 处理登录的响应逻辑
void doLoginResponse(json &responsejs)
{
    if (0 != responsejs["errno"].get<int>()) // 登录失败
    {
        cerr << responsejs["errmsg"] << endl;
        g_isLoginSuccess = false;
    }
    else // 登录成功
    {
        // 记录当前用户的id和name
        g_currentUser.setId(responsejs["id"].get<int>());
        g_currentUser.setName(responsejs["name"]);

        // 记录当前用户的好友列表信息
        if (responsejs.contains("friends"))
        {
            // 初始化
            g_currentUserFriendList.clear();

            vector<string> vec = responsejs["friends"];
            for (string &str : vec)
            {
                json js = json::parse(str); // 反序列化
                User user;
                user.setId(js["id"].get<int>());
                user.setName(js["name"]);
                user.setState(js["state"]);
                g_currentUserFriendList.push_back(user);
            }
        }

        // 记录当前用户的群组列表信息
        if (responsejs.contains("groups"))
        {
            // 初始化
            g_currentUserGroupList.clear();

            vector<string> vec1 = responsejs["groups"];
            for (string &groupstr : vec1)
            {
                json grpjs = json::parse(groupstr); // 反序列化
                Group group;
                group.setId(grpjs["id"].get<int>());
                group.setName(grpjs["groupname"]);
                group.setDesc(grpjs["groupdesc"]);

                vector<string> vec2 = grpjs["users"]; // 群组内成员信息
                for (string &userstr : vec2)
                {
                    GroupUser user;
                    json js = json::parse(userstr);
                    user.setId(js["id"].get<int>());
                    user.setName(js["name"]);
                    user.setState(js["state"]);
                    user.setRole(js["role"]);
                    group.getUsers().push_back(user);
                }

                g_currentUserGroupList.push_back(group);
            }
        }

        // 显示登录用户的基本信息
        showCurrentUserData();

        // 显示当前用户的离线消息  个人聊天信息或者群组消息
        if (responsejs.contains("offlinemsg"))
        {
            vector<string> vec = responsejs["offlinemsg"];
            for (string &str : vec)
            {
                json js = json::parse(str);                 // 反序列化
                if (ONE_CHAT_MSG == js["msgid"].get<int>()) // 点对点聊天
                {
                    cout << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
                         << " said: " << js["msg"].get<string>() << endl;
                }
                else // 群聊
                {
                    cout << "群消息[" << js["groupid"] << "]:" << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
                         << " said: " << js["msg"].get<string>() << endl;
                }
            }
        }

        g_isLoginSuccess = true;
    }
}

<2>注册功能

读取客户端输入姓名和密码,使用json序列化发送给服务器端后等待线程同步信号量,子线程完成注册业务后会通知主线程

子线程接收到服务器端响应信息后,反序列化为json对象,获取msgid,如果为注册响应信息,调用处理注册响应的业务逻辑

注册业务逻辑函数:依据服务器返回的errno字段判断是否注册成功,进行相应显示

// 处理注册的响应逻辑
void doRegResponse(json &responsejs)
{
    if (0 != responsejs["errno"].get<int>()) // 注册失败
    {
        cerr << "name is already exist, register error!" << endl;
    }
    else // 注册成功
    {
        cout << "name register success, userid is " << responsejs["id"]
             << ", do not forget it!" << endl;
    }
}

<3>退出功能

关闭客户端连接套接字、销毁信号量、关闭程序即可

main函数如下:

// 聊天客户端程序实现,main线程用作发送线程,子线程用作接收线程
int main(int argc, char **argv) // argc 参数个数    argv 参数序列或指针
{
    if (argc < 3)
    {
        cerr << "command invalid! example: ./ChatClient 127.0.0.1 6000" << endl; // cerr:程序错误信息
        exit(-1);
    }

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

    // 创建client端的socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET:IPv4  SOCK_STREAM:可靠的、双向的通信数据流 0:调用者不指定协议
    if (-1 == clientfd)
    {
        cerr << "socket create error" << endl;
        exit(-1);
    }

    // 填写client需要连接的server信息ip+port
    sockaddr_in server;
    memset(&server, 0, sizeof(sockaddr_in)); // memset清0

    server.sin_family = AF_INET;            // IPv4
    server.sin_port = htons(port);          // 端口
    server.sin_addr.s_addr = inet_addr(ip); // ip地址

    // client和server进行连接
    if (-1 == connect(clientfd, (sockaddr *)&server, sizeof(sockaddr_in)))
    {
        cerr << "connect server error" << endl;
        close(clientfd);
        exit(-1);
    }

    // 初始化读写线程通信用的信号量
    sem_init(&rwsem, 0, 0);

    // 连接服务器成功,启动接收子线程
    std::thread readTask(readTaskHandler, clientfd); // pthread_create
    readTask.detach();                               // pthread_detach

    // main线程用于接收用户输入,负责发送数据
    for (;;)
    {
        // 显示首页面菜单 登录、注册、退出
        cout << "========================" << endl;
        cout << "======  1. login  ======" << endl;
        cout << "======  2. register  ===" << endl;
        cout << "======  3. quit  =======" << endl;
        cout << "========================" << endl;
        cout << "Please input your choice:";
        int choice = 0;
        cin >> choice; // 读取功能选项
        cin.get();     // 读掉缓冲区残留的回车

        switch (choice)
        {
        case 1: // 登录业务
        {
            int id = 0;
            char pwd[50] = {0};
            cout << "user id:";
            cin >> id;
            cin.get(); // 读掉缓冲区残留的回车
            cout << "user password:";
            cin.getline(pwd, 50);

            json js;
            js["msgid"] = LOGIN_MSG;
            js["id"] = id;
            js["password"] = pwd;
            string request = js.dump();

            g_isLoginSuccess = false;

            int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 通过网络send给服务器端
            if (len == -1)
            {
                cerr << "send login msg error:" << request << endl;
            }

            sem_wait(&rwsem); // 等待信号量,由子线程处理完登录的响应消息后,通知这里

            if (g_isLoginSuccess)
            {
                // 进入聊天主菜单页面
                isMainMenuRunning = true;
                // mainMenu(clientfd);
            }
        }
        break;
        case 2: // 注册业务
        {
            char name[50] = {0};
            char pwd[50] = {0};
            cout << "user name:";
            cin.getline(name, 50);
            cout << "user password:";
            cin.getline(pwd, 50);

            json js;
            js["msgid"] = REG_MSG;
            js["name"] = name;
            js["password"] = pwd;
            string request = js.dump();

            int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 通过网络send给服务器端
            if (len == -1)
            {
                cerr << "send reg msg error:" << request << endl;
            }

            sem_wait(&rwsem); // 等待信号量,子线程处理完注册消息会通知
        }
        break;
        case 3: // 退出业务
            close(clientfd);
            sem_destroy(&rwsem);
            exit(0);
        default:
            cerr << "invalid input!" << endl;
            break;
        }
    }

    return 0;
}

2.4 接收子线程

// 子线程 - 接收线程
void readTaskHandler(int clientfd)
{
    for (;;)
    {
        char buffer[1024] = {0};
        int len = recv(clientfd, buffer, 1024, 0); // 阻塞
        if (-1 == len || 0 == len)
        {
            close(clientfd);
            exit(-1);
        }

        // 接收服务器转发的数据,反序列化生成json数据对象
        json js = json::parse(buffer);
        int msgtype = js["msgid"].get<int>(); // 获取处理业务msgid
        if (ONE_CHAT_MSG == msgtype)          // 点对点聊天
        {
            cout << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
                 << " said: " << js["msg"].get<string>() << endl;
            continue;
        }

        if (GROUP_CHAT_MSG == msgtype) // 群聊
        {
            cout << "群消息[" << js["groupid"] << "]:" << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
                 << " said: " << js["msg"].get<string>() << endl;
            continue;
        }

        if (LOGIN_MSG_ACK == msgtype) // 登录业务
        {
            doLoginResponse(js); // 处理登录响应的业务逻辑
            sem_post(&rwsem);    // 通知主线程,登录结果处理完成
            continue;
        }

        if (REG_MSG_ACK == msgtype) // 注册业务
        {
            doRegResponse(js); // 处理注册响应的业务逻辑
            sem_post(&rwsem);  // 通知主线程,注册结果处理完成
            continue;
        }
    }
}

2.5 整体代码

// 自定义头文件
#include "json.hpp"
#include "group.hpp"
#include "user.hpp"
#include "public.hpp"
using json = nlohmann::json;

// 标准库头文件
#include <iostream>
#include <thread>
#include <string>
#include <vector>
#include <chrono>
#include <ctime>
#include <unordered_map>
#include <functional>
using namespace std;

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

// 全局变量、对象、函数
User g_currentUser;                   // 记录当前系统登录的用户信息
vector<User> g_currentUserFriendList; // 记录当前登录用户的好友列表信息
vector<Group> g_currentUserGroupList; // 记录当前登录用户的群组列表信息
bool isMainMenuRunning = false;       // 控制主菜单页面程序

// 信号量
sem_t rwsem;                         // 用于读写线程之间的通信

/*atomic原子变量是一种多线程编程中常用的同步机制,它能够确保对共享变量的操作在执行时不会被其他线程的操作干扰
原子变量它具有类似于普通变量的操作,但是这些操作都是原子级别的,即要么全部完成,要么全部未完成。*/

atomic_bool g_isLoginSuccess{false}; // 记录登录状态            

// 获取系统时间(聊天信息需要添加时间信息)
string getCurrentTime();

// 显示当前登录成功用户的基本信息
void showCurrentUserData();

// 主聊天页面程序
// void mainMenu(int);

// 接收线程
void readTaskHandler(int clientfd);

// 聊天客户端程序实现,main线程用作发送线程,子线程用作接收线程
int main(int argc, char **argv) // argc 参数个数    argv 参数序列或指针
{
    if (argc < 3)
    {
        cerr << "command invalid! example: ./ChatClient 127.0.0.1 6000" << endl; // cerr:程序错误信息
        exit(-1);
    }

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

    // 创建client端的socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET:IPv4  SOCK_STREAM:可靠的、双向的通信数据流 0:调用者不指定协议
    if (-1 == clientfd)
    {
        cerr << "socket create error" << endl;
        exit(-1);
    }

    // 填写client需要连接的server信息ip+port
    sockaddr_in server;
    memset(&server, 0, sizeof(sockaddr_in)); // memset清0

    server.sin_family = AF_INET;            // IPv4
    server.sin_port = htons(port);          // 端口
    server.sin_addr.s_addr = inet_addr(ip); // ip地址

    // client和server进行连接
    if (-1 == connect(clientfd, (sockaddr *)&server, sizeof(sockaddr_in)))
    {
        cerr << "connect server error" << endl;
        close(clientfd);
        exit(-1);
    }

    // 初始化读写线程通信用的信号量
    sem_init(&rwsem, 0, 0);

    // 连接服务器成功,启动接收子线程
    std::thread readTask(readTaskHandler, clientfd); // pthread_create
    readTask.detach();                               // pthread_detach

    // main线程用于接收用户输入,负责发送数据
    for (;;)
    {
        // 显示首页面菜单 登录、注册、退出
        cout << "========================" << endl;
        cout << "======  1. login  ======" << endl;
        cout << "======  2. register  ===" << endl;
        cout << "======  3. quit  =======" << endl;
        cout << "========================" << endl;
        cout << "Please input your choice:";
        int choice = 0;
        cin >> choice; // 读取功能选项
        cin.get();     // 读掉缓冲区残留的回车

        switch (choice)
        {
        case 1: // 登录业务
        {
            int id = 0;
            char pwd[50] = {0};
            cout << "user id:";
            cin >> id;
            cin.get(); // 读掉缓冲区残留的回车
            cout << "user password:";
            cin.getline(pwd, 50);

            json js;
            js["msgid"] = LOGIN_MSG;
            js["id"] = id;
            js["password"] = pwd;
            string request = js.dump();

            g_isLoginSuccess = false;

            int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 通过网络send给服务器端
            if (len == -1)
            {
                cerr << "send login msg error:" << request << endl;
            }

            sem_wait(&rwsem); // 等待信号量,由子线程处理完登录的响应消息后,通知这里

            if (g_isLoginSuccess)
            {
                // 进入聊天主菜单页面
                isMainMenuRunning = true;
                // mainMenu(clientfd);
            }
        }
        break;
        case 2: // 注册业务
        {
            char name[50] = {0};
            char pwd[50] = {0};
            cout << "user name:";
            cin.getline(name, 50);
            cout << "user password:";
            cin.getline(pwd, 50);

            json js;
            js["msgid"] = REG_MSG;
            js["name"] = name;
            js["password"] = pwd;
            string request = js.dump();

            int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 通过网络send给服务器端
            if (len == -1)
            {
                cerr << "send reg msg error:" << request << endl;
            }

            sem_wait(&rwsem); // 等待信号量,子线程处理完注册消息会通知
        }
        break;
        case 3: // 退出业务
            close(clientfd);
            sem_destroy(&rwsem);
            exit(0);
        default:
            cerr << "invalid input!" << endl;
            break;
        }
    }

    return 0;
}

// 显示当前登录成功用户的基本信息
void showCurrentUserData()
{
    cout << "======================login user======================" << endl;
    cout << "current login user -> id:" << g_currentUser.getId() << " name:" << g_currentUser.getName() << endl;
    cout << "----------------------friend list---------------------" << endl;
    if (!g_currentUserFriendList.empty())
    {
        for (User &user : g_currentUserFriendList)
        {
            cout << user.getId() << " " << user.getName() << " " << user.getState() << endl;
        }
    }
    cout << endl;
    cout << "----------------------group list----------------------" << endl;
    if (!g_currentUserGroupList.empty())
    {
        for (Group &group : g_currentUserGroupList)
        {
            cout << group.getId() << " " << group.getName() << " " << group.getDesc() << endl;
            for (GroupUser &user : group.getUsers())
            {
                cout << user.getId() << " " << user.getName() << " " << user.getState()
                     << " " << user.getRole() << endl;
            }
            cout << endl;
        }
    }
    cout << "======================================================" << endl;
}

// 获取系统时间(聊天信息需要添加时间信息)
string getCurrentTime()
{
    auto tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    struct tm *ptm = localtime(&tt);
    char date[60] = {0};
    sprintf(date, "%d-%02d-%02d %02d:%02d:%02d",
            (int)ptm->tm_year + 1900, (int)ptm->tm_mon + 1, (int)ptm->tm_mday,
            (int)ptm->tm_hour, (int)ptm->tm_min, (int)ptm->tm_sec);
    return std::string(date);
}

// 处理注册的响应逻辑
void doRegResponse(json &responsejs)
{
    if (0 != responsejs["errno"].get<int>()) // 注册失败
    {
        cerr << "name is already exist, register error!" << endl;
    }
    else // 注册成功
    {
        cout << "name register success, userid is " << responsejs["id"]
             << ", do not forget it!" << endl;
    }
}

// 处理登录的响应逻辑
void doLoginResponse(json &responsejs)
{
    if (0 != responsejs["errno"].get<int>()) // 登录失败
    {
        cerr << responsejs["errmsg"] << endl;
        g_isLoginSuccess = false;
    }
    else // 登录成功
    {
        // 记录当前用户的id和name
        g_currentUser.setId(responsejs["id"].get<int>());
        g_currentUser.setName(responsejs["name"]);

        // 记录当前用户的好友列表信息
        if (responsejs.contains("friends"))
        {
            // 初始化
            g_currentUserFriendList.clear();

            vector<string> vec = responsejs["friends"];
            for (string &str : vec)
            {
                json js = json::parse(str); // 反序列化
                User user;
                user.setId(js["id"].get<int>());
                user.setName(js["name"]);
                user.setState(js["state"]);
                g_currentUserFriendList.push_back(user);
            }
        }

        // 记录当前用户的群组列表信息
        if (responsejs.contains("groups"))
        {
            // 初始化
            g_currentUserGroupList.clear();

            vector<string> vec1 = responsejs["groups"];
            for (string &groupstr : vec1)
            {
                json grpjs = json::parse(groupstr); // 反序列化
                Group group;
                group.setId(grpjs["id"].get<int>());
                group.setName(grpjs["groupname"]);
                group.setDesc(grpjs["groupdesc"]);

                vector<string> vec2 = grpjs["users"]; // 群组内成员信息
                for (string &userstr : vec2)
                {
                    GroupUser user;
                    json js = json::parse(userstr);
                    user.setId(js["id"].get<int>());
                    user.setName(js["name"]);
                    user.setState(js["state"]);
                    user.setRole(js["role"]);
                    group.getUsers().push_back(user);
                }

                g_currentUserGroupList.push_back(group);
            }
        }

        // 显示登录用户的基本信息
        showCurrentUserData();

        // 显示当前用户的离线消息  个人聊天信息或者群组消息
        if (responsejs.contains("offlinemsg"))
        {
            vector<string> vec = responsejs["offlinemsg"];
            for (string &str : vec)
            {
                json js = json::parse(str);                 // 反序列化
                if (ONE_CHAT_MSG == js["msgid"].get<int>()) // 点对点聊天
                {
                    cout << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
                         << " said: " << js["msg"].get<string>() << endl;
                }
                else // 群聊
                {
                    cout << "群消息[" << js["groupid"] << "]:" << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
                         << " said: " << js["msg"].get<string>() << endl;
                }
            }
        }

        g_isLoginSuccess = true;
    }
}

// 子线程 - 接收线程
void readTaskHandler(int clientfd)
{
    for (;;)
    {
        char buffer[1024] = {0};
        int len = recv(clientfd, buffer, 1024, 0); // 阻塞
        if (-1 == len || 0 == len)
        {
            close(clientfd);
            exit(-1);
        }

        // 接收服务器转发的数据,反序列化生成json数据对象
        json js = json::parse(buffer);
        int msgtype = js["msgid"].get<int>(); // 获取处理业务msgid
        if (ONE_CHAT_MSG == msgtype)          // 点对点聊天
        {
            cout << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
                 << " said: " << js["msg"].get<string>() << endl;
            continue;
        }

        if (GROUP_CHAT_MSG == msgtype) // 群聊
        {
            cout << "群消息[" << js["groupid"] << "]:" << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
                 << " said: " << js["msg"].get<string>() << endl;
            continue;
        }

        if (LOGIN_MSG_ACK == msgtype) // 登录业务
        {
            doLoginResponse(js); // 处理登录响应的业务逻辑
            sem_post(&rwsem);    // 通知主线程,登录结果处理完成
            continue;
        }

        if (REG_MSG_ACK == msgtype) // 注册业务
        {
            doRegResponse(js); // 处理注册响应的业务逻辑
            sem_post(&rwsem);  // 通知主线程,注册结果处理完成
            continue;
        }
    }
}

三、客户端功能实现

使用CMake编译通过后,执行ChatServer和ChatClient程序

3.1 验证登录功能

我们使用张三的账号登录,即id=13,password = 123456

登录成功,回显张三信息、好友信息、群组信息,由于张三没有离线信息,此处没有显示

查看底层数据库,判断客户端是否显示正确

登录功能验证成功

3.2 验证注册功能

创建新用户Rabbit,密码666666,服务端返回id号为24

数据库插入成功

我们登录一下

注册、登录成功

3.3 验证退出业务

功能验证成功!

客户端主菜单的登录、注册、退出业务编码完成

感兴趣的小伙伴一起来试一下吧~

如果有问题还请及时联系我哦,感谢~

四、项目流程

 1、项目环境搭建 

C++项目——集群聊天服务器项目(一)项目介绍、环境搭建、Boost库安装、Muduo库安装、Linux与vscode配置_c++集群聊天服务器-CSDN博客

2、Json第三方库介绍

C++项目——集群聊天服务器项目(二)Json第三方库-CSDN博客

3、muduo网络库介绍

C++项目——集群聊天服务器项目(三)muduo网络库-CSDN博客

4、MySQL数据库创建

C++项目——集群聊天服务器项目(四)MySQL数据库-CSDN博客

5、网络模块与业务模块代码编写

C++项目——集群聊天服务器项目(五)网络模块与业务模块-CSDN博客

6、MySQL模块编写

C++项目——集群聊天服务器项目(六)MySQL模块-CSDN博客

7、Model层设计、注册业务实现

C++项目——集群聊天服务器项目(七)Model层设计、注册业务实现-CSDN博客

8、用户登录业务

C++项目——集群聊天服务器项目(八)用户登录业务-CSDN博客

9、客户端异常退出业务

C++项目——集群聊天服务器项目(九)客户端异常退出业务-CSDN博客

10、点对点聊天业务

11、服务器异常退出与添加好友业务

12、群组业务

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/513143.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ComplexHeatmap绘图:注释、图例、热图基础(自备)

目录 基础介绍 Heatmap绘图基础参数 数据 作图参数 Heatmap Annotations&#xff08;注释&#xff09; 基础注释设置 简单注释测试 anno_points散点注释 anno_lines连线注释 anno_barplot条形图 anno_boxplot箱线图 anno_histogram直方图 热图组合 基础组合 进行…

使用idea一次性删除java文件中所有的注释内容 /* */

将.class文件转成.java文件后&#xff0c;.java文件每行都会生成注释/* */&#xff0c;下面是通过idea的替换功能&#xff0c;使用正则表达式删除注释/* */。 我使用MacBook&#xff0c;commandR打开替换查找替换界面&#xff0c;第一步选中 .* &#xff0c;第二步在…

虚拟机与开发板之间互传文件、文件夹

1.配置桥接模式实现外网访问 1.1设置 VMnet0 要桥接的网卡 打开【编辑】-【虚拟网络编辑器】 选择【更改设置】 选择【VMnet0】&#xff0c;选择桥接到宿主机上的哪个网卡。 通过打开安装虚拟机的宿主机的【网络适配器】&#xff0c;可以查看网卡名称。 1.2虚拟机配置桥接模式…

Idea2023创建Servlet项目

① Java EE 只是一个抽象的规范&#xff0c;具体实现称为应用服务器。 ② Java EE 只需要两个包 jsp-api.jar 和 servlet-api.jar&#xff0c;而这两个包是没有官方版本的。也就是说&#xff0c;Java 没有提供这两个包&#xff0c;只提供了一个规范。那么这两个包是谁提供的…

Java23种常见设计模式汇总

七大原则网站地址&#xff1a;设计模式7大原则&#xff0b;类图关系-CSDN博客 创建型设计模式&#xff1a;创建型设计模式合集-CSDN博客 七大结构型设计模式&#xff1a;7大结构型设计模式-CSDN博客 11种行为型设计模式&#xff1a; 11种行为型模式&#xff08;上&#xff0…

Xmind安装在指定目录

场景&#xff1a; Xmind安装默认是安装C盘。 问题描述 一般用户都习惯将软件安装在其他盘&#xff0c;但是Xmind不支持安装的时候指定磁盘或目录。 解决方案&#xff1a; 1、在D盘创建一个文件夹&#xff0c;用于安装Xmind&#xff0c;比如创建一个D:\Program Files (x86)…

MyBatis动态SQL--if 标签

mybatis动态sql对我们来说是非常常见的&#xff0c;比如在下面这样一个场景中&#xff0c; 我们需要多条件查询&#xff0c;但是查询的条件又不是固定的&#xff0c;是可以动态改变的&#xff0c;那我们就需要用到动态sql去完成。 动态SQL之 if 标签 接下来我们介绍第一个动态…

【Java代码审计】S2-045 远程代码执行漏洞分析复现

【Java代码审计】S2-045 远程代码执行漏洞分析复现 1.漏洞原理2.靶场复现 1.漏洞原理 官方对该漏洞的描述是这样的&#xff1a;Struts2 默认处理 multipart 上传报文的解析器为 Jakarta 插件&#xff08;org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest类&a…

基于opencv的SVM算法的车牌识别系统设计与实现

基于opencv的SVM算法的车牌识别系统设计与实现 车牌识别技术是智能交通系统中的一项关键技术&#xff0c;它能够自动识别车辆的车牌号码。本文将详细介绍如何使用Python编程语言结合OpenCV库和SVM算法来实现车牌识别系统。 系统架构 车牌识别系统主要包括以下几个模块&…

外贸建站:WordPress搭建外贸独立站零基础自建站完整教程(2024)

对于做外贸来说&#xff0c;拥有自己的外贸独立网站真的非常重要。在外贸领域&#xff0c;如今各平台竞争激烈&#xff0c;规则多&#xff0c;成本高&#xff0c;价格战、政策变化快&#xff0c;还存在封店风险等等因素。在这种情况下&#xff0c;拥有外贸独立站就能很好规避上…

JavaSE:抽象类和接口

目录 一、前言 二、抽象类 &#xff08;一&#xff09;抽象类概念 &#xff08;二&#xff09;使用抽象类的注意事项 &#xff08;三&#xff09;抽象类的作用 三、接口 &#xff08;一&#xff09;接口概念 &#xff08;二&#xff09;接口语法规则 &#xff08;三&a…

八口快速以太网交换机芯片方案分享/JL5110

以太网交换机(switch)是一种网络设备&#xff0c;用于在局域网中连接多个计算机和其他网络设备。它可以实现多个端口之间的同时传输&#xff0c;并根据MAC地址进行帧过滤和转发。交换机通过自学习的方式&#xff0c;将MAC地址与相应的接口关联起来&#xff0c;以便将数据帧准确…

Termius for Mac v8.4.0激活版下载

Termius for Mac是一款功能强大的多协议远程管理软件&#xff0c;专为开发人员、系统管理员和网络专业人士设计。它支持多种远程连接协议&#xff0c;如SSH、Telnet、RDP、VNC和RFB等&#xff0c;使得用户可以轻松连接到不同类型的远程服务器和设备。 软件下载&#xff1a;Term…

湖北兴发MES和用友NCC单据接口对接

湖北兴发MES和用友NCC单据接口对接 对接系统用友NCC 用友NCCloud&#xff0c;为客户提供面向大型企业集团、制造业、消费品、建筑、房地产、金融保险等14个行业大类&#xff0c;68个细分行业&#xff0c;涵盖数字营销、智能制造、财务共享、数字采购等18大解决方案&#xff0c;…

为什么要用能量回馈式负载

能量回馈式负载是一种能够将电能转化为其他形式能量的装置&#xff0c;同时还能将其他形式的能量转化为电能。这种负载在许多应用中具有重要的意义&#xff0c;因为它可以提高能源利用效率&#xff0c;减少能源浪费&#xff0c;降低环境污染&#xff0c;提高系统的稳定性和可靠…

Python3.10 - 列表的常用方法

01 列表的切片 lst [a, b, 1, 2, [c1, 1]]# 01 列表的切片 lst1 lst[0:2] # 从0开始切到1(顾头不顾尾), 切得长度超过1时, 切出来为list类型 print(lst1, type(lst1))lst2 lst[0] # 从中切某个元素时, 切出来的类型即元素本身, 以下同理 print(lst2, type(lst2))lst3 ls…

“由于找不到opencv_world3413.dll,无法继续执行代码”的解决方法

问题 在Windows系统中&#xff0c;编译完涉及到opencv的项目后&#xff0c;提示&#xff0c; 由于找不到opencv_world3413.dll&#xff0c;无法继续执行代码 解决方法 在编译好的opencv的bin文件内&#xff08;如&#xff1a;D:\code\vs2017\opencv\build\x64\vc15\bin&…

Spring Boot 学习(2)——HelloWorld

HelloWorld&#xff01;全宇宙码农的第一个&#xff08;行&#xff09;程序&#xff08;代码&#xff09;。 1、创建项目 打开idea&#xff0c;新建一个maven项目。 1&#xff09;选择项目sdk&#xff08;本例是1.8&#xff09; 2&#xff09;输入GroupId&#xff08;co…

FME学习之旅---day20

我们付出一些成本&#xff0c;时间的或者其他&#xff0c;最终总能收获一些什么。 教程&#xff1a;AutoCAD 入门 FME使用四种主要格式来读取和写入AutoCAD图形文件;初级教程重点介绍AutoDesk AutoCAD DWG\DXF(ACAD) AutoCAD中常用的术语 实体&#xff1a;AutoCAD 图元表示 D…

超强命令行解析工具Apache Commons CLI

概述 为什么要写这篇文章呢?因为在读flink cdc3.0源码的时候发现了这个工具包,感觉很牛,之前写过shell命令,shell是用getopts来处理命令行参数的,但是其实写起来很麻烦,长时间不写已经完全忘记了,现在才发现原来java也有这种工具类,所以先学习一下这个的使用,也许之后自己在写…