SpringBoot+sockjs client+stompjs实现websocket

SpringBoot+sockjs client+stompjs实现websocket

什么是sockjs-client

​ sockjs-client是从SockJS中分离出来的用于客户端使用的通信模块.所以我们就直接来看看SockJS. SockJS是一个浏览器的JavaScript库,它提供了一个类似于网络的对象,SockJS提供了一个连贯的,跨浏览器的JavaScriptAPI,它在浏览器和Web服务器之间创建了一个低延迟,全双工,跨域通信通道. 你可能会问,我为什么不直接用原生的WebSocket而要使用SockJS呢?这得益于SockJS的一大特性,一些浏览器中缺少对WebSocket的支持,因此,回退选项是必要的,而Spring框架提供了基于SockJS协议的透明的回退选项。SockJS提供了浏览器兼容性,优先使用原生的WebSocket,如果某个浏览器不支持WebSocket,SockJS会自动降级为轮询.

SockJS模仿WebSockets API,但它不是WebSocket,而是一个SockJS Javascript对象。首先,您需要加载SockJS JavaScript库。例如,你可以把它放在你的HTML head里:

<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>

stomjs

​ STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议; WebSocket是一个消息架构,不强制使用任何特定的消息协议,它依赖于应用层解释消息的含义. 与HTTP不同,WebSocket是处在TCP上非常薄的一层,会将字节流转化为文本/二进制消息,因此,对于实际应用来说,WebSocket的通信形式层级过低,因此,可以在 WebSocket 之上使用STOMP协议,来为浏览器 和 server间的 通信增加适当的消息语义。

STOMP与WebSocket 的关系:

  1. HTTP协议解决了web浏览器发起请求以及web服务器响应请求的细节,假设HTTP协议不存在,只能使用TCP套接字来编写web应用,你可能认为这是一件疯狂的事情;
  2. 直接使用WebSocket(SockJS)就很类似于使用TCP套接字来编写web应用,因为没有高层协议,就需要我们定义应用间发送消息的语义,还需要确保连接的两端都能遵循这些语义;
  3. 同HTTP在TCP套接字上添加请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义.

开始搭建

创建spring boot 项目,并且依赖勾选 Thymeleaf(前端使用的是模板引擎,你也可以些其他的,只要导入sockjs库就行)

POM:

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-thymeleafartifactId>
    dependency>
    
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-websocketartifactId>
    dependency>
    

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
    dependency>

    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <version>1.16.18version>
        <scope>providedscope>
    dependency>

dependencies>

WebSocket 配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.messaging.StompSubProtocolErrorHandler;

/**
 * @Author: OF //作者及
 * @Date: 2019-09-04 10:00//完成日期
 * @Description: //初始化WebSocket
 * @Version: // 版本信息
 * @Function // 主要函数及其功能
 * @Others: // 其它内容的说明
 * @History: // 历史修改记录
 */
@Configuration
/**
 * @EnableWebSocketMessageBroker
 * 开启使用 STOMP 协议来传输基于代理的消息,Broker是代理
 */
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer
{
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry)
    {
        /**
         * setErrorHandler: 设置一个错误处理的 Handler, 以便捕捉错误信息
         * addEndpoint: 切入点, 客户端在 new SockJs 的时候用到
         * setAllowedOrigins:设置为「*」表示接收 http 和 https 的请求
         * withSockJS: 使用 SockJS
         */
        registry.setErrorHandler(this.webSocketHandler())
                .addEndpoint("/endpointNiu")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry)
    {
        /**
         *  参数是多个 destinationPrefixes, 服务端发送消息的 destination 要有这些前缀
         */
        registry.enableSimpleBroker("/topic", "/queue");
        /**
         *  设置点对点时, destination 的前缀, 如客户端订阅
         */
        registry.setUserDestinationPrefix("/user");
    }

    /**
     * WebSocket Error 处理
     *
     * @return WebSocket Error 处理器
     */
    @Bean
    public StompSubProtocolErrorHandler webSocketHandler() {
        return new WebSocketErrorHandler();
    }
}

WebSocketErrorHandler.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.web.socket.messaging.StompSubProtocolErrorHandler;

/**
 * @Author: OF //作者及
 * @Date: 2019-09-04 10:18//完成日期
 * @Description: // 描述
 * @Version: // 版本信息
 * @Function // 主要函数及其功能
 * @Others: // 其它内容的说明
 * @History: // 历史修改记录
 */

