默认
打赏 发表评论 14
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
基于APNs最新HTTP/2接口实现iOS的高性能消息推送(服务端篇)
阅读(95617) | 评论(14 收藏3 淘帖1 3
微信扫一扫关注!

本文原作者:liuyan731,原文地址:liuyan731.github.io/2017/12/05/How-To-Use-APNs-Pushy,内容有改动。


1、前言


本文要分享的消息推送指的是当iOS端APP被关闭或者处于后台时,还能收到消息/信息/指令的能力。

这种在APP处于后台或关闭情况下的消息推送能力,通常在以下场景下非常有用:

  • 1)IM即时通讯聊天应用:聊天消息通知、音视频聊天呼叫等,典型代表有:微信、QQ、易信、米聊、钉钉、Whatsup、Line;
  • 2)新闻资讯应用:最新资讯通知等,典型代码有:网易新闻客户端、腾讯新闻客户端;
  • 3)SNS社交应用:转发/关注/赞等通知,典型代表有:微博、知乎;
  • 4)邮箱客户端:新邮件通知等,典型代表有:QQ邮箱客户端、Foxmail客户端、网易邮箱大师;
  • 5)金融支付应用:收款通知、转账通知等,典型代表有:支付宝、各大银行的手机银行等;
  •   .... ....

除了以上典型场景下,消息推送这种能力已经被越来越多的APP作为基础能力之一,因为移动互联网时代下,用户的“全时在线”能力非常诱人和强大,能随时随地即时地将各种重要信息推送给用户,无疑是非常有意义的。

众所周之,iOS端的这项消息推送能力就是使用苹果提供的APNs服务来实现(有些iOS小白开发者可能看到各种第3方的iOS端消息推送SDK,总会习惯性地认为这是完全由第3方提供的能力,实际上同样是使用APNs,只是封装了一下而已)。目前介绍APNs消息推送的文章多讨论的是手机端的实现,而服务端的消息要怎么“推”出来这样的文章,要么太老,要么只是介绍如何调用第3方的服务端SDK接口而已(如极光推广、友盟推送、腾讯信鸽推送等)。所以本文趁着最近对项目组的老苹果iOS推送进行升级修改机会,详细查阅了最新苹果的APNs接口文档,同时为了避免重复造轮子(懒),在调研了一些开源常用的库之后,选择了Turo团队开发和维护的pushy开源工程来实现在Java服务端调用苹果最新的APNs HTTP/2接口进行消息推送,并借此文对Pushy的使用方法进行了总结和记录,希望对你用。

补充说明:网上目前能查到的有关iOS端APNs消息推送的Java服务端代码实现,多是介绍如何使用Java-APNS这个工程,但这个工程以及类似的其它工程都很久没有维护了,跟最新的苹果APNs服务已经很难匹配了。相较而言puhsy这个工程一直比较活跃,也对苹果的最新APNs跟进的比较及时,因而本文作者在公司的项目进行升级和重构过程中,毫不犹豫的使用了pushy

2、相关文章


有关iOS客户端APNs消息推送技术的介绍文章:


有关消息推送技术服务端架构方面的文章:


3、提一下Android端的消息推送


论坛里做IM或消息推送服务的朋友都很清楚,相对于苹果为iOS包办好的APNs技术,Android上的消息推送技术乱七八糟、一塌糊涂,原因是国内的Android厂商将Android原生的GCM(现在叫FCM,跟iOS的APNs是类似的技术)进行了阉割,加上各厂商的省电策略、这全策略各不相同,导致为了实现IM和其它各种应用中的后台消息推送,不得不为了进程保活、网络保活搞出各种黑科技(当然,自从Android 6.0发布以后,谷歌为了打击这种不道德的行为,进行了越来越严格的限制,保活黑科技越来越难搞了)。

