| // |
| // ======================================================================== |
| // Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // 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.client; |
| |
| import java.io.IOException; |
| import java.net.CookieStore; |
| import java.net.SocketAddress; |
| import java.net.URI; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.Future; |
| |
| import org.eclipse.jetty.client.HttpClient; |
| import org.eclipse.jetty.io.ByteBufferPool; |
| import org.eclipse.jetty.io.MappedByteBufferPool; |
| import org.eclipse.jetty.util.DecoratedObjectFactory; |
| import org.eclipse.jetty.util.StringUtil; |
| import org.eclipse.jetty.util.component.ContainerLifeCycle; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| import org.eclipse.jetty.util.ssl.SslContextFactory; |
| import org.eclipse.jetty.util.thread.Scheduler; |
| import org.eclipse.jetty.util.thread.ShutdownThread; |
| import org.eclipse.jetty.websocket.api.Session; |
| import org.eclipse.jetty.websocket.api.WebSocketPolicy; |
| import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; |
| import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory; |
| import org.eclipse.jetty.websocket.client.io.ConnectionManager; |
| import org.eclipse.jetty.websocket.client.io.UpgradeListener; |
| import org.eclipse.jetty.websocket.client.masks.Masker; |
| import org.eclipse.jetty.websocket.client.masks.RandomMasker; |
| import org.eclipse.jetty.websocket.common.SessionFactory; |
| import org.eclipse.jetty.websocket.common.SessionListener; |
| import org.eclipse.jetty.websocket.common.WebSocketSession; |
| import org.eclipse.jetty.websocket.common.WebSocketSessionFactory; |
| import org.eclipse.jetty.websocket.common.events.EventDriverFactory; |
| import org.eclipse.jetty.websocket.common.extensions.WebSocketExtensionFactory; |
| import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope; |
| |
| /** |
| * WebSocketClient provides a means of establishing connections to remote websocket endpoints. |
| */ |
| public class WebSocketClient extends ContainerLifeCycle implements SessionListener, WebSocketContainerScope |
| { |
| private static final Logger LOG = Log.getLogger(WebSocketClient.class); |
| |
| // From HttpClient |
| private final HttpClient httpClient; |
| private boolean syntheticHttpClient = false; |
| |
| // Other |
| private final WebSocketPolicy policy = WebSocketPolicy.newClientPolicy(); |
| private final WebSocketExtensionFactory extensionRegistry; |
| private final EventDriverFactory eventDriverFactory; |
| private final SessionFactory sessionFactory; |
| private final DecoratedObjectFactory objectFactory; |
| private Masker masker; |
| |
| /** |
| * Instantiate a WebSocketClient with defaults |
| */ |
| public WebSocketClient() |
| { |
| // Create synthetic HttpClient |
| this(new HttpClient()); |
| this.syntheticHttpClient = true; |
| } |
| |
| /** |
| * Instantiate a WebSocketClient using HttpClient for defaults |
| * |
| * @param httpClient |
| * the HttpClient to base internal defaults off of |
| */ |
| public WebSocketClient(HttpClient httpClient) |
| { |
| this(httpClient,new DecoratedObjectFactory()); |
| } |
| |
| /** |
| * Instantiate a WebSocketClient using HttpClient for defaults |
| * |
| * @param httpClient |
| * the HttpClient to base internal defaults off of |
| * @param objectFactory |
| * the DecoratedObjectFactory for all client instantiated classes |
| */ |
| public WebSocketClient(HttpClient httpClient, DecoratedObjectFactory objectFactory) |
| { |
| this.httpClient = httpClient; |
| |
| this.objectFactory = objectFactory; |
| this.extensionRegistry = new WebSocketExtensionFactory(this); |
| this.masker = new RandomMasker(); |
| this.eventDriverFactory = new EventDriverFactory(policy); |
| this.sessionFactory = new WebSocketSessionFactory(this); |
| } |
| |
| /** |
| * Create a new WebSocketClient |
| * |
| * @param executor |
| * the executor to use |
| * @deprecated use {@link #WebSocketClient(HttpClient)} instead |
| */ |
| @Deprecated |
| public WebSocketClient(Executor executor) |
| { |
| this(null,executor); |
| } |
| |
| /** |
| * Create a new WebSocketClient |
| * |
| * @param bufferPool |
| * byte buffer pool to use |
| * @deprecated use {@link #WebSocketClient(HttpClient)} instead |
| */ |
| @Deprecated |
| public WebSocketClient(ByteBufferPool bufferPool) |
| { |
| this(null,null,bufferPool); |
| } |
| |
| /** |
| * Create a new WebSocketClient |
| * |
| * @param sslContextFactory |
| * ssl context factory to use |
| * @deprecated use {@link #WebSocketClient(HttpClient)} instead |
| */ |
| @Deprecated |
| public WebSocketClient(SslContextFactory sslContextFactory) |
| { |
| this(sslContextFactory,null); |
| } |
| |
| /** |
| * Create a new WebSocketClient |
| * |
| * @param sslContextFactory |
| * ssl context factory to use |
| * @param executor |
| * the executor to use |
| * @deprecated use {@link #WebSocketClient(HttpClient)} instead |
| */ |
| @Deprecated |
| public WebSocketClient(SslContextFactory sslContextFactory, Executor executor) |
| { |
| this(sslContextFactory,executor,new MappedByteBufferPool()); |
| } |
| |
| /** |
| * Create WebSocketClient other Container Scope, to allow sharing of |
| * internal features like Executor, ByteBufferPool, SSLContextFactory, etc. |
| * |
| * @param scope |
| * the Container Scope |
| */ |
| public WebSocketClient(WebSocketContainerScope scope) |
| { |
| this(scope.getSslContextFactory(),scope.getExecutor(),scope.getBufferPool(),scope.getObjectFactory()); |
| } |
| |
| /** |
| * Create WebSocketClient other Container Scope, to allow sharing of |
| * internal features like Executor, ByteBufferPool, SSLContextFactory, etc. |
| * |
| * @param scope |
| * the Container Scope |
| * @param sslContextFactory |
| * SSL ContextFactory to use in preference to one from |
| * {@link WebSocketContainerScope#getSslContextFactory()} |
| */ |
| public WebSocketClient(WebSocketContainerScope scope, SslContextFactory sslContextFactory) |
| { |
| this(sslContextFactory,scope.getExecutor(),scope.getBufferPool(),scope.getObjectFactory()); |
| } |
| |
| /** |
| * Create WebSocketClient using sharing instances of SSLContextFactory |
| * Executor, and ByteBufferPool |
| * |
| * @param sslContextFactory |
| * shared SSL ContextFactory |
| * @param executor |
| * shared Executor |
| * @param bufferPool |
| * shared ByteBufferPool |
| */ |
| public WebSocketClient(SslContextFactory sslContextFactory, Executor executor, ByteBufferPool bufferPool) |
| { |
| this(sslContextFactory,executor,bufferPool,new DecoratedObjectFactory()); |
| } |
| |
| /** |
| * Create WebSocketClient using sharing instances of SSLContextFactory |
| * Executor, and ByteBufferPool |
| * |
| * @param sslContextFactory |
| * shared SSL ContextFactory |
| * @param executor |
| * shared Executor |
| * @param bufferPool |
| * shared ByteBufferPool |
| * @param objectFactory |
| * shared DecoratedObjectFactory |
| */ |
| public WebSocketClient(SslContextFactory sslContextFactory, Executor executor, ByteBufferPool bufferPool, DecoratedObjectFactory objectFactory) |
| { |
| this.httpClient = new HttpClient(sslContextFactory); |
| this.httpClient.setExecutor(executor); |
| this.httpClient.setByteBufferPool(bufferPool); |
| |
| if (objectFactory == null) |
| this.objectFactory = new DecoratedObjectFactory(); |
| else |
| this.objectFactory = objectFactory; |
| |
| this.extensionRegistry = new WebSocketExtensionFactory(policy,this.objectFactory,bufferPool); |
| |
| this.masker = new RandomMasker(); |
| this.eventDriverFactory = new EventDriverFactory(policy); |
| this.sessionFactory = new WebSocketSessionFactory(this); |
| } |
| |
| /** |
| * Create WebSocketClient based on pre-existing Container Scope, to allow sharing of |
| * internal features like Executor, ByteBufferPool, SSLContextFactory, etc. |
| * |
| * @param scope |
| * the Container Scope |
| * @param eventDriverFactory |
| * the EventDriver Factory to use |
| * @param sessionFactory |
| * the SessionFactory to use |
| */ |
| public WebSocketClient(WebSocketContainerScope scope, EventDriverFactory eventDriverFactory, SessionFactory sessionFactory) |
| { |
| this.httpClient = new HttpClient(scope.getSslContextFactory()); |
| this.httpClient.setExecutor(scope.getExecutor()); |
| |
| this.objectFactory = new DecoratedObjectFactory(); |
| this.extensionRegistry = new WebSocketExtensionFactory(policy,this.objectFactory,httpClient.getByteBufferPool()); |
| |
| this.masker = new RandomMasker(); |
| this.eventDriverFactory = eventDriverFactory; |
| this.sessionFactory = sessionFactory; |
| } |
| |
| public Future<Session> connect(Object websocket, URI toUri) throws IOException |
| { |
| ClientUpgradeRequest request = new ClientUpgradeRequest(toUri); |
| request.setRequestURI(toUri); |
| request.setLocalEndpoint(websocket); |
| |
| return connect(websocket,toUri,request); |
| } |
| |
| /** |
| * Connect to remote websocket endpoint |
| * |
| * @param websocket |
| * the websocket object |
| * @param toUri |
| * the websocket uri to connect to |
| * @param request |
| * the upgrade request information |
| * @return the future for the session, available on success of connect |
| * @throws IOException |
| * if unable to connect |
| */ |
| public Future<Session> connect(Object websocket, URI toUri, ClientUpgradeRequest request) throws IOException |
| { |
| return connect(websocket,toUri,request,(UpgradeListener)null); |
| } |
| |
| /** |
| * Connect to remote websocket endpoint |
| * |
| * @param websocket |
| * the websocket object |
| * @param toUri |
| * the websocket uri to connect to |
| * @param request |
| * the upgrade request information |
| * @param upgradeListener |
| * the upgrade listener |
| * @return the future for the session, available on success of connect |
| * @throws IOException |
| * if unable to connect |
| */ |
| public Future<Session> connect(Object websocket, URI toUri, ClientUpgradeRequest request, UpgradeListener upgradeListener) throws IOException |
| { |
| /* Note: UpgradeListener is used by javax.websocket.ClientEndpointConfig.Configurator |
| * See: org.eclipse.jetty.websocket.jsr356.JsrUpgradeListener |
| */ |
| if (!isStarted()) |
| { |
| throw new IllegalStateException(WebSocketClient.class.getSimpleName() + "@" + this.hashCode() + " is not started"); |
| } |
| |
| // Validate websocket URI |
| if (!toUri.isAbsolute()) |
| { |
| throw new IllegalArgumentException("WebSocket URI must be absolute"); |
| } |
| |
| if (StringUtil.isBlank(toUri.getScheme())) |
| { |
| throw new IllegalArgumentException("WebSocket URI must include a scheme"); |
| } |
| |
| String scheme = toUri.getScheme().toLowerCase(Locale.ENGLISH); |
| if (("ws".equals(scheme) == false) && ("wss".equals(scheme) == false)) |
| { |
| throw new IllegalArgumentException("WebSocket URI scheme only supports [ws] and [wss], not [" + scheme + "]"); |
| } |
| |
| request.setRequestURI(toUri); |
| request.setLocalEndpoint(websocket); |
| |
| // Validate Requested Extensions |
| for (ExtensionConfig reqExt : request.getExtensions()) |
| { |
| if (!extensionRegistry.isAvailable(reqExt.getName())) |
| { |
| throw new IllegalArgumentException("Requested extension [" + reqExt.getName() + "] is not installed"); |
| } |
| } |
| |
| if (LOG.isDebugEnabled()) |
| LOG.debug("connect websocket {} to {}",websocket,toUri); |
| |
| init(); |
| |
| WebSocketUpgradeRequest wsReq = new WebSocketUpgradeRequest(httpClient,request); |
| wsReq.setUpgradeListener(upgradeListener); |
| return wsReq.sendAsync(); |
| } |
| |
| private void warnOnReplacment(ContainerLifeCycle container, Class<?> beanClass) |
| { |
| Object bean = container.getBean(beanClass); |
| if (bean != null) |
| { |
| LOG.warn("Replacing existing Bean {} in {}",bean,container); |
| } |
| } |
| |
| @Override |
| protected void doStart() throws Exception |
| { |
| warnOnReplacment(this,HttpClient.class); |
| warnOnReplacment(httpClient,WebSocketClient.class); |
| |
| if (this.syntheticHttpClient) |
| { |
| addManaged(this.httpClient); |
| } |
| else |
| { |
| addBean(this.httpClient); |
| } |
| this.httpClient.addBean(this); |
| |
| super.doStart(); |
| } |
| |
| @Override |
| protected void doStop() throws Exception |
| { |
| if (ShutdownThread.isRegistered(this)) |
| { |
| ShutdownThread.deregister(this); |
| } |
| |
| super.doStop(); |
| |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Stopped {}",this); |
| } |
| |
| @Deprecated |
| public boolean isDispatchIO() |
| { |
| return httpClient.isDispatchIO(); |
| } |
| |
| /** |
| * Return the number of milliseconds for a timeout of an attempted write operation. |
| * |
| * @return number of milliseconds for timeout of an attempted write operation |
| */ |
| public long getAsyncWriteTimeout() |
| { |
| return this.policy.getAsyncWriteTimeout(); |
| } |
| |
| public SocketAddress getBindAddress() |
| { |
| return httpClient.getBindAddress(); |
| } |
| |
| public ByteBufferPool getBufferPool() |
| { |
| return httpClient.getByteBufferPool(); |
| } |
| |
| @Deprecated |
| public ConnectionManager getConnectionManager() |
| { |
| throw new UnsupportedOperationException("ConnectionManager is no longer supported"); |
| } |
| |
| public long getConnectTimeout() |
| { |
| return httpClient.getConnectTimeout(); |
| } |
| |
| public CookieStore getCookieStore() |
| { |
| return httpClient.getCookieStore(); |
| } |
| |
| public EventDriverFactory getEventDriverFactory() |
| { |
| return eventDriverFactory; |
| } |
| |
| public Executor getExecutor() |
| { |
| return httpClient.getExecutor(); |
| } |
| |
| public ExtensionFactory getExtensionFactory() |
| { |
| return extensionRegistry; |
| } |
| |
| public Masker getMasker() |
| { |
| return masker; |
| } |
| |
| /** |
| * Get the maximum size for buffering of a binary message. |
| * |
| * @return the maximum size of a binary message buffer. |
| */ |
| public int getMaxBinaryMessageBufferSize() |
| { |
| return this.policy.getMaxBinaryMessageBufferSize(); |
| } |
| |
| /** |
| * Get the maximum size for a binary message. |
| * |
| * @return the maximum size of a binary message. |
| */ |
| public long getMaxBinaryMessageSize() |
| { |
| return this.policy.getMaxBinaryMessageSize(); |
| } |
| |
| /** |
| * Get the max idle timeout for new connections. |
| * |
| * @return the max idle timeout in milliseconds for new connections. |
| */ |
| public long getMaxIdleTimeout() |
| { |
| return this.policy.getIdleTimeout(); |
| } |
| |
| /** |
| * Get the maximum size for buffering of a text message. |
| * |
| * @return the maximum size of a text message buffer. |
| */ |
| public int getMaxTextMessageBufferSize() |
| { |
| return this.policy.getMaxTextMessageBufferSize(); |
| } |
| |
| /** |
| * Get the maximum size for a text message. |
| * |
| * @return the maximum size of a text message. |
| */ |
| public long getMaxTextMessageSize() |
| { |
| return this.policy.getMaxTextMessageSize(); |
| } |
| |
| @Override |
| public DecoratedObjectFactory getObjectFactory() |
| { |
| return this.objectFactory; |
| } |
| |
| public Set<WebSocketSession> getOpenSessions() |
| { |
| return Collections.unmodifiableSet(new HashSet<>(getBeans(WebSocketSession.class))); |
| } |
| |
| public WebSocketPolicy getPolicy() |
| { |
| return this.policy; |
| } |
| |
| public Scheduler getScheduler() |
| { |
| return httpClient.getScheduler(); |
| } |
| |
| public SessionFactory getSessionFactory() |
| { |
| return sessionFactory; |
| } |
| |
| /** |
| * @return the {@link SslContextFactory} that manages TLS encryption |
| * @see #WebSocketClient(SslContextFactory) |
| */ |
| public SslContextFactory getSslContextFactory() |
| { |
| return httpClient.getSslContextFactory(); |
| } |
| |
| private synchronized void init() throws IOException |
| { |
| if (!ShutdownThread.isRegistered(this)) |
| { |
| ShutdownThread.register(this); |
| } |
| |
| // TODO: reevaluate |
| // if (executor == null) |
| // { |
| // QueuedThreadPool threadPool = new QueuedThreadPool(); |
| // String name = WebSocketClient.class.getSimpleName() + "@" + hashCode(); |
| // threadPool.setName(name); |
| // threadPool.setDaemon(daemon); |
| // executor = threadPool; |
| // addManaged(threadPool); |
| // } |
| // else |
| // { |
| // addBean(executor,false); |
| // } |
| } |
| |
| /** |
| * Factory method for new ConnectionManager (used by other projects like cometd) |
| * |
| * @return the ConnectionManager instance to use |
| * @deprecated use HttpClient instead |
| */ |
| @Deprecated |
| protected ConnectionManager newConnectionManager() |
| { |
| throw new UnsupportedOperationException("ConnectionManager is no longer supported"); |
| } |
| |
| @Override |
| public void onSessionClosed(WebSocketSession session) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Session Closed: {}",session); |
| removeBean(session); |
| } |
| |
| @Override |
| public void onSessionOpened(WebSocketSession session) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Session Opened: {}",session); |
| addManaged(session); |
| } |
| |
| public void setAsyncWriteTimeout(long ms) |
| { |
| this.policy.setAsyncWriteTimeout(ms); |
| } |
| |
| /** |
| * @param bindAddress |
| * the address to bind to |
| * @deprecated use {@link #setBindAddress(SocketAddress)} instead |
| */ |
| @Deprecated |
| public void setBindAdddress(SocketAddress bindAddress) |
| { |
| setBindAddress(bindAddress); |
| } |
| |
| public void setBindAddress(SocketAddress bindAddress) |
| { |
| this.httpClient.setBindAddress(bindAddress); |
| } |
| |
| public void setBufferPool(ByteBufferPool bufferPool) |
| { |
| this.httpClient.setByteBufferPool(bufferPool); |
| } |
| |
| /** |
| * Set the timeout for connecting to the remote server. |
| * |
| * @param ms |
| * the timeout in milliseconds |
| */ |
| public void setConnectTimeout(long ms) |
| { |
| this.httpClient.setConnectTimeout(ms); |
| } |
| |
| public void setCookieStore(CookieStore cookieStore) |
| { |
| this.httpClient.setCookieStore(cookieStore); |
| } |
| |
| public void setDaemon(boolean daemon) |
| { |
| // do nothing |
| } |
| |
| @Deprecated |
| public void setDispatchIO(boolean dispatchIO) |
| { |
| this.httpClient.setDispatchIO(dispatchIO); |
| } |
| |
| public void setExecutor(Executor executor) |
| { |
| this.httpClient.setExecutor(executor); |
| } |
| |
| public void setMasker(Masker masker) |
| { |
| this.masker = masker; |
| } |
| |
| public void setMaxBinaryMessageBufferSize(int max) |
| { |
| this.policy.setMaxBinaryMessageBufferSize(max); |
| } |
| |
| /** |
| * Set the max idle timeout for new connections. |
| * <p> |
| * Existing connections will not have their max idle timeout adjusted. |
| * |
| * @param ms |
| * the timeout in milliseconds |
| */ |
| public void setMaxIdleTimeout(long ms) |
| { |
| this.policy.setIdleTimeout(ms); |
| } |
| |
| public void setMaxTextMessageBufferSize(int max) |
| { |
| this.policy.setMaxTextMessageBufferSize(max); |
| } |
| |
| @Override |
| public void dump(Appendable out, String indent) throws IOException |
| { |
| dumpThis(out); |
| dump(out,indent,getOpenSessions()); |
| } |
| |
| // TODO |
| public HttpClient getHttpClient() |
| { |
| return this.httpClient; |
| } |
| } |