├── .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 |
--------------------------------------------------------------------------------