LoopJump's Blog

Cwinux源码解析7

2014-01-11

本文介绍Cwinux框架中,noticeXXX和onXXX的实现。

先对Framework做个简要介绍。

Cwinux是一个框架(Framework),不单纯是库(lib)。

框架和库的主要区别,在于:

  1.  框架封装了处理流程(all the control flow is already there),库通常只是提供了工具型的代码。从复用角度看,库是简答的代码复用,框架有控制流(处理流程)的复用。

  2.  框架、库和用户代码的关系不同。框架会调用用户的代码,而库的代码通常只被用户代码调用。

用形象一点的比喻的话,可以如下描述:当我们建造房屋时,库提供的是墙壁、门、窗等部件;框架则提供的是一个空架子,留下了很多空白的洞,用户去填充。

用比较常见的MFC和glibc为例,当我们要写MFC程序时,需要继承自MFC内置类,然后填充上自己的代码(如onDraw);glibc提供的则多是诸如sqrt(x)的工具类。

库和框架各适合有其应用的场景。通常来说,编写一个框架要比编写一个库难度大。

好,框架和库的概念介绍到这里,文末贴有几个链接,可供读者详细阅读。

下面具体到Cwinux框架,来看看框架本身和用户代码的关系,以及这种调用关系是如何实现的。

首先,介绍一下Cwinux的使用时的关键点。

  1.  开发者的应用需要继承CwxAppFramework。例如 CwxEchoApp:CwxAppFramework。

  2.  CwxAppFramework提供两类函数,开发者的应用按需重载其中的函数。其中的函数基本划分为两类:noticeXXXonXXX。noticeXXX是主动的动作(通知框架去做XXX),例如,noticeTcpConn通知框架去建立一个Tcp连接。onXXX 是被动动作,当应用收到XXX事件后,框架会调用应用的onXXX函数,例如,收到消息时,App的onRecvMsg会被调用。

  3.  通常需要创建一个处理消息的Handler。CmdOp的子类,注册给Commander以便分发。

  4.  通常需要建立线程池。将Commander注册给ThreadPool,线程执行时调用Commander的分发函数。

第3、4两个点正是此前几篇博文介绍的Cwinux消息处理框架。现在重点介绍1,2的内容。

Cwinux框架封装了网络编程应用的处理流程,这个流程可以被复用,并以CwxAppFramework的形式暴漏给用户(使用Cwinux的应用程序开发者)。

现在,我们深入Cwinux内部,看看onXXX和noticeXXX是如何实现的。

首先,看CwxAppFramework类的定义(只保留了用于本文解释的部分)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CWX_API CwxAppFramework
{
virtual int init(int argc, char** argv);
virtual int initRunEnv();

int noticeStdinListen();
int noticeTcpListen(...);
int noticeLsockListen(...);
int noticeTcpConnect(...);

virtual void onTime(...);
virtual void onSignal(int signum);
virtual int onConnCreated(...);
virtual int onConnCreated(...);
virtual int onConnClosed(CwxAppHandler4Msg & conn);
virtual int onRecvMsg(...);
virtual int onRecvMsg(...); //overload

CwxAppReactor* m_pReactor; ///<the reactor for event loop.
};

如前所述,CwxAppFramework中声明了很多用于主动执行型的noticeXXX和事件发生响应型的onXXX的函数(截取的代码中只是一部分)。此外,CwxAppFramework的成员变量中有CwxAppReactor*对象m_pReactor

在看代码之前,我们先自己思考一下,如何实现onXXX这样的响应函数。

首先,要写一个框架,必然得有一个CwxAppFramework类来将框架留出的空白的洞暴漏给用户,处理流程已经封装在框架中,用户填充上这些空白的洞后,一个应用程序就可以运行了。

接下来,我们以CwxAppFramework:: onRecvMsg函数为例,开始思考一个具体的onXXX的实现。onRecvMsg的语义是onRecvMsg函数在接收到连接上的数据时调用。这里,对用户来说,网络连接上数据到来是事件,onRecvMsg执行是对应的响应(被动响应)。我们知道,说到底,事件的来源是硬件中断。当然,应用层的Cwinux看不到硬件中断,它只能看到操作系统对中断的封装,具体到当前问题,这个封装表现出来的API就是epoll。

继续思考,我们想想这个事件是如何从硬件到Cwinux的onRecvMsg的。首先网卡硬件中断,该事件被操作系统路由到网络驱动中处理,再经过TCP/IP协议层处理,最后反映到epoll相关函数(epoll_create / epoll_ctl / epoll_wait)在内核中建立的数据结构。这时,某个已经执行epoll_wait函数的线程从epoll_wait的等待过程中返回,并且epoll_wait还告诉我们fd=N的网络连接有数据到来,我们从该连接读取数据。这时,我们需要通知CwxAppFramework调用onRecvMsg函数。

