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:
parent
f2c483fcfa
commit
2339735966
101 changed files with 2980 additions and 3177 deletions
31
build.gradle
31
build.gradle
|
@ -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 {
|
spotbugs {
|
||||||
effort = "max"
|
toolVersion = '3.1.3'
|
||||||
reportLevel = "low"
|
sourceSets = [sourceSets.main]
|
||||||
//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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()));
|
|
@ -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;
|
|
@ -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
|
|
@ -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;
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* HTTP handlers for Netty HTTP client.
|
||||||
|
*/
|
||||||
|
package org.xbib.netty.http.client.handler.http;
|
|
@ -1,4 +0,0 @@
|
||||||
/**
|
|
||||||
* HTTP handlers for Netty HTTP client.
|
|
||||||
*/
|
|
||||||
package org.xbib.netty.http.client.handler.http1;
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,31 +92,116 @@ 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) {
|
||||||
|
@ -171,21 +209,13 @@ abstract class BaseTransport implements Transport {
|
||||||
} 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());
|
||||||
|
|
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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/")
|
||||||
|
|
|
@ -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("");
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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("");
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
|
@ -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());
|
||||||
|
|
|
@ -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();
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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))));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
/**
|
|
||||||
* Internal classes for Netty HTTP server.
|
|
||||||
*/
|
|
||||||
package org.xbib.netty.http.server.internal;
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 '&', '>' and '<' must always
|
||||||
|
* be escaped, and single and double quotes must be escaped within
|
||||||
|
* attribute values; this method escapes them always. This method can
|
||||||
|
* be used for generating both HTML and XHTML valid content.
|
||||||
|
*
|
||||||
|
* @param s the string to escape
|
||||||
|
* @return the escaped string
|
||||||
|
* @see <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 = "&";
|
||||||
|
break;
|
||||||
|
case '>':
|
||||||
|
ref = ">";
|
||||||
|
break;
|
||||||
|
case '<':
|
||||||
|
ref = "<";
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
ref = """;
|
||||||
|
break;
|
||||||
|
case '\'':
|
||||||
|
ref = "'";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ref != null) {
|
||||||
|
es.append(s.substring(start, i)).append(ref);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return start == 0 ? s : es.append(s.substring(start)).toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Hacking Netty for showing server functions.
|
||||||
|
*/
|
||||||
|
package org.xbib.netty.http.hacks;
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue