先序
对服务器来说,一开始肯定会分配一个监听套接字用来接收客户端的连接;这一部分实际上就是由 Acceptor 实现的;muduo 库对 socket 进行了一系列的封装,这部分内容不是很难,但是很杂有兴趣可以自行查看;Endian.h 封装了字节序转换函数、SocketOps 封装了 socket 的系统调用函数、Socket 用 RAII 方法封装了 socket 文件描述符、InetAddress 封装了网络地址;
Acceptor 源码分析
重要的变量
EventLoop* loop_; //所属的 EvenLoop
Socket acceptSocket_; //listen socket
Channel acceptChannel_; //绑定 listen socket 的通道
typedef std::function<void (int sockfd, const InetAddress&)> NewConnectionCallback;
//新连接到来时的回调函数
NewConnectionCallback newConnectionCallback_;
bool listening_;
int idleFd_; //一个文件描述符
重要的函数
Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport);
~Acceptor();
void setNewConnectionCallback(const NewConnectionCallback& cb)
{ newConnectionCallback_ = cb; }
void listen();
void handleRead();
具体实现
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
: loop_(loop),
//根据传递进来的地址类型创建文件描述符
acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),
//监听套接字
acceptChannel_(loop, acceptSocket_.fd()),
listening_(false),
//?为什么这里要打开一个 idleFd_?
idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
{
assert(idleFd_ >= 0);
//设置地址、端口重复利用
acceptSocket_.setReuseAddr(true);
acceptSocket_.setReusePort(reuseport);
//绑定地址
acceptSocket_.bindAddress(listenAddr);
//为 channel 设置一个 handlerRead 回调函数,即可读事件到来后触发的函数
acceptChannel_.setReadCallback(
std::bind(&Acceptor::handleRead, this));
}
- Acceptor 构造函数主要的功能就是创建 listen socket,绑定地址和设置读回调函数;
- 为什么要使用 idleFd_ ?
- 构造函数并没有打开通道的读事件、有没有调用 listen socket 的监听函数;
//由于 listen socket 是用 RAII 封装好的,所以不需要我们手动关闭
Acceptor::~Acceptor()
{
//关闭通道
acceptChannel_.disableAll();
acceptChannel_.remove();
//关闭 idleFd_
::close(idleFd_);
}
//只能在 IO 线程中调用
void Acceptor::listen()
{
loop_->assertInLoopThread();
listening_ = true;
//监听套接字
acceptSocket_.listen();
//关注套接字的可读事件
acceptChannel_.enableReading();
}
- 调用完 listen() 后,listen socket 的读事件才被监听,但客户端连接服务器时就会触发 listen socket 的可读事件,调用 handleRead();【这个函数是本节的重点】
//只能在 IO 线程中调用
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";
// Read the section named "The special problem of
// accept()ing when you can't" in libev's doc.
// By Marc Lehmann, author of libev.
//如果客户端套接字太多了,我们需要对其进行处理
//如果是电平触发,我们不去处理的话,缓冲区一直为高电平就会 busyloop
//如果是边沿触发,我们不去处理的话,当文件描述符可写入时,由于缓冲区一直为高电平,不会触发事件
if (errno == EMFILE)
{
//我们之前已经获得了一个文件描述符,将其关闭
::close(idleFd_);
//此时就可以接收,相当于腾出来一个位置了,缓冲区的内容被读取了
idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
//再将其关闭,发送一个关闭命令
::close(idleFd_);
//再次取得这个 fd
idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
}
}
}