网络编程(5)—— Reactor实现(v2)

回顾

在上一篇文章中我们我们实现了Reactor的封装部分,把socket,listen,bind,IO等系统调用,使用C++的类进行类封装,我们要明确一点为什么要使用C++进行封装,C语言也能完成这些工作。因为使用C++类进行封装会有更好的可拓展性,方便后续的功能添加和版本迭代。

Reactor_v2版本的实现

我们已经完成了封装的部分,下面要是实现我们的核心功能,就是事件驱动。也就是对epoll相关进行封装。

本质上就是将epoll的三个函数(epoll_create、epoll_ctl、epoll_wait)以及对应的两个文件描述符的可读事件的处理(Socket类创建的listenfd、以及Acceptor中的accept函数的返回的标识连接创建的文件描述符connfd)

对应的数据成员设计如下:

  • epoll_create函数的返回结果,需要将其设置为数据成员,因为该文件描述符需要被epoll的另外两个函数进行使用,所以要设置为数据成员 int _epfd

  • epoll_wait函数需要将就绪的文件描述符放在struct epoll_event结构体,也就是填充epoll_wait的第二个参数,后续还需要使用该结构体,也需要将其设置为数据成员,vector<struct epoll_event> _evtList

  • 如果监听的文件描述符listenfd可读,就说明有新的连接,就需要调用accept函数进行接受,但是该函数被封装到Acceptor类中了,所以需要使用Acceptor类创建的对象,才能获取到accept函数,可以使用Acceptor的对象(对应的引用也可以、对象的指针也可以,目的都是为了获取其中的成员函数accept),这里可以选择Acceptor类型的引用,Acceptor & _acceptor

  • 对于每一个accept执行结束后,说明连接已经建立,所以需要有一个对应的连接对象,也就TcpConnection,但是每个对象会关联一个文件描述符connfd,所以有一个记录文件描述符到TcpConnection的数据结构,可以使用键值对进行存储,这样以后就可以通过文件描述符就可以找到对应的连接,同时文件描述符是不重复的,所以这里可以选择的数据结构有map或者unordered_map,我们这里选择map,即map<int, TcpConnection>,但是注意,TcpConnection是不能进行复制或者赋值的,并且创建出来之后还需要在后续使用该连接,所以最好可以设置为堆对象,否则有可能提前销毁了,所以为了更好的管理,可以使用智能指针shared_ptr管理,也就是 map<int, shared_ptr<TcpConnection>> _conns ;

  • 为了标识整个事件循环是否运行,可以设置标志位 bool _isLooping

成员函数的设计如下:

  • createEpollFd函数:作用就是为了封装epoll的epoll_create函数,创建文件描述符;

  • addEpollReadFd函数:作用就是为了封装epoll的epoll_ctl函数,监听对应的文件描述符,例如:listenfd与connfd(这两个文件描述符上述都有描述);

  • delEpollReadFd函数:作用也是为了封装epoll的epoll_ctl函数,不过是删除对文件描述符的监听。

  • 构造函数:作用就是为了初始化数据成员,这里只有需要对引用数据成员需要额外进行初始化,所以使用EventLoop(Acceptor &acceptor);

  • 循环函数loop:对于IO多路复用epoll而言,核心就是大循环,里面封装epoll_wait函数;

  • 非循环函数loop:这里可以让其不循环,直接跳出;

  • waitEpollFd函数:里面封装epoll_wait函数,然后就是两大业务逻辑,文件描述符listenfd对应可读的新连接的建立、以及connfd文件描述符可读的读写操作;

  • handleNewConnection函数:处理文件描述listenfd的可读事件,调用Acceptor中的accept函数获得文件描述符connfd以及使用文件描述符connfd创建连接TcpConnection的对象,然后监听文件描述符connfd,创建文件描述符connfd与连接TcpConnection的对象键值对,便于通过文件描述符可以找到对应的连接;

  • handleMessage函数:通过文件描述符,映射出对应的连接,然后处理读写事件。

这就是对于epoll逻辑的分析而设计出来的数据成员与成员函数

类图设计

需要通过epoll_create函数进行创建的文件描述符_epfd,并且该文件描述符需要跨函数进行使用,所以将其设置为了数据成员。

为了将就绪的文件描述符存起来,所以设置了vector<struct epoll_event> _evtList数据结构;

