2.14 Tomcat8.5的WebSocket支持

2.14 Tomcat8.5的WebSocket支持

严格来说,WebSocket并不属于JavaWeb相关规范,WebSocket属于HTML5规范的一部分,WebSocket允许通过JavaScript建立与远程服务器的连接,从而允许远程服务器将数据推送给浏览器。
通过使用WebSocket,可以构建出实时性要求比较高的应用,比如在线游戏在线证券设备监控新闻在线播报等,只要服务器端有了新数据,服务端就可以直接将数据推送给浏览器,让浏览器显示最新的状态
WebSocket规范已经相当成熟,而且各种主流浏览器(如FirefoxChromeSafariOpera等)都已经支持WebSocket技术,Java EE规范则提供了WebSocket服务端规范,而Tomcat8.5则对该规范提供了优秀的实现。

开发WebSocket服务端的方式

使用Tomcat8.5开发WebSocket服务端非常简单,大致有如下两种方式:

  • 使用注解方式开发,被@ServerEndpoint修饰的Java类即可作为WebSocket服务端。
  • 继承Endpoint基类实现WebSocket服务端

由于使用注解方式开发不仅开发简单,而且是目前的主流方式,因此本书将介绍使用注解方式进行开发。
与开发Servlet一样,Servlet并不需要处理底层并发、网络通信等通用的底层细节——因为Servlet处于Web服务器中运行,Web服务器为Servlet处理了底层的并发、网络通信等;开发WebSocket服务端同样需要位于web服务器中运行,因此此处开发的WebSocket同样无须处理并发、网络通信等细节。
开发被@ServerEndpoint修饰的Java类之后,该类中还可以定义如下方法:

  • @OnOpen修饰的方法:当客户端与该WebSocket服务端建立连接时激发该方法。
  • @OnClose修饰的方法:当客户端与该WebSocket服务端断开连接时激发该方法
  • @OnMessage修饰的方法:当WebSocket服务端收到客户端消息时激发该方法。
  • @OnError修饰的方法:当客户端与该WebSocket服务端连接出现错误时激发该方法。

多人实时聊天程序

思路

下面将基于WebSocket开发一个多人实时聊天的程序,该程序的思路很简单:

  • 在这个程序中,每个客户所用的浏览器都与服务器建立一个WebSocket,从而保持实时连接,这样客户端的浏览器可以随时把数据发送到服务器端;
  • 当服务器收到任何一个浏览器发送来的消息之后,将该消息依次向每个客户端浏览器发送一遍。

图2.54显示了基于WebSocket的多人实时聊天示意图

步骤

为了实现图2.54所示的示意图,按如下步骤开发WebSocket服务端程序即可:

  1. 定义@OnOpen修饰的方法,每当客户端连接进来时激发该方法,程序使用集合保存所有连接进来的客户端。
  2. 定义@OnMessage修饰的方法,每当该服务端收到客户端消息时激发该方法,服务端收到消息之后遍历保存客户端的集合,并将消息逐个发给所有客户端。
  3. 定义@OnClose修饰的方法,每当客户端断开与该服务端连接时激发该方法,程序将该客户端从集合中删除。

程序示例

项目结构

E:\workspacne_JDK8Tomcat8.5\WebSocket
├─src\
│ └─lee\
│   └─ChatEntpoint.java
└─WebContent\
  ├─chat.html
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    ├─lib\
    └─web.xml

ChatEntpoint.java

下面程序就是基于WebSocket实现多人实时聊天的服务器程序。

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
package lee;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.*;
import javax.websocket.server.*;

