APNS:Apple Push Notification Service。本文對推送相關概念再也不贅述,只側重完整流程。 php
Demo 開發環境:Mac os 10.9.4 ,Xcode 6.0.1 ;測試設備:iphone 4s(ios 7.1)html
服務端開發環境:mac 10.9.4 + php 5.4.2四、node
Demo 下載地址:點擊打開連接ios
如對apns相關概念不清楚,可參考如下幾個博客:(博客中部份內容重複,但整體來講,通讀一遍,仍是大有裨益的)程序員
http://cshbbrain.iteye.com/blog/1859810 =》IOS 基於APNS消息推送原理與實現(JAVA後臺)web
http://www.cnblogs.com/qq78292959/archive/2012/07/16/2593651.html =》iOS消息推送機制的實現
apache
http://blog.csdn.net/xunyn/article/details/8243573 =》APNS編程----iOS真機測試消息推送
編程
http://blog.csdn.net/wswqiang/article/details/8208581 =》IOS APNS 處理
json
http://eric-gao.iteye.com/blog/1567777 =》 IOS PEM 文件的生成瀏覽器
http://www.36coder.com/study/996.html =》PHP 實現APNS 推送
http://blog.csdn.net/sxfcct/article/details/7939082 =》 APNS 相關總結(推薦)
消息推送:
開發接口:gateway.sandbox.push.apple.com:2195
發佈接口:gateway.push.apple.com:2195
反饋服務:
開發接口:feedback.sandbox.push.apple.com:2196
發佈接口:產品接口:feedback.push.apple.com:2196
2、製做Push證書和Pem文件
新建流程再也不贅述,這裏只提醒兩點:1》App ID Suffix 中,必定要選擇Explicit App ID;2》App Services 中,記得勾選Push Notifications。這裏以新建一個id爲:com.eversoft.PushDemo 爲例。
在App IDs中,選中剛纔新建的App id:com.eversoft.PushDemo ,單擊,展開詳細信息屬性。
在詳細信息屬性中,單擊下方的「Edit」按鈕,
在新打開的編輯界面,單擊「Create Certificate」,
在新打開的界面中,會提示咱們,建立一個csr 證書籤名請求文件。具體的建立步驟,界面中已經給出了詳細的英文說明。
在進行下一步以前,咱們先按照英文說明,建立一個 CSR 文件。
在mac電腦上,打開應用程序 keychain(鑰匙串訪問);
在keychain菜單欄中,依次選擇「鑰匙串訪問」=》「證書助理」=》「從證書頒發機構請求證書」;
在新打開的「證書助理」界面中,填寫用戶電子郵件地址,經常使用名稱,CA電子郵件地址,這兩個郵件地址直接填寫你的蘋果帳號的郵件地址便可,而後選擇「存儲到磁盤」,而後點擊「繼續」;
選擇CSR文件保存位置,「存儲」便可。至此, CSR 文件,製做完成。
回到剛纔咱們的web頁面上,點擊「Continue」,進入下一頁面;新的頁面中,會要求咱們上傳剛纔製做的csr文件,選擇「Choose File」,找到咱們剛纔存儲的csr文件,單擊「打開」,最後,點擊頁面上的「Generate」按鈕,到此,開發使用的push證書製做完畢。
證書生成成功後,選擇「Download」,將製做好的證書下載到本地。而後雙擊下載的證書aps_development.cer,雙擊後,證書就自動導入到鑰匙串中了。
打開 keychain,左側鑰匙串選擇「登陸」,種類選擇「全部項目」,在右側窗口中,選中剛纔導入的Apple Development IOS Push Services證書(不用選中專用密鑰),右鍵,選擇導出,命名爲:ck.p12 ,存儲時,會提示輸入保護密碼,這裏爲演示方便,就輸入了123456。以後又會要求輸入電腦登陸密碼,輸入便可。
最後,打開終端,執行如下命令,生成pem文件
openssl pkcs12 -in ck.p12 -out ck.pem -nodes
執行時,會要求輸入導入密碼,這裏輸入剛纔的保護密碼123456便可。
到此,php 服務端使用的pem證書就製做完畢了。
Development PP 文件製做再也不贅述。
首先,在AppDelegate.m 中:
[objc] view plaincopyprint?
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
ViewController *mainCtrl=[[ViewController alloc] init];
self.window.rootViewController=mainCtrl;
//註冊通知
if ([UIDevice currentDevice].systemVersion.doubleValue<8.0) {
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeBadge)];
}
else {
[[UIApplication sharedApplication] registerForRemoteNotifications];
[[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge|UIUserNotificationTypeSound|UIUserNotificationTypeAlert categories:nil]];
}
//判斷是否由遠程消息通知觸發應用程序啓動
if (launchOptions) {
//獲取應用程序消息通知標記數(即小紅圈中的數字)
NSInteger badge = [UIApplication sharedApplication].applicationIconBadgeNumber;
if (badge>0) {
//若是應用程序消息通知標記數(即小紅圈中的數字)大於0,清除標記。
badge--;
//清除標記。清除小紅圈中數字,小紅圈中數字爲0,小紅圈纔會消除。
[UIApplication sharedApplication].applicationIconBadgeNumber = badge;
NSDictionary *pushInfo = [launchOptions objectForKey:@"UIApplicationLaunchOptionsRemoteNotificationKey"];
//獲取推送詳情
NSString *pushString = [NSString stringWithFormat:@"%@",[pushInfo objectForKey:@"aps"]];
UIAlertView *alert=[[UIAlertView alloc] initWithTitle:@"finish Loaunch" message:pushString delegate:nil cancelButtonTitle:@"cancel" otherButtonTitles:nil, nil nil];
[alert show];
}
}
return YES;
}
[objc] view plaincopyprint?
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSString *token = [NSString stringWithFormat:@"%@", deviceToken];
NSLog(@"My token is:%@", token);
//這裏應將device token發送到服務器端
}
- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
NSString *error_str = [NSString stringWithFormat: @"%@", error];
NSLog(@"Failed to get token, error:%@", error_str);
}
[objc] view plaincopyprint?
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
[UIApplication sharedApplication].applicationIconBadgeNumber=0;
for (id key in userInfo) {
NSLog(@"key: %@, value: %@", key, [userInfo objectForKey:key]);
}
/* eg.
key: aps, value: {
alert = "\U8fd9\U662f\U4e00\U6761\U6d4b\U8bd5\U4fe1\U606f";
badge = 1;
sound = default;
}
*/
UIAlertView *alert=[[UIAlertView alloc] initWithTitle:@"remote notification" message:userInfo[@"aps"][@"alert"] delegate:nil cancelButtonTitle:@"cancel" otherButtonTitles:nil, nil nil];
[alert show];
}
注意:app 前臺運行時,會調用 remote notification;app後臺運行時,點擊提醒框,會調用remote notification,點擊app 圖標,不調用remote notification,沒反應;app 沒有運行時,點擊提醒框,finishLaunching 中,launchOptions 傳參,點擊app 圖標,launchOptions 不傳參,不調用remote notification。
此章不在IOS程序員職責範圍以內,故只給出示例代碼,不作深刻討論。
[php] view plaincopyprint?
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>APNS</title>
</head>
<body>
<?php
/**
* @file apns.php
* @synopsis apple APNS class
* @author Yee, <rlk002@gmail.com>
* @version 1.0
* @date 2012-09-17 11:27:59
*/
class APNS
{
const ENVIRONMENT_PRODUCTION = 0;
const ENVIRONMENT_SANDBOX = 1;
const DEVICE_BINARY_SIZE = 32;
const CONNECT_RETRY_INTERVAL = 1000000;
const SOCKET_SELECT_TIMEOUT = 1000000;
const COMMAND_PUSH = 1;
const STATUS_CODE_INTERNAL_ERROR = 999;
const ERROR_RESPONSE_SIZE = 6;
const ERROR_RESPONSE_COMMAND = 8;
const PAYLOAD_MAXIMUM_SIZE = 256;
const APPLE_RESERVED_NAMESPACE = 'aps';
protected $_environment;
protected $_providerCertificateFile;
protected $_rootCertificationAuthorityFile;
protected $_connectTimeout;
protected $_connectRetryTimes = 3;
protected $_connectRetryInterval;
protected $_socketSelectTimeout;
protected $_hSocket;
protected $_deviceTokens = array();
protected $_text;
protected $_badge;
protected $_sound;
protected $_customProperties;
protected $_expiryValue = 604800;
protected $_customIdentifier;
protected $_autoAdjustLongPayload = true;
protected $asurls = array('ssl://gateway.push.apple.com:2195','ssl://gateway.sandbox.push.apple.com:2195');
protected $_errorResponseMessages = array
(
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',
self::STATUS_CODE_INTERNAL_ERROR => 'Internal error'
);
function __construct($environment,$providerCertificateFile)
{
if($environment != self::ENVIRONMENT_PRODUCTION && $environment != self::ENVIRONMENT_SANDBOX)
{
throw new Exception(
"Invalid environment '{$environment}'"
);
}
$this->_environment = $environment;
if(!is_readable($providerCertificateFile))
{
throw new Exception(
"Unable to read certificate file '{$providerCertificateFile}'"
);
}
$this->_providerCertificateFile = $providerCertificateFile;
$this->_connectTimeout = @ini_get("default_socket_timeout");
$this->_connectRetryInterval = self::CONNECT_RETRY_INTERVAL;
$this->_socketSelectTimeout = self::SOCKET_SELECT_TIMEOUT;
}
public function setRCA($rootCertificationAuthorityFile)
{
if(!is_readable($rootCertificationAuthorityFile))
{
throw new Exception(
"Unable to read Certificate Authority file '{$rootCertificationAuthorityFile}'"
);
}
$this->_rootCertificationAuthorityFile = $rootCertificationAuthorityFile;
}
public function getRCA()
{
return $this->_rootCertificationAuthorityFile;
}
protected function _connect()
{
$sURL = $this->asurls[$this->_environment];
$streamContext = stream_context_create(
array
(
'ssl' => array
(
'verify_peer' => isset($this->_rootCertificationAuthorityFile),
'cafile' => $this->_rootCertificationAuthorityFile,
'local_cert' => $this->_providerCertificateFile
)
)
);
$this->_hSocket = @stream_socket_client($sURL,$nError,$sError,$this->_connectTimeout,STREAM_CLIENT_CONNECT, $streamContext);
if (!$this->_hSocket)
{
throw new Exception
(
"Unable to connect to '{$sURL}': {$sError} ({$nError})"
);
}
stream_set_blocking($this->_hSocket, 0);
stream_set_write_buffer($this->_hSocket, 0);
return true;
}
public function connect()
{
$bConnected = false;
$retry = 0;
while(!$bConnected)
{
try
{
$bConnected = $this->_connect();
}catch (Exception $e)
{
if ($nRetry >= $this->_connectRetryTimes)
{
throw $e;
}else
{
usleep($this->_nConnectRetryInterval);
}
}
$retry++;
}
}
public function disconnect()
{
if (is_resource($this->_hSocket))
{
return fclose($this->_hSocket);
}
return false;
}
protected function getBinaryNotification($deviceToken, $payload, $messageID = 0, $Expire = 604800)
{
$tokenLength = strlen($deviceToken);
$payloadLength = strlen($payload);
$ret = pack('CNNnH*', self::COMMAND_PUSH, $messageID, $Expire > 0 ? time() + $Expire : 0, self::DEVICE_BINARY_SIZE, $deviceToken);
$ret .= pack('n', $payloadLength);
$ret .= $payload;
return $ret;
}
protected function readErrorMessage()
{
$errorResponse = @fread($this->_hSocket, self::ERROR_RESPONSE_SIZE);
if ($errorResponse === false || strlen($errorResponse) != self::ERROR_RESPONSE_SIZE)
{
return;
}
$errorResponse = $this->parseErrorMessage($errorResponse);
if (!is_array($errorResponse) || empty($errorResponse))
{
return;
}
if (!isset($errorResponse['command'], $errorResponse['statusCode'], $errorResponse['identifier']))
{
return;
}
if ($errorResponse['command'] != self::ERROR_RESPONSE_COMMAND)
{
return;
}
$errorResponse['timeline'] = time();
$errorResponse['statusMessage'] = 'None (unknown)';
if (isset($this->_aErrorResponseMessages[$errorResponse['statusCode']]))
{
$errorResponse['statusMessage'] = $this->_errorResponseMessages[$errorResponse['statusCode']];
}
return $errorResponse;
}
protected function parseErrorMessage($errorMessage)
{
return unpack('Ccommand/CstatusCode/Nidentifier', $errorMessage);
}
public function send()
{
if (!$this->_hSocket)
{
throw new Exception
(
'Not connected to Push Notification Service'
);
}
$sendCount = $this->getDTNumber();
$messagePayload = $this->getPayload();
foreach($this->_deviceTokens AS $key => $value)
{
$apnsMessage = $this->getBinaryNotification($value, $messagePayload, $messageID = 0, $Expire = 604800);
$nLen = strlen($apnsMessage);
$aErrorMessage = null;
if ($nLen !== ($nWritten = (int)@fwrite($this->_hSocket, $apnsMessage)))
{
$aErrorMessage = array
(
'identifier' => $key,
'statusCode' => self::STATUS_CODE_INTERNAL_ERROR,
'statusMessage' => sprintf('%s (%d bytes written instead of %d bytes)',$this->_errorResponseMessages[self::STATUS_CODE_INTERNAL_ERROR], $nWritten, $nLen)
);
}
}
}
public function addDT($deviceToken)
{
if (!preg_match('~^[a-f0-9]{64}$~i', $deviceToken))
{
throw new Exception
(
"Invalid device token '{$deviceToken}'"
);
}
$this->_deviceTokens[] = $deviceToken;
}
public function getDTNumber()
{
return count($this->_deviceTokens);
}
public function setText($text)
{
$this->_text = $text;
}
public function getText()
{
return $this->_text;
}
public function setBadge($badge)
{
if (!is_int($badge))
{
throw new Exception
(
"Invalid badge number '{$badge}'"
);
}
$this->_badge = $badge;
}
public function getBadge()
{
return $this->_badge;
}
public function setSound($sound = 'default')
{
$this->_sound = $sound;
}
public function getSound()
{
return $this->_sound;
}
public function setCP($name, $value)
{
if ($name == self::APPLE_RESERVED_NAMESPACE)
{
throw new Exception
(
"Property name '" . self::APPLE_RESERVED_NAMESPACE . "' can not be used for custom property."
);
}
$this->_customProperties[trim($name)] = $value;
}
protected function _getPayload()
{
$aPayload[self::APPLE_RESERVED_NAMESPACE] = array();
if (isset($this->_text))
{
$aPayload[self::APPLE_RESERVED_NAMESPACE]['alert'] = (string)$this->_text;
}
if (isset($this->_badge) && $this->_badge > 0)
{
$aPayload[self::APPLE_RESERVED_NAMESPACE]['badge'] = (int)$this->_badge;
}
if (isset($this->_sound))
{
$aPayload[self::APPLE_RESERVED_NAMESPACE]['sound'] = (string)$this->_sound;
}
if (is_array($this->_customProperties))
{
foreach($this->_customProperties as $propertyName => $propertyValue)
{
$aPayload[$propertyName] = $propertyValue;
}
}
return $aPayload;
}
public function setExpiry($expiryValue)
{
if (!is_int($expiryValue))
{
throw new Exception
(
"Invalid seconds number '{$expiryValue}'"
);
}
$this->_expiryValue = $expiryValue;
}
public function getExpiry()
{
return $this->_expiryValue;
}
public function setCustomIdentifier($customIdentifier)
{
$this->_customIdentifier = $customIdentifier;
}
public function getCustomIdentifier()
{
return $this->_customIdentifier;
}
public function getPayload()
{
$sJSONPayload = str_replace
(
'"' . self::APPLE_RESERVED_NAMESPACE . '":[]',
'"' . self::APPLE_RESERVED_NAMESPACE . '":{}',
json_encode($this->_getPayload())
);
$nJSONPayloadLen = strlen($sJSONPayload);
if ($nJSONPayloadLen > self::PAYLOAD_MAXIMUM_SIZE)
{
if ($this->_autoAdjustLongPayload)
{
$maxTextLen = $textLen = strlen($this->_text) - ($nJSONPayloadLen - self::PAYLOAD_MAXIMUM_SIZE);
if ($nMaxTextLen > 0)
{
while (strlen($this->_text = mb_substr($this->_text, 0, --$textLen, 'UTF-8')) > $maxTextLen);
return $this->getPayload();
}else
{
throw new Exception
(
"JSON Payload is too long: {$nJSONPayloadLen} bytes. Maximum size is " .
self::PAYLOAD_MAXIMUM_SIZE . " bytes. The message text can not be auto-adjusted."
);
}
}else
{
throw new Exception
(
"JSON Payload is too long: {$nJSONPayloadLen} bytes. Maximum size is " .
self::PAYLOAD_MAXIMUM_SIZE . " bytes"
);
}
}
return $sJSONPayload;
}
}
?>
<?php
date_default_timezone_set('PRC');
echo "we are young,test apns. -".date('Y-m-d h:i:s',time());
$rootpath = 'entrust_root_certification_authority.pem'; //ROOT證書地址
$cp = 'ck.pem'; //provider證書地址
$apns = new APNS(1,$cp);
try
{
//$apns->setRCA($rootpath); //設置ROOT證書
$apns->connect(); //鏈接
$apns->addDT('acc5150a4df26507a84f19ba145ca3c1be5842a6177511ce7c43d01badb1bd96'); //加入deviceToken
$apns->setText('這是一條測試信息'); //發送內容
$apns->setBadge(1); //設置圖標數
$apns->setSound(); //設置聲音
$apns->setExpiry(3600); //過時時間
$apns->setCP('custom operation',array('type' => '1','url' => 'http://www.google.com.hk')); //自定義操做
$apns->send(); //發送
echo ' sent ok';
}catch(Exception $e)
{
echo $e;
}
?>
</body>
</html>
mac 自帶apache,可直接運行php。
打開「終端(terminal)」,輸入 sudo apachectl -v,可顯示Apache的版本;
輸入 sudo apachectl start,這樣Apache就啓動了。
編輯文件 /etc/apache2/httpd.conf , 把 LoadModule php5_module libexec/apache2/libphp5.so 前面的註釋去掉;而後重啓apache: sudo apachectl restart
打開Safari瀏覽器地址欄輸入 「http://localhost」,能夠看到內容爲「It works!」的頁面。其位
於「/Library/WebServer/Documents/」下,這就是Apache的默認根目錄。
將服務器端寫好的apns.php 文件以及生成的 ck.pem 文件,直接拷貝到 /Library/WebServer/Documents/ 下,在瀏覽器中,直接瀏覽: http://localhost/apns.php 。這樣消息就發送到了蘋果服務器。