├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitattributes ├── src └── main │ └── java │ └── dev │ └── gustavoavila │ └── websocketclient │ ├── exceptions │ ├── IllegalSchemeException.java │ ├── InvalidReceivedFrameException.java │ └── InvalidServerHandshakeException.java │ ├── model │ └── Payload.java │ ├── common │ └── Utils.java │ └── WebSocketClient.java ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries 2 | bin/ 3 | build/ 4 | 5 | # Gradle 6 | .gradle 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusavila92/java-android-websocket-client/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.java text 3 | build.gradle text 4 | gradlew text 5 | *.properties text 6 | LICENSE text 7 | *.md text 8 | 9 | *.bat text eol=crlf 10 | 11 | *.jar binary 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/dev/gustavoavila/websocketclient/exceptions/IllegalSchemeException.java: -------------------------------------------------------------------------------- 1 | package dev.gustavoavila.websocketclient.exceptions; 2 | 3 | /** 4 | * Exception which indicates that the received schema is invalid 5 | * 6 | * @author Gustavo Avila 7 | * 8 | */ 9 | public class IllegalSchemeException extends IllegalArgumentException { 10 | public IllegalSchemeException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/gustavoavila/websocketclient/exceptions/InvalidReceivedFrameException.java: -------------------------------------------------------------------------------- 1 | package dev.gustavoavila.websocketclient.exceptions; 2 | 3 | /** 4 | * Used to indicate a protocol problem with a received frame 5 | * 6 | * @author Gustavo Avila 7 | * 8 | */ 9 | public class InvalidReceivedFrameException extends RuntimeException { 10 | public InvalidReceivedFrameException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/gustavoavila/websocketclient/exceptions/InvalidServerHandshakeException.java: -------------------------------------------------------------------------------- 1 | package dev.gustavoavila.websocketclient.exceptions; 2 | 3 | /** 4 | * Exception which indicates that the handshake received from the server is 5 | * invalid 6 | * 7 | * @author Gustavo Avila 8 | * 9 | */ 10 | public class InvalidServerHandshakeException extends RuntimeException { 11 | public InvalidServerHandshakeException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/dev/gustavoavila/websocketclient/model/Payload.java: -------------------------------------------------------------------------------- 1 | package dev.gustavoavila.websocketclient.model; 2 | 3 | /** 4 | * A payload that will be send through the WebSocket connection 5 | * 6 | * @author Gustavo Avila 7 | * 8 | */ 9 | public class Payload { 10 | /** 11 | * Opcode of the payload 12 | */ 13 | private int opcode; 14 | 15 | /** 16 | * Data included into the payload 17 | */ 18 | private byte[] data; 19 | 20 | /** 21 | * This is true if a close frame was previously received and this payload represents the echo 22 | */ 23 | private boolean isCloseEcho; 24 | 25 | /** 26 | * Initializes the variables 27 | * 28 | * @param opcode 29 | * @param data 30 | */ 31 | public Payload(int opcode, byte[] data, boolean isCloseEcho) { 32 | this.opcode = opcode; 33 | this.data = data; 34 | this.isCloseEcho = isCloseEcho; 35 | } 36 | 37 | /** 38 | * Returns the opcode 39 | * 40 | * @return 41 | */ 42 | public int getOpcode() { 43 | return opcode; 44 | } 45 | 46 | /** 47 | * Returns the data 48 | * 49 | * @return 50 | */ 51 | public byte[] getData() { 52 | return data; 53 | } 54 | 55 | public boolean isCloseEcho() { 56 | return isCloseEcho; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/dev/gustavoavila/websocketclient/common/Utils.java: -------------------------------------------------------------------------------- 1 | package dev.gustavoavila.websocketclient.common; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | 6 | /** 7 | * Utility class 8 | * 9 | * @author Gustavo Avila 10 | * 11 | */ 12 | public class Utils { 13 | /** 14 | * Converts the int value passed as parameter to a 2 byte array 15 | * 16 | * @param value 17 | * @return 18 | */ 19 | public static byte[] to2ByteArray(int value) { 20 | return new byte[] { (byte) (value >>> 8), (byte) value }; 21 | } 22 | 23 | /** 24 | * Converts the int value passed as parameter to a 8 byte array. 25 | * Even though the specification allows payloads with sizes greater than 32 bits, 26 | * Java only allows integers with 32 bit size, so the first 4 bytes will be zeroes. 27 | * 28 | * @param value 29 | * @return 30 | */ 31 | public static byte[] to8ByteArray(int value) { 32 | return new byte[] { 0, 0, 0, 0, 33 | (byte) (value >>> 24), (byte) (value >>> 16), (byte) (value >>> 8), (byte) value }; 34 | } 35 | 36 | /** 37 | * Converts the byte array passed as parameter to an integer 38 | * 39 | * @param bytes 40 | * @return 41 | */ 42 | public static int fromByteArray(byte[] bytes) { 43 | return bytes[0] << 24 | (bytes[1] & 0xFF) << 16 | (bytes[2] & 0xFF) << 8 | (bytes[3] & 0xFF); 44 | } 45 | 46 | /** 47 | * Encode data to base 64. 48 | * It checks if the VM is Dalvik (Android) and uses reflection to call the right classes 49 | * 50 | * @param data Data to be encoded 51 | * @return The encoded data 52 | */ 53 | public static String encodeToBase64String(byte[] data) { 54 | String vmName = System.getProperties().getProperty("java.vm.name"); 55 | 56 | try { 57 | if (vmName.equals("Dalvik")) { 58 | Method encodeToString = Class.forName("android.util.Base64").getMethod("encodeToString", byte[].class, int.class);; 59 | return (String) encodeToString.invoke(null, data, 2); 60 | } else { 61 | Method encoderMethod = Class.forName("java.util.Base64").getMethod("getEncoder"); 62 | Object encoder = encoderMethod.invoke(null); 63 | Method encodeToString = encoder.getClass().getMethod("encodeToString", byte[].class); 64 | return (String) encodeToString.invoke(encoder, data); 65 | } 66 | } catch (ClassNotFoundException e) { 67 | throw new RuntimeException("Base64 class not found"); 68 | } catch (NoSuchMethodException e) { 69 | throw new RuntimeException("Base64 class not found"); 70 | } catch (IllegalAccessException e) { 71 | throw new RuntimeException("Base64 class not found"); 72 | } catch (InvocationTargetException e) { 73 | throw new RuntimeException("Base64 class not found"); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /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 init 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 init 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 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java/Android WebSocket Client 2 | A very lightweight WebSocket client library for JVM based clients or Android which aims to implement the WebSocket protocol as defined in RFC 6455. It has no dependencies and the _jar_ file is only 22KB. 3 | 4 | ## Download 5 | This library is published to Maven Central. Versions prior to 2.0.0 were also published to JCenter. 6 | 7 | ## _dev.gustavoavila_ is now the official namespace 8 | Former versions were using the namespace _tech.gusavila92_ but now the official namespace starting from version 2.0.0 is _dev.gustavoavila_ 9 | 10 | ### Gradle 11 | ``` 12 | compile 'dev.gustavoavila:java-android-websocket-client:2.0.2' 13 | ``` 14 | ### Maven 15 | ```xml 16 | 17 | dev.gustavoavila 18 | java-android-websocket-client 19 | 2.0.2 20 | pom 21 | 22 | ``` 23 | 24 | This is an example of how you can start a new connection. 25 | ```java 26 | private WebSocketClient webSocketClient; 27 | 28 | private void createWebSocketClient() { 29 | URI uri; 30 | try { 31 | uri = new URI("ws://localhost:8080/test"); 32 | } 33 | catch (URISyntaxException e) { 34 | e.printStackTrace(); 35 | return; 36 | } 37 | 38 | webSocketClient = new WebSocketClient(uri) { 39 | @Override 40 | public void onOpen() { 41 | System.out.println("onOpen"); 42 | webSocketClient.send("Hello, World!"); 43 | } 44 | 45 | @Override 46 | public void onTextReceived(String message) { 47 | System.out.println("onTextReceived"); 48 | } 49 | 50 | @Override 51 | public void onBinaryReceived(byte[] data) { 52 | System.out.println("onBinaryReceived"); 53 | } 54 | 55 | @Override 56 | public void onPingReceived(byte[] data) { 57 | System.out.println("onPingReceived"); 58 | } 59 | 60 | @Override 61 | public void onPongReceived(byte[] data) { 62 | System.out.println("onPongReceived"); 63 | } 64 | 65 | @Override 66 | public void onException(Exception e) { 67 | System.out.println(e.getMessage()); 68 | } 69 | 70 | @Override 71 | public void onCloseReceived() { 72 | System.out.println("onCloseReceived"); 73 | } 74 | }; 75 | 76 | webSocketClient.setConnectTimeout(10000); 77 | webSocketClient.setReadTimeout(60000); 78 | webSocketClient.addHeader("Origin", "http://developer.example.com"); 79 | webSocketClient.enableAutomaticReconnection(5000); 80 | webSocketClient.connect(); 81 | } 82 | ``` 83 | If you don't specify a port into the URI, the default port will be 80 for *ws* and 443 for *wss*. 84 | 85 | This is the list of the default HTTP Headers that will be included into the WebSocket client handshake 86 | - Host 87 | - Upgrade 88 | - Connection 89 | - Sec-WebSocket-Key 90 | - Sec-WebSocket-Version 91 | 92 | If you wish to include more headers, like *Origin*, you can add them using ```addHeader(String key, String value)``` method. 93 | 94 | You can also override the default SSLSocketFactory using ```setSSLSocketFactory(SSLSocketFactory factory)```. 95 | 96 | When an Exception occurs, the library calls ```onException(Exception e)```. 97 | 98 | When you are finished using the WebSocket, you can call ```webSocketClient.close()``` to close the connection. 99 | 100 | ## Automatic reconnection 101 | Automatic reconnection is supported through ```enableAutomaticReconnection(long waitTimeBeforeReconnection)``` method. Every time an *IOException* occurs, ```onException(Exception e)``` method is called and automatically a new connection is created and started. For performance reasons, between every reconnection intent you should specify a wait time before trying to reconnect again, using ```waitTimeBeforeReconnection``` parameter. 102 | 103 | ## Timeouts 104 | Connect and read timeouts are supported through ```setConnectTimeout(int connectTimeout)``` and ```setReadTimeout(int readTimeout)```. If one of those timeouts expires, ```onException(Exception e)``` is called. If automatic reconnection is enabled, a new connection could be established automatically. 105 | 106 | Connect timeout is used in establishing a TCP connection between this client and the server. Read timeout is used when this WebSocket Client doesn't received data for a long time. A server could send data periodically to ensure that the underlying TCP connection is not closed unexpectedly due to an idle connection, and this read timeout is designed for this purpose. 107 | 108 | ## ws and wss 109 | This library supports secure and insecure WebSockets. You just need to define the scheme as *wss* or *ws* (case-sensitive) into the URI. 110 | 111 | ## Ping and Pong frames 112 | When a Ping frame is received, automatically a Pong frame is sent with the same Application Data of the Ping frame. You can also send Ping and Pong frames unsolicited using ```sendPing(byte[] data)``` and ```sendPong(byte[] data)```. Data can be *null* if don't want to send Application Data. 113 | 114 | ## Minimum requirements 115 | This library requires at minimum Java 1.6 or Android 1.6 (API 4) 116 | 117 | ## License 118 | 119 | Copyright 2022 Gustavo Avila 120 | 121 | Licensed under the Apache License, Version 2.0 (the "License"); 122 | you may not use this file except in compliance with the License. 123 | You may obtain a copy of the License at 124 | 125 | http://www.apache.org/licenses/LICENSE-2.0 126 | 127 | Unless required by applicable law or agreed to in writing, software 128 | distributed under the License is distributed on an "AS IS" BASIS, 129 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 130 | See the License for the specific language governing permissions and 131 | limitations under the License. 132 | -------------------------------------------------------------------------------- /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 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/dev/gustavoavila/websocketclient/WebSocketClient.java: -------------------------------------------------------------------------------- 1 | package dev.gustavoavila.websocketclient; 2 | 3 | import dev.gustavoavila.websocketclient.common.Utils; 4 | import dev.gustavoavila.websocketclient.exceptions.IllegalSchemeException; 5 | import dev.gustavoavila.websocketclient.exceptions.InvalidReceivedFrameException; 6 | import dev.gustavoavila.websocketclient.exceptions.InvalidServerHandshakeException; 7 | import dev.gustavoavila.websocketclient.model.Payload; 8 | 9 | import java.io.BufferedInputStream; 10 | import java.io.BufferedOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.net.InetSocketAddress; 14 | import java.net.Socket; 15 | import java.net.URI; 16 | import java.nio.charset.Charset; 17 | import java.security.MessageDigest; 18 | import java.security.NoSuchAlgorithmException; 19 | import java.security.SecureRandom; 20 | import java.util.*; 21 | 22 | import javax.net.SocketFactory; 23 | import javax.net.ssl.SSLSocketFactory; 24 | 25 | /** 26 | * Implements the WebSocket protocol as defined in RFC 6455 27 | * 28 | * @author Gustavo Avila 29 | */ 30 | public abstract class WebSocketClient { 31 | public static final int CLOSE_CODE_NORMAL = 1000; 32 | 33 | /** 34 | * Max number of response handshake bytes to read before raising an exception 35 | */ 36 | private static final int MAX_HEADER_SIZE = 16392; 37 | 38 | /** 39 | * GUID used when processing Sec-WebSocket-Accept response header 40 | */ 41 | private static final String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 42 | 43 | /** 44 | * Denotes a continuation frame 45 | */ 46 | private static final int OPCODE_CONTINUATION = 0x0; 47 | 48 | /** 49 | * Denotes a UTF-8 encoded text frame 50 | */ 51 | private static final int OPCODE_TEXT = 0x1; 52 | 53 | /** 54 | * Denotes a binary frame 55 | */ 56 | private static final int OPCODE_BINARY = 0x2; 57 | 58 | /** 59 | * Denotes a close frame 60 | */ 61 | private static final int OPCODE_CLOSE = 0x8; 62 | 63 | /** 64 | * Denotes a Ping frame 65 | */ 66 | private static final int OPCODE_PING = 0x9; 67 | 68 | /** 69 | * Denotes a Pong frame 70 | */ 71 | private static final int OPCODE_PONG = 0xA; 72 | 73 | /** 74 | * Global lock for synchronized statements 75 | */ 76 | private final Object globalLock; 77 | 78 | /** 79 | * Connection URI 80 | */ 81 | private final URI uri; 82 | 83 | /** 84 | * Cryptographically secure random generator used for the masking key 85 | */ 86 | private final SecureRandom secureRandom; 87 | 88 | /** 89 | * Timeout in milliseconds to be used while the WebSocket is being connected 90 | */ 91 | private int connectTimeout; 92 | 93 | /** 94 | * Timeout in milliseconds for considering and idle connection as dead An 95 | * idle connection is a connection that has not received data for a long 96 | * time 97 | */ 98 | private int readTimeout; 99 | 100 | /** 101 | * Indicates if a connection must be reopened automatically due to an 102 | * IOException 103 | */ 104 | private boolean automaticReconnection; 105 | 106 | /** 107 | * Time in milliseconds to wait before opening a new WebSocket connection 108 | */ 109 | private long waitTimeBeforeReconnection; 110 | 111 | /** 112 | * Indicates if the connect() method was called 113 | */ 114 | private volatile boolean isRunning; 115 | 116 | /** 117 | * Custom headers to be included into the handshake 118 | */ 119 | private Map headers; 120 | 121 | /** 122 | * Underlying WebSocket connection This instance could change due to an 123 | * automatic reconnection Every time an automatic reconnection is fired, 124 | * this reference changes 125 | */ 126 | private volatile WebSocketConnection webSocketConnection; 127 | 128 | /** 129 | * Thread used for reconnection intents 130 | */ 131 | private volatile Thread reconnectionThread; 132 | 133 | /** 134 | * Allows to customize the SSL Socket factory instance 135 | */ 136 | private SSLSocketFactory sslSocketFactory; 137 | 138 | private volatile Timer closeTimer; 139 | 140 | /** 141 | * Initialize all the variables 142 | * 143 | * @param uri URI of the WebSocket server 144 | */ 145 | public WebSocketClient(URI uri) { 146 | this.globalLock = new Object(); 147 | this.uri = uri; 148 | this.secureRandom = new SecureRandom(); 149 | this.connectTimeout = 0; 150 | this.readTimeout = 0; 151 | this.automaticReconnection = false; 152 | this.waitTimeBeforeReconnection = 0; 153 | this.isRunning = false; 154 | this.headers = new HashMap(); 155 | webSocketConnection = new WebSocketConnection(); 156 | } 157 | 158 | /** 159 | * Called when the WebSocket handshake has been accepted and the WebSocket 160 | * is ready to send and receive data 161 | */ 162 | public abstract void onOpen(); 163 | 164 | /** 165 | * Called when a text message has been received 166 | * 167 | * @param message The UTF-8 encoded text received 168 | */ 169 | public abstract void onTextReceived(String message); 170 | 171 | /** 172 | * Called when a binary message has been received 173 | * 174 | * @param data The binary message received 175 | */ 176 | public abstract void onBinaryReceived(byte[] data); 177 | 178 | /** 179 | * Called when a ping message has been received 180 | * 181 | * @param data Optional data 182 | */ 183 | public abstract void onPingReceived(byte[] data); 184 | 185 | /** 186 | * Called when a pong message has been received 187 | * 188 | * @param data Optional data 189 | */ 190 | public abstract void onPongReceived(byte[] data); 191 | 192 | /** 193 | * Called when an exception has occurred 194 | * 195 | * @param e The exception that occurred 196 | */ 197 | public abstract void onException(Exception e); 198 | 199 | /** 200 | * Called when a close code has been received 201 | */ 202 | public abstract void onCloseReceived(int reason, String description); 203 | 204 | /** 205 | * Adds a new header to the set of headers that will be send into the 206 | * handshake This header will be added to the set of headers: Host, Upgrade, 207 | * Connection, Sec-WebSocket-Key, Sec-WebSocket-Version 208 | * 209 | * @param key Name of the new header 210 | * @param value Value of the new header 211 | */ 212 | public void addHeader(String key, String value) { 213 | synchronized (globalLock) { 214 | if (isRunning) { 215 | throw new IllegalStateException("Cannot add header while WebSocketClient is running"); 216 | } 217 | this.headers.put(key, value); 218 | } 219 | } 220 | 221 | /** 222 | * Set the timeout that will be used while the WebSocket is being connected 223 | * If timeout expires before connecting, an IOException will be thrown 224 | * 225 | * @param connectTimeout Timeout in milliseconds 226 | */ 227 | public void setConnectTimeout(int connectTimeout) { 228 | synchronized (globalLock) { 229 | if (isRunning) { 230 | throw new IllegalStateException("Cannot set connect timeout while WebSocketClient is running"); 231 | } else if (connectTimeout < 0) { 232 | throw new IllegalStateException("Connect timeout must be greater or equal than zero"); 233 | } 234 | this.connectTimeout = connectTimeout; 235 | } 236 | } 237 | 238 | /** 239 | * Sets the timeout for considering and idle connection as dead An idle 240 | * connection is a connection that has not received data for a long time If 241 | * timeout expires, an IOException will be thrown and you should consider 242 | * opening a new WebSocket connection, or delegate this functionality to 243 | * this WebSocketClient using the method setAutomaticReconnection(true) 244 | * 245 | * @param readTimeout Read timeout in milliseconds before considering an idle 246 | * connection as dead 247 | */ 248 | public void setReadTimeout(int readTimeout) { 249 | synchronized (globalLock) { 250 | if (isRunning) { 251 | throw new IllegalStateException("Cannot set read timeout while WebSocketClient is running"); 252 | } else if (readTimeout < 0) { 253 | throw new IllegalStateException("Read timeout must be greater or equal than zero"); 254 | } 255 | this.readTimeout = readTimeout; 256 | } 257 | } 258 | 259 | /** 260 | * Indicates that a connection must be reopened automatically due to an 261 | * IOException. Every time a connection fails due to an IOException, 262 | * onException() method is called before establishing a new connection. A 263 | * connection will be reopened automatically if an IOException occurred, but 264 | * other kinds of Exception will not reopen a connection 265 | * 266 | * @param waitTimeBeforeReconnection Wait time in milliseconds before trying to establish a new 267 | * WebSocket connection. For performance reasons, you should set 268 | * a wait time greater than zero 269 | */ 270 | public void enableAutomaticReconnection(long waitTimeBeforeReconnection) { 271 | synchronized (globalLock) { 272 | if (isRunning) { 273 | throw new IllegalStateException( 274 | "Cannot enable automatic reconnection while WebSocketClient is running"); 275 | } else if (waitTimeBeforeReconnection < 0) { 276 | throw new IllegalStateException("Wait time between reconnections must be greater or equal than zero"); 277 | } 278 | this.automaticReconnection = true; 279 | this.waitTimeBeforeReconnection = waitTimeBeforeReconnection; 280 | } 281 | } 282 | 283 | /** 284 | * Indicates that a connection must not be reopened automatically due to an 285 | * IOException 286 | */ 287 | public void disableAutomaticReconnection() { 288 | synchronized (globalLock) { 289 | if (isRunning) { 290 | throw new IllegalStateException( 291 | "Cannot disable automatic reconnection while WebSocketClient is running"); 292 | } 293 | this.automaticReconnection = false; 294 | } 295 | } 296 | 297 | /** 298 | * Starts a new connection to the WebSocket server 299 | */ 300 | public void connect() { 301 | synchronized (globalLock) { 302 | if (isRunning) { 303 | throw new IllegalStateException("WebSocketClient is not reusable"); 304 | } 305 | 306 | this.isRunning = true; 307 | createAndStartConnectionThread(); 308 | } 309 | } 310 | 311 | /** 312 | * Sets the SSL Socket factory used to create secure TCP connections 313 | * @param sslSocketFactory SSLSocketFactory 314 | */ 315 | public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { 316 | synchronized (globalLock) { 317 | if (isRunning) { 318 | throw new IllegalStateException("Cannot set SSLSocketFactory while WebSocketClient is running"); 319 | } else if (sslSocketFactory == null) { 320 | throw new IllegalStateException("SSLSocketFactory cannot be null"); 321 | } 322 | this.sslSocketFactory = sslSocketFactory; 323 | } 324 | } 325 | 326 | /** 327 | * Creates and starts the thread that will handle the WebSocket connection 328 | */ 329 | private void createAndStartConnectionThread() { 330 | new Thread(new Runnable() { 331 | @Override 332 | public void run() { 333 | try { 334 | boolean success = webSocketConnection.createAndConnectTCPSocket(); 335 | if (success) { 336 | webSocketConnection.startConnection(); 337 | } 338 | } catch (Exception e) { 339 | synchronized (globalLock) { 340 | if (isRunning) { 341 | webSocketConnection.closeInternal(); 342 | 343 | onException(e); 344 | 345 | if (e instanceof IOException && automaticReconnection) { 346 | createAndStartReconnectionThread(); 347 | } 348 | } 349 | } 350 | } 351 | } 352 | }).start(); 353 | } 354 | 355 | /** 356 | * Creates and starts the thread that will open a new WebSocket connection 357 | */ 358 | private void createAndStartReconnectionThread() { 359 | reconnectionThread = new Thread(new Runnable() { 360 | @Override 361 | public void run() { 362 | try { 363 | Thread.sleep(waitTimeBeforeReconnection); 364 | 365 | synchronized (globalLock) { 366 | if (isRunning) { 367 | webSocketConnection = new WebSocketConnection(); 368 | createAndStartConnectionThread(); 369 | } 370 | } 371 | } catch (InterruptedException e) { 372 | // Expected behavior when the WebSocket connection is closed 373 | } 374 | } 375 | }); 376 | reconnectionThread.start(); 377 | } 378 | 379 | /** 380 | * If the close method wasn't called, call onOpen method. 381 | */ 382 | private void notifyOnOpen() { 383 | synchronized (globalLock) { 384 | if (isRunning) { 385 | onOpen(); 386 | } 387 | } 388 | } 389 | 390 | /** 391 | * If the close method wasn't called, call onTextReceived(String message) 392 | * method. 393 | */ 394 | private void notifyOnTextReceived(String message) { 395 | synchronized (globalLock) { 396 | if (isRunning) { 397 | onTextReceived(message); 398 | } 399 | } 400 | } 401 | 402 | /** 403 | * If the close method wasn't called, call onBinaryReceived(byte[] data) 404 | * method. 405 | */ 406 | private void notifyOnBinaryReceived(byte[] data) { 407 | synchronized (globalLock) { 408 | if (isRunning) { 409 | onBinaryReceived(data); 410 | } 411 | } 412 | } 413 | 414 | /** 415 | * If the close method wasn't called, call onPingReceived(byte[] data) 416 | * method. 417 | */ 418 | private void notifyOnPingReceived(byte[] data) { 419 | synchronized (globalLock) { 420 | if (isRunning) { 421 | onPingReceived(data); 422 | } 423 | } 424 | } 425 | 426 | /** 427 | * If the close method wasn't called, call onPongReceived(byte[] data) 428 | * method. 429 | */ 430 | private void notifyOnPongReceived(byte[] data) { 431 | synchronized (globalLock) { 432 | if (isRunning) { 433 | onPongReceived(data); 434 | } 435 | } 436 | } 437 | 438 | /** 439 | * If the close method wasn't called, call onException(Exception e) method. 440 | */ 441 | private void notifyOnException(Exception e) { 442 | synchronized (globalLock) { 443 | if (isRunning) { 444 | onException(e); 445 | } 446 | } 447 | } 448 | 449 | /** 450 | * If the close method wasn't called, call onCloseReceived() method. 451 | */ 452 | private void notifyOnCloseReceived(int reason, String description) { 453 | synchronized (globalLock) { 454 | if (isRunning) { 455 | onCloseReceived(reason, description); 456 | } 457 | } 458 | } 459 | 460 | private void forceClose() { 461 | new Thread(new Runnable() { 462 | @Override 463 | public void run() { 464 | synchronized (globalLock) { 465 | isRunning = false; 466 | 467 | if (reconnectionThread != null) { 468 | reconnectionThread.interrupt(); 469 | } 470 | 471 | webSocketConnection.closeInternal(); 472 | } 473 | } 474 | }).start(); 475 | } 476 | 477 | /** 478 | * Sends a text message If the WebSocket is not connected yet, message will 479 | * be send the next time the connection is opened 480 | * 481 | * @param message Message that will be send to the WebSocket server 482 | */ 483 | public void send(String message) { 484 | byte[] data = message.getBytes(Charset.forName("UTF-8")); 485 | final Payload payload = new Payload(OPCODE_TEXT, data, false); 486 | 487 | new Thread(new Runnable() { 488 | @Override 489 | public void run() { 490 | webSocketConnection.sendInternal(payload); 491 | } 492 | 493 | }).start(); 494 | } 495 | 496 | /** 497 | * Sends a binary message If the WebSocket is not connected yet, message 498 | * will be send the next time the connection is opened 499 | * 500 | * @param data Binary data that will be send to the WebSocket server 501 | */ 502 | public void send(byte[] data) { 503 | final Payload payload = new Payload(OPCODE_BINARY, data, false); 504 | 505 | new Thread(new Runnable() { 506 | @Override 507 | public void run() { 508 | webSocketConnection.sendInternal(payload); 509 | } 510 | }).start(); 511 | } 512 | 513 | /** 514 | * Sends a PING frame with an optional data. 515 | * 516 | * @param data Data to be sent, or null if there is no data. 517 | */ 518 | public void sendPing(byte[] data) { 519 | if (data != null && data.length > 125) { 520 | throw new IllegalArgumentException("Control frame payload cannot be greater than 125 bytes"); 521 | } 522 | 523 | final Payload payload = new Payload(OPCODE_PING, data, false); 524 | new Thread(new Runnable() { 525 | @Override 526 | public void run() { 527 | webSocketConnection.sendInternal(payload); 528 | } 529 | }).start(); 530 | } 531 | 532 | /** 533 | * Sends a PONG frame with an optional data. 534 | * 535 | * @param data Data to be sent, or null if there is no data. 536 | */ 537 | public void sendPong(byte[] data) { 538 | if (data != null && data.length > 125) { 539 | throw new IllegalArgumentException("Control frame payload cannot be greater than 125 bytes"); 540 | } 541 | 542 | final Payload payload = new Payload(OPCODE_PONG, data, false); 543 | new Thread(new Runnable() { 544 | @Override 545 | public void run() { 546 | webSocketConnection.sendInternal(payload); 547 | } 548 | }).start(); 549 | } 550 | 551 | /** 552 | * Closes the WebSocket connection 553 | */ 554 | public void close(final int timeout, int code, String reason) { 555 | if (timeout == 0) { 556 | forceClose(); 557 | } else if (code < 0 || code >= 5000) { 558 | throw new IllegalArgumentException("Close frame code must be greater or equal than zero and less than 5000"); 559 | } else { 560 | byte[] internalReason = new byte[0]; 561 | if (reason != null) { 562 | internalReason = reason.getBytes(Charset.forName("UTF-8")); 563 | if (internalReason.length > 123) { 564 | throw new IllegalArgumentException("Close frame reason is too large"); 565 | } 566 | } 567 | 568 | byte[] codeLength = Utils.to2ByteArray(code); 569 | byte[] data = Arrays.copyOf(codeLength, 2 + internalReason.length); 570 | System.arraycopy(internalReason, 0, data, codeLength.length, internalReason.length); 571 | 572 | final Payload payload = new Payload(OPCODE_CLOSE, data, false); 573 | new Thread(new Runnable() { 574 | @Override 575 | public void run() { 576 | webSocketConnection.sendInternal(payload); 577 | } 578 | }).start(); 579 | 580 | closeTimer = new Timer(); 581 | closeTimer.schedule(new TimerTask() { 582 | @Override 583 | public void run() { 584 | forceClose(); 585 | } 586 | }, timeout); 587 | } 588 | } 589 | 590 | /** 591 | * This represents an existing WebSocket connection 592 | * 593 | * @author Gustavo Avila 594 | */ 595 | private class WebSocketConnection { 596 | /** 597 | * Flag indicating if there are pending changes waiting to be read by 598 | * the writer thread. It is used to avoid a missed signal between 599 | * threads 600 | */ 601 | private volatile boolean pendingMessages; 602 | 603 | /** 604 | * Flag indicating if the closeInternal() method was called 605 | */ 606 | private volatile boolean isClosed; 607 | 608 | /** 609 | * Flag that indicates that a graceful close is in process 610 | */ 611 | private volatile boolean isClosing; 612 | 613 | /** 614 | * Data waiting to be read from the writer thread 615 | */ 616 | private final Queue queue; 617 | 618 | /** 619 | * This will act as a lock for synchronized statements 620 | */ 621 | private final Object internalLock; 622 | 623 | /** 624 | * Writer thread 625 | */ 626 | private final Thread writerThread; 627 | 628 | /** 629 | * TCP socket for the underlying WebSocket connection 630 | */ 631 | private Socket socket; 632 | 633 | /** 634 | * Socket input stream 635 | */ 636 | private BufferedInputStream bis; 637 | 638 | /** 639 | * Socket output stream 640 | */ 641 | private BufferedOutputStream bos; 642 | 643 | /** 644 | * Initialize the variables that will be used during a valid WebSocket 645 | * connection 646 | */ 647 | private WebSocketConnection() { 648 | this.pendingMessages = false; 649 | this.isClosed = false; 650 | this.isClosing = false; 651 | this.queue = new LinkedList(); 652 | this.internalLock = new Object(); 653 | 654 | this.writerThread = new Thread(new Runnable() { 655 | @Override 656 | public void run() { 657 | synchronized (internalLock) { 658 | while (true) { 659 | if (!pendingMessages) { 660 | try { 661 | internalLock.wait(); 662 | } catch (InterruptedException e) { 663 | // This should never happen 664 | } 665 | } 666 | 667 | pendingMessages = false; 668 | 669 | if (socket.isClosed()) { 670 | return; 671 | } else { 672 | while (queue.size() > 0) { 673 | Payload payload = queue.poll(); 674 | int opcode = payload.getOpcode(); 675 | byte[] data = payload.getData(); 676 | 677 | try { 678 | send(opcode, data); 679 | 680 | if (payload.isCloseEcho()) { 681 | closeInternalInsecure(); 682 | } 683 | } catch (IOException e) { 684 | // Reader thread will notify this 685 | // exception 686 | // This thread just need to stop 687 | return; 688 | } 689 | } 690 | } 691 | } 692 | } 693 | } 694 | }); 695 | } 696 | 697 | /** 698 | * Creates and connects a TCP socket for the underlying connection 699 | * 700 | * @return true is the socket was successfully connected, false 701 | * otherwise 702 | * @throws IOException 703 | */ 704 | private boolean createAndConnectTCPSocket() throws IOException { 705 | synchronized (internalLock) { 706 | if (!isClosed) { 707 | String scheme = uri.getScheme(); 708 | int port = uri.getPort(); 709 | if (scheme != null) { 710 | if (scheme.equals("ws")) { 711 | SocketFactory socketFactory = SocketFactory.getDefault(); 712 | socket = socketFactory.createSocket(); 713 | socket.setSoTimeout(readTimeout); 714 | 715 | if (port != -1) { 716 | socket.connect(new InetSocketAddress(uri.getHost(), port), connectTimeout); 717 | } else { 718 | socket.connect(new InetSocketAddress(uri.getHost(), 80), connectTimeout); 719 | } 720 | } else if (scheme.equals("wss")) { 721 | if (sslSocketFactory == null) { 722 | sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); 723 | } 724 | socket = sslSocketFactory.createSocket(); 725 | socket.setSoTimeout(readTimeout); 726 | 727 | if (port != -1) { 728 | socket.connect(new InetSocketAddress(uri.getHost(), port), connectTimeout); 729 | } else { 730 | socket.connect(new InetSocketAddress(uri.getHost(), 443), connectTimeout); 731 | } 732 | } else { 733 | throw new IllegalSchemeException("The scheme component of the URI should be ws or wss"); 734 | } 735 | } else { 736 | throw new IllegalSchemeException("The scheme component of the URI cannot be null"); 737 | } 738 | 739 | return true; 740 | } 741 | 742 | return false; 743 | } 744 | } 745 | 746 | /** 747 | * Starts the WebSocket connection 748 | * 749 | * @throws IOException 750 | */ 751 | private void startConnection() throws IOException { 752 | bos = new BufferedOutputStream(socket.getOutputStream(), 65536); 753 | 754 | byte[] key = new byte[16]; 755 | Random random = new Random(); 756 | random.nextBytes(key); 757 | String base64Key = Utils.encodeToBase64String(key); 758 | 759 | byte[] handshake = createHandshake(base64Key); 760 | bos.write(handshake); 761 | bos.flush(); 762 | 763 | InputStream inputStream = socket.getInputStream(); 764 | verifyServerHandshake(inputStream, base64Key); 765 | notifyOnOpen(); 766 | writerThread.start(); 767 | 768 | bis = new BufferedInputStream(inputStream, 65536); 769 | read(); 770 | } 771 | 772 | /** 773 | * Creates and returns a byte array containing the client handshake 774 | * 775 | * @param base64Key Random generated Sec-WebSocket-Key 776 | * @return Byte array containing the client handshake 777 | */ 778 | private byte[] createHandshake(String base64Key) { 779 | StringBuilder builder = new StringBuilder(); 780 | 781 | String path = uri.getRawPath(); 782 | String query = uri.getRawQuery(); 783 | 784 | String requestUri; 785 | if (path != null && !path.isEmpty()) { 786 | requestUri = path; 787 | } else { 788 | requestUri = "/"; 789 | } 790 | 791 | if (query != null && !query.isEmpty()) { 792 | requestUri = requestUri + "?" + query; 793 | } 794 | 795 | builder.append("GET " + requestUri + " HTTP/1.1"); 796 | builder.append("\r\n"); 797 | 798 | String host; 799 | if (uri.getPort() == -1) { 800 | host = uri.getHost(); 801 | } else { 802 | host = uri.getHost() + ":" + uri.getPort(); 803 | } 804 | 805 | builder.append("Host: " + host); 806 | builder.append("\r\n"); 807 | 808 | builder.append("Upgrade: websocket"); 809 | builder.append("\r\n"); 810 | 811 | builder.append("Connection: Upgrade"); 812 | builder.append("\r\n"); 813 | 814 | builder.append("Sec-WebSocket-Key: " + base64Key); 815 | builder.append("\r\n"); 816 | 817 | builder.append("Sec-WebSocket-Version: 13"); 818 | builder.append("\r\n"); 819 | 820 | for (Map.Entry entry : headers.entrySet()) { 821 | builder.append(entry.getKey() + ": " + entry.getValue()); 822 | builder.append("\r\n"); 823 | } 824 | 825 | builder.append("\r\n"); 826 | 827 | String handshake = builder.toString(); 828 | return handshake.getBytes(Charset.forName("ASCII")); 829 | } 830 | 831 | /** 832 | * Verifies the validity of the server handshake 833 | * 834 | * @param inputStream Socket input stream 835 | * @param secWebSocketKey Random generated Sec-WebSocket-Key 836 | * @throws IOException 837 | */ 838 | private void verifyServerHandshake(InputStream inputStream, String secWebSocketKey) throws IOException { 839 | Queue lines = new LinkedList(); 840 | StringBuilder builder = new StringBuilder(); 841 | boolean lastLineBreak = false; 842 | int bytesRead = 0; 843 | 844 | outer:do { 845 | inner:do { 846 | int result = inputStream.read(); 847 | if (result == -1) { 848 | throw new IOException("Unexpected end of stream"); 849 | } 850 | 851 | char c = (char) result; 852 | bytesRead++; 853 | if (c == '\r') { 854 | result = inputStream.read(); 855 | if (result == -1) { 856 | throw new IOException("Unexpected end of stream"); 857 | } 858 | 859 | c = (char) result; 860 | bytesRead++; 861 | if (c == '\n') { 862 | if (lastLineBreak) { 863 | break outer; 864 | } 865 | lastLineBreak = true; 866 | break inner; 867 | } else { 868 | throw new InvalidServerHandshakeException("Invalid handshake format"); 869 | } 870 | } else if (c == '\n') { 871 | if (lastLineBreak) { 872 | break outer; 873 | } 874 | lastLineBreak = true; 875 | break inner; 876 | } else { 877 | lastLineBreak = false; 878 | builder.append(c); 879 | } 880 | } while (bytesRead <= MAX_HEADER_SIZE); 881 | 882 | lines.offer(builder.toString()); 883 | builder.setLength(0); 884 | } while (bytesRead <= MAX_HEADER_SIZE); 885 | 886 | if (bytesRead > MAX_HEADER_SIZE) { 887 | throw new RuntimeException("Entity too large"); 888 | } 889 | 890 | String statusLine = lines.poll(); 891 | if (statusLine == null) { 892 | throw new InvalidServerHandshakeException("There is no status line"); 893 | } 894 | 895 | String[] statusLineParts = statusLine.split(" "); 896 | if (statusLineParts.length > 1) { 897 | String statusCode = statusLineParts[1]; 898 | if (!statusCode.equals("101")) { 899 | throw new InvalidServerHandshakeException("Invalid status code. Expected 101, received: " + statusCode); 900 | } 901 | } else { 902 | throw new InvalidServerHandshakeException("Invalid status line format"); 903 | } 904 | 905 | Map headers = new HashMap(); 906 | for (String s : lines) { 907 | String[] parts = s.split(":", 2); 908 | if (parts.length == 2) { 909 | headers.put(parts[0].trim().toLowerCase(), parts[1].trim()); 910 | } else { 911 | throw new InvalidServerHandshakeException("Invalid headers format"); 912 | } 913 | } 914 | 915 | String upgradeValue = headers.get("upgrade"); 916 | if (upgradeValue == null) { 917 | throw new InvalidServerHandshakeException("There is no header named Upgrade"); 918 | } 919 | upgradeValue = upgradeValue.toLowerCase(); 920 | if (!upgradeValue.equals("websocket")) { 921 | throw new InvalidServerHandshakeException("Invalid value for header Upgrade. Expected: websocket, received: " + upgradeValue); 922 | } 923 | 924 | String connectionValue = headers.get("connection"); 925 | if (connectionValue == null) { 926 | throw new InvalidServerHandshakeException("There is no header named Connection"); 927 | } 928 | connectionValue = connectionValue.toLowerCase(); 929 | if (!connectionValue.equals("upgrade")) { 930 | throw new InvalidServerHandshakeException("Invalid value for header Connection. Expected: upgrade, received: " + connectionValue); 931 | } 932 | 933 | String secWebSocketAcceptValue = headers.get("sec-websocket-accept"); 934 | if (secWebSocketAcceptValue == null) { 935 | throw new InvalidServerHandshakeException("There is no header named Sec-WebSocket-Accept"); 936 | } 937 | 938 | String keyConcatenation = secWebSocketKey + GUID; 939 | try { 940 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 941 | md.update(keyConcatenation.getBytes(Charset.forName("ASCII"))); 942 | byte[] sha1 = md.digest(); 943 | String secWebSocketAccept = Utils.encodeToBase64String(sha1); 944 | if (!secWebSocketAcceptValue.equals(secWebSocketAccept)) { 945 | throw new InvalidServerHandshakeException("Invalid value for header Sec-WebSocket-Accept. Expected: " + secWebSocketAccept + ", received: " + secWebSocketAcceptValue); 946 | } 947 | } catch (NoSuchAlgorithmException e) { 948 | throw new RuntimeException("Your platform does not support the SHA-1 algorithm"); 949 | } 950 | } 951 | 952 | /** 953 | * Sends a message to the WebSocket server 954 | * 955 | * @param opcode Message opcode 956 | * @param payload Message payload 957 | * @throws IOException 958 | */ 959 | private void send(int opcode, byte[] payload) throws IOException { 960 | // The position of the data frame in which the next portion of code 961 | // will start writing bytes 962 | int nextPosition; 963 | 964 | // The data frame 965 | byte[] frame; 966 | 967 | // The length of the payload data. 968 | // If the payload is null, length will be 0. 969 | int length = payload == null ? 0 : payload.length; 970 | 971 | if (length < 126) { 972 | // If payload length is less than 126, 973 | // the frame must have the first two bytes, plus 4 bytes for the 974 | // masking key 975 | // plus the length of the payload 976 | frame = new byte[6 + length]; 977 | 978 | // The first two bytes 979 | frame[0] = (byte) (-128 | opcode); 980 | frame[1] = (byte) (-128 | length); 981 | 982 | // The masking key will start at position 2 983 | nextPosition = 2; 984 | } else if (length < 65536) { 985 | // If payload length is greater than 126 and less than 65536, 986 | // the frame must have the first two bytes, plus 2 bytes for the 987 | // extended payload length, 988 | // plus 4 bytes for the masking key, plus the length of the 989 | // payload 990 | frame = new byte[8 + length]; 991 | 992 | // The first two bytes 993 | frame[0] = (byte) (-128 | opcode); 994 | frame[1] = -2; 995 | 996 | // Puts the length into the data frame 997 | byte[] array = Utils.to2ByteArray(length); 998 | frame[2] = array[0]; 999 | frame[3] = array[1]; 1000 | 1001 | // The masking key will start at position 4 1002 | nextPosition = 4; 1003 | } else { 1004 | // If payload length is greater or equal than 65536, 1005 | // the frame must have the first two bytes, plus 8 bytes for the 1006 | // extended payload length, 1007 | // plus 4 bytes for the masking key, plus the length of the 1008 | // payload 1009 | frame = new byte[14 + length]; 1010 | 1011 | // The first two bytes 1012 | frame[0] = (byte) (-128 | opcode); 1013 | frame[1] = -1; 1014 | 1015 | // Puts the length into the data frame 1016 | byte[] array = Utils.to8ByteArray(length); 1017 | frame[2] = array[0]; 1018 | frame[3] = array[1]; 1019 | frame[4] = array[2]; 1020 | frame[5] = array[3]; 1021 | frame[6] = array[4]; 1022 | frame[7] = array[5]; 1023 | frame[8] = array[6]; 1024 | frame[9] = array[7]; 1025 | 1026 | // The masking key will start at position 10 1027 | nextPosition = 10; 1028 | } 1029 | 1030 | // Generate a random 4-byte masking key 1031 | byte[] mask = new byte[4]; 1032 | secureRandom.nextBytes(mask); 1033 | 1034 | // Puts the masking key into the data frame 1035 | frame[nextPosition] = mask[0]; 1036 | frame[nextPosition + 1] = mask[1]; 1037 | frame[nextPosition + 2] = mask[2]; 1038 | frame[nextPosition + 3] = mask[3]; 1039 | nextPosition += 4; 1040 | 1041 | // Puts the masked payload data into the data frame 1042 | for (int i = 0; i < length; i++) { 1043 | frame[nextPosition] = ((byte) (payload[i] ^ mask[i % 4])); 1044 | nextPosition++; 1045 | } 1046 | 1047 | // Sends the data frame 1048 | bos.write(frame); 1049 | bos.flush(); 1050 | } 1051 | 1052 | /** 1053 | * Listen for changes coming from the WebSocket server 1054 | * 1055 | * @throws IOException 1056 | */ 1057 | private void read() throws IOException { 1058 | // If message contains fragmented parts we should put it all together. 1059 | int opcodeFragment = -1; 1060 | LinkedList messageParts = new LinkedList(); 1061 | 1062 | // The first byte of every data frame 1063 | int firstByte; 1064 | 1065 | // Loop until end of stream is reached. 1066 | while ((firstByte = bis.read()) != -1) { 1067 | // Data contained in the first byte 1068 | 1069 | // If the flag is on we have more frames for this message. 1070 | int fin = (firstByte << 24) >>> 31; 1071 | 1072 | // int rsv1 = (firstByte << 25) >>> 31; 1073 | // int rsv2 = (firstByte << 26) >>> 31; 1074 | // int rsv3 = (firstByte << 27) >>> 31; 1075 | 1076 | int opcode = (firstByte << 28) >>> 28; 1077 | 1078 | // Only first frame will have real opcode (text or binary), 1079 | // In next frames opcode will be zero (continuation) 1080 | if (fin == 0x0 && opcodeFragment == -1) { 1081 | opcodeFragment = opcode; 1082 | } 1083 | 1084 | // Reads the second byte 1085 | int secondByte = bis.read(); 1086 | 1087 | // Data contained in the second byte 1088 | // int mask = (secondByte << 24) >>> 31; 1089 | int payloadLength = (secondByte << 25) >>> 25; 1090 | 1091 | // If the length of payload data is less than 126, that's the 1092 | // final 1093 | // payload length 1094 | // Otherwise, it must be calculated as follows 1095 | if (payloadLength == 126) { 1096 | // Attempts to read the next 2 bytes 1097 | byte[] nextTwoBytes = new byte[2]; 1098 | for (int i = 0; i < 2; i++) { 1099 | byte b = (byte) bis.read(); 1100 | nextTwoBytes[i] = b; 1101 | } 1102 | 1103 | // Those last 2 bytes will be interpreted as a 16-bit 1104 | // unsigned 1105 | // integer 1106 | byte[] integer = new byte[]{0, 0, nextTwoBytes[0], nextTwoBytes[1]}; 1107 | payloadLength = Utils.fromByteArray(integer); 1108 | } else if (payloadLength == 127) { 1109 | // Attempts to read the next 8 bytes 1110 | byte[] nextEightBytes = new byte[8]; 1111 | for (int i = 0; i < 8; i++) { 1112 | byte b = (byte) bis.read(); 1113 | nextEightBytes[i] = b; 1114 | } 1115 | 1116 | // Only the last 4 bytes matter because Java doesn't support 1117 | // arrays with more than 2^31 -1 elements, so a 64-bit 1118 | // unsigned 1119 | // integer cannot be processed 1120 | // Those last 4 bytes will be interpreted as a 32-bit 1121 | // unsigned 1122 | // integer 1123 | byte[] integer = new byte[]{nextEightBytes[4], nextEightBytes[5], nextEightBytes[6], 1124 | nextEightBytes[7]}; 1125 | payloadLength = Utils.fromByteArray(integer); 1126 | } 1127 | 1128 | // Attempts to read the payload data 1129 | byte[] data = new byte[payloadLength]; 1130 | for (int i = 0; i < payloadLength; i++) { 1131 | byte b = (byte) bis.read(); 1132 | data[i] = b; 1133 | } 1134 | 1135 | if (fin == 0x1 && opcode == OPCODE_CONTINUATION) { 1136 | // This is only called for fragments so a control frame may be injected in the middle of a fragment as per specification 1137 | 1138 | // If we already have some fragments, just add last and put it together 1139 | messageParts.add(data); 1140 | // Calculate total size of all parts 1141 | int fullSize = 0; 1142 | int offset = 0; 1143 | for (byte[] fragment : messageParts) 1144 | fullSize += fragment.length; 1145 | 1146 | byte[] fullMessage = new byte[fullSize]; 1147 | 1148 | // Copy all parts into one array 1149 | for (byte[] fragment : messageParts) { 1150 | System.arraycopy(fragment, 0, fullMessage, offset, fragment.length); 1151 | offset += fragment.length; 1152 | } 1153 | 1154 | data = fullMessage; 1155 | messageParts.clear(); 1156 | 1157 | opcode = opcodeFragment; 1158 | opcodeFragment = -1; 1159 | } else if (fin == 0x0 && (opcode == OPCODE_CONTINUATION || opcode == OPCODE_TEXT || opcode == OPCODE_BINARY)) { 1160 | // Collect this fragment and go read next frame. 1161 | messageParts.add(data); 1162 | continue; 1163 | } 1164 | 1165 | // Execute the action depending on the opcode 1166 | switch (opcode) { 1167 | case OPCODE_TEXT: 1168 | notifyOnTextReceived(new String(data, Charset.forName("UTF-8"))); 1169 | break; 1170 | case OPCODE_BINARY: 1171 | notifyOnBinaryReceived(data); 1172 | break; 1173 | case OPCODE_CLOSE: 1174 | if (data.length > 125) { 1175 | closeInternal(); 1176 | Exception e = new InvalidReceivedFrameException("Close frame payload is too big"); 1177 | notifyOnException(e); 1178 | return; 1179 | } else { 1180 | int code = getCloseCode(data); 1181 | String reason = getCloseReason(data); 1182 | notifyOnCloseReceived(code, reason); 1183 | } 1184 | 1185 | synchronized (internalLock) { 1186 | if (isClosing) { 1187 | // This is the echo of a client initiated close so the connection can be closed immediately 1188 | closeInternalInsecure(); 1189 | return; 1190 | } else { 1191 | // This is a server initiated close so an echo must be sent 1192 | Payload payload = new Payload(OPCODE_CLOSE, data, true); 1193 | sendInternalInsecure(payload); 1194 | break; 1195 | } 1196 | } 1197 | case OPCODE_PING: 1198 | notifyOnPingReceived(data); 1199 | sendPong(data); 1200 | break; 1201 | case OPCODE_PONG: 1202 | notifyOnPongReceived(data); 1203 | break; 1204 | default: 1205 | closeInternal(); 1206 | Exception e = new InvalidReceivedFrameException("Unknown opcode: 0x" + Integer.toHexString(opcode)); 1207 | notifyOnException(e); 1208 | return; 1209 | } 1210 | } 1211 | 1212 | synchronized (internalLock) { 1213 | // There is no need to notify an exception if the connection is closing 1214 | if (!isClosing) { 1215 | // An IOException must be thrown because the connection didn't close gracefully 1216 | throw new IOException("Unexpected end of stream"); 1217 | } 1218 | } 1219 | } 1220 | 1221 | /** 1222 | * Puts the payload into the out queue and notifies the writer thread that new data is available 1223 | * 1224 | * @param payload Payload to be sent to the WebSocket server 1225 | */ 1226 | private void sendInternal(Payload payload) { 1227 | synchronized (internalLock) { 1228 | sendInternalInsecure(payload); 1229 | } 1230 | } 1231 | 1232 | private void sendInternalInsecure(Payload payload) { 1233 | if (!isClosing) { 1234 | if (payload.getOpcode() == OPCODE_CLOSE) { 1235 | isClosing = true; 1236 | } 1237 | queue.offer(payload); 1238 | pendingMessages = true; 1239 | internalLock.notify(); 1240 | } 1241 | } 1242 | 1243 | /** 1244 | * Closes the underlying WebSocket connection and notifies the writer 1245 | * thread and the reconnection thread that they must finish 1246 | */ 1247 | private void closeInternal() { 1248 | synchronized (internalLock) { 1249 | closeInternalInsecure(); 1250 | } 1251 | } 1252 | 1253 | private void closeInternalInsecure() { 1254 | try { 1255 | if (!isClosed) { 1256 | isClosed = true; 1257 | if (socket != null) { 1258 | socket.close(); 1259 | pendingMessages = true; 1260 | internalLock.notify(); 1261 | } 1262 | } 1263 | 1264 | if (closeTimer != null) { 1265 | closeTimer.cancel(); 1266 | closeTimer = null; 1267 | } 1268 | } catch (IOException e) { 1269 | // This should never happen 1270 | } 1271 | } 1272 | 1273 | private int getCloseCode(byte[] data) { 1274 | if (data.length > 1) { 1275 | byte[] baseCode = Arrays.copyOfRange(data, 0, 2); 1276 | return Utils.fromByteArray(new byte[]{0, 0, baseCode[0], baseCode[1]}); 1277 | } 1278 | return -1; 1279 | } 1280 | 1281 | private String getCloseReason(byte[] data) { 1282 | if (data.length > 2) { 1283 | byte[] baseReason = Arrays.copyOfRange(data, 2, data.length); 1284 | return new String(baseReason, Charset.forName("UTF-8")); 1285 | } 1286 | return null; 1287 | } 1288 | } 1289 | } 1290 | --------------------------------------------------------------------------------