/* * Copyright 2016 Netflix, Inc. * * Licensed 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.reactivex.netty.protocol.http.client.internal; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.AttributeKey; import io.reactivex.netty.client.ClientConnectionToChannelBridge; import io.reactivex.netty.client.ClientConnectionToChannelBridge.ConnectionReuseEvent; import io.reactivex.netty.client.ClientConnectionToChannelBridge.PooledConnectionReleaseEvent; import io.reactivex.netty.client.pool.PooledConnection; import io.reactivex.netty.events.Clock; import io.reactivex.netty.events.EventAttributeKeys; import io.reactivex.netty.events.EventPublisher; import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge; import java.net.InetSocketAddress; import java.net.SocketAddress; import static java.util.concurrent.TimeUnit.*; public class HttpClientToConnectionBridge<C> extends AbstractHttpConnectionBridge<C> { /** * This attribute stores the value of any dynamic idle timeout value sent via an HTTP keep alive header. * This follows the proposal specified here: http://tools.ietf.org/id/draft-thomson-hybi-http-timeout-01.html * The attribute can be extracted from an HTTP response header using the helper method * {@link HttpClientResponseImpl#getKeepAliveTimeoutSeconds()} */ public static final AttributeKey<Long> KEEP_ALIVE_TIMEOUT_MILLIS_ATTR = PooledConnection.DYNAMIC_CONN_KEEP_ALIVE_TIMEOUT_MS; private HttpClientEventsListener eventsListener; private EventPublisher eventPublisher; private String hostHeader; private long requestWriteCompletionTimeNanos; @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { eventsListener = ctx.channel().attr(HttpChannelProvider.HTTP_CLIENT_EVENT_LISTENER).get(); eventPublisher = ctx.channel().attr(EventAttributeKeys.EVENT_PUBLISHER).get(); super.handlerAdded(ctx); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { SocketAddress remoteAddr = ctx.channel().remoteAddress(); if (remoteAddr instanceof InetSocketAddress) { InetSocketAddress inetSock = (InetSocketAddress) remoteAddr; String hostString = inetSock.getHostString(); // Don't use hostname that does a DNS lookup. hostHeader = hostString + ':' + inetSock.getPort(); } super.channelActive(ctx); } @Override protected void beforeOutboundHeaderWrite(HttpMessage httpMsg, ChannelPromise promise, long startTimeNanos) { /*Reset on every request write, we do not currently support pipelining, otherwise, this should be stored in a queue.*/ requestWriteCompletionTimeNanos = -1; if (null != hostHeader) { if (!httpMsg.headers().contains(HttpHeaderNames.HOST)) { httpMsg.headers().set(HttpHeaderNames.HOST, hostHeader); } } if (eventPublisher.publishingEnabled()) { eventsListener.onRequestWriteStart(); } } @Override protected void onOutboundLastContentWrite(LastHttpContent msg, ChannelPromise promise, final long headerWriteStartTimeNanos) { if (eventPublisher.publishingEnabled()) { promise.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (eventPublisher.publishingEnabled()) { requestWriteCompletionTimeNanos = Clock.newStartTimeNanos(); if (future.isSuccess()) { eventsListener.onRequestWriteComplete(Clock.onEndNanos(headerWriteStartTimeNanos), NANOSECONDS); } else { eventsListener.onRequestWriteFailed(Clock.onEndNanos(headerWriteStartTimeNanos), NANOSECONDS, future.cause()); } } } }); } } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof ConnectionReuseEvent) { resetSubscriptionState(connectionInputSubscriber); connectionInputSubscriber = null; } else if (PooledConnectionReleaseEvent.INSTANCE == evt) { onPooledConnectionRelease(connectionInputSubscriber); } super.userEventTriggered(ctx, evt); } @Override protected void onClosedBeforeReceiveComplete(Channel channel) { if (channel.isActive()) { /* * If the close is triggerred by the user, the channel will be active. * If the response, isn't complete, then the connection can not be used. */ channel.attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); } } @Override protected boolean isInboundHeader(Object nextItem) { return nextItem instanceof HttpResponse; } @Override protected boolean isOutboundHeader(Object nextItem) { return nextItem instanceof HttpRequest; } @Override protected Object newHttpObject(Object nextItem, Channel channel) { final HttpResponse nettyResponse = (HttpResponse) nextItem; if (eventPublisher.publishingEnabled()) { long duration = -1; if (requestWriteCompletionTimeNanos != -1) { duration = Clock.onEndNanos(requestWriteCompletionTimeNanos); } eventsListener.onResponseHeadersReceived(nettyResponse.status().code(), duration, NANOSECONDS); } final HttpClientResponseImpl<C> rxResponse = HttpClientResponseImpl.unsafeCreate(nettyResponse); Long keepAliveTimeoutSeconds = rxResponse.getKeepAliveTimeoutSeconds(); if (null != keepAliveTimeoutSeconds) { channel.attr(KEEP_ALIVE_TIMEOUT_MILLIS_ATTR).set(keepAliveTimeoutSeconds * 1000); } if (!rxResponse.isKeepAlive()) { channel.attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); /*Discard connection when done with this response.*/ } return rxResponse; } @Override protected void onContentReceived() { if (eventPublisher.publishingEnabled()) { eventsListener.onResponseContentReceived(); } } @Override protected void onContentReceiveComplete(long receiveStartTimeNanos) { connectionInputSubscriber.onCompleted(); /*Unsubscribe from the input and hence close/release connection*/ if (eventPublisher.publishingEnabled()) { long headerWriteStart = getHeaderWriteStartTimeNanos(); eventsListener.onResponseReceiveComplete(Clock.onEndNanos(receiveStartTimeNanos), NANOSECONDS); eventsListener.onRequestProcessingComplete(Clock.onEndNanos(headerWriteStart), NANOSECONDS); } } private void onPooledConnectionRelease(ConnectionInputSubscriber connectionInputSubscriber) { onChannelClose(connectionInputSubscriber); } }