Skip to content

Commit

Permalink
Add HTTP/2 Netty tiles example
Browse files Browse the repository at this point in the history
Motivation:

Adding an example that showcases Netty’s HTTP/2 codec and that is
slightly more complex than the existing hello-world example. It is
based on the Gopher tiles example available here:
https://http2.golang.org/gophertiles?latency=0

Modifications:

Moved current http2 example to http2/helloworld.
Added http2 tiles example under http2/tiles.

Result:

A Netty tiles example is available.
  • Loading branch information
leogomes authored and Scottmitch committed May 18, 2015
1 parent c6d61f9 commit 781a855
Show file tree
Hide file tree
Showing 220 changed files with 756 additions and 16 deletions.
55 changes: 55 additions & 0 deletions example/src/main/java/io/netty/example/http2/Http2ExampleUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
*/
package io.netty.example.http2;

import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.QueryStringDecoder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
* Utility methods used by the example client and server.
*/
Expand All @@ -24,5 +33,51 @@ public final class Http2ExampleUtil {
*/
public static final String UPGRADE_RESPONSE_HEADER = "Http-To-Http2-Upgrade";

/**
* Size of the block to be read from the input stream.
*/
private static final int BLOCK_SIZE = 1024;

private Http2ExampleUtil() { }

/**
* @param string the string to be converted to an integer.
* @param defaultValue the default value
* @return the integer value of a string or the default value, if the string is either null or empty.
*/
public static int toInt(String string, int defaultValue) {
if (string != null && !string.isEmpty()) {
return Integer.valueOf(string);
}
return defaultValue;
}

/**
* Reads an InputStream into a byte array.
* @param input the InputStream.
* @return a byte array representation of the InputStream.
* @throws IOException if an I/O exception of some sort happens while reading the InputStream.
*/
public static ByteBuf toByteBuf(InputStream input) throws IOException {
ByteBuf buf = Unpooled.buffer();
int n = 0;
do {
n = buf.writeBytes(input, BLOCK_SIZE);
} while (n > 0);
return buf;
}

/**
* @param query the decoder of query string
* @param key the key to lookup
* @return the first occurrence of that key in the string parameters
*/
public static String firstValue(QueryStringDecoder query, String key) {
checkNotNull(query, "Query can't be null!");
List<String> values = query.parameters().get(key);
if (values == null || values.isEmpty()) {
return null;
}
return values.get(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2.client;
package io.netty.example.http2.helloworld.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2.client;
package io.netty.example.http2.helloworld.client;

import static io.netty.handler.logging.LogLevel.INFO;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2.client;
package io.netty.example.http2.helloworld.client;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2.client;
package io.netty.example.http2.helloworld.client;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.example.http2.server;
package io.netty.example.http2.helloworld.server;

import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* the License.
*/

package io.netty.example.http2.server;
package io.netty.example.http2.helloworld.server;

import static io.netty.buffer.Unpooled.copiedBuffer;
import static io.netty.buffer.Unpooled.unreleasableBuffer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2.server;
package io.netty.example.http2.helloworld.server;

import io.netty.channel.ChannelHandler;
import io.netty.handler.codec.http2.Http2ConnectionHandler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* under the License.
*/

package io.netty.example.http2.server;
package io.netty.example.http2.helloworld.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
Expand Down Expand Up @@ -71,12 +71,11 @@ public static void main(String[] args) throws Exception {
sslCtx = null;
}
// Configure the server.
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024);
b.group(bossGroup, workerGroup)
b.group(group)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new Http2ServerInitializer(sslCtx));
Expand All @@ -88,8 +87,7 @@ public static void main(String[] args) throws Exception {

ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
group.shutdownGracefully();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* under the License.
*/

package io.netty.example.http2.server;
package io.netty.example.http2.helloworld.server;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2015 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package io.netty.example.http2.tiles;

import static io.netty.buffer.Unpooled.copiedBuffer;
import static io.netty.buffer.Unpooled.unmodifiableBuffer;
import static io.netty.buffer.Unpooled.unreleasableBuffer;
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.CharsetUtil.UTF_8;
import io.netty.buffer.ByteBuf;
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.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http2.Http2CodecUtil;

/**
* Handles the exceptional case where HTTP 1.x was negotiated under TLS.
*/
public final class FallbackRequestHandler extends SimpleChannelInboundHandler<HttpRequest> {

private static final ByteBuf response = unmodifiableBuffer(unreleasableBuffer(copiedBuffer("<!DOCTYPE html>"
+ "<html><body><h2>To view the example you need a browser that supports HTTP/2 ("
+ Http2CodecUtil.TLS_UPGRADE_PROTOCOL_NAME
+ ")</h2></body></html>", UTF_8)));

@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
if (HttpHeaderUtil.is100ContinueExpected(req)) {
ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
}

ByteBuf content = ctx.alloc().buffer();
content.writeBytes(response.duplicate());

FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());

ctx.write(response).addListener(ChannelFutureListener.CLOSE);
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
79 changes: 79 additions & 0 deletions example/src/main/java/io/netty/example/http2/tiles/Html.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2015 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package io.netty.example.http2.tiles;

import static io.netty.util.CharsetUtil.UTF_8;

import java.util.Random;

/**
* Static and dynamically generated HTML for the example.
*/
public final class Html {

public static final String IP = System.getProperty("ip", "127.0.0.1");

public static final byte[] FOOTER = "</body></html>".getBytes(UTF_8);

public static final byte[] HEADER = ("<!DOCTYPE html><html><head lang=\"en\"><title>Netty HTTP/2 Example</title>"
+ "<style>body {background:#DDD;} div#netty { line-height:0;}</style>"
+ "<link rel=\"shortcut icon\" href=\"about:blank\">"
+ "<meta charset=\"UTF-8\"></head><body>A grid of 200 tiled images is shown below. Compare:"
+ "<p>[<a href='https://" + url(Http2Server.PORT) + "?latency=0'>HTTP/2, 0 latency</a>] [<a href='http://"
+ url(HttpServer.PORT) + "?latency=0'>HTTP/1, 0 latency</a>]<br/>" + "[<a href='https://"
+ url(Http2Server.PORT) + "?latency=30'>HTTP/2, 30ms latency</a>] [<a href='http://" + url(HttpServer.PORT)
+ "?latency=30'>HTTP/1, 30ms latency</a>]<br/>" + "[<a href='https://" + url(Http2Server.PORT)
+ "?latency=200'>HTTP/2, 200ms latency</a>] [<a href='http://" + url(HttpServer.PORT)
+ "?latency=200'>HTTP/1, 200ms latency</a>]<br/>" + "[<a href='https://" + url(Http2Server.PORT)
+ "?latency=1000'>HTTP/2, 1s latency</a>] [<a href='http://" + url(HttpServer.PORT)
+ "?latency=1000'>HTTP/1, " + "1s latency</a>]<br/>").getBytes(UTF_8);

private static final int IMAGES_X_AXIS = 20;

private static final int IMAGES_Y_AXIS = 10;

private Html() {
}

private static String url(int port) {
return IP + ":" + port + "/http2";
}

public static byte[] body(int latency) {
int r = Math.abs(new Random().nextInt());
// The string to be built contains 13192 fixed characters plus the variable latency and random cache-bust.
int numberOfCharacters = 13192 + stringLength(latency) + stringLength(r);
StringBuilder sb = new StringBuilder(numberOfCharacters).append("<div id=\"netty\">");
for (int y = 0; y < IMAGES_Y_AXIS; y++) {
for (int x = 0; x < IMAGES_X_AXIS; x++) {
sb.append("<img width=30 height=29 src='/http2?x=")
.append(x)
.append("&y=").append(y)
.append("&cachebust=").append(r)
.append("&latency=").append(latency)
.append("'>");
}
sb.append("<br/>\r\n");
}
sb.append("</div>");
return sb.toString().getBytes(UTF_8);
}

private static int stringLength(int value) {
return Integer.toString(value).length() * IMAGES_X_AXIS * IMAGES_Y_AXIS;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2015 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package io.netty.example.http2.tiles;

import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderUtil.isKeepAlive;
import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
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.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaderValues;

import java.util.concurrent.TimeUnit;

/**
* Handles the requests for the tiled image using HTTP 1.x as a protocol.
*/
public final class Http1RequestHandler extends Http2RequestHandler {

@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (HttpHeaderUtil.is100ContinueExpected(request)) {
ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
}
super.channelRead0(ctx, request);
}

@Override
protected void sendResponse(final ChannelHandlerContext ctx, String streamId, int latency,
final FullHttpResponse response, final FullHttpRequest request) {
HttpHeaderUtil.setContentLength(response, response.content().readableBytes());
ctx.executor().schedule(new Runnable() {
@Override
public void run() {
if (isKeepAlive(request)) {
response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.writeAndFlush(response);
} else {
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
}, latency, TimeUnit.MILLISECONDS);
}
}
Loading

0 comments on commit 781a855

Please sign in to comment.