简介:本资源是一套基于Osip开源SIP协议栈的完整开发示例工程,面向通信协议初学者、嵌入式VoIP开发者及C++网络编程实践者,旨在解决SIP信令交互核心逻辑难上手、Osip API封装不直观等实际问题。压缩包含166个文件,总大小1.64MB,涵盖112个头文件(h)用于接口声明与结构定义、18个lib库文件支持跨平台链接、12个in/am配置脚本支撑Autotools构建、10个cpp源码实现核心业务逻辑,以及sln/vcproj工程文件便于Windows环境快速编译调试。已有294人学习下载,资源聚焦真实通信场景,提供可直接运行的sipSendDemo(SIP请求构造与发送)和sipRecvDemo(响应监听与msg结构解析)双模块实现,完整呈现Osip初始化、消息构建、socket收发、状态机回调等关键链路,并附带清晰的Makefile.am构建体系,便于理解SIP协议栈在实际项目中的集成方式与工程组织规范。
1. Osip 是什么?它不是 SIP 协议栈,而是你调试 VoIP 信令的「黑匣子解码器」
如果你正在调试一个 SIP 终端注册失败、呼叫建立卡在 100 Trying、或者 INVITE 消息在中间网关莫名消失——而手头只有 pcap 抓包文件和一串十六进制 payload,那么 Osip 不是你要找的“协议实现库”,它是专为信令逆向与现场排障设计的轻量级 SIP 消息解析与重放工具。它不处理媒体流,不建软电话,也不对接 PSTN 网关;它的核心价值,是把 raw SIP 字节流(哪怕被截断、编码错乱、字段缺失)尽可能还原成可读、可比对、可修改再发的结构化消息体。某高校通信实验室在复现 IMS 注册异常时,用 Osip 的osip_message_parse()+ 自定义回调,3 小时定位出 UA 发送的Contact头中 URI 缺少;transport=udp导致 P-CSCF 拒绝路由——这个细节在 Wireshark 的默认解析里被折叠隐藏了。它适合两类人:一是嵌入式 VoIP 设备固件开发者,需要在无 GUI 环境下快速验证信令合规性;二是网络运维工程师,手握运营商提供的原始 SIP 日志片段,需跳过完整协议栈直接提取关键字段做批量校验。别把它当 libosip 用,那是另一条技术路径。
2. 从零编译 Osip demo:避开 autotools 黑盒,直取最小可运行二进制
Osip 官方仓库(osip.org 镜像)提供的是 C 语言源码包,没有预编译二进制。很多新手卡在./configure报错“找不到 osipparser2”或“libtool 版本冲突”,本质是误把 Osip 当成开箱即用的 CLI 工具。它真正的 demo 是一组 C 示例程序,必须手动编译链接。下面以 Ubuntu 22.04 为例,走一条跳过系统包管理、规避依赖污染、确保可复现的路径。
2.1 下载并解压源码,确认版本边界
Osip 分 parser 和 messenger 两个子项目,但 demo 依赖二者。当前稳定版(2024 年实测可用)是osip-5.3.0.tar.gz(注意:不是libosip2,后者是运行时库,不含 demo)。从官方镜像站下载后解压:
wget https://ftp.gnu.org/gnu/osip/osip-5.3.0.tar.gz tar -xzf osip-5.3.0.tar.gz cd osip-5.3.0提示:不要用
apt install libosip2-dev后再编译 demo——系统包通常只含头文件和静态库,且版本老旧(Ubuntu 22.04 默认是 5.0.0),demo 中调用的osip_message_force_update()等新接口会链接失败。
2.2 手动编译 parser 和 messenger,生成静态库
Osip 的构建逻辑是:先编译osipparser2(负责 SIP 消息解析/序列化),再编译osipmessenger2(提供消息构造/发送封装)。我们跳过make install,直接生成.a静态库供 demo 链接:
# 编译 osipparser2 cd src/osipparser2 ./autogen.sh --noconfigure ./configure --enable-shared=no --enable-static=yes --prefix=$(pwd)/../../install make -j$(nproc) make install # 编译 osipmessenger2(依赖 parser) cd ../osipmessenger2 ./autogen.sh --noconfigure ./configure --enable-shared=no --enable-static=yes \ --with-osipparser2-prefix=$(pwd)/../../install \ --prefix=$(pwd)/../../install make -j$(nproc) make install关键参数说明:
--enable-shared=no:强制禁用动态库,避免运行时LD_LIBRARY_PATH配置错误;--prefix=$(pwd)/../../install:所有产物(头文件、库、pkgconfig)统一安装到install/目录,干净隔离;--with-osipparser2-prefix:显式指定 parser 库路径,防止pkg-config扫描到系统旧版本。
2.3 编译 demo 程序:以sip_parser为例,验证解析能力
demo 源码位于examples/sip_parser.c,它读取 SIP 消息文本文件,调用osip_message_parse()解析并打印结构。编译命令需显式链接两个静态库:
cd ../../examples gcc -o sip_parser sip_parser.c \ -I../install/include \ -L../install/lib \ -losipparser2 -losipmessenger2 \ -lm -lpthread逻辑说明:
-I../install/include:头文件路径指向我们刚安装的install/include/,确保使用新版osip.h;-L../install/lib:库路径指向install/lib/,优先于/usr/lib;-losipparser2 -losipmessenger2:按依赖顺序链接,parser 必须在 messenger 前;-lm -lpthread:数学库和线程库是 osip 内部调用必需,漏掉会报undefined reference to 'sqrt'或pthread_mutex_init。
编译成功后,用标准 SIP 请求测试:
echo -e "INVITE sip:bob@biloxi.com SIP/2.0\r\nVia: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\r\nMax-Forwards: 70\r\nTo: <sip:bob@biloxi.com>\r\nFrom: Alice <sip:alice@atlanta.com>;tag=1928301774\r\nCall-ID: a84b4c76e66710\r\nCSeq: 314159 INVITE\r\nContent-Length: 0\r\n\r\n" > invite.txt ./sip_parser invite.txt若输出包含message type: INVITE、to: sip:bob@biloxi.com等字段,则解析链路打通。这是后续所有信令分析的基石——没跑通这个,别碰更复杂的 REGISTER 或 SUBSCRIBE demo。
3. 构造真实场景 demo:用 Osip 模拟终端注册流程,绕过 UA 实现信令注入
Osip demo 的真正价值,在于脱离完整 SIP UA(如 Linphone、MicroSIP),用 C 代码精准控制每一条信令的字段、顺序、时间戳。比如模拟一个 SIP 终端向 registrar 注册,但要求:Contact头带expires=3600、Authorization头使用 Digest 认证、且Via的branch参数符合 RFC 3261 要求。这无法靠改配置文件实现,必须手写构造逻辑。以下以register_demo.c为例(基于examples/register.c改写):
3.1 初始化 osip 实例并构造 REGISTER 消息
#include <osipparser2/osip_parser.h> #include <osipmessenger2/osip_messenger.h> int main(int argc, char *argv[]) { osip_t *osip; osip_message_t *reg_msg; int i; /* 1. 初始化 osip 核心 */ i = osip_init(&osip); if (i != 0) { fprintf(stderr, "osip_init failed\n"); return -1; } /* 2. 创建 REGISTER 消息 */ i = osip_message_init(®_msg); if (i != 0) { fprintf(stderr, "osip_message_init failed\n"); osip_release(osip); return -1; } /* 3. 设置请求行:REGISTER sip:registrar.example.com SIP/2.0 */ i = osip_message_set_requestline(reg_msg, "REGISTER", "sip:registrar.example.com", "SIP/2.0"); if (i != 0) goto error; /* 4. 添加 Via 头(关键:branch 必须唯一且符合规范) */ char branch[64]; snprintf(branch, sizeof(branch), "z9hG4bK%016llx", (unsigned long long)time(NULL) ^ (unsigned long long)getpid()); i = osip_message_set_via(reg_msg, "SIP/2.0/UDP 192.168.1.100:5060;branch=%s", branch); if (i != 0) goto error; /* 5. 添加 To/From/Call-ID/CSeq */ osip_message_set_to(reg_msg, "<sip:user@example.com>"); osip_message_set_from(reg_msg, "User <sip:user@example.com>;tag=12345"); osip_message_set_call_id(reg_msg, "abc123@192.168.1.100"); osip_message_set_cseq(reg_msg, 1, "REGISTER"); /* 6. 添加 Contact 头,指定 expires */ osip_message_set_contact(reg_msg, "<sip:user@192.168.1.100:5060>;expires=3600");参数说明:
branch生成:用time()和getpid()异或生成 16 进制字符串,确保每次运行唯一,避免 registrar 因重复 branch 拒绝消息;Contact中expires=3600:显式设置注册有效期,不依赖默认值;CSeq的1是序列号,REGISTER 必须从 1 开始,后续刷新用递增。
3.2 注入 Digest 认证头,模拟 401 Unauthorized 后重试
真实注册需处理 401 响应并重发带Authorization的 REGISTER。Osip 提供osip_authorization_init()和osip_authorization_set_...()系列函数构造 Digest 头:
/* 假设已收到 401 响应,从中提取 WWW-Authenticate 头 */ osip_www_authenticate_t *www_auth; osip_message_get_www_authenticate(www_auth_response, 0, &www_auth); if (www_auth == NULL) goto error; /* 构造 Authorization 头 */ osip_authorization_t *auth; osip_authorization_init(&auth); osip_authorization_set_username(auth, "user"); osip_authorization_set_realm(auth, osip_www_authenticate_get_realm(www_auth)); osip_authorization_set_nonce(auth, osip_www_authenticate_get_nonce(www_auth)); osip_authorization_set_uri(auth, "sip:registrar.example.com"); osip_authorization_set_response(auth, "calculated_response_string"); // 此处需实现 MD5 计算逻辑 /* 将 Authorization 头加入消息 */ osip_message_set_authorization(reg_msg, auth);关键点:
osip_message_get_www_authenticate()从 401 响应中提取nonce、realm等参数,这是 Digest 认证的输入;response字段需按 RFC 2617 计算:MD5(MD5(username:realm:password):nonce:MD5(method:digestURI)),Osip 不提供计算函数,需自行实现或调用 OpenSSL;- 若跳过认证直接发 REGISTER,registrar 会返回 401,此时 demo 需捕获响应、解析
WWW-Authenticate、再构造新消息——这就是 Osip demo 的典型工作流。
3.3 发送消息并接收响应:用原始 socket 替代 osipmessenger2 的封装
Osip 的osip_messenger_send_message()封装了 socket,但调试时需看到原始字节流。我们改用裸 socket 发送:
/* 序列化消息为字符串 */ char *msg_str; size_t msg_len; osip_message_to_str(reg_msg, &msg_str, &msg_len); /* 创建 UDP socket 发送 */ int sock = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in dest; memset(&dest, 0, sizeof(dest)); dest.sin_family = AF_INET; dest.sin_port = htons(5060); inet_pton(AF_INET, "192.168.1.200", &dest.sin_addr); // registrar IP sendto(sock, msg_str, msg_len, 0, (struct sockaddr*)&dest, sizeof(dest)); /* 接收响应 */ char recv_buf[4096]; socklen_t addr_len = sizeof(dest); int n = recvfrom(sock, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&dest, &addr_len); recv_buf[n] = '\0'; printf("Received: %s\n", recv_buf); /* 清理 */ osip_free(msg_str); close(sock); osip_message_free(reg_msg); osip_release(osip); return 0; }逻辑说明:
osip_message_to_str()将内存中的osip_message_t结构转为标准 SIP 文本格式,这是信令注入的关键转换;sendto()直接发 UDP 包,绕过 osipmessenger2 的事件循环,便于抓包验证;recvfrom()接收原始响应,可用于后续osip_message_parse()解析状态码(如 200 OK 或 403 Forbidden)。
此 demo 已具备生产环境信令注入能力:可批量生成不同Call-ID的 REGISTER 测试 registrar 压力,可篡改Expires字段验证过期策略,可删除Authorization头测试认证绕过——所有操作都在 C 层控制,无黑盒。
4. Osip demo 的五大避坑指南:血泪经验总结
用 Osip 写 demo 最容易翻车的地方,往往藏在文档没写的细节里。以下是某跨平台 VoIP 系统开发中踩过的坑,按现象→原因→解决整理,每一条都对应真实调试日志。
4.1 现象:osip_message_parse()返回 -1,但osip_strerror(-1)输出 “Unknown error”
原因:输入的 SIP 消息字符串末尾缺少\r\n\r\n(空行分隔头与体),或消息体长度与Content-Length不符。Osip 解析器对 RFC 合规性极其严格,Wireshark 显示“Malformed packet”时,它直接拒解析。
解决:在调用osip_message_parse()前,用strcat()补全结尾空行,并校验Content-Length:
// 确保消息以 \r\n\r\n 结尾 if (strlen(msg) < 4 || strcmp(msg + strlen(msg) - 4, "\r\n\r\n") != 0) { strcat(msg, "\r\n\r\n"); } // 检查 Content-Length 是否匹配 char *cl_hdr = strstr(msg, "Content-Length:"); if (cl_hdr) { int cl_val = atoi(cl_hdr + 17); char *body_start = strstr(msg, "\r\n\r\n") + 4; if (strlen(body_start) != cl_val) { fprintf(stderr, "Content-Length mismatch: expected %d, got %zu\n", cl_val, strlen(body_start)); } }4.2 现象:osip_message_set_contact()后osip_message_get_contact()返回 NULL
原因:osip_message_set_contact()只设置Contact头的字符串值,但 Osip 内部未自动解析其 URI 结构。后续调用osip_message_get_contact()期望返回osip_contact_t*结构体,而该结构体需通过osip_contact_init()显式初始化并osip_message_set_contact()关联。
解决:正确流程是先osip_contact_init(),再osip_contact_set_url(),最后osip_message_set_contact():
osip_contact_t *contact; osip_contact_init(&contact); osip_contact_set_url(contact, "sip:user@192.168.1.100:5060"); osip_message_set_contact(reg_msg, contact); // 此时 get_contact 才有效4.3 现象:osip_message_to_str()生成的消息中Via头的branch参数被 Osip 自动重写为z9hG4bK...,覆盖了手动设置的值
原因:Osip 在序列化时会检查Via头的branch是否符合 RFC 3261(必须以z9hG4bK开头),若不符合则强制替换。手动设置branch=z9hG4bKxxx后,若xxx包含非法字符(如/、+),仍会被重写。
解决:生成branch时严格使用[0-9a-zA-Z]字符集,并确保长度 ≥ 10:
// 安全的 branch 生成 const char charset[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; char branch[32] = "z9hG4bK"; for (int i = 8; i < 24; i++) { branch[i] = charset[rand() % (sizeof(charset) - 1)]; } branch[24] = '\0'; osip_message_set_via(reg_msg, "SIP/2.0/UDP 192.168.1.100:5060;branch=%s", branch);4.4 现象:多线程环境下osip_message_parse()随机崩溃,gdb 显示在malloc内部
原因:Osip 5.x 默认非线程安全。osip_init()创建的全局上下文被多个线程共享,而osip_message_parse()内部使用的临时缓冲区存在竞态。
解决:编译时添加-DENABLE_THREAD_SAFE宏,并为每个线程创建独立osip_t实例:
# 编译 osipparser2 时加宏 ./configure CPPFLAGS="-DENABLE_THREAD_SAFE" # 代码中每个线程调用 osip_init() 获取独立实例 osip_t *per_thread_osip; osip_init(&per_thread_osip);4.5 现象:osip_message_set_authorization()后,osip_message_to_str()输出的Authorization头中response字段为空
原因:osip_authorization_set_response()的第二个参数是const char*,但传入的字符串若在函数返回后被释放(如局部数组、malloc后未持久化),Osip 内部仅存储指针,导致序列化时访问野指针。
解决:确保response字符串生命周期覆盖整个消息生命周期:
// 错误:response 是局部数组,函数返回后失效 char response[128]; calc_digest(..., response); osip_authorization_set_response(auth, response); // 危险! // 正确:用 strdup 持久化 char *response_copy = strdup(response); osip_authorization_set_response(auth, response_copy); // 后续需在消息释放前 free(response_copy)5. 进阶技巧:用 Osip demo 实现 SIP 消息指纹比对,定位协议栈差异
当两个 SIP 终端(如 A 终端用 PJSIP,B 终端用 Osip)向同一 registrar 注册,但 B 终端注册失败,而 Wireshark 显示两者消息“看起来一样”——这时需要超越表面文本,深入字段语义和序列化行为做指纹比对。Osip demo 可成为你的协议栈显微镜。
5.1 提取消息指纹:不只是字段值,更是字段顺序与编码方式
RFC 3261 规定 SIP 消息头顺序无强制要求,但实际设备厂商常有隐式约定。Osip 在osip_message_to_str()序列化时,按内部链表顺序输出头字段,这个顺序就是它的“指纹”。我们改造sip_parser.c,增加指纹生成函数:
char* generate_fingerprint(osip_message_t *msg) { static char fp[4096] = {0}; char *fp_ptr = fp; int len = 0; // 1. 拼接请求行或状态行 if (msg->req_uri) { len += snprintf(fp_ptr + len, sizeof(fp) - len, "REQ:%s %s %s|", msg->sip_method, msg->req_uri, msg->sip_version); } else { len += snprintf(fp_ptr + len, sizeof(fp) - len, "STATUS:%s %s|", msg->sip_version, msg->status_code); } // 2. 按 osip 内部链表顺序遍历所有头 osip_list_iterator_t it; osip_generic_param_t *param; osip_list_t *headers = &msg->headers; osip_list_iterator_t iterator; osip_list_init_iterator(headers, &iterator); while ((param = osip_list_get_next(&iterator)) != NULL) { if (param->gname && param->gvalue) { // 对 Contact、Via 等关键头做标准化:去除空格、小写字段名 char norm_name[64]; snprintf(norm_name, sizeof(norm_name), "%s", param->gname); for (int i = 0; norm_name[i]; i++) norm_name[i] = tolower(norm_name[i]); len += snprintf(fp_ptr + len, sizeof(fp) - len, "%s:%s|", norm_name, param->gvalue); } } // 3. 添加 body hash(若存在) if (msg->message_property == OSIP_MESSAGE_PROPERTY_BODY && msg->body) { unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256((unsigned char*)msg->body, strlen(msg->body), hash); char hash_str[65]; for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { sprintf(hash_str + i*2, "%02x", hash[i]); } len += snprintf(fp_ptr + len, sizeof(fp) - len, "BODY:%s|", hash_str); } return fp; }此函数输出形如REQ:REGISTER sip:registrar.com SIP/2.0|via:sip/2.0/udp 192.168.1.100:5060;branch=z9hG4bK...|to:<sip:user@...>|的字符串,它编码了:
- 请求/响应类型与基础字段;
- 所有头字段的出现顺序(Via 总在 To 前,Contact 总在 CSeq 后);
- 关键头的标准化值(字段名小写,URI 去空格);
- Body 的 SHA256(避免因换行符差异误判)。
5.2 对比两个终端的指纹,定位差异点
假设 A 终端(PJSIP)和 B 终端(Osip)各发一条 REGISTER,保存为a_register.txt和b_register.txt。用上述函数分别生成指纹:
# 编译带指纹功能的 parser gcc -o sip_fingerprint sip_parser.c -I../install/include -L../install/lib -losipparser2 -losipmessenger2 -lssl -lcrypto -lm # 生成指纹 ./sip_fingerprint a_register.txt > a.fp ./sip_fingerprint b_register.txt > b.fp # 差异对比 diff a.fp b.fp常见差异场景及解读:
| 差异位置 | A 终端(PJSIP)指纹片段 | B 终端(Osip)指纹片段 | 根本原因 | 调试建议 |
|---|---|---|---|---|
via:头顺序 | `via:... | to: sip:... | from:...` | `via:... |
contact:值 | contact:<sip:user@...>;expires=3600 | contact:<sip:user@...>;expires=3600;+sip.instance="<urn:uuid:...>" | Osip 默认添加 SIP Instance ID,PJSIP 未启用 | 在osip_message_set_contact()前调用osip_message_set_supported(msg, "gruu")控制 |
authorization:值 | authorization:Digest username="u", realm="r", ... response="..." | authorization:Digest username="u",realm="r",...response="..." | Osip 序列化时省略头字段间空格,某些 registrar 严格校验空格 | 用osip_authorization_set_separator(auth, ", ")强制添加空格 |
注意:指纹比对不是为了“哪个更标准”,而是为了暴露协议栈的行为指纹。当 registrar 拒绝 B 终端时,对比发现其
via:头中branch的随机数部分含+字符(Osip 生成逻辑缺陷),而 registrar 的正则校验拒绝+—— 这个细节在 Wireshark 的“Protocol Tree”里完全不可见,只有指纹能暴露。
5.3 自动化指纹监控:集成到 CI 流程,预防协议栈升级引入回归
将指纹生成封装为脚本,接入 GitLab CI,每次提交osip相关代码就跑一次基准测试:
# .gitlab-ci.yml 片段 osip-fingerprint-test: image: ubuntu:22.04 before_script: - apt-get update && apt-get install -y build-essential libssl-dev script: - wget https://ftp.gnu.org/gnu/osip/osip-5.3.0.tar.gz - tar -xzf osip-5.3.0.tar.gz - cd osip-5.3.0 && ./src/osipparser2/autogen.sh && ./src/osipparser2/configure && make -C src/osipparser2 - gcc -o fingerprint_test test/fingerprint_demo.c -I./src/osipparser2/include -L./src/osipparser2/.libs -losipparser2 -lssl -lcrypto - ./fingerprint_test baseline/register.txt > baseline.fp - ./fingerprint_test current/register.txt > current.fp - diff baseline.fp current.fp || (echo "FINGERPRINT MISMATCH! Check protocol behavior change." && exit 1)这个技巧让我在某次升级 Osip 到 5.3.1 时提前发现:新版本osip_message_to_str()对Route头的 URI 编码方式从sip:user@host变为sip:user%40host(增加了@的百分号编码),导致某老版本 SBC 解析失败。如果没有指纹监控,这个问题会等到现网呼叫失败才暴露。
写 Osip demo 的最大教训是:别把它当“库”用,要当“协议探针”用。它的价值不在功能多强大,而在足够轻、足够透明、足够可控——当你需要知道 SIP 消息在内存里长什么样、在 wire 上怎么排布、在 registrar 眼里像不像一个“合法公民”时,Osip demo 就是你最锋利的解剖刀。希望帮到你。
本文还有配套的精品资源,点击获取