/******************************************************************************* * Copyright (c) 2011 Intalio, Inc. * ====================================================================== * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. *******************************************************************************/ package org.eclipse.jetty.websocket; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ProtocolException; import java.net.SocketAddress; import java.net.URI; import java.nio.channels.ByteChannel; import java.nio.channels.SocketChannel; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.eclipse.jetty.util.log.Logger; /* ------------------------------------------------------------ */ /** * <p>{@link WebSocketClient} allows to create multiple connections to multiple destinations * that can speak the websocket protocol.</p> * <p>When creating websocket connections, {@link WebSocketClient} accepts a {@link WebSocket} * object (to receive events from the server), and returns a {@link WebSocket.Connection} to * send data to the server.</p> * <p>Example usage is as follows:</p> * <pre> * WebSocketClientFactory factory = new WebSocketClientFactory(); * factory.start(); * * WebSocketClient client = factory.newWebSocketClient(); * // Configure the client * * WebSocket.Connection connection = client.open(new URI("ws://127.0.0.1:8080/"), new WebSocket.OnTextMessage() * { * public void onOpen(Connection connection) * { * // open notification * } * * public void onClose(int closeCode, String message) * { * // close notification * } * * public void onMessage(String data) * { * // handle incoming message * } * }).get(5, TimeUnit.SECONDS); * * connection.sendMessage("Hello World"); * </pre> */ public class WebSocketClient { private final static Logger __log = org.eclipse.jetty.util.log.Log.getLogger(WebSocketClient.class.getName()); private final WebSocketClientFactory _factory; private final Map<String,String> _cookies=new ConcurrentHashMap<String, String>(); private final List<String> _extensions=new CopyOnWriteArrayList<String>(); private String _origin; private String _protocol; private int _maxIdleTime=-1; private int _maxTextMessageSize=16*1024; private int _maxBinaryMessageSize=-1; private MaskGen _maskGen; private SocketAddress _bindAddress; /* ------------------------------------------------------------ */ /** * <p>Creates a WebSocketClient from a private WebSocketClientFactory.</p> * <p>This can be wasteful of resources if many clients are created.</p> * * @deprecated Use {@link WebSocketClientFactory#newWebSocketClient()} * @throws Exception if the private WebSocketClientFactory fails to start */ @Deprecated public WebSocketClient() throws Exception { _factory=new WebSocketClientFactory(); _factory.start(); _maskGen=_factory.getMaskGen(); } /* ------------------------------------------------------------ */ /** * <p>Creates a WebSocketClient with shared WebSocketClientFactory.</p> * * @param factory the shared {@link WebSocketClientFactory} */ public WebSocketClient(WebSocketClientFactory factory) { _factory=factory; _maskGen=_factory.getMaskGen(); } /* ------------------------------------------------------------ */ /** * @return The WebSocketClientFactory this client was created with. */ public WebSocketClientFactory getFactory() { return _factory; } /* ------------------------------------------------------------ */ /** * @return the address to bind the socket channel to * @see #setBindAddress(SocketAddress) */ public SocketAddress getBindAddress() { return _bindAddress; } /* ------------------------------------------------------------ */ /** * @param bindAddress the address to bind the socket channel to * @see #getBindAddress() */ public void setBindAddress(SocketAddress bindAddress) { this._bindAddress = bindAddress; } /* ------------------------------------------------------------ */ /** * @return The maxIdleTime in ms for connections opened by this client, * or -1 if the default from {@link WebSocketClientFactory#getSelectorManager()} is used. * @see #setMaxIdleTime(int) */ public int getMaxIdleTime() { return _maxIdleTime; } /* ------------------------------------------------------------ */ /** * @param maxIdleTime The max idle time in ms for connections opened by this client * @see #getMaxIdleTime() */ public void setMaxIdleTime(int maxIdleTime) { _maxIdleTime=maxIdleTime; } /* ------------------------------------------------------------ */ /** * @return The subprotocol string for connections opened by this client. * @see #setProtocol(String) */ public String getProtocol() { return _protocol; } /* ------------------------------------------------------------ */ /** * @param protocol The subprotocol string for connections opened by this client. * @see #getProtocol() */ public void setProtocol(String protocol) { _protocol = protocol; } /* ------------------------------------------------------------ */ /** * @return The origin URI of the client * @see #setOrigin(String) */ public String getOrigin() { return _origin; } /* ------------------------------------------------------------ */ /** * @param origin The origin URI of the client (eg "http://example.com") * @see #getOrigin() */ public void setOrigin(String origin) { _origin = origin; } /* ------------------------------------------------------------ */ /** * <p>Returns the map of the cookies that are sent during the initial HTTP handshake * that upgrades to the websocket protocol.</p> * @return The read-write cookie map */ public Map<String,String> getCookies() { return _cookies; } /* ------------------------------------------------------------ */ /** * @return The list of websocket protocol extensions */ public List<String> getExtensions() { return _extensions; } /* ------------------------------------------------------------ */ /** * @return the mask generator to use, or null if not mask generator should be used * @see #setMaskGen(MaskGen) */ public MaskGen getMaskGen() { return _maskGen; } /* ------------------------------------------------------------ */ /** * @param maskGen the mask generator to use, or null if not mask generator should be used * @see #getMaskGen() */ public void setMaskGen(MaskGen maskGen) { _maskGen = maskGen; } /* ------------------------------------------------------------ */ /** * @return The initial maximum text message size (in characters) for a connection */ public int getMaxTextMessageSize() { return _maxTextMessageSize; } /* ------------------------------------------------------------ */ /** * Set the initial maximum text message size for a connection. This can be changed by * the application calling {@link WebSocket.Connection#setMaxTextMessageSize(int)}. * @param maxTextMessageSize The default maximum text message size (in characters) for a connection */ public void setMaxTextMessageSize(int maxTextMessageSize) { _maxTextMessageSize = maxTextMessageSize; } /* ------------------------------------------------------------ */ /** * @return The initial maximum binary message size (in bytes) for a connection */ public int getMaxBinaryMessageSize() { return _maxBinaryMessageSize; } /* ------------------------------------------------------------ */ /** * Set the initial maximum binary message size for a connection. This can be changed by * the application calling {@link WebSocket.Connection#setMaxBinaryMessageSize(int)}. * @param maxBinaryMessageSize The default maximum binary message size (in bytes) for a connection */ public void setMaxBinaryMessageSize(int maxBinaryMessageSize) { _maxBinaryMessageSize = maxBinaryMessageSize; } /* ------------------------------------------------------------ */ /** * <p>Opens a websocket connection to the URI and blocks until the connection is accepted or there is an error.</p> * * @param uri The URI to connect to. * @param websocket The {@link WebSocket} instance to handle incoming events. * @param maxConnectTime The interval to wait for a successful connection * @param units the units of the maxConnectTime * @return A {@link WebSocket.Connection} * @throws IOException if the connection fails * @throws InterruptedException if the thread is interrupted * @throws TimeoutException if the timeout elapses before the connection is completed * @see #open(URI, WebSocket) */ public WebSocket.Connection open(URI uri, WebSocket websocket,long maxConnectTime,TimeUnit units) throws IOException, InterruptedException, TimeoutException { try { return open(uri,websocket).get(maxConnectTime,units); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof IOException) throw (IOException)cause; if (cause instanceof Error) throw (Error)cause; if (cause instanceof RuntimeException) throw (RuntimeException)cause; throw new RuntimeException(cause); } } /* ------------------------------------------------------------ */ /** * <p>Asynchronously opens a websocket connection and returns a {@link Future} to obtain the connection.</p> * <p>The caller must call {@link Future#get(long, TimeUnit)} if they wish to impose a connect timeout on the open.</p> * * @param uri The URI to connect to. * @param websocket The {@link WebSocket} instance to handle incoming events. * @return A {@link Future} to the {@link WebSocket.Connection} * @throws IOException if the connection fails * @see #open(URI, WebSocket, long, TimeUnit) */ public Future<WebSocket.Connection> open(URI uri, WebSocket websocket) throws IOException { if (!_factory.isStarted()) throw new IllegalStateException("Factory !started"); InetSocketAddress address = toSocketAddress(uri); SocketChannel channel = SocketChannel.open(); if (_bindAddress != null) channel.socket().bind(_bindAddress); channel.socket().setTcpNoDelay(true); WebSocketFuture holder = new WebSocketFuture(websocket, uri, this, channel); channel.configureBlocking(false); channel.connect(address); _factory.getSelectorManager().register(channel, holder); return holder; } public static InetSocketAddress toSocketAddress(URI uri) { String scheme = uri.getScheme(); if (!("ws".equalsIgnoreCase(scheme) || "wss".equalsIgnoreCase(scheme))) throw new IllegalArgumentException("Bad WebSocket scheme: " + scheme); int port = uri.getPort(); if (port == 0) throw new IllegalArgumentException("Bad WebSocket port: " + port); if (port < 0) port = "ws".equals(scheme) ? 80 : 443; return new InetSocketAddress(uri.getHost(), port); } /* ------------------------------------------------------------ */ /** The Future Websocket Connection. */ static class WebSocketFuture implements Future<WebSocket.Connection> { final WebSocket _websocket; final URI _uri; final WebSocketClient _client; final CountDownLatch _done = new CountDownLatch(1); ByteChannel _channel; WebSocketConnection _connection; Throwable _exception; private WebSocketFuture(WebSocket websocket, URI uri, WebSocketClient client, ByteChannel channel) { _websocket=websocket; _uri=uri; _client=client; _channel=channel; } public void onConnection(WebSocketConnection connection) { try { _client.getFactory().addConnection(connection); connection.getConnection().setMaxTextMessageSize(_client.getMaxTextMessageSize()); connection.getConnection().setMaxBinaryMessageSize(_client.getMaxBinaryMessageSize()); WebSocketConnection con; synchronized (this) { if (_channel!=null) _connection=connection; con=_connection; } if (con!=null) { if (_websocket instanceof WebSocket.OnFrame) ((WebSocket.OnFrame)_websocket).onHandshake((WebSocket.FrameConnection)con.getConnection()); _websocket.onOpen(con.getConnection()); } } finally { _done.countDown(); } } public void handshakeFailed(Throwable ex) { try { ByteChannel channel=null; synchronized (this) { if (_channel!=null) { channel=_channel; _channel=null; _exception=ex; } } if (channel!=null) { if (ex instanceof ProtocolException) closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_PROTOCOL,ex.getMessage()); else closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,ex.getMessage()); } } finally { _done.countDown(); } } public Map<String,String> getCookies() { return _client.getCookies(); } public String getProtocol() { return _client.getProtocol(); } public WebSocket getWebSocket() { return _websocket; } public URI getURI() { return _uri; } public int getMaxIdleTime() { return _client.getMaxIdleTime(); } public String getOrigin() { return _client.getOrigin(); } public MaskGen getMaskGen() { return _client.getMaskGen(); } @Override public String toString() { return "[" + _uri + ","+_websocket+"]@"+hashCode(); } public boolean cancel(boolean mayInterruptIfRunning) { try { ByteChannel channel=null; synchronized (this) { if (_connection==null && _exception==null && _channel!=null) { channel=_channel; _channel=null; } } if (channel!=null) { closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,"cancelled"); return true; } return false; } finally { _done.countDown(); } } public boolean isCancelled() { synchronized (this) { return _channel==null && _connection==null; } } public boolean isDone() { synchronized (this) { return _connection!=null && _exception==null; } } public org.eclipse.jetty.websocket.WebSocket.Connection get() throws InterruptedException, ExecutionException { try { return get(Long.MAX_VALUE,TimeUnit.SECONDS); } catch(TimeoutException e) { throw new IllegalStateException("The universe has ended",e); } } public org.eclipse.jetty.websocket.WebSocket.Connection get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { _done.await(timeout,unit); ByteChannel channel=null; org.eclipse.jetty.websocket.WebSocket.Connection connection=null; Throwable exception; synchronized (this) { exception=_exception; if (_connection==null) { exception=_exception; channel=_channel; _channel=null; } else connection=_connection.getConnection(); } if (channel!=null) closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,"timeout"); if (exception!=null) throw new ExecutionException(exception); if (connection!=null) return connection; throw new TimeoutException(); } private void closeChannel(ByteChannel channel,int code, String message) { try { _websocket.onClose(code,message); } catch(Exception e) { __log.warn(e); } try { channel.close(); } catch(IOException e) { __log.debug(e); } } } }