HttpURLConnection 源码解读

我们知道,自Android6.0后HttpClient已经被废除,我们连接网络的选择基本在HttpURLConnection和OkHttp间抉择,本篇准备从源码角度解读HttpURLConnection。

基本使用

HttpURLConnection的使用大体可以分为以下几步:

  1. 初始化URL,使用openConnection获取连接。
  2. 通过HttpURLConnection设置请求参数,connect()开启真正连接。
  3. 通过getOutputStream()写入请求体(POST等)
  4. 通过getInputStream()读取相关内容。
  5. 关闭资源

以下是一个使用HttpURLConnection以GET方式读取字符串的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//获取字符串
public static String loadStringFromURL(String urlString) {
HttpURLConnection httpConn = null;
InputStream is=null;
try {
// 创建url对象
URL urlObj = new URL(urlString);
// 创建HttpURLConnection对象,通过这个对象打开跟远程服务器之间的连接
httpConn = (HttpURLConnection) urlObj.openConnection();
httpConn.connect();//连接

// 判断跟服务器的连接状态。如果是200,则说明连接正常,服务器有响应
if (httpConn.getResponseCode() == 200) {
is=httpConn.getInputStream();//获取输入流
ByteArrayOutputStream baos=new ByteArrayOutputStream();//内存流无需关闭,也根本无法关闭
int len;
byte[] buffer=new byte[1024];
while((len=is.read(buffer))!=-1){
baos.write(buffer,0,len);//写入到内存
}

return baos.toString("utf-8");//从内存中读出
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 关闭流
if(is!=null){
is.close();
}
if(httpConn!=null){
httpConn.disconnect();
}

} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}

POST请求需要注意的两个地方在于:

  1. httpConn.setDoOutput(true)必须设置为可写
  2. setRequestMethod("POST")必须指明请求为POST
  3. getOutputStream()将参数写入请求体
    1
    2
    3
    4
    httpConn = (HttpURLConnection) urlObj.openConnection();
    httpConn.setDoOutput(true);//设置为可写
    httpConn.setRequestMethod("POST");//指明为POST请求
    httpConn.connect();//连接

完整源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//map为表单参数
public static String doPost(String url, Map<String, String> map) {
HttpURLConnection httpConn = null;
BufferedInputStream bis = null;
DataOutputStream dos = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
// 实例化URL对象。调用URL有参构造方法,参数是一个url地址;
URL urlObj = new URL(url);
// 调用URL对象的openConnection()方法,创建HttpURLConnection对象;
httpConn = (HttpURLConnection) urlObj.openConnection();
// 调用HttpURLConnection对象setDoOutput(true)、setDoInput(true)、setRequestMethod("POST");
httpConn.setDoOutput(true);
httpConn.setRequestMethod("POST");
// 设置Http请求头信息;(Accept、Connection、Accept-Encoding、Cache-Control、Content-Type、User-Agent)
httpConn.setUseCaches(false);
httpConn.setRequestProperty("Connection", "Keep-Alive");
httpConn.setRequestProperty("Accept", "*/*");
httpConn.setRequestProperty("Accept-Encoding", "gzip, deflate");
httpConn.setRequestProperty("Cache-Control", "no-cache");
httpConn.setRequestProperty("Content-Type","application/x-www-form-urlencoded");

// 调用HttpURLConnection对象的connect()方法,建立与服务器的真实连接;
httpConn.connect();

// 调用HttpURLConnection对象的getOutputStream()方法构建输出流对象;
dos = new DataOutputStream(httpConn.getOutputStream());
// 获取表单数据,写入到输出流对象
String params=null;
if (map != null && !map.isEmpty()) {
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = map.get(key);
if(params==null){
params=key+"="+ URLEncoder.encode(value,CHAR_SET);
}else{
params+="&"+key+"="+ URLEncoder.encode(value,CHAR_SET);
}

}
}
if(params!=null){
dos.writeBytes(params);//写入请求体中
dos.flush();
}

// 调用HttpURLConnection对象的getInputStream()方法构建输入流对象;
byte[] buffer = new byte[8 * 1024];
int len ;
// 调用HttpURLConnection对象的getResponseCode()获取客户端与服务器端的连接状态码。如果是200,则执行以下操作,否则返回null;
if (httpConn.getResponseCode() == 200) {
bis = new BufferedInputStream(httpConn.getInputStream());
while ((len = bis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.flush();
}
// 将输入流转成字节数组,返回给客户端。
return new String(baos.toByteArray(), CHAR_SET);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(dos!=null){
dos.close();
}
if(bis!=null){
bis.close();
}
baos.close();
if(httpConn!=null){
httpConn.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}

源码解读

通过URL获取连接

在了解URL如何使用之前,先来了解一下URL的构成;
http://username:password@host:8080/directory/file?query#ref;

组成 举例
Protocol http
Authority username:password@host:8080
User Info username:password
Host host
Port 8080
File /directory/file?query
Path /directory/file
Query query
Ref ref

URL的核心构造方法如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

private static URLStreamHandlerFactory streamHandlerFactory;

//key为protocol,value为URLStreamHandler,用来获取连接
private static final Hashtable<String, URLStreamHandler> streamHandlers
= new Hashtable<String, URLStreamHandler>();

transient URLStreamHandler streamHandler;



public URL(String spec) throws MalformedURLException {
this((URL) null, spec, null);
}


public URL(URL context, String spec, URLStreamHandler handler) throws MalformedURLException {
//..
//省略了部分源码
protocol = UrlUtils.getSchemePrefix(spec);//获取协议

if (streamHandler == null) {
setupStreamHandler();//设置URLStreamHandler
if (streamHandler == null) {
throw new MalformedURLException("Unknown protocol: " + protocol);
}
}

//通过URLStreamHandler解析URL
try {
streamHandler.parseURL(this, spec, schemeSpecificPartStart, spec.length());//解析URL组成成分
} catch (Exception e) {
throw new MalformedURLException(e.toString());
}
}

可以看出,构造方法主要做了两件事情。首先根据协议(protocol)来获取一个URLStreamHandler对象,然后调用了streamHandler.parseURL来解析URL,这一流程可能看的一头雾水。那么接下来来看下面这个个方法,就能明白URLStreamHandler的重要性了。

1
2
3
public URLConnection openConnection() throws IOException {
return streamHandler.openConnection(this);
}

通过streamHandler.openConnection就可以获取一个URLConnection对象。
URLStreamHandler是一个抽象类,用来处理URL通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class URLStreamHandler {
//..
//省略了部分源码
//获取URLConnection
protected abstract URLConnection openConnection(URL u) throws IOException;

//解析URL,主要对spec进行解析,然后将各个构成赋值给URL
protected void parseURL(URL url, String spec, int start, int end) {
if (this != url.streamHandler) {
throw new SecurityException("Only a URL's stream handler is permitted to mutate it");
}
if (end < start) {
throw new StringIndexOutOfBoundsException(spec, start, end - start);
}
//...
//省略了解析构成的源码
//设置给URL
setURL(url, url.getProtocol(), host, port, authority, userInfo, path, query, ref);
}

}

既然URLStreamHandler那么重要,那要怎么获取到这个对象呢?答案就在setupStreamHandler中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
void setupStreamHandler() {

streamHandler = streamHandlers.get(protocol);//检查hash表中有没有保存
if (streamHandler != null) {
return;
}

//如果有工厂,就通过工厂创建
if (streamHandlerFactory != null) {
streamHandler = streamHandlerFactory.createURLStreamHandler(protocol);
if (streamHandler != null) {
//创建成功就缓存到哈希表中
streamHandlers.put(protocol, streamHandler);
return;
}
}


//检查java.protocol.handler.pkgs包中有没有,有就创建
String packageList = System.getProperty("java.protocol.handler.pkgs");
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (packageList != null && contextClassLoader != null) {
for (String packageName : packageList.split("\\|")) {
//遍历包名
String className = packageName + "." + protocol + ".Handler";
try {
//加载相应类
Class<?> c = contextClassLoader.loadClass(className);
streamHandler = (URLStreamHandler) c.newInstance();
//创造实例
if (streamHandler != null) {
//创建成功就缓存到哈希表中
streamHandlers.put(protocol, streamHandler);
}
return;
} catch (IllegalAccessException ignored) {
} catch (InstantiationException ignored) {
} catch (ClassNotFoundException ignored) {
}
}
}


//根据协议创建
if (protocol.equals("file")) {
//file
streamHandler = new FileHandler();
} else if (protocol.equals("ftp")) {
//ftp
streamHandler = new FtpHandler();
} else if (protocol.equals("http")) {
//http
try {
String name = "com.android.okhttp.HttpHandler";
streamHandler = (URLStreamHandler) Class.forName(name).newInstance();
} catch (Exception e) {
throw new AssertionError(e);
}
} else if (protocol.equals("https")) {
//htts
try {
String name = "com.android.okhttp.HttpsHandler";
streamHandler = (URLStreamHandler) Class.forName(name).newInstance();
} catch (Exception e) {
throw new AssertionError(e);
}
} else if (protocol.equals("jar")) {
//jar
streamHandler = new JarHandler();
}
if (streamHandler != null) {
//放入hash表
streamHandlers.put(protocol, streamHandler);
}
}

setupStreamHandler()用于获取相应的URLStreamHandler,可以看出,首先回去hashtable中获取是否缓存过,然后通过工厂创建,接着检查java.protocol.handler.pkgs包中有没有,最后会根据协议创建对应的URLStreamHandler,可以看出,在Android新版源码中(据说是4.4开始)底层的Http协议请求使用的是Okhttp。
HttpHandler的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class HttpHandler extends URLStreamHandler {
@Override protected URLConnection openConnection(URL url) throws IOException {
return new OkHttpClient().open(url);//通过Okhttp获取URLConnection
}
@Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
if (url == null || proxy == null) {
throw new IllegalArgumentException("url == null || proxy == null");
}
return new OkHttpClient().setProxy(proxy).open(url);
}
@Override protected int getDefaultPort() {
return 80;
}
}

由于本篇只是解读HttpURLConnection,我们更关心的是HttpURLConnection相关源码,可以看出,通过HttpURLConnectionImpl创建了一个URLConnection。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 public final class HttpHandler extends URLStreamHandler {

@Override protected URLConnection openConnection(URL u) throws IOException {
return new HttpURLConnectionImpl(u, getDefaultPort());
}

@Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
if (url == null || proxy == null) {
throw new IllegalArgumentException("url == null || proxy == null");
}
return new HttpURLConnectionImpl(url, getDefaultPort(), proxy);//HttpURLConnection的实现类
}

@Override protected int getDefaultPort() {
return 80;
}
}

HttpURLConnectionImpl继承于HttpURLConnection。关于HttpURLConnectionImpl的源码后面再说。我们现在只需知道,通过HttpHandler获取到了URLConnection,下面先来看看HttpURLConnection的相关使用。

HttpURLConnection的使用

HttpURLConnection继承于URLConnection,是一个抽象类,实现源码在HttpURLConnectionImpl中
URLConnection的属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 
protected URL url;//URL对象

private String contentType;//内容MIME类型

private static boolean defaultAllowUserInteraction;//默认允许用户交互

private static boolean defaultUseCaches = true;//默认使用缓存

ContentHandler defaultHandler = new DefaultContentHandler();//内容处理器,将URLConnection转为Object

private long lastModified = -1;//最后修改时间

protected long ifModifiedSince;


protected boolean useCaches = defaultUseCaches;//是否使用缓存,默认为true


protected boolean connected; //是否已经连接到远程资源的标识


protected boolean doOutput; //允许发送数据,默认为false


protected boolean doInput = true;//允许接收数据,默认true


protected boolean allowUserInteraction = defaultAllowUserInteraction;//允许用户交互

private static ContentHandlerFactory contentHandlerFactory; //ContentHandler的工厂

private int readTimeout = 0; //读超时

private int connectTimeout = 0; //连接超时


static Hashtable<String, Object> contentHandlers = new Hashtable<String, Object>();//缓存ContentHandler的哈希表


private static FileNameMap fileNameMap; //一个根据文件扩展名来判定内容MIME类型的接口

HttpURLConnection的属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private static final int DEFAULT_CHUNK_LENGTH = 1024;//默认段长度


//允许用户使用的连接类型
private static final String[] PERMITTED_USER_METHODS = {
"OPTIONS",
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
"TRACE"
// Note: we don't allow users to specify "CONNECT"
};

//默认连接类型为GET
protected String method = "GET";

/**
* 响应码
* <p>
* <li>1xx: 临时响应</li>
* <li>2xx: 成功</li>
* <li>3xx: 重定向</li>
* <li>4xx: 客户端错误</li>
* <li>5xx: 服务器错误</li>
*/

protected int responseCode = -1;

//响应消息,与responseCode相对应
protected String responseMessage;


//允许重定向?默认为true
protected boolean instanceFollowRedirects = followRedirects;

private static boolean followRedirects = true;


//HTTP 数据段长度。-1表示不使用
protected int chunkLength = -1;

//请求体的长度,如果超过 int的表示范围(2 GiB),就会返回int的最大值
protected int fixedContentLength = -1;

//请求体的长度
protected long fixedContentLengthLong = -1;

看完以上两个类后,接下来去HttpURLConnectionImpl中看看HttpURLConnection的实现,狗仔方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class HttpURLConnectionImpl extends HttpURLConnection {

private final int defaultPort;//端口

private Proxy proxy;//代理

private final RawHeaders rawRequestHeaders = new RawHeaders();//请求头

private int redirectionCount;//重连次数

protected IOException httpEngineFailure;
protected HttpEngine httpEngine;//Http请求引擎

protected HttpURLConnectionImpl(URL url, int port) {
super(url);
defaultPort = port;
}

protected HttpURLConnectionImpl(URL url, int port, Proxy proxy) {
this(url, port);
this.proxy = proxy;
}

我们知道,可以给Http请求设置请求头。那么源码又是怎么实现的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override public final void setRequestProperty(String field, String newValue) {
if (connected) {
throw new IllegalStateException("Cannot set request property after connection is made");
}
if (field == null) {
throw new NullPointerException("field == null");
}
rawRequestHeaders.set(field, newValue);//可以看出,内部调用了rawRequestHeaders.set
}

@Override public final void addRequestProperty(String field, String value) {
if (connected) {
throw new IllegalStateException("Cannot add request property after connection is made");
}
if (field == null) {
throw new NullPointerException("field == null");
}
rawRequestHeaders.add(field, value);//可以看出,内部调用了rawRequestHeaders.add
}

真正的逻辑代码在RawHeaders类中,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void add(String fieldName, String value) {
if (fieldName == null) {
throw new IllegalArgumentException("fieldName == null");
}
if (value == null) {
return;
}
namesAndValues.add(fieldName);//属性名,namesAndValues为List<String>
namesAndValues.add(value.trim());//值,namesAndValues为List<String>
}

public void set(String fieldName, String value) {
removeAll(fieldName);//移除已存在的头
add(fieldName, value);//添加
}

add和set的区别在于头是否唯一。上面的逻辑主要讲键值对依次加入List中。当然最终会转换为String

1
2
3
4
5
6
7
8
9
10
11
public String toHeaderString() {
StringBuilder result = new StringBuilder(256);
result.append(statusLine).append("\r\n");//追加请求行
for (int i = 0; i < namesAndValues.size(); i += 2) {
//追加请求头
result.append(namesAndValues.get(i)).append(": ")
.append(namesAndValues.get(i + 1)).append("\r\n");
}
result.append("\r\n");
return result.toString();
}

在设置完一系列请求头后,使用connect开启连接。

1
2
3
4
5
6
7
8
9
@Override public final void connect() throws IOException {
initHttpEngine();//初始化引擎
try {
httpEngine.sendRequest();//发送请求
} catch (IOException e) {
httpEngineFailure = e;
throw e;
}
}

connect()中的源码主要做了两件事,首先初始化引擎,然后发送请求。
initHttpEngine()源码如下,将connected标志位设成true,然后检查有没有开启doOutput,如果开启了,就强制设置GET请求方法转为POST等可以写入请求体的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  private void initHttpEngine() throws IOException {
if (httpEngineFailure != null) {
throw httpEngineFailure;
} else if (httpEngine != null) {
return;
}

connected = true;
try {
if (doOutput) {
if (method == HttpEngine.GET) {
method = HttpEngine.POST;
} else if (method != HttpEngine.POST && method != HttpEngine.PUT) {
throw new ProtocolException(method + " does not support writing");
}
}
httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);//初始化HttpEngine
} catch (IOException e) {
httpEngineFailure = e;
throw e;
}
}

//初始化HttpEngine
protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
HttpConnection connection, RetryableOutputStream requestBody) throws IOException
{

return new HttpEngine(this, method, requestHeaders, connection, requestBody);
}

