我们知道,自Android6.0后HttpClient已经被废除,我们连接网络的选择基本在HttpURLConnection和OkHttp间抉择,本篇准备从源码角度解读HttpURLConnection。
基本使用
HttpURLConnection的使用大体可以分为以下几步:
- 初始化URL,使用
openConnection
获取连接。 - 通过HttpURLConnection设置请求参数,
connect()
开启真正连接。 - 通过
getOutputStream()
写入请求体(POST等) - 通过
getInputStream()
读取相关内容。 - 关闭资源
以下是一个使用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请求需要注意的两个地方在于:
httpConn.setDoOutput(true)
必须设置为可写setRequestMethod("POST")
必须指明请求为POSTgetOutputStream()
将参数写入请求体1
2
3
4httpConn = (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
3public 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
21public 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
75void 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
14public final class HttpHandler extends URLStreamHandler {
protected URLConnection openConnection(URL url) throws IOException {
return new OkHttpClient().open(url);//通过Okhttp获取URLConnection
}
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);
}
protected int getDefaultPort() {
return 80;
}
}
由于本篇只是解读HttpURLConnection,我们更关心的是HttpURLConnection相关源码,可以看出,通过HttpURLConnectionImpl
创建了一个URLConnection。
1 | public final class HttpHandler extends URLStreamHandler { |
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
47private 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
22class 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
19public 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
}
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
15public 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中。当然最终会转换为String1
2
3
4
5
6
7
8
9
10
11public 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
9public 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
33private 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。