Doubango代码学习(二):well-defined objects Doubango代码学习(三):fsm Doubango代码学习(四):ragel state和message parser Doubango代码学习(五):runnable Doubango代码学习(六):timer

一,

学习原因:

需要储备点自己的知识了。由于目前笔者所做的是IMS,还是学习点开源的比较好。

查了一下开源的方案。里面相对Doubango靠谱的多,但它是一个IMS的开源方案,距离RCS5.1还有一段路要走。

因此就有了学习并开发doubango到RCS5.1想法。

选取doubango的原因:

网上介绍Doubango的文章很多,这里就不多说了,一个字就是“靠谱”。


Doubango代码的下载和编译:

需要翻墙下载代码,编译也需要做点修改。我是做android开发的,编译时也费了点功夫,脚本环境啥的都需要配置。有一点事肯定的,在windows下你按照说明肯定是编不出来的。遇到错误百度或者检查下脚本就行了,总之是能折腾出来的。


Doubango在android中的实现:

Doubango是跨平台的,所有主流系统都有实现。在android中的实现是IMSDroid,他使用了android-ngn-stack作为协议栈。android-ngn-stack是包装了doubango的适配层(语言贫乏,找不到合适的词)。

tinyWRAPJNI.java是JNI的接口,里面列出了所有Doubango的对外接口。在org.doubango.tinyWRAP这个包里,还有很多结构体,在Doubango中都有对应。


Doubango的目录:

Doubango最新的版本是2.0,以前的1.0已经弃用。建议只看2.0版本。

在目录下可以看到如下目录:

bindings

documentation

tinyDAV

tinyDEMO

tinyHTTP

tinyIPSec

tinyMEDIA

tinySAK

tinySIP

...

其中binding是用来binding到不同平台上用的。里面_common是通用的部分,csharp/java/objc/perl/etc则是对应不同平台的绑定。

documentation放的是一些文档,虽然简单,但还是很有用,建议先看看这些文档。

tiny***是各个功能模块,根据名字能大概判断出来干啥的,其中tinySAK中SAK是Swiss Arm Knigf,也就是一个工具箱。这个很重要,建议看完doc再把这个目录下的各个文件看一遍.

二,

 

Doubango代码学习(二):well-defined objects


虽刚开始看Doubango源码时还是让我郁闷不已,不过还好,渐渐适应了。刚开始最让我头疼的就是well-defined objects

由于Doubango是用纯C开发的,没有面向对象功能,就加上了这么一个东西。当然这套机制还是很不错的,毕竟纯C搞大型项目还是需要有些技巧的。

well-defined objects是用来模拟面向对象功能的,其中主要就是内存和指针的管理的,毕竟现实的malloc和free太容易出问题了。

源代码在tsk_object.c中:

先看一个结构体

typedef struct tsk_object_def_s
{
    //! The size of the object.
    tsk_size_t size;
    //! Pointer to the constructor.
    tsk_object_t*    (* constructor) (tsk_object_t *, va_list *);
    //! Pointer to the destructor.
    tsk_object_t*    (* destructor) (tsk_object_t *);
    //! Pointer to the comparator.
    int        (* comparator) (const tsk_object_t *, const tsk_object_t *);
}
tsk_object_def_t;

这个定义了一个类对象的结构体,其中size是对象大小,constructor是对象构造函数,destructor是对象销毁函数,comparator是对象比较函数。

同java一样,对于每个类(object)都有一个实际的".class"对象存在,这里也是一样。tsk_object_def_t相当于".class"的结构体。

如对于tsip_stack_t类,存在一个类对象tsip_stack_def_t

static const tsk_object_def_t tsip_stack_def_s = 
{
    sizeof(tsip_stack_t),
    tsip_stack_ctor, 
    tsip_stack_dtor,
    tsk_null, 
};
const tsk_object_def_t *tsip_stack_def_t = &tsip_stack_def_s;

构造stack对象时,需要用到类对象,方法如下:

stack = tsk_object_new(tsip_stack_def_t).

tsk_obj_new函数就不贴了,大致就是分配一块size大小的内存,然后调用构造函数初始化,再把引用置成1.

当用完一个对象时,避免使用tsk_object_delete来释放,应当使用tsk_object_unref。把引用值减一时,会检查引用是否为0,如果为0就自动释放掉内存。

