more work on HTTP transports with pooling and multithreading, update to Netty 4.1.24, update to Gradle 4.7

This commit is contained in:
Jörg Prante 2018-05-04 11:32:53 +02:00
parent f2c483fcfa
commit 2339735966
101 changed files with 2980 additions and 3177 deletions

View file

@ -38,7 +38,6 @@ subprojects {
dependencies { dependencies {
alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}" 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')}" wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}"
} }
@ -61,7 +60,7 @@ subprojects {
jvmArgs "-javaagent:" + configurations.alpnagent.asPath jvmArgs "-javaagent:" + configurations.alpnagent.asPath
} }
testLogging { testLogging {
showStandardStreams = false showStandardStreams = true
exceptionFormat = 'full' exceptionFormat = 'full'
} }
} }
@ -82,7 +81,7 @@ subprojects {
'source-highlighter': 'coderay' 'source-highlighter': 'coderay'
} }
javadoc { /*javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List) options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = "org.xbib.asciidoclet.Asciidoclet" options.doclet = "org.xbib.asciidoclet.Asciidoclet"
options.overview = "src/docs/asciidoclet/overview.adoc" options.overview = "src/docs/asciidoclet/overview.adoc"
@ -92,7 +91,7 @@ subprojects {
configure(options) { configure(options) {
noTimestamp = true noTimestamp = true
} }
} }*/
task javadocJar(type: Jar, dependsOn: classes) { task javadocJar(type: Jar, dependsOn: classes) {
from javadoc from javadoc
@ -116,7 +115,6 @@ subprojects {
} }
} }
ext { ext {
user = 'jprante' user = 'jprante'
name = 'netty-http-client' name = 'netty-http-client'
@ -126,7 +124,6 @@ subprojects {
scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
} }
task xbibUpload(type: Upload) { task xbibUpload(type: Upload) {
group = 'publish' group = 'publish'
configuration = configurations.archives configuration = configurations.archives
@ -195,18 +192,20 @@ subprojects {
} }
} }
spotbugs {
} toolVersion = '3.1.3'
sourceSets = [sourceSets.main]
spotbugs {
effort = "max"
reportLevel = "low"
//includeFilter = file("findbugs-exclude.xml")
}
tasks.withType(com.github.spotbugs.SpotBugsTask) {
ignoreFailures = true ignoreFailures = true
reports { 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 xml.enabled = false
html.enabled = true html.enabled = true
} }

View file

@ -1,8 +1,8 @@
group = org.xbib group = org.xbib
name = netty-http-client 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 tcnative.version = 2.0.7.Final
conscrypt.version = 1.0.1 conscrypt.version = 1.0.1
bouncycastle.version = 1.57 bouncycastle.version = 1.57
@ -10,7 +10,6 @@ xbib-net-url.version = 1.1.0
alpnagent.version = 2.0.7 alpnagent.version = 2.0.7
junit.version = 4.12 junit.version = 4.12
jackson.version = 2.8.11.1 jackson.version = 2.8.11.1
asciidoclet.version = 1.6.0.0
wagon.version = 3.0.0 wagon.version = 3.0.0

Binary file not shown.

View file

@ -1,6 +1,6 @@
#Fri Mar 02 19:15:04 CET 2018 #Sat Apr 28 00:39:47 CEST 2018
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View file

