📹 第九章 SIP及eXosip开源库应用
📄 一句话概括

osip_message_t 就是把一条报文拆成一堆字段的结构体—— 起始行三个词、每个头一个字段、正文一个 body。 但有三类头是链表不是单个指针,把它们当单个用就会只拿到第一个。

📝 本节和前面几节的关系

09.02.04 跑通了流程,这一节打开库的内部看看。 09.01.01 拆过报文的五段结构,osip_message_t 就是那五段的 C 语言表示。

一、字段与报文的对应

字段 来自哪 类型
sip_method 起始行第一个词 char *
req_uri 起始行第二个词 osip_uri_t *
sip_version 起始行第三个词 char *
call_id Call-ID 头 char *
cseq CSeq 头 osip_cseq_t *
from From 头 osip_from_t *
to To 头 osip_to_t *
body 空行之后 osip_list_t *

第三列才是重点:不是所有字段都是 char *。 from、to、cseq 是结构体指针,因为它们本身还带子字段(from 里有 URI 和 tag)。 所以「取 From 里的 tag」要写两层:msg->from->tag,不是一个字符串操作。 这一点和「用正则从报文里抠」完全不同,刚接触库的人常常在这里卡住。 第四行那个 call_id 是纯字符串, 因为它没有子结构——这种「什么用指针、什么用字符串」的区分,看结构体定义最清楚。

二、三类头是链表

头 结构体里是 为什么
Via osip_list_t * 多跳,每跳一个
Contact osip_list_t * 可以列多个地址
Route osip_list_t * 路由链

这三类都必须用 osip_list_* 系列函数遍历,不能当下标数组用。 第一行那个「多跳」是 SIP 的转发模型决定的:一个请求经过几个代理,就有几个 Via 头, 应答要靠它们原路返回。 所以 Via 的顺序不能乱——代理加自己的 Via 在最上面,往回走时逐层弹出。 把 Contact 当单个指针用是最常见的 bug:遇到多 Contact 的报文只拿到第一个, 而现场恰恰是「设备有多个地址」的时候才需要看第二个。

三、headers 是兜底链表

结构体里没有专门字段的头,全挂在 headers 里:

头 有专门字段吗 在哪
Call-ID 有 msg->call_id
Expires 有 msg->expires
厂家私有头 没有 msg->headers

第三行那种情况在实际项目里很常见:海康、大华的设备会加自己的 X-… 头, oSIP 不认识它们,但也不会丢——按原始文本挂在 headers 链表里。 所以「取私有头」的做法是遍历 headers,逐个比对名字; 而不是指望有一个 msg->x_hik_xxx 字段。 这个设计很聪明: 它让「不认识的头」也能被转发——代理服务器不需要理解每个头,只要原样传递就行。

四、回收也有讲究

oSIP 的所有对象都有配对的 _free:

对象 怎么建 怎么放
osip_message_t osip_message_init osip_message_free
osip_transaction_t 库内部建 osip_transaction_free
osip_event_t eXosip_event_wait eXosip_event_free

第二行那个别自己建:事务是库根据报文自动创建的,你只管在合适的时候释放。 自己 osip_transaction_init 会打乱库的状态机,表现是「同一个请求被处理两次」。 第三行那个是最常漏的:eXosip_event_wait 返回的事件必须 free, 漏了就是每条报文漏几十字节。 跑一天几万条报文,就是几 MB。

五、为什么不用正则抠报文

有人觉得「反正都是文本,我正则匹配不是更快」:

| 做法 | 好处 | 问题 | |---|---| | 正则抠 | 不用学结构体 | 多值头会漏 | | 结构体 | 多值、子字段都对 | 要学 API |

第一行的「快」只在「取一个单值头」时成立, 遇到 Contact: <a>, <b> 这种就麻烦——正则要处理引号、尖括号、逗号转义,越写越复杂。 第二行那个「要学 API」是真实成本, 但只学一次;正则写错一次就要查很久,而且错得很隐蔽——取到一个看起来正常但其实是拼接错的值。 所以工程上的结论是:单值头可以用字符串处理省事,多值与带子结构的头一定要走结构体。

六、动手 Demo:一条报文落到哪个字段

