STOMP over WebSockets:Spring Boot expects JSON; NodeJs STOMP.js 客户端连接失败

STOMP over WebSockets: Spring Boot expects JSON; NodeJs STOMP.js client fails to connect

在通过 WebSockets 尝试 STOMP 时,我注意到不同实现之间存在不一致,即 Spring Boot Java 实现和使用 STOMP.js.[=33= 编写的 NodeJs 客户端之间的不一致]

调试它时,不同之处在于在 Spring 启动应用程序中,CONNECT 消息应该是一个 JSON 数组。例如,这条消息是由他们的测试客户端发送的(使用 SocksJS 库在 Java 脚本中编写):

["CONNECT\naccept-version:1.1,1.0\nheart-beat:10000,10000\n\n\u0000"]

相比之下,我的 NodeJs STOMP.js 测试客户端(代码如下)发送以下帧:

CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000

^@

不幸的是,我对 STOMP 没有经验,但在阅读 through the specification 之后,我不明白为什么 Spring Boot 期望数据表示为 JSON 数组。这是一个已知问题吗?


为了演示,让我分享两个示例运行。一次成功 运行 连接到 RabbitMQ,随后尝试连接 Java Spring 启动应用程序失败。 (可以在最后找到带有代码的可重现设置。)

  1. 连接到 RabbitMQ 实例,该实例配置为通过 WebSockets 使用 STOMP(ws://localhost:15674/ws 上的 运行):
$ node client.js 
Opening Web Socket...
Web Socket Opened...
>>> CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000


Received data
<<< CONNECTED
server:RabbitMQ/3.8.8
session:session-WkKD6rN5BNc_ObKpziikYA
heart-beat:4000,4000
version:1.2



connected to server RabbitMQ/3.8.8
send PING every 4000ms
check PONG every 4000ms
onConnect called
<<< PONG
Received data
<<< 

<<< PONG
>>> PING
Received data
<<< 
  1. 现在(未成功)连接 Spring 引导应用程序 (ws://localhost:5555/chat/123/k2qn3dl7/websocket):
node client.js 
Opening Web Socket...
Web Socket Opened...
>>> CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000


Received data
<<< o
Received data
<<< c[1007,""]
Connection closed to ws://localhost:5555/chat/123/k2qn3dl7/websocket
STOMP: scheduling reconnection in 5000ms
Opening Web Socket...
Web Socket Opened...
>>> CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000


Received data
<<< o
^C

失败的原因是 Jackson(JSON 解析器)未能解析该负载:

CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000

^@

如前所述,在 Spring 引导示例附带的客户端中,有效负载如下所示:

["CONNECT\naccept-version:1.1,1.0\nheart-beat:10000,10000\n\n\u0000"]

这是 Spring 引导应用程序中的完整错误:

2021-07-22 13:58:59.546  INFO 74313 --- [nio-5555-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2021-07-22 13:58:59.594 ERROR 74313 --- [nio-5555-exec-1] s.w.s.s.t.s.WebSocketServerSockJsSession : Broken data received. Terminating WebSocket connection abruptly

com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'CONNECT': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (String)"CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000

"; line: 1, column: 8]
    at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2337) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:720) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._reportInvalidToken(ReaderBasedJsonParser.java:2903) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._handleOddValue(ReaderBasedJsonParser.java:1949) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.core.json.ReaderBasedJsonParser.nextToken(ReaderBasedJsonParser.java:781) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4684) ~[jackson-databind-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4586) ~[jackson-databind-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548) ~[jackson-databind-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516) ~[jackson-databind-2.12.3.jar:2.12.3]
    at org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec.decode(Jackson2SockJsMessageCodec.java:64) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession.handleMessage(WebSocketServerSockJsSession.java:187) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler.handleTextMessage(SockJsWebSocketHandler.java:93) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.handler.AbstractWebSocketHandler.handleMessage(AbstractWebSocketHandler.java:43) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:114) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access[=19=]0(StandardWebSocketHandlerAdapter.java:43) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.onMessage(StandardWebSocketHandlerAdapter.java:85) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.onMessage(StandardWebSocketHandlerAdapter.java:82) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:415) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.java:129) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:515) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:301) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:85) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:183) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:162) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:156) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:60) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]

