{"id":19993390,"url":"https://github.com/Lihuanghe/SMSGate","last_synced_at":"2025-05-04T12:31:27.931Z","repository":{"id":35421613,"uuid":"39686613","full_name":"Lihuanghe/SMSGate","owner":"Lihuanghe","description":"这是一个在netty4框架下实现的三网合一短信网关核心框架，支持(cmpp/smpp3.4/sgip1.2/smgp3) 短信协议解析，支持长短信合并和拆分，也支持wap短信和闪信。","archived":false,"fork":false,"pushed_at":"2025-03-19T11:13:27.000Z","size":6587,"stargazers_count":1021,"open_issues_count":0,"forks_count":479,"subscribers_count":67,"default_branch":"netty4","last_synced_at":"2025-03-19T12:25:26.513Z","etag":null,"topics":["cmpp","netty4","sgip","smgp","smpp","sms"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Lihuanghe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-07-25T13:34:07.000Z","updated_at":"2025-03-19T11:13:33.000Z","dependencies_parsed_at":"2023-02-18T07:01:07.767Z","dependency_job_id":"491f3902-e83a-4439-a5a4-41b3332c85ca","html_url":"https://github.com/Lihuanghe/SMSGate","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Lihuanghe%2FSMSGate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Lihuanghe%2FSMSGate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Lihuanghe%2FSMSGate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Lihuanghe%2FSMSGate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Lihuanghe","download_url":"https://codeload.github.com/Lihuanghe/SMSGate/tar.gz/refs/heads/netty4","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252334401,"owners_count":21731400,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cmpp","netty4","sgip","smgp","smpp","sms"],"created_at":"2024-11-13T04:52:37.738Z","updated_at":"2025-05-04T12:31:22.924Z","avatar_url":"https://github.com/Lihuanghe.png","language":"Java","funding_links":[],"categories":["Java"],"sub_categories":[],"readme":"# 技术问题请加QQ群\n![qq 20180420170449](https://user-images.githubusercontent.com/7598107/39042453-6fcfaac0-44bd-11e8-94bf-101c8dad8400.png)\n\n群名称：cmppGate短信\n\u003cbr/\u003e群   号：770738500\n\n# How To Use\n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003ecom.chinamobile.cmos\u003c/groupId\u003e\n  \u003cartifactId\u003esms-core\u003c/artifactId\u003e\n  \u003cversion\u003e2.1.13.6\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n# 商用短信网关平台推荐\n- `平台名称:SMSWG短信网关`\n   系统官网:www.smswg.com 系统有3个PC端：管理员端+用户端+代理商端，支持代理分销短信，日发送支持上亿，实时精准计费，不阻塞高并发，单机每秒可支持1.5万条短信下发,更多系统功能请进官网查看演示系统。\n\n# 常见问题\n\n- `纯客户端发送短信`\n\n  可以使用[sms-client](https://github.com/Lihuanghe/sms-client), 一个纯发送短信的客户端jar包，Api简单。【sgip协议用sms-client无法接收上行和状态报告】 \n  \n  也可以参考[htt2cmpp](https://github.com/Lihuanghe/http2cmpp) 实现将一个短信长连接协议封装成http接口。\n  \n  或者参考[smsServer](https://github.com/Lihuanghe/smsServer)用SpringBoot实现一个能支持http,cmpp,sgip,smgp,smpp等多种协议的网关服务。\n\n- `没看懂如何发送短信？`\n\n  短信协议是tcp长连接，类似数据库连接，如jdbc-connection. 所以发送短信前必须要先有一个短信连接。因此你需要在程序启动时建立短信连接。参考demo里的client，调用manager.openEntity()方法，,调用manager.startConnectionCheckTask()开启断线重连。\n  然后就像调用其它库一样，在需要发送短信的地方，new 一个对应的Message,调用\n  \n  List\u003c Future \u003e f = ChannelUtil.syncWriteLongMsgToEntity([clientEntityId],message)方法发送，`要判断f是否为Null，为Null表示发送失败,一条短信可能拆分成多条，因此返回List`。\n\n- `关闭默认超速错误自动重发功能`\n\n  如CMPP协议接收到错误码为8的响应（超速错误），系统默认会再次重发直到成功，最大重试次数默认是30次。如果要关闭默认重试功能，须设置 `entity.overSpeedSendCountLimit `为 `0`\n  \n  SGIP、SMPP的超速错误码是88，于CMPP协议相同，也会超速重发。 \n  \n  SMGP 协议因为未定义超速错误码，不会超速重试。\n\n- `如何发送长短信？`\n\n  smsgate默认已经处理好长短信了，就像发送普通短信一样。长短信发送的时候，框架内部自动拆分成短短信分片发送(一般按67个汉字拆分)。\n  \n- `如何发送闪信?`\n\n```java\n  //创建一个闪信对象，跟发送普通短信一样\n  CmppSubmitRequestMessage msg = CmppSubmitRequestMessage.create(phone, \"10690021\", \"\");\n  msg.setMsgContent(new SmsTextMessage(\"你好，我是闪信！\",SmsAlphabet.UCS2,SmsMsgClass.CLASS_0));  //class0是闪信\n```\n\n- `如何接收短信？`\n\n  如果你了解netty的handler,那么请看AbstractBusinessHandler的源码即可，这是一个netty的handler.\n  \n  如果你不了解netty, 你只需知道：\n  \n  当连接刚刚建立时[指登陆验证成功]，smsgate会自动调用handler里的userEventTriggered方法，因此在此方法中可以开启一个Consumer去消费MQ里的消息发送到网络连接上；\n  \n  当对方发送任意一个消息给你时[包括request,response消息]，smsgate会自动调用handler里的channelRead方法，因此可在此方法内接收消息并作处理业务，但避免作非常耗时的操作，会影响netty的处理效率，甚至完全耗完netty的io线程造成消息不响应。在channelRead方法里能获取接收到的消息对象，同时通过本Handler的 `getEndpointEntity()`方法，或者 `ctx.channel().attr(GlobalConstance.entityPointKey).get();`能够获取该消息的发送方账号实体Entity对象。\n  \n  当连接关闭时，smsgate会自动调用handler里的channelInactive方法，可在此方法中实现连接关闭后的一些清理操作。\n\n- `如何不改源码，实现修改框架默认的handler`\n\n  比如SGIP协议要设置NodeId;你需要这样做：\n  \n  1、写一个扩展的SgipClientEndpointEntity子类，如：MySgipClientEndpointEntity，重写buildConnector()方法\n  \n  2、再写一个SgipClientEndpointConnector子类，如：MySgipClientEndpointConnector,重写doinitPipeLine()方法\n  \n  3、最后再写一个SgipSessionLoginManager子类，如：MySgipSessionLoginManager，重写doLogin方法，实现登陆方法的重写，在方法里创建自己定义的实现。\n  \n  4、最后在openEntity通道里，new MySgipClientEndpointEntity就可以了\n\n- `使用 http 或者 socks 代理`\n\n  SmsGate支持HTTP、SOCKS代理以方便在使用代理访问服务器的情况。代理设置方式：\n\n```\n\t// 无username 和 password 可写为  http://ipaddress:port\n\tclient.setProxy(\"http://username:password@ipaddress:port\");  //http代理\n\tclient.setProxy(\"https://username:password@ipaddress:port\");  //https代理\n\tclient.setProxy(\"socks://username@ipaddress:port\");  //socks4代理\n\tclient.setProxy(\"socks4://username@ipaddress:port\");  //socks4代理\n\tclient.setProxy(\"socks5://username:password@ipaddress:port\");  //socks5代理\n\n```\n\n- `抓包，打印二进制的收发日志`\n\n  框架使用 `entity.[EntityId]`的loggerName打印该 EntityID上所有的收发记录。\n  \n  Debug 级别打印短信消息对象的`toString`内容。\n  \n  Trace 级别打印短信消息对象的`二进制`内容。\n  \n  如：针对`cmppclientEntityId` 通道的 logback.xml , log4j2.xml配置 \n  \n```\n    \u003clogger name=\"entity.cmppclientEntityId\" level =\"debug\" additivity=\"false\"\u003e\n\t\u003c/logger\u003e\n```\n\n  \t如： log4j.properties 配置 \n\n```\n   log4j.logger.entity.cmppclientEntityId=debug\n```\n\n- `在Java9以上版上运行`\n\n```\n\t在java9以上运行，启动java进程要增加以下参数：\n\t\n--add-opens java.base/java.lang=ALL-UNNAMED\n--add-opens java.base/java.math=ALL-UNNAMED  \n--add-opens java.base/java.util=ALL-UNNAMED  \n--add-opens java.base/java.util.concurrent=ALL-UNNAMED  \n--add-opens java.base/java.net=ALL-UNNAMED   \n--add-opens java.base/java.text=ALL-UNNAMED \n--add-exports java.base/sun.security.x509=ALL-UNNAMED \n\n```\n\n\n# 新手指引\n\n- 先看doc目录下的`CMPP接口协议V3.0.0.doc`文档 (看不懂的到群里咨询)\n- 再看readme里的说明  (看不懂的到群里咨询)\n- 导入工程后，运行测试demo: TestCMPPEndPoint，学会配置账号密码等参数\n- 由于代码是基于netty网络框架，您有必要先有一些Netty的基础\n\n\n# 开发短信网关的常见问题\n\n- `长短信拆分合并原理`\n\n短信支持长短信功能是在手机终端实现的，即：手机陆续收到多个短信片断后，会根据短信PDU里的前6个byte信息进行合并。最终在手机上显示为一条短信，但实际却是接收了多条短信（因此收多条的费用）。\n\n因此，长短信在发送时要进行拆分。在开发短信网关时，由于要对短信内容进行校验，拼签名等处理，因此在接收到短信分片后，要进行合并成一条处理，之后发送时再拆分为多条（当然有可能始终只收到一个片断，造成永远无法合并成一条完整的短信）。\n\n短信内容(PDU)字段的前6字节是长短信的协议头（其余内容才是短信文本），前3个字节固定是 `0x050003`，后3个字节用来做长短信合并的依据（类似IP包的分片）\n\n`1字节 包ID[最大255], `\u003cbr/\u003e\n`1字节 包总分片数`\u003cbr/\u003e\n`1字节 分片序号`\n\n如：45，03，01表示ID为45的第1个分片，总共3个分片。45，03，02表示ID为45的第2个分片，总共3个分片。            当手机收到完整的3个分片后，手机才进行合并显示。\n\n- `使用redis实现集群长短信合并`\n\n框架内部自带一个JVM内存缓存(Guava Cache)的LongMessageFrameCache类，用于保存未完成合并的短信片断。\n但集群（多进程，多节点）部署服务时，有可能从不同的节点上（主机上）接收到同一个长短信的不同片断，此时框架默认的JVM内存缓存无法完成长短信合并。\n为解决此问题，框架使用SPI机制加载LongMessageFrameCache的实现类,业务侧以SPI方式提供Redis版的LongMessageFrameProvider实现类。\n为了让业务自制的LongMessageFrameProvider实现类生效， 要确保业务自制的LongMessageFrameProvider实现类 order() 大于0 。框架优先使用order最大的实现。\n\n具体为：\n\n1)  打开该通道账号的配置 `EndpointEntity.isRecvLongMsgOnMultiLink`属性，用于标识该通道的长短信要使用集群部署的长短信合并能力（由于只有少量系统有此问题，不需要所有账号打开该特性，会影响合并性能）。\n\n2)  提供一个Redis 的合并实现类，可以参考测试包中的代码：`RedisLongMessageFrameCache` ,  `RedisLongMessageFrameProvider`\n\n- `网关服务前边有nginx，haproxy代理的时候如何获取真实的客户端IP?`\n\n首先感谢群友 `狠人` 提供了使用[`proxy protocol协议`](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)支持从代理服务器获取真实客户IP的思路。\n\n针对ServerEndpoint ，通过设置`setProxyProtocol(true)` 开启proxy protocol协议开关。框架从channel上第一个消息（HAProxyMessage）获取真实的客户端IP后，设置到channel的 Attribute属性上。业务代码可以从 `channel.attr(GlobalConstance.proxyProtocolKey)` 获取该信息，从而拿到真实的客户IP.\n该特性使得短信网关的集群部署架构更为灵活，比如：服务入口使用nginx,haproxy等代理，真实网关服务以集群的方式部署在后端，横向扩展。\n\n- `如何关联状态报告【即短信回执，以下都称为状态报告】和submit消息?`\n\n运宽商网关响应`submitRequest`消息时，你会收到`submitResponse`消息。在`response`里会有`msgId`。通过这个`msgId`跟之后收到的状态报告(`reportMessage`)里的`msgId`关联。\n\n- `如何记录每个消息的发送日志，并向我的客户发送状态报告【即短信回执，以下都称为状态报告】?`\n\n当接收到来源客户的`submitRequest`消息后，要回复`response`,注意此时要记录回复`response`时所使用的`msgId`，即你回复给来源客户的`msgId`。\n\n将消息转发给通道后，当接收到`submitResponse后`，通过`response.getRequest()`获取对应的`request` 。注意此时有两个`msgID`，一个是通道给你的`msgID`，一个是你给来源客户的。在数据库里记录相关信息（至少包括消息来源客户，消息出去的通道，两个`msgId`,消息详情）。之后在接收到状态报告后，通过通道给你的`msgId`更新消息状态报告里的`msgId`，并根据来源客户将状态报告回传给客户，注意回传`reportMessage`里的`msgId`要使用你给客户回复`response`时用的`msgId`.  [详见流程图](https://www.processon.com/view/link/598c16ace4b02e9a26eeed11)\n\n- `关于长短信类 LongSMSMessage 中 UniqueLongMsgId 的使用`\n\n由于cmpp，sgip等短信协议的异步化特点，框架默认实现长短信的拆分与合并，接收Sp发送的MT消息并匹配上游状态报告【即短信回执，以下都称为状态报告】时，由于缺少短信唯一标识，从Sp接收的短信和最终发送给运营商的短信之间没有关联标识，\n造成状态报告回来时难以匹配，实现起来很复杂。为了解决cmpp协议的接收的短信与发送出去的短信关联问题，给长短信增加了这个`UniqueLongMsgId`。 对http协议接收的短信同样可以使用`UniqueLongMsgId`： 通过http接收的长短信对象在发送到cmpp协议的短信通道连接以前是没有`UniqueLongMsgId`的，发送以后框架会设置UniqueLongMsgId 的值 。因此可以在发送完成收到response后通过`response.getRequest()`获取Request对象从而拿到`UniqueLongMsgId`。\n\n`UniqueLongMsgId` 中 id 是唯一标识,即使在极短时间内收到相同手机号端口号的短信也能保持唯一性。该ID当短信从网络上接收到还未合并时进行设置，直到转发给运营商通道都不会变化，并且相同长短信的不同分片的ID也相同。\n\u003cDIV\u003e\n\u003cimg src=\"./doc/QQ20221219154405.jpg\" width=\"100%\" height=\"100%\"\u003e\n\u003cDIV\u003e\n\n`UniqueLongMsgId` 除了 id 以外，还包含其它信息如：从消息从哪个通道账号Id提交的，从哪个IP端口提交的、长短信的分片ID、总分片数、分片序号以及消息序列号、时间戳。\n在Test包里有一个模拟的匹配状态报告的测试用例用是用 `UniqueLongMsgId` 实现的，并且经过相同手机号、端口号在极限并发压力下的匹配测试，单JVM多线程安全。逻辑供参考： [`com.zx.sms.transgate.TestReportForward`](./src/test/java/com/zx/sms/transgate/TestReportForward.java)\n\n\n- `集群环境如何平均分配上游连接数?`\n\n网关平台通常会有多个服务节点，而对接的通道给的连接数通常不是服务节点数的整倍数，极端情况连接数小于服务节点数，这样如何平均分配连接数就成了一个问题。\n\n这里介绍一个算法：通过在redis里记录 {全局的服务节点列表}，来计算每个服务节点连几个tcp连接。\n\n```\nvar curNodeIndex = getCurNodeIndexFromRedis(thisNode); // 当前节点在全局服务节点的排序号，{0,1,2,3,...}\nvar cntNode ; // 从Redis里获取的总的服务进程节点数\n//所有短信通道，逐一计算每一个通道，在当前节点上最大允许的tcp连接数， \nallEntityPointList.foreach(e-\u003e{\n\tvar curEntityIndex = getCurEntityIndex(e); //所有短信通道根据Id排序后，当前通道的排序号，{0,1,2,3,4,5,6,7,...}\n\tvar curMaxChannel = e.getMaxChannel(); //当前通道全局允许的最大连接数\n\t\n\t//连接数不是服务节点数的整倍数，按服务节点数平均分配后一定会有余数， 按当前节点的排序号先后把余下的连接数分完。\n\t//但是服务节点排序号是固定不变的，这样排序号靠前的节点总是优先分到余下的连接数，造成全局通道总连接数分配不均，因此要结合\"当前通道的排序号\" 对 \"服务节点排序号\"进行位移\n\t//因此，当\"服务节点\"或者\"全局通道账号\"有任一个变化时，都会影响连接的分配。\n\tvar shiftNodeIndex = (curNodeIndex + curEntityIndex) % cntNode;\n\t//平均分配后,余下的连接数\n\tvar remainderChannel = curMaxChannel % cntNode;\n\t//平均分配连接数\n\tvar hostMaxChannel = curMaxChannel / cntNode;\n\t//余数处理\n\tif(remainderChannel \u003e 0 \u0026\u0026 shiftNodeIndex \u003c remainderChannel){\n\t\thostMaxChannel = hostMaxChannel + 1;\n\t}\n\tvar hostChannel = getConnectionCountAtCurrentNode(e);   //当前通道在本节点上的连接数\n\tif(hostChannel \u003c hostMaxChannel){\n\t\topenChannel(e); //新建一个连接\n\t}else{\n\t\t//关闭该通道超过数量的连接\n\t\tcloseSomeChannel(e,hostMaxChannel - hostChannel);\n\t}\n});\n\n```\n\n- `框架内部的netty的Handler前后顺序`\n如图：\n\u003cDIV\u003e\n\u003cimg src=\"./doc/handler.jpg\" width=\"100%\" height=\"100%\"\u003e\n\u003cDIV\u003e\n\n# CMPPGate , SMPPGate , SGIPGate, SMGPGate\n中移短信cmpp协议/smpp协议 netty实现编解码\n\n这是一个在netty4框架下实现的cmpp3.0/cmpp2.0短信协议解析及网关端口管理。\n代码copy了 `huzorro@gmail.com` 基于netty3.7的cmpp协议解析 [huzorro@gmail.com 的代码 ](https://github.com/huzorro/netty3ext)\n\n目前已支持发送和解析`长文本短信拆分合并`，`WapPush短信`，以及`彩信通知`类型的短信。可以实现对彩信或者wap-push短信的拦截和加工处理。wap短信的解析使用 [smsj](https://github.com/marre/smsj) 的短信库\n\ncmpp协议已经跟华为，东软，亚信的短信网关都做过联调测试，兼容了不同厂家的错误和异常，如果跟网关通信出错，可以打开trace日志查看二进制数据。\n\n因要与短信中心对接，新增了对SMPP协议的支持。\n\nSMPP的协议解析代码是从  [Twitter-SMPP 的代码](https://github.com/fizzed/cloudhopper-smpp) copy过来的。\n\n新增对sgip协议(联通短信协议)的支持\n\nsgip的协议解析代码是从 [huzorro@gmail.com 的代码 ](https://github.com/huzorro/sgipsgw) copy过来后改造的。\n\n新增对smgp协议(电信短信协议)的支持\n\nsmgp的协议解析代码是从 [SMS-China 的代码 ](https://github.com/clonalman/SMS-China) copy过来后改造的。\n\n支持发送彩信通知，WAP短信以及闪信(Flash Message):\n\n\u003cDIV\u003e\n\u003cimg src=\"./doc/QQ20180518143313.jpg\" width=\"25%\" height=\"25%\"\u003e\n\u003cDIV\u003e\n\n## 性能测试 \n在48core，128G内存的物理服务器上测试协议解析效率：35K条/s, cpu使用率25%. \n\n## Build\n执行mvn package . jdk1.6以上. \n\n## 增加了业务处理API\n业务层实现接口：BusinessHandlerInterface，或者继承AbstractBusinessHandler抽象类实现业务即可。 连接保活，消息重发，消息持久化，连接鉴权都已封装，不须要业务层再实现。\n\n## 如何实现自己的Handler,比如按短短信计费\n参考 CMPPChargingDemoTest 里的扩展位置\n\n# 实体类说明\n\n## CMPP的连接端口\n\n`com.zx.sms.connect.manager.cmpp.CMPPEndpointEntity`\n表示一个Tcp连接的发起端，或者接收端。用来记录连接的IP.port,以及CMPP协议的用户名，密码，业务处理的ChannelHandler集合等其它端口参数。包含三个子类：\n\n1. com.zx.sms.connect.manager.cmpp.CMPPServerEndpointEntity\n服务监听端口，包含一个List\u003cCMPPServerChildEndpointEntity\u003e属性。 一个服务端口包含多个CMPPServerChildEndpointEntity端口\n\n2. com.zx.sms.connect.manager.cmpp.CMPPServerChildEndpointEntity\n服务接收端口，包含CMPP连接用户名，密码，以及协议版本等信息\n\n3. com.zx.sms.connect.manager.cmpp.CMPPClientEndpointEntity\n客户端端口，包含CMPP连接用户名，密码，以及协议版本，以及服务端IP.port. 用于连接服务端\n\n## 端口连接器接口\n`com.zx.sms.connect.manager.EndpointConnector`\n负责一个端口的打开，关闭，查看当前连接数，新增连接，移除连接。每个端口的实体类都对应一个EndpointConnector.当CMPP连接建立完成，将连接加入连接器管理，并给pipeLine上挂载业务处理的ChannelHandler.\n\n1. com.zx.sms.connect.manager.cmpp.CMPPServerEndpointConnector\n这个类的open()调用netty的ServerBootstrap.bind()开一个服务监听\n\n2. com.zx.sms.connect.manager.cmpp.CMPPServerChildEndpointConnector\n用来收集CMPPServerChildEndpointEntity端口下的所有连接。它的open()方法为空.\n\n3. com.zx.sms.connect.manager.cmpp.CMPPClientEndpointConnector\n这个类open()调用netty的Bootstrap.connect()发起一个TCP连接\n\n## 端口管理器\n`com.zx.sms.connect.manager.EndpointManager`\n该类是单例模式，管理所有端口，并负责所有端口的打开，关闭，以及端口信息保存，以及连接断线重连。\n\n## CMPP协议的连接登陆管理\n`com.zx.sms.session.cmpp.SessionLoginManager`\n这是一个netty的ChannelHandler实现，主要负责CMPP连接的建立。当CMPP连接登陆成功、会话建立完成后，会调用EndpointConnector.addChannel(channel)方法，把连接加入连接器管理，连接器负责给channel的pipeline上挂载业务处理的Handler,最后触发\nSessionState.Connect事件，通知业务处理Handler连接已建立成功。\n\n## CMPP的连接状态管理器\n`com.zx.sms.session.cmpp.SessionStateManager`\n这是一个netty的ChannelHandler实现。负责每个连接上CMPP消息的存储，短信重发，流量窗口控制，过期短信的处理\n\n## CMPP协议解析器\nCMPP20MessageCodecAggregator [2.0协议]\nCMPPMessageCodecAggregator [这是3.0协议]\n聚合了CMPP主要消息协议的解析，编码，长短信拆分，合并处理。\n\n## 短信持久化存储实现 StoredMapFactory \n使用BDB的StoreMap实现消息持久化，防止系统意外丢失短信。\n\n## 程序启动处理流程\n\n1. 程序启动类 new 一个CMPPEndpointEntity的实体类并设置IP,port,用户名，密码，业务处理的Handler等参数,\n2. 程序启动类 调用EndpointManager.addEndpointEntity(endpoint)方法，将端口加入管理器\n3. 程序启动类 调用EndpointManager.openAll()或者EndpointManager.openEndpoint()方法打开端口。\n4. EndpointManager会调用EndpointEntity.buildConnector()创建一个端口连接器，并调用EndpointConnector.open()方法打开端口。\n5. 如果是CMPPClientEndpointEntity的话，就会向服务器发起TCP连接请求，如果是CMPPServerEndpointEntity则会在本机开启一个服务端口等客户端连接。\n6. TCP连接建立完成后。netty会调用EndpointConnector.initPipeLine()方法初始化PipeLine，把CMPP协议解析器，SessionLoginManager加到PipeLine里去，然后netty触发ChannelActive事件。\n7. 在SessionLoginManager类里，客户端收到ChannelActive事件后会发送一个CMPPConnnect消息，请求建立CMPP连接.\n8. 同样在SessionLoginManager.channelRead()方法里,服务端会收到CMPPConnnect消息，开始对用户名，密码进行鉴权，并给客户端返回鉴权结果。\n9. 鉴权通过后，SessionLoginManager调用EndpointConnector.addChannel(channel)方法，把channel加入ArrayList,并给pipeLine上挂载SessionStateManager和业务处理的ChannelHandler，如心跳处理，日志记录，长短信合并拆分处理类。\n10. EndpointConnector.addChannel(channel)完成后，SessionLoginManager调用ctx.fireUserEventTriggered()方法，触发\tSessionState.Connect事件。\n\n以上CMPP连接建立完成。\n\n11. 业务处理类收到SessionState.Connect事件，开始业务处理，如从MQ获取短信下发，或开启Consumer接收MQ推送的消息。\n12. SessionStateManager会拦截所有read()和write()的消息，进行消息持久化，消息重发，流量控制。\n\n## 增加同步调用api\nsmsgate自开发以来，一直使用netty的异步发送消息，但实际使用场景中同步发送消息的更方便，或者能方便的取到response。因此增加一个同步调用的api。即：发送消息后等接收到对应的响应后才返回。\n使用方法如下：\n\n```java\n\n\t//因为长短信要拆分，因此返回一个promiseList.每个拆分后的短信对应一个promise\n\tList\u003cPromise\u003e futures = ChannelUtil.syncWriteLongMsgToEntity(\"client\",submitmessage);\n\tfor(Promise  future: futures){\n\t\t//调用sync()方法，阻塞线程。等待接收response\n\t\tfuture.sync(); \n\t\t//接收成功，如果失败可以获取失败原因，比如遇到连接突然中断错误等等\n\t\tif(future.isSuccess()){\n\t\t\t//打印收到的response消息\n\t\t\tlogger.info(\"response:{}\",future.get());\n\t\t}else{\n\t\t\t打印错误原因\n\t\t\tlogger.error(\"response:{}\",future.cause());\n\t\t}\n\t}\n\n\t//或者不阻塞进程，不调用sync()方法。\n\tList\u003cPromise\u003e promises = ChannelUtil.syncWriteLongMsgToEntity(\"client\",submitmessage);\n\tfor(Promise  promise: promises){\n\t\t//接收到response后回调Listener方法\n\t\tpromise.addListener(new GenericFutureListener() {\n\t\t\t@Override\n\t\t\tpublic void operationComplete(Future future) throws Exception {\n\t\t\t\t//接收成功，如果失败可以获取失败原因，比如遇到连接突然中断错误等等\n\t\t\t\tif(future.isSuccess()){\n\t\t\t\t\t//打印收到的response消息\n\t\t\t\t\tlogger.info(\"response:{}\",future.get());\n\t\t\t\t}else{\n\t\t\t\t\t打印错误原因\n\t\t\t\t\tlogger.error(\"response:{}\",future.cause());\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n```\n\n## CMPP Api使用举例\n\n```java\npublic class TestCMPPEndPoint {\n\tprivate static final Logger logger = LoggerFactory.getLogger(TestCMPPEndPoint.class);\n\n\t@Test\n\tpublic void testCMPPEndpoint() throws Exception {\n\t\tResourceLeakDetector.setLevel(Level.ADVANCED);\n\t\tfinal EndpointManager manager = EndpointManager.INS;\n\n\t\tCMPPServerEndpointEntity server = new CMPPServerEndpointEntity();\n\t\tserver.setId(\"server\");\n\t\tserver.setHost(\"127.0.0.1\");\n\t\tserver.setPort(7890);\n\t\tserver.setValid(true);\n\t\t//使用ssl加密数据流\n\t\tserver.setUseSSL(false);\n\n\t\tCMPPServerChildEndpointEntity child = new CMPPServerChildEndpointEntity();\n\t\tchild.setId(\"child\");  //自定义通道账号ID，保持全局唯一\n\t\tchild.setChartset(Charset.forName(\"utf-8\"));\n\t\tchild.setGroupName(\"test\");   //自定义通道账号分组ID，用于对通道标识不同组，方便路由实现\n\t\tchild.setUserName(\"901783\");  //通道账号，可能和企业代码相同\n\t\tchild.setPassword(\"ICP001\");  //密码\n\n\t\tchild.setValid(true);\n\t\tchild.setVersion((short)0x30);   //协议版本号，48是3.0 协议，32是2.0协议\n\n\t\tchild.setMaxChannels((short)4);\n\t\tchild.setRetryWaitTimeSec((short)30);\n\t\tchild.setMaxRetryCnt((short)3);\n\t\tchild.setReSendFailMsg(true);\n//\t\tchild.setWriteLimit(200);\n//\t\tchild.setReadLimit(200);\n\t\tList\u003cBusinessHandlerInterface\u003e serverhandlers = new ArrayList\u003cBusinessHandlerInterface\u003e();\n\t\tserverhandlers.add(new CMPPMessageReceiveHandler()); //在这个handler里接收短信\n\t\tchild.setBusinessHandlerSet(serverhandlers);\n\t\tserver.addchild(child);\n\t\t\n\t\tmanager.addEndpointEntity(server);\n\t\n\t\tCMPPClientEndpointEntity client = new CMPPClientEndpointEntity();\n\t\tclient.setId(\"client\");   //自定义通道账号ID，保持全局唯一\n\t\tclient.setHost(\"127.0.0.1\");\n//\t\tclient.setLocalhost(\"127.0.0.1\");\n//\t\tclient.setLocalport(65521);\n\t\tclient.setPort(7890);\n\t\tclient.setChartset(Charset.forName(\"utf-8\"));\n\t\tclient.setGroupName(\"test\"); //自定义通道账号分组ID，用于对通道标识不同组，方便路由实现\n\t\tclient.setUserName(\"901783\"); //通道账号，可能和企业代码相同\n\t\tclient.setPassword(\"ICP001\"); //密码\n\t\tclient.setMsgSrc(\"902176\");  //企业代码 ，可能和UserName相同\n\t\tclient.setSpCode(\"10658762\"); //服务代码，即显示到手机上的号码\n\t\tclient.setMaxChannels((short)10);  //最大连接数\n\t\tclient.setVersion((short)0x30);    //协议版本号\n\t\tclient.setRetryWaitTimeSec((short)30);//发送request后 等待N秒后没有收到response，则重发消息\n\t\tclient.setMaxRetryCnt((short)3);  // 发送消息的最大次数，如果为3，则表示连带第一次发送，再重试2次，一共发送3次\n\t\tclient.setCloseWhenRetryFailed(false);  // 当发送消息次数达到最大（MaxRetryCnt）后 ，是否关闭连接。默认是 true 关闭连接\n\t\tclient.setUseSSL(false);          //是否使用SSL加密连接，默认为false，不加密\n\t\tclient.setWriteLimit(100);        //发送request消息的最大速度（单位条数）\n\t\tclient.setReadLimit(100);         //接收request的最大速度（单位条数），当消息量超过一定限制后，消息将积压在TCP网络协议栈的接收缓冲区\n\t\tclient.setWindow(16);         //设置发送消息的滑动窗口，滑动窗口默认为16，该大小根据网络时延不同，会影响发送速度\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t//默认为false ,发送request是否保存在本地磁盘。如果为true，当进程关闭后，本地磁盘会保存未收到response的消息，当进程再次启动框架自动读取消息并发送。可能造成消息重复发送\n\t\tclient.setReSendFailMsg(true);  \n\t\t\n\t\tclient.setSupportLongmsg(SupportLongMessage.BOTH);  //是否支持长短信的自动拆分和拼接\n\t\t\n\t\tList\u003cBusinessHandlerInterface\u003e clienthandlers = new ArrayList\u003cBusinessHandlerInterface\u003e();\n\t\tclienthandlers.add( new CMPPSessionConnectedHandler(10000));  //在这个handler里发送短信\n\t\tclient.setBusinessHandlerSet(clienthandlers);\n\t\t\n\t\tmanager.addEndpointEntity(client);\n\t\t\n\t\tmanager.openEndpoint(server);\n\t\t\n\t\tThread.sleep(1000);\n\t\tfor(int i=0;i\u003c=child.getMaxChannels()+1;i++)\n\t\t\tmanager.openEndpoint(client);\n\n        System.out.println(\"start.....\");\n        \n//\t\tThread.sleep(300000);\n        LockSupport.park();\n\t\tEndpointManager.INS.close();\n\t}\n}\n```\n\n## SMPP Api使用举例\n\n```java\n\npublic class TestSMPPEndPoint {\n\tprivate static final Logger logger = LoggerFactory.getLogger(TestSMPPEndPoint.class);\n\n\t@Test\n\tpublic void testSMPPEndpoint() throws Exception {\n\t\n\t\tfinal EndpointManager manager = EndpointManager.INS;\n\n\t\tSMPPServerEndpointEntity server = new SMPPServerEndpointEntity();\n\t\tserver.setId(\"smppserver\");\n\t\tserver.setHost(\"127.0.0.1\");\n\t\tserver.setPort(2776);\n\t\tserver.setValid(true);\n\t\t//使用ssl加密数据流\n\t\tserver.setUseSSL(false);\n\t\t\n\t\tSMPPServerChildEndpointEntity child = new SMPPServerChildEndpointEntity();\n\t\tchild.setId(\"smppchild\");\n\t\tchild.setSystemId(\"901782\");\n\t\tchild.setPassword(\"ICP\");\n\n\t\tchild.setValid(true);\n\t\tchild.setChannelType(ChannelType.DUPLEX);\n\t\tchild.setMaxChannels((short)3);\n\t\tchild.setRetryWaitTimeSec((short)30);\n\t\tchild.setMaxRetryCnt((short)3);\n\t\tchild.setReSendFailMsg(true);\n\t\tchild.setIdleTimeSec((short)15);\n//\t\tchild.setWriteLimit(200);\n//\t\tchild.setReadLimit(200);\n\t\tList\u003cBusinessHandlerInterface\u003e serverhandlers = new ArrayList\u003cBusinessHandlerInterface\u003e();\n\t\tserverhandlers.add(new SMPPSessionConnectedHandler(10000));   \n\t\tchild.setBusinessHandlerSet(serverhandlers);\n\t\tserver.addchild(child);\n\t\t\n\t\tSMPPClientEndpointEntity client = new SMPPClientEndpointEntity();\n\t\tclient.setId(\"smppclient\");\n\t\tclient.setHost(\"127.0.0.1\");\n\t\tclient.setPort(2776);\n\t\tclient.setSystemId(\"901782\");\n\t\tclient.setPassword(\"ICP\");\n\t\tclient.setChannelType(ChannelType.DUPLEX);\n\n\t\tclient.setMaxChannels((short)12);\n\t\tclient.setRetryWaitTimeSec((short)100);\n\t\tclient.setUseSSL(false);\n\t\tclient.setReSendFailMsg(true);\n//\t\tclient.setWriteLimit(200);\n//\t\tclient.setReadLimit(200);\n\t\tclient.setSupportLongmsg(SupportLongMessage.SEND);  //接收长短信时不自动合并\n\t\tList\u003cBusinessHandlerInterface\u003e clienthandlers = new ArrayList\u003cBusinessHandlerInterface\u003e();\n\t\tclienthandlers.add( new SMPPMessageReceiveHandler()); \n\t\tclient.setBusinessHandlerSet(clienthandlers);\n\t\t\n\t\t\n\t\tmanager.addEndpointEntity(server);\n\t\tmanager.addEndpointEntity(client);\n\t\tmanager.openAll();\n\t\tmanager.startConnectionCheckTask();\n\t\tThread.sleep(1000);\n\t\tfor(int i=0;i\u003cchild.getMaxChannels();i++)\n\t\t\tmanager.openEndpoint(client);\n\t\tSystem.out.println(\"start.....\");\n\t\tLockSupport.park();\n\t\tEndpointManager.INS.close();\n\t}\n}\n\t\n```\n\n## SGIP Api使用举例\n\n```java\npublic class TestSgipEndPoint {\n\tprivate static final Logger logger = LoggerFactory.getLogger(TestSgipEndPoint.class);\n\n\t@Test\n\tpublic void testsgipEndpoint() throws Exception {\n\t\tResourceLeakDetector.setLevel(Level.ADVANCED);\n\t\tfinal EndpointManager manager = EndpointManager.INS;\n\n\t\tSgipServerEndpointEntity server = new SgipServerEndpointEntity();\n\t\tserver.setId(\"sgipserver\");\n\t\tserver.setHost(\"127.0.0.1\");\n\t\tserver.setPort(8001);\n\t\tserver.setValid(true);\n\t\t//使用ssl加密数据流\n\t\tserver.setUseSSL(false);\n\t\t\n\t\tSgipServerChildEndpointEntity child = new SgipServerChildEndpointEntity();\n\t\tchild.setId(\"sgipchild\");\n\t\tchild.setLoginName(\"333\");\n\t\tchild.setLoginPassowrd(\"0555\");\n\n\t\tchild.setValid(true);\n\t\tchild.setChannelType(ChannelType.DUPLEX);\n\t\tchild.setMaxChannels((short)3);\n\t\tchild.setRetryWaitTimeSec((short)30);\n\t\tchild.setMaxRetryCnt((short)3);\n\t\tchild.setReSendFailMsg(false);\n\t\tchild.setIdleTimeSec((short)30);\n//\t\tchild.setWriteLimit(200);\n//\t\tchild.setReadLimit(200);\n\t\tchild.setSupportLongmsg(SupportLongMessage.SEND);  //接收长短信时不自动合并\n\t\tList\u003cBusinessHandlerInterface\u003e serverhandlers = new ArrayList\u003cBusinessHandlerInterface\u003e();\n\t\t\n\t\tserverhandlers.add(new SgipReportRequestMessageHandler());\n\t\tserverhandlers.add(new SGIPMessageReceiveHandler());   \n\t\tchild.setBusinessHandlerSet(serverhandlers);\n\t\tserver.addchild(child);\n\t\t\n\t\tmanager.addEndpointEntity(server);\n\t\t\n\t\t\n\t\tSgipClientEndpointEntity client = new SgipClientEndpointEntity();\n\t\tclient.setId(\"sgipclient\");\n\t\tclient.setHost(\"127.0.0.1\");\n\t\tclient.setPort(8001);\n\t\tclient.setLoginName(\"333\");\n\t\tclient.setLoginPassowrd(\"0555\");\n\t\tclient.setChannelType(ChannelType.DUPLEX);\n\n\t\tclient.setMaxChannels((short)10);\n\t\tclient.setRetryWaitTimeSec((short)100);\n\t\tclient.setUseSSL(false);\n\t\tclient.setReSendFailMsg(true);\n//\t\tclient.setWriteLimit(200);\n//\t\tclient.setReadLimit(200);\n\t\tList\u003cBusinessHandlerInterface\u003e clienthandlers = new ArrayList\u003cBusinessHandlerInterface\u003e();\n\t\tclienthandlers.add(new SGIPSessionConnectedHandler(10000));\n\t\tclient.setBusinessHandlerSet(clienthandlers);\n\t\tmanager.addEndpointEntity(client);\n\t\tmanager.openAll();\n\t\tThread.sleep(1000);\n\t\tfor(int i=0;i\u003cchild.getMaxChannels();i++)\n\t\t\tmanager.openEndpoint(client);\n\t\tSystem.out.println(\"start.....\");\n      \n        LockSupport.park();\n\n\t\tEndpointManager.INS.close();\n\t}\n}\n```\n\n## Demo 执行日志\n\n```\n\n11:31:52.842 [workGroup2] INFO  c.z.s.c.m.AbstractEndpointConnector - handlers is not shareable . clone it success. com.zx.sms.codec.smpp.SMPP2CMPPBusinessHandler@1d7059df\n11:31:52.852 [workGroup1] INFO  c.z.s.c.m.AbstractEndpointConnector - handlers is not shareable . clone it success. com.zx.sms.codec.smpp.SMPP2CMPPBusinessHandler@75e134be\n11:31:52.852 [workGroup1] INFO  c.z.s.c.m.AbstractEndpointConnector - handlers is not shareable . clone it success. com.zx.sms.handler.api.gate.SessionConnectedHandler@aa80b58\n11:31:52.869 [workGroup1] INFO  c.z.s.s.AbstractSessionLoginManager - login in success on channel [id: 0xfdc7b81e, L:/127.0.0.1:11481 - R:/127.0.0.1:2776]\n11:31:52.867 [workGroup2] INFO  c.z.s.s.AbstractSessionLoginManager - login in success on channel [id: 0x1fba3767, L:/127.0.0.1:2776 - R:/127.0.0.1:11481]\n11:31:53.863 [busiWork-3] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:343,   speed : 343/s\n11:31:54.872 [busiWork-1] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:1381,   speed : 1038/s\n11:31:55.873 [busiWork-8] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:2704,   speed : 1323/s\n11:31:56.875 [busiWork-2] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:4010,   speed : 1306/s\n11:31:57.880 [busiWork-5] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:5416,   speed : 1406/s\n11:31:58.881 [busiWork-7] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:7442,   speed : 2026/s\n11:31:59.882 [busiWork-8] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:9581,   speed : 2139/s\n11:32:00.883 [busiWork-2] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:12865,   speed : 3284/s\n11:32:01.884 [busiWork-5] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:15937,   speed : 3072/s\n11:32:02.886 [busiWork-5] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:19489,   speed : 3552/s\n11:32:03.887 [busiWork-6] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:23065,   speed : 3576/s\n11:32:04.888 [busiWork-2] INFO  c.z.s.h.a.s.MessageReceiveHandler - Totle Receive Msg Num:26337,   speed : 3272/s\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FLihuanghe%2FSMSGate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FLihuanghe%2FSMSGate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FLihuanghe%2FSMSGate/lists"}