org.eclipse.jetty.http2.client.HTTP2Client Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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.http2.client;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory;
import org.eclipse.jetty.http2.BufferingFlowControlStrategy;
import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.hpack.HpackContext;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ManagedSelector;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.io.SelectorManager;
import org.eclipse.jetty.io.SocketChannelEndPoint;
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
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;
/**
* {@link HTTP2Client} provides an asynchronous, non-blocking implementation
* to send HTTP/2 frames to a server.
* Typical usage:
*
* // Create and start HTTP2Client.
* HTTP2Client client = new HTTP2Client();
* SslContextFactory sslContextFactory = new SslContextFactory();
* client.addBean(sslContextFactory);
* client.start();
*
* // Connect to host.
* String host = "webtide.com";
* int port = 443;
*
* FuturePromise<Session> sessionPromise = new FuturePromise<>();
* client.connect(sslContextFactory, new InetSocketAddress(host, port), new ServerSessionListener.Adapter(), sessionPromise);
*
* // Obtain the client Session object.
* Session session = sessionPromise.get(5, TimeUnit.SECONDS);
*
* // Prepare the HTTP request headers.
* HttpFields requestFields = new HttpFields();
* requestFields.put("User-Agent", client.getClass().getName() + "/" + Jetty.VERSION);
* // Prepare the HTTP request object.
* MetaData.Request request = new MetaData.Request("PUT", new HttpURI("https://" + host + ":" + port + "/"), HttpVersion.HTTP_2, requestFields);
* // Create the HTTP/2 HEADERS frame representing the HTTP request.
* HeadersFrame headersFrame = new HeadersFrame(request, null, false);
*
* // Prepare the listener to receive the HTTP response frames.
* Stream.Listener responseListener = new new Stream.Listener.Adapter()
* {
* @Override
* public void onHeaders(Stream stream, HeadersFrame frame)
* {
* System.err.println(frame);
* }
*
* @Override
* public void onData(Stream stream, DataFrame frame, Callback callback)
* {
* System.err.println(frame);
* callback.succeeded();
* }
* };
*
* // Send the HEADERS frame to create a stream.
* FuturePromise<Stream> streamPromise = new FuturePromise<>();
* session.newStream(headersFrame, streamPromise, responseListener);
* Stream stream = streamPromise.get(5, TimeUnit.SECONDS);
*
* // Use the Stream object to send request content, if any, using a DATA frame.
* ByteBuffer content = ...;
* DataFrame requestContent = new DataFrame(stream.getId(), content, true);
* stream.data(requestContent, Callback.Adapter.INSTANCE);
*
* // When done, stop the client.
* client.stop();
*
*/
@ManagedObject
public class HTTP2Client extends ContainerLifeCycle
{
private Executor executor;
private Scheduler scheduler;
private ByteBufferPool bufferPool;
private ClientConnectionFactory connectionFactory;
private SelectorManager selector;
private int selectors = 1;
private long idleTimeout = 30000;
private long connectTimeout = 10000;
private long streamIdleTimeout;
private boolean connectBlocking;
private SocketAddress bindAddress;
private boolean tcpNoDelay = true;
private int inputBufferSize = 8192;
private List protocols = Arrays.asList("h2", "h2-17", "h2-16", "h2-15", "h2-14");
private int initialSessionRecvWindow = 16 * 1024 * 1024;
private int initialStreamRecvWindow = 8 * 1024 * 1024;
private int maxFrameSize = Frame.DEFAULT_MAX_LENGTH;
private int maxConcurrentPushedStreams = 32;
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private int maxDecoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int maxEncoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int maxHeaderBlockFragment = 0;
private int maxResponseHeadersSize = 8 * 1024;
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
@Override
protected void doStart() throws Exception
{
if (executor == null)
setExecutor(new QueuedThreadPool());
if (scheduler == null)
setScheduler(new ScheduledExecutorScheduler());
if (bufferPool == null)
setByteBufferPool(new MappedByteBufferPool());
if (connectionFactory == null)
{
HTTP2ClientConnectionFactory h2 = new HTTP2ClientConnectionFactory();
setClientConnectionFactory((endPoint, context) ->
{
ClientConnectionFactory factory = h2;
SslContextFactory sslContextFactory = (SslContextFactory)context.get(SslClientConnectionFactory.SSL_CONTEXT_FACTORY_CONTEXT_KEY);
if (sslContextFactory != null)
{
ALPNClientConnectionFactory alpn = new ALPNClientConnectionFactory(getExecutor(), h2, getProtocols());
factory = newSslClientConnectionFactory(sslContextFactory, alpn);
}
return factory.newConnection(endPoint, context);
});
}
if (selector == null)
{
selector = newSelectorManager();
addBean(selector);
}
selector.setConnectTimeout(getConnectTimeout());
super.doStart();
}
protected SelectorManager newSelectorManager()
{
return new ClientSelectorManager(getExecutor(), getScheduler(), getSelectors());
}
protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory sslContextFactory, ClientConnectionFactory connectionFactory)
{
return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory);
}
public Executor getExecutor()
{
return executor;
}
public void setExecutor(Executor executor)
{
this.updateBean(this.executor, executor);
this.executor = executor;
}
public Scheduler getScheduler()
{
return scheduler;
}
public void setScheduler(Scheduler scheduler)
{
this.updateBean(this.scheduler, scheduler);
this.scheduler = scheduler;
}
public ByteBufferPool getByteBufferPool()
{
return bufferPool;
}
public void setByteBufferPool(ByteBufferPool bufferPool)
{
this.updateBean(this.bufferPool, bufferPool);
this.bufferPool = bufferPool;
}
public ClientConnectionFactory getClientConnectionFactory()
{
return connectionFactory;
}
public void setClientConnectionFactory(ClientConnectionFactory connectionFactory)
{
this.updateBean(this.connectionFactory, connectionFactory);
this.connectionFactory = connectionFactory;
}
public FlowControlStrategy.Factory getFlowControlStrategyFactory()
{
return flowControlStrategyFactory;
}
public void setFlowControlStrategyFactory(FlowControlStrategy.Factory flowControlStrategyFactory)
{
this.flowControlStrategyFactory = flowControlStrategyFactory;
}
@ManagedAttribute("The number of selectors")
public int getSelectors()
{
return selectors;
}
public void setSelectors(int selectors)
{
this.selectors = selectors;
}
@ManagedAttribute("The idle timeout in milliseconds")
public long getIdleTimeout()
{
return idleTimeout;
}
public void setIdleTimeout(long idleTimeout)
{
this.idleTimeout = idleTimeout;
}
@ManagedAttribute("The stream idle timeout in milliseconds")
public long getStreamIdleTimeout()
{
return streamIdleTimeout;
}
public void setStreamIdleTimeout(long streamIdleTimeout)
{
this.streamIdleTimeout = streamIdleTimeout;
}
@ManagedAttribute("The connect timeout in milliseconds")
public long getConnectTimeout()
{
return connectTimeout;
}
public void setConnectTimeout(long connectTimeout)
{
this.connectTimeout = connectTimeout;
SelectorManager selector = this.selector;
if (selector != null)
selector.setConnectTimeout(connectTimeout);
}
@ManagedAttribute("Whether the connect() operation is blocking")
public boolean isConnectBlocking()
{
return connectBlocking;
}
public void setConnectBlocking(boolean connectBlocking)
{
this.connectBlocking = connectBlocking;
}
public SocketAddress getBindAddress()
{
return bindAddress;
}
public void setBindAddress(SocketAddress bindAddress)
{
this.bindAddress = bindAddress;
}
public boolean isTCPNoDelay()
{
return tcpNoDelay;
}
public void setTCPNoDelay(boolean tcpNoDelay)
{
this.tcpNoDelay = tcpNoDelay;
}
@ManagedAttribute("The size of the buffer used to read from the network")
public int getInputBufferSize()
{
return inputBufferSize;
}
public void setInputBufferSize(int inputBufferSize)
{
this.inputBufferSize = inputBufferSize;
}
@ManagedAttribute("The ALPN protocol list")
public List getProtocols()
{
return protocols;
}
public void setProtocols(List protocols)
{
this.protocols = protocols;
}
@ManagedAttribute("The initial size of session's flow control receive window")
public int getInitialSessionRecvWindow()
{
return initialSessionRecvWindow;
}
public void setInitialSessionRecvWindow(int initialSessionRecvWindow)
{
this.initialSessionRecvWindow = initialSessionRecvWindow;
}
@ManagedAttribute("The initial size of stream's flow control receive window")
public int getInitialStreamRecvWindow()
{
return initialStreamRecvWindow;
}
public void setInitialStreamRecvWindow(int initialStreamRecvWindow)
{
this.initialStreamRecvWindow = initialStreamRecvWindow;
}
@Deprecated
@ManagedAttribute("The max frame length in bytes")
public int getMaxFrameLength()
{
return getMaxFrameSize();
}
@Deprecated
public void setMaxFrameLength(int maxFrameLength)
{
setMaxFrameSize(maxFrameLength);
}
@ManagedAttribute("The max frame size in bytes")
public int getMaxFrameSize()
{
return maxFrameSize;
}
public void setMaxFrameSize(int maxFrameSize)
{
this.maxFrameSize = maxFrameSize;
}
@ManagedAttribute("The max number of concurrent pushed streams")
public int getMaxConcurrentPushedStreams()
{
return maxConcurrentPushedStreams;
}
public void setMaxConcurrentPushedStreams(int maxConcurrentPushedStreams)
{
this.maxConcurrentPushedStreams = maxConcurrentPushedStreams;
}
@ManagedAttribute("The max number of keys in all SETTINGS frames")
public int getMaxSettingsKeys()
{
return maxSettingsKeys;
}
public void setMaxSettingsKeys(int maxSettingsKeys)
{
this.maxSettingsKeys = maxSettingsKeys;
}
@ManagedAttribute("The HPACK encoder dynamic table maximum capacity")
public int getMaxEncoderTableCapacity()
{
return maxEncoderTableCapacity;
}
/**
* Sets the limit for the encoder HPACK dynamic table capacity.
* Setting this value to {@code 0} disables the use of the dynamic table.
*
* @param maxEncoderTableCapacity The HPACK encoder dynamic table maximum capacity
*/
public void setMaxEncoderTableCapacity(int maxEncoderTableCapacity)
{
this.maxEncoderTableCapacity = maxEncoderTableCapacity;
}
@ManagedAttribute("The HPACK decoder dynamic table maximum capacity")
public int getMaxDecoderTableCapacity()
{
return maxDecoderTableCapacity;
}
public void setMaxDecoderTableCapacity(int maxDecoderTableCapacity)
{
this.maxDecoderTableCapacity = maxDecoderTableCapacity;
}
@Deprecated
@ManagedAttribute("The HPACK dynamic table maximum size")
public int getMaxDynamicTableSize()
{
return getMaxDecoderTableCapacity();
}
@Deprecated
public void setMaxDynamicTableSize(int maxDynamicTableSize)
{
setMaxDecoderTableCapacity(maxDynamicTableSize);
}
@ManagedAttribute("The max size of header block fragments")
public int getMaxHeaderBlockFragment()
{
return maxHeaderBlockFragment;
}
public void setMaxHeaderBlockFragment(int maxHeaderBlockFragment)
{
this.maxHeaderBlockFragment = maxHeaderBlockFragment;
}
@ManagedAttribute("The max size of response headers")
public int getMaxResponseHeadersSize()
{
return maxResponseHeadersSize;
}
public void setMaxResponseHeadersSize(int maxResponseHeadersSize)
{
this.maxResponseHeadersSize = maxResponseHeadersSize;
}
public void connect(InetSocketAddress address, Session.Listener listener, Promise promise)
{
connect(null, address, listener, promise);
}
public void connect(SslContextFactory sslContextFactory, InetSocketAddress address, Session.Listener listener, Promise promise)
{
connect(sslContextFactory, address, listener, promise, null);
}
public void connect(SslContextFactory sslContextFactory, InetSocketAddress address, Session.Listener listener, Promise promise, Map context)
{
try
{
SocketChannel channel = SocketChannel.open();
SocketAddress bindAddress = getBindAddress();
if (bindAddress != null)
channel.bind(bindAddress);
configure(channel);
boolean connected = true;
if (isConnectBlocking())
{
channel.socket().connect(address, (int)getConnectTimeout());
channel.configureBlocking(false);
}
else
{
channel.configureBlocking(false);
connected = channel.connect(address);
}
context = contextFrom(sslContextFactory, address, listener, promise, context);
if (connected)
selector.accept(channel, context);
else
selector.connect(channel, context);
}
catch (Throwable x)
{
promise.failed(x);
}
}
public void accept(SslContextFactory sslContextFactory, SocketChannel channel, Session.Listener listener, Promise promise)
{
try
{
if (!channel.isConnected())
throw new IllegalStateException("SocketChannel must be connected");
channel.configureBlocking(false);
Map context = contextFrom(sslContextFactory, (InetSocketAddress)channel.getRemoteAddress(), listener, promise, null);
selector.accept(channel, context);
}
catch (Throwable x)
{
promise.failed(x);
}
}
private Map contextFrom(SslContextFactory sslContextFactory, InetSocketAddress address, Session.Listener listener, Promise promise, Map context)
{
if (context == null)
context = new ConcurrentHashMap<>();
context.put(HTTP2ClientConnectionFactory.CLIENT_CONTEXT_KEY, this);
context.put(HTTP2ClientConnectionFactory.SESSION_LISTENER_CONTEXT_KEY, listener);
context.put(HTTP2ClientConnectionFactory.SESSION_PROMISE_CONTEXT_KEY, promise);
if (sslContextFactory != null)
context.put(SslClientConnectionFactory.SSL_CONTEXT_FACTORY_CONTEXT_KEY, sslContextFactory);
context.put(SslClientConnectionFactory.SSL_PEER_HOST_CONTEXT_KEY, address.getHostString());
context.put(SslClientConnectionFactory.SSL_PEER_PORT_CONTEXT_KEY, address.getPort());
context.putIfAbsent(ClientConnectionFactory.CONNECTOR_CONTEXT_KEY, this);
return context;
}
protected void configure(SocketChannel channel) throws IOException
{
channel.socket().setTcpNoDelay(isTCPNoDelay());
}
private class ClientSelectorManager extends SelectorManager
{
private ClientSelectorManager(Executor executor, Scheduler scheduler, int selectors)
{
super(executor, scheduler, selectors);
}
@Override
protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) throws IOException
{
SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, selectionKey, getScheduler());
endp.setIdleTimeout(getIdleTimeout());
return endp;
}
@Override
public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) throws IOException
{
@SuppressWarnings("unchecked")
Map context = (Map)attachment;
context.put(HTTP2ClientConnectionFactory.BYTE_BUFFER_POOL_CONTEXT_KEY, getByteBufferPool());
context.put(HTTP2ClientConnectionFactory.EXECUTOR_CONTEXT_KEY, getExecutor());
context.put(HTTP2ClientConnectionFactory.SCHEDULER_CONTEXT_KEY, getScheduler());
return getClientConnectionFactory().newConnection(endpoint, context);
}
@Override
protected void connectionFailed(SelectableChannel channel, Throwable failure, Object attachment)
{
@SuppressWarnings("unchecked")
Map context = (Map)attachment;
if (LOG.isDebugEnabled())
{
Object host = context.get(SslClientConnectionFactory.SSL_PEER_HOST_CONTEXT_KEY);
Object port = context.get(SslClientConnectionFactory.SSL_PEER_PORT_CONTEXT_KEY);
LOG.debug("Could not connect to {}:{}", host, port);
}
@SuppressWarnings("unchecked")
Promise promise = (Promise)context.get(HTTP2ClientConnectionFactory.SESSION_PROMISE_CONTEXT_KEY);
promise.failed(failure);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy