服务器预约系统linux小项目-第六节课

一、先讲整体:第六节课的预约功能到底在做什么

这节课你们不是一下子就把"预约成功"全部做完,而是先把预约功能拆成了两步。

第一步

客户端先告诉服务器:

我要看现在有哪些票可以预约。

服务器收到后,把数据库里的票务信息查出来,整理后发给客户端显示。

这个阶段对应的函数就是:

cpp 复制代码
Show_ticket()

第二步

客户端看到列表以后,再从中选一个具体的票,比如某个 ticket_id,再发给服务器,服务器才真正处理预约。

这个阶段对应的是:

cpp 复制代码
YD_ticket()

所以你一定要先记住一句话:

第六节课真正完成的重点,是预约功能的第一步:先把可预约列表显示出来。

二、先讲完整流程:预约第一步是怎么跑起来的

你现在先不要急着看代码,先把流程背熟。

预约第一步完整流程

客户端发请求

→ 告诉服务器"我要查看可预约信息"

→ 服务器收到请求

→ 根据 type 判断这是预约查看业务

→ 服务器查询数据库里的票务表

→ 把多条票务信息封装成 JSON 数组

→ 服务器把 JSON 发回客户端

→ 客户端解析 JSON

→ 把票务列表一条一条打印出来

这一整条链路,就是第六节课的核心。

三、先讲客户端:Show_ticket() 是怎么发起请求的

客户端这一步不是"提交预约",而是"请求查看可预约列表"。

1)先看代码

cpp 复制代码
void Client::Show_ticket()
{
    // 定义一个 JSON 对象 a
    // 这个对象用来表示客户端要发送给服务器的请求包
    Json::Value a;

    // type = "YUYUE"
    // 表示这次请求的业务类型是"预约相关业务"
    // 这里先表示"我要查看当前可预约的信息"
    a["type"] = "YUYUE";

    // 把 JSON 对象序列化成字符串
    string send_str = a.toStyledString();

    // 通过 socket 把字符串发送给服务器
    send(sockfd, send_str.c_str(), strlen(send_str.c_str()), 0);
}

这就是客户端发起"查看预约列表"的核心代码。

2)这段代码在做什么

第一部分:创建 JSON 对象

cpp 复制代码
Json::Value a;

这里的 a 就是一个 JSON 对象。

你可以把它理解成一个"快递箱子",客户端要往里面装数据,然后发给服务器。

第二部分:设置业务类型

cpp 复制代码
a["type"] = "YUYUE";

这一步非常关键。

因为服务器不是自动知道你这次想干什么。

客户端必须通过 type 字段告诉服务器:

我这次不是登录,不是注册,而是预约相关请求。

所以这里写:

cpp 复制代码
"type" = "YUYUE"

就是在给服务器打标签。


第三部分:序列化并发送

cpp 复制代码
string send_str = a.toStyledString();
send(sockfd, send_str.c_str(), strlen(send_str.c_str()), 0);

这里做了两件事:

第一,toStyledString()

把 JSON 对象变成字符串。

第二,send()

通过 socket 发送给服务器。

为什么不能直接发 a

因为网络传输只能发字节流,不能直接发一个 C++ 里的 JSON 对象。

所以必须先变成字符串,再发出去。

3)这段代码在项目流程中的位置

这段代码在整个预约流程里,属于起点

也就是说:

  • 这是客户端第一次发起预约业务

  • 它不是选票

  • 它只是先问服务器:"能预约的票有哪些?"

所以这一步的本质,不是"预约成功",而是:

先申请拿到一份可预约列表。

四、服务端收到请求后,怎么判断该进预约业务

服务端还是沿用你前面登录、注册时的老套路:

  • recv() 收包

  • parse() 反序列化

  • 再看 type

  • 再根据 type 分发到不同函数

1)先看业务映射表代码

cpp 复制代码
Recv_CallBack::Recv_CallBack(int fd)
{
c = fd;

// 注册业务
cho_table.insert(make_pair("ZC", ZC));

// 登录业务
cho_table.insert(make_pair("DL", DL));

// 预约业务
cho_table.insert(make_pair("YUYUE", YUYUE));

// 查看业务
cho_table.insert(make_pair("CK", CK));

// 取消预约业务
cho_table.insert(make_pair("QXYY", QXYY));
}

2)这段代码在做什么

这里本质上是在建立一张"翻译表"。

比如:

  • 客户端发 "ZC",服务器知道你要注册

  • 客户端发 "DL",服务器知道你要登录

  • 客户端发 "YUYUE",服务器就知道你现在是预约业务

所以这里的作用可以概括成一句话:

