/* * Copyright (C) 2015 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.async.AsyncCancellable; import io.datakernel.async.CompletionCallback; import io.datakernel.async.ConnectCallback; import io.datakernel.async.ResultCallback; import io.datakernel.bytebuf.ByteBufStrings; import io.datakernel.dns.AsyncDnsClient; import io.datakernel.dns.IAsyncDnsClient; import io.datakernel.eventloop.AsyncTcpSocket; import io.datakernel.eventloop.AsyncTcpSocketImpl; import io.datakernel.eventloop.Eventloop; import io.datakernel.eventloop.EventloopService; import io.datakernel.jmx.*; import io.datakernel.net.SocketSettings; import io.datakernel.util.MemSize; import javax.net.ssl.SSLContext; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map.Entry; import java.util.concurrent.ExecutorService; import static io.datakernel.eventloop.AsyncSslSocket.wrapClientSocket; import static io.datakernel.eventloop.AsyncTcpSocketImpl.wrapChannel; import static io.datakernel.http.AbstractHttpConnection.*; import static io.datakernel.util.Preconditions.checkState; /** * A client which works asynchronously. It's responsibility is to send requests * to the specified server. Client's work is based on {@link Eventloop}, which * must be provided when calling {@link #create(Eventloop)} method. There are * a lot of methods applicable for configuring a client: * <ul> * <li>{@link #withSocketSettings(SocketSettings)}</li> * <li>{@link #withMaxHttpMessageSize(int)} and overloaded * {@link #withMaxHttpMessageSize(MemSize)}</li> * <li>{@link #withKeepAliveTimeout(long)} or * {@link #withNoKeepAlive()}</li> * <li>{@link #withDnsClient(IAsyncDnsClient)}</li> * <li>{@link #withSslEnabled(SSLContext, ExecutorService)}</li> * </ul> * Assuming that some server (e.g. an {@link AsyncHttpServer async http server} * example) runs on port {@code 40000}, the code for creating an asynchronous * client is straightforward: * <pre> * <code>final {@link Eventloop Eventloop} eventloop = Eventloop.create(); * final AsyncHttpClient client = AsyncHttpClient.create(eventloop); * {@link HttpRequest} r = HttpRequest.get("http://127.0.0.1:" + 40000); * final int timeout = 1000; * client.send(r, timeout, new {@link ResultCallback}<HttpResponse>()} { * {@literal @}Override * public void onResult(final {@link HttpResponse} result) { * System.out.println({@link ByteBufStrings#decodeAscii}(result.getBody()); * client.close(); * } * * {@literal @}Override * public void onException(Exception exception) { * client.close(); * } * }); * eventloop.run(); * </code> * </pre> */ @SuppressWarnings("ThrowableInstanceNeverThrown") public final class AsyncHttpClient implements IAsyncHttpClient, EventloopService, EventloopJmxMBean { public static final SocketSettings DEFAULT_SOCKET_SETTINGS = SocketSettings.create(); public static final long DEFAULT_KEEP_ALIVE_MILLIS = 30 * 1000L; private final Eventloop eventloop; private IAsyncDnsClient asyncDnsClient; private SocketSettings socketSettings = DEFAULT_SOCKET_SETTINGS; int connectionsCount; final HashMap<InetSocketAddress, AddressLinkedList> addresses = new HashMap<>(); final ConnectionsLinkedList poolKeepAlive = new ConnectionsLinkedList(); final ConnectionsLinkedList poolReading = new ConnectionsLinkedList(); final ConnectionsLinkedList poolWriting = new ConnectionsLinkedList(); private int poolKeepAliveExpired; private int poolReadingExpired; private int poolWritingExpired; private final char[] headerChars = new char[MAX_HEADER_LINE_SIZE]; private AsyncCancellable expiredConnectionsCheck; private int maxHttpMessageSize = Integer.MAX_VALUE; // timeouts private int connectTimeoutMillis = 0; int keepAliveTimeoutMillis = (int) DEFAULT_KEEP_ALIVE_MILLIS; private int readTimeoutMillis = 0; private int writeTimeoutMillis = 0; // SSL private SSLContext sslContext; private ExecutorService sslExecutor; protected Inspector inspector = new JmxInspector(); public interface Inspector { AsyncTcpSocketImpl.Inspector socketInspector(HttpRequest httpRequest, InetSocketAddress address, boolean https); void onRequest(HttpRequest request); void onResolve(HttpRequest request, InetAddress[] inetAddresses); void onResolveError(HttpRequest request, Exception e); void onConnect(HttpRequest request, HttpClientConnection connection); void onConnectError(HttpRequest request, InetSocketAddress address, Exception e); void onHttpResponse(HttpClientConnection connection, HttpResponse response); void onHttpError(HttpClientConnection connection, boolean keepAliveConnection, Exception e); } public static class JmxInspector implements Inspector { private static final double SMOOTHING_WINDOW = ValueStats.SMOOTHING_WINDOW_1_MINUTE; protected final AsyncTcpSocketImpl.JmxInspector socketStats = new AsyncTcpSocketImpl.JmxInspector(); protected final AsyncTcpSocketImpl.JmxInspector socketStatsForSSL = new AsyncTcpSocketImpl.JmxInspector(); private final EventStats totalRequests = EventStats.create(SMOOTHING_WINDOW); private final ExceptionStats resolveErrors = ExceptionStats.create(); private final EventStats connected = EventStats.create(SMOOTHING_WINDOW); private final ExceptionStats connectErrors = ExceptionStats.create(); private long responses; private final EventStats httpTimeouts = EventStats.create(SMOOTHING_WINDOW); private final ExceptionStats httpErrors = ExceptionStats.create(); private long responsesErrors; @Override public AsyncTcpSocketImpl.Inspector socketInspector(HttpRequest httpRequest, InetSocketAddress address, boolean https) { return https ? socketStatsForSSL : socketStats; } @Override public void onRequest(HttpRequest request) { totalRequests.recordEvent(); } @Override public void onResolve(HttpRequest request, InetAddress[] inetAddresses) { } @Override public void onResolveError(HttpRequest request, Exception e) { resolveErrors.recordException(e, request.getUrl().getHost()); } @Override public void onConnect(HttpRequest request, HttpClientConnection connection) { connected.recordEvent(); } @Override public void onConnectError(HttpRequest request, InetSocketAddress address, Exception e) { connectErrors.recordException(e, request.getUrl().getHost()); } @Override public void onHttpResponse(HttpClientConnection connection, HttpResponse response) { responses++; } @Override public void onHttpError(HttpClientConnection connection, boolean keepAliveConnection, Exception e) { if (e == AbstractHttpConnection.READ_TIMEOUT_ERROR || e == AbstractHttpConnection.WRITE_TIMEOUT_ERROR) { httpTimeouts.recordEvent(); } else { httpErrors.recordException(e); if (!keepAliveConnection) { responsesErrors++; } } } @JmxAttribute public AsyncTcpSocketImpl.JmxInspector getSocketStats() { return socketStats; } @JmxAttribute(extraSubAttributes = "totalCount", description = "all requests that were sent (both successful and failed)") public EventStats getTotalRequests() { return totalRequests; } @JmxAttribute public ExceptionStats getResolveErrors() { return resolveErrors; } @JmxAttribute(description = "number of \"open connection\" events)") public EventStats getConnected() { return connected; } @JmxAttribute public EventStats getHttpTimeouts() { return httpTimeouts; } @JmxAttribute public ExceptionStats getHttpErrors() { return httpErrors; } @JmxAttribute(reducer = JmxReducers.JmxReducerSum.class) public long getActiveRequests() { return totalRequests.getTotalCount() - (resolveErrors.getTotal() + connectErrors.getTotal() + responsesErrors + responses); } @JmxAttribute(reducer = JmxReducers.JmxReducerSum.class) public long getTotalResponses() { return responses; } } private int inetAddressIdx = 0; // region builders private AsyncHttpClient(Eventloop eventloop, IAsyncDnsClient asyncDnsClient) { this.eventloop = eventloop; this.asyncDnsClient = asyncDnsClient; } public static AsyncHttpClient create(Eventloop eventloop) { IAsyncDnsClient defaultDnsClient = AsyncDnsClient.create(eventloop); return new AsyncHttpClient(eventloop, defaultDnsClient); } public AsyncHttpClient withSocketSettings(SocketSettings socketSettings) { this.socketSettings = socketSettings; return this; } public AsyncHttpClient withDnsClient(IAsyncDnsClient asyncDnsClient) { this.asyncDnsClient = asyncDnsClient; return this; } public AsyncHttpClient withSslEnabled(SSLContext sslContext, ExecutorService sslExecutor) { this.sslContext = sslContext; this.sslExecutor = sslExecutor; return this; } public AsyncHttpClient withKeepAliveTimeout(long keepAliveTimeMillis) { this.keepAliveTimeoutMillis = (int) keepAliveTimeMillis; return this; } public AsyncHttpClient withNoKeepAlive() { return withKeepAliveTimeout(0); } public AsyncHttpClient withReadTimeout(long readTimeoutMillis) { this.readTimeoutMillis = (int) readTimeoutMillis; return this; } public AsyncHttpClient withWriteTimeout(long writeTimeoutMillis) { this.writeTimeoutMillis = (int) writeTimeoutMillis; return this; } public AsyncHttpClient withConnectTimeout(long connectTimeoutMillis) { this.connectTimeoutMillis = (int) connectTimeoutMillis; return this; } public AsyncHttpClient withMaxHttpMessageSize(int maxHttpMessageSize) { this.maxHttpMessageSize = maxHttpMessageSize; return this; } public AsyncHttpClient withMaxHttpMessageSize(MemSize maxHttpMessageSize) { return withMaxHttpMessageSize((int) maxHttpMessageSize.get()); } public AsyncHttpClient withInspector(Inspector inspector) { this.inspector = inspector; return this; } // endregion private void scheduleExpiredConnectionsCheck() { assert expiredConnectionsCheck == null; expiredConnectionsCheck = eventloop.scheduleBackground(eventloop.currentTimeMillis() + 1000L, new Runnable() { @Override public void run() { expiredConnectionsCheck = null; poolKeepAliveExpired += poolKeepAlive.closeExpiredConnections(eventloop.currentTimeMillis() - keepAliveTimeoutMillis); if (readTimeoutMillis != 0) poolReadingExpired += poolReading.closeExpiredConnections(eventloop.currentTimeMillis() - readTimeoutMillis, READ_TIMEOUT_ERROR); if (writeTimeoutMillis != 0) poolWritingExpired += poolWriting.closeExpiredConnections(eventloop.currentTimeMillis() - writeTimeoutMillis, WRITE_TIMEOUT_ERROR); if (connectionsCount != 0) scheduleExpiredConnectionsCheck(); } }); } private HttpClientConnection takeKeepAliveConnection(InetSocketAddress address) { AddressLinkedList addresses = this.addresses.get(address); if (addresses == null) return null; HttpClientConnection connection = addresses.removeLastNode(); assert connection.pool == poolKeepAlive; assert connection.remoteAddress.equals(address); connection.pool.removeNode(connection); connection.pool = null; if (addresses.isEmpty()) { this.addresses.remove(address); } return connection; } void returnToKeepAlivePool(HttpClientConnection connection) { assert !connection.isClosed(); AddressLinkedList addresses = this.addresses.get(connection.remoteAddress); if (addresses == null) { addresses = new AddressLinkedList(); this.addresses.put(connection.remoteAddress, addresses); } addresses.addLastNode(connection); assert connection.pool == poolReading; poolReading.removeNode(connection); (connection.pool = poolKeepAlive).addLastNode(connection); connection.poolTimestamp = eventloop.currentTimeMillis(); if (expiredConnectionsCheck == null) { scheduleExpiredConnectionsCheck(); } } /** * Sends the request to server, waits the result timeout and handles result with callback * * @param request request for server * @param callback callback for handling result */ @Override public void send(final HttpRequest request, final ResultCallback<HttpResponse> callback) { assert eventloop.inEventloopThread(); if (inspector != null) inspector.onRequest(request); String host = request.getUrl().getHost(); asyncDnsClient.resolve4(host, new ResultCallback<InetAddress[]>() { @Override public void onResult(InetAddress[] inetAddresses) { if (inspector != null) inspector.onResolve(request, inetAddresses); doSend(request, inetAddresses, callback); } @Override protected void onException(Exception e) { if (inspector != null) inspector.onResolveError(request, e); request.recycleBufs(); callback.setException(e); } }); } private void doSend(final HttpRequest request, final InetAddress[] inetAddresses, final ResultCallback<HttpResponse> callback) { final InetAddress inetAddress = inetAddresses[((inetAddressIdx++) & Integer.MAX_VALUE) % inetAddresses.length]; final InetSocketAddress address = new InetSocketAddress(inetAddress, request.getUrl().getPort()); HttpClientConnection connection = takeKeepAliveConnection(address); if (connection != null) { connection.send(request, callback); return; } eventloop.connect(address, connectTimeoutMillis, new ConnectCallback() { @Override public void onConnect(SocketChannel socketChannel) { boolean https = request.isHttps(); AsyncTcpSocketImpl asyncTcpSocketImpl = wrapChannel(eventloop, socketChannel, socketSettings) .withInspector(inspector == null ? null : inspector.socketInspector(request, address, https)); if (https && sslContext == null) { throw new IllegalArgumentException("Cannot send HTTPS Request without SSL enabled"); } AsyncTcpSocket asyncTcpSocket = https ? wrapClientSocket(eventloop, asyncTcpSocketImpl, request.getUrl().getHost(), request.getUrl().getPort(), sslContext, sslExecutor) : asyncTcpSocketImpl; HttpClientConnection connection = new HttpClientConnection(eventloop, address, asyncTcpSocket, AsyncHttpClient.this, headerChars, maxHttpMessageSize); asyncTcpSocket.setEventHandler(connection); asyncTcpSocketImpl.register(); if (inspector != null) inspector.onConnect(request, connection); connectionsCount++; if (expiredConnectionsCheck == null) scheduleExpiredConnectionsCheck(); // connection was unexpectedly closed by the peer if (connection.getCloseError() != null) { callback.setException(connection.getCloseError()); return; } connection.send(request, callback); } @Override public void onException(Exception e) { if (inspector != null) inspector.onConnectError(request, address, e); request.recycleBufs(); callback.setException(e); } @Override public String toString() { return "ConnectCallback for address: " + address.toString(); } }); } @Override public Eventloop getEventloop() { return eventloop; } @Override public void start(final CompletionCallback callback) { checkState(eventloop.inEventloopThread()); callback.setComplete(); } private CompletionCallback closeCallback; public void onConnectionClosed() { connectionsCount--; if (connectionsCount == 0 && closeCallback != null) { closeCallback.postComplete(eventloop); closeCallback = null; } } @Override public void stop(final CompletionCallback callback) { checkState(eventloop.inEventloopThread()); poolKeepAlive.closeAllConnections(); assert addresses.isEmpty(); keepAliveTimeoutMillis = 0; if (connectionsCount == 0) { assert poolReading.isEmpty() && poolWriting.isEmpty(); callback.postComplete(eventloop); } else { this.closeCallback = callback; } } // region jmx @JmxAttribute(description = "current number of connections", reducer = JmxReducers.JmxReducerSum.class) public int getConnectionsCount() { return connectionsCount; } @JmxAttribute(reducer = JmxReducers.JmxReducerSum.class) public int getConnectionsKeepAliveCount() { return poolKeepAlive.size(); } @JmxAttribute(reducer = JmxReducers.JmxReducerSum.class) public int getConnectionsReadingCount() { return poolReading.size(); } @JmxAttribute(reducer = JmxReducers.JmxReducerSum.class) public int getConnectionsWritingCount() { return poolWriting.size(); } @JmxAttribute(reducer = JmxReducers.JmxReducerSum.class) public int getConnectionsKeepAliveExpired() { return poolKeepAliveExpired; } @JmxAttribute(reducer = JmxReducers.JmxReducerSum.class) public int getConnectionsReadingExpired() { return poolReadingExpired; } @JmxAttribute(reducer = JmxReducers.JmxReducerSum.class) public int getConnectionsWritingExpired() { return poolWritingExpired; } @JmxAttribute(description = "number of connections per address") public List<String> getAddressConnections() { if (addresses.isEmpty()) return null; List<String> result = new ArrayList<>(); result.add("SocketAddress,ConnectionsCount"); for (Entry<InetSocketAddress, AddressLinkedList> entry : addresses.entrySet()) { InetSocketAddress address = entry.getKey(); AddressLinkedList connections = entry.getValue(); result.add(address + "," + connections.size()); } return result; } @JmxAttribute(name = "") public JmxInspector getStats() { return (inspector instanceof JmxInspector ? (JmxInspector) inspector : null); } // endregion }