首先,無圖無真相,先上圖:
這是一個基於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:
配置好後啟動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 ArrayListconnections = null; // 用於發送消息的線程 private MessageSender messageSender = null; // 啟動消息處理線程 public void init() { connections = new ArrayList (); 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(""); // 注意加上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初始化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 ArrayListconnections; // 未發送給客戶端的消息集合 public static ArrayBlockingQueue messages = new ArrayBlockingQueue (10); public MessageSender(ArrayList 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 + "
')</script>"); out.flush(); } catch (IOException e) { e.printStackTrace(); } } } } } }
這個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); } }
<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對應的URL xmlhttp.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後再重新加載iframe function iframeRefresh() { var iframeElement = document.getElementById("iframe"); iframeElement.src = iframeElement.src; } </script> <iframe id="iframe" src="comet" onload="iframeRefresh();"></iframe> 姓名:
消息:
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 + "
')</script>");
浏覽器加載到這段JS後會立即運行,調用addMsg函數將信息顯示在頁面上。
4、源碼
需要DEMO源碼的同學回復中留下E-mail。
作者:叉叉哥 轉載請注明出處:http://blog.csdn.net/xiao__gui/article/details/38487117