当别人传进来一个对象时,如果你要异步地使用它,最好是tsk_object_ref一下,把引用值+1。当使用完以后,再unref一下。

还有就是模拟继承的方法,这个最好能不用就不用,因为我感觉模拟的不好,会带来很多弊端。

总结一下:

当你需要一个新类时,你要先生成一个类对象,如下:

static const tsk_object_def_t tsip_XXX_def_s = 
{
    sizeof(tsip_XXX_t), //大小
    tsip_XXX_ctor,  //构造函数
    tsip_XXX_dtor, //销毁函数
    tsk_XXX_cmp, //比较函数 
};

然后定义你的类tsip_XXX_t

typedef struct tsip_XXX_s
{
    TSK_DECLARE_OBJECT;//这个必须带,而且必须放到第一位。
    ... //定义你自己的成员.
}
tsip_XXX_t;

这样你的类就定义完成了,当想生成一个对象时,要调用tsk_object_new来生成。

当想销毁一个对象时要调用tsk_object_unref.

因为没有自动处理引用,当你需要保存一个对象是,调用tsk_object_ref来增加引用。一旦你增加了引用,必须在对应的unref处理,来保证不会有内存泄露。

当你比较两个对象是否相等时,调用tsk_object_cmp来比较.

 

Doubango代码学习(三):fsm


状态机是事务处理中必不可少,在tsk_fsm.h/c里就实现了状态机机制,而且在各个协议层都会用到大量的状态机。只有学好了状态机,才能真正了解Doubango的运行机制,才能对Doubango修改和定制。

要定义一个状态机,首先需要有状态(state),还有事件(action),还有对事件的处理(exec)。下面是状态机的结构:

typedef struct tsk_fsm_s
{
    TSK_DECLARE_OBJECT;

    unsigned debug:1;
    tsk_fsm_state_id current;
    tsk_fsm_state_id term;
    tsk_fsm_entries_L_t* entries;

    tsk_fsm_onterminated_f callback_term;
    const void* callback_data;

    TSK_DECLARE_SAFEOBJ;
}
tsk_fsm_t;

TSK_DECLARE_OBJECT学了前一批就知道是啥了,略过。current和term分别是起始状态和结束状态;callback_term是状态机结束时调用的清理函数;callback_data是状态机构造时保存的数据,待到状态机结束时用的;entries是个list,里面对应的是每个状态对每个事件的处理,结构体如下:

typedef struct tsk_fsm_entry_s
{
    TSK_DECLARE_OBJECT;

    tsk_fsm_state_id from;
    tsk_fsm_action_id action;
    tsk_fsm_cond cond;
    tsk_fsm_state_id to;
    tsk_fsm_exec exec;
    const char* desc;
}
tsk_fsm_entry_t;

其中from是当前状态,action是输入的动作,cond是个返回boolean的函数,如果调用cond返回false,则说明当前action无效,状态机不做任何变化;如果是true则把状态切换到to,然后执行exec。可以阅读以下tsk_fsm_act函数,里面很简单就不贴上了。

总结一下FSM的用法:

1,调用tsk_fsm_create创建一个状态机,两个参数分别是起始状态(current)和结束状态(term)。

2,调用tsk_fsm_set_callback_terminated来设置callback_term和callback_data.

3,调用tsk_fsm_set来初始化fsm。这一步设置fsm_entrys,查看一下TSK_FSM_ADD宏还有调用的地方,看看如何使用的。

4, 至此状态机初始化完成,之后调用tsk_fsm_act来为fsm输入事件和数据。

5,其他还有一些辅助函数和宏,检查一下tsk_fsm.h吧。

Doubango代码学习(四):ragel state和message parser


使用SIP不可避免遇到SIP的解析。 Doubango使用了Ragel来解析的,效率当然是高,但是代码实在是晦涩难懂。我曾经看了tsip_message_parser_execute一整天的时间,单步跟踪一步一步的看了好几遍都没有看出是怎么回事。

一般遇到这种怎么也看不懂的情况,可以肯定的是你走错了路,走到了死胡同。事实却是如此,这个代码是机器生产的。由于个人专业知识的贫瘠,从来没有听说过Ragel这个东西。简单的说就是一种有限状态机语言,专门处理各种解析用的,然后可以把它的语言翻译成主流的各种语言。