@Slf4j
public class WebSocketErrorHandler extends StompSubProtocolErrorHandler
{
    public WebSocketErrorHandler()
    {
        super();
    }
    @Override
    public Message<byte[]> handleClientMessageProcessingError(Message<byte[]> clientMessage, Throwable ex) {
        log.error("handleClientMessageProcessingError:clientMessage-" + clientMessage + ", error-"+ex.getMessage());
        return super.handleClientMessageProcessingError(clientMessage, ex);
    }

    @Override
    public Message<byte[]> handleErrorMessageToClient(Message<byte[]> errorMessage) {
        log.error("handleErrorMessageToClient:errorMessage-" + errorMessage);
        return super.handleErrorMessageToClient(errorMessage);
    }

    @Override
    protected Message<byte[]> handleInternal(StompHeaderAccessor errorHeaderAccessor, byte[] errorPayload, Throwable cause, StompHeaderAccessor clientHeaderAccessor) {
        log.error("handleInternal:errorHeaderAccessor-" + errorHeaderAccessor + ", errorPayload-" + errorPayload + ", error-" + cause.getMessage() + ", clientHeaderAccessor-"+clientHeaderAccessor);
        return super.handleInternal(errorHeaderAccessor, errorPayload, cause, clientHeaderAccessor);
    }

}

控制层编写

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @Author: OF //作者及
 * @Date: 2019-09-04 10:24//完成日期
 * @Description: // 描述
 * @Version: // 版本信息
 * @Function // 主要函数及其功能
 * @Others: // 其它内容的说明
 * @History: // 历史修改记录
 */
@Controller
@Slf4j
public class WebController
{
    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    /**
     * 接收消息
     * @param name 姓名
     * @return welcome, [姓名] !
     *
     * @MessageMapping 类似于 @RequestMapping, 只不过映射的是 webSocket 的请求地址
     * @SendTo("/topic/getBro") 指定该方法响应给哪个 topic, 客户端订阅了 /topic/getBro 的都能收到方法响应
     */
    @MessageMapping("/welcome")
    @SendTo("/topic/getBro")
    public String say(String name) {
        log.info("name: " + name);
        return "welcome, " + name + " !";
    }

    /**
     * 广播式发送消息给订阅了「/topic/getBro」的客户端
     */
    @RequestMapping("sendMsgBro")
    @ResponseBody
    public void sendMsg() {
        messagingTemplate.convertAndSend("/topic/getBro", "服务器主动推送的广播消息");
    }

    /**
     * 发送消息给指定 sessionId 的客户端, 且该客户端订阅了「/topic/getBro」
     *
     * @param sessionId 客户端的 sessionId
     */
    @RequestMapping("sendMsgPoint")
    @ResponseBody
    public void sendMsgPoint(String sessionId) {
        messagingTemplate.convertAndSendToUser(sessionId, "/queue/getPoint", "服务器主动推送的点对点消息", createHeaders(sessionId));
    }

    private MessageHeaders createHeaders(String sessionId) {
        SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
        headerAccessor.setSessionId(sessionId);
        headerAccessor.setLeaveMutable(true);
        return headerAccessor.getMessageHeaders();
    }
}

监听器

新客户端连接
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;

/**
 * @Author: OF //作者及
 * @Date: 2019-09-04 10:27//完成日期
 * @Description: // 新客户端连接监听器
 * @Version: // 版本信息
 * @Function // 主要函数及其功能
 * @Others: // 其它内容的说明
 * @History: // 历史修改记录
 */
@Slf4j
@Component
public class WebSocketConnectListener implements ApplicationListener<SessionConnectEvent>
{
    @Override
    public void onApplicationEvent(SessionConnectEvent sessionConnectEvent)
    {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(sessionConnectEvent.getMessage());
        String sessionId = accessor.getSessionId();
        log.info("sessionId: {} 已连接", sessionId);
    }
}

断开连接监听器
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

/**
 * @Author: OF //作者及
 * @Date: 2019-09-04 10:31//完成日期
 * @Description: // 断开连接监听器
 * @Version: // 版本信息
 * @Function // 主要函数及其功能
 * @Others: // 其它内容的说明
 * @History: // 历史修改记录
 */
@Slf4j
@Component
public class WebSocketDisconnectListener implements ApplicationListener<SessionDisconnectEvent>
{

    @Override
    public void onApplicationEvent(SessionDisconnectEvent event) {
        StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
        String sessionId = sha.getSessionId();
        log.info("sessionId: {} 已断开", sessionId);
    }
}

