muduo网络库设计与实现(二)

muduo网络库设计与实现(一)

文章目录

  • muduo网络库设计与实现(一)
    • base
      • InetAddress
      • Socket
    • 单线程网络库
      • Acceptor
      • TcpServer
      • TcpConnection
      • Buffer
    • echo server 例子
      • 主要类的包含关系
      • echo server
        • 连接
        • 信息到达
        • 信息发送
        • 关闭连接
    • 代码地址

前面一节已经实现了Reactor事件处理框架,这节我们逐步实现一个单线程非阻塞TCP网络编程库,主要包括:

  • Accepter:用于接受TCP连接,它是TcpServer的成员
  • TcpServer:用于编写网络服务器,接受客户连接
  • TcpConnection:整个网络库的核心,封装一次TCP连接
  • Buffer:socket数据的读写通过buffer,用户不需要调用read或write,只需要处理收到的数据和准备好要发送的数据。

base

InetAddress

对sockaddr_in的简单封装,能自动转换字节序。

class InetAddress{
public:
    // Constructs an endpoint with given port number.
    // Mostly used in TcpServer listening.
    explicit InetAddress(uint16_t port);

    // Constructs an endpoint with given ip and port.
    // ip should be "1.2.3.4"
    InetAddress(const std::string& ip, uint16_t port);

    // Constructs an endpoint with given struct sockaddr_in
    // Mostly used when accepting new connections
    InetAddress(const struct sockaddr_in& addr) : addr_(addr) {}

    std::string toHostPort() const;

    const struct sockaddr_in& getSockAddrInet() const { return addr_; }
    void setSockAddrInet(const struct sockaddr_in& addr) { addr_ = addr; }

private:
    struct sockaddr_in addr_;
};

Socket

Socket类是对socket文件描述符的封装,也包括一些对socket操作的工具函数。

int createNonblocking();
struct sockaddr_in getLocalAddr(int sockfd);
int getSocketError(int sockfd);

class Socket : noncopyable{
public:
    explicit Socket(int sockfd) : sockfd_(sockfd) {}
    ~Socket();
    int fd() const {return sockfd_;}
    void bindAddress(const InetAddress& localaddr);
    void listen();
    // 返回已经设置了non-blocking and close-on-exec的fd
    int accept(InetAddress* peeraddr);
    void setReuseAddr(bool on);

    void shutdownWrite();

    void setTcpNoDelay(bool on);

private:
    const int sockfd_;
};

单线程网络库

Acceptor

Acceptor用于接受新TCP连接,并通过回调通知使用者,它是内部class,供TcpServer所用,生命期由后者控制。

class Acceptor : noncopyable{
public:
    typedef std::function NewConnectionCallback;

    Acceptor(EventLoop* loop, const InetAddress& listenAddr);
    void setNewConnectionCallback(const NewConnectionCallback& cb) {newConnectionCallback_ = cb;}

    bool listenning() const { return listenning_; }
    void listen();

private:
    void handleRead();

    EventLoop* loop_;
    Socket acceptSocket_;
    Channel acceptChannel_;
    NewConnectionCallback newConnectionCallback_;
    bool listenning_;
};

Acceptor在构造时会将其handleRead()注册为其channel的readCallback_,而在handleRead()中则会调用TcpServer注册的回调函数newConnectionCallback_

TcpServer

Tcpserver类的功能是管理accept获得的TcpConnectionTcpserver是供用户直接使用的,生命期由用户管理。

class TcpServer : noncopyable{
public:
    TcpServer(EventLoop* loop, const InetAddress& listenAddr);
    ~TcpServer();

    void start();

    void setConnectionCallback(const ConnectionCallback& cb) { connectionCallback_ = cb; }
    void setMessageCallback(const MessageCallback& cb) { messageCallback_ = cb; }
    void setWriteCompleteCallback(const WriteCompleteCallback& cb) { writeCompleteCallback_ = cb; }

private:
    void newConnection(int sockfd, const InetAddress& peerAddr);
    void removeConnection(const TcpConnectionPtr& conn);
    
    typedef std::map ConnectionMap;

    EventLoop* loop_; // the acceptor loop
    const std::string name_;
    std::unique_ptr acceptor_;
    ConnectionCallback connectionCallback_;
    MessageCallback messageCallback_;
    WriteCompleteCallback writeCompleteCallback_;
    bool started_;
    int nextConnId_;
    ConnectionMap connections_;
};

在新连接到达时,Acceptor会回调newConnection(),后者会创建TcpConnection对象conn,并把它加入ConnectionMap,设置好callback,再调用conn->connectEstablished(),其中会回调用户提供的ConnectionCallback

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr){
    loop_->assertInLoopThread();
    char buf[32];
    snprintf(buf, sizeof(buf), "#%d", nextConnId_);
    ++nextConnId_;
    std::string connName = name_ + buf;

    InetAddress localAddr(getLocalAddr(sockfd));

    TcpConnectionPtr conn(new TcpConnection(loop_, connName, sockfd, localAddr, peerAddr));
    connections_[connName] = conn;
    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setCloseCallback(std::bind(&TcpServer::removeConnection, this, std::placeholders::_1));
    conn->connectEstablished();
}

TcpConnection

TcpConnection类是最核心也是最复杂的类,它继承了enable_shared_from_this。我们一步一步逐渐实现

其数据成员如下。TcpConnection类使用Channel来获取socket上的IO事件,它会自己处理writable事件,而把readable事件通过MessageCallback传达给用户。TcpConnection拥有Tcp socket,它会在析构函数中close(fd)。

enum StateE { kConnecting, kConnected, };

void setState(StateE s) { state_ = s; }
void handleRead();
void handleWrite();
void handleClose();
void handleError();
    
EventLoop* loop_;
std::string name_;
StateE state_;
std::unique_ptr socket_;
std::unique_ptr channel_;
InetAddress localAddr_;
InetAddress peerAddr_;
ConnectionCallback connectionCallback_;
MessageCallback messageCallback_;
CloseCallback closeCallback_;

TcpConnection::handleRead()会根据read函数的返回值,分别调用messageCallback_handleClose()handleError()

TcpConnection::handleClose()的主要功能是调用closeCallback_,这个回调绑定到TcpServer::removeConnection()

TcpConnection::connectDestroyed()TcpConnection对象在析构前最后一个调用的函数,它通知用户连接断开。

这里稍微总结一下连接关闭的流程:

  1. 发生可读事件,IO复用函数返回
  2. 调用可读事件的回调函数,TcpConnection::handleRead()
  3. TcpConnection::handleRead() 中read返回0,执行TcpConnection::handleclose()
  4. TcpConnection::handleclose中首先将连接套接字从epoll中移除(此时并没有在epoller中删除channel),再调用closeCallback_
  5. 第4步中的closeCallback_绑定到TcpServer::removeConnection()
  6. TcpServer::removeConnection()将TcpConnection从自己的map中删除,然后将TcpConnection::connectDestroyed()注册到IO线程中执行
  7. IO线程中执行TcpConnection::connectDestroyed(),将channel从epoller中删除channel。然后TcpConnection会被析构,相应的channel也被析构。

Buffer

Buffer是非阻塞TCP网络编程必不可少的东西,因为我们的IO线程只能阻塞在IO复用上,而对于应用程序,它只管生成数据,不关心数据是一次性发送还是分成几次发送,所以需要 output buffer,在接收数据时,可能遇到数据不完整,需要 input buffer暂存,等构成一条完整的消息再通知应用程序。

muduo的buffer比较代码比较直观,不在这里过多介绍。

echo server 例子

至此,我们基本实现了一个单线程的Reactor网络库,我们通过一个echo server的例子来具体看看各种回调函数是如何被调用的。

主要类的包含关系

EventLoop:
持有一个Epoller对象poller_,一个TimerQueue对象timerQueue_和一个Channel对象wakeupChannel_
在构造时,会将自己的handleRead()注册到wakeupChannel_中(其实就是一个discard函数)。
EventLoop的指针会被很多运行在该线程的对象持有,有EpollerTcpServerAcceptorTcpConnectionEventLoop,主要目的是为了将一些事务放到IO线程中运行,这样可以避免很多同步的问题。

Epoller:
持有EventLoop的指针ownerLoop_,自身所有epollfd_,和由其管理的Channel的表ChannelMap,这个表是fd->Channel*的映射。

Channel:
持有EventLoop的指针loop_,负责的fd_,同时有四个回调函数接口供其使用者注册readCallback_writeCallback_errorCallback_closeCallback_。这些回调函数是一切回调函数的起点,当有事件到来时,会通过调用Channel::handleEvent,进行事件分发,调用不同的回调函数。

