Skip to content

Commit

Permalink
Netty HTTP/2 ref #418
Browse files Browse the repository at this point in the history
  • Loading branch information
jknack committed Sep 4, 2016
1 parent 12328a3 commit 8779643
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 52 deletions.
58 changes: 57 additions & 1 deletion jooby-netty/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@
<artifactId>netty-codec-http</artifactId>
</dependency>

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http2</artifactId>
<version>${netty.version}</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty.npn</groupId>
<artifactId>npn-api</artifactId>
<version>8.1.2.v20120308</version>
</dependency>

<dependency>
<groupId>org.mortbay.jetty.alpn</groupId>
<artifactId>alpn-boot</artifactId>
<version>8.1.9.v20160720</version>
</dependency>

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
Expand Down Expand Up @@ -118,7 +136,7 @@

<profiles>
<profile>
<id>linux-epoll</id>
<id>linux</id>
<activation>
<os>
<family>linux</family>
Expand All @@ -131,6 +149,44 @@
<version>${netty.version}</version>
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>1.1.33.Fork19</version>
<classifier>linux-x86_64</classifier>
</dependency>
</dependencies>
</profile>
<profile>
<id>windows_x86</id>
<activation>
<os>
<family>windows</family>
</os>
</activation>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>1.1.33.Fork19</version>
<classifier>windows-x86_64</classifier>
</dependency>
</dependencies>
</profile>
<profile>
<id>mac_x86_64</id>
<activation>
<os>
<family>mac</family>
</os>
</activation>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>1.1.33.Fork19</version>
<classifier>osx-x86_64</classifier>
</dependency>
</dependencies>
</profile>
</profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
Expand Down Expand Up @@ -82,9 +83,10 @@ public void channelRead0(final ChannelHandlerContext ctx, final Object msg) {
boolean keepAlive = HttpUtil.isKeepAlive(req);

try {
String streamId = req.headers().get(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
handler.handle(
new NettyRequest(ctx, req, tmpdir, wsMaxMessageSize),
new NettyResponse(ctx, bufferSize, keepAlive));
new NettyResponse(ctx, bufferSize, keepAlive, streamId));
} catch (Throwable ex) {
exceptionCaught(ctx, ex);
}
Expand Down Expand Up @@ -113,7 +115,8 @@ public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cau
if (ws != null && ws.get() != null) {
ws.get().handle(cause);
} else {
log.error("execution of: " + ctx.channel().attr(PATH).get() + " resulted in error", cause);
log.debug("execution of: " + ctx.channel().attr(PATH).get() + " resulted in error",
cause);
}
}
} finally {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.jooby.internal.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
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.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;

public class NettyHttpAPN extends ApplicationProtocolNegotiationHandler {

private NettyInitializer initializer;

public NettyHttpAPN(final NettyInitializer initializer) {
super(ApplicationProtocolNames.HTTP_1_1);
this.initializer = initializer;
}

@Override
protected void configurePipeline(final ChannelHandlerContext ctx, final String protocol)
throws Exception {

if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
DefaultHttp2Connection connection = new DefaultHttp2Connection(true);
InboundHttp2ToHttpAdapter listener = new InboundHttp2ToHttpAdapterBuilder(connection)
.propagateSettings(false)
.validateHttpHeaders(false)
.maxContentLength(initializer.maxContentLength)
.build();

ChannelPipeline pipeline = ctx.pipeline();
pipeline.addLast(new HttpToHttp2ConnectionHandlerBuilder()
.frameListener(listener)
.connection(connection).build());
initializer.pipeline(pipeline, false);
} else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
initializer.pipeline(ctx.pipeline(), true);
} else {
throw new IllegalStateException("Unknown protocol: " + protocol);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@ public class NettyInitializer extends ChannelInitializer<SocketChannel> {

private int maxChunkSize;

private int maxContentLength;
int maxContentLength;

private long idleTimeOut;

private SslContext sslCtx;

private boolean http2;

public NettyInitializer(final EventExecutorGroup executor, final HttpHandler handler,
final Config config, final SslContext sslCtx) {
final Config config, final SslContext sslCtx, final boolean http2) {
this.executor = executor;
this.handler = handler;
this.config = config;
Expand All @@ -66,28 +68,38 @@ public NettyInitializer(final EventExecutorGroup executor, final HttpHandler han
maxContentLength = config.getBytes("netty.http.MaxContentLength").intValue();
idleTimeOut = config.getDuration("netty.http.IdleTimeout", TimeUnit.MILLISECONDS);
this.sslCtx = sslCtx;
this.http2 = http2;
}

@Override
protected void initChannel(final SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();

if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
if (http2) {
ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new NettyHttpAPN(this));
} else {
if (sslCtx != null) {
ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()));
}
pipeline(ch.pipeline(), true);
}
}

public void pipeline(final ChannelPipeline pipeline, final boolean http1) {

if (http1) {
pipeline
.addLast("decoder",
new HttpRequestDecoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, false))
.addLast("encoder", new HttpResponseEncoder());

pipeline
.addLast("decoder",
new HttpRequestDecoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, false))
.addLast("encoder", new HttpResponseEncoder());
if (idleTimeOut > 0) {
pipeline.addLast("timeout", new IdleStateHandler(0, 0, idleTimeOut, TimeUnit.MILLISECONDS));
}

if (idleTimeOut > 0) {
pipeline.addLast("timeout", new IdleStateHandler(0, 0, idleTimeOut, TimeUnit.MILLISECONDS));
pipeline
.addLast("aggregator", new HttpObjectAggregator(maxContentLength));
}

pipeline
.addLast("aggregator", new HttpObjectAggregator(maxContentLength))
.addLast(executor, "handler", new NettyHandler(handler, config));
pipeline.addLast(executor, "handler", new NettyHandler(handler, config));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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.http2.HttpConversionUtil;
import io.netty.handler.stream.ChunkedStream;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.Attribute;
Expand All @@ -67,10 +68,18 @@ public class NettyResponse implements NativeResponse {

public NettyResponse(final ChannelHandlerContext ctx, final int bufferSize,
final boolean keepAlive) {
this(ctx, bufferSize, keepAlive, null);
}

public NettyResponse(final ChannelHandlerContext ctx, final int bufferSize,
final boolean keepAlive, final String streamId) {
this.ctx = ctx;
this.bufferSize = bufferSize;
this.keepAlive = keepAlive;
this.headers = new DefaultHttpHeaders();
if (streamId != null) {
headers.set(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId);
}
this.status = HttpResponseStatus.OK;
}

Expand Down
61 changes: 45 additions & 16 deletions jooby-netty/src/main/java/org/jooby/internal/netty/NettyServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,19 @@
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.util.ResourceLeakDetector;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.DefaultThreadFactory;
Expand All @@ -81,56 +90,60 @@ public class NettyServer implements Server {

private Channel ch;

private Config config;
private Config conf;

private HttpHandler dispatcher;

@Inject
public NettyServer(final HttpHandler dispatcher, final Config config) {
this.dispatcher = dispatcher;
this.config = config;
this.conf = config;
}

@Override
public void start() throws Exception {
int parentThreads = config.getInt("netty.threads.Parent");
int parentThreads = conf.getInt("netty.threads.Parent");
bossLoop = eventLoop(parentThreads, "parent");
if (config.hasPath("netty.threads.Child")) {
int childThreads = config.getInt("netty.threads.Child");
if (conf.hasPath("netty.threads.Child")) {
int childThreads = conf.getInt("netty.threads.Child");
workerLoop = eventLoop(childThreads, "child");
} else {
workerLoop = bossLoop;
}

ThreadFactory threadFactory = new DefaultThreadFactory(config.getString("netty.threads.Name"));
ThreadFactory threadFactory = new DefaultThreadFactory(conf.getString("netty.threads.Name"));
DefaultEventExecutorGroup executor = new DefaultEventExecutorGroup(
config.getInt("netty.threads.Max"), threadFactory);
conf.getInt("netty.threads.Max"), threadFactory);

this.ch = bootstrap(executor, null, config.getInt("application.port"));
boolean http2 = conf.getBoolean("server.http2.enabled");

if (config.hasPath("application.securePort")) {
bootstrap(executor, sslCtx(config), config.getInt("application.securePort"));
this.ch = bootstrap(executor, null, conf.getInt("application.port"), false);

boolean securePort = conf.hasPath("application.securePort");

if (securePort) {
bootstrap(executor, sslCtx(conf, http2), conf.getInt("application.securePort"), http2);
}
}

private Channel bootstrap(final EventExecutorGroup executor, final SslContext sslCtx,
final int port) throws InterruptedException {
final int port, final boolean http2) throws InterruptedException {
ServerBootstrap bootstrap = new ServerBootstrap();

boolean epoll = bossLoop instanceof EpollEventLoopGroup;
bootstrap.group(bossLoop, workerLoop)
.channel(epoll ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
.handler(new LoggingHandler(Server.class, LogLevel.DEBUG))
.childHandler(new NettyInitializer(executor, dispatcher, config, sslCtx));
.childHandler(new NettyInitializer(executor, dispatcher, conf, sslCtx, http2));

configure(config.getConfig("netty.options"), "netty.options",
configure(conf.getConfig("netty.options"), "netty.options",
(option, value) -> bootstrap.option(option, value));

configure(config.getConfig("netty.child.options"), "netty.child.options",
configure(conf.getConfig("netty.child.options"), "netty.child.options",
(option, value) -> bootstrap.childOption(option, value));

return bootstrap
.bind(host(config.getString("application.host")), port)
.bind(host(conf.getString("application.host")), port)
.sync()
.channel();
}
Expand Down Expand Up @@ -196,7 +209,8 @@ private EventLoopGroup eventLoop(final int threads, final String name) {
return new NioEventLoopGroup(threads, threadFactory);
}

private SslContext sslCtx(final Config config) throws IOException, CertificateException {
private SslContext sslCtx(final Config config, final boolean http2)
throws IOException, CertificateException {
String tmpdir = config.getString("application.tmpdir");
File keyStoreCert = toFile(config.getString("ssl.keystore.cert"), tmpdir);
File keyStoreKey = toFile(config.getString("ssl.keystore.key"), tmpdir);
Expand All @@ -206,6 +220,21 @@ private SslContext sslCtx(final Config config) throws IOException, CertificateEx
if (config.hasPath("ssl.trust.cert")) {
scb.trustManager(toFile(config.getString("ssl.trust.cert"), tmpdir));
}
if (http2) {
SslProvider provider = OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK;
return scb.sslProvider(provider)
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
Protocol.ALPN,
// NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK
// providers.
SelectorFailureBehavior.NO_ADVERTISE,
// ACCEPT is currently the only mode supported by both OpenSsl and JDK providers.
SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1))
.build();
}
return scb.build();
}

Expand Down
18 changes: 18 additions & 0 deletions jooby-netty/src/test/java/netty/H2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package netty;

import org.jooby.Jooby;

public class H2 extends Jooby {

{
http2();
securePort(8443);

assets("/assets/**");
assets("/", "index.html");
}

public static void main(final String[] args) throws Throwable {
run(H2::new, args);
}
}
Loading

0 comments on commit 8779643

Please sign in to comment.