蘋果平臺開發的應用程序,不支持后臺運行程序,所以蘋果有一個推送服務在軟件的一些信息推送給用戶。
JAVA中,有一個開源軟件,JavaPNS實現了Java平臺中連接蘋果服務器與推送消息的服務。但是在使用的過程中,有兩點需要使用者注意一下,希望后續使用的同志們能避免我走過的覆轍。
1、一是向蘋果的服務推送消息時,如果遇到無效的deviceToken,蘋果會斷開網絡連接,而JavaPNS不會進行重連。蘋果原文:
If you send a notification and APNs finds the notification malformed or otherwise unintelligible, it returns an error-response packet prior to disconnecting. (If there is no error, APNs doesn’t return anything.) Figure 5-3 depicts the format of the error-response packet.
2、JavaPNS是一條條發送通知的,但是對于大規模發送的生產環境,顯然是不可以的,建議使用批量發送。蘋果原文:
The binary interface employs a plain TCP socket for binary content that is streaming in nature. For optimum performance, you should batch multiple notifications in a single transmission over the interface, either explicitly or using a TCP/IP Nagle algorithm.
對于此,我對JavaAPNS的代碼進行了改動,使其支持批量發送和蘋果斷開重連,但是有一個問題需要大家注意一下,在正常發送的情況下,蘋果是不會向Socket中寫任何數據的,需要等待其讀超時,this.socket.getInputStream().read(),確訂推送結果的正常。通過持續的向Socket中寫數據,實現批量發送,調用flush方法時,完成一次批量發送。在PushNotificationManager增加如下方法:
Java代碼

- public List<ResponsePacket> sendNotification(List<PushedNotification> pnl) {
- logger.info(psn + "RR批量推送時消息體的大小為:" + pnl.size());
- List<ResponsePacket> failList = new ArrayList<ResponsePacket>();
- if (pnl.size() == 0) {
- return failList;
- }
- Set<Integer> sendSet = new HashSet<Integer>();
- int counter = 0;
- while (counter < pnl.size()) {
- try {
- this.socket.setSoTimeout(3000);
- this.socket.setSendBufferSize(25600);
- this.socket.setReceiveBufferSize(600);
- for (; counter < pnl.size(); counter++) {
- PushedNotification push = pnl.get(counter);
- if (sendSet.contains(push.getIdentifier())) {
- logger.warn("信息[" + push.getIdentifier() + "]已經被推送");
- continue;
- }
- byte[] bytes = getMessage(push.getDevice().getToken(), push.getPayload(), push.getIdentifier(), push);
- this.socket.getOutputStream().write(bytes);
- // 考慮到重發的問題
- // sendSet.add(push.getIdentifier());
- }
- this.socket.getOutputStream().flush();
- // 等待回饋數據,比單個發送時延時長一點,否則將無法獲取到回饋數據
- this.socket.setSoTimeout(1000);
-
- StringBuffer allResult = new StringBuffer();
- ResponsePacket rp = new ResponsePacket();
- int readCounter = 0;
- // 處理讀回寫數據的異常
- try {
- logger.info(psn + "檢查流數據是否可用:" + this.socket.getInputStream().available());
- byte[] sid = new byte[4];// 發送標記
- while (true) {
- int value = this.socket.getInputStream().read();
- if (value < 0) {
- break;
- }
- readCounter++;
- if (readCounter == 1) {
- rp.setCommand(value);
- }
- if (readCounter == 2) {
- rp.setStatus(value);
- }
- if (readCounter >= 3 && readCounter <= 6) {
- sid[readCounter - 3] = (byte) value;
- if (readCounter == 6) {
- rp.setIdentifier(ByteBuffer.wrap(sid).getInt());
- if (failList.contains(rp)) {
- logger.error("錯誤返饋數據中已經包含當前數據," + rp.getIdentifier());
- }
- failList.add(rp);
- }
- }
- allResult.append(value + "_");
- }
- this.socket.getInputStream().close();
- } catch (SocketTimeoutException ste) {
- logger.debug(psn + "消息推送成功,無任何返回!", ste);
- } catch (IOException e) {
- logger.debug(psn + "消息推送成功,關閉連接流時出錯!", e);
- }
- logger.info("蘋果返回的數據為:" + allResult.toString());
- if (readCounter >= 6) {
- // 找到出錯的地方
- for (int i = 0; i < pnl.size(); i++) {
- PushedNotification push = pnl.get(i);
- if (push.getIdentifier() == rp.getIdentifier()) {
- counter = i + 1;
- break;
- // 從出錯的地方再次發送,
- }
- }
- try {
- this.createNewSocket();
- } catch (Exception e) {
- logger.warn("連接時出錯,", e);
- }
- }
- } catch (SSLHandshakeException she) {
- // 握手出錯,標記不加
- logger.warn("SHE消息推送時出錯,", she);
- try {
- this.createNewSocket();
- } catch (Exception e) {
- logger.warn("連接時出錯,", e);
- }
- } catch (SocketException se) {
- logger.warn("SE消息推送時出錯", se);
- counter++;
- try {
- this.createNewSocket();
- } catch (Exception e) {
- logger.warn("連接時出錯,", e);
- }
- } catch (IOException e) {
- logger.warn("IE消息推送時出錯", e);
- counter++;
- } catch (Exception e) {
- logger.warn("E消息推送時出錯", e);
- counter++;
- }
-
- if (counter >= pnl.size()) {
- break;
- }
- }
- return failList;
- }
評論
在javapns2.2 中,重發確實存在bug。但是我發現。在注釋重發后,notifications的返回結果會變得非常不準確(雖然不注釋也不準確),所以根據系統的需求,如果需要比較準確的返回結果,還是不注銷重發比較好,如果系統偏重與推送功能,對推送結果沒有很高的要求,注釋掉重發代碼可以修復重復推送的bug。期待在以后的javapns版本中修復重復推送的bug。
在正常發送的情況下,蘋果是不會向Socket中寫任何數據的,需要等待其讀超時。想問一下樓主,這個蘋果讀取超時是在哪看到的,能否給個鏈接?
lz,,,,你的
持續寫入數據,遇到無效的deviceToken蘋果會斷開連接,并返回相應的消息編號,這時需要重新連接,再次發送數據
博文體現在哪里?是從這里開始嗎:
if (readCounter >= 6) {
// 找到出錯的地方
如果是,lz可以去看看javapns2.2版本的
PushNotificationManager.stopConnection()這個方法。這里面實現了重發。望回復。
請問,你的這個方法的輸入參數: List<PushedNotification> pnl 怎么創建?
lsqwind 寫道
rock 寫道
PushQueue支持重連,
源碼:
Java代碼

- private void sendNotification(PushedNotification notification, boolean closeAfter) throws CommunicationException {
- try {
- Device device = notification.getDevice();
- Payload payload = notification.getPayload();
- try {
- payload.verifyPayloadIsNotEmpty();
- } catch (IllegalArgumentException e) {
- throw new PayloadIsEmptyException();
- } catch (Exception e) {
- }
-
- if (notification.getIdentifier() <= 0) notification.setIdentifier(newMessageIdentifier());
- if (!pushedNotifications.containsKey(notification.getIdentifier())) pushedNotifications.put(notification.getIdentifier(), notification);
- int identifier = notification.getIdentifier();
-
- String token = device.getToken();
- // even though the BasicDevice constructor validates the token, we revalidate it in case we were passed another implementation of Device
- BasicDevice.validateTokenFormat(token);
- // PushedNotification pushedNotification = new PushedNotification(device, payload);
- byte[] bytes = getMessage(token, payload, identifier, notification);
- // pushedNotifications.put(pushedNotification.getIdentifier(), pushedNotification);
-
- /* Special simulation mode to skip actual streaming of message */
- boolean simulationMode = payload.getExpiry() == 919191;
-
- boolean success = false;
-
- BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
- int socketTimeout = getSslSocketTimeout();
- if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
- notification.setTransmissionAttempts(0);
- // Keep trying until we have a success
- while (!success) {
- try {
- logger.debug("Attempting to send notification: " + payload.toString() + "");
- logger.debug(" to device: " + token + "");
- notification.addTransmissionAttempt();
- boolean streamConfirmed = false;
- try {
- if (!simulationMode) {
- this.socket.getOutputStream().write(bytes);
- streamConfirmed = true;
- } else {
- logger.debug("* Simulation only: would have streamed " + bytes.length + "-bytes message now..");
- }
- } catch (Exception e) {
- if (e != null) {
- if (e.toString().contains("certificate_unknown")) {
- throw new InvalidCertificateChainException(e.getMessage());
- }
- }
- throw e;
- }
- logger.debug("Flushing");
- this.socket.getOutputStream().flush();
- if (streamConfirmed) logger.debug("At this point, the entire " + bytes.length + "-bytes message has been streamed out successfully through the SSL connection");
-
- success = true;
- logger.debug("Notification sent on " + notification.getLatestTransmissionAttempt());
- notification.setTransmissionCompleted(true);
-
- } catch (IOException e) {
- // throw exception if we surpassed the valid number of retry attempts
- if (notification.getTransmissionAttempts() >= retryAttempts) {
- logger.error("Attempt to send Notification failed and beyond the maximum number of attempts permitted");
- notification.setTransmissionCompleted(false);
- notification.setException(e);
- logger.error("Delivery error", e);
- throw e;
-
- } else {
- logger.info("Attempt failed (" + e.getMessage() + ")... trying again");
- //Try again
- try {
- this.socket.close();
- } catch (Exception e2) {
- // do nothing
- }
- this.socket = connectionToAppleServer.getSSLSocket();
- if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
- }
- }
- }
- } catch (CommunicationException e) {
- throw e;
- } catch (Exception ex) {
-
- notification.setException(ex);
- logger.error("Delivery error: " + ex);
- try {
- if (closeAfter) {
- logger.error("Closing connection after error");
- stopConnection();
- }
- } catch (Exception e) {
- }
- }
- }
我也看到這段代碼了。確實是有重發功能,默認重發次數是3次。請教下樓主@ autumnrain_zgq 跟你寫的這個方法比有哪些需要注意的地方?
這個代碼正常情況下沒有問題,遇到無效的deviceToken就無法重新發送信息了,還有,他不是批量提交信息的,
rock 寫道
PushQueue支持重連,
源碼:
Java代碼

- private void sendNotification(PushedNotification notification, boolean closeAfter) throws CommunicationException {
- try {
- Device device = notification.getDevice();
- Payload payload = notification.getPayload();
- try {
- payload.verifyPayloadIsNotEmpty();
- } catch (IllegalArgumentException e) {
- throw new PayloadIsEmptyException();
- } catch (Exception e) {
- }
-
- if (notification.getIdentifier() <= 0) notification.setIdentifier(newMessageIdentifier());
- if (!pushedNotifications.containsKey(notification.getIdentifier())) pushedNotifications.put(notification.getIdentifier(), notification);
- int identifier = notification.getIdentifier();
-
- String token = device.getToken();
- // even though the BasicDevice constructor validates the token, we revalidate it in case we were passed another implementation of Device
- BasicDevice.validateTokenFormat(token);
- // PushedNotification pushedNotification = new PushedNotification(device, payload);
- byte[] bytes = getMessage(token, payload, identifier, notification);
- // pushedNotifications.put(pushedNotification.getIdentifier(), pushedNotification);
-
- /* Special simulation mode to skip actual streaming of message */
- boolean simulationMode = payload.getExpiry() == 919191;
-
- boolean success = false;
-
- BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
- int socketTimeout = getSslSocketTimeout();
- if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
- notification.setTransmissionAttempts(0);
- // Keep trying until we have a success
- while (!success) {
- try {
- logger.debug("Attempting to send notification: " + payload.toString() + "");
- logger.debug(" to device: " + token + "");
- notification.addTransmissionAttempt();
- boolean streamConfirmed = false;
- try {
- if (!simulationMode) {
- this.socket.getOutputStream().write(bytes);
- streamConfirmed = true;
- } else {
- logger.debug("* Simulation only: would have streamed " + bytes.length + "-bytes message now..");
- }
- } catch (Exception e) {
- if (e != null) {
- if (e.toString().contains("certificate_unknown")) {
- throw new InvalidCertificateChainException(e.getMessage());
- }
- }
- throw e;
- }
- logger.debug("Flushing");
- this.socket.getOutputStream().flush();
- if (streamConfirmed) logger.debug("At this point, the entire " + bytes.length + "-bytes message has been streamed out successfully through the SSL connection");
-
- success = true;
- logger.debug("Notification sent on " + notification.getLatestTransmissionAttempt());
- notification.setTransmissionCompleted(true);
-
- } catch (IOException e) {
- // throw exception if we surpassed the valid number of retry attempts
- if (notification.getTransmissionAttempts() >= retryAttempts) {
- logger.error("Attempt to send Notification failed and beyond the maximum number of attempts permitted");
- notification.setTransmissionCompleted(false);
- notification.setException(e);
- logger.error("Delivery error", e);
- throw e;
-
- } else {
- logger.info("Attempt failed (" + e.getMessage() + ")... trying again");
- //Try again
- try {
- this.socket.close();
- } catch (Exception e2) {
- // do nothing
- }
- this.socket = connectionToAppleServer.getSSLSocket();
- if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
- }
- }
- }
- } catch (CommunicationException e) {
- throw e;
- } catch (Exception ex) {
-
- notification.setException(ex);
- logger.error("Delivery error: " + ex);
- try {
- if (closeAfter) {
- logger.error("Closing connection after error");
- stopConnection();
- }
- } catch (Exception e) {
- }
- }
- }
我也看到這段代碼了。確實是有重發功能,默認重發次數是3次。請教下樓主@ autumnrain_zgq 跟你寫的這個方法比有哪些需要注意的地方?
sorehead 寫道
autumnrain_zgq 寫道
sorehead 寫道
樓主可以貼一份完整的代碼嗎,我也在糾結這個問題。
下載下來JavaAPNS源代碼項目,在PushNotificationManager這個類中增加我文章中增加的那個方法就可以了。其它地方我也沒有改的,
this.createNewSocket(); 都做了些什么事情?只是重新建立一個socket連接嗎?方便貼出這個方法嗎?
Java代碼

- public void createNewSocket() throws SocketException, KeystoreException, CommunicationException {
- logger.info(psn + "創建新的Socke的連接");
- if (this.socket != null) {
- try {
- socket.close();
- } catch (Exception e) {
- logger.info("關閉Socket連接時出錯...", e);
- }
- }
- this.socket = connectionToAppleServer.getSSLSocket();
- this.socket.setSoTimeout(3000);
- this.socket.setKeepAlive(true);
- }
autumnrain_zgq 寫道
sorehead 寫道
樓主可以貼一份完整的代碼嗎,我也在糾結這個問題。
下載下來JavaAPNS源代碼項目,在PushNotificationManager這個類中增加我文章中增加的那個方法就可以了。其它地方我也沒有改的,
this.createNewSocket(); 都做了些什么事情?只是重新建立一個socket連接嗎?方便貼出這個方法嗎?
sorehead 寫道
樓主可以貼一份完整的代碼嗎,我也在糾結這個問題。
下載下來JavaAPNS源代碼項目,在PushNotificationManager這個類中增加我文章中增加的那個方法就可以了。其它地方我也沒有改的,
PushQueue支持重連,
源碼:
Java代碼

- private void sendNotification(PushedNotification notification, boolean closeAfter) throws CommunicationException {
- try {
- Device device = notification.getDevice();
- Payload payload = notification.getPayload();
- try {
- payload.verifyPayloadIsNotEmpty();
- } catch (IllegalArgumentException e) {
- throw new PayloadIsEmptyException();
- } catch (Exception e) {
- }
-
- if (notification.getIdentifier() <= 0) notification.setIdentifier(newMessageIdentifier());
- if (!pushedNotifications.containsKey(notification.getIdentifier())) pushedNotifications.put(notification.getIdentifier(), notification);
- int identifier = notification.getIdentifier();
-
- String token = device.getToken();
- // even though the BasicDevice constructor validates the token, we revalidate it in case we were passed another implementation of Device
- BasicDevice.validateTokenFormat(token);
- // PushedNotification pushedNotification = new PushedNotification(device, payload);
- byte[] bytes = getMessage(token, payload, identifier, notification);
- // pushedNotifications.put(pushedNotification.getIdentifier(), pushedNotification);
-
- /* Special simulation mode to skip actual streaming of message */
- boolean simulationMode = payload.getExpiry() == 919191;
-
- boolean success = false;
-
- BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
- int socketTimeout = getSslSocketTimeout();
- if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
- notification.setTransmissionAttempts(0);
- // Keep trying until we have a success
- while (!success) {
- try {
- logger.debug("Attempting to send notification: " + payload.toString() + "");
- logger.debug(" to device: " + token + "");
- notification.addTransmissionAttempt();
- boolean streamConfirmed = false;
- try {
- if (!simulationMode) {
- this.socket.getOutputStream().write(bytes);
- streamConfirmed = true;
- } else {
- logger.debug("* Simulation only: would have streamed " + bytes.length + "-bytes message now..");
- }
- } catch (Exception e) {
- if (e != null) {
- if (e.toString().contains("certificate_unknown")) {
- throw new InvalidCertificateChainException(e.getMessage());
- }
- }
- throw e;
- }
- logger.debug("Flushing");
- this.socket.getOutputStream().flush();
- if (streamConfirmed) logger.debug("At this point, the entire " + bytes.length + "-bytes message has been streamed out successfully through the SSL connection");
-
- success = true;
- logger.debug("Notification sent on " + notification.getLatestTransmissionAttempt());
- notification.setTransmissionCompleted(true);
-
- } catch (IOException e) {
- // throw exception if we surpassed the valid number of retry attempts
- if (notification.getTransmissionAttempts() >= retryAttempts) {
- logger.error("Attempt to send Notification failed and beyond the maximum number of attempts permitted");
- notification.setTransmissionCompleted(false);
- notification.setException(e);
- logger.error("Delivery error", e);
- throw e;
-
- } else {
- logger.info("Attempt failed (" + e.getMessage() + ")... trying again");
- //Try again
- try {
- this.socket.close();
- } catch (Exception e2) {
- // do nothing
- }
- this.socket = connectionToAppleServer.getSSLSocket();
- if (socketTimeout > 0) this.socket.setSoTimeout(socketTimeout);
- }
- }
- }
- } catch (CommunicationException e) {
- throw e;
- } catch (Exception ex) {
-
- notification.setException(ex);
- logger.error("Delivery error: " + ex);
- try {
- if (closeAfter) {
- logger.error("Closing connection after error");
- stopConnection();
- }
- } catch (Exception e) {
- }
- }
- }
鐜嬫旦 寫道
鐜嬫旦 寫道
pns 2.2版本不是支持群發嘛?
樓主,看到盡快回復下,,最近也在整這個
你好,是不支持群發的,群發的情況下,是在一個連接打開的情況下,持續寫入數據,遇到無效的deviceToken蘋果會斷開連接,并返回相應的消息編號,這時需要重新連接,再次發送數據。博文的代碼中這是這樣實現的,
鐜嬫旦 寫道
pns 2.2版本不是支持群發嘛?
樓主,看到盡快回復下,,最近也在整這個
linbaoji 寫道
請問你的 JavaPNS 版本是什么 啊!??
謝謝1
你好,我查看一下,版本是:2.2
請問你的 JavaPNS 版本是什么 啊!??
謝謝1