前言
HTML5 WebSocket實(shí)現(xiàn)了服務(wù)器與瀏覽器的雙向通訊,雙向通訊使服務(wù)器消息推送開(kāi)發(fā)更加簡(jiǎn)單,最常見(jiàn)的就是即時(shí)通訊和對(duì)信息實(shí)時(shí)性要求比較高的應(yīng)用。以前的服務(wù)器消息推送大部分采用的都是“輪詢”和“長(zhǎng)連接”技術(shù),這兩中技術(shù)都會(huì)對(duì)服務(wù)器產(chǎn)生相當(dāng)大的開(kāi)銷,而且實(shí)時(shí)性不是特別高。WebSocket技術(shù)對(duì)只會(huì)產(chǎn)生很小的開(kāi)銷,并且實(shí)時(shí)性特別高。下面就開(kāi)始講解如何利用WebSocket技術(shù)開(kāi)發(fā)聊天室。在這個(gè)實(shí)例中,采用的是Tomcat7服務(wù)器,每個(gè)服務(wù)器對(duì)于WebSocket的實(shí)現(xiàn)都是不一樣的,所以這個(gè)實(shí)例只能在Tomcat服務(wù)器中運(yùn)行,不過(guò)目前Spring已經(jīng)推出了WebSocket的API,能夠兼容各個(gè)服務(wù)器的實(shí)現(xiàn),大家可以查閱相關(guān)的資料進(jìn)行了解,在這里就不介紹了,下圖是聊天室的效果圖:

在這里實(shí)例中,實(shí)現(xiàn)了消息的實(shí)時(shí)推送,還實(shí)現(xiàn)了聊天用戶的上下線通知。下面就開(kāi)始具體講解如何實(shí)現(xiàn)。
后臺(tái)處理
Tomcat實(shí)現(xiàn)WebSocket的主要是依靠org.apache.catalina.websocket.MessageInbound這個(gè)類,這個(gè)類的在{TOMCAT_HOME}/lib/catalina.jar中,所以你開(kāi)發(fā)的時(shí)候需要將catalina.jar和tomcat-coyote.jar引入進(jìn)來(lái),下面這段代碼就是暴露給客戶端連接地址的Servlet:
package com.ibcio;  import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest;  import org.apache.catalina.websocket.StreamInbound;  @WebServlet(urlPatterns = { "/message"}) //如果要接收瀏覽器的ws://協(xié)議的請(qǐng)求就必須實(shí)現(xiàn)WebSocketServlet這個(gè)類 public class WebSocketMessageServlet extends org.apache.catalina.websocket.WebSocketServlet {    private static final long serialVersionUID = 1L;      public static int ONLINE_USER_COUNT = 1;      public String getUser(HttpServletRequest request){     return (String) request.getSession().getAttribute("user");   }    //跟平常Servlet不同的是,需要實(shí)現(xiàn)createWebSocketInbound,在這里初始化自定義的WebSocket連接對(duì)象   @Override   protected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {     return new WebSocketMessageInbound(this.getUser(request));   } } 這個(gè)Servlet跟普通的Servlet有些不同,繼承的WebSocketServlet類,并且要重寫(xiě)createWebSocketInbound方法。這個(gè)類中Session中的user屬性是用戶進(jìn)入index.jsp的時(shí)候設(shè)置的,記錄當(dāng)前用戶的昵稱。下面就是自己實(shí)現(xiàn)的WebSocket連接對(duì)象類WebSocketMessageInbound類的代碼:
 package com.ibcio;  import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer;  import net.sf.json.JSONObject;  import org.apache.catalina.websocket.MessageInbound; import org.apache.catalina.websocket.WsOutbound;  public class WebSocketMessageInbound extends MessageInbound {    //當(dāng)前連接的用戶名稱   private final String user;    public WebSocketMessageInbound(String user) {     this.user = user;   }    public String getUser() {     return this.user;   }    //建立連接的觸發(fā)的事件   @Override   protected void onOpen(WsOutbound outbound) {     // 觸發(fā)連接事件,在連接池中添加連接     JSONObject result = new JSONObject();     result.element("type", "user_join");     result.element("user", this.user);     //向所有在線用戶推送當(dāng)前用戶上線的消息     WebSocketMessageInboundPool.sendMessage(result.toString());          result = new JSONObject();     result.element("type", "get_online_user");     result.element("list", WebSocketMessageInboundPool.getOnlineUser());     //向連接池添加當(dāng)前的連接對(duì)象     WebSocketMessageInboundPool.addMessageInbound(this);     //向當(dāng)前連接發(fā)送當(dāng)前在線用戶的列表     WebSocketMessageInboundPool.sendMessageToUser(this.user, result.toString());   }    @Override   protected void onClose(int status) {     // 觸發(fā)關(guān)閉事件,在連接池中移除連接     WebSocketMessageInboundPool.removeMessageInbound(this);     JSONObject result = new JSONObject();     result.element("type", "user_leave");     result.element("user", this.user);     //向在線用戶發(fā)送當(dāng)前用戶退出的消息     WebSocketMessageInboundPool.sendMessage(result.toString());   }    @Override   protected void onBinaryMessage(ByteBuffer message) throws IOException {     throw new UnsupportedOperationException("Binary message not supported.");   }    //客戶端發(fā)送消息到服務(wù)器時(shí)觸發(fā)事件   @Override   protected void onTextMessage(CharBuffer message) throws IOException {     //向所有在線用戶發(fā)送消息     WebSocketMessageInboundPool.sendMessage(message.toString());   } } 代碼中的主要實(shí)現(xiàn)了onOpen、onClose、onTextMessage方法,分別處理用戶上線、下線、發(fā)送消息。在這個(gè)類中有個(gè)WebSocketMessageInboundPool連接池類,這個(gè)類是用來(lái)管理目前在線的用戶的連接,下面是這個(gè)類的代碼:
package com.ibcio;  import java.io.IOException; import java.nio.CharBuffer; import java.util.HashMap; import java.util.Map; import java.util.Set;  public class WebSocketMessageInboundPool {    //保存連接的MAP容器   private static final Map<String,WebSocketMessageInbound > connections = new HashMap<String,WebSocketMessageInbound>();      //向連接池中添加連接   public static void addMessageInbound(WebSocketMessageInbound inbound){     //添加連接     System.out.println("user : " + inbound.getUser() + " join..");     connections.put(inbound.getUser(), inbound);   }      //獲取所有的在線用戶   public static Set<String> getOnlineUser(){     return connections.keySet();   }      public static void removeMessageInbound(WebSocketMessageInbound inbound){     //移除連接     System.out.println("user : " + inbound.getUser() + " exit..");     connections.remove(inbound.getUser());   }      public static void sendMessageToUser(String user,String message){     try {       //向特定的用戶發(fā)送數(shù)據(jù)       System.out.println("send message to user : " + user + " ,message content : " + message);       WebSocketMessageInbound inbound = connections.get(user);       if(inbound != null){         inbound.getWsOutbound().writeTextMessage(CharBuffer.wrap(message));       }     } catch (IOException e) {       e.printStackTrace();     }   }      //向所有的用戶發(fā)送消息   public static void sendMessage(String message){     try {       Set<String> keySet = connections.keySet();       for (String key : keySet) {         WebSocketMessageInbound inbound = connections.get(key);         if(inbound != null){           System.out.println("send message to user : " + key + " ,message content : " + message);           inbound.getWsOutbound().writeTextMessage(CharBuffer.wrap(message));         }       }     } catch (IOException e) {       e.printStackTrace();     }   } } 前臺(tái)展示
上面的代碼就是聊天室后臺(tái)的代碼,主要是由3個(gè)對(duì)象組成,Servlet、連接對(duì)象、連接池,下面就是前臺(tái)的代碼,前臺(tái)的代碼主要是實(shí)現(xiàn)與服務(wù)器進(jìn)行連接,展示用戶列表及信息列表,前臺(tái)的展示使用了Ext框架,不熟悉Ext的同學(xué)可以初步的了解下Ext,下面的是index.jsp的代碼:
<%@ page language="java" pageEncoding="UTF-8" import="com.ibcio.WebSocketMessageServlet"%> <%   String user = (String)session.getAttribute("user");   if(user == null){     //為用戶生成昵稱     user = "游客" + WebSocketMessageServlet.ONLINE_USER_COUNT;     WebSocketMessageServlet.ONLINE_USER_COUNT ++;     session.setAttribute("user", user);   }   pageContext.setAttribute("user", user); %> <html> <head>   <title>WebSocket 聊天室</title>   <!-- 引入CSS文件 -->   <link rel="stylesheet" type="text/css" href="ext4/resources/css/ext-all.css">   <link rel="stylesheet" type="text/css" href="ext4/shared/example.css" />   <link rel="stylesheet" type="text/css" href="css/websocket.css" />      <!-- 映入Ext的JS開(kāi)發(fā)包,及自己實(shí)現(xiàn)的webscoket. -->   <script type="text/javascript" src="ext4/ext-all-debug.js"></script>   <script type="text/javascript" src="websocket.js"></script>   <script type="text/javascript">     var user = "${user}";   </script> </head>  <body>   <h1>WebSocket聊天室</h1>   <p>通過(guò)HTML5標(biāo)準(zhǔn)提供的API與Ext富客戶端框架相結(jié)合起來(lái),實(shí)現(xiàn)聊天室,有以下特點(diǎn):</p>   <ul class="feature-list" style="padding-left: 10px;">     <li>實(shí)時(shí)獲取數(shù)據(jù),由服務(wù)器推送,實(shí)現(xiàn)即時(shí)通訊</li>     <li>利用WebSocket完成數(shù)據(jù)通訊,區(qū)別于輪詢,長(zhǎng)連接等技術(shù),節(jié)省服務(wù)器資源</li>     <li>結(jié)合Ext進(jìn)行頁(yè)面展示</li>     <li>用戶上線下線通知</li>   </ul>   <div id="websocket_button"></div> </body> </html> 頁(yè)面的展示主要是在websocket.js中進(jìn)行控制,下面是websocket.jsd的代碼:
//用于展示用戶的聊天信息 Ext.define('MessageContainer', {    extend : 'Ext.view.View',    trackOver : true,    multiSelect : false,    itemCls : 'l-im-message',    itemSelector : 'div.l-im-message',    overItemCls : 'l-im-message-over',    selectedItemCls : 'l-im-message-selected',    style : {     overflow : 'auto',     backgroundColor : '#fff'   },    tpl : [       '<div class="l-im-message-warn">交談中請(qǐng)勿輕信匯款、中獎(jiǎng)信息、陌生電話。 請(qǐng)遵守相關(guān)法律法規(guī)。</div>',       '<tpl for=".">',       '<div class="l-im-message">',       '<div class="l-im-message-header l-im-message-header-{source}">{from} {timestamp}</div>',       '<div class="l-im-message-body">{content}</div>', '</div>',       '</tpl>'],    messages : [],    initComponent : function() {     var me = this;     me.messageModel = Ext.define('Leetop.im.MessageModel', {           extend : 'Ext.data.Model',           fields : ['from', 'timestamp', 'content', 'source']         });     me.store = Ext.create('Ext.data.Store', {           model : 'Leetop.im.MessageModel',           data : me.messages         });     me.callParent();   },    //將服務(wù)器推送的信息展示到頁(yè)面中   receive : function(message) {     var me = this;     message['timestamp'] = Ext.Date.format(new Date(message['timestamp']),         'H:i:s');     if(message.from == user){       message.source = 'self';     }else{       message.source = 'remote';     }     me.store.add(message);     if (me.el.dom) {       me.el.dom.scrollTop = me.el.dom.scrollHeight;     }   } }); 這段代碼主要是實(shí)現(xiàn)了展示消息的容器,下面就是頁(yè)面加載完成后開(kāi)始執(zhí)行的代碼:
  Ext.onReady(function() {       //創(chuàng)建用戶輸入框       var input = Ext.create('Ext.form.field.HtmlEditor', {             region : 'south',             height : 120,             enableFont : false,             enableSourceEdit : false,             enableAlignments : false,             listeners : {               initialize : function() {                 Ext.EventManager.on(me.input.getDoc(), {                       keyup : function(e) {                         if (e.ctrlKey === true                             && e.keyCode == 13) {                           e.preventDefault();                           e.stopPropagation();                           send();                         }                       }                     });               }             }           });       //創(chuàng)建消息展示容器       var output = Ext.create('MessageContainer', {             region : 'center'           });        var dialog = Ext.create('Ext.panel.Panel', {             region : 'center',             layout : 'border',             items : [input, output],             buttons : [{                   text : '發(fā)送',                   handler : send                 }]           });       var websocket;        //初始話WebSocket       function initWebSocket() {         if (window.WebSocket) {           websocket = new WebSocket(encodeURI('ws://localhost:8080/WebSocket/message'));           websocket.onopen = function() {             //連接成功             win.setTitle(title + ' (已連接)');           }           websocket.onerror = function() {             //連接失敗             win.setTitle(title + ' (連接發(fā)生錯(cuò)誤)');           }           websocket.onclose = function() {             //連接斷開(kāi)             win.setTitle(title + ' (已經(jīng)斷開(kāi)連接)');           }           //消息接收           websocket.onmessage = function(message) {             var message = JSON.parse(message.data);             //接收用戶發(fā)送的消息             if (message.type == 'message') {               output.receive(message);             } else if (message.type == 'get_online_user') {               //獲取在線用戶列表               var root = onlineUser.getRootNode();               Ext.each(message.list,function(user){                 var node = root.createNode({                   id : user,                   text : user,                   iconCls : 'user',                   leaf : true                 });                 root.appendChild(node);               });             } else if (message.type == 'user_join') {               //用戶上線                 var root = onlineUser.getRootNode();                 var user = message.user;                 var node = root.createNode({                   id : user,                   text : user,                   iconCls : 'user',                   leaf : true                 });                 root.appendChild(node);             } else if (message.type == 'user_leave') {                 //用戶下線                 var root = onlineUser.getRootNode();                 var user = message.user;                 var node = root.findChild('id',user);                 root.removeChild(node);             }           }         }       };        //在線用戶樹(shù)       var onlineUser = Ext.create('Ext.tree.Panel', {             title : '在線用戶',             rootVisible : false,             region : 'east',             width : 150,             lines : false,             useArrows : true,             autoScroll : true,             split : true,             iconCls : 'user-online',             store : Ext.create('Ext.data.TreeStore', {                   root : {                     text : '在線用戶',                     expanded : true,                     children : []                   }                 })           });       var title = '歡迎您:' + user;       //展示窗口       var win = Ext.create('Ext.window.Window', {             title : title + ' (未連接)',             layout : 'border',             iconCls : 'user-win',             minWidth : 650,             minHeight : 460,             width : 650,             animateTarget : 'websocket_button',             height : 460,             items : [dialog,onlineUser],             border : false,             listeners : {               render : function() {                 initWebSocket();               }             }           });        win.show();        //發(fā)送消息       function send() {         var message = {};         if (websocket != null) {           if (input.getValue()) {             Ext.apply(message, {                   from : user,                   content : input.getValue(),                   timestamp : new Date().getTime(),                   type : 'message'                 });             websocket.send(JSON.stringify(message));             //output.receive(message);             input.setValue('');           }         } else {           Ext.Msg.alert('提示', '您已經(jīng)掉線,無(wú)法發(fā)送消息!');         }       }     }); 上面的代碼就是頁(yè)面完成加載后自動(dòng)連接服務(wù)器,并創(chuàng)建展示界面的代碼。
注意
需要注意的兩點(diǎn),在部署完成之后需要將在tomcat應(yīng)用目錄中的lib目錄下的catalina.jar和tomcat-coyote.jar刪掉,比如項(xiàng)目的lib目錄在D:/workspace/WebSocket/WebRoot/WEB-INF/lib,而部署的應(yīng)用lib目錄是在D:/tools/apache-tomcat-7.0.32/webapps/WebSocket/WEB-INF/lib,刪掉部署目錄的lib目錄中連兩個(gè)jar就可以了,否則會(huì)包Could not initialize class com.ibcio.WebSocketMessageServlet錯(cuò)誤,切記。
如果還是無(wú)法建立連接,請(qǐng)下載最新的tomcat,忘了是那個(gè)版本的tomcatcreateWebSocketInbound是沒(méi)有request參數(shù)的,現(xiàn)在的這個(gè)代碼是有這個(gè)參數(shù)了,7.0.3XX版本都是帶這個(gè)參數(shù)的,切記。
總結(jié)
使用WebSocket開(kāi)發(fā)服務(wù)器推送非常方便,這個(gè)是個(gè)簡(jiǎn)單的應(yīng)用,其實(shí)還可以結(jié)合WebRTC實(shí)現(xiàn)視頻聊天和語(yǔ)音聊天。
實(shí)例下載
下載地址:demo
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持武林網(wǎng)。
新聞熱點(diǎn)
疑難解答
圖片精選