How Can We Help?

使用 PortSIP PBX 12.0 在本地 iOS 应用中实施推送通知

You are here:
← All Topics

本指南详细指导您如何基于 PortSIP VoIP SDK 创建本地 iOS 应用,且该应用能够接收从 PortSIP PBX 12.0 发出的 VoIP 通知。

本手册支持最新的 PushKit 推送通知政策,该政策自 iOS 13 和 Xcode 11 开始实施。

从 iOS 13.0 开始,如果未向 CallKit 报告通话,系统会终止应用。多次未报告会导致系统不再向应用发送 VoIP 推送通知。如需在不使用 CallKit 的情况下发起 VoIP 通话,则需要使用 UserNotifications 框架替代 PushKit,用于注册 PUSH 通知。

PortPBX 12 利用 VoIP PUSH 推送 VoIP 呼叫,使用 PUSH 推送即时消息。

1. VoIP 通知

官方文档可在此查看。其中优势包括:

  • 仅当出现 VoIP 推送时才唤醒设备(节省电量)
  • 标准推送通知要求用户必须响应,然后应用才能执行某项操作;VoIP 推送则直接转到应用进行处理。
  • VoIP 推送被视为最高优先级的通知,会立即发送,不会产生延迟。
  • VoIP 推送包含的数据可多于标准推送通知提供的数据。
  • 收到 VoIP 推送时,如果应用未在运行,会自动重新启动。
  • 即使应用在后台运行,仍可以在运行时处理推送
2. 先决条件设置

Apple 提供了 PushKit 框架,可支持使用 VoIP 推送功能。但是,我们需要配置一些额外设置,以正常运行。

3. 创建应用 ID

如果您没有应用(以及对应的应用 ID),您需要创建一个 ID。首先,登录至  Apple developer account,并访问证书、标识和资料

iOS PUSH with PortSIP PBX

接下来,转至标识,然后单击 + 按钮。

此处需要填写两处重要内容:应用 ID 描述对应的资源包 ID(类似 com.yourdomain.yourappname):

选择“推送通知”。

上方截图未显示,我使用了 com.portsip.portsipvoipdemo 作为资源包 ID。该选项在下一步非常重要。

4. 为 VoIP 通话生成 VoIP 推送证书

单击左边证书部分的全部按钮,然后单击 + 按钮:

在下一页,您需要选择 VoIP 服务证书:

PortSIP VoIP SDK support mobile PUSH notifications

然后,您需要选择为之创建此 VoIP 证书的应用 ID:

接下来,您将看到如何选择 CSR(证书签名请求)文件的指示信息:

创建文件后,您需要在下一屏幕选择将其上传。如果操作顺利,您将获得需要下载的证书:

下载证书后,将其打开,Keychain Access 应用程序随即打开。现在您可在“我的证书”部分查看该证书:

5. 为即时通信生成 APNs 推送证书

单击左侧导航栏“证书”部分的“全部”按钮,然后单击 + 按钮:

在接下来的页面,您需要选择 Apple 推送通知服务 SSL(沙箱和生产):

然后,选择要为其创建 Apple 推送通知的应用 ID:

接下来,您将看到如何选择 CSR(证书签名请求)文件的指示信息:

创建文件后,您需要在下一屏幕选择将其上传。如果操作顺利,您将获得需要下载的证书:

下载证书后,将其打开,Keychain Access 应用程序随即打开。现在您可在“我的证书”部分查看该证书:

6. 向 PortSIP SIPSample 项目添加 VoIP 推送支持

我们的最新版 SIPSample 已支持该功能,请从官网下载最新版 SIPSample。

在设置产品名称时要特别小心,因为资源包标识符是自动根据它设置的。我们需要将其设置为在上面的步骤中设置的同一资源包标识符。

7. 设置对应功能

在项目的“签名和功能”选项卡,添加“推送通知”和“后台模式”。确保已启用“音频、AirPlay 和画中画”、“VoIP 技术”和“远程通知”选项。

