大纲
在《Websocket在Java中的实践——SockJS连接服务端》中,我们介绍了如何使用SockJS和Websocket通信。本文我们将介绍如何使用StompJS和Websocket服务端通信。
STOMP(Simple Text Orientated Messaging Protocol)介绍
STOMP,即简单面向文本的消息协议,是一种为处理在消息中间件上传输的文本消息而设计的简单协议。它提供了一种类似于HTTP或SMTP的文本帧格式,允许客户端与消息中间件进行交互,如发布、订阅和处理消息。STOMP的设计初衷是提供一个简单、可互操作的协议,以便在多种不同的消息中间件产品之间实现通信。
主要特点
- 简单性:STOMP使用简单的文本命令和响应格式,易于理解和实现。这使得开发人员可以轻松地构建基于STOMP的客户端和服务器应用程序。
- 可互操作性:由于STOMP是标准化的协议,因此它可以在不同的消息中间件产品之间实现通信。这意味着开发人员可以选择最适合其需求的消息中间件,而无需担心与现有系统不兼容的问题。
- 灵活性:STOMP支持多种消息类型(如文本、二进制、JSON等),并且允许在消息中添加自定义头部字段。这使得开发人员可以根据需要灵活地处理消息。
- 可靠性:STOMP提供了可靠的消息传递机制,确保消息能够准确地从发送者传递到接收者。此外,它还支持持久化消息和事务处理等功能,以满足对可靠性的高要求。
- 可扩展性:STOMP是一种可扩展的协议,可以根据需要进行扩展以满足特定需求。例如,可以添加新的命令、头部字段或消息类型来支持特定的应用场景。
样例
STOMP是一种基于“帧”的协议。其内容样例如下:
COMMAND
header1:value1
header2:value2Body^@
应用场景
STOMP广泛应用于各种需要可靠消息传递的场景,如企业集成、物联网、实时数据流处理等。它适用于各种编程语言和框架,并且已经有许多现成的客户端库和工具可供开发人员使用。通过使用STOMP,开发人员可以轻松地构建高性能、可伸缩的消息传递系统,以满足各种复杂的应用需求。
STOMP客户端与Websocket的通信过程
握手
Websocket服务需要提供一个地址供STOMP客户端与其握手。
在我们的案例中,STOMP客户端会发出帧的内容如下:
CONNECT
accept-version:1.2,1.1,1.0
heart-beat:4000,4000
如果连接建立成功过,则会收到如下帧:
CONNECTED
heart-beat:0,0
version:1.2
content-length:0
^@
订阅
STOMP客户端发送下面帧请求订阅服务端消息
SUBSCRIBE
id:sub-0
destination:/receive/msg-to-user
^@
发送
STOMP客户端发送下面帧向服务端发送消息
SEND
destination:/send/msg-from-user
content-length:22
{“content”:“messages”}^@
接收消息
接收到消息的帧内容如下:
MESSAGE
content-length:22
message-id:33cbe0ce-39e3-12da-b711-9d4351b01dcc-1
subscription:sub-0
content-type:text/plain;charset=UTF-8
destination:/receive/msg-to-user
content-length:22
{“content”:“messages”}^@
服务端
提供handshake地址
src/main/java/com/nyctlc/stomp/config/WebSocketConfig.java
package com.nyctlc.stomp.config;
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;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/handshake");
}
创建代理
在这个框架中,Spring程序会充当STOMP客户端的代理(Broker)。所以上面代码使用了EnableWebSocketMessageBroker注解来开启代理。
EnableWebSocketMessageBroker的作用
- 支持WebSocket通信:在传统的HTTP通信中,客户端向服务器发送请求,服务器响应请求后关闭连接。而在WebSocket通信中,客户端和服务器之间的连接始终保持打开状态,允许双方随时互相发送消息,实现实时通信。
EnableWebSocketMessageBroker
注解为Spring Boot应用程序提供了对WebSocket的支持,使得应用程序能够支持WebSocket通信。 - 配置消息代理:该注解通过配置消息代理来支持WebSocket通信。在使用
EnableWebSocketMessageBroker
注解之前,需要先定义一个WebSocket配置类,并通过@Configuration
注解标记该类为配置类。在该配置类中,EnableWebSocketMessageBroker
注解会自动配置一个WebSocketMessageBrokerConfigurer
实例,并将其注册到Spring应用程序上下文中。WebSocketMessageBrokerConfigurer
是Spring框架中用于配置WebSocket消息代理的接口。 - 设置消息代理参数:通过实现
WebSocketMessageBrokerConfigurer
接口,可以配置WebSocket消息代理的相关参数,如消息代理的地址、消息类型、消息发送和接收的线程池等。 - 定义消息处理方法:在启用了WebSocket消息代理之后,可以使用
@MessageMapping
注解来定义WebSocket消息的处理方法。@MessageMapping
注解用于指定WebSocket请求的地址,当客户端向该地址发送请求时,会自动调用对应的处理方法进行处理。
配置发布-订阅端口
setApplicationDestinationPrefixes方法用于配置发布端口。它允许你定义这些目的地的前缀,使得服务器能够识别并正确地将消息路由到相应的处理程序(Handler)。这样我们就可以在后续使用MessageMapping定义多个接受消息的端点。本例中我们的消息发布端点是/send/msg-from-user。
enableSimpleBroker表示启用一个内置的内存级消息代理,从而不需要外置的诸如Rabbitmq之类的其他中间件。给它传递的"/receive"是STOMP订阅端点的前缀。本例中我们的消息订阅端点是/receive/msg-to-user。
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/send");
registry.enableSimpleBroker("/receive");
}
}
配置消息发布端点逻辑
src/main/java/com/nyctlc/stomp/controller/WebSocketController.java
下面代码融合服务端接受消息以及服务端发送消息的两个逻辑:
- @MessageMapping(“/msg-from-user”)注解表示/send/msg-from-user端点过来的消息交由该函数处理。
- @SendTo(“/receive/msg-to-user”)表示这个函数的返回值发送给"/receive/msg-to-user",进而让客户端可以订阅到消息。
package com.nyctlc.stomp.controller;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import jakarta.websocket.server.PathParam;
@Controller
public class WebSocketController {
@MessageMapping("/msg-from-user")
@SendTo("/receive/msg-to-user")
public String handle(String msg) {
System.out.println("Received message: " + msg);
return msg;
}
}
测试
测试页面
我们使用了stompjs来建立连接,发送消息。
需要注意的是,如果连接建立使用的是SockJS的话,handshake接口要做相应改动。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>STOMP over WebSocket Example with StompJs.Client</title>
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs"></script>
</head>
<body>
<h2>STOMP over WebSocket Example with StompJs.Client</h2>
<button id="connectButton">Connect</button>
<form id="messageForm">
<input type="text" id="messageInput" placeholder="Type a message..."/>
<button type="submit">Send</button>
</form>
<div id="messages"></div>
<script>
var client = null;
function connect() {
client = new StompJs.Client({
brokerURL: 'ws://localhost:8080/handshake', // WebSocket服务端点
connectHeaders: {},
debug: function (str) {
console.log(str);
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
client.onConnect = function(frame) {
console.log('Connected: ' + frame);
client.subscribe('/receive/msg-to-user', function(message) { // 订阅端点
showMessageOutput(JSON.parse(message.body).content);
});
};
client.onStompError = function(frame) {
console.error('Broker reported error: ' + frame.headers['message']);
console.error('Additional details: ' + frame.body);
};
client.activate();
}
function sendMessage(event) {
event.preventDefault(); // 阻止表单默认提交行为
var messageContent = document.getElementById('messageInput').value.trim();
if(messageContent && client && client.connected) {
var chatMessage = { content: messageContent };
client.publish({destination: "/send/msg-from-user", body: JSON.stringify(chatMessage)}); // 发送端点
document.getElementById('messageInput').value = '';
}
}
function showMessageOutput(message) {
var messagesDiv = document.getElementById('messages');
var messageElement = document.createElement('div');
messageElement.appendChild(document.createTextNode(message));
messagesDiv.appendChild(messageElement);
}
document.getElementById('messageForm').addEventListener('submit', sendMessage);
document.getElementById('connectButton').addEventListener('click', connect);
</script>
</body>
</html>
配置Controller
这个主要是为了让上面页面可以通过URL访问。
src/main/java/com/nyctlc/stomp/controller/FileController.java
package com.nyctlc.stomp.controller;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
@Controller
public class FileController {
@GetMapping("/")
public String index() {
return "index"; // 返回index.html
}
@RequestMapping(value = "/favicon.ico")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void favicon() {
// No operation. Just to avoid 404 error for favicon.ico
}
}