PWA系列 -- Web Push技术

PWA系列 -- Web Push技术

前言

我们在之前发布的文章中为读者们分享了 PWA 这项技术,并有专门的文章对 PWA 技术的关键 -- Service Worker -- 做了详细的讲解。除了离线存储外,消息推送也是 Service Worker 的关键能力,Web App 通过该能力接收并向用户展示来自服务器推送的消息,这也是当前应用中必不可少的能力。今天,我们将为读者分享 Service Worker 的推送技术 -- Web Push。

基本概念

  • Web App
    网页应用程序,是指使用 Web 技术实现的应用程序,在 Web 浏览器或其它 Web 运行环境的上下文中执行。
  • Application server
    网页应用服务器,是指 Web App 的服务器端。
  • Push message
    推送消息,是指网页应用服务器发送给 Web App 的消息。网页应用服务器在推送消息给推送服务(例如,GCM)时,会带上 push subscription,推送服务会找到与 push subscription 相关联的 active worker ,然后把消息推送给它,并触发它的 onpush。
  • Push subscription
推送订阅,是代表 Web App 在用户代理(例如,客户端)和推送服务(例如,GCM)之间建立的消息传递上下文。
每个 push subscription 都与一个 service worker registration 相关联,而一个 service worker registration 有且仅有一个 push subscription。
每个 push subscription 都有一个 push endpoint 和一个公钥,公钥是用于给网页应用服务器加密要推送的消息数据。
  • Push endpoint
    Push endpoint,在 ServiceWorker 向 push service 注册时,由 push service 生成。在 Chrome 45 版本之前,push endpoint 是指推送服务的地址,例如,GCM 服务器的地址。在 Chrome 45 及之后版本,push endpoint = push server address + subscriptionId,其中 subscriptionId 可以唯一标识 push subscription。在 Web 客户端,通过 subscriptionId 可以找到相应的 app_id 和 sender_id。app_id 包含作用域和 workerId,例如,push#http://example.com/test/#1。
  • sender_id
    sender_id,是创建 Google API 项目时生成的项目 ID。ServiceWorker 通过 Web 客户端向 GCM 注册订阅消息时,需要上传 sender_id,才能订阅成功。GCM 会使用 sender_id,通过 Instance ID API 到 Instance ID Service 去注册生成 Instance ID,此步骤最终生成的注册信息,也就是前面提到的 subscriptionId。注:Chrome52 之前版本对应的 GCM/FCM 还不支持标准的 Web Push Protocol,所以才需要通过 gcm_sender_id 去找到相应的 Web 客户端,Chrome52 版本及其对应的 GCM/FCM 已支持标准的 Web Push Protocol,不再需要 sender_id 了,即支持标准的 Web Push Protocol 的推送服务器都是不需要 sender_id 的。
  • Subscription refreshes
    Web 客户端或推送服务,可以在任何时间更新 push subscription。更新成功后,必须触发与 push subscription 相关联的 service worker registration 的 pushsubscriptionchange 事件,如果更新失败,Web 客户端应定期触发更新,直至成功。
  • Subscription deactivation
    Push subscription 停止使用时,Web 客户端和推送服务必须删除已保存的副本,相关消息不再推送。service worker registration 在注销或清除注册信息时,相关联的 push subscription 会被停用。
  • Push service
    推送服务,是指一个推送服务系统,允许应用服务器通过它推送消息给 Web App。

Push流程

  1. 页面通过 Web 客户端注册一个 ServiceWorker,Web 客户端生成对应的 ServiceWorkerRegistration 对象。
  2. 页面通过 ServiceWorker 的 PushManager.subscribe 方法向 push service(GCM)订阅消息,GCM 返回 push subscription (endpoint+subscriptionId)给 Web 客户端,Web 客户端找到对应的 ServiceWorker,并把 push subscription 传递给 ServiceWorker。
  3. 页面通过 PushManager.getSubscription 获取到 push subscription,并将它发送给页面服务器。注,可以使用 ajax 请求发送数据给页面服务器。
  4. 页面服务器需要推送消息时,将 push message 和 push subscription 发送给 push service(例如,GCM)。GCM 根据 push subscription 找到对应的 Web 客户端并把消息推送给它,Web 客户端通过 app_id 找到相应的 ServiceWorker,激活和把消息传递给它并触发它的 onpush。
  5. 页面JS根据实际需要去处理 onpush 事件,比如,弹出消息通知,存储数据到本地,提前到服务器去 fetch 资源,等等。
  6. 页面在不再需要订阅消息时,可以发送 ajax 请求通知页面服务器去清除 push subscription。然后使用 PushSubscription.unsubscribe 向 push service 去取消订阅。
  7. 基本流程的示例代码:
// https://example.com/serviceworker.jsthis.onpush = function(event) {
  console.log(event.data);
  // From here we can write the data to IndexedDB, send it to any open
  // windows, display a notification, etc.}
 // https://example.com/webapp.jsnavigator.serviceWorker.register('serviceworker.js').then(
  function(serviceWorkerRegistration) {
    serviceWorkerRegistration.pushManager.subscribe().then(
      function(pushSubscription) {
        console.log(pushSubscription.endpoint);
        console.log(pushSubscription.getKey('p256dh'));
        console.log(pushSubscription.getKey('auth'));
        // The push subscription details needed by the application
        // server are now available, and can be sent to it using,
        // for example, an XMLHttpRequest.
      }, function(error) {
        // During development it often helps to log errors to the
        // console. In a production environment it might make sense to
        // also report information about errors back to the
        // application server.
        console.log(error);
      }
    );
  });

Push服务

  1. GCM/FCM

注1:页面服务器(app server)使用 HTTP/XMPP 协议发送消息给 Google GCM Connection Servers,GCM Connection Server 推送消息给相应的 Web 客户端。

注2:页面服务器(app server)发送消息的格式如下,

curl --header "Authorization: key=<YOUR_API_KEY>" --header "Content-Type: application/json" https://android.googleapis.com/gcm/send -d "{\"registration_ids\":[\"<YOUR_REGISTRATION_ID>\"]}"

其中,API_KEY 是创建 Google API 项目时生成的 Server Key,GCM 可通过它找到对应的 sender_id。YOUR_REGISTRATION_ID 是订阅消息时生成的 subscriptionId。
注3:Google Chrome 是第一个支持 Push API 的浏览器,最先支持的版本为 Chrome 42,这个时候 Web Push Protocol 还未完稿,所以其 push service 使用的 GCM/FCM 服务器并不支持标准 Web Push Protocol。Chrome 52 版本实现了对标准 Web Push Protocol 的支持,此时,GCM/FCM也已支持标准 Web Push Protocol。

2. 第三方 Push 服务器

Push API 标准是允许替换 endpoint 的,即任何实现了 Web Push Protocol 的服务器都可作为 endpoint,并且对使用者是透明的。那么需要做哪些工作来实现我们自己的 push 服务呢?
我们先看看 Web Push Protocol 定义了那些内容:

  • web 客户端连接 push service

需要使用 HTTPS。

  • web 客户端订阅 push message

Web 客户端要发送 POST 请求,到 push service 去订阅消息。比如,

POST /subscribe HTTP/1.1
    Host: push.example.net

订阅成功后 push service 的响应如下,

HTTP/1.1 201 Created
Date: Thu, 11 Dec 2014 23:56:52 GMT
Link: </push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV>;
                rel="urn:ietf:params:push"Link: </subscription-set/4UXwi2Rd7jGS7gp5cuutF8ZldnEuvbOy>;
                rel="urn:ietf:params:push:set"Location: https://push.example.net/subscription/LBhhw0OohO-Wl4Oi971UG

这个步骤中,push service 需要能够根据不同情况返回不同的响应码,能够识别出同一 Web 客户端的请求。

  • 页面服务器发送推送消息

网页服务器发送 POST 请求给 push service,请求它将消息推送给 Web 客户端。请求如下,

POST /push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV HTTP/1.1Host: push.example.net
TTL: 15Content-Type: text/plain;charset=utf8
Content-Length: 36iChYuI3jMzt3ir20P8r_jgRR-dSuN182x7iB