把 JSON 里的业务类型字符串,映射成程序内部要执行的业务逻辑。


3)再看业务分发代码

cpp 复制代码
switch (cho_type)
{
case ZC:
Zc_User();
break;

case DL:
Dl_User();
break;

case YUYUE:
Show_ticket();
break;

case CK:
Ck_User();
break;

case QXYY:
Qxyy_User();
break;

default:
Send_err();
break;
}

4)这段代码在做什么

这一步就是服务端的"总调度中心"。

当服务器发现:

cpp 复制代码
type = "YUYUE"

就会进入:

cpp 复制代码
Show_ticket();

这说明:

客户端发起预约查看请求以后,服务端会进入 Show_ticket() 来处理。

所以 Show_ticket() 是服务端预约第一步的核心函数。

五、服务端:Show_ticket() 到底在做什么

这是这一节最关键的函数之一。

你可以先用一句话记住它的作用:

Show_ticket() 的职责,就是把数据库里的票务列表查出来,封装成 JSON,再发给客户端。


1)先看完整代码

cpp 复制代码
void Recv_CallBack::Show_ticket()
{
// 第一步:创建数据库对象
// 后面所有数据库操作都通过这个对象完成
Mysql_Client mysqlcli;

// 第二步:初始化 MySQL
if (!mysqlcli.Init())
{
Send_err();
return;
}

// 第三步:连接数据库服务器
if (!mysqlcli.Connect_mysql_ser())
{
Send_err();
return;
}

// 第四步:执行查询 SQL
// 从票务表 ticket_table 中查出所有票务信息
string sql = string("select * from ticket_table");
if (!mysqlcli.Query_sql(sql))
{
Send_err();
mysqlcli.Close();
return;
}

// 第五步:定义一个 JSON 对象 res_val
// 用来保存最终要返回给客户端的预约列表
Json::Value res_val;

// 第六步:把数据库结果集封装进 JSON
if (!mysqlcli.Read_YuYue(res_val))
{
Send_err();
mysqlcli.Close();
return;
}

// 第七步:数据库任务完成后关闭连接
mysqlcli.Close();

// 第八步:把 JSON 对象序列化成字符串
string res_str = res_val.toStyledString();

// 第九步:通过 socket 发给客户端
send(c, res_str.c_str(), strlen(res_str.c_str()), 0);
}

这段代码就是预约第一步的服务端核心逻辑。


2)逐段讲解这段代码

第一部分:创建数据库对象

cpp 复制代码
Mysql_Client mysqlcli;

这一步说明:

服务端并没有直接把所有数据库操作都写在 Show_ticket() 里面,

而是先创建一个数据库类对象,通过它去做:

  • 初始化

  • 连接

  • 查表

  • 读取结果

  • 关闭连接

这就是"封装"的思想。

你可以把 Mysql_Client 理解成一个专门帮你和数据库打交道的"中间人"。


第二部分:初始化和连接数据库

cpp 复制代码
if (!mysqlcli.Init())
{
Send_err();
return;
}

if (!mysqlcli.Connect_mysql_ser())
{
Send_err();
return;
}

这两步和前面登录时一模一样:

  • Init():先把 MySQL 客户端对象准备好

  • Connect_mysql_ser():真正连到数据库服务器

也就是说,这节课虽然业务从"登录"切到了"预约",

但数据库操作的大框架没有变。


第三部分:执行 SQL 查询票务表

cpp 复制代码
string sql = string("select * from ticket_table");
if (!mysqlcli.Query_sql(sql))
{
Send_err();
mysqlcli.Close();
return;
}

这条 SQL 非常重要。

它的意思是:

ticket_table 表里,把所有票务记录都查出来。

这里你要特别注意:

前面登录查的是用户表 user_info

现在预约查的是票务表 ticket_table

说明项目已经开始从"用户系统"走向"具体业务系统"了。


第四部分:为什么要定义 res_val

cpp 复制代码
Json::Value res_val;

这里的 res_val 是一个 JSON 对象,用来装最终发给客户端的数据。

也就是说:

  • 数据源来自数据库

  • 但不能把数据库结果集原样发给客户端

  • 必须先整理成客户端和服务器约定好的 JSON 协议格式

所以这一步的本质是:

先准备一个"响应包"。


第五部分:调用 Read_YuYue(res_val)

cpp 复制代码
if (!mysqlcli.Read_YuYue(res_val))
{
Send_err();
mysqlcli.Close();
return;
}

这是今天最关键的一步之一。

因为 SQL 虽然执行了,但结果还放在 MySQL 的结果集里。

你还没有真正把这些票务信息"读出来"。

Read_YuYue() 干的事,就是:

把数据库里的多条票务记录,一行一行读出来,并封装成 JSON 数组放进 res_val

这一步是今天最核心的协议转换过程。


第六部分:发给客户端

cpp 复制代码
string res_str = res_val.toStyledString();
send(c, res_str.c_str(), strlen(res_str.c_str()), 0);

和前面的登录、注册一样:

  • 先把 JSON 变成字符串

  • 再通过 socket 发送给客户端

也就是说,预约第一步到这里正式闭环:

客户端发查看请求

→ 服务器查票务表

→ 服务器返回票务列表。

六、今天最重要的知识点:Read_YuYue() 为什么这么写

如果说 Show_ticket() 是预约第一步的"主流程函数",

Read_YuYue() 就是这节课最有技术含量的"封装函数"。

因为它真正完成了:

把数据库结果集转成 JSON 数组。


1)先看完整代码

cpp 复制代码
bool Mysql_Client::Read_YuYue(Json::Value &res_val)
{
// 第一步:从 mysql_con 中取出 SQL 执行后的结果集
MYSQL_RES *r = mysql_store_result(&mysql_con);
if (r == NULL)
{
return false;
}

// 第二步:统计结果集中有多少条票务记录
int num = mysql_num_rows(r);

// 把状态和条数先写入返回 JSON
res_val["status"] = "OK";
res_val["num"] = num;

// 如果数据库里没有任何票务记录
// 也返回一个合法 JSON,只是 num = 0
if (num == 0)
{
mysql_free_result(r);
return true;
}

// 第三步:定义 row 用来一行一行读取数据库记录
MYSQL_ROW row;

// i 用来表示 JSON 数组下标
int i = 0;

// 第四步:循环读取结果集中的每一行
while ((row = mysql_fetch_row(r)) != NULL)
{
// 定义一个 JSON 对象 one_ticket
// 表示一张票的信息
Json::Value one_ticket;

// 把数据库表中的每一列,按字段名放进 JSON 对象
one_ticket["ticket_id"] = row[0];
one_ticket["ticket_name"] = row[1];
one_ticket["ticket_max"] = row[2];
one_ticket["count"] = row[3];
one_ticket["day_time"] = row[4];
one_ticket["status"] = row[5];

// 把这一张票对象,放进 ticket_arr 数组里
res_val["ticket_arr"][i] = one_ticket;
i++;
}

// 第五步:释放结果集内存
mysql_free_result(r);

return true;
}

这段代码就是把"数据库行"变成"JSON 数组"的关键函数。


2)逐段讲解这段代码

第一部分:先把结果集拿出来

cpp 复制代码
MYSQL_RES *r = mysql_store_result(&mysql_con);

前面的 Query_sql(sql) 只是让数据库执行了查询。

但查出来的数据,还在数据库内部。

mysql_store_result() 的作用就是:

把 SQL 查询后的结果集,真正拿到程序里。

你可以把它理解成:

  • Query_sql():去仓库里找货

  • mysql_store_result():把货从仓库里搬出来


第二部分:先看有几条票

cpp 复制代码
int num = mysql_num_rows(r);
res_val["status"] = "OK";
res_val["num"] = num;

这里先数结果集里一共有多少条记录。

然后先把两个字段写进 JSON:

  • status

  • num

这说明最终返回给客户端的 JSON 外层,至少有这两个成员:

cpp 复制代码
{
"status": "OK",
"num": 2
}

所以客户端后面可以先看:

  • 返回状态对不对

  • 一共有几条票务信息


第三部分:为什么 num == 0 也不算报错

cpp 复制代码
if (num == 0)
{
mysql_free_result(r);
return true;
}

这一步很容易被忽略,但特别重要。

如果数据库里当前没有票务信息,

程序并不是返回 false,也不是直接报错。

而是返回一个合法的 JSON,只不过:

cpp 复制代码
num = 0

这样客户端就可以根据这个结果提示:

当前没有可预约信息。

这是一种很好的"协议设计"思路:
没有数据,不等于程序出错。


第四部分:为什么要 while 循环读取

cpp 复制代码
while ((row = mysql_fetch_row(r)) != NULL)

因为你查询出来的不是一条票,而是很多条票。

所以这里不能像登录那样只读一行,

而是要一行一行读,直到结果集读完为止。

这一步说明:

预约列表是多条记录,不是单条记录。

这也正是这节课为什么要引入 JSON 数组的根本原因。


第五部分:一行数据库记录,怎么变成一个 JSON 对象

cpp 复制代码
Json::Value one_ticket;

one_ticket["ticket_id"] = row[0];
one_ticket["ticket_name"] = row[1];
one_ticket["ticket_max"] = row[2];
one_ticket["count"] = row[3];
one_ticket["day_time"] = row[4];
one_ticket["status"] = row[5];

这里就是"数据库字段"到"JSON 字段"的映射过程。

一行数据库记录,本来只是:

  • 第 0 列

  • 第 1 列

  • 第 2 列

  • 第 3 列

  • 第 4 列

  • 第 5 列

但客户端如果收到的只是 row[0] row[1] row[2] 这种形式,是看不懂的。

所以必须重新加上清晰的字段名。

于是就变成:

  • ticket_id

  • ticket_name

  • ticket_max

  • count

  • day_time

  • status

这一步本质上是在做:

把数据库里的"表格行",转成网络传输里的"结构化对象"。


第六部分:为什么要放进 ticket_arr[i]

cpp 复制代码
res_val["ticket_arr"][i] = one_ticket;
i++;

这句就是今天最关键的 JSON 数组操作。

含义是:

  • ticket_arr 是一个数组

  • 数组的第 i 个元素,是一张票的对象

也就是说:

cpp 复制代码
ticket_arr[0] = 第一张票
ticket_arr[1] = 第二张票
ticket_arr[2] = 第三张票

这就是为什么说:

一张票是一个 JSON 对象,很多张票组成一个 JSON 数组。


3)最后形成的 JSON 长什么样

最后服务端发给客户端的,大概会长这样:

cpp 复制代码
{
"status": "OK",
"num": 2,
"ticket_arr": [
{
"ticket_id": "1",
"ticket_name": "历史博物馆",
"ticket_max": "100",
"count": "20",
"day_time": "2026-04-01",
"status": "1"
},
{
"ticket_id": "2",
"ticket_name": "图书馆",
"ticket_max": "50",
"count": "10",
"day_time": "2026-04-02",
"status": "1"
}
]
}

这个结构你不用死记每个括号,

但你一定要记住三个关键成员:

  • status

  • num

  • ticket_arr

这三个字段就是客户端后面解析列表时的核心。


七、客户端收到以后,怎么解析并显示这些票务信息

这一步也很重要,因为客户端已经开始不只是"收包",而是要把结果真正展示出来。


1)先看代码

cpp 复制代码
string status = res_val["status"].asString();
if (status.compare("OK") != 0)
{
cout << "获取可预约信息失败" << endl;
return;
}

int ticket_num = res_val["num"].asInt();
if (ticket_num == 0)
{
cout << "当前没有可预约信息" << endl;
return;
}

cout << "当前可预约信息如下:" << endl;
for (int i = 0; i < ticket_num; i++)
{
Json::Value one = res_val["ticket_arr"][i];

cout << "票id: " << one["ticket_id"].asString() << " ";
cout << "名称: " << one["ticket_name"].asString() << " ";
cout << "最大票数: " << one["ticket_max"].asString() << " ";
cout << "已预约: " << one["count"].asString() << " ";
cout << "日期: " << one["day_time"].asString() << endl;
}

这段代码说明客户端已经形成了完整的"接收---校验---解析---显示"流程。


2)逐段讲解

第一步:先看状态码

cpp 复制代码
string status = res_val["status"].asString();
if (status.compare("OK") != 0)
{
cout << "获取可预约信息失败" << endl;
return;
}

这说明客户端不会一收到包就直接开始打印。

它会先判断协议是不是成功响应。

这就是一种"防御式编程":

先确认包是不是正常,再继续往下处理。


第二步:再看一共有几条票

cpp 复制代码
int ticket_num = res_val["num"].asInt();
if (ticket_num == 0)
{
cout << "当前没有可预约信息" << endl;
return;
}

这一步说明:

  • 如果服务器虽然成功返回了

  • 但数据库里确实没有票

  • 客户端就应该提示"没有可预约信息"

而不是盲目进入 for 循环。


第三步:循环遍历数组

cpp 复制代码
for (int i = 0; i < ticket_num; i++)
{
Json::Value one = res_val["ticket_arr"][i];
...
}

这里说明客户端已经知道:

  • ticket_arr 是数组

  • 数组里每个元素都是一张票的 JSON 对象

所以它一条一条取出来,再逐字段打印。


第四步:为什么不是直接打印整包 JSON

因为整包 JSON 虽然机器容易处理,但人看起来不够直观。

而你这里把它打印成:

  • 票 id

  • 名称

  • 最大票数

  • 已预约

  • 日期

这样更适合终端用户去看。

也就是说,这一步其实已经开始在做"界面展示层"的工作了。


