org.vertx.java.core.http.impl.ClientConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.vertx.java.core.http.impl.ClientConnection.java

Source

/*
 * Copyright (c) 2011-2013 The original author or authors
 * ------------------------------------------------------
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution.
 *
 *     The Eclipse Public License is available at
 *     http://www.eclipse.org/legal/epl-v10.html
 *
 *     The Apache License v2.0 is available at
 *     http://www.opensource.org/licenses/apache2.0.php
 *
 * You may elect to redistribute this code under either of these licenses.
 */

package org.vertx.java.core.http.impl;

import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import io.netty.util.ReferenceCountUtil;
import org.vertx.java.core.Handler;
import org.vertx.java.core.MultiMap;
import org.vertx.java.core.buffer.Buffer;
import org.vertx.java.core.http.WebSocket;
import org.vertx.java.core.http.WebSocketVersion;
import org.vertx.java.core.http.impl.ws.WebSocketFrameInternal;
import org.vertx.java.core.impl.DefaultContext;
import org.vertx.java.core.impl.VertxInternal;
import org.vertx.java.core.logging.Logger;
import org.vertx.java.core.logging.impl.LoggerFactory;
import org.vertx.java.core.net.NetSocket;
import org.vertx.java.core.net.impl.ConnectionBase;
import org.vertx.java.core.net.impl.DefaultNetSocket;
import org.vertx.java.core.net.impl.VertxNetHandler;

import java.net.URI;
import java.util.*;

/**
 * @author <a href="http://tfox.org">Tim Fox</a>
 */
class ClientConnection extends ConnectionBase {
    private static final Logger log = LoggerFactory.getLogger(ClientConnection.class);

    final DefaultHttpClient client;
    final String hostHeader;
    private final boolean ssl;
    private final String host;
    private final int port;
    boolean keepAlive;
    private boolean upgradedConnection;
    private WebSocketClientHandshaker handshaker;
    private volatile DefaultHttpClientRequest currentRequest;
    // Requests can be pipelined so we need a queue to keep track of requests
    private final Queue<DefaultHttpClientRequest> requests = new ArrayDeque<>();
    private volatile DefaultHttpClientResponse currentResponse;
    private DefaultWebSocket ws;

    ClientConnection(VertxInternal vertx, DefaultHttpClient client, Channel channel, boolean ssl, String host,
            int port, boolean keepAlive, DefaultContext context) {
        super(vertx, channel, context);
        this.client = client;
        this.ssl = ssl;
        this.host = host;
        this.port = port;
        if ((port == 80 && !ssl) || (port == 443 && ssl)) {
            this.hostHeader = host;
        } else {
            this.hostHeader = host + ':' + port;
        }
        this.keepAlive = keepAlive;
    }