对于SIP的解析,就不要看翻译过的代码了,要看对应的“.rl”文件,如"./ragel/tsip_parser_message.rl"。

由于目前没有需要,就打算暂时忽略掉。如果有需要再继续研究Ragel。

 

Doubango代码学习(五):runnable


代码在tsk_runnable.h(c)中。

有一个tsk_runnable_t的结构体,如下:

typedef struct tsk_runnable_s
{
    TSK_DECLARE_OBJECT;
    
    const tsk_object_def_t *objdef;
    
    tsk_thread_handle_t* h_thread[1];
    tsk_runnable_func_run run;
    tsk_thread_id_t id_thread; // no way to get this value from "h_thread" on WINXP
    tsk_semaphore_handle_t *semaphore;
    
    tsk_bool_t running;
    tsk_bool_t started;
    tsk_bool_t initialized;
    /** whether the enqueued data are important or not. 
    * if yes, the thread will not be joined until all data in the queue have been consumed.
    * default value: tsk_false
    */
    tsk_bool_t important;

    int32_t priority;
    
    tsk_list_t *objects;
}
tsk_runnable_t;

可以看出,它是一个类,但这个类没有实例化过,全部是作为基类用的。可以理解为虚类(当然,这里面也没有这个概念)。派生类有tsip_stack_t,tnet_transport_t, tdav_runnable_video_t等。继承直接在第一句包含TSK_DECLARE_RUNNABLE就行了。

tsk_runnable_t顾名思义,就是可自行的。当你需要一个线程执行你的任务时,这就是一个比较好的选择。

总结一下用法:

1,编写一个结构体继承tsk_runnable_t,方法就是把TSK_DECLARE_RUNNABLE放到结构体的第一句就行了。

2,由于TSK_DECLARE_RUNNABLE继承于OBJECT,因此你也需要按照之前的OBJECT章节描述的,来定义你的类对象(包括构造函数,销毁函数和比较函数)。

3,用tsk_object_new来生成你的对象。

4,tsk_runnable_start来执行你的对象。注意要先设置好run函数。一般都是用来事件处理,run函数读取事件,然后处理。循环处理代码夹在TSK_RUNNABLE_RUN_BEGIN和TSK_RUNNABLE_RUN_END之间,用TSK_RUNNABLE_POP_FIRST来取出消息。消息生产者用TSK_RUNNABLE_ENQUEUE_OBJECT来发送消息。

5,tsk_runnable_stop来停止你的对象。

6, 还有一些辅助的宏,注意他们的用法,如TSK_RUNNABLE_RUN_BEGIN, TSK_RUNNABLE_RUN_END,TSK_RUNNABLE_ENQUEUE_OBJECT等。

 

Doubango代码学习(六):timer


一个系统肯定少不了timer,在doubango中关于timer的声明(实现)是在tsk_timer.h(c)中。

总结一下timer的用法:

1,调用tsk_timer_mgr_global_ref获取timer mgr的引用。注意当使用完时一定要tsk_timer_mgr_global_unref,避免内存泄露。

2,调用tsk_timer_mgr_global_start来启动timer mgr。注意这个是可以多次调用无副作用的。

3,tsk_timer_mgr_global_schedule来启动一个timer,参数看字面意思即可理解。返回一个timer_id,如果启动多个timer使用相同的callback函数或者有需要cancel的情况,则要保存这个id。

4,tsk_timer_manager_cancel用来取消一个timer。

以上是使用global timer mgr的方法,对于一般情况足够了,但如果你需要自己的timer mgr也是可以的,调用对应的接口可以create,start,schedule和stock等操作。

下面来看一下timer的实现:

typedef struct tsk_timer_manager_s
{
    TSK_DECLARE_RUNNABLE;

    void* mainThreadId[1];
    tsk_condwait_handle_t *condwait;
    tsk_mutex_handle_t *mutex;
    tsk_semaphore_handle_t *sem;

    tsk_timers_L_t *timers;
}
tsk_timer_manager_t;

这是tm的结构体,可以看出是一个Runnable,按照上一篇讲的,它是可以执行的。timer mgr start函数中确实执行了它。

