[evpp/muduo/reactor] evpp事件驱动网络库 整体架构梳理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sai_j/article/details/82322836

介绍

muduo很多人都听说过,那evpp可以理解成是muduo用C++11改写后的升级版。
相比muduo的代码风格,evpp会显得更加现代一点,更讨我们年轻人的喜欢。
作为例子,这里是一段TCP Echo Server的示例代码:

    evpp::EventLoop loop;
    evpp::TCPServer server(&loop, "0.0.0.0:9099", "TCPEchoServer", thread_num);
    server.SetMessageCallback([](const evpp::TCPConnPtr& conn,
                                  evpp::Buffer* msg) {
        conn->Send(msg);
    });
    server.SetConnectionCallback([](const evpp::TCPConnPtr& conn) {
        if (conn->IsConnected()) {
            LOG_INFO << "A new connection from " << conn->remote_addr();
        } else {
            LOG_INFO << "Lost the connection from " << conn->remote_addr();
        }
    });
    server.Init();
    server.Start();
    loop.Run();

evpp的核心代码其实不多,之前也已经花了一段时间去阅读;
总的来说,代码本身还是十分通俗易懂的,不像某些天书C++;
相比muduo,evpp一个很明显的变化就是,IO复用相关的抽象直接使用了libevent;
但是,看完之后,总是缺少一个系统性的认识,因此,想借此机会进行梳理;
此外,网上虽然有很多muduo的解析,但是介绍evpp的还是挺少的;
对于习惯C++11风格的人,应该算是多了一种选择;
https://github.com/Qihoo360/evpp

核心类

这里,我先罗列下evpp中的核心类

  • TCPServer
  • Listener
  • TCPClient
  • Connector
  • TCPConn
  • FdChannel
  • EventLoop
  • EventLoopThread
  • EventLoopThreadPool

只要是熟悉Socket编程的同学,看见这几个类后应该还是有点亲切感的。
其中,TcpClient/TcpServer分别抽象TCP客户端和服务器;Connector/Listener分别包装TCP客户端和服务器的建立连接/接受连接;TcpConnection抽象一个TCP连接,无论是客户端还是服务器只要建立了网络连接就会使用TcpConnection;FdChannel是设备fd的包装,在muduo中主要包装socket以及该socket对应的事件处理回调函数;EventLoop是一个事件发生器,它驱动底层的IO复用器产生/发现事件,然后将事件派发到Channel处理;EventLoopThread是一个带有EventLoop的线程;EventLoopThreadPool自然是一EventLoopThread的资源池,维护一堆EventLoopThread。

TCPServer类

这里,我们以自顶向下的方式进行梳理。所以,我们先来看看TCPServer的数据成员。

    // TCPServer的名字,比如"Echo", "Discard"等
    const std::string name_; 

    // TCPServer最重要的一点就是,创建ListenFD然后Accept新连接;
    // 接下来的3项,给出了必要的信息;
    // 第一项是“监听地址”,值得提的一点是,evpp借鉴了Golang,可以直接使用字符串指定IP地址;
    // 第二项是一个指针,指向Listener结构体;在evpp中,监听端口创建新连接 相关的细节都被封装在这个类之中;
    // 这个类对应至muduo中的acceptor类;另外一点就是,accepted的连接,在evpp中被抽象成TCPConn这个类,这个类我们稍后会提到;
    // 第三项就是事件驱动的引擎,可以理解成是对IO复用器的封装;
    // ListenFD会注册到该IO复用器上,当一个ListenFD上有新的连接时,该FD就会变成“可读”,从而回调“可读Callback”;
    // 当然,这个可读Callback执行的是创建TCPConn的逻辑,而不是读数据啥的;
    // 另外,这个loop_就是TCPServer_的主事件循环;
    const std::string listen_addr_; // ip:port
    std::unique_ptr<Listener> listener_;
    EventLoop* loop_;  // the listening loop

    // 一个EventLoop会绑定到一个特定的线程上运行;
    // 为了能够更充分地利用多核心CPU,直观的想法就是创建更多的线程(线程池),
    // 然后在每个线程上运行EventLoop;
    std::shared_ptr<EventLoopThreadPool> tpool_;

    ConnectionCallback conn_fn_;  // 连接建立/断开时的回调函数,由用户注册;
    MessageCallback msg_fn_;   // 当收到消息时的回调函数,由用户注册;
    DoneCallback stopped_cb_;

    // 一个TCPServer可以有多个TCP连接,下面的这两项就反映了这样一个客观现实;
    // TCPServer会为每个TCP连接分配一个ID;与此同时,以该ID为key将TCPConn保存起来;
    uint64_t next_conn_id_ = 0;
    std::map<uint64_t, TCPConnPtr> connections_;

