ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CMPP3.0 Java实现:协议拆解与避坑指南

CMPP3.0 Java实现:协议拆解与避坑指南 简介面向Java短信网关开发者的CMPP3.0协议实现参考包围绕中国移动CMPP3.0规范覆盖短信提交、接收、状态查询等核心业务可直接作为短信服务接入与二次开发的基础示例。压缩包共17个文件以14个Java源文件为主辅以properties配置文件与2个txt说明文档整体仅23KB代码量精简但模块划分清晰common包封装公共处理逻辑msg包处理消息业务定时器示例配合心跳机制使用。实现中重点演示了TCP长连接与心跳保持、GBK编码转换、多线程处理并发请求、异常重试与短信状态跟踪等关键模块与CMPP3.0十大学习要点一一对应说明文档对配置项和运行方式也给出必要引导。目前已有949人学习适合具备一定Java基础、正在对接短信网关或需要快速理解CMPP3.0报文格式与收发流程的开发者通过阅读源码与配置可掌握Java环境下CMPP3.0的落地结构并据此扩展自己的生产级实现。1. cmpp3.0_JAVA_实现为什么你的短信网关项目绕不开这组关键词做短信网关对接的 Java 工程师十有八九都在项目里见过“cmpp3.0_JAVA_实现”这组关键词。它不是什么高深算法而是中国移动短信网关 CMPP3.0 协议的接入落地用 Java 写一个能收发短信、能收状态报告、能扛住一定并发的客户端模块。网上资料零散协议文档又是十六进制黑话导致很多人卡在登录报文和滑动窗口上连上就断、发了没响应。这篇文章我按自己的落地路径讲清楚协议怎么拆、代码怎么组织、哪些参数不能乱调最后把最容易翻车的坑挨个点出来。适合正在接短信通道、或者被派去维护老短信系统的读者新手能照着写熟手可以拿避坑清单当检查项。2. CMPP3.0 协议先立住四种消息、无符号整数和滑动窗口2.1 四种消息类型和 Java 里的命令字CMPP3.0 的通信不是 HTTP是一条 TCP 长连接上的二进制消息流。虽然文档里有十几种消息但 Java 实现里真正高频的只有四组Connect登录鉴权、Submit下发短信、Deliver上行短信和状态报告、ActiveTest心跳。真正断开连接用的 Terminate 在客户端主动退出时才会用到。先把命令字整理成表后面写解码器会反复用到消息作用Command_Id建议常量名登录请求0x00000001CMPP_CONNECT登录响应0x80000001CMPP_CONNECT_RESP下发短信请求0x00000002CMPP_SUBMIT下发短信响应0x80000002CMPP_SUBMIT_RESP上行/状态报告请求0x00000003CMPP_DELIVER上行/状态报告响应0x80000003CMPP_DELIVER_RESP心跳请求0x00000004CMPP_ACTIVE_TEST心跳响应0x80000004CMPP_ACTIVE_TEST_RESP注意 Command_Id 的规律请求的最高位是 0响应最高位是 1。这个规律在调试时很有用看到 0x8 开头就知道是网关回包。但在 Java 里有个小坑0x80000001 超过了 int 的正数范围读出来可能是负数。我一般用 long 或者 Integer.compareUnsigned 来做比较避免“这个数怎么是负的”这种问题。2.2 消息头、字节序和 Sequence_IdCMPP3.0 每条消息开头固定 12 字节的消息头三个 int 字段全部是大端序Total_Length、Command_Id、Sequence_Id。Total_Length 是整个消息的长度包含这 12 字节本身Sequence_Id 是流水号从 0 开始累加用来匹配请求和响应。协议里几乎全是无符号整数Java 的 int 也是 32 位但最高位是符号位。好消息是用 Netty 的 ByteBuf 写入时 writeInt 只是按位写Java 正负数不影响网络字节序坏消息是从 ByteBuf 读的时候要用 readUnsignedInt 才能拿到正确的 0-4294967295 范围值。我习惯把消息头独立封装出来避免每个消息体都重复处理粘包和半包。public class CMPPMessageHeader { public int totalLength; public int commandId; public int sequenceId; public void encode(ByteBuf out) { out.writeInt(totalLength); out.writeInt(commandId); out.writeInt(sequenceId); } public void decode(ByteBuf in) { totalLength in.readInt(); commandId in.readInt(); sequenceId (int) in.readUnsignedInt(); } }这段代码里的关键点是 sequenceId 读取。协议里 Sequence_Id 是无符号如果直接 readInt 会读到负数后续用这个值做 key 匹配响应时容易出问题。网络字节序方面ByteBuf 默认就是大端和 CMPP 协议一致不需要额外调 ByteOrder。参数上sequenceId 用 AtomicInteger 生成就够了。注意它最大到 0xFFFFFFFF到达上限后要归零。如果用了 readUnsignedInt就不会因为符号问题导致回绕判断错误。这里再说一句别用 synchronized 保护一个 int 自增AtomicInteger 足够网关接口是长连接请求量上来后锁竞争会拖慢整个发送链路。2.3 滑动窗口并发发送前的第一个控制参数CMPP3.0 的滑动窗口机制简单说就是同一时刻最多能有多少条 Submit 消息没收到 Submit_Resp。窗口大小规范默认是 16具体值由网关侧配置决定客户端必须遵守。如果客户端无限往里灌网关会直接断开连接而且不会告诉你原因。Java 里实现窗口最干净的方式是信号量。每条 Submit 发送前 acquire收到 Submit_Resp 后 release。这样发送线程会被自然阻塞而不是把消息堆进无界队列后内存爆掉。private final Semaphore window new Semaphore(16); public void acquireWindow() throws InterruptedException { if (!window.tryAcquire(3, TimeUnit.SECONDS)) { throw new IllegalStateException(滑动窗口已满网关响应过慢); } } public void releaseWindow() { window.release(); }窗口大小为什么是 16 而不是 100这是协议设计好的背压阈值超过阈值网关会认为客户端失控。我用 tryAcquire 而不是 acquire是为了在窗口长期占满时让发送线程快速失败而不是无限阻塞否则故障时线程池会被卡满。3 秒超时是个经验值真实网关一般几十毫秒到几百毫秒就回 Submit_Resp如果 3 秒都没窗口说明响应链路已经不正常应该告警而不是继续等。2.4 用 Java 对象建模协议字段定长字符串的坑CMPP3.0 的消息体里大量使用定长字符串比如 Source_Addr 固定 6 字节Service_Id 固定 10 字节。协议规定不足部分按位补 0不是补空格。很多人把 String 直接 getBytes 塞进去结果长度不够多出来的随机数据导致网关解析错乱。我一般先封装一个定长编码方法统一处理这种情况public static byte[] fixedString(String value, int length, Charset charset) { byte[] raw value.getBytes(charset); if (raw.length length) { throw new IllegalArgumentException(字段超长当前值 value); } byte[] out new byte[length]; System.arraycopy(raw, 0, out, 0, raw.length); return out; }补充说明定长字段在 CMPP 文档里通常标注“字符串”但具体是 ASCII 还是 GBK要看字段类型。比如 Source_Addr 和 Msg_Src 是数字组成的企业代码用 ASCII 就可以Service_Id 可能是字母加数字也建议 ASCII。Msg_Content 的业务内容才根据 Msg_Fmt 用 UCS2 或 GBK。用 charset 参数显式传入能避免将来换服务器后平台默认编码变了导致乱码。这个细节就是 CMPP3.0_Java 实现里最常见的“看着代码没问题一上线就出事”的源头。3. 从零搭一个 CMPP3.0 Java 客户端五个可复现的步骤3.1 选型Netty 还是原生 SocketCMPP3.0 是二进制协议必然涉及粘包、半包、字节序转换。原生 Socket 也能做但所有协议解析都要自己写还要自己管理线程池。Netty 的优势在于 ByteBuf、ChannelPipeline 和内置的定时任务能让代码结构干净很多。Mina 也见过人用但近年新项目选 Netty 更多社区资料也全。选型对比可以按这个参考方案协议解析线程模型维护成本原生 Socket自己处理容易漏字节每连接一线程扩展麻烦低依赖但出问题全得自己扛Mina自带解码器有 IoHandler 模型老项目多新资料少NettyByteBuf 解码器EventLoop 异步模型需要一点学习曲线我选 Netty。下面是客户端初始化的最小骨架EventLoopGroup group new NioEventLoopGroup(2); Bootstrap bootstrap new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_KEEPALIVE, true) .handler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new CMPPDecoder()); ch.pipeline().addLast(new CMPPHandler()); } }); ChannelFuture future bootstrap.connect(host, port).sync(); Channel channel future.channel();TCP_NODELAY 必须设为 true否则小字节的 CMPP 包会被 Nagle 算法合并导致网关侧响应延迟明显变大。SO_KEEPALIVE 只是内核级保活不能替代业务心跳这个后面会专门说。CMPPDecoder 要做的事就是读前 4 字节的 Total_Length再按长度读完整包解决粘包半包。NioEventLoopGroup 线程数我用 2一个负责 IO一个留给协议处理实际连接数和消息量上来后再调。3.2 登录鉴权CMPP_CONNECT 的 Java 实现登录是第一个坑点。CMPP_CONNECT 消息体包含 Source_Addr、AuthenticatorSource、Version、Timestamp 四个字段。其中 AuthenticatorSource 是 MD5 结果16 字节算法是MD5(Source_Addr 9字节0 sharedSecret Timestamp)。这里 9 字节 0 很容易漏掉漏了网关回你 3认证失败。private ByteBuf buildConnectRequest(String spCode, String sharedSecret, int timestamp) { ByteBuf buf Unpooled.buffer(); int totalLength 12 6 16 1 4; int sequenceId sequenceIdGenerator.incrementAndGet(); buf.writeInt(totalLength); buf.writeInt(0x00000001); buf.writeInt(sequenceId); buf.writeBytes(fixedString(spCode, 6, StandardCharsets.ASCII)); buf.writeBytes(buildAuthenticatorSource(spCode, sharedSecret, timestamp)); buf.writeByte(0x30); // Version 3.0 buf.writeInt(timestamp); return buf; } private byte[] buildAuthenticatorSource(String spCode, String sharedSecret, int timestamp) throws Exception { byte[] spBytes spCode.getBytes(StandardCharsets.ASCII); byte[] secretBytes sharedSecret.getBytes(StandardCharsets.ASCII); ByteBuffer input ByteBuffer.allocate(spBytes.length 9 secretBytes.length 4); input.put(spBytes); input.put(new byte[9]); input.put(secretBytes); input.putInt(timestamp); return MessageDigest.getInstance(MD5).digest(input.array()); }timestamp 不是常见的时间戳而是 MMDDHHMMSS 格式比如 4 月 15 日 14 时 30 分 05 秒就是 0415143005作为 int 写入。组装时注意 Source_Addr 固定 6 字节如果 spCode 不足 6 位用 fixedString 补 0。Version 是 0x30表示 3.0不是 3。很多文档写“版本为30”结果有人直接写 3网关也能连上但某些网关上功能受限我遇到过。3.3 心跳与重连CMPP_ACTIVE_TEST 和指数退避CMPP 网关一般要求 30 秒内至少有一次业务报文或心跳否则会断开连接。我习惯用一个 ScheduledExecutorService 固定每 30 秒发一次 ActiveTest即使刚发送过 Submit 也照发逻辑简单不会因为忘记重置计时器而被踢下线。private ScheduledExecutorService heartBeatScheduler Executors.newSingleThreadScheduledExecutor(); public void startHeartBeat() { heartBeatScheduler.scheduleAtFixedRate(() - { if (channel ! null channel.isActive()) { channel.writeAndFlush(new CMPPActiveTestRequest(sequenceIdGenerator.incrementAndGet())); } }, 30, 30, TimeUnit.SECONDS); }收到心跳响应用一个 AtomicInteger 记录最近一次响应时间如果连续 3 次心跳没响应就判定连接已死主动关闭并触发重连。重连不要写死循环用指数退避第一次等待 1 秒第二次 2 秒最多 30 秒避免网关恢复期间客户端高频重连把网关打崩。另外心跳线程一定要独立不能和业务线程共用如果业务线程被滑动窗口阻塞心跳还能继续发这个隔离能救很多次。3.4 发送 CMPP_SUBMIT组装报文和控制窗口Submit 是项目里流量最大的部分。消息体字段多但关键的就几个Msg_Id8 字节本地填 0响应里回填、Pk_total、Pk_number、Registered_Delivery、Msg_Fmt、Msg_Src、Src_Id、Msg_Length、Msg_Content。Registered_Delivery 设为 1才能收到状态报告Msg_Fmt 这里先按 ASCII 处理中文短信用 UCS2后面长短信拆分再细讲。发送前必须走窗口信号量。完整发送代码如下public void sendSubmit(CMPPSubmitRequest request) throws InterruptedException { acquireWindow(); ByteBuf buf Unpooled.buffer(); request.encode(buf); channel.writeAndFlush(buf).addListener((ChannelFuture future) - { if (!future.isSuccess()) { releaseWindow(); log.error(submit 发送失败, future.cause()); } }); } public void onSubmitResp(CMPPSubmitResp resp) { releaseWindow(); if (resp.getStatus() ! 0) { log.warn(submit 返回错误 status{}, msgId{}, resp.getStatus(), resp.getMsgId()); } else { log.info(submit 成功 msgId{}, resp.getMsgId()); } }注意 writeAndFlush 失败时也要 releaseWindow否则窗口会被永久占用。onSubmitResp 里只做 window 释放和日志记录具体业务更新放在另一个异步线程池避免阻塞 Netty 的 EventLoop。如果在这个 Handler 里直接操作数据库网关并发一高EventLoop 卡住心跳就发不出去紧接着就是连接断开这是很多压测翻车的直接原因。3.5 Spring Boot 里的配置组织项目里我不会把协议代码和业务配置混在一起。用 Spring Boot 的话连接参数、窗口大小、心跳间隔全放 application.ymlcmpp: host: 192.168.10.20 port: 3150 sp-code: 100001 shared-secret: test123 window-size: 16 heartbeat-interval-sec: 30 reconnect-max-wait-sec: 30然后写一个 CMPPProperties 类用 ConfigurationProperties 绑定。服务启动时创建 CMPPClient用 SmartLifecycle 控制启动顺序应用关闭时先发 Terminate 再释放连接。这里要提醒一句连接建立不等于登录成功登录成功报文是 CONNECT_RESP这里的 Status 字段 0 才表示认证通过。我见过有的项目只检测了 TCP 是否连接就对外报通道可用结果状态监控一片绿短信一条都发不出去。4. 消息路由与长短信拆分Java 实现里的高频业务点4.1 区分 Deliver 上行和状态报告Is_Report 字段说了算网关推送的 CMPP_DELIVER 有两类用户上行短信和状态报告。区分方式很简单看消息体里的 Is_Report 字段。Is_Report0 是用户上行需要往业务系统转Is_Report1 是状态报告要解析里面的 Stat 字段更新短信发送状态。状态报告的 Msg_Content 是一段格式化文本常见是空行分隔的字段比如stat:DELIVRD done_time:20250615143005 sub_time:20250615142930解析代码不要写复杂正则按行 split 再按冒号拆一次就够了public static MapString, String parseStatusReport(byte[] msgContent, Charset charset) { String text new String(msgContent, charset); MapString, String result new HashMap(); for (String line : text.split(\\r?\\n)) { int idx line.indexOf(:); if (idx 0) { result.put(line.substring(0, idx).trim(), line.substring(idx 1).trim()); } } return result; }状态报告常见 Stat 值就三种DELIVRD成功、EXPIRED过期、UNDELIV不可达。我建议建一个枚举把未知状态先按失败处理并告警不要默默丢弃。另外状态报告的消息体编码不一定和上行短信一样有的网关用 GBK。Java 里不要默认 new String(msgContent)显式指定编码否则 Linux 部署后中文编译环境一变解析出来就乱。4.2 长短信拆分67 字一条不是 70 字中文短信一条最多 70 个汉字但这指的是不带 UDHI 头的普通短信。CMPP3.0 长短信需要在消息体前面加 6 字节的 UDHI 头用来标识分片信息所以真正留给短信内容的只有 67 个汉字。拆分时如果按 70 切分片会超长网关要么拒绝要么用户收到乱码。一个可用的拆分方法public static ListCMPPSubmitRequest splitLongMessage(String content, String mobile) { int maxCharsPerPart 67; int total (int) Math.ceil(content.length() / (double) maxCharsPerPart); ListCMPPSubmitRequest result new ArrayList(); for (int i 0; i total; i) { int start i * maxCharsPerPart; int end Math.min((i 1) * maxCharsPerPart, content.length()); String part content.substring(start, end); CMPPSubmitRequest request new CMPPSubmitRequest(); request.setMobile(mobile); request.setPkTotal(total); request.setPkNumber(i 1); request.setTpUdhi(1); request.setMsgFmt(8); byte[] partBytes part.getBytes(StandardCharsets.UTF_16BE); byte[] udhi buildUdhiHeader(total, i 1); byte[] msgContent new byte[udhi.length partBytes.length]; System.arraycopy(udhi, 0, msgContent, 0, udhi.length); System.arraycopy(partBytes, 0, msgContent, udhi.length, partBytes.length); request.setMsgContent(msgContent); request.setMsgLength(msgContent.length); result.add(request); } return result; } private static byte[] buildUdhiHeader(int total, int number) { return new byte[]{0x05, 0x00, 0x03, 0x0A, (byte) total, (byte) number}; }拆分时按 Java 的 char 数切不是按字节切。UCS2 下每个汉字是一个 char每个 char 两个字节67 个 char 正好 134 字节加 6 字节 UDHI 头是 140 字节。buildUdhiHeader 里 0x05 表示后面有 5 个长度字节0x00 0x03 是 TP_UDHI 的拆分标识0x0A 是参考号后两字节分别是总条数和当前条数。参考号可以固定也可以每条消息用随机数但总分片数不能超过 255因为这是 1 字节字段。这条逻辑里有个隐藏边界如果内容里包含 emojiJava 的 String.length 会把一个 emoji 记成两个 char按这个思路拆某些分片可能把代理对切半。遇到这种内容建议升级到按 CodePoint 切分或者直接限制用户输入短信场景里 emoji 本来就容易乱码。4.3 去重、存储和异常恢复数据库唯一索引是最后的兜底CMPP 消息在网络传输中可能重发。Deliver 上行、状态报告如果重复处理会给业务方造成重复订单或者错误状态。常见的做法是在数据库表里给网关消息 Msg_Id 加唯一索引入库时捕获 DuplicateKeyException直接忽略第二遍。CREATE TABLE sms_deliver_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, msg_id BIGINT NOT NULL, mobile VARCHAR(32) NOT NULL, is_report TINYINT NOT NULL, content TEXT, stat VARCHAR(20), receive_time DATETIME NOT NULL, UNIQUE KEY uk_msg_id (msg_id) );Java 端的处理逻辑要注意顺序先查一次再插入不如直接插入靠唯一索引拦并发下后一种才能真正防重。数据库层面的唯一约束是最靠谱的兜底应用层用 ConcurrentHashMap 做去重只能挡住单机 JVM 内的重复。发送侧的异常恢复更讲究。Submit 发出去了但没收到 Submit_Resp这时不能一概重发。因为消息可能已经到达网关重发会重复下发。我会在发送前给每条消息生成一个业务批次号把 Submit_Resp、Deliver 状态报告都关联到同一条发送记录重发前先查这条记录有没有任何回执有就不发。这样处理重启应用后也不会造成大面积重复短信。5. CMPP3.0_JAVA_实现避坑指南5 个让我翻车过的黑匣子CMPP3.0 的 Java 实现里最折磨人的往往不是代码本身而是问题现象看起来像网络玄学实际上都是协议细节。下面五条都是我在真实联调里踩过的每一条都按现象、原因、解决的顺序说。5.1 登录后立刻被断开先抓报文别猜现象TCP 连接已经建立也发送了 CMPP_CONNECT甚至收到了 CONNECT_RESPStatus 为 0但紧接着几秒内连接被网关断开。日志里没有任何异常只有连接关闭。原因AuthenticatorSource 的 MD5 计算错了。最常见的是漏掉 9 字节的 0 填充或者 Timestamp 格式写成了 Unix 时间戳。网关认证通过但后续第一个 Submit 报文格式不对也可能被立刻断开。解决先把收发的字节流打印出来用十六进制对比协议文档。Java 里加一个工具方法public static String toHex(byte[] data) { StringBuilder sb new StringBuilder(data.length * 2); for (byte b : data) { sb.append(String.format(%02x , b)); } return sb.toString(); }在发送 CONNECT 前后各打一行。看 Total_Length 是不是 39Command_Id 是不是 00000001AuthenticatorSource 是不是 16 字节。如果和样例报文不一致就别怀疑网关先修本地代码。5.2 Submit 返回 Status0用户却收不到短信现象CMPP_SUBMIT_RESP 里 Status 是 0业务日志显示发送成功但手机迟迟收不到短信状态报告也始终不来。原因账号配置和网关侧分配不一致。常见的有 Source_Addr 的 SP 企业代码填错Msg_Src 和 Source_Addr 混用Src_Id 设置了不存在的扩展短号。网关只校验来源认证不校验这些业务字段所以认证能过但消息被内部路由丢弃。解决拿网关分配的开户资料逐项核对。Source_Addr 是 6 位企业代码Msg_Src 是 SP_CodeSrc_Id 是显示主叫号码通常是服务代码或者扩展短号。先用最简消息测一条纯 ASCII 文本附带 Registered_Delivery1确认状态报告能回来再换成真实业务内容。不要直接灰度大批量发送否则收不到你得从成千上万条记录里排查。5.3 并发一上来就频繁重连EventLoop 被业务代码卡死了现象单条消息测试正常压测到几十条并发时开始出现 ACTIVE_TEST_RESP 超时然后连接断开客户端自动重连重连后又断。原因Netty 的 EventLoop 线程被阻塞了。最常见的是在 ChannelHandler 里直接同步查数据库、调用外部接口或者发送窗口没有控制消息队列积压导致响应处理延迟。心跳也走同一个 EventLoop心跳响应没人处理网关就判定超时断开。解决把 IO 线程和业务线程严格分开。Netty 的 Handler 只做协议编解码和窗口释放业务处理丢给独立线程池。窗口控制用前面写的 Semaphore发送前 tryAcquire拿不到就快速失败绝不无界堆积。另外检查是否在 EventLoop 里调用了 channel.writeAndFlush 的大包同步等待应该用监听器异步回调。5.4 内存溢出从几百 MB 涨到几个 G无界队列是元凶现象Java 进程启动时内存正常运行一段时间后堆内存持续上涨最终抛出 OutOfMemoryError应用重启后重复出现。原因发送线程和网关响应速度不匹配。网关响应慢提交到线程池的任务越来越多如果用的 LinkedBlockingQueue 没设容量任务全部积压在堆里。CMPP 消息内容一多内存直接被打满。解决有界队列加拒绝策略。不要用 Executors.newFixedThreadPool 里默认的无界队列改成BlockingQueueRunnable queue new ArrayBlockingQueue(10000); ThreadPoolExecutor pool new ThreadPoolExecutor( 8, 16, 60, TimeUnit.SECONDS, queue, new ThreadPoolExecutor.CallerRunsPolicy());CallerRunsPolicy 让提交线程自己执行任务形成天然背压比 AbortPolicy 更友好。JVM 启动参数按机器内存来不要跟风配大我一般用 -Xms512m -Xmx1024m堆太大反而让问题暴露得晚。5.5 Linux 上中文乱码显式指定字符集别吃平台默认值现象本地 Windows 开发测试正常部署到 Linux 后发送的中文短信到手机变问号或者收到的状态报告解析乱码。原因CMPP3.0 的消息内容是编码字节不携带字符集声明。Java 代码里用了 String.getBytes() 无参版本Windows 默认 GBKLinux 默认 UTF-8两边编码不一致字节流自然不对。网关按协议里 Msg_Fmt 指定的编码解析时数据已经错了。解决Java 代码里所有 CMPP 编解码都显式写字符集参数。中文短信 Msg_Fmt8 时用 UTF-16BE状态报告解析如果需要 GBK 就传 GBK不要依赖默认环境。编译时也固定编码mvn clean package -Dfile.encodingUTF-8同时检查 Spring Boot 的 server.servlet.encoding 配置虽然它影响不到这些字节流但统一 UTF-8 能减少其他环节的干扰。这个问题是血泪经验曾经线上乱码查了两天最后就是一行 getBytes() 少了 charset 参数。6. 进阶给 CMPP3.0 Java 客户端加一个 Mock 网关做回归验证6.1 用 Netty 写一个最小 Mock 网关真实网关不是随便就能连的联调要等工单、要排期出了问题两边还容易扯皮。我的习惯是在项目里保留一个 Mock 网关用来做自动化回归测试。它能做的就是收到 CONNECT 回 CONNECT_RESP收到 SUBMIT 回 SUBMIT_RESP收到 ACTIVE_TEST 回 ACTIVE_TEST_RESP。这样客户端代码改完跑一遍用例就可以确认协议层没坏。一个最小 Mock 网关的核心逻辑可以这样写public class MockCMPPServer { public void start() throws InterruptedException { EventLoopGroup bossGroup new NioEventLoopGroup(1); EventLoopGroup workerGroup new NioEventLoopGroup(1); ServerBootstrap bootstrap new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new CMPPDecoder()); ch.pipeline().addLast(new SimpleChannelInboundHandlerByteBuf() { Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { int commandId msg.getInt(4); if (commandId 0x00000001) { ctx.writeAndFlush(buildConnectResp(msg.getInt(8))); } else if (commandId 0x00000002) { ctx.writeAndFlush(buildSubmitResp(msg.getInt(8))); } else if (commandId 0x00000004) { ctx.writeAndFlush(buildActiveTestResp(msg.getInt(8))); } } }); } }); bootstrap.bind(3151).sync(); } }Mock 网关里不需要完整解析每个字段只需要读 Command_Id 和 Sequence_Id然后按相同 Sequence_Id 回包。CMPP 客户端一般用自己的流水号匹配响应所以 Mock 网关回包时把请求里的 Sequence_Id 原样带回去即可。这段代码的边界在于它不会校验 AuthenticatorSource所以只适合做客户端回归测试不适合做协议正确性验证。6.2 压测参数建议用 Mock 网关做压测时参数别随便拍脑袋。最基础的一组建议参数建议值说明发送线程数8不要超过窗口大小的 2 倍滑动窗口大小16与真实网关配置保持一致Submit 超时5 秒超过则记录失败压测时长10 分钟观察内存和连接稳定性心跳间隔30 秒模拟真实节奏压测时重点看两个指标成功发送的 TPS 和未响应消息积压数。如果 TPS 上不去但窗口一直为空说明发送线程被网关响应延迟拖着先查 Mock 网关的日志如果窗口一直满说明消费速度不够调大线程池之前先确认数据库写入有没有瓶颈。真实网关的响应时间和 Mock 网关差别很大正式上线前还是要用真实网关小流量跑一遍。我前两年接一个新网关上来就急着联调结果连不上折腾两天发现是 MD5 里少补了 9 个字节。那次之后我养成了一个习惯每个 CMPP 客户端项目都必须保留 Mock 网关协议层改动先跑回归再上真实环境验证。短信通道这东西不提前准备好后悔药出事时连定位的抓手都没有。希望帮到你。本文还有配套的精品资源点击获取
返回列表