/*
 * Copyright (C) 2012 Square, 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 okhttp3.internal.connection;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import okhttp3.Address;
import okhttp3.Call;
import okhttp3.EventListener;
import okhttp3.HttpUrl;
import okhttp3.Route;
import okhttp3.internal.Util;

Selects routes to connect to an origin server. Each connection requires a choice of proxy server, IP address, and TLS mode. Connections may also be recycled.
/** * Selects routes to connect to an origin server. Each connection requires a choice of proxy server, * IP address, and TLS mode. Connections may also be recycled. */
public final class RouteSelector { private final Address address; private final RouteDatabase routeDatabase; private final Call call; private final EventListener eventListener; /* State for negotiating the next proxy to use. */ private List<Proxy> proxies = Collections.emptyList(); private int nextProxyIndex; /* State for negotiating the next socket address to use. */ private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList(); /* State for negotiating failed routes */ private final List<Route> postponedRoutes = new ArrayList<>(); public RouteSelector(Address address, RouteDatabase routeDatabase, Call call, EventListener eventListener) { this.address = address; this.routeDatabase = routeDatabase; this.call = call; this.eventListener = eventListener; resetNextProxy(address.url(), address.proxy()); }
Returns true if there's another set of routes to attempt. Every address has at least one route.
/** * Returns true if there's another set of routes to attempt. Every address has at least one route. */
public boolean hasNext() { return hasNextProxy() || !postponedRoutes.isEmpty(); } public Selection next() throws IOException { if (!hasNext()) { throw new NoSuchElementException(); } // Compute the next set of routes to attempt. List<Route> routes = new ArrayList<>(); while (hasNextProxy()) { // Postponed routes are always tried last. For example, if we have 2 proxies and all the // routes for proxy1 should be postponed, we'll move to proxy2. Only after we've exhausted // all the good routes will we attempt the postponed routes. Proxy proxy = nextProxy(); for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) { Route route = new Route(address, proxy, inetSocketAddresses.get(i)); if (routeDatabase.shouldPostpone(route)) { postponedRoutes.add(route); } else { routes.add(route); } } if (!routes.isEmpty()) { break; } } if (routes.isEmpty()) { // We've exhausted all Proxies so fallback to the postponed routes. routes.addAll(postponedRoutes); postponedRoutes.clear(); } return new Selection(routes); }
Clients should invoke this method when they encounter a connectivity failure on a connection returned by this route selector.
/** * Clients should invoke this method when they encounter a connectivity failure on a connection * returned by this route selector. */
public void connectFailed(Route failedRoute, IOException failure) { if (failedRoute.proxy().type() != Proxy.Type.DIRECT && address.proxySelector() != null) { // Tell the proxy selector when we fail to connect on a fresh connection. address.proxySelector().connectFailed( address.url().uri(), failedRoute.proxy().address(), failure); } routeDatabase.failed(failedRoute); }
Prepares the proxy servers to try.
/** Prepares the proxy servers to try. */
private void resetNextProxy(HttpUrl url, Proxy proxy) { if (proxy != null) { // If the user specifies a proxy, try that and only that. proxies = Collections.singletonList(proxy); } else { // Try each of the ProxySelector choices until one connection succeeds. List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri()); proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty() ? Util.immutableList(proxiesOrNull) : Util.immutableList(Proxy.NO_PROXY); } nextProxyIndex = 0; }
Returns true if there's another proxy to try.
/** Returns true if there's another proxy to try. */
private boolean hasNextProxy() { return nextProxyIndex < proxies.size(); }
Returns the next proxy to try. May be PROXY.NO_PROXY but never null.
/** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
private Proxy nextProxy() throws IOException { if (!hasNextProxy()) { throw new SocketException("No route to " + address.url().host() + "; exhausted proxy configurations: " + proxies); } Proxy result = proxies.get(nextProxyIndex++); resetNextInetSocketAddress(result); return result; }
Prepares the socket addresses to attempt for the current proxy or host.
/** Prepares the socket addresses to attempt for the current proxy or host. */
private void resetNextInetSocketAddress(Proxy proxy) throws IOException { // Clear the addresses. Necessary if getAllByName() below throws! inetSocketAddresses = new ArrayList<>(); String socketHost; int socketPort; if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) { socketHost = address.url().host(); socketPort = address.url().port(); } else { SocketAddress proxyAddress = proxy.address(); if (!(proxyAddress instanceof InetSocketAddress)) { throw new IllegalArgumentException( "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass()); } InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress; socketHost = getHostString(proxySocketAddress); socketPort = proxySocketAddress.getPort(); } if (socketPort < 1 || socketPort > 65535) { throw new SocketException("No route to " + socketHost + ":" + socketPort + "; port is out of range"); } if (proxy.type() == Proxy.Type.SOCKS) { inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort)); } else { eventListener.dnsStart(call, socketHost); // Try each address for best behavior in mixed IPv4/IPv6 environments. List<InetAddress> addresses = address.dns().lookup(socketHost); if (addresses.isEmpty()) { throw new UnknownHostException(address.dns() + " returned no addresses for " + socketHost); } eventListener.dnsEnd(call, socketHost, addresses); for (int i = 0, size = addresses.size(); i < size; i++) { InetAddress inetAddress = addresses.get(i); inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort)); } } }
Obtain a "host" from an InetSocketAddress. This returns a string containing either an actual host name or a numeric IP address.
/** * Obtain a "host" from an {@link InetSocketAddress}. This returns a string containing either an * actual host name or a numeric IP address. */
// Visible for testing static String getHostString(InetSocketAddress socketAddress) { InetAddress address = socketAddress.getAddress(); if (address == null) { // The InetSocketAddress was specified with a string (either a numeric IP or a host name). If // it is a name, all IPs for that name should be tried. If it is an IP address, only that IP // address should be tried. return socketAddress.getHostName(); } // The InetSocketAddress has a specific address: we should only try that address. Therefore we // return the address and ignore any host name that may be available. return address.getHostAddress(); }
A set of selected Routes.
/** A set of selected Routes. */
public static final class Selection { private final List<Route> routes; private int nextRouteIndex = 0; Selection(List<Route> routes) { this.routes = routes; } public boolean hasNext() { return nextRouteIndex < routes.size(); } public Route next() { if (!hasNext()) { throw new NoSuchElementException(); } return routes.get(nextRouteIndex++); } public List<Route> getAll() { return new ArrayList<>(routes); } } }