文章目录
工作模型和工作流程
服务器端工作流程

客户端工作流程

Code实现
服务器端实现
cpp
📂 SERVER
├── 📂 build/ # 编译输出目录(存放中间文件和可执行程序)
├── 📂 include/ # 头文件目录 (.hpp)
│ ├── 📄 DataManager.hpp
│ └── 📄 server.hpp
├── 📂 src/ # 源文件目录 (.cpp)
│ ├── 📄 DataManager.cpp
│ ├── 📄 main.cpp
│ └── 📄 server.cpp
├── 📜 CMakeLists.txt # CMake 构建配置文件
└── 📑 Msg.proto # Protobuf 协议定义文件
cpp
#pragma once
#include<string>
#include<memory>
#include<mysqlx/xdevapi.h>
#include<mutex>
class DataManager
{
private:
// 使用指针避开"无默认构造函数"的问题
std::unique_ptr<mysqlx::Session> sess_;
std::unique_ptr<mysqlx::Schema> user_schema_;
std::unique_ptr<mysqlx::Schema> dict_schema_;
// 细粒度锁:保护非线程安全的数据库对象
std::mutex user_mtx_;
std::mutex dict_mtx_;
// 初始化数据表
void initalizeDatabase();
void initalizeUsrDB();
void initalizeDictDB();
void importDict(const std::string& filePath,const std::string& tableName);
public:
DataManager(const std::string& user, const std::string& pwd,const std::string& host="localhost", int port=33060);
~DataManager();
// 用户相关的操作
bool registerUser(const std::string &name, const std::string &password); // 用户注册操作
bool loginUser(const std::string &name, const std::string &password, bool &is_online); // 用户登录操作
bool logoutUser(const std::string &name); // 用户退出操作
// 单词表的操作
bool querryWord(const std::string &word, std::string &meaning); // 查询单词
bool recordHistory(const std::string &name, const std::string &word, const std::string &meaning, const std::string &time); // 历史记录
bool getHistory(const std::string name, std::string &history);
};
cpp
#pragma once
#include"DataManager.hpp"
#include<map>
#include"arpa/inet.h"
#include<sys/socket.h>
#include<thread>
#include<mutex>
#include<queue>
#include<condition_variable>
#include<functional>
#include"Msg.pb.h"
#include<string>
class server
{
private:
const int BUF_SIZE=128;
//数据库管理类
DataManager manager;
//套接字文件
int sockfd;
//存储线程的线程容器
std::vector<std::thread>workers;
//存储任务的工作队列
std::queue<std::function<void()>>tasks;
//互斥锁:解决线程池的互斥问题
std::mutex task_mutex;
//条件变量,用于解决同步问题
std::condition_variable task_cv;
//线程池工作状态的标志
bool isRun;
private:
//启动线程池
void startThreadPool(const int ThreadSize);
//@task:指定的任务
//添加任务到线程池的工作队列
void addTask(std::function<void()>task);
//@client_fd:客户端的文件描述符
//@cin:客户端的信息结构体
// 处理客户端连接的函数
void handleClient(int client_fd, struct sockaddr_in cin);
public:
//@ip:服务器端绑定的ip
//@port:服务器端绑定的端口号
//@ThreadSize:服务器端线程池初始化的工作线程数量
server(const std::string&ip,const int port,const std::string&user,const std::string&password,const int ThreadSize=4);
//析构函数
~server();
//服务器端主线程主流程
void run();
};
cpp
#include"DataManager.hpp"
#include<iostream>
#include<fstream>
#include <glog/logging.h>
DataManager::DataManager(const std::string& user, const std::string& pwd,const std::string& host, int port){
// 1. 创建 Session
sess_ = std::make_unique<mysqlx::Session>(host, port, user, pwd);
//2.如果数据库不存在,创建数据库
try {
// 执行创建数据库的 SQL
sess_->sql("CREATE DATABASE IF NOT EXISTS user_db CHARACTER SET utf8").execute();
sess_->sql("create database if not exists dict_db character set utf8").execute();
std::cout << "数据库 user_db 检查/创建完成。" << std::endl;
} catch (const mysqlx::Error &err) {
std::cerr << "创建数据库失败: " << err.what() << std::endl;
}
// 3. 获取不同的数据库 Schema
user_schema_ = std::make_unique<mysqlx::Schema>(sess_->getSchema("user_db"));
dict_schema_ = std::make_unique<mysqlx::Schema>(sess_->getSchema("dict_db"));
//4.初始化数据库
initalizeDatabase();
}
DataManager::~DataManager(){
}
// 初始化数据表
void DataManager::initalizeDatabase(){
initalizeUsrDB();
initalizeDictDB();
}
void DataManager::initalizeUsrDB(){
try {
// 1. 创建用户表 (usr)
// 注意:MySQL中 PRIMARY KEY 建议使用 VARCHAR 并指定长度,TEXT 做主键限制较多
sess_->sql("CREATE TABLE IF NOT EXISTS user_db.usr ("
"name VARCHAR(64) PRIMARY KEY, "
"passwd VARCHAR(128), "
"stage INT DEFAULT 0" // 0表示离线
")").execute();
// 2. 创建历史记录表 (history)
sess_->sql("CREATE TABLE IF NOT EXISTS user_db.history ("
"name VARCHAR(64), "
"word VARCHAR(64), "
"mean TEXT, "
"time VARCHAR(32)"
")").execute();
std::cout << "表 usr 和 history 检查/创建成功" << std::endl;
} catch (const mysqlx::Error &err) {
LOG(ERROR)<<"建表错误:"<<err.what();
throw; // 向上抛出,让服务器知道数据库初始化失败
}
}
void DataManager::initalizeDictDB() {
try {
std::unique_lock<std::mutex>lock(dict_mtx_);
// 1. 建表
sess_->sql("CREATE TABLE IF NOT EXISTS dict_db.dict ("
"word VARCHAR(64) PRIMARY KEY, "
"mean TEXT"
")").execute();
mysqlx::Table dict=dict_schema_->getTable("dict");
if(dict.count()==0){
importDict("./dict.txt","dict");
}
} catch (const mysqlx::Error &e) {
LOG(ERROR)<<"initalize DictDB 失败: " << e.what() ;
throw;
}
}
void DataManager::importDict(const std::string& filePath,const std::string& tableName) {
std::ifstream file(filePath);
if (!file.is_open()) {
std::cerr << "无法打开词典文件: " << filePath << std::endl;
return;
}
std::cout<<"打开文件成功"<<std::endl;
mysqlx::Table dict = dict_schema_->getTable(tableName);
std::string line;
int count = 0;
try {
// 1. 开启事务
sess_->startTransaction();
std::cout << "开始导入数据并开启事务..." << std::endl;
// 建议使用批量插入对象来进一步提升速度
auto inserter = dict.insert("word", "mean");
while (std::getline(file, line)) {
if (line.empty()) continue;
auto pos = line.find(' ');
if (pos == std::string::npos) continue;
std::string word = line.substr(0, pos);
std::string mean = line.substr(pos + 1);
// 清洗 mean 开头的空格
size_t first = mean.find_first_not_of(' ');
if (first != std::string::npos) mean = mean.substr(first);
if (word.empty() || mean.empty()) continue;
// 将数据放入缓存队列
inserter.values(word, mean);
count++;
// 每 2000 条执行一次 execute,但此时还没 commit 到磁盘
if (count % 2000 == 0) {
inserter.execute();
inserter = dict.insert("word", "mean"); // 重置插入对象
}
}
// 提交剩余的数据到服务器缓冲区
if (count % 2000 != 0) {
inserter.execute();
}
// 2. 提交事务(真正落盘)
sess_->commit();
std::cout << "导入完成,共计: " << count << " 条记录已成功提交。" << std::endl;
} catch (const mysqlx::Error &err) {
// 3. 发生错误时回滚事务,保证数据库不被污染
sess_->rollback();
LOG(ERROR)<<"数据导入出错,已执行回滚。错误信息: " << err.what();
return;
}
}
// 用户相关的操作
bool DataManager::registerUser(const std::string &name, const std::string &password){
//将用户插入用户表中
try{
//保护数据库
std::unique_lock<std::mutex>lock(user_mtx_);
//获取用户表
mysqlx::Table usr=user_schema_->getTable("usr");
//查询是否查询表中是否有对应用户,如果有,返回true,没有则插入表中
mysqlx::RowResult res=usr.select("name","passwd").
where("name=:n and passwd=:p").bind("n",name).bind("p",password).execute();
if(res.count()>0){
return false;
}
else{
mysqlx::Result result=usr.insert("name","passwd").values(name,password).execute();
if(result.getAffectedItemsCount()>0)return true;
else return true;
}
}catch(const mysqlx::Error&e){
LOG(ERROR)<<name<<":"<<password<<" "<<"register error:"<<e.what();
throw;
return false;
}
}
bool DataManager::loginUser(const std::string &name, const std::string &password, bool &is_online){
//更新用户状态
try{
//保护数据库
std::unique_lock<std::mutex>lock(user_mtx_);
//获取用户表
mysqlx::Table usr=user_schema_->getTable("usr");
//查询表中是否有对应用户,如果有,更新状态返回true,如果没有则返回false
mysqlx::RowResult res=usr.select("stage").
where("name=:n and passwd=:p").bind("n",name).bind("p",password).execute();
if(res.count()==1){
mysqlx::Row row=res.fetchOne();
is_online=(int)row[0];
//更新状态
mysqlx::Result result=usr.update().set("stage",1).where("name=:n and passwd=:p").bind("n",name).bind("p",password).execute();
if(result.getAffectedItemsCount()>0)return true;
else return false;
}
else return false;
}catch(const mysqlx::Error&e){
LOG(ERROR)<<name<<":"<<password<<" "<<"login error:"<<e.what();
throw;
return false;
}
}
bool DataManager::logoutUser(const std::string &name){
//更新用户状态
try{
//保护数据库
std::unique_lock<std::mutex>lock(user_mtx_);
//获取用户表
mysqlx::Table usr=user_schema_->getTable("usr");
//查询表中是否有对应用户,如果有,更新状态返回true,如果没有则返回false
mysqlx::RowResult res=usr.select("name").
where("name=:n").bind("n",name).execute();
if(res.count()==1){
//更新状态
mysqlx::Result result=usr.update().set("stage",0).where("name=:n").bind("n",name).execute();
if(result.getAffectedItemsCount()>0)return true;
else return false;
}
else return false;
}catch(const mysqlx::Error&e){
LOG(ERROR)<<name<<" "<<"logout error:"<<e.what();
throw;
return false;
}
}
// 单词表的操作
bool DataManager::querryWord(const std::string &word, std::string &meaning){
try{
//保护数据库
std::unique_lock<std::mutex>lock(user_mtx_);
//获取单词表
mysqlx::Table dict=dict_schema_->getTable("dict");
mysqlx::RowResult rows=dict.select("mean").where("word=:w").bind("w",word).execute();
if(rows.count()>0){
mysqlx::Row row=rows.fetchOne();
meaning=(std::string)row[0];
return true;
}
else return false;
}catch(const mysqlx::Error&e){
LOG(ERROR)<<word<<"querry error:"<<" "<<e.what();
throw;
return false;
}
}
bool DataManager::recordHistory(const std::string &name, const std::string &word, const std::string &meaning, const std::string &time){
//插入历史信息
try{
//保护数据库
std::unique_lock<std::mutex>lock(user_mtx_);
//获取历史表
mysqlx::Table history=user_schema_->getTable("history");
mysqlx::Result res=history.insert("name","word","mean","time").values(name,word,meaning,time).execute();
if(res.getAffectedItemsCount()>0)return true;
else return false;
}catch(const mysqlx::Error&e){
LOG(ERROR)<<name<<" "<<"histroy error:"<<e.what();
throw;
return false;
}
}
bool DataManager::getHistory(const std::string name, std::string &history){
try{
//保护数据库
std::unique_lock<std::mutex>lock(user_mtx_);
//获取历史表
mysqlx::Table history_t=user_schema_->getTable("history");
std::string word,mean,time;
mysqlx::RowResult rows=history_t.select("word","mean","time").where("name=:n").bind("n",name).execute();
if(rows.count()>0){
history.clear();
for(const auto&row:rows){
word=(std::string)row[0];
mean=(std::string)row[1];
time=(std::string)row[2];
if(!word.empty()&&!mean.empty()&&!time.empty()){
history+=word+'\t';
history+=mean+'\t';
history+=time+'\n';
}
}
return true;
}else return false;
}catch(const mysqlx::Error&e){
LOG(ERROR)<<name<<"getHistory error:"<<e.what();
throw;
return false;
}
}
cpp
#include"server.hpp"
#include <unistd.h>
#include <netinet/in.h>
#include<iostream>
#include <chrono>
#include <ctime>
server::server(const std::string&ip,const int port,const std::string&user,const std::string&password,const int ThreadSize):isRun(true),manager(user,password){
sockfd=socket(AF_INET,SOCK_STREAM,0);
//1.创建套接字
if(sockfd<0){
std::cerr<<"socket error"<<std::endl;
return;
}
//2.设置端口快速复用
int res=1;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&res,sizeof(res))<0){
close(sockfd);
return;
}
//3.绑定ip和端口号
struct sockaddr_in sin;
sin.sin_family=AF_INET;
sin.sin_addr.s_addr=inet_addr(ip.c_str());
sin.sin_port=htons(port);
socklen_t len=sizeof(sin);
if(bind(sockfd,(const sockaddr*)&sin,len)<0){
close(sockfd);
return;
}
//4.启动监听状态
if(listen(sockfd,128)<0){
return;
}
//5.初始化线程池
startThreadPool(ThreadSize);
}
server::~server(){
//避免死锁:防止当工作线程在执行任务就被通知
{
//主线程抢锁,防止工作线程影响
std::unique_lock<std::mutex> lock(task_mutex);
//关闭线程池
isRun=false;
//当局部变量离开作用域时调用析构函数自动释放锁
}
//主线程唤醒所有工作线程
task_cv.notify_all();
//主线程等待回收所有工作线程
for(auto&worker:workers)worker.join();
close(sockfd);
}
void server::run(){
//创建接受客户端的信息结构体
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
//循环接受客户端
while(1){
int fd=accept(sockfd,(sockaddr*)&cli,&len);
if(fd<0){
std::cerr<<"accept error"<<std::endl;
break;
}
std::cout<<"accept from"<<cli.sin_addr.s_addr<<":"<<cli.sin_port<<std::endl;
//添加客户端任务到任务队列中
addTask([this,fd,cli]{
handleClient(fd,cli);
});
}
}
void server::addTask(std::function<void()>task){
{
//主线程抢锁,防止工作线程影响
std::unique_lock<std::mutex> lock(task_mutex);
//将任务加入到工作队列中,等待有空间的工作线程执行任务
tasks.emplace(task);
//当局部变量离开作用域时调用析构函数自动释放锁
}
//主线程通知一个线程执行任务
task_cv.notify_one();
}
void server::startThreadPool(const int ThreadSize){
//创建并初始化指定数量的线程
for(int i=0;i<ThreadSize;i++){
//emplace_back:直接创建一个线程添加到线程容器中
//lambda表达式作为线程的任务
workers.emplace_back([this]{
while(1){
//创建一个任务,工作线程可能同时执行各自任务,所以执行任务时不占用锁
//但是从工作队列中拿取任务时操作临界资源需要抢占锁
std::function<void()>task;
{
//抢占锁
std::unique_lock<std::mutex> lock(task_mutex);
//等待被主线程唤醒:当线程池停止或有任务可取时返回
task_cv.wait(lock,[this]{
return isRun==false || !tasks.empty();
});
//当线程池停止且工作队列为空时,线程退出
if(isRun==false && tasks.empty()) return;
//线程池正常时,工作线程从工作队列中取出任务
task=std::move(tasks.front());
tasks.pop();
}
//拿到任务后开始执行
task();
}
});
}
}
void server::handleClient(int client_fd, struct sockaddr_in cin){
//创建接受消息的容器
char buf[BUF_SIZE]="";
Msg msg;
int recv_len=0;
while(1){
memset(buf,0,BUF_SIZE);
recv_len=recv(client_fd,buf,BUF_SIZE,0);
//表示客户端下线,删除客户端并结束工作线程
if(recv_len<=0){
if(recv_len==0){
std::cout<<"Cilent disconnect:"<<inet_ntoa(cin.sin_addr)<<std::endl;
}else std::cerr<<"recv error"<<std::endl;
close(client_fd);
break;
}
//成功收到客户端消息,反序列化消息,并判断消息类型
msg.ParseFromArray(buf,recv_len);
std::string name=msg.name();
std::string data=msg.data();
//std::cout<<msg.type()<<" "<<msg.name()<<" "<<msg.data()<<std::endl;
switch (msg.type())
{
case LOGIN:
{
bool is_online=0;
if(manager.loginUser(name,data,is_online))msg.set_data(is_online?"EXISTS":"OK");
else msg.set_data("FAIL");
break;
}
case REGISTER:
{
if(manager.registerUser(name,data))msg.set_data("OK");
else msg.set_data("EXISTS");
break;
}
case QUERY:
{
std::string mean,word=msg.data();
//查询单词
if(manager.querryWord(data,mean))msg.set_data(word+" "+mean);
else msg.set_data("Not Found");
//记录查询历史
//获取当前系统时间对应的秒数
time_t now=time(NULL);
//将以秒数为单位的时间转变成时间的结构
tm*local=localtime(&now);
char time_str[20]="";
//格式化时间
strftime(time_str,sizeof(time_str),"%Y-%m-%d %H:%M:%S",local);
std::string cur_time(time_str);
//记录时间进入表中
manager.recordHistory(name,word,mean,cur_time);
break;
}
case HISTORY:
{
std::string history;
if(manager.getHistory(name,history))msg.set_data(history);
else msg.set_data("No history");
break;
}
case QUIT:
{
manager.logoutUser(name);
break;
}
default:
{
msg.set_data("Invaild Command");
}
return;
}
if(msg.type()==QUIT)continue;
//发送响应包
std::string output;
msg.SerializeToString(&output);
if(send(client_fd,output.c_str(),output.size(),0)<0){
std::cerr<<"send error"<<std::endl;
return;
}
}
}
cpp
#include<iostream>
#include<string.h>
#include <glog/logging.h>
#include"server.hpp"
#include"DataManager.hpp"
int main(int argc, const char* argv[]) {
if (argc < 5) {
std::cerr << "Usage: " << argv[0] << " <ip> <port> <user> <pwd>" << std::endl;
return -1;
}
std::string ip = argv[1];
int port = std::stoi(argv[2]); // 用 stoi 替代 atoi,更安全
std::string user=argv[3];
std::string pwd=argv[4];
// 1. 初始化 glog
// 参数是程序名,日志文件通常默认存放在 /tmp 目录下
google::InitGoogleLogging(argv[0]);
// 2. 设置日志输出到标准错误(控制台)
FLAGS_logtostderr = 1;
try {
server s(ip,port,user,pwd);
s.run();
} catch (const std::exception& e) {
LOG(ERROR)<<"异常退出: " << e.what();
return -1;
}
// 3.停止 glog(清理资源)
google::ShutdownGoogleLogging();
return 0;
}
protobuf
syntax = "proto3";
enum Type {
//注册
REGISTER=0;
//登录
LOGIN=1;
//查询单词
QUERY=2;
//查看历史
HISTORY=3;
//退出
QUIT=4;
}
message Msg {
//消息类型
Type type=1;
//用户名
bytes name=2;
//正文:密码/单词
bytes data=3;
}
cmake
cmake_minimum_required(VERSION 3.20)
project(DICTIONARY_SERVER CXX)
# 设置 C++ 标准(Connector/C++ 8.0 要求至少 C++11,建议使用 17)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Protobuf REQUIRED)
find_package(Threads REQUIRED)
find_package(glog REQUIRED)
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS Msg.proto)
# --- 手动指定 MySQL Connector 路径 ---
set(CONNECTOR_DIR "/usr/local/mysql-connector-cpp")
# 1. 指定头文件搜索路径
include_directories(${CONNECTOR_DIR}/include)
include_directories(${CMAKE_SOURCE_DIR}/include)
include_directories(${CMAKE_BINARY_DIR})
# 2. 指定库文件搜索路径
link_directories(${CONNECTOR_DIR}/lib64)
# 3. 生成可执行文件
add_executable(server src/main.cpp src/DataManager.cpp src/main.cpp src/server.cpp ${PROTO_SRCS})
# 4. 链接核心库
# mysqlcppconn8 是 X DevAPI 库(如果用的是旧版 JDBC 风格,则链接 mysqlcppconn)
target_link_libraries(server PUBLIC
mysqlcppconn8
${Protobuf_LIBRARIES}
Threads::Threads
glog::glog)
客户端实现
cpp
📂 CLIENT
├── 📂 build/ # 编译输出目录(存放中间文件和可执行程序)
├── 📂 include/ # 头文件目录 (.hpp)
│ └── 📄 client.hpp
├── 📂 src/ # 源文件目录 (.cpp)
│ ├── 📄 main.cpp
│ └── 📄 client.cpp
├── 📜 CMakeLists.txt # CMake 构建配置文件
└── 📑 Msg.proto # Protobuf 协议定义文件
cpp
#include"Msg.pb.h"
#include<string>
class client
{
private:
const int BUF_SIZE=128;
int sockfd;
std::string name;
std::string pwd;
bool is_logged;
bool running;
private:
void MenuView();
void Register();
void Login();
void Querry();
void History();
void Logout();
void gameView();
public:
client(const std::string&ip,const int port);
~client();
void run();
};
cpp
#include"client.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include"arpa/inet.h"
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <iomanip>
// 定义颜色转义码
#define RESET "\033[0m"
#define BOLD "\033[1m"
#define CYAN "\033[36m"
#define GREEN "\033[32m"
#define YELLOW "\033[33m"
#define MAGENTA "\033[35m"
#define BLUE "\033[34m"
#define ERR_LOG(msg) \
do \
{ \
perror(msg); \
std::cout << __LINE__ << " " << __func__ << " " << __FILE__ << std::endl; \
} while (0)
client::client(const std::string&ip,const int port)
{
//1.创建套接字
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0){
ERR_LOG("socket error");
close(sockfd);
return;
}
//2.连接服务器端
struct sockaddr_in sin;
sin.sin_addr.s_addr=inet_addr(ip.c_str());
sin.sin_family=AF_INET;
sin.sin_port=htons(port);
socklen_t len=sizeof(sin);
if(connect(sockfd,(const sockaddr*)&sin,len)<0){
ERR_LOG("connect error");
close(sockfd);
return;
}
}
client::~client()
{
Logout();
if(sockfd>0)close(sockfd);
}
void client::run()
{
int choice;
while(1){
MenuView();
// 1. 尝试读取输入
if (!(std::cin >> choice)) {
// 2. 如果输入不是数字(读取失败)
std::cout << "\033[31m输入无效!请输入数字。\033[0m" << std::endl;
std::cin.clear(); // 清除错误状态标志位
// 跳过缓冲区中所有非法字符,直到遇到换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
system("pause"); // 让用户看清错误提示
continue;
}
// 3. 输入是数字,开始逻辑判断
switch (choice) {
case 1:
{
Login();
// 执行登录逻辑
break;
}
case 2:
{
Register();
// 执行注册逻辑
break;
}
case 3:
{
Logout();
return; // 退出程序
}
default:
{
// 数字超出了 1-3 的范围
std::cout << "\033[31m无效的选择,请输入 1, 2 或 3。\033[0m" << std::endl;
system("pause");
break;
}
}
std::cin.get();
std::cin.clear();
}
}
void client::MenuView() {
// 清屏(Windows使用 "cls", Linux/Unix使用 "clear")
// 为了跨平台,这里打印一些空行或尝试系统调用
#ifdef _WIN32
system("cls");
#else
system("clear");
#endif
std::cout << MAGENTA << BOLD;
std::cout << "================================================" << std::endl;
std::cout << " __ __ ______ _ _ _ _ " << std::endl;
std::cout << " | \\/ | ____| \\ | | | | | " << std::endl;
std::cout << " | \\ / | |__ | \\| | | | | " << std::endl;
std::cout << " | |\\/| | __| | . ` | | | | " << std::endl;
std::cout << " | | | | |____| |\\ | |__| | " << std::endl;
std::cout << " |_| |_|______|_| \\_|\\____/ " << std::endl;
std::cout << "================================================" << RESET << std::endl;
std::cout << std::endl;
std::cout << CYAN << " 欢迎访问系统,请选择您要执行的操作:" << RESET << std::endl;
std::cout << std::endl;
// 绘制功能选项卡
std::cout << YELLOW << " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" << RESET << std::endl;
std::cout << YELLOW << " ┃" << GREEN << " [1] 用户登录 (Login) " << YELLOW << "┃" << RESET << std::endl;
std::cout << YELLOW << " ┃" << GREEN << " [2] 新用户注册 (Register) " << YELLOW << "┃" << RESET << std::endl;
std::cout << YELLOW << " ┃" << GREEN << " [3] 退出系统 (Exit) " << YELLOW << "┃" << RESET << std::endl;
std::cout << YELLOW << " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛" << RESET << std::endl;
std::cout << std::endl;
std::cout << BOLD << BLUE << " 请输入指令 (1-3) > " << RESET;
}
void client::Register(){
//1.阻塞获取用户名和密码
std::string name,pwd;
std::cout<<"请输入用户名:";
while(!(std::cin>>name));
// 跳过缓冲区中所有非法字符,直到遇到换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout<<"请输入密码:";
while(!(std::cin>>pwd));
// 跳过缓冲区中所有非法字符,直到遇到换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
//2.构建请求注册消息
Msg msg;
msg.set_name(name);
msg.set_type(REGISTER);
msg.set_data(pwd);
//3.序列化消息
std::string output;
msg.SerializeToString(&output);
//4.发送消息
if(send(sockfd,output.c_str(),output.size(),0)<0){
ERR_LOG("send error");
}
//5.接受消息,判断是否注册成功
char buf[BUF_SIZE]="";
int recv_len=0;
recv_len=recv(sockfd,buf,BUF_SIZE,0);
//表示客户端下线,删除客户端并结束工作线程
if(recv_len<=0){
std::cerr<<"recv error"<<std::endl;
return;
}
//6.反序列化消息
msg.ParseFromArray(buf,recv_len);
std::string response=msg.data();
if(response=="OK")std::cout<<"注册成功"<<std::endl;
else if(response=="EXISTS")std::cout<<"用户已经注册过了"<<std::endl;
}
void client::Login(){
//1.阻塞获取用户名和密码
std::string name,pwd;
std::cout<<"请输入用户名:";
while(!(std::cin>>name));
// 跳过缓冲区中所有非法字符,直到遇到换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout<<"请输入密码:";
while(!(std::cin>>pwd));
// 跳过缓冲区中所有非法字符,直到遇到换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
//2.构建请求登录消息
Msg msg;
msg.set_name(name);
msg.set_type(LOGIN);
msg.set_data(pwd);
//3.发送请求登录消息
std::string output;
msg.SerializeToString(&output);
if(send(sockfd,output.c_str(),output.size(),0)<0){
std::cerr<<"send error"<<std::endl;
return;
}
//4.接受响应消息
char buf[BUF_SIZE]="";
int recv_len=0;
recv_len=recv(sockfd,buf,BUF_SIZE,0);
if(recv_len<=0){
std::cerr<<"recv error"<<std::endl;
return;
}
//5.反序列化消息
msg.ParseFromArray(buf,recv_len);
std::string response=msg.data();
if(response == "OK"){
this->name = name;
is_logged = true;
while (true) {
// 1. 显示菜单
gameView();
int choice;
// 2. 输入验证循环
while (true) {
std::cout << "请输入选项 (1-3): "; // 提示用户输入
std::cin >> choice;
// 检查输入流是否出错(例如用户输入了 'a' 而不是数字)
if (std::cin.fail()) {
std::cin.clear(); // 清除错误标志
// 丢弃这一行输入的所有剩余字符(包括那个非法的字母和回车)
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "❌ 无效输入,请输入数字!" << std::endl;
} else {
// 输入是数字,清空该行可能残留的字符(虽然对于int输入通常不需要,但为了保险)
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
break; // 退出输入循环,执行下面的 switch
}
}
// 3. 处理逻辑
if (choice == 1) {
Querry();
} else if (choice == 2) {
History();
} else if (choice == 3) {
Logout();
break; // 退出登录循环
} else {
// 处理输入了数字但不在 1-3 范围内的情况
std::cout << "❌ 选项不存在,请重新输入。" << std::endl;
}
std::cin.get();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
}
else if(response=="EXISTS"){
std::cout<<"用户已在别处登录"<<std::endl;
}else if(response=="FAIL"){
std::cout<<"用户名和密码错误"<<std::endl;
}
}
void client::Querry(){
//1.阻塞获取需要查询的单词
std::string word;
std::cout<<"请输入查询的单词:";
while(!(std::cin>>word));
// 跳过缓冲区中所有非法字符,直到遇到换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
//2.构建请求查询消息
Msg msg;
msg.set_name(this->name);
msg.set_type(QUERY);
msg.set_data(word);
//3.发送请求查询消息
std::string output;
msg.SerializeToString(&output);
if(send(sockfd,output.c_str(),output.size(),0)<0){
std::cerr<<"send error"<<std::endl;
return;
}
//4.接受响应消息
char buf[BUF_SIZE]="";
int recv_len=0;
recv_len=recv(sockfd,buf,BUF_SIZE,0);
if(recv_len<=0){
std::cerr<<"recv error"<<std::endl;
return;
}
//5.反序列化消息
msg.ParseFromArray(buf,recv_len);
std::string mean=msg.data();
std::cout<<mean<<std::endl;
}
void client::History(){
//2.构建请求查询消息
Msg msg;
msg.set_name(name);
msg.set_type(HISTORY);
//3.发送请求历史消息
std::string output;
msg.SerializeToString(&output);
if(send(sockfd,output.c_str(),output.size(),0)<0){
std::cerr<<"send error"<<std::endl;
return;
}
//4.接受历史消息
char buf[BUF_SIZE]="";
int recv_len=0;
recv_len=recv(sockfd,buf,BUF_SIZE,0);
if(recv_len<=0){
std::cerr<<"recv error"<<std::endl;
return;
}
//5.反序列化消息
msg.ParseFromArray(buf,recv_len);
std::string history=msg.data();
std::cout<<history<<std::endl;
}
void client::gameView(){
// 清屏
#ifdef _WIN32
system("cls");
#else
system("clear");
#endif
// 顶部状态栏
std::cout << CYAN << BOLD;
std::cout << std::string(50, '-') << std::endl; // 分隔线
std::cout << std::endl;
// 中心标题装饰
std::cout << MAGENTA << BOLD;
std::cout << " ==================================" << std::endl;
std::cout << " | WELCOME TO USER DASHBOARD |" << std::endl;
std::cout << " ==================================" << RESET << std::endl;
std::cout << std::endl;
std::cout << CYAN << " 您已进入个人工作台,请选择功能:" << RESET << std::endl;
std::cout << std::endl;
// 绘制功能选项卡 - 增加了简单的 ASCII 图标以增加区分度
std::cout << YELLOW << " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" << RESET << std::endl;
std::cout << YELLOW << " ┃" << GREEN << " [1] 🔍 查询单词 (Search) " << YELLOW << "┃" << RESET << std::endl;
std::cout << YELLOW << " ┃" << GREEN << " [2] 📜 查看历史 (History) " << YELLOW << "┃" << RESET << std::endl;
std::cout << YELLOW << " ┃" << GREEN << " [3] 🚪 返回上一级 (Back) " << YELLOW << "┃" << RESET << std::endl;
std::cout << YELLOW << " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛" << RESET << std::endl;
std::cout << std::endl;
std::cout << BOLD << BLUE << " 请输入指令 (1-3) > " << RESET;
}
void client::Logout(){
//1.构建信息
Msg msg;
msg.set_name(name);
msg.set_type(QUIT);
//2.序列化消息
std::string output;
msg.SerializeToString(&output);
//3.发送消息
if(send(sockfd,output.c_str(),output.size(),0)<0){
ERR_LOG("send error");
return;
}
}
cpp
#include<iostream>
#include"client.hpp"
int main(int argc,const char*argv[]){
if(argc!=3){
std::cerr<<"please input <ip> <port>"<<std::endl;
return -1;
}
std::string ip=argv[1];
int port=std::stoi(argv[2]);
try{
client c(ip,port);
c.run();
}catch(const std::exception&e){
std::cerr<<e.what()<<std::endl;
return -1;
}
return 0;
}
protobuf
syntax = "proto3";
enum Type {
//注册
REGISTER=0;
//登录
LOGIN=1;
//查询单词
QUERY=2;
//查看历史
HISTORY=3;
//退出
QUIT=4;
}
message Msg {
//消息类型
Type type=1;
//用户名
bytes name=2;
//正文:密码/单词
bytes data=3;
}
cmake
cmake_minimum_required(VERSION 3.20)
project(CHATROOM_CLIENT CXX)
find_package(Threads REQUIRED)
find_package(Protobuf REQUIRED)
include_directories(${CMAKE_SOURCE_DIR}/include)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
protobuf_generate_cpp(PROTO_SRCS PROTO_HADS Msg.proto)
add_executable(client src/main.cpp src/client.cpp ${PROTO_SRCS})
target_link_libraries(client PUBLIC Threads::Threads ${Protobuf_LIBRARIES})
值得注意的点
数据库管理
在该项目中使用的数据库为非关系型数据库MySQL ,主要通过一个数据库管理类 DataManager 来实现服务器主流程和数据库之间的交互 ,而 DataManager 类通过 MySQL Connector C++的 API 来实现 C++和 MySQL 之间的联系以此来管理和操作数据库。
MySQL Connector C++
💢**注意:不要引入错的头文件和库,和版本相对应,否则会导致段错误**等。😭
DataManager
该类主要负责连接服务器端和数据库之间的交互,对于服务器端,需要向上提供服务器端操作需要的接口 API ,对于数据库,需要使用数据库相关 API 操作数据库。
比如,该项目中服务器端主要借助两个数据库:usr_db(用户数据库) 和 dict_db(单词数据库)。在 DataManager 类中管理这两个库的方式为建立一个 Session,通过会话维护两个 Schema 来保持和数据库之间的持续交互,而不用每次操作都切换数据库或者重新打开数据库。
cpp
std::unique_ptr<mysqlx::Session> sess_;
std::unique_ptr<mysqlx::Schema> user_schema_;
std::unique_ptr<mysqlx::Schema> dict_schema_;
cpp
// 用户相关的操作
bool registerUser(const std::string &name, const std::string &password); // 用户注册操作
bool loginUser(const std::string &name, const std::string &password, bool &is_online); // 用户登录操作
bool logoutUser(const std::string &name); // 用户退出操作
// 单词表的操作
bool querryWord(const std::string &word, std::string &meaning); // 查询单词
bool recordHistory(const std::string &name, const std::string &word, const std::string &meaning, const std::string &time); // 历史记录
此外,该服务器端采用了线程池处理并发客户端,由于 MySQL 数据库 API 是非线程安全 的,对于多线程互斥操作数据库时需要额外维护数据库的锁 ,保证对于每个库,只有一个工作线程在读出或写入数据 ,而不出现两个或两个以上工作线程写入/读出数据,避免数据库操作冗余混乱。当然,++在多线程编程中可以依靠锁来实现互斥操作,而在数据库本身也可以依靠事务的隔离级别来避免多进程编程中互斥操作数据库。++
cpp
// 细粒度锁:保护非线程安全的数据库对象
std::mutex user_mtx_;
std::mutex dict_mtx_;
事务操作
在操作数据库时事务是保证每个改变数据库值的操作都完成或不完成的机制。
比如,当服务器端启动时如果单词数据库中的单词表并没有建立,那么 DataManager 类就需要负责建立表并向表中导入单词数据。由于单词数据可能会很庞大,导入需要一定的时间,而为了避免在导入过程中服务器突然故障(比如断电等)导致数据只导入一部分的情况(下次导入可能重复),在导入开始前开启事务,成功导入后提交事务,失败导入后回滚并尝试重新导入的事务操作就显得很有必要。
cpp
void DataManager::importDict(const std::string& filePath,const std::string& tableName) {
std::ifstream file(filePath);
if (!file.is_open()) {
std::cerr << "无法打开词典文件: " << filePath << std::endl;
return;
}
std::cout<<"打开文件成功"<<std::endl;
mysqlx::Table dict = dict_schema_->getTable(tableName);
std::string line;
int count = 0;
try {
// 1. 开启事务
sess_->startTransaction();
std::cout << "开始导入数据并开启事务..." << std::endl;
// 建议使用批量插入对象来进一步提升速度
auto inserter = dict.insert("word", "mean");
while (std::getline(file, line)) {
if (line.empty()) continue;
auto pos = line.find(' ');
if (pos == std::string::npos) continue;
std::string word = line.substr(0, pos);
std::string mean = line.substr(pos + 1);
// 清洗 mean 开头的空格
size_t first = mean.find_first_not_of(' ');
if (first != std::string::npos) mean = mean.substr(first);
if (word.empty() || mean.empty()) continue;
// 将数据放入缓存队列
inserter.values(word, mean);
count++;
// 每 2000 条执行一次 execute,但此时还没 commit 到磁盘
if (count % 2000 == 0) {
inserter.execute();
inserter = dict.insert("word", "mean"); // 重置插入对象
}
}
// 提交剩余的数据到服务器缓冲区
if (count % 2000 != 0) {
inserter.execute();
}
// 2. 提交事务(真正落盘)
sess_->commit();
std::cout << "导入完成,共计: " << count << " 条记录已成功提交。" << std::endl;
} catch (const mysqlx::Error &err) {
// 3. 发生错误时回滚事务,保证数据库不被污染
sess_->rollback();
LOG(ERROR)<<"数据导入出错,已执行回滚。错误信息: " << err.what();
return;
}
}
牢记 TCP 通信流程

线程池和 Protobuf
关于线程池和 Protobuf 的使用关注基于TCP的网络聊天室,用法基本一样。
有一点不同的地方在于,这里的消息的枚举类型定义在 protobuf 中,但无伤大雅了。
获取当前时间
在查询单词时我们需要将单词信息加入该用户的查询历史中,单词信息包括:用户名、查询单词、单词意思以及查询时间。针对查询时间,需要进行格式化并存储为字符串形式。
cpp
//获取当前系统时间对应的秒数
time_t now=time(NULL);
//将以秒数为单位的时间转变成时间的结构
tm*local=localtime(&now);
char time_str[20]="";
//格式化时间
strftime(time_str,sizeof(time_str),"%Y-%m-%d %H:%M:%S",local);
std::string cur_time(time_str);
glog 日志
glog (Google Logging) 是由 Google 开发的一套高性能、功能丰富的 C++ 日志库。它在 Google 内部被广泛使用,现在也是许多知名开源项目(如 Caffe, Ceres Solver, Kubernetes 等)的首选日志系统。
glog 的核心优势
相比于简单的 <font style="color:rgb(68, 71, 70);">std::cout</font> 或基础的日志库(如 <font style="color:rgb(68, 71, 70);">log4cpp</font>),glog 的优势体现在以下几个方面:
- 分级日志系统 :支持
<font style="color:rgb(68, 71, 70);">INFO</font>,<font style="color:rgb(68, 71, 70);">WARNING</font>,<font style="color:rgb(68, 71, 70);">ERROR</font>,<font style="color:rgb(68, 71, 70);">FATAL</font>四个等级。<font style="color:rgb(68, 71, 70);">FATAL</font>日志会在记录后自动终止程序。 - 条件日志:支持"每隔 N 次记录一次"或"满足特定条件时记录",极大减少了高频循环中的日志冗余。
- 信号处理:当程序崩溃(如段错误)时,glog 可以自动捕捉信号并 dump 出堆栈信息,方便调试。
- 灵活的输出流 :支持类 C++
<font style="color:rgb(68, 71, 70);">ostream</font>的用法(即<font style="color:rgb(68, 71, 70);">LOG(INFO) << "msg"</font>),非常直观。 - 轻量且高性能:相比一些重量级库,glog 的配置更简单,且在高性能异步写入方面表现优异。
如何安装(以 Linux 为例)
bash
sudo apt-get install libgoogle-glog-dev
bash
git clone https://github.com/google/glog.git
cd glog && mkdir build && cd build
cmake .. && make && sudo make install
使用示例
cpp
#include <glog/logging.h>
#include <string>
void someFunction(int i) {
// 1. 条件日志:只有当 i > 10 时才记录
LOG_IF(INFO, i > 10) << "i is greater than 10, current value: " << i;
// 2. 周期性日志:每执行 10 次记录一次(常用于循环检查)
LOG_EVERY_N(INFO, 10) << "Got iteration " << google::COUNTER;
// 3. 前 N 次日志:只记录前 5 次
LOG_FIRST_N(INFO, 5) << "This will only show 5 times.";
}
int main(int argc, char* argv[]) {
// A. 初始化 glog
// 设置日志文件保存目录(如果目录不存在,glog不会报错,但不会写文件)
FLAGS_log_dir = "./logs";
// 设置是否将日志也输出到屏幕
FLAGS_alsologtostderr = true;
// 设置颜色输出
FLAGS_colorlogtostderr = true;
google::InitGoogleLogging(argv[0]);
// B. 崩溃处理:程序崩溃时自动打印堆栈信息
google::InstallFailureSignalHandler();
LOG(INFO) << "系统启动...";
LOG(WARNING) << "这是一个警告日志";
for(int i = 0; i < 50; ++i) {
someFunction(i);
}
// C. 检查性日志(类似断言,但更强大)
int x = 5, y = 10;
CHECK_EQ(x + 5, y) << "加法计算错误!"; // 如果不相等,程序直接退出
LOG(ERROR) << "模拟一个错误,但程序不会停止";
// D. 停止 glog
google::ShutdownGoogleLogging();
return 0;
}
注意事项
- 目录权限 :如果设置了
<font style="color:rgb(68, 71, 70);">FLAGS_log_dir</font>,请确保该目录已经存在。glog 不会自动创建文件夹,如果目录不存在且没开启屏幕输出,你将看不到任何日志。 - FATAL 等级会杀掉进程 :调用
<font style="color:rgb(68, 71, 70);">LOG(FATAL)</font>后,程序会立即终止并生成堆栈镜像。在生产环境的常规逻辑中慎用。 - 线程安全 :glog 是线程安全的,多个线程同时调用
<font style="color:rgb(68, 71, 70);">LOG(INFO)</font>不会造成乱序输出或崩溃。 - 性能损耗 :虽然 glog 很快,但在极高频的内层循环(每秒百万次以上)中频繁调用
<font style="color:rgb(68, 71, 70);">LOG</font>仍会产生开销。此时应配合<font style="color:rgb(68, 71, 70);">LOG_EVERY_N</font>使用。 - CMake 链接 :在
<font style="color:rgb(68, 71, 70);">CMakeLists.txt</font>中,记得链接 glog 库:
cmake
find_package(glog REQUIRED)
target_link_libraries(your_project glog::glog)
- 清理工作 :程序退出前务必调用
<font style="color:rgb(68, 71, 70);">google::ShutdownGoogleLogging()</font>,否则缓冲区中的最后几条日志可能无法正确写入文件。