SpringBoot集成WebSocket-1

101 阅读5分钟

WebSocket定义

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

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket 是独立的、创建在 TCP 上的协议。

Websocket 通过HTTP/1.1 协议的101状态码进行握手。

为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(handshaking)。

集成注解方式

注解

  • @ServerEndpoint 注解是 tomcat 7 中新增加的一个注解,用于标识一个类为 WebSocket 服务端点

    WebSocket 服务端点监听客户端的 WebSocket 连接,并将连接管理起来,供客户端和服务端进行实时通信

    一个标注有 @ServerEndpoint 的类必须包含一个无参构造函数,并且可以有一个或多个注解为 @OnOpen、@OnClose、 @OnMessage、@OnError 的方法。

  • @OnOpen:当 WebSocket 连接建立时,会调用标注有 @onOpen 的方法

  • @OnClose:当 WebSocket 连接关闭时,会调用标注有 @OnClose 的方法

  • @OnMessage:当收到客户端发送的消息时,会调用标注有 @OnMessage 的方法

    在 @OnMessage 方法中,可以通过参数文本、二进制、PongMessage 等方式来接收客户端发送的消息。

    同样可以通过 Session 给客户端发送消息,以实现双向通信。

  • @OnError:当出现错误时,会调用标注有 @OnError 的方法

注意:标注 @ServerEndpoint 注解的类对象是多例的,即每个连接都会创建一个新的对象

@ServerEndpoint 注解的参数配置

value 参数:必选参数,用于指定 WebSocket 服务端点的 URI 地址

decoders 参数:数组类型,指定解码器

包含用于将 WebSocket 消息解码为 Java 对象的解码器(Decoder)的类

解码器帮助将原始消息转换为 Java 对象

encoders 参数:数组类型,指定编解码器

包含用于将 Java 对象编码为 WebSocket 消息的编码器(Encoder)的类。

编码器帮助将 Java 对象转换为可以发送到客户端的消息

subprotocols 参数:用于指定一个或多个 WebSocket 的子协议

configurator 参数:一个类,用于提供配置 WebSocket 端点的自定义配置器

这允许在 WebSocket 端点创建和配置过程中进行额外的设置

注解使用实例

依赖

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

核心处理类

import com.blackcrow.common.utils.UUIDUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
​
/**
 * websocket 服务端点。注意:websocket对象是多例的
 */
@Slf4j
@Component
@ServerEndpoint(value = "/webSocket/{uid}", configurator = WebSocketServerConfigurator.class)
public class WebSocketServer {
  
  //静态变量,用来记录当前在线连接数。应设计成线程安全的。
    private static final AtomicInteger onlineNum = new AtomicInteger(0);
    /**
      存储客户端对象和web Socket对象可以整合到一起
    */
    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
    private static CopyOnWriteArraySet<Session> sessionPools = new CopyOnWriteArraySet<Session>();
    // 用于存储每个用户客户端对象
    public static Map<String, WebsocketServer> onlineUserMap = new ConcurrentHashMap<>();
​
    // 用户id
    private String userId;
    // 会话
    private Session session;
​
    @OnOpen
    public void onOpen(Session session, EndpointConfig config){
        this.session = session;
        this.userId = UUIDUtil.get4UUID();
        log.info("收到来自窗口的连接,userId={}", this.userId);
        onlineUserMap.put(this.userId, this);
        sessionPools.add(session);
        onlineNum.incrementAndGet();
        log.info(uid + "加入webSocket!当前人数为" + onlineNum);
    }
​
    @OnMessage
    public void onMessage(String message, Session session){
        log.info("收到来自服务端[{}]的的信息: {}", this.userId, message);
    }
​
    @OnClose
    public void onClose(Session session){
        onlineUserMap.remove(this.userId);
        this.session = session;
        sessionPools.remove(session);
        int cnt = onlineNum.decrementAndGet();
        log.info("有连接关闭,当前连接数为:{}", cnt);
    }
​
    @OnError
    public void onError(Session session, Throwable throwable){
        log.error("发生错误");
        throwable.printStackTrace();
    }
​
    /**
     * 給session连接推送消息
     * 由于getBasicRemote()的同步特性,并且它支持部分消息的发送即sendText(xxx,boolean isLast). isLast的值表示是否一次发     * 送消息中的部分消息,对于如下情况:
     *   session.getBasicRemote().sendText(message, false); 
     *   session.getBasicRemote().sendBinary(data);
     *   session.getBasicRemote().sendText(message, true); 
     *   由于同步特性,第二行的消息必须等待第一行的发送完成才能进行,而第一行的剩余部分消息要等第二行发送完才能继续发送,所以        *  在第二行会抛出IllegalStateException异常。如果要使用getBasicRemote()同步发送消息,则避免尽量一次发送全部消息,使用     *  部分消息来发送。
     */
    private void sendMessage(Object message) {
        try {
            this.session.getBasicRemote().sendObject(message);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("向客户端推送数据发生错误", e);
        }
    }
  
    public static void sendMessage(Session session, String message) throws IOException {
        if (session != null) {
            synchronized (session){
                //getAsyncRemote()和getBasicRemote()确实是异步与同步的区别,大部分情况下,推荐使用getAsyncRemote()
                session.getAsyncRemote().sendText(message);
            }
        }
    }
​
    /**
     * 向所有连接群发消息
     */
    public static void sendMessageToAll(Object message){
        for (WebsocketServer item : onlineUserMap.values()) {
            try {
                item.sendMessage(message);
            } catch (Exception e) {
                log.error("向客户端推送数据发生错误", e);
            }
        }
    }
}
​

WebSocketConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
​
@Configuration
public class WebSocketConfig {
​
    /**
    * 注入一个ServerEndpointExporter,
     * 该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
     *
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

WebSocketServerConfigurator

请求链接拦截器

自定义校验权限,设置自定义请求头

sec-websocket-protocol

ws和http区别之一,但是http可以自定义设置请求头,ws却不能

var aWebSocket = new WebSocket(url [, protocols]);
url
要连接的URL;这应该是WebSocket服务器将响应的URL。
protocols 可选
一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个WebSocket子协议(例如,您可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互)。如果不指定协议字符串,则假定为空字符串。

protocols对应的就是发起ws连接时, 携带在请求头中的Sec-WebSocket-Protocol属性, 服务端可以获取到此属性的值用于通信逻辑(即通信子协议,当然用来进行token认证也是完全没问题的)

import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import java.util.List;
import java.util.Map;
​
/**
 * 握手处理器
 */
public class WebSocketServerConfigurator extends ServerEndpointConfig.Configurator {
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 获取客户端发送的HTTP请求头信息
        Map<String, List<String>> headers = request.getHeaders();
        // 获取token
        String submitedToken = headers.get("sec-websocket-protocol")
        // TODO verifyToken 校验token
        // 检查某个特定的请求头是否存在或是否符合要求
        List<String> customHeaderValues = headers.get("Custom-Header");
        if (customHeaderValues == null || customHeaderValues.isEmpty() || !customHeaderValues.get(0).equals("ExpectedValue")) {
            // 如果请求头不符合要求,则拒绝握手
            throw new RuntimeException("Custom header is missing or invalid");
        }
​
        // 如果请求头符合要求,则继续握手过程
        super.modifyHandshake(sec, request, response);
    }
}
​