@ -21,15 +21,11 @@ import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandler;
import org.xbib.net.URL; import org.xbib.netty.http.client.handler.http.HttpChannelInitializer;
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.http2.Http2ChannelInitializer; 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.pool.BoundedChannelPool;
import org.xbib.netty.http.client.transport.Http2Transport; 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.client.transport.Transport;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.NetworkUtils; import org.xbib.netty.http.common.NetworkUtils;
@ -73,12 +69,6 @@ public final class Client {
if (System.getProperty("io.netty.noKeySetOptimization") == null) { if (System.getProperty("io.netty.noKeySetOptimization") == null) {
System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); 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; private final ClientConfig clientConfig;
@ -91,16 +81,8 @@ public final class Client {
private final Bootstrap bootstrap; private final Bootstrap bootstrap;
private final HttpResponseHandler httpResponseHandler;
private final Http2SettingsHandler http2SettingsHandler;
private final Http2ResponseHandler http2ResponseHandler;
private final List<Transport> transports; private final List<Transport> transports;
private TransportListener transportListener;
private BoundedChannelPool<HttpAddress> pool; private BoundedChannelPool<HttpAddress> pool;
public Client() { public Client() {
@ -126,7 +108,7 @@ public final class Client {
this.bootstrap = new Bootstrap() this.bootstrap = new Bootstrap()
.group(this.eventLoopGroup) .group(this.eventLoopGroup)
.channel(this.socketChannelClass) .channel(this.socketChannelClass)
//.option(ChannelOption.ALLOCATOR, byteBufAllocator) .option(ChannelOption.ALLOCATOR, byteBufAllocator)
.option(ChannelOption.TCP_NODELAY, clientConfig.isTcpNodelay()) .option(ChannelOption.TCP_NODELAY, clientConfig.isTcpNodelay())
.option(ChannelOption.SO_KEEPALIVE, clientConfig.isKeepAlive()) .option(ChannelOption.SO_KEEPALIVE, clientConfig.isKeepAlive())
.option(ChannelOption.SO_REUSEADDR, clientConfig.isReuseAddr()) .option(ChannelOption.SO_REUSEADDR, clientConfig.isReuseAddr())
@ -134,9 +116,6 @@ public final class Client {
.option(ChannelOption.SO_RCVBUF, clientConfig.getTcpReceiveBufferSize()) .option(ChannelOption.SO_RCVBUF, clientConfig.getTcpReceiveBufferSize())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientConfig.getConnectTimeoutMillis()) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientConfig.getConnectTimeoutMillis())
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, clientConfig.getWriteBufferWaterMark()); .option(ChannelOption.WRITE_BUFFER_WATER_MARK, clientConfig.getWriteBufferWaterMark());
this.httpResponseHandler = new HttpResponseHandler();
this.http2SettingsHandler = new Http2SettingsHandler();
this.http2ResponseHandler = new Http2ResponseHandler();
this.transports = new CopyOnWriteArrayList<>(); this.transports = new CopyOnWriteArrayList<>();
if (!clientConfig.getPoolNodes().isEmpty()) { if (!clientConfig.getPoolNodes().isEmpty()) {
List<HttpAddress> nodes = clientConfig.getPoolNodes(); List<HttpAddress> nodes = clientConfig.getPoolNodes();
@ -151,7 +130,8 @@ public final class Client {
} }
ClientChannelPoolHandler clientChannelPoolHandler = new ClientChannelPoolHandler(); ClientChannelPoolHandler clientChannelPoolHandler = new ClientChannelPoolHandler();
this.pool = new BoundedChannelPool<>(semaphore, clientConfig.getPoolVersion(), this.pool = new BoundedChannelPool<>(semaphore, clientConfig.getPoolVersion(),
clientConfig.isPoolSecure(), nodes, bootstrap, clientChannelPoolHandler, retries); nodes, bootstrap, clientChannelPoolHandler, retries,
BoundedChannelPool.PoolKeySelectorType.ROUNDROBIN);
Integer nodeConnectionLimit = clientConfig.getPoolNodeConnectionLimit(); Integer nodeConnectionLimit = clientConfig.getPoolNodeConnectionLimit();
if (nodeConnectionLimit == null || nodeConnectionLimit == 0) { if (nodeConnectionLimit == null || nodeConnectionLimit == 0) {
nodeConnectionLimit = nodes.size(); nodeConnectionLimit = nodes.size();
@ -176,22 +156,10 @@ public final class Client {
return byteBufAllocator; return byteBufAllocator;
} }
public EventLoopGroup getEventLoopGroup() {
return eventLoopGroup;
}
public void setTransportListener(TransportListener transportListener) {
this.transportListener = transportListener;
}
public boolean hasPooledConnections() { public boolean hasPooledConnections() {
return pool != null && !clientConfig.getPoolNodes().isEmpty(); return pool != null && !clientConfig.getPoolNodes().isEmpty();
} }
public BoundedChannelPool<HttpAddress> getPool() {
return pool;
}
public void logDiagnostics(Level level) { public void logDiagnostics(Level level) {
logger.log(level, () -> "OpenSSL available: " + OpenSsl.isAvailable() + logger.log(level, () -> "OpenSSL available: " + OpenSsl.isAvailable() +
" OpenSSL ALPN support: " + OpenSsl.isAlpnSupported() + " OpenSSL ALPN support: " + OpenSsl.isAlpnSupported() +
@ -206,29 +174,22 @@ public final class Client {
return newTransport(null); return newTransport(null);
} }
public Transport newTransport(URL url, HttpVersion httpVersion) {
return newTransport(HttpAddress.of(url, httpVersion));
}
public Transport newTransport(HttpAddress httpAddress) { public Transport newTransport(HttpAddress httpAddress) {
Transport transport = null; Transport transport;
if (httpAddress != null) { if (httpAddress != null) {
if (httpAddress.getVersion().majorVersion() == 1) { if (httpAddress.getVersion().majorVersion() == 1) {
transport = new Http1Transport(this, httpAddress); transport = new HttpTransport(this, httpAddress);
} else { } else {
transport = new Http2Transport(this, httpAddress); transport = new Http2Transport(this, httpAddress);
} }
} else if (hasPooledConnections()) { } else if (hasPooledConnections()) {
if (pool.getVersion().majorVersion() == 1) { if (pool.getVersion().majorVersion() == 1) {
transport = new Http1Transport(this, null); transport = new HttpTransport(this, null);
} else { } else {
transport = new Http2Transport(this, null); transport = new Http2Transport(this, null);
} }
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException("no address given to connect to");
}
if (transportListener != null) {
transportListener.onOpen(transport);
} }
transports.add(transport); transports.add(transport);
return transport; return transport;
@ -238,14 +199,13 @@ public final class Client {
Channel channel; Channel channel;
if (httpAddress != null) { if (httpAddress != null) {
HttpVersion httpVersion = httpAddress.getVersion(); HttpVersion httpVersion = httpAddress.getVersion();
ChannelInitializer<SocketChannel> initializer; ChannelInitializer<Channel> initializer;
SslHandler sslHandler = newSslHandler(clientConfig, byteBufAllocator, httpAddress); SslHandler sslHandler = newSslHandler(clientConfig, byteBufAllocator, httpAddress);
if (httpVersion.majorVersion() == 1) { if (httpVersion.majorVersion() == 1) {
initializer = new HttpChannelInitializer(clientConfig, httpAddress, initializer = new HttpChannelInitializer(clientConfig, httpAddress, sslHandler,
sslHandler, httpResponseHandler); new Http2ChannelInitializer(clientConfig, httpAddress, sslHandler));
} else { } else {
initializer = new Http2ChannelInitializer(clientConfig, httpAddress, initializer = new Http2ChannelInitializer(clientConfig, httpAddress, sslHandler);
sslHandler, http2SettingsHandler, http2ResponseHandler);
} }
try { try {
channel = bootstrap.handler(initializer) channel = bootstrap.handler(initializer)
@ -267,23 +227,20 @@ public final class Client {
return channel; return channel;
} }
public Channel newChannel() throws IOException { public void releaseChannel(Channel channel, boolean close) throws IOException{
return newChannel(null); if (channel == null) {
return;
} }
public void releaseChannel(Channel channel) throws IOException{
if (channel != null) {
if (hasPooledConnections()) { if (hasPooledConnections()) {
try { try {
pool.release(channel); pool.release(channel, close);
} catch (Exception e) { } catch (Exception e) {
throw new IOException(e); throw new IOException(e);
} }
} else { } else if (close) {
channel.close(); channel.close();
} }
} }
}
public Transport execute(Request request) throws IOException { public Transport execute(Request request) throws IOException {
Transport transport = newTransport(HttpAddress.of(request.url(), request.httpVersion())); Transport transport = newTransport(HttpAddress.of(request.url(), request.httpVersion()));
@ -293,19 +250,15 @@ public final class Client {
public <T> CompletableFuture<T> execute(Request request, public <T> CompletableFuture<T> execute(Request request,
Function<FullHttpResponse, T> supplier) throws IOException { Function<FullHttpResponse, T> supplier) throws IOException {
return newTransport(HttpAddress.of(request.url(), request.httpVersion())).execute(request, supplier); 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;
} }
/** /**
* For following redirects, construct a new transport. * For following redirects, construct a new transport.
* @param transport the previous transport * @param transport the previous transport
* @param request the new request for continuing the request. * @param request the new request for continuing the request.
* @throws IOException if continuation fails
*/ */
public void continuation(Transport transport, Request request) throws IOException { public void continuation(Transport transport, Request request) throws IOException {
Transport nextTransport = newTransport(HttpAddress.of(request.url(), request.httpVersion())); Transport nextTransport = newTransport(HttpAddress.of(request.url(), request.httpVersion()));
@ -328,14 +281,7 @@ public final class Client {
close(transport); close(transport);
} }
public Transport prepareRequest(Request request) {
return newTransport(HttpAddress.of(request.url(), request.httpVersion()));
}
public void close(Transport transport) throws IOException { public void close(Transport transport) throws IOException {
if (transportListener != null) {
transportListener.onClose(transport);
}
transport.close(); transport.close();
transports.remove(transport); transports.remove(transport);
} }
@ -344,12 +290,13 @@ public final class Client {
for (Transport transport : transports) { for (Transport transport : transports) {
close(transport); close(transport);
} }
} // how to wait for all responses for the pool?
public void shutdownGracefully() throws IOException {
if (hasPooledConnections()) { if (hasPooledConnections()) {
pool.close(); pool.close();
} }
}
public void shutdownGracefully() throws IOException {
close(); close();
shutdown(); shutdown();
} }
@ -439,13 +386,6 @@ public final class Client {
ApplicationProtocolNames.HTTP_2); ApplicationProtocolNames.HTTP_2);
} }
public interface TransportListener {
void onOpen(Transport transport);
void onClose(Transport transport);
}
static class HttpClientThreadFactory implements ThreadFactory { static class HttpClientThreadFactory implements ThreadFactory {
private int number = 0; private int number = 0;
@ -474,17 +414,12 @@ public final class Client {
HttpVersion httpVersion = httpAddress.getVersion(); HttpVersion httpVersion = httpAddress.getVersion();
SslHandler sslHandler = newSslHandler(clientConfig, byteBufAllocator, httpAddress); SslHandler sslHandler = newSslHandler(clientConfig, byteBufAllocator, httpAddress);
if (httpVersion.majorVersion() == 1) { if (httpVersion.majorVersion() == 1) {
HttpChannelInitializer initializer = new HttpChannelInitializer(clientConfig, httpAddress, HttpChannelInitializer initializer = new HttpChannelInitializer(clientConfig, httpAddress, sslHandler,
sslHandler, httpResponseHandler); new Http2ChannelInitializer(clientConfig, httpAddress, sslHandler));
if (channel instanceof SocketChannel) { initializer.initChannel(channel);
initializer.initChannel((SocketChannel) channel);
}
} else { } else {
Http2ChannelInitializer initializer = new Http2ChannelInitializer(clientConfig, httpAddress, Http2ChannelInitializer initializer = new Http2ChannelInitializer(clientConfig, httpAddress, sslHandler);
sslHandler, http2SettingsHandler, http2ResponseHandler); initializer.initChannel(channel);
if (channel instanceof SocketChannel) {
initializer.initChannel((SocketChannel) channel);
}
} }
} }
} }

View file

@ -222,6 +222,11 @@ public class ClientBuilder {
return this; return this;
} }
public ClientBuilder enableNegotiation(boolean enableNegotiation) {
clientConfig.setEnableNegotiation(enableNegotiation);
return this;
}
public Client build() { public Client build() {
return new Client(clientConfig, byteBufAllocator, eventLoopGroup, socketChannelClass); return new Client(clientConfig, byteBufAllocator, eventLoopGroup, socketChannelClass);
} }

View file

@ -163,6 +163,8 @@ public class ClientConfig {
* Default for backoff. * Default for backoff.
*/ */
BackOff BACK_OFF = BackOff.ZERO_BACKOFF; BackOff BACK_OFF = BackOff.ZERO_BACKOFF;
Boolean ENABLE_NEGOTIATION = false;
} }
private static TrustManagerFactory TRUST_MANAGER_FACTORY; private static TrustManagerFactory TRUST_MANAGER_FACTORY;
@ -249,6 +251,8 @@ public class ClientConfig {
private BackOff backOff = Defaults.BACK_OFF; private BackOff backOff = Defaults.BACK_OFF;
private boolean enableNegotiation = Defaults.ENABLE_NEGOTIATION;
public ClientConfig setDebug(boolean debug) { public ClientConfig setDebug(boolean debug) {
this.debug = debug; this.debug = debug;
return this; return this;
@ -611,6 +615,15 @@ public class ClientConfig {
return backOff; return backOff;
} }
public ClientConfig setEnableNegotiation(boolean enableNegotiation) {
this.enableNegotiation = enableNegotiation;
return this;
}
public boolean isEnableNegotiation() {
return enableNegotiation;
}
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

View file

@ -10,8 +10,7 @@ import io.netty.handler.codec.http.cookie.Cookie;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.client.listener.CookieListener; import org.xbib.netty.http.client.listener.CookieListener;
import org.xbib.netty.http.client.listener.HttpHeadersListener; import org.xbib.netty.http.client.listener.ResponseListener;
import org.xbib.netty.http.client.listener.HttpResponseListener;
import org.xbib.netty.http.client.retry.BackOff; import org.xbib.netty.http.client.retry.BackOff;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -51,9 +50,7 @@ public class Request {
private CompletableFuture<?> completableFuture; private CompletableFuture<?> completableFuture;
private HttpResponseListener responseListener; private ResponseListener responseListener;
private HttpHeadersListener headersListener;
private CookieListener cookieListener; private CookieListener cookieListener;
@ -136,6 +133,12 @@ public class Request {
return true; return true;
} }
public void release() {
if (content != null) {
content.release();
}
}
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@ -160,14 +163,6 @@ public class Request {
return completableFuture; return completableFuture;
} }
public Request setHeadersListener(HttpHeadersListener httpHeadersListener) {
this.headersListener = httpHeadersListener;
return this;
}
public HttpHeadersListener getHeadersListener() {
return headersListener;
}
public Request setCookieListener(CookieListener cookieListener) { public Request setCookieListener(CookieListener cookieListener) {
this.cookieListener = cookieListener; this.cookieListener = cookieListener;
@ -178,12 +173,12 @@ public class Request {
return cookieListener; return cookieListener;
} }
public Request setResponseListener(HttpResponseListener httpResponseListener) { public Request setResponseListener(ResponseListener responseListener) {
this.responseListener = httpResponseListener; this.responseListener = responseListener;
return this; return this;
} }
public HttpResponseListener getResponseListener() { public ResponseListener getResponseListener() {
return responseListener; return responseListener;
} }
@ -224,7 +219,7 @@ public class Request {
} }
public static RequestBuilder builder(HttpMethod httpMethod) { 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) { public static RequestBuilder builder(ByteBufAllocator allocator, HttpMethod httpMethod) {

View file

@ -350,7 +350,8 @@ public class RequestBuilder {
} }
private void content(byte[] buf, AsciiString contentType) { 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) { private void content(ByteBuf body, AsciiString contentType) {

View file

@ -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.ChannelInitializer;
import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.logging.LogLevel; 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 io.netty.handler.ssl.SslHandler;
import org.xbib.netty.http.client.ClientConfig; import org.xbib.netty.http.client.ClientConfig;
import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> { public class HttpChannelInitializer extends ChannelInitializer<Channel> {
private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName()); private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName());
@ -26,18 +30,21 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
private final HttpResponseHandler httpResponseHandler; private final HttpResponseHandler httpResponseHandler;
private final Http2ChannelInitializer http2ChannelInitializer;
public HttpChannelInitializer(ClientConfig clientConfig, public HttpChannelInitializer(ClientConfig clientConfig,
HttpAddress httpAddress, HttpAddress httpAddress,
SslHandler sslHandler, SslHandler sslHandler,
HttpResponseHandler httpResponseHandler) { Http2ChannelInitializer http2ChannelInitializer) {
this.clientConfig = clientConfig; this.clientConfig = clientConfig;
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
this.sslHandler = sslHandler; this.sslHandler = sslHandler;
this.httpResponseHandler = httpResponseHandler; this.http2ChannelInitializer = http2ChannelInitializer;
this.httpResponseHandler = new HttpResponseHandler();
} }
@Override @Override
public void initChannel(SocketChannel channel) { public void initChannel(Channel channel) {
if (clientConfig.isDebug()) { if (clientConfig.isDebug()) {
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
} }
@ -47,17 +54,43 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
configureCleartext(channel); configureCleartext(channel);
} }
if (clientConfig.isDebug()) { 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(); ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(sslHandler); pipeline.addLast(sslHandler);
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); configureCleartext(channel);
} }
}
private void configureCleartext(SocketChannel channel) { private void configureCleartext(Channel channel) {
ChannelPipeline pipeline = channel.pipeline(); ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new HttpClientCodec(clientConfig.getMaxInitialLineLength(), pipeline.addLast(new HttpClientCodec(clientConfig.getMaxInitialLineLength(),
clientConfig.getMaxHeadersSize(), clientConfig.getMaxChunkSize())); clientConfig.getMaxHeadersSize(), clientConfig.getMaxChunkSize()));

View file

@ -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.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;

View file

@ -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.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
@ -10,11 +10,9 @@ import org.xbib.netty.http.client.transport.Transport;
public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> { public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
@Override @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 transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get();
transport.headersReceived(null, httpResponse.headers()); transport.responseReceived(ctx.channel(),null, httpResponse);
transport.responseReceived(null, httpResponse);
transport.success();
} }
@Override @Override

View file

@ -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.buffer.ByteBuf;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;

View file

@ -0,0 +1,4 @@
/**
* HTTP handlers for Netty HTTP client.
*/
package org.xbib.netty.http.client.handler.http;

View file

@ -1,4 +0,0 @@
/**
* HTTP handlers for Netty HTTP client.
*/
package org.xbib.netty.http.client.handler.http1;

View file

@ -1,26 +1,35 @@
package org.xbib.netty.http.client.handler.http2; package org.xbib.netty.http.client.handler.http2;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame;
import io.netty.handler.codec.http2.Http2Connection; 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.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.Http2FrameLogger;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; 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.logging.LogLevel;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandler;
import org.xbib.netty.http.client.ClientConfig; 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 org.xbib.netty.http.common.HttpAddress;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class Http2ChannelInitializer extends ChannelInitializer<SocketChannel> { public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName()); private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName());
@ -30,29 +39,16 @@ public class Http2ChannelInitializer extends ChannelInitializer<SocketChannel> {
private final SslHandler sslHandler; private final SslHandler sslHandler;
private final Http2SettingsHandler http2SettingsHandler;
private final Http2ResponseHandler http2ResponseHandler;
public Http2ChannelInitializer(ClientConfig clientConfig, public Http2ChannelInitializer(ClientConfig clientConfig,
HttpAddress httpAddress, HttpAddress httpAddress,
SslHandler sslHandler, SslHandler sslHandler) {
Http2SettingsHandler http2SettingsHandler,
Http2ResponseHandler http2ResponseHandler) {
this.clientConfig = clientConfig; this.clientConfig = clientConfig;
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
this.sslHandler = sslHandler; this.sslHandler = sslHandler;
this.http2SettingsHandler = http2SettingsHandler;
this.http2ResponseHandler = http2ResponseHandler;
} }
/**
* The channel initialization for HTTP/2.
*
* @param channel socket channel
*/
@Override @Override
public void initChannel(SocketChannel channel) { public void initChannel(Channel channel) {
if (clientConfig.isDebug()) { if (clientConfig.isDebug()) {
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
} }
@ -66,44 +62,73 @@ public class Http2ChannelInitializer extends ChannelInitializer<SocketChannel> {
} }
} }
private void configureEncrypted(SocketChannel channel) { private void configureEncrypted(Channel channel) {
channel.pipeline().addLast(sslHandler); channel.pipeline().addLast(sslHandler);
ApplicationProtocolNegotiationHandler negotiationHandler = new ApplicationProtocolNegotiationHandler("") { configureCleartext(channel);
}
public void configureCleartext(Channel ch) {
ChannelInitializer<Channel> initializer = new ChannelInitializer<Channel>() {
@Override @Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { protected void initChannel(Channel ch) {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { throw new IllegalStateException();
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);
} }
}; };
channel.pipeline().addLast(negotiationHandler); Http2MultiplexCodecBuilder clientMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forClient(initializer)
} .initialSettings(clientConfig.getHttp2Settings());
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()));
if (clientConfig.isDebug()) { if (clientConfig.isDebug()) {
Http2FrameLogger http2FrameLogger = new Http2FrameLogger(clientConfig.getDebugLogLevel(), "client"); clientMultiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "client"));
http2ConnectionHandlerBuilder.frameLogger(http2FrameLogger); }
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();
} }
} }

View file

@ -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);
}
}

View file

@ -7,24 +7,14 @@ import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.codec.http2.HttpConversionUtil;
import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.client.transport.Transport;
import java.io.IOException;
@ChannelHandler.Sharable @ChannelHandler.Sharable
public class Http2ResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> { public class Http2ResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
@Override @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(); Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get();
Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
transport.headersReceived(streamId, httpResponse.headers()); transport.responseReceived(ctx.channel(), streamId, httpResponse);
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"));
} }
@Override @Override

View file

@ -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<Http2Settings> {
@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);
}
}

View file

@ -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<Http2StreamFrame, HttpObject> {
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<Object> 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<Object> 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<Object> 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);
}
}

View file

@ -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);
}

View file

@ -3,7 +3,7 @@ package org.xbib.netty.http.client.listener;
import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse;
@FunctionalInterface @FunctionalInterface
public interface HttpResponseListener { public interface ResponseListener {
void onResponse(FullHttpResponse fullHttpResponse); void onResponse(FullHttpResponse fullHttpResponse);
} }

View file

@ -5,12 +5,15 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPromise;
import io.netty.channel.pool.ChannelPoolHandler; import io.netty.channel.pool.ChannelPoolHandler;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
import org.xbib.netty.http.common.PoolKey; import org.xbib.netty.http.common.PoolKey;
import java.io.IOException;
import java.net.ConnectException; import java.net.ConnectException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -21,6 +24,7 @@ import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
@ -36,8 +40,6 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
private final HttpVersion httpVersion; private final HttpVersion httpVersion;
private final boolean isSecure;
private final ChannelPoolHandler channelPoolhandler; private final ChannelPoolHandler channelPoolhandler;
private final List<K> nodes; private final List<K> nodes;
@ -60,26 +62,36 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
private final AttributeKey<K> attributeKey; private final AttributeKey<K> attributeKey;
private PoolKeySelector<K> poolKeySelector;
/** /**
* @param semaphore the concurrency level * @param semaphore the concurrency level
* @param httpVersion the HTTP version of the pool connections * @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 ":") * @param nodes the endpoint nodes, any element may contain the port (followed after ":")
* to override the defaultPort argument * to override the defaultPort argument
* @param bootstrap bootstrap instance * @param bootstrap bootstrap instance
* @param channelPoolHandler channel pool handler being notified upon new connection is created * @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 * @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. * 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<K> nodes, Bootstrap bootstrap, List<K> nodes, Bootstrap bootstrap,
ChannelPoolHandler channelPoolHandler, int retriesPerNode) { ChannelPoolHandler channelPoolHandler, int retriesPerNode,
PoolKeySelectorType poolKeySelectorType) {
this.semaphore = semaphore; this.semaphore = semaphore;
this.httpVersion = httpVersion; this.httpVersion = httpVersion;
this.isSecure = isSecure;
this.channelPoolhandler = channelPoolHandler; this.channelPoolhandler = channelPoolHandler;
this.nodes = nodes; this.nodes = nodes;
this.retriesPerNode = retriesPerNode; this.retriesPerNode = retriesPerNode;
switch (poolKeySelectorType) {
case RANDOM:
this.poolKeySelector = new RandomPoolKeySelector();
break;
case ROUNDROBIN:
this.poolKeySelector = new RoundRobinKeySelector();
break;
}
this.lock = new ReentrantLock(); this.lock = new ReentrantLock();
this.attributeKey = AttributeKey.valueOf("poolKey"); this.attributeKey = AttributeKey.valueOf("poolKey");
if (nodes == null || nodes.isEmpty()) { if (nodes == null || nodes.isEmpty()) {
@ -105,10 +117,6 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
return httpVersion; return httpVersion;
} }
public boolean isSecure() {
return isSecure;
}
public AttributeKey<K> getAttributeKey() { public AttributeKey<K> getAttributeKey() {
return attributeKey; return attributeKey;
} }
@ -121,7 +129,7 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
for (int i = 0; i < channelCount; i++) { for (int i = 0; i < channelCount; i++) {
Channel channel = newConnection(); Channel channel = newConnection();
if (channel == null) { if (channel == null) {
throw new ConnectException("failed to prepare"); throw new ConnectException("failed to prepare channels");
} }
K key = channel.attr(attributeKey).get(); K key = channel.attr(attributeKey).get();
if (channel.isActive()) { if (channel.isActive()) {
@ -133,7 +141,7 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
channel.close(); channel.close();
} }
} }
logger.log(Level.FINE,"prepared " + channelCount + " channels"); logger.log(Level.FINE,"prepared " + channelCount + " channels: " + availableChannels);
} }
@Override @Override
@ -156,35 +164,7 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
} }
@Override @Override
public int acquire(List<Channel> channels, int maxCount) throws Exception { public void release(Channel channel, boolean close) 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 {
try { try {
if (channel != null) { if (channel != null) {
if (channel.isActive()) { if (channel.isActive()) {
@ -193,10 +173,9 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
if (channelQueue != null) { if (channelQueue != null) {
channelQueue.add(channel); channelQueue.add(channel);
} }
} else if (channel.isOpen()) { } else if (channel.isOpen() && close) {
logger.log(Level.FINE, "trying to close channel " + channel);
channel.close(); channel.close();
} else {
logger.log(Level.WARNING, "channel not active or open while release");
} }
if (channelPoolhandler != null) { if (channelPoolhandler != null) {
channelPoolhandler.channelReleased(channel); channelPoolhandler.channelReleased(channel);
@ -208,16 +187,10 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
} }
@Override @Override
public void release(List<Channel> channels) throws Exception { public void close() throws IOException {
for (Channel channel : channels) {
release(channel);
}
}
@Override
public void close() {
lock.lock(); lock.lock();
try { try {
logger.log(Level.FINE, "closing pool");
int count = 0; int count = 0;
Set<Channel> channelSet = new HashSet<>(); Set<Channel> channelSet = new HashSet<>();
for (Map.Entry<K, Queue<Channel>> entry : availableChannels.entrySet()) { for (Map.Entry<K, Queue<Channel>> entry : availableChannels.entrySet()) {
@ -228,7 +201,21 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
} }
for (Channel channel : channelSet) { for (Channel channel : channelSet) {
if (channel != null && channel.isOpen()) { 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(); channel.close();
count++; count++;
} }
@ -237,7 +224,7 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
channels.clear(); channels.clear();
bootstraps.clear(); bootstraps.clear();
counts.clear(); counts.clear();
logger.log(Level.FINE, "closed " + count + " connections"); logger.log(Level.FINE, "closed pool (found " + count + " connections open)");
} finally { } finally {
lock.unlock(); lock.unlock();
} }
@ -246,17 +233,13 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
private Channel newConnection() throws ConnectException { private Channel newConnection() throws ConnectException {
Channel channel = null; Channel channel = null;
K key = null; K key = null;
K nextKey; Integer min = Integer.MAX_VALUE;
int min = Integer.MAX_VALUE; Integer next;
int next; //int r = ThreadLocalRandom.current().nextInt(numberOfNodes);
int i = ThreadLocalRandom.current().nextInt(numberOfNodes); for (int j = 0; j < numberOfNodes; j++) {
for (int j = i; j < numberOfNodes; j ++) { K nextKey = poolKeySelector.key(); //nodes.get(j % numberOfNodes);
nextKey = nodes.get(j % numberOfNodes);
if (counts == null) {
throw new ConnectException("strange");
}
next = counts.get(nextKey); next = counts.get(nextKey);
if (next == 0) { if (next == null || next == 0) {
key = nextKey; key = nextKey;
break; break;
} else if (next < min) { } else if (next < min) {
@ -305,7 +288,6 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
if (retriesPerNode > 0) { if (retriesPerNode > 0) {
failedCounts.put(key, 0); failedCounts.put(key, 0);
} }
logger.log(Level.FINE,"new connection to " + key + " created");
} }
return channel; return channel;
} }
@ -319,19 +301,11 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
} }
private Channel poll() { private Channel poll() {
int i = ThreadLocalRandom.current().nextInt(numberOfNodes);
Queue<Channel> channelQueue; Queue<Channel> channelQueue;
Channel channel; Channel channel;
for(int j = i; j < i + numberOfNodes; j ++) { //int r = ThreadLocalRandom.current().nextInt(numberOfNodes);
K key = nodes.get(j % numberOfNodes); for (int j = 0; j < numberOfNodes; j++) {
// for HTTP/2, use channel list K key = poolKeySelector.key(); //nodes.get(j % numberOfNodes);
logger.log(Level.FINE, "pool version = " + httpVersion);
if (httpVersion.majorVersion() == 2) {
List<Channel> list = channels.get(key);
if (!list.isEmpty()) {
logger.log(Level.INFO, "we have a channel " + list);
}
}
channelQueue = availableChannels.get(key); channelQueue = availableChannels.get(key);
if (channelQueue != null) { if (channelQueue != null) {
channel = channelQueue.poll(); channel = channelQueue.poll();
@ -339,12 +313,39 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
return channel; return channel;
} }
} else { } else {
logger.log(Level.FINE, "channelqueue is null"); logger.log(Level.WARNING, "channel queue is null?");
} }
} }
return null; return null;
} }
public enum PoolKeySelectorType {
RANDOM, ROUNDROBIN
}
private interface PoolKeySelector<K extends PoolKey> {
K key();
}
private class RandomPoolKeySelector implements PoolKeySelector<K> {
@Override
public K key() {
int r = ThreadLocalRandom.current().nextInt(numberOfNodes);
return nodes.get(r % numberOfNodes);
}
}
private class RoundRobinKeySelector implements PoolKeySelector<K> {
int r = 0;
@Override
public K key() {
return nodes.get(r++ % numberOfNodes);
}
}
private class CloseChannelListener implements ChannelFutureListener { private class CloseChannelListener implements ChannelFutureListener {
private final K key; private final K key;

View file

@ -1,7 +1,6 @@
package org.xbib.netty.http.client.pool; package org.xbib.netty.http.client.pool;
import java.io.Closeable; import java.io.Closeable;
import java.util.List;
public interface Pool<T> extends Closeable { public interface Pool<T> extends Closeable {
@ -9,9 +8,5 @@ public interface Pool<T> extends Closeable {
T acquire() throws Exception; T acquire() throws Exception;
int acquire(List<T> list, int maxCount) throws Exception; void release(T t, boolean close) throws Exception;
void release(T t) throws Exception;
void release(List<T> list) throws Exception;
} }

View file

@ -13,12 +13,9 @@ import org.xbib.netty.http.client.transport.Transport;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
public class RestClient { public class RestClient {
private static final Logger logger = Logger.getLogger(RestClient.class.getName());
private Client client; private Client client;
private Transport transport; private Transport transport;
@ -70,6 +67,4 @@ public class RestClient {
transport.execute(requestBuilder.build().setResponseListener(restClient::setResponse)).get(); transport.execute(requestBuilder.build().setResponseListener(restClient::setResponse)).get();
return restClient; return restClient;
} }
} }

View file

@ -1,7 +1,5 @@
package org.xbib.netty.http.client.retry; package org.xbib.netty.http.client.retry;
import java.io.IOException;
/** /**
* Back-off policy when retrying an operation. * 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 * Gets the number of milliseconds to wait before retrying the operation or {@link #STOP} to
* indicate that no retries should be made. * indicate that no retries should be made.
* *
* @return milliseconds before operation retry
*
* <p> * <p>
* Example usage: * Example usage:
* </p> * </p>

View file

@ -185,6 +185,10 @@ public class ExponentialBackOff implements BackOff {
/** /**
* Returns a random value from the interval [randomizationFactor * currentInterval, * Returns a random value from the interval [randomizationFactor * currentInterval,
* 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) { public static int getRandomValueFromInterval(double randomizationFactor, double random, int currentIntervalMillis) {
double delta = randomizationFactor * currentIntervalMillis; double delta = randomizationFactor * currentIntervalMillis;
@ -196,14 +200,17 @@ public class ExponentialBackOff implements BackOff {
return (int) (minInterval + (random * (maxInterval - minInterval + 1))); 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() { public final int getInitialIntervalMillis() {
return initialIntervalMillis; return initialIntervalMillis;
} }
/** /**
* Returns the randomization factor to use for creating a range around the retry interval. * Returns the randomization factor to use for creating a range around the retry interval.
* * @return randomization factor
* <p> * <p>
* A randomization factor of 0.5 results in a random period ranging between 50% below and 50% * A randomization factor of 0.5 results in a random period ranging between 50% below and 50%
* above the retry interval. * above the retry interval.
@ -215,6 +222,7 @@ public class ExponentialBackOff implements BackOff {
/** /**
* Returns the current retry interval in milliseconds. * Returns the current retry interval in milliseconds.
* @return current interval in milliseconds
*/ */
public final int getCurrentIntervalMillis() { public final int getCurrentIntervalMillis() {
return currentIntervalMillis; return currentIntervalMillis;
@ -222,6 +230,7 @@ public class ExponentialBackOff implements BackOff {
/** /**
* Returns the value to multiply the current interval with for each retry attempt. * Returns the value to multiply the current interval with for each retry attempt.
* @return multiplier
*/ */
public final double getMultiplier() { public final double getMultiplier() {
return multiplier; 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 * Returns the maximum value of the back off period in milliseconds. Once the current interval
* reaches this value it stops increasing. * reaches this value it stops increasing.
* @return maximum interval value in milliseconds
*/ */
public final int getMaxIntervalMillis() { public final int getMaxIntervalMillis() {
return maxIntervalMillis; return maxIntervalMillis;
@ -237,7 +247,7 @@ public class ExponentialBackOff implements BackOff {
/** /**
* Returns the maximum elapsed time in milliseconds. * Returns the maximum elapsed time in milliseconds.
* * @return maximum elapsed time in milliseconds
* <p> * <p>
* If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the * If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the
* max_elapsed_time then the method {@link #nextBackOffMillis()} starts returning * 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 * Returns the elapsed time in milliseconds since an {@link ExponentialBackOff} instance is
* created and is reset when {@link #reset()} is called. * created and is reset when {@link #reset()} is called.
* * @return the elapsed time in milliseconds
* <p> * <p>
* The elapsed time is computed using {@link System#nanoTime()}. * The elapsed time is computed using {@link System#nanoTime()}.
* </p> * </p>
@ -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 * 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()}. * measure elapsed time, to match the behavior of {@link System#nanoTime()}.
* @return value of timer in nanoseconds
*/ */
long nanoTime(); long nanoTime();
@ -345,7 +356,8 @@ public class ExponentialBackOff implements BackOff {
/** /**
* Builds a new instance of {@link ExponentialBackOff}. * Builds a new instance of {@link ExponentialBackOff}.
* */ * @return an {@link ExponentialBackOff} instance
*/
public ExponentialBackOff build() { public ExponentialBackOff build() {
if (initialIntervalMillis <= 0) { if (initialIntervalMillis <= 0) {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
@ -365,17 +377,11 @@ public class ExponentialBackOff implements BackOff {
return new ExponentialBackOff(this); 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 * Sets the initial retry interval in milliseconds. The default value is
* {@link #DEFAULT_INITIAL_INTERVAL_MILLIS}. Must be {@code > 0}. * {@link #DEFAULT_INITIAL_INTERVAL_MILLIS}. Must be {@code > 0}.
* @param initialIntervalMillis interval milliseconds
* @return the builder
* *
* <p> * <p>
* Overriding is only supported for the purpose of calling the super implementation and changing * 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; return this;
} }
/**
* Returns the randomization factor to use for creating a range around the retry interval. The
* default value is {@link #DEFAULT_RANDOMIZATION_FACTOR}.
*
* <p>
* A randomization factor of 0.5 results in a random period ranging between 50% below and 50%
* above the retry interval.
* </p>
*
* <p>
* Overriding is only supported for the purpose of calling the super implementation and changing
* the return type, but nothing else.
* </p>
*/
public final double getRandomizationFactor() {
return randomizationFactor;
}
/** /**
* Sets the randomization factor to use for creating a range around the retry interval. The * 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 * default value is {@link #DEFAULT_RANDOMIZATION_FACTOR}. Must fall in the range
* {@code 0 <= randomizationFactor < 1}. * {@code 0 <= randomizationFactor < 1}.
* @param randomizationFactor the randomization factor
* @return the builder
* *
* <p> * <p>
* A randomization factor of 0.5 results in a random period ranging between 50% below and 50% * 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; 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 * Sets the value to multiply the current interval with for each retry attempt. The default
* value is {@link #DEFAULT_MULTIPLIER}. Must be {@code >= 1}. * value is {@link #DEFAULT_MULTIPLIER}. Must be {@code >= 1}.
* @param multiplier the multiplier
* @return the builder
* *
* <p> * <p>
* Overriding is only supported for the purpose of calling the super implementation and changing * 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; 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 * 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 * reaches this value it stops increasing. The default value is
* {@link #DEFAULT_MAX_INTERVAL_MILLIS}. * {@link #DEFAULT_MAX_INTERVAL_MILLIS}.
* @param maxIntervalMillis maximum interval in miliseconds
* @return the builder
* *
* <p> * <p>
* Overriding is only supported for the purpose of calling the super implementation and changing * 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; return this;
} }
/**
* Returns the maximum elapsed time in milliseconds. The default value is
* {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}.
*
* <p>
* 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()}.
* </p>
*/
public final int getMaxElapsedTimeMillis() {
return maxElapsedTimeMillis;
}
/** /**
* Sets the maximum elapsed time in milliseconds. The default value is * Sets the maximum elapsed time in milliseconds. The default value is
* {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}. Must be {@code > 0}. * {@link #DEFAULT_MAX_ELAPSED_TIME_MILLIS}. Must be {@code > 0}.
* @param maxElapsedTimeMillis maximum elapsed time millis
* @return the builder
* *
* <p> * <p>
* If the time elapsed since an {@link ExponentialBackOff} instance is created goes past the * 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; return this;
} }
/**
* Returns the nano clock.
*/
public final NanoClock getNanoClock() {
return nanoClock;
}
/** /**
* Sets the nano clock ({@link NanoClock#SYSTEM} by default). * Sets the nano clock ({@link NanoClock#SYSTEM} by default).
* * @param nanoClock the nano clock
* @return the builder
* <p> * <p>
* Overriding is only supported for the purpose of calling the super implementation and changing * Overriding is only supported for the purpose of calling the super implementation and changing
* the return type, but nothing else. * the return type, but nothing else.

View file

@ -1,16 +1,10 @@
package org.xbib.netty.http.client.transport; package org.xbib.netty.http.client.transport;
import io.netty.channel.Channel; 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.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames; 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.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.http.cookie.Cookie;
import io.netty.handler.codec.http2.HttpConversionUtil;
import org.xbib.net.PercentDecoder; import org.xbib.net.PercentDecoder;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.net.URLSyntaxException; 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.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.RequestBuilder; 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 org.xbib.netty.http.client.retry.BackOff;
import java.io.IOException; import java.io.IOException;
@ -27,15 +19,15 @@ import java.net.ConnectException;
import java.nio.charset.MalformedInputException; import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException; import java.nio.charset.UnmappableCharacterException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function; import java.util.function.Function;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -49,64 +41,26 @@ abstract class BaseTransport implements Transport {
protected final HttpAddress httpAddress; protected final HttpAddress httpAddress;
protected Channel channel;
protected SortedMap<Integer, Request> requests;
protected Throwable throwable; protected Throwable throwable;
private static final Request DUMMY = Request.builder(HttpMethod.GET).build();
private final Map<Request, Channel> channels;
final Map<String, Flow> channelFlowMap;
final SortedMap<String, Request> requests;
private Map<Cookie, Boolean> cookieBox; private Map<Cookie, Boolean> cookieBox;
BaseTransport(Client client, HttpAddress httpAddress) { BaseTransport(Client client, HttpAddress httpAddress) {
this.client = client; this.client = client;
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
this.channels = new ConcurrentHashMap<>();
this.channelFlowMap = new ConcurrentHashMap<>();
this.requests = new ConcurrentSkipListMap<>(); 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<Cookie> 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. * Experimental method for executing in a wrapping completable future.
* @param request request * @param request request
@ -124,9 +78,8 @@ abstract class BaseTransport implements Transport {
} }
@Override @Override
public synchronized void close() throws IOException { public synchronized void close() {
get(); get();
client.releaseChannel(channel);
} }
@Override @Override
@ -139,53 +92,130 @@ abstract class BaseTransport implements Transport {
return throwable; return throwable;
} }
/**
* The underlying network layer failed, not possible to know the request.
* So we fail all (open) promises.
* @param throwable the exception
*/
@Override @Override
public void headersReceived(Integer streamId, HttpHeaders httpHeaders) { public void fail(Throwable throwable) {
Request request = fromStreamId(streamId); // do not fail more than once
if (request != null) { if (this.throwable != null) {
HttpHeadersListener httpHeadersListener = request.getHeadersListener(); return;
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);
}
} }
logger.log(Level.SEVERE, "failing: " + throwable.getMessage(), throwable);
this.throwable = throwable;
for (Flow flow : channelFlowMap.values()) {
flow.fail(throwable);
} }
} }
private void ensureConnect() throws IOException { @Override
public Transport get() {
return get(client.getClientConfig().getReadTimeoutMillis(), TimeUnit.MILLISECONDS);
}
@Override
public Transport get(long value, TimeUnit timeUnit) {
for (Map.Entry<String, Flow> 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<String, Flow> 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) { if (channel == null) {
channel = client.newChannel(httpAddress); 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) { if (channel != null) {
channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this); channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this);
awaitSettings(); waitForSettings();
} else { } else {
ConnectException connectException; ConnectException connectException;
if (httpAddress != null) { if (httpAddress != null) {
connectException = new ConnectException("unable to connect to " + httpAddress); connectException = new ConnectException("unable to connect to " + httpAddress);
} else if (client.hasPooledConnections()){ } else if (client.hasPooledConnections()) {
connectException = new ConnectException("unable to get channel from pool"); connectException = new ConnectException("unable to get channel from pool");
} else { } else {
// if API misuse // API misuse
connectException = new ConnectException("unable to get channel"); connectException = new ConnectException("unable to get channel");
} }
this.throwable = connectException; this.throwable = connectException;
this.channel = null;
throw connectException; throw connectException;
} }
} return channel;
}
protected Request fromStreamId(Integer streamId) {
if (streamId == null) {
streamId = requests.lastKey();
}
return requests.get(streamId);
} }
protected Request continuation(Request request, FullHttpResponse httpResponse) throws URLSyntaxException { protected Request continuation(Request request, FullHttpResponse httpResponse) throws URLSyntaxException {
@ -224,7 +254,6 @@ abstract class BaseTransport implements Transport {
request.cookies().forEach(newHttpRequestBuilder::addCookie); request.cookies().forEach(newHttpRequestBuilder::addCookie);
Request newHttpRequest = newHttpRequestBuilder.build(); Request newHttpRequest = newHttpRequestBuilder.build();
newHttpRequest.setResponseListener(request.getResponseListener()); newHttpRequest.setResponseListener(request.getResponseListener());
newHttpRequest.setHeadersListener(request.getHeadersListener());
newHttpRequest.setCookieListener(request.getCookieListener()); newHttpRequest.setCookieListener(request.getCookieListener());
StringBuilder hostAndPort = new StringBuilder(); StringBuilder hostAndPort = new StringBuilder();
hostAndPort.append(redirUrl.getHost()); hostAndPort.append(redirUrl.getHost());
@ -235,6 +264,7 @@ abstract class BaseTransport implements Transport {
logger.log(Level.FINE, "redirect url: " + redirUrl + logger.log(Level.FINE, "redirect url: " + redirUrl +
" old request: " + request.toString() + " old request: " + request.toString() +
" new request: " + newHttpRequest.toString()); " new request: " + newHttpRequest.toString());
request.release();
return newHttpRequest; return newHttpRequest;
} }
break; break;
@ -297,20 +327,20 @@ abstract class BaseTransport implements Transport {
return cookieBox; return cookieBox;
} }
private void addCookie(Cookie cookie) { void addCookie(Cookie cookie) {
if (cookieBox == null) { if (cookieBox == null) {
this.cookieBox = Collections.synchronizedMap(new LRUCache<Cookie, Boolean>(32)); this.cookieBox = Collections.synchronizedMap(new LRUCache<Cookie, Boolean>(32));
} }
cookieBox.put(cookie, true); cookieBox.put(cookie, true);
} }
private List<Cookie> matchCookiesFromBox(Request request) { List<Cookie> matchCookiesFromBox(Request request) {
return cookieBox == null ? Collections.emptyList() : cookieBox.keySet().stream().filter(cookie -> return cookieBox == null ? Collections.emptyList() : cookieBox.keySet().stream().filter(cookie ->
matchCookie(request.url(), cookie) matchCookie(request.url(), cookie)
).collect(Collectors.toList()); ).collect(Collectors.toList());
} }
private List<Cookie> matchCookies(Request request) { List<Cookie> matchCookies(Request request) {
return request.cookies().stream().filter(cookie -> return request.cookies().stream().filter(cookie ->
matchCookie(request.url(), cookie) matchCookie(request.url(), cookie)
).collect(Collectors.toList()); ).collect(Collectors.toList());

View file

@ -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<Integer, CompletableFuture<Boolean>> map;
Flow() {
this.counter = new AtomicInteger(3);
this.map = new ConcurrentSkipListMap<>();
}
CompletableFuture<Boolean> get(Integer key) {
return map.get(key);
}
Set<Integer> keys() {
return map.keySet();
}
Integer firstKey() {
return map.firstKey();
}
Integer lastKey() {
return map.lastKey();
}
void put(Integer key, CompletableFuture<Boolean> 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<Boolean> promise : map.values()) {
promise.completeExceptionally(throwable);
}
}
public void close() {
map.clear();
}
@Override
public String toString() {
return "[next=" + counter + ", " + map + "]";
}
}

View file

@ -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<Integer, CompletableFuture<Boolean>> 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<Boolean> 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<Boolean> 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<Boolean> promise : sequentialPromiseMap.values()) {
promise.complete(true);
}
}
@Override
public void fail(Throwable throwable) {
this.throwable = throwable;
for (CompletableFuture<Boolean> promise : sequentialPromiseMap.values()) {
promise.completeExceptionally(throwable);
}
}
}

View file

@ -1,23 +1,38 @@
package org.xbib.netty.http.client.transport; package org.xbib.netty.http.client.transport;
import io.netty.channel.Channel; 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.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.Http2Headers;
import io.netty.handler.codec.http2.Http2Settings; 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.net.URLSyntaxException;
import org.xbib.netty.http.client.Client; 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.common.HttpAddress;
import org.xbib.netty.http.client.Request; 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.io.IOException;
import java.util.SortedMap; import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -27,33 +42,76 @@ public class Http2Transport extends BaseTransport {
private CompletableFuture<Boolean> settingsPromise; private CompletableFuture<Boolean> settingsPromise;
private final AtomicInteger streamIdCounter; private final ChannelInitializer<Channel> initializer;
private SortedMap<Integer, CompletableFuture<Boolean>> streamidPromiseMap;
public Http2Transport(Client client, HttpAddress httpAddress) { public Http2Transport(Client client, HttpAddress httpAddress) {
super(client, httpAddress); super(client, httpAddress);
streamIdCounter = new AtomicInteger(3); this.settingsPromise = httpAddress != null ? new CompletableFuture<>() : null;
streamidPromiseMap = new ConcurrentSkipListMap<>(); final Transport transport = this;
settingsPromise = (httpAddress != null /*&& httpAddress.isSecure() */) || this.initializer = new ChannelInitializer<Channel>() {
(client.hasPooledConnections() && client.getPool().isSecure()) ? @Override
new CompletableFuture<>() : null; 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 @Override
public Integer nextStream() { public Transport execute(Request request) throws IOException {
Integer streamId = streamIdCounter.getAndAdd(2); Channel channel = mapChannel(request);
if (streamId == Integer.MIN_VALUE) { if (throwable != null) {
// reset if overflow, Java wraps atomic integers to Integer.MIN_VALUE return this;
streamIdCounter.set(3);
streamId = 3;
} }
streamidPromiseMap.put(streamId, new CompletableFuture<>()); final String channelId = channel.id().toString();
return streamId; 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<Cookie> 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 @Override
public void settingsReceived(Channel channel, Http2Settings http2Settings) { public void settingsReceived(Http2Settings http2Settings) {
if (settingsPromise != null) { if (settingsPromise != null) {
settingsPromise.complete(true); settingsPromise.complete(true);
} else { } else {
@ -62,36 +120,53 @@ public class Http2Transport extends BaseTransport {
} }
@Override @Override
public void awaitSettings() { public void waitForSettings() {
if (settingsPromise != null) { if (settingsPromise != null) {
try { try {
logger.log(Level.FINE, "waiting for settings");
settingsPromise.get(client.getClientConfig().getReadTimeoutMillis(), TimeUnit.MILLISECONDS); settingsPromise.get(client.getClientConfig().getReadTimeoutMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) { } catch (TimeoutException e) {
logger.log(Level.WARNING, "settings timeout"); logger.log(Level.WARNING, "timeout in client while waiting for settings");
settingsPromise.completeExceptionally(e); settingsPromise.completeExceptionally(e);
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException e) {
settingsPromise.completeExceptionally(e); settingsPromise.completeExceptionally(e);
} }
} else {
logger.log(Level.WARNING, "settings promise is null");
} }
} }
@Override @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) { 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; return;
} }
CompletableFuture<Boolean> promise = streamidPromiseMap.get(streamId); // format of childchan channel ID is <parent channel ID> "/" <substream ID>
if (promise == null) { String channelId = channel.id().toString();
logger.log(Level.WARNING, "response received for stream ID " + streamId + " but found no promise"); int pos = channelId.indexOf('/');
channelId = pos > 0 ? channelId.substring(0, pos) : channelId;
Flow flow = channelFlowMap.get(channelId);
if (flow == null) {
return; return;
} }
Request request = fromStreamId(streamId); String requestKey = getRequestKey(channelId, streamId);
if (request != null) { CompletableFuture<Boolean> promise = flow.get(streamId);
HttpResponseListener responseListener = request.getResponseListener(); 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);
}
}
ResponseListener responseListener = request.getResponseListener();
if (responseListener != null) { if (responseListener != null) {
responseListener.onResponse(fullHttpResponse); responseListener.onResponse(fullHttpResponse);
} }
@ -107,93 +182,26 @@ public class Http2Transport extends BaseTransport {
client.continuation(this, continueRequest); client.continuation(this, continueRequest);
} }
} }
promise.complete(true);
} catch (URLSyntaxException | IOException e) { } catch (URLSyntaxException | IOException e) {
logger.log(Level.WARNING, e.getMessage(), e); promise.completeExceptionally(e);
} }
} }
promise.complete(true); }
channelFlowMap.get(channelId).remove(streamId);
requests.remove(requestKey);
} }
@Override @Override
public void pushPromiseReceived(Integer streamId, Integer promisedStreamId, Http2Headers headers) { public void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers) {
streamidPromiseMap.put(promisedStreamId, new CompletableFuture<>()); String channelId = channel.id().toString();
requests.put(promisedStreamId, fromStreamId(streamId)); channelFlowMap.get(channelId).put(promisedStreamId, new CompletableFuture<>());
String requestKey = getRequestKey(channel.id().toString(), promisedStreamId);
requests.put(requestKey, requests.get(requestKey));
} }
@Override @Override
public void awaitResponse(Integer streamId) throws IOException { protected String getRequestKey(String channelId, Integer streamId) {
if (streamId == null) { return channelId + "#" + streamId;
return;
}
if (throwable != null) {
return;
}
CompletableFuture<Boolean> 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();
}
promise.get(millis, TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
this.throwable = e;
throw new IOException(e);
} finally {
streamidPromiseMap.remove(streamId);
}
}
}
@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;
}
@Override
public void success() {
for (CompletableFuture<Boolean> 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<Boolean> 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);
}
} }
} }

