Spring使用WebSocket

一、WebSocket简介

WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据。HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是WebSocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已。WebSocket的握手基于HTTP中的协议升级机制,当服务端收到这个HTTP的协议升级请求后,如果支持WebSocket协议则返回HTTP状态码101,WebSocket的握手便成功了,之后客户端与服务端会使用之前HTTP请求所使用的TCP连接来相互发送消息。它和 HTTP 最大不同是:WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动地向对方发送或接收数据。

二、原生websocket实现

1、使用@ServerEndpoint注解(该注解由java提供)监听一个WebSocket请求路径,这里监听了客户端的连接端口/reverse,并定义了如何处理客户端发来的消息。

@ServerEndpoint("/reverse")
public class ReverseWebSocketEndpoint {
 
    @OnMessage
    public void handleMessage(Session session, String message) throws IOException {
        session.getBasicRemote().sendText("Reversed: " + new StringBuilder(message).reverse());
    }

    /**
     * 连接开启
     */
    @OnOpen
    ...

    /**
     * 连接关闭
     */
    @OnClose
    ...

    /**
     * 连接异常
     */
    @OnError
    ... 
 
}

三、使用spring实现WebSocket

注意:在springboot工程中使用websocket时,@EnableWebSocket***要在启动类上使用此注解。

1、握手拦截

WebSocket 首次连接时,默认使用了 OriginHandshakeInterceptor 拦截,它主要做域名拦截,我们可以通过继承它做其他连接处理。这个类中的方法,只会在连接时执行一次,数据传输过程中会走通道拦截。

@Component
public class AuthHandshakeInterceptor extends OriginHandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler
            wsHandler, Map attributes) throws Exception {
        System.out.println("beforeHandshake");
        return super.beforeHandshake(request, response, wsHandler, attributes);
    }


    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
                               @Nullable Exception ex) {
        System.out.println("afterHandshake");
        super.afterHandshake(request, response, wsHandler, ex);
    }
}

2、通道拦截

WebSockt 是通过 Channel 传输数据,通过继承 ChannelInterceptorAdapter 可做到对输入和输出数据的拦截处理。

@Component
public class InboundChannelInterceptor extends ChannelInterceptorAdapter {

    @Autowired
    private WebSocketService webSocketService;

    @Override
    public Message preSend(Message message, MessageChannel channel) {
        System.out.println("preSend:" + message.getHeaders());
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        StompCommand stompCommand = accessor.getCommand();
        if (StompCommand.CONNECT.equals(stompCommand)) {
            String userId = accessor.getFirstNativeHeader("userId");
            String simpSessionId = accessor.getHeader("simpSessionId").toString();
            this.webSocketService.connect(simpSessionId, userId);
        } else if (StompCommand.DISCONNECT.equals(stompCommand)) {
            this.webSocketService.disconnect(accessor.getHeader("simpSessionId").toString());
        }
        return super.preSend(message, channel);
    }

    @Override
    public void postSend(Message message, MessageChannel channel, boolean sent) {
        System.out.println("postSend输入数据处理后");
        super.postSend(message, channel, sent);
    }

    @Override
    public void afterSendCompletion(Message message, MessageChannel channel, boolean sent, @Nullable Exception ex) {
        super.afterSendCompletion(message, channel, sent, ex);
        System.out.println("afterSendCompletion");
    }
}

3、控制器

控制器写法和 SpringMVC 类似,主要是使用 @MessageMapping 做匹配处理。

@MessageMapping("/stomp")
@RestController
public class WebSocketController {

    // @SendTo("/topic/message") 广播发送给 /topic/message 订阅客户端
    // @SendToUser("1/message") 发送给指定订阅用户
    @Autowired
    public WebSocketService webSocketService;

    @MessageMapping("/send/{pathParam}")
    public void sendBroadcast(@Headers Map headers, @Header String simpSessionId, TextMessage
            textMessage, @DestinationVariable String pathParam) {
        TextMessage sendTM = new TextMessage(this.webSocketService.getPool().get(simpSessionId) + ": " + textMessage
                .getContent());
        String userId = textMessage.getUserId();
        if (StringUtils.isEmpty(userId)) {
            this.webSocketService.send(sendTM);
        } else {
            this.webSocketService.sendToUser(userId, sendTM);
        }
    }

}

4、业务层

业务层 WebSocketService 实现了登录、断开、群发和定点发送消息的功能。

@Service
public class WebSocketService {

    // 连接池
    private Map pool; 

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    // 被注解的方法,在对象加载完依赖注入后执行,且只执行一次
    @PostConstruct
    void init() {
        this.pool = new HashMap();
    }

    public Map getPool() {
        return pool;
    }

    /** * 连接 * * @param simpSessionId 会话id * @param userId 用户id */
    public void connect(String simpSessionId, String userId) {
        if (!(StringUtils.isEmpty(simpSessionId) || StringUtils.isEmpty(userId))) {
            this.pool.put(simpSessionId, userId);
            TextMessage textMessage = new TextMessage(userId + "上线,当前在线人数:" + this.pool.size());
            this.simpMessagingTemplate.convertAndSend("/topic/message", textMessage);
        }
    }


