Tomcat學習總結(4)——基於Tomcat七、Java、WebSocket的服務器推送聊天室

前言


        HTML5 WebSocket實現了服務器與瀏覽器的雙向通信,雙向通信使服務器消息推送開發更加簡單,最多見的就是即時通信和對信息實時性要求比較高的應用。之前的服務器消息推送大部分採用的都是「輪詢」和「長鏈接」技術,這兩中技術都會對服務器產生至關大的開銷,並且實時性不是特別高。WebSocket技術對只會產生很小的開銷,而且實時性特別高。下面就開始講解如何利用WebSocket技術開發聊天室。在這個實例中,採用的是Tomcat7服務器,每一個服務器對於WebSocket的實現都是不同的,因此這個實例只能在Tomcat服務器中運行,不過目前Spring已經推出了WebSocket的API,可以兼容各個服務器的實現,你們能夠查閱相關的資料進行了解,在這裏就不介紹了,下圖是聊天室的效果圖:

        在這裏實例中,實現了消息的實時推送,還實現了聊天用戶的上下線通知。下面就開始具體講解如何實現。

後臺處理


        Tomcat實現WebSocket的主要是依靠org.apache.catalina.websocket.MessageInbound這個類,這個類的在{TOMCAT_HOME}/lib/catalina.jar中,因此你開發的時候須要將catalina.jar和tomcat-coyote.jar引入進來,下面這段代碼就是暴露給客戶端鏈接地址的Servlet:

[java]  view plain copy
  1. package com.ibcio;  
  2.   
  3. import javax.servlet.annotation.WebServlet;  
  4. import javax.servlet.http.HttpServletRequest;  
  5.   
  6. import org.apache.catalina.websocket.StreamInbound;  
  7.   
  8. @WebServlet(urlPatterns = { "/message"})  
  9. //若是要接收瀏覽器的ws://協議的請求就必須實現WebSocketServlet這個類  
  10. public class WebSocketMessageServlet extends org.apache.catalina.websocket.WebSocketServlet {  
  11.   
  12.     private static final long serialVersionUID = 1L;  
  13.       
  14.     public static int ONLINE_USER_COUNT = 1;  
  15.       
  16.     public String getUser(HttpServletRequest request){  
  17.         return (String) request.getSession().getAttribute("user");  
  18.     }  
  19.   
  20.     //跟日常Servlet不一樣的是,須要實現createWebSocketInbound,在這裏初始化自定義的WebSocket鏈接對象  
  21.     @Override  
  22.     protected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {  
  23.         return new WebSocketMessageInbound(this.getUser(request));  
  24.     }  
  25. }  
        這個Servlet跟普通的Servlet有些不一樣,繼承的WebSocketServlet類,而且要重寫createWebSocketInbound方法。這個類中Session中的user屬性是用戶進入index.jsp的時候設置的,記錄當前用戶的暱稱。下面就是本身實現的WebSocket鏈接對象類WebSocketMessageInbound類的代碼:
[java]  view plain copy
  1. package com.ibcio;  
  2.   
  3. import java.io.IOException;  
  4. import java.nio.ByteBuffer;  
  5. import java.nio.CharBuffer;  
  6.   
  7. import net.sf.json.JSONObject;  
  8.   
  9. import org.apache.catalina.websocket.MessageInbound;  
  10. import org.apache.catalina.websocket.WsOutbound;  
  11.   
  12. public class WebSocketMessageInbound extends MessageInbound {  
  13.   
  14.     //當前鏈接的用戶名稱  
  15.     private final String user;  
  16.   
  17.     public WebSocketMessageInbound(String user) {  
  18.         this.user = user;  
  19.     }  
  20.   
  21.     public String getUser() {  
  22.         return this.user;  
  23.     }  
  24.   
  25.     //創建鏈接的觸發的事件  
  26.     @Override  
  27.     protected void onOpen(WsOutbound outbound) {  
  28.         // 觸發鏈接事件,在鏈接池中添加鏈接  
  29.         JSONObject result = new JSONObject();  
  30.         result.element("type""user_join");  
  31.         result.element("user"this.user);  
  32.         //向全部在線用戶推送當前用戶上線的消息  
  33.         WebSocketMessageInboundPool.sendMessage(result.toString());  
  34.           
  35.         result = new JSONObject();  
  36.         result.element("type""get_online_user");  
  37.         result.element("list", WebSocketMessageInboundPool.getOnlineUser());  
  38.         //向鏈接池添加當前的鏈接對象  
  39.         WebSocketMessageInboundPool.addMessageInbound(this);  
  40.         //向當前鏈接發送當前在線用戶的列表  
  41.         WebSocketMessageInboundPool.sendMessageToUser(this.user, result.toString());  
  42.     }  
  43.   
  44.     @Override  
  45.     protected void onClose(int status) {  
  46.         // 觸發關閉事件,在鏈接池中移除鏈接  
  47.         WebSocketMessageInboundPool.removeMessageInbound(this);  
  48.         JSONObject result = new JSONObject();  
  49.         result.element("type""user_leave");  
  50.         result.element("user"this.user);  
  51.         //向在線用戶發送當前用戶退出的消息  
  52.         WebSocketMessageInboundPool.sendMessage(result.toString());  
  53.     }  
  54.   
  55.     @Override  
  56.     protected void onBinaryMessage(ByteBuffer message) throws IOException {  
  57.         throw new UnsupportedOperationException("Binary message not supported.");  
  58.     }  
  59.   
  60.     //客戶端發送消息到服務器時觸發事件  
  61.     @Override  
  62.     protected void onTextMessage(CharBuffer message) throws IOException {  
  63.         //向全部在線用戶發送消息  
  64.         WebSocketMessageInboundPool.sendMessage(message.toString());  
  65.     }  
  66. }  

     代碼中的主要實現了onOpen、onClose、onTextMessage方法,分別處理用戶上線、下線、發送消息。在這個類中有個WebSocketMessageInboundPool鏈接池類,這個類是用來管理目前在線的用戶的鏈接,下面是這個類的代碼:
[java]  view plain copy
  1. package com.ibcio;  
  2.   
  3. import java.io.IOException;  
  4. import java.nio.CharBuffer;  
  5. import java.util.HashMap;  
  6. import java.util.Map;  
  7. import java.util.Set;  
  8.   
  9. public class WebSocketMessageInboundPool {  
  10.   
  11.     //保存鏈接的MAP容器  
  12.     private static final Map<String,WebSocketMessageInbound > connections = new HashMap<String,WebSocketMessageInbound>();  
  13.       
  14.     //向鏈接池中添加鏈接  
  15.     public static void addMessageInbound(WebSocketMessageInbound inbound){  
  16.         //添加鏈接  
  17.         System.out.println("user : " + inbound.getUser() + " join..");  
  18.         connections.put(inbound.getUser(), inbound);  
  19.     }  
  20.       
  21.     //獲取全部的在線用戶  
  22.     public static Set<String> getOnlineUser(){  
  23.         return connections.keySet();  
  24.     }  
  25.       
  26.     public static void removeMessageInbound(WebSocketMessageInbound inbound){  
  27.         //移除鏈接  
  28.         System.out.println("user : " + inbound.getUser() + " exit..");  
  29.         connections.remove(inbound.getUser());  
  30.     }  
  31.       
  32.     public static void sendMessageToUser(String user,String message){  
  33.         try {  
  34.             //向特定的用戶發送數據  
  35.             System.out.println("send message to user : " + user + " ,message content : " + message);  
  36.             WebSocketMessageInbound inbound = connections.get(user);  
  37.             if(inbound != null){  
  38.                 inbound.getWsOutbound().writeTextMessage(CharBuffer.wrap(message));  
  39.             }  
  40.         } catch (IOException e) {  
  41.             e.printStackTrace();  
  42.         }  
  43.     }  
  44.       
  45.     //向全部的用戶發送消息  
  46.     public static void sendMessage(String message){  
  47.         try {  
  48.             Set<String> keySet = connections.keySet();  
  49.             for (String key : keySet) {  
  50.                 WebSocketMessageInbound inbound = connections.get(key);  
  51.                 if(inbound != null){  
  52.                     System.out.println("send message to user : " + key + " ,message content : " + message);  
  53.                     inbound.getWsOutbound().writeTextMessage(CharBuffer.wrap(message));  
  54.                 }  
  55.             }  
  56.         } catch (IOException e) {  
  57.             e.printStackTrace();  
  58.         }  
  59.     }  
  60. }  