看完数据成员,我们再来看看TCPServer的开放接口;

// 对于TCPServer的构造函数,当中无非是对我们刚刚提到的数据成员进行初始化;
// 但是,值得提的一点是Eventloop是由用户分配的;
    TCPServer(EventLoop* loop,  
              const std::string& listen_addr/*ip:port*/,
              const std::string& name,
              uint32_t thread_num);

// 一个Server端正常的流程如下:创建套接字、绑定地址、设置成Listen状态、调用Accept开始接受链接
// 在evpp/muduo的设计中,前三项被单独放在了Init()初始化函数中,而真正调用Accept的操作被单独放在了Start()当中;
    bool Init() {
        listener_.reset(new Listener(loop_, listen_addr_));
        listener_->Listen();
        status_.store(kInitialized);
    }

// 在调用前面提到的Accept之前,Start()当中为listener指定了回调函数,
// 当有新的连接到来时,这个回调函数就会被调用;
// 值得提醒的是,到目前为止,在这几个初始化启动函数中,我们并没有发现与“业务”相关的逻辑;
// 一个TCP服务器总有其业务逻辑,那么业务逻辑在哪里得到处理呢?
// 让我们去看下TCPServer::HandleNewConn;
// (这里再赘述下,HandleNewConn是TCPServer类中的函数,
// 但是通过listener_->SetNewConnectionCallback()的接口被注册到listener_中,由listener_调用;
// 这种操作方式,在evpp/muduo中是十分普遍的;接下来,我们还会看到更多的这样的例子;
// 所以,我们应该留神这个callback到底是哪里来的;)
    bool Start() {
        tpool_->Start(true);
        listener_->SetNewConnectionCallback(...TCPServer::HandleNewConn...)
        listener_->Accept();
    }

在接着讲HandleNewConn之前,我们先过一眼剩下的两个开放(public)接口;
这两个函数看似十分简单,甚至是微不足道,但是它起到了 “封装业务逻辑”的巨大作用;
用户首先单独编写“业务处理函数”,然后以“回调”的形式注册到框架中,
实现“业务”和“框架”的解耦;
稍后,我们就会立马看到它们的身影;

    void SetConnectionCallback(const ConnectionCallback& cb) { conn_fn_ = cb; }
    void SetMessageCallback(MessageCallback cb) { msg_fn_ = cb; }

void TCPServer::HandleNewConn(evpp_socket_t sockfd,
                              const std::string& remote_addr/*ip:port*/,
                              const struct sockaddr_in* raddr) {
// 接上文提到的,当Listener上有新的链接到来时,HandleNewConn函数会被回调;
// 由于是事件驱动模型,一个TCP链接总是要对应到一个IO复用器(或者说Eventloop事件循环);
// 因此,代码中的第一步就是获取一个Eventloop;
    EventLoop* io_loop = GetNextLoop(raddr);

// 在evpp/muduo中,TCP连接被抽象成TCPConn类;
// 既然现在有新连接到来,那我们new一个出来;
// 抛去一个TCP连接应该具备的属性之外,在构造函数中我们还传递了io_loop;
    TCPConnPtr conn(new TCPConn(io_loop, ConnectionName, sockfd, listen_addr_, remote_addr, ++next_conn_id_));

// 在这里,TCPServer用户通过Setter函数设置的回调函数,被进一步注册到了底层的TCPConn当中;
// 这种回调函数一层一层往下注册的情况,接下来还会经常看到;
    conn->SetMessageCallback(msg_fn_);
    conn->SetConnectionCallback(conn_fn_);

// 既然TCPConn本身初始化完毕,同时上头也指定了“当连接状态变化”或“当有数据到达时”该如何处理,
// 那我们就把这个TCPConn Attach到事件循环当中;
// 至此,一个TCP连接就算是正式建立,可以服务客户端了;
// 这里剧透下,SockFD以及刚刚提到的“回调函数”实际上又被封装在了Channel这个类中;
// 这个类,我们稍后再进行解释;
    io_loop->RunInLoop(std::bind(&TCPConn::OnAttachedToLoop, conn));

// 每个TCPConn被分配一个ID,以这种形式TCPServer便于对TCPConn进行集中管理;
    connections_[conn->id()] = conn;
}

