前言
为什么需要 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(...): 启用一个简单的内存消息代理。对于生产环境,你可能需要集成更强大的消息代理,如 rabbitmq 或 redis(通过@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 连接,路径必须与后端websocketconfig中addendpoint("/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.java 的 main 方法。
访问页面: 打开浏览器,访问 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 连接与用户的登录会话关联。可以在 
websocketconfig的registerstompendpoints中添加拦截器,或者在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、消息代理、发布/订阅模式。
 - 配置: 使用 
@enablewebsocketmessagebroker和websocketmessagebrokerconfigurer进行配置。 - 后端开发: 使用 
@messagemapping,@sendto,simpmessagingtemplate处理消息和发送消息。 - 前端开发: 使用 
sockjs-client和@stomp/stompjs库建立连接、订阅、发送消息。 - 高级主题: 安全、消息代理、异常处理。
 
spring boot 的 websocket 支持使得构建实时 web 应用变得相对简单和高效。掌握这些知识,你就可以为你的应用添加强大的实时交互能力了。
下一步:
- 尝试集成 spring security 进行用户认证。
 - 将简单消息代理替换为 rabbitmq 或 redis。
 - 实现更复杂的聊天功能,如群组、在线状态、消息历史记录。
 - 探索 websocket 在游戏、协作工具等领域的应用。
 
以上就是深入浅出springboot websocket构建实时应用全面指南的详细内容,更多关于springboot websocket构建实时应用的资料请关注代码网其它相关文章!
            
                                            
                                            
                                            
                                            
                                            
                                            
发表评论