程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 基於Codeigniter框架實現的APNS批量推送—叮咚,查水表,codeigniterapns

基於Codeigniter框架實現的APNS批量推送—叮咚,查水表,codeigniterapns

編輯:關於PHP編程

基於Codeigniter框架實現的APNS批量推送—叮咚,查水表,codeigniterapns


   最近兼職公司已經眾籌成功的無線門鈴的消息推送出現了問題,導致有些用戶接收不到推送的消息,真是嚇死寶寶了,畢竟自己一手包辦的後台服務,影響公司信譽是多麼的尴尬,容我簡單介紹一下我們的需求:公司開發的是一款無線門鈴系統,如果有人在門外按了門鈴開關,門鈴開關會發射一個信號,屋裡的接收網關接收到信號會發出響聲,同時也會推送一條消息到用戶手機,即使這個手機是遠程的,也就是主人不在家也知道有人按了家裡的門鈴。這裡後台需要解決的問題是搭建APNS推送的Provider,因為要想把消息推送到蘋果手機,按照蘋果公司設計的機制,必須通過自己的服務器推送到蘋果的PUSH服務器,再由它推送到手機,每個手機對應一個deviceToken,我這裡介紹的重點並不是這個平台怎麼搭建,這個國內網上的教程已經相當豐富了。比如你可以參考:一步一步教你做ios推送

網上的教程大多是走的通的,但是他們操作的對象是一個手機,我的意思是它們是一次給一個手機終端推送消息,在我們公司設計的產品中,同一個賬戶可以在多個手機上登錄(理論上是無數個,因為我在後台並沒有限制),每個手機對應的deviceToken是不同的,另外公司的產品還設計了分享功能,也就是主用戶可以把設備分享給其他用戶,而其他用戶也有可能在不同設備上同時登錄,如果有人按了門鈴要向所有已經登錄的用戶包括分享的用戶推送消息,也就是要批量推送到很多個手機終端。當然我這裡舉的例子並不會有這麼復雜,所有的問題抽象出來其實就是一個問題:給你一個存儲deviceToken的數組,APNS如何批量推送給多個用戶?

 

首先我們設計一個數據庫,用來存儲用戶的推送令牌(deviceToken),為簡單起見,這個表就兩個字段。

client_id deviceToken 1 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

 

這裡我使用的是CodeIgniter3的框架,我們新建一個Model,來管理用戶deviceToken數據。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 <?php // ios推送令牌管理       class Apns_model extends CI_Model {       public function __construct()     {         $this->load->database();     }         /**      * 新建推送令牌      * create a apns如果已經存在就更新這個deviceToken      * $data is an array organized by controller      */     public function create($data)     {         if($this->db->replace('tb_apns', $data))         {             return TRUE;         }         else         {             return FALSE;         }     }             //刪除某個用戶的推送令牌     public function delete($user_id)     {                   if(isset($user_id)){             $result=$this->db->delete('tb_apns', array('client_id' => $user_id));              return TRUE;                    }else{             return FALSE;         }       }         //根據推送令牌刪除推送令牌     public function deletebytoken($token)     {                   if (isset($token)) {             $result=$this->db->delete('tb_apns', array('deviceToken'=>$token));             return TRUE;           }else{             return FALSE;         }       }           //查詢某個用戶的iso推送令牌     public function get($client_id)     {         $sql = "SELECT deviceToken FROM `tb_apns` WHERE `client_id`='$client_id'";           $result = $this->db->query($sql);           if ($result->num_rows()>0)         {             return $result->result_array();         }         else         {             return FALSE;         }     }         }

