通常服务器在处理客户端连接请求时,为了不阻塞在accept
函数上,会将监听套接字注册到io复用函数中,当客户端请求连接时,监听套接字变为可读,随后在回调函数调用accept
接收客户端连接。muduo将这一部分封装成了Acceptor
类,用于执行接收客户端请求的任务。
类的定义如下,主要就是监听套接字变为可读的回调函数
class EventLoop;
class InetAddress;
///
/// Acceptor of incoming TCP connections.
///
/*
* 对TCP socket, bind, listen, accept的封装
* 将sockfd以Channel的形式注册到EventLoop的Poller中,检测到sockfd可读时,接收客户端
*/
class Acceptor : noncopyable
{
public:
typedef std::function<void (int sockfd, const InetAddress&)> NewConnectionCallback;
Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport);
~Acceptor();
/* 由服务器TcpServer设置的回调函数,在接收完客户端请求后执行,用于创建TcpConnection */
void setNewConnectionCallback(const NewConnectionCallback& cb)
{ newConnectionCallback_ = cb; }
bool listenning() const { return listenning_; }
/* 调用listen函数,转为监听套接字,同时将监听套接字添加到Poller中 */
void listen();
private:
/* 回调函数,当有客户端请求连接时执行(监听套接字变为可读) */
void handleRead();
/* 事件驱动主循环 */
EventLoop* loop_;
/* 封装socket的一些接口 */
Socket acceptSocket_;
/* Channel,保存着sockfd,被添加到Poller中,等待被激活 */
Channel acceptChannel_;
/*
* 当有客户端连接时首先内部接收连接,然后调用的用户提供的回调函数
* 客户端套接字和地址作为参数传入
*/
NewConnectionCallback newConnectionCallback_;
bool listenning_;
/*
* Tcp连接建立的流程
* 1.服务器调用socket,bind,listen开启监听套接字监听客户端请求
* 2.客户端调用socket,connect连接到服务器
* 3.第一次握手客户端发送SYN请求分节(数据序列号)
* 4.服务器接收SYN后保存在本地然后发送自己的SYN分节(数据序列号)和ACK确认分节告知客户端已收到
* 同时开启第二次握手
* 5.客户端接收到服务器的SYN分节和ACK确认分节后保存在本地然后发送ACK确认分节告知服务器已收到
* 此时第二次握手完成,客户端connect返回
* 此时,tcp连接已经建立完成,客户端tcp状态转为ESTABLISHED,而在服务器端,新建的连接保存在内核tcp
* 连接的队列中,此时服务器端监听套接字变为可读,等待服务器调用accept函数取出这个连接
* 6.服务器接收到客户端发来的ACK确认分节,服务器端调用accept尝试找到一个空闲的文件描述符,然后
* 从内核tcp连接队列中取出第一个tcp连接,分配这个文件描述符用于这个tcp连接
* 此时服务器端tcp转为ESTABLISHED,三次握手完成,tcp连接建立
*
* 服务器启动时占用的一个空闲文件描述符,/dev/null,作用是解决文件描述符耗尽的情况
* 原理如下:
* 当服务器端文件描述符耗尽,当客户端再次请求连接,服务器端由于没有可用文件描述符
* 会返回-1,同时errno为EMFILE,意为描述符到达hard limit,无可用描述符,此时服务器端
* accept函数在获取一个空闲文件描述符时就已经失败,还没有从内核tcp连接队列中取出tcp连接
* 这会导致监听套接字一直可读,因为tcp连接队列中一直有客户端的连接请求
*
* 所以服务器在启动时打开一个空闲描述符/dev/null(文件描述符),先站着'坑‘,当出现上面
* 情况accept返回-1时,服务器暂时关闭idleFd_让出'坑',此时就会多出一个空闲描述符
* 然后再次调用accept接收客户端请求,然后close接收后的客户端套接字,优雅的告诉
* 客户端关闭连接,然后再将'坑'占上
*/
int idleFd_;
};
一个不好理解的变量是idleFd_;
,它是一个文件描述符,这里是打开"/dev/null"
文件后返回的描述符,用于解决服务器端描述符耗尽的情况。
如果当服务器文件描述符耗尽后,服务器端accept
还没等从tcp连接队列中取出连接请求就已经失败返回了,此时内核tcp队列中一直有客户端请求,内核会一直通知监听套接字,导致监听套接字一直处于可读,在下次直接poll
函数时会直接返回。
解决的办法就是在服务器刚启动时就预先占用一个文件描述符,通常可以是打开一个文件,这里是"/dev/null"
。此时服务器就有一个空闲的文件描述符了,当出现上述情况无法取得tcp连接队列中的请求时,先关闭这个文件让出一个文件描述符,此时调用accept
函数再次接收,由于已经有一个空闲的文件描述符了,accept
会正常返回,将连接请求从tcp队列中取出,然后优雅的关闭这个tcp连接(调用close
函数),最后再打开"/dev/null"
这个文件把”坑“占住。
成员函数的实现也有比较重点的地方,首先是构造函数
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
: loop_(loop),
acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),
acceptChannel_(loop, acceptSocket_.fd()),
listenning_(false),
idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
{
assert(idleFd_ >= 0);
/*
* setsockopt设置套接字选项SO_REUSEADDR,对于端口bind,如果这个地址/端口处于TIME_WAIT,也可bind成功
* int flag = 1;
* setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
*/
acceptSocket_.setReuseAddr(true);
/*
* setsockopt设置套接字选项SO_REUSEPORT,作用是对于多核cpu,允许在同一个<ip, port>对上运行多个相同服务器
* 内核会采用负载均衡的的方式分配客户端的连接请求给某一个服务器
*/
acceptSocket_.setReusePort(reuseport);
acceptSocket_.bindAddress(listenAddr);
/* Channel设置读事件的回调函数,此时还没有开始监听这个Channel,需要调用Channel::enableReading() */
acceptChannel_.setReadCallback(
std::bind(&Acceptor::handleRead, this));
}
构造