APNs入门学习和使用

这篇文章费了我好多心血啊,这都是在我测试了一堆失败的代码,看了大量的博客之后,把其中最有用,最精华的部分提取出来的集成,也是我艰辛的踩坑历程,满满的干货啊!可能是我太渣了,这些东西以前都没接触过,所以下面会有很多很基础的东西,大神切莫见怪。。。

HTTP/2

  • 看过这些之后,应该对http/2协议有了一些最初的了解,知道大体是怎么回事,其实和http1/1差不多,区别点主要是以下几点:
  1. 二进制分帧:每个请求分成多个帧进行传送,都送到之后再进行拼装
  2. 多路复用:二进制分帧之后的一个好处,多个请求共享同一个tcp连接,节约连接数量,提高连接利用率
  3. 服务器推送:推测客户端之后可能要的数据提前推送
  4. 头部压缩:两端各维护一个头部表,每次请求只传送头部不同的部分,减少传输冗余资源
  5. ALPN应用层协议协商:和http1/1的兼容协商机制,这个下面会有关于java的相关说明
  6. 支持异步编程,非阻塞,提高效率
  • 看了这些算是了解一些大概吧,很多东西到了实际使用中再来慢慢体会

OkHttp学习和使用

这东西好久之前就用过,这次算是复习和提升以下,以前就是当个http客户端模拟使用,没处理过cookie、证书什么的,这次由于下面要做的事情的需要,就做了这些测试:

用的很爽啊,链式编程、API设计易于理解、sample众多、直接搬砖。。。

APNs

github上找了很多项目来实验啊,最后还是Pushy这个最满意,也最实用,并且也由它又发现了新世界,开启新世界,新的征程开始~
先来点介绍文吧~ 这些介绍看完,相信你也对APNs新版的协议有了比较清楚的了解。

Pushy神器来啦~下面的APNs调用都用这个项目来作为底层支撑。

额外收获

你可能会觉得上面的废话好多,好多东西好像不需要了解,直接使用Pushy就行了呗,其实我也不敢说上面那些东西是否真的对下面的有用,但是知道这些原理,对后面发生的异常才能心中有数,至少在我的踩坑过程中也是深有体会的,其实上面那些东西不是我一开始就都看完的,是在我踩坑的过程中一步步补充的,每个人的只是学习顺序也许有所不同,你可以根据自己的情况合理安排~
我之所以会事先看这些东西,原因也是因为http/2、APNs、IOS推送、TLS等这些东西我真的不是很了解,会有一种恐惧之心,算是我自己的一个知识补充吧,所以对于对这些知识掌握很好的大神其实上面那些基础是完全可以不看的~
下面开始开发:

环境配置:

在Pushy的README.md中详细说明了Pushy所需的环境,我个人由于感激这篇文章在我踩坑过程中的作用,因此特别的把它翻译出来Pushy README.md中文翻译本
因为Pushy本身依赖了其他类库,为了方便,也由于我是是使用Maven管理和构件项目,我下面的教程完全都在Maven下进行部署和开发,请悉知:

环境说明:

必须JDK7以上版本,这个Pushy README.md中文翻译本上面详细说明了。

  1. JDK8+Tomcat9 M11:部署成功
  2. JDK7+Tomcat7:部署成功
  3. JDK7+Tomcat9 M11:Tomcat启动失败,原因不明,我从Tomcat启动失败的报错中认为可能是Tomcat9 M11中调用了JDK8中特有的API,导致在JDK7中启动失败。

所以你部署环境的时候这个要注意,特别是部署到服务器当中的时候。

步骤:

  1. 添加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服务器发起一个推送了~~

  1. 添加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
    原因:
  1. 这种方式添加的jar包会替换JVM底层运行的相关API,你可以理解为加载优先级更高的jar,但是这种方式加载的jar是和平台相关的,所以你下载的jar包要选择和你平台相匹配的才行哦~~,当然,这里的这个jar其实已经是linux、win、macOS全平台都具有的了,它会根据平台加载相应的那个组件。
  2. 加载这个jar的理由是,我们上面加载的netty-tcnative-boringssl-static这个依赖,和Tomcat内部的tcnative实现相冲突了,所以要用这个jar包要进行适配,具体的底层原理这里不研究,你只要记住,如果你使用Tomcat,就要加上步骤3的这个依赖,Jetty实测不需要这个依赖。
  1. 添加从Apple开发者平台申请到的app证书文件到项目资源目录下
  1. 开始编码:创建ApnsClient对象实例:根据证书和证书密码创建和APNs服务器的连接对象
ApnsClient apnsClient = new ApnsClientBuilder().setClientCredentials(new File("/path/to/p12-file"), "p12-file-password").build();
  1. 等待和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();
  1. 封装消息体:

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();
  1. 关闭连接,释放资源:

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框架)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容

  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,677评论 0 15
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,601评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,524评论 25 707
  • 本文的大部分内容是对苹果关于APNs官方文档的翻译以及整理。 一、设备token和消息的生命周期 关于设备toke...
    EA88阅读 17,444评论 14 44
  • 洗澡时冒出个想法,写本书。 内容写什么呢,在没写之前,我脑子里想的是源源不断的收入。好像不管写什么,都能产生巨大的...
    时汝佳阅读 404评论 0 0