陈硕 muduo
参考资料
muduo源码剖析
Muduo是一个基于Reactor模式的现代C++网络库,它采用非阻塞IO模型,基于时间驱动和回调,原生支持多核多线程,适合编写Linux服务端多线程网络应用程序。
muduo基于Reactor模式实现
Reactor模式也是目前大多数Linux端高性能网络编程框架和网络应用所选择的主要架构,例如内存数据库Redis和Java的Netty库等。
muduo 架构
整个架构依照reactor模式,如如下图:
所谓的reactor模型,是有一个循环的过程,监听对应事件是否触发,触发时调用对应的callback函数
进行处理;
这里的事件在muduo
中包括socket可读写事件、定时器事件。在其他网络库中如libevent也包括了signal、用户自定义事件
负责事件循环的部分在muduo
命名为EventLoop
,其他库如netty、libevent也都有对应的组件。
负责监听事件是否触发的部分,在muduo中叫做Poller
。
muduo
提供了epoll
和poll
两种来实现,默认是epoll实现
。 通过环境变量MUDUO_USE_POLL来决定是否使用poll:
Poller* Poller::newDefaultPoller(EventLoop* loop)
{
// 通过此环境变量来决定使用poll还是epoll
if (::getenv("MUDUO_USE_POLL"))
{
return new PollPoller(loop);
}
else
{
return new EPollPoller(loop);
}
}
此外,图中的acceptor负责accept新连接,并将新连接分发到subReactor。这个组件在muduo中也叫做Acceptor
通过简单的例子介绍muduo的基本使用:
从最简单的echo server
入手:
void onMessage(const muduo::net::TcpConnectionPtr& conn,
muduo::net::Buffer* buf,
muduo::Timestamp time)
{
conn->send(buf);
}
int main()
{
muduo::net::EventLoop loop;
muduo::net::InetAddress listenAddr(2007);
TcpServer server(&loop, listenAddr);
server.setMessageCallback(onMessage);
server.start();
loop.loop();
}
echo-server
的代码量非常简洁。一个典型的muduo的TcpServer
工作流程如下:
EventLoop
TcpServer
TcpServer
的Callback
server
陈硕认为,TCP网络编程的本质是处理三个半事件
,即:
我们接下来分析下muduo是怎么处理和实现这三个半事件的
:
连接的建立
我们使用linux的API编写一个简单的tcp服务器时,建立一个新的连接通常需要四步:
步骤1. socket() // 调用socket函数建立监听socket
步骤2. bind() // 绑定地址和端口
步骤3. listen() // 开始监听端口
步骤4. accept() // 返回新建立连接的fd
接下来我们分析一下这四个步骤在muduo中是何时进行的:
首先在tcpserver
对象构建是,tcpserver
的属性acceptor
同时也被建立。在acceptor
的构造函数中分别调用了socket
函数和bind
函数完成了步骤一二。 即 当tcpserver server(&loop,listenAddr)
; 执行结束时,监听socket已经建立好,并已绑定到对应地址和端口了
而当执行server.start()
时,主要做了两个工作:
socket
上启动listen
函数,也就是步骤3;socket
的可读事件注册到EventLoop
中。EventLoop
尚未启动。 当调用loop.loop()时
,程序开始监听该socket的可读事件。callback
在EventLoop::loop()
中被调用。 该事件的callback实际上就是Acceptor::handleRead()
方法。在Acceptor::handleRead()
方法中,做了三件事:
fd
TcpConnection
对象TcpConnnection对象是个shared_ptr
,该对象会被保存在TcpServer的connections中。这样才能保证引用计数大于0,对象不被释放。
至此,一个新的连接已完全建立好,其可读事件也已注册到EventLoop中了。
消息的读取
上节讲到,在新连接建立的时候,会将新连接的socket
的可读事件注册到EventLoop
中。 假如客户端发送消息,导致已连接socket的可读事件触发,该事件对应的callback
同样也会在EventLoop::loop()
中被调用。
该事件的callback实际上就是TcpConnection::handleRead方法
。 在TcpConnection::handleRead
方法中,主要做了两件事:
inputbuffer
中messageCallback
,执行业务逻辑。ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0)
{
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}
messageCallback
是在建立新连接时,将TcpServer::messageCallback
方法bind
到了TcpConnection::messageCallback
的方法。
TcpServer::messageCallback
就是业务逻辑的主要实现函数。通常情况下,我们可以在里面实现消息的编解码、消息的分发等工作,这里就不再深入探讨了。
在我们上面给出的示例代码中,echo-server的messageCallback非常简单,就是直接将得到的数据,重新send回去。在实际的业务处理中,一般都会调用TcpConnection::send()方法,给客户端回复消息。
这里需要注意的是,在messageCallback中,用户会有可能会把任务抛给自定义的Worker线程池处理。 但是这个在Worker线程池中任务,切忌直接对Buffer的操作。因为Buffer并不是线程安全的。
我们需要记住一个准则:
所有对IO和buffer的读写,都应该在IO线程中完成。
一般情况下,先在交给Worker线程池之前,应该现在IO线程中把Buffer进行切分解包等动作。将解包后的消息交由线程池处理,避免多个线程操作同一个资源。
消息的发送
用户通过调用TcpConnection::send()
向客户端回复消息。由于muduo
中使用了OutputBuffer
,因此消息的发送过程比较复杂。
首先需要注意的是线程安全问题, 对于消息的读写必须都在EventLoop
的同一个线程(通常称为IO线程)中进行: 因此,TcpConnection::send
保证了线程安全性,它是这么做的:
void TcpConnection::send(const StringPiece& message)
{
if (state_ == kConnected)
{
if (loop_->isInLoopThread())
{
sendInLoop(message);
}
else
{
loop_->runInLoop(
boost::bind(&TcpConnection::sendInLoop,
this, // FIXME
message.as_string()));
}
}
}
检测send的时候,是否在当前IO线程,如果是的话,直接进行写相关操作sendInLoop
。 如果不在一个线程的话,需要将该任务抛给IO线程执行runInloop
, 以保证write动作是在IO线程中执行的。我们后面会讲解runInloop的具体实现。
在sendlnloop
中,做了以下几件事:
1、假如OutputBuffer为空,则直接向socket写数据
2、如果向socket写数据没有写完,则统计剩余的字节个数,并进行下一步。没有写完可能是因为此时socket的TCP缓冲区已满了。
3、如果此时OutputBuffer中的旧数据的个数和未写完字节个数之和大于highWaterMark,则将highWaterMarkCallback放入待执行队列中
4、将对应socket的可写事件注册到EventLoop中
注意,直到发送的时候,才把socket的可写事件注册到了EventLoop中。之前只注册了可读事件。
连接socket的可写事件对应的callback是TcpConnection::handleWrite()
当某个socket的可写事件触发时,TcpConnection::handleWrite
会做两个工作:
为什么要移除可写事件?
因为当OutputBuffer
中没数据时,我们不需要向socket中写入数据。但是此时socket一直是处于可写状态的, 这将会导致TcpConnection::handleWrite()
一直被触发。然而这个触发毫无意义,因为并没有什么可以写的。
所以muduo的处理方式是,当OutputBuffer还有数据时,socket可写事件是注册状态。当OutputBuffer为空时,则将socket的可写事件移除。
连接的中断
我们看下muduo对于连接的断开是怎么处理的。
连接的断开分为被动断开和主动断开。主动断开和被动断开的处理方式基本一致,因此本文只讲下被动断开的部分。
被动断开即远程端断开了连接,server端需要感知到这个断开的过程,然后进行的相关的处理。
其中感知远程断开这一步是在Tcp连接的可读事件处理函数handleRead中进行的:当对socket进行read操作时,返回值为0,则说明此时连接已断开。
接下来会做四件事情: