Add the sse-websocket module
This commit is contained in:
96
sse-websocket/README.md
Normal file
96
sse-websocket/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# `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**](https://ankurm.com/spring-boot-4-sse-websocket-stomp/)
|
||||
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`](scripts/run-all.sh) regenerates
|
||||
everything under [`docs/output/`](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
|
||||
|
||||
```bash
|
||||
./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
|
||||
|
||||
1. [Two protocols, one question](docs/01-two-protocols.md)
|
||||
2. [The SSE lifecycle, and the three ways a stream ends](docs/02-sse-lifecycle.md)
|
||||
3. [The payload, and a media type that does less than you think](docs/03-the-payload.md)
|
||||
4. [STOMP: four lines of configuration, three surprising defaults](docs/04-stomp.md)
|
||||
5. [The limits: origin, size, and which one actually fires](docs/05-limits.md)
|
||||
6. [Choosing, and when the answer is neither](docs/06-choosing.md)
|
||||
|
||||
## Captured output
|
||||
|
||||
| File | What it shows |
|
||||
|---|---|
|
||||
| [`sse-wire-format.txt`](docs/output/sse-wire-format.txt) | the raw bytes of an SSE response, headers included |
|
||||
| [`sse-payload-conversion.txt`](docs/output/sse-payload-conversion.txt) | `data(x)` vs `data(x, APPLICATION_JSON)` for a record and a `String` |
|
||||
| [`sse-lifecycle.txt`](docs/output/sse-lifecycle.txt) | an async timeout as the client sees it, and how many writes a dead client absorbs |
|
||||
| [`async-timeout.txt`](docs/output/async-timeout.txt) | the container default and the property override, read off `AsyncContext` |
|
||||
| [`sse-concurrency.txt`](docs/output/sse-concurrency.txt) | 40 open streams on a 10-thread connector |
|
||||
| [`stomp-routing.txt`](docs/output/stomp-routing.txt) | fan-out, the default destination, the controller bypass, user destinations, presence |
|
||||
| [`websocket-size-limits.txt`](docs/output/websocket-size-limits.txt) | the ceiling binary-searched at four container buffer sizes |
|
||||
| [`handshake-origin.txt`](docs/output/handshake-origin.txt) | 101 / 101 / 403 for absent, same and foreign `Origin` |
|
||||
| [`tests.txt`](docs/output/tests.txt) | 17 tests |
|
||||
|
||||
## Six things this module exists to prove
|
||||
|
||||
1. **SSE does not pin a thread per client.** Forty streams stay open on a connector pool capped
|
||||
at ten, with zero active threads.
|
||||
2. **An unset `spring.mvc.async.request-timeout` means 30 seconds on Tomcat 11.** Read off the
|
||||
live `AsyncContext`, not quoted. That is why dashboards reconnect every 30 s with nothing in
|
||||
the log.
|
||||
3. **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.
|
||||
4. **The media type argument to `SseEmitter.data(..)` is a filter, not a preference.** For a
|
||||
record it changes nothing; for a `String` it changes nothing either, because
|
||||
`StringHttpMessageConverter` claims the write first.
|
||||
5. **A STOMP client can publish straight to `/topic/**` and skip every controller.** The
|
||||
timestamp the controller would have overwritten arrives untouched.
|
||||
6. **Spring's 64 KB `messageSizeLimit` is 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.
|
||||
Reference in New Issue
Block a user