Sofia-SIP辅助文档十六 - Sofia SIP用户代理库 - "msg" - 消息解析模块

时间:2021-09-08 17:00:07

http://sofia-sip.sourceforge.net/refdocs/msg/index.html翻译自官网的这张网页。

MIME介绍参考:http://blog.csdn.net/cxm_hwj/article/details/6690058。


模块元信息

模块包括可以处理基于文本协议(SIP,HTTP和RTSP)的消息和头的解析器和函数。还提供MIME头和MIME multipart消息的解析,MIME对这些协议来说比较常见。

联系人:
Pekka Pessi < Pekka.Pessi@nokia-email.address.hidden>
状态:
Sofia SIP Core library
许可:
LGPL
贡献者:

msg模块的内容

msg模块包括如下一些公共头文件:

除了这些接口,解析器文档描述了扩展解析器使其支持新的头所需具备的功能,或者扩展解析器使其支持新的协议所需具备的功能。为解析器增加一些新的头,或者扩充已有头的定义都是可行的。组成这些解析器的头文件是:

解析器,消息和头

Sofia msg模块包含基于文本的类似RFC 822消息的接口。当前,定义了三个解析器:SIP,HTTP和MIME。

每个头对应的C结构体在<sofia-sip/msg_types.h>头文件中给出,或者在协议特定的头文件中给出。这些协议特定的头文件有<sofia-sip/sip.h>,<sofia-sip/http.h>和<sofia-sip/msg_mime.h>。对每个头,都会有头类结构体,一些标准函数,以及在tag列表中的tag。

作为惯例,所有SIP头的标识符都以sip开始,所有的宏都以SIP开始。HTTP也有着同样的惯例:使用http前缀。和他们相关的MIME头和函数在msg模块中定义,他们使用msg前缀。SIP或HTTP头使用的结构体在<sofia-sip/msg_types.h>头文件中定义,为每个特定的协议都会有一个单独的typdedef,,例如Accept头会被定义多次:

typedef struct msg_accept_s sip_accept_t;
typedef struct msg_accept_s http_accept_t;

对协议NS的头X来说,有这么一些类型、函数、宏和头类:

  • ns_X_t是存储解析过后的头的结构体
  • ns_hclass_t ns_X_class[]包含头X的头类
  • NS_X_INIT()初始化ns_X_t的一个静态实例
  • ns_X_init()初始化ns_X_t的一个动态实例
  • ns_is_X()测试一个头对象是否是头X的的实例
  • ns_X_make()通过给定的字串创建一个头X对象
  • ns_X_format()通过解析给定的printf()列表创建一个头X对象
  • ns_X_dup()复制(深度靠背)头X对象
  • ns_X_copy()拷贝头X
  • NSTAG_X()用来在tag列表内包含ns_X_t list
  • NSTAG_X_STR()用来包括包含值头的字串在tag list

头tags申明和函数原型可以从不同的类型定义里导入。例如,SIP头相关的tags在<sofia-sip/sip_tag.h>头文件里定义,特定头的函数在<sofia-sip/sip_header.h>头文件中定义。

解析文本消息

Sofia文本解析器遵循递归下降原则 。换句话说,它是一个自顶向下递归下降语法树的程序(所有的语法树在顶端有一个根,树向下生长)。

像SIP,HTTP和其他类似的协议,这样一个解析器是非常高效的。解析器可以在基于每个记号的不同的格式中选择,因为协议语法是被小心设计的,以便只需要很少的向前扫描。通过标准API可以很容易扩展递归下降解析器,而不是例如用Bison生成的LALR解析器。

抽象消息模块msg包含一个高层解析器引擎,这个引擎驱动了解析过程和为每个头调用特定协议的解析器。因为RFC 822风格的消息之间没有低一层级的分帧,因此解析器可以认为任何收到的数据,无论是UDP包还是TCP包,是一个字节流。特定协议的解析器控制着一个字节流如何被分成不同的消息,或者只是一个单独的消息。

解析器分解流成fragments,然后将fragment提供给适合的解析器。一个fragment是消息的一个片段:第一行、每个头、头区域和消息体之间的空行(拿HTTP来说,消息体由被称为chunks的多个fragment组成)。

解析器以从字节流中分离出第一行作为开始(例如,请求或状态行),然后将第一行提供给适合的解析器。第一行后跟着的就是消息头。解析器通过抽取头来继续解析过程,并且将头内容提供给解析器。消息结构基于解析的结果。当遇到一个空行,指出头区域结束,控制被转给特定协议的解析器。特定协议函数将负责从字节流中抽取消息体。

解析过程完成后,控制将交给上层(典型情况下是协议状态机)。解析器持续处理流,并且将消息提供给协议引擎直到到达流的尾端。

Sofia-SIP辅助文档十六 - Sofia SIP用户代理库 - "msg" - 消息解析模块

Separating byte stream to messages

当解析过程结束,第一行、所有头、分隔符和消息体都在他们自己的fragment结构体内。所有的fragment形成一个被称为双向链接的列表,如上图所示。消息的内存缓冲区,fragment链,以及其他一些东西都由一个通用的消息类型msg_t所维护,类型在<sofia-sip/msg.h>头文件中定义。msg_t的内部结构只在msg模块内部所知,它对其他模块来说是透明的。

