PJSIP 开发者手册阅读笔记(一)—— 整体设计与模块

Published: 2020-04-28

Tags: pjsip


官方开发者文档:https://www.pjsip.org/release/0.5.4/PJSIP-Dev-Guide.pdf

我花了几天的碎片时间过了两遍官方的英文开发者手册,据说它是 PJSIP 开发者的终极指南,大致来说本文是官方文档的翻译,整个内容顺序也是按照手册走的,不过有些涉及到函数说明的细节,及我认为暂时不那么重要的内容可能被忽略,同时一些重要的知识点及概念,我也参考了其它文章进行了补充。

如果你时间充裕,过一遍官方文档是个不错的主意,如果时间比较赶,看我的整理相信也会对你有所帮助。

SIP 协议先行知识

pjsip

在了解PJSIP之前,至少要先了解下SIP中一些概念。上图是一次 Session 会话,包含两个 Dialog 对话,共四个 Transaction 事务。

Messages(消息) 消息是在服务器和客户端之间交换的独立文本,有两种类型的消息,分别是请求(Requests)和响应(Responses)

Transaction(事务) 事务发生于客户端和服务器端之间,包含从客户端发出请求给服务器,到服务器响应给客户端的最终消息(non-1xx message)之间的所有消息。如果请求是一个 Invite 消息,并且最终的响应是一个 non-2xx 消息,那么该事务包含一个 Ack 响应消息。如果服务器的响应是一个 2xx 消息,那么随后的 ACK 是一个单独的事务。

Dialog(对话) 对话是两个UA(user agent)之间持续一段时间的端到端(peer-to-peer)的SIP 关系。一个对话由一个 Call-ID,一个 Local Tag 和一个 Remote Tag 来标识。Dialog 的建立是收到 UAS 的响应(To Tag)时开始建立的。收到 1xx 响应时建立 Dialog 叫做早期对话(early dialog),收到 2xx 的应答开始才是真正的 dialog 建立。

Session(会话) 是媒体交换之后才建立的。具体而言就是通过 offer/answer 方式交换 sdp 的媒体。 session 的建立可以使INVITE-200 也可以是 200-ACK。这要看媒体的交换发生的时间。 具体来说,INVITE 中的消息体用 sdp 语言来描述自己可处理的媒体类型,200OK 中带回 UAS 端可处理的媒体类型。这个时候媒体交换就算是完成了。也就是 session 建立起来了。

(一)PJSIP 简介,通用设计

PJSIP 通讯示例图,<==> 表示消息的传递路径。

pjsip

PJSIP 类示例图

pjsip

可以看到,Endpoint 是非常重要的类,是 PJSIP 的核心。

它提供以下功能:内存分配与释放、为其它模块提供时间管理、拥有传输管理器、能有控制消息解析和打印、拥有一个 PJLIB 的 ioqueue 实例来处理网络事件、提供有线程安全策略、管理 PJSIP 模块(PJSIP 模块是从内部获取消息,进行自定义处理解析的主要方法,它还负责接收传输管理器发来的 SIP 消息并将其分发给模块)。

(二)模块

模块框架是 PJSIP 应用中组件间分配SIP消息的主要手段。PJSIP 中的所有组件,包括事务层(transaction layer)和对话层(dialog layer)都是作为模块实现的。如果没有模块,核心堆栈(pjsip_endpoint 和 transport) 就不知道如何处理SIP消息。

该模块框架基于一个简单但功能强大的接口抽象。对于传入消息,Endpoint(pjsip_endpoint)从优先级最高的模块开始,将消息分发给所有模块,直到其中一个模块说它已经处理了消息。对于传出消息,Endpoint 在将传出消息传输到网络之前调用模块,以便允许模块根据需要对消息进行最后的修改。

pjsip

上图为模块的声明

PS:前一阵儿我在网上找基于 PJSUA2 处理注册的代码时,看到代码中用到 pjsip_module 创建模块并被注册使用,当时还在想这些函数、结构体都是从哪里找来的,PJSUA2 文档可没有提及,原来是在PJSIP开发者手册中进行了介绍。

这个结构体中的所有回调函数都是可选的,没指定的话,会被认为返回了 success

前四个回调函数,loadstartstopunload,它们在 endpoint 管理模块的时候被触发,下图展示了模块状态的生命周期。

pjsip

on_rx_request()on_rx_response() 回调函数是模块从 endpoint 获取 SIP 消息的主要方法,它的返回值非常重要,如果返回值非零(如:true 条件),从语义上来讲,代表模块已经处理了消息,endpoint 将停止将消息继续分发给其它模块。

on_tx_request()on_tx_response() 回调函数在传输管理器(transport manager)将消息发送出去之前调用,给一些模块处理消息的机会(如:信令压缩、消息签名),这两个函数的返回值必须为 PJ_SUCCESS,否则消息将被取消发送。

