23年11月份时,在青芒果写过websocket实现医生问诊的消息提醒(JAVA+JS); 24年12月份,在北京实现websocket前后端主动传输消息(JS); 25年2月份开始了解,智能体前端流式传输的全流程。 谨以此文回顾一下历史。
websocket
什么是websocket
WebSocket 是一种基于 TCP 的应用层网络通信协议,它允许在客户端(如浏览器)和服务器之间建立全双工(双向实时通信)的持久化连接。与传统的 HTTP 请求不同,WebSocket 的设计目标是解决实时通信的效率问题,特别适合需要高频数据交互的场景(如聊天、游戏、实时监控等)。
websocket应用场景
- 实时聊天(如微信网页版)
- 多人在线游戏(实时同步玩家状态)
- 股票行情、监控系统(实时数据推送)
- 协同编辑工具(如 Google Docs)
核心特点
-
双向通信
- 客户端和服务器可以主动向对方发送数据,无需等待请求-响应模式。
- 例如:聊天室中用户发送消息(客户端→服务器),同时实时接收他人消息(服务器→客户端)。
-
低延迟
- 建立连接后,数据通过轻量级的帧(Frame)传输,减少了 HTTP 的头部开销。
- 无需频繁建立/断开连接(HTTP 的短连接会导致重复握手)。
-
持久化连接
- 通过一次 HTTP 握手升级为 WebSocket 连接,之后保持长连接,直到主动关闭。
- 对比 HTTP 轮询(Polling)或长轮询(Long Polling),性能更优。
与HTTP的区别
| 特性 | WebSocket | HTTP |
|---|---|---|
| 通信模式 | 全双工(双向实时) | 半双工(请求-响应) |
| 连接生命周期 | 持久化长连接 | 短连接(默认关闭) |
| 数据开销 | 轻量(帧头部小) | 较高(每次携带完整头) |
| 适用场景 | 实时交互 | 静态资源、简单请求 |
websocket基础介绍
实例属性
主要方法
-
url- 作用:返回 WebSocket 连接的完整 URL(只读)。
- 例如:
"wss://example.com/chat"。
-
bufferedAmount-
作用:返回尚未发送到服务器的缓冲字节数(用于流量控制)。
-
用途:避免发送速度超过网络处理能力。
javascript // 检查缓冲区是否为空 if (socket.bufferedAmount === 0) { socket.send(largeData); }
-
-
protocol- 作用:返回服务器选择的子协议(在握手时由
Sec-WebSocket-Protocol头协商决定)。 - 例如:客户端请求
["chat", "superchat"],服务器选择"chat"。
- 作用:返回服务器选择的子协议(在握手时由
-
extensions- 作用:返回服务器选择的扩展(如压缩算法),通常为空字符串。
-
binaryType- 作用:设置或返回二进制数据的接收类型(
"blob"或"arraybuffer")。 - 默认值为
"blob"。
- 作用:设置或返回二进制数据的接收类型(
除了 addEventListener,还可以通过以下属性直接绑定事件:
-
onopen:连接建立时触发。 -
onmessage:收到服务器数据时触发(event.data包含数据)。 -
onerror:发生错误时触发。 -
onclose:连接关闭时触发(event.code和event.reason包含关闭信息)。
socket.binaryType = 'arraybuffer'; // 接收二进制数据为 ArrayBuffer
- 几种状态
通过实例的 **readyState** 属性获取当前连接状态,值为以下常量之一:
| 状态常量 | 值 | 含义 |
|---|---|---|
WebSocket.CONNECTING | 0 | 连接尚未建立(正在握手或连接中)。 |
WebSocket.OPEN | 1 | 连接已建立,可以通信。 |
WebSocket.CLOSING | 2 | 连接正在关闭(调用了 close() 方法,但未完全关闭)。 |
WebSocket.CLOSED | 3 | 连接已关闭或未能建立(可能是网络错误或握手失败)。 |
if (socket.readyState === WebSocket.OPEN) {
socket.send('数据');
}
- 使用示例
const socket = new WebSocket('wss://example.com');
// 监听状态变化
socket.onopen = () => {
if (socket.readyState === WebSocket.OPEN) {
socket.send('Hello Server!');
}
};
socket.onmessage = (event) => {
console.log('收到:', event.data);
};
socket.onclose = (event) => {
console.log(`关闭原因: ${event.code} - ${event.reason}`);
};
// 主动关闭连接
setTimeout(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.close(1000, '用户离开');
}
}, 5000);
websocket特色使用与封装
心跳检测(保活机制)
作用:防止连接因网络波动或服务器静默断开,通过定期发送心跳包维持连接活性。
实现步骤:
- 连接建立后,启动心跳定时器(
setInterval)。 - 客户端定时向服务器发送心跳消息(如
ping)。 - 服务器需响应心跳(如
pong),若超时未响应则判定连接失效,触发重连。
class WebSocketManager {
constructor(url) {
this.ws = new WebSocket(url);
this.pingInterval = 30000; // 30秒发送一次心跳
this.pongTimeout = 5000; // 5秒内未收到响应则断开
this.heartbeatTimer = null;
this.pongTimer = null;
this.ws.onopen = () => {
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
if (event.data === 'pong') {
this.clearPongTimeout();
}
};
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
this.ws.send('ping');
this.waitPong();
}, this.pingInterval);
}
waitPong() {
this.pongTimer = setTimeout(() => {
this.ws.close(); // 主动关闭,触发重连逻辑
}, this.pongTimeout);
}
clearPongTimeout() {
clearTimeout(this.pongTimer);
}
}
短线自动重连
作用:网络中断或服务器重启后,自动尝试重新建立连接,提升容错性。
实现步骤:
- 监听
onclose事件,根据状态码判断是否需要重连。 - 使用指数退避策略(避免高频重连)。
- 限制最大重试次数,避免无限重试。
class WebSocketManager {
constructor(url) {
this.url = url;
this.reconnectLimit = 5; // 最大重连次数
this.reconnectCount = 0; // 当前重连次数
this.reconnectTimer = null; // 重连定时器
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onclose = (event) => {
if (event.code !== 1000) { // 1000 表示正常关闭,不重连
this.reconnect();
}
};
}
reconnect() {
if (this.reconnectCount < this.reconnectLimit) {
this.reconnectCount++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectCount), 30000); // 指数退避
this.reconnectTimer = setTimeout(() => this.connect(), delay);
}
}
}
重连数据丢失,重新发送
作用:连接中断期间未发送成功的数据,在重连后重新发送,避免丢失。
实现步骤:
- 维护一个待发送消息队列(
pendingQueue)。 - 连接断开时缓存新消息。
- 重连成功后,遍历队列重新发送。
class WebSocketManager {
constructor(url) {
this.pendingQueue = []; // 待发送消息队列
// ...其他初始化
this.ws.onopen = () => {
this.resendPendingMessages();
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
this.pendingQueue.push(data); // 缓存到队列
}
}
resendPendingMessages() {
while (this.pendingQueue.length > 0 && this.ws.readyState === WebSocket.OPEN) {
const message = this.pendingQueue.shift();
this.ws.send(message);
}
}
}
刷新页面websocket断开
作用:页面刷新或关闭前,优雅关闭 WebSocket 连接,避免资源浪费。
实现步骤:
- 监听
beforeunload事件,主动关闭连接。 - 在页面加载时自动初始化连接。
class WebSocketManager {
constructor(url) {
// 页面加载时初始化连接
this.connect();
// 刷新或关闭页面前关闭连接
window.addEventListener('beforeunload', () => {
this.ws.close(1000, '用户离开页面');
});
}
connect() {
this.ws = new WebSocket(this.url);
// ...其他事件绑定
}
}
websocket完整封装示例
class WebSocketManager {
constructor(url) {
this.url = url;
this.pendingQueue = [];
this.reconnectLimit = 5;
this.reconnectCount = 0;
this.reconnectTimer = null;
this.pingInterval = 30000;
this.pongTimeout = 5000;
this.heartbeatTimer = null;
this.pongTimer = null;
this.connect();
this.bindPageEvents();
}
connect() {
this.ws = new WebSocket(this.url);
this.bindEvents();
}
bindEvents() {
this.ws.onopen = () => {
this.reconnectCount = 0; // 重置重连次数
this.resendPendingMessages();
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
if (event.data === 'pong') {
this.clearPongTimeout();
}
// 处理业务数据...
};
this.ws.onclose = (event) => {
if (event.code !== 1000) {
this.reconnect();
}
this.clearHeartbeat();
};
}
// 心跳检测、重连、数据重发等方法(参考前文实现)
bindPageEvents() {
window.addEventListener('beforeunload', () => {
this.ws.close(1000, '页面关闭');
});
}
// 清理资源
destroy() {
clearTimeout(this.reconnectTimer);
clearInterval(this.heartbeatTimer);
clearTimeout(this.pongTimer);
this.ws.close();
}
}
// 使用示例
const wsManager = new WebSocketManager('wss://example.com');
websocket前端边界情况
1. 服务端主动断开
场景:服务器主动关闭连接(如用户被踢出、维护)。
方案:
- 监听
onclose事件,解析event.code和event.reason。 - 状态码处理:根据约定状态码决定是否重连或提示用户(与服务端协商)。
socket.onclose = (event) => {
if (event.code === 1000) {
console.log('连接正常关闭');
} else if (event.code === 4000) { // 自定义状态码(如 token 过期)
alert('登录已失效,请重新登录');
redirectToLogin();
} else {
this.reconnect(); // 触发自动重连
}
};
2. 网络关闭导致断开
场景:用户网络断开后恢复,需自动重连。
方案:
- 监听
navigator.onLine事件,检测网络恢复。 - 结合自动重连逻辑(指数退避)。
// 检测网络恢复
window.addEventListener('online', () => {
if (socket.readyState === WebSocket.CLOSED) {
this.reconnect();
}
});
3. 页面失活状态(重点)
使用定时器来做心跳检测会有问题,页面失活一段时间定时器就变惰性了(偶尔执行一下甚至不执行),所以页面失活状态下连接是极高概率会断开的。
- 方案1(我的iframe重载也是这么做的)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pauseHeartbeat(); // 暂停心跳
} else {
this.resumeHeartbeat(); // 恢复心跳
}
});
- 方案2(todo)
一般是在电脑息屏后15分钟左右定时器就会自动停止运行,此时心跳也会停止发送,就会出现服务断开的情况,用worker可以解决该问题。
把websocket置于worker中工作,通过JS主线程与worker通讯,来调取websocket的api即可
4. 同一页面多开复用同一websocket实例
场景:多个标签页打开同一应用,共享 WebSocket 连接以减少资源消耗。
方案:
- BroadcastChannel API:跨标签页通信,主页面维持连接,其他页面通过消息代理。
- SharedWorker:使用 Web Worker 共享连接(兼容性较差)。
// 主页面代码
const channel = new BroadcastChannel('ws_control');
// 主页面维持连接
if (isPrimaryTab()) {
const socket = new WebSocket(url);
socket.onmessage = (event) => {
channel.postMessage({ type: 'message', data: event.data });
};
}
// 其他页面监听消息
channel.onmessage = (event) => {
if (event.data.type === 'message') {
handleMessage(event.data.data);
}
};
websocket后端实现
当时记得第一次写这个调试得我好费劲
service方法
package vip.xiaonuo.biz.modular.websocket;
import lombok.extern.slf4j.Slf4j;
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;
import java.util.concurrent.atomic.AtomicInteger;
@ServerEndpoint("/webSocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static AtomicInteger onlineNum = new AtomicInteger();
//concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
//发送消息
public void sendMessage(Session session, String message) throws IOException {
if(session != null){
synchronized (session) {
System.out.println("发送数据:" + message);
session.getBasicRemote().sendText(message);
}
}
}
//给指定用户发送信息
public void sendInfo(String userId, String message){
Session session = sessionPools.get(userId);
System.out.println("session:" + session);
System.out.println("session:" + sessionPools);
try {
sendMessage(session, message);
}catch (Exception e){
e.printStackTrace();
}
}
// 群发消息
public void broadcast(String message){
for (Session session: sessionPools.values()) {
try {
sendMessage(session, message);
} catch(Exception e){
e.printStackTrace();
continue;
}
}
}
//建立连接成功调用
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId){
sessionPools.put(userId, session);
addOnlineCount();
System.out.println(userId + "加入webSocket!当前人数为" + onlineNum);
System.out.println(userId + "加入webSocket!当前人数为" + sessionPools);
log.warn("---------------------上线了----------------------"+userId);
// 广播上线消息
}
//关闭连接时调用
@OnClose
public void onClose(@PathParam(value = "username") String userName){
sessionPools.remove(userName);
subOnlineCount();
System.out.println(userName + "断开webSocket连接!当前人数为" + onlineNum);
// 广播下线消息
}
//收到客户端信息后,根据接收人的username把消息推下去或者群发
// to=-1群发消息
@OnMessage
public void onMessage(String userId) throws IOException{
System.out.println("server get" + userId);
}
//错误时调用
@OnError
public void onError(Session session, Throwable throwable){
System.out.println("发生错误");
throwable.printStackTrace();
}
public static void addOnlineCount(){
onlineNum.incrementAndGet();
}
public static void subOnlineCount() {
onlineNum.decrementAndGet();
}
public static AtomicInteger getOnlineNumber() {
return onlineNum;
}
public static ConcurrentHashMap<String, Session> getSessionPools() {
return sessionPools;
}
}
如何调用
@Transactional(rollbackFor = Exception.class)
@Override
public void add(BizBookingAddParam bizBookingAddParam) throws IOException {
BizBooking bizBooking = BeanUtil.toBean(bizBookingAddParam, BizBooking.class);
SaBaseClientLoginUser clientLoginUser = StpClientLoginUserUtil.getClientLoginUser();
QueryWrapper<BizBooking>queryWrapper=new QueryWrapper<>();
queryWrapper.lambda().eq(BizBooking::getCreateUser,clientLoginUser.getId());
queryWrapper.lambda().eq(BizBooking::getDoctorId,bizBooking.getDoctorId());
queryWrapper.lambda().eq(BizBooking::getStatus,"0");
List<BizBooking> list = this.list(queryWrapper);
// if (list.size()>0){
// throw new CommonException("您已邀请该医生");
// }
JSONObject obj = new JSONObject();
obj.put("type", "newUnderSeeBooking");//业务类型
obj.put("msgCreatePatient", clientLoginUser.getId());//消息内容
//全体发送
// webSocket.sendAllMessage(obj.toJSONString());
//单个用户发送 (userId为用户id)
webSocketServer.sendInfo(bizBooking.getDoctorId(),obj.toJSONString());
//多个用户发送 (userIds为多个用户id,逗号‘,’分隔)
// webSocket.sendMoreMessage(userIds, obj.toJSONString());
this.save(bizBooking);
}
参考文章
websocket详解(心跳检测+断线重连+useWebSocket源码解析+边界处理)WebSocket 是基于 TC - 掘金