转自:微点阅读 https://www.weidianyuedu.com/content/0417775689505.html
exosip2协议栈学习总结
1、exosip2协议栈介绍
eXosip是osip2的一个扩展协议集,它部分封装了osip2协议栈,使得它更容易被使用。
使用 sip 协议建立多媒体会话是一个复杂的过程,exosip 库开发的目的在于隐藏这种复杂性。正如它的名称所表示的,eXosip2 - the eXtended osip Library,它扩展了osip 库,实现了一个简单的高层API。通过使用exosip,我们可以避免直接使用osip 带来的困难。需要注意,exosip 并不是对osip 的简单封装包裹,而是扩展。osip 专注于sip 消息的解析,事务状态机的实现,而exosip 则基于osip 实现了call、options、register、publish 等更倾向于功能性的接口。当然,这些实现都是依赖于底层osip 库已有的功能的。
2、exosip的模块构成
2.1 底层连接管理
extl.c、extl_udp.c、extl_tcp.c、extl_dtls.c、extl_tls.c 是与网络连接有关的文件。实现了连接的建立,数据的接收以及发送等相关的接口。其中,extl_udp.c 为使用UDP 连接的实现,extl_tcp.c 为使用TcP 连接的实现。Extl_dtls.c 以及extl_tls.c 都是使用安全socket 连接的实现。 2.2 内部功能模块实现 Jauth.c、jcall.c、jdialog.c、jevents.c、jnotify.c、jpublish.c、jreg.c、jrequest.c、jresponse.c、jsubscribe.c 等文件实现了内部对一些模块的管理,这些模块正如其文件名所表示的,jauth.c
主要是认证,jcall.c 则是通话等等。 2.3 上层API 封装实现
Excall_api.c、exinsubsription_api.c、exmessag_api.c、exoptions_api.c、expublish_api.c、exrefer_api.c、exregister_api.c、exsubsribtion_api.c 这几个以api 为后缀的文件,实现各个子模块的管理。应用程序可以调用这里提供的接口,方便的构造或者发送sip 消息。 2.4 其他
Inet_ntop.c 实现ip 地址的点分十进制与十六进制表示之间的转换。 Jcallback.c 实现一堆回调函数,这些回调函数就是用来注册到osip 库的。我们使用exosip 库,就是避免直接使用osip 库,因为一些工作exosip 已经帮我们做了,所以这样一来,可 以简化上层的实现。
Udp.c 文件主要用来对通过UDP 连接接收到的消息进行分类处理。 Exutilis.c 文件实现一些杂项的函数。有ip 地址到字符串之间的转换,域名的解析等一些 辅助的功能函数。
Exconf.c 文件实现了exosip 的初始化相关的接口,包括后台任务的实现。实际上是 “configuration api”的实现。
Exosip.c 文件实现了与exconf.c 文件相似的功能。比如管道的使用,exosip 上事务的创建和查找,register 和subscribe 的更新,认证信息的处理等。
3、exosip关键数据结构及其说明
1
3.1 eXtl_protocol
eXtl_protocol 是为实现网络通信专门定义的一个数据结构,包括了变量和方法两部分。其中,变量包括了建立网络连接过程中使用的ip 地址、端口等;方法部分封装了网络socket编程常用的系统调用接口。
代码中定义了四个该数据结构体的全局变量:
eXtl_udp、eXtl_tcp、eXtl_tls 以及eXtl_dtls。 分别针对使用UDP、TcP 以及安全加密连接进行了实现。
3.2 eXosip_call_t
Exosip_call_t 定义了call 相关的信息,包括call 的id,call 的dialogs,call 上incoming的事务和outgoing 的事务。另外,还包括了前向和后向指针,所以,所有的call 可以通过该结构体串接起来。
3.3 eXosip_dialog_t
exosip_dialog_t 包含了dialog 相关的信息。
2
3.4 eXosip_reg_t
用来管理Register模块 3.5 eXosip_subscribe_t
用来管理subscribe模块 3.6 eXosip_pub_t
用来管理publish模块 3.7 eXosip_notify_t
用来管理notify模块 3.8 jinfo_t
这个结构体关联了dialog、call、subscribe以及notify几个结构体
3
3.9 eXosip_event_t
与 event 有关的结构体。这个结构体主要用来在应用层和exosip 之间通信。Exosip 在处理事务的过程中,如果需要将结果反馈给上层应用,则会生成如上结构类型的事件,并将其放到exosip 的事件队列中。应用层会不断循环从事件队列中读取事件,然后进行应用层的处理。
3.10 eXosip_t
exosip_t 是exosip 中最重要的结构体之一。从图可以看出,这个结构体比较大,其中包含了exosip 中用到的各个子模块的结构。比如call、reg、pub 等等。代码中定义了一个该 结构类型的全局变量,通过该全局变量,就可以对exosip 前的状态进行掌控(许多相关的信息要么包含在该结构上,要么可以通过该结构找到)。
Extl 是eXtl_protocol 类型的指针,保存了网络接口类。 J_osip 保存了osip 初始化时返回的osip 结构体。 J_transactions 一般是等待释放的事务。在事务经过osip 处理完后,不再需要时,exosip会将其放在j_transactions 上,等待释放。
4、exosip的初始化
Exosip 的初始化有两部分组成,这主要是从使用exosip 的角度看。 4.1 对exosip 全局结构体变量的配置
这步通过调用接口eXosip_init 完成。主要完成工作如下: 4.1.1 初始化条件变量和互斥信号量。
4.1.2 调用osip_init 初始化osip 库,并将生成osip 结构体给exosip,同时也让osip 的
4
application_contexgt 指针指向exosip,也就是二者相互指向。
4.1.3 调用eXosip_set_callbacks 设置osip 的回调函数,所以回调函数都是exosip 自己实现。
4.1.4 调用jpipe 创建通信用的pipe,之前已经说了,对于windows 平台,是通过socket 接口模拟实现的。
4.1.5 初始化其上的事务和事件队列。主要,这不同于osip 的事务和事件队列。 4.1.6 调用extl 指向的结构体的init 函数指针,初始化网络接口。
4.2在socket 接口上进行监听
这步通过调用eXosip_listen_addr 接口完成。 主要完成工作如下:
4.2.1 将eXosip 全局变量的eXtl 指针指向eXtl_udp 全局变量。
4.2.2 根据参数,配置extl_protocol 和exosip 上有关ip 端口地址等信息。另外,调用extl_udp的tl_open 函数指针,完成在本机指定的端口上监听连接的工作。需要注意的是,虽然是监听,但是使用的UDP 来建立连接的,所以消息的recv 和发送在同一个socket 上完成。在osip中设置的out_socket 并不会起作用。
4.2.3 调用osip_thread_create 创建exosip 后台任务,用于驱动osip 的状态机。(在osip 中, 在发送sip 消息部分,提到将9 个函数放到一个线程中执行,exosip 就是这样做的)
下面展示了初始化的示例代码: include int i;
TRAcE_InITIALIZE (6, stdout); i=eXosip_init(); if (i!=0) return -1;
i = eXosip_listen_addr (IPPRoTo_UDP, nULL, port, AF_InET, 0); if (i!=0) {
eXosip_quit();
fprintf (stderr, \return -1; }
... then you have to send messages and wait for eXosip events...
这样,在初始化完成后,我们基本上完成了对内存中所用数据结构的配置,同时启动了 一个后台任务负责osip 状态机的驱动。
5、数据收发整体框架
5.1 接收过程
在初始化过程中我们创建了一个后台任务,现在可以看看这个后台任务都做了哪些操作。 任务的执行函数为_eXosip_thread,在该接口中,循环不断的调用eXosip_execute。在每一次 的eXosip_execute 执行中,完成如下的工作:
a. 首先计算出底层osip 离当前时间最近的超时时间。也就是查看底层所有的超时事件,
5
找出其中的最小值,然后将其与当前时间做差,结果就是最小的超时间隔了。这步是通 过调用接口osip_timers_gettimeout 完成的。主要检查osip 全局结构体上的ict、ist、nict、 nist 以及ixt 上所有事务的事件的超时时间。如果 ict 事务队列上没有事件,则说明没 有有效的数据交互存在,返回值为默认的一年,实际上就是让后面的接收接口死等。如 果有事务队列上的事件的超时时间小于当前值,则说明已经超时了,需要马上处理,此 时将超时时间清为零,并返回。
b. 调用 eXosip_read_message 接口从底层接收消息并处理。如果返回-2,则任务退出。 c. 执行 osip 的状态机。具体为执行osip_timers_ict(ist|nict|nist)_execute 和osip_ict (ist|nict|nist)_execute 这几个函数。最后还检查释放已经终结的call、registrations 以及 publications。
d. 如果 keep_alive 设置了,则调用_eXosip_keep_alive 检查发送keep_alive 消息。 这样,当远端的终端代理发送sip 消息过来时,会被之前创建的监听端口捕获(sip 协议 默认的端口为5060)。在调用eXosip_read_message 接口时会将其接收上来。接收上来的数据存放在buffer 中交给接口_eXosip_handle_incoming_message 来处理。在其中首先调用osip_parse 进行消息的解析,这是osip 的核心功能之一。数据解析后,会生成一个osip_event 类型的事件。接着调用osip_message_fix_last_via_header 将接收到该消息的ip 地址和端口根据需要设置到数据头的via 域中。这在消息返回时有可能发挥作用。为了能够让消息正确的被处理,调用osip_find_transaction_and_add_event 接口将其添加到osip 的事务队列上。处理在这之后发生了分叉,如果osip 接纳了该事件,接口直接返回,因为这说明该事件在osip 上已经有匹配的事务了,或者说该事件是某一个事务过程的一部分。这样在后面执行状态机的接口时,该事件会被正确的处理。如果osip 没有拿走该事件,则说明针对该事件还没有事务与之对应。此时,我们首先检查其类型,如果是request,则说明很可能是一个新的事件到来( 这将触发服务端的状态机的建立), 调用eXosip_process_newrequest 接口进行处理。如果是response , 则调用接口eXosip_process_response_out_of_transaction 处理。在 eXosip_process_newrequest 接口中,如果是合法的事件,则会为其创建一个新的事务。 也就是说这是新事务的第一个事件。经过一大堆的处理后,该事件可能就被osip 消化了, 或者被exosip 消化了。如果需要上报给应用,由应用拿来对一些信息进行存储或者进行图 形显示之类,则会将该事件添加到exosip 的事件队列上。如下图所示:
6
应用程序在exosip 初始化完之后需要调用如下类似的代码,不断从事件队列上读取事件, 并进行处理。
eXosip_event_t *je; for (;;) {
je = eXosip_event_wait (0, 50); eXosip_lock();
eXosip_automatic_action (); eXosip_unlock(); if (je == nULL) break;
if (je->type == EXoSIP_cALL_nEW) { .... .... }
else if (je->type == EXoSIP_cALL_AcK) { .... .... }
else if (je->type == EXoSIP_cALL_AnSWERED) { .... .... }
else ..... .... ....
eXosip_event_free(je); }
读到事件后,判断其类型进行对应的处理。这样整个接收流程就完成了。
5.2 发送过程
要发送数据时,需要根据消息类型,调用exosip 对应模块的api 接口函数来完成。如果 要发送的sip 消息不属于当前已有的任何事务,则类似接收过程,调用osip 的相关接口创建 一个新的事务,同时根据消息生成一个事件,加到事务的事件队列上。最后,唤醒exosip 后台进程,使其驱动osip 状态机,执行刚添加的事件,从而完成数据的状态处理和发送。 当然,也有一些消息并不通过osip 状态机,而是由exosip 直接调用回调函数cb_snd_message 完成发送。
7
6、 exosip 与上层应用以及osip 之间的流程关系
exosip 是对osip 库的扩展,那么它与osip 之间是什么样的关系呢,这可参看下图:
上图为接收过程的示意图。Exosip 后台任务不断从网络另一端读取sip 消息,交给osip 的parser 模块解析,并将其转换为events,添加到事务队列上。同时,exosip 后台任务在不断的驱动osip 的状态机,这样,事务队列上的事件就会被处理。如果需要响应对端,状态机会根据回调函数的设置,直接完成数据的发送。同样,如果要将当前处理反馈给应用,则将其发送到事件队列上(这里是exosip 的事件队列),并通过e-ctl 管道通知应用进行处理。应用需要发送数据时,流程如下图所示:
此时,应用调用exosip 提供的辅助函数(上图中虚线示意此关系),构造osip 事件,将 其添加到osip 的事务队列上。同时,应用通过s-ctl 管道通知exosip 后台任务执行状态机。 在exosip 执行状态机的过程中,sip 消息会被发送到网络另一端的终端。
8
7、linphone与exosip2的关系分析
7.1 linphone功能模块说明
Liblinphone 核心引擎实现了linphone 所有的功能函数,而且能够方便的添加音频和视频的呼叫功能。Liblinphone 也提供高层的API,用来初始化,接收或者终止呼叫。Liblinphone 依赖于下面三个组件: 1 Mediastreamer2
这是一个支持多种平台的轻量级的流技术引擎,主要适合于开发语音和视频电话应用程序。该引擎主要为 linphone 的多媒体流的收发,包括语音和视频的捕获、编码解码以及渲染。 2 ortp2
ortp 是一个RTP 库。为基于RTP 协议的媒体流传输提供支持。通过mediastream2 编码的数据就是使用ortp 库发送到网络的另一端。 3 eXosip2
Exosip2 为sip 协议的实现。这部分实际上是由exosip2 和osip2 两个库共同完成的。使用sip 协议完成路由、媒体协商以及会话的建立和管理,为直接的媒体流的传输提供基础。
通话双方在通信前使用exosip 进行会话协商。Exosip 后台任务完成数据的接收和发送,并通过事件队列通知linphone 底层的状态变化。filter 的构建在会话协商成功建立后就顺带完成了,并且ticker 任务也跑起来了。此时按照 filter graphics 构建的通道,音视频流不断的从硬件设备上读取,并经过编码压缩送给RTP 会话,之后送到对端,对端到达的音视频流也经过RTP 会话接收送到解码解压缩filter,还原出原始的音视频流交给硬件设备播放。媒体数据在这两路流中源源不断的流动,完成了双方的可视通话。上层linphone 的core 任务也不断的对底层进行迭代检查。所做的基本工作如下:对于sip 协议部分,core 一直等待从事件队列上拿事件。这些事件是exosip 任务在处理sip 消息过程中添加到事件队列上的。每当得到新的事件后,core 就从应用层的角度出发,进行处理。对于视频流:基本上只处理rtcp 数据包到达的事件。stream 上也有一个事件队列,用于保存该流上的相关事件。对于rtcp 数据包事件,core 也只处理sr 类型rtcp 包,即发送端报告,得到jitter 和包丢失率。如果设置了自适应比特率,则调用相关接口进行处理。此过程不断进行,直到当前事件上的包处理完。对于音频流,检查流是否还是活动的。通过比较RTP stats 中接收的数据包数目是否发生变化,如果在超时时间到达后,接收的数据量还没有发生变化,则认为音频没有响应。
7.2 linphone中sal模块完成对exosip的封装
Sal 模块其实应该是最重要的,最核心的模块了。该模块对exosip 进行了简单的封装,间接的对osip 模块进行了封装,使用该模块的接口可以完成sip 协议的处理以及媒体描述的处理。
Sal.c 文件主要是对一些sal 相关的结构体的操作,包括SalMediaDescription 和sal_op。处理包括创建这些结构体的实例,获取或者设置其中的一些操作域。
Sal_exosip2_sdp.c 基于osip 库提供的sdp 相关操作的接口,在sal 层实现将其与sal 相关的结构体关联起来操作。比如根据SalMediaDescription 结构体信息将其转换为sdp 结构体,或者反之。
9
Sal_exosip2_presence.c 包括了对in 和out 的subscribe 的操作。Text 数据的发送(基于osip 和exosip)。
Sal_exosip2.c sip 这块比较重要的封装。包含了对sal_op 结构体的创建和基本操作。对exosip 重要结构体的封装,包括初始化和释放。包含了对sal 结构体的创建和基本操作的封装,更重要的是包含了对sal 和sal_op,sal_media_desc,sal_stream_desc 这些上层结构体与底层osip_message,
sip_message,sdp_message 等数据结构之间数据的转换和共享,以及对底层相关接口的调用。这种调用主要包括跟据上层结构体中包含的信息设置底层结构体,并调用底层接口完成具体功能,以及根据底层结构体得到的数据设置上层结构体的相关信息。一个基本的描述就是:sal 作为signal abstract layer 包含了上层所主要理解的交互信息,这些信息对于理解电话操作而言已经足够了,在底层,选择了osip 和exosip 来支持这项操作。所以实际上来说,可以用其他支持sip 的库的接口来替代现有的,保留sal 层接口的功能定义。在linphone 中,虽然大部分使用了sal 层的封装来完成sip 交互过程,但是也调用osip 和exosip 库本身的其他接口,所以这层封装主要还是再次简化协议层的处理,使得功能更具体,而不是更单一。
几个关键数据结构之间的关系:
Sal 一个基本的结构体,通过这个结构体可以搜寻到上层所需的所有sip 协议相关的信息。具体的call,register 等信息保存在sal_op 这个结构体中,多个实体通过链表串接起来,挂在sal 上。Sal_op包含了sal_op_base 结构体,这个结构体保存了一些通用的不变的信息,对多个实体而言,比如路由信息,本地媒体信息,远端媒体信息等。其root 指针由返回指向了sal 这个基础,所以通过sal_op可以找到sal。另外,在媒体信息中包含了所有流的信息,所有这些类似一个树的组织结构,sal 类似树根,通过它可以找到所有的枝叶及其上的信息。
7.3 linphone初始化过程中对sip协议栈的初始化
7.3.1 调用 sal_init 进行sip 协议栈的初始化。该过程将返回一个sal 结构体。 Exosip 全局结构体的创建以及初始化。需要注意,在这里相当于有三层封装调用:一层为sal 层的封装调用,一层为exosip 层的封装调用,最底层为osip 层的基本调用。另外需要注意的是在这里没有创建exosip 任务,而是在后面的读取并配置sip 配置信息时才创建 exosip 任务,并监听特定端口。
将lc->sal 的up 指针指回linphone core 全局结构体 设置sal 上的回调函数,这些回调函数在对应的sip 协议处理完后用于调用来处理外层有关call与media 流的一些处理。如果配置文件中没有设置sip 会话的过期时间,则在这时将其设置为200将所有sip setup 配置串联到registered_sip_setups 全局链表上
7.3.2 读取配置文件中有关sip 协议的相关信息,并以此来配置linphone 的sip 模块。 配置是否在发送数字时使用sip info 信息。 配置是否在发送数字时使用rfc2833 信息 配置是否使用ipv6
配置sip 的传输端口信息。指定是使用随机值还是知名5060 端口 将端口信息设置到linphone core 中,并启动sip 监听。这样,当sip 协议数据到达时即可被处理。
首先调用sal_listen_port 启动监听端口。在这里,协议层被选择和设置,一般情况下都是udp,这里为eXtl_udp。之后创建并启动_eXosip_thread 任务,该任务处理sip 协议数据
10
的接收,协议的处理,状态机的处理,数据的发送等。即几乎所有与sip 协议有关的处理都会在该任务中处理完。最后保存用户代理信息。获取配置文件中的联系人信息,如果联系人为空,或者配置文件中联系人信息不为空,但在将其设置为主联系人信息时出错(比如格式错误),则基于环境变量中的host 和user 信息创建主联系人,否则将配置文件中的联系人信息设置到sip_conf 结构体的联系人上。如果配置文件中设置了猜测主机名,则将该配置设置到linphone core 的sipconfigure 结构体上配置incoming call 的超时时间,如果超过超时时间没有answer 则terminate 该call
读取并配置代理信息,所有的代理者信息都被添加到sip configure 的代理者链表上 读取并配置默认代理者信息。默认代理者会从所有代理者中挑选,根据配置文件,然后放到linphone core 结构体的default_proxy 上读取并配置认证信息。首先从配置文件中读取usrname,userid,password,ha1,realm等信息,并根据这些信息创建一个新的认证信息结构体,将其添加到linphone core 的auth_info 链表上。同时,查找所有处于pending 状态的待认证事件,如果linphone core 中能找到一致的认证信息结构体,则对其进行认证。 根据配置文件对sip_conf 的其他变量进行设置
再续。。。。。。。。。。。。。。
11
的接收,协议的处理,状态机的处理,数据的发送等。即几乎所有与sip 协议有关的处理都会在该任务中处理完。最后保存用户代理信息。获取配置文件中的联系人信息,如果联系人为空,或者配置文件中联系人信息不为空,但在将其设置为主联系人信息时出错(比如格式错误),则基于环境变量中的host 和user 信息创建主联系人,否则将配置文件中的联系人信息设置到sip_conf 结构体的联系人上。如果配置文件中设置了猜测主机名,则将该配置设置到linphone core 的sipconfigure 结构体上配置incoming call 的超时时间,如果超过超时时间没有answer 则terminate 该call
读取并配置代理信息,所有的代理者信息都被添加到sip configure 的代理者链表上 读取并配置默认代理者信息。默认代理者会从所有代理者中挑选,根据配置文件,然后放到linphone core 结构体的default_proxy 上读取并配置认证信息。首先从配置文件中读取usrname,userid,password,ha1,realm等信息,并根据这些信息创建一个新的认证信息结构体,将其添加到linphone core 的auth_info 链表上。同时,查找所有处于pending 状态的待认证事件,如果linphone core 中能找到一致的认证信息结构体,则对其进行认证。 根据配置文件对sip_conf 的其他变量进行设置