Skip to content

WebSocket

简介

WebSocket是一种网络通信协议,它在单个TCP连接上进行全双工通讯。WebSocket协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。

特点

1.双向实时通讯:WebSocket允许服务器主动向客户端推送数据,而不是等待客户端请求。 2.低延迟:WebSocket的通信延迟比HTTP请求/应答模式低得多。 3.长连接:WebSocket连接一旦建立,就可以持续通信,直至一方关闭连接。

基本原理

WebSocket通信分为三个阶段:握手、数据交换和关闭连接。 1.握手:客户端通过HTTP请求向服务器发起WebSocket连接请求,包含特殊的Upgrade头部字段。服务器确认请求后,握手完成。 2.数据交换:握手完成后,客户端和服务器可以通过WebSocket连接发送任意数据。数据被封装成帧,以便于解析和传输。 3.关闭连接:任何一方都可以通过发送特定的帧来关闭WebSocket连接。接收到关闭帧的一方将关闭连接。

应用场景

WebSocket广泛应用于实时通信、在线游戏、实时推送等场景。

总结

WebSocket协议提供了一种高效、低延迟的双向实时通信机制,适用于需要实时交互的Web应用。了解WebSocket的基本原理和应用场景,可以帮助我们更好地设计和开发实时Web应用。

Springboot中使用

SpringBoot整合WebSocket的相关依赖

在pom.xml里添加依赖。

java
<!-- Spring WebSocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

添加WebSocket的配置类在SpringBoot中启用WebSocket

java
/**
 * 开启WebSocket支持
 */
@Configuration  
public class WebSocketConfig {  
    @Bean  
    public ServerEndpointExporter serverEndpointExporter() {  
        return new ServerEndpointExporter();  
    }
}

WebSokcet工具类记录当前的在线连接对链接进行操作

java
public class WebsocketUtil {
  /**
   * 记录当前在线的Session
   */
  private static final Map<String, Session> ONLINE_SESSION = new ConcurrentHashMap<>();

  /**
   * 添加session
   * @param userId
   * @param session
   */
  public static void addSession(String userId, Session session){
    // 此处只允许一个用户的session链接。一个用户的多个连接,我们视为无效。
    ONLINE_SESSION.putIfAbsent ( userId, session );
  }

  /**
   * 关闭session
   * @param userId
   */
  public static void removeSession(String userId){
    ONLINE_SESSION.remove ( userId );
  }

  /**
   * 给单个用户推送消息
   * @param session
   * @param message
   */
  public static void sendMessage(Session session, String message){
    if(session == null){
      return;
    }
    // 同步
    RemoteEndpoint.Async async = session.getAsyncRemote ();
    async.sendText ( message );
  }

  /**
   * 向所有在线人发送消息
   * @param message
   */
  public static void sendMessageForAll(String message) {
    //jdk8 新方法
    ONLINE_SESSION.forEach((sessionId, session) -> sendMessage(session, message));
  }
}

WebSocket接口处理类

也就是websocket真正请求的地址,userId相当于房间名,根据房间名进入到房间,开始监听房间里的信息。

java
@Component
@ServerEndpoint(value = "/chat/{userId}")
public class WebsocketController {
    /**
     * 连接事件,加入注解
     * @param userId
     * @param session
     */
    @OnOpen
    public void onOpen(@PathParam(value = "userId") String userId, Session session) {
        String message = "[" + userId + "]加入聊天室!!";
        // 添加到session的映射关系中
        WebsocketUtil.addSession(userId, session);
        // 广播通知,某用户上线了
//        WebsocketUtil.sendMessageForAll(message);
    }

    /**
     * 连接事件,加入注解
     * 用户断开链接
     *
     * @param userId
     * @param session
     */
    @OnClose
    public void onClose(@PathParam(value = "userId") String userId, Session session) {
        String message = "[" + userId + "]退出了聊天室...";
        // 删除映射关系
        WebsocketUtil.removeSession(userId);
        // 广播通知,用户下线了
        WebsocketUtil.sendMessageForAll(message);
    }

    /**
     * 当接收到用户上传的消息
     *
     * @param userId
     * @param session
     */
    @OnMessage
    public void onMessage(@PathParam(value = "userId") String userId, Session session, String message) {
        String msg = "[" + userId + "]:" + message;
        System.out.println("接收到信息:" + msg);
        // 直接广播
        WebsocketUtil.sendMessageForAll(msg);
    }

    /**
     * 处理用户活连接异常
     *
     * @param session
     * @param throwable
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        throwable.printStackTrace();
    }
}

服务器端单个用户推送消息

走正常http方式,请求之后就会把信息发到对应房间id里

扫码就是用这种方式,虽然websocket支持双向通信,用户也可以send信息到服务器里,但是因为扫码用的设备是手机,而展示二维码与监听是否登陆成功的是在网页里,是分开的,所以手机那边需要走http,如果是聊天室那种功能,那直接调用send方法就能够把信息发送给服务器。

java
@PostMapping("/send")
public void send(@RequestParam("id")  String id,@RequestParam("message")String message) {
    Session session = ONLINE_SESSION.get(id);
    WebsocketUtil.sendMessage(session,message);
    System.out.println("发送成功");
}

https://juejin.cn/post/7372591578755940367?searchId=202408290947537954A9E9F373E8C4D0BC

前端使用

javascript
const socket = new WebSocket('ws://localhost:8888/chat/1');
socket.onopen = (event) => {
        console.log('WebSocket connection opened:', event);
};
//接收后端消息
socket.onmessage = (event) => {
    const data = event.data;
};

socket.onclose = (event) => {
    console.log('WebSocket connection closed:', event);
};
function sendMessage(message) {
    //发送消息到后端
    socket.send(message);
 }

WebSocket心跳重连机制详解

一、WebSocket的"不稳定性"

想象你和朋友打电话,说着说着信号断了,但双方都不知道(没有提示)。这时你继续说,对方却听不见,造成沟通失效。WebSocket也有类似问题:网络波动时连接可能悄悄断开,但客户端和服务器都不知道。

二、什么是心跳包?

心跳包就是定时发送的"小问候",用来确认双方还活着,连接还在。

比如:

  • 客户端每5秒发一个消息:"我还在哦~"(这就是心跳包)
  • 服务器收到后回复:"我也在~"
  • 如果客户端连续3次没收到回复,就认为连接断了

心跳包的作用

  1. 检测连接是否存活
  2. 防止因长时间不通信被防火墙切断
  3. 像"体检"一样定期检查连接状态

三、如何实现心跳重连?

以聊天软件为例,实现步骤如下:

1. 建立WebSocket连接

javascript
let ws = new WebSocket('ws://example.com');

2. 启动心跳定时器

javascript
let heartBeatTimer;
let heartBeatInterval = 5000; // 5秒发一次心跳

function startHeartBeat() {
  heartBeatTimer = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send('ping'); // 发送心跳包
    }
  }, heartBeatInterval);
}

3. 处理服务器响应

javascript
ws.onmessage = function(event) {
  if (event.data === 'pong') {
    // 收到服务器的心跳响应,说明连接正常
    console.log('心跳响应正常');
  } else {
    // 处理正常消息
  }
};

4. 实现重连机制

javascript
let reconnectTimer;
let reconnectInterval = 3000; // 3秒尝试重连一次
let maxReconnectTimes = 10; // 最多重连10次
let currentReconnectTimes = 0;

function reconnect() {
  if (currentReconnectTimes >= maxReconnectTimes) {
    console.log('重连次数过多,放弃');
    return;
  }
  
  currentReconnectTimes++;
  console.log(`尝试第${currentReconnectTimes}次重连...`);
  
  ws = new WebSocket('ws://example.com');
  
  ws.onopen = function() {
    console.log('重连成功');
    currentReconnectTimes = 0;
    startHeartBeat(); // 重连成功后重启心跳
  };
  
  ws.onclose = function() {
    console.log('连接又断开了,准备再次重连');
    clearTimeout(reconnectTimer);
    reconnectTimer = setTimeout(reconnect, reconnectInterval);
  };
}

// 监听连接关闭事件,触发重连
ws.onclose = function() {
  console.log('连接已关闭,开始重连');
  clearInterval(heartBeatTimer); // 停止心跳
  reconnect();
};

四、心跳重连的关键参数

  1. 心跳间隔:多久发一次心跳包(推荐5-30秒)

    • 间隔太短:浪费流量
    • 间隔太长:不能及时发现断开
  2. 超时时间:多久没收到响应算超时(推荐是心跳间隔的2-3倍)

    • 比如心跳间隔5秒,超时设为15秒
  3. 重连策略

    • 固定间隔:每次等3秒重连
    • 指数退避:每次等待时间翻倍(3秒→6秒→12秒)

五、常见问题与解决方案

1. 心跳包被防火墙拦截

解决方案

  • 让服务器主动发心跳(反向心跳)
  • 调整心跳间隔,避免触发防火墙规则

2. 频繁重连导致性能问题

解决方案

  • 实现指数退避策略(等待时间递增)
  • 设置最大重连次数,避免无限重连

3. 重连后状态丢失

解决方案

  • 重连成功后,重新发送未完成的请求
  • 使用本地缓存(如localStorage)保存关键状态

六、完整代码示例

javascript
class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.heartBeatTimer = null;
    this.reconnectTimer = null;
    this.heartBeatInterval = 5000;
    this.reconnectInterval = 3000;
    this.maxReconnectTimes = 10;
    this.currentReconnectTimes = 0;
    this.isConnecting = false;
    
    this.connect();
  }
  
  connect() {
    if (this.isConnecting) return;
    
    this.isConnecting = true;
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      console.log('WebSocket连接已建立');
      this.isConnecting = false;
      this.currentReconnectTimes = 0;
      this.startHeartBeat();
    };
    
    this.ws.onmessage = (event) => {
      if (event.data === 'pong') {
        console.log('收到心跳响应');
      } else {
        this.onMessage(event.data);
      }
    };
    
    this.ws.onclose = () => {
      console.log('WebSocket连接已关闭');
      this.stopHeartBeat();
      
      if (!this.isConnecting) {
        this.scheduleReconnect();
      }
    };
    
    this.ws.onerror = (error) => {
      console.error('WebSocket发生错误:', error);
      this.ws.close();
    };
  }
  
  startHeartBeat() {
    this.stopHeartBeat();
    
    this.heartBeatTimer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send('ping');
      }
    }, this.heartBeatInterval);
  }
  
  stopHeartBeat() {
    if (this.heartBeatTimer) {
      clearInterval(this.heartBeatTimer);
      this.heartBeatTimer = null;
    }
  }
  
  scheduleReconnect() {
    if (this.currentReconnectTimes >= this.maxReconnectTimes) {
      console.log('已达到最大重连次数,停止重连');
      return;
    }
    
    this.currentReconnectTimes++;
    console.log(`准备第${this.currentReconnectTimes}次重连,${this.reconnectInterval/1000}秒后...`);
    
    this.reconnectTimer = setTimeout(() => {
      this.connect();
    }, this.reconnectInterval);
  }
  
  send(message) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(message);
    } else {
      console.log('WebSocket未连接,消息发送失败');
      // 可以选择缓存消息,重连后发送
    }
  }
  
  onMessage(message) {
    // 子类可以重写此方法处理消息
    console.log('收到消息:', message);
  }
  
  close() {
    this.stopHeartBeat();
    clearTimeout(this.reconnectTimer);
    this.isConnecting = false;
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

// 使用示例
const client = new WebSocketClient('ws://example.com');
client.onMessage = (message) => {
  console.log('处理业务消息:', message);
};
client.send('Hello, server!');

七、总结

心跳重连机制就像是:

  1. 两个人约定每隔5分钟说一句"我还在"
  2. 如果连续3次没收到回应,就认为对方失联了
  3. 尝试打电话重新联系,一次打不通就等一会儿再打
  4. 如果打了10次都不通,就暂时放弃

这种机制大大提高了WebSocket通信的稳定性,特别适合实时聊天、监控系统、在线游戏等需要长期保持连接的场景。