8. 添加代码

打开 AppDelegate.m,在其顶部添加导入 PushKit 和 UserNotifications 语句。

#import <PushKit/PushKit.h>

#import <UserNotifications/UserNotifications.h> 

@interface AppDelegate ()<PKPushRegistryDelegate,UNUserNotificationCenterDelegate> 

@end

接下来,确保已在应用程序函数的 didFinishLaunchingWithOptions 部分注册了通知,如下所示:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Register VoIP PUSH
PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:nil];
pushRegistry.delegate = self;
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];

// Register APNs PUSH
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0) {
// iOS > 10
UNUserNotificationCenter *center =
[UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
[center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge |
UNAuthorizationOptionSound |
UNAuthorizationOptionAlert)
completionHandler:^(BOOL granted,
NSError *_Nullable error) {

if (!error) {
NSLog(@"request User Notification succeeded!");
}

}];
} else { // iOS 8-10
if ([UIApplication instancesRespondToSelector:
@selector(registerUserNotificationSettings:)]) {
[[UIApplication sharedApplication]
registerUserNotificationSettings:
[UIUserNotificationSettings
settingsForTypes:UIUserNotificationTypeAlert |
UIUserNotificationTypeBadge |
UIUserNotificationTypeSound
categories:nil]];
}
}

// Calling this will result in either
// application:didRegisterForRemoteNotificationsWithDeviceToken: or
// application:didFailToRegisterForRemoteNotificationsWithError: to be called
// on the application delegate.
[application registerForRemoteNotifications];
return YES;
}

我们需要为其实施委托回调函数 didRegisterUserNotificationSettings:

- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings

{

PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:nil];

pushRegistry.delegate = self;

pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];

}

此时,您会从 pushRegistry.delegate = self; 收到一个错误,告知无法为类型“PKPushRegistryDelegate!”指定类型值“AppDelegate”。

pushRegistry 的委托函数类型为 PKPushRegistryDelegate,共有三种方法,其中两种方法是必须的。(didUpdatePushCredentials 和 didReceiveIncomingPushWithPayload)。我们需要定义一个类别为 AppDelegate 的分机,需要通过将以下代码添加至 AppDelegate.m 文件现有代码后:

APNs methods:
#pragma mark - APNs message PUSH
- (NSString *)stringFromDeviceToken:(NSData *)deviceToken {
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 13.0) {
NSUInteger length = deviceToken.length;
if (length == 0) {
return nil;
}
const unsigned char *buffer = deviceToken.bytes;
NSMutableString *hexString =
[NSMutableString stringWithCapacity:(length * 2)];
for (int i = 0; i < length; ++i) {
[hexString appendFormat:@"%02x", buffer[i]];
}
return [hexString copy];
} else {
NSString *token = [NSString stringWithFormat:@"%@", deviceToken];

token =
[token stringByTrimmingCharactersInSet:
[NSCharacterSet characterSetWithCharactersInString:@"<>"]];

return [token stringByReplacingOccurrencesOfString:@" " withString:@""];
}
}

- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {

_APNsPushToken = [self stringFromDeviceToken:deviceToken];
NSLog(@"_APNsPushToken :%@", deviceToken);
[self refreshPushStatusToSipServer:YES];
}
// 8.0 < iOS version < 10.0
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:
(void (^)(UIBackgroundFetchResult))completionHandler {

NSLog(@"didReceiveRemoteNotification %@", userInfo);

completionHandler(UIBackgroundFetchResultNewData);
}

// iOS version > 10.0 Background
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(nonnull UNNotificationResponse *)response
withCompletionHandler:(nonnull void (^)(void))completionHandler {

NSDictionary *userInfo = response.notification.request.content.userInfo;
NSLog(@"Background Notification:%@", userInfo);

completionHandler();
}