国内的厂商为了跟进新版本Android的GCM(现在叫FCM),也都在搞自已的消息推送通道:小米手机有小米推送、魅族手机有魅族推送、华为手机有华为推送等等,开发者在放弃保活黑科技以后,只能一家一家接入各厂商的推送通道,而这这又涉到同一厂商的手机版本、不同厂商通道的自动识别等,麻烦事乱到你无法想象,就连第3方推送服务也只能就范——一家一家接入(比如信鸽的《[资讯] 信鸽新版上线:号称Android首家统一推送服务)。

为了解决上述乱象,好消息是去年有政府背景的“统一推送联盟”成立了(详见《[资讯] 统一推送联盟在京成立:结束国内安卓生态混乱),广大Android开发者真是翘首以盼,但坏消息是好进展并不顺利(大家心知肚明啊,各厂商的利益不好均衡嘛),最近一次跟消息推送服务有关的活动还是3个月前的《[资讯] 统一推送联盟2018成员大会如期召开》。虽然进展不大,但总算还是有希望,Android同行们再等等,总有Android端消息推送一统江湖的方案出现的那天。

当然,本文主要是讨论iOS端的消息推送,本节文字只是写给Android端消息推送感兴趣的同行看的,更多Android消息推送技术的文章,请前往:http://www.52im.net/forum.php?mod=collection&action=view&ctid=11

4、说一说为什么不使用第3方推送服务SDK?


目前主流的iOS第3方推送SDK有:友盟推送、极光推送、信鸽推送等。

使用第3方推送的优点主要是:

  • 1)简单:开箱即用,无需关注技术细节;
  • 2)统计:提供了推送数据的统计能力等;
  • 3)性能:无需关注性能负载,因为第3方都帮你实现好了,你只要调用它的接口即可。

使用第3方推送的缺点也很明显:

  • 1)到达率:虽然第3方移动端消息推送产品都宣传到达率能够达到 90%及以上,但是实际使用起来,发现远远达不到;
  • 2)实时性:第3方移动端消息推送产品的推送通道是共用的,会面向多个推送客户,如果某一个客户PUSH推送量特别大,那么其他的移动端消息推送消息实时性可能就会受到影响;
  • 3)不可控:虽然各种技术细节无需你关注是个优点,但它也同时是个缺点——因为你不可控的东西太多了,想要做一个定制化的需求就力不从心了;
  • 4)被限流:因为第3方的推送服务多是免费提供,所以接口调用等都是有限制要求的(即使纸面上没有说出来),限流是一定要做的,不然这些成本谁抗的住?

针对以上问题,58同城团队在《58同城高性能移动端消息推送技术架构演进之路》也有讨论。

更为关键的是,如果是实现iOS的消息推送,苹果官方提供的APNs服务已经足够简单,如果不是为了项目赶进度或偷懒,自已来实现是更靠谱的选择,简单的事情没有必要复杂化,这也正是本文作者的选择。

好了,言归正传,继续聊回使用pushy实现iOS高性能推送这个话题。

5、APNs和Pushy


苹果设备的消息推送是依靠苹果的APNs(Apple Push Notification service)服务的,APNs的官方简介如下:

Apple Push Notification service (APNs) is the centerpiece of the remote notifications feature. It is a robust, secure, and highly efficient service for app developers to propagate information to iOS (and, indirectly, watchOS), tvOS, and macOS devices.


如果英文看起来不方便,可以看看《iOS的推送服务APNs详解:设计思路、技术原理及缺陷等

IOS设备(tvOS、macOS)上的所有消息推送都需要经过APNs,APNs服务确实非常厉害,每天需要推送上百亿的消息,可靠、安全、高效。就算是微信和QQ这种用户级别的即时通讯app在程序没有启动或者后台运行过程中也是需要使用APNs的(当程序启动时,使用自己建立的长连接),只不过腾讯优化了整条从他们服务器到苹果服务器的线路而已,所以觉得推送要快(参考知乎)。

