bigywater.tech
Spring · 63

WebSocket 与 STOMP

WebSocket 消息太大连接为什么断了?多线程向 session 发消息为什么报错?STOMP 的 /app 和 /topic 有什么区别?客户端能不能直接广播?简单代理能不能集群?这篇用手写的最小 WebSocket 客户端在 Spring Boot 4.0.8 上逐个验证。

8192 / 8193
默认单条文本消息:8192 字节能过,8193 字节连接以 1009 断开
485 / 600
两个线程各发 300 条,直接 sendMessage 失败 485 次;用 ConcurrentWebSocketSessionDecorator 则全部成功
0
订阅者连在另一个实例时收到的广播条数(简单代理不跨实例)

握手与消息上限

实测 · 本机执行输出
  握手响应:HTTP/1.1 101 ;Upgrade=websocket;Connection=upgrade;Sec-WebSocket-Accept 校验通过=True
  发送 hello → 收到 echo:hello
  发送 4000 字节 → 收到 echo:<4000 字节>
  发送 8192 字节 → 收到 echo:<8192 字节>
  发送 8193 字节 → 收到 CLOSE code=1009
  (服务端)连接建立:session=77ab 线程=http-nio-9601-exec-2 协议=
  (服务端)连接关闭:CloseStatus[code=1009, reason=The decoded text message was too big for the output buffer and the endpoint does not support partial messages]

默认文本消息上限 8192 字节,超限整条连接断开。

调大上限

实测 · 本机执行输出
  bigbuf:发送 20000 字节 → echo:<20000 字节>
  发送 65536 字节 → echo:<65536 字节>
  发送 65537 字节 → CLOSE code=1009

ServletServerContainerFactoryBean 显式设置。

并发发送

实测 · 本机执行输出
  直接用 session.sendMessage,两个线程各发 300 条:客户端收到 115 条普通消息;SUMMARY ok=115 errors=485 first=IllegalStateException: The remote endpoint was in state [TEXT_PARTIAL_WRITING] which is an invalid state for called method
  用 ConcurrentWebSocketSessionDecorator 包起来,两个线程各发 300 条:客户端收到 600 条普通消息;SUMMARY ok=600 errors=0 first=

WebSocketSession 不是线程安全的,并发发送要包装。

STOMP

实测 · 本机执行输出
  客户端 1 CONNECT →  CONNECTED | version:1.2 | heart-beat:0,0
  客户端 2 CONNECT →  CONNECTED | version:1.2 | heart-beat:0,0
  客户端 3 CONNECT →  CONNECTED | version:1.2 | heart-beat:0,0
  客户端 3(没订阅)发 SEND /app/hello "Tom" 之后:
    客户端 1 收到: MESSAGE | destination:/topic/greetings | content-type:text/plain;charset=UTF-8 | subscription:sub-1 | message-id:5ae9d4ab-6d44-4997-96fc-0b51eefec4df-1 | content-length:10 |  | Hello, Tom
    客户端 2 收到: MESSAGE | destination:/topic/greetings | content-type:text/plain;charset=UTF-8 | subscription:sub-2 | message-id:49eae2fe-7051-4674-ae60-211af69edf52-0 | content-length:10 |  | Hello, Tom
    客户端 3 收到: (超时,没有收到)
  客户端 3 直接 SEND 到 /topic/greetings(不经过 @MessageMapping):
    客户端 1 收到: MESSAGE | destination:/topic/greetings | content-type:text/plain | subscription:sub-1 | message-id:5ae9d4ab-6d44-4997-96fc-0b51eefec4df-3 | content-length:42 |  | 绕过 @MessageMapping,直接发到 /topic
    客户端 2 收到: MESSAGE | destination:/topic/greetings | content-type:text/plain | subscription:sub-2 | message-id:49eae2fe-7051-4674-ae60-211af69edf52-2 | content-length:42 |  | 绕过 @MessageMapping,直接发到 /topic
  SEND 到没有 @MessageMapping 的 /app/unknown → (超时,没有收到)
  (服务端)@MessageMapping 收到 'Tom' 线程=clientInboundChannel-16

/app 走应用方法,/topic 是代理;客户端能直发 /topic。

心跳

实测 · 本机执行输出
  客户端声明 heart-beat:10000,10000 → CONNECTED | version:1.2 | heart-beat:0,0
  -- 五补:enableSimpleBroker 设置 heartbeat 5000 并提供 TaskScheduler
  客户端声明 heart-beat:10000,10000 → CONNECTED | version:1.2 | heart-beat:5000,5000

简单代理默认关闭心跳,需要显式配置。

多实例

实测 · 本机执行输出
  订阅者连在实例 A,发送者连在实例 B 发 SEND /app/hello → 实例 A 上的订阅者收到: (超时,没有收到)
  同一个发送者(实例 B)自己订阅后再发 Bob:实例 B 上的订阅者收到: MESSAGE | destination:/topic/greetings | content-type:text/plain;charset=UTF-8 | subscription:s | message-id:e78c1607-b74e-4243-b8bf-278b5b92cbae-0 | content-length:10 |  | Hello, Bob ;实例 A 上的订阅者收到: (超时,没有收到)

简单代理在内存里,跨实例不广播。

分步演示

WebSocket 与 STOMP
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 Spring Boot 4.0.8 的 SpringApplication(spring-boot-starter-websocket,内嵌 Tomcat 11.0.24);客户端是 Python 手写的最小 WebSocket 客户端(握手、掩码、帧收发),STOMP 帧手工拼接;并发发送的失败条数每次运行会略有不同
    • 没有验证:SockJS 回退;Spring Security 对 WebSocket / STOMP 的权限控制(见 Security 章节);/user 目的地与 convertAndSendToUser;外部 STOMP 中继(RabbitMQ)的配置;WebFlux 上的 WebSocket(WebSocketHandler 返回 Mono);消息的压缩扩展(permessage-deflate);空闲超时与服务端 ping/pong;大量连接下的内存占用
    京ICP备2026067502号-1京公网安备11010502063487号