Demo 拿一条真 INVITE,把每个字段对应到报文的哪一部分。 十个字段里有四个是结构体指针,这一列要看清。

msg = S.build_req("INVITE", cseq=1, body=S.sdp())
m = S.parse(msg)
print(m["method"], m["start"])
print(S.hget(m, "Call-ID"), S.hget(m, "CSeq"))
print(len(m["body"].encode()), "字节")

实跑输出:

--------------------------------------------------------------------
一条报文落到哪个字段
--------------------------------------------------------------------
**`osip_message_t` 是 oSIP 的核心结构**,**它把一条报文拆成一堆字段。**
**下面拿真报文对照一遍**:


  字段               来自哪                本例
  -----------------------------------------------------------------
  `sip_method`       起始行第一个词        `INVITE`
  `req_uri`          起始行第二个词        `sip:…@3402000000`
  `sip_version`      起始行第三个词        `SIP/2.0`
  `call_id`          `Call-ID` 头          `1c383cd30b7c…`
  `cseq`             `CSeq` 头             `1 INVITE`
  `from`             `From` 头             含 tag
  `to`               `To` 头               **应答里才有 tag**
  `contact`          `Contact` 头          是链表不是单个
  `content_type`     `Content-Type` 头     `application/sdp`
  `body`             空行之后              `211` 字节(LF 归一后)

**十行覆盖了核心字段,但真实结构比这大得多**——
**`osip_message_t` 里还有二十多个头字段(`accept`、`allow`、`authorization`、`expires`……)。**
**而且注意第八行那个「是链表不是单个」**:**`Contact`、`Via`、`Route` 这三类头可以出现多次**,
**所以它们在结构体里是 `osip_list_t *` 而不是单个指针。**
**这个差别在代码里很致命**:**把 `contact` 当单个指针用,遇到多 Contact 的报文就只拿到第一个。**

**还有一条要记**:
**`headers` 是「兜底链表」**——**凡是结构体里没有专门字段的头,都按原始文本挂在 `headers` 里。**
**这带来一个便利**:**厂家私有头(比如海康的 `X-…`)不会丢**,**只是要自己遍历 `headers` 去找。**

表里那个 body 显示 211 字节, 而 09.01.07 说 SDP 是 222 字节——差的那 11 字节就是每行少一个回车符。 这个差异不是 bug,是解析时的换行归一:我们把 \r\n 统一成了 \n 才去拆行, 所以正文里每行少 1 个字节。 这一点在真实代码里要小心: 如果你按 Content-Length 去读字节、又自己做了换行转换,长度就对不上了。 正确做法是「先按字节长度截取,再转换」。 表里第八行那个「是链表不是单个」也值得再看一眼:contact 在结构体里是链表, 所以「取第一个 Contact」要写遍历的第一项,不能直接解引用。

30 秒实验

  1. 用 grep -n 'osip_list_t' include/osip2/*.h 数数有多少个头是链表。
  2. 在 osip_message.h 里找 expires 字段,看它是字符串还是整数。
  3. 构造一条带两个 Contact 的报文,看解析后链表长度是几。

常见误区

  1. 以为所有字段都是 char *。 from、to、cseq、req_uri 都是结构体指针, 取 tag 要写 msg->from->tag。
  2. 把 Contact 当单个指针用。 它是链表,多 Contact 时只拿到第一个, 而这恰恰是「设备有多个地址」时最需要的信息。
  3. 自己建 osip_transaction_t。 事务由库按报文自动创建,手动建会打乱状态机, 表现是「同一请求处理两次」。
自测三问(点开对答案) 1. `From` 头为什么不能用字符串直接取? —— **它是 `osip_from_t *`,里面还有 URI 与 tag 两个子字段**。 取 tag 要写 `msg->from->tag`,不是字符串操作。 2. 厂家私有头在结构体里存哪? —— **`msg->headers` 这个兜底链表**。 结构体没有专门字段的头,都按原始文本挂在这里,遍历比对名字就能取。 3. 为什么正文 211 字节和 SDP 的 222 字节对不上? —— **解析时做了换行归一**,`\r\n` 变 `\n`,11 行各少 1 字节。 真实代码里要先按 `Content-Length` 截字节再转换。

下一节讲初始化——四步,每一步的失败表现都不一样。

相关笔记