com.neovisionaries.ws.client.WebSocket Maven / Gradle / Ivy
Show all versions of nv-websocket-client Show documentation
/*
* Copyright (C) 2015-2017 Neo Visionaries Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.neovisionaries.ws.client;
import static com.neovisionaries.ws.client.WebSocketState.CLOSED;
import static com.neovisionaries.ws.client.WebSocketState.CLOSING;
import static com.neovisionaries.ws.client.WebSocketState.CONNECTING;
import static com.neovisionaries.ws.client.WebSocketState.CREATED;
import static com.neovisionaries.ws.client.WebSocketState.OPEN;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import com.neovisionaries.ws.client.StateManager.CloseInitiator;
/**
* WebSocket.
*
* Create WebSocketFactory
*
*
* {@link WebSocketFactory} is a factory class that creates
* {@link WebSocket} instances. The first step is to create a
* {@code WebSocketFactory} instance.
*
*
*
* // Create a WebSocketFactory instance.
* WebSocketFactory factory = new {@link WebSocketFactory#WebSocketFactory()
* WebSocketFactory()};
*
*
*
* By default, {@code WebSocketFactory} uses {@link
* javax.net.SocketFactory SocketFactory}{@code .}{@link
* javax.net.SocketFactory#getDefault() getDefault()} for
* non-secure WebSocket connections ({@code ws:}) and {@link
* javax.net.ssl.SSLSocketFactory SSLSocketFactory}{@code
* .}{@link javax.net.ssl.SSLSocketFactory#getDefault()
* getDefault()} for secure WebSocket connections ({@code
* wss:}). You can change this default behavior by using
* {@code WebSocketFactory.}{@link
* WebSocketFactory#setSocketFactory(javax.net.SocketFactory)
* setSocketFactory} method, {@code WebSocketFactory.}{@link
* WebSocketFactory#setSSLSocketFactory(javax.net.ssl.SSLSocketFactory)
* setSSLSocketFactory} method and {@code WebSocketFactory.}{@link
* WebSocketFactory#setSSLContext(javax.net.ssl.SSLContext)
* setSSLContext} method. Note that you don't have to call a {@code
* setSSL*} method at all if you use the default SSL configuration.
* Also note that calling {@code setSSLSocketFactory} method has no
* meaning if you have called {@code setSSLContext} method. See the
* description of {@code WebSocketFactory.}{@link
* WebSocketFactory#createSocket(URI) createSocket(URI)} method for
* details.
*
*
*
* The following is an example to set a custom SSL context to a
* {@code WebSocketFactory} instance. (Again, you don't have to call a
* {@code setSSL*} method if you use the default SSL configuration.)
*
*
*
* // Create a custom SSL context.
* SSLContext context = NaiveSSLContext.getInstance("TLS");
*
* // Set the custom SSL context.
* factory.{@link WebSocketFactory#setSSLContext(javax.net.ssl.SSLContext)
* setSSLContext}(context);
*
* // Disable manual hostname verification for NaiveSSLContext.
* //
* // Manual hostname verification has been enabled since the
* // version 2.1. Because the verification is executed manually
* // after Socket.connect(SocketAddress, int) succeeds, the
* // hostname verification is always executed even if you has
* // passed an SSLContext which naively accepts any server
* // certificate. However, this behavior is not desirable in
* // some cases and you may want to disable the hostname
* // verification. You can disable the hostname verification
* // by calling WebSocketFactory.setVerifyHostname(false).
* factory.{@link WebSocketFactory#setVerifyHostname(boolean) setVerifyHostname}(false);
*
*
*
* NaiveSSLContext used in the above example is a factory class to
* create an {@link javax.net.ssl.SSLContext SSLContext} which naively
* accepts all certificates without verification. It's enough for testing
* purposes. When you see an error message
* "unable to find valid certificate path to requested target" while
* testing, try {@code NaiveSSLContext}.
*
*
* HTTP Proxy
*
*
* If a WebSocket endpoint needs to be accessed via an HTTP proxy,
* information about the proxy server has to be set to a {@code
* WebSocketFactory} instance before creating a {@code WebSocket}
* instance. Proxy settings are represented by {@link ProxySettings}
* class. A {@code WebSocketFactory} instance has an associated
* {@code ProxySettings} instance and it can be obtained by calling
* {@code WebSocketFactory.}{@link WebSocketFactory#getProxySettings()
* getProxySettings()} method.
*
*
*
* // Get the associated ProxySettings instance.
* {@link ProxySettings} settings = factory.{@link
* WebSocketFactory#getProxySettings() getProxySettings()};
*
*
*
* {@code ProxySettings} class has methods to set information about
* a proxy server such as {@link ProxySettings#setHost(String) setHost}
* method and {@link ProxySettings#setPort(int) setPort} method. The
* following is an example to set a secure (https
) proxy
* server.
*
*
*
* // Set a proxy server.
* settings.{@link ProxySettings#setServer(String)
* setServer}("https://proxy.example.com");
*
*
*
* If credentials are required for authentication at a proxy server,
* {@link ProxySettings#setId(String) setId} method and {@link
* ProxySettings#setPassword(String) setPassword} method, or
* {@link ProxySettings#setCredentials(String, String) setCredentials}
* method can be used to set the credentials. Note that, however,
* the current implementation supports only Basic Authentication.
*
*
*
* // Set credentials for authentication at a proxy server.
* settings.{@link ProxySettings#setCredentials(String, String)
* setCredentials}(id, password);
*
*
*
* Create WebSocket
*
*
* {@link WebSocket} class represents a WebSocket. Its instances are
* created by calling one of {@code createSocket} methods of a {@link
* WebSocketFactory} instance. Below is the simplest example to create
* a {@code WebSocket} instance.
*
*
*
* // Create a WebSocket. The scheme part can be one of the following:
* // 'ws', 'wss', 'http' and 'https' (case-insensitive). The user info
* // part, if any, is interpreted as expected. If a raw socket failed
* // to be created, an IOException is thrown.
* WebSocket ws = new {@link WebSocketFactory#WebSocketFactory()
* WebSocketFactory()}
* .{@link WebSocketFactory#createSocket(String)
* createWebSocket}("ws://localhost/endpoint");
*
*
*
* There are two ways to set a timeout value for socket connection. The
* first way is to call {@link WebSocketFactory#setConnectionTimeout(int)
* setConnectionTimeout(int timeout)} method of {@code WebSocketFactory}.
*
*
*
* // Create a WebSocket factory and set 5000 milliseconds as a timeout
* // value for socket connection.
* WebSocketFactory factory = new WebSocketFactory().{@link
* WebSocketFactory#setConnectionTimeout(int) setConnectionTimeout}(5000);
*
* // Create a WebSocket. The timeout value set above is used.
* WebSocket ws = factory.{@link WebSocketFactory#createSocket(String)
* createWebSocket}("ws://localhost/endpoint");
*
*
*
* The other way is to give a timeout value to a {@code createSocket} method.
*
*
*
* // Create a WebSocket factory. The timeout value remains 0.
* WebSocketFactory factory = new WebSocketFactory();
*
* // Create a WebSocket with a socket connection timeout value.
* WebSocket ws = factory.{@link WebSocketFactory#createSocket(String, int)
* createWebSocket}("ws://localhost/endpoint", 5000);
*
*
*
* The timeout value is passed to {@link Socket#connect(java.net.SocketAddress, int)
* connect}{@code (}{@link java.net.SocketAddress SocketAddress}{@code , int)}
* method of {@link java.net.Socket}.
*
*
* Register Listener
*
*
* After creating a {@code WebSocket} instance, you should call {@link
* #addListener(WebSocketListener)} method to register a {@link
* WebSocketListener} that receives WebSocket events. {@link
* WebSocketAdapter} is an empty implementation of {@link
* WebSocketListener} interface.
*
*
*
* // Register a listener to receive WebSocket events.
* ws.{@link #addListener(WebSocketListener) addListener}(new {@link
* WebSocketAdapter#WebSocketAdapter() WebSocketAdapter()} {
* {@code @}Override
* public void {@link WebSocketListener#onTextMessage(WebSocket, String)
* onTextMessage}(WebSocket websocket, String message) throws Exception {
* // Received a text message.
* ......
* }
* });
*
*
*
* The table below is the list of callback methods defined in {@code WebSocketListener}
* interface.
*
*
*
*
* {@code WebSocketListener} methods
*
*
* Method
* Description
*
*
*
*
* {@link WebSocketListener#handleCallbackError(WebSocket, Throwable) handleCallbackError}
* Called when an onXxx()
method threw a {@code Throwable}.
*
*
* {@link WebSocketListener#onBinaryFrame(WebSocket, WebSocketFrame) onBinaryFrame}
* Called when a binary frame was received.
*
*
* {@link WebSocketListener#onBinaryMessage(WebSocket, byte[]) onBinaryMessage}
* Called when a binary message was received.
*
*
* {@link WebSocketListener#onCloseFrame(WebSocket, WebSocketFrame) onCloseFrame}
* Called when a close frame was received.
*
*
* {@link WebSocketListener#onConnected(WebSocket, Map) onConnected}
* Called after the opening handshake succeeded.
*
*
* {@link WebSocketListener#onConnectError(WebSocket, WebSocketException) onConnectError}
* Called when {@link #connectAsynchronously()} failed.
*
*
* {@link WebSocketListener#onContinuationFrame(WebSocket, WebSocketFrame) onContinuationFrame}
* Called when a continuation frame was received.
*
*
* {@link WebSocketListener#onDisconnected(WebSocket, WebSocketFrame, WebSocketFrame, boolean) onDisconnected}
* Called after a WebSocket connection was closed.
*
*
* {@link WebSocketListener#onError(WebSocket, WebSocketException) onError}
* Called when an error occurred.
*
*
* {@link WebSocketListener#onFrame(WebSocket, WebSocketFrame) onFrame}
* Called when a frame was received.
*
*
* {@link WebSocketListener#onFrameError(WebSocket, WebSocketException, WebSocketFrame) onFrameError}
* Called when a frame failed to be read.
*
*
* {@link WebSocketListener#onFrameSent(WebSocket, WebSocketFrame) onFrameSent}
* Called when a frame was sent.
*
*
* {@link WebSocketListener#onFrameUnsent(WebSocket, WebSocketFrame) onFrameUnsent}
* Called when a frame was not sent.
*
*
* {@link WebSocketListener#onMessageDecompressionError(WebSocket, WebSocketException, byte[]) onMessageDecompressionError}
* Called when a message failed to be decompressed.
*
*
* {@link WebSocketListener#onMessageError(WebSocket, WebSocketException, List) onMessageError}
* Called when a message failed to be constructed.
*
*
* {@link WebSocketListener#onPingFrame(WebSocket, WebSocketFrame) onPingFrame}
* Called when a ping frame was received.
*
*
* {@link WebSocketListener#onPongFrame(WebSocket, WebSocketFrame) onPongFrame}
* Called when a pong frame was received.
*
*
* {@link WebSocketListener#onSendError(WebSocket, WebSocketException, WebSocketFrame) onSendError}
* Called when an error occurred on sending a frame.
*
*
* {@link WebSocketListener#onSendingFrame(WebSocket, WebSocketFrame) onSendingFrame}
* Called before a frame is sent.
*
*
* {@link WebSocketListener#onSendingHandshake(WebSocket, String, List) onSendingHandshake}
* Called before an opening handshake is sent.
*
*
* {@link WebSocketListener#onStateChanged(WebSocket, WebSocketState) onStateChanged}
* Called when the state of WebSocket changed.
*
*
* {@link WebSocketListener#onTextFrame(WebSocket, WebSocketFrame) onTextFrame}
* Called when a text frame was received.
*
*
* {@link WebSocketListener#onTextMessage(WebSocket, String) onTextMessage}
* Called when a text message was received.
*
*
* {@link WebSocketListener#onTextMessageError(WebSocket, WebSocketException, byte[]) onTextMessageError}
* Called when a text message failed to be constructed.
*
*
* {@link WebSocketListener#onThreadCreated(WebSocket, ThreadType, Thread) onThreadCreated}
* Called after a thread was created.
*
*
* {@link WebSocketListener#onThreadStarted(WebSocket, ThreadType, Thread) onThreadStarted}
* Called at the beginning of a thread's {@code run()} method.
*
*
* {@link WebSocketListener#onThreadStopping(WebSocket, ThreadType, Thread) onThreadStopping}
* Called at the end of a thread's {@code run()} method.
*
*
* {@link WebSocketListener#onUnexpectedError(WebSocket, WebSocketException) onUnexpectedError}
* Called when an uncaught throwable was detected.
*
*
*
*
*
* Configure WebSocket
*
*
* Before starting a WebSocket opening handshake with the server, you can configure the
* {@code WebSocket} instance by using the following methods.
*
*
*
*
* Methods for Configuration
*
*
* METHOD
* DESCRIPTION
*
*
*
*
* {@link #addProtocol(String) addProtocol}
* Adds an element to {@code Sec-WebSocket-Protocol}
*
*
* {@link #addExtension(WebSocketExtension) addExtension}
* Adds an element to {@code Sec-WebSocket-Extensions}
*
*
* {@link #addHeader(String, String) addHeader}
* Adds an arbitrary HTTP header.
*
*
* {@link #setUserInfo(String, String) setUserInfo}
* Adds {@code Authorization} header for Basic Authentication.
*
*
* {@link #getSocket() getSocket}
* Gets the underlying {@link Socket} instance to configure it.
*
*
* {@link #setExtended(boolean) setExtended}
* Disables validity checks on RSV1/RSV2/RSV3 and opcode.
*
*
* {@link #setFrameQueueSize(int) setFrameQueueSize}
* Set the size of the frame queue for congestion control.
*
*
* {@link #setMaxPayloadSize(int) setMaxPayloadSize}
* Set the maximum payload size.
*
*
* {@link #setMissingCloseFrameAllowed(boolean) setMissingCloseFrameAllowed}
* Set whether to allow the server to close the connection without sending a close frame.
*
*
*
*
*
* Connect To Server
*
*
* By calling {@link #connect()} method, connection to the server is
* established and a WebSocket opening handshake is performed
* synchronously. If an error occurred during the handshake,
* a {@link WebSocketException} would be thrown. Instead, when the
* handshake succeeds, the {@code connect()} implementation creates
* threads and starts them to read and write WebSocket frames
* asynchronously.
*
*
*
* try
* {
* // Connect to the server and perform an opening handshake.
* // This method blocks until the opening handshake is finished.
* ws.{@link #connect()};
* }
* catch ({@link OpeningHandshakeException} e)
* {
* // A violation against the WebSocket protocol was detected
* // during the opening handshake.
* }
* catch ({@link HostnameUnverifiedException} e)
* {
* // The certificate of the peer does not match the expected hostname.
* }
* catch ({@link WebSocketException} e)
* {
* // Failed to establish a WebSocket connection.
* }
*
*
*
* In some cases, {@code connect()} method throws {@link OpeningHandshakeException}
* which is a subclass of {@code WebSocketException} (since version 1.19).
* {@code OpeningHandshakeException} provides additional methods such as
* {@link OpeningHandshakeException#getStatusLine() getStatusLine()},
* {@link OpeningHandshakeException#getHeaders() getHeaders()} and
* {@link OpeningHandshakeException#getBody() getBody()} to access the
* response from a server. The following snippet is an example to print
* information that the exception holds.
*
*
*
* catch ({@link OpeningHandshakeException} e)
* {
* // Status line.
* {@link StatusLine} sl = e.{@link OpeningHandshakeException#getStatusLine() getStatusLine()};
* System.out.println("=== Status Line ===");
* System.out.format("HTTP Version = %s\n", sl.{@link StatusLine#getHttpVersion() getHttpVersion()});
* System.out.format("Status Code = %d\n", sl.{@link StatusLine#getStatusCode() getStatusCode()});
* System.out.format("Reason Phrase = %s\n", sl.{@link StatusLine#getReasonPhrase() getReasonPhrase()});
*
* // HTTP headers.
* Map<String, List<String>> headers = e.{@link OpeningHandshakeException#getHeaders() getHeaders()};
* System.out.println("=== HTTP Headers ===");
* for (Map.Entry<String, List<String>> entry : headers.entrySet())
* {
* // Header name.
* String name = entry.getKey();
*
* // Values of the header.
* List<String> values = entry.getValue();
*
* if (values == null || values.size() == 0)
* {
* // Print the name only.
* System.out.println(name);
* continue;
* }
*
* for (String value : values)
* {
* // Print the name and the value.
* System.out.format("%s: %s\n", name, value);
* }
* }
* }
*
*
*
* Also, {@code connect()} method throws {@link HostnameUnverifiedException}
* which is a subclass of {@code WebSocketException} (since version 2.1) when
* the certificate of the peer does not match the expected hostname.
*
*
* Connect To Server Asynchronously
*
*
* The simplest way to call {@code connect()} method asynchronously is to
* use {@link #connectAsynchronously()} method. The implementation of the
* method creates a thread and calls {@code connect()} method in the thread.
* When the {@code connect()} call failed, {@link
* WebSocketListener#onConnectError(WebSocket, WebSocketException)
* onConnectError()} of {@code WebSocketListener} would be called. Note that
* {@code onConnectError()} is called only when {@code connectAsynchronously()}
* was used and the {@code connect()} call executed in the background thread
* failed. Neither direct synchronous {@code connect()} nor
* {@link WebSocket#connect(java.util.concurrent.ExecutorService)
* connect(ExecutorService)} (described below) will trigger the callback method.
*
*
*
* // Connect to the server asynchronously.
* ws.{@link #connectAsynchronously()};
*
*
*
*
* Another way to call {@code connect()} method asynchronously is to use
* {@link #connect(ExecutorService)} method. The method performs a WebSocket
* opening handshake asynchronously using the given {@link ExecutorService}.
*
*
*
* // Prepare an ExecutorService.
* {@link ExecutorService} es = {@link java.util.concurrent.Executors Executors}.{@link
* java.util.concurrent.Executors#newSingleThreadExecutor() newSingleThreadExecutor()};
*
* // Connect to the server asynchronously.
* {@link Future}{@code } future = ws.{@link #connect(ExecutorService) connect}(es);
*
* try
* {
* // Wait for the opening handshake to complete.
* future.get();
* }
* catch ({@link java.util.concurrent.ExecutionException ExecutionException} e)
* {
* if (e.getCause() instanceof {@link WebSocketException})
* {
* ......
* }
* }
*
*
*
* The implementation of {@code connect(ExecutorService)} method creates
* a {@link java.util.concurrent.Callable Callable}{@code }
* instance by calling {@link #connectable()} method and passes the
* instance to {@link ExecutorService#submit(Callable) submit(Callable)}
* method of the given {@code ExecutorService}. What the implementation
* of {@link Callable#call() call()} method of the {@code Callable}
* instance does is just to call the synchronous {@code connect()}.
*
*
* Send Frames
*
*
* WebSocket frames can be sent by {@link #sendFrame(WebSocketFrame)}
* method. Other sendXxx
methods such as {@link
* #sendText(String)} are aliases of {@code sendFrame} method. All of
* the sendXxx
methods work asynchronously.
* However, under some conditions, sendXxx
methods
* may block. See Congestion Control
* for details.
*
*
*
* Below
* are some examples of sendXxx
methods. Note that
* in normal cases, you don't have to call {@link #sendClose()} method
* and {@link #sendPong()} (or their variants) explicitly because they
* are called automatically when appropriate.
*
*
*
* // Send a text frame.
* ws.{@link #sendText(String) sendText}("Hello.");
*
* // Send a binary frame.
* byte[] binary = ......;
* ws.{@link #sendBinary(byte[]) sendBinary}(binary);
*
* // Send a ping frame.
* ws.{@link #sendPing(String) sendPing}("Are you there?");
*
*
*
* If you want to send fragmented frames, you have to know the details
* of the specification (5.4. Fragmentation). Below is an example to send a text message
* ({@code "How are you?"}) which consists of 3 fragmented frames.
*
*
*
* // The first frame must be either a text frame or a binary frame.
* // And its FIN bit must be cleared.
* WebSocketFrame firstFrame = WebSocketFrame
* .{@link WebSocketFrame#createTextFrame(String)
* createTextFrame}("How ")
* .{@link WebSocketFrame#setFin(boolean) setFin}(false);
*
* // Subsequent frames must be continuation frames. The FIN bit of
* // all continuation frames except the last one must be cleared.
* // Note that the FIN bit of frames returned from
* // WebSocketFrame.createContinuationFrame methods is cleared, so
* // the example below does not clear the FIN bit explicitly.
* WebSocketFrame secondFrame = WebSocketFrame
* .{@link WebSocketFrame#createContinuationFrame(String)
* createContinuationFrame}("are ");
*
* // The last frame must be a continuation frame with the FIN bit set.
* // Note that the FIN bit of frames returned from
* // WebSocketFrame.createContinuationFrame methods is cleared, so
* // the FIN bit of the last frame must be set explicitly.
* WebSocketFrame lastFrame = WebSocketFrame
* .{@link WebSocketFrame#createContinuationFrame(String)
* createContinuationFrame}("you?")
* .{@link WebSocketFrame#setFin(boolean) setFin}(true);
*
* // Send a text message which consists of 3 frames.
* ws.{@link #sendFrame(WebSocketFrame) sendFrame}(firstFrame)
* .{@link #sendFrame(WebSocketFrame) sendFrame}(secondFrame)
* .{@link #sendFrame(WebSocketFrame) sendFrame}(lastFrame);
*
*
*
* Alternatively, the same as above can be done like this.
*
*
*
* // Send a text message which consists of 3 frames.
* ws.{@link #sendText(String, boolean) sendText}("How ", false)
* .{@link #sendContinuation(String) sendContinuation}("are ")
* .{@link #sendContinuation(String, boolean) sendContinuation}("you?", true);
*
*
* Send Ping/Pong Frames Periodically
*
*
* You can send ping frames periodically by calling {@link #setPingInterval(long)
* setPingInterval} method with an interval in milliseconds between ping frames.
* This method can be called both before and after {@link #connect()} method.
* Passing zero stops the periodical sending.
*
*
*
* // Send a ping per 60 seconds.
* ws.{@link #setPingInterval(long) setPingInterval}(60 * 1000);
*
* // Stop the periodical sending.
* ws.{@link #setPingInterval(long) setPingInterval}(0);
*
*
*
* Likewise, you can send pong frames periodically by calling {@link
* #setPongInterval(long) setPongInterval} method. "A Pong frame MAY be sent
* unsolicited." (RFC 6455, 5.5.3. Pong)
*
*
*
* You can customize payload of ping/pong frames that are sent automatically by using
* {@link #setPingPayloadGenerator(PayloadGenerator)} and
* {@link #setPongPayloadGenerator(PayloadGenerator)} methods. Both methods take an
* instance of {@link PayloadGenerator} interface. The following is an example to
* use the string representation of the current date as payload of ping frames.
*
*
*
* ws.{@link #setPingPayloadGenerator(PayloadGenerator)
* setPingPayloadGenerator}(new {@link PayloadGenerator} () {
* {@code @}Override
* public byte[] generate() {
* // The string representation of the current date.
* return new Date().toString().getBytes();
* }
* });
*
*
*
* Note that the maximum payload length of control frames (e.g. ping frames) is 125.
* Therefore, the length of a byte array returned from {@link PayloadGenerator#generate()
* generate()} method must not exceed 125.
*
*
*
* You can change the names of the {@link java.util.Timer Timer}s that send ping/pong
* frames periodically by using {@link #setPingSenderName(String)} and
* {@link #setPongSenderName(String)} methods.
*
*
*
* // Change the Timers' names.
* ws.{@link #setPingSenderName(String)
* setPingSenderName}("PING_SENDER");
* ws.{@link #setPongSenderName(String)
* setPongSenderName}("PONG_SENDER");
*
*
* Auto Flush
*
*
* By default, a frame is automatically flushed to the server immediately after
* {@link #sendFrame(WebSocketFrame) sendFrame} method is executed. This automatic
* flush can be disabled by calling {@link #setAutoFlush(boolean) setAutoFlush}{@code
* (false)}.
*
*
*
* // Disable auto-flush.
* ws.{@link #setAutoFlush(boolean) setAutoFlush}(false);
*
*
*
* To flush frames manually, call {@link #flush()} method. Note that this method
* works asynchronously.
*
*
*
* // Flush frames to the server manually.
* ws.{@link #flush()};
*
*
* Congestion Control
*
*
* sendXxx
methods queue a {@link WebSocketFrame} instance to the
* internal queue. By default, no upper limit is imposed on the queue size, so
* sendXxx
methods do not block. However, this behavior may cause
* a problem if your WebSocket client application sends too many WebSocket frames in
* a short time for the WebSocket server to process. In such a case, you may want
* sendXxx
methods to block when many frames are queued.
*
*
*
* You can set an upper limit on the internal queue by calling {@link #setFrameQueueSize(int)}
* method. As a result, if the number of frames in the queue has reached the upper limit
* when a sendXxx
method is called, the method blocks until the
* queue gets spaces. The code snippet below is an example to set 5 as the upper limit
* of the internal frame queue.
*
*
*
* // Set 5 as the frame queue size.
* ws.{@link #setFrameQueueSize(int) setFrameQueueSize}(5);
*
*
*
* Note that under some conditions, even if the queue is full, sendXxx
* methods do not block. For example, in the case where the thread to send frames
* ({@code WritingThread}) is going to stop or has already stopped. In addition,
* method calls to send a control frame (e.g. {@link #sendClose()} and {@link #sendPing()}) do not block.
*
*
* Maximum Payload Size
*
*
* You can set an upper limit on the payload size of WebSocket frames by calling
* {@link #setMaxPayloadSize(int)} method with a positive value. Text, binary and
* continuation frames whose payload size is bigger than the maximum payload size
* you have set will be split into multiple frames.
*
*
*
* // Set 1024 as the maximum payload size.
* ws.{@link #setMaxPayloadSize(int) setMaxPayloadSize}(1024);
*
*
*
* Control frames (close, ping and pong frames) are never split as per the specification.
*
*
*
* If permessage-deflate extension is enabled and if the payload size of a WebSocket
* frame after compression does not exceed the maximum payload size, the WebSocket
* frame is not split even if the payload size before compression execeeds the
* maximum payload size.
*
*
* Compression
*
*
* The permessage-deflate extension (RFC 7692) has been supported
* since the version 1.17. To enable the extension, call {@link #addExtension(String)
* addExtension} method with {@code "permessage-deflate"}.
*
*
*
* // Enable "permessage-deflate" extension (RFC 7692).
* ws.{@link #addExtension(String) addExtension}({@link WebSocketExtension#PERMESSAGE_DEFLATE});
*
*
* Missing Close Frame
*
*
* Some server implementations close a WebSocket connection without sending a
* close frame to
* a client in some cases. Strictly speaking, this is a violation against the
* specification (RFC 6455). However, this
* library has allowed the behavior by default since the version 1.29. Even if the
* end of the input stream of a WebSocket connection were reached without a close
* frame being received, it would trigger neither {@link
* WebSocketListener#onError(WebSocket, WebSocketException) onError()} method nor
* {@link WebSocketListener#onFrameError(WebSocket, WebSocketException, WebSocketFrame)
* onFrameError()} method of {@link WebSocketListener}. If you want to make a
* {@code WebSocket} instance report an error in the case, pass {@code false} to
* {@link #setMissingCloseFrameAllowed(boolean)} method.
*
*
*
* // Make this library report an error when the end of the input stream
* // of the WebSocket connection is reached before a close frame is read.
* ws.{@link #setMissingCloseFrameAllowed(boolean) setMissingCloseFrameAllowed}(false);
*
*
* Direct Text Message
*
*
* When a text message was received, {@link WebSocketListener#onTextMessage(WebSocket, String)
* onTextMessage(WebSocket, String)} is called. The implementation internally converts
* the byte array of the text message into a {@code String} object before calling the
* listener method. If you want to receive the byte array directly without the string
* conversion, call {@link #setDirectTextMessage(boolean)} with {@code true}, and
* {@link WebSocketListener#onTextMessage(WebSocket, byte[]) onTextMessage(WebSocket, byte[])}
* will be called instead.
*
*
*
* // Receive text messages without string conversion.
* ws.{@link #setDirectTextMessage(boolean) setDirectTextMessage}(true);
*
*
* Disconnect WebSocket
*
*
* Before a WebSocket is closed, a closing handshake is performed. A closing handshake
* is started (1) when the server sends a close frame to the client or (2) when the
* client sends a close frame to the server. You can start a closing handshake by calling
* {@link #disconnect()} method (or by sending a close frame manually).
*
*
*
* // Close the WebSocket connection.
* ws.{@link #disconnect()};
*
*
*
* {@code disconnect()} method has some variants. If you want to change the close code
* and the reason phrase of the close frame that this client will send to the server,
* use a variant method such as {@link #disconnect(int, String)}. {@code disconnect()}
* method itself is an alias of {@code disconnect(}{@link WebSocketCloseCode}{@code
* .NORMAL, null)}.
*
*
* Reconnection
*
*
* {@code connect()} method can be called at most only once regardless of whether the
* method succeeded or failed. If you want to re-connect to the WebSocket endpoint,
* you have to create a new {@code WebSocket} instance again by calling one of {@code
* createSocket} methods of a {@code WebSocketFactory}. You may find {@link #recreate()}
* method useful if you want to create a new {@code WebSocket} instance that has the
* same settings as the original instance. Note that, however, settings you made on
* the raw socket of the original {@code WebSocket} instance are not copied.
*
*
*
* // Create a new WebSocket instance and connect to the same endpoint.
* ws = ws.{@link #recreate()}.{@link #connect()};
*
*
*
* There is a variant of {@code recreate()} method that takes a timeout value for
* socket connection. If you want to use a timeout value that is different from the
* one used when the existing {@code WebSocket} instance was created, use {@link
* #recreate(int) recreate(int timeout)} method.
*
*
*
* Note that you should not trigger reconnection in {@link
* WebSocketListener#onError(WebSocket, WebSocketException) onError()} method
* because {@code onError()} may be called multiple times due to one error. Instead,
* {@link WebSocketListener#onDisconnected(WebSocket, WebSocketFrame, WebSocketFrame,
* boolean) onDisconnected()} is the right place to trigger reconnection.
*
*
*
* Also note that the reason I use an expression of "to trigger reconnection"
* instead of "to call recreate().connect()
" is that I myself
* won't do it synchronously in WebSocketListener
callback
* methods but will just schedule reconnection or will just go to the top of a kind
* of application loop that repeats to establish a WebSocket connection until
* it succeeds.
*
*
* Error Handling
*
*
* {@code WebSocketListener} has some {@code onXxxError()} methods such as {@link
* WebSocketListener#onFrameError(WebSocket, WebSocketException, WebSocketFrame)
* onFrameError()} and {@link
* WebSocketListener#onSendError(WebSocket, WebSocketException, WebSocketFrame)
* onSendError()}. Among such methods, {@link
* WebSocketListener#onError(WebSocket, WebSocketException) onError()} is a special
* one. It is always called before any other {@code onXxxError()} is called. For
* example, in the implementation of {@code run()} method of {@code ReadingThread},
* {@code Throwable} is caught and {@code onError()} and {@link
* WebSocketListener#onUnexpectedError(WebSocket, WebSocketException)
* onUnexpectedError()} are called in this order. The following is the implementation.
*
*
*
* {@code @}Override
* public void run()
* {
* try
* {
* main();
* }
* catch (Throwable t)
* {
* // An uncaught throwable was detected in the reading thread.
* {@link WebSocketException} cause = new WebSocketException(
* {@link WebSocketError}.{@link WebSocketError#UNEXPECTED_ERROR_IN_READING_THREAD UNEXPECTED_ERROR_IN_READING_THREAD},
* "An uncaught throwable was detected in the reading thread", t);
*
* // Notify the listeners.
* ListenerManager manager = mWebSocket.getListenerManager();
* manager.callOnError(cause);
* manager.callOnUnexpectedError(cause);
* }
* }
*
*
*
* So, you can handle all error cases in {@code onError()} method. However, note
* that {@code onError()} may be called multiple times for one error cause, so don't
* try to trigger reconnection in {@code onError()}. Instead, {@link
* WebSocketListener#onDisconnected(WebSocket, WebSocketFrame, WebSocketFrame, boolean)
* onDiconnected()} is the right place to trigger reconnection.
*
*
*
* All {@code onXxxError()} methods receive a {@link WebSocketException} instance
* as the second argument (the first argument is a {@code WebSocket} instance). The
* exception class provides {@link WebSocketException#getError() getError()} method
* which returns a {@link WebSocketError} enum entry. Entries in {@code WebSocketError}
* enum are possible causes of errors that may occur in the implementation of this
* library. The error causes are so granular that they can make it easy for you to
* find the root cause when an error occurs.
*
*
*
* {@code Throwable}s thrown by implementations of {@code onXXX()} callback methods
* are passed to {@link WebSocketListener#handleCallbackError(WebSocket, Throwable)
* handleCallbackError()} of {@code WebSocketListener}.
*
*
*
* {@code @}Override
* public void {@link WebSocketListener#handleCallbackError(WebSocket, Throwable)
* handleCallbackError}(WebSocket websocket, Throwable cause) throws Exception {
* // Throwables thrown by onXxx() callback methods come here.
* }
*
*
* Thread Callbacks
*
*
* Some threads are created internally in the implementation of {@code WebSocket}.
* Known threads are as follows.
*
*
*
*
* Internal Threads
*
*
* THREAD TYPE
* DESCRIPTION
*
*
*
*
* {@link ThreadType#READING_THREAD READING_THREAD}
* A thread which reads WebSocket frames from the server.
*
*
* {@link ThreadType#WRITING_THREAD WRITING_THREAD}
* A thread which sends WebSocket frames to the server.
*
*
* {@link ThreadType#CONNECT_THREAD CONNECT_THREAD}
* A thread which calls {@link WebSocket#connect()} asynchronously.
*
*
* {@link ThreadType#FINISH_THREAD FINISH_THREAD}
* A thread which does finalization of a {@code WebSocket} instance.
*
*
*
*
*
*
* The following callback methods of {@link WebSocketListener} are called according
* to the life cycle of the threads.
*
*
*
*
* Thread Callbacks
*
*
* METHOD
* DESCRIPTION
*
*
*
*
* {@link WebSocketListener#onThreadCreated(WebSocket, ThreadType, Thread) onThreadCreated()}
* Called after a thread was created.
*
*
* {@link WebSocketListener#onThreadStarted(WebSocket, ThreadType, Thread) onThreadStarted()}
* Called at the beginning of the thread's {@code run()} method.
*
*
* {@link WebSocketListener#onThreadStopping(WebSocket, ThreadType, Thread) onThreadStopping()}
* Called at the end of the thread's {@code run()} method.
*
*
*
*
*
*
* For example, if you want to change the name of the reading thread,
* implement {@link WebSocketListener#onThreadCreated(WebSocket, ThreadType, Thread)
* onThreadCreated()} method like below.
*
*
*
* {@code @}Override
* public void {@link WebSocketListener#onThreadCreated(WebSocket, ThreadType, Thread)
* onThreadCreated}(WebSocket websocket, {@link ThreadType} type, Thread thread)
* {
* if (type == ThreadType.READING_THREAD)
* {
* thread.setName("READING_THREAD");
* }
* }
*
*
* @see RFC 6455 (The WebSocket Protocol)
* @see RFC 7692 (Compression Extensions for WebSocket)
* @see [GitHub] nv-websocket-client
*
* @author Takahiko Kawasaki
*/
public class WebSocket
{
private static final long DEFAULT_CLOSE_DELAY = 10 * 1000L;
private final WebSocketFactory mWebSocketFactory;
private final SocketConnector mSocketConnector;
private final StateManager mStateManager;
private HandshakeBuilder mHandshakeBuilder;
private final ListenerManager mListenerManager;
private final PingSender mPingSender;
private final PongSender mPongSender;
private final Object mThreadsLock = new Object();
private WebSocketInputStream mInput;
private WebSocketOutputStream mOutput;
private ReadingThread mReadingThread;
private WritingThread mWritingThread;
private Map> mServerHeaders;
private List mAgreedExtensions;
private String mAgreedProtocol;
private boolean mExtended;
private boolean mAutoFlush = true;
private boolean mMissingCloseFrameAllowed = true;
private boolean mDirectTextMessage;
private int mFrameQueueSize;
private int mMaxPayloadSize;
private boolean mOnConnectedCalled;
private Object mOnConnectedCalledLock = new Object();
private boolean mReadingThreadStarted;
private boolean mWritingThreadStarted;
private boolean mReadingThreadFinished;
private boolean mWritingThreadFinished;
private WebSocketFrame mServerCloseFrame;
private WebSocketFrame mClientCloseFrame;
private PerMessageCompressionExtension mPerMessageCompressionExtension;
WebSocket(WebSocketFactory factory, boolean secure, String userInfo,
String host, String path, SocketConnector connector)
{
mWebSocketFactory = factory;
mSocketConnector = connector;
mStateManager = new StateManager();
mHandshakeBuilder = new HandshakeBuilder(secure, userInfo, host, path);
mListenerManager = new ListenerManager(this);
mPingSender = new PingSender(this, new CounterPayloadGenerator());
mPongSender = new PongSender(this, new CounterPayloadGenerator());
}
/**
* Create a new {@code WebSocket} instance that has the same settings
* as this instance. Note that, however, settings you made on the raw
* socket are not copied.
*
*
* The {@link WebSocketFactory} instance that you used to create this
* {@code WebSocket} instance is used again.
*
*
*
* This method calls {@link #recreate(int)} with the timeout value that
* was used when this instance was created. If you want to create a
* socket connection with a different timeout value, use {@link
* #recreate(int)} method instead.
*
*
* @return
* A new {@code WebSocket} instance.
*
* @throws IOException
* {@link WebSocketFactory#createSocket(URI)} threw an exception.
*
* @since 1.6
*/
public WebSocket recreate() throws IOException
{
return recreate(mSocketConnector.getConnectionTimeout());
}
/**
* Create a new {@code WebSocket} instance that has the same settings
* as this instance. Note that, however, settings you made on the raw
* socket are not copied.
*
*
* The {@link WebSocketFactory} instance that you used to create this
* {@code WebSocket} instance is used again.
*
*
* @return
* A new {@code WebSocket} instance.
*
* @param timeout
* The timeout value in milliseconds for socket timeout.
* A timeout of zero is interpreted as an infinite timeout.
*
* @throws IllegalArgumentException
* The given timeout value is negative.
*
* @throws IOException
* {@link WebSocketFactory#createSocket(URI)} threw an exception.
*
* @since 1.10
*/
public WebSocket recreate(int timeout) throws IOException
{
if (timeout < 0)
{
throw new IllegalArgumentException("The given timeout value is negative.");
}
WebSocket instance = mWebSocketFactory.createSocket(getURI(), timeout);
// Copy the settings.
instance.mHandshakeBuilder = new HandshakeBuilder(mHandshakeBuilder);
instance.setPingInterval(getPingInterval());
instance.setPongInterval(getPongInterval());
instance.setPingPayloadGenerator(getPingPayloadGenerator());
instance.setPongPayloadGenerator(getPongPayloadGenerator());
instance.mExtended = mExtended;
instance.mAutoFlush = mAutoFlush;
instance.mMissingCloseFrameAllowed = mMissingCloseFrameAllowed;
instance.mDirectTextMessage = mDirectTextMessage;
instance.mFrameQueueSize = mFrameQueueSize;
// Copy listeners.
List listeners = mListenerManager.getListeners();
synchronized (listeners)
{
instance.addListeners(listeners);
}
return instance;
}
@Override
protected void finalize() throws Throwable
{
if (isInState(CREATED))
{
// The raw socket needs to be closed.
finish();
}
super.finalize();
}
/**
* Get the current state of this WebSocket.
*
*
* The initial state is {@link WebSocketState#CREATED CREATED}.
* When {@link #connect()} is called, the state is changed to
* {@link WebSocketState#CONNECTING CONNECTING}, and then to
* {@link WebSocketState#OPEN OPEN} after a successful opening
* handshake. The state is changed to {@link
* WebSocketState#CLOSING CLOSING} when a closing handshake
* is started, and then to {@link WebSocketState#CLOSED CLOSED}
* when the closing handshake finished.
*
*
*
* See the description of {@link WebSocketState} for details.
*
*
* @return
* The current state.
*
* @see WebSocketState
*/
public WebSocketState getState()
{
synchronized (mStateManager)
{
return mStateManager.getState();
}
}
/**
* Check if the current state of this WebSocket is {@link
* WebSocketState#OPEN OPEN}.
*
* @return
* {@code true} if the current state is OPEN.
*
* @since 1.1
*/
public boolean isOpen()
{
return isInState(OPEN);
}
/**
* Check if the current state is equal to the specified state.
*/
private boolean isInState(WebSocketState state)
{
synchronized (mStateManager)
{
return (mStateManager.getState() == state);
}
}
/**
* Add a value for {@code Sec-WebSocket-Protocol}.
*
* @param protocol
* A protocol name.
*
* @return
* {@code this} object.
*
* @throws IllegalArgumentException
* The protocol name is invalid. A protocol name must be
* a non-empty string with characters in the range U+0021
* to U+007E not including separator characters.
*/
public WebSocket addProtocol(String protocol)
{
mHandshakeBuilder.addProtocol(protocol);
return this;
}
/**
* Remove a protocol from {@code Sec-WebSocket-Protocol}.
*
* @param protocol
* A protocol name. {@code null} is silently ignored.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket removeProtocol(String protocol)
{
mHandshakeBuilder.removeProtocol(protocol);
return this;
}
/**
* Remove all protocols from {@code Sec-WebSocket-Protocol}.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket clearProtocols()
{
mHandshakeBuilder.clearProtocols();
return this;
}
/**
* Add a value for {@code Sec-WebSocket-Extension}.
*
* @param extension
* An extension. {@code null} is silently ignored.
*
* @return
* {@code this} object.
*/
public WebSocket addExtension(WebSocketExtension extension)
{
mHandshakeBuilder.addExtension(extension);
return this;
}
/**
* Add a value for {@code Sec-WebSocket-Extension}. The input string
* should comply with the format described in 9.1. Negotiating
* Extensions in RFC 6455.
*
* @param extension
* A string that represents a WebSocket extension. If it does
* not comply with RFC 6455, no value is added to {@code
* Sec-WebSocket-Extension}.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket addExtension(String extension)
{
mHandshakeBuilder.addExtension(extension);
return this;
}
/**
* Remove an extension from {@code Sec-WebSocket-Extension}.
*
* @param extension
* An extension to remove. {@code null} is silently ignored.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket removeExtension(WebSocketExtension extension)
{
mHandshakeBuilder.removeExtension(extension);
return this;
}
/**
* Remove extensions from {@code Sec-WebSocket-Extension} by
* an extension name.
*
* @param name
* An extension name. {@code null} is silently ignored.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket removeExtensions(String name)
{
mHandshakeBuilder.removeExtensions(name);
return this;
}
/**
* Remove all extensions from {@code Sec-WebSocket-Extension}.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket clearExtensions()
{
mHandshakeBuilder.clearExtensions();
return this;
}
/**
* Add a pair of extra HTTP header.
*
* @param name
* An HTTP header name. When {@code null} or an empty
* string is given, no header is added.
*
* @param value
* The value of the HTTP header.
*
* @return
* {@code this} object.
*/
public WebSocket addHeader(String name, String value)
{
mHandshakeBuilder.addHeader(name, value);
return this;
}
/**
* Remove pairs of extra HTTP headers.
*
* @param name
* An HTTP header name. {@code null} is silently ignored.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket removeHeaders(String name)
{
mHandshakeBuilder.removeHeaders(name);
return this;
}
/**
* Clear all extra HTTP headers.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket clearHeaders()
{
mHandshakeBuilder.clearHeaders();
return this;
}
/**
* Set the credentials to connect to the WebSocket endpoint.
*
* @param userInfo
* The credentials for Basic Authentication. The format
* should be id:password
.
*
* @return
* {@code this} object.
*/
public WebSocket setUserInfo(String userInfo)
{
mHandshakeBuilder.setUserInfo(userInfo);
return this;
}
/**
* Set the credentials to connect to the WebSocket endpoint.
*
* @param id
* The ID.
*
* @param password
* The password.
*
* @return
* {@code this} object.
*/
public WebSocket setUserInfo(String id, String password)
{
mHandshakeBuilder.setUserInfo(id, password);
return this;
}
/**
* Clear the credentials to connect to the WebSocket endpoint.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket clearUserInfo()
{
mHandshakeBuilder.clearUserInfo();
return this;
}
/**
* Check if extended use of WebSocket frames are allowed.
*
*
* When extended use is allowed, values of RSV1/RSV2/RSV3 bits
* and opcode of frames are not checked. On the other hand,
* if not allowed (default), non-zero values for RSV1/RSV2/RSV3
* bits and unknown opcodes cause an error. In such a case,
* {@link WebSocketListener#onFrameError(WebSocket,
* WebSocketException, WebSocketFrame) onFrameError} method of
* listeners are called and the WebSocket is eventually closed.
*
*
* @return
* {@code true} if extended use of WebSocket frames
* are allowed.
*/
public boolean isExtended()
{
return mExtended;
}
/**
* Allow or disallow extended use of WebSocket frames.
*
* @param extended
* {@code true} to allow extended use of WebSocket frames.
*
* @return
* {@code this} object.
*/
public WebSocket setExtended(boolean extended)
{
mExtended = extended;
return this;
}
/**
* Check if flush is performed automatically after {@link
* #sendFrame(WebSocketFrame)} is done. The default value is
* {@code true}.
*
* @return
* {@code true} if flush is performed automatically.
*
* @since 1.5
*/
public boolean isAutoFlush()
{
return mAutoFlush;
}
/**
* Enable or disable auto-flush of sent frames.
*
* @param auto
* {@code true} to enable auto-flush. {@code false} to
* disable it.
*
* @return
* {@code this} object.
*
* @since 1.5
*/
public WebSocket setAutoFlush(boolean auto)
{
mAutoFlush = auto;
return this;
}
/**
* Check if this instance allows the server to close the WebSocket
* connection without sending a close frame
* to this client. The default value is {@code true}.
*
* @return
* {@code true} if the configuration allows for the server to
* close the WebSocket connection without sending a close frame
* to this client. {@code false} if the configuration requires
* that an error be reported via
* {@link WebSocketListener#onError(WebSocket, WebSocketException)
* onError()} method and {@link WebSocketListener#onFrameError(WebSocket,
* WebSocketException, WebSocketFrame) onFrameError()} method of
* {@link WebSocketListener}.
*
* @since 1.29
*/
public boolean isMissingCloseFrameAllowed()
{
return mMissingCloseFrameAllowed;
}
/**
* Set whether to allow the server to close the WebSocket connection
* without sending a close frame
* to this client.
*
* @param allowed
* {@code true} to allow the server to close the WebSocket
* connection without sending a close frame to this client.
* {@code false} to make this instance report an error when the
* end of the input stream of the WebSocket connection is reached
* before a close frame is read.
*
* @return
* {@code this} object.
*
* @since 1.29
*/
public WebSocket setMissingCloseFrameAllowed(boolean allowed)
{
mMissingCloseFrameAllowed = allowed;
return this;
}
/**
* Check if text messages are passed to listeners without string conversion.
*
*
* If this method returns {@code true}, when a text message is received,
* {@link WebSocketListener#onTextMessage(WebSocket, byte[])
* onTextMessage(WebSocket, byte[])} will be called instead of
* {@link WebSocketListener#onTextMessage(WebSocket, String)
* onTextMessage(WebSocket, String)}. The purpose of this behavior
* is to skip internal string conversion which is performed in the
* implementation of {@code ReadingThread}.
*
*
* @return
* {@code true} if text messages are passed to listeners without
* string conversion.
*
* @since 2.6
*/
public boolean isDirectTextMessage()
{
return mDirectTextMessage;
}
/**
* Set whether to receive text messages directly as byte arrays without
* string conversion.
*
*
* If {@code true} is set to this property, when a text message is received,
* {@link WebSocketListener#onTextMessage(WebSocket, byte[])
* onTextMessage(WebSocket, byte[])} will be called instead of
* {@link WebSocketListener#onTextMessage(WebSocket, String)
* onTextMessage(WebSocket, String)}. The purpose of this behavior
* is to skip internal string conversion which is performed in the
* implementation of {@code ReadingThread}.
*
*
* @param direct
* {@code true} to receive text messages as byte arrays.
*
* @return
* {@code this} object.
*
* @since 2.6
*/
public WebSocket setDirectTextMessage(boolean direct)
{
mDirectTextMessage = direct;
return this;
}
/**
* Flush frames to the server. Flush is performed asynchronously.
*
* @return
* {@code this} object.
*
* @since 1.5
*/
public WebSocket flush()
{
synchronized (mStateManager)
{
WebSocketState state = mStateManager.getState();
if (state != OPEN && state != CLOSING)
{
return this;
}
}
// Get the reference to the instance of WritingThread.
WritingThread wt = mWritingThread;
// If and only if an instance of WritingThread is available.
if (wt != null)
{
// Request flush.
wt.queueFlush();
}
return this;
}
/**
* Get the size of the frame queue. The default value is 0 and it means
* there is no limit on the queue size.
*
* @return
* The size of the frame queue.
*
* @since 1.15
*/
public int getFrameQueueSize()
{
return mFrameQueueSize;
}
/**
* Set the size of the frame queue. The default value is 0 and it means
* there is no limit on the queue size.
*
*
* sendXxx
methods queue a {@link WebSocketFrame}
* instance to the internal queue. If the number of frames in the queue
* has reached the upper limit (which has been set by this method) when
* a sendXxx
method is called, the method blocks
* until the queue gets spaces.
*
*
*
* Under some conditions, even if the queue is full, sendXxx
* methods do not block. For example, in the case where the thread to send
* frames ({@code WritingThread}) is going to stop or has already stopped.
* In addition, method calls to send a control frame (e.g.
* {@link #sendClose()} and {@link #sendPing()}) do not block.
*
*
* @param size
* The queue size. 0 means no limit. Negative numbers are not allowed.
*
* @return
* {@code this} object.
*
* @throws IllegalArgumentException
* {@code size} is negative.
*
* @since 1.15
*/
public WebSocket setFrameQueueSize(int size) throws IllegalArgumentException
{
if (size < 0)
{
throw new IllegalArgumentException("size must not be negative.");
}
mFrameQueueSize = size;
return this;
}
/**
* Get the maximum payload size. The default value is 0 which means that
* the maximum payload size is not set and as a result frames are not split.
*
* @return
* The maximum payload size. 0 means that the maximum payload size
* is not set.
*
* @since 1.27
*/
public int getMaxPayloadSize()
{
return mMaxPayloadSize;
}
/**
* Set the maximum payload size.
*
*
* Text, binary and continuation frames whose payload size is bigger than
* the maximum payload size will be split into multiple frames. Note that
* control frames (close, ping and pong frames) are not split as per the
* specification even if their payload size exceeds the maximum payload size.
*
*
* @param size
* The maximum payload size. 0 to unset the maximum payload size.
*
* @return
* {@code this} object.
*
* @throws IllegalArgumentException
* {@code size} is negative.
*
* @since 1.27
*/
public WebSocket setMaxPayloadSize(int size) throws IllegalArgumentException
{
if (size < 0)
{
throw new IllegalArgumentException("size must not be negative.");
}
mMaxPayloadSize = size;
return this;
}
/**
* Get the interval of periodical
* ping
* frames.
*
* @return
* The interval in milliseconds.
*
* @since 1.2
*/
public long getPingInterval()
{
return mPingSender.getInterval();
}
/**
* Set the interval of periodical
* ping
* frames.
*
*
* Setting a positive number starts sending ping frames periodically.
* Setting zero stops the periodical sending. This method can be called
* both before and after {@link #connect()} method.
*
*
* @param interval
* The interval in milliseconds. A negative value is
* regarded as zero.
*
* @return
* {@code this} object.
*
* @since 1.2
*/
public WebSocket setPingInterval(long interval)
{
mPingSender.setInterval(interval);
return this;
}
/**
* Get the interval of periodical
* pong
* frames.
*
* @return
* The interval in milliseconds.
*
* @since 1.2
*/
public long getPongInterval()
{
return mPongSender.getInterval();
}
/**
* Set the interval of periodical
* pong
* frames.
*
*
* Setting a positive number starts sending pong frames periodically.
* Setting zero stops the periodical sending. This method can be called
* both before and after {@link #connect()} method.
*
*
*
*
* -
* An excerpt from RFC 6455, 5.5.3. Pong
*
* -
*
* A Pong frame MAY be sent unsolicited. This serves as a
* unidirectional heartbeat. A response to an unsolicited Pong
* frame is not expected.
*
*
*
*
*
* @param interval
* The interval in milliseconds. A negative value is
* regarded as zero.
*
* @return
* {@code this} object.
*
* @since 1.2
*/
public WebSocket setPongInterval(long interval)
{
mPongSender.setInterval(interval);
return this;
}
/**
* Get the generator of payload of ping frames that are sent automatically.
*
* @return
* The generator of payload ping frames that are sent automatically.
*
* @since 1.20
*/
public PayloadGenerator getPingPayloadGenerator()
{
return mPingSender.getPayloadGenerator();
}
/**
* Set the generator of payload of ping frames that are sent automatically.
*
* @param generator
* The generator of payload ping frames that are sent automatically.
*
* @since 1.20
*/
public WebSocket setPingPayloadGenerator(PayloadGenerator generator)
{
mPingSender.setPayloadGenerator(generator);
return this;
}
/**
* Get the generator of payload of pong frames that are sent automatically.
*
* @return
* The generator of payload pong frames that are sent automatically.
*
* @since 1.20
*/
public PayloadGenerator getPongPayloadGenerator()
{
return mPongSender.getPayloadGenerator();
}
/**
* Set the generator of payload of pong frames that are sent automatically.
*
* @param generator
* The generator of payload ppng frames that are sent automatically.
*
* @since 1.20
*/
public WebSocket setPongPayloadGenerator(PayloadGenerator generator)
{
mPongSender.setPayloadGenerator(generator);
return this;
}
/**
* Get the name of the {@code Timer} that sends ping frames periodically.
*
* @return
* The {@code Timer}'s name.
*
* @since 2.5
*/
public String getPingSenderName()
{
return mPingSender.getTimerName();
}
/**
* Set the name of the {@code Timer} that sends ping frames periodically.
*
* @param name
* A name for the {@code Timer}.
*
* @return
* {@code this} object.
*
* @since 2.5
*/
public WebSocket setPingSenderName(String name)
{
mPingSender.setTimerName(name);
return this;
}
/**
* Get the name of the {@code Timer} that sends pong frames periodically.
*
* @return
* The {@code Timer}'s name.
*
* @since 2.5
*/
public String getPongSenderName()
{
return mPongSender.getTimerName();
}
/**
* Set the name of the {@code Timer} that sends pong frames periodically.
*
* @param name
* A name for the {@code Timer}.
*
* @return
* {@code this} object.
*
* @since 2.5
*/
public WebSocket setPongSenderName(String name)
{
mPongSender.setTimerName(name);
return this;
}
/**
* Add a listener to receive events on this WebSocket.
*
* @param listener
* A listener to add.
*
* @return
* {@code this} object.
*/
public WebSocket addListener(WebSocketListener listener)
{
mListenerManager.addListener(listener);
return this;
}
/**
* Add listeners.
*
* @param listeners
* Listeners to add. {@code null} is silently ignored.
* {@code null} elements in the list are ignored, too.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket addListeners(List listeners)
{
mListenerManager.addListeners(listeners);
return this;
}
/**
* Remove a listener from this WebSocket.
*
* @param listener
* A listener to remove. {@code null} won't cause an error.
*
* @return
* {@code this} object.
*
* @since 1.13
*/
public WebSocket removeListener(WebSocketListener listener)
{
mListenerManager.removeListener(listener);
return this;
}
/**
* Remove listeners.
*
* @param listeners
* Listeners to remove. {@code null} is silently ignored.
* {@code null} elements in the list are ignored, too.
*
* @return
* {@code this} object.
*
* @since 1.14
*/
public WebSocket removeListeners(List listeners)
{
mListenerManager.removeListeners(listeners);
return this;
}
/**
* Remove all the listeners from this WebSocket.
*
* @return
* {@code this} object.
*
* @since 1.13
*/
public WebSocket clearListeners()
{
mListenerManager.clearListeners();
return this;
}
/**
* Get the raw socket which this WebSocket uses internally.
*
* @return
* The underlying {@link Socket} instance.
*/
public Socket getSocket()
{
return mSocketConnector.getSocket();
}
/**
* Get the URI of the WebSocket endpoint. The scheme part is either
* {@code "ws"} or {@code "wss"}. The authority part is always empty.
*
* @return
* The URI of the WebSocket endpoint.
*
* @since 1.1
*/
public URI getURI()
{
return mHandshakeBuilder.getURI();
}
/**
* Connect to the server, send an opening handshake to the server,
* receive the response and then start threads to communicate with
* the server.
*
*
* As necessary, {@link #addProtocol(String)}, {@link #addExtension(WebSocketExtension)}
* {@link #addHeader(String, String)} should be called before you call this
* method. It is because the parameters set by these methods are used in the
* opening handshake.
*
*
*
* Also, as necessary, {@link #getSocket()} should be used to set up socket
* parameters before you call this method. For example, you can set the
* socket timeout like the following.
*
*
*
* WebSocket websocket = ......;
* websocket.{@link #getSocket() getSocket()}.{@link Socket#setSoTimeout(int)
* setSoTimeout}(5000);
*
*
*
* If the WebSocket endpoint requires Basic Authentication, you can set
* credentials by {@link #setUserInfo(String) setUserInfo(userInfo)} or
* {@link #setUserInfo(String, String) setUserInfo(id, password)} before
* you call this method.
* Note that if the URI passed to {@link WebSocketFactory}{@code
* .createSocket} method contains the user-info part, you don't have to
* call {@code setUserInfo} method.
*
*
*
* Note that this method can be called at most only once regardless of
* whether this method succeeded or failed. If you want to re-connect to
* the WebSocket endpoint, you have to create a new {@code WebSocket}
* instance again by calling one of {@code createSocket} methods of a
* {@link WebSocketFactory}. You may find {@link #recreate()} method
* useful if you want to create a new {@code WebSocket} instance that
* has the same settings as this instance. (But settings you made on
* the raw socket are not copied.)
*
*
* @return
* {@code this} object.
*
* @throws WebSocketException
*
* - The current state of the WebSocket is not {@link
* WebSocketState#CREATED CREATED}
*
- Connecting the server failed.
*
- The opening handshake failed.
*
*/
public WebSocket connect() throws WebSocketException
{
// Change the state to CONNECTING. If the state before
// the change is not CREATED, an exception is thrown.
changeStateOnConnect();
// HTTP headers from the server.
Map> headers;
try
{
// Connect to the server.
mSocketConnector.connect();
// Perform WebSocket handshake.
headers = shakeHands();
}
catch (WebSocketException e)
{
// Close the socket.
mSocketConnector.closeSilently();
// Change the state to CLOSED.
mStateManager.setState(CLOSED);
// Notify the listener of the state change.
mListenerManager.callOnStateChanged(CLOSED);
// The handshake failed.
throw e;
}
// HTTP headers in the response from the server.
mServerHeaders = headers;
// Extensions.
mPerMessageCompressionExtension = findAgreedPerMessageCompressionExtension();
// Change the state to OPEN.
mStateManager.setState(OPEN);
// Notify the listener of the state change.
mListenerManager.callOnStateChanged(OPEN);
// Start threads that communicate with the server.
startThreads();
return this;
}
/**
* Execute {@link #connect()} asynchronously using the given {@link
* ExecutorService}. This method is just an alias of the following.
*
*
* executorService.{@link ExecutorService#submit(Callable) submit}({@link #connectable()})
*
*
* @param executorService
* An {@link ExecutorService} to execute a task created by
* {@link #connectable()}.
*
* @return
* The value returned from {@link ExecutorService#submit(Callable)}.
*
* @throws NullPointerException
* If the given {@link ExecutorService} is {@code null}.
*
* @throws RejectedExecutionException
* If the given {@link ExecutorService} rejected the task
* created by {@link #connectable()}.
*
* @see #connectAsynchronously()
*
* @since 1.7
*/
public Future connect(ExecutorService executorService)
{
return executorService.submit(connectable());
}
/**
* Get a new {@link Callable}{@code <}{@link WebSocket}{@code >} instance
* whose {@link Callable#call() call()} method calls {@link #connect()}
* method of this {@code WebSocket} instance.
*
* @return
* A new {@link Callable}{@code <}{@link WebSocket}{@code >} instance
* for asynchronous {@link #connect()}.
*
* @see #connect(ExecutorService)
*
* @since 1.7
*/
public Callable connectable()
{
return new Connectable(this);
}
/**
* Execute {@link #connect()} asynchronously by creating a new thread and
* calling {@code connect()} in the thread. If {@code connect()} failed,
* {@link WebSocketListener#onConnectError(WebSocket, WebSocketException)
* onConnectError()} method of {@link WebSocketListener} is called.
*
* @return
* {@code this} object.
*
* @since 1.8
*/
public WebSocket connectAsynchronously()
{
Thread thread = new ConnectThread(this);
// Get the reference (just in case)
ListenerManager lm = mListenerManager;
if (lm != null)
{
lm.callOnThreadCreated(ThreadType.CONNECT_THREAD, thread);
}
thread.start();
return this;
}
/**
* Disconnect the WebSocket.
*
*
* This method is an alias of {@link #disconnect(int, String)
* disconnect}{@code (}{@link WebSocketCloseCode#NORMAL}{@code , null)}.
*
*
* @return
* {@code this} object.
*/
public WebSocket disconnect()
{
return disconnect(WebSocketCloseCode.NORMAL, null);
}
/**
* Disconnect the WebSocket.
*
*
* This method is an alias of {@link #disconnect(int, String)
* disconnect}{@code (closeCode, null)}.
*
*
* @param closeCode
* The close code embedded in a close frame
* which this WebSocket client will send to the server.
*
* @return
* {@code this} object.
*
* @since 1.5
*/
public WebSocket disconnect(int closeCode)
{
return disconnect(closeCode, null);
}
/**
* Disconnect the WebSocket.
*
*
* This method is an alias of {@link #disconnect(int, String)
* disconnect}{@code (}{@link WebSocketCloseCode#NORMAL}{@code , reason)}.
*
*
* @param reason
* The reason embedded in a close frame
* which this WebSocket client will send to the server. Note that
* the length of the bytes which represents the given reason must
* not exceed 125. In other words, {@code (reason.}{@link
* String#getBytes(String) getBytes}{@code ("UTF-8").length <= 125)}
* must be true.
*
* @return
* {@code this} object.
*
* @since 1.5
*/
public WebSocket disconnect(String reason)
{
return disconnect(WebSocketCloseCode.NORMAL, reason);
}
/**
* Disconnect the WebSocket.
*
*
* This method is an alias of {@link #disconnect(int, String, long)
* disconnect}{@code (closeCode, reason, 10000L)}.
*
*
* @param closeCode
* The close code embedded in a close frame
* which this WebSocket client will send to the server.
*
* @param reason
* The reason embedded in a close frame
* which this WebSocket client will send to the server. Note that
* the length of the bytes which represents the given reason must
* not exceed 125. In other words, {@code (reason.}{@link
* String#getBytes(String) getBytes}{@code ("UTF-8").length <= 125)}
* must be true.
*
* @return
* {@code this} object.
*
* @see WebSocketCloseCode
*
* @see RFC 6455, 5.5.1. Close
*
* @since 1.5
*/
public WebSocket disconnect(int closeCode, String reason)
{
return disconnect(closeCode, reason, DEFAULT_CLOSE_DELAY);
}
/**
* Disconnect the WebSocket.
*
* @param closeCode
* The close code embedded in a close frame
* which this WebSocket client will send to the server.
*
* @param reason
* The reason embedded in a close frame
* which this WebSocket client will send to the server. Note that
* the length of the bytes which represents the given reason must
* not exceed 125. In other words, {@code (reason.}{@link
* String#getBytes(String) getBytes}{@code ("UTF-8").length <= 125)}
* must be true.
*
* @param closeDelay
* Delay in milliseconds before calling {@link Socket#close()} forcibly.
* This safeguard is needed for the case where the server fails to send
* back a close frame. The default value is 10000 (= 10 seconds). When
* a negative value is given, the default value is used.
*
* If a very short time (e.g. 0) is given, it is likely to happen either
* (1) that this client will fail to send a close frame to the server
* (in this case, you will probably see an error message "Flushing frames
* to the server failed: Socket closed") or (2) that the WebSocket
* connection will be closed before this client receives a close frame
* from the server (in this case, the second argument of {@link
* WebSocketListener#onDisconnected(WebSocket, WebSocketFrame,
* WebSocketFrame, boolean) WebSocketListener.onDisconnected} will be
* {@code null}).
*
* @return
* {@code this} object.
*
* @see WebSocketCloseCode
*
* @see RFC 6455, 5.5.1. Close
*
* @since 1.26
*/
public WebSocket disconnect(int closeCode, String reason, long closeDelay)
{
synchronized (mStateManager)
{
switch (mStateManager.getState())
{
case CREATED:
finishAsynchronously();
return this;
case OPEN:
break;
default:
// - CONNECTING
// It won't happen unless the programmer dare call
// open() and disconnect() in parallel.
//
// - CLOSING
// A closing handshake has already been started.
//
// - CLOSED
// The connection has already been closed.
return this;
}
// Change the state to CLOSING.
mStateManager.changeToClosing(CloseInitiator.CLIENT);
// Create a close frame.
WebSocketFrame frame = WebSocketFrame.createCloseFrame(closeCode, reason);
// Send the close frame to the server.
sendFrame(frame);
}
// Notify the listeners of the state change.
mListenerManager.callOnStateChanged(CLOSING);
// If a negative value is given.
if (closeDelay < 0)
{
// Use the default value.
closeDelay = DEFAULT_CLOSE_DELAY;
}
// Request the threads to stop.
stopThreads(closeDelay);
return this;
}
/**
* Get the agreed extensions.
*
*
* This method works correctly only after {@link #connect()} succeeds
* (= after the opening handshake succeeds).
*
*
* @return
* The agreed extensions.
*/
public List getAgreedExtensions()
{
return mAgreedExtensions;
}
/**
* Get the agreed protocol.
*
*
* This method works correctly only after {@link #connect()} succeeds
* (= after the opening handshake succeeds).
*
*
* @return
* The agreed protocol.
*/
public String getAgreedProtocol()
{
return mAgreedProtocol;
}
/**
* Send a WebSocket frame to the server.
*
*
* This method just queues the given frame. Actual transmission
* is performed asynchronously.
*
*
*
* When the current state of this WebSocket is not {@link
* WebSocketState#OPEN OPEN}, this method does not accept
* the frame.
*
*
*
* Sending a close frame changes the state to {@link WebSocketState#CLOSING
* CLOSING} (if the current state is neither {@link WebSocketState#CLOSING
* CLOSING} nor {@link WebSocketState#CLOSED CLOSED}).
*
*
*
* Note that the validity of the give frame is not checked.
* For example, even if the payload length of a given frame
* is greater than 125 and the opcode indicates that the
* frame is a control frame, this method accepts the given
* frame.
*
*
* @param frame
* A WebSocket frame to be sent to the server.
* If {@code null} is given, nothing is done.
*
* @return
* {@code this} object.
*/
public WebSocket sendFrame(WebSocketFrame frame)
{
if (frame == null)
{
return this;
}
synchronized (mStateManager)
{
WebSocketState state = mStateManager.getState();
if (state != OPEN && state != CLOSING)
{
return this;
}
}
// The current state is either OPEN or CLOSING. Or, CLOSED.
// Get the reference to the writing thread.
WritingThread wt = mWritingThread;
// Some applications call sendFrame() without waiting for the
// notification of WebSocketListener.onConnected() (Issue #23),
// and/or even after the connection is closed. That is, there
// are chances that sendFrame() is called when mWritingThread
// is null. So, it should be checked whether an instance of
// WritingThread is available or not before calling queueFrame().
if (wt == null)
{
// An instance of WritingThread is not available.
return this;
}
// Split the frame into multiple frames if necessary.
List frames = splitIfNecessary(frame);
// Queue the frame or the frames. Even if the current state is
// CLOSED, queueing won't be a big issue.
// If the frame was not split.
if (frames == null)
{
// Queue the frame.
wt.queueFrame(frame);
}
else
{
for (WebSocketFrame f : frames)
{
// Queue the frame.
wt.queueFrame(f);
}
}
return this;
}
private List splitIfNecessary(WebSocketFrame frame)
{
return WebSocketFrame.splitIfNecessary(frame, mMaxPayloadSize, mPerMessageCompressionExtension);
}
/**
* Send a continuation frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createContinuationFrame()
* createContinuationFrame()}{@code )}.
*
*
*
* Note that the FIN bit of a frame sent by this method is {@code false}.
* If you want to set the FIN bit, use {@link #sendContinuation(boolean)
* sendContinuation(boolean fin)} with {@code fin=true}.
*
*
* @return
* {@code this} object.
*/
public WebSocket sendContinuation()
{
return sendFrame(WebSocketFrame.createContinuationFrame());
}
/**
* Send a continuation frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createContinuationFrame()
* createContinuationFrame()}{@code .}{@link
* WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
*
*
* @param fin
* The FIN bit value.
*
* @return
* {@code this} object.
*/
public WebSocket sendContinuation(boolean fin)
{
return sendFrame(WebSocketFrame.createContinuationFrame().setFin(fin));
}
/**
* Send a continuation frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createContinuationFrame(String)
* createContinuationFrame}{@code (payload))}.
*
*
*
* Note that the FIN bit of a frame sent by this method is {@code false}.
* If you want to set the FIN bit, use {@link #sendContinuation(String,
* boolean) sendContinuation(String payload, boolean fin)} with {@code
* fin=true}.
*
*
* @param payload
* The payload of a continuation frame.
*
* @return
* {@code this} object.
*/
public WebSocket sendContinuation(String payload)
{
return sendFrame(WebSocketFrame.createContinuationFrame(payload));
}
/**
* Send a continuation frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createContinuationFrame(String)
* createContinuationFrame}{@code (payload).}{@link
* WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
*
*
* @param payload
* The payload of a continuation frame.
*
* @param fin
* The FIN bit value.
*
* @return
* {@code this} object.
*/
public WebSocket sendContinuation(String payload, boolean fin)
{
return sendFrame(WebSocketFrame.createContinuationFrame(payload).setFin(fin));
}
/**
* Send a continuation frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createContinuationFrame(byte[])
* createContinuationFrame}{@code (payload))}.
*
*
*
* Note that the FIN bit of a frame sent by this method is {@code false}.
* If you want to set the FIN bit, use {@link #sendContinuation(byte[],
* boolean) sendContinuation(byte[] payload, boolean fin)} with {@code
* fin=true}.
*
*
* @param payload
* The payload of a continuation frame.
*
* @return
* {@code this} object.
*/
public WebSocket sendContinuation(byte[] payload)
{
return sendFrame(WebSocketFrame.createContinuationFrame(payload));
}
/**
* Send a continuation frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createContinuationFrame(byte[])
* createContinuationFrame}{@code (payload).}{@link
* WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
*
*
* @param payload
* The payload of a continuation frame.
*
* @param fin
* The FIN bit value.
*
* @return
* {@code this} object.
*/
public WebSocket sendContinuation(byte[] payload, boolean fin)
{
return sendFrame(WebSocketFrame.createContinuationFrame(payload).setFin(fin));
}
/**
* Send a text message to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createTextFrame(String)
* createTextFrame}{@code (message))}.
*
*
*
* If you want to send a text frame that is to be followed by
* continuation frames, use {@link #sendText(String, boolean)
* setText(String payload, boolean fin)} with {@code fin=false}.
*
*
* @param message
* A text message to be sent to the server.
*
* @return
* {@code this} object.
*/
public WebSocket sendText(String message)
{
return sendFrame(WebSocketFrame.createTextFrame(message));
}
/**
* Send a text frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createTextFrame(String)
* createTextFrame}{@code (payload).}{@link
* WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
*
*
* @param payload
* The payload of a text frame.
*
* @param fin
* The FIN bit value.
*
* @return
* {@code this} object.
*/
public WebSocket sendText(String payload, boolean fin)
{
return sendFrame(WebSocketFrame.createTextFrame(payload).setFin(fin));
}
/**
* Send a binary message to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createBinaryFrame(byte[])
* createBinaryFrame}{@code (message))}.
*
*
*
* If you want to send a binary frame that is to be followed by
* continuation frames, use {@link #sendBinary(byte[], boolean)
* setBinary(byte[] payload, boolean fin)} with {@code fin=false}.
*
*
* @param message
* A binary message to be sent to the server.
*
* @return
* {@code this} object.
*/
public WebSocket sendBinary(byte[] message)
{
return sendFrame(WebSocketFrame.createBinaryFrame(message));
}
/**
* Send a binary frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createBinaryFrame(byte[])
* createBinaryFrame}{@code (payload).}{@link
* WebSocketFrame#setFin(boolean) setFin}{@code (fin))}.
*
*
* @param payload
* The payload of a binary frame.
*
* @param fin
* The FIN bit value.
*
* @return
* {@code this} object.
*/
public WebSocket sendBinary(byte[] payload, boolean fin)
{
return sendFrame(WebSocketFrame.createBinaryFrame(payload).setFin(fin));
}
/**
* Send a close frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createCloseFrame() createCloseFrame()}).
*
*
* @return
* {@code this} object.
*/
public WebSocket sendClose()
{
return sendFrame(WebSocketFrame.createCloseFrame());
}
/**
* Send a close frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createCloseFrame(int)
* createCloseFrame}{@code (closeCode))}.
*
*
* @param closeCode
* The close code.
*
* @return
* {@code this} object.
*
* @see WebSocketCloseCode
*/
public WebSocket sendClose(int closeCode)
{
return sendFrame(WebSocketFrame.createCloseFrame(closeCode));
}
/**
* Send a close frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createCloseFrame(int, String)
* createCloseFrame}{@code (closeCode, reason))}.
*
*
* @param closeCode
* The close code.
*
* @param reason
* The close reason.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* {@code this} object.
*
* @see WebSocketCloseCode
*/
public WebSocket sendClose(int closeCode, String reason)
{
return sendFrame(WebSocketFrame.createCloseFrame(closeCode, reason));
}
/**
* Send a ping frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createPingFrame() createPingFrame()}).
*
*
* @return
* {@code this} object.
*/
public WebSocket sendPing()
{
return sendFrame(WebSocketFrame.createPingFrame());
}
/**
* Send a ping frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createPingFrame(byte[])
* createPingFrame}{@code (payload))}.
*
*
* @param payload
* The payload for a ping frame.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* {@code this} object.
*/
public WebSocket sendPing(byte[] payload)
{
return sendFrame(WebSocketFrame.createPingFrame(payload));
}
/**
* Send a ping frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createPingFrame(String)
* createPingFrame}{@code (payload))}.
*
*
* @param payload
* The payload for a ping frame.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* {@code this} object.
*/
public WebSocket sendPing(String payload)
{
return sendFrame(WebSocketFrame.createPingFrame(payload));
}
/**
* Send a pong frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createPongFrame() createPongFrame()}).
*
*
* @return
* {@code this} object.
*/
public WebSocket sendPong()
{
return sendFrame(WebSocketFrame.createPongFrame());
}
/**
* Send a pong frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createPongFrame(byte[])
* createPongFrame}{@code (payload))}.
*
*
* @param payload
* The payload for a pong frame.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* {@code this} object.
*/
public WebSocket sendPong(byte[] payload)
{
return sendFrame(WebSocketFrame.createPongFrame(payload));
}
/**
* Send a pong frame to the server.
*
*
* This method is an alias of {@link #sendFrame(WebSocketFrame)
* sendFrame}{@code (WebSocketFrame.}{@link
* WebSocketFrame#createPongFrame(String)
* createPongFrame}{@code (payload))}.
*
*
* @param payload
* The payload for a pong frame.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* {@code this} object.
*/
public WebSocket sendPong(String payload)
{
return sendFrame(WebSocketFrame.createPongFrame(payload));
}
private void changeStateOnConnect() throws WebSocketException
{
synchronized (mStateManager)
{
// If the current state is not CREATED.
if (mStateManager.getState() != CREATED)
{
throw new WebSocketException(
WebSocketError.NOT_IN_CREATED_STATE,
"The current state of the WebSocket is not CREATED.");
}
// Change the state to CONNECTING.
mStateManager.setState(CONNECTING);
}
// Notify the listeners of the state change.
mListenerManager.callOnStateChanged(CONNECTING);
}
/**
* Perform the opening handshake.
*/
private Map> shakeHands() throws WebSocketException
{
// The raw socket created by WebSocketFactory.
Socket socket = mSocketConnector.getSocket();
// Get the input stream of the socket.
WebSocketInputStream input = openInputStream(socket);
// Get the output stream of the socket.
WebSocketOutputStream output = openOutputStream(socket);
// Generate a value for Sec-WebSocket-Key.
String key = generateWebSocketKey();
// Send an opening handshake to the server.
writeHandshake(output, key);
// Read the response from the server.
Map> headers = readHandshake(input, key);
// Keep the input stream and the output stream to pass them
// to the reading thread and the writing thread later.
mInput = input;
mOutput = output;
// The handshake succeeded.
return headers;
}
/**
* Open the input stream of the WebSocket connection.
* The stream is used by the reading thread.
*/
private WebSocketInputStream openInputStream(Socket socket) throws WebSocketException
{
try
{
// Get the input stream of the raw socket through which
// this client receives data from the server.
return new WebSocketInputStream(
new BufferedInputStream(socket.getInputStream()));
}
catch (IOException e)
{
// Failed to get the input stream of the raw socket.
throw new WebSocketException(
WebSocketError.SOCKET_INPUT_STREAM_FAILURE,
"Failed to get the input stream of the raw socket: " + e.getMessage(), e);
}
}
/**
* Open the output stream of the WebSocket connection.
* The stream is used by the writing thread.
*/
private WebSocketOutputStream openOutputStream(Socket socket) throws WebSocketException
{
try
{
// Get the output stream of the socket through which
// this client sends data to the server.
return new WebSocketOutputStream(
new BufferedOutputStream(socket.getOutputStream()));
}
catch (IOException e)
{
// Failed to get the output stream from the raw socket.
throw new WebSocketException(
WebSocketError.SOCKET_OUTPUT_STREAM_FAILURE,
"Failed to get the output stream from the raw socket: " + e.getMessage(), e);
}
}
/**
* Generate a value for Sec-WebSocket-Key.
*
*
*
* The request MUST include a header field with the name Sec-WebSocket-Key.
* The value of this header field MUST be a nonce consisting of a randomly
* selected 16-byte value that has been base64-encoded (see Section 4 of
* RFC 4648). The nonce MUST be selected randomly for each connection.
*
*
*
* @return
* A randomly generated WebSocket key.
*/
private static String generateWebSocketKey()
{
// "16-byte value"
byte[] data = new byte[16];
// "randomly selected"
Misc.nextBytes(data);
// "base64-encoded"
return Base64.encode(data);
}
/**
* Send an opening handshake request to the WebSocket server.
*/
private void writeHandshake(WebSocketOutputStream output, String key) throws WebSocketException
{
// Generate an opening handshake sent to the server from this client.
mHandshakeBuilder.setKey(key);
String requestLine = mHandshakeBuilder.buildRequestLine();
List headers = mHandshakeBuilder.buildHeaders();
String handshake = HandshakeBuilder.build(requestLine, headers);
// Call onSendingHandshake() method of listeners.
mListenerManager.callOnSendingHandshake(requestLine, headers);
try
{
// Send the opening handshake to the server.
output.write(handshake);
output.flush();
}
catch (IOException e)
{
// Failed to send an opening handshake request to the server.
throw new WebSocketException(
WebSocketError.OPENING_HAHDSHAKE_REQUEST_FAILURE,
"Failed to send an opening handshake request to the server: " + e.getMessage(), e);
}
}
/**
* Receive an opening handshake response from the WebSocket server.
*/
private Map> readHandshake(WebSocketInputStream input, String key) throws WebSocketException
{
return new HandshakeReader(this).readHandshake(input, key);
}
/**
* Start both the reading thread and the writing thread.
*
*
* The reading thread will call {@link #onReadingThreadStarted()}
* as its first step. Likewise, the writing thread will call
* {@link #onWritingThreadStarted()} as its first step. After
* both the threads have started, {@link #onThreadsStarted()} is
* called.
*
*/
private void startThreads()
{
ReadingThread readingThread = new ReadingThread(this);
WritingThread writingThread = new WritingThread(this);
synchronized (mThreadsLock)
{
mReadingThread = readingThread;
mWritingThread = writingThread;
}
// Execute onThreadCreated of the listeners.
readingThread.callOnThreadCreated();
writingThread.callOnThreadCreated();
readingThread.start();
writingThread.start();
}
/**
* Stop both the reading thread and the writing thread.
*
*
* The reading thread will call {@link #onReadingThreadFinished(WebSocketFrame)}
* as its last step. Likewise, the writing thread will call {@link
* #onWritingThreadFinished(WebSocketFrame)} as its last step.
* After both the threads have stopped, {@link #onThreadsFinished()}
* is called.
*
*/
private void stopThreads(long closeDelay)
{
ReadingThread readingThread;
WritingThread writingThread;
synchronized (mThreadsLock)
{
readingThread = mReadingThread;
writingThread = mWritingThread;
mReadingThread = null;
mWritingThread = null;
}
if (readingThread != null)
{
readingThread.requestStop(closeDelay);
}
if (writingThread != null)
{
writingThread.requestStop();
}
}
/**
* Get the input stream of the WebSocket connection.
*/
WebSocketInputStream getInput()
{
return mInput;
}
/**
* Get the output stream of the WebSocket connection.
*/
WebSocketOutputStream getOutput()
{
return mOutput;
}
/**
* Get the manager that manages the state of this {@code WebSocket} instance.
*/
StateManager getStateManager()
{
return mStateManager;
}
/**
* Get the manager that manages registered listeners.
*/
ListenerManager getListenerManager()
{
return mListenerManager;
}
/**
* Get the handshake builder. {@link HandshakeReader} uses this method.
*/
HandshakeBuilder getHandshakeBuilder()
{
return mHandshakeBuilder;
}
/**
* Set the agreed extensions. {@link HandshakeReader} uses this method.
*/
void setAgreedExtensions(List extensions)
{
mAgreedExtensions = extensions;
}
/**
* Set the agreed protocol. {@link HandshakeReader} uses this method.
*/
void setAgreedProtocol(String protocol)
{
mAgreedProtocol = protocol;
}
/**
* Called by the reading thread as its first step.
*/
void onReadingThreadStarted()
{
boolean bothStarted = false;
synchronized (mThreadsLock)
{
mReadingThreadStarted = true;
if (mWritingThreadStarted)
{
// Both the reading thread and the writing thread have started.
bothStarted = true;
}
}
// Call onConnected() method of listeners if not called yet.
callOnConnectedIfNotYet();
// If both the reading thread and the writing thread have started.
if (bothStarted)
{
onThreadsStarted();
}
}
/**
* Called by the writing thread as its first step.
*/
void onWritingThreadStarted()
{
boolean bothStarted = false;
synchronized (mThreadsLock)
{
mWritingThreadStarted = true;
if (mReadingThreadStarted)
{
// Both the reading thread and the writing thread have started.
bothStarted = true;
}
}
// Call onConnected() method of listeners if not called yet.
callOnConnectedIfNotYet();
// If both the reading thread and the writing thread have started.
if (bothStarted)
{
onThreadsStarted();
}
}
/**
* Call {@link WebSocketListener#onConnected(WebSocket, Map)} method
* of the registered listeners if it has not been called yet. Either
* the reading thread or the writing thread calls this method.
*/
private void callOnConnectedIfNotYet()
{
synchronized (mOnConnectedCalledLock)
{
// If onConnected() has already been called.
if (mOnConnectedCalled)
{
// Do not call onConnected() twice.
return;
}
mOnConnectedCalled = true;
}
// Notify the listeners that the handshake succeeded.
mListenerManager.callOnConnected(mServerHeaders);
}
/**
* Called when both the reading thread and the writing thread have started.
* This method is called in the context of either the reading thread or
* the writing thread.
*/
private void onThreadsStarted()
{
// Start sending ping frames periodically.
// If the interval is zero, this call does nothing.
mPingSender.start();
// Likewise, start the pong sender.
mPongSender.start();
}
/**
* Called by the reading thread as its last step.
*/
void onReadingThreadFinished(WebSocketFrame closeFrame)
{
synchronized (mThreadsLock)
{
mReadingThreadFinished = true;
mServerCloseFrame = closeFrame;
if (mWritingThreadFinished == false)
{
// Wait for the writing thread to finish.
return;
}
}
// Both the reading thread and the writing thread have finished.
onThreadsFinished();
}
/**
* Called by the writing thread as its last step.
*/
void onWritingThreadFinished(WebSocketFrame closeFrame)
{
synchronized (mThreadsLock)
{
mWritingThreadFinished = true;
mClientCloseFrame = closeFrame;
if (mReadingThreadFinished == false)
{
// Wait for the reading thread to finish.
return;
}
}
// Both the reading thread and the writing thread have finished.
onThreadsFinished();
}
/**
* Called when both the reading thread and the writing thread have finished.
* This method is called in the context of either the reading thread or
* the writing thread.
*/
private void onThreadsFinished()
{
finish();
}
void finish()
{
// Stop the ping sender and the pong sender.
mPingSender.stop();
mPongSender.stop();
try
{
// Close the raw socket.
mSocketConnector.getSocket().close();
}
catch (Throwable t)
{
// Ignore any error raised by close().
}
synchronized (mStateManager)
{
// Change the state to CLOSED.
mStateManager.setState(CLOSED);
}
// Notify the listeners of the state change.
mListenerManager.callOnStateChanged(CLOSED);
// Notify the listeners that the WebSocket was disconnected.
mListenerManager.callOnDisconnected(
mServerCloseFrame, mClientCloseFrame, mStateManager.getClosedByServer());
}
/**
* Call {@link #finish()} from within a separate thread.
*/
private void finishAsynchronously()
{
WebSocketThread thread = new FinishThread(this);
// Execute onThreadCreated() of the listeners.
thread.callOnThreadCreated();
thread.start();
}
/**
* Find a per-message compression extension from among the agreed extensions.
*/
private PerMessageCompressionExtension findAgreedPerMessageCompressionExtension()
{
if (mAgreedExtensions == null)
{
return null;
}
for (WebSocketExtension extension : mAgreedExtensions)
{
if (extension instanceof PerMessageCompressionExtension)
{
return (PerMessageCompressionExtension)extension;
}
}
return null;
}
/**
* Get the PerMessageCompressionExtension in the agreed extensions.
* This method returns null if a per-message compression extension
* is not found in the agreed extensions.
*/
PerMessageCompressionExtension getPerMessageCompressionExtension()
{
return mPerMessageCompressionExtension;
}
}