欢迎光临
我们一直在努力

C++ 预约系统实战(一):项目总览与 C/S 架构设计

摘要:本文是 C++ 预约系统实战系列的第一篇,从零拆解一个基于 TCP Socket + JSON 协议 + MySQL 的 C/S 架构项目。文章涵盖业务场景、整体架构、目录结构、核心类职责,并以“用户预约”为例完整走通一次请求的来龙去脉,最后给出技术选型理由和面试高频考点。

本文涉及的以下知识点,在之前的文章中已详细讲解:

  • Tcp/Socket 编程基础Linux TCP 网络编程完全指南:从三次握手到高并发服务器-CSDN博客

  • IO 多路复用(select / poll / epoll )

  • Libevent 事件驱动 → Libevent实战:高性能网络编程指南-CSDN博客

  • MySQL 基础(CRUD、事务、索引)→ MySQL 核心特性深挖:ACID、隔离级别、索引与视图的底层原理-CSDN博客

  • JSON 数据格式 → JSON 完全指南:从语法到项目实战-CSDN博客


一、这个项目要做什么

一句话:一个跑在终端里的预约系统。

用户可以注册、登录、查看可预约的资源(比如场馆、票务)、下单预约、查看自己的预约记录、取消预约。服务端负责业务逻辑和数据库读写,客户端只负责界面交互和网络收发。

功能清单如下:

功能客户端方法服务端处理数据库操作
注册 User_Resgister() User_Resgister() INSERT user_info
登录 User_Login() User_Login() SELECT user_info
查看可预约 User_Show_Ticket() User_Show_Ticket() SELECT ticket_info
预约 User_Subscribe_Ticket() User_Subscribe_Ticket() 事务:UPDATE + INSERT
查看我的预约 User_Show_Sub_Ticket() User_Show_Sub_Ticket() JOIN 联表查询
取消预约 User_Cancel_Sub_Ticket() User_Cancel_Sub_Ticket() 事务:UPDATE + DELETE

注意:这是一个学习项目,不会真正上线。它的价值在于:把「Socket 编程 + 协议设计 + 数据库操作 + 事件驱动」串成一条完整的工程链路,而不是停留在单点知识。


二、整体架构图

三层职责非常清晰:

  • 客户端:只管“界面 + 收发”,不做任何业务判断(除了输入合法性)。

  • 服务端:业务逻辑 + 协议解析 + 数据库操作,是真正的大脑。

  • 数据库:持久化,用外键约束兜底数据一致性。


三、目录结构与类职责

客户端:socket_client

class socket_client
{
private:
string ips; // 服务器 IP,默认 127.0.0.1
short port; // 服务器端口,默认 6000
int sockfd; // 通信套接字
bool dl_flg; // 是否已登录
string username; // 当前用户名
string usertel; // 当前用户手机号(后续请求要带上)
int user_op; // 用户当前选择的菜单项
bool runing; // 主循环开关
Json::Value m_val; // 复用的 JSON 对象
};

关键设计:

  • dl_flg 区分登录态:未登录时菜单只有 1/2/3,登录后变成 1/2/3/4/5。

  • usertel 是后续所有请求的“身份证”:预约、查看我的预约、取消预约都要带上它。

  • user_op 加 OFFSET:菜单输入 1~5,加上 OFFSET=2 后映射到枚举 CKYY(3)~QXYD(7),这样就能直接复用 switch。

const int OFFSET = 2;
enum OP_TYPE { DL=1, ZC, CKYY, YD, CKYD, QXYD, TC };

为什么要偏移?因为 DL=1, ZC=2 是未登录菜单,登录后菜单第 1 项应该对应 CKYY=3,所以 1 + OFFSET = 3。这个设计很隐晦,但省了一套枚举。

服务端:三个类

类职责
socket_listen 创建监听套接字、bind、listen,持有 event_base
socket_con 一个连接一个对象,负责收数据、解析 JSON、路由到业务函数、发响应
mysql_client 封装所有 SQL 操作,含事务 begin/commit/rollback

重点:socket_con 是每个连接 new 出来的,挂在 libevent 上。

socket_con *q = new socket_con(c);
struct event *c_ev = event_new(p->Get_base(), c, EV_READ | EV_PERSIST, SOCK_CON_CALLBACK, q);
q->Set_ev(c_ev);
event_add(c_ev, NULL);

当连接可读时,libevent 回调 SOCK_CON_CALLBACK → q->Recv_data()。处理完一个请求后,事件仍然挂着(EV_PERSIST),等下一个请求。

这已经是标准的 Reactor 模式,不是单线程阻塞。这一点在你之前发的概述文档里写错了,面试时要说清楚。


四、一次“预约”请求的完整生命周期

这是本文最核心的部分。以「用户预约」为例,从按下键盘到数据库落盘,完整走一遍。

时序图

关键代码逐段拆解

第 4 步:客户端构造 JSON 并发送

void socket_client::User_Subscribe_Ticket()
{
User_Show_Ticket(); // 先展示可预约列表
cout << "请输入要预定的编号:" << endl;
int index = 0;
cin >> index;

Json::Value val;
val["type"] = YD; // 操作类型:预约
val["tel"] = usertel; // 谁在预约
val["index"] = index; // 预约哪个

send(sockfd, val.toStyledString().c_str(),
strlen(val.toStyledString().c_str()), 0);

char buff[256] = {0};
int n = recv(sockfd, buff, 255, 0);
// … 解析响应
}