// iOS version > 10.0 foreground
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:
(void (^)(UNNotificationPresentationOptions))completionHandler {

NSDictionary *userInfo = notification.request.content.userInfo;
NSLog(@"Foreground Notification:%@", userInfo);

completionHandler(UNNotificationPresentationOptionBadge);
}

VoIP PUSH methods:
- (void)pushRegistry:(PKPushRegistry *)registry
didUpdatePushCredentials:(PKPushCredentials *)credentials
forType:(PKPushType)type {
_VoIPPushToken = [self stringFromDeviceToken:credentials.token];

NSLog(@"didUpdatePushCredentials:%@", _VoIPPushToken);
[self refreshPushStatusToSipServer:YES];
}

// iOS version > 11.0
- (void)pushRegistry:(PKPushRegistry *)registry
didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
forType:(PKPushType)type
withCompletionHandler:(void (^)(void))completion {
[self processPushMessageFromPortPBX:payload.dictionaryPayload
withCompletionHandler:completion];
}

// 8.0 < iOS version < 11.0
(void)pushRegistry:(PKPushRegistry *)registry
didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
forType:(PKPushType)type {
[self processPushMessageFromPortPBX:payload.dictionaryPayload
withCompletionHandler:nil];
}

添加此分机后,您会注意到先前提及的错误已消失。

在第一个函数中,我们仅需输出设备令牌。接下来部分中通过发送 VoIP 推送通知测试应用时需要使用该令牌。

在第二个函数,我们“采取行动”收到 VoIP 推送通知。自 iOS 13.0 起,我们必须向 CallKit 报告每一个呼叫。第三个函数 (didInvalidatePushTokenForType) 处于处理令牌失效时的状况。

我们需要告知 PortSIP,客户端已通过向 REGISTER 消息添加 SIP 消息头“x-p-push”启用推送。

- (void)addPushSupportWithPortPBX:(BOOL)enablePush {
if (_VoIPPushToken == nil || _APNsPushToken == nil ||
!_enablePushNotification)
return;

// This VoIP Push is only work with
// PortPBX(https://www.portsip.com/portsip-pbx/)
// if you want work with other PBX, please contact your PBX Provider
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
[portSIPSDK clearAddedSipMessageHeaders];
NSString *pushMessage;
NSString *token = [[NSString alloc]

initWithFormat:@"%@|%@", _VoIPPushToken, _APNsPushToken];
if (enablePush) {
pushMessage = [[NSString alloc]
initWithFormat:@"device-os=ios;device-uid=%@;allow-call-push=true;"
@"allow-message-push=true;app-id=%@",
token, bundleIdentifier];

NSLog(@"Enable pushMessage:{%@}", pushMessage);
} else {
pushMessage = [[NSString alloc]
initWithFormat:@"device-os=ios;device-uid=%@;allow-call-push=false;"
@"allow-message-push=false;app-id=%@",
token, bundleIdentifier];
NSLog(@"Disable pushMessage:{%@}", pushMessage);
}

[portSIPSDK addSipMessageHeader:-1
methodName:@"REGISTER"
msgType:1

headerName:@"x-p-push"
headerValue:pushMessage];
}

应用收到推送或应用正在运行时,会自动注册到服务器:

- (void)doAutoRegister
{
if([_textUsername.text length] > 1 &&
[_textPassword.text length] > 1 &&
[_textSIPserver.text length] > 1 &&
[_textSIPPort.text length] > 1 &&
[_textToken.text length] > 1){
[self onLine];
}
}

9. 准备证书文件

我们下载并添加至 KeyChain 的 VoIP 证书文件已转换为其他文件格式,因此我们无法使用我上面所述的工具和服务将其打开。

首先,您需要在 Mac 上打开 KeyChain 应用,并导出 VoIP 服务证书(右键单击,然后选择“导出”):

您将获得一个 YOUR_CERT.p12(例如 pvoip_push.p12)文件。现在导出该证书密钥文件。

现在,浏览到您将该文件到处置的文件夹,并执行以下命令:

使用 VoIP 服务证书创建推送凭证:

$> openssl pkcs12 -in voip_push.p12 -nocerts -out voip_push_key.pem
Enter Import Password: (Enter the password when you export form Keychain)
MAC verified OK
Enter PEM pass phrase: (Must Enter the password, e.g:1234)
Verifying - Enter PEM pass phrase: (Must Enter the above password, e.g:1234)
$> openssl rsa -in voip_push_key.pem -out voip_push_key_nopws.pem
Enter pass phrase for voip_push_key.pem: (Must Enter the above password, e.g:1234)

$> openssl pkcs12 -in voip_push.p12 -clcerts -nokeys -out voip_push.pem

使用 Apple 推送服务证书创建推送凭证:

$> openssl pkcs12 -in apns_push.p12 -nocerts -out apns_push_key.pem
Enter Import Password: (Enter the password when you export form Keychain, empty is allowed)
MAC verified OK
Enter PEM pass phrase: (Must Enter the password, e.g:1234)
Verifying - Enter PEM pass phrase: (Must Enter the above password, e.g:1234)

$> openssl rsa -in apns_push_key.pem -out apns_push_key_nopws.pem
Enter pass phrase for apns_push_key.pem: (Must Enter the above password, e.g:1234)
$> openssl pkcs12 -in apns_push.p12 -clcerts -nokeys -out apns_push.pem

我们需要将两个推送凭证合并为一个文件:

$> cat voip_push.pem apns_push.pem > portpbx_push.pem
$> cat voip_push_key_nopws.pem apns_push_key_nopws.pem > portpbx_push_key.pem

该操作将生成 portpbx_push.pemportpbx_push_key.pem 文件,我们后续将在 PBX 服务器中使用。

10. Houston

Houston 让我们能够从终端窗口发送推送通知以进行测试。

我们下载并添加至 KeyChain 的 VoIP 证书文件已转换为其他文件格式,因此我们无法使用我上面所述的工具和服务将其打开。尽管文档提示说您只需通过 gem 即可安装 Houston,您最终(在进行一些 StackOverflow 搜索后)可能还是需要使用命令进行安装:

sudo gem install -n /usr/local/bin houston

通过该方法,您可以将其安装至您具有完整权限的本地 bin 目录。
Houston 安装了另一个工具,可帮助我们通过以下方式发送通知:
通过终端浏览至您的证书储存的文件夹:

测试 APNs PUSH:

$> openssl pkcs12 -in apns_push.p12 -out apnspush_onlyone.pem -nodes -clcerts

APP didUpdatePushCredentials 复制设备令牌,然后执行以下命令:

apn push "<40cc4209 d0f3ac25 95a7e937 3282897b 211231ef ba66764c 6fd2befa b42076cb>" -c portgo2in1.pem -m "Testing VoIP notifications!"

您将在终端收到以下输出:

1 push notification sent successfully

如果该应用正在前端运行,您将在手机中看到一条 APP didReceiveIncomingPushWithPayload 推送信息。

如果您偏好 UI 方式,可下载 Apple 推送通知服务 (APNs) 调试应用程序 Knuff

11. PortSIP PBX

现在,登录至 PortSIP PBX 12.0 管理控制台,选择菜单“设置”>“移动推送消息”。

单击“添加新应用”按钮,然后您可看到以下屏幕:

PortSIP PBX send PUSH notifications to mobile device

请设置以下项:

  1. 已启用 - 选中以启动推送,取消选中即禁用推送。
  2. Apple 和 Google 均提供了生产推送服务器和部署推送服务器,用于发送推送通知。开发生产服务器通常用于开发阶段。应用发布后,您可将其设置更改为生产服务器。
  3. App ID – 您在第 3 步中创建的 ID。注意:该 ID 区分大小写。
  4. Apple 证书文件和密钥文件,即您在第 9 步生成的证书文件。请谨记,密钥文件必须无密码(portpbx_push.pem portpbx_push_key.pem)。

单击“应用”按钮,推送服务即在 PBX 中启用。