百科.dev
全部条目在线工具AI 编程趋势榜开源项目技术资讯提交条目
登录
< 返回工具列表
S

SMSGate

> 编程语言
开源

这是一个在netty4框架下实现的三网合一短信网关核心框架,支持(cmpp/smpp3.4/sgip1.2/smgp3) 短信协议解析,支持长短信合并和拆分,也支持wap短信和闪信。

1.2K stars0 点赞1 次浏览
访问官网GitHub

工具介绍

这是一个在netty4框架下实现的三网合一短信网关核心框架,支持(cmpp/smpp3.4/sgip1.2/smgp3) 短信协议解析,支持长短信合并和拆分,也支持wap短信和闪信。

技术问题请加QQ群

群名称:cmppGate短信

群 号:770738500

How To Use

xml

  com.chinamobile.cmos
  sms-core
  2.1.13.6

商用短信网关平台推荐

  • 平台名称: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 f = ChannelUtil.syncWriteLongMsgToEntity([clientEntityId],message)方法发送,要判断f是否为Null,为Null表示发送失败,一条短信可能拆分成多条,因此返回List。

  • 关闭默认超速错误自动重发功能

    如CMPP协议接收到错误码为8的响应(超速错误),系统默认会再次重发直到成功,最大重试次数默认是30次。如果要关闭默认重试功能,须设置 entity.overSpeedSendCountLimit 为 0

    SGIP、SMPP的超速错误码是88,于CMPP协议相同,也会超速重发。

    SMGP 协议因为未定义超速错误码,不会超速重试。

  • 如何发送长短信?

    smsgate默认已经处理好长短信了,就像发送普通短信一样。长短信发送的时候,框架内部自动拆分成短短信分片发送(一般按67个汉字拆分)。

  • 如何发送闪信?

java
  //创建一个闪信对象,跟发送普通短信一样
  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配置

    
	
	如: 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],

1字节 包总分片数

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最大的实现。

具体为:

  1. 打开该通道账号的配置 EndpointEntity.isRecvLongMsgOnMultiLink属性,用于标识该通道的长短信要使用集群部署的长短信合并能力(由于只有少量系统有此问题,不需要所有账号打开该特性,会影响合并性能)。

  2. 提供一个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连接。

…

java

//因为长短信要拆分,因此返回一个promiseList.每个拆分后的短信对应一个promise
List futures = ChannelUtil.syncWriteLongMsgToEntity("client",submitmessage);
for(Promise  future: futures){
	//调用sync()方法,阻塞线程。等待接收response
	future.sync(); 
	//接收成功,如果失败可以获取失败原因,比如遇到连接突然中断错误等等
	if(future.isSuccess()){
		//打印收到的response消息
		logger.info("response:{}",future.get());
	}else{
		打印错误原因
		logger.error("response:{}",future.cause());
	}
}

//或者不阻塞进程,不调用sync()方法。
List promises = ChannelUtil.syncWriteLongMsgToEntity("client",submitmessage);
for(Promise  promise: promises){
	//接收到response后回调Listener方法
	promise.addListener(new GenericFutureListener() {
		@Override
		public void operationComplete(Future future) throws Exception {
			//接收成功,如果失败可以获取失败原因,比如遇到连接突然中断错误等等
			if(future.isSuccess()){
				//打印收到的response消息
				logger.info("response:{}",future.get());
			}else{
				打印错误原因
				logger.error("response:{}",future.cause());
			}
		}
	});
}

## CMPP Api使用举例

