SIP客户端集成MSRP:从SDP协商到文件分片传输的完整指南

发布时间:2026/10/1 15:21:00
SIP客户端集成MSRP:从SDP协商到文件分片传输的完整指南 简介一个包含SIP客户端中MSRP协议C/C实现源码与工程文件的资源包面向网络语音、即时通信及多媒体会话开发人员用于解决在SIP会话中传输图片、文件、富文本等长消息的问题。包内共四十六个文件其中包含二十三个头文件、十六个源文件另附构建脚本、工程配置及辅助文件等压缩包整体约56KB可支撑不同环境下编译与阅读。已有一百三十五人浏览学习。通过研读源码可掌握MSRP的地址参数、报文头部与事务标识设计了解SIP邀请消息中携带MSRP能力协商、建立传输通道、发送消息以及结束清理会话等核心流程同时源码按模块划分了连接管理、监听器、请求响应、路径与地址解析等组件便于理解SIP控制面与MSRP传输面的协作关系。对研究即时消息传输、文件发送或网络语音富媒体扩展的开发者是一份具备较高参考价值的协议实现样本。1. 拿到 MSRP.zip 之后要解决的真问题SIP 谈条件MSRP 传内容第一次拿到 MSRP.zip 的时候我以为是某个 SIP 软电话的安装包解压完才意识到名字里的 msrp 是一个和 sip 协议绑得很紧的独立协议。做 SIP Client 的人迟早会遇到这一类包sip 协议栈负责注册、呼叫、媒体协商而真正要传的聊天消息、文件碎片走的是 MSRPMessage Session Relay Protocol。它解决的问题非常具体——SIP MESSAGE 发大消息会被 413 或 488 弹回来UDP 传输也扛不住几百 KB 的负载MSRP 则在 TCP 上建立一条独立的“消息会话”把大消息和文件切成块发过去再组装。这个方向适合正在做或打算做 SIP 软电话包括安卓版消息功能、需要文件传输和大消息能力的人也适合所有被 SIP MESSAGE 的字节上限卡过的开发者。2. SIP 协议只负责牵线MSRP 才负责传话SDP 里怎么描述一路消息会话2.1 为什么大消息要绕过 SIP MESSAGE三个硬边界SIP 消息里其实已经有一个发文本的方式就是 SIP MESSAGE大多数第一次做聊天功能的开发者会先用它。但它在真实项目里撑不住大消息这不是某个库的缺陷是协议设计决定的。第一个边界是传输层SIP 默认跑在 UDP 上单个报文一旦超过底层 MTU 就要分片中间代理和 NAT 设备对分片报文的处理态度完全不同很多网络环境下超过十几 KB 的 SIP MESSAGE 就直接沉默了。第二个边界是语义SIP MESSAGE 是一个独立事务没有会话概念没有流控状态也没有断点续传。你发一个 500 KB 的文件如果中途断了整个事务重来对端还分不清这是新消息还是重传。第三个边界是业务确认SIP 的 200 OK 只表示事务被信令层接受了不表示业务内容送达更不能表达“已读”和“第几段没收到”这种状态。MSRP 的做法是把“传内容”这件事从信令里拆出来单独成为一条 TCP 会话。SIP 仍然负责会话生命周期也就是 INVITE、200 OK、ACK、BYE 这一套MSRP 负责在这条会话里传字节。所以这个 MSRP.zip 的包名里“SIP Client MSRP”不是并列的两样东西而是 SIP Client 领域里一个不可拆分的组合。你在项目里做选型的时候可以这样判断消息体基本在几百字节以内数量不大SIP MESSAGE 够用一旦要发文件、要开聊天室、要支持大消息或需要明确到“第几段已收到”的进度就应走 MSRP。这个判断标准我在多个项目里验证过比看功能列表靠谱。2.2 SDP 协商mmessage 之后的六个关键参数MSRP 会话是像媒体流一样通过 SIP 的 SDP offer/answer 协商出来的。也就是说SIP INVITE 里带着一段特殊的 SDP描述“我这里有一条 MSRP 消息通道你接不接”。下面是一段最小可用的 SDP 描述风格取自 RFC 4975也是大多数 SIP Client 实现里能直接照抄的形状v0 oalice 2890844526 2890844526 IN IP4 192.0.2.1 sMSRP Session cIN IP4 192.0.2.1 t0 0 mmessage 2855 TCP/MSRP * aaccept-types:text/plain message/cpim apath:msrp://192.0.2.1:2855/798a;tcp asetup:active这段 SDP 的读法是c行给出本端可达的 IPm行声明这是一个 message 类型媒体流走 TCP端口 2855格式不限aaccept-types 声明接收方愿意处理的内容类型apath 给出本端 MSRP 的完整 URI这个 URI 会在后面的 MSRP 报文中作为 From-Path 或 To-Path 出现asetup:active 表示本端会主动发起 TCP 连接。对端回复 200 OK 时会带上自己的 SDP通常 asetup 是 passive两端各写各的地址和端口然后由 active 方主动去连 passive 方。这里有一个关键点apath 里的地址和 c 行里地址必须一致可达否则协商出来一个对端永远连不上的 URI。MSRP 默认 TCP 端口是 2855端口冲突时可以在 m行里改但改了之后 apath 里的 URI 端口也要跟着改。下面这个表是每次对接我都会逐一核对的参数建议直接复制到你的检查清单里SDP 参数作用典型值容易踩的坑mmessage声明 MSRP 媒体流mmessage 2855 TCP/MSRP *写成TCP/RTP/AVP或漏掉端口apath本端 MSRP URI报文的 From/To-Path 来源msrp://host:port/会话id;tcp端口与 m行不一致aaccept-types接收的内容类型text/plain message/cpim写死 text/plain 后对端发文件直接失败asetupTCP 连接角色active / passive / actpass两端都 active 会双方向同时连接造成会话分裂c行本端 IP 地址cIN IP4 1.2.3.4填入内网地址后公网对端连不上msrp vs msrps是否走 TLSmsrp://或msrps://TLS 环境忘记改 scheme连接被拒参数里需要多说一句的是 accept-types。如果对端发来的是一个 message/cpim 包装的消息而你的 aaccept-types 里没有这一项对端会终止 SEND 并返回错误。很多互操作问题的根源不在解析而在这一行没写全。交互上还有一个容易被忽略的点SIP 流程是先 INVITE200 OKACK然后才是 TCP 建链和 SEND。不要在收到 100 Trying 就去建 MSRP 连接那是白费劲。用 SIP 软电话测试的时候先看 SDP 里有没有 mmessage再决定后面调什么这是最快的定位方法。3. 把 MSRP.zip 里的 SIP Client 跑起来解压、编译与最小配置3.1 解压前先看产物类型源码包还是预编译包拿到一个以 zip 分发的 SIP Client 相关代码包第一件事不是急着解压而是确认它到底是什么形态。常见做法是先用 unzip -l 看一眼清单再决定走哪条落地路径。很多人在这一步翻车看到一个 CMakeLists.txt 就当源码包编译结果缺依赖编译了半小时看到一个 bin 目录就当成品直接用结果运行环境缺动态库一启动就报错。# 1. 先列清单不要直接解压 unzip -l MSRP.zip | head -30 # 2. 如果看到 configure / CMakeLists.txt走源码编译 # 有 configure 就 ./configure make -j$(nproc) # 3. 如果看到 bin/ 或 *.so / *.dll走预编译把可执行文件路径加进 PATH file bin/* 2/dev/null | head这段命令的逻辑是unzip -l 只读中央目录不做解压能快速判断包内是源码结构还是二进制产物同时验证这个 zip 文件本身是否完整。第二步里configure 存在说明包遵守 autotools 习惯也可以先跑./configure --help看有哪些开关。第三步的 file 命令用来确认二进制架构x86_64 的程序在 ARM 机器上跑不起来这是预编译包最常见的坑。这一阶段还有两个和 zip 本身有关的坑要提前排掉。一个是解压时提示输入密码而发布页没有提供任何密码这多半是 zip 伪加密也就是本地文件头里的加密标志被强行置位数据本身并没有加密用 7-Zip 解压通常能直接通过。另一个是解压后文件数比 unzip -l 列出的少说明包被截断或二次打包过别浪费时间修复回到发布源重新取包。源码包落地时还要注意路径不要有中文MSRP 客户端如果带资源文件路径解析逻辑出中文路径问题很难查属于玄学里的稳定复现项。3.2 配置 SIP 账号与 MSRP 边界七个必填参数缺一不可编译通过只是第一步能不能跑通取决于配置。我见过太多人把 MSRP 客户端当成普通 IM 聊天软件只填一个服务器地址和用户名就去点登录然后发现消息根本发不出去。不管这个 zip 包里的配置文件叫什么名字落到协议层这七个参数是必填的缺一个都会在特定场景下暴露问题[sip] server192.0.2.10 ; SIP 注册服务器 port5060 user1001 ; 账号 passwordsecret ; 认证密码 local_ip192.0.2.1 ; 本端 IP要和你 SDP 里 apath 使用的一致 local_port5060 [msrp] local_urimsrp://192.0.2.1:2855/abc123;tcp ; 本端 MSRP URI transporttcp ; tcp 或 tlstls 时 scheme 要改成 msrps setupactive ; active/passive/actpass一般主叫 active accept_typestext/plain,message/cpim chunk_size4096 ; 单条 SEND 的消息体字节上限这里的逻辑是SIP 段负责让本端能注册上服务器、能被对端呼叫MSRP 段负责让会话协商完成后双方知道往哪个 TCP 端口连、以什么角色建链、能收什么类型的内容。local_uri 是最容易被忽略的它同时出现在 SDP 的 apath 和对端收到的 From-Path 里如果你填的 host 在别的网络不可达协商成功但连接永远建不起来。setup 角色建议先固定成主叫 active、被叫 passive这是最简单的组合。actpass 虽然灵活但需要双方按 RFC 4145 的角色判定逻辑决策实作偏差大第一轮联调不要用它。accept_types 里带上 message/cpim 是为了兼容对端用 CPIM 包装的消息很多开源软电话默认会包一层你不声明就收不到。如果你做的是安卓版 SIP 软电话的 MSRP 消息功能还要额外注意 Android 后台对 TCP 长连接的限制MSRP 连接被系统回收后要能检测到并重建这个逻辑必须在客户端实现配置里没有开关能解决。4. 从 INVITE 到 MSRP SEND完整会话时序与分片上报逻辑4.1 用 Python 模拟一次 MSRP SEND请求行、To-Path 与 200 OK 的对应跑通 SIP 协商之后MSRP 层的行为可以单独验证。完整时序是这样的主叫发 INVITE对端回 200 OK 带自己的 SDP主叫 ACK随后主叫作为 active 方发起 TCP 连接到对端 SDP 里给出的地址和端口连接建立后发送 MSRP SEND。这里我先给一个绕过 SIP 直接测试 MSRP 报文的 Python 脚本用来验证你对端透传逻辑是否正确。这是在联调时最常用的手段比猜对端有没有收到消息快得多。import socket def send_msrp(peer_host, peer_port, from_path, to_path, message_id, body, finishedTrue): # peer_host/peer_port 从对端 SDP 的 c 行和 m 行解析 sock socket.create_connection((peer_host, peer_port), timeout5) headers [ fMSRP {message_id} SEND, fTo-Path: {to_path}, fFrom-Path: {from_path}, fMessage-ID: {message_id}, fByte-Range: 1-{len(body)}/{len(body)}, Content-Type: text/plain, ] req \r\n.join(headers) \r\n body # 每片事务都有自己的定界符以 $ 结尾表示整条消息结束 term ------- message_id ($ if finished else ) sock.sendall(req.encode(utf-8) b\r\n term.encode(utf-8) b\r\n) resp sock.recv(4096).decode(utf-8) sock.close() if 200 OK in resp: print(MSRP 200 OK:, resp.splitlines()[0]) else: print(响应异常:, resp.splitlines()[0]) if __name__ __main__: # 这里的地址是文档示例实际以对端 SDP 为准 send_msrp( peer_host192.0.2.2, peer_port2855, from_pathmsrp://192.0.2.1:2855/abc123;tcp, to_pathmsrp://192.0.2.2:2855/9di4ea;tcp, message_idd93kswow, bodyHello MSRP, )这段脚本里最关键的两行是 To-Path 和 From-Path。To-Path 必须是对端 SDP 里 apath 给出的完整 URIFrom-Path 必须是本端 SDP 里 apath 的完整 URI两个 URI 里的 host、port、会话 id、分号后的 tcp 都不能丢。丢了会话 id对端可能把消息关联到错误的会话丢了 ;tcp对端解析 URI scheme 时可能走默认端口。Message-ID 同时用作报文标识和事务标识但它不等于 SDP 里的会话 id两套 id 各管各的。脚本里最后的定界符必须以-------开头和请求行里的 transaction id 相同否则对端会一直等后续分片直到超时。收到的 200 OK 里请求行是MSRP d93kswow 200 OK它关联的是事务 id 而不是 Message-ID。所以客户端在同一连接上并发发多条消息时必须按事务 id 匹配响应不能按消息序匹配。我在实际对接中用这个脚本验证过三个不同厂家的 MSRP 网关全部通过说明报文格式只要按 RFC 4975 写互操作没有想象中那么难。4.2 大消息分片与断点续传Range 语义怎么对上单条 SEND 能承载的字节数没有硬性限制但工程上会把消息体控制在 4 KB 左右。过大的 SEND 会让接收缓冲和解析逻辑变复杂一个包卡住整条 TCP 连接过小则会产生大量重复的 To-Path/From-Path 头带宽浪费严重。发送端把文件拆成多个 SEND每一片是一个独立事务但 Message-ID 必须相同接收端按照 Byte-Range 来对齐顺序。下面是一个分片发送的骨架def send_chunks(sock, from_path, to_path, msg_id, data, chunk_size4096): total len(data) pos 0 index 1 while pos total: chunk data[pos:pos chunk_size] start pos 1 pos len(chunk) tid f{msg_id}-{index} # 每片的 transaction id 不同 headers [ fMSRP {tid} SEND, fTo-Path: {to_path}, fFrom-Path: {from_path}, fMessage-ID: {msg_id}, # Message-ID 全片相同 fByte-Range: {start}-{pos}/{total}, Content-Type: application/octet-stream, ] last pos total term ------- tid ($ if last else ) sock.sendall( (\r\n.join(headers) \r\n).encode(utf-8) chunk b\r\n term.encode(utf-8) b\r\n ) index 1这段代码的逻辑要点在 Byte-Range 的格式它是“起始字节-结束字节/总字节数”起始从 1 开始不是从 0 开始也不是从当前片的序号开始。比如总长 10000 的文件第一片是1-4096/10000第二片是4097-8192/10000最后一片是8193-10000/10000。接收方就是靠这三个数字判断是否连续、是否有缺片。定界符尾部$表示这是最后一片表示后面还有接收方靠这个标记判断整条消息何时组装完成而不是靠收到 200 OK 的数量。断点续传的机制也落在 Range 语义上。发送方如果收到对端返回的 200 OK里面带有Range: 1-4096/10000意思是这一片已收发送方下一片从 4097 开始发。但要注意这不是文件级确认是报文级确认。如果应用层要求“文件 100% 已到达”才能算完成光靠 MSRP 的 200 OK 是不够的因为整个文件拆成 10 片每一片都会收到 200 OK但没有任何一个报文告诉你“10 片已经拼完整”。我一般会在文件传输业务里加一层应用确认最后一个 SEND 发完后接收方主动回一条 application/json 或 message/cpim 包装的业务消息带上文件哈希发送方收到后才把状态置为完成。这个设计能绕开 REPORT 机制的各种兼容性问题后面避坑章节会展开说。5. MSRP 会话落地避坑5 个让互操作翻车的真实问题5.1 NAT 环境里 apath 给的是内网地址对端连不过来现象SIP 注册和呼叫都正常INVITE、200 OK、ACK 全部走通但 TCP 连接就是建不起来对端日志显示 Connection refused。抓包看SIP 层报文到了MSRP 的 SYN 没有响应。原因SDP 和 apath 里填的是 192.168.x.x 内网地址对端在公网上回连这个地址直接被路由丢弃或拒绝。SIP 信令能通是因为注册服务器做了中转MSRP 是端到端 TCP 直连没有中转就暴露了私网地址。解决最常见的做法是把 MSRP 连接交给网关或 B2BUA 做 relay也就是让服务器介入消息会话两端的 MSRP 连接都只连到服务器不直接互连。另一个做法是给 MSRP 媒体流配合 STUN 探测公网映射地址把探测到的地址写入 apath。实作时至少要做到发送 SDP 之前判断本端 IP 是否私有如果是私网地址且没有 relay 能力直接拒绝发起 MSRP 会话而不是等对端超时。这个问题在所有 NAT 场景下都会发生属于必现问题不是概率问题。5.2 SIP 200 OK 和 MSRP 200 OK 混为一谈现象客户端把消息放进缓冲后SIP 层收到 200 OKUI 立刻显示已送达但接收方实际一个字节都没收到。重试机制不生效发送缓冲被清空消息丢失。原因两个协议都有 200 OK语义完全不同。SIP 的 200 OK 只表示 INVITE 协商成功、会话建立MSRP 的 200 OK 才表示某个 SEND 事务被对端接收。开发者往往只接了 SIP 事务回调就以为 MSRP 报文也被确认了。解决发送链路上把两个回调分开。MSRP SEND 的确认必须在 TCP 流里收到事务 id 匹配的 MSRP 200 OK才能把这条报文标记为已发送在这之前任何 SIP 层事件都不能触发 UI 的送达状态变更。排查这类问题时不要盯着业务日志直接 tcpdump 看 2855 端口上有没有 MSRP 200 OK 回包这一眼就能定位。5.3 对端不回 REPORT文件传输进度卡在 99%现象文件分片全部发完所有 SEND 都收到 200 OK接收方文件也完整落盘了但发送方进度条停在那里不动业务状态永远不是“完成”。原因REPORT 是 MSRP 里接收方向发送方回报状态的机制但默认情况下接收方没有义务回 REPORT除非发送方在 SEND 头里显式要求。很多实现根本不实现 REPORT或者只在出错时回。发送方如果把它当作必达的最终确认就会永远等下去。解决第一检查发送方的 SEND 报文里是否带了 Report-Success 或 Report-Failure 头如果对方的协议栈不支持 REPORT这些头也是白搭第二不要依赖 REPORT 做文件级完成判定改用应用层确认。文件发完后接收方返一个业务消息里面带文件哈希或行数发送方收到这个才算完成。我的经验是和第三方网关对接时默认对方不支持 REPORT能省掉一半互操作问题。5.4 MSRP.zip 解压提示要密码输入什么都是错的现象解压时 unzip 提示输入密码输入发布文档里的密码也报错甚至按回车直接报错。原因zip 伪加密也就是 zip 文件头里的加密标志位被置位但数据区实际上没有加密或加密方式不标准。这类文件在论坛和网盘传播的包里很常见可能是打包工具残留也可能是二次打包的人故意改的标志位。中央目录和本地文件头的标志位不一致导致不同解压工具表现完全不同。解决先用zipinfo -v MSRP.zip | head -30看 General Purpose Flag 的 bit 0再用 7-Zip 直接解压7-Zip 对伪加密的容错比 unzip 好。如果解压出来的文件数量或大小和发布页描述不一致直接放弃这个包回到原始源重新取。这里提醒一句只靠 zip 包判断不出来一定要拿 unzip -l 的输出和发布页的文件清单核对。5.5 TCP 粘包和分片定界符把报文边界搞丢现象同一 TCP 连接上连发多个 SEND接收方解析时看到两条请求连在一起To-Path 和上一片的定界符混在同一行二进制文件内容里恰好出现以-------开头的字节序列导致解析器提前截断。原因MSRP 是文本协议但没有固定长度报文边界靠事务定界符决定而 TCP 是流协议没有报文边界。很多实现按行读遇到消息体是二进制时逐行拆分必然出错。解决接收循环不能按行读要用固定缓冲拼接每次以\r\n-------事务id为边界扫描整条报文匹配到$或标记后再交给上层解析。事务 id 要在会话建立时加上本端随机前缀确保同一连接上两端生成的事务 id 不会撞车。定位这类问题用抓包工具在 2855 端口上看 ASCII 输出最直观tcpdump -A -i any -s 0 tcp port 2855用 -A 参数把包内容以文本形式打印重点检查每个 SEND 的收尾标记是$还是以及下一条 SEND 的请求行是不是从新的一行开始。多数粘包问题在抓包里一眼就能看出来。6. 把 MSRP 会话验证做扎实抓包判据与回归脚本6.1 三条抓包判据先于业务逻辑验证MSRP 联调最容易出现的状态是“双方都认为自己在正常工作”为了不被假象带偏我现在接任何一个 MSRP 相关项目先跑一组不依赖业务的验证。判据只有三条INVITE 的 SDP 里必须出现mmessage并且有对应 apath200 OK 返回的 SDP 里apath 的地址能从本端 TCP 直连第一笔 MSRP SEND 的 From-Path 和本端 SDP 的 apath 完全一致。三条全过再开始写业务逻辑任何一条不过后面的问题都不需要报给我。def test_msrp_sdp(invite_sdp: str) - None: lines invite_sdp.strip().splitlines() assert any(l.startswith(mmessage ) for l in lines), 缺少 mmessage 媒体行 assert any(l.startswith(apath:msrp://) for l in lines), 缺少 apath assert any(l.startswith(asetup:) for l in lines), 缺少 asetup 角色声明这个脚本可以做成 CI 里的固定用例也可以放进本地回归。它验证的是“协商出的内容确实是 MSRP 会话而不是退回了 SIP MESSAGE”。在我对接过的项目里有三次所谓“MSRP 传大文件失败”的问题最终根因都是 SDP 里根本没有协商出 mmessageSIP 层直接回了 488 或静默丢弃。回归脚本挡住的正是这一类问题。MSRP 这个方向值不值得做我的回答取决于你的具体诉求只发短文本SIP MESSAGE 更省事要传真实文件、要做聊天室、要端到端大消息MSRP 是当前标准化的唯一可靠路径。我现在的习惯是任何新对接方先用上面三条判据跑通再谈功能这帮我挡掉了大量后续互操作翻车。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询