前言

http协议是互联网中最重要的协议之一,虽然看上去很简单,但是实际中经常遇到问题,我们就已经遇到好几次了。有长连接相关的,有报文解析相关的。对http协议不能一知半解,必须透彻理解才行。本文通过一个线上实例,详细介绍http协议中的content-length字段。

问题

我们的手机App在做更新时会从服务器上下载的一些资源,一般都是一些小文件,更新的代码差不多是下面这样的:

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了。

原因分析

Http协议的请求报文和回复报文都有header和body,body就是你要获取的资源,例如一个html页面,一个jpeg图片,而header是用来做某些约定的。例如客户端与服务端商定一些传输格式,客户端先获取头部,得知一些格式信息,然后才开始读取body。

客户端: Accept-Encoding:gzip (给我压缩一下,我用的是流量,先下载下来我再慢慢解压吧)

服务端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里了。

总结

一个好的http客户端,必须充分实现协议,不然就可能出问题,浏览器对于服务端可能产生的各种情况都很好的做了处理,但是自己实现http协议的解析时一定得注意考虑多种情况。

Http协议Content-Length详解的更多相关文章

  1. HTTP协议Keep-Alive模式详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp22 HTTP协议Keep-Alive模式详解 1.什么是Keep-Aliv ...

  2. HTTP协议状态码详解(HTTP Status Code)(转)

    原文链接:HTTP协议状态码详解(HTTP Status Code) 使用ASP.NET/PHP/JSP 或者javascript都会用到http的不同状态,一些常见的状态码为: 200 – 服务器成 ...

  3. http协议之报文详解

    一. 概述 用于HTTP协议交互的信息被称为HTTP报文.请求端(客户端)的http报文叫做请求报文,响应端的叫做响应报文. 报文,是网络中交换和传输的数据单元,即站点一次性要发送的数据块.报文包含了 ...

  4. HTTP协议头域详解

    HTTP协议头域详解 Requests部分 Accept 指定客户端能够接收的内容类型 Accept: text/plain, text/html Accept-Charset 浏览器可以接受的字符编 ...

  5. HTTP协议状态码详解

    HTTP状态码,我都是现查现用. 我以前记得几个常用的状态码,比如200,302,304,404, 503. 一般来说我也只需要了解这些常用的状态码就可以了.  如果是做AJAX,REST,网络爬虫, ...

  6. HTTP协议-状态码详解(转)

    原文:http://www.cnblogs.com/lebronjames/archive/2013/01/10/2854981.html HTTP状态码的学习资料到处都有,但是都是理论上讲解.  本 ...

  7. 浏览器 HTTP 协议缓存机制详解

    最近在准备优化日志请求时遇到了一些令人疑惑的问题,比如为什么响应头里出现了两个 cache control.为什么明明设置了 no cache 却还是发请求,为什么多次访问时有时请求里带了 etag, ...

  8. nginx平台初识(二) 浏览器 HTTP 协议缓存机制详解

    1.缓存的分类 缓存分为服务端侧(server side,比如 Nginx.Apache)和客户端侧(client side,比如 web browser). 服务端缓存又分为 代理服务器缓存 和 反 ...

  9. USB协议枚举过程详解

    一 枚举过程之文字描述 ?主机集线器监视着每个端口的信号电压,当有新设备接入时便可觉察.(集线器端口的两根信号线的每一根都有15kΩ的下拉电阻,而每一个设备在D+都有一个1.5kΩ的上拉电阻.当用US ...

  10. RTP协议分析和详解

    一.RTP协议分析 第1章.     RTP概述 1.1.  RTP是什么 RTP全名是Real-time Transport Protocol(实时传输协议).它是IETF提出的一个标准,对应的RF ...

随机推荐

  1. yzoj P2350 逃离洞穴 题解

    题意 跑两边spfa的水题,注意判断有人才取最大值 代码 #include<bits/stdc++.h> using namespace std; inline int read(){ i ...

  2. 多级树的深度遍历与广度遍历(Java实现)

    目录 多级树的深度遍历与广度遍历 节点模型 深度优先遍历 广度优先遍历 多级树的深度遍历与广度遍历 深度优先遍历与广度优先遍历其实是属于图算法的一种,多级树可以看做是一种特殊的图,所以多级数的深/广遍 ...

  3. ASP.NET Core结合Nacos来完成配置管理和服务发现

    目录 前言 Nacos的简介 启动Nacos 配置管理 服务发现 写在最后 前言 今年4月份的时候,和平台组的同事一起调研了一下Nacos,也就在那个时候写了.net core版本的非官方版的SDK. ...

  4. mysql之innodb-锁

    本篇主要根据innodb存储引擎的锁进行阐述,包括分类,算法,以及锁的一些问题 一.锁的概述 为了保证最大程度的利用数据库的并发访问,又要确保每个用户能以一致的方式读取和修改数据,为此锁就派上了用场, ...

  5. Android开发学习路线的七个阶段和步骤

    Android开发学习路线的七个阶段和步骤           Android学习参考路线     第一阶段:Java面向对象编程 1.Java基本数据类型与表达式,分支循环. 2.String和St ...

  6. helm在kubernetes环境中搭建

    1.安装helm 1.1.安装helm客户端 各个版本的helm:https://github.com/helm/helm/releases wget https://get.helm.sh/helm ...

  7. 用button 属性来保存字符串地址

    我用到for循环创建button  通过点击不同的按钮拿到每个button对应的链接地址,因为button的个数也是通过后台数据返回.上代码: //保存到数组 _array = [Article mj ...

  8. 09 (OC)* 键路径(keyPath)、键值编码(KVC)、键值观察(KVO)

    键路径在一个给定的实体中,同一个属性的所有值具有相同的数据类型.键-值编码技术用于进行这样的查找—它是一种间接访问对象属性的机制. - 键路径是一个由用点作分隔符的键组成的字符串,用于指定一个连接在一 ...

  9. 06.Django基础五之django模型层(二)多表操作

    一 创建模型 表和表之间的关系 一对一.多对一.多对多 ,用book表和publish表自己来想想关系,想想里面的操作,加外键约束和不加外键约束的区别,一对一的外键约束是在一对多的约束上加上唯一约束. ...

  10. [sonarqube的使用] sonarqube安装

    一 . SonarQube代码质量检查工具简介 Sonar (SonarQube)是一个开源平台,用于管理源代码的质量 Sonar 不只是一个质量数据报告工具,更是代码质量管理平台 支持Java, C ...