客户端

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>WebSockettitle>
    <script th:src="@{js/sockjs.min.js}">script>
    <script th:src="@{js/stomp.js}">script>
    <script th:src="@{js/jquery-3.1.1.js}">script>
head>
<body onload="disconnect()">
<noscript><h2 style="color: #e80b0a;">Sorry,浏览器不支持WebSocketh2>noscript>
<div>
    <div>
            <button id="connect" onclick="connect();">连接button>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接button>
    div>

    <div id="conversationDiv">
        <label>输入你的名字label><input type="text" id="name"/>
        <button id="sendName" onclick="sendName();">发送button>
        <p id="response">p>
    div>
div>
<script type="text/javascript">
    var stompClient = null;
    function setConnected(connected) {
        document.getElementById("connect").disabled = connected;
        document.getElementById("disconnect").disabled = !connected;
        document.getElementById("conversationDiv").style.visibility = connected ? 'visible' : 'hidden';
//        $("#connect").disabled = connected;
//        $("#disconnect").disabled = !connected;
        $("#response").html();
    }
    function connect() {
        var socket = new SockJS('/endpointNiu');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            setConnected(true);
            console.log('Connected:' + frame);
            // 订阅 /user/queue/getPoint
            stompClient.subscribe('/user/queue/getPoint', function (response) {
                showResponse("getPoint " + response.body);
            });
            // 订阅 /topic/getBro
            stompClient.subscribe('/topic/getBro', function (response) {
                showResponse("getBro " + response.body);
            })
        });

    }
    // 断开连接
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log('Disconnected');
    }
    // 向服务器发送
    function sendName() {
        var name = $('#name').val();
        console.log('name:' + name);
        stompClient.send("/welcome", {}, name);
    }
    // 显示message
    function showResponse(message) {
        $("#response").html(message);
    }
script>
body>
html>

添加地址映射

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * @Author: OF //作者及
 * @Date: 2019-09-04 10:41//完成日期
 * @Description: // 描述
 * @Version: // 版本信息
 * @Function // 主要函数及其功能
 * @Others: // 其它内容的说明
 * @History: // 历史修改记录
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport
{

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/ws").setViewName("/ws");
    }

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/js/**")
                .addResourceLocations("classpath:/static/js/");
    }
}

以上代码借鉴该文章

SpringBoot+Webocket 初步使用

前端VUE写法(含心跳)

// 安装并引入相关模块
import SockJS from  'sockjs-client';  
import  Stomp from 'stompjs';
export default {
    data() {
      return {
        dataList: []
      };
    },
    mounted:function(){
      this.initWebSocket();
    },
    beforeDestroy: function () {
      // 页面离开时断开连接,清除定时器
      this.disconnect();
      clearInterval(this.timer);
    },
    methods: {
      initWebSocket() {
        this.connection();
        let self = this;
        // 断开重连机制,尝试发送消息,捕获异常发生时重连
        this.timer = setInterval(() => {
          try {
            self.stompClient.send("test");
          } catch (err) {
            console.log("断线了: " + err);
            self.connection();
          }
        }, 5000);
      },
      removeTab(targetName) {
        console.log(targetName)
      },
      connection() {
      // 建立连接对象
        this.socket = new SockJS('http://xxxxxx:8089/ws');//连接服务端提供的通信接口,连接以后才可以订阅广播消息和个人消息
        // 获取STOMP子协议的客户端对象
        this.stompClient = Stomp.over(this.socket);
        // 定义客户端的认证信息,按需求配置
        var headers = {
          login: 'mylogin',
          passcode: 'mypasscode',
          // additional header
          'client-id': 'my-client-id'
        };
        // 向服务器发起websocket连接
        this.stompClient.connect(headers,(frame) => {
          this.stompClient.subscribe('/topic/chat_msg', (msg) => { // 订阅服务端提供的某个topic
           consolel.log(msg.body);  // msg.body存放的是服务端发送给我们的信息
          });
        }, (err) => {
            // 连接发生错误时的处理函数
            console.log(err);
        });

      },
      // 断开连接
      disconnect() {
        if (this.stompClient != null) {
          this.stompClient.disconnect();
          console.log("Disconnected");
        }
      }
    }
};

参考:

在vue中使用SockJS实现webSocket通

STOMP 客户端 API 整理

你可能感兴趣的:(java,webSocket)