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 请求和响应过程
但是那些代码到底是什么意思呢?
听我慢慢说来。

结构

说起来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的重头戏当属那些预定义的首部了。

首部字段名说明通用首部字段请求报文和响应报文两方都会使用的首部CacheControl控制缓存的行Connection允许客户端和服务器指定与请求/响应连接有关的选项Date报文创建时间Progma报文指令Trailer报文末端的首部一览Transfer-Encoding指定报文主体的传输编码方式Upgrade升级为其它协议Via代理服务器的相关信息Warning错误通知请求首部字段从客户端向服务器端发送请求报文时使用的首部。补充了请求的附加内容、客户端信息、响应内容相关优先级等信息Accept用户代理可处理的媒体类型Accept-Charset优先的字符集Content-Encoding优先的内容编码Connectionntent-Language优先的语言AuthorizationWeb认证信息Expect期待服务器的特定行为From用户的电子邮箱地址Host请求资源所在服务器If-Match比较实体标记(ETag)If-Modified-Since比较资源的更新时间If-None-Match比较实体标记较实体标记(与If-Match相反)If-Range资源未更新时发送实体Byte的范围请求If-Unmodified-Since比较资源的更新æ¶间(与If-Modified-Since相反)Max-Forwards最大传输逐跳数Proxy-Authorization代理服务器要求客户端的认证信息Range实体的字节范围请求Referer对请求中URI的原始获取方TE传输编码的优先级User-AgentHTTP客户端程序的信息响应首部字段从服务器端向客户端返回响应报文时使用的首部。补充了响应的附加内容,也会要求客户端附加额外的内容信息Accept-Ranges是否接受字节范围请求Agente推算资源创建经过时间Etag资源的匹配信息Location令客户端重定向至指定URIProxy-Authenticate代理服务器对客户端的认证信息Retry-After对再次发起请求的时机要求ServerHTTP服务器的安装信息Vary代理服务器缓存的管理信息WWW-Authenticate服务器对客户端的认证信息实体首部字段针对请求报文和响应报文的实体部分使用的首部。补充了资源内容更新时间等与实体相关的信息Allow资源可支持的HTTP方法Content-Encoding实体主体适用的编码方式Content-Language实体主体的自然语言Content-Length实体主体的大小Content-Location替代对应资源的URIContent-MD5实体主体的报文摘要Content-Rangesge实体主体的位置范围Content-Type实体主体的媒体类型Expires实体主体过期的日期时间Last-Modified资源的最后修改日期时间

对一些常用字段深入了解是很有必要,这里不做详述,有些字段单独拿出来就能另外再写一篇文章了,请参考文末附录。

常见状态码

HTTP状态码标明客户端HTTP请求的返回结果,结果是否正确,应该怎么处理等信息。

状态码描述200OK301Moved Permanently302Found304Not Modified307Temporary Redirect400Bad Request401Unauthorized403Forbidden404Not Found410Gone500Internal Server Error501Not Implemented

值得注意的几个热点

1. 持久连接

在这个无网不冲浪,推送满天飞的年代,理解持久连接的概念非常重要。
引用wiki的解释:

HTTP持久连接(HTTP persistent connection,也称作HTTP keep-alive或HTTP connection reuse)是使用同一个TCP连接来发送和接收多个HTTP请求/应答,而不是为每一个新的请求/应答打开新的连接的方法。

可以说,http1.1相对于http1.0的一个最大的改进就是默认支持http持久连接了。
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/
0 0