WebSocket 与 STOMP 协议

WebSocket 是在TCP上分层的全双工(允许数据在两个方向上同时传输)通信协议,它允许你在应用程序直接双向通信,通常用于浏览器和服务器之间的交互通信。它最大的特点就是服务器可以主动向客户端(浏览器)推送消息,客户端也可以主动向服务器发送消息。

WebSocket协议与Http协议对比

STOMP 是一种基于文本的简单消息传递协议,任何 STOMP 客户端都可以与任何 STOMP 消息代理进行通信。与 WebSocket 不同的是, STOMP 描述了客户端和服务器之间交换的消息格式,而 WebSocket 是一种通信协议。我们不能仅使用 STOMP 与服务器或消息代理通信,我们必须使用传输方式发送 STOMP 消息,其中之一就是 WebSocket。仅使用 WebSocket 也很难实现仅向订阅了特定主题的用户发送消息的功能,但是 STOMP 具有这些功能,因为它本来就是用来与消息代理进行交互的。因此两者搭配使用可以更快速简单的实现功能。

服务端配置

添加 Maven 依赖

pom.xml

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

创建 DTO

新建类 MessageResult ,用于返回前端;

public class MessageResult {
+
    public MessageResult() {
    }

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

    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

新建类 MessageParam ,用于接收前端参数;

public class MessageParam {

    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

创建控制器 Controller

import cn.hutool.core.date.DateUtil;
import com.lanweihong.hotel.pms.bean.MessageParam;
import com.lanweihong.hotel.pms.bean.MessageResult;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * @author lanweihong
 */
@Controller
public class WebSocketController {

    @MessageMapping("/hello")
    @SendTo("/topic/messages")
    public MessageResult test(@RequestBody MessageParam message) {
        return new MessageResult(DateUtil.now() + ":" + message.getContent());
    }
}

创建 WebSocket 配置类,用于 STOMP 消息传递

新建类 WebSocketConfig 用于配置 Spring 以启用 WebSocketSTOMP 消息传递;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.config.ChannelRegistration;
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.config.annotation.WebSocketTransportRegistration;

import java.util.List;

/**
 * @author lanweihong
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration webSocketTransportRegistration) {

    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration channelRegistration) {

    }

    @Override
    public void configureClientOutboundChannel(ChannelRegistration channelRegistration) {

    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> list) {

    }

    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> list) {

    }

    @Override
    public boolean configureMessageConverters(List<MessageConverter> list) {
        return false;
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

     @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/broadcast").withSockJS();
    }
}

@EnableWebSocketMessageBroker 表示启用由消息代理支持的WebSocket消息处理;

方法 configureMessageBroker() 实现 WebSocketMessageBrokerConfigurer 用于配置消息代理的默认方法,首先调用 enableSimpleBroker() 来启用一个简单的基于内存的消息代理,将请求消息发送到目的地 /topic ;并设置应用程序目标前缀为 /app,此前缀将用于定义所有消息映射,如 /app/message 端点则会调用 WebSocketController.test() 方法;

方法 registerStompEndpoints() 注册 /broadcast 端点;启用 SockJS 后备选项,以便在 WebSocket 不可用时可以使用备用传输。

前端客户端使用

安装插件

npm install sockjs-client
npm install stompjs

编写页面

<template>
  <div class="websocket-test-container whui-router-view-container">
    <div class="form-wrapper">
      <Input type="text" v-model="form.message" placeholder="消息内容"></Input>
    </div>
    <div class="action">
      <Button type="primary" @click="connectSocket">连接</Button>
      <Button type="info" @click="disconnectSocket">关闭连接</Button>
      <Button type="error" @click="sendMessage">发送</Button>
    </div>
    <div class="message-content">
      <div class="message-list">
        <ul>
          <li v-for="item in messageList" :key="item.content">
            {{item.content}}
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>
<script>
import SockJS from 'sockjs-client'
import Stomp from 'stompjs'
export default {
  data () {
    return {
      form: {
        message: ''
      },
      stompClient: null,
      messageList: []
    }
  },
  methods: {
    connectSocket () {
      // 连接
      let socket = new SockJS('http://127.0.0.1:8091/broadcast')
      // 获取 STOMP 子协议的客户端对象
      this.stompClient = Stomp.over(socket)
      // 发起 websocket 连接
      this.stompClient.connect({}, (frame) => {
        // 订阅 topic
        this.stompClient.subscribe('/topic/messages', (res) => {
          console.log(JSON.stringify(res.body))
          this.messageList.push(JSON.parse(res.body))
        })
      })
    },
    disconnectSocket () {
      if (this.stompClient != null) {
        this.stompClient.disconnect()
      }
    },
    sendMessage () {
      let data = {
        content: this.form.message
      }
      let dataStr = JSON.stringify(data)
      this.stompClient.send('/app/hello', {}, dataStr)
    }
  }
}
</script>
<style lang="stylus">
li
  list-style none
.websocket-test-container
  padding 20px
.message-list
  padding-top 10px
.form-wrapper
  padding-bottom 10px
</style>

运行测试

页面效果

前端效果图

单击 连接 后,请求标头内容如下:

GET ws://127.0.0.1:8091/broadcast/616/g1jm4jnu/websocket HTTP/1.1
Host: 127.0.0.1:8091
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36
Upgrade: websocket
Origin: http://127.0.0.1:8080
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
Sec-WebSocket-Key: 8koVldvnTgAED1I1TXR+hQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

响应标头内容如下:

HTTP/1.1 101
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://127.0.0.1:8080
Access-Control-Allow-Credentials: true
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: 7nqt8KuROUFLlDZbt8XgE4XooOE=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Date: Wed, 28 Oct 2020 10:14:56 GMT

在文本框中输入消息,单击 发送 ,可查看消息内容:

消息内容

参考文档

  1. Using WebSocket to build an interactive web application

  2. Using Spring Boot for WebSocket Implementation with STOMP

文章目录