软件设计的两种方法不过于自顶向下与自底向上。
对于自顶向下而言,先设计好用户接口,再往下延伸至各个功能块的具体实现。而对于自底向上而言,自然是有了设计好的各个功能代码块,再将这些功能代码块拼接成具体的用户接口,然而实际的软件实践中,这两种方法是同时展开的,因为无论单独实施哪一种,都是有所缺陷,会令人陷入某种困境。
设计软件如此,那么在分析一款软件的时候,结合自顶向下与自底向上的方法,必然是更好的一种捷径,对软件的分析能更通透和彻底。
Doubango是一个sip协议客户端软件,通过学习它,能更好地掌握sip协议栈,不仅如此,在学习Doubango的过程中,对于其他软件的设计问题,例如怎样设计才能使各个软件模块达到高聚类低耦合的设计原则,各个模块间的接口如何才能很好的衔接,都在Doubango的设计中有一定的体现。对于一个设计很好的软件系统,功能模块的作用必然是明晰的,聚类的,与其他模块间的功能是能达到最低限度依赖的。
在Doubango的最顶层,对于协议栈的抽象描述便是这个SipStack类。它有两方面的作用:
1) 提供对于协议栈的抽象。
2) 底层代码与高层代码的粘合层。
SipStack 类定义在SipStack.cxx中,其接口描述定义在SipStack.h中。
分析一个类,最好的方法是:首先读其私有字段,这代表了它的内部机理;其次读其接口,这代表了它的对外功能,也就是用户可以用它来做什么。
SipStack的私有字段有四个:
a) SipCallback*callback;
这个字段由上层代码提供,也就是由用户提供,在创建一个SipStack协议时,用户必须提供一个SipCallBack类的实例,传入SipStack的构造函数:
SipStack(SipCallback*callback,
const char* realm_uri, const char* impi_uri, const char* impu_uri);
b) DDebugCallback*debugCallback;
这个字段功调试所用。
c) tsip_stack_handle_t*handle;
这个字段代表了底层对于协议栈真正的描述。在SipStack类被构造时,会根据传入的参数真正地创建一个在底层运行着的协议栈。
d) static unsignedcount;
代表了该协议栈的实用计数。
由这四个字段不难看出,SipStack类是作为一个粘合层而存在的,它是高层和底层的粘合剂,高层通过SipCallBack注册到SipStack类,而SipStack类又通过tsip_stack_handle_t实例关联底层,当SipStack在其构造函数中创建tsip_stack_handle_t实例时,会将本粘合层的回调函数intstack_callback(consttsip_event_t*sipevent)注册在tsip_stack_handle_t的对应字段中,这样,当底层的sipevent到来时,会通过回调机制层层传递到应用程序的最顶层。
在底层,真正运转的协议栈结构式tsip_stack_handle_t的一个实例,它的创建时机为SipStack类在构造时,也就是说,当上层不管用何种方式触发一个SipStack的创建动作时。在上层,SipStack会被继续抽象封装,这种封装有可能是封装成为一个java类,一个C#类,但是都不重要,重要的是,这些高层代码类必然关联了一个粘合层的SipStack实例。当粘合层(SipStack.cxx)中得SipStack被创建时,其构造函数会被调用。在它的构造函数类,会调用底层函数tsip_stack_create(…) 创建协议栈结构实例。
A. 记录通过可变参数传入的一系列变量到stack(tsip_stack_handle_t实例)中。包括服务器地址(假设为:192.168.1.101),本机用户名(假设为:1002),sip-uri(sip:[email protected])。这些都将以字符窜形式记录到对应的stack字段中。
需要注意的是:由于传入的字符串可能是托管资源,因此,这里我们没有必要去显式释放它们。释放他们应该是高层代码的责任(有可能通过托管释放,也有可能需要手动释放)。设置为tsip_stack_set(tsip_stack_handle_t * stack,…)这是一个通过可变参数进行结构类实例设置的函数,设置时通过type-value的形势传递参数,也就是在一系列的可变参数中,其中一对可能是(type: tsip_pname_realm, value:”192.168.1.101”),在该函数内,对于字符串有一个拷贝动作。也就是说,记录的是stack自己管理的内存区。
B. 记录粘合层SipStack提供的回调函数到stack->callback字段,以提供sipevent到来时的回调。在上一节中,可以看到SipStack类提供的回调函数为:
Int stack_callback(tsip_event_t *);
C. 创建一个基于stack的全局timer管理结构,用来处理该协议栈中各种需要定时器功能的模块,在后面我们可以看到,对于一个dialog的四种事物:tsip_trsaction_ict,
tsip_trsaction_ist, tsip_trsaction_nict, tsip_trsaction_nist,都需要有定时重传的机制,这里仅仅是创建了这个定时管理结构,还没有启动该定时线程,定时线程稍后会在start stack 的时候启动,定时线程拥有两个队列:收集队列和分发队列。
D. 创建一个sessions的队列,用来管理基于该stack的各种session。
E. 最后创建三个层,可以说,sip协议是层次分明的,上层提供给下一层的回调,下一层将sip消息处理后传递给上一层处理,这是典型的网络处理方式。
三个层次为:
struct tsip_dialog_layer_s *layer_dialog;
struct tsip_transac_layer_s *layer_transac;
struct tsip_transport_layer_s *layer_transport;
分别是对话层,事物层和传输层,具体关系以后分析。
/* 3GPP IMS/LTE stack (forinternal use). only tsip_stack_handle_t should be visible. */
typedef struct tsip_stack_s
{
TSK_DECLARE_RUNNABLE;//用于标志该结构是一个可以运行run()线程的结构
tsk_bool_t timer_mgr_started; //全局定时器启动标志
tsk_bool_t started; //协议栈启动标志
tsip_stack_callback_f callback;//记录粘合层的回调函数
tsip_uris_L_t* paths;
tsip_uris_L_t* service_routes;
tsip_uris_L_t* associated_uris;
/* DNS context */
tnet_dns_ctx_t *dns_ctx;
/* DHCP context */
/* QoS */
/* Internals. */
//tsk_timer_manager_handle_t* timer_mgr;
tsip_timers_t timers;
tsip_ssessions_L_t *ssessions; //会话队列
tsk_params_L_t *headers; //记录的各种键值对
const void* userdata; //上层用户私有数据,一般记录粘合层SipStack实例地址
/* Layers */
}
tsip_stack_t;
对于Doubango中得sip协议栈,是通过SipStack类粘合上层代码与底层代码的,该类定义在SipStack.h中,实现在SipStack.cxx中。当构造好一个SipStack实例之后,对于底层而言,实际上是创建了一个tsip_stack_t 的实例,这个实例在SipStack类中通过tsip_stack_handle_t *handle字段指向。此时,handle对于上层而言是不透明的,从tsip_stack_handle_t的定义也可以看出:
typedefvoid tsip_stack_handle_t
因此,handle实际上是一个void指针。指向具体的tsip_stack_t实例。实际上,stack的底层实例已经创建好,并且已经设置好了很多字段信息。但是stack仍旧是没有启动的,若要启动协议栈,对于上层代码而言,可以通过SipStack->start()达到此目的。
SipStack->start(),正是提供给上层启动底层协议栈的粘合接口。而该接口仅仅是对底层协议栈启动的真正函数tsip_stack_start(tsip_stack_t *stack)函数的一个封装而已。
tsip_stack_start(tsip_stack_t *stack)函数主要完成三件事情:
判断工作模式为服务器模式或者是客户端模式,在默认情况下,Doubango是作为一个客户端软电话的底层运作机制配置的,因此在这种情况下,我们得到的是客户端运行模式。
另外,还要判断网络层协议族和传输层(tcp,udp)类型,默认情况下为IPV4以及UDP。选择一个最合适的本地ip地址(根据目前的网口选择)网口的IP地址最终由getaddrinfo(…)获得。
在stack的结构中,有一个TSK_DECLARE_RUNNABLE的宏声明,这个宏实际上在stack的顶端嵌入了一个tsk_runnable_t__runnable__的结构实例,因此,stack可以被安全的强制转化为一个tsk_runnable_t的类型实例,而这就是TSK_RUNNABLE宏的作用。
TSK_RUNNABLE(stack)->objects是一个队列头,传入其中的元素是通用的tsk_list_item_t,而tsk_list_item_t的作用就是将一个需要记录的实例(任何结构的实例)挂接在tsk_list_item_t的void *data 字段。
因此,TSK_RUNNABLE(stack)->objects实际上是一个队列,该队列中挂载的结构实例便是tsip_event_t结构的实例。
Stack的run(…)线程在stack的生存周期内一直在运行,它的作用是从TSK_RUNNABLE(stack)->objects中取出一个tsip_event_t实例,然后通过注册到stack的回调函数,将该sip事件传递到高层的用户代码中。实际上,这里的传递需要穿越粘合层,因为stack中回调函数即stack->callback(…),便是由SipStack类注册的stack_callback(…)函数,该函数的原型如下:
int stack_callback(tsip_event_t*sipevent);
首先,生成一个默认的tsip_transport_t实例,链接如stack的tsip_transport_layer的transports队列。
其次,启动tsip_transport_layer,这里便完成了套接字的生成,端口绑定,若是tcp协议还要完成connect的相关事宜。
到了这里,粘合层的任务便已经完成了,底层的套接口已经启动,可以接受来自对端的sip消息和处理该sip消息了。
对于一个刚启动的协议栈来说,它需要有一个传输层,支持若干的传输结点。每一个传输结点对应于一个端口,若采用TCP连接,一个传输结点就针对于一个点到点的连接,这个连接负责sip信令的可靠交换;若采用UDP进行sip信令的交换,则需要由应用程序维护一个定时器,以防数据包丢失的时候用于重传。
Doubango里一个协议栈对应于一个tsip_stack_t实例,而这样一个实例又拥有有三个层次,从上到下依次为:
事物层,对话层,传输层。
各种关系如下图所示:
在实际中,在协议栈启动的时候,会首先生成一个默认的传输实例,并挂接到传输层的transports队列中,完成该工作的函数是:
在创建默认传输实例时,各个形参对应的实参为:
在创建一个tsip_transport_t实例的时候,会随便创建一个tnet_transport_t实例,tsip_transpor_t与tnet_transport_t是一一对应的关系。
而tnet_transport_t对应了两个线程,一个线程成为mainthread线程,一个称为run线程。他们的作用描述如下:
Mainthread线程:在其主循环内用于从套接口缓冲区读取数据,并生成tnet_transport_event_t实例,这个实例代表到达的一个网络层消息。生成以后把它连接入tnet_transport_t的一个队列,该队列负责管理各个tnet_transport_event_t实例。
Run线程:把tnet_transport_event_t实例从上所述队列中出队列,通过回调传入tsip_transpor_t的处理函数,对于UDP和TCP对应的回调函数分别是,
tsip_transport_layer_dgram_cb(…)和tsip_transport_layer_stream_cb(…)
这两个函数是在启动第一个默认传输实例tsip_transport_t实例时记录到tnet_transport_t的callback字段的,callback是一个函数指针,tnet_transport_t用它来把消息回传给tsip_transport_t进行处理。
对于一个刚启动的协议栈来说,它需要有一个传输层,支持若干的传输结点。每一个传输结点对应于一个端口,若采用TCP连接,一个传输结点就针对于一个点到点的连接,这个连接负责sip信令的可靠交换;若采用UDP进行sip信令的交换,则需要由应用程序维护一个定时器,以防数据包丢失的时候用于重传。
Doubango里一个协议栈对应于一个tsip_stack_t实例,而这样一个实例又拥有有三个层次,从上到下依次为:
事物层,对话层,传输层。
各种关系如下图所示:
在实际中,在协议栈启动的时候,会首先生成一个默认的传输实例,并挂接到传输层的transports队列中,完成该工作的函数是:
在创建默认传输实例时,各个形参对应的实参为:
在创建一个tsip_transport_t实例的时候,会随便创建一个tnet_transport_t实例,tsip_transpor_t与tnet_transport_t是一一对应的关系。
而tnet_transport_t对应了两个线程,一个线程成为mainthread线程,一个称为run线程。他们的作用描述如下:
Mainthread线程:在其主循环内用于从套接口缓冲区读取数据,并生成tnet_transport_event_t实例,这个实例代表到达的一个网络层消息。生成以后把它连接入tnet_transport_t的一个队列,该队列负责管理各个tnet_transport_event_t实例。
Run线程:把tnet_transport_event_t实例从上所述队列中出队列,通过回调传入tsip_transpor_t的处理函数,对于UDP和TCP对应的回调函数分别是,
tsip_transport_layer_dgram_cb(…)和tsip_transport_layer_stream_cb(…)
这两个函数是在启动第一个默认传输实例tsip_transport_t实例时记录到tnet_transport_t的callback字段的,callback是一个函数指针,tnet_transport_t用它来把消息回传给tsip_transport_t进行处理。
当通过sip协议发起一个会话时,需要通过invite消息实现该流程。而SIP协议是一个基于事务的协议,每一个sip会话的都是通过sip部件间的一系列消息来完成的。首先需要明确的重要概念就是事务。
在SIP协议中,一个事务是指完成一次消息交互的整个流程。以INVITE消息为例,一个基于代理服务器交换信令和语音视频数据包的事务模型如下图所示:
如图所示,对于1003来说,虚线框内的从INVITE消息到代理服务器回送的200OK整个消息交互流程称为一个客户端事务,而对于1005来说,虚线框内的从INVITE消息到代理服务器回送的200OK整个消息交互流程称为一个服务器事务。
不管是客户端事务还是服务器事务,都必须维护一个有限状态机,记录当前事务的进展情况,事务和其状态机的维护,构成了一款SIP终端软件最重要的一部分。
客户端INVITE事务的状态转换如上图所示。
当用两个终端发起INVITE呼叫时,用wireshark抓包得到的结果如下图所示:
其中ip:192.168.1.33是代理服务器的地址,ip:192.168.1.104是客户端sip终端的地址。这里,代理服务器的作用相当于一个UAS。这里总共有两个事务流程,整个过程如下:
a) Sip终端在地址192.168.1.104向服务器发起一个会议3000的INVITE消息,客户端进入calling状态,启动A和B的定时器,用于INVITE消息的超时重传。
b) 服务器发送100/trying,客户端收到后进入proceeding状态,取消A和B定时器。
c) 服务器发送407要求认证,客户端进入Completed状态。
d) 客户端通过ACK发送认证信息。
e) 进入Terminated状态后销毁该事务。
到此为止,一个客户端INVITE事务结束。
a) Sip终端在地址192.168.1.104再次向服务器发起一个会议3000的INVITE消息,启动A和B的定时器,用于INVITE消息的超时重传
b) 服务器发送100/trying,客户端收到后进入proceeding状态,取消A和B定时器。
c) 服务器发送200OK,客户端进入Accepted状态。
d) 客户端向服务器发送ACK应答。
注:其中在进入calling状态之前,也就是在发送INVITE消息时,客户端必须将其中的SDP消息包含着INVITE消息的content中传送到。
在客户端接收到Accepted消息(200Ok)后,或根据得到的SDP做解析,启动正确的音视频编解码器,生成RTP端口,在最后的ACK中发送给服务器,这是,通话开始进行。
在Doubango协议栈中,最后一步的处理由tsip_dialog_invite.client.c文件的int c0000_Outgoing_2_Connected_X_i2xxINVITE(va_list *app)函数处理。
描述:
一个invite_dialog代表了一个invite期间的所有的信令流程,因此,它首先是一个普遍的dialog的特殊化结构,在该结构的起始部分,有一个TSIP_DECLARE_DIALOG声明,该声明展开后是一个tsip_dilog_t __dialog字段的定义,这是一种在C中一个具化对象对通用对象的继承机制,tsip_dialog_t对象代表了更通用的实例,而tsip_dialog_invite_t对象则是tsip_dialog_t对象的具化和拓展。
一个dialog本身有自己的有限状态处理机,有自己的当前状态和当前状态应执行的动作,还有一个状态变化时的回调函数。它还需要有一个字段指向它所属的session,这些字段,反应在数据类型上上,分别是:
tsk_fsm_t(有限状态机),tsip_action_t(状态执行动作),tsip_dialog_state_t(当前状态),
tsip_dialog_event_callback_f(回调函数),tsip_ssession_t(所属session)。
创建时机:
tsip_action_INVITE()函数是上层应用与底层协议栈的接口,当上层应用发起一个INVITE时,便会分层调用到该接口。
tsip_dialog_invite_t类型的实例便是在该接口中创建的,对于创建时,还需要在协议栈的全局dialog队列中寻找一遍,看是否相关的dialog已经在之前被创建,若是,则沿用老的实例,若否,则用tsip_dialog_layer_new()函数创建一个新的实例,并链入到协议栈dialog_layer层的全局队列中。
关键点:
描述:
一个tmedia_session_mgr_t实例是由上述的tsip_dialog_invite_t实例的session_mgr字段记录的,代表了在一个对话期间各种媒体设置的管理者。
当发起一个INVITE时,首先必须存在一个代表信令流程的对象即dialog对象,其次,还要有一个负责管理媒体信息的对象,及该结构的实例。
创建时机:
该结构的首次创建是在invite_dialog的状态转换过程中,c0000_Started_2_Outgoing_X_oINVITE()函数是一个状态转化函数,它代表着当前dialog由初始状态向发起INVITE后状态转化需要执行的操作,在该函数内部,通过tmedia_session_mgr_create()创建tmedia_session_mgr_t实例,然后填写本地媒体信息(即sdp中得各个字段),申请rtp端口号,生成代表rtp的网络传输实例(tnet_transport_t对象),最后在该状态转换函数中,通过协议栈的代表信令的网络传输实例将INVITE消息传递出去。
关键点:
在tmedia_session_mgr_t中,有一个tmedia_sessions_L_t*sessions的字段,这个字段其实是一个队列,挂载的表示代表了各种媒体的信息(音频,视频),每一个队列节点是一个tmedia_session_t的实例。
关键函数:
_tmedia_session_mgr_load_sessions(tmedia_session_mgr_t *mgr)函数
原型:_tmedia_session_mgr_load_sessions(tmedia_session_mgr_t *mgr)
作用:用于生成和加载各种类型的tmedia_session_t实例。
说明:_tmedia_session_mgr_load_sessions()函数在tmedia_session_mgr_create()函数中被调用,在这个函数内,会生成各个tmedia_session_t实例,挂入tmedia_session_mgr_t实例的sessions队列。其中在_tmedia_session_mgr_load_sessions()函数内的处理片段为:
… } |
__tmedia_session_plugins是一个tmedia_session_plugin_def_t类型的全局指针数组,用于存放各种媒体类型的定义。
tmedia_session_create()函数用于更具媒体的类型创建一个tmedia_session_t类型的实例,媒体类型由上层应用指定,记录在tmedia_session_mgr_t实例的type字段,常用的有audio,vedio,audiovideo三个类型。
描述:
这其实也是一个通用结构,被更具体的结构包含着。
而tdav_session_audio_t,tdav_session_video_t等结构便是更具体的结构,在这些结构的其实,包含了该通用的结构。每一个更通用的结构都记录一个trtp_manager_s的实例,这个结构便是代表语音流和视频流的管理者。
在创建tmedia_session_t实例时,是根据plugin->objdef来调用tsk_object_new()的,调用语句为tsk_object_new(plugin->objdef),plugin是上一节所述的__tmedia_session_plugins 所记录的tmedia_session_plugin_def_t 实例,故当plugin->type为audio类型时,创建的真正结构为tdav_session_audio_t,真正的构造函数为tdav_session_audio_ctor(),定义在tdav_session_audio.c中。
这个过程的调用堆栈为:
|
创建时机:
见上一条tmedia_session_mgr_t的关键点。
关键点:
在一个tmedia_session_t结构的实例中,承载了这个tmedia_session_t实际类型的编解码插件,例如tmedia_session_t的type为video时,在tmedia_session_init(tmedia_session_t*self)函数中会初始化一个tmedia_session_t的实例,而根据多态的原理,这个tmedia_session_t实例的真正类型为tdav_session_video_t实例,因此,在初始化时,会从__tmedia_codec_plugins数组中加载所有的type为video的解码模块,__tmedia_codec_plugins数组会在启动协议栈时初始化,具体的初始化函数是tdav_init(),这是也会根据能够加载的编解码模块,将各个编解码插件记录到该__tmedia_codec_plugins全局指针数组中,数组的每一个项都指向了一个plugin,一个plugin代表了一个实际的编解码模块。
关键函数描述:
tmedia_session_init(…)函数:
原型: tmedia_session_init(tmedia_session_t*self,tmedia_type_ttype)
作用: 初始化一个新生成的tmedia_session_t实例,加载各个编解码模块
_tmedia_session_load_codecs(tmedia_session_t *self)函数;
描述:该函数被tmedia_session_init(…)函数调用,用于加载编解码模块
加载编解码模块的代码片段为:
|
描述:
在一个tsip_dialog_invite_t实例被创建,进入它的状态机执行时,第一个状态的转化为:
Started -> (oINVITE)-> Outgoing
此时由函数int c0000_Started_2_Outgoing_X_oINVITE(va_list*app)进行该状态的处理,在此函数内部,会为tsip_dialog_invite_t实例创建tmedia_session_mgr_t实例并记录在tsip_dialog_invite_t实例的msession_mgr字段,创建msession_mgr的同时,会建立msession_mgr的sessions队列和加载各种编解码模块。完成这一切后,会调用send_INVITEorUPDATE(…)函数,完成一个sip的invite request。
该函数的原型为:
int send_INVITEorUPDATE(tsip_dialog_invite_t*self,tsk_bool_tis_INVITE,tsk_bool_tforce_sdp)
从名字可以看出,函数除了完成INVITE外,还可以完成一个sip会话的更新操作,例如可能编解码变更,而不想重新发起一个INVITE请求时,便在原有请求的基础上进行UPDATE。
具体是一个新的INVITE还是一个UPDATE,由参数is_INVITE指定。
在一个新的INVITE请求发起时,最重要的结构便是tsip_request_t结构,这个结构代表了一个真正的sip消息。
关键点:
在一个INVITE请求发起时,生成一个tsip_request_t实例的过程有以下几步:
a) 用tsip_dialog_request_new(…)函数生成tsip_request_t结构的实例。并做相应的初始化。
b) 当这个dialog的状态为tsip_initial时,需要为SIP_INVITE消息的消息体生成SDP包,此时分解为以下几个小步骤(假设代码中的self为tsip_dialog_invite_t实例):
1) 调用用函数tmedia_session_mgr_get_lo(self->msession_mgr),通过msession_mgr中sessions队列记录的tmedia_session_t实例和每个tmedia_session_t实例codecs队列中记录的编解码模块生成一个tmedia_sdp_t的实例。该实例便携带了媒体协商的所有数据。在这个过程中,还要完成RTP端口的生成,为以后的音视频通话搭建真正的通道,这个过程将在后面解析。
2) 调用tsdp_message_tostring(…)函数将tmedia_sdp_t的实例转化为ASCII字符。假设这一步中得到的字符串为sdp。
3) 调用tsip_message_add_content(…)函数将sdp拷贝到tsip_request_t实例的的message_body中。释放sdp。
原型:const tsdp_message_t*tmedia_session_mgr_get_lo(tmedia_session_mgr_t*self)
作用:准备SDP信息,给新的通话开一个RTP端口
目录(?)[+]
描述:
一个invite_dialog代表了一个invite期间的所有的信令流程,因此,它首先是一个普遍的dialog的特殊化结构,在该结构的起始部分,有一个TSIP_DECLARE_DIALOG声明,该声明展开后是一个tsip_dilog_t __dialog字段的定义,这是一种在C中一个具化对象对通用对象的继承机制,tsip_dialog_t对象代表了更通用的实例,而tsip_dialog_invite_t对象则是tsip_dialog_t对象的具化和拓展。
一个dialog本身有自己的有限状态处理机,有自己的当前状态和当前状态应执行的动作,还有一个状态变化时的回调函数。它还需要有一个字段指向它所属的session,这些字段,反应在数据类型上上,分别是:
tsk_fsm_t(有限状态机),tsip_action_t(状态执行动作),tsip_dialog_state_t(当前状态),
tsip_dialog_event_callback_f(回调函数),tsip_ssession_t(所属session)。
创建时机:
tsip_action_INVITE()函数是上层应用与底层协议栈的接口,当上层应用发起一个INVITE时,便会分层调用到该接口。
tsip_dialog_invite_t类型的实例便是在该接口中创建的,对于创建时,还需要在协议栈的全局dialog队列中寻找一遍,看是否相关的dialog已经在之前被创建,若是,则沿用老的实例,若否,则用tsip_dialog_layer_new()函数创建一个新的实例,并链入到协议栈dialog_layer层的全局队列中。
关键点:
描述:
一个tmedia_session_mgr_t实例是由上述的tsip_dialog_invite_t实例的session_mgr字段记录的,代表了在一个对话期间各种媒体设置的管理者。
当发起一个INVITE时,首先必须存在一个代表信令流程的对象即dialog对象,其次,还要有一个负责管理媒体信息的对象,及该结构的实例。
创建时机:
该结构的首次创建是在invite_dialog的状态转换过程中,c0000_Started_2_Outgoing_X_oINVITE()函数是一个状态转化函数,它代表着当前dialog由初始状态向发起INVITE后状态转化需要执行的操作,在该函数内部,通过tmedia_session_mgr_create()创建tmedia_session_mgr_t实例,然后填写本地媒体信息(即sdp中得各个字段),申请rtp端口号,生成代表rtp的网络传输实例(tnet_transport_t对象),最后在该状态转换函数中,通过协议栈的代表信令的网络传输实例将INVITE消息传递出去。
关键点:
在tmedia_session_mgr_t中,有一个tmedia_sessions_L_t*sessions的字段,这个字段其实是一个队列,挂载的表示代表了各种媒体的信息(音频,视频),每一个队列节点是一个tmedia_session_t的实例。
关键函数:
_tmedia_session_mgr_load_sessions(tmedia_session_mgr_t *mgr)函数
原型:_tmedia_session_mgr_load_sessions(tmedia_session_mgr_t *mgr)
作用:用于生成和加载各种类型的tmedia_session_t实例。
说明:_tmedia_session_mgr_load_sessions()函数在tmedia_session_mgr_create()函数中被调用,在这个函数内,会生成各个tmedia_session_t实例,挂入tmedia_session_mgr_t实例的sessions队列。其中在_tmedia_session_mgr_load_sessions()函数内的处理片段为:
… } |
__tmedia_session_plugins是一个tmedia_session_plugin_def_t类型的全局指针数组,用于存放各种媒体类型的定义。
tmedia_session_create()函数用于更具媒体的类型创建一个tmedia_session_t类型的实例,媒体类型由上层应用指定,记录在tmedia_session_mgr_t实例的type字段,常用的有audio,vedio,audiovideo三个类型。
描述:
这其实也是一个通用结构,被更具体的结构包含着。
而tdav_session_audio_t,tdav_session_video_t等结构便是更具体的结构,在这些结构的其实,包含了该通用的结构。每一个更通用的结构都记录一个trtp_manager_s的实例,这个结构便是代表语音流和视频流的管理者。
在创建tmedia_session_t实例时,是根据plugin->objdef来调用tsk_object_new()的,调用语句为tsk_object_new(plugin->objdef),plugin是上一节所述的__tmedia_session_plugins 所记录的tmedia_session_plugin_def_t 实例,故当plugin->type为audio类型时,创建的真正结构为tdav_session_audio_t,真正的构造函数为tdav_session_audio_ctor(),定义在tdav_session_audio.c中。
这个过程的调用堆栈为:
|
创建时机:
见上一条tmedia_session_mgr_t的关键点。
关键点:
在一个tmedia_session_t结构的实例中,承载了这个tmedia_session_t实际类型的编解码插件,例如tmedia_session_t的type为video时,在tmedia_session_init(tmedia_session_t*self)函数中会初始化一个tmedia_session_t的实例,而根据多态的原理,这个tmedia_session_t实例的真正类型为tdav_session_video_t实例,因此,在初始化时,会从__tmedia_codec_plugins数组中加载所有的type为video的解码模块,__tmedia_codec_plugins数组会在启动协议栈时初始化,具体的初始化函数是tdav_init(),这是也会根据能够加载的编解码模块,将各个编解码插件记录到该__tmedia_codec_plugins全局指针数组中,数组的每一个项都指向了一个plugin,一个plugin代表了一个实际的编解码模块。
关键函数描述:
tmedia_session_init(…)函数:
原型: tmedia_session_init(tmedia_session_t*self,tmedia_type_ttype)
作用: 初始化一个新生成的tmedia_session_t实例,加载各个编解码模块
_tmedia_session_load_codecs(tmedia_session_t *self)函数;
描述:该函数被tmedia_session_init(…)函数调用,用于加载编解码模块
加载编解码模块的代码片段为:
|
描述:
在一个tsip_dialog_invite_t实例被创建,进入它的状态机执行时,第一个状态的转化为:
Started -> (oINVITE)-> Outgoing
此时由函数int c0000_Started_2_Outgoing_X_oINVITE(va_list*app)进行该状态的处理,在此函数内部,会为tsip_dialog_invite_t实例创建tmedia_session_mgr_t实例并记录在tsip_dialog_invite_t实例的msession_mgr字段,创建msession_mgr的同时,会建立msession_mgr的sessions队列和加载各种编解码模块。完成这一切后,会调用send_INVITEorUPDATE(…)函数,完成一个sip的invite request。
该函数的原型为:
int send_INVITEorUPDATE(tsip_dialog_invite_t*self,tsk_bool_tis_INVITE,tsk_bool_tforce_sdp)
从名字可以看出,函数除了完成INVITE外,还可以完成一个sip会话的更新操作,例如可能编解码变更,而不想重新发起一个INVITE请求时,便在原有请求的基础上进行UPDATE。
具体是一个新的INVITE还是一个UPDATE,由参数is_INVITE指定。
在一个新的INVITE请求发起时,最重要的结构便是tsip_request_t结构,这个结构代表了一个真正的sip消息。
关键点:
在一个INVITE请求发起时,生成一个tsip_request_t实例的过程有以下几步:
a) 用tsip_dialog_request_new(…)函数生成tsip_request_t结构的实例。并做相应的初始化。
b) 当这个dialog的状态为tsip_initial时,需要为SIP_INVITE消息的消息体生成SDP包,此时分解为以下几个小步骤(假设代码中的self为tsip_dialog_invite_t实例):
1) 调用用函数tmedia_session_mgr_get_lo(self->msession_mgr),通过msession_mgr中sessions队列记录的tmedia_session_t实例和每个tmedia_session_t实例codecs队列中记录的编解码模块生成一个tmedia_sdp_t的实例。该实例便携带了媒体协商的所有数据。在这个过程中,还要完成RTP端口的生成,为以后的音视频通话搭建真正的通道,这个过程将在后面解析。
2) 调用tsdp_message_tostring(…)函数将tmedia_sdp_t的实例转化为ASCII字符。假设这一步中得到的字符串为sdp。
3) 调用tsip_message_add_content(…)函数将sdp拷贝到tsip_request_t实例的的message_body中。释放sdp。
原型:const tsdp_message_t*tmedia_session_mgr_get_lo(tmedia_session_mgr_t*self)
作用:准备SDP信息,给新的通话开一个RTP端口