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

Java tutorial

Introduction

Here is the source code for io.vertx.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 io.vertx.core.http.impl;

import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.ReferenceCountUtil;
import io.vertx.codegen.annotations.Nullable;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.*;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.http.impl.ws.WebSocketFrameInternal;
import io.vertx.core.impl.ContextImpl;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.impl.ConnectionBase;
import io.vertx.core.net.impl.NetSocketImpl;
import io.vertx.core.net.impl.VertxNetHandler;
import io.vertx.core.spi.metrics.HttpClientMetrics;

import java.net.URI;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;

import static io.vertx.core.http.HttpHeaders.ACCEPT_ENCODING;
import static io.vertx.core.http.HttpHeaders.CLOSE;
import static io.vertx.core.http.HttpHeaders.CONNECTION;
import static io.vertx.core.http.HttpHeaders.DEFLATE_GZIP;
import static io.vertx.core.http.HttpHeaders.HOST;
import static io.vertx.core.http.HttpHeaders.KEEP_ALIVE;
import static io.vertx.core.http.HttpHeaders.TRANSFER_ENCODING;

/**
 *
 * This class is optimised for performance when used on the same event loop. However it can be used safely from other threads.
 *
 * The internal state is protected using the synchronized keyword. If always used on the same event loop, then
 * we benefit from biased locking which makes the overhead of synchronized near zero.
 *
 * @author <a href="http://tfox.org">Tim Fox</a>
 */
class ClientConnection extends ConnectionBase implements HttpClientConnection, HttpClientStream {

    private static final Logger log = LoggerFactory.getLogger(ClientConnection.class);

    private final HttpClientImpl client;
    private final boolean ssl;
    private final String host;
    private final int port;
    private final Http1xPool pool;
    private final Object endpointMetric;
    // Requests can be pipelined so we need a queue to keep track of requests
    private final Deque<HttpClientRequestImpl> requests = new ArrayDeque<>();
    private final HttpClientMetrics metrics;
    private final HttpVersion version;

    private WebSocketClientHandshaker handshaker;
    private HttpClientRequestImpl currentRequest;
    private HttpClientResponseImpl currentResponse;
    private HttpClientRequestImpl requestForResponse;
    private WebSocketImpl ws;

    private boolean reset;
    private boolean paused;
    private Buffer pausedChunk;

    ClientConnection(HttpVersion version, HttpClientImpl client, Object endpointMetric, Channel channel,
            boolean ssl, String host, int port, ContextImpl context, Http1xPool pool, HttpClientMetrics metrics) {
        super(client.getVertx(), channel, context);
        this.client = client;
        this.ssl = ssl;
        this.host = host;
        this.port = port;
        this.pool = pool;
        this.metrics = metrics;
        this.version = version;
        this.endpointMetric = endpointMetric;
    }

    public HttpClientMetrics metrics() {
        return metrics;
    }

    synchronized HttpClientRequestImpl getCurrentRequest() {
        return currentRequest;
    }

    synchronized void toWebSocket(String requestURI, MultiMap headers, WebsocketVersion vers, String subProtocols,
            int maxWebSocketFrameSize, Handler<WebSocket> wsConnect) {
        if (ws != null) {
            throw new IllegalStateException("Already websocket");
        }

        try {
            URI wsuri = new URI(requestURI);
            if (!wsuri.isAbsolute()) {
                // Netty requires an absolute url
                wsuri = new URI((ssl ? "https:" : "http:") + "//" + host + ":" + port + requestURI);
            }
            WebSocketVersion version = WebSocketVersion
                    .valueOf((vers == null ? WebSocketVersion.V13 : vers).toString());
            HttpHeaders nettyHeaders;
            if (headers != null) {
                nettyHeaders = new DefaultHttpHeaders();
                for (Map.Entry<String, String> entry : headers) {
                    nettyHeaders.add(entry.getKey(), entry.getValue());
                }
            } else {
                nettyHeaders = null;
            }
            handshaker = WebSocketClientHandshakerFactory.newHandshaker(wsuri, version, subProtocols, false,
                    nettyHeaders, maxWebSocketFrameSize, !client.getOptions().isSendUnmaskedFrames(), false);
            ChannelPipeline p = channel.pipeline();
            p.addBefore("handler", "handshakeCompleter",
                    new HandshakeInboundHandler(wsConnect, version != WebSocketVersion.V00));
            handshaker.handshake(channel).addListener(future -> {
                Handler<Throwable> handler = exceptionHandler();
                if (!future.isSuccess() && handler != null) {
                    handler.handle(future.cause());
                }
            });
        } catch (Exception e) {
            handleException(e);
        }
    }

    private final class HandshakeInboundHandler extends ChannelInboundHandlerAdapter {

        private final boolean supportsContinuation;
        private final Handler<WebSocket> wsConnect;
        private final ContextImpl context;
        private final Queue<Object> buffered = new ArrayDeque<>();
        private FullHttpResponse response;
        private boolean handshaking = true;

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

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            super.channelInactive(ctx);
            // 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(ChannelHandlerContext ctx, Object msg) throws Exception {
            if (handshaker != null && handshaking) {
                if (msg instanceof HttpResponse) {
                    HttpResponse resp = (HttpResponse) msg;
                    if (resp.getStatus().code() != 101) {
                        handshaker = null;
                        close();
                        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) {
                                close();
                                handleException(e);
                            }
                        }
                    }
                }
            } else {
                buffered.add(msg);
            }
        }

        private void handleException(WebSocketHandshakeException e) {
            handshaking = false;
            buffered.clear();
            Handler<Throwable> handler = exceptionHandler();
            if (handler != null) {
                context.executeFromIO(() -> {
                    handler.handle(e);
                });
            } else {
                log.error("Error in websocket handshake", 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);
            }
            // Need to set context before constructor is called as writehandler registration needs this
            ContextImpl.setContext(context);
            WebSocketImpl webSocket = new WebSocketImpl(vertx, ClientConnection.this, supportsContinuation,
                    client.getOptions().getMaxWebsocketFrameSize(),
                    client.getOptions().getMaxWebsocketMessageSize());
            ws = webSocket;
            handshaker.finishHandshake(channel, response);
            context.executeFromIO(() -> {
                log.debug("WebSocket handshake complete");
                webSocket.setMetric(metrics().connected(endpointMetric, metric(), webSocket));
                wsConnect.handle(webSocket);
            });
        }
    }

    public ClientConnection closeHandler(Handler<Void> handler) {
        return (ClientConnection) super.closeHandler(handler);
    }

    public boolean isValid() {
        return channel.isOpen();
    }

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

    @Override
    public void checkDrained() {
        handleInterestedOpsChanged();
    }

    @Override
    public synchronized void handleInterestedOpsChanged() {
        if (!isNotWritable()) {
            if (currentRequest != null) {
                currentRequest.handleDrained();
            } else if (ws != null) {
                ws.writable();
            }
        }
    }

    void handleResponse(HttpResponse resp) {
        if (resp.status().code() == 100) {
            //If we get a 100 continue it will be followed by the real response later, so we don't remove it yet
            requestForResponse = requests.peek();
        } else {
            requestForResponse = requests.poll();
        }
        if (requestForResponse == null) {
            throw new IllegalStateException("No response handler");
        }
        io.netty.handler.codec.http.HttpVersion nettyVersion = resp.protocolVersion();
        HttpVersion vertxVersion;
        if (nettyVersion == io.netty.handler.codec.http.HttpVersion.HTTP_1_0) {
            vertxVersion = HttpVersion.HTTP_1_0;
        } else if (nettyVersion == io.netty.handler.codec.http.HttpVersion.HTTP_1_1) {
            vertxVersion = HttpVersion.HTTP_1_1;
        } else {
            vertxVersion = null;
        }
        HttpClientResponseImpl nResp = new HttpClientResponseImpl(requestForResponse, vertxVersion, this,
                resp.status().code(), resp.status().reasonPhrase(), new HeadersAdaptor(resp.headers()));
        currentResponse = nResp;
        if (metrics.isEnabled()) {
            metrics.responseBegin(requestForResponse.metric(), nResp);
        }
        if (vertxVersion != null) {
            requestForResponse.handleResponse(nResp);
        } else {
            requestForResponse
                    .handleException(new IllegalStateException("Unsupported HTTP version: " + nettyVersion));
        }
    }

    public void doPause() {
        super.doPause();
        paused = true;
    }

    public void doResume() {
        super.doResume();
        paused = false;
        if (pausedChunk != null) {
            vertx.runOnContext(v -> {
                if (pausedChunk != null) {
                    Buffer chunk = pausedChunk;
                    pausedChunk = null;
                    currentResponse.handleChunk(chunk);
                }
            });
        }
    }

    void handleResponseChunk(Buffer buff) {
        if (paused) {
            if (pausedChunk == null) {
                pausedChunk = buff.copy();
            } else {
                pausedChunk.appendBuffer(buff);
            }
        } else {
            if (pausedChunk != null) {
                buff = pausedChunk.appendBuffer(buff);
                pausedChunk = null;
            }
            currentResponse.handleChunk(buff);
        }
    }

    void handleResponseEnd(LastHttpContent trailer) {
        if (metrics.isEnabled()) {
            HttpClientRequestBase req = currentResponse.request();
            Object reqMetric = req.metric();
            if (req.exceptionOccurred != null) {
                metrics.requestReset(reqMetric);
            } else {
                metrics.responseEnd(reqMetric, currentResponse);
            }
        }
        Buffer last = pausedChunk;
        pausedChunk = null;
        currentResponse.handleEnd(last, new HeadersAdaptor(trailer.trailingHeaders()));

        // We don't signal response end for a 100-continue response as a real response will follow
        // Also we keep the connection open for an HTTP CONNECT
        if (currentResponse.statusCode() != 100
                && requestForResponse.method() != io.vertx.core.http.HttpMethod.CONNECT) {

            boolean close = false;
            // See https://tools.ietf.org/html/rfc7230#section-6.3
            String responseConnectionHeader = currentResponse.getHeader(HttpHeaders.Names.CONNECTION);
            io.vertx.core.http.HttpVersion protocolVersion = client.getOptions().getProtocolVersion();
            String requestConnectionHeader = requestForResponse.headers().get(HttpHeaders.Names.CONNECTION);
            // We don't need to protect against concurrent changes on forceClose as it only goes from false -> true
            if (HttpHeaders.Values.CLOSE.equalsIgnoreCase(responseConnectionHeader)
                    || HttpHeaders.Values.CLOSE.equalsIgnoreCase(requestConnectionHeader)) {
                // In all cases, if we have a close connection option then we SHOULD NOT treat the connection as persistent
                close = true;
            } else if (protocolVersion == io.vertx.core.http.HttpVersion.HTTP_1_0
                    && !HttpHeaders.Values.KEEP_ALIVE.equalsIgnoreCase(responseConnectionHeader)) {
                // In the HTTP/1.0 case both request/response need a keep-alive connection header the connection to be persistent
                // currently Vertx forces the Connection header if keepalive is enabled for 1.0
                close = true;
            }

            if (close) {
                pool.responseEnded(this, true);
            } else {
                if (reset) {
                    if (requests.isEmpty()) {
                        pool.responseEnded(this, true);
                    }
                } else {
                    pool.responseEnded(this, false);
                }
            }
        }
        currentResponse = null;
    }

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

    protected synchronized void handleClosed() {
        super.handleClosed();
        if (ws != null) {
            ws.handleClosed();
        }
        Exception e = new VertxException("Connection was closed");

        // Signal requests failed
        if (metrics.isEnabled()) {
            for (HttpClientRequestImpl req : requests) {
                metrics.requestReset(req.metric());
            }
            if (currentResponse != null) {
                metrics.requestReset(currentResponse.request().metric());
            }
        }

        // Connection was closed - call exception handlers for any requests in the pipeline or one being currently written
        for (HttpClientRequestImpl req : requests) {
            req.handleException(e);
        }
        if (currentRequest != null) {
            currentRequest.handleException(e);
        } else if (currentResponse != null) {
            currentResponse.handleException(e);
        }

        // The connection has been closed - tell the pool about it, this allows the pool to create more
        // connections. Note the pool doesn't actually remove the connection, when the next person to get a connection
        // gets the closed on, they will check if it's closed and if so get another one.
        pool.connectionClosed(this);
    }

    public ContextImpl getContext() {
        return super.getContext();
    }

    public void resetRequest(long code) {
        if (!reset) {
            reset = true;
            currentRequest = null;
            requests.removeLast();
            if (requests.size() == 0) {
                pool.responseEnded(this, true);
            }
        }
    }

    @Override
    public void resetResponse(long code) {
        reset = true;
        pool.responseEnded(this, true);
    }

    private HttpRequest createRequest(HttpVersion version, HttpMethod method, String rawMethod, String uri,
            MultiMap headers) {
        DefaultHttpRequest request = new DefaultHttpRequest(HttpUtils.toNettyHttpVersion(version),
                HttpUtils.toNettyHttpMethod(method, rawMethod), uri, false);
        if (headers != null) {
            for (Map.Entry<String, String> header : headers) {
                // Todo : multi valued headers
                request.headers().add(header.getKey(), header.getValue());
            }
        }
        return request;
    }

    private void prepareHeaders(HttpRequest request, String hostHeader, boolean chunked) {
        HttpHeaders headers = request.headers();
        headers.remove(TRANSFER_ENCODING);
        if (!headers.contains(HOST)) {
            request.headers().set(HOST, hostHeader);
        }
        if (chunked) {
            HttpHeaders.setTransferEncodingChunked(request);
        }
        if (client.getOptions().isTryUseCompression() && request.headers().get(ACCEPT_ENCODING) == null) {
            // if compression should be used but nothing is specified by the user support deflate and gzip.
            request.headers().set(ACCEPT_ENCODING, DEFLATE_GZIP);
        }
        if (!client.getOptions().isKeepAlive()
                && client.getOptions().getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_1) {
            request.headers().set(CONNECTION, CLOSE);
        } else if (client.getOptions().isKeepAlive()
                && client.getOptions().getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_0) {
            request.headers().set(CONNECTION, KEEP_ALIVE);
        }
    }

    public void writeHead(HttpMethod method, String rawMethod, String uri, MultiMap headers, String hostHeader,
            boolean chunked) {
        HttpRequest request = createRequest(version, method, rawMethod, uri, headers);
        prepareHeaders(request, hostHeader, chunked);
        writeToChannel(request);
    }

    public void writeHeadWithContent(HttpMethod method, String rawMethod, String uri, MultiMap headers,
            String hostHeader, boolean chunked, ByteBuf buf, boolean end) {
        HttpRequest request = createRequest(version, method, rawMethod, uri, headers);
        prepareHeaders(request, hostHeader, chunked);
        if (end) {
            if (buf != null) {
                writeToChannel(new AssembledFullHttpRequest(request, buf));
            } else {
                writeToChannel(new AssembledFullHttpRequest(request));
            }
        } else {
            writeToChannel(new AssembledHttpRequest(request, buf));
        }
    }

    @Override
    public void writeBuffer(ByteBuf buff, boolean end) {
        if (end) {
            if (buff != null && buff.isReadable()) {
                writeToChannel(new DefaultLastHttpContent(buff, false));
            } else {
                writeToChannel(LastHttpContent.EMPTY_LAST_CONTENT);
            }
        } else if (buff != null) {
            writeToChannel(new DefaultHttpContent(buff));
        }
    }

    @Override
    public void writeFrame(int type, int flags, ByteBuf payload) {
        throw new IllegalStateException("Cannot write an HTTP/2 frame over an HTTP/1.x connection");
    }

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

    public synchronized void beginRequest(HttpClientRequestImpl req) {
        if (currentRequest != null) {
            throw new IllegalStateException("Connection is already writing a request");
        }
        if (metrics.isEnabled()) {
            Object reqMetric = client.httpClientMetrics().requestBegin(endpointMetric, metric(), localAddress(),
                    remoteAddress(), req);
            req.metric(reqMetric);
        }
        this.currentRequest = req;
        this.requests.add(req);
    }

    public synchronized void endRequest() {
        if (currentRequest == null) {
            throw new IllegalStateException("No write in progress");
        }
        if (metrics.isEnabled()) {
            metrics.requestEnd(currentRequest.metric());
        }
        currentRequest = null;
        pool.requestEnded(this);
    }

    @Override
    public synchronized void close() {
        if (handshaker == null) {
            super.close();
        } else {
            // make sure everything is flushed out on close
            endReadAndFlush();
            // close the websocket connection by sending a close frame.
            handshaker.close(channel, new CloseWebSocketFrame(1000, null));
        }
    }

    public NetSocket createNetSocket() {
        // connection was upgraded to raw TCP socket
        NetSocketImpl socket = new NetSocketImpl(vertx, channel, context, client.getSslHelper(), metrics);
        socket.metric(metric());
        Map<Channel, NetSocketImpl> connectionMap = new HashMap<>(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<NetSocketImpl>(channel, socket, connectionMap) {
            @Override
            public void exceptionCaught(ChannelHandlerContext chctx, Throwable t) throws Exception {
                // remove from the real mapping
                pool.removeChannel(channel);
                super.exceptionCaught(chctx, t);
            }

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

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

            @Override
            protected void handleMsgReceived(NetSocketImpl conn, Object msg) {
                ByteBuf buf = (ByteBuf) msg;
                conn.handleDataReceived(Buffer.buffer(buf));
            }
        });
        return socket;
    }

    @Override
    public HttpClientConnection connection() {
        return this;
    }

    @Override
    public HttpVersion version() {
        // Used to determine the http version in the HttpClientRequest#sendHead handler , for HTTP/1.1 it will
        // not yet know but it will for HTTP/2
        return null;
    }

    @Override
    public int id() {
        return -1;
    }

    //

    @Override
    public HttpConnection goAway(long errorCode, int lastStreamId, Buffer debugData) {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY");
    }

    @Override
    public HttpConnection goAwayHandler(@Nullable Handler<GoAway> handler) {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY");
    }

    @Override
    public HttpConnection shutdownHandler(@Nullable Handler<Void> handler) {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY");
    }

    @Override
    public HttpConnection shutdown() {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY");
    }

    @Override
    public HttpConnection shutdown(long timeoutMs) {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY");
    }

    @Override
    public Http2Settings settings() {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS");
    }

    @Override
    public HttpConnection updateSettings(Http2Settings settings) {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS");
    }

    @Override
    public HttpConnection updateSettings(Http2Settings settings, Handler<AsyncResult<Void>> completionHandler) {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS");
    }

    @Override
    public Http2Settings remoteSettings() {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS");
    }

    @Override
    public HttpConnection remoteSettingsHandler(Handler<Http2Settings> handler) {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS");
    }

    @Override
    public HttpConnection ping(Buffer data, Handler<AsyncResult<Buffer>> pongHandler) {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support PING");
    }

    @Override
    public HttpConnection pingHandler(@Nullable Handler<Buffer> handler) {
        throw new UnsupportedOperationException("HTTP/1.x connections don't support PING");
    }

    @Override
    public ClientConnection exceptionHandler(Handler<Throwable> handler) {
        return (ClientConnection) super.exceptionHandler(handler);
    }
}