2021-07-22 13:59:04.610 ERROR 74313 --- [nio-5555-exec-2] s.w.s.s.t.s.WebSocketServerSockJsSession : Broken data received. Terminating WebSocket connection abruptly

复制路径:

  1. NodeJs 客户端代码
  2. Spring 启动测试应用程序
  3. RabbitMQ 测试实例

  1. 用 NodeJs 编写的客户端代码:
// Required dependencies:
// "@stomp/stompjs": "6.1.0"
// "websocket": "1.0.34"

// Polyfills. For details see:
// https://stomp-js.github.io/guide/stompjs/rx-stomp/ng2-stompjs/pollyfils-for-stompjs-v5.html
Object.assign(global, { WebSocket: require('websocket').w3cwebsocket });

const StompJs = require('@stomp/stompjs');

const client = new StompJs.Client({
        //brokerURL: 'ws://localhost:15674/ws', // RabbitMQ (should work)
        brokerURL: 'ws://localhost:5555/chat/123/k2qn3dl7/websocket', // Spring app (should fail)
        reconnectDelay: 5000,
        heartbeatIncoming: 4000,
        heartbeatOutgoing: 4000,
        logRawCommunication: true,
        debug: (x) => console.log(x),
});

client.onConnect = function (frame) {
  console.log('onConnect called');
};

client.activate();

  1. 可以找到 Spring 启动应用 here。我在端口 5555 上启动它:
git clone git@github.com:eugenp/tutorials.git
cd tutorials/spring-websockets
SERVER_PORT=5555 mvn spring-boot:run

注意:如果您随后转到 http://localhost:5555,您将看到 Spring 启动应用程序提供的聊天应用程序。当您点击连接时,将建立 STOMP 连接。


  1. 要启动 RabbitMQ,您可以使用 STOMP.js 中用于测试的 Docker container:
git clone git@github.com:stomp-js/stompjs.git
cd stompjs
sudo docker build -t myrabbitmq rabbitmq/
sudo docker run --rm -p 15674:15674 myrabbitmq

简而言之:JSON 消息不是“STOMP over native WebSockets”,而是“STOMP over SocksJS”。额外的 JSON 层由 SocksJS 协议引入,用于 Spring Boot 示例应用程序。


这是一个较长的故事。事实证明,我的端点是错误的。而不是

'ws://localhost:5555/chat/123/k2qn3dl7/websocket'

应该是

'ws://localhost:5555/chat'

它有错误的 URI,因为我正在复制我在浏览器中看到的输出。相反,我应该看看 at the configuration:

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

现在是令人困惑的部分。从配置中可以看出,Spring Boot 应用程序使用 SocksJS 定义了回退。

如果删除回退,令人困惑的错误消息就会消失。然而,当回退处于活动状态时,Spring 将尝试将请求作为 SocksJS 处理。这就是它尝试将 STOMP 帧解析为 JSON 的原因,这会导致误导性错误消息。

此外,我对JavaScript client used in the Spring Boot example感到困惑:

function connect() {
  var socket = new SockJS('/chat');
  stompClient = Stomp.over(socket);

  stompClient.connect({}, function(frame) {
    setConnected(true);
    console.log('Connected: ' + frame);
    stompClient.subscribe('/topic/messages', function(messageOutput) {
      showMessageOutput(JSON.parse(messageOutput.body));
    });
  });
}

它不是通过本机 WebSocket 连接的,而是通过 SocksJS 连接的。这就解释了为什么 Firefox 显示 JSON 请求,而不是预期的 STOMP 帧。