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

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

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


一、这个项目要做什么

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

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

功能清单如下:

功能 客户端方法 服务端处理 数据库操作
注册 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

cpp 复制代码
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_opOFFSET :菜单输入 1~5,加上 OFFSET=2 后映射到枚举 CKYY(3)~QXYD(7),这样就能直接复用 switch

cpp 复制代码
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 创建监听套接字、bindlisten,持有 event_base
socket_con 一个连接一个对象,负责收数据、解析 JSON、路由到业务函数、发响应
mysql_client 封装所有 SQL 操作,含事务 begin/commit/rollback

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

cpp 复制代码
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_CALLBACKq->Recv_data()。处理完一个请求后,事件仍然挂着(EV_PERSIST),等下一个请求。

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


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

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

时序图

关键代码逐段拆解

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

cpp 复制代码
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() 或直接 writeStreamWriterBuilder

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

cpp 复制代码
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 步:数据库事务

cpp 复制代码
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;
}

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

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

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

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

  4. 每个请求新建一个 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:这个项目的架构有什么问题?

答:至少四个:

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

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

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

  4. 客户端用固定缓冲区 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 两条管道串联起来。

相关推荐
xxxiugou1231 小时前
双指针解题秘籍:从入门到精通
c语言·数据结构·c++·算法
我叫汪枫1 小时前
RAG 进阶:切分、检索、重排序,以及它和 Agent、微调、MCP 都是什么关系
开发语言·python
弘毅 失败的 mian1 小时前
数组和指针基础
c语言·开发语言·经验分享·笔记
ai小陈2 小时前
Python 3.12与CUDA 12.8镜像实战:启动GPU实例后的五项自检
开发语言·人工智能·python·深度学习·ai·gpu算力
Logic1012 小时前
C语言/数据结构位运算题解:异或XOR找出地铁规划中的“独特坐标“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
AZaLEan__2 小时前
ts接口梳理
开发语言
蓝桉柒72 小时前
java判断语句
java·开发语言
Lost of 程序猿2 小时前
M2 实战:询价与采购订单聚合落地——跨聚合一致性、领域事件与 Outbox
开发语言·c#·asp.net·.net
速易达网络2 小时前
用 Three.js 打造一座会呼吸的海岛:海风之屿飞艇巡航的技术实现
开发语言·javascript·ecmascript