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

时间:2022-04-15 19:02:44

前言

HTML5 WebSocket实现了服务器与浏览器的双向通讯,双向通讯使服务器消息推送开发更加简单,最常见的就是即时通讯和对信息实时性要求比较高的应用。以前的服务器消息推送大部分采用的都是“轮询”和“长连接”技术,这两中技术都会对服务器产生相当大的开销,而且实时性不是特别高。WebSocket技术对只会产生很小的开销,并且实时性特别高。下面就开始讲解如何利用WebSocket技术开发聊天室。在这个实例中,采用的是Tomcat7服务器,每个服务器对于WebSocket的实现都是不一样的,所以这个实例只能在Tomcat服务器中运行,不过目前Spring已经推出了WebSocket的API,能够兼容各个服务器的实现,大家可以查阅相关的资料进行了解,在这里就不介绍了,下图是聊天室的效果图:

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

在这里实例中,实现了消息的实时推送,还实现了聊天用户的上下线通知。下面就开始具体讲解如何实现。

后台处理

Tomcat实现WebSocket的主要是依靠org.apache.catalina.websocket.MessageInbound这个类,这个类的在{TOMCAT_HOME}/lib/catalina.jar中,所以你开发的时候需要将catalina.jar和tomcat-coyote.jar引入进来,下面这段代码就是暴露给客户端连接地址的Servlet:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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类的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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连接池类,这个类是用来管理目前在线的用户的连接,下面是这个类的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<%@ 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的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//用于展示用户的聊天信息
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;
    }
  }
});

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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

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

原文链接:http://blog.csdn.net/leecho571/article/details/9707497