☰
CMPP 2.0短信接入实战:报文解析、鉴权与心跳重连全指南
2026/10/6 1:32:23 网站建设 项目流程

简介:CMPP2.0协议是中国移动通信互联网短信网关接口协议(China Mobile Peer to Peer),为SP(Service Provider)与短信服务中心(SMSC)之间的数据传输提供了标准化通信规范,是第三方业务接入中国移动短信网络的基础。这份PDF教案面向短信业务开发工程师、SP接入人员及通信协议学习者,系统梳理了协议适用范围、缩略语、网络结构、CMPP功能概述、协议栈等基础内容,帮助读者快速建立整体认知。资源包为单个PDF文件,大小仅478KB,但章节完整,涵盖通信方式(长连接/短连接)、消息定义、消息头格式(消息ID、命令长度、命令码等),以及CMPP_CONNECT、CMPP_SUBMIT、CMPP_QUERY、CMPP_DELIVER等关键命令的详细交互流程和应答机制,并说明了SP与ISMG之间的消息定义。文档对编码规则、错误处理、心跳机制也有具体规定,既可直接作为CMPP2.0协议开发调试的手册,也适合用作教学培训讲义。已有79人学习浏览,对需要快速查阅协议细则或备课的技术人员来说,是一份轻量而实用的参考资料。

1. 短信接入为何绕不开CMPP 2.0:SP与ISMG之间的那点事

做验证码、通知短信和营销消息,只要走中国移动短信网关通讯协议CMPP 2.0,你会发现所有报文、鉴权、心跳和重连都围绕一个TCP长连接在转。CMPP全称China Mobile Peer to Peer,2.0是目前存量接入里覆盖面最广的版本,定义了SP(服务提供商)和ISMG(互联网短信网关)之间消息交换的完整格式。这篇文章要解决的是几个实操问题:报文怎么拼、登录怎么校验、长短信怎么拆、断线怎么救。适合正在接入移动通道的运维、通信开发以及刚上手短信网关协议的人照着搭一套最小可用客户端。

2. 从TCP抓包开始拆CMPP 2.0报文:头部三个字段和命令字映射

2.1 CMPP 2.0的消息头为什么只有12字节

CMPP 2.0所有报文共用一个固定消息头,长度12字节,结构简单到几乎没有回旋余地。三个字段分别是Total_Length、Command_Id、Sequence_Id,顺序固定,全部用网络字节序(大端)传输,不按这个顺序解析必然翻车。

Total_Length占4字节,表示整条报文的字节数,包含消息头本身;Command_Id 4字节,是命令字,区分报文类型;Sequence_Id 4字节,是消息流水号。请求和响应的Sequence_Id必须一致,否则无法做事务匹配。我之前接过一个渠道,他们客户端把Sequence_Id每次请求都从1重新开始,结果网关侧串包、重发、状态报告错位全来了,最后抓包才发现流水号没做全局自增。

为什么说CMPP 2.0比后来的3.0在接入时更烦人?因为很多东西没有显式协商,比如长短信分片、编解码方式,都需要两边事先约定好。所以12字节头只是入口,真正的变量全在消息体里。

2.2 命令字镜像关系:请求和响应差一个0x80000000

命令字这块是新手最容易看花眼的地方。CMPP 2.0里请求命令字是0x00000001到0x00000008,对应的响应命令字则在最高位加1,也就是0x80000001到0x80000008。打个比方,收到0x80000001就说明这是对CMPP_CONNECT的应答,不是一个新的连接请求。

命令字含义方向
0x00000001CMPP_CONNECT 登录请求SP → ISMG
0x80000001CMPP_CONNECT_RESP 登录应答ISMG → SP
0x00000002CMPP_TERMINATE 终止连接SP → ISMG
0x80000002CMPP_TERMINATE_RESP 终止应答ISMG → SP
0x00000004CMPP_SUBMIT 短信提交SP → ISMG
0x80000004CMPP_SUBMIT_RESP 提交应答ISMG → SP
0x00000005CMPP_DELIVER 短信下发(上行/状态报告)ISMG → SP
0x80000005CMPP_DELIVER_RESP 下发应答SP → ISMG
0x00000008CMPP_ACTIVE_TEST 链路探测双向
0x80000008CMPP_ACTIVE_TEST_RESP 探测应答双向

