zoukankan      html  css  js  c++  java
  • Tomcat学习总结(4)——基于Tomcat7、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 plaincopy
    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 plaincopy
    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 plaincopy
    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 plaincopy
    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 plaincopy
    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 plaincopy
    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:workspaceWebSocketWebRootWEB-INFlib,而部署的应用lib目录是在D: oolsapache-tomcat-7.0.32webappsWebSocketWEB-INFlib,删掉部署目录的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项目的效果图:


    实例下载

  • 相关阅读:
    上机小笔记
    机器人搬重物(BFS)
    排序汇总
    棋盘(BFS)
    权限控制:分配权限1
    asp.net identity 基础概念篇-理解什么是声明
    EntityFramework中几种更改数据的方式
    CodeFirst迁移时出现的中文乱码问题
    javascript Function()
    C#操作数据库(二)【操作SQL Server数据库的常用的类介绍】
  • 原文地址:https://www.cnblogs.com/zhanghaiyang/p/7213094.html
Copyright © 2011-2022 走看看