项目组老的苹果推送服务使用的是苹果以前的基于二进制socket的APNs,同时使用的是一个javapns的开源库,这个javapns貌似效果不是很好,在网上也有人有过讨论。javapns现在也停止维护DEPRECATED掉了。作者建议转向基于苹果新APNs服务的库。

苹果新APNs基于HTTP/2,通过连接复用,更加高效,当然还有其它方面的优化和改善,可以参考APNs的一篇介绍,讲解的比较清楚。

再说一下我们使用的Pushy,官方简介如下:

Pushy is a Java library for sending APNs (iOS, macOS, and Safari) push notifications. It is written and maintained by the engineers at Turo......We believe that Pushy is already the best tool for sending APNs push notifications from Java applications, and we hope you'll help us make it even better via bug reports and pull requests.


Pushy的文档和说明很全,讨论也很活跃,作者基本有问必答,大部分疑问都可以找到答案,使用难度也不大。

6、在Java端使用Pushy进行APNs消息推送


6.1首先加入包


<dependency>
    <groupId>com.turo</groupId>
    <artifactId>pushy</artifactId>
    <version>0.11.1</version>
</dependency>

6.2身份认证


苹果APNs提供了两种认证的方式:基于JWT的身份信息token认证和基于证书的身份认证。Pushy也同样支持这两种认证方式,这里我们使用证书认证方式,关于token认证方式可以查看Pushy的文档。

如何获取苹果APNs身份认证证书可以查考官方文档

6.3Pushy使用


ApnsClient apnsClient = new ApnsClientBuilder()
    .setClientCredentials(new File("/path/to/certificate.p12"), "p12-file-password")
    .build();

ps:这里的setClientCredentials函数也可以支持传入一个InputStream和证书密码。

同时也可以通过setApnsServer函数来指定是开发环境还是生产环境:
ApnsClient apnsClient = new ApnsClientBuilder().setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST)
    .setClientCredentials(new File("/path/to/certificate.p12"), "p12-file-password")
    .build();

Pushy是基于Netty的,通过ApnsClientBuilder我们可以根据需要来修改ApnsClient的连接数和EventLoopGroups的线程数:
EventLoopGroup eventLoopGroup = new NioEventLoopGroup(4);
ApnsClient apnsClient = new ApnsClientBuilder()
        .setClientCredentials(new File("/path/to/certificate.p12"), "p12-file-password")
        .setConcurrentConnections(4).setEventLoopGroup(eventLoopGroup).build();

关于连接数和EventLoopGroup线程数官网有如下的说明,简单来说,不要配置EventLoopGroups的线程数超过APNs连接数:

Because connections are bound to a single event loop (which is bound to a single thread), it never makes sense to give an ApnsClient more threads in an event loop than concurrent connections. A client with an eight-thread EventLoopGroup that is configured to maintain only one connection will use one thread from the group, but the other seven will remain idle. Opening a large number of connections on a small number of threads will likely reduce overall efficiency by increasing competition for CPU time.


关于消息的推送,注意一定要使用异步操作,Pushy发送消息会返回一个Netty Future对象,通过它可以拿到消息发送的情况:
for (final ApnsPushNotification pushNotification : collectionOfPushNotifications) {
    final Future sendNotificationFuture = apnsClient.sendNotification(pushNotification);

    sendNotificationFuture.addListener(new GenericFutureListener<Future<PushNotificationResponse>>() {
        
        @Override
        public void operationComplete(final Future<PushNotificationResponse> future) throws Exception {
            // This will get called when the sever has replied and returns immediately
            final PushNotificationResponse response = future.getNow();
        }
    });
}

APNs服务器可以保证同时发送1500条消息,当超过这个限制时,Pushy会缓存消息,所以我们不必担心异步操作发送的消息过多

当我们的消息非常多,达到上亿时,我们也得做一些控制,避免缓存过大,内存不足,Pushy给出了使用Semaphore的解决方法:

The APNs server allows for (at the time of this writing) 1,500 notifications in flight at any time. If we hit that limit, Pushy will buffer notifications automatically behind the scenes and send them to the server as in-flight notifications are resolved.

In short, asynchronous operation allows Pushy to make the most of local resources (especially CPU time) by sending notifications as quickly as possible.


以上仅是Pushy的基本用法,在我们的生产环境中情况可能会更加复杂,我们可能需要知道什么时候所有推送都完成了,可能需要对推送成功消息进行计数,可能需要防止内存不足,也可能需要对不同的发送结果进行不同处理....

不多说,上代码(请看下节...)。

7、Pushy的最佳实践


参考Pushy的官方最佳实践,我们加入了如下操作:

  • 通过Semaphore来进行流控,防止缓存过大,内存不足;
  • 通过CountDownLatch来标记消息是否发送完成;
  • 使用AtomicLong完成匿名内部类operationComplete方法中的计数;
  • 使用Netty的Future对象进行消息推送结果的判断。

具体用法参考如下代码:
public class IOSPush {

    private static final Logger logger = LoggerFactory.getLogger(IOSPush.class);

    private static final ApnsClient apnsClient = null;

    private static final Semaphore semaphore = new Semaphore(10000);

    public void push(final List<String> deviceTokens, String alertTitle, String alertBody) {

        long startTime = System.currentTimeMillis();

        if (apnsClient == null) {
            try {
                EventLoopGroup eventLoopGroup = new NioEventLoopGroup(4);
                apnsClient = new ApnsClientBuilder().setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST)
                        .setClientCredentials(new File("/path/to/certificate.p12"), "p12-file-password")
                        .setConcurrentConnections(4).setEventLoopGroup(eventLoopGroup).build();
            } catch (Exception e) {
                logger.error("ios get pushy apns client failed!");
                e.printStackTrace();
            }
        }

        long total = deviceTokens.size();

        final CountDownLatch latch = new CountDownLatch(deviceTokens.size());

        final AtomicLong successCnt = new AtomicLong(0);

        long startPushTime =  System.currentTimeMillis();

        for (String deviceToken : deviceTokens) {
            ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
            payloadBuilder.setAlertBody(alertBody);
            payloadBuilder.setAlertTitle(alertTitle);
            
            String payload = payloadBuilder.buildWithDefaultMaximumLength();
            final String token = TokenUtil.sanitizeTokenString(deviceToken);
            SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, "com.example.myApp", payload);

            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                logger.error("ios push get semaphore failed, deviceToken:{}", deviceToken);
                e.printStackTrace();
            }
            final Future<PushNotificationResponse<SimpleApnsPushNotification>> future = apnsClient.sendNotification(pushNotification);

            future.addListener(new GenericFutureListener<Future<PushNotificationResponse>>() {
                @Override
                public void operationComplete(Future<PushNotificationResponse> pushNotificationResponseFuture) throws Exception {
                    if (future.isSuccess()) {
                        final PushNotificationResponse<SimpleApnsPushNotification> response = future.getNow();
                        if (response.isAccepted()) {
                            successCnt.incrementAndGet();
                        } else {
                            Date invalidTime = response.getTokenInvalidationTimestamp();
                            logger.error("Notification rejected by the APNs gateway: " + response.getRejectionReason());
                            if (invalidTime != null) {
                                logger.error("\t…and the token is invalid as of " + response.getTokenInvalidationTimestamp());
                            }
                        }
                    } else {
                        logger.error("send notification device token={} is failed {} ", token, future.cause().getMessage());
                    }
                    latch.countDown();
                    semaphore.release();
                }
            });
        }

        try {
            latch.await(20, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            logger.error("ios push latch await failed!");
            e.printStackTrace();
        }

        long endPushTime = System.currentTimeMillis();

        logger.info("test pushMessage success. [共推送" + total + "个][成功" + (successCnt.get()) + "个], 
            totalcost= " + (endPushTime - startTime) + ", pushCost=" + (endPushTime - startPushTime));
    }
}