TcpServer:
持有EventLoop的指针loop_,一个Acceptor对象acceptor_,和由其管理的TcpConnection的表TcpConnection,这个表保存TcpConnection::name_TcpConnectionPtr的映射,还有三个回调函数接口供用户设置connectionCallback_messageCallback_writeCompleteCallback_
在构造时会将TcpServer::newConnection()注册到acceptor_newConnectionCallback_,而在TcpServer::newConnection()中会创建一个新的TcpConnection对象,并将上面三个回调函数接口和TcpServer::removeConnection4个回调函数注册到TcpConnection对象中。

Acceptor:
持有EventLoop的指针loop_,一个Socket对象acceptSocket_,管理acceptSocket_.fdChannel对象acceptChannel_,一个回调函数接口newConnectionCallback_供其拥有者TcpServer注册。
在构造函数中会将acceptSocket_和指定地址进行绑定,并将Acceptor::handleRead()注册到acceptChannel_readCallback_,而在Acceptor::handleRead()中会调用newConnectionCallback_

TcpConnection:
持有EventLoop的指针loop_,一个Socket对象socket_,管理socket_.fdChannel对象channel_,这点和Acceptor很像,其实Acceptor可以看作一个特殊的TcpConnection。除此之外还会保存双方的地址localAddr_peerAddr_,四个回调函数接口connectionCallback_messageCallback_writeCompleteCallback_closeCallback_供管理它的TcpServer注册。最后还有两个Buffer对象inputBuffer_outputBuffer_
在构造函数中,会将handleRead(),handleWrite(),handleClose(),handleError()注册到channel_中,而这四个函数又会调用上面由TcpServer注册的回调函数。

TimerQueue:
持有EventLoop的指针loop_,自身所有timerfd_,管理timerfd_Channel对象timerfdChannel_,还有保存定时器的列表timers_
在构造函数中,会将TimerQueue::handleRead()注册到timerfdChannel_readCallback_,而在TimerQueue::handleRead()会取出定时器的列表中到时的定时器,并执行对应的定时函数。

echo server

#include "TcpServer.h"
#include "EventLoop.h"
#include "InetAddress.h"
#include "utils.h"
#include 

void onConnection(const TcpConnectionPtr& conn)
{
  if (conn->connected())
  {
    printf("onConnection(): new connection [%s] from %s\n",
           conn->name().c_str(),
           conn->peerAddress().toHostPort().c_str());
  }
  else
  {
    printf("onConnection(): connection [%s] is down\n",
           conn->name().c_str());
  }
}

void onMessage(const TcpConnectionPtr& conn,
               Buffer* buf,
               int64_t receiveTime)
{
  printf("onMessage(): received %zd bytes from connection [%s] at %s\n",
         buf->readableBytes(),
         conn->name().c_str(),
         TimeToString(receiveTime).c_str());

  conn->send(buf->retrieveAsString());
}

int main()
{
  printf("main(): pid = %d\n", getpid());

  InetAddress listenAddr(9981);
  EventLoop loop;

  TcpServer server(&loop, listenAddr);
  server.setConnectionCallback(onConnection);
  server.setMessageCallback(onMessage);
  server.start();

  loop.loop();
}

陈硕认为,TCP网络编程的本质是处理三个半事件,即:

  1. 连接的建立
  2. 连接的断开:包括主动断开和被动断开
  3. 消息到达,文件描述符可读
  4. 消息发送完毕,这算半个事件

我们来讨论一下,当我们运行上面的 echo server 程序之后,使用 netcat 命令与其连接,发送信息,最后断开的整个过程网络库是怎么实现的(主要是各个回调函数是怎么被调用的)。

连接

我们知道,我们调用server.start()后,Acceptor就开始监听了,调用loop.loop()就开始处理事件,此时epoller处理的channel就只有Acceptor持有的channel。当有新连接到来,epoll_wait返回,这个channel可读,所以调用channel的readCallback_,这个回调接口被Acceptor注册了函数,所以本质上,当有新连接到来时,第一个被调用的回调函数是Acceptor::handleRead()

void Acceptor::handleRead(){
    loop_->assertInLoopThread();
    InetAddress peerAddr(0);
    int connfd = acceptSocket_.accept(&peerAddr);
    if(connfd >= 0){
        if(newConnectionCallback_){
            newConnectionCallback_(connfd, peerAddr);
        }
        else{
            close(connfd);
        }
    }
}