run函数代码就不贴了。在run函数里又启动了一个线程(引用保存在mainThreadId里)。然后接受event(timer),调用callback函数。它说接受的event都是来自于刚刚启动的那个线程。

在__tsk_timer_manager_mainthread函数中有个while循环,第一句就是tsk_semaphore_decrement(manager->sem);,可以看出这是一个生产者-消费者的问题,生产者是tsk_timer_manager_schedule。在之后的语句中先取出第一个timer,然后跟当前时间相比,如果已经超时,这发送消息到run函数。如果没有超时则调用tsk_condwait_timedwait阻塞对应的时间。

此时如果再有新的timer加入(调用schedule函数)。则先把timer按照升序插入到队列中,然后调用tsk_semaphore_increment和tsk_condwait_signal(manager->condwait);,前者用来增加信号量,后者则结束掉刚才的阻塞。这样run里的while循环可以继续执行。。。

如果到超时之前一直没有新的timer,则tsk_condwait_timedwait会超时,然后继续执行。。。




虽刚开始看Doubango源码时还是让我郁闷不已,不过还好,渐渐适应了。刚开始最让我头疼的就是well-defined objects

由于Doubango是用纯C开发的,没有面向对象功能,就加上了这么一个东西。当然这套机制还是很不错的,毕竟纯C搞大型项目还是需要有些技巧的。

well-defined objects是用来模拟面向对象功能的,其中主要就是内存和指针的管理的,毕竟现实的malloc和free太容易出问题了。

源代码在tsk_object.c中:

先看一个结构体

typedef struct tsk_object_def_s
{
    //! The size of the object.
    tsk_size_t size;
    //! Pointer to the constructor.
    tsk_object_t*    (* constructor) (tsk_object_t *, va_list *);
    //! Pointer to the destructor.
    tsk_object_t*    (* destructor) (tsk_object_t *);
    //! Pointer to the comparator.
    int        (* comparator) (const tsk_object_t *, const tsk_object_t *);
}
tsk_object_def_t;

这个定义了一个类对象的结构体,其中size是对象大小,constructor是对象构造函数,destructor是对象销毁函数,comparator是对象比较函数。

同java一样,对于每个类(object)都有一个实际的".class"对象存在,这里也是一样。tsk_object_def_t相当于".class"的结构体。

如对于tsip_stack_t类,存在一个类对象tsip_stack_def_t

static const tsk_object_def_t tsip_stack_def_s = 
{
    sizeof(tsip_stack_t),
    tsip_stack_ctor, 
    tsip_stack_dtor,
    tsk_null, 
};
const tsk_object_def_t *tsip_stack_def_t = &tsip_stack_def_s;

构造stack对象时,需要用到类对象,方法如下:

stack = tsk_object_new(tsip_stack_def_t).

tsk_obj_new函数就不贴了,大致就是分配一块size大小的内存,然后调用构造函数初始化,再把引用置成1.

当用完一个对象时,避免使用tsk_object_delete来释放,应当使用tsk_object_unref。把引用值减一时,会检查引用是否为0,如果为0就自动释放掉内存。

当别人传进来一个对象时,如果你要异步地使用它,最好是tsk_object_ref一下,把引用值+1。当使用完以后,再unref一下。

还有就是模拟继承的方法,这个最好能不用就不用,因为我感觉模拟的不好,会带来很多弊端。

总结一下:

当你需要一个新类时,你要先生成一个类对象,如下:

static const tsk_object_def_t tsip_XXX_def_s = 
{
    sizeof(tsip_XXX_t), //大小
    tsip_XXX_ctor,  //构造函数
    tsip_XXX_dtor, //销毁函数
    tsk_XXX_cmp, //比较函数 
};

然后定义你的类tsip_XXX_t

typedef struct tsip_XXX_s
{
    TSK_DECLARE_OBJECT;//这个必须带,而且必须放到第一位。
    ... //定义你自己的成员.
}
tsip_XXX_t;

这样你的类就定义完成了,当想生成一个对象时,要调用tsk_object_new来生成。

当想销毁一个对象时要调用tsk_object_unref.

