實戰 Comet 應用程序開發



 級別: 中級

成 富 ([email protected]), 軟件工程師, IBM 中國軟件開發中心

2008 年 7 月 15 日

Comet 是一種新的 Web 應用架構。基於這種架構開發的應用中,服務器端會主動以異步的方式向客戶端程序推送數據,而不需要客戶端顯式的發出請求。Comet 架構非常適合事件驅動的 Web 應用,以及對交互性和實時性要求很強的應用,如股票交易行情分析、聊天室和 Web 版在線遊戲等。本文在介紹 Comet 架構的基礎上,詳細說明了如何利用 WebSphere Application Server Feature Pack for Web 2.0 和 Dojo 來開發基於 Comet 的應用程序,並給出了兩個具體的實例。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->

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”
 


 
圖 2. 在“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 項目
 

 

接下來就按照嚮導的默認選項就可以了。

爲了啓用 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<PositionPair> 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 operation = 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 operation = 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 架構比較適合交互性和實時性要求比較高的應用,聊天室就是其中的一種。在聊天室中,用戶總是希望自己的發送的消息能更快的讓其他用戶看到,同時能夠更快的看到其他用戶的消息。

在聊天室這個應用中,主要使用客戶端發送數據,服務器端只是負責中轉數據。需要在服務器端的 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 = ["<b>", 
                decodeURIComponent(msgObject["sender"]) || "匿名用戶", 
                "</b> 說:",     
                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 爲開發這樣的應用提供了良好的支持,可以作爲很好的出發點。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章