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

一、这节课在整个项目里干什么

我们现在做的是预约系统,但当前阶段先不做数据库,先把客户端和服务器端的通信框架搭起来。

上节课我们已经完成了:

  • 封装监听套接字 Lis_Socket

  • 让服务器具备 socket -> bind -> listen 的能力

这节课继续补全:

  • LibEvent

  • 回调基类 CallBack

  • Accept_CallBack

  • Recv_CallBack

  • 事件注册

  • 事件循环

  • 数据接收与回复

本节课最终实现的效果

  • 客户端成功连接服务器

  • 客户端发送数据

  • 服务器接收数据

  • 服务器回复 "ok"

  • 客户端收到 "ok"

也就是说:

客户端---服务器端最基础的通信主线已经跑通。


二、这节课的主线流程

复习时先记这 8 步:

  1. 服务器先创建监听套接字

  2. 服务器初始化 LibEvent

  3. 把监听套接字注册到 LibEvent

  4. 客户端连接服务器

  5. 服务器执行 accept(),得到新的连接套接字

  6. 把新的连接套接字继续注册到 LibEvent

  7. 客户端发送数据

  8. 服务器 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 类主要用来封装服务器监听套接字,完成 socketbindlisten 三个基础步骤,让服务器具备等待客户端连接的能力。

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. 开门

  2. 注册事件

  3. 开始等事发生


七、最应该会写的模板代码清单

面试真让你手写,你最起码要会下面这些模板。

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,完成 socketbindlisten。在此基础上,又封装了 LibEvent 类来管理事件,并设计了回调基类 CallBack 以及两个子类:Accept_CallBack 用来处理新连接,Recv_CallBack 用来处理客户端数据接收。这样通过继承和多态,把不同类型 socket 的处理逻辑分开。最终实现了客户端连接服务器、发送数据,服务器接收并回复 ok,为后续 JSON 通信和数据库业务处理打下了基础。


十、最短背诵版

这节课在上节课监听套接字封装的基础上,补全了 LibEvent 类和回调类体系。通过 CallBack 基类以及 Accept_CallBackRecv_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 类型管理,事件发生时再通过多态调用各自真正的处理函数。

相关推荐
路溪非溪2 小时前
关于Linux中的日志问题
linux·arm开发·驱动开发
linux修理工2 小时前
ip a 命令解析与 IP 地址提取
linux·服务器·php
万象.2 小时前
Linux网络层相关知识及报文格式
linux·网络·智能路由器
盛世宏博北京2 小时前
6. 物联网环境监测新标杆:POE供电以太网温湿度变送器技术详解
大数据·运维·网络·以太网·poe·温湿度变送器
穷途末路程序员2 小时前
linux设备驱动程序框架(进阶1)——利用udev自动生成设备文件
linux
程序猿编码2 小时前
轻量又灵活:一款伪造TCP数据包的iptables扩展实现解析(C/C++代码实现)
linux·c语言·网络·c++·tcp/ip·内核·内核模块
_OP_CHEN2 小时前
【Linux网络编程】(二)计算机网络概念进阶:彻底搞懂协议本质、传输流程与封装分用
linux·运维·服务器·网络·网络协议·计算机网络·c/c++
风曦Kisaki2 小时前
# 云计算基础Day06:Linux权限管理
linux·云计算
badwomen__2 小时前
流水线数据冒险与转发:x86和ARM的不同打法
服务器·性能优化