因为没有自动处理引用,当你需要保存一个对象是,调用tsk_object_ref来增加引用。一旦你增加了引用,必须在对应的unref处理,来保证不会有内存泄露。

当你比较两个对象是否相等时,调用tsk_object_cmp来比较.

 

Doubango代码学习(三):fsm


状态机是事务处理中必不可少,在tsk_fsm.h/c里就实现了状态机机制,而且在各个协议层都会用到大量的状态机。只有学好了状态机,才能真正了解Doubango的运行机制,才能对Doubango修改和定制。

要定义一个状态机,首先需要有状态(state),还有事件(action),还有对事件的处理(exec)。下面是状态机的结构:

typedef struct tsk_fsm_s
{
    TSK_DECLARE_OBJECT;

    unsigned debug:1;
    tsk_fsm_state_id current;
    tsk_fsm_state_id term;
    tsk_fsm_entries_L_t* entries;

    tsk_fsm_onterminated_f callback_term;
    const void* callback_data;

    TSK_DECLARE_SAFEOBJ;
}
tsk_fsm_t;

TSK_DECLARE_OBJECT学了前一批就知道是啥了,略过。current和term分别是起始状态和结束状态;callback_term是状态机结束时调用的清理函数;callback_data是状态机构造时保存的数据,待到状态机结束时用的;entries是个list,里面对应的是每个状态对每个事件的处理,结构体如下:

typedef struct tsk_fsm_entry_s
{
    TSK_DECLARE_OBJECT;

    tsk_fsm_state_id from;
    tsk_fsm_action_id action;
    tsk_fsm_cond cond;
    tsk_fsm_state_id to;
    tsk_fsm_exec exec;
    const char* desc;
}
tsk_fsm_entry_t;

其中from是当前状态,action是输入的动作,cond是个返回boolean的函数,如果调用cond返回false,则说明当前action无效,状态机不做任何变化;如果是true则把状态切换到to,然后执行exec。可以阅读以下tsk_fsm_act函数,里面很简单就不贴上了。

总结一下FSM的用法:

1,调用tsk_fsm_create创建一个状态机,两个参数分别是起始状态(current)和结束状态(term)。

2,调用tsk_fsm_set_callback_terminated来设置callback_term和callback_data.

3,调用tsk_fsm_set来初始化fsm。这一步设置fsm_entrys,查看一下TSK_FSM_ADD宏还有调用的地方,看看如何使用的。

4, 至此状态机初始化完成,之后调用tsk_fsm_act来为fsm输入事件和数据。

5,其他还有一些辅助函数和宏,检查一下tsk_fsm.h吧。

Doubango代码学习(四):ragel state和message parser


使用SIP不可避免遇到SIP的解析。 Doubango使用了Ragel来解析的,效率当然是高,但是代码实在是晦涩难懂。我曾经看了tsip_message_parser_execute一整天的时间,单步跟踪一步一步的看了好几遍都没有看出是怎么回事。

一般遇到这种怎么也看不懂的情况,可以肯定的是你走错了路,走到了死胡同。事实却是如此,这个代码是机器生产的。由于个人专业知识的贫瘠,从来没有听说过Ragel这个东西。简单的说就是一种有限状态机语言,专门处理各种解析用的,然后可以把它的语言翻译成主流的各种语言。

对于SIP的解析,就不要看翻译过的代码了,要看对应的“.rl”文件,如"./ragel/tsip_parser_message.rl"。

由于目前没有需要,就打算暂时忽略掉。如果有需要再继续研究Ragel。

 

Doubango代码学习(五):runnable


代码在tsk_runnable.h(c)中。

有一个tsk_runnable_t的结构体,如下:

typedef struct tsk_runnable_s
{
    TSK_DECLARE_OBJECT;
    
    const tsk_object_def_t *objdef;
    
    tsk_thread_handle_t* h_thread[1];
    tsk_runnable_func_run run;
    tsk_thread_id_t id_thread; // no way to get this value from "h_thread" on WINXP
    tsk_semaphore_handle_t *semaphore;
    
    tsk_bool_t running;
    tsk_bool_t started;
    tsk_bool_t initialized;
    /** whether the enqueued data are important or not. 
    * if yes, the thread will not be joined until all data in the queue have been consumed.
    * default value: tsk_false
    */
    tsk_bool_t important;

    int32_t priority;
    
    tsk_list_t *objects;
}
tsk_runnable_t;

