iOS实现消息推送及原理分析

iOS实现消息推送及原理分析

什么是消息推送?

消息推送是指在无需打开应用程序的情况下,向手机用户发送通知消息。消息推送可以通过苹果官方提供的APNs(Apple Push Notification service,苹果推送服务)完成。

APNs的工作原理

APNs与苹果设备之间的通信是基于一种专门为该服务设计的二进制协议,这个协议被称为APNs协议。APNs协议工作的必要前提是设备必须向APNs注册。注册的过程分为三步:

  1. 应用程序向APNs服务器发送一个请求,表示应用程序想要获取推送通知。
  2. APNs服务器向设备发送一条唯一的标识该设备的令牌。
  3. 应用程序将此令牌与设备相关联上传至自己开发的服务器上,推送消息时,APNs服务就需要知道要推送给哪些设备。

注册成功后,当服务器需要推送消息时,它会将要发送的消息及设备令牌一起发送给APNs服务器。APNs接收到消息及令牌后,就会对每个设备进行验证并确定该消息最终使用的传输通道。成功验证并传输过程中,APNs则会将消息推送到设备上。

如何实现消息推送?

在iOS中实现消息推送其实很简单,关键在于对APNs协议的理解和应用。以下是具体步骤:

  1. 注册开通APNs服务(这里不再赘述)。
  2. 在项目中引用PushKit框架,并在Capabilities选项卡中开启Push Notifications和Background Modes功能。Programmatic Only和Remote notifications两个选项都要勾选并关联对应的代码。
  3. 获取设备的DeviceToken:在AppDelegate.swift中实现didRegisterForRemoteNotificationsWithDeviceToken代理方法,并将获取到的DeviceToken上传到服务器上。
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 注册APNs服务
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
            if granted == true {
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
            }
        }

        // Override point for customization after application launch.
        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // 发送设备的Token给服务器(这里省略具体实现代码)
    }

}
  1. 发送消息到服务器:将要向设备展示的消息以指定格式(例如JSON)发送到自己的服务端。同时,还需要将对应的设备令牌上传到服务端,并确保服务器与APNs服务器进行了注册和通信以获取对应的令牌。具体消息推送内容和格式可以根据自己的需求进行定制。
  2. 服务端向APNs服务器发送消息和设备令牌,APNs服务器根据令牌进行验证并推送消息到指定设备。

示例1:APNs本地通知

APNs不仅可以实现远程通知,还可以实现本地推送。本地推送不需要与服务端进行通信,只需要在应用程序中添加本地通知即可。

以下是一个简单的示例代码,可以实现5秒后通知用户一个本地通知。需要注意的是,在XS及以上设备中,会自动弹出通知,但是在较老的设备上,需要在AppDelegate.swift中实现UNUserNotificationCenterDelegate代理方法并手动展示通知。

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
            if granted {
                let content = UNMutableNotificationContent()
                content.title = "通知标题"
                content.body = "通知内容"
                content.sound = UNNotificationSound.default

                let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
                let request = UNNotificationRequest(identifier: "localNotification", content: content, trigger: trigger)
                UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
            }
        }

        return true
    }

    // xs以上设备会自动弹出通知,较旧的设备需要手动展示通知。
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.sound, .alert])
    }

}

示例2:APNs远程推送

下面是一个APNs远程推送的示例代码。服务器端可以使用HTTP/2 推送通知 API 或者是向 APNs 服务器发送HTTP/2 请求发送推送通知,此处我们仅提供单独调用APNs函数的简单代码。

