2013年7月5日 星期五

GWT 與 WebSocket [上]

前言

這一陣子因為專案需要 server 與 browser 之間有即時雙向溝通的能力,所以就用了 websocket。 又由於 server 端綁定 PHP(還好 browser 也綁定 Chrome XD), 再加上傳輸的資料沒有很複雜,單純字串就可以解決, 所以沒有用現有的 GWT websocket library,統統自己來了...... [遮臉]

於是也就順便寫了這篇文章,借 websocket 介紹下面兩個主題:

  1. GWT 如何整合 JavaScript 程式碼(JSNI)
  2. EventHandler 的使用

(當然行有餘力的話也想涵蓋 GWT RPC,不過目前無法)

這三個主題在作〈GWT 版 GAE Channel API〉的時候都有用到 (但是 RPC 的部份只抄其 code、不明其理 [遮臉]), 不過拿 websocket 來介紹可能比較實在一點,GAE 的 channel API 可能太少人用了 [死]。

webocket

因為我們要用 GWT 把 websocket 給包起來,所以還是得理解一下 websocket client 端的行為。

// ======== JavaScript code ======== //
//建立 websocket,目前似乎 FOO_PATH 打啥都沒差,主要還是看 address 跟 port 號
var websocket = new WebSocket("ws://localhost:8000/FOO_PATH");

websocket.onopen = function(evnt) { alert("connected!"); }
websocket.onmessage = function(evnt) {
    alert("Receive : " + evnt.data);
}
//其他還有 websocket.onerror 跟 websocket.onclose,自己比照辦理

//送資料
websocket.send(SOMETHING);

大抵上可以望文生意,就不特別作解釋了 [逃]

JSNI

JSNI 全名 JavaScript Native Interface, 用不太嚴謹的類比講法就是 GWT 中的 JNI,不過沒有 JNI 那麼複雜。 畢竟 GWT compile 完的最終結果來看,所有程式碼統統會變成 JavaScript, 沒有 JavaScript 反向呼叫 Java code 的問題,因此單純很多。

開發人員可以透過 JSNI 把某一些 JavaScript 程式碼重新包裝成 Java code, 然後就可以完全用 Java 的思維與寫法去使用它(其實 GWT 底層滿滿的都是 JSNI)。 例如 JavaScript 當中常用的 console.log(),就可以這樣包起來:

public class Console {
    public static native void log(String message) /*-{
        $wnd.console.log(message);
    }-*/;
}

//使用
Console.log("hello world");

完整的 JSNI 文件可以參閱 GWT 文件,或是這兩篇中文 Blog:DontCare 1DontCare 2。 這篇文章只針對必要的部份作講解。

要把 JavaScript 的 websocket 包起來,會稍微複雜一點。 首先必須得要建立一個 JsWebSocket 物件像這樣:

// ======== Java code ======== //
class JsWebSocket extends JavaScriptObject {
    //必須要有一個 protected 的 contructor
    protected JsWebSocket() {}

    //將 WebSocket.send() 包成 Java method
    public native final void send(String msg) /*-{
        this.send(msg);
    }-*/;
}

JsWebSocket 算是 Java object 與 JavaScript object 中間的橋樑, 畢竟以 Java 的角度,必須要明確定義 class 有哪些 method 與 field, 所以包一個 class 來滿足這個需求。

再來是我們真正要在程式當中使用的 WebSocket

// ======== Java code ======== //
package psmonkey.client.websocket;
public class WebSocket {
    private String uri;
    private JsWebSocket jsWebSocket;

    public WebSocket(String uri) {
        this.uri = uri;
    }

    public void send(String msg) {
        jsWebSocket.send(msg);
    }

    public native void open() /*-{
        //在 native method 當中就是寫 JavaScript
        var websocket = new $wnd.WebSocket(this.@psmonkey.client.websocket.WebSocket::uri);
        this.@psmonkey.client.websocket.WebSocket::jsWebSocket = websocket;

        //直接複製貼上剛剛的 JavaScript 碼
        websocket.onopen = function(evnt) { alert("connected!"); }
        websocket.onmessage = function(evnt) {
            alert("Receive : " + evnt.data);
        }

        //onerror、onclose 繼續跳過 XD
    }-*/;
}

關鍵點在於 open() 這個 method。 首先,透過 this.@PACKAGE_NAME.CLASS_NAME::FIELD_NAME 這種扭曲的方式, 我們可以在 native method 當中存取 Java code 的 field; 而 jsWebSocket 也是用這樣的方式 assign 值。 然後我們就可以用很 Java 的方式來作到一開始 JavaScript 程式碼的功能:

// ======== Java code ======== //
WebSocket websocket = new WebSocket("ws://localhost:8000/FOO_PATH");
websocket.open();

websocket.send(SOMETHING);

但是這樣還不夠 Java,因為 onopenonmessage 的行為都還是用 JavaScript 寫。 所以接下來要把這部份也給 Java 化

// ======== Java code ======== //
//in class : WebSocket
public native void open() /*-{
    //在 native method 當中就是寫 JavaScript
    var websocket = new $wnd.WebSocket(this.@psmonkey.client.websocket.WebSocket::uri);
    this.@psmonkey.client.websocket.WebSocket::jsWebSocket = websocket;

    var self = this;
    websocket.onopen = function(event) {
        self.@psmonkey.client.websocket.WebSocket::onOpen()();
    }
    websocket.onmessage = function(event) {
        self.@psmonkey.client.websocket.WebSocket::onMessage(Ljava/lang/String;)(event.data);
    }
}

public void onOpen() {
    Window.alert("connected!");
}

public void onMessage(String data) {
    Window.alert("Receive : " + data);
}

一樣也是 this.@PACKAGE_NAME.CLASS_NAME::METHOD_NAME 這種扭曲的方式, 可以在 native method 當中去指定要呼叫的 Java method。 如此一來,後頭就都是 Java 的事情了!

沒有留言:

張貼留言