muduo网络库剖析——接受新连接Acceptor类
- 前情
- 从muduo到my_muduo
- 概要
- 框架与细节
- 成员
- 函数
- 使用方法
- 源码
- 结尾
前情
从muduo到my_muduo
作为一个宏大的、功能健全的muduo库,考虑的肯定是众多情况是否可以高效满足;而作为学习者,我们需要抽取其中的精华进行简要实现,这要求我们足够了解muduo库。
做项目 = 模仿 + 修改,不要担心自己学了也不会写怎么办,重要的是积累,学到了这些方法,如果下次在遇到通用需求的时候你能够回想起之前的解决方法就够了。送上一段话!
概要
对于Acceptor,是mainloop用来接收新连接的类。
框架与细节
成员
Acceptor在mainloop中,acceptor作为新连接的类,需要接受监听新连接。如果说poller监听比如epollpoller使用epoll_wait,pollpoller使用poll,selectpoller使用select,那acceptor监听用户端的新连接则用socket套接字。于是还需要acceptSocket套接字,另外为了acceptor本身的事件触发有所反馈,使用一个acceptChannel来监听acceptor内部发生的事件。对于发生的新连接,会使用到NewConnectionCallback新连接的回调函数来处理新连接。最后还需要acceptor的一个状态位来判断是否处在监听状态下。
函数
首先是Acceptor的构造函数,传参是所属的mainloop,用于监听用户端连接的网络地址listenAddr,以及是否需要复用网络端口。对于网络地址与网络端口这些套接字选项,在网络编程中是很重要的。对于acceptSocket套接字,因为在socket类中只实现了bind绑定,listen监听,accept接受用户端的新连接以及setsockopt设置套接字选项。对于套接字的创建socket还没有实现,因此我们如果要完成acceptSocket的实现,首先就是完成socket的方法,这里单独创建了一个createNonblocking。后面就是对acceptSocket的相关端口绑定和选项设定。对于Channel而言,作为一个监听新连接的类,读回调事件会经常发生,因此需要设置读回调函数,是之后需要调用到的handleRead。
对于createNonblocking,其实就是调用了socket函数,对于socket编程,想要更加详细了解的uu可以看我前面写的socket类的实现,里面对socket编程进行了详细的讲解。
对于Acceptor的析构函数,主要是对Channel的一些析构操作,这里我觉得可以在尝试加一些对socket的析构操作。
对于listen函数,其实就是将Acceptor的监听状态位置1,并让AcceptSocket进入监听状态,对Channel能对读事件感兴趣。
对于新连接回调函数handleRead,新连接请求肯定需要accept接受用户端的新连接,得到的连接套接字connfd如果 > 0,就调用新连接回调函数,具体逻辑就是轮询找到subLoop,唤醒,分发当前的新客户端的Channel。如果 不是 > 0,就error。对于具体的error,如果是EMFILE。
错误代码 EMFILE(文件太多)通常表示程序试图打开的文件数量超过了系统允许的最大数量。在 C++ 语言中,它是一个头文件中的宏定义,表示文件打开操作失败,并且通常会在 <sys/types.h> 或 <stdio.h> 头文件中定义。
实现原理:EMFILE 的实现可能涉及到文件打开操作的计数,当程序尝试打开一个新文件时,计数器增加。如果计数器超过某个阈值(通常是系统限制或用户定义的),则打开操作失败,返回错误代码 EMFILE。
用途:EMFILE 错误代码通常用于检测程序是否因为打开文件过多而导致了性能问题或资源不足。在现代操作系统中,文件打开操作通常是高效的,不会导致性能问题。但是,如果你的程序需要打开大量文件,可能需要考虑优化打开文件的操作,或者降低对文件的数量要求。
注意事项:在实际编程中,要正确处理 EMFILE 错误,你需要知道系统允许的最大文件打开数量(通常为1024或2048),并在程序中设置一个阈值。当尝试打开新文件时,检查当前打开的文件数量是否超过阈值,如果超过,则适当限制文件数量或采取其他优化措施。
使用方法
源码
//Acceptor.h
#pragma once
#include "noncopyable.h"
#include "Socket.h"
#include "Channel.h"
#include <functional>
class EventLoop;
class InetAddress;
class Acceptor : noncopyable {
public:
using NewConnectionCallback = std::function<void(int sockfd, const InetAddress&)>;
Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport);
~Acceptor();
void setNewConnectionCallback(const NewConnectionCallback &cb) {
newConnectionCallback_ = cb;
}
bool listening() const { return listening_; }
void listen();
private:
void handleRead();
EventLoop *loop_; // Acceptor用的就是用户定义的那个baseLoop,也称作mainLoop
Socket acceptSocket_;
Channel acceptChannel_;
NewConnectionCallback newConnectionCallback_;
bool listening_;
};
//Acceptor.cc
#include "Acceptor.h"
#include "Log.h"
#include "InetAddress.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
static int createNonblocking() {
int sockfd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (sockfd < 0) {
LOG_FATAL("%s--%s--%d--%d : socket error\n", __FILE__, __FUNCTION__, __LINE__, errno);
}
}
Acceptor::Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport)
: loop_(loop)
, acceptSocket_(createNonblocking()) // socket
, acceptChannel_(loop, acceptSocket_.fd())
, listening_(false) {
acceptSocket_.setReuseAddr(true);
acceptSocket_.setReusePort(true);
acceptSocket_.bind(listenAddr); // bind
// TcpServer::start() Acceptor.listen 有新用户的连接,要执行一个回调(connfd=》channel=》subloop)
// baseLoop => acceptChannel_(listenfd) =>
acceptChannel_.setReadCallback(std::bind(&Acceptor::handleRead, this));
}
Acceptor::~Acceptor() {
acceptChannel_.disableAll();
acceptChannel_.remove();
}
void Acceptor::listen() {
listening_ = true;
acceptSocket_.listen(); // listen
acceptChannel_.enableReading(); // acceptChannel_ => Poller
}
// listenfd有事件发生了,就是有新用户连接了
void Acceptor::handleRead() {
InetAddress peerAddr;
int connfd = acceptSocket_.accept(peerAddr);
if (connfd >= 0)
{
if (newConnectionCallback_)
{
newConnectionCallback_(connfd, peerAddr); // 轮询找到subLoop,唤醒,分发当前的新客户端的Channel
}
else
{
::close(connfd);
}
}
else
{
LOG_ERROR("%s--%s--%d--%d : accept error\n", __FILE__, __FUNCTION__, __LINE__, errno);
if (errno == EMFILE)
{
LOG_ERROR("%s--%s--%d--%d : sockfd reached limit error\n", __FILE__, __FUNCTION__, __LINE__, errno);
}
}
}
结尾
以上就是接受新连接Acceptor类的相关介绍,以及我在进行项目重写的时候遇到的一些问题,和我自己的一些心得体会。发现写博客真的会记录好多你的成长,而且对于一个好的项目,写博客也是证明你确实有过深度思考,并且在之后面试或者工作时遇到同样的问题能够进行复盘的一种有效的手段。所以,希望uu们也可以像我一样,养成写博客的习惯,逐渐脱离菜鸡队列,向大佬前进!!!加油!!!
也希望我能够完成muduo网络库项目的深度学习与重写,并在功能上能够拓展。也希望在完成这个博客系列之后,能够引导想要学习muduo网络库源码的人,更好地探索这篇美丽繁华的土壤。致敬chenshuo大神!!!
鉴于博主只是一名平平无奇的大三学生,没什么项目经验,所以可能很多东西有所疏漏,如果有大神发现了,还劳烦您在评论区留言,我会努力尝试解决问题!