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

初始化是四步,顺序不能换——而其中一步失败时程序不崩,只是「谁也不来」。 四步加起来是百微秒量级,所以「启动慢」从来不是 init 的问题。

📝 本节和前面几节的关系

09.02.04 的第一个动作就是初始化,这一节把它拆开看。 08.08.03 讲 gSOAP 的 configure 时也强调过「检查返回值」, 这里的问题一样:可选步骤失败不报错。

一、四步

步 函数 失败会怎样
1 eXosip_init 后面全崩
2 eXosip_listen_addr 收不到任何请求
3 eXosip_set_user_agent 只是标识不好看
4 osip_trace_enable 一行日志都没有

第二行是唯一「失败也不崩」的关键步骤:端口被占用、地址写错,它只是返回错误码, 程序继续往下跑,然后「一直没人来」。 所以「启动成功」不等于「能工作」:这四步的返回值必须每个都查。 第三、四行是「不影响功能但影响排障」的两步: 用户代理字符串决定对端日志里怎么显示你;日志不开则出问题时完全瞎。

二、为什么顺序不能换

换法 会怎样
先 listen 后 init 库还没初始化,直接失败
先 set_user_agent 后 init 同上
不 init 直接 register 未定义行为

第三行那个「未定义行为」是最危险的:有些版本的库会「碰巧能跑」, 因为它内部的全局指针恰好是零、初始化逻辑在第一次调用时补上了。 这种「碰巧能跑」比直接崩更坏:它让人以为「顺序无所谓」,然后换个版本或换个编译选项就崩了。 所以「按文档顺序写」这件事不是形式主义,它是在规避「实现相关的偶然正确」。

三、耗时在百微秒量级

四步里只有「建 socket」那一步真的花时间:

步 量级 说明
init 亚微秒 只是内存分配
listen_addr 几十到一百多微秒 建 socket、绑定
set_user_agent 亚微秒 拷个字符串
trace_enable 亚微秒 设个标志

这个量级说明「初始化不慢」——如果现场发现启动要几秒,问题不在 init,而在三个地方: DNS 解析、网卡枚举、或者日志文件写在慢盘上。 第一行那个「只是内存分配」也解释了「为什么 init 不会失败」:除非内存耗尽,它几乎总是成功。 所以 init 返回非零,基本可以断定是「库版本不对」或「参数传错」。

四、日志开不开的差别

这一条在排障时是决定性的:

日志级别 能看到什么
关 什么都没有
低 只报错
高 收发报文全文

第三行那个「收发报文全文」就是 SIP 排障的命根子:没有它,你只能靠抓包; 而现场往往不允许装抓包工具。 但日志开高有两个代价:① 量大(每通电话几十 KB),② 有敏感信息(摘要、密码)。 所以生产环境通常是「平时关、出事临时开」。 如果要做成「可远程开启」, 记得把日志文件按天切、限制总大小——不然一个死循环能把磁盘写满。

五、初始化的返回值清单

四步各自返回什么,代码里要这样检查:

if (eXosip_init() != 0) { /* 直接退出 */ }
if (eXosip_listen_addr(IPPROTO_UDP, NULL, 5060,
                       AF_INET, 0) != 0) {
    /* 端口被占用或地址非法,必须报错 */
}
eXosip_set_user_agent("GB28181-Client/1.0");
osip_trace_enable(TRACE_LEVEL3, NULL);

第一、二行必须判断,后两行可以不判断(失败了也不影响业务)。 这个「哪些必须查、哪些可以不查」的区分很重要: 全都判断会让代码很啰嗦,全都不判断则埋下「一直没人来」的隐患。 判断标准就是第三列那个「失败会怎样」:影响业务就必须查,只影响观感就可以放过。

六、动手 Demo:初始化四步,每步都可能失败

Demo 按四步的顺序跑一遍,每步单独计时。 这是本机等价实现——它量的不是 oSIP 的真实耗时,而是「这四类动作各自的代价」。

