推送系統作為通用的元件,存在的價值主要有以下幾點
- 會被多個業務專案使用,推送系統獨立維護可降低維護成本
- 推送系統一般都是呼叫三方api進行推送,三方api一般會有呼叫頻率/次數限制,被推送的訊息需要走佇列來合理呼叫三方api,控制呼叫的頻率和次數
- 業務無關,一般推送系統設計成不需要關心業務邏輯
核心技術
- 訊息佇列
- 三方服務api呼叫
- 安卓app推送
- 蘋果app推送
- 微信小程式推送
- 郵件推送
- 釘釘推送
- 簡訊推送
訊息佇列選用阿里雲提供的rocketmq,官方文件:help.aliyun.com/document_de…
推送時序圖
右鍵新視窗開啟可以檢視高清大圖
可以看到訊息推送系統接入的第三方服務比較多,三方推送的質量很難統一,就需要考慮訊息的推送的重試了
思路流程
為了控制併發,所有的推送都先發到rocketmq佇列裡,每次推送的個數通過控制佇列的消費的客戶端的數量來實現
安卓和蘋果都可以使用信鴿的推送服務
信鴿推送需要客戶端進行整合,客戶端sdk參考:xg.qq.com/xg/ctr_inde…
目前信鴿個人開發者仍然是可以申請的,賬號建立後,建立andorid和ios專案
記錄下這裡的 APP ID和SECRET KEY,服務端進行推送時需要這兩個引數推送異常處理,推送異常時,需要重試,重試可以利用訊息佇列本身的重試機制,也可以自行實現重試邏輯
安卓app推送
<!-- 信鴿推送客戶端 -->
<dependency>
<groupId>com.github.xingePush</groupId>
<artifactId>xinge</artifactId>
<version>1.2.1</version>
</dependency>
複製程式碼
核心程式碼如下
Map<String, Object> messagePayload = new HashMap<String, Object>(1);
messagePayload.put("user_id", 1);
messagePayload.put("msg_title", "訊息標題");
messagePayload.put("msg_content", "訊息內容");
messagePayload.put("msg_type", 1);
messagePayload.put("data", Lists.newHashMap("order_id", 1));
XingeApp xinge = new XingeApp(androidAccessId, androidSecretKey);
MessageAndroid message = new MessageAndroid();
ClickAction action = new ClickAction();
action.setActionType(ClickAction.TYPE_ACTIVITY);
message.setAction(action);
message.setContent(JsonUtil.toJsonString(messagePayload));
message.setType(MessageAndroid.TYPE_MESSAGE);
message.setExpireTime(86400);
message.setCustom(new HashMap<String, Object>(1));
JSONObject response = xinge.pushSingleDevice("使用者的PushToken", message);
if (response.getInt(RET_CODE) != 0) {
// 推送異常了,進行日誌記錄等
}
複製程式碼
蘋果app推送
使用pushy庫進行推送
<!-- IOS推送客戶端 -->
<dependency>
<groupId>com.turo</groupId>
<artifactId>pushy</artifactId>
<version>0.13.3</version>
</dependency>
複製程式碼
Map<String, Object> aps = new HashMap<String, Object>(1);
aps.put("alert", "推送內容");
aps.put("sound", "default");
aps.put("badge", 1);
Map<String, Object> data = new HashMap<String, Object>(1);
data.put("msgTitle", "推送標題");
data.put("msgContent", "推送內容");
data.put("msgType", "1");
data.put("userId", "13288888888");
data.put("data", Lists.newHashMap("order_id", 1));
Map<String, Object> messagePayload = new HashMap<String, Object>(1);
messagePayload.put("aps", aps);
messagePayload.put("data", data);
ApnsClient apnsClient = new ApnsClientBuilder()
.setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST)
.setClientCredentials(this.getClass().getClassLoader().getResourceAsStream("推送證照相對resources目錄的路徑"), "")
.build();
String payload = JsonUtil.toJsonString(messagePayload);
String token = TokenUtil.sanitizeTokenString("app使用者的pushToken");
SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, "com.suxiaolin.app1", payload);
PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>>
sendNotificationFuture = apnsClient.sendNotification(pushNotification);
final PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse =
sendNotificationFuture.get();
if (pushNotificationResponse.isAccepted()) {
System.out.println("Push notification accepted by APNs gateway.");
} else {
System.out.println("Notification rejected by the APNs gateway: " +
pushNotificationResponse.getRejectionReason());
if (pushNotificationResponse.getTokenInvalidationTimestamp() != null) {
System.out.println("\t…and the token is invalid as of " +
pushNotificationResponse.getTokenInvalidationTimestamp());
}
}
複製程式碼
使用信鴿庫進行推送
當然也可以使用信鴿提供的ios推送,邏輯和安卓app的推送差不多
ios的信鴿客戶端可以和android的客戶端共用,不需要引入新的jar包了
JSONObject messagePayload = new JSONObject();
Map<String, Object> custom = new HashMap<String, Object>();
messagePayload.put("title", "推送標題");
messagePayload.put("body", "推送內容");
messagePayload.put("user_id", 1);
messagePayload.put("msg_type", 1);
messagePayload.put("data", Lists.newArrayList("orderId", 1));
XingeApp xinge = new XingeApp(iosAccessId, iosSecretKey);
MessageIOS message = new MessageIOS();
message.setType(MessageIOS.TYPE_APNS_NOTIFICATION);
message.setExpireTime(86400);
message.setAlert(content);
message.setBadge(1);
message.setCategory("INVITE_CATEGORY");
message.setCustom(messagePayload);
response = xinge.pushSingleDevice("app使用者的pushToken", message, XingeApp.IOSENV_PROD);
if (response.getInt(RET_CODE) != 0) {
// 推送異常了
}
複製程式碼
小程式推送
官方文件:mp.weixin.qq.com/wiki?t=reso…
可以看到微信小程式推送介面是普通的post請求小程式api地址:api.weixin.qq.com/cgi-bin/mes…
http請求,http請求庫可以參考:HttpUtil
釘釘推送
官方文件:open-doc.dingtalk.com/microapp/se… 程式碼示例
public static boolean send(String content, String title, Set<String> receivers) {
try {
HttpUtil.ResponseWrap result = HttpUtil.postWrap(ddUrl,
"{\n"
+ " \"msgtype\": \"text\",\n"
+ " \"text\": {\"content\":\"" + title + "\r\n" + content + "\n|"
+ receivers.stream().map(r -> "@" + r).collect(Collectors.joining(" ")) + "\"},\n"
+ " \"at\": {\n"
+ " \"atMobiles\": [" + receivers.stream().map(r -> "\"" + r + "\"").collect(Collectors.joining(",")) + "], \n"
+ " \"isAtAll\": false\n"
+ " }\n"
+ " }");
return result.getStatusCode() == 200;
} catch (Exception e) {
return false;
}
}
複製程式碼
完整程式碼參考 DingTalkUtil.java
使用http請求就可以請求了
郵件推送
傳送郵件可以使用java的javax.mail庫,smtp協議傳送郵件
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
複製程式碼
示例程式碼參考:EmailSender.java
簡訊推送
簡訊服務商眾多,郵件一般有統一的smtp協議可以使用,簡訊沒有協議,但是一般使用http傳送簡訊 比如以下的簡訊服務商
- 253雲通訊:zz.253.com/api_doc/kai…
- 簡訊服務- 又拍雲: www.upyun.com/products/sm…
- 訊息&簡訊_MSGSMS_雲通訊_簡訊- 華為雲: www.huaweicloud.com/product/msg…
訊息佇列的特性
訊息佇列消費異常後會自動進行重試
一些注意的點
微信小程式每次支付可以生成一個推送碼,需要儲存到資料庫或者快取裡,並且每個碼只能推送3條訊息
因為訊息佇列的消費在訊息量大的時候具有一定的延時,這就為取消訊息推送提供了可能,可以為每條訊息生成一個唯一的uuid,取消的時候把這個uuid設計進redis裡,推送時檢查這個uuid是否在redis裡決定推送與否
雖然推送存在不可控制的異常,比如三方推送服務出現了異常,但是也存在呼叫方傳遞引數異常,可以推送介面呼叫的返回值判斷是否呼叫推送系統成功,也可以記錄到日誌裡,這樣在調查異常原因時就比較容易
訊息佇列預設的重試次數,消費時長是無法控制的,可以對訊息佇列的客戶端進行修改支援這個特性,參考:github.com/jibaole/spr… 核心邏輯是先給訊息設定一個最大消費次數和消費時長,然後當訊息消費次數和消費時長達到閾值時,直接置為成功
ios使用信鴿推送時,需要上傳開發證照和生產證照,這兩個證照至少需要上傳一個