关于HttpEngine的源码这里不做深究,只需了解newHttpEngine初始化完毕后,就可以通过sendRequest()发送请求了。

而Android4.4以上,内部改为Okhttp实现,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private HttpEngine newHttpEngine(String method, Connection connection,
RetryableSink requestBody, Response priorResponse)
{

//构建Request
Request.Builder builder = new Request.Builder()
.url(getURL())
.method(method, null);
Headers headers = requestHeaders.build();
for (int i = 0; i < headers.size(); i++) {
//添加头
builder.addHeader(headers.name(i), headers.value(i));
}

boolean bufferRequestBody = false;
if (HttpMethod.hasRequestBody(method)) {
if (fixedContentLength != -1) {
builder.header("Content-Length", Long.toString(fixedContentLength));
} else if (chunkLength > 0) {
builder.header("Transfer-Encoding", "chunked");
} else {
bufferRequestBody = true;
}
}

Request request = builder.build();
//初始化OkHttpClient
OkHttpClient engineClient = client;
if (engineClient.getOkResponseCache() != null && !getUseCaches()) {
engineClient = client.clone().setOkResponseCache(null);
}

return new HttpEngine(engineClient, request, bufferRequestBody, connection, null, requestBody,
priorResponse);
}

最后

如果你还分不清请求行、请求头和请求体等,建议阅读你应该知道的HTTP基础知识这篇文章,写得非常好。
本期解读到此结束,下一期,OkHttp3。