diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java index a2b3f476f6..94b635eb42 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java @@ -29,6 +29,8 @@ import java.io.Closeable; import java.io.IOException; +import java.net.SocketAddress; +import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; @@ -97,12 +99,14 @@ import org.apache.hc.core5.http2.protocol.H2RequestContent; import org.apache.hc.core5.http2.protocol.H2RequestTargetHost; import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.io.IOFunction; import org.apache.hc.core5.reactor.Command; import org.apache.hc.core5.reactor.DefaultConnectingIOReactor; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; +import org.apache.hc.core5.reactor.SocketChannelFactory; import org.apache.hc.core5.util.Args; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.VersionInfo; @@ -172,6 +176,7 @@ private ExecInterceptorEntry( } private IOReactorConfig ioReactorConfig; + private IOFunction socketChannelFactory; private IOSessionListener ioSessionListener; private H2Config h2Config; private CharCodingConfig charCodingConfig; @@ -249,6 +254,18 @@ public final H2AsyncClientBuilder setIOReactorConfig(final IOReactorConfig ioRea return this; } + /** + * Sets the factory used by the I/O reactor to create socket channels for + * outgoing connections. + * + * @return this instance. + * @since 5.7 + */ + public final H2AsyncClientBuilder setSocketChannelFactory(final IOFunction socketChannelFactory) { + this.socketChannelFactory = socketChannelFactory; + return this; + } + /** * Sets {@link IOSessionListener} listener. * @@ -906,7 +923,8 @@ public CloseableHttpAsyncClient build() { ioSessionDecorator != null ? ioSessionDecorator : LoggingIOSessionDecorator.INSTANCE, ioReactorExceptionCallback != null ? ioReactorExceptionCallback : LoggingExceptionCallback.INSTANCE, ioSessionListener, - ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE)); + ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE), + socketChannelFactory); if (execInterceptors != null) { for (final ExecInterceptorEntry entry: execInterceptors) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java index d010ac8617..7ab0a778f6 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java @@ -29,6 +29,8 @@ import java.io.Closeable; import java.net.ProxySelector; +import java.net.SocketAddress; +import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; @@ -114,6 +116,7 @@ import org.apache.hc.core5.http2.protocol.H2RequestContent; import org.apache.hc.core5.http2.protocol.H2RequestTargetHost; import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.io.IOFunction; import org.apache.hc.core5.pool.ConnPoolControl; import org.apache.hc.core5.reactor.Command; import org.apache.hc.core5.reactor.DefaultConnectingIOReactor; @@ -200,6 +203,7 @@ private ExecInterceptorEntry( private AsyncClientConnectionManager connManager; private boolean connManagerShared; private IOReactorConfig ioReactorConfig; + private IOFunction socketChannelFactory; private IOSessionListener ioSessionListener; private Callback ioReactorExceptionCallback; private Http1Config h1Config; @@ -346,6 +350,18 @@ public final HttpAsyncClientBuilder setIOReactorConfig(final IOReactorConfig ioR return this; } + /** + * Sets the factory used by the I/O reactor to create socket channels for + * outgoing connections. + * + * @return this instance. + * @since 5.7 + */ + public final HttpAsyncClientBuilder setSocketChannelFactory(final IOFunction socketChannelFactory) { + this.socketChannelFactory = socketChannelFactory; + return this; + } + /** * Sets {@link IOSessionListener} listener. * @@ -1197,7 +1213,8 @@ public CloseableHttpAsyncClient build() { ioSessionDecorator != null ? ioSessionDecorator : LoggingIOSessionDecorator.INSTANCE, ioReactorExceptionCallback != null ? ioReactorExceptionCallback : LoggingExceptionCallback.INSTANCE, ioSessionListener, - ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE)); + ioSession -> ioSession.enqueue(new ShutdownCommand(CloseMode.GRACEFUL), Command.Priority.IMMEDIATE), + socketChannelFactory); if (execInterceptors != null) { for (final ExecInterceptorEntry entry: execInterceptors) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.java index 651c14856c..201efd0d37 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManagerBuilder.java @@ -34,6 +34,7 @@ import org.apache.hc.client5.http.SchemePortResolver; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.TlsConfig; +import org.apache.hc.client5.http.io.DetachedSocketFactory; import org.apache.hc.client5.http.io.HttpClientConnectionOperator; import org.apache.hc.client5.http.io.ManagedHttpClientConnection; import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; @@ -60,6 +61,7 @@ public class PoolingHttpClientConnectionManagerBuilder { private HttpConnectionFactory connectionFactory; + private DetachedSocketFactory detachedSocketFactory; private TlsSocketStrategy tlsSocketStrategy; private SchemePortResolver schemePortResolver; private DnsResolver dnsResolver; @@ -96,6 +98,18 @@ public final PoolingHttpClientConnectionManagerBuilder setConnectionFactory( return this; } + /** + * Sets the factory used to create detached sockets for outgoing connections. + * + * @return this instance. + * @since 5.7 + */ + public final PoolingHttpClientConnectionManagerBuilder setDetachedSocketFactory( + final DetachedSocketFactory detachedSocketFactory) { + this.detachedSocketFactory = detachedSocketFactory; + return this; + } + /** * Sets {@link org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory} instance. * @@ -325,7 +339,10 @@ protected HttpClientConnectionOperator createConnectionOperator( final SchemePortResolver schemePortResolver, final DnsResolver dnsResolver, final TlsSocketStrategy tlsSocketStrategy) { - return new DefaultHttpClientConnectionOperator(schemePortResolver, dnsResolver, + return new DefaultHttpClientConnectionOperator( + detachedSocketFactory != null ? detachedSocketFactory : DefaultHttpClientConnectionOperator.PLAIN_SOCKET_FACTORY, + schemePortResolver, + dnsResolver, RegistryBuilder.create() .register(URIScheme.HTTPS.id, tlsSocketStrategy) .build()); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/io/DetachedSocketFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/io/DetachedSocketFactory.java index 68b553e803..8b67929c83 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/io/DetachedSocketFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/io/DetachedSocketFactory.java @@ -32,13 +32,13 @@ import java.net.Socket; import org.apache.hc.core5.annotation.Contract; -import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; /** + * Factory for detached sockets used for outgoing classic connections. + * * @since 5.4 */ -@Internal @Contract(threading = ThreadingBehavior.STATELESS) public interface DetachedSocketFactory { diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/CustomTransportBuilderTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/CustomTransportBuilderTest.java new file mode 100644 index 0000000000..323548e6e0 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/CustomTransportBuilderTest.java @@ -0,0 +1,72 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.async; + +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.channels.SocketChannel; + +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.DetachedSocketFactory; +import org.apache.hc.core5.io.IOFunction; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class CustomTransportBuilderTest { + + @Test + void testClassicBuilderAcceptsDetachedSocketFactory() { + final DetachedSocketFactory socketFactory = proxy -> new Socket(); + final PoolingHttpClientConnectionManager connectionManager = + PoolingHttpClientConnectionManagerBuilder.create() + .setDetachedSocketFactory(socketFactory) + .build(); + try { + Assertions.assertNotNull(connectionManager); + } finally { + connectionManager.close(); + } + } + + @Test + void testAsyncBuildersAcceptSocketChannelFactory() throws Exception { + final IOFunction socketChannelFactory = remoteAddress -> SocketChannel.open(); + try (CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create() + .setSocketChannelFactory(socketChannelFactory) + .build()) { + Assertions.assertNotNull(client); + } + try (CloseableHttpAsyncClient client = H2AsyncClientBuilder.create() + .setSocketChannelFactory(socketChannelFactory) + .build()) { + Assertions.assertNotNull(client); + } + } + +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/SocketChannelFactoryBuilderTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/SocketChannelFactoryBuilderTest.java new file mode 100644 index 0000000000..c0e908acc0 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/SocketChannelFactoryBuilderTest.java @@ -0,0 +1,89 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.async; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.channels.SocketChannel; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.core5.io.IOFunction; +import org.apache.hc.core5.reactor.SocketChannelFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SocketChannelFactoryBuilderTest { + + @Test + void testHttpAsyncClientBuilderUsesSocketChannelFactory() throws Exception { + final AtomicInteger invocationCount = new AtomicInteger(); + final IOException expected = new IOException("custom transport"); + final IOFunction socketChannelFactory = remoteAddress -> { + invocationCount.incrementAndGet(); + throw expected; + }; + try (CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create() + .setSocketChannelFactory(socketChannelFactory) + .disableAutomaticRetries() + .build()) { + assertFactoryUsed(client, invocationCount); + } + } + + @Test + void testH2AsyncClientBuilderUsesSocketChannelFactory() throws Exception { + final AtomicInteger invocationCount = new AtomicInteger(); + final IOException expected = new IOException("custom transport"); + final SocketChannelFactory socketChannelFactory = remoteAddress -> { + invocationCount.incrementAndGet(); + throw expected; + }; + try (CloseableHttpAsyncClient client = H2AsyncClientBuilder.create() + .setSocketChannelFactory(socketChannelFactory) + .disableAutomaticRetries() + .build()) { + assertFactoryUsed(client, invocationCount); + } + } + + private static void assertFactoryUsed( + final CloseableHttpAsyncClient client, + final AtomicInteger invocationCount) throws Exception { + client.start(); + final Future future = client.execute( + SimpleRequestBuilder.get("http://localhost:18080/").build(), null); + Assertions.assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + Assertions.assertEquals(1, invocationCount.get()); + } + +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/DetachedSocketFactoryBuilderTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/DetachedSocketFactoryBuilderTest.java new file mode 100644 index 0000000000..dda6d43f1d --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/DetachedSocketFactoryBuilderTest.java @@ -0,0 +1,64 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.io; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.io.DetachedSocketFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DetachedSocketFactoryBuilderTest { + + @Test + void testPoolingBuilderUsesDetachedSocketFactory() throws Exception { + final AtomicInteger invocationCount = new AtomicInteger(); + final IOException expected = new IOException("custom transport"); + final DetachedSocketFactory socketFactory = proxy -> { + invocationCount.incrementAndGet(); + throw expected; + }; + final PoolingHttpClientConnectionManager connectionManager = + PoolingHttpClientConnectionManagerBuilder.create() + .setDetachedSocketFactory(socketFactory) + .build(); + try (CloseableHttpClient client = HttpClients.custom() + .setConnectionManager(connectionManager) + .disableAutomaticRetries() + .build()) { + Assertions.assertThrows(IOException.class, () -> + client.executeOpen(null, new HttpGet("http://localhost:18080/"), null)); + Assertions.assertEquals(1, invocationCount.get()); + } + } + +}