這篇文章費(fèi)了我好多心血啊闭树,這都是在我測(cè)試了一堆失敗的代碼,看了大量的博客之后荒澡,把其中最有用报辱,最精華的部分提取出來(lái)的集成,也是我艱辛的踩坑歷程单山,滿滿的干貨鞍帧!可能是我太渣了米奸,這些東西以前都沒(méi)接觸過(guò)昼接,所以下面會(huì)有很多很基礎(chǔ)的東西,大神切莫見(jiàn)怪悴晰。慢睡。。
HTTP/2
- HTTP/2掃盲:http://www.cnblogs.com/yingsmirk/p/5248506.html
- HTTP/2官網(wǎng)文檔:https://http2.github.io/
- HTTP/2標(biāo)準(zhǔn)文檔中英文對(duì)照:https://github.com/fex-team/http2-spec
- Wireshark抓包教程:http://fangxin.blog.51cto.com/1125131/735178
- HTTP/2 大神匯總博客:https://imququ.com/post/http2-resource.html
- 看過(guò)這些之后铡溪,應(yīng)該對(duì)http/2協(xié)議有了一些最初的了解漂辐,知道大體是怎么回事,其實(shí)和http1/1差不多棕硫,區(qū)別點(diǎn)主要是以下幾點(diǎn):
- 二進(jìn)制分幀:每個(gè)請(qǐng)求分成多個(gè)幀進(jìn)行傳送髓涯,都送到之后再進(jìn)行拼裝
- 多路復(fù)用:二進(jìn)制分幀之后的一個(gè)好處,多個(gè)請(qǐng)求共享同一個(gè)tcp連接哈扮,節(jié)約連接數(shù)量纬纪,提高連接利用率
- 服務(wù)器推送:推測(cè)客戶(hù)端之后可能要的數(shù)據(jù)提前推送
- 頭部壓縮:兩端各維護(hù)一個(gè)頭部表蚓再,每次請(qǐng)求只傳送頭部不同的部分,減少傳輸冗余資源
- ALPN應(yīng)用層協(xié)議協(xié)商:和http1/1的兼容協(xié)商機(jī)制包各,這個(gè)下面會(huì)有關(guān)于java的相關(guān)說(shuō)明
- 支持異步編程对途,非阻塞,提高效率
- 看了這些算是了解一些大概吧髓棋,很多東西到了實(shí)際使用中再來(lái)慢慢體會(huì)
OkHttp學(xué)習(xí)和使用
這東西好久之前就用過(guò),這次算是復(fù)習(xí)和提升以下惶洲,以前就是當(dāng)個(gè)http客戶(hù)端模擬使用按声,沒(méi)處理過(guò)cookie、證書(shū)什么的恬吕,這次由于下面要做的事情的需要签则,就做了這些測(cè)試:
- okhttp教程:http://gold.xitu.io/entry/5728441d128fe1006058b6b9
- OkHttp使用進(jìn)階 譯自O(shè)kHttp Github官方教程:http://www.cnblogs.com/ct2011/p/3997368.html
- OkHttp使用完全教程:http://www.reibang.com/p/ca8a982a116b
用的很爽啊,鏈?zhǔn)骄幊填砹稀PI設(shè)計(jì)易于理解渐裂、sample眾多、直接搬磚钠惩。柒凉。。
APNs
github上找了很多項(xiàng)目來(lái)實(shí)驗(yàn)啊篓跛,最后還是Pushy這個(gè)最滿意膝捞,也最實(shí)用,并且也由它又發(fā)現(xiàn)了新世界愧沟,開(kāi)啟新世界蔬咬,新的征程開(kāi)始~
先來(lái)點(diǎn)介紹文吧~ 這些介紹看完,相信你也對(duì)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簡(jiǎn)要介紹:基于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神器來(lái)啦~下面的APNs調(diào)用都用這個(gè)項(xiàng)目來(lái)作為底層支撐林艘。
- Pushy開(kāi)源項(xiàng)目: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依賴(lài)下載: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/
- 書(shū):《Netty權(quán)威指南》
你可能會(huì)覺(jué)得上面的廢話好多,好多東西好像不需要了解混坞,直接使用Pushy就行了唄狐援,其實(shí)我也不敢說(shuō)上面那些東西是否真的對(duì)下面的有用,但是知道這些原理拔第,對(duì)后面發(fā)生的異常才能心中有數(shù)咕村,至少在我的踩坑過(guò)程中也是深有體會(huì)的,其實(shí)上面那些東西不是我一開(kāi)始就都看完的蚊俺,是在我踩坑的過(guò)程中一步步補(bǔ)充的懈涛,每個(gè)人的只是學(xué)習(xí)順序也許有所不同,你可以根據(jù)自己的情況合理安排~
我之所以會(huì)事先看這些東西泳猬,原因也是因?yàn)閔ttp/2批钠、APNs宇植、IOS推送、TLS等這些東西我真的不是很了解埋心,會(huì)有一種恐懼之心指郁,算是我自己的一個(gè)知識(shí)補(bǔ)充吧,所以對(duì)于對(duì)這些知識(shí)掌握很好的大神其實(shí)上面那些基礎(chǔ)是完全可以不看的~
下面開(kāi)始開(kāi)發(fā):
環(huán)境配置:
在Pushy的README.md中詳細(xì)說(shuō)明了Pushy所需的環(huán)境拷呆,我個(gè)人由于感激這篇文章在我踩坑過(guò)程中的作用闲坎,因此特別的把它翻譯出來(lái)Pushy README.md中文翻譯本。
因?yàn)镻ushy本身依賴(lài)了其他類(lèi)庫(kù)茬斧,為了方便腰懂,也由于我是是使用Maven管理和構(gòu)件項(xiàng)目,我下面的教程完全都在Maven下進(jìn)行部署和開(kāi)發(fā)项秉,請(qǐng)悉知:
環(huán)境說(shuō)明:
必須JDK7以上版本绣溜,這個(gè)Pushy README.md中文翻譯本上面詳細(xì)說(shuō)明了。
- JDK8+Tomcat9 M11:部署成功
- JDK7+Tomcat7:部署成功
- JDK7+Tomcat9 M11:Tomcat啟動(dòng)失敗娄蔼,原因不明怖喻,我從Tomcat啟動(dòng)失敗的報(bào)錯(cuò)中認(rèn)為可能是Tomcat9 M11中調(diào)用了JDK8中特有的API,導(dǎo)致在JDK7中啟動(dòng)失敗岁诉。
所以你部署環(huán)境的時(shí)候這個(gè)要注意锚沸,特別是部署到服務(wù)器當(dāng)中的時(shí)候。
步驟:
- 添加Pushy依賴(lài):
<dependency>
<groupId>com.relayrides</groupId>
<artifactId>pushy</artifactId>
<version>0.8.1</version>
</dependency>
>2. 添加native SSL provider依賴(lài)涕癣,注意版本哦:
>```xml
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>1.1.33.Fork22</version>
</dependency>
這個(gè)是ALPN協(xié)議協(xié)商的實(shí)現(xiàn)依賴(lài)包咒吐,在Pushy README.md中文翻譯本有詳細(xì)說(shuō)明的。到這一步属划,你的普通Java程序就能跑起來(lái)向APNs服務(wù)器發(fā)起一個(gè)推送了~~
- 添加alpn-boot依賴(lài)(Tomcat中所需):
這一步和上面的不太一樣恬叹,因?yàn)閖ar包需要添加到bootclasspath中,和普通的classpath不太一樣同眯,在JVM啟動(dòng)參數(shù)添加如下:
-Xbootclasspath/p:/Users/coselding/Downloads/alpn-boot-8.1.9.v20160720.jar
p:
后面的部分就是你下載下來(lái)的alpn-boot的jar包的本地地址绽昼,這個(gè)jar包的下載地址是這個(gè)http://mvnrepository.com/artifact/org.mortbay.jetty.alpn/alpn-boot
原因:
- 這種方式添加的jar包會(huì)替換JVM底層運(yùn)行的相關(guān)API,你可以理解為加載優(yōu)先級(jí)更高的jar须蜗,但是這種方式加載的jar是和平臺(tái)相關(guān)的硅确,所以你下載的jar包要選擇和你平臺(tái)相匹配的才行哦~~,當(dāng)然明肮,這里的這個(gè)jar其實(shí)已經(jīng)是linux菱农、win、macOS全平臺(tái)都具有的了柿估,它會(huì)根據(jù)平臺(tái)加載相應(yīng)的那個(gè)組件循未。
- 加載這個(gè)jar的理由是,我們上面加載的netty-tcnative-boringssl-static這個(gè)依賴(lài)秫舌,和Tomcat內(nèi)部的tcnative實(shí)現(xiàn)相沖突了的妖,所以要用這個(gè)jar包要進(jìn)行適配绣檬,具體的底層原理這里不研究,你只要記住嫂粟,如果你使用Tomcat娇未,就要加上步驟3的這個(gè)依賴(lài),Jetty實(shí)測(cè)不需要這個(gè)依賴(lài)星虹。
- 添加從Apple開(kāi)發(fā)者平臺(tái)申請(qǐng)到的app證書(shū)文件到項(xiàng)目資源目錄下
- 開(kāi)始編碼:創(chuàng)建ApnsClient對(duì)象實(shí)例:根據(jù)證書(shū)和證書(shū)密碼創(chuàng)建和APNs服務(wù)器的連接對(duì)象
ApnsClient apnsClient = new ApnsClientBuilder().setClientCredentials(new File("/path/to/p12-file"), "p12-file-password").build();
- 等待和APNs的連接成功(HTTP/2是異步的零抬,但是這里連接沒(méi)成功后續(xù)步驟無(wú)法繼續(xù),所以需要等待):
Future<Void> connectFuture = apnsClient.connect(ApnsClient.PRODUCTION_APNS_HOST);
connectFuture.await();
>鏈接地址有`DEVELOPMENT_APNS_HOST`和`PRODUCTION_APNS_HOST`兩個(gè)宽涌,你要確認(rèn)你拿到的證書(shū)是否支持開(kāi)發(fā)者模式連接開(kāi)發(fā)者服務(wù)器媚值,我拿到的證書(shū)就是不支持的,需要直接連接正式服務(wù)器护糖,這是我踩的坑。
>7. 封裝推送消息內(nèi)容體:
```java
ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
payloadBuilder.setAlertBody("alert-message-body");
ApnsPayloadBuilder這個(gè)類(lèi)可以好好看看嚼松,就是這個(gè)類(lèi)封裝推送消息體嫡良,攜帶了APNs推送能發(fā)送的各個(gè)字段,比如顯示按鈕献酗、通知消息標(biāo)題寝受、消息體、圖片名罕偎、消息聲音文件名等很澄,還由于APNs對(duì)每個(gè)消息最大長(zhǎng)度限制為4K,因此還對(duì)過(guò)長(zhǎng)的消息進(jìn)行了智能化地截取工作颜及。最后這這個(gè)類(lèi)會(huì)被序列化為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è)務(wù)所需的其他鍵值方便客戶(hù)端接收到推送消息之后進(jìn)行處理。
>######智能截取4K長(zhǎng)度:
```java
String payload = payloadBuilder.buildWithDefaultMaximumLength();
- 封裝消息體:
SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, "com.example.AppName", payload);
>其中token是每個(gè)設(shè)備生成的token串經(jīng)過(guò)如下代碼加工后得到的肄扎,相當(dāng)于是設(shè)備的唯一id:
>```java
String token = TokenUtil.sanitizeTokenString("<device token>");
"com.example.AppName":這個(gè)是你的證書(shū)簽名墨林,必須保證證書(shū)簽名、證書(shū)犯祠、證書(shū)密碼旭等、產(chǎn)生token的app簽名全部一致,不然就會(huì)報(bào)錯(cuò)衡载。
你的整個(gè)推送消息體封裝好之后搔耕,在網(wǎng)絡(luò)http/2傳輸過(guò)程中的最終傳輸?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);
這是一個(gè)異步阻塞方法痰娱,調(diào)用之后推送通知會(huì)放到內(nèi)部消息隊(duì)列度迂,等待APNs接收到消息并反饋的時(shí)候才能通過(guò)下面的方法得到響應(yīng)藤乙,否則下面這個(gè)方法就會(huì)阻塞著,上線產(chǎn)品建議寫(xiě)成異步回調(diào)的方式:
PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse = sendNotificationFuture.get();
>10. 接收APNs服務(wù)器響應(yīng):
>```java
pushNotificationResponse.isAccepted();
以下方法獲取APNs服務(wù)器拒絕消息的相關(guān)響應(yīng)信息:
pushNotificationResponse.getRejectionReason();//獲取拒絕原因
pushNotificationResponse.getTokenInvalidationTimestamp();//獲取token失效時(shí)間
>11. 連接斷開(kāi)重連:
>```java
apnsClient.getReconnectionFuture().await();
- 關(guān)閉連接惭墓,釋放資源:
Future<Void> disconnectFuture = apnsClient.disconnect();
disconnectFuture.await();
>* 踩坑記錄:Pushy項(xiàng)目中依賴(lài)了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就會(huì)和Pushy中的gson沖突,然后出現(xiàn)未知的錯(cuò)誤腊凶。划咐。。知道就行钧萍,具體和這兩個(gè)gson的差別有關(guān)褐缠,我沒(méi)了解。风瘦。队魏。這種坑也只有我這種人品的能踩到。万搔。胡桨。最好是兩個(gè)夠不添加,反正Pushy在Maven中就會(huì)自動(dòng)依賴(lài)引入了瞬雹,何必多此一舉昧谊。
# 消息包裝實(shí)戰(zhàn)
我本人沒(méi)接觸過(guò)iOS開(kāi)發(fā),因此對(duì)這個(gè)消息體那些字段有些什么用不是搞得很清楚酗捌,只是大概知道圖標(biāo)呢诬、按鈕顯示、聲音等意思胖缤,但是具體到iOS設(shè)備接收到之后會(huì)有什么樣的消息體現(xiàn)尚镰,我不是很清楚,但是懂得的人看了下面我的測(cè)試樣例哪廓,我相信也能很快包裝出自己想要的消息~
##### 消息json中的`aps字段`一看就知道是apple推送到設(shè)備之后的專(zhuān)屬識(shí)別字段钓猬,在該字段下的每個(gè)子字段分別有相應(yīng)的作用和意義,再來(lái)就是`自定義字段`撩独,在json中會(huì)和aps同一級(jí)別目錄下展示敞曹,這個(gè)是開(kāi)發(fā)者自己知道的字段,在客戶(hù)端自行解析和提取使用综膀。
>* 消息包裝代碼展示:
>```java
ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
//體現(xiàn)在aps的category字段
payloadBuilder.setCategoryName("category");
//體現(xiàn)在aps的content-available字段
payloadBuilder.setContentAvailable(true);
//彈出窗消息圖標(biāo)澳迫,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);
//自定義鍵值對(duì)剧劝,其中value是Object橄登,可以支持多層的json字串,這個(gè)根據(jù)業(yè)務(wù)需求而定
payloadBuilder.addCustomProperty("name","value");
//是否顯示動(dòng)作按鈕,這個(gè)沒(méi)在json中體現(xiàn)啊拢锹,可能在header中體現(xiàn)吧谣妻,沒(méi)研究
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"
}
結(jié)語(yǔ)
教程完結(jié),有了這個(gè)教程充坑,差不多就可以在生產(chǎn)環(huán)境中部署新版的APNs推送服務(wù)了减江,你只需要將以上的教程代碼進(jìn)行相應(yīng)的封裝,根據(jù)業(yè)務(wù)場(chǎng)景對(duì)消息體json也進(jìn)行相應(yīng)的封裝捻爷,剩余的事情都交給這個(gè)Pushy框架即可辈灼,內(nèi)部對(duì)消息隊(duì)列、失敗重傳等都進(jìn)行了處理也榄,不過(guò)為了更好地開(kāi)發(fā)出高性能高并發(fā)的推送服務(wù)器巡莹,最好還是對(duì)內(nèi)部原理深入理解,特別是Netty內(nèi)部細(xì)節(jié)(這個(gè)可是Pushy底層的網(wǎng)絡(luò)支持和IO框架)甜紫。