Muduo网络库简介
muduo 是一个基于 Reactor 模式的现代 C++ 网络库,作者陈硕。它采用非阻塞 IO 模型,基于事件驱动和回调,原生支持多核多线程,适合编写 Linux 服务端多线程网络应用程序。
muduo网络库的核心代码只有数千行,在网络编程技术学习的进阶阶段,muduo是一个非常值得学习的开源库。目前我也是刚刚开始学习这个网络库的源码,希望将这个学习过程记录下来。这个网络库的源码已经发布在GitHub上,可以点击这里阅读。目前Github上这份源码已经被作者用c++11重写,我学习的版本是没有使用c++11版本的。不过二者大同小异,核心思想是没有变化的。点这里可以看我的源代码。从笔记十七开始记录muduo的net库的实现过程。如果你需要看一下基础库(base)的复现过程,可以点击这里:muduo的base库实现过程。而网络库的笔记在这里:
muduo网络库源码复现笔记(十七):什么都不做的EventLoop
muduo网络库源码复现笔记(十八):Reactor的关键结构
muduo网络库源码复现笔记(十九):TimeQueue定时器
muduo网络库源码复现笔记(二十):EventLoop::runInloop()函数和EventLoopThread类
0 介绍
通过前面几节的内容,Reactor事件处理框架已经搭的差不多了。从本节开始,我们可以实现一个非阻塞的TCP网络编程库。首先是要封装Acceptor类作为服务器的监听者,但是封装之前我们要做许多预备工作。
1 SocektsOps
SocketsOps中对Sockets套接字Api做了一些封装,包括socket、bind、listen、accept、connect、close等。这些封装不难,阅读代码即可明白它的意思。如果你对这些Api不是很熟悉,可以看这篇文章。其中比较值得注意的是createNonblockingOrDie方法,它可以返回一个非阻塞和close-on-exec的文件描述符。
int sockets::createNonblockingOrDie()
{
#if VALGRIND
int sockfd = ::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
{
LOG_SYSFATAL << "sockets::createNonBlockingOrDie";
}
setNonBlockingAndCloseOnExec(sockfd);
#else
//after Linux 2.6.27
int sockfd = ::socket(AF_INET,
SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,IPPROTO_TCP);
if(sockfd < 0)
{
LOG_SYSFATAL << "sockets::createNonBlockingOrDie";
}
#endif
return sockfd;
}
2 InetAddress类
InetAddress类实际上是对struct sockaddr_in的封装。看一下它的构造函数,可以只传入端口号,那么addr_的ip将会是本机(INADDR_ANY)。也可以传入ip和端口号来进行设置。由于muduo只支持IPv4,所以通信domain均为AF_INET。其余函数见注释。
class InetAddress : public muduo::copyable
{
public:
//mostly uesd in TcpServer listening
explicit InetAddress(uint16_t port);
InetAddress(const StringPiece& ip,uint16_t port);
InetAddress(const struct sockaddr_in& addr)
: addr_(addr)
{ }
string toIp() const;//返回地址Ip(字符串形式)
string toIpPort() const;//返回地址和端口号(字符串形式)
string toHostPort() const __attribute__((deprecated))//弃用
{ return toIpPort(); }
const struct sockaddr_in& getSockAddrInet() const {return addr_;}
void setSockAddrInet(const struct sockaddr_in& addr) {addr_ = addr;}
uint32_t ipNetEndian() const {return addr_.sin_addr.s_addr;}
uint16_t portNetEndian() const {return addr_.sin_port;}
private:
struct sockaddr_in addr_;
};
3 Socket类
Socket类是对文件描述符的封装,它的成员函数依然多是对SocketOps的调用。
class Socket : boost::noncopyable
{
public:
explicit Socket(int sockfd)
: sockfd_(sockfd)
{ }
~Socket();
int fd() const {return sockfd_;}
//绑定地址
void bindAddress(const InetAddress& localaddr);
//启动监听
void listen();
int accept(InetAddress* peeraddr);
//半关闭写
void shutdownWrite();
//禁用 Nagle?
void setTcpNoDelay(bool on);
//端口复用
void setReuseAddr(bool on);
void setKeepAlive(bool on);
private:
const int sockfd_;
};
4 Acceptor
Acceptor用于接受新的TCP连接,通过回调通知使用者。它由TcpServer(后面讲)使用。它的封装原理类似于TimerQueue,我们使用它的时候,首先将它注册到EvetLoop中,Acceptor的acceptChannel_将加入Eventloop的Poller中,至于我们就可以监听来自其他Socket的连接了。
class Acceptor : boost::noncopyable
{
public:
typedef boost::function<void(int sockfd,
const InetAddress&)> NewConnectionCallback;
Acceptor(EventLoop* loop,const InetAddress& listenAddr);
~Acceptor();
void setNewConnectionCallback(const NewConnectionCallback& cb)
{ newConnectionCallback_ = cb; }
bool listenning() const {return listenning_;}
void listen();
private:
void handleRead();
EventLoop* loop_;
Socket acceptSocket_;
Channel acceptChannel_;
NewConnectionCallback newConnectionCallback_;
bool listenning_;
int idleFd_;
};
handleRead是作为acceptChannel_的回调函数使用的,NewConnectionCallback是连接之后执行的回调。另外值得注意的是muduo对于文件描述符耗尽的情况的处理,它的方法是预先准备一个文件描述符idleFd_,如果出现耗尽的情况,则使用这个文件描述符接受,然后立刻断开连接,然后重新使用idleFd_打开空白文件。
void Acceptor::handleRead()
{
loop_ -> assertInLoopThread();
InetAddress peerAddr(0);
int connfd = acceptSocket_.accept(&peerAddr);
if(connfd >= 0)
{
if(newConnectionCallback_)
{
newConnectionCallback_(connfd,peerAddr);
}
else
{
sockets::close(connfd);
}
}
else
{
if(errno = EMFILE)
{
::close(idleFd_);
idleFd_ = ::accept(acceptSocket_.fd(),NULL,NULL);
::close(idleFd_);
idleFd_ = ::open("/dev/null",O_RDONLY | O_CLOEXEC);
}
}
}