当前位置: 代码网 > it编程>编程语言>Java > 深入浅出SpringBoot WebSocket构建实时应用全面指南

深入浅出SpringBoot WebSocket构建实时应用全面指南

2025年07月30日 Java 我要评论
前言为什么需要 websocket在传统的 web 应用中,通信模式主要是 http 请求-响应。客户端(通常是浏览器)发起一个请求,服务器处理后返回一个响应,然后连接关闭。这种模式对于获取网页内容、

前言

为什么需要 websocket

在传统的 web 应用中,通信模式主要是 http 请求-响应。客户端(通常是浏览器)发起一个请求,服务器处理后返回一个响应,然后连接关闭。这种模式对于获取网页内容、提交表单等操作非常有效。

然而,随着 web 应用的复杂化,我们越来越多地需要实时、双向、持续的通信能力。例如:

  • 在线聊天室: 用户发送消息,所有在线用户能立即看到。
  • 实时通知: 新邮件、好友请求、系统告警需要即时推送给用户。
  • 股票行情/数据仪表盘: 价格、状态需要秒级甚至毫秒级更新。
  • 在线游戏: 玩家状态、游戏事件需要实时同步。
  • 协作编辑: 多人同时编辑文档,彼此的修改需要实时可见。

如果使用传统的 http 轮询(polling)或长轮询(long polling)来实现这些功能,会带来巨大的服务器压力、延迟高、效率低下。websocket 协议的出现,正是为了解决这些问题。

websocket 是什么

websocket 是一种在单个 tcp 连接上进行全双工(full-duplex)通信的协议。它允许服务器主动向客户端推送数据,而无需客户端先发起请求。一旦建立连接,客户端和服务器就可以像打电话一样,随时向对方发送消息,实现真正的实时双向通信。

spring boot 如何简化 websocket 开发

spring boot 提供了强大的 spring-boot-starter-websocket 模块,它基于 spring framework 的 websocket 支持,极大地简化了在 spring 应用中集成 websocket 的过程。它不仅支持原始的 websocket api,还集成了 stomp(simple text oriented messaging protocol)协议,使得消息的发布/订阅、点对点通信、用户特定消息等复杂场景变得异常简单。

第一部分:准备工作

1.创建 spring boot 项目

