Android网络编程(八) 之 HttpURLConnection原理分析
1 使用回顾
我们在前面博文《Android网络编程(四) 之 HttpClient与HttpURLConnection》中已经对HttpURLConnection的使用进行过介绍。今天我们接着往下来阅读HttpURLConnection的关键源码从而它进行更加深入的理解。开始前,先来回顾一下简单的使用,通过使用步骤来深入分析每行代码背后的原理,代码如:
InputStream inStream = null;
HttpURLConnection conn = null;
try {URL url = new URL("https://blog.csdn.net/lyz_zyx");conn = (HttpURLConnection)url.openConnection();if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {inStream = conn.getInputStream();String result = inputStreamToString(inStream); // inputStreamToString方法用于解析转换,实现请见前面文章// ……}
} catch (Exception e) {e.printStackTrace();
} finally {try {if (inStream != null) {inStream.close();}} catch (IOException e) {e.printStackTrace();}if (conn != null) {conn.disconnect();}
}
这是一个最简单的使用例子,可见通过创建URL对象、创建HttpURLConnection对象、设置请求方法、获得响应状态代码并判断是否为HTTP_OK(200),最后解析结果。
2 原理分析
2.1 创建URL对象
来看看URL类,源码如下:
URL.java
public URL(String spec) throws MalformedURLException {this(null, spec);
}
public URL(URL context, String spec) throws MalformedURLException {this(context, spec, null);
}
public URL(URL context, String spec, URLStreamHandler handler) throws MalformedURLException {String original = spec;int i, limit, c;int start = 0;String newProtocol = null;boolean aRef=false;boolean isRelative = false;// Check for permission to specify a handlerif (handler != null) {SecurityManager sm = System.getSecurityManager();if (sm != null) {checkSpecifyHandler(sm);}}try {limit = spec.length();while ((limit > 0) && (spec.charAt(limit - 1) <= ' ')) {limit--; //eliminate trailing whitespace}while ((start < limit) && (spec.charAt(start) <= ' ')) {start++; // eliminate leading whitespace}if (spec.regionMatches(true, start, "url:", 0, 4)) {start += 4;}if (start < spec.length() && spec.charAt(start) == '#') {/* we're assuming this is a ref relative to the context URL.* This means protocols cannot start w/ '#', but we must parse* ref URL's like: "hello:there" w/ a ':' in them.*/aRef=true;}for (i = start ; !aRef && (i < limit) &&((c = spec.charAt(i)) != '/') ; i++) {if (c == ':') {String s = spec.substring(start, i).toLowerCase();if (isValidProtocol(s)) {newProtocol = s;start = i + 1;}break;}}// 关键代码1,解析URL的协议,赋予类成员变量protocol// Only use our context if the protocols match.protocol = newProtocol;if ((context != null) && ((newProtocol == null) || newProtocol.equalsIgnoreCase(context.protocol))) {// ……}if (protocol == null) {throw new MalformedURLException("no protocol: "+original);}// 关键代码2,获取URLStreamHandler对象赋予类成员变量handlerif (handler == null && (handler = getURLStreamHandler(protocol)) == null) {throw new MalformedURLException("unknown protocol: "+protocol);}this.handler = handler;// ……// 关键代码3,调用URLStreamHandler对象的parseURL方法解析URL信息handler.parseURL(this, spec, start, limit);} catch(MalformedURLException e) {throw e;} catch(Exception e) {MalformedURLException exception = new MalformedURLException(e.getMessage());exception.initCause(e);throw exception;}
}
请看三个参数的构造函数,函数内部主要做了三个事情:
- 解析URL的协议,赋予类成员变量protocol,该逻辑在方法内完成,如上述代码;
- 通过方法getURLStreamHandler并传入protocol来获取URLStreamHandler对象赋予类成员变量handler;
- 调用变量handler(URLStreamHandler类对象)的parseURL方法解析URL信息。
2.1.1 getURLStreamHandler方法
来看看关键代码2中getURLStreamHandler方法内部是如何获得URLStreamHandler对象的,代码见如下:
static URLStreamHandler getURLStreamHandler(String protocol) {URLStreamHandler handler = handlers.get(protocol);if (handler == null) {// …… // BEGIN Android-added: Custom built-in URLStreamHandlers for http, https.// Fallback to built-in stream handler.if (handler == null) {try {// 关键代码handler = createBuiltinHandler(protocol);} catch (Exception e) {throw new AssertionError(e);}}// END Android-added: Custom built-in URLStreamHandlers for http, https.// ……}return handler;
}
private static URLStreamHandler createBuiltinHandler(String protocol)throws ClassNotFoundException, InstantiationException, IllegalAccessException {URLStreamHandler handler = null;if (protocol.equals("file")) {handler = new sun.net.www.protocol.file.Handler();} else if (protocol.equals("ftp")) {handler = new sun.net.www.protocol.ftp.Handler();} else if (protocol.equals("jar")) {handler = new sun.net.www.protocol.jar.Handler();} else if (protocol.equals("http")) {handler = (URLStreamHandler)Class.forName("com.android.okhttp.HttpHandler").newInstance();} else if (protocol.equals("https")) {handler = (URLStreamHandler)Class.forName("com.android.okhttp.HttpsHandler").newInstance();}return handler;
}
URLStreamHandler方法接收第1步获得的协议变量protocol,然后内部又访问了一个关键的方法createBuiltinHandler,createBuiltinHandler方法内部通过判断协议类型来返回相对应的URLStreamHandler,这里我们只来讨论http和https协议情况,它们分别返回的URLStreamHandler对象是:com.android.okhttp.HttpHandler和com.android.okhttp.HttpsHandler。
com.android.okhttp.HttpHandler和com.android.okhttp.HttpsHandler的源码需要单独往https://android.googlesource.com/platform/external/okhttp/ 进行下载。HttpsHandler是继承自HttpHandler,区别在于添加了对TLS的支持,而且从源码可见,它们分别指定了默认端口是80和433。
HttpHandler.java
public class HttpHandler extends URLStreamHandler {// ……
}
HttpsHandler.java
public final class HttpsHandler extends HttpHandler {// ……
}
2.1.2 URLStreamHandler.parseURL方法
我们再回头看回URL类的构造函数关键代码3,它内部调用变量handler(URLStreamHandler类对象)的parseURL方法解析URL信息,代码见如下:
URLStreamHandler.java
protected void parseURL(URL u, String spec, int start, int limit) {// These fields may receive context content if this was relative URLString protocol = u.getProtocol();String authority = u.getAuthority();String userInfo = u.getUserInfo();String host = u.getHost();int port = u.getPort();String path = u.getPath();String query = u.getQuery();String ref = u.getRef();// ……setURL(u, protocol, host, port, authority, userInfo, path, query, ref);}protected void setURL(URL u, String protocol, String host, int port,String authority, String userInfo, String path,String query, String ref) {if (this != u.handler) {throw new SecurityException("handler for url different from " + "this handler");}// ensure that no one can reset the protocol on a given URL.u.set(u.getProtocol(), host, port, authority, userInfo, path, query, ref);
}
parseURL方法中主要是解析出URL中的字符串信息,最后将这些字符串结果再回设置给URL类中去。以https://blog.csdn.net/lyz_zyx 为例,完成了URL对象的创建后,调用以下代码可见上述7个参数的值是:
URL url = new URL("https://blog.csdn.net/lyz_zyx");
String protocol = url.getProtocol(); // 代表协议,值是:https
String authority = url.getAuthority(); // 代表host和port两部分,值是:blog.csdn.net
String userInfo = url.getUserInfo(); // 代表用户信息,值是:null
String host = url.getHost(); // 代表主机,值:blog.csdn.net
int port = url.getPort(); // 代表端口,这里没有指定,所以值是:-1,如果使用的是getDefaultPort方法获取的话,值是:443
String path = url.getPath(); // 代表路径,值:/lyz_zyx
String query = url.getQuery(); // 代表参数,这里后面没跟有参数,值是:null
String ref = url.getRef(); // 代表锚点,值是:null
2.2 创建HttpURLConnection对象
HttpURLConnection对象通过URL的openConnection方法返回,来看看代码:
URL.java
public URLConnection openConnection() throws java.io.IOException {return handler.openConnection(this);
}
从上面创建URL对象得知,handler对象是HttpsHandler,而HttpsHandler又继承于HttpHandler,所以这里的实现代码在okhttp框架中的HttpHandler中,代码如下:
okhttp->HttpHandler.java
@Override
protected URLConnection openConnection(URL url) throws IOException {
// 关键代码return newOkUrlFactory(null /* proxy */).open(url);
}
@Override
protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {if (url == null || proxy == null) {throw new IllegalArgumentException("url == null || proxy == null");}// 关键代码return newOkUrlFactory(proxy).open(url);
}
两个重载的方法都是指向于newOkUrlFactory方法,该方法虽然只有一行关键代码,但其实是做了两件事情:
- 通过newOkUrlFactory方法创建一个OkUrlFactory对象;
- 调用OkUrlFactory对象的open方法返回URLConnection对象。
2.2.1 newOkUrlFactory方法
来看看newOkUrlFactory方法的源代码做了什么:
okhttp->HttpHandler.java
protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
// 关键代码1,创建OkUrlFactory对象OkUrlFactory okUrlFactory = createHttpOkUrlFactory(proxy);// For HttpURLConnections created through java.net.URL Android uses a connection pool that// is aware when the default network changes so that pooled connections are not re-used when// the default network changes.
// 关键代码2,设置连接池okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());return okUrlFactory;
}
该方法最终是创建了一个newOkUrlFactory对象并返回,其中里面也做了两件事情:
1. 通过createHttpOkUrlFactory方法创建OkUrlFactory对象;
2. 给okUrlFactory对象的client方法返回的对象设置连接池。
2.2.1.1 createHttpOkUrlFactory方法
okhttp->HttpHandler.java
public static OkUrlFactory createHttpOkUrlFactory(Proxy proxy) {OkHttpClient client = new OkHttpClient();// Explicitly set the timeouts to infinity.client.setConnectTimeout(0, TimeUnit.MILLISECONDS);client.setReadTimeout(0, TimeUnit.MILLISECONDS);client.setWriteTimeout(0, TimeUnit.MILLISECONDS);// Set the default (same protocol) redirect behavior. The default can be overridden for// each instance using HttpURLConnection.setInstanceFollowRedirects().client.setFollowRedirects(HttpURLConnection.getFollowRedirects());// Do not permit http -> https and https -> http redirects.client.setFollowSslRedirects(false);// Permit cleartext traffic only (this is a handler for HTTP, not for HTTPS).client.setConnectionSpecs(CLEARTEXT_ONLY);// When we do not set the Proxy explicitly OkHttp picks up a ProxySelector using// ProxySelector.getDefault().if (proxy != null) {client.setProxy(proxy);}// OkHttp requires that we explicitly set the response cache.OkUrlFactory okUrlFactory = new OkUrlFactory(client);// Use the installed NetworkSecurityPolicy to determine which requests are permitted over// http.OkUrlFactories.setUrlFilter(okUrlFactory, CLEARTEXT_FILTER);ResponseCache responseCache = ResponseCache.getDefault();if (responseCache != null) {AndroidInternal.setResponseCache(okUrlFactory, responseCache);}return okUrlFactory;
}
该方法中,首先new了一个OkHttpClient对象,然后给该对象设置了:连接超时时间、读超时时间、写超时时间、重定向相关、通信方式、代理等。
setConnectTimeout、setReadTimeout 和 setWriteTimeout,其值为0表示无超时。
setConnectionSpecs方法设置通信方式,CLEARTEXT_ONLY实际上是Collections.singletonList(ConnectionSpec.CLEARTEXT)的列表,表示不需要TLS,即明文传输。
接着通过OkHttpClient对象来创建一个OkUrlFactory对象,并对其设置指定host的服务器进行明文通信(setUrlFilter方法)和请求缓存信息(setResponseCache),最后就是返回对象。
HttpsHandler继承于HttpHandler,最大的区别在于OkUrlFactory对象的创建,我们也来看看它的源码:
okhttp->HttpsHandler.java
public static OkUrlFactory createHttpsOkUrlFactory(Proxy proxy) {// The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.OkUrlFactory okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);// All HTTPS requests are allowed.OkUrlFactories.setUrlFilter(okUrlFactory, null);OkHttpClient okHttpClient = okUrlFactory.client();// Only enable HTTP/1.1 (implies HTTP/1.0). Disable SPDY / HTTP/2.0.okHttpClient.setProtocols(HTTP_1_1_ONLY);okHttpClient.setConnectionSpecs(Collections.singletonList(TLS_CONNECTION_SPEC));// Android support certificate pinning via NetworkSecurityConfig so there is no need to// also expose OkHttp's mechanism. The OkHttpClient underlying https HttpsURLConnections// in Android should therefore always use the default certificate pinner, whose set of// {@code hostNamesToPin} is empty.okHttpClient.setCertificatePinner(CertificatePinner.DEFAULT);// OkHttp does not automatically honor the system-wide HostnameVerifier set with// HttpsURLConnection.setDefaultHostnameVerifier().okUrlFactory.client().setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());// OkHttp does not automatically honor the system-wide SSLSocketFactory set with// HttpsURLConnection.setDefaultSSLSocketFactory().// See https://github.com/square/okhttp/issues/184 for details.okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());return okUrlFactory;
}
setProtocols:方法传入了HTTP_1_1_ONLY是一个列表,它对应着TLS握手ClientHello阶段中,支持最高的协议版本version。关于TLS握手过程,请参考《Android网络编程(三) 之 网络请求握手过程》。
setConnectionSpecs:方法传入的是Collections.singletonList(TLS_CONNECTION_SPEC),表示支持TLS,即密文传输。
setCertificatePinner:方法设置证书锁定,在Android中始终使用默认的证书CertificatePinner.DEFAULT。
setHostnameVerifier:方法指定验证证书的HostnameVerifier。
sslSocketFactory:方法设置SSLSocket的工厂。
2.2.1.2 setConnectionPool方法
okhttp->OkHttpClient.java
public OkHttpClient setConnectionPool(ConnectionPool connectionPool) {this.connectionPool = connectionPool;return this;
}
通过setConnectionPool方法给OkUrlFactory内的OkHttpClient对象设置连接池configAwareConnectionPool.get(),来看看参数ConnectionPool是何方神圣代码:
okhttp->HttpHandler.java
private final ConfigAwareConnectionPool configAwareConnectionPool = ConfigAwareConnectionPool.getInstance();
configAwareConnectionPool事实上是一个ConfigAwareConnectionPool单例,再来看看ConfigAwareConnectionPool的代码:
okhttp->ConfigAwareConnectionPool.java
public synchronized ConnectionPool get() {if (connectionPool == null) {// Only register the listener once the first time a ConnectionPool is created.if (!networkEventListenerRegistered) {networkEventDispatcher.addListener(new NetworkEventListener() {@Overridepublic void onNetworkConfigurationChanged() {synchronized (ConfigAwareConnectionPool.this) {// If the network config has changed then existing pooled connections should not be// re-used. By setting connectionPool to null it ensures that the next time// getConnectionPool() is called a new pool will be created.connectionPool = null;}}});networkEventListenerRegistered = true;}connectionPool = new ConnectionPool(CONNECTION_POOL_MAX_IDLE_CONNECTIONS, CONNECTION_POOL_KEEP_ALIVE_DURATION_MS);}return connectionPool;
}
代码中可见,get方法内做了监听网络变化事件,一旦网络发生变化,将会把connectionPool对象置为null,方法下面会执行创建出新的ConnectionPool对象,其构造函数参数:
CONNECTION_POOL_MAX_IDLE_CONNECTIONS:表示空闲TCP连接的最大数量,默认为5个。
CONNECTION_POOL_KEEP_ALIVE_DURATION_MS:表示TCP连接最长的空闲时长,默认为5分钟。
ConnectionPool类用于TCP连接池的封装,内部维护着一个双向的先进先出的ArrayDeque队列
2.2.2 open方法
看回上面,创建HttpURLConnection对象中的第2件事,调用OkUrlFactory对象的open方法,请看源代码:
okhttp->OkUrlFactory.java
public HttpURLConnection open(URL url) {return open(url, client.getProxy());
}
HttpURLConnection open(URL url, Proxy proxy) {String protocol = url.getProtocol();OkHttpClient copy = client.copyWithDefaults();copy.setProxy(proxy);if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy, urlFilter);if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy, urlFilter);throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}
可见,方法内根据协议创建出HttpURLConnectionImpl或HttpsURLConnectionImpl对象并返回。其中:
HttpURLConnectionImpl继承于HttpURLConnection;
HttpsURLConnectionImpl同样经过继承DelegatingHttpsURLConnection和HttpsURLConnection最终也是继承于HttpURLConnection。
2.3 执行网络请求
当创建了URL和HttpURLConnection对象后,接下来就是执行网络的请求。我们在使用上知道,无论是调用:conn.getResponseCode()、conn.connect() 还是conn.getInputStream() 都可以完成网络的请求操作。那么我们先来看看这三个方法到底内部做了些什么事情。请看代码:
HttpURLConnection.java
public int getResponseCode() throws IOException {if (responseCode != -1) {return responseCode;}Exception exc = null;try {getInputStream();} catch (Exception e) {exc = e;}// ……
}
getResponseCode方法很好理解,首先判断responseCode是否不为-1,代表如果已经执行过请求,就直接返回上次的请求码就可以。否则就执行getInputStream方法进行网络请求。可以理解成getResponseCode方法如果从未执行过网络请求的话,会调用到getInputStream方法中去完成网络请求。
我们再来看看connect方法代码:
okhttp->HttpURLConnectionImpl.java
@Override
public final void connect() throws IOException {initHttpEngine();boolean success;do {success = execute(false);} while (!success);
}
connect方法很简单的几行代码,核心就是两件事情:initHttpEngine方法和execute方法。
我们再来看看getInputStream方法的代码:
okhttp->HttpURLConnectionImpl.java
@Override
public final InputStream getInputStream() throws IOException {if (!doInput) {throw new ProtocolException("This protocol does not support input");}HttpEngine response = getResponse();// if the requested file does not exist, throw an exception formerly the// Error page from the server was returned if the requested file was// text/html this has changed to return FileNotFoundException for all// file typesif (getResponseCode() >= HTTP_BAD_REQUEST) {throw new FileNotFoundException(url.toString());}return response.getResponse().body().byteStream();
}
getInputStream方法内里也是两件核心的事情:请求getResponse方法获得HttpEngine对象,然后返回HttpEngine对象的内容字节流。所以关键点就在于getResponse方法中,再继续看代码:
okhttp->HttpURLConnectionImpl.java
private HttpEngine getResponse() throws IOException {initHttpEngine();if (httpEngine.hasResponse()) {return httpEngine;}while (true) {if (!execute(true)) {continue;}Response response = httpEngine.getResponse();Request followUp = httpEngine.followUpRequest();if (followUp == null) {httpEngine.releaseStreamAllocation();return httpEngine;}if (++followUpCount > HttpEngine.MAX_FOLLOW_UPS) {throw new ProtocolException("Too many follow-up requests: " + followUpCount);}// The first request was insufficient. Prepare for another...url = followUp.url();requestHeaders = followUp.headers().newBuilder();// Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM redirect// should keep the same method, Chrome, Firefox and the RI all issue GETs// when following any redirect.Sink requestBody = httpEngine.getRequestBody();if (!followUp.method().equals(method)) {requestBody = null;}if (requestBody != null && !(requestBody instanceof RetryableSink)) {throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);}StreamAllocation streamAllocation = httpEngine.close();if (!httpEngine.sameConnection(followUp.httpUrl())) {streamAllocation.release();streamAllocation = null;}httpEngine = newHttpEngine(followUp.method(), streamAllocation, (RetryableSink) requestBody,response);}
}
从代码可见,getResponse方法内部同样也存在connect方法中要执行的initHttpEngine方法和execute方法。所以上述三个方法:getResponseCode、connect 和getInputStream,它们的核心原理就可以通过分析getResponse方法来得到答案了。先来了解一下getResponse方法内部有要关键就是调用了两个方法:
- initHttpEngine是初始化HttpEngine;
- execute是最终执行的发送网络请求。
2.3.1 initHttpEngine方法
来看看initHttpEngine方法源码。
okhttp->HttpURLConnectionImpl.java
private void initHttpEngine() throws IOException {if (httpEngineFailure != null) {throw httpEngineFailure;} else if (httpEngine != null) {return;}connected = true;try {if (doOutput) {if (method.equals("GET")) {// they are requesting a stream to write to. This implies a POST methodmethod = "POST";} else if (!HttpMethod.permitsRequestBody(method)) {throw new ProtocolException(method + " does not support writing");}}// If the user set content length to zero, we know there will not be a request body.httpEngine = newHttpEngine(method, null, null, null);} catch (IOException e) {httpEngineFailure = e;throw e;}
}
上述方法中,会判断doOutput是否为true,如果是且请求方法是GET的话,会将其改为POST。先来聊聊doOutput和doInput是什么:
doOutput:表示是否通过请求体发送数据给服务端,默认为false。
doInput:表示是否读取服务端返回的响应体中的数据,默认为true。
因为GET请求方法是没有请求体的,所以如果doOutput为true的话,就要将其修改为POST的方式来请求。
接着,通过newHttpEngine方法来创建HttpEngine对象。往下看代码:
okhttp->HttpURLConnectionImpl.java
private HttpEngine newHttpEngine(String method, StreamAllocation streamAllocation, RetryableSink requestBody, Response priorResponse)throws MalformedURLException, UnknownHostException {// OkHttp's Call API requires a placeholder body; the real body will be streamed separately.RequestBody placeholderBody = HttpMethod.requiresRequestBody(method) ? EMPTY_REQUEST_BODY : null;URL url = getURL();HttpUrl httpUrl = Internal.instance.getHttpUrlChecked(url.toString());// 关键代码1,创建Request对象Request.Builder builder = new Request.Builder().url(httpUrl).method(method, placeholderBody);Headers headers = requestHeaders.build();for (int i = 0, size = headers.size(); i < size; i++) {builder.addHeader(headers.name(i), headers.value(i));}boolean bufferRequestBody = false;if (HttpMethod.permitsRequestBody(method)) {// Specify how the request body is terminated.if (fixedContentLength != -1) {builder.header("Content-Length", Long.toString(fixedContentLength));} else if (chunkLength > 0) {builder.header("Transfer-Encoding", "chunked");} else {bufferRequestBody = true;}// Add a content type for the request body, if one isn't already present.if (headers.get("Content-Type") == null) {builder.header("Content-Type", "application/x-www-form-urlencoded");}}if (headers.get("User-Agent") == null) {builder.header("User-Agent", defaultUserAgent());}Request request = builder.build();// If we're currently not using caches, make sure the engine's client doesn't have one.OkHttpClient engineClient = client;if (Internal.instance.internalCache(engineClient) != null && !getUseCaches()) {engineClient = client.clone().setCache(null);}// 关键代码2,创建 HttpEngine对象return new HttpEngine(engineClient, request, bufferRequestBody, true, false, streamAllocation, requestBody, priorResponse);
}
上述方法开始前,先通过HttpUrl和RequestBody来new了一个Request对象,然后给该对象添加header和设置:Content-Length、Transfer-Encoding、Content-Type、User-Agent等这些header值,更多header的介绍可见HTTP响应头和请求头信息对照表。最后就是创建HttpEngine对象,代码如下。
okhttp->HttpEngine.java
public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody,boolean callerWritesRequestBody, boolean forWebSocket, StreamAllocation streamAllocation,RetryableSink requestBodyOut, Response priorResponse) {this.client = client;this.userRequest = request;this.bufferRequestBody = bufferRequestBody;this.callerWritesRequestBody = callerWritesRequestBody;this.forWebSocket = forWebSocket;this.streamAllocation = streamAllocation != null ? streamAllocation : new StreamAllocation(client.getConnectionPool(), createAddress(client, request));this.requestBodyOut = requestBodyOut;this.priorResponse = priorResponse;
}
2.3.2 execute方法
完成了HttpEngine的初始化后,接着回到HttpURLConnectionImpl类中调用的execute方法做了什么事情。
okhttp->HttpURLConnectionImpl.java
private boolean execute(boolean readResponse) throws IOException {boolean releaseConnection = true;if (urlFilter != null) {
// 关键代码1,判断是否可以与指定host的服务器进行明文通信 urlFilter.checkURLPermitted(httpEngine.getRequest().url());}try {
//关键代码2,发起网络请求httpEngine.sendRequest();Connection connection = httpEngine.getConnection();if (connection != null) {route = connection.getRoute();handshake = connection.getHandshake();} else {route = null;handshake = null;}
//关键代码3,读取响应if (readResponse) {httpEngine.readResponse();}releaseConnection = false;return true;} catch (RequestException e) {// ……} catch (RouteException e) {// ……} catch (IOException e) {// ……} finally {// ……}
}
execute方法里做了3件事情:
- 判断指定host服务器进行明文通信
- 发起网络请求
- 读取响应
2.3.2.1判断指定host服务器进行明文通信
先看关键代码1处,方法先判断若urlFilter不为空,则调用checkURLPermitted来判断是否可以与指定host的服务器进行明文通信。urlFilter对象经构造方法传入,从OkUrlFactory.java中new HttpURLConnectionImpl或new HttpsURLConnectionImpl得知,urlFilter是在HttpHandler.java中通过setUrlFilter方法传入CLEARTEXT_FILTER,而在HttpsHandler.java中通过setUrlFilter方法传入null,所以具体实现代码在HttpHandler.java中。
okhttp->HttpHandler.java
private static final class CleartextURLFilter implements URLFilter {@Overridepublic void checkURLPermitted(URL url) throws IOException {String host = url.getHost();if (!NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(host)) {throw new IOException("Cleartext HTTP traffic to " + host + " not permitted");}}
}
2.3.2.2 发起网络请求
看回execute中关键代码2,这里就是发起网络请求,我们看回HttpEngine类中的实现:
okhttp->HttpEngine.java
public void sendRequest() throws RequestException, RouteException, IOException {if (cacheStrategy != null) return; // Already sent.if (httpStream != null) throw new IllegalStateException();// 关键代码1,通过newworkRequest方法处理Request对象,添加Host、Connection、Accept-Encoding、User-Agent请求头Request request = networkRequest(userRequest);//关键代码2,获取缓存,默认情况下是开启缓存的,除非在HttpURLConnection中setUseCaches传入falseInternalCache responseCache = Internal.instance.internalCache(client);Response cacheCandidate = responseCache != null ? responseCache.get(request) : null;//关键代码2,通过CacheStrategy.Factory里缓存相关策略计算,获得networkRequest和cacheResponse两个对象,分别表示网络请求对象和缓存请求对象long now = System.currentTimeMillis();cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();networkRequest = cacheStrategy.networkRequest;cacheResponse = cacheStrategy.cacheResponse;if (responseCache != null) {responseCache.trackResponse(cacheStrategy);}if (cacheCandidate != null && cacheResponse == null) {closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.}// 关键代码3,判断networkRequest不为null,表示无缓存或缓存过期,需求请求网络if (networkRequest != null) {
// 建立Socket连接httpStream = connect();httpStream.setHttpEngine(this);// If the caller's control flow writes the request body, we need to create that stream// immediately. And that means we need to immediately write the request headers, so we can// start streaming the request body. (We may already have a request body if we're retrying a failed POST.)if (callerWritesRequestBody && permitsRequestBody(networkRequest) && requestBodyOut == null) {long contentLength = OkHeaders.contentLength(request);if (bufferRequestBody) {if (contentLength > Integer.MAX_VALUE) {throw new IllegalStateException("Use setFixedLengthStreamingMode() or "+ "setChunkedStreamingMode() for requests larger than 2 GiB.");}if (contentLength != -1) {// 已知长度情况下,即设置过Content-Length情况下,往httpStream写入请求头信息和创建RetryableSink对象httpStream.writeRequestHeaders(networkRequest);requestBodyOut = new RetryableSink((int) contentLength);} else {// 未知长度情况下,只创建RetryableSink对象,待后面body准备完毕后再设置requestBodyOut = new RetryableSink();}} else {httpStream.writeRequestHeaders(networkRequest);requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);}}}
// 关键代码4,该请求的缓存有效,直接使用缓存请求
else {if (cacheResponse != null) {// We have a valid cached response. Promote it to the user response immediately.this.userResponse = cacheResponse.newBuilder().request(userRequest).priorResponse(stripBody(priorResponse)).cacheResponse(stripBody(cacheResponse)).build();} else {// We're forbidden from using the network, and the cache is insufficient.this.userResponse = new Response.Builder().request(userRequest).priorResponse(stripBody(priorResponse)).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(EMPTY_BODY).build();}userResponse = unzip(userResponse);}
}
这方法很重要,我们将其分为做了4件事,如上述注释中的关键代码,分别有:
- 通过newworkRequest方法处理Request对象,添加Host、Connection、Accept-Encoding、User-Agent请求头;
- 通过计算缓存是否可用,然后获得网络请求对象networkRequest和缓存对象cacheResponse;
- 判断网络请求对象不为空,则执行网络相关请求操作;
- 否则执行缓存相关请求操作。
上述4件事中,我们重点来关注第2和第3件事情。
缓存策略
来来看下关键代码2中所做的第2件事,获得网络请求和缓存请求对象,关键看cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();这行代码所调用的逻辑。
okhttp->CacheStrategy.java
public CacheStrategy get() {CacheStrategy candidate = getCandidate();if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {// We're forbidden from using the network and the cache is insufficient.return new CacheStrategy(null, null);}return candidate;
}
get方法调用了getCandidate方法获得返回的结果对象,判断如果禁止使用网络且缓存不足则返回一个新创建的networkRequest和cacheResponse都为空的CacheStrategy对象。我们重点来关注getCandidate方法的逻辑。
okhttp->CacheStrategy.java
private CacheStrategy getCandidate() {// 如果请求对应的缓存为空,则返回新创建的cacheResponse为空的CacheStrategy对象if (cacheResponse == null) {return new CacheStrategy(request, null);}// 如果缺少所需的握手,则返回新创建的cacheResponse为空的CacheStrategy对象if (request.isHttps() && cacheResponse.handshake() == null) {return new CacheStrategy(request, null);}// 如果不允许缓存,则返回新创建的cacheResponse为空的CacheStrategy对象if (!isCacheable(cacheResponse, request)) {return new CacheStrategy(request, null);}CacheControl requestCaching = request.cacheControl();if (requestCaching.noCache() || hasConditions(request)) {return new CacheStrategy(request, null);}long ageMillis = cacheResponseAge();long freshMillis = computeFreshnessLifetime();if (requestCaching.maxAgeSeconds() != -1) {freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));}long minFreshMillis = 0;if (requestCaching.minFreshSeconds() != -1) {minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());}long maxStaleMillis = 0;CacheControl responseCaching = cacheResponse.cacheControl();if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());}// 判断缓存是否过期,如果新鲜,则返回新创建的networkRequest为空的CacheStrategy对象if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {Response.Builder builder = cacheResponse.newBuilder();if (ageMillis + minFreshMillis >= freshMillis) {builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");}long oneDayMillis = 24 * 60 * 60 * 1000L;if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");}return new CacheStrategy(null, builder.build());}// 如果缓存已过期,需要进一步与服务端进行验证是否还可以使用Request.Builder conditionalRequestBuilder = request.newBuilder();if (etag != null) {conditionalRequestBuilder.header("If-None-Match", etag);} else if (lastModified != null) {conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);} else if (servedDate != null) {conditionalRequestBuilder.header("If-Modified-Since", servedDateString);}Request conditionalRequest = conditionalRequestBuilder.build();return hasConditions(conditionalRequest)? new CacheStrategy(conditionalRequest, cacheResponse): new CacheStrategy(conditionalRequest, null);
}
上述方法就是整个HttpURLConnection的缓存策略所关键的地方。缓存计算完毕后,接着就是来处理网络请求或者缓存请求了。
网络请求
再来来看下关键代码3中所做的第3件事,网络请求相关的逻辑,就要跳到connect方法去,请继续看代码。
okhttp->HttpEngine.java
private HttpStream connect() throws RouteException, RequestException, IOException {boolean doExtensiveHealthChecks = !networkRequest.method().equals("GET");return streamAllocation.newStream(client.getConnectTimeout(),client.getReadTimeout(), client.getWriteTimeout(),client.getRetryOnConnectionFailure(), doExtensiveHealthChecks);
}
方法内主要是调用了streamAllocation对象的newStream方法,接着往下看
okhttp->StreamAllocation.java
public HttpStream newStream(int connectTimeout, int readTimeout, int writeTimeout,boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)throws RouteException, IOException {try {
// 关键代码,寻找一个健康的连接RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);HttpStream resultStream;if (resultConnection.framedConnection != null) {
//使用HTTP/2协议resultStream = new Http2xStream(this, resultConnection.framedConnection);} else {
// 使用HTTP/1.x协议resultConnection.getSocket().setSoTimeout(readTimeout);resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);resultStream = new Http1xStream(this, resultConnection.source, resultConnection.sink);}// ……
}
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)throws IOException, RouteException {while (true) {
// 关键代码RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,connectionRetryEnabled);// ……}
}
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,boolean connectionRetryEnabled) throws IOException, RouteException {// ……Route route = routeSelector.next();RealConnection newConnection = new RealConnection(route);acquire(newConnection);synchronized (connectionPool) {Internal.instance.put(connectionPool, newConnection);this.connection = newConnection;if (canceled) throw new IOException("Canceled");}// 关键代码,创建与服务端建立Socket连接newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.getConnectionSpecs(),connectionRetryEnabled);routeDatabase().connected(newConnection.getRoute());return newConnection;
}
okhttp->RealConnection.java
public void connect(int connectTimeout, int readTimeout, int writeTimeout,List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {// ……while (protocol == null) {try {rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP? address.getSocketFactory().createSocket(): new Socket(proxy);
// 关键代码,发起与服务端的TCP连接connectSocket(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);} catch (IOException e) {// ……}}
}
private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,ConnectionSpecSelector connectionSpecSelector) throws IOException {rawSocket.setSoTimeout(readTimeout);try {
// 关键代码,服务端建立TCP连接Platform.get().connectSocket(rawSocket, route.getSocketAddress(), connectTimeout);} catch (ConnectException e) {throw new ConnectException("Failed to connect to " + route.getSocketAddress());}source = Okio.buffer(Okio.source(rawSocket));sink = Okio.buffer(Okio.sink(rawSocket));if (route.getAddress().getSslSocketFactory() != null) {
// 关键代码,TLS握手connectTls(readTimeout, writeTimeout, connectionSpecSelector);} else {protocol = Protocol.HTTP_1_1;socket = rawSocket;}// ……
}
由于底层代码过多和复杂的原因,这里省略了较多的过程,只保留一部分关键代码,我们对其进行一个大概的了解即可。能看见得是,这些方法的逻辑中最后就是跟Socket进行相应的处理。
2.3.2.3 读取响应
完成了请求后,最后就是读取响应了,继续看回execute中关键代码。
okhttp->HttpEngine.java
public void readResponse() throws IOException {if (userResponse != null) {return; // Already ready.}if (networkRequest == null && cacheResponse == null) {throw new IllegalStateException("call sendRequest() first!");}if (networkRequest == null) {return; // No network response to read.}Response networkResponse;if (forWebSocket) {httpStream.writeRequestHeaders(networkRequest);networkResponse = readNetworkResponse();} else if (!callerWritesRequestBody) {networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest);} else {// ……
// 关键代码1,获得网络请求结果networkResponse = readNetworkResponse();}receiveHeaders(networkResponse.headers());// If we have a cache response too, then we're doing a conditional get.if (cacheResponse != null) {if (validate(cacheResponse, networkResponse)) {
//关键代码2,读取缓存请求结果userResponse = cacheResponse.newBuilder().request(userRequest).priorResponse(stripBody(priorResponse)).headers(combine(cacheResponse.headers(), networkResponse.headers())).cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();networkResponse.body().close();releaseStreamAllocation();// Update the cache after combining headers but before stripping the// Content-Encoding header (as performed by initContentStream()).InternalCache responseCache = Internal.instance.internalCache(client);responseCache.trackConditionalCacheHit();responseCache.update(cacheResponse, stripBody(userResponse));userResponse = unzip(userResponse);return;} else {closeQuietly(cacheResponse.body());}}//关键代码3,读取网络请求结果userResponse = networkResponse.newBuilder().request(userRequest).priorResponse(stripBody(priorResponse)).cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();if (hasBody(userResponse)) {
//关键代码4,保存缓存maybeCache();userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));}
}
private void maybeCache() throws IOException {InternalCache responseCache = Internal.instance.internalCache(client);if (responseCache == null) return;// Should we cache this response for this request?if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {if (HttpMethod.invalidatesCache(networkRequest.method())) {try {
// 关键代码,移除无效的缓存responseCache.remove(networkRequest);} catch (IOException ignored) {// The cache cannot be written.}}return;}// 关键代码,将本次请求结果缓存storeRequest = responseCache.put(stripBody(userResponse));
}
从上述注释可见,方法开头先判断readResponse是否为真,因为我们从前面得知,通过conn.connect()进行请求的话,传入的是false,而通过conn.getInputStream()进行请求会传入true。
过程中关键代码1处是获得网络请求的结果,然后去验证网络请求的结果和缓存请求的结果哪个是有效的,而进行相应的结果读取,最后就是更新缓存。
3 原理总结
到这里,通过源码分析原理就全部完成了,整个过程代码量还是非常多。我们平时在学习优秀框架时,并不一定全部代码完全读懂,其实能明白大概原理以及从中吸收框架设计的思想就已经足够了。那我们现在用简短的话来总结一下HttpURLConnection的原理:
第1 首先new了一个URL类的对象,内部就是解析URL的协议、主机名、端口、路径、参数等信息。
1.1 其中在解析协议时创建对应的URLStreamHandler对象,若协议是http或https的话,其URLStreamHandler对象是google的okhttp框架中的com.android.okhttp.HttpHandler和com.android.okhttp.HttpsHandler。
1.2 所以说,使用httpURLConnection框架进行网络请求,实际上就是使用okhttp进行的。
第2 通过URL类的openConnection方法创建HttpURLConnection类对象,现实过程也是在okhttp中进行,
2.1 内部先创建一个指定的host服务端和请求缓存信息的OkUrlFactory对象,该对象内包含着一个OkHttpClient对象,过程中给OkHttpClient对象设置连接时间、读写时间、通信方式(是否需要TLS)、连接池(双向的先进先出的ArrayDeque队列,默认空闲TCP连接的最大数量为5个,默认TCP连接最长的空闲时长为5分钟)。
2.2 创建OkHttpClient对象后便调用其open方法返回HttpURLConnection的实现类对象:HttpURLConnectionImpl或HttpsURLConnectionImpl。
第3 创建完了HttpURLConnection对象后,就是让其执行网络请求了。
3.1 通过简单分析得知,无论是conn.getResponseCode()、conn.connect() 还是conn.getInputStream() 进行网络请求,最终会调用到initHttpEngine和 execute方法
3.2 initHttpEngine方法从名字便得知是初始化一个HttpEngine对象,过程中通过创建一个Request对象,并使Request对象添加header和设置:Content-Length、Transfer-Encoding、Content-Type、User-Agent等这些header值。
3.3 execute方法就是核心网络请求的逻辑,其内部主要是先通过一个缓存策略计算缓存是否不为空、是否握手正常、是否允许缓存、是否新鲜等逻辑来返回一个缓存请求对象cacheResponse和网络请求对象。如果缓存方案有效,则网络请求对象是null,相反若缓存方案无效,该网络请求对象就是上面所创建的Request对象。网络请求过程内在就是通过Socket来进行与服务端的TCP握手和TLS握手连接的相关网络处理。
3.4 最后同样判断缓存请求或网络请求对象是否有效再进行相应的结果读取,并更新缓存。
Android网络编程(八) 之 HttpURLConnection原理分析相关推荐
- Android网络框架-Volley(一) 工作原理分析
使用Volley可以让我们轻松访问网络,不用每次都要写大量重复的代码,而且它是异步的,不用我们再去写异步任务,这样代码非常简洁.还有很重要的一点,他还可以异步加载图片并且有缓存机制,防止OOM发生,V ...
- Android网络编程(一)HTTP协议原理
相关文章 Android网络编程(一)HTTP协议原理 Android网络编程(二)HttpClient与HttpURLConnection Android网络编程(三)Volley用法全解析 And ...
- Android初学八之Android网络编程
目录 0 环境 1 目标 2 OkHttp框架的原理 3 界面展示 4 核心代码 4.1 UI设计 4.2 安装网络框架环境 4.3 使用网络框架连接后台数据并在APP中展示数据结果 5 代码仓库 6 ...
- 基于Http协议的Android网络编程
之前写过一篇Android网络编程<浅谈android网络编程>,随着了解了更多Android知识,我意识到HttpClient已经不推荐使用了,更是在Android 6.0中被废弃了,原 ...
- Android 网络编程(二) HTTP协议解析
文章目录 引言 HTTP 简介 什么是HTTP,HTTPS 又是什么? HTTP 协议的特点 HTTP URL 格式 HTTP的报文 请求报文 请求行 请求报头 请求数据 响应保文 状态行 响应报头 ...
- 腾讯T3亲自教你!2021年Android网络编程总结篇,最强技术实现
前言 Android常用知识体系是什么鬼?所谓常用知识体系,就是指对项目中重复使用率较高的功能点进行梳理.注意哦,不是Android知识体系. 古语道:学而不思则罔,思而不学则殆.如果将做项目类比为& ...
- android网络编程及网络超时处理
android网络编程及网络超时处理 本文是收录的两篇关于这方面的文章 Android超时机制的处理 由于手机端应用的响应,与当时的无线通信网络状况有很大的关联.而通信网络往往具有不稳定,延迟长的特点 ...
- Android网络编程系列 一 Socket抽象层
在<Android网络编程>系列文章中,前面已经将Java的通信底层大致的描述了,在我们了解了TCP/IP通信族架构及其原理,接下来我们就开始来了解基于tcp/ip协议层的Socket抽象 ...
- 【Android 应用开发】Android 网络编程 API笔记 - java.net 包 权限 地址 套接字 相关类 简介
Android 网络编程相关的包 : 9 包, 20 接口, 103 类, 6 枚举, 14异常; -- Java包 : java.net 包 (6接口, 34类, 2枚举, 12异常); -- An ...
最新文章
- 部署通用基础设施, 满足顶级 SLA 要求
- 《从零开始学Swift》学习笔记(Day 70)——Swift与Objective-C混合编程之Swift与Objective-CAPI映射...
- java.sql.SQLException: Fail to convert to internal representation
- mysql concat 去掉重复_mysql - concat字段具有共同的重复密钥,并删除那些重复项,从而留下一个 - 堆栈内存溢出...
- boost之内存池使用实例
- java学生背景知识要求,好好学习Java并发 一、背景知识
- 2012/7/25Extjs学习随笔
- 构建自己的不可替代性
- 也谈免拆机破解中兴B860av1.1(解决不能安装软件/解决遥控)
- 关于bootbox.js自定义弹出框样式,如修改宽度等
- Goolge Chrome 浏览器下载不了文件的解决办法
- 互联网B端产品设计经验总结
- qemu启动openEuler Embedded镜像
- html input hiden,input hidden属性
- IntelliJ IDEA 代码提示快捷键
- 隐私政策网址 (URL)
- vcruntime140.dll不可用或缺少
- Python全栈开发【第一篇】:初识Python
- java 爬数据工具 知乎_知乎爬虫之4:抓取页面数据
- <人生重开模拟器>——《Python项目实战》