前臺展現


    上面的代碼就是聊天室後臺的代碼,主要是由3個對象組成,Servlet、鏈接對象、鏈接池,下面就是前臺的代碼,前臺的代碼主要是實現與服務器進行鏈接,展現用戶列表及信息列表,前臺的展現使用了Ext框架,不熟悉Ext的同窗能夠初步的瞭解下Ext,下面的是index.jsp的代碼:
[html]  view plain copy
  1. <%@ page language="java" pageEncoding="UTF-8" import="com.ibcio.WebSocketMessageServlet"%>  
  2. <%  
  3.     String user = (String)session.getAttribute("user");  
  4.     if(user == null){  
  5.         //爲用戶生成暱稱  
  6.         user = "遊客" + WebSocketMessageServlet.ONLINE_USER_COUNT;  
  7.         WebSocketMessageServlet.ONLINE_USER_COUNT ++;  
  8.         session.setAttribute("user", user);  
  9.     }  
  10.     pageContext.setAttribute("user", user);  
  11. %>  
  12. <html>  
  13. <head>  
  14.     <title>WebSocket 聊天室</title>  
  15.     <!-- 引入CSS文件 -->  
  16.     <link rel="stylesheet" type="text/css" href="ext4/resources/css/ext-all.css">  
  17.     <link rel="stylesheet" type="text/css" href="ext4/shared/example.css" />  
  18.     <link rel="stylesheet" type="text/css" href="css/websocket.css" />  
  19.       
  20.     <!-- 映入Ext的JS開發包,及本身實現的webscoket. -->  
  21.     <script type="text/javascript" src="ext4/ext-all-debug.js"></script>  
  22.     <script type="text/javascript" src="websocket.js"></script>  
  23.     <script type="text/javascript">  
  24.         var user = "${user}";  
  25.     </script>  
  26. </head>  
  27.   
  28. <body>  
  29.     <h1>WebSocket聊天室</h1>  
  30.     <p>經過HTML5標準提供的API與Ext富客戶端框架相結合起來,實現聊天室,有如下特色:</p>  
  31.     <ul class="feature-list" style="padding-left: 10px;">  
  32.         <li>實時獲取數據,由服務器推送,實現即時通信</li>  
  33.         <li>利用WebSocket完成數據通信,區別於輪詢,長鏈接等技術,節省服務器資源</li>  
  34.         <li>結合Ext進行頁面展現</li>  
  35.         <li>用戶上線下線通知</li>  
  36.     </ul>  
  37.     <div id="websocket_button"></div>  
  38. </body>  
  39. </html>  
       頁面的展現主要是在websocket.js中進行控制,下面是websocket.jsd的代碼:

[javascript]  view plain copy
  1. //用於展現用戶的聊天信息  
  2. Ext.define('MessageContainer', {  
  3.   
  4.     extend : 'Ext.view.View',  
  5.   
  6.     trackOver : true,  
  7.   
  8.     multiSelect : false,  
  9.   
  10.     itemCls : 'l-im-message',  
  11.   
  12.     itemSelector : 'div.l-im-message',  
  13.   
  14.     overItemCls : 'l-im-message-over',  
  15.   
  16.     selectedItemCls : 'l-im-message-selected',  
  17.   
  18.     style : {  
  19.         overflow : 'auto',  
  20.         backgroundColor : '#fff'  
  21.     },  
  22.   
  23.     tpl : [  
  24.             '<div class="l-im-message-warn">​交談中請勿輕信匯款、中獎信息、陌生電話。 請遵照相關法律法規。</div>',  
  25.             '<tpl for=".">',  
  26.             '<div class="l-im-message">',  
  27.             '<div class="l-im-message-header l-im-message-header-{source}">{from}  {timestamp}</div>',  
  28.             '<div class="l-im-message-body">{content}</div>''</div>',  
  29.             '</tpl>'],  
  30.   
  31.     messages : [],  
  32.   
  33.     initComponent : function() {  
  34.         var me = this;  
  35.         me.messageModel = Ext.define('Leetop.im.MessageModel', {  
  36.                     extend : 'Ext.data.Model',  
  37.                     fields : ['from''timestamp''content''source']  
  38.                 });  
  39.         me.store = Ext.create('Ext.data.Store', {  
  40.                     model : 'Leetop.im.MessageModel',  
  41.                     data : me.messages  
  42.                 });  
  43.         me.callParent();  
  44.     },  
  45.   
  46.     //將服務器推送的信息展現到頁面中  
  47.     receive : function(message) {  
  48.         var me = this;  
  49.         message['timestamp'] = Ext.Date.format(new Date(message['timestamp']),  
  50.                 'H:i:s');  
  51.         if(message.from == user){  
  52.             message.source = 'self';  
  53.         }else{  
  54.             message.source = 'remote';  
  55.         }  
  56.         me.store.add(message);  
  57.         if (me.el.dom) {  
  58.             me.el.dom.scrollTop = me.el.dom.scrollHeight;  
  59.         }  
  60.     }  
  61. });  
      這段代碼主要是實現了展現消息的容器,下面就是頁面加載完成後開始執行的代碼:

[javascript]  view plain copy
  1. Ext.onReady(function() {  
  2.             //建立用戶輸入框  
  3.             var input = Ext.create('Ext.form.field.HtmlEditor', {  
  4.                         region : 'south',  
  5.                         height : 120,  
  6.                         enableFont : false,  
  7.                         enableSourceEdit : false,  
  8.                         enableAlignments : false,  
  9.                         listeners : {  
  10.                             initialize : function() {  
  11.                                 Ext.EventManager.on(me.input.getDoc(), {  
  12.                                             keyup : function(e) {  
  13.                                                 if (e.ctrlKey === true  
  14.                                                         && e.keyCode == 13) {  
  15.                                                     e.preventDefault();  
  16.                                                     e.stopPropagation();  
  17.                                                     send();  
  18.                                                 }  
  19.                                             }  
  20.                                         });  
  21.                             }  
  22.                         }  
  23.                     });  
  24.             //建立消息展現容器  
  25.             var output = Ext.create('MessageContainer', {  
  26.                         region : 'center'  
  27.                     });  
  28.   
  29.             var dialog = Ext.create('Ext.panel.Panel', {  
  30.                         region : 'center',  
  31.                         layout : 'border',  
  32.                         items : [input, output],  
  33.                         buttons : [{  
  34.                                     text : '發送',  
  35.                                     handler : send  
  36.                                 }]  
  37.                     });  
  38.             var websocket;  
  39.   
  40.             //初始話WebSocket  
  41.             function initWebSocket() {  
  42.                 if (window.WebSocket) {  
  43.                     websocket = new WebSocket(encodeURI('ws://localhost:8080/WebSocket/message'));  
  44.                     websocket.onopen = function() {  
  45.                         //鏈接成功  
  46.                         win.setTitle(title + '  (已鏈接)');  
  47.                     }  
  48.                     websocket.onerror = function() {  
  49.                         //鏈接失敗  
  50.                         win.setTitle(title + '  (鏈接發生錯誤)');  
  51.                     }  
  52.                     websocket.onclose = function() {  
  53.                         //鏈接斷開  
  54.                         win.setTitle(title + '  (已經斷開鏈接)');  
  55.                     }  
  56.                     //消息接收  
  57.                     websocket.onmessage = function(message) {  
  58.                         var message = JSON.parse(message.data);  
  59.                         //接收用戶發送的消息  
  60.                         if (message.type == 'message') {  
  61.                             output.receive(message);  
  62.                         } else if (message.type == 'get_online_user') {  
  63.                             //獲取在線用戶列表  
  64.                             var root = onlineUser.getRootNode();  
  65.                             Ext.each(message.list,function(user){  
  66.                                 var node = root.createNode({  
  67.                                     id : user,  
  68.                                     text : user,  
  69.                                     iconCls : 'user',  
  70.                                     leaf : true  
  71.                                 });  
  72.                                 root.appendChild(node);  
  73.                             });  
  74.                         } else if (message.type == 'user_join') {  
  75.                             //用戶上線  
  76.                                 var root = onlineUser.getRootNode();  
  77.                                 var user = message.user;  
  78.                                 var node = root.createNode({  
  79.                                     id : user,  
  80.                                     text : user,  
  81.                                     iconCls : 'user',  
  82.                                     leaf : true  
  83.                                 });  
  84.                                 root.appendChild(node);  
  85.                         } else if (message.type == 'user_leave') {  
  86.                                 //用戶下線  
  87.                                 var root = onlineUser.getRootNode();  
  88.                                 var user = message.user;  
  89.                                 var node = root.findChild('id',user);  
  90.                                 root.removeChild(node);  
  91.                         }  
  92.                     }  
  93.                 }  
  94.             };  
  95.   
  96.             //在線用戶樹  
  97.             var onlineUser = Ext.create('Ext.tree.Panel', {  
  98.                         title : '在線用戶',  
  99.                         rootVisible : false,  
  100.                         region : 'east',  
  101.                         width : 150,  
  102.                         lines : false,  
  103.                         useArrows : true,  
  104.                         autoScroll : true,  
  105.                         split : true,  
  106.                         iconCls : 'user-online',  
  107.                         store : Ext.create('Ext.data.TreeStore', {  
  108.                                     root : {  
  109.                                         text : '在線用戶',  
  110.                                         expanded : true,  
  111.                                         children : []  
  112.                                     }  
  113.                                 })  
  114.                     });  
  115.             var title = '歡迎您:' + user;  
  116.             //展現窗口  
  117.             var win = Ext.create('Ext.window.Window', {  
  118.                         title : title + '  (未鏈接)',  
  119.                         layout : 'border',  
  120.                         iconCls : 'user-win',  
  121.                         minWidth : 650,  
  122.                         minHeight : 460,  
  123.                         width : 650,  
  124.                         animateTarget : 'websocket_button',  
  125.                         height : 460,  
  126.                         items : [dialog,onlineUser],  
  127.                         border : false,  
  128.                         listeners : {  
  129.                             render : function() {  
  130.                                 initWebSocket();  
  131.                             }  
  132.                         }  
  133.                     });  
  134.   
  135.             win.show();  
  136.   
  137.             //發送消息  
  138.             function send() {  
  139.                 var message = {};  
  140.                 if (websocket != null) {  
  141.                     if (input.getValue()) {  
  142.                         Ext.apply(message, {  
  143.                                     from : user,  
  144.                                     content : input.getValue(),  
  145.                                     timestamp : new Date().getTime(),  
  146.                                     type : 'message'  
  147.                                 });  
  148.                         websocket.send(JSON.stringify(message));  
  149.                         //output.receive(message);  
  150.                         input.setValue('');  
  151.                     }  
  152.                 } else {  
  153.                     Ext.Msg.alert('提示''您已經掉線,沒法發送消息!');  
  154.                 }  
  155.             }  
  156.         });  
      上面的代碼就是頁面完成加載後自動鏈接服務器,並建立展現界面的代碼。