    void toWebSocket(String uri, final WebSocketVersion wsVersion, final MultiMap headers,
            int maxWebSocketFrameSize, final Set<String> subProtocols, final Handler<WebSocket> wsConnect) {
        if (ws != null) {
            throw new IllegalStateException("Already websocket");
        }

        try {
            URI wsuri = new URI(uri);
            if (!wsuri.isAbsolute()) {
                // Netty requires an absolute url
                wsuri = new URI((ssl ? "https:" : "http:") + "//" + host + ":" + port + uri);
            }
            io.netty.handler.codec.http.websocketx.WebSocketVersion version;
            if (wsVersion == WebSocketVersion.HYBI_00) {
                version = io.netty.handler.codec.http.websocketx.WebSocketVersion.V00;
            } else if (wsVersion == WebSocketVersion.HYBI_08) {
                version = io.netty.handler.codec.http.websocketx.WebSocketVersion.V08;
            } else if (wsVersion == WebSocketVersion.RFC6455) {
                version = io.netty.handler.codec.http.websocketx.WebSocketVersion.V13;
            } else {
                throw new IllegalArgumentException("Invalid version");
            }
            HttpHeaders nettyHeaders;
            if (headers != null) {
                nettyHeaders = new DefaultHttpHeaders();
                for (Map.Entry<String, String> entry : headers) {
                    nettyHeaders.add(entry.getKey(), entry.getValue());
                }
            } else {
                nettyHeaders = null;
            }
            String wsSubProtocols = null;
            if (subProtocols != null && !subProtocols.isEmpty()) {
                StringBuilder sb = new StringBuilder();

                Iterator<String> protocols = subProtocols.iterator();
                while (protocols.hasNext()) {
                    sb.append(protocols.next());
                    if (protocols.hasNext()) {
                        sb.append(",");
                    }
                }
                wsSubProtocols = sb.toString();
            }
            handshaker = WebSocketClientHandshakerFactory.newHandshaker(wsuri, version, wsSubProtocols, false,
                    nettyHeaders, maxWebSocketFrameSize);
            final ChannelPipeline p = channel.pipeline();
            p.addBefore("handler", "handshakeCompleter", new HandshakeInboundHandler(wsConnect));
            handshaker.handshake(channel).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (!future.isSuccess()) {
                        client.handleException((Exception) future.cause());
                    }
                }
            });
            upgradedConnection = true;

        } catch (Exception e) {
            handleException(e);
        }
    }

    private final class HandshakeInboundHandler extends ChannelInboundHandlerAdapter {
        private final Handler<WebSocket> wsConnect;
        private final DefaultContext context;
        private FullHttpResponse response;
        private boolean handshaking = true;
        private final Queue<Object> buffered = new ArrayDeque<>();

        public HandshakeInboundHandler(final Handler<WebSocket> wsConnect) {
            this.wsConnect = wsConnect;
            this.context = vertx.getContext();
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            super.channelInactive(ctx);
            context.execute(ctx.channel().eventLoop(), new Runnable() {
                @Override
                public void run() {
                    // if still handshaking this means we not got any response back from the server and so need to notify the client
                    // about it as otherwise the client would never been notified.
                    if (handshaking) {
                        handleException(
                                new WebSocketHandshakeException("Connection closed while handshake in process"));
                    }
                }
            });
        }

        @Override
        public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
            context.execute(ctx.channel().eventLoop(), new Runnable() {
                public void run() {
                    if (handshaker != null && handshaking) {
                        if (msg instanceof HttpResponse) {
                            HttpResponse resp = (HttpResponse) msg;
                            if (resp.getStatus().code() != 101) {
                                handleException(new WebSocketHandshakeException(
                                        "Websocket connection attempt returned HTTP status code "
                                                + resp.getStatus().code()));
                                return;
                            }
                            response = new DefaultFullHttpResponse(resp.getProtocolVersion(), resp.getStatus());
                            response.headers().add(resp.headers());
                        }

                        if (msg instanceof HttpContent) {
                            if (response != null) {
                                response.content().writeBytes(((HttpContent) msg).content());
                                if (msg instanceof LastHttpContent) {
                                    response.trailingHeaders().add(((LastHttpContent) msg).trailingHeaders());
                                    try {
                                        handshakeComplete(ctx, response);
                                        channel.pipeline().remove(HandshakeInboundHandler.this);
                                        for (;;) {
                                            Object m = buffered.poll();
                                            if (m == null) {
                                                break;
                                            }
                                            ctx.fireChannelRead(m);
                                        }
                                    } catch (WebSocketHandshakeException e) {
                                        actualClose();
                                        handleException(e);
                                    }
                                }
                            }
                        }
                    } else {
                        buffered.add(msg);
                    }
                }
            });
        }

        private void handleException(WebSocketHandshakeException e) {
            handshaking = false;
            buffered.clear();
            client.handleException(e);
        }

        private void handshakeComplete(ChannelHandlerContext ctx, FullHttpResponse response) {
            handshaking = false;
            ChannelHandler handler = ctx.pipeline().get(HttpContentDecompressor.class);
            if (handler != null) {
                // remove decompressor as its not needed anymore once connection was upgraded to websockets
                ctx.pipeline().remove(handler);
            }
            ws = new DefaultWebSocket(vertx, ClientConnection.this);
            handshaker.finishHandshake(channel, response);
            log.debug("WebSocket handshake complete");
            wsConnect.handle(ws);
        }
    }

    public void closeHandler(Handler<Void> handler) {
        this.closeHandler = handler;
    }

    @Override
    public void close() {
        if (upgradedConnection) {
            // Close it
            actualClose();
        } else if (!keepAlive) {
            // Close it
            actualClose();
        } else {
            // Keep alive
            client.returnConnection(this);
        }
    }

    void actualClose() {
        super.close();
    }

    boolean isClosed() {
        return !channel.isOpen();
    }

    int getOutstandingRequestCount() {
        return requests.size();
    }

    //TODO - combine these with same in ServerConnection and NetSocket
    @Override
    public void handleInterestedOpsChanged() {
        try {
            if (!doWriteQueueFull()) {
                if (currentRequest != null) {
                    setContext();
                    currentRequest.handleDrained();
                } else if (ws != null) {
                    ws.writable();
                }
            }
        } catch (Throwable t) {
            handleHandlerException(t);
        }
    }

    void handleResponse(HttpResponse resp) {
        DefaultHttpClientRequest req;
        if (resp.getStatus().code() == 100) {
            //If we get a 100 continue it will be followed by the real response later, so we don't remove it yet
            req = requests.peek();
        } else {
            req = requests.poll();
        }
        if (req == null) {
            throw new IllegalStateException("No response handler");
        }
        setContext();
        DefaultHttpClientResponse nResp = new DefaultHttpClientResponse(vertx, req, this, resp);
        currentResponse = nResp;
        req.handleResponse(nResp);
    }

    void handleResponseChunk(Buffer buff) {
        setContext();
        try {
            currentResponse.handleChunk(buff);
        } catch (Throwable t) {
            handleHandlerException(t);
        }
    }

    void handleResponseEnd(LastHttpContent trailer) {
        setContext();
        try {
            currentResponse.handleEnd(trailer);
        } catch (Throwable t) {
            handleHandlerException(t);
        }
        if (!keepAlive) {
            close();
        }
    }

    void handleWsFrame(WebSocketFrameInternal frame) {
        if (ws != null) {
            setContext();
            ws.handleFrame(frame);
        }
    }

    protected void handleClosed() {
        super.handleClosed();
        if (ws != null) {
            ws.handleClosed();
        }
    }

    protected DefaultContext getContext() {
        return super.getContext();
    }

    @Override
    protected void handleException(Throwable e) {
        super.handleException(e);
        if (currentRequest != null) {
            currentRequest.handleException(e);
        } else if (currentResponse != null) {
            currentResponse.handleException(e);
        }
    }

    void setCurrentRequest(DefaultHttpClientRequest req) {
        if (currentRequest != null) {
            throw new IllegalStateException("Connection is already writing a request");
        }
        this.currentRequest = req;
        this.requests.add(req);
    }

    void endRequest() {
        if (currentRequest == null) {
            throw new IllegalStateException("No write in progress");
        }
        currentRequest = null;

        if (keepAlive) {
            //Close just returns connection to the pool
            close();
        } else {
            //The connection gets closed after the response is received
        }
    }

    NetSocket createNetSocket() {
        // connection was upgraded to raw TCP socket
        upgradedConnection = true;
        DefaultNetSocket socket = new DefaultNetSocket(vertx, channel, context, client.tcpHelper, true);
        Map<Channel, DefaultNetSocket> connectionMap = new HashMap<Channel, DefaultNetSocket>(1);
        connectionMap.put(channel, socket);

        // Flush out all pending data
        endReadAndFlush();

        // remove old http handlers and replace the old handler with one that handle plain sockets
        ChannelPipeline pipeline = channel.pipeline();
        ChannelHandler inflater = pipeline.get(HttpContentDecompressor.class);
        if (inflater != null) {
            pipeline.remove(inflater);
        }
        pipeline.remove("codec");
        pipeline.replace("handler", "handler", new VertxNetHandler(client.vertx, connectionMap) {
            @Override
            public void exceptionCaught(ChannelHandlerContext chctx, Throwable t) throws Exception {
                // remove from the real mapping
                client.connectionMap.remove(channel);
                super.exceptionCaught(chctx, t);
            }

            @Override
            public void channelInactive(ChannelHandlerContext chctx) throws Exception {
                // remove from the real mapping
                client.connectionMap.remove(channel);
                super.channelInactive(chctx);
            }

            @Override
            public void channelRead(ChannelHandlerContext chctx, Object msg) throws Exception {
                if (msg instanceof HttpContent) {
                    ReferenceCountUtil.release(msg);
                    return;
                }
                super.channelRead(chctx, msg);
            }
        });
        return socket;
    }
}