为了能标识整个代码是否在循环,设置了标志位_isLooping

因为在listenfd就绪的时候,需要处理新的连接请求,所以需要调用Acceptor类中的accept函数,所以将Acceptor类型的对象的引用作为数据成员;

只要Acceptor类的对象调用accept有正确的返回结果,就表明三次握手建立成功了,就可以创建TcpConnection对象,为了更好的管理TcpConnection对象,可以将其用map存起来,并且为了防止复制或者赋值,可以使用shared_ptr将其存起来 map<int, shared_ptr<TcpConnection>>,对于智能指针可以用using起别名为TcpConnectionPtr

TCP网络编程过程中的三个半事件

再回顾一下对socket网络编程的认识,里面使用了TCP协议、TCP的三次握手建立连接、TCP的四次挥手断开连接、以及在连接建立好后与客户端进行数据的读写操作,这里面实际上包含着一些事件,也就是:

对于服务端而言

连接建立:包括服务器端被动接受连接(accept)和客户端主动发起连接(connect)。TCP连接一旦建立,客户端和服务端就是平等的,可以各自收发数据。

连接断开:包括主动断开(close、shutdown)和被动断开(read()返回0)。

消息到达:文件描述符可读(如果是listenfd可读表示新的连接请求,如果是connectfd可读表示客户端发数据了),这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如何设计等等)。

消息发送完毕:这算半个。对于低流量的服务,可不必关心这个事件;这里的"发送完毕"是指应用程序将数据写入操作系统缓冲区(内核缓冲区),不代表对方已经接收到数据。因为后续将由TCP协议栈负责数据的发送与重传,这部分不属于应用程序能掌控的范围。例如在网络延迟、丢包等情况下,即使应用层显示发送完毕,数据也可能没到达接收方,所以从完整通信链条看,它只完成了前半部分,故称 "半个事件" 。

可以在测试代码中加上这三个事件的函数,都跟连接相关,那就需要通过传参提供TcpConnection

在连接建立、消息到达、连接断开时,分别做些什么事情

添加三个事件的回调(类图)

注意

  • 三个回调都是与连接相关的,所以应该直接注册给TcpConnection,但是TcpConnection的对象是在EventLoop中创建的,所以需要先将三个回调交给EventLoop,然后传递给TcpConnection对象。
  • 三个回调用的形式其实就是function对象,但是为了表示都与TcpConnection相关,所以不能设置为function<void()>,而是需要将TcpConnection对象带上,才能表示与连接相关,所以可以设置为function<void(const TcpConnectionPtr &)>
添加回调的类图


用作回调的这三个function对象作为EventLoop类的数据成员,可以交给本类的成员函数handleNewConnection,因为在这个成员函数中创建了TcpConnection对象。
TcpConnection中添加与EventLoop新增的一样的三个数据成员与三个成员函数,因为本质上EventLoop仅仅只是作为中转。

通过三个setXXX系列的成员函数接收了三个回调(测试文件中提供给EventLoop,EventLoop提供给TcpConnection)后,还需要添加执行相应回调的函数,再加上三个handleXXX系列的成员函数

接下来又要考虑一个新的问题

对于TcpConnection类的setXXX函数而言(以setNewConnectionCallback函数为例),如果说像EventLoop类的setNewConnectionCallback函数一样使用右值引用作为函数形参,那么在EventLoop的handleNewConnection函数中调用TcpConnection的setNewConnectionCallback函数时,传入的是EventLoop的数据成员_onNewConnection,需要用到std::move,而TcpConnection的数据成员_onNewConnection接收后实际起到了接管底层函数的效果(移动赋值函数),相当于EventLoop的数据成员_onNewConnection成了空的function,可能会影响后面的连接。所以可以在TcpConnection类的setNewConnectionCallback函数的形参位置写上const左值引用的形式,既可以接左值,也可以接右值,并且用这个const引用给TcpConnection类的_onNewConnection赋值时就会调用function的赋值运算符函数(起到复制效果,而不是移交管理权效果)

当然,如果不考虑function对象复制的开销,在所有的函数参数中都直接使用function对象也是可以的。

EventLoop.h

cpp 复制代码
#ifndef __EVENTLOOP_H
#define __EVENTLOOP_H

#include<sys/epoll.h>
#include"Acceptor.h"
#include"TcpConnection.h"
#include<vector>
#include<map>
#include<memory>
#include<functional>
using std::vector;
using std::map;
using std::shared_ptr;
using std::function;

