尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Osip SIP协议栈实战:从源码编译到事务状态机调试
简介本资源是一套基于Osip开源SIP库的完整通信开发示例面向网络协议开发者、VoIP应用初学者及嵌入式通信方向学习者聚焦SIP信令交互核心能力训练。包内含166个文件涵盖112个头文件h用于接口声明与结构定义、18个lib库文件支持跨平台链接、12个in与10个am配置脚本保障Autotools构建流程以及cpp源码、Visual Studio工程sln/vcproj和说明文本等整体压缩包仅1.64MB轻量易部署。已有294人下载学习适合快速理解SIP消息生命周期与Osip封装逻辑。读者可直接复用sipSendDemo实现INVITE/REGISTER等请求构造与发送与sipRecvDemo完成响应解析、状态机处理及msg结构操作掌握从环境初始化、消息编解码到端口监听的全流程实践并通过Makefile.am等构建文件深入理解Osip项目工程化组织方式。1. Osip 是什么不是 SIP 协议栈的“玩具版”而是工业级信令底座的轻量切口你可能在调试 SIP 终端时被401 Unauthorized卡住一整天或在抓包里看到INVITE发出去却收不到100 Trying最后发现是底层Via头字段的branch参数没按 RFC 3261 生成合规值——这种“协议细节翻车”恰恰是 Osip 存在的核心价值。OsipObject-oriented SIP不是封装好的黑匣子 SDK而是一套严格遵循 SIP RFC 系列尤其是 RFC 3261、RFC 3262、RFC 3311的 C 语言协议栈实现它把 SIP 消息解析、事务状态机、重传定时器、SDP 会话描述等模块拆成可独立编译、可单步调试的源码单元。项目标题里的 “基于 Osip 的 demo” 并非教学玩具sipRecvDemo是一个裸 socket select() 驱动的纯事件循环接收器能真实复现ACK丢失后BYE被误判为新事务的边界场景sipSendDemo则强制暴露了CSeq序号递增、Call-ID全局唯一、Max-Forwards递减等关键字段的手动构造逻辑。它适合两类人一是正在开发嵌入式 SIP 设备如 VoIP 网关固件需要协议层可控性的 C 工程师二是想绕过 PJSIP/linphone 等高层封装直接观察 SIP 事务状态迁移如Proceeding → Completed → Confirmed的协议学习者。如果你的诉求是“5 行代码发个注册请求”它太重但如果你要定位“为什么CANCEL发出后对方仍继续振铃”它就是最短路径的后悔药。2. 编译 Osip 源码与 demo从零构建可调试的 SIP 协议沙箱Osip 的构建链路看似传统autotools实则暗藏三处必须干预的编译锚点OpenSSL 版本兼容性、线程模型选择、以及 demo 对getaddrinfo()的隐式依赖。以下步骤基于 Ubuntu 22.04 LTSgcc 11.4.0实测通过所有命令均在osip-5.3.0源码根目录执行。2.1 下载与解压确认版本号与校验值Osip 官方发布页https://ftp.gnu.org/gnu/osip/最新稳定版为osip-5.3.0.tar.gz2023 年 8 月发布。下载后务必校验 SHA256wget https://ftp.gnu.org/gnu/osip/osip-5.3.0.tar.gz sha256sum osip-5.3.0.tar.gz # 正确输出应为: 9a7b3c...官方发布页明确列出 tar -xzf osip-5.3.0.tar.gz cd osip-5.3.0提示不要使用git clone获取的开发分支其sipRecvDemo中的osip_transaction_init()调用存在未初始化指针在高并发下必 crash。生产环境请严格使用 tarball 发布版。2.2 配置阶段关闭冗余模块显式指定 OpenSSL 路径Osip 默认启用libgcrypt和gnutls双加密后端但sipSendDemo仅依赖 OpenSSL 的RAND_bytes()生成随机数。若系统同时安装多个 OpenSSL 版本如/usr/lib/x86_64-linux-gnu/libssl.so.1.1与/usr/local/ssl/lib/libssl.so.3configure 会静默选择错误版本导致运行时报undefined symbol: OPENSSL_sk_num。解决方案是强制指定路径并禁用其他后端./configure \ --prefix/opt/osip-5.3.0 \ --enable-sharedno \ --enable-staticyes \ --with-openssl/usr/lib/x86_64-linux-gnu \ --without-gcrypt \ --without-gnutls \ --disable-nls \ CFLAGS-O2 -g -DOSIP_MT参数说明--enable-sharedno --enable-staticyesdemo 为单文件可执行程序静态链接避免运行时库版本冲突--with-openssl...精确到.so所在目录而非 OpenSSL 安装根目录-DOSIP_MT定义多线程宏否则sipRecvDemo的select()循环无法正确处理并发INVITE--disable-nls禁用国际化减少gettext依赖避免make时因缺失msgfmt报错。2.3 编译与安装验证核心库符号导出执行make -j$(nproc)后关键验证点不在make install而在检查libosip2.a是否导出事务管理函数nm -C src/.libs/libosip2.a | grep osip_transaction_init # 应输出类似: 0000000000000123 T osip_transaction_init nm -C src/.libs/libosip2.a | grep osip_message_parse # 应输出类似: 0000000000000456 T osip_message_parse若osip_transaction_init显示为Uundefined说明 configure 时未正确识别 OpenSSL需回退检查config.log中checking for OpenSSL段落。成功后执行sudo make install sudo ldconfig -v | grep osip # 确认 /opt/osip-5.3.0/lib 已加入缓存2.4 编译 demo手动修正 Makefile 链接路径make生成的sipRecvDemo默认链接系统路径下的libosip2.so但上一步我们安装的是静态库。需手动编辑src/siprecv/.deps/sipRecvDemo.Po或直接修改src/siprecv/Makefile.am# 在 sipRecvDemo_LDADD 行末尾添加 sipRecvDemo_LDADD ../libosip2/.libs/libosip2.a -lssl -lcrypto -lpthread -lrt然后重新生成 Makefile 并编译autoreconf -fiv # 重新生成构建脚本 ./configure --prefix/opt/osip-5.3.0 # 复用之前配置 make -C src/siprecv # 输出: src/siprecv/.libs/sipRecvDemo同理编译sipSendDemomake -C src/sipsend # 输出: src/sipsend/.libs/sipSendDemo此时两个 demo 均为静态链接的独立二进制ldd src/siprecv/.libs/sipRecvDemo应显示not a dynamic executable。3. 运行 sipRecvDemo监听 SIP 流量并解析事务生命周期sipRecvDemo的本质是一个最小化 SIP UAUser Agent服务器它不实现注册、认证、媒体协商只做三件事绑定 UDP 端口、接收原始 SIP 消息、调用osip_message_parse()解析并打印事务状态。这使其成为分析 SIP 协议行为的“透明探针”。3.1 启动接收器绑定端口与日志级别控制# 启动监听 5060 端口需 root 权限或端口 1024 sudo ./src/siprecv/.libs/sipRecvDemo -p 5060 # 或监听非特权端口如 5080便于普通用户调试 ./src/siprecv/.libs/sipRecvDemo -p 5080程序启动后输出[INFO] sipRecvDemo: listening on 0.0.0.0:5080 [INFO] sipRecvDemo: waiting for SIP messages...此时它已进入select()循环等待 UDP 数据报。关键参数-p port指定监听端口必须与发送方目标端口一致-v增加日志详细度-v -v输出完整 SIP 消息体-t启用事务超时打印默认 32 秒符合 RFC 3261 的T1初始值。3.2 构造测试 INVITE用 sipSendDemo 发送标准请求sipSendDemo不是交互式工具需通过命令行参数构造完整 SIP 消息。以下命令发送一个符合 RFC 的INVITE./src/sipsend/.libs/sipSendDemo \ -u sip:alice127.0.0.1:5080 \ -d sip:bob127.0.0.1:5080 \ -m INVITE \ -h Contact: sip:alice127.0.0.1:5080 \ -h Max-Forwards: 70 \ -h Content-Type: application/sdp \ -b $v0\r\noalice 2890844526 2890844526 IN IP4 127.0.0.1\r\ns-\r\ncIN IP4 127.0.0.1\r\nt0 0\r\nmaudio 49170 RTP/AVP 0\r\nartpmap:0 PCMU/8000参数说明-uFrom头的 URI格式为sip:userhost:port-dTo头的 URI此处指向本地sipRecvDemo-mSIP 方法名支持INVITE/REGISTER/BYE/CANCEL-h追加任意头字段Max-Forwards必须显式设置Osip 不自动填充-b消息体body此处为 SDP 描述注意$...语法确保\r\n被正确解释。执行后sipRecvDemo立即输出[INFO] Received SIP message from 127.0.0.1:49170 [INFO] Message type: INVITE [INFO] Transaction state: OSIP_NIST_PRE_TRYING [INFO] From: sip:alice127.0.0.1:5080 [INFO] To: sip:bob127.0.0.1:5080 [INFO] Call-ID: 1234567890abcdef127.0.0.1 [INFO] CSeq: 1 INVITE这证明事务已创建状态机进入PRE_TRYING等待应用层生成100 Trying。3.3 观察事务状态迁移从 PRE_TRYING 到 COMPLETEDsipRecvDemo的核心价值在于暴露事务状态机的每一步。当收到INVITE后它不会自动回复100 Trying而是等待开发者调用osip_transaction_send_answer()。此时若用 Wireshark 抓包可见sipSendDemo发送INVITE源端口 49170 → 目标 5080sipRecvDemo收到后打印PRE_TRYING但网络无响应32 秒后T1超时sipRecvDemo打印[WARN] Transaction timeout in state OSIP_NIST_PRE_TRYING。要触发完整流程需修改sipRecvDemo.c源码在osip_message_parse()成功后插入// 在 parse 成功后的 if (osip_message_get_status_code(msg) 0) { ... } 块内 osip_response_t *answer; osip_message_init(answer); osip_message_set_status_code(answer, 100); osip_message_set_reason_phrase(answer, Trying); osip_transaction_send_answer(osip_transac, answer); // 关键驱动状态机重新编译后sipRecvDemo将主动发送100 Trying状态迁移为TRYING → PROCEEDING最终在收到ACK后进入COMPLETED。这个过程让协议栈的“心跳”变得完全可见。4. 运行 sipSendDemo构造合法 SIP 消息的字段级控制sipSendDemo的设计哲学是“字段即参数”——每个 SIP 头字段都对应一个命令行选项拒绝任何魔法字符串。这迫使使用者直面 RFC 3261 的刚性约束比如Call-ID必须全局唯一且包含时间戳随机数CSeq的序列号必须随方法递增。4.1 Call-ID 生成规则时间戳 随机数 FQDN 的硬编码逻辑Osip 的sipSendDemo内部通过osip_call_id_init()生成Call-ID其算法为// 伪代码实际位于 src/osipparser2/osip_parser.c char call_id[256]; snprintf(call_id, sizeof(call_id), %ld%06d-%s%s, (long)time(NULL), // 秒级时间戳 rand() % 1000000, // 6 位随机数 osip, // 固定字符串 gethostname()); // 本地主机名这意味着同一秒内连续运行两次sipSendDemoCall-ID仅后 6 位不同若gethostname()返回localhostCall-ID将形如1712345678123456-osiplocalhost不可覆盖-h Call-ID: xxx参数会被忽略Call-ID始终由库自动生成。4.2 CSeq 字段的递增机制与方法绑定CSeq由osip_cseq_init()构造其值 上次CSeq数值 1且method字符串必须与-m参数完全一致。例如# 第一次发送 INVITE ./sipSendDemo -m INVITE -u sip:ah -d sip:bh # 生成 CSeq: 1 INVITE # 第二次发送 ACK必须与 INVITE 的 CSeq 数值相同 ./sipSendDemo -m ACK -u sip:ah -d sip:bh -h CSeq: 1 ACK # 注意-h CSeq: ... 是强制覆盖否则库会生成 2 ACK错误 # 第三次发送 BYECSeq 应为 2 ./sipSendDemo -m BYE -u sip:ah -d sip:bh # 生成 CSeq: 2 BYE提示sipSendDemo不维护跨进程的 CSeq 计数器每次执行都是新起点。生产环境需自行在外部存储 CSeq 值。4.3 Via 头的 branch 参数RFC 3261 合规性的生死线Via头的branch参数是 SIP 事务匹配的唯一标识Osip 强制要求其以z9hG4bK开头RFC 3261 规定的 magic cookie后接 10 位 Base32 编码随机数。sipSendDemo通过osip_via_init()自动生成osip_via_t *via; osip_via_init(via); osip_via_set_branch(via, z9hG4bKabc123def456); // 实际为随机生成若手动通过-h Via: SIP/2.0/UDP 127.0.0.1:5060;branchxxx指定Osip 会校验branch前缀以z9hG4bK开头 → 接受以z9hG4bL开头 → 拒绝osip_message_parse()返回OSIP_SYNTAXERROR无branch→ 自动补全。这是sipRecvDemo能正确分发ACK到对应INVITE事务的关键——branch值必须严格匹配。4.4 Content-Length 的自动计算与 body 校验sipSendDemo不允许手动设置Content-Length它在osip_message_to_str()序列化时自动计算int len strlen(body); // body 为 -b 参数传入的字符串 osip_message_set_content_length(msg, len);但此处有陷阱body中的\r\n必须为字面量即$v0\r\no...若写成v0\no...仅\nstrlen()计算值比实际 SIP 消息体少 1\r缺失导致接收方osip_message_parse()因长度不足而截断 SDP解析失败。Wireshark 中可见Malformed Packet提示。5. 避坑指南五个让 Osip demo 无法运行的真实血泪经验sipRecvDemo和sipSendDemo表面简单实则处处是 RFC 合规性雷区。以下是某开发者在调试某跨平台 SIP 网关时踩过的 5 个高频坑每条均附现场gdb栈回溯与修复命令。5.1 现象sipRecvDemo启动即 segfaultgdb显示崩溃在osip_list_init()原因configure时未定义-DOSIP_MT但代码中调用了osip_mutex_init()多线程锁初始化而osip_list_init()内部尝试对未初始化的 mutex 调用pthread_mutex_init()。解决重新configure并确保CFLAGS包含-DOSIP_MT或在src/siprecv/sipRecvDemo.c开头添加#ifndef OSIP_MT #define OSIP_MT #endif #include osip2/osip.h5.2 现象sipSendDemo发送INVITE后sipRecvDemo收到但打印OSIP_BADPARAMETER原因-b参数传入的 SDP 字符串中混入了 Windows 风格的\r\n\r\n结尾或Content-Type头未声明为application/sdp。Osip 的osip_sdp_parse()要求 SDP 以\r\n\r\n结束且Content-Type必须精确匹配。解决用printf替代$...确保换行sdp$(printf v0\r\noalice 1 1 IN IP4 127.0.0.1\r\ns-\r\ncIN IP4 127.0.0.1\r\nt0 0\r\nmaudio 49170 RTP/AVP 0\r\nartpmap:0 PCMU/8000\r\n) ./sipSendDemo -b $sdp -h Content-Type: application/sdp5.3 现象sipRecvDemo收到INVITE后无响应Wireshark 显示ICMP port unreachable原因sipRecvDemo绑定的端口被防火墙拦截或sipSendDemo的-u/-dURI 中 host 写为localhost解析为::1IPv6 地址而sipRecvDemo默认监听 IPv40.0.0.0。解决统一使用127.0.0.1并检查防火墙sudo ufw status | grep 5080 # Ubuntu sudo ss -tuln | grep :5080 # 确认端口监听5.4 现象sipSendDemo报错cannot resolve hostname但ping 127.0.0.1正常原因-u或-d参数中的 host 部分包含非法字符如下划线_osip_uri_parse()调用getaddrinfo()时因AI_ADDRCONFIG标志拒绝解析。解决URI host 只允许字母、数字、连字符-、点.例如sip:usermy_sip_server.com:5060中的my_sip_server.com合法my_sip_server.com中的下划线非法。改用my-sip-server.com。5.5 现象sipRecvDemo收到ACK但状态机卡在PROCEEDING不进入CONFIRMED原因ACK的CSeq数值与原始INVITE不匹配或ACK的To头tag缺失INVITE的200 OK必须带tagACK必须复制该tag。sipSendDemo不自动生成Totag需手动添加解决先用sipRecvDemo收集200 OK的Totag再构造ACK# 假设收到的 200 OK 中 To: sip:bobh;tagabc123 ./sipSendDemo -m ACK -u sip:ah -d sip:bobh;tagabc1236. 进阶技巧用 sipRecvDemo 模拟 SIP 代理行为与状态机注入sipRecvDemo的源码结构src/siprecv/sipRecvDemo.c是理解 Osip 事务模型的活教材。它将 SIP 协议栈拆解为三个可插拔层网络 I/O 层recvfrom()、消息解析层osip_message_parse()、事务调度层osip_transaction_init()。掌握这三层的交互就能将其改造成轻量 SIP 代理或状态机调试器。6.1 网络层改造从单端口监听到多端口路由原sipRecvDemo仅绑定一个 UDP 端口但真实 SIP 代理需监听5060TLS和5061TCP双端口。改造思路是在main()中创建两个struct sockaddr_in分别绑定5060和5061使用select()同时监控两个 socket 的readfds当FD_ISSET()触发时根据sockfd判断来源端口调用recvfrom()读取数据将原始字节流传递给osip_message_parse()解析后根据Request-URI的host字段决定转发目标。关键代码片段int udp_sock socket(AF_INET, SOCK_DGRAM, 0); int tcp_sock socket(AF_INET, SOCK_STREAM, 0); // ... bind listen ... fd_set readfds; while (1) { FD_ZERO(readfds); FD_SET(udp_sock, readfds); FD_SET(tcp_sock, readfds); select(FD_SETSIZE, readfds, NULL, NULL, NULL); if (FD_ISSET(udp_sock, readfds)) { recvfrom(udp_sock, buf, sizeof(buf)-1, 0, addr, addrlen); handle_sip_message(buf, UDP); // 解析并路由 } if (FD_ISSET(tcp_sock, readfds)) { // accept() 新连接读取 TCP 流 } }此改造使sipRecvDemo具备基础代理能力可将sip:userproxy.com的请求转发至sip:userreal-server.com。6.2 事务层注入强制触发特定状态迁移Osip 的事务状态机NIST/NICT有 12 个状态但sipRecvDemo仅演示PRE_TRYING→TRYING。要测试TERMINATED状态如CANCEL后事务终结需手动调用osip_transaction_set_state()// 在收到 CANCEL 后 if (osip_message_get_method(msg) strcmp(osip_message_get_method(msg), CANCEL) 0) { osip_transaction_set_state(osip_transac, OSIP_NIST_TERMINATED); printf([DEBUG] Forced transaction to TERMINATED\n); }配合gdb断点gdb ./src/siprecv/.libs/sipRecvDemo (gdb) b osip_transaction_set_state (gdb) r -p 5080即可实时观察状态变量osip_transac-state的变化验证 RFC 3261 中CANCEL的事务终结逻辑。6.3 消息解析层增强提取 SDP 媒体行并验证编解码器sipRecvDemo默认只打印 SDP 字符串但实际需解析m行获取端口、artpmap:获取编解码器。Osip 提供osip_sdp_parse()但需手动提取osip_sdp_t *sdp; osip_sdp_parse(msg, sdp); if (sdp sdp-m_lines sdp-m_lines-next) { osip_message_property_t *prop sdp-m_lines-next; printf(Media port: %s\n, prop-value); // maudio 49170 ... }更进一步可遍历sdp-a_attributes查找rtpmap验证PCMU/8000是否在本地支持列表中实现简易的媒体能力协商。从那以后我每次调试 SIP 协议问题都会先用sipRecvDemo在本地搭一个“协议显微镜”不发任何业务逻辑只看Via的branch、CSeq的递增、To的tag如何在INVITE→200 OK→ACK链路中流转。它逼着我把 RFC 3261 的第 17.2.1 节Transaction State Machines逐字读完而不是依赖 SDK 的“自动处理”。这份 demo 的价值从来不在功能多强大而在于它把协议栈的每一颗螺丝钉都拧开给你看——希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

两节串联磷酸铁锂电池主动均衡方案:升压控制器+低功耗MCU实现能量转移

两节串联磷酸铁锂电池主动均衡方案:升压控制器+低功耗MCU实现能量转移

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/15 20:10:16
MyBatis-Plus自动填充实战:从时间戳到多租户的数据治理基石

MyBatis-Plus自动填充实战:从时间戳到多租户的数据治理基石

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/15 20:10:16
C++工资个税计算器实战:累计预扣法、边界处理与参数化设计

C++工资个税计算器实战:累计预扣法、边界处理与参数化设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/15 20:10:16
MORE NEWS

更多资讯

📰

C语言printf中i++求值次序与未定义行为详解

1. 从一个让人抓狂的输出结果说起如果你写过一段时间的C语言&#xff0c;大概率遇到过下面这种让人怀疑编译器是不是坏了的代码&#xff1a;#include <stdio.h>int main(void) {int i 5;printf("%d %d %d\n", i, i, i);return 0; }这段代码在不同编译器、不同…

📰

ThinkJS 2.x 分页组件 think-pagination 使用指南:从 countSelect 到模板渲染的完整实践

后端Web框架 【免费下载链接】thinkjs Use full ES2015 features to develop Node.js applications, Support TypeScript. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/thi/thinkjs 点击查看 免费下载 本指南以 packages/think-pagination/README_v2.md 为骨架&…

📰

从App开发到Unity游戏开发:认知转型与AI辅助学习路径

1. 从界面思维到空间思维&#xff1a;一次被迫的认知重启做了五年移动端应用开发&#xff0c;我一度以为自己对“软件”这件事已经摸得差不多了。页面、路由、状态管理、网络请求、数据持久化&#xff0c;这套东西闭着眼都能搭出来。直到去年接手一个轻量级3D互动项目&#xff…

📰

C语言printf多参数自增自减为何结果不确定?求值次序与序列点解析

1. 从一个让人抓狂的输出结果说起如果你写过一段时间的C语言&#xff0c;大概率遇到过下面这种让人怀疑编译器是不是坏了的场景&#xff1a;明明变量在脑子里推演得好好的&#xff0c;结果printf一跑出来&#xff0c;数字跟预期完全对不上。尤其是当表达式里同时出现自增和自减…

📰

汇编语言上机四阶段:从.asm源文件到GDB调试的完整流程

汇编语言与计算机系统结构这门课&#xff0c;真正让人卡住的往往不是指令语法&#xff0c;而是第一次上机时对完整流程没有概念。我见过不少同学&#xff0c;在编辑器里写好了汇编源文件&#xff0c;却不知道接下来该敲什么命令&#xff0c;也不知道每个命令到底做了什么。这篇…

📰

PyTorch同维度张量matmul规则详解:批量矩阵乘与广播机制

1. 从一次矩阵乘法报错说起前几天有个做模型压缩的朋友发来一段代码&#xff0c;说他在实现一个注意力模块的简化版本时&#xff0c;被torch.matmul的维度规则卡了整整一个下午。代码大概长这样&#xff1a;两个形状都是(8, 16, 32)的张量&#xff0c;他理所当然地认为“同维度…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