STOMP

简介

直接使用WebSocket(或SockJS)就很类似于使用TCP套接字来编写Web应用。因为没有高层级的线路协议(wire protocol),因此就需要我们定义应用之间所发送消息的语义,还需要确保连接的两端都能遵循这些语义。

​ 就像HTTP在TCP套接字之上添加了请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式(frame-based wire format)层,用来定义消息的语义。

​ 与HTTP请求和响应类似,STOMP帧由命令、一个或多个头信息以及负载所组成。例如,如下就是发送数据的一个STOMP帧:

>>> SEND
transaction:tx-0
destination:/app/marco
content-length:20

{"message":"Marco!"}

​ 在这个例子中,STOMP命令是send,表明会发送一些内容。紧接着是三个头信息:一个表示消息的的事务机制,一个用来表示消息要发送到哪里的目的地,另外一个则包含了负载的大小。然后,紧接着是一个空行,STOMP帧的最后是负载内容。

服务端实现

启用STOMP功能

​ STOMP 的消息根据前缀的不同分为三种。

如下:

以 /app 开头的消息都会被路由到带有@MessageMapping 或 @SubscribeMapping 注解的方法中;

以/topic 或 /queue 开头的消息都会发送到STOMP代理中,根据你所选择的STOMP代理不同,目的地的可选前缀也会有所限制;

以/user开头的消息会将消息重路由到某个用户独有的目的地上。

STOMP_第1张图片

@Configuration
@EnableWebSocketMessageBroker
@PropertySource("classpath:resources.properties")
public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Value("${rabbitmq.host}")
    private String host;

    @Value("${rabbitmq.port}")
    private Integer port;

    @Value("${rabbitmq.userName}")
    private String userName;

    @Value("${rabbitmq.password}")
    private String password;

    /**
     * 将 "/stomp" 注册为一个 STOMP 端点。这个路径与之前发送和接收消息的目的地路径有所
     * 不同。这是一个端点,客户端在订阅或发布消息到目的地路径前,要连接到该端点。
     *
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/stomp").withSockJS();
    }


    /**
     * 如果不重载它的话,将会自动配置一个简单的内存消息代理,用它来处理以"/topic"为前缀的消息
     *
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //基于内存的STOMP消息代理
        registry.enableSimpleBroker("/queue", "/topic");

        //基于RabbitMQ 的STOMP消息代理
/*        registry.enableStompBrokerRelay("/queue", "/topic")
                .setRelayHost(host)
                .setRelayPort(port)
                .setClientLogin(userName)
                .setClientPasscode(password);*/

        registry.setApplicationDestinationPrefixes("/app", "/foo");
        registry.setUserDestinationPrefix("/user");
    }
}

处理来自客户端的STOMP消息

​ 服务端处理客户端发来的STOMP消息,主要用的是 @MessageMapping 注解。如下:

  @MessageMapping("/marco")
  @SendTo("/topic/marco")
  public Shout stompHandle(Shout shout){
      LOGGER.info("接收到消息:" + shout.getMessage());
      Shout s = new Shout();
      s.setMessage("Polo!");
      return s;
  }

@MessageMapping指定目的地是“/app/marco”(“/app”前缀是隐含的,因为我们将其配置为应用的目的地前缀)。

方法接收一个Shout参数,因为Spring的某一个消息转换器会将STOMP消息的负载转换为Shout对象。Spring 4.0提供了几个消息转换器,作为其消息API的一部分:

STOMP_第2张图片

尤其注意,这个处理器方法有一个返回值,这个返回值并不是返回给客户端的,而是转发给消息代理的,如果客户端想要这个返回值的话,只能从消息代理订阅。@SendTo 注解重写了消息代理的目的地,如果不指定@SendTo,帧所发往的目的地会与触发处理器方法的目的地相同,只不过会添加上“/topic”前缀。

如果客户端就是想要服务端直接返回消息呢?听起来不就是HTTP做的事情!即使这样,STOMP 仍然为这种一次性的响应提供了支持,用的是@SubscribeMapping注解,与HTTP不同的是,这种请求-响应模式是异步的…

   @SubscribeMapping("/getShout")
   public Shout getShout(){
       Shout shout = new Shout();
       shout.setMessage("Hello STOMP");
       return shout;
   }

发送消息到客户端

在处理消息之后发送消息

​ 正如前面看到的那样,使用 @MessageMapping 或者 @SubscribeMapping 注解可以处理客户端发送过来的消息,并选择方法是否有返回值。

​ 如果 @MessageMapping 注解的控制器方法有返回值的话,返回值会被发送到消息代理,只不过会添加上"/topic"前缀。可以使用@SendTo 重写消息目的地;

​ 如果 @SubscribeMapping 注解的控制器方法有返回值的话,返回值会直接发送到客户端,不经过代理。如果加上@SendTo 注解的话,则要经过消息代理。

在应用的任意地方发送消息

​ spring-websocket 定义了一个 SimpMessageSendingOperations 接口(或者使用SimpMessagingTemplate ),可以实现自由的向任意目的地发送消息,并且订阅此目的地的所有用户都能收到消息。

  @Autowired
  private SimpMessageSendingOperations simpMessageSendingOperations;


  /**
  * 广播消息,不指定用户,所有订阅此的用户都能收到消息
  * @param shout
  */
  @MessageMapping("/broadcastShout")
  public void broadcast(Shout shout) {
      simpMessageSendingOperations.convertAndSend("/topic/shouts", shout);
  }

为指定用户发送消息

​ 3.2介绍了如何广播消息,订阅目的地的所有用户都能收到消息。如果消息只想发送给特定的用户呢?spring-websocket 介绍了两种方式来实现这种功能,一种是 基于@SendToUser注解和Principal参数,一种是SimpMessageSendingOperations 接口的convertAndSendToUser方法。

  • 基于@SendToUser注解和Principal参数

​ @SendToUser 表示要将消息发送给指定的用户,会自动在消息目的地前补上"/user"前缀。如下,最后消息会被发布在 /user/queue/notifications-username。但是问题来了,这个username是怎么来的呢?就是通过 principal 参数来获得的。那么,principal 参数又是怎么来的呢?需要在spring-websocket 的配置类中重写 configureClientInboundChannel 方法,添加上用户的认证。

spring-websocket 用户认证

  @MessageMapping("/shout")
  @SendToUser("/queue/notifications")
  public Shout userStomp(Principal principal, Shout shout) {
        String name = principal.getName();
        String message = shout.getMessage();
        LOGGER.info("认证的名字是:{},收到的消息是:{}", name, message);
        return shout;
  }
  • convertAndSendToUser方法

​ 除了convertAndSend()以外,SimpMessageSendingOperations 还提供了convertAndSendToUser()方法。按照名字就可以判断出来,convertAndSendToUser()方法能够让我们给特定用户发送消息。

    @MessageMapping("/singleShout")
    public void singleUser(Shout shout, StompHeaderAccessor stompHeaderAccessor) {
        String message = shout.getMessage();
        LOGGER.info("接收到消息:" + message);
        Principal user = stompHeaderAccessor.getUser();
        simpMessageSendingOperations.convertAndSendToUser(user.getName(), "/queue/shouts", shout);
    }

如上,这里虽然我还是用了认证的信息得到用户名。但是,其实大可不必这样,因为 convertAndSendToUser 方法可以指定要发送给哪个用户。也就是说,完全可以把用户名的当作一个参数传递给控制器方法,从而绕过身份认证!convertAndSendToUser 方法最终会把消息发送到 /user/sername/queue/shouts 目的地上。

处理消息异常

​ 在处理消息的时候,有可能会出错并抛出异常。因为STOMP消息异步的特点,发送者可能永远也不会知道出现了错误。@MessageExceptionHandler标注的方法能够处理消息方法中所抛出的异常。我们可以把错误发送给用户特定的目的地上,然后用户从该目的地上订阅消息,从而用户就能知道自己出现了什么错误啦…

@MessageExceptionHandler(Exception.class)
     @SendToUser("/queue/errors")
     public Exception handleExceptions(Exception t){
         t.printStackTrace();
         return t;
     }

springboot使用STOMP消息步骤

  • 添加pom文件依赖

  • java方式配置websocket stomp

  • 消息实体类

  • 书写控制层

  • 书写客户端

添加pom文件依赖

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

java方式配置websocket stomp

package com.ahut.config;

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

/**

- 
- @ClassName: WebSocketStompConfig
- @Description: springboot websocket stomp配置
- @author cheng
- @date 2017年9月27日 下午3:45:36
  */

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer {
/**
 * 注册stomp的端点
 */
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    // 允许使用socketJs方式访问,访问点为webSocketServer,允许跨域
    // 在网页上我们就可以通过这个链接
    // http://localhost:8080/webSocketServer
    // 来和服务器的WebSocket连接
    registry.addEndpoint("/webSocketServer").setAllowedOrigins("*").withSockJS();
}

/**
 * 配置信息代理
 */
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
    // 订阅Broker名称
    registry.enableSimpleBroker("/queue", "/topic");
    
  // 全局使用的消息前缀(客户端订阅路径上会体现出来)
    registry.setApplicationDestinationPrefixes("/app");
    
  // 点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
    // registry.setUserDestinationPrefix("/user/");
}

消息实体类(略)

书写控制层

package com.ahut.action;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.stereotype.Controller;

import com.ahut.entity.ClientMessage;
import com.ahut.entity.ServerMessage;

/**
- @ClassName: WebSocketAction

- @Description: websocket控制层

- @author cheng

- @date 2017年9月27日 下午4:20:58
  */
  @Controller
  public class WebSocketAction {
    
    @Autowired
    private SimpMessagingTemplate template;

  private Logger logger = LoggerFactory.getLogger(this.getClass());

  @MessageMapping("/sendTest")
  @SendTo("/topic/subscribeTest")
  public ServerMessage sendDemo(ClientMessage message) {

  ```
  logger.info("接收到了信息" + message.getName());
  return new ServerMessage("你发送的消息为:" + message.getName());
  ```

  }

  @SubscribeMapping("/subscribeTest")
  public ServerMessage sub() {

  ```
  logger.info("XXX用户订阅了我。。。");
  return new ServerMessage("感谢你订阅了我。。。");

  ```

  }
    //以下两个接口都是属于服务器主动推数据到前端
    //广播推送消息
    @Scheduled(fixedRate = 10000)
    public void sendTopicMessage() {
	System.out.println("后台广播推送!");
	User user=new User();
	user.setUserName("oyzc");
	user.setAge(10);
    	this.template.convertAndSend("/topic/getResponse",user);
    }

     //一对一推送消息
    @Scheduled(fixedRate = 10000)
    public void sendQueueMessage() {
	System.out.println("后台一对一推送!");
	User user=new User();
	user.setUserId(1);
	user.setUserName("oyzc");
	user.setAge(10);
	this.template.convertAndSendToUser(user.getUserId()+"","/queue/getResponse",user);
    }


 }

SimpMessagingTemplate:SpringBoot提供操作WebSocket的对象

@Scheduled(fixedRate = 10000):为了测试,定时10S执行这个方法,向客户端推送

template.convertAndSend("/topic/getResponse",new AricResponse(“后台实时推送:,Oyzc!”)); :直接向前端推送消息。

参数一:客户端监听指定通道时,设定的访问服务器的URL

参数二:发送的消息(可以是对象、字符串等等)

.template.convertAndSendToUser(user.getUserId()+"","/queue/getResponse",user); :直接向前端推送消息。

参数一:指定客户端接收的用户标识(一般用用户ID)

参数二:客户端监听指定通道时,设定的访问服务器的URL(客户端访问URL跟广播有些许不同)

参数三:向目标发送消息体(实体、字符串等等)


<html>

<head>
<title>stomptitle>
head>

<body>
Welcome<br/><input id="text" type="text" />
<button onclick="send()">发送消息button>
<button onclick="subscribe2()">订阅消息/topic/sendTestbutton>
<button onclick="subscribe1()">订阅消息/topic/subscribeTestbutton>
<hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接button>
<hr/>
<div id="message">div>
body>

<script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js">script>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js">script>
<script type="text/javascript">
// 建立连接对象(还未发起连接)
var socket = new SockJS("http://localhost:8080/webSocketServer");

// 获取 STOMP 子协议的客户端对象
var stompClient = Stomp.over(socket);

// 向服务器发起websocket连接并发送CONNECT帧
stompClient.connect(
    {},
    function connectCallback(frame) {
      var userId=1;
        // 连接成功时(服务器响应 CONNECTED 帧)的回调方法
        setMessageInnerHTML("连接成功");
      stompClient.subscribe('/user/' + userId +'/queue/light', function (response) {
                setMessageInnerHTML("已成功订阅 "+'/user/' + userId +'/queue/light');
                var returnData = JSON.parse(response.body);
                setMessageInnerHTML('/user/' + userId +'/queue/light'+" 你接收到的消息为:" + returnData.responseMessage);
            });
    },
    function errorCallBack(error) {
        // 连接失败时(服务器响应 ERROR 帧)的回调方法
        setMessageInnerHTML("连接失败");
    }
);

//发送消息
function send() {
    var message = document.getElementById('text').value;
    var messageJson = JSON.stringify({ "name": message });
    stompClient.send("/app/sendTest", {}, messageJson);
    setMessageInnerHTML("/app/sendTest 你发送的消息:" + message);
}

//订阅消息
function subscribe1() {
    stompClient.subscribe('/topic/subscribeTest', function (response) {
        setMessageInnerHTML("已成功订阅/topic/subscribeTest");
        var returnData = JSON.parse(response.body);
        setMessageInnerHTML("/topic/subscribeTest 你接收到的消息为:" + returnData.responseMessage);
    });
}

//订阅消息
function subscribe2() {
    stompClient.subscribe('/topic/sendTest', function (response) {
        setMessageInnerHTML("已成功订阅/topic/sendTest");
        var returnData = JSON.parse(response.body);
        setMessageInnerHTML("/topic/sendTest 你接收到的消息为:" + returnData.responseMessage);
    });
}

//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
    document.getElementById('message').innerHTML += innerHTML + '
'
; }
script> html>

你可能感兴趣的:(java,web,java,websocket,spring,网络,ajax)