using TcpConnectionPtr = shared_ptr<TcpConnection>;
using TcpConnectionCallback = function<void(const TcpConnectionPtr &)>;

class EventLoop{
public:
    EventLoop(Acceptor & acc);

    ~EventLoop();

    void loop();

    void unloop();

    // 注册回调
    void setNewConnectionCallback(TcpConnectionCallback && cb);
    void setMessageCallback(TcpConnectionCallback && cb);
    void setCloseCallback(TcpConnectionCallback && cb);

private:
    void waitEpollfd();

    void handleNewConnection();
    void handleMessage(int fd);
    int createEpollFd();
    void addEpollReadFd(int fd);
    void delEpollReadFd(int fd);


private:
    int _epfd;      // epoll_create创建的文件描述符
    vector<struct epoll_event> _evtList;    // 存放就绪文件描述符的容器
    bool _isLooping;                        // 是否在循环的标志
    Acceptor & _acceptor;                   // Acceptor 引用
    map<int, shared_ptr<TcpConnection>> _conns; // 存放描述符与TcpConnection连接的键值对

    // 三个回调函数
    TcpConnectionCallback _onNewConnection;
    TcpConnectionCallback _onMessage;
    TcpConnectionCallback _onClose;

};




#endif

EventLoop.cpp

cpp 复制代码
/* ************************************************************************
> File Name:     EventLoop.cpp
> Author:        hyj
> mail:          1683958261@qq.com
> Created Time:  Mon 03 Aug 2026 09:45:24 AM CST
> Description:
 ************************************************************************/
#include"EventLoop.h"
#include"logger.h"
#include<iostream>
#include<string.h>
#define LISTEN_CLIENT_MAX_NUM 1024


EventLoop::EventLoop(Acceptor & acc):
    _epfd(createEpollFd()),
    _evtList(LISTEN_CLIENT_MAX_NUM),
    _isLooping(false),
    _acceptor(acc),
    _conns(){

    // 把listenfd放到红黑树上
    int listenfd = _acceptor.fd();
    addEpollReadFd(listenfd);
}

EventLoop::~EventLoop(){
    ::close(_epfd);
}

void EventLoop::loop(){
    _isLooping = true;
    while(_isLooping){
        waitEpollfd();
    }
}

void EventLoop::unloop(){
    _isLooping = false;
}
void EventLoop::waitEpollfd(){
    int nready = 0;

    nready = epoll_wait(_epfd, _evtList.data(), _evtList.capacity(),3000);

    if(nready == -1){
        INFO_LOG("epoll_wait error");
        return ;
    }
    else if(nready == 0){
        INFO_LOG("epoll_wait timeout");
    }

    else{
        // 处理文件描述符个数到达上限,触发扩容
        if(nready == (int)_evtList.capacity()){
            _evtList.resize(2 * nready);
        }

        for(int idx = 0;idx < nready; ++idx){
            int listenfd = _acceptor.fd();
            int fd = _evtList[idx].data.fd;
            // 如果监听到listenfd则说明有有新的连接
            if(fd == listenfd){
                handleNewConnection();
            }
            // 如果是其它文件描述符,则说明有客户端发来消息
            else{
                handleMessage(fd);
            }
        }
    }
}

void EventLoop::handleNewConnection(){
    // 获取新的连接
    int connfd = _acceptor.accept();
    if(connfd < 0){
        ERROR_LOG("accept failed!");
        return ;
    }
    // 放到红黑树上
    addEpollReadFd(connfd);
    // 智能指针管理新的连接
    TcpConnectionPtr con(new TcpConnection(connfd));

    con->setNewConnectionCallback(_onNewConnection);
    con->setMessageCallback(_onMessage);
    con->setCloseCallback(_onClose);

    /* 使用回调
    // 打印连接日志
    cout << con->toString() << "has connected" << endl;
    _onNewConnection(con)*/
    con->handleNewConnectionCallback();

    //把新的连接放到map中进行管理 _conn[connfd] = con;
    _conns.insert({connfd, con});
}
void EventLoop::handleMessage(int fd){
    auto it = _conns.find(fd);
    if(it != _conns.end()){
        bool isclose = it->second->isClosed();
        if(isclose){
            /*换成回调*/
            //cout << it->second->toString() << "has closed" << endl;
            it->second->handleCloseCallback();

            delEpollReadFd(fd);
            _conns.erase(it);
        }
        else{
            /*换成回调
            // 接收客户端的数据
            string msg = it->second->receive();
            cout << ">> recv msg from client:" << msg << endl;
            */
            it->second->handleMessageCallback();
            // 服务器处理数据
            //
            //
            //

            // 发回给客户端
            //it->second->send(msg);
        }
    }
    else{
        INFO_LOG("connection dispear");
        return ;
    }
}

