IOS remote push is far from configuring two certificates and integrating an SDK.

This article will start from the practice, combined with apple’s official documentation, bring you a comprehensive understanding of Apple APNs service. In addition to the basic principles and integration methods, it also introduces in detail how to call the APNs service interface and the basic principles of each push SDK.

The sample code here includes both the iOS client and Java server code.

An overview of the

At present, most apps in the market have requirements for the server to actively push messages to users according to certain conditions. When faced with this requirement, the first thing that comes to mind is long links, or Server Push if the Server integrates HTTP/2. This is what Android does with similar services, and it’s called telemessaging. !

However, we expect that no matter the app exits the background or the process is terminated, the message can be received normally. This is where push of the system set is needed. On iOS, or Apple’s entire system, it’s the great Apple Push Notification Service, or APNs.

Let the client have the ability to receive push

Enable app push notification capability

Under the Capability TAB of the project, turn on the Push Notifications switch

And we can see that it does two things for us.

One is to add push notifications under your bundleId App. This is the same Configuration you would do under Edit Your App ID Configuration.

The other thing, is to generate the corresponding.Entitlements file

Obtain push permission

The code for obtaining push permissions is simple:

UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.badge,.sound]) { (isGrand, error) in
	DispatchQueue.main.async {
	    if isGrand {
	        // The user has allowed push permission
	    }else{
	        // The user denied push permission}}}Copy the code

Push has several forms of presentation, the most commonly used are alert, sound, badge. This can be specified in the options parameter.

If you need a pre-ios 10 device, you can use:

UIApplication.shared.registerUserNotificationSettings(.init(types:[.alert,.badge,.sound], categories:nil))
Copy the code

It is called back in the following methods:

func application(_ application: UIApplication, didRegister notificationSettings: UIUserNotificationSettings) {
      if notificationSettings.types.isEmpty {
          print("User denied push permission.")}else{
          print("User has granted push permission.")}}Copy the code

It is worth noting:

  • Access is not connected to the Internet, and failure is basically a result of users clicking reject or disabling push access in their Settings
  • There is no need to obtain the current permission status before applying for permission. It will pop up an authorization message or return success/failure directly according to the current permission status.
  • Callback functions are executed in the child thread, switching back to the main thread if necessary

Get DeviceToken

To push messages to the device, APNs must know the IP address of the device. The mobile network is constantly changing. Every time an Apple device connects to the Internet, it establishes a long link with an Apple server, and Apple logs this connection session. As long as a unique and unique identifier is established for the device and an association is established with the session, the device can be pushed to the specified device. And that unique identifier is DeviceToken.

We need to have a session with APNs through Apple’s long link to generate DeviceToken and associate it with the BundleId of App.

Obtaining a deviceToken is simple by calling the following methods directly

UIApplication.shared.registerForRemoteNotifications()
Copy the code

DeviceToken is then returned in the following proxy methods of UIApplication

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let deviceTokenString = deviceToken.reduce("", {$0 + String(format:"%02x".The $1)})}Copy the code

If the fetch fails, the following methods are triggered

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) 
{}
Copy the code

This method is rarely triggered on real computers, even without a network. Remote Notifications are not supported in the simulator. In most cases, you don’t have to deal with failure.

It is worth noting:

  • Obtaining deviceToken has nothing to do with whether the app has applied for push permission. Even if remote push is disabled, deviceToken can be correctly obtained. If you do not have permission to apply, it will be presented in the form of silent push.

If you do not request and receive authorization for your app’s interactions, the system delivers all remote notifications to your app silently.

  • You must be connected to the Internet when obtaining deviceToken for the first time. If there is no network at the time of acquisition, deviceToken is returned immediately after reconnecting to the network.
  • Never cache DeviceToken. Just call the above method every time. If a valid DeviceToken has been generated and the system may have a cache, DeviceToken will be generated again and the cache will be updated when the App is reinstalled, the system is reinstalled, or the system is restored from a backup.
  • In the DEBUG environment, DeviceToken is applied for from the APNs server in the development environment, and in the Release environment, DeviceToken is applied for from the APNs server in the production environment. Occurs if the environment does not matchBad deviceTokenError.
  • DeviceToken is associated with bundleId. Returns if the deviceToken does not match the bundleId in the certificateDeviceTokenNotForTopic

Upload DeviceToken

We need to upload DeviceToken to the server and associate it with our user Id. In some SDKS, there are often several sticky notes bound, or groups, when deviceToken is uploaded.

Handling foreground push

At the front desk to receive push, will trigger a UNUserNotificationCenterDelegate the following methods:

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    completionHandler([.sound,.alert])
}
Copy the code

It is worth noting that the front desk receives push, but also can be the same as the background receives push, pop up banner, play sound and vibration. It all into the appropriate in the completionHandler UNNotificationPresentationOptions is ok.

If you need to adapt to pre-ios10, you can implement the following methods on UIApplicationDelegate:

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
    if application.applicationState == .inactive {
        print("Click push to invoke app userInfo=\(userInfo)")}else{
        print("The foreground receives the push userInfo=\(userInfo)")}}Copy the code

It’s worth noting that clicking the push banner to invoke the app triggers the same method, depending on whether the app is inactive.

Process user responses

When the user clicks our app’s push notification, if the app is still alive, the app will be aroused and trigger the following methods. We often need to perform page jump and other operations according to push information here.

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping (a) -> Void) {
    let userInfo = response.notification.request.content.userInfo
    handleUserResponse(userInfo:userInfo)
    completionHandler()
}
Copy the code

If the app has been killed, the app will be restarted, and we need to process the user’s response after the app has finished loading.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    if letremovePushUserInfo = launchOptions? [UIApplication.LaunchOptionsKey.remoteNotification]{
    	// Process the user's response to the push
   NotificationUtil.shared.handleUserResponse(userInfo:removePushUserInfo as! [AnyHashable:Any])}return true
}
Copy the code

Process the background receiving push

When the background receives push, in most cases, we do not have to deal with it.

Background push needs to be handled in the following scenarios

  • If the arrival rate of push is to be counted, both the foreground and the background can monitor and report it.
  • Need to change the content of the push, or need to customize the presentation of the push.

For the processing of background push, we generally apply App Extension.

Push the plugin

Notification Service Extension and Notification Content Extension are two main App extensions related to push

Notification Service Extension

This plug-in is mainly used for background push monitoring and dynamic modification of push content. A common service scenario is voice broadcast, which can be combined with local push and local voice package.

We can implement changes to push content in Extension in the following methods.

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
      self.contentHandler = contentHandler
      bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
      if let bestAttemptContent = bestAttemptContent {
          bestAttemptContent.title = "Revised title"
          bestAttemptContent.body = "Modified body"
          bestAttemptContent.sound = UNNotificationSound(named:UNNotificationSoundName(rawValue: "101.mp3"))
          addLocationNotification()
          contentHandler(bestAttemptContent)
      }
  }
Copy the code

The above method can be invoked only if the following conditions are met.

  • In the contentmutable-content1
  • The payload must contain Alert and has the Alert push permission

More introduction please see Modifying the Content in Newly Delivered Notifications, UNNotificationServiceExtension

Notification Content Extension

Such plug-ins can customize the presentation of a push, but are rarely used in actual development.

In NotificationViewController, can be like ordinary UIViewController custom UI.

A necessary condition for it to take effect is, the category field in the content, must and the extension of the Info.) specified in the plist UNNotificationExtensionCategory field values.

It must be in the notification center, left click to view the push to display our custom notification interface.

For details, please refer to the official documentation

Other Listening Methods

The following methods can also be used to listen for background pushes

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {}Copy the code

In principle, the app will wake up when the background receives push through silent push and trigger the above method. But practice shows that there are two necessary conditions to trigger him:

  • Turn on Background Modes and check Remote Notification

  • Contained in the payloadcontent-availableAnd the value is 1

Silent push

When we do not request the push permission, or the user refuses or actively closes the push permission, the push can still reach the device normally. But there were no banners and no sound. It was silent.

You must turn on background mode and check Remote Notification to trigger the methods we described earlier.

Payload does not contain alert, sound, or badge. Regardless of whether app push permission is enabled, it belongs to silent push.

In principle, push in background mode should be silent push. After iOS13, you need to set the apns-push-type field in the request header to backgroud. If it is not a true silent push, it is in danger of being discarded by APNs.

The server invokes the APNs interface

Remote push is initiated by the server. Whether we build push service by ourselves or choose third-party SDK. It all ends up being the interface that calls APNs.

The network protocol used by the APNs interface is HTTP/2 Over TLS.

For the sake of description, the sample code includes swift, which is familiar to iOS developers, and the Java version used more often on the actual server.

Establish a secure connection with APNs

APNs supports two authentication modes: certificate-based and token-based. Among them, certificate authentication is the most commonly used method

Certificate-Based Connection

Push the certificate

There are two types of push certificates: push certificates and production certificates. The certificate creation method is simple and won’t be covered here.

Production certificates can only be used in the iOS development environment. Production certificates, also known as universal certificates, can be used in both production and development environments. Some SDKS can set up production certificates for use in the development environment. In most cases, a production certificate will suffice.

The certificate we provide to the server or third-party SDK generally has two formats:. P12 and. Pem. P12 certificates can be easily exported from keystrings.

You can run the following command to generate a PEM certificate from a P12:

openssl pkcs12 -in push_dis.p12 -out push_dis.pem -nodes
Copy the code
Establishing an HTTPS Connection

A TLS handshake establishes a connection like this

We initiate a TLS connection, APNs returns its server certificate, we provide our own client certificate, APNs verifies that the client certificate is successful, then a secure and trusted connection can be established. Once the connection is successful, a remote push request can be sent to APNs.

Swift implementation

Let’s use the SWIFT code to illustrate the principle. The first time we call the APNs interface, we fire the URLSessionDelegate didReceive Challenge proxy method because we’re going HTTPS. The handshake process is as follows:

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
      let space = challenge.protectionSpace
      switch space.authenticationMethod {
      case NSURLAuthenticationMethodServerTrust:
          if let trust = space.serverTrust {
              let credential = URLCredential(trust:trust)
              completionHandler(.useCredential,credential)
          }else{
              completionHandler(.rejectProtectionSpace,nil)}case NSURLAuthenticationMethodClientCertificate:
          if let credential = self.certificate? .credential { completionHandler(.useCredential,credential) }else{
              completionHandler(.performDefaultHandling,nil)}default:
          completionHandler(.performDefaultHandling,nil)}}Copy the code

Where, the credential for client authentication is generated by a P12 file. The p12 file contains the private key and certificate information:

guard let data = try? Data(contentsOf: URL(fileURLWithPath:p12Path)) else { return nil }

var item = CFArrayCreate(nil.nil.0.nil)
letoptions = pwd ! =nil ? [kSecImportExportPassphrase:pwd!] : [:]
let status = SecPKCS12Import(data as CFData,options as CFDictionary,&item)
ifstatus ! = noErr {return nil
}

guard  let itemArr = item as? [Any].let dict = itemArr.first as? [String:Any] else{
    return nil
}
guard let secIdentity = dict[kSecImportItemIdentity as String] else {
    return nil
}
guard let cers = dict[kSecImportItemCertChain as String] as? [SecCertificate] else{
    return nil
} 

self.credential = URLCredential(
                    identity:secIdentity as! SecIdentity,
                                certificates:cers, 			
                                persistence:.permanent)
Copy the code
Java implementation

We use Netty, an excellent event-driven asynchronous network library in Java, to establish a secure connection with APNs.

We need to generate the private key and certificate from the P12 file, and then configure the SslContext for establishing the SSL connection

final SslContext sslContext(String p12File, String password) throws SSLException, FileNotFoundException {
    P12 generates the private key and certificate
    final KeyStore.PrivateKeyEntry privateKeyEntry = this.getPrivKeyEntry(p12File, password);
    final X509Certificate certificate = (X509Certificate) privateKeyEntry.getCertificate();
    final PrivateKey privateKey = privateKeyEntry.getPrivateKey();

    // Generate netty's SslContext from the private key and certificate
    final SslProvider sslProvider;
    if (OpenSsl.isAvailable()) {
        // Native SSL provider is available; will use native provider.
        sslProvider = SslProvider.OPENSSL_REFCNT;
    } else {
        // Native SSL provider not available; will use JDK SSL provider.
        sslProvider = SslProvider.JDK;
    }
    final SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().sslProvider(sslProvider)
            .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE);
    sslContextBuilder.keyManager(privateKey, password, certificate);

    return sslContextBuilder.build();
}
Copy the code

If the pem file is used, it is simpler:

final SslContext sslContext(String pemFilePath) throws SSLException {
    final SslProvider sslProvider;
    if (OpenSsl.isAvailable()) {
        // Native SSL provider is available; will use native provider.
        sslProvider = SslProvider.OPENSSL_REFCNT;
    } else {
        // Native SSL provider not available; will use JDK SSL provider.
        sslProvider = SslProvider.JDK;
    }

    final SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().sslProvider(sslProvider)
            .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE);

    final File pemFile = new File(pemFilePath);
    sslContextBuilder.trustManager(pemFile);

    return sslContextBuilder.build();
}
Copy the code

Token-Based Connection

Token authentication mainly uses the JSON Web Token (JWT) technology.

Preparation information before push

If you don’t have keyId and.p8 files, you need to create a new AuthKey here

Remember to download and keep the resulting.p8 file safe after creation, as it can only be downloaded once.

The KeyId is included directly in the file name, for example the file name authKey_mtb28kc884. p8 is MTB28KC884. You can also click to the corresponding Key details page to obtain.

The Team Id can be found in Apple Develop -> Account -> Membership. That’s right there

The basic principle of
JWT overview

JWT, which stands for JSON Web Token, is an encoded string, such as:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Copy the code

A typical JWT consists of three parts, separated by dot signs. Each part is a Base64 URL-encoded string.

  • The first partHeader. The Base64Url decoded is JSON.
  • The second partPayload(also calledClaims). After decoding, it is JSON.
  • The third partThe signatureIs the result of signing the first two parts with a key.
Base64Url codec

Base64 is familiar to everyone. With the spread of the Internet, one of the big “disadvantages” of this encoding is the use of +, / and =. These characters are very unfriendly in urls, and escaping is required as a transport. Base64Url is the improvement of this problem, specifically:

Replace + with -; Replace/with _; Kill the trailing =.