TCPClient类

在熟悉了TCPServer之后,再去看对称的TCPClient类就会觉得轻车熟路;

    evpp::EventLoop loop;
    evpp::TCPClient client(&loop, "127.0.0.1:9099", "TCPPingPongClient");
    client.SetMessageCallback([&loop, &client](const evpp::TCPConnPtr& conn,
                               evpp::Buffer* msg) {
        // 用户业务逻辑
    });

    client.SetConnectionCallback([](const evpp::TCPConnPtr& conn) {
        // 用户业务逻辑
    });

    client.Connect();

    loop.Run();

在用户代码层面,二者唯一明显的区别在于,listen/accept的封装函数被替换成了Connect()函数;因此,让我们先来探一探Connect();

// 还记得TCPServer中的Listener类吗? Listener类封装了监听套接字、创建新连接相关的细节;
// 类似地,在TCPClient这边也有一个Connector类,该类封装了socket connect相关的细节;
// 当用户调用client.Connect()时,将会发起异步的建立连接操作; 
// 当TCP连接建立成功(或失败)时,TCPClient::OnConnection函数将会被回调;
void TCPClient::Connect() {
    auto f = [this]() {
        connector_.reset(new Connector(loop_, this));  // 对Connector中的数据成员进行必要的赋值;
        connector_->SetNewConnectionCallback(...TCPClient::OnConnection...);        
        connector_->Start();
    };
    loop_->RunInLoop(f);  // 在loop_所在的线程中调用函数f
}

// 当TCP连接建立成功或失败时,将会回调TCPClient::OnConnection
void TCPClient::OnConnection(evpp_socket_t sockfd, const std::string& laddr) {
    if (sockfd < 0) {  // 当TCP连接建立失败时,通过回调用户设置的回调函数conn_fn_,从而使得上层有机会对错误进行处理;`
        conn_fn_(TCPConnPtr(new TCPConn(loop_, "", sockfd, laddr, remote_addr_, 0)));
        return;
    }

    // 和TCPServer那边的情况类似,用户调用Setter函数设置的MessageCallback/ConnectionCallback,将会被进一步注册到TCPConn中;
    TCPConnPtr c = TCPConnPtr(new TCPConn(loop_, name_, sockfd, laddr, remote_addr_, id++));
    c->set_type(TCPConn::kOutgoing);
    c->SetMessageCallback(msg_fn_); 
    c->SetConnectionCallback(conn_fn_);
    c->SetCloseCallback(...TCPClient::OnRemoveConnection...);

    {
        std::lock_guard<std::mutex> guard(mutex_);
        conn_ = c;
    }

    // 到此为止,一个TCP连接就完成了建立操作;
    // 这时候,我们可以把这个TCP连接添加到事件循环中,并回调用户设置的conn_fn_函数;
    // 回调这一操作,使得“连接建立成功”这个事件能够被通知到上层用户;
    c->OnAttachedToLoop();
}

持续更新Ing

参考文献

https://github.com/Qihoo360/evpp
https://www.cnblogs.com/gaorong/p/6476757.html
https://blog.csdn.net/yusiguyuan/article/details/40593721
https://blog.csdn.net/Shreck66/article/details/50945929
https://blog.csdn.net/Shreck66/article/details/50948878
http://www.cppblog.com/kevinlynx/archive/2014/05/04/206817.html

猜你喜欢

转载自blog.csdn.net/sai_j/article/details/82322836
今日推荐