on_tsx_state() 回调函数用来在事务状态(transaction state)发生更改时接收通知,如收到消息、传输消息、时间事件、传输错误。

PS:这里举一个 PJSUA2 注册模块处理注册消息的例子

作为服务端,当 SIP Client 发送注册请求时,代码中定义的 default_mod_on_rx_request() 回调函数能够获取到这个请求,通过头部信息进行判断,当其为 pjsip_register_method 类型时表明其为注册请求,进行相应处理,然后手动调用 pjsip_endpt_create_response()pjsip_endpt_send_response2() 函数回复消息,return PJ_TRUE,告诉 endpoint 我已经处理了这个请求,endpoint 就不会再将消息发送给其它模块了。

// 模块用于处理外部发起请求的回调
static pj_bool_t default_mod_on_rx_request(pjsip_rx_data *rdata)
{
  pjsip_tx_data *tdata;
  if (pjsip_method_cmp(&rdata->msg_info.msg->line.req.method, &pjsip_register_method) == 0)
  {
    // 注册验证的具体实现
    // process_registrar(rdata);
    PJ_LOG(3, (THIS_FILE, "===> REGISTRATAR <===\n\n\n"));
    pjsip_endpt_create_response(pjsua_get_pjsip_endpt(), rdata, 200, NULL, &tdata);
    pjsip_endpt_send_response2(pjsua_get_pjsip_endpt(), rdata, tdata, NULL, NULL);
    return PJ_TRUE;
  }
  pjsip_endpt_create_response(pjsua_get_pjsip_endpt(), rdata, 200, NULL, &tdata);
  pjsip_endpt_send_response2(pjsua_get_pjsip_endpt(), rdata, tdata, NULL, NULL);
  return PJ_TRUE;
}

// 创建一个模块
static pjsip_module mod_default_handler =
{
  NULL, NULL,       /* prev, next.    */
  { (char *)"mod-default-handler", 19 },  /* Name.    */
  -1,         /* Id     */
  PJSIP_MOD_PRIORITY_APPLICATION + 99,  /* Priority         */
  NULL,       /* load()   */
  NULL,       /* start()    */
  NULL,       /* stop()   */
  NULL,       /* unload()   */
  &default_mod_on_rx_request,   /* on_rx_request()  */
  NULL,       /* on_rx_response() */
  NULL,       /* on_tx_request() */
  NULL,       /* on_tx_response() */
  NULL,       /* on_tsx_state() */
};


// 注册模块
pjsip_endpt_register_module(pjsua_get_pjsip_endpt(), &mod_default_handler);

模块优先级(Module Priorities)

优先级越高的模块,当消息来到时,它的 on_rx_request()on_rx_response() 就越先被调用,并且 on_tx_request()on_tx_response() 越后被调用。数值越小,优先级越高。

pjsip

  • PJSIP_MOD_PRIORITY_TRANSPORT_LAYER 被传输管理器使用,其默认优先级最高,目前仅用于控制消息传输。

  • PJSIP_MOD_PRIORITY_TSX_LAYER 被事务层使用,事务层接收所有与事务有关的消息。

  • PJSIP_MOD_PRIORITY_UA_PROXY_LAYER 被 UA 层(如:dialog framework)或 proxy 层使用,UA 层接收属于 Dialog 的所有传入消息(传出消息同理)。
  • PJSIP_MOD_PRIORITY_DIALOG_USAGE 被 dialog usage 使用,当前 PJSIP 实现了两种类型的 dialog usage:INVITE 会话(invite session)与事件订阅(event subscription,包括REFER订阅),这个优先级被用来接收属于这两种类型的消息。
  • PJSIP_MOD_PRIORITY_APPLICATION 典型的应用层模块优先级最低,因为它需要使用事务,dialogs 及 dialog usage 提供的功能。

关于 Dialog 与 Dialog Usage

Dialog 是通用的对话框,Dialog usage 基于 Dialog,为特定的会话(Invite,事件订阅)提供特别的对话框,拥有更多的针对性功能,消息经由 Dialog,如果它为 Invite 消息或事件订阅,那么它还会被发送给 Dialog usage 进行处理。

The INVITE session uses the Base Dialog framework to manage the underlying dialog, and is one type of usages that can use a particular dialog instance (other usages are event subscription, discussed in SIP Event Notification (RFC 3265) Module). The INVITE session manages the life-time of the session, and also manages the SDP negotiation.

INVITE 会话使用基础的 Dialog 来管理基本对话框,并且它属于 dialog usages 的一种,可以使用特定的对话框(particular dialog)实例,另一个 usage 是事件订阅,在 RFC 3265 文档中定义。INVITE 会话管理会话的生命周期与SDP协商。

引用自:https://www.pjsip.org/pjsip/docs/html/group__PJSIP__INV.htm

关于传入传出数据的进一步补充。

