5.3 KiB
5.3 KiB
sse-websocket — a live dashboard over SSE and a chat over STOMP
Companion project for Server-Sent Events and WebSocket on Spring Boot 4: SseEmitter, STOMP, and Which to Pick on ankurm.com.
Seventeen tests against a real embedded Tomcat, talking over real sockets. No Docker, no
broker to install, nothing but a JDK. ./scripts/run-all.sh regenerates
everything under docs/output/.
Versions
| Version | Notes | |
|---|---|---|
| JDK | 25 (Temurin 25.0.4.1+1) | current LTS |
| Spring Boot | 4.1.1 | latest GA of the 4.1 line |
| Spring Framework | 7.0.9 | Boot-managed |
| Tomcat | 11.0.24 | Boot-managed; its WebSocket buffer is the limit that matters |
| Jackson | 3.1.5 (tools.jackson) |
why JacksonJsonMessageConverter, not MappingJackson2MessageConverter |
Read from repo1.maven.org/.../maven-metadata.xml and from Boot's own
spring-boot-dependencies POM, not from release announcements.
Quickstart
./scripts/run-all.sh # every test, then regenerate docs/output/
mvn test # the same without the capture
mvn spring-boot:run # then: curl -N localhost:8080/sse/metrics
curl -s localhost:8080/diag # open streams, counters, converter order, container buffers
Endpoints
| Path | What it is for |
|---|---|
GET /sse/metrics |
the dashboard stream; SseEmitter(Long.MAX_VALUE), fed by a scheduled broadcast |
GET /sse/raw |
three events and a close, so the wire format can be captured verbatim |
GET /sse/payload |
the same record with and without a media type, plus the same for a String |
GET /sse/silent?timeoutMs= |
one event then silence, to observe an async timeout from the client side |
GET /sse/resume |
echoes Last-Event-ID and replays from it |
GET /diag |
emitter counters, SimpUserRegistry count, converter order, container buffer sizes |
GET /diag/async-timeout |
the effective async timeout, read off the live AsyncContext |
WS /ws |
the STOMP endpoint |
WS /ws-sockjs |
the SockJS fallback — a separate endpoint, not an option on the first |
STOMP destinations: SEND /app/chat.send → /topic/room; SEND /app/chat.echo → /topic/chat.echo
(no @SendTo, on purpose); SEND /app/chat.whisper → /user/queue/whisper.
Profiles
| Profile | Effect |
|---|---|
| (none) | Tomcat's own WebSocket buffers — 8 192 bytes |
bigframes |
ServletServerContainerFactoryBean with 256 KB buffers; -Dws.buffer-bytes= to vary it |
Documentation
- Two protocols, one question
- The SSE lifecycle, and the three ways a stream ends
- The payload, and a media type that does less than you think
- STOMP: four lines of configuration, three surprising defaults
- The limits: origin, size, and which one actually fires
- Choosing, and when the answer is neither
Captured output
| File | What it shows |
|---|---|
sse-wire-format.txt |
the raw bytes of an SSE response, headers included |
sse-payload-conversion.txt |
data(x) vs data(x, APPLICATION_JSON) for a record and a String |
sse-lifecycle.txt |
an async timeout as the client sees it, and how many writes a dead client absorbs |
async-timeout.txt |
the container default and the property override, read off AsyncContext |
sse-concurrency.txt |
40 open streams on a 10-thread connector |
stomp-routing.txt |
fan-out, the default destination, the controller bypass, user destinations, presence |
websocket-size-limits.txt |
the ceiling binary-searched at four container buffer sizes |
handshake-origin.txt |
101 / 101 / 403 for absent, same and foreign Origin |
tests.txt |
17 tests |
Six things this module exists to prove
- SSE does not pin a thread per client. Forty streams stay open on a connector pool capped at ten, with zero active threads.
- An unset
spring.mvc.async.request-timeoutmeans 30 seconds on Tomcat 11. Read off the liveAsyncContext, not quoted. That is why dashboards reconnect every 30 s with nothing in the log. - A dead SSE client is discovered by a failed write, and not by the first one. One full serialise-and-write succeeded into a socket whose client had already gone.
- The media type argument to
SseEmitter.data(..)is a filter, not a preference. For a record it changes nothing; for aStringit changes nothing either, becauseStringHttpMessageConverterclaims the write first. - A STOMP client can publish straight to
/topic/**and skip every controller. The timestamp the controller would have overwritten arrives untouched. - Spring's 64 KB
messageSizeLimitis never reached on a stock Tomcat. The container's 8 192-byte buffer caps a STOMP frame at about 16 KB and closes the connection with status 1009 — no exception, no log line from Spring.