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.
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.
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.
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();
}
});
}
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.
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.
ps:由于是测试,没有大量的设备可以用于群发推送测试,所以以往一个设备发送多条推送替代。这里短时间往一个设备发送大量的推送,APNs会报TooManyRequests错误,Too many requests were made consecutively to the same device token。所以会有少量消息无法发出。
你好。
我看了你的【基于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