首页 > 编程 > Java > 正文

基于Tomcat7、Java、WebSocket的服务器推送聊天室实例

2019-11-26 13:24:58
字体:
来源:转载
供稿:网友

前言

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:

package com.ibcio;  import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest;  import org.apache.catalina.websocket.StreamInbound;  @WebServlet(urlPatterns = { "/message"}) //如果要接收浏览器的ws://协议的请求就必须实现WebSocketServlet这个类 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不同的是,需要实现createWebSocketInbound,在这里初始化自定义的WebSocket连接对象   @Override   protected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {     return new WebSocketMessageInbound(this.getUser(request));   } } 

这个Servlet跟普通的Servlet有些不同,继承的WebSocketServlet类,并且要重写createWebSocketInbound方法。这个类中Session中的user属性是用户进入index.jsp的时候设置的,记录当前用户的昵称。下面就是自己实现的WebSocket连接对象类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 {    //当前连接的用户名称   private final String user;    public WebSocketMessageInbound(String user) {     this.user = user;   }    public String getUser() {     return this.user;   }    //建立连接的触发的事件   @Override   protected void onOpen(WsOutbound outbound) {     // 触发连接事件,在连接池中添加连接     JSONObject result = new JSONObject();     result.element("type", "user_join");     result.element("user", this.user);     //向所有在线用户推送当前用户上线的消息     WebSocketMessageInboundPool.sendMessage(result.toString());          result = new JSONObject();     result.element("type", "get_online_user");     result.element("list", WebSocketMessageInboundPool.getOnlineUser());     //向连接池添加当前的连接对象     WebSocketMessageInboundPool.addMessageInbound(this);     //向当前连接发送当前在线用户的列表     WebSocketMessageInboundPool.sendMessageToUser(this.user, result.toString());   }    @Override   protected void onClose(int status) {     // 触发关闭事件,在连接池中移除连接     WebSocketMessageInboundPool.removeMessageInbound(this);     JSONObject result = new JSONObject();     result.element("type", "user_leave");     result.element("user", this.user);     //向在线用户发送当前用户退出的消息     WebSocketMessageInboundPool.sendMessage(result.toString());   }    @Override   protected void onBinaryMessage(ByteBuffer message) throws IOException {     throw new UnsupportedOperationException("Binary message not supported.");   }    //客户端发送消息到服务器时触发事件   @Override   protected void onTextMessage(CharBuffer message) throws IOException {     //向所有在线用户发送消息     WebSocketMessageInboundPool.sendMessage(message.toString());   } } 

代码中的主要实现了onOpen、onClose、onTextMessage方法,分别处理用户上线、下线、发送消息。在这个类中有个WebSocketMessageInboundPool连接池类,这个类是用来管理目前在线的用户的连接,下面是这个类的代码:

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 {       //向特定的用户发送数据       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();     }   }      //向所有的用户发送消息   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();     }   } } 

前台展示

上面的代码就是聊天室后台的代码,主要是由3个对象组成,Servlet、连接对象、连接池,下面就是前台的代码,前台的代码主要是实现与服务器进行连接,展示用户列表及信息列表,前台的展示使用了Ext框架,不熟悉Ext的同学可以初步的了解下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开发包,及自己实现的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>通过HTML5标准提供的API与Ext富客户端框架相结合起来,实现聊天室,有以下特点:</p>   <ul class="feature-list" style="padding-left: 10px;">     <li>实时获取数据,由服务器推送,实现即时通讯</li>     <li>利用WebSocket完成数据通讯,区别于轮询,长连接等技术,节省服务器资源</li>     <li>结合Ext进行页面展示</li>     <li>用户上线下线通知</li>   </ul>   <div id="websocket_button"></div> </body> </html> 

页面的展示主要是在websocket.js中进行控制,下面是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">​交谈中请勿轻信汇款、中奖信息、陌生电话。 请遵守相关法律法规。</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();   },    //将服务器推送的信息展示到页面中   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;     }   } }); 

这段代码主要是实现了展示消息的容器,下面就是页面加载完成后开始执行的代码:

  Ext.onReady(function() {       //创建用户输入框       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();                         }                       }                     });               }             }           });       //创建消息展示容器       var output = Ext.create('MessageContainer', {             region : 'center'           });        var dialog = Ext.create('Ext.panel.Panel', {             region : 'center',             layout : 'border',             items : [input, output],             buttons : [{                   text : '发送',                   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 + ' (连接发生错误)');           }           websocket.onclose = function() {             //连接断开             win.setTitle(title + ' (已经断开连接)');           }           //消息接收           websocket.onmessage = function(message) {             var message = JSON.parse(message.data);             //接收用户发送的消息             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);             }           }         }       };        //在线用户树       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();        //发送消息       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('提示', '您已经掉线,无法发送消息!');         }       }     }); 

上面的代码就是页面完成加载后自动连接服务器,并创建展示界面的代码。

注意

需要注意的两点,在部署完成之后需要将在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实现视频聊天和语音聊天。

实例下载

下载地址:demo

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持武林网。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表