int EventLoop::createEpollFd(){
    int epfd = epoll_create(LISTEN_CLIENT_MAX_NUM);
    if(epfd == -1){
        ERROR_LOG("Epoll Create Error");
        exit(EXIT_FAILURE);
    }
    return epfd;
}

void EventLoop::addEpollReadFd(int fd){
    struct epoll_event evt;
    memset(&evt, 0, sizeof(evt));
    evt.events = EPOLLIN;
    evt.data.fd = fd;

    if(epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &evt) == -1){
        ERROR_LOG("epoll_ctl add error");
        exit(EXIT_FAILURE);
    }
}

void EventLoop::delEpollReadFd(int fd){
    if(epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, NULL) == -1){
        ERROR_LOG("epoll_ctl del error");
        exit(EXIT_FAILURE);
    }
}



void EventLoop::setNewConnectionCallback(TcpConnectionCallback && cb){
    _onNewConnection = cb;
}
void EventLoop::setMessageCallback(TcpConnectionCallback && cb){
    _onMessage = cb;
}
void EventLoop::setCloseCallback(TcpConnectionCallback && cb){
    _onClose = cb;
}

TcpConnection.h

cpp 复制代码
/**
 * 连接管理
 */


#ifndef __TCPCONNECTION_H
#define __TCPCONNECTION_H

#include<string>
#include"Socket.h"
#include"SocketIO.h"
#include"InetAddress.h"
#include<functional>
#include<memory>
using std::string;
using std::function;
using std::shared_ptr;

class TcpConnection;


using TcpConnectionPtr = shared_ptr<TcpConnection>;
using TcpConnectionCallback = function<void(const TcpConnectionPtr &)>;

class TcpConnection
:public std::enable_shared_from_this <TcpConnection>{
public:

/**
 * @param fd
 */
explicit TcpConnection(int fd);

~TcpConnection();

string receive();

string toString();

bool isClosed();


void setNewConnectionCallback(const TcpConnectionCallback & cb);
void setMessageCallback(const TcpConnectionCallback & cb);
void setCloseCallback(const TcpConnectionCallback & cb);

void handleNewConnectionCallback();
void handleMessageCallback();
void handleCloseCallback();

void send(const string & msg);
private:
    InetAddress getLocalAddr();
    InetAddress getPeerAddr();

/**
 * @param msg
 */
private:
    SocketIO _sockIO;
    // 辅助打印连接日志信息
    Socket _sock;
    InetAddress _localAddress;
    InetAddress _peerAddr;

    // 回调
    TcpConnectionCallback _onNewConnection;
    TcpConnectionCallback _onMessage;
    TcpConnectionCallback _onClose;
};

#endif //_TCPCONNECTION_H

TcpConnection.cpp

cpp 复制代码
#include "TcpConnection.h"
#include<sstream>
#include"logger.h"
using std::ostringstream;
/**
 * TcpConnection implementation
 */


/**
 * @param fd
 */
TcpConnection::TcpConnection(int fd)
    :_sockIO(fd),
    _sock(fd),
    _localAddress(getLocalAddr()),
    _peerAddr(getPeerAddr()){

}

TcpConnection::~TcpConnection() {

}

/**
 * @return string
 */
string TcpConnection::receive() {
    char buffer[1024] = {0};
    _sockIO.readLine(buffer, sizeof(buffer));

    return string(buffer);
}

/**
 * @param msg
 * @return void
 */
void TcpConnection::send(const string & msg) {
    _sockIO.writen(msg.c_str(), msg.size());


    return;
}

string TcpConnection::toString(){
    ostringstream oss;
    oss << _localAddress.getIp() << ":"
        << _localAddress.getPort() << "------------>"
        << _peerAddr.getIp() << ":"
        << _peerAddr.getPort() ;
    return oss.str();
}


