什么是HTTP?
超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP
协议传输数据,互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷是为了提供一种发布和接收HTML页面的方法。
这个Akamai公司建立的一个官方的演示,使用HTTP/1.1和HTTP/2同时请求379张图片,观察请求的时间,明显看出HTTP/2性能占优势。
image.png
多路复用:
通过单一的HTTP/2连接请求发起多重的请求-响应消息,多个请求stream共享一个TCP连接,实现多留并行而不是依赖建立多个TCP连接。
HTTP通信传输
HTTP通信传输客户端输入
URL
回车,DNS
解析域名得到服务器的IP地址,服务器在80端口监听客户端请求,端口通过TCP/IP
协议(可以通过Socket实现
)建立连接。HTTP
属于TCP/IP
模型中的运用层协议,所以通信的过程其实是对应数据的入栈和出栈。image.png
报文从运用层传送到运输层,运输层通过TCP三次握手和服务器建立连接,四次挥手释放连接。
TCP三次握手
TCP三次握手为什么需要三次握手呢?
为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
四次挥手比如:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段,但是server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求,于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了,由于client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据,但server却以为新的运输连接已经建立,并一直等待client发来数据。所以没有采用
三次握手
,这种情况下server的很多资源就白白浪费掉了。
为什么需要四次挥手呢?
TCP是全双工模式
- 1.当client发出FIN报文段时,只是表示client已经没有数据要发送了,client告诉server,它的数据已经全部发送完毕了;但是,这个时候client还是可以接受来server的数据;
- 2.当server返回ACK报文段时,表示它已经知道client没有数据发送了,但是server还是可以发送数据到client的;
- 3.当server也发送了FIN报文段时,这个时候就表示server也没有数据要发送了,就会告诉client,我也没有数据要发送了;
- 4.如果Sever收到client确认报文段,之后彼此就会愉快的中断这次TCP连接。
HTTP协议分析
上面我们把HTTP协议相关的数据给解构了,下面我将对照上面的数据拆解结果,一步步带你深入理解HTTP协议。
整体介绍
HTTP(Hypertext Transfer Protocol
)超文本传输协议,是在互联网上进行通信时使用的一种协议。说得更形象一点: HTTP
是现代互联网中使用的公共语言。它最著名的应用是用在浏览器的服务器间的通信。
HTTP
属于应用层协议,底层是靠TCP
进行可靠地信息传输。
HTTP
在传输一段报文时,会以 流 的形式将报文数据的内容通过 一条打开 的TCP
连接按序传输。TCP
接到上层应用交给它的数据流之后,会按序将数据流打散成一个个的分段。再交到IP层,通过网络进行传输。另一端的接收方则相反,它们将接收到的分段按序组装好,交给上层HTTP
协议进行处理。
编码
原始的url值:
/ data ? cmd = Fence2Area & meta ={"caller": "test", "TraceId": "test"}& request ={"fence":[{"lng": 10.2 , "lat": 10.2 }, {"lng": 10.2 , "lat": 8.2 }, {"lng": 8.2 , "lat": 8.2 }, {"lng": 8.2 , "lat": 10.2 }], "coordtype": 2 }
编码后的url值:
/ data ? cmd = Fence2Area & meta ={% 22caller % 22 :% 22test % 22 ,% 22TraceId % 22 :% 22test % 22 }& request ={% 22fence % 22 :[{% 22lng % 22 : 10.2 ,% 22lat % 22 : 10.2 },% 20 {% 22lng % 22 : 10.2 ,% 22lat % 22 : 8.2 },% 20 {% 22lng % 22 : 8.2 ,% 22lat % 22 : 8.2 },% 20 {% 22lng % 22 : 8.2 ,% 22lat % 22 : 10.2 }],% 22coordtype % 22 : 2 }
在之前的报文拆解过程中,我们看到多了很多 %22
,其实, 0x22
是双引号 "的ascii
值,
一方面,URL
描述的资源为了能通过其他各种协议传送,但是有些协议在传输过程中会剥去一些特定的字符;另一方面,URL
还是可读的,所以那些不可打印的字符就不能在URL
中使用了,比如空格;最后,URL
还得是完整的,它需要支持所有语言的字符。
总之,基于很多原因,URL
设计者将US-ASCII
码和其转义序列集成到URL
中,通过转义序列,就可以用US-ASCII
字符集的有限子集对任意字符或数据进行编码了。
转义的方法:百分号(%
)后跟着两个表示ASCII
码的十六进制数。比如:
所以上面在浏览器发送给服务器的URL进行了非安全字符编码,也就不奇怪了吧?
image.png
在
URL
中,当上面的保留字符用在保留用途之外的场合时,需要对URL
进行编码。
MIME类型
响应数据中,我们注意到有一个首部:
Content-Type:text/plain;charset=utf-8
互联网上有数千种不同的数据类型,HTTP
给每种对象都打上了MIME(Multipurpose Internet Media Extension, 多用途因特网邮件扩展)
标签,也就是响应数据中的 Content-Type. MIME
本来是用在邮件协议中的,后来被移植到了HTTP
中。浏览器从服务器上取回了一个对象时,会去查看MIME
类型,从而得知如何处理这种对象,是该展示图片,还是调用声卡播放声音。MIME
通过斜杠来标识对象的主类型和其中的特定的子类型,下表展示了一些常见的类型,其中的实体主体是指body
部分:
URL/URI/URN
URI(Uniform Resource Identifier, 统一资源标识符)
表示服务器资源,URL(Uniform Resource Locator, 统一资源定位符)
和URN(Uniform Resource Name, 统一资源名)
是URI
的具体实现。URI
是一个通用的概念,由两个主要的子集URL和URN
构成,URL
通过位置、URN
通过名字来标识资源。
URL
定义了资源的位置,表示资源的实际地址,在使用URL
的过程中,如果URL
背后的资源发生了位置移动,访问者就找不到它了。这个时候就要用到URN
了,它给定资源一个名字,无论它移动到哪里,都可以通过这个名字来访问到它,简直完美!
URL
通常的格式是:
协议方案+服务器地址+具体的资源路径
协议方案(scheme
),如 http, ftp
,告知web
客户端怎样访问资源);服务器地址,如 www.oreilly.com
; 具体的资源路径,如 index.htm
.
HTTP方法
HTTP
支持几种不同的请求方法,每种方法对服务器要求的动作不同,如下图是几种常见的方法:
报文头HEAD
方法只获取头部,不获取数据部分。通过头部可以获取比如资源的类型(Content-Type)
、资源的长度(Content-Length
)这些信息。这样,客户端可以获取即将请求资源的一些情况,可以做到心中有数。
POST
用于向服务器发送数据,常见的是提交表单;PUT
用于向服务器上的资源存储数据。
状态码
每条HTTP的响应报文都会带上一个三位数字的状态码和一条解释性的原因短语,通知客户端本次请求的状态,帮助客户端快速理解事务处理结果,最常见的是:
200 OK
404 Not Found
500 Internal Server Error
我们平时使用浏览器的时候,很多的错误码其实是由浏览器处理的,我们感知不到。但是 404 Not Found会穿透重重迷雾,来到我们面前,为何?那是因为他对我们爱的深沉啊!
客户端可以据此状态码,决定下一步的行动(如重定向等)。
三位数字的第一位表示分类:
状态码
报文格式
HTTP
报文实际上是由一行行的字符串组成的,每行字符串的末尾用\r\n
分隔,人类可以很方便的阅读。顺便说一句,不是所有的协议都对人类这么友好的,像thrift
协议,直接甩一堆字节给你,告诉你说 0x0001
表示调用方法,诸如此类的,你只能对着一个十六进制的数据块一个个地去“解码”。不可能像HTTP
协议这样,直接将字符编码,人类可以直接读懂。
举个简单的请求报文和响应报文的格式的例子:
实际上,请求报文也是可以有
body
(主体)部分的。请求报文是由 **请求行(request line
)、请求头部(header
)、空行、请求数据四个部分组成。唯一要注意的一点就是,请求报文即使body
部分是空的,请求头部后的 回车换行符也是必须要有的。请求报文
响应报文的格式和请求报文的格式类似:
响应报文
请求报文、响应报文的起始行和响应头部里的字段都是文本化、结构化的。而请求body却可以包含任意二进制数据(如图片、视频、软件等),当然也可以包含文本。
有些首部是通用的,有些则是请求或者响应报文才会有的。
响应头
HTTP协议进阶
代理
HTTP的代理服务器既是Web服务器,又是Web客户端。
使用代理可以“接触”到所有流过的HTTP流量,代理可以对其进行监视和修改。常见的就是对儿童过滤一些“成人”内容;网络工程师会利用代理服务器来提高安全性,它可以限制哪些应用层的协议数据可以通过,过滤“病毒”等数据;代理可以存储缓存的文件,直接返回给访问者,无需请求原始的服务器资源;对于访问慢速网络上的公共内容时,可以假扮服务器提供服务,从而提高访问速度;这被称为 反向代理;可以作为内容路由器,如对付费用户,则将请求导到缓存服务器,提高访问速度;可以将页面的语言转换到与客户端相匹配,这称为 内容转码器; 匿名代理会主动从
HTTP
报文中删除身份相关的信息,如 User-Agent, Cookie
等字段。
现实中,请求通过以下几种方式打到代理服务器上去:
报文每经过一个中间点(代理或网关),都需要在首部
via
字段的末尾插入一个可以代表本节点的独特的字符串,包含实现的协议版本和主机地址。注意图中的via
字段。VIA
请求和响应的报文传输路径通常都是一致的,只不过方向是相反的。因此,响应报文上的via
字段表示的中间节点的顺序是刚好相反的。
缓存
Http 缓存机制作为 web 性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系库中的一个基础环节,同时对于有志成为前端架构师的同学来说是必备的知识技能。
但是对于很多前端同学来说,仅仅只是知道浏览器会对请求的静态文件进行缓存,但是为什么被缓存,缓存是怎样生效的,却并不是很清楚。
在此,我会尝试用简单明了的文字,像大家系统的介绍HTTP缓存机制,期望对各位正确的理解前端缓存有所帮助。
缓存规则解析
为方便大家理解,我们认为浏览器存在一个缓存数据库,用于存储缓存信息。
在客户端第一次请求数据时,此时缓存数据库中没有对应的缓存数据,需要请求服务器,服务器返回后,将数据存储至缓存数据库中。
HTTP缓存有多种规则,根据是否需要重新向服务器发起请求来分类,我将其分为两大类(强制缓存,对比缓存)
在详细介绍这两种规则之前,先通过时序图的方式,让大家对这两种规则有个简单了解。
已存在缓存数据时,仅基于
强制缓存
,请求数据的流程如下已存在强制缓存数据
已存在缓存数据时,仅基于
对比缓存
,请求数据的流程如下已存在对比缓存数据
对缓存机制不太了解的同学可能会问,基于对比缓存
的流程下,不管是否使用缓存,都需要向服务器发送请求,那么还用缓存干什么?
这个问题,我们暂且放下,后文在详细介绍每种缓存规则的时候,会带给大家答案。
我们可以看到两类缓存规则的不同,强制缓存
如果生效,不需要再和服务器发生交互,而对比缓存
不管是否生效,都需要与服务端发生交互。
两类缓存规则可以同时存在,强制缓存
优先级高于对比缓存
,也就是说,当执行强制缓存
的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存
规则。
强制缓存
从上文我们得知,强制缓存,在缓存数据未失效的情况下,可以直接使用缓存数据,那么浏览器是如何判断缓存数据是否失效呢?
我们知道,在没有缓存数据的时候,浏览器向服务器请求数据时,服务器会将数据和缓存规则一并返回,缓存规则信息包含在响应header
中。
对于强制缓存来说,响应header
中会有两个字段来标明失效规则(Expires/Cache-Control
)
使用chrome的开发者工具,可以很明显的看到对于强制缓存生效时,网络请求的情况
Expires
Expires
的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。不过
Expires
是HTTP 1.0
的东西,现在默认浏览器均默认使用HTTP 1.1
,所以它的作用基本忽略。另一个问题是,到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。
所以
HTTP 1.1
的版本,使用Cache-Control
替代。
Cache-Control
Cache-Control
是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store
,默认为private。
private
: 客户端可以缓存
public
: 客户端和代理服务器都可缓存(前端的同学,可以认为public和private
是一样的)
max-age=xxx
: 缓存的内容将在 xxx 秒后失效
no-cache
: 需要使用对比缓存来验证缓存数据(后面介绍)
no-store
: 所有内容都不会缓存,强制缓存,对比缓存都不会触发
图中
Cache-Control
仅指定了max-age
,所以默认为private
,缓存时间为31536000
秒(365天)也就是说,在
365
天内再次请求这条数据,都会直接获取缓存数据库中的数据,直接使用。
对比缓存
对比缓存
,顾名思义,需要进行比较判断是否可以使用缓存。
浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。
再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304
状态码,通知客户端比较成功,可以使用缓存数据。
第一次访问:
再次访问:
通过两图的对比,我们可以很清楚的发现,在
对比缓存
生效时,状态码为304
,并且报文大小和请求时间大大减少。原因是,服务端在进行标识比较后,只返回header部分,通过状态码通知客户端使用缓存,不再需要将报文主体部分返回给客户端。
对于对比缓存
来说,缓存标识的传递是我们着重需要理解的,它在请求header和响应header
间进行传递,
一共分为两种标识传递,接下来,我们分开介绍。
Last-Modified / If-Modified-Since
Last-Modified:
服务器在响应请求时,告诉浏览器资源的最后修改时间。
image.png
If-Modified-Since:
再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间。
image.png
服务器收到请求后发现有头If-Modified-Since
则与被请求资源的最后修改时间进行比对。
若资源的最后修改时间大于If-Modified-Since
,说明资源又被改动过,则响应整片资源内容,返回状态码200
;
若资源的最后修改时间小于或等于If-Modified-Since
,说明资源无新修改,则响应HTTP 304
,告知浏览器继续使用所保存的cache
。
Etag / If-None-Match(优先级高于Last-Modified / If-Modified-Since)
Etag:
服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。
image.png
If-None-Match:
再次请求服务器时,通过此字段通知服务器客户段缓存数据的唯一标识。
服务器收到请求后发现有头If-None-Match 则与被请求资源的唯一标识进行比对,
不同,说明资源又被改动过,则响应整片资源内容,返回状态码200;
相同,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache。
image.png
对于强制缓存
,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。
对于比较缓存
,将缓存信息中的Etag和Last-Modified通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。
cookie
cookie
是服务器“贴在”客户端身上的标签,由客户端维护的状态片段,并且只会回送给合适的站点。
有两类cookie
: 会话cookie、持久cookie
. 会话cookie
在退出浏览器后就被删除了;而持久cookie
则保存在硬盘中,计算机重启后仍然存在。
服务器在给客户端的响应字段首部加上 Set-cookie或 Set-cookie2
, 值为 名字=值
的列表,即可以包含多个字段。当下次浏览器再次访问到相同的网站时,会将这些字段通过 Cookie
带上。cookie
中保留的内容是服务器给此客户端打的标签,方便服务进行追踪的识别码。浏览器会将cookie
以特定的格式存储在特定的文件中。
浏览器只会向产生这条cookie
的站点发生cookie
. Set-cookie
字段的值会包含domain
这个字段,告知浏览器可以把这条cookie
发送给给相关的匹配的站点。path
字段也是相似的功能。如i浏览器收到如下的cookie
:
Set - cookie : user = "mary"; domain = "stefno.com"
那么浏览器在访问任意以 stefno.com
结尾的站点都会发送:
Cookie : user = "mary"
实体和编码
响应报文中的body
部分传输的数据本质上都是二进制。我们从上面的报文数据也可以看出来,都是用十六进制数来表示,关键是怎么解释这块内容。如果 Content-Type
定义是 text/plain
, 那说明body内容就是文本,我们直接按文本编码来解释;如果 Content-Type
定义是 image/png
, 说明body部分是一幅图片,那我们就按图片的格式去解释数据。
Content-Length
标示报文主体部分的数据长度大小,如果内容是压缩的,那它表示的就是压缩后的大小。另外, Content-Length
在长连接的情况下,可以对多个报文进行正确地分段。所以,如果没有采用分块编码,响应数据中必须带上Content-Length
字段。分块编码的情形中,数据被拆分成很多小块,每块都有大小说明。因此,任何带有主体部分的报文(请求或是响应)都应带上正确的 Content-Length
首部。
HTTP的早期版本采用关闭连接的方式来划定报文的结束
。这带来的问题是显而易见的:客户端并不能分清是因为服务器正常结束还是中途崩溃了。这里,如果是客户端用关闭来表示请求报文主体部分的结束,是不可取的,因为关闭之后,就无法获取服务器的响应了。当然,客户端可以采用半关闭的方式,只关闭数据发送方向,但是很多服务器是不识别的,会把半关闭当成客户端要成服务器断开来处理。
HTTP
报文在传输的过程中可能会遭到代理或是其他通信实体的无意修改,为了让接收方知道这种情况,服务器会对body
部分作一个md5
, 并把值放到 Content-MD5
这个字段中。但是,如果中间的代理即修改了报文主体,又修改了md5
, 就不好检测了。因此规定代理是不能修改 Content-MD5
首部的。这样,客户端在收到数据后,先进行解码,再算出md5
, 并与 Content-MD5
首部进行比较。这主要是防止代理对报文进行了无意的改动。
HTTP
在发送内容之前需要对其进行编码,它是对报文主体进行的可逆变换。比如将报文用gzip
格式进行压缩,减少传输时间。常见的编码类型如下:
当然,客户端为了避免服务器返回自己不能解码的数据,请求的时候,会在
Accept-Encoding
首部里带上自己支持的编码方式。如果不传输的话,默认可以接受任何编码方式。
上面提到的编码是内容编码,它只是在响应报文的主体报文将原始数据进行编码,改变的是内容的格式。还有另一种编码: 传输编码。它与内容无关,它是为了改变报文数据在网络上传输的方式。传输编码是在HTTP 1.1
中引入的一个新特性。
通常,服务器需要先生成数据,再进行传输,这时,可以计算数据的长度,并将其编码到 Content-Length
中。但是,有时,内容是动态生成的,服务器希望在数据生成之前就开始传输,这时,是没有办法知道数据大小的。这种情况下,就要用到 传输编码来标注数据的结束的。
HTTP
协议中通过如下两个首部来描述和控制传输编码:
分块编码的报文形式是这样的:
image.png
每个分块包含一个长度值(十六进制,字节数)和该分块的数据。
<CR><LF>
用于区隔长度值和数据。长度值不包含分块中的任何 <CR><LF>
序列。最后一个分块,用长度值0来表示结束。注意报文首部包含一个 Trailer:Content-MD5
, 所以在紧跟着最后一个报文结束之后,就是一个拖挂。其他如, Content-Length, Trailer, Transfer-Encoding
也可以作为拖挂。
内容编码和传输编码是可以结合起来使用的。
image.png
国际化支持
HTTP为了支持国际化的内容,客户端要告知服务器自己能理解的何种语言,以及浏览器上安装了何种字母表编码算法。这通过 Accept-Charset和 Accept-Language首部实现。
Accept - Language : fr , en ; q = 0.8
Accept - Charset : iso - 8859 - 1 , utf - 8
表示:客户端接受法语(fr, 优先级默认为1.0
)、英语(en, 优先级为0.8
),支持iso-8859-1, utf-8
两种字符集编码。服务器则会在 Content-Type
首部里放上 charset
.
本质上,HTTP
报文的body
部分存放的就是一串二进制码,我们先把二进制码转换成字符代码(如ascii是一个字节表示一个字符,而utf-8则表示一个字符的字节数不定,每个字符1~6个字节),之后,用字符代码去字符集中找到对应的元素。
比较常见的字符集是 US-ASCII
: 这个字符集是所有字符集的始祖,早在1968年就发布了标准。ASCII
码的代码值从0到127, 只需要7个bit位就可以覆盖代码空间。HTT
P报文的首部、URL使用的字符集就是ASCII
码。可以再看下上文报文分析部分的acsii
码集。
US-ASCII
是把每个字符编码成固定的7位二进制值。 UTF-8
则是无固定的编码方案。第一个字节的高位用来表示编码后的字符所用的字节数(如果所用的字节数是5,则第一个字节前5bit都是1,第6bit是0),所需的后续的字节都含有6位的代码值,前两个bit位是用 10
标识。
举个例子,汉字“严”的
Unicode
编码为 4E25( 100111000100101)
, 共有15位,落在上表中的第三行,因此“严”的编码就需要三个字节。将 100111000100101
填入上表中的 c
位即可。因此,严的 UTF-8编码是11100100 10111000 10100101
,转换成十六进制就是E4B8A5. 比如我在谷歌搜索框里搜索“严”字,google发出的请求如下
https :// www . google . com . hk / search ? q =% E4 % B8 % A5 & oq =% E4 % B8 % A5 & aqs = chrome .. 69i57j0l5.3802j0j4 & sourceid = chrome & ie = UTF - 8 & gws_rd = cr
q=%E4%B8%A5
这个就是搜索的词了。
重定向与负载均衡
Web内容通常分散地分布在很多地方,这可以防止“单点故障”,万一某个地方发生地震了,机房被毁了,那还有其他地方的机房可以提供服务。一般都会有所谓的“双活”,“多活”,所谓 狡兔三窟
嘛。
这样,用户的请求会根据 负载均衡的原则,被 重定向到它应该去的地方。
HTTP重定向
服务器收到客户端请求后,向客户端返回一条带有状态码 302
重定向的报文,告诉他们应该去其他的地方试试。web站点将重定向看成一种简单的负载均衡策略来使用, 重定向
服务器找到可用的负载最小的机器,由于服务器知道客户端的地址,理论上来说,可以做到最优的重定向选择。
当然,缺点也是显而易见的,由于客户端要发送两次请求,因此会增加耗时。
DNS重定向
DNS
将几个IP地址关联到一个域上,采用算法决定返回的IP地址。可以是简单的 轮转
;也可以是更高级的算法,如返回负载最轻的服务器的IP地址,称为 负载均衡算法
;如果考虑地理位置,返回给客户端最近位置的地址,称为 邻接路由算法
;还有一种是绕过出现故障的地址,称为 故障屏蔽算法
。
DNS
服务器总是会返回所有的IP地址,但是DNS
客户端一般只会使用第一个IP地址,而且会缓存下来,之后会一直用这个地址。所以,DNS
轮转通常不会平衡单个客户端的负载。但是,由于DNS服务器对于不同的请求,总是会返回轮转后的IP地址列表,因此,会把负载分散到多个客户端。
HTTP链接
HTTP
连接是HTTP
报文传输的关键通道。
并行链接
对于一个页面上同时出现多个对象的时候,如果浏览器并行地打开多个连接,同时去获取这些对象,多个连接的TCP握手时延可以进行重叠,速度会快起来。
如一个包含3张图片的页面,浏览器要发送4次HTTP请求来获取页面。1个用于顶层的HTML页面,3个用于图片。如果采用串行方式,那么连接时延会进行叠加。
image.png
采用并行连接之后:
采用并行连接
但是并行连接也不绝对提升速度,如果一个页面有数百个内嵌对象,那要启动数百个连接,对服务器的性能也是非常大的挑战。所以,通常浏览器会限制并行连接的总数据在一个较小的值,通常是4个,而且服务端可以随意关闭客户端超量的连接。
另一方面,如果客户端网络带宽较小,每个连接都会去争抢有限的带宽,每个连接都会获取较小的速度,即每个对象都会以较小的速度去加载。这样,并行连接带来的速度提升就会比较小,甚至没有提升。
持久连接
HTTP keep-alive机制
我们知道HTTP请求是“请求-应答”
模式,每次请求-应答都要新建一个连接,完成之后要断开连接。HTTP是无状态的,连接之间没有任何关系。
HTTP
是应用层协议,TCP
是传输层协议。HTTP
底层仍然采用TCP
进行传输数据。TCP
为HTTP
提供了一层可靠的比特传输通道。HTTP
一般交换的数据都不大,而每次连接都要进行TCP三次握手
,很大一部分时间都消耗在这上面,有时候甚至能达到50%。如果能复用连接,就可以减少由于TCP三次握手所带来的时延。
HTTP 1.1
默认开启keep-alive
机制,从上面抓到的包也可以看到。这样,数据传输完成之后保持TCP连接不断开,之后同域名下复用连接,继续用这个通道传输数据。服务器在响应一个请求后,可以保持这个连接keep-alive timeout
的时间,在这个时间内没有请求,则关闭此连接;否则,重新开始倒计时keep-alive timeout
时间。
HTTP
有keep-alive
机制,目的是可以在一个TCP
连接上传输多个
HTTP
事务,以此提高通信效率。底层的TCP
其实也有keep-alive
机制,它是为了探测TCP
连接的活跃性。TCP
层的keepalive
可以在任何一方设置,可以是一端设置、两端同时设置或者两端都没有设置。新建socket
的时候需要设置,从而使得协议栈调用相关函数tcpsetkeepalive
,来激活连接的keep-alive
属性。
当网络两端建立了TCP连接之后,闲置(双方没有任何数据流发送往来
)时间超过 tcp_keepalive_time
后,服务器内核就会尝试向客户端发送侦测包,来判断TCP连接状况(有可能客户端崩溃、强制关闭了应用、主机不可达等等
)。如果没有收到对方的回答(ack包
),则会在 tcp_keepalive_intvl
后再次尝试发送侦测包,直到收到对方的ack
,如果一直没有收到对方的ack
,一共会尝试 tcpkeepaliveprobes
次,每次的间隔时间在这里分别是15s, 30s, 45s, 60s, 75s
。如果尝试 tcp_keepalive_probes
次后,依然没有收到对方的ack
包,则会丢弃该TCP
连接。TCP
连接默认闲置时间是2小时,一般设置为30分钟足够了。
管道化链接
在keep-alive的
基础上,我们可以做得更进一步,在响应到达之前,我们将多条请求按序放入请求队列,服务端在收到请求后,必须按照顺序对应请求的响应。但由于网络环境非常复杂,因此即使请求是按顺序发送的,也不一定是按顺序到达服务端的。而且就算是服务端按序处理的,也不一定是按序返回给客户端,所以最好是在响应中附带一些可以标识请求的参数。
为了安全起见,管道化的连接只适合“幂等”的请求,一般我们认为:GET/HEAD/PUT/DELETE/TRACE/OPTIONS
等方法都是幂等的。
网友评论