华为短信网关SMProxy合并协议Java开发包实战:从协议解析到短信收发 简介本资源为华为短信网关协议开发包面向从事短信网关对接的Java开发人员提供SMProxy合并协议的完整API与配套文档帮助开发者快速完成SMPP、SMGP、SGIP、CNGP、CMPP等多种短消息代理协议的接入与调试。压缩包共32个文件约2.25MB包含13个doc使用说明书、5个C源码文件、4个zip与4个txt说明、1个jar包及ini、dll、map、bmp、h等辅助文件覆盖Windows与Unix双平台说明书分别对应各协议版本便于按需查阅。目前已有199人学习下载。借助其中的jar包与API示例读者可快速搭建短信收发环境理解各协议报文结构与调用方式并参考C源码与配置文件排查连接、编码等常见问题适合需要对接华为短信网关的中高级Java开发者参考使用。1. 从一条短信的“长征”说起这个开发包到底解决什么问题做过运营商短信对接的兄弟都懂一条短信从业务系统发出到用户手机震动中间要穿过短信网关、SMProxy、SMSC 好几道关卡。最折磨人的不是业务逻辑而是协议层——CMPP、SGIP、SMGP、SMPP 这几套东西字段顺序、心跳机制、长短信拆分规则各不相同换个省份可能就得重写一遍通信层。这份“华为短信网关协议开发包unixwindowsSMProxy合并协议 Java版本”就是冲着这个痛点来的它把 SMProxy 合并协议用 Java 实现了一遍同时给了 Unix 和 Windows 两套环境的适配让你不用再对着几百页协议文档一行行抠字节序。适合谁正在做短信网关对接的后端、需要把短信能力嵌进现有 Java 系统的中间件开发者以及想搞懂 SMProxy 合并协议到底怎么跑起来的学习者。下面我按“能干什么 → 怎么跑 → 怎么改”的顺序把这个包拆开讲。2. 协议栈与工程结构SMProxy 合并协议在 Java 里怎么落地2.1 SMProxy 合并协议的核心设计思路SMProxy 合并协议的本质是在标准短信网关协议之上做了一层“聚合代理”。传统做法是业务系统直接连 SMSC但 SMSC 的连接数有限、鉴权方式各异一旦要接多个省份的网关代码里就会堆满 if-else。SMProxy 的思路是业务系统只跟 SMProxy 说话由 SMProxy 去跟后面的真实网关做协议转换和路由。这个 Java 版本把合并协议拆成了三层。最底层是Codec层负责字节流和消息对象之间的编解码所有字段的偏移量、长度、字节序都固化在这里中间是Session层管理连接的生命周期包括登录、心跳、重连、提交和下发最上面是Dispatcher层把上行的 deliver 消息按业务类型分发给注册的处理器。这种分层的好处是换协议只需要动 Codec换网络库只需要动 Session业务代码基本不用碰。常见做法是每个协议单独一个 Codec 类但这里用了合并协议所以 Codec 里会有一个protocolType字段来区分当前走的是哪套底层协议。我一般会在配置文件里把协议类型、网关地址、端口、账号密码、心跳间隔都抽出来避免硬编码。2.2 目录结构拿到包先看这几个文件解压之后工程结构大致是这样的不同版本可能略有出入但核心目录跑不掉smproxy-java/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── smproxy/ │ │ │ ├── codec/ # 编解码协议字段定义 │ │ │ ├── session/ # 连接、心跳、重连 │ │ │ ├── dispatcher/ # 上行消息分发 │ │ │ ├── message/ # 消息实体类 │ │ │ └── util/ # 字节操作、BCD 转换 │ │ └── resources/ │ │ ├── smproxy.properties # 主配置 │ │ └── logback.xml # 日志配置 ├── lib/ # 依赖 jarUnix/Windows 各一套 ├── bin/ │ ├── start.sh # Unix 启动脚本 │ └── start.bat # Windows 启动脚本 └── pom.xml # Maven 构建文件重点看codec和session两个包。codec里的SMProxyCodec是核心所有encode和decode方法都在这里session里的SMProxySession管连接状态HeartbeatTask是心跳定时任务。resources下的smproxy.properties是你跑起来之前必须改的文件里面至少有网关 IP、端口、账号、密码、心跳间隔、重连次数这几个参数。提示Unix 和 Windows 的差异主要在启动脚本和本地库加载路径上。Java 层代码是跨平台的但lib目录下的依赖 jar 可能因为 JNI 或系统属性不同而分开放别拿错。2.3 核心配置项跑之前必须改的五个参数smproxy.properties里参数不少但真正决定能不能连上的就这几个参数名含义典型值注意点smproxy.gateway.host短信网关 IP10.x.x.x别填成 SMSC 的地址smproxy.gateway.port网关端口7890不同省份可能不同smproxy.account账号企业代码区分大小写smproxy.password密码加密串有的版本要求 MD5smproxy.heartbeat.interval心跳间隔秒30太长会被网关断连我一般会先把日志级别调到 DEBUG跑一次看登录请求和响应确认账号密码没问题再调业务。如果登录返回错误码先查账号是否开通了对应协议权限再查 IP 是否在白名单里——这两个是血泪经验里出现频率最高的翻车点。3. 跑起来从编译到收发第一条短信3.1 环境准备与编译这个包是 Java 版本所以 JDK 是必须的。从代码风格看用的应该是 JDK 8 或 11 的语法没有看到 record 或 var 的大面积使用。Maven 构建依赖都在pom.xml里但lib目录下也放了一些 jar可能是为了离线环境准备的。编译步骤# 进入工程根目录 cd smproxy-java # 如果有 Maven直接编译打包 mvn clean package -DskipTests # 如果没有 Maven用 lib 下的 jar 手动编译 javac -cp lib/* -d target/classes src/main/java/com/smproxy/**/*.java编译完之后target目录下会生成smproxy-java-1.0.jar名字可能不同。如果是手动编译需要自己把target/classes和lib下的 jar 一起放到 classpath 里。注意Windows 下路径分隔符是分号Unix 下是冒号。写脚本的时候别搞混否则会报ClassNotFoundException。3.2 启动脚本与运行参数bin目录下的启动脚本已经写好了 classpath 和主类但你需要确认几个地方。以start.sh为例#!/bin/bash # 设置 Java 环境 JAVA_HOME/usr/local/jdk1.8.0_291 # 设置 classpath注意 lib 下所有 jar 都要包含 CLASSPATH.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar for jar in lib/*.jar; do CLASSPATH$CLASSPATH:$jar done CLASSPATH$CLASSPATH:target/classes # 启动主类传入配置文件路径 $JAVA_HOME/bin/java -cp $CLASSPATH \ -Dsmproxy.configresources/smproxy.properties \ com.smproxy.SMProxyMain关键参数是-Dsmproxy.config它告诉程序去哪里读配置。如果你把配置文件放在别的地方改这个路径就行。SMProxyMain是入口类里面会初始化 Session、注册 Dispatcher、启动心跳线程。Windows 下对应的start.bat逻辑一样只是路径分隔符和JAVA_HOME写法不同。我一般会在启动前先telnet一下网关 IP 和端口确认网络通再跑程序不然日志里全是连接超时浪费时间。3.3 发送第一条短信代码调用示例跑起来之后怎么发短信这个包一般会提供一个SMProxyClient或者MessageSender之类的工具类。下面是一个典型的调用示例// 初始化客户端加载配置 SMProxyClient client new SMProxyClient(); client.init(resources/smproxy.properties); // 构造短信消息 SubmitMessage msg new SubmitMessage(); msg.setPhoneNumber(13800138000); // 接收号码 msg.setContent(测试短信内容); // 短信内容 msg.setServiceType(SMS); // 业务类型 msg.setPriority(1); // 优先级1 最高 // 发送并获取响应 SubmitResponse resp client.submit(msg); if (resp.isSuccess()) { System.out.println(发送成功消息 ID resp.getMessageId()); } else { System.err.println(发送失败错误码 resp.getErrorCode() 错误描述 resp.getErrorDesc()); } // 关闭连接 client.close();这段代码的逻辑很直白init加载配置并建立连接SubmitMessage封装了提交请求的所有字段submit方法内部会走 Codec 编码、Session 发送、等待响应。SubmitResponse里的messageId是网关返回的唯一标识后面查状态或者对账都用它。参数说明phoneNumber必须是 11 位手机号不带国家码content长度超过 70 个字符时Codec 会自动拆成多条长短信但你要确认网关是否支持长短信合并serviceType一般填SMS或空具体看网关文档priority影响网关的调度顺序不是所有网关都支持。提示第一次跑建议先发给自己别直接上生产号码。短信发出去是要计费的测试阶段用测试账号或者白名单号码。3.4 接收上行短信与状态报告发出去只是第一步上行短信和状态报告才是业务闭环的关键。这个包在Dispatcher层提供了注册处理器的方式// 注册上行短信处理器 client.registerDeliverHandler(new DeliverHandler() { Override public void onDeliver(DeliverMessage msg) { System.out.println(收到上行短信 msg.getPhoneNumber() - msg.getContent()); // 这里可以写业务逻辑比如入库、转发 } }); // 注册状态报告处理器 client.registerReportHandler(new ReportHandler() { Override public void onReport(ReportMessage msg) { System.out.println(状态报告消息 ID msg.getMessageId() 状态 msg.getStatus()); // 更新数据库里的发送状态 } });DeliverMessage里包含上行号码、内容、时间戳ReportMessage里包含原消息 ID 和最终状态DELIVRD 表示成功其他都是失败。状态报告是异步的可能延迟几秒到几分钟所以业务侧要做好幂等别重复更新。我一般会把状态报告先落库再用定时任务去对账避免网络抖动导致报告丢失。这个包本身不提供持久化需要你自己在 Handler 里实现。4. 避坑与排查连不上、发不出、收不到怎么查4.1 登录失败账号密码都对就是连不上现象程序启动后日志显示Login failed错误码是0x0001或类似。原因最常见的是 IP 没加到网关白名单或者账号没有开通 SMProxy 合并协议的权限。其次是密码加密方式不对——有的网关要求明文有的要求 MD5还有的要求先 Base64 再 MD5。解决先找网关侧确认白名单和权限再对照协议文档检查密码加密方式。这个包的smproxy.properties里一般有个password.encrypt开关确认它和网关要求一致。4.2 心跳超时连接莫名其妙断了现象跑一段时间后日志出现Heartbeat timeout然后自动重连业务中断几秒。原因心跳间隔设置太长超过了网关的空闲超时时间或者心跳包格式不对网关不认。解决把heartbeat.interval调到 30 秒以内一般 20-30 秒比较稳。如果还是断抓包看心跳包的字节内容对照协议文档检查字段。有的网关要求心跳包和登录包用同一个序列号这个包的HeartbeatTask里应该处理了但版本不同可能有差异。4.3 长短信被拆成多条用户收到好几条重复的现象发一条 100 字的短信用户收到两条内容被截断。原因Codec 在编码时按 70 个字符拆分但没有设置TP_udhi或concat字段导致网关把每条都当成独立短信发出去。解决检查SubmitMessage里有没有setLongSms(true)或者类似的开关。如果有打开它如果没有需要在 Codec 里手动设置 UDH 头。这个包的合并协议版本应该支持长短信但配置里可能默认关闭了。4.4 状态报告收不到消息 ID 对不上现象短信发出去了但ReportHandler一直不触发或者触发了但消息 ID 和提交时返回的不一样。原因有的网关在状态报告里返回的是自己内部的消息 ID不是提交时返回的那个。还有的网关要求客户端在提交时带上registeredDelivery字段才会回报告。解决先确认提交请求里registeredDelivery设为 1。如果消息 ID 对不上需要在SubmitResponse里把网关返回的 ID 和本地业务 ID 做映射收到报告后用网关 ID 反查。4.5 Unix 下跑正常Windows 下报 UnsatisfiedLinkError现象Windows 启动时报java.lang.UnsatisfiedLinkError提示找不到某个本地库。原因lib目录下的 jar 可能包含 JNI 本地库Unix 和 Windows 的.so和.dll不通用。或者启动脚本里的java.library.path没设置对。解决确认用的是 Windows 版的lib目录启动脚本里加上-Djava.library.pathlib。如果还是不行检查 JDK 位数是否和本地库匹配32 位 vs 64 位。5. 二次开发改协议字段、加业务钩子、做压测5.1 改协议字段Codec 层怎么动二次开发最常见的需求是适配不同省份的网关它们的协议字段可能有细微差异。比如某个省份要求serviceType字段填SMS另一个省份要求填空。这时候不需要改业务代码只需要在 Codec 层做适配。// 在 SMProxyCodec 的 encode 方法里根据配置动态设置字段 public byte[] encode(SubmitMessage msg) { ByteBuffer buffer ByteBuffer.allocate(256); // ... 其他字段编码 // 根据配置决定 serviceType 的值 String serviceType config.getProperty(smproxy.serviceType, SMS); if (StringUtils.isBlank(serviceType)) { // 填空字符串占位但不写内容 buffer.put((byte) 0x00); } else { byte[] stBytes serviceType.getBytes(StandardCharsets.US_ASCII); buffer.put(stBytes); } // ... 继续编码其他字段 return buffer.array(); }这段代码的关键是config.getProperty它从配置文件读smproxy.serviceType如果没配就用默认值SMS。这样换省份只需要改配置文件不用重新编译。我一般会把所有可能变化的字段都抽成配置项Codec 里只做“读配置 → 编码”的动作。参数说明ByteBuffer.allocate(256)里的 256 是预估的最大包长如果短信内容很长需要动态计算。StandardCharsets.US_ASCII是因为协议字段一般用 ASCII 编码别用 UTF-8否则中文会出问题。5.2 加业务钩子Dispatcher 层怎么扩展Dispatcher层是业务逻辑的入口。这个包默认只提供了打印日志的 Handler实际项目里你需要注册自己的 Handler 来做入库、转发、告警。// 自定义上行处理器实现业务逻辑 public class MyDeliverHandler implements DeliverHandler { private final MessageService messageService; public MyDeliverHandler(MessageService messageService) { this.messageService messageService; } Override public void onDeliver(DeliverMessage msg) { // 1. 先落库保证不丢 Long id messageService.saveUpstream(msg); // 2. 根据内容做业务路由 if (msg.getContent().startsWith(TD)) { // 退订指令调用退订服务 messageService.unsubscribe(msg.getPhoneNumber()); } else { // 普通上行转发到业务系统 messageService.forward(msg); } // 3. 记录处理结果 messageService.updateStatus(id, PROCESSED); } }这个 Handler 的逻辑是“先落库再处理”避免业务逻辑抛异常导致消息丢失。saveUpstream返回的id用来后续更新状态。startsWith(TD)是退订指令的常见判断方式实际项目里可能更复杂比如支持多个关键词。注册方式在启动时调用client.registerDeliverHandler(new MyDeliverHandler(messageService))就行。注意 Handler 里不要做耗时操作否则会阻塞 Dispatcher 线程影响后续消息处理。耗时逻辑应该丢到线程池里异步执行。5.3 压测与性能调优怎么知道能扛多少量上线前必须压测。这个包本身没有压测工具但你可以写一个简单的 JMeter 脚本或者 Java 压测程序直接调client.submit方法。// 简单压测并发提交 1000 条短信 ExecutorService pool Executors.newFixedThreadPool(50); CountDownLatch latch new CountDownLatch(1000); AtomicInteger success new AtomicInteger(0); AtomicInteger fail new AtomicInteger(0); for (int i 0; i 1000; i) { final int index i; pool.submit(() - { try { SubmitMessage msg new SubmitMessage(); msg.setPhoneNumber(13800138000); msg.setContent(压测短信 index); SubmitResponse resp client.submit(msg); if (resp.isSuccess()) { success.incrementAndGet(); } else { fail.incrementAndGet(); } } catch (Exception e) { fail.incrementAndGet(); } finally { latch.countDown(); } }); } latch.await(60, TimeUnit.SECONDS); System.out.println(成功 success.get() 失败 fail.get());压测时重点看三个指标TPS每秒提交数、响应时间P99 是否超过 500ms、错误率。如果 TPS 上不去先看网关侧有没有限流再看 Session 层是不是单连接瓶颈。这个包默认可能是单连接高并发下需要改成连接池。我一般会在压测前把日志级别调到 WARN避免 DEBUG 日志拖慢性能。另外压测号码要用测试号段别用真实用户号码否则会被投诉。5.4 一个容易忽略的细节序列号回绕SMProxy 协议里每个请求都有一个序列号通常是 4 字节整数。这个包在Session层用AtomicInteger或者AtomicLong来生成序列号但 4 字节最大是 2^31-1跑久了会回绕。回绕本身没问题但如果网关侧用序列号做去重回绕后可能把新请求当成重复请求丢掉。解决方式是在序列号回绕时主动断开重连或者跟网关确认它是否用序列号去重。我一般会在Session里加一个判断当序列号超过某个阈值比如 2^30时主动重置连接。这个细节在协议文档里通常不会写但生产环境跑几个月就会遇到。从那以后我每次对接新网关都会先问清楚序列号回绕怎么处理再跑压测验证。希望帮到你。本文还有配套的精品资源点击获取