實戰Comet 應用程式開發

CloudSpace發表於2008-07-21

Comet 是一種新的 Web 應用架構。基於這種架構開發的應用中,伺服器端會主動以非同步的方式向客戶端程式推送資料,而不需要客戶端顯式的發出請求。Comet 架構非常適合事件驅動的 Web 應用,以及對互動性和實時性要求很強的應用,如股票交易行情分析、聊天室和 Web 版線上遊戲等。本文在介紹 Comet 架構的基礎上,詳細說明了如何利用 WebSphere Application Server Feature Pack for Web 2.0 和 Dojo 來開發基於 Comet 的應用程式,並給出了兩個具體的例項。

Comet 及相關技術簡介

Comet 指的是一種 Web 應用程式的架構。在這種架構中,客戶端程式(通常是瀏覽器)不需要顯式的向伺服器端發出請求,伺服器端會在其資料發生變化的時候主動的將資料非同步的傳送給客戶端,從而使得客戶端能夠及時的更新使用者介面以反映伺服器端資料的變化。

這種架構既不同於傳統的 Web 應用,也不同於新興的 Ajax 應用。在傳統的 Web 應用中,通常是客戶端主動的發出請求,伺服器端生成整個 HTML 頁面交給客戶端去處理。在 Ajax 應用中,同樣是客戶端主動的發出請求,只是伺服器通常返回的是 XML 或是 JSON 格式的資料,然後客戶端使用這些資料來對頁面進行區域性更新。Comet 架構非常適合事件驅動的 Web 應用和對互動性和實時性要求很強的應用。這樣的應用的例子包括股票交易行情分析、聊天室和 Web 版線上遊戲等。

基於 Comet 架構的 Web 應用使用客戶端和伺服器端之間的 HTTP 長連線來作為資料傳輸的通道。每當伺服器端的資料因為外部的事件而發生改變時,伺服器端就能夠及時把相關的資料推送給客戶端。通常來說,有兩種實現長連線的策略:

HTTP 流(HTTP Streaming) 這種情況下,客戶端開啟一個單一的與伺服器端的 HTTP 持久連線。伺服器通過此連線把資料傳送過來,客戶端增量的處理它們。 HTTP 長輪詢(HTTP Long Polling) 這種情況下,由客戶端向伺服器端發出請求並開啟一個連線。這個連線只有在收到伺服器端的資料之後才會關閉。伺服器端傳送完資料之後,就立即關閉連線。客戶端則馬上再開啟一個新的連線,等待下一次的資料。

WebSphere Application Server Feature Pack for Web 2.0 簡介

WebSphere Application Server Feature Pack for Web 2.0 是 IBM 支援的解決方案,用於在 Websphere Application Server 上建立基於 Ajax 的應用和 mashup。除了 Ajax 開發工具之外,該功能部件包還包含了對伺服器端的增強功能,用來支援通用的 Web 2.0 應用模式。該功能部件包提供了對開發 Web 2.0 應用的很多增強。主要有三個方面:Web 2.0 到 SOA 的連線性、Ajax 訊息處理和 Ajax 開發工具箱。關於該功能部件包的具體內容,請看 參考資源。該功能部件包有適用於 WebSphere Application Server 和 WebSphere Application Server Community Edition 的不同版本。

Dojox.cometd 簡介

Dojo 的創始人 Alex Russell 最開始提出“Comet”這個詞。Dojo 基金會提出了 Bayeux 協議用來標準化 Comet 應用中客戶端和伺服器端之間的通訊。關於 Bayeux 協議的具體資訊,請看 參考資源。Dojox.cometd 實現了 Bayeux 協議的客戶端部分,使用 HTTP 長輪詢來作為資料的傳輸通道。

 

構建開發環境

為了能夠開發使用 WebSphere Application Server Feature Pack for Web 2.0 的 Comet 應用,需要下載 WebSphere Application Server Feature Pack for Web 2.0。WebSphere Application Server Feature Pack for Web 2.0 有適用於 WebSphere Application Server 和 WebSphere Application Server Community Edition 的不同版本,請注意下載正確的版本。本文中使用的是適用於 WebSphere Application Server Community Edition 的版本。適用於 WebSphere Application Server 上的版本的配置與 Community Edition 有所不同,您需要參考相應的說明文件。您可以在 參考資源 中找到相關的下載地址。