extension Data {
    // Encode `self` with URL escaping considered.
    var base64URLEncoded: String {
        let base64Encoded = base64EncodedString()
        return base64Encoded
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")}}extension String {
    // Returns the data of `self` (which is a base64 string), with URL related characters decoded.
    var base64URLDecoded: Data? {
        let paddingLength = 4 - count % 4
        // Filling = for %4 padding.
        let padding = (paddingLength < 4)?String(repeating: "=".count: paddingLength) : ""
        let base64EncodedString = self
            .replacingOccurrences(of: "-", with: "+")
            .replacingOccurrences(of: "_", with: "/")
            + padding
        return Data(base64Encoded: base64EncodedString)
    }
}
Copy the code
Common signature modes

In the JWT Header, “alg” is the value that must be specified to indicate how the JWT is signed. The common signature modes are as follows

Generic names For example, Signature algorithm Introduction to the
HSXXX HS256 HMAC The operation uses a hash algorithm, which takes a key and a message as input and generates a message digest as output.
RSXXX RSA512 RSA RSA signature/authentication, XXX is mode length (bit)
ESXXX ES256 ECDSA Elliptic Curve Digital Signature Algorithm (ECDSA) is used for signature. It is also an asymmetric algorithm. But it’s based on an elliptic curve.
PSXXX PS256 RSA Similar to RSXXX, RSA algorithm is used, but PSS is used as padding for signature. For comparison, the PADDING of PKCS1-v1_5 is used in RSXXX.

In addition to “alg”, we will also use “kid”, which represents the key ID of the public key obtained from the JWK Host for validation purposes.

JWT structure and signature of APNS

The header part:

key describe
alg Encryption algorithm. APNs supports only ES256
kid keyId

Content (Claims)

key describe
iss Team ID
iat Time stamp, number of seconds from 1970

APNs supports only ES256, the Elliptic Curve Digital Signature Algorithm (ECDSA).

Elliptic Curve Digital Signature Algorithm (ECDSA) is a simulation of digital signature algorithm (DSA) using elliptic curve cryptography (ECC).

When ECC is used for digital signature, you need to construct a curve, or you can choose a standard curve, such as PRIME256V1, SECP256R1, NISTP256, SECP256K1, etc.

And ES256 stands for, we choose the prime256V1 standard curve.

Hash function algorithm uses SHA256.

For more information on ECC (ECDSA), see this article

Create a token

We can build head and body from keyId, teamId, and timestamp. We then Base64Url each of them, concatenate them with dot delimiters to get unsignedJWT, and convert them to Data to get unsignedData. Then use.p8 to generate the private key, and finally use the private key and ECDSA signature algorithm for head +. + body.

Swift implementation

The general process is achieved by SWIFT as follows:

/ / the head part
var head = [String:Any]()
head["kid"] = kKeyId
head["alg"] = "ES256"
let headString = APNsJwt.base64UrlString(json:head)

/ / the claim parts
var claim = [String:Any]()
claim["iss"] = kTeamId
claim["iat"] = Date().timeIntervalSince1970
let claimString = APNsJwt.base64UrlString(json:claim)

// Generate the data before the signature
let jwtString = headString + "." + claimString
let data = jwtString.data(using:.utf8)!

Get the private key from.p8
let key : SecKey? = APNsJwt.getPrivKey(p8Path:self.p8Path)

// Use the private key and the ECDSA algorithm to sign
letsignData = p256Sign(privKey:key! , data:data)// The signed data is baseurl encoded
guard letsignString = signData? .base64URLEncodedelse { return nil }

// Concatenate the final token
let token = headString + "." + claimString + "." + signString
Copy the code
Java implementation
// Generate JSON for head and Claims
AuthenticationTokenHeader header = new AuthenticationTokenHeader(keyId);
AuthenticationTokenClaims claims = new AuthenticationTokenClaims(teamId, issuedAt);
final String headerJson = GSON.toJson(header);
final String claimsJson = GSON.toJson(claims);

// base64Url encoding is concatenated into a string before the signature
final StringBuilder payloadBuilder = new StringBuilder();   payloadBuilder.append(encodeUnpaddedBase64UrlString(headerJson.getBytes(StandardCharsets.US_ASCII)));
payloadBuilder.append('. ');
payloadBuilder.append(encodeUnpaddedBase64UrlString(claimsJson.getBytes(StandardCharsets.US_ASCII)));

P8 generates the private key
ECPrivateKey signingKey = loadPrivKey(p8path);

/ / SHA256withECDSA signature
final Signature signature = Signature.getInstance("SHA256withECDSA");
signature.initSign(signingKey);
signature.update(payloadBuilder.toString().getBytes(StandardCharsets.US_ASCII));
byte[] signatureBytes = signature.sign();

// Signature base64Url encoding, and concatenated at the end
payloadBuilder.append('. ');
payloadBuilder.append(encodeUnpaddedBase64UrlString(signatureBytes));

// Get the token
String token = payloadBuilder.toString();
Copy the code

Loading the private key is like this

// Remove the header and tail and carriage return characters
final InputStream inputStream = new FileInputStream(p8path);
final StringBuilder privateKeyBuilder = new StringBuilder();
final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
boolean haveReadHeader = false;
for(String line; (line = reader.readLine()) ! =null;) {
    if(! haveReadHeader) {if (line.contains("BEGIN PRIVATE KEY")) {
            haveReadHeader = true; }}else {
        if (line.contains("END PRIVATE KEY")) {
            break;
        } else{ privateKeyBuilder.append(line); }}}final String base64EncodedPrivateKey = privateKeyBuilder.toString();
reader.close();

/ / base64 decoding
final byte[] keyBytes = decodeBase64EncodedString(base64EncodedPrivateKey);

// Convert to ECPrivateKey
final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
final KeyFactory keyFactory = KeyFactory.getInstance("EC");
ECPrivateKey signingKey = (ECPrivateKey) keyFactory.generatePrivate(keySpec);
Copy the code
Adds the token to the request header

Add bearer < Provider_Token > to the authorization field of the request header, where provider_Token is the token generated above. Such as:

authorization: bearer eyAia2lkIjogIjhZTDNHM1JSWDciIH0.eyAiaXNzIjogIkM4Nk5WOUpYM0QiLCAiaWF0I
		 jogIjE0NTkxNDM1ODA2NTAiIH0.MEYCIQDzqyahmH1rz1s-LFNkylXEa2lZ_aOCX4daxxTZkVEGzwIhALvkClnx5m5eAT6
		 Lxw7LZtEQcH6JENhJTMArwLf3sXwi
Copy the code
The refresh Token

For security reasons, APNs requires regular refreshing of tokens. The refresh cycle is between 20 and 60 minutes. APNs will reject all requests for expired tokens. Similarly, if you update your token too often, an error will be reported.

In other words, our Provider Server must cache tokens and set up a regular update mechanism.

Request header field

The request header mainly contains the following fields:

Header fields Whether must describe
:method Must be POST
:path Must be /3/device/<device_token>
authorization This parameter is mandatory in token-based mode bearer <provider_token>
apns-topic Must be BundleId
apns-push-type You have to after iOS13 alertbackground; Background is a silent push, and alert is a normal push.
apns-id Not a must A unique identifier for push notifications

For other request header fields, see Sending Notification Requests to APNs

Generate playload

Payload is our request body, it’s a JSON. It mainly contains the following fields

Key type describe
alert Dictionary or String The content displayed in the banner
badge Number Desktop ICONS display small red dot numbers
sound String Playing sound
content-available Number Background push identifier. When the value is 1, the push received in the background can arouse the APP for relevant updates. In principle, it can only be pushed silently without alert, sound or badge.
mutable-content Number When the value is 1, the Notification Service Extension works normally.

For details about multi-fields, see Generating a Remote Notification.

Alert

Alert generally contains the following fields

Key type describe
title String Push title
subtile String Additional information explaining the purpose of the notice.
body String The specific content of push

When the Alert field is a string, it is equivalent to the body field.

The alert field also contains localization related fields. Please check the official documentation for details

Sound

The value of the sound field is the file name of the sound file. Valid sound files can only exist in two places. One is in the main bundle and the other is in the Library/Sound directory of the main program’s user directory.

For an introduction to sound push, see UNNotificationSound

Send push request

After the authentication is complete and the payload is generated, we can send a push request:

Swift implementation

func push(token:String,bundleId:String,payload:[String:Any],isSanBox:Bool,completion:@escaping APNsCommonResponse){
    var url : String
    if isSanBox {
        url = "https://api.sandbox.push.apple.com/3/device/\(token)"
    } else {
        url = "https://api.push.apple.com/3/device/\(token)"
    }
    var req = URLRequest(url: URL(string:url)!)
    req.httpMethod = "POST"
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.setValue(bundleId, forHTTPHeaderField:"apns-topic")
    if let auth = self.jwt? .authorization { req.setValue(auth, forHTTPHeaderField:"Authorization")
    }
    req.httpBody = try? JSONSerialization.data(withJSONObject:payload, options:.prettyPrinted)
    let task = self.session.dataTask(with:req) { (data,response,error) in
        let statusCode = (response as? HTTPURLResponse)? .statusCodevar reason : String? = nil
        if let data = data,
            let dict = try? JSONSerialization.jsonObject(with:data, options:.allowFragments) as? [String:Any]{
            reason = dict["reason"] as? String
        }
        if statusCode == 200 {
            completion(.success)
        }else{
            completion(.fail(code:statusCode ?? 400, msg:reason ?? error! .localizedDescription)) } } task.resume() }Copy the code

Java implementation

The general process of sending push requests is as follows:

// Create the request
String path = "/3/device/" + deviceToken;
HttpScheme scheme = HttpScheme.HTTPS;
AsciiString hostName = new AsciiString("api.sandbox.push.apple.com:443");
HttpVersion version = HttpVersion.valueOf("HTTP / 1.1");
ByteBuf body = Unpooled.wrappedBuffer(payload.getBytes(CharsetUtil.UTF_8));
FullHttpRequest request = new DefaultFullHttpRequest(version, new HttpMethod("POST"), path,body);
request.headers().add(HttpHeaderNames.HOST, hostName);
request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name());
request.headers().add("apns-topic", topic);
if(this.jwt ! =null){
    request.headers().add(HttpHeaderNames.AUTHORIZATION, "bearer " + this.jwt.token);
}

