这篇文章费了我好多心血啊,这都是在我测试了一堆失败的代码,看了大量的博客之后,把其中最有用,最精华的部分提取出来的集成,也是我艰辛的踩坑历程,满满的干货啊!可能是我太渣了,这些东西以前都没接触过,所以下面会有很多很基础的东西,大神切莫见怪。。。
HTTP/2
- HTTP/2扫盲:http://www.cnblogs.com/yingsmirk/p/5248506.html
- HTTP/2官网文档: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协议有了一些最初的了解,知道大体是怎么回事,其实和http1/1差不多,区别点主要是以下几点:
- 二进制分帧:每个请求分成多个帧进行传送,都送到之后再进行拼装
- 多路复用:二进制分帧之后的一个好处,多个请求共享同一个tcp连接,节约连接数量,提高连接利用率
- 服务器推送:推测客户端之后可能要的数据提前推送
- 头部压缩:两端各维护一个头部表,每次请求只传送头部不同的部分,减少传输冗余资源
- ALPN应用层协议协商:和http1/1的兼容协商机制,这个下面会有关于java的相关说明
- 支持异步编程,非阻塞,提高效率
- 看了这些算是了解一些大概吧,很多东西到了实际使用中再来慢慢体会
OkHttp学习和使用
这东西好久之前就用过,这次算是复习和提升以下,以前就是当个http客户端模拟使用,没处理过cookie、证书什么的,这次由于下面要做的事情的需要,就做了这些测试:
- okhttp教程:http://gold.xitu.io/entry/5728441d128fe1006058b6b9
- OkHttp使用进阶 译自OkHttp Github官方教程:http://www.cnblogs.com/ct2011/p/3997368.html
- OkHttp使用完全教程://www.greatytc.com/p/ca8a982a116b
用的很爽啊,链式编程、API设计易于理解、sample众多、直接搬砖。。。
APNs
github上找了很多项目来实验啊,最后还是Pushy这个最满意,也最实用,并且也由它又发现了新世界,开启新世界,新的征程开始~
先来点介绍文吧~ 这些介绍看完,相信你也对APNs新版的协议有了比较清楚的了解。
- APNs官网文档:https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1
- APNs简要介绍:基于HTTP2的全新APNs协议: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官网: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官网:http://netty.io/
- 书:《Netty权威指南》
你可能会觉得上面的废话好多,好多东西好像不需要了解,直接使用Pushy就行了呗,其实我也不敢说上面那些东西是否真的对下面的有用,但是知道这些原理,对后面发生的异常才能心中有数,至少在我的踩坑过程中也是深有体会的,其实上面那些东西不是我一开始就都看完的,是在我踩坑的过程中一步步补充的,每个人的只是学习顺序也许有所不同,你可以根据自己的情况合理安排~
我之所以会事先看这些东西,原因也是因为http/2、APNs、IOS推送、TLS等这些东西我真的不是很了解,会有一种恐惧之心,算是我自己的一个知识补充吧,所以对于对这些知识掌握很好的大神其实上面那些基础是完全可以不看的~
下面开始开发:
环境配置:
在Pushy的README.md中详细说明了Pushy所需的环境,我个人由于感激这篇文章在我踩坑过程中的作用,因此特别的把它翻译出来Pushy README.md中文翻译本。
因为Pushy本身依赖了其他类库,为了方便,也由于我是是使用Maven管理和构件项目,我下面的教程完全都在Maven下进行部署和开发,请悉知:
环境说明:
必须JDK7以上版本,这个Pushy README.md中文翻译本上面详细说明了。
- JDK8+Tomcat9 M11:部署成功
- JDK7+Tomcat7:部署成功
- JDK7+Tomcat9 M11:Tomcat启动失败,原因不明,我从Tomcat启动失败的报错中认为可能是Tomcat9 M11中调用了JDK8中特有的API,导致在JDK7中启动失败。
所以你部署环境的时候这个要注意,特别是部署到服务器当中的时候。
步骤:
- 添加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协议协商的实现依赖包,在Pushy README.md中文翻译本有详细说明的。到这一步,你的普通Java程序就能跑起来向APNs服务器发起一个推送了~~
- 添加alpn-boot依赖(Tomcat中所需):
这一步和上面的不太一样,因为jar包需要添加到bootclasspath中,和普通的classpath不太一样,在JVM启动参数添加如下:
-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,你可以理解为加载优先级更高的jar,但是这种方式加载的jar是和平台相关的,所以你下载的jar包要选择和你平台相匹配的才行哦~~,当然,这里的这个jar其实已经是linux、win、macOS全平台都具有的了,它会根据平台加载相应的那个组件。
- 加载这个jar的理由是,我们上面加载的netty-tcnative-boringssl-static这个依赖,和Tomcat内部的tcnative实现相冲突了,所以要用这个jar包要进行适配,具体的底层原理这里不研究,你只要记住,如果你使用Tomcat,就要加上步骤3的这个依赖,Jetty实测不需要这个依赖。
- 添加从Apple开发者平台申请到的app证书文件到项目资源目录下
- 开始编码:创建ApnsClient对象实例:根据证书和证书密码创建和APNs服务器的连接对象
ApnsClient apnsClient = new ApnsClientBuilder().setClientCredentials(new File("/path/to/p12-file"), "p12-file-password").build();
- 等待和APNs的连接成功(HTTP/2是异步的,但是这里连接没成功后续步骤无法继续,所以需要等待):
Future<Void> connectFuture = apnsClient.connect(ApnsClient.PRODUCTION_APNS_HOST);
connectFuture.await();
>链接地址有`DEVELOPMENT_APNS_HOST`和`PRODUCTION_APNS_HOST`两个,你要确认你拿到的证书是否支持开发者模式连接开发者服务器,我拿到的证书就是不支持的,需要直接连接正式服务器,这是我踩的坑。
>7. 封装推送消息内容体:
```java
ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
payloadBuilder.setAlertBody("alert-message-body");
ApnsPayloadBuilder这个类可以好好看看,就是这个类封装推送消息体,携带了APNs推送能发送的各个字段,比如显示按钮、通知消息标题、消息体、图片名、消息声音文件名等,还由于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规定的那些键值就行,而额外的,你也可以自定义地添加一些自己业务所需的其他键值方便客户端接收到推送消息之后进行处理。
>######智能截取4K长度:
```java
String payload = payloadBuilder.buildWithDefaultMaximumLength();
- 封装消息体:
SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, "com.example.AppName", payload);
>其中token是每个设备生成的token串经过如下代码加工后得到的,相当于是设备的唯一id:
>```java
String token = TokenUtil.sanitizeTokenString("<device token>");
"com.example.AppName":这个是你的证书签名,必须保证证书签名、证书、证书密码、产生token的app签名全部一致,不然就会报错。
你的整个推送消息体封装好之后,在网络http/2传输过程中的最终传输的数据格式如下,主要包括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. 发送消息推送:
>```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冲突,然后出现未知的错误。。。知道就行,具体和这两个gson的差别有关,我没了解。。。这种坑也只有我这种人品的能踩到。。。最好是两个够不添加,反正Pushy在Maven中就会自动依赖引入了,何必多此一举。
# 消息包装实战
我本人没接触过iOS开发,因此对这个消息体那些字段有些什么用不是搞得很清楚,只是大概知道图标、按钮显示、声音等意思,但是具体到iOS设备接收到之后会有什么样的消息体现,我不是很清楚,但是懂得的人看了下面我的测试样例,我相信也能很快包装出自己想要的消息~
##### 消息json中的`aps字段`一看就知道是apple推送到设备之后的专属识别字段,在该字段下的每个子字段分别有相应的作用和意义,再来就是`自定义字段`,在json中会和aps同一级别目录下展示,这个是开发者自己知道的字段,在客户端自行解析和提取使用。
>* 消息包装代码展示:
>```java
ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
//体现在aps的category字段
payloadBuilder.setCategoryName("category");
//体现在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字串,这个根据业务需求而定
payloadBuilder.addCustomProperty("name","value");
//是否显示动作按钮,这个没在json中体现啊,可能在header中体现吧,没研究
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"
}
结语
教程完结,有了这个教程,差不多就可以在生产环境中部署新版的APNs推送服务了,你只需要将以上的教程代码进行相应的封装,根据业务场景对消息体json也进行相应的封装,剩余的事情都交给这个Pushy框架即可,内部对消息队列、失败重传等都进行了处理,不过为了更好地开发出高性能高并发的推送服务器,最好还是对内部原理深入理解,特别是Netty内部细节(这个可是Pushy底层的网络支持和IO框架)。