```java
public class TestCMPPEndPoint {
	private static final Logger logger = LoggerFactory.getLogger(TestCMPPEndPoint.class);

	@Test
	public void testCMPPEndpoint() throws Exception {
		ResourceLeakDetector.setLevel(Level.ADVANCED);
		final EndpointManager manager = EndpointManager.INS;

		CMPPServerEndpointEntity server = new CMPPServerEndpointEntity();
		server.setId("server");
		server.setHost("127.0.0.1");
		server.setPort(7890);
		server.setValid(true);
		//使用ssl加密数据流
		server.setUseSSL(false);

		CMPPServerChildEndpointEntity child = new CMPPServerChildEndpointEntity();
		child.setId("child");  //自定义通道账号ID,保持全局唯一
		child.setChartset(Charset.forName("utf-8"));
		child.setGroupName("test");   //自定义通道账号分组ID,用于对通道标识不同组,方便路由实现
		child.setUserName("901783");  //通道账号,可能和企业代码相同
		child.setPassword("ICP001");  //密码

		child.setValid(true);
		child.setVersion((short)0x30);   //协议版本号,48是3.0 协议,32是2.0协议

		child.setMaxChannels((short)4);
		child.setRetryWaitTimeSec((short)30);
		child.setMaxRetryCnt((short)3);
		child.setReSendFailMsg(true);
//		child.setWriteLimit(200);
//		child.setReadLimit(200);
		List serverhandlers = new ArrayList();
		serverhandlers.add(new CMPPMessageReceiveHandler()); //在这个handler里接收短信
		child.setBusinessHandlerSet(serverhandlers);
		server.addchild(child);
		
		manager.addEndpointEntity(server);
	
		CMPPClientEndpointEntity client = new CMPPClientEndpointEntity();
		client.setId("client");   //自定义通道账号ID,保持全局唯一
		client.setHost("127.0.0.1");
//		client.setLocalhost("127.0.0.1");
//		client.setLocalport(65521);
		client.setPort(7890);
		client.setChartset(Charset.forName("utf-8"));
		client.setGroupName("test"); //自定义通道账号分组ID,用于对通道标识不同组,方便路由实现
		client.setUserName("901783"); //通道账号,可能和企业代码相同
		client.setPassword("ICP001"); //密码
		client.setMsgSrc("902176");  //企业代码 ,可能和UserName相同
		client.setSpCode("10658762"); //服务代码,即显示到手机上的号码
		client.setMaxChannels((short)10);  //最大连接数
		client.setVersion((short)0x30);    //协议版本号
		client.setRetryWaitTimeSec((short)30);//发送request后 等待N秒后没有收到response,则重发消息
		client.setMaxRetryCnt((short)3);  // 发送消息的最大次数,如果为3,则表示连带第一次发送,再重试2次,一共发送3次
		client.setCloseWhenRetryFailed(false);  // 当发送消息次数达到最大(MaxRetryCnt)后 ,是否关闭连接。默认是 true 关闭连接
		client.setUseSSL(false);          //是否使用SSL加密连接,默认为false,不加密
		client.setWriteLimit(100);        //发送request消息的最大速度(单位条数)
		client.setReadLimit(100);         //接收request的最大速度(单位条数),当消息量超过一定限制后,消息将积压在TCP网络协议栈的接收缓冲区
		client.setWindow(16);         //设置发送消息的滑动窗口,滑动窗口默认为16,该大小根据网络时延不同,会影响发送速度
													
		//默认为false ,发送request是否保存在本地磁盘。如果为true,当进程关闭后,本地磁盘会保存未收到response的消息,当进程再次启动框架自动读取消息并发送。可能造成消息重复发送
		client.setReSendFailMsg(true);  
		
		client.setSupportLongmsg(SupportLongMessage.BOTH);  //是否支持长短信的自动拆分和拼接
		
		List clienthandlers = new ArrayList();
		clienthandlers.add( new CMPPSessionConnectedHandler(10000));  //在这个handler里发送短信
		client.setBusinessHandlerSet(clienthandlers);
		
		manager.addEndpointEntity(client);
		
		manager.openEndpoint(server);
		
		Thread.sleep(1000);
		for(int i=0;i<=child.getMaxChannels()+1;i++)
			manager.openEndpoint(client);

        System.out.println("start.....");
        
//		Thread.sleep(300000);
        LockSupport.park();
		EndpointManager.INS.close();
	}
}

SMPP Api使用举例

java

public class TestSMPPEndPoint {
	private static final Logger logger = LoggerFactory.getLogger(TestSMPPEndPoint.class);

	@Test
	public void testSMPPEndpoint() throws Exception {
	
		final EndpointManager manager = EndpointManager.INS;

		SMPPServerEndpointEntity server = new SMPPServerEndpointEntity();
		server.setId("smppserver");
		server.setHost("127.0.0.1");
		server.setPort(2776);
		server.setValid(true);
		//使用ssl加密数据流
		server.setUseSSL(false);
		
		SMPPServerChildEndpointEntity child = new SMPPServerChildEndpointEntity();
		child.setId("smppchild");
		child.setSystemId("901782");
		child.setPassword("ICP");

		child.setValid(true);
		child.setChannelType(ChannelType.DUPLEX);
		child.setMaxChannels((short)3);
		child.setRe

Issues· 0 开放

查看全部 Issues在 GitHub 打开

暂无开放 Issues,或尚未同步最近议题。

> 标签

Javacmppnetty4sgipsmgp

暂无评论,来聊聊你的看法吧

> 工具信息

发布日期2026年8月1日
最后更新2026年9月17日
分类编程语言
定价开源

> 相关工具

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言