spserver代码阅读笔记
从这里可以下载doc/pdf版:spserver代码阅读.pdf
http://code.google.com/p/spserver/
spserver 是一个实现了半同步/半异步(Half-Sync/Half-Async)和领导者/追随者(Leader/Follower) 模式的服务器框架,能够简化 TCP server 的开发工作。
spserver 使用 c++ 实现,目前实现了以下功能:
- 封装了 TCP server 中接受连接的功能
- 使用非阻塞型I/O和事件驱动模型,由主线程负责处理所有 TCP 连接上的数据读取和发送,因此连接数不受线程数的限制
- 主线程读取到的数据放入队列,由一个线程池处理实际的业务
- 一个 http 服务器框架,即嵌入式 web 服务器(请参考: SPWebServer:一个基于 SPServer 的 web 服务器框架)
目录
一切从类SP_LFServer的构造函数开始。如果SP_LFServer是在主线程中实例化的,那就从主线程讲起。
以[M]代表主线程[MainThread],
[M]建立的线程池中的线程[ChildThread]为[C]
- [M]SP_IOUtils::tcpListen初始化监听socket
- [M]创建socket listenFD
- [M]bind listenFD到本地某端口
- [M]listen在listenFD上
- [M]初始化libevent事件基
- [M]把listenFD的读事件绑在事件基上,回调函数是SP_EventCallback::onAccept
- [M]建立线程池,每个线程都跑去执行SP_LFServer ::lfHandler [C]阻塞在这里
- [C]一旦有线连接进来,某个[C]在SP_LFServer ::handleOneEvent抢到全局锁,跑去执行一次event_base_loop,也就是执行了SP_EventCallback::onAccept把新连接接进门来。
- [C]SP_EventCallback::onAccept中,主要是把当前连接放入会话管理器,并为新连接创建SP_Session对象,然后setHandler及setIOChannel,注册连接的读写到SP_LFServer的事件基,并宣告”此连接已经可以读写,某个[C]来处理读写事件吧”。
- 再就是调用事务的初始化函数(由具体的Handler实现):doStart
- [C]或者是有数据要收发,那些阻塞在SP_LFServer :: lfHandler -> SP_LFServer :: handleOneEvent中event_base_loop的[C],会被激活并调用相应的读写回调,接收数据或者向连接写数据。
- [C]或者是有任务要处理,那些阻塞在SP_LFServer :: lfHandler -> SP_LFServer :: handleOneEvent中InputResultQueue->pop的[C],会被激活并取得这个任务。
- [C]一旦有线连接进来,某个[C]在SP_LFServer ::handleOneEvent抢到全局锁,跑去执行一次event_base_loop,也就是执行了SP_EventCallback::onAccept把新连接接进门来。
- [M]做完这些事情,主线程自己就睡觉去了(以后再也不干事了) [M]阻塞在这里
此种模式中,实际的数据收发,是由某个最早抢到全局锁mMutex的[C]来做的,它不仅把新连接通过SP_EventCallback::onAccept接进来,也同时在event_base_loop中把其它事件(如果有)也一同处理了,直到某个事件向任务队列InputResultQueue塞了任务(这任务通常是接收了足够多的数据或者其它事件导致的)。
一旦这个[C]接到任务,它就跑去干正经活去,数据收发的事情交由下一个苦力接手,这就要看线程池中哪个线程抢到了,应该是不确定的。干完正经活的线程回到线程池里,跟其它人抢mMutex。
反正几乎每个[C]都会经历:抢mMutext,处理libevent事件(收发数据当然在这里)或者接到任务。
以[M]代表主线程[MainThread],以[D]代表[M]创建的[DispatcherThread]线程
- [M]SP_IOUtils::tcpListen初始化监听socket
- [M]创建socket listenFD
- [M]bind listenFD到本地某端口
- [M]listen在listenFD上
- [M]SP_Dispatcher:: SP_Dispatcher 初始化
- [M]创建libevent事件基
- [M]创建阻塞队列,存放连接
- [M]创建新线程[DispatcherThread]
- [D]创建两个线程池
- [D]workerExecutor池中有较多线程,用于执行实际事务
- [D]actExecutor池中线程较小,用于处理收尾事件
- [D]进入event_base_loop (DispatcherThread阻塞在这里)
- [D]有新事件发生,workerExecutor执行,actExecutor收尾
- [M]在循环中accept ([M]阻塞在这里)
- 有新连接进来,激活了[M],空闲,推入阻塞连接队列
- SP_IOUtils::setNonblock设置连接为非阻塞型
- 触发onPush事件,激活了[D]
- [D]为连接设置事务处理对象setHandler
- [D]为连接初始化数据渠道(ssl/raw/ encrypted)setIOChannel
- [D]将连接之读写事件加入Dispatcher的事件基中进行监听
- SP_EventCallback::onRead处理读数据
- SP_EventCallback::onWrite处理写数据
- [D]开始调用事务处理的doStart或仅激发可读写可写消息
- 系统忙,超过连接数,往连接里写” System busy, try again later”,断开连接
- 有新连接进来,激活了[M],空闲,推入阻塞连接队列
所有进来的连接,都是先经[M]Accept的,因为Accept后直接抛入连接队列,所以Accept可以很快进行,不管当前有多少连接都会非常及时响应。
待[D]开始监听新连接的读写事件后,数据的收发都是由[D]自已负责,这是在2.e步骤行的。
一旦读写完(或者处理完其它信号或者事件),就去看看任务队列InputResultQueue中有无需要处理的事件(比如在读写阶段,读完一段数据如果符合条件,就会被组成一个任务扔到任务任务队列中)。当然,[D]不会事必躬亲,而是把任务取出来扔给workerExecutor线程池。
然后,[D]会去看任务执行结果队列OutputResultQueue,这里的元素是由workerExecutor中的某线程在执行事务的时候产生的,即执行完一个任务就把任务打包放到结果队列。[D]从结果队列取出来,扔给actExecutor线程池,由它去收尾。其实一般情况下只是做些释放内存之类的工作,或者统计一下成功执行了多少任务(请求)。
以[M]代表主线程[MainThread]
一切从类SP_Server::start讲起
- [M]SP_IOUtils::tcpListen初始化监听socket
- [M]创建socket listenFD
- [M]bind listenFD到本地某端口
- [M]listen在listenFD上
- [M]通过SP_EventArg建立libevent事件基
- [M]把listenFD的读事件绑在事件基上,回调函数是SP_EventCallback::onAccept
- [M]建立大线程池workerExecutor和小线程池actExecutor,这些池中的线程都是阻塞在判断队列中是否有元素的函数上
- [M]进入libevent事件处理循环:event_base_loop [M]阻塞在这里
- 直到有事件发生,比如新连接进来,[M]调用onAccept去迎接新连接,并将其读写事件注册到事件基上
- 处理完事件后,去任务队列中看看有无要处理的任务(InputResultQueue),有的话提出来扔给大线程池workerExecutor,由池中的线程去干实际的活
- 然后看看有无处理完的任务(OutputResultQueue),有的话提出来扔给小线程池actExecutor
这个模型里,所有数据读写及其它libevent事件处理都由实例化SP_Serverr 的线程来做的,如果是文件传送这样的应用,此线程会很忙。
我猜想,名字中的同步是否是指实际的事务处理是同步的,比如流程一直都是接收数据,处理数据(比如处理的时候可以很长时间),返回数据这么个固定的顺序;而实际数据的收发,在事务处理时不需要关心,只要认为数据已经发出去就好了,至于真正是在什么时候发给客户端的,事务处理函数管不着,这种不等待数据真正地外发做法是否就是异步呢?
每个连接开始都是可读又可写的,根据要实现的功能的不同,可以由Client/Server任一方先出招。所有的交流总是会有某方主动的,像http服务,总是会由客户端先行发个请求,然后服务器回应。
在代码编写的时候,一般只需要实现这么几个地方:
- SP_MsgDecoder子类的decode
- SP_Handler子类的handle/start/error/timeout
SP_MsgDecoder::decode在每次接收完数据的时候被调用,返回值代表了是不是已经收到一个完整的数据包了,如果是则必须返回eOK让接受数据的函数组个任务放到任务队列,让线程池中的某位[C]来调用SP_Handler::
handle。如果还只是不完整的数据包,那就返回eMoreData,告诉接收函数我还没吃够,继续喂我吧。
这其实会有个小问题,如果我们的逻辑数据包太大,内存会占用较多,效率不高。并且这样的处理方式在需要连接接收大量数据的地方不太实用,比如像接收客户端传送的大文件。有人会说,像传文件的时候,可以设置让decode一直返回eOK嘛,这确实是可行的,但是decode函数还是会被调用N次,这个调用还是挺耗资源的。每接收一小片数据,都会去调用一下decode+handle,还有个深层次的问题是这里的decode+handle都是父类虚函数的实现,效率就更低了。
每个东西都会有它适合的应用场合,像以上的模式更适应像http或者telnet这种交互性较多,但每次传递数据较少的应用。如果是做ftp文件传送之类,还是考虑其它模型吧。
不过我倒还真是用它来实现过一个文件服务器,文件内容传送是被分片打包进自定义的数据包的,和其它命令混合着传送到服务器,服务端一片片地将文件内容写入目标文件。由于文件碎片数据包中包含了足够的信息,所以这样断断续续地写也是可以传文件的。传输速度由于只是测试过单个客户端连接的情况,所以也就没什么参考意义。