From 20d2fb8c2ef375579aac30c2086ae0c51aa0e905 Mon Sep 17 00:00:00 2001 From: Leonardo Freitas Gomes Date: Fri, 24 Jan 2014 01:25:46 +0100 Subject: [PATCH] SPDY client example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demonstrates the usage of SPDY from a client perspective. One can also use a SPDY-enabled browser as a client, but it’s easier to understand the internals of the protocol from a client point-of-view if you have some code you can debug. --- .../spdyclient/HttpResponseClientHandler.java | 88 ++++++++++++ .../netty/example/spdyclient/SpdyClient.java | 135 ++++++++++++++++++ .../spdyclient/SpdyClientInitializer.java | 64 +++++++++ .../spdyclient/SpdyClientProvider.java | 59 ++++++++ .../spdyclient/SpdyClientStreamIdHandler.java | 47 ++++++ .../example/spdyclient/SpdyFrameLogger.java | 75 ++++++++++ .../example/spdyclient/package-info.java | 36 +++++ 7 files changed, 504 insertions(+) create mode 100644 example/src/main/java/io/netty/example/spdyclient/HttpResponseClientHandler.java create mode 100644 example/src/main/java/io/netty/example/spdyclient/SpdyClient.java create mode 100644 example/src/main/java/io/netty/example/spdyclient/SpdyClientInitializer.java create mode 100644 example/src/main/java/io/netty/example/spdyclient/SpdyClientProvider.java create mode 100644 example/src/main/java/io/netty/example/spdyclient/SpdyClientStreamIdHandler.java create mode 100644 example/src/main/java/io/netty/example/spdyclient/SpdyFrameLogger.java create mode 100644 example/src/main/java/io/netty/example/spdyclient/package-info.java diff --git a/example/src/main/java/io/netty/example/spdyclient/HttpResponseClientHandler.java b/example/src/main/java/io/netty/example/spdyclient/HttpResponseClientHandler.java new file mode 100644 index 0000000000..5a8fc676e5 --- /dev/null +++ b/example/src/main/java/io/netty/example/spdyclient/HttpResponseClientHandler.java @@ -0,0 +1,88 @@ +/* + * 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.spdyclient; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.example.http.snoop.HttpSnoopClientHandler; +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.HttpResponse; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.CharsetUtil; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * This is a modified version of {@link HttpSnoopClientHandler} that uses a {@link BlockingQueue} to wait until an + * HTTPResponse is received. + */ +public class HttpResponseClientHandler extends SimpleChannelInboundHandler { + + private final BlockingQueue queue = new LinkedBlockingQueue(); + + @Override + public void messageReceived(ChannelHandlerContext ctx, HttpObject msg) throws Exception { + if (msg instanceof HttpResponse) { + HttpResponse response = (HttpResponse) msg; + + System.out.println("STATUS: " + response.getStatus()); + System.out.println("VERSION: " + response.getProtocolVersion()); + System.out.println(); + + if (!response.headers().isEmpty()) { + for (String name : response.headers().names()) { + for (String value : response.headers().getAll(name)) { + System.out.println("HEADER: " + name + " = " + value); + } + } + System.out.println(); + } + + if (HttpHeaders.isTransferEncodingChunked(response)) { + System.out.println("CHUNKED CONTENT {"); + } else { + System.out.println("CONTENT {"); + } + } + if (msg instanceof HttpContent) { + HttpContent content = (HttpContent) msg; + + System.out.print(content.content().toString(CharsetUtil.UTF_8)); + System.out.flush(); + + if (content instanceof LastHttpContent) { + System.out.println("} END OF CONTENT"); + this.queue.add(ctx.channel().newSucceededFuture()); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + this.queue.add(ctx.channel().newFailedFuture(cause)); + cause.printStackTrace(); + ctx.close(); + } + + public BlockingQueue queue() { + return this.queue; + } + +} diff --git a/example/src/main/java/io/netty/example/spdyclient/SpdyClient.java b/example/src/main/java/io/netty/example/spdyclient/SpdyClient.java new file mode 100644 index 0000000000..9625838691 --- /dev/null +++ b/example/src/main/java/io/netty/example/spdyclient/SpdyClient.java @@ -0,0 +1,135 @@ +/* + * 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.spdyclient; + +import static java.util.concurrent.TimeUnit.SECONDS; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +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.HttpVersion; + +import java.net.InetSocketAddress; +import java.util.concurrent.BlockingQueue; + +/** + * An SPDY client that allows you to send HTTP GET to a SPDY server. + *

+ * This class must be run with the JVM parameter: {@code java -Xbootclasspath/p: ...}. The + * "path_to_npn_boot_jar" is the path on the file system for the NPN Boot Jar file which can be downloaded from Maven at + * coordinates org.mortbay.jetty.npn:npn-boot. Different versions applies to different OpenJDK versions. See + * {@link http://www.eclipse.org/jetty/documentation/current/npn-chapter.html Jetty docs} for more information. + *

+ */ +public class SpdyClient { + + private final String host; + private final int port; + private final HttpResponseClientHandler httpResponseHandler; + private Channel channel; + private EventLoopGroup workerGroup; + + public SpdyClient(String host, int port) { + this.host = host; + this.port = port; + this.httpResponseHandler = new HttpResponseClientHandler(); + } + + public void start() { + if (this.channel != null) { + System.out.println("Already running!"); + return; + } + + this.workerGroup = new NioEventLoopGroup(); + + Bootstrap b = new Bootstrap(); + b.group(workerGroup); + b.channel(NioSocketChannel.class); + b.option(ChannelOption.SO_KEEPALIVE, true); + b.remoteAddress(new InetSocketAddress(this.host, this.port)); + b.handler(new SpdyClientInitializer(this.httpResponseHandler)); + + // Start the client. + this.channel = b.connect().syncUninterruptibly().channel(); + System.out.println("Connected to [" + this.host + ":" + this.port + "]"); + } + + public void stop() { + try { + // Wait until the connection is closed. + this.channel.close().syncUninterruptibly(); + } finally { + if (this.workerGroup != null) { + this.workerGroup.shutdownGracefully(); + } + } + } + + public ChannelFuture send(HttpRequest request) { + // Sends the HTTP request. + return this.channel.writeAndFlush(request); + } + + public HttpRequest get() { + HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); + request.headers().set(HttpHeaders.Names.HOST, this.host); + request.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP); + return request; + } + + public BlockingQueue httpResponseQueue() { + return this.httpResponseHandler.queue(); + } + + public static void main(String[] args) throws Exception { + int port; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } else { + port = 8443; + } + + final SpdyClient client = new SpdyClient("localhost", port); + + try { + client.start(); + ChannelFuture requestFuture = client.send(client.get()).sync(); + + if (!requestFuture.isSuccess()) { + requestFuture.cause().printStackTrace(); + } + + // Waits for the complete HTTP response + ChannelFuture responseFuture = client.httpResponseQueue().poll(5, SECONDS); + + if (!responseFuture.isSuccess()) { + responseFuture.cause().printStackTrace(); + } + + System.out.println("Finished SPDY HTTP GET"); + } finally { + client.stop(); + } + } +} diff --git a/example/src/main/java/io/netty/example/spdyclient/SpdyClientInitializer.java b/example/src/main/java/io/netty/example/spdyclient/SpdyClientInitializer.java new file mode 100644 index 0000000000..84adabc09a --- /dev/null +++ b/example/src/main/java/io/netty/example/spdyclient/SpdyClientInitializer.java @@ -0,0 +1,64 @@ +/* + * 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.spdyclient; + +import static io.netty.handler.codec.spdy.SpdyVersion.SPDY_3_1; +import static io.netty.util.internal.logging.InternalLogLevel.INFO; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.example.securechat.SecureChatSslContextFactory; +import io.netty.handler.codec.spdy.SpdyFrameDecoder; +import io.netty.handler.codec.spdy.SpdyFrameEncoder; +import io.netty.handler.codec.spdy.SpdyHttpDecoder; +import io.netty.handler.codec.spdy.SpdyHttpEncoder; +import io.netty.handler.codec.spdy.SpdySessionHandler; +import io.netty.handler.ssl.SslHandler; + +import javax.net.ssl.SSLEngine; + +import org.eclipse.jetty.npn.NextProtoNego; + +public class SpdyClientInitializer extends ChannelInitializer { + + private final HttpResponseClientHandler httpResponseHandler; + + public SpdyClientInitializer(HttpResponseClientHandler httpResponseHandler) { + this.httpResponseHandler = httpResponseHandler; + } + + private static final int MAX_SPDY_CONTENT_LENGTH = 1024 * 1024; // 1 MB + + @Override + public void initChannel(SocketChannel ch) throws Exception { + SSLEngine engine = SecureChatSslContextFactory.getClientContext().createSSLEngine(); + engine.setUseClientMode(true); + NextProtoNego.put(engine, new SpdyClientProvider()); + NextProtoNego.debug = true; + + ChannelPipeline pipeline = ch.pipeline(); + + pipeline.addLast("ssl", new SslHandler(engine)); + pipeline.addLast("spdyEncoder", new SpdyFrameEncoder(SPDY_3_1)); + pipeline.addLast("spdyDecoder", new SpdyFrameDecoder(SPDY_3_1)); + pipeline.addLast("spdyFrameLogger", new SpdyFrameLogger(INFO)); + pipeline.addLast("spdySessionHandler", new SpdySessionHandler(SPDY_3_1, false)); + pipeline.addLast("spdyHttpEncoder", new SpdyHttpEncoder(SPDY_3_1)); + pipeline.addLast("spdyHttpDecoder", new SpdyHttpDecoder(SPDY_3_1, MAX_SPDY_CONTENT_LENGTH)); + pipeline.addLast("spdyStreamIdHandler", new SpdyClientStreamIdHandler()); + pipeline.addLast("httpHandler", httpResponseHandler); + } +} diff --git a/example/src/main/java/io/netty/example/spdyclient/SpdyClientProvider.java b/example/src/main/java/io/netty/example/spdyclient/SpdyClientProvider.java new file mode 100644 index 0000000000..621162391f --- /dev/null +++ b/example/src/main/java/io/netty/example/spdyclient/SpdyClientProvider.java @@ -0,0 +1,59 @@ +/* + * 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.spdyclient; + +import static io.netty.handler.codec.spdy.SpdyOrHttpChooser.SelectedProtocol.HTTP_1_1; +import static io.netty.handler.codec.spdy.SpdyOrHttpChooser.SelectedProtocol.SPDY_3_1; + +import java.util.List; + +import org.eclipse.jetty.npn.NextProtoNego.ClientProvider; + +/** + * The Jetty project provides an implementation of the Transport Layer Security (TLS) extension for Next Protocol + * Negotiation (NPN) for OpenJDK 7 or greater. NPN allows the application layer to negotiate which protocol to use + * over the secure connection. + *

+ * This NPN service provider negotiates using SPDY. + *

+ * To enable NPN support, start the JVM with: {@code java -Xbootclasspath/p: ...}. The + * "path_to_npn_boot_jar" is the path on the file system for the NPN Boot Jar file which can be downloaded from Maven + * at coordinates org.mortbay.jetty.npn:npn-boot. Different versions applies to different OpenJDK versions. + * + * @see http://www.eclipse.org/jetty/documentation/current/npn-chapter.html + */ +public class SpdyClientProvider implements ClientProvider { + + private String selectedProtocol; + + @Override + public String selectProtocol(List protocols) { + if (protocols.contains(SPDY_3_1.protocolName())) { + return SPDY_3_1.protocolName(); + } + return selectedProtocol; + } + + @Override + public boolean supports() { + return true; + } + + @Override + public void unsupported() { + this.selectedProtocol = HTTP_1_1.protocolName(); + } +} diff --git a/example/src/main/java/io/netty/example/spdyclient/SpdyClientStreamIdHandler.java b/example/src/main/java/io/netty/example/spdyclient/SpdyClientStreamIdHandler.java new file mode 100644 index 0000000000..a224ab6332 --- /dev/null +++ b/example/src/main/java/io/netty/example/spdyclient/SpdyClientStreamIdHandler.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.spdyclient; + +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.spdy.SpdyHttpHeaders; + +/** + * Adds a unique client stream ID to the SPDY header. Client stream IDs MUST be odd. + */ +public class SpdyClientStreamIdHandler extends ChannelHandlerAdapter { + + private int currentStreamId = 1; + + public boolean acceptOutboundMessage(Object msg) throws Exception { + return msg instanceof HttpMessage; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (acceptOutboundMessage(msg)) { + HttpMessage httpMsg = (HttpMessage) msg; + if (!httpMsg.headers().contains(SpdyHttpHeaders.Names.STREAM_ID)) { + SpdyHttpHeaders.setStreamId(httpMsg, this.currentStreamId); + // Client stream IDs are always odd + currentStreamId += 2; + } + } + super.write(ctx, msg, promise); + } +} diff --git a/example/src/main/java/io/netty/example/spdyclient/SpdyFrameLogger.java b/example/src/main/java/io/netty/example/spdyclient/SpdyFrameLogger.java new file mode 100644 index 0000000000..1c019a9ca9 --- /dev/null +++ b/example/src/main/java/io/netty/example/spdyclient/SpdyFrameLogger.java @@ -0,0 +1,75 @@ +/* + * 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.spdyclient; + +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.spdy.SpdyFrame; +import io.netty.util.internal.logging.InternalLogLevel; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +/** + * Logs SPDY frames for debugging purposes. + */ +public class SpdyFrameLogger extends ChannelHandlerAdapter { + + private enum Direction { + INBOUND, OUTBOUND + } + + protected final InternalLogger logger; + private final InternalLogLevel level; + + public SpdyFrameLogger(InternalLogLevel level) { + if (level == null) { + throw new NullPointerException("level"); + } + + this.logger = InternalLoggerFactory.getInstance(getClass()); + this.level = level; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (acceptMessage(msg)) { + log((SpdyFrame) msg, Direction.INBOUND); + } + super.channelRead(ctx, msg); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (acceptMessage(msg)) { + log((SpdyFrame) msg, Direction.OUTBOUND); + } + super.write(ctx, msg, promise); + } + + private boolean acceptMessage(Object msg) throws Exception { + return msg instanceof SpdyFrame; + } + + private void log(SpdyFrame msg, Direction d) { + if (logger.isEnabled(this.level)) { + StringBuilder b = new StringBuilder("\n----------------").append(d.name()).append("--------------------\n"); + b.append(msg.toString()); + b.append("\n------------------------------------"); + logger.log(this.level, b.toString()); + } + } +} diff --git a/example/src/main/java/io/netty/example/spdyclient/package-info.java b/example/src/main/java/io/netty/example/spdyclient/package-info.java new file mode 100644 index 0000000000..c454145548 --- /dev/null +++ b/example/src/main/java/io/netty/example/spdyclient/package-info.java @@ -0,0 +1,36 @@ +/* + * 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 an example SPDY HTTP client. It will behave like a SPDY-enabled browser and you can see the + * SPDY frames flowing in and out using the {@link io.netty.example.spdyclient.SpdyFrameLogger}. + * + *

+ * This package relies on the Jetty project's implementation of the Transport Layer Security (TLS) extension for Next + * Protocol Negotiation (NPN) for OpenJDK 7 is required. NPN allows the application layer to negotiate which + * protocol, SPDY or HTTP, to use. + *

+ * To start, run {@link io.netty.example.spdy.SpdyServer} with the JVM parameter: + * {@code java -Xbootclasspath/p: ...}. + * The "path_to_npn_boot_jar" is the path on the file system for the NPN Boot Jar file which can be downloaded from + * Maven at coordinates org.mortbay.jetty.npn:npn-boot. Different versions applies to different OpenJDK versions. + * See {@link http://www.eclipse.org/jetty/documentation/current/npn-chapter.html Jetty docs} for more information. + *

+ * After that, you can run {@link io.netty.example.spdyclient.SpdyClient}, also settings the JVM parameter + * mentioned above. + */ +package io.netty.example.spdyclient; +