muduo网络库——Acceptor

OK了兄弟们。
今天咱们分析一手,muduo网络库的——Acceptor类。

Acceptor类是在TcpServer类中被使用的,可以说是TcpServer的头号马仔。
顾名思义啊,咱们的Acceptor类主要的功能就是进行::accept的功能,也就是接收新连接。
它会在server接收到新的连接之后触发响应函数,接收到对应的客户端。然后触发老大(TcpServer)交代给它的任务,也就是把接收到的客户端分发给下层的打工loop们中的一个,让打工loop处理这个客户端的读写操作。

首先我们看头文件:

class Acceptor : noncopyable
{
 public:
  typedef std::function<void (int sockfd, const InetAddress&)> NewConnectionCallback;//有新客户端连接上来的时候触发

  Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport);
  ~Acceptor();

  void setNewConnectionCallback(const NewConnectionCallback& cb)
  { newConnectionCallback_ = cb; }

  void listen();

  bool listening() const { return listening_; }
  
 private:
  void handleRead();

  EventLoop* loop_;//我们的base loop,或者叫main loop
  Socket acceptSocket_;
  Channel acceptChannel_;
  NewConnectionCallback newConnectionCallback_;
  bool listening_;
  int idleFd_;//平平无奇的int类型变量,隐隐透露出一丝优雅的气质
};

很质朴的头文件。

最后一个变量的作用我们会在下文中提到。我们先看主要工作内容的流程。

首先是构造函数:

Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
  : loop_(loop),
    acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),
    acceptChannel_(loop, acceptSocket_.fd()),
    listening_(false),
    idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
{
  assert(idleFd_ >= 0);
  acceptSocket_.setReuseAddr(true);
  acceptSocket_.setReusePort(reuseport);
  acceptSocket_.bindAddress(listenAddr);
  acceptChannel_.setReadCallback(
      std::bind(&Acceptor::handleRead, this));
}

初始化列表内容如下:

1.这里传进来的 loop 是我们的 base loop 。

2.然后我们会创建一个 acceptSocket_ 。

3.用 acceptSocket_ 初始化 acceptChannel_ 。

4.给 listening_ 一个初始值。

5.对神秘文件描述符 idleFd_ 进行初始化。

函数体中内容如下:

1.判断神秘文件是否打开成功。

2.启用套接字的地址复用功能。

3.启用套接字的端口复用功能。

4.会调用 socket 的 bindOrDie (不 bind 就去死)。

5.在 Channel 设置读回调。由于这是 acceptChannel_ 故而对应的读事件就是有新连接上来。

Listen函数:

void Acceptor::listen()
{
  loop_->assertInLoopThread();
  listening_ = true;
  acceptSocket_.listen();
  acceptChannel_.enableReading();
}

1.做一个判断,如果不是EventLoop绑定的线程直接退出程序。

2.更新状态。

3.正式开始listen。

4.在 acceptChannel_ 上设置对读事件的监听,确保我们可以触发 handleRead() 函数。

handleRead() 读回调函数:

void Acceptor::handleRead()
{
  loop_->assertInLoopThread();
  InetAddress peerAddr;

  int connfd = acceptSocket_.accept(&peerAddr);
  if (connfd >= 0)
  {
    if (newConnectionCallback_)
    {
      newConnectionCallback_(connfd, peerAddr);
    }
    else
    {
      sockets::close(connfd);
    }
  }
  else
  {//关于文件描述符不足的处理方案
    LOG_SYSERR << "in Acceptor::handleRead";
	
    if (errno == EMFILE)
    {
      ::close(idleFd_);
      idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
      ::close(idleFd_);
      idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
    }
  }
}

天气很冷,手很凉,这个函数我们就不逐句分析了。

聪明的读者大大应该不难看懂,这里主要是进行了一个对新连接的客户端的接收。然后调用了 newConnectionCallback_() 函数,这个函数是老大交代给我们的任务。也就是 TcpServer 类的 TcpServer::newConnection() 函数。

后面是一些错误处理,关于文件描述符不足的处理方案我们会放到最后说。你提前看懂了我也没办法。

我们先看老大交代的任务是什么:

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
  loop_->assertInLoopThread();
  EventLoop* ioLoop = threadPool_->getNextLoop();
  char buf[64];
  snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
  ++nextConnId_;
  string connName = name_ + buf;

  LOG_INFO << "TcpServer::newConnection [" << name_
           << "] - new connection [" << connName
           << "] from " << peerAddr.toIpPort();
  InetAddress localAddr(sockets::getLocalAddr(sockfd));
  
  TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                          connName,
                                          sockfd,
                                          localAddr,
                                          peerAddr));
  connections_[connName] = conn;
  conn->setConnectionCallback(connectionCallback_);
  conn->setMessageCallback(messageCallback_);
  conn->setWriteCompleteCallback(writeCompleteCallback_);
  conn->setCloseCallback(
      std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
  ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}

1.以上函数首先做了一个判断,如果不是EventLoop绑定的线程直接退出程序。

2.然后会从线程池里面拿到一个空闲的loop(这里内部使用的是轮询的方法)。

3.再然后给对应连接起一个名字(TcpConnection类的成员)。nextConnId_ 变量是用来记录线程池中,下一个线程池的标识(因为我们刚拿了一个,所以把标识更新到下一个。)。生成一个本地的地址(TcpConnection类的成员)。然后生成一个 TcpConnection 类的对象(muduo网络库中每一个客户端连接都要对应一个 TcpConnection 类的对象 )。

4.connections_ 是一个 map<string, TcpConnectionPtr> 里面存的是 TcpServer 的所有的连接。

5.后面的内容是给生成的连接类设置各种情况的回调函数。

以上是Acceptor类的总体上的工作流程。

文件描述符不足的处理方案:

在不断的接收客户端,生成客户端文件描述符的过程中,如果有大量的客户端连接上来,可能会出现文件描述符被耗尽的情况。这时候如果又有一个新的客户端尝试连接上来的,由于我们无法处理此连接,会导致 acceptChannel_ 的读事件一直触发。非常烦人。

{//关于文件描述符不足的处理方案
    LOG_SYSERR << "in Acceptor::handleRead";
	
    if (errno == EMFILE)
    {
      ::close(idleFd_);
      idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
      ::close(idleFd_);
      idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
    }
  }

以上为 handleRead() 函数中对该部分内容的处理方式(也就是之前埋的伏笔)。

在这里我们的文件描述符 idleFd_ 就会站出来释放自己,为我们的服务端换来一个文件描述符的空位。我们使用这个文件描述符接收客户端,然后再关掉这个客户端。最后在重新按原本的方式打开文件,占用这个文件描述符。仿佛一切都没有发生过。优雅的解决了文件描述符不足的问题。

“事了拂衣去,深藏功与名。” —— idleFd_

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值