一、项目介绍

我们现在做的项目是一个预约系统 。系统主要由 服务器端(server) 、管理员端(admin) 、客户端(cli) 和 MySQL 数据库 组成。
其中:
-
server 是系统核心,负责处理客户端请求、连接数据库、验证预约信息。
-
admin 主要负责设置预约信息,包括可预约人数、预约详细内容,也可以暂停预约和查看预约情况。
-
cli 是客户端,用户可以通过命令行完成登录、查询、查看已有预约、取消预约和退出系统等操作。
-
MySQL 用来保存预约系统中的各种数据,比如预约信息、用户信息和预约记录。
需要注意的是:
管理员和服务器不是直接耦合在一起的,它们之间没有直接关系,而是通过数据库进行关联。
整个系统可以理解为:
管理员设置数据 → 数据库存储 → 服务器读取并处理 → 客户端使用系统。
我们测试时可以先封装一个服务器端,然后做一个客户端
①让服务器端和客户端先连上用json来通信,只要能通讯不管是登录还是查询无非就是换内容就行。
②服务器收到数据后如何和数据库进行交互
二、数据库表设计

1. user_info(用户信息表)
存放用户的基本信息。
字段含义:
-
id_tel:用户账号或手机号 -
name:用户名 -
密码:登录密码 -
身份证:用户身份信息 -
状态:用户当前是否可正常使用系统,比如正常、禁用
2. ticket_table(预约信息表)
存放管理员设置的预约项目。
字段含义:
-
res_id:预约项目编号 -
res_name:预约项目名称 -
总数目:可预约总人数 -
已使用:已经预约的人数 -
是否开启预约:当前项目是否开放预约
3. query_res(预约记录表)
存放用户的预约结果。
字段含义:
-
id:用户编号 -
res_id:预约项目编号 -
是否使用过:该预约是否已经使用
要先将表设计好,定好需求。
三、现阶段任务以及系统整体结构
3.1目前的目标主要有两点:
-
服务器端使用 libevent 启动起来
-
客户端能够连接服务器,并通过 JSON 把数据发送给服务器
也就是说,现阶段先完成:
-
连接建立
-
事件处理
-
数据收发
等这一步跑通之后,再继续做数据库和具体业务。
四、服务器端
这里我们封装一个 libevent 类,用来实现服务器和客户端之间的连接与通信。

1. 为什么要封装 Libevent 类
服务器端不能只处理一个连接,而是要同时监听多个事件,所以这里使用 libevent 做事件驱动。
为了方便管理,我们把它封装成一个 Libevent 类。
1.1这个类主要提供三个核心功能:
-
初始化
启动 libevent,完成事件机制的基础准备。
-
注册事件
提供
add(fd, fun)方法,给某个文件描述符fd绑定对应的回调函数fun。也就是告诉系统:
-
监听哪个描述符
-
当这个描述符上有事件发生时,调用哪个函数处理
-
-
事件循环
事件循环启动后,libevent 会不断检测:①哪个描述符有事件②然后调用对应回调函数处理
一句话理解
libevent 启动后主要做两件事:监听描述符、为描述符绑定回调函数;然后进入事件循环,谁有事件就处理谁。
2.监听套接字和连接套接字
在服务器里,有两类很重要的套接字。

