简介本资源为基于Osip开源SIP库的通信示例工程面向具备一定C/C基础、希望理解SIP信令交互与协议栈封装的开发者。包内围绕sipSendDemo与sipRecvDemo两个示例演示了SIP消息的构建、发送、接收与解析流程并给出将Osip封装为易用类的设计思路涵盖初始化、消息构建、网络收发与解码等核心环节适合用于学习REGISTER、INVITE等请求及200 OK、404等响应的处理逻辑。资源共166个文件以112个h头文件、18个lib库文件、12个in配置、10个am构建脚本及6个cpp源文件为主另含vcproj、sln等工程文件压缩包约1.64MB结构完整便于直接编译调试。目前已有295人学习下载可作为理解SIP协议与Osip库实际应用的入门参考。1. 从两个可执行文件说起这套 Osip demo 到底能跑出什么如果你手头正好有一份基于 Osip 的 demo打开目录大概率会看到sipRecvDemo和sipSendDemo两个可执行目标而不是一个能点开就用的图形界面。很多人第一次拿到它会下意识去找main函数里的业务逻辑结果翻半天发现全是 SIP 信令的收发回调。这恰恰是它的价值所在它把 Osip 协议栈最核心的两条链路——收消息和发消息——拆成了两个最小可运行单元让你能在不引入任何业务框架的前提下把 SIP 注册、邀请、响应、ACK 这些流程跑通。适合谁适合正在做 VoIP 终端、SIP 网关、呼叫中心坐席软电话或者单纯想搞懂 Osip 到底怎么把一包 SIP 文本变成回调事件的开发者。它不是产品是一把手术刀。2. 先看清骨架Osip 协议栈在 demo 里的分层与目录结构2.1 从源码树看 Osip 的模块划分一份典型的 Osip demo 源码包目录结构不会太深但每一层都有明确职责。我拆过的版本大致长这样osip-demo/ ├── src/ │ ├── sipRecvDemo.c # 接收侧入口负责监听、解析、回调分发 │ ├── sipSendDemo.c # 发送侧入口负责构造请求、发送、等待响应 │ └── common/ │ ├── sip_util.c # 封装 osip_message_t 的创建与释放 │ └── sip_log.c # 日志钩子把 Osip 内部 trace 引到 stdout ├── include/ │ └── osip/ # Osip 头文件通常来自系统安装或源码内嵌 ├── build/ │ └── Makefile # 链接 -losip2 -losipparser2 -leXosip2视版本而定 └── README这里的关键是src/common这一层。很多 demo 为了“最小”直接把osip_message_init、osip_message_free散落在两个 main 文件里结果改一处收发包逻辑要动两个地方。合格的 demo 会把消息生命周期收拢到sip_util.c让sipRecvDemo和sipSendDemo只关心“收到什么、发什么”不关心“怎么分配内存”。2.2 收与发为什么必须分开编译sipRecvDemo和sipSendDemo分开不是为了好看而是因为 Osip 的事件模型是单线程轮询式的。接收侧要阻塞在osip_event_wait上等网络事件发送侧则要主动调用osip_message_send并等待OSIP_TRANSACTION状态迁移。如果硬塞进一个进程你得自己写状态机去区分“当前是等收还是等发”反而把 demo 的示范价值冲淡了。分开之后接收侧可以专注演示osip_event_t的解析与回调注册发送侧专注演示osip_message_t的构造与事务匹配。2.3 编译前必须确认的三个依赖在动手make之前先确认系统里有没有这三样东西否则报错会让人怀疑人生依赖项作用检查命令libosip2SIP 信令解析与事务层pkg-config --modversion libosip2libosipparser2消息头解析器ldconfig -p | grep osipparserpthreadOsip 内部锁与事件线程ldconfig -p | grep pthread如果pkg-config查不到说明开发包没装需要从源码编译 Osip 并make install。常见做法是先把 Osip 装到/usr/local再在 demo 的 Makefile 里加-I/usr/local/include -L/usr/local/lib。这一步不做后面链接阶段会直接报undefined reference to osip_message_init。3. 接收侧拆解sipRecvDemo 从 socket 到回调的完整链路3.1 初始化与监听把 Osip 挂到 UDP 端口上接收侧的第一步不是写业务而是让 Osip 知道“从哪个 socket 收包”。核心代码通常长这样#include osip2/osip.h #include osipparser2/osip_message.h int main(int argc, char *argv[]) { osip_t *osip_ctx; int port 5060; // 初始化 Osip 上下文内部会创建事务管理器 if (osip_init(osip_ctx) ! 0) { fprintf(stderr, osip_init failed\n); return -1; } // 告诉 Osip 监听哪个 UDP 端口第三个参数是传输层回调 if (osip_set_udp_socket(osip_ctx, port) ! 0) { fprintf(stderr, bind udp %d failed\n, port); osip_release(osip_ctx); return -1; } // 注册收到消息后的处理函数这是业务入口 osip_set_callback(osip_ctx, OSIP_MESSAGE_RECEIVED, on_message_received); // 进入事件循环阻塞等待网络事件 while (1) { osip_event_t *evt osip_event_wait(osip_ctx, 0, 1000); if (evt NULL) continue; // 超时继续轮询 osip_handle_event(osip_ctx, evt); osip_event_free(evt); } osip_release(osip_ctx); return 0; }这段代码里osip_init负责创建事务管理器和解析器实例osip_set_udp_socket把 Osip 绑定到 5060 端口内部会创建 UDP socket 并加入 select 监听集合osip_set_callback注册的是OSIP_MESSAGE_RECEIVED事件也就是“完整收到一条 SIP 消息”后的回调。osip_event_wait的第二个参数是超时秒数第三个是微秒这里设成 1 秒超时避免死等导致无法响应退出信号。osip_handle_event是 Osip 内部状态机入口它会根据事件类型决定是解析消息、匹配事务还是触发回调。3.2 回调里到底拿到了什么on_message_received的签名通常是void on_message_received(int type, osip_transaction_t *tr, osip_message_t *msg)。很多人第一次写回调会直接去读msg-sip_method但更稳妥的做法是先判断事务类型void on_message_received(int type, osip_transaction_t *tr, osip_message_t *msg) { if (msg NULL) return; // 打印请求行或状态行确认是请求还是响应 if (MSG_IS_REQUEST(msg)) { printf(收到请求: %s\n, msg-sip_method); } else if (MSG_IS_RESPONSE(msg)) { printf(收到响应: %d %s\n, msg-status_code, msg-reason_phrase); } // 如果是 INVITE回 100 Trying 防止对方重传 if (MSG_IS_INVITE(msg)) { osip_message_t *resp NULL; osip_message_init(resp); osip_message_set_status_code(resp, 100); osip_message_set_reason_phrase(resp, Trying); // 这里需要根据事务上下文发送demo 里通常直接调用 osip_message_send osip_message_free(resp); } }关键点MSG_IS_REQUEST和MSG_IS_RESPONSE是 Osip 提供的宏比手动比较msg-sip_method更安全因为响应消息的sip_method字段可能为空。回 100 Trying 是 SIP 协议里的“后悔药”——INVITE 发出后对方没收到临时响应会重传回一个 100 能立刻止住重传风暴。但注意demo 里如果直接osip_message_send而不走事务层可能会绕过 Osip 的重传管理生产环境要用osip_transaction_send或 eXosip 的封装。3.3 消息释放的时机别在回调里 freeOsip 的内存模型是“谁创建谁释放但回调里的 msg 归 Osip 管”。如果你在on_message_received里调用osip_message_free(msg)大概率会触发 double free。正确做法是回调里只读不释放等osip_handle_event返回后Osip 内部会自己清理。如果你需要把消息存下来异步处理必须用osip_message_clone复制一份然后自己管理克隆体的生命周期。这个坑我见过不止一次现象是程序跑几分钟就 segfault用 valgrind 一查全是osip_message_free的重复调用。4. 发送侧拆解sipSendDemo 怎么构造并发出一个合法 INVITE4.1 构造请求从 osip_message_init 到 set_request_uri发送侧的核心是“手工拼一条 SIP 请求”。Osip 不提供类似osip_message_build_invite的高级 API所有头域都要自己填。一个最小可用的 INVITE 构造流程如下osip_message_t *invite NULL; osip_message_init(invite); // 设置请求行INVITE sip:bob192.168.1.100 SIP/2.0 osip_message_set_method(invite, INVITE); osip_message_set_sip_version(invite, SIP/2.0); osip_message_set_request_uri(invite, sip:bob192.168.1.100); // 必填头域Via、From、To、Call-ID、CSeq、Contact、Max-Forwards osip_message_set_via(invite, SIP/2.0/UDP 192.168.1.10:5060;branchz9hG4bK12345); osip_message_set_from(invite, Alice sip:alice192.168.1.10;tagabc); osip_message_set_to(invite, Bob sip:bob192.168.1.100); osip_message_set_call_id(invite, call-123456192.168.1.10); osip_message_set_cseq(invite, 1 INVITE); osip_message_set_contact(invite, Alice sip:alice192.168.1.10:5060); osip_message_set_max_forwards(invite, 70); // 设置 SDP 体否则对方可能回 488 Not Acceptable osip_message_set_body(invite, sdp_body, strlen(sdp_body)); osip_message_set_content_type(invite, application/sdp);这里每个头域都有讲究。Via里的branch参数必须以z9hG4bK开头否则对方可能不认这是 RFC 3261 事务From的tag是本地生成的随机串用于区分同一用户的不同呼叫Call-ID必须全局唯一demo 里常用时间戳加 IP 拼出来CSeq的序号在同一个 dialog 里必须递增否则对方会回 500。Max-Forwards设 70 是惯例防止环路。4.2 发送与事务匹配为什么发了没反应构造完消息后调用osip_message_send只是把字节流丢到 socket真正的“发送成功”要看事务层有没有收到响应。发送侧通常需要注册OSIP_TRANSACTION相关回调// 发送前先创建事务Osip 会根据 method 和 branch 匹配响应 osip_transaction_t *tr NULL; osip_transaction_init(tr, ICT, osip_ctx, invite); // 把事务加入 Osip 管理之后收到的响应会自动匹配到这个事务 osip_transaction_set_your_instance(tr, my_context); // 真正发送 osip_message_send(invite, tr); // 进入事件循环等响应 while (1) { osip_event_t *evt osip_event_wait(osip_ctx, 0, 1000); if (evt NULL) continue; osip_handle_event(osip_ctx, evt); osip_event_free(evt); }如果只调osip_message_send而不创建事务对方回的 200 OK 到了本地Osip 找不到匹配的事务会直接丢弃现象就是“抓包能看到响应但程序没反应”。这是新手最容易翻车的地方。ICT是 Invite Client Transaction 的缩写对应 INVITE 的客户端状态机如果是发 OPTIONS 或 BYE要用NICTNon-Invite Client Transaction。4.3 收到 200 OK 后必须回 ACKINVITE 事务的完整流程是INVITE → 1xx → 200 OK → ACK。如果收到 200 OK 后不回 ACK对方会周期性重传 200 OK直到超时拆线。ACK 的构造和普通请求类似但它是独立事务不走 ICTvoid on_response(int type, osip_transaction_t *tr, osip_message_t *msg) { if (msg-status_code 200) { osip_message_t *ack NULL; osip_message_init(ack); osip_message_set_method(ack, ACK); osip_message_set_request_uri(ack, msg-from-url-username); // 简化写法 // ACK 的 Via、From、To、Call-ID、CSeq 必须和 INVITE 一致 // 实际代码里通常从原 INVITE 克隆这些头域 osip_message_send(ack, NULL); // ACK 不需要事务 osip_message_free(ack); } }注意 ACK 的CSeq序号必须和 INVITE 相同但方法名是 ACK。很多 demo 在这里偷懒直接复用 INVITE 的消息体改方法名结果CSeq没改对对方回 400。稳妥做法是从事务上下文里取出原始 INVITE克隆头域后再改方法。5. 避坑与排查跑 Osip demo 时最常见的五个翻车现场5.1 现象编译报 undefined reference toosip_init原因链接顺序不对或者没链接libosip2。Osip 的库依赖是-losip2 -losipparser2如果用了 eXosip 还要加-leXosip2。解决在 Makefile 里把库放在源文件之后gcc sipRecvDemo.c -o sipRecvDemo -losip2 -losipparser2。如果还报错用pkg-config --libs libosip2看实际库名。5.2 现象程序启动后收不到任何包原因osip_set_udp_socket绑定的端口被占用或者防火墙拦了 UDP。解决先用netstat -anu | grep 5060确认端口没被占再用tcpdump -i any udp port 5060抓包看对方发的包有没有到网卡。如果抓到了但程序没反应检查osip_event_wait的超时参数是不是设成了 00 表示非阻塞会立刻返回 NULL。5.3 现象收到 INVITE 后回 100 Trying对方仍然重传原因100 Trying 没有正确关联到原事务对方认为你没收到。解决回临时响应必须用osip_transaction_send而不是osip_message_send并且事务上下文要从回调参数tr里取。直接osip_message_send会新建一个无状态发送对方匹配不上。5.4 现象程序跑几分钟后 segfault原因在回调里释放了 Osip 拥有的消息或者多线程同时操作同一个osip_ctx。解决回调里只读不 free如果要多线程每个线程用独立的osip_ctx或者加锁。用 valgrind 跑一遍看osip_message_free的调用栈基本能定位到重复释放的位置。5.5 现象发送 INVITE 后收到 488 Not Acceptable原因SDP 体缺失或格式不对。解决检查osip_message_set_body是否真的写入了 SDP以及Content-Type是不是application/sdp。SDP 里至少要有maudio行和artpmap行否则对方无法协商编解码。demo 里常用一段硬编码的 PCMU SDP直接抄过来改端口即可。6. 二次开发把 demo 改成能注册、能呼叫的最小软电话6.1 先加注册流程否则对方不知道你在哪原始 demo 只演示了收和发没有 REGISTER。真实软电话必须先向 SIP 服务器注册把自己的 Contact 地址告诉服务器。改造思路是在sipSendDemo里加一个send_register函数构造 REGISTER 请求To和From都填自己的 AOR如sip:alicedomain.comContact填实际 IP 和端口Expires设 3600。注册成功后服务器才知道往哪个地址转发 INVITE。void send_register(osip_t *ctx, const char *aor, const char *contact) { osip_message_t *reg NULL; osip_message_init(reg); osip_message_set_method(reg, REGISTER); osip_message_set_request_uri(reg, aor); osip_message_set_from(reg, aor); osip_message_set_to(reg, aor); osip_message_set_contact(reg, contact); osip_message_set_expires(reg, 3600); // Call-ID、CSeq、Via 按规则填 osip_message_send(reg, NULL); osip_message_free(reg); }注册成功后服务器会回 200 OK里面带Contact和Expires。如果回 401/407说明需要鉴权得从响应头里取WWW-Authenticate或Proxy-Authenticate用 MD5 算 response 再重发 REGISTER。这一步是二次开发里最耗时的部分因为鉴权算法涉及 nonce、cnonce、qop 等参数建议直接参考 Osip 自带的osip_www_authenticate解析函数。6.2 把收和发合并成一个事件循环两个 demo 分开跑只能自娱自乐。要变成软电话得把接收和发送合并到一个osip_ctx里用一个事件循环同时处理收包和事务超时。改造时注意osip_event_wait返回的事件可能是网络包也可能是事务定时器超时osip_handle_event会自动分发。你只需要在回调里根据MSG_IS_INVITE、MSG_IS_REGISTER、MSG_IS_BYE分别处理。6.3 验证方法用 tcpdump 和日志双确认改完之后怎么确认真的通了我一般同时开两个终端一个跑tcpdump -i any -A udp port 5060看原始 SIP 文本一个跑程序看日志。如果 tcpdump 里能看到完整的 INVITE 和 200 OK但程序日志没打印说明 Osip 解析失败检查消息头有没有缺Content-Length或Via的branch格式不对。如果 tcpdump 里根本没有包说明 socket 没绑上或者路由不对。6.4 一个容易忽略的细节SDP 里的 IP 要写对很多 demo 的 SDP 里cIN IP4 0.0.0.0对方收到后不知道往哪发 RTP。二次开发时要把c行改成实际网卡 IP或者用osip_message_set_body动态拼。如果机器有多张网卡建议在配置文件里指定media_ip别让程序自己猜。这个坑我踩过本机测试时0.0.0.0能通一到跨网段就单通抓包发现 RTP 发到了错误网卡。从那以后我每次改 Osip 相关代码都强制先跑一遍tcpdump确认信令面通了再去看媒体面。希望帮到你。本文还有配套的精品资源点击获取