八、这一节课真正学到的知识点,我帮你串起来

1. 预约功能要拆成两步

第六节课不是一步做到预约成功,而是先看列表,再正式预约。

今天完成的是第一步。

2. 多条同类数据必须用 JSON 数组

前面登录注册是单条对象,这里票务列表是多条对象,所以要用数组。

3. 一条数据库记录要转换成一个 JSON 对象

数据库里是一行一行的表格;

网络协议里则是一张票一个 JSON 对象。

4. Read_YuYue() 是数据库封装升级后的体现

前面数据库类主要负责"查";

现在数据库类开始负责"查完之后还要封装成 JSON 数组"。

5. 客户端解析层也开始变复杂

客户端不再只看一个 status

而是要解析:

  • status

  • num

  • ticket_arr

说明协议复杂度上升了。


九、最适合你复习的版本

详细复习版

第六节课主要实现了预约功能的第一步,也就是"查看可预约信息"。客户端通过 Show_ticket() 发送一个 type="YUYUE" 的 JSON 请求给服务器,表示自己要查看当前可预约的票务列表。服务器收到请求后,通过业务分发进入 Show_ticket(),连接数据库并执行 select * from ticket_table,从票务表中查询所有票务记录。由于返回结果不再是一条数据,而是多条票务信息,所以这节课引入了 JSON 数组。服务器通过 Read_YuYue() 把数据库结果集逐行读取出来,并把每一行封装成一个 JSON 对象,再统一放入 ticket_arr 数组中,最后把包含 statusnumticket_arr 的 JSON 返回给客户端。客户端收到后,会先检查状态码,再检查票务数量,最后遍历数组并把票务信息打印出来。这样,预约功能的第一步就形成了完整闭环:客户端发请求,服务器查库并封装 JSON 数组,客户端解析并展示预约列表。

简短背诵版

第六节课先做预约功能的第一步:查看可预约列表。客户端发送 type="YUYUE" 给服务器,服务器查询 ticket_table,再通过 Read_YuYue() 把数据库结果集封装成包含 statusnumticket_arr 的 JSON 返回。ticket_arr 是 JSON 数组,里面每个元素都是一张票的对象。客户端收到后再解析并打印列表。


十、最后给你一个"死记版"

  1. 预约功能不是一步完成,而是先看列表,再正式预约。

  2. 今天完成的是第一步 Show_ticket()

  3. 多条票务信息要用 JSON 数组传。

  4. ticket_arr 里的每个元素,本质上都是一个 JSON 对象。

  5. Read_YuYue() 的作用,是把数据库结果集封装成 JSON 协议。

  6. 客户端已经能完整接收、校验、解析并显示预约列表。

你要是愿意,我下一条可以继续把这一节再整理成你最喜欢的那种格式:

预约 分两步 第一步 先预约 将能预约的信息打印出来,第二步 选择预约哪一个信息 ,对应的函数

第一步:客户端要给服务器端发一个信息,告诉它要预约。 然后服务器 会给客户端 回一个可以预约的信息。(这算一次交互)查看可预约的信息 Show_ticket()

第二部:根据服务器反馈的信息,选择一条我们要预约的,给服务器端

服务器给我们回复状态YD_ticket()

服务器 创建数据库的表,用来记录所有票据信息。

相关推荐
爱学习的程序媛2 小时前
WSL2:Windows上运行Linux的完整指南
linux·运维·windows·ubuntu·wsl2
会飞的大可2 小时前
Jenkins 企业级集成实战:从规划到落地的完整指南
运维·jenkins
Are_You_Okkk_2 小时前
AI开源知识库跨部门闭环搭建,效率提升40%
大数据·运维·人工智能·架构·开源
Yupureki2 小时前
《Linux系统编程》20.常见程序设计模式
linux·服务器·c语言·c++·单例模式·建造者模式·责任链模式
M1nat0_2 小时前
Linux基础 Ext 文件系统:从磁盘硬件到目录路径的全链路解析
linux·服务器·网络·数据库
AIminminHu2 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(一-2-(3)-当你的协同CAD服务器面临“千人同屏”时:从单机优化到分布式高并发)
运维·服务器·分布式
上海云盾安全满满2 小时前
游戏被攻击了要如何选择防护,接高防服务器还是游戏盾
服务器·网络·游戏
舰长1152 小时前
Diffie-Hellman Key Agreement Protocol 资源管理错误漏洞(CVE-2022-40735)【原理扫描】openssl升级
运维·服务器
xxjj998a2 小时前
若依部署Nginx和Tomcat
运维·nginx·tomcat