对于外部发送过来的消息,传输层接收消息后,会将消息保存到 pjsip_rx_data 结构体。Endpoint 向模块分发消息时从优先级高的模块开始发送,如果有模块处理了消息,那么 Endpoint 将停止向其它模块分发消息,但是这个处理的模块可以在自己的模块内部再将消息分发,比如调用其它模块的 on_rx_request()on_rx_response() 函数进行触发。

对于传出消息,它保存在 pjsip_tx_data 结构体,除了消息本身,还包含内存池、连续缓冲区和传输信息等。在传输之前,会按照模块优先级相反的顺序依次调用,如果想要模块能够记录真实的传输层发往电缆的数据,那么设置模块优先级高于传输模块(优先级数字小于传输模块),再次强调,如果模块没能返回 PJ_SUCCESS,消息不会被发送,并将错误代码返回给 pjsip_transport_send() 调用者。

一个特别的回调函数 on_tsx_state() 在一些特定的事务状态改变时被触发,此回调是惟一的,事务状态可能因为一些与消息无关的事件而更改。

事件与回调触发

事件 on_rx_request() 或者 on_rx_response() on_tsx_state()
接收新的请求或响应 被调用 被调用
收到重传的请求或响应 仅当优先级高于事务层时被调用 不被调用
发送新的请求或响应 不被调用 被调用
重传请求或响应 不被调用 不被调用
事务超时 不被调用 被调用
其他事务失败(如:DNS查询超时,传输失败) 不被调用 被调用

消息回调示例图

1)Incoming 消息,不在事务及对话表中

pjsip

  1. 传输管理器收到消息,解析后传递给 Endpoint。
  2. Endpoint 开始分发消息,根据优先级,第一个收到消息的模块是事务层,事务层通过事务表判断这个消息是不是属于已有事务,此例没有找到对应的事务。
  3. Endpoint 继续分发消息给其它模块,到了 User agent 用户代理层(Dialog 对话框就处于这一层级)。
  4. User agent 在 dialog 表中查找,也有没找到这条消息对应的 dialog 对话框。
  5. Endpoint 继续按照优先级分发消息到已注册的模块,直到发送到应用层,应用层处理这条消息(例如:对消息进行无状态响应,创建UAS事务,代理请求或者创建 dialog 等)。

2)Incoming 消息,属于一个事务

pjsip

  1. 传输管理器收到消息,解析后传递给 Endpoint。
  2. Endpoint 开始分发消息,根据优先级,第一个收到消息的模块是事务层,事务层通过事务表判断这个消息是不是属于已存在事务,此例找到匹配的事务。
  3. 因为事务回调返回了 PJ_TRUE,Endpoint 不再继续分发消息到其它模块。
  4. 事务层模块处理这个响应(例如:更新 FSM),如果是重传消息,处理将会停在这步,否则事务将把把消息发送给事务用户(Transaction user,TU),事务用户可以是一个对话(dialog)或 应用(application)。
  5. 如果事务用户是 dialog,那么这个 dialog 将处理消息,并把消息发送给 dialog 用户(dialog user,DU,如:application)。
  6. 如果收到的消息改变了事务的状态,事务将把最新的状态通知给它的事务用户。
  7. 如果事务用户是 dialog,dialog 将进一步将状态变更通知给它的 dialog 用户(如:application)。

3)Incoming 消息,不属于已存在事务,但存在于Dialog对话表

pjsip

  1. 传输管理器收到消息,解析后传递给 Endpoint。
  2. Endpoint 开始分发消息,根据优先级,第一个收到消息的模块是事务层,事务层通过事务表判断这个消息是不是属于已有事务,此例没有找到对应的事务。
  3. Endpoint 继续分发消息给其它模块,直到 User agent 用户代理层。
  4. UA 层模块在表中发现匹配的 dialog 对话框。
  5. UA 层模块将消息发送到对应的 dialog 对话框。
  6. Dialog 会为到来的请求创建事务,然后通过 on_rx_request() 和 on_tsx_state() 分发请求给 dialog usage(注:如果请求属于 Invite 或 event subscription)。

模块管理

模块被 Endpoint 管理,应用程序必须手动将每个模块注册到 Endpoint,这样堆栈能够识别它。模块注册和卸载使用的函数是 pjsip_endpt_register_module()pj_status_t pjsip_endpt_unregister_module()

声明模块功能

模块可以为 Endpoint 增加新的功能,当前 Endpoint 支持自动设置 Allow,Supported,Accept 头域,这些头域将被适时地自动添加到发出的请求或响应中。

模块可以通过调用 pjsip_endpt_add_capability() 函数来添加新功能声明到指定头域,可选的有 Allow,Supported,Accept,通过 pjsip_endpt_get_capability() 获取指定头域的功能支持。

参考

  1. PJSIP Developer's Guide (PDF)
  2. smllyy的学习时间轴
  3. sip里面的几个概念,会话 事务