muduo设计思想总结

https://blog.csdn.net/qq_38345430/article/details/80372895 

首先是设计的分析,有了设计才能根据设计编码:

1、单线程编程模型采用"non-blocking IO + IO multiplexing",即Reactor模型。基本结构是一个事件循环,以事件驱动和事件回调的方式实现业务逻辑。优点:可以用于读写socket,连接的建立,甚至DNS解析都可以用非阻塞方式进行,以提高并发度和吞吐量,利于IO密集的应用。缺点:事件回调函数是非阻塞的,对于涉及网络IO请求响应式协议容易分割业务逻辑。

2、多线程编程模型采用"one loop per thread + thread pool"。event loop,用于IO multiplexing ,配合non-blocking IO和定时器。thread pool用做计算。基本结构:TcpSever所在的EventLoop用来接收新的Tcp连接,同时在thread pool里挑选一个loop给新的Tcp连接使用。可参考多线程Reactor模型。

3、进程间的通信只用TCP。优点:可以跨主机,具有伸缩性;是双向通信的。一个分布式系统由运行在多台机器上的多个进程组成的,进程之间采用TCP长连接通信,提倡在功能划分后,实现同一类服务进程时可以借助多线程来提高性能。TCP长连接好处:容易定位分布式系统中的服务之间的依赖关系;通过接受和发送队列的长度也必将容易定位网络或程序故障。

4、单线程服务器场景:看门狗进程。优点:只有单线程程序能fork,多线程会产生很多麻烦。单线程程序能限制CPU的占用率。

5、多线程程序要求:1)多个CPU可使用。2)线程之间可共享数据并修改 。3)事件的响应有优先级。 4)划分责任与功能,每个线程逻辑单一。 5)性能可预测。 6)使用异步进行。

Buffer类

non-blocking的核心思想是避免阻塞在read()和write()或其他IO系统调用上,这样可以最大限度复用thread-of-control,让一个线程能服务于多个socket连接。IO 线程只能阻塞在 IO-multiplexing 函数上,如 select()/poll()/epoll_wait()。这样一来,应用层的缓冲是必须的,每个 TCP socket 都要有 stateful 的 input buffer 和 output buffer。

若不能一次性将读写的数据全部处理,将未读写的数据写入buffer中,在onMessage()中根据socket的状态将buffer中的数据处理。

由于是缓冲区,希望缓冲区越大越好,但是开辟太大的空间给每一个连接会造成内存浪费,于是结合栈上的临时空间解决这个问题。准备两块iovec,第一块指向buffer的writeable字节,第二块指向栈上的extrabuf,将多出的字节读到extrabuf里,再将其中的数据append到buffer中。同时避免反复调用read()的系统开销。

内部实现为vector,因此大小不是固定的,有自适应性,即大小被扩充后不会变回原来的大小。

EventLoop类

负责IO时间和定时器的分发。

One loop per thread原则:每个线程只能有一个EventLoop对象,其构造函数会记住本对象所属的线程,创建了EventLoop对象的线程是IO线程,主要功能是运行事件循环,事件循环必须在IO线程执行,生命周期和其所属的线程一样长。
用scoped_ptr来间接持有Poller,保证指针在析构的时候能够析构指向的Poller,从而正确回收资源并保证指针不能被转让所有权。

在loop()函数中通过调用poller_的poll()函数获取就绪事件并保存到activeChannels,然后遍历activeChannels并执行它的handleEvent()函数,即处理所代表的fd事件。

Channel类

每个Channel对象只属于一个EventLoop,即只属于一个IO线程。只负责一个文件描述符(fd)的IO时间分发,但不拥有这个fd。Channel把不同的IO事件分发为不同的回调,回调用boost::function表示。声明周期由拥有它的类负责。

总的来说,Channel就是对fd事件的封装,包括注册它的事件以及回调。EventLoop通过调用handleEvent()来执行Channel的读写事件。

Poller类

Poller是EventLoop的间接成员,供其所属的EventLoop在IO线程调用。生命周期与EventLoop相等。

封装了poll和Epoll两种系统调用。

调用poll()函数来更新pollfds,再调用fillActiveChannels()将有就绪事件的Channel加入到ActiveChannels供handleEvent()用。

TimerQueue类

定时器队列。

TimerQueue底层使用timerfd_*系列函数将定时器转换为fd添加到事件循环中,当时间到达后就会自动触发事件, 其内部使用 set 管理一些注册好的Timer,set的key为pair<Timestamp,Timer*>,这样能够处理到期时间相同的定时器,由于set有自动排序功能,所以注册到事件循环的总是第一个需要处理的Timer。

Acceptor类

用于accept(2)新TCP连接,并通过回调通知使用者。它是内部class,供TcpServer使用,生命期由后者控制。
调用acceptor.listen(),使内部数据成员acceptChannel_观察acceptSocket_上的readable事件,在socket可读时调用handleRead(),后者会接受并回调newConncectionCallback_。

TcpServer类

管理TcpConnection,供用户使用,生命期又用户控制。内部使用Acceptor获得新连接的fd,保存用户提供的ConnectionCallback和MessageCallback,在新建TcpConnection时会传递进去。持有目前存活的TcpConnection的shared_ptr,用map来管理连接。在新连接到达时,Acceptor会回调newConnection(),后者会创建TcpConnection对象con,加入到ConnectionMap,设置好回调,再调用conn->connectEstablished()。

TcpConnection类

表示一次TCP连接。
TcpConnection使用Channel来获得socket上的IO事件,会自己处理writeable事件,而把readable事件通过MessageCallback回调给客户。

总结


首先是有一个TcpServer,它管理一个Acceptor(监听socket套接字)和一个TcpConnectionMap(管理TCP连接),用一个端口号listenAddr和事件循环loop和该TcpServer绑定,并为它设置回调函数,然后server.start()就调用acceptor.listen()开启监听。内部使用Acceptor类来获取新连接,在新TcpConnection到达时Acceptor会回调newConnection(),后者会创建TcpConnection对象con,并加入到TcpServer的ConnectionMap中。从event loop pool 里挑选一个loop给TcpConnection用,同时绑定回调函数connectEstablished。server所处的线程只用来接受新连接,新连接用其他event loop来执行IO。每个event loop在loop()函数中通过调用poller_的poll()函数来更新pollfds,再调用fillActiveChannels()遍历pollfds,根据有活动事件的fd找到对应的channel,并将Channel保存到activeChannels,然后遍历activeChannels并执行它的handleEvent()函数,该函数通过revents的值判断具体执行哪个回调。
 

猜你喜欢

转载自blog.csdn.net/songchuwang1868/article/details/89448375