Github address of Blog:Github.com/liuyan731/b…


Recently updated the project team’s old Apple IOS push. Looking at apple’s interface documentation, I find it difficult to write a stable and efficient interface directly, and to avoid repeating the wheel,…. After investigating some common open source libraries, I chose PusHY, which was developed and maintained by the Turo team.

APNs and Pushy

The Notification Push of Apple devices relies on the Apple Push Notification Service (APNs). The official introduction of APNs is as follows:

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.

All message push on IOS devices (tvOS, macOS) needs to go through APNs. APNs service is really powerful, with tens of billions of messages pushed every day, which is reliable, safe and efficient. Even WeChat and QQ this instant messaging app user levels in the process of the program is not started or the background is also need to use APNs (when the application starts, the use of building their own long connection), tencent only optimizes the whole lines in from their server to apple’s server, so that push to fast zhihu (reference).

The project team’s old Apple push service uses Apple’s previous APNs based on binary sockets, and also uses an open source library of JavapNS. It seems that the JavapNS effect is not very good, and some people have discussed on the Internet. Javapns has now stopped maintaining DEPRECATED. The authors suggest switching to a library based on Apple’s new APNs service.

Apple’s new APNs based on HTTP/2, through connection reuse, more efficient, of course, there are other aspects of optimization and improvement, you can refer to an APNs introduction, explain more clearly.

To mention the Pushy we use, here’s the official profile:

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 documents and instructions are very complete, the discussion is also very active, the author basically has the question must answer, most questions can be found the answer, the use is not difficult.

Use Pushy to push APNs messages

Add the package first

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

The identity authentication

Apple APNs provides two authentication modes: JWT-BASED token authentication and certificate-based authentication. Pushy also supports the two authentication modes. Here we use certificate authentication mode. You can check the documentation of Pushy about token authentication mode.

How to obtain an Apple APNs certificate of identity can be found in the official documentation.

Pushy use

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

Ps. The setClientCredentials function can also support passing in an InputStream and a certificate password.

You can also specify development or production using the setApnsServer function:

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

Pushy is based on Netty. With ApnsClientBuilder we can modify the number of connections for ApnsClient and the number of threads for EventLoopGroups as needed.

EventLoopGroup eventLoopGroup = new NioEventLoopGroup(4);
ApnsClient apnsClient = new ApnsClientBuilder()
        .setClientCredentials(new File("/path/to/certificate.p12"), "p12-file-password")
        .setConcurrentConnections(4).setEventLoopGroup(eventLoopGroup).build();
Copy the code

Do not set the number of threads in An EventLoopGroup to exceed the number of APNs connections.

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.

When Pushy sends a message, it returns a Netty Future object, which can be used to get the message sent.

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();
        }
    });
}
Copy the code

The APNs server can guarantee to send 1500 messages at the same time. When this limit is exceeded, Pushy will cache messages, so we don’t have to worry about sending too many messages by asynchronous operation (when we have a lot of messages, reaching hundreds of millions, we also have to do some control to avoid too much cache and insufficient memory, Pushy gives a solution using 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.

This is just the basic use of Pushy, but in our production environment it can be more complicated, we may need to know when all pushes are complete, we may need to count push success messages, we may need to prevent out of memory, and we may need to treat different send results differently…. Without further ado, go to the code.

Best practices

Referring to Pushy’s official best practices, we added the following:

  • Semaphore is used for flow control to prevent excessive cache and insufficient memory
  • CountDownLatch is used to mark the completion of the message delivery
  • Use AtomicLong to do the counting in the anonymous inner class operationComplete method
  • Netty’s Future object is used to judge the result of message push

Refer to the following code for specific usage:

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 + "Success" + (successCnt.get()) + "Totalcost =" + (endPushTime - startTime) + ", pushCost="+ (endPushTime - startPushTime)); }}Copy the code
  • About multi-threaded call client

Pushy ApnsClient is thread-safe and can be called using multiple threads

  • About creating multiple clients

Creating multiple clients can speed up the sending speed, but the improvement is not great. The author suggests:

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.

  • About APNs Response information (error message)

You can check the error code form on the official website (link) to understand the error and adjust it in time.

Pushy performance

The author said in the Google discussion group that Pushy push can reach 10K/s-20K /s with a single thread, as shown in the figure below:

Author’s suggestion about creating multiple clients and Pushy performance description

However, my test result is not so good because of network or other reasons. Please post the test result for your reference only (time MS) :

Ps. Since it is a test, there is not a large number of devices that can be used for group push test, so one device used to send multiple push instead. In this case, when a large number of pushes were sent to a device over a short period of time, APNs reported TooManyRequests errors, and Too many requests were made consecutively to the same device token. So there are a few messages that don’t go out.

Ps. The push time here does not include the initialization time of the client.

Ps. The message push time is related to the size of the message pushed. Here, I did not control the message variable during the test (I filled in all the messages randomly and they were all very short messages), so the data is for reference only.

  • ConcurrentConnections: 1, EventLoopGroup Thread: 1
Push 1 device Push 13 devices Push 100 for the same device Push 1000 for the same device
Average push success (PCS) 1 13 100 998
Average push time (ms) 222 500 654 3200
  • ConcurrentConnections: 5, EventLoopGroup Thread: 1
Push 1 device Push 13 devices Push 100 for the same device Push 1000 for the same device
Average push success (PCS) 1 13 100 999
Average push time (ms) 310 330 1600 1200
  • ConcurrentConnections: 4, EventLoopGroup Thread: 4
Push 1 device Push 13 devices Push 100 for the same device Push 1000 for the same device
Average push success (PCS) 1 13 100 999
Average push time (ms) 250 343 700 1700

For performance optimization, see Threads, Concurrent Connections, and Performance

You can also share the test data and discuss it together.

Today (December 11), I tested it again and pushed it to 3 devices, each of which repeatedly pushed 1000 pieces, a total of 3000 pieces. The results are as follows (time: ms) :

thread/connection No.1 No.2 No.3 No.4 No.5 No.6 No.7 No.8 No.9 No.10 Avg
1/1 12903 12782 10181 10393 11292 13608 11859.8
4/4 2861 3289 6258 5488 6649 6113 7042 5393 4591 7269 5495.3
20/20 1575 1456 1640 2761 2321 2154 1796 1634 2440 2114 1989.1
40/40 1535 2134 3312 2311 1553 2088 1734 1834 1530 1724 1975.5

At the same time, it was tested that 100000 messages were repeatedly pushed to these three devices, with a total time of 300,000 messages. The results are as follows (time is ms) :

thread/connection No.1
20/20 43547

thinking

Apple APNs is constantly being updated and optimized, consistently embracing new technologies (HTTP/2, JWT, etc.), and is a great service.

It is still a bit difficult to call the APNs service directly yourself to meet the requirements of the generated environment. Turo provides us with a great Java library: Pushy. Pushy also has some other features and uses (Metrics, proxy, Logging…) On the whole, it’s very good.

It also feels like we can tune with Pushy…


2017/12/07 done

This article is also synchronized toPersonal Github blog