Package com.dbay.apns4j.impl

Source Code of com.dbay.apns4j.impl.ApnsConnectionImpl

/*
* Copyright 2013 DiscoveryBay Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.dbay.apns4j.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;

import javax.net.SocketFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import static com.dbay.apns4j.model.ApnsConstants.*;

import com.dbay.apns4j.IApnsConnection;
import com.dbay.apns4j.model.Command;
import com.dbay.apns4j.model.ErrorResponse;
import com.dbay.apns4j.model.Payload;
import com.dbay.apns4j.model.PushNotification;
import com.dbay.apns4j.tools.ApnsTools;

/**
* @author RamosLi
*
*/
public class ApnsConnectionImpl implements IApnsConnection {
 
  private static AtomicInteger IDENTIFIER = new AtomicInteger(100);
 
  private Log logger = LogFactory.getLog(ApnsConnectionImpl.class);
 
  // never expire
  private int EXPIRE = Integer.MAX_VALUE;
 
  private SocketFactory factory;
  /**
   * EN: There are two threads using the socket, one for writing, one for reading.
   * CN: 一个socket,最多有两个线程在使用它,一个读一个写。
   */
  private Socket socket;
  /**
   * When a notification is sent, cache it into this queue. It may be resent.
   */
  private Queue<PushNotification> notificationCachedQueue = new LinkedList<PushNotification>();
 
  /**
   * Whether find error in the last connection
   */
  private boolean errorHappendedLastConn = false;
 
  /**
   * Whether first write data in the connection
   */
  private boolean isFirstWrite = false;
 
  private int maxRetries;
  private int maxCacheLength;
 
  private int readTimeOut;
 
  private String host;
  private int port;
 
  /**
   * You can find the properly ApnsService to resend notifications by this name 
   */
  private String name;
 
  /**
   * connection name
   */
  private String connName;
  private int intervalTime;
  private long lastSuccessfulTime = 0;
 
  private AtomicInteger notificaionSentCount = new AtomicInteger(0);
 
  private Object lock = new Object();
 
  public ApnsConnectionImpl(SocketFactory factory, String host, int port, int maxRetries,
      int maxCacheLength, String name, String connName, int intervalTime, int timeout) {
    this.factory = factory;
    this.host = host;
    this.port = port;
    this.maxRetries = maxRetries;
    this.maxCacheLength = maxCacheLength;
    this.name = name;
    this.connName = connName;
    this.intervalTime = intervalTime;
    this.readTimeOut = timeout;
  }
 
  @Override
  public void sendNotification(String token, Payload payload) {
    PushNotification notification = new PushNotification();
    notification.setId(IDENTIFIER.incrementAndGet());
    notification.setExpire(EXPIRE);
    notification.setToken(token);
    notification.setPayload(payload);
    sendNotification(notification);
  }
 
  @Override
  public void sendNotification(PushNotification notification) {
    byte[] plBytes = null;
    String payload = notification.getPayload().toString();
    try {
      plBytes = payload.getBytes(CHARSET_ENCODING);
      if (plBytes.length > PAY_LOAD_MAX_LENGTH) {
        logger.error("Payload execeed limit, the maximum size allowed is 256 bytes. " + payload);
        return;
      }
    } catch (UnsupportedEncodingException e) {
      logger.error(e.getMessage(), e);
      return;
    }
   
    /**
     * EN: If error happened before, just wait until the resending work finishes by another thread
     *     and close the current socket
     * CN: 如果发现当前连接有error-response,加锁等待,直到另外一个线程把重发做完后再继续发送 
     */
    synchronized (lock) {
      if (errorHappendedLastConn) {
        closeSocket(socket);
        socket = null;
      }
      byte[] data = notification.generateData(plBytes);
      boolean isSuccessful = false;
      int retries = 0;
      while (retries < maxRetries) {
        try {
          boolean exceedIntervalTime = lastSuccessfulTime > 0 && (System.currentTimeMillis() - lastSuccessfulTime) > intervalTime;
          if (exceedIntervalTime) {
            closeSocket(socket);
            socket = null;
          }
         
          if (socket == null || socket.isClosed()) {
            socket = createNewSocket();
          }
         
          OutputStream socketOs = socket.getOutputStream();
          socketOs.write(data);
          socketOs.flush();
          isSuccessful = true;
          break;
        } catch (Exception e) {
          logger.error(connName + " " + e.getMessage(), e);
          closeSocket(socket);
          socket = null;
        }
        retries++;
      }
      if (!isSuccessful) {
        logger.error(String.format("%s Notification send failed. %s", connName, notification));
        return;
      } else {
        logger.info(String.format("%s Send success. count: %s, notificaion: %s", connName,
            notificaionSentCount.incrementAndGet(), notification));
       
        notificationCachedQueue.add(notification);
        lastSuccessfulTime = System.currentTimeMillis();
       
        /** TODO there is a bug, maybe, theoretically.
         *  CN: 假如我们发了一条错误的通知,然后又发了 maxCacheLength 条正确的通知。这时APNS服务器
         *      才返回第一条通知的error-response。此时,第一条通知已经从队列移除了。。
         *      其实后面100条该重发,但却没有。不过这个问题的概率很低,我们还是信任APNS服务器能及时返回
         */     
        if (notificationCachedQueue.size() > maxCacheLength) {
          notificationCachedQueue.poll();
        }
      }
    }

    if (isFirstWrite) {
      isFirstWrite = false;
      /**
       * EN: When we create a socket, just a TCP/IP connection created. After we wrote data to the stream,
       *     the SSL connection had been created. Now, it's time to read data from InputStream
       * CN: createSocket时只建立了TCP连接,还没有进行SSL认证,第一次写完数据后才真正完成认证。所以此时才开始
       *     监听InputStream
       */
      startErrorWorker();
    }
  }
  private Socket createNewSocket() throws IOException, UnknownHostException {
    if (logger.isDebugEnabled()) {
      logger.debug(connName + " create a new socket.");
    }
    isFirstWrite = true;
    errorHappendedLastConn = false;
    Socket socket = factory.createSocket(host, port);
    socket.setSoTimeout(readTimeOut);
    // enable tcp_nodelay, any data will be sent immediately.
    socket.setTcpNoDelay(true);
   
    return socket;
  }
  private void closeSocket(Socket socket) {
    try {
      if (socket != null) {
        socket.close();
      }
    } catch (Exception e) {
      logger.error(e.getMessage(), e);
    }
  }
  private boolean isSocketAlive(Socket socket) {
    if (socket != null && socket.isConnected()) {
      return true;
    }
    return false;
  }
 