因此,在epoll_wait返回后,框架要能够找到CwxAppFramework对象。我们猜测,一种可能的方法就是把CwxAppFramework对象(指针)通过某种方式传递到响应epoll_wait的对象中。这样,该对象在响应处理epoll_wait反馈的新发生的事件时,调用CwxAppFramework对象指针的onRecvMsg,这样达到通知用户的App应该该调用onRecvMsg。

下面我们来看看Cwinux中到底是如何实现onXXX的。

由前面的分析,再回顾一下前面所描述的Reactor模式相关代码,我们知道,响应epoll_wait的对象是CwxAppHandler4Base。查看其代码,没有发现CwxAppFramework的踪影!好吧,再看看CwxAppHandler4Base的具体子类,例如CwxAppHandler4Msg,找到了CwxAppFramework*****。CwxAppHandler4Msg的代码大意如下(为方便阅读,做了修改,只保留了核心内容):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class CWX_API CwxAppHandler4Msg:public CwxAppHandler4Base
{
///构造函数
CwxAppHandler4Msg(CwxAppFramework* pApp, CwxAppReactor *reactor);

virtual int handle_event(int event, CWX_HANDLE handle=CWX_INVALID_HANDLE);

CwxAppFramework* getApp()
{
return m_pApp;
}

CwxAppFramework* m_pApp;
};

//////////////////////////////////////////////////////////////////////

CwxAppHandler4Msg::CwxAppHandler4Msg(CwxAppFramework* pApp, CwxAppReactor *reactor) : CwxAppHandler4Base(reactor)
{
m_pApp = pApp;
}

int CwxAppHandler4Msg::handle_event(int event, CWX_HANDLE)
{
if (CwxAppConnInfo::ESTABLISHED == m_conn.getState())
{///通信状态
if (CWX_CHECK_ATTR(event, CwxAppHandler4Base::WRITE_MASK))
{
if (0 != handle_output()) return -1;
}
if (CWX_CHECK_ATTR(event, CwxAppHandler4Base::READ_MASK))
{
if (0 != handle_input()) return -1;
}
}
}

int CwxAppHandler4Msg::handle_input ()
{
if (this->m_conn.isRawData())
{//recv raw data
result = this->getApp()->onRecvMsg(*this, bSuspend);
return result>=0?0:-1;
}

//not recv raw data
while( msg_not_complete )
{
recv_size = CwxSocket::recv(getHandle(), this->m_szHeadBuf + this->m_uiRecvHeadLen, need_size);
}

// OK, now it is a correct msg object
result = this->getApp()->recvMessage(m_header, this->m_recvMsgData, *this, bSuspend);
return result>=0?0:-1;
}

这段代码清晰表明了事件响应过程中是如何通知用户应用程序的。代码很清晰,不再赘述。另外,需要填个坑。在Cwinux简介那篇博文中,最后部分说过:建立Tcp监听时有两种模式可选,对应的响应函数不同(一个由框架接收数据生成消息,另一个只是由框架通知有数据到来)。这里正可从代码看出,两个处理方式的不同。建立Tcp监听时,指定isRawData的值,可以决定框架的不同动作:raw情况下,框架只通知有数据到来,getApp()->onRecvMsg(*this, bSuspend);not raw情况下,框架接收数据生成消息,通知有消息到来, this->getApp()->recvMessage(m_header, this->m_recvMsgData, *this, bSuspend)。

至此,我们已经解析了Cwinux实现onXXX这类通知用户应用程序有事件到来的具体过程。

下面介绍noticeXXX,其实现过程如图示。

notice.png

CwxAppNoticePipe是一个列表,里面串了CwxAppNotice对象。CwxAppNotice对象用来传递通知命令。AppFramework调用noticeTcpConnect函数,该函数会创建一个Handler4TcpConn对象handle,handle中填充该notice事件的相关参数,并调用m_pReactor串入noticePipe中。AppFramework在启动后进入事件循环run。在hook函数中,AppFramework检查是否有notice事件,如果有,则调用Reactor::noticed处理。

这里有一点需要注意:由于AppFramework在执行事件循环中会因epoll_wait等待(当前没有网络事件等),当这是我们执行noticeXXX时,需要唤醒AppFramework的事件循环,Cwinux的做法是预先创建一个管道(两个fd:m_pipeReader和m_pipeWriter),读取端m_pipeReader加入epoll_wait中监听,写入端m_pipeWriter留着,执行noticeXXX时,向写入端m_pipeWriter写入一个字节,则读取端m_pipeReader发生可读事件,epoll_wait旋即返回,进而有机会执行hook和noticeEvent函数来处理notice事件。

Referrence:

Framework vs Library:

http://www.programcreek.com/2011/09/what-is-the-difference-between-a-java-library-and-a-framework/

http://book.51cto.com/art/200801/65025.htm

http://blog.csdn.net/rflyee/article/details/8631753

扫描二维码,分享此文章