package com.koushikdutta.async.http; import android.net.Uri; import com.koushikdutta.async.ArrayDeque; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.callback.ContinuationCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.Continuation; import com.koushikdutta.async.future.SimpleCancellable; import com.koushikdutta.async.future.TransformFuture; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Hashtable; import java.util.Locale; public class AsyncSocketMiddleware extends SimpleMiddleware { String scheme; int port; // 5 min idle timeout int idleTimeoutMs = 300 * 1000; public AsyncSocketMiddleware(AsyncHttpClient client, String scheme, int port) { mClient = client; this.scheme = scheme; this.port = port; } public void setIdleTimeoutMs(int idleTimeoutMs) { this.idleTimeoutMs = idleTimeoutMs; } public int getSchemePort(Uri uri) { if (uri.getScheme() == null || !uri.getScheme().equals(scheme)) return -1; if (uri.getPort() == -1) { return port; } else { return uri.getPort(); } } public AsyncSocketMiddleware(AsyncHttpClient client) { this(client, "http", 80); } protected AsyncHttpClient mClient; protected ConnectCallback wrapCallback(GetSocketData data, Uri uri, int port, boolean proxied, ConnectCallback callback) { return callback; } boolean connectAllAddresses; public boolean getConnectAllAddresses() { return connectAllAddresses; } public void setConnectAllAddresses(boolean connectAllAddresses) { this.connectAllAddresses = connectAllAddresses; } String proxyHost; int proxyPort; InetSocketAddress proxyAddress; public void disableProxy() { proxyPort = -1; proxyHost = null; proxyAddress = null; } public void enableProxy(String host, int port) { proxyHost = host; proxyPort = port; proxyAddress = null; } String computeLookup(Uri uri, int port, String proxyHost, int proxyPort) { String proxy; if (proxyHost != null) proxy = proxyHost + ":" + proxyPort; else proxy = ""; if (proxyHost != null) proxy = proxyHost + ":" + proxyPort; return uri.getScheme() + "//" + uri.getHost() + ":" + port + "?proxy=" + proxy; } class IdleSocketHolder { public IdleSocketHolder(AsyncSocket socket) { this.socket = socket; } AsyncSocket socket; long idleTime = System.currentTimeMillis(); } static class ConnectionInfo { int openCount; ArrayDeque<GetSocketData> queue = new ArrayDeque<GetSocketData>(); ArrayDeque<IdleSocketHolder> sockets = new ArrayDeque<IdleSocketHolder>(); } Hashtable<String, ConnectionInfo> connectionInfo = new Hashtable<String, ConnectionInfo>(); int maxConnectionCount = Integer.MAX_VALUE; public int getMaxConnectionCount() { return maxConnectionCount; } public void setMaxConnectionCount(int maxConnectionCount) { this.maxConnectionCount = maxConnectionCount; } @Override public Cancellable getSocket(final GetSocketData data) { final Uri uri = data.request.getUri(); final int port = getSchemePort(data.request.getUri()); if (port == -1) { return null; } data.state.put("socket-owner", this); final String lookup = computeLookup(uri, port, data.request.getProxyHost(), data.request.getProxyPort()); ConnectionInfo info = getOrCreateConnectionInfo(lookup); synchronized (AsyncSocketMiddleware.this) { if (info.openCount >= maxConnectionCount) { // wait for a connection queue to free up SimpleCancellable queueCancel = new SimpleCancellable(); info.queue.add(data); return queueCancel; } info.openCount++; while (!info.sockets.isEmpty()) { IdleSocketHolder idleSocketHolder = info.sockets.pop(); final AsyncSocket socket = idleSocketHolder.socket; if (idleSocketHolder.idleTime + idleTimeoutMs < System.currentTimeMillis()) { socket.setClosedCallback(null); socket.close(); continue; } if (!socket.isOpen()) continue; data.request.logd("Reusing keep-alive socket"); data.connectCallback.onConnectCompleted(null, socket); // just a noop/dummy, as this can't actually be cancelled. SimpleCancellable ret = new SimpleCancellable(); ret.setComplete(); return ret; } } if (!connectAllAddresses || proxyHost != null || data.request.getProxyHost() != null) { // just default to connecting to a single address data.request.logd("Connecting socket"); String unresolvedHost; int unresolvedPort; boolean proxied = false; if (data.request.getProxyHost() == null && proxyHost != null) data.request.enableProxy(proxyHost, proxyPort); if (data.request.getProxyHost() != null) { unresolvedHost = data.request.getProxyHost(); unresolvedPort = data.request.getProxyPort(); proxied = true; } else { unresolvedHost = uri.getHost(); unresolvedPort = port; } if (proxied) { data.request.logv("Using proxy: " + unresolvedHost + ":" + unresolvedPort); } return mClient.getServer().connectSocket(unresolvedHost, unresolvedPort, wrapCallback(data, uri, port, proxied, data.connectCallback)); } // try to connect to everything... data.request.logv("Resolving domain and connecting to all available addresses"); return mClient.getServer().getAllByName(uri.getHost()) .then(new TransformFuture<AsyncSocket, InetAddress[]>() { Exception lastException; @Override protected void error(Exception e) { super.error(e); wrapCallback(data, uri, port, false, data.connectCallback).onConnectCompleted(e, null); } @Override protected void transform(final InetAddress[] result) throws Exception { Continuation keepTrying = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { // if it completed, that means that the connection failed if (lastException == null) lastException = new ConnectionFailedException("Unable to connect to remote address"); if (setComplete(lastException)) { wrapCallback(data, uri, port, false, data.connectCallback).onConnectCompleted(lastException, null); } } }); for (final InetAddress address: result) { final String inetSockAddress = String.format(Locale.ENGLISH, "%s:%s", address, port); keepTrying.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, final CompletedCallback next) throws Exception { data.request.logv("attempting connection to " + inetSockAddress); mClient.getServer().connectSocket(new InetSocketAddress(address, port), wrapCallback(data, uri, port, false, new ConnectCallback() { @Override public void onConnectCompleted(Exception ex, AsyncSocket socket) { if (isDone()) { lastException = new Exception("internal error during connect to " + inetSockAddress); next.onCompleted(null); return; } // try the next address if (ex != null) { lastException = ex; next.onCompleted(null); return; } // if the socket is no longer needed, just hang onto it... if (isDone() || isCancelled()) { data.request.logd("Recycling extra socket leftover from cancelled operation"); idleSocket(socket); recycleSocket(socket, data.request); return; } if (setComplete(null, socket)) { data.connectCallback.onConnectCompleted(null, socket); } } })); } }); } keepTrying.start(); } }); } private ConnectionInfo getOrCreateConnectionInfo(String lookup) { ConnectionInfo info = connectionInfo.get(lookup); if (info == null) { info = new ConnectionInfo(); connectionInfo.put(lookup, info); } return info; } private void maybeCleanupConnectionInfo(String lookup) { ConnectionInfo info = connectionInfo.get(lookup); if (info == null) return; while (!info.sockets.isEmpty()) { IdleSocketHolder idleSocketHolder = info.sockets.peekLast(); AsyncSocket socket = idleSocketHolder.socket; if (idleSocketHolder.idleTime + idleTimeoutMs > System.currentTimeMillis()) break; info.sockets.pop(); // remove the callback, prevent reentrancy. socket.setClosedCallback(null); socket.close(); } if (info.openCount == 0 && info.queue.isEmpty() && info.sockets.isEmpty()) connectionInfo.remove(lookup); } private void recycleSocket(final AsyncSocket socket, AsyncHttpRequest request) { if (socket == null) return; Uri uri = request.getUri(); int port = getSchemePort(uri); final String lookup = computeLookup(uri, port, request.getProxyHost(), request.getProxyPort()); final ArrayDeque<IdleSocketHolder> sockets; final IdleSocketHolder idleSocketHolder = new IdleSocketHolder(socket); synchronized (AsyncSocketMiddleware.this) { ConnectionInfo info = getOrCreateConnectionInfo(lookup); sockets = info.sockets; sockets.push(idleSocketHolder); } socket.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { synchronized (AsyncSocketMiddleware.this) { sockets.remove(idleSocketHolder); maybeCleanupConnectionInfo(lookup); } } }); } private void idleSocket(final AsyncSocket socket) { // must listen for socket close, otherwise log will get spammed. socket.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { socket.setClosedCallback(null); socket.close(); } }); socket.setWriteableCallback(null); // should not get any data after this point... // if so, eat it and disconnect. socket.setDataCallback(new DataCallback.NullDataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { super.onDataAvailable(emitter, bb); bb.recycle(); socket.setClosedCallback(null); socket.close(); } }); } private void nextConnection(AsyncHttpRequest request) { Uri uri = request.getUri(); final int port = getSchemePort(uri); String key = computeLookup(uri, port, request.getProxyHost(), request.getProxyPort()); synchronized (AsyncSocketMiddleware.this) { ConnectionInfo info = connectionInfo.get(key); if (info == null) return; --info.openCount; while (info.openCount < maxConnectionCount && info.queue.size() > 0) { GetSocketData gsd = info.queue.remove(); SimpleCancellable socketCancellable = (SimpleCancellable)gsd.socketCancellable; if (socketCancellable.isCancelled()) continue; Cancellable connect = getSocket(gsd); socketCancellable.setParent(connect); } maybeCleanupConnectionInfo(key); } } @Override public void onResponseComplete(final OnResponseCompleteDataOnRequestSentData data) { if (data.state.get("socket-owner") != this) return; try { idleSocket(data.socket); if (data.exception != null || !data.socket.isOpen()) { data.request.logv("closing out socket (exception)"); data.socket.setClosedCallback(null); data.socket.close(); return; } if (!HttpUtil.isKeepAlive(data.response.protocol(), data.response.headers()) || !HttpUtil.isKeepAlive(Protocol.HTTP_1_1, data.request.getHeaders())) { data.request.logv("closing out socket (not keep alive)"); data.socket.setClosedCallback(null); data.socket.close(); return; } data.request.logd("Recycling keep-alive socket"); recycleSocket(data.socket, data.request); } finally { nextConnection(data.request); } } }