本文多图,流量打开需谨慎!
HTTP协议作为我们平时开发过程中使用最为广泛的网络协议,相信大家至少对它都有简单的了解,它也是很多互联网公司平时面试时经常问的点,比如各种HTTP方法、状态码字、持久连接等。今天我们就来聊聊HTTP协议在客户端和服务器连接过程中那些有意思的点。
大家都知道,HTTP协议是基于TCP协议的协议,所以我们如果要建立一个HTTP连接首先需要建立一根TCP连接。为了更加直观的展示这个过程,我这里使用Telnet工具来模拟一下HTTP请求和响应的过程:
- 连接到目标主机(www.maoyan.com)的HTTP端口80
http_connect.gif
我们看到这个时候TCP连接已经建立,服务器在等待我们往TCP管道中写入数据。 - 按照HTTP规范,写入请求
http_request.gif
我们可以看到,写入了一个GET请求,客户端HTTP版本是1.1,请求的url是/index.html,输入之后,后台直接返回了我们这个请求的结果,结果的状态码是301,表示这个页面已经重定向了,注意一点,服务器返回了重定向的页面之后,我们的TCP连接并没有结束。
一 尴尬的Host头部
刚学习HTTP协议的时候,我就对Host首部很疑惑,存在的意义是啥?
在上面的栗子中,可能有些同学已经发现了www.maoyan.com
这个主机地址写了两次,一次是是建立TCP的时候,一次是我们写入HTTP请求时,放到了Host头部。
在HTTP 1.0协议里面不写Host是没有任何问题的,大家可以自己去试一下,但是在HTTP 1.1里面如果不写Host首部,会直接报400错误。
我们在发起TCP连接的时候已经输入了www.maoyan.com
,为什么还需要用一个Host首部再写入一次呢?原来我们考虑一下HTTP被设置代理的情况,比如我们设置了路由代理,我们的HTTP客户端的流量都会被发送到代理服务器。客户端会直接和代理服务器建立TCP连接,如果在HTTP层面不提供目标主机的地址,代理服务器蒙圈了,这个请求我TM发给谁?所以在HTTP 1.1中规定所有的HTTP请求都需要指定Host首部。
二 强大的持久化连接
在栗子中我们看到服务器返回一个重定向的响应之后,并没有关闭TCP连接,如果我们接着写入HTTP请求,这条连接还是会返回数据:
这就是HTTP协议的持久化连接,对同一个主机的多个HTTP请求可以复用同一个TCP连接。相信大家都了解HTTP的持久化连接的作用,它可以减少TCP连接的重复创建,特别是对于那种通信量非常小的HTTP请求和响应,大部分的传输流量都浪费在建立和断开连接上面了。
相信简单了解过HTTP协议的同学对这个理解都没有问题,那我们聊聊另外一个持久连接的优势:TCP拥塞窗口冷却。
2.1 TCP拥塞窗口
我们在学习计算机网络时,肯定都学过在TCP层存在两个叼炸天的控制策略:流量控制和拥塞控制。所谓流量控制指的是发送方需要调整自己的发送窗口,以免接收方来不及处理数据而导致数据丢失和重传,典型的就是滑动窗口来做流量控制。而拥塞控制考虑的更加复杂,它主要用于依据当前网络环境的状况来动态调整我们发送数据的速度,从而避免过多数据注入到网络中,导致整个网络性能下降。所以在TCP层面进行数据传输的窗口大小是由发送窗口和拥塞窗口共同决定的。
我们这里暂时只讨论拥塞控制对于HTTP持久连接的影响(因为发送窗口和当时服务器的负载有关系,和协议层面关系不大)。
目前最常见TCP的拥塞控制算法是慢启动算法+拥塞避免算法,一图胜千言:
image.png
相信学过计算机网络(谢希仁版)的同学对于这个图真是百感交集,哈哈,老师喜欢考,没想到工作之后还有人贴出来..
所谓慢启动,就是拥塞窗口开始非常小,依据对收到ACK报文来预测网络状态的性能,如果网络较好,拥塞窗口才会慢慢增加。然而如果HTTP不启动持久连接,你发现问题了么?
是的,每次连接都需要重新走一边慢启动,来逐步增加窗口,这不是坑爹么。如果使用持久连接,那么拥塞窗口在网络较好的情况下,每次新的HTTP报文在TCP层面直接有了非常合适的拥塞窗口。
虽然持久连接有这么多好处,但是HTTP其实一开始是不支持的。一直到HTTP 1.0版本的时很多人都觉得不能忍,于是把它写入了扩展协议,实际上大部分HTTP 1.0 Web服务器都支持了持久连接。然而在1.0和1.1版本对于持久连接支持还略有区别,下面分开来说。
2.2 1.0 VS 1.1 持久连接
在HTTP1.0协议中是没有支持持久连接的说明,但是大家自觉的扩展了一个名为Connection
,如果客户端往里面写入了keep-alive
值,那么服务器如果能够理解这个语意,回返回Connection:keep-alive
来告诉客户端我们可以保持TCP连接。但是在1.0中默认是不支持持久连接的,我们看看效果:
可以看到服务器返回了数据之后,TCP连接就被关闭了。
然而在HTTP 1.1中,持久连接被默认支持,参考我的第一个栗子。如果你不想维持长连接,那么可以手动往头部中加入Connection:close
来关闭TCP连接。
2.3 郁闷的Proxy-Connection头部
由于1.0版本中依赖于Connection扩展头部来进行持久化,但是在网络中存在很多运行旧版本的HTTP应用,它们不一定认识Connection字段的含义,比如下图中的笨代理(图片来自网络,如有侵权,马上删除
):
客户端支持持久连接,然而中间的代理使用的旧版本的HTTP协议,所以它并不知道这个含义,然后直接将所有的头部都转发给服务器,服务器看到客户端要建立持久连接,当然欣然同意,客户端看到服务器返回了
Connection:keep-alive
,也不会去关闭持久连接。然而此时的代理服务器认为这次通信已经结束了,以后客户端发送给服务器的任何数据它都不会转发给服务器了。这种情况就比较尴尬了,只有等着客户端 或者 服务器有一方TCP超时,才能断开连接,然而大好资源已经被浪费了。为了解决这个问题,网景公司的大神们只得屁颠屁颠的出补丁,这就是我们今天说的
Proxy-Connection
,它能工作的前提是如果一个代理不知道Connection:keep-alive
,自然也就不理解Proxy-Connection
,所以它会直接把请求发送给服务器,服务器看到的Proxy-Connection
,所以不会创建长连接。如果代理知道
Connection:keep-alive
语意,那么它会把Proxy-Connection
转换成Connection:keep-alive
。COOL!
哑代理1.png
虽然上面的方案看起来是解决问题了,但是你可以想想如果客户端和服务器直接的通道是这样的:
Client--->聪明代理--->笨代理-->Server
gg了,问题依旧~~
三 传输边界
刚接触HTTP时,经常想两个问题:
- 建立连接之后,客户端开始往TCP连接中写入数据,那什么时候算输入的结束,Service可以开始响应?
- 持久化连接之后,服务器返回了数据,客户端怎么知道服务器数据已经写完了?
原来在HTTP协议已经对这种传输边界进行了定义,本质上就是通信双方需要知道传输的数据一共有多大,最传统的做法就是加一个Content-Length
头部用来描述传输中的主体大小,那么接收方只有在收到头部描述的这么多字节之后才会开始进行请求解析:
我们看到和第一个栗子不同的时候,这里我多输入了5个
CRLF
回车换行,服务器才认为数据已经写入完毕,开始解析客户端的请求。
但是Content-Length
其实有局限性,就是在传输数据之前我需要知道需要传输的东西大小,对于传输静态文件或者已知数据来说,这通常来说没什么问题。但是考虑一种场景,比如服务器需要对一个视频文件转码之后传送给客户端,这个文件有10G,转码之后的大小肯定之前不知道,如果需要把文件完全转换之后才给客户端传送延迟就会很长同时也会浪费服务器资源,最理想的情况是服务器一边转码一边给客户端进行传输。
chunked编码就应运而生,它将数据进行分块,每块都包含数据大小和数据本身,每个chunk的格式如下:
长度 (十六进制表示)CRLF
数据
比如我们有如下HTTP报文:
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
25 //注意,这里是十六进制,表示37个字节
This is the data in the first chunk
1A
and this is the second one
0 //0字节表示结束
Java构建这个响应的代码如下:
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 200 OK\r\n");
sb.append("Content-Type: text/plain\r\n");
sb.append("Transfer-Encoding: chunked\r\n\r\n");
sb.append("25\r\n");
sb.append("This is the data in the first chunk\r\n"); // 37 bytes of payload
// (conveniently consisting of ASCII characters only)
sb.append("\r\n1A\r\n");
sb.append("and this is the second one"); // 26 bytes of payload
// (conveniently consisting of ASCII characters only)
sb.append("\r\n0\r\n\r\n");
注意,关于传输边界的问题对于请求实体和响应实体都有效。
最后插播一条广告,猫眼电影正在招Android中高级工程师,有兴趣的同学可以戳拉勾JD,也可以简历发我邮箱
"pengliang".concat("02").concat"@".concat("maoyan.com")
内推,公司目前正处于快速上升期,有不少技术项目可以挑战。
网友评论