View file

@ -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<Cookie> 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<Boolean> 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();
}
}

View file

@ -2,8 +2,8 @@ package org.xbib.netty.http.client.transport;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.handler.codec.http.FullHttpResponse; 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.http.cookie.Cookie;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Settings;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
@ -12,7 +12,7 @@ import org.xbib.netty.http.client.Request;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeUnit;
import java.util.function.Function; import java.util.function.Function;
public interface Transport { public interface Transport {
@ -23,27 +23,23 @@ public interface Transport {
<T> CompletableFuture<T> execute(Request request, Function<FullHttpResponse, T> supplier) throws IOException; <T> CompletableFuture<T> execute(Request request, Function<FullHttpResponse, T> 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<Cookie, Boolean> cookieBox); void setCookieBox(Map<Cookie, Boolean> cookieBox);
Map<Cookie, Boolean> getCookieBox(); Map<Cookie, Boolean> 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(); Transport get();
void success(); Transport get(long value, TimeUnit timeUnit);
void cancel();
void fail(Throwable throwable); void fail(Throwable throwable);

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client.test; package org.xbib;
import java.util.logging.ConsoleHandler; import java.util.logging.ConsoleHandler;
import java.util.logging.Handler; import java.util.logging.Handler;
@ -7,20 +7,25 @@ import java.util.logging.LogManager;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.logging.SimpleFormatter; import java.util.logging.SimpleFormatter;
public class LoggingBase { public class TestBase {
static { 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", 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(); LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger(""); Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler(); Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter()); handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler); rootLogger.addHandler(handler);
rootLogger.setLevel(Level.ALL); rootLogger.setLevel(Level.FINE);
for (Handler h : rootLogger.getHandlers()) { for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter()); handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.ALL); h.setLevel(Level.FINE);
} }
} }
} }

