什么是 WebSocket?
WebSocket 是一种网络通信协议,它实现了客户端(通常是浏览器)与服务器之间的全双工、双向、长连接的通信通道。它解决了传统 HTTP 协议在实时通信场景下的痛点:
- HTTP 的短板 :HTTP 协议是无状态、单向的。客户端发起请求,服务器响应后连接就关闭。要实现实时数据推送(如聊天消息、实时行情),只能通过低效的"轮询"(客户端不断向服务器发送请求询问是否有新数据),这浪费了大量带宽和服务器资源。
- WebSocket 的优势 :WebSocket 在初次握手后,会建立一个持久化的长连接 。在这个连接上,服务器和客户端可以随时、主动地向对方发送数据,实现了真正的实时、低延迟、高效的双向通信。
WebSocket 的通信原理
WebSocket 的通信过程可以分为两个阶段:握手连接 和数据传输。
1. 握手连接 (Handshake)
WebSocket 并非凭空建立连接,它巧妙地利用了 HTTP 协议来完成初始握手,以此兼容现有的网络基础设施(如防火墙、代理服务器)。
-
客户端请求 (HTTP Upgrade Request) :客户端首先发送一个标准的 HTTP 请求,但这个请求包含特殊的头信息,表明它希望将协议升级(Upgrade) 为 WebSocket。
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 一个随机生成的Base64编码的密钥
Sec-WebSocket-Version: 13
Origin: http://example.com
Upgrade: websocket和Connection: Upgrade:表明客户端希望升级协议。Sec-WebSocket-Key:一个随机密钥,用于安全验证。Sec-WebSocket-Version:指定协议版本(13是当前最广泛使用的版本)。
-
服务器响应 (HTTP Switching Protocols) :服务器如果支持 WebSocket,会返回一个特殊的 HTTP 响应(状态码 101)。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 由客户端的Key计算得出的值
- 状态码
101表示协议切换成功。 Sec-WebSocket-Accept是服务器使用标准算法对客户端的Sec-WebSocket-Key处理后的结果,用于验证连接的有效性,防止意外的跨协议攻击。
- 状态码
一旦握手成功,TCP连接保持不变,但通信协议从此从 HTTP 切换到了 WebSocket 协议。
2. 数据传输 (Data Transfer)
握手完成后,连接保持打开状态,进入全双工通信阶段。
- 数据帧 (Frames):数据被分割成一个个的"帧(Frame)"进行传输。WebSocket 协议定义了如何封装数据帧,帧头包含操作码(Opcode)来指明这是文本数据、二进制数据、还是控制帧(如连接关闭、心跳ping/pong)。
- 双向通信 :在此通道上,服务器可以不再等待客户端请求,直接主动推送(Push)数据给客户端。客户端也可以随时发送数据给服务器。所有通信都在同一个TCP连接上完成,开销极小(帧头只有2-10字节),效率远高于HTTP。
3. 连接关闭
任何一方都可以发起关闭连接的请求,发送一个关闭帧(Close Frame),另一方回应后,连接终止。
WebSocket 的使用场景
WebSocket 适用于所有需要高实时性、低延迟的Web应用:
- 实时聊天应用:最经典的场景。如微信网页版、在线客服系统、群聊工具,消息需要瞬间送达。
- 多人在线游戏:网页游戏需要实时同步玩家位置、状态和动作。
- 实时数据推送 :
- 金融财经:股票、期货的实时价格变动、K线图。
- 体育博彩:实时赔率更新。
- 监控系统:实时服务器性能指标、日志流。
- 协同工具:如在线文档(Google Docs)、协同绘图工具,实时看到他人的编辑光标和操作。
- 物联网 (IoT):实时显示和控制智能设备的状态。
使用 Spring MVC 实现一个简单的 WebSocket 应用
我们将使用 Spring 框架提供的 spring-websocket 模块来实现一个简单的广播式聊天室。
1. 添加依赖 (Maven)
在 pom.xml 中添加依赖:
xml
<!-- WebSocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.3.23</version> <!-- 请使用你的Spring版本 -->
</dependency>
<!-- 为了简化,使用内置的STOMP代理。生产环境建议用RabbitMQ或ActiveMQ -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.3.23</version>
</dependency>
2. 启用 WebSocket 支持
创建一个Java配置类 WebSocketConfig:
java
import org.springframework.context.annotation.Configuration;
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 // 启用WebSocket消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册一个STOMP端点,客户端将使用它来连接到我们的WebSocket服务器
// withSockJS() 提供了降级方案,在不支持WS的浏览器中使用其他方式模拟
registry.addEndpoint("/ws").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 设置消息代理的前缀
// 以/app开头的消息将被路由到@MessageMapping注解的方法
registry.setApplicationDestinationPrefixes("/app");
// 以/topic开头的消息将被路由到消息代理,再广播给所有连接的客户端
// (内置的简单内存消息代理)
registry.enableSimpleBroker("/topic");
}
}
3. 创建消息控制器
创建一个普通的Spring MVC控制器来处理消息:
java
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
// 处理所有发送到 `/app/chat` 的消息
@MessageMapping("/chat")
// 将方法的返回值广播给所有订阅了 `/topic/messages` 的客户端
@SendTo("/topic/messages")
public ChatMessage sendMessage(ChatMessage message) {
// 这里可以添加业务逻辑,如保存到数据库
return message; // 直接将接收到的消息广播出去
}
}
**4. 创建消息实体类**
public class ChatMessage {
private String from;
private String text;
// 必须有无参构造函数
public ChatMessage() {
}
public ChatMessage(String from, String text) {
this.from = from;
this.text = text;
}
// Getter and Setter 方法
public String getFrom() { return from; }
public void setFrom(String from) { this.from = from; }
public String getText() { return text; }
public void setText(String text) { this.text = text; }
}
5. 前端客户端 (HTML + JavaScript)
创建一个 index.html 页面:
html
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Chat</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
</head>
<body>
<div id="messageArea"></div>
<form>
<input type="text" id="messageInput" placeholder="Type a message..." />
<button type="button" onclick="sendMessage()">Send</button>
</form>
<script>
// 建立连接
const socket = new SockJS('/ws'); // 连接到我们注册的端点
const stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
// 订阅广播地址,当服务器向/topic/messages发送消息时,这里的回调函数会被触发
stompClient.subscribe('/topic/messages', function (messageOutput) {
showMessage(JSON.parse(messageOutput.body));
});
});
function sendMessage() {
const from = "User"; // 在实际应用中,这应该是登录的用户名
const text = document.getElementById('messageInput').value;
// 向服务器发送消息,目的地是 /app/chat
stompClient.send("/app/chat", {}, JSON.stringify({'from': from, 'text': text}));
document.getElementById('messageInput').value = '';
}
function showMessage(message) {
const messageArea = document.getElementById('messageArea');
const messageElement = document.createElement('p');
messageElement.textContent = message.from + ": " + message.text;
messageArea.appendChild(messageElement);
}
</script>
</body>
</html>
总结与运行
- 将以上代码整合到你的Spring Boot或Spring MVC项目中。
- 启动应用服务器。
- 在浏览器中打开
http://localhost:8080/index.html(端口号根据你的配置调整)。 - 打开多个浏览器窗口,在一个窗口中发送消息,所有其他窗口都会实时收到广播的消息。
这个例子使用了 STOMP 子协议,它是在 WebSocket 之上提供了一个更高级的、基于帧的消息格式,类似于 HTTP,使得在客户端和服务器之间传递消息变得更加简单和结构化。