
在当今的互联网时代,实时通信已经成为许多应用不可或缺的功能。从在线聊天工具到实时游戏互动,从股票行情推送再到物联网数据传输,都对实时性有着极高的要求。而在 Java 技术栈中,WebSocket 技术的出现,为开发者打开了一扇通往高效、实时双向通信的大门。本文将带您深入探索 Java 与 WebSocket 的完美结合,从基础概念到实战案例,全方位解析这一强大技术组合的奥秘。
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它于 2011 年被 IETF 标准化为 RFC 6455,并由 W3C 制定了相应的 API 标准。与传统的 HTTP 协议相比,WebSocket 最大的特点在于允许服务器主动向客户端推送信息,同时客户端也可以主动向服务器发送信息,实现了真正意义上的双向平等对话。
在 WebSocket 出现之前,要实现实时通信功能,开发者通常采用轮询(Polling)或长轮询(Long Polling)等方式。轮询是客户端每隔一段时间就向服务器发送一次请求,询问是否有新的数据,服务器则立即返回响应,无论是否有新数据。这种方式会导致大量的无效请求,浪费带宽和服务器资源。长轮询则是客户端向服务器发送请求后,服务器如果没有新数据,不会立即返回响应,而是将连接保持一段时间,直到有新数据或连接超时才返回。虽然长轮询比轮询减少了请求次数,但仍然存在连接建立和关闭的开销,而且在高并发场景下,服务器的压力依然很大。
WebSocket 的出现彻底改变了这种状况。它通过一次握手建立连接后,就可以在客户端和服务器之间进行持续的双向通信,不需要频繁地建立和关闭连接,极大地减少了网络开销和服务器负担,提高了通信效率和实时性。
WebSocket 和 HTTP 同属于应用层协议,并且都基于 TCP 协议进行数据传输,这是它们的相同之处。但它们在通信方式、连接状态等方面存在着显著的差异:
WebSocket 的工作过程主要包括握手阶段和数据传输阶段:
JSR-356 定义了 Java WebSocket API,为 Java 开发者提供了标准化的 WebSocket 编程接口。它允许开发者在 Java EE 应用中创建 WebSocket 服务器端点和客户端端点,实现 WebSocket 通信。
下面是一个简单的 WebSocket 服务器端点实现,用于处理客户端的连接和消息:
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/chat")
public class ChatServer {
// 存储所有连接的会话
private static CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<>();
// 当连接建立时调用
@OnOpen
public void onOpen(Session session) {
System.out.println("新的客户端连接,会话ID:" + session.getId());
sessions.add(session);
try {
session.getBasicRemote().sendText("欢迎连接到聊天服务器!");
} catch (IOException e) {
e.printStackTrace();
}
}
// 当收到客户端消息时调用
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("收到来自会话" + session.getId() + "的消息:" + message);
// 向所有连接的客户端广播消息
for (Session s : sessions) {
if (s.isOpen()) {
try {
s.getBasicRemote().sendText("用户" + session.getId() + ":" + message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 当连接关闭时调用
@OnClose
public void onClose(Session session, CloseReason closeReason) {
System.out.println("客户端断开连接,会话ID:" + session.getId() + ",原因:" + closeReason.getReasonPhrase());
sessions.remove(session);
}
// 当发生错误时调用
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("会话" + session.getId() + "发生错误:" + throwable.getMessage());
throwable.printStackTrace();
}
}在这个例子中,@ServerEndpoint("/chat")标注了ChatServer类作为 WebSocket 服务器端点,路径为/chat。onOpen方法在客户端连接时被调用,将新的会话添加到CopyOnWriteArraySet中(CopyOnWriteArraySet是线程安全的,适合多线程环境),并向客户端发送欢迎消息。onMessage方法在收到客户端消息时被调用,将消息广播给所有连接的客户端。onClose方法在客户端断开连接时被调用,将会话从集合中移除。onError方法处理连接过程中发生的错误。
客户端可以使用 JavaScript 的WebSocket对象来连接 WebSocket 服务器并进行通信:
<!DOCTYPE html>
<html>
<head>
<title>WebSocket聊天客户端</title>
</head>
<body>
<h1>WebSocket聊天</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="请输入消息">
<button onclick="sendMessage()">发送</button>
<script>
// 连接WebSocket服务器,注意协议是ws(非加密)或wss(加密)
let websocket = new WebSocket("ws://localhost:8080/chat");
// 当连接建立时触发
websocket.onopen = function(event) {
console.log("连接已建立");
};
// 当收到服务器消息时触发
websocket.onmessage = function(event) {
let messagesDiv = document.getElementById("messages");
messagesDiv.innerHTML += "<p>" + event.data + "</p>";
};
// 当连接关闭时触发
websocket.onclose = function(event) {
console.log("连接已关闭,代码:" + event.code + ",原因:" + event.reason);
};
// 当发生错误时触发
websocket.onerror = function(event) {
console.log("发生错误");
};
// 发送消息到服务器
function sendMessage() {
let input = document.getElementById("messageInput");
let message = input.value;
if (message && websocket.readyState === WebSocket.OPEN) {
websocket.send(message);
input.value = "";
}
}
</script>
</body>
</html>这个客户端页面创建了一个 WebSocket 连接到ws://localhost:8080/chat,当连接建立、收到消息、连接关闭或发生错误时,会在控制台输出相应的信息。页面上有一个输入框和一个发送按钮,点击发送按钮会将输入的消息发送到服务器。
要运行上述示例,需要一个支持 JSR-356 的 Java Web 容器,如 Tomcat 7.0.47 及以上版本、Jetty 9.1 及以上版本等。将服务器端代码打包成 WAR 文件,部署到 Web 容器中,然后在浏览器中打开客户端 HTML 页面,就可以进行聊天测试了。
Spring Framework 对 WebSocket 提供了良好的支持,简化了 WebSocket 的开发。Spring 的 WebSocket 支持基于 JSR-356,并提供了更高层次的抽象和更多的功能,如消息代理、 STOMP 协议支持等。
要在 Spring 中使用 WebSocket,首先需要进行配置。可以通过 Java 配置类来启用 WebSocket 并注册端点:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 注册WebSocket处理器,指定路径为/chat,允许跨域访问
registry.addHandler(new ChatHandler(), "/chat").setAllowedOrigins("*");
}
}在这个配置类中,@EnableWebSocket注解启用了 Spring 的 WebSocket 支持。WebSocketConfig类实现了WebSocketConfigurer接口,并重写了registerWebSocketHandlers方法,在该方法中注册了一个 WebSocket 处理器ChatHandler,并指定了端点路径为/chat,setAllowedOrigins("*")允许所有域的客户端访问。
ChatHandler是一个实现了WebSocketHandler接口的类,用于处理 WebSocket 连接和消息:
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
public class ChatHandler extends TextWebSocketHandler {
// 存储所有连接的会话
private static CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>();
// 当连接建立时调用
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("新的客户端连接,会话ID:" + session.getId());
sessions.add(session);
session.sendMessage(new TextMessage("欢迎连接到Spring WebSocket聊天服务器!"));
}
// 当收到文本消息时调用
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println("收到来自会话" + session.getId() + "的消息:" + message.getPayload());
// 向所有连接的客户端广播消息
for (WebSocketSession s : sessions) {
if (s.isOpen()) {
s.sendMessage(new TextMessage("用户" + session.getId() + ":" + message.getPayload()));
}
}
}
// 当连接关闭时调用
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("客户端断开连接,会话ID:" + session.getId() + ",状态:" + status);
sessions.remove(session);
}
}ChatHandler继承了TextWebSocketHandler,TextWebSocketHandler是WebSocketHandler的一个子类,专门用于处理文本消息。afterConnectionEstablished方法在连接建立时被调用,handleTextMessage方法在收到文本消息时被调用,afterConnectionClosed方法在连接关闭时被调用,这些方法的功能与前面 JSR-356 示例中的相应方法类似。
Spring WebSocket 的客户端实现与 JSR-356 的客户端类似,可以使用 JavaScript 的WebSocket对象:
<!DOCTYPE html>
<html>
<head>
<title>Spring WebSocket聊天客户端</title>
</head>
<body>
<h1>Spring WebSocket聊天</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="请输入消息">
<button onclick="sendMessage()">发送</button>
<script>
// 连接Spring WebSocket服务器
let websocket = new WebSocket("ws://localhost:8080/chat");
websocket.onopen = function(event) {
console.log("连接已建立");
};
websocket.onmessage = function(event) {
let messagesDiv = document.getElementById("messages");
messagesDiv.innerHTML += "<p>" + event.data + "</p>";
};
websocket.onclose = function(event) {
console.log("连接已关闭,代码:" + event.code + ",原因:" + event.reason);
};
websocket.onerror = function(event) {
console.log("发生错误");
};
function sendMessage() {
let input = document.getElementById("messageInput");
let message = input.value;
if (message && websocket.readyState === WebSocket.OPEN) {
websocket.send(message);
input.value = "";
}
}
</script>
</body>
</html>这个客户端与前面 JSR-356 示例中的客户端基本相同,只是连接的 URL 是 Spring WebSocket 服务器的端点路径。
STOMP(Simple Text Oriented Messaging Protocol)是一种简单的面向文本的消息传递协议,它定义了客户端和服务器之间进行消息传递的格式和规则。Spring WebSocket 支持 STOMP 协议,通过 STOMP 可以实现更复杂的消息传递模式,如发布 / 订阅、点对点通信等。
要使用 STOMP 协议,需要配置消息代理。可以使用内置的简单消息代理,也可以使用外部的消息代理如 RabbitMQ、ActiveMQ 等。以下是使用内置消息代理的配置:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单消息代理,用于广播消息到以/topic/开头的目的地
config.enableSimpleBroker("/topic");
// 配置应用程序的前缀,客户端发送到服务器的消息需要以/app/开头
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册STOMP端点,允许跨域访问,支持SockJS备用传输
registry.addEndpoint("/stomp-chat").setAllowedOrigins("*").withSockJS();
}
}在这个配置类中,@EnableWebSocketMessageBroker注解启用了 Spring 的 WebSocket 消息代理支持。configureMessageBroker方法配置了消息代理,enableSimpleBroker("/topic")启用了内置的简单消息代理,用于处理以/topic开头的目的地的消息广播。setApplicationDestinationPrefixes("/app")指定了客户端发送到服务器的消息需要以/app开头。registerStompEndpoints方法注册了一个 STOMP 端点/stomp-chat,并支持 SockJS 备用传输,当浏览器不支持 WebSocket 时,会自动切换到其他传输方式如 HTTP 长轮询等。
接下来创建一个控制器来处理 STOMP 消息:
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class StompController {
// 处理客户端发送到/app/chat的消息
@MessageMapping("/chat")
// 将消息广播到/topic/messages目的地
@SendTo("/topic/messages")
public ChatMessage handleChat(ChatMessage message) {
return new ChatMessage("用户" + message.getSender() + ":" + message.getContent());
}
public static class ChatMessage {
private String sender;
private String content;
// 构造方法、getter和setter
public ChatMessage() {}
public ChatMessage(String content) {
this.content = content;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
}StompController中的handleChat方法使用@MessageMapping("/chat")注解,用于处理客户端发送到/app/chat的消息。@SendTo("/topic/messages")注解指定将处理后的消息广播到/topic/messages目的地。ChatMessage类是一个简单的消息实体类,包含发送者和消息内容。
客户端可以使用 SockJS 和 STOMP.js 来连接服务器并进行通信:
<!DOCTYPE html>
<html>
<head>
<title>STOMP聊天客户端</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@5/dist/stomp.min.js"></script>
</head>
<body>
<h1>STOMP聊天</h1>
<div id="messages"></div>
<input type="text" id="sender" placeholder="请输入用户名">
<input type="text" id="messageInput" placeholder="请输入消息">
<button onclick="sendMessage()">发送</button>
<script>
// 连接STOMP端点
let socket = new SockJS('/stomp-chat');
let stompClient = Stomp.over(socket);
// 连接成功后的回调函数
stompClient.connect({}, function(frame) {
console.log('连接成功:' + frame);
// 订阅/topic/messages目的地,接收广播消息
stompClient.subscribe('/topic/messages', function(message) {
let chatMessage = JSON.parse(message.body);
let messagesDiv = document.getElementById("messages");
messagesDiv.innerHTML += "<p>" + chatMessage.content + "</p>";
});
});
// 发送消息到服务器
function sendMessage() {
let senderInput = document.getElementById("sender");
let messageInput = document.getElementById("messageInput");
let sender = senderInput.value;
let content = messageInput.value;
if (sender && content && stompClient.connected) {
// 发送消息到/app/chat目的地
stompClient.send("/app/chat", {}, JSON.stringify({sender: sender, content: content}));
messageInput.value = "";
}
}
</script>
</body>
</html>在这个客户端中,首先创建了一个SockJS对象连接到/stomp-chat端点,然后通过Stomp.over(socket)创建了一个 STOMP 客户端。stompClient.connect方法用于连接服务器,连接成功后,通过stompClient.subscribe方法订阅了/topic/messages目的地,以接收服务器广播的消息。sendMessage方法用于发送消息到/app/chat目的地,服务器的StompController会处理该消息,并将其广播到/topic/messages目的地。
实时聊天是 WebSocket 最典型的应用场景之一。无论是在线客服聊天、社交软件中的即时通讯,还是团队协作工具中的群聊,都需要实时地传递消息。使用 WebSocket 可以实现客户端和服务器之间的实时双向通信,用户发送的消息能够立即被其他用户收到,提供良好的用户体验。
在实时聊天系统中,通常采用发布 / 订阅模式,当一个用户发送消息时,服务器将消息广播给所有订阅了该聊天频道的用户。使用 STOMP 协议可以很方便地实现这种模式,通过订阅不同的主题来区分不同的聊天频道。
在线游戏对实时性要求很高,玩家的操作需要实时地反馈给服务器,服务器的状态也需要实时地同步到所有玩家的客户端。WebSocket 的低延迟和双向通信特性非常适合在线游戏场景,能够保证游戏的流畅性和实时性。
在多人在线游戏中,服务器需要实时接收玩家的操作指令,如移动、攻击等,并进行处理,然后将游戏状态的变化广播给所有玩家,确保所有玩家看到的游戏状态是一致的。
在工业监控、金融交易、网络监控等领域,需要实时地获取和展示数据。例如,股票行情监控系统需要实时推送股票价格的变化;网络监控系统需要实时显示网络设备的运行状态和流量数据。
使用 WebSocket 可以将实时数据从服务器推送到客户端,客户端可以实时更新展示界面,让用户及时了解最新的数据信息。相比传统的轮询方式,WebSocket 能够减少网络流量和服务器负载,提高数据传输的效率。
协作编辑工具如在线文档编辑、代码协作工具等,允许多个用户同时编辑同一个文档或代码文件,用户的修改需要实时地被其他用户看到。WebSocket 可以实现用户之间的实时数据同步,当一个用户修改内容时,服务器会将修改内容实时推送给其他用户,确保所有用户看到的内容是一致的。
WebSocket 作为一种实时双向通信协议,为 Java 开发者提供了构建高效、实时的 Web 应用程序的能力。通过 JSR-356 和 Spring Framework 等技术,Java 开发者可以方便地实现 WebSocket 功能,满足各种实时通信场景的需求。
在实际应用中,需要注意连接管理、消息处理、安全性等方面的问题,并采取相应的优化技巧,以提高 WebSocket 应用的性能和可靠性。随着互联网技术的不断发展,实时通信的需求会越来越广泛,WebSocket 作为一种重要的实时通信技术,将会在更多的领域得到应用。
未来,WebSocket 可能会在性能、安全性、扩展性等方面得到进一步的提升,同时也会与其他技术如物联网、人工智能等结合,创造出更多新的应用场景。作为 Java 开发者,我们需要不断学习和掌握 WebSocket 技术,以便更好地应对实时通信领域的挑战和机遇。
PS:吐槽下公众号的排版,不支持MD,大家想格式好看可以移步CSDN:
https://blog.csdn.net/jam_yin/article/details/149541262?sharetype=blogdetail&sharerId=149541262&sharerefer=PC&sharesource=jam_yin&spm=1011.2480.3001.8118