View file

@ -32,7 +32,7 @@ public class CompletableFutureTest {
.exceptionally(Throwable::getMessage) .exceptionally(Throwable::getMessage)
.thenCompose(content -> { .thenCompose(content -> {
logger.log(Level.INFO, 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 { try {
return client.execute(Request.post() return client.execute(Request.post()
.url("http://google.com/") .url("http://google.com/")

View file

@ -1,8 +1,8 @@
package org.xbib.netty.http.client.test; package org.xbib.netty.http.client.test;
import org.conscrypt.Conscrypt; import org.conscrypt.Conscrypt;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; 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.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class ConscryptTest extends LoggingBase { public class ConscryptTest extends TestBase {
private static final Logger logger = Logger.getLogger(""); private static final Logger logger = Logger.getLogger("");

View file

@ -1,6 +1,7 @@
package org.xbib.netty.http.client.test; package org.xbib.netty.http.client.test;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; 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()); 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") .url("http://httpbin.org/cookies/set?name=value")
.build() .build()
.setCookieListener(cookie -> logger.log(Level.INFO, "this is the cookie: " + cookie.toString())) .setCookieListener(cookie -> logger.log(Level.INFO, "this is the cookie: " + cookie.toString()))
.setHeadersListener(headers -> logger.log(Level.INFO, "headers = " + headers.entries().toString()))
.setResponseListener(fullHttpResponse -> { .setResponseListener(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);

View file

@ -2,6 +2,7 @@ package org.xbib.netty.http.client.test;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.Request;
@ -20,7 +21,7 @@ import java.util.logging.Logger;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@Ignore @Ignore
public class ElasticsearchTest extends LoggingBase { public class ElasticsearchTest extends TestBase {
private static final Logger logger = Logger.getLogger(ElasticsearchTest.class.getName()); private static final Logger logger = Logger.getLogger(ElasticsearchTest.class.getName());
@ -103,7 +104,7 @@ public class ElasticsearchTest extends LoggingBase {
} }
try { try {
for (int i = 0; i < max; i++) { for (int i = 0; i < max; i++) {
client.pooledExecute(queries.get(i)).get(); client.newTransport().execute(queries.get(i)).get();
} }
} catch (IOException e) { } catch (IOException e) {
logger.log(Level.WARNING, e.getMessage(), e); logger.log(Level.WARNING, e.getMessage(), e);

View file

@ -2,6 +2,7 @@ package org.xbib.netty.http.client.test;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; 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.Level;
import java.util.logging.Logger; 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()); private static final Logger logger = Logger.getLogger(Http1Test.class.getName());

View file

@ -3,6 +3,7 @@ package org.xbib.netty.http.client.test;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; 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.Level;
import java.util.logging.Logger; 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()); private static final Logger logger = Logger.getLogger(Http2Test.class.getName());

View file

@ -3,6 +3,7 @@ package org.xbib.netty.http.client.test;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; 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.Level;
import java.util.logging.Logger; 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()); private static final Logger logger = Logger.getLogger(SecureHttp1Test.class.getName());

View file

@ -2,6 +2,7 @@ package org.xbib.netty.http.client.test;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import java.io.IOException; import java.io.IOException;
@ -9,9 +10,15 @@ import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; 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 @After
public void checkThreads() { public void checkThreads() {
@ -19,10 +26,4 @@ public class LeakTest {
logger.log(Level.INFO, "threads = " + threadSet.size() ); logger.log(Level.INFO, "threads = " + threadSet.size() );
threadSet.forEach( thread -> logger.log(Level.INFO, thread.toString())); threadSet.forEach( thread -> logger.log(Level.INFO, thread.toString()));
} }
@Test
public void testForLeaks() throws IOException {
Client client = new Client();
client.shutdownGracefully();
}
} }

View file

@ -3,6 +3,7 @@ package org.xbib.netty.http.client.test;
import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.proxy.HttpProxyHandler;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; 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.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class XbibTest extends LoggingBase { public class XbibTest extends TestBase {
private static final Logger logger = Logger.getLogger(""); private static final Logger logger = Logger.getLogger("");

View file

@ -1,15 +1,18 @@
package org.xbib.netty.http.client.test.pool; package org.xbib.netty.http.client.test.pool;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption; import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.socket.SocketChannel; 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.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import java.io.Closeable;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.atomic.LongAdder;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -76,8 +81,8 @@ public class EpollTest {
.option(ChannelOption.SO_KEEPALIVE, true) .option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.SO_REUSEADDR, true) .option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.TCP_NODELAY, true); .option(ChannelOption.TCP_NODELAY, true);
channelPool = new BoundedChannelPool<>(semaphore, HttpVersion.HTTP_1_1,false, channelPool = new BoundedChannelPool<>(semaphore, HttpVersion.HTTP_1_1,
NODES, bootstrap, null, 0); NODES, bootstrap, null, 0, BoundedChannelPool.PoolKeySelectorType.ROUNDROBIN);
channelPool.prepare(CONCURRENCY); channelPool.prepare(CONCURRENCY);
} }
@ -101,7 +106,7 @@ public class EpollTest {
Thread.sleep(1); // very short? Thread.sleep(1); // very short?
} }
channel.writeAndFlush(PAYLOAD.retain()).sync(); channel.writeAndFlush(PAYLOAD.retain()).sync();
channelPool.release(channel); channelPool.release(channel, false);
longAdder.increment(); longAdder.increment();
} catch (InterruptedException e) { } catch (InterruptedException e) {
break; 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<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
if (dropEveryRequest > 0) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<Object>() {
@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();
}
}
} }

View file

@ -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<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
if (dropEveryRequest > 0) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<Object>() {
@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();
}
}

View file

@ -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<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<Object>() {
@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();
}
}

View file

@ -1,9 +1,11 @@
package org.xbib.netty.http.client.test.pool; package org.xbib.netty.http.client.test.pool;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption; import io.netty.channel.ChannelOption;
@ -11,6 +13,7 @@ import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import org.junit.After; 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.Pool;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.pool.BoundedChannelPool;
import java.io.Closeable;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.atomic.LongAdder;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -73,8 +78,8 @@ public class NioTest {
.option(ChannelOption.SO_KEEPALIVE, true) .option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.SO_REUSEADDR, true) .option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.TCP_NODELAY, true); .option(ChannelOption.TCP_NODELAY, true);
channelPool = new BoundedChannelPool<>(semaphore, HttpVersion.HTTP_1_1,false, channelPool = new BoundedChannelPool<>(semaphore, HttpVersion.HTTP_1_1,
NODES, bootstrap, null, 0); NODES, bootstrap, null, 0, BoundedChannelPool.PoolKeySelectorType.ROUNDROBIN);
channelPool.prepare(CONCURRENCY); channelPool.prepare(CONCURRENCY);
} }
@ -98,7 +103,7 @@ public class NioTest {
Thread.sleep(1); Thread.sleep(1);
} }
channel.writeAndFlush(PAYLOAD.retain()).sync(); channel.writeAndFlush(PAYLOAD.retain()).sync();
channelPool.release(channel); channelPool.release(channel, false);
longAdder.increment(); longAdder.increment();
} catch (InterruptedException e) { } catch (InterruptedException e) {
break; break;
@ -127,4 +132,47 @@ public class NioTest {
logger.log(Level.WARNING, cause.getMessage(), cause); 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<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<Object>() {
@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();
}
}
} }

View file

