PJSIP开发人员提供了大量的极有价值的文档资料供大家使用。
PJMEDIA配备了相当不错的文档。
TURN是一个中继通信协议,通过使用中继,并结合ICE,提供了高效的最低代价的通信路径。PJNATH中TURN的实现,符合draft-ietf-behave-turn-14草案。
ICE是一个发现两个端点之间的通信路径协议。PJNATH中ICE的实现符合draft-ietf-mmusic-ice-19.txt草案
如图1.1展示了PJSIP框架的各模块,可以看出从上到下,Application(pjsua)模块可调用下层所有的模块,也即是PJSUA处于最高层,其整合了下层模块的全部功能以
这也就是为什么我们基本的操作都在PJSUA这里进行。是因为通过PJSUA,我们就能很方便的深入到其他模块中。接着Application模块往下就是PJSUA_lib层,要让应用层(
PJSUA)能更好的调用,当然得有个封装好的库,这个库就是PJSUA_LIB库,称为高层用户代理库,集合SIP,Media以及NAT穿越,所以也就有了往下的PJMEDIA-CODEC和PJMEDIA(负责SDP协商媒体编码和媒体传输),PJNATH(解决NAT穿越),PJSUA-UA(提供SIP用户代理库),PJSIP-SIMPLE(实现presence和及时消息),PJSIP(核协议栈,SIP协议),PJLIB-UTIL(提供有用的工具函数)以及PJLIB(每个功能根据其所在的层次以及负责的功能提供丰富的接口)等模块。
从实现上来看,最上层为应用层,该层将在Android SDK的框架内,采用Java语言来实现;第二层为JNI层,SIP协议栈有很多种实现,其中,采用C语言的SIP协议栈在效
、速度、系统占用方面有着超越其他库(如Java协议栈)的优势,因此,该方案将在第三层采用纯C语言实现的PJSIP协议栈。为了让Java应用层能调用协议栈层,在两层之
间需要一个衔接的桥梁,这就是JNI层。最后一层是驱动层,这部分一般是由手机厂商来实现的,此处将不做重点介绍。
SIP协议栈直接关系到整个系统的质量与效率,许多开源项目基本上都是采用纯C语言开发的PJSIP库。该库采用C语言开发,且源码开放,在兼容性与效率上有明显优势,不仅体积小(完整的SIP封装也不过150 KB),同时还实现了一个内存池,使得安全系数与运行效率大为提高
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协议实时传输加密后的数据,进行通话。最后的过程是挂起或销毁呼叫。
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用户为“[email protected]. 1.100”,“300@192.168.1.100”。本文中也使用这种方式来测试通信。
代码:simple_pjsua.c
/**
* simple_pjsua.c
*
* This is a very simple but fully featured SIP user agent, with the
* following capabilities:
* - SIP registration
* - Making and receiving call
* - Audio/media to sound device.
*
* Usage:
* - To make outgoing call, start simple_pjsua with the URL of remote
* destination to contact.
* E.g.:
* simpleua sip:user@remote
*
* - Incoming calls will automatically be answered with 200.
*
* This program will quit once it has completed a single call.
*/
#include
#define THIS_FILE "APP"
#define SIP_DOMAIN "example.com"
#define SIP_USER "alice"
#define SIP_PASSWD "secret"
/* Callback called by the library upon receiving incoming call */
static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id,
pjsip_rx_data *rdata)
{
pjsua_call_info ci;
PJ_UNUSED_ARG(acc_id);
PJ_UNUSED_ARG(rdata);
pjsua_call_get_info(call_id, &ci);
PJ_LOG(3,(THIS_FILE, "Incoming call from %.*s!!",
(int)ci.remote_info.slen,
ci.remote_info.ptr));
/* Automatically answer incoming calls with 200/OK */
pjsua_call_answer(call_id, 200, NULL, NULL);
}
/* Callback called by the library when call's state has changed */
static void on_call_state(pjsua_call_id call_id, pjsip_event *e)
{
pjsua_call_info ci;
PJ_UNUSED_ARG(e);
pjsua_call_get_info(call_id, &ci);
PJ_LOG(3,(THIS_FILE, "Call %d state=%.*s", call_id,
(int)ci.state_text.slen,
ci.state_text.ptr));
}
/* Callback called by the library when call's media state has changed */
static void on_call_media_state(pjsua_call_id call_id)
{
pjsua_call_info ci;
pjsua_call_get_info(call_id, &ci);
if (ci.media_status == PJSUA_CALL_MEDIA_ACTIVE) {
// When media is active, connect call to sound device.
pjsua_conf_connect(ci.conf_slot, 0);
pjsua_conf_connect(0, ci.conf_slot);
}
}
/* Display error and exit application */
static void error_exit(const char *title, pj_status_t status)
{
pjsua_perror(THIS_FILE, title, status);
pjsua_destroy();
exit(1);
}
/*
* main()
*
* argv[1] may contain URL to call.
*/
int main(int argc, char *argv[])
{
pjsua_acc_id acc_id;
pj_status_t status;
// 创建PJSIP
/* Create pjsua first! */
status = pjsua_create();
if (status != PJ_SUCCESS) error_exit("Error in pjsua_create()", status);
// 校验被叫SIP地址是否正确
/* If argument is specified, it's got to be a valid SIP URL */
if (argc > 1) {
status = pjsua_verify_url(argv[1]);
if (status != PJ_SUCCESS) error_exit("Invalid URL in argv", status);
}
// 初始化PJSUA,设置回调函数
/* Init pjsua */
{
pjsua_config cfg;
pjsua_logging_config log_cfg;
pjsua_config_default(&cfg);
cfg.cb.on_incoming_call = &on_incoming_call;
cfg.cb.on_call_media_state = &on_call_media_state;
cfg.cb.on_call_state = &on_call_state;
pjsua_logging_config_default(&log_cfg);
log_cfg.console_level = 4;
status = pjsua_init(&cfg, &log_cfg, NULL);
if (status != PJ_SUCCESS) error_exit("Error in pjsua_init()", status);
}
// 创建PJSIP的传输端口
/* Add UDP transport. */
{
pjsua_transport_config cfg;
pjsua_transport_config_default(&cfg);
cfg.port = 5060;
status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &cfg, NULL);
if (status != PJ_SUCCESS) error_exit("Error creating transport", status);
}
// 启动PJSIP
/* Initialization is done, now start pjsua */
status = pjsua_start();
if (status != PJ_SUCCESS) error_exit("Error starting pjsua", status);
// 设置SIP用户帐号
/* Register to SIP server by creating SIP account. */
{
pjsua_acc_config cfg;
pjsua_acc_config_default(&cfg);
cfg.id = pj_str("sip:" SIP_USER "@" SIP_DOMAIN);
cfg.reg_uri = pj_str("sip:" SIP_DOMAIN);
cfg.cred_count = 1;
cfg.cred_info[0].realm = pj_str(SIP_DOMAIN);
cfg.cred_info[0].scheme = pj_str("digest");
cfg.cred_info[0].username = pj_str(SIP_USER);
cfg.cred_info[0].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD;
cfg.cred_info[0].data = pj_str(SIP_PASSWD);
status = pjsua_acc_add(&cfg, PJ_TRUE, &acc_id);
if (status != PJ_SUCCESS) error_exit("Error adding account", status);
}
// 发起一个呼叫
/* If URL is specified, make call to the URL. */
if (argc > 1) {
pj_str_t uri = pj_str(argv[1]);
status = pjsua_call_make_call(acc_id, &uri, 0, NULL, NULL, NULL);
if (status != PJ_SUCCESS) error_exit("Error making call", status);
}
// 循环等待
/* Wait until user press "q" to quit. */
for (;;) {
char option[10];
puts("Press 'h' to hangup all calls, 'q' to quit");
if (fgets(option, sizeof(option), stdin) == NULL) {
puts("EOF while reading stdin, will quit now..");
break;
}
if (option[0] == 'q')
break;
if (option[0] == 'h')
pjsua_call_hangup_all();
}
/* Destroy pjsua */
pjsua_destroy();
return 0;
}
simple_pjsua.c的main函数主要流程:
这里可以分析一下它的代码及流程图: