简介:本资源为华为短信网关协议开发包,面向从事短信网关对接的Java开发者,尤其是需要在Unix与Windows双平台下接入运营商短信通道的工程师。包内整合了SMProxy合并协议相关API与Jar包,覆盖SMPP、SMGP、SGIP、CNGP、CMPP30、CMPP20等多种短消息代理协议,并配有对应的中文使用说明书,便于开发者理解各协议的交互流程与接口调用方式。资源共32个文件,以13个doc说明文档为主,辅以C源码、zip示例包、txt配置说明及jar、dll、h等库文件,压缩包约2.25MB,结构紧凑、便于按协议模块查阅。目前已有199人学习下载,适合需要快速搭建短信收发环境、对照协议文档完成联调与排错的Java开发人员参考使用。
1. 短信网关协议开发包:从协议合并到跨平台落地的完整实践
做过短信网关对接的开发者大概都有过这种体验:手里拿着运营商给的协议文档,CMPP、SGIP、SMGP、SMPP 几套协议各说各话,光是理解消息头结构和交互时序就得啃上好几天。更麻烦的是,很多存量系统跑在 Unix 上,新业务又要求 Windows 部署,同一套网关逻辑得维护两份代码。这个 SMProxy 合并协议 Java 版本要解决的就是这个问题——它把多套短信网关协议抽象成统一的 Java 接口层,同时提供 Unix 和 Windows 两个平台的原生库适配,让上层业务代码不用关心底层跑在什么系统上,也不用为每种协议单独写一套收发逻辑。适合正在做短信网关对接、协议适配层开发,或者需要把存量 Unix 网关服务迁移到 Java 体系的开发者。
2. 架构拆解:SMProxy 合并协议的分层设计与跨平台适配机制
2.1 协议合并的核心思路
SMProxy 这个名字里的“合并”不是简单地把几套协议塞进一个 jar 包,而是在消息模型层面做统一抽象。短信网关协议虽然细节不同,但核心交互模式高度相似:客户端登录认证、提交短信、接收状态报告、心跳保活、退出登录。SMProxy 把这些共性抽成Message基类和Session接口,各协议的具体实现类只负责处理自己特有的字段编码和交互时序。
这种设计的好处在于,上层业务只需要面向Session接口编程。比如你要发一条短信,调用的都是session.submit(message),至于底层走的是 CMPP 的Submit还是 SGIP 的Submit,由配置决定。协议切换对业务代码透明,这在需要同时对接多家运营商网关的场景下特别实用。
从代码组织上看,核心包结构大致是这样的:
com.smproxy.core // 核心接口与抽象类 com.smproxy.protocol.cmpp // CMPP 协议实现 com.smproxy.protocol.sgip // SGIP 协议实现 com.smproxy.protocol.smgp // SMGP 协议实现 com.smproxy.protocol.smpp // SMPP 协议实现 com.smproxy.transport // 传输层,含 Unix/Windows 适配 com.smproxy.util // 编解码工具、字节缓冲等transport包是跨平台的关键。Unix 下通常用原生 socket 或 domain socket,Windows 下则走 Winsock 封装。SMProxy 的做法是在这一层做条件编译式的适配——通过 JNI 调用本地库,或者用 Java NIO 做纯 Java 实现,具体取决于部署环境。
2.2 消息编解码的字节级实现
短信网关协议基本都是二进制协议,消息头固定长度,消息体按 TLV 或固定字段排列。SMProxy 里每个协议实现都有一个Codec类,负责Message对象和字节数组之间的转换。以 CMPP 的 Connect 消息为例:
public class CmppConnectCodec implements Codec<CmppConnectMessage> { @Override public byte[] encode(CmppConnectMessage msg) { ByteBuffer buf = ByteBuffer.allocate(12 + 6 + 16 + 1 + 4); // 消息头:总长度(4) + 命令ID(4) + 序列号(4) buf.putInt(0); // 占位,最后回填 buf.putInt(CMPP_CONNECT); // 命令ID buf.putInt(msg.getSequenceId()); // 序列号 // 消息体:源地址(6) + 认证码(16) + 版本号(1) + 时间戳(4) buf.put(fixedString(msg.getSourceAddr(), 6)); buf.put(fixedString(msg.getAuthenticator(), 16)); buf.put((byte) msg.getVersion()); buf.putInt(msg.getTimestamp()); // 回填总长度 buf.putInt(0, buf.position()); return buf.array(); } @Override public CmppConnectMessage decode(byte[] data) { ByteBuffer buf = ByteBuffer.wrap(data); buf.getInt(); // 跳过总长度 int cmdId = buf.getInt(); int seqId = buf.getInt(); CmppConnectMessage msg = new CmppConnectMessage(); msg.setSequenceId(seqId); msg.setSourceAddr(readFixedString(buf, 6)); msg.setAuthenticator(readFixedString(buf, 16)); msg.setVersion(buf.get()); msg.setTimestamp(buf.getInt()); return msg; } }这里有几个关键点值得注意。fixedString方法处理的是定长字符串的填充和截断——短信协议里地址字段通常是 6 字节,不足补零,超长截断。ByteBuffer的字节序默认是大端,这符合绝大多数短信协议的规定,但如果遇到小端协议,需要在ByteBuffer上显式设置order(ByteOrder.LITTLE_ENDIAN)。序列号的生成策略也很重要,SMProxy 里用了一个原子递增的AtomicInteger,保证同一连接内序列号不重复,这在异步收发场景下是必须的。
2.3 跨平台传输层的适配策略
Unix 和 Windows 在 socket 行为上有一些微妙差异,比如SO_REUSEADDR的默认行为、连接超时的处理、以及select和epoll/IOCP的模型区别。SMProxy 的传输层没有试图完全屏蔽这些差异,而是提供了一个TransportFactory,根据os.name系统属性返回不同的Transport实现。
public class TransportFactory { public static Transport createTransport(TransportConfig config) { String os = System.getProperty("os.name").toLowerCase(); if (os.contains("win")) { return new WindowsNioTransport(config); } else { return new UnixNioTransport(config); } } }WindowsNioTransport和UnixNioTransport都实现了Transport接口,核心方法包括connect、send、receive、close。在 Unix 下,如果配置了 domain socket 路径,会走UnixDomainSocketAddress;Windows 下则统一走 TCP。这种设计让上层协议实现完全不用关心底层传输细节,只需要调用transport.send(bytes)就行。
注意:跨平台适配最容易翻车的地方是超时处理。Unix 下
SO_TIMEOUT对 NIO 的Selector不生效,需要用select(timeout)手动控制;Windows 下则相对统一。SMProxy 在TransportConfig里提供了connectTimeout和readTimeout两个参数,底层实现各自处理。
3. 跑起来:从环境准备到第一条短信发出的完整流程
3.1 环境依赖与项目导入
SMProxy 是 Java 项目,运行需要 JDK 8 或以上。如果用到 JNI 本地库,还需要对应平台的编译工具链——Unix 下是 gcc + make,Windows 下是 Visual Studio 的 cl.exe。不过纯 Java 模式下不需要这些,直接 Maven 构建即可。
# 克隆项目后进入根目录 mvn clean package -DskipTests # 如果只需要纯 Java 实现,跳过 native 编译 mvn clean package -DskipTests -Pjava-only构建产物在target/下,核心 jar 包是smproxy-core-x.x.x.jar,协议实现包按需引入。如果项目里用了 JNI,还会生成libsmproxy.so(Unix)或smproxy.dll(Windows),需要放到java.library.path能找到的目录。
3.2 配置文件与连接参数
SMProxy 的配置走的是 properties 文件,放在 classpath 下或者通过启动参数指定路径。一个典型的 CMPP 网关配置长这样:
# 协议类型:cmpp / sgip / smgp / smpp smproxy.protocol=cmpp # 网关地址和端口 smproxy.host=10.0.0.1 smproxy.port=7890 # 登录参数 smproxy.sourceAddr=106900001 smproxy.authenticator=your_auth_code smproxy.version=0x30 # 传输参数 smproxy.connectTimeout=5000 smproxy.readTimeout=30000 smproxy.heartbeatInterval=60000 # 跨平台参数 smproxy.transport.type=nio smproxy.transport.unixDomainSocket=falsesourceAddr是网关分配的企业代码,authenticator是认证码,这两个参数填错的话,登录会直接返回错误码。heartbeatInterval控制心跳间隔,一般设成 60 秒,太短浪费连接资源,太长可能被网关判定为死连接。
3.3 发送第一条短信
配置就绪后,发短信的代码其实很简洁:
public class SendSmsDemo { public static void main(String[] args) throws Exception { // 加载配置 SmProxyConfig config = SmProxyConfig.load("smproxy.properties"); // 创建会话 Session session = SmProxy.newSession(config); session.connect(); // 建立连接并登录 // 构造短信消息 SubmitMessage msg = new SubmitMessage(); msg.setPhoneNumber("13800138000"); msg.setContent("测试短信内容"); msg.setServiceId("SVC001"); msg.setFeeType((byte) 1); // 1=免费,2=按条收费 // 提交并获取结果 SubmitResult result = session.submit(msg); if (result.isSuccess()) { System.out.println("发送成功,消息ID:" + result.getMessageId()); } else { System.err.println("发送失败,错误码:" + result.getErrorCode()); } session.close(); } }session.connect()内部做了三件事:建立 TCP 连接、发送 Connect 消息、等待 ConnectResp 并校验结果。如果登录失败,会抛出SmProxyException,里面包含网关返回的错误码。session.submit()是同步调用,底层发完 Submit 后等待 SubmitResp,拿到消息 ID 才算成功。如果需要高吞吐,可以用session.submitAsync(),返回CompletableFuture<SubmitResult>。
提示:第一次跑的时候建议把日志级别调到 DEBUG,SMProxy 会把收发的原始字节流打出来,方便对照协议文档排查问题。
4. 避坑指南:协议对接中常见的五个翻车现场
4.1 登录返回 0x03 错误码
现象:session.connect()抛出异常,错误码 0x03,提示“认证失败”。
原因:authenticator字段填的是明文认证码,但 CMPP 协议要求的是 MD5 加密后的 16 字节。有些网关还要求加上时间戳一起做 MD5。
解决:检查CmppConnectCodec里的authenticator处理逻辑,确认是否需要 MD5。SMProxy 默认做了 MD5,但如果网关要求的是“认证码+时间戳”的拼接 MD5,需要改encode方法里的认证码生成部分。
4.2 长短信被截断
现象:发送超过 70 个字符的短信,接收方只收到前 70 个字符。
原因:短信协议里长短信需要拆分成多条Submit,每条带 TP_udhi 头标识和分片序号。SMProxy 默认不自动拆分,需要显式配置。
解决:在配置里加上smproxy.longSms.split=true,或者手动调用LongSmsSplitter.split(content)拿到分片列表,逐条提交。注意每个分片的pkTotal和pkNumber字段要正确设置。
4.3 Windows 下连接超时不生效
现象:在 Windows 上跑,connectTimeout设了 5 秒,但实际等了 30 秒才报错。
原因:Windows 的 NIOSocketChannel.connect在非阻塞模式下不直接支持超时,需要配合Selector做超时控制。SMProxy 的WindowsNioTransport里如果没正确处理,就会走系统默认超时。
解决:确认WindowsNioTransport.connect方法里用了Selector.open()+select(timeout)的模式。如果用的是阻塞模式,需要设置channel.socket().setSoTimeout()。
4.4 心跳包发了但网关还是断连
现象:日志显示心跳包正常发送,但网关每隔几分钟就断开连接。
原因:心跳包的序列号没有递增,网关认为收到了重复包,直接丢弃。或者心跳包的命令 ID 用错了,比如 CMPP 的心跳是0x00000008,不是0x00000001。
解决:检查HeartbeatTask里的序列号生成逻辑,确保每次心跳都用新的序列号。同时对照协议文档确认命令 ID。
4.5 状态报告收不到
现象:短信发送成功,但状态报告回调一直没触发。
原因:状态报告是网关主动推送的Deliver消息,需要客户端在Session上注册监听器。如果没注册,或者监听器里抛了异常被吞掉,就看不到报告。
解决:调用session.setDeliverListener(listener)注册回调,并在回调里打印日志确认收到。注意Deliver消息的registeredDelivery字段要设为 1,否则网关不会推报告。
5. 二次开发:协议扩展与性能调优的实操建议
5.1 新增一套协议实现
SMProxy 的协议扩展点设计得比较清晰,新增一套协议大概需要做四件事:
第一,在com.smproxy.protocol下新建包,比如com.smproxy.protocol.xxx。第二,实现Protocol接口,提供createCodec()和createSession()方法。第三,为每种消息类型写Codec实现,继承AbstractCodec可以省掉一些样板代码。第四,在ProtocolRegistry里注册新协议,或者在配置文件里指定全限定类名。
public class XxxProtocol implements Protocol { @Override public Codec<?> createCodec(int commandId) { switch (commandId) { case XXX_CONNECT: return new XxxConnectCodec(); case XXX_SUBMIT: return new XxxSubmitCodec(); case XXX_DELIVER: return new XxxDeliverCodec(); default: throw new UnsupportedOperationException("未知命令: " + commandId); } } @Override public Session createSession(SmProxyConfig config) { return new XxxSession(config, this); } }XxxSession继承AbstractSession,主要覆写doConnect()、doSubmit()、doHeartbeat()这几个模板方法。AbstractSession已经处理了连接管理、序列号生成、超时重试这些通用逻辑,子类只需要关注协议特有的交互流程。
5.2 性能调优的几个关键参数
SMProxy 的性能瓶颈通常不在协议编解码,而在连接管理和线程模型。下面这几个参数值得根据实际压测结果调整:
| 参数 | 默认值 | 建议范围 | 说明 |
|---|---|---|---|
smproxy.threadPool.size | 4 | 8-32 | 处理 Deliver 回调的线程数 |
smproxy.buffer.size | 4096 | 8192-16384 | 收发缓冲区大小 |
smproxy.heartbeatInterval | 60000 | 30000-120000 | 心跳间隔,太短浪费资源 |
smproxy.reconnect.interval | 5000 | 3000-10000 | 断线重连间隔 |
smproxy.max.pending | 1000 | 500-5000 | 异步提交的最大待确认数 |
如果吞吐量上不去,先看threadPool.size是不是太小,Deliver 回调处理不过来会阻塞后续消息。如果延迟高,检查buffer.size和网络 RTT。max.pending设太大可能导致内存积压,设太小则异步提交会被阻塞。
5.3 一个容易忽略的细节:序列号回绕
短信协议的序列号通常是 4 字节整数,范围 0 到 2^31-1。高并发场景下,序列号增长很快,虽然回绕到 0 的概率极低,但一旦发生,如果网关那边还有旧序列号的待确认消息,就可能出现串包。SMProxy 的做法是在序列号接近上限时主动重建连接,把序列号重置。这个逻辑在AbstractSession的nextSequenceId()里:
protected int nextSequenceId() { int seq = sequenceId.incrementAndGet(); if (seq < 0) { // 回绕了 synchronized (this) { if (sequenceId.get() < 0) { // 重建连接,重置序列号 reconnect(); sequenceId.set(1); } } return sequenceId.incrementAndGet(); } return seq; }这段代码看起来简单,但少了synchronized和双重检查,在并发下就可能出问题。我当初第一次写的时候就没加锁,压测时偶尔出现序列号重复,排查了好久才定位到。从那以后我每次写这种带状态的回绕逻辑,都强制走一遍并发测试,确认没有竞态条件才敢上线。希望这个开发包能帮你在短信网关对接上少走些弯路。
本文还有配套的精品资源,点击获取