注意


        須要注意的兩點,在部署完成以後須要將在tomcat應用目錄中的lib目錄下的catalina.jar和tomcat-coyote.jar刪掉,好比項目的lib目錄在D:\workspace\WebSocket\WebRoot\WEB-INF\lib,而部署的應用lib目錄是在D:\tools\apache-tomcat-7.0.32\webapps\WebSocket\WEB-INF\lib,刪掉部署目錄的lib目錄中連兩個jar就能夠了,不然會包 Could not initialize class com.ibcio.WebSocketMessageServlet錯誤,切記
    若是仍是沒法創建鏈接,請下載最新的tomcat,忘了是那個版本的tomcatcreateWebSocketInbound是沒有request參數的,如今的這個代碼是有這個參數了,7.0.3XX版本都是帶這個參數的,切記。

總結


       使用WebSocket開發服務器推送很是方便,這個是個簡單的應用,其實還能夠結合WebRTC實現視頻聊天和語音聊天。在個人Leetop項目中已經實現了瀏覽器端視頻聊天的功能,你們能夠去看看  www.ibcio.com,在個人另一篇文章中有詳細的介紹: http://blog.csdn.net/leecho571/article/details/8207102,下圖就是Leetop項目的效果圖:


實例下載

相關文章
相關標籤/搜索