ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

短信网关对接必读:CMPP2.0协议核心机制与工程实现

短信网关对接必读:CMPP2.0协议核心机制与工程实现 简介中国移动CMPP2.0短信网关通讯协议规范是面向短信网关对接开发、服务提供商SP接入及通信协议学习者的标准技术文档。这份PDF教案完整收录中国移动2002年发布的CMPP2.0接口协议原文从协议适用范围、术语约定、网络结构、功能概述、协议栈模型到长连接与短连接通信方式以及完整消息定义格式均有逐项说明。其中重点介绍了SP与短信网关之间的连接建立、短信提交、短信下投、状态查询和短信取消等核心命令流程并对消息头字段、基本数据类型、差错处理、心跳机制和应答方式做了详细规定非常适合在联调互联网短信网关时作为接口手册查阅。资源为1个PDF文件约478KB轻量便于随时检索目前已有79人学习浏览。对于从事SP业务接入、短信平台开发或准备运营商接口联调的技术人员这份协议原文能够帮助快速理清TCP/IP承载下的应用层报文结构和交互流程减少对接中的理解偏差是一份不可多得的工具型教案。1. 对接中国移动短信网关先吃透这份 CMPP2.0 协议做短信通道开发的同行应该都有过这种经历SP 方接口调通了结果提交短信后网关一直不回包或者直接返回“认证错”排查半天发现是协议细节没吃透。中国移动短信网关通讯协议 CMPP2.0 正是解决这类问题的关键文档——它定义了 SP业务提供方与 ISMG互联网短信网关之间从连接建立、鉴权、短信提交到状态报告回传的完整交互规范。无论是做验证码通知、营销短信平台还是企业内部消息系统只要走中国移动的短信通道CMPP2.0 就是绕不开的协议基础。这份 PDF 是官方 2002 年发布的 2.0 版本全文把消息格式、端口号、超时重传参数都写得很清楚适合正在做短信网关对接的开发者和需要排查通道问题的运维人员。2. 协议框架与连接机制先搞懂长连接和短连接的区别2.1 CMPP 在协议栈中的位置与网络结构CMPP 协议以 TCP/IP 作为底层承载位置在应用层。这意味着所有 CMPP 消息都封装在 TCP 数据段里传输TCP 的可靠传输机制顺序到达、错误检测、重传保证了 CMPP 消息不丢包、不乱序。从网络结构看整个链路涉及三个角色SPService Provider业务提供方也就是我们自己的服务器ISMGInternet Short Message Gateway互联网短信网关SP 直接对接的移动侧设备GNSGateway Name Server汇接网关负责路由查询ISMG 之间转发消息时需要通过它获取目标网关地址SP 与 ISMG 之间采用客户端/服务器模式但有个硬性要求SP 必须首先以客户端身份请求连接到 ISMG成功注册之后才能进行数据传输。这一点在实现时容易忽略——有些人直接就开始发 SUBMIT 消息结果连接被网关直接丢弃。2.2 长连接与短连接的选择协议里明确规定了两种连接方式长连接在一个 TCP 连接上连续发送多个数据包。连接保持期间如果没有数据交互双方要定时发链路检测包维持连接。短连接每次只有数据交互时才建立 TCP 连接一次操作完成后立即断开即每次 TCP 连接只完成一对 CMPP 消息的发送。从实际生产环境看绝大多数短信发送场景都用长连接因为频繁建连的开销在单日百万级消息量下非常可观。短连接适合偶尔发几条测试消息或者低频业务。长连接的核心参数协议给出了建议值参数含义建议值C无数据时链路检测间隔3 分钟T发送消息后等待响应的超时时间60 秒N超时后连续重发次数3 次W滑动窗口大小16 条链路检测用 CMPP_ACTIVE_TEST 消息实现发出后超过 T 秒未收到响应就重发连续 N-1 次没响应则断开连接。我一般把检测间隔压到 2 分钟因为有些运营商网关的 NAT 超时时间比较短3 分钟容易在空闲时被掐断。2.3 端口号与异步应答机制协议明确列出了各场景使用的端口号端口用途7890长连接SP 与网关间7900短连接SP 与网关间或网关之间7930长连接网关之间9168短连接短信网关与汇接网关之间SP 对接时用 7890 端口建立长连接这是最常用的配置。交互过程中的应答方式是异步的任一个网元收到请求消息后应立即回送响应消息不需要等业务处理完成再回。例如 SP 提交 CMPP_SUBMIT 后ISMG 先回 CMPP_SUBMIT_RESP携带 Msg_Id后续短信的实际投递结果通过 CMPP_DELIVER 消息异步发给 SP。这个机制和 HTTP 的同步请求响应模式完全不同初接触的人容易在这里卡住。3. 消息结构与核心操作读懂消息头就成功了一半3.1 消息头格式与 Command_Id 定义所有 CMPP 消息都包含一个 12 字节的消息头Message Header这是解析一切消息的基础字段名字节数类型描述Total_Length4Unsigned Integer消息总长度含消息头及消息体Command_Id4Unsigned Integer命令或响应类型Sequence_Id4Unsigned Integer消息流水号顺序累加步长为 1循环使用一对请求和应答消息的流水号必须相同这是匹配请求与响应的重要依据。Command_Id 是识别消息类型的关键字段SP 对接主要用到这几个Command_Id消息类型方向0x00000001CMPP_CONNECTSP - ISMG0x80000001CMPP_CONNECT_RESPISMG - SP0x00000002CMPP_TERMINATE双向0x80000002CMPP_TERMINATE_RESP双向0x00000004CMPP_SUBMITSP - ISMG0x80000004CMPP_SUBMIT_RESPISMG - SP0x00000005CMPP_DELIVERISMG - SP0x80000005CMPP_DELIVER_RESPSP - ISMG0x00000008CMPP_ACTIVE_TEST双向0x80000008CMPP_ACTIVE_TEST_RESP双向注意高位置 1 表示响应消息这是 CMPP 消息解析时判断请求还是响应的重要规律。3.2 连接建立与鉴权CMPP_CONNECT 的实现细节CMPP_CONNECT 是 SP 向 ISMG 注册身份的操作注册成功后建立应用层连接。消息体格式如下字段名字节数描述Source_Addr6源地址即 SP_Id企业代码AuthenticatorSource16鉴权码MD5 计算得出Version1版本号高位 4bit 主版本低位 4bit 次版本Timestamp4时间戳明文格式 MMDDHHMMSS10 位数字整型右对齐鉴权码的计算公式是AuthenticatorSource MD5(Source_Addr 9字节的0 shared_secret timestamp)注意几个细节。shared secret 是和中国移动事先商定的密钥不会出现在协议文档里需要找移动侧要。9 字节的 0 是固定补位不能省略。timestamp 用的是明文时间戳格式是月日时分秒各两位拼接成 10 位数字。响应消息 CMPP_CONNECT_RESP 里的 Status 字段含义要记清楚0 正确1 消息结构错2 非法源地址3 认证错4 版本太高5 及以上是其他错误。收到 3 就检查 MD5 计算是否正确收到 4 就检查 Version 字段是否填了 2。3.3 短信提交与状态报告CMPP_SUBMIT 和 CMPP_DELIVERCMPP_SUBMIT 是 SP 向 ISMG 提交短信的操作。消息体中几个关键字段Registered_Delivery是否要求返回状态确认报告。0 不需要1 需要2 产生 SMC 话单仅计费不发送Msg_Fmt信息格式。0 是 ASCII 串8 是 UCS2 编码15 是含 GB 汉字Msg_Length信息长度。Msg_Fmt 为 0 时小于 160 字节其他格式小于等于 140 字节Msg_Content信息内容Fee_UserType计费用户类型0 对目的终端计费1 对源终端计费2 对 SP 计费Src_Id源号码SP 的服务代码或前缀为服务代码的长号码用户手机上显示的主叫号码这个字段是 CMPP_SUBMIT 里最容易填错的一个。Src_Id 填的是服务代码不是 SP_Id。服务代码是用户点播时看到的号码比如 1069 开头的号码而 SP_Id 是 6 位企业代码两个概念完全不同。ISMG 收到提交后返回 CMPP_SUBMIT_RESP携带 8 字节的 Msg_Id。这个 Msg_Id 的生成算法很有讲究64 位整数中 bit64~bit39 是时间月日时分秒bit38~bit17 是网关代码bit16~bit1 是序列号。SP 通过请求和应答消息的 Sequence_Id 一致性来对应 Msg_Id。CMPP_DELIVER 是 ISMG 向 SP 送交短信的操作包括两类消息一类是用户上行短信MO另一类是短信状态报告MT 状态报告。判断是哪类消息看 Msg_Content 开头的字段状态报告的 Msg_Content 里会包含类似“id:xxxxxxxxxx sub:001 dlvrd:001 submit date:xxxxx done date:xxxxx stat:DELIVRD”的文本。3.4 长短信拆分的实现协议里 CMPP_SUBMIT 的消息体包含 Pk_total 和 Pk_number 字段分别表示相同 Msg_Id 的信息总条数和当前序号。当短信内容超过单条长度限制时ASCII 小于 160 字节UCS2 小于等于 140 字节需要拆分成多条 SUBMIT 消息发送。拆分的关键是每个分片使用相同的 Msg_IdPk_total 是总片数Pk_number 从 1 开始递增。接收方根据这三个字段重组完整短信。注意协议里有一句话“若 SP 对于群发消息不要求状态报告的回送时才可以考虑群发否则必须逐条发送。”这意味着如果业务需要状态报告就不能用群发方式每条短信要单独提交。4. 从零实现 CMPP2.0 客户端连接、鉴权、提交一条龙4.1 定义消息结构体用 C 语言写一个最小可用的 CMPP2.0 客户端第一步是定义协议消息结构。根据协议的消息头格式所有消息共用一个 12 字节头部// 消息头结构所有CMPP消息通用 typedef struct { unsigned int total_length; // 消息总长度含消息头与消息体 unsigned int command_id; // 命令字请求与响应的区分标记 unsigned int sequence_id; // 流水号请求与应答必须一致 } cmpp_header_t; // 连接请求消息体 typedef struct { char source_addr[6]; // SP企业代码6字节 char authenticator_source[16]; // MD5鉴权码 char version; // 版本号CMPP2.0填0x20 unsigned int timestamp; // MMDDHHMMSS格式时间戳 } cmpp_connect_t; // 提交短信消息头只列关键字段 typedef struct { unsigned long long msg_id; // 信息标识提交时填0 char pk_total; // 总条数从1开始 char pk_number; // 序号从1开始 char registered_delivery; // 是否要状态报告 char msg_level; // 信息级别 char service_id[10]; // 业务类型 char fee_user_type; // 计费用户类型 char fee_terminal_id[21]; // 被计费号码 char tp_pid; // GSM协议类型 char tp_udhi; // GSM协议类型 char msg_fmt; // 信息格式 char msg_src[6]; // 信息内容来源 char fee_type[2]; // 资费类别 char fee_code[6]; // 资费代码 // ... 后续字段按协议定义展开 } cmpp_submit_t;结构体定义的关键是字节对齐问题。C 语言编译器默认会对齐结构体成员但 CMPP 协议要求字段严格按字节顺序排列没有填充字节。所以定义结构体时必须用#pragma pack(push, 1)或__attribute__((packed))强制单字节对齐否则结构体大小和实际发送的数据长度不一致网关会返回“消息结构错”。4.2 实现连接与鉴权流程TCP 连接建立后第一步发 CMPP_CONNECT。这里的鉴权码计算是重点#include stdio.h #include string.h #include openssl/md5.h #include time.h // 生成时间戳格式MMDDHHMMSS unsigned int make_timestamp() { time_t now time(NULL); struct tm *tm_now localtime(now); // 月份1-12日期1-31小时0-23分钟0-59秒0-59 // 拼成10位十进制整数如 0402153045 表示4月2日15点30分45秒 return (tm_now-tm_mon 1) * 100000000 tm_now-tm_mday * 1000000 tm_now-tm_hour * 10000 tm_now-tm_min * 100 tm_now-tm_sec; } // 计算AuthenticatorSource // 公式MD5(Source_Addr 9字节0 shared_secret timestamp明文) void make_auth_source(const char *sp_id, const char *shared_secret, unsigned int timestamp, unsigned char *output) { char input[128] {0}; int len 0; // 拼接SP_ID(6字节) 9字节0 shared_secret 10位时间戳 memcpy(input len, sp_id, 6); len 6; memset(input len, 0, 9); // 9字节的全零补位 len 9; strcpy(input len, shared_secret); len strlen(shared_secret); sprintf(input len, %010u, timestamp); // 时间戳右对齐10位 MD5((unsigned char *)input, len, output); } // 发送连接请求 int cmpp_connect(int sockfd, const char *sp_id, const char *shared_secret) { unsigned char buffer[1024] {0}; cmpp_header_t *header (cmpp_header_t *)buffer; cmpp_connect_t *conn (cmpp_connect_t *)(buffer sizeof(cmpp_header_t)); unsigned int timestamp make_timestamp(); // 填写消息头 header-total_length 12 27; // 消息头12字节 连接消息体27字节 header-command_id 0x00000001; // CMPP_CONNECT header-sequence_id next_sequence_id(); // 全局递增流水号 // 填写消息体 memcpy(conn-source_addr, sp_id, 6); make_auth_source(sp_id, shared_secret, timestamp, conn-authenticator_source); conn-version 0x20; // 版本2.0主版本2次版本0 conn-timestamp timestamp; // 发送数据 int sent send(sockfd, buffer, header-total_length, 0); if (sent ! header-total_length) { printf(发送CMPP_CONNECT失败\n); return -1; } // 等待CMPP_CONNECT_RESP unsigned char resp[1024] {0}; int recved recv(sockfd, resp, sizeof(resp), 0); if (recved 12) return -1; cmpp_header_t *resp_header (cmpp_header_t *)resp; if (resp_header-command_id ! 0x80000001) { printf(收到非CMPP_CONNECT_RESP消息\n); return -1; } // 检查状态码0为认证成功 unsigned char status resp[12]; if (status ! 0) { printf(连接认证失败状态码: %d\n, status); return -1; } printf(CMPP连接成功\n); return 0; } // 全局流水号生成器每发一条消息1循环使用 unsigned int next_sequence_id() { static unsigned int seq 1000000; // 起点自定 if (seq 0xFFFFFFFF) seq 1; return seq; }鉴权码计算是整个对接流程中最容易出问题的环节。MD5 的输入字符串拼接顺序必须是Source_Addr 9字节0 shared_secret timestamp少一个字节或者顺序错了都会导致认证失败。shared secret 是移动侧分配的密钥务必存放在服务端的配置中心或环境变量里别写死在代码仓库里。时间戳的格式是 MMDDHHMMSS 共 10 位十进制数月份不足两位时左补零。比如 4 月 2 日 15 点 30 分 45 秒就是 0402153045。这个值同时出现在鉴权码计算和消息体的 Timestamp 字段里两处必须一致。4.3 提交短信与处理响应连接建立后提交短信的流程相对直接// 构建并发送CMPP_SUBMIT支持单条和长短信拆分 int cmpp_submit(int sockfd, const char *sp_id, const char *service_code, const char *dest_phone, const unsigned char *content, int content_len, int msg_fmt, int need_report) { // 计算分片数UCS2编码每条最多140字节ASCII最多160字节 int max_len (msg_fmt 0) ? 159 : 140; int total_pk (content_len max_len - 1) / max_len; if (total_pk 0) total_pk 1; // 长短信拆分每片用相同msg_idpk_number从1递增 for (int pk 1; pk total_pk; pk) { unsigned char buffer[2048] {0}; cmpp_header_t *header (cmpp_header_t *)buffer; // 这里按协议字段顺序填充submit消息体 // 注意msg_id填0pk_total填total_pkpk_number填pk int offset 0; unsigned char *body buffer 12; // 填充关键字段 memset(body offset, 0, 8); // Msg_Id填空 offset 8; body[offset] total_pk; // Pk_total body[offset] pk; // Pk_number body[offset] need_report ? 1 : 0; // Registered_Delivery body[offset] 0; // Msg_level // ... 中间字段按协议定义依次填充 // 计算本次发送的片内容 int send_len (pk total_pk) ? (content_len - max_len * (pk - 1)) : max_len; // 填充Msg_Content header-total_length 12 offset send_len; header-command_id 0x00000004; // CMPP_SUBMIT header-sequence_id next_sequence_id(); send(sockfd, buffer, header-total_length, 0); } // 读取CMPP_SUBMIT_RESP检查Result字段 // Result为0时提交成功此时从响应中提取Msg_Id用于后续状态报告匹配 return 0; }提交短信后必须立即读取响应不能等所有分片发完再统一读。因为滑动窗口机制限制接收方未应答的请求最多 16 条如果只发不读窗口满了网关会丢弃后续消息。建议在代码里实现一个简单的发送队列和应答匹配逻辑每发一条消息就把流水号记下来收到响应后从队列里移除。Msg_Id 的匹配逻辑要特别注意。CMPP_SUBMIT_RESP 里的 Msg_Id 是网关生成的 8 字节整数而请求里的 Sequence_Id 是流水号。两者不是同一个值。要做的是用 Sequence_Id 匹配请求和响应再从响应里取 Msg_Id。后续状态报告CMPP_DELIVER里的 Msg_Id 就是提交响应里的那个值据此关联到原始短信。4.4 链路检测与断开连接长连接模式下链路检测不能省。需要单独起一个定时线程每 3 分钟发一次 CMPP_ACTIVE_TEST// 发送链路检测包保持长连接 int cmpp_active_test(int sockfd) { unsigned char buffer[12] {0}; cmpp_header_t *header (cmpp_header_t *)buffer; header-total_length 12; // 无消息体 header-command_id 0x00000008; // CMPP_ACTIVE_TEST header-sequence_id next_sequence_id(); int sent send(sockfd, buffer, 12, 0); if (sent ! 12) { printf(发送链路检测包失败\n); return -1; } // 等待CMPP_ACTIVE_TEST_RESP超时60秒 // 超时后重发连续3次无响应则断开连接 return 0; }链检测包的响应消息同样没有消息体。如果在 60 秒内没收到响应要立即重发连续 3 次没响应就认为连接已经断了需要主动 close socket 并重新走连接流程。我见过有的实现不做链路检测结果连接被网关静默断开后发送消息一直超时重传造成消息堆积。5. 避坑指南对接 CMPP2.0 时最常翻车的五个问题5.1 认证失败MD5 鉴权码计算错误现象CMPP_CONNECT_RESP 返回 Status3认证错连接无法建立。原因AuthenticatorSource 的计算有多个细节坑。最常见的是 MD5 输入串拼接顺序错误把 shared_secret 放到了 Source_Addr 前面其次是 9 字节的补位零被省略还有时间戳用了字符串拼接但位数不足 10 位。解决严格按公式MD5(Source_Addr 9字节0 shared_secret timestamp)计算时间戳用%010u格式化成 10 位。调试时可以先用已知的测试密钥和数据算一遍和移动侧提供的联调文档对照结果。5.2 消息结构错结构体字节对齐问题现象CMPP_CONNECT_RESP 返回 Status1消息结构错或者对方收到非法的 Total_Length。原因C 语言结构体默认有字节对齐比如 4 字节的 int 成员前面可能有填充字节。CMPP 协议要求字段连续排列不带任何填充结构体大小和实际字节数不一致。解决所有协议结构体定义都要用#pragma pack(push, 1)或__attribute__((packed))。另外用sizeof()验证结构体大小消息头必须是 12 字节CMPP_CONNECT 消息体必须是 27 字节不对就查对齐问题。5.3 消息序号重复导致请求被丢弃现象偶发发送超时网关长时间不回 CMPP_SUBMIT_RESP日志里出现“消息序号重复”的错误。原因Sequence_Id 是 32 位无符号整数步长为 1 循环使用。如果程序重启后从同一个初始值开始计数而网关侧还保留着之前的部分流水号记录新连接的消息序号就和之前重复。解决程序启动时用当前时间戳做流水号初始值的一部分比如取时间戳的低位作为起始序号。同时确保每发一条消息流水号严格加 1不能在同一个连接里重复使用同一个流水号。5.4 长短信拆分后状态报告对应不上现象长短信发出后收到的多个状态报告无法和原始短信关联业务侧统计送达率错乱。原因拆分后的每个分片是独立的 CMPP_SUBMIT 消息各自拿到不同的 Msg_Id状态报告也按分片返回。如果没有在提交时记录分片与原始短信的对应关系就无法回推整条长短信的送达情况。解决提交前先在业务层生成自己的消息唯一 ID拆分发送时记录业务ID - 分片序号 - Msg_Id的映射表。收到状态报告后先根据 Msg_Id 找到对应的分片再汇总同一条业务 ID 下所有分片的状态取最终状态作为整条短信的结果。5.5 TCP 粘包导致消息解析错乱现象高并发下消息解析出现乱码CMPP_SUBMIT_RESP 里读出来的 Msg_Id 完全不对或者直接解析出非法的 Command_Id。原因TCP 是流式协议多条 CMPP 消息可能在一个包里到达也可能一条消息被拆到两个包里。如果直接按 recv 返回值解析就会错位。解决维护一个接收缓冲区按照消息头的 Total_Length 字段来判断完整消息边界——先把 12 字节消息头读完解析出 Total_Length再读取剩余的数据凑够 Total_Length 才算一条完整消息。缓冲区里可能有多条消息循环解析直到缓冲区的数据不足一条消息的长度为止。代码示例// 从TCP流中逐条解析CMPP消息 // 返回一条完整的消息数据不足则等待下次读取 int cmpp_recv_message(int sockfd, unsigned char *out_buf, int buf_size) { static unsigned char recv_buf[8192]; static int recv_len 0; // 读取数据到缓冲区 int n recv(sockfd, recv_buf recv_len, sizeof(recv_buf) - recv_len, 0); if (n 0) return -1; recv_len n; // 至少要有12字节的消息头才能解析Total_Length if (recv_len 12) return 0; // 数据不足继续等待 // 解析消息总长度 unsigned int total_len 0; memcpy(total_len, recv_buf, 4); total_len ntohl(total_len); // 注意大小端转换 // 校验消息长度合法性 if (total_len 12 || total_len 8000) { // 说明粘包后解析错位需要重新同步 return -2; } // 凑够一条完整消息再返回 if (recv_len total_len) return 0; memcpy(out_buf, recv_buf, total_len); // 移除已消费的数据 memmove(recv_buf, recv_buf total_len, recv_len - total_len); recv_len - total_len; return total_len; }这段代码有几个关键点。第一个是大小端转换CMPP 的消息头字段都是网络字节序x86 机器上必须用 ntohl 转换成本地字节序才能正确解析。第二个是缓冲区要足够大否则一条长短信加上其他消息可能把缓冲区撑爆。第三个是异常同步处理如果解析出的 Total_Length 非法小于 12 或大于合理上限说明粘包后数据已经错位需要断开连接重新建连这是最稳妥的恢复方式。6. 验证对接成果用抓包和日志状态码定位问题对接完成后验证环节要有章法。我习惯按三层递进排查先看 TCP 连接是否建立再看 CMPP 层的响应状态码最后看消息内容的字段是否正确。用 tcpdump 抓包是最直接的验证方法# 抓到与短信网关7890端口交互的所有数据包 sudo tcpdump -i eth0 host 网关IP and port 7890 -w cmpp.pcap # 用 -X 参数以十六进制和ASCII方式显示数据内容 sudo tcpdump -i eth0 host 网关IP and port 7890 -X抓到包后重点看前三件事CMPP_CONNECT 的 AuthenticatorSource 是否是 16 字节CMPP_CONNECT_RESP 的 Status 是否为 0CMPP_SUBMIT 的 Total_Length 与实际数据字节数是否一致。这些都能在 pcap 文件里逐字节核对。连接建立后我总会做一次完整的协议自检检查项通过标准TCP 三次握手抓到 SYN、SYN-ACK、ACK 三个包CMPP_CONNECT消息总长度 39 字节12 消息头 27 消息体CMPP_CONNECT_RESPStatus0AuthenticatorISMG 为 16 字节CMPP_SUBMITMsg_Fmt 与短信内容编码一致Msg_Length 正确CMPP_SUBMIT_RESPResult0Msg_Id 为 8 字节非零值CMPP_ACTIVE_TEST每 3 分钟出现一次响应及时状态报告的处理在验证阶段容易被忽略。CMPP_DELIVER 消息的 Msg_Content 里包含一段格式化文本典型格式是id:xxx sub:001 dlvrd:001 submit date:xxxxxx done date:xxxxxx stat:DELIVRD err:0。其中 stat 字段是最终投递状态DELIVRD 表示成功UNDELIV 表示未送达。验证时做一个脚本把 stat 字段抽出来统计能快速看出通道的送达率。Msg_Id 的生成规则也值得在验证阶段顺手核一下。8 字节的 Msg_Id 前 5 字节是时间月日时分秒中间是网关代码最后是序列号。如果从状态报告里解析出来的 Msg_Id 对应的时间戳和实际发送时间相差太大说明消息在途时间异常这时候要检查链路质量。从那以后我每次对接新的短信网关都强制自己先抓包、再逐字段核对、最后才看业务数据。哪怕是已经跑通的通道每次发版前也会走一遍这个流程——协议这东西平时不出问题一出问题就是消息结构错乱或者密码找不回来自检流程能省下不少半夜加班的力气。希望这份 CMPP2.0 协议的拆解和踩坑记录能帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进