在互联网企业里,*nux下的C/C++编程主要的焦点还是server开发,关于不同的server模型,在UNP第30章里有过简单的讨论,这里得出的结论就是多线程和多进程的server模型效率较高。但书中缺乏对多路复用机制的讨论,而当前主流的server模型则是epoll+multi-threads/multi-processes,lighttpd就属于这种模型。本文将探讨一个更加高效的server模型,half-synchorize/half-asynchorize模式。
此模式最早是由著名的C++网络编程框架ACE的作者的一篇文章,其主要思想就是用异步的方式来处理IO事件,而用同步的方式来处理业务逻辑,同步和异步之间使用一个队列作为缓冲,如下图:
此模型详细介绍可参考原文。
引用作者对spserver的介绍:spserver 是一个实现了半同步/半异步(Half-Sync/Half-Async)和领导者/追随者(Leader/Follower) 模式的服务器框架,能够简化 TCP server 的开发工作。并且spserve使用了libevent作为底层的异步响应机制的实现。
我抱着一种学习的态度,阅读了其half-sync/half-async模式的实现代码,本文也可以看做是一篇spserver的源码分析。
1) spserver:主文件,程序从start方法里启动。
2) speventcb:主要的回调函数实现逻辑。
3) spsession:代表一个会话。
4) sprequest:封装了客户端ip和messageDecoder,个人认为封装的不好。
5) spresponse:封装了响应的内容。
6) spiochannel:封装底层IO。
7) spmsgdecoder:消息解析器,判断消息是否完整。
8) spbuffer:对libevent里的buffer的简单封装。
9) spthread、spthreadpool、spexecutor:线程池的封装。
spsession代表一个连接的会话,它属于half-sync/half-async模型的3层结构中的queue,业务逻辑和网络IO通过session中的input buffer和output list来通信。
先来看一下SP_Session类的成员:
SP_Sid_t mSid; //session id,详见下一节
struct event * mReadEvent; //和session关联的读写事件
struct event * mWriteEvent;
SP_Handler * mHandler; //用户实现业务逻辑的handler
void * mArg; //event args
SP_Buffer * mInBuffer; //输入缓冲队列
SP_Request * mRequest; //对请求的封装
int mOutOffset; //输出偏移量,以字节为单位,表明out list中已输出的字节数
SP_ArrayList * mOutList; // list of msg,输出的缓冲队列
char mStatus; //当前session的状态,可为eNormal, eWouldExit, eExit
char mRunning; //是否在运行
char mWriting; //是否在写
char mReading; //是否在读
unsigned int mTotalRead, mTotalWrite; //本session已读写的字节数
SP_IOChannel * mIOChannel; //关联的IOChannel
session是通过SP_SessionManager来进行管理的,它实际上是一个64*1024的二维数组,entry的类型定义如下:
typedef struct tagSP_SessionEntry {
uint16_t mSeq;
uint16_t mNext;
SP_Session * mSession;
} SP_SessionEntry;
其中每个entry都是通过一个key来标识其在matrix中的坐标,key=1024*row+col,而seq标识这个entry被使用过多少次,通过key和seq就可以形成一个ssion id了。entry中的mNext成员指向list中下一个成员的key,这样就可以快速的定位和分配list的tailor。
程序从SP_Server中的start方法开始,绑定server的地址和端口后,注册signal handler,然后注册listenfd的onAccept异步回调函数。这里值得一提的是libevent对所有的异步调用做了抽象,甚至包括signal handler。
程序的流程是通过libevent对不同的事件来回调不同的函数推进的,主要的回调函数有onAccept,onRead,onWrite和onResponse,下面将逐一讨论它们的流程。
下面结合流程图对代码分析:
onAccept中完成了连接的初始化,主要包括:
1) accept并为返回的clientfd注册读写事件。
2) 从session manager中为连接分配一个session。
3) 调用SP_EventHelper::doStart方法。
以上这些过程虽然都是通过事件触发的,但都是在主线程的main loop里的event_base_loop中调用的。而HS-HA模式主要是通过doStart方法来体现的。
doStart实际上是把一个task放到一个task queue里,而后task由一个线程完成。其主要逻辑在SP_EventHelper :: start中实现。
在start中的执行步骤如下:
1) 初始化IOChannel。
2) 分配response对象。
3) 调用业务逻辑实现SP_Handler中的start方法。SP_Handler中的start方法可以调用block函数,这也是为何此函数会由一个线程来调用。其实凡是用户实现的业务逻辑都是通过线程来异步调用的。
4) 调用msgqueue_push,将经过start中用户返回的response对象放入msgqueue中。msgqueue是一个带异步通知机制的队列,当response对象放入队列后,msgqueue_pop方法将被调用,它实际将response对象作为参数,调用onResponse方法。
onResponse函数并不是通过libevent的回调机制触发的,实际上它是SP_EventArg中的response queue(实际为event_msgqueue类型)的回调函数,当队列中有response对象时,对每个response对象均调用onResponse函数。onResponse函数的主要作用就是将response对象中包含的msg对象加入到session的outList中。这里值得一提的是,SP_Message和SP_Session式多对多的关系,这样可以节省重复的SP_Message占用的内存。
onRead函数在fd可读和超时的情况下被调用,其主要流程为:
1)判断触发事件是否为可读,若为超时,则调用SP_EventHelper::doTimeout,它将用户实现的timeout函数封装为task,task被push到eventArg中的InputResultQueue中,而后由executor来执行。
2)若为可读,则读入数据并解码,解码成功则调用SP_EventHelper::doWork,它将用户实现的handle函数封装为task,task被push到eventArg中的InputResultQueue中,而后由executor来执行。
onRead函数在fd可写和超时的情况下被调用,其主要流程为:
1)判断触发事件是否为可写,若为超时,则调用SP_EventHelper::doTimeout,它将用户实现的timeout函数封装为task,由线程调用。task被push到eventArg中的InputResultQueue中,而后由executor来执行。
2)若为可写,则将session中的outList中的msg发往client,值得注意的是,这里是通过writev来一次尽可能的多的写出数据。
由上可知,一般来说Server是通过session来管理不同的连接,且在session中保留输入缓冲和输出缓冲,而后通过异步事件机制来向网络中读写数据,这便形成了half-async端。而用户实现自身的业务逻辑,且这些业务逻辑中可调用阻塞式的函数,这便形成了half-sync端,而为了提高效率,在half-sync端可使用多线程机制。