From cbf52961f2ef6ae994e27d4663100f11af65b7a3 Mon Sep 17 00:00:00 2001 From: Matthias Einwag Date: Wed, 1 Oct 2014 22:02:22 +0200 Subject: [PATCH] Adding a benchmark for websockets Motivation: It is often helpful to measure the performance of connections, e.g. the latency and the throughput. This can be performed through benchmarks. Modification: This adds a simple but configurable benchmark for websockets into the example directory. The Netty WebSocket server will echo all received websocket frames and will provide an HTML/JS page which serves as the client for the benchmark. The benchmark also provides a verification mode that verifies the sent against the received data. This can be used for the verification ob websocket frame encoding and decoding funtionality. Result: A benchmark is added in form a further Netty websocket example. With this benchmark it is easily possible to measure the performance between Netty and a browser --- .../benchmarkserver/WebSocketServer.java | 70 +++++++ .../WebSocketServerBenchmarkPage.java | 191 ++++++++++++++++++ .../WebSocketServerHandler.java | 161 +++++++++++++++ .../WebSocketServerInitializer.java | 47 +++++ .../benchmarkserver/package-info.java | 27 +++ .../websocketx/server/WebSocketServer.java | 2 +- 6 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServer.java create mode 100644 example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerBenchmarkPage.java create mode 100644 example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java create mode 100644 example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerInitializer.java create mode 100644 example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/package-info.java diff --git a/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServer.java b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServer.java new file mode 100644 index 0000000000..73fee73497 --- /dev/null +++ b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2014 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.http.websocketx.benchmarkserver; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.util.SelfSignedCertificate; + +/** + * A Benchmark application for websocket which is served at: + * + * http://localhost:8080/websocket + * + * Open your browser at http://localhost:8080/, then the benchmark page will be loaded and a Web Socket connection will + * be made automatically. + */ +public final class WebSocketServer { + + static final boolean SSL = System.getProperty("ssl") != null; + static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8080")); + + public static void main(String[] args) throws Exception { + // Configure SSL. + final SslContext sslCtx; + if (SSL) { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + sslCtx = SslContext.newServerContext(ssc.certificate(), ssc.privateKey()); + } else { + sslCtx = null; + } + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new WebSocketServerInitializer(sslCtx)); + + Channel ch = b.bind(PORT).sync().channel(); + + System.out.println("Open your web browser and navigate to " + + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); + + ch.closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerBenchmarkPage.java b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerBenchmarkPage.java new file mode 100644 index 0000000000..b77a65c5ea --- /dev/null +++ b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerBenchmarkPage.java @@ -0,0 +1,191 @@ +/* + * Copyright 2014 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.http.websocketx.benchmarkserver; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; + +/** + * Generates the benchmark HTML page which is served at http://localhost:8080/ + */ +public final class WebSocketServerBenchmarkPage { + + private static final String NEWLINE = "\r\n"; + + public static ByteBuf getContent(String webSocketLocation) { + return Unpooled.copiedBuffer( + "Web Socket Performance Test" + NEWLINE + + "" + NEWLINE + + "

WebSocket Performance Test

" + NEWLINE + + "" + NEWLINE + + "
" + NEWLINE + + + "
" + NEWLINE + + "Message size:" + + "
" + NEWLINE + + "Number of messages:" + + "
" + NEWLINE + + "Data Type:" + + "text" + + "binary
" + NEWLINE + + "Mode:
" + NEWLINE + + "" + + "Wait for response after each messages
" + NEWLINE + + "" + + "Send all messages and then wait for all responses
" + NEWLINE + + "Verify responded messages
" + NEWLINE + + "" + NEWLINE + + "

Output

" + NEWLINE + + "" + NEWLINE + + "
" + NEWLINE + + "" + NEWLINE + + "
" + NEWLINE + + + "" + NEWLINE + + "" + NEWLINE + + "" + NEWLINE, CharsetUtil.US_ASCII); + } + + private WebSocketServerBenchmarkPage() { + // Unused + } +} diff --git a/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java new file mode 100644 index 0000000000..374203a77b --- /dev/null +++ b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java @@ -0,0 +1,161 @@ +/* + * Copyright 2014 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.http.websocketx.benchmarkserver; + +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.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.HttpHeaderUtil; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; +import io.netty.util.CharsetUtil; + +import static io.netty.handler.codec.http.HttpHeaders.Names.*; +import static io.netty.handler.codec.http.HttpMethod.*; +import static io.netty.handler.codec.http.HttpResponseStatus.*; +import static io.netty.handler.codec.http.HttpVersion.*; + +/** + * Handles handshakes and messages + */ +public class WebSocketServerHandler extends SimpleChannelInboundHandler { + + private static final String WEBSOCKET_PATH = "/websocket"; + + private WebSocketServerHandshaker handshaker; + + @Override + public void messageReceived(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof FullHttpRequest) { + handleHttpRequest(ctx, (FullHttpRequest) msg); + } else if (msg instanceof WebSocketFrame) { + handleWebSocketFrame(ctx, (WebSocketFrame) msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) { + // Handle a bad request. + if (!req.decoderResult().isSuccess()) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST)); + return; + } + + // Allow only GET methods. + if (req.method() != GET) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN)); + return; + } + + // Send the demo page and favicon.ico + if ("/".equals(req.uri())) { + ByteBuf content = WebSocketServerBenchmarkPage.getContent(getWebSocketLocation(req)); + FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content); + + res.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); + HttpHeaderUtil.setContentLength(res, content.readableBytes()); + + sendHttpResponse(ctx, req, res); + return; + } + if ("/favicon.ico".equals(req.uri())) { + FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND); + sendHttpResponse(ctx, req, res); + return; + } + + // Handshake + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + getWebSocketLocation(req), null, true, 5 * 1024 * 1024); + handshaker = wsFactory.newHandshaker(req); + if (handshaker == null) { + WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); + } else { + handshaker.handshake(ctx.channel(), req); + } + } + + private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { + + // Check for closing frame + if (frame instanceof CloseWebSocketFrame) { + handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); + return; + } + if (frame instanceof PingWebSocketFrame) { + ctx.write(new PongWebSocketFrame(frame.content().retain())); + return; + } + if (frame instanceof TextWebSocketFrame) { + // Echo the frame + ctx.write(frame.retain()); + return; + } + if (frame instanceof BinaryWebSocketFrame) { + // Echo the frame + ctx.write(frame.retain()); + return; + } + } + + private static void sendHttpResponse( + ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { + // Generate an error page if response getStatus code is not OK (200). + if (res.status().code() != 200) { + ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8); + res.content().writeBytes(buf); + buf.release(); + HttpHeaderUtil.setContentLength(res, res.content().readableBytes()); + } + + // Send the response and close the connection if necessary. + ChannelFuture f = ctx.channel().writeAndFlush(res); + if (!HttpHeaderUtil.isKeepAlive(req) || res.status().code() != 200) { + f.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } + + private static String getWebSocketLocation(FullHttpRequest req) { + String location = req.headers().get(HOST) + WEBSOCKET_PATH; + if (WebSocketServer.SSL) { + return "wss://" + location; + } else { + return "ws://" + location; + } + } +} diff --git a/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerInitializer.java b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerInitializer.java new file mode 100644 index 0000000000..1647c29859 --- /dev/null +++ b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerInitializer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 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.http.websocketx.benchmarkserver; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; +import io.netty.handler.ssl.SslContext; + +/** + */ +public class WebSocketServerInitializer extends ChannelInitializer { + + private final SslContext sslCtx; + + public WebSocketServerInitializer(SslContext sslCtx) { + this.sslCtx = sslCtx; + } + + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + if (sslCtx != null) { + pipeline.addLast(sslCtx.newHandler(ch.alloc())); + } + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(65536)); + // pipeline.addLast(new WebSocketServerCompressionHandler()); + pipeline.addLast(new WebSocketServerHandler()); + } +} diff --git a/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/package-info.java b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/package-info.java new file mode 100644 index 0000000000..71fec7e3e7 --- /dev/null +++ b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright 2014 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. + */ + +/** + *

This package contains a benchmark application for websockets. + *

The websocket server will accept websocket upgrade requests and simply echo + * all incoming frames. + *

The supplied index page contains a benchmark client that runs in a browser. + *

Once started, you can start benchmarking by by navigating + * to http://localhost:8080/ with your browser. + *

You can also test it with a web socket client. Send web socket traffic to + * ws://localhost:8080/websocket. + */ +package io.netty.example.http.websocketx.benchmarkserver; diff --git a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java index 81e5265443..199a08149a 100644 --- a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java +++ b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java @@ -70,7 +70,7 @@ public final class WebSocketServer { Channel ch = b.bind(PORT).sync().channel(); - System.err.println("Open your web browser and navigate to " + + System.out.println("Open your web browser and navigate to " + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); ch.closeFuture().sync();