├── .gitignore ├── README.md ├── WS_RaceCondition_demo.gif ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── java └── io └── redrays └── ws └── concept ├── client ├── WebSocketParallel_Failed.java └── WebSocketParallel_Success.java └── server └── EchoWebSocketServer.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Race Conditions in Websockets 2 | 3 | In the field of information technology, ensuring data reliability and integrity is crucial, especially with millions of users and terabytes of data in the mix. However, as software systems become more complex, issues such as race conditions can arise, significantly impacting system operation and leading to unpredictable outcomes. 4 | 5 | ### What are race conditions? 6 | 7 | Race conditions are errors that occur in multitasking programs when two or more threads or processes attempt to modify shared data or resources simultaneously without synchronization. This can result in unexpected and unpredictable outcomes since the order of execution of operations depends on which threads or processes finish first. Detecting race conditions is a challenging issue in software systems due to their unpredictable nature. 8 | 9 | ### Sources and Further Reading: 10 | 11 | - "Race Condition" on Wikipedia 12 | - "Smashing the State Machine: the True Potential of Web Race Conditions" by James Kettle 13 | 14 | While there are numerous resources available about classic race conditions on the internet, this article explores whether race conditions can occur in WebSockets. 15 | 16 | ### WebSockets 17 | 18 | WebSockets are a cutting-edge technology that significantly improves interaction in web applications by providing an open bidirectional connection between a user's web browser and a web server. This seamless connection enables data exchange without the need for constantly initiating new HTTP requests, making them ideal for creating interactive applications. 19 | 20 | ### Advantages of WebSockets: 21 | 22 | 1. Low Latency: WebSockets minimize data exchange delays, making them an excellent choice for applications where real-time matters, such as online games or chat applications. 23 | 2. Resource Efficiency: WebSockets support a persistent connection, reducing the overhead associated with establishing and tearing down connections compared to numerous consecutive HTTP requests. 24 | 3. Bidirectional Data Exchange: Both the client and server can send data to each other without waiting for a request, making WebSockets an excellent tool for creating interactive web applications. 25 | 4. Compatibility: WebSockets can be used in various programming languages and technologies, making them a universal tool for creating interactive web applications. 26 | 27 | To demonstrate the concept, this article includes a Java code that represents a WebSocket server that interacts with a PostgreSQL database. The server uses the Java-WebSocket library to handle WebSocket connections and performs the following tasks: 28 | 29 | 1. After launching the program, the Java code connects to the database and checks if the "example" table exists. If it doesn't exist, it creates the table and inserts random data: 30 | - RandomName0 31 | - RandomName1 32 | - ... 33 | - RandomName9 34 | 35 | 2. The most interesting code is found in the "onMessage" function. 36 | ```java 37 | public static int a = 0; 38 | 39 | @Override 40 | public void onMessage(WebSocket conn, String message) { 41 | if (a == 0) { 42 | try { 43 | // some activity with db 44 | int rowCount = getCountFromExampleTable(); 45 | 46 | } catch (SQLException e) { 47 | System.out.println("Error executing query: " + e.getMessage()); 48 | } 49 | conn.send("Echo: " + message); 50 | a = a + 1; 51 | System.out.println(a); 52 | } 53 | } 54 | 55 | ``` 56 | There's a global variable "a" initialized to 0. When a client connects to the server and sends a message, it checks if "id == 0", indicating whether this function has already been executed. If not, a simple SQL command is executed to select the count of rows from the "example" table. Then, "a" is incremented by 1, and its value is printed. And in theory, the function should not be executed 2 times. 57 | 58 | In regards to the client, two types of clients have been created: "WebSocketParallel_Success" 59 | ```java 60 | package io.redrays.ws.concept.client; 61 | 62 | import org.java_websocket.client.WebSocketClient; 63 | import org.java_websocket.handshake.ServerHandshake; 64 | 65 | import java.net.URI; 66 | import java.net.URISyntaxException; 67 | import java.util.ArrayList; 68 | import java.util.List; 69 | import java.util.concurrent.ExecutorService; 70 | import java.util.concurrent.Executors; 71 | import java.util.concurrent.TimeUnit; 72 | 73 | public class WebSocketParallel_Success { 74 | 75 | public static void main(String[] args) { 76 | // Define the WebSocket server URI 77 | String serverUri = "ws://127.0.0.1:8080"; 78 | 79 | // Number of WebSocket clients to create 80 | int numClients = 100; 81 | 82 | // Create an ExecutorService to manage multiple WebSocket client threads 83 | ExecutorService executor = Executors.newFixedThreadPool(numClients); 84 | 85 | // Create a list to store WebSocket client instances 86 | List clients = new ArrayList<>(); 87 | 88 | // Loop to create and configure multiple WebSocket clients 89 | for (int i = 0; i < numClients; i++) { 90 | int clientId = i + 1; 91 | try { 92 | // Create a WebSocket client for each connection 93 | WebSocketClient webSocketClient = new WebSocketClient(new URI(serverUri)) { 94 | @Override 95 | public void onOpen(ServerHandshake handshakedata) { 96 | // Handle WebSocket connection opened event 97 | System.out.println("Client " + clientId + " connected to the WebSocket server"); 98 | this.send("Hello, WebSocket server! From client " + clientId); 99 | } 100 | 101 | @Override 102 | public void onMessage(String message) { 103 | // Handle incoming WebSocket messages 104 | System.out.println("Client " + clientId + " received message: " + message); 105 | } 106 | 107 | @Override 108 | public void onClose(int code, String reason, boolean remote) { 109 | // Handle WebSocket connection closed event 110 | System.out.println("Client " + clientId + " connection closed: " + reason); 111 | } 112 | 113 | @Override 114 | public void onError(Exception ex) { 115 | // Handle WebSocket error 116 | System.out.println("Client " + clientId + " error occurred: " + ex.getMessage()); 117 | } 118 | }; 119 | 120 | // Add the WebSocket client to the list 121 | clients.add(webSocketClient); 122 | 123 | // Connect the WebSocket client in a separate thread 124 | executor.submit(webSocketClient::connect); 125 | 126 | } catch (URISyntaxException e) { 127 | System.out.println("Invalid WebSocket server URI: " + e.getMessage()); 128 | } 129 | } 130 | 131 | // Shutdown the executor after all tasks are submitted 132 | executor.shutdown(); 133 | 134 | // Wait for all WebSocket client threads to complete 135 | try { 136 | executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); 137 | } catch (InterruptedException e) { 138 | System.out.println("Interrupted while waiting for tasks to complete: " + e.getMessage()); 139 | } 140 | } 141 | } 142 | 143 | ``` 144 | 145 | and "WebSocketParallel_Failed". 146 | 147 | ```java 148 | package io.redrays.ws.concept.client; 149 | 150 | import org.java_websocket.client.WebSocketClient; 151 | import org.java_websocket.handshake.ServerHandshake; 152 | 153 | import java.net.URI; 154 | import java.net.URISyntaxException; 155 | import java.util.concurrent.ExecutorService; 156 | import java.util.concurrent.Executors; 157 | import java.util.concurrent.TimeUnit; 158 | 159 | public class WebSocketParallel_Failed { 160 | 161 | public static void main(String[] args) { 162 | String serverUri = "ws://127.0.0.1:8080"; // WebSocket server URI 163 | int numParallelRequests = 285; // Number of parallel WebSocket requests 164 | 165 | try { 166 | WebSocketClient webSocketClient = new WebSocketClient(new URI(serverUri)) { 167 | 168 | // This method is called when the WebSocket connection is successfully opened. 169 | @Override 170 | public void onOpen(ServerHandshake handshakedata) { 171 | System.out.println("Connected to the WebSocket server"); 172 | 173 | // Create a fixed thread pool to manage parallel requests 174 | ExecutorService executor = Executors.newFixedThreadPool(numParallelRequests); 175 | 176 | for (int i = 0; i < numParallelRequests; i++) { 177 | int messageId = i + 1; 178 | executor.submit(() -> { 179 | this.send("Hello, WebSocket server! Message ID: " + messageId); 180 | System.out.println("Sent message with ID: " + messageId); 181 | }); 182 | } 183 | 184 | // Shutdown the executor after all tasks are submitted 185 | executor.shutdown(); 186 | 187 | // Wait for the tasks to complete 188 | try { 189 | executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); 190 | } catch (InterruptedException e) { 191 | System.out.println("Interrupted while waiting for tasks to complete: " + e.getMessage()); 192 | } 193 | } 194 | 195 | // This method is called when a WebSocket message is received. 196 | @Override 197 | public void onMessage(String message) { 198 | System.out.println("Received message: " + message); 199 | } 200 | 201 | // This method is called when the WebSocket connection is closed. 202 | @Override 203 | public void onClose(int code, String reason, boolean remote) { 204 | System.out.println("Connection closed: " + reason); 205 | } 206 | 207 | // This method is called when an error occurs in the WebSocket connection. 208 | @Override 209 | public void onError(Exception ex) { 210 | System.out.println("Error occurred: " + ex.getMessage()); 211 | } 212 | }; 213 | 214 | // Connect to the WebSocket server 215 | webSocketClient.connect(); 216 | 217 | } catch (URISyntaxException e) { 218 | System.out.println("Invalid WebSocket server URI: " + e.getMessage()); 219 | } 220 | } 221 | } 222 | 223 | ``` 224 | 225 | Attempts have been made to create a race condition using two different methods. In the first file, multiple connections are created in parallel, and data is sent to the server, while in the second file, only one connection is established, but data is sent one after the other in serial. 226 | 227 | As evident from the class names, if multiple connections are created in parallel and data is sent, the race condition will occur. However, it won't occur in the second case since WebSockets transmit data sequentially in a single connection. 228 | 229 | As shown in the screenshots below, race conditions can occur. 230 | ![image](https://github.com/vah13/WS_RaceCondition_PoC/assets/7976421/e9f094d5-4981-4159-b5c6-ed789b602b34) 231 | 232 | ![image](https://github.com/vah13/WS_RaceCondition_PoC/blob/master/WS_RaceCondition_demo.gif) 233 | 234 | 235 | Original demo video here https://redrays.io/wp-content/uploads/2023/09/WS_RaceCondition_demo_video.mp4 236 | ### Are there any race condition vulnerabilities in WebSockets in the wild? 237 | 238 | Yes, a few critical race condition vulnerabilities have been detected in some cryptocurrency exchanges using WS API. These issues have been reported, and the exchanges subsequently fixed them. 239 | 240 | [@vah_13](https://twitter.com/vah_13) 241 | -------------------------------------------------------------------------------- /WS_RaceCondition_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redrays-io/WS_RaceCondition_PoC/2fb8cdd6ea3c1b83ee81beab51e199ffde207263/WS_RaceCondition_demo.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group = 'io.redrays.ws.concept' 6 | version = '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation platform('org.junit:junit-bom:5.9.1') 14 | testImplementation 'org.junit.jupiter:junit-jupiter' 15 | 16 | implementation 'org.xerial:sqlite-jdbc:3.43.0.0' 17 | implementation 'org.postgresql:postgresql:42.6.0' 18 | implementation 'org.java-websocket:Java-WebSocket:1.5.4' 19 | } 20 | 21 | test { 22 | useJUnitPlatform() 23 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redrays-io/WS_RaceCondition_PoC/2fb8cdd6ea3c1b83ee81beab51e199ffde207263/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Sep 13 22:08:29 AMT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'WS_RaceCondition' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/io/redrays/ws/concept/client/WebSocketParallel_Failed.java: -------------------------------------------------------------------------------- 1 | package io.redrays.ws.concept.client; 2 | 3 | import org.java_websocket.client.WebSocketClient; 4 | import org.java_websocket.handshake.ServerHandshake; 5 | 6 | import java.net.URI; 7 | import java.net.URISyntaxException; 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | public class WebSocketParallel_Failed { 13 | 14 | public static void main(String[] args) { 15 | String serverUri = "ws://127.0.0.1:8080"; // WebSocket server URI 16 | int numParallelRequests = 100; // Number of parallel WebSocket requests 17 | 18 | try { 19 | WebSocketClient webSocketClient = new WebSocketClient(new URI(serverUri)) { 20 | 21 | // This method is called when the WebSocket connection is successfully opened. 22 | @Override 23 | public void onOpen(ServerHandshake handshakedata) { 24 | System.out.println("Connected to the WebSocket server"); 25 | 26 | // Create a fixed thread pool to manage parallel requests 27 | ExecutorService executor = Executors.newFixedThreadPool(numParallelRequests); 28 | 29 | for (int i = 0; i < numParallelRequests; i++) { 30 | int messageId = i + 1; 31 | executor.submit(() -> { 32 | this.send("Hello, WebSocket server! Message ID: " + messageId); 33 | System.out.println("Sent message with ID: " + messageId); 34 | }); 35 | } 36 | 37 | // Shutdown the executor after all tasks are submitted 38 | executor.shutdown(); 39 | 40 | // Wait for the tasks to complete 41 | try { 42 | executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); 43 | } catch (InterruptedException e) { 44 | System.out.println("Interrupted while waiting for tasks to complete: " + e.getMessage()); 45 | } 46 | } 47 | 48 | // This method is called when a WebSocket message is received. 49 | @Override 50 | public void onMessage(String message) { 51 | System.out.println("Received message: " + message); 52 | } 53 | 54 | // This method is called when the WebSocket connection is closed. 55 | @Override 56 | public void onClose(int code, String reason, boolean remote) { 57 | System.out.println("Connection closed: " + reason); 58 | } 59 | 60 | // This method is called when an error occurs in the WebSocket connection. 61 | @Override 62 | public void onError(Exception ex) { 63 | System.out.println("Error occurred: " + ex.getMessage()); 64 | } 65 | }; 66 | 67 | // Connect to the WebSocket server 68 | webSocketClient.connect(); 69 | 70 | } catch (URISyntaxException e) { 71 | System.out.println("Invalid WebSocket server URI: " + e.getMessage()); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/io/redrays/ws/concept/client/WebSocketParallel_Success.java: -------------------------------------------------------------------------------- 1 | package io.redrays.ws.concept.client; 2 | 3 | import org.java_websocket.client.WebSocketClient; 4 | import org.java_websocket.handshake.ServerHandshake; 5 | 6 | import java.net.URI; 7 | import java.net.URISyntaxException; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | public class WebSocketParallel_Success { 15 | 16 | public static void main(String[] args) { 17 | // Define the WebSocket server URI 18 | String serverUri = "ws://127.0.0.1:8080"; 19 | 20 | // Number of WebSocket clients to create 21 | int numClients = 100; 22 | 23 | // Create an ExecutorService to manage multiple WebSocket client threads 24 | ExecutorService executor = Executors.newFixedThreadPool(numClients); 25 | 26 | // Create a list to store WebSocket client instances 27 | List clients = new ArrayList<>(); 28 | 29 | // Loop to create and configure multiple WebSocket clients 30 | for (int i = 0; i < numClients; i++) { 31 | int clientId = i + 1; 32 | try { 33 | // Create a WebSocket client for each connection 34 | WebSocketClient webSocketClient = new WebSocketClient(new URI(serverUri)) { 35 | @Override 36 | public void onOpen(ServerHandshake handshakedata) { 37 | // Handle WebSocket connection opened event 38 | System.out.println("Client " + clientId + " connected to the WebSocket server"); 39 | this.send("Hello, WebSocket server! From client " + clientId); 40 | } 41 | 42 | @Override 43 | public void onMessage(String message) { 44 | // Handle incoming WebSocket messages 45 | System.out.println("Client " + clientId + " received message: " + message); 46 | } 47 | 48 | @Override 49 | public void onClose(int code, String reason, boolean remote) { 50 | // Handle WebSocket connection closed event 51 | System.out.println("Client " + clientId + " connection closed: " + reason); 52 | } 53 | 54 | @Override 55 | public void onError(Exception ex) { 56 | // Handle WebSocket error 57 | System.out.println("Client " + clientId + " error occurred: " + ex.getMessage()); 58 | } 59 | }; 60 | 61 | // Add the WebSocket client to the list 62 | clients.add(webSocketClient); 63 | 64 | // Connect the WebSocket client in a separate thread 65 | executor.submit(webSocketClient::connect); 66 | 67 | } catch (URISyntaxException e) { 68 | System.out.println("Invalid WebSocket server URI: " + e.getMessage()); 69 | } 70 | } 71 | 72 | // Shutdown the executor after all tasks are submitted 73 | executor.shutdown(); 74 | 75 | // Wait for all WebSocket client threads to complete 76 | try { 77 | executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); 78 | } catch (InterruptedException e) { 79 | System.out.println("Interrupted while waiting for tasks to complete: " + e.getMessage()); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/io/redrays/ws/concept/server/EchoWebSocketServer.java: -------------------------------------------------------------------------------- 1 | package io.redrays.ws.concept.server; 2 | 3 | import org.java_websocket.WebSocket; 4 | import org.java_websocket.handshake.ClientHandshake; 5 | import org.java_websocket.server.WebSocketServer; 6 | 7 | import java.net.InetSocketAddress; 8 | import java.sql.Connection; 9 | import java.sql.DriverManager; 10 | import java.sql.SQLException; 11 | import java.sql.Statement; 12 | import java.util.Random; 13 | import java.sql.ResultSet; 14 | 15 | public class EchoWebSocketServer extends WebSocketServer { 16 | 17 | // PostgreSQL JDBC URL and driver class name 18 | private static final String POSTGRESQL_JDBC_URL = "jdbc:postgresql://localhost:5432/postgres"; 19 | private static final String POSTGRESQL_JDBC_DRIVER = "org.postgresql.Driver"; 20 | private static final String POSTGRESQL_USERNAME = "postgres"; 21 | private static final String POSTGRESQL_PASSWORD = "passw"; 22 | 23 | public EchoWebSocketServer(InetSocketAddress address) { 24 | super(address); 25 | } 26 | 27 | // Operation: Handle client connection 28 | @Override 29 | public void onOpen(WebSocket conn, ClientHandshake handshake) { 30 | //System.out.println("New client connected: " + conn.getRemoteSocketAddress()); 31 | } 32 | 33 | // Operation: Handle client disconnection 34 | @Override 35 | public void onClose(WebSocket conn, int code, String reason, boolean remote) { 36 | System.out.println("Client disconnected: " + conn.getRemoteSocketAddress() + " Reason: " + reason); 37 | } 38 | 39 | public static int id = 0; 40 | 41 | @Override 42 | public void onMessage(WebSocket conn, String message) { 43 | if (id == 0) { 44 | try { 45 | // some activity with db 46 | int rowCount = getCountFromExampleTable(); 47 | } catch (SQLException e) { 48 | System.out.println("Error executing query: " + e.getMessage()); 49 | } 50 | conn.send("Echo: " + message); 51 | id = id + 1; 52 | System.out.println(id); 53 | } 54 | } 55 | 56 | /** 57 | * Execute a 'SELECT COUNT(*) FROM example' query and return the result. 58 | * 59 | * @return The count from the 'example' table. 60 | * @throws SQLException if a database error occurs during the query. 61 | */ 62 | private int getCountFromExampleTable() throws SQLException { 63 | int count = 0; 64 | try (Connection connection = DriverManager.getConnection(POSTGRESQL_JDBC_URL, POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD); 65 | Statement statement = connection.createStatement()) { 66 | String query = "SELECT COUNT(*) FROM example"; 67 | try (ResultSet resultSet = statement.executeQuery(query)) { 68 | if (resultSet.next()) { 69 | count = resultSet.getInt(1); 70 | } 71 | } 72 | } 73 | return count; 74 | } 75 | 76 | // Operation: Handle WebSocket error 77 | @Override 78 | public void onError(WebSocket conn, Exception ex) { 79 | System.out.println("Error occurred: " + ex.getMessage()); 80 | } 81 | 82 | // Operation: Handle WebSocket server startup 83 | @Override 84 | public void onStart() { 85 | System.out.println("WebSocket server started on " + getAddress()); 86 | 87 | // Create the table and insert random data when the server starts 88 | try { 89 | createAndPopulateTable(); 90 | } catch (Exception e) { 91 | throw new RuntimeException(e); 92 | } 93 | } 94 | 95 | /** 96 | * Create a table "example" (id serial primary key, name text) if it doesn't exist. 97 | * 98 | * @throws SQLException if a database error occurs during table creation. 99 | */ 100 | private void createTableIfNotExists(Connection connection) throws SQLException { 101 | try (Statement createTableStatement = connection.createStatement()) { 102 | String createTableQuery = "CREATE TABLE IF NOT EXISTS example (id serial primary key, name text)"; 103 | createTableStatement.execute(createTableQuery); 104 | } 105 | } 106 | 107 | /** 108 | * Insert 10 random records into the "example" table. 109 | * 110 | * @throws SQLException if a database error occurs during data insertion. 111 | */ 112 | private void insertRandomData(Connection connection) throws SQLException { 113 | try (Statement insertStatement = connection.createStatement()) { 114 | Random random = new Random(); 115 | for (int j = 0; j < 10; j++) { 116 | String name = "RandomName" + j; 117 | String insertQuery = "INSERT INTO example (name) VALUES ('" + name + "')"; 118 | insertStatement.execute(insertQuery); 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Create the "example" table if it doesn't exist and insert random data into it. 125 | * 126 | * @throws SQLException if a database error occurs. 127 | */ 128 | private void createAndPopulateTable() throws SQLException { 129 | try (Connection connection = DriverManager.getConnection(POSTGRESQL_JDBC_URL, POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD)) { 130 | // Create the table if it doesn't exist 131 | createTableIfNotExists(connection); 132 | 133 | // Insert 10 random records into the table 134 | insertRandomData(connection); 135 | } catch (SQLException ex) { 136 | System.out.println("Error occurred while creating and populating the table: " + ex.getMessage()); 137 | } 138 | } 139 | 140 | // Main method to start the WebSocket server 141 | public static void main(String[] args) { 142 | int port = 8080; // Set the server's port 143 | // Load PostgreSQL JDBC driver 144 | try { 145 | Class.forName(POSTGRESQL_JDBC_DRIVER); 146 | } catch (ClassNotFoundException e) { 147 | System.out.println("PostgreSQL JDBC driver not found."); 148 | return; 149 | } 150 | EchoWebSocketServer server = new EchoWebSocketServer(new InetSocketAddress(port)); 151 | server.start(); 152 | } 153 | } 154 | --------------------------------------------------------------------------------