注意 toStyledString() 会生成带缩进和换行的 JSON,可读性好但浪费带宽。生产环境一般用 toFastString() 或直接 write 到 StreamWriterBuilder。

第 6 步:服务端解析并路由

void socket_con::Recv_data()
{
char buff[256] = {0};
int n = recv(c, buff, 255, 0);
if (n <= 0) { delete this; return; }

Json::Reader Read;
if (!Read.parse(buff, val)) { Send_err(); return; }

int ops = val["type"].asInt();
switch (ops)
{
case DL: User_Login(); break;
case ZC: User_Resgister(); break;
case CKYY: User_Show_Ticket(); break;
case YD: User_Subscribe_Ticket(); break;
case CKYD: User_Show_Sub_Ticket(); break;
case QXYD: User_Cancel_Sub_Ticket(); break;
default: break;
}
}

这就是一个极简的协议路由:type 字段是路由键,switch 是路由表。

第 9~13 步:数据库事务

bool mysql_client::mysql_Subscribe_Ticket(int tk_id, string tel)
{
mysql_user_begin(); // begin

string s1 = "select max,num from ticket_info where tk_id=" + to_string(tk_id);
mysql_query(&mysql_con, s1.c_str());
MYSQL_RES *r = mysql_store_result(&mysql_con);
MYSQL_ROW row = mysql_fetch_row(r);

int tk_max = atoi(row[0]);
int tk_num = atoi(row[1]);

if (tk_max <= tk_num) // 判断是否满员
{
mysql_user_rollback();
return false;
}

tk_num++;
string s2 = "update ticket_info set num=" + to_string(tk_num)
+ " where tk_id=" + to_string(tk_id);
mysql_query(&mysql_con, s2.c_str()); // 更新已预约数

string s3 = "insert into sub_ticket values(0,"
+ to_string(tk_id) + ",'" + tel + "',now())";
mysql_query(&mysql_con, s3.c_str()); // 插入预约记录

mysql_user_commit(); // commit
return true;
}

这段代码有几个面试必问的坑,我逐一标出来:

  • select 没加 for update → 并发下两个线程可能都查到 num=3,都判断通过,都执行 update,最终 num 变成 4 但实际插入了 2 条记录,超卖。

  • SQL 字符串拼接 → tel 如果传 ' or '1'='1,就能注入。

  • update 用绝对赋值 num=4 而不是 num=num+1 → 也是并发问题的根源。

  • 每个请求新建一个 MySQL 连接 → 高并发下连接数爆炸,应该用连接池。

  • 这些不是“错误”,而是学习项目的简化。面试时主动说出来,比被问倒强。


    五、技术选型理由

    选型理由面试怎么答
    C++ 贴近底层,能展示 Socket、内存管理、指针操作 “想真正理解网络编程和内存模型,而不是调库”
    TCP 可靠传输,面向连接,适合请求-响应模式 “预约不能丢请求,TCP 的重传和有序保证更合适”
    JSON 可读性好,jsoncpp 库成熟,开发快 “小规模系统 JSON 足够;protobuf 适合高性能、强 schema 场景”
    libevent 跨平台事件驱动,封装了 epoll/select “比裸 epoll 开发效率高,又比多线程模型更省资源”
    MySQL C API 直接写 SQL,理解底层 “不想被 ORM 屏蔽细节,想自己控制 SQL 和事务”

    一个关键点:为什么服务端用 libevent 而不是多线程?

    • 多线程模型:一个连接一个线程,代码直观,但线程切换开销大,连接数一多就崩。

    • libevent 模型:单线程事件循环,一个线程处理所有连接,资源占用低,适合 IO 密集型。

    • 本项目的业务逻辑很短(查库、写库),瓶颈在 IO 不在 CPU,所以事件驱动更合适。


    六、面试高频题

    Q1:为什么用 C++ 而不是 Java/Python 做这个项目?

    答:主要目的是学习。C++ 需要自己管理 Socket、内存、指针,能暴露更多底层细节。Java/Python 有成熟的框架,开发快,但会屏蔽网络编程的本质。另外 C++ 在面试中更能体现对内存和系统调用的理解。

    Q2:这个项目的架构有什么问题?

    答:至少四个:

  • 每个请求新建 MySQL 连接,应该用连接池;

  • 预约事务没加行锁,并发下可能超卖;

  • SQL 字符串拼接有注入风险,应该用预处理语句;

  • 客户端用固定缓冲区 recv,没处理 TCP 粘包/半包。

  • Q3:JSON 和 protobuf 怎么选?

    答:JSON 可读性好、调试方便、语言无关,适合小规模和对外接口;protobuf 序列化后体积小、解析快、有强 schema,适合内部高性能 RPC。本项目请求量小,JSON 够用。

    Q4:libevent 的 Reactor 模式是怎样的?

    答:Reactor 核心是“事件循环 + 回调”。libevent 把 fd 和事件注册到 event_base,底层用 epoll/select 监听。事件就绪时,event_base_dispatch 调用对应回调。本项目里监听 socket 的回调负责 accept,连接 socket 的回调负责 recv 和业务处理。


    七、一句话总结

    C/S 三层架构的本质,是让客户端只管交互、服务端只管逻辑、数据库只管存储,三层通过 Socket + JSON 和 MySQL C API 两条管道串联起来。

    赞(0)
    未经允许不得转载:171主机测评 » C++ 预约系统实战(一):项目总览与 C/S 架构设计
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址