关于多线程调用client:
Pushy ApnsClient是线程安全的,可以使用多线程来调用

关于创建多个client:
创建多个client是可以加快发送速度的,但是提升并不大,作者建议:

ApnsClient instances are designed to stick around for a long time. They're thread-safe and can be shared between many threads in a large application. We recommend creating a single client (per APNs certificate/key), then keeping that client around for the lifetime of your application.


关于APNs响应信息(错误信息):
可以查看APNs官网的error code表格,了解出错情况,及时调整。

8、来看看Pushy的性能


作者在Google讨论组中说Pushy推送可以单核单线程达到10k/s-20k/s,如下图所示:
基于APNs最新HTTP/2接口实现iOS的高性能消息推送(服务端篇)_1.jpg
▲ 作者关于创建多client的建议及Pushy性能描述

但是可能是网络或其他原因,我的测试结果没有这么好,把测试结果贴出来,仅供参考(时间ms)。

ps:由于是测试,没有大量的设备可以用于群发推送测试,所以以往一个设备发送多条推送替代。这里短时间往一个设备发送大量的推送,APNs会报TooManyRequests错误,Too many requests were made consecutively to the same device token。所以会有少量消息无法发出。

ps:这里的推送时间,没有加上client初始化的时间。

ps:消息推送时间与被推消息的大小有关系,这里我在测试时没有控制消息变量(都是我瞎填的,都是很短的消息)所以数据仅供参考。

基于APNs最新HTTP/2接口实现iOS的高性能消息推送(服务端篇)_2.38.jpg

关于Pushy性能优化也可以看看官网作者的建议:Threads, concurrent connections, and performance

大家有测试的数据也可以分享出来一起讨论一下。

基于APNs最新HTTP/2接口实现iOS的高性能消息推送(服务端篇)_3.jpg

8、思考和小结


苹果APNs一直在更新优化,一直在拥抱新技术(HTTP/2,JWT等),是一个非常了不起的服务。

自己来直接调用APNs服务来达到生成环境要求还是有点困难。Turo给我们提供了一个很好的Java库:Pushy。Pushy还有一些其他的功能与用法(Metrics、proxy、Logging...),总体来说还是非常不错的。

同时感觉我们使用Pushy还可以调优...

(原文链接:https://liuyan731.github.io/

附录:更多消息推送技术文章


[1] 有关IM/推送技术原理和服务端架构等:
iOS的推送服务APNs详解:设计思路、技术原理及缺陷等
信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑
Android端消息推送总结:实现原理、心跳保活、遇到的问题等
扫盲贴:认识MQTT通信协议
一个基于MQTT通信协议的完整Android推送Demo
IBM技术经理访谈:MQTT协议的制定历程、发展现状等
求教android消息推送:GCM、XMPP、MQTT三种方案的优劣
移动端实时消息推送技术浅析
扫盲贴:浅谈iOS和Android后台实时消息推送的原理和区别
绝对干货:基于Netty实现海量接入的推送服务技术要点
移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)
为何微信、QQ这样的IM工具不使用GCM服务推送消息?
极光推送系统大规模高并发架构的技术实践分享
从HTTP到MQTT:一个基于位置服务的APP数据通信实践概述
魅族2500万长连接的实时消息推送架构的技术实践分享
专访魅族架构师:海量长连接的实时消息推送系统的心得体会
深入的聊聊Android消息推送这件小事
基于WebSocket实现Hybrid移动应用的消息推送实践(含代码示例)
一个基于长连接的安全可扩展的订阅/推送服务实现思路
实践分享:如何构建一套高可用的移动端消息推送系统?
Go语言构建千万级在线的高并发消息推送系统实践(来自360公司)
腾讯信鸽技术分享:百亿级实时消息推送的实战经验
百万在线的美拍直播弹幕系统的实时推送技术实践之路
京东京麦商家开放平台的消息推送架构演进之路
了解iOS消息推送一文就够:史上最全iOS Push技术详解
基于APNs最新HTTP/2接口实现iOS的高性能消息推送(服务端篇)
>> 更多同类文章 ……

