场景

公司的产品主要是在APP端,和智能硬件交互,涉及到硬件告警、状态的推送,必须实时传递到用户手里,最开始我们选择集成的极光推送免费版,效果也能达到,但是4-1号的时候,极光服务器下午崩溃了, 导致服务不可用,生产环境的客户都炸锅了,于是我们打算自己造轮子,把 IOS 的换成原生的 APNS

调研

基于java集成APNS原生推送,自己研究了两天,通过百度,对比各个框架的优缺点,最后选择了 pushy 这个框架,下面是我调研的时候的几个框架,可能不是最好最全的,仅供参考

  1. apns-http2
  2. pushy
  3. java-apns

集成

因之前极光推送的维护都是APP同事在维护,选择了 pushy 做原生推送之后,发现需要 .p12 证书以及证书密码,百度了下.p12证书,让IOS 开发同事s生成之后发给我。 有个很坑的地方,因为我们工程里面,安卓还是用的极光推送,所以没有剔除掉极光推送的依赖包,导致导入 pushy 之后,运行的时候报错 ,查看错误日志发现是极光推送版本依赖的 的netty 版本为 4.1.6.Final 版本过低的原因,导致运行的时候 io.netty.resolver.dns.RoundRobinDnsAddressResolverGroup 这类参数不对,剔除掉极光推送依赖的netty即可 记一次工作中接入原生APNS的经历-LMLPHP 记一次工作中接入原生APNS的经历-LMLPHP

 <dependencies>

        <dependency>
            <groupId>cn.jpush.api</groupId>
            <artifactId>jpush-client</artifactId>
            <version>3.3.4</version>
            <exclusions>
                <exclusion>
                    <groupId>io.netty</groupId>
                    <artifactId>netty-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


        <dependency>
            <groupId>com.eatthepath</groupId>
            <artifactId>pushy</artifactId>
            <version>0.13.11</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-tcnative-boringssl-static</artifactId>
            <version>2.0.26.Final</version>
            <scope>runtime</scope>
        </dependency>

    </dependencies>



推送核心代码

public class APNSConnect {
    private static final Logger logger = LoggerFactory.getLogger(APNSConnect.class);

    private static ApnsClient apnsClient = null;

    public static ApnsClient getAPNSConnect() {

        if (apnsClient == null) {
            try {
                //证书
                final String p12Password = "123456789a";
                InputStream certificate = APNSConnect.class.getResourceAsStream("/iosPush.p12");
                EventLoopGroup eventLoopGroup = new NioEventLoopGroup(4);
                apnsClient = new ApnsClientBuilder().setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST)
                        .setClientCredentials(certificate, p12Password)
                        .setConcurrentConnections(4).setEventLoopGroup(eventLoopGroup).build();

            } catch (Exception e) {
                logger.error("ios get pushy apns client failed!");
                e.printStackTrace();
            }
        }
        return apnsClient;

    }
}



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

    /**
     * Semaphore又称信号量,是操作系统中的一个概念,在Java并发编程中,信号量控制的是线程并发的数量。
     */
    private static final Semaphore semaphore = new Semaphore(10000);
    private static final String topic = "com.elzj.camera";

    /**
     * ios的推送
     *
     * @param deviceTokens     推送的唯一ID
     * @param alertTitle       推送的标题
     * @param alertBody        推送内容
     * @param contentAvailable true:表示的是产品发布推送服务 false:表示的是产品测试推送服务
     * @param customProperty   附加参数
     * @param badge            如果badge小于0,则不推送这个右上角的角标,主要用于消息盒子新增或者已读时,更新此状态
     */
    @SuppressWarnings("rawtypes")
    public static void push(final List<String> deviceTokens, String alertTitle, String alertBody, boolean contentAvailable, Map<String, Object> customProperty, int badge) {
        long startTime = System.currentTimeMillis();
        ApnsClient apnsClient = APNSConnect.getAPNSConnect();
        long total = deviceTokens.size();
        //每次完成一个任务(不一定需要线程走完),latch减1,直到所有的任务都完成,就可以执行下一阶段任务,可提高性能
        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();
            if (alertBody != null && alertTitle != null) {
                payloadBuilder.setAlertBody(alertBody);
                payloadBuilder.setAlertTitle(alertTitle);
            }
            //如果badge小于0,则不推送这个右上角的角标,主要用于消息盒子新增或者已读时,更新此状态
            if (badge > 0) {
                payloadBuilder.setBadgeNumber(badge);
            }

            //将所有的附加参数全部放进去
            if (customProperty != null) {
                for (Map.Entry<String, Object> map : customProperty.entrySet()) {
                    payloadBuilder.addCustomProperty(map.getKey(), map.getValue());
                }
            }
            // true:表示的是产品发布推送服务 false:表示的是产品测试推送服务
            payloadBuilder.setContentAvailable(contentAvailable);
            String payload = payloadBuilder.buildWithDefaultMaximumLength();
            final String token = TokenUtil.sanitizeTokenString(deviceToken);
            SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, topic, payload);
            try {
                //从信号量中获取一个允许机会
                semaphore.acquire();
            } catch (Exception e) {
                //线程太多了,没有多余的信号量可以获取了
                logger.error("ios push get semaphore failed, deviceToken:{}", deviceToken);
                e.printStackTrace();
            }

            final PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>> sendNotificationFuture = apnsClient.sendNotification(pushNotification);

            //---------------------------------------------------------------------------------------------------
            try {
                final PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse = sendNotificationFuture.get();
//                System.out.println(sendNotificationFuture.isSuccess());
//                System.out.println(pushNotificationResponse.isAccepted());

                sendNotificationFuture.addListener(new PushNotificationResponseListener<SimpleApnsPushNotification>() {

                    @Override
                    public void operationComplete(final PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>> future) throws Exception {
                        // When using a listener, callers should check for a failure to send a
                        // notification by checking whether the future itself was successful
                        // since an exception will not be thrown.
                        if (future.isSuccess()) {
                            final PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse =
                                    sendNotificationFuture.getNow();
                            if (pushNotificationResponse.isAccepted()) {
                                successCnt.incrementAndGet();
                            } else {
                                Date invalidTime = pushNotificationResponse.getTokenInvalidationTimestamp();
                                logger.error("Notification rejected by the APNs gateway: " + pushNotificationResponse.getRejectionReason());
                                if (invalidTime != null) {
                                    logger.error("\t…and the token is invalid as of " + pushNotificationResponse.getTokenInvalidationTimestamp());
                                }
                            }
                            // Handle the push notification response as before from here.
                        } else {
                            // Something went wrong when trying to send the notification to the
                            // APNs gateway. We can find the exception that caused the failure
                            // by getting future.cause().
                            future.cause().printStackTrace();
                        }
                        latch.countDown();
                        semaphore.release();//释放允许,将占有的信号量归还
                    }
                });
                //------------------------------------------------------------------

                if (pushNotificationResponse.isAccepted()) {
                } else {
                    logger.error("Notification rejected by the APNs gateway: " + pushNotificationResponse.getRejectionReason());

                    if (pushNotificationResponse.getTokenInvalidationTimestamp() != null) {
                        logger.error("\t…and the token is invalid as of " + pushNotificationResponse.getTokenInvalidationTimestamp());
                    }
                }
            } catch (final ExecutionException e) {
                logger.error(e.getMessage(), e);
            } catch (InterruptedException e) {
                logger.error(e.getMessage(), e);
            }
            //---------------------------------------------------------------------------------------------------

        }

        try {
            latch.await(20, TimeUnit.SECONDS);
        } catch (Exception 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));
    }

后记

ios 原生推送的接入算是告一段落了,目前在生产环境稳定运行一个月,效果很好,没有出现任何问题。通过这次事情,长了一个教训,那就是任何第三方的服务不保证一定可靠,有的时候还是需要自己重复造轮子保险

03-30 21:50