這篇文章費了我好多心血啊,這都是在我測試了一堆失敗的代碼,看了大量的博客之后,把其中最有用,最精華的部分提取出來的集成,也是我艱辛的踩坑歷程,滿滿的干貨?。】赡苁俏姨?,這些東西以前都沒接觸過,所以下面會有很多很基礎的東西,大神切莫見怪。。。
HTTP/2
- HTTP/2掃盲:http://www.cnblogs.com/yingsmirk/p/5248506.html
- HTTP/2官網(wǎng)文檔:https://http2.github.io/
- HTTP/2標準文檔中英文對照:https://github.com/fex-team/http2-spec
- Wireshark抓包教程:http://fangxin.blog.51cto.com/1125131/735178
- HTTP/2 大神匯總博客:https://imququ.com/post/http2-resource.html
- 看過這些之后,應該對http/2協(xié)議有了一些最初的了解,知道大體是怎么回事,其實和http1/1差不多,區(qū)別點主要是以下幾點:
- 二進制分幀:每個請求分成多個幀進行傳送,都送到之后再進行拼裝
- 多路復用:二進制分幀之后的一個好處,多個請求共享同一個tcp連接,節(jié)約連接數(shù)量,提高連接利用率
- 服務器推送:推測客戶端之后可能要的數(shù)據(jù)提前推送
- 頭部壓縮:兩端各維護一個頭部表,每次請求只傳送頭部不同的部分,減少傳輸冗余資源
- ALPN應用層協(xié)議協(xié)商:和http1/1的兼容協(xié)商機制,這個下面會有關于java的相關說明
- 支持異步編程,非阻塞,提高效率
- 看了這些算是了解一些大概吧,很多東西到了實際使用中再來慢慢體會
OkHttp學習和使用
這東西好久之前就用過,這次算是復習和提升以下,以前就是當個http客戶端模擬使用,沒處理過cookie、證書什么的,這次由于下面要做的事情的需要,就做了這些測試:
- okhttp教程:http://gold.xitu.io/entry/5728441d128fe1006058b6b9
- OkHttp使用進階 譯自OkHttp Github官方教程:http://www.cnblogs.com/ct2011/p/3997368.html
- OkHttp使用完全教程:http://www.lxweimin.com/p/ca8a982a116b
用的很爽啊,鏈式編程、API設計易于理解、sample眾多、直接搬磚。。。
APNs
github上找了很多項目來實驗啊,最后還是Pushy這個最滿意,也最實用,并且也由它又發(fā)現(xiàn)了新世界,開啟新世界,新的征程開始~
先來點介紹文吧~ 這些介紹看完,相信你也對APNs新版的協(xié)議有了比較清楚的了解。
- APNs官網(wǎng)文檔:https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1
- APNs簡要介紹:基于HTTP2的全新APNs協(xié)議:https://github.com/ChenYilong/iOS9AdaptationTips
- https://github.com/ChenYilong/iOS9AdaptationTips/blob/master/%E5%9F%BA%E4%BA%8EHTTP2%E7%9A%84%E5%85%A8%E6%96%B0APNs%E5%8D%8F%E8%AE%AE/%E5%9F%BA%E4%BA%8EHTTP2%E7%9A%84%E5%85%A8%E6%96%B0APNs%E5%8D%8F%E8%AE%AE.md
Pushy神器來啦~下面的APNs調用都用這個項目來作為底層支撐。
- Pushy開源項目:https://github.com/relayrides/pushy
- Pushy官網(wǎng):http://relayrides.github.io/pushy/
- Pushy文檔:http://relayrides.github.io/pushy/apidocs/0.8/overview-summary.html
- Jetty ALPN配置:http://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-starting
- alpn-boot依賴下載:http://mvnrepository.com/artifact/org.mortbay.jetty.alpn/alpn-boot
額外收獲
- Netty高性能之道:http://www.infoq.com/cn/articles/netty-high-performance
- Netty官網(wǎng):http://netty.io/
- 書:《Netty權威指南》
你可能會覺得上面的廢話好多,好多東西好像不需要了解,直接使用Pushy就行了唄,其實我也不敢說上面那些東西是否真的對下面的有用,但是知道這些原理,對后面發(fā)生的異常才能心中有數(shù),至少在我的踩坑過程中也是深有體會的,其實上面那些東西不是我一開始就都看完的,是在我踩坑的過程中一步步補充的,每個人的只是學習順序也許有所不同,你可以根據(jù)自己的情況合理安排~
我之所以會事先看這些東西,原因也是因為http/2、APNs、IOS推送、TLS等這些東西我真的不是很了解,會有一種恐懼之心,算是我自己的一個知識補充吧,所以對于對這些知識掌握很好的大神其實上面那些基礎是完全可以不看的~
下面開始開發(fā):
環(huán)境配置:
在Pushy的README.md中詳細說明了Pushy所需的環(huán)境,我個人由于感激這篇文章在我踩坑過程中的作用,因此特別的把它翻譯出來Pushy README.md中文翻譯本。
因為Pushy本身依賴了其他類庫,為了方便,也由于我是是使用Maven管理和構件項目,我下面的教程完全都在Maven下進行部署和開發(fā),請悉知:
環(huán)境說明:
必須JDK7以上版本,這個Pushy README.md中文翻譯本上面詳細說明了。
- JDK8+Tomcat9 M11:部署成功
- JDK7+Tomcat7:部署成功
- JDK7+Tomcat9 M11:Tomcat啟動失敗,原因不明,我從Tomcat啟動失敗的報錯中認為可能是Tomcat9 M11中調用了JDK8中特有的API,導致在JDK7中啟動失敗。
所以你部署環(huán)境的時候這個要注意,特別是部署到服務器當中的時候。
步驟:
- 添加Pushy依賴:
<dependency>
<groupId>com.relayrides</groupId>
<artifactId>pushy</artifactId>
<version>0.8.1</version>
</dependency>
>2. 添加native SSL provider依賴,注意版本哦:
>```xml
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>1.1.33.Fork22</version>
</dependency>
這個是ALPN協(xié)議協(xié)商的實現(xiàn)依賴包,在Pushy README.md中文翻譯本有詳細說明的。到這一步,你的普通Java程序就能跑起來向APNs服務器發(fā)起一個推送了~~
- 添加alpn-boot依賴(Tomcat中所需):
這一步和上面的不太一樣,因為jar包需要添加到bootclasspath中,和普通的classpath不太一樣,在JVM啟動參數(shù)添加如下:
-Xbootclasspath/p:/Users/coselding/Downloads/alpn-boot-8.1.9.v20160720.jar
p:
后面的部分就是你下載下來的alpn-boot的jar包的本地地址,這個jar包的下載地址是這個http://mvnrepository.com/artifact/org.mortbay.jetty.alpn/alpn-boot
原因:
- 這種方式添加的jar包會替換JVM底層運行的相關API,你可以理解為加載優(yōu)先級更高的jar,但是這種方式加載的jar是和平臺相關的,所以你下載的jar包要選擇和你平臺相匹配的才行哦~~,當然,這里的這個jar其實已經(jīng)是linux、win、macOS全平臺都具有的了,它會根據(jù)平臺加載相應的那個組件。
- 加載這個jar的理由是,我們上面加載的netty-tcnative-boringssl-static這個依賴,和Tomcat內部的tcnative實現(xiàn)相沖突了,所以要用這個jar包要進行適配,具體的底層原理這里不研究,你只要記住,如果你使用Tomcat,就要加上步驟3的這個依賴,Jetty實測不需要這個依賴。
- 添加從Apple開發(fā)者平臺申請到的app證書文件到項目資源目錄下
- 開始編碼:創(chuàng)建ApnsClient對象實例:根據(jù)證書和證書密碼創(chuàng)建和APNs服務器的連接對象
ApnsClient apnsClient = new ApnsClientBuilder().setClientCredentials(new File("/path/to/p12-file"), "p12-file-password").build();
- 等待和APNs的連接成功(HTTP/2是異步的,但是這里連接沒成功后續(xù)步驟無法繼續(xù),所以需要等待):
Future<Void> connectFuture = apnsClient.connect(ApnsClient.PRODUCTION_APNS_HOST);
connectFuture.await();
>鏈接地址有`DEVELOPMENT_APNS_HOST`和`PRODUCTION_APNS_HOST`兩個,你要確認你拿到的證書是否支持開發(fā)者模式連接開發(fā)者服務器,我拿到的證書就是不支持的,需要直接連接正式服務器,這是我踩的坑。
>7. 封裝推送消息內容體:
```java
ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
payloadBuilder.setAlertBody("alert-message-body");
ApnsPayloadBuilder這個類可以好好看看,就是這個類封裝推送消息體,攜帶了APNs推送能發(fā)送的各個字段,比如顯示按鈕、通知消息標題、消息體、圖片名、消息聲音文件名等,還由于APNs對每個消息最大長度限制為4K,因此還對過長的消息進行了智能化地截取工作。最后這這個類會被序列化為json串,就像如下的
{
"aps" : {
"category" : "NEW_MESSAGE_CATEGORY"
"alert" : {
"body" : "Acme message received from Johnny Appleseed",
},
"badge" : 3,
"sound" : “chime.aiff"
},
"acme-account" : "jane.appleseed@apple.com",
"acme-message" : "message123456"
}
>如果你要自己封裝json也行,只要最后的json中有apple規(guī)定的那些鍵值就行,而額外的,你也可以自定義地添加一些自己業(yè)務所需的其他鍵值方便客戶端接收到推送消息之后進行處理。
>######智能截取4K長度:
```java
String payload = payloadBuilder.buildWithDefaultMaximumLength();
- 封裝消息體:
SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, "com.example.AppName", payload);
>其中token是每個設備生成的token串經(jīng)過如下代碼加工后得到的,相當于是設備的唯一id:
>```java
String token = TokenUtil.sanitizeTokenString("<device token>");
"com.example.AppName":這個是你的證書簽名,必須保證證書簽名、證書、證書密碼、產生token的app簽名全部一致,不然就會報錯。
你的整個推送消息體封裝好之后,在網(wǎng)絡http/2傳輸過程中的最終傳輸?shù)臄?shù)據(jù)格式如下,主要包括headers和body data:
HEADERS
- END_STREAM
+ END_HEADERS
:method = POST
:scheme = https
:path = /3/device/00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0
host = api.development.push.apple.com
apns-id = eabeae54-14a8-11e5-b60b-1697f925ec7b
apns-expiration = 0
apns-priority = 10
apns-topic = <MyAppTopic>
DATA
+ END_STREAM
{ "aps" : { "alert" : "Hello" } }
>9. 發(fā)送消息推送:
>```java
Future<PushNotificationResponse<SimpleApnsPushNotification>> sendNotificationFuture = apnsClient.sendNotification(pushNotification);
這是一個異步阻塞方法,調用之后推送通知會放到內部消息隊列,等待APNs接收到消息并反饋的時候才能通過下面的方法得到響應,否則下面這個方法就會阻塞著,上線產品建議寫成異步回調的方式:
PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse = sendNotificationFuture.get();
>10. 接收APNs服務器響應:
>```java
pushNotificationResponse.isAccepted();
以下方法獲取APNs服務器拒絕消息的相關響應信息:
pushNotificationResponse.getRejectionReason();//獲取拒絕原因
pushNotificationResponse.getTokenInvalidationTimestamp();//獲取token失效時間
>11. 連接斷開重連:
>```java
apnsClient.getReconnectionFuture().await();
- 關閉連接,釋放資源:
Future<Void> disconnectFuture = apnsClient.disconnect();
disconnectFuture.await();
>* 踩坑記錄:Pushy項目中依賴了gson,如下:
>```xml
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.6.2</version>
</dependency>
如果你添加了如下的gson:
<dependency>
<groupId>com.google</groupId>
<artifactId>gson</artifactId>
<version>1.3</version>
</dependency>
>那你的com.google的gson就會和Pushy中的gson沖突,然后出現(xiàn)未知的錯誤。。。知道就行,具體和這兩個gson的差別有關,我沒了解。。。這種坑也只有我這種人品的能踩到。。。最好是兩個夠不添加,反正Pushy在Maven中就會自動依賴引入了,何必多此一舉。
# 消息包裝實戰(zhàn)
我本人沒接觸過iOS開發(fā),因此對這個消息體那些字段有些什么用不是搞得很清楚,只是大概知道圖標、按鈕顯示、聲音等意思,但是具體到iOS設備接收到之后會有什么樣的消息體現(xiàn),我不是很清楚,但是懂得的人看了下面我的測試樣例,我相信也能很快包裝出自己想要的消息~
##### 消息json中的`aps字段`一看就知道是apple推送到設備之后的專屬識別字段,在該字段下的每個子字段分別有相應的作用和意義,再來就是`自定義字段`,在json中會和aps同一級別目錄下展示,這個是開發(fā)者自己知道的字段,在客戶端自行解析和提取使用。
>* 消息包裝代碼展示:
>```java
ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
//體現(xiàn)在aps的category字段
payloadBuilder.setCategoryName("category");
//體現(xiàn)在aps的content-available字段
payloadBuilder.setContentAvailable(true);
//彈出窗消息圖標,aps的alert字段的launch-image字段
payloadBuilder.setLaunchImageFileName("icon.icon");
//以下為兩種彈出窗的消息封裝模式
//彈出窗消息封裝1
payloadBuilder.setAlertBody("Example!");//aps的alert字段的body字段
payloadBuilder.setAlertSubtitle("AlertSubtitle");//aps的alert字段的title字段
payloadBuilder.setAlertTitle("AlertTitle");//aps的alert字段的subtitle字段
//彈出窗消息封裝2
payloadBuilder.setLocalizedActionButtonKey("LocalizedActionButtonKey");//aps的alert字段的action-loc-key字段
payloadBuilder.setLocalizedAlertMessage("LocalizedAlertMessage");//aps的alert字段的loc-key字段
payloadBuilder.setLocalizedAlertSubtitle("LocalizedAlertSubtitle");//aps的alert字段的subtitle-loc-key字段
payloadBuilder.setLocalizedAlertTitle("LocalizedAlertTitle");//aps的alert字段的title-loc-key字段
//消息通知聲音,aps的sound字段
payloadBuilder.setSoundFileName("sound.wav");
//aps的badge字段
payloadBuilder.setBadgeNumber(1);
//aps的mutable-content字段
payloadBuilder.setMutableContent(true);
//自定義鍵值對,其中value是Object,可以支持多層的json字串,這個根據(jù)業(yè)務需求而定
payloadBuilder.addCustomProperty("name","value");
//是否顯示動作按鈕,這個沒在json中體現(xiàn)啊,可能在header中體現(xiàn)吧,沒研究
payloadBuilder.setShowActionButton(true);
String payload = payloadBuilder.buildWithDefaultMaximumLength();
String token = TokenUtil.sanitizeTokenString("aa1e3286fcf87a68f9e8be642d9661c4a4537e34fe4abab68a9681ced773c18f");
System.out.println("payload = " + payload);
System.out.println("token = " + token);
SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, "cn.geili.KoudaiGouwu", payload);
System.out.println(pushNotification.toString());
- 其中彈出窗消息封裝有兩種,如下所示
- 彈出窗消息封裝1,json展示:
{
"aps": {
"category": "category",
"content-available": 1,
"alert": {
"body": "Example!",
"launch-image": "icon.icon",
"title": "AlertTitle",
"subtitle": "AlertSubtitle"
},
"sound": "sound.wav",
"badge": 1,
"mutable-content": 1
},
"name": "value"
}
>* 彈出窗消息封裝2,json展示:
>```javascript
{
"aps": {
"category": "category",
"content-available": 1,
"alert": {
"launch-image": "icon.icon",
"action-loc-key": "LocalizedActionButtonKey",
"loc-key": "LocalizedAlertMessage",
"subtitle-loc-key": "LocalizedAlertSubtitle",
"title-loc-key": "LocalizedAlertTitle"
},
"sound": "sound.wav",
"badge": 1,
"mutable-content": 1
},
"name": "value"
}
結語
教程完結,有了這個教程,差不多就可以在生產環(huán)境中部署新版的APNs推送服務了,你只需要將以上的教程代碼進行相應的封裝,根據(jù)業(yè)務場景對消息體json也進行相應的封裝,剩余的事情都交給這個Pushy框架即可,內部對消息隊列、失敗重傳等都進行了處理,不過為了更好地開發(fā)出高性能高并發(fā)的推送服務器,最好還是對內部原理深入理解,特別是Netty內部細節(jié)(這個可是Pushy底層的網(wǎng)絡支持和IO框架)。