最近在学习SIP,刚刚看了一个很好的开源工程 pjsip 很强大!
RFC 文档参见:http://www.ietf.org/rfc/rfc3261.txt
SIP 也是类似 HTTP 的一个协议集合,在网上搜索了一下相关的信息,摘录如下:
SIP消息的第一行包含消息的类型和所使用的SIP版本(2.0)。在请求中,这一行还包含一个叫做SIP URI的地址。这代表消息的目的地。
这个例子说明了如何使用请求消息INVITE、ACK和BYE,以及200 OK响应消息。SIP中还存在许多其他消息。下面给出一些请求:
消息 | 用法 |
---|---|
INVITE | 呼叫一个用户代理,传送一次呼叫。 |
ACK | 确认呼叫。 |
BYE | 终止呼叫。 |
CANCEL | 终止还未OK的呼叫。 |
REGISTER | 提供一项注册服务,带有一个联系地址和可以用来代替的别名。例如,在前面的例子中,地址sip:UAA@example.com就是sip:UserA@10.20.30.40的别名。然后,注册服务器example.com就可以把呼叫转发给地址10.20.30.40。 |
OPTIONS | 询问一个用户代理的“能力”(例如,该用户代理能够识别的消息和编码)。 |
现在给出一些经常使用的响应消息:
消息 | 用法 |
---|---|
100 Trying | 消息已收到,但是最终用户代理尚未进行处理。请等待。 |
180 Ringing | 最终用户代理已经收到消息,正在提示用户。请等待。 |
200 OK | 最终用户已经接受消息。 |
301 Moved Permanently & 302 Moved Temporarily | 用户代理的地址已经改变,新的永久或临时地址位于Contact字段中。 |
400 Bad Request | 普通错误消息。客户端不能识别消息。 |
401 Unauthorized & 407 Proxy Authentication Required | 请使用证书重试。 |
404 Not Found | 要联系的用户不存在或尚未注册。 |
408 Request Timeout | 另一方没有响应。这意味着SIP消息永远不会OK。所有重试都将被丢弃。这并不意味着电话响太长时间(电话可以永远响铃)。 |
消息使用类似的头字段类型。下面给出其中的一些:
头字段 | 用法 |
---|---|
From | SIP请求的发送者。 |
To | SIP请求的接受者。这通常与SIP URI相同(可以是一个“别名”或一个实际地址)。 |
Contact | 用户代理的实际地址。 |
Call-ID | 这并不是呼叫者的电话号码。它惟一地代表两个用户代理之间的完整呼叫或对话。所有相关的SIP消息都使用同一个Call-ID。例如,当一个用户代理收到一条BYE消息,根据Call-ID,它就知道要挂断哪次呼叫。 |
CSeq | 消息的顺序编号。这在一次对话或一个Call-ID中是惟一的。这用于区别新的消息和“重试消息”。当一条初始消息没有及时OK时,重试就会进行,并会定时发送。 |
Content-Type | 消息内payload的MIME类型。 |
Content-Length | payload的大小,以字节为单位。信封和payload之间由一空行隔开。 |
还有一些与消息路由选择功能相关的头字段,如:Via、Route和Record-Route。许多头字段提供像Accept、User-Agent和Supported这样的功能。其他头字段则提供像Authorization、Privacy和WWW-Authenticate这样的安全性功能。还有很多其他的头字段存在。此外,这些字段中许多都有缩写语法(比如,From = f,To = t,等等)。
SIP 在移动开发中,经常让人提及的是 pjsip, 官方网站: http://www.pjsip.org/
他的优缺点网上和官方自有介绍。
我的学习计划就是对这个开源软件进行分析及与 SIP 协议进行对比,从而加大对 SIP 这块领域的了解。
下载源码 pjproject-1.10
通过网络了解如下信息:
PJSIP 这个库实际上是几个部分组成的:
通过 build 目录来看,这绝对是一个很强的跨平台平项目。还有对 Symbian 平台专门编译的目录。
按照以往的经验和上面的信息来讲 pjlib 是对跨平台进行支持的一个库!
而我的源码分析计划也就有了先后的顺序:
PJLIB
还有二个目录 third_party 与 pjsip-apps 以后再看分析情况进行了解。
随意打开 PJLIB 目录,能够很好的看到目录的作用,
bin | 生成的二进制文件 |
build | 编译脚本 |
docs | 文档目录 |
include | 公开头文件目录 |
lib | 生成的库目录 |
src | 源码目录 |
打开 PJLIB 的 include 目录
pjlib.h 非常明显的头文件,做为基础库的头文件,是我们分析的起点
- #ifndef
__PJLIB_H__ - #define
__PJLIB_H__ -
-
-
- #include
// 网络相关 - #include
// 网络 - #include
// 数组 - #include
// 断言 - #include
// 字符相关 - #include
// 错误支持 - #include
// 异常支持 - #include
// 管道缓冲? - #include
// IO 支持 - #include
// IO 支持 - #include
// GUID Globally Unique Identifier(全球唯一标识符) - #include
// Hash - #include
// IO 队列 - #include
// 网络 - #include
// 数据处理 LIST - #include
// 锁 - #include
// 日志 - #include
// 数学函数 - #include
// 操作系统相关 - #include
// 内存池 - #include
// 内存管里相关 - #include
// 随机数 - #include
// 红黑树,数据处理相关 - #include
// 网络相关 - #include
// 网络 - #include
// 网络多路选择 - #include
// 网络加密相关 - #include
// 字符串处理 - #include
// 定时器 - #include
// 宽字符,这在移动设备中经常使用 -
- #include
-
- #endif
包含一很多 pj 目录下的头文件,我们尝试揣测一下作者的意图,既然是基础包,并且是跨平台台的项目,哪么这个库肯定是提供了一些系统最为基本相关的包装。而我们知道对跨平台进行处理的首先就是对基本数据类型进行包装。
types.h
-
- typedef
int pj_int32_t; -
-
- typedef
unsigned int pj_uint32_t; -
-
- typedef
short pj_int16_t; -
-
- typedef
unsigned short pj_uint16_t; -
-
- typedef
signed char pj_int8_t; -
-
- typedef
unsigned char pj_uint8_t; -
-
- typedef
size_t pj_size_t; -
-
- typedef
long pj_ssize_t; -
-
- typedef
int pj_status_t; -
-
- typedef
int pj_bool_t; -
- ......
-
-
- #define
PJ_SUCCESS 0 -
-
- #define
PJ_TRUE 1 -
-
- #define
PJ_FALSE 0 -
- ......
基本的数据类型都定义了,再看上面的头文件,一个操作系统所支持的一些特征都包装好,后续的开发,如果是用 pjlib 支持的,只要 pjlib 支持的平台,应该上面的 pjsip 就是支持这个平台,同时,这个库的性能也是上面的开发程序的性能的瓶颈。
我们打开官网的文档,官方对此库的说明如下:
It's Open Source!
Extreme Portability
Small in Size
Big in Performance
No Dynamic Memory Allocations
Operating System Abstraction
Low-Level Network I/O
Timer Management
Various Data Structures
Logging Facility
Random and GUID Generation
由此可见这个库确实是一个不错的库,可以单独拿出来使用,也可以向自己的新平台进行移值,当成为基础库去使用。具体的每一个功能不一一分析了!
古语日:
适合而此,目是SIP不能因为 PJLIB 的精彩而忽略SIP.
这个库看名字应该是对 PJLIB 进行扩展的一个包。同样打开 include 看看,发现一个头文件 pjlib-util.h
- #ifndef
__PJLIB_UTIL_H__ - #define
__PJLIB_UTIL_H__ -
-
-
-
- #include
- #include
-
-
- #include
-
-
- #include
- #include
- #include
- #include
- #include
- #include
-
-
- #include
- #include
- #include
-
-
- #include
-
-
- #include
-
-
- #include
-
-
- #include
-
-
- #include
-
-
- #include
-
- #endif
大概有如下内容:
- DNS 查询
- DNS 服务器
- 文本扫描
- XML 处理
- STUN
在网上查是 (Simple Traversal of UDP over NATs,NAT 的UDP简单穿越)是一种网络协议 - PCAP
- HTTP
由此可见,这个包提供了更为高级的一些工具库,特别是针对网络方面,进行了相关封装。暂时不深入去了解,不过可以确定的是,如果可行我们其它地方需要用网络实现,特别是跨平台的项目,使用这里面的 XML HTTP 都是一件不错的选择!
以后有需要再详细分析该部份代码
PJSIP 我们要分析的重点
引用官方的一张图:
PJLIB 做为与系统打交道底层支持库
PJLIB-UTIL 是封装的一些工具库
PJNATH
PJMEDIA 完成 SDP 与媒体栈的封装
而上面的又需要第三方库支持,就是源码目录中的 third_party
PJSIP 则简单很多只需要 PJLIB-UTIL 与 PJLIB 的支持
在上面又包装一层 PJSIP-SIMPLE 增加个人信息与IM的支持。
然后整合 PJSIP-SIMPLE 和 PJMEDIA 包装成 PJSIP-UA
最后包装成 PJSUA 来完成对应用程序的支持。
PJLIB 也开放给应用程序。
这就是PJSIP 团队想表达出整个设计,及用户使用的一个分层图表。同时也包含了各个部件在整个PJSIP库中的作用。
PJSIP URI Parser
在 SIP 中,用户 URI 描述是最基本的一项信息,也有相应的格式,先看看这个是怎样解析的吧。
实际上在好的工程下面都有完备的单元测试代码,同样在 PJSIP 中也是能找到相关的测试代码,我们可以打开 pjproject-1.10\pjsip\src\test\uri_test.c
在代码中举例出不下 38 种 URI 格式,用以程序分析,可见仅仅一个字符串解析,这个工程也是做的相当严谨。
在本工程中可以有如下收获,对以后的分析也是很重要的。
测试框架
为了让测试更加简单,更加有效,PJSIP中使用了一个很好的测试框架。本测试的入口函数是 uri_test 是为 test.c 中调用
在
在本测试中使用了查表法进行测试用例的扩充。
在测试后还进行了简单的统计。
最后输入出信息是以 HTML 格式输出,更容易查阅。
查表法用例
查表法的使用,更方便测试用例的扩充。
与平台无关性编程
以 PJLIB 为基础,进行了平台无关性编程,能够让测试在不同平台上有效快速的运行。
pjsip_sip_uri 格式
通过不同的测试用例,也学到了最为关键的 pjsip_sip_uri 结构体的格式。
-
- typedef
struct pjsip_sip_uri - {
-
pjsip_uri_vptr *vptr; -
pj_str_t user; -
pj_str_t passwd; -
pj_str_t host; -
int port; -
pj_str_t user_param; -
pj_str_t method_param; -
pj_str_t transport_param; -
int ttl_param; -
int lr_param; -
pj_str_t maddr_param; -
pjsip_param other_param; -
pjsip_param header_param; - }
pjsip_sip_uri;
注释写的很清楚,但是还是要说一下,并且拿出代码中测试的最长的一个 URI 来对照
sip:user:password@localhost:5060;transport=tcp;user=ip;ttl=255;lr=1;maddr=127.0.0.1;method=ACK;pickup=hurry;message=I am sorry?Subject=Hello There&Server=SIP Server
user | user |
passwd | password |
host | localhost |
port | 5060 |
user_param | ip |
method_param | ACK |
transport_param | tcp |
ttl_param | 255 |
lr_param | 1 |
maddr_param | 127.0.0.1 |
other_param | pickup, |
header_param | Subject, Server |
由此可见一些规律
- sip: 打头
- 用分号进行分割
- 除第一段后每段都可以用 key=value 的样式进行填充
- 有些 key 是固定的,其它的 key 可以扩展,会放到 other_param 中
- 最后可以带 ? 附加更多的信息,此信息会放到 header_param 中
相信我们知道了这些,这个 URI 的分析也就很清楚了,同进更明白 SIP 中的 URI 可以如何的扩充自己的参数。
最后留下一个疑问?
他的结构体中第一行 pjsip_uri_vptr *vptr; 是用来做什么呢, 后面分析!
C语言中的面向对像
上文的 uri 分析中我们发现结构体总是有一个指针,注释写的很明白,是一个虚拟函数指针,和C++中的虚表中却是十分想像。
不此在 PJSIP 中十分常见,在 Android 源码中,还有其它的开源工程也是很常见的。好的,写个简单工程来解释疑惑。
-
-
- #include
- #include
- #include
-
- //
定义一个动物类,有二个方法 - struct
v_animal_ptr { -
const char* (*get_name)(); // 获取名字 -
void (*shout)(int volume); // 喊叫 - };
-
- typedef
struct v_animal_ptr animal_vptr; -
- typedef
const char* (*GET_NAME)(); - typedef
void (*SHOUT)(int volume); -
- //
定义一个动物类 - struct
animal { -
animal_vptr *vptr; - };
-
- static
const char* animal_get_name(const void *class) - {
-
return (*((struct animal*)class)->vptr->get_name)(); - }
-
- static
void animal_shout(const void *class, int volume) - {
-
return (*((struct animal*)class)->vptr->shout)(volume); - }
-
-
- //
定义一个猪类,有一个高度值。 - struct
pig { -
animal_vptr *vptr; -
int height; - };
-
- static
const char* pig_get_name() { -
return "i am pig"; - };
-
- static
void pig_shout(int volume) { -
printf("heng heng %d\n", volume); - };
-
- //
定义猪的实现方法 - static
animal_vptr pig_vptr = - {
-
(GET_NAME) &pig_get_name, -
(SHOUT) &pig_shout, - };
-
- //
定义一个狗类,有一个颜色值。 - struct
dog { -
animal_vptr *vptr; -
int color; - };
-
- static
const char* dog_get_name() { -
return "i am dog"; - };
-
- static
void dog_shout(int volume) { -
printf("wang wang %d\n", volume); - };
-
- static
animal_vptr dog_vptr = - {
-
(GET_NAME) &dog_get_name, -
(SHOUT) &dog_shout, - };
-
- static
struct pig* init_pig() { -
struct pig* anim = (struct pig*) malloc(sizeof(struct pig)); -
anim->vptr = &pig_vptr; -
anim->height = 10; -
-
return anim; - }
-
- static
struct dog* init_dog() { -
struct dog* anim = (struct dog*) malloc(sizeof(struct dog)); -
anim->vptr = &dog_vptr; -
anim->color = 255; -
-
return anim; - }
-
- int
main() - {
-
struct animal *anim = NULL; -
struct pig *pig = NULL; -
struct dog *dog = NULL; -
-
// pig 子类 -
pig = init_pig(); -
printf("name=%s\n", animal_get_name(pig)); -
-
printf("------------------------------------\n"); -
-
// dog 子类 -
dog = init_dog(); -
printf("name=%s\n", animal_get_name(dog)); -
-
printf("------------------------------------\n"); -
-
// 转向 动物父类进行操作 -
anim = (struct animal *)pig; -
printf("name=%s\n", animal_get_name(anim)); -
animal_shout(anim, 20); -
-
printf("------------------------------------\n"); -
-
// 转向 动物父类进行操作 -
anim = (struct animal *)dog; -
printf("name=%s\n", animal_get_name(anim)); -
animal_shout(anim, 100); -
-
return 0; - }
运行结果:
- $
./test - name=i
am pig - ------------------------------------
- name=i
am dog - ------------------------------------
- name=i
am pig - heng
heng 20 - ------------------------------------
- name=i
am dog - wang
wang 100
上面的例子有点长,可以仔细看看,如果不关心实现细节,可以看一下 main 函数
确实可以模拟 C++ 类继承的方式进行操作了!
从开源工程确实能够了解很多有意思的东西!
下图是来至 http://zhangwenjie.net
可惜在写此文的时候不能够访问!
从上图中可以看到 END POINT 是一个中心,有着非常重要的作用。而 PJSIP 官网中:
https://trac.pjsip.org/repos/doxygen/pjsip/html/modules.htm
也说到是一个 Very Core 部份,并且放到第一位!
我们必需要了解一下这部份!
官网是这样说的:
Detailed Description
SIP Endpoint instance (pjsip_endpoint) can be viewed as the master/owner of all SIP objects in an application. It performs the following roles:
- it manages the allocation/deallocation of memory pools for all objects.
- it manages listeners and transports, and how they are used by transactions.
- it receives incoming messages from transport layer and automatically dispatches them to the correct transaction (or create a new one).
- it has a single instance of timer management (timer heap).
- it manages modules, which is the primary means of extending the library.
- it provides single polling function for all objects and distributes events.
- it automatically handles incoming requests which can not be handled by existing modules (such as when incoming request has unsupported method).
- and so on..
看来是非常重要的一个模块!