在下載並安裝好 WebSphere Application Server Community Edition 和相應版本的 Feature Pack for Web 2.0 之後,就可以繼續下面的步驟了。為了能夠更加有效的開發,我推薦使用 Eclipse 的 Web Tools Platform(WTP)來進行開發。Eclipse WTP 整合了對各種應用伺服器的內嵌支援,可以很容易的在 Eclipse 內部啟動、停止和配置應用伺服器。Eclipse WTP 預設沒有 WebSphere Application Server Community Edition 的支援,您需要通過 WTP 來手動安裝。您可以在 參考資源 中找到相關的下載地址。

您可以參考下面兩張截圖來為 WTP 安裝 WebSphere Application Server Community Edition 的支援。


圖 1. 在“New Server Runtime”選擇“Download additional server adapters”
在 New Server Runtime 選擇 Download additional server adapters

圖 2. 在“Install New Server Adapter”中選擇“WASCE v2.0 Server Adapter”
在 Install New Server Adapter 中選擇 WASCE v2.0 Server Adapter 

建立新的 Comet 專案

在為 Eclipse WTP 安裝完成對 WebSphere Application Server Community Edition 的支援之後,就可以開始建立 Comet 專案了。

建立 Comet 專案和一般的 Dynamic Web Project 是類似的。只是在選擇“Target Runtime”的時候要選擇“IBM WASCE v2.0”。如下圖所示:


圖 3. 建立新的 Comet 專案
建立新的 Comet 專案

接下來就按照嚮導的預設選項就可以了。

為了啟用 WebSphere Application Server Community Edition 對 Comet 的支援,還需要做進一步的配置。這些配置包括為 Tomcat 啟用 HTTP NIO 監聽器,提供 JMS 訊息服務等。關於這些配置的具體資訊,可以在 Feature Pack for Web 2.0 中找到詳細的文件。

Comet 應用基本架構

使用 WebSphere Application Server Feature Pack for Web 2.0 和 Dojo 開發的 Comet 應用由伺服器端和客戶端兩部分組成。伺服器端由 com.ibm.webmsg.servlet.BayeuxServlet 提供 HTTP 長連線支援,客戶端則由 dojox.cometd 包提供支援。兩者都實現了 Bayeux 協議。

伺服器端

Comet 應用的伺服器端需要提供一個繼承自 com.ibm.webmsg.servlet.BayeuxServlet 的 Servlet 來提供與客戶端之間的持久 HTTP 連線。通常來說,這個 Servlet 的實現類似如下程式碼所示:


清單 1. Comet 應用伺服器端程式碼
                
public class BrownianMotionServlet extends BayeuxServlet {

    @Override
    public void registerURL() {
        getServletUtil().addClientManager("/brownianMotionServlet", clientManager);
    }

    @Override
    public void setProperties() {
        setCometTimeout(30000);
        setClientPollInterval(2);
        setRouterType(JMS);
        setClientsCanPublish(false);
    }
}

首先,需要為該 Servlet 指定一個 URI 來傳送資料,這是通過 registerURL 方法來實現的。接著可以在 setProperties 方法設定相關屬性:用 setCometTimeout 設定客戶端請求的超時時間;用 setClientPollInterval 設定客戶端請求之間的間隔時間;用 setRouterType 設定資料傳輸的通道型別,目前有使用記憶體和 JMS 兩種可以選擇,分別用 setRouterType(SIMPLE)setRouterType(JMS) 來設定;用 setClientsCanPublish 設定客戶端是否可以釋出資料。

當服務端需要釋出資料給客戶端的時候,可以通過 com.ibm.ws.webmsg.publisher.DataPublisherpublish 方法來傳送針對特定主題的資料。

客戶端

客戶端為了能夠接收伺服器端釋出的資料,首先要初始化到伺服器端某個通道的連線,然後定義對於特定主題資料的處理方法。參看下面的程式碼:


清單 2. Comet 應用客戶端程式碼
                
dojo.addOnLoad(function(){


    dojox.cometd.subscribe("/motion", window, "display");
    initControls();
    getTemperature();
});

在上面的程式碼中,dojox.cometd.init("brownianMotionServlet") 用來初始化到伺服器端某個通道的連線。這裡使用的 URI brownianMotionServlet 和之前在伺服器端用 registerURL 方法宣告的 URI 是一樣的。dojox.cometd.subscribe 用來宣告對某個主題的資料執行的處理。如上所示,每當接收到名為“/motion”的主題的資料時,就呼叫 window 物件的 display 方法。接收到的資料會作為 display 方法的引數傳入。

在介紹完 Comet 應用的基本架構之後,接下來將通過兩個具體的例子來說明如何開發 Comet 應用。第一個例子是布朗運動的模擬。這個例子主要展示的是如何在伺服器端將持續變化的資料以推送的方式傳送給客戶端做處理。這個是典型的事件驅動的應用。第二個例子是基於 Comet 的聊天室。這個例子主要展示的是如何利用 Comet 的客戶端釋出資料的能力,把伺服器作為資料傳輸的匯流排。這個是典型的對互動性和實時性要求很強的應用。

布朗運動模擬

布朗運動指的是懸浮微粒不停地做無規則運動的現象。它是 1826 年由英國植物學家布朗用顯微鏡觀察懸浮在水中的花粉時發現的。不只是花粉和小炭粒,對於液體中各種不同的懸浮微粒,都可以觀察到布朗運動。布朗運動模擬在物理教學上有一定的意義,可以方便學生更直觀的看到微粒的運動情況。

下面的這個 Comet 應用是在 Web 頁面上模擬布朗運動。布朗運動的模擬需要大量的資料計算,這樣的工作是交給伺服器端來處理。伺服器端根據一定的演算法計算出每個微粒在不同時刻的位置,然後把相應的資料推送給瀏覽器。瀏覽器負責根據這些資料生成相應的使用者介面,方便使用者直觀的看到微粒的運動情況。

出於簡化問題的需要,該示例應用中只是模擬少量的微粒,預設只有 100 個微粒。它們的運動規律是每隔一段時間,其移動方向就會相對當前方向發生一定的偏移。溫度越高,偏移的角度就越大。這是符合布朗運動的規律的。在瀏覽器端,是以紅色小方塊來表示微粒的當前位置的。在瀏覽器端也提供使用者介面讓使用者設定模擬時的溫度,方便使用者看到溫度的改變對微粒運動的影響。

在該 Comet 應用中,瀏覽器和伺服器端既有資料流,又有控制流。資料流是通過 HTTP 長連線來傳輸資料的,而控制流是通過一般的 HTTP GET 和 POST 請求來實現的。資料流是用來傳輸布朗運動模擬中微粒的位置資訊,而控制流用來獲取和設定模擬時的溫度。

資料流

首先介紹資料流。在應用啟動之後,會啟動一個定時器(MotionTimer),該定時器定時的將模擬出來的微粒的位置資料以 JSON 格式傳送到特定的主題上。這是通過 com.ibm.ws.webmsg.publisher.DataPublisherpublish 方法來實現的。


清單 3. 伺服器端定時將微粒的位置資訊以 JSON 格式推送給瀏覽器
                
public class AppInit extends javax.servlet.http.HttpServlet {
    
    private static final int SNAPSHOT_INTERVAL = 5000;

    private static final int PARTICLE_NUMBER = 100;

    public static final String TIMER_KEY = "PublishTimer";
    
    public static final String UPDATER_KEY = "MotionUpdater";
    
    public static final String MOTION_TOPIC = "/motion";
    
    private static final Logger logger = Logger.getLogger(AppInit.class.getName());

    @Override
    public void init() throws ServletException {
        super.init();
        MotionSnapshot snapshot = new MotionSnapshot();
        snapshot.setParticles(ParticleGenerator.generate(PARTICLE_NUMBER));
        MotionUpdater updater = new MotionUpdater();
        getServletContext().setAttribute(UPDATER_KEY, updater);
        try {
            DataPublisher publisher = new DataPublisher();
            Timer timer = new Timer();
            //建立定時器
            MotionTimer mt = new MotionTimer(snapshot, updater, publisher);
            timer.scheduleAtFixedRate(mt, 1000, SNAPSHOT_INTERVAL);
            getServletContext().setAttribute(TIMER_KEY, timer);
            
            logger.info("Brownian motion simulation started successfully.");
            
        } catch (Exception e) {
            logger.log(Level.WARNING, e.getMessage(), e);
        }
    }

    private class MotionTimer extends TimerTask {
        
        private MotionSnapshot snapshot;

        private MotionUpdater updater;

        private DataPublisher publisher;

        public MotionTimer(MotionSnapshot snapshot, MotionUpdater updater,
                DataPublisher publisher) {
            this.snapshot = snapshot;
            this.updater = updater;
            this.publisher = publisher;
        }