// Send the request
Channel channel = initializer.channel;
channel.write(request);
channel.flush();

// Wait for the result to return
Result result = initializer.responseHandler.awaite();
System.out.println("sent " + (result.isSuccess ? "Success" : "Failure" ) + result.reason);
Copy the code

Handling the result of the call

We need to process the results returned by calling the APNs interface. The return value of apNS consists of two parts: the response status code of the corresponding header and reason in the response body.

status code reason describe
200 / successful
400 BadDeviceToken DeviceToken is invalid or the environment is wrong.
400 BadTopic Apns – topic value is invalid
400 DeviceTokenNotForTopic DeviceToken and Topic do not match
400 MissingDeviceToken There is no DeviceToken
400 MissingTopic The request header is missing apnS-topic
403 BadCertificate The certificate is invalid
403 BadCertificateEnvironment The certificate environment is incorrect
403 ExpiredProviderToken Token is out of date
403 MissingProviderToken The client authentication certificate is not provided, and authorization is not set
413 PayloadTooLarge Content is too big. (not larger than 4 KB)
429 TooManyProviderTokenUpdates Token updates too frequently (less than 20 minutes)
429 TooManyRequests Sending too many requests to the same device at the same time

For other status codes, see Sending Notification Requests to APNs

Best practices

In the example code above, we have simplified a lot of the code to make it easier to describe the principle. All connections and requests are synchronized, resulting in a significant performance penalty. For broadcasts or multicast with large numbers of users, we need to find ways to make our code more efficient.

For performance reasons, we can start with the following aspects:

  • Long links and multiplexing: We expect all requests to go through the same session after a successful SSL handshake, and ensure that the lifetime of the connection is as long as possible. Disconnection and reconnection mechanism if necessary.
  • Asynchronous processing of APNs responses: APNs returns the request results to us via HTTP/2 ServerPush. Do not wait for the previous request to complete before sending a new request. We need to asynchronously listen to the APNs response, and then process the request results uniformly.
  • To deal within flightRequest:in flightAPNs allows about 1,500 of these push requests to exist simultaneously and the rest to be blocked. If the concurrency is too high, we need to cache the remaining requests that are not sent.
  • Flow control: Although we have cached the sent requests, if the number of concurrent requests is too large, the request queue will consume a lot of memory resources. So we have to control the amount of concurrency, especially when sending a broadcast.
  • Weigh the number of concurrent and connection counts. We need to set up thread pools and run loops to control the number of threads opened to maximize CPU performance. We can establish multiple connections to maximize bandwidth resources. And often we need a balance.

Pushy implements this for us, and we can use it directly.

Setting up a client

ApnsClient cerClient(Boolean isProduct,String p12Path, String p12Pwd) throws SSLException, IOException {
    File file = new File(p12Path);
    String host = isProduct ? ApnsClientBuilder.PRODUCTION_APNS_HOST : ApnsClientBuilder.DEVELOPMENT_APNS_HOST;
    ApnsClientBuilder apnsClientBuiler = new ApnsClientBuilder();
    apnsClientBuiler.setApnsServer(host);
    apnsClientBuiler.setClientCredentials(file, p12Pwd);
    ApnsClient client = apnsClientBuiler.build();
    return client;
}
Copy the code

The generated Client should be as long as possible to improve the efficiency of push.

The new push

SimpleApnsPushNotification notification(String deviceToken,String title, String body){
    final ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
    payloadBuilder.setAlertTitle(title);
    payloadBuilder.setAlertBody(body);
    payloadBuilder.setSound("default");
    final String payload = payloadBuilder.buildWithDefaultMaximumLength();
    final String token = TokenUtil.sanitizeTokenString(deviceToken);
    SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, kBundleId, payload);
    return pushNotification;
}
Copy the code

Send push requests asynchronously

private final Semaphore semaphore = new Semaphore(10 _000);
    PushNotificationFuture<ApnsPushNotification,PushNotificationResponse<ApnsPushNotification>> asyncSent(ApnsPushNotification notification){
  try {
      semaphore.acquire();
      final PushNotificationFuture<ApnsPushNotification,PushNotificationResponse<ApnsPushNotification>> sendNotificationFuture =
      client.sendNotification(notification);
      sendNotificationFuture.addListener(future -> semaphore.release());
      return sendNotificationFuture;
  } catch (InterruptedException e1) {
      System.err.println("Failed to acquire semaphore.");
      e1.printStackTrace();
      return null; }}Copy the code

This is the lowest level method for global flow control.

The response to a push request can be obtained synchronously in the following ways

ApnsResponse getResult(PushNotificationFuture<ApnsPushNotification,PushNotificationResponse<ApnsPushNotification>> future){
    try {
        PushNotificationResponse<ApnsPushNotification> pushNotificationResponse = future.get();
        ApnsResponse response = new ApnsResponse(pushNotificationResponse.getPushNotification(), pushNotificationResponse.isAccepted(), pushNotificationResponse.getRejectionReason());
        return response;
    } catch (InterruptedException | ExecutionException e) {
        ApnsResponse response = new ApnsResponse(future.getPushNotification(), false."sent Interrupted");
        returnresponse; }}Copy the code

Push to a specified device

ApnsResponse sentSingleNotification(String deviceToken,String title,String body){
    ApnsPushNotification notification = notification(deviceToken, title, body);
    return getResult( asyncSent(notification) );
}
Copy the code

To broadcast or multicast, you need to read a batch of DeviceToken from the database and send it using the following methods:

List<ApnsResponse> sentNotification(List<String>deviceTokens,String title,String body){
  
 ArrayList<PushNotificationFuture<ApnsPushNotification,PushNotificationResponse<ApnsPushNotification>>> futureList = new ArrayList<PushNotificationFuture<ApnsPushNotification,PushNotificationResponse<ApnsPushNotification>>>();
     // Send push requests asynchronously
    for (String deviceToken : deviceTokens) {
        ApnsPushNotification notification = notification(deviceToken, title, body);
        futureList.add( asyncSent(notification) );
    }

    // block until the last request completes
    ArrayList<ApnsResponse> resultList = new ArrayList<ApnsResponse>();
    for (PushNotificationFuture<ApnsPushNotification,PushNotificationResponse<ApnsPushNotification>> future : futureList) {
        ApnsResponse response = getResult(future);
        resultList.add(response);
    }

    return resultList;
}
Copy the code

Push SDK which strong

Major SDK comparison

Currently, there are several main push SDKS:

platform Supported Certificate formats Token authentication Android Vendor Channel History/Push statistics idfa
A homing pigeon pem Does not support free Only a certain amount of time is recorded in history Default does not include
Their Allies p12 support free History/statistics The default include
The aurora p12 support charge There are only statistical charts The default include
A push p12 Does not support Does not support There are only statistical charts The default include

In principle, iOS is the interface to call APNs. Basically, every MAJOR SDK calls the APNs interface and ends without caching messages. So it’s pretty much the same thing. Basically be integrated convenient ok.

In terms of integration, it’s all about registering the SDK, then opening authorization, then uploading deviceToken and setting some tags. Finally, for statistics, it needs to be reported when push is received and users click on it.

Some SDKS push authorization and click event reporting into a function that requires passing in launchOptions. However, we sometimes do not expect to start the push authorization prompt, which is a bit troublesome to deal with. If IDFA is used in the SDK, we will either integrate the idFA version that is not included, or we will need to make special Settings at the time of presentation. Umeng also uses the old iOS version of the method, which is slightly crappy to use.

And the implementation is often the android side. For Android to make notifications arrive in a timely manner, it’s best to integrate vendor channels. The SDK can integrate vendor channels for free, so it’s a natural choice for us.

The history of the push can help us identify problems that have occurred after the push has been sent. Some statistics are also necessary for operations.

Remote push of difficult diseases

Unable to receive push problem

Failure to receive push is the most common problem.

First we can use some debugging tools, such as Knuff for debugging. Problems verifying client code integrity, as well as certificate configuration issues.

If we use the SDK, we can try sending a few messages to its management platform. See if there is a problem with our integration, or if the uploaded certificate is not set up in the wrong environment, or if there is an error in the conversion of the certificate format. For fraternies, the mapping in the keystring cannot be expanded when generating a P12.

We can have the background provide some validation interface. If it can be pushed to the SDK management background, the adjustment interface cannot be pushed. You can check whether the service was successful in calling a third-party interface, or whether a third-party call to APNs was successful. This can generally be seen in the interface’s response, and in some administrative backgrounds, in the history.

If the interface is successfully called, it is still not received. Generally, the permission on the mobile phone is not open, there is no Internet. The alert and sound Settings of the payload are correct.

Push delay

First, push delay exists objectively. No matter calling third-party interfaces or APNs, there will be peak times, and there will be congestion and queuing.

We can monitor how long it takes to call a third-party interface or APNs to return when a delay occurs. If all the returns are timely, the APNs cannot process them, or there is a problem in negotiating with the device.

  • Registering Your App with APNs
  • Setting Up a Remote Notification Server
  • Token-Based Connection
  • Establishing a Certificate-Based Connection to APNs
  • IOS development of the new APNs build essential knowledge
  • High-performance Message Push for iOS Based on APNs latest HTTP/2 Interface
  • UserNotifications framework in detail
  • Details of iOS push service APNs: design ideas, technical principles and defects
  • Establishing a Token-Based Connection to APNs
  • Days of Fighting JOSE – Cryptology 101 for iOS Developers (Practice)
  • Days of Fighting JOSE – Cryptology Primer for iOS Developers (Basics)
  • Java cryptography asymmetric encryption and digital signature using SECP256K1 (ECDSA) (