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_