push service 的响应如下,

HTTP/1.1 201 Created
Date: Thu, 11 Dec 2014 23:56:55 GMT
Location: https://push.example.net/message/qDIYHNcfAIPP_5ITvURr-d6BGt

这个步骤中,push service 需要能够根据不同情况返回不同的响应码,能够根据 TTL 保存推送消息一段时间并且含有 Topic 头部的新消息能替换旧消息,能够根据 Urgency 头部决定推送的优先级。

  • Web 客户端接收订阅的推送消息

Web 客户端发 GET 请求请求接收推送消息,请求信息如下,

   HEADERS [stream 7] +END_STREAM +END_HEADERS    :method = GET    :path = /subscription/LBhhw0OohO-Wl4Oi971UG    :authority = push.example.net

push service 允许保持请求,在应用服务器发送推送消息之后,它将消息通过 HTTP/2 server push 将消息推送给 Web 客户端,响应信息如下,

   PUSH_PROMISE [stream 7; promised stream 4] +END_HEADERS    :method = GET    :path = /message/qDIYHNcfAIPP_5ITvURr-d6BGt    :authority = push.example.net
    HEADERS [stream 4] +END_HEADERS    :status = 200
    date = Thu, 11 Dec 2014 23:56:56 GMT
    last-modified = Thu, 11 Dec 2014 23:56:55 GMT
    cache-control = private
    link = </push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV>;
                 rel="urn:ietf:params:push"
    content-type = text/plain;charset=utf8
    content-length = 36
    DATA [stream 4] +END_STREAM
    iChYuI3jMzt3ir20P8r_jgRR-dSuN182x7iB
    HEADERS [stream 7] +END_STREAM +END_HEADERS    :status = 200

Web 客户端接收到消息后,必须回应一个 HTTP DELETE 给 push service,

DELETE /message/qDIYHNcfAIPP_5ITvURr-d6BGt HTTP/1.1Host: push.example.net

push service 在收到 Web 客户端的回应后,必须返回一个 204 的响应给 application server。如果没有收到回应,则会认为消息还未派发到 Web 客户端,此时 push service 应该继续重试,直至消息过期。

  • 安全私隐方面的考虑

推送服务需要考虑安全相关的一些问题,比如推送的消息需要进行加密,用户设备信息不应包含在消息推送中,过多异常请求时能够拒绝服务,服务器日志不应暴露用户信息,等等。
从上面可以看看,推送服务至少需要具备的一些能力,比如,支持HTTPS,支持 http/2 server push,支持 POST,GET,DELETE 请求,能够根据请求生成订阅信息,能够根据请求头部控制响应的优先级,能够存储消息一段时间。即服务器端有一定的实现成本。
Chrome 52 已支持标准 Web Push Protocol,其余 Web 客户端可参考其实现,可以降低实现成本。

支持情况

在国外,Google Chrome,Firefox,以及一些厂商的系统浏览器,比如三星,都已支持 Push API。预计后面会越来越多的浏览器厂商支持。

在国内,除了使用了 U4 内核的客户端外,还尚未有其他客户端在官方渠道宣布支持 Push API,未来情况也不明朗,主要门槛在于 push service 的缺失。

总结

从上面我们可以看到,Push API 提供了一种页面服务器与页面交互的方法,是 Web page 能成长为 Web App 所需的关键能力。同时,我们也可看到,国内是不能使用 GCM/FCM 作为 push service 的,原因众所周知。如果自己搭建 push service,服务器的软硬件都需要不小的成本,而 push service 所带来的回报却无法预知。另外,基于 Web Push 推送的消息如何审核也是一个必须考虑的问题。
如果 Web Push 一直得不到支持,对于提供内核能力的 SDK 来说,可以提供一套非标准的 API,允许客户端(比如基于 U4 内核的一款 APP)推送消息给内核的 ServiceWorker,push service 则由客户端自行实现。


最新最好的内核技术文章,请搜索并关注公众号"U4内核技术"或"u4core"。

发布于 2018-12-06 17:49