  @Override
  public void close() throws IOException {
    closeSocket(socket);
  }
 
  private void startErrorWorker() {
    Thread thread = new Thread(new Runnable() {

      @Override
      public void run() {
        Socket curSocket = socket;
        try {
          if (!isSocketAlive(curSocket)) {
            return;
          }
          InputStream socketIs = curSocket.getInputStream();
          byte[] res = new byte[ERROR_RESPONSE_BYTES_LENGTH];
          int size = 0;
         
          while (true) {
            try {
              size = socketIs.read(res);
              if (size > 0 || size == -1) {
                // break, when something was read or there is no data any more
                break;
              }
            } catch (SocketTimeoutException e) {
              // There is no data. Keep reading.
            }
          }
         
          int command = res[0];
          /** EN: error-response,close the socket and resent notifications
           *  CN: 一旦遇到错误返回就关闭连接,并且重新发送在它之后发送的通知
           */     
          if (size == res.length && command == Command.ERROR) {
            int status = res[1];
            int errorId = ApnsTools.parse4ByteInt(res[2], res[3], res[4], res[5]);
           
            if (logger.isInfoEnabled()) {
              logger.info(String.format("%s Received error response. status: %s, id: %s, error-desc: %s", connName, status, errorId, ErrorResponse.desc(status)));
            }
           
            Queue<PushNotification> resentQueue = new LinkedList<PushNotification>();

            synchronized (lock) {
              boolean found = false;
              errorHappendedLastConn = true;
              while (!notificationCachedQueue.isEmpty()) {
                PushNotification pn = notificationCachedQueue.poll();
                if (pn.getId() == errorId) {
                  found = true;
                } else {
                  /**
                   * https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html
                   * As the document said, add the notifications which need be resent to the queue.
                   * Igonre the error one
                   */
                  if (found) {
                    resentQueue.add(pn);
                  }
                }
              }
              if (!found) {
                logger.warn(connName + " Didn't find error-notification in the queue. Maybe it's time to adjust cache length. id: " + errorId);
              }
            }
            // resend notifications
            if (!resentQueue.isEmpty()) {
              ApnsResender.getInstance().resend(name, resentQueue);
            }
          } else {
            // ignore and continue reading
            logger.error(connName + " Unexpected command or size. commend: " + command + " , size: " + size);
          }
        } catch (Exception e) {
//          logger.error(connName + " " + e.getMessage(), e);
          logger.error(connName + " " + e.getMessage());
        } finally {
          /**
           * EN: close the old socket although it may be closed once before.
           * CN: 介个连接可能已经被关过一次了,再关一下也无妨,万无一失嘛
           */
          closeSocket(curSocket);
        }
      }
    });
   
    thread.start();
  }
}
 
TOP

Related Classes of com.dbay.apns4j.impl.ApnsConnectionImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.