1 背景: 小豆社保半夜短信费用完

阿亮: 小豆社保CTO, 原安邦集团系统架构师 。

半夜电话: 我们公司短信用完, 短信服务商半夜无法充值,怎么办?
应急办法: 提供企业短信网关,限制IP 调用 ,几行代码搞定, 问题解决。
为何企业短信网关能快速解决问题,以下将对什么叫企业短信网关 。

2 企业短信网关

  • 短信网关, 指同时对接移动、联通、电信的大网关, 根据短信号码自动识别属于哪家运营商。
  • 企业短信网关, 同一个企业可以同时对接多家短信服务商,比如助通科技、亿美软通、云掌通等.
    更多短信服务商参考《2021全网最全短信服务商排名》。
    区别: 前者(短信网关)是和移动、联通、电信(类似银行)直接对接,该一般由短信服务商建设。
    后者(企业短信网关)是和短信服务商(类似第三方支付,比如易宝支付)对接 。该系统由终端用户建设。

2.1企业短信网关架构

参考支付网关《最早的支付网关(滴滴支付)和最新的聚合支付设计架构》

2.2 路由及接口配置 (sms_config.ini)

{"route": {"izton": "30","ztinfo":"50","aliyun":"20","emay":"0","tzhl":"0","monyun":"0"},"izton": {"smsUrl": "http://139.129.107.160:8085/sendsms.php","userid": "","password": "","ext": "2033"},"emay": {"smsUrl": "http://www.btom.cn:8080","appId": "","secretKey": ""},"tzhl": {"smsUrl": "http://sms.tongzhouhl.com:9885/c123","tzId": "","tzPwd": ""},"aliyun": {"endpoint": "http://dysmsapi.aliyuncs.com","accessKeyId": "","accessKeySecret": ""},"ztinfo": {"smsUrl": "http://api.mix2.zthysms.com","username": "","password": "!"},"monyun": {"smsUrl": "http://api01.monyun.cn:7901","userid": "","password": ""}
}

2.3 路由及动态加载接口

