高级IO|从封装epoll服务器到实现reactor服务器|Part2

该博客围绕从封装epoll_server到实现reactor服务器展开。先指出epoll的recv存在问题,需封装sock并维护缓冲区;接着介绍封装epoll、编写tcp_server,对connection进行管理;还阐述了派发概念、事件处理、回调函数编写,以及解耦上层业务、响应逻辑和异常处理等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目复习:从封装epoll_server到实现reactor服务器(part2)

仓库:https://github.com/Yufccode/Reactor-based-HyperWebServer/

这一部分不多说,直接进入代码编写,后面有需要解释的特性,再解释。

基本结构搭建好

tcp_server.hpp

#ifndef __YUFC_TCP_SERVER__
#define __YUFC_TCP_SERVER__
#include <iostream>
#include <string>
class tcp_server {
   
   
private:
public:
};
#endif

为什么上面我们写的epoll的recv是不正确的?

我们好好的看一下这一份代码。

    void epoll_recver(int sock) {
   
   
        // 读取这个普通套接字里面的内容
        // 1. 读取数据
        // 2. 处理数据
        char buffer[10240];
        size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (n > 0) {
   
   
            // 假设这里就是读到了一个完整的报文
            buffer[n] = 0;
            __handler_request(buffer); // 进行回调!
        } else if (n <= 0) {
   
   
            // 对端关闭文件描述符
            // 让epoll不再关注这个文件描述符
            // 一定要先从epoll中去掉,才能close文件描述符
            bool res = __epoll::control_epoll(__epoll_fd, EPOLL_CTL_DEL, sock, 0);
            assert(res); // 保证是成功的,因为一般来说都是成功的,所以直接assert
            (void)res;
            close(sock);
            if (n == 0)
                logMessage(NORMAL, "client %d quit, me quit too ...", sock);
            else if (n < 0)
                logMessage(NORMAL, "client recv %d error, close error sock", sock);
        }
    }

调用recv的时候读取的是完整的报文吗?不知道,所以我们buffer[10240]里面的也是不完整的报文。

此时能够直接交付给__handler_request()做我们的业务逻辑吗?不可以!这样是错的!

但是,我们上节课写的epoll服务器,也没有对应的缓冲区让我们暂时存放,等报文完整才__handler_request(),所以,上面的epoll_recver是错误的。

而且,一个epoll_server可能有成百上千的sock,每一个sock,都需要缓冲区才行!

sock要封装了,要维护缓冲区

所以要维护!

tcp_server.hpp

class connection {
   
   
public:
    using func_t = std::function<void(connection*)>;

public:
    connection();
    ~connection();

public:
    int __sock; // io的文件描述符
    func_t __recv_callback;
    func_t __send_callback;
    func_t __except_callback;
    std::string __in_buffer; // 输入缓冲区(暂时没有处理二进制流)
    std::string __out_buffer; // 输出缓冲区
    tcp_server* tsvr; // 回指指针
};

每一个文件描述符,都要封装!因为每一个文件描述符,都有可能有三种时间,读事件,写事件,异常事件。

简单来说就是,我们通过封装func_t,后面这个sock需要读的时候,就去调用__recv_callback。后面的同理!

这三个函数对象,都要从用户层传过来!

当然,输入输出缓冲区也是需要的!目前的用string做缓冲区,暂时没有办法处理二进制流,但是文本是可以的。

tcp_server* tsvr;

这个指针也很好理解,毕竟我这个connector总要知道是哪个服务器在调用我吧。

然后初始化什么的这里就不说了,看代码就行。然后我也写了一个void set_callback(func_t recv_cb, func_t send_cb, func_t except_cb)来设置这个sock封装(connector)的回调。

封装epoll(1)

这次封装和上次就不同了,这次我们把epoll的所有动作都封装到epoll.hpp里面去,包括epollfd什么的,我们的tcp_server就不管这个epoll里面的事情了,tcp_server只负责调用接口!

class tcp_server {
   
   
public:
    const static int gport = 8080;
private:
    int __listen_sock;
    int __port;
    __epoll __poll; // 这里直接维护一个epoll

然后,其实在很多真正的开发场景中,多路转接的方法都是可以选的,一般就是写一个虚基类class poll {},里面的函数就是纯虚的,然后去继承epoll, select, poll 这些。

所以对于tcp_server, 我们多路转接都叫poll比较好,因为也不一定要用epoll。

继续先写tcp_server

    tcp_server(int port = gport)
        : __port(gport) {
   
   
        // 1. 创建listensock
        __listen_sock = Sock::Socket();
        Sock::Bind(__listen_sock, __port);
        Sock::Listen(__listen_sock);
        // 2. 创建多路转接对象
        __poll.create_poll();
        // 3. 封装listensock, 因为以后在tcpserver里面,已经不会再有裸露的sock了,都是要封装的
        connection* conn = new connection(__listen_sock);
        conn->set_callback(read_callback, nullptr, nullptr);
        conn->__tsvr = this; 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@行李箱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值