初始化是四步,顺序不能换——而其中一步失败时程序不崩,只是「谁也不来」。 四步加起来是百微秒量级,所以「启动慢」从来不是 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 秒实验
- 把
listen_addr的端口改成 5060,看能不能绑上(可能被占用)。 - 连续调两次
listen_addr绑不同端口,看第二次成不成功。 - 把
osip_trace_enable放到 init 之前调,看会怎样。
常见误区
- 忽略
listen_addr的返回值。 失败时程序不崩、只是没人来,表现是「注册一直超时」, 很容易误判成网络问题。 - 以为 init 慢导致启动慢。 init 是亚微秒级,启动慢要去查 DNS、网卡枚举、日志磁盘。
- 生产环境常开最高级日志。 量大且有敏感信息,还可能被死循环写满磁盘。要按天切、限总量。
自测三问(点开对答案)
1. 四步里哪一步失败时「程序不崩」但业务不通? —— **`eXosip_listen_addr`**。它只返回错误码,不终止程序,于是「谁也连不上」,表现是注册超时。 2. 为什么「不 init 直接 register」有时也能跑? —— **未定义行为**:某些实现里全局指针恰好为零,第一次调用时补上了初始化。 这种「偶然正确」换版本就崩。 3. 哪些步骤的返回值可以不查?判据是什么? —— **影响业务就必须查,只影响观感的可以放过**。`set_user_agent` 与 `trace_enable` 属于后者。下一节讲收发——所有结果都从事件循环里出来。
- ☐ (本节完,下一节 09.02.07 oSIP收发消息机制)