认识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(); for(int i = 0; i < size; i++){ String responseHeaderKey = conn.getHeaderFieldKey(i); String responseHeaderValue = conn.getHeaderField(i); sbResponseHeader.append(responseHeaderKey); sbResponseHeader.append(":"); sbResponseHeader.append(responseHeaderValue); sbResponseHeader.append("\n"); } return sbResponseHeader.toString(); } //根据字节数组构建UTF-8字符串 private String getStringByBytes(byte[] bytes) { String str = ""; try { str = new String(bytes, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return str; } //从InputStream中读取数据,转换成byte数组,最后关闭InputStream private byte[] getBytesByInputStream(InputStream is) { byte[] bytes = null; BufferedInputStream bis = new BufferedInputStream(is); ByteArrayOutputStream baos = new ByteArrayOutputStream(); BufferedOutputStream bos = new BufferedOutputStream(baos); byte[] buffer = new byte[1024 * 8]; int length = 0; try { while ((length = bis.read(buffer)) > 0) { bos.write(buffer, 0, length); } bos.flush(); bytes = baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } finally { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } return bytes; } //根据文件名,从asserts目录中读取文件的字节数组 private byte[] getBytesFromAssets(String fileName){ byte[] bytes = null; AssetManager assetManager = getAssets(); InputStream is = null; try{ is = assetManager.open(fileName); bytes = getBytesByInputStream(is); }catch (IOException e){ e.printStackTrace(); } return bytes; } //将表示xml的字节数组进行解析 private String parseXmlResultByBytes(byte[] bytes) { InputStream is = new ByteArrayInputStream(bytes); StringBuilder sb = new StringBuilder(); List<Person> persons = XmlParser.parse(is); for (Person person : persons) { sb.append(person.toString()).append("\n"); } return sb.toString(); } //将表示json的字节数组进行解析 private String parseJsonResultByBytes(byte[] bytes){ String jsonString = getStringByBytes(bytes); List<Person> persons = JsonParser.parse(jsonString); StringBuilder sb = new StringBuilder(); for (Person person : persons) { sb.append(person.toString()).append("\n"); } return sb.toString(); } } }
这个App是用来发送http请求的客户端,除此之外,我还创建了一个JSP的WebProject作为服务端,用Servlet对客户端发来的请求进行处理,Servlet的代码如下所示:
import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.*;import java.util.Enumeration;@WebServlet(name = "MyServlet")public class MyServlet extends HttpServlet { //GET请求 private static final String NETWORK_GET = "NETWORK_GET"; //用POST发送键值对 private static final String NETWORK_POST_KEY_VALUE = "NETWORK_POST_KEY_VALUE"; //用POST发送XML数据 private static final String NETWORK_POST_XML = "NETWORK_POST_XML"; //用POST发送JSON数据 private static final String NETWORK_POST_JSON = "NETWORK_POST_JSON"; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getHeader("action"); //将输入与输出都设置为UTF-8编码 request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=UTF-8"); //response.setHeader("content-type","text/plain;charset=UTF-8"); if(NETWORK_GET.equals(action) || NETWORK_POST_KEY_VALUE.equals(action)){ //对于NETWORK_GET和NETWORK_POST_KEY_VALUE,遍历键值对,并将键值对重新写回到输出结果中 Enumeration<String> parameterNames = request.getParameterNames(); PrintWriter writer = response.getWriter(); while(parameterNames.hasMoreElements()){ String name = parameterNames.nextElement(); String value = request.getParameter(name); if(request.getMethod().toUpperCase().equals("GET")){ //GET请求需要进行编码转换,POST不需要 value = new String(value.getBytes("ISO-8859-1"), "UTF-8"); } writer.write(name + "=" + value + "\n"); } writer.flush(); writer.close(); }else if(NETWORK_POST_XML.equals(action) || NETWORK_POST_JSON.equals(action)){ //对于NETWORK_POST_XML和NETWORK_POST_JSON,将请求体重新写入到响应体的输出流中 //通过request.getInputStream()得到http请求的请求体 BufferedInputStream bis = new BufferedInputStream(request.getInputStream()); //通过response.getOutputStream()得到http请求的响应体 BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream()); byte[] buffer = new byte[1024 * 8]; int length = 0; while ( (length = bis.read(buffer)) > 0){ bos.write(buffer, 0, length); } bos.flush(); bos.close(); bis.close(); }else{ PrintWriter writer = response.getWriter(); writer.write("非法的请求头: action"); writer.flush(); writer.close(); } } }
发送GET请求
由于网络请求耗时而且会阻塞当前线程,所以我们将发送http请求的操作都放到NetworkAsyncTask中,NetworkAsyncTask是继承自AsyncTask。
点击”GET”按钮后,界面如下所示:
GET请求是最简单的http请求,其发送请求的代码如下所示:
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); }
上面的注释写的比较详细了,此处对代码进行一下简单说明。
我们在URL的后面添加了
?name=孙群&age=27
,这样就相当于添加了两个键值对,其形式如?key1=value1&key2=value2&key3=value3
,在?
后面添加键值对,键和值之间用=
相连,键值对之间用&
分隔,服务端可以读取这些键值对信息。HttpURLConnection默认就是用GET发送请求,当然也可以用conn.setRequestMethod(“GET”)将其显式地设置为GET请求。
通过setRequestProperty方法可以设置请求头,既可以是标准的请求头,也可以是自定义的请求头,此处我们设置了自定义的请求头action,用于服务端判断请求的类型。
GET请求容易被缓存,我们可以用conn.setUseCaches(false)禁用缓存。
通过调用connect方法可以让客户端和服务器之间建立TCP连接,建立连接之后不会立即传输数据,只是表示处于connected状态了。该方法不必显式调用,因为在之后的getInputStream方法中会隐式地调用该方法。
调用HttpURLConnection的getInputStream()方法可以获得响应结果的输入流,即服务器向客户端输出的信息,需要注意的是,getInputStream()方法的调用必须在一系列的set方法之后进行。
然后在方法getBytesByInputStream中,通过输入流的read方法得到字节数组,read方法是阻塞式的,每read一次,其实就是从服务器上下载一部分数据,直到将服务器的输出全部下载完成,这样就得到响应体responseBody了。
我们可以通过getReqeustHeader方法读取请求头,代码如下所示:
//读取请求头 private String getReqeustHeader(HttpURLConnection conn) { 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(); }
由上可以看出,以上方法主要还是调用了HttpURLConnection的方法getRequestProperty获取请求头,需要注意的是getRequestProperty方法执行时,客户端和服务器之间必须还未建立TCP连接,即还没有调用connect方法,在connected之后执行getRequestProperty会抛出异常,详见https://github.com/square/okhttp/blob/master/okhttp-urlconnection/src/main/java/okhttp3/internal/huc/HttpURLConnectionImpl.java#L236
我们还可以通过getResponseHeader方法获取响应头,代码如下所示:
//读取响应头 private String getResponseHeader(HttpURLConnection conn) { Map<String, List<String>> responseHeaderMap = conn.getHeaderFields(); int size = responseHeaderMap.size(); StringBuilder sbResponseHeader = new StringBuilder(); for(int i = 0; i < size; i++){ String responseHeaderKey = conn.getHeaderFieldKey(i); String responseHeaderValue = conn.getHeaderField(i); sbResponseHeader.append(responseHeaderKey); sbResponseHeader.append(":"); sbResponseHeader.append(responseHeaderValue); sbResponseHeader.append("\n"); } return sbResponseHeader.toString(); }
通过方法getHeaderFieldKey可以获得响应头的key值,通过方法getHeaderField可以获得响应头的value值。
在后台的Servelt中将键值对信息重新原样写入到客户端,服务端的代码如下所示:
if(NETWORK_GET.equals(action) || NETWORK_POST_KEY_VALUE.equals(action)){ //对于NETWORK_GET和NETWORK_POST_KEY_VALUE,遍历键值对,并将键值对重新写回到输出结果中 Enumeration<String> parameterNames = request.getParameterNames(); PrintWriter writer = response.getWriter(); while(parameterNames.hasMoreElements()){ String name = parameterNames.nextElement(); String value = request.getParameter(name); if(request.getMethod().toUpperCase().equals("GET")){ //GET请求需要进行编码转换,POST不需要 value = new String(value.getBytes("ISO-8859-1"), "UTF-8"); } writer.write(name + "=" + value + "\n"); } writer.flush(); writer.close(); }
在接收到服务端返回的数据后,在AsyncTask的onPostExecute方法中会将Url、请求头、响应头、响应体的值展示在UI上。
用POST发送键值对数据
点击”POST KEY VALUE”按钮,可以用POST发送键值对数据,界面如下所示:
代码如下所示:
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); }
使用POST发送请求的代码与用GET发送请求的代码大部分类似,我们只对其中不同的地方做下说明。
需要通过setRequestMethod将conn设置成POST方法。
如果想用POST发送请求体,那么需要调用setDoOutput方法,将其设置为true。
通过conn.getOutputStream()获得输出流,可以向输出流中写入请求体,最后记得调用输出流的flush方法,注意此时并没有真正将请求体发送到服务器端。
当调用getInputStream方法后,才真正将请求体的内容发送到服务器。
在我们的服务器端的Servlet中,在接收到POST请求发送的键值对数据后,也只是简单地将键值对数据原样写入给客户端,具体代码参见上文,不再赘述。
用POST发送XML数据
点击”POST XML”按钮,可以用POST发送XML数据,界面如下所示:
代码如下所示:
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); }
上面的代码与用POST发送键值对的代码很相似,对其进行简单说明。
上述代码通过getBytesFromAssets方法读取了assets目录下的person.xml文件,将xml文件的字节流作为请求体requestBody,然后将该请求体发送到服务器。
person.xml文件如下所示:
<?xml version="1.0" encoding="utf-8"?><persons> <person id="101"> <name>张三</name> <age>27</age> </person> <person id="102"> <name>李四</name> <age>28</age> </person></persons>
<person>
标签对应着Person
类,Person
类代码如下所示:
package com.ispring.httpurlconnection;public class Person { private String id = ""; private String name = ""; private int age = 0; public String getId(){ return id; } public void setId(String id){ this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return new StringBuilder().append("name:").append(getName()).append(", age:").append(getAge()).toString(); } }
服务端接收到客户端发送来的XML数据之后,只是简单的将其原样写回到客户端,即客户端接收到的响应体还是原来的XML数据,然后客户端通过parseXmlResultByBytes方法对该XML数据进行解析,将字节数组转换成List<Person>
,parseXmlResultByBytes代码如下所示:
//将表示xml的字节数组进行解析 private String parseXmlResultByBytes(byte[] bytes) { InputStream is = new ByteArrayInputStream(bytes); StringBuilder sb = new StringBuilder(); List<Person> persons = XmlParser.parse(is); for (Person person : persons) { sb.append(person.toString()).append("\n"); } return sb.toString(); }
该方法使用了自定义的XmlParser类对XML数据进行解析,其源码如下所示:
package com.ispring.httpurlconnection;import org.xml.sax.Attributes;import org.xml.sax.SAXException;import org.xml.sax.helpers.DefaultHandler;import java.io.InputStream;import java.util.ArrayList;import java.util.List;import javax.xml.parsers.SAXParser;import javax.xml.parsers.SAXParserFactory;public class XmlParser { public static List<Person> parse(InputStream is) { List<Person> persons = new ArrayList<>(); try{ SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser parser = factory.newSAXParser(); PersonHandler personHandler = new PersonHandler(); parser.parse(is, personHandler); persons = personHandler.getPersons(); }catch (Exception e){ e.printStackTrace(); } return persons; } static class PersonHandler extends DefaultHandler { private List<Person> persons; private Person temp; private StringBuilder sb; public List<Person> getPersons(){ return persons; } @Override public void startDocument() throws SAXException { super.startDocument(); persons = new ArrayList<>(); sb = new StringBuilder(); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); sb.setLength(0); if(localName.equals("person")){ temp = new Person(); int length = attributes.getLength(); for(int i = 0; i < length; i++){ String name = attributes.getLocalName(i); if(name.equals("id")){ String value = attributes.getValue(i); temp.setId(value); } } } } @Override public void characters(char[] ch, int start, int length) throws SAXException { super.characters(ch, start, length); sb.append(ch, start, length); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); if(localName.equals("name")){ String name = sb.toString(); temp.setName(name); }else if(localName.equals("age")){ int age = Integer.parseInt(sb.toString()); temp.setAge(age); }else if(localName.equals("person")){ persons.add(temp); } } } }
用POST发送JSON数据
点击”POST JSON”按钮,可以用POST发送JSON数据,界面如下所示:
代码如下所示:
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); }
上面的代码与用POST发送XML的代码很相似,对其进行简单说明。
上述代码通过getBytesFromAssets方法读取了assets目录下的person.json文件,将json文件的字节流作为请求体requestBody,然后将该请求体发送到服务器。
person.json文件如下所示:
{ "persons": [{ "id": "101", "name":"张三", "age":27 }, { "id": "102", "name":"李四", "age":28 }] }
persons数组中的每个元素都对应着一个
Person
对象。服务端接收到客户端发送来的JSON数据之后,只是简单的将其原样写回到客户端,即客户端接收到的响应体还是原来的JSON数据,然后客户端通过parseJsonResultByBytes方法对该XML数据进行解析,将字节数组转换成List,parseXmlResultByBytes代码如下所示:
//将表示json的字节数组进行解析 private String parseJsonResultByBytes(byte[] bytes){ String jsonString = getStringByBytes(bytes); List<Person> persons = JsonParser.parse(jsonString); StringBuilder sb = new StringBuilder(); for (Person person : persons) { sb.append(person.toString()).append("\n"); } return sb.toString(); }
该方法又使用了自定义的JsonParset类,源码如下所示:
package com.ispring.httpurlconnection;import org.json.JSONArray;import org.json.JSONException;import org.json.JSONObject;import java.util.ArrayList;import java.util.List;public class JsonParser { public static List<Person> parse(String jsonString){ List<Person> persons = new ArrayList<>(); try{ JSONObject jsonObject = new JSONObject(jsonString); JSONArray jsonArray = jsonObject.getJSONArray("persons"); int length = jsonArray.length(); for(int i = 0; i < length; i++){ JSONObject personObject = jsonArray.getJSONObject(i); String id = personObject.getString("id"); String name = personObject.getString("name"); int age = personObject.getInt("age"); Person person = new Person(); person.setId(id); person.setName(name); person.setAge(age); persons.add(person); } }catch (JSONException e){ e.printStackTrace(); } return persons; } }
其他
如果Http请求体的数据很大,就可以认为该请求主要是完成数据上传的作用;如果响应体的数据很大,就可以认为该请求主要完成数据下载的作用。
上面我们通过demo演示了如何上传XML文件和JSON文件,并对二者进行解析。在上传的过程中,Android要写入Content-Length这个请求头,Content-Length就是请求体的字节长度,注意是字节长度,而不是字符长度(汉字等会占用两个字节)。默认情况下,Android为了得到Content-Length的长度,Android会把请求体放到内存中的,直到输出流调用了close方法后,才会读取内存中请求体的字节长度,将其作为请求头Content-Length。当要上传的请求体很大时,这会非常占用内存,为此Android提供了两个方法来解决这个问题。
setFixedLengthStreamingMode (int contentLength)
如果请求体的大小是知道的,那么可以调用HttpURLConnection的setFixedLengthStreamingMode (int contentLength) 方法,该方法会告诉Android要传输的请求头Content-Length的大小,这样Android就无需读取整个请求体的大小,从而不必一下将请求体全部放到内存中,这样就避免了请求体占用巨大内存的问题。setChunkedStreamingMode (int chunkLength)
如果请求体的大小不知道,那么可以调用setChunkedStreamingMode (int chunkLength)方法。该方法将传输的请求体分块传输,即将原始的数据分成多个数据块,chunkLength表示每块传输的字节大小。比如我们要传输的请求体大小是10M,我们将chunkLength设置为1024 * 1024 byte,即1M,那么Android会将请求体分10次传输,每次传输1M,具体的传输规则是:每次传输一个数据块时,首先在一行中写明该数据块的长度,比如1024 * 1024,然后在后面的一行中写入要传输的数据块的字节数组,再然后是一个空白行,这样第一数据块就这样传输,在空白行之后就是第二个数据块的传输,与第一个数据块的格式一样,直到最后没有数据块要传输了,就在用一行写明要传输的字节为0,这样在服务器端就知道读取完了整个请求体了。如果设置的chunkLength的值为0,那么表示Android会使用默认的一个值作为实际的chunkLength。使用setChunkedStreamingMode方法的前提是服务器支持分块数据传输,分块数据传输是从HTTP 1.1开始支持的,所以如果你的服务器只支持HTTP 1.0的话,那么不能使用setChunkedStreamingMode方法。
希望本文对大家使用HttpURLConnection有所帮助!
共同学习,写下你的评论
评论加载中...
作者其他优质文章