• 移动互联网实战--Apple的APNS桩推送服务的实现(2)


    前记:
      相信大家在搞IOS推送服务的开发时, 会直接使用javapns api来简单实现, 调试也直连Apple的APNS服务(产品/测试版)来实现. 很少有人会写个APNS的桩服务, 事实也是如此. 只是当时我所面临的应用场景有些特殊, 为了测试服务的性能和调试功能方便, 特地写了APNS的桩服务(其实主要原因是当时的iphone测试机, 被小组长"霸占"占为己用, ⊙﹏⊙b汗). 在此写一篇关于APNS桩服务的文章, 以此纪念逝去的"青春", 也希望对读者有所帮助.
      有些事情回想起来, 历历在目, 清晰异常, 仿佛发生在昨天. 犹记一年前, 开发IOS的APNS推送后端服务, 吐血三升...

    秉承:
      上一篇文章, 详见: Apple的APNS桩推送服务的实现(1) 
      文章简要介绍了APNS协议/证书管理/JSSE的API的一些基础概念和实现原理.

    需求分析:
      从开发者的角度去理解和分析需求. 对于IOS的消息推送的功能和性能测试, 大致可分为如下几种:
      1). 推送平台QPS/RT的性能指标
      2). Bad Device Token对服务的影响评估
      3). 证书对服务的影响评估
      4). 推送可靠性评估(推送弱ack机制, 丢消息和消息重复之间的折中)
      通过记录日志来评估QPS/RT和服务的质量, 引入黑名单机制来模拟bad device token对推送服务的影响评估.

    技术分析:
      对APNS服务的理解和自身的功能/性能测试需求:
      1). APNS推送服务采用SSL长连接/二进制流/异步的方式来实现, 追求推送消息的吞吐量.
      2). APNS支持V1/V2两种二进制协议, 遇到异常时, APNS的默认行为不一样.
      3). APNS客户端是TSL(SSL)的单向认证(客户端携带证书, 服务端信任检查).
      结合桩的定位, 我们采用One Connection Per Thread的线程池模式去模拟处理它, 一方面能基本满足需求, 另一方面实现高效简单. 同时为了方便测试, 服务端关闭对客户端认证机制. 

    服务实现:
      APNS模拟桩的实现过程
      1). 服务端证书引入

    keytool -genkey -v -alias ssl-server -keyalg RSA -keystore ./server_ks -storepass server -keypass 123456 -dname "CN=Unknown"

      生成效果如图

      2). 自定义TrustManager的实现类

    private class MyX509TrustManager implements X509TrustManager {
    
      @Override
      public X509Certificate[] getAcceptedIssuers() {
        return null;
      }
      // 对服务端证书的认证, 抛出异常表示不通过
      @Override
      public void checkServerTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
      }
      // 对客户端证书进行认证, 抛出异常表示不通过
      @Override
      public void checkClientTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
      }
    
    }; 

      评注: 这边提供空实现, 对客户端的证书不进行任何校验
      3). 创建SSLServerSocket代码片段

    SSLContext ctx = SSLContext.getInstance("SSL");
    
    // *) 创建KeyManagerFactory类实例
    KeyManagerFactory kmf = KeyManagerFactory
        .getInstance(KeyManagerFactory.getDefaultAlgorithm());
    
    // *) 初始化KeyStore, 这边的KEY_PASSWORD为"123456"
    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
    ks.load(ApnsStubServer.class.getResourceAsStream("/server_ks"), null);
    kmf.init(ks, KEY_PASSWOR.toCharArray());
    
    // *) 进行SSLContext类的初始化工作 
    ctx.init(kmf.getKeyManagers(),
        new TrustManager[] { new MyX509TrustManager() },
        new SecureRandom());
    
    // *) SSLServerSocket的生成 
    SSLServerSocket serverSocket = (SSLServerSocket) ctx
        .getServerSocketFactory()
        .createServerSocket(serverPort);
    serverSocket.setReuseAddress(true);
    // *) 设置Client认证开关关闭
    serverSocket.setNeedClientAuth(false);

      评注: 把server_ks代码放到classpath目录下, 同时这边的key_password为"123456", 而不是keystore的密码"server", 注意keypass和storepass的区别
      4). 服务端连接处理代码

    // One Connection Per Thread的机制	
    ExecutorService workerPools = Executors.newCachedThreadPool();
    
    while ( true ) {
      final Socket cliSocket = sslServerSocket.accept();
      workerPools.execute(new Runnable() {
        @Override
        public void run() {
          onHandle(cliSocket);
        }
      });
    }

      评注: serverSocket用于接受客户端连接, 并放入工作池去处理
      onHandle函数定义如下:

    private void onHandle(Socket cliSocket) {
    
      DeviceTokenManager dtm = apnsStubController.getDeviceTokenManager();
      DataInputStream dis = new DataInputStream(cliSocket.getInputStream());   int ch = 0;   while ( (ch = dis.read()) != -1 ) {     IVResult<ApnsMessage> result = ApnsProtocolUtility.readMessage(ch, dis);     ApnsMessage message = result.getValue();     // 属于不同版本的消息, 进行分发和处理     if ( ApnsVersion.APNS_BINARY_PROTOCOL_V1 == message.getVersion() ) {       // *) 如果属于bad device token黑名单       if ( dtm.judgeBadDeviceToken(message.getDeviceToken()) ) {         break;       }       printMessage(message);     } else if ( ApnsVersion.APNS_BINARY_PROTOCOL_V2 == message.getVersion() ) {       // *) 如果属于bad device token黑名单       if ( dtm.judgeBadDeviceToken(message.getDeviceToken()) ) {         // 8 为令牌错误, 就是bad device token         ApnsProtocolUtility. writeResponse(8, message.getIdentifier(), cliSocket.getOutputStream());         break;       }       printMessage(message);     }   } }

      5). 黑名单的引入和接口定义

      由外置黑名单文件来指定

    public class DeviceTokenManager {
    
      // 用于配置参数的初始化
      public void init(Properties prop);
    
      // 判断是否为bad device token
      public boolean judgeBadDeviceToken(String deviceToken);
    
      // 载入bad device token 名单
      private void loadBadDeviceTokenFile(String filename);
    
    }

      总结: 难点在于JSSE和证书的理解, 至于APNS的网络协议, 处理还是相对简单的.

    APNS推送服务平台:
      对于APNS推送服务平台, 还是有些优化技巧, 这边并不深入展开, 仅从个人的观点描述几个.
      1). 对gateway.push.apple.com的ip地址解析, 挑选rrt时间最短的服务ip进行连接
      
      2). 引入队列异步化/构建DadDeviceToken仓库
      作为推送服务, 对于推送请求, 作简单的校验(消息格式合法/是否属于bad device token). 然后储存消息于队列中, 由后端worker慢慢消化. 不再等待真实的APNS服务反馈(APNS服务弱ACK机制), 立即返回结果(允许一定程度的推送成功误报率).
      构建自己的Bad Device Token仓库, 一方来自Feedback服务接受, 一方来自APNS推送的响应结果收集.

    后记:
      感觉自己还是没有把自己想表达的事情说清楚, 甚是遗憾. 无论如何, 还是希望对读者有些帮助, 对我而言, 则是充满了回忆. Fighting, Let it go!!!

     

  • 相关阅读:
    Lily.Core.FileDataProvider文件管理使用范例。
    CruiseControl.NET,Nant持续集成(1)
    如何为当前进程设置环境变量?
    unix时间戳与datetime的转换函数
    Mac 平台下功能强大的Shimo软件使用指南
    如何解决源码包安装时的依赖性问题
    《Linux企业应用案例精解》一书配套视频发布
    ZoneMinder配置与使用
    网站优化IIS7下静态文件的优化
    WIN7常用功能的介绍
  • 原文地址:https://www.cnblogs.com/mumuxinfei/p/3934677.html
Copyright © 2020-2023  润新知