在我後台的第一個版本中,按照網上的教程,大多是一次給一個終端推送消息的,我稍微改了一下,將所有取得的deviceToken存在$deviceTokens數組中,參數$message是需要推送的消息,使用for循環依次從數組中取出一個deviceToken來推送,然後計數,如果所有的推送成功則返回true。這個方法看似是沒有任何破綻的,而且也測試成功了,所以我就直接上線了,(主要是我也沒想到公司會突然出這樣一個產品,把推送功能的地位抬得很高,我一直以為是可有可無的)。

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 function _send_apns($deviceTokens,$message)     {           // Put your private key's passphrase here:密語        $passphrase = 'xxxxx';              ////////////////////////////////////////////////////////////////////////////////          $ctx = stream_context_create();        stream_context_set_option($ctx, 'ssl', 'local_cert', 'xxxx.pem');        stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase);          // Open a connection to the APNS server        $fp = stream_socket_client(            'ssl://gateway.push.apple.com:2195', $err,            $errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);          if (!$fp)            exit("Failed to connect: $err $errstr" . PHP_EOL);          echo 'Connected to APNS' . PHP_EOL;           // Create the payload body        $body['aps'] = array(            'alert' => $message,            'sound' => 'default'            );            // Encode the payload as JSON        $payload = json_encode($body);          $num=count($deviceTokens);        $countOK=0;//統計發送成功的條數          for($i=0;$i<$num;$i++)        {            $deviceToken=$deviceTokens[$i];              $deviceToken=preg_replace("/\s/","",$deviceToken);//刪除deviceToken裡的空格             // Build the binary notification            $msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken) . pack('n', strlen($payload)) . $payload;           // Send it to the server            $result = fwrite($fp, $msg, strlen($msg));              if ($result)                      {                $countOK++;                         }        }         // Close the connection to the server        fclose($fp);        if($countOK==$num)            return TRUE;        else            return FALSE;      }

就是上面的代碼導致了後來推送出現了一系列問題。

第一個大問題是:這裡默認了所有的推送令牌都是有效的,而實際上,如果用戶直接刪除了app或者app升級都有可能造成後台數據庫裡的deviceToken沒有發生更新,從而使推送令牌失效。但是有人按了門鈴,後台還是會把它當成有效的deviceToken納入到$deviceTokens中,如何清除失效過期的deviceToken是個必須考慮的問題。

查閱相關資料發現APNS服務有一個The Feedback Service的服務,國內的博客基本上忽略了這個環節,很少有資料提及,還是谷歌找個官方網站比較靠譜。下面簡要介紹一下這個服務:

 

在進行APNS遠程推送時,如果由於用戶卸載了app而導致推送失敗,APNS服務器會記錄下這個deviceToken,加入到一個列表中,可以通過查詢這個列表,獲取失效的推送令牌,從數據庫中清除這些失效的令牌就可以避免下次推送時被加入到推送數組中來。連接這項服務很簡單和推送工程類似,只不過地址不同,開發環境為feedback.push.apple.com ,測試環境為feedback.sandbox.push.apple.com端口都是2196。APNS服務器返回的數據格式為:

Timestamp

A timestamp (as a four-byte time_t value) indicating when APNs determined that the app no longer exists on the device. This value, which is in network order, represents the seconds since 12:00 midnight on January 1, 1970 UTC.

Token length

The length of the device token as a two-byte integer value in network order.

Device token

The device token in binary format.

為了進行這項服務,我寫了一個CI框架的控制器

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <?php defined('BASEPATH') OR exit('No direct script access allowed');   class Admin extends CI_Controller {       public function __construct()     {         parent::__construct();         // 加載數據庫         $this->load->database();         $this->load->model('apns_model');     }       public function apnsfeedback()     {         $ctx = stream_context_create();          $passphrase = 'xxxxx';         stream_context_set_option($ctx, 'ssl', 'local_cert', 'xxxxxxx.pem');         stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase);           $fp = stream_socket_client('ssl://feedback.push.apple.com:2196', $error, $errorString, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);         if (!$fp) {             echo "Failed to connect feedback server: $err $errstr\n";             return;         }         else {             echo "Connection to feedback server OK\n";             echo "<br>";         }           while ($devcon = fread($fp, 38))         {             $arr = unpack("H*", $devcon);             $rawhex = trim(implode("", $arr));             // $feedbackTime = hexdec(substr($rawhex, 0, 8));             // $feedbackDate = date('Y-m-d H:i', $feedbackTime);             // $feedbackLen = hexdec(substr($rawhex, 8, 4));             $feedbackDeviceToken = substr($rawhex, 12, 64);             if (!empty($feedbackDeviceToken)) {                 echo "Invalid token $feedbackDeviceToken\n";                 echo "<br>";                 $this->apns_model->deletebytoken($feedbackDeviceToken);                               }         }         fclose($fp);     }       }

通過循環讀取38個字節的數據,可以查詢出所有失效的令牌並在數據庫中刪除。
不過需要注意的是:一旦你讀取了數據,APNS就會清除已有的列表,下次查詢時返回的是自從上次查詢後再次累積的無效的令牌。

The feedback service’s list is cleared after you read it. Each time you connect to the feedback service, the information it returns lists only the failures that have happened since you last connected.

還有一點就是你胡亂造的deviceToken是不會被服務器記錄的。

第二個問題:假如$deviceTokens數組裡有很多個元素,有時會發生前面幾個令牌推送成功,手機收到了消息,但是後面的令牌沒有推送成功,沒有收到消息。

 關於這個問題,國內的博客也是很少提及,直到在官網上看到下面的幾句話:

 

 

If you send a notification that is accepted by APNs, nothing is returned.

If you send a notification that is malformed or otherwise unintelligible, APNs returns an error-response packet and closes the connection. Any notifications that you sent after the malformed notification using the same connection are discarded, and must be resent. 

上面幾句話的意思大致是:每次針對一個deviceToken推送消息時,如果推送失敗,沒有任何數據返回,如果apns服務器不能識別推送的令牌APNS會返回一個錯誤消息並關閉當前的連接,所有後續通過同一連接推送的消息都會放棄,必須重新連接跳過無效的再發送。這也解釋了為什麼後面的推送會失敗。下圖是返回的數據及對應的錯誤碼,這裡需要重點關注錯誤碼8,其對應這無效的令牌。

      Codes in error-response packet

Status code

Description

0

No errors encountered

1

Processing error

2

Missing device token

3

Missing topic

4

Missing payload

5

Invalid token size

6

Invalid topic size

7

Invalid payload size

8

Invalid token

10

Shutdown

128

Protocol error (APNs could not parse the notification)

255

None (unknown)

那麼如何讓APNS服務器返回錯誤消息呢,實際上我之前的第一種解決方案中,APNS並不會返回錯誤消息,我只是一廂情願的在統計發送成功次數,如果需要APNS返回錯誤消息,需要改變發送數據的格式。發送數據的格式同樣推薦參考官方文檔,因為國內的博客基本上沒好好嚴格按照文檔來打包數據。下面的格式介紹來自官方文檔。

Figure A-1  Notification format

Note: All data is specified in network order, that is big endian.

 

The top level of the notification format is made up of the following, in order:

Table A-1  Top-level fields for remote notifications

Field name

Length

Discussion

Command

1 byte

Populate with the number 2.

Frame length

4 bytes

The size of the frame data.

Frame data

variable length

The frame contains the body, structured as a series of items.

The frame data is made up of a series of items. Each item is made up of the following, in order:

Table A-2  Fields for remote notification frames

Field name

Length

Discussion

Item ID

1 byte

The item identifier, as listed in Table A-3. For example, the item identifier of the payload is 2.

Item data length

2 bytes

The size of the item data.

Item data

variable length

The value for the item.

The items and their identifiers are as follows:

Table A-3  Item identifiers for remote notifications

Item ID

Item Name

Length

Data

1

Device token

32 bytes

The device token in binary form, as was registered by the device.

A remote notification must have exactly one device token.

2

Payload

variable length, less than or equal to 2 kilobytes

The JSON-formatted payload. A remote notification must have exactly one payload.

The payload must not be null-terminated.

3

Notification identifier

4 bytes

An arbitrary, opaque value that identifies this notification. This identifier is used for reporting errors to your server.

Although this item is not required, you should include it to allow APNs to restart the sending of notifications upon encountering an error.

4

Expiration date

4 bytes

A UNIX epoch date expressed in seconds (UTC) that identifies when the notification is no longer valid and can be discarded.

If this value is non-zero, APNs stores the notification tries to deliver the notification at least once. Specify zero to indicate that the notification expires immediately and that APNs should not store the notification at all.

5

Priority

1 byte

The notification’s priority. Provide one of the following values:

  • 10 The push message is sent immediately.

    The remote notification must trigger an alert, sound, or badge on the device. It is an error to use this priority for a push that contains only the content-available key.

  • 5 The push message is sent at a time that conserves power on the device receiving it.

    Notifications with this priority might be grouped and delivered in bursts. They are throttled, and in some cases are not delivered.

注意上面我標紅的幾句話,大意是說這個字段作為此次推送消息唯一的標識符,有了這個標識符,APNS就能向我們的服務器報告錯誤。這也解釋了為什麼我上面的解決方案沒有返回錯誤信息。下面是新的推送數據打包方式。這裡我直接以發送的數組下標$i作為標識符,更優化的方法是使用deviceToken在數據庫中對應的id作為唯一標識符。

1 $msg = pack("C", 1) . pack("N", $i) . pack("N", $apple_expiry) . pack("n", 32) . pack('H*', str_replace(' ', '',$deviceToken)) . pack("n", strlen($payload)) . $payload;

 

如果批量推送中途有一個推送失敗,連接會被關閉,需要重新連接推送後面的令牌,所以最好把建立連接的過程封裝成一個方法。

 

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private function create_apns_link(){                                       $scc = stream_context_create();         stream_context_set_option($scc, 'ssl', 'local_cert', realpath($this->certificate));         stream_context_set_option($scc, 'ssl', 'passphrase', $this->passphrase);                   $fp = stream_socket_client($this->link, $err,$errstr, 60, STREAM_CLIENT_CONNECT, $scc);                   $i = 0;         while (gettype($fp) != 'resource'){//如果建立失敗就每隔100ms重新建立一次,如果失敗3次就放棄             if($i < 3){                 usleep(100000);                 $fp = stream_socket_client($link, $err,$errstr, 60, STREAM_CLIENT_CONNECT, $scc);                 $i++;             }else{                 break;             }         }                   stream_set_blocking ($fp, 0);         if($fp){             echo 'Connected to APNS for Push Notification';         }         return $fp;     }

至此,我把前面講到的推送過程封裝到一個CodeIgniter類庫中

 

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 <?php defined('BASEPATH') or exit('No direct script access allowed');   class Apns{     private $certificate="xxxx.pem";     private $passphrase="xxxx";     private $link = 'ssl://gateway.push.apple.com:2195';     protected $CI;       public function __construct()     {         $this->CI=&get_instance();         // 加載數據庫         $this->CI->load->database();         $this->CI->load->model('apns_model');     }           public function send($deviceTokens,$data){         $fp = $this->create_apns_link();                   $body['aps'] = $data;         $apple_expiry = 0;         $payload = json_encode($body);         $num=count($deviceTokens);           for ($i=0; $i <$num ; $i++) {             $deviceToken=$deviceTokens[$i];             $msg = pack("C", 1) . pack("N", $i) . pack("N", $apple_expiry) . pack("n", 32) . pack('H*', str_replace(' ', '',$deviceToken)) . pack("n", strlen($payload)) . $payload;           $rtn=fwrite($fp, $msg);           usleep(400000);//每次推送過後,因為php是同步的apns服務器不會立即返回錯誤消息,所以這裡等待400ms           $errorcode=$this->checkAppleErrorResponse($fp);//檢查是否存在錯誤消息           if($errorcode){                       if ($errorcode=='8') {//如果令牌無效就刪除                 $this->CI->apns_model->deletebytoken($deviceToken);                            }             if($i<$num-1){//如果還沒推送完,需要重新建立連接推送後面的                 $fp = $this->create_apns_link();             }                         }           }                   fclose($fp);         return true;       }                   private function create_apns_link(){                                       $scc = stream_context_create();         stream_context_set_option($scc, 'ssl', 'local_cert', realpath($this->certificate));         stream_context_set_option($scc, 'ssl', 'passphrase', $this->passphrase);                   $fp = stream_socket_client($this->link, $err,$errstr, 60, STREAM_CLIENT_CONNECT, $scc);                   $i = 0;         while (gettype($fp) != 'resource'){//如果建立失敗就每隔100ms重新建立一次,如果失敗3次就放棄             if($i < 3){                 usleep(100000);                 $fp = stream_socket_client($link, $err,$errstr, 60, STREAM_CLIENT_CONNECT, $scc);                 $i++;             }else{                 break;             }         }                   stream_set_blocking ($fp, 0);         if($fp){             echo 'Connected to APNS for Push Notification';         }         return $fp;     }                    private function checkAppleErrorResponse($fp) {           //byte1=always 8, byte2=StatusCode, bytes3,4,5,6=identifier(rowID).         // Should return nothing if OK.                   //NOTE: Make sure you set stream_set_blocking($fp, 0) or else fread will pause your script and wait         // forever when there is no response to be sent.                   $apple_error_response = fread($fp, 6);          if ($apple_error_response) {           // unpack the error response (first byte 'command" should always be 8)             $error_response = unpack('Ccommand/Cstatus_code/Nidentifier', $apple_error_response);                       return $error_response['status_code'];         }                   return false;     }             }

總結:官方文檔才是王道啊,英語好才是王道啊!!!!

參考

Binary Provider API

APNS SSL operation failed with code 1

 

 

 

 



來自為知筆記(Wiz)



  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved