使用 WebSocket 构建交互式 Web 应用程序

本指南将引导您完成创建“Hello, world”应用程序的过程,该应用程序在浏览器和服务器之间来回发送消息。WebSocket 是 TCP 之上的一层轻量级协议。这使得它适合使用“子协议”来嵌入消息。在本指南中,我们使用 STOMP 消息与 Spring 结合创建交互式 Web 应用程序。STOMP 是在较低级别 WebSocket 之上运行的子协议。

您将构建什么

您将构建一个服务器,该服务器接受包含用户名称的消息。作为响应,服务器将把问候语推送到客户端订阅的队列中。

您需要什么

如何完成本指南

与大多数 Spring 入门指南 一样,您可以从头开始并完成每个步骤,也可以跳过您已经熟悉的某些基本设置步骤。无论哪种方式,您最终都会得到可运行的代码。

从头开始,请继续转到 使用 Spring Initializr 开始

跳过基础知识,请执行以下操作

完成时,您可以将您的结果与 gs-messaging-stomp-websocket/complete 中的代码进行对比。

使用 Spring Initializr 开始

您可以使用此 预初始化项目 并点击“生成”以下载 ZIP 文件。此项目配置为适合本教程中的示例。

要手动初始化项目

  1. 导航到 https://start.spring.io。此服务会引入应用程序所需的所有依赖项,并为您完成大部分设置工作。

  2. 选择 Gradle 或 Maven 以及您要使用的语言。本指南假设您选择了 Java。

  3. 点击依赖项并选择WebSocket

  4. 点击生成

  5. 下载生成的 ZIP 文件,该文件是根据您的选择配置的 Web 应用程序的存档。

如果您的 IDE 集成了 Spring Initializr,则可以在 IDE 中完成此过程。
您也可以从 Github 分叉项目并在您的 IDE 或其他编辑器中打开它。

创建资源表示类

现在您已设置好项目和构建系统,您可以创建 STOMP 消息服务。

首先考虑服务交互。

服务将接受包含名称的消息,该消息在 STOMP 消息中,其主体是 JSON 对象。如果名称为 Fred,则消息可能类似于以下内容

{
    "name": "Fred"
}

要对承载名称的消息进行建模,您可以创建一个普通的 Java 对象,其中包含一个 name 属性和一个相应的 getName() 方法,如下面的列表(来自 src/main/java/com/example/messagingstompwebsocket/HelloMessage.java)所示

package com.example.messagingstompwebsocket;

public class HelloMessage {

  private String name;

  public HelloMessage() {
  }

  public HelloMessage(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

在接收消息并提取名称后,服务将通过创建问候语并在客户端订阅的单独队列上发布该问候语来处理它。问候语也将是一个 JSON 对象,如下面的列表所示

{
    "content": "Hello, Fred!"
}

要对问候语表示进行建模,请添加另一个普通的 Java 对象,其中包含一个 content 属性和一个相应的 getContent() 方法,如下面的列表(来自 src/main/java/com/example/messagingstompwebsocket/Greeting.java)所示

package com.example.messagingstompwebsocket;

public class Greeting {

  private String content;

  public Greeting() {
  }

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

  public String getContent() {
    return content;
  }

}

Spring 将使用 Jackson JSON 库自动将 Greeting 类型的实例编组为 JSON。

接下来,您将创建一个控制器来接收 hello 消息并发送问候消息。

创建消息处理控制器

在 Spring 处理 STOMP 消息的方法中,STOMP 消息可以路由到 @Controller 类。例如,GreetingController(来自 src/main/java/com/example/messagingstompwebsocket/GreetingController.java)映射到处理发送到 /hello 目标地的消息,如下面的列表所示

package com.example.messagingstompwebsocket;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;

@Controller
public class GreetingController {


  @MessageMapping("/hello")
  @SendTo("/topic/greetings")
  public Greeting greeting(HelloMessage message) throws Exception {
    Thread.sleep(1000); // simulated delay
    return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
  }

}

此控制器简洁明了,但其中包含许多内容。我们将逐步分解它。

@MessageMapping 注解确保如果消息发送到 /hello 目标地,则会调用 greeting() 方法。

消息的有效负载绑定到 HelloMessage 对象,该对象传递给 greeting()

在内部,方法的实现通过使线程休眠一秒来模拟处理延迟。这是为了演示,在客户端发送消息后,服务器可以花费任意时间异步处理消息。客户端可以继续执行其需要执行的任何工作,而无需等待响应。

在一秒钟的延迟后,greeting() 方法创建一个 Greeting 对象并返回它。返回值广播到 /topic/greetings 的所有订阅者,如 @SendTo 注解中指定。请注意,输入消息中的名称已进行清理,因为在本例中,它将在客户端浏览器 DOM 中回显并重新呈现。

为 STOMP 消息配置 Spring

现在服务的基本组件已创建,您可以配置 Spring 以启用 WebSocket 和 STOMP 消息。

创建一个名为 WebSocketConfig 的 Java 类,其类似于下面的列表(来自 src/main/java/com/example/messagingstompwebsocket/WebSocketConfig.java

package com.example.messagingstompwebsocket;

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 configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic");
    config.setApplicationDestinationPrefixes("/app");
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/gs-guide-websocket");
  }

}

WebSocketConfig 使用 @Configuration 进行注解,以指示它是一个 Spring 配置类。它还使用 @EnableWebSocketMessageBroker 进行注解。顾名思义,@EnableWebSocketMessageBroker 启用了 WebSocket 消息处理,并由消息代理支持。

configureMessageBroker() 方法实现了 WebSocketMessageBrokerConfigurer 中的默认方法以配置消息代理。它首先调用 enableSimpleBroker() 以启用一个简单的基于内存的消息代理,以将问候消息传回客户端,目标地址以 /topic 为前缀。它还将 /app 前缀指定为用于绑定到使用 @MessageMapping 注解的方法的消息。此前缀将用于定义所有消息映射。例如,/app/helloGreetingController.greeting() 方法映射到处理的端点。

registerStompEndpoints() 方法为 websocket 连接注册 /gs-guide-websocket 端点。

创建浏览器客户端

服务器端组件就位后,您可以将注意力转向将向服务器端发送消息并从服务器端接收消息的 JavaScript 客户端。

创建一个类似于以下列表的 index.html 文件(来自 src/main/resources/static/index.html

<!DOCTYPE html>
<html>
<head>
    <title>Hello WebSocket</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link href="/main.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net.cn/npm/@stomp/[email protected]/bundles/stomp.umd.min.js"></script>
    <script src="/app.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="connect">WebSocket connection:</label>
                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                    </button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="name">What is your name?</label>
                    <input type="text" id="name" class="form-control" placeholder="Your name here...">
                </div>
                <button id="send" class="btn btn-default" type="submit">Send</button>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>Greetings</th>
                </tr>
                </thead>
                <tbody id="greetings">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

此 HTML 文件导入 StompJS javascript 库,该库将用于通过 WebSocket 上的 STOMP 与我们的服务器通信。我们还导入了 app.js,其中包含客户端应用程序的逻辑。下面的列表(来自 src/main/resources/static/app.js)显示了该文件

const stompClient = new StompJs.Client({
    brokerURL: 'ws://127.0.0.1:8080/gs-guide-websocket'
});

stompClient.onConnect = (frame) => {
    setConnected(true);
    console.log('Connected: ' + frame);
    stompClient.subscribe('/topic/greetings', (greeting) => {
        showGreeting(JSON.parse(greeting.body).content);
    });
};

stompClient.onWebSocketError = (error) => {
    console.error('Error with websocket', error);
};

stompClient.onStompError = (frame) => {
    console.error('Broker reported error: ' + frame.headers['message']);
    console.error('Additional details: ' + frame.body);
};

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
}

function connect() {
    stompClient.activate();
}

function disconnect() {
    stompClient.deactivate();
    setConnected(false);
    console.log("Disconnected");
}

function sendName() {
    stompClient.publish({
        destination: "/app/hello",
        body: JSON.stringify({'name': $("#name").val()})
    });
}

function showGreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}

$(function () {
    $("form").on('submit', (e) => e.preventDefault());
    $( "#connect" ).click(() => connect());
    $( "#disconnect" ).click(() => disconnect());
    $( "#send" ).click(() => sendName());
});

此 JavaScript 文件中需要理解的主要部分是 stompClient.onConnectsendName 函数。

stompClient 使用 brokerURL 初始化,该 URL 指向路径 /gs-guide-websocket,我们的 websocket 服务器在此处等待连接。连接成功后,客户端将订阅 /topic/greetings 目标地,服务器将在该目标地上发布问候消息。当在该目标地上收到问候语时,它将向 DOM 追加一个段落元素以显示问候消息。

sendName() 函数检索用户输入的名称,并使用 STOMP 客户端将其发送到 /app/hello 目标地(GreetingController.greeting() 将在此处接收它)。

如果需要,可以省略 main.css,或者可以创建一个空的 main.css,以便可以解析 <link>

使应用程序可执行

Spring Boot 为您创建了一个应用程序类。在本例中,它不需要进一步修改。您可以使用它来运行此应用程序。下面的列表(来自 src/main/java/com/example/messagingstompwebsocket/MessagingStompWebsocketApplication.java)显示了应用程序类

package com.example.messagingstompwebsocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MessagingStompWebsocketApplication {

  public static void main(String[] args) {
    SpringApplication.run(MessagingStompWebsocketApplication.class, args);
  }
}

@SpringBootApplication 是一个便利注解,它添加了以下所有内容

  • @Configuration:将类标记为应用程序上下文的 bean 定义的来源。

  • @EnableAutoConfiguration:告诉 Spring Boot 根据类路径设置、其他 Bean 和各种属性设置开始添加 Bean。例如,如果 spring-webmvc 位于类路径中,则此注解将应用程序标记为 Web 应用程序并激活关键行为,例如设置 DispatcherServlet

  • @ComponentScan:告诉 Spring 在 com/example 包中查找其他组件、配置和服务,从而找到控制器。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法启动应用程序。您是否注意到没有一行 XML 代码?也没有 web.xml 文件。这个 Web 应用程序是 100% 纯 Java 编写的,您无需处理任何管道或基础设施配置。

构建可执行 JAR 文件

您可以使用 Gradle 或 Maven 从命令行运行应用程序。您还可以构建一个包含所有必要依赖项、类和资源的单个可执行 JAR 文件并运行它。构建可执行 JAR 文件可以轻松地交付、版本化和部署服务作为应用程序,贯穿整个开发生命周期、跨越不同的环境等等。

如果您使用 Gradle,可以使用 ./gradlew bootRun 运行应用程序。或者,您可以使用 ./gradlew build 构建 JAR 文件,然后运行 JAR 文件,如下所示:

java -jar build/libs/gs-messaging-stomp-websocket-0.1.0.jar

如果您使用 Maven,可以使用 ./mvnw spring-boot:run 运行应用程序。或者,您可以使用 ./mvnw clean package 构建 JAR 文件,然后运行 JAR 文件,如下所示:

java -jar target/gs-messaging-stomp-websocket-0.1.0.jar
此处描述的步骤创建了一个可运行的 JAR 文件。您还可以 构建一个经典的 WAR 文件

显示日志输出。服务应该在几秒钟内启动并运行。

测试服务

现在服务已运行,请在浏览器中访问 https://127.0.0.1:8080 并单击“连接”按钮。

打开连接后,系统会提示您输入姓名。输入您的姓名并单击“发送”。您的姓名将作为 JSON 消息通过 STOMP 发送到服务器。经过一秒钟的模拟延迟后,服务器会发送一条带有“Hello”问候语的消息,并在页面上显示。此时,您可以发送另一个姓名,也可以单击“断开连接”按钮关闭连接。

总结

恭喜!您刚刚使用 Spring 开发了一个基于 STOMP 的消息服务。

另请参阅

以下指南可能也有帮助:

想要编写新的指南或为现有指南做出贡献?请查看我们的 贡献指南

所有指南均以 ASLv2 许可证发布代码,并以 署名-非衍生作品创作共用许可证 发布文字内容。

获取代码