@ServerEndpoint(value = "/websocket/chat")
public class ChatEntpoint {
private static final String GUEST_PREFIX = "访客";
private static final AtomicInteger connectionIds = new AtomicInteger(0);
// 定义一个集合,用于保存所有接入的WebSocket客户端
private static final Set<ChatEntpoint> clientSet = new CopyOnWriteArraySet<>();
// 定义一个成员变量,记录WebSocket客户端的聊天昵称
private final String nickname;
// 定义一个成员变量,记录与WebSocket之间的会话
private Session session;

public ChatEntpoint() {
nickname = GUEST_PREFIX + connectionIds.getAndIncrement();
}

// 当客户端连接进来时自动激发该方法
@OnOpen
public void start(Session session) {
this.session = session;
// 将WebSocket客户端会话添加到集合中
clientSet.add(this);
String message = String.format("【%s %s】", nickname, "加入了聊天室!");
// 发送消息
broadcast(message);
}

// 当客户端断开连接时自动激发该方法
@OnClose
public void end() {
clientSet.remove(this);
String message = String.format("【%s %s】", nickname, "离开了聊天室!");
// 发送消息
broadcast(message);
}

// 每当收到客户端消息时自动激发该方法
@OnMessage
public void incoming(String message) {
String filteredMessage = String.format("%s: %s", nickname, filter(message));
// 发送消息
broadcast(filteredMessage);
}

// 当客户端通信出现错误时,激发该方法
@OnError
public void onError(Throwable t) throws Throwable {
System.out.println("WebSocket服务端错误 " + t);
}

// 实现广播消息的工具方法
private static void broadcast(String msg) {
// 遍历服务器关联的所有客户端
for (ChatEntpoint client : clientSet) {
try {
synchronized (client) {
// 发送消息
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
System.out.println("聊天错误,向客户端 " + client + " 发送消息出现错误。");
clientSet.remove(client);
try {
client.session.close();
} catch (IOException e1) {
}
String message = String.format("【%s %s】", client.nickname, "已经被断开了连接。");
broadcast(message);
}
}
}

// 定义一个工具方法,用于对字符串中的HTML字符标签进行转义
private static String filter(String message) {
if (message == null)
return null;
char content[] = new char[message.length()];
message.getChars(0, message.length(), content, 0);
StringBuilder result = new StringBuilder(content.length + 50);
for (int i = 0; i < content.length; i++) {
// 控制对尖括号等特殊字符进行转义
switch (content[i]) {
case '<':
result.append("&lt;");
break;
case '>':
result.append("&gt;");
break;
case '&':
result.append("&amp;");
break;
case '"':
result.append("&quot;");
break;
default:
result.append(content[i]);
}
}
return (result.toString());
}
}

上面的ChatEntpoint主要就是实现了@OnOpen@OnClose@OnMessage@OnError这4个注解修饰的方法。
需要说明的是,该ChatEntpoint类并不是真正的WebSocket服务端,它只实现了WebSocket服务端的核心功能,Tomcat会调用它的方法作为WebSocket服务端。因此,Tomcat会为每个WebSocket客户端创建一个ChatEntpoint对象,也就是说,有一个WebSocket客户端,程序就有一个ChatEntpoint对象。所以上面程序中clientSet集合保存了多个ChatEntpoint对象,其中每个ChatEndpoint对象对应一个WebSocket客户端。
编译ChatEntpoint类,并将生成的class文件放在Web应用的WEB-NF/Classes目录下,该ChatEntpoint即可作为WebSocket服务端使用。
上面ChatEntpoint类用到了WebSocket API规范的相关注解,因此读者需要将Tomcatlib目录下的websocket-api.jar添加到CLASSPATH环境变量下。

chat.html

接下来使用JavaScript开发WebSocket客户端。此处WebSocket客户端使用一个简单的HTML页面即可

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
<!DOCTYPE html>
<html>
<head>
<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title> 使用WebSocket通信 </title>
<script type="text/javascript">
// 创建WebSocket对象
var webSocket = new WebSocket("ws://127.0.0.1:8888/WebSocket/websocket/chat");
var sendMsg = function()
{
var inputElement = document.getElementById('msg');
// 发送消息
webSocket.send(inputElement.value);
// 清空单行文本框
inputElement.value = "";
}
var send = function(event)
{
if (event.keyCode == 13)
{
sendMsg();
}
};
webSocket.onopen = function()
{
// 为onmessage事件绑定监听器,接收消息
webSocket.onmessage= function(event)
{
var show = document.getElementById('show')
// 接收、并显示消息
show.innerHTML += event.data + "<br/>";
show.scrollTop = show.scrollHeight;
}
document.getElementById('msg').onkeydown = send;
document.getElementById('sendBn').onclick = sendMsg;
};
webSocket.onclose = function ()
{
document.getElementById('msg').onkeydown = null;
document.getElementById('sendBn').onclick = null;
Console.log('WebSocket已经被关闭。');
};
</script>
</head>
<body>
<div style="width:600px;height:240px;
overflow-y:auto;border:1px solid #333;" id="show"></div>
<input type="text" size="80" id="msg" name="msg" placeholder="输入聊天内容"/>
<input type="button" value="发送" id="sendBn" name="sendBn"/>
</body>
</html>

上面程序中代码:

1
var webSocket = new WebSocket("ws://127.0.0.1:8888/WebSocket/websocket/chat");

创建了一个WebSocket对象(WebSocketHTML5规范新增的类)创建对象时指定WebSocket服务端的地址。一旦程序得到了WebSocket对象,接下来程序即可调用WebSocketsend()方法向服务器发送消息。
除此之外,还可以为WebSocket绑定如下三个事件处理函数:

  • onopen:当WebSocket客户端与服务端建立连接时自动激发该事件处理函数。
  • onclose:当WebSocket客户端与服务端关闭连接时自动激发该事件处理函数。
  • onmessage:当WebSocket客户端收到服务端消息时自动激发该事件处理函数。

WebSocket肯定会成为Web应用开发的主流技术,这种技术颠覆了传统Web应用请求响应架构模型,它可以让服务端与浏览器建立实时通信的Socket,因此具有广泛的用途。为了使用WebSocket,还需要JavaScript编程知识,关于JavaScriptWebSocket相关知识,请参考《疯狂HTML5/CSS3/JavaScript讲义》

测试

首先启动ChatEntpoint所在的web应用,使用多个浏览器登录chat.html页面聊天即可看到如图2.55所示的聊天效果。

1
http://localhost:8080/WebSocket/chat.html