package org.neo4j.smack.test.util; import static org.jboss.netty.channel.Channels.pipeline; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelPipelineFactory; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.channel.group.ChannelGroup; import org.jboss.netty.channel.group.DefaultChannelGroup; import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; import org.jboss.netty.handler.codec.frame.TooLongFrameException; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpVersion; import org.jboss.netty.handler.codec.replay.ReplayingDecoder; import org.jboss.netty.util.CharsetUtil; import org.neo4j.smack.pipeline.http.NettyChannelTrackingHandler; /** * Incomplete implementation of a HTTP client that does pipelining. * Used as a lab tool to see how we can maximize performance. */ public class PipelinedHttpClient { static final byte SP = 32; //tab ' ' static final byte HT = 9; /** * Carriage return */ static final byte CR = 13; /** * Equals '=' */ static final byte EQUALS = 61; /** * Line feed character */ static final byte LF = 10; /** * carriage return line feed */ static final byte[] CRLF = new byte[] { CR, LF }; /** * Colon ':' */ static final byte COLON = 58; /** * Semicolon ';' */ static final byte SEMICOLON = 59; /** * comma ',' */ static final byte COMMA = 44; static final byte DOUBLE_QUOTE = '"'; static final Charset DEFAULT_CHARSET = CharsetUtil.UTF_8; public class HttpClientPipelineFactory implements ChannelPipelineFactory { private HttpResponseHandler responseHandler; private ChannelGroup openChannels; public HttpClientPipelineFactory(ChannelGroup openChannels, HttpResponseHandler responseHandler) { this.responseHandler = responseHandler; this.openChannels = openChannels; } public ChannelPipeline getPipeline() throws Exception { // Create a default pipeline implementation. ChannelPipeline pipeline = pipeline(); //pipeline.addLast("codec", new HttpClientCodec()); // Uncomment the following line if you don't want to handle HttpChunks. //pipeline.addLast("aggregator", new HttpChunkAggregator(1048576)); pipeline.addLast("channeltracker",new NettyChannelTrackingHandler(openChannels)); pipeline.addLast("handler", responseHandler); return pipeline; } } public static class HttpResponseHandler extends SimpleChannelUpstreamHandler { static class HttpDecoder extends ReplayingDecoder<HttpDecoder.State> { enum State { SKIP_CONTROL_CHARS, READ_INITIAL, READ_HEADERS; } private AtomicLong responseCount; HttpDecoder(AtomicLong responseCount) { super(State.SKIP_CONTROL_CHARS, true); this.responseCount = responseCount; } /** * Capable of parsing a single type of HTTP responses, namely: * * HTTP/1.1 200 OK * Content-Length: 0 * * Used to count responses for performance testing. */ @Override @SuppressWarnings ("fallthrough") protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state) throws Exception { switch (state) { case SKIP_CONTROL_CHARS: { try { skipControlCharacters(buffer); checkpoint(State.READ_INITIAL); } finally { checkpoint(); } } case READ_INITIAL: { readLine(buffer, 1000); checkpoint(State.READ_HEADERS); } case READ_HEADERS: { readLine(buffer, 1000); responseCount.incrementAndGet(); return reset(); } default: { throw new Error("Shouldn't reach here."); } } } private Object reset() { checkpoint(State.SKIP_CONTROL_CHARS); return null; } private void skipControlCharacters(ChannelBuffer buffer) { for (;;) { char c = (char) buffer.readUnsignedByte(); if (!Character.isISOControl(c) && !Character.isWhitespace(c)) { buffer.readerIndex(buffer.readerIndex() - 1); break; } } } private void readLine(ChannelBuffer buffer, int maxLineLength) throws TooLongFrameException { //StringBuilder sb = new StringBuilder(64); int lineLength = 0; while (true) { byte nextByte = buffer.readByte(); if (nextByte == CR) { nextByte = buffer.readByte(); if (nextByte == LF) { return; } } else if (nextByte == LF) { return; } else { if (lineLength >= maxLineLength) { // TODO: Respond with Bad Request and discard the traffic // or close the connection. // No need to notify the upstream handlers - just log. // If decoding a response, just throw an exception. throw new TooLongFrameException( "An HTTP line is larger than " + maxLineLength + " bytes."); } lineLength ++; //sb.append((char) nextByte); } } } } public AtomicLong responseCount = new AtomicLong(); public HttpResponse lastResponse; public Throwable lastException = null; HttpDecoder decode = new HttpDecoder(responseCount); @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { decode.messageReceived(ctx, e); } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { lastException = e.getCause(); } } protected Channel channel; private ClientBootstrap bootstrap; public HttpResponseHandler responseHandler = new HttpResponseHandler(); private ChannelGroup openChannels = new DefaultChannelGroup("SmackClient"); public PipelinedHttpClient(String host, int port) { // Configure the client. bootstrap = new ClientBootstrap(new NioClientSocketChannelFactory( Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); // Set up the event pipeline factory. bootstrap.setPipelineFactory(new HttpClientPipelineFactory(openChannels, responseHandler)); // Start the connection attempt. ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port)); // Wait until the connection attempt succeeds or fails. channel = future.awaitUninterruptibly().getChannel(); if (!future.isSuccess()) { bootstrap.releaseExternalResources(); throw new RuntimeException(future.getCause()); } } public ChannelFuture handle(HttpMethod method, URI uri, String payload) { if(responseHandler.lastException != null) { throw new RuntimeException(responseHandler.lastException); } String host = uri.getHost() == null ? "localhost" : uri.getHost(); // Prepare the HTTP request. HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, method, uri.getPath()); request.setHeader(HttpHeaders.Names.HOST, host); request.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive"); if(payload != null) { request.setContent(ChannelBuffers.copiedBuffer(payload, CharsetUtil.UTF_8)); request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, request.getContent().readableBytes()); } else { request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, 0); } // Send the HTTP request. return channel.write(request); } // Quick hack to wait for responses public void waitForXResponses(long count) { while(responseHandler.responseCount.get() < count) { if(responseHandler.lastException != null) { throw new RuntimeException(responseHandler.lastException); } try { Thread.sleep(0, 10); } catch(Exception e) { throw new RuntimeException(e); } } } public void close() { if (openChannels!=null) openChannels.close().awaitUninterruptibly(); bootstrap.releaseExternalResources(); } }