远程服务器推送
本文为博主原创文章,未经博主允许不得转载。
一、背景概述:
1,环境配置
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 下载地址:点击打开链接
2,APNS 相关博客
如对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 相关总结(推荐)
3,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文件
1,新建一个App ID
2,配置push开发证书
- 在mac电脑上,打开应用程序 keychain(钥匙串访问);
- 在keychain菜单栏中,依次选择“钥匙串访问”=》“证书助理”=》“从证书颁发机构请求证书”;
- 在新打开的“证书助理”界面中,填写用户电子邮件地址,常用名称,CA电子邮件地址,这两个邮件地址直接填写你的苹果账号的邮件地址即可,然后选择“存储到磁盘”,然后点击“继续”;
- 选择CSR文件保存位置,“存储”即可。至此, CSR 文件,制作完成。
3,生成PEM文件
openssl pkcs12 -in ck.p12 -out ck.pem -nodes
执行时,会要求输入导入密码,这里输入刚才的保护密码123456即可。
到此,php 服务端使用的pem证书就制作完毕了。
Development PP 文件制作不再赘述。
三、IOS 代码编写
首先,在AppDelegate.m 中:
1,注册通知
- - (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;
- }
2,注册通知后,获取device token
- - (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);
- }
3,接收推送通知
- - (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。
四、服务器端代码编写
1,php 源码:
- <!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>
2,启动 Apache
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的默认根目录。