截止到上节,我们已将服务器端主要代码介绍完毕,由于不可能一直手动输入信息,所以我们还需编写客户端代码,进行双向通信。
客户端不要求高并发,因此我们这里不使用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、点对点聊天业务
C++项目——集群聊天服务器项目(十)点对点聊天业务_c++ 公共集群聊天 csdn-CSDN博客
11、服务器异常退出与添加好友业务
C++项目——集群聊天服务器项目(十一)服务器异常退出与添加好友业务-CSDN博客
12、群组业务