def s2():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.bind(("127.0.0.1", 0))
    p = s.getsockname()[1]
    s.close()
    return p

for name, fn in (("init", s1), ("listen_addr", s2), ...):
    t = time.perf_counter()
    r = fn()
    print(name, "%.2f" % ((time.perf_counter() - t) * 1e6), r)

实跑输出:

--------------------------------------------------------------------
初始化四步,每步都可能失败
--------------------------------------------------------------------
**eXosip 的初始化是四步,顺序不能换**:

  步     函数                        失败会怎样
  ------------------------------------------------------
  1      `eXosip_init`               **后面全崩**
  2      `eXosip_listen_addr`        **收不到任何请求**
  3      `eXosip_set_user_agent`     只是标识不好看
  4      `osip_trace_enable`         **一行日志都没有**

**第二行那个最容易忽略**:**`listen_addr` 失败时程序不崩**,
**它只是「谁也不来」**——**表现是「注册一直超时」,而不是「启动报错」。**
**所以「启动成功」不等于「能工作」**:**要显式检查这一步的返回值。**

**下面真跑一遍这四步(本机等价实现),并量每步耗时**:

  步骤                微秒       返回值
  --------------------------------------
  init                0.50       0
  listen_addr         119.60     60484
  set_user_agent      1.00       18
  timestamp_alloc     0.20       1

**四步加起来是百微秒量级**(本机等价实现),**建 socket 那一步最贵**,其余三步几乎不耗时。
**这个量级说明「初始化不慢」**——**如果现场发现启动要几秒,问题不在 init,而在 DNS 或网卡枚举。**
**第四步那个 `osip_trace_enable` 是开日志的**,**开不开对功能没影响**,
**但不开就一行日志都没有。**

输出里 listen_addr 那一步占了绝大部分微秒数, 其余三步都在 1 微秒上下——这个分布和真实 oSIP 是一致的,因为耗时都在系统调用上。 注意这一步的返回值是端口号即 5 位数, 而其余三步返回 0 或小整数——所以「返回值」这一列不能统一按「0 表示成功」理解。 四步加起来是百微秒量级,这个数值得记住: 以后遇到「启动要三秒」,直接排除 init,去查 DNS 和磁盘。 最后一步 trace_enable 在本机实现里返回 1,表示「日志级别已设」; 真实库里它返回 0 表示成功——这种「不同库对成功的约定不同」的情况很常见,查文档最稳。

30 秒实验

  1. 把 listen_addr 的端口改成 5060,看能不能绑上(可能被占用)。
  2. 连续调两次 listen_addr 绑不同端口,看第二次成不成功。
  3. 把 osip_trace_enable 放到 init 之前调,看会怎样。

常见误区

  1. 忽略 listen_addr 的返回值。 失败时程序不崩、只是没人来,表现是「注册一直超时」, 很容易误判成网络问题。
  2. 以为 init 慢导致启动慢。 init 是亚微秒级,启动慢要去查 DNS、网卡枚举、日志磁盘。
  3. 生产环境常开最高级日志。 量大且有敏感信息,还可能被死循环写满磁盘。要按天切、限总量。
自测三问(点开对答案) 1. 四步里哪一步失败时「程序不崩」但业务不通? —— **`eXosip_listen_addr`**。它只返回错误码,不终止程序,于是「谁也连不上」,表现是注册超时。 2. 为什么「不 init 直接 register」有时也能跑? —— **未定义行为**:某些实现里全局指针恰好为零,第一次调用时补上了初始化。 这种「偶然正确」换版本就崩。 3. 哪些步骤的返回值可以不查?判据是什么? —— **影响业务就必须查,只影响观感的可以放过**。`set_user_agent` 与 `trace_enable` 属于后者。

下一节讲收发——所有结果都从事件循环里出来。

相关笔记