ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

基于oSIP的SIP信令Demo:从注册到呼叫的完整实现 简介基于 Osip 的 SIP 通信示例工程专门面向对会话初始化协议与多媒体通信控制感兴趣的 C/C 开发者也适合正在完成毕业设计或通信实验的初学者无论商用软交换调试还是个人学习研究均可复用。资源内含发送示例与接收示例两个可直接运行的工程分别演示请求消息的构造与发送、响应消息的接收与解析能够帮助读者快速掌握开源库 Osip 在真实网络环境下的调用方式。压缩包共 166 个文件整体约 1.64MB核心代码以头文件和源文件为主同时包含静态库文件、工程配置文件、自动构建脚本及解决方案文件便于在开发环境中直接打开编译。该资源已有 292 人浏览学习适合正在编写用户代理或需要完成协议栈封装工作的开发者。内容围绕 Osip 封装类设计展开覆盖初始化、消息构建、发送、接收及解码等完整流程并通过消息结构体封装简化信令传递通过阅读这两个演示工程能够理解注册、邀请、成功应答等典型信令消息的交互过程掌握请求行、消息头和消息体的组装方式为后续扩展鉴权、注册、代理转发等功能打下良好基础。1. “基于oSIP的demo”到底在做什么做通信类开发的人对SIP协议栈应该都不陌生。oSIPGNU 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这些参数你要保证在构造后续消息时用对了对话实例。呼叫方主流程是构造INVITE带SDP body我这里用空body或者写死一个简单SDP字段。发送INVITE进入Calling状态。收到100 Trying忽略或打印日志。收到180 Ringing说明对端振铃。收到200 OK回复ACK此时媒体理论上可以开始协商传输。被叫方流程是收到INVITE保存对话。回100 Trying。回180 Ringing。用户接听后回200 OK。收到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_SENTINVITE已发出。SIP_ICT_STATUS_RECEIVED收到临时响应或最终响应。SIP_ICT_ACK_SENTACK已发送。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 -lpthread4.2 配置文件与启动参数为了让demo能被不同场景复用我给它加了几个启动参数./sip_demo -s 192.168.1.100 -p 5060 -u 1001 -a 123456 -m call-sSIP服务器地址。-pSIP服务器端口。-u本地分机号。-a鉴权密码。-m运行模式call/answer/register三选一。call模式主动向外呼叫answer模式作为被叫等待INVITEregister模式只做注册测试。三个模式用同一个二进制就是省得每次改代码重新编译。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, expires3600注册成功后我又起了另一个实例分别跑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库开发都会顺手很多。本文还有配套的精品资源点击获取
返回列表