传统的HTTP协议只能以请求/响应的方式进行浏览器和服务器的通信。WebSocket是HTML5规范定义的新功能,它能实现浏览器和服务器之间的全双工通信,这种通信方式非常适合聊天室、游戏等即时应用。JavaEE7规范提供了JSR-356 WebSocket API,这组API定义在javax.websocket.*
下,我们可以在JavaEE7兼容的应用服务器上编写和运行WebSocket服务端程序,此外尽管JSR-356实际上不属于Servlet规范不过在Tomcat中也对其进行了支持。
为了使用WebSocketAPI,我们首先要添加JavaEE SDK的Maven依赖。
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0.1</version>
<scope>provided</scope>
</dependency>
不过如果你仅使用WebSocket API,我们也可以单独引入javax.websocket-api
这个依赖。
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
下面我们编写一个包含前端和后端的简单例子,演示WebSocket API的使用。
在服务端,我们需要使用@ServerEndpoint
注解来标注一个WebSocket端点,它是value
属性是WebSocket端点的路径。
package com.gacfox.demo.demoweb;
import javax.websocket.Session;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint(value = "/chat")
public class WebSocketEndpoint {
private static final String GUEST_PREFIX = "Guest";
private static final AtomicInteger connectionIds = new AtomicInteger(0);
private static final Set<WebSocketEndpoint> connections = new CopyOnWriteArraySet<>();
private final String nickname;
private Session session;
public WebSocketEndpoint() {
nickname = GUEST_PREFIX + connectionIds.getAndIncrement();
}
@OnOpen
public void start(Session session) {
this.session = session;
connections.add(this);
String message = String.format("* %s %s", nickname, "has joined.");
broadcast(message);
}
@OnClose
public void end() {
connections.remove(this);
String message = String.format("* %s %s", nickname, "has disconnected.");
broadcast(message);
}
@OnMessage
public void incoming(String message) {
broadcast(message);
}
@OnError
public void onError(Throwable t) throws Throwable {
System.out.println("Chat Error: " + t.toString());
}
private static void broadcast(String msg) {
for (WebSocketEndpoint client : connections) {
try {
client.session.getBasicRemote().sendText(msg);
} catch (IOException e) {
System.out.println("Chat Error: Failed to send message to client");
connections.remove(client);
try {
client.session.close();
} catch (IOException ignore) {
}
String message = String.format("* %s %s", client.nickname, "has been disconnected.");
broadcast(message);
}
}
}
}
代码中,我们用注解标注了四个方法,分别在用户连接WebSocket,关闭WebSocket,发消息和出错时回调。这里要注意的是该类WebSocketEndpoint
是多例的,也就是一个请求会创建一个实例并在新的线程中运行对应部分的逻辑,所以我们需要使用static
修饰的connections
来记录所有的连接。
客户端包含一个JSP页面和一个JavaScript文件,这部分我们可以参考HTML5中WebSocket相关的章节。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-CH">
<head>
<meta charset="UTF-8">
<title>WebSocket Example</title>
<script src="resources/app.js"></script>
</head>
<body>
<div>
<input type="text" placeholder="输入聊天信息" id="chat-input"/>
<div id="chat-box"></div>
</div>
</body>
</html>
var Chat = {};
window.onload = function () {
Chat.socket = new WebSocket('ws://localhost:8080/demoweb/chat');
Chat.chatbox = document.getElementById('chat-box');
Chat.showMessage = function (msg) {
var p = document.createElement('p');
p.innerText = msg;
this.chatbox.appendChild(p);
};
Chat.sendMessage = function (msg) {
Chat.socket.send(msg);
};
Chat.socket.onopen = function () {
Chat.showMessage('WebSocket open');
};
Chat.socket.onclose = function () {
Chat.showMessage('WebSocket closed');
};
Chat.socket.onmessage = function (sock_msg) {
Chat.showMessage(sock_msg.data);
};
document.getElementById('chat-input').addEventListener('keyup', function (event) {
if (event.keyCode === 13) {
var msg = document.getElementById('chat-input').value;
Chat.sendMessage(msg);
}
});
};
运行结果如下图。