预约系统版本2
基于 C++ + epoll + pthread + MySQL 的多线程并发预约系统
包含连接池、线程池、epoll I/O 多路复用、JSON 协议、SQL 事务
一、项目概述
1.1 项目定位
一个支持多客户端同时在线的票务预约系统,客户端通过 TCP 长连接与服务端交互,服务端使用 epoll 监听 I/O 事件,并通过线程池把任务交给 worker 线程处理,worker 线程通过连接池借用 MySQL 连接执行 SQL。
1.2 核心特性
| 特性 | 实现方式 |
|---|---|
| 并发模型 | epoll + pthread 线程池 |
| 数据库复用 | Connect_Pool 连接池(5 个连接复用) |
| 协议格式 | JSON(基于 jsoncpp) |
| 事务支持 | 预约/取消操作使用 MySQL 事务 |
| 容错 | 连接池满时返回 "MYSQL CLIENT BUSSY",客户端提示重试 |
1.3 业务功能
- 用户注册(手机号 + 用户名 + 密码)
- 用户登录
- 查看可预约票列表
- 预约票(带事务,防止超卖)
- 查看我的预约
- 取消预约(带事务,保持 count 一致)
二、项目结构
csproject2/
├── service.h # 服务端头文件(类声明)
├── service.cpp # 服务端实现(主流程 + 业务逻辑)
├── client.h # 客户端头文件
├── client.cpp # 客户端实现
├── service.conf # 服务端配置(IP/Port/lismax)
├── mysql.conf # 数据库配置(IP/Port/db_name)
├── Makefile # 构建脚本
├── ser_start.sh # 启动脚本
├── service # 编译产物(服务端可执行)
└── client # 编译产物(客户端可执行)
编译 & 启动
bash
make clean && make all
./service # 启动服务端
./client # 启动客户端
三、改进建议
3.1 性能改进
3.1.1 连接池加条件变量(当前最大短板)
现状 :Connect_Mysql_Server 借不到连接直接 return false,构造失败 → 客户端收到 BUSSY。
问题 :并发请求超过 5 个时,第 6 个直接被拒,用户体验差。
改进 :在 Connect_Pool 加 pthread_cond_t,借不到时 pthread_cond_wait,等他人归还后唤醒。
cpp
// 借连接
while (C_P_array[i].status == false) pthread_cond_wait(&cond, &mutex);
// 归还时
pthread_cond_signal(&cond);
3.1.2 worker 线程绑定固定连接(彻底消除锁)
现状 :5 个 worker 共享 5 个连接,借/还都要加锁。
改进 :worker 启动时绑死一个连接(线程局部存储 __thread 或 pthread_key),完全无锁。
3.1.3 send/recv 改非阻塞 + 边沿触发
现状 :epoll 用 Level Triggered + EPOLLONESHOT,每次要重新 MOD。
改进:改 Edge Triggered + 非阻塞 fd,循环 recv 到 EAGAIN,减少 epoll_ctl 调用。
3.1.4 SQL 预处理语句
现状 :每次 mysql_query 拼字符串,服务端每次都要 parse SQL。
改进 :用 mysql_stmt_prepare + mysql_stmt_bind_param,复用执行计划。
3.2 安全改进
3.2.1 SQL 注入(必改)
现状 :Db_User_Register、Db_User_Login 等直接拼字符串:
cpp
string sql = "select Name,Passwd from user_info where Tel='" + tel + "'";
风险 :用户 tel 输入 ' OR '1'='1 直接绕过密码校验。
改进 :用 mysql_real_escape_string 转义,或改 prepared statement。
3.2.2 密码明文存储
现状 :Db_User_Register values(0,'tel','name','passwd',...) 直接存明文。
风险 :数据库被拖库后密码全暴露。
改进 :用 crypt()/SHA256/bcrypt 存哈希,登录时比哈希。
3.2.3 客户端不验证服务端身份
现状 :客户端直接 connect,无法识别中间人。
改进:用 TLS(OpenSSL)加密通道。
3.3 健壮性改进
3.3.1 signal 处理
现状 :Ctrl+C 时 epoll_wait 会被 EINTR 中断,但 L895-L900 直接 exit(1),不释放资源。
改进 :注册 SIGINT/SIGTERM handler,设 Status = false,让主循环优雅退出,析构 Tcp_Service → 析构 Thread_Pool → join 所有 worker。
3.3.2 客户端断连感知
现状 :客户端 Ctrl+C 退出,服务端 Do_client 收到 recv_num == 0 走 delete this,OK。但如果客户端网络异常(没发 FIN),服务端不知道。
改进 :用 TCP keepalive(setsockopt SO_KEEPALIVE)或应用层心跳。
3.3.3 EPOLLONESHOT 处理一致
现状 :listen socket 没用 EPOLLONESHOT,client socket 用了。
风险 :多线程同时 accept 同一个 listen fd,可能竞争。
改进:listen fd 也用 EPOLLONESHOT,accept 后重新 MOD。
3.3.4 配置文件硬编码路径
现状 :<service.h#L22> #define Conf_path "/home/xiaoc/mycode/csproject2/service.conf",换机器就废。
改进:从 argv1 读,或读环境变量。
3.3.5 mysql_real_connect 密码硬编码
现状 :Connect_Pool::Run "root","111111" 写死在代码里。
改进 :从 mysql.conf 读用户名/密码,或用 MySQL 配置文件 [client] 段。
3.4 可维护性改进
3.4.1 头文件依赖混乱
现状 :<service.h> 把 Service_Config、Socket、AccSocket、RecvSocket、Thread_Pool、Mysql_config、Mysql_Client、Mysql_cli、Connect_Pool、Tcp_Service 10 个类型全堆在一个文件。
改进 :拆分成 socket.h、thread_pool.h、mysql_pool.h、mysql_client.h、tcp_service.h、service_config.h,每个文件单一职责。
3.4.2 全局变量
现状 :service.cpp#L15-L16 Mysql_config file; 和 Connect_Pool* g_pool; 是全局变量。
问题 :难追踪修改点,难单元测试。
改进 :g_pool 改成 Connect_Pool::GetInstance() 单例;file 放到 Tcp_Service 里作成员。
3.4.3 裸指针 + 手动 new/delete
现状 :Socket* task、Mysql_cli* C_P_array 等全是裸指针,靠 delete 手动管理。
风险 :异常路径漏 delete 会泄漏。
改进 :用 std::unique_ptr / std::shared_ptr,析构自动释放。
3.4.4 错误日志不分级
现状 :所有错误都 cout << 打到 stdout。
改进:引入简单日志库(如 spdlog),分 INFO/WARN/ERROR/FATAL,支持文件分卷。
3.4.5 magic number
现状 :<service.cpp#L787> "root","111111"、<client.h#L18> PORTADRESS 8888、MAXLINE 256、d_size 1024 等硬编码。
改进:全部移到配置文件或 const 常量。
3.5 功能扩展
3.5.1 缺少客户端超时
客户端 recv 是阻塞的,服务端挂了客户端会一直等。
改进 :用 setsockopt SO_RCVTIMEO 设 5 秒超时,超时提示。
3.5.2 缺少管理员后台
现在无法后台管理票务(增删改查票、查用户、统计)。
改进:加一个管理端协议 type=Admin_Login + type=Admin_Add_Ticket 等。
3.5.3 缺少并发数统计
无法查看当前在线用户、池使用率。
改进:服务端加一个 stats 接口(可通过 SIGUSR1 信号 dump 状态到日志)。
四、各文件详解
4.1 <service.h> --- 服务端类声明
完整代码
cpp
#include <stdlib.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <jsoncpp/json/json.h>
#include <mysql/mysql.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <unistd.h>
#define MAXLINE 256
#define START -1
#define Conf_path "/home/xiaoc/mycode/csproject2/service.conf"
#define Sql_Conf_path "/home/xiaoc/mycode/csproject2/mysql.conf"
#define d_size 1024
#define thread_num 5
#define o "ok"
class Connect_Pool;
extern Connect_Pool* g_pool;
struct Service_Config{
const char* config_path=Conf_path;
std::string IP;
short Port;
short Lismax;
void ReadConfig(const char* config_path);
void PrintConfig() const;
};
class Socket{
public:
int fd;
int epfd;
Socket(int fd,int epfd);
virtual ~Socket();
virtual void Do_client() = 0;
};
class AccSocket : public Socket{
public:
AccSocket(int fd,int epfd);
~AccSocket();
void Do_client() override;
private:
static unsigned int count;
};
class RecvSocket : public Socket{
public:
RecvSocket(int fd,int epfd);
~RecvSocket();
void Do_client() override;
void send_Ok();
void send_false();
};
typedef struct{
Socket* task;
}task_t;
class Thread_Pool{
private:
task_t * dequeue;
int dequeue_size;
int front;
int rear;
int task_Num;
int thread_Num;
bool status;
pthread_mutex_t mutex;
pthread_cond_t cond;
pthread_t Thread_array[thread_num];
public:
Thread_Pool();
~Thread_Pool();
bool Init_Thread_pool();
void add_Task(Socket* csocket);
void Create_Thread();
void Work();
};
void* General_Thread(void* arg);
struct Mysql_config{
short db_port;
std::string db_ip;
std::string db_name;
const char* Sql_config_path=Sql_Conf_path;
void Init_Mysql_config(const char* Sql_config_path);
void print_Mysql_config () const;
};
class Mysql_Client{
private:
struct Mysql_config* file;
MYSQL* mysql_con;//mysql的连接句柄
int who;
bool connected; // 是否成功从池里借到连接
public:
Mysql_Client(struct Mysql_config * Sql_config,int who);
void ResetSTatus();
~Mysql_Client();
bool IsConnected() const { return connected; } // 供外部检查是否借到连接
bool Connect_Mysql_Server();
bool Db_User_Register(const std::string &user_tel,const std::string &user_name,const std::string &user_passwd);
bool Db_User_Login(const std::string& tel,std::string& name,const std::string& passwd);
bool Db_Show_Ticket(Json::Value &res);
bool Db_Yd_Ticket(std::string usertel,std::string tk_id);
bool Db_Show_My_Ticket(const std::string&user_tel,Json::Value &res);
bool Db_Cancel_Ticket(const std::string &user_tel,const std::string &yd_id);
void Send_full();
void Mysql_Begin();
void Mysql_RollBack();
void Mysql_Commit();
};
//连接池中的每一个与数据库进行连接的客户端入口结构体
struct Mysql_cli{
MYSQL* mysql_con;
bool status;
};
class Connect_Pool{
private:
bool status;
int MAX_USER;
Mysql_config* Sql_config;
public:
Mysql_cli* C_P_array;
int user;
pthread_mutex_t mutex;
Connect_Pool(Mysql_config* Sql_config);
~Connect_Pool();
bool Run();
void Reset();
};
class Tcp_Service{
private:
const Service_Config* config;
Thread_Pool* pool;
int lis_fd = START;
int ep_fd = START;
bool Status;
struct epoll_event ready_events[MAXLINE]{0};
public:
Tcp_Service(const Service_Config* config);
~Tcp_Service();
void Init(const Service_Config* config);
};
文件作用
集中声明服务端所有类型:配置类、Socket 体系、线程池、MySQL 客户端、连接池、Tcp_Service 主控。
关键类型一览
| 类型 | 行号 | 职责 |
|---|---|---|
Service_Config |
L34-L43 | 读 service.conf,存 IP/Port/Lismax |
Socket(抽象基类) |
L48-L56 | 持有 fd + epfd,纯虚 Do_client() |
AccSocket |
L58-L66 | 处理 listen fd 上的 accept 事件 |
RecvSocket |
L68-L76 | 处理 client fd 上的 recv 事件 |
task_t |
L78-L80 | 任务结构体(包 Socket* 指针) |
Thread_Pool |
L82-L105 | 任务队列 + 5 个 worker 线程 |
Mysql_config |
L110-L118 | 读 mysql.conf,存 db_ip/port/name |
Mysql_Client |
L120-L145 | 业务层,封装 SQL 操作 |
Mysql_cli |
L148-L151 | 连接池中单个 slot(MYSQL* + status) |
Connect_Pool |
L155-L172 | 5 个 Mysql_cli + mutex |
Tcp_Service |
L174-L191 | 主控:listen + epoll 循环 |
设计亮点
- 前置声明
class Connect_Pool;(L31)让g_pool全局指针可用,定义放在后面 Socket抽象基类 +Do_client()纯虚函数,多态分发 accept/recv 任务Mysql_Client::IsConnected()(L130)提供连接状态查询,避免外部访问私有成员
不足
- 10 个类型挤一个文件,违反单一职责
Mysql_cli是 struct 但成员公开,封装不严
4.2 <service.cpp> --- 服务端实现
完整代码
cpp
#include "service.h"
#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <jsoncpp/json/reader.h>
#include <jsoncpp/json/value.h>
#include <mysql/mysql.h>
#include <netinet/in.h>
#include <pthread.h>
#include <string>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
using namespace std;
Mysql_config file;
Connect_Pool* g_pool = nullptr;
enum OP_TYPE {
Login = 1,
Register,
CheckApoint,
Appointment,
AppoinMessage,
CancelAppoin,
Exit
};
void Service_Config::ReadConfig(const char *config_path) {
if (config_path == nullptr) {
cout << "config_path is nullptr!" << endl;
return;
}
FILE *fp = fopen(config_path, "r");
if (fp == nullptr) {
perror("fopen config file failed");
cout << "fopen config file failed! errno=" << errno << endl;
return;
}
char line[MAXLINE]{0};
int num = 0;
while (fgets(line, MAXLINE - 2, fp) != nullptr) {
num++;
if (line[0] == '#' || line[0] == '\n') {
continue;
}
char *Before = strtok(line, "=");
char *After = strtok(nullptr, "=");
if (Before == nullptr || After == nullptr) {
continue;
}
After[strlen(After) - 1] = '\0';
if (strcmp("ip", Before) == 0) {
IP = After; // 禁止共享内存,line是一次性使用,After会被覆盖
} else if (strcmp("port", Before) == 0) {
Port = atoi(After);
} else if (strcmp("lismax", Before) == 0) {
Lismax = atoi(After);
} else if (strcmp("service_conf_path", Before) == 0) {
config_path = After;
} else {
cout << "unknown config item: " << Before << " at line " << num << endl;
}
memset(line, 0, sizeof(line));
}
fclose(fp);
}
void Service_Config::PrintConfig() const {
cout << "IP: " << IP << endl;
cout << "Port: " << Port << endl;
cout << "Lismax: " << Lismax << endl;
cout << "config_path: " << config_path << endl;
}
Tcp_Service::Tcp_Service(const Service_Config *config) { Init(config); }
Tcp_Service::~Tcp_Service() {
cout << "~Tcp_Service()" << endl;
close(lis_fd);
close(ep_fd);
if (pool != nullptr)
delete pool;
}
Socket::Socket(int fd, int epfd) : fd(fd), epfd(epfd) {}
Socket::~Socket() {}
AccSocket::AccSocket(int fd, int epfd) : Socket(fd, epfd) {}
unsigned int AccSocket::count = 0;
AccSocket::~AccSocket() {}
void AccSocket::Do_client() {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
memset(&client_addr, 0, sizeof(client_addr));
// lis_fd对accept的影响,在默认情况下,lis_fd在队列为空时,会默认让accept在获取这个空队列时阻塞,线程会直接阻塞卡死在这
// 所以我们要用非阻塞模式的accept,当队列为空时,accept会返回EAGAIN或EWOULDBLOCK,而不是阻塞线程
int conn_fd = accept(fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (conn_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// listen 队列空了,别的 worker 已经把连接取走了,正常退出
delete this;
return;
}
perror("accept failed");
cout << "accept failed! errno=" << errno << endl;
delete this;
return;
}
cout << "client:" << conn_fd << " connect success!" << endl;
cout << "Service accept client numb now: " << ++count << endl;
cout << "-------------------------------------" << endl;
struct epoll_event ev_client_accpet;
ev_client_accpet.events = EPOLLIN | EPOLLONESHOT;
auto *recvSock = new RecvSocket(conn_fd, epfd);
ev_client_accpet.data.ptr = recvSock;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev_client_accpet) == -1) {
perror("epoll_ctl add client fd failed");
cout << "epoll_client_ctl add false! errno=" << errno << endl;
close(conn_fd); // 关闭连接,不受监控的不要
delete recvSock; // 删除对象
}
delete this;
}
RecvSocket::RecvSocket(int fd, int pdfd) : Socket(fd, pdfd) {}
RecvSocket::~RecvSocket() { close(fd); }
void RecvSocket::send_Ok() {
Json::Value value;
string a = o;
value["status"] = a;
send(fd, value.toStyledString().c_str(),
strlen(value.toStyledString().c_str()), 0);
}
void RecvSocket::send_false() {
Json::Value value;
string a = "false";
value["status"] = a;
send(fd, value.toStyledString().c_str(),
strlen(value.toStyledString().c_str()), 0);
}
void RecvSocket::Do_client() {
char buff[MAXLINE]{0};
ssize_t recv_num = recv(fd, buff, sizeof(buff) - 1, 0);
bool tag = false;
if (recv_num < 0) {
perror("recv failed");
cout << "[fd=" << fd << "] recv 出错 errno=" << errno << endl;
tag = true;
} else if (recv_num == 0) {
cout << "[fd=" << fd << "] 客户端正常关闭连接" << endl;
tag = true;
} else {
buff[recv_num] = '\0';
cout << "[fd=" << fd << "] 收到 " << recv_num << " 字节" << endl;
cout << "---------------------------------------------" << endl;
Json::Value val;
Json::Reader read;
if (!read.parse(buff, val)) {
cout << "[fd=" << fd << "] Json 解析失败! 收到内容: " << buff << endl;
tag = true;
} else {
Mysql_Client sql(&file,fd);
if (sql.IsConnected()) {
int i = val["type"].asInt();
if (i == Login) {
string user_tel = val["user_tel"].asString();
string passwd = val["passwd"].asString();
string user_name = val["user_name"].asString();
if (sql.Db_User_Login(user_tel, user_name, passwd)) {
send_Ok();
} else {
send_false();
}
} else if (i == Register) {
string tel = val["user_tel"].asString();
string name = val["user_name"].asString();
string passwd = val["passwd"].asString();
if (sql.Db_User_Register(tel, name, passwd)) {
send_Ok();
} else {
send_false();
}
} else if (i == CheckApoint) {
Json::Value Reader;
if (!sql.Db_Show_Ticket(Reader)) {
send_false();
} else
send(fd, Reader.toStyledString().c_str(),
strlen(Reader.toStyledString().c_str()), 0);
} else if (i == Appointment) {
string user_tel = val["user_tel"].asString();
string ticket_id = val["ticket_id"].asString();
if (!sql.Db_Yd_Ticket(user_tel, ticket_id)) {
send_false();
} else {
send_Ok();
}
} else if (i == AppoinMessage) {
string user_tel = val["user_tel"].asString();
Json::Value res;
if (!sql.Db_Show_My_Ticket(user_tel, res)) {
send_false();
} else {
send(fd, res.toStyledString().c_str(),
strlen(res.toStyledString().c_str()), 0);
}
} else if (i == CancelAppoin) {
string user_tel = val["user_tel"].asString();
string yd_id = val["yd_id"].asString();
if (!sql.Db_Cancel_Ticket(user_tel, yd_id)) {
send_false();
} else {
send_Ok();
}
} else {
tag = true;
}
}
}
}
if (tag) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
delete this;
} else {
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLONESHOT;
ev.data.ptr = this;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
}
Thread_Pool::Thread_Pool() {
if (!Init_Thread_pool()) {
exit(1);
}
Create_Thread();
}
Thread_Pool::~Thread_Pool() {
pthread_mutex_lock(&mutex);
status = false;
pthread_cond_broadcast(&cond); // 唤醒所有在 cond_wait 的线程
pthread_mutex_unlock(&mutex);
for (int i = 0; i < thread_num; i++) {
pthread_join(Thread_array[i], NULL);
}
free(dequeue);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
bool Thread_Pool::Init_Thread_pool() {
dequeue = (task_t *)malloc(sizeof(task_t) * d_size);
if (dequeue == nullptr) {
cout << "dequeue malloc failure" << endl;
return false;
}
dequeue_size = d_size;
front = 0;
rear = front;
task_Num = 0;
thread_Num = thread_num;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
status = true;
cout << "ThreadPool init success!" << endl;
cout << "---------------------------------------" << endl;
return true;
}
void Thread_Pool::add_Task(Socket *csocket) {
pthread_mutex_lock(&mutex);
if (task_Num >= dequeue_size) {
delete csocket;
cout << "dequeue_size is full!add_Task false!" << endl;
pthread_mutex_unlock(&mutex);
return;
}
dequeue[rear].task = csocket;
rear = (rear + 1) % dequeue_size;
task_Num++;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
void *General_Thread(void *arg) {
Thread_Pool *poo = (Thread_Pool *)arg;
poo->Work();
return NULL;
}
void Thread_Pool::Create_Thread() {
for (int i = 0; i < thread_num; i++) {
pthread_create(&Thread_array[i], NULL, General_Thread, this);
}
}
void Thread_Pool::Work() {
while (true) {
pthread_mutex_lock(&mutex);
while (task_Num == 0 && status == true) {
pthread_cond_wait(&cond, &mutex);
// 这是条件休眠,进入到条件变量的休眠队列,进入条件变量的休眠队列是会自动释放锁,这与sleep是不相同的。
} // 不能用if,要用while,以防虚假唤醒。
if (status == false) {
cout << "ThreadPool not run....." << endl;
break;
} // 处理线程池未启用的状态。
Socket *tmp = dequeue[front].task;
if (tmp == nullptr) {
cout << "tmp is nullptr" << endl;
break;
}
front = (front + 1) % dequeue_size;
task_Num--;
// cout<<"Thread id: "<<pthread_self()<<endl;
pthread_mutex_unlock(&mutex);
tmp->Do_client();
}
// 发生意外情况也要及时释放锁
pthread_mutex_unlock(&mutex);
}
void Mysql_config::Init_Mysql_config(const char *Sql_config_path) {
if (Sql_config_path == nullptr) {
cout << "config_path is nullptr!" << endl;
return;
}
FILE *fp = fopen(Sql_config_path, "r");
if (fp == nullptr) {
perror("fopen config file failed");
cout << "fopen config file failed! errno=" << errno << endl;
return;
}
char line[MAXLINE]{0};
int num = 0;
while (fgets(line, MAXLINE - 2, fp) != nullptr) {
num++;
if (line[0] == '#' || line[0] == '\n') {
continue;
}
char *Before = strtok(line, "=");
char *After = strtok(nullptr, "=");
if (Before == nullptr || After == nullptr) {
continue;
}
After[strlen(After) - 1] = '\0';
if (strcmp("dp_ip", Before) == 0) {
db_ip = After; // 禁止共享内存,line是一次性使用,After会被覆盖
} else if (strcmp("dp_port", Before) == 0) {
db_port = atoi(After);
} else if (strcmp("dp_name", Before) == 0) {
db_name = After;
} else if (strcmp("dp_mysql_path", Before) == 0) {
Sql_config_path = After;
} else {
cout << "unknown config item: " << Before << " at line " << num << endl;
}
memset(line, 0, sizeof(line));
}
fclose(fp);
print_Mysql_config();
}
void Mysql_config::print_Mysql_config() const {
cout << "db_ip: " << db_ip << endl;
cout << "db_port: " << db_port << endl;
cout << "db_name: " << db_name << endl;
cout << "config_path: " << Sql_config_path << endl;
}
void Mysql_Client::Send_full(){
string a = "MYSQL CLIENT BUSSY";
Json::Value value;
value["status"] = a;
send(who, value.toStyledString().c_str(),
strlen(value.toStyledString().c_str()), 0);
}
Mysql_Client::
Mysql_Client(struct Mysql_config *Sql_config,int who):who(who) {
connected = false;
mysql_con = nullptr;
if (Sql_config == nullptr) {
cout << "Mysql_config_path nullptr!" << endl;
Send_full();
return; // 不 exit, 让客户端收到 busy 后自己重试
}
file = (struct Mysql_config *)Sql_config;
if (!Connect_Mysql_Server()) {
cout << "---------------------------------------------" << endl;
cout << "connect mysql false (pool busy)" << endl;
cout << "---------------------------------------------" << endl;
Send_full(); // 给客户端发 "MYSQL CLIENT BUSSY"
return; // 不再继续, 后续 Db_* 也不会被调用
}
connected = true;
cout << "connect mysql succeed" << endl;
cout << "---------------------------------------------" << endl;
}
void Mysql_Client::Mysql_Begin() {
if (!connected) return;
if (mysql_query(mysql_con, "begin") != 0) {
cout << "事物启动失败!" << endl;
}
}
void Mysql_Client::Mysql_RollBack() {
if (!connected) return;
if (mysql_query(mysql_con, "rollback") != 0) {
cout << "事物回滚失败!" << endl;
}
}
void Mysql_Client::Mysql_Commit() {
if (!connected) return;
if (mysql_query(mysql_con, "commit") != 0) {
cout << "事物提交失败!" << endl;
}
}
bool Mysql_Client::Connect_Mysql_Server() {
pthread_mutex_lock(&g_pool->mutex);
int i=0;
while(i<thread_num){
if(g_pool->C_P_array[i].status==true)
{
mysql_con = g_pool->C_P_array[i].mysql_con;
g_pool->C_P_array[i].status=false;
break;
}
i++;
}
if(i>=thread_num){
pthread_mutex_unlock(&g_pool->mutex);
return false;
}
else{
g_pool->user++;
pthread_mutex_unlock(&g_pool->mutex);
return true;
}
}
bool Mysql_Client::Db_User_Register(const std::string &user_tel,
const std::string &user_name,
const std::string &user_passwd) {
if (!connected) return false;
string sql = string("insert into user_info values(0,'") + user_tel +
string("','") + user_name + string("','") + user_passwd +
string("',1,CURDATE())");
if (mysql_query(mysql_con, sql.c_str()) != 0) {
return false;
}
return true;
}
bool Mysql_Client::Db_User_Login(const std::string &tel, std::string &name,
const std::string &passwd) {
if (!connected) return false;
string sql = string("select Name,Passwd from user_info where Tel='") + tel +
string("'");
if (mysql_query(mysql_con, sql.c_str()) != 0) {
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == nullptr) {
return false;
}
int num = mysql_num_rows(r);
if (num == 0) {
return false;
}
MYSQL_ROW row = mysql_fetch_row(r);
if (row == nullptr) {
return false;
}
string username = row[0];
string userpasswd = row[1];
mysql_free_result(r);
if (userpasswd != passwd || name != username) {
return false;
}
return true;
}
bool Mysql_Client::Db_Show_Ticket(Json::Value &res) {
if (!connected) return false;
string sql = "select * from ticket_table where status=1";
if (mysql_query(mysql_con, sql.c_str()) != 0) {
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == nullptr) {
return false;
}
int num = mysql_num_rows(r);
if (num == 0) {
return false;
}
res["status"] = o;
res["num"] = num;
for (int i = 0; i < num; i++) {
MYSQL_ROW row = mysql_fetch_row(r);
Json::Value val;
val["ticket_id"] = atoi(row[0]);
val["ticket_name"] = row[1];
val["ticket_max"] = row[2];
val["ticket_count"] = row[3];
val["day_time"] = row[4];
res["ticket_arr"].append(val); // Json对象也可以添加进去
}
mysql_free_result(r);
return true;
}
bool Mysql_Client::Db_Yd_Ticket(std::string usertel, std::string tk_id) {
if (!connected) return false;
string sql_max_count =
string("select tk_max,count from ticket_table where Tk_id=") + tk_id;
if (mysql_query(mysql_con, sql_max_count.c_str()) != 0) {
cout << "[预约] 查询票信息失败: " << mysql_error(mysql_con)
<< " | SQL: " << sql_max_count << endl;
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == NULL) {
cout << "[预约] 取结果集失败: " << mysql_error(mysql_con) << endl;
return false;
}
int num = mysql_num_rows(r);
if (num == 0) {
cout << "[预约] 未找到 Tk_id=" << tk_id << " 的票记录" << endl;
mysql_free_result(r);
return false;
}
MYSQL_ROW row = mysql_fetch_row(r);
int max = atoi(row[0]);
int count = atoi(row[1]);
mysql_free_result(r);
if (count >= max) {
cout << "[预约] 票数不足 max=" << max << " count=" << count << endl;
return false;
}
count++;
Mysql_Begin();
// 一旦开启事物,这两条sql语句都成功执行的情况下,才会去进行提交事物的操作,
// 否则一旦有其中一个未成功,就算其中一个成功的操作也会撤销,二者立刻回滚,所有改动立即撤销,
string sql_setcount = string("update ticket_table set count=") +
to_string(count) + string(" where Tk_id=") + tk_id;
if (mysql_query(mysql_con, sql_setcount.c_str()) != 0) {
cout << "[预约] 更新票数失败: " << mysql_error(mysql_con)
<< " | SQL: " << sql_setcount << endl;
Mysql_RollBack();
return false;
}
string sql_yd = string("insert into yd_table values(0,'") + usertel +
string("','") + tk_id + string("',now())");
if (mysql_query(mysql_con, sql_yd.c_str()) != 0) {
cout << "[预约] 写入预约表失败: " << mysql_error(mysql_con)
<< " | SQL: " << sql_yd << endl;
Mysql_RollBack();
return false;
}
Mysql_Commit();
cout << "[预约] 成功: user_tel=" << usertel << " Tk_id=" << tk_id
<< " 剩余=" << (max - count) << endl;
return true;
}
bool Mysql_Client::Db_Show_My_Ticket(const std::string &user_tel,
Json::Value &res) {
if (!connected) return false;
string sql =
string("select y.yd_id, t.tk_name, t.tk_max, t.`count`, t.DateTime ") +
string("from yd_table y, ticket_table t ") +
string("where y.user_tel='") + user_tel + string("' and y.tk_id=t.Tk_id");
if (mysql_query(mysql_con, sql.c_str()) != 0) {
cout << mysql_error(mysql_con) << endl;
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == nullptr) {
res["status"] = o;
res["num"] = 0;
return true;
}
int num = mysql_num_rows(r);
res["status"] = o;
res["num"] = num;
for (int i = 0; i < num; i++) {
MYSQL_ROW row = mysql_fetch_row(r);
Json::Value val;
val["yd_id"] = row[0];
val["ticket_name"] = row[1];
val["ticket_max"] = row[2];
val["ticket_count"] = row[3];
val["day_time"] = row[4];
res["my_ticket_arr"].append(val);
}
mysql_free_result(r);
return true;
}
bool Mysql_Client::Db_Cancel_Ticket(const std::string &user_tel,
const std::string &yd_id) {
if (!connected) return false;
string sql_select = string("select tk_id from yd_table where yd_id=") +
yd_id + string(" and user_tel='") + user_tel +
string("'");
// 因为我们删除的时候是删除我们自己的预约表的最前面的那个编号,而不是票的票号,所以我们要额外的去存储这个票的票号,对这个票的数据进行减一的操作。
if (mysql_query(mysql_con, sql_select.c_str()) != 0) {
cout << "查询预约记录失败: " << mysql_error(mysql_con) << endl;
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == nullptr) {
return false;
}
int num = mysql_num_rows(r);
if (num == 0) {
mysql_free_result(r);
return false; // 预约记录不存在,无法取消
}
MYSQL_ROW row = mysql_fetch_row(r);
string tk_id = row[0];
mysql_free_result(r);
Mysql_Begin();
string sql_del = string("delete from yd_table where yd_id=") + yd_id;
if (mysql_query(mysql_con, sql_del.c_str()) != 0) {
cout << mysql_error(mysql_con) << endl;
Mysql_RollBack();
return false;
}
string sql_update =
string("update ticket_table set `count`=`count`-1 where Tk_id=") + tk_id;
if (mysql_query(mysql_con, sql_update.c_str()) != 0) {
cout << mysql_error(mysql_con) << endl;
Mysql_RollBack();
return false;
}
Mysql_Commit();
string sql_reset = "ALTER TABLE yd_table AUTO_INCREMENT = 1";
if (mysql_query(mysql_con, sql_reset.c_str()) != 0) {
cout << mysql_error(mysql_con) << endl;
}
return true;
}
Mysql_Client::~Mysql_Client() {
pthread_mutex_lock(&g_pool->mutex);
for(int i=0;i<thread_num;i++){
if(mysql_con==g_pool->C_P_array[i].mysql_con){
g_pool->C_P_array[i].status=true;
g_pool->user--;
}
}
pthread_mutex_unlock(&g_pool->mutex);
}
Connect_Pool::Connect_Pool(Mysql_config *Sql_config) {
if (Sql_config == nullptr) {
cout << "Connect Sqlconfig == nullptr!" << endl;
return;
}
user = 0;
status = true;
MAX_USER = thread_num;
this->Sql_config = Sql_config;
C_P_array = new Mysql_cli[thread_num];
Reset();
pthread_mutex_init(&mutex,NULL);
if(!Run()){
cout<<"Connect_Pool Run failed!"<<endl;
Reset();
exit(1);
}
cout<<"-------------------------"<<endl;
cout<<"Connect_Pool Run success!"<<endl;
cout<<"-------------------------"<<endl;
}
bool Connect_Pool::Run() {
if (status == false) {
return false;
}
for (int i = 0; i < thread_num; i++) {
MYSQL *mysql = mysql_init(C_P_array[i].mysql_con);
if (mysql == nullptr) {
return false;
}
mysql = mysql_real_connect(mysql, Sql_config->db_ip.c_str(), "root",
"111111", Sql_config->db_name.c_str(),
Sql_config->db_port, NULL, 0);
if (mysql == nullptr) {
return false;
}
C_P_array[i].mysql_con = mysql;
C_P_array[i].status = true;
}
return true;
}
void Connect_Pool::Reset(){
for(int i=0;i<thread_num;i++){
if(C_P_array[i].mysql_con!=nullptr){
mysql_close( C_P_array[i].mysql_con);
}
C_P_array[i].mysql_con= nullptr;
C_P_array[i].status = false;
}
}
Connect_Pool::~Connect_Pool(){
for(int i = 0;i<thread_num;i++){
mysql_close(C_P_array[i].mysql_con);
}
delete C_P_array;
pthread_mutex_destroy(&mutex);
}
void Tcp_Service::Init(const Service_Config *config) {
if (config == nullptr) {
cout << "config is nullptr!" << endl;
return;
}
this->config = config;
Status = true;
pool = new Thread_Pool;
Mysql_config file;
file.Init_Mysql_config(Sql_Conf_path);
lis_fd = socket(AF_INET, SOCK_STREAM, 0);
if (lis_fd == START) {
perror("socket failed");
cout << "socket failed! errno=" << errno << endl;
exit(1);
}
// listen socket 设为非阻塞,accept 队列空时返回 EAGAIN 而不是阻塞
int flags = fcntl(lis_fd, F_GETFL, 0);
fcntl(lis_fd, F_SETFL, flags | O_NONBLOCK);
// 因为是lis_fd为空导致的阻塞,所以我们这个将模式设为非阻塞,在此之后,accept会直接返回EAGAIN或EWOULDBLOCK,而不是阻塞线程
// 不会阻塞
int opt = 1;
setsockopt(lis_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(config->IP.c_str());
server_addr.sin_port = htons(config->Port);
config->PrintConfig();
if (bind(lis_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
cout << "bind failed! errno=" << errno << endl;
exit(1);
}
if (listen(lis_fd, config->Lismax) < 0) {
perror("listen failed");
cout << "listen init error! errno=" << errno << endl;
exit(1);
}
int epfd =
epoll_create1(EPOLL_CLOEXEC); // 防止fork版本公用一个文件描述符的情况
if (-1 == epfd) {
perror("epoll_create1 failed");
cout << "create epoll error! errno=" << errno << endl;
close(lis_fd);
exit(1);
}
ep_fd = epfd;
cout << "epoll success!" << endl;
struct epoll_event ev_listen;
ev_listen.events = EPOLLIN;
ev_listen.data.fd = lis_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, lis_fd, &ev_listen) == -1) {
perror("epoll_ctl add listen fd failed");
cout << "epoll_ctl add false! errno=" << errno << endl;
close(lis_fd);
exit(1);
}
cout << "Tcp service success!" << endl;
cout << "------------------------------------------------------" << endl;
while (Status) {
int ready_count = epoll_wait(epfd, ready_events, MAXLINE, -1);
//- 内核 :负责把发生事件的 fd 放入就绪双链表(这是中断/回调驱动的,随时发生)
// epoll_wait :负责从就绪双链表取出事件,拷贝给用户态的 ready_events
if (ready_count == -1) {
perror("epoll_wait failed");
cout << "epoll_wait error! errno=" << errno << endl;
close(lis_fd);
exit(1);
}
for (int i = 0; i < ready_count; i++) {
int tmfd = ready_events[i].data.fd;
if (tmfd == lis_fd) {
auto csocket = new AccSocket(lis_fd, epfd);
pool->add_Task(csocket);
} else {
auto csocket = ready_events[i].data.ptr;
if (csocket == nullptr) {
cout << "csocket is nullptr" << endl;
continue;
}
pool->add_Task((Socket *)csocket);
}
}
}
}
int main() {
file.Init_Mysql_config(Sql_Conf_path);
file.print_Mysql_config();
cout << "-----------------------------------------" << endl;
g_pool = new Connect_Pool(&file);
Service_Config a;
a.ReadConfig(a.config_path);
cout << "-----------------------------------------" << endl;
Tcp_Service tcp_service(&a);
delete g_pool;
return 0;
}
文件作用
实现 service.h 里所有类的方法 + main 函数(约 935 行)。
核心流程
main()
├─ file.Init_Mysql_config() // 读 mysql.conf
├─ g_pool = new Connect_Pool(&file) // 建连接池(5 个连接)
├─ a.ReadConfig() // 读 service.conf
└─ Tcp_Service tcp_service(&a) // 进入主循环
├─ new Thread_Pool // 起 5 个 worker
├─ listen + bind + socket // 起 TCP 服务
└─ while (Status) { // epoll 主循环
epoll_wait()
├─ if listen fd → new AccSocket → add_Task
└─ else → 取 RecvSocket* → add_Task
}
关键方法详解
Tcp_Service::Init (L815-L917)
主控函数:建线程池 → 建 listen fd(设非阻塞 + SO_REUSEADDR)→ bind → listen → epoll_create1 → epoll_ctl ADD listen fd → while(Status) 循环 epoll_wait。
Thread_Pool::Work (L338-L366)
worker 线程主循环:pthread_mutex_lock → while(task_Num==0) cond_wait → 取出 dequeue[front].task → unlock → tmp->Do_client() 多态分发。
RecvSocket::Do_client (L164-L260)
- recv 数据 → JSON 解析 → 构造
Mysql_Client sql(&file, fd) - 检查
sql.IsConnected():- false(池满)→ 跳过 Db_* 调用,构造里已
Send_full()发了 BUSSY - true → 按
val["type"]走 6 个业务分支
- false(池满)→ 跳过 Db_* 调用,构造里已
- 结束后
epoll_ctl MOD重新监听(EPOLLONESHOT)
Mysql_Client::Connect_Mysql_Server (L474-L499)
加锁遍历 C_P_array,找到 status==true 的 slot → mysql_con = C_P_array[i].mysql_con → status=false → user++ → 返回 true。借不到返回 false。
Mysql_Client::~Mysql_Client (L740-L749)
加锁遍历 C_P_array,找到 mysql_con 相等的 slot → status=true → user-- 归还。
Connect_Pool::Run (L776-L798)
循环 5 次:mysql_init → mysql_real_connect → 存进 C_P_array[i].mysql_con + status=true。
Connect_Pool::Reset (L801-L809)
循环 5 次:若 mysql_con != nullptr 则 mysql_close,然后置 nullptr + status=false。
业务方法
Db_User_Register(L501) --- 插入 user_infoDb_User_Login(L517) --- select Name,Passwd 比对Db_Show_Ticket(L552) --- select * 拼成 JSONDb_Yd_Ticket(L585) --- 事务:查 max/count → 更新 count → 插 yd_tableDb_Show_My_Ticket(L647) --- JOIN yd_table + ticket_tableDb_Cancel_Ticket(L684) --- 事务:查 tk_id → 删 yd → count-1
设计亮点
Mysql_Client::IsConnected()让 Do_client 可以无侵入处理"池满"Send_full()直接发 BUSSY 给客户端 fd,避免在 Do_client 多发一次 false- 事务在
Db_Yd_Ticket/Db_Cancel_Ticket中正确 begin/commit/rollback - listen fd 设非阻塞 + accept 返回 EAGAIN 时不报错静默退出(多 worker 竞争保护)
不足
Mysql_Client::mysql_con之前是野指针(已加mysql_con = nullptr;初始化修好)day-timevsday_time字段名不一致(已修)Connect_Pool::Run失败时局部 mysql 指针丢失(OS 兜底)- SQL 全字符串拼接,存在注入风险
4.3 <client.h> --- 客户端类声明
完整代码
cpp
#include <stdlib.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <jsoncpp/json/json.h>
#include <mysql/mysql.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <unistd.h>
#define IPADRESS "127.0.0.1"
#define PORTADRESS 8888
#define START -1
#define Length 1024
#define k "ok"
#define BUSY "MYSQL CLIENT BUSSY" // 服务器连接池满时返回的标记
class Client{
private:
short Port;
int conn_fd;
int User_Login_way;
bool Running;
bool Dl_Status;
std::string IP;
std::string user_name;
std::string user_tel;
public:
Client();
~Client();
void Client_Init();
void Print_Config();
void Run();
void Show_Menu();
void User_Register();
void User_login();
void Show_Ticket();
void Yd_Ticket();
void Show_My_Ticket();
void Qx_Ticket();
};
文件作用
声明 Client 类,定义客户端常量(IP/PORT/Buff 大小)。
关键成员
cpp
#define IPADRESS "127.0.0.1"
#define PORTADRESS 8888
#define Length 1024
#define k "ok"
#define BUSY "MYSQL CLIENT BUSSY" // 服务器连接池满时返回的标记
Client 类
| 成员 | 作用 |
|---|---|
conn_fd |
TCP 连接 fd |
Dl_Status |
登录状态 |
Running |
主循环开关 |
user_name / user_tel |
当前用户信息 |
方法
Client_Init()--- socket + connectShow_Menu()--- 显示菜单(登录前后不同)User_Register / User_login / Show_Ticket / Yd_Ticket / Show_My_Ticket / Qx_Ticket--- 6 个业务Run()--- 主循环 switch 分发
4.4 <client.cpp> --- 客户端实现
完整代码
cpp
#include "client.h"
#include <jsoncpp/json/value.h>
#include <ostream>
#include <sys/socket.h>
#include <iostream>
using namespace std;
enum OP_TYPE{
Login = 1,
Register,
CheckApoint,
Appointment,
AppoinMessage,
CancelAppoin,
Exit
};
Client::Client()
{
IP=IPADRESS;
Port = PORTADRESS;
conn_fd = START;
Dl_Status = false;
Running = true;
Client_Init();
}
Client::~Client()
{
if(conn_fd != START){
close(conn_fd);
}
cout<<"Client close"<<endl;
}
void Client::Client_Init(){
int sock_fd = socket(AF_INET,SOCK_STREAM,0);
if(sock_fd<=0){
cout<<"Client socket Error!"<<endl;
exit(1);
}
conn_fd = sock_fd;
Print_Config();
struct sockaddr_in Client_sock_ad;
memset(&Client_sock_ad,0,sizeof(Client_sock_ad));
Client_sock_ad.sin_family = AF_INET;
Client_sock_ad.sin_addr.s_addr=inet_addr(IPADRESS);
Client_sock_ad.sin_port = htons(PORTADRESS);
if(connect(sock_fd,(const struct sockaddr* )&Client_sock_ad,sizeof(Client_sock_ad))==START){
cout<<"connect failed!"<<endl;
close(sock_fd);
exit(1);
}
cout<<"connect succed!"<<endl;
cout<<" client start begin "<<endl;
cout<<"------------------------------------------------------"<<endl;
Run();
}
void Client::Print_Config(){
cout<<"IP: "<<IP<<endl;
cout<<"Port: "<<Port<<endl;
cout<<"conn_fd: "<<conn_fd<<endl;
cout<<"this: "<<this<<endl;
}
void Client::Show_Menu(){
if(!Dl_Status){
cout <<"---用户名:游客---------状态:未登录-----"<<endl;
cout << "1 登陆 2 注册 3 退出" << endl;
cout << "---------------------------------" << endl;
cout << "请输入选项编号:" << endl;
}
else{
cout << "---用户名:" << user_name << "--------状态:已登陆------" << endl;
cout << "1 查看预约 2 预定 " << endl;
cout << "3 查看我的预约 4 取消预定 " << endl;
cout << " 5 退出" << endl;
cout << "---------------------------------" << endl;
cout << "请输入选项编号:" << endl;
cout << "---------------------------------" << endl;
}
}
void Client::User_Register(){
cout<<"请输入手机号码:"<<endl;
cin>>user_tel;
cout<<"请输入用户名:"<<endl;
cin>>user_name;
cout<<"请输入密码"<<endl;
string passwd;
cin>>passwd;
Json::Value mess;
mess["type"] = Register;
mess["user_tel"] = user_tel;
mess["user_name"] = user_name;
mess["passwd"] = passwd;
send(conn_fd,mess.toStyledString().c_str(),strlen(mess.toStyledString().c_str()),0);
char buff_status[Length] = {0};
int n = recv(conn_fd,buff_status,Length-1,0);
if(n<0){
cout<<"ser err"<<endl;
return;
}
Json::Value Acmess;
Json::Reader Read;
if(!Read.parse(buff_status,Acmess)){
cout<<"Json Read error!"<<endl;
return;
}
string st = Acmess["status"].asString();
if(st == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return ;
}
if(st.compare(k)!=0){
cout<<"Register false!"<<endl;
return ;
}
cout<<"Register success!"<<endl;
Dl_Status = true;
}
void Client::User_login(){
cout<<"请输入手机号码"<<endl;
cin>>user_tel;
cout<<"请输入用户名"<<endl;
cin>>user_name;
cout<<"请输入密码"<<endl;
string passwd;
cin>>passwd;
Json::Value val;
val["type"] = Login;
val["user_tel"] = user_tel;
val["passwd"] = passwd;
val["user_name"] = user_name;
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff_status[Length] = {0};
int n = recv(conn_fd,buff_status,Length-1,0);
if(n<0){
cout<<"ser err"<<endl;
return;
}
Json::Value Acmess;
Json::Reader Read;
if(!Read.parse(buff_status,Acmess)){
cout<<"Json Read error!"<<endl;
return;
}
string st = Acmess["status"].asString();
if(st == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return ;
}
if(st.compare(k)!=0){
cout<<"Login false!"<<endl;
return ;
}
cout<<"Login success!"<<endl;
Dl_Status = true;
}
void Client::Show_Ticket(){
Json::Value val;
val["type"] = CheckApoint;
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff[Length]={0};
int n = recv(conn_fd,buff,Length-1,0);
if(n<=0){
cout<<"ser close"<<endl;
return;
}
Json::Value res_val;
Json::Reader Read;
if(!Read.parse(buff,res_val)){
cout<<"Json 解析失败"<<endl;
return;
}
string s = res_val["status"].asString();
if(s == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return;
}
if(s.compare(k)!=0){
cout<<"查看预约信息失败"<<endl;
return;
}
int num = res_val["num"].asInt();
if(num<0){
cout<<"暂时没有可以预约的信息"<<endl;
return ;
}
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
cout<<"| 序号 | 名称 | 总票数 | 已预定 | 时间 |"<<endl;
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
for(int i=0;i<num;i++){
printf("| %8s | %12s | %6s | %6s | %10s |\n",
res_val["ticket_arr"][i]["ticket_id"].asString().c_str(),
res_val["ticket_arr"][i]["ticket_name"].asString().c_str(),
res_val["ticket_arr"][i]["ticket_max"].asString().c_str(),
res_val["ticket_arr"][i]["ticket_count"].asString().c_str(),
res_val["ticket_arr"][i]["day_time"].asString().c_str());
}
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
}
void Client::Yd_Ticket(){
Show_Ticket();
cout<<"请输入需要预订的序号"<<endl;
int index;
cin>>index;
Json::Value val;
val["type"] = Appointment;
val["user_tel"] = user_tel;
val["ticket_id"] = to_string(index);
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff[Length] = {0};
int n = recv(conn_fd,buff,Length-1,0);
if(n<0){
cout<<"ser close"<<endl;
return ;
}
Json::Value res_val;
Json::Reader Read;
if(!Read.parse(buff,res_val)){
cout<<"Json 解析失败"<<endl;
return;
}
string s = res_val["status"].asString();
if(s == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return;
}
if(s.compare(k)!=0){
cout<<"预订失败"<<endl;
return;
}
cout<<"预订成功!"<<endl;
}
void Client::Show_My_Ticket(){
Json::Value val;
val["type"] = AppoinMessage;
val["user_tel"] = user_tel;
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff[Length]={0};
int n = recv(conn_fd,buff,Length,0);
if(n<=0){
cout<<"ser close"<<endl;
return;
}
Json::Value res_val;
Json::Reader Read;
if(!Read.parse(buff,res_val)){
cout<<"Json 解析失败"<<endl;
return;
}
string s = res_val["status"].asString();
if(s == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return;
}
if(s.compare(k)!=0){
cout<<"查看我的预约失败"<<endl;
return;
}
int num = res_val["num"].asInt();
if(num == 0){
cout<<"您还没有预约任何信息"<<endl;
return ;
}
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
cout<<"| 预约ID | 名称 | 总票数 | 已预定 | 时间 |"<<endl;
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
for(int i=0;i<num;i++){
printf("| %8s | %12s | %6s | %6s | %10s |\n",
res_val["my_ticket_arr"][i]["yd_id"].asString().c_str(),
res_val["my_ticket_arr"][i]["ticket_name"].asString().c_str(),
res_val["my_ticket_arr"][i]["ticket_max"].asString().c_str(),
res_val["my_ticket_arr"][i]["ticket_count"].asString().c_str(),
res_val["my_ticket_arr"][i]["day_time"].asString().c_str());
}
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
}
void Client::Qx_Ticket(){
Show_My_Ticket();
cout<<"请输入需要取消的预约ID:"<<endl;
string yd_id;
cin>>yd_id;
Json::Value val;
val["type"] = CancelAppoin;
val["user_tel"] = user_tel;
val["yd_id"] = yd_id;
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff[Length] = {0};
int n = recv(conn_fd,buff,Length-1,0);
if(n <= 0){
cout<<"ser close"<<endl;
return ;
}
Json::Value res_val;
Json::Reader Read;
if(!Read.parse(buff,res_val)){
cout<<"Json解析失败"<<endl;
return ;
}
string s = res_val["status"].asString();
if(s == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return ;
}
if(s.compare(k)!=0){
cout<<"取消预约失败"<<endl;
return ;
}
cout<<"取消预约成功"<<endl;
}
void Client::Run(){
while(Running){
Show_Menu();
int tmp;
cin>>tmp;
if(tmp < Login || tmp > Exit){
cout<<"输入错误"<<endl;
continue;
}
if(Dl_Status){
tmp = tmp+2;
}
else if(tmp==3){
tmp = Exit;
}
switch(tmp){
case Login:
User_login();
break;
case Register:
User_Register();
break;
case CheckApoint:
Show_Ticket();
break;
case Appointment:
Yd_Ticket();
break;
case AppoinMessage:
Show_My_Ticket();
break;
case CancelAppoin:
Qx_Ticket();
break;
case Exit:
Running = false;
break;
}
}
}
int main(){
Client client;
return 0;
}
文件作用
实现 Client 类所有方法 + main(约 410 行)。
通信协议(JSON)
客户端发送:
json
{ "type": 1, "user_tel": "...", "passwd": "...", "user_name": "..." }
服务端返回(成功):
json
{ "status": "ok", "num": 3, "ticket_arr": [...] }
服务端返回(失败):
json
{ "status": "false" }
服务端返回(池满):
json
{ "status": "MYSQL CLIENT BUSSY" }
6 个业务方法
| 方法 | type | 流程 |
|---|---|---|
User_Register |
2 | 输入 tel/name/passwd → send → recv → BUSY?/ok? |
User_login |
1 | 同上 |
Show_Ticket |
3 | 不带参数 → recv 列表 → 表格打印 |
Yd_Ticket |
4 | 先 Show_Ticket → 输入序号 → send → recv |
Show_My_Ticket |
5 | 带 user_tel → recv → 表格 |
Qx_Ticket |
6 | 先 Show_My_Ticket → 输入 yd_id → send → recv |
菜单逻辑
cpp
switch(tmp) {
case Login: User_login(); break;
case Register: User_Register(); break;
...
case Exit: Running = false; break;
}
设计亮点
- 统一 BUSY 检查模板:6 个方法都先判断
s == BUSY提示"服务器繁忙",再判断s != k提示具体失败 - 业务方法可组合(Yd_Ticket 复用 Show_Ticket,Qx_Ticket 复用 Show_My_Ticket)
不足
recv阻塞,无超时(服务端挂起客户端会一直等)cin >> tmp没检查失败状态(输入非数字会陷入死循环)- 表格用
printf硬编码列宽,长名会错位 - 全局 enum
OP_TYPE与服务端重复定义(应放共享头)
4.5 --- 构建脚本
完整代码
makefile
all:service client
service:service.cpp
g++ -g -O0 -o service service.cpp -lpthread -lmysqlclient -ljsoncpp
client:client.cpp
g++ -g -O0 -o client client.cpp -lpthread -lmysqlclient -ljsoncpp
clean:
rm -f *.o service client
依赖说明
| 库 | 用途 |
|---|---|
-lpthread |
pthread_t / mutex / cond |
-lmysqlclient |
MySQL C API |
-ljsoncpp |
JSON 解析 |
改进建议
- 加
-Wall -Wextra开启警告 - 加
-std=c++11(用了auto、nullptr) - 拆分头文件后改用
.o中间产物 + 自动依赖
4.6 <ser_start.sh> --- 启动脚本
完整代码
bash
#!/bin/bash
if [ -f Makefile ]
then
make clean
make all
fi
if [ -f service ]
then
./service service.conf
fi
不足
- 没加
set -e,make 失败仍会启动旧 service - 没传
service.conf路径给程序读(代码里也硬编码了) - 没用
nohup后台运行 + 日志重定向
4.7 <service.conf> --- 服务端配置
完整内容
conf
ip=127.0.0.1
port=8888
lismax=1024
service_conf_path=/home/xiaoc/mycode/csproject/service.conf
注意
service_conf_path字段在代码里赋值给局部参数,无实际作用- 修改后必须 Ctrl+S 保存磁盘才生效(之前 IDE 缓冲区与磁盘不一致问题)
4.8 <mysql.conf> --- 数据库配置
完整内容
conf
dp_ip=127.0.0.1
dp_port=3306
dp_name=c2411db
dp_mysql_path=/home/xiaoc/mycode/csproject/mysql.conf
注意
dp_mysql_path路径写错(少了 "2"),但代码里赋值给局部参数,无实际作用- 缺少
dp_user/dp_password字段,密码硬编码在 <service.cpp#L787>
五、Bug 修复记录(本次版本)
| Bug | 描述 | 状态 |
|---|---|---|
| 1 | Connect_Mysql_Server 中 i 不递归导致死循环 |
✅ 已修 |
| 2 | Run() 没把 mysql 保存到 C_P_arrayi.mysql_con |
✅ 已修 |
| 3 | status 语义反(true 应表示可用) | ✅ 已修 |
| 4 | 借出后没设 status=false | ✅ 已修 |
| 5 | Mysql_Client 构造失败仍执行 SQL | ✅ 已修 |
| 6 | Thread_Pool 构造 delete this |
✅ 已修 |
| 7 | 借/还不加锁 | ✅ 已修 |
| A | Mysql_Client::mysql_con 构造时未初始化为 nullptr | ✅ 已修 |
| B | day-time vs day_time 字段名不匹配 |
✅ 已修 |
| C | Connect_Pool::Run 失败时局部 mysql 指针丢失 | ⚠️ 不修(OS 兜底) |
| D | Tcp_Service::Init 局部 file 遮蔽全局 | ✅ 已修 |
六、改进路线图(按优先级)
| 优先级 | 改进项 | 影响 |
|---|---|---|
| P0 | SQL 注入修复(prepared statement) | 安全 |
| P0 | 密码哈希存储 | 安全 |
| P1 | 连接池加条件变量 | 性能/体验 |
| P1 | signal handler 优雅退出 | 健壮性 |
| P1 | 配置文件路径可配置 | 可移植 |
| P2 | 头文件拆分 | 可维护性 |
| P2 | 全局变量改单例 | 可维护性 |
| P2 | 日志分级 | 运维 |
| P3 | 客户端 recv 超时 | 健壮性 |
| P3 | 管理员后台 | 功能扩展 |
| P3 | send/recv 改 ET 模式 | 性能 |