设备端通过苹果官方提供的SDK向APNs服务器注册自己的token,APNs 将提供一个唯一的deviceToken,当有推送通知需要发送给你的 app 时,服务器就会用这个 token 来标识你的 app。

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
            if granted {
                DispatchQueue.global().async {
                    // 向APNs服务器发送推送消息
                    let apnsHost = "gateway.push.apple.com"
                    let apnsPort: Int32 = 2195

                    let certificatePath = Bundle.main.path(forResource: "certificate", ofType: "p12")
                    let certificateData = try! Data(contentsOf: URL(fileURLWithPath: certificatePath!))

                    let options: [String: Any] = [kSecImportExportPassphrase as String: "your_password_here"]
                    var imported = CFArrayCreate(nil, nil, 0, nil)
                    let importStatus = SecPKCS12Import(certificateData as CFData, options as CFDictionary, &imported)
                    guard importStatus == errSecSuccess else {
                        fatalError()
                    }

                    let importArray = imported as! Array<Dictionary<String, Any>>
                    let identity = importArray[0][kSecImportItemIdentity as String] as! SecIdentity

                    var cert: UnsafeMutablePointer<Cert>? = nil
                    cert = useCertificate(identity: identity)

                    connectAPNs(certificate: cert!, apnsHost: apnsHost, apnsPort: apnsPort)
                }
            }
        }

        return true
    }

    struct Cert {
        let identity: SecIdentity
        let certificate: SecCertificate
    }

    func useCertificate(identity: SecIdentity) -> UnsafeMutablePointer<Cert> {
        var certificate: SecCertificate?
        SecIdentityCopyCertificate(identity, &certificate)
        let cert = UnsafseMutablePointer<Cert>.allocate(capacity: 1)
        cert.pointee = Cert(identity: identity, certificate: certificate!)
        return cert
    }

    func connectAPNs(certificate: UnsafeMutablePointer<Cert>, apnsHost: String, apnsPort: Int32) {
        let sock = openAPNsConnection(host: apnsHost, port: apnsPort, cert: certificate.pointee.certificate, key: certificate.pointee.identity, chain: nil)
        let deviceToken = "device_token_here"

        let message = "Hello, World!"
        let payload = "{\"aps\": {\"alert\": \"\(message)\", \"sound\": \"default\"}}"

        var buffer = [UInt8]()
        buffer.append(0) // payload 的通用头
        buffer.append(0)
        buffer.append(32) // deviceToken 长度
        buffer.append(contentsOf: deviceToken.hexadecimalToBytes())
        buffer.append(0) // payload 的长度
        buffer.append(UInt8(payload.utf8.count))
        buffer.append(contentsOf: payload.utf8)

        write(sock: sock, data: buffer)
        close(sock)
    }

    func openAPNsConnection(host: String, port: Int32, cert: SecCertificate, key: SecIdentity, chain: [SecCertificate]?) -> Int32 {
        var result: Int32 = -1
        let clientContext = SSLClientContext(cert: cert, key: key, chain: chain)
        var inputStream: CFReadStream!
        var outputStream: CFWriteStream!
        CFStreamCreatePairWithSocketToHost(nil, host as CFString, port, &inputStream, &outputStream)
        CFReadStreamSetProperty(inputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext), clientContext)
        CFWriteStreamSetProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext), clientContext)
        if CFReadStreamOpen(inputStream) == true && CFWriteStreamOpen(outputStream) == true {
            result = outputStream as! Int32
        }
        return result
    }

    func close(_ sock: Int32) {
        #if os(iOS) || os(tvOS) || os(watchOS)
        _ = Darwin.close(sock)
        #elseif os(macOS)
        _ = Darwin.close(sock)
        #endif
    }

    func write(sock: Int32, data: [UInt8]) {
        if data.isEmpty == false {
            var index = 0
            repeat {
                let written = Darwin.write(sock, data, data.count - index)
                // -1 表示写入失败
                if written == -1 {
                    throw NSError(domain: "com.company.app", code: 1, userInfo: nil)
                }
                index += written
            } while index != data.count
        }
    }

}

结论

APNs是一个十分方便且稳定的消息推送服务。在实现过程中,关键在于对APNs协议的掌握和服务端的编写,每个部分都有其特定的要求和考虑点。在使用PushKit框架时,需要注意权限及背景运行的相关设置,并在实现远程推送时,需要对APNs服务器进行认证和验证,确保推送的消息是合法、可靠的。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:iOS实现消息推送及原理分析 - Python技术站

(0)
上一篇 2023年6月26日
下一篇 2023年6月26日