    /** * 断开连接 * * @param simpSessionId 会话id */
    public void disconnect(String simpSessionId) {
        String userId = this.pool.get(simpSessionId);
        if (userId != null) {
            this.pool.remove(simpSessionId);
            TextMessage textMessage = new TextMessage(userId + "离线,当前在线人数:" + this.pool.size());
            this.send(textMessage);
        }
    }

    /** * 给所有用户发送消息 * * @param payload 消息 * @throws MessagingException */
    public void send(Object payload) throws MessagingException {
        this.simpMessagingTemplate.convertAndSend("/topic/message", payload);
    }

    /** * 给指定用户发消息 * * @param userId 用户id * @param payload 消息 * @throws MessagingException */
    public void sendToUser(String userId, Object payload) throws MessagingException {
        this.simpMessagingTemplate.convertAndSendToUser(userId, "/message", payload);
    }

}

5、数据模型

public class TextMessage {

    // 为了避免客户端传空,引起解析报错,需在字段上添加@Nullable。
    @Nullable
    // 用户id
    private String userId;

    @Nullable
    // 内容
    private String content; 

    public TextMessage() {
    }

    public TextMessage(String content) {
        this.content = content;
    }

    @Nullable
    public String getUserId() {
        return userId;
    }

    @Nullable
    public String getContent() {
        return content;
    }
}

6、注册WebSocket路由,设置handler处理

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 使用MessageHandler处理器处理"websocket"过来的请求
        registry.addHandler(new MessageHandler(), "/websocket")
        .addInterceptors(new HttpSessionHandshakeInterceptor())
        // 允许跨域访问
        .setAllowedOrigins("*"); 
    }
}

其他拦截器

1、实现WebSocketHandler接口来处理握手、连接、关闭、接收信息、发送信息的处理类。

void afterConnectionEstablished(WebSocketSession session) throws Exception;

void handleMessage(WebSocketSession session, WebSocketMessage message) throws Exception;

void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;

void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;

boolean supportsPartialMessages();

2、继承AbstractWebSocketHandler抽象类,根据不同业务重写信息处理。

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception;

protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception;

protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception;

3、继承Spring写好的文本处理类TextWebSocketHandler。对建立连接、接收/发送消息、异常等情况进行处理。

// 关闭连接
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception;

// 三次握手成功
public void afterConnectionEstablished(WebSocketSession session) throws Exception;

// 处理客户发送的信息
public void handleMessage(WebSocketSession session, WebSocketMessage message) throws Exception;


// 实现案例
public class MessageHandler extends TextWebSocketHandler{
    
    Logger log = LoggerFactory.getLogger(MessageHandler.class);
    
    //用来保存连接进来session
    private  List sessions = new CopyOnWriteArrayList<>();

    /**
     * 关闭连接进入这个方法处理,将session从 list中删除
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
      sessions.remove(session);
      log.info("{} 连接已经关闭,现从list中删除 ,状态信息{}", session, status);
    }

    /**
     * 三次握手成功,进入这个方法处理,将session 加入list 中
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
        log.info("用户{}连接成功.... ",session);
    }

    /**
     * 处理客户发送的信息,将客户发送的信息转给其他用户
     */
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage message) throws Exception {
        log.info("reveice client msg: {}",message.getPayload());
        session.sendMessage(new TextMessage("i reveice client msg...."+System.nanoTime()));
        for(WebSocketSession wss : sessions) 
            if(!wss.getId().equals(session.getId()))
                wss.sendMessage(message);   
    }
}

三、Spring启用STOMP

 

WebSocket在握手之后便直接基于TCP进行消息通信,但WebSocket只是TCP上面非常轻的一层,它仅仅将TCP的字节流转换成消息流(文本或二进制),至于怎么解析这些消息的内容完全依赖于应用本身。因此为了协助client与server进行消息格式的协商,WebSocket在握手的时候保留了一个子协议字段。STOMP是一种简单的面向文本的消息传递协议,最初是为脚本语言(如Ruby,Python和Perl)创建的,用于连接到企业消息代理。它旨在解决常用消息传递模式的一个子集。STOMP可以用于任何可靠的双向流媒体网络协议,如TCP和WebSocket。虽然STOMP是面向文本的协议,但消息的有效载荷可以是文本或二进制。Spring框架支持通过Spring-messaging和spring-websocket模块在WebSocket上使用STOMP。

Spring的spring-messaging模块支持STOMP协议,包含了消息处理的关键抽象。下面是一个简单的消息处理示意图:

Spring使用WebSocket_第1张图片

关键实体的作用如下:

Message:消息。里面带有header和payload。

MessageHandler:处理client消息的实体。

MessageChannel:解耦消息发送者与消息接收者的实体。client可以发送消息到channel,而不用管这条消息最终被谁处理。

Broker:存放消息的中间件。client可以订阅broker中的消息。

以下是一个STOMP帧:

// 同HTTP在TCP套接字上添加 请求-响应 模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义。
// SEND:STOMP命令,表明会发送一些内容
SEND
// destination:头信息,用来表示消息发送到哪里
destination:/app/marco
// content-length:头信息,用来表示负载内容的大小
content-length:20
// 空行

// 帧内容(负载)内容
{\"message\":\"hello word!\"}

1、配置类

1)在spring中启用STOMP通讯不用我们自己去写原生态的帧,spring的消息功能是基于代理模式构建。在实现WebSocketMessageBrokerConfigurer接口的websocket配置类上使用@EnableWebSocketMessageBroker (表明启用WebSocket消息中间件,以及WebSocket消息处理)。

public interface WebSocketMessageBrokerConfigurer {

    // 添加这个Endpoint,这样在网页中就可以通过websocket连接上服务,也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs
    void registerStompEndpoints(StompEndpointRegistry var1);

    // 配置发送与接收的消息参数,可以指定消息字节大小,缓存大小,发送超时时间
    void configureWebSocketTransport(WebSocketTransportRegistration var1);

    // 设置输入消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
    void configureClientInboundChannel(ChannelRegistration var1);
    
    // 设置输出消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
    void configureClientOutboundChannel(ChannelRegistration var1);

    // 添加自定义的消息转换器,spring 提供多种默认的消息转换器,返回false,不会添加消息转换器,返回true,会添加默认的消息转换器,当然也可以把自己写的消息转换器添加到转换链中
    boolean configureMessageConverters(List var1);

    // 配置消息代理,哪种路径的消息会进行代理处理
    void configureMessageBroker(MessageBrokerRegistry var1);
    
    // 自定义控制器方法的参数类型,有兴趣可以百度google HandlerMethodArgumentResolver这个的用法
    void addArgumentResolvers(List var1);

    // 自定义控制器方法返回值类型,有兴趣可以百度google HandlerMethodReturnValueHandler这个的用法
    void addReturnValueHandlers(List var1);
}

2)配置类的一种实现方式。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{
    
    @Autowired
    private MyChannelInterceptor myChannelInterceptor;
 
	 /**
	 * 添加Endpoint,在网页中就可以通过websocket连接上服务,
	 * 也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs
	 */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        /**
         * 1. 将 /stomp/websocketJs路径注册为STOMP的端点,
         *    用户连接了这个端点后就可以进行websocket通讯。
         * 2. setAllowedOrigins("*")表示可以跨域。
         * 3. withSockJS()表示支持socktJS访问(在浏览器并不是直接使用原生的的WebSocket协议,而是使用更高级的SockJS和StompJS。
	 *    为了应对许多浏览器不支持WebSocket协议的问题,Spring对备选协议SockJS提供了支持。)
         * 4. 添加自定义拦截器。
         */
        registry.addEndpoint("/stomp/websocketJS")
                .setInterceptors(new WebSocketHandshakeInterceptor())
                .setAllowedOrigins("*")
                .withSockJS();
    }
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 定义了两个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
        registry.enableSimpleBroker("/topic", "/queue");
        
        // 定义了服务端接收地址的前缀(也是全局使用的订阅前缀),也即客户端给服务端发消息的地址前缀 
        registry.setApplicationDestinationPrefixes("/app");
        
        // 使用客户端一对一通信的时候的前缀(默认就是/user)通常与@SendToUser搭配使用
        registry.setUserDestinationPrefix("/user"); 
    }
 
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(myChannelInterceptor);
    }
	
}

3、如果我们业务关心用户的数量、在线数量、连接状况等数据,我们也可以通过ChannelRegistration对象的setInterceptors方法添加监听,这里先展示一个完整的实现类,监听接口在后面会介绍,代码中的WebSocketHandShakeInterceptor拦截器,是上一个例子已经实现的,用于存储httpsession,在WebSocketChannelInterceptor拦截器中可以做一些在线人数统计等操作,后面会介绍。

spring中关于WebSocket注解简介

@MessageMapping("/sendTest")
接收客户端发送的消息,当客户端发送消息的目的地为/app/sendTest时,交给该注解所在的方法处理消息,其中/app是在WebSocketConfig配置类的configureMessageBroker方法中添加。
registry.setApplicationDestinationPrefixes("/app");
若没有添加@SendTo注解且该方法有返回值,则返回的目的地地址为/topic/sendTest,经过消息代理,客户端需要订阅了这个主题才能收到返回消息。

@SubscribeMapping("/subscribeTest")
接收客户端发送的订阅,当客户端订阅的目的地为/app/subscribeTest时,交给该注解所在的方法处理订阅,其中/app为客户端请求前缀。
若没有添加@SendTo注解且该方法有返回值,则返回的目的地地址为/app/sendTest,不经过消息代理,客户端需要订阅了这个主题才能收到返回消息。

通过@SubscribeMapping 、SimpMessagingTemplate (服务器主动推送)的形式所订阅的地址结果(如果点击关闭WebSocket连接后将无法获取订阅消息)

@SendTo("/topic/subscribeTest")
返回消息的目的地地址为/topic/subscribeTest,经过消息代理,客户端需要订阅了这个主题才能收到返回消息。

 

你可能感兴趣的:(Spring使用WebSocket)