1.对Reactor模式的理解
个人理解Reactor(反应堆)模型,就是用一个线程来监听不止一个文件描述符,每当一个文件描述符产生被关注的事件时,就会分发给其他线程或者自己来解决这个事件。其核心我觉得就是使用epoll实现多路复用,也就是一个线程可以实现多个文件描述符的监听。
2.muduo的模式总结
muduo的编程模型称之为one loop per thread,我觉得也可以称之为Multiple Reactors,其解释就是每一个IO线程都有一个Eventloop类,其实每一个Eventloop类就可以称为一个Reactor,可以监听多个文件描述符。以一个比较复杂和完整的muduo运行框架图来展示一下:
这是一个由muduo组成的最复杂的模型结构,首先有两个Reactor,一个主Reactor,一个子Reactor,
主Reactor对应muduo源码就是TCPServer,是负责监听socket套接字的,Acceptor类就是在这个IO线程中执行的,也就是负责与客户端建立连接
子Reactor则是负责与连接套接字通信的,每个子Reactor绑定一个TCPconnection类,每次与一个客户端建立连接以后,可以获得一个连接套接字,这个套接字就会挂在子Reactor的epoll队列中,然后由TCPconnection类负责和这个连接套接字进行read和send。并且可以设置不止一个子Reactor,每个Reactor上都绑定一个TCPconnection类,因为IO操作比较费时间,所以当多个客户端连接时,可以使用多个子Reactor来负责,这就是EventLoopThreadPool,子线程的线程池。
然后还有一个ThreadPool,主要是负责计算任务的,当子Reactor读取客户端发来的数据以后,如果计算量特别大,并且也不涉及IO操作,那么用子Reactor来计算很浪费资源,所以就开设一个线程池,来专门进行计算,并将计算的结果发给子Reactor,然后再发送出去。
注意:整个结构,并不是所有部分都需要的,其实可以根据具体的情况,有所取舍。比如并发性不高,IO要求不高,可以只用主Reactor既负责监听套接字,也负责连接套接字的处理,可以把子Reactor舍去。比如计算量不大,可以不用线程池,计算任务在Reactor线程中进行。最简单的模式就是只有一个主Reactor,具体代码可以参考echo程序。
在最复杂的模式下,threadPool类绑定在主Reactor的线程下,也就是主Reactor负责开启线程池和管理线程池,所有的子Reactor读到的数据再从线程池中拿一条线程来计算。
3.muduo中源码的头文件总结
base
LogFile.h
主要是与日志文件的IO函数,其中有两个类,一个File类,主要是用来建立用户对单个文件的进程缓冲区,以及打开,追加等文件IO函数。另一个LogFile类封装了File类,主要实现滚动日志文件的功能(也就是当一个日志文件过大,或者写入时间过长时,就关闭这个日志文件,重新打开一个新文件)。
AsyncLogging.h
是实现异步日志的头文件,用户需要实现一步日志主要依靠这个头文件。主要就两个函数threadFunc和append,append函数使用是前端工作线程,如果产生了日志,就会把日志存到双缓存之中。threadFunc则是由专门负责写入日志的线程操作LogFile类来写入到对应的IO日志文件中去。
注意:前端线程写入的缓存区是使用的LogStream.h中的FixedBuffer缓存实现的,而后端写入到文件之间也有一个缓存,是靠简单的char数组,依靠setbuffer函数实现的。
LogStream.h
其中主要有FixedBuffer类,是一个缓冲区的封装,LogStream,是缓冲区流的封装,也就是封装了缓冲区,并且重载了一系列的<<符号。以及类Fmt,是用来处理小数的
Logging.h
其中有SourceFile类,是用来根据路径获得文件名,主要是用来由linux中的__FILE__宏获得执行程序所在的文件名,Impl类则是封装了LogStream类,可以将对应内容用符号<<输出到缓冲区中。最后就是Logger类,其实就是组织日志的内容,先存入到缓存中,然后再从缓存中输出到对应的设备或者文件中。其中默认的设备是标准输出。
Atomic.h
实现一个原子操作的计数器类
BlockingQueue.h
是一个线程安全的无界阻塞队列,就是队列的长度没有限制,并且在队列为空时,会进行条件阻塞
BoundedBlockingQueue.h
是一个线程安全的有界阻塞队列,和BlockingQueue.h类似
Condition.h
封装了条件变量的一系列函数
copyable.h
可复制类,就是一个空类
CountDownLatch.h
是一个封装了互斥锁和条件变量以及一个计数器的类,当计数器为0时可以唤醒所有线程,当计数器不为0时,会条件阻塞等待。
CurrentThread.h
获取了当前进程和线程的一些信息
Exception.h
出现异常的显示类
FileUtil.h
对文件内容的读写
Mutex.h
对互斥锁的封装
ProcessInfo.h
从linux的/proc目录下获取进程相关信息
Singleton.h
单例模式,其中成员变量中只有一个静态指针是有用的
StringPiece.h
封装了字符串,用以实现高效的字符串传递,原理就是这个类的私有变量就是char指针和一个字符串长度,所以在传递这个类的字符串时,只需要传递字符串的指针和长度即可,不需要整个字符串都拷贝过去
Thread.h
对创建子线程的函数封装,以及获得一些线程信息的函数
ThreadLocal.h
线程本地类,保证存在这个类中的私有变量,是每个线程单独拥有的,而不会互相干扰。
ThreadLocalSingleton.h
既是线程本地类,又是单例模式。也就是这个类只能有一个类变量,并且这个类变量可以在每个线程中单独修改,而不互相影响
ThreadPool.h
最普通的线程池,我觉得其中在signal前应该加一个判断更加稳妥一些,就是判断是否有空闲线程在池中等待
Timestamp.h
时间戳类,私有变量是一个从1970年1月1日到当前的总微秒数,可以通过静态成员变量now来获得当前时间
types.h
就是一些强制类型转换的封装
net
Acceptor.h
在Acceptor类中会创建一个Channel来处理监听套接字的可读事件,并且也封装了套接字的创建,绑定,监听等函数
Buffer.h
是一个缓冲区类,主要用于与客户端的连接套接字通信时,先从连接套接字将数据读到缓冲区中,或者将缓冲区中数据写入套接字,在TCpConnection中使用
Callbacks.h
主要是对函数指针的定义
Channel.h
Channel类是最小的事件处理单元,每个Channel会绑定一个套接字和一个Eventloop,如果这个套接字发生的事件被监听到,就会调用这个Channel中的回调函数去处理事件,另外监听这个套接字,以及处理这个套接字的线程就是绑定的Eventloop的IO线程。
Connector.h
Connector封装了客户端的Connect函数,在连接成功后会监听连接描述符,并调用回调函数处理事件
Endian.h
主要封装了linux特有的字节序转换函数
EventLoop.h
一个Eventloop变量就代表一个Reactor,是基于Reactor的核心类,主要作用就是维护一个epoll队列,并阻塞监听事件的发生,还封装了几个定时器的函数。
EventLoopThread.h
封装了一个可以创建IO线程的线程类,主要用于Eventloop线程池
EventLoopThreadPool.h
封装了IO线程的线程池
InetAddress.h
这个类主要用来初始化sockaddr_in类结构体,只需要输入端口号和IP,就可以自动生成sockaddr_in结构体
Poller.h
是EPollPoller的父类, 其中函数都是纯虚函数,需要用子类中的函数来实现
EPollPoller.h
封装了epoll的函数,并且里面的文件描述符用channel类来包装了
DefaultPoller.cc
定义了创建poller类的函数
Socket.h
用socket类封装了一系列操作socket的函数,包括bind,listen,accept等,在Acceptor类中使用到
SocketsOps.h
封装了一系列操作socket的全局函数,在命名空间sockets中
TcpClient.h
用户可以直接继承或者定义这个类来生成TCP客户端,里面主要包含Connector类用来连接,TcpConnection用来从套接字中收发数据。
TcpConnection.h
主要用来监听连接套接字,与套接字通信,是TCP的核心类,其中用Buffer缓存来实现进程内的缓存。每个连接上的客户端都有一个单独的TcpConnection与之对应。
TcpServer.h
用户可以直接继承或者定义这个类来生成服务端程序,其中主要包含了Acceptor类用来监听端口,和每一个客户端建立一次连接,就会创建一个TcpConnection变量来负责和这个客户端通信。并且这些TcpConnection是依靠shared_ptr指针来管理和自动销毁的。
Timer.h
是一个定时器类,但是并没有真正计时的功能,只是用来设定超时时间。
TimerId.h
是一个用RAII思想封装的Timer类,就是在构造函数中赋值,在析构函数中销毁值。
TimerQueue.h
定时器管理类,其中timer类就是TimerQueue需要管理的元素,而timerId就是一个简单的timer封装,避免销毁和创建操作但是要注意的是timer并没有自己计时的功能,所以需要依靠timerfd这个系统函数统一计时timerfd是一个系统计时函数,当所设置的时间到了,会通过timerfd这个文件描述符进行提示通信,而其他计时函数可能是通过信号量,或者其他方式,但是都没有文件描述符好,并且也可以用timerfd监听,具体原因可以查看一下博客的网络库定时器实现如何使用timerfd来为所有的计时器计时:timerfd每次都设置在计时器列表中到期时间最近的那个到期时间,这样timerfd到期以后,也就是最近的那个计时器到期所以每次都是手动重置timerfd的计时时间,为最近的计时器到期时间
4.C++源码阅读经验
在看一个C++类时,最主要的是看成员变量,而不是看成员函数,成员函数都是围绕成员变量实现的,所以先搞清楚成员变量代表的含义。