使用Tomcat實現基於iframe streaming的Comet聊天室

來源:互聯網
上載者:User

標籤:comet   iframe   stream   阻塞隊列   arrayblockingqueue   

首先,無圖無真相,先:


這是一個基於Comet實現的聊天室Demo,功能類似於QQ群聊。聊天過程中如果有新想訊息,那麼就需要伺服器推送訊息到瀏覽器,所以這裡可以使用Comet技術。

Comet一般有兩種實現方式:長輪詢(long-polling)、流(streaming)。而本文中的這個Demo的實現方式是基於流(streaming),前端使用了一個隱藏的iframe,這也是比較常用的一種方式。不過由於使用iframe流,導致瀏覽器上面的進度一直在轉,這是因為iframe一直在載入的原因,先不要在意這些細節。

Tomcat提供了Comet相關的API,用Servlet實現CometProcessor介面就可以很簡單的實現Comet了。

1、準備工作

1.1、首先,需要配置Tomcat串連為NIO,否則無法使用Tomcat Comet。

Tomcat目錄下conf/server.xml,protocol更改為org.apache.coyote.http11.Http11NioProtocol:

<Connector connectionTimeout="20000" port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" redirectPort="8443"/>

配置好後啟動Tomcat應該是這樣:


1.2、在開發過程中,需要用到Tomcat的catalina.jar包,在Tomcat的lib目錄下。程式在Tomcat中運行時再去掉。

2、Java後台

2.1、CometServlet

這個Servlet是處理Comet Http長串連的Servlet,這個Servlet實現Tomcat提供的CometProcessor介面,通過event方法來處理Http長串連周期內的多種事件:

BEGIN事件:有新的HTTP串連;
END事件:串連關閉,例如瀏覽器關閉;
ERROR事件:串連錯誤,例如timeout。

有關事件更詳細介紹在Tomcat官方文檔中有:http://tomcat.apache.org/tomcat-7.0-doc/aio.html

public class CometServlet extends HttpServlet implements CometProcessor {// 所有正在等待響應的HTTP長串連private ArrayList<HttpServletResponse> connections = null;// 用於發送訊息的線程private MessageSender messageSender = null;// 啟動訊息處理線程public void init() {connections = new ArrayList<HttpServletResponse>();messageSender = new MessageSender(connections);Thread messageSenderThread = new Thread(messageSender);messageSenderThread.start();}public void event(CometEvent event) throws IOException, ServletException {HttpServletResponse response = event.getHttpServletResponse();response.setCharacterEncoding("UTF-8");if (event.getEventType() == CometEvent.EventType.BEGIN) {System.out.println("BEGIN");// 一段大於1024的字串,針對某些瀏覽器緩衝PrintWriter out = response.getWriter();StringBuilder sb = new StringBuilder();          for(int i = 0; i < 1024; i++) {              sb.append(‘a‘);          }          out.println("<!-- " + sb.toString() + " -->"); // 注意加上HTML注釋          out.flush();synchronized(connections) {                connections.add(response);                System.out.println("當前線上使用者:" + connections.size());            }} else if (event.getEventType() == CometEvent.EventType.ERROR) {System.out.println("ERROR");synchronized(connections) {                connections.remove(response);                System.out.println("當前線上使用者:" + connections.size());            }event.close();} else if (event.getEventType() == CometEvent.EventType.END) {System.out.println("END");synchronized(connections) {                connections.remove(response);                System.out.println("當前線上使用者:" + connections.size());            }event.close();}}}

在這個Servlet中,ArrayList<HttpServletResponse> connections用於儲存正在等待響應的HTTP長串連,是HttpServletResponse對象,可以理解為所有線上使用者。在BEGIN事件中會向connections中添加一個串連,在END和ERROR事件中會將對應的串連刪除。

在Servlet初始化init的時候,啟動一個線程用於處理聊天訊息,並把connections傳過去。

在BEGIN事件中,先通過response的輸出資料流輸出了一段大於1024的字串,這是由於瀏覽器的緩衝原因,如果沒有的話在某些瀏覽器下會有要等到流寫到一定位元組數後再顯示的情況。這段字串沒有實際意義,所以可以隨便寫什麼,但不要忘了加上HTML注釋。

2.2、MessageSender

MessageSender是處理聊天訊息的一個線程,實現Runnable介面。當有新的聊天資訊時,它通過HttpServletResponse的輸出資料流立即將資訊發送到所有串連的用戶端,沒有新的資訊則處於阻塞狀態。

處理聊天訊息的時候使用了java.util.concurrent中的阻塞隊列ArrayBlockingQueue。ArrayBlockingQueue.take()方法用於擷取並移除隊列中的一個元素,當隊列為空白時該方法阻塞當前線程,直到有其他線程向這個隊列中添加新元素。當然這裡也可以用wait/notify來替代。

實際上可以將其理解成一個生產者消費者問題,有使用者發送訊息到伺服器相當於生產一條訊息,而這個線程將訊息發送給所以使用者相當於消費一條訊息,而這個阻塞隊列即是緩衝區。

public class MessageSender implements Runnable {// 所有正在等待響應的HTTP長串連private ArrayList<HttpServletResponse> connections;// 未發送給用戶端的訊息集合public static ArrayBlockingQueue<String> messages = new ArrayBlockingQueue<String>(10);public MessageSender(ArrayList<HttpServletResponse> connections) {this.connections = connections;}public void run() {while(true) {// 訊息阻塞隊列中擷取一條訊息,如果隊列為空白則阻塞String message = null;try {message = messages.take();} catch (InterruptedException e) {e.printStackTrace();}// 給每個用戶端發送訊息synchronized (connections) {                        for(HttpServletResponse response : connections) {                    try {                        PrintWriter out = response.getWriter();                                                // 輸出一段指令碼,調用JS將訊息顯示在頁面上                        out.println("<script>parent.addMsg(‘" + message + "<br>‘)</script>");                         out.flush();                    } catch (IOException e) {                        e.printStackTrace();                    }                }            }                }}}

2.3、AjaxMessageServlet

這個Servlet用於處理使用者發送資訊的請求,這是一個普通的Http請求而不是長串連。點擊頁面中的“發送”按鈕時,就會通過Ajax向這個Servlet提交聊天資訊。

當接受到新的訊息時,向MessageSender中的阻塞隊列ArrayBlockingQueue中put添加一條資料。當有新的資料,隊列不為空白時,MessageSender線程不再阻塞,會立即將訊息發送到用戶端瀏覽器。這就相當於通知MessageSender線程發送訊息給用戶端。

public class AjaxMessageServlet extends HttpServlet {public void doPost(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {request.setCharacterEncoding("UTF-8");try {// 這就相當於通知MessageSender線程發送訊息給用戶端MessageSender.messages.put("[" + request.getParameter("name") + "]: " + request.getParameter("msg"));} catch (InterruptedException e) {e.printStackTrace();}}public void doGet(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {doPost(request, response);}}

3、Web前端

<!DOCTYPE HTML><html><head><meta http-equiv="content-type" content="text/html;charset=utf-8"><script type="text/javascript">// 向HTML追加message,這個函數是給伺服器向iframe中添加的javascript指令碼調用function addMsg(msg) {var msgElement = document.getElementById("msg");msgElement.innerHTML += msg;}// 點擊“發送”按鈕後Ajax發送訊息function sendMsg() {var xmlhttp = new XMLHttpRequest();xmlhttp.open("POST", "sendMsg"); // sendMsg是AjaxMessageServlet對應的URLxmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");var name = document.getElementById("input-name").value;var msg = document.getElementById("input-msg").value;xmlhttp.send("name=" + encodeURIComponent(name) + "&msg=" + encodeURIComponent(msg));document.getElementById("input-msg").value = "";}// 伺服器timeout後再重新載入iframefunction iframeRefresh() {var iframeElement = document.getElementById("iframe");iframeElement.src = iframeElement.src;}</script></head><body><!-- 隱藏的iframe,src="comet"是CometServlet對應的URL,用於載入伺服器推送的JS指令碼 --><iframe id="iframe" style="display: none;" src="comet" onload="iframeRefresh();"></iframe><div id="msg" style="height: 400px; width: 400px; overflow: scroll;"></div>姓名:<input type="text" id="input-name"><br>訊息:<br><textarea id="input-msg" cols="30" rows="4"></textarea><br><input type="button" value="發送" onclick="sendMsg();"></body></html>

JS中,addMsg函數是提供給MessageSender中輸出的js指令碼來調用的,用於將訊息顯示在頁面上。

sendMsg函數是“發送”按鈕點擊事件,將聊天資訊發送到AjaxMessageServlet。

iframeRefresh函數是在伺服器逾時的時候reload重新載入iframe,timeout對伺服器來說是逾時,對用戶端來說是載入完成,所以在iframe的onload中調用。設定timeout逾時時間可以在BEGIN事件中用event.setTimeout(30*1000)或event.getHttpServletRequest().setAttribute("org.apache.tomcat.comet.timeout", new Integer(30 * 1000))來設定。

頁面上的iframe設定成display: none也就是不顯示,src是CometServle對應的URL,當有新的資訊時,MessageSender會向iframe中輸出一段JS:

out.println("<script>parent.addMsg(‘" + message + "<br>‘)</script>");

瀏覽器載入到這段JS後會立即運行,調用addMsg函數將資訊顯示在頁面上。

4、源碼

需要DEMO源碼的同學回複中留下E-mail。



叉叉哥   轉載請註明出處:http://blog.csdn.net/xiao__gui/article/details/38487117



聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.