[WebSocket]之上层协议STOMP

参考教程:http://jmesnil.net/stomp-websocket/doc/

在使用 stomp.js 时,能找到的较完整的 API 说明基本都是英文,中文资料比较少,因此,参考上边的教程做了以下的笔记和总结

介绍

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 之上提供了一个基于帧的线路格式层,用来定义消息语义;

STOMP帧

STOMP帧由命令,一个或多个头信息、一个空行及负载(文本或字节)所组成;

其中可用的COMMAND 包括:

CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、BEGIN、COMMIT、ABORT、ACK、NACK、DISCONNECT;

例:
发送消息

SEND
destination:/queue/trade
content-type:application/json
content-length:44
{“action”:”BUY”,”ticker”:”MMM”,”shares”,44}^@

订阅消息

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@

服务器进行广播消息

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
{“ticker”:”MMM”,”price”:129.45}^@

客户端 API

引入stomp.js

  • 1

发起连接

客户端可以通过使用Stomp.js和sockjs-client连接

 
  1. // 建立连接对象(还未发起连接)

  2. var socket=new SockJS("/spring-websocket-portfolio/portfolio");

  3.  
  4. // 获取 STOMP 子协议的客户端对象

  5. var stompClient = Stomp.over(socket);

  6.  
  7. // 向服务器发起websocket连接并发送CONNECT帧

  8. stompClient.connect(

  9. {},

  10. function connectCallback (frame) {

  11. // 连接成功时(服务器响应 CONNECTED 帧)的回调方法

  12. document.getElementById("state-info").innerHTML = "连接成功";

  13. console.log('已连接【' + frame + '】');

  14. stompClient.subscribe('/topic/getResponse', function (response) {

  15. showResponse(response.body);

  16. });

  17. },

  18. function errorCallBack (error) {

  19. // 连接失败时(服务器响应 ERROR 帧)的回调方法

  20. document.getElementById("state-info").innerHTML = "连接失败";

  21. console.log('连接失败【' + error + '】');

  22. }

  23. );

说明:
1) socket连接对象也可通过WebSocket(不通过SockJS)连接

var socket=new WebSocket("/spring-websocket-portfolio/portfolio");

2) stompClient.connect()方法签名:

client.connect(headers, connectCallback, errorCallback);

其中
headers表示客户端的认证信息,如:

 
  1. var headers = {

  2. login: 'mylogin',

  3. passcode: 'mypasscode',

  4. // additional header

  5. 'client-id': 'my-client-id'

  6. };

若无需认证,直接使用空对象 “{}” 即可;

connectCallback 表示连接成功时(服务器响应 CONNECTED 帧)的回调方法;
errorCallback 表示连接失败时(服务器响应 ERROR 帧)的回调方法,非必须;

断开连接

若要从客户端主动断开连接,可调用 disconnect() 方法

 
  1. client.disconnect(function () {

  2. alert("See you next time!");

  3. };

该方法为异步进行,因此包含了回调参数,操作完成时自动回调;

心跳机制

若使用STOMP 1.1 版本,默认开启了心跳检测机制,可通过client对象的heartbeat field进行配置(默认值都是10000 ms):

 
  1. client.heartbeat.outgoing = 20000; // client will send heartbeats every 20000ms

  2. client.heartbeat.incoming = 0; // client does not want to receive heartbeats from the server

  3. // The heart-beating is using window.setInterval() to regularly send heart-beats and/or check server heart-beats

发送信息

连接成功后,客户端可使用 send() 方法向服务器发送信息:

client.send(destination url[, headers[, body]]);

其中
destination url 为服务器 controller中 @MessageMapping 中匹配的URL,字符串,必须参数;
headers 为发送信息的header,JavaScript 对象,可选参数;
body 为发送信息的 body,字符串,可选参数;

例:

 
  1. client.send("/queue/test", {priority: 9}, "Hello, STOMP");

  2. client.send("/queue/test", {}, "Hello, STOMP");

订阅、接收信息

STOMP 客户端要想接收来自服务器推送的消息,必须先订阅相应的URL,即发送一个 SUBSCRIBE 帧,然后才能不断接收来自服务器的推送消息;
订阅和接收消息通过 subscribe() 方法实现:

subscribe(destination url, callback[, headers])

其中
destination url 为服务器 @SendTo 匹配的 URL,字符串;
callback 为每次收到服务器推送的消息时的回调方法,该方法包含参数 message;
headers 为附加的headers,JavaScript 对象;什么作用?
该方法返回一个包含了id属性的 JavaScript 对象,可作为 unsubscribe() 方法的参数;

例:

 
  1. var headers = {ack: 'client', 'selector': "location = 'Europe'"};

  2. var callback = function(message) {

  3. if (message.body) {

  4. alert("got message with body " + message.body)

  5. } else {

  6. alert("got empty message");

  7. }

  8. });

  9. var subscription = client.subscribe("/queue/test", callback, headers);

取消订阅

 
  1. var subscription = client.subscribe(...);

  2.  
  3. subscription.unsubscribe();

JSON 支持

STOMP 帧的 body 必须是 string 类型,若希望接收/发送 json 对象,可通过 JSON.stringify() and JSON.parse() 实现;
例:

 
  1. var quote = {symbol: 'APPL', value: 195.46};

  2. client.send("/topic/stocks", {}, JSON.stringify(quote));

  3.  
  4. client.subcribe("/topic/stocks", function(message) {

  5. var quote = JSON.parse(message.body);

  6. alert(quote.symbol + " is at " + quote.value);

  7. });

事务支持

STOMP 客户端支持在发送消息时用事务进行处理:
举例说明:

 
  1. // start the transaction

  2. // 该方法返回一个包含了事务 id、commit()、abort() 的JavaScript 对象

  3. var tx = client.begin();

  4. // send the message in a transaction

  5. // 最关键的在于要在 headers 对象中加入事务 id,若没有添加,则会直接发送消息,不会以事务进行处理

  6. client.send("/queue/test", {transaction: tx.id}, "message in a transaction");

  7. // commit the transaction to effectively send the message

  8. tx.commit();

  9. // tx.abort();

Debug 信息

STOMP 客户端默认将传输过程中的所有 debug 信息以 console.log() 形式输出到客户端浏览器中,也可通过以下方式输出到 DOM 中:

 
  1. client.debug = function(str) {

  2. // str 参数即为 debug 信息

  3. // append the debug log to a #debug div somewhere in the page using JQuery:

  4. $("#debug").append(str + "\n");

  5. };

认证

这一部分内容看的不是很理解,因此直接将原文放在这里了,待补充。
By default, STOMP messages will be automatically acknowledged by the server before the message is delivered to the client.

The client can chose instead to handle message acknowledgement by subscribing to a destination and specify a ack header set to client or client-individual.

In that case, the client must use the message.ack() method to inform the server that it has acknowledge the message.

 
  1. var subscription = client.subscribe("/queue/test",

  2. function(message) {

  3. // do something with the message

  4. ...

  5. // and acknowledge it

  6. message.ack();

  7. },

  8. {ack: 'client'}

  9. );

The ack() method accepts a headers argument for additional headers to acknowledge the message. For example, it is possible to acknowledge a message as part of a transaction and ask for a receipt when the ACK STOMP frame has effectively be processed by the broker:
var tx = client.begin();
message.ack({ transaction: tx.id, receipt: ‘my-receipt’ });
tx.commit();
The nack() method can also be used to inform STOMP 1.1 brokers that the client did not consume the message. It takes the same arguments than the ack() method.

 

 
  1. 1、JavaScript 依赖

  2.  
  3. STOMP 依赖 sockjs.js 和 stomp.min.js。stomp.min.js的下载链接:http://www.bootcdn.cn/stomp.js/

  4.  
  5.  
  6. 2、JavaScript 客户端实现

  7.  
  8. [点击并拖拽以移动]

  9.  
  10. /*STOMP*/

  11. var url = 'http://localhost:8080/stomp';

  12. var sock = new SockJS(url);

  13. var stomp = Stomp.over(sock);

  14.  
  15. var strJson = JSON.stringify({'message': 'Marco!'});

  16.  
  17. //默认的和STOMP端点连接

  18. /*stomp.connect("guest", "guest", function (franme) {

  19.  
  20. });*/

  21.  
  22. var headers={

  23. username:'admin',

  24. password:'admin'

  25. };

  26.  
  27. stomp.connect(headers, function (frame) {

  28.  
  29. //发送消息

  30. //第二个参数是一个头信息的Map,它会包含在STOMP的帧中

  31. //事务支持

  32. var tx = stomp.begin();

  33. stomp.send("/app/marco", {transaction: tx.id}, strJson);

  34. tx.commit();

  35.  
  36.  
  37. //订阅服务端消息 subscribe(destination url, callback[, headers])

  38. stomp.subscribe("/topic/marco", function (message) {

  39. var content = message.body;

  40. var obj = JSON.parse(content);

  41. console.log("订阅的服务端消息:" + obj.message);

  42. }, {});

  43.  
  44.  
  45. stomp.subscribe("/app/getShout", function (message) {

  46. var content = message.body;

  47. var obj = JSON.parse(content);

  48. console.log("订阅的服务端直接返回的消息:" + obj.message);

  49. }, {});

  50.  
  51.  
  52. /*以下是针对特定用户的订阅*/

  53. var adminJSON = JSON.stringify({'message': 'ADMIN'});

  54. /*第一种*/

  55. stomp.send("/app/singleShout", {}, adminJSON);

  56. stomp.subscribe("/user/queue/shouts",function (message) {

  57. var content = message.body;

  58. var obj = JSON.parse(content);

  59. console.log("admin用户特定的消息1:" + obj.message);

  60. });

  61. /*第二种*/

  62. stomp.send("/app/shout", {}, adminJSON);

  63. stomp.subscribe("/user/queue/notifications",function (message) {

  64. var content = message.body;

  65. var obj = JSON.parse(content);

  66. console.log("admin用户特定的消息2:" + obj.message);

  67. });

  68.  
  69. /*订阅异常消息*/

  70. stomp.subscribe("/user/queue/errors", function (message) {

  71. console.log(message.body);

  72. });

  73.  
  74. //若使用STOMP 1.1 版本,默认开启了心跳检测机制(默认值都是10000ms)

  75. stomp.heartbeat.outgoing = 20000;

  76.  
  77. stomp.heartbeat.incoming = 0; //客户端不从服务端接收心跳包

  78. });

STOMP 服务端整理

一、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帧的最后是负载内容。

 

1、启用STOMP功能

    STOMP 的消息根据前缀的不同分为三种。如下,以 /app 开头的消息都会被路由到带有@MessageMapping 或 @SubscribeMapping 注解的方法中;以/topic 或 /queue 开头的消息都会发送到STOMP代理中,根据你所选择的STOMP代理不同,目的地的可选前缀也会有所限制;以/user开头的消息会将消息重路由到某个用户独有的目的地上。

[WebSocket]之上层协议STOMP_第1张图片

 View Code

2、处理来自客户端的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;
  }

 

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

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

[WebSocket]之上层协议STOMP_第2张图片

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

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

 

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

 

3、发送消息到客户端

3.1 在处理消息之后发送消息

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

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

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

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

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

 

  @Autowired
  private SimpMessageSendingOperations simpMessageSendingOperations;


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

 

3.3 为指定用户发送消息

    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 目的地上。

4、处理消息异常

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

 

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

 

原文转发自:https://blog.csdn.net/m0_37542889/article/details/83750665(如有侵权、烦请告知处理)

你可能感兴趣的:(Java,websocket)