消息推送之APNS

原理

APNS 是Apple Push Notification Service(Apple Push服务器)的缩写,是苹果的服务器。

APNS推送可以分为三个阶段:

第一阶段:推送服务器应用程序把要发送的消息、目的iPhone的标识打包,发给APNS。

第二阶段:APNS在自身的已注册推送服务的iPhone列表中,查找有相应标识的iPhone,并把消息发到iPhone。

第三阶段:iPhone把发来的消息传递给相应的应用程序,并且按照设定弹出推送通知。

详细流程如下:

  1. 首先是应用程序注册消息推送服务。

  2. APNS向应用程序返回deviceToken。

  3. 5应用程序将deviceToken发送给推送服务端程序。

  4. 服务端程序向APNS服务发送消息。

  5. APNS服务将消息发送给iPhone应用程序。

证书生成

网上有很多关于证书生成的详细步骤,这里不再说明了。

最终生成的证书共包含下面四个

  1. pushNotification.certSigningRequest

  2. aps_development.cer(下载生成的支持推送服务的证书。)

  3. pushNotificationDevprofile.mobileprovision

  4. pushNotification.p12

下面直接上代码。

客户端

1、应用程序注册消息推送服务

在AppDelegate.m的(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中加入注册消息通知推送服务。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  //判断是否由远程消息通知触发应用程序启动
    if ([launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]!=nil) {
        NSLog(@"远程消息通知触发应用程序启动");
    }
    //消息推送注册
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:UIRemoteNotificationTypeSound|UIRemoteNotificationTypeAlert|UIRemoteNotificationTypeBadge];
}

2、接收deviceToken的方法

在项目的AppDelegate.m中加入以下两个代理方法

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSString *token = [NSString stringWithFormat:@"%@", deviceToken];
    //获取终端设备标识,标识获取后需要将其发送到服务器端,服务器端推送消息到APNS时需要知道终端的标识,APNS通过注册的终端标识找到终端设备。
    NSLog(@"My token is:%@", token);
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    NSString *error_str = [NSString stringWithFormat: @"%@", error];
    NSLog(@"Failed to get token, error:%@", error_str);
}

3、接收消息的处理方法

在项目AppDelegate.m中加入消息接收处理代理方法。

- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    //在此处理接收到的消息。
    NSLog(@"Receive remote notification : %@",userInfo);
}

至此,IOS端的代码已经码完了。(^_^)

服务器

服务器端可以用php、Java、.net等语言实现。本文使用Java语言实现

1、导入jar包

工程建好之后,将JavaPNS_2.2.jar、javapns-jdk16-163.jar、bcprov-jdk16-145.jar这几个jar包拷贝到工程的lib目录下。

2、生成服务器端所用的.p12文件(.net或Java等后台)

在mac终端下执行以下指令:

(1)、将aps_development.cer转换成aps_development.pem格式

$ openssl x509 -in aps_development.cer -inform DER -out aps_development.pem -outform PEM

(2)、将p12格式的私钥转换成pem

$ openssl pkcs12 -nocerts -out Push_Noenc.pem -in pushNotification.p12

(3)、创建p12文件

$ openssl pkcs12 -export -in aps_development.pem -inkey Push_Noenc.pem -certfile pushNotification.certSigningRequest -name "aps_development" -out aps_development.p12

这样我们就得到了在.net或java等后台应用程序中使用的证书文件:aps_development.p12

3、编写服务器端代码

package com.push.server;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import javapns.devices.Device;
import javapns.devices.implementations.basic.BasicDevice;
import javapns.notification.AppleNotificationServerBasicImpl;
import javapns.notification.PushNotificationManager;
import javapns.notification.PushNotificationPayload;
import javapns.notification.PushedNotification;

public class SendToAPNS {
    // 从客户端获取的deviceToken,在此为了测试,设一个固定值
    private static final String DEVICE_TOKEN =

"13b11050a3fc064b3692e25c0fbd3b774b39ecb0c55a51ff4fb1373e004577a0";
    List<String> deviceToken = new ArrayList<String>();

