最近做一个项目,主要功能是语音实时对讲。里面要到 voip push能力拉活。将实现push功能的过程总节一下,以作备忘。期间当然参考了不少资料,一一列在文章尾部。
模拟器不支持推送,所以首选需要一台苹果设备,iphone或ipad
我们的客户端与苹果服务器之间和我们自己的服务器与苹果服务器之间都需要证书来进行链接。下面我们来开始进入证书的制作过程。
首先们要有生成一个Certificate Signing Request(CSR)的请求文件
在应用程序里的使用工具中找到钥匙串访问
。
填上你的邮箱和常用名,常用名要记一下,一会会用到。然后选择保存到磁盘,继续
选择继续将会提示选择保存的位置,我保存在桌面
到这里点击完成后我们会在桌面上看到一个CertificateSigningRequest.certSigningRequest
的请求文件,也就是我们说的CSR文件。在我们生成CSR文件的同时,会在钥匙串访问中生成一对秘钥,名称为刚才我们填写的常用名
用付费账号登录到:http://developer.apple.com/iphone/index.action
在“ Identifiers”一栏下选择“App IDs”,可查看所有已申请的App IDs,点击右上“+”
进入Register iOS App ID界面,在“App ID Description”栏下的“Name”项中输入名称
在“Explicit App ID”栏下的“Bundle ID”项中输入App ID(反域名格式,如:com.company.test)
这里“Bundle ID”对应Xcode中的“Bundle identifier”
Explicit App ID:唯一的App ID,用于唯一标识一个应用程序。
注:
由于要申请Push,所以不能通配符
在“App Services”栏下选择应用要使用到的服务(如要使用推送功能,勾选“Push Notifications”)
iOS证书是用来证明iOS App内容(executable code)的合法性和完整性的数字证书。对于想安装到真机或发布到AppStore的应用程序(App),只有经过签名验证(Signature Validated)才能确保来源可信,并且保证App内容是完整、未经篡改的。
iOS证书分两种:开发证书(Development)和生产证书(Production)。
点击”+”号创建证书
证书类型选择”Apple Push Notification service SSL (Sandbox)”
此处需要选择一个App id,此处选择的appid不能出现通配符
上传之前制作的CSR文件
下载证书
下载证书,双击导入Keychain Access,可在Keychain Access->“证书”中查看 如下图所未
下载后证书取名”aps_development.cer”
打开Keychain Access,选择安装成功的证书,右键选择“导出”
输入名字,默认格式为.p12类型,选择“Save”,假设保存为aps_development.p12
设置密码,点击确认
注:
这个密码后面还使用到。
现在我们的准备工作已经做完了。要开始对生成的文件进行处理了,因为我们的服务链接苹果服务器也是需要证书的,但是我们直接生成的证书windows系统(我们一般的服务器都是win系统的)是不识别的,所以我们需要生成一个后缀为pem的带证书带秘钥的文件。
openssl x509 -in aps_development.cer -inform DER -out PushCert.pem -outform PEM
openssl pkcs12 -nocerts -out PushKey.pem -in aps_development.p12
此部份中将会要求输入密码,密码为前面保存aps_development.p12时所设置的密码
同时设置PushVoipKey.pem的密码
将上面的PushKey.pem和PushCert.pem交给nodejs后台服务端,服务端就可以发送push消息给APNS了。这时服务端已经可以工作了,但客户端还必须配置相应的Provisioning Profile才能启动应用的push功能。
服务器配置需要注意的是,由于我们生成的是开发环境的push证书,所以服务器应该连接APNS的sandbox环境地址:gateway.sandbox.push.apple.com:2195, 如果是应用正式发布,就需要连接正式环境,必须生成相应的发布证书,并连接APNS正式环境地址:gateway.push.apple.com:2195
telnet gateway.sandbox.push.apple.com 2195
它将尝试发送一个规则的,不加密的连接到APNS服务。如果你看到上面的反馈,那说明你的MAC能够到达APNS。按下Ctrl+C关闭连接。如果得到一个错误信息,那么你需要确保你的防火墙允许2195端口。一般这里都不会出现什么问题。
下面我们要使用我们生成的SSL证书和私钥来设置一个安全的链接去链接苹果服务器:
openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert PushCert.pem -key PushKey.pem
执行完这一句后会提示输入private_key.pem的密码
Enter pass phrase for private_key.pem:
你会看到一个完整的输出,让你明白OpenSSL在后台做什么。如果链接是成功的,你可以随便输入一个字符,按下回车,服务器就会断开链接,如果建立连接时有问题,OpenSSL会给你返回一个错误信息。
当你在最后的时候你看到这样说明你已经成功了:
在AppDelegate里didFinishLaunchingWithOptions
函数里注册
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
let center = UNUserNotificationCenter.current()
center.
(options: [.alert, .sound, .badge]) { (granted, error) in
// Enable or disable features based on authorization.
if granted == true
{
print("Allow")
UIApplication.shared.registerForRemoteNotifications()
}
else
{
print("Don't Allow")
}
}
return true;
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
var token = ""
for i in 0..token = token + String(format: "%02.2hhx", arguments: [deviceToken[i]])
}
print("didRegisterForRemoteNotificationsWithDeviceToken deviceToken = \(token)")
}
func application(_ application: UIApplication,didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
NSLog("Payload: %@", userInfo);
}
服务器配置好证书并拿到deviceToken后就可以向APNS发送消息了。发送消息的格式如下图所示:
Payload就是push的消息负载,这就是应用需要关心的数据。Payload是一个JSON字典,最大值是256字节,超过这个限制,APNS将拒绝转发。基本格式如下:
{
"aps": {
"alert":"Hello Push!",
"badge":1,
"sound":"default"
}
}
必须包含aps
键值。badge
表示应用程序图标显示数字, sound
表示收到push的提示音。Payload的具体格式参考Apple Push Notification Service。要在结构中添加自定义数据,加在aps空间之外。
示例:
{
"aps": {
"alert":"Hello Push!",
"badge":1,
"sound":"default"
},
"page":"home"
}
-通过工具模拟推送(NWPusher)
Github 上面有位大神分享了他的推送工具NWPusher ,大大减少了开发人员的工作量。具体用法说明上面写的很清楚,这里就不再重复。
-在线测试工具 http://pushtry.com/
// Put your device token here (without spaces):
$deviceToken = 'xxx';
// Put your private key's passphrase here:密语
$passphrase = 'password';
// Put your alert message here:
$message = '推送消息';
////////////////////////////////////////////////////////////////////////////////
$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'local_cert', 'ck.pem');
stream_context_set_option($ctx, 'ssl', 'cafile', 'entrust_2048_ca.cer');
//如果此处不加这个证书会报后面出现的错误:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
//此证书的下载地址:https://www.entrust.com/get-support/ssl-certificate-support/root-certificate-downloads/
stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase);
// Open a connection to the APNS server
$fp = stream_socket_client(
'ssl://gateway.sandbox.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);
// 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)
echo 'Message not delivered' . PHP_EOL;
else
echo 'Message successfully delivered' . PHP_EOL;
// Close the connection to the server
fclose($fp);
?>
$ php pushMe.php
遇到以下错误
Warning: stream_socket_client(): SSL operation failed with code 1. OpenSSL Error messages:
error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed in /Users/hefei/workspace/ptt/ptt_ios/Certificates/pushMe.php on line 21
Warning: stream_socket_client(): Failed to enable crypto in /Users/hefei/workspace/ptt/ptt_ios/Certificates/pushMe.php on line 21
Warning: stream_socket_client(): unable to connect to ssl://gateway.sandbox.push.apple.com:2195 (Unknown error) in /Users/hefei/workspace/ptt/ptt_ios/Certificates/pushMe.php on line 21
Failed to connect: 0
解决方法
下载证书entrust_2048_ca.cer
apn是nodejs里发送ios apn push的一个工具库,使用它可以简化ios push发送
使用npm install安装
$ npm install apn
或在packge.json里添加依赖
"dependencies": {
"apn": "^2.0"
}
...
const apn = require("apn");
...
sendMessageToIOS(tokens, msg){
const self = this;
let service = new apn.Provider({
cert: "certs/PushCert.pem",
key: "certs/PushKey.pem",
passphrase:"xxx", //ios push 证书密码
production: false //true使用ios正式环境push通道
});
const payload = {"msg": "Hello World!"};
_.forEach(tokens, (token) => {
let note = new apn.Notification();
try {
note.payload = payload;
// The topic is usually the bundle identifier of your application.
log.info(`Sending: ${note.compile()} to ${token}`);
service.send(note, token).then( result => {
log.info("sent:", result.sent.length);
log.info("failed:", result.failed.length);
log.info(result.failed);
});
}catch(e) {
log.info("sendMessageToIOS failed for token = ", token)
}
});
// For one-shot notification tasks you may wish to shutdown the connection
// after everything is sent, but only call shutdown if you need your
// application to terminate.
service.shutdown();
}
创建生成证书的流程与生成普通push的流程一样
只是在选择证书时选择”Voip Services Certificate”
个性info.plist添加一项Required background modes
代码中请求push能力
方法与前面普通push请求一样。在AppDelegate里didFinishLaunchingWithOptions
函数里注册
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
// Enable or disable features based on authorization.
if granted == true
{
NSLog("Allow")
self.registerForVoIPPushes()
}
else
{
NSLog("Don't Allow")
}
}
return true;
}
上面的registerForVoIPPushes
方法实现如下
func registerForVoIPPushes() {
NSLog("registerForVoiPPushes")
self.voipRegistry = PKPushRegistry(queue: nil)
self.voipRegistry!.delegate = self
self.voipRegistry!.desiredPushTypes = [PKPushType.voIP]
}
获取voip device token
VOIP所使用token跟普通push所使用的token不同,需要单独获取
在PKPushRegistryDelegate
里的回调方法pushRegistry didUpdate
中获取tokden。将获取 到的token注册到后台server,以后自己的server就可以根据需要通过token向客户端发送消息了。
public func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, forType type: PKPushType){
var token = ""
for i in 0.."%02.2hhx", arguments: [credentials.token[i]])
}
...
NSLog("AppDelegate.pushRegistry didUpdate voip token = \(token)")
//register the token to server
}
接收voip push消息
public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType)
{
if UIApplication.shared.applicationState == UIApplicationState.background {
NSLog("incoming notificaiton from background")
let localNotification = UILocalNotification();
localNotification.alertBody = message
localNotification.applicationIconBadgeNumber = 1;
localNotification.soundName = UILocalNotificationDefaultSoundName;
UIApplication.shared.presentLocalNotificationNow(localNotification);
}
else {
NSLog("incoming notificaiton from frontend")
DispatchQueue.main.async{
let alertController = UIAlertController(title: "Title", message: "This is UIAlertController default", preferredStyle: UIAlertControllerStyle.alert)
let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler: nil)
let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil)
alertController.addAction(cancelAction)
alertController.addAction(okAction)
UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
}
}
NSLog("incoming voip notfication: \(payload.dictionaryPayload)")
}