Android HttpClient文件下载
临近上线,运维布置了测试、预发布、正式环境,几乎常见的下载方式都碰上了,踩过一个又一个坑,闲下来总结一下吧。这里用的是android-async-http
1. 基础概念
1.1 域名、DNS
域名即我们常说的网址前半部分,因为服务器IP地址难记而出的解决方案。我们发起一个请求时,首先就要把域名转换成对应的ip地址。这一步能做很多文章,因为这个对应关系是可被篡改的。比如
- 我们现有的所有操作系统,其找这个对应关系的优先级首先都是找本地hosts文件(windows路径是
C:\Windows\System32\drivers\etc\hosts,其他基本为/etc/hosts),基于此我们就可以简单的在文件里配置一些好记的域名映射到我们的内部服务器,比如package.xxx.topbuild.client.comdownload.client.com - 如果在
hosts里没找到映射关系,下一步就该去问我们配置的dns服务器了,手机wifi、电脑、路由器只要改成手动获取IP,基本都可以设置这个服务器地址。像我们内网有非技术时,搭建一个DNS服务器来做上面的自定义域名映射就很有必要 - 全世界这么多域名,买了一个或者修改了映射怎么告知其他所有人我这域名是对应哪个IP呢,这就有我们说的9个根服务器,我们去dns服务器找映射时,如果它在本地缓存没有,那就会去它上级dns找,上级也没有就继续往上,一直到根服务器。我们在阿里云或万网买了或修改了域名映射的IP后,基本都会上报更新到根服务器或某级DNS服务器,然后才能看到改变。
- 全球通网的多了之后,就会发现不同地区访问某台服务器会太卡了,这时域名商就想出了个法子,叫CDN(内容分发网络)。你们不都是要来问域名商这个域名是到哪个IP吗?它就看你是哪个地区的,返回给你个近点的IP,这个IP是域名商自己的服务器,它把你服务器的资源缓存起来了。别人来请求的时候,第一次它找不到,会去你自己的实际服务器(称之为源站)要(这过程称之为回源),第一次之后它就会缓存起来,下次再请求就直接给缓存了。所以开了CDN的第一次访问下载慢是正常的
1.2 HTTPS
HTTP是明文传输的,随着对安全性的需求越来越高,现在网站基本都开始使用起HTTPS了。HTTPS是在HTTP基础上,加了TLS/SSL协议进行加密传输。客户端从DNS服务器或hosts拿到服务器IP后,建立连接三次握手时,服务器会返回域名的证书给客户端,客户端能从中解析出证书链,看其根证书是否为内置的可信组织颁发且未过期,客户端校验后发送自己公钥给服务器,之后传输用非对称加密,即彼此以自己的私钥加密,对方用握手接受到的公钥进行解密。加解密算法也是在握手时协商随机确定。
目前微信小游戏和苹果在线安装软件 page37 都需要用到HTTPS,特别是苹果安装,从plist到ipa都严格校验了证书合法性,这也是内网服务器分发ios安装包必须使用域名的原因。
1.3 调试工具
- WireShark 查看数据包工具,能看到完整握手流程,如截图所示:

- curl 简单而强大的命令行工具,能快捷获取到很多详细的网络信息
最常用的测试命令curl -v 网址,比如- 证书过期

- 详细信息和响应头

- 证书过期
- Fiddler 代理服务器工具,只能捕获http和https请求,当调试ios等不方便查看日志或release包时,用fillder开个代理服务器并在手机上设置,就能看到它到底请求了哪些网址并返回了什么,以此来定位问题。

2. 响应头
参考完整文档HTTP 响应头信息
调试时可以用curl查看,或者浏览器F12打开开发工具的网络标签页查看对应的请求的详细信息。
2.1 Content-Encoding
表示以何种编码格式进行传输,相对应的客户端也可以在请求头上带上Accept-Encoding表明自己能处理的编码格式,通常为gzip或deflate,是两种不同的压缩方式。如果是不压缩传输,则不应有这个字段。浏览器或客户端收到数据后,应该根据这个值做对应的解压缩后,才是真正的内容。
2.2 Content-Length
表示网络传输的数据长度,不一定是实际内容的长度,因为像2.1可能压缩后再传输,这时Content-Length是压缩后的大小而非源文件大小。在统计下载进度时,需要谨记:下载的内容长度和Content-Length对应,而非解压后的写入文件的长度。
2.3 Transfer-Encoding
分块传输编码
一般只要有值即为chunked。用于那些持续返回内容,连服务器自己都不知道要发给你多长内容的情况。比如下载大文件,又开启了gzip,就有可能一边压缩一边传输,这时因为没压缩完,服务器也没法给你Content-Length,这时就是分块传输的用武之地。它是把数据分块,每块都是固定的格式,比如前面几个字段告诉你这块有多少内容,接着是实际内容,最后还有一些结尾附加字段,客户端处理分块传输,应该把各分块的头尾去掉,取中间内容,有必要就解压后,一块块合并之后才是真正的总内容。
我们偶尔浏览器下载文件时,会遇到不显示总大小的,不用怀疑,就是用了chunked传输。
2.4 Accept-Ranges
表示服务器支持的范围的单位,通常为none或bytes,意味着不支持断点续传和按字节续传。在Accept-Ranges为bytes的情况下,客户端发起请求是可以带上请求头Range: bytes=START-END的格式,START和END分别代表你想接收数据的起始和结束字节位置, END可为空表示接收剩余全部内容。服务器接收到带Range头的请求后,返回信息会带上头Content-Range: bytes:START-END/TOTAL START和END分别代表这次返回内容是全部内容的起始和结束字节位置,TOTAL则是总字节数。
3. 踩坑记录
3.1 SNI
如今的服务器,往往上面会不止运行一个网站,不同的域名可以映射到不同的目录、端口或处理程序,此项技术通常称之为虚拟主机(vhosts),这些不同域名在dns上必然都是指向同一台服务器,同一个IP,而我们建立连接时底层肯定只能通过IP,而HTTPS的证书又是在TCP握手时下发的,服务器该怎么知道要给你那个域名的证书呢? 这就是这次踩的坑: SNI,它是指在握手ClientHello阶段让客户端带上域名信息server_name,直接看Wireshark解包:

这是2006年发布,在SSLv3/TLSv1就已被启用的特性,按理应该被现今大多数库支持,很不幸,我们用的库没封装好不支持!详细bug信息和分析可查看转载的另一篇文章:一次 TLS SNI 问题
3.2 断点续传
从2.4我们知道断点续传其实只是带不带上某个请求头,所以底层肯定是支持的,我们的问题发生情形为:下载某个大文件,下载到中间时,网络突然断开,在触发超时下载失败前,重新连接上网络,这时我们的库并没有断点续传而是从头开始下载,这虽然体验不好但也没啥大问题,关键是他从头下载时之前下载的内容未删除,导致两次下载内容合并了。See FileAsyncHttpResponseHandler.java和RangeFileAsyncHttpResponseHandler.java。为了能断点续传和解决此问题,我们干脆直接把文件下载的换成了RangeFileAsyncHttpResponseHandler
3.3 chunked+ContentLength
这次是改3.2冒出的新问题,问题出在RangeFileAsyncHttpResponseHandler.java#L80,这次运维突然说要试试oss(对象存储),发现返回用的是chunked传输,2.3我们知道这种方式是拿不到ContentLength的,很不幸,这个类去取了还拿去做了判断。因为是直接用的打包好的库而非源码,只好写了个继承自它,修改此方法,判断是chunked传输则忽略长度判断。
3.4 gzip+ContentLength
这次跟3.3问题差不多,还是拿ContentLength判断的问题: RangeFileAsyncHttpResponseHandler.java#L86。我们之前说过,ContentLength是传输大小,而此代码中累加的current则是已下载文件大小,假如传输用了gzip压缩,而这又是个循环读取+判断,就会导致current累加到最后比ContentLength大时,还有部分数据没取完,而循环却要结束了。这次已经忍不了了,改为源码引用直接修改删掉了长度判断。
3.5 Proxy
改完后终于顺利了一段时间,倒要区分预发布和正式环境时,运维提供了一个代理服务器,即直接访问是正式环境,设置代理后连接的预发布环境,在大部分手机上表现很正常,但爱国为就不一样了,用这个库下载时竟然没走系统设置的代理而跑到正式环境了,还好浏览器下载也有类似问题,应该是华为系统本身限制,故没再纠结原因,直接找了接口,获取系统代理,创建下载时手动设置。
本文详细介绍了在Android中使用HttpClient进行文件下载时的基础概念,包括域名解析、HTTPS加密以及调试工具的使用。在响应头部分,讲解了Content-Encoding、Content-Length、Transfer-Encoding和Accept-Ranges等关键字段。在踩坑记录中,重点讨论了SNI问题、断点续传的实现、chunked与ContentLength的冲突以及gzip压缩的问题,并给出了相应的解决方案。
2617

被折叠的 条评论
为什么被折叠?



