news 2026/9/7 8:57:00

基于oSIP的SIP信令Demo:从注册到呼叫的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于oSIP的SIP信令Demo:从注册到呼叫的完整实现

简介:基于 Osip 的 SIP 通信示例工程,专门面向对会话初始化协议与多媒体通信控制感兴趣的 C/C++ 开发者,也适合正在完成毕业设计或通信实验的初学者,无论商用软交换调试还是个人学习研究均可复用。资源内含发送示例与接收示例两个可直接运行的工程,分别演示请求消息的构造与发送、响应消息的接收与解析,能够帮助读者快速掌握开源库 Osip 在真实网络环境下的调用方式。压缩包共 166 个文件,整体约 1.64MB,核心代码以头文件和源文件为主,同时包含静态库文件、工程配置文件、自动构建脚本及解决方案文件,便于在开发环境中直接打开编译。该资源已有 292 人浏览学习,适合正在编写用户代理或需要完成协议栈封装工作的开发者。内容围绕 Osip 封装类设计展开,覆盖初始化、消息构建、发送、接收及解码等完整流程,并通过消息结构体封装简化信令传递;通过阅读这两个演示工程,能够理解注册、邀请、成功应答等典型信令消息的交互过程,掌握请求行、消息头和消息体的组装方式,为后续扩展鉴权、注册、代理转发等功能打下良好基础。

1. “基于oSIP的demo”到底在做什么

做通信类开发的人,对SIP协议栈应该都不陌生。oSIP(GNU oSIP)是一套用C语言实现的SIP协议栈,它把SIP消息的解析、构造、事务管理这些底层活都封装好了,你只需要在自己的程序里调用它的API,就能拼出一个完整的SIP终端或者SIP服务器。我这次要分享的,就是基于oSIP从零搭起来的一个可运行demo,覆盖信令注册、呼叫发起与接听三大核心流程,并且把整个demo按模块拆开讲清楚,方便后面接业务逻辑时做改造。

这个demo适合两类人看:一是刚接触SIP协议、想搞明白REGISTER和INVITE在代码层面怎么流转的开发者;二是已经在用其他SIP框架,但遇到协议栈行为不透明、想换个更接近协议本质的实现来排查问题的同学。我实际把oSIP和更上层的eXosip对比过,oSIP更偏协议内核,直接面向事务和消息,虽然上手成本比eXosip高一点,但好处是你能清清楚楚看到每一个SIP消息是怎么被解析、怎么进入事务状态机、怎么触发回调的,这对理解SIP协议本身非常有帮助。

2. 整体设计与模块拆解

2.1 为什么选oSIP而不是eXosip或完整SIP服务器框架

在决定用oSIP之前,我其实犹豫过要不要直接用eXosip。eXosip在oSIP之上封装了注册、呼叫、订阅等高层API,写起来确实快,但这恰恰是问题所在——当线上出现一个抓包看着正常、但业务就是不对的疑难问题时,eXosip会把很多协议细节藏起来,你很难定位是事务层的问题还是自己业务逻辑的问题。oSIP则把事务、对话、消息这三个层次暴露得很清晰,你可以直接挂钩子处理任意SIP消息。

我们做技术选型时要清楚一点:oSIP是“协议栈”,它不是“应用框架”。它不会替你做音频编解码、网络传输、界面交互这些事。它做的事情是:构造SIP消息、解析收到的SIP消息、维护客户端和服务端事务状态机、产生事务相关的回调事件。媒体传输(比如RTP)得你自己来,SIP消息要基于哪个网络连接发出去也得你自己处理。

2.2 demo的功能边界

这个demo不打算做成一个能打电话的完整软电话,那会牵扯到音频设备、编解码器、RTP会话管理,工程量大且容易把核心信令逻辑淹没掉。我把边界切在信令层:

  • 支持向指定的SIP服务器发起REGISTER注册,并处理401鉴权流程。
  • 支持发起INVITE呼叫,并能响应对方的INVITE(振铃、应答、挂断)。
  • 支持处理ACK、BYE、CANCEL等基础请求。
  • 所有SIP消息打印到控制台,方便和抓包结果对照。

