Android中HttpURLConnection使用详解
认识Http协议
Android中发送http网络请求是很常见的,要有GET请求和POST请求。一个完整的http请求需要经历两个过程:客户端发送请求到服务器,然后服务器将结果返回给客户端,如下图所示:
-
客户端->服务器
客户端向服务器发送请求主要包含以下信息:请求的Url地址、请求头以及可选的请求体,打开百度首页,客户端向服务器发送的信息如下所示:-
请求URL(Request URL)
上图中的Request URL就是请求的Url地址,即https://www.baidu.com,该Url没有附加其他的参数。其实可以通过?和&符向URL地址后面追加一系列的键值对参数,比如地址https://www.baidu.com/s?ie=utf-8&wd=Android,该Url包含两个键值对,ie=utf-8,以及wd=Android,ie和wd是key,utf-8和Android分别是其对应的value,服务端可以获取ie和wd所对应的value的值。由此我们可以看出,Url可以携带额外的数据信息。一般情况下,URL的长度不能超过2048个字符,即2KB,超过此限制的话服务器可能就不识别。 -
请求头(Request Headers)
上图中Request Headers部分就是请求头,请求头其实也是一些键值对,不过这些键值通常都是W3C定义了的一些标准的Http请求头的名称,请求头包含了客户端想告诉服务端的一些元数据信息,注意是元数据,而不是数据,比如请求头User-Agent会告诉服务器这条请求来自于什么浏览器,再比如请求头Accept-Encoding会告诉服务器客户端支持的压缩格式。除了这些标准的请求头,我们还可以添加自定义的请求头。 -
请求体(Request Body)
之前我们提到,URL的最大长度就是2048个字符,如果我们发送的数据很大,超过了2KB怎么办?我们可以将很大的数据放到请求体中,GET请求不支持请求体,只有POST请求才能设置请求体。请求体中可以放置任意的字节流,从而可以很方便地发送任意格式的数据,服务端只需要读取该输入流即可。
-
-
服务器->客户端
服务器接收到客户端发来的请求后,会进行相应的处理,并向客户端输出信息,输出的信息包括响应头和响应体。-
响应头 (Response Headers)
响应头也是一些键值对,如下所示:响应头包含了服务器想要告诉客户端的一些元数据信息,注意不是数据,是元数据,比如通过响应头Content-Encoding告诉客户端服务器所采用的压缩格式,响应头Content-Type告诉客户端响应体是什么格式的数据,再比如服务端可以通过多个Set-Cookie响应头向客户端写入多条Cookie信息,等等。刚刚提到的几个请求头都是W3C规定的标准的请求头名称,我们也可以在服务端向客户端写入自定义的响应头。
-
响应体 (Response Body)
响应体是服务端向客户端传输的实际的数据信息,本质就是一堆字节流,可以表示文本,也可以表示图片或者其他格式的信息,如下所示:
-
GET vs POST
Http协议支持的操作有GET、POST、HEAD、PUT、TRACE、OPTIONS、DELETE,其中最最常用的还是GET和POST操作,下面我们看一下GET和POST的区别。
GET:
- GET请求可以被缓存。
- 我们之前提到,当发送键值对信息时,可以在URL上面直接追加键值对参数。当用GET请求发送键值对时,键值对会随着URL一起发送的。
- 由于GET请求发送的键值对时随着URL一起发送的,所以一旦该URL被黑客截获,那么就能看到发送的键值对信息,所以GET请求的安全性很低,不能用GET请求发送敏感的信息(比如用户名密码)。
- 由于URL不能超过2048个字符,所以GET请求发送数据是有长度限制的。
- 由于GET请求较低的安全性,我们不应该用GET请求去执行增加、删除、修改等的操作,应该只用它获取数据。
POST:
- POST请求从不会被缓存。
- POST请求的URL中追加键值对参数,不过这些键值对参数不是随着URL发送的,而是被放入到请求体中发送的,这样安全性稍微好一些。
- 应该用POST请求发送敏感信息,而不是用GET。
- 由于可以在请求体中发送任意的数据,所以理论上POST请求不存在发送数据大小的限制。
- 当执行增减、删除、修改等操作时,应该使用POST请求,而不应该使用GET请求。
HttpURLConnection vs DefaultHttpClient
在Android API Level 9(Android 2.2)之前之能使用DefaultHttpClient类发送http请求。DefaultHttpClient是Apache用于发送http请求的客户端,其提供了强大的API支持,而且基本没有什么bug,但是由于其太过复杂,Android团队在保持向后兼容的情况下,很难对DefaultHttpClient进行增强。为此,Android团队从Android API Level 9开始自己实现了一个发送http请求的客户端类——–HttpURLConnection。
相比于DefaultHttpClient,HttpURLConnection比较轻量级,虽然功能没有DefaultHttpClient那么强大,但是能够满足大部分的需求,所以Android推荐使用HttpURLConnection代替DefaultHttpClient,并不强制使用HttpURLConnection。
但从Android API Level 23(Android 6.0)开始,不能再在Android中使用DefaultHttpClient,强制使用HttpURLConnection。
Demo介绍
为了演示HttpURLConnection的常见用法,我做了一个App,界面如下所示:
主界面MainActivity有四个按钮,分别表示用GET发送请求、用POST发送键值对数据、用POST发送XML数据以及用POST发送JSON数据,点击对应的按钮会启动NetworkActivity并执行相应的操作。
NetworkActivity的源码如下所示,此处先贴出代码,后面会详细说明。
package com.ispring.httpurlconnection;
import android.content.Intent;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class NetworkActivity extends AppCompatActivity {
private NetworkAsyncTask networkAsyncTask = new NetworkAsyncTask();
private TextView tvUrl = null;
private TextView tvRequestHeader = null;
private TextView tvRequestBody = null;
private TextView tvResponseHeader = null;
private TextView tvResponseBody = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_network);
tvUrl = (TextView) findViewById(R.id.tvUrl);
tvRequestHeader = (TextView) findViewById(R.id.tvRequestHeader);
tvRequestBody = (TextView) findViewById(R.id.tvRequestBody);
tvResponseHeader = (TextView) findViewById(R.id.tvResponseHeader);
tvResponseBody = (TextView) findViewById(R.id.tvResponseBody);
Intent intent = getIntent();
if (intent != null && intent.getExtras() != null) {
String networkAction = intent.getStringExtra("action");
networkAsyncTask.execute(networkAction);
}
}
//用于进行网络请求的AsyncTask
class NetworkAsyncTask extends AsyncTask<String, Integer, Map<String, Object>> {
//NETWORK_GET表示发送GET请求
public static final String NETWORK_GET = "NETWORK_GET";
//NETWORK_POST_KEY_VALUE表示用POST发送键值对数据
public static final String NETWORK_POST_KEY_VALUE = "NETWORK_POST_KEY_VALUE";
//NETWORK_POST_XML表示用POST发送XML数据
public static final String NETWORK_POST_XML = "NETWORK_POST_XML";
//NETWORK_POST_JSON表示用POST发送JSON数据
public static final String NETWORK_POST_JSON = "NETWORK_POST_JSON";
@Override
protected Map<String, Object> doInBackground(String... params) {
Map<String,Object> result = new HashMap<>();
URL url = null;//请求的URL地址
HttpURLConnection conn = null;
String requestHeader = null;//请求头
byte[] requestBody = null;//请求体
String responseHeader = null;//响应头
byte[] responseBody = null;//响应体
String action = params[0];//http请求的操作类型
try {
if (NETWORK_GET.equals(action)) {
//发送GET请求
url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet?name=孙群&age=27");
conn = (HttpURLConnection) url.openConnection();
//HttpURLConnection默认就是用GET发送请求,所以下面的setRequestMethod可以省略
conn.setRequestMethod("GET");
//HttpURLConnection默认也支持从服务端读取结果流,所以下面的setDoInput也可以省略
conn.setDoInput(true);
//用setRequestProperty方法设置一个自定义的请求头:action,由于后端判断
conn.setRequestProperty("action", NETWORK_GET);
//禁用网络缓存
conn.setUseCaches(false);
//获取请求头
requestHeader = getReqeustHeader(conn);
//在对各种参数配置完成后,通过调用connect方法建立TCP连接,但是并未真正获取数据
//conn.connect()方法不必显式调用,当调用conn.getInputStream()方法时内部也会自动调用connect方法
conn.connect();
//调用getInputStream方法后,服务端才会收到请求,并阻塞式地接收服务端返回的数据
InputStream is = conn.getInputStream();
//将InputStream转换成byte数组,getBytesByInputStream会关闭输入流
responseBody = getBytesByInputStream(is);
//获取响应头
responseHeader = getResponseHeader(conn);
} else if (NETWORK_POST_KEY_VALUE.equals(action)) {
//用POST发送键值对数据
url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet");
conn = (HttpURLConnection) url.openConnection();
//通过setRequestMethod将conn设置成POST方法
conn.setRequestMethod("POST");
//调用conn.setDoOutput()方法以显式开启请求体
conn.setDoOutput(true);
//用setRequestProperty方法设置一个自定义的请求头:action,由于后端判断
conn.setRequestProperty("action", NETWORK_POST_KEY_VALUE);
//获取请求头
requestHeader = getReqeustHeader(conn);
//获取conn的输出流
OutputStream os = conn.getOutputStream();
//获取两个键值对name=孙群和age=27的字节数组,将该字节数组作为请求体
requestBody = new String("name=孙群&age=27").getBytes("UTF-8");
//将请求体写入到conn的输出流中
os.write(requestBody);
//记得调用输出流的flush方法
os.flush();
//关闭输出流
os.close();
//当调用getInputStream方法时才真正将请求体数据上传至服务器
InputStream is = conn.getInputStream();
//获得响应体的字节数组
responseBody = getBytesByInputStream(is);
//获得响应头
responseHeader = getResponseHeader(conn);
} else if (NETWORK_POST_XML.equals(action)) {
//用POST发送XML数据
url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet");
conn = (HttpURLConnection) url.openConnection();
//通过setRequestMethod将conn设置成POST方法
conn.setRequestMethod("POST");
//调用conn.setDoOutput()方法以显式开启请求体
conn.setDoOutput(true);
//用setRequestProperty方法设置一个自定义的请求头:action,由于后端判断
conn.setRequestProperty("action", NETWORK_POST_XML);
//获取请求头
requestHeader = getReqeustHeader(conn);
//获取conn的输出流
OutputStream os = conn.getOutputStream();
//读取assets目录下的person.xml文件,将其字节数组作为请求体
requestBody = getBytesFromAssets("person.xml");
//将请求体写入到conn的输出流中
os.write(requestBody);
//记得调用输出流的flush方法
os.flush();
//关闭输出流
os.close();
//当调用getInputStream方法时才真正将请求体数据上传至服务器
InputStream is = conn.getInputStream();
//获得响应体的字节数组
responseBody = getBytesByInputStream(is);
//获得响应头
responseHeader = getResponseHeader(conn);
} else if (NETWORK_POST_JSON.equals(action)) {
//用POST发送JSON数据
url = new URL("http://192.168.31.200:8080/HttpServer/MyServlet");
conn = (HttpURLConnection) url.openConnection();
//通过setRequestMethod将conn设置成POST方法
conn.setRequestMethod("POST");
//调用conn.setDoOutput()方法以显式开启请求体
conn.setDoOutput(true);
//用setRequestProperty方法设置一个自定义的请求头:action,由于后端判断
conn.setRequestProperty("action", NETWORK_POST_JSON);
//获取请求头
requestHeader = getReqeustHeader(conn);
//获取conn的输出流
OutputStream os = conn.getOutputStream();
//读取assets目录下的person.json文件,将其字节数组作为请求体
requestBody = getBytesFromAssets("person.json");
//将请求体写入到conn的输出流中
os.write(requestBody);
//记得调用输出流的flush方法
os.flush();
//关闭输出流
os.close();
//当调用getInputStream方法时才真正将请求体数据上传至服务器
InputStream is = conn.getInputStream();
//获得响应体的字节数组
responseBody = getBytesByInputStream(is);
//获得响应头
responseHeader = getResponseHeader(conn);
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//最后将conn断开连接
if (conn != null) {
conn.disconnect();
}
}
result.put("url", url.toString());
result.put("action", action);
result.put("requestHeader", requestHeader);
result.put("requestBody", requestBody);
result.put("responseHeader", responseHeader);
result.put("responseBody", responseBody);
return result;
}
@Override
protected void onPostExecute(Map<String, Object> result) {
super.onPostExecute(result);
String url = (String)result.get("url");//请求的URL地址
String action = (String) result.get("action");//http请求的操作类型
String requestHeader = (String) result.get("requestHeader");//请求头
byte[] requestBody = (byte[]) result.get("requestBody");//请求体
String responseHeader = (String) result.get("responseHeader");//响应头
byte[] responseBody = (byte[]) result.get("responseBody");//响应体
//更新tvUrl,显示Url
tvUrl.setText(url);
//更新tvRequestHeader,显示请求头
if (requestHeader != null) {
tvRequestHeader.setText(requestHeader);
}
//更新tvRequestBody,显示请求体
if(requestBody != null){
try{
String request = new String(requestBody, "UTF-8");
tvRequestBody.setText(request);
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
}
//更新tvResponseHeader,显示响应头
if (responseHeader != null) {
tvResponseHeader.setText(responseHeader);
}
//更新tvResponseBody,显示响应体
if (NETWORK_GET.equals(action)) {
String response = getStringByBytes(responseBody);
tvResponseBody.setText(response);
} else if (NETWORK_POST_KEY_VALUE.equals(action)) {
String response = getStringByBytes(responseBody);
tvResponseBody.setText(response);
} else if (NETWORK_POST_XML.equals(action)) {
//将表示xml的字节数组进行解析
String response = parseXmlResultByBytes(responseBody);
tvResponseBody.setText(response);
} else if (NETWORK_POST_JSON.equals(action)) {
//将表示json的字节数组进行解析
String response = parseJsonResultByBytes(responseBody);
tvResponseBody.setText(response);
}
}
//读取请求头
private String getReqeustHeader(HttpURLConnection conn) {
//https://github.com/square/okhttp/blob/master/okhttp-urlconnection/src/main/java/okhttp3/internal/huc/HttpURLConnectionImpl.java#L236
Map<String, List<String>> requestHeaderMap = conn.getRequestProperties();
Iterator<String> requestHeaderIterator = requestHeaderMap.keySet().iterator();
StringBuilder sbRequestHeader = new StringBuilder();
while (requestHeaderIterator.hasNext()) {
String requestHeaderKey = requestHeaderIterator.next();
String requestHeaderValue = conn.getRequestProperty(requestHeaderKey);
sbRequestHeader.append(requestHeaderKey);
sbRequestHeader.append(":");
sbRequestHeader.append(requestHeaderValue);
sbRequestHeader.append("\n");
}
return sbRequestHeader.toString();
}
//读取响应头
private String getResponseHeader(HttpURLConnection conn) {
Map<String, List<String>> responseHeaderMap = conn.getHeaderFields();
int size = responseHeaderMap.size();
StringBuilder sbResponseHeader = new StringBuilder();