BLOG
Enjoy when you can, and endure when you must.
JUL 15, 2016/Python
苹果推送 APNs Provider API 在 Python 中的使用

当参与后端开发,并且所涉及的项目是为 APP 提供服务的时候,就不可避免的会遇到推送这个需求。就 iOS 的推送而言,要规规矩矩的来做,当属直接与 APNS 进行对接来实现推送。APNS 的接口有两种,一种为 Binary Provider API,还有一种为最新的 APNs Provider API。

Binary Provider API 如果有接触的话一定会有一种相当“奇怪的”感觉,必须使用 binary format 进行数据通信,这对于 API 来说确实不多见。而令人最烦的一点恐怕是在 APNS 认定收到的数据存在问题时,比如 BadToken,APNS 就会关闭 socket 的连接,并导致在此过程中传输的其他数据也一并视为无效。具体的表现就是:假如我们一次 push 了 10 条推送,但其中的第一条就因为 token 原因认定为无效推送,那这 10 条推送就全都无效了。

于是在 2015 年,苹果为 APNS 推出了新的 API,基于 HTTP/2。虽然相比于我们常用的 API 还是很奇怪,不过其采用最新的 HTTP/2 来实现,还是很有趣且具备更多优势,值得研究研究。在很多地方 HTTP/2 与目前常见的 HTTP/1 都不太一样,甚至需要转变一下理念。

本文并不关注技术细节,如果感兴趣,以下一些文章我认为得知一读:

  • 有关 HTTP/2 的一切:http://httpwg.org/specs/rfc7540.html
  • 苹果 APNS 官方文档:https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html

而正如标题所说,这里主要分享一下如何在 Python 中用上这个最新的接口。那么要解决两个问题,一个是如何用 http/2 通信,一个是如何使用 APNs Provider API。

 

如何在 Python 中建立 http/2 通信

自己通过 socket 来实现 http/2 的通信肯定是很好的,这能更深入的理解 socket 及 http/2 本身。不过那样需要大量的时间。很多时候,我也乐意先站在巨人的肩膀上,然后再深入其中了解底层的原理。到目前为止,我认为 hyper 库较为成熟,虽然在文档中它提到这还是早期的 alpha 版本,可能会遇到一些 bug。hyper 对 Python 的版本有一些要求:2.7.9 以上或 3.4 以上版本。安装好之后即可开始使用,这里引用一个 hyper 官方文档中的示例:

>>> from hyper import HTTPConnection
>>> c = HTTPConnection('http2bin.org')
>>> first = c.request('GET', '/get', headers={'key': 'value'})
>>> second = c.request('POST', '/post', body=b'hello')
>>> third = c.request('GET', '/ip')
>>> second_response = c.get_response(second)
>>> first_response = c.get_response(first)
>>> third_response = c.get_response(third)

使用固然很简单,有一点值得注意的就是多个 request 和 response 对。看一看相关源码:

class HTTP20Connection(object):

    ...

    def request(self, method, url, body=None, headers=None):
        with self._write_lock:
            stream_id = self.putrequest(method, url)

        ...

    def putrequest(self, method, selector, **kwargs):
        """
        This should be the first call for sending a given HTTP request to a
        server. It returns a stream ID for the given connection that should be
        passed to all subsequent request building calls.

        Concurrency
        -----------

        This method is thread-safe. It can be called from multiple threads,
        and each thread should receive a unique stream ID.

        :param method: The request method, e.g. ``'GET'``.
        :param selector: The path selector.
        :returns: A stream ID for the request.
        """
        # Create a new stream.
        s = self._new_stream()

        ...

可以看出,对于每一个 request,都会有一个新的 stream 来承载它,这就是 http/2 的特色之一:在通信过程中,连接被分成多个 stream,每个 stream 都包含一个 request 和一个 response。stream 间都相互独立,互不影响。所以在之前的示例中,可以一个性发送多个 request,然后再根据每一个 request 的 stream_id 来获取对应的 response。这可能有很多人会想到,这是不是意味着可以再多个线程中同时使用一个 HTTPConnection?很可惜,官网中也提到了这一点,目前不是线程安全的。不过基于 request 和 response 不相互影响这一点,也确实可以考虑将他们独立在两个线程中,一个线程专门负责请求,一个线程专门负责接收和处理响应,这样在一定程度上可以大大提升性能。简单尝试后,我们来进入下一个问题。

 

如何使用 APNs Provider API

既然 http/2 已经搞定,使用 APNs Provider API 就完全不是问题了,只要了解数据交互的方式即可。主要是以下四点:

  • 证书问题:我曾写过一篇关于 APNS 证书生成和使用的博客:https://www.dannysite.com/blog/145/
  • http/2 中的基本头部::method、 :scheme 和 :path
  • API 通信过程中所需的其他头部:apns-id、apns-expiration、apns-priority 和 apns-topic
  • API 通信过程中的 body,即 payload,是 json 格式的数据

具体可以阅读一下 APNS 的官方文档。

当然,Github 上也早有人基于 hyper 写出了 PyAPNs2(https://github.com/Pr0Ger/PyAPNs2)。不过自己使用时我对其 Client.send_notification() 方法做了一点小小的修改。作者原本的代码如下:

class APNsClient(object):
    
    ...

    def send_notification(self, token_hex, notification, priority=NotificationPriority.Immediate, topic=None, expiration=None):
        
        ...

        stream_id = self.__connection.request('POST', url, json_payload, headers)
        resp = self.__connection.get_response(stream_id)
        if resp.status != 200:
            raw_data = resp.read().decode('utf-8')
            data = json.loads(raw_data)
            raise exception_class_for_reason(data['reason'])

这里作者将发送和接收放在了一起,也就意味着必须等待返回,一次推送才算结束。这明显不利于性能的发挥。因为就像之前所说的,我们完全可以把 request 和 response 拆分到多个线程中来处理以提高性能。因此我对其做的一点小小的修改就是将发送与接收分开:

class APNsClient(object):

    ...

    def send_notification(self, token_hex, notification, priority=NotificationPriority.Immediate,
                          topic=None, expiration=None):

        ...

        return self.__connection.request('POST', url, json_payload, headers)

    def get_callback(self, stream_id):
        resp = self.__connection.get_response(stream_id)
        if resp.status != 200:
            raw_data = resp.read().decode('utf-8')
            data = json.loads(raw_data)
            raise exception_class_for_reason(data['reason'])

在开始的时候,我就提到一点是曾经老的 Binary Provider API 会因为一点点“过失”而关闭连接,并导致在此时候的所有数据一并失效。可谓是相当的“霸道”。而在最新的 APNs Provider API 中该问题就不再存在了。因为在通信过程中,一个 stream 承载一条推送,它有问题只是它的事,并不会影响其他的 stream,事情由此变得容易了许多。

COMMENTS
LEAVE COMMNT