前言html
http协议是互联网中最重要的协议之一,虽然看上去很简单,可是实际中常常遇到问题,咱们就已经遇到好几回了。有长链接相关的,有报文解析相关的。对http协议不能只知其一;不知其二,必须透彻理解才行。因此就写了这个系列分享http协议的问题与经验。java
问题nginx
咱们的手机App在作更新时会从服务器上下载的一些资源,通常都是一些小文件,更新的代码差很少是下面这样的:web
static void update() throws IOException { URL url = new URL("http://172.16.59.129:8000/update/test.so"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); if(conn.getResponseCode() == 200) { int totalLength = conn.getContentLength(); BufferedInputStream in = new BufferedInputStream(conn.getInputStream()); byte[] buffer = new byte[512]; int readLength = 0; int length = 0; while((length=in.read(buffer)) != -1) { readLength += length; //进度条 System.out.println(((float)readLength) /((float)(totalLength))); } } }
好比上面的代码更新一个so文件,先经过content-length获取文件的总大小,而后读Stream,每读一段,就计算出当前读的总大小,除以content-length,用来显示进度条。浏览器
结果weblogic从10升级到12后,content-length一直返回-1,这样就不能显示进度条了,可是文件流还能正常读。把weblogic重启了,一开始还能返回content-length,一会又是-1了。bash
缘由分析服务器
Http协议的请求报文和回复报文都有header和body,body就是你要获取的资源,例如一个html页面,一个jpeg图片,而header是用来作某些约定的。例如客户端与服务端商定一些传输格式,客户端先获取头部,得知一些格式信息,而后才开始读取body。tcp
客户端: Accept-Encoding:gzip (给我压缩一下,我用的是流量,先下载下来我再慢慢解压吧)
ide
服务端1:Content-Encoding:null(没有Content-Encoding头。 我不给压缩,CPU没空,你爱要不要)测试
服务端2:Content-Encoding:gzip (给你节省流量,压缩一下)
客户端:Connection: keep-alive (大哥,咱好不容易建了个TCP链接,下次接着用)
服务端1: Connection: keep-alive (都不容易,接着用)
服务端2: Connection: close (谁跟你接着用,咱们这个TCP是一次性的,下次再找我还得从新连)
http协议没有三次握手,通常客户端向服务端请求资源时,以服务端为准。还有一些header并无协商的过程,而是服务端直接告诉客户端按什么来。例如上述的Content-Length,是服务端告诉客户端body的大小有多大。可是!服务端并不必定能准确的提早告诉你body有多大。服务端要先写header,再写body,若是要在header里把body大小写进去,就得提早知道body大小。若是这个body是动态生成的,服务端先生成完,再开始写header,这样须要不少额外的开销,因此header里不必定有content-length。
那客户端怎么知道body的大小呢?服务器有三种方式告诉你。
1. 服务器已经知道资源大小,经过content-length这个header告诉你。
Content-Length:1076(body的大小是1076B,你读取1076B就能够完成任务了)
Transfer-Encoding: null
2. 服务器无法提早知道资源的大小,或者不肯意花费资源提早计算资源大小,就会把http回复报文中加一个header叫Transfer-Encoding:chunked,就是分块传输的意思。每一块都使用固定的格式,前边是块的大小,后面是数据,而后最后一块大小是0。这样客户端解析的时候就须要注意去掉一些无用的字段。
Content-Length:null
Transfer-Encoding:chunked (接下来的body我要一块一块的传,每一块开始是这一块的大小,等我传到大小为0的块时,就没了)
3. 服务器不知道资源的大小,同时也不支持chunked的传输模式,那么就既没有content-length头,也没有transfer-encoding头,这种状况下必须使用短链接,以链接结束来标示数据传输结束,传输结束就能知道大小了。这时候服务器返回的header里Connection必定是close。
Content-Length:null
Transfer-Encoding:null
Connection:close(我不知道大小,我也用不了chunked,啥时候我关了tcp链接,就说明传输结束了)
实验
我经过nginx在虚拟机里作实验,默认nginx是支持chunked模式的,能够关掉。
使用的代码以下,可能会调整参数。
static void update() throws IOException { URL url = new URL("http://172.16.59.129:8000/update/test.so"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //conn.setRequestProperty("Accept-Encoding", "gzip"); //conn.setRequestProperty("Connection", "keep-alive"); conn.connect(); if(conn.getResponseCode() == 200) { System.out.println(conn.getHeaderFields().keySet()); System.out.println(conn.getHeaderField("transfer-encoding")); System.out.println(conn.getHeaderField("Content-Length")); System.out.println(conn.getHeaderField("Content-Encoding")); System.out.println(conn.getHeaderField("Connection")); } }
1. nginx在开启chunked_transfer_encoding的时候
(1) 在reqeust header里不使用gzip,也就是不加accept-encoding:gzip
test.so文件大小 |
结果 |
100B |
能正常返回content-length,没有transfer-encoding头 |
69M |
能正常返回content-length,没有transfer-encoding头 |
3072M |
能正常返回content-length,没有transfer-encoding头 |
能够发现nginx无论资源多大,若是客户端不接受gzip的压缩格式,就不会使用chunked模式,并且跟是否使用短链接不要紧。
(2)在request header里加入gzip,accepting-encoding:gzip
test.so文件大小 |
结果 |
100B |
没有content-length,transfer-encoding=trunked |
69M |
没有content-length,transfer-encoding=trunked |
3072M |
没有content-length,transfer-encoding=trunked |
能够看到nginx在开启chunked_transfer_encoding,而且客户端接受gzip的时候,会使用chunked模式,nginx开启gzip后不会计算资源的大小,直接用chunked模式。
2.nginx关闭chunked_transfer_encoding
(1) 在reqeust header里不使用gzip,也就是不加accept-encoding:gzip
test.so文件大小 |
结果 |
100B |
能正常返回content-length,没有transfer-encoding头 |
69M |
能正常返回content-length,没有transfer-encoding头 |
3072M |
能正常返回content-length,没有transfer-encoding头 |
由于能很容易的知道文件大小,因此nginx仍是能返回content-length。
(2)在request header里加入gzip,accepting-encoding:gzip
test.so文件大小 |
结果 |
100B |
没有content-length和transfer-encoding头,不论客户端connection为keep-alive仍是close,服务端返回的connection头都是close |
69M |
没有content-length和transfer-encoding头,不论客户端connection为keep-alive仍是close,服务端返回的connection头都是close |
3072M |
没有content-length和transfer-encoding头,不论客户端connection为keep-alive仍是close,服务端返回的connection头都是close |
这就是上面说的第三种状况,不知道大小,也不支持trunked,那就必须使用短链接来标示结束。
问题解决方案
咨询了中间件组的同事,之前也遇到相似的问题,由于升级了Weblogic致使客户端解析XML出错,由于使用了chunked模式,中间有一些格式化的字符,而客户端解析的代码并无考虑chunked模式的解析,致使解析出错。
由于咱们客户端必须用content-length展现进度,所以不能用chunked模式,Weblogic能够把chunked模式关闭。用下面的方法:
#!java weblogic.WLST connect('username’,'password', 't3://localhost:7001') edit() startEdit() cd("Servers/AdminServer/WebServer/AdminServer") cmo.setChunkedTransferDisabled(true) save() activate() exit()
改了以后,确实不返回chunked了,可是也没有content-length,由于Weblogic就是不提早获取文件大小,而是强制加了connection:close,也就是前边说的第三种,经过链接结束标识数据结束。因为生产上咱们用了Apache,测试环境为了方便就直接用的Weblogic,因此只能在测试环境再加个Apache了。
总结
一个好的http客户端,必须充分实现协议,否则就可能出问题,浏览器对于服务端可能产生的各类状况都很好的作了处理,可是本身实现http协议的解析时必定得注意考虑多种状况。