2.1 监听套接字 sock_listen
监听套接字的作用是:
专门负责等待客户端连接
它本身不负责和客户端真正通信,只负责监听"有没有新的客户端来连接服务器"。
所以服务器启动时,首先要创建监听套接字。
2.2 为什么监听套接字也要注册到 Libevent
因为 libevent 是事件驱动模型,谁要被监听,谁就要注册进去。
因此:
-
sock_listen也要注册到Libevent -
当有新的连接到来时,就会触发它对应的回调函数
2.3 新连接套接字的产生
当客户端连接服务器时:
-
监听套接字检测到连接事件
-
回调函数中调用
accept() -
accept()会返回一个新的套接字
这个新的套接字就是 连接套接字
2.4 连接套接字的作用
连接套接字不是负责监听新连接,而是负责:
-
接收客户端数据
-
给客户端返回数据
可以简单理解为:
-
sock_listen:负责"接人" -
新的连接套接字:负责"和这个人通信"
2.5 为什么新的连接套接字也要注册
因为客户端后续发消息,都是通过这个新连接套接字完成的。
所以:
-
每产生一个新的连接套接字
-
都要继续注册到
Libevent -
这样 libevent 才能继续监听它的读写事件
3.回调基类设计
3.1为什么要设计回调基类
服务器中会出现不同类型的事件,比如:
-
有新的客户端连接
-
某个客户端发送了数据
这两类事件处理逻辑不同,但本质上都属于:
事件发生后,需要执行一个回调函数
所以这里可以抽象出一个统一的基类。
3.2call_back 基类
定义一个回调基类:
call_back
在这个类中定义统一接口:
call_back_fun()
它的作用是:
规定所有回调类都必须实现这个函数。
基类只定义接口,不写具体处理逻辑。
3.3accept_call_back
这个类继承 call_back,主要负责处理:
监听套接字上的新连接事件
当监听套接字有事件时:
-
调用
accept() -
生成新的连接套接字
-
再把新的连接套接字注册到事件系统中
所以它负责的是:
连接建立
3.4recv_call_back
这个类也继承 call_back,主要负责处理:
连接套接字上的数据接收事件
当客户端发送数据时:
-
调用
recv() -
接收客户端发来的数据
-
后续再交给业务逻辑处理
所以它负责的是:
数据收发
3.5这样设计的意义
这种设计体现了 继承和多态 思想。
好处有:
-
统一回调接口
-
不同事件分开处理
-
结构更清晰
-
后续更容易扩展
也就是说:
-
基类负责定义规范
-
子类负责具体实现
-
libevent 根据不同事件调用不同对象的回调函数
四、代码展示
1.server.h
cpp
#include <iostream>
#include <string>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
// 监听队列的最大长度
const int Max_listen = 10;
// 监听套接字类
// 作用:封装服务器监听 socket,完成 socket 创建、绑定和监听
class Lis_Socket
{
private:
int sockfd; // 监听套接字描述符
string ip; // 监听的 IP 地址
short port; // 监听的端口号
public:
// 默认构造函数
// 这里只做成员变量初始化,不做容易失败的系统调用
Lis_Socket()
{
sockfd = -1; // -1 表示当前还没有创建 socket
ip = "127.0.0.1"; // 默认监听本机地址
port = 6000; // 默认监听端口
}
// 带参构造函数
// 可以自定义监听的 IP 和端口
Lis_Socket(const char* ips, short p)
{
sockfd = -1;
ip = string(ips);
port = p;
}
// 初始化监听套接字
// 完成三件事:
// 1. 创建 socket
// 2. 绑定 IP 和端口
// 3. 进入监听状态
bool bind()
{
// 1. 创建 TCP 套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
cout << "socket err" << endl;
return false;
}
// 2. 准备服务器地址信息
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET; // IPv4
saddr.sin_port = htons(port); // 端口转网络字节序
saddr.sin_addr.s_addr = inet_addr(ip.c_str()); // 字符串 IP 转网络地址
// 3. 将 socket 绑定到指定 IP 和端口
int res = ::bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (res == -1)
{
cout << "bind err" << endl;
return false;
}
// 4. 进入监听状态
res = listen(sockfd, Max_listen);
if (res == -1)
{
cout << "listen err" << endl;
return false;
}
return true;
}
// 获取监听套接字描述符
int Get_Sockfd()
{
return sockfd;
}
// 析构函数
// 对象销毁时关闭 socket,防止资源泄漏
~Lis_Socket()
{
if (sockfd != -1)
{
close(sockfd);
sockfd = -1;
}
}
};
2.server.cpp
cpp
#include "server.h"
#include <stdlib.h>
int main()
{
// 创建服务器监听对象
Lis_Socket ser;
// 初始化监听套接字
if (!ser.bind())
{
exit(1);
}
cout << "server start success..." << endl;
return 0;
}
五、本节课总结
这节课的核心目标,是先把 预约系统的客户端和服务器端通信框架 搭起来,而不是马上做数据库和完整业务。
整个项目后面会包括:
-
客户端
-
服务器端
-
管理员端
-
数据库
但当前阶段重点是先完成:客户端能连上服务器,服务器能接收客户端发来的数据。
1. 服务器端学了什么
服务器端这节课先封装了 监听套接字类 Lis_Socket,作用是完成服务器启动最基础的三步:
-
socket()创建 TCP 套接字 -
bind()绑定 IP 和端口 -
listen()开始监听客户端连接
也就是说,这一步先让服务器具备"在门口等客户端连接"的能力。
其中类里保存了:
-
sockfd:套接字描述符 -
ip:监听地址 -
port:监听端口
构造函数只负责初始化这些成员,真正可能失败的操作,比如创建 socket、绑定、监听,都放在普通成员函数里处理,这体现了封装思想。
2. 这节课还明确了两类套接字
监听套接字
专门负责等待新的客户端连接。
连接套接字
当客户端连进来后,通过 accept() 产生新的连接套接字,后续真正的数据收发是在这个套接字上完成的。
所以可以记成:
-
监听套接字负责"接人"
-
连接套接字负责"通信"
3. 为什么要用 Libevent
因为服务器后面不只处理一个客户端,还会处理多种事件,所以要用 libevent 做事件驱动。
这节课明确了 Libevent 类的三个核心功能:
-
初始化
-
注册事件
add(fd, fun) -
事件循环
dispatch()
它的作用就是统一管理"哪个描述符发生了什么事,并调用哪个回调函数处理"。
4. 回调类设计学了什么
为了处理不同类型的事件,这节课引入了 回调基类 call_back ,并定义统一接口 call_back_fun()。
然后再派生出不同子类:
-
accept_call_back:处理新连接事件 -
recv_call_back:处理接收数据事件
这样做的意义是:
-
把不同事件的处理逻辑分开
-
通过继承和多态统一管理回调
也就是:
虽然外部统一调用 call_back_fun(),但不同对象会执行不同逻辑。
5. 客户端这节课的目标
客户端当前阶段不用做复杂业务,重点是先完成:
-
连接服务器
-
从键盘读取输入
-
把数据封装成 JSON
-
发送给服务器
所以这一阶段客户端的重点不是业务功能,而是先把数据发过去。
6. 这节课整条主线
这节课完整流程可以概括为:
-
服务器先创建监听套接字并注册到
Libevent -
客户端发起连接
-
服务器通过回调函数处理连接事件,并生成新的连接套接字
-
新的连接套接字继续注册到事件系统
-
客户端把键盘输入封装成 JSON 发给服务器
-
服务器再通过接收回调函数把数据接住
到这里,客户端和服务器端最基础的通信框架就搭起来了。
7. 这节课的意义
这节课虽然还没有接数据库,也没有完成登录、查询、预约这些业务,但已经把预约系统最底层的通信骨架搭好了。
也就是说,后面数据库、用户功能、管理员功能,都是在这套 "客户端发送数据 + 服务器事件驱动接收数据" 的框架上继续往下做。
一段背诵版总结
这节课主要完成了预约系统第一阶段的通信框架搭建。服务器端通过封装监听套接字类完成了 socket、bind、listen 三个基础步骤,并明确了监听套接字和连接套接字的区别;同时引入 Libevent 管理事件,并通过回调基类及其子类实现不同事件的多态处理。客户端则负责连接服务器、读取键盘输入,并将数据封装成 JSON 发送给服务器。这样,整个系统最基础的客户端---服务器通信主线就建立起来了。