WebSocket--简单介绍

发布于:2025-08-15 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、什么是 WebSocket?

  • 定义:WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。

  • 作用:实现客户端(浏览器)和服务器之间的实时、双向通信。

  • 优势

    • 连接保持,通信实时性强(不像 HTTP 需要每次请求都建立连接)。

    • 节省带宽,减少延迟。

    • 适合聊天室、实时推送、游戏、股票行情、物联网等场景。


二、WebSocket 工作原理简述

  1. 客户端发起 HTTP 请求(带有 Upgrade: websocket 头)请求升级协议。

  2. 服务器响应协议升级,建立 WebSocket 连接。

  3. 建立连接后,客户端和服务器可以随时互相发送消息,连接保持,直到关闭。

  4. 连接关闭后,双方不能再通信。


三、Java 原生 WebSocket 使用示例

Java 标准库中 javax.websocket 提供了 WebSocket 支持。下面示例是一个简单的服务端:

1. 添加依赖(以 Maven 为例)

<!-- 只在Java EE容器或者支持Java WebSocket API的容器中需要 -->
<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
</dependency>

2. 编写WebSocketConfig配置类

Spring Boot 默认不支持直接扫描 @ServerEndpoint,需要做个配置类注册它:

@Configuration
public class WebSocketConfig {
   @Bean
   public ServerEndpointExporter serverEndpointExporter() {
      return new ServerEndpointExporter();
   }
}

这个 ServerEndpointExporter 负责注册所有带 @ServerEndpoint 注解的 WebSocket 类。

(1) @ServerEndpoint 注解的类和 Spring 容器
  • 使用标准 Java EE javax.websocket API 的 @ServerEndpoint 注解时,WebSocket 服务端点是由 Java EE 容器(如 Tomcat、Jetty)扫描并管理的,不依赖 Spring 容器。

  • 但是,Spring Boot 本身不会自动扫描和管理@ServerEndpoint 注解的类。


(2) 为什么需要 ServerEndpointExporter
  • ServerEndpointExporter 是 Spring Boot WebSocket 模块提供的一个 bean,作用是:

    • 让 Spring Boot 容器扫描并注册所有用 @ServerEndpoint 注解的类。

    • 把这些端点交给底层的 WebSocket 容器(Tomcat 等)管理。


(3) 什么时候需要 ServerEndpointExporter
  • 如果你用的是 Spring Boot 嵌入式 Tomcat 或其他容器,且你的 WebSocket 端点是用标准的 @ServerEndpoint 注解实现的,通常需要配置这个 bean

3. 编写 WebSocket 服务器端

@ServerEndpoint("/myWs")
@Component
@Slf4j
public class WsServerEndpont {
    static Map<String,Session> sessionMap = new ConcurrentHashMap<>();
    //连接建立时执行的操作
    @OnOpen
    public void onOpen(Session session){
        sessionMap.put(session.getId(),session);
        log.info("websocket is open");
    }
    //收到了客户端消息执行的操作
    @OnMessage
    public String onMessage(String text){
        log.info("收到了一条消息:"+text);
        return "已收到你的消息";
    }
    //连接关闭的时候执行的操作
    @OnClose
    public void onClose(Session session){
        sessionMap.remove(session.getId());
        log.info("websocket is close");
    }
    //每2s发送给客户端心跳消息
    @Scheduled(fixedRate = 2000)
    public void sendMsg() throws IOException {
        for(String key:sessionMap.keySet()){
            sessionMap.get(key).getBasicRemote().sendText("心跳");
        }
    }
}

3. 部署运行

  • 需要在支持 Java EE WebSocket 的容器中(如 Tomcat 8+、Jetty、Glassfish)部署。

  • 客户端可用浏览器或工具连接:ws://localhost:8080/your-app/websocket


四、Spring Boot 中使用 WebSocket(实现多人聊天)

        Spring Boot 提供了非常方便的 WebSocket 支持,通常结合 STOMP 协议和 SockJS 来实现消息的订阅和广播。

1. 添加依赖(Maven)

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Spring Boot 的依赖管理机制(依赖版本继承)

  • Spring Boot 使用了 “依赖管理(Dependency Management)”,这是它的核心特性之一。

  • 你只需在你的项目中声明 spring-boot-starter-parent 或通过 spring-boot-dependencies 管理依赖版本,Spring Boot 会自动帮你管理和统一版本号。

2. 配置 MyWsConfig

// WebSocket配置类,注册WebSocket处理器和拦截器
@Configuration
@EnableWebSocket  // 启用WebSocket支持
public class MyWsConfig implements WebSocketConfigurer {

    @Resource
    MyWsHandler myWsHandler;  // 自定义的WebSocket处理器

    @Resource
    MyWsInterceptor myWsInterceptor; // 自定义的握手拦截器

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册WebSocket处理器,路径为/myWs1
        // 添加握手拦截器,允许所有源跨域请求(*)
           registry.addHandler(myWsHandler, "/myWs1")        // 注册WebSocket处理器及路径
          .addInterceptors(myWsInterceptor)          // 添加握手阶段的拦截器,做一些额外处理
          .setAllowedOrigins("*");                    // 允许所有域发起跨域连接请求

    }
}
2.1 代码讲解:
(1) addHandler(myWsHandler, "/myWs1")
  • 作用:给 WebSocket 服务器注册一个处理器(Handler),并指定客户端连接时使用的 URL 路径。

  • myWsHandler 是你自定义的 WebSocket 业务处理类(继承自 WebSocketHandlerAbstractWebSocketHandler)。

  • "/myWs1" 是 WebSocket 连接的访问路径,客户端通过 ws://服务器地址/myWs1 来建立 WebSocket 连接。

总结:这句相当于告诉服务器,“当客户端请求 /myWs1 路径时,交给 myWsHandler 来处理这次 WebSocket 连接和消息。”


(2)addInterceptors(myWsInterceptor)
  • 作用:给这个 WebSocket 处理器绑定一个或多个拦截器(HandshakeInterceptor),用来拦截 WebSocket 握手阶段的请求。

  • 握手阶段是 WebSocket 建立连接的第一步,类似 HTTP 请求升级。

  • 你可以在拦截器里做一些额外操作,比如:

    • 记录日志

    • 权限校验

    • 传递用户信息到 WebSocket Session

    • 修改握手请求和响应

  • 你的 myWsInterceptor 继承自 HttpSessionHandshakeInterceptor,默认支持把 HTTP Session 关联到 WebSocket Session。

总结:这句是告诉服务器,“在建立 WebSocket 连接握手时,执行 myWsInterceptor 中的逻辑。”


(3)setAllowedOrigins("*")
  • 作用:设置允许连接的客户端来源(Origin),即支持跨域连接的源。

  • 这里 "*" 表示允许所有源都能连接你的 WebSocket 服务。

  • Origin 是浏览器在 WebSocket 握手请求头里自动带上的,服务器根据它判断是否允许该请求。

  • 注意:生产环境一般不要用 "*",建议指定可信的域名,如 setAllowedOrigins("https://example.com")

总结:这句是告诉服务器,“允许哪些来源的网页可以发起 WebSocket 连接请求”。

3. 配置 MyWsHandler

@Slf4j
@Component
public class MyWsHandler extends AbstractWebSocketHandler {

    // 存放所有连接的客户端会话,线程安全的Map
    private static Map<String, SessionBean> sessionBeanMap;

    // 用于生成客户端唯一ID的原子计数器
    private static AtomicInteger clientIdMaker;

    // 用于存放群聊消息的缓冲区
    private static StringBuffer stringBuffer;

    static {
        sessionBeanMap = new ConcurrentHashMap<>();
        clientIdMaker = new AtomicInteger(0);
        stringBuffer = new StringBuffer();
    }