使用 spring initializr (https://start.spring.io/) 创建一个新的项目。确保添加以下依赖:

pom.xml (maven) 相关依赖示例:

<dependencies>
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-web</artifactid>
    </dependency>
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-websocket</artifactid>
    </dependency>
    <!-- 可选:用于模板渲染 -->
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-thymeleaf</artifactid>
    </dependency>
    <!-- 可选:简化代码 -->
    <dependency>
        <groupid>org.projectlombok</groupid>
        <artifactid>lombok</artifactid>
        <scope>provided</scope>
    </dependency>
</dependencies>
  • spring web (spring-boot-starter-web)
  • spring websocket (spring-boot-starter-websocket)
  • (可选) thymeleaf (spring-boot-starter-thymeleaf) - 用于创建简单的 html 前端页面进行演示。
  • (可选) lombok - 简化 java 代码(如 @data, @allargsconstructor)。

2.项目结构

一个典型的结构可能如下:

src/
├── main/
│   ├── java/
│   │   └── com/example/websocketdemo/
│   │       ├── websocketconfig.java
│   │       ├── websocketcontroller.java
│   │       ├── model/
│   │       │   └── message.java
│   │       └── websocketdemoapplication.java
│   └── resources/
│       ├── static/
│       │   └── js/
│       │       └── app.js
│       └── templates/
│           └── index.html
└── test/
    └── ...

第二部分:配置 websocket (websocketconfig)

这是启用和配置 websocket 功能的核心步骤。我们需要创建一个配置类。

package com.example.websocketdemo;

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;

/**
 * websocket 配置类
 * @enablewebsocketmessagebroker 注解启用 stomp 消息代理功能。
 */
@configuration
@enablewebsocketmessagebroker
public class websocketconfig implements websocketmessagebrokerconfigurer {

    /**
     * 配置消息代理(message broker)
     * 消息代理负责处理来自客户端的消息,并将消息广播给订阅了特定目的地的客户端。
     *
     * @param config messagebrokerregistry
     */
    @override
    public void configuremessagebroker(messagebrokerregistry config) {
        // 1. 启用一个简单的内存消息代理,用于处理以 "/topic" 或 "/queue" 开头的消息。
        //    - "/topic" 通常用于**发布/订阅**模式(一对多广播)。
        //    - "/queue" 通常用于**点对点**模式(一对一,但多个订阅者时会负载均衡)。
        config.enablesimplebroker("/topic", "/queue");

        // 2. 定义应用目的地前缀。
        //    所有以 "/app" 开头的 stomp 消息都会被路由到带有 @messagemapping 注解的控制器方法中。
        //    例如:客户端发送到 "/app/hello" 的消息会被 @messagemapping("/hello") 的方法处理。
        config.setapplicationdestinationprefixes("/app");

        // (可选) 设置用户目的地前缀 (用于用户特定消息)
        // config.setuserdestinationprefix("/user");
    }

    /**
     * 注册 stomp 协议的 websocket 端点。
     * 客户端通过这些端点与服务器建立 websocket 连接。
     *
     * @param registry stompendpointregistry
     */
    @override
    public void registerstompendpoints(stompendpointregistry registry) {
        // 1. 注册一个名为 "/ws" 的端点。
        //    客户端将连接到 "ws://<server>:<port>/ws" (http) 或 "wss://<server>:<port>/ws" (https)。
        registry.addendpoint("/ws")

                // 2. 启用 sockjs 作为后备机制。
                //    sockjs 是一个 javascript 库,它在浏览器不支持原生 websocket 时,
                //    会尝试使用其他技术(如轮询)来模拟 websocket 行为,提高兼容性。
                //    客户端连接时,如果使用 sockjs,url 会是 "/ws/sockjs/info" 等。
                .withsockjs();

        // (可选) 可以注册多个端点
        // registry.addendpoint("/another-endpoint").withsockjs();
    }
}

关键点解析:

@enablewebsocketmessagebroker: 这个注解是开启 spring websocket 支持的关键,它启用了 stomp 消息代理。

configuremessagebroker:

  • enablesimplebroker(...): 启用一个简单的内存消息代理。对于生产环境,你可能需要集成更强大的消息代理,如 rabbitmqredis(通过 @enablestompbrokerrelay 配置),以实现集群部署和消息持久化。
  • setapplicationdestinationprefixes(...): 定义了应用处理消息的前缀。/app 是约定俗成的前缀。

registerstompendpoints:

  • addendpoint("/ws"): 定义了 websocket 连接的实际路径。
  • .withsockjs(): 强烈建议启用,以确保在老旧浏览器或网络环境下的兼容性。

第三部分:定义消息模型 (message.java)

创建一个简单的 pojo 类来表示我们要发送和接收的消息。

package com.example.websocketdemo.model;

import lombok.data;
import lombok.allargsconstructor;

/**
 * 消息实体类
 */
@data
@allargsconstructor
public class message {
    private string content; // 消息内容
    private string sender;  // 发送者
    private long timestamp; // 时间戳

    // 无参构造函数(json 反序列化需要)
    public message() {}

    // (可选) 可以添加更多字段,如消息类型、接收者等
}

第四部分:创建 websocket 控制器 (websocketcontroller.java)

这个控制器负责处理来自客户端的消息(通过 @messagemapping)以及向客户端发送消息(通过 simpmessagingtemplate)。

package com.example.websocketdemo;

import com.example.websocketdemo.model.message;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.messaging.handler.annotation.messagemapping;
import org.springframework.messaging.handler.annotation.payload;
import org.springframework.messaging.handler.annotation.sendto;
import org.springframework.messaging.simp.simpmessagingtemplate;
import org.springframework.stereotype.controller;
import org.springframework.web.util.htmlutils;

import java.time.instant;

/**
 * websocket 消息处理控制器
 */
@controller // 使用 @controller 而不是 @restcontroller,因为通常不直接返回 http 响应
public class websocketcontroller {

    // simpmessagingtemplate 用于从服务器端任意位置向客户端发送消息
    @autowired
    private simpmessagingtemplate messagingtemplate;

    /**
     * 处理客户端发送到 "/app/hello" 的消息。
     * 此方法将处理消息,并将处理后的结果广播给所有订阅了 "/topic/greetings" 的客户端。
     *
     * @param message 客户端发送的原始消息 (message 对象)
     * @return 处理后的消息 (message 对象) - 这个返回值会被 @sendto 指定的目的地接收
     * @throws exception
     */
    @messagemapping("/hello") // 监听目的地 "/app/hello"
    @sendto("/topic/greetings") // 将方法返回值发送到 "/topic/greetings"
    public message greeting(@payload message message) throws exception {
        // 模拟一些处理延迟
        thread.sleep(1000);

        // 返回一个处理后的消息,包含原内容、发送者和当前时间戳
        return new message(
                "hello, " + htmlutils.htmlescape(message.getsender()) + "!",
                "server",
                instant.now().toepochmilli()
        );
    }

    /**
     * 处理客户端发送到 "/app/chat" 的消息。
     * 这个方法展示了如何使用 simpmessagingtemplate 进行更灵活的消息发送。
     * 它不会返回值给 @sendto,而是直接使用 messagingtemplate 发送消息。
     *
     * @param message 客户端发送的聊天消息
     */
    @messagemapping("/chat")
    public void handlechatmessage(@payload message message) {
        // 可以在这里进行消息验证、存储到数据库等操作
        // ...

        // 使用 simpmessagingtemplate 将消息广播给所有订阅了 "/topic/chat" 的客户端
        messagingtemplate.convertandsend("/topic/chat", message);

        // (示例) 向特定用户发送消息 (需要配置用户目的地前缀)
        // messagingtemplate.convertandsendtouser("username", "/queue/private", privatemessage);
    }

    /**
     * (可选) 示例:从服务器内部其他地方(如定时任务、服务)触发消息发送
     */
    // @scheduled(fixedrate = 5000)
    // public void sendservertime() {
    //     message timemessage = new message("server time: " + instant.now(), "system", instant.now().toepochmilli());
    //     messagingtemplate.convertandsend("/topic/greetings", timemessage);
    // }
}

关键点解析:

  • @controller: 标记为控制器。

@messagemapping("/hello"): 将方法映射到 stomp 消息的目的地 /app/hello。客户端发送到 /app/hello 的消息会触发此方法。

@payload: 明确指定参数是从消息体(payload)中提取并反序列化为 message 对象的。

@sendto("/topic/greetings"): 指定该方法的返回值应该发送到 /topic/greetings 这个目的地。所有订阅了此目的地的客户端都会收到此消息。

simpmessagingtemplate: 这是一个强大的工具,允许你在代码的任何地方(而不仅限于 @messagemapping 方法)发送消息。

  • convertandsend(destination, payload) 方法会将 payload 对象序列化(通常是 json)并发送到指定的 destination
  • convertandsendtouser(user, destination, payload) 用于向特定用户发送消息(需要配置用户目的地前缀和用户识别机制)。

第五部分:创建前端页面 (index.html)

使用 thymeleaf 创建一个简单的 html 页面来测试我们的 websocket 功能。

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="utf-8" />
    <title>spring boot websocket demo</title>
    <!-- 引入 sockjs 客户端库 (如果配置了 withsockjs) -->
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
    <!-- 引入 stomp 客户端库 -->
    <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@6.1.0/bundles/stomp.umd.min.js"></script>
    <!-- (可选) bootstrap 用于美化 -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="external nofollow" 
      rel="stylesheet"
    />
  </head>
  <body>
    <div class="container mt-5">
      <h1>websocket chat & greeting demo</h1>

      <div class="row">
        <div class="col-md-6">
          <h3>send greeting</h3>
          <form id="greetingform">
            <div class="mb-3">
              <label for="greetingsender" class="form-label">your name:</label>
              <input
                type="text"
                class="form-control"
                id="greetingsender"
                placeholder="enter your name"
                required
              />
            </div>
            <button type="submit" class="btn btn-primary">send greeting</button>
          </form>
        </div>

        <div class="col-md-6">
          <h3>chat room</h3>
          <form id="chatform">
            <div class="mb-3">
              <label for="chatsender" class="form-label">nickname:</label>
              <input
                type="text"
                class="form-control"
                id="chatsender"
                placeholder="enter nickname"
                required
              />
            </div>
            <div class="mb-3">
              <label for="chatmessage" class="form-label">message:</label>
              <textarea
                class="form-control"
                id="chatmessage"
                rows="3"
                placeholder="type your message..."
                required
              ></textarea>
            </div>
            <button type="submit" class="btn btn-success">send message</button>
          </form>
        </div>
      </div>

      <div class="row mt-4">
        <div class="col-md-6">
          <h3>greetings received</h3>
          <ul id="greetinglist" class="list-group"></ul>
        </div>
        <div class="col-md-6">
          <h3>chat messages</h3>
          <ul id="chatlist" class="list-group"></ul>
        </div>
      </div>
    </div>

    <!-- 引入自定义 javascript -->
    <script th:src="@{/js/app.js}"></script>
  </body>
</html>

第六部分:编写前端 javascript (app.js)

这是前端与 websocket 交互的核心逻辑。

// 定义全局变量
let stompclient = null
let connected = false

// 页面加载完成后执行
document.addeventlistener("domcontentloaded", function () {
  connect()
})

// 连接到 websocket 服务器
function connect() {
  // 1. 创建 sockjs 实例,连接到后端配置的端点 "/ws"
  //    如果后端没有配置 withsockjs,则使用 new websocket("ws://localhost:8080/ws");
  const socket = new sockjs("/ws") // 注意:路径是相对于当前页面的,这里假设在根路径

  // 2. 使用 sockjs 实例创建 stomp 客户端
  stompclient = stomp.over(socket)

  // 3. 连接到 stomp 代理
  stompclient.connect(
    {},
    function (frame) {
      console.log("connected: " + frame)
      connected = true
      // 更新 ui 状态 (可选)
      // document.getelementbyid('connectionstatus').innerhtml = 'connected';

      // 4. 订阅目的地 "/topic/greetings"
      //    当服务器向 "/topic/greetings" 发送消息时,ongreetingreceived 函数会被调用
      stompclient.subscribe("/topic/greetings", ongreetingreceived)

      // 5. 订阅目的地 "/topic/chat"
      stompclient.subscribe("/topic/chat", onchatmessagereceived)
    },
    function (error) {
      console.error("connection error: " + error)
      connected = false
      // 重连逻辑 (可选)
      // settimeout(function() { connect(); }, 5000);
    }
  )
}

// 处理从 "/topic/greetings" 接收到的消息
function ongreetingreceived(payload) {
  const message = json.parse(payload.body)
  const greetinglist = document.getelementbyid("greetinglist")
  const item = document.createelement("li")
  item.textcontent = `[${new date(message.timestamp).tolocaletimestring()}] ${
    message.sender
  }: ${message.content}`
  item.classname = "list-group-item list-group-item-info"
  greetinglist.appendchild(item)
  // 自动滚动到底部
  greetinglist.scrolltop = greetinglist.scrollheight
}

// 处理从 "/topic/chat" 接收到的消息
function onchatmessagereceived(payload) {
  const message = json.parse(payload.body)
  const chatlist = document.getelementbyid("chatlist")
  const item = document.createelement("li")
  item.textcontent = `[${new date(message.timestamp).tolocaletimestring()}] ${
    message.sender
  }: ${message.content}`
  item.classname = "list-group-item"
  chatlist.appendchild(item)
  chatlist.scrolltop = chatlist.scrollheight
}

// 处理 "send greeting" 表单提交
document
  .getelementbyid("greetingform")
  .addeventlistener("submit", function (event) {
    event.preventdefault() // 阻止表单默认提交行为
    const senderinput = document.getelementbyid("greetingsender")
    const sender = senderinput.value.trim()
    if (sender && connected) {
      // 发送消息到目的地 "/app/hello"
      // 消息体是一个 json 字符串
      stompclient.send(
        "/app/hello",
        {},
        json.stringify({ sender: sender, content: "greeting request" })
      )
      senderinput.value = "" // 清空输入框
    } else if (!connected) {
      alert("websocket not connected!")
    }
  })

// 处理 "send message" 表单提交
document
  .getelementbyid("chatform")
  .addeventlistener("submit", function (event) {
    event.preventdefault()
    const senderinput = document.getelementbyid("chatsender")
    const messageinput = document.getelementbyid("chatmessage")
    const sender = senderinput.value.trim()
    const content = messageinput.value.trim()
    if (sender && content && connected) {
      // 发送消息到目的地 "/app/chat"
      const chatmessage = {
        sender: sender,
        content: content,
        timestamp: new date().gettime(), // 客户端时间戳,服务器会用自己的
      }
      stompclient.send("/app/chat", {}, json.stringify(chatmessage))
      // 清空输入框
      messageinput.value = ""
      // (可选) 立即将消息显示在本地聊天列表(回显),服务器广播后会再次收到
      // onchatmessagereceived({body: json.stringify(chatmessage)});
    } else if (!connected) {
      alert("websocket not connected!")
    }
  })

// (可选) 断开连接函数
function disconnect() {
  if (stompclient) {
    stompclient.disconnect()
    connected = false
    console.log("disconnected")
    // 更新 ui 状态
    // document.getelementbyid('connectionstatus').innerhtml = 'disconnected';
  }
}

// 页面卸载时断开连接
window.addeventlistener("beforeunload", function () {
  disconnect()
})

关键点解析:

  • sockjs('/ws'): 创建 sockjs 连接,路径必须与后端 websocketconfigaddendpoint("/ws") 一致。
  • stomp.over(socket): 使用 sockjs 连接创建 stomp 客户端。
  • stompclient.connect(headers, connectcallback, errorcallback): 连接到 stomp 代理。headers 通常为空对象 {}
  • stompclient.subscribe(destination, callback): 订阅一个目的地。callback 函数接收一个 payload 参数,其 body 属性是服务器发送的原始消息字符串(通常是 json)。
  • stompclient.send(destination, headers, body): 向指定目的地发送消息。body 是消息内容(字符串)。
  • json.parse(payload.body): 将接收到的 json 字符串解析成 javascript 对象。
  • json.stringify(object): 将 javascript 对象序列化成 json 字符串发送。

第七部分:运行与测试

启动应用: 运行 websocketdemoapplication.javamain 方法。

访问页面: 打开浏览器,访问 http://localhost:8080 (或你配置的端口和路径)。

观察控制台: 打开浏览器的开发者工具(f12),查看 network 和 console 标签页。你应该能看到 sockjs 或 websocket 连接建立成功 (connected 帧)。

测试功能:

  • 在 “send greeting” 区域输入名字并点击 “send greeting”。稍等 1 秒,你会在 “greetings received” 列表中看到服务器返回的 “hello, [你的名字]!” 消息。
  • 在 “chat room” 区域输入昵称和消息,点击 “send message”。消息会立即出现在 “chat messages” 列表中(因为服务器广播回所有客户端,包括发送者)。
  • 打开多个浏览器标签页或窗口访问同一个页面。在一个窗口发送消息,其他所有窗口都会实时收到更新!这就是 websocket 的魔力。

第八部分:高级主题与最佳实践

1.用户认证与授权 (security):

  • 通常需要将 websocket 连接与用户的登录会话关联。可以在 websocketconfigregisterstompendpoints 中添加拦截器,或者在 httpsessionhandshakeinterceptor 中将用户信息存入 websocketsession 的属性。
  • 使用 spring security 保护 /ws 端点,确保只有认证用户才能连接。
  • @messagemapping 方法上使用 @preauthorize 进行细粒度权限控制。
  • 使用 messagingtemplate.convertandsendtouser(username, destination, payload) 向特定用户发送私有消息。需要配置 setuserdestinationprefix("/user")

2.消息代理 (message broker):

simple broker: 适用于单机部署的简单应用。在集群环境下,不同实例间的客户端无法互相通信。

stomp broker relay (推荐生产环境): 配置 spring boot 应用连接到外部的、功能更强大的 stomp 消息代理(如 rabbitmq, activemq, redis)。

// 在 websocketconfig 中
@override
public void configuremessagebroker(messagebrokerregistry config) {
    // 配置应用目的地前缀
    config.setapplicationdestinationprefixes("/app");
    // 配置用户目的地前缀
    config.setuserdestinationprefix("/user");

    // 启用 stomp 代理中继,连接到外部的 broker
    config.enablestompbrokerrelay("/topic", "/queue")
          .setrelayhost("localhost") // 外部 broker 的主机
          .setrelayport(61613)      // stomp 端口 (rabbitmq 默认 61613)
          .setclientlogin("guest")  // broker 用户名
          .setclientpasscode("guest"); // broker 密码
}

优势: 支持集群、消息持久化、更复杂的消息路由、高可用性。

3.异常处理:

  • 可以使用 @controlleradvice@messageexceptionhandler 注解来处理 @messagemapping 方法中抛出的异常,并向客户端发送错误消息。
  • 处理连接断开 (websocketsession 关闭) 的逻辑。

4.性能与监控:

  • 监控连接数、消息吞吐量。
  • 考虑消息大小和频率,避免网络拥塞。
  • 对于高并发场景,优化线程池配置。

5.前端库选择:

  • @stomp/stompjs 是目前最流行和维护良好的 stomp 客户端库。
  • sockjs-client 是 sockjs 的官方库。

第九部分:总结

通过本文的详细步骤,我们成功地在 spring boot 应用中集成并实现了 websocket 功能。我们学习了:

  • 核心概念: websocket 协议、stomp、消息代理、发布/订阅模式。
  • 配置: 使用 @enablewebsocketmessagebrokerwebsocketmessagebrokerconfigurer 进行配置。
  • 后端开发: 使用 @messagemapping, @sendto, simpmessagingtemplate 处理消息和发送消息。
  • 前端开发: 使用 sockjs-client@stomp/stompjs 库建立连接、订阅、发送消息。
  • 高级主题: 安全、消息代理、异常处理。

spring boot 的 websocket 支持使得构建实时 web 应用变得相对简单和高效。掌握这些知识,你就可以为你的应用添加强大的实时交互能力了。

下一步:

  • 尝试集成 spring security 进行用户认证。
  • 将简单消息代理替换为 rabbitmq 或 redis。
  • 实现更复杂的聊天功能,如群组、在线状态、消息历史记录。
  • 探索 websocket 在游戏、协作工具等领域的应用。

以上就是深入浅出springboot websocket构建实时应用全面指南的详细内容,更多关于springboot websocket构建实时应用的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com