Android HTTP必知必会
来源:互联网 发布:英文版遗传算法 编辑:程序博客网 时间:2024/05/29 10:23
HTTP协议使用如此广泛,开发者务必要做到“知”,“会”。
引子
用curl请求百度首页全解析的过程:
12345678910111213141516171819202122232425262728293031323334353637
@feng ➜ jayfeng.com (master) ✗ curl -v http://www.baidu.com > ~/http_get.txt* Rebuilt URL to: http://www.baidu.com/% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 119.75.217.109...* Connected to www.baidu.com (119.75.217.109) port 80 (#0)> GET / HTTP/1.1> Host: www.baidu.com> User-Agent: curl/7.43.0> Accept: */*>< HTTP/1.1 200 OK< Date: Thu, 28 Jan 2016 14:53:51 GMT< Content-Type: text/html; charset=utf-8< Transfer-Encoding: chunked< Connection: Keep-Alive< Vary: Accept-Encoding< Set-Cookie: BAIDUID=D75C20ED3D7551221E1C32F79C698867:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com< Set-Cookie: BIDUPSID=D75C20ED3D7551221E1C32F79C698867; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com< Set-Cookie: PSTM=1453992831; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com< Set-Cookie: BDSVRTM=0; path=/< Set-Cookie: BD_HOME=0; path=/< Set-Cookie: H_PS_PSSID=18880_1458_18879_12824_18205_18777_17000_17072_15544_11476_10634; path=/; domain=.baidu.com< P3P: CP=" OTI DSP COR IVA OUR IND COM "< Cache-Control: private< Cxy_all: baidu+1257c154f891fc3a17374fed141622bd< Expires: Thu, 28 Jan 2016 14:53:00 GMT< X-Powered-By: HPHP< Server: BWS/1.1< X-UA-Compatible: IE=Edge,chrome=1< BDPAGETYPE: 1< BDQID: 0xe5ff10b4000dd380< BDUSERID: 0<{ [2880 bytes data]100 98345 0 98345 0 0 665k 0 --:--:-- --:--:-- --:--:-- 671k* Connection #0 to host www.baidu.com left intact
示意图
把上面的过程画成示意图如下:
但是那些代码到底是什么意思呢?
听我慢慢说来。
结构
说起来http的结构确实是简单,从上面的示意图大概也能看出来,包括三部分(请求和响应用/区分):
12345678910
- - - - - - - - - - - - - - - - - - - - - - - - - -| Request Line / Response Line |- - - - - - - - - - - - - - - - - - - - - - - - - -| ... || Request Header / Response Header || ... |- - - - - - - - - - - - - - - - - - - - - - - - - -| Optional Request Body / Optional Response Body || ... |- - - - - - - - - - - - - - - - - - - - - - - - - -
1. 请求行/状态行
以上面百度为例子,请求行是:
12
// 包括了基本的请求方法: GET,请求资源路径: /, HTTP协议版本: HTTP/1.1> GET / HTTP/1.1
状态行是:
12
// 包括服务器响应的HTTP协议版本: HTTP/1.1, 响应状态码: 200, 状态码描述: OK< HTTP/1.1 200 OK
2. 首部
首部可分为请求首部,响应首部, 实体首部,非正式首部,但是这些首部会有一些相同名称的首部,我们把它们定位为通用首部。
请求首部:
1
> User-Agent: curl/7.43.0
响应首部:
1
< Connection: Keep-Alive
实体首部:
1
Content-Type: text/html; charset=utf-8
非正式首部:
1
Set-Cookie: BDSVRTM=0; path=/
更多首部,下一节会专门详解。
3. 实体内容
对于请求消息,如果是POST请求,可以设置请求内容:传参,甚至上传文件。
对于响应消息,返回的主体内容,就是响应内容:网页,图片等资源都是。
首部字段概览
从HTTP的结构来看,HTTP的重头戏当属那些预定义的首部了。
对一些常用字段深入了解是很有必要,这里不做详述,有些字段单独拿出来就能另外再写一篇文章了,请参考文末附录。
常见状态码
HTTP状态码标明客户端HTTP请求的返回结果,结果是否正确,应该怎么处理等信息。
值得注意的几个热点
1. 持久连接
在这个无网不冲浪,推送满天飞的年代,理解持久连接的概念非常重要。
引用wiki的解释:
HTTP持久连接(HTTP persistent connection,也称作HTTP keep-alive或HTTP connection reuse)是使用同一个TCP连接来发送和接收多个HTTP请求/应答,而不是为每一个新的请求/应答打开新的连接的方法。
可以说,http1.1相对于http1.0的一个最大的改进就是默认支持http持久连接了。
在android客户端中如果要关闭持久连接(以google http client为例)
1
request.getHeaders().set("Connection", "close");
另外,关于持久连接造成EOFException的问题,我一直没用找到可靠的解决方案,okhttp的issues下关于这个讨论也是很热闹:
EOFException in RealBufferedSource.readUtf8LineStrict
EOFException in RealBufferedSource.readUtf8LineStrict(): 0-bytes in stream
EOFException in RealBufferedSource.readUtf8LineStrict(): corrupt stream
但是,像xutils3这样的修复方案是真的对吗?
尝试修复Android4.4之前HttpUrlConnection偶发的EOFException问题
直接把4.4之前的长连接给关闭了,虽然干净了,但是是否会对性能造成影响?这个问题的解法是否要联调一下服务器的keepalive_timeout?如果真的和keepalive_timeout,keepalive_timeout设置应该设置多少(这个值不能设置太大,否则可能会把服务器搞挂)?
请高手赐教。
2. 断点续传
断点续传的原理其实非常简单,就是利用HTTP的请求首部中的Range字段。
第一步,计算本地文件大小。
12345678910111213
FileInputStream fis = null;try { // 读取本地文件 fis = new FileInputStream(dest); // currentSize就是本地文件大小 currentSize = fis.available();} catch (IOException e) { throw e;} finally { if (fis != null) { fis.close(); }}
第二步,设置Range值,明确告知服务器从哪里接着下载。
1234567
HttpURLConnection conn;...// 如果本地文件存在,设置RANGE为"bytes=currentSize-", -后面不写具体值,表示接着下载到文件结尾if (currentSize > 0) { conn.setRequestProperty("RANGE", "bytes=" + currentSize + "-");}...
完整代码请参考:http之download方法
PS: 这里只是说明原理,如果是可变文件(比如图片资源一般定义为不变文件),还要考虑文件校验。
3. 上传文件
对上传文件的理解程度某个意义上就代表了你对HTTP结构的理解程度。
第一步,为了后续代码可读性,先定义几个常量。
1234
String BOUNDARY = "--------------" + UUID.randomUUID().toString();String PREFIX = "--",String LINEND = "\r\n";String MULTIPART_FROM_DATA = "multipart/form-data";
第二步,定义Content-Type。
Content-Type为”multipart/form-data”,因为有文件只能以二进制的形式传输。同时定义内容分隔符。
12
// ${bound} 是一个占位符, 为了表示唯一,可以用一些特殊的随机组合,比如---------------4365423423423423Content-Type: multipart/form-data; boundary=${bound}
第三步,传参数(可选)。
传文件并不是说就不能再传参数了。
1234567891011
for (Map.Entry<String, String> entry : params.entrySet()) { sb.append(PREFIX); sb.append(BOUNDARY); sb.append(LINEND); sb.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + LINEND); sb.append("Content-Type: text/plain; charset=GBK" + LINEND); sb.append("Content-Transfer-Encoding: 8bit" + LINEND); sb.append(LINEND); sb.append(entry.getValue()); sb.append(LINEND);}
第四步,传文件。
123456789101112131415161718192021
for (Map.Entry<String, File> file : files.entrySet()) { StringBuilder sb1 = new StringBuilder(); sb1.append(PREFIX); sb1.append(BOUNDARY); sb1.append(LINEND); // 添加文件描述 sb1.append("Content-Disposition: form-data; name=\"uploadfile\"; filename=\"" + file.getValue().getName() + "\"" + LINEND); sb1.append("Content-Type: application/octet-stream; charset=GBK" + LINEND); sb1.append(LINEND); os.write(sb1.toString().getBytes()); is = new FileInputStream(file.getValue()); byte[] buffer = new byte[1024]; int len = 0; while ((len = is.read(buffer)) != -1) { os.write(buffer, 0, len); } is.close(); os.write(LINEND.getBytes());}
第五步,末尾边界。
特别写出这一步是为了强调,请务必注意各个段落的分割。
12
byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes();os.write(end_data);
可以看的出来,所谓上传文件,就是以二进制的形式把这些参数,文件等数据以一定边界区分并拼装在一起发送给服务器。
完整代码请参考:http之upload方法
关于上传如果想了解更多,可以学习一下lite http的部分源码:lite http之content
4. Last Modified和ETag
通过Last Modified作为服务器文件的时间戳,来判断服务器文件是否有更新。
1234567891011
public static long getLastModified(URL url) throws IOException { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(TIME_OUT); connection.setReadTimeout(TIME_OUT); long lastModified = connection.getLastModified(); connection.disconnect(); return lastModified;}
ETag,其实和Last Modified一样,只不过它不是时间戳而是一串标志量,也可以判断服务器文件是否发生变化。
这个我没有使用过,这里不细讲。
具体请参考: ETag使用效果对比&经验分享 、 对站点服务器如何配置ETag
5. HTTPS
HTTPS是在HTTP层之下添加了SSL层,大大增强了数据传输的安全性。
在android中,如何解析https的接口呢?(以下代码因为是多年前代码,可能有些地方欠缺严谨,仅供学习参考)
第一步,生成客户端私钥。
12345678910111213141516171819202122232425
if [ -z $1 ]; thenecho "Usage: importcert.sh <CA cert PEM file>"exit 1fiCACERT=$1BCJAR=bcprov-jdk16-145.jarTRUSTSTORE=../app/src/main/res/raw/mytruststore.bksALIAS=`openssl x509 -inform PEM -subject_hash -noout -in $CACERT`if [ -f $TRUSTSTORE ]; thenrm $TRUSTSTORE || exit 1fiecho "Adding certificate to $TRUSTSTORE..."keytool -import -v -trustcacerts -alias $ALIAS \ -file $CACERT \ -keystore $TRUSTSTORE -storetype BKS \ -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \ -providerpath $BCJAR \ -storepass 123456abcecho ""echo "Added '$CACERT' with alias '$ALIAS' to $TRUSTSTORE..."
使用这个脚本,利用pem文件,最终在res/raw目录下生成一个mytruststore.bks文件。
第二步,根据私钥和密码生成SSLSocketFactory:
123456789101112131415161718192021222324
// 为了更好的性能,这里使用全局静态变量private static SSLSocketFactory sCustomerSSLSocketFactory = null;public static SSLSocketFactory getCustomerSSLSocketFactory(Context context) { if (sCustomerSSLSocketFactory != null) { return sCustomerSSLSocketFactory; } try { KeyStore trusted = KeyStore.getInstance("BKS"); InputStream in = context.getResources().openRawResource(R.raw.mytruststore); try { trusted.load(in, "aike_client".toCharArray()); } finally { in.close(); } sCustomerSSLSocketFactory = new SSLSocketFactory(trusted); sCustomerSSLSocketFactory.setHostnameVerifier(new AllowAllHostnameVerifier()); return sCustomerSSLSocketFactory; } catch(Exception e) { throw new AssertionError(e); }}
第三步,在google http client中使用SSLSocketFactory。
1234567
ApacheHttpTransport.Builder builder = new ApacheHttpTransport.Builder();HttpRequestFactory httpRequestFactory = builder .setSocketFactory(AppConfig.getCustomerSSLSocketFactory(mContext)) .build() .createRequestFactory();HttpRequest request = httpRequestFactory.buildPostRequest(url, content);...
至此https的基本使用流程大概是这样的。
小结
通过对HTTP结构和首部的深入学习,相信大家对http协议的理解会上一个台阶。
如果有兴趣,可自行去拓展学习一下HTTP2.0,SPDY,WebSocket等。
附录
[1]. What really happens when you navigate to a URL
[2]. HTTP专题 by Jerry Qu
[3]. HTTP/2专题 by Jerry Qu
[4]. HTTP 协议中的 Transfer-Encoding
[5]. Http 协议中的Range请求头例子
[6]. HTTP 2.0的那些事
[7]. HTTP持久连接
原文地址: http://jayfeng.com/2016/01/08/Android%20HTTP%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/
- Android HTTP必知必会
- Android Http
- android - http
- android http
- android-http
- Android http
- android http
- android Http
- Android http
- android http
- Android HTTP
- android http
- Android.Http
- Android Http
- android-async-http(http通信)
- [Android]浅析Http框架 - Android-Async-http
- android http post 参数
- android http post请求
- 关于MVC模式的笔记(一)
- 面试总结--阿里杭州
- NSFileManager 使用 详解
- hdu2059 龟兔赛跑--DP
- java锁——ReentrantLock
- Android HTTP必知必会
- HDU5653 Bomber Man wants to bomb an Array. DP
- 数据结构之Trie树
- Vim 学习笔记
- POJ 2976 Dropping tests(二分查找 最大化平均值)
- 数据结构之块状链表
- 搭建一个日常好用的linux系统
- MOB短信验证码开发
- Android打包的那些事