SMSGate
这是一个在netty4框架下实现的三网合一短信网关核心框架,支持(cmpp/smpp3.4/sgip1.2/smgp3) 短信协议解析,支持长短信合并和拆分,也支持wap短信和闪信。
Install / Use
/learn @Lihuanghe/SMSGateREADME
技术问题请加QQ群

群名称:cmppGate短信 <br/>群 号:770738500
How To Use
<dependency>
<groupId>com.chinamobile.cmos</groupId>
<artifactId>sms-core</artifactId>
<version>2.1.13.6</version>
</dependency>
商用短信网关平台推荐
平台名称:SMSWG短信网关系统官网:www.smswg.com 系统有3个PC端:管理员端+用户端+代理商端,支持代理分销短信,日发送支持上亿,实时精准计费,不阻塞高并发,单机每秒可支持1.5万条短信下发,更多系统功能请进官网查看演示系统。
常见问题
-
纯客户端发送短信可以使用sms-client, 一个纯发送短信的客户端jar包,Api简单。【sgip协议用sms-client无法接收上行和状态报告】
也可以参考htt2cmpp 实现将一个短信长连接协议封装成http接口。
或者参考smsServer用SpringBoot实现一个能支持http,cmpp,sgip,smgp,smpp等多种协议的网关服务。
-
没看懂如何发送短信?短信协议是tcp长连接,类似数据库连接,如jdbc-connection. 所以发送短信前必须要先有一个短信连接。因此你需要在程序启动时建立短信连接。参考demo里的client,调用manager.openEntity()方法,,调用manager.startConnectionCheckTask()开启断线重连。 然后就像调用其它库一样,在需要发送短信的地方,new 一个对应的Message,调用
List< Future > f = ChannelUtil.syncWriteLongMsgToEntity([clientEntityId],message)方法发送,
要判断f是否为Null,为Null表示发送失败,一条短信可能拆分成多条,因此返回List。 -
关闭默认超速错误自动重发功能如CMPP协议接收到错误码为8的响应(超速错误),系统默认会再次重发直到成功,最大重试次数默认是30次。如果要关闭默认重试功能,须设置
entity.overSpeedSendCountLimit为0SGIP、SMPP的超速错误码是88,于CMPP协议相同,也会超速重发。
SMGP 协议因为未定义超速错误码,不会超速重试。
-
如何发送长短信?smsgate默认已经处理好长短信了,就像发送普通短信一样。长短信发送的时候,框架内部自动拆分成短短信分片发送(一般按67个汉字拆分)。
-
如何发送闪信?
//创建一个闪信对象,跟发送普通短信一样
CmppSubmitRequestMessage msg = CmppSubmitRequestMessage.create(phone, "10690021", "");
msg.setMsgContent(new SmsTextMessage("你好,我是闪信!",SmsAlphabet.UCS2,SmsMsgClass.CLASS_0)); //class0是闪信
-
如何接收短信?如果你了解netty的handler,那么请看AbstractBusinessHandler的源码即可,这是一个netty的handler.
如果你不了解netty, 你只需知道:
当连接刚刚建立时[指登陆验证成功],smsgate会自动调用handler里的userEventTriggered方法,因此在此方法中可以开启一个Consumer去消费MQ里的消息发送到网络连接上;
当对方发送任意一个消息给你时[包括request,response消息],smsgate会自动调用handler里的channelRead方法,因此可在此方法内接收消息并作处理业务,但避免作非常耗时的操作,会影响netty的处理效率,甚至完全耗完netty的io线程造成消息不响应。在channelRead方法里能获取接收到的消息对象,同时通过本Handler的
getEndpointEntity()方法,或者ctx.channel().attr(GlobalConstance.entityPointKey).get();能够获取该消息的发送方账号实体Entity对象。当连接关闭时,smsgate会自动调用handler里的channelInactive方法,可在此方法中实现连接关闭后的一些清理操作。
-
如何不改源码,实现修改框架默认的handler比如SGIP协议要设置NodeId;你需要这样做:
1、写一个扩展的SgipClientEndpointEntity子类,如:MySgipClientEndpointEntity,重写buildConnector()方法
2、再写一个SgipClientEndpointConnector子类,如:MySgipClientEndpointConnector,重写doinitPipeLine()方法
3、最后再写一个SgipSessionLoginManager子类,如:MySgipSessionLoginManager,重写doLogin方法,实现登陆方法的重写,在方法里创建自己定义的实现。
4、最后在openEntity通道里,new MySgipClientEndpointEntity就可以了
-
使用 http 或者 socks 代理SmsGate支持HTTP、SOCKS代理以方便在使用代理访问服务器的情况。代理设置方式:
// 无username 和 password 可写为 http://ipaddress:port
client.setProxy("http://username:password@ipaddress:port"); //http代理
client.setProxy("https://username:password@ipaddress:port"); //https代理
client.setProxy("socks://username@ipaddress:port"); //socks4代理
client.setProxy("socks4://username@ipaddress:port"); //socks4代理
client.setProxy("socks5://username:password@ipaddress:port"); //socks5代理
-
抓包,打印二进制的收发日志框架使用
entity.[EntityId]的loggerName打印该 EntityID上所有的收发记录。Debug 级别打印短信消息对象的
toString内容。Trace 级别打印短信消息对象的
二进制内容。如:针对
cmppclientEntityId通道的 logback.xml , log4j2.xml配置
<logger name="entity.cmppclientEntityId" level ="debug" additivity="false">
</logger>
如: log4j.properties 配置
log4j.logger.entity.cmppclientEntityId=debug
在Java9以上版上运行
在java9以上运行,启动java进程要增加以下参数:
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.text=ALL-UNNAMED
--add-exports java.base/sun.security.x509=ALL-UNNAMED
新手指引
- 先看doc目录下的
CMPP接口协议V3.0.0.doc文档 (看不懂的到群里咨询) - 再看readme里的说明 (看不懂的到群里咨询)
- 导入工程后,运行测试demo: TestCMPPEndPoint,学会配置账号密码等参数
- 由于代码是基于netty网络框架,您有必要先有一些Netty的基础
开发短信网关的常见问题
长短信拆分合并原理
短信支持长短信功能是在手机终端实现的,即:手机陆续收到多个短信片断后,会根据短信PDU里的前6个byte信息进行合并。最终在手机上显示为一条短信,但实际却是接收了多条短信(因此收多条的费用)。
因此,长短信在发送时要进行拆分。在开发短信网关时,由于要对短信内容进行校验,拼签名等处理,因此在接收到短信分片后,要进行合并成一条处理,之后发送时再拆分为多条(当然有可能始终只收到一个片断,造成永远无法合并成一条完整的短信)。
短信内容(PDU)字段的前6字节是长短信的协议头(其余内容才是短信文本),前3个字节固定是 0x050003,后3个字节用来做长短信合并的依据(类似IP包的分片)
1字节 包ID[最大255], <br/>
1字节 包总分片数<br/>
1字节 分片序号
如:45,03,01表示ID为45的第1个分片,总共3个分片。45,03,02表示ID为45的第2个分片,总共3个分片。 当手机收到完整的3个分片后,手机才进行合并显示。
使用redis实现集群长短信合并
框架内部自带一个JVM内存缓存(Guava Cache)的LongMessageFrameCache类,用于保存未完成合并的短信片断。 但集群(多进程,多节点)部署服务时,有可能从不同的节点上(主机上)接收到同一个长短信的不同片断,此时框架默认的JVM内存缓存无法完成长短信合并。 为解决此问题,框架使用SPI机制加载LongMessageFrameCache的实现类,业务侧以SPI方式提供Redis版的LongMessageFrameProvider实现类。 为了让业务自制的LongMessageFrameProvider实现类生效, 要确保业务自制的LongMessageFrameProvider实现类 order() 大于0 。框架优先使用order最大的实现。
具体为:
-
打开该通道账号的配置
EndpointEntity.isRecvLongMsgOnMultiLink属性,用于标识该通道的长短信要使用集群部署的长短信合并能力(由于只有少量系统有此问题,不需要所有账号打开该特性,会影响合并性能)。 -
提供一个Redis 的合并实现类,可以参考测试包中的代码:
RedisLongMessageFrameCache,RedisLongMessageFrameProvider
网关服务前边有nginx,haproxy代理的时候如何获取真实的客户端IP?
首先感谢群友 狠人 提供了使用proxy protocol协议支持从代理服务器获取真实客户IP的思路。
针对ServerEndpoint ,通过设置setProxyProtocol(true) 开启proxy protocol协议开关。框架从channel上第一个消息(HAProxyMessage)获取真实的客户端IP后,设置到channel的 Attribute属性上。业务代码可以从 channel.attr(GlobalConstance.proxyProtocolKey) 获取该信息,从而拿到真实的客户IP.
该特性使得短信网关的集群部署架构更为灵活,比如:服务入口使用nginx,haproxy等代理,真实网关服务以集群的方式部署在后端,横向扩展。
如何关联状态报告【即短信回执,以下都称为状态报告】和submit消息?
运宽商网关响应submitRequest消息时,你会收到submitResponse消息。在response里会有msgId。通过这个msgId跟之后收到的状态报告(reportMessage)里的msgId关联。
如何记录每个消息的发送日志,并向我的客户发送状态报告【即短信回执,以下都称为状态报告】?
当接收到来源客户的submitRequest消息后,要回复response,注意此时要记录回复response时所使用的msgId,即你回复给来源客户的msgId。
将消息转发给通道后,当接收到submitResponse后,通过response.getRequest()获取对应的request 。注意此时有两个msgID,一个是通道给你的msgID,一个是你给来源客户的。在数据库里记录相关信息(至少包括消息来源客户,消息出去的通道,两个msgId,消息详情)。之后在接收到状态报告后,通过通道给你的msgId更新消息状态报告里的msgId,并根据来源客户将状态报告回传给客户,注意回传reportMessage里的msgId要使用你给客户回复response时用的msgId. 详见流程图
关于长短信类 LongSMSMessage 中 UniqueLongMsgId 的使用
由于cmpp,sgip等短信协议的异步化特点,框架默认实现长短信的拆分与合并,接收Sp发送的MT消息并匹配上游状态报告【即短信回执,以下都称为状态报告】时,由于缺少短信唯一标识,从Sp接收的短信和最终发送给运营商的短信之间没有关联标识,
造成状态报告回来时难以匹配,实现起来很复杂。为了解决cmpp协议的接收的短信与发送出去的短信关联问题,给长短信增加了这个UniqueLongMsgId。 对http协议接收的短信同样可以使用UniqueLongMsgId: 通过http接收的长短信对象在发送到cmpp协议的短信通道连接以前是没有UniqueLongMsgId的,发送以后框架会设置UniqueLongMsgId 的值 。因此可以在发送完成收到response后通过response.getRequest()获取Request对象从而拿到UniqueLongMsgId。
UniqueLongMsgId 中 id 是唯一标识,即使在极短时间内收到相同手机号端口号的短信也能保持唯一性。该ID当短信从网络上接收到还未合并时进行设置,直到转发给运营商通道都不会变化,并且相同长短信的不同分片的ID也相同。
UniqueLongMsgId 除了 id 以外,还包含其它信息如:从消息从哪个通道账号Id提交的,从哪个IP端口提交的、长短信的分片ID、总分片数、分片序号以及消息序列号、时间戳。
在Test包里有一个模拟的匹配状态报告的测试用例用是用 UniqueLongMsgId 实现的,并且经过相同手机号、端口号在极限并发压力下的匹配测试,单JVM多线程安全。逻辑供参考: com.zx.sms.transgate.TestReportForward
集群环境如何平均分配上游连接数?
网关平台通常会有多个服务节点,而对接的通道给的连接数通常不是服务节点数的整倍数,极端情况连接数小于服务节点数,这样如何平均分配连接数就成了一个问题。
这里介绍一个算法:通过在redis里记录 {全局的服务节点列表},来计算每个服务节点连几个tcp连接。
var curNodeIndex = getCurNodeIndexFromRedis(thisNode); // 当前节点在全局服务节点的排序号,{0,1,2,3,...}
var cntNode ; // 从Redis里获取的总的服务进程节点数
//所有短信通道,逐一计算每一个通道,在当前节点上最大允许的tcp连接数,
allEntityPointList.foreach(e->{
var curEntityIndex = getCurEntityIndex(e); //所有短信通道根据Id排序后,当前通道的排序号,{0,1,2,3,4,5,6,7,...}
var curMaxChannel = e.getMaxChannel(); //当前通道全局允许的最大连接数
//连接数不是服务节点数的整倍数,按服务节点数平均分配后一定会有余数, 按当前节点的排序号先后把余下的连接数分完。
//但是服务节点排序号是固定不变的,这样排序号靠前的节点总是优先分到余下的连接数,造成全局通道总连接数分配不均,因此要结合"当前通道的排序号" 对 "服务节点排序号"进行位移
//因此,当"服务节点"或者"全局通道账号"有任一个变化时,都会影响连接的分配。
var shiftNodeIndex = (curNodeIndex + curEntityIndex) % cntNode;
//平均分配后,余下的连接数
var remainderChannel = curMaxChannel % cntNode;
//平均分配连接数
var hostMaxChannel = curMaxChannel / cntNode;
//余数处理
if(remainderChannel > 0 && shiftNodeIndex < remainderChannel){
hostMaxChannel = hostMaxChannel + 1;
}
var hostChannel = getConnectionCountAtCurrentNode(e); //当前通道在本节点上的连接数
if(hostChannel < hostMaxChannel){
openChannel(e); //新建一个连接
}else{
//关闭该通道超过数量的连接
closeSomeChannel(e,hostMaxChannel - hostChannel);
}
});
框架内部的netty的Handler前后顺序如图:
CMPPGate , SMPPGate , SGIPGate, SMGPGate
中移短信cmpp协议/smpp协议 netty实现编解码
这是一个在netty4框架下实现的cmpp3.0/cmpp2.0短信协议解析及网关端口管理。
代码copy了 huzorro@gmail.com 基于netty3.7的cmpp协议解析 huzorro@gmail.com 的代码
目前已支持发送和解析长文本短信拆分合并,WapPush短信,以及彩信通知类型的短信。可以实现对彩信或者wap-push短信的拦截和加工处理。wap短信的解析使用 smsj 的短信库
cmpp协议已经跟华为,东软,亚信的短信网关都做过联调测试,兼容了不同厂家的错误和异常,如果跟网关通信出错,可以打开trace日志查看二进制数据。
因要与短信中心对接,新增了对SMPP协议的支持。
SMPP的协议解析代码是从 Twitter-SMPP 的代码 copy过来的。
新增对sgip协议(联通短信协议)的支持
sgip的协议解析代码是从 huzorro@gmail.com 的代码 copy过来后改造的。
新增对smgp协议(电信短信协议)的支持
smgp的协议解析代码是从 SMS-China 的代码 copy过来后改造的。
支持发送彩信通知,WAP短信以及闪信(Flash Message):
<DIV> <img src="./doc/QQ20180518143313.jpg" width="25%" height="25%"> <DIV>性能测试
在48core,128G内存的物理服务器上测试协议解析效率:35K条/s, cpu使用率25%.
Build
执行mvn package . jdk1.6以上.
增加了业务处理API
业务层实现接口:BusinessHandlerInterface,或者继承AbstractBusinessHandler抽象类实现业务即可。 连接保活,消息重发,消息持久化,连接鉴权都已封装,不须要业务层再实现。
如何实现自己的Handler,比如按短短信计费
参考 CMPPChargingDemoTest 里的扩展位置
实体类说明
CMPP的连接端口
com.zx.sms.connect.manager.cmpp.CMPPEndpointEntity
表示一个Tcp连接的发起端,或者接收端。用来记录连接的IP.port,以及CMPP协议的用户名,密码,业务处理的ChannelHandler集合等其它端口参数。包含三个子类:
- com.zx.sms.connect.manager.cmpp.CMPPServerEndpointEntity 服务监听端口,包含一个List<CMPPServerChild
Related Skills
node-connect
338.7kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
338.7kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.6kCommit, push, and open a PR
