简介:MSRP(Message Session Relay Protocol)是SIP会话中承载富媒体内容的重要扩展,这套C++源码以轻量实现展示了SIP客户端如何集成MSRP协议,适合VoIP、即时通信及SIP协议栈开发者作为参考样例。压缩包内共46个文件,解压后约56KB,其中23个.h头文件与16个.cpp源文件构成主体,并附带Makefile、vcproj工程文件以及说明文档,便于在Linux或Windows工程环境中直接编译研究。具体代码覆盖MSRP URI解析、报文头部与消息体封装、TCP连接管理、状态与报告处理等关键模块;协议栈按连接、地址、消息、报告等模块拆分组织,基于SIP INVITE协商完成后,通过SETUP/SEND/TEARDOWN方法完成通道建立、消息传输与会话释放,从URI构造、SEND消息分片到成功/失败报告解析均有实现可对照,逻辑路径清晰,基本构成一套可学习的MSRP协议栈雏形。目前已有135人访问学习,适合期望快速理解SIP与MSRP交互细节、需要构建客户端协议模块的开发者。
1. 把 MSRP 塞进 SIP 会话里:这个 zip 到底解决什么问题
MSRP 全称 Message Session Relay Protocol,是跑在 SIP 信令之上的一条消息/文件传输通道。很多从业者第一次见到它,是在 IMS 里的即时消息、企业通讯录推送、或者语音网关的“大容量留言”场景——SIP INVITE 里带一段 m=message 的 SDP,协商完就建起一条 TCP 连接,消息以 MSRP SEND 请求的方式在这条连接上交换。标题里这个 “MSRP.zip_SIP Client MSRP” 压缩包,本质就是一份可以直接落地的 SIP MSRP 客户端实现:解压后既能当信令模块用,也能把 MSRP 收发消息的细节封装好,省去你从零啃 RFC 4975 的功夫。适合谁?做 SIP 终端、IMS 应用、企业 IM 网关、或者给海康类平台做 SIP 对接的工程师都适用。它能帮你少踩 TCP 分片、Byte-Range 拼接和 SDP 协商这三种最容易翻车的坑。
2. 拆开 zip 看门道:SIP 信令里那段 SDP 就是 MSRP 的“会话合同”
拿到这个MSRP.zip压缩包,别急着编译。先把它当成一个SIP 信令服务器源码 + 消息客户端的组合来读,你才能理解为什么会有那么多文件名里重复 msrp、sip。我一般先解压到一个固定目录,比如/opt/msrp_client/,然后按“信令层 → 消息层 → 传输层”的顺序看代码。大多数打包的 SIP Client 都会有一个负责 INVITE 事务的模块,一个负责 MSRP 消息收发的模块,还有一个管理 TCP 连接的 transport 模块。如果你打开源码发现这三个模块是揉在一个文件里的,建议先花时间拆清楚,否则后面调试字节偏移会非常痛苦。
2.1 SDP 协商:m=message 一行决定了你的 MSRP 会话怎么建
MSRP 会话不是凭空出现的,它必须由 SIP 信令先“牵线”。客户端发起 INVITE 时,SDP 里不再是 audio/video,而是这样一段:
m=message 9000 TCP/MSRP a=accept-types:text/plain message/cpim a=path:msrp://192.168.1.10:9000/abcdefgh;tcp a=setup:active a=connection:new这段 SDP 的含义:m 行声明了传输协议是 TCP/MSRP,端口 9000;a=path 是 MSRP 层的 URI,末尾的;tcp表示用 TCP 承载;a=setup 则告诉对端谁主动建 TCP 连接。逻辑说明:setup:active表示本端是主动连接方,对端应答时通常会回setup:passive,于是本端立刻向 a=path 里的 IP 和端口发起 TCP 连接。参数说明:accept-types 决定了这条会话能传什么类型,如果对端不支持你列出的类型,会回 488 Not Acceptable Here,所以别把一种类型写死,常用做法是写text/plain message/cpim。
你可能会遇到一种情况:INVITE 里没带 m=message,只带了 audio。那 MSRP 根本起不来,终端会一直等对端发 183。很多人在这里踩坑——拿普通 SIP 话机调试 MSRP 客户端,结果话机只协商 audio,消息自然发不出去。所以拿到这个 zip 后第一件事,是确认它的 INVITE 构造代码里 SDP 是硬编码 m=message,还是开放了接口让你自己填。
2.2 MSRP 消息格式:SEND 请求和 200 响应是怎么对上号的
SDP 协商完成后,两条 TCP 连接上跑的就是 MSRP 消息了。MSRP 请求长这样:
MSRP a54h9 SEND To-Path: msrp://192.168.1.20:9000/abcdef;tcp From-Path: msrp://192.168.1.10:9000/abcdefgh;tcp Message-ID: 1234567890 Byte-Range: 1-100/100 Content-Type: text/plain Hello, MSRP! -------a54h9$关键点说明:第一行MSRP a54h9 SEND是方法行,a54h9是事务标识,对端回复时会把 SEND 换成200 OK;Byte-Range 的1-100/100表示这是唯一分片,总共 100 字节、当前是第 1 到第 100 字节;消息尾部用-------a54h9$表示结束,如果还有后续分片,则用-------a54h9+来告诉对端“更多分片要来了”。这个尾标记是新手最容易搞错的地方:$和+差一个字符,写错的话对端会一直等后续分片,直到 TCP 超时。
我建议拿到 zip 后先不要看它的 SEND 构造代码,而是用 tcpdump 抓一次真实会话,把上面这段结构在抓包里对照一遍。这个zip 里如果有示例抓包文件,优先看那一个,比读十遍代码都有用。
2.3 最小可复现:解压、编译、跑通一次 MSRP 会话的命令
这个 zip 里的客户端如果带 Makefile 或 CMakeLists,编译路径一般是这样:
cd /opt/msrp_client && unzip MSRP.zip ls -la # 看有没有 README 或 config.example ./configure --with-sip-stack=pjsip && make -j4逻辑说明:第一行把压缩包解压到固定目录;configure这一步常见做法是让你选择 SIP 信令栈——标题里这个客户端通常有 PJSIP 和自带轻量栈两个选项,我一般选 PJSIP,因为它的事务状态机和重传处理比裸写稳定得多。参数说明:如果你只做消息传输、不注册到服务器,可以用--disable-register之类选项关掉注册模块,减少出问题的面。编译完成后,跑一个自带的演示程序,通常是这样的命令:
./msrp_client -l 9000 -s sip:msrp-test@192.168.1.20如果你不先看 README 就乱跑,大概率会在“缺少编译选项”和“运行时找不到配置文件”上浪费半小时。别问我怎么知道的。
3. 从零写一个能用的 MSRP SIP Client:选型、信令时序和最小实现
如果你不想依赖 zip 里那份现成代码,或者你手里的 zip 只提供了库文件而没有源码,那完全可以自己写一个最小实现。方向上有两条路:一是基于 PJSIP 的 pjsua 或 pjsip 高层 API 改;二是只依赖一个 SIP 事务栈,自己拼 SDP 和 MSRP 消息。对绝大多数人,我推荐前者,因为 PJSIP 已经把 SIP 重传、认证摘要、事务超时这些都处理好了,你只需要关系 MSRP 消息本身。用裸栈听上去很“硬核”,但光是一个 TCP 半开连接的重连逻辑,就够你调一整天。
3.1 选型:为什么我不用 SIPp 而用 PJSIP 扩展一个 Client
很多人测试 SIP 的时候会想到 SIPp。但 SIPp 对 MSRP 的支持非常有限,它擅长的是 INVITE、REGISTER 这类标准信令压测,到了 MSRP 消息层,SIPp 根本不会帮你拼接 SEND 请求和处理Byte-Range。所以“SIP 信令服务器源码”可以在你自己的项目里参考,但客户端这边,我常见做法是直接用 PJSIP 的pjsua_call回调,在on_media_update之后自行管理一条 TCP socket。这样做的好处是:信令仍然由 PJSIP 负责,你只写 MSRP 部分的几百行代码,工作量小、坑也少。zip 里那份实现如果也是这么组织的,那它的架构就是干净可借鉴的。
3.2 最小时序:INVITE → 183 → 200 → ACK → SEND
一次 MSRP 客户端完整会话的时序是这样的:
- 客户端 A 发 INVITE,SDP 携带 m=message;
- 对端 B 回 183 Session Progress,SDP 里写入自己的 msrp URI;
- 客户端 A 确认 B 的 a=setup 值,如果自己是 active 就往 B 的 TCP 端口建连接;
- B 回 200 OK,A 发 ACK;
- TCP 连接建立后,A 发 MSRP SEND,B 回 MSRP 200 OK;
- 需要关闭时,A 发 BYE。
很多刚上手的工程师会在第 3 步翻车:他们以为 183 就代表 TCP 连接已建好,实际 183 只是信令层的答复,TCP 连接要等你自己去 connect。如果 a=setup 写的是 passive,那就要等对端主动连你,结果对端也没来连,两边干等,这就是经典的“黑匣子”问题。调试方法很简单,在代码里把每步的状态打点打到日志里,看看卡在哪个阶段。
3.3 最小 Python 脚本:不依赖任何库先跑通 MSRP SEND
如果你只是想验证 MSRP 消息拼接逻辑,不一定要先编译整个 C 工程。我用 Python 写过一个最小模拟器,核心就是对 TCP socket 做一次原始的 MSRP 请求交换:
import socket MSRP_MSG = ( "MSRP 1a2b3 SEND\r\n" "To-Path: msrp://192.168.1.20:9000/abcdef;tcp\r\n" "From-Path: msrp://192.168.1.10:9000/abcdefgh;tcp\r\n" "Message-ID: 9876543210\r\n" "Byte-Range: 1-12/12\r\n" "Content-Type: text/plain\r\n" "\r\n" "hello msrp\r\n" "-------1a2b3$\r\n" ) sock = socket.create_connection(("192.168.1.20", 9000), timeout=5) sock.sendall(MSRP_MSG.encode()) resp = sock.recv(4096) print(resp.decode()) sock.close()逻辑说明:这段脚本构造了一个完整的 MSRP SEND 帧,然后连到对端的 MSRP 端口 9000。对端如果是合法 MSRP 服务端,会返回MSRP 1a2b3 200 OK,头部里会带上原事务 ID,表示消息已接收。参数说明:Byte-Range: 1-12/12中的 12 必须和hello msrp的字节数一致——注意末尾有一个换行符,实际长度是 11 个字符加 1 个 CRLF,所以写 12 是安全的。如果你分片数写错,对端会回复413或直接不回。这个脚本最大的价值是帮你把 MSRP 帧结构彻底吃透,之后再去看 C 工程代码,一眼就能看出它哪里写错。
4. 从 demo 到生产:IMS Trunk 与语音平台对接要调的 5 个 MSRP 参数
跑通 demo 只是第一步。真要把它用到set sip voice trunk ims on router这类网关注册场景,或者跟海康平台做 SIP 对接时,有 5 个参数你必须逐一校一遍。这些参数在 zip 里通常散落在配置文件中,默认值往往和实际网络环境不匹配,直接套用就会遇到莫名其妙的消息丢失。
| 参数 | 默认常见值 | 生产建议值 | 说明 |
|---|---|---|---|
| a=setup | active | 听 SDP 对端决定 | 两端都 active 会导致连接重入 |
| Byte-Range 分片大小 | 无 | 1024 或 2048 字节 | 大消息不切片会被中间网元丢弃 |
| MSRP keepalive | 无 | 每 30 秒发一个空 SEND | 防止 NAT 和运营商超时断连 |
| 最大消息大小 | 无 | 按平台规格设,常见 5MB | 超过后会回 413 拒绝 |
| TLS 模式 | tcp | msrps | 明文 TCP 在跨网段时容易被丢 |
4.1 a=path 和路由:谁决定 MSRP 消息往哪走
MSRP 的 SDP 里a=path有两个作用:一是告诉对端你愿意接收消息的 URI,二是作为 SIP 消息路由的补充。在 IMS 场景里,S-CSCF 会通过 Path 头来定位用户归属,但 MSRP 的连接是端到端直连的,中间最多经过一个 MRF 或 SBC 做中继。如果你在网关注册后发现 MSRP 消息能发出去但收不到,八成是 a=path 里写的地址是内网 IP,对端回包打到内网直接被防火墙丢弃。常见做法是:在 SDP 里填公网可达的地址,并且把端口固定下来,不要用 ephemeral 端口。
4.2 大消息分片:为什么大于 4KB 的消息会被静默丢弃
很多 MSRP 客户端在发送超过 4KB 的消息时会突然失败,这是因为 TCP 层会分段,但 MSRP 层面要求你在一个 SEND 帧里明确声明Byte-Range。如果你把整个 10MB 的消息塞进一个 SEND 帧,中间网元可能因为缓冲区溢出直接断开 TCP 连接。正确做法是应用层把消息切成 1KB–2KB 的块,每个块发一个 SEND 请求,第一块的 Byte-Range 写1-n/total,中间块尾部用+,最后一块用$。我在实际对接中常用 1024 字节分片,这样既能避开 MTU 问题,也不会因为分片太多导致 CPU 消耗过高。
4.3 NAT 穿透与 keepalive:别让 TCP 连接被静默回收
MSRP 使用长连接,在 NAT 后面的客户端如果长时间不发数据,NAT 映射会老化,对端的 TCP 连接状态也会被中间设备强制清理。最直接的现象就是:消息发出去后一直收不到 200,客户端超时后重发,又被对端以事务冲突拒绝。要解决这个问题,除了在 SBC 上开 rport 保持绑定,客户端自身也必须周期性发送 keepalive。MSRP 没有专门的 keepalive 方法,常见做法是发一个带空消息体的 SEND,头部的 Message-ID 每次变化,对端收到后回 200,这样连接就被续期一次。我一般把间隔设在 25 到 30 秒,比运营商 NAT 老化的典型 60 秒留出一倍余量。
4.4 认证与 TLS:msrps 端口的选择和证书坑
真实网络里 MSRP 基本不会跑明文 TCP。SDP 中的 m 行如果写成TCP/MSRP就是明文,TCP/MSRPS才是 TLS。在 IMS 环境中,虽然信令走 SIP over TLS,媒体层面同样要求 MSRP over TLS。这时会引入一个常见坑:客户端用自签名证书连对端,对端校验失败直接握手断掉。我建议在你的on_establish回调里先打印对端证书指纹,再决定要不要做深度校验;如果是在小范围试点,可以把对端 CA 加进本地信任库,而不是全局关闭验证,否则后面安全审计会让你返工。
4.5 与既有系统对接:海康平台、IMS Trunk 与 zip 里的配置模板
如果你是要做“海康平台 SIP 对接配置”这类工作,往往不只是调 MSRP,还要关注 SIP 注册、Keepalive 和呼叫流程。zip 里一般会附带若干平台对接配置模板,命名可能是config.platform.example,里面已经写好了常见的海康、华为 IMS 平台的参数。我第一次做这类对接时,犯过一个很傻的错误:直接把模板里的setup:passive改成了active,结果两边互相等待对方建连。后来养成了习惯:配置文件里每个参数都记下来源,是平台文档里来的,还是模板自带的,这样出了问题能往回查,不用把责任归给“玄学”。
5. 避坑:MSRP 客户端调试里最常见的 5 个翻车现场
5.1 现象:INVITE 发出后一直收不到 183,客户端界面永远“呼出中”
原因:对端是普通 SIP 终端或旧版软交换,只支持 audio,不支持 message。INVITE 里的 m=message 被对端忽略,或者直接被回 488。解决:在抓包确认 SDP 中 m 行确实存在且accept-types包含对端能识别的内容类,然后查对端日志是否显示拒绝原因。另一个隐蔽原因是 zip 里附带的客户端默认把 INVITE 的 Content-Disposition 写成了session,而正确写法是render——这也能导致部分平台不识别。
5.2 现象:大文件发送时,对端只收到第一块,后续分片全部丢失
原因:Byte-Range 的尾标记写错了。第一块用+结尾,最后一块用$;如果所有块都用$,对端在收完第一块后以为事务结束,后续 SEND 被视为新事务,但因为 Message-ID 一致,对端会直接忽略。解决:在代码里把分片函数单独抽出来测试,用 3 个分片分别检查尾部标记。另外确认每块的字节偏移是连续的,比如第一块1-1024/2048,第二块必须是1025-2048/2048,如果出现跳跃,对端会回 481 并关闭会话。
5.3 现象:SEND 发出去后收到了 200,但业务层面没有触发“消息已读”
原因:MSRP 的 200 是传输层确认,服务端收到并落盘了,但业务逻辑没处理。这在对接 IMS 时很常见:S-CSCF 帮你把 MSRP 消息转给了 AS,但 AS 上的业务逻辑没被触发。解决:看 200 OK 里有没有附带Report-To头,如果应用需要接收方回执,你必须在 SDP 里声明a=accept-types: message/cpim,同时业务端要支持 CPIM 格式的Message-ID关联。很多 zip 里的 demo 只实现了 SEND,REPORT方法没有实现,导致上层看不到完整的消息状态。
5.4 现象:TCP 连接建立后,一段时间没消息,再发 SEND 就超时
原因:NAT 或中间防火墙回收了空闲连接,但客户端没有感知,仍然往旧 socket 写数据。解决:在传输层做 socket 错误检测,写失败时立即触发重建连接;在会话层做 keepalive,每 30 秒发一次空 SEND。另外一个容易忽略的点:有些路由器会针对长连接设置空闲超时,需要主动在客户端配置 TCP keepalive 的内核参数,而不是只靠应用层空包。Linux 下常见做法是设置net.ipv4.tcp_keepalive_time = 30。
5.5 现象:zip 包解压后编译缺依赖,运行时提示找不到某符号
原因:这个 zip 里的二进制或源码生成于特定版本依赖环境。常见的有libpj版本不对、openssl 版本太新导致 TLS 回调签名不一致,还有就是你解压时用了系统 zip 工具,结果没有保留可执行权限。解决:先看 README 里要求的依赖版本,然后在干净环境编译;如果 zip 里带了.so或.a,先确认它们和你系统架构一致,不要直接拷到 x86 机器上跑。用ldd检查一遍可执行文件的依赖,缺什么补什么,不必重新编译全部源码。
6. 用 tshark 验证一次完整 MSRP 会话:把“能跑”变成“能证明”
最后一件事,我建议你把这套流程固化成每次调试的固定动作:不只是看日志,而是直接抓包验证。日志是你程序视角看到的,抓包是协议栈真正发生的,两者对照能发现大量隐藏问题。操作很简单,同时抓信令和媒体两路流量:
sudo tshark -i any -f "tcp port 5060 or tcp port 9000" -Y "sip || msrp" -w msrp.pcap跑完一个完整会话后,用下面的过滤条件检查关键帧:
tshark -r msrp.pcap -Y "msrp"我一般重点看三个地方:INVITE 里的 SDP 是否包含 m=message;TCP 三次握手是否发生在 183 之后;以及最后一个 SEND 的尾标记是不是$。这些通过 tshark 的msrp协议解析字段都可以直接看到。做完整条链路后,我自己的习惯是写一个小的验证清单,把 SDP、连接方向、Byte-Range、尾部标记、keepalive 间隔五项逐条勾掉,再交给测试同事去验收,避免反复“能跑但说不出原因”的状态。希望这份笔记能帮你把 MSRP 这条常常被当成黑匣子的通道彻底打开,调试和对接都少走点弯路。
本文还有配套的精品资源,点击获取