CMPP_DELIVER尤其要注意,它不只是用户上行短信,还承载短信状态报告。状态报告里带Msg_Id和Stat字段,你需要用Msg_Id去关联之前发出的SUBMIT。如果Msg_Id匹配不上,多半是你在SUBMIT_RESP里没有保存网关返回的Msg_Id,只存了自己本地流水号。这个问题我后面避坑章节会再讲。

2.3 用tcpdump和Wireshark把协议从黑匣子变成可见

接入调试期最有价值的动作就是抓包。SP连接移动ISMG一般走TCP端口7890,但实际端口以中国移动分配给SP的接入参数为准,有些省份会调整。抓包命令常用这一条:

tcpdump -i any -s 0 -X -nn tcp port 7890 -w cmpp.pcap

抓完后用Wireshark打开cmpp.pcap。Wireshark没有内置CMPP 2.0的标准解码器,不过没关系,你只要做两步过滤:先看TCP流,找到第一个带0x00000001的包;然后右键Follow TCP Stream,就能看到二进制报文。对着消息头逐字节切,三个字段立刻就清晰了。

在实际操作中,我一般会先抓一次登录过程,确认两件事:一是CMPP_CONNECT里的AuthenticatorSource是不是正确的16字节MD5;二是CMPP_CONNECT_RESP返回的Status是不是0。如果Status不是0,立刻查状态码表,而不是继续往下调试SUBMIT,能省掉半天瞎试的时间。抓包不是性能工具,而是定位问题的第一现场,宁可先抓包再动代码。

3. 用Python实现最小CMPP 2.0客户端:从登录到提交短信

3.1 报文头封装:struct处理大端序

知道了报文结构,下一步就是动手拼包。我用Python做最小实现,因为struct一把梭处理二进制最直接,不需要引第三方库。

import struct def pack_header(command_id: int, seq_id: int, body: bytes) -> bytes: total_length = 12 + len(body) return struct.pack('>III', total_length, command_id, seq_id) + body

这里struct.pack的格式串'>III'很关键。I表示无符号4字节整数,大端序对应协议要求的网络字节序。Total_Length必须把12字节消息头包含进去,这个很多人第一次写会漏,导致网关收包后认为报文不完整,连接被断开重来。

调用方式举例:发送一个CMPP_ACTIVE_TEST探测包,命令字是0x00000008,消息体为空,直接pack_header(0x00000008, 10001, b'')就能得到20字节的报文。注意这里是20字节,不是12字节,因为12字节头加上8字节空消息体?不对,CMPP_ACTIVE_TEST没有消息体,总长度就是12字节,这里别把消息体长度混进去。函数里len(body)为0,所以总长度是12,输出12字节,这才是对的。

3.2 登录报文:AuthenticatorSource的MD5算法

登录是CMPP 2.0的第一个硬门槛,一大堆接入失败都死在鉴权上。CMPP_CONNECT报文格式按顺序是:Source_Addr 6字节、AuthenticatorSource 16字节、Version 1字节、Timestamp 4字节。Source_Addr是你的SP企业代码,不足6位左补零,比如0123456789是10位就截取前6位?不对,Source_Addr固定6字节,接入时移动会分配一个6位的SP_Id,直接填进去。

AuthenticatorSource的计算方法是行业里最容易传错的点。正确的计算方式是MD5(Source_Addr + 9字节的0x00 + Shared_Secret + Timestamp),其中Timestamp取当前时间的月日时分秒,格式为MMDDHHMMSS,转成4字节整数。Shared_Secret是接入时移动分配的密码,两边各存一份,不进报文明文传输。

import hashlib import time def build_authenticator_source(sp_id: str, shared_secret: str, timestamp: int) -> bytes: md5 = hashlib.md5() md5.update(sp_id.encode('utf-8')) md5.update(b'\x00' * 9) md5.update(shared_secret.encode('utf-8')) md5.update(struct.pack('>I', timestamp)) return md5.digest() def build_connect(sp_id: str, shared_secret: str, seq_id: int) -> bytes: timestamp = int(time.strftime('%m%d%H%M%S')) auth_source = build_authenticator_source(sp_id, shared_secret, timestamp) body = struct.pack('>6s16sBI', sp_id.encode('utf-8'), auth_source, 0x20, timestamp) return pack_header(0x00000001, seq_id, body)

两个最容易踩坑的地方:一是MD5拼接时Sp_Id后面要补9个字节的0x00,不是补SP_Id到16字节,也不是直接拼密码,这个9字节补丁是协议定义死的;二是Timestamp的格式,2025年2月14日13点25分30秒,Timestamp就是0214132530,转成无符号4字节整数。如果你把年份也加进去,鉴权结果必然和网关算出来的不一致。

CMPP_CONNECT_RESP只有3字节消息体:Status 1字节、AuthenticatorISMG 16字节、Version 1字节。收到后只要判断Status是否为0即可,0表示登录成功,非0需要查状态码。版本号0x20表示CMPP 2.0,0x10表示1.0,如果网关返回版本不匹配,多半是你这里填错了。

3.3 构造CMPP_SUBMIT文本短信

登录成功后的核心操作就是提交短信。CMPP_SUBMIT的消息体有一段固定长度部分加一段动态部分。固定部分依次是Msg_Id 8字节、Pk_Total 1字节、Pk_Number 1字节、Registered_Delivery 1字节、Msg_Level 1字节、Service_Id 10字节、Fee_User_Type 1字节、Fee_Terminal_Id 32字节、Fee_Terminal_Type 1字节、TP_Pid 1字节、TP_Udhi 1字节、Msg_Fmt 1字节、Msg_Src 6字节、Fee_Code 6字节、Src_Id 21字节、DestUsr_tl 1字节。动态部分是被叫号码列表和消息内容。

def build_submit(seq_id: int, msg_src: str, service_id: str, src_id: str, dest_terminal: str, content: str, need_report: int = 0) -> bytes: msg_fmt = 15 payload = content.encode('gbk') dest_tl = 1 fixed_part = struct.pack( '>8sBBB10sB32sBBB6s6s21sB', b'\x00' * 8, # Msg_Id,提交时填0 1, # Pk_Total,单条短信填1 1, # Pk_Number,分片序号 need_report, # Registered_Delivery,是否需要状态报告 0, # Msg_Level,优先级 service_id.encode('utf-8').ljust(10, b'\x00'), # Service_Id 0, # Fee_User_Type,计费用户类型 b'\x00' * 32, # Fee_Terminal_Id 0, # Fee_Terminal_Type 0, # TP_Pid 0, # TP_Udhi,先按单条发 msg_fmt, # Msg_Fmt,15表示GBK msg_src.encode('utf-8'), # Msg_Src,SP的企业代码 b'\x00' * 6, # Fee_Code,先置空 src_id.encode('utf-8').ljust(21, b'\x00'), # Src_Id,扩展码 dest_tl, # DestUsr_tl,号码个数 ) dest_bytes = dest_terminal.encode('utf-8').ljust(32, b'\x00') return pack_header(0x00000004, seq_id, fixed_part + dest_bytes + bytes([len(payload)]) + payload)

Msg_Fmt字段要特别说明:0表示ASCII,8表示UCS2,15表示GBK。中文短信我在实际项目中优先用15,字符集是GBK,字节数比UCS2少一半,网关侧支持也普遍;如果遇到个别省份网关GBK乱码,再切回8重发。Msg_Src字段填SP的企业代码,这个值和登录时的Source_Addr一样,提交时混填会导致网关在业务层拒收。

submit发出后等待CMPP_SUBMIT_RESP,消息体是Msg_Id 8字节加Result 1字节。Result为0代表网关受理,不等于短信已经送达,真是的送达状态要看状态报告。Msg_Id是网关回填的,你要把它和本地关联保存。

4. CMPP 2.0的会话护城河:心跳、断线重连与流量控制

4.1 CMPP_ACTIVE_TEST心跳与超时参数

TCP连接建好后,移动ISMG并不会一直等你。网关侧通常有一个空闲超时时间,超过一定秒数没有数据传输,连接就会被断开。CMPP_ACTIVE_TEST就是用来保活的,双方都可以发,收到后必须回CMPP_ACTIVE_TEST_RESP。报文格式是最简单的:消息头加空消息体,命令字0x00000008。

def build_active_test(seq_id: int) -> bytes: return pack_header(0x00000008, seq_id, b'') def build_active_test_resp(seq_id: int) -> bytes: return pack_header(0x80000008, seq_id, b'')

心跳间隔怎么设?我一般设30秒到60秒。设太短会浪费网关资源,设太长容易触发网关空闲断开。如果你发现连接总是在几分钟后悄无声息地被断,优先把心跳间隔调到30秒,再看网关侧是否还有别的空闲策略。发送心跳后要启动一个超时定时器,比如8秒内没收到RESP就认为链路异常,进入重连流程。这个超时不要设得比心跳间隔还长,否则链路死了你还在傻等。

4.2 断线重连要讲退避:不要用固定1秒疯狂重试

短信网关不是高可用架构,ISMG会重启、会切换、会杀空闲连接。客户端必须做重连,但重连策略绝不能是固定间隔死循环。网关如果在重启,你每秒重连一次,重启过程持续两分钟,你就能攒出120个失败连接日志,纯粹在制造告警噪音,还有可能被网关侧当作恶意连接封IP。

常见的做法是二进制退避重连:第一次失败等1秒,第二次等2秒,第三次等4秒,最多等到30秒封顶,然后保持该间隔持续重试。连续重试期间不要每轮都打印完整堆栈,打印一条简短的错误摘要就够了,等重连成功后再打一条恢复日志。

import socket import time def connect_with_backoff(host: str, port: int, build_login: callable, max_backoff: int = 30) -> socket.socket: delay = 1 while True: try: sock = socket.create_connection((host, port), timeout=10) sock.sendall(build_login()) return sock except (socket.timeout, ConnectionRefusedError, OSError) as e: print(f'connect failed: {e}, retry in {delay}s') time.sleep(delay) delay = min(delay * 2, max_backoff)

这里要注意create_connection只能保证三次握手成功,登录结果要靠发送CMPP_CONNECT后读响应来判断。如果登录失败返回Status非0,看起来是连接建立了,实际没有进入可用状态,这种场景下要主动close再重连,不要拿着一个没登录成功的socket继续发SUBMIT。

重连的同时要把内嵌的会话状态清理掉,比如Sequence_Id是全局递增继续用没问题,但Msg_Id关联表要清空。如果不清理,重连前发出的短信跑出的状态报告回来后匹配不上号,容易误判成投递失败。

4.3 流量控制:单连接吞吐上限和窗口设计

CMPP 2.0规范本身没有定义滑动窗口,但因为底层是TCP,你发得再快,网关处理不过来也会背压或者断开。很多省份的ISMG对单连接下发速度有明确限制,比如每秒200条到500条之间,超出后连接会被流控错误中断。

实际操作中我习惯把提交速率限制做成可配置的令牌桶,默认速率从每分钟600条开始调,稳定运行再逐步上调,每次调50条,观察5分钟看有没有SUBMIT_RESP超时或者状态错误。不要一上来顶着最大速率猛灌,网关侧把你流控了,全部短信积压,反而拖慢整体下发效率。

流量控制的另外一个维度是连接数量。CMPP协议允许SP同时建立多条连接,但移动侧通常限制每条SP最多几路连接同时在线。多连接并发时,Sequence_Id要全局唯一,不能每个连接各自从1开始,否则网关那边按Sequence_Id防重,会把不同连接的请求误判为重复消息而丢弃。

5. CMPP 2.0避坑指南:5个高频故障的排查与解决

5.1 登录失败返回Status=4:AuthenticatorSource鉴权失败

现象:客户端发完CMPP_CONNECT,网关秒回Status=4,连接立即被断开。原因有三种常见可能:Shared_Secret配置不一致,Timestamp格式不对,MD5拼接顺序漏了9字节补零。排查时先做三件事:确认本地时间与北京标准时间误差不超过10秒;确认Shared_Secret两边完全一致,大小写和末尾换行符都算字符;用抓包工具抓下Sent和Received的报文,人工按MD5算法走一遍比对。

解决:我在代码里加了自校验函数,把报文的二进制内容传进去,重新算一遍AuthenticatorSource,能算出不一致就说明问题出在报文构造,算得一致但还是登录失败,就去找移动侧核对账号密码状态。登录失败不要盲目重试,连续失败5次就停下来人工排查,否则账号存在临时锁定风险。

5.2 发了SUBMIT之后一直等不到SUBMIT_RESP

现象:客户端发送CMPP_SUBMIT后,服务端没有任何响应,TCP连接也不断开,一直挂到超时。原因通常是网关侧认为你登录态失效但没发终止包,或者连接被设备会话保持策略静默回收,客户端这边还傻乎乎以为连接可用。

解决:每个SUBMIT发送后都设一个等待响应超时,我一般设10秒,超时后主动断开连接并重连。别把超时设太大,否则网关侧已经拉到流控错误,你还在等响应,整个链路就卡死了。同时检查Registered_Delivery字段,如果你设为1(请求状态报告),那在SUBMIT_RESP之外还会收到DELIVER类型的状态报告,不要把这两者混在一起。

5.3 中文短信到手机上变成乱码

现象:短信发出后状态报告显示成功,但用户收到的是乱码。原因多数是Msg_Fmt编码字段与消息体实际编码不一致。最常见的是内容用UTF-8编码,Msg_Fmt却填了15(GBK);或者内容用GBK编码,Msg_Fmt填了8(UCS2)。

解决:把Msg_Fmt和编码算法的配对写成一个独立函数,比如我用15时就固定content.encode('gbk'),用8时就content.encode('utf-16-be'),用0时就只允许ASCII字符。在测试环境里分别用三个报文做一次真机测试,确认平台支持后就锁定组合,不允许运行时随意切换。乱码问题一旦出现,所有已发短信都会产生投诉,所以上线前编码测试是必做项。

5.4 连接会莫名消失,重连后一切正常,但过几分钟又断

现象:TCP连接在没有任何业务流量时被服务端断开,客户端检测到后重连,恢复运行一段时间又断。原因基本是心跳丢失或心跳间隔大于网关空闲超时。部分ISMG要求SP主动发送CMPP_ACTIVE_TEST,网关只负责回应,不主动探测;如果你没有发心跳,空闲连接会被自动回收。

解决:把心跳逻辑和业务收发解耦,放到独立线程里跑,间隔30秒一次。发送心跳后同样要等响应,连续3次无响应就触发重连。如果间隔30秒还断,就改用20秒间隔,并在抓包里确认ACTIVE_TEST和RESP是不是成对出现。心跳包不算业务流量,网关侧对它也有计数阈值,所以千万不要把心跳逻辑写成发送后不等待直接继续发。

5.5 长短信被网关拆分但手机收到乱序或丢失

现象:超过140字节的短信内容拆成多条发送,手机收到了,但顺序错乱或者中间缺一条。原因在于分片时需要设置TP_Udhi=1,并在消息内容前加一个6字节的用户数据头(UDH),用UDH里的序号让对方重组。如果你只是把内容硬切成两段,把Pk_Total和Pk_Number填了2和1,没加UDH,网关会把每一片当成独立短信下发,自然不会重组。

解决:发长短信时,先把内容按编码后的字节长度拆分,GBK单条容量134字节,UCS2单条容量67个UTF-16码元。每条分片的消息体开头加6字节UDH:0x05 0x00 0x03 引用号 总片数 当前片号。同时TP_Udhi字段设为1。引用号每批长短信用一个随机数,两批短信的引用号不能相同,否则手机会把不同批次的片拼在一起。

6. 上线前把CMPP 2.0接入变成可验证的检查动作

CMPP 2.0接入上线前,我会先做一轮逐项自检,而不是直接拿生产账号去冲量。准备一个测试号码和一张测试卡,按下面顺序走一遍。

第一查鉴权:连续登录10次,每次间隔随机,确认Status全为0,同时抓包比对AuthenticatorSource与本地重算值一致。第二查心跳:建立连接后不做任何业务操作,保持心跳30分钟,确认连接没有被断开。第三查单条编码:分别用Msg_Fmt=0、8、15发一条文本短信到测试手机,核对显示内容是否正常。第四查长短信:发送超长短信,拆成2条和3条两种场景,确认接收顺序正确且不丢片。第五查断线恢复:用kill命令杀掉服务器端进程模拟ISMG重启,观察客户端是否在30秒内自动重连并恢复发送。

每次上线新省份,我都会把这个检查动作清单放进发布流程文档里。别忘了测一下状态报告链路:发一条需要状态报告的SUBMIT,确认能收到DELIVER类型的state报告,并且Msg_Id能和SUBMIT_RESP返回的Msg_Id对上。如果对不上,把Msg_Id关联逻辑修好再上线,不然你的短信送达率永远是一个黑匣子。希望这几条来自一线的实测经验能帮你在接入CMPP 2.0的路上少踩几个坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询