怎么说呢,又看了一个模仿moduo的server,不过之前那个mini-muduo是echo服务器,这个是http服务器,思想都差不多,在并发模型上这个服务器使用的是Reactor +线程池 ,每个连接子线程也是一个Reactor,这也是我第一次看到这种模型的实现,它相对mini-muduo的优点还有它使用了智能指针来避免内存泄漏,这的确增大了编程的难度,但其保证内存不泄漏的确是程序员格外需要去注意的地方.
大致有这些类
Server(入口类,其中包含了Accptor 用来处理连接事件)
Channel (对于文件描述符的封装)
Epoll (对于epoll的封装,在其中添删Channel)
EventLoop(while循环的封装,其中有个Epoll在那收集事件)
EventLoopThread(EventLoop线程类,是子线程来使用EventLoop来监控连接套接字的)
EventLoopThreadPool(包含指定数量的EventLoopThread来分担所有连接套接字)
HTTPData(连接类,在其中读取数据,处理数据,分发数据)
Logging(异步日志)
流程:(符号不完全一样)
mainLoop()
myHttpServer()->eventLoopThreadPool_(),accpetChannel()->myHttpServer.start()->eventLoopThreadPool_.start()-> eventLoopThread(),eventLoopThread.startLoop()->thread.start()-> subEventLoop()->subEventLoop.loop()->mainloop.addToPoller(acceptChannel)
mainLoop.loop()
对应的说明:
- 初始化化主循环mainloop
- 初始化Server
- 其中先初始化eventLoopThreadPool,再初始化监听套接字并放入accpetChannel
- 接着启动Server.start
- 其中eventLoopThreadPool中初始化4个eventLoopThread
- 并开启线程调用它们注册的线程函数,也就是启动 一个线程独有的EventLoop subReactor
- 每个subEventLoop 调用loop也就是Epoll类调用的poll(epoll_wait)此时这些epoll上都没有客户套接字
- 此时子线程的工作完成,回到父线程中的Server::start,让mainLoop监听acceptChannel
- 再调用mainloop.loop来等待客户连接,此时万事具备,只欠东风.
流程:
mainLoop.loop返回->accpetChannel.handleEvents->myHttpServer.handNewConn->accept得到conn_fd->HttpData req_info(conn_fd)->Channel connChannel(conn_fd)->subloop.queueInLoop异步执行req_info.newEvent,subloop.addToPoller(connChannel,2000)
对应的说明:
- 此时来了客户连接,会在mainLoop.loop中返回,
- 通过回调handNewConn来accept,将得到的conn_fd绑定到HttpData和Channel上
- 通过EventLoopThreadPool来轮转获取subEventLoop,在subEventLoop中用queueInLoop来将任务函数HttpData::newEvent添加到到subEventLoop中
- 其中会将conn_Channel加入到subLoop监听的红黑树中
- 还会在conn_Channel绑定一个2秒的计时器,这个计时器由全局的timerManage管理,2s内客户没发送消息,过时会删去计时器(不过会在EventLoop中延迟执行),在计时器类的析构函数中让conn_Channel的智能指针的计数-1,起到"清除"超时客户的作用.
当客户消息来了
流程:
subLoop.loop返回-> EPOLLIN?->req_info.handleRead() EPOLLOUT?->req_info.handleWrite()
req_info.handleRead()->read,parseURI,parseHeaders,analysisRequest ,req_info.handleWrite()->write
req_info.handleConn ->查看并修改连接类的Epoll状态->subEventloop.updatePoller(connChennel)/subEventloop.runInLoop(HttpData::handleClose)异步执行req_info.handleClose->subLoop.removeFromPoller(connChennel)
对应的说明:
- 当客户消息来了,subLoop.loop返回,Channel会根据返回的事件来执行读写,handleRead中读取输入,并处理http请求, handleWrite中将需要回发的数据发出去,接着在handleConn中会查看并修改连接类的Epoll状态,根据现在的状态选择更新connChennel 的Epoll事件或者异步关闭连接.
一次的请求完毕
Log类的设计也比较巧妙,专门用一个线程来发送需要写入日志的信息,用户接口非常容易Log<<"xxx"
,详见源码
经验:
void EventLoop::queueInLoop(Functor&& cb) {
{
MutexLockGuard lock(mutex_);
pendingFunctors_.emplace_back(std::move(cb));
}
if (!isInLoopThread() || callingPendingFunctors_) wakeup();
}
- (纠正下我上一篇博客讲mini-muduo里面 queueInLoop的错误)``queueInLoop 异步执行任务,queueInLoop中的判断callingPendingFunctors_的作用:在EventLoop::loop中callingPendingFunctors_=true的区间取出所有任务并执行,在queueInLoop中先判断如果发送的任务所在线程不是EventLoop所创建的线程那么如果EventLoop的所在线程已经从pendingFunctors_以vector.swap的方式中取出任务(避免迭代器失效),这个时候 callingPendingFunctors_为真,则EventLoop.weakup可以让epoll的下一轮执行任务,如果此时queueInLoop调用的线程是EventLoop所创建的线程的话,那同样!isInLoopThread()为假,callingPendingFunctors_也为假,不调用wakeup异步唤醒,之后等到eventloop下一轮苏醒了或者说执行到doPendingFunctors了,就能保证此次添加的任务在任务vector中.
- runInLoop也是异步执行任务,queueInLoop功能更像是异步将任务添加到任务Vector
- 线程局部存储_thread的用法
- 通过linux新api eventfd实现唤醒epoll的功能
- 将面向过程代码通过包装成类改造成面向对象代码
- http中head中的每个
域名:域值
选项以map的key,value存储,方便解析 - 使用应用层的缓冲区,读取到的数据append到buffer的末尾,以string动态扩容的方式,解决了原本一个定长数组的不足之处.
- ,了解了mainReactor+subReactor+threadpool的模式的功能,(这个threadpool不是用来计算,而是给subReactor来loop)
- 利用函数getopt来方便解析命令行参数,这简直是写ls,cp等各种命令的福音
- 通过setReadHandler(bind(&Server::handNewConn, this))的方式设置回调函数,可以执行对象的函数,还可以保证setReadHandler的接口统一,需要注意的是传入对象的this指针会让绑定这个函数不安全(这在muduo前几章有所提及)
- ET模式下每次读取或者输出需要一直read或write直到EAGAIN
bug:服务器bind的80端口已被占用,需要绑定别的.在网页上有回车显示不出来的问题
缺点:未支持utf-8,POST的省略,计时器的设计不佳