使用WebSocket

在Servlet 3.0时代,WebSocket相关的支持被引入Servlet API中,Spring基于此为我们进行了封装,提供了方便的WebSocketHandler组件来快速开发WebSocket服务端程序。这篇笔记我们主要介绍SpringBoot 3.x工程中如何实现WebSocket相关的功能。

引入Maven依赖

对于SpringBoot工程,使用WebSocket需要引入起步依赖。

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

工程配置

工程中我们需要在启动类上标注@EnableWebSocket来开启WebSocket支持。

WsConfig.java

package com.gacfox.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.socket.config.annotation.EnableWebSocket;

@EnableWebSocket
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

编写WebSocketHandler

Spring的WebSocket封装提供了WebSocketHandler接口。具体代码中,我们可以继承抽象类AbstractWebSocketHandler,或者更具体的直接继承TextWebSocketHandlerBinaryWebSocketHandler,后两个类分别专用于处理文本消息和二进制消息,足够覆盖绝大部分WebSocket的使用场景了。

这里我们继承TextWebSocketHandler,实现一个最简单的文本消息处理功能,接收客户端发送的输入并打印,然后回复“收到”。为了处理WebSocket消息,我们需要编写我们自己的Handler类。

ChatHandler.java

package com.gacfox.demo.ws;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Slf4j
public class ChatHandler extends TextWebSocketHandler {
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        log.info(message.getPayload());
        session.sendMessage(new TextMessage("收到"));
    }
}

例子代码比较简单,我们直接继承了TextWebSocketHandler来处理文本消息。其中session参数包含连接的信息,例如需要实现消息广播等功能时,我们就需要保存所有的连接并统一发送消息;message则是具体的文本消息体。具体的Handler逻辑就是直接打印客户端发来的文本消息。

此时我们的Handler类还不能直接使用,我们需要将其注册到框架中,下面代码我们将ChatHandler注册到/ws/chat路径。

package com.gacfox.demo.config;

import com.gacfox.demo.ws.ChatHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
public class WsConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler(), "/ws/chat");
    }

    @Bean
    public ChatHandler chatHandler() {
        return new ChatHandler();
    }
}

跨域设置

虽然WebSocket建立连接后不再是简单的HTTP请求响应了,但它的第一步仍是HTTP Upgrade请求,因此仍存在跨域安全限制。如果我们有跨域需求,需要在注册Handler时进行设置。

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(chatHandler(), "/ws/chat")
            .setAllowedOriginPatterns("*");
}

这里我们直接设置了*即允许全部跨域连接,生产环境中这样做并不安全,实际使用时应该根据具体的限制写出明确的跨域域名。

HandshakeInterceptor握手拦截器

除了Handler类,Spring的WebSocket封装还提供了HandshakeInterceptor接口,它用于对WebSocket建立握手连接前和建立握手连接后进行AOP风格的拦截处理。HandshakeInterceptor接口有如下两个重要方法。

package org.springframework.web.socket.server;

import java.util.Map;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.socket.WebSocketHandler;

public interface HandshakeInterceptor {
    boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception;

    void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, @Nullable Exception exception);
}

beforeHandshake:建立握手连接前处理,返回true继续连接,返回false则中断

afterHandshake:建立握手连接后处理

前面说过WebSocket建立连接的第一步是HTTP Upgrade请求,HandshakeInterceptor就是在这一步起作用的,我们可以看到参数中都是ServerHttpRequestServerHttpResponse等HTTP协议请求和响应的对象,我们可以借此进行建立WebSocket连接前的认证、鉴权等操作。

下面例子中我们编写了一个拦截器,并进行注册。

ChatInterceptor.java

package com.gacfox.demo.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

@Slf4j
public class ChatInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        log.info("建立连接前");
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        log.info("建立连接后");
    }
}

WsConfig.java

package com.gacfox.demo.config;

import com.gacfox.demo.interceptor.ChatInterceptor;
import com.gacfox.demo.ws.ChatHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
public class WsConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler(), "/ws/chat")
                .addInterceptors(new ChatInterceptor());
    }

    @Bean
    public ChatHandler chatHandler() {
        return new ChatHandler();
    }
}

代码非常简单,这里就不多解释了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。