        @Override
        public void run() {
            updater.update(snapshot);
            List pairs = snapshot.getSnapshot();
            StringBuilder builder = new StringBuilder();
            builder.append("[");
            for (PositionPair pair : pairs) {
                builder.append("{\"x\":");
                builder.append(pair.getPosX());
                builder.append(",\"y\":");
                builder.append(pair.getPosY());
                builder.append("},");
            }
            builder.deleteCharAt(builder.length() - 1);
            builder.append("]");
            try {
                //傳送資料
                publisher.publish(MOTION_TOPIC, builder.toString());
            } catch (JMSException e) {
                logger.log(Level.WARNING, e.getMessage(), e);
            }
        }

    }

}

瀏覽器端只需要在同樣的主題上註冊處理相應的方法就可以對伺服器端釋出的資料進行處理。這是通過 dojox.cometd.subscribe 方法來實現的。


清單 4. 瀏覽器端處理微粒位置資訊
                
dojo.require("dojox.cometd");
dojo.addOnLoad(function(){
    dojox.cometd.init("brownianMotionServlet")
    dojox.cometd.subscribe("/motion", window, "display");
    initControls();
    getTemperature();
});
            
function display(msg){
    dojo.byId("motionArea").innerHTML = "";
    dojo.forEach(msg.data || [], function(particle) {
        var div = dojo.doc.createElement("div");
        dojo.addClass(div, "particle");
        dojo._setBox(div, particle.x, particle.y);
        dojo.byId("motionArea").appendChild(div);
    });
}

從上面可以看到,瀏覽器端根據伺服器端釋出的微粒的位置資訊,以一個 HTML DIV 元素表示一個微粒,並放置在適當的位置。

控制流

對於控制流的處理相對簡單。處理控制邏輯的是一個普通的 Servlet,在其 doGetdoPost 方法中實現獲取和設定溫度的邏輯。


清單 5. 伺服器端處理控制邏輯的程式碼
                
public class MotionControlServlet extends HttpServlet implements Servlet {

    private static final int MAX_TEMPERATURE = 200;

    private static final Logger logger = Logger
            .getLogger(MotionControlServlet.class.getName());

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/plain");
        String peration = req.getParameter("operation");
        if (operation == null) {
            logger.warning("Client has sent empty operation!");
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            resp.getOutputStream().print("Please specify the operation!");
            return;
        }

        if (operation.equalsIgnoreCase("getTemperature")) {
            MotionUpdater updater = (MotionUpdater) getServletConfig()
                    .getServletContext().getAttribute(AppInit.UPDATER_KEY);
            if (updater != null) {
                int temperature = updater.getTemperature();
                resp.setStatus(HttpServletResponse.SC_OK);
                resp.getOutputStream().print(temperature);
            }
            else {
                resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                resp.getOutputStream()
                    .print("Can not get the temperature, please try again later!");
            }
        } else {
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            resp.getOutputStream().print("Unknown operation type!");
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/plain");
        String peration = req.getParameter("operation");

        if (operation == null) {
            logger.warning("Client has sent empty operation!");
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            resp.getOutputStream().print("Please specify the operation!");
            return;
        }
        if (operation.equalsIgnoreCase("changeTemperature")) {
            String tempStr = req.getParameter("temperature");
            if (tempStr == null || tempStr.trim().equals("")) {
                logger.warning("Client has sent empty value of temperature!");
                resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                resp.getOutputStream().print("Please specify the temperature!");
                return;
            }

            int temperature = 0;
            try {
                temperature = Math.min(Integer.parseInt(tempStr),
                        MAX_TEMPERATURE);
            } catch (NumberFormatException nfe) {
                logger.log(Level.WARNING,
                        "Client has sent invalid value of temperature!", nfe);
                resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                resp.getOutputStream()
                    .print("The value of temperature must be a number!");
                return;
            }

            resp.setStatus(HttpServletResponse.SC_OK);

            MotionUpdater updater = (MotionUpdater) getServletConfig()
                    .getServletContext().getAttribute(AppInit.UPDATER_KEY);
            if (updater != null) {
                updater.setTemperature(temperature);
                resp.getOutputStream().print(temperature);
                logger.info("Temperature has been changed to " + temperature);
            }
        } else {
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            resp.getOutputStream().print("Unknown operation type!");
        }
    }

}

在瀏覽器端,使用 Dojo 的 xhrGetxhrPost 來與伺服器端互動。


清單 6. 瀏覽器端處理控制邏輯的程式碼
                
