封装fd的对应事件变化情况,和关注事件
fd 、events、 revents、 callbacks,
两种channel: listenfd-acceptorChannel, connfd-connectionChannel
std::unordered_map
负责 Channel 和Poller通信
std::vector
std::unique_ptr
int wakeupFd_;
// 当mainloop获取一个新用户的channel,通过轮询算法选择一个subloop,通过该fd唤醒对应subloop来执行Channel回调
std::unique_ptr
c++11线程封装 以及 one loop per thread
的封装
getNextLoop()
以RR
方式获取下一个 subLoop
特殊的TcpConnection
? 封装listenfd
相关操作如bind
和 listen
, 在baseLoop
中进行检测。
参考Netty的实现, prendable
, readerindex
, writerindex
封装 成功建立连接的connfd
,一个connfd
唯一对应一个TcpConnection
,关键成员有:Socket
对象、Channe
l对象、发送和接受缓冲区;
TcpConnection
对Channel
各种事件回调的设置
关键成员有:Acceptor
、 EventLoopThreadPool
、 std::unordered_map
C++11、 Socket、 Reactor模型、多线程、epoll
在对源码剖析的同时也针对Muduo
网络库进行基于C++11
的重新实现,保留关键部分,使其不用依赖boost库就可以实现主要功能。
此项目是一个基于Muduo库和C++11的高性能网络库,能够让用户实现稳定、可靠、高性能、高并发的服务器应用程序
eventfd()
来实现 mainLoop
对 subLoop
的线程间通知操作,相比于使用任务队列需要加锁降低了开销我自己对Muduo库网络部分核心做了一个总结
Muduo库采用one loop per thread
+ nonblocking IO
网络模型,在mainLoop中 关注listenfd的读事件,并且将该listenfd封装成一个特殊的 TcpConnection
类即 Acceptor
,因为其读事件的处理是调用 accept
建立连接,并且将连接发送到一个subLoop上,选择subLoop的方法采用轮询的方法。
EventLoop模块
首先是最重要的Reactor
模块,在Muduo库中也就是EventLoop
类的,其中主要的功能负责调度和分发网络事件即调用 epoll_wait
监视fd 感兴趣的事件,并执行对应的回调函数,在Muduo中 将这一个逻辑流程进行面向对象的拆解,将epoll_wait
封装到 EPollPoller
中,将fd 以及其感兴趣的事件和对应回调封装到Channel中。EventLoop 负责 Channel 和 Poller的通信,EventLoop::loop()
调用 Poller
进行检测事件,当有事件到来,再通过EventLoop
调用 Channel
中由TcpConnection
和 用户 设置的相应事件回调。
eventfd()
注意到这里每一个Loop 与 其构造时的线程一一对应,在subLoop初始化时,会自动创建一个 eventfd()
来供mainLoop唤醒它本身,这样就不需要使用生产者消费者队列来管理任务,从而避免subLoop争抢任务而要将队列加锁带来的开销。
缓冲区Buffer
非阻塞网络编程中应用层buffer
是必须的:非阻塞IO的核心思想是避免阻塞在read()或write()或其他I/O系统调用上,这样可以最大限度复用thread-of-control,让一个线程能服务于多个socket连接。I/O线程只能阻塞在IO-multiplexing
函数上,如select()/poll()/epoll_wait()
。这样一来,应用层的缓冲是必须的,每个TCP socket都要有inputBuffer
和outputBuffer
。TcpConnection
必须有output buffer
:使程序在write()
操作上不会产生阻塞,当write()操作后,操作系统一次性没有发送完时,网络库把剩余数据则放入outputBuffer
中,然后注册POLLOUT
事件,一旦socket变得可写,则立刻调用write()进行写入数据。即将应用层buffer数据拷贝到操作系统buffer。
TcpConnection
必须有input buffer
:当发送方send数据后,接收方收到数据不一定是整个的数据,网络库在处理socket可读事件的时候,必须一次性把socket里的数据读完(加一个栈空间的数组用readv分散读),否则会反复触发POLLIN事件,造成busy-loop。所以muduo库为了应对数据不完整的情况,收到的数据先放到inputBuffer里。——操作系统buffer到应用层buffer。
muduo库 connfd 采用触发模式是LT,这样优点是
不会丢失数据或者消息
应用没有读取完数据,内核是会不断上报的
低延迟处理
每次读数据只需要一次系统调用;照顾了多个连接的公平性,不会因为某个连接上的数据量过大而影响其他连接处理消息
跨平台处理
像select一样可以跨平台使用
Muduo几大网络组件的抽象如下
项目多处使用C++11特性如原子变量实现无锁编程,以及智能指针对资源进行安全管理。
能更加熟练地运用C++11新特性,如bind、智能指针和原子变量等语法,同时我对于one loop per thread + nonblocking IO网络模型的理解得到了提升。此外,在编码中对类的封装和设计的实践,让我对设计模式、基于事件驱动的编程方法以及事件回调有了更具体的认识。
服务器没有启动监听Acceptor::listen
,排查后发现
TcpServer::start()
中 执行该函数的判断变量没有初始化,导致判断条件不成立,无法执行Acceptor::listen
即控制TcpServer::start()只执行一次的**原子变量started_**未初始化,数值为65538
调用aceept
处出错,错误码为 errno = 22
通过 perror
查询发现是invalid argument
,查阅资料后发现具体是因为
accept函数参数不合法
对返回的connfd
没设置非阻塞,因为本项目是Reactor模型 one loop per thread
+ non-blocking IO
为了方便起见,不想再多写fcntl,于是采用accept4
替代 accept
sockaddr_in addr;
socklen_t len = sizeof addr;
bzero(&addr, sizeof addr);
// sockfd是listen用
int connfd = ::accept4(sockfd_, (sockaddr*)&addr, &len, SOCK_CLOEXEC | SOCK_NONBLOCK);
ps -ef | grep testserver
查找程序对应的进程号
netstat -anpt
查看网络状态,如下表明./testserver
在本机8888端口处于监听状态