[2] 有关IM/消息推送的通信格式、协议的选择等:
Protobuf通信协议详解:代码演示、详细原理介绍等
一个基于Protocol Buffer的Java代码演示
简述传输层协议TCP和UDP的区别
为什么QQ用的是UDP协议而不是TCP协议?
移动端即时通讯协议选择:UDP还是TCP?
如何选择即时通讯应用的数据传输格式
强列建议将Protobuf作为你的即时通讯应用数据传输格式
全方位评测:Protobuf性能到底有没有比JSON快5倍?
移动端IM开发需要面对的技术问题(含通信协议选择)
简述移动端IM开发的那些坑:架构设计、通信协议和客户端
理论联系实际:一套典型的IM通信协议设计详解
58到家实时消息系统的协议设计等技术实践分享
详解如何在NodeJS中使用Google的Protobuf
技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解
金蝶随手记团队分享:还在用JSON? Protobuf让数据传输更省更快(原理篇)
金蝶随手记团队分享:还在用JSON? Protobuf让数据传输更省更快(实战篇)
>> 更多同类文章 ……

[3] 有关Android端IM/消息推送的心跳保活处理等:
应用保活终极总结(一):Android6.0以下的双进程守护保活实践
应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)
应用保活终极总结(三):Android6.0及以上的保活实践(被杀复活篇)
Android进程保活详解:一篇文章解决你的所有疑问
Android端消息推送总结:实现原理、心跳保活、遇到的问题等
深入的聊聊Android消息推送这件小事
为何基于TCP协议的移动端IM仍然需要心跳保活机制?
微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)
微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)
移动端IM实践:实现Android版微信的智能心跳机制
移动端IM实践:WhatsApp、Line、微信的心跳策略分析
>> 更多同类文章 ……

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

上一篇:了解iOS消息推送一文就够:史上最全iOS Push技术详解下一篇:Android P正式版即将到来:后台应用保活、消息推送的真正噩梦

本帖已收录至以下技术专辑

推荐方案
评论 14
好文,已转给我的后端同事了
签名: 不想上班,啦啦啦
引用:大马仕格 发表于 2018-08-10 11:00
好文,已转给我的后端同事了

Pushy是好东西,必须收藏
hi:
    使用main方法测试代码出现Stream closed before a reply was received。证书,密码和签名无问题,有时间烦请指点。
引用:liming1990 发表于 2019-02-15 10:35
hi:
    使用main方法测试代码出现Stream closed before a reply was received。证书,密码和签名无问题 ...

我的RainbowChat的ios推送就是基于文章里的Pushy代码实现的,没有遇到过你说的问题,运行的很好。你google上查查看看。或者,你把完整的log抓出来我帮你也看看
请问你在实际使用中的耗时是多久
我的10秒+ 主要耗时在这两段日志
10:43:06.060 [nioEventLoopGroup-2-3] DEBUG i.netty.resolver.dns.DnsQueryContext - [[id: 0xd3ea315f], 4588, /10.200.46.108:53, DefaultDnsQuestion(api.push.apple.com. IN AAAA)] WRITE: [[[id: 0xd3ea315f], 4588, /10.200.46.108:53, DefaultDnsQuestion(api.push.apple.com. IN AAAA)]: {}], {}
10:43:11.063 [nioEventLoopGroup-2-3] DEBUG i.netty.resolver.dns.DnsQueryContext - [[id: 0xd3ea315f], 51948, /10.200.41.54:53, DefaultDnsQuestion(api.push.apple.com. IN A)] WRITE: [[[id: 0xd3ea315f], 51948, /10.200.41.54:53, DefaultDnsQuestion(api.push.apple.com. IN A)]: {}], {}

10:43:12.353 [nioEventLoopGroup-2-3] DEBUG i.netty.resolver.dns.DnsQueryContext - [[id: 0xd3ea315f], 14785, /10.200.46.108:53, DefaultDnsQuestion(api.push-apple.com.akadns.net. IN A)] WRITE: [[[id: 0xd3ea315f], 14785, /10.200.46.108:53, DefaultDnsQuestion(api.push-apple.com.akadns.net. IN A)]: {}], {}
10:43:16.237 [nioEventLoopGroup-2-3] DEBUG i.netty.resolver.dns.DnsQueryContext - [[id: 0xd3ea315f], 30706, /10.200.41.54:53, DefaultDnsQuestion(api.push-apple.com.akadns.net. IN AAAA)] WRITE: [[[id: 0xd3ea315f], 30706, /10.200.41.54:53, DefaultDnsQuestion(api.push-apple.com.akadns.net. IN AAAA)]: {}], {}

totalcost= 14383ms, pushCost=13195ms
引用:liming1990 发表于 2019-02-16 11:11
请问你在实际使用中的耗时是多久
我的10秒+ 主要耗时在这两段日志
10:43:06.060 [nioEventLoopGroup-2-3] ...

10秒太夸张了,看你这异常像是dns解析有问题。

你完全可以先用独立的Push工具验证一下你的证书、网络是不是能正常完推送,你可以在这篇《了解iOS消息推送一文就够:史上最全iOS Push技术详解》的第9章节找到这个工具!
你好。
       我看了你的【基于Apns最新HTTP/2接口实现ios的高性能消息推送(服务端)篇】的文章,我按照你文章中给的例子,再main方法 中测试代码,出现了Stream closed before a reply was received。麻烦指导一下,万分感谢。
       测试工程代码地址:
               链接:https://pan.baidu.com/s/1CpVx0ojOYW2aQtOwH6tWmg
              提取码:qjse
      错误日志如下:
      [id: 0x55441210] RECEIVED: [12642: /172.16.1.251:53], DatagramDnsResponse(from: /172.16.1.251:53, to: /0:0:0:0:0:0:0:0:53271, 12642, QUERY(0), NoError(0), RD RA)
        DefaultDnsQuestion(api.sandbox.push-apple.com.akadns.net. IN AAAA)
        DefaultDnsRawRecord(akadns.net. 180 IN SOA 54B)
        DefaultDnsRawRecord(OPT flags:0 udp:4000 0B)
[DEBUG] 2019-05-17 13:25:30,679 method:io.netty.handler.ssl.SslHandler.setHandshakeSuccess(SslHandler.java:1746)
[id: 0x05db604f, L:/172.16.2.32:55833 - R:api.development.push.apple.com/17.188.166.29:443] HANDSHAKEN: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
Failed to send push notification.
java.util.concurrent.ExecutionException: java.io.IOException: Stream closed before a reply was received
        at io.netty.util.concurrent.AbstractFuture.get(AbstractFuture.java:41)
        at com.test.IOSPush4.push(IOSPush4.java:103)
        at com.test.MainTest.main(MainTest.java:11)