msg解析器引擎还驱动着递归处理,为每个fragment调用编码方法以便整个消息可以被正确编码。

作为一个C结构体的消息头

只是分离出头和消息体通常来说还是不够的。当一个头包含了结构化数据,头的内容应该转换成能被C程序方便使用的格式。为了达到这个目的,消息解析器需要针对每个消息的特定解析函数。这个解析函数分离头内容形成各个语义上有意义的segment,存入头特定的结构体。

解析器引擎从消息中分离出fragment后将fragment的内容提供给解析函数。解析器引擎选择正确的头类,要么像第一行数据这种明显的头类,要么依据头名称作为哈希键从哈希表内查找头类。头类中包含了解析器函数的指针。解析器还有一类特殊的头类:错误和无法识别的头类,这些头的名称无法被解析器所识别。

例如,Accept头有如下的句法:

   Accept         = "Accept" ":" #( media-range [ accept-params ] )

   media-range    = ( "*" "/" "*"
                    | ( type "/" "*" )
                    | ( type "/" subtype ) ) *( ";" parameter )

   accept-params  = ";" "q" "=" qvalue *( accept-extension )

   accept-extension = ";" token [ "=" ( token | quoted-string ) ]

当一个Accept头被解析,头解析器函数(msg_accept_d())将分理出type,subtype,以及列表内的每个参数。解析结果被赋值给msg_accept_t结构体对象。结构体的定义如下:

typedef struct msg_accept_s
{
  msg_common_t        ac_common[1]; //< Common fragment info
  msg_accept_t       *ac_next;      //< Pointer to next Accept header
  char const         *ac_type;      //< Pointer to type/subtype
  char const         *ac_subtype;   //< Points after first slash in type
  msg_param_t const  *ac_params;    //< List of parameters
  msg_param_t         ac_q;         //< Value of q parameter
}
msg_accept_t;

包含type的字串被放入ac_type成员中,斜杠后的subtype被放入ac_subtype成员中,accept-params列表(与媒体相关的参数)被放入ac_params数组中。如果q参数出现了,指向qvalue的指针被赋给ac_q成员。

在头结构的开始处有两个样板成员。ac_common[1]包含对所有消息的fragment共同的信息。ac_next是指向下一个名称相同的头成员的指针。有时一个消息包含多个Accept头,或者多个逗号分隔的头成员在同一行中出现。

用C结构体表示一个消息

只是用包含所有头的列表形式表示一个消息是不够的。程序员需要一个在消息层面更加便捷的方式访问特定的头。例如,直接访问Accept头,而不是遍历所有的头检查他们的名称是否是Accept。通过一个特定消息的结构体,提供了消息的结构化视图。通常,它的类型是msg_pub_t(它提供了针对消息的公开视图)。特定协议的类型是sip_thttp_tmsg_multipart_t,分别对应的是SIP、HTTP和MIME。

因此,一个消息用两个对象来表示,第一个对象(msg_t)对msg模块来说是私有的、对应用程序开发人员而言是透明的,第二个对象(sip_thttp_tmsg_multipart_t)是公开的、特定协议的结构体,可任意存取。

注意:
应用程序开发人员可以从msg_t对象通过函数msg_public()获得一个指向特定协议结构体的指针。 msg_public()函数接受一个协议tag,一个知名的标识符,作为它的参数。SIP、HTTP和MIME已经定义了一个在msg_public()基础上的包裹器,例如,一个sip_t结构体可以通过sip_object()函数(宏)获得。

列举一个例子,sip_t结构体可以如下:

typedef struct sip_s {
  msg_common_t        sip_common[1];    // Used with recursive inclusion
  msg_pub_t          *sip_next;         // Ditto
  void               *sip_user;         // Application data
  unsigned            sip_size;         // Size of the structure with
                                        // extension headers
  int                 sip_flags;        // Parser flags

  sip_error_t        *sip_error;        // Erroneous headers

  sip_request_t      *sip_request;      // Request line
  sip_status_t       *sip_status;       // Status line

  sip_via_t          *sip_via;          // Via (v)
  sip_route_t        *sip_route;        // Route
  sip_record_route_t *sip_record_route; // Record-Route
  sip_max_forwards_t *sip_max_forwards; // Max-Forwards
  ...
} sip_t;

正如你看到的那样,公开的sip_t结构体包含一个common头成员,同样可以在头结构体的开始处找到。sip_size指出结构体的大小 - 应用程序可以扩展解析器,sip_t结构体的大小会超过原始大小。sip_flags包括很多解析和打印过程中使用的标志。他们的文档在<sofia-sip/msg.h>头文件内。这些模板成员之后是指向各类消息元素和头的指针。

解析处理结果

开始展示一个简单的消息是如何被解析和展示给应用程序的。选择了SIP请求消息BYE,只包括了必须的信息:

BYE sip:joe@example.com SIP/2.0
Via: SIP/2.0/UDP sip.example.edu;branch=d7f2e89c.74a72681
Via: SIP/2.0/UDP pc104.example.edu:1030;maddr=110.213.33.19
From: Bobby Brown <sip:bb@example-email.address.hidden>;tag=77241a86
To: Joe User <sip:joe@example-email.address.hidden>;tag=7c6276c1
Call-ID: 4c4e911b@pc104.example.edu
CSeq: 2

下图展示了上条BYE消息解析后的布局图:

Sofia-SIP辅助文档十六 - Sofia SIP用户代理库 - "msg" - 消息解析模块

BYE message and its representation in C

最左边的方框展示的是消息的msg_t类型。紧挨最左边方框的方框展示的是sip_t结构体,它包含了很多指向头对象的指针。接下来的列包含了很多头对象。每个头对象对应一个消息fragment。最右边的方框展示的是当消息收到后的I/O缓冲区。请注意,I/O缓冲区也许并不是连续的,有可能是由很多块分散的内存区域构成。

msg_t对象有一个指向公开消息结构体m_object的指针,指向fragment双向链表m_frags的指针,和指向I/O缓冲区m_buffer的指针。公共消息结构体对象包括指向各个类型头的列表。如果一种类型的头出现了多个(例子消息中出现了两个Via头),那么这些头将会放入一个单向列表。

每一个fragment对象都有指向前一个和后一个fragment对象的指针。fragment对象同样还有指向I/O缓冲区内相应数据的指针和大小。

fragment链的目的是保留所有头的原始次序。如果第三个Via头在CSeg头后出现,那么fragment链内它会出现在CSeq头的后面,但在头列表内会出现在第二个Via头之后。

例子:解析一个完整的消息

下面的代码片断是解析一个完整消息的例子。The parsing process is more hairy when there is stream to be parsed.

msg_t *parse_memory(msg_mclass_t const *mclass, char const data[], int len)
{
  msg_t *msg;
  int m;
  msg_iovec_t iovec[2] = {{ 0 }};

  msg  = msg_create(mclass, 0);
  if (!msg)
    return NULL;

  m = msg_recv_iovec(msg, iovec, 2, n, 1);
  if (m < 0) {
    msg_destroy(msg);
    return NULL;
  }
  assert(m <= 2);
  assert(iovec[0].mv_len + iovec[1].mv_len == n);

  memcpy(iovec[0].mv_base, data, n = iovec[0].mv_len);
  if (m == 2)
    memcpy(iovec[1].mv_base + n, data + n, iovec[1].mv_len);

  msg_recv_commit(msg, iovec[0].mv_len + iovec[1].mv_len, 1);

  m = msg_extract(msg);
  assert(m != 0);
  if (m < 0) {
     msg_destroy(msg);
     return NULL;
  }
  return msg;
}

让我们一步一步来过一遍这个函数。首先,有一个data指针,以及它的大小(字节数)大小。初始化一个I/O vector,解析器用来表示消息。

msg_t *parse_memory(msg_mclass_t const *mclass, char const data[], int len)
{
  msg_t *msg;
  int m;
  msg_iovec_t iovec[2] = {{ 0 }};

消息类mclass (一个解析器驱动器对象,msg_mclass_t)被用来表示一个特殊的特定协议解析器实例。消息对象被创建时作为参数提供给msg_create()函数:

  msg  = msg_create(mclass, 0);
  if (!msg)
    return NULL;

接着用msg_recv_iovec()函数获取一个存放数据的内存缓冲区。内存缓冲区通常是一块连续的内存区域,但有时也有可能由两个不同的区域组成。在这里,iovec被用来传递缓冲区。iovec 可直接提供给众多的系统I/O调用。

  m = msg_recv_iovec(msg, iovec, 2, n, 1);
  if (m < 0) {
    msg_destroy(msg);
    return NULL;
  }

当针对一个完整的消息第一次调用msg_recv_iovec()函数这些断言总是对的:

  assert(m >= 1 && m <= 2);
  assert(iovec[0].mv_len + iovec[1].mv_len == n);

接着,拷贝数据给I/O vector,确定拷贝的数据已经给消息对象了。稍早用msg_recv_iovec()函数申请了空间给数据,现在调用msg_recv_commit()函数指出合法数据已经拷贝到缓冲区了。提供给msg_recv_commit()函数的最后一个参数说明已经到达了流的尾端没有更多数据了。

  memcpy(iovec[0].mv_base, data, n = iovec[0].mv_len);
  if (m == 2)
    memcpy(iovec[1].mv_base + n, data + n, iovec[1].mv_len);

  msg_recv_commit(msg, iovec[0].mv_len + iovec[1].mv_len, 1);

接着调用msg_extract()函数,它负责解析消息。返回-1指出是一个致命的解析错误。返回0说明消息头不完整。如果一个完整的消息被解析了,将返回一个正整数。消息数据不可能是不完整的,因此调用msg_recv_commit()函数时就已向解析器指出已经到达了流的尾端。

  m = msg_extract(msg);
  assert(m != 0);
  if (m < 0) {
     msg_destroy(msg);
     return NULL;
  }
  return msg;
}