All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.datakernel.http.HttpClientConnection Maven / Gradle / Ivy

/*
 * Copyright (C) 2015-2018 SoftIndex LLC.
 *
 * 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.datakernel.http;

import io.datakernel.bytebuf.ByteBuf;
import io.datakernel.common.parse.ParseException;
import io.datakernel.common.parse.UnknownFormatException;
import io.datakernel.csp.ChannelSupplier;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.http.AsyncHttpClient.Inspector;
import io.datakernel.net.AsyncTcpSocket;
import io.datakernel.promise.Promise;
import io.datakernel.promise.SettablePromise;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.net.InetSocketAddress;

import static io.datakernel.bytebuf.ByteBufStrings.SP;
import static io.datakernel.bytebuf.ByteBufStrings.decodePositiveInt;
import static io.datakernel.http.HttpHeaders.CONNECTION;
import static io.datakernel.http.HttpMessage.MUST_LOAD_BODY;
import static java.nio.charset.StandardCharsets.ISO_8859_1;

/**
 * 

* This class is responsible for sending and receiving HTTP requests. * It's made so that one instance of it corresponds to one networking socket. * That's why instances of those classes are all stored in one of three pools in their * respective {@link AsyncHttpClient} instance. *

*

* Those pools are: poolKeepAlive, poolReading, and poolWriting. *

* Path between those pools that any connection takes can be represented as a state machine, * described as a GraphViz graph below. * Nodes with (null) descriptor mean that the connection is not in any pool. *
 * digraph {
 *     label="Single HttpConnection state machine"
 *     rankdir="LR"
 *
 *     "open(null)"
 *     "closed(null)"
 *     "reading"
 *     "writing"
 *     "keep-alive"
 *     "taken(null)"
 *
 *     "writing" -> "closed(null)" [color="#ff8080", style=dashed, label="value reset/write timeout"]
 *     "reading" -> "closed(null)" [color="#ff8080", style=dashed, label="value reset/read timeout"]
 *     "keep-alive" -> "closed(null)" [color="#ff8080", style=dashed, label="value reset"]
 *     "taken(null)" -> "closed(null)" [color="#ff8080", style=dashed, label="value reset"]
 *
 *     "open(null)" -> "writing" [label="send request"]
 *     "writing" -> "reading"
 *     "reading" -> "closed(null)" [label="received response\n(no keep-alive)"]
 *     "reading" -> "keep-alive" [label="received response"]
 *     "keep-alive" -> "taken(null)" [label="reuse connection"]
 *     "taken(null)" -> "writing" [label="send request"]
 *     "keep-alive" -> "closed(null)" [label="expiration"]
 *
 *     { rank=same; "open(null)", "closed(null)" }
 *     { rank=same; "reading", "writing", "keep-alive" }
 * }
 * 
*/ final class HttpClientConnection extends AbstractHttpConnection { public static final ParseException INVALID_RESPONSE = new UnknownFormatException(HttpClientConnection.class, "Invalid response"); public static final ParseException CONNECTION_CLOSED = new ParseException(HttpClientConnection.class, "Connection closed"); @Nullable private SettablePromise promise; @Nullable private HttpResponse response; private final AsyncHttpClient client; @Nullable private final Inspector inspector; final InetSocketAddress remoteAddress; @Nullable HttpClientConnection addressPrev; HttpClientConnection addressNext; final int maxBodySize; HttpClientConnection(Eventloop eventloop, AsyncHttpClient client, AsyncTcpSocket asyncTcpSocket, InetSocketAddress remoteAddress) { super(eventloop, asyncTcpSocket); this.remoteAddress = remoteAddress; this.client = client; this.maxBodySize = client.maxBodySize; this.inspector = client.inspector; } @Override public void onClosedWithError(@NotNull Throwable e) { if (inspector != null) inspector.onHttpError(this, (flags & KEEP_ALIVE) != 0, e); if (promise != null) { SettablePromise promise = this.promise; this.promise = null; promise.setException(e); } } @Override protected void onStartLine(byte[] line, int limit) throws ParseException { boolean http1x = line[0] == 'H' && line[1] == 'T' && line[2] == 'T' && line[3] == 'P' && line[4] == '/' && line[5] == '1'; boolean http11 = line[6] == '.' && line[7] == '1' && line[8] == SP; if (!http1x) throw INVALID_RESPONSE; int sp1; if (http11) { flags |= KEEP_ALIVE; sp1 = 9; } else if (line[6] == '.' && line[7] == '0' && line[8] == SP) { sp1 = 9; } else if (line[6] == SP) { sp1 = 7; } else { throw new ParseException(HttpClientConnection.class, "Invalid response: " + new String(line, 0, limit, ISO_8859_1)); } int sp2; for (sp2 = sp1; sp2 < limit; sp2++) { if (line[sp2] == SP) { break; } } int statusCode = decodePositiveInt(line, sp1, sp2 - sp1); if (!(statusCode >= 100 && statusCode < 600)) { throw new UnknownFormatException(HttpClientConnection.class, "Invalid HTTP Status Code " + statusCode); } response = new HttpResponse(statusCode); response.maxBodySize = maxBodySize; /* RFC 2616, section 4.4 1.Any response message which "MUST NOT" include a message-body (such as the 1xx, 204, and 304 responses and any response to a HEAD request) is always terminated by the first empty line after the header fields, regardless of the entity-header fields present in the message. */ int messageCode = response.getCode(); if ((messageCode >= 100 && messageCode < 200) || messageCode == 204 || messageCode == 304) { // Reset Content-Length for the case keep-alive connection contentLength = 0; } } @Override protected void onHeaderBuf(ByteBuf buf) { //noinspection ConstantConditions response.addHeaderBuf(buf); } @Override protected void onHeader(HttpHeader header, byte[] array, int off, int len) throws ParseException { assert response != null; if (response.headers.size() >= MAX_HEADERS) throw TOO_MANY_HEADERS; response.addHeader(header, array, off, len); } @Override protected void onHeadersReceived(@Nullable ByteBuf body, @Nullable ChannelSupplier bodySupplier) { assert !isClosed(); HttpResponse response = this.response; //noinspection ConstantConditions response.flags |= MUST_LOAD_BODY; response.body = body; response.bodyStream = bodySupplier; if (inspector != null) inspector.onHttpResponse(this, response); SettablePromise promise = this.promise; this.promise = null; //noinspection ConstantConditions promise.set(response); } @Override protected void onBodyReceived() { if (response != null && (flags & (BODY_SENT | BODY_RECEIVED)) == (BODY_SENT | BODY_RECEIVED)) { onHttpMessageComplete(); } } @Override protected void onBodySent() { if (response != null && (flags & (BODY_SENT | BODY_RECEIVED)) == (BODY_SENT | BODY_RECEIVED)) { onHttpMessageComplete(); } } private void onHttpMessageComplete() { assert response != null; response.recycle(); response = null; if ((flags & KEEP_ALIVE) != 0 && client.keepAliveTimeoutMillis != 0) { flags = 0; socket.read() .whenComplete((buf, e) -> { if (e == null) { if (buf != null) { closeWithError(UNEXPECTED_READ); } else { close(); } } else { closeWithError(e); } }); client.returnToKeepAlivePool(this); } else { close(); } } /** * Sends the request, recycles it and closes connection in case of timeout * * @param request request for sending */ public Promise send(HttpRequest request) { SettablePromise promise = new SettablePromise<>(); this.promise = promise; assert pool == null; (pool = client.poolReadWrite).addLastNode(this); poolTimestamp = eventloop.currentTimeMillis(); HttpHeaderValue connectionHeader = CONNECTION_KEEP_ALIVE_HEADER; if (client.maxKeepAliveRequests != 0) { if (++numberOfKeepAliveRequests >= client.maxKeepAliveRequests) { connectionHeader = CONNECTION_CLOSE_HEADER; } } request.addHeader(CONNECTION, connectionHeader); ByteBuf buf = renderHttpMessage(request); if (buf != null) { writeBuf(buf); } else { writeHttpMessageAsStream(request); } request.recycle(); if (!isClosed()) { try { readHttpMessage(); } catch (ParseException e) { closeWithError(e); } } return promise; } /** * After closing this connection it removes it from its connections cache and recycles * Http response. */ @Override protected void onClosed() { if (promise != null) { if (inspector != null) inspector.onHttpError(this, (flags & KEEP_ALIVE) != 0, CONNECTION_CLOSED); SettablePromise promise = this.promise; this.promise = null; promise.setException(CONNECTION_CLOSED); } if (pool == client.poolKeepAlive) { AddressLinkedList addresses = client.addresses.get(remoteAddress); addresses.removeNode(this); if (addresses.isEmpty()) { client.addresses.remove(remoteAddress); } } // pool will be null if socket was closed by the value just before connection.send() invocation // (eg. if connection was in open(null) or taken(null) states) //noinspection ConstantConditions pool.removeNode(this); //noinspection AssertWithSideEffects,ConstantConditions assert (pool = null) == null; client.onConnectionClosed(); if (response != null) { response.recycle(); response = null; } } @Override public String toString() { return "HttpClientConnection{" + "promise=" + promise + ", response=" + response + ", httpClient=" + client + ", keepAlive=" + (pool == client.poolKeepAlive) + // ", lastRequestUrl='" + (request.getFullUrl() == null ? "" : request.getFullUrl()) + '\'' + ", remoteAddress=" + remoteAddress + ',' + super.toString() + '}'; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy