SpringBoot--Other篇01--WebSocket

一.引入

在介绍websocket之前我们需要先认识一下在此之前的其它有关技术,轮询,长轮询,SSE

在上述的技术的实现协议都是基于HTTP协议的是实现的,我们知道Http是基于TCP的连接进行通信的,虽然TCP是有状态的有链接的;但是Http确实美誉状态的协议。原因在于

  1. 简化设计和实现:通过让每个HTTP请求独立于其他请求,可以大大简化服务器的设计和实现。服务器不需要维护大量会话信息的状态数据,这减少了服务器资源的消耗,并使得服务器更容易扩展。

  2. 提高性能和可伸缩性:由于服务器不存储客户端的状态信息,处理一个请求时无需考虑之前的请求,这样就可以更高效地处理并发请求。此外,这也意味着请求可以分发到不同的服务器上处理,提高了系统的可伸缩性和容错能力。

  3. 隐私和安全:无状态特性有助于保护用户隐私,因为服务器不会保留用户的交互历史。这对于保护用户数据和遵守隐私法规是非常重要的。

这就决定了使用Http的通信不能实时获取服务器所转发的消息,为了解决这个问题上述的技术都是因此而诞生的:

特点

短轮询:客户端定时发送请求到服务器询问是否有新的数据。

长轮询:客户端发起请求后,服务器保持连接打开,直到有新数据或超时才返回响应。一旦响应返回,客户端立即发送另一个请求,以保持连接不断开。

SSE:允许服务器向客户端推送更新,主要用于从服务器到客户端的单向通信。使用HTTP协议,支持自动重连和消息格式化。

上述的技术都有一个共同的特点,那就是单向通信,这使得要么牺牲服务的资源为代价或者是牺牲服务器的内存为代价从而获得消息,都不能完全解决客户端和服务器端实时通信的问题。

短轮询示意图:

特点:客户端定时发送请求到服务器询问是否有新的数据。通常使用HTTP协议。

优点:实现简单,适用于不需要实时性特别高的场景。

缺点:效率低,因为即使没有新数据也需要频繁发送请求。延迟较高,依赖于轮询的频率。对服务器资源消耗较大,尤其是在高并发情况下。

长轮询示意图

特点:客户端发起请求后,服务器保持连接打开,直到有新数据或超时才返回响应。一旦响应返回,客户端立即发送另一个请求,以保持连接不断开。

优点:相比短轮询减少了不必要的请求次数,提高了效率。能够实现实时更新,减少延迟。

缺点:服务器需要维护长时间的HTTP连接,增加了复杂性和资源消耗。并发量大时,对服务器的压力也较大。

SSE示意图

特点:允许服务器向客户端推送更新,主要用于从服务器到客户端的单向通信。使用HTTP协议,支持自动重连和消息格式化。

优点:实现简单,尤其适合需要服务器向客户端主动推送更新的场景。自动重连机制,确保连接可靠性。

缺点:只支持单向通信,不适用于需要双向交互的应用场景。在某些浏览器中可能存在兼容性问题。

WebSockt

特点:提供全双工通信通道,允许服务器和客户端进行双向数据交换。不基于HTTP协议,而是有自己的ws://或wss://协议;ws协议完美继承TCP的有状态的连接通信

WebSocket是一种通信协议,提供了在单个TCP连接上进行全双工通信的能力。它使得客户端和服务器之间的数据交换更加简单,允许服务器主动向客户端推送数据。这种特性使得WebSocket非常适合需要低延迟和/或高频率更新的应用场景

  • 全双工通信:与HTTP不同,WebSocket允许服务器和客户端同时发送数据,而不需要一方先等待另一方的响应。这意味着一旦建立了连接,双方就可以自由地发送消息。

  • 单一TCP连接:WebSocket通过一个持久的TCP连接来传输数据,减少了建立和断开连接的开销。这对于频繁通信的应用来说尤其有利。

  • 较低的控制开销:相比于传统的HTTP请求,WebSocket的数据帧格式更为紧凑,携带更少的元数据,这有助于减少网络流量和提高传输效率。

