基于PJSIP协议栈和Android的VoIP系统设计方案介绍【详解】

基于PJSIP协议栈和Android的VoIP系统设计方案介绍【详解】

  本文提出一种基于PJSIP协议栈的解决方案,通过Android本地开发工具(NDK),实现一个高效、稳定且功能强大的VoIP系统,具有较高的参考和实用价值。

  1 VoIP设计方案

  1.1 设计目标

  本方案所设计的系统包含以下功能:首先,完成用户终端(如手机)中语音数据的采集与编码,并通过RTP(实时传输协议)/RTCP(RTP传输控制协议)进行传输和控制;其次,完成会话的控制,包括会话的注册、发起、维护与结束、注销等;再次,作为一个应用程序,必须实现一个良好的界面,与用户交互;最后,作为一个开放系统,需具有良好的可扩展性。

  1.2 总体设计

  本方案基本上符合Android的NDK框架的开发规范,将系统分为4层,如图1所示。最上层为应用层,该层将在Android SDK的框架内,采用Java语言来实现;第二层为JNI层,SIP协议栈有很多种实现,其中,采用C语言的SIP协议栈在效率、速度、系统占用方面有着超越其他库(如Java协议栈)的优势,因此,该方案将在第三层采用纯C语言实现的PJSIP协议栈。为了让Java应用层能调用协议栈层,在两层之间需要一个衔接的桥梁,这就是JNI层。最后一层是驱动层,这部分一般是由手机厂商来实现的,本文将不做重点介绍。

  2 VoIP的具体实现

  这里将实现一个完整的VoIP系统,包括协议栈的实现、JNI的编写以及上层UI的设计实现等。

  2.1 SIP协议栈及UA

  SIP协议栈直接关系到整个系统的质量与效率,本文将采用纯C语言开发的PJSIP库。该库采用C语言开发,且源码开放,在兼容性与效率上有明显优势,不仅体积小(完整的SIP封装也不过150 KB),同时还实现了一个内存池,使得安全系数与运行效率大为提高,该系统所采用的就是优化后的PJSIP库。

  2.1.1 PJSIP协议栈

  PJSIP协议栈遵循标准的SIP协议,采用分层架构:SIP/SDP消息编码解析层、传输管理层、SIP终端、事务层、会话层以及应用层等。由于SIP协议采用文本消息发送请求和响应,所以首先需要将SIP消息按照巴斯克范式(ABNF)编码和解析,这就是SIP/SDP消息编码解析层所完成的功能。传输管理层用来管理用户代理与服务器之间的请求和相应;SIP终端是PJSIP中转机制的实现,它主要负责管理各个SIP组建,例如像SIP终端实例注册组件,分发消息到事务层、会话层及应用层,回传处理结果,管理定时器、I/O队列等;事务层通过状态机机制管理SIP信令,每一次状态机状态的改变都将触发回调函数;会话层负责会话的发起与响应,一般与应用层结合在一起,用于用户交互,不同的平台有不同的实现,本文使用Andriod的GUI来实现。

  PJSIP是一个高度封装的库,实际上它是通过PJSUA子库来实现应用的。一个完整的PJSUA生命周期,首先需要初始化,通过函数init()来实现。在这个函数中,将创建代理、初始化变量和堆栈,以及创建一个UDP传输并在最后启动代理;第二步将为UA添加用户,如果需要的话,还要向服务器注册用户;当用户添加成功后,此时可以建立一个呼叫连接,发起会话;当会话连接成功后,就可以使用SRTP协议实时传输加密后的数据,进行通话。最后的过程是挂起或销毁呼叫。

  2.1.2 UA原理

  UA(User Agency)是协议栈的具体实现,PJSIP通过封装了的PJSUA来实现,在这一点上,大部分的SIP库都大同小异,在此将介绍UA的工作原理。

  一个典型的UA包含UAC(User Agency Client)和UAS(User Agency Server)两部分。会话由UAC发起。当呼叫发起时,UAC将首先发送“IN-VITE”消息给SIP代理服务器,服务器收到“INVITE”消息后将返回一个应答“200 OK”,并回答“ACK”进行确认,同时通知主叫用户(即会话发起用户)上线通话。如果主叫端(用户端)主动结束会话,UAC将返回“BYE”消息,同时通知服务器;如果用户端收到服务器传来的“BY-E”消息,回答“200”,并结束会话。

  服务器端,UAS收到UAC(用户端)发来的“INVITE”消息,首先从消息中提取出主、被叫对象,然后检查当前是否有空闲信道,若没有则返回“486 BUSY HERE”(即系统忙)消息;接着将检查被叫用户是否在服务区,如果被叫对象不在服务范围,则返回“404 NOT FOUND”(即用户不在服务区);若被叫用户成功上线,则返回“200 OK”,同时准备开始会话。

  SIP协议栈一般使用SIP统一资源定位符(URL)来标识,它根据URL来寻址,如集群用户“200”,“300”分别对应SIP用户为“200@192.168. 1.100”,“300@192.168.1.100”。本文中也使用这种方式来测试通信。

  2.2 JNI的实现

  PJSIP库和Java类连接是通过JNI来实现的,这也是Android NDK的实现机制,JNI是SUN公司推出的用于Java调用其他语言的接口。

  首先需要一个中间类,这个类中主要建立一些方法用于调用C/C++本地函数。它们的类型均为“publicstatic native int”,以便其他的Java类能够调用。

  2.2.1 新建PJSIP类

  为各个待实现的类新建一个包,可以命名为“com.android.voip.pjsip”,在该包中添加该系统相关的一些类,这些类分别为上节中原理各个步骤的实现。这部分仅仅是为C库的调用提供一个接口,因此具体的实现将放在本地C/C++程序中。

  2.2.2 头文件的生成

  C库与Java间还需通过一个后缀为“.h”的头文件来衔接,这个头文件可以手动编写,也可以通过“Javah”来生成,该工具包含在JDK中,是由SUN公司提供的。

  Javah生产的头文件包含一定的规则,例如,本例中,它将生成的函数声明为“Java_com_android_IMSandroid_pjsip_**”的形式,以便在调用C库时能正确识别。

  由于Java中的数据类型与C/C++不尽相同,因此还必须注意参数传递时参数类型的转换。本文所涉及到的Java数据类型有String和int型,Javah生成的头文件中会先定义好需要传递的参数类型以及函数返回类型,例如方法“add_account(String sip_user,Stringsip_dom-ain,String sip_passwd)”,在头文件中将定义函数的形式为“JNIEXPORT jint JNICALL Java_com_android_IMSandroid_pjsip_add_lac-count(JNIEnv*,jclass,jstring,jstring,jstring)”,其中JNIEXPORT为JNI外部函数声明,jint是“jni.h”中定义C语言中整形的对应类型,JNCALL是JNI关键字。比较特殊的是JNIEnv,它是一个指向类型为JNIEnv_的一个特殊JNI数据结构的指针,它的每个元素都指向一个JNI函数的指针,jclass会根据引用Java类的不同而不同,本文中“pjsip.class”是静态类,因此这里的jclass指的是类本身,如果是非静态类则指的是对象。后面几个就是pjsip类需要传递的参数,根据“jni.h”的定义,String类型对应jstring,int对应jint。然而这只是函数申明与类中方法的形式对应,参数的具体传递还需要相应的转化,具体实现将在下一节详细介绍。

  2.2.3 JNI接口函数的实现

  创建了pjsip库类和头文件之后,必须应用一个库接口函数,这部分是pjsip接口的实现,限于篇幅,本文只讲解几个重要函数的实现。

  (1)init函数

  首先是init函数,对应的接口函数为JNICALL Java_com_android_IMSandroid_pjsip_init。该函数在系统初始化时调用,其作用是配置相关参数,并发起一个pjsua应用。它先通过函数“pjsua_create()”创建一个“pjsua”应用,然后通过三个函数“pjsua_config_default

  (&cfg)”,“pjsua_logging_config_default(&log_cfg”),“pjsua_media_config_default(&media_cfg)”配置其相关参数,其中cfg是pjsua的相关参数,主要是状态改变时的回调函数;log_cfg用来配置log级别;media_cfg中包含时钟频率、声道数目等相关参数。

  完成配置之后就可以使用pjsua_init(&cfg,&log_cfg,&media_cfg)将先前配置的参数初始化。在初始化之后,还需为pjsua添加一个udp传输,这一步是通过pjsua_transport_create(PJSIP_TRANSPORT_UDP,&cfg,NULL)来实现的,cfg中包含指定的通讯端口,3GPP建议使用5060。

  需要注意的是,配置完以上参数之后,还需指定SPEEX编码优先级,一般将其设为最大,可以通过函数pisua_codec_set_priority(&-speex_codec_id,255)来实现。所有配置完成之后,就可以发起pjsua,即最后调用pjsua_start()。成功的话,本函数的返回类型为PJ_SUCCESS。

  (2)make_call函数

  另一个很重要的函数是make_call,其在本接口文件中对应的函数为Java_com_android_IMSandroid_pjsip_make_lcall,这个函数一般在发起会话时调用,它与上一个函数在结构上最大的不同在于本函数需要传递一个字符串参数,前面提到,Java与C/C++在参数结构上并不完全相同,因此这里需要将Java传递过来的String类型的参数转化,可以通过“url_ptr=(char*)env->GetStringUTFChars(url,&iscopy)”来实现。env->GetStringUTFChars在“jni.h”中定义,其功能是将jsting类型(Java)的url复制到char*类型(C)的url_ptr中,以此来完成参数类型的转换。

  为了保证传递地址的有效性,还需要使用pjsua_verify_sip_url(url_ptr)验证,这个函数主要验证url_ptr是符合SIP的规则,即是否是一个合法的SIP地址。然而char*型的地址pjsua中还是不能直接使用的,这是因为pjsua重新封装了参数类型,所以最后还需要将其转化成pj_ str_t类型,pjlib提供pj_str()函数可以完成转化。在完成了参数的转化后,调用“pjsua_call_make_call()”,将发起会话。

  (3)hangup函数和pjsua_destroy函数

  这两个函数用来销毁和挂断会话,一般在需结束的时候调用,它们在接口函数中对应Java_com_android_

  IMSandroid_pjsip_hangup和Java_com_android_IMSandroid_pjsip_destroy,由于没有参数传递,也没有其他的调用,因此这两个函数非常简单,基本上直接调用pjsua提供的pjsua_call_hangup_all()和pjsua_destroy()就能实现。pisua中这两个函数将完成内存释放、账户注销等工作。

  (4)add_account函数

  该函数在基本的pjsua中并不是必须的,但如果要使用SIP服务器的话,就必须实现该函数,它在接口函数中对应“Java_com_android_I-MSandroid_pjsip_add_1account”,同“make_call”一样,也需要传递参数,不同的是,它传递三个参数,只是原理大体一样。

  首先它将参数转化后保持到cfg,通过“pjsua_acc_add(&cfg,PJ_TRUE,&ace_id)”将参数添加到pjsua。pjsua将以其中的sip服务器为目的地址,注册会话发起申请,经过一系列的操作之后,与目的地址发起会话。

  2.2.4 主程序与用户界面

  系统的主程序是一个标准的Android应用程序,它使用Java语言开发,符合SDK规范。与一般的SDK程序不同的是,该类中必须使用Syst-em.loadLibrary加载PJSIP库文件。形式如下:

  System.loadLibrary(“pjsip-jni”);

  其中,pjsip-jni就是上节中PJSIP协议栈生成的库。

  主程序中的基本方式均按照上节中的过程,创建并初始化PJSUA;当call按键被触发时发起会话,调用make-call()方法;当用户接受通话时,点击hang或cancel按键,触发hang()或采用destry()方法等。

  用户接口是通过Android SDK来实现的,这部分几乎全都是Java语言,由于UI不是本文的重点,因此只介绍一个简单的界面,实际应用中用户交互是非常重要的。为了实现所需的功能,至少需要一个文本框来提供SIP地址,以及两个按键来控制会话发起和结束。另外,在呼叫与通话过程中,还需要一个页面来显示,这里可以通过对话框来显示

  3 封装与调试

  为了能在Android平台上方便地使用该系统,在实现了PJSIP协议栈、JNI接口以及UI之后,还需将上面所有的模块进行封装。Android SDK提供了一些很有用的工具,如aapt等,由于本文重点不在AndriodSDK,所以可以采用集成开发工具(如集成在Eclipse中的ADT)来封装。在工程libs(如果不存在则新建)目录下新建一个名为armeabi的目录,将上节生成的.so库文件放到该目录下。ADT在封装资源时会自动将该库文件封装到apk文件中,apk是Android操作系统中应用程序的封装形式,在所有android平台中均能使用。

  封装后安装到Android手机、MID或虚拟机中,并发起会话。与开源SIP软件Linphone通信的结果如图2所示。

  4 结语

  通过测试表明,该系统能够对发起并很好地控制SIP信令,该系统由于采用SIP协议,因此与所有采用这一协议的软件均能通信,如Lin-phone,Kphone等,功能测试中表现良好,实现了VoIP的基本需求。同时如果要增加功能,可以在Java类中添加相应的方法并在应用层调用即可,具有一定的可扩展性。

推荐阅读