可以看出,它是一个类,但这个类没有实例化过,全部是作为基类用的。可以理解为虚类(当然,这里面也没有这个概念)。派生类有tsip_stack_t,tnet_transport_t, tdav_runnable_video_t等。继承直接在第一句包含TSK_DECLARE_RUNNABLE就行了。

tsk_runnable_t顾名思义,就是可自行的。当你需要一个线程执行你的任务时,这就是一个比较好的选择。

总结一下用法:

1,编写一个结构体继承tsk_runnable_t,方法就是把TSK_DECLARE_RUNNABLE放到结构体的第一句就行了。

2,由于TSK_DECLARE_RUNNABLE继承于OBJECT,因此你也需要按照之前的OBJECT章节描述的,来定义你的类对象(包括构造函数,销毁函数和比较函数)。

3,用tsk_object_new来生成你的对象。

4,tsk_runnable_start来执行你的对象。注意要先设置好run函数。一般都是用来事件处理,run函数读取事件,然后处理。循环处理代码夹在TSK_RUNNABLE_RUN_BEGIN和TSK_RUNNABLE_RUN_END之间,用TSK_RUNNABLE_POP_FIRST来取出消息。消息生产者用TSK_RUNNABLE_ENQUEUE_OBJECT来发送消息。

5,tsk_runnable_stop来停止你的对象。

6, 还有一些辅助的宏,注意他们的用法,如TSK_RUNNABLE_RUN_BEGIN, TSK_RUNNABLE_RUN_END,TSK_RUNNABLE_ENQUEUE_OBJECT等。

 

Doubango代码学习(六):timer


一个系统肯定少不了timer,在doubango中关于timer的声明(实现)是在tsk_timer.h(c)中。

总结一下timer的用法:

1,调用tsk_timer_mgr_global_ref获取timer mgr的引用。注意当使用完时一定要tsk_timer_mgr_global_unref,避免内存泄露。

2,调用tsk_timer_mgr_global_start来启动timer mgr。注意这个是可以多次调用无副作用的。

3,tsk_timer_mgr_global_schedule来启动一个timer,参数看字面意思即可理解。返回一个timer_id,如果启动多个timer使用相同的callback函数或者有需要cancel的情况,则要保存这个id。

4,tsk_timer_manager_cancel用来取消一个timer。

以上是使用global timer mgr的方法,对于一般情况足够了,但如果你需要自己的timer mgr也是可以的,调用对应的接口可以create,start,schedule和stock等操作。

下面来看一下timer的实现:

typedef struct tsk_timer_manager_s
{
    TSK_DECLARE_RUNNABLE;

    void* mainThreadId[1];
    tsk_condwait_handle_t *condwait;
    tsk_mutex_handle_t *mutex;
    tsk_semaphore_handle_t *sem;

    tsk_timers_L_t *timers;
}
tsk_timer_manager_t;

这是tm的结构体,可以看出是一个Runnable,按照上一篇讲的,它是可以执行的。timer mgr start函数中确实执行了它。

run函数代码就不贴了。在run函数里又启动了一个线程(引用保存在mainThreadId里)。然后接受event(timer),调用callback函数。它说接受的event都是来自于刚刚启动的那个线程。

在__tsk_timer_manager_mainthread函数中有个while循环,第一句就是tsk_semaphore_decrement(manager->sem);,可以看出这是一个生产者-消费者的问题,生产者是tsk_timer_manager_schedule。在之后的语句中先取出第一个timer,然后跟当前时间相比,如果已经超时,这发送消息到run函数。如果没有超时则调用tsk_condwait_timedwait阻塞对应的时间。

此时如果再有新的timer加入(调用schedule函数)。则先把timer按照升序插入到队列中,然后调用tsk_semaphore_increment和tsk_condwait_signal(manager->condwait);,前者用来增加信号量,后者则结束掉刚才的阻塞。这样run里的while循环可以继续执行。。。

如果到超时之前一直没有新的timer,则tsk_condwait_timedwait会超时,然后继续执行。。。




猜你喜欢

转载自blog.csdn.net/zhangdong305/article/details/44679041