我们看看这个函数执行了什么,它accept后调用newConnectionCallback_,这个函数接口被TcpServer注册了函数,所以当有新连接到来时,第二个被调用的回调函数是TcpServer::newConnection()

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr){
    loop_->assertInLoopThread();
    char buf[32];
    snprintf(buf, sizeof(buf), "#%d", nextConnId_);
    ++nextConnId_;
    std::string connName = name_ + buf;

    InetAddress localAddr(getLocalAddr(sockfd));

    TcpConnectionPtr conn(new TcpConnection(loop_, connName, sockfd, localAddr, peerAddr));
    connections_[connName] = conn;
    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setCloseCallback(std::bind(&TcpServer::removeConnection, this, std::placeholders::_1));
    conn->connectEstablished();
}

在这个函数中,首先给这个新连接定义了一个name,然后建立了一个TcpConnection管理新连接,在设置好TcpConnection的回调接口后,调用了conn->connectEstablished()

void TcpConnection::connectEstablished(){
    loop_->assertInLoopThread();
    assert(state_ == kConnecting);
    setState(kConnected);

    channel_->enableReading();
    connectionCallback_(shared_from_this());
}

这个函数调用channel_->enableReading(),将Channel对象放入poller_,具体过程是修改channel_关心的IO事件,然后调用Channel::update(),这个函数会调用loop_->updateChannel(this),也就是EventLoop::updateChannel(),这个函数又会调用poller_->updateChannel(channel),最终修改poller_
然后调用了第三个回调函数connectionCallback_,这个函数是由用户注册到TcpSerer再由TcpSerer注册到TcpConnection中的,也就是 echo server 程序中的void onConnection(const TcpConnectionPtr& conn)

至此,一个新连接就被建立完成,并由一个TcpConnection对象管理,其对应的Channel对象也放到了epoller中。

信息到达

这部分比较简单,和连接的过程比较类似。客户端程序发送信息后,对应的文件描述符可读,调用TcpConnection持有的channel的readCallback_,也就是TcpConnection::handleRead()

void TcpConnection::handleRead(int64_t receiveTime){
    int savedErrno = 0;
    ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
    if(n > 0){
        messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
    }
    else if(n == 0){
        handleClose();
    }
    else{
        errno = savedErrno;
        handleError();
    }
}

这个函数在往inputBuffer_中读入数据后,调用messageCallback_,这个函数是由用户注册到TcpSerer再由TcpSerer注册到TcpConnection中的,也就是 echo server 程序中的void onMessage(const TcpConnectionPtr& conn, Buffer* buf, int64_t receiveTime)。它会调用conn->send(buf->retrieveAsString()),将接收到的数据回显回去。

信息发送

对用户来说,信息发送的工作在成功调用conn->send(buf->retrieveAsString())就结束了,剩下的工作由网络库负责。虽然目前是一个单线程的网络库,但是其很多模块是可以运用于多线程场景的,比如TcpConnection::send()

void TcpConnection::send(const std::string& message){
    if(state_ == kConnected){
        if(loop_->isInLoopThread()){
            sendInLoop(message);
        }
        else{
            loop_->runInLoop(std::bind(&TcpConnection::sendInLoop, this, message));
        }
    }
}

muduo很多同步问题都是通过runInLoop解决的,send函数如果是在本线程被调用,就会尝试直接发送,否则就调用runInLoop,这就避免了加锁的操作,如果没有这一步,有可能多个线程尝试对这个TcpConnection执行send操作,就会发生数据的混乱。

因为这是在本线程中的操作所以会直接调用TcpConnection::sendInLoop(),如果 output buffer 中没内容,会尝试直接向socket fd中写,如果没写完,或者output buffer本来就有东西,那么向output buffer中写,并channel_->enableWriting(),之所以需要这一步是因为采用LT模式,所以在需要写的时候才注册写事件,写完后需要注销写事件。

关闭连接

当客户端关闭连接(注意这里不考虑异常情况,比如断电或拔网线,这种情况只能通过主动探测得知连接是否断开,但是通过exit或kill是可以的,都会发送FIN),会发送一个0字节的数据报,通过回调会调用TcpConnection::handleRead()(具体过程同上),因为读入的字节数为0,所以调用TcpConnection::handleClose()

void TcpConnection::handleClose(){
    loop_->assertInLoopThread();
    assert(state_ == kConnected || state_ == kDisconnecting);
    channel_->disableAll();
    closeCallback_(shared_from_this());
}

之后的具体过程在前文中介绍TcpConnection中有介绍,这里就不重复。

代码地址

https://github.com/ZhaoxuWang/simple-muduo/tree/main/stage2

你可能感兴趣的:(muduo网络库设计与实现(二))