All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.java_websocket.client.WebSocketClient Maven / Gradle / Ivy

There is a newer version: 1.5.7
Show newest version
/*
 * Copyright (c) 2010-2020 Nathan Rajlich
 *
 *  Permission is hereby granted, free of charge, to any person
 *  obtaining a copy of this software and associated documentation
 *  files (the "Software"), to deal in the Software without
 *  restriction, including without limitation the rights to use,
 *  copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following
 *  conditions:
 *
 *  The above copyright notice and this permission notice shall be
 *  included in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 *  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 *  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 *  OTHER DEALINGS IN THE SOFTWARE.
 */

package org.java_websocket.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.net.SocketFactory;
import javax.net.ssl.*;

import org.java_websocket.AbstractWebSocket;
import org.java_websocket.WebSocket;
import org.java_websocket.WebSocketImpl;
import org.java_websocket.drafts.Draft;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.enums.Opcode;
import org.java_websocket.enums.ReadyState;
import org.java_websocket.exceptions.InvalidHandshakeException;
import org.java_websocket.framing.CloseFrame;
import org.java_websocket.framing.Framedata;
import org.java_websocket.handshake.HandshakeImpl1Client;
import org.java_websocket.handshake.Handshakedata;
import org.java_websocket.handshake.ServerHandshake;

/**
 * A subclass must implement at least onOpen, onClose, and onMessage to be
 * useful. At runtime the user is expected to establish a connection via {@link #connect()}, then receive events like {@link #onMessage(String)} via the overloaded methods and to {@link #send(String)} data to the server.
 */
public abstract class WebSocketClient extends AbstractWebSocket implements Runnable, WebSocket {

	/**
	 * The URI this channel is supposed to connect to.
	 */
	protected URI uri = null;

	/**
	 * The underlying engine
	 */
	private WebSocketImpl engine = null;

	/**
	 * The socket for this WebSocketClient
	 */
	private Socket socket = null;

	/**
	 * The SocketFactory for this WebSocketClient
	 * @since 1.4.0
	 */
	private SocketFactory socketFactory = null;

	/**
	 * The used OutputStream
	 */
	private OutputStream ostream;

	/**
	 * The used proxy, if any
	 */
	private Proxy proxy = Proxy.NO_PROXY;

	/**
	 * The thread to write outgoing message
	 */
	private Thread writeThread;

	/**
	 * The thread to connect and read message
	 */
	private Thread connectReadThread;

	/**
	 * The draft to use
	 */
	private Draft draft;

	/**
	 * The additional headers to use
	 */
	private Map headers;

	/**
	 * The latch for connectBlocking()
	 */
	private CountDownLatch connectLatch = new CountDownLatch( 1 );

	/**
	 * The latch for closeBlocking()
	 */
	private CountDownLatch closeLatch = new CountDownLatch( 1 );

	/**
	 * The socket timeout value to be used in milliseconds.
	 */
	private int connectTimeout = 0;

	/**
	 * DNS resolver that translates a URI to an InetAddress
	 *
	 * @see InetAddress
	 * @since 1.4.1
	 */
	private DnsResolver dnsResolver = null;

	/**
	 * Constructs a WebSocketClient instance and sets it to the connect to the
	 * specified URI. The channel does not attampt to connect automatically. The connection
	 * will be established once you call connect.
	 *
	 * @param serverUri the server URI to connect to
	 */
	public WebSocketClient( URI serverUri ) {
		this( serverUri, new Draft_6455());
	}

	/**
	 * Constructs a WebSocketClient instance and sets it to the connect to the
	 * specified URI. The channel does not attampt to connect automatically. The connection
	 * will be established once you call connect.
	 * @param serverUri the server URI to connect to
	 * @param protocolDraft The draft which should be used for this connection
	 */
	public WebSocketClient( URI serverUri , Draft protocolDraft ) {
		this( serverUri, protocolDraft, null, 0 );
	}

	/**
	 * Constructs a WebSocketClient instance and sets it to the connect to the
	 * specified URI. The channel does not attampt to connect automatically. The connection
	 * will be established once you call connect.
	 * @param serverUri the server URI to connect to
	 * @param httpHeaders Additional HTTP-Headers
	 * @since 1.3.8
	 */
	public WebSocketClient( URI serverUri, Map httpHeaders) {
		this(serverUri, new Draft_6455(), httpHeaders);
	}

	/**
	 * Constructs a WebSocketClient instance and sets it to the connect to the
	 * specified URI. The channel does not attampt to connect automatically. The connection
	 * will be established once you call connect.
	 * @param serverUri the server URI to connect to
	 * @param protocolDraft The draft which should be used for this connection
	 * @param httpHeaders Additional HTTP-Headers
	 * @since 1.3.8
	 */
	public WebSocketClient( URI serverUri , Draft protocolDraft , Map httpHeaders) {
		this(serverUri, protocolDraft, httpHeaders, 0);
	}

	/**
	 * Constructs a WebSocketClient instance and sets it to the connect to the
	 * specified URI. The channel does not attampt to connect automatically. The connection
	 * will be established once you call connect.
	 * @param serverUri the server URI to connect to
	 * @param protocolDraft The draft which should be used for this connection
	 * @param httpHeaders Additional HTTP-Headers
	 * @param connectTimeout The Timeout for the connection
	 */
	public WebSocketClient( URI serverUri , Draft protocolDraft , Map httpHeaders , int connectTimeout ) {
		if( serverUri == null ) {
			throw new IllegalArgumentException();
		} else if( protocolDraft == null ) {
			throw new IllegalArgumentException( "null as draft is permitted for `WebSocketServer` only!" );
		}
		this.uri = serverUri;
		this.draft = protocolDraft;
		this.dnsResolver = new DnsResolver() {
			@Override
			public InetAddress resolve(URI uri) throws UnknownHostException {
				return InetAddress.getByName(uri.getHost());
			}
		};
		if(httpHeaders != null) {
			headers = new TreeMap(String.CASE_INSENSITIVE_ORDER);
			headers.putAll(httpHeaders);
		}
		this.connectTimeout = connectTimeout;
		setTcpNoDelay( false );
		setReuseAddr( false );
		this.engine = new WebSocketImpl( this, protocolDraft );
	}

	/**
	 * Returns the URI that this WebSocketClient is connected to.
	 * @return the URI connected to
	 */
	public URI getURI() {
		return uri;
	}

