一、这节课在整个项目里干什么
我们现在做的是预约系统,但当前阶段先不做数据库,先把客户端和服务器端的通信框架搭起来。
上节课我们已经完成了:
-
封装监听套接字
Lis_Socket -
让服务器具备
socket -> bind -> listen的能力
这节课继续补全:
-
LibEvent类 -
回调基类
CallBack -
Accept_CallBack -
Recv_CallBack -
事件注册
-
事件循环
-
数据接收与回复
本节课最终实现的效果
-
客户端成功连接服务器
-
客户端发送数据
-
服务器接收数据
-
服务器回复
"ok" -
客户端收到
"ok"
也就是说:
客户端---服务器端最基础的通信主线已经跑通。
二、这节课的主线流程
复习时先记这 8 步:
-
服务器先创建监听套接字
-
服务器初始化
LibEvent -
把监听套接字注册到
LibEvent -
客户端连接服务器
-
服务器执行
accept(),得到新的连接套接字 -
把新的连接套接字继续注册到
LibEvent -
客户端发送数据
-
服务器
recv()收到数据,并回复"ok"
一句话记忆
监听 socket 负责接人,连接 socket 负责聊天,LibEvent 负责统一调度。
三、通俗理解每个类在干什么
1. Lis_Socket:服务器门口
这个类专门负责服务器监听套接字。
它干的事情很简单,就是三步:
-
socket():开一个 socket -
bind():绑定地址和端口 -
listen():开始监听
你可以这样记
Lis_Socket = 服务器门口
作用就是:
站在门口等客户端来。
最重要的代码模板
cpp
class Lis_Socket
{
private:
int sockfd;
string ip;
short port;
public:
Lis_Socket()
{
sockfd = -1;
ip = "127.0.0.1";
port = 6000;
}
bool Bind();
int Get_Sockfd()
{
return sockfd;
}
};
Bind() 模板代码
这是你以后最起码要会默写的服务器监听模板:
cpp
bool Lis_Socket::Bind()
{
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
cout << "socket err" << endl;
return false;
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = inet_addr(ip.c_str());
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (res == -1)
{
cout << "bind err" << endl;
return false;
}
res = listen(sockfd, Max_listen);
if (res == -1)
{
cout << "listen err" << endl;
return false;
}
return true;
}
这一段你面试怎么说
Lis_Socket类主要用来封装服务器监听套接字,完成socket、bind、listen三个基础步骤,让服务器具备等待客户端连接的能力。
2. LibEvent:事件管理员
这个类是这节课真正补全的重点。
它的作用不是通信,而是:
谁有事了,我负责发现并叫对应的人去处理。
它主要有三个函数:
Init()
初始化事件系统
你可以理解成:
把事件管理中心搭起来
Lib_Add(fd, p)
给某个描述符 fd 注册事件,并绑定一个处理对象 p
你可以理解成:
把"谁出事了交给谁处理"这个关系记下来
Dispatch()
启动事件循环
你可以理解成:
事件管理员正式开始上班
插图位置说明:
放在这一小节后面最合适,因为这张图讲的就是 LibEvent 背后的原理。
图下配文:
图 1 展示了 libevent 的基本流程:先通过 event_init() 创建事件管理器 base,再通过 event_add() 注册事件,接着通过 event_base_dispatch(base) 进入事件循环,最后释放事件和 base。本项目中的 LibEvent 类,本质上就是对这套流程的封装。
LibEvent 类模板代码
cpp
class LibEvent
{
public:
LibEvent()
{
base = NULL;
}
bool Init()
{
base = event_init();
if (base == NULL)
{
return false;
}
return true;
}
bool Lib_Add(int fd, CallBack *p);
bool Dispatch();
private:
struct event_base* base;
};
Lib_Add() 模板代码
这个函数非常重要,建议记住大致样子:
Dispatch() 模板代码
cpp
bool LibEvent::Dispatch()
{
if (event_base_dispatch(base) == -1)
{
return false;
}
return true;
}
这一段你面试怎么说
LibEvent类是对 libevent 的封装,主要负责事件系统初始化、事件注册和事件循环。它本身不处理业务,而是负责事件监听和分发。
3. CallBack:统一回调基类
这个类本身不干具体工作,它只是规定:
所有处理事件的类,都必须有一个 CallBack_Fun()
所以它像一个统一标准。
你可以这样记
CallBack = 所有事件处理类的共同模板
模板代码
cpp
class CallBack
{
public:
virtual void CallBack_Fun() = 0;
struct event *ev;
};
为什么要有这个类
因为服务器里不同事件处理逻辑不同:
-
新连接来了,要
accept() -
数据来了,要
recv()
但是它们又有共同点:
都是事件来了以后,调用一个函数处理。
所以就抽象出一个基类。
面试说法
CallBack是统一回调基类,作用是定义统一接口,方便后续用继承和多态分别处理不同类型的 socket 事件。
4. Accept_CallBack:专门接人
这个类专门处理:
监听套接字上的事件
也就是:
-
有客户端来连接了
-
就执行
accept() -
接收这个新客户端
-
再把这个新客户端交给后续的数据处理类
你可以这样记
Accept_CallBack = 接待员
作用是:
负责把新来的客户端接进来
类模板代码
cpp
class Accept_CallBack : public CallBack
{
public:
Accept_CallBack(int fd, LibEvent *plib)
{
sockfd = fd;
m_base = plib;
}
void CallBack_Fun();
private:
int sockfd;
LibEvent *m_base;
};
核心处理函数模板
cpp
void Accept_CallBack::CallBack_Fun()
{
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
if (c < 0)
{
return;
}
Recv_CallBack *r = new Recv_CallBack(c);
m_base->Lib_Add(c, r);
}
这一段怎么理解
这段代码的核心意思就是:
有新客户端来了,我先接住他,再把他交给专门负责通信的人。
面试说法
Accept_CallBack负责处理监听套接字上的连接事件,当有新客户端连接时,通过accept()获取新的连接套接字,并继续把它注册到事件系统中。
5. Recv_CallBack:专门收消息
这个类专门处理:
连接套接字上的数据接收
也就是:
-
客户端发数据来了
-
就执行
recv() -
收到数据后打印出来
-
再回复
"ok"
你可以这样记
Recv_CallBack = 聊天的人
作用是:
负责和已经连上的客户端通信
类模板代码
cpp
class Recv_CallBack : public CallBack
{
public:
Recv_CallBack(int fd)
{
c = fd;
}
void CallBack_Fun();
private:
int c;
};
核心处理函数模板
cpp
void Recv_CallBack::CallBack_Fun()
{
char buff[128] = {0};
int n = recv(c, buff, 127, 0);
if (n <= 0)
{
event_free(ev);
close(c);
delete this;
return;
}
printf("recv(%d):%s\n", c, buff);
send(c, "ok", 2, 0);
}
这一段怎么理解
这段代码的核心意思就是:
如果对方发消息了,我就接收;如果连接断了,我就清理资源。
面试说法
Recv_CallBack负责处理连接套接字上的数据接收事件,通过recv()获取客户端数据,并通过send()给客户端返回确认信息。
四、为什么要这样设计
这部分是这节课真正的知识点,复习和面试都很重要。
1. 为什么要封装 LibEvent
因为服务器后面要面对多个连接、多种事件。
如果不用事件驱动,代码会越来越乱。
所以这里引入 libevent,再封装成 LibEvent 类。
一句话记忆
socket 负责通信,LibEvent 负责监听和调度。
2. 为什么要区分监听套接字和连接套接字
监听套接字
只负责:
等客户端连接
连接套接字
只负责:
和客户端聊天
一句话记忆
-
监听套接字:接人
-
连接套接字:聊天
3. 为什么要设计 CallBack 基类
因为不同 socket 的处理逻辑不一样:
-
监听 socket:
accept() -
连接 socket:
recv()
所以不能写一个大函数把所有逻辑都揉在一起。
最好的方式就是:
-
先定义统一基类
-
再写不同子类分别处理
这就是:
继承 + 多态
这里插入【图 2:CallBack / Accept / Recv 关系图】
插图位置说明:
放在这一节后面最合适,因为这张图正好解释为什么要设计 CallBack 基类,以及它和两个子类的关系。
图下配文:
图 2 展示了回调类的继承关系。CallBack 作为统一基类,定义 CallBack_Fun() 接口;Accept_CallBack 用来处理监听套接字上的新连接事件;Recv_CallBack 用来处理连接套接字上的数据接收事件。外部统一按 CallBack* 管理,事件触发时通过多态执行不同子类中的逻辑。
五、结合代码理解整条流程
这一节建议你重点背,因为这是最适合面试复述的部分。
第一步:服务器先把门口搭起来
cpp
Lis_Socket ser;
ser.Bind();
意思是:
-
创建监听 socket
-
绑定
127.0.0.1:6000 -
开始监听
第二步:事件管理员开始准备
cpp
LibEvent *plib = new LibEvent();
plib->Init();
意思是:
-
创建
LibEvent -
初始化
event_base
第三步:给门口配一个接待员
cpp
Accept_CallBack *pacb = new Accept_CallBack(ser.Get_Sockfd(), plib);
plib->Lib_Add(ser.Get_Sockfd(), pacb);
意思是:
-
监听套接字交给
LibEvent -
一旦有新连接,就由
Accept_CallBack处理
第四步:启动事件循环
cpp
plib->Dispatch();
意思是:
-
服务器正式开始运行
-
持续等待事件发生
第五步:客户端连接时
-
监听 socket 触发事件
-
调用
Accept_CallBack::CallBack_Fun() -
执行
accept() -
生成新的连接 socket
-
创建
Recv_CallBack -
再把新的连接 socket 注册进事件系统
第六步:客户端发数据时
-
新连接 socket 触发事件
-
调用
Recv_CallBack::CallBack_Fun() -
执行
recv() -
服务器收到数据
-
再
send("ok")回复客户端
六、主函数模板代码
这部分建议你会写,至少会照着写。
cpp
int main()
{
Lis_Socket ser;
if (!ser.Bind())
{
cout << "bind err" << endl;
exit(1);
}
LibEvent *plib = new LibEvent();
if (plib == NULL)
{
cout << "new LibEvent err" << endl;
exit(1);
}
if (!plib->Init())
{
cout << "LibEvent init err" << endl;
exit(1);
}
Accept_CallBack *pacb = new Accept_CallBack(ser.Get_Sockfd(), plib);
if (pacb == NULL)
{
cout << "new accept callback err" << endl;
exit(1);
}
plib->Lib_Add(ser.Get_Sockfd(), pacb);
plib->Dispatch();
return 0;
}
这一段怎么记
主函数就是做三件事:
-
开门
-
注册事件
-
开始等事发生
七、最应该会写的模板代码清单
面试真让你手写,你最起码要会下面这些模板。
1. 服务器监听 socket 模板
cpp
socket();
bind();
listen();
2. sockaddr_in 初始化模板
cpp
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = inet_addr(ip.c_str());
3. LibEvent::Lib_Add() 模板
cpp
event_new(base, fd, EV_READ | EV_PERSIST, G_CallBack_Fun, p);
event_add(ev, NULL);
4. accept() 处理模板
cpp
int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
5. recv() / send() 模板
cpp
int n = recv(c, buff, 127, 0); send(c, "ok", 2, 0);
八、课堂笔记版总结
本节主题
补全服务端通信框架,实现客户端与服务器端最基础的数据收发。
本节目标
-
服务器基于
libevent启动 -
利用多态处理监听套接字和连接套接字
-
客户端成功连接服务器
-
客户端发送数据,服务器接收并回复
本节核心类
-
Lis_Socket:监听 socket,负责等客户端连接 -
LibEvent:事件管理类,负责初始化、注册事件、事件循环 -
CallBack:回调基类,统一定义CallBack_Fun() -
Accept_CallBack:处理新连接 -
Recv_CallBack:处理接收数据
本节最重要思想
-
监听 socket 和连接 socket 职责不同
-
不同事件逻辑不同,所以用回调基类 + 子类实现继承和多态
-
libevent负责监听和调度,socket 负责通信
本节结果
已经实现客户端连接服务器、发送数据,服务器接收数据并回复 "ok",说明最基础的通信主线已经跑通。
九、面试时怎么说
你直接背这一段都可以:
这个预约系统项目第一阶段,我主要负责客户端和服务器端通信框架的搭建。服务端先封装了监听套接字类
Lis_Socket,完成socket、bind、listen。在此基础上,又封装了LibEvent类来管理事件,并设计了回调基类CallBack以及两个子类:Accept_CallBack用来处理新连接,Recv_CallBack用来处理客户端数据接收。这样通过继承和多态,把不同类型 socket 的处理逻辑分开。最终实现了客户端连接服务器、发送数据,服务器接收并回复ok,为后续 JSON 通信和数据库业务处理打下了基础。
十、最短背诵版
这节课在上节课监听套接字封装的基础上,补全了
LibEvent类和回调类体系。通过CallBack基类以及Accept_CallBack、Recv_CallBack两个子类,分别处理新连接和数据接收,最终实现了客户端连接服务器、发送数据,服务器接收并回复ok。
一、先看这张图在表达什么
图里有这几个东西:
-
struct event -
fd -
CallBack类 -
accept_CallBack -
recv_CallBack -
CallBack_Fun()
它想表达的是:
每个被监听的描述符 fd
都会对应一个 回调对象
而这个回调对象虽然统一看成 CallBack 类型,
但实际可能是不同子类,比如:
-
accept_CallBack -
recv_CallBack
这样当事件发生时,就能根据对象真实类型,执行不同处理逻辑。
二、这张图背后的问题是什么
在服务器里,不同的 fd 干的事不一样。
比如:
1. 监听套接字的 fd
它的作用是:
-
有客户端连接时
-
调用
accept() -
生成新的连接套接字
所以它对应的处理逻辑应该是:
处理新连接
2. 已连接套接字的 fd
它的作用是:
-
接收客户端发来的数据
-
调用
recv()
所以它对应的处理逻辑应该是:
处理收数据
如果不用类来区分,你就得在一个大函数里不停判断:
-
这个
fd是监听套接字吗 -
还是连接套接字
-
如果是监听就
accept -
如果是连接就
recv
这样代码会越来越乱。
三、所以才有了这个设计
这张图表达的就是:
先定义一个统一的基类 CallBack
比如里面规定一个统一接口:
virtual void CallBack_Fun() = 0;
意思是:
所有回调类都必须提供一个
CallBack_Fun()函数
这个基类本身不处理具体业务,它只是定规则。
再定义不同子类
accept_CallBack
专门处理监听套接字事件
它的 CallBack_Fun() 里做的事一般是:
-
accept() -
得到新的连接套接字
-
再把新套接字注册到事件系统
recv_CallBack
专门处理连接套接字事件
它的 CallBack_Fun() 里做的事一般是:
-
recv() -
接收客户端发来的数据
-
做后续处理
四、这张图中 struct event -- fd -- 类 是什么意思
这里的意思可以理解成:
一个 event
本质上是在监听某个 fd
而这个 fd 又会绑定一个对应的 回调类对象
也就是:
-
event负责"监听事件" -
fd表示"监听谁" -
CallBack对象表示"出了事谁来处理"
所以这三者关系可以理解成:
event 监听 fd,fd 对应一个回调对象,事件发生时调用这个对象的方法
五、这张图真正体现的思想:多态
这是最重要的一点。
外部统一都是:
CallBack* p
也就是说,表面上它们都被当成 CallBack 类型看待。
但实际上传进去的可能是:
-
accept_CallBack -
recv_CallBack
当事件发生时,统一调用:
p->CallBack_Fun();
虽然写法一样,但因为对象实际类型不同,所以执行结果不同:
-
如果
p指向accept_CallBack,就执行处理连接的逻辑 -
如果
p指向recv_CallBack,就执行处理接收的逻辑
这就是 多态。
六、这张图的作用是什么
它的作用主要有三个:
1. 把不同事件的处理逻辑分开
监听连接和接收数据不是一回事,拆开后更清楚。
2. 让事件处理更容易扩展
以后如果你还要加别的事件,比如:
-
发送数据事件
-
定时器事件
-
管理员命令事件
你只需要再写新的 CallBack 子类就行,不用大改原有代码。
3. 让 libevent 和 C++ 类结合起来
libevent 本身偏 C 风格,
但你们项目想用面向对象的方式来组织代码。
所以这套设计就是在做一件事:
用 C++ 的类对象,去承接 C 风格事件回调
七、你可以把这张图翻译成一句很容易懂的话
不同的描述符干的事情不一样,所以给它们绑定不同的回调类对象;外部统一按
CallBack类型管理,事件发生时再通过多态调用各自真正的处理函数。