@ -1,7 +1,12 @@
package org.xbib.netty.http.client.test.pool; package org.xbib.netty.http.client.test.pool;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel; 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.handler.codec.http.HttpVersion;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
import org.junit.Test; import org.junit.Test;
@ -45,20 +50,41 @@ public class PoolTest {
public static Collection<Object[]> generateData() { public static Collection<Object[]> generateData() {
return Arrays.asList(new Object[][] { return Arrays.asList(new Object[][] {
{1, 1}, {1, 1},
{10, 1}, {10, 2}, {10, 5}, {10, 10}, {10, 1},
{100, 1}, {100, 2}, {100, 5}, {100, 10}, {10, 2},
{1000, 1}, {1000, 2}, {1000, 5}, {1000, 10} //{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<Channel>() {
@Override
protected void initChannel(Channel ch) {
}
});
Channel serverChannel = serverBootstrap.bind("localhost", 8008).sync().channel();
this.nodeCount = nodeCount; this.nodeCount = nodeCount;
List<HttpAddress> nodes = new ArrayList<>(); List<HttpAddress> nodes = new ArrayList<>();
for (int i = 0; i < nodeCount; i ++) { for (int i = 0; i < nodeCount; i ++) {
nodes.add(HttpAddress.http1("localhost" + i)); nodes.add(HttpAddress.http1("localhost", 8008));
} }
try (Pool<Channel> pool = new BoundedChannelPool<>(new Semaphore(concurrencyLevel), HttpVersion.HTTP_1_1, false, try (Pool<Channel> pool = new BoundedChannelPool<>(new Semaphore(concurrencyLevel), HttpVersion.HTTP_1_1,
nodes, new Bootstrap(), null, 0)) { nodes, new Bootstrap().group(new NioEventLoopGroup()).channel(NioSocketChannel.class),
null, 0, BoundedChannelPool.PoolKeySelectorType.ROUNDROBIN)) {
int n = Runtime.getRuntime().availableProcessors(); int n = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(n); ExecutorService executorService = Executors.newFixedThreadPool(n);
for(int i = 0; i < n; i ++) { for(int i = 0; i < n; i ++) {
@ -80,7 +106,7 @@ public class PoolTest {
channels.add(channel); channels.add(channel);
} }
for (k = 0; k < j; k ++) { for (k = 0; k < j; k ++) {
pool.release(channels.get(k)); pool.release(channels.get(k), false);
} }
channels.clear(); channels.clear();
} }
@ -99,6 +125,7 @@ public class PoolTest {
} catch (Throwable t) { } catch (Throwable t) {
logger.log(Level.WARNING, t.getMessage(), t); logger.log(Level.WARNING, t.getMessage(), t);
} finally { } finally {
serverChannel.close();
long connCountSum = nodeFreq.values().stream().mapToLong(LongAdder::sum).sum(); long connCountSum = nodeFreq.values().stream().mapToLong(LongAdder::sum).sum();
logger.log(Level.INFO, "concurrency = " + concurrencyLevel + ", nodes = " + nodeCount + " -> rate: " + logger.log(Level.INFO, "concurrency = " + concurrencyLevel + ", nodes = " + nodeCount + " -> rate: " +
connCountSum / TEST_STEP_TIME_SECONDS); connCountSum / TEST_STEP_TIME_SECONDS);

View file

@ -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 io.netty.handler.codec.http.HttpVersion;
import org.junit.Test; import org.junit.Test;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.client.Client; 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.common.HttpAddress;
import org.xbib.netty.http.client.Request; 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.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class PooledClientTest extends LoggingBase { public class PooledClientTest extends TestBase {
private static final Logger logger = Logger.getLogger(""); private static final Logger logger = Logger.getLogger("");
@ -31,6 +33,11 @@ public class PooledClientTest extends LoggingBase {
.setPoolNodeConnectionLimit(threads) .setPoolNodeConnectionLimit(threads)
.build(); .build();
AtomicInteger count = new AtomicInteger(); 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 { try {
ExecutorService executorService = Executors.newFixedThreadPool(threads); ExecutorService executorService = Executors.newFixedThreadPool(threads);
for (int n = 0; n < threads; n++) { for (int n = 0; n < threads; n++) {
@ -38,17 +45,12 @@ public class PooledClientTest extends LoggingBase {
try { try {
logger.log(Level.INFO, "starting " + Thread.currentThread()); logger.log(Level.INFO, "starting " + Thread.currentThread());
for (int i = 0; i < loop; i++) { for (int i = 0; i < loop; i++) {
Request request = Request.get() Request request = Request.get().setVersion(httpAddress.getVersion())
.url(url.toString()) .url(url.toString())
.setVersion(httpAddress.getVersion())
//.setTimeoutInMillis(25000L) //.setTimeoutInMillis(25000L)
.build() .build()
.setResponseListener(fullHttpResponse -> { .setResponseListener(responseListener);
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); client.newTransport().execute(request).get();
//logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
count.getAndIncrement();
});
client.pooledExecute(request).get();
} }
logger.log(Level.INFO, "done " + Thread.currentThread()); logger.log(Level.INFO, "done " + Thread.currentThread());
} catch (Throwable e) { } catch (Throwable e) {

View file

@ -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.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
@ -28,6 +28,7 @@ import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName; import javax.net.ssl.SNIServerName;
@ -39,7 +40,8 @@ import java.util.concurrent.CompletableFuture;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class Http2FramesTest { @Ignore
public class Http2FramesTest extends TestBase {
private static final Logger logger = Logger.getLogger(Http2FramesTest.class.getName()); private static final Logger logger = Logger.getLogger(Http2FramesTest.class.getName());

View file

@ -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.bootstrap.Bootstrap;
import io.netty.channel.Channel; 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.handler.codec.http.HttpVersion;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
import org.junit.After; import org.junit.After;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -38,17 +40,14 @@ import java.util.logging.LogManager;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.logging.SimpleFormatter; import java.util.logging.SimpleFormatter;
/** @Ignore
* public class SimpleHttp1Test extends TestBase {
*/
public class SimpleHttp1Test {
private static final Logger logger = Logger.getLogger(SimpleHttp1Test.class.getName()); private static final Logger logger = Logger.getLogger(SimpleHttp1Test.class.getName());
static { static {
System.setProperty("io.netty.leakDetection.level", "paranoid"); System.setProperty("io.netty.leakDetection.level", "paranoid");
System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true));
System.setProperty("java.util.logging.SimpleFormatter.format", 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] %2$s %5$s %6$s%n");
LogManager.getLogManager().reset(); LogManager.getLogManager().reset();

View file

@ -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.bootstrap.Bootstrap;
import io.netty.channel.Channel; 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.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -53,9 +53,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
/** @Ignore
*/ public class SimpleHttp2Test extends TestBase {
public class SimpleHttp2Test {
private static final Logger logger = Logger.getLogger(SimpleHttp2Test.class.getName()); private static final Logger logger = Logger.getLogger(SimpleHttp2Test.class.getName());
@ -113,8 +112,6 @@ public class SimpleHttp2Test {
private final Initializer initializer; private final Initializer initializer;
private final List<Http2Transport> transports;
Client() { Client() {
eventLoopGroup = new NioEventLoopGroup(); eventLoopGroup = new NioEventLoopGroup();
http2SettingsHandler = new Http2SettingsHandler(); http2SettingsHandler = new Http2SettingsHandler();
@ -124,7 +121,6 @@ public class SimpleHttp2Test {
.group(eventLoopGroup) .group(eventLoopGroup)
.channel(NioSocketChannel.class) .channel(NioSocketChannel.class)
.handler(initializer); .handler(initializer);
transports = new ArrayList<>();
} }
Bootstrap bootstrap() { Bootstrap bootstrap() {
@ -137,23 +133,8 @@ public class SimpleHttp2Test {
Http2Transport newTransport(String host, int port) { Http2Transport newTransport(String host, int port) {
Http2Transport transport = new Http2Transport(this, new InetSocketAddress(host, port)); Http2Transport transport = new Http2Transport(this, new InetSocketAddress(host, port));
transports.add(transport);
return transport; return transport;
} }
List<Http2Transport> transports() {
return transports;
}
void close(Http2Transport transport) {
transports.remove(transport);
}
void close() {
for (Http2Transport transport : transports) {
transport.close();
}
}
} }
class Http2Transport { class Http2Transport {
@ -179,10 +160,6 @@ public class SimpleHttp2Test {
streamIdCounter = new AtomicInteger(3); streamIdCounter = new AtomicInteger(3);
} }
Client client() {
return client;
}
InetSocketAddress inetSocketAddress() { InetSocketAddress inetSocketAddress() {
return inetSocketAddress; return inetSocketAddress;
} }
@ -270,12 +247,6 @@ public class SimpleHttp2Test {
} }
} }
void complete() {
for (CompletableFuture<Boolean> promise : streamidPromiseMap.values()) {
promise.complete(true);
}
}
void fail(Throwable throwable) { void fail(Throwable throwable) {
for (CompletableFuture<Boolean> promise : streamidPromiseMap.values()) { for (CompletableFuture<Boolean> promise : streamidPromiseMap.values()) {
promise.completeExceptionally(throwable); promise.completeExceptionally(throwable);
@ -286,7 +257,6 @@ public class SimpleHttp2Test {
if (channel != null) { if (channel != null) {
channel.close(); channel.close();
} }
client.close(this);
} }
} }

View file

@ -39,10 +39,18 @@ public class HttpAddress implements PoolKey {
} }
public static HttpAddress http2(String host) { 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) { 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); return new HttpAddress(host, port, HTTP_2_0, true);
} }

View file

@ -22,9 +22,9 @@ import io.netty.util.DomainNameMapping;
import io.netty.util.DomainNameMappingBuilder; import io.netty.util.DomainNameMappingBuilder;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.context.VirtualServer; 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.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.Http2ServerTransport;
import org.xbib.netty.http.server.transport.ServerTransport; import org.xbib.netty.http.server.transport.ServerTransport;
import org.xbib.netty.http.server.util.NetworkUtils; import org.xbib.netty.http.server.util.NetworkUtils;
@ -49,7 +49,9 @@ public final class Server {
static { static {
// extend Java system properties by detected network interfaces // 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 // change Netty defaults to safer ones, but still allow override from arg line
if (System.getProperty("io.netty.noUnsafe") == null) { if (System.getProperty("io.netty.noUnsafe") == null) {
System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); System.setProperty("io.netty.noUnsafe", Boolean.toString(true));
@ -57,12 +59,6 @@ public final class Server {
if (System.getProperty("io.netty.noKeySetOptimization") == null) { if (System.getProperty("io.netty.noKeySetOptimization") == null) {
System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); 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; 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. * 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, public Server(ServerConfig serverConfig,
ByteBufAllocator byteBufAllocator, ByteBufAllocator byteBufAllocator,
@ -112,7 +114,6 @@ public final class Server {
.childOption(ChannelOption.SO_RCVBUF, serverConfig.getTcpReceiveBufferSize()) .childOption(ChannelOption.SO_RCVBUF, serverConfig.getTcpReceiveBufferSize())
.childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, serverConfig.getConnectTimeoutMillis()) .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, serverConfig.getConnectTimeoutMillis())
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverConfig.getWriteBufferWaterMark()); .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverConfig.getWriteBufferWaterMark());
if (serverConfig.isDebug()) { if (serverConfig.isDebug()) {
bootstrap.handler(new LoggingHandler("bootstrap-server", serverConfig.getDebugLogLevel())); bootstrap.handler(new LoggingHandler("bootstrap-server", serverConfig.getDebugLogLevel()));
} }
@ -178,6 +179,7 @@ public final class Server {
/** /**
* Start accepting incoming connections. * Start accepting incoming connections.
* @return the channel future
*/ */
public ChannelFuture accept() { public ChannelFuture accept() {
logger.log(Level.INFO, () -> "trying to bind to " + serverConfig.getAddress()); logger.log(Level.INFO, () -> "trying to bind to " + serverConfig.getAddress());
@ -198,7 +200,7 @@ public final class Server {
} }
public ServerTransport newTransport(HttpVersion httpVersion) { 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 { public synchronized void shutdownGracefully() throws IOException {

View file

@ -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<SocketChannel> {
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<SslContext> domainNameMapping;
public HttpServerChannelInitializer(Server server, ServerConfig serverConfig,
DomainNameMapping<SslContext> 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<Channel>() {
@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);
}
}
}

View file

@ -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.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;

View file

@ -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.buffer.ByteBuf;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;

View file

@ -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.ChannelInitializer;
import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel; 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.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpObjectAggregator; 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.HttpServerCodec;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.SniHandler; import io.netty.handler.ssl.SniHandler;
import io.netty.handler.ssl.SslContext; 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.common.HttpAddress;
import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.ServerConfig; 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.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -21,6 +31,8 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName()); private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName());
private final Server server;
private final ServerConfig serverConfig; private final ServerConfig serverConfig;
private final HttpAddress httpAddress; private final HttpAddress httpAddress;
@ -32,6 +44,7 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
public HttpChannelInitializer(Server server, public HttpChannelInitializer(Server server,
HttpAddress httpAddress, HttpAddress httpAddress,
DomainNameMapping<SslContext> domainNameMapping) { DomainNameMapping<SslContext> domainNameMapping) {
this.server = server;
this.serverConfig = server.getServerConfig(); this.serverConfig = server.getServerConfig();
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
this.domainNameMapping = domainNameMapping; this.domainNameMapping = domainNameMapping;
@ -40,6 +53,8 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override @Override
public void initChannel(SocketChannel channel) { public void initChannel(SocketChannel channel) {
ServerTransport serverTransport = server.newTransport(httpAddress.getVersion());
channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport);
if (serverConfig.isDebug()) { if (serverConfig.isDebug()) {
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
} }
@ -70,6 +85,46 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
false); false);
httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents()); httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents());
pipeline.addLast(httpObjectAggregator); pipeline.addLast(httpObjectAggregator);
pipeline.addLast(new HttpPipeliningHandler(1024));
pipeline.addLast(httpHandler); 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))));
}
}
} }

View file

@ -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;
}
}

View file

@ -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<HttpPipelinedResponse> {
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;
}
}

View file

@ -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<HttpPipelinedResponse> 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);
}
}
}

View file

@ -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))));
}
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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<FullHttpRequest> {
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();
}
}

View file

@ -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!".
*
* <p>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));
}
}

View file

