org.eclipse.jetty.io.ClientConnector Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.io;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProtocolFamily;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketOption;
import java.net.StandardProtocolFamily;
import java.net.StandardSocketOptions;
import java.nio.channels.NetworkChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.JavaVersion;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The client-side component that connects to server sockets.
* ClientConnector delegates the handling of {@link SocketChannel}s
* to a {@link SelectorManager}, and centralizes the configuration of
* necessary components such as the executor, the scheduler, etc.
* ClientConnector offers a low-level API that can be used to
* connect {@link SocketChannel}s to listening servers via the
* {@link #connect(SocketAddress, Map)} method.
* However, a ClientConnector instance is typically just configured
* and then passed to an HttpClient transport, so that applications
* can use high-level APIs to make HTTP requests to servers:
*
* // Create a ClientConnector instance.
* ClientConnector connector = new ClientConnector();
*
* // Configure the ClientConnector.
* connector.setSelectors(1);
* connector.setSslContextFactory(new SslContextFactory.Client());
*
* // Pass it to the HttpClient transport.
* HttpClientTransport transport = new HttpClientTransportDynamic(connector);
* HttpClient httpClient = new HttpClient(transport);
* httpClient.start();
*
*/
@ManagedObject
public class ClientConnector extends ContainerLifeCycle
{
public static final String CLIENT_CONNECTOR_CONTEXT_KEY = "org.eclipse.jetty.client.connector";
public static final String REMOTE_SOCKET_ADDRESS_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".remoteSocketAddress";
public static final String CLIENT_CONNECTION_FACTORY_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".clientConnectionFactory";
public static final String CONNECTION_PROMISE_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".connectionPromise";
public static final String APPLICATION_PROTOCOLS_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".applicationProtocols";
private static final Logger LOG = LoggerFactory.getLogger(ClientConnector.class);
/**
* Creates a ClientConnector configured to connect via Unix-Domain sockets to the given Unix-Domain path
*
* @param path the Unix-Domain path to connect to
* @return a ClientConnector that connects to the given Unix-Domain path
*/
public static ClientConnector forUnixDomain(Path path)
{
return new ClientConnector(Configurator.forUnixDomain(path));
}
private final Configurator configurator;
private Executor executor;
private Scheduler scheduler;
private ByteBufferPool byteBufferPool;
private SslContextFactory.Client sslContextFactory;
private SelectorManager selectorManager;
private int selectors = 1;
private boolean connectBlocking;
private Duration connectTimeout = Duration.ofSeconds(5);
private Duration idleTimeout = Duration.ofSeconds(30);
private SocketAddress bindAddress;
private boolean tcpNoDelay = true;
private boolean reuseAddress = true;
private boolean reusePort;
private int receiveBufferSize = -1;
private int sendBufferSize = -1;
public ClientConnector()
{
this(new Configurator());
}
public ClientConnector(Configurator configurator)
{
this.configurator = Objects.requireNonNull(configurator);
addBean(configurator);
configurator.addBean(this, false);
}
/**
* @param address the SocketAddress to connect to
* @return whether the connection to the given SocketAddress is intrinsically secure
* @see Configurator#isIntrinsicallySecure(ClientConnector, SocketAddress)
*/
public boolean isIntrinsicallySecure(SocketAddress address)
{
return configurator.isIntrinsicallySecure(this, address);
}
public Executor getExecutor()
{
return executor;
}
public void setExecutor(Executor executor)
{
if (isStarted())
throw new IllegalStateException();
updateBean(this.executor, executor);
this.executor = executor;
}
public Scheduler getScheduler()
{
return scheduler;
}
public void setScheduler(Scheduler scheduler)
{
if (isStarted())
throw new IllegalStateException();
updateBean(this.scheduler, scheduler);
this.scheduler = scheduler;
}
public ByteBufferPool getByteBufferPool()
{
return byteBufferPool;
}
public void setByteBufferPool(ByteBufferPool byteBufferPool)
{
if (isStarted())
throw new IllegalStateException();
updateBean(this.byteBufferPool, byteBufferPool);
this.byteBufferPool = byteBufferPool;
}
public SslContextFactory.Client getSslContextFactory()
{
return sslContextFactory;
}
public void setSslContextFactory(SslContextFactory.Client sslContextFactory)
{
if (isStarted())
throw new IllegalStateException();
updateBean(this.sslContextFactory, sslContextFactory);
this.sslContextFactory = sslContextFactory;
}
/**
* @return the number of NIO selectors
*/
@ManagedAttribute("The number of NIO selectors")
public int getSelectors()
{
return selectors;
}
public void setSelectors(int selectors)
{
if (isStarted())
throw new IllegalStateException();
this.selectors = selectors;
}
/**
* @return whether {@link #connect(SocketAddress, Map)} operations are performed in blocking mode
*/
@ManagedAttribute("Whether connect operations are performed in blocking mode")
public boolean isConnectBlocking()
{
return connectBlocking;
}
public void setConnectBlocking(boolean connectBlocking)
{
this.connectBlocking = connectBlocking;
}
/**
* @return the timeout of {@link #connect(SocketAddress, Map)} operations
*/
@ManagedAttribute("The timeout of connect operations")
public Duration getConnectTimeout()
{
return connectTimeout;
}
public void setConnectTimeout(Duration connectTimeout)
{
this.connectTimeout = connectTimeout;
if (selectorManager != null)
selectorManager.setConnectTimeout(connectTimeout.toMillis());
}
/**
* @return the max duration for which a connection can be idle (that is, without traffic of bytes in either direction)
*/
@ManagedAttribute("The duration for which a connection can be idle")
public Duration getIdleTimeout()
{
return idleTimeout;
}
public void setIdleTimeout(Duration idleTimeout)
{
this.idleTimeout = idleTimeout;
}
/**
* @return the address to bind a socket to before the connect operation
*/
@ManagedAttribute("The socket address to bind sockets to before the connect operation")
public SocketAddress getBindAddress()
{
return bindAddress;
}
/**
* Sets the bind address of sockets before the connect operation.
* In multi-homed hosts, you may want to connect from a specific address:
*
* clientConnector.setBindAddress(new InetSocketAddress("127.0.0.2", 0));
*
* Note the use of the port {@code 0} to indicate that a different ephemeral port
* should be used for each different connection.
* In the rare cases where you want to use the same port for all connections,
* you must also call {@link #setReusePort(boolean) setReusePort(true)}.
*
* @param bindAddress the socket address to bind to before the connect operation
*/
public void setBindAddress(SocketAddress bindAddress)
{
this.bindAddress = bindAddress;
}
/**
* @return whether small TCP packets are sent without delay
*/
@ManagedAttribute("Whether small TCP packets are sent without delay")
public boolean isTCPNoDelay()
{
return tcpNoDelay;
}
public void setTCPNoDelay(boolean tcpNoDelay)
{
this.tcpNoDelay = tcpNoDelay;
}
/**
* @return whether rebinding is allowed with sockets in tear-down states
*/
@ManagedAttribute("Whether rebinding is allowed with sockets in tear-down states")
public boolean getReuseAddress()
{
return reuseAddress;
}
/**
* Sets whether it is allowed to bind a socket to a socket address
* that may be in use by another socket in tear-down state, for example
* in TIME_WAIT state.
* This is useful when ClientConnector is restarted: an existing connection
* may still be using a network address (same host and same port) that is also
* chosen for a new connection.
*
* @param reuseAddress whether rebinding is allowed with sockets in tear-down states
* @see #setReusePort(boolean)
*/
public void setReuseAddress(boolean reuseAddress)
{
this.reuseAddress = reuseAddress;
}
/**
* @return whether binding to same host and port is allowed
*/
@ManagedAttribute("Whether binding to same host and port is allowed")
public boolean isReusePort()
{
return reusePort;
}
/**
* Sets whether it is allowed to bind multiple sockets to the same
* socket address (same host and same port).
*
* @param reusePort whether binding to same host and port is allowed
*/
public void setReusePort(boolean reusePort)
{
this.reusePort = reusePort;
}
/**
* @return the receive buffer size in bytes, or -1 for the default value
*/
@ManagedAttribute("The receive buffer size in bytes")
public int getReceiveBufferSize()
{
return receiveBufferSize;
}
public void setReceiveBufferSize(int receiveBufferSize)
{
this.receiveBufferSize = receiveBufferSize;
}
/**
* @return the send buffer size in bytes, or -1 for the default value
*/
@ManagedAttribute("The send buffer size in bytes")
public int getSendBufferSize()
{
return sendBufferSize;
}
public void setSendBufferSize(int sendBufferSize)
{
this.sendBufferSize = sendBufferSize;
}
@Override
protected void doStart() throws Exception
{
if (executor == null)
{
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName(String.format("client-pool@%x", hashCode()));
setExecutor(clientThreads);
}
if (scheduler == null)
setScheduler(new ScheduledExecutorScheduler(String.format("client-scheduler@%x", hashCode()), false));
if (byteBufferPool == null)
setByteBufferPool(new MappedByteBufferPool());
if (sslContextFactory == null)
setSslContextFactory(newSslContextFactory());
selectorManager = newSelectorManager();
selectorManager.setConnectTimeout(getConnectTimeout().toMillis());
addBean(selectorManager);
super.doStart();
}
@Override
protected void doStop() throws Exception
{
super.doStop();
removeBean(selectorManager);
}
protected SslContextFactory.Client newSslContextFactory()
{
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(false);
sslContextFactory.setEndpointIdentificationAlgorithm("HTTPS");
return sslContextFactory;
}
protected SelectorManager newSelectorManager()
{
return new ClientSelectorManager(getExecutor(), getScheduler(), getSelectors());
}
public void connect(SocketAddress address, Map context)
{
SelectableChannel channel = null;
try
{
if (context == null)
context = new ConcurrentHashMap<>();
context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, this);
context.putIfAbsent(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address);
Configurator.ChannelWithAddress channelWithAddress = configurator.newChannelWithAddress(this, address, context);
channel = channelWithAddress.getSelectableChannel();
address = channelWithAddress.getSocketAddress();
configure(channel);
SocketAddress bindAddress = getBindAddress();
if (bindAddress != null && channel instanceof NetworkChannel)
bind((NetworkChannel)channel, bindAddress);
boolean connected = true;
if (channel instanceof SocketChannel)
{
SocketChannel socketChannel = (SocketChannel)channel;
boolean blocking = isConnectBlocking() && address instanceof InetSocketAddress;
if (LOG.isDebugEnabled())
LOG.debug("Connecting {} to {}", blocking ? "blocking" : "non-blocking", address);
if (blocking)
{
socketChannel.socket().connect(address, (int)getConnectTimeout().toMillis());
socketChannel.configureBlocking(false);
}
else
{
socketChannel.configureBlocking(false);
connected = socketChannel.connect(address);
}
}
else
{
channel.configureBlocking(false);
}
if (connected)
selectorManager.accept(channel, context);
else
selectorManager.connect(channel, context);
}
// Must catch all exceptions, since some like
// UnresolvedAddressException are not IOExceptions.
catch (Throwable x)
{
// If IPv6 is not deployed, a generic SocketException "Network is unreachable"
// exception is being thrown, so we attempt to provide a better error message.
if (x.getClass() == SocketException.class)
x = new SocketException("Could not connect to " + address).initCause(x);
IO.close(channel);
connectFailed(x, context);
}
}
public void accept(SelectableChannel selectable, Map context)
{
try
{
SocketChannel channel = (SocketChannel)selectable;
context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, this);
if (!channel.isConnected())
throw new IllegalStateException("SocketChannel must be connected");
configure(channel);
channel.configureBlocking(false);
selectorManager.accept(channel, context);
}
catch (Throwable failure)
{
if (LOG.isDebugEnabled())
LOG.debug("Could not accept {}", selectable);
IO.close(selectable);
Promise> promise = (Promise>)context.get(CONNECTION_PROMISE_CONTEXT_KEY);
if (promise != null)
promise.failed(failure);
}
}
private void bind(NetworkChannel channel, SocketAddress bindAddress) throws IOException
{
if (LOG.isDebugEnabled())
LOG.debug("Binding {} to {}", channel, bindAddress);
channel.bind(bindAddress);
}
protected void configure(SelectableChannel selectable) throws IOException
{
if (selectable instanceof NetworkChannel)
{
NetworkChannel channel = (NetworkChannel)selectable;
setSocketOption(channel, StandardSocketOptions.TCP_NODELAY, isTCPNoDelay());
setSocketOption(channel, StandardSocketOptions.SO_REUSEADDR, getReuseAddress());
setSocketOption(channel, StandardSocketOptions.SO_REUSEPORT, isReusePort());
int receiveBufferSize = getReceiveBufferSize();
if (receiveBufferSize >= 0)
setSocketOption(channel, StandardSocketOptions.SO_RCVBUF, receiveBufferSize);
int sendBufferSize = getSendBufferSize();
if (sendBufferSize >= 0)
setSocketOption(channel, StandardSocketOptions.SO_SNDBUF, sendBufferSize);
}
}
private void setSocketOption(NetworkChannel channel, SocketOption option, T value)
{
try
{
channel.setOption(option, value);
}
catch (Throwable x)
{
if (LOG.isTraceEnabled())
LOG.trace("Could not configure {} to {} on {}", option, value, channel, x);
}
}
protected EndPoint newEndPoint(SelectableChannel selectable, ManagedSelector selector, SelectionKey selectionKey)
{
@SuppressWarnings("unchecked")
Map context = (Map)selectionKey.attachment();
SocketAddress address = (SocketAddress)context.get(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY);
return configurator.newEndPoint(this, address, selectable, selector, selectionKey);
}
protected Connection newConnection(EndPoint endPoint, Map context) throws IOException
{
SocketAddress address = (SocketAddress)context.get(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY);
return configurator.newConnection(this, address, endPoint, context);
}
protected void connectFailed(Throwable failure, Map context)
{
if (LOG.isDebugEnabled())
LOG.debug("Could not connect to {}", context.get(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY));
Promise> promise = (Promise>)context.get(CONNECTION_PROMISE_CONTEXT_KEY);
if (promise != null)
promise.failed(failure);
}
protected class ClientSelectorManager extends SelectorManager
{
public ClientSelectorManager(Executor executor, Scheduler scheduler, int selectors)
{
super(executor, scheduler, selectors);
}
@Override
protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey)
{
EndPoint endPoint = ClientConnector.this.newEndPoint(channel, selector, selectionKey);
endPoint.setIdleTimeout(getIdleTimeout().toMillis());
return endPoint;
}
@Override
public Connection newConnection(SelectableChannel channel, EndPoint endPoint, Object attachment) throws IOException
{
@SuppressWarnings("unchecked")
Map context = (Map)attachment;
return ClientConnector.this.newConnection(endPoint, context);
}
@Override
public void connectionOpened(Connection connection, Object context)
{
super.connectionOpened(connection, context);
// TODO: the block below should be moved to Connection.onOpen() in each implementation,
// so that each implementation can decide when to notify the promise, possibly not in onOpen().
@SuppressWarnings("unchecked")
Map contextMap = (Map)context;
@SuppressWarnings("unchecked")
Promise promise = (Promise)contextMap.get(CONNECTION_PROMISE_CONTEXT_KEY);
if (promise != null)
promise.succeeded(connection);
}
@Override
protected void connectionFailed(SelectableChannel channel, Throwable failure, Object attachment)
{
@SuppressWarnings("unchecked")
Map context = (Map)attachment;
connectFailed(failure, context);
}
}
/**
* Configures a {@link ClientConnector}.
*/
public static class Configurator extends ContainerLifeCycle
{
/**
* Returns whether the connection to a given {@link SocketAddress} is intrinsically secure.
* A protocol such as HTTP/1.1 can be transported by TCP; however, TCP is not secure because
* it does not offer any encryption.
* Encryption is provided by using TLS to wrap the HTTP/1.1 bytes, and then transporting the
* TLS bytes over TCP.
* On the other hand, protocols such as QUIC are intrinsically secure, and therefore it is
* not necessary to wrap the HTTP/1.1 bytes with TLS: the HTTP/1.1 bytes are transported over
* QUIC in an intrinsically secure way.
*
* @param clientConnector the ClientConnector
* @param address the SocketAddress to connect to
* @return whether the connection to the given SocketAddress is intrinsically secure
*/
public boolean isIntrinsicallySecure(ClientConnector clientConnector, SocketAddress address)
{
return false;
}
/**
* Creates a new {@link SocketChannel} to connect to a {@link SocketAddress}
* derived from the input socket address.
* The input socket address represents the destination socket address to
* connect to, as it is typically specified by a URI authority, for example
* {@code localhost:8080} if the URI is {@code http://localhost:8080/path}.
* However, the returned socket address may be different as the implementation
* may use a Unix-Domain socket address to physically connect to the virtual
* destination socket address given as input.
* The return type is a pair/record holding the socket channel and the
* socket address, with the socket channel not yet connected.
* The implementation of this methods must not call
* {@link SocketChannel#connect(SocketAddress)}, as this is done later,
* after configuring the socket, by the {@link ClientConnector} implementation.
*
* @param clientConnector the client connector requesting channel with associated address
* @param address the destination socket address, typically specified in a URI
* @param context the context to create the new socket channel
* @return a new {@link SocketChannel} with an associated {@link SocketAddress} to connect to
* @throws IOException if the socket channel or the socket address cannot be created
*/
public ChannelWithAddress newChannelWithAddress(ClientConnector clientConnector, SocketAddress address, Map context) throws IOException
{
return new ChannelWithAddress(SocketChannel.open(), address);
}
public EndPoint newEndPoint(ClientConnector clientConnector, SocketAddress address, SelectableChannel selectable, ManagedSelector selector, SelectionKey selectionKey)
{
return new SocketChannelEndPoint((SocketChannel)selectable, selector, selectionKey, clientConnector.getScheduler());
}
public Connection newConnection(ClientConnector clientConnector, SocketAddress address, EndPoint endPoint, Map context) throws IOException
{
ClientConnectionFactory factory = (ClientConnectionFactory)context.get(CLIENT_CONNECTION_FACTORY_CONTEXT_KEY);
return factory.newConnection(endPoint, context);
}
/**
* A pair/record holding a {@link SelectableChannel} and a {@link SocketAddress} to connect to.
*/
public static class ChannelWithAddress
{
private final SelectableChannel channel;
private final SocketAddress address;
public ChannelWithAddress(SelectableChannel channel, SocketAddress address)
{
this.channel = channel;
this.address = address;
}
public SelectableChannel getSelectableChannel()
{
return channel;
}
public SocketAddress getSocketAddress()
{
return address;
}
}
private static Configurator forUnixDomain(Path path)
{
return new Configurator()
{
@Override
public ChannelWithAddress newChannelWithAddress(ClientConnector clientConnector, SocketAddress address, Map context)
{
try
{
ProtocolFamily family = Enum.valueOf(StandardProtocolFamily.class, "UNIX");
SocketChannel socketChannel = (SocketChannel)SocketChannel.class.getMethod("open", ProtocolFamily.class).invoke(null, family);
Class> addressClass = Class.forName("java.net.UnixDomainSocketAddress");
SocketAddress socketAddress = (SocketAddress)addressClass.getMethod("of", Path.class).invoke(null, path);
return new ChannelWithAddress(socketChannel, socketAddress);
}
catch (Throwable x)
{
String message = "Unix-Domain SocketChannels are available starting from Java 16, your Java version is: " + JavaVersion.VERSION;
throw new UnsupportedOperationException(message, x);
}
}
};
}
}
}