APNS:Apple Push Notification Service。本文对推送相关概念不再赘述,只侧重完整流程。
Demo 开发环境:Mac os 10.9.4 ,Xcode 6.0.1 ;测试设备:iphone 4s(ios 7.1)
服务端开发环境:mac 10.9.4 + php 5.4.24、
Demo 下载地址:点击打开链接
如对apns相关概念不清楚,可参考以下几个博客:(博客中部分内容重复,但总体来说,通读一遍,还是大有裨益的)
http://cshbbrain.iteye.com/blog/1859810 =》IOS 基于APNS消息推送原理与实现(JAVA后台)
http://www.cnblogs.com/qq78292959/archive/2012/07/16/2593651.html =》iOS消息推送机制的实现
http://blog.csdn.net/xunyn/article/details/8243573 =》APNS编程----iOS真机测试消息推送
http://blog.csdn.net/wswqiang/article/details/8208581 =》IOS APNS 处理
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
二、制作Push证书和Pem文件
openssl pkcs12 -in ck.p12 -out ck.pem -nodes
执行时,会要求输入导入密码,这里输入刚才的保护密码123456即可。
到此,php 服务端使用的pem证书就制作完毕了。
Development PP 文件制作不再赘述。
首先,在AppDelegate.m 中:
- (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]; [alert show]; } } return YES; }
- (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); }
- (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]; [alert show]; }
<!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, <[email protected]> * @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>
打开“终端(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的默认根目录。