WebSocket 详解
什么是 WebSocket?
WebSocket 是一种在单个 TCP 连接 上进行全双工通信 的网络协议,实现了浏览器与服务器之间的实时双向通信。
与 HTTP 的根本区别
HTTP(请求-响应模式)
客户端: 请求 → 服务器
客户端: ← 响应 服务器
(连接关闭)
WebSocket(全双工模式)
客户端: ↔ 服务器
客户端: ↔ 服务器
客户端: ↔ 服务器
(持续连接,双向实时通信)
WebSocket 协议特点
1. 一次握手,持久连接
javascript
// 握手过程
客户端 → 服务器: HTTP Upgrade 请求
客户端 ← 服务器: HTTP 101 Switching Protocols
// 之后就是 WebSocket 协议通信
2. 极小的协议开销
java
// 建立连接后,数据帧头很小
// HTTP 每次请求都有完整的头部,WebSocket 只有 2-14 字节的帧头
3. 真正的实时性
- 服务器可以主动推送 数据给客户端
- 无需客户端轮询询问
- 毫秒级延迟
WebSocket 核心技术原理
连接建立过程
java
// 1. 客户端发起握手
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
// 2. 服务器响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
数据帧格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
Spring Boot 中的 WebSocket 实现
1. 依赖配置
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 配置类
java
package com.panda.wiki.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3. 端点类(注解方式)
java
package com.panda.wiki.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint("/ws/{token}")
public class WebSocketServer {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketServer.class); // 修正 Logger
/**
* 每个客户端一个token
*/
private String token = "";
/**
* 使用线程安全的 ConcurrentHashMap
*/
public static ConcurrentHashMap<String, Session> map = new ConcurrentHashMap<>();
/**
* 连接成功
*/
@OnOpen // 修正注解大小写
public void onOpen(Session session, @PathParam("token") String token) { // 修正参数语法
map.put(token, session);
this.token = token;
LOG.info("有新连接:token:{},session id:{},当前连接数:{}", token, session.getId(), map.size()); // 修正日志语法
}
/**
* 连接关闭
*/
@OnClose // 修正注解大小写
public void onClose(Session session) {
map.remove(this.token);
LOG.info("连接关闭,token:{},session id:{},当前连接数:{}", this.token, session.getId(), map.size()); // 修正日志语法
}
/**
* 收到消息
*/
@OnMessage
public void onMessage(String message, Session session) {
LOG.info("收到消息: {},内容: {}", token, message); // 修正日志语法
}
/**
* 连接错误
*/
@OnError
public void onError(Session session, Throwable error) {
LOG.error("发生错误", error);
}
/**
* 群发消息
*/
public static void sendInfo(String message) { // 改为静态方法
for (String token : map.keySet()) {
Session session = map.get(token);
try {
if (session.isOpen()) { // 检查会话是否仍然打开
session.getBasicRemote().sendText(message);
LOG.info("推送消息: {}, 内容: {}", token, message); // 修正日志文字
} else {
// 如果会话已关闭,从map中移除
map.remove(token);
}
} catch (IOException e) {
LOG.error("推送消息失败: {}, 内容: {}", token, message, e); // 修正日志文字
// 发送失败,从map中移除无效连接
map.remove(token);
}
}
}
/**
* 向指定用户发送消息
*/
public static void sendToUser(String token, String message) {
Session session = map.get(token);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
LOG.info("向用户 {} 发送消息: {}", token, message);
} catch (IOException e) {
LOG.error("向用户 {} 发送消息失败", token, e);
map.remove(token); // 移除无效连接
}
} else {
LOG.warn("用户 {} 不在线或连接已关闭", token);
if (session != null) {
map.remove(token); // 清理无效连接
}
}
}
/**
* 获取当前在线连接数
*/
public static int getOnlineCount() {
return map.size();
}
/**
* 检查用户是否在线
*/
public static boolean isOnline(String token) {
Session session = map.get(token);
return session != null && session.isOpen();
}
}