");
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-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-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-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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 | SockJS transport:
37 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
Responses
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------