当参与后端开发,并且所涉及的项目是为 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 就完全不是问题了,只要了解数据交互的方式即可。主要是以下四点:
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,事情由此变得容易了许多。