@ -2,51 +2,34 @@ package org.xbib.netty.http.server.handler.http2;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPipeline;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler; import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler;
import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame;
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.Http2CodecUtil; 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.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.Http2MultiplexCodec;
import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; 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.Http2ServerUpgradeCodec;
import io.netty.handler.codec.http2.Http2Settings; 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.logging.LogLevel;
import io.netty.handler.ssl.SniHandler; import io.netty.handler.ssl.SniHandler;
import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContext;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import io.netty.util.DomainNameMapping; import io.netty.util.DomainNameMapping;
import io.netty.util.ReferenceCountUtil;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.ServerConfig; import org.xbib.netty.http.server.ServerConfig;
import org.xbib.netty.http.server.handler.http1.HttpHandler; import org.xbib.netty.http.server.handler.TrafficLoggingHandler;
import org.xbib.netty.http.server.handler.http1.TrafficLoggingHandler; import org.xbib.netty.http.server.transport.ServerTransport;
import java.io.IOException;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -71,13 +54,10 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
this.domainNameMapping = domainNameMapping; this.domainNameMapping = domainNameMapping;
} }
/**
* The channel initialization for HTTP/2.
*
* @param channel socket channel
*/
@Override @Override
public void initChannel(Channel channel) { public void initChannel(Channel channel) {
ServerTransport serverTransport = server.newTransport(httpAddress.getVersion());
channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport);
if (serverConfig.isDebug()) { if (serverConfig.isDebug()) {
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG)); channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
} }
@ -97,155 +77,71 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
} }
private void configureCleartext(Channel ch) { private void configureCleartext(Channel ch) {
Http2SettingsHandler http2SettingsHandler = new Http2SettingsHandler(); ChannelPipeline p = ch.pipeline();
Http2RequestHandler http2RequestHandler = new Http2RequestHandler(); Http2MultiplexCodecBuilder serverMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer<Channel>() {
//HttpHandler httpHandler = new HttpHandler(server);
ch.pipeline()
//.addLast(newConnectionHandler())
.addLast(upgradeHandler());
//.addLast(http2SettingsHandler)
//.addLast(http2RequestHandler);
// .addLast(sourceCodec)
/*final Http2MultiplexCodec http2Codec = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer<Channel>() {
@Override @Override
protected void initChannel(Channel ch) { protected void initChannel(Channel channel) {
logger.log(Level.INFO, "initChannel multiplex "); ServerTransport serverTransport = server.newTransport(httpAddress.getVersion());
} channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport);
}).build(); ChannelPipeline p = channel.pipeline();
HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> p.addLast("multiplex-server-frame-converter",
new Http2ServerUpgradeCodec(http2Codec); new Http2StreamFrameToHttpObjectCodec(true));
final HttpServerCodec serverCodec = new HttpServerCodec(); p.addLast("multiplex-server-chunk-aggregator",
ch.pipeline().addLast(serverCodec) new HttpObjectAggregator(serverConfig.getMaxContentLength()));
.addLast(new HttpServerUpgradeHandler(serverCodec, upgradeCodecFactory)) p.addLast("multiplex-server-request-handler",
.addLast(new SimpleChannelInboundHandler<HttpMessage>() { new ServerRequestHandler());
@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));
} }
}) })
.addLast(new UserEventLogger()) .initialSettings(Http2Settings.defaultSettings());
.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));
if (serverConfig.isDebug()) { if (serverConfig.isDebug()) {
builder.frameLogger(new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server")); serverMultiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "server"));
} }
return builder.build(); Http2MultiplexCodec serverMultiplexCodec = serverMultiplexCodecBuilder.build();
} HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> {
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)) { if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) {
return upgradeCodec(); return new Http2ServerUpgradeCodec("server-codec", serverMultiplexCodec);
} else { } else {
return null; return null;
} }
}; };
private Http2ServerUpgradeCodec upgradeCodec() {
return new Http2ServerUpgradeCodec(Http2MultiplexCodecBuilder.forServer(http2MultiplexCodec()).build());
}
private HttpServerUpgradeHandler upgradeHandler() {
HttpServerCodec sourceCodec = new HttpServerCodec(); 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() { class ServerRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
Http2FrameLogger frameLogger = new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server");
return Http2MultiplexCodecBuilder.forServer(new DummyHandler()) @Override
.frameLogger(frameLogger) protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException {
.initialSettings(serverConfig.getHttp2Settings()) ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
.build(); 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);
}
}
} }

View file

@ -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.
* <p>
* 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.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* 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.
* <p>
* 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.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* 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.
* <p>
* Promised requests MUST be authoritative, cacheable, and safe.
* See <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.2">[RFC http2], Section 8.2</a>.
* <p>
* 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.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* 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.
* <p>
* If a {@link RuntimeException} is thrown it will be logged and <strong>not propagated</strong>.
* 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
* <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @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 <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @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<AsciiString> translations = isRequest ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
try {
for (Map.Entry<CharSequence, CharSequence> 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<AsciiString>
REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
private static final CharSequenceMap<AsciiString>
RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
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());
}
}

View file

@ -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<FullHttpRequest> {
@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();
}
}

View file

@ -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<Http2Settings> {
@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);
}
}

View file

@ -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<Http2StreamFrame, HttpObject> {
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<Object> 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<Object> 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<Object> 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);
}
}

View file

@ -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);
}
}

View file

@ -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 {
}

View file

@ -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> 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<HttpObject, ChannelPromise> 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<HttpObject, ChannelPromise> 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<HttpObject, ChannelPromise> 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<Entry<HttpObject, ChannelPromise>> queue : pendingWrites.values()) {
while (true) {
final Entry<HttpObject, ChannelPromise> e = queue.poll();
if (e == null) {
break;
}
e.getValue().tryFailure(cause);
}
}
pendingWrites.clear();
}
private static final class PendingWrites extends ArrayDeque<Entry<HttpObject, ChannelPromise>> {
private boolean endOfStream;
PendingWrites() {
super(4);
}
boolean isEndOfStream() {
return endOfStream;
}
void setEndOfStream() {
endOfStream = true;
}
}
}

View file

@ -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<AsciiString> HTTP_TO_HTTP2_HEADER_BLACKLIST =
new CharSequenceMap<AsciiString>();
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<Map.Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
while (iter.hasNext()) {
Map.Entry<CharSequence, CharSequence> 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());
}
}
}
}
}

View file

@ -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());
}
}

View file

@ -1,4 +0,0 @@
/**
* Internal classes for Netty HTTP server.
*/
package org.xbib.netty.http.server.internal;

View file

@ -1,5 +1,6 @@
package org.xbib.netty.http.server.transport; 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.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
@ -14,9 +15,13 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
abstract class BaseServerTransport implements ServerTransport { abstract class BaseServerTransport implements ServerTransport {
private static final Logger logger = Logger.getLogger(BaseServerTransport.class.getName());
protected static final AtomicInteger requestCounter = new AtomicInteger(); protected static final AtomicInteger requestCounter = new AtomicInteger();
private static final List<String> METHODS = Arrays.asList("GET", "HEAD", "OPTIONS"); private static final List<String> METHODS = Arrays.asList("GET", "HEAD", "OPTIONS");
@ -27,6 +32,11 @@ abstract class BaseServerTransport implements ServerTransport {
this.server = server; 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 * Accepts a request, performing various validation checks
* and required special header handling, possibly returning an * 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 // "*" is a special server-wide (no-context) request supported by OPTIONS
boolean isServerOptions = path.equals("*") && method.equals("OPTIONS"); boolean isServerOptions = path.equals("*") && method.equals("OPTIONS");
methods.addAll(isServerOptions ? virtualServer.getMethods() : handlers.keySet()); 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 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); serverResponse.write(200);
} else if (virtualServer.getMethods().contains(method)) { } else if (virtualServer.getMethods().contains(method)) {
serverResponse.write(405); // supported by server, but not this context (nor built-in) serverResponse.write(405); // supported by server, but not this context (nor built-in)

View file

@ -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("<!DOCTYPE html>%n<html>%n<head><title>%d %s</title></head>%n" +
"<body><h1>%d %s</h1>%n<p>%s</p>%n</body></html>",
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 '&amp;', '&gt;' and '&lt;' 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 <a href="http://www.w3.org/International/questions/qa-escapes">The W3C FAQ</a>
*/
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 = "&amp;";
break;
case '>':
ref = "&gt;";
break;
case '<':
ref = "&lt;";
break;
case '"':
ref = "&quot;";
break;
case '\'':
ref = "&#39;";
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();
}
}

View file

@ -4,41 +4,42 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http2.Http2Settings; 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.common.HttpAddress;
import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.context.VirtualServer; import org.xbib.netty.http.server.context.VirtualServer;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Http2ServerTransport extends BaseServerTransport { public class Http2ServerTransport extends BaseServerTransport {
private static final Logger logger = Logger.getLogger(Http2ServerTransport.class.getName());
public Http2ServerTransport(Server server) { public Http2ServerTransport(Server server) {
super(server); super(server);
} }
@Override @Override
public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException { 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(); int requestId = requestCounter.incrementAndGet();
VirtualServer virtualServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); VirtualServer virtualServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST));
if (virtualServer == null) { if (virtualServer == null) {
virtualServer = server.getDefaultVirtualServer(); virtualServer = server.getDefaultVirtualServer();
} }
HttpAddress httpAddress = server.getServerConfig().getAddress(); HttpAddress httpAddress = server.getServerConfig().getAddress();
Integer streamId = fullHttpRequest.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
ServerRequest serverRequest = new ServerRequest(virtualServer, httpAddress, fullHttpRequest, ServerRequest serverRequest = new ServerRequest(virtualServer, httpAddress, fullHttpRequest,
null, requestId); sequenceId, streamId, requestId);
ServerResponse serverResponse = new Http1ServerResponse(httpAddress.getVersion(), serverRequest, ctx); ServerResponse serverResponse = new Http2ServerResponse(serverRequest, ctx);
if (acceptRequest(serverRequest, serverResponse)) { if (acceptRequest(serverRequest, serverResponse)) {
handle(serverRequest, serverResponse); handle(serverRequest, serverResponse);
} }
} }
@Override @Override
public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception { public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) {
logger.log(Level.INFO, "settings received");
} }
} }

View file

@ -12,17 +12,20 @@ import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion; 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.ServerName;
import org.xbib.netty.http.server.handler.http.HttpPipelinedResponse;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; 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; private final ServerRequest serverRequest;
@ -32,8 +35,7 @@ public class Http1ServerResponse implements ServerResponse {
private HttpHeaders trailingHeaders; private HttpHeaders trailingHeaders;
public Http1ServerResponse(HttpVersion httpVersion, ServerRequest serverRequest, ChannelHandlerContext ctx) { public HttpServerResponse(ServerRequest serverRequest, ChannelHandlerContext ctx) {
this.httpVersion = httpVersion;
this.serverRequest = serverRequest; this.serverRequest = serverRequest;
this.ctx = ctx; this.ctx = ctx;
this.headers = new DefaultHttpHeaders(); this.headers = new DefaultHttpHeaders();
@ -41,8 +43,8 @@ public class Http1ServerResponse implements ServerResponse {
} }
@Override @Override
public HttpHeaders getHeaders() { public void setHeader(AsciiString name, String value) {
return headers; headers.set(name, value);
} }
@Override @Override
@ -120,14 +122,22 @@ public class Http1ServerResponse implements ServerResponse {
headers.add(HttpHeaderNames.SERVER, ServerName.getServerName()); headers.add(HttpHeaderNames.SERVER, ServerName.getServerName());
} }
FullHttpResponse fullHttpResponse = byteBuf != null ? FullHttpResponse fullHttpResponse = byteBuf != null ?
new DefaultFullHttpResponse(httpVersion, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.valueOf(status), byteBuf, headers, trailingHeaders) : HttpResponseStatus.valueOf(status), byteBuf, headers, trailingHeaders) :
new DefaultFullHttpResponse(httpVersion, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.valueOf(status), Unpooled.EMPTY_BUFFER, headers, trailingHeaders); HttpResponseStatus.valueOf(status), Unpooled.EMPTY_BUFFER, headers, trailingHeaders);
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()) { if (ctx.channel().isWritable()) {
ctx.channel().writeAndFlush(fullHttpResponse); ctx.channel().writeAndFlush(fullHttpResponse);
} }
} }
}
/** /**
* Returns an HTML-escaped version of the given string for safe display * Returns an HTML-escaped version of the given string for safe display

View file

@ -10,14 +10,20 @@ import org.xbib.netty.http.server.context.VirtualServer;
import java.io.IOException; import java.io.IOException;
public class Http1ServerTransport extends BaseServerTransport { public class HttpServerTransport extends BaseServerTransport {
public Http1ServerTransport(Server server) { public HttpServerTransport(Server server) {
super(server); super(server);
} }
@Override @Override
public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException { 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(); int requestId = requestCounter.incrementAndGet();
VirtualServer virtualServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); VirtualServer virtualServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST));
if (virtualServer == null) { if (virtualServer == null) {
@ -25,15 +31,15 @@ public class Http1ServerTransport extends BaseServerTransport {
} }
HttpAddress httpAddress = server.getServerConfig().getAddress(); HttpAddress httpAddress = server.getServerConfig().getAddress();
ServerRequest serverRequest = new ServerRequest(virtualServer, httpAddress, fullHttpRequest, ServerRequest serverRequest = new ServerRequest(virtualServer, httpAddress, fullHttpRequest,
null, requestId); sequenceId, null, requestId);
ServerResponse serverResponse = new Http1ServerResponse(httpAddress.getVersion(), serverRequest, ctx); ServerResponse serverResponse = new HttpServerResponse(serverRequest, ctx);
if (acceptRequest(serverRequest, serverResponse)) { if (acceptRequest(serverRequest, serverResponse)) {
handle(serverRequest, serverResponse); handle(serverRequest, serverResponse);
} }
} }
@Override @Override
public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception { public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) {
// there are no settings in HTTP 1
} }
} }

View file

@ -15,15 +15,18 @@ public class ServerRequest {
private final FullHttpRequest httpRequest; private final FullHttpRequest httpRequest;
private final Integer sequenceId;
private final Integer streamId; private final Integer streamId;
private final Integer requestId; private final Integer requestId;
public ServerRequest(VirtualServer virtualServer, HttpAddress httpAddress, FullHttpRequest httpRequest, public ServerRequest(VirtualServer virtualServer, HttpAddress httpAddress,
Integer streamId, Integer requestId) { FullHttpRequest httpRequest, Integer sequenceId, Integer streamId, Integer requestId) {
this.virtualServer = virtualServer; this.virtualServer = virtualServer;
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
this.httpRequest = httpRequest; this.httpRequest = httpRequest;
this.sequenceId = sequenceId;
this.streamId = streamId; this.streamId = streamId;
this.requestId = requestId; this.requestId = requestId;
} }
@ -40,6 +43,10 @@ public class ServerRequest {
return httpRequest; return httpRequest;
} }
public Integer getSequenceId() {
return sequenceId;
}
public Integer streamId() { public Integer streamId() {
return streamId; return streamId;
} }

View file

@ -1,7 +1,7 @@
package org.xbib.netty.http.server.transport; package org.xbib.netty.http.server.transport;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.HttpHeaders; import io.netty.util.AsciiString;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -10,7 +10,7 @@ import java.nio.charset.Charset;
*/ */
public interface ServerResponse { public interface ServerResponse {
HttpHeaders getHeaders(); void setHeader(AsciiString name, String value);
void write(String text); void write(String text);

View file

@ -13,6 +13,9 @@ public interface ServerTransport {
void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException; 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 settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception;
void exceptionReceived(ChannelHandlerContext ctx, Throwable throwable) throws IOException;
} }

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.server.test; package org.xbib;
import java.util.logging.ConsoleHandler; import java.util.logging.ConsoleHandler;
import java.util.logging.Handler; import java.util.logging.Handler;
@ -7,20 +7,26 @@ import java.util.logging.LogManager;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.logging.SimpleFormatter; import java.util.logging.SimpleFormatter;
public class LoggingBase { public class TestBase {
static { 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", 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(); LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger(""); Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler(); Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter()); handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler); rootLogger.addHandler(handler);
rootLogger.setLevel(Level.ALL); rootLogger.setLevel(Level.FINE);
for (Handler h : rootLogger.getHandlers()) { for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter()); handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.ALL); h.setLevel(Level.FINE);
} }
} }
} }

View file

@ -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.Bootstrap;
import io.netty.bootstrap.ServerBootstrap; 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.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler; import io.netty.handler.logging.LoggingHandler;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger; 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 clientLogger = Logger.getLogger("client");
private static final Logger serverLogger = Logger.getLogger("server"); 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 serverFrameLogger = new Http2FrameLogger(logLevel, "server");
private static final Http2FrameLogger clientFrameLogger = new Http2FrameLogger(logLevel, "client"); private static final Http2FrameLogger clientFrameLogger = new Http2FrameLogger(logLevel, "client");
static { private CompletableFuture<ChannelHandlerContext> settingsPrefaceFuture;
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 private CompletableFuture<Boolean> completableFuture;
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<ChannelHandlerContext> settingsPrefaceFuture = new CompletableFuture<>();
private final CompletableFuture<Boolean> completableFuture = new CompletableFuture<>();
@Test @Test
public void testHttp2() throws Exception { public void testHttp2() throws Exception {
final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008);
settingsPrefaceFuture = new CompletableFuture<>();
completableFuture = new CompletableFuture<>();
EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup();
EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup();

View file

@ -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<String, CountDownLatch> 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<String> 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<HttpRequest> {
static final Queue<String> STRINGS = new LinkedTransferQueue<>();
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest request) {
STRINGS.add(request.uri());
}
}
private class WorkEmulatorHandler extends SimpleChannelInboundHandler<HttpPipelinedRequest> {
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);
}
}
}
}
}

View file

@ -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.Bootstrap;
import io.netty.bootstrap.ServerBootstrap; 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.LogLevel;
import io.netty.handler.logging.LoggingHandler; import io.netty.handler.logging.LoggingHandler;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger; 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 clientLogger = Logger.getLogger("client");
private static final Logger serverLogger = Logger.getLogger("server"); 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 InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8443);
private final CompletableFuture<Boolean> settingsPrefaceFuture = new CompletableFuture<>(); private final CompletableFuture<Boolean> settingsPrefaceFuture = new CompletableFuture<>();
@ -195,10 +173,11 @@ public class MultiplexCodecCleartextHttp2Test {
p.addLast("child-client-response-handler", new ClientResponseHandler()); p.addLast("child-client-response-handler", new ClientResponseHandler());
} }
}).open().syncUninterruptibly().getNow(); }).open().syncUninterruptibly().getNow();
Http2Headers request = new DefaultHttp2Headers().method(HttpMethod.GET.asciiName()) Http2Headers request = new DefaultHttp2Headers()
.method(HttpMethod.GET.asciiName())
.path("/foobar/0/0") .path("/foobar/0/0")
.scheme("http") .scheme("http")
.authority(inetSocketAddress.getHostName()); .authority(inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort());
childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(request, true)); childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(request, true));
clientLogger.log(Level.INFO, "waiting max. 10 seconds"); clientLogger.log(Level.INFO, "waiting max. 10 seconds");
responseFuture.get(10, TimeUnit.SECONDS); responseFuture.get(10, TimeUnit.SECONDS);

View file

@ -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.Bootstrap;
import io.netty.bootstrap.ServerBootstrap; 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.Http2Settings;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -35,56 +37,36 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; 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.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger; 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 clientLogger = Logger.getLogger("client");
private static final Logger serverLogger = Logger.getLogger("server"); private static final Logger serverLogger = Logger.getLogger("server");
private static final Level level = Level.FINE; private static final Level level = Level.FINE;
static { private InetSocketAddress inetSocketAddress;
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 private CompletableFuture<ChannelHandlerContext> settingsPrefaceFuture;
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 final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); private CompletableFuture<Boolean> responseFuture;
private final CompletableFuture<ChannelHandlerContext> settingsPrefaceFuture = new CompletableFuture<>(); private final int threads = 4;
private final CompletableFuture<Boolean> responseFuture = new CompletableFuture<>(); private final int requestsPerThread = 500;
private final int threads = 10;
private final int requestsPerThread = 100000;
private final AtomicInteger responseCounter = new AtomicInteger(); private final AtomicInteger responseCounter = new AtomicInteger();
@Test @Test
public void testMultiThreadedHttp2() throws Exception { public void testMultiThreadedHttp2() throws Exception {
inetSocketAddress = new InetSocketAddress("localhost", 8008);
settingsPrefaceFuture = new CompletableFuture<>();
responseFuture = new CompletableFuture<>();
EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup();
EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup();
@ -180,7 +162,7 @@ public class MultithreadedCleartextHttp2Test {
clientLogger.log(level, "waiting"); clientLogger.log(level, "waiting");
responseFuture.get(60, TimeUnit.SECONDS); responseFuture.get(60, TimeUnit.SECONDS);
if (responseFuture.isDone()) { if (responseFuture.isDone()) {
clientLogger.log(Level.INFO, "done"); clientLogger.log(Level.INFO, "stop");
} }
} finally { } finally {
@ -229,7 +211,7 @@ public class MultithreadedCleartextHttp2Test {
if (msg instanceof FullHttpRequest) { if (msg instanceof FullHttpRequest) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK); HttpResponseStatus.OK);
ctx.writeAndFlush(response); ctx.write(response);
} }
} }

View file

@ -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.Bootstrap;
import io.netty.bootstrap.ServerBootstrap; 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.Http2StreamChannelBootstrap;
import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -43,61 +45,41 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; 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.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/** /**
* *
* Multithreaded Http2MultiplexCodec demo for cleartext HTTP/2 between a server and a client. * 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 clientLogger = Logger.getLogger("client");
private static final Logger serverLogger = Logger.getLogger("server"); private static final Logger serverLogger = Logger.getLogger("server");
private Level level = Level.FINE; private Level level = Level.FINE;
static { private InetSocketAddress inetSocketAddress;
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 private CompletableFuture<Boolean> settingsPrefaceFuture;
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 final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); private CompletableFuture<Boolean> responseFuture;
private final CompletableFuture<Boolean> settingsPrefaceFuture = new CompletableFuture<>(); private final int threads = 4;
private final CompletableFuture<Boolean> responseFuture = new CompletableFuture<>(); private final int requestsPerThread = 500;
private final int threads = 10;
private final int requestsPerThread = 100000;
private final AtomicInteger responseCounter = new AtomicInteger(); private final AtomicInteger responseCounter = new AtomicInteger();
@Test @Test
public void testMultithreadedMultiplexHttp2() throws Exception { public void testMultithreadedMultiplexHttp2() throws Exception {
inetSocketAddress = new InetSocketAddress("localhost", 8008);
settingsPrefaceFuture = new CompletableFuture<>();
responseFuture = new CompletableFuture<>();
EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup();
EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup();
@ -193,8 +175,8 @@ public class MultithreadedMultiplexCodecCleartextHttp2Test {
childChannel.write(new DefaultHttp2HeadersFrame(request, true)); childChannel.write(new DefaultHttp2HeadersFrame(request, true));
//do not close child channel after write, a response is expected //do not close child channel after write, a response is expected
} }
});
clientChannel.flush(); clientChannel.flush();
});
} }
executorService.shutdown(); executorService.shutdown();
executorService.awaitTermination(60, TimeUnit.SECONDS); executorService.awaitTermination(60, TimeUnit.SECONDS);
@ -252,7 +234,7 @@ public class MultithreadedMultiplexCodecCleartextHttp2Test {
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) { protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) {
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK); HttpResponseStatus.OK);
ctx.writeAndFlush(response); ctx.write(response);
} }
} }

View file

@ -0,0 +1,4 @@
/**
* Hacking Netty for showing server functions.
*/
package org.xbib.netty.http.hacks;

View file

@ -1,61 +1,165 @@
package org.xbib.netty.http.server.test; package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.HttpVersion;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; 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.client.transport.Transport;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Server;
import java.nio.charset.StandardCharsets; 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.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; 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 @Test
public void testClearTextHttp1() throws Exception { public void testSimpleClearTextHttp1() throws Exception {
int loop = 1024; 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); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008);
Server server = Server.builder() Server server = Server.builder()
//.enableDebug() //.enableDebug()
.bind(httpAddress).build(); .bind(httpAddress).build();
server.getDefaultVirtualServer().addContext("/", (request, response) -> { 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(); server.accept();
Client httpClient = Client.builder() Client client = Client.builder()
//.enableDebug() //.enableDebug()
.addPoolNode(httpAddress)
.setPoolNodeConnectionLimit(2)
.build(); .build();
AtomicInteger counter = new AtomicInteger(); AtomicInteger counter = new AtomicInteger();
try { final ResponseListener responseListener = fullHttpResponse -> {
// 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); String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
//logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
counter.incrementAndGet(); counter.incrementAndGet();
}); };
Transport transport = httpClient.execute(request); try {
for (int i = 0; i < loop; i++) {
Request request = Request.get().setVersion("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()) { if (transport.isFailed()) {
logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure());
break; break;
} }
// each execution needs to be synchronized
transport.get(); transport.get();
} }
} finally { } finally {
httpClient.shutdownGracefully(); client.shutdownGracefully();
server.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());
} }
} }

View file

@ -1,73 +1,255 @@
package org.xbib.netty.http.server.test; 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.junit.Test;
import org.xbib.net.URL; import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; 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.client.transport.Transport;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Server;
import java.io.IOException;
import java.nio.charset.StandardCharsets; 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.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; 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()); private static final Logger logger = Logger.getLogger(CleartextHttp2Test.class.getName());
@Test @Test
public void testCleartextHttp2() throws Exception { public void testSimpleCleartextHttp2() throws Exception {
int loop = 1; HttpAddress httpAddress = HttpAddress.http2("localhost", 8008);
// 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() Server server = Server.builder()
.enableDebug()
.bind(httpAddress) .bind(httpAddress)
.build(); .build();
//server.logDiagnostics(Level.INFO);
server.getDefaultVirtualServer().addContext("/", (request, response) -> 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(); server.accept();
Client httpClient = Client.builder() Client client = Client.builder()
.enableDebug()
.setWriteBufferWaterMark(new WriteBufferWaterMark(low, high))
.build(); .build();
AtomicInteger counter = new AtomicInteger(); 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 { try {
URL serverURL = server.getServerConfig().getAddress().base(); String payload = Integer.toString(0) + "/" + Integer.toString(0);
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);
for (int i = 0; i < loop; i++) {
Request request = Request.get().setVersion("HTTP/2.0") Request request = Request.get().setVersion("HTTP/2.0")
.url(server.getServerConfig().getAddress().base()) .url(server.getServerConfig().getAddress().base())
.content(Integer.toString(i), "text/plain") .content(payload, "text/plain")
.build() .build()
.setResponseListener(fullHttpResponse -> { .setResponseListener(responseListener);
String content = fullHttpResponse.content().toString(StandardCharsets.UTF_8); Transport transport = client.newTransport(httpAddress);
logger.log(Level.INFO, fullHttpResponse.toString() + " content=" + content); 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(); counter.incrementAndGet();
}); };
// submit request 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); transport.execute(request);
if (transport.isFailed()) { if (transport.isFailed()) {
logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure());
break; break;
} }
} }
// wait for transport to complete
transport.get(); transport.get();
} finally { } finally {
httpClient.shutdownGracefully(); client.shutdownGracefully();
server.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());
} }
} }

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -1,53 +1,181 @@
package org.xbib.netty.http.server.test; package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.HttpVersion;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; 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.common.HttpAddress;
import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Server;
import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.Security; 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.Level;
import java.util.logging.Logger; 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 private static final Logger logger = Logger.getLogger(SecureHttp1Test.class.getName());
public void testSecureHttp1() throws Exception {
static {
if (Security.getProvider("BC") == null) { if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider()); 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() .setSelfCert()
.bind(HttpAddress.secureHttp1("localhost", 8143))
.build(); .build();
Client httpClient = Client.builder() Client client = Client.builder()
.setJdkSslProvider()
.trustInsecure() .trustInsecure()
.build(); .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 { try {
server.getDefaultVirtualServer().addContext("/", (request, response) -> server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write("Hello World")); response.write(200, "text/plain", request.getRequest().content().retain()));
server.accept(); server.accept();
httpClient.execute(Request.get().setVersion("HTTP/1.1") Request request = Request.get().setVersion(HttpVersion.HTTP_1_1)
.url(server.getServerConfig().getAddress().base()) .url(server.getServerConfig().getAddress().base())
.build() .build()
.setResponseListener(fullHttpResponse -> { .setResponseListener(responseListener);
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); client.execute(request).get();
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();
} finally { } finally {
httpClient.shutdownGracefully(); client.shutdownGracefully();
server.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());
} }
} }

View file

@ -1,65 +1,117 @@
package org.xbib.netty.http.server.test; 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.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Ignore;
import org.junit.Test; 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.Client;
import org.xbib.netty.http.client.Request; 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.client.transport.Transport;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Server;
import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.Security; 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.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; 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 private static final Logger logger = Logger.getLogger(SecureHttp2Test.class.getName());
public void testSecureHttp2() throws Exception {
// for self-signed certificate, we need Bouncycastle static {
if (Security.getProvider("BC") == null) { if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider()); 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() .setSelfCert()
.bind(httpAddress)
.build(); .build();
//server.logDiagnostics(Level.INFO);
server.getDefaultVirtualServer().addContext("/", (request, response) -> 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(); server.accept();
Client httpClient = Client.builder() Client client = Client.builder()
.setJdkSslProvider()
.trustInsecure() .trustInsecure()
.setWriteBufferWaterMark(new WriteBufferWaterMark(low, high))
.build(); .build();
AtomicInteger counter = new AtomicInteger(); 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 { try {
URL serverURL = server.getServerConfig().getAddress().base(); Transport transport = client.newTransport(httpAddress);
HttpVersion serverVersion = server.getServerConfig().getAddress().getVersion(); String payload = Integer.toString(0) + "/" + Integer.toString(0);
Transport transport = httpClient.newTransport(serverURL, serverVersion); Request request = Request.get()
.setVersion("HTTP/2.0")
.uri("/")
//.url(server.getServerConfig().getAddress().base())
.content(payload, "text/plain")
.build()
.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++) { for (int i = 0; i < loop; i++) {
String payload = Integer.toString(0) + "/" + Integer.toString(i);
Request request = Request.get().setVersion("HTTP/2.0") Request request = Request.get().setVersion("HTTP/2.0")
.url(server.getServerConfig().getAddress().base()) .url(server.getServerConfig().getAddress().base())
.content(Integer.toString(i), "text/plain") .content(payload, "text/plain")
.build() .build()
.setResponseListener(fullHttpResponse -> { .setResponseListener(responseListener);
String content = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
//logger.log(Level.INFO, fullHttpResponse.toString() + " content=" + content);
counter.incrementAndGet();
});
transport.execute(request); transport.execute(request);
if (transport.isFailed()) { if (transport.isFailed()) {
logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure());
@ -68,9 +120,76 @@ public class SecureHttp2Test extends LoggingBase {
} }
transport.get(); transport.get();
} finally { } finally {
httpClient.shutdownGracefully(); client.shutdownGracefully();
server.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());
} }
} }

View file

@ -7,8 +7,6 @@ import org.xbib.netty.http.server.security.tls.SelfSignedCertificate;
import java.security.Security; import java.security.Security;
import java.util.logging.Logger; import java.util.logging.Logger;
/**
*/
public class SelfSignedCertificateTest { public class SelfSignedCertificateTest {
@Test @Test

View file

@ -1,8 +1,10 @@
package org.xbib.netty.http.server.test; package org.xbib.netty.http.server.test;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.Server;
@Ignore
public class ServerTest { public class ServerTest {
@Test @Test

Some files were not shown because too many files have changed in this diff Show more