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 秒实验
- 用
grep -n 'osip_list_t' include/osip2/*.h数数有多少个头是链表。 - 在
osip_message.h里找expires字段,看它是字符串还是整数。 - 构造一条带两个
Contact的报文,看解析后链表长度是几。
常见误区
- 以为所有字段都是
char *。from、to、cseq、req_uri都是结构体指针, 取 tag 要写msg->from->tag。 - 把
Contact当单个指针用。 它是链表,多 Contact 时只拿到第一个, 而这恰恰是「设备有多个地址」时最需要的信息。 - 自己建
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` 截字节再转换。下一节讲初始化——四步,每一步的失败表现都不一样。
- ☐ (本节完,下一节 09.02.06 oSIP的初始化工作)