websocket跨平台移动消息推送

       消息推送对于 APP 至关重要,一个及时有效的消息推送能够帮助客户获取有价值的信息,所以消息推送是移动应用的一项重要功能。目前主流的移动操作系统(Android、iOS)的 webview 都已经支持 websocket, 所以对于移动应用,webscoket 也将成为消息推送选择。

移动应用消息推送

   应用一般使用 Cordova 之类的中间件,以 WebView 作为用户界面层,以 Javascript 作为基本逻辑,以及和中间件通讯,再由中间件访问底层 API 的方式,进行应用开发。开发时可能不采用或者大部分不采用原生语言,但是却有所有原生应用的特性。而开发消息推送功能时,既可以使用 Native App 的系统自带推送如 GCM 和 APNS,又可以使用基于 html5 的 websocket 推送。

对于混合应用的 websocket 消息推送,其基本原理如下:

wKiom1glQ3SQSAWrAAArNZH-Pw0275.png

图 2. 原生 app 消息推送工作流程

wKiom1glQ_2CQ4DjAACOFQ147eI331.png

       后者主要应用于原生 app, 而前者由于开发周期短,跨平台性好,维护成本低,用于混合应用的消息推送。基于 websocket 的消息推送,实现消息推送的服务端,这样掌握了推送服务的主动权,对于安全性极高的企业,websocket 推送无疑是最好的选择,因为如果使用 GCM 或者 APNS 推送,不得不将信息发送到 GCM server 或者 APNS server, 再由 GCM 或者 APNS 服务端转发到客户端,信息安全性不得而知。一旦推送服务器出现异常,消息推送将变得非常被动。 但是 GCM 和 APNS 也是使用长连接进行消息推送,而且一个手机上的所有 app 共用一个长连接,对于手机性能将会有极大的帮助。两种推送各有利弊,读者可自己选择。本文将针对 websocket 的消息推送进行介绍


Websocket 接口简介

       WebSocket 的实现分为客户端和服务端两部分,客户端(通常为浏览器)发出 WebSocket 连接请求,服务端响应,实现类似 TCP 握手的动作,从而在浏览器客户端和 WebSocket 服务端之间形成一条 HTTP 长连接快速通道。两者之间后续进行直接的数据互相传送,不再需要发起连接和响应。同时两者都可以关闭这个长连接。是利用了混合移动应用的 webview 可以支持 websocket 的这个特性来实现服务器端对客户端的一个消息推送。Websocket 针对客户端而言,性能,资源使用以及及时性要比传统的轮询更好。

Websocket 客户端 API

对于 websocket 客户端,目前主流的移动操作系统的 webview 层都已经支持 websocket 服务。以下列举了常见的移动系统支持情况:

表 1. 主流移动系统的 webview 对 websocket 的支持情况

移动系统版本
IOSIOS 5+
AndroidAndroid 4.4
Windows phoneWindows phone 8+

常见浏览器和移动系统的 webview 都已经实现了 w3 规范的 websocket 接口,具体接口参考清单 1

1. Websocket 客户端接口

[Constructor(in DOMString url, in optional DOMString protocol)]
interface WebSocket {
  readonly attribute DOMString URL;
       // ready state
  const unsigned short CONNECTING = 0;
  const unsigned short OPEN = 1;
  const unsigned short CLOSED = 2;
  readonly attribute unsigned short readyState;
  readonly attribute unsigned long bufferedAmount;
  //networking
  attribute Function onopen;
  attribute Function onmessage;
  attribute Function onclose;
  boolean send(in DOMString data);
  void close();
};
WebSocket implements EventTarget;

       URL 属性代表 WebSocket 服务器的网络地址,协议通常”ws”或者”wss“,send 方法就是发送数据到服务器端,close 方法就是关闭连接。除了这些方法,还有一些很重要的事件:onopen,onmessage,onerror 以及 onclose。详细解释请参考表 2

表 2. Websocket 的对象方法属性

名称类型描述
WebSocket对象提供到远程主机的双向通道
close方法关闭 websocket
send方法使用 websocket 发送数据到服务器
binaryType属性由 onmessage 接收的二进制数据格式。
bufferedAmount属性使用 send 的已排队的数据字节数
extensions属性报告服务器所选中的扩展名。
onclose属性当套接字关闭时调用的事件处理程序。
onerror属性当出现错误时调用的事件处理程序。
onmessage属性通知接收到消息的事件处理程序。
onopen属性当 websocket 已连接时调用的事件处理程序。
protocol属性报告服务器所选中的协议。
readyState属性报告 websocket 连接的状态。
url属性报告套接字的当前 URL。

下面一段代码展示了建立 websocket 实例:

2. Websocket 创建连接实例

var ws= new WebSocket("ws://localhost:8080/PushNotification");
ws.onopen = function (event) {
console.log("connected to server");
};
ws.onmessage = function (event) {
//when a new message coming, we will call Cordova plugin here

    当有新消息到达时,onmessage 会自动触发,在这个方法里利用 Cordova plugin 去实现调用 android 或者 ios 的 notification。


Websocket 服务器端 API

对于 websocket 服务器端,目前主流的 web 服务器都已经支持。以下列举了常见的服务器支持情况:

WEBSOCKETS IN NGINX

    1.3.13版本的Nginx支持的连接升级,作为WebSockets代理!很多人都在等待着这个“WebSocketsd的Nginx中的支持。以下是最常见的进行在Nginx的WebSocket实现。

Nginx中websoket设置

location /chat/ {
   proxy_pass http://backend;
   proxy_http_version 1.1;
   proxy_set_header Upgrade $http_upgrade;
   proxy_set_header Connection "Upgrade";
}

    这是相当简单的配置,没有使用所有新的HTTP版本。通过创建一个连接变量,让proxy_set_headers成通用的包含文件。

map $http_connection $upgrade_requested {
   default upgrade;
   ''      close;
}
,,

      这使得变量$ upgrade_requested可连接proxy_set_header,如果连接升级不要求回应,默认值不干扰正常的请求。即如果你总是代理HTTP / 1.1那么你不需要一个专门处理WebSocket的位置。
      连接升级似乎不太可能被移植到稳定的分支,所以如果你想使用这个,你将不得不使用qi分支。值得庆幸的是,在nginx的发展并不意味着它不运行稳定,它只是意味着API可能会改变,因此它只影响模块的作者。不要害怕安装开发版本来玩这个新的功能。

对websocke的限制

客户端必须完成 Connection Upgrade
不然会导致错误或丢失功能
WebSockets 的Time Out
WebSockets的proxy_read_timeout 缺省值为:60秒.
Keep-Alive 和 WebSockets
Keep-alive pings 不能使用,TCP端为空包。继续切换proxy_read_timeout的值,不要害怕。
WebSockets 支持 SSL
由于WebSockets 以进入正常的proxy模式 SSL 应使用相同的方法.
Proxy Buffers代理缓存
WebSockets包有两个内存Buffer,决定proxy_buffer_size的大小,。一个upstream data数据,还有一个到磁盘.
Upgrade Header的问题例子
一些带宽做upgrade header检测同时请求header到另一个“upgrade”或“Upgrade”, 有时看上去正确但是不工作,请改变header值.

其他还有Weblogic12c+、iis7.0+、tomcat7.0.5+、jetty7.0+


利用 Cordova plugin 调用本地 notification

以上了解了如何在 App 中创建 websocket 连接,当我们收到信息时,这个时候只需要调用 IOS 或者 Android 的本地消息推送即可让用户知道有新的信息到达。这个时候我们就要创建一个新的 Cordova plugin 去触发 notification。

创建一个 Cordova plugin

Cordova plugin 是我们通过 JavaScript 调用系统 API 的中间件,一般情况下通过 JavaScript 不能够完成而系统 API 可以完成的任务时,我们就要创建一个 plugin。不同的平台在调用底层 API 时会有不同,但是前段代码不会改变。 以下我们将创建一个 Cordova plugin 用来调用本地消息推送。下文以 android 平台为例,其他平台思路类似。

1. 在 cordova_plugins.js 中引入新的 plugin

{
"file": "plugins/com.test.notification/www/notification.js",
"id": "com.test.notification.localNotification",
"clobbers": [
"cordova.plugins.localNotification" ]
}

2. 然后在 plugins/com.test.notification/www 目录下边创建 notification.js,代码示例如下:

cordova.define("com.test.notification.localNotification",
 function(require, exports, module) {
var argscheck = require('cordova/argscheck'),
   utils = require('cordova/utils'),
   exec = require('cordova/exec');
var localNotification = function() {
};
localNotification.sendNotify = function(message,success, error) {  
  cordova.exec(success, error, 'localNotification', 'sendNotify', message);
};
module.exports = localNotification;
});

3. 创建一个 java 类来继承 Cordova 接口,代码示例如下

package com.test.notification;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

//ellipsis code
// …… .

package com.test.notification;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

//ellipsis code
// …… .


public class localNotification extends CordovaPlugin {
//ellipsis code
// … ..
@Override
public boolean execute(String action, final JSONArray args,
final CallbackContext callbackContext) throws JSONException {
if ("sendNotify".equals(action)) {
     NotificationManager manager = (NotificationManager) this.cordova
         .getActivity().getSystemService(
             Context.NOTIFICATION_SERVICE);

     String title = args.getString(0);
     String text = args.getString(1);
     Notification notification
     = new Notification.Builder(this.cordova.getActivity())
     .setTicker("New notification").setDefaults(1)
     .setSmallIcon(R.drawable.icon)
     .setAutoCancel(true)
     .setContentTitle(title).setContentText(text)
     .setContentIntent(PendingIntent.getActivity(this.cordova.getActivity(),
             0, this.cordova.getActivity().getIntent(), 0)).build();
     manager.notify(1, notification);

     return true;
                 }
        }
}

在 res/xml/config.xml 配置 plugin feature

至此一个完整的 plugin 已经建立完成。然后我们只需要在 JavaScript 端调用这个 plugin 就可以触发一个 notification,具体调用方式参考清单 4:

4. 客户端调用系统 notification

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

//ellipsis code
// …… .

package com.test.notification;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
 var ws= new WebSocket("ws://localhost:8080/PushNotification");
ws.onopen = function (event) {
console.log("connected to server");
};
ws.onmessage = function (event) {
var notifys = jQuery.parseJSON(evnt.data);
   var message = ["New Job Notification",notifys[0].message];
   cordova.plugins.localNotification.sendNotify(message);
};

   当执行 cordova.plugins.localNotification.sendNotify 方法时,cordova 会向 webview 发送一个 XMLHttpRequest 请求并且包含的参数中有 sendNotify 关键字,这个请求会被我们已经实现的 Cordova plugin 拦截住,并且执行一个 android Notification,这个时候一个消息就成功推送到客户端。如下图

图 3. 通过 websocket server 成功将信息推送到客户端

wKiom1gnHXjBR1gvAAGz9IEZ2gQ794.png-wh_50

Websocket 消息推送优点 :
1. 开发周期短,维护成本低。
2. 消息不经转第三方服务器,直接由服务器发送到客户端,安全性好。基于 GCM 或者 APNS 的消息推送会把消息发送 GCM 服务器或 APNS 服务器,再由他们转发到客户端。
3. 自己开发服务端,可扩展性好。
4. 对于客户端而言,长连接比轮询的方式性能和及时性更好。

参考资料