[muduo网络库]——muduo库Socket类,Acceptor类,Connector类
《Linux多线程服务端编程-使用muduo C++网络库》中给出的简化muduo库,白色是用户可见模块,灰色是用户不可见模块,我们剖析的[Muduo网络库]:三大核心库Channel类、Poller/EpollPoller类以及EventLoop类,我们接下来继续看muduo库中的其他类,这一篇,我们先来介绍灰色的Socket类,Acceptor类,Connector类,这些我们随后剖析其它类的基础
一 Socket类
1.1 Socket类概述
Socket类实际上就是封装套接字socket fd,请参考游双《Linux高性能服务器编程》第五章
1.2 重要成员变量
const int sockfd_
:服务器监听套接字
1.3重要成员函数
- 绑定服务端IP端口
bindAddress(const InetAddress& localaddr)
void Socket::bindAddress(const InetAddress& addr)
{
sockets::bindOrDie(sockfd_, addr.getSockAddr());
}
void sockets::bindOrDie(int sockfd, const struct sockaddr* addr)
{
int ret = ::bind(sockfd, addr, static_cast<socklen_t>(sizeof(struct sockaddr_in6)));
if (ret < 0)
{
LOG_SYSFATAL << "sockets::bindOrDie";
}
}
调用底层bind来绑定服务器IP端口
- 监听
listen()
void Socket::listen()
{
sockets::listenOrDie(sockfd_);
}
void sockets::listenOrDie(int sockfd)
{
int ret = ::listen(sockfd, SOMAXCONN);
if (ret < 0)
{
LOG_SYSFATAL << "sockets::listenOrDie";
}
}
- 接受连接
accept()
int Socket::accept(InetAddress* peeraddr)
{
struct sockaddr_in6 addr;
memZero(&addr, sizeof addr);
int connfd = sockets::accept(sockfd_, &addr);
if (connfd >= 0)
{
peeraddr->setSockAddrInet6(addr);//把客户端地址传出去
}
return connfd;
}
调用底层accept接受新的客户端的连接请求
- 关闭写端
void Socket::shutdownWrite()
{
sockets::shutdownWrite(sockfd_);
}
void sockets::shutdownWrite(int sockfd)
{
if (::shutdown(sockfd, SHUT_WR) < 0)
{
LOG_SYSERR << "sockets::shutdownWrite";
}
}
- 调用setsockopt来设置一些socket选项
void setTcpNoDelay(bool on);
void setReuseAddr(bool on);
void setReusePort(bool on);
void setKeepAlive(bool on);
setTcpNoDelay
禁用 Nagle 算法,避免连续发包出现延迟,这对编写低延迟网络服务很重要SOL_SOCKET
是协议级别,表示我们正在设置套接字级的选。SO_REUSEADDR 设置了一个套接字的地址复用选项。用于允许套接字在关闭后立即重新使用其地址。这在进行服务器编程时特别有用,因为在服务器重启或关闭后,它通常需要立即重新绑定到相同的地址和端口。SO_REUSEPORT
设置了一个套接字的端口复用选项。用于允许多个套接字绑定到同一个端口上。SO_KEEPALIVE
用于启用或禁用 “Keep-Alive” 功能。启用这个功能后,如果套接字在一段时间内没有活动,操作系统会发送一个数据包来检查连接是否仍然有效。
二 Acceptor类
2.1 Acceptor类概述
用于接受TCP连接, 它是TcpServer
的成员, 生命期由后者控制
。Acceptor类用于创建套接字socket,接受新客户端连接并分发连接给SubReactor(SubEventLoop)
2.2 重要成员变量
EventLoop* loop_
:监听套接字的fd由哪个EventLoop负责循环监听以及处理相应事件,其实这个EventLoop就是main EventLoopSocket acceptSocket_
:服务器监听套接字的文件描述符Channel acceptChannel_
:把acceptSocket_及其感兴趣事件和事件对应的处理函数进行封装newConnectionCallback_
:非常重要的成员函数,在TcpServer
构造函数中通过acceptor_->setNewConnetionCallback(std::bind(&TcpServer::newConnection, this, std::placeholders::_1,std::placeholders::_2));
将TcpServer::newConnection
函数注册给了这个成员变量。这个TcpServer::newConnection
函数的功能是通过轮询EventLoop *ioLoop = threadPool_->getNextLoop()
;选择一个subEventLoop,并把已经接受的连接分发给这个subEventLoop。bool listening_
:是一个标志位int idleFd_
:一个空的文件描述符
2.3 重要成员函数
- 构造函数
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
: loop_(loop),
acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),//创建socket
acceptChannel_(loop, acceptSocket_.fd()),//封装成channel
listening_(false),
idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
{
assert(idleFd_ >= 0);
acceptSocket_.setReuseAddr(true); //更改TCP选项
acceptSocket_.setReusePort(reuseport);
acceptSocket_.bindAddress(listenAddr); //绑定套接字
acceptChannel_.setReadCallback(
std::bind(&Acceptor::handleRead, this));//设置回调函数
}
- 要知道
Acceptor
是TcpServer
管理的,在构造函数中,首先是TcpServer给的loop,这里实际就是mainloop
- 通过
createNonblockingOrDie
创建acceptSocket_
套接字,一个服务器只创建一个监听socket文件描述符,它在该服务器的生命周期内一直存在,而内核为由每个服务器进程接受的客户端连接自动创建一个已连接socket描述符在accept()
函数中,当服务器完成了对某个客户的服务,相应的已连接的socket描述符就被关闭 - 将loop和socket封装成Channel
- 给listenning_一个初始化
- 通过调用Socket中的setReuseAddr、setReusePort更改了一些选项。
- 绑定了套接字
- 绑定一个回调,用于新用户的连接
listen()
函数
void Acceptor::listen()
{
loop_->assertInLoopThread();
listening_ = true;
acceptSocket_.listen();
acceptChannel_.enableReading();
}
实际上调用了Sockert的listen()函数,本质上调用了内核系统的listen(),同时将acceptChannel及其感兴趣事件(可读事件)注册到main EventLoop的Epollpoll中,总结就是让mainloop事件监听器去监听acceptSocket_
headleRead()
函数
void Acceptor::handleRead()
{
loop_->assertInLoopThread();
InetAddress peerAddr;
//FIXME loop until no more
int connfd = acceptSocket_.accept(&peerAddr);
if (connfd >= 0)
{
// string hostport = peerAddr.toIpPort();
// LOG_TRACE << "Accepts of " << hostport;
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.
if (errno == EMFILE)
{
::close(idleFd_);
idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
::close(idleFd_);
idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
}
}
}
建立连接,通过调用Socket的accept
函数,底层调用系统的accept
函数,返回一个已连接的socket描述字,这样连接就建立起来了,同时内部调用成员变量newConnectionCallback_
保存的函数,当mainLoop监听到acceptChannel_
上发生了可读事件时(新用户连接事件),就是调用这个handleRead( )
方法,内部调用newConnetionCallback
_,也就是TcpServer设置的一个回调函数setNewConnetionCallback
,绑定了TcpServer::newConnection
,通过 轮询算法,选择一个subloop
,分发当前的新客户端的Channel,并且绑定了一些回调
需要注意的是:在muduo库的源码中,实际上还创建了一个空的文件描述符idleFd_
这个思想也是很巧妙的,在调用accept的过程中,如果已用文件描述符过多,accept会返回-1,构造函数中注册的idleFd_就派上用场了。当前文件描述符过多,无法接收新的连接。但是由于我们采用LT模式,如果无法接收,可读事件会一直触发。那么在这个地方的处理机制就是,关掉之前创建的空的idleFd_,然后去accept,这样进行了连接,让这个事件不会一直触发,然后再关掉该文件描述符
,重新将它设置为空文件描述符。这样就优雅的解决 EMFIFE 问题
三 Connector类
3.1 Connector类概述
用于发起TCP连接, 它是TcpClient
的成员, 生命期由后者控制
。它负责主动发起连接,不负责创建socket,只负责连接的建立,和Acceptor类恰好相反。Connector
具有重连的功能和停止连接的功能,连接成功建立后返回到TcpClient
。
3.2 重要成员变量
EventLoop *loop_
:所属的EventLoopInetAddress serverAddr_
: 服务器端的地址States state_
:客户端的状态 ,有三种状态
enum States
{
kDisconnected, //无连接
kConnecting, //请求连接
kConnected //已连接
};
std::unique_ptr<Channel> channel_
:Connector所对应的ChannelnewConnectionCallback_
: 连接成功回调函数int retryDelayMs_
:重连间隔时间
与Acceptor 相比少了一个 acceptSocket_ 成员,因为Connector 是创建一个新的sockfd 并connect 它,创建过程如下:Connector::start()–>Connector::startInLoop()–>void Connector::connect()。
3.3 重要成员函数
- 外部接口函数
start()
void Connector::start()
{
connect_ = true;
loop_->runInLoop(std::bind(&Connector::startInLoop, this)); // FIXME: unsafe
}
- startInloop()函数
void Connector::startInLoop()
{
loop_->assertInLoopThread();
assert(state_ == kDisconnected);
if (connect_)//调用前必须connect_为true,start()函数中会这么做
{
connect();
}
else
{
LOG_DEBUG << "do not connect";
}
}
- connect()函数
void Connector::connect()
{
int sockfd = sockets::createNonblockingOrDie(serverAddr_.family());
int ret = sockets::connect(sockfd, serverAddr_.getSockAddr());
int savedErrno = (ret == 0) ? 0 : errno;
switch (savedErrno)
{
case 0:
case EINPROGRESS:
case EINTR:
case EISCONN:
connecting(sockfd);
break;
case EAGAIN:
case EADDRINUSE:
case EADDRNOTAVAIL:
case ECONNREFUSED:
case ENETUNREACH:
retry(sockfd);
break;
case EACCES:
case EPERM:
case EAFNOSUPPORT:
case EALREADY:
case EBADF:
case EFAULT:
case ENOTSOCK:
LOG_SYSERR << "connect error in Connector::startInLoop " << savedErrno;
sockets::close(sockfd);
break;
default:
LOG_SYSERR << "Unexpected error in Connector::startInLoop " << savedErrno;
sockets::close(sockfd);
// connectErrorCallback_();
break;
}
}
这个函数虽然很长,但实际上就是设置套接字为非阻塞,然后底层调用socket的conenct()函数,进而判断errno采取相应的操作
errno
为EINPROGRESS
、EINTR
、EISCONN
//如果连接成功
void Connector::connecting(int sockfd)
{
setState(kConnecting);
assert(!channel_);
//Channel与sockfd关联
channel_.reset(new Channel(loop_, sockfd));
//设置可写回调函数,这时候如果socket没有错误,sockfd就处于可写状态
channel_->setWriteCallback(
boost::bind(&Connector::handleWrite, this)); // FIXME: unsafe
//设置错误回调函数
channel_->setErrorCallback(
boost::bind(&Connector::handleError, this)); // FIXME: unsafe
// channel_->tie(shared_from_this()); is not working,
// as channel_ is not managed by shared_ptr
//关注可写事件
channel_->enableWriting();
}
errno
为EAGAIN
等错误码- 说明连接暂时失败,但是仍可能成功,需要重连。调用retry()函数
//重连函数,采用back-off策略重连,也就是退避策略
//也就是重连时间逐渐延长,0.5s,1s,2s,...一直到30s
void Connector::retry(int sockfd)
{
sockets::close(sockfd); //先关闭连接
setState(kDisconnected);
if (connect_)
{
LOG_INFO << "Connector::retry - Retry connecting to " << serverAddr_.toIpPort()
<< " in " << retryDelayMs_ << " milliseconds. ";
//隔一段时间后重连,重新启用startInLoop
loop_->runAfter(retryDelayMs_/1000.0,
boost::bind(&Connector::startInLoop, shared_from_this()));
//间隔时间2倍增长
retryDelayMs_ = std::min(retryDelayMs_ * 2, kMaxRetryDelayMs);
}
else //超出最大重连时间后,输出连接失败
{
LOG_DEBUG << "do not connect";
}
}
- 彻底失败,返回errno为EACCES等错误码
这种情况只能关掉sockfd,因为再怎么试也成功不了的。
sockets::close(sockfd);