package com.newxtc.sms;import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import com.alibaba.fastjson.JSONObject;
import com.newxtc.sms.cache.SpAccCache;
import com.newxtc.sms.entity.SmsRetMsg;public class SmsProvider {private final static Logger logger = LoggerFactory.getLogger(SmsProvider.class);public static SmsApi getSmsApi(String spCode) {Map<String, String> configMap = SmsInit.getInstance().getConfig(spCode);return configMap != null ? new SmsClassLoad(configMap, spCode) : null;}public static SmsApi getTpSmsApi(String spCode) {Map<String, String> configMap = SmsInit.getInstance().getConfig(spCode);return configMap != null ? new SmsClassLoad(configMap, spCode) : null;     }public static String sendTpSms(String phone, String templateId, String templateJson) {if (phone == null || templateId == null) {logger.error("sendTpSms() phone=" + phone + "|templateId=" + templateId);}String routeType = "route";Map<String, String> configMap = SmsInit.getInstance().getConfig(routeType);Set<String> key = configMap != null ? configMap.keySet() : null;if (key != null && key.size() > 0) {// 通道个数String spCode = null;SmsRetMsg retMsg = null;SmsApi smsImpl = null;try {boolean isJson = templateJson != null && templateJson.contains("{") && templateJson.contains("}") && templateJson.contains(":");@SuppressWarnings("unchecked")Map<String, String> paramMap = isJson ? JSONObject.parseObject(templateJson, Map.class) : null;for (int i = 0; i <= 1; i++) {spCode = SmsProvider.getRoute(configMap, null, routeType);smsImpl = isTpSp(spCode) ? getTpSmsApi(spCode) : getSmsApi(spCode);retMsg = smsImpl != null ? smsImpl.sendTemplateSms(phone, templateId, paramMap) : null;Integer ret = (retMsg != null) ? retMsg.getRet() : null;if (ret != null && ret.equals(0)) {break;} else {if (configMap.size() == 1)spCode = null;logger.error("first ret=" + ret);continue;}}if (retMsg != null) {JSONObject json = new JSONObject();json.put("ret", retMsg.getRet());json.put("msg", retMsg.getMsg());json.put("spCode", spCode);String message = json.toJSONString();return message;} else {logger.error("spCode=" + spCode + "|retMsg=" + retMsg);return null;}} catch (Exception e) {logger.error("spCode=" + spCode + "|e=" + e.toString());for (StackTraceElement elment : e.getStackTrace()) {logger.error(elment.toString());}return null;}} else {logger.error("no exist smsProvider");return null;}}private static boolean isTpSp(String spCode) {Map<String, String> configMap = SmsInit.getInstance().getConfig("route_msg");return !configMap.containsKey(spCode);}public static String sendSms(String phone, String msg) {String routeType = "route_msg";Map<String, String> configMap = SmsInit.getInstance().getConfig(routeType);Set<String> key = configMap != null ? configMap.keySet() : null;if (key != null && key.size() > 0) {// 通道个数String spCode = null;SmsRetMsg retMsg = null;SmsApi smsImpl = null;spCode = SmsProvider.getRoute(configMap, null, routeType);smsImpl = getSmsApi(spCode);retMsg = smsImpl != null ? smsImpl.sendSms(phone, msg) : null;if (retMsg.getRet() != 0) {logger.error("first retMsg=" + retMsg);spCode = SmsProvider.getRoute(configMap, spCode, routeType);smsImpl = getSmsApi(spCode);retMsg = smsImpl != null ? smsImpl.sendSms(phone, msg) : null;}if (retMsg != null) {JSONObject json = new JSONObject();json.put("ret", retMsg.getRet());json.put("msg", retMsg.getMsg());json.put("spCode", spCode);String message = json.toJSONString();return message;} else {logger.error("no exist smsProvider");return null;}} else {logger.error("no exist smsProvider");return null;}}/*** 根据当前发送量,和目标比率对比,选择发送的通道* * @return*/public static String getRoute(Map<String, String> configMap, String exinclude, String smsType) {// 1 计算的各通道实际发送比率// 配置分母值 Denominatorlong configSum = 0, cacheSum = 0;// 内存中实际的总署long cacheCount = 0;// 第一次遍历计算总数for (String spCode : configMap.keySet()) {configSum += Long.parseLong(configMap.get(spCode));cacheSum += SpAccCache.getInstance().get(spCode);}// 第二次遍历计算每个通道的比率double from, destn;// 目标比率String retSp = null;// 命中通道for (String spCode : configMap.keySet()) {retSp = spCode;if (exinclude != null && spCode.equals(exinclude))continue;long configSp = Long.parseLong(configMap.get(spCode));cacheCount = SpAccCache.getInstance().get(spCode);from = (cacheCount * 1.0) / cacheSum;destn = (configSp * 1.0) / configSum;if (from < destn) {logger.debug("getRoute() spCode=" + spCode + "|from=" + from + "|destn=" + destn);break;}}return retSp;}public static void main(String[] args) throws Exception {SmsInit.getInstance().init(null);String templateId = "1";String templateJson = "{\"code\":\"8988788\"}";for (int i = 0; i < 20; i++) {SmsProvider.sendTpSms("13718211912", templateId, templateJson);Thread.sleep(20000);}Map<String, AtomicLong> m = SpAccCache.getInstance().getAccMap();for (String spCode : m.keySet()) {Long l = m.get(spCode).get();System.out.println(spCode + "=" + l);}}}

2.4 发送次数计数器

利用原子技术及内存结构, 分别为每一个通道计数

package com.newxtc.sms.cache;import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;public class SpAccCache {// 内存 key: spCode : value : 计数器private static Map<String, AtomicLong> accMap = null;private static SpAccCache statisticsResultCache = null;public static SpAccCache getInstance() {if (statisticsResultCache == null) {statisticsResultCache = new SpAccCache();accMap = new ConcurrentHashMap<String, AtomicLong>();}return statisticsResultCache;}public Map<String, AtomicLong> getAccMap() {return accMap;}// 累加器public long inc(String spCode) {AtomicLong atomic = accMap.get(spCode);if (atomic == null) {atomic = new AtomicLong();accMap.put(spCode, atomic);}return atomic.incrementAndGet();}public long get(String spCode) {AtomicLong atomic = accMap.get(spCode);return atomic != null ? atomic.get() : 0;}}

2.5 发送回执多线程异步获取

短信请求后, 短信服务商的回应只是说这笔请求已经收到,但并不是真正发送的结果,
获取的的方式有2种,

  1. 查询,一般可以查询最近一个时段的, 查询过的不再返回,
  2. 有些服务商也会主动通知,但主动通知需要暴露一个互联网端口,为了收取短信回执不值得的, 所以采用的比较少。

多线程延时队列,采用延时队列,可以按指定的时间执行任务。

package com.newxtc.sms.delay;import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** @author RAF**/
public class DelayThread {// logprivate final static Logger logger = LoggerFactory.getLogger(DelayThread.class);private BlockingQueue<Runnable> taskQueue;private ThreadPoolExecutor pool;private int dispatchQueueSize = 10000;private int disatchThreadSize = 4;private static DelayThread instance = null;public synchronized static DelayThread getInstance() {if (instance == null) {instance = new DelayThread();System.out.println("DelayThread init ");}return instance;}/*** * @param config_path*            配置路径* @param threadName*            线程名称, 线程的配置文件为:配置路径 + 线程名称.ini* @param taskEntityCls*            任务实体类 , 必须实现ScanTaskApi 接口* @param runCls*            任务运行类, 必须实现 ThreadRunApi 接口*/public DelayThread() {try {taskQueue = new LinkedBlockingQueue<Runnable>(dispatchQueueSize);pool = new ThreadPoolExecutor(disatchThreadSize, disatchThreadSize * 2, 10 * 1000L, TimeUnit.MILLISECONDS, taskQueue, new ThreadPoolExecutor.AbortPolicy());} catch (Exception e) {logger.error(e.toString());}}public ThreadPoolExecutor getThreadPoolExecutor() {return this.pool;}public void addTask(DelayTask task) {try {pool.execute(task);} catch (RejectedExecutionException e) {logger.error(e.toString());} catch (Throwable e) {logger.error(e.toString());}}
}

执行任务

package com.newxtc.sms.delay;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import com.newxtc.sms.SmsApi;
import com.newxtc.sms.delay.cache.DelayCache;
import com.newxtc.sms.delay.entity.Delay;public class DelayTask implements Runnable {private final static Logger logger = LoggerFactory.getLogger(DelayTask.class);private SmsApi smsApi = null;private String uniqId = null; // 支持根据具体单号来查询private int repeat = 0;public DelayTask(String uniqId, SmsApi smsApi) {this.uniqId = uniqId;this.smsApi = smsApi;}public String getUniqId() {return uniqId;}@Overridepublic void run() {try {repeat++;if (smsApi == null) {logger.error("uniqId=" + uniqId + "|smsApi=" + smsApi);return;}int ret = smsApi.getReport();// 碰到错误返回,最多重试3次,避免系统性问题导致重复调用if (ret != 0 && repeat <= 3) {Delay delay = new Delay();delay.setTask(this);delay.setE(System.currentTimeMillis() + 10000);DelayCache.getInstance().put(delay);logger.debug("delayTask repeat=" + repeat + "|next time=" + delay.getE());}} catch (Throwable e) {logger.error("run Error", e);for (StackTraceElement ele : e.getStackTrace())logger.error(ele.toString());}}}

短信调用测试

测试类
com.newxtc.sms.SmsProvider

public static void main(String[] args) throws Exception {SmsInit.getInstance().init(null);String templateId = "1";String templateJson = "{\"code\":\"8988788\"}";for (int i = 0; i < 20; i++) {SmsProvider.sendTpSms("13718211912", templateId, templateJson);Thread.sleep(20000);}Map<String, AtomicLong> m = SpAccCache.getInstance().getAccMap();for (String spCode : m.keySet()) {Long l = m.get(spCode).get();System.out.println(spCode + "=" + l);}}

测试日志:
SmsConfig init success totalWeight=100
|–route={izton=30, ztinfo=50, aliyun=20}
|–key=izton
|–key=emay
|–key=tzhl
|–key=aliyun
|–key=ztinfo
|–key=monyun
|–route_msg={izton=30, ztinfo=50, aliyun=20}
templateSign=新昕科技
template=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|-1->您的验证码是:${code},有效期为1分钟。如非本人操作,可不用理会。
|-2->您购买的商品已支付成功,支付金额 payprice元,订单号{pay_price}元,订单号payp​rice元,订单号{order_id},感谢您的光临!
|-3->亲爱的用户${nickname},您的商品 storename,订单号{store_name},订单号storen​ame,订单号{order_id}已发货,请注意查收
|-4->亲,您的订单orderid,商品{order_id},商品orderi​d,商品{store_name}已确认收货,感谢您的光临!
|-5->adminname管理员,您有一笔已支付的订单待处理,订单号为{admin_name}管理员,您有一笔已支付的订单待处理,订单号为adminn​ame管理员,您有一笔已支付的订单待处理,订单号为{order_id}!
|-6->adminname管理员,您有一笔支付成功的订单待处理,订单号{admin_name}管理员,您有一笔支付成功的订单待处理,订单号adminn​ame管理员,您有一笔支付成功的订单待处理,订单号{order_id}!
|-7->adminname管理员,您有一笔退款订单待处理,订单号{admin_name}管理员,您有一笔退款订单待处理,订单号adminn​ame管理员,您有一笔退款订单待处理,订单号{order_id}!
|-8->adminname管理员,您有一笔订单已经确认收货,订单号{admin_name}管理员,您有一笔订单已经确认收货,订单号adminn​ame管理员,您有一笔订单已经确认收货,订单号{order_id}!
|-9->您有未付款订单,订单号为:orderid,商品数量有限,请及时付款。∣−10−>您的订单{order_id},商品数量有限,请及时付款。 |-10->您的订单orderi​d,商品数量有限,请及时付款。∣−10−>您的订单{order_id},实际支付金额已被修改为${pay_price}
aliyun=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|-1->SMS_193517056
|-2->SMS_193506971
|-3->SMS_193521858
|-4->SMS_193521862
|-5->SMS_193516931
|-6->SMS_193511960
|-7->SMS_193511961
|-8->SMS_193516936
|-9->SMS_193506990
|-10->SMS_193516944
19:14:12.163 [main] ERROR com.newxtc.sms.impl.AliyunSmsImpl - sendTpSms() paramsMap={Format=JSON, SignName=新昕科技, SignatureMethod=HMAC-SHA1, TemplateCode=SMS_193517056, Signature=aBR/qWCZb8CqP709yDWdKEJ1i6Q=, Timestamp=2021-02-07T11:14:11Z, TemplateParam={“code”:“8988788”}, OutId=13718211912-1612696451861, AccessKeyId=, Action=SendSms, RegionId=cn-hangzhou, SignatureNonce=6236648b-937a-4cf1-bd42-2ddac5540d32, SignatureVersion=1.0, Version=2017-05-25, PhoneNumbers=13718211912}|response=
19:14:12.166 [main] ERROR com.newxtc.sms.SmsProvider - first ret=-99
19:14:12.166 [main] DEBUG com.newxtc.sms.SmsProvider - getRoute() spCode=izton|from=0.0|destn=0.3
19:14:12.167 [main] DEBUG com.newxtc.sms.impl.IztonSmsImpl - sendTpSms() {code=8988788}
19:14:12.201 [main] INFO com.newxtc.sms.impl.IztonSmsImpl - sendSms() spCode=izton|phone=13718211912|total=1|msg=【新昕科技】您的验证码是:8988788,有效期为1分钟。如非本人操作,可不用理会。
19:14:12.202 [main] ERROR com.newxtc.sms.SmsProvider - first ret=-1
19:14:32.202 [main] DEBUG com.newxtc.sms.SmsProvider - getRoute() spCode=ztinfo|from=0.0|destn=0.5
19:14:32.203 [main] DEBUG com.newxtc.sms.impl.ZtinfoSmsImpl - sendTpSms() {code=8988788}
19:14:32.335 [main] ERROR com.newxtc.sms.impl.ZtinfoSmsImpl - sendSms() jsonObj={“content”:"【新昕科技】您的验证码是:8988788,有效期为1分钟。如非本人操作,可不用理会。",“username”:"",“tKey”:“1612696472”,“extend”:“13718211912-1612696472”,“password”:“e7e2bc55be19eedae481fcb398c7c891”,“mobile”:“13718211912”}|response={“code”:4001,“msg”:“username wrong”,“msgId”:“161269646780396789761”,“contNum”:0}
19:14:32.336 [main] ERROR com.newxtc.sms.SmsProvider - first ret=4001

代码参考: 下载地址

相关阅读:
百家企业短信网关(背景及核心代码)-1-同时对接多家短信公司的开源免费代码
2021全网最全短信服务商排名(100余家短信商户对照)

百家企业短信网关(背景及核心代码)-1-同时对接多家短信公司的开源免费代码相关推荐

  1. java实现手机短信发送的功能详细代码

    我调用的是阿里的短信服务,如果是个人测试,可以不用办服务. 直接拿支付宝账号登录就可以 需要获取阿里的key的步骤 第一步:登录后进入控制台,先给自己的账号余额中充入2元(做测试足够了) 第二步:获取 ...

  2. 短信字数的验证JAVA代码

    短信验证码字数验证的代码 public class Test67{public static void main(String[]args){boolean countPass = ValidateC ...

  3. 开源免费代码_02_单摇杆远程遥控小车,基于Arduino的ESP-NOW,ESP32发送指令、ESP32接收指令,实现小车毫秒级完美控制_公羽兴

    发文希望能够帮忙物联网爱好者少走弯路,少被割韭菜.如果觉得此文对您有帮助的话帮忙点个赞,感谢!!! 哔哩哔哩网址:开源免费代码_02_单摇杆远程遥控小车,基于Arduino的ESP-NOW,ESP32 ...

  4. 开源免费代码_01_基于Arduino的ESP-NOW,ESP32发送指令、ESP-01S接收指令,通过ESP-01S继电器实现LED灯开关控制_公羽兴

    发文希望能够帮忙物联网爱好者少走弯路,少被割韭菜.如果觉得此文对您有帮助的话帮忙点个赞,感谢!!! 哔哩哔哩网址:开源免费代码_01_基于Arduino的ESP-NOW,ESP32发送指令.ESP-0 ...

  5. JAVA实现SMS短信发送(附:代码)

    **先决条件:网建短信通提供了短信发送API,进入 [http://www.smschinese.cn/api.shtml](http://www.smschinese.cn/api.shtml) 进 ...

  6. 阿里云短信服务(申请与代码)

    目前阿里云短信签名审核更加严格了,如果不通过请转至<腾讯云短信申请与代码>,腾讯云简单易通过 记录申请短信服务签名的坑 短信服务签名申请注意事项: 自2020年12月17日开始,阿里云短信 ...

  7. php短信接口怎么使用_PHP代码示例_PHP短信接口 | 微米-中国领先的短信彩信接口平台服务商...

    PHP短信接口一代码示例 请求 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "http://api.weimi.cc/2/sms/sen ...

  8. 让 MTK手机 黑屏死机 的短信代码 让山寨机死机的短信 让手机死机的短信

    BEGIN:IMELODY VERSION:1.2 FORMAT:CLASS1.0 BEAT:100 MELODY:(ledoffledonbackoffbackonvibeon@0) END:IME ...

  9. android短信验证码代码,Android短信验证码自动填写实现代码

    今天给大家分享一个Android短信验证码自动填写的功能.先看下效果图,我发送了一条短信到手机,自动填写验证码.如图: 这个小功能运用到了观察者模式,什么是观察者模式? 观察者模式 :定义对象间的一种 ...

  10. 阿里个人短信验证码从申请到代码实现全流程

    首先:进入阿里云-云市场官网,然后进行就是登陆操作:这个就不用说了,有阿里云账号就用阿里云账号,用支付宝或淘宝账号也可以,都没有就注册一个就行了. 说明:目前个人开发者比较容易申请到的只有验证码的短信 ...

最新文章

  1. 如何让asp.net应用程序定时自动执行代码
  2. call()、apply()、bind()
  3. Pokémon Go火遍全球,开启全民捕捉小精灵的时代
  4. vcpkg安装_微软牌包管理器vcpkg更新及路线图计划
  5. html:(15):用css样式给表格添加边框和caption标签
  6. Vue中computed 计算机属性、getter、setter
  7. 未在本地计算机上注册“microsoft.ACE.oledb.12.0”提供程序解决办法
  8. 我的Python学习笔记(四):动态添加属性和方法
  9. Spring JDBC 【继承JdbcDaoSupport】
  10. java 虚拟机 分析_Java 虚拟机中的运行时数据区分析
  11. (二)目前主流的 Java 虚拟机有哪些?
  12. 西数推出首款SanDisk驱动器:蓝盘和绿盘
  13. 【错误率、精度、查准率、查全率和F1度量】详细介绍
  14. css外联式6,css样式内联式,外联式,嵌入式的格式是什么?
  15. 软件测试学习资料大全
  16. 3.19字节懂车帝一面
  17. 函数柯里化的几个例子
  18. Could not find a version that satisfies the requirement cryptography (from pymysql) (from versions:
  19. 前端上传视频至阿里云
  20. 求n的阶乘及1~n的阶乘之和

热门文章

  1. 树莓派SPI连接TFT屏幕
  2. 有没有能排列待办事项无广告的Windows版便签软件推荐
  3. java 订单支付_Java多订单多支付方式分配金额问题的解决
  4. 华为鸿蒙系统是安卓套壳?直接看代码
  5. C++类的常数据成员和常成员函数
  6. Python实验、Pandas数据处理与分析
  7. 6.26 mongoDB是无法find未初始的值的,mongoose的Schema需具象化及个人商品Schema设计,租赁网登录态初次尝试,vue对对象未赋初值的监听,forEach等api深拷贝问题
  8. JavaScript多线程编程
  9. 开放信息抽取(OIE)系统(一)--概述
  10. 【企业分析】拉勾、Boss直聘、猎聘