Android HTTP必知必会

HTTP协议使用如此广泛,开发者务必要做到“知”,“会”。

引子

用curl请求百度首页全解析的过程:

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
@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  Current
Dload  Upload   Total   Spent    Left  Speed
0     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的结构确实是简单,从上面的示意图大概也能看出来,包括三部分(请求和响应用/区分):

1
2
3
4
5
6
7
8
9
10
- - - - - - - - - - - - - - - - - - - - - - - - - -
| Request Line / Response Line                    |
- - - - - - - - - - - - - - - - - - - - - - - - - -
| ...                                             |
| Request Header / Response Header                |
| ...                                             |
- - - - - - - - - - - - - - - - - - - - - - - - - -
| Optional Request Body / Optional Response Body  |
| ...                                             |
- - - - - - - - - - - - - - - - - - - - - - - - - -

1. 请求行/状态行

以上面百度为例子,请求行是:

1
2
// 包括了基本的请求方法: GET,请求资源路径: /, HTTP协议版本: HTTP/1.1
> GET / HTTP/1.1

状态行是:

1
2
// 包括服务器响应的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 优先的语言
Authorization Web认证信息
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-Agent HTTP客户端程序的信息
响应首部字段 从服务器端向客户端返回响应报文时使用的首部。补充了响应的附加内容,也会要求客户端附加额外的内容信息
Accept-Ranges 是否接受字节范围请求
Agente 推算资源创建经过时间
Etag 资源的匹配信息
Location 令客户端重定向至指定URI
Proxy-Authenticate 代理服务器对客户端的认证信息
Retry-After 对再次发起请求的时机要求
Server HTTP服务器的安装信息
Vary 代理服务器缓存的管理信息
WWW-Authenticate 服务器对客户端的认证信息
实体首部字段 针对请求报文和响应报文的实体部分使用的首部。补充了资源内容更新时间等与实体相关的信息
Allow 资源可支持的HTTP方法
Content-Encoding 实体主体适用的编码方式
Content-Language 实体主体的自然语言
Content-Length 实体主体的大小
Content-Location 替代对应资源的URI
Content-MD5 实体主体的报文摘要
Content-Rangesge 实体主体的位置范围
Content-Type 实体主体的媒体类型
Expires 实体主体过期的日期时间
Last-Modified 资源的最后修改日期时间

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

常见状态码

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

状态码 描述
200 OK
301 Moved Permanently
302 Found
304 Not Modified
307 Temporary Redirect
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
410 Gone
500 Internal Server Error
501 Not 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字段。
第一步,计算本地文件大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
FileInputStream fis = null;
try {
    // 读取本地文件
    fis = new FileInputStream(dest);
    // currentSize就是本地文件大小
    currentSize = fis.available();
} catch (IOException e) {
    throw e;
} finally {
    if (fis != null) {
        fis.close();
    }
}

第二步,设置Range值,明确告知服务器从哪里接着下载。

1
2
3
4
5
6
7
HttpURLConnection conn;
...
// 如果本地文件存在,设置RANGE为"bytes=currentSize-", -后面不写具体值,表示接着下载到文件结尾
if (currentSize > 0) {
    conn.setRequestProperty("RANGE", "bytes=" + currentSize + "-");
}
...

完整代码请参考:http之download方法
PS: 这里只是说明原理,如果是可变文件(比如图片资源一般定义为不变文件),还要考虑文件校验。

3. 上传文件

对上传文件的理解程度某个意义上就代表了你对HTTP结构的理解程度。
第一步,为了后续代码可读性,先定义几个常量。

1
2
3
4
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”,因为有文件只能以二进制的形式传输。同时定义内容分隔符。

1
2
// ${bound} 是一个占位符, 为了表示唯一,可以用一些特殊的随机组合,比如---------------4365423423423423
Content-Type: multipart/form-data; boundary=${bound}

第三步,传参数(可选)。
传文件并不是说就不能再传参数了。

1
2
3
4
5
6
7
8
9
10
11
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);
}

第四步,传文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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());
}

第五步,末尾边界。
特别写出这一步是为了强调,请务必注意各个段落的分割。

1
2
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作为服务器文件的时间戳,来判断服务器文件是否有更新。

1
2
3
4
5
6
7
8
9
10
11
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的接口呢?(以下代码因为是多年前代码,可能有些地方欠缺严谨,仅供学习参考)
第一步,生成客户端私钥。

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
if [ -z $1 ]; then
echo "Usage: importcert.sh <CA cert PEM file>"
exit 1
fi

CACERT=$1
BCJAR=bcprov-jdk16-145.jar

TRUSTSTORE=../app/src/main/res/raw/mytruststore.bks
ALIAS=`openssl x509 -inform PEM -subject_hash -noout -in $CACERT`

if [ -f $TRUSTSTORE ]; then
rm $TRUSTSTORE || exit 1
fi

echo "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 123456abc

echo ""
echo "Added '$CACERT' with alias '$ALIAS' to $TRUSTSTORE..."

使用这个脚本,利用pem文件,最终在res/raw目录下生成一个mytruststore.bks文件。

第二步,根据私钥和密码生成SSLSocketFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 为了更好的性能,这里使用全局静态变量
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。

1
2
3
4
5
6
7
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/
资源下载链接为: https://pan.quark.cn/s/5c50e6120579 在Android移动应用开发中,定位功能扮演着极为关键的角色,尤其是在提供导航、本地搜索等服务时,它能够帮助应用获取用户的位置信息。以“baiduGPS.rar”为例,这是一个基于百度地图API实现定位功能的示例项目,旨在展示如何在Android应用中集成百度地图的GPS定位服务。以下是对该技术的详细阐述。 百度地图API简介 百度地图API是由百度提供的一系列开放接口,开发者可以利用这些接口将百度地图的功能集成到自己的应用中,涵盖地图展示、定位、路径规划等多个方面。借助它,开发者能够开发出满足不同业务需求的定制化地图应用。 Android定位方式 Android系统支持多种定位方式,包括GPS(全球定位系统)和网络定位(通过Wi-Fi及移动网络)。开发者可以根据应用的具体需求选择合适的定位方法。在本示例中,主要采用GPS实现高精度定位。 权限声明 在Android应用中使用定位功能前,必须在Manifest.xml文件中声明相关权限。例如,添加<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />,以获取用户的精确位置信息。 百度地图SDK初始化 集成百度地图API时,需要在应用启动时初始化地图SDK。通常在Application类或Activity的onCreate()方法中调用BMapManager.init(),并设置回调监听器以处理初始化结果。 MapView的创建 在布局文件中添加MapView组件,它是地图显示的基础。通过设置其属性(如mapType、zoomLevel等),可以控制地图的显示效果。 定位服务的管理 使用百度地图API的LocationClient类来管理定位服务
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值