Caused by: java.io.IOException: Stream closed before a reply was received
        at com.turo.pushy.apns.ApnsClientHandler.<clinit>(ApnsClientHandler.java:79)
        at com.turo.pushy.apns.ApnsClientHandler$ApnsClientHandlerBuilder.build(ApnsClientHandler.java:135)
        at com.turo.pushy.apns.ApnsClientHandler$ApnsClientHandlerBuilder.build(ApnsClientHandler.java:88)
        at io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder.buildFromCodec(AbstractHttp2ConnectionHandlerBuilder.java:420)
        at io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder.buildFromConnection(AbstractHttp2ConnectionHandlerBuilder.java:413)
        at io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder.build(AbstractHttp2ConnectionHandlerBuilder.java:381)
        at com.turo.pushy.apns.ApnsClientHandler$ApnsClientHandlerBuilder.build(ApnsClientHandler.java:142)
        at com.turo.pushy.apns.ApnsChannelFactory$1$1.operationComplete(ApnsChannelFactory.java:149)
        at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:502)
        at io.netty.util.concurrent.DefaultPromise.notifyListeners0(DefaultPromise.java:495)
        at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:474)
        at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:415)
        at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:540)
        at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:529)
        at io.netty.util.concurrent.DefaultPromise.trySuccess(DefaultPromise.java:101)
        at io.netty.handler.ssl.SslHandler.setHandshakeSuccess(SslHandler.java:1743)
        at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1412)
        at io.netty.handler.ssl.SslHandler.decodeNonJdkCompatible(SslHandler.java:1239)
        at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1276)
        at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502)
        at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:441)
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1408)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930)
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:682)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:617)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:534)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496)
        at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:906)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.lang.Thread.run(Unknown Source)
[INFO ] 2019-05-17 13:25:31,112 method:com.test.IOSPush4.push(IOSPush4.java:169)
test pushMessage success. [共推送1个][成功0个],totalcost= 11921, pushCost=11016
引用:peng-aihua 发表于 2019-05-17 17:32
你好。
       我看了你的【基于Apns最新HTTP/2接口实现ios的高性能消息推送(服务端)篇】的文章,我按照 ...

RainbowChat里一直用的是这个库,并没有遇到过你说的情况。

你这个问题是持续出现,还是偶尔出现?
引用:JackJiang 发表于 2019-05-17 17:47
RainbowChat里一直用的是这个库,并没有遇到过你说的情况。

你这个问题是持续出现,还是偶尔出现?

是一直出现 。测试代码工程如下:      
     链接:https://pan.baidu.com/s/1CpVx0ojOYW2aQtOwH6tWmg
     提取码:qjse
能抽空帮我看看是哪里的问题吗?感谢!!
引用:peng-aihua 发表于 2019-05-17 17:52
是一直出现 。测试代码工程如下:      
     链接:https://pan.baidu.com/s/1CpVx0ojOYW2aQtOwH6tWmg ...

你有没有用ios的Push工具验证你的APNS证书这些有没有问题
引用:JackJiang 发表于 2019-05-17 17:54
你有没有用ios的Push工具验证你的APNS证书这些有没有问题

验证了,证书没有问题
引用:peng-aihua 发表于 2019-05-17 18:31
验证了,证书没有问题

Pushy这个库,因为需要跟苹果的APNs服务进行TLS握手,它需要的库你都找对了吗?

比如,RainbowChat中跟Pushy相关的库列表如下:
【关于向iOS设备进行APNs推送相关jar的依赖和说明】:
Pushy依赖的Jar包如下:
(5)pushy-0.13.3.jar:pushy主jar包;
(6)netty-all-4.1.28.Final.jar
(7)gson 2.6
(8)slf4j 1.7
(9)netty-tcnative-boringssl-static-2.0.12.Final.jar:此包的作用是实现与苹果APNS的SSL长连接,官方地址:http://netty.io/wiki/forked-tomcat-native.html
(10)fast-uuid-0.1.jar:此包作为pushy依赖的高性能生成uuid的工具类,github:https://github.com/jchambers/fast-uuid

你参考一下。
引用:JackJiang 发表于 2019-05-17 18:59
Pushy这个库,因为需要跟苹果的APNs服务进行TLS握手,它需要的库你都找对了吗?

比如,RainbowChat中 ...

非常感谢,已解决。就是少了fast-uuid-0.1.jar这个包。
引用:peng-aihua 发表于 2019-05-20 17:09
非常感谢,已解决。就是少了fast-uuid-0.1.jar这个包。

解决了就好。看来你是没有仔细读一遍官方的文档。。。
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部