相关文章

  • word首行怎么缩两个字段呢?

    当我们需要在Word文档中对某一个段落进行缩进操作时,我们就可以使用Word的缩进功能。其中,首行缩进是一种常见的排版方式,即让段落的第一行向右缩进一定距离,使整个段落看起来更加整齐美观。下面是Word首行缩进的完整攻略: 方法一:使用快捷键 使用快捷键可以方便地实现首行缩进。具体步骤如下: 选中你需要进行首行缩进的段落。 按下键盘上的“Ctrl”和“T”键…

    other 2023年6月25日
    00
  • c++ KMP字符串匹配算法

    C++ KMP字符串匹配算法攻略 简介 KMP(Knuth-Morris-Pratt)算法是一种高效的字符串匹配算法,用于在一个主串中查找一个模式串的出现位置。相比于朴素的字符串匹配算法,KMP算法具有更快的匹配速度。 算法原理 KMP算法的核心思想是利用已经匹配过的信息,避免不必要的回溯。它通过构建一个部分匹配表(Partial Match Table),…

    other 2023年8月6日
    00
  • java链表应用–基于链表实现队列详解(尾指针操作)

    标题设置 首先我们需要设置好标题,格式如下: Java链表应用–基于链表实现队列详解(尾指针操作) 队列简介 接下来我们先介绍一下队列的概念和特点: 队列是一种先进先出(FIFO)的数据结构,可以看成是一种特殊的线性表。队列只允许在队尾插入元素,在队头删除元素,故又称为先进先出表。在队列中插入元素的操作称为入队,删除元素的操作称为出队。 链表实现队列的基本…

    other 2023年6月27日
    00
  • mysql 替换字段部分内容及mysql 替换函数replace()

    MySQL 是一个广泛使用的关系型数据库管理系统,其中提供了很多适用于数据处理的函数。replace() 函数是 MySQL 中的一种函数,它可以用来替换掉某个字符串中的一部分内容,常用于处理字符串型字段的内容更新。 一、replace() 函数的基本用法 replace() 函数的基本用法如下: replace(str,from_str,to_str) 其…

    other 2023年6月25日
    00
  • Microsoft VBScript 编译器错误 错误 ‘800a03e9’ 内存不够的解决方法

    首先,这个错误表示VBScript编译器尝试运行时没有足够的可用内存。下面是完整的解决方法: 1. 参数优化 这个错误通常是由脚本中使用了太多的变量或数组所致。可以通过优化一下参数来尝试解决这个问题。例如: ‘ 确认输入参数是否正确 if Wscript.Arguments.Count < 2 then Wscript.Echo "Usage…

    other 2023年6月26日
    00
  • js获取天气

    以下是JS获取天气的完整攻略,包括基本介绍、使用方法、示例说明等内容。 1. 基本介绍 在Web开发中,我们经常需要获取天气信息。JS获取天气信息是其中的一种常见需求。通过JS获取天气信息,我们可以实现动态更新页面内容,提高用户体验。 2. 使用方法 以下是使用JS获取天气信息的基本步骤: 获取天气API。我们可以使用第三方天气API获取天气信息。常用的天气…

    other 2023年5月10日
    00
  • (转)mysql联表查询

    (转)MySQL联表查询 MySQL联表查询(Join),是针对多个表的查询操作。在数据库设计中,一个完整的数据信息往往需要多个表来存储,这时候就需要使用多表查询,以获取完整的数据信息。 一、内联接(INNER JOIN) 内联接是最最常用的联接方法。用 INNER JOIN 关键字连结表,并且只输出符合连接条件的行。 SELECT column_name(…

    其他 2023年3月28日
    00
  • linux目录详解linux目录结构详细分析

    Linux目录详解:Linux目录结构详细分析 Linux系统的一大特色就是其树形目录结构,不同于其他操作系统的文件结构。 在本文中,我们将会深入分析整个Linux目录结构的每一个主要目录,以及它们的作用和用途。 根目录(/) 根目录是整个Linux目录结构的顶级目录,在Linux中,所有的目录和文件都挂载在根目录下。 示例 下面是一个例子,它演示了如何列出…

    other 2023年6月27日
    00
合作推广
合作推广
分享本页
返回顶部