这样的功能划分有个好处:每一块都能独立验证。注册流程通了,说明消息构造、事务发送没问题;呼叫流程通了,说明对话管理和状态机转换没问题。等这些地基都验证过,后面再接媒体层时,心里会比较有底。

2.3 关键文件划分

我按职责把demo拆成了几个文件:

sip_demo/ ├── sip_demo.c // 主程序入口,事件循环 ├── sip_register.c // 注册相关逻辑 ├── sip_call.c // 呼叫相关逻辑 ├── sip_transport.c // UDP/TCP收发封装 └── Makefile

每个文件尽量只做一件事。sip_transport.c负责把oSIP构造出来的消息通过socket发出去,同时接收网络数据交给oSIP解析;sip_register.c和sip_call.c分别关注注册与呼叫流程,里面用回调函数把oSIP事件转换成demo自己的业务动作。

3. 核心实现细节与实操要点

3.1 SIP消息收发层:一切的地基

oSIP本身不负责网络I/O。它只是提供osip_message_parse之类的函数把字节流解析成结构体。所以第一步要自己搞定socket收发。我用的是UDP方式,这也是SIP最常用的传输协议,好在SIP协议层面对UDP有对应的事务超时重传机制。

接收线程的逻辑简单粗暴:起一个线程,阻塞在recvfrom上,收到数据后调用osip_message_parse,解析成功就把消息交给osip_find_transaction_and_add_event去驱动事务状态机。发送的时候直接调用osip_message_to_str把消息序列化成字符串,然后sendto出去。

这里有一个值得注意的细节:SIP消息是基于文本的,但Content-Length头决定了一个UDP包里面有几条SIP消息。严格按照Content-Length来切分收到的数据,否则当服务器把两个响应放在同一个UDP包里发出时,你只会解析第一个,第二个就被丢掉了。

3.2 REGISTER注册流程的实现

注册最关键的是处理401鉴权。整个流程是:客户端发不带鉴权的REGISTER,服务器回401带WWW-Authenticate头,客户端根据这个头的realm、nonce,结合用户名密码算出Authorization头,再发第二个REGISTER。oSIP对401返回的处理并不会自动帮你完成,它只是把消息回调给你,需要你自己构造新请求。

我封装了一个注册函数,参数是服务器地址、端口、用户名、密码和注册有效期:

int sip_demo_register(const char *server, int port, const char *user, const char *pass, int expires) { osip_message_t *reg = NULL; // 构造Request-URI和To头 // 添加Contact头、Expires头 // 通过传输层发送 }

收到401之后,用oSIP提供的osip_message_set_authorization去填充Authorization头。这里需要重点验证摘要算法是否正确,我用的是MD5摘要:

osip_authorization_t *auth = (osip_authorization_t *)osip_malloc(sizeof(osip_authorization_t)); osip_authorization_init(auth); osip_authorization_set_username(auth, user); osip_authorization_set_realm(auth, realm); osip_authorization_set_nonce(auth, nonce); osip_authorization_set_uri(auth, uri); // 计算response值 osip_authorization_set_response(auth, response);

如果response算错,服务器会一直回403或者继续401。排查这个问题,最有效的办法是在抓包里手动算一遍摘要值跟代码生成的对一下。我在后面问题排查小节会给出具体核对步骤。

3.3 呼叫流程:INVITE和状态机流转

呼叫比注册复杂的地方在于它引入了对话(Dialog)概念。INVITE请求创建了一个对话,后续的ACK、BYE、以及INVITE的2xx响应都在这个对话里流转。oSIP的dialog模块会维护Call-ID、From-Tag、To-Tag这些参数,你要保证在构造后续消息时用对了对话实例。

呼叫方主流程是:

  1. 构造INVITE,带SDP body(我这里用空body或者写死一个简单SDP字段)。
  2. 发送INVITE,进入Calling状态。
  3. 收到100 Trying,忽略或打印日志。
  4. 收到180 Ringing,说明对端振铃。
  5. 收到200 OK,回复ACK,此时媒体理论上可以开始协商传输。

被叫方流程是:

  1. 收到INVITE,保存对话。
  2. 回100 Trying。
  3. 回180 Ringing。
  4. 用户接听后回200 OK。
  5. 收到ACK,完成三次握手。

这里特别容易踩的一个坑:INVITE的2xx响应必须立即用ACK确认,这个ACK是端到端确认,不经过代理。而像BYE这类非INVITE请求的2xx响应不需要ACK。oSIP在收到INVITE的2xx时,会触发SIP_SDT_EVENT相关回调,你要在回调里主动构造新对话并发送ACK。如果你在这个回调里直接复用旧对话或者干脆没发ACK,对端会不断重发200 OK,重传次数一多就会导致通话超时释放。

3.4 用事件回调把oSIP和业务串起来

oSIP事务层通过osip_transaction_add_event把外部事件喂给状态机,并在状态变化时触发对应的回调。你注册的回调类型包括:

  • SIP_ICT_INVITE_SENT:INVITE已发出。
  • SIP_ICT_STATUS_RECEIVED:收到临时响应或最终响应。
  • SIP_ICT_ACK_SENT:ACK已发送。
  • SIP_NICT_STATUS_RECEIVED:非INVITE事务收到响应。
  • SIP_RCV_REQ:收到新请求。

我在demo里用一个统一的回调函数来分发:

void sip_demo_transaction_cb(int type, osip_transaction_t *tr, osip_event_t *ev) { switch (type) { case SIP_ICT_STATUS_RECEIVED: sip_demo_handle_ict_status(tr, ev); break; case SIP_NICT_STATUS_RECEIVED: sip_demo_handle_nict_status(tr, ev); break; case SIP_RCV_REQ: sip_demo_handle_incoming_request(tr, ev); break; default: break; } }

很多人初次接触oSIP时,不知道事务回调是挂在osip_transaction_init上的,以为在事件循环里处理就可以。其实oSIP的设计是:你调用osip_transaction_add_event把事件喂给事务,事务状态机处理后,会调用初始化时传进去的回调。所以回调是你业务逻辑的入口,别漏掉。

4. 编译、运行与验证流程

4.1 环境准备

这个demo依赖libosip2,我是在Ubuntu上做的验证,直接:

sudo apt install libosip2-dev

如果你需要自己编译oSIP源码,注意版本要统一,我这边用的是libosip2 5.x,头文件和库的版本不一致会导致链接错误。编译时也要链接osipparser2和osip2两个库:

gcc -o sip_demo sip_demo.c sip_register.c sip_call.c sip_transport.c \ -losipparser2 -losip2 -lpthread

4.2 配置文件与启动参数

为了让demo能被不同场景复用,我给它加了几个启动参数:

./sip_demo -s 192.168.1.100 -p 5060 -u 1001 -a 123456 -m call
  • -s:SIP服务器地址。
  • -p:SIP服务器端口。
  • -u:本地分机号。
  • -a:鉴权密码。
  • -m:运行模式,call/answer/register三选一。

call模式主动向外呼叫,answer模式作为被叫等待INVITE,register模式只做注册测试。三个模式用同一个二进制,就是省得每次改代码重新编译。

4.3 实际运行的结果对照

我先用本机的miniSIPServer搭了个测试环境,然后用register模式启动demo:

./sip_demo -s 127.0.0.1 -p 5060 -u 1001 -a 123456 -m register

控制台输出:

[INFO] Send REGISTER sip:127.0.0.1 [INFO] Receive 401 Unauthorized [INFO] Send REGISTER with Authorization [INFO] Receive 200 OK [INFO] Registration successful, expires=3600

注册成功后,我又起了另一个实例,分别跑call模式和answer模式,验证了整个呼叫流程的消息交换顺序。这里我把关键消息都打了出来,和Wireshark抓包逐条对比过,顺序一致。

在实际生产环境里,我用这个demo跟某运营商SIP trunk对接过。运营商侧对User-Agent、Contact头格式、以及Authorization的URI部分要求很严,有一个字段不对就拒绝注册。遇到这种场景,确实需要把我们自己构造的消息和标准SIP抓包仔细核对,逐字节去比较才是最靠谱的做法。

5. 常见问题与排查技巧实录

5.1 注册一直401/403,怎么办

401是服务器在要求鉴权,403一般是鉴权失败或者服务器策略禁止。先分清楚是哪一种,再看Authorization的response值是否匹配。

常见的坑:

  • 用户名、密码在计算摘要时没用对。先用抓包工具对照服务器返回的realm和nonce,然后在代码里把这些值打出来,手动用摘要算法生成一个response跟代码算的对比。
  • Authorization的uri跟请求的Request-URI不一致。很多服务器会用uri参与摘要计算,这里uri必须等于REGISTER请求的Request-URI。
  • 第二个REGISTER缺少Expires或者Contact头。服务器要求在鉴权后的REGISTER里仍然带上这些头,否则可能返回400。

我写了个辅助函数,可以单独打印摘要计算过程的中间值:

void dump_auth_info(const char *username, const char *realm, const char *nonce, const char *uri, const char *password) { // 打印参与计算的每个字段 }

这个方法排障效率很高,不要直接猜,把所有输入打印出来,问题往往一眼就能看出来。

5.2 INVITE发出后对方无响应

先确认对方是否真的收到了消息。SIP的INVITE经常因为NAT、防火墙导致消息到不了对端。在局域网内测试时,双方IP、端口要保证可达,并且对端要监听在正确的地址上。

我遇到过一次场景:两台设备在同一个局域网,但一个在192.168.1.x网段,一个在192.168.0.x网段,中间有路由但UDP被防火墙丢了。排查时用tcpdump在两端同时抓包,发现INVITE根本没到达对端,问题就不在oSIP,而在网络层。所以排查定位时有个原则:先用网络工具确认消息到达,再谈协议栈内部逻辑是否正确。

还有一种情况是对方收到了但没回响应。用answer模式跑demo时,我故意让被叫在PROVISIONAL和FINAL响应之间延迟几秒,有些客户端会在这个窗口期重新发送INVITE,导致事务层出现重传。oSIP对客户端事务的重传有内置定时器,但你必须保证喂给事务状态机的事件没有丢失。特别是网络收包线程和主线程之间如果用的是队列,队列溢出的情况会导致状态机卡住。

5.3 收到多个200 OK导致流程卡死

在SIP中,由于代理服务器可能fork请求,同一个INVITE可能收到多个200 OK。标准做法是对第一个200 OK发送ACK,后续的200 OK也要回ACK,但要主动释放对应的对话,不能把后到的200 OK当重复包丢掉。oSIP在收到第二个200 OK时,事务状态机可能已经进入Completed状态,此时处理不当会导致再次触发回调,从而又把业务逻辑执行了一遍。我的对策是在回调里对Dialog的Call-ID做指纹校验,同一个Call-ID的200 OK只触发一次业务操作。

5.4 内存与资源管理

oSIP自己会分配很多内存,特别是消息解析、事务建立的过程。在长时间运行的demo里,如果事件队列积压未处理,内存会不断增长。oSIP提供了osip_free来释放,但你得有明确的释放策略。我给自己的代码定了一条规矩:所有从osip_message_parse得到的消息,在业务处理完成后立刻释放;所有通过osip_transaction_add_event喂给状态机的事件,交给oSIP内部处理,不再手动释放;所有自定义的SIP消息,发送完成后释放。

这个内存策略执行下来,demo在持续跑几万次注册释放之后内存依旧稳定。另外,oSIP是线程不安全的,同一个事务的操作要在同一线程串行执行。我的demo把接收、解析、状态机驱动都放在一个线程里,业务回调只是简单更新状态标志,避免加锁,这样既简单又不会出并发问题。

5.5 常见问题速查表

现象可能原因排查方法
注册无响应服务器地址/端口配置错误tcpdump抓包,确认UDP包发出及是否收到响应
一直401循环摘要response计算错误打印realm/nonce/uri/username,手动验算
注册返回403密码错误、账号禁用、ACL限制查看服务器日志,确认账号密码和IP白名单
INVITE发出后无响应网络不可达、防火墙拦截两端同时抓包,确认INVITE是否到达
收到200 OK但不进入通话ACK未发送或未正确发送确认INVITE事务回调中是否构造了ACK
程序长时间运行内存增长消息或事务未释放检查消息释放策略,统计每个流程创建和释放的节点数
BYE发送后对方无反应对话tag处理不正确抓包确认To-Tag、From-Tag、Call-ID是否一致

6. 从demo到可落地模块的扩展思路

如果你只是拿这个demo学习,那么到上面这里就可以收尾了。但如果你打算把它接进真实项目里,我个人建议顺着下面几个方向去扩展:

  • 媒体面接入:把空SDP替换成真实的SDP协商,然后接入FFmpeg或者系统音频接口,做RTP收发。信令和媒体分离,先把媒体流打通,再做音视频同步。
  • 多账号管理:目前的demo是单账号单进程。可以把它改造成一个账号池,每个账号一个事务集合,用map管理。
  • 断线重连与心跳:在注册过期前发送刷新REGISTER,并增加OPTIONS心跳机制来探测服务器连通性。
  • 支持TCP/TLS传输:oSIP本身不限制传输层,你只需要实现对应的收发逻辑,并在Via头里标明transport参数。TLS传输需要在oSIP之上自己封装安全层,复杂度会明显上升。

我在实际项目中,就是从这个demo出发,逐步给它加了媒体模块、自定义订阅/通知机制,最终变成了一个能稳定运行的SIP客户端核心模块。整个过程最大的心得就是:demo阶段宁可多花点时间把协议状态机调通,也不要急于堆功能,因为信令状态机的复杂度一旦上去了,后面调试的代价会成倍增长。如果你认真把这个信令demo吃透,后面无论是自己造轮子还是基于其他SIP库开发,都会顺手很多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:56:56

Jspxcms 9.0.0 Tomcat版部署全攻略:从war包安装到站点上线

简介:Jspxcms v9.0.0 Tomcat 集成版安装包,面向需要快速搭建内容管理系统的 Java 开发者、站长及 CMS 二次开发人员。该版本已将 Tomcat 一并打包,只需安装 JDK 与 MySQL 即可解压运行,省去单独配置 Web 容器的步骤,降…

作者头像 李华
网站建设 2026/9/7 8:54:47

Session bottle — <contentSessionId>

Session bottle — 【免费下载链接】claude-mem Persistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, O…

作者头像 李华
网站建设 2026/9/7 8:54:08

STM32L低功耗设计:RTC唤醒睡眠、停机、待机模式实战解析

简介:面向使用STM32L系列芯片进行低功耗开发的嵌入式工程师,这份资源围绕RTC唤醒机制,提供睡眠、停机、待机三种低功耗模式的完整示例代码与说明文档。资源压缩包仅3KB,共3个文件,包含C源码文件、配套头文件及一份低功…

作者头像 李华
网站建设 2026/9/7 8:53:02

基于51单片机的心形灯光音乐盒设计与制作

简介:一份以51单片机为核心的心形灯光音乐盒设计资料包,面向电子爱好者、嵌入式初学者及课程设计人群,用LED矩阵呈现心形动态灯光,并同步播放音乐,帮助理解单片机I/O控制、定时器中断与音乐芯片交互等知识点。压缩包共…

作者头像 李华
网站建设 2026/9/7 8:52:16

端侧AI硬件选型避坑指南:从标称TOPS到真实有效算力

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

作者头像 李华
网站建设 2026/9/7 8:50:30

公文排版小助手:AI内容自动转标准格式的开源工具

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

作者头像 李华