工作流程

  1. 握手阶段:WebSocket连接开始于一个HTTP请求,这个请求包含了特殊的“Upgrade”头部字段,指示服务器将协议从HTTP升级到WebSocket。如果服务器支持WebSocket协议,它会响应一个状态码为101(Switching Protocols)的HTTP响应,表示同意升级。

  2. 数据传输阶段:一旦握手成功,HTTP连接就转换成了WebSocket连接,之后便可以在这条连接上进行双向数据传输了。

  3. 关闭阶段:当任一方想要终止连接时,可以通过发送一个关闭帧来优雅地结束会话。

工作示意图

 建立WebSocket的通信请求

 协议升级完成之后就可以建立一个websocket连接通道,此时服务器可以基于这个通道向客户端发送信息,同时也可以让客户端基于通道发送消息给服务器端

此时就不在基于HTTP通信,而是基于通道的有状态的连接通信

二.主要代码分析

 前端JS代码分析

<script>
let ws = new WebSocket("ws://localhost:8080/chat");
ws.onopen = function (){
  //webSocket连接打开的时候需要做到事
};
ws.onmessage = function (event) {
  // 服务器发送到事件,可以通过事件获取相关的信息
  //event.data 获取到服务器发送的数据
}
ws.onerror = function () {
  //webSocket连接关闭

}
function send(message) {
  //向服务器发送信息
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(message);
  }
}
function disconnect() {
  //断开连接时做一些善后工作
  if (ws) {
    ws.close();
    ws = null;
  }
}
</script>

 

  • WebSocket 构造函数:用于创建一个新的 WebSocket 实例,参数是服务器的 URL(ws:// 或 wss:// 表示 WebSocket 协议)。这是与服务器建立连接的第一步。
  • open 事件:当 WebSocket 连接成功建立时触发。在这个事件中,你可以执行一些初始化操作,比如发送消息给服务器。
  • message 事件:每当从服务器接收到消息时触发。event.data 包含了从服务器接收到的数据,它可以是文本、Blob 或者 ArrayBuffer 格式。
  • close 事件:当 WebSocket 连接关闭时触发。这可能是由于正常的关闭过程、错误或网络问题导致的。
  • error 事件:当出现错误时触发。这可以帮助你识别并处理连接过程中可能出现的问题。
  • send 方法:通过 WebSocket 连接向服务器发送数据。可以发送字符串、Blob、ArrayBuffer 等类型的数据。

后端Spring Boot代码

@EnableWebSocket

 启用 Spring 的 WebSocket 支持。通常应用在配置类上,告诉 Spring Boot 需要开启 WebSocket 功能。

@ServerEndpoint

用于标注一个类为 WebSocket 的服务端点,定义 WebSocket 连接的相关信息,如路径等。该注解来自 javax.websocket-api,而不是直接由 Spring 提供,但可以与 Spring Boot 共同使用。

@OnOpen@OnMessage@OnClose@OnError

作用:这些注解用于标注在 WebSocket 端点(即被 @ServerEndpoint 注解的类)中的方法,分别处理 WebSocket 连接打开、接收消息、连接关闭和错误发生的事件。

@Component 或 @Service

作用:虽然这不是 WebSocket 特有的注解,但在 Spring Boot 应用中,通常会将 WebSocket 相关的服务或组件标记为 @Component 或 @Service,以便它们能够被 Spring 容器管理,并通过依赖注入的方式在其他地方使用。

ServerEndpointExporter

  • 自动注册:ServerEndpointExporter 负责扫描应用程序上下文中的所有被 @ServerEndpoint 注解标记的类,并将它们注册为 WebSocket 端点。这样,当客户端尝试连接到特定路径时,Servlet 容器就知道如何将该请求映射到相应的 WebSocket 处理器。

  • Bean 注册:它确保那些使用了 @ServerEndpoint 注解的类作为 Spring Beans 注册在应用上下文中。这允许你在 WebSocket 端点中利用 Spring 的依赖注入特性,例如依赖其他 Spring 管理的 Bean。

@ServerEndpoint(value = "/chat")
@Component
public class ChatEndPoint {

    /**
     * 建立websocket连接之后被调用
     * @param session
     */
    @OnOpen
    public void onOpen(Session session) {

    }

    /**
     * 浏览器发送消息的时候被调用
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {

    }

    /**
     * 连接断开时使用
     * @param session
     */

    @OnClose
    public void onClose(Session session) {
    }
}

 

三.代码测试

前端JS代码

webSocket.js 位于utils包下:

// src/utils/websocket.js
let socket = null;

export function connect() {
    socket = new WebSocket('ws://localhost:8081/chat');

    socket.onopen = () => {
        console.log('WebSocket connected');
        if (window.handleWebSocketOpen) {
            window.handleWebSocketOpen();
        }
    };

    socket.onmessage = (event) => {
        console.log('Received:', event.data);
        if (window.handleWebSocketMessage) {
            window.handleWebSocketMessage(event.data);
        }
    };

    socket.onclose = () => {
        console.log('WebSocket disconnected');
    };

    socket.onerror = (error) => {
        console.error('WebSocket error:', error);
    };
}

export function disconnect() {
    if (socket) {
        socket.close();
        socket = null;
    }
}

export function send(message) {
    if (socket && socket.readyState === WebSocket.OPEN) {
        socket.send(message);
    }
}

 

App.vue代码

<template>
  <div class="container">
    <h1>WebSocket Chat</h1>
    <button @click="connect" :disabled="isConnected">Connect</button>
    <button @click="disconnect" :disabled="!isConnected">Disconnect</button>
    <input v-model="message" placeholder="Type message">
    <button @click="send" :disabled="!isConnected">Send</button>

    <div class="messages">
      <div v-for="(msg, index) in messages" :key="index" class="message">
        {{ msg }}
      </div>
    </div>
  </div>
</template>

<script>
import { ref, onBeforeUnmount } from 'vue';
import { connect, disconnect, send } from '@/utils/websocket';

export default {
  setup() {
    const message = ref('');
    const messages = ref([]);
    const isConnected = ref(false);

    const connectWebSocket = () => {
      window.handleWebSocketMessage = (msg) => {
        messages.value.push(msg);
      };
      connect();
      isConnected.value = true;
    };

    const disconnectWebSocket = () => {
      disconnect();
      isConnected.value = false;
    };

    const sendMessage = () => {
      if (message.value.trim()) {
        send(message.value);
        messages.value.push("You: " + message.value);
        message.value = '';
      }
    };

    onBeforeUnmount(() => {
      disconnectWebSocket();
    });

    return {
      message,
      messages,
      isConnected,
      connect: connectWebSocket,
      disconnect: disconnectWebSocket,
      send: sendMessage
    };
  }
};
</script>

 

后端SpringBoot代码

ws接口,用于与前端产生连接代码

import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;


import java.util.Set;

import java.util.concurrent.CopyOnWriteArraySet;

@ServerEndpoint(value = "/chat")
@Component
public class ChatEndPoint {
    private static final Set<Session> onlineSessions = new CopyOnWriteArraySet<>();
    /**
     * 建立websocket连接之后被调用
     * @param session
     */
    @OnOpen
    public void onOpen(Session session) {

        onlineSessions.add(session);
    }

    /**
     * 浏览器发送消息的时候被调用
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        try {
            //将消息推送给指定的用户
            onlineSessions.forEach(session -> {
                if (session.isOpen()) {
                    session.getAsyncRemote().sendText(message+":"+session.getId());
                }
            });
        }catch (Exception e){
            System.out.println("ChatEndPoint:onMessage"+e.getMessage());
        }
    }

    /**
     * 连接断开时使用
     * @param session
     */

    @OnClose
    public void onClose(Session session) {
        onlineSessions.remove(session);
    }
}

 

向Spring容器注入ServerEndpointExporter对象:

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();
    }
}

 

解决前后端分离项目跨域的问题:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

 

测试

 如上,其中一个socket发送消息给服务器后,自己和其它的websocket都可以收集到这个消息,因为代码我们定义了服务器主动向所有的socket推送收到的消息

这就可以看出客户端和服务器在websocket技术下是全双工模式工作的

 

 

-------END-----------

 

posted @ 2025-06-15 20:53  回忆也交给时间  阅读(43)  评论(0)    收藏  举报