Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.arangodb.shaded.vertx.core.http.impl.Http1xClientConnection Maven / Gradle / Ivy
/*
* Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/
package com.arangodb.shaded.vertx.core.http.impl;
import com.arangodb.shaded.netty.buffer.ByteBuf;
import com.arangodb.shaded.netty.buffer.Unpooled;
import com.arangodb.shaded.netty.channel.*;
import com.arangodb.shaded.netty.handler.codec.DecoderResult;
import com.arangodb.shaded.netty.handler.codec.compression.Brotli;
import com.arangodb.shaded.netty.handler.codec.compression.ZlibCodecFactory;
import com.arangodb.shaded.netty.handler.codec.http.DefaultHttpContent;
import com.arangodb.shaded.netty.handler.codec.http.DefaultHttpHeaders;
import com.arangodb.shaded.netty.handler.codec.http.DefaultHttpRequest;
import com.arangodb.shaded.netty.handler.codec.http.DefaultLastHttpContent;
import com.arangodb.shaded.netty.handler.codec.http.FullHttpRequest;
import com.arangodb.shaded.netty.handler.codec.http.HttpContent;
import com.arangodb.shaded.netty.handler.codec.http.HttpContentDecompressor;
import com.arangodb.shaded.netty.handler.codec.http.HttpHeaderNames;
import com.arangodb.shaded.netty.handler.codec.http.HttpHeaderValues;
import com.arangodb.shaded.netty.handler.codec.http.HttpHeaders;
import com.arangodb.shaded.netty.handler.codec.http.HttpObject;
import com.arangodb.shaded.netty.handler.codec.http.HttpRequest;
import com.arangodb.shaded.netty.handler.codec.http.HttpResponseStatus;
import com.arangodb.shaded.netty.handler.codec.http.HttpUtil;
import com.arangodb.shaded.netty.handler.codec.http.LastHttpContent;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocket07FrameDecoder;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocket13FrameDecoder;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker00;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker07;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker08;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketClientHandshaker13;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketFrame;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketFrameDecoder;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketVersion;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandler;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameClientExtensionHandshaker;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
import com.arangodb.shaded.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker;
import com.arangodb.shaded.netty.handler.timeout.IdleStateEvent;
import com.arangodb.shaded.netty.util.ReferenceCountUtil;
import com.arangodb.shaded.netty.util.concurrent.FutureListener;
import com.arangodb.shaded.vertx.core.AsyncResult;
import com.arangodb.shaded.vertx.core.Future;
import com.arangodb.shaded.vertx.core.Handler;
import com.arangodb.shaded.vertx.core.MultiMap;
import com.arangodb.shaded.vertx.core.Promise;
import com.arangodb.shaded.vertx.core.VertxException;
import com.arangodb.shaded.vertx.core.buffer.Buffer;
import com.arangodb.shaded.vertx.core.http.HttpClientOptions;
import com.arangodb.shaded.vertx.core.http.HttpFrame;
import com.arangodb.shaded.vertx.core.http.HttpMethod;
import com.arangodb.shaded.vertx.core.http.HttpVersion;
import com.arangodb.shaded.vertx.core.http.StreamPriority;
import com.arangodb.shaded.vertx.core.http.WebSocket;
import com.arangodb.shaded.vertx.core.http.WebsocketVersion;
import com.arangodb.shaded.vertx.core.http.impl.headers.HeadersAdaptor;
import com.arangodb.shaded.vertx.core.impl.ContextInternal;
import com.arangodb.shaded.vertx.core.impl.future.PromiseInternal;
import com.arangodb.shaded.vertx.core.impl.logging.Logger;
import com.arangodb.shaded.vertx.core.impl.logging.LoggerFactory;
import com.arangodb.shaded.vertx.core.net.SocketAddress;
import com.arangodb.shaded.vertx.core.net.impl.NetSocketImpl;
import com.arangodb.shaded.vertx.core.net.impl.NetSocketInternal;
import com.arangodb.shaded.vertx.core.net.impl.VertxHandler;
import com.arangodb.shaded.vertx.core.spi.metrics.ClientMetrics;
import com.arangodb.shaded.vertx.core.spi.metrics.HttpClientMetrics;
import com.arangodb.shaded.vertx.core.spi.tracing.SpanKind;
import com.arangodb.shaded.vertx.core.spi.tracing.TagExtractor;
import com.arangodb.shaded.vertx.core.spi.tracing.VertxTracer;
import com.arangodb.shaded.vertx.core.streams.WriteStream;
import com.arangodb.shaded.vertx.core.streams.impl.InboundBuffer;
import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import static com.arangodb.shaded.netty.handler.codec.http.websocketx.WebSocketVersion.*;
import static com.arangodb.shaded.vertx.core.http.HttpHeaders.*;
/**
*
* 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 Tim Fox
*/
public class Http1xClientConnection extends Http1xConnectionBase implements HttpClientConnection {
private static final Logger log = LoggerFactory.getLogger(Http1xClientConnection.class);
private static final Handler INVALID_MSG_HANDLER = msg -> {
ReferenceCountUtil.release(msg);
throw new IllegalStateException("Invalid object " + msg);
};
private final HttpClientImpl client;
private final HttpClientOptions options;
private final boolean ssl;
private final SocketAddress server;
public final ClientMetrics metrics;
private final HttpVersion version;
private final long lowWaterMark;
private final long highWaterMark;
private Deque requests = new ArrayDeque<>();
private Deque responses = new ArrayDeque<>();
private boolean closed;
private boolean evicted;
private Handler evictionHandler = DEFAULT_EVICTION_HANDLER;
private Handler invalidMessageHandler = INVALID_MSG_HANDLER;
private boolean close;
private boolean shutdown;
private long shutdownTimerID = -1L;
private boolean isConnect;
private int keepAliveTimeout;
private long expirationTimestamp;
private int seq = 1;
private long readWindow;
private long writeWindow;
private boolean writeOverflow;
private long lastResponseReceivedTimestamp;
Http1xClientConnection(HttpVersion version,
HttpClientImpl client,
ChannelHandlerContext channel,
boolean ssl,
SocketAddress server,
ContextInternal context,
ClientMetrics metrics) {
super(context, channel);
this.client = client;
this.options = client.options();
this.ssl = ssl;
this.server = server;
this.metrics = metrics;
this.version = version;
this.readWindow = 0L;
this.writeWindow = 0L;
this.highWaterMark = channel.channel().config().getWriteBufferHighWaterMark();
this.lowWaterMark = channel.channel().config().getWriteBufferLowWaterMark();
this.keepAliveTimeout = options.getKeepAliveTimeout();
this.expirationTimestamp = expirationTimestampOf(keepAliveTimeout);
}
@Override
public HttpClientConnection evictionHandler(Handler handler) {
evictionHandler = handler;
return this;
}
@Override
public HttpClientConnection concurrencyChangeHandler(Handler handler) {
// Never changes
return this;
}
@Override
public long concurrency() {
return options.isPipelining() ? options.getPipeliningLimit() : 1;
}
/**
* @return a raw {@code NetSocket} - for internal use
*/
public NetSocketInternal toNetSocket() {
removeChannelHandlers();
NetSocketImpl socket = new NetSocketImpl(context, chctx, null, metrics(), false);
socket.metric(metric());
evictionHandler.handle(null);
chctx.pipeline().replace("handler", "handler", VertxHandler.create(ctx -> socket));
return socket;
}
private HttpRequest createRequest(
HttpMethod method,
String uri,
MultiMap headerMap,
String authority,
boolean chunked,
ByteBuf buf,
boolean end) {
HttpRequest request = new DefaultHttpRequest(HttpUtils.toNettyHttpVersion(version), method.toNetty(), uri, false);
HttpHeaders headers = request.headers();
if (headerMap != null) {
for (Map.Entry header : headerMap) {
headers.add(header.getKey(), header.getValue());
}
}
if (!headers.contains(HOST)) {
request.headers().set(HOST, authority);
} else {
headers.remove(TRANSFER_ENCODING);
}
if (chunked) {
HttpUtil.setTransferEncodingChunked(request, true);
}
if (options.isTryUseCompression() && request.headers().get(ACCEPT_ENCODING) == null) {
// if compression should be used but nothing is specified by the user support deflate and gzip.
CharSequence acceptEncoding = determineCompressionAcceptEncoding();
request.headers().set(ACCEPT_ENCODING, acceptEncoding);
}
if (!options.isKeepAlive() && options.getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_1) {
request.headers().set(CONNECTION, CLOSE);
} else if (options.isKeepAlive() && options.getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_0) {
request.headers().set(CONNECTION, KEEP_ALIVE);
}
if (end) {
if (buf != null) {
request = new AssembledFullHttpRequest(request, buf);
} else {
request = new AssembledFullHttpRequest(request);
}
} else if (buf != null) {
request = new AssembledHttpRequest(request, buf);
}
return request;
}
static CharSequence determineCompressionAcceptEncoding() {
if (isBrotliAvailable()) {
return DEFLATE_GZIP_BR;
} else {
return DEFLATE_GZIP;
}
}
// Encapsulated in a method, so GraalVM can substitute it
private static boolean isBrotliAvailable() {
return Brotli.isAvailable();
}
private void beginRequest(Stream stream, HttpRequestHead request, boolean chunked, ByteBuf buf, boolean end, boolean connect, Handler> handler) {
request.id = stream.id;
request.remoteAddress = remoteAddress();
stream.bytesWritten += buf != null ? buf.readableBytes() : 0L;
HttpRequest nettyRequest = createRequest(request.method, request.uri, request.headers, request.authority, chunked, buf, end);
synchronized (this) {
responses.add(stream);
this.isConnect = connect;
if (this.metrics != null) {
stream.metric = this.metrics.requestBegin(request.uri, request);
}
VertxTracer tracer = context.tracer();
if (tracer != null) {
BiConsumer headers = (key, val) -> new HeadersAdaptor(nettyRequest.headers()).add(key, val);
String operation = request.traceOperation;
if (operation == null) {
operation = request.method.name();
}
stream.trace = tracer.sendRequest(stream.context, SpanKind.RPC, options.getTracingPolicy(), request, operation, headers, HttpUtils.CLIENT_HTTP_REQUEST_TAG_EXTRACTOR);
}
}
writeToChannel(nettyRequest, handler == null ? null : context.promise(handler));
if (end) {
endRequest(stream);
}
}
private void writeBuffer(Stream s, ByteBuf buff, boolean end, FutureListener listener) {
s.bytesWritten += buff != null ? buff.readableBytes() : 0L;
Object msg;
if (isConnect) {
msg = buff != null ? buff : Unpooled.EMPTY_BUFFER;
if (end) {
writeToChannel(msg, channelFuture()
.addListener(listener)
.addListener(v -> close())
);
} else {
writeToChannel(msg);
}
} else {
if (end) {
if (buff != null && buff.isReadable()) {
msg = new DefaultLastHttpContent(buff, false);
} else {
msg = LastHttpContent.EMPTY_LAST_CONTENT;
}
} else {
msg = new DefaultHttpContent(buff);
}
writeToChannel(msg, listener);
if (end) {
endRequest(s);
}
}
}
private void endRequest(Stream s) {
Stream next;
boolean checkLifecycle;
synchronized (this) {
requests.pop();
next = requests.peek();
checkLifecycle = s.responseEnded;
if (metrics != null) {
metrics.requestEnd(s.metric, s.bytesWritten);
}
}
flushBytesWritten();
if (next != null) {
next.promise.complete((HttpClientStream) next);
}
if (checkLifecycle) {
checkLifecycle();
}
}
/**
* Resets the given {@code stream}.
*
* @param stream to reset
* @return whether the stream should be considered as closed
*/
private boolean reset(Stream stream) {
boolean inflight;
synchronized (this) {
inflight = responses.contains(stream) || stream.responseEnded;
if (!inflight) {
requests.remove(stream);
}
close = inflight;
}
checkLifecycle();
return !inflight;
}
private void receiveBytes(int len) {
boolean le = readWindow <= highWaterMark;
readWindow += len;
boolean gt = readWindow > highWaterMark;
if (le && gt) {
doPause();
}
}
private void ackBytes(int len) {
EventLoop eventLoop = context.nettyEventLoop();
if (eventLoop.inEventLoop()) {
boolean gt = readWindow > lowWaterMark;
readWindow -= len;
boolean le = readWindow <= lowWaterMark;
if (gt && le) {
doResume();
}
} else {
eventLoop.execute(() -> ackBytes(len));
}
}
private abstract static class Stream {
protected final Promise promise;
protected final ContextInternal context;
protected final int id;
private Object trace;
private Object metric;
private HttpResponseHead response;
private boolean responseEnded;
private long bytesRead;
private long bytesWritten;
Stream(ContextInternal context, int id) {
this.context = context;
this.id = id;
this.promise = context.promise();
}
// Not really elegant... but well
Object metric() {
return metric;
}
Object trace() {
return trace;
}
abstract void handleContinue();
abstract void handleEarlyHints(MultiMap headers);
abstract void handleHead(HttpResponseHead response);
abstract void handleChunk(Buffer buff);
abstract void handleEnd(LastHttpContent trailer);
abstract void handleWritabilityChanged(boolean writable);
abstract void handleException(Throwable cause);
abstract void handleClosed();
}
/**
* We split the stream class in two classes so that the base {@link #Stream} class defines the (mutable)
* state managed by the connection and this class defines the state managed by the stream implementation
*/
private static class StreamImpl extends Stream implements HttpClientStream {
private final Http1xClientConnection conn;
private final InboundBuffer queue;
private boolean reset;
private boolean closed;
private HttpRequestHead request;
private Handler headHandler;
private Handler chunkHandler;
private Handler endHandler;
private Handler drainHandler;
private Handler continueHandler;
private Handler earlyHintsHandler;
private Handler exceptionHandler;
private Handler closeHandler;
StreamImpl(ContextInternal context, Http1xClientConnection conn, int id) {
super(context, id);
this.conn = conn;
this.queue = new InboundBuffer<>(context, 5)
.handler(item -> {
if (!reset) {
if (item instanceof MultiMap) {
Handler handler = endHandler;
if (handler != null) {
handler.handle((MultiMap) item);
}
} else {
Buffer buffer = (Buffer) item;
int len = buffer.length();
conn.ackBytes(len);
Handler handler = chunkHandler;
if (handler != null) {
handler.handle(buffer);
}
}
}
})
.exceptionHandler(context::reportException);
}
@Override
public void continueHandler(Handler handler) {
continueHandler = handler;
}
@Override
public void earlyHintsHandler(Handler handler) {
earlyHintsHandler = handler;
}
@Override
public StreamImpl drainHandler(Handler handler) {
drainHandler = handler;
return this;
}
@Override
public StreamImpl exceptionHandler(Handler handler) {
exceptionHandler = handler;
return this;
}
@Override
public WriteStream setWriteQueueMaxSize(int maxSize) {
return null;
}
@Override
public boolean writeQueueFull() {
return false;
}
@Override
public void headHandler(Handler handler) {
this.headHandler = handler;
}
@Override
public void closeHandler(Handler handler) {
closeHandler = handler;
}
@Override
public void priorityHandler(Handler handler) {
// No op
}
@Override
public void pushHandler(Handler handler) {
// No op
}
@Override
public void unknownFrameHandler(Handler handler) {
// No op
}
@Override
public int id() {
return id;
}
@Override
public Object metric() {
return super.metric();
}
@Override
public Object trace() {
return super.trace();
}
@Override
public HttpVersion version() {
return conn.version;
}
@Override
public HttpClientConnection connection() {
return conn;
}
@Override
public ContextInternal getContext() {
return context;
}
@Override
public void writeHead(HttpRequestHead request, boolean chunked, ByteBuf buf, boolean end, StreamPriority priority, boolean connect, Handler> handler) {
writeHead(request, chunked, buf, end, connect, handler == null ? null : context.promise(handler));
}
private void writeHead(HttpRequestHead request, boolean chunked, ByteBuf buf, boolean end, boolean connect, Handler> handler) {
EventLoop eventLoop = conn.context.nettyEventLoop();
if (eventLoop.inEventLoop()) {
this.request = request;
conn.beginRequest(this, request, chunked, buf, end, connect, handler);
} else {
eventLoop.execute(() -> writeHead(request, chunked, buf, end, connect, handler));
}
}
@Override
public void writeBuffer(ByteBuf buff, boolean end, Handler> handler) {
if (buff != null || end) {
FutureListener listener = handler == null ? null : context.promise(handler);
writeBuffer(buff, end, listener);
}
}
private void writeBuffer(ByteBuf buff, boolean end, FutureListener listener) {
FutureListener l;
if (buff != null) {
int size = buff.readableBytes();
l = future -> {
Handler drain;
synchronized (conn) {
conn.writeWindow -= size;
if (conn.writeOverflow && conn.writeWindow < conn.lowWaterMark) {
drain = drainHandler;
conn.writeOverflow = false;
} else {
drain = null;
}
}
if (drain != null) {
context.emit(drain);
}
if (listener != null) {
listener.operationComplete(future);
}
};
synchronized (conn) {
conn.writeWindow += size;
if (conn.writeWindow > conn.highWaterMark) {
conn.writeOverflow = true;
}
}
} else {
l = listener;
}
EventLoop eventLoop = conn.context.nettyEventLoop();
if (eventLoop.inEventLoop()) {
conn.writeBuffer(this, buff, end, l);
} else {
eventLoop.execute(() -> writeBuffer(buff, end, l));
}
}
@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
public void doSetWriteQueueMaxSize(int size) {
conn.doSetWriteQueueMaxSize(size);
}
@Override
public boolean isNotWritable() {
synchronized (conn) {
return conn.writeWindow > conn.highWaterMark;
}
}
@Override
public void doPause() {
queue.pause();
}
@Override
public void doFetch(long amount) {
queue.fetch(amount);
}
@Override
public void reset(Throwable cause) {
synchronized (conn) {
if (reset) {
return;
}
reset = true;
}
EventLoop eventLoop = conn.context.nettyEventLoop();
if (eventLoop.inEventLoop()) {
_reset(cause);
} else {
eventLoop.execute(() -> _reset(cause));
}
}
private void _reset(Throwable cause) {
boolean removed = conn.reset(this);
context.execute(cause, this::handleException);
if (removed) {
context.execute(this::handleClosed);
}
}
@Override
public StreamPriority priority() {
return null;
}
@Override
public void updatePriority(StreamPriority streamPriority) {
}
@Override
void handleWritabilityChanged(boolean writable) {
}
void handleContinue() {
if (continueHandler != null) {
continueHandler.handle(null);
}
}
void handleEarlyHints(MultiMap headers) {
if (earlyHintsHandler != null) {
earlyHintsHandler.handle(headers);
}
}
@Override
void handleHead(HttpResponseHead response) {
Handler handler = headHandler;
if (handler != null) {
context.emit(response, handler);
}
}
@Override
public void chunkHandler(Handler handler) {
chunkHandler = handler;
}
@Override
public void endHandler(Handler handler) {
endHandler = handler;
}
void handleChunk(Buffer buff) {
queue.write(buff);
}
void handleEnd(LastHttpContent trailer) {
queue.write(new HeadersAdaptor(trailer.trailingHeaders()));
tryClose();
}
void handleException(Throwable cause) {
if (exceptionHandler != null) {
exceptionHandler.handle(cause);
}
}
@Override
void handleClosed() {
handleException(HttpUtils.CONNECTION_CLOSED_EXCEPTION);
tryClose();
}
/**
* Attempt to close the stream.
*/
private void tryClose() {
if (!closed) {
closed = true;
if (closeHandler != null) {
closeHandler.handle(null);
}
}
}
}
private void checkLifecycle() {
if (close || (shutdown && requests.isEmpty() && responses.isEmpty())) {
close();
} else if (!isConnect) {
expirationTimestamp = expirationTimestampOf(keepAliveTimeout);
}
}
@Override
public Future close() {
if (!evicted) {
evicted = true;
if (evictionHandler != null) {
evictionHandler.handle(null);
}
}
return super.close();
}
private Throwable validateMessage(Object msg) {
if (msg instanceof HttpObject) {
HttpObject obj = (HttpObject) msg;
DecoderResult result = obj.decoderResult();
if (result.isFailure()) {
return result.cause();
} else if (obj instanceof io.netty.handler.codec.http.HttpResponse) {
io.netty.handler.codec.http.HttpVersion version = ((com.arangodb.shaded.netty.handler.codec.http.HttpResponse) obj).protocolVersion();
if (version != io.netty.handler.codec.http.HttpVersion.HTTP_1_0 && version != io.netty.handler.codec.http.HttpVersion.HTTP_1_1) {
return new IllegalStateException("Unsupported HTTP version: " + version);
}
}
}
return null;
}
public void handleMessage(Object msg) {
Throwable error = validateMessage(msg);
if (error != null) {
ReferenceCountUtil.release(msg);
fail(error);
} else if (msg instanceof HttpObject) {
handleHttpMessage((HttpObject) msg);
} else if (msg instanceof ByteBuf && isConnect) {
handleChunk((ByteBuf) msg);
} else if (msg instanceof WebSocketFrame) {
handleWsFrame((WebSocketFrame) msg);
} else {
invalidMessageHandler.handle(msg);
}
}
private void handleHttpMessage(HttpObject obj) {
Stream stream;
synchronized (this) {
stream = responses.peekFirst();
}
if (stream == null) {
fail(new VertxException("Received HTTP message with no request in progress"));
} else if (obj instanceof io.netty.handler.codec.http.HttpResponse) {
io.netty.handler.codec.http.HttpResponse response = (com.arangodb.shaded.netty.handler.codec.http.HttpResponse) obj;
HttpVersion version;
if (response.protocolVersion() == io.netty.handler.codec.http.HttpVersion.HTTP_1_0) {
version = io.vertx.core.http.HttpVersion.HTTP_1_0;
} else {
version = io.vertx.core.http.HttpVersion.HTTP_1_1;
}
handleResponseBegin(stream, new HttpResponseHead(
version,
response.status().code(),
response.status().reasonPhrase(),
new HeadersAdaptor(response.headers())));
} else if (obj instanceof HttpContent) {
HttpContent chunk = (HttpContent) obj;
if (chunk.content().isReadable()) {
handleResponseChunk(stream, chunk.content());
}
if (!isConnect && chunk instanceof LastHttpContent) {
handleResponseEnd(stream, (LastHttpContent) chunk);
}
}
}
private void handleChunk(ByteBuf chunk) {
Stream stream;
synchronized (this) {
stream = responses.peekFirst();
if (stream == null) {
return;
}
}
if (chunk.isReadable()) {
handleResponseChunk(stream, chunk);
}
}
private void handleResponseBegin(Stream stream, HttpResponseHead response) {
// How can we handle future undefined 1xx informational response codes?
if (response.statusCode == HttpResponseStatus.CONTINUE.code()) {
stream.context.execute(null, v -> stream.handleContinue());
} else if (response.statusCode == HttpResponseStatus.EARLY_HINTS.code()) {
stream.context.execute(null, v -> stream.handleEarlyHints(response.headers));
} else {
HttpRequestHead request;
synchronized (this) {
request = ((StreamImpl)stream).request;
stream.response = response;
if (metrics != null) {
metrics.responseBegin(stream.metric, response);
}
//
if (response.statusCode != 100 && request.method != HttpMethod.CONNECT) {
// See https://tools.ietf.org/html/rfc7230#section-6.3
String responseConnectionHeader = response.headers.get(HttpHeaderNames.CONNECTION);
String requestConnectionHeader = request.headers != null ? request.headers.get(HttpHeaderNames.CONNECTION) : null;
// We don't need to protect against concurrent changes on forceClose as it only goes from false -> true
if (HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(responseConnectionHeader) || HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(requestConnectionHeader)) {
// In all cases, if we have a close connection option then we SHOULD NOT treat the connection as persistent
this.close = true;
} else if (response.version == HttpVersion.HTTP_1_0 && !HttpHeaderValues.KEEP_ALIVE.contentEqualsIgnoreCase(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
this.close = true;
}
String keepAliveHeader = response.headers.get(HttpHeaderNames.KEEP_ALIVE);
if (keepAliveHeader != null) {
int timeout = HttpUtils.parseKeepAliveHeaderTimeout(keepAliveHeader);
if (timeout != -1) {
this.keepAliveTimeout = timeout;
}
}
}
}
//
stream.handleHead(response);
if (isConnect) {
if ((request.method == HttpMethod.CONNECT &&
response.statusCode == 200) || (
request.method == HttpMethod.GET &&
request.headers != null && request.headers.contains(CONNECTION, UPGRADE, true) &&
response.statusCode == 101)) {
removeChannelHandlers();
} else {
isConnect = false;
}
}
}
}
/**
* Remove all HTTP channel handlers of this connection
*
* @return the messages emitted by the removed handlers during their removal
*/
private void removeChannelHandlers() {
ChannelPipeline pipeline = chctx.pipeline();
ChannelHandler inflater = pipeline.get(HttpContentDecompressor.class);
if (inflater != null) {
pipeline.remove(inflater);
}
// removing this codec might fire pending buffers in the HTTP decoder
// this happens when the channel reads the HTTP response and the following data in a single buffer
Handler prev = invalidMessageHandler;
invalidMessageHandler = msg -> {
ReferenceCountUtil.release(msg);
};
try {
pipeline.remove("codec");
} finally {
invalidMessageHandler = prev;
}
}
private void handleResponseChunk(Stream stream, ByteBuf chunk) {
Buffer buff = Buffer.buffer(VertxHandler.safeBuffer(chunk));
int len = buff.length();
receiveBytes(len);
stream.bytesRead += len;
stream.context.execute(buff, stream::handleChunk);
}
private void handleResponseEnd(Stream stream, LastHttpContent trailer) {
boolean check;
synchronized (this) {
if (stream.response == null) {
// 100-continue
return;
}
responses.pop();
close |= !options.isKeepAlive();
stream.responseEnded = true;
check = requests.peek() != stream;
}
VertxTracer tracer = context.tracer();
if (tracer != null) {
tracer.receiveResponse(stream.context, stream.response, stream.trace, null, HttpUtils.CLIENT_RESPONSE_TAG_EXTRACTOR);
}
if (metrics != null) {
metrics.responseEnd(stream.metric, stream.bytesRead);
}
flushBytesRead();
if (check) {
checkLifecycle();
}
lastResponseReceivedTimestamp = System.currentTimeMillis();
stream.context.execute(trailer, stream::handleEnd);
}
public HttpClientMetrics metrics() {
return client.metrics();
}
synchronized void toWebSocket(
ContextInternal context,
String requestURI,
MultiMap headers,
boolean allowOriginHeader,
WebsocketVersion vers,
List subProtocols,
long handshakeTimeout,
boolean registerWriteHandlers,
int maxWebSocketFrameSize,
Promise promise) {
try {
URI wsuri = new URI(requestURI);
if (!wsuri.isAbsolute()) {
// Netty requires an absolute url
wsuri = new URI((ssl ? "https:" : "http:") + "//" + server.host() + ":" + server.port() + requestURI);
}
WebSocketVersion version =
WebSocketVersion.valueOf((vers == null ?
WebSocketVersion.V13 : vers).toString());
HttpHeaders nettyHeaders;
if (headers != null) {
nettyHeaders = new DefaultHttpHeaders();
for (Map.Entry entry: headers) {
nettyHeaders.add(entry.getKey(), entry.getValue());
}
} else {
nettyHeaders = null;
}
long timer;
if (handshakeTimeout > 0L) {
timer = vertx.setTimer(handshakeTimeout, id -> {
close();
});
} else {
timer = -1;
}
ChannelPipeline p = chctx.channel().pipeline();
ArrayList extensionHandshakers = initializeWebSocketExtensionHandshakers(client.options());
if (!extensionHandshakers.isEmpty()) {
p.addBefore("handler", "webSocketsExtensionsHandler", new WebSocketClientExtensionHandler(
extensionHandshakers.toArray(new WebSocketClientExtensionHandshaker[0])));
}
String subp = null;
if (subProtocols != null) {
subp = String.join(",", subProtocols);
}
WebSocketClientHandshaker handshaker = newHandshaker(
wsuri,
version,
subp,
!extensionHandshakers.isEmpty(),
allowOriginHeader,
nettyHeaders,
maxWebSocketFrameSize,
!options.isSendUnmaskedFrames());
Handler> webSocketHandshakeComplete = ar -> {
if (timer > 0L) {
vertx.cancelTimer(timer);
}
if (ar.failed()) {
close();
promise.fail(ar.cause());
} else {
WebSocketImpl ws = finish(context, version, registerWriteHandlers, handshaker, ar.result());
webSocket = ws;
getContext().emit(ws, w -> {
promise.handle(Future.succeededFuture(w));
webSocket.headers(null);
});
}
};
WebSocketHandshakeInboundHandler handshakeInboundHandler = new WebSocketHandshakeInboundHandler(handshaker, webSocketHandshakeComplete);
p.addBefore("handler", "handshakeCompleter", handshakeInboundHandler);
} catch (Exception e) {
handleException(e);
}
}
private WebSocketImpl finish(ContextInternal context,
WebSocketVersion version,
boolean registerWriteHandlers,
WebSocketClientHandshaker handshaker,
MultiMap headers) {
WebSocketImpl ws = new WebSocketImpl(
context,
Http1xClientConnection.this,
version != V00,
options.getWebSocketClosingTimeout(),
options.getMaxWebSocketFrameSize(),
options.getMaxWebSocketMessageSize(),
registerWriteHandlers);
ws.subProtocol(handshaker.actualSubprotocol());
ws.registerHandler(vertx.eventBus());
log.debug("WebSocket handshake complete");
HttpClientMetrics metrics = client.metrics();
if (metrics != null) {
ws.setMetric(metrics.connected(ws));
}
ws.headers(headers);
return ws;
}
static WebSocketClientHandshaker newHandshaker(
URI webSocketURL, WebSocketVersion version, String subprotocol,
boolean allowExtensions, boolean allowOriginHeader, HttpHeaders customHeaders, int maxFramePayloadLength,
boolean performMasking) {
WebSocketDecoderConfig config = WebSocketDecoderConfig.newBuilder()
.expectMaskedFrames(false)
.allowExtensions(allowExtensions)
.maxFramePayloadLength(maxFramePayloadLength)
.allowMaskMismatch(false)
.closeOnProtocolViolation(false)
.build();
if (version == V13) {
return new WebSocketClientHandshaker13(
webSocketURL, V13, subprotocol, allowExtensions, customHeaders,
maxFramePayloadLength, performMasking, false, -1) {
@Override
protected WebSocketFrameDecoder newWebsocketDecoder() {
return new WebSocket13FrameDecoder(config);
}
@Override
protected FullHttpRequest newHandshakeRequest() {
FullHttpRequest request = super.newHandshakeRequest();
if (!allowOriginHeader) {
request.headers().remove(ORIGIN);
}
return request;
}
};
}
if (version == V08) {
return new WebSocketClientHandshaker08(
webSocketURL, V08, subprotocol, allowExtensions, customHeaders,
maxFramePayloadLength, performMasking, false, -1) {
@Override
protected WebSocketFrameDecoder newWebsocketDecoder() {
return new WebSocket08FrameDecoder(config);
}
@Override
protected FullHttpRequest newHandshakeRequest() {
FullHttpRequest request = super.newHandshakeRequest();
if (!allowOriginHeader) {
request.headers().remove(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN);
}
return request;
}
};
}
if (version == V07) {
return new WebSocketClientHandshaker07(
webSocketURL, V07, subprotocol, allowExtensions, customHeaders,
maxFramePayloadLength, performMasking, false, -1) {
@Override
protected WebSocketFrameDecoder newWebsocketDecoder() {
return new WebSocket07FrameDecoder(config);
}
@Override
protected FullHttpRequest newHandshakeRequest() {
FullHttpRequest request = super.newHandshakeRequest();
if (!allowOriginHeader) {
request.headers().remove(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN);
}
return request;
}
};
}
if (version == V00) {
return new WebSocketClientHandshaker00(
webSocketURL, V00, subprotocol, customHeaders, maxFramePayloadLength, -1) {
@Override
protected FullHttpRequest newHandshakeRequest() {
FullHttpRequest request = super.newHandshakeRequest();
if (!allowOriginHeader) {
request.headers().remove(ORIGIN);
}
return request;
}
};
}
throw new WebSocketHandshakeException("Protocol version " + version + " not supported.");
}
ArrayList initializeWebSocketExtensionHandshakers(HttpClientOptions options) {
ArrayList extensionHandshakers = new ArrayList<>();
if (options.getTryWebSocketDeflateFrameCompression()) {
extensionHandshakers.add(new DeflateFrameClientExtensionHandshaker(options.getWebSocketCompressionLevel(),
false));
}
if (options.getTryUsePerMessageWebSocketCompression()) {
extensionHandshakers.add(new PerMessageDeflateClientExtensionHandshaker(options.getWebSocketCompressionLevel(),
ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE,
options.getWebSocketCompressionAllowClientNoContext(), options.getWebSocketCompressionRequestServerNoContext()));
}
return extensionHandshakers;
}
@Override
public void handleInterestedOpsChanged() {
boolean writable = !isNotWritable();
ContextInternal context;
Handler handler;
synchronized (this) {
Stream current = requests.peek();
if (current != null) {
context = current.context;
handler = current::handleWritabilityChanged;
} else if (webSocket != null) {
context = webSocket.context;
handler = webSocket::handleWritabilityChanged;
} else {
return;
}
}
context.execute(writable, handler);
}
protected void handleClosed() {
super.handleClosed();
long timerID = shutdownTimerID;
if (timerID != -1) {
shutdownTimerID = -1L;
vertx.cancelTimer(timerID);
}
closed = true;
if (metrics != null) {
HttpClientMetrics met = client.metrics();
met.endpointDisconnected(metrics);
}
if (!evicted) {
evicted = true;
if (evictionHandler != null) {
evictionHandler.handle(null);
}
}
WebSocketImpl ws;
VertxTracer tracer = context.tracer();
List allocatedStreams;
List sentStreams;
synchronized (this) {
ws = webSocket;
sentStreams = new ArrayList<>(responses);
allocatedStreams = new ArrayList<>(requests);
allocatedStreams.removeAll(responses);
}
if (ws != null) {
ws.handleConnectionClosed();
}
for (Stream stream : allocatedStreams) {
stream.context.execute(null, v -> stream.handleClosed());
}
for (Stream stream : sentStreams) {
if (metrics != null) {
metrics.requestReset(stream.metric);
}
Object trace = stream.trace;
if (tracer != null && trace != null) {
tracer.receiveResponse(stream.context, null, trace, HttpUtils.CONNECTION_CLOSED_EXCEPTION, TagExtractor.empty());
}
stream.context.execute(null, v -> stream.handleClosed());
}
}
protected void handleIdle(IdleStateEvent event) {
synchronized (this) {
if (webSocket == null && responses.isEmpty() && requests.isEmpty()) {
return;
}
}
super.handleIdle(event);
}
@Override
protected void handleException(Throwable e) {
super.handleException(e);
WebSocketImpl ws;
LinkedHashSet allStreams = new LinkedHashSet<>();
synchronized (this) {
ws = webSocket;
allStreams.addAll(requests);
allStreams.addAll(responses);
}
if (ws != null) {
ws.handleException(e);
}
for (Stream stream : allStreams) {
stream.handleException(e);
}
}
@Override
public void createStream(ContextInternal context, Handler> handler) {
EventLoop eventLoop = context.nettyEventLoop();
if (eventLoop.inEventLoop()) {
StreamImpl stream;
synchronized (this) {
if (closed) {
stream = null;
} else {
stream = new StreamImpl(context, this, seq++);
requests.add(stream);
if (requests.size() == 1) {
stream.promise.complete(stream);
}
}
}
if (stream != null) {
stream.promise.future().onComplete(handler);
} else {
handler.handle(Future.failedFuture(HttpUtils.CONNECTION_CLOSED_EXCEPTION));
}
} else {
eventLoop.execute(() -> {
createStream(context, handler);
});
}
}
@Override
public long lastResponseReceivedTimestamp() {
return lastResponseReceivedTimestamp;
}
@Override
public boolean isValid() {
return expirationTimestamp == 0 || System.currentTimeMillis() <= expirationTimestamp;
}
@Override
public void shutdown(long timeout, Handler> handler) {
shutdown(timeout, vertx.promise(handler));
}
@Override
public Future shutdown(long timeoutMs) {
PromiseInternal promise = vertx.promise();
shutdown(timeoutMs, promise);
return promise.future();
}
private synchronized void shutdownNow() {
shutdownTimerID = -1L;
close();
}
private void shutdown(long timeoutMs, PromiseInternal promise) {
synchronized (this) {
if (shutdown) {
promise.fail("Already shutdown");
return;
}
shutdown = true;
closeFuture().onComplete(promise);
}
synchronized (this) {
if (!closed) {
if (timeoutMs > 0L) {
shutdownTimerID = context.setTimer(timeoutMs, id -> shutdownNow());
} else {
close = true;
}
}
}
checkLifecycle();
}
/**
* Compute the expiration timeout of the connection, relative to the current time.
*
* @param timeout the timeout
* @return the expiration timestamp
*/
private static long expirationTimestampOf(long timeout) {
return timeout == 0 ? 0L : System.currentTimeMillis() + timeout * 1000;
}
}