简介:中国移动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 消息不丢包、不乱序。
从网络结构看,整个链路涉及三个角色:
- SP(Service Provider):业务提供方,也就是我们自己的服务器
- ISMG(Internet Short Message Gateway):互联网短信网关,SP 直接对接的移动侧设备
- GNS(Gateway 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_Length | 4 | Unsigned Integer | 消息总长度(含消息头及消息体) |
| Command_Id | 4 | Unsigned Integer | 命令或响应类型 |
| Sequence_Id | 4 | Unsigned Integer | 消息流水号,顺序累加,步长为 1,循环使用 |
一对请求和应答消息的流水号必须相同,这是匹配请求与响应的重要依据。
Command_Id 是识别消息类型的关键字段,SP 对接主要用到这几个:
| Command_Id | 消息类型 | 方向 |
|---|---|---|
| 0x00000001 | CMPP_CONNECT | SP -> ISMG |
| 0x80000001 | CMPP_CONNECT_RESP | ISMG -> SP |
| 0x00000002 | CMPP_TERMINATE | 双向 |
| 0x80000002 | CMPP_TERMINATE_RESP | 双向 |
| 0x00000004 | CMPP_SUBMIT | SP -> ISMG |
| 0x80000004 | CMPP_SUBMIT_RESP | ISMG -> SP |
| 0x00000005 | CMPP_DELIVER | ISMG -> SP |
| 0x80000005 | CMPP_DELIVER_RESP | SP -> ISMG |
| 0x00000008 | CMPP_ACTIVE_TEST | 双向 |
| 0x80000008 | CMPP_ACTIVE_TEST_RESP | 双向 |
注意高位置 1 表示响应消息,这是 CMPP 消息解析时判断请求还是响应的重要规律。
3.2 连接建立与鉴权:CMPP_CONNECT 的实现细节
CMPP_CONNECT 是 SP 向 ISMG 注册身份的操作,注册成功后建立应用层连接。消息体格式如下:
| 字段名 | 字节数 | 描述 |
|---|---|---|
| Source_Addr | 6 | 源地址,即 SP_Id(企业代码) |
| AuthenticatorSource | 16 | 鉴权码,MD5 计算得出 |
| Version | 1 | 版本号,高位 4bit 主版本,低位 4bit 次版本 |
| Timestamp | 4 | 时间戳明文,格式 MMDDHHMMSS,10 位数字整型,右对齐 |
鉴权码的计算公式是:
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_DELIVER
CMPP_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_Id,Pk_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_id,pk_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填0,pk_total填total_pk,pk_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 返回 Status=3(认证错),连接无法建立。
原因:AuthenticatorSource 的计算有多个细节坑。最常见的是 MD5 输入串拼接顺序错误,把 shared_secret 放到了 Source_Addr 前面;其次是 9 字节的补位零被省略;还有时间戳用了字符串拼接但位数不足 10 位。
解决:严格按公式MD5(Source_Addr + 9字节0 + shared_secret + timestamp)计算,时间戳用%010u格式化成 10 位。调试时可以先用已知的测试密钥和数据算一遍,和移动侧提供的联调文档对照结果。
5.2 消息结构错:结构体字节对齐问题
现象:CMPP_CONNECT_RESP 返回 Status=1(消息结构错),或者对方收到非法的 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 是否为 0,CMPP_SUBMIT 的 Total_Length 与实际数据字节数是否一致。这些都能在 pcap 文件里逐字节核对。
连接建立后,我总会做一次完整的协议自检:
| 检查项 | 通过标准 |
|---|---|
| TCP 三次握手 | 抓到 SYN、SYN-ACK、ACK 三个包 |
| CMPP_CONNECT | 消息总长度 39 字节(12 消息头 + 27 消息体) |
| CMPP_CONNECT_RESP | Status=0,AuthenticatorISMG 为 16 字节 |
| CMPP_SUBMIT | Msg_Fmt 与短信内容编码一致,Msg_Length 正确 |
| CMPP_SUBMIT_RESP | Result=0,Msg_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 协议的拆解和踩坑记录能帮到你。
本文还有配套的精品资源,点击获取