	/**
	 * Returns the protocol version this channel uses.
* For more infos see https://github.com/TooTallNate/Java-WebSocket/wiki/Drafts * @return The draft used for this client */ public Draft getDraft() { return draft; } /** * Returns the socket to allow Hostname Verification * @return the socket used for this connection */ public Socket getSocket() { return socket; } /** * @since 1.4.1 * Adds an additional header to be sent in the handshake.
* If the connection is already made, adding headers has no effect, * unless reconnect is called, which then a new handshake is sent.
* If a header with the same key already exists, it is overridden. * @param key Name of the header to add. * @param value Value of the header to add. */ public void addHeader(String key, String value){ if(headers == null) headers = new TreeMap(String.CASE_INSENSITIVE_ORDER); headers.put(key, value); } /** * @since 1.4.1 * Removes a header from the handshake to be sent, if header key exists.
* @param key Name of the header to remove. * @return the previous value associated with key, or * null if there was no mapping for key. */ public String removeHeader(String key) { if(headers == null) return null; return headers.remove(key); } /** * @since 1.4.1 * Clears all previously put headers. */ public void clearHeaders() { headers = null; } /** * Sets a custom DNS resolver. * * @param dnsResolver The DnsResolver to use. * * @since 1.4.1 */ public void setDnsResolver(DnsResolver dnsResolver) { this.dnsResolver = dnsResolver; } /** * Reinitiates the websocket connection. This method does not block. * @since 1.3.8 */ public void reconnect() { reset(); connect(); } /** * Same as reconnect but blocks until the websocket reconnected or failed to do so.
* @return Returns whether it succeeded or not. * @throws InterruptedException Thrown when the threads get interrupted * @since 1.3.8 */ public boolean reconnectBlocking() throws InterruptedException { reset(); return connectBlocking(); } /** * Reset everything relevant to allow a reconnect * @since 1.3.8 */ private void reset() { Thread current = Thread.currentThread(); if (current == writeThread || current == connectReadThread) { throw new IllegalStateException("You cannot initialize a reconnect out of the websocket thread. Use reconnect in another thread to insure a successful cleanup."); } try { closeBlocking(); if( writeThread != null ) { this.writeThread.interrupt(); this.writeThread = null; } if( connectReadThread != null ) { this.connectReadThread.interrupt(); this.connectReadThread = null; } this.draft.reset(); if( this.socket != null ) { this.socket.close(); this.socket = null; } } catch ( Exception e ) { onError( e ); engine.closeConnection( CloseFrame.ABNORMAL_CLOSE, e.getMessage() ); return; } connectLatch = new CountDownLatch( 1 ); closeLatch = new CountDownLatch( 1 ); this.engine = new WebSocketImpl( this, this.draft ); } /** * Initiates the websocket connection. This method does not block. */ public void connect() { if( connectReadThread != null ) throw new IllegalStateException( "WebSocketClient objects are not reuseable" ); connectReadThread = new Thread( this ); connectReadThread.setName( "WebSocketConnectReadThread-" + connectReadThread.getId() ); connectReadThread.start(); } /** * Same as connect but blocks until the websocket connected or failed to do so.
* @return Returns whether it succeeded or not. * @throws InterruptedException Thrown when the threads get interrupted */ public boolean connectBlocking() throws InterruptedException { connect(); connectLatch.await(); return engine.isOpen(); } /** * Same as connect but blocks with a timeout until the websocket connected or failed to do so.
* @param timeout * The connect timeout * @param timeUnit * The timeout time unit * @return Returns whether it succeeded or not. * @throws InterruptedException Thrown when the threads get interrupted */ public boolean connectBlocking(long timeout, TimeUnit timeUnit) throws InterruptedException { connect(); return connectLatch.await(timeout, timeUnit) && engine.isOpen(); } /** * Initiates the websocket close handshake. This method does not block
* In oder to make sure the connection is closed use closeBlocking */ public void close() { if( writeThread != null ) { engine.close( CloseFrame.NORMAL ); } } /** * Same as close but blocks until the websocket closed or failed to do so.
* @throws InterruptedException Thrown when the threads get interrupted */ public void closeBlocking() throws InterruptedException { close(); closeLatch.await(); } /** * Sends text to the connected websocket server. * * @param text * The string which will be transmitted. */ public void send( String text ) { engine.send( text ); } /** * Sends binary data to the connected webSocket server. * * @param data * The byte-Array of data to send to the WebSocket server. */ public void send( byte[] data ) { engine.send( data ); } @Override public T getAttachment() { return engine.getAttachment(); } @Override public void setAttachment(T attachment) { engine.setAttachment( attachment ); } @Override protected Collection getConnections() { return Collections.singletonList((WebSocket ) engine ); } @Override public void sendPing() { engine.sendPing( ); } public void run() { InputStream istream; try { boolean isNewSocket = false; if (socketFactory != null) { socket = socketFactory.createSocket(); } else if( socket == null ) { socket = new Socket( proxy ); isNewSocket = true; } else if( socket.isClosed() ) { throw new IOException(); } socket.setTcpNoDelay( isTcpNoDelay() ); socket.setReuseAddress( isReuseAddr() ); if (!socket.isConnected()) { InetSocketAddress addr = new InetSocketAddress(dnsResolver.resolve(uri), this.getPort()); socket.connect(addr, connectTimeout); } // if the socket is set by others we don't apply any TLS wrapper if (isNewSocket && "wss".equals( uri.getScheme())) { SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); sslContext.init(null, null, null); SSLSocketFactory factory = sslContext.getSocketFactory(); socket = factory.createSocket(socket, uri.getHost(), getPort(), true); } if (socket instanceof SSLSocket) { SSLSocket sslSocket = (SSLSocket)socket; SSLParameters sslParameters = sslSocket.getSSLParameters(); // Make sure we perform hostname validation sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); onSetSSLParameters(sslParameters); sslSocket.setSSLParameters(sslParameters); } istream = socket.getInputStream(); ostream = socket.getOutputStream(); sendHandshake(); } catch ( /*IOException | SecurityException | UnresolvedAddressException | InvalidHandshakeException | ClosedByInterruptException | SocketTimeoutException */Exception e ) { onWebsocketError( engine, e ); engine.closeConnection( CloseFrame.NEVER_CONNECTED, e.getMessage() ); return; } catch (InternalError e) { // https://bugs.openjdk.java.net/browse/JDK-8173620 if (e.getCause() instanceof InvocationTargetException && e.getCause().getCause() instanceof IOException) { IOException cause = (IOException) e.getCause().getCause(); onWebsocketError(engine, cause); engine.closeConnection(CloseFrame.NEVER_CONNECTED, cause.getMessage()); return; } throw e; } writeThread = new Thread( new WebsocketWriteThread(this) ); writeThread.start(); byte[] rawbuffer = new byte[ WebSocketImpl.RCVBUF ]; int readBytes; try { while ( !isClosing() && !isClosed() && ( readBytes = istream.read( rawbuffer ) ) != -1 ) { engine.decode( ByteBuffer.wrap( rawbuffer, 0, readBytes ) ); } engine.eot(); } catch ( IOException e ) { handleIOException(e); } catch ( RuntimeException e ) { // this catch case covers internal errors only and indicates a bug in this websocket implementation onError( e ); engine.closeConnection( CloseFrame.ABNORMAL_CLOSE, e.getMessage() ); } connectReadThread = null; } /** * Apply specific SSLParameters * * @param sslParameters the SSLParameters which will be used for the SSLSocket */ protected void onSetSSLParameters(SSLParameters sslParameters) { } /** * Extract the specified port * @return the specified port or the default port for the specific scheme */ private int getPort() { int port = uri.getPort(); if( port == -1 ) { String scheme = uri.getScheme(); if( "wss".equals( scheme ) ) { return WebSocketImpl.DEFAULT_WSS_PORT; } else if( "ws".equals( scheme ) ) { return WebSocketImpl.DEFAULT_PORT; } else { throw new IllegalArgumentException( "unknown scheme: " + scheme ); } } return port; } /** * Create and send the handshake to the other endpoint * @throws InvalidHandshakeException a invalid handshake was created */ private void sendHandshake() throws InvalidHandshakeException { String path; String part1 = uri.getRawPath(); String part2 = uri.getRawQuery(); if( part1 == null || part1.length() == 0 ) path = "/"; else path = part1; if( part2 != null ) path += '?' + part2; int port = getPort(); String host = uri.getHost() + ( (port != WebSocketImpl.DEFAULT_PORT && port != WebSocketImpl.DEFAULT_WSS_PORT) ? ":" + port : "" ); HandshakeImpl1Client handshake = new HandshakeImpl1Client(); handshake.setResourceDescriptor( path ); handshake.put( "Host", host ); if( headers != null ) { for( Map.Entry kv : headers.entrySet() ) { handshake.put( kv.getKey(), kv.getValue() ); } } engine.startHandshake( handshake ); } /** * This represents the state of the connection. */ public ReadyState getReadyState() { return engine.getReadyState(); } /** * Calls subclass' implementation of onMessage. */ @Override public final void onWebsocketMessage( WebSocket conn, String message ) { onMessage( message ); } @Override public final void onWebsocketMessage( WebSocket conn, ByteBuffer blob ) { onMessage( blob ); } /** * Calls subclass' implementation of onOpen. */ @Override public final void onWebsocketOpen( WebSocket conn, Handshakedata handshake ) { startConnectionLostTimer(); onOpen( (ServerHandshake) handshake ); connectLatch.countDown(); } /** * Calls subclass' implementation of onClose. */ @Override public final void onWebsocketClose( WebSocket conn, int code, String reason, boolean remote ) { stopConnectionLostTimer(); if( writeThread != null ) writeThread.interrupt(); onClose( code, reason, remote ); connectLatch.countDown(); closeLatch.countDown(); } /** * Calls subclass' implementation of onIOError. */ @Override public final void onWebsocketError( WebSocket conn, Exception ex ) { onError( ex ); } @Override public final void onWriteDemand( WebSocket conn ) { // nothing to do } @Override public void onWebsocketCloseInitiated( WebSocket conn, int code, String reason ) { onCloseInitiated( code, reason ); } @Override public void onWebsocketClosing( WebSocket conn, int code, String reason, boolean remote ) { onClosing( code, reason, remote ); } /** * Send when this peer sends a close handshake * * @param code The codes can be looked up here: {@link CloseFrame} * @param reason Additional information string */ public void onCloseInitiated( int code, String reason ) { //To overwrite } /** Called as soon as no further frames are accepted * * @param code The codes can be looked up here: {@link CloseFrame} * @param reason Additional information string * @param remote Returns whether or not the closing of the connection was initiated by the remote host. */ public void onClosing( int code, String reason, boolean remote ) { //To overwrite } /** * Getter for the engine * @return the engine */ public WebSocket getConnection() { return engine; } @Override public InetSocketAddress getLocalSocketAddress( WebSocket conn ) { if( socket != null ) return (InetSocketAddress) socket.getLocalSocketAddress(); return null; } @Override public InetSocketAddress getRemoteSocketAddress( WebSocket conn ) { if( socket != null ) return (InetSocketAddress) socket.getRemoteSocketAddress(); return null; } // ABTRACT METHODS ///////////////////////////////////////////////////////// /** * Called after an opening handshake has been performed and the given websocket is ready to be written on. * @param handshakedata The handshake of the websocket instance */ public abstract void onOpen( ServerHandshake handshakedata ); /** * Callback for string messages received from the remote host * * @see #onMessage(ByteBuffer) * @param message The UTF-8 decoded message that was received. **/ public abstract void onMessage( String message ); /** * Called after the websocket connection has been closed. * * @param code * The codes can be looked up here: {@link CloseFrame} * @param reason * Additional information string * @param remote * Returns whether or not the closing of the connection was initiated by the remote host. **/ public abstract void onClose( int code, String reason, boolean remote ); /** * Called when errors occurs. If an error causes the websocket connection to fail {@link #onClose(int, String, boolean)} will be called additionally.
* This method will be called primarily because of IO or protocol errors.
* If the given exception is an RuntimeException that probably means that you encountered a bug.
* * @param ex The exception causing this error **/ public abstract void onError( Exception ex ); /** * Callback for binary messages received from the remote host * * @see #onMessage(String) * * @param bytes * The binary message that was received. **/ public void onMessage( ByteBuffer bytes ) { //To overwrite } private class WebsocketWriteThread implements Runnable { private final WebSocketClient webSocketClient; WebsocketWriteThread(WebSocketClient webSocketClient) { this.webSocketClient = webSocketClient; } @Override public void run() { Thread.currentThread().setName( "WebSocketWriteThread-" + Thread.currentThread().getId() ); try { runWriteData(); } catch ( IOException e ) { handleIOException( e ); } finally { closeSocket(); writeThread = null; } } /** * Write the data into the outstream * @throws IOException if write or flush did not work */ private void runWriteData() throws IOException { try { while( !Thread.interrupted() ) { ByteBuffer buffer = engine.outQueue.take(); ostream.write( buffer.array(), 0, buffer.limit() ); ostream.flush(); } } catch ( InterruptedException e ) { for (ByteBuffer buffer : engine.outQueue) { ostream.write( buffer.array(), 0, buffer.limit() ); ostream.flush(); } Thread.currentThread().interrupt(); } } /** * Closing the socket */ private void closeSocket() { try { if( socket != null ) { socket.close(); } } catch ( IOException ex ) { onWebsocketError( webSocketClient, ex ); } } } /** * Method to set a proxy for this connection * @param proxy the proxy to use for this websocket client */ public void setProxy( Proxy proxy ) { if( proxy == null ) throw new IllegalArgumentException(); this.proxy = proxy; } /** * Accepts bound and unbound sockets.
* This method must be called before connect. * If the given socket is not yet bound it will be bound to the uri specified in the constructor. * @param socket The socket which should be used for the connection * @deprecated use setSocketFactory */ @Deprecated public void setSocket( Socket socket ) { if( this.socket != null ) { throw new IllegalStateException( "socket has already been set" ); } this.socket = socket; } /** * Accepts a SocketFactory.
* This method must be called before connect. * The socket will be bound to the uri specified in the constructor. * @param socketFactory The socket factory which should be used for the connection. */ public void setSocketFactory(SocketFactory socketFactory) { this.socketFactory = socketFactory; } @Override public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin ) { engine.sendFragmentedFrame( op, buffer, fin ); } @Override public boolean isOpen() { return engine.isOpen(); } @Override public boolean isFlushAndClose() { return engine.isFlushAndClose(); } @Override public boolean isClosed() { return engine.isClosed(); } @Override public boolean isClosing() { return engine.isClosing(); } @Override public boolean hasBufferedData() { return engine.hasBufferedData(); } @Override public void close( int code ) { engine.close( code ); } @Override public void close( int code, String message ) { engine.close( code, message ); } @Override public void closeConnection( int code, String message ) { engine.closeConnection( code, message ); } @Override public void send( ByteBuffer bytes ) { engine.send( bytes ); } @Override public void sendFrame( Framedata framedata ) { engine.sendFrame( framedata ); } @Override public void sendFrame( Collection frames ) { engine.sendFrame( frames ); } @Override public InetSocketAddress getLocalSocketAddress() { return engine.getLocalSocketAddress(); } @Override public InetSocketAddress getRemoteSocketAddress() { return engine.getRemoteSocketAddress(); } @Override public String getResourceDescriptor() { return uri.getPath(); } @Override public boolean hasSSLSupport() { return engine.hasSSLSupport(); } @Override public SSLSession getSSLSession() { return engine.getSSLSession(); } /** * Method to give some additional info for specific IOExceptions * @param e the IOException causing a eot. */ private void handleIOException( IOException e ) { if (e instanceof SSLException) { onError( e ); } engine.eot(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy