一、先讲整体:第六节课的预约功能到底在做什么
这节课你们不是一下子就把"预约成功"全部做完,而是先把预约功能拆成了两步。
第一步
客户端先告诉服务器:
我要看现在有哪些票可以预约。
服务器收到后,把数据库里的票务信息查出来,整理后发给客户端显示。
这个阶段对应的函数就是:
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 数组中,最后把包含 status、num 和 ticket_arr 的 JSON 返回给客户端。客户端收到后,会先检查状态码,再检查票务数量,最后遍历数组并把票务信息打印出来。这样,预约功能的第一步就形成了完整闭环:客户端发请求,服务器查库并封装 JSON 数组,客户端解析并展示预约列表。
简短背诵版
第六节课先做预约功能的第一步:查看可预约列表。客户端发送 type="YUYUE" 给服务器,服务器查询 ticket_table,再通过 Read_YuYue() 把数据库结果集封装成包含 status、num 和 ticket_arr 的 JSON 返回。ticket_arr 是 JSON 数组,里面每个元素都是一张票的对象。客户端收到后再解析并打印列表。
十、最后给你一个"死记版"
-
预约功能不是一步完成,而是先看列表,再正式预约。
-
今天完成的是第一步
Show_ticket()。 -
多条票务信息要用 JSON 数组传。
-
ticket_arr里的每个元素,本质上都是一个 JSON 对象。 -
Read_YuYue()的作用,是把数据库结果集封装成 JSON 协议。 -
客户端已经能完整接收、校验、解析并显示预约列表。
你要是愿意,我下一条可以继续把这一节再整理成你最喜欢的那种格式:
预约 分两步 第一步 先预约 将能预约的信息打印出来,第二步 选择预约哪一个信息 ,对应的函数
第一步:客户端要给服务器端发一个信息,告诉它要预约。 然后服务器 会给客户端 回一个可以预约的信息。(这算一次交互)查看可预约的信息 Show_ticket()
第二部:根据服务器反馈的信息,选择一条我们要预约的,给服务器端
服务器给我们回复状态YD_ticket()

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