    public void send () {
        // 证书文件(.p12)在服务器端的目录
        String filePath = null;
        try {
            String path = this.getClass().getClassLoader().getResource("/").getPath();
            filePath = java.net.URLDecoder.decode(path,"utf-8");
        } catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("filePath=" + filePath);
        String certificatePath = (filePath + "conf/ios_development.p12");
        // 获取ios_development.p12的密码
        Properties prop = new Properties();
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(filePath + "conf/pushmessage.properties");

        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        try {
            prop.load(fis);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        String certificatePassword = prop.getProperty("password");

        // 构建发送的消息
        String message="{'aps':{'alert':'this is a push message'}}";

        // 设别标识
        deviceToken.add(DEVICE_TOKEN);
        // 发送消息
        sendpush(deviceToken, certificatePath, certificatePassword, message, 4, false);
    }

     /************************************************
     测试用URL gateway.sandbox.push.apple.com /2195
     正式发布用URL gateway.push.apple.com / 2195
    javaPNS_2.2.jar必须
     ***************************************************/
    /**
   * @param tokens   iphone设备的唯一标识

   * @param path   .p12证书文件的路径

   * @param password  .p12证书文件的密码

   * @param message   发送的消息内容

   * @param count

   * @param sendCount  true 一对一发送      false 群发

   */
    public void sendpush(List<String> tokens,String path, String password, String message,Integer

count,boolean sendCount) {
        try {
            // message:{"aps":{"alert":"一条新消息"}}
            PushNotificationPayload payLoad =  PushNotificationPayload.fromJSON(message);
            //payLoad.addAlert(message);
            payLoad.addBadge(count);
            payLoad.addSound("default");

            PushNotificationManager pushManager = new PushNotificationManager();
            // true 正式发布用URL
            // false 测试用URL
            pushManager.initializeConnection(new AppleNotificationServerBasicImpl(path, password,

false));
            List<PushedNotification> notifications = new ArrayList<PushedNotification>();
            // 推送方式
            if (sendCount) {
                System.out.println("-------现在进行一对一推送-------");
                Device device = new BasicDevice();
                device.setToken(tokens.get(0));
                PushedNotification notification = pushManager.sendNotification(device, payLoad,

true);
                notifications.add(notification);
            } else {
                System.out.println("------现在进行群发-------");
                List<Device> device = new ArrayList<Device>();
                for (String token : tokens) {
                    device.add(new BasicDevice(token));
                }
                notifications = pushManager.sendNotifications(payLoad, device);
            }

            List<PushedNotification> failedNotifications = PushedNotification.findFailedNotifications

(notifications);
            List<PushedNotification> successfulNotifications =

PushedNotification.findSuccessfulNotifications(notifications);
            int failed = failedNotifications.size();
            int successful = successfulNotifications.size();

            if (successful > 0 && failed == 0) {
                //log.debug("-----All notifications pushed success (" +

successfulNotifications.size() + "):");
                System.out.println("-----All notifications pushed success (" +

successfulNotifications.size() + "):");
            }
            else if (successful == 0 && failed > 0) {
                //log.debug("-----All notifications pushed failed(" + failedNotifications.size() +

"):");
                System.out.println("-----All notifications pushed failed(" +

failedNotifications.size() + "):");
            }
            else if (successful == 0 && failed == 0) {
                System.out.println("No notifications could be sent, probably because of a critical

error");
            }
            else {
                //log.debug("------Some notifications pushed failed (" + failedNotifications.size

() + "):");
                //log.debug("------Others pushed success(" + successfulNotifications.size() +

"):");
                System.out.println("------Some notifications pushed failed (" +

failedNotifications.size() + "):");
                System.out.println("------Others pushed success(" + successfulNotifications.size()

+ "):");
            }
            pushManager.stopConnection();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

至此,服务器端代码也码完了。(^_^)

APNS推送的特点

从APNS的文档中大概总结了以下几点:

  1. 提供单一发送和群发功能。

  2. 当用户手机不在线(可能没有信号或者关机),APNs会存储转发,等用户在线时再发送。

  3. 如果用户长时间不在线,这条信息会被忽略。

  4. 如果用户不在线,通知会合并,只会保留最新的一条。

  5. payload,就是最后生成的那段Json,不得超过256字节。如果超过了,建议去掉一些不需要的参数,把alert,就是提示信息的字数减少。

  6. 发送成功的没有返回,只有发送失败的才会返回。

  7. 如果有error-response,那么这条之后的通知都需要重发。

  8. 如果出错了,需要关闭当前的连接,并且重新连接再发。error-response中返回的通知ID,可以帮助我们找出哪条出错了,这样就能知道哪些需要重发了。

  9. 不要反复多次连接、终止与APNS的连接,否则会被APNS拒绝连接。

  10. APNS的feedback service会返回那些已经卸载的设备的deviceToken。对于这些token,下次就不用再发了。可以节省点资源。需要注意的是:feedback的接口读取一次,APNS就会清空它的列表,下次再读取时,返回的就是这两次读取之间这段时间新产生的deviceToken

作者:dykingdy
出处:https://lidaguang1989.github.io/
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。