    /**
     * 连接建立成功时调用//相当于上面原生的onOpe*****************************
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);

        // 新建SessionBean,给该连接分配唯一clientId
        SessionBean sessionBean = new SessionBean(session, clientIdMaker.getAndIncrement());

        // 放入sessionMap管理
        sessionBeanMap.put(session.getId(), sessionBean);

        log.info(sessionBean.getClientId() + "建立了连接");

        // 群聊消息中记录进入群聊通知
        stringBuffer.append(sessionBean.getClientId() + "进入了群聊<br/>");

        // 给所有客户端发送当前群聊消息
        sendMessage(sessionBeanMap);
    }

    /**
     * 收到客户端消息时调用//相当于上面原生的onMessage********************************
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);

        // 记录日志,打印客户端发送的消息
        log.info(sessionBeanMap.get(session.getId()).getClientId() + ":" + message.getPayload());

        // 将消息追加到群聊缓冲区
        stringBuffer.append(sessionBeanMap.get(session.getId()).getClientId() + ":" + message.getPayload() + "<br/>");

        // 广播给所有客户端
        sendMessage(sessionBeanMap);
    }

    /**
     * 传输发生异常时调用
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        super.handleTransportError(session, exception);

        // 如果会话还开着,先关闭
        if (session.isOpen()) {
            session.close();
        }

        // 移除该会话
        sessionBeanMap.remove(session.getId());
    }

    /**
     * 连接关闭时调用 //相当于上面原生的onClose****************************
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);

        // 获取即将关闭连接的clientId
        int clientId = sessionBeanMap.get(session.getId()).getClientId();

        // 移除关闭的会话
        sessionBeanMap.remove(session.getId());

        log.info(clientId + "关闭了连接");

        // 群聊消息中记录退出通知
        stringBuffer.append(clientId + "退出了群聊<br/>");

        // 广播给所有客户端
        sendMessage(sessionBeanMap);
    }

    /**
     * 给所有客户端发送消息
     */
    public void sendMessage(Map<String, SessionBean> sessionBeanMap) {
        for (String key : sessionBeanMap.keySet()) {
            try {
                // 发送群聊缓冲区里的消息给每个客户端
                sessionBeanMap.get(key).getWebSocketSession()
                        .sendMessage(new TextMessage(stringBuffer.toString()));
            } catch (IOException e) {
                // 日志记录异常
                log.error(e.getMessage());
            }
        }
    }
}

4. 配置 MyWsInterceptor

@Slf4j
@Component
public class MyWsInterceptor extends HttpSessionHandshakeInterceptor {

    /**
     * 握手之前调用,可用于记录日志或做权限校验
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        log.info(request.getRemoteAddress().toString() + "开始握手");
        return super.beforeHandshake(request, response, wsHandler, attributes);
    }

    /**
     * 握手完成调用
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                               WebSocketHandler wsHandler, Exception ex) {
        log.info(request.getRemoteAddress().toString() + "完成握手");
        super.afterHandshake(request, response, wsHandler, ex);
    }
}

5. 配置 SessionBean


// 简单的会话封装类,保存WebSocketSession和客户端唯一ID
@AllArgsConstructor
@Data
public class SessionBean {
    private WebSocketSession webSocketSession;
    private Integer clientId;
}

6. 启动类WsDemoApplication 

// 启动类,开启定时任务支持
@EnableScheduling
@SpringBootApplication
public class WsDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(WsDemoApplication.class, args);
    }
}

7. 前端示例(HTML + JavaScript)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ws client</title>
</head>
<body>
<p style="border:1px solid black;width: 600px;height: 500px" id="talkMsg"></p>
<input id="message" /><button id="sendBtn" onclick="sendMsg()">发送</button>
</body>
<script>
    let ws = new WebSocket("ws://localhost:8080/myWs1")
    // ws.onopen=function () {
    // }
    ws.onmessage=function (message) {
        document.getElementById("talkMsg").innerHTML = message.data
    }
    function sendMsg() {
        ws.send(document.getElementById("message").value)
        document.getElementById("message").value=""
    }
</script>
</html>

五、小结

特点 Java 原生 WebSocket Spring Boot WebSocket + STOMP
适用场景 简单直接的 WebSocket 应用 需要消息订阅/广播、复杂消息路由的应用
配置复杂度 低(容器支持即可) 需要配置消息代理,依赖更多
功能支持 基础双向通信 支持订阅、广播、分组、消息转换
前端开发支持 需自行实现协议 使用 STOMP.js 等库简化开发