InetAddress TcpConnection::getLocalAddr(){
    struct sockaddr_in addr;
    socklen_t len = sizeof(struct sockaddr_in);
    if(::getsockname(_sock.getFd(),(struct sockaddr*)&addr, &len)){
        WARN_LOG("getLocalAddr");
    }
    return InetAddress(addr);
}
InetAddress TcpConnection::getPeerAddr(){
    struct sockaddr_in addr;
    socklen_t len = sizeof(struct sockaddr_in);
    if(::getpeername(_sock.getFd(),(struct sockaddr*)&addr, &len)){
        WARN_LOG("getPeerAddr");
    }
    return InetAddress(addr);
}

bool TcpConnection::isClosed(){
    // 进行一次拷贝式的预读
    char buff[5] = {0};
    int ret = recv(_sock.getFd(), buff, sizeof(buff), MSG_PEEK);
    return 0 == ret;
}


void TcpConnection::setNewConnectionCallback(const TcpConnectionCallback & cb){
    _onNewConnection = cb;
}
void TcpConnection::setMessageCallback(const TcpConnectionCallback & cb){
    _onMessage = cb;
}
void TcpConnection::setCloseCallback(const TcpConnectionCallback & cb){
    _onClose = cb;
}

void TcpConnection::handleNewConnectionCallback(){
    if(_onNewConnection){
        _onNewConnection(shared_from_this());
    }
    else{
        INFO_LOG("_onNewConnection is empty");
    }
}
void TcpConnection::handleMessageCallback(){
    if(_onMessage){
        _onMessage(shared_from_this());
    }
    else{
        INFO_LOG("_onMessage is empty");
    }
}
void TcpConnection::handleCloseCallback(){
    if(_onClose){
        _onClose(shared_from_this());
    }
    else{
        INFO_LOG("_onClose is empty");
    }
}

main.cpp

cpp 复制代码
#include "Acceptor.h"
#include "TcpConnection.h"
#include <iostream>
#include "EventLoop.h"
using std::cout;
using std::endl;

void onNewConnection(const TcpConnectionPtr & con){
    cout << con->toString() << " connected!" << endl;
}

void onMessage(const TcpConnectionPtr & con){
    string msg = con->receive();
    cout << "recv msg from client:" << msg <<endl;
    msg = "msg:"+msg;
    con->send(msg);
}

void onClose(const TcpConnectionPtr & con){
    cout << con->toString() << " closed " << endl;
}

void test0()
{
    Acceptor acceptor("0.0.0.0",8080);
    acceptor.ready();

    EventLoop eloop(acceptor);
    eloop.setNewConnectionCallback(onNewConnection);
    eloop.setMessageCallback(onMessage);
    eloop.setCloseCallback(onClose);
    eloop.loop();
}

int main()
{
    test0();
    return 0;
}

运行结果

完成到这里其实我们已经完成了核心的功能框架,为了更好的使用,可以对当前进行一次封装,但这是次要问题。封装之后我们会发现一个问题,我们的服务器响应是串行执行的,这就极大的降低了我们服务器的性能。所以在下一步最主要的问题应该是解决并发响应的问题

相关推荐
我是乌鸦的空间11 小时前
什么是网络安全中的电子数据取证与响应?
网络·网络安全
跨境旺仔小拳头11 小时前
如何使用Crawl4AI进行网页爬虫?从0搭建教程与代理配置指南
网络·人工智能·爬虫·chatgpt·aigc
沫儿笙11 小时前
焊接机器人氩弧焊省气装置
网络·机器人
80s77711 小时前
什么是动态IP?静态IP和动态IP有什么区别?
网络·网络协议·tcp/ip
那年窗外下的雪.12 小时前
AIDC 学习日志|第 15 天|EVPN 多归属故障收敛与撤销机制
网络·学习·php
国科安芯13 小时前
轨道供电韧性:ASP4644四通道降压架构在低轨卫星平台电源管理中的适用性分析
运维·网络·数据库·嵌入式硬件·架构·状态模式·低轨卫星星座
少陽君14 小时前
用于InfiniBand网络的实时终端监控器——高速互连的htop
网络
发量惊人的中年网工16 小时前
如何验证DDoS高防真的生效?从源站隐藏到正常访问的完整验收方法
运维·网络·数据库·ddos
GuoFeng.Wan17 小时前
深入理解经典蓝牙的L2CAP协议
网络