diff --git a/build.gradle b/build.gradle index 48a49ed..5f0c322 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,6 @@ subprojects { dependencies { alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}" - asciidoclet "org.xbib:asciidoclet:${project.property('asciidoclet.version')}" wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}" } @@ -61,7 +60,7 @@ subprojects { jvmArgs "-javaagent:" + configurations.alpnagent.asPath } testLogging { - showStandardStreams = false + showStandardStreams = true exceptionFormat = 'full' } } @@ -82,7 +81,7 @@ subprojects { 'source-highlighter': 'coderay' } - javadoc { + /*javadoc { options.docletpath = configurations.asciidoclet.files.asType(List) options.doclet = "org.xbib.asciidoclet.Asciidoclet" options.overview = "src/docs/asciidoclet/overview.adoc" @@ -92,7 +91,7 @@ subprojects { configure(options) { noTimestamp = true } - } + }*/ task javadocJar(type: Jar, dependsOn: classes) { from javadoc @@ -116,7 +115,6 @@ subprojects { } } - ext { user = 'jprante' name = 'netty-http-client' @@ -126,7 +124,6 @@ subprojects { scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' } - task xbibUpload(type: Upload) { group = 'publish' configuration = configurations.archives @@ -195,18 +192,20 @@ subprojects { } } - -} - -spotbugs { - effort = "max" - reportLevel = "low" - //includeFilter = file("findbugs-exclude.xml") -} - -tasks.withType(com.github.spotbugs.SpotBugsTask) { - ignoreFailures = true - reports { + spotbugs { + toolVersion = '3.1.3' + sourceSets = [sourceSets.main] + ignoreFailures = true + effort = "max" + reportLevel = "high" + // includeFilter = file("config/findbugs/findbugs-include.xml") + // excludeFilter = file("config/findbugs/findbugs-excludes.xml") + } + spotbugsMain.reports { + xml.enabled = false + html.enabled = true + } + spotbugsTest.reports { xml.enabled = false html.enabled = true } diff --git a/gradle.properties b/gradle.properties index 9a3a20d..4ebacba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ group = org.xbib name = netty-http-client -version = 4.1.22.2 +version = 4.1.24.0 -netty.version = 4.1.22.Final +netty.version = 4.1.24.Final tcnative.version = 2.0.7.Final conscrypt.version = 1.0.1 bouncycastle.version = 1.57 @@ -10,7 +10,6 @@ xbib-net-url.version = 1.1.0 alpnagent.version = 2.0.7 junit.version = 4.12 jackson.version = 2.8.11.1 -asciidoclet.version = 1.6.0.0 wagon.version = 3.0.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961f..91ca28c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e65c9e2..8c59352 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Mar 02 19:15:04 CET 2018 +#Sat Apr 28 00:39:47 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java index 43d4d97..b506794 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java @@ -21,15 +21,11 @@ import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslHandler; -import org.xbib.net.URL; -import org.xbib.netty.http.client.handler.http1.HttpChannelInitializer; -import org.xbib.netty.http.client.handler.http1.HttpResponseHandler; +import org.xbib.netty.http.client.handler.http.HttpChannelInitializer; import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; -import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler; -import org.xbib.netty.http.client.handler.http2.Http2SettingsHandler; import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.transport.Http2Transport; -import org.xbib.netty.http.client.transport.Http1Transport; +import org.xbib.netty.http.client.transport.HttpTransport; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.NetworkUtils; @@ -73,12 +69,6 @@ public final class Client { if (System.getProperty("io.netty.noKeySetOptimization") == null) { System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); } - if (System.getProperty("io.netty.recycler.maxCapacity") == null) { - System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); - } - if (System.getProperty("io.netty.leakDetection.level") == null) { - System.setProperty("io.netty.leakDetection.level", "paranoid"); - } } private final ClientConfig clientConfig; @@ -91,16 +81,8 @@ public final class Client { private final Bootstrap bootstrap; - private final HttpResponseHandler httpResponseHandler; - - private final Http2SettingsHandler http2SettingsHandler; - - private final Http2ResponseHandler http2ResponseHandler; - private final List transports; - private TransportListener transportListener; - private BoundedChannelPool pool; public Client() { @@ -126,7 +108,7 @@ public final class Client { this.bootstrap = new Bootstrap() .group(this.eventLoopGroup) .channel(this.socketChannelClass) - //.option(ChannelOption.ALLOCATOR, byteBufAllocator) + .option(ChannelOption.ALLOCATOR, byteBufAllocator) .option(ChannelOption.TCP_NODELAY, clientConfig.isTcpNodelay()) .option(ChannelOption.SO_KEEPALIVE, clientConfig.isKeepAlive()) .option(ChannelOption.SO_REUSEADDR, clientConfig.isReuseAddr()) @@ -134,9 +116,6 @@ public final class Client { .option(ChannelOption.SO_RCVBUF, clientConfig.getTcpReceiveBufferSize()) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientConfig.getConnectTimeoutMillis()) .option(ChannelOption.WRITE_BUFFER_WATER_MARK, clientConfig.getWriteBufferWaterMark()); - this.httpResponseHandler = new HttpResponseHandler(); - this.http2SettingsHandler = new Http2SettingsHandler(); - this.http2ResponseHandler = new Http2ResponseHandler(); this.transports = new CopyOnWriteArrayList<>(); if (!clientConfig.getPoolNodes().isEmpty()) { List nodes = clientConfig.getPoolNodes(); @@ -151,7 +130,8 @@ public final class Client { } ClientChannelPoolHandler clientChannelPoolHandler = new ClientChannelPoolHandler(); this.pool = new BoundedChannelPool<>(semaphore, clientConfig.getPoolVersion(), - clientConfig.isPoolSecure(), nodes, bootstrap, clientChannelPoolHandler, retries); + nodes, bootstrap, clientChannelPoolHandler, retries, + BoundedChannelPool.PoolKeySelectorType.ROUNDROBIN); Integer nodeConnectionLimit = clientConfig.getPoolNodeConnectionLimit(); if (nodeConnectionLimit == null || nodeConnectionLimit == 0) { nodeConnectionLimit = nodes.size(); @@ -176,22 +156,10 @@ public final class Client { return byteBufAllocator; } - public EventLoopGroup getEventLoopGroup() { - return eventLoopGroup; - } - - public void setTransportListener(TransportListener transportListener) { - this.transportListener = transportListener; - } - public boolean hasPooledConnections() { return pool != null && !clientConfig.getPoolNodes().isEmpty(); } - public BoundedChannelPool getPool() { - return pool; - } - public void logDiagnostics(Level level) { logger.log(level, () -> "OpenSSL available: " + OpenSsl.isAvailable() + " OpenSSL ALPN support: " + OpenSsl.isAlpnSupported() + @@ -206,29 +174,22 @@ public final class Client { return newTransport(null); } - public Transport newTransport(URL url, HttpVersion httpVersion) { - return newTransport(HttpAddress.of(url, httpVersion)); - } - public Transport newTransport(HttpAddress httpAddress) { - Transport transport = null; + Transport transport; if (httpAddress != null) { if (httpAddress.getVersion().majorVersion() == 1) { - transport = new Http1Transport(this, httpAddress); + transport = new HttpTransport(this, httpAddress); } else { transport = new Http2Transport(this, httpAddress); } } else if (hasPooledConnections()) { if (pool.getVersion().majorVersion() == 1) { - transport = new Http1Transport(this, null); + transport = new HttpTransport(this, null); } else { transport = new Http2Transport(this, null); } } else { - throw new IllegalStateException(); - } - if (transportListener != null) { - transportListener.onOpen(transport); + throw new IllegalStateException("no address given to connect to"); } transports.add(transport); return transport; @@ -238,14 +199,13 @@ public final class Client { Channel channel; if (httpAddress != null) { HttpVersion httpVersion = httpAddress.getVersion(); - ChannelInitializer initializer; + ChannelInitializer initializer; SslHandler sslHandler = newSslHandler(clientConfig, byteBufAllocator, httpAddress); if (httpVersion.majorVersion() == 1) { - initializer = new HttpChannelInitializer(clientConfig, httpAddress, - sslHandler, httpResponseHandler); + initializer = new HttpChannelInitializer(clientConfig, httpAddress, sslHandler, + new Http2ChannelInitializer(clientConfig, httpAddress, sslHandler)); } else { - initializer = new Http2ChannelInitializer(clientConfig, httpAddress, - sslHandler, http2SettingsHandler, http2ResponseHandler); + initializer = new Http2ChannelInitializer(clientConfig, httpAddress, sslHandler); } try { channel = bootstrap.handler(initializer) @@ -267,21 +227,18 @@ public final class Client { return channel; } - public Channel newChannel() throws IOException { - return newChannel(null); - } - - public void releaseChannel(Channel channel) throws IOException{ - if (channel != null) { - if (hasPooledConnections()) { - try { - pool.release(channel); - } catch (Exception e) { - throw new IOException(e); - } - } else { - channel.close(); + public void releaseChannel(Channel channel, boolean close) throws IOException{ + if (channel == null) { + return; + } + if (hasPooledConnections()) { + try { + pool.release(channel, close); + } catch (Exception e) { + throw new IOException(e); } + } else if (close) { + channel.close(); } } @@ -293,19 +250,15 @@ public final class Client { public CompletableFuture execute(Request request, Function supplier) throws IOException { - return newTransport(HttpAddress.of(request.url(), request.httpVersion())).execute(request, supplier); - } - - public Transport pooledExecute(Request request) throws IOException { - Transport transport = newTransport(); - transport.execute(request); - return transport; + return newTransport(HttpAddress.of(request.url(), request.httpVersion())) + .execute(request, supplier); } /** * For following redirects, construct a new transport. * @param transport the previous transport * @param request the new request for continuing the request. + * @throws IOException if continuation fails */ public void continuation(Transport transport, Request request) throws IOException { Transport nextTransport = newTransport(HttpAddress.of(request.url(), request.httpVersion())); @@ -328,14 +281,7 @@ public final class Client { close(transport); } - public Transport prepareRequest(Request request) { - return newTransport(HttpAddress.of(request.url(), request.httpVersion())); - } - public void close(Transport transport) throws IOException { - if (transportListener != null) { - transportListener.onClose(transport); - } transport.close(); transports.remove(transport); } @@ -344,12 +290,13 @@ public final class Client { for (Transport transport : transports) { close(transport); } - } - - public void shutdownGracefully() throws IOException { + // how to wait for all responses for the pool? if (hasPooledConnections()) { pool.close(); } + } + + public void shutdownGracefully() throws IOException { close(); shutdown(); } @@ -439,13 +386,6 @@ public final class Client { ApplicationProtocolNames.HTTP_2); } - public interface TransportListener { - - void onOpen(Transport transport); - - void onClose(Transport transport); - } - static class HttpClientThreadFactory implements ThreadFactory { private int number = 0; @@ -474,17 +414,12 @@ public final class Client { HttpVersion httpVersion = httpAddress.getVersion(); SslHandler sslHandler = newSslHandler(clientConfig, byteBufAllocator, httpAddress); if (httpVersion.majorVersion() == 1) { - HttpChannelInitializer initializer = new HttpChannelInitializer(clientConfig, httpAddress, - sslHandler, httpResponseHandler); - if (channel instanceof SocketChannel) { - initializer.initChannel((SocketChannel) channel); - } + HttpChannelInitializer initializer = new HttpChannelInitializer(clientConfig, httpAddress, sslHandler, + new Http2ChannelInitializer(clientConfig, httpAddress, sslHandler)); + initializer.initChannel(channel); } else { - Http2ChannelInitializer initializer = new Http2ChannelInitializer(clientConfig, httpAddress, - sslHandler, http2SettingsHandler, http2ResponseHandler); - if (channel instanceof SocketChannel) { - initializer.initChannel((SocketChannel) channel); - } + Http2ChannelInitializer initializer = new Http2ChannelInitializer(clientConfig, httpAddress, sslHandler); + initializer.initChannel(channel); } } } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java index ee73632..3782cd9 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java @@ -222,6 +222,11 @@ public class ClientBuilder { return this; } + public ClientBuilder enableNegotiation(boolean enableNegotiation) { + clientConfig.setEnableNegotiation(enableNegotiation); + return this; + } + public Client build() { return new Client(clientConfig, byteBufAllocator, eventLoopGroup, socketChannelClass); } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java index 855315a..43bfe9b 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java @@ -163,6 +163,8 @@ public class ClientConfig { * Default for backoff. */ BackOff BACK_OFF = BackOff.ZERO_BACKOFF; + + Boolean ENABLE_NEGOTIATION = false; } private static TrustManagerFactory TRUST_MANAGER_FACTORY; @@ -249,6 +251,8 @@ public class ClientConfig { private BackOff backOff = Defaults.BACK_OFF; + private boolean enableNegotiation = Defaults.ENABLE_NEGOTIATION; + public ClientConfig setDebug(boolean debug) { this.debug = debug; return this; @@ -611,6 +615,15 @@ public class ClientConfig { return backOff; } + public ClientConfig setEnableNegotiation(boolean enableNegotiation) { + this.enableNegotiation = enableNegotiation; + return this; + } + + public boolean isEnableNegotiation() { + return enableNegotiation; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java index b52381a..9ea25e5 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java @@ -10,8 +10,7 @@ import io.netty.handler.codec.http.cookie.Cookie; import org.xbib.net.URL; import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.HttpHeadersListener; -import org.xbib.netty.http.client.listener.HttpResponseListener; +import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.retry.BackOff; import java.nio.charset.StandardCharsets; @@ -51,9 +50,7 @@ public class Request { private CompletableFuture completableFuture; - private HttpResponseListener responseListener; - - private HttpHeadersListener headersListener; + private ResponseListener responseListener; private CookieListener cookieListener; @@ -136,6 +133,12 @@ public class Request { return true; } + public void release() { + if (content != null) { + content.release(); + } + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -160,14 +163,6 @@ public class Request { return completableFuture; } - public Request setHeadersListener(HttpHeadersListener httpHeadersListener) { - this.headersListener = httpHeadersListener; - return this; - } - - public HttpHeadersListener getHeadersListener() { - return headersListener; - } public Request setCookieListener(CookieListener cookieListener) { this.cookieListener = cookieListener; @@ -178,12 +173,12 @@ public class Request { return cookieListener; } - public Request setResponseListener(HttpResponseListener httpResponseListener) { - this.responseListener = httpResponseListener; + public Request setResponseListener(ResponseListener responseListener) { + this.responseListener = responseListener; return this; } - public HttpResponseListener getResponseListener() { + public ResponseListener getResponseListener() { return responseListener; } @@ -224,7 +219,7 @@ public class Request { } public static RequestBuilder builder(HttpMethod httpMethod) { - return new RequestBuilder(PooledByteBufAllocator.DEFAULT).setMethod(httpMethod); + return builder(PooledByteBufAllocator.DEFAULT, httpMethod); } public static RequestBuilder builder(ByteBufAllocator allocator, HttpMethod httpMethod) { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java index 42624f0..39070d9 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java @@ -350,7 +350,8 @@ public class RequestBuilder { } private void content(byte[] buf, AsciiString contentType) { - content(allocator.buffer().writeBytes(buf), contentType); + ByteBuf byteBuf = allocator.buffer(); + content(byteBuf.writeBytes(buf), contentType); } private void content(ByteBuf body, AsciiString contentType) { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java similarity index 50% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java index 45ab3a8..81f3d91 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java @@ -1,20 +1,24 @@ -package org.xbib.netty.http.client.handler.http1; +package org.xbib.netty.http.client.handler.http; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.logging.LogLevel; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.SslHandler; import org.xbib.netty.http.client.ClientConfig; +import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; import org.xbib.netty.http.common.HttpAddress; import java.util.logging.Level; import java.util.logging.Logger; -public class HttpChannelInitializer extends ChannelInitializer { +public class HttpChannelInitializer extends ChannelInitializer { private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName()); @@ -26,18 +30,21 @@ public class HttpChannelInitializer extends ChannelInitializer { private final HttpResponseHandler httpResponseHandler; + private final Http2ChannelInitializer http2ChannelInitializer; + public HttpChannelInitializer(ClientConfig clientConfig, HttpAddress httpAddress, SslHandler sslHandler, - HttpResponseHandler httpResponseHandler) { + Http2ChannelInitializer http2ChannelInitializer) { this.clientConfig = clientConfig; this.httpAddress = httpAddress; this.sslHandler = sslHandler; - this.httpResponseHandler = httpResponseHandler; + this.http2ChannelInitializer = http2ChannelInitializer; + this.httpResponseHandler = new HttpResponseHandler(); } @Override - public void initChannel(SocketChannel channel) { + public void initChannel(Channel channel) { if (clientConfig.isDebug()) { channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); } @@ -47,17 +54,43 @@ public class HttpChannelInitializer extends ChannelInitializer { configureCleartext(channel); } if (clientConfig.isDebug()) { - logger.log(Level.FINE, "HTTP 1 channel initialized: " + channel.pipeline().names()); + logger.log(Level.FINE, "HTTP 1.1 client channel initialized: " + channel.pipeline().names()); } } - private void configureEncrypted(SocketChannel channel) { + private void configureEncrypted(Channel channel) { ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast(sslHandler); - configureCleartext(channel); + if (clientConfig.isEnableNegotiation()) { + ApplicationProtocolNegotiationHandler negotiationHandler = + new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) { + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + http2ChannelInitializer.configureCleartext(ctx.channel()); + if (clientConfig.isDebug()) { + logger.log(Level.FINE, "after negotiation to HTTP/2: " + ctx.pipeline().names()); + } + return; + } + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + configureCleartext(ctx.channel()); + if (clientConfig.isDebug()) { + logger.log(Level.FINE, "after negotiation to HTTP 1.1: " + ctx.pipeline().names()); + } + return; + } + ctx.close(); + throw new IllegalStateException("protocol not accepted: " + protocol); + } + }; + channel.pipeline().addLast(negotiationHandler); + } else { + configureCleartext(channel); + } } - private void configureCleartext(SocketChannel channel) { + private void configureCleartext(Channel channel) { ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast(new HttpClientCodec(clientConfig.getMaxInitialLineLength(), clientConfig.getMaxHeadersSize(), clientConfig.getMaxChunkSize())); diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChunkContentCompressor.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChunkContentCompressor.java similarity index 94% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChunkContentCompressor.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChunkContentCompressor.java index 1643365..5da36fe 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChunkContentCompressor.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChunkContentCompressor.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.handler.http1; +package org.xbib.netty.http.client.handler.http; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpResponseHandler.java similarity index 71% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpResponseHandler.java index ead69a5..6ee0e15 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpResponseHandler.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.handler.http1; +package org.xbib.netty.http.client.handler.http; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; @@ -10,11 +10,9 @@ import org.xbib.netty.http.client.transport.Transport; public class HttpResponseHandler extends SimpleChannelInboundHandler { @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) { + public void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) throws Exception { Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); - transport.headersReceived(null, httpResponse.headers()); - transport.responseReceived(null, httpResponse); - transport.success(); + transport.responseReceived(ctx.channel(),null, httpResponse); } @Override diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/TrafficLoggingHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/TrafficLoggingHandler.java similarity index 95% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/TrafficLoggingHandler.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/TrafficLoggingHandler.java index 333a2f0..b4bfcd9 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/TrafficLoggingHandler.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/TrafficLoggingHandler.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.handler.http1; +package org.xbib.netty.http.client.handler.http; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/package-info.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/package-info.java new file mode 100644 index 0000000..fe6afdc --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/package-info.java @@ -0,0 +1,4 @@ +/** + * HTTP handlers for Netty HTTP client. + */ +package org.xbib.netty.http.client.handler.http; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/package-info.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/package-info.java deleted file mode 100644 index 4cb6b26..0000000 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * HTTP handlers for Netty HTTP client. - */ -package org.xbib.netty.http.client.handler.http1; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java index 4d3f904..a16f869 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java @@ -1,26 +1,35 @@ package org.xbib.netty.http.client.handler.http2; +import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.http2.DefaultHttp2Connection; -import io.netty.handler.codec.http2.Http2Connection; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame; +import io.netty.handler.codec.http2.Http2ConnectionAdapter; +import io.netty.handler.codec.http2.Http2ConnectionDecoder; +import io.netty.handler.codec.http2.Http2ConnectionEncoder; import io.netty.handler.codec.http2.Http2ConnectionHandler; +import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2FrameAdapter; +import io.netty.handler.codec.http2.Http2FrameCodec; import io.netty.handler.codec.http2.Http2FrameLogger; -import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; -import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2MultiplexCodec; +import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; +import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.logging.LogLevel; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.SslHandler; import org.xbib.netty.http.client.ClientConfig; -import org.xbib.netty.http.client.handler.http1.TrafficLoggingHandler; +import org.xbib.netty.http.client.handler.http.TrafficLoggingHandler; +import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import java.util.logging.Level; import java.util.logging.Logger; -public class Http2ChannelInitializer extends ChannelInitializer { +public class Http2ChannelInitializer extends ChannelInitializer { private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName()); @@ -30,29 +39,16 @@ public class Http2ChannelInitializer extends ChannelInitializer { private final SslHandler sslHandler; - private final Http2SettingsHandler http2SettingsHandler; - - private final Http2ResponseHandler http2ResponseHandler; - public Http2ChannelInitializer(ClientConfig clientConfig, HttpAddress httpAddress, - SslHandler sslHandler, - Http2SettingsHandler http2SettingsHandler, - Http2ResponseHandler http2ResponseHandler) { + SslHandler sslHandler) { this.clientConfig = clientConfig; this.httpAddress = httpAddress; this.sslHandler = sslHandler; - this.http2SettingsHandler = http2SettingsHandler; - this.http2ResponseHandler = http2ResponseHandler; } - /** - * The channel initialization for HTTP/2. - * - * @param channel socket channel - */ @Override - public void initChannel(SocketChannel channel) { + public void initChannel(Channel channel) { if (clientConfig.isDebug()) { channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); } @@ -66,44 +62,73 @@ public class Http2ChannelInitializer extends ChannelInitializer { } } - private void configureEncrypted(SocketChannel channel) { + private void configureEncrypted(Channel channel) { channel.pipeline().addLast(sslHandler); - ApplicationProtocolNegotiationHandler negotiationHandler = new ApplicationProtocolNegotiationHandler("") { + configureCleartext(channel); + } + + public void configureCleartext(Channel ch) { + ChannelInitializer initializer = new ChannelInitializer() { @Override - protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { - if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - ctx.pipeline().addLast(newConnectionHandler(), http2SettingsHandler, http2ResponseHandler); - if (clientConfig.isDebug()) { - logger.log(Level.FINE, "after negotiation: " + ctx.pipeline().names()); - } - return; - } - // we do not fall back to HTTP1 - ctx.close(); - throw new IllegalStateException("protocol not accepted: " + protocol); + protected void initChannel(Channel ch) { + throw new IllegalStateException(); } }; - channel.pipeline().addLast(negotiationHandler); -} - - private void configureCleartext(SocketChannel ch) { - ch.pipeline().addLast(newConnectionHandler(), http2SettingsHandler, http2ResponseHandler); - } - - private Http2ConnectionHandler newConnectionHandler() { - Http2Connection http2Connection = new DefaultHttp2Connection(false); - HttpToHttp2ConnectionHandlerBuilder http2ConnectionHandlerBuilder = new HttpToHttp2ConnectionHandlerBuilder() - .initialSettings(clientConfig.getHttp2Settings()) - .connection(http2Connection) - .frameListener(new Http2PushPromiseHandler(http2Connection, - new InboundHttp2ToHttpAdapterBuilder(http2Connection) - .maxContentLength(clientConfig.getMaxContentLength()) - .propagateSettings(true) - .build())); + Http2MultiplexCodecBuilder clientMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forClient(initializer) + .initialSettings(clientConfig.getHttp2Settings()); if (clientConfig.isDebug()) { - Http2FrameLogger http2FrameLogger = new Http2FrameLogger(clientConfig.getDebugLogLevel(), "client"); - http2ConnectionHandlerBuilder.frameLogger(http2FrameLogger); + clientMultiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "client")); + } + Http2MultiplexCodec http2MultiplexCodec = clientMultiplexCodecBuilder.build(); + ChannelPipeline p = ch.pipeline(); + p.addLast("client-codec", http2MultiplexCodec); + //p.addLast("client-push-promise", new PushPromiseHandler()); + p.addLast("client-messages", new ClientMessages()); + } + + class ClientMessages extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof DefaultHttp2SettingsFrame) { + DefaultHttp2SettingsFrame settingsFrame = (DefaultHttp2SettingsFrame) msg; + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + if (transport != null) { + transport.settingsReceived(settingsFrame.settings()); + } + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) { + Http2ConnectionPrefaceAndSettingsFrameWrittenEvent event = + (Http2ConnectionPrefaceAndSettingsFrameWrittenEvent)evt; + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + if (transport != null) { + transport.settingsReceived(null); + } + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + if (transport != null) { + transport.fail(cause); + } + } + } + + class PushPromiseHandler extends Http2FrameAdapter { + + @Override + public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, + Http2Headers headers, int padding) throws Http2Exception { + super.onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding); + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + transport.pushPromiseReceived(ctx.channel(), streamId, promisedStreamId, headers); } - return http2ConnectionHandlerBuilder.build(); } } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2PushPromiseHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2PushPromiseHandler.java deleted file mode 100644 index 10f98bf..0000000 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2PushPromiseHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.xbib.netty.http.client.handler.http2; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2Exception; -import io.netty.handler.codec.http2.Http2FrameListener; -import io.netty.handler.codec.http2.Http2Headers; -import org.xbib.netty.http.client.transport.Transport; - -public class Http2PushPromiseHandler extends DelegatingDecompressorFrameListener { - - public Http2PushPromiseHandler(Http2Connection connection, Http2FrameListener listener) { - super(connection, listener); - } - - @Override - public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, - Http2Headers headers, int padding) throws Http2Exception { - super.onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding); - Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); - transport.pushPromiseReceived(streamId, promisedStreamId, headers); - } -} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java index 412b776..918422a 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java @@ -7,24 +7,14 @@ import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http2.HttpConversionUtil; import org.xbib.netty.http.client.transport.Transport; -import java.io.IOException; - @ChannelHandler.Sharable public class Http2ResponseHandler extends SimpleChannelInboundHandler { @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) { + protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) throws Exception { Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); - transport.headersReceived(streamId, httpResponse.headers()); - transport.responseReceived(streamId, httpResponse); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) { - ctx.fireChannelInactive(); - Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); - transport.fail(new IOException("channel closed")); + transport.responseReceived(ctx.channel(), streamId, httpResponse); } @Override diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java deleted file mode 100644 index e9fb6ab..0000000 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.xbib.netty.http.client.handler.http2; - -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http2.Http2Settings; -import org.xbib.netty.http.client.transport.Transport; - -@ChannelHandler.Sharable -public class Http2SettingsHandler extends SimpleChannelInboundHandler { - - @Override - protected void channelRead0(ChannelHandlerContext ctx, Http2Settings http2Settings) { - Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); - transport.settingsReceived(ctx.channel(), http2Settings); - ctx.pipeline().remove(this); - } -} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2StreamFrameToHttpObjectCodec.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2StreamFrameToHttpObjectCodec.java new file mode 100644 index 0000000..d908b74 --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2StreamFrameToHttpObjectCodec.java @@ -0,0 +1,227 @@ +package org.xbib.netty.http.client.handler.http2; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.EncoderException; +import io.netty.handler.codec.MessageToMessageCodec; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpScheme; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http2.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2MultiplexCodec; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamFrame; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.internal.UnstableApi; + +import java.util.List; + +/** + * This handler converts from {@link Http2StreamFrame} to {@link HttpObject}, + * and back. It can be used as an adapter in conjunction with {@link + * Http2MultiplexCodec} to make http/2 connections backward-compatible with + * {@link ChannelHandler}s expecting {@link HttpObject}. + * + * For simplicity, it converts to chunked encoding unless the entire stream + * is a single header. + * + * Patched version of original Netty's Http2StreamFrameToHttpObjectCodec. + * This one is using the streamId from {@code frame.stream().id()}. + */ +@UnstableApi +@Sharable +public class Http2StreamFrameToHttpObjectCodec extends MessageToMessageCodec { + + private final boolean isServer; + + private final boolean validateHeaders; + + private HttpScheme scheme; + + public Http2StreamFrameToHttpObjectCodec(final boolean isServer, + final boolean validateHeaders) { + this.isServer = isServer; + this.validateHeaders = validateHeaders; + scheme = HttpScheme.HTTP; + } + + public Http2StreamFrameToHttpObjectCodec(final boolean isServer) { + this(isServer, true); + } + + @Override + public boolean acceptInboundMessage(Object msg) throws Exception { + return (msg instanceof Http2HeadersFrame) || (msg instanceof Http2DataFrame); + } + + @Override + protected void decode(ChannelHandlerContext ctx, Http2StreamFrame frame, List out) throws Exception { + if (frame instanceof Http2HeadersFrame) { + int id = frame.stream() != null ? frame.stream().id() : -1; + Http2HeadersFrame headersFrame = (Http2HeadersFrame) frame; + Http2Headers headers = headersFrame.headers(); + + final CharSequence status = headers.status(); + + // 100-continue response is a special case where Http2HeadersFrame#isEndStream=false + // but we need to decode it as a FullHttpResponse to play nice with HttpObjectAggregator. + if (null != status && HttpResponseStatus.CONTINUE.codeAsText().contentEquals(status)) { + final FullHttpMessage fullMsg = newFullMessage(id, headers, ctx.alloc()); + out.add(fullMsg); + return; + } + + if (headersFrame.isEndStream()) { + if (headers.method() == null && status == null) { + LastHttpContent last = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders); + HttpConversionUtil.addHttp2ToHttpHeaders(id, headers, last.trailingHeaders(), + HttpVersion.HTTP_1_1, true, true); + out.add(last); + } else { + FullHttpMessage full = newFullMessage(id, headers, ctx.alloc()); + out.add(full); + } + } else { + HttpMessage req = newMessage(id, headers); + if (!HttpUtil.isContentLengthSet(req)) { + req.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + } + out.add(req); + } + } else if (frame instanceof Http2DataFrame) { + Http2DataFrame dataFrame = (Http2DataFrame) frame; + if (dataFrame.isEndStream()) { + out.add(new DefaultLastHttpContent(dataFrame.content().retain(), validateHeaders)); + } else { + out.add(new DefaultHttpContent(dataFrame.content().retain())); + } + } + } + + private void encodeLastContent(LastHttpContent last, List out) { + boolean needFiller = !(last instanceof FullHttpMessage) && last.trailingHeaders().isEmpty(); + if (last.content().isReadable() || needFiller) { + out.add(new DefaultHttp2DataFrame(last.content().retain(), last.trailingHeaders().isEmpty())); + } + if (!last.trailingHeaders().isEmpty()) { + Http2Headers headers = HttpConversionUtil.toHttp2Headers(last.trailingHeaders(), validateHeaders); + out.add(new DefaultHttp2HeadersFrame(headers, true)); + } + } + + /** + * Encode from an {@link HttpObject} to an {@link Http2StreamFrame}. This method will + * be called for each written message that can be handled by this encoder. + * + * NOTE: 100-Continue responses that are NOT {@link FullHttpResponse} will be rejected. + * + * @param ctx the {@link ChannelHandlerContext} which this handler belongs to + * @param obj the {@link HttpObject} message to encode + * @param out the {@link List} into which the encoded msg should be added + * needs to do some kind of aggregation + * @throws Exception is thrown if an error occurs + */ + @Override + protected void encode(ChannelHandlerContext ctx, HttpObject obj, List out) throws Exception { + // 100-continue is typically a FullHttpResponse, but the decoded + // Http2HeadersFrame should not be marked as endStream=true + if (obj instanceof HttpResponse) { + final HttpResponse res = (HttpResponse) obj; + if (res.status().equals(HttpResponseStatus.CONTINUE)) { + if (res instanceof FullHttpResponse) { + final Http2Headers headers = toHttp2Headers(res); + out.add(new DefaultHttp2HeadersFrame(headers, false)); + return; + } else { + throw new EncoderException( + HttpResponseStatus.CONTINUE.toString() + " must be a FullHttpResponse"); + } + } + } + + if (obj instanceof HttpMessage) { + Http2Headers headers = toHttp2Headers((HttpMessage) obj); + boolean noMoreFrames = false; + if (obj instanceof FullHttpMessage) { + FullHttpMessage full = (FullHttpMessage) obj; + noMoreFrames = !full.content().isReadable() && full.trailingHeaders().isEmpty(); + } + + out.add(new DefaultHttp2HeadersFrame(headers, noMoreFrames)); + } + + if (obj instanceof LastHttpContent) { + LastHttpContent last = (LastHttpContent) obj; + encodeLastContent(last, out); + } else if (obj instanceof HttpContent) { + HttpContent cont = (HttpContent) obj; + out.add(new DefaultHttp2DataFrame(cont.content().retain(), false)); + } + } + + private Http2Headers toHttp2Headers(final HttpMessage msg) { + if (msg instanceof HttpRequest) { + msg.headers().set( + HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), + scheme.name()); + } + + return HttpConversionUtil.toHttp2Headers(msg, validateHeaders); + } + + private HttpMessage newMessage(final int id, + final Http2Headers headers) throws Http2Exception { + return isServer ? + HttpConversionUtil.toHttpRequest(id, headers, validateHeaders) : + HttpConversionUtil.toHttpResponse(id, headers, validateHeaders); + } + + private FullHttpMessage newFullMessage(final int id, + final Http2Headers headers, + final ByteBufAllocator alloc) throws Http2Exception { + return isServer ? + HttpConversionUtil.toFullHttpRequest(id, headers, alloc, validateHeaders) : + HttpConversionUtil.toFullHttpResponse(id, headers, alloc, validateHeaders); + } + + @Override + public void handlerAdded(final ChannelHandlerContext ctx) throws Exception { + super.handlerAdded(ctx); + + // this handler is typically used on an Http2StreamChannel. at this + // stage, ssl handshake should've been established. checking for the + // presence of SslHandler in the parent's channel pipeline to + // determine the HTTP scheme should suffice, even for the case where + // SniHandler is used. + scheme = isSsl(ctx) ? HttpScheme.HTTPS : HttpScheme.HTTP; + } + + protected boolean isSsl(final ChannelHandlerContext ctx) { + final Channel ch = ctx.channel(); + final Channel connChannel = (ch instanceof Http2StreamChannel) ? ch.parent() : ch; + return null != connChannel.pipeline().get(SslHandler.class); + } +} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java deleted file mode 100644 index 311436c..0000000 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.xbib.netty.http.client.listener; - -import io.netty.handler.codec.http.HttpHeaders; - -@FunctionalInterface -public interface HttpHeadersListener { - - void onHeaders(HttpHeaders httpHeaders); -} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ResponseListener.java similarity index 81% rename from netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ResponseListener.java index f06b1ee..5acd2a2 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ResponseListener.java @@ -3,7 +3,7 @@ package org.xbib.netty.http.client.listener; import io.netty.handler.codec.http.FullHttpResponse; @FunctionalInterface -public interface HttpResponseListener { +public interface ResponseListener { void onResponse(FullHttpResponse fullHttpResponse); } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java index 3181322..b66d368 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java @@ -5,12 +5,15 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPromise; import io.netty.channel.pool.ChannelPoolHandler; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame; import io.netty.util.AttributeKey; import org.xbib.netty.http.common.PoolKey; +import java.io.IOException; import java.net.ConnectException; import java.util.ArrayList; import java.util.HashMap; @@ -21,6 +24,7 @@ import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.locks.Lock; @@ -36,8 +40,6 @@ public class BoundedChannelPool implements Pool { private final HttpVersion httpVersion; - private final boolean isSecure; - private final ChannelPoolHandler channelPoolhandler; private final List nodes; @@ -60,26 +62,36 @@ public class BoundedChannelPool implements Pool { private final AttributeKey attributeKey; + private PoolKeySelector poolKeySelector; + /** * @param semaphore the concurrency level * @param httpVersion the HTTP version of the pool connections - * @param isSecure if this pool has secure connections * @param nodes the endpoint nodes, any element may contain the port (followed after ":") * to override the defaultPort argument * @param bootstrap bootstrap instance * @param channelPoolHandler channel pool handler being notified upon new connection is created * @param retriesPerNode the max count of the subsequent connection failures to the node before * the node will be excluded from the pool. If set to 0, the value is ignored. + * @param poolKeySelectorType pool key selector type */ - public BoundedChannelPool(Semaphore semaphore, HttpVersion httpVersion, boolean isSecure, + public BoundedChannelPool(Semaphore semaphore, HttpVersion httpVersion, List nodes, Bootstrap bootstrap, - ChannelPoolHandler channelPoolHandler, int retriesPerNode) { + ChannelPoolHandler channelPoolHandler, int retriesPerNode, + PoolKeySelectorType poolKeySelectorType) { this.semaphore = semaphore; this.httpVersion = httpVersion; - this.isSecure = isSecure; this.channelPoolhandler = channelPoolHandler; this.nodes = nodes; this.retriesPerNode = retriesPerNode; + switch (poolKeySelectorType) { + case RANDOM: + this.poolKeySelector = new RandomPoolKeySelector(); + break; + case ROUNDROBIN: + this.poolKeySelector = new RoundRobinKeySelector(); + break; + } this.lock = new ReentrantLock(); this.attributeKey = AttributeKey.valueOf("poolKey"); if (nodes == null || nodes.isEmpty()) { @@ -105,10 +117,6 @@ public class BoundedChannelPool implements Pool { return httpVersion; } - public boolean isSecure() { - return isSecure; - } - public AttributeKey getAttributeKey() { return attributeKey; } @@ -121,7 +129,7 @@ public class BoundedChannelPool implements Pool { for (int i = 0; i < channelCount; i++) { Channel channel = newConnection(); if (channel == null) { - throw new ConnectException("failed to prepare"); + throw new ConnectException("failed to prepare channels"); } K key = channel.attr(attributeKey).get(); if (channel.isActive()) { @@ -133,7 +141,7 @@ public class BoundedChannelPool implements Pool { channel.close(); } } - logger.log(Level.FINE,"prepared " + channelCount + " channels"); + logger.log(Level.FINE,"prepared " + channelCount + " channels: " + availableChannels); } @Override @@ -156,35 +164,7 @@ public class BoundedChannelPool implements Pool { } @Override - public int acquire(List channels, int maxCount) throws Exception { - int availableCount = semaphore.drainPermits(); - if (availableCount == 0) { - return availableCount; - } - if (availableCount > maxCount) { - semaphore.release(availableCount - maxCount); - availableCount = maxCount; - } - Channel channel; - for (int i = 0; i < availableCount; i ++) { - if ((channel = poll()) == null) { - channel = newConnection(); - } - if (channel == null) { - semaphore.release(availableCount - i); - throw new ConnectException(); - } else { - if (channelPoolhandler != null) { - channelPoolhandler.channelAcquired(channel); - } - channels.add(channel); - } - } - return availableCount; - } - - @Override - public void release(Channel channel) throws Exception { + public void release(Channel channel, boolean close) throws Exception { try { if (channel != null) { if (channel.isActive()) { @@ -193,10 +173,9 @@ public class BoundedChannelPool implements Pool { if (channelQueue != null) { channelQueue.add(channel); } - } else if (channel.isOpen()) { + } else if (channel.isOpen() && close) { + logger.log(Level.FINE, "trying to close channel " + channel); channel.close(); - } else { - logger.log(Level.WARNING, "channel not active or open while release"); } if (channelPoolhandler != null) { channelPoolhandler.channelReleased(channel); @@ -208,16 +187,10 @@ public class BoundedChannelPool implements Pool { } @Override - public void release(List channels) throws Exception { - for (Channel channel : channels) { - release(channel); - } - } - - @Override - public void close() { + public void close() throws IOException { lock.lock(); try { + logger.log(Level.FINE, "closing pool"); int count = 0; Set channelSet = new HashSet<>(); for (Map.Entry> entry : availableChannels.entrySet()) { @@ -228,7 +201,21 @@ public class BoundedChannelPool implements Pool { } for (Channel channel : channelSet) { if (channel != null && channel.isOpen()) { - logger.log(Level.FINE, "closing channel " + channel); + logger.log(Level.FINE, "trying to abort channel " + channel); + if (httpVersion.majorVersion() == 2) { + // be polite, send a go away frame + DefaultHttp2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(0); + ChannelPromise channelPromise = channel.newPromise(); + channel.writeAndFlush(goAwayFrame, channelPromise); + try { + channelPromise.get(); + logger.log(Level.FINE, "goaway frame sent to " + channel); + } catch (ExecutionException e) { + // ignore error if goaway can not be sent + } catch (InterruptedException e) { + throw new IOException(e); + } + } channel.close(); count++; } @@ -237,7 +224,7 @@ public class BoundedChannelPool implements Pool { channels.clear(); bootstraps.clear(); counts.clear(); - logger.log(Level.FINE, "closed " + count + " connections"); + logger.log(Level.FINE, "closed pool (found " + count + " connections open)"); } finally { lock.unlock(); } @@ -246,17 +233,13 @@ public class BoundedChannelPool implements Pool { private Channel newConnection() throws ConnectException { Channel channel = null; K key = null; - K nextKey; - int min = Integer.MAX_VALUE; - int next; - int i = ThreadLocalRandom.current().nextInt(numberOfNodes); - for (int j = i; j < numberOfNodes; j ++) { - nextKey = nodes.get(j % numberOfNodes); - if (counts == null) { - throw new ConnectException("strange"); - } + Integer min = Integer.MAX_VALUE; + Integer next; + //int r = ThreadLocalRandom.current().nextInt(numberOfNodes); + for (int j = 0; j < numberOfNodes; j++) { + K nextKey = poolKeySelector.key(); //nodes.get(j % numberOfNodes); next = counts.get(nextKey); - if (next == 0) { + if (next == null || next == 0) { key = nextKey; break; } else if (next < min) { @@ -305,7 +288,6 @@ public class BoundedChannelPool implements Pool { if (retriesPerNode > 0) { failedCounts.put(key, 0); } - logger.log(Level.FINE,"new connection to " + key + " created"); } return channel; } @@ -319,19 +301,11 @@ public class BoundedChannelPool implements Pool { } private Channel poll() { - int i = ThreadLocalRandom.current().nextInt(numberOfNodes); Queue channelQueue; Channel channel; - for(int j = i; j < i + numberOfNodes; j ++) { - K key = nodes.get(j % numberOfNodes); - // for HTTP/2, use channel list - logger.log(Level.FINE, "pool version = " + httpVersion); - if (httpVersion.majorVersion() == 2) { - List list = channels.get(key); - if (!list.isEmpty()) { - logger.log(Level.INFO, "we have a channel " + list); - } - } + //int r = ThreadLocalRandom.current().nextInt(numberOfNodes); + for (int j = 0; j < numberOfNodes; j++) { + K key = poolKeySelector.key(); //nodes.get(j % numberOfNodes); channelQueue = availableChannels.get(key); if (channelQueue != null) { channel = channelQueue.poll(); @@ -339,12 +313,39 @@ public class BoundedChannelPool implements Pool { return channel; } } else { - logger.log(Level.FINE, "channelqueue is null"); + logger.log(Level.WARNING, "channel queue is null?"); } } return null; } + public enum PoolKeySelectorType { + RANDOM, ROUNDROBIN + } + + private interface PoolKeySelector { + K key(); + } + + private class RandomPoolKeySelector implements PoolKeySelector { + + @Override + public K key() { + int r = ThreadLocalRandom.current().nextInt(numberOfNodes); + return nodes.get(r % numberOfNodes); + } + } + + private class RoundRobinKeySelector implements PoolKeySelector { + + int r = 0; + + @Override + public K key() { + return nodes.get(r++ % numberOfNodes); + } + } + private class CloseChannelListener implements ChannelFutureListener { private final K key; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/Pool.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/Pool.java index 041759c..a100dc2 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/Pool.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/Pool.java @@ -1,7 +1,6 @@ package org.xbib.netty.http.client.pool; import java.io.Closeable; -import java.util.List; public interface Pool extends Closeable { @@ -9,9 +8,5 @@ public interface Pool extends Closeable { T acquire() throws Exception; - int acquire(List list, int maxCount) throws Exception; - - void release(T t) throws Exception; - - void release(List list) throws Exception; + void release(T t, boolean close) throws Exception; } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/rest/RestClient.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/rest/RestClient.java index e5375ad..92f44d7 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/rest/RestClient.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/rest/RestClient.java @@ -13,12 +13,9 @@ import org.xbib.netty.http.client.transport.Transport; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.logging.Logger; public class RestClient { - private static final Logger logger = Logger.getLogger(RestClient.class.getName()); - private Client client; private Transport transport; @@ -70,6 +67,4 @@ public class RestClient { transport.execute(requestBuilder.build().setResponseListener(restClient::setResponse)).get(); return restClient; } - - } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/BackOff.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/BackOff.java index b346438..bd1ca97 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/BackOff.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/BackOff.java @@ -1,7 +1,5 @@ package org.xbib.netty.http.client.retry; -import java.io.IOException; - /** * Back-off policy when retrying an operation. */ @@ -20,6 +18,8 @@ public interface BackOff { * Gets the number of milliseconds to wait before retrying the operation or {@link #STOP} to * indicate that no retries should be made. * + * @return milliseconds before operation retry + * *

* Example usage: *

diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java index 8f20b8c..7a64ce6 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java @@ -185,6 +185,10 @@ public class ExponentialBackOff implements BackOff { /** * Returns a random value from the interval [randomizationFactor * currentInterval, * randomizationFactor * currentInterval]. + * @param randomizationFactor the randomization factor + * @param random scaling factor + * @param currentIntervalMillis milliseconds + * @return random value */ public static int getRandomValueFromInterval(double randomizationFactor, double random, int currentIntervalMillis) { double delta = randomizationFactor * currentIntervalMillis; @@ -196,14 +200,17 @@ public class ExponentialBackOff implements BackOff { return (int) (minInterval + (random * (maxInterval - minInterval + 1))); } - /** Returns the initial retry interval in milliseconds. */ + /** + * Returns the initial retry interval in milliseconds. + * @return interval milliseconds + */ public final int getInitialIntervalMillis() { return initialIntervalMillis; } /** * Returns the randomization factor to use for creating a range around the retry interval. - * + * @return randomization factor *

* A randomization factor of 0.5 results in a random period ranging between 50% below and 50% * above the retry interval. @@ -215,6 +222,7 @@ public class ExponentialBackOff implements BackOff { /** * Returns the current retry interval in milliseconds. + * @return current interval in milliseconds */ public final int getCurrentIntervalMillis() { return currentIntervalMillis; @@ -222,6 +230,7 @@ public class ExponentialBackOff implements BackOff { /** * Returns the value to multiply the current interval with for each retry attempt. + * @return multiplier */ public final double getMultiplier() { return multiplier; @@ -230,6 +239,7 @@ public class ExponentialBackOff implements BackOff { /** * Returns the maximum value of the back off period in milliseconds. Once the current interval * reaches this value it stops increasing. + * @return maximum interval value in milliseconds */ public final int getMaxIntervalMillis() { return maxIntervalMillis; @@ -237,7 +247,7 @@ public class ExponentialBackOff implements BackOff { /** * Returns the maximum elapsed time in milliseconds. - * + * @return maximum elapsed time in milliseconds *

* If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the * max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning @@ -251,7 +261,7 @@ public class ExponentialBackOff implements BackOff { /** * Returns the elapsed time in milliseconds since an {@link ExponentialBackOff} instance is * created and is reset when {@link #reset()} is called. - * + * @return the elapsed time in milliseconds *

* The elapsed time is computed using {@link System#nanoTime()}. *

@@ -286,6 +296,7 @@ public class ExponentialBackOff implements BackOff { /** * Returns the current value of the most precise available system timer, in nanoseconds for use to * measure elapsed time, to match the behavior of {@link System#nanoTime()}. + * @return value of timer in nanoseconds */ long nanoTime(); @@ -345,7 +356,8 @@ public class ExponentialBackOff implements BackOff { /** * Builds a new instance of {@link ExponentialBackOff}. - * */ + * @return an {@link ExponentialBackOff} instance + */ public ExponentialBackOff build() { if (initialIntervalMillis <= 0) { throw new IllegalArgumentException(); @@ -365,17 +377,11 @@ public class ExponentialBackOff implements BackOff { return new ExponentialBackOff(this); } - /** - * Returns the initial retry interval in milliseconds. The default value is - * {@link #DEFAULT_INITIAL_INTERVAL_MILLIS}. - */ - public final int getInitialIntervalMillis() { - return initialIntervalMillis; - } - /** * Sets the initial retry interval in milliseconds. The default value is * {@link #DEFAULT_INITIAL_INTERVAL_MILLIS}. Must be {@code > 0}. + * @param initialIntervalMillis interval milliseconds + * @return the builder * *

* Overriding is only supported for the purpose of calling the super implementation and changing @@ -387,28 +393,12 @@ public class ExponentialBackOff implements BackOff { return this; } - /** - * Returns the randomization factor to use for creating a range around the retry interval. The - * default value is {@link #DEFAULT_RANDOMIZATION_FACTOR}. - * - *

- * A randomization factor of 0.5 results in a random period ranging between 50% below and 50% - * above the retry interval. - *

- * - *

- * Overriding is only supported for the purpose of calling the super implementation and changing - * the return type, but nothing else. - *

- */ - public final double getRandomizationFactor() { - return randomizationFactor; - } - /** * Sets the randomization factor to use for creating a range around the retry interval. The * default value is {@link #DEFAULT_RANDOMIZATION_FACTOR}. Must fall in the range * {@code 0 <= randomizationFactor < 1}. + * @param randomizationFactor the randomization factor + * @return the builder * *

* A randomization factor of 0.5 results in a random period ranging between 50% below and 50% @@ -425,17 +415,11 @@ public class ExponentialBackOff implements BackOff { return this; } - /** - * Returns the value to multiply the current interval with for each retry attempt. The default - * value is {@link #DEFAULT_MULTIPLIER}. - */ - public final double getMultiplier() { - return multiplier; - } - /** * Sets the value to multiply the current interval with for each retry attempt. The default * value is {@link #DEFAULT_MULTIPLIER}. Must be {@code >= 1}. + * @param multiplier the multiplier + * @return the builder * *

* Overriding is only supported for the purpose of calling the super implementation and changing @@ -447,19 +431,12 @@ public class ExponentialBackOff implements BackOff { return this; } - /** - * Returns the maximum value of the back off period in milliseconds. Once the current interval - * reaches this value it stops increasing. The default value is - * {@link #DEFAULT_MAX_INTERVAL_MILLIS}. Must be {@code >= initialInterval}. - */ - public final int getMaxIntervalMillis() { - return maxIntervalMillis; - } - /** * Sets the maximum value of the back off period in milliseconds. Once the current interval * reaches this value it stops increasing. The default value is * {@link #DEFAULT_MAX_INTERVAL_MILLIS}. + * @param maxIntervalMillis maximum interval in miliseconds + * @return the builder * *

* Overriding is only supported for the purpose of calling the super implementation and changing @@ -471,23 +448,11 @@ public class ExponentialBackOff implements BackOff { return this; } - /** - * Returns the maximum elapsed time in milliseconds. The default value is - * {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}. - * - *

- * If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the - * max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning - * {@link BackOff#STOP}. The elapsed time can be reset by calling {@link #reset()}. - *

- */ - public final int getMaxElapsedTimeMillis() { - return maxElapsedTimeMillis; - } - /** * Sets the maximum elapsed time in milliseconds. The default value is * {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}. Must be {@code > 0}. + * @param maxElapsedTimeMillis maximum elapsed time millis + * @return the builder * *

* If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the @@ -505,16 +470,10 @@ public class ExponentialBackOff implements BackOff { return this; } - /** - * Returns the nano clock. - */ - public final NanoClock getNanoClock() { - return nanoClock; - } - /** * Sets the nano clock ({@link NanoClock#SYSTEM} by default). - * + * @param nanoClock the nano clock + * @return the builder *

* Overriding is only supported for the purpose of calling the super implementation and changing * the return type, but nothing else. diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java index fe9e9d9..cb38236 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java @@ -1,16 +1,10 @@ package org.xbib.netty.http.client.transport; import io.netty.channel.Channel; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.cookie.ClientCookieDecoder; -import io.netty.handler.codec.http.cookie.ClientCookieEncoder; import io.netty.handler.codec.http.cookie.Cookie; -import io.netty.handler.codec.http2.HttpConversionUtil; import org.xbib.net.PercentDecoder; import org.xbib.net.URL; import org.xbib.net.URLSyntaxException; @@ -18,8 +12,6 @@ import org.xbib.netty.http.client.Client; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.RequestBuilder; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.HttpHeadersListener; import org.xbib.netty.http.client.retry.BackOff; import java.io.IOException; @@ -27,15 +19,15 @@ import java.net.ConnectException; import java.nio.charset.MalformedInputException; import java.nio.charset.StandardCharsets; import java.nio.charset.UnmappableCharacterException; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; @@ -49,64 +41,26 @@ abstract class BaseTransport implements Transport { protected final HttpAddress httpAddress; - protected Channel channel; - - protected SortedMap requests; - protected Throwable throwable; + private static final Request DUMMY = Request.builder(HttpMethod.GET).build(); + + private final Map channels; + + final Map channelFlowMap; + + final SortedMap requests; + private Map cookieBox; BaseTransport(Client client, HttpAddress httpAddress) { this.client = client; this.httpAddress = httpAddress; + this.channels = new ConcurrentHashMap<>(); + this.channelFlowMap = new ConcurrentHashMap<>(); this.requests = new ConcurrentSkipListMap<>(); } - @Override - public Transport execute(Request request) throws IOException { - ensureConnect(); - if (throwable != null) { - return this; - } - // Some HTTP 1 servers do not understand URIs in HTTP command line in spite of RFC 7230. - // The "origin form" requires a "Host" header. - // Our algorithm is: use always "origin form" for HTTP 1, use absolute form for HTTP 2. - // The reason is that Netty derives the HTTP/2 scheme header from the absolute form. - String uri = request.httpVersion().majorVersion() == 1 ? - request.url().relativeReference() : request.url().toString(); - FullHttpRequest fullHttpRequest = request.content() == null ? - new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri) : - new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri, - request.content()); - Integer streamId = nextStream(); - if (streamId != null && streamId > 0) { - request.headers().set(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), Integer.toString(streamId)); - } else { - if (request.httpVersion().majorVersion() == 2) { - logger.log(Level.WARNING, "no streamId but HTTP/2 request. Strange!!! " + getClass().getName()); - } - } - // add matching cookies from box (previous requests) and new cookies from request builder - Collection cookies = new ArrayList<>(); - cookies.addAll(matchCookiesFromBox(request)); - cookies.addAll(matchCookies(request)); - if (!cookies.isEmpty()) { - request.headers().set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookies)); - } - // add stream-id and cookie headers - fullHttpRequest.headers().set(request.headers()); - if (streamId != null) { - requests.put(streamId, request); - } - // flush after putting request into requests map - if (channel.isWritable()) { - channel.writeAndFlush(fullHttpRequest); - - } - return this; - } - /** * Experimental method for executing in a wrapping completable future. * @param request request @@ -124,9 +78,8 @@ abstract class BaseTransport implements Transport { } @Override - public synchronized void close() throws IOException { + public synchronized void close() { get(); - client.releaseChannel(channel); } @Override @@ -139,53 +92,130 @@ abstract class BaseTransport implements Transport { return throwable; } + /** + * The underlying network layer failed, not possible to know the request. + * So we fail all (open) promises. + * @param throwable the exception + */ @Override - public void headersReceived(Integer streamId, HttpHeaders httpHeaders) { - Request request = fromStreamId(streamId); - if (request != null) { - HttpHeadersListener httpHeadersListener = request.getHeadersListener(); - if (httpHeadersListener != null) { - httpHeadersListener.onHeaders(httpHeaders); - } - for (String cookieString : httpHeaders.getAll(HttpHeaderNames.SET_COOKIE)) { - Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); - addCookie(cookie); - CookieListener cookieListener = request.getCookieListener(); - if (cookieListener != null) { - cookieListener.onCookie(cookie); - } - } + public void fail(Throwable throwable) { + // do not fail more than once + if (this.throwable != null) { + return; + } + logger.log(Level.SEVERE, "failing: " + throwable.getMessage(), throwable); + this.throwable = throwable; + for (Flow flow : channelFlowMap.values()) { + flow.fail(throwable); } } - private void ensureConnect() throws IOException { - if (channel == null) { - channel = client.newChannel(httpAddress); - if (channel != null) { - channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this); - awaitSettings(); + @Override + public Transport get() { + return get(client.getClientConfig().getReadTimeoutMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public Transport get(long value, TimeUnit timeUnit) { + for (Map.Entry entry : channelFlowMap.entrySet()) { + Flow flow = entry.getValue(); + for (Integer key : flow.keys()) { + try { + flow.get(key).get(value, timeUnit); + } catch (Exception e) { + String requestKey = getRequestKey(entry.getKey(), key); + Request request = requests.get(requestKey); + if (request != null && request.getCompletableFuture() != null) { + request.getCompletableFuture().completeExceptionally(e); + } + flow.fail(e); + } finally { + flow.remove(key); + } + } + flow.close(); + } + channels.values().forEach(channel -> { + try { + client.releaseChannel(channel, true); + } catch (IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + }); + channelFlowMap.clear(); + channels.clear(); + requests.clear(); + return this; + } + + @Override + public void cancel() { + for (Map.Entry entry : channelFlowMap.entrySet()) { + Flow flow = entry.getValue(); + for (Integer key : flow.keys()) { + try { + flow.get(key).cancel(true); + } catch (Exception e) { + String requestKey = getRequestKey(entry.getKey(), key); + Request request = requests.get(requestKey); + if (request != null && request.getCompletableFuture() != null) { + request.getCompletableFuture().completeExceptionally(e); + } + flow.fail(e); + } finally { + flow.remove(key); + } + } + flow.close(); + } + channels.values().forEach(channel -> { + try { + client.releaseChannel(channel, true); + } catch (IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + }); + channelFlowMap.clear(); + channels.clear(); + requests.clear(); + } + + protected abstract String getRequestKey(String channelId, Integer streamId); + + Channel mapChannel(Request request) throws IOException { + Channel channel; + if (!client.hasPooledConnections()) { + channel = channels.get(DUMMY); + if (channel == null) { + channel = switchNextChannel(); + } + channels.put(DUMMY, channel); + } else { + channel = switchNextChannel(); + channels.put(request, channel); + } + return channel; + } + + private Channel switchNextChannel() throws IOException { + Channel channel = client.newChannel(httpAddress); + if (channel != null) { + channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this); + waitForSettings(); + } else { + ConnectException connectException; + if (httpAddress != null) { + connectException = new ConnectException("unable to connect to " + httpAddress); + } else if (client.hasPooledConnections()) { + connectException = new ConnectException("unable to get channel from pool"); } else { - ConnectException connectException; - if (httpAddress != null) { - connectException = new ConnectException("unable to connect to " + httpAddress); - } else if (client.hasPooledConnections()){ - connectException = new ConnectException("unable to get channel from pool"); - } else { - // if API misuse - connectException = new ConnectException("unable to get channel"); - } - this.throwable = connectException; - this.channel = null; - throw connectException; + // API misuse + connectException = new ConnectException("unable to get channel"); } + this.throwable = connectException; + throw connectException; } - } - - protected Request fromStreamId(Integer streamId) { - if (streamId == null) { - streamId = requests.lastKey(); - } - return requests.get(streamId); + return channel; } protected Request continuation(Request request, FullHttpResponse httpResponse) throws URLSyntaxException { @@ -224,7 +254,6 @@ abstract class BaseTransport implements Transport { request.cookies().forEach(newHttpRequestBuilder::addCookie); Request newHttpRequest = newHttpRequestBuilder.build(); newHttpRequest.setResponseListener(request.getResponseListener()); - newHttpRequest.setHeadersListener(request.getHeadersListener()); newHttpRequest.setCookieListener(request.getCookieListener()); StringBuilder hostAndPort = new StringBuilder(); hostAndPort.append(redirUrl.getHost()); @@ -235,6 +264,7 @@ abstract class BaseTransport implements Transport { logger.log(Level.FINE, "redirect url: " + redirUrl + " old request: " + request.toString() + " new request: " + newHttpRequest.toString()); + request.release(); return newHttpRequest; } break; @@ -297,20 +327,20 @@ abstract class BaseTransport implements Transport { return cookieBox; } - private void addCookie(Cookie cookie) { + void addCookie(Cookie cookie) { if (cookieBox == null) { this.cookieBox = Collections.synchronizedMap(new LRUCache(32)); } cookieBox.put(cookie, true); } - private List matchCookiesFromBox(Request request) { + List matchCookiesFromBox(Request request) { return cookieBox == null ? Collections.emptyList() : cookieBox.keySet().stream().filter(cookie -> matchCookie(request.url(), cookie) ).collect(Collectors.toList()); } - private List matchCookies(Request request) { + List matchCookies(Request request) { return request.cookies().stream().filter(cookie -> matchCookie(request.url(), cookie) ).collect(Collectors.toList()); diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Flow.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Flow.java new file mode 100644 index 0000000..6147e31 --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Flow.java @@ -0,0 +1,72 @@ +package org.xbib.netty.http.client.transport; + +import java.util.Set; +import java.util.SortedMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.atomic.AtomicInteger; + +class Flow { + + private final AtomicInteger counter; + + private final SortedMap> map; + + Flow() { + this.counter = new AtomicInteger(3); + this.map = new ConcurrentSkipListMap<>(); + } + + CompletableFuture get(Integer key) { + return map.get(key); + } + + Set keys() { + return map.keySet(); + } + + Integer firstKey() { + return map.firstKey(); + } + + Integer lastKey() { + return map.lastKey(); + } + + void put(Integer key, CompletableFuture promise) { + map.put(key, promise); + } + + void remove(Integer key) { + if (key != null) { + map.remove(key); + } + } + + Integer nextStreamId() { + Integer streamId = counter.getAndAdd(2); + if (streamId == Integer.MIN_VALUE) { + // reset if overflow, Java wraps atomic integers to Integer.MIN_VALUE + // should we send a GOAWAY? + counter.set(3); + streamId = 3; + } + map.put(streamId, new CompletableFuture<>()); + return streamId; + } + + void fail(Throwable throwable) { + for (CompletableFuture promise : map.values()) { + promise.completeExceptionally(throwable); + } + } + + public void close() { + map.clear(); + } + + @Override + public String toString() { + return "[next=" + counter + ", " + map + "]"; + } +} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java deleted file mode 100644 index 1da4eb9..0000000 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.xbib.netty.http.client.transport; - -import io.netty.channel.Channel; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.Http2Settings; -import org.xbib.net.URLSyntaxException; -import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.HttpResponseListener; - -import java.io.IOException; -import java.util.SortedMap; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class Http1Transport extends BaseTransport { - - private static final Logger logger = Logger.getLogger(Http1Transport.class.getName()); - - private final AtomicInteger sequentialCounter; - - private SortedMap> sequentialPromiseMap; - - public Http1Transport(Client client, HttpAddress httpAddress) { - super(client, httpAddress); - this.sequentialCounter = new AtomicInteger(); - this.sequentialPromiseMap = new ConcurrentSkipListMap<>(); - } - - @Override - public Integer nextStream() { - Integer streamId = sequentialCounter.getAndIncrement(); - if (streamId == Integer.MIN_VALUE) { - // reset if overflow, Java wraps atomic integers to Integer.MIN_VALUE - sequentialCounter.set(0); - streamId = 0; - } - sequentialPromiseMap.put(streamId, new CompletableFuture<>()); - return streamId; - } - - @Override - public void settingsReceived(Channel channel, Http2Settings http2Settings) { - } - - @Override - public void awaitSettings() { - } - - @Override - public void responseReceived(Integer streamId, FullHttpResponse fullHttpResponse) { - Request request = fromStreamId(streamId); - if (request != null) { - HttpResponseListener responseListener = request.getResponseListener(); - if (responseListener != null) { - responseListener.onResponse(fullHttpResponse); - } - } - try { - Request retryRequest = retry(request, fullHttpResponse); - if (retryRequest != null) { - // retry transport, wait for completion - client.retry(this, retryRequest); - } else { - Request continueRequest = continuation(request, fullHttpResponse); - if (continueRequest != null) { - // continue with new transport, synchronous call here, wait for completion - client.continuation(this, continueRequest); - } - } - } catch (URLSyntaxException | IOException e) { - logger.log(Level.WARNING, e.getMessage(), e); - } - if (!sequentialPromiseMap.isEmpty()) { - CompletableFuture promise = sequentialPromiseMap.get(sequentialPromiseMap.firstKey()); - if (promise != null) { - promise.complete(true); - } - } - } - - @Override - public void pushPromiseReceived(Integer streamId, Integer promisedStreamId, Http2Headers headers) { - } - - @Override - public void awaitResponse(Integer streamId) throws IOException, TimeoutException { - if (streamId == null) { - return; - } - if (throwable != null) { - return; - } - CompletableFuture promise = sequentialPromiseMap.get(streamId); - if (promise != null) { - long millis = client.getClientConfig().getReadTimeoutMillis(); - Request request = fromStreamId(streamId); - if (request != null && request.getTimeoutInMillis() > 0) { - millis = request.getTimeoutInMillis(); - } - try { - promise.get(millis, TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { - this.throwable = e; - throw new TimeoutException("timeout of " + millis + " milliseconds exceeded"); - } catch (InterruptedException | ExecutionException e) { - this.throwable = e; - throw new IOException(e); - } finally { - sequentialPromiseMap.remove(streamId); - } - } - } - - @Override - public Transport get() { - try { - for (Integer streamId : sequentialPromiseMap.keySet()) { - awaitResponse(streamId); - client.releaseChannel(channel); - } - } catch (IOException | TimeoutException e) { - logger.log(Level.WARNING, e.getMessage(), e); - } finally { - sequentialPromiseMap.clear(); - } - return this; - } - - @Override - public void success() { - for (CompletableFuture promise : sequentialPromiseMap.values()) { - promise.complete(true); - } - } - - @Override - public void fail(Throwable throwable) { - this.throwable = throwable; - for (CompletableFuture promise : sequentialPromiseMap.values()) { - promise.completeExceptionally(throwable); - } - } -} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java index 0fe1ae1..b066a71 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java @@ -1,23 +1,38 @@ package org.xbib.netty.http.client.transport; import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http2.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; +import io.netty.handler.codec.http2.HttpConversionUtil; import org.xbib.net.URLSyntaxException; import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler; +import org.xbib.netty.http.client.handler.http2.Http2StreamFrameToHttpObjectCodec; +import org.xbib.netty.http.client.listener.CookieListener; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.listener.HttpResponseListener; +import org.xbib.netty.http.client.listener.ResponseListener; import java.io.IOException; -import java.util.SortedMap; +import java.util.ArrayList; +import java.util.Collection; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -27,33 +42,76 @@ public class Http2Transport extends BaseTransport { private CompletableFuture settingsPromise; - private final AtomicInteger streamIdCounter; - - private SortedMap> streamidPromiseMap; + private final ChannelInitializer initializer; public Http2Transport(Client client, HttpAddress httpAddress) { super(client, httpAddress); - streamIdCounter = new AtomicInteger(3); - streamidPromiseMap = new ConcurrentSkipListMap<>(); - settingsPromise = (httpAddress != null /*&& httpAddress.isSecure() */) || - (client.hasPooledConnections() && client.getPool().isSecure()) ? - new CompletableFuture<>() : null; + this.settingsPromise = httpAddress != null ? new CompletableFuture<>() : null; + final Transport transport = this; + this.initializer = new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ch.attr(TRANSPORT_ATTRIBUTE_KEY).set(transport); + ChannelPipeline p = ch.pipeline(); + p.addLast("child-client-frame-converter", + new Http2StreamFrameToHttpObjectCodec(false)); + p.addLast("child-client-chunk-aggregator", + new HttpObjectAggregator(client.getClientConfig().getMaxContentLength())); + p.addLast("child-client-response-handler", + new Http2ResponseHandler()); + } + }; } @Override - public Integer nextStream() { - Integer streamId = streamIdCounter.getAndAdd(2); - if (streamId == Integer.MIN_VALUE) { - // reset if overflow, Java wraps atomic integers to Integer.MIN_VALUE - streamIdCounter.set(3); - streamId = 3; + public Transport execute(Request request) throws IOException { + Channel channel = mapChannel(request); + if (throwable != null) { + return this; } - streamidPromiseMap.put(streamId, new CompletableFuture<>()); - return streamId; + final String channelId = channel.id().toString(); + channelFlowMap.putIfAbsent(channelId, new Flow()); + Http2StreamChannel childChannel = new Http2StreamChannelBootstrap(channel) + .handler(initializer).open().syncUninterruptibly().getNow(); + String authority = request.url().getHost() + (request.url().getPort() != null ? ":" + request.url().getPort() : ""); + String path = request.url().getPath() != null && !request.url().getPath().isEmpty() ? + request.url().getPath() : "/"; + Http2Headers http2Headers = new DefaultHttp2Headers() + .method(request.httpMethod().asciiName()) + .scheme(request.url().getScheme()) + .authority(authority) + .path(path); + final Integer streamId = channelFlowMap.get(channelId).nextStreamId(); + if (streamId == null) { + throw new IllegalStateException(); + } + requests.put(getRequestKey(channelId, streamId), request); + http2Headers.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId); + // add matching cookies from box (previous requests) and new cookies from request builder + Collection cookies = new ArrayList<>(); + cookies.addAll(matchCookiesFromBox(request)); + cookies.addAll(matchCookies(request)); + if (!cookies.isEmpty()) { + request.headers().set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookies)); + } + // add stream-id and cookie headers + HttpConversionUtil.toHttp2Headers(request.headers(), http2Headers); + boolean hasContent = request.content() != null && request.content().readableBytes() > 0; + DefaultHttp2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(http2Headers, !hasContent); + childChannel.write(headersFrame); + if (hasContent) { + DefaultHttp2DataFrame dataFrame = new DefaultHttp2DataFrame(request.content(), true); + childChannel.write(dataFrame); + } + childChannel.flush(); + if (client.hasPooledConnections()) { + client.releaseChannel(channel, false); + } + return this; } @Override - public void settingsReceived(Channel channel, Http2Settings http2Settings) { + public void settingsReceived(Http2Settings http2Settings) { if (settingsPromise != null) { settingsPromise.complete(true); } else { @@ -62,138 +120,88 @@ public class Http2Transport extends BaseTransport { } @Override - public void awaitSettings() { + public void waitForSettings() { if (settingsPromise != null) { try { - logger.log(Level.FINE, "waiting for settings"); settingsPromise.get(client.getClientConfig().getReadTimeoutMillis(), TimeUnit.MILLISECONDS); } catch (TimeoutException e) { - logger.log(Level.WARNING, "settings timeout"); + logger.log(Level.WARNING, "timeout in client while waiting for settings"); settingsPromise.completeExceptionally(e); } catch (InterruptedException | ExecutionException e) { settingsPromise.completeExceptionally(e); } - } else { - logger.log(Level.WARNING, "settings promise is null"); } } @Override - public void responseReceived(Integer streamId, FullHttpResponse fullHttpResponse) { + public void responseReceived(Channel channel, Integer streamId, FullHttpResponse fullHttpResponse) { + if (throwable != null) { + logger.log(Level.WARNING, "throwable not null for response " + fullHttpResponse, throwable); + return; + } if (streamId == null) { - logger.log(Level.WARNING, "no stream ID, unexpected message received: " + fullHttpResponse); + logger.log(Level.WARNING, "stream ID is null for response " + fullHttpResponse); return; } - CompletableFuture promise = streamidPromiseMap.get(streamId); - if (promise == null) { - logger.log(Level.WARNING, "response received for stream ID " + streamId + " but found no promise"); + // format of childchan channel ID is "/" + String channelId = channel.id().toString(); + int pos = channelId.indexOf('/'); + channelId = pos > 0 ? channelId.substring(0, pos) : channelId; + Flow flow = channelFlowMap.get(channelId); + if (flow == null) { return; } - Request request = fromStreamId(streamId); - if (request != null) { - HttpResponseListener responseListener = request.getResponseListener(); - if (responseListener != null) { - responseListener.onResponse(fullHttpResponse); - } - try { - Request retryRequest = retry(request, fullHttpResponse); - if (retryRequest != null) { - // retry transport, wait for completion - client.retry(this, retryRequest); - } else { - Request continueRequest = continuation(request, fullHttpResponse); - if (continueRequest != null) { - // continue with new transport, synchronous call here, wait for completion - client.continuation(this, continueRequest); + String requestKey = getRequestKey(channelId, streamId); + CompletableFuture promise = flow.get(streamId); + if (promise != null) { + Request request = requests.get(requestKey); + if (request == null) { + promise.completeExceptionally(new IllegalStateException()); + } else { + for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) { + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + addCookie(cookie); + CookieListener cookieListener = request.getCookieListener(); + if (cookieListener != null) { + cookieListener.onCookie(cookie); } } - } catch (URLSyntaxException | IOException e) { - logger.log(Level.WARNING, e.getMessage(), e); - } - } - promise.complete(true); - } - - @Override - public void pushPromiseReceived(Integer streamId, Integer promisedStreamId, Http2Headers headers) { - streamidPromiseMap.put(promisedStreamId, new CompletableFuture<>()); - requests.put(promisedStreamId, fromStreamId(streamId)); - } - - @Override - public void awaitResponse(Integer streamId) throws IOException { - if (streamId == null) { - return; - } - if (throwable != null) { - return; - } - CompletableFuture promise = streamidPromiseMap.get(streamId); - if (promise != null) { - try { - long millis = client.getClientConfig().getReadTimeoutMillis(); - Request request = fromStreamId(streamId); - if (request != null && request.getTimeoutInMillis() > 0) { - millis = request.getTimeoutInMillis(); + ResponseListener responseListener = request.getResponseListener(); + if (responseListener != null) { + responseListener.onResponse(fullHttpResponse); + } + try { + Request retryRequest = retry(request, fullHttpResponse); + if (retryRequest != null) { + // retry transport, wait for completion + client.retry(this, retryRequest); + } else { + Request continueRequest = continuation(request, fullHttpResponse); + if (continueRequest != null) { + // continue with new transport, synchronous call here, wait for completion + client.continuation(this, continueRequest); + } + } + promise.complete(true); + } catch (URLSyntaxException | IOException e) { + promise.completeExceptionally(e); } - promise.get(millis, TimeUnit.MILLISECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - this.throwable = e; - throw new IOException(e); - } finally { - streamidPromiseMap.remove(streamId); } } + channelFlowMap.get(channelId).remove(streamId); + requests.remove(requestKey); } @Override - public Transport get() { - for (Integer streamId : streamidPromiseMap.keySet()) { - try { - awaitResponse(streamId); - } catch (IOException e) { - notifyRequest(streamId, e); - } - } - if (throwable != null) { - streamidPromiseMap.clear(); - } - return this; + public void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers) { + String channelId = channel.id().toString(); + channelFlowMap.get(channelId).put(promisedStreamId, new CompletableFuture<>()); + String requestKey = getRequestKey(channel.id().toString(), promisedStreamId); + requests.put(requestKey, requests.get(requestKey)); } @Override - public void success() { - for (CompletableFuture promise : streamidPromiseMap.values()) { - promise.complete(true); - } - } - - /** - * The underlying network layer failed, not possible to know the request. - * So we fail all (open) promises. - * @param throwable the exception - */ - @Override - public void fail(Throwable throwable) { - // fail fast, do not fail more than once - if (this.throwable != null) { - return; - } - this.throwable = throwable; - for (CompletableFuture promise : streamidPromiseMap.values()) { - promise.completeExceptionally(throwable); - } - } - - /** - * Try to notify request about failure. - * @param streamId stream ID - * @param throwable the exception - */ - private void notifyRequest(Integer streamId, Throwable throwable) { - Request request = fromStreamId(streamId); - if (request != null && request.getCompletableFuture() != null) { - request.getCompletableFuture().completeExceptionally(throwable); - } + protected String getRequestKey(String channelId, Integer streamId) { + return channelId + "#" + streamId; } } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java new file mode 100644 index 0000000..982ce85 --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java @@ -0,0 +1,141 @@ +package org.xbib.netty.http.client.transport; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.HttpConversionUtil; +import org.xbib.net.URLSyntaxException; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.listener.CookieListener; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.listener.ResponseListener; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class HttpTransport extends BaseTransport { + + private static final Logger logger = Logger.getLogger(HttpTransport.class.getName()); + + public HttpTransport(Client client, HttpAddress httpAddress) { + super(client, httpAddress); + } + + @Override + public Transport execute(Request request) throws IOException { + Channel channel = mapChannel(request); + if (throwable != null) { + return this; + } + final String channelId = channel.id().toString(); + channelFlowMap.putIfAbsent(channelId, new Flow()); + // Some HTTP 1 servers do not understand URIs in HTTP command line in spite of RFC 7230. + // The "origin form" requires a "Host" header. + // Our algorithm is: use always "origin form" for HTTP 1, use absolute form for HTTP 2. + // The reason is that Netty derives the HTTP/2 scheme header from the absolute form. + String uri = request.httpVersion().majorVersion() == 1 ? + request.url().relativeReference() : request.url().toString(); + FullHttpRequest fullHttpRequest = request.content() == null ? + new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri) : + new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri, request.content()); + final Integer streamId = channelFlowMap.get(channelId).nextStreamId(); + if (streamId == null) { + throw new IllegalStateException(); + } + String requestKey = channelId + "#" + streamId; + requests.put(requestKey, request); + // do we need the stream ID here in HTTP 1 header? + request.headers().set(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), Integer.toString(streamId)); + // add matching cookies from box (previous requests) and new cookies from request builder + Collection cookies = new ArrayList<>(); + cookies.addAll(matchCookiesFromBox(request)); + cookies.addAll(matchCookies(request)); + if (!cookies.isEmpty()) { + request.headers().set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookies)); + } + // add stream-id and cookie headers + fullHttpRequest.headers().set(request.headers()); + // flush after putting request into requests map + if (channel.isWritable()) { + channel.writeAndFlush(fullHttpRequest); + } + return this; + } + + @Override + public void responseReceived(Channel channel, Integer streamId, FullHttpResponse fullHttpResponse) { + if (throwable != null) { + logger.log(Level.WARNING, "throwable not null for response " + fullHttpResponse, throwable); + return; + } + // streamID is expected to be null, last request on memory is expected to be current, remove request from memory + Request request = requests.remove(requests.lastKey()); + if (request != null) { + for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) { + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + addCookie(cookie); + CookieListener cookieListener = request.getCookieListener(); + if (cookieListener != null) { + cookieListener.onCookie(cookie); + } + } + ResponseListener responseListener = request.getResponseListener(); + if (responseListener != null) { + responseListener.onResponse(fullHttpResponse); + } + } + try { + Request retryRequest = retry(request, fullHttpResponse); + if (retryRequest != null) { + // retry transport, wait for completion + client.retry(this, retryRequest); + } else { + Request continueRequest = continuation(request, fullHttpResponse); + if (continueRequest != null) { + // continue with new transport, synchronous call here, wait for completion + client.continuation(this, continueRequest); + } + } + } catch (URLSyntaxException | IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + String channelId = channel.id().toString(); + Flow flow = channelFlowMap.get(channelId); + if (flow == null) { + return; + } + CompletableFuture promise = flow.get(flow.lastKey()); + if (promise != null) { + promise.complete(true); + } + } + + @Override + public void settingsReceived(Http2Settings http2Settings) { + } + + @Override + public void waitForSettings() { + } + + @Override + public void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers) { + } + + @Override + protected String getRequestKey(String channelId, Integer streamId) { + return requests.lastKey(); + } +} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java index 2d3d395..3888871 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java @@ -2,8 +2,8 @@ package org.xbib.netty.http.client.transport; import io.netty.channel.Channel; import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Settings; import io.netty.util.AttributeKey; @@ -12,7 +12,7 @@ import org.xbib.netty.http.client.Request; import java.io.IOException; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; import java.util.function.Function; public interface Transport { @@ -23,27 +23,23 @@ public interface Transport { CompletableFuture execute(Request request, Function supplier) throws IOException; - Integer nextStream(); + void waitForSettings(); - void settingsReceived(Channel channel, Http2Settings http2Settings); + void settingsReceived(Http2Settings http2Settings) throws IOException; - void awaitSettings(); + void responseReceived(Channel channel, Integer streamId, FullHttpResponse fullHttpResponse) throws IOException; + + void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers) throws Http2Exception; void setCookieBox(Map cookieBox); Map getCookieBox(); - void responseReceived(Integer streamId, FullHttpResponse fullHttpResponse); - - void headersReceived(Integer streamId, HttpHeaders httpHeaders); - - void pushPromiseReceived(Integer streamId, Integer promisedStreamId, Http2Headers headers); - - void awaitResponse(Integer streamId) throws IOException, TimeoutException; - Transport get(); - void success(); + Transport get(long value, TimeUnit timeUnit); + + void cancel(); void fail(Throwable throwable); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/LoggingBase.java b/netty-http-client/src/test/java/org/xbib/TestBase.java similarity index 62% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/LoggingBase.java rename to netty-http-client/src/test/java/org/xbib/TestBase.java index e734d15..1449c1e 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/LoggingBase.java +++ b/netty-http-client/src/test/java/org/xbib/TestBase.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.test; +package org.xbib; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; @@ -7,20 +7,25 @@ import java.util.logging.LogManager; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; -public class LoggingBase { +public class TestBase { static { + System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); + System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); + //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); + //System.setProperty("io.netty.leakDetection.level", "paranoid"); + System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n"); LogManager.getLogManager().reset(); Logger rootLogger = LogManager.getLogManager().getLogger(""); Handler handler = new ConsoleHandler(); handler.setFormatter(new SimpleFormatter()); rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); + rootLogger.setLevel(Level.FINE); for (Handler h : rootLogger.getHandlers()) { handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); + h.setLevel(Level.FINE); } } } diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java index e4d2edc..253e122 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java @@ -32,7 +32,7 @@ public class CompletableFutureTest { .exceptionally(Throwable::getMessage) .thenCompose(content -> { logger.log(Level.INFO, content); - // POST is not allowed, we don't care + // POST is not allowed, will give a 405. We don't care try { return client.execute(Request.post() .url("http://google.com/") diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java index 3c0b287..92f4c7a 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java @@ -1,8 +1,8 @@ package org.xbib.netty.http.client.test; import org.conscrypt.Conscrypt; -import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; @@ -11,7 +11,7 @@ import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; -public class ConscryptTest extends LoggingBase { +public class ConscryptTest extends TestBase { private static final Logger logger = Logger.getLogger(""); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java index 5f2e788..1065076 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java @@ -1,6 +1,7 @@ package org.xbib.netty.http.client.test; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; @@ -11,7 +12,7 @@ import java.util.logging.Logger; /** */ -public class CookieSetterHttpBinTest extends LoggingBase { +public class CookieSetterHttpBinTest extends TestBase { private static final Logger logger = Logger.getLogger(CookieSetterHttpBinTest.class.getName()); @@ -36,7 +37,6 @@ public class CookieSetterHttpBinTest extends LoggingBase { .url("http://httpbin.org/cookies/set?name=value") .build() .setCookieListener(cookie -> logger.log(Level.INFO, "this is the cookie: " + cookie.toString())) - .setHeadersListener(headers -> logger.log(Level.INFO, "headers = " + headers.entries().toString())) .setResponseListener(fullHttpResponse -> { String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java index e4055d9..5570e23 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java @@ -2,6 +2,7 @@ package org.xbib.netty.http.client.test; import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.client.Request; @@ -20,7 +21,7 @@ import java.util.logging.Logger; import static org.junit.Assert.assertEquals; @Ignore -public class ElasticsearchTest extends LoggingBase { +public class ElasticsearchTest extends TestBase { private static final Logger logger = Logger.getLogger(ElasticsearchTest.class.getName()); @@ -103,7 +104,7 @@ public class ElasticsearchTest extends LoggingBase { } try { for (int i = 0; i < max; i++) { - client.pooledExecute(queries.get(i)).get(); + client.newTransport().execute(queries.get(i)).get(); } } catch (IOException e) { logger.log(Level.WARNING, e.getMessage(), e); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http1Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http1Test.java index 18a3853..09a9eeb 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http1Test.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http1Test.java @@ -2,6 +2,7 @@ package org.xbib.netty.http.client.test; import io.netty.handler.codec.http.HttpMethod; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; @@ -10,7 +11,7 @@ import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; -public class Http1Test extends LoggingBase { +public class Http1Test extends TestBase { private static final Logger logger = Logger.getLogger(Http1Test.class.getName()); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http2Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http2Test.java index 4b45574..3569ebf 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http2Test.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http2Test.java @@ -3,6 +3,7 @@ package org.xbib.netty.http.client.test; import io.netty.handler.codec.http.HttpMethod; import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; @@ -11,7 +12,7 @@ import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; -public class Http2Test extends LoggingBase { +public class Http2Test extends TestBase { private static final Logger logger = Logger.getLogger(Http2Test.class.getName()); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttp1Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttp1Test.java index f3c40ac..a31b560 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttp1Test.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttp1Test.java @@ -3,6 +3,7 @@ package org.xbib.netty.http.client.test; import io.netty.handler.codec.http.HttpMethod; import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; @@ -11,7 +12,7 @@ import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; -public class SecureHttp1Test extends LoggingBase { +public class SecureHttp1Test extends TestBase { private static final Logger logger = Logger.getLogger(SecureHttp1Test.class.getName()); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/LeakTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ThreadLeakTest.java similarity index 80% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/LeakTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/ThreadLeakTest.java index fefb811..0e70876 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/LeakTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ThreadLeakTest.java @@ -2,6 +2,7 @@ package org.xbib.netty.http.client.test; import org.junit.After; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import java.io.IOException; @@ -9,9 +10,15 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -public class LeakTest { +public class ThreadLeakTest extends TestBase { - private static final Logger logger = Logger.getLogger(LeakTest.class.getName()); + private static final Logger logger = Logger.getLogger(ThreadLeakTest.class.getName()); + + @Test + public void testForLeaks() throws IOException { + Client client = new Client(); + client.shutdownGracefully(); + } @After public void checkThreads() { @@ -19,10 +26,4 @@ public class LeakTest { logger.log(Level.INFO, "threads = " + threadSet.size() ); threadSet.forEach( thread -> logger.log(Level.INFO, thread.toString())); } - - @Test - public void testForLeaks() throws IOException { - Client client = new Client(); - client.shutdownGracefully(); - } } diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/XbibTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/XbibTest.java index cb011cb..5c3b2a8 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/XbibTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/XbibTest.java @@ -3,6 +3,7 @@ package org.xbib.netty.http.client.test; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.proxy.HttpProxyHandler; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; @@ -14,7 +15,7 @@ import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; -public class XbibTest extends LoggingBase { +public class XbibTest extends TestBase { private static final Logger logger = Logger.getLogger(""); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java index 73c19c4..72fc753 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java @@ -1,15 +1,18 @@ package org.xbib.netty.http.client.test.pool; import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.socket.SocketChannel; @@ -25,12 +28,14 @@ import org.xbib.netty.http.client.pool.BoundedChannelPool; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import java.io.Closeable; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; import java.util.logging.Level; import java.util.logging.Logger; @@ -76,8 +81,8 @@ public class EpollTest { .option(ChannelOption.SO_KEEPALIVE, true) .option(ChannelOption.SO_REUSEADDR, true) .option(ChannelOption.TCP_NODELAY, true); - channelPool = new BoundedChannelPool<>(semaphore, HttpVersion.HTTP_1_1,false, - NODES, bootstrap, null, 0); + channelPool = new BoundedChannelPool<>(semaphore, HttpVersion.HTTP_1_1, + NODES, bootstrap, null, 0, BoundedChannelPool.PoolKeySelectorType.ROUNDROBIN); channelPool.prepare(CONCURRENCY); } @@ -101,7 +106,7 @@ public class EpollTest { Thread.sleep(1); // very short? } channel.writeAndFlush(PAYLOAD.retain()).sync(); - channelPool.release(channel); + channelPool.release(channel, false); longAdder.increment(); } catch (InterruptedException e) { break; @@ -131,4 +136,48 @@ public class EpollTest { } } + class MockEpollServer implements Closeable { + + private final EventLoopGroup dispatchGroup; + + private final EventLoopGroup workerGroup; + + private final ChannelFuture bindFuture; + + private final AtomicLong reqCounter; + + public MockEpollServer(int port, int dropEveryRequest) throws InterruptedException { + dispatchGroup = new EpollEventLoopGroup(); + workerGroup = new EpollEventLoopGroup(); + reqCounter = new AtomicLong(0); + ServerBootstrap bootstrap = new ServerBootstrap() + .group(dispatchGroup, workerGroup) + .channel(EpollServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + if (dropEveryRequest > 0) { + ch.pipeline().addLast(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) { + if (reqCounter.incrementAndGet() % dropEveryRequest == 0) { + Channel channel = ctx.channel(); + logger.log(Level.INFO,"dropping the connection " + channel); + channel.close(); + } + } + }); + } + } + }); + bindFuture = bootstrap.bind(port).sync(); + } + + @Override + public void close() { + bindFuture.channel().close(); + workerGroup.shutdownGracefully(); + dispatchGroup.shutdownGracefully(); + } + } } diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockEpollServer.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockEpollServer.java deleted file mode 100644 index 04c3891..0000000 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockEpollServer.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.xbib.netty.http.client.test.pool; - -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.epoll.EpollEventLoopGroup; -import io.netty.channel.epoll.EpollServerSocketChannel; -import io.netty.channel.socket.SocketChannel; - -import java.io.Closeable; -import java.util.concurrent.atomic.AtomicLong; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class MockEpollServer implements Closeable { - - private static final Logger logger = Logger.getLogger(MockEpollServer.class.getName()); - - private final EventLoopGroup dispatchGroup; - - private final EventLoopGroup workerGroup; - - private final ChannelFuture bindFuture; - - private final AtomicLong reqCounter; - - public MockEpollServer(int port, int dropEveryRequest) throws InterruptedException { - dispatchGroup = new EpollEventLoopGroup(); - workerGroup = new EpollEventLoopGroup(); - reqCounter = new AtomicLong(0); - ServerBootstrap bootstrap = new ServerBootstrap() - .group(dispatchGroup, workerGroup) - .channel(EpollServerSocketChannel.class) - .childHandler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) { - if (dropEveryRequest > 0) { - ch.pipeline().addLast(new SimpleChannelInboundHandler() { - @Override - protected void channelRead0(ChannelHandlerContext ctx, Object msg) { - if (reqCounter.incrementAndGet() % dropEveryRequest == 0) { - Channel channel = ctx.channel(); - logger.log(Level.INFO,"dropping the connection " + channel); - channel.close(); - } - } - }); - } - } - }); - bindFuture = bootstrap.bind(port).sync(); - } - - @Override - public void close() { - bindFuture.channel().close(); - workerGroup.shutdownGracefully(); - dispatchGroup.shutdownGracefully(); - } -} diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockNioServer.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockNioServer.java deleted file mode 100644 index 3bc5a82..0000000 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockNioServer.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.xbib.netty.http.client.test.pool; - -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; - -import java.io.Closeable; -import java.util.concurrent.atomic.AtomicLong; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class MockNioServer implements Closeable { - - private static final Logger logger = Logger.getLogger(MockNioServer.class.getName()); - - private final EventLoopGroup dispatchGroup; - - private final EventLoopGroup workerGroup; - - private final ChannelFuture bindFuture; - - private final AtomicLong reqCounter; - - public MockNioServer(int port, int dropEveryRequest) throws InterruptedException { - dispatchGroup = new NioEventLoopGroup(); - workerGroup = new NioEventLoopGroup(); - reqCounter = new AtomicLong(0); - ServerBootstrap bootstrap = new ServerBootstrap() - .group(dispatchGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .childHandler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) { - ch.pipeline().addLast(new SimpleChannelInboundHandler() { - @Override - protected void channelRead0(ChannelHandlerContext ctx, Object msg) { - if (reqCounter.incrementAndGet() % dropEveryRequest == 0) { - Channel channel = ctx.channel(); - logger.log(Level.INFO, "dropping the connection " + channel); - channel.close(); - } - } - }); - } - }); - bindFuture = bootstrap.bind(port).sync(); - } - - @Override - public void close() { - bindFuture.channel().close(); - workerGroup.shutdownGracefully(); - dispatchGroup.shutdownGracefully(); - } -} diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java index 6fbc2ea..5b1b1b0 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java @@ -1,9 +1,11 @@ package org.xbib.netty.http.client.test.pool; import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; @@ -11,6 +13,7 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.HttpVersion; import org.junit.After; @@ -20,12 +23,14 @@ import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.client.pool.Pool; import org.xbib.netty.http.client.pool.BoundedChannelPool; +import java.io.Closeable; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; import java.util.logging.Level; import java.util.logging.Logger; @@ -73,8 +78,8 @@ public class NioTest { .option(ChannelOption.SO_KEEPALIVE, true) .option(ChannelOption.SO_REUSEADDR, true) .option(ChannelOption.TCP_NODELAY, true); - channelPool = new BoundedChannelPool<>(semaphore, HttpVersion.HTTP_1_1,false, - NODES, bootstrap, null, 0); + channelPool = new BoundedChannelPool<>(semaphore, HttpVersion.HTTP_1_1, + NODES, bootstrap, null, 0, BoundedChannelPool.PoolKeySelectorType.ROUNDROBIN); channelPool.prepare(CONCURRENCY); } @@ -98,7 +103,7 @@ public class NioTest { Thread.sleep(1); } channel.writeAndFlush(PAYLOAD.retain()).sync(); - channelPool.release(channel); + channelPool.release(channel, false); longAdder.increment(); } catch (InterruptedException e) { break; @@ -127,4 +132,47 @@ public class NioTest { logger.log(Level.WARNING, cause.getMessage(), cause); } } + public class MockNioServer implements Closeable { + + private final EventLoopGroup dispatchGroup; + + private final EventLoopGroup workerGroup; + + private final ChannelFuture bindFuture; + + private final AtomicLong reqCounter; + + public MockNioServer(int port, int dropEveryRequest) throws InterruptedException { + dispatchGroup = new NioEventLoopGroup(); + workerGroup = new NioEventLoopGroup(); + reqCounter = new AtomicLong(0); + ServerBootstrap bootstrap = new ServerBootstrap() + .group(dispatchGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) { + if (reqCounter.incrementAndGet() % dropEveryRequest == 0) { + Channel channel = ctx.channel(); + logger.log(Level.INFO, "dropping the connection " + channel); + channel.close(); + } + } + }); + } + }); + bindFuture = bootstrap.bind(port).sync(); + } + + @Override + public void close() { + bindFuture.channel().close(); + workerGroup.shutdownGracefully(); + dispatchGroup.shutdownGracefully(); + } + } + } diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java index 048f8ab..7478802 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java @@ -1,7 +1,12 @@ package org.xbib.netty.http.client.test.pool; import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.AttributeKey; import org.junit.Test; @@ -44,21 +49,42 @@ public class PoolTest { @Parameterized.Parameters public static Collection generateData() { return Arrays.asList(new Object[][] { - {1, 1}, - {10, 1}, {10, 2}, {10, 5}, {10, 10}, - {100, 1}, {100, 2}, {100, 5}, {100, 10}, - {1000, 1}, {1000, 2}, {1000, 5}, {1000, 10} - }); + {1, 1}, + {10, 1}, + {10, 2}, + //{10, 5}, + //{10, 10}, + {100, 1}, + {100, 2}, + //{100, 5}, + //{100, 10}, + //{1000, 1}, + //{1000, 2}, + //{1000, 5}, + //{1000, 10} + }); } - public PoolTest(int concurrencyLevel, int nodeCount) { + public PoolTest(int concurrencyLevel, int nodeCount) throws InterruptedException { + + ServerBootstrap serverBootstrap = new ServerBootstrap() + .group(new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + } + }); + Channel serverChannel = serverBootstrap.bind("localhost", 8008).sync().channel(); + this.nodeCount = nodeCount; List nodes = new ArrayList<>(); for (int i = 0; i < nodeCount; i ++) { - nodes.add(HttpAddress.http1("localhost" + i)); + nodes.add(HttpAddress.http1("localhost", 8008)); } - try (Pool pool = new BoundedChannelPool<>(new Semaphore(concurrencyLevel), HttpVersion.HTTP_1_1, false, - nodes, new Bootstrap(), null, 0)) { + try (Pool pool = new BoundedChannelPool<>(new Semaphore(concurrencyLevel), HttpVersion.HTTP_1_1, + nodes, new Bootstrap().group(new NioEventLoopGroup()).channel(NioSocketChannel.class), + null, 0, BoundedChannelPool.PoolKeySelectorType.ROUNDROBIN)) { int n = Runtime.getRuntime().availableProcessors(); ExecutorService executorService = Executors.newFixedThreadPool(n); for(int i = 0; i < n; i ++) { @@ -80,7 +106,7 @@ public class PoolTest { channels.add(channel); } for (k = 0; k < j; k ++) { - pool.release(channels.get(k)); + pool.release(channels.get(k), false); } channels.clear(); } @@ -99,6 +125,7 @@ public class PoolTest { } catch (Throwable t) { logger.log(Level.WARNING, t.getMessage(), t); } finally { + serverChannel.close(); long connCountSum = nodeFreq.values().stream().mapToLong(LongAdder::sum).sum(); logger.log(Level.INFO, "concurrency = " + concurrencyLevel + ", nodes = " + nodeCount + " -> rate: " + connCountSum / TEST_STEP_TIME_SECONDS); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/PooledClientTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PooledClientTest.java similarity index 75% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/PooledClientTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PooledClientTest.java index a659f4f..42563a7 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/PooledClientTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PooledClientTest.java @@ -1,9 +1,11 @@ -package org.xbib.netty.http.client.test; +package org.xbib.netty.http.client.test.pool; import io.netty.handler.codec.http.HttpVersion; import org.junit.Test; import org.xbib.net.URL; import org.xbib.netty.http.client.Client; +import org.xbib.TestBase; +import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.client.Request; @@ -16,7 +18,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -public class PooledClientTest extends LoggingBase { +public class PooledClientTest extends TestBase { private static final Logger logger = Logger.getLogger(""); @@ -31,6 +33,11 @@ public class PooledClientTest extends LoggingBase { .setPoolNodeConnectionLimit(threads) .build(); AtomicInteger count = new AtomicInteger(); + ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + count.getAndIncrement(); + }; try { ExecutorService executorService = Executors.newFixedThreadPool(threads); for (int n = 0; n < threads; n++) { @@ -38,17 +45,12 @@ public class PooledClientTest extends LoggingBase { try { logger.log(Level.INFO, "starting " + Thread.currentThread()); for (int i = 0; i < loop; i++) { - Request request = Request.get() + Request request = Request.get().setVersion(httpAddress.getVersion()) .url(url.toString()) - .setVersion(httpAddress.getVersion()) //.setTimeoutInMillis(25000L) .build() - .setResponseListener(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - count.getAndIncrement(); - }); - client.pooledExecute(request).get(); + .setResponseListener(responseListener); + client.newTransport().execute(request).get(); } logger.log(Level.INFO, "done " + Thread.currentThread()); } catch (Throwable e) { diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/hacks/Http2FramesTest.java similarity index 98% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/hacks/Http2FramesTest.java index 9ae6ef6..0cba6a2 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/hacks/Http2FramesTest.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.test.simple; +package org.xbib.netty.http.hacks; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; @@ -28,6 +28,7 @@ import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; @@ -39,7 +40,8 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import java.util.logging.Logger; -public class Http2FramesTest { +@Ignore +public class Http2FramesTest extends TestBase { private static final Logger logger = Logger.getLogger(Http2FramesTest.class.getName()); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/hacks/SimpleHttp1Test.java similarity index 98% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java rename to netty-http-client/src/test/java/org/xbib/netty/http/hacks/SimpleHttp1Test.java index 15d75b5..6368175 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/hacks/SimpleHttp1Test.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.test.simple; +package org.xbib.netty.http.hacks; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; @@ -20,7 +20,9 @@ import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.AttributeKey; import org.junit.After; +import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import java.io.IOException; import java.net.InetSocketAddress; @@ -38,17 +40,14 @@ import java.util.logging.LogManager; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; -/** - * - */ -public class SimpleHttp1Test { +@Ignore +public class SimpleHttp1Test extends TestBase { private static final Logger logger = Logger.getLogger(SimpleHttp1Test.class.getName()); static { System.setProperty("io.netty.leakDetection.level", "paranoid"); System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); - System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); LogManager.getLogManager().reset(); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/hacks/SimpleHttp2Test.java similarity index 94% rename from netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java rename to netty-http-client/src/test/java/org/xbib/netty/http/hacks/SimpleHttp2Test.java index adac51c..759d067 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/hacks/SimpleHttp2Test.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.test.simple; +package org.xbib.netty.http.hacks; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; @@ -35,14 +35,14 @@ import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.util.AttributeKey; +import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import javax.net.ssl.SSLException; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; @@ -53,9 +53,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -/** - */ -public class SimpleHttp2Test { +@Ignore +public class SimpleHttp2Test extends TestBase { private static final Logger logger = Logger.getLogger(SimpleHttp2Test.class.getName()); @@ -113,8 +112,6 @@ public class SimpleHttp2Test { private final Initializer initializer; - private final List transports; - Client() { eventLoopGroup = new NioEventLoopGroup(); http2SettingsHandler = new Http2SettingsHandler(); @@ -124,7 +121,6 @@ public class SimpleHttp2Test { .group(eventLoopGroup) .channel(NioSocketChannel.class) .handler(initializer); - transports = new ArrayList<>(); } Bootstrap bootstrap() { @@ -137,23 +133,8 @@ public class SimpleHttp2Test { Http2Transport newTransport(String host, int port) { Http2Transport transport = new Http2Transport(this, new InetSocketAddress(host, port)); - transports.add(transport); return transport; } - - List transports() { - return transports; - } - - void close(Http2Transport transport) { - transports.remove(transport); - } - - void close() { - for (Http2Transport transport : transports) { - transport.close(); - } - } } class Http2Transport { @@ -179,10 +160,6 @@ public class SimpleHttp2Test { streamIdCounter = new AtomicInteger(3); } - Client client() { - return client; - } - InetSocketAddress inetSocketAddress() { return inetSocketAddress; } @@ -270,12 +247,6 @@ public class SimpleHttp2Test { } } - void complete() { - for (CompletableFuture promise : streamidPromiseMap.values()) { - promise.complete(true); - } - } - void fail(Throwable throwable) { for (CompletableFuture promise : streamidPromiseMap.values()) { promise.completeExceptionally(throwable); @@ -286,7 +257,6 @@ public class SimpleHttp2Test { if (channel != null) { channel.close(); } - client.close(this); } } diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java index 99add9a..ca3212f 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java @@ -39,10 +39,18 @@ public class HttpAddress implements PoolKey { } public static HttpAddress http2(String host) { - return new HttpAddress(host, 443, HTTP_2_0, true); + return new HttpAddress(host, 443, HTTP_2_0, false); } public static HttpAddress http2(String host, int port) { + return new HttpAddress(host, port, HTTP_2_0, false); + } + + public static HttpAddress secureHttp2(String host) { + return new HttpAddress(host, 443, HTTP_2_0, true); + } + + public static HttpAddress secureHttp2(String host, int port) { return new HttpAddress(host, port, HTTP_2_0, true); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java index 4287023..f506a2a 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java @@ -22,9 +22,9 @@ import io.netty.util.DomainNameMapping; import io.netty.util.DomainNameMappingBuilder; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.context.VirtualServer; -import org.xbib.netty.http.server.handler.http1.HttpChannelInitializer; +import org.xbib.netty.http.server.handler.http.HttpChannelInitializer; import org.xbib.netty.http.server.handler.http2.Http2ChannelInitializer; -import org.xbib.netty.http.server.transport.Http1ServerTransport; +import org.xbib.netty.http.server.transport.HttpServerTransport; import org.xbib.netty.http.server.transport.Http2ServerTransport; import org.xbib.netty.http.server.transport.ServerTransport; import org.xbib.netty.http.server.util.NetworkUtils; @@ -49,7 +49,9 @@ public final class Server { static { // extend Java system properties by detected network interfaces - //NetworkUtils.extendSystemProperties(); + if (System.getProperty("xbib.netty.http.client.extendsystemproperties") != null) { + NetworkUtils.extendSystemProperties(); + } // change Netty defaults to safer ones, but still allow override from arg line if (System.getProperty("io.netty.noUnsafe") == null) { System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); @@ -57,12 +59,6 @@ public final class Server { if (System.getProperty("io.netty.noKeySetOptimization") == null) { System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); } - if (System.getProperty("io.netty.recycler.maxCapacity") == null) { - System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); - } - //if (System.getProperty("io.netty.leakDetection.level") == null) { - // System.setProperty("io.netty.leakDetection.level", "paranoid"); - //} } private final ServerConfig serverConfig; @@ -83,6 +79,12 @@ public final class Server { /** * Create a new HTTP server. Use {@link #builder()} to build HTTP client instance. + * @param serverConfig server configuration + * @param byteBufAllocator byte buf allocator + * @param parentEventLoopGroup parent event loop group + * @param childEventLoopGroup child event loop group + * @param socketChannelClass socket channel class + * @throws SSLException if SSL can not be configured */ public Server(ServerConfig serverConfig, ByteBufAllocator byteBufAllocator, @@ -112,7 +114,6 @@ public final class Server { .childOption(ChannelOption.SO_RCVBUF, serverConfig.getTcpReceiveBufferSize()) .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, serverConfig.getConnectTimeoutMillis()) .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverConfig.getWriteBufferWaterMark()); - if (serverConfig.isDebug()) { bootstrap.handler(new LoggingHandler("bootstrap-server", serverConfig.getDebugLogLevel())); } @@ -178,6 +179,7 @@ public final class Server { /** * Start accepting incoming connections. + * @return the channel future */ public ChannelFuture accept() { logger.log(Level.INFO, () -> "trying to bind to " + serverConfig.getAddress()); @@ -198,7 +200,7 @@ public final class Server { } public ServerTransport newTransport(HttpVersion httpVersion) { - return httpVersion.majorVersion() == 1 ? new Http1ServerTransport(this) : new Http2ServerTransport(this); + return httpVersion.majorVersion() == 1 ? new HttpServerTransport(this) : new Http2ServerTransport(this); } public synchronized void shutdownGracefully() throws IOException { diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/HttpServerChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/HttpServerChannelInitializer.java deleted file mode 100644 index 0ba5172..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/HttpServerChannelInitializer.java +++ /dev/null @@ -1,220 +0,0 @@ -package org.xbib.netty.http.server.handler; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.http.HttpContentDecompressor; -import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http.HttpServerCodec; -import io.netty.handler.codec.http.HttpServerUpgradeHandler; -import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; -import io.netty.handler.codec.http2.Http2ConnectionHandler; -import io.netty.handler.codec.http2.Http2MultiplexCodec; -import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; -import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; -import io.netty.handler.ssl.SniHandler; -import io.netty.handler.ssl.SslContext; -import io.netty.util.AsciiString; -import io.netty.util.DomainNameMapping; -import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.ServerConfig; -import org.xbib.netty.http.server.handler.http1.HttpHandler; -import org.xbib.netty.http.server.handler.http1.IdleTimeoutHandler; -import org.xbib.netty.http.server.handler.http1.TrafficLoggingHandler; -import org.xbib.netty.http.server.handler.http2.UserEventLogger; -import org.xbib.netty.http.server.internal.Http1ObjectEncoder; -import org.xbib.netty.http.server.internal.Http2ObjectEncoder; -import org.xbib.netty.http.server.internal.HttpObjectEncoder; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * HTTP server channel initializer. - */ -public class HttpServerChannelInitializer extends ChannelInitializer { - - private static final Logger logger = Logger.getLogger(HttpServerChannelInitializer.class.getName()); - - private final Server server; - - private final ServerConfig serverConfig; - - private final Http2ConnectionHandler http2ConnectionHandler; - - private final DomainNameMapping domainNameMapping; - - public HttpServerChannelInitializer(Server server, ServerConfig serverConfig, - DomainNameMapping domainNameMapping) { - this.server = server; - this.serverConfig = serverConfig; - this.domainNameMapping = domainNameMapping; - this.http2ConnectionHandler = null;//createHttp2ConnectionHandler(serverConfig); - - } - - @Override - public void initChannel(SocketChannel ch) { - if (serverConfig.isDebug()) { - ch.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); - } - if (serverConfig.getAddress().isSecure()) { - configureSecure(ch); - } else { - configureClearText(ch); - } - HttpObjectEncoder encoder = serverConfig.getAddress().getVersion().majorVersion() == 2 ? - new Http2ObjectEncoder(http2ConnectionHandler.encoder()) : - new Http1ObjectEncoder(); - if (serverConfig.isDebug()) { - logger.log(Level.FINE, "server channel initialized: " + ch.pipeline().names()); - } - } - - private void configureClearText(SocketChannel ch) { - ChannelPipeline pipeline = ch.pipeline(); - if (serverConfig.getAddress().getVersion().majorVersion() == 1) { - if (serverConfig.isInstallHttp2Upgrade()) { - installHttp2Upgrade(pipeline); - } else { - pipeline.addFirst(new IdleTimeoutHandler()); - pipeline.addLast(new UserEventLogger()); - pipeline.addLast(createHttp1ConnectionHandler(serverConfig)); - configureHttp1Pipeline(pipeline); - } - } else if (serverConfig.getAddress().getVersion().majorVersion() == 2) { - pipeline.addLast(http2ConnectionHandler); - configureHttp2Pipeline(pipeline); - } - } - - private void installHttp2Upgrade(ChannelPipeline pipeline) { - HttpServerCodec httpServerCodec = new HttpServerCodec(); - HttpServerUpgradeHandler httpServerUpgradeHandler = new HttpServerUpgradeHandler(httpServerCodec, protocol -> { - if (AsciiString.contentEquals("h2c", protocol)) { - return new Http2ServerUpgradeCodec(http2ConnectionHandler); - } else { - return null; - } - }); - pipeline.addLast(new CleartextHttp2ServerUpgradeHandler(httpServerCodec, httpServerUpgradeHandler, - new HttpHandler(server))); - } - - private void configureSecure(SocketChannel ch) { - ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(new SniHandler(domainNameMapping)); - pipeline.addLast(new Http2NegotiationHandler(ApplicationProtocolNames.HTTP_1_1)); - } - - private HttpServerCodec createHttp1ConnectionHandler(ServerConfig context) { - return new HttpServerCodec(context.getMaxInitialLineLength(), - context.getMaxHeadersSize(), context.getMaxChunkSize()); - } - - private void configureHttp1Pipeline(ChannelPipeline pipeline) { - if (serverConfig.isEnableGzip()) { - pipeline.addLast(new HttpContentDecompressor()); - } - HttpObjectAggregator httpObjectAggregator = - new HttpObjectAggregator(serverConfig.getMaxContentLength(), false); - httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents()); - pipeline.addLast(httpObjectAggregator); - pipeline.addLast(new HttpHandler(server)); - } - - private void configureHttp2Pipeline(ChannelPipeline pipeline) { - pipeline.addLast(new UserEventLogger()); - pipeline.addLast(new HttpHandler(server)); - } - - /*private static Http2ConnectionHandler createHttp2ConnectionHandler(ServerConfig serverConfig) { - Http2Settings initialSettings = serverConfig.getHttp2Settings(); - Http2Connection http2Connection = new DefaultHttp2Connection(true); - Long maxHeaderListSize = initialSettings.maxHeaderListSize(); - Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ? - new DefaultHttp2HeadersDecoder(true) : - new DefaultHttp2HeadersDecoder(true, maxHeaderListSize)); - Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(); - Http2FrameLogger frameLogger = null; - if (serverConfig.isDebug()) { - frameLogger = new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server"); - } - if (frameLogger != null) { - frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger); - frameReader = new Http2InboundFrameLogger(frameReader, frameLogger); - } - Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(http2Connection, frameWriter); - Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(http2Connection, encoder, frameReader); - Http2ConnectionHandler http2ConnectionHandler = new Http2ServerConnectionHandler(decoder, encoder, initialSettings); - Http2Handler http2Handler = new Http2Handler(serverConfig, http2Connection, true); - http2ConnectionHandler.connection().addListener(http2Handler); - http2ConnectionHandler.decoder().frameListener(new DelegatingDecompressorFrameListener(http2Connection, http2Handler)); - if (serverConfig.getIdleTimeoutMillis() > 0) { - http2ConnectionHandler.gracefulShutdownTimeoutMillis(serverConfig.getIdleTimeoutMillis()); - } - return http2ConnectionHandler; - }*/ - - private ChannelHandler createMultiplexInitializer() { - /*HttpObjectAggregator httpObjectAggregator = - new HttpObjectAggregator(serverConfig.getMaxContentLength(), false); - httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents());*/ - return new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) { - ch.pipeline().addLast(http2ConnectionHandler); - configureHttp2Pipeline(ch.pipeline()); - //.addLast(new Http2StreamFrameToHttpObjectCodec(true)) - //.addLast(httpObjectAggregator) - //.addLast(httpHandler); - } - }; - } - - private Http2MultiplexCodec createHttp2MultiplexCodec() { - Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(createMultiplexInitializer()); - multiplexCodecBuilder.initialSettings(serverConfig.getHttp2Settings()); - if (serverConfig.getIdleTimeoutMillis() > 0) { - multiplexCodecBuilder.gracefulShutdownTimeoutMillis(serverConfig.getIdleTimeoutMillis()); - } - return multiplexCodecBuilder.build(); - } - - /** - * Negotiates with the browser if HTTP/2 or HTTP is going to be used. Once decided, the - * pipeline is setup with the correct handlers for the selected protocol. - */ - class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler { - - Http2NegotiationHandler(String fallbackProtocol) { - super(fallbackProtocol); - } - - @Override - protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { - ChannelPipeline pipeline = ctx.pipeline(); - if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { - pipeline.addLast(createHttp1ConnectionHandler(serverConfig)); - configureHttp1Pipeline(pipeline); - return; - } - if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - pipeline.addLast(http2ConnectionHandler); - configureHttp2Pipeline(pipeline); - if (serverConfig.isDebug()) { - logger.log(Level.INFO, "after successful HTTP/2 negotiation: " + pipeline.names()); - } - return; - } - ctx.close(); - throw new IllegalStateException("unknown protocol: " + protocol); - } - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/IdleTimeoutHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/IdleTimeoutHandler.java similarity index 94% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/IdleTimeoutHandler.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/handler/IdleTimeoutHandler.java index 62f7a09..1f26775 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/IdleTimeoutHandler.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/IdleTimeoutHandler.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.handler.http1; +package org.xbib.netty.http.server.handler; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/TrafficLoggingHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/TrafficLoggingHandler.java similarity index 95% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/TrafficLoggingHandler.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/handler/TrafficLoggingHandler.java index 63a2c19..92a27fe 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/TrafficLoggingHandler.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/TrafficLoggingHandler.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.handler.http1; +package org.xbib.netty.http.server.handler; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java similarity index 54% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpChannelInitializer.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java index 05a5813..7e18fee 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpChannelInitializer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java @@ -1,11 +1,18 @@ -package org.xbib.netty.http.server.handler.http1; +package org.xbib.netty.http.server.handler.http; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.logging.LogLevel; import io.netty.handler.ssl.SniHandler; import io.netty.handler.ssl.SslContext; @@ -13,7 +20,10 @@ import io.netty.util.DomainNameMapping; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerConfig; +import org.xbib.netty.http.server.handler.TrafficLoggingHandler; +import org.xbib.netty.http.server.transport.ServerTransport; +import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; @@ -21,6 +31,8 @@ public class HttpChannelInitializer extends ChannelInitializer { private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName()); + private final Server server; + private final ServerConfig serverConfig; private final HttpAddress httpAddress; @@ -32,6 +44,7 @@ public class HttpChannelInitializer extends ChannelInitializer { public HttpChannelInitializer(Server server, HttpAddress httpAddress, DomainNameMapping domainNameMapping) { + this.server = server; this.serverConfig = server.getServerConfig(); this.httpAddress = httpAddress; this.domainNameMapping = domainNameMapping; @@ -40,6 +53,8 @@ public class HttpChannelInitializer extends ChannelInitializer { @Override public void initChannel(SocketChannel channel) { + ServerTransport serverTransport = server.newTransport(httpAddress.getVersion()); + channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport); if (serverConfig.isDebug()) { channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); } @@ -70,6 +85,46 @@ public class HttpChannelInitializer extends ChannelInitializer { false); httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents()); pipeline.addLast(httpObjectAggregator); + pipeline.addLast(new HttpPipeliningHandler(1024)); pipeline.addLast(httpHandler); } + + @Sharable + class HttpHandler extends ChannelInboundHandlerAdapter { + + private final Logger logger = Logger.getLogger(HttpHandler.class.getName()); + + private final Server server; + + public HttpHandler(Server server) { + this.server = server; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpPipelinedRequest) { + HttpPipelinedRequest httpPipelinedRequest = (HttpPipelinedRequest) msg; + if (httpPipelinedRequest.getRequest() instanceof FullHttpRequest) { + FullHttpRequest fullHttpRequest = (FullHttpRequest) httpPipelinedRequest.getRequest(); + ServerTransport serverTransport = server.newTransport(fullHttpRequest.protocolVersion()); + serverTransport.requestReceived(ctx, fullHttpRequest, httpPipelinedRequest.getSequenceId()); + } + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.log(Level.WARNING, cause.getMessage(), cause); + ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.INTERNAL_SERVER_ERROR, + Unpooled.copiedBuffer(cause.getMessage().getBytes(StandardCharsets.UTF_8)))); + } + } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpPipelinedRequest.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpPipelinedRequest.java new file mode 100644 index 0000000..990eccf --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpPipelinedRequest.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.server.handler.http; + +import io.netty.handler.codec.http.LastHttpContent; + +public class HttpPipelinedRequest { + + private final LastHttpContent request; + + private final int sequenceId; + + public HttpPipelinedRequest(LastHttpContent request, int sequenceId) { + this.request = request; + this.sequenceId = sequenceId; + } + + public LastHttpContent getRequest() { + return request; + } + + public int getSequenceId() { + return sequenceId; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpPipelinedResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpPipelinedResponse.java new file mode 100644 index 0000000..575ac05 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpPipelinedResponse.java @@ -0,0 +1,34 @@ +package org.xbib.netty.http.server.handler.http; + +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpResponse; + +public class HttpPipelinedResponse implements Comparable { + + private final HttpResponse response; + private final ChannelPromise promise; + private final int sequenceId; + + public HttpPipelinedResponse(HttpResponse response, ChannelPromise promise, int sequenceId) { + this.response = response; + this.promise = promise; + this.sequenceId = sequenceId; + } + + public int getSequenceId() { + return sequenceId; + } + + public HttpResponse getResponse() { + return response; + } + + public ChannelPromise getPromise() { + return promise; + } + + @Override + public int compareTo(HttpPipelinedResponse other) { + return this.sequenceId - other.sequenceId; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpPipeliningHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpPipeliningHandler.java new file mode 100644 index 0000000..d1b8f5e --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpPipeliningHandler.java @@ -0,0 +1,75 @@ +package org.xbib.netty.http.server.handler.http; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.LastHttpContent; + +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Implements HTTP pipelining ordering, ensuring that responses are completely served in the same order as their + * corresponding requests. + * + * Based on https://github.com/typesafehub/netty-http-pipelining - which uses netty 3 + */ +public class HttpPipeliningHandler extends ChannelDuplexHandler { + + private final int pipelineCapacity; + + private final Queue httpPipelinedResponses; + + private final AtomicInteger requestCounter; + + private final AtomicInteger writtenRequests; + + /** + * @param pipelineCapacity the maximum number of channel events that will be retained prior to aborting the channel + * connection. This is required as events cannot queue up indefinitely; we would run out of + * memory if this was the case. + */ + public HttpPipeliningHandler(int pipelineCapacity) { + this.pipelineCapacity = pipelineCapacity; + this.httpPipelinedResponses = new PriorityQueue<>(3); + this.requestCounter = new AtomicInteger(); + this.writtenRequests = new AtomicInteger(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof LastHttpContent) { + super.channelRead(ctx, new HttpPipelinedRequest((LastHttpContent) msg, requestCounter.getAndIncrement())); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof HttpPipelinedResponse) { + boolean channelShouldClose = false; + synchronized (httpPipelinedResponses) { + if (httpPipelinedResponses.size() < pipelineCapacity) { + HttpPipelinedResponse currentEvent = (HttpPipelinedResponse) msg; + httpPipelinedResponses.add(currentEvent); + while (!httpPipelinedResponses.isEmpty()) { + HttpPipelinedResponse queuedPipelinedResponse = httpPipelinedResponses.peek(); + if (queuedPipelinedResponse.getSequenceId() != writtenRequests.get()) { + break; + } + httpPipelinedResponses.remove(); + super.write(ctx, queuedPipelinedResponse.getResponse(), queuedPipelinedResponse.getPromise()); + writtenRequests.getAndIncrement(); + } + } else { + channelShouldClose = true; + } + } + if (channelShouldClose) { + ctx.close(); + } + } else { + super.write(ctx, msg, promise); + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpHandler.java deleted file mode 100644 index 442a758..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.xbib.netty.http.server.handler.http1; - -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpVersion; -import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.transport.ServerTransport; - -import java.nio.charset.StandardCharsets; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * HTTP handler. - */ -@ChannelHandler.Sharable -public class HttpHandler extends ChannelInboundHandlerAdapter { - - private static final Logger logger = Logger.getLogger(HttpHandler.class.getName()); - - private final Server server; - - public HttpHandler(Server server) { - this.server = server; - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof FullHttpRequest) { - FullHttpRequest fullHttpRequest = (FullHttpRequest) msg; - ServerTransport serverTransport = server.newTransport(fullHttpRequest.protocolVersion()); - serverTransport.requestReceived(ctx, fullHttpRequest); - } else { - super.channelRead(ctx, msg); - } - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) { - ctx.flush(); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - logger.log(Level.WARNING, cause.getMessage(), cause); - ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, - HttpResponseStatus.INTERNAL_SERVER_ERROR, - Unpooled.copiedBuffer(cause.getMessage().getBytes(StandardCharsets.UTF_8)))); - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/DummyHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/DummyHandler.java deleted file mode 100644 index 221bcaf..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/DummyHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.xbib.netty.http.server.handler.http2; - -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandlerContext; - -import java.util.logging.Level; -import java.util.logging.Logger; - -public class DummyHandler extends ChannelDuplexHandler { - - private static final Logger logger = Logger.getLogger(DummyHandler.class.getName()); - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - logger.log(Level.INFO, "msg = " + msg + " class = " + msg.getClass().getName()); - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/FrameListener.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/FrameListener.java deleted file mode 100644 index 0b4094d..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/FrameListener.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.xbib.netty.http.server.handler.http2; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http2.DefaultHttp2DataFrame; -import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; -import io.netty.handler.codec.http2.Http2DataFrame; -import io.netty.handler.codec.http2.Http2EventAdapter; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.Http2HeadersFrame; - -import java.util.logging.Level; -import java.util.logging.Logger; - -public class FrameListener extends Http2EventAdapter { - - private static final Logger logger = Logger.getLogger(FrameListener.class.getName()); - - @Override - public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, - short weight, boolean exclusive, int padding, boolean endStream) { - logger.log(Level.FINE, "onHeadersRead"); - Http2HeadersFrame frame = new DefaultHttp2HeadersFrame(headers,endStream,padding); - ctx.fireChannelRead(frame); - } - - public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) { - logger.log(Level.FINE, "onDataRead"); - Http2DataFrame frame = new DefaultHttp2DataFrame(data, endOfStream, padding); - ctx.fireChannelRead(frame); - return data.readableBytes() + padding; - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp1Handler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp1Handler.java deleted file mode 100644 index e6870b6..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp1Handler.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.xbib.netty.http.server.handler.http2; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpUtil; - -import java.nio.charset.StandardCharsets; - -import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; -import static io.netty.handler.codec.http.HttpResponseStatus.OK; -import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; -import static io.netty.util.internal.ObjectUtil.checkNotNull; - -/** - * HTTP handler that responds with a "Hello World" - */ -public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler { - - static final ByteBuf RESPONSE_BYTES = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hello World", StandardCharsets.UTF_8)); - - private final String establishApproach; - - public HelloWorldHttp1Handler(String establishApproach) { - this.establishApproach = checkNotNull(establishApproach, "establishApproach"); - } - - @Override - public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) { - if (HttpUtil.is100ContinueExpected(req)) { - ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE)); - } - boolean keepAlive = HttpUtil.isKeepAlive(req); - - ByteBuf content = ctx.alloc().buffer(); - content.writeBytes(RESPONSE_BYTES.duplicate()); - ByteBufUtil.writeAscii(content, " - via " + req.protocolVersion() + " (" + establishApproach + ")"); - - FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content); - response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); - response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); - - if (!keepAlive) { - ctx.write(response).addListener(ChannelFutureListener.CLOSE); - } else { - response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE); - ctx.write(response); - } - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) { - ctx.flush(); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - ctx.close(); - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp2Handler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp2Handler.java deleted file mode 100644 index e5e490b..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp2Handler.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.xbib.netty.http.server.handler.http2; - -import static io.netty.buffer.Unpooled.copiedBuffer; -import static io.netty.buffer.Unpooled.unreleasableBuffer; -import static io.netty.handler.codec.http.HttpResponseStatus.OK; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http2.DefaultHttp2DataFrame; -import io.netty.handler.codec.http2.DefaultHttp2Headers; -import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; -import io.netty.handler.codec.http2.Http2DataFrame; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.Http2HeadersFrame; -import io.netty.util.CharsetUtil; - -/** - * A simple handler that responds with the message "Hello World!". - * - *

This example is making use of the "multiplexing" http2 API, where streams are mapped to child - * Channels. This API is very experimental and incomplete. - */ -@Sharable -public class HelloWorldHttp2Handler extends ChannelDuplexHandler { - - private static final ByteBuf RESPONSE_BYTES = unreleasableBuffer(copiedBuffer("Hello World", CharsetUtil.UTF_8)); - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - super.exceptionCaught(ctx, cause); - cause.printStackTrace(); - ctx.close(); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof Http2HeadersFrame) { - onHeadersRead(ctx, (Http2HeadersFrame) msg); - } else if (msg instanceof Http2DataFrame) { - onDataRead(ctx, (Http2DataFrame) msg); - } else { - super.channelRead(ctx, msg); - } - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - ctx.flush(); - } - - /** - * If receive a frame with end-of-stream set, send a pre-canned response. - */ - private static void onDataRead(ChannelHandlerContext ctx, Http2DataFrame data) throws Exception { - if (data.isEndStream()) { - sendResponse(ctx, data.content()); - } else { - // We do not send back the response to the remote-peer, so we need to release it. - data.release(); - } - } - - /** - * If receive a frame with end-of-stream set, send a pre-canned response. - */ - private static void onHeadersRead(ChannelHandlerContext ctx, Http2HeadersFrame headers) - throws Exception { - if (headers.isEndStream()) { - ByteBuf content = ctx.alloc().buffer(); - content.writeBytes(RESPONSE_BYTES.duplicate()); - ByteBufUtil.writeAscii(content, " - via HTTP/2"); - sendResponse(ctx, content); - } - } - - /** - * Sends a "Hello World" DATA frame to the client. - */ - private static void sendResponse(ChannelHandlerContext ctx, ByteBuf payload) { - // Send a frame for the response status - Http2Headers headers = new DefaultHttp2Headers().status(OK.codeAsText()); - ctx.write(new DefaultHttp2HeadersFrame(headers)); - ctx.write(new DefaultHttp2DataFrame(payload, true)); - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java index 41258e4..18662e4 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java @@ -2,51 +2,34 @@ package org.xbib.netty.http.server.handler.http2; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerUpgradeHandler; -import io.netty.handler.codec.http2.DefaultHttp2Connection; -import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; -import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder; -import io.netty.handler.codec.http2.DefaultHttp2FrameReader; -import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; -import io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder; -import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; +import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame; import io.netty.handler.codec.http2.Http2CodecUtil; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2ConnectionDecoder; -import io.netty.handler.codec.http2.Http2ConnectionEncoder; -import io.netty.handler.codec.http2.Http2ConnectionHandler; -import io.netty.handler.codec.http2.Http2ConnectionHandlerBuilder; import io.netty.handler.codec.http2.Http2FrameLogger; -import io.netty.handler.codec.http2.Http2FrameReader; -import io.netty.handler.codec.http2.Http2FrameWriter; -import io.netty.handler.codec.http2.Http2InboundFrameLogger; import io.netty.handler.codec.http2.Http2MultiplexCodec; import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; -import io.netty.handler.codec.http2.Http2OutboundFrameLogger; import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; -import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter; -import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; import io.netty.handler.logging.LogLevel; import io.netty.handler.ssl.SniHandler; import io.netty.handler.ssl.SslContext; import io.netty.util.AsciiString; import io.netty.util.DomainNameMapping; -import io.netty.util.ReferenceCountUtil; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerConfig; -import org.xbib.netty.http.server.handler.http1.HttpHandler; -import org.xbib.netty.http.server.handler.http1.TrafficLoggingHandler; +import org.xbib.netty.http.server.handler.TrafficLoggingHandler; +import org.xbib.netty.http.server.transport.ServerTransport; +import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -71,13 +54,10 @@ public class Http2ChannelInitializer extends ChannelInitializer { this.domainNameMapping = domainNameMapping; } - /** - * The channel initialization for HTTP/2. - * - * @param channel socket channel - */ @Override public void initChannel(Channel channel) { + ServerTransport serverTransport = server.newTransport(httpAddress.getVersion()); + channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport); if (serverConfig.isDebug()) { channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); } @@ -97,155 +77,71 @@ public class Http2ChannelInitializer extends ChannelInitializer { } private void configureCleartext(Channel ch) { - Http2SettingsHandler http2SettingsHandler = new Http2SettingsHandler(); - Http2RequestHandler http2RequestHandler = new Http2RequestHandler(); - //HttpHandler httpHandler = new HttpHandler(server); - - ch.pipeline() - //.addLast(newConnectionHandler()) - .addLast(upgradeHandler()); - //.addLast(http2SettingsHandler) - //.addLast(http2RequestHandler); - // .addLast(sourceCodec) - - /*final Http2MultiplexCodec http2Codec = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) { - logger.log(Level.INFO, "initChannel multiplex "); - } - }).build(); - HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> - new Http2ServerUpgradeCodec(http2Codec); - final HttpServerCodec serverCodec = new HttpServerCodec(); - ch.pipeline().addLast(serverCodec) - .addLast(new HttpServerUpgradeHandler(serverCodec, upgradeCodecFactory)) - .addLast(new SimpleChannelInboundHandler() { + ChannelPipeline p = ch.pipeline(); + Http2MultiplexCodecBuilder serverMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer() { @Override - protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception { - // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP. - System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)"); - ChannelPipeline pipeline = ctx.pipeline(); - ChannelHandlerContext thisCtx = pipeline.context(this); - pipeline.addAfter(thisCtx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); - pipeline.replace(this, null, new HttpObjectAggregator(Integer.MAX_VALUE)); - ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); + protected void initChannel(Channel channel) { + ServerTransport serverTransport = server.newTransport(httpAddress.getVersion()); + channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport); + ChannelPipeline p = channel.pipeline(); + p.addLast("multiplex-server-frame-converter", + new Http2StreamFrameToHttpObjectCodec(true)); + p.addLast("multiplex-server-chunk-aggregator", + new HttpObjectAggregator(serverConfig.getMaxContentLength())); + p.addLast("multiplex-server-request-handler", + new ServerRequestHandler()); } }) - .addLast(new UserEventLogger()) - .addLast(new HttpHandler(server)); -*/ - - /* - Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forServer().build(); - - Http2StreamFrameToHttpObjectCodec http2StreamFrameToHttpObjectCodec = - new Http2StreamFrameToHttpObjectCodec(true, true); - - HttpObjectAggregator httpObjectAggregator = - new HttpObjectAggregator(serverConfig.getMaxContentLength(), false); - httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents()); - HttpHandler httpHandler = new HttpHandler(server); - - Http2ConnectionHandler http2ConnectionHandler = newConnectionHandler(server.getServerConfig()); - Http2Connection http2Connection = http2ConnectionHandler.connection(); - Http2Handler http2Handler = new Http2Handler(serverConfig, http2Connection, true); - http2Connection.addListener(http2Handler); - http2ConnectionHandler.decoder().frameListener(new DelegatingDecompressorFrameListener(http2Connection, http2Handler)); - - channel.pipeline().addLast(http2ConnectionHandler) - .addLast(new UserEventLogger()) - .addLast(new HttpHandler(server)); - - //.addLast(new Http2StreamFrameToHttpObjectCodec(true)) - //.addLast(httpObjectAggregator) - //.addLast(httpHandler); - */ - } - - private Http2ConnectionHandler newStandardConnectionHandler() { - Http2Connection http2Connection = new DefaultHttp2Connection(true); - InboundHttp2ToHttpAdapter inboundHttp2ToHttpAdapter = - new InboundHttp2ToHttpAdapterBuilder(http2Connection) - .maxContentLength(serverConfig.getMaxContentLength()) - .propagateSettings(true) - .validateHttpHeaders(true) - .build(); - Http2ConnectionHandlerBuilder builder = new Http2ConnectionHandlerBuilder() - .connection(http2Connection) - .initialSettings(serverConfig.getHttp2Settings()) - .frameListener(new DelegatingDecompressorFrameListener(http2Connection, inboundHttp2ToHttpAdapter)); + .initialSettings(Http2Settings.defaultSettings()); if (serverConfig.isDebug()) { - builder.frameLogger(new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server")); + serverMultiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "server")); } - return builder.build(); - } - - private Http2ConnectionHandler newConnectionHandler() { - Http2Settings initialSettings = serverConfig.getHttp2Settings(); - Http2Connection http2Connection = new DefaultHttp2Connection(true); - Long maxHeaderListSize = initialSettings.maxHeaderListSize(); - Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ? - new DefaultHttp2HeadersDecoder(true) : - new DefaultHttp2HeadersDecoder(true, maxHeaderListSize)); - Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(); - Http2FrameLogger frameLogger = null; - if (serverConfig.isDebug()) { - frameLogger = new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server"); - } - if (frameLogger != null) { - frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger); - frameReader = new Http2InboundFrameLogger(frameReader, frameLogger); - } - Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(http2Connection, frameWriter); - Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(http2Connection, encoder, frameReader); - - Http2ConnectionHandlerBuilder builder = new Http2ConnectionHandlerBuilder() - .connection(http2Connection) - //.codec(decoder, encoder) - //.initialSettings(initialSettings) - .frameListener(new FrameListener()) - .frameLogger(new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server")); - if (serverConfig.getIdleTimeoutMillis() > 0) { - builder.gracefulShutdownTimeoutMillis(serverConfig.getIdleTimeoutMillis()); - } - return builder.build(); - //Http2Handler http2Handler = new Http2Handler(server, http2Connection, true); - //http2ConnectionHandler.connection().addListener(http2Handler); - //http2ConnectionHandler.decoder().frameListener(); - //return http2ConnectionHandler; - } - - static class Http2ServerConnectionHandler extends Http2ConnectionHandler { - - Http2ServerConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, - Http2Settings initialSettings) { - super(decoder, encoder, initialSettings); - } - } - - private final HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> { - if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { - return upgradeCodec(); - } else { - return null; - } - }; - - private Http2ServerUpgradeCodec upgradeCodec() { - return new Http2ServerUpgradeCodec(Http2MultiplexCodecBuilder.forServer(http2MultiplexCodec()).build()); - } - - private HttpServerUpgradeHandler upgradeHandler() { + Http2MultiplexCodec serverMultiplexCodec = serverMultiplexCodecBuilder.build(); + HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec("server-codec", serverMultiplexCodec); + } else { + return null; + } + }; HttpServerCodec sourceCodec = new HttpServerCodec(); - return new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory); + CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = + new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, serverMultiplexCodec); + p.addLast("server-upgrade", cleartextHttp2ServerUpgradeHandler); + p.addLast("server-messages", new ServerMessages()); } - private Http2MultiplexCodec http2MultiplexCodec() { - Http2FrameLogger frameLogger = new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server"); - return Http2MultiplexCodecBuilder.forServer(new DummyHandler()) - .frameLogger(frameLogger) - .initialSettings(serverConfig.getHttp2Settings()) - .build(); + class ServerRequestHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException { + ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get(); + serverTransport.requestReceived(ctx, fullHttpRequest); + } } + class ServerMessages extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get(); + if (msg instanceof DefaultHttp2SettingsFrame) { + DefaultHttp2SettingsFrame http2SettingsFrame = (DefaultHttp2SettingsFrame) msg; + serverTransport.settingsReceived(ctx, http2SettingsFrame.settings()); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get(); + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws IOException { + ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get(); + serverTransport.exceptionReceived(ctx, cause); + } + } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2Handler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2Handler.java deleted file mode 100644 index 4e22d61..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2Handler.java +++ /dev/null @@ -1,483 +0,0 @@ -package org.xbib.netty.http.server.handler.http2; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.FullHttpMessage; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http2.CharSequenceMap; -import io.netty.handler.codec.http2.Http2CodecUtil; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2Error; -import io.netty.handler.codec.http2.Http2EventAdapter; -import io.netty.handler.codec.http2.Http2Exception; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.Http2LocalFlowController; -import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.codec.http2.Http2Stream; -import io.netty.handler.codec.http2.HttpConversionUtil; -import io.netty.util.AsciiString; -import io.netty.util.internal.ObjectUtil; -import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.ServerConfig; -import org.xbib.netty.http.server.transport.ServerTransport; - -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A HTTP/2 event adapter for a server. - * - * This event adapter expects {@link Http2Settings} are sent from the server before the - * {@link HttpRequest} is submitted by sending a header frame, and, if a body exists, a - * data frame. - */ -@ChannelHandler.Sharable -public class Http2Handler extends Http2EventAdapter { - - private static final Logger logger = Logger.getLogger(Http2Handler.class.getName()); - - private static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0"); - - private final Server server; - - private final ServerConfig serverConfig; - - private final ServerTransport serverTransport; - - private final Http2Connection connection; - - private final Http2Connection.PropertyKey messageKey; - - private final boolean validateHttpHeaders; - - /** - * Constructor for {@link Http2Handler}. - * @param server the server - * @param connection the HTTP/2 connection - * @param validateHeaders true if headers should be validated - */ - public Http2Handler(Server server, Http2Connection connection, boolean validateHeaders) { - this.server = server; - this.serverConfig = server.getServerConfig(); - this.connection = connection; - this.validateHttpHeaders = validateHeaders; - this.messageKey = connection.newKey(); - this.serverTransport = server.newTransport(HTTP_2_0); - } - - /** - * Handles an inbound {@code SETTINGS} frame. - * After frame is received, the request is sent. - * - * @param ctx the context from the handler where the frame was read. - * @param settings the settings received from the remote endpoint. - */ - @Override - public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) { - if (serverConfig.isDebug()) { - logger.log(Level.FINE, () -> "settings received " + settings); - } - try { - serverTransport.settingsReceived(ctx, settings); - } catch (Exception e) { - logger.log(Level.WARNING, e.getMessage(), e); - } - } - - /** - * Handles an inbound {@code HEADERS} frame. - *

- * Only one of the following methods will be called for each {@code HEADERS} frame sequence. - * One will be called when the {@code END_HEADERS} flag has been received. - *

    - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
  • - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
  • - *
  • {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
  • - *
- *

- * To say it another way; the {@link Http2Headers} will contain all of the headers - * for the current message exchange step (additional queuing is not necessary). - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the subject stream for the frame. - * @param headers the received headers. - * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and - * 256 (inclusive). - * @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint - * for this stream. - */ - @Override - public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, - boolean endOfStream) throws Http2Exception { - if (serverConfig.isDebug()) { - logger.log(Level.FINE, () -> "headers received " + headers + " endOfStream " + endOfStream); - } - Http2Stream stream = connection.stream(streamId); - FullHttpMessage msg = beginHeader(ctx, stream, headers); - endHeader(ctx, stream, msg, endOfStream); - } - - /** - * Handles an inbound {@code HEADERS} frame with priority information specified. - * Only called if {@code END_HEADERS} encountered. - *

- * Only one of the following methods will be called for each {@code HEADERS} frame sequence. - * One will be called when the {@code END_HEADERS} flag has been received. - *

    - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
  • - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
  • - *
  • {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
  • - *
- *

- * To say it another way; the {@link Http2Headers} will contain all of the headers - * for the current message exchange step (additional queuing is not necessary). - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the subject stream for the frame. - * @param headers the received headers. - * @param streamDependency the stream on which this stream depends, or 0 if dependent on the - * connection. - * @param weight the new weight for the stream. - * @param exclusive whether or not the stream should be the exclusive dependent of its parent. - * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and - * 256 (inclusive). - * @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint - * for this stream. - */ - @Override - public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, - short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception { - if (serverConfig.isDebug()) { - logger.log(Level.FINE, () -> "headers received (weighted) " + headers + " endOfStream " + endOfStream); - } - Http2Stream stream = connection.stream(streamId); - FullHttpMessage msg = beginHeader(ctx, stream, headers); - if (streamDependency != Http2CodecUtil.CONNECTION_STREAM_ID) { - msg.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), - streamDependency); - } - msg.headers().setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), weight); - endHeader(ctx, stream, msg, endOfStream); - } - - /** - * Handles an inbound {@code DATA} frame. - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the subject stream for the frame. - * @param data payload buffer for the frame. This buffer will be released by the codec. - * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and - * 256 (inclusive). - * @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint for this stream. - * @return the number of bytes that have been processed by the application. The returned bytes are used by the - * inbound flow controller to determine the appropriate time to expand the inbound flow control window (i.e. send - * {@code WINDOW_UPDATE}). Returning a value equal to the length of {@code data} + {@code padding} will effectively - * opt-out of application-level flow control for this frame. Returning a value less than the length of {@code data} - * + {@code padding} will defer the returning of the processed bytes, which the application must later return via - * {@link Http2LocalFlowController#consumeBytes(Http2Stream, int)}. The returned value must - * be >= {@code 0} and <= {@code data.readableBytes()} + {@code padding}. - */ - @Override - public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) - throws Http2Exception { - if (serverConfig.isDebug()) { - logger.log(Level.FINE, () -> "data received " + data); - } - Http2Stream stream = connection.stream(streamId); - FullHttpMessage msg = getMessage(stream); - if (msg == null) { - throw Http2Exception.connectionError(Http2Error.PROTOCOL_ERROR, - "data frame received for unknown stream id %d", streamId); - } - ByteBuf content = msg.content(); - final int dataReadableBytes = data.readableBytes(); - if (content.readableBytes() > serverConfig.getMaxContentLength() - dataReadableBytes) { - throw Http2Exception.connectionError(Http2Error.INTERNAL_ERROR, - "content length exceeded maximum of %d for stream id %d", - serverConfig.getMaxContentLength(), streamId); - } - content.writeBytes(data, data.readerIndex(), dataReadableBytes); - if (endOfStream) { - fireChannelRead(ctx, msg, false, stream); - } - return dataReadableBytes + padding; - } - - /** - * Handles an inbound {@code RST_STREAM} frame. Deletes push stream id if present. - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the stream that is terminating. - * @param errorCode the error code identifying the type of failure. - */ - @Override - public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) { - if (serverConfig.isDebug()) { - logger.log(Level.FINE, () -> "rst stream received: error code = " + errorCode); - } - Http2Stream stream = connection.stream(streamId); - FullHttpMessage msg = getMessage(stream); - if (msg != null) { - removeMessage(stream, true); - } - } - - /** - * Handles an inbound {@code PUSH_PROMISE} frame. Only called if {@code END_HEADERS} encountered. - *

- * Promised requests MUST be authoritative, cacheable, and safe. - * See [RFC http2], Section 8.2. - *

- * Only one of the following methods will be called for each {@code HEADERS} frame sequence. - * One will be called when the {@code END_HEADERS} flag has been received. - *

    - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
  • - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
  • - *
  • {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
  • - *
- *

- * To say it another way; the {@link Http2Headers} will contain all of the headers - * for the current message exchange step (additional queuing is not necessary). - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the stream the frame was sent on. - * @param promisedStreamId the ID of the promised stream. - * @param headers the received headers. - * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and - * 256 (inclusive). - */ - @Override - public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, - Http2Headers headers, int padding) { - if (serverConfig.isDebug()) { - logger.log(Level.FINE, () -> "push promise received: streamId " + streamId + - " promised stream ID = " + promisedStreamId + " headers =" + headers); - } - throw new IllegalStateException("server is not allowd to receive push promise"); - } - - /** - * Notifies the listener that the given stream has now been removed from the connection and - * will no longer be returned via {@link Http2Connection#stream(int)}. The connection may - * maintain inactive streams for some time before removing them. - *

- * If a {@link RuntimeException} is thrown it will be logged and not propagated. - * Throwing from this method is not supported and is considered a programming error. - */ - @Override - public void onStreamRemoved(Http2Stream stream) { - if (serverConfig.isDebug()) { - logger.log(Level.FINE, () -> "stream removed " + stream); - } - removeMessage(stream, true); - } - - - /** - * Get the {@link FullHttpMessage} associated with {@code stream}. - * @param stream The stream to get the associated state from - * @return The {@link FullHttpMessage} associated with {@code stream}. - */ - private FullHttpMessage getMessage(Http2Stream stream) { - return (FullHttpMessage) stream.getProperty(messageKey); - } - - /** - * Make {@code message} be the state associated with {@code stream}. - * @param stream The stream which {@code message} is associated with. - * @param message The message which contains the HTTP semantics. - */ - private void putMessage(Http2Stream stream, FullHttpMessage message) { - FullHttpMessage previous = stream.setProperty(messageKey, message); - if (previous != message && previous != null) { - previous.release(); - } - } - /** - * The stream is out of scope for the HTTP message flow and will no longer be tracked. - * @param stream The stream to remove associated state with - * @param release {@code true} to call release on the value if it is present. {@code false} to not call release. - */ - private void removeMessage(Http2Stream stream, boolean release) { - FullHttpMessage msg = stream.removeProperty(messageKey); - if (release && msg != null) { - msg.release(); - } - } - - private FullHttpMessage beginHeader(ChannelHandlerContext ctx, Http2Stream stream, Http2Headers headers) throws Http2Exception { - FullHttpMessage msg = getMessage(stream); - if (msg == null) { - msg = newMessage(stream, headers, validateHttpHeaders, ctx.alloc()); - } else { - addHttp2ToHttpHeaders(stream.id(), headers, msg.headers(), msg.protocolVersion(), - true, msg instanceof HttpRequest); - } - return msg; - } - - private void endHeader(ChannelHandlerContext ctx, Http2Stream stream, FullHttpMessage msg, boolean endOfStream) { - if (endOfStream) { - fireChannelRead(ctx, msg, getMessage(stream) != msg, stream); - } else { - putMessage(stream, msg); - } - } - - /** - * Set final headers and fire a channel read event. - * - * @param ctx The context to fire the event on - * @param msg The message to send - * @param release {@code true} to call release on the value if it is present. {@code false} to not call release. - * @param stream the stream of the message which is being fired - */ - private void fireChannelRead(ChannelHandlerContext ctx, FullHttpMessage msg, boolean release, - Http2Stream stream) { - removeMessage(stream, release); - HttpUtil.setContentLength(msg, msg.content().readableBytes()); - ctx.fireChannelRead(msg); - } - - /** - * Create a new {@link FullHttpMessage} based upon the current connection parameters. - * - * @param stream The stream to create a message for - * @param headers The headers associated with {@code stream} - * @param validateHttpHeaders - *

    - *
  • {@code true} to validate HTTP headers in the http-codec
  • - *
  • {@code false} not to validate HTTP headers in the http-codec
  • - *
- * @param alloc The {@link ByteBufAllocator} to use to generate the content of the message - * @throws Http2Exception if message can not be created - */ - private FullHttpMessage newMessage(Http2Stream stream, Http2Headers headers, boolean validateHttpHeaders, - ByteBufAllocator alloc) throws Http2Exception { - FullHttpMessage fullHttpMessage = toFullHttpRequest(stream.id(), headers, alloc, validateHttpHeaders); - if (serverConfig.isDebug()) { - logger.log(Level.FINE, headers.toString()); - logger.log(Level.FINE, fullHttpMessage::toString); - } - return fullHttpMessage; - } - - /** - * Create a new object to contain the request data - * - * @param streamId The stream associated with the request - * @param http2Headers The initial set of HTTP/2 headers to create the request with - * @param alloc The {@link ByteBufAllocator} to use to generate the content of the message - * @param validateHttpHeaders
    - *
  • {@code true} to validate HTTP headers in the http-codec
  • - *
  • {@code false} not to validate HTTP headers in the http-codec
  • - *
- * @return A new request object which represents headers/data - * @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x. - */ - public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, - ByteBufAllocator alloc, - boolean validateHttpHeaders) - throws Http2Exception { - final CharSequence method = ObjectUtil.checkNotNull(http2Headers.method(),"method header cannot be null"); - final CharSequence path = ObjectUtil.checkNotNull(http2Headers.path(),"path header cannot be null "); - ByteBuf byteBuf = alloc.buffer(); - FullHttpRequest msg = new DefaultFullHttpRequest(HTTP_2_0, HttpMethod.valueOf(method.toString()), - path.toString(), byteBuf, validateHttpHeaders); - try { - addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, true); - } catch (Http2Exception e) { - msg.release(); - throw e; - } catch (Throwable t) { - msg.release(); - throw Http2Exception.streamError(streamId, Http2Error.PROTOCOL_ERROR, t, "HTTP/2 full request conversion error"); - } - return msg; - } - - /** - * Translate and add HTTP/2 headers to HTTP/1.x headers. - * - * @param streamId The stream associated with {@code sourceHeaders}. - * @param inputHeaders The HTTP/2 headers to convert. - * @param outputHeaders The object which will contain the resulting HTTP/1.x headers.. - * @param httpVersion What HTTP/1.x version {@code outputHeaders} should be treated as when doing the conversion. - * @param isTrailer {@code true} if {@code outputHeaders} should be treated as trailing headers. - * {@code false} otherwise. - * @param isRequest {@code true} if the {@code outputHeaders} will be used in a request message. - * {@code false} for response message. - * @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x. - */ - public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders, - HttpHeaders outputHeaders, - HttpVersion httpVersion, - boolean isTrailer, - boolean isRequest) throws Http2Exception { - - final CharSequenceMap translations = isRequest ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS; - try { - for (Map.Entry entry : inputHeaders) { - final CharSequence name = entry.getKey(); - final CharSequence value = entry.getValue(); - AsciiString translatedName = translations.get(name); - if (translatedName != null) { - outputHeaders.add(translatedName, AsciiString.of(value)); - } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { - // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 - // All headers that start with ':' are only valid in HTTP/2 context - if (name.length() == 0 || name.charAt(0) == ':') { - throw Http2Exception.streamError(streamId, Http2Error.PROTOCOL_ERROR, - "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name); - } - if (HttpHeaderNames.COOKIE.equals(name)) { - // combine the cookie values into 1 header entry. - // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 - String existingCookie = outputHeaders.get(HttpHeaderNames.COOKIE); - outputHeaders.set(HttpHeaderNames.COOKIE, - (existingCookie != null) ? (existingCookie + "; " + value) : value); - } else { - outputHeaders.add(name, value); - } - } - } - } catch (Http2Exception ex) { - throw ex; - } catch (Throwable t) { - throw Http2Exception.streamError(streamId, Http2Error.PROTOCOL_ERROR, t, "HTTP/2 headers conversion error"); - } - outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING); - outputHeaders.remove(HttpHeaderNames.TRAILER); - if (!isTrailer) { - outputHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId); - HttpUtil.setKeepAlive(outputHeaders, httpVersion, true); - } - } - - /** - * Translations from HTTP/2 header name to the HTTP/1.x equivalent. - */ - private static final CharSequenceMap - REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap(); - private static final CharSequenceMap - RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap(); - static { - RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(), - HttpHeaderNames.HOST); - RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.SCHEME.value(), - HttpConversionUtil.ExtensionHeaderNames.SCHEME.text()); - REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS); - RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(), - HttpConversionUtil.ExtensionHeaderNames.PATH.text()); - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2RequestHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2RequestHandler.java deleted file mode 100644 index 78734ed..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2RequestHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.xbib.netty.http.server.handler.http2; - -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.FullHttpRequest; -import org.xbib.netty.http.server.transport.ServerTransport; - -import java.io.IOException; - -@ChannelHandler.Sharable -public class Http2RequestHandler extends SimpleChannelInboundHandler { - - @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest httpRequest) throws IOException { - ServerTransport transport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get(); - transport.requestReceived(ctx, httpRequest); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) { - ctx.fireChannelInactive(); - ServerTransport transport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get(); - //transport.fail(new IOException("channel closed")); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - ServerTransport transport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get(); - //transport.fail(cause); - ctx.channel().close(); - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2SettingsHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2SettingsHandler.java deleted file mode 100644 index 17f7fb6..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2SettingsHandler.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.xbib.netty.http.server.handler.http2; - -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http2.Http2Settings; -import org.xbib.netty.http.server.transport.ServerTransport; - -@ChannelHandler.Sharable -public class Http2SettingsHandler extends SimpleChannelInboundHandler { - - @Override - protected void channelRead0(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception { - ServerTransport transport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get(); - transport.settingsReceived(ctx, http2Settings); - ctx.pipeline().remove(this); - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java new file mode 100644 index 0000000..d4122be --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java @@ -0,0 +1,228 @@ +package org.xbib.netty.http.server.handler.http2; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.EncoderException; +import io.netty.handler.codec.MessageToMessageCodec; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpScheme; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http2.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2MultiplexCodec; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamFrame; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.internal.UnstableApi; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This handler converts from {@link Http2StreamFrame} to {@link HttpObject}, + * and back. It can be used as an adapter in conjunction with {@link + * Http2MultiplexCodec} to make http/2 connections backward-compatible with + * {@link ChannelHandler}s expecting {@link HttpObject}. + * + * For simplicity, it converts to chunked encoding unless the entire stream + * is a single header. + */ +@UnstableApi +@Sharable +public class Http2StreamFrameToHttpObjectCodec extends MessageToMessageCodec { + + private static final Logger logger = Logger.getLogger(Http2StreamFrameToHttpObjectCodec.class.getName()); + + private final boolean isServer; + + private final boolean validateHeaders; + + private HttpScheme scheme; + + public Http2StreamFrameToHttpObjectCodec(final boolean isServer, + final boolean validateHeaders) { + this.isServer = isServer; + this.validateHeaders = validateHeaders; + scheme = HttpScheme.HTTP; + } + + public Http2StreamFrameToHttpObjectCodec(final boolean isServer) { + this(isServer, true); + } + + @Override + public boolean acceptInboundMessage(Object msg) throws Exception { + return (msg instanceof Http2HeadersFrame) || (msg instanceof Http2DataFrame); + } + + @Override + protected void decode(ChannelHandlerContext ctx, Http2StreamFrame frame, List out) throws Exception { + if (frame instanceof Http2HeadersFrame) { + int id = frame.stream() != null ? frame.stream().id() : -1; + Http2HeadersFrame headersFrame = (Http2HeadersFrame) frame; + Http2Headers headers = headersFrame.headers(); + + final CharSequence status = headers.status(); + + // 100-continue response is a special case where Http2HeadersFrame#isEndStream=false + // but we need to decode it as a FullHttpResponse to play nice with HttpObjectAggregator. + if (null != status && HttpResponseStatus.CONTINUE.codeAsText().contentEquals(status)) { + final FullHttpMessage fullMsg = newFullMessage(id, headers, ctx.alloc()); + out.add(fullMsg); + return; + } + + if (headersFrame.isEndStream()) { + if (headers.method() == null && status == null) { + LastHttpContent last = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders); + HttpConversionUtil.addHttp2ToHttpHeaders(id, headers, last.trailingHeaders(), + HttpVersion.HTTP_1_1, true, true); + out.add(last); + } else { + FullHttpMessage full = newFullMessage(id, headers, ctx.alloc()); + out.add(full); + } + } else { + HttpMessage req = newMessage(id, headers); + if (!HttpUtil.isContentLengthSet(req)) { + req.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + } + out.add(req); + } + } else if (frame instanceof Http2DataFrame) { + Http2DataFrame dataFrame = (Http2DataFrame) frame; + if (dataFrame.isEndStream()) { + out.add(new DefaultLastHttpContent(dataFrame.content().retain(), validateHeaders)); + } else { + out.add(new DefaultHttpContent(dataFrame.content().retain())); + } + } + } + + private void encodeLastContent(LastHttpContent last, List out) { + boolean needFiller = !(last instanceof FullHttpMessage) && last.trailingHeaders().isEmpty(); + if (last.content().isReadable() || needFiller) { + out.add(new DefaultHttp2DataFrame(last.content().retain(), last.trailingHeaders().isEmpty())); + } + if (!last.trailingHeaders().isEmpty()) { + Http2Headers headers = HttpConversionUtil.toHttp2Headers(last.trailingHeaders(), validateHeaders); + out.add(new DefaultHttp2HeadersFrame(headers, true)); + } + } + + /** + * Encode from an {@link HttpObject} to an {@link Http2StreamFrame}. This method will + * be called for each written message that can be handled by this encoder. + * + * NOTE: 100-Continue responses that are NOT {@link FullHttpResponse} will be rejected. + * + * @param ctx the {@link ChannelHandlerContext} which this handler belongs to + * @param obj the {@link HttpObject} message to encode + * @param out the {@link List} into which the encoded msg should be added + * needs to do some kind of aggregation + * @throws Exception is thrown if an error occurs + */ + @Override + protected void encode(ChannelHandlerContext ctx, HttpObject obj, List out) throws Exception { + // 100-continue is typically a FullHttpResponse, but the decoded + // Http2HeadersFrame should not be marked as endStream=true + if (obj instanceof HttpResponse) { + final HttpResponse res = (HttpResponse) obj; + if (res.status().equals(HttpResponseStatus.CONTINUE)) { + if (res instanceof FullHttpResponse) { + final Http2Headers headers = toHttp2Headers(res); + out.add(new DefaultHttp2HeadersFrame(headers, false)); + return; + } else { + throw new EncoderException( + HttpResponseStatus.CONTINUE.toString() + " must be a FullHttpResponse"); + } + } + } + + if (obj instanceof HttpMessage) { + Http2Headers headers = toHttp2Headers((HttpMessage) obj); + boolean noMoreFrames = false; + if (obj instanceof FullHttpMessage) { + FullHttpMessage full = (FullHttpMessage) obj; + noMoreFrames = !full.content().isReadable() && full.trailingHeaders().isEmpty(); + } + + out.add(new DefaultHttp2HeadersFrame(headers, noMoreFrames)); + } + + if (obj instanceof LastHttpContent) { + LastHttpContent last = (LastHttpContent) obj; + encodeLastContent(last, out); + } else if (obj instanceof HttpContent) { + HttpContent cont = (HttpContent) obj; + out.add(new DefaultHttp2DataFrame(cont.content().retain(), false)); + } + } + + private Http2Headers toHttp2Headers(final HttpMessage msg) { + if (msg instanceof HttpRequest) { + msg.headers().set( + HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), + scheme.name()); + } + + return HttpConversionUtil.toHttp2Headers(msg, validateHeaders); + } + + private HttpMessage newMessage(final int id, + final Http2Headers headers) throws Http2Exception { + return isServer ? + HttpConversionUtil.toHttpRequest(id, headers, validateHeaders) : + HttpConversionUtil.toHttpResponse(id, headers, validateHeaders); + } + + private FullHttpMessage newFullMessage(final int id, + final Http2Headers headers, + final ByteBufAllocator alloc) throws Http2Exception { + return isServer ? + HttpConversionUtil.toFullHttpRequest(id, headers, alloc, validateHeaders) : + HttpConversionUtil.toFullHttpResponse(id, headers, alloc, validateHeaders); + } + + @Override + public void handlerAdded(final ChannelHandlerContext ctx) throws Exception { + super.handlerAdded(ctx); + + // this handler is typically used on an Http2StreamChannel. at this + // stage, ssl handshake should've been established. checking for the + // presence of SslHandler in the parent's channel pipeline to + // determine the HTTP scheme should suffice, even for the case where + // SniHandler is used. + scheme = isSsl(ctx) ? HttpScheme.HTTPS : HttpScheme.HTTP; + } + + protected boolean isSsl(final ChannelHandlerContext ctx) { + final Channel ch = ctx.channel(); + final Channel connChannel = (ch instanceof Http2StreamChannel) ? ch.parent() : ch; + return null != connChannel.pipeline().get(SslHandler.class); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/UserEventLogger.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/UserEventLogger.java deleted file mode 100644 index b0341c7..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/UserEventLogger.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.xbib.netty.http.server.handler.http2; - -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A Netty handler that logs user events. - */ -@ChannelHandler.Sharable -public class UserEventLogger extends ChannelInboundHandlerAdapter { - - private static final Logger logger = Logger.getLogger(UserEventLogger.class.getName()); - - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { - logger.log(Level.FINE, () -> "got user event " + evt); - ctx.fireUserEventTriggered(evt); - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/ClosedSessionException.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/ClosedSessionException.java deleted file mode 100644 index 9048ce1..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/ClosedSessionException.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.xbib.netty.http.server.internal; - -/** - * A {@link RuntimeException} raised when the connection to the remote peer has been closed unexpectedly. - */ -public final class ClosedSessionException extends RuntimeException { -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http1ObjectEncoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http1ObjectEncoder.java deleted file mode 100644 index 4f42a46..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http1ObjectEncoder.java +++ /dev/null @@ -1,192 +0,0 @@ -package org.xbib.netty.http.server.internal; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.http.DefaultHttpContent; -import io.netty.handler.codec.http.DefaultHttpResponse; -import io.netty.handler.codec.http.DefaultLastHttpContent; -import io.netty.handler.codec.http.HttpContent; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http2.Http2Error; -import io.netty.util.collection.IntObjectHashMap; -import io.netty.util.collection.IntObjectMap; - -import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.ArrayDeque; -import java.util.Map.Entry; -import java.util.Queue; -import java.util.logging.Logger; - -/** - * HTTP 1 object encoder. - */ -public final class Http1ObjectEncoder extends HttpObjectEncoder { - - private static final Logger logger = Logger.getLogger(Http1ObjectEncoder.class.getName()); - - /** - * The map which maps a request ID to its related pending response. - */ - private final IntObjectMap pendingWrites = new IntObjectHashMap<>(); - /** - * The ID of the request which is at its turn to send a response. - */ - private int currentId = 1; - /** - * The minimum ID of the request whose stream has been closed/reset. - */ - private int minClosedId = Integer.MAX_VALUE; - /** - * The maximum known ID with pending writes. - */ - private int maxIdWithPendingWrites = Integer.MIN_VALUE; - - @Override - protected ChannelFuture doWriteHeaders(ChannelHandlerContext ctx, int id, int streamId, - HttpHeaders headers, HttpResponseStatus status, boolean endStream) { - if (id >= minClosedId) { - return ctx.newFailedFuture(new ClosedSessionException()); - } - try { - return write(ctx, id, new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, headers), endStream); - } catch (Throwable t) { - return ctx.newFailedFuture(t); - } - } - - @Override - protected ChannelFuture doWriteData(ChannelHandlerContext ctx, int id, int streamId, ByteBuf buf, boolean endStream) { - if (id >= minClosedId) { - return ctx.newFailedFuture(new ClosedSessionException()); - } - try { - final HttpContent content; - if (endStream) { - content = new DefaultLastHttpContent(buf); - } else { - content = new DefaultHttpContent(buf); - } - return write(ctx, id, content, endStream); - } catch (Throwable t) { - return ctx.newFailedFuture(t); - } - } - - private ChannelFuture write(ChannelHandlerContext ctx, int id, HttpObject obj, boolean endStream) { - if (id < currentId) { - return ctx.newFailedFuture(new ClosedSessionException()); - } - final PendingWrites currentPendingWrites = pendingWrites.get(id); - if (id == currentId) { - if (currentPendingWrites != null) { - pendingWrites.remove(id); - flushPendingWrites(ctx, currentPendingWrites); - } - final ChannelFuture future = ctx.write(obj); - if (endStream) { - currentId++; - for (;;) { - final PendingWrites nextPendingWrites = pendingWrites.get(currentId); - if (nextPendingWrites == null) { - break; - } - flushPendingWrites(ctx, nextPendingWrites); - if (!nextPendingWrites.isEndOfStream()) { - break; - } - pendingWrites.remove(currentId); - currentId++; - } - } - ctx.flush(); - return future; - } else { - final ChannelPromise promise = ctx.newPromise(); - final Entry entry = new SimpleImmutableEntry<>(obj, promise); - if (currentPendingWrites == null) { - final PendingWrites newPendingWrites = new PendingWrites(); - maxIdWithPendingWrites = Math.max(maxIdWithPendingWrites, id); - newPendingWrites.add(entry); - pendingWrites.put(id, newPendingWrites); - } else { - currentPendingWrites.add(entry); - if (endStream) { - currentPendingWrites.setEndOfStream(); - } - } - return promise; - } - } - - private static void flushPendingWrites(ChannelHandlerContext ctx, PendingWrites pendingWrites) { - while (true) { - final Entry e = pendingWrites.poll(); - if (e == null) { - break; - } - ctx.write(e.getKey(), e.getValue()); - } - } - - @Override - protected ChannelFuture doWriteReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) { - minClosedId = Math.min(minClosedId, id); - for (int i = minClosedId; i <= maxIdWithPendingWrites; i++) { - final PendingWrites pendingWrites = this.pendingWrites.remove(i); - while (true) { - final Entry e = pendingWrites.poll(); - if (e == null) { - break; - } - e.getValue().tryFailure(new ClosedSessionException()); - } - } - final ChannelFuture f = ctx.write(Unpooled.EMPTY_BUFFER); - if (currentId >= minClosedId) { - f.addListener(ChannelFutureListener.CLOSE); - } - return f; - } - - @Override - protected void doClose() { - if (pendingWrites.isEmpty()) { - return; - } - ClosedSessionException cause = new ClosedSessionException(); - for (Queue> queue : pendingWrites.values()) { - while (true) { - final Entry e = queue.poll(); - if (e == null) { - break; - } - e.getValue().tryFailure(cause); - } - } - pendingWrites.clear(); - } - - private static final class PendingWrites extends ArrayDeque> { - - private boolean endOfStream; - - PendingWrites() { - super(4); - } - - boolean isEndOfStream() { - return endOfStream; - } - - void setEndOfStream() { - endOfStream = true; - } - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http2ObjectEncoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http2ObjectEncoder.java deleted file mode 100644 index dd299a9..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http2ObjectEncoder.java +++ /dev/null @@ -1,167 +0,0 @@ -package org.xbib.netty.http.server.internal; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http2.CharSequenceMap; -import io.netty.handler.codec.http2.DefaultHttp2Headers; -import io.netty.handler.codec.http2.Http2ConnectionEncoder; -import io.netty.handler.codec.http2.Http2Error; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.Http2Stream; -import io.netty.handler.codec.http2.HttpConversionUtil; -import io.netty.util.AsciiString; - -import java.util.Iterator; -import java.util.Map; -import java.util.Objects; - -import static io.netty.util.AsciiString.EMPTY_STRING; -import static io.netty.util.ByteProcessor.FIND_SEMI_COLON; - -/** - * - */ -public final class Http2ObjectEncoder extends HttpObjectEncoder { - - /** - * The set of headers that should not be directly copied when converting headers from HTTP to HTTP/2. - */ - private static final CharSequenceMap HTTP_TO_HTTP2_HEADER_BLACKLIST = - new CharSequenceMap(); - static { - HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.CONNECTION, EMPTY_STRING); - @SuppressWarnings("deprecation") - AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE; - HTTP_TO_HTTP2_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING); - @SuppressWarnings("deprecation") - AsciiString proxyConnection = HttpHeaderNames.PROXY_CONNECTION; - HTTP_TO_HTTP2_HEADER_BLACKLIST.add(proxyConnection, EMPTY_STRING); - HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING); - HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.HOST, EMPTY_STRING); - HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING); - HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING); - HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING); - HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpConversionUtil.ExtensionHeaderNames.PATH.text(), EMPTY_STRING); - } - - private final Http2ConnectionEncoder encoder; - - public Http2ObjectEncoder(Http2ConnectionEncoder encoder) { - super(); - this.encoder = Objects.requireNonNull(encoder, "encoder"); - } - - @Override - protected ChannelFuture doWriteHeaders(ChannelHandlerContext ctx, int id, int streamId, - HttpHeaders headers, HttpResponseStatus status, - boolean endStream) { - final ChannelFuture future = validateStream(ctx, streamId); - if (future != null) { - return future; - } - Http2Headers http2Headers = toHttp2Headers(headers, status, false); - return encoder.writeHeaders(ctx, streamId, http2Headers, 0, endStream, ctx.newPromise()); - } - - @Override - protected ChannelFuture doWriteData(ChannelHandlerContext ctx, int id, int streamId, ByteBuf data, - boolean endStream) { - final ChannelFuture future = validateStream(ctx, streamId); - if (future != null) { - return future; - } - return encoder.writeData(ctx, streamId, data, 0, endStream, ctx.newPromise()); - } - - @Override - protected ChannelFuture doWriteReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) { - final ChannelFuture future = validateStream(ctx, streamId); - if (future != null) { - return future; - } - return encoder.writeRstStream(ctx, streamId, error.code(), ctx.newPromise()); - } - - - @Override - protected void doClose() { - } - - private ChannelFuture validateStream(ChannelHandlerContext ctx, int streamId) { - final Http2Stream stream = encoder.connection().stream(streamId); - if (stream != null) { - switch (stream.state()) { - case RESERVED_LOCAL: - case OPEN: - case HALF_CLOSED_REMOTE: - break; - default: - return ctx.newFailedFuture(new IllegalStateException("stream state = " + stream.state().name())); - } - } else if (encoder.connection().streamMayHaveExisted(streamId)) { - return ctx.newFailedFuture(new IllegalStateException("stream may have existed")); - } - return null; - } - - public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, - HttpResponseStatus status, - boolean validateHeaders) { - final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size()); - out.status(status.codeAsText()); - toHttp2Headers(inHeaders, out); - return out; - } - - public static void toHttp2Headers(HttpHeaders inHeaders, Http2Headers outHeaders) { - Iterator> iter = inHeaders.iteratorCharSequence(); - while (iter.hasNext()) { - Map.Entry entry = iter.next(); - final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase(); - if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName)) { - // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 makes a special exception for TE - if (aName.contentEqualsIgnoreCase(HttpHeaderNames.TE) && - !AsciiString.contentEqualsIgnoreCase(entry.getValue(), HttpHeaderValues.TRAILERS)) { - throw new IllegalArgumentException("Invalid value for " + HttpHeaderNames.TE + ": " + - entry.getValue()); - } - if (aName.contentEqualsIgnoreCase(HttpHeaderNames.COOKIE)) { - AsciiString value = AsciiString.of(entry.getValue()); - // split up cookies to allow for better compression - // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 - try { - int index = value.forEachByte(FIND_SEMI_COLON); - if (index != -1) { - int start = 0; - do { - outHeaders.add(HttpHeaderNames.COOKIE, value.subSequence(start, index, false)); - // skip 2 characters "; " (see https://tools.ietf.org/html/rfc6265#section-4.2.1) - start = index + 2; - } while (start < value.length() && - (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1); - if (start >= value.length()) { - throw new IllegalArgumentException("cookie value is of unexpected format: " + value); - } - outHeaders.add(HttpHeaderNames.COOKIE, value.subSequence(start, value.length(), false)); - } else { - outHeaders.add(HttpHeaderNames.COOKIE, value); - } - } catch (Exception e) { - // This is not expect to happen because FIND_SEMI_COLON never throws but must be caught - // because of the ByteProcessor interface. - throw new IllegalStateException(e); - } - } else { - outHeaders.add(aName, entry.getValue()); - } - } - } - } -} - - diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/HttpObjectEncoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/HttpObjectEncoder.java deleted file mode 100644 index d60fa23..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/HttpObjectEncoder.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.xbib.netty.http.server.internal; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http2.Http2Error; - -/** - * HTTP object encoder. - */ -public abstract class HttpObjectEncoder { - - private volatile boolean closed; - - /** - * Writes an {@link HttpHeaders}. - */ - public final ChannelFuture writeHeaders(ChannelHandlerContext ctx, int id, int streamId, HttpHeaders headers, - HttpResponseStatus status, boolean endStream) { - if (!ctx.channel().eventLoop().inEventLoop()) { - throw new IllegalStateException(); - } - if (closed) { - return newFailedFuture(ctx); - } - return doWriteHeaders(ctx, id, streamId, headers, status, endStream); - } - - protected abstract ChannelFuture doWriteHeaders(ChannelHandlerContext ctx, int id, int streamId, - HttpHeaders headers, HttpResponseStatus status, boolean endStream); - - public final ChannelFuture writeData(ChannelHandlerContext ctx, int id, int streamId, ByteBuf data, boolean endStream) { - if (!ctx.channel().eventLoop().inEventLoop()) { - throw new IllegalStateException(); - } - if (closed) { - return newFailedFuture(ctx); - } - return doWriteData(ctx, id, streamId, data, endStream); - } - - protected abstract ChannelFuture doWriteData(ChannelHandlerContext ctx, int id, int streamId, ByteBuf data, - boolean endStream); - - /** - * Resets the specified stream. If the session protocol doesn't support multiplexing or the connection - * is in unrecoverable state, the connection will be closed. For example, in an HTTP/1 connection, this - * will lead the connection to be closed immediately or after the previous requests that are not reset. - */ - public final ChannelFuture writeReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) { - if (closed) { - return newFailedFuture(ctx); - } - return doWriteReset(ctx, id, streamId, error); - } - - protected abstract ChannelFuture doWriteReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error); - - /** - * Releases the resources related with this encoder and fails any unfinished writes. - */ - public void close() { - if (closed) { - return; - } - closed = true; - doClose(); - } - - protected abstract void doClose(); - - private static ChannelFuture newFailedFuture(ChannelHandlerContext ctx) { - return ctx.newFailedFuture(new ClosedSessionException()); - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/package-info.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/package-info.java deleted file mode 100644 index cd28b37..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Internal classes for Netty HTTP server. - */ -package org.xbib.netty.http.server.internal; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java index 780eaab..251e4a3 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java @@ -1,5 +1,6 @@ package org.xbib.netty.http.server.transport; +import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpVersion; @@ -14,9 +15,13 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; abstract class BaseServerTransport implements ServerTransport { + private static final Logger logger = Logger.getLogger(BaseServerTransport.class.getName()); + protected static final AtomicInteger requestCounter = new AtomicInteger(); private static final List METHODS = Arrays.asList("GET", "HEAD", "OPTIONS"); @@ -27,6 +32,11 @@ abstract class BaseServerTransport implements ServerTransport { this.server = server; } + @Override + public void exceptionReceived(ChannelHandlerContext ctx, Throwable throwable) throws IOException { + logger.log(Level.WARNING, throwable.getMessage(), throwable); + } + /** * Accepts a request, performing various validation checks * and required special header handling, possibly returning an @@ -92,9 +102,9 @@ abstract class BaseServerTransport implements ServerTransport { // "*" is a special server-wide (no-context) request supported by OPTIONS boolean isServerOptions = path.equals("*") && method.equals("OPTIONS"); methods.addAll(isServerOptions ? virtualServer.getMethods() : handlers.keySet()); - serverResponse.getHeaders().add(HttpHeaderNames.ALLOW, String.join(", ", methods)); + serverResponse.setHeader(HttpHeaderNames.ALLOW, String.join(", ", methods)); if (method.equals("OPTIONS")) { // default OPTIONS handler - serverResponse.getHeaders().add(HttpHeaderNames.CONTENT_LENGTH, "0"); // RFC2616#9.2 + serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, "0"); // RFC2616#9.2 serverResponse.write(200); } else if (virtualServer.getMethods().contains(method)) { serverResponse.write(405); // supported by server, but not this context (nor built-in) diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java new file mode 100644 index 0000000..e495544 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java @@ -0,0 +1,175 @@ +package org.xbib.netty.http.server.transport; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http2.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.util.AsciiString; +import org.xbib.netty.http.server.ServerName; + +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class Http2ServerResponse implements ServerResponse { + + private final ServerRequest serverRequest; + + private final ChannelHandlerContext ctx; + + private Http2Headers headers; + + public Http2ServerResponse(ServerRequest serverRequest, ChannelHandlerContext ctx) { + this.serverRequest = serverRequest; + this.ctx = ctx; + this.headers = new DefaultHttp2Headers(); + } + + @Override + public void setHeader(AsciiString name, String value) { + headers.set(name, value); + } + + @Override + public void write(String text) { + write(200, "text/plain; charset=utf-8", text); + } + + /** + * Sends an error response with the given status and default body. + * + * @param status the response status + */ + @Override + public void writeError(int status) { + writeError(status, status < 400 ? ":)" : "sorry it didn't work out :("); + } + + /** + * Sends an error response with the given status and detailed message. + * An HTML body is created containing the status and its description, + * as well as the message, which is escaped using the + * {@link #escapeHTML escape} method. + * + * @param status the response status + * @param text the text body (sent as text/html) + */ + @Override + public void writeError(int status, String text) { + write(status, "text/html; charset=utf-8", + String.format("%n%n%d %s%n" + + "

%d %s

%n

%s

%n", + status, HttpResponseStatus.valueOf(status).reasonPhrase(), + status, HttpResponseStatus.valueOf(status).reasonPhrase(), + escapeHTML(text))); + } + + @Override + public void write(int status) { + write(status, null, (ByteBuf) null); + } + + @Override + public void write(int status, String contentType, String text) { + write(status, contentType, ByteBufUtil.writeUtf8(ctx.alloc(), text)); + } + + @Override + public void write(int status, String contentType, String text, Charset charset) { + write(status, contentType, ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.allocate(text.length()).append(text), charset)); + } + + @Override + public void write(int status, String contentType, ByteBuf byteBuf) { + if (byteBuf != null) { + CharSequence s = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (s == null) { + s = contentType != null ? contentType : HttpHeaderValues.APPLICATION_OCTET_STREAM; + headers.add(HttpHeaderNames.CONTENT_TYPE, s); + } + if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH) && !headers.contains(HttpHeaderNames.TRANSFER_ENCODING)) { + int length = byteBuf.readableBytes(); + if (length < 0) { + headers.add(HttpHeaderNames.TRANSFER_ENCODING, "chunked"); + } else { + headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(length)); + } + } + if (serverRequest != null && "close".equalsIgnoreCase(serverRequest.getRequest().headers().get(HttpHeaderNames.CONNECTION)) && + !headers.contains(HttpHeaderNames.CONNECTION)) { + headers.add(HttpHeaderNames.CONNECTION, "close"); + } + if (!headers.contains(HttpHeaderNames.DATE)) { + headers.add(HttpHeaderNames.DATE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC))); + } + headers.add(HttpHeaderNames.SERVER, ServerName.getServerName()); + } + if (serverRequest != null) { + Integer streamId = serverRequest.streamId(); + if (streamId != null) { + headers.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId); + } + } + Http2Headers http2Headers = new DefaultHttp2Headers() + .status(HttpResponseStatus.valueOf(status).codeAsText()) + .add(headers); + ctx.channel().write(new DefaultHttp2HeadersFrame(http2Headers,byteBuf == null)); + if (byteBuf != null) { + ctx.channel().write(new DefaultHttp2DataFrame(byteBuf, true)); + } + ctx.channel().flush(); + } + + /** + * Returns an HTML-escaped version of the given string for safe display + * within a web page. The characters '&', '>' and '<' must always + * be escaped, and single and double quotes must be escaped within + * attribute values; this method escapes them always. This method can + * be used for generating both HTML and XHTML valid content. + * + * @param s the string to escape + * @return the escaped string + * @see The W3C FAQ + */ + private static String escapeHTML(String s) { + int len = s.length(); + StringBuilder es = new StringBuilder(len + 30); + int start = 0; + for (int i = 0; i < len; i++) { + String ref = null; + switch (s.charAt(i)) { + case '&': + ref = "&"; + break; + case '>': + ref = ">"; + break; + case '<': + ref = "<"; + break; + case '"': + ref = """; + break; + case '\'': + ref = "'"; + break; + default: + break; + } + if (ref != null) { + es.append(s.substring(start, i)).append(ref); + start = i + 1; + } + } + return start == 0 ? s : es.append(s.substring(start)).toString(); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java index ea5aefe..69e29b6 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java @@ -4,41 +4,42 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.HttpConversionUtil; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.context.VirtualServer; import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; public class Http2ServerTransport extends BaseServerTransport { - private static final Logger logger = Logger.getLogger(Http2ServerTransport.class.getName()); - public Http2ServerTransport(Server server) { super(server); } @Override public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException { - logger.log(Level.INFO, "requestReceived"); + requestReceived(ctx, fullHttpRequest, null); + } + + @Override + public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) throws IOException { int requestId = requestCounter.incrementAndGet(); VirtualServer virtualServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); if (virtualServer == null) { virtualServer = server.getDefaultVirtualServer(); } HttpAddress httpAddress = server.getServerConfig().getAddress(); + Integer streamId = fullHttpRequest.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); ServerRequest serverRequest = new ServerRequest(virtualServer, httpAddress, fullHttpRequest, - null, requestId); - ServerResponse serverResponse = new Http1ServerResponse(httpAddress.getVersion(), serverRequest, ctx); + sequenceId, streamId, requestId); + ServerResponse serverResponse = new Http2ServerResponse(serverRequest, ctx); if (acceptRequest(serverRequest, serverResponse)) { handle(serverRequest, serverResponse); } } @Override - public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception { - logger.log(Level.INFO, "settings received"); + public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) { } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java similarity index 84% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerResponse.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java index 6452e16..e381b69 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java @@ -12,17 +12,20 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.AsciiString; import org.xbib.netty.http.server.ServerName; +import org.xbib.netty.http.server.handler.http.HttpPipelinedResponse; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.logging.Logger; -public class Http1ServerResponse implements ServerResponse { +public class HttpServerResponse implements ServerResponse { - private final HttpVersion httpVersion; + private static final Logger logger = Logger.getLogger(HttpServerResponse.class.getName()); private final ServerRequest serverRequest; @@ -32,8 +35,7 @@ public class Http1ServerResponse implements ServerResponse { private HttpHeaders trailingHeaders; - public Http1ServerResponse(HttpVersion httpVersion, ServerRequest serverRequest, ChannelHandlerContext ctx) { - this.httpVersion = httpVersion; + public HttpServerResponse(ServerRequest serverRequest, ChannelHandlerContext ctx) { this.serverRequest = serverRequest; this.ctx = ctx; this.headers = new DefaultHttpHeaders(); @@ -41,8 +43,8 @@ public class Http1ServerResponse implements ServerResponse { } @Override - public HttpHeaders getHeaders() { - return headers; + public void setHeader(AsciiString name, String value) { + headers.set(name, value); } @Override @@ -120,12 +122,20 @@ public class Http1ServerResponse implements ServerResponse { headers.add(HttpHeaderNames.SERVER, ServerName.getServerName()); } FullHttpResponse fullHttpResponse = byteBuf != null ? - new DefaultFullHttpResponse(httpVersion, + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(status), byteBuf, headers, trailingHeaders) : - new DefaultFullHttpResponse(httpVersion, + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(status), Unpooled.EMPTY_BUFFER, headers, trailingHeaders); - if (ctx.channel().isWritable()) { - ctx.channel().writeAndFlush(fullHttpResponse); + if (serverRequest != null && serverRequest.getSequenceId() != null) { + HttpPipelinedResponse httpPipelinedResponse = new HttpPipelinedResponse(fullHttpResponse, + ctx.channel().newPromise(), serverRequest.getSequenceId()); + if (ctx.channel().isWritable()) { + ctx.channel().writeAndFlush(httpPipelinedResponse); + } + } else { + if (ctx.channel().isWritable()) { + ctx.channel().writeAndFlush(fullHttpResponse); + } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerTransport.java similarity index 70% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerTransport.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerTransport.java index 4d8af49..c6962dc 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerTransport.java @@ -10,14 +10,20 @@ import org.xbib.netty.http.server.context.VirtualServer; import java.io.IOException; -public class Http1ServerTransport extends BaseServerTransport { +public class HttpServerTransport extends BaseServerTransport { - public Http1ServerTransport(Server server) { + public HttpServerTransport(Server server) { super(server); } @Override public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException { + requestReceived(ctx, fullHttpRequest, 0); + } + + @Override + public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) + throws IOException { int requestId = requestCounter.incrementAndGet(); VirtualServer virtualServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); if (virtualServer == null) { @@ -25,15 +31,15 @@ public class Http1ServerTransport extends BaseServerTransport { } HttpAddress httpAddress = server.getServerConfig().getAddress(); ServerRequest serverRequest = new ServerRequest(virtualServer, httpAddress, fullHttpRequest, - null, requestId); - ServerResponse serverResponse = new Http1ServerResponse(httpAddress.getVersion(), serverRequest, ctx); + sequenceId, null, requestId); + ServerResponse serverResponse = new HttpServerResponse(serverRequest, ctx); if (acceptRequest(serverRequest, serverResponse)) { handle(serverRequest, serverResponse); } } @Override - public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception { - + public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) { + // there are no settings in HTTP 1 } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java index eaf506f..3bd4559 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java @@ -15,15 +15,18 @@ public class ServerRequest { private final FullHttpRequest httpRequest; + private final Integer sequenceId; + private final Integer streamId; private final Integer requestId; - public ServerRequest(VirtualServer virtualServer, HttpAddress httpAddress, FullHttpRequest httpRequest, - Integer streamId, Integer requestId) { + public ServerRequest(VirtualServer virtualServer, HttpAddress httpAddress, + FullHttpRequest httpRequest, Integer sequenceId, Integer streamId, Integer requestId) { this.virtualServer = virtualServer; this.httpAddress = httpAddress; this.httpRequest = httpRequest; + this.sequenceId = sequenceId; this.streamId = streamId; this.requestId = requestId; } @@ -40,6 +43,10 @@ public class ServerRequest { return httpRequest; } + public Integer getSequenceId() { + return sequenceId; + } + public Integer streamId() { return streamId; } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerResponse.java index 2643be6..297e813 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerResponse.java @@ -1,7 +1,7 @@ package org.xbib.netty.http.server.transport; import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.http.HttpHeaders; +import io.netty.util.AsciiString; import java.nio.charset.Charset; @@ -10,7 +10,7 @@ import java.nio.charset.Charset; */ public interface ServerResponse { - HttpHeaders getHeaders(); + void setHeader(AsciiString name, String value); void write(String text); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java index a62fd0e..14ef451 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java @@ -13,6 +13,9 @@ public interface ServerTransport { void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException; + void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) throws IOException; + void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception; + void exceptionReceived(ChannelHandlerContext ctx, Throwable throwable) throws IOException; } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/LoggingBase.java b/netty-http-server/src/test/java/org/xbib/TestBase.java similarity index 62% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/LoggingBase.java rename to netty-http-server/src/test/java/org/xbib/TestBase.java index 18f49d7..7b46847 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/LoggingBase.java +++ b/netty-http-server/src/test/java/org/xbib/TestBase.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test; +package org.xbib; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; @@ -7,20 +7,26 @@ import java.util.logging.LogManager; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; -public class LoggingBase { +public class TestBase { static { + + System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); + System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); + //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); + //System.setProperty("io.netty.leakDetection.level", "paranoid"); + System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n"); LogManager.getLogManager().reset(); Logger rootLogger = LogManager.getLogManager().getLogger(""); Handler handler = new ConsoleHandler(); handler.setFormatter(new SimpleFormatter()); rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); + rootLogger.setLevel(Level.FINE); for (Handler h : rootLogger.getHandlers()) { handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); + h.setLevel(Level.FINE); } } } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/CleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/CleartextHttp2Test.java similarity index 86% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/CleartextHttp2Test.java rename to netty-http-server/src/test/java/org/xbib/netty/http/hacks/CleartextHttp2Test.java index 3fa08ba..a4ffe5d 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/CleartextHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/CleartextHttp2Test.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test.simple; +package org.xbib.netty.http.hacks; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; @@ -29,19 +29,18 @@ import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; +import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import java.net.InetSocketAddress; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; -public class CleartextHttp2Test { +@Ignore +public class CleartextHttp2Test extends TestBase { private static final Logger clientLogger = Logger.getLogger("client"); private static final Logger serverLogger = Logger.getLogger("server"); @@ -52,36 +51,19 @@ public class CleartextHttp2Test { private static final Http2FrameLogger serverFrameLogger = new Http2FrameLogger(logLevel, "server"); private static final Http2FrameLogger clientFrameLogger = new Http2FrameLogger(logLevel, "client"); - static { - System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); - System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); - //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); - //System.setProperty("io.netty.leakDetection.level", "paranoid"); + private CompletableFuture settingsPrefaceFuture; - // expand Java logging to full level - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } - - private final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); - - private final CompletableFuture settingsPrefaceFuture = new CompletableFuture<>(); - - private final CompletableFuture completableFuture = new CompletableFuture<>(); + private CompletableFuture completableFuture; @Test public void testHttp2() throws Exception { + final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); + + settingsPrefaceFuture = new CompletableFuture<>(); + + completableFuture = new CompletableFuture<>(); + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/hacks/HttpPipeliningHandlerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/HttpPipeliningHandlerTest.java new file mode 100644 index 0000000..5f82f00 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/HttpPipeliningHandlerTest.java @@ -0,0 +1,207 @@ +package org.xbib.netty.http.hacks; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.QueryStringDecoder; +import org.junit.After; +import org.junit.Ignore; +import org.junit.Test; +import org.xbib.netty.http.server.handler.http.HttpPipelinedRequest; +import org.xbib.netty.http.server.handler.http.HttpPipelinedResponse; +import org.xbib.netty.http.server.handler.http.HttpPipeliningHandler; +import org.xbib.TestBase; + +import java.nio.channels.ClosedChannelException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNotNull; + +@Ignore +public class HttpPipeliningHandlerTest extends TestBase { + + private static final Logger logger = Logger.getLogger(HttpPipeliningHandlerTest.class.getName()); + + private static Map waitingRequests = new ConcurrentHashMap<>(); + + @After + public void closeResources() { + for (String url : waitingRequests.keySet()) { + finishRequest(url); + } + } + + @Test + public void testThatPipeliningWorksWithFastSerializedRequests() { + WorkEmulatorHandler handler = new WorkEmulatorHandler(); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(new HttpPipeliningHandler(10000), + handler); + for (int i = 0; i < 5; i++) { + embeddedChannel.writeInbound(createHttpRequest("/" + String.valueOf(i))); + } + for (String url : waitingRequests.keySet()) { + finishRequest(url); + } + handler.shutdownExecutorService(); + for (int i = 0; i < 5; i++) { + assertReadHttpMessageHasContent(embeddedChannel, String.valueOf(i)); + } + assertThat(embeddedChannel.isOpen(), is(true)); + } + + @Test + public void testThatPipeliningWorksWhenSlowRequestsInDifferentOrder() { + WorkEmulatorHandler handler = new WorkEmulatorHandler(); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(new HttpPipeliningHandler(10000), + handler); + for (int i = 0; i < 5; i++) { + embeddedChannel.writeInbound(createHttpRequest("/" + String.valueOf(i))); + } + List urls = new ArrayList<>(waitingRequests.keySet()); + Collections.shuffle(urls); + for (String url : urls) { + finishRequest(url); + } + handler.shutdownExecutorService(); + for (int i = 0; i < 5; i++) { + assertReadHttpMessageHasContent(embeddedChannel, String.valueOf(i)); + } + assertThat(embeddedChannel.isOpen(), is(true)); + } + + @Test + public void testThatPipeliningWorksWithChunkedRequests() { + WorkEmulatorHandler handler = new WorkEmulatorHandler(); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(new AggregateUrisAndHeadersHandler(), + new HttpPipeliningHandler(10000), handler); + DefaultHttpRequest httpRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/0"); + embeddedChannel.writeInbound(httpRequest); + embeddedChannel.writeInbound(LastHttpContent.EMPTY_LAST_CONTENT); + httpRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/1"); + embeddedChannel.writeInbound(httpRequest); + embeddedChannel.writeInbound(LastHttpContent.EMPTY_LAST_CONTENT); + finishRequest("1"); + finishRequest("0"); + handler.shutdownExecutorService(); + for (int i = 0; i < 2; i++) { + assertReadHttpMessageHasContent(embeddedChannel, String.valueOf(i)); + } + assertThat(embeddedChannel.isOpen(), is(true)); + } + + @Test(expected = ClosedChannelException.class) + public void testThatPipeliningClosesConnectionWithTooManyEvents() { + WorkEmulatorHandler handler = new WorkEmulatorHandler(); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(new HttpPipeliningHandler(2), + handler); + embeddedChannel.writeInbound(createHttpRequest("/0")); + embeddedChannel.writeInbound(createHttpRequest("/1")); + embeddedChannel.writeInbound(createHttpRequest("/2")); + embeddedChannel.writeInbound(createHttpRequest("/3")); + finishRequest("1"); + finishRequest("2"); + finishRequest("3"); + finishRequest("0"); + handler.shutdownExecutorService(); + embeddedChannel.writeInbound(createHttpRequest("/")); + } + + private void assertReadHttpMessageHasContent(EmbeddedChannel embeddedChannel, String expectedContent) { + FullHttpResponse response = (FullHttpResponse) embeddedChannel.outboundMessages().poll(); + assertNotNull("Expected response to exist, maybe you did not wait long enough?", response); + assertNotNull("Expected response to have content " + expectedContent, response.content()); + String data = new String(ByteBufUtil.getBytes(response.content()), StandardCharsets.UTF_8); + assertThat(data, is(expectedContent)); + } + + private void finishRequest(String url) { + waitingRequests.get(url).countDown(); + } + + private FullHttpRequest createHttpRequest(String uri) { + return new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); + } + + private static class AggregateUrisAndHeadersHandler extends SimpleChannelInboundHandler { + + static final Queue STRINGS = new LinkedTransferQueue<>(); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpRequest request) { + STRINGS.add(request.uri()); + } + } + + private class WorkEmulatorHandler extends SimpleChannelInboundHandler { + + private final ExecutorService executorService = Executors.newFixedThreadPool(5); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest pipelinedRequest) { + QueryStringDecoder decoder; + if (pipelinedRequest.getRequest() instanceof FullHttpRequest) { + FullHttpRequest fullHttpRequest = (FullHttpRequest) pipelinedRequest.getRequest(); + decoder = new QueryStringDecoder(fullHttpRequest.uri()); + } else { + decoder = new QueryStringDecoder(AggregateUrisAndHeadersHandler.STRINGS.poll()); + } + String uri = decoder.path().replace("/", ""); + ByteBuf content = Unpooled.copiedBuffer(uri, StandardCharsets.UTF_8); + DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, content); + httpResponse.headers().add(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); + CountDownLatch latch = new CountDownLatch(1); + waitingRequests.put(uri, latch); + executorService.submit(() -> { + try { + latch.await(2, TimeUnit.SECONDS); + HttpPipelinedResponse httpPipelinedResponse = new HttpPipelinedResponse(httpResponse, + ctx.channel().newPromise(), pipelinedRequest.getSequenceId()); + ctx.writeAndFlush(httpPipelinedResponse); + } catch (InterruptedException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + }); + } + + void shutdownExecutorService() { + if (!executorService.isShutdown()) { + executorService.shutdown(); + try { + executorService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + } + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/MultiplexCodecCleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/MultiplexCodecCleartextHttp2Test.java similarity index 90% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/MultiplexCodecCleartextHttp2Test.java rename to netty-http-server/src/test/java/org/xbib/netty/http/hacks/MultiplexCodecCleartextHttp2Test.java index 7a9b666..19eaee5 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/MultiplexCodecCleartextHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/MultiplexCodecCleartextHttp2Test.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test.simple; +package org.xbib.netty.http.hacks; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; @@ -40,17 +40,15 @@ import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.util.AsciiString; +import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import java.net.InetSocketAddress; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; /** * @@ -70,32 +68,12 @@ import java.util.logging.SimpleFormatter; * * */ -public class MultiplexCodecCleartextHttp2Test { +@Ignore +public class MultiplexCodecCleartextHttp2Test extends TestBase { private static final Logger clientLogger = Logger.getLogger("client"); private static final Logger serverLogger = Logger.getLogger("server"); - static { - System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); - System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); - System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); - System.setProperty("io.netty.leakDetection.level", "paranoid"); - - // expand Java logging to full level - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } - private final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8443); private final CompletableFuture settingsPrefaceFuture = new CompletableFuture<>(); @@ -195,10 +173,11 @@ public class MultiplexCodecCleartextHttp2Test { p.addLast("child-client-response-handler", new ClientResponseHandler()); } }).open().syncUninterruptibly().getNow(); - Http2Headers request = new DefaultHttp2Headers().method(HttpMethod.GET.asciiName()) + Http2Headers request = new DefaultHttp2Headers() + .method(HttpMethod.GET.asciiName()) .path("/foobar/0/0") .scheme("http") - .authority(inetSocketAddress.getHostName()); + .authority(inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort()); childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(request, true)); clientLogger.log(Level.INFO, "waiting max. 10 seconds"); responseFuture.get(10, TimeUnit.SECONDS); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedCleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/MultithreadedCleartextHttp2Test.java similarity index 84% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedCleartextHttp2Test.java rename to netty-http-server/src/test/java/org/xbib/netty/http/hacks/MultithreadedCleartextHttp2Test.java index e681b69..47a45fe 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedCleartextHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/MultithreadedCleartextHttp2Test.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test.multithread; +package org.xbib.netty.http.hacks; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; @@ -27,7 +27,9 @@ import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWritte import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import java.net.InetSocketAddress; import java.util.concurrent.CompletableFuture; @@ -35,56 +37,36 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; -public class MultithreadedCleartextHttp2Test { +@Ignore +public class MultithreadedCleartextHttp2Test extends TestBase { private static final Logger clientLogger = Logger.getLogger("client"); private static final Logger serverLogger = Logger.getLogger("server"); private static final Level level = Level.FINE; - static { - System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); - System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); - //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); - //System.setProperty("io.netty.leakDetection.level", "paranoid"); + private InetSocketAddress inetSocketAddress; - // expand Java logging to full level - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.OFF); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } + private CompletableFuture settingsPrefaceFuture; - private final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); + private CompletableFuture responseFuture; - private final CompletableFuture settingsPrefaceFuture = new CompletableFuture<>(); + private final int threads = 4; - private final CompletableFuture responseFuture = new CompletableFuture<>(); - - private final int threads = 10; - - private final int requestsPerThread = 100000; + private final int requestsPerThread = 500; private final AtomicInteger responseCounter = new AtomicInteger(); @Test public void testMultiThreadedHttp2() throws Exception { + inetSocketAddress = new InetSocketAddress("localhost", 8008); + settingsPrefaceFuture = new CompletableFuture<>(); + responseFuture = new CompletableFuture<>(); + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); @@ -180,7 +162,7 @@ public class MultithreadedCleartextHttp2Test { clientLogger.log(level, "waiting"); responseFuture.get(60, TimeUnit.SECONDS); if (responseFuture.isDone()) { - clientLogger.log(Level.INFO, "done"); + clientLogger.log(Level.INFO, "stop"); } } finally { @@ -229,7 +211,7 @@ public class MultithreadedCleartextHttp2Test { if (msg instanceof FullHttpRequest) { FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); - ctx.writeAndFlush(response); + ctx.write(response); } } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedMultiplexCodecCleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/MultithreadedMultiplexCodecCleartextHttp2Test.java similarity index 72% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedMultiplexCodecCleartextHttp2Test.java rename to netty-http-server/src/test/java/org/xbib/netty/http/hacks/MultithreadedMultiplexCodecCleartextHttp2Test.java index 268d40a..373be89 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedMultiplexCodecCleartextHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/MultithreadedMultiplexCodecCleartextHttp2Test.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test.multithread; +package org.xbib.netty.http.hacks; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; @@ -35,7 +35,9 @@ import io.netty.handler.codec.http2.Http2StreamChannel; import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; import io.netty.util.AsciiString; +import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import java.net.InetSocketAddress; import java.util.concurrent.CompletableFuture; @@ -43,61 +45,41 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; /** * * Multithreaded Http2MultiplexCodec demo for cleartext HTTP/2 between a server and a client. * */ -public class MultithreadedMultiplexCodecCleartextHttp2Test { +@Ignore +public class MultithreadedMultiplexCodecCleartextHttp2Test extends TestBase { private static final Logger clientLogger = Logger.getLogger("client"); private static final Logger serverLogger = Logger.getLogger("server"); private Level level = Level.FINE; - static { - System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); - System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); - //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); - //System.setProperty("io.netty.leakDetection.level", "paranoid"); + private InetSocketAddress inetSocketAddress; - // expand Java logging to full level - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.INFO); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } + private CompletableFuture settingsPrefaceFuture; - private final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); + private CompletableFuture responseFuture; - private final CompletableFuture settingsPrefaceFuture = new CompletableFuture<>(); + private final int threads = 4; - private final CompletableFuture responseFuture = new CompletableFuture<>(); - - private final int threads = 10; - - private final int requestsPerThread = 100000; + private final int requestsPerThread = 500; private final AtomicInteger responseCounter = new AtomicInteger(); @Test public void testMultithreadedMultiplexHttp2() throws Exception { + inetSocketAddress = new InetSocketAddress("localhost", 8008); + settingsPrefaceFuture = new CompletableFuture<>(); + responseFuture = new CompletableFuture<>(); + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); @@ -108,31 +90,31 @@ public class MultithreadedMultiplexCodecCleartextHttp2Test { .childHandler(new ChannelInitializer() { @Override protected void initChannel(Channel ch) { - ChannelPipeline p = ch.pipeline(); - Http2MultiplexCodec serverMultiplexCodec = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer() { - @Override - protected void initChannel(Channel channel) { - ChannelPipeline p = channel.pipeline(); - p.addLast("multiplex-server-frame-converter", new Http2StreamFrameToHttpObjectCodec(true)); - p.addLast("multiplex-server-chunk-aggregator", new HttpObjectAggregator(1048576)); - p.addLast("multiplex-server-request-handler", new ServerRequestHandler()); + ChannelPipeline p = ch.pipeline(); + Http2MultiplexCodec serverMultiplexCodec = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) { + ChannelPipeline p = channel.pipeline(); + p.addLast("multiplex-server-frame-converter", new Http2StreamFrameToHttpObjectCodec(true)); + p.addLast("multiplex-server-chunk-aggregator", new HttpObjectAggregator(1048576)); + p.addLast("multiplex-server-request-handler", new ServerRequestHandler()); + } + }) + .initialSettings(Http2Settings.defaultSettings()) + .build(); + HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec("server-codec", serverMultiplexCodec); + } else { + return null; } - }) - .initialSettings(Http2Settings.defaultSettings()) - .build(); - HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> { - if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { - return new Http2ServerUpgradeCodec("server-codec", serverMultiplexCodec); - } else { - return null; - } - }; - HttpServerCodec sourceCodec = new HttpServerCodec(); - HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory); - CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = - new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, serverMultiplexCodec); - p.addLast("server-upgrade", cleartextHttp2ServerUpgradeHandler); - p.addLast("server-messages", new ServerMessages()); + }; + HttpServerCodec sourceCodec = new HttpServerCodec(); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory); + CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = + new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, serverMultiplexCodec); + p.addLast("server-upgrade", cleartextHttp2ServerUpgradeHandler); + p.addLast("server-messages", new ServerMessages()); } }); Channel serverChannel = serverBootstrap.bind(inetSocketAddress).sync().channel(); @@ -193,8 +175,8 @@ public class MultithreadedMultiplexCodecCleartextHttp2Test { childChannel.write(new DefaultHttp2HeadersFrame(request, true)); //do not close child channel after write, a response is expected } + clientChannel.flush(); }); - clientChannel.flush(); } executorService.shutdown(); executorService.awaitTermination(60, TimeUnit.SECONDS); @@ -252,7 +234,7 @@ public class MultithreadedMultiplexCodecCleartextHttp2Test { protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) { DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); - ctx.writeAndFlush(response); + ctx.write(response); } } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/hacks/package-info.java b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/package-info.java new file mode 100644 index 0000000..61b2924 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/hacks/package-info.java @@ -0,0 +1,4 @@ +/** + * Hacking Netty for showing server functions. + */ +package org.xbib.netty.http.hacks; diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java index 6e2cf62..9381ef6 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java @@ -1,61 +1,165 @@ package org.xbib.netty.http.server.test; +import io.netty.handler.codec.http.HttpVersion; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -public class CleartextHttp1Test extends LoggingBase { +import static org.junit.Assert.assertEquals; - private static final Logger logger = Logger.getLogger(""); +public class CleartextHttp1Test extends TestBase { + + private static final Logger logger = Logger.getLogger(CleartextHttp1Test.class.getName()); @Test - public void testClearTextHttp1() throws Exception { - int loop = 1024; + public void testSimpleClearTextHttp1() throws Exception { + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Server server = Server.builder() + .bind(httpAddress).build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write(200, "text/plain", request.getRequest().content().retain())); + server.accept(); + Client client = Client.builder() + .build(); + AtomicInteger counter = new AtomicInteger(); + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + counter.incrementAndGet(); + }; + try { + Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base()) + .content("Hello world", "text/plain") + .build() + .setResponseListener(responseListener); + client.execute(request).get(); + } finally { + client.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "exepecting=1 counter=" + counter.get()); + assertEquals(1, counter.get()); + } + + @Test + public void testPooledClearTextHttp1() throws Exception { + int loop = 4096; HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); Server server = Server.builder() //.enableDebug() .bind(httpAddress).build(); server.getDefaultVirtualServer().addContext("/", (request, response) -> { - response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8)); + response.write(200, "text/plain", request.getRequest().content().retain()); }); server.accept(); - Client httpClient = Client.builder() + Client client = Client.builder() //.enableDebug() + .addPoolNode(httpAddress) + .setPoolNodeConnectionLimit(2) .build(); AtomicInteger counter = new AtomicInteger(); + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + counter.incrementAndGet(); + }; try { - // will not work for several thousands channels - "java.net.SocketException: Too many open files in system" for (int i = 0; i < loop; i++) { Request request = Request.get().setVersion("HTTP/1.1") .url(server.getServerConfig().getAddress().base()) - .addParameter("test", Integer.toString(i)) .content(Integer.toString(i), "text/plain") .build() - .setResponseListener(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - counter.incrementAndGet(); - }); - Transport transport = httpClient.execute(request); + .setResponseListener(responseListener); + Transport transport = client.newTransport(); + transport.execute(request); if (transport.isFailed()) { logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); break; } - // each execution needs to be synchronized transport.get(); } } finally { - httpClient.shutdownGracefully(); + client.shutdownGracefully(); server.shutdownGracefully(); } - logger.log(Level.INFO, "counter=" + counter.get()); + logger.log(Level.INFO, "expecting=" + loop + " counter=" + counter.get()); + assertEquals(loop, counter.get()); + } + + @Test + public void testMultithreadedPooledClearTextHttp1() throws Exception { + int threads = 4; + int loop = 4 * 1024; + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Server server = Server.builder() + //.enableDebug() + .bind(httpAddress).build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> { + response.write(200, "text/plain", request.getRequest().content().retain()); + }); + server.accept(); + Client client = Client.builder() + .addPoolNode(httpAddress) + .setPoolNodeConnectionLimit(threads) + .build(); + AtomicInteger counter = new AtomicInteger(); + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + + // " response=" + response + " payload=" + payload); + counter.incrementAndGet(); + }; + try { + ExecutorService executorService = Executors.newFixedThreadPool(threads); + for (int n = 0; n < threads; n++) { + final int t = n; + executorService.submit(() -> { + try { + for (int i = 0; i < loop; i++) { + String payload = Integer.toString(t) + "/" + Integer.toString(i); + Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base()) + .content(payload, "text/plain") + .build() + .setResponseListener(responseListener); + // note: a new transport is created per execution + Transport transport = client.newTransport(); + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, "transport failed: " + transport.getFailure().getMessage(), transport.getFailure()); + break; + } + transport.get(); + } + } catch (Exception e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + }); + } + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(30, TimeUnit.SECONDS); + logger.log(Level.INFO, "terminated = " + terminated + ", now waiting for transport to complete"); + } catch (Exception e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } finally { + client.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "expecting=" + (threads * loop) + " counter=" + counter.get()); + assertEquals(threads * loop, counter.get()); } } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java index 243e23a..36d82e8 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java @@ -1,73 +1,255 @@ package org.xbib.netty.http.server.test; -import io.netty.channel.WriteBufferWaterMark; -import io.netty.handler.codec.http.HttpVersion; import org.junit.Test; -import org.xbib.net.URL; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -public class CleartextHttp2Test extends LoggingBase { +import static org.junit.Assert.assertEquals; + +public class CleartextHttp2Test extends TestBase { private static final Logger logger = Logger.getLogger(CleartextHttp2Test.class.getName()); @Test - public void testCleartextHttp2() throws Exception { - int loop = 1; - // we assume slow server and reserve a large write buffer for the client, 32-64 bytes for each request, - // to avoid channel.isWritable() drop-outs - int low = 32 * loop; - int high = 64 * loop; - HttpAddress httpAddress = HttpAddress.of("localhost", 8008, HttpVersion.valueOf("HTTP/2.0"), false); + public void testSimpleCleartextHttp2() throws Exception { + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); Server server = Server.builder() - .enableDebug() .bind(httpAddress) .build(); - //server.logDiagnostics(Level.INFO); server.getDefaultVirtualServer().addContext("/", (request, response) -> - response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8))); + response.write(200, "text/plain", request.getRequest().content().retain())); server.accept(); - Client httpClient = Client.builder() - .enableDebug() - .setWriteBufferWaterMark(new WriteBufferWaterMark(low, high)) + Client client = Client.builder() .build(); AtomicInteger counter = new AtomicInteger(); + // a single instance of HTTP/2 response listener, always receives responses out-of-order + ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, "response listener: headers = " + fullHttpResponse.headers().entries() + + " response body = " + response); + counter.incrementAndGet(); + }; try { - URL serverURL = server.getServerConfig().getAddress().base(); - HttpVersion serverVersion = server.getServerConfig().getAddress().getVersion(); - // yes, HTTP/2 uses a single transport, and we can send many thousand requests per second asynchronously - Transport transport = httpClient.newTransport(serverURL, serverVersion); + String payload = Integer.toString(0) + "/" + Integer.toString(0); + Request request = Request.get().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base()) + .content(payload, "text/plain") + .build() + .setResponseListener(responseListener); + Transport transport = client.newTransport(httpAddress); + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + } + transport.get(); + } finally { + client.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "counter = " + counter.get()); + assertEquals(1, counter.get()); + } + + @Test + public void testPooledClearTextHttp2() throws Exception { + int loop = 4096; + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); + Server server = Server.builder() + .bind(httpAddress).build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write(200, "text/plain", request.getRequest().content().retain())); + //server.getDefaultVirtualServer().addContext("/", (request, response) -> + // response.write(request.getRequest().content().toString(StandardCharsets.UTF_8))); + server.accept(); + Client client = Client.builder() + //.enableDebug() + .addPoolNode(httpAddress) + .setPoolNodeConnectionLimit(2) + .build(); + AtomicInteger counter = new AtomicInteger(); + // a single instance of HTTP/2 response listener, always receives responses out-of-order + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "response listener: headers = " + fullHttpResponse.headers().entries() + + // " response body = " + response); + counter.incrementAndGet(); + }; + try { + // single transport, single thread + Transport transport = client.newTransport(); for (int i = 0; i < loop; i++) { + String payload = Integer.toString(0) + "/" + Integer.toString(i); Request request = Request.get().setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) - .content(Integer.toString(i), "text/plain") + .content(payload, "text/plain") .build() - .setResponseListener(fullHttpResponse -> { - String content = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, fullHttpResponse.toString() + " content=" + content); - counter.incrementAndGet(); - }); - // submit request + .setResponseListener(responseListener); transport.execute(request); if (transport.isFailed()) { logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); break; } } - // wait for transport to complete transport.get(); } finally { - httpClient.shutdownGracefully(); + client.shutdownGracefully(); server.shutdownGracefully(); } - logger.log(Level.INFO, "counter = " + counter.get()); + logger.log(Level.INFO, "expecting=" + loop + " counter=" + counter.get()); + assertEquals(loop, counter.get()); + } + + @Test + public void testMultithreadPooledClearTextHttp2() throws Exception { + int threads = 2; + int loop = 4 * 1024; + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); + Server server = Server.builder() + .bind(httpAddress) + .build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write(request.getRequest().content().toString(StandardCharsets.UTF_8)) + ); + server.accept(); + Client client = Client.builder() + .addPoolNode(httpAddress) + .setPoolNodeConnectionLimit(threads) + .build(); + AtomicInteger counter = new AtomicInteger(); + // a HTTP/2 listener always receives responses out-of-order + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "response listener: headers = " + fullHttpResponse.headers().entries() + + // " response body = " + response); + counter.incrementAndGet(); + }; + try { + // note: for HTTP/2 only, we can use a single shared transport + final Transport transport = client.newTransport(); + ExecutorService executorService = Executors.newFixedThreadPool(threads); + for (int n = 0; n < threads; n++) { + final int t = n; + executorService.submit(() -> { + try { + for (int i = 0; i < loop; i++) { + String payload = Integer.toString(t) + "/" + Integer.toString(i); + Request request = Request.get().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base()) + .content(payload, "text/plain") + .build() + .setResponseListener(responseListener); + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + } + } catch (IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + }); + } + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(30, TimeUnit.SECONDS); + logger.log(Level.INFO, "terminated = " + terminated + ", now waiting for transport to complete"); + transport.get(30, TimeUnit.SECONDS); + } finally { + client.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "expected=" + (threads * loop) + " counter=" + counter.get()); + assertEquals(threads * loop , counter.get()); + } + + @Test + public void testTwoPooledClearTextHttp2() throws Exception { + int threads = 2; + int loop = 4 * 1024; + + HttpAddress httpAddress1 = HttpAddress.http2("localhost", 8008); + AtomicInteger counter1 = new AtomicInteger(); + Server server1 = Server.builder() + .bind(httpAddress1).build(); + server1.getDefaultVirtualServer().addContext("/", (request, response) -> { + response.write(request.getRequest().content().toString(StandardCharsets.UTF_8)); + counter1.incrementAndGet(); + }); + server1.accept(); + + HttpAddress httpAddress2 = HttpAddress.http2("localhost", 8009); + AtomicInteger counter2 = new AtomicInteger(); + Server server2 = Server.builder() + .bind(httpAddress2).build(); + server2.getDefaultVirtualServer().addContext("/", (request, response) -> { + response.write(request.getRequest().content().toString(StandardCharsets.UTF_8)); + counter2.incrementAndGet(); + }); + server2.accept(); + + Client client = Client.builder() + .addPoolNode(httpAddress1) + .addPoolNode(httpAddress2) + .setPoolNodeConnectionLimit(threads) + .build(); + AtomicInteger counter = new AtomicInteger(); + // a single instance of HTTP/2 response listener, always receives responses out-of-order + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "response listener: headers = " + fullHttpResponse.headers().entries() + + // " response body = " + response); + counter.incrementAndGet(); + }; + try { + // note: for HTTP/2 only, we can use a single shared transport + final Transport transport = client.newTransport(); + ExecutorService executorService = Executors.newFixedThreadPool(threads); + for (int n = 0; n < threads; n++) { + final int t = n; + executorService.submit(() -> { + try { + for (int i = 0; i < loop; i++) { + String payload = Integer.toString(t) + "/" + Integer.toString(i); + Request request = Request.get().setVersion("HTTP/2.0") + .uri("/") + .content(payload, "text/plain") + .build() + .setResponseListener(responseListener); + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + } + } catch (IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + }); + } + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(30, TimeUnit.SECONDS); + logger.log(Level.INFO, "terminated = " + terminated + ", now waiting for transport to complete"); + transport.get(30, TimeUnit.SECONDS); + } finally { + client.shutdownGracefully(); + server1.shutdownGracefully(); + server2.shutdownGracefully(); + } + logger.log(Level.INFO, "counter1=" + counter1.get() + " counter2=" + counter2.get()); + logger.log(Level.INFO, "expecting=" + threads * loop + " counter=" + counter.get()); + assertEquals(threads * loop, counter.get()); } } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/MultithreadedCleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/MultithreadedCleartextHttp2Test.java deleted file mode 100644 index dab5ad2..0000000 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/MultithreadedCleartextHttp2Test.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.xbib.netty.http.server.test; - -import io.netty.channel.WriteBufferWaterMark; -import io.netty.handler.codec.http.HttpVersion; -import org.junit.Test; -import org.xbib.net.URL; -import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.transport.Transport; -import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.server.Server; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class MultithreadedCleartextHttp2Test extends LoggingBase { - - private static final Logger logger = Logger.getLogger(""); - - /** - * 2018-03-09 18:27:08.975 WARNUNG [io.netty.channel.ChannelInitializer] - * io.netty.channel.ChannelInitializer exceptionCaught Failed to initialize a channel. - * Closing: [id: 0x4af3e71a, L:/127.0.0.1:8008 - R:/127.0.0.1:59996] - * io.netty.channel.ChannelPipelineException: org.xbib.netty.http.server.handler.Http2ServerConnectionHandler - * is not a @Sharable handler, so can't be added or removed multiple times. - * @throws Exception if test fails - */ - @Test - public void testmultithreadedCleartextHttp2() throws Exception { - int loop = 1000; - int threads = 4; - // we assume slow server and reserve a large write buffer for the client, 32-64 bytes for each request, - // to avoid channel.isWritable() drop-outs - int low = 32 * loop; - int high = 64 * loop; - HttpAddress httpAddress = HttpAddress.of("localhost", 8008, HttpVersion.valueOf("HTTP/2.0"), false); - Server server = Server.builder() - .bind(httpAddress) - .build(); - server.getDefaultVirtualServer().addContext("/", (request, response) -> - response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8))); - server.accept(); - Client httpClient = Client.builder() - .setWriteBufferWaterMark(new WriteBufferWaterMark(low, high)) - .build(); - AtomicInteger counter = new AtomicInteger(); - try { - URL serverURL = server.getServerConfig().getAddress().base(); - HttpVersion serverVersion = server.getServerConfig().getAddress().getVersion(); - ExecutorService executorService = Executors.newFixedThreadPool(threads); - for (int n = 0; n < threads; n++) { - executorService.submit(() -> { - try { - Transport transport = httpClient.newTransport(serverURL, serverVersion); - for (int i = 0; i < loop; i++) { - Request request = Request.get().setVersion("HTTP/2.0") - .content(Integer.toString(i), "text/plain") - .build() - .setResponseListener(fullHttpResponse -> { - String content = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - //logger.log(Level.INFO, fullHttpResponse.toString() + " content=" + content); - counter.incrementAndGet(); - }); - // submit request - transport.execute(request); - if (transport.isFailed()) { - logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); - break; - } - } - // wait for transport to complete - transport.get(); - } catch (IOException e) { - logger.log(Level.WARNING, e.getMessage(), e); - } - }); - } - executorService.shutdown(); - executorService.awaitTermination(60, TimeUnit.SECONDS); - } finally { - httpClient.shutdownGracefully(); - server.shutdownGracefully(); - } - logger.log(Level.INFO, "counter = " + counter.get()); - } -} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PooledCleartextHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PooledCleartextHttp1Test.java deleted file mode 100644 index 4fe6ada..0000000 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PooledCleartextHttp1Test.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.xbib.netty.http.server.test; - -import org.junit.Test; -import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.transport.Transport; -import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.server.Server; - -import java.nio.charset.StandardCharsets; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class PooledCleartextHttp1Test extends LoggingBase { - - private static final Logger logger = Logger.getLogger(""); - - @Test - public void testClearTextHttp1() throws Exception { - int loop = 10000; - HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - Server server = Server.builder() - //.enableDebug() - .bind(httpAddress).build(); - server.getDefaultVirtualServer().addContext("/", (request, response) -> { - response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8)); - }); - server.accept(); - org.xbib.netty.http.common.HttpAddress poolNode = org.xbib.netty.http.common.HttpAddress.http1("localhost", 8008); - Client httpClient = Client.builder() - //.enableDebug() - .addPoolNode(poolNode) - .setPoolNodeConnectionLimit(8) - .build(); - AtomicInteger counter = new AtomicInteger(); - try { - for (int i = 0; i < loop; i++) { - Request request = Request.get().setVersion("HTTP/1.1") - .url(server.getServerConfig().getAddress().base()) - .addParameter("test", Integer.toString(i)) - .content(Integer.toString(i), "text/plain") - .build() - .setResponseListener(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - counter.incrementAndGet(); - }); - Transport transport = httpClient.pooledExecute(request); - if (transport.isFailed()) { - logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); - break; - } - // each execution needs to be synchronized - transport.get(); - } - } finally { - httpClient.shutdownGracefully(); - server.shutdownGracefully(); - } - logger.log(Level.INFO, "counter=" + counter.get()); - } -} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java index 231afd9..cd3400b 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java @@ -1,53 +1,181 @@ package org.xbib.netty.http.server.test; +import io.netty.handler.codec.http.HttpVersion; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Ignore; import org.junit.Test; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.listener.ResponseListener; +import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.Security; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -public class SecureHttp1Test extends LoggingBase { +import static org.junit.Assert.assertEquals; - private static final Logger logger = Logger.getLogger(""); +public class SecureHttp1Test extends TestBase { - @Test - public void testSecureHttp1() throws Exception { + private static final Logger logger = Logger.getLogger(SecureHttp1Test.class.getName()); + + static { if (Security.getProvider("BC") == null) { Security.addProvider(new BouncyCastleProvider()); } - Server server = Server.builder().bind(HttpAddress.secureHttp1("localhost", 8143)) + } + + @Test + public void testSimpleSecureHttp1() throws Exception { + Server server = Server.builder() + .setJdkSslProvider() .setSelfCert() + .bind(HttpAddress.secureHttp1("localhost", 8143)) .build(); - Client httpClient = Client.builder() + Client client = Client.builder() + .setJdkSslProvider() .trustInsecure() .build(); + AtomicInteger counter = new AtomicInteger(); + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + counter.getAndIncrement(); + }; try { server.getDefaultVirtualServer().addContext("/", (request, response) -> - response.write("Hello World")); + response.write(200, "text/plain", request.getRequest().content().retain())); server.accept(); - httpClient.execute(Request.get().setVersion("HTTP/1.1") + Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base()) .build() - .setResponseListener(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - })).get(); - httpClient.execute(Request.get().setVersion("HTTP/1.1") - .url(server.getServerConfig().getAddress().base()) - .build() - .setResponseListener(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - })).get(); + .setResponseListener(responseListener); + client.execute(request).get(); } finally { - httpClient.shutdownGracefully(); + client.shutdownGracefully(); server.shutdownGracefully(); } + logger.log(Level.INFO, "counter=" + counter.get()); + assertEquals(1, counter.get()); + } + + @Test + public void testPooledSecureHttp1() throws Exception { + int loop = 4096; + HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); + Server server = Server.builder() + .setJdkSslProvider() + .setSelfCert() + .bind(httpAddress).build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write(200, "text/plain", request.getRequest().content().retain())); + server.accept(); + Client client = Client.builder() + .setJdkSslProvider() + .trustInsecure() + .addPoolNode(httpAddress) + .setPoolNodeConnectionLimit(2) + .build(); + AtomicInteger counter = new AtomicInteger(); + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + counter.incrementAndGet(); + }; + try { + for (int i = 0; i < loop; i++) { + Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base()) + .content(Integer.toString(i), "text/plain") + .build() + .setResponseListener(responseListener); + Transport transport = client.newTransport(); + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + transport.get(); + } + } finally { + client.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "expecting=" + loop + " counter=" + counter.get()); + assertEquals(loop, counter.get()); + } + + @Test + public void testMultithreadPooledSecureHttp1() throws Exception { + int threads = 4; + int loop = 4 * 1024; + HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); + Server server = Server.builder() + .setJdkSslProvider() + .setSelfCert() + .bind(httpAddress) + .build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write(200, "text/plain", request.getRequest().content().retain()) + ); + server.accept(); + Client client = Client.builder() + .setJdkSslProvider() + .trustInsecure() + .addPoolNode(httpAddress) + .setPoolNodeConnectionLimit(threads) + .build(); + AtomicInteger counter = new AtomicInteger(); + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "response listener: headers = " + fullHttpResponse.headers().entries() + + // " response body = " + response); + counter.incrementAndGet(); + }; + try { + ExecutorService executorService = Executors.newFixedThreadPool(threads); + for (int n = 0; n < threads; n++) { + final int t = n; + executorService.submit(() -> { + try { + for (int i = 0; i < loop; i++) { + String payload = Integer.toString(t) + "/" + Integer.toString(i); + Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base()) + .content(payload, "text/plain") + .build() + .setResponseListener(responseListener); + // note: a new transport is created per execution + final Transport transport = client.newTransport(); + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + transport.get(); + } + } catch (IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + }); + } + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(30, TimeUnit.SECONDS); + logger.log(Level.INFO, "terminated = " + terminated); + } finally { + client.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "expecting=" + (threads * loop) + " counter=" + counter.get()); + assertEquals(threads * loop , counter.get()); } } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java index 22c381a..60a26a3 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java @@ -1,65 +1,117 @@ package org.xbib.netty.http.server.test; -import io.netty.channel.WriteBufferWaterMark; -import io.netty.handler.codec.http.HttpVersion; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Ignore; import org.junit.Test; -import org.xbib.net.URL; +import org.xbib.TestBase; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.Security; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -public class SecureHttp2Test extends LoggingBase { +import static org.junit.Assert.assertEquals; - private static final Logger logger = Logger.getLogger(""); +public class SecureHttp2Test extends TestBase { - @Test - public void testSecureHttp2() throws Exception { - // for self-signed certificate, we need Bouncycastle + private static final Logger logger = Logger.getLogger(SecureHttp2Test.class.getName()); + + static { if (Security.getProvider("BC") == null) { Security.addProvider(new BouncyCastleProvider()); } - int threads = 4; - int loop = 100000; - // we assume slow server and reserve a large write buffer for the client, 32-64 bytes for each request, - // to avoid channel.isWritable() drop-outs - int low = 32 * loop; - int high = 64 * loop; + } - Server server = Server.builder().bind(HttpAddress.http2("localhost", 8143)) + @Test + public void testSimpleSecureHttp2() throws Exception { + HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); + Server server = Server.builder() + .setJdkSslProvider() .setSelfCert() + .bind(httpAddress) .build(); - //server.logDiagnostics(Level.INFO); server.getDefaultVirtualServer().addContext("/", (request, response) -> - response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8))); + response.write(200, "text/plain", request.getRequest().content().retain())); server.accept(); - Client httpClient = Client.builder() + Client client = Client.builder() + .setJdkSslProvider() .trustInsecure() - .setWriteBufferWaterMark(new WriteBufferWaterMark(low, high)) .build(); AtomicInteger counter = new AtomicInteger(); + // a single instance of HTTP/2 response listener, always receives responses out-of-order + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, "response listener: headers = " + fullHttpResponse.headers().entries() + + " response body = " + response); + counter.incrementAndGet(); + }; try { - URL serverURL = server.getServerConfig().getAddress().base(); - HttpVersion serverVersion = server.getServerConfig().getAddress().getVersion(); - Transport transport = httpClient.newTransport(serverURL, serverVersion); - for (int i = 0; i < loop; i++) { - Request request = Request.get().setVersion("HTTP/2.0") - .url(server.getServerConfig().getAddress().base()) - .content(Integer.toString(i), "text/plain") + Transport transport = client.newTransport(httpAddress); + String payload = Integer.toString(0) + "/" + Integer.toString(0); + Request request = Request.get() + .setVersion("HTTP/2.0") + .uri("/") + //.url(server.getServerConfig().getAddress().base()) + .content(payload, "text/plain") .build() - .setResponseListener(fullHttpResponse -> { - String content = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - //logger.log(Level.INFO, fullHttpResponse.toString() + " content=" + content); - counter.incrementAndGet(); - }); + .setResponseListener(responseListener); + transport.execute(request); + transport.get(); + } finally { + client.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "counter = " + counter.get()); + assertEquals(1, counter.get()); + } + + @Test + public void testPooledSecureHttp2() throws Exception { + int loop = 4096; + HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); + Server server = Server.builder() + .setJdkSslProvider() + .setSelfCert() + .bind(httpAddress) + .build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write(200, "text/plain", request.getRequest().content().retain())); + server.accept(); + Client client = Client.builder() + .setJdkSslProvider() + .trustInsecure() + .addPoolNode(httpAddress) + .setPoolNodeConnectionLimit(2) + .build(); + AtomicInteger counter = new AtomicInteger(); + // a single instance of HTTP/2 response listener, always receives responses out-of-order + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "response listener: headers = " + fullHttpResponse.headers().entries() + + // " response body = " + response); + counter.incrementAndGet(); + }; + try { + // single transport, single thread + Transport transport = client.newTransport(); + for (int i = 0; i < loop; i++) { + String payload = Integer.toString(0) + "/" + Integer.toString(i); + Request request = Request.get().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base()) + .content(payload, "text/plain") + .build() + .setResponseListener(responseListener); transport.execute(request); if (transport.isFailed()) { logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); @@ -68,9 +120,76 @@ public class SecureHttp2Test extends LoggingBase { } transport.get(); } finally { - httpClient.shutdownGracefully(); + client.shutdownGracefully(); server.shutdownGracefully(); } - logger.log(Level.INFO, "counter = " + counter.get()); + logger.log(Level.INFO, "counter=" + counter.get()); + assertEquals(loop, counter.get()); + } + + @Test + public void testMultithreadPooledSecureHttp2() throws Exception { + int threads = 4; + int loop = 4 * 1024; + HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); + Server server = Server.builder() + .setJdkSslProvider() + .setSelfCert() + .bind(httpAddress) + .build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write(200, "text/plain", request.getRequest().content().retain()) + ); + server.accept(); + Client client = Client.builder() + .setJdkSslProvider() + .trustInsecure() + .addPoolNode(httpAddress) + .setPoolNodeConnectionLimit(threads) + .build(); + AtomicInteger counter = new AtomicInteger(); + // a HTTP/2 listener always receives responses out-of-order + final ResponseListener responseListener = fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "response listener: headers = " + fullHttpResponse.headers().entries() + + // " response body = " + response); + counter.incrementAndGet(); + }; + try { + // note: for HTTP/2 only, we can use a single shared transport + final Transport transport = client.newTransport(); + ExecutorService executorService = Executors.newFixedThreadPool(threads); + for (int n = 0; n < threads; n++) { + final int t = n; + executorService.submit(() -> { + try { + for (int i = 0; i < loop; i++) { + String payload = Integer.toString(t) + "/" + Integer.toString(i); + Request request = Request.get().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base()) + .content(payload, "text/plain") + .build() + .setResponseListener(responseListener); + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + } + } catch (IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + }); + } + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(30, TimeUnit.SECONDS); + logger.log(Level.INFO, "terminated = " + terminated + ", now waiting for transport to complete"); + transport.get(30, TimeUnit.SECONDS); + } finally { + client.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "expected=" + (threads * loop) + " counter=" + counter.get()); + assertEquals(threads * loop , counter.get()); } } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SelfSignedCertificateTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SelfSignedCertificateTest.java index 7c30472..ecea3b2 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SelfSignedCertificateTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SelfSignedCertificateTest.java @@ -7,8 +7,6 @@ import org.xbib.netty.http.server.security.tls.SelfSignedCertificate; import java.security.Security; import java.util.logging.Logger; -/** - */ public class SelfSignedCertificateTest { @Test diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java index 0610458..cc8db3c 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java @@ -1,8 +1,10 @@ package org.xbib.netty.http.server.test; +import org.junit.Ignore; import org.junit.Test; import org.xbib.netty.http.server.Server; +@Ignore public class ServerTest { @Test diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java new file mode 100644 index 0000000..bbe63be --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java @@ -0,0 +1,41 @@ +package org.xbib.netty.http.server.test; + +import io.netty.buffer.UnpooledByteBufAllocator; +import org.junit.After; +import org.junit.Test; +import org.xbib.TestBase; +import org.xbib.netty.http.server.Server; + +import java.io.IOException; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ThreadLeakTest extends TestBase { + + private static final Logger logger = Logger.getLogger(ThreadLeakTest.class.getName()); + + @Test + public void testForLeaks() throws IOException { + Server server = Server.builder() + .setByteBufAllocator(UnpooledByteBufAllocator.DEFAULT) + .build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write("Hello World")); + try { + server.accept(); + } finally { + server.shutdownGracefully(); + } + } + + @After + public void checkThreads() throws Exception { + Thread.sleep(1000L); + System.gc(); + Thread.sleep(3000L); + Set threadSet = Thread.getAllStackTraces().keySet(); + logger.log(Level.INFO, "threads = " + threadSet.size() ); + threadSet.forEach( thread -> logger.log(Level.INFO, thread.toString())); + } +}