├── .gitignore ├── README.md ├── article1.md ├── article2.md ├── article3.md ├── build.gradle ├── cross-origin-requests-foreign-origin ├── build.gradle └── src │ └── main │ ├── java │ └── demo │ │ └── cross_origin_requests │ │ └── foreign_origin │ │ ├── CdmController.java │ │ ├── CorsController.java │ │ ├── CrossOriginRequestsForeignOriginApplication.java │ │ ├── JsonpController.java │ │ └── XfoController.java │ └── resources │ ├── application.properties │ ├── static │ ├── cdm-iframe.html │ └── xfo.html │ └── views │ └── framed.html ├── cross-origin-requests-local-origin ├── build.gradle └── src │ └── main │ ├── java │ └── demo │ │ └── cross_origin_requests │ │ └── local_origin │ │ └── CrossOriginRequestsLocalOriginApplication.java │ └── resources │ ├── application.properties │ └── static │ ├── cdm-window.html │ ├── cors.html │ ├── jsonp.html │ └── xfo.html ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── HTTP long polling.vsdx ├── HTTP polling.vsdx ├── HTTP streaming.vsdx ├── HTTP_long_polling.png ├── HTTP_polling.png ├── HTTP_streaming.png ├── STOMP broker acknowledgment.puml ├── STOMP client acknowledgment.puml ├── STOMP connecting.puml ├── STOMP sending.puml ├── STOMP subscribing.puml ├── STOMP_broker_acknowledgment.png ├── STOMP_client_acknowledgment.png ├── STOMP_connecting.png ├── STOMP_sending.png ├── STOMP_subscribing.png ├── WebSocket.png ├── WebSocket.vsdx ├── browser-sockjs-websocket.png ├── browser-sockjs-xhr-polling.png ├── browser-sockjs-xhr-streaming.png ├── browser-stomp.png ├── browser-websocket.png ├── caniuse.com-websockets.png └── header.iuml ├── settings.gradle ├── websocket-client ├── build.gradle └── src │ └── main │ └── java │ └── demo │ └── websocket │ └── client │ └── example1 │ ├── ClientWebSocketApplication.java │ ├── ClientWebSocketConfig.java │ └── ClientWebSocketHandler.java ├── websocket-server ├── build.gradle └── src │ └── main │ ├── java │ └── demo │ │ └── websocket │ │ └── server │ │ ├── example0 │ │ └── SecWebSocketAccept.java │ │ └── example1 │ │ ├── SchedulerConfig.java │ │ ├── ServerWebSocketApplication.java │ │ ├── ServerWebSocketConfig.java │ │ └── ServerWebSocketHandler.java │ └── resources │ ├── application.properties │ └── static │ ├── css │ └── application.css │ ├── index.html │ └── js │ └── application.js ├── websocket-sockjs-client ├── build.gradle └── src │ └── main │ └── java │ └── demo │ └── websocket │ └── client │ └── example2 │ ├── ClientWebSocketHandler.java │ ├── ClientWebSocketSockJsApplication.java │ └── ClientWebSocketSockJsConfig.java ├── websocket-sockjs-server ├── build.gradle └── src │ └── main │ ├── java │ └── demo │ │ └── websocket │ │ └── server │ │ └── example2 │ │ ├── ServerWebSocketHandler.java │ │ ├── ServerWebSocketSockJsApplicaion.java │ │ └── ServerWebSocketSockJsConfig.java │ └── resources │ ├── application.properties │ └── static │ ├── css │ └── application.css │ ├── index.html │ └── js │ └── application.js ├── websocket-sockjs-stomp-client ├── build.gradle └── src │ └── main │ └── java │ └── demo │ └── websocket │ └── client │ └── example3 │ ├── ClientStompSessionHandler.java │ ├── ClientWebSocketSockJsStompApplication.java │ └── ClientWebSocketSockJsStompConfig.java ├── websocket-sockjs-stomp-highcharts ├── build.gradle └── src │ └── main │ ├── java │ └── demo │ │ └── websocket │ │ └── server │ │ └── example4 │ │ ├── PerformanceApplication.java │ │ ├── config │ │ └── WebSocketConfig.java │ │ ├── controller │ │ ├── PerformanceController.java │ │ └── WebSocketMessageBrokerStatsController.java │ │ ├── domain │ │ └── Performance.java │ │ ├── service │ │ └── PerformanceService.java │ │ └── websocket │ │ ├── interceptor │ │ └── LoggingChannelInterceptor.java │ │ └── listener │ │ ├── SessionConnectEventListener.java │ │ ├── SessionConnectedEventListener.java │ │ ├── SessionDisconnectEventListener.java │ │ ├── SessionSubscribeEventListener.java │ │ └── SessionUnsubscribeEventListener.java │ └── resources │ ├── application.properties │ └── static │ ├── css │ └── application.css │ ├── index.html │ └── js │ ├── application.js │ └── chart.js └── websocket-sockjs-stomp-server ├── build.gradle └── src └── main ├── java └── demo │ └── websocket │ └── server │ └── example3 │ ├── MessageMappingController.java │ ├── ScheduledController.java │ ├── ServerWebSocketSockJsStompApplication.java │ ├── StompWebSocketConfig.java │ └── SubscribeMappingController.java └── resources ├── application.properties └── static ├── application.css ├── application.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | !**/src/main/**/out/ 24 | !**/src/test/**/out/ 25 | 26 | ### NetBeans ### 27 | /nbproject/private/ 28 | /nbbuild/ 29 | /dist/ 30 | /nbdist/ 31 | /.nb-gradle/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSockets with Spring 2 | 3 | * [WebSockets with Spring, part 1: HTTP and WebSocket](article1.md) 4 | * [WebSockets with Spring, part 2: WebSocket with SockJS fallback](article2.md) 5 | * [WebSockets with Spring, part 3: STOMP over WebSocket](article3.md) 6 | -------------------------------------------------------------------------------- /article1.md: -------------------------------------------------------------------------------- 1 | # WebSockets with Spring, part 1: HTTP and WebSocket 2 | 3 | ## Introduction 4 | 5 | The HTTP protocol is a _request-response_ protocol. That means that only a client can send HTTP requests to a server. A server can only service HTTP requests by sending back HTTP responses, but a server can not send unrequested HTTP responses to a client. 6 | 7 | This is because HTTP was originally designed for request-response resources transfer in distributed hypermedia systems but not for simultaneous bi-directional communication. To overcome these architecture limitations are used several HTTP mechanisms (grouped under the unofficial name _Comet_) that are often complicated and inefficient. 8 | 9 | The WebSocket protocol is designed to replace existing workaround HTTP mechanisms and provide an effective protocol for low-latency, simultaneous, bi-directional communication between browsers and servers over a single TCP connection. 10 | 11 | >This article describes the relationships between WebSocket and HTTP/1.1. 12 | 13 | ## HTTP-based mechanisms 14 | 15 | Because HTTP was not designed to support server-initiated messages, several mechanisms to achieve this have been developed, each with different benefits and drawbacks. 16 | 17 | ### HTTP polling 18 | 19 | During the _polling_ mechanism, a client sends periodic requests to a server, and the server responds immediately. If there is new data, the server returns it, otherwise the server returns an empty response. After receiving the response, the client waits for a while before sending another request. 20 | 21 | ![HTTP polling](/images/HTTP_polling.png) 22 | 23 | Polling can be efficient if we know the update period of the data on the server. Otherwise, the client may poll the server either too rarely (adding additional latency in transferring data from the server to the client) or too often (wasting server processing and network resources). 24 | 25 | ### HTTP long polling 26 | 27 | During the _long polling_ mechanism, a client sends a request to a server and starts waiting for a response. The server does not respond until new data arrives or a timeout occurs. When new data becomes available, the server sends a response to the client. After receiving the response, the client immediately sends another request. 28 | 29 | ![HTTP long polling](/images/HTTP_long_polling.png) 30 | 31 | Long polling reduces the use of server processing and network resources to receive data updates with low latency, especially where new data becomes available at irregular intervals. However, the server must keep track of multiple open requests. Also, long-running requests can time out, and the new requests must be sent periodically, even if the data is not updated. 32 | 33 | ### HTTP streaming 34 | 35 | During the _streaming_ mechanism, a client sends a request to a server and keeps it open indefinitely. The server does not respond until new data arrives. When new data becomes available, the server sends it back to the client as a part of the response. The data sent by the server does not close the request. 36 | 37 | ![HTTP streaming](/images/HTTP_streaming.png) 38 | 39 | Streaming is based on the capability of the server to send several pieces of data in the same response, without closing the request. This mechanism significantly reduces the network latency because the client and the server do not need to send and receive new requests. 40 | 41 | However, the client and server need to agree on how to interpret the response stream so that the client will know where one piece of data ends and another begins. Also, network intermediaries can disrupt streaming - they may buffer the response and cause latency or disconnect connections that are kept open for a long time. 42 | 43 | ### Server-Sent Events 44 | 45 | Server-Sent Events (SSE) is a standardized streaming mechanism that has the network protocol and the [EventSource API](https://html.spec.whatwg.org/multipage/server-sent-events.html) for browsers. SSE defines a uni-directional UTF-8 encoded events stream from a server to a browser. Events have mandatory values and can have optional types and unique identifiers. In case of failure, SSE supports automatic client reconnection from the last received event. 46 | 47 | An example of the SSE request: 48 | 49 | ``` 50 | GET /sse HTTP/1.1 51 | Host: server.com 52 | Accept: text/event-stream 53 | ``` 54 | 55 | An example of the SSE response: 56 | 57 | ``` 58 | HTTP/1.1 200 OK 59 | Connection: keep-alive 60 | Content-Type: text/event-stream 61 | Transfer-Encoding: chunked 62 | 63 | retry: 1000 64 | 65 | data: A text message 66 | 67 | data: {"message": "a JSON message"} 68 | 69 | event: text 70 | data: A message of type 'text' 71 | 72 | id: 1 73 | event: text 74 | data: A message of type 'text' with a unique identifier 75 | 76 | :ping 77 | ``` 78 | 79 | >Server-Sent Events can send streaming data only from a server to a browser and supports only text data. 80 | 81 | ## WebSocket 82 | 83 | ### Prerequisites 84 | 85 | WebSocket is designed to overcome the limitations of HTTP-based mechanisms (polling, long polling, streaming) in _full-duplex_ communication between browsers and servers: 86 | 87 | >In _full-duplex_ communication, both parties can send and receive messages in both directions at the same time. 88 | 89 | >In _half-duplex_ communication, both parties can send and receive messages in both directions, but in one direction at a time. 90 | 91 | HTTP allows _half-duplex_ communication between a browser and a server: a browser can either send requests to a server or receive responses from a server, but not both at the same time. To overcome these limitations, several Comet mechanisms use two simultaneous HTTP connections for upstream and downstream communication between a browser and a server that leads to additional complexity. 92 | 93 | Here are the main design differences between HTTP and WebSocket: 94 | 95 | * HTTP is a text protocol, WebSocket is a binary protocol (binary protocols transfer fewer data over the network than text protocols) 96 | * HTTP has request and response headers, WebSocket messages can have a format suitable for specific applications (unnecessary metadata are not transmitted over the network) 97 | * HTTP is a half-duplex protocol, WebSocket is a full-duplex protocol (low-latency messages can be transmitted at the same time in both directions) 98 | 99 | ### Design 100 | 101 | WebSocket is a protocol that allows simultaneous bi-directional transmission of text and binary messages between clients (mostly browsers) and servers over a single TCP connection. WebSocket can communicate over TCP on port 80 ("ws" scheme) or over TLS/TCP on port 443 ("wss" scheme). 102 | 103 | ![WebSocket](/images/WebSocket.png) 104 | 105 | WebSocket is an independent TCP-based protocol distinguished from HTTP. However, it is designed to coexist with HTTP: 106 | 107 | * WebSocket handshake is interpreted by HTTP servers as HTTP _Upgrade_ request 108 | * WebSocket shares the same 80 and 443 ports as HTTP and HTTPS 109 | * WebSocket supports HTTP network intermediaries (proxies, firewalls, routers, etc.) 110 | 111 | WebSocket is designed to add support for TCP sockets with as little modifications as possible to browser-server communication, providing necessary security constraints of the Web. WebSocket adds just minimum functionality on top of TCP, nothing more than the following: 112 | 113 | * origin-based security model 114 | * conversion between IP addresses used in TCP to URLs used on the Web 115 | * message protocol on top of byte stream protocol 116 | * closing handshake 117 | 118 | The WebSocket protocol is designed to be a simple protocol and to provide a foundation to build application subprotocols on top of it, similar to how the TCP protocol allows building application protocols (HTTP, FTP, SMTP, POP3, Telnet, etc.). 119 | 120 | The WebSocket standard contains two parts: the WebSocket protocol standardized as [RFC 6455](https://tools.ietf.org/html/rfc6455) and the [WebSocket API](https://html.spec.whatwg.org/multipage/web-sockets.html). 121 | 122 | ### The WebSocket protocol 123 | 124 | The WebSocket network protocol consists of two components: 125 | 126 | 1. the opening handshake for negotiating the parameters of the WebSocket connection 127 | 2. the binary message framing for sending text and binary messages 128 | 129 | #### Opening handshake 130 | 131 | Before starting the exchange of messages, the client and server negotiate the parameters of the establishing connection. WebSocket reuses the existing HTTP _Upgrade_ mechanism with special _Sec-WebSocket-*_ headers to perform the connection negotiation. 132 | 133 | >WebSocket _subprotocols_ are top-level protocols that provide additional functionality for applications (for example, the STOMP subprotocol provides the publish-subscribe messaging model). 134 | 135 | >WebSocket _extensions_ are a mechanism to modify message framing without affecting application protocols. (for example, the _permessage-deflate_ extension compresses payload data by the LZ77 algorithm). 136 | 137 | An example of an HTTP to WebSocket upgrade request: 138 | 139 | ``` 140 | GET /socket HTTP/1.1 141 | Host: server.com 142 | Connection: Upgrade 143 | Upgrade: websocket 144 | Origin: http://example.com 145 | Sec-WebSocket-Version: 8, 13 146 | Sec-WebSocket-Key: 7c0RT+Z1px24ypyYfnPNbw== 147 | Sec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp 148 | Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits 149 | ``` 150 | 151 | An example of an HTTP to WebSocket upgrade response: 152 | 153 | ``` 154 | HTTP/1.1 101 Switching Protocols 155 | Connection: Upgrade 156 | Upgrade: websocket 157 | Access-Control-Allow-Origin: http://example.com 158 | Sec-WebSocket-Accept: O1a/o0MeFzoDgn+kCKR91UkYDO4= 159 | Sec-WebSocket-Protocol: v12.stomp 160 | Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15 161 | ``` 162 | 163 | The opening handshake consists of the following parts: _protocol upgrade, origin policies negotiation, protocol negotiation, subprotocol negotiation, extensions negotiation_. 164 | 165 | To pass the _protocol upgrade_: 166 | 167 | * the client sends a request with the _Connection_ and _Upgrade_ headers 168 | * the server confirms the protocol upgrade with _101 Switching Protocols_ response line and the same _Connection_ and _Upgrade_ headers 169 | 170 | To pass the _origin policies negotiation_: 171 | 172 | * the client sends the _Origin_ header (scheme, host name, port number) 173 | * the server confirms that the client from this origin is allowed to access the resource via the _Access-Control-Allow-Origin_ header 174 | 175 | To pass the _protocol negotiation_: 176 | 177 | * the client sends the _Sec-WebSocket-Version_ (a list of protocol versions, 13 for RFC 6455) and _Sec-WebSocket-Key_ (an auto-generated key) headers 178 | * the server confirms the protocol by returning the _Sec-WebSocket-Accept_ header 179 | 180 | >Equivalent Java code for calculating the _Sec-WebSocket-Accept_ header: 181 | 182 | ``` 183 | Base64 184 | .getEncoder() 185 | .encodeToString( 186 | MessageDigest 187 | .getInstance("SHA-1") 188 | .digest((secWebSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") 189 | .getBytes(StandardCharsets.UTF_8))); 190 | ``` 191 | 192 | To pass _subprotocol negotiation_: 193 | 194 | * the client sends a list of subprotocols via the _Sec-WebSocket-Protocol_ header 195 | * the server select one of the subprotocols via the _Sec-WebSocket-Protocol_ header (if the server does not support any subprotocol, then the connection is canceled) 196 | 197 | To pass the _extensions negotiation_: 198 | 199 | * the client sends a list of extensions via the _Sec-WebSocket-Extensions_ header 200 | * the server confirms _one or more_ extensions via the _Sec-WebSocket-Extensions_ header (if the server does not support some extensions, then the connection proceeds without them) 201 | 202 | After a successful handshake, the client and the server switch from text HTTP protocol to binary WebSocket message framing and can perform full-duplex communication. 203 | 204 | #### Message framing 205 | 206 | WebSocket uses a binary message framing: the sender splits each application _message_ into one or more _frames_, transports them across the network to the destination, reassembles them, and notifies the receiver once the entire message has been received. 207 | 208 | WebSocking framing has the following format: 209 | 210 | 1. FIN (1 bit) - the flag that indicates whether the frame is the final frame of a message 211 | 2. reserve (3 bits) - the reserve flags for extensions 212 | 3. operation code (4 bits) - the type of frame: data frames (text or binary) or control frames (connection close, ping/pong for connection liveness checks) 213 | 4. mask (1 bit) - the flag that indicates whether the payload data is masked (all frames sent from client to server are masked) 214 | 5. payload length (7 bits, or 7+16 bits, or 7+64 bits) - the variable-length payload length (if 0-125, then that is the payload length; if 126, then the following 2 bytes represent the payload length; if 127, then the following 8 bytes represent the payload length) 215 | 6. masking key (0 or 4 bytes) - the masking key contains a 32-bit value used to XOR the payload data 216 | 7. payload data (n bytes) - the payload data contains extension data (if extensions are used) concatenated with application data 217 | 218 | In such binary message framing, the variable-length payload length field allows low framing overhead during exchanging as small as big messages. According to some sources, the WebSocket protocol compared with the HTTP protocol can provide about 500:1 reduction in traffic and 3:1 reduction in latency. 219 | 220 | #### Closing handshake 221 | 222 | Either party can initiate a closing handshake by sending a closing frame. On receiving such a frame, the other party sends a closing frame in response, if it has not already sent one. After sending the closing frame, a party does not send any further data. After receiving a closing frame, a party discards any further data received. Once a party has both sent and received a closing frame, that endpoint closes the WebSocket connection. 223 | 224 | Besides closing the connection by a closing handshake, a WebSocket connection might be closed abruptly when another party goes away or the underlying TCP collection closes. Status codes in closing frames can identify the reason. 225 | 226 | ### The WebSocket API 227 | 228 | The WebSocket API is the interface that a browser must implement to communicate with servers using the WebSocket protocol. 229 | 230 | Before using the WebSocket API, it is necessary to make sure that the browser supports it. 231 | 232 | ``` 233 | if (window.WebSocket) { 234 | // WebSocket is supported 235 | } else { 236 | // WebSocket is not supported 237 | } 238 | ``` 239 | 240 | To establish a connection to the server, the API provides the _WebSocket_ constructor with a mandatory server URL and optional subprotocols. Once the connection is established, the _onopen_ event listener is called. After the connection, it is possible to read the _protocol_ and _extensions_ properties to determine the connection parameters selected by the server. 241 | 242 | The API provides the _readyState_ property to determine the current state of the connection: whether the connection is established, has not yet been established, already closed, or is going through the closing handshake. 243 | 244 | The API allows sending and receiving text and binary messages. Text messages are encoded in UTF-8 and use the _DOMString_ objects. Binary messages can use either _Blob_ objects (when messages are supposed to be immutable) or _ArrayBuffer_ objects (when messages may be modified). The _binaryType_ property specifies the type of binary objects being used by the connection. 245 | 246 | The API provides the _send_ method to send messages. It is important, that this method is non-blocking: it enqueues the data to be transmitted to the server and returns immediately. The _bufferedAmount_ property returns the number of bytes that have been queued using the _send_ method but not yet transmitted to the network. 247 | 248 | The API provides receiving messages in a non-blocking manner. Once a message is received, the _onmessage_ event listener is called. 249 | 250 | The API provides the _close_ method to close the connection. The method has an optional status _code_ and an optional human-readable _reason_. Once the connection is closed, the _onclose_ event listener is called. 251 | 252 | Once an error occurs, the _onerror_ event listener is called. After any error, the connection is closed. 253 | 254 | An example of WebSocket browser application: 255 | 256 | ``` 257 | const ws = new WebSocket('ws://server.com/socket'); 258 | ws.binaryType = "blob"; 259 | 260 | ws.onopen = function () { 261 | // send binary messages 262 | ws.send(new Blob([new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]).buffer])); 263 | 264 | // send text messages 265 | ws.send("Hello!"); 266 | } 267 | 268 | ws.onclose = function () { 269 | // handle disconnect 270 | } 271 | 272 | ws.onmessage = function(msg) { 273 | if (msg.data instanceof Blob) { 274 | // receive binary messages 275 | } else { 276 | // receive text messages 277 | } 278 | } 279 | 280 | ws.onerror = function (error) { 281 | // handle errors 282 | } 283 | ``` 284 | 285 | >The WebSocket API exposes neither framing information nor ping/pong methods to applications. 286 | 287 | ## Examples 288 | 289 | ### Introduction 290 | 291 | The Spring Framework provides support for WebSocket clients and servers in the _spring-websocket_ module. 292 | 293 | The following example implements full-duplex WebSocket text communication between a server and clients. The server and the clients work according to the following algorithm: 294 | 295 | * the server sends a one-time message to the client 296 | * the server sends periodic messages to the client 297 | * the server receives messages from a client, logs them, and sends them back to the client 298 | * the client sends aperiodic messages to the server 299 | * the client receives messages from a server and logs them 300 | 301 | The server is implemented as a Spring web application with Spring Web MVC framework to handle static web resources. One client is implemented as a JavaScript browser client and another client is implemented as a Java Spring console application. 302 | 303 | ### Java Spring server 304 | 305 | Java Spring server consists of two parts: Spring WebSocket events handler and Spring WebSocket configuration. 306 | 307 | Because the server uses text (not binary) messages, the events handler extends the existing _TextWebSocketHandler_ class as the required implementation of the _WebSocketHandler_ interface. The handler uses the _handleTextMessage_ callback method to receive messages from a client and the _sendMessage_ method to send messages back to the client. 308 | 309 | Existing Spring WebSocket event handlers do not support broadcasting messages to many clients. To implement this manually, the _afterConnectionEstablished_ and _afterConnectionClosed_ methods maintain the thread-safe list of active clients. The _@Scheduled_ method broadcasts periodic messages to active clients with the same _sendMessage_ method. 310 | 311 | ``` 312 | public class ServerWebSocketHandler extends TextWebSocketHandler implements SubProtocolCapable { 313 | 314 | private final Set sessions = new CopyOnWriteArraySet<>(); 315 | 316 | @Override 317 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 318 | logger.info("Server connection opened"); 319 | sessions.add(session); 320 | 321 | TextMessage message = new TextMessage("one-time message from server"); 322 | logger.info("Server sends: {}", message); 323 | session.sendMessage(message); 324 | } 325 | 326 | @Override 327 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 328 | logger.info("Server connection closed: {}", status); 329 | sessions.remove(session); 330 | } 331 | 332 | @Scheduled(fixedRate = 10000) 333 | void sendPeriodicMessages() throws IOException { 334 | for (WebSocketSession session : sessions) { 335 | if (session.isOpen()) { 336 | String broadcast = "server periodic message " + LocalTime.now(); 337 | logger.info("Server sends: {}", broadcast); 338 | session.sendMessage(new TextMessage(broadcast)); 339 | } 340 | } 341 | } 342 | 343 | @Override 344 | public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 345 | String request = message.getPayload(); 346 | logger.info("Server received: {}", request); 347 | 348 | String response = String.format("response from server to '%s'", HtmlUtils.htmlEscape(request)); 349 | logger.info("Server sends: {}", response); 350 | session.sendMessage(new TextMessage(response)); 351 | } 352 | 353 | @Override 354 | public void handleTransportError(WebSocketSession session, Throwable exception) { 355 | logger.info("Server transport error: {}", exception.getMessage()); 356 | } 357 | 358 | @Override 359 | public List getSubProtocols() { 360 | return Collections.singletonList("subprotocol.demo.websocket"); 361 | } 362 | } 363 | ``` 364 | 365 | The following Spring configuration enables WebSocket support in the Spring server with the _@EnableWebSocket_ annotation. This configuration also registers the implemented WebSocket handler for the WebSocket endpoint. 366 | 367 | ``` 368 | @Configuration 369 | @EnableWebSocket 370 | public class ServerWebSocketConfig implements WebSocketConfigurer { 371 | 372 | @Override 373 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 374 | registry.addHandler(webSocketHandler(), "/websocket"); 375 | } 376 | 377 | @Bean 378 | public WebSocketHandler webSocketHandler() { 379 | return new ServerWebSocketHandler(); 380 | } 381 | } 382 | ``` 383 | 384 | The server is a Spring Boot web application with Spring Web MVC framework to handle static web resources for the JavaScript browser client. However, Spring WebSocket support does not depend on Spring MVC and can be used with any Java Servlet framework. 385 | 386 | ``` 387 | @SpringBootApplication 388 | @EnableScheduling 389 | public class ServerWebSocketApplicaion { 390 | 391 | public static void main(String[] args) { 392 | SpringApplication.run(ServerWebSocketApplicaion.class, args); 393 | } 394 | } 395 | ``` 396 | 397 | ### JavaScript browser client 398 | 399 | The JavaScript browser client uses the standard _WebSocket_ browser object. It is important, that the client uses the "ws" scheme to specify the server URL. 400 | 401 | When a user clicks the 'Connect' button, the client uses the _WebSocket_ constructor (with the server URL and the subprotocol) to initiate a connection to the server. When the connection is established, the _WebSocket.onopen_ callback handler is called. 402 | 403 | When the user clicks the 'Disconnect' button, the client uses the _WebSocket.close_ method to initiate the close of the connection. When the connection is closed, the _WebSocket.onclose_ callback handler is called. 404 | 405 | ``` 406 | let webSocket = null; 407 | 408 | // 'Connect' button click handler 409 | function connect() { 410 | webSocket = new WebSocket('ws://localhost:8080/websocket', 411 | 'subprotocol.demo.websocket'); 412 | 413 | webSocket.onopen = function () { 414 | log('Client connection opened'); 415 | 416 | console.log('Subprotocol: ' + webSocket.protocol); 417 | console.log('Extensions: ' + webSocket.extensions); 418 | }; 419 | 420 | webSocket.onmessage = function (event) { 421 | log('Client received: ' + event.data); 422 | }; 423 | 424 | webSocket.onerror = function (event) { 425 | log('Client error: ' + event); 426 | }; 427 | 428 | webSocket.onclose = function (event) { 429 | log('Client connection closed: ' + event.code); 430 | }; 431 | } 432 | 433 | // 'Disconnect' button click handler 434 | function disconnect() { 435 | if (webSocket != null) { 436 | webSocket.close(); 437 | webSocket = null; 438 | } 439 | } 440 | ``` 441 | 442 | When the user clicks the 'Send' button, the client uses the _WebSocket.send_ method to send a text message to the server. 443 | 444 | ``` 445 | // 'Send' button click handler 446 | function send() { 447 | const message = $("#request").val(); 448 | log('Client sends: ' + message); 449 | webSocket.send(message); 450 | } 451 | ``` 452 | 453 | When the client receives a message, the _WebSocket.onmessage_ callback handler is called. Incoming messages are received and outgoing messages are transmitted independently of each other. 454 | 455 | ![WebSocket](/images/browser-websocket.png) 456 | 457 | ### Java Spring client 458 | 459 | Java Spring client consists of two parts: Spring WebSocket events handler and Spring WebSocket configuration. 460 | 461 | The client (as the server) extends the existing _TextWebSocketHandler_ class. The handler uses the _handleTextMessage_ callback method to receive messages from a server and the _sendMessage_ method to send messages to the server. 462 | 463 | ``` 464 | public class ClientWebSocketHandler extends TextWebSocketHandler { 465 | 466 | @Override 467 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 468 | logger.info("Client connection opened"); 469 | 470 | TextMessage message = new TextMessage("one-time message from client"); 471 | logger.info("Client sends: {}", message); 472 | session.sendMessage(message); 473 | } 474 | 475 | @Override 476 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 477 | logger.info("Client connection closed: {}", status); 478 | } 479 | 480 | @Override 481 | public void handleTextMessage(WebSocketSession session, TextMessage message) { 482 | logger.info("Client received: {}", message); 483 | } 484 | 485 | @Override 486 | public void handleTransportError(WebSocketSession session, Throwable exception) { 487 | logger.info("Client transport error: {}", exception.getMessage()); 488 | } 489 | } 490 | ``` 491 | 492 | The following Spring configuration enables WebSocket support in the Spring client. The configuration defines a _WebSocketConnectionManager_ object that uses two Spring beans: 493 | 494 | * the _StandardWebSocketClient_ class (from the _tomcat-embed-websocket_ dependency) as an implementation of the _WebSocketClient_ interface - to connect to the WebSocket server 495 | * the implemented _WebSocketHandler_ class - to handle WebSocket events during communication 496 | 497 | ``` 498 | @Configuration 499 | public class ClientWebSocketConfig { 500 | 501 | @Bean 502 | public WebSocketConnectionManager webSocketConnectionManager() { 503 | WebSocketConnectionManager manager = new WebSocketConnectionManager( 504 | webSocketClient(), 505 | webSocketHandler(), 506 | "ws://localhost:8080/websocket" 507 | ); 508 | manager.setAutoStartup(true); 509 | return manager; 510 | } 511 | 512 | @Bean 513 | public WebSocketClient webSocketClient() { 514 | return new StandardWebSocketClient(); 515 | } 516 | 517 | @Bean 518 | public WebSocketHandler webSocketHandler() { 519 | return new ClientWebSocketHandler(); 520 | } 521 | } 522 | ``` 523 | 524 | The client is a console Spring Boot application without Spring Web MVC. 525 | 526 | ``` 527 | @SpringBootApplication 528 | public class ClientWebSocketApplication { 529 | 530 | public static void main(String[] args) { 531 | new SpringApplicationBuilder(ClientWebSocketApplication.class) 532 | .web(WebApplicationType.NONE) 533 | .run(args); 534 | } 535 | } 536 | ``` 537 | 538 | ## Conclusion 539 | 540 | WebSocket is another communication technology for the Web designed to solve a specific range of problems where the capabilities of HTTP-based solutions are limited. But like any other technology, WebSockets is not a "silver bullet" and it has its advantages and drawbacks. 541 | 542 | It is better to use WebSocket when: 543 | 544 | * it is necessary to get _updates_ of a resource with the lowest possible latency 545 | * high-frequency messages with small payloads are used 546 | * the messaging communication model is used - when messages are sent by either party independently of each other 547 | * in enterprise applications when browsers and networks infrastructure is under control 548 | 549 | It is better to use HTTP when: 550 | 551 | * it is necessary to get the _current state_ of a resource 552 | * it is possible to benefit from idempotency, safety, cacheability HTTP requests 553 | * the request-response communication model is used - when requests are always acknowledged by responses 554 | * it is expensive to modify the existing hardware and software infrastructure to support WebSocket 555 | 556 | Complete code examples are available in the [GitHub repository](https://github.com/aliakh/demo-spring-websocket/tree/master/websocket-server). 557 | -------------------------------------------------------------------------------- /article2.md: -------------------------------------------------------------------------------- 1 | # WebSockets with Spring, part 2: WebSocket with SockJS fallback 2 | 3 | ## Introduction 4 | 5 | According to some sources, the WebSocket API is currently (2020) implemented in the [most common browsers](https://caniuse.com/websockets): 6 | 7 | ![https://caniuse.com/#feat=websockets](/images/caniuse.com-websockets.png) 8 | 9 | But in addition to outdated browsers (mainly on mobile platforms), there are network intermediaries (proxies, firewalls, routers, etc.) that can prevent WebSocket communication. These intermediaries may not pass HTTP to WebSocket protocol upgrade or may close long-lived connections. 10 | 11 | One possible solution to this problem is WebSocket emulation - first trying to use WebSocket and then falling back to HTTP-based techniques that expose the same API. 12 | 13 | ## SockJS 14 | 15 | ### Design 16 | 17 | SockJS is one of the existing WebSocket browser fallbacks. SockJS includes a protocol, a JavaScript browser client, and Node.js, Erlang, Ruby servers. There are also third-party SockJS clients and servers for different programming languages and platforms. SockJS was designed to emulate the WebSocket API as close as possible to provide the ability to use WebSocket subprotocols on top of it. 18 | 19 | SockJS has the following design considerations: 20 | 21 | * simple browser API as close to WebSocket API as possible 22 | * only JavaScript, no Flash/Java plugins 23 | * support of scaling and load balancing techniques 24 | * support of cross-domain communication and cookies 25 | * fast connection establishment 26 | * _graceful degradation_ in case of outdated browsers and restrictive proxies 27 | 28 | >_Gradceful degradation_ in web development is a design principle that focuses on trying to use the best features that work in newer browsers but falls back on other features that, while not as good, still provides essential functionality in older browsers. 29 | 30 | SockJS supports a wide range of browser versions and has at least one long polling and streaming transport for each of them. It is important, that streaming transports support _cross-domain communications_ and _cookies_. _Cross-domain communication_ is required when SockJS is hosted on a different server than the main web application. _Cookies_ are essential for authentication in web applications and cookie-based sticky sessions in load balancers. 31 | 32 | #### SockJS and the same-origin policy 33 | 34 | The _same-origin policy_ is a critical concept in web application security. An _origin_ is a combination of scheme, host name, and port number. This policy describes how a document or script loaded from one origin can interact with a resource from another origin. Some resources such as images, sounds, videos, stylesheets, fonts, iframes can be accessed across origins. Scripts can be loaded across origins as well, but some of their actions (such as cross-origin Ajax calls) are disabled by default. 35 | 36 | Some of the techniques related to cross-domain communication used in SockJS are briefly described below ( their source codes are available in the provided GitHub repository). 37 | 38 | ##### JSON with Padding (JSONP) 39 | 40 | This outdated technique is based on the ability of scripts to be loaded from other origins. According to it, a script element is inserted into a document at run-time and the script body is loaded dynamically from a server. The server returns a JSON that is wrapped in a function that is already defined in the JavaScript environment. When the script is loaded, the function is executed with the JSON argument. This method is vulnerable because the server (if compromised) can execute any JavaScript on the client. 41 | 42 | ##### The iframe element 43 | 44 | A page from one origin can be loaded into an iframe on a page from another origin. However, some servers may prevent their pages from being included in iframes. The _X-Frame-Options_ response header can be sent by the server to indicate if the browser is allowed to load the page in an iframe. This header can have the following values: 45 | 46 | * _DENY_ - the page cannot be loaded in a frame, regardless of the site attempting to do so 47 | * _SAMEORIGIN_ - the page can only be loaded in a frame on the same origin as the page itself 48 | * _ALLOW-FROM origin_ - the page can be loaded in a frame only on the specified _origin_ 49 | 50 | It is important, that a script inside an iframe is not allowed to access or modify a document of its parent window and vice-versa unless both have the same origin. 51 | 52 | ##### Cross-Origin Resource Sharing (CORS) 53 | 54 | CORS is a standard that uses special _Access-Control-Allow-*_ headers to specify policies on how pages running at one origin can provide access to their resources to pages from other origins. 55 | 56 | For the cross-origin requests that can only read data (the GET and HEAD methods or the POST method with certain content types; all the methods without custom headers), a browser uses a "simple" request. The browser sends a request with the _Origin_ request header and a server responds with: 57 | 58 | * the _Access-Control-Allow-Origin_ header with this origin (if requests from this origin are allowed) 59 | * the _Access-Control-Allow-Origin_ header with a wildcard * (if requests from all origins are allowed) 60 | * an error (if requests from this origin are forbidden) 61 | 62 | For the cross-origin requests that can modify data, the browser first sends the "preflight" request by the OPTIONS method to determine if the actual request is allowed to send. If the server responds with the appropriate _Access-Control-Allow-Origin_ response header, the browser sends the actual request. During the "preflight" browser can also identify by using special _Access-Control-Allow-*_ headers, if it is allowed to use specific HTTP methods, custom HTTP headers, user credentials (cookies or HTTP authentication). 63 | 64 | ##### Cross-document messaging 65 | 66 | Cross-document messaging is a standard that allows different browser windows (iframes, pop-ups, tabs) to communicate with each other, even if they are from different origins. The _Window_ browser object has the _postMessage_ method to send messages to another window and the _onmessage_ event handler to receive messages from it. 67 | 68 | This standard allows implementing a cross-origin communication technique that is known as _iframe via postMessage_. A page from a "local" origin loads in its iframe a page from a "remote" origin. The page in the iframe has a script loaded from the same "remote" origin that can therefore make calls to the server. So the script from the "local" origin communicates across origins with the script in the iframe with the _postMessage_ and _onmessage_ methods, and the script from the iframe makes the same-origin Ajax calls to the server. 69 | 70 | #### SockJS and load balancers 71 | 72 | SockJS supports sticky sessions in load balancers. With sticky sessions, a load balancer identifies a user and routes all of the requests from this user to a specific server. Among other methods, load balancers can use application cookies for this (for example, _JSESSIONID_ cookie that uses Java Servlet containers for an HTTP session). 73 | 74 | Some load balancers do not support WebSocket. In this case, it is necessary to exclude WebSocket from the list of available SockJS transports. 75 | 76 | ### SockJS transports 77 | 78 | SockJS transports fall into three categories: native WebSocket, HTTP streaming, HTTP long polling. 79 | 80 | #### WebSocket transport 81 | 82 | _WebSocket_ is the transport with the best latency and throughput, and it has built-in support for cross-domain communication. 83 | 84 | >SockJS exists precisely because some browsers or network intermediaries still do not support WebSocket. 85 | 86 | ![SockJS over WebSocket](/images/browser-sockjs-websocket.png) 87 | 88 | #### Streaming transports 89 | 90 | SockJS streaming transports are based on HTTP 1.1 _chunked transfer encoding_ (the _Transfer-Encoding: chunked_ response header) that allows the browser to receive a single response from the server in many parts. 91 | 92 | Every browser supports a different set of streaming transports and they usually do not support cross-domain communication. SockJS overcomes that limitation by using an iframe and communicating with it using cross-document messaging (the technique is known as _iframe via postMessage_). 93 | 94 | SockJS has the following streaming transports: 95 | 96 | * _xhr-streaming_ - the transport using _XMLHttpRequest_ object via streaming capability 97 | * _xdr-streaming_ - the transport using _XDomainRequest_ object via streaming capability 98 | * _eventsource_ - the transport using _EventSource_ object (Server-Sent Events) 99 | * _iframe-eventsource_ - the transport using _EventSource_ object (Server-Sent Events) from an _iframe via postMessage_ 100 | * _htmlfile_ - the transport using ActiveXObject _HtmlFile_ object 101 | * _iframe-htmlfile_ - the transport using ActiveXObject _HtmlFile_ object from an _iframe via postMessage_ 102 | 103 | >_XMLHttpRequest_ (XHR) is a browser interface to make Ajax calls. 104 | 105 | >_XDomainRequest_ (XDR) is a deprecated browser interface in Internet Explorer to make Ajax calls. 106 | 107 | ##### XhrStreaming transport 108 | 109 | _XhrStreaming_ transport is an example of streaming transports. This transport uses (as all Comet techniques) two simultaneous half-duplex HTTP connections to emulate a full-duplex WebSocket connection. 110 | 111 | The first HTTP connection sends each message from the browser to the server in a separate request. The second HTTP connection sends all messages from the server to the browser using the same request (response has the _Transfer-Encoding: chunked_ header). In a browser, the _XMLHttpRequest_ object process the partial responses using its streaming capability (the technique is known as _readyState=3_). 112 | 113 | Equivalen example of using the _XMLHttpRequest_ object for streaming: 114 | 115 | ``` 116 | const xhr = new XMLHttpRequest(); 117 | xhr.open('POST', '/xhr_streaming'); 118 | xhr.seenBytes = 0; 119 | 120 | xhr.onreadystatechange = function() { 121 | if (xhr.readyState == 3) { 122 | const data = xhr.response.substr(xhr.seenBytes); 123 | // new data is received 124 | xhr.seenBytes = xhr.responseText.length; 125 | } 126 | }; 127 | 128 | xhr.send(); 129 | ``` 130 | 131 | ![SockJS over xhr-streaming](/images/browser-sockjs-xhr-streaming.png) 132 | 133 | #### Polling transports 134 | 135 | SockJS supports a few polling transports for outdated browsers. SockJS uses slow and outdated JSONP transport when nothing else works. 136 | 137 | SockJS has the following polling transports: 138 | 139 | * _xhr-polling_ - transport using _XMLHttpRequest_ object 140 | * _xdr-polling_ - transport using _XDomainRequest_ object 141 | * _iframe-xhr-polling_ - transport using _XMLHttpRequest_ object from an _iframe via postMessage_ 142 | * _jsonp-polling_ - transport using _JSONP_ technique 143 | 144 | ##### XhrPolling transport 145 | 146 | _XhrPolling_ transport is an example of long polling transports. This transport also uses two simultaneous half-duplex HTTP connections to emulate a full-duplex WebSocket connection. 147 | 148 | The first HTTP connection sends each message from the browser to the server in a separate request. The second HTTP connection sends each message from the server to the browser using a separate request as well. 149 | 150 | Equivalen example of using the _XMLHttpRequest_ object for long polling: 151 | 152 | ``` 153 | const xhr = new XMLHttpRequest(); 154 | xhr.open('POST','/xhr); 155 | xhr.timeout = 60000; 156 | 157 | xhr.onreadystatechange = function() { 158 | if (xhr.readyState == 4) { 159 | // response is received 160 | const data = xhr.response; 161 | } 162 | }; 163 | 164 | xhr.ontimeout = function () { 165 | // timeout has happened 166 | }; 167 | 168 | xhr.send(); 169 | ``` 170 | 171 | ![SockJS over xhr-polling](/images/browser-sockjs-xhr-polling.png) 172 | 173 | ### SockJS protocol 174 | 175 | The SockJS network protocol consists of two components: 176 | 177 | 1. the opening handshake for identifying the parameters of the SockJS session 178 | 2. the session for full-duplex communication 179 | 180 | Before starting a session, a SockJS client sends a GET request to the _/info_ server URL to obtain basic information from the server. The server responds with the following properties: 181 | 182 | * _websocket_ - the property to specify whether the server needs WebSocket transport 183 | * _cookie_needed_ - the property to specify whether the server needs cookie support 184 | * _origins_ - the list of allowed origins 185 | * _entropy_ - the source of entropy for the random number generator 186 | 187 | After the handshake, the client decides which transport to use. If it is possible, the client selects WebSocket. If not, in most browsers there is at least one HTTP streaming transport. Otherwise, the client uses some HTTP long polling transport. 188 | 189 | During a SockJS session, the client sends requests to the _/server/session/transport_ server URLs, where: 190 | 191 | * _server_ - the identifier of a server in a cluster 192 | * _session_ - the identifier of a SockJS session 193 | * _transport_ - the transport type 194 | 195 | A SockJS client accepts the following frames: 196 | 197 | * "o" - open frame is sent by the server every time a new session is established 198 | * "h" - heartbeat frame is periodically sent (if there is no message flow) by the server to prevent load balancers from closing connection by timeout 199 | * "a" - messages frame is an array of JSON-encoded messages (for example, _a["Hello!"]_) 200 | * "c" - close frame is sent by the server to close the session; close frame contains a code and a string explaining a reason for closure (for example, _c[3000, "Go away!"_]) 201 | 202 | A SockJS server does not define any framing. All incoming data is accepted as incoming messages, either single JSON-encoded messages or an array of JSON-encoded messages (depending on the transport). 203 | 204 | ## Examples 205 | 206 | ### Introduction 207 | 208 | The Spring Framework provides support for WebSocket/SockJS clients and servers in the _spring-websocket_ module. 209 | 210 | The following example implements full-duplex WebSocket text communication with SockJS fallback between a server and clients. The server and the clients work according to the following algorithm: 211 | 212 | * the server sends a one-time message to the client 213 | * the server sends periodic messages to the client 214 | * the server receives messages from a client, logs them, and sends them back to the client 215 | * the client sends aperiodic messages to the server 216 | * the client receives messages from a server and logs them 217 | 218 | The server is implemented as a Spring web application with Spring Web MVC framework to handle static web resources. One client is implemented as a JavaScript browser client and another client is implemented as a Java Spring console application. 219 | 220 | ### Java Spring server 221 | 222 | Java Spring server consists of two parts: Spring WebSocket events handler and Spring WebSocket/SockJS configuration. 223 | 224 | Because the server uses text (not binary) messages, the events handler extends the existing _TextWebSocketHandler_ class as the required implementation of the _WebSocketHandler_ interface. The handler uses the _handleTextMessage_ callback method to receive messages from a client and the _sendMessage_ method to send messages back to the client. 225 | 226 | Existing Spring WebSocket event handlers do not support broadcasting messages to many clients. To implement this manually, the _afterConnectionEstablished_ and _afterConnectionClosed_ methods maintain the thread-safe list of active clients. The _@Scheduled_ method broadcasts periodic messages to active clients with the same _sendMessage_ method. 227 | 228 | ``` 229 | public class ServerWebSocketHandler extends TextWebSocketHandler implements SubProtocolCapable { 230 | 231 | private final Set sessions = new CopyOnWriteArraySet<>(); 232 | 233 | @Override 234 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 235 | logger.info("Server connection opened"); 236 | sessions.add(session); 237 | 238 | TextMessage message = new TextMessage("one-time message from server"); 239 | logger.info("Server sends: {}", message); 240 | session.sendMessage(message); 241 | } 242 | 243 | @Override 244 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 245 | logger.info("Server connection closed: {}", status); 246 | sessions.remove(session); 247 | } 248 | 249 | @Scheduled(fixedRate = 10000) 250 | void sendPeriodicMessages() throws IOException { 251 | for (WebSocketSession session : sessions) { 252 | if (session.isOpen()) { 253 | String broadcast = "server periodic message " + LocalTime.now(); 254 | logger.info("Server sends: {}", broadcast); 255 | session.sendMessage(new TextMessage(broadcast)); 256 | } 257 | } 258 | } 259 | 260 | @Override 261 | public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 262 | String request = message.getPayload(); 263 | logger.info("Server received: {}", request); 264 | 265 | String response = String.format("response from server to '%s'", HtmlUtils.htmlEscape(request)); 266 | logger.info("Server sends: {}", response); 267 | session.sendMessage(new TextMessage(response)); 268 | } 269 | 270 | @Override 271 | public void handleTransportError(WebSocketSession session, Throwable exception) { 272 | logger.info("Server transport error: {}", exception.getMessage()); 273 | } 274 | 275 | @Override 276 | public List getSubProtocols() { 277 | return Collections.singletonList("subprotocol.demo.websocket"); 278 | } 279 | } 280 | ``` 281 | 282 | >We can notice, that the Spring WebSocket events handlers for the servers with plain WebSocket and with WebSocket with SockJS fallback are the same. 283 | 284 | The following Spring configuration enables WebSocket support in the Spring server with the _@EnableWebSocket_ annotation. This configuration also registers the implemented WebSocket handler for the WebSocket endpoint with the SockJS fallback. 285 | 286 | ``` 287 | @Configuration 288 | @EnableWebSocket 289 | public class ServerWebSocketSockJsConfig implements WebSocketConfigurer { 290 | 291 | @Override 292 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 293 | registry.addHandler(webSocketHandler(), "/websocket-sockjs") 294 | .setAllowedOrigins("*") 295 | .withSockJS() 296 | .setWebSocketEnabled(true) 297 | .setHeartbeatTime(25000) 298 | .setDisconnectDelay(5000) 299 | .setClientLibraryUrl("/webjars/sockjs-client/1.1.2/sockjs.js") 300 | .setSessionCookieNeeded(false); 301 | } 302 | 303 | @Bean 304 | public WebSocketHandler webSocketHandler() { 305 | return new ServerWebSocketHandler(); 306 | } 307 | } 308 | ``` 309 | 310 | This configuration demonstrates some of the configuration properties available for a Spring SockJS server: 311 | 312 | * _allowedOrigins_ - this property can be used to specify allowed origins for CORS 313 | * _webSocketEnabled_ - this property can be used to disable the WebSocket transport if the load balancer does not support WebSocket 314 | * _heartbeatTime_ - this property can be used to specify the period with which server (if there is no message flow) sends heartbeat frames to the client to keep the connection from closing 315 | * _disconnectDelay_ - this property can be used to specify a timeout before closing an expired session 316 | * _clientLibraryUrl_ - this property can be used to specify the URL of the SockJS JavaScript client library for the iframe-based transports 317 | * _sessionCookieNeeded_ - this property can be used to specify whether the server needs cookies for load balancing or in Java Servlet containers to use HTTP session 318 | 319 | The server is a Spring Boot web application with Spring Web MVC framework to handle static web resources for the JavaScript browser client. However, Spring WebSocket support does not depend on Spring MVC and can be used with any Java Servlet framework. 320 | 321 | ``` 322 | @SpringBootApplication 323 | @EnableScheduling 324 | public class ServerWebSocketSockJsApplicaion { 325 | 326 | public static void main(String[] args) { 327 | SpringApplication.run(ServerWebSocketSockJsApplicaion.class, args); 328 | } 329 | } 330 | ``` 331 | 332 | ### JavaScript browser client 333 | 334 | The JavaScript browser client uses the _SockJS_ object from the [SockJS](https://github.com/sockjs/sockjs-client) library. It is important, that the client uses the "http" scheme (not the "ws" scheme) to specify the server URL. 335 | 336 | When a user clicks the 'Connect' button, the client uses the _SockJS_ constructor (with the server URL, the subprotocol, and the selected SockJS transports) to initiate a connection to the server. When the connection is established, the _SockJS.onopen_ callback handler is called. 337 | 338 | When the user clicks the 'Disconnect' button, the client uses the _SockJS.close_ method to initiate the close of the connection. When the connection is closed, the _SockJS.onclose_ callback handler is called. 339 | 340 | ``` 341 | let sockJS = null; 342 | 343 | // 'Connect' button click handler 344 | function connect() { 345 | const option = $("#transports").find('option:selected').val(); 346 | const transports = (option === 'all') ? [] : [option]; 347 | 348 | sockJS = new SockJS('http://localhost:8080/websocket-sockjs', 349 | 'subprotocol.demo.websocket', {debug: true, transports: transports}); 350 | 351 | sockJS.onopen = function () { 352 | log('Client connection opened'); 353 | 354 | console.log('Subprotocol: ' + sockJS.protocol); 355 | console.log('Extensions: ' + sockJS.extensions); 356 | }; 357 | 358 | sockJS.onmessage = function (event) { 359 | log('Client received: ' + event.data); 360 | }; 361 | 362 | sockJS.onerror = function (event) { 363 | log('Client error: ' + event); 364 | }; 365 | 366 | sockJS.onclose = function (event) { 367 | log('Client connection closed: ' + event.code); 368 | }; 369 | } 370 | 371 | // 'Disconnect' button click handler 372 | function disconnect() { 373 | if (sockJS != null) { 374 | sockJS.close(); 375 | sockJS = null; 376 | } 377 | } 378 | ``` 379 | 380 | When the user clicks the 'Send' button, the client uses the _SockJS.send_ method to send a text message to the server. 381 | 382 | ``` 383 | // 'Send' button click handler 384 | function send() { 385 | var message = $("#request").val(); 386 | log('Client sends: ' + message); 387 | sockJS.send(message); 388 | } 389 | ``` 390 | 391 | When the client receives a message, the _SockJS.onmessage_ callback handler is called. Incoming messages are received and outgoing messages are transmitted independently of each other. 392 | 393 | >We can notice, that the JavaScript browser client for the clients with plain WebSocket and with WebSocket with SockJS fallback are very similar (they differ only in the creation of the communicating object). 394 | 395 | ### Java Spring client 396 | 397 | Java Spring client consists of two parts: Spring WebSocket events handler and Spring WebSocket configuration. 398 | 399 | The client (as the server) extends the existing _TextWebSocketHandler_ class. The handler uses the _handleTextMessage_ callback method to receive messages from a server and the _sendMessage_ method to send messages to the server. 400 | 401 | ``` 402 | public class ClientWebSocketHandler extends TextWebSocketHandler { 403 | 404 | @Override 405 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 406 | logger.info("Client connection opened"); 407 | 408 | TextMessage message = new TextMessage("one-time message from client"); 409 | logger.info("Client sends: {}", message); 410 | session.sendMessage(message); 411 | } 412 | 413 | @Override 414 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 415 | logger.info("Client connection closed: {}", status); 416 | } 417 | 418 | @Override 419 | public void handleTextMessage(WebSocketSession session, TextMessage message) { 420 | logger.info("Client received: {}", message); 421 | } 422 | 423 | @Override 424 | public void handleTransportError(WebSocketSession session, Throwable exception) { 425 | logger.info("Client transport error: {}", exception.getMessage()); 426 | } 427 | } 428 | ``` 429 | 430 | >We can notice, that the Spring WebSocket events handlers for the clients (as for the servers) with plain WebSocket and with WebSocket with SockJS fallback are the same. 431 | 432 | The following Spring configuration enables WebSocket support in the Spring client. The configuration defines a _WebSocketConnectionManager_ object, that uses two Spring beans: 433 | 434 | * the _SockJsClient_ class (from the _spring-websocket_ dependency) as an implementation of the _WebSocketClient_ interface - to connect to the WebSocket/SockJS server 435 | * the implemented _WebSocketHandler_ - to handle WebSocket events during communication 436 | 437 | The _SockJsClient_ object uses two transports: 438 | 439 | * the _WebSocketTransport_ object, which supports SockJS _WebSocket_ transport 440 | * the _RestTemplateXhrTransport_ object, which supports SockJS _XhrStreaming_ and _XhrPolling_ transports 441 | 442 | ``` 443 | @Configuration 444 | public class ClientWebSocketSockJsConfig { 445 | 446 | @Bean 447 | public WebSocketConnectionManager webSocketConnectionManager() { 448 | WebSocketConnectionManager manager = new WebSocketConnectionManager( 449 | webSocketClient(), 450 | webSocketHandler(), 451 | "http://localhost:8080/websocket-sockjs" 452 | ); 453 | manager.setAutoStartup(true); 454 | return manager; 455 | } 456 | 457 | @Bean 458 | public WebSocketClient webSocketClient() { 459 | List transports = new ArrayList<>(); 460 | transports.add(new WebSocketTransport(new StandardWebSocketClient())); 461 | transports.add(new RestTemplateXhrTransport()); 462 | return new SockJsClient(transports); 463 | } 464 | 465 | @Bean 466 | public WebSocketHandler webSocketHandler() { 467 | return new ClientWebSocketHandler(); 468 | } 469 | } 470 | ``` 471 | 472 | >We can notice, that the Spring WebSocket configuration with plain WebSocket and with WebSocket with SockJS fallback are very similar (they differ only in the used implementation of the _WebSocketClient_ interface). 473 | 474 | The client is a console Spring Boot application without Spring Web MVC. 475 | 476 | ``` 477 | @SpringBootApplication 478 | public class ClientWebSocketSockJsApplication { 479 | 480 | public static void main(String[] args) { 481 | new SpringApplicationBuilder(ClientWebSocketSockJsApplication.class) 482 | .web(WebApplicationType.NONE) 483 | .run(args); 484 | } 485 | } 486 | ``` 487 | 488 | ## Conclusion 489 | 490 | There are two strategies to deal with the absence of WebSocket support in browsers and network infrastructure: _emulation_ and _extensions_. The first strategy (SockJS follows it) is to emulate the WebSocket API as close as possible. The second strategy (most of the other fallbacks follow it) is to build a top-level API and use WebSocket as one of the transports along with Flash/Java plugins or Comet techniques. 491 | 492 | Emulation strategy does not provide any additional API on top of the WebSocket protocol and might require additional development. However, this strategy may be beneficial in the long term when all browsers will eventually get WebSocket support, and fallbacks are no longer needed. 493 | 494 | Many commercial operators provide their solutions that include WebSocket as one of the protocols: Kaazing WebSocket Gateway, PubNub, Pusher, Ably, etc. Although all these solutions are non-compliant and proprietary, using them can be a reasonable decision to get a business solution right now and do not deal with the Web development that requires a lot of time and expertise. 495 | 496 | Complete code examples are available in the [GitHub repository](https://github.com/aliakh/demo-spring-websocket/tree/master/websocket-sockjs-server). 497 | -------------------------------------------------------------------------------- /article3.md: -------------------------------------------------------------------------------- 1 | # WebSockets with Spring, part 3: STOMP over WebSocket 2 | 3 | ## Introduction 4 | 5 | The WebSocket protocol is designed to overcome the architecture limitations of HTTP-based solutions in simultaneous bi-directional communication. Most importantly, WebSocket has another communication model (simultaneous bi-directional messaging) than HTTP (request-response). 6 | 7 | WebSocket works over TCP that allows transmitting of two-way streams of _bytes_. WebSocket provides thin functionality on top of TCP that allows transmitting binary and text _messages_ providing necessary security constraints of the Web. But WebSocket does not specify the format of such messages. 8 | 9 | WebSocket is intentionally designed to be as simple as possible. To avoid additional _protocol_ complexity, clients and servers are intended to use _subprotocols_ on top of WebSocket. STOPM is one such application subprotocol that can work over WebSocket to exchange messages between clients via intermediate servers (message brokers). 10 | 11 | ## STOMP 12 | 13 | ### Design 14 | 15 | STOMP (Simple/Streaming Text Oriented Message Protocol) is an interoperable text-based protocol for messaging between clients via message brokers. 16 | 17 | STOMP is _a simple protocol_ because it implements only a small number of the most commonly used messaging operations of message brokers. 18 | 19 | STOMP is _a streaming protocol_ because it can work over any reliable bi-directional streaming network protocol (TCP, WebSocket, Telnet, etc.). 20 | 21 | STOMP is _a text protocol_ because clients and message brokers exchange text frames that contain a mandatory command, optional headers, and an optional body (the body is separated from headers by a blank line). 22 | 23 | ``` 24 | COMMAND 25 | header1:value1 26 | Header2:value2 27 | 28 | body 29 | ``` 30 | 31 | STOMP is _a messaging protocol_ because clients can produce messages (send messages to a broker destination) and consume them (subscribe to and receive messages from a broker destination). 32 | 33 | STOMP is _an interoperable protocol_ because it can work with multiple message brokers (ActiveMQ, RabbitMQ, HornetQ, OpenMQ, etc.) and clients written in many languages and platforms. 34 | 35 | ### Сonnecting clients to a broker 36 | 37 | ![STOMP connecting](/images/STOMP_connecting.png) 38 | 39 | #### Connecting 40 | 41 | To connect to a broker, a client sends a CONNECT frame with two mandatory headers: 42 | 43 | * _accept-version_ - the versions of the STOMP protocol the client supports 44 | * _host_ - the name of a virtual host that the client wishes to connect to 45 | 46 | To accent the connection, the broker sends to the client a CONNECTED frame with the mandatory header: 47 | 48 | * _version_ - the version of the STOMP protocol the session will be using 49 | 50 | #### Disconnecting 51 | 52 | A client can disconnect from a broker at any time by closing the socket, but there is no guarantee that the previously sent frames have been received by the broker. To disconnect properly, where the client is assured that all previous frames have been received by the broker, the client must: 53 | 54 | 1. send a DISCONNECT frame with a _receipt_ header 55 | 2. receive a RECEIPT frame 56 | 3. close the socket 57 | 58 | ### Sending messages from clients to a broker 59 | 60 | ![STOMP sending](/images/STOMP_sending.png) 61 | 62 | To send a message to a destination, a client sends a SEND frame with the mandatory header: 63 | 64 | * _destination_ - the destination to which the client wants to send 65 | 66 | If the SEND frame has a body, it must include the _content-length_ and _content-type_ headers. 67 | 68 | ### Subscribing clients to messages from a broker 69 | 70 | ![STOMP subscribing](/images/STOMP_subscribing.png) 71 | 72 | #### Subscribing 73 | 74 | To subscribe to a destination a client sends a SUBSCRIBE frame with two mandatory headers: 75 | 76 | * _destination_ - the destination to which the client wants to subscribe 77 | * _id_ - the unique identifier of the subscription 78 | 79 | #### Messaging 80 | 81 | To transmit messages from subscriptions to the client, the server sends a MESSAGE frame with three mandatory headers: 82 | 83 | * _destination_ - the destination the message was sent to 84 | * _subscription_ - the identifier of the subscription that is receiving the message 85 | * _message-id_ - the unique identifier for that message 86 | 87 | #### Unsubscribing 88 | 89 | To remove an existing subscription, the client sends an UNSUBSCRIBE frame with the mandatory header: 90 | 91 | * _id_ - the unique identifier of the subscription 92 | 93 | ### Acknowledgment 94 | 95 | To avoid lost or duplicated frames, if a client and a broker are parts of a distributed system, it is necessary to use frames acknowledgment. 96 | 97 | #### Client messages acknowledgment 98 | 99 | ![STOMP client acknowledgment](/images/STOMP_client_acknowledgment.png) 100 | 101 | The SUBSCRIBE frame may contain the optional _ack_ header that controls the message acknowledgment mode: _auto_ (by default), _client_, _client-individual_. 102 | 103 | When the acknowledgment mode is _auto_, then the client does not need to confirm the messages it receives. The broker will assume the client has received the message as soon as it sends it to the client. 104 | 105 | When the acknowledgment mode is _client_, then the client must send the server confirmation for all previous messages: they acknowledge not only the specified message but also all messages sent to the subscription before this one. 106 | 107 | When the acknowledgment mode is _client-individual_, then the client must send the server confirmation for the specified message only. 108 | 109 | The client uses an ACK frame to confirm the consumption of a message from a subscription using the _client_ or _client-individual_ acknowledgment modes. The client uses a NACK frame to negate the consumption of a message from a subscription. The ACK and NAK frames must include the _id_ header matching the _ack_ header of the MESSAGE frame being acknowledged. 110 | 111 | #### Broker commands acknowledgment 112 | 113 | ![STOMP broker acknowledgment](/images/STOMP_broker_acknowledgment.png) 114 | 115 | A broker sends a RECEIPT frame to a client once the broker has successfully processed a client frame that requests a receipt. The RECEIPT frame includes the _receipt-id_ header matching the _receipt_ header of the command being acknowledged. 116 | 117 | ## Examples 118 | 119 | ### Introduction 120 | 121 | The Spring Framework provides support for STOMP over WebSocket clients and servers in the _spring-websocket_ and _spring-messaging_ modules. 122 | 123 | Messages from and to STOMP clients can be handled by a message broker: 124 | 125 | * a simple STOMP broker (which only supports a subset of STOMP commands) embedded into a Spring application 126 | * an external STOMP broker connected to a Spring application via TCP 127 | 128 | Messages from and to STOMP clients also can be handled by a Spring application: 129 | 130 | * messages can be received and sent by _annotated controllers_ 131 | * messages can be sent by _message templates_ 132 | 133 | The following example implements STOMP over WebSocket messaging with SockJS fallback between a server and clients. The server and the clients work according to the following algorithm: 134 | 135 | * the server sends a one-time message to the client 136 | * the server sends periodic messages to the client 137 | * the server receives messages from a client, logs them, and sends them back to the client 138 | * the client sends aperiodic messages to the server 139 | * the client receives messages from a server and logs them 140 | 141 | The server is implemented as a Spring web application with Spring Web MVC framework to handle static web resources. One client is implemented as a JavaScript browser client and another client is implemented as a Java Spring console application. 142 | 143 | ### Java Spring server 144 | 145 | #### Configuration 146 | 147 | The following Spring configuration enables STOMP support in the Java Spring server. 148 | 149 | ``` 150 | @Configuration 151 | @EnableWebSocketMessageBroker 152 | public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer { 153 | 154 | @Override 155 | public void registerStompEndpoints(StompEndpointRegistry registry) { 156 | registry.addEndpoint("/websocket-sockjs-stomp").withSockJS(); 157 | } 158 | 159 | @Override 160 | public void configureMessageBroker(MessageBrokerRegistry registry) { 161 | registry.enableSimpleBroker("/queue", "/topic"); 162 | registry.setApplicationDestinationPrefixes("/app"); 163 | } 164 | } 165 | ``` 166 | 167 | Firstly, this configuration registers a STOMP over WebSocket endpoint with SockJS fallback. 168 | 169 | Secondly, this configuration configures a STOMP message broker: 170 | 171 | * the destinations with the _/queue_ and _/topic_ prefixes are handled by the embedded simple STOMP broker 172 | * the destinations with the _/app_ prefix are handled by the annotated controllers in the Spring application 173 | 174 | >For the embedded simple broker, destinations with the _/topic_ and _/queue_ prefixes do not have any special meaning. For external brokers, destinations with the _/topic_ prefix often mean _publish-subscribe_ messaging (one producer and many consumers), and destinations with the _/queue_ prefix mean _point-to-point_ messaging (one producer and one consumer). 175 | 176 | #### Receiving and sending messages in annotated controllers 177 | 178 | Messages from and to STOMP clients can be handled according to the Spring programming model: by _annotated controllers_ and _message templates_. 179 | 180 | ##### @SubscribeMapping 181 | 182 | The _@SubscribeMapping_ annotation is used for one-time messaging from application to clients, for example, to load initial data during a client startup. 183 | 184 | In the following example, a client sends a SUBSCRIBE frame to the _/app/subscribe_ destination. The server sends a MESSAGE frame to the same _/app/subscribe_ destination directly to the client without involving a broker. 185 | 186 | ``` 187 | @Controller 188 | public class SubscribeMappingController { 189 | 190 | @SubscribeMapping("/subscribe") 191 | public String sendOneTimeMessage() { 192 | return "server one-time message via the application"; 193 | } 194 | } 195 | ``` 196 | 197 | ##### @MessageMapping 198 | 199 | The _@MessageMapping_ annotation is used for repetitive messaging from application to clients. 200 | 201 | In the following example, the method annotated with the _@MessageMapping_ annotation with the _void_ return type receives a SEND frame from a client to the _/app/request_ destination, performs some action but does not send any response. 202 | 203 | ``` 204 | @Controller 205 | public class MessageMappingController { 206 | 207 | @MessageMapping("/request") 208 | public void handleMessageWithoutResponse(String message) { 209 | logger.info("Message without response: {}", message); 210 | } 211 | } 212 | ``` 213 | 214 | In the following example, the method annotated with the _@MessageMapping_ and _@SendTo_ annotations with the _String_ return type receives a SEND frame from a client to the _/app/request_ destination, performs some action, and sends a MESSAGE frame to the explicit _/queue/responses_ destination. 215 | 216 | ``` 217 | @Controller 218 | public class MessageMappingController { 219 | 220 | @MessageMapping("/request") 221 | @SendTo("/queue/responses") 222 | public String handleMessageWithExplicitResponse(String message) { 223 | logger.info("Message with response: {}", message); 224 | return "response to " + HtmlUtils.htmlEscape(message); 225 | } 226 | } 227 | ``` 228 | 229 | In the following example, the method annotated with the _@MessageMapping_ annotation with the _String_ return type receives a SEND frame from a client to the _/app/request_ destination, performs some action, and sends a MESSAGE frame to the implicit _/app/request_ destination (with the _/topic_ prefix and the _/request_ suffix of the inbound destination). 230 | 231 | ``` 232 | @Controller 233 | public class MessageMappingController { 234 | 235 | @MessageMapping("/request") 236 | public String handleMessageWithImplicitResponse(String message) { 237 | logger.info("Message with response: {}", message); 238 | return "response to " + HtmlUtils.htmlEscape(message); 239 | } 240 | } 241 | ``` 242 | 243 | ##### @MessageExceptionHandler 244 | 245 | The _@MessageExceptionHandler_ annotation is used to handle exceptions in the _@SubscribeMapping_ and _@MessageMapping_ annotated controllers. 246 | 247 | In the following example, the method annotated with the _@MessageMapping_ annotations receives a SEND frame from a client to the _/app/request_ destination. In case of success, the method sends a MESSAGE frame to the _/queue/responses_ destination. In case of an error, the exception handling method sends a MESSAGE frame to the _/queue/errors_ destination. 248 | 249 | ``` 250 | @Controller 251 | public class MessageMappingController { 252 | 253 | @MessageMapping("/request") 254 | @SendTo("/queue/responses") 255 | public String handleMessageWithResponse(String message) { 256 | logger.info("Message with response: {}" + message); 257 | if (message.equals("zero")) { 258 | throw new RuntimeException(String.format("'%s' is rejected", message)); 259 | } 260 | return "response to " + HtmlUtils.htmlEscape(message); 261 | } 262 | 263 | @MessageExceptionHandler 264 | @SendTo("/queue/errors") 265 | public String handleException(Throwable exception) { 266 | return "server exception: " + exception.getMessage(); 267 | } 268 | } 269 | ``` 270 | 271 | >It is possible to handle exceptions for a single _@Controller_ class or across many controllers with a _@ControllerAdvice_ class. 272 | 273 | #### Sending messages by message templates 274 | 275 | It is possible to send MESSAGE frames to destinations by message templates using the methods of the _MessageSendingOperations_ interface. Also, it is possible to use an implementation of this interface, the _SimpMessagingTemplate_ class, that has additional methods to send messages to specific users. 276 | 277 | In the following example, a client sends a SUBSCRIBE frame to the _/topic/periodic_ destination. The server broadcasts MESSAGE frames to each subscriber of the _/topic/periodic_ destination. 278 | 279 | ``` 280 | @Component 281 | public class ScheduledController { 282 | 283 | private final MessageSendingOperations messageSendingOperations; 284 | 285 | public ScheduledController(MessageSendingOperations messageSendingOperations) { 286 | this.messageSendingOperations = messageSendingOperations; 287 | } 288 | 289 | @Scheduled(fixedDelay = 10000) 290 | public void sendPeriodicMessages() { 291 | String broadcast = String.format("server periodic message %s via the broker", LocalTime.now()); 292 | this.messageSendingOperations.convertAndSend("/topic/periodic", broadcast); 293 | } 294 | } 295 | ``` 296 | 297 | ### JavaScript browser client 298 | 299 | The JavaScript browser client uses the _webstomp_ object from the [webstomp-client](https://github.com/JSteunou/webstomp-client) library. As the underlying communicating object the client uses a _SockJS_ object from the [SockJS](https://github.com/sockjs/sockjs-client) library. 300 | 301 | When a user clicks the 'Connect' button, the client uses the _webstomp_._over_ method (with a _SockJS_ object argument) to create a _webstomp_ object. After that, the client uses the _webstomp.connect_ method (with empty headers and a callback handler) to initiate a connection to the server. When the connection is established, the callback handler is called. 302 | 303 | After the connection, the client uses the _webstomp.subscribe_ methods to subscribe to destinations. This method accepts a destination and a callback handler that is called when a message is received and returns a subscription. The client uses the _unsubscribe_ method to cancel the existing subscription. 304 | 305 | When the user clicks the 'Disconnect' button, the client uses the _webstomp.disconnect_ method (with a callback handler) to initiate the close of the connection. When the connection is closed, the callback handler is called. 306 | 307 | ``` 308 | let stomp = null; 309 | 310 | // 'Connect' button click handler 311 | function connect() { 312 | stomp = webstomp.over(new SockJS('/websocket-sockjs-stomp')); 313 | 314 | stomp.connect({}, function (frame) { 315 | stomp.subscribe('/app/subscribe', function (response) { 316 | log(response); 317 | }); 318 | 319 | const subscription = stomp.subscribe('/queue/responses', function (response) { 320 | log(response); 321 | }); 322 | 323 | stomp.subscribe('/queue/errors', function (response) { 324 | log(response); 325 | 326 | console.log('Client unsubscribes: ' + subscription); 327 | subscription.unsubscribe({}); 328 | }); 329 | 330 | stomp.subscribe('/topic/periodic', function (response) { 331 | log(response); 332 | }); 333 | }); 334 | } 335 | 336 | // 'Disconnect' button click handler 337 | function disconnect() { 338 | if (stomp !== null) { 339 | stomp.disconnect(function() { 340 | console.log("Client disconnected"); 341 | }); 342 | stomp = null; 343 | } 344 | } 345 | ``` 346 | 347 | When the user clicks the 'Send' button, the client uses the _webstomp.send_ method to send a message to the destination (with empty headers). 348 | 349 | ``` 350 | // 'Send' button click handler 351 | function send() { 352 | const output = $("#output").val(); 353 | console.log("Client sends: " + output); 354 | stomp.send("/app/request", output, {}); 355 | } 356 | ``` 357 | 358 | ![WebSocket](/images/browser-stomp.png) 359 | 360 | ### Java Spring client 361 | 362 | Java Spring client consists of two parts: Spring STOMP events handler and Spring STOMP over WebSocket configuration. 363 | 364 | To handle STOMP session events, the client implements the _StompSessionHandler_ interface. The handler uses the _subscribe_ method to subscribe to server destinations, the _handleFrame_ callback method to receive messages from a server, and the _sendMessage_ method to send messages to the server. 365 | 366 | ``` 367 | public class ClientStompSessionHandler extends StompSessionHandlerAdapter { 368 | 369 | @Override 370 | public void afterConnected(StompSession session, StompHeaders headers) { 371 | logger.info("Client connected: headers {}", headers); 372 | 373 | session.subscribe("/app/subscribe", this); 374 | session.subscribe("/queue/responses", this); 375 | session.subscribe("/queue/errors", this); 376 | session.subscribe("/topic/periodic", this); 377 | 378 | String message = "one-time message from client"; 379 | logger.info("Client sends: {}", message); 380 | session.send("/app/request", message); 381 | } 382 | 383 | @Override 384 | public void handleFrame(StompHeaders headers, Object payload) { 385 | logger.info("Client received: payload {}, headers {}", payload, headers); 386 | } 387 | 388 | @Override 389 | public void handleException(StompSession session, StompCommand command, 390 | StompHeaders headers, byte[] payload, Throwable exception) { 391 | logger.error("Client error: exception {}, command {}, payload {}, headers {}", 392 | exception.getMessage(), command, payload, headers); 393 | } 394 | 395 | @Override 396 | public void handleTransportError(StompSession session, Throwable exception) { 397 | logger.error("Client transport error: error {}", exception.getMessage()); 398 | } 399 | } 400 | ``` 401 | 402 | The following Spring configuration enables STOMP over WebSocket support in the Spring client. The configuration defines three Spring beans: 403 | 404 | * the implemented _ClientStompSessionHandler_ class as an implementation of _StompSessionHandler_ interface - for handling STOMP session events 405 | * the _SockJsClient_ class with selected transports as an implementation of _WebSocketClient_ interface - to provide transports to connect to the WebSocket/SockJS server 406 | * the _WebSocketStompClient_ class - to connect to a STOMP server using the given URL with the provided transports and to handle STOMP session events in the provided event handler. 407 | 408 | The _SockJsClient_ object uses two transports: 409 | 410 | * the _WebSocketTransport_ object, which supports SockJS _WebSocket_ transport 411 | * the _RestTemplateXhrTransport_ object, which supports SockJS _XhrStreaming_ and _XhrPolling_ transports 412 | 413 | ``` 414 | @Configuration 415 | public class ClientWebSocketSockJsStompConfig { 416 | 417 | @Bean 418 | public WebSocketStompClient webSocketStompClient(WebSocketClient webSocketClient, 419 | StompSessionHandler stompSessionHandler) { 420 | WebSocketStompClient webSocketStompClient = new WebSocketStompClient(webSocketClient); 421 | webSocketStompClient.setMessageConverter(new StringMessageConverter()); 422 | webSocketStompClient.connect("http://localhost:8080/websocket-sockjs-stomp", stompSessionHandler); 423 | return webSocketStompClient; 424 | } 425 | 426 | @Bean 427 | public WebSocketClient webSocketClient() { 428 | List transports = new ArrayList<>(); 429 | transports.add(new WebSocketTransport(new StandardWebSocketClient())); 430 | transports.add(new RestTemplateXhrTransport()); 431 | return new SockJsClient(transports); 432 | } 433 | 434 | @Bean 435 | public StompSessionHandler stompSessionHandler() { 436 | return new ClientStompSessionHandler(); 437 | } 438 | } 439 | ``` 440 | 441 | The client is a console Spring Boot application without Spring Web MVC. 442 | 443 | ``` 444 | @SpringBootApplication 445 | public class ClientWebSocketSockJsStompApplication { 446 | 447 | public static void main(String[] args) { 448 | new SpringApplicationBuilder(ClientWebSocketSockJsStompApplication.class) 449 | .web(WebApplicationType.NONE) 450 | .run(args); 451 | } 452 | } 453 | ``` 454 | 455 | ## Conclusion 456 | 457 | Because WebSocket provides full-duplex communication for the Web, it is a good choice to implement various messaging protocols on top of it. Among STOPM, there are [officially registered](https://www.iana.org/assignments/websocket/websocket.xhtml#subprotocol-name) several messaging subprotocols that work over WebSocket, among them: 458 | 459 | * AMQP (Advanced Message Queuing Protocol) - another protocol to communicate between clients and message brokers 460 | * MSRP (Message Session Relay Protocol) - a protocol for transmitting a series of related instant messages during a session 461 | * WAMP (Web Application Messaging Protocol) - a general-purpose messaging protocol for publishing-subscribe communication and remote procedure calls 462 | * XMPP (Extensible Messaging and Presence Protocol) - a protocol for near real-time instant messaging, presence information, and contact list maintenance 463 | 464 | Before implementing your own subprotocol on top of WebSocket, try to reuse an existing protocol and its client and server libraries - you can save a lot of time and avoid many design and implementation errors. 465 | 466 | Complete code examples are available in [the GitHub repository](https://github.com/aliakh/demo-spring-websocket/tree/master/websocket-sockjs-stomp-server). 467 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | sourceCompatibility = '11' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | sourceCompatibility = '11' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version:'2.2.2.RELEASE' 13 | implementation group: 'org.webjars', name: 'jquery', version:'3.4.1' 14 | } 15 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/src/main/java/demo/cross_origin_requests/foreign_origin/CdmController.java: -------------------------------------------------------------------------------- 1 | package demo.cross_origin_requests.foreign_origin; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import java.time.LocalTime; 8 | 9 | @RestController 10 | @RequestMapping 11 | public class CdmController { 12 | 13 | @GetMapping(value = "/cdm-server") 14 | public String getTime() { 15 | return LocalTime.now().toString(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/src/main/java/demo/cross_origin_requests/foreign_origin/CorsController.java: -------------------------------------------------------------------------------- 1 | package demo.cross_origin_requests.foreign_origin; 2 | 3 | import org.springframework.web.bind.annotation.CrossOrigin; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import java.time.LocalTime; 9 | 10 | @RestController 11 | @RequestMapping 12 | public class CorsController { 13 | 14 | @GetMapping(value = "/time-without-origin") 15 | public String getTimeNoOrigin() { 16 | return LocalTime.now().toString(); 17 | } 18 | 19 | @CrossOrigin(origins = "*") 20 | @GetMapping(value = "/time-any-origin") 21 | public String getTimeAnyOrigin() { 22 | return LocalTime.now().toString(); 23 | } 24 | 25 | @CrossOrigin(origins = "http://localhost:8001") 26 | @GetMapping(value = "/time-explicit-origin") 27 | public String getTimeExplicitOrigin() { 28 | return LocalTime.now().toString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/src/main/java/demo/cross_origin_requests/foreign_origin/CrossOriginRequestsForeignOriginApplication.java: -------------------------------------------------------------------------------- 1 | package demo.cross_origin_requests.foreign_origin; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class CrossOriginRequestsForeignOriginApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(CrossOriginRequestsForeignOriginApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/src/main/java/demo/cross_origin_requests/foreign_origin/JsonpController.java: -------------------------------------------------------------------------------- 1 | package demo.cross_origin_requests.foreign_origin; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | 10 | import java.time.LocalTime; 11 | 12 | @Controller 13 | public class JsonpController { 14 | 15 | @RequestMapping(value = "/time", produces = MediaType.APPLICATION_JSON_VALUE) 16 | public ResponseEntity getTime(@RequestParam("callback") String callback) { 17 | String json = String.format("{'time': '%s'}", LocalTime.now().toString()); 18 | String response = String.format("%s(%s)", callback, json); 19 | return new ResponseEntity<>(response, HttpStatus.OK); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/src/main/java/demo/cross_origin_requests/foreign_origin/XfoController.java: -------------------------------------------------------------------------------- 1 | package demo.cross_origin_requests.foreign_origin; 2 | 3 | import org.springframework.http.MediaType; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.ResponseBody; 7 | 8 | import javax.servlet.http.HttpServletResponse; 9 | 10 | @Controller 11 | public class XfoController { 12 | 13 | public static final String HTML = "X-Frame-Options exampleHello!"; 14 | 15 | @GetMapping(value = "/html-no-xfo", produces = MediaType.TEXT_HTML_VALUE) 16 | @ResponseBody 17 | public String getHtmlNoXfo() { 18 | return HTML; 19 | } 20 | 21 | @GetMapping(value = "/html-xfo-deny", produces = MediaType.TEXT_HTML_VALUE) 22 | @ResponseBody 23 | public String getHtmlXfoDeny(HttpServletResponse response) { 24 | response.setHeader("X-Frame-Options", "DENY"); 25 | return HTML; 26 | } 27 | 28 | @GetMapping(value = "/html-xfo-sameorigin", produces = MediaType.TEXT_HTML_VALUE) 29 | @ResponseBody 30 | public String getHtmlXfoSameOrigin(HttpServletResponse response) { 31 | response.setHeader("X-Frame-Options", "SAMEORIGIN"); 32 | return HTML; 33 | } 34 | 35 | @GetMapping(value = "/html-xfo-allowfrom", produces = MediaType.TEXT_HTML_VALUE) 36 | @ResponseBody 37 | public String getHtmlXfoAllowFrom(HttpServletResponse response) { 38 | response.setHeader("X-Frame-Options", "ALLOW-FROM http://localhost:8001"); 39 | return HTML; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode=off 2 | logging.level.root=INFO 3 | server.port=8002 4 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/src/main/resources/static/cdm-iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cross-document messaging example 6 | 7 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/src/main/resources/static/xfo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | X-Frame-Options example 6 | 7 | 8 |
9 | without X-Frame-Options 10 |
11 |
12 | 13 |
14 |
15 | X-Frame-Options=DENY 16 |
17 |
18 | 19 |
20 |
21 | X-Frame-Options=SAMEORIGIN 22 |
23 |
24 | 25 |
26 |
27 | X-Frame-Options=ALLOW-FROM http://localhost:8001 28 |
29 |
30 | 31 |
32 | 33 | -------------------------------------------------------------------------------- /cross-origin-requests-foreign-origin/src/main/resources/views/framed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Iframe example 6 | 7 | 8 | Iframe body 9 | 10 | 11 | -------------------------------------------------------------------------------- /cross-origin-requests-local-origin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | sourceCompatibility = '11' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version:'2.2.2.RELEASE' 13 | implementation group: 'org.webjars', name: 'jquery', version:'3.4.1' 14 | } 15 | -------------------------------------------------------------------------------- /cross-origin-requests-local-origin/src/main/java/demo/cross_origin_requests/local_origin/CrossOriginRequestsLocalOriginApplication.java: -------------------------------------------------------------------------------- 1 | package demo.cross_origin_requests.local_origin; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class CrossOriginRequestsLocalOriginApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(CrossOriginRequestsLocalOriginApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cross-origin-requests-local-origin/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode=off 2 | logging.level.root=INFO 3 | server.port=8001 4 | -------------------------------------------------------------------------------- /cross-origin-requests-local-origin/src/main/resources/static/cdm-window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cross-document messaging example 6 | 19 | 20 | 21 |
22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /cross-origin-requests-local-origin/src/main/resources/static/cors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CORS example 6 | 7 | 8 | 32 | 33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /cross-origin-requests-local-origin/src/main/resources/static/jsonp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSONP example 6 | 18 | 19 | 20 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /cross-origin-requests-local-origin/src/main/resources/static/xfo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | X-Frame-Options example 6 | 7 | 8 |
9 | without X-Frame-Options 10 |
11 |
12 | 13 |
14 |
15 | X-Frame-Options=DENY 16 |
17 |
18 | 19 |
20 |
21 | X-Frame-Options=SAMEORIGIN 22 |
23 |
24 | 25 |
26 |
27 | X-Frame-Options=ALLOW-FROM http://localhost:8001 28 |
29 |
30 | 31 |
32 | 33 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /images/HTTP long polling.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/HTTP long polling.vsdx -------------------------------------------------------------------------------- /images/HTTP polling.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/HTTP polling.vsdx -------------------------------------------------------------------------------- /images/HTTP streaming.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/HTTP streaming.vsdx -------------------------------------------------------------------------------- /images/HTTP_long_polling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/HTTP_long_polling.png -------------------------------------------------------------------------------- /images/HTTP_polling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/HTTP_polling.png -------------------------------------------------------------------------------- /images/HTTP_streaming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/HTTP_streaming.png -------------------------------------------------------------------------------- /images/STOMP broker acknowledgment.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include header.iuml 3 | 4 | client->>broker : 5 | note left 6 | receipt:rcp-0 7 | end note 8 | 9 | broker->>client: RECEIPT 10 | note right 11 | receipt-id:rcp-0 12 | end note 13 | 14 | @enduml -------------------------------------------------------------------------------- /images/STOMP client acknowledgment.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include header.iuml 3 | 4 | client->>broker: SUBSCRIBE 5 | note left 6 | destination:/topic/tpc-0 7 | id:sub-0 8 | ask:client 9 | end note 10 | 11 | broker->>client: MESSAGE 12 | note right 13 | destination:/topic/tpc-0 14 | subscription:sub-0 15 | message-id:msg-0 16 | ask:id-0 17 | end note 18 | 19 | alt success 20 | client->>broker: ASK 21 | note left 22 | id:id-0 23 | end note 24 | else failure 25 | client->>broker: NAK 26 | note left 27 | id:id-0 28 | end note 29 | end 30 | 31 | @enduml -------------------------------------------------------------------------------- /images/STOMP connecting.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include header.iuml 3 | 4 | client->>broker: CONNECT 5 | note left 6 | accept-version:1.2,1.1,1.0 7 | host:... 8 | login:... 9 | passcode:... 10 | heart-beat:10000,10000 11 | end note 12 | 13 | broker->>client: CONNECTED 14 | note right 15 | version:1.2 16 | session:... 17 | server:... 18 | heart-beat:0,0 19 | end note 20 | 21 | client->>broker: DISCONNECT 22 | note left 23 | receipt:rcp-0 24 | end note 25 | 26 | broker->>client: RECEIPT 27 | note right 28 | receipt-id:rcp-0 29 | end note 30 | 31 | @enduml -------------------------------------------------------------------------------- /images/STOMP sending.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include header.iuml 3 | 4 | client->>broker: SEND 5 | note left 6 | destination:/queue/dst-0 7 | end note 8 | 9 | client->>broker: SEND 10 | note left 11 | destination:/queue/dst-1 12 | content-length:4 13 | content-type:text/plain 14 | 15 | body 16 | end note 17 | 18 | @enduml -------------------------------------------------------------------------------- /images/STOMP subscribing.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include header.iuml 3 | 4 | client->>broker: SUBSCRIBE 5 | note left 6 | destination:/topic/dst-0 7 | id:sub-0 8 | ask:auto 9 | end note 10 | 11 | broker->>client: MESSAGE 12 | note right 13 | destination:/topic/dst-0 14 | subscription:sub-0 15 | message-id:msg-0 16 | end note 17 | broker->>client: MESSAGE 18 | note right 19 | destination:/topic/dst-0 20 | subscription:sub-0 21 | message-id:msg-1 22 | end note 23 | 24 | client->>broker: UNSUBSCRIBE 25 | note left 26 | id:sub-0 27 | end note 28 | 29 | @enduml -------------------------------------------------------------------------------- /images/STOMP_broker_acknowledgment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/STOMP_broker_acknowledgment.png -------------------------------------------------------------------------------- /images/STOMP_client_acknowledgment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/STOMP_client_acknowledgment.png -------------------------------------------------------------------------------- /images/STOMP_connecting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/STOMP_connecting.png -------------------------------------------------------------------------------- /images/STOMP_sending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/STOMP_sending.png -------------------------------------------------------------------------------- /images/STOMP_subscribing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/STOMP_subscribing.png -------------------------------------------------------------------------------- /images/WebSocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/WebSocket.png -------------------------------------------------------------------------------- /images/WebSocket.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/WebSocket.vsdx -------------------------------------------------------------------------------- /images/browser-sockjs-websocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/browser-sockjs-websocket.png -------------------------------------------------------------------------------- /images/browser-sockjs-xhr-polling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/browser-sockjs-xhr-polling.png -------------------------------------------------------------------------------- /images/browser-sockjs-xhr-streaming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/browser-sockjs-xhr-streaming.png -------------------------------------------------------------------------------- /images/browser-stomp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/browser-stomp.png -------------------------------------------------------------------------------- /images/browser-websocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/browser-websocket.png -------------------------------------------------------------------------------- /images/caniuse.com-websockets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliakh/demo-spring-websocket/37cab129458e76a5829b8857933f0471383f766e/images/caniuse.com-websockets.png -------------------------------------------------------------------------------- /images/header.iuml: -------------------------------------------------------------------------------- 1 | scale 1.1 2 | hide footbox 3 | skinparam monochrome true 4 | skinparam defaultFontName Source Sans Pro 5 | skinparam titleFontSize 15 6 | skinparam sequenceMessageAlign left 7 | skinparam participantBackgroundColor #white 8 | skinparam noteBackgroundColor #white 9 | skinparam sequenceGroupBackgroundColor #white 10 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo-spring-websocket' 2 | 3 | include ':websocket-server' 4 | include ':websocket-client' 5 | 6 | include ':cross-origin-requests-local-origin' 7 | include ':cross-origin-requests-foreign-origin' 8 | 9 | include ':websocket-sockjs-server' 10 | include ':websocket-sockjs-client' 11 | 12 | include ':websocket-sockjs-stomp-server' 13 | include ':websocket-sockjs-stomp-client' 14 | 15 | include ':websocket-sockjs-stomp-highcharts' 16 | -------------------------------------------------------------------------------- /websocket-client/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.3.4.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.10.RELEASE' 4 | id 'java' 5 | } 6 | 7 | sourceCompatibility = '11' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 15 | } 16 | -------------------------------------------------------------------------------- /websocket-client/src/main/java/demo/websocket/client/example1/ClientWebSocketApplication.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.client.example1; 2 | 3 | import org.springframework.boot.WebApplicationType; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | 7 | @SpringBootApplication 8 | public class ClientWebSocketApplication { 9 | 10 | public static void main(String[] args) { 11 | new SpringApplicationBuilder(ClientWebSocketApplication.class) 12 | .web(WebApplicationType.NONE) 13 | .run(args); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /websocket-client/src/main/java/demo/websocket/client/example1/ClientWebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.client.example1; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.WebSocketHandler; 6 | import org.springframework.web.socket.client.WebSocketClient; 7 | import org.springframework.web.socket.client.WebSocketConnectionManager; 8 | import org.springframework.web.socket.client.standard.StandardWebSocketClient; 9 | 10 | @Configuration 11 | public class ClientWebSocketConfig { 12 | 13 | @Bean 14 | public WebSocketConnectionManager webSocketConnectionManager() { 15 | WebSocketConnectionManager manager = new WebSocketConnectionManager( 16 | webSocketClient(), 17 | webSocketHandler(), 18 | "ws://localhost:8080/websocket" 19 | ); 20 | manager.setAutoStartup(true); 21 | return manager; 22 | } 23 | 24 | @Bean 25 | public WebSocketClient webSocketClient() { 26 | return new StandardWebSocketClient(); 27 | } 28 | 29 | @Bean 30 | public WebSocketHandler webSocketHandler() { 31 | return new ClientWebSocketHandler(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /websocket-client/src/main/java/demo/websocket/client/example1/ClientWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.client.example1; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.web.socket.CloseStatus; 6 | import org.springframework.web.socket.TextMessage; 7 | import org.springframework.web.socket.WebSocketSession; 8 | import org.springframework.web.socket.handler.TextWebSocketHandler; 9 | 10 | public class ClientWebSocketHandler extends TextWebSocketHandler { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(ClientWebSocketHandler.class); 13 | 14 | @Override 15 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 16 | logger.info("Client connection opened"); 17 | 18 | TextMessage message = new TextMessage("one-time message from client"); 19 | logger.info("Client sends: {}", message); 20 | session.sendMessage(message); 21 | } 22 | 23 | @Override 24 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 25 | logger.info("Client connection closed: {}", status); 26 | } 27 | 28 | @Override 29 | public void handleTextMessage(WebSocketSession session, TextMessage message) { 30 | logger.info("Client received: {}", message); 31 | } 32 | 33 | @Override 34 | public void handleTransportError(WebSocketSession session, Throwable exception) { 35 | logger.info("Client transport error: {}", exception.getMessage()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /websocket-server/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.3.4.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.10.RELEASE' 4 | id 'java' 5 | } 6 | 7 | sourceCompatibility = '11' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 15 | implementation group: 'org.webjars', name: 'jquery', version:'3.4.1' 16 | implementation group: 'org.webjars', name: 'bootstrap', version:'4.4.1' 17 | } 18 | -------------------------------------------------------------------------------- /websocket-server/src/main/java/demo/websocket/server/example0/SecWebSocketAccept.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example0; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.security.MessageDigest; 5 | import java.security.NoSuchAlgorithmException; 6 | import java.util.Base64; 7 | 8 | public class SecWebSocketAccept { 9 | 10 | public static void main(String[] args) throws NoSuchAlgorithmException { 11 | String secWebSocketKey = "7c0RT+Z1px24ypyYfnPNbw=="; 12 | String secWebSocketAccept = Base64 13 | .getEncoder() 14 | .encodeToString( 15 | MessageDigest 16 | .getInstance("SHA-1") 17 | .digest((secWebSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") 18 | .getBytes(StandardCharsets.UTF_8) 19 | ) 20 | ); 21 | System.out.println(secWebSocketAccept); // O1a/o0MeFzoDgn+kCKR91UkYDO4= 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /websocket-server/src/main/java/demo/websocket/server/example1/SchedulerConfig.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example1; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.TaskScheduler; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 8 | 9 | @Configuration 10 | @EnableScheduling 11 | public class SchedulerConfig { 12 | 13 | @Bean 14 | public TaskScheduler taskScheduler() { 15 | ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); 16 | 17 | scheduler.setPoolSize(2); 18 | scheduler.setThreadNamePrefix("scheduling-"); 19 | scheduler.setDaemon(true); 20 | 21 | return scheduler; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /websocket-server/src/main/java/demo/websocket/server/example1/ServerWebSocketApplication.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example1; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ServerWebSocketApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ServerWebSocketApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /websocket-server/src/main/java/demo/websocket/server/example1/ServerWebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example1; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.WebSocketHandler; 6 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 7 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 8 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 9 | 10 | @Configuration 11 | @EnableWebSocket 12 | public class ServerWebSocketConfig implements WebSocketConfigurer { 13 | 14 | @Override 15 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 16 | registry.addHandler(webSocketHandler(), "/websocket"); 17 | } 18 | 19 | @Bean 20 | public WebSocketHandler webSocketHandler() { 21 | return new ServerWebSocketHandler(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /websocket-server/src/main/java/demo/websocket/server/example1/ServerWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example1; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.scheduling.annotation.Scheduled; 6 | import org.springframework.web.socket.CloseStatus; 7 | import org.springframework.web.socket.SubProtocolCapable; 8 | import org.springframework.web.socket.TextMessage; 9 | import org.springframework.web.socket.WebSocketSession; 10 | import org.springframework.web.socket.handler.TextWebSocketHandler; 11 | import org.springframework.web.util.HtmlUtils; 12 | 13 | import java.io.IOException; 14 | import java.time.LocalTime; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Set; 18 | import java.util.concurrent.CopyOnWriteArraySet; 19 | 20 | public class ServerWebSocketHandler extends TextWebSocketHandler implements SubProtocolCapable { 21 | 22 | private static final Logger logger = LoggerFactory.getLogger(ServerWebSocketHandler.class); 23 | 24 | private final Set sessions = new CopyOnWriteArraySet<>(); 25 | 26 | @Override 27 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 28 | logger.info("Server connection opened"); 29 | sessions.add(session); 30 | 31 | TextMessage message = new TextMessage("one-time message from server"); 32 | logger.info("Server sends: {}", message); 33 | session.sendMessage(message); 34 | } 35 | 36 | @Override 37 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 38 | logger.info("Server connection closed: {}", status); 39 | sessions.remove(session); 40 | } 41 | 42 | @Scheduled(fixedRate = 10000) 43 | void sendPeriodicMessages() throws IOException { 44 | for (WebSocketSession session : sessions) { 45 | if (session.isOpen()) { 46 | String broadcast = "server periodic message " + LocalTime.now(); 47 | logger.info("Server sends: {}", broadcast); 48 | session.sendMessage(new TextMessage(broadcast)); 49 | } 50 | } 51 | } 52 | 53 | @Override 54 | public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 55 | String request = message.getPayload(); 56 | logger.info("Server received: {}", request); 57 | 58 | String response = String.format("response from server to '%s'", HtmlUtils.htmlEscape(request)); 59 | logger.info("Server sends: {}", response); 60 | session.sendMessage(new TextMessage(response)); 61 | } 62 | 63 | @Override 64 | public void handleTransportError(WebSocketSession session, Throwable exception) { 65 | logger.info("Server transport error: {}", exception.getMessage()); 66 | } 67 | 68 | @Override 69 | public List getSubProtocols() { 70 | return Collections.singletonList("subprotocol.demo.websocket"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /websocket-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode=off 2 | logging.level.root=INFO 3 | -------------------------------------------------------------------------------- /websocket-server/src/main/resources/static/css/application.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f5f5f5; 3 | } 4 | 5 | #main-content { 6 | max-width: 940px; 7 | padding: 2em 3em; 8 | margin: 0 auto 20px; 9 | background-color: #fff; 10 | border: 1px solid #e5e5e5; 11 | -webkit-border-radius: 5px; 12 | -moz-border-radius: 5px; 13 | border-radius: 5px; 14 | } 15 | -------------------------------------------------------------------------------- /websocket-server/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket example 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
Responses
43 |
44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /websocket-server/src/main/resources/static/js/application.js: -------------------------------------------------------------------------------- 1 | var webSocket = null; 2 | 3 | function setConnected(connected) { 4 | $("#connect").prop("disabled", connected); 5 | $("#disconnect").prop("disabled", !connected); 6 | $("#send").prop("disabled", !connected); 7 | 8 | if (connected) { 9 | $("#conversation").show(); 10 | } else { 11 | $("#conversation").hide(); 12 | } 13 | 14 | $("#responses").html(""); 15 | } 16 | 17 | function connect() { 18 | webSocket = new WebSocket('ws://localhost:8080/websocket', 19 | 'subprotocol.demo.websocket'); 20 | 21 | webSocket.onopen = function () { 22 | setConnected(true); 23 | log('Client connection opened'); 24 | 25 | console.log('Subprotocol: ' + webSocket.protocol); 26 | console.log('Extensions: ' + webSocket.extensions); 27 | }; 28 | 29 | webSocket.onmessage = function (event) { 30 | log('Client received: ' + event.data); 31 | }; 32 | 33 | webSocket.onerror = function (event) { 34 | log('Client error: ' + event); 35 | }; 36 | 37 | webSocket.onclose = function (event) { 38 | setConnected(false); 39 | log('Client connection closed: ' + event.code); 40 | }; 41 | } 42 | 43 | function disconnect() { 44 | if (webSocket != null) { 45 | webSocket.close(); 46 | webSocket = null; 47 | } 48 | setConnected(false); 49 | } 50 | 51 | function send() { 52 | const message = $("#request").val(); 53 | log('Client sends: ' + message); 54 | webSocket.send(message); 55 | } 56 | 57 | function log(message) { 58 | $("#responses").append("" + message + ""); 59 | console.log(message); 60 | } 61 | 62 | $(function () { 63 | $("form").on('submit', function (e) { 64 | e.preventDefault(); 65 | }); 66 | $("#connect").click(function () { 67 | connect(); 68 | }); 69 | $("#disconnect").click(function () { 70 | disconnect(); 71 | }); 72 | $("#send").click(function () { 73 | send(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /websocket-sockjs-client/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.3.4.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.10.RELEASE' 4 | id 'java' 5 | } 6 | 7 | sourceCompatibility = '11' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 15 | } 16 | -------------------------------------------------------------------------------- /websocket-sockjs-client/src/main/java/demo/websocket/client/example2/ClientWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.client.example2; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.web.socket.CloseStatus; 6 | import org.springframework.web.socket.TextMessage; 7 | import org.springframework.web.socket.WebSocketSession; 8 | import org.springframework.web.socket.handler.TextWebSocketHandler; 9 | 10 | public class ClientWebSocketHandler extends TextWebSocketHandler { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(ClientWebSocketHandler.class); 13 | 14 | @Override 15 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 16 | logger.info("Client connection opened"); 17 | 18 | TextMessage message = new TextMessage("one-time message from client"); 19 | logger.info("Client sends: {}", message); 20 | session.sendMessage(message); 21 | } 22 | 23 | @Override 24 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 25 | logger.info("Client connection closed: {}", status); 26 | } 27 | 28 | @Override 29 | public void handleTextMessage(WebSocketSession session, TextMessage message) { 30 | logger.info("Client received: {}", message); 31 | } 32 | 33 | @Override 34 | public void handleTransportError(WebSocketSession session, Throwable exception) { 35 | logger.info("Client transport error: {}", exception.getMessage()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /websocket-sockjs-client/src/main/java/demo/websocket/client/example2/ClientWebSocketSockJsApplication.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.client.example2; 2 | 3 | import org.springframework.boot.WebApplicationType; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | 7 | @SpringBootApplication 8 | public class ClientWebSocketSockJsApplication { 9 | 10 | public static void main(String[] args) { 11 | new SpringApplicationBuilder(ClientWebSocketSockJsApplication.class) 12 | .web(WebApplicationType.NONE) 13 | .run(args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /websocket-sockjs-client/src/main/java/demo/websocket/client/example2/ClientWebSocketSockJsConfig.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.client.example2; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.WebSocketHandler; 6 | import org.springframework.web.socket.client.WebSocketClient; 7 | import org.springframework.web.socket.client.WebSocketConnectionManager; 8 | import org.springframework.web.socket.client.standard.StandardWebSocketClient; 9 | import org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport; 10 | import org.springframework.web.socket.sockjs.client.SockJsClient; 11 | import org.springframework.web.socket.sockjs.client.Transport; 12 | import org.springframework.web.socket.sockjs.client.WebSocketTransport; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | @Configuration 18 | public class ClientWebSocketSockJsConfig { 19 | 20 | @Bean 21 | public WebSocketConnectionManager webSocketConnectionManager() { 22 | WebSocketConnectionManager manager = new WebSocketConnectionManager( 23 | webSocketClient(), 24 | webSocketHandler(), 25 | "http://localhost:8080/websocket-sockjs" 26 | ); 27 | manager.setAutoStartup(true); 28 | return manager; 29 | } 30 | 31 | @Bean 32 | public WebSocketClient webSocketClient() { 33 | List transports = new ArrayList<>(); 34 | transports.add(new WebSocketTransport(new StandardWebSocketClient())); 35 | transports.add(new RestTemplateXhrTransport()); 36 | return new SockJsClient(transports); 37 | } 38 | 39 | @Bean 40 | public WebSocketHandler webSocketHandler() { 41 | return new ClientWebSocketHandler(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /websocket-sockjs-server/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.3.4.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.10.RELEASE' 4 | id 'java' 5 | } 6 | 7 | sourceCompatibility = '11' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 15 | implementation group: 'org.webjars', name: 'sockjs-client', version:'1.1.2' 16 | implementation group: 'org.webjars', name: 'jquery', version:'3.4.1' 17 | implementation group: 'org.webjars', name: 'bootstrap', version:'4.4.1' 18 | } 19 | -------------------------------------------------------------------------------- /websocket-sockjs-server/src/main/java/demo/websocket/server/example2/ServerWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example2; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.scheduling.annotation.Scheduled; 6 | import org.springframework.web.socket.CloseStatus; 7 | import org.springframework.web.socket.SubProtocolCapable; 8 | import org.springframework.web.socket.TextMessage; 9 | import org.springframework.web.socket.WebSocketSession; 10 | import org.springframework.web.socket.handler.TextWebSocketHandler; 11 | import org.springframework.web.util.HtmlUtils; 12 | 13 | import java.io.IOException; 14 | import java.time.LocalTime; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Set; 18 | import java.util.concurrent.CopyOnWriteArraySet; 19 | 20 | public class ServerWebSocketHandler extends TextWebSocketHandler implements SubProtocolCapable { 21 | 22 | private static final Logger logger = LoggerFactory.getLogger(ServerWebSocketHandler.class); 23 | 24 | private final Set sessions = new CopyOnWriteArraySet<>(); 25 | 26 | @Override 27 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 28 | logger.info("Server connection opened"); 29 | sessions.add(session); 30 | 31 | TextMessage message = new TextMessage("one-time message from server"); 32 | logger.info("Server sends: {}", message); 33 | session.sendMessage(message); 34 | } 35 | 36 | @Override 37 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 38 | logger.info("Server connection closed: {}", status); 39 | sessions.remove(session); 40 | } 41 | 42 | @Scheduled(fixedRate = 10000) 43 | void sendPeriodicMessages() throws IOException { 44 | for (WebSocketSession session : sessions) { 45 | if (session.isOpen()) { 46 | String broadcast = "server periodic message " + LocalTime.now(); 47 | logger.info("Server sends: {}", broadcast); 48 | session.sendMessage(new TextMessage(broadcast)); 49 | } 50 | } 51 | } 52 | 53 | @Override 54 | public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 55 | String request = message.getPayload(); 56 | logger.info("Server received: {}", request); 57 | 58 | String response = String.format("response from server to '%s'", HtmlUtils.htmlEscape(request)); 59 | logger.info("Server sends: {}", response); 60 | session.sendMessage(new TextMessage(response)); 61 | } 62 | 63 | @Override 64 | public void handleTransportError(WebSocketSession session, Throwable exception) { 65 | logger.info("Server transport error: {}", exception.getMessage()); 66 | } 67 | 68 | @Override 69 | public List getSubProtocols() { 70 | return Collections.singletonList("subprotocol.demo.websocket"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /websocket-sockjs-server/src/main/java/demo/websocket/server/example2/ServerWebSocketSockJsApplicaion.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example2; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | public class ServerWebSocketSockJsApplicaion { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ServerWebSocketSockJsApplicaion.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /websocket-sockjs-server/src/main/java/demo/websocket/server/example2/ServerWebSocketSockJsConfig.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example2; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.WebSocketHandler; 6 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 7 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 8 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 9 | 10 | @Configuration 11 | @EnableWebSocket 12 | public class ServerWebSocketSockJsConfig implements WebSocketConfigurer { 13 | 14 | @Override 15 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 16 | registry.addHandler(webSocketHandler(), "/websocket-sockjs") 17 | .setAllowedOrigins("*") 18 | .withSockJS() 19 | .setWebSocketEnabled(true) 20 | .setHeartbeatTime(25000) 21 | .setDisconnectDelay(5000) 22 | .setClientLibraryUrl("/webjars/sockjs-client/1.1.2/sockjs.js") 23 | .setSessionCookieNeeded(false); 24 | } 25 | 26 | @Bean 27 | public WebSocketHandler webSocketHandler() { 28 | return new ServerWebSocketHandler(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /websocket-sockjs-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode=off 2 | logging.level.root=INFO 3 | logging.level.org.springframework.web.socket=TRACE 4 | -------------------------------------------------------------------------------- /websocket-sockjs-server/src/main/resources/static/css/application.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f5f5f5; 3 | } 4 | 5 | #main-content { 6 | max-width: 940px; 7 | padding: 2em 3em; 8 | margin: 0 auto 20px; 9 | background-color: #fff; 10 | border: 1px solid #e5e5e5; 11 | -webkit-border-radius: 5px; 12 | -moz-border-radius: 5px; 13 | border-radius: 5px; 14 | } 15 | -------------------------------------------------------------------------------- /websocket-sockjs-server/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket/SockJS example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 | SockJS transport: 37 | 51 |
52 |
53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
Responses
65 |
66 |
67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /websocket-sockjs-server/src/main/resources/static/js/application.js: -------------------------------------------------------------------------------- 1 | let sockJS = null; 2 | 3 | function setConnected(connected) { 4 | $("#connect").prop("disabled", connected); 5 | $("#disconnect").prop("disabled", !connected); 6 | $("#send").prop("disabled", !connected); 7 | 8 | if (connected) { 9 | $("#conversation").show(); 10 | } else { 11 | $("#conversation").hide(); 12 | } 13 | 14 | $("#responses").html(""); 15 | } 16 | 17 | function connect() { 18 | const option = $("#transports").find('option:selected').val(); 19 | console.log('Option: ' + option); 20 | 21 | const transports = (option === 'all') ? [] : [option]; 22 | console.log('Transports: ' + transports); 23 | 24 | sockJS = new SockJS('http://localhost:8080/websocket-sockjs', 25 | 'subprotocol.demo.websocket', {debug: true, transports: transports}); 26 | 27 | sockJS.onopen = function () { 28 | setConnected(true); 29 | log('Client connection opened'); 30 | 31 | console.log('Subprotocol: ' + sockJS.protocol); 32 | console.log('Extensions: ' + sockJS.extensions); 33 | }; 34 | 35 | sockJS.onmessage = function (event) { 36 | log('Client received: ' + event.data); 37 | }; 38 | 39 | sockJS.onerror = function (event) { 40 | log('Client error: ' + event); 41 | }; 42 | 43 | sockJS.onclose = function (event) { 44 | setConnected(false); 45 | log('Client connection closed: ' + event.code); 46 | }; 47 | } 48 | 49 | function disconnect() { 50 | if (sockJS != null) { 51 | sockJS.close(); 52 | sockJS = null; 53 | } 54 | setConnected(false); 55 | } 56 | 57 | function send() { 58 | const message = $("#request").val(); 59 | log('Client sends: ' + message); 60 | sockJS.send(message); 61 | } 62 | 63 | function log(message) { 64 | $("#responses").append("" + message + ""); 65 | console.log(message); 66 | } 67 | 68 | $(function () { 69 | $("form").on('submit', function (e) { 70 | e.preventDefault(); 71 | }); 72 | $("#connect").click(function () { 73 | connect(); 74 | }); 75 | $("#disconnect").click(function () { 76 | disconnect(); 77 | }); 78 | $("#send").click(function () { 79 | send(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-client/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.3.4.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.10.RELEASE' 4 | id 'java' 5 | } 6 | 7 | sourceCompatibility = '11' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 15 | } 16 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-client/src/main/java/demo/websocket/client/example3/ClientStompSessionHandler.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.client.example3; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.messaging.simp.stomp.StompCommand; 6 | import org.springframework.messaging.simp.stomp.StompHeaders; 7 | import org.springframework.messaging.simp.stomp.StompSession; 8 | import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; 9 | 10 | public class ClientStompSessionHandler extends StompSessionHandlerAdapter { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(ClientStompSessionHandler.class); 13 | 14 | @Override 15 | public void afterConnected(StompSession session, StompHeaders headers) { 16 | logger.info("Client connected: headers {}", headers); 17 | 18 | session.subscribe("/app/subscribe", this); 19 | session.subscribe("/queue/responses", this); 20 | session.subscribe("/queue/errors", this); 21 | session.subscribe("/topic/periodic", this); 22 | 23 | String message = "one-time message from client"; 24 | logger.info("Client sends: {}", message); 25 | session.send("/app/request", message); 26 | } 27 | 28 | @Override 29 | public void handleFrame(StompHeaders headers, Object payload) { 30 | logger.info("Client received: payload {}, headers {}", payload, headers); 31 | } 32 | 33 | @Override 34 | public void handleException(StompSession session, StompCommand command, 35 | StompHeaders headers, byte[] payload, Throwable exception) { 36 | logger.error("Client error: exception {}, command {}, payload {}, headers {}", 37 | exception.getMessage(), command, payload, headers); 38 | } 39 | 40 | @Override 41 | public void handleTransportError(StompSession session, Throwable exception) { 42 | logger.error("Client transport error: error {}", exception.getMessage()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-client/src/main/java/demo/websocket/client/example3/ClientWebSocketSockJsStompApplication.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.client.example3; 2 | 3 | import org.springframework.boot.WebApplicationType; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | 7 | @SpringBootApplication 8 | public class ClientWebSocketSockJsStompApplication { 9 | 10 | public static void main(String[] args) { 11 | new SpringApplicationBuilder(ClientWebSocketSockJsStompApplication.class) 12 | .web(WebApplicationType.NONE) 13 | .run(args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-client/src/main/java/demo/websocket/client/example3/ClientWebSocketSockJsStompConfig.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.client.example3; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.messaging.converter.StringMessageConverter; 6 | import org.springframework.messaging.simp.stomp.StompSessionHandler; 7 | import org.springframework.web.socket.client.WebSocketClient; 8 | import org.springframework.web.socket.client.standard.StandardWebSocketClient; 9 | import org.springframework.web.socket.messaging.WebSocketStompClient; 10 | import org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport; 11 | import org.springframework.web.socket.sockjs.client.SockJsClient; 12 | import org.springframework.web.socket.sockjs.client.Transport; 13 | import org.springframework.web.socket.sockjs.client.WebSocketTransport; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | @Configuration 19 | public class ClientWebSocketSockJsStompConfig { 20 | 21 | @Bean 22 | public WebSocketStompClient webSocketStompClient(WebSocketClient webSocketClient, 23 | StompSessionHandler stompSessionHandler) { 24 | WebSocketStompClient webSocketStompClient = new WebSocketStompClient(webSocketClient); 25 | webSocketStompClient.setMessageConverter(new StringMessageConverter()); 26 | webSocketStompClient.connect("http://localhost:8080/websocket-sockjs-stomp", stompSessionHandler); 27 | return webSocketStompClient; 28 | } 29 | 30 | @Bean 31 | public WebSocketClient webSocketClient() { 32 | List transports = new ArrayList<>(); 33 | transports.add(new WebSocketTransport(new StandardWebSocketClient())); 34 | transports.add(new RestTemplateXhrTransport()); 35 | return new SockJsClient(transports); 36 | } 37 | 38 | @Bean 39 | public StompSessionHandler stompSessionHandler() { 40 | return new ClientStompSessionHandler(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.3.4.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.10.RELEASE' 4 | id 'java' 5 | } 6 | 7 | sourceCompatibility = '11' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 15 | implementation group: 'org.webjars', name: 'sockjs-client', version:'1.1.2' 16 | implementation group: 'org.webjars', name: 'stomp-websocket', version:'2.3.3-1' 17 | implementation group: 'org.webjars', name: 'jquery', version:'3.4.1' 18 | implementation group: 'org.webjars', name: 'highcharts', version:'5.0.14' 19 | } 20 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/PerformanceApplication.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | public class PerformanceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(PerformanceApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.config; 2 | 3 | import demo.websocket.server.example4.websocket.interceptor.LoggingChannelInterceptor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.messaging.simp.config.ChannelRegistration; 6 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 7 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 8 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 9 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 10 | 11 | @Configuration 12 | @EnableWebSocketMessageBroker 13 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 14 | 15 | @Override 16 | public void configureMessageBroker(MessageBrokerRegistry config) { 17 | config.enableSimpleBroker("/queue/", "/topic/"); 18 | config.setApplicationDestinationPrefixes("/app"); 19 | } 20 | 21 | @Override 22 | public void registerStompEndpoints(StompEndpointRegistry registry) { 23 | registry.addEndpoint("/performance").withSockJS(); 24 | } 25 | 26 | @Override 27 | public void configureClientInboundChannel(ChannelRegistration registration) { 28 | registration.interceptors(new LoggingChannelInterceptor()); 29 | } 30 | 31 | @Override 32 | public void configureClientOutboundChannel(ChannelRegistration registration) { 33 | registration.interceptors(new LoggingChannelInterceptor()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/controller/PerformanceController.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.controller; 2 | 3 | import demo.websocket.server.example4.domain.Performance; 4 | import demo.websocket.server.example4.service.PerformanceService; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.context.ApplicationListener; 8 | import org.springframework.messaging.core.MessageSendingOperations; 9 | import org.springframework.messaging.handler.annotation.MessageMapping; 10 | import org.springframework.messaging.handler.annotation.SendTo; 11 | import org.springframework.messaging.simp.annotation.SubscribeMapping; 12 | import org.springframework.messaging.simp.broker.BrokerAvailabilityEvent; 13 | import org.springframework.scheduling.annotation.Scheduled; 14 | import org.springframework.stereotype.Controller; 15 | 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.concurrent.atomic.AtomicBoolean; 19 | 20 | @Controller 21 | public class PerformanceController implements ApplicationListener { 22 | 23 | private static final Logger logger = LoggerFactory.getLogger(PerformanceController.class); 24 | 25 | private final PerformanceService performanceService; 26 | 27 | private final MessageSendingOperations messageSendingOperations; 28 | 29 | private final AtomicBoolean brokerAvailable = new AtomicBoolean(false); 30 | 31 | public PerformanceController(PerformanceService performanceService, MessageSendingOperations messageSendingOperations) { 32 | this.performanceService = performanceService; 33 | this.messageSendingOperations = messageSendingOperations; 34 | } 35 | 36 | @SubscribeMapping("/names") 37 | public List getNames() { 38 | return Arrays.asList( 39 | "committedVirtualMemorySize", 40 | "totalPhysicalMemorySize", 41 | "freePhysicalMemorySize", 42 | "totalSwapSpaceSize", 43 | "freePhysicalMemorySize" 44 | ); 45 | } 46 | 47 | @MessageMapping("/request") 48 | @SendTo("/queue/performance") 49 | public Performance onDemandPerformance() { 50 | return performanceService.getPerformance(); 51 | } 52 | 53 | @Override 54 | public void onApplicationEvent(BrokerAvailabilityEvent event) { 55 | logger.info("Broker availability event: {}", event); 56 | brokerAvailable.set(event.isBrokerAvailable()); 57 | logger.info("Broker is available: {}", brokerAvailable.get()); 58 | } 59 | 60 | @Scheduled(fixedDelay = 5000) 61 | public void periodicPerformance() { 62 | if (brokerAvailable.get()) { 63 | messageSendingOperations.convertAndSend("/topic/performance", performanceService.getPerformance()); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/controller/WebSocketMessageBrokerStatsController.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | import org.springframework.web.socket.config.WebSocketMessageBrokerStats; 7 | 8 | @RestController 9 | public class WebSocketMessageBrokerStatsController { 10 | 11 | @Autowired 12 | private WebSocketMessageBrokerStats stats; 13 | 14 | @RequestMapping("/stats") 15 | public WebSocketMessageBrokerStats getStats() { 16 | return stats; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/domain/Performance.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.domain; 2 | 3 | import java.util.StringJoiner; 4 | 5 | public class Performance { 6 | 7 | private long time; 8 | 9 | private long committedVirtualMemorySize; 10 | 11 | private long totalSwapSpaceSize; 12 | private long freeSwapSpaceSize; 13 | 14 | private long totalPhysicalMemorySize; 15 | private long freePhysicalMemorySize; 16 | 17 | private double systemCpuLoad; 18 | private double processCpuLoad; 19 | 20 | public long getTime() { 21 | return time; 22 | } 23 | 24 | public void setTime(long time) { 25 | this.time = time; 26 | } 27 | 28 | public long getCommittedVirtualMemorySize() { 29 | return committedVirtualMemorySize; 30 | } 31 | 32 | public void setCommittedVirtualMemorySize(long committedVirtualMemorySize) { 33 | this.committedVirtualMemorySize = committedVirtualMemorySize; 34 | } 35 | 36 | public long getTotalSwapSpaceSize() { 37 | return totalSwapSpaceSize; 38 | } 39 | 40 | public void setTotalSwapSpaceSize(long totalSwapSpaceSize) { 41 | this.totalSwapSpaceSize = totalSwapSpaceSize; 42 | } 43 | 44 | public long getFreeSwapSpaceSize() { 45 | return freeSwapSpaceSize; 46 | } 47 | 48 | public void setFreeSwapSpaceSize(long freeSwapSpaceSize) { 49 | this.freeSwapSpaceSize = freeSwapSpaceSize; 50 | } 51 | 52 | public long getTotalPhysicalMemorySize() { 53 | return totalPhysicalMemorySize; 54 | } 55 | 56 | public void setTotalPhysicalMemorySize(long totalPhysicalMemorySize) { 57 | this.totalPhysicalMemorySize = totalPhysicalMemorySize; 58 | } 59 | 60 | public long getFreePhysicalMemorySize() { 61 | return freePhysicalMemorySize; 62 | } 63 | 64 | public void setFreePhysicalMemorySize(long freePhysicalMemorySize) { 65 | this.freePhysicalMemorySize = freePhysicalMemorySize; 66 | } 67 | 68 | public double getSystemCpuLoad() { 69 | return systemCpuLoad; 70 | } 71 | 72 | public void setSystemCpuLoad(double systemCpuLoad) { 73 | this.systemCpuLoad = systemCpuLoad; 74 | } 75 | 76 | public double getProcessCpuLoad() { 77 | return processCpuLoad; 78 | } 79 | 80 | public void setProcessCpuLoad(double processCpuLoad) { 81 | this.processCpuLoad = processCpuLoad; 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | return new StringJoiner(", ", Performance.class.getSimpleName() + "[", "]") 87 | .add("time=" + time) 88 | .add("committedVirtualMemorySize=" + committedVirtualMemorySize) 89 | .add("totalSwapSpaceSize=" + totalSwapSpaceSize) 90 | .add("freeSwapSpaceSize=" + freeSwapSpaceSize) 91 | .add("totalPhysicalMemorySize=" + totalPhysicalMemorySize) 92 | .add("freePhysicalMemorySize=" + freePhysicalMemorySize) 93 | .add("systemCpuLoad=" + systemCpuLoad) 94 | .add("processCpuLoad=" + processCpuLoad) 95 | .toString(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/service/PerformanceService.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.service; 2 | 3 | import com.sun.management.OperatingSystemMXBean; 4 | import demo.websocket.server.example4.domain.Performance; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.lang.management.ManagementFactory; 8 | import java.time.ZonedDateTime; 9 | 10 | @Service 11 | public class PerformanceService { 12 | 13 | private final OperatingSystemMXBean operatingSystemMXBean; 14 | 15 | PerformanceService() { 16 | this.operatingSystemMXBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); 17 | } 18 | 19 | public Performance getPerformance() { 20 | Performance performance = new Performance(); 21 | 22 | performance.setTime(ZonedDateTime.now().toInstant().toEpochMilli()); 23 | 24 | performance.setCommittedVirtualMemorySize(operatingSystemMXBean.getCommittedVirtualMemorySize()); 25 | 26 | performance.setTotalSwapSpaceSize(operatingSystemMXBean.getTotalSwapSpaceSize()); 27 | performance.setFreeSwapSpaceSize(operatingSystemMXBean.getFreeSwapSpaceSize()); 28 | 29 | performance.setTotalPhysicalMemorySize(operatingSystemMXBean.getTotalPhysicalMemorySize()); 30 | performance.setFreePhysicalMemorySize(operatingSystemMXBean.getFreePhysicalMemorySize()); 31 | 32 | performance.setSystemCpuLoad(operatingSystemMXBean.getSystemCpuLoad()); 33 | performance.setProcessCpuLoad(operatingSystemMXBean.getProcessCpuLoad()); 34 | 35 | return performance; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/websocket/interceptor/LoggingChannelInterceptor.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.websocket.interceptor; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.messaging.Message; 6 | import org.springframework.messaging.MessageChannel; 7 | import org.springframework.messaging.support.ChannelInterceptor; 8 | 9 | public class LoggingChannelInterceptor implements ChannelInterceptor { 10 | 11 | private static final Logger logger = LoggerFactory.getLogger(LoggingChannelInterceptor.class); 12 | 13 | @Override 14 | public Message preSend(Message message, MessageChannel channel) { 15 | logger.info("Before the message {} is send to {}", message, channel); 16 | return message; 17 | } 18 | 19 | @Override 20 | public void postSend(Message message, MessageChannel channel, boolean sent) { 21 | logger.info("After the message {} is send to {}", message, channel); 22 | } 23 | 24 | @Override 25 | public boolean preReceive(MessageChannel channel) { 26 | logger.info("Before a message is received from {}", channel); 27 | return true; 28 | } 29 | 30 | @Override 31 | public Message postReceive(Message message, MessageChannel channel) { 32 | logger.info("After the message {} is received from {}", message, channel); 33 | return message; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/websocket/listener/SessionConnectEventListener.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.websocket.listener; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.socket.messaging.SessionConnectEvent; 8 | 9 | @Component 10 | public class SessionConnectEventListener implements ApplicationListener { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(SessionConnectEventListener.class); 13 | 14 | @Override 15 | public void onApplicationEvent(SessionConnectEvent event) { 16 | logger.info("Session connects: {}", event); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/websocket/listener/SessionConnectedEventListener.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.websocket.listener; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.socket.messaging.SessionConnectedEvent; 8 | 9 | @Component 10 | public class SessionConnectedEventListener implements ApplicationListener { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(SessionConnectedEventListener.class); 13 | 14 | @Override 15 | public void onApplicationEvent(SessionConnectedEvent event) { 16 | logger.info("Session connected: {}", event); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/websocket/listener/SessionDisconnectEventListener.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.websocket.listener; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.socket.messaging.SessionDisconnectEvent; 8 | 9 | @Component 10 | public class SessionDisconnectEventListener implements ApplicationListener { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(SessionDisconnectEventListener.class); 13 | 14 | @Override 15 | public void onApplicationEvent(SessionDisconnectEvent event) { 16 | logger.info("Session disconnected: {}", event); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/websocket/listener/SessionSubscribeEventListener.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.websocket.listener; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.socket.messaging.SessionSubscribeEvent; 8 | 9 | @Component 10 | public class SessionSubscribeEventListener implements ApplicationListener { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(SessionSubscribeEventListener.class); 13 | 14 | @Override 15 | public void onApplicationEvent(SessionSubscribeEvent event) { 16 | logger.info("Session subscribed: {}", event); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/java/demo/websocket/server/example4/websocket/listener/SessionUnsubscribeEventListener.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example4.websocket.listener; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.socket.messaging.SessionUnsubscribeEvent; 8 | 9 | @Component 10 | public class SessionUnsubscribeEventListener implements ApplicationListener { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(SessionUnsubscribeEventListener.class); 13 | 14 | @Override 15 | public void onApplicationEvent(SessionUnsubscribeEvent event) { 16 | logger.info("Session unsubscribes: {}", event); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.root=info 2 | logging.level.org.springframework.web.socket=debug 3 | logging.level.org.springframework.messaging.simp=info 4 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/resources/static/css/application.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | } 5 | 6 | #performanceChart { 7 | height: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Performance charts with WebSocket example 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/resources/static/js/application.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | const stomp = Stomp.over(new SockJS('/performance')); 3 | 4 | stomp.connect({}, function (frame) { 5 | console.log('Client connected: ' + frame); 6 | 7 | stomp.subscribe("/app/names", function (message) { 8 | const names = JSON.parse(message.body); 9 | console.log('Names: ' + frame); 10 | 11 | createChart('performanceChart', names); 12 | stomp.subscribe("/topic/performance", function (message) { 13 | const performance = JSON.parse(message.body); 14 | updateChart(names, performance); 15 | }); 16 | 17 | stomp.subscribe("/queue/performance", function (message) { 18 | const performance = JSON.parse(message.body); 19 | updateChart(names, performance); 20 | }); 21 | 22 | $("#performanceChart").click(function () { 23 | stomp.send("/app/request", {}, {}) 24 | }); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-highcharts/src/main/resources/static/js/chart.js: -------------------------------------------------------------------------------- 1 | let chart; 2 | 3 | function createChart(id, names) { 4 | const series = []; 5 | 6 | let i; 7 | for (i = 0; i < names.length; i++) { 8 | series.push({ 9 | name: names[i], 10 | marker: {symbol: 'circle'}, 11 | data: [] 12 | }); 13 | } 14 | 15 | chart = Highcharts.chart(id, { 16 | chart: { 17 | type: 'line', 18 | }, 19 | title: { 20 | text: false 21 | }, 22 | xAxis: { 23 | type: 'datetime', 24 | minRange: 60 * 1000 25 | }, 26 | yAxis: { 27 | title: { 28 | text: false 29 | } 30 | }, 31 | legend: { 32 | layout: 'vertical', 33 | align: 'right', 34 | verticalAlign: 'middle' 35 | }, 36 | series: series 37 | }); 38 | } 39 | 40 | function updateChart(names, performance) { 41 | const time = performance.time; 42 | const shift = chart.series[0].data.length > 60; 43 | 44 | let i; 45 | for (i = 0; i < names.length; i++) { 46 | const name = names[i]; 47 | const value = performance[name]; 48 | chart.series[i].addPoint([time, value], true, shift); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.3.4.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.10.RELEASE' 4 | id 'java' 5 | } 6 | 7 | sourceCompatibility = '11' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 15 | implementation group: 'org.webjars', name: 'sockjs-client', version:'1.1.2' 16 | implementation group: 'org.webjars.npm', name: 'webstomp-client', version:'1.2.6' 17 | implementation group: 'org.webjars', name: 'jquery', version:'3.4.1' 18 | implementation group: 'org.webjars', name: 'bootstrap', version:'4.4.1' 19 | } 20 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/src/main/java/demo/websocket/server/example3/MessageMappingController.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example3; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.messaging.handler.annotation.MessageExceptionHandler; 6 | import org.springframework.messaging.handler.annotation.MessageMapping; 7 | import org.springframework.messaging.handler.annotation.SendTo; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.util.HtmlUtils; 10 | 11 | @Controller 12 | public class MessageMappingController { 13 | 14 | private static final Logger logger = LoggerFactory.getLogger(MessageMappingController.class); 15 | 16 | @MessageMapping("/request-without-response") 17 | public void handleMessageWithoutResponse(String message) { 18 | logger.info("Message without response: {}", message); 19 | } 20 | 21 | // response is sent to the endpoint /topic/request-with-implicit-response 22 | @MessageMapping("/request-with-implicit-response") 23 | public String handleMessageWithImplicitResponse(String message) { 24 | logger.info("Message with response: {}", message); 25 | return "response to " + HtmlUtils.htmlEscape(message); 26 | } 27 | 28 | @MessageMapping("/request") 29 | @SendTo("/queue/responses") 30 | public String handleMessageWithExplicitResponse(String message) { 31 | logger.info("Message with response: {}", message); 32 | if (message.equals("zero")) { 33 | throw new RuntimeException(String.format("'%s' is rejected", message)); 34 | } 35 | return "response to " + HtmlUtils.htmlEscape(message); 36 | } 37 | 38 | @MessageExceptionHandler 39 | @SendTo("/queue/errors") 40 | public String handleException(Throwable exception) { 41 | logger.error("Server exception", exception); 42 | return "server exception: " + exception.getMessage(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/src/main/java/demo/websocket/server/example3/ScheduledController.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example3; 2 | 3 | import org.springframework.messaging.core.MessageSendingOperations; 4 | import org.springframework.scheduling.annotation.Scheduled; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.time.LocalTime; 8 | 9 | @Component 10 | public class ScheduledController { 11 | 12 | private final MessageSendingOperations messageSendingOperations; 13 | 14 | public ScheduledController(MessageSendingOperations messageSendingOperations) { 15 | this.messageSendingOperations = messageSendingOperations; 16 | } 17 | 18 | @Scheduled(fixedDelay = 10000) 19 | public void sendPeriodicMessages() { 20 | String broadcast = String.format("server periodic message %s via the broker", LocalTime.now()); 21 | this.messageSendingOperations.convertAndSend("/topic/periodic", broadcast); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/src/main/java/demo/websocket/server/example3/ServerWebSocketSockJsStompApplication.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example3; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | public class ServerWebSocketSockJsStompApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ServerWebSocketSockJsStompApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/src/main/java/demo/websocket/server/example3/StompWebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example3; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void registerStompEndpoints(StompEndpointRegistry registry) { 15 | registry.addEndpoint("/websocket-sockjs-stomp"); 16 | registry.addEndpoint("/websocket-sockjs-stomp").withSockJS(); 17 | } 18 | 19 | @Override 20 | public void configureMessageBroker(MessageBrokerRegistry registry) { 21 | registry.enableSimpleBroker("/queue", "/topic"); 22 | registry.setApplicationDestinationPrefixes("/app"); 23 | registry.setPreservePublishOrder(true); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/src/main/java/demo/websocket/server/example3/SubscribeMappingController.java: -------------------------------------------------------------------------------- 1 | package demo.websocket.server.example3; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.messaging.simp.annotation.SubscribeMapping; 6 | import org.springframework.stereotype.Controller; 7 | 8 | @Controller 9 | public class SubscribeMappingController { 10 | 11 | private static final Logger logger = LoggerFactory.getLogger(SubscribeMappingController.class); 12 | 13 | @SubscribeMapping("/subscribe") 14 | public String sendOneTimeMessage() { 15 | logger.info("Subscription via the application"); 16 | return "server one-time message via the application"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.root=info 2 | logging.level.org.springframework.web.socket=debug 3 | logging.level.org.springframework.messaging.simp=debug 4 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/src/main/resources/static/application.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f5f5f5; 3 | } 4 | 5 | #main-content { 6 | max-width: 940px; 7 | padding: 2em 3em; 8 | margin: 0 auto 20px; 9 | background-color: #fff; 10 | border: 1px solid #e5e5e5; 11 | -webkit-border-radius: 5px; 12 | -moz-border-radius: 5px; 13 | border-radius: 5px; 14 | } 15 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/src/main/resources/static/application.js: -------------------------------------------------------------------------------- 1 | let stomp = null; 2 | 3 | function setConnected(connected) { 4 | $("#connect").prop("disabled", connected); 5 | $("#disconnect").prop("disabled", !connected); 6 | $("#send").prop("disabled", !connected); 7 | 8 | if (connected) { 9 | $("#conversation").show(); 10 | } else { 11 | $("#conversation").hide(); 12 | } 13 | 14 | $('#output').val(''); 15 | $("#responses").html(""); 16 | } 17 | 18 | function connect() { 19 | stomp = webstomp.over(new SockJS('/websocket-sockjs-stomp')); 20 | 21 | stomp.connect({}, function (frame) { 22 | setConnected(true); 23 | console.log('Client connected: ' + frame); 24 | 25 | stomp.subscribe('/app/subscribe', function (response) { 26 | log(response, 'table-success'); 27 | }); 28 | 29 | const subscription = stomp.subscribe('/queue/responses', function (response) { 30 | log(response, 'table-success'); 31 | }); 32 | 33 | stomp.subscribe('/queue/errors', function (response) { 34 | log(response, 'table-danger'); 35 | 36 | console.log('Client unsubscribes: ' + subscription); 37 | subscription.unsubscribe({}); 38 | }); 39 | 40 | stomp.subscribe('/topic/periodic', function (response) { 41 | log(response, 'table-info'); 42 | }); 43 | }); 44 | } 45 | 46 | function disconnect() { 47 | if (stomp !== null) { 48 | stomp.disconnect(function() { 49 | setConnected(false); 50 | console.log("Client disconnected"); 51 | }); 52 | stomp = null; 53 | } 54 | } 55 | 56 | function send() { 57 | const output = $("#output").val(); 58 | console.log("Client sends: " + output); 59 | stomp.send("/app/request", output, {}); 60 | } 61 | 62 | function log(response, clazz) { 63 | const input = response.body; 64 | console.log("Client received: " + input); 65 | $("#responses").append("" + input + ""); 66 | } 67 | 68 | $(function () { 69 | $("form").on('submit', function (e) { 70 | e.preventDefault(); 71 | }); 72 | $("#connect").click(function () { 73 | connect(); 74 | }); 75 | $("#disconnect").click(function () { 76 | disconnect(); 77 | }); 78 | $("#send").click(function () { 79 | send(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /websocket-sockjs-stomp-server/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | STOMP over WebSocket/SockJS example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
Responses
46 |
47 |
48 |
49 | 50 | 51 | --------------------------------------------------------------------------------