//獲取模擬時的溫度
function getTemperature() {
    var messageBox = dojo.byId("messageBox");
    messageBox.innerHTML = "";
    dojo.xhrGet({
        url : "/BrownianMotion/control?operation=getTemperature",
        handleAs : "text",
        load : function(response) {
            var temperature = dojo.byId("temperature");
            temperature.innerHTML = response;
        },
        error : function(response, ioArgs) {
            messageBox.innerHTML = ioArgs.xhr.responseText;
        }
    });
}

//更新模擬時的溫度            
function updateTemperature() {
    var messageBox = dojo.byId("messageBox");
    messageBox.innerHTML = "";
    var tempInput = dojo.byId("temperatureInput");
    var value = dojo.trim(tempInput.value);
    if (value.length > 0) {
        dojo.xhrPost({
            url : "/BrownianMotion/control",
            handleAs : "text",
            content : {
                "operation" : "changeTemperature",
                "temperature" : value
            },
            load : function(response) {
                var temperature = dojo.byId("temperature");
                temperature.innerHTML = response;
            },
            error : function(response) {
                messageBox.innerHTML = ioArgs.xhr.responseText;
            }
        });
    }
}

該 Comet 應用實際執行的截圖如下:


圖 4. 布朗運動模擬的 Comet 應用截圖
布朗運動模擬的 Comet 應用截圖 

基於 Comet 的聊天室

前面提到過,Comet 架構比較適合互動性和實時性要求比較高的應用,聊天室就是其中的一種。在聊天室中,使用者總是希望自己的傳送的訊息能更快的讓其他使用者看到,同時能夠更快的看到其他使用者的訊息。

在聊天室這個應用中,主要使用客戶端傳送資料,伺服器端只是負責中轉資料。需要在伺服器端的 Servlet 設定 setClientsCanPublish(true)。在聊天室中同時有多個使用者,當其中一個使用者輸入了訊息之後,伺服器會把這些訊息廣播給在聊天室的其他使用者。


清單 7. 聊天室伺服器端程式碼
                
public class MeetingRoomServlet extends BayeuxServlet {

    @Override
    public void registerURL() {
        getServletUtil().addClientManager("/meetingRoomServlet", clientManager);
    }

    @Override
    public void setProperties() {
        setCometTimeout(30000);
        setClientPollInterval(5);
        setRouterType(SIMPLE);
        setClientsCanPublish(true);
    }
}


清單 8. 聊天室客戶端主要的 JavaScript
                
var MeetingRoom = (function() {
    var nickName = "匿名使用者";
    
    var chatArea;
    
    var topic = "/chat";
    
    return {
        //顯示訊息
        displayMessage : function(msgObject) {
            var date = new Date();
            try {
                date.setTime(msgObject["dateTime"]);
            }
            catch (error) {
                
            }
             var msg = ["", 
                decodeURIComponent(msgObject["sender"]) || "匿名使用者", 
                " 說:",     
                decodeURIComponent(msgObject["message"]), 
                "  (", date.toLocaleString(), ")"].join("");
            var div = dojo.doc.createElement("div");
            div.innerHTML = msg;
            chatArea.appendChild(div);
        },
        
        //傳送訊息
        sendMessage : function(message) {
            message = dojo.trim(message);
            if (message.length > 0) {
                dojox.cometd.publish(topic, 
                {"sender" : encodeURIComponent(nickName), 
                "message": encodeURIComponent(message), 
                "dateTime" : new Date().getTime()});
            }
        },
        
        //修改暱稱
        changeNickName : function(newNickName) {
            nickName = newNickName;
        },
        
        init : function() {
            chatArea = dojo.byId("chatArea");
        }
    }
})();

該聊天室實際執行起來的截圖如下,我使用了幾個不同的瀏覽器,並用了不同的使用者來模擬多使用者的效果。


圖 5. 聊天室應用截圖
聊天室應用截圖 

總結

本文從兩個例項出發,具體地介紹瞭如何使用 WebSphere Application Server Feature Pack for Web 2.0 和 Dojo 開發基於 Comet 架構的應用程式。可以看到,Comet 架構在很多的應用場景下都是很適合的。WebSphere Application Server Feature Pack for Web 2.0 和 Dojo 為開發這樣的應用提供了良好的支援,可以作為很好的出發點。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/14789789/viewspace-406628/,如需轉載,請註明出處,否則將追究法律責任。

相關文章