很想了解服务器端的一些知识,但nginx这种代码让我直接来看估计是受不了的,而且是C。
muduo是陈硕编写的一款开源C++网络库,据说性能以及实现上都不错,而且有配套书《Linux多线程服务端编程:使用muduo C++网络库》,正好结合看一下源码,涨涨计算机网络和服务器端的姿势。
Reactor
Reactor是广泛应用于服务器中的一种设计模式,它是一种事件驱动模型,常见结构如下
其中Reactor只负责处理新的连接,并将它注册到一个<fd, EventHandler>的map中。它同时持有一个EventDemultiplexer,EventDemultiplexer通过epoll来监听fd,发现有读写后,就会调用对应的EventHandler来处理事件。
具体的逻辑可以看一下的例子,感觉会更清晰一点。
Logging Server接收连接
-
Logging Server注册LoggingAcceptor到InitiationDispatcher。
-
Logging Server调用InitiationDispatcher的handle_events()方法启动。
-
InitiationDispatcher内部调用select()方法(Synchronous Event Demultiplexer),阻塞等待Client连接。
-
Client连接到Logging Server。
-
InitiationDisptcher中的select()方法返回,并通知LoggingAcceptor有新的连接到来。
-
LoggingAcceptor调用accept方法accept这个新连接。
-
LoggingAcceptor创建新的LoggingHandler。
-
新的LoggingHandler注册到InitiationDispatcher中(同时也注册到Synchonous Event Demultiplexer中),等待Client发起写log请求。
Client向Logging Server写Log
-
Client发送log到Logging server。
-
InitiationDispatcher监测到相应的Handle中有事件发生,返回阻塞等待,根据返回的Handle找到LoggingHandler,并回调LoggingHandler中的handle_event()方法。
-
LoggingHandler中的handle_event()方法中读取Handle中的log信息。
-
将接收到的log写入到日志文件、数据库等设备中。 3.4步骤循环直到当前日志处理完成。
-
返回到InitiationDispatcher等待下一次日志写请求。
要注意到,一开始我们是需要把服务器的fd先注册进去的,因为我们也要监听连接的到来。而服务器的EventHandler里就是注册新连接的过程。
Muduo的基本思想
Muduo的一个重要的思想是one loop per thread,这里的loop其实可以看作是一个reactor(代码中是EventLoop)。这种方案的特点是有一个main Reactor负责accept新的连接(Acceptor),然后把连接挂在某个sub Reactor中。muduo采用的是固定大小的一个Reactor Pool,在服务器开启时初始化。
一些关键类的分析
EventLoop
EventLoop可以看作就是一个Reactor,根据one loop per thread的原则,EventLoop在构造时,会检查当前线程是否已经创建了其他的EventLoop对象。
__thread EventLoop* t_loopInThisThread = 0; //__thread关键字保证每个线程有一个t_loopInThisThread
EventLoop::EventLoop()
: looping_(false),
threadId_(CurrentThread::tid())
{
LOG_TRACE << "EventLoop created " << this << " in thread " << threadId_;
if (t_loopInThisThread)
{
LOG_FATAL << "Another EventLoop " << t_loopInThisThread
<< " exists in this thread " << threadId_;
}
else
{
t_loopInThisThread = this;
}
}
EventLoop的构造函数会检查t_loopInThisThread的值,如果已经被赋值了,那么说明该线程已经有一个EventLoop对象了,会报错,否则就把该值置为this指针。EventLoop还提供了判断当前线程是不是EventLoop所在线程的函数
bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }
EventLoop还有一个重要的函数,runInLoop,它可以让用户在线程间调配任务。
void EventLoop::runInLoop(Functor cb)
{
if (isInLoopThread())
{
cb(); //如果用户在当前线程调用,则直接执行
}
else
{
queueInLoop(std::move(cb)); //否则,将cb压入一个任务队列
}
}
void EventLoop::queueInLoop(Functor cb)
{
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(std::move(cb));
}
if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup(); //当queueInLoop的操作不在当前线程时,需要唤醒,如果正在执行pendingFunctor,那么也需要唤醒
}
}
这里唤醒线程使用了eventfd,这是Linux提供的一种新的线程间通信的方式
Channel
每一个Channel负责一个fd的IO事件分发,但是它不拥有这个fd,也不会在析构时关闭这个fd。每一个Channel始终只属于一个EventLoop,因此也就只属于一个线程。常用函数set*callback以及enableReading,前者用于设置回调函数,后者用于将该channel负责的fd加入到epoll监听的fd列表中。enableReading会调用update()函数,进而调用EventLoop的updateChannel(),然后会调用poller的updateChannel()。handleEvent()函数是Channel的核心,他会根据revents_(目前活动事件)的值来确定调用哪个回调函数。
Poller
poller是封装了poll或者epoll的类,并且维护了fd到channel的map。每当监听到有活动 fd时,会返回对应Channel的列表,然后在EventLoop中执行对应的HandleEvent
以上几个类是Reactor模式的关键组件,有了他们基本就可以构建出基本的模型了
Acceptor
Acceptor用于accept新的TCP连接,并通过回调来通知使用者。它是供TcpServer使用的,生命周期由TcpServer控制。Acceptor中包含有一个服务器端的socket,以及一个绑定在此socket上的channel,channel用于监听socket上是否有新的连接进入,并执行Acceptor的handleRead回调。在handleRead回调中,Acceptor调用accept来产生tcp连接的fd,并将其传给回调函数处理。
TcpServer
TcpServer用来管理建立的TcpConnection。TcpSever是供用户使用的,生命周期由用户控制。TcpServer持有目前存活的TcpConnection的shared_ptr。在新连接到达后,Acceptor会回调newConnection(),后者会创建TcpConnection的conn对象并将其加入到ConnectionMap,设置好callback,再调用conn->connectEstablished(),其中会回调用户提供的ConnectionCallback()
TcpConnection
TcpConnection是整个muduo最核心的类,它拥有TCP socket,在析构的时候回关闭连接。但是它没有发起连接的功能,它的构造函数传入的是已经建立好的Tcp连接的fd。