场景
公司的产品主要是在APP端,和智能硬件交互,涉及到硬件告警、状态的推送,必须实时传递到用户手里,最开始我们选择集成的极光推送免费版,效果也能达到,但是4-1号的时候,极光服务器下午崩溃了, 导致服务不可用,生产环境的客户都炸锅了,于是我们打算自己造轮子,把 IOS
的换成原生的 APNS
调研
基于java集成APNS原生推送,自己研究了两天,通过百度,对比各个框架的优缺点,最后选择了 pushy 这个框架,下面是我调研的时候的几个框架,可能不是最好最全的,仅供参考
集成
因之前极光推送的维护都是APP同事在维护,选择了 pushy 做原生推送之后,发现需要 .p12 证书以及证书密码,百度了下.p12证书,让IOS 开发同事s生成之后发给我。 有个很坑的地方,因为我们工程里面,安卓还是用的极光推送,所以没有剔除掉极光推送的依赖包,导致导入 pushy 之后,运行的时候报错 ,查看错误日志发现是极光推送版本依赖的 的netty 版本为 4.1.6.Final 版本过低的原因,导致运行的时候 io.netty.resolver.dns.RoundRobinDnsAddressResolverGroup
这类参数不对,剔除掉极光推送依赖的netty即可
<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 原生推送的接入算是告一段落了,目前在生产环境稳定运行一个月,效果很好,没有出现任何问题。通过这次事情,长了一个教训,那就是任何第三方的服务不保证一定可靠,有的时候还是需要自己重复造轮子保险