Http
1.Http报文
HTTP 报文是在 HTTP 应用程序之间发送的数据块( 用于 HTTP 协议交互的信息)。请求端(客户端)的 HTTP 报文叫做请求报文,响应端(服务器端)的叫做响应报文。
1.1 debug模式下查看报文
- General
- Remote Address:访问目标URL解析出来的IP地址,443:表示当前https协议。
- Referrer Policy : Referrer用户指明当前请求的来源页面,对于同源的请求,会发送完整的url作为引用地址,防盗链。
- Request Headers
- accept:请求可以支持的响应格式列表信息 text/html, application/xml等
- accept-encoding:告知服务器本地浏览器支持是压缩方式 gzip,deflate,br等
- sec-fetch-dest:期望获得什么类型的资源 document
- sec-fetch-mode :navigate,表示这是一个浏览器的页面切换请求
- sec-fetch-site:表示一个请求发起的来源和目标资源来源之间的关系,crosssite:跨域请求,same-origin:同源请求。
- sec-fetch-user:?1表示的true
- upgrade-insecure-requests :1,表示当前浏览器告诉服务器,浏览器是可以处理https请求的,即使访问的https请求中又包含了其他的http请求。
- user-agent:描述浏览器的信息
- Response Headers
- server :web应用程序部署的容器,openresty:封装了Nginx以及第三方的类库,Lua语言,redis等等。
- vary :accept-Encoding,告诉代理服务器缓存两种版本的资源(压缩、不压缩)
- content-Type: text/html等
1.2 报文流动
HTTP 使用术语流入(inbound) 和流出(outbound) 来描述事务处理(transaction)的方向。 报文流入源端服务器, 工作完成之后, 会流回用户的 Agent 代理中
1.3 报文的组成部分
HTTP 报文是简单的格式化数据块。 每条报文都包含一条来自客户端的请求,或者一条来自服务器的响应。 它们由三个部分组成:
- 对报文进行描述的起始行(start line)
- 包含属性的首部(header)块
- 可选的包含数据的主体(body) 部分
请求报文:
响应报文:
HTTP消息由采用ASCII编码的多行文本构成。在HTTP/1.1及早期版本中,这些消息通过连接公开地发送。在HTTP/2中,为了优化和性能方面的改进,曾经可人工阅读的消息被分到多个HTTP帧中。
Web 开发人员或网站管理员,很少自己手工创建这些原始的HTTP消息︰ 由软件、浏览器、 代理或服务器完成
1.4 http请求
起始行
起始行包含三个元素
**HTTP 方法:**一个动词 ([ GET , PUT 或者 POST ) 或者一个名词 (像 HEAD或者 OPTIONS ), 描述要执行的动作. 例如, GET 表示要获取资源, POST 表示向服务器推送数据 (创建或修改资源)。
请求目标 (request target): 通常是一个URL,或者是协议、端口和域名的绝对路径,通常以请求的环境为特征。请求的格式因不同的 HTTP 方法而异。它可以是:
一个完整的URL,被称为 绝对形式 (absolute form),主要在使用 GET方法连接到代理时使用。
GET http://developer.mozilla.org/en-US/docs/Web/HTTP/Messages HTTP/1.1
由域名和可选端口(以’:’ 为前缀)组成的 URL 的 authority component,称为 authority form。 仅在使用 CONNECT 建立 HTTP 隧道时才使用。
CONNECT developer.mozilla.org:80 HTTP/1.1
星号形式 (asterisk form),一个简单的星号( ‘*’ ),配合 OPTIONS 方法使用,代表整个服务器。
OPTIONS * HTTP/1.1
**HTTP 版本 (HTTP version):**定义了剩余报文的结构,作为对期望的响应版本的指示符
Headers
来自请求的 HTTP headers遵循和 HTTP header 相同的基本结构:不区分大小写的字符串,紧跟着的冒号 (’:’) 和一个结构取决于 header 的值。 整个header(包括值)由一行组成,这一行可以相当长。
Body
请求的最后一部分是它的 body。不是所有的请求都有一个 body:例如获取资源的请求,GET,HEAD,DELETE 和 OPTIONS,通常它们不需要 body。 有些请求将数据发送到服务器以便更新数据:常见的的情况是 POST 请求(包含HTML 表单数据)。
1.5 http响应
状态行
HTTP 响应的起始行被称作 状态行 (status line),包含以下信息:
协议版本: 通常为 HTTP/1.1。
**状态码 (status code):**表明请求是成功或失败。常见的状态码是 200 ,404 ,或 302 。
状态文本 (status text):一个简短的,纯粹的信息,通过状态码的文本描述,帮助人们理解该 HTTP 消息。
HTTP/1.1 404 Not Found
Headers
响应的 HTTP headers:不区分大小写的字符串,紧跟着的冒号 ( ‘:’ ) 和一个结构取决于 header 类型的值。 整个 header(包括其值)表现为单行形式。
Body
响应的最后一部分是 body。不是所有的响应都有 body
1.6 http请求方法
get
通常用于请求服务器发送某个资源。 HTTP/1.1 要求服务器实现此方法
head
HEAD 方法与 GET 方法的行为很类似, 但服务器在响应中只返回首部。 不会返回实体的主体部分。 这就允许客户端在未获取实际资源的情况下, 对资源的首部进行检查
在不获取资源的情况下了解资源的情况(比如, 判断其类型)
通过查看响应中的状态码, 看看某个对象是否存在
通过查看首部, 测试资源是否被修改了
put
PUT 方法会向服务器写入(更新)文档
post
POST 方法起初是用来向服务器输入数据的 。 实际上, 通常会用它来支持HTML的表单。 表单中填好的数据通常会被送给服务器处理。
trace
TRACE客户端发起一个请求时, 这个请求可能要穿过防火墙、 代理、 网关或其他一些应用程序。 每个中间节点都可能会修改原始的 HTTP 请求。 TRACE 方法允许客户端在最终将请求发送给服务器时, 看看它变成了什么样子。TRACE 请求会在目的服务器端发起一个“环回” 诊断。 行程最后一站的服务器会弹回一条TRACE 响应, 并在响应主体中携带它收到的原始请求报文。 这样客户端就可以查看在所有中间 HTTP 应用程序组成的请求 / 响应链上, 原始报文是否, 以及如何被毁坏或修改过
TRACE 方法主要用于诊断; 也就是说, 用于验证请求是否如愿穿过了请求 / 响应链。
options
OPTIONS 方法请求 Web 服务器告知其支持的各种功能。 可以询问服务器通常支持哪些方法, 或者对某些特殊资源支持哪些方法。
delete
DELETE 方法所做的事情就是请服务器删除请求 URL 所指定的资源
1.7 http状态码
状态码用来告诉客户端,事情执行的结果。状态码位于响应的起始行中。 服务器通常会返回一个数字状态和一个可读的状态。 数字码便于程序进行差错处理, 而原因短语则更便于人们理解。
- 200 到 299 之间的状态码表示成功。
- 300 到 399 之间的代码表示资源已经被移走(重定向)
- 400 到 499 之间的代码表示客户端的请求出错
- 500 到 599 之间的代码表示服务器出错
成功状态码
重定向状态码
如:web应用支持https,客户端访问http://edu.lagou.com,服务器收到请求之后(Nginx)发现请求的是http请求,可以返回301告知浏览器重新发出请求。
301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)——这是它们的共同点
他们的不同在于。301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。
客户端错误代码
有时客户端会发送一些服务器无法处理的东西, 比如格式错误的请求报文, 或者最常见的是, 请求一个不存在的 URL。浏览网页时, 我们都看到过臭名昭著的 404 Not Found 错误码——这只是服务器在告诉我们, 它对我们请求的资源一无所知。
服务器错误代码
2.连接管理
2.1 tcp连接
HTTP 通信由 TCP/IP 承载的, TCP/IP 是全球计算机及网络设备都在使用的一种常用的分组交换网络分层协议集。 客户端应用程序可以打开一条 TCP/IP 连接,连接到可能运行在世界任何地方的服务器应用程序。 一旦连接建立, 在客户端和服务器的计算机之间交换的报文就永远不会丢失、 受损或失序。
三步握手
TCP协议目的是为了保证数据能在两端准确连续的流动,可以想象两个建立起TCP通道的设备就如同接起了一根水管,数据就是水管中的水由一头流向另一头。然而TCP为了能让一个设备连接多根“水管”,让一个设备能同时与多个设备交互信息,它必须要保证不同水管之间不会产生串联或相互影响
为了确保数据能够正确分发,TCP用一种叫做TCB,也叫传输控制块的数据结构把发给不同设备的数据封装起来,我们可以把该结构看做是信封。一个TCB数据块包含了数据发送双方对应的socket信息以及拥有装载数据的缓冲区。
在两个设备要建立连接发送数据之前,双方都必须要做一些准备工作,分配内存建立起TCB数据块就是连接建立前必须要做的准备工作。
准备工作: 最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态
一次握手: TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x 。此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
二次握手:TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。ACK为1表示确认号有效,为0表示报文中不包含确认信息
三次握手: TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。当服务器收到客户端的确认后也进入established状态,此后双方就可以开始通信了。
注:tcp建立连接需要三次握手,SYN是发送标志位,ACK是确认标志位.
为什么TCP客户端最后还要发送一次确认呢?
主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
为什么要3次握手?
客户端和服务端通信前要进行连接,3次握手的作用就是双方都能明确自己和对方的收、发能力是正常的。
第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。
第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力,服务端的发送、接收能力是正常的。
TCP协议缺陷
DDOS又称为分布式拒绝服务,全称是Distributed Denial of Service。DDOS本是利用合理的请求造成服务器资源过载,导致服务不可用。常见的DDOS攻击有SYN flood(SYN flood)、UDP flood、ICMP、flood等,其中SYN flood是一种最为经典的DDOS攻击。SYN flood如此猖獗是因为它利用了TCP协议设计中的缺陷,而TCP/IP协议是整个互联网的基础,牵一发而动全身,如今想要修复这样的缺陷几乎成为不可能的事情。
SYN flood在攻击时,首先伪造大量的源IP地址,分别向服务器端发送大量的SYN包。
服务器端返回SYN/ACK包,因为源地址是伪造的,所以伪造的IP并不会应答。
服务器端没有收到伪造IP的回应,会重试3~5次并且等待一个SYN Time(—般为30秒至2分钟),如果超时则丢弃这个连接。
攻击者大量发送这种伪造源地址的SYN请求,服务器端将会消耗非常多的资源来处理这种半连接,同时还要不断地对这些IP进行SYN+ACK重试。
最后的结果是服务器无暇理睬正常的连接请求,导致拒绝服务。
四次挥手
数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于established(表示连接已经建立)状态,然后客户端主动关闭,服务器被动关闭。
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部, FIN=1(FIN表示关闭连接,SYN表示建立连接),其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1(确认序号为收到的序号加1),并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。 CLOSE_WAIT:表示在等待关闭状态
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1, ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2*MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。 FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态
简洁版:
(1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送。 (2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。 (3) 服务器关闭客户端的连接,发送一个FIN给客户端。 (4) 客户端发回ACK报文确认,并将确认序号设置为收到序号加1。
为什么客户端最后还要等待2MSL? MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
去向ACK消息最大存活时间(MSL) + 来向FIN消息的最大存活时间(MSL)。这恰恰就是**2MSL( Maximum Segment Life)。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。 第二,等待2MSL时间,客户端就可以放心地释放TCP占用的资源、端口号。如果不等,释放的端口可能会重连刚断开的服务器端口,这样依然存活在网络里的老的TCP报文可能与新TCP连接报文冲突,造成数据冲突,为避免此种情况,需要耐心等待网络老的TCP连接的活跃报文全部死翘翘,2MSL时间可以满足这个需求(尽管非常保守)!
为什么建立连接是三次握手,关闭连接确是四次挥手呢?
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。 关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必已经将全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
2.2 tcp数据传输
传输原理
TCP通过 “ 发送 — 应答 (ACK确认)”来确保传输的可靠性,它是端到端传输的。 TCP传输是分段的,一个HTTP响应报文会被操作系统切成多个MSS(Maximum Segment Size)大小的段,直到接收端接受到完整的报文为止。在此过程中,报文分段按照顺序进行发送,每个报文段在发送时,会做顺序编号,以便能够完整正确地组装。
MSS:Maximum Segment Size 最大报文段长度,是TCP协议的一个选项,用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度(不包括文段头)。如果MSS选项数据为512,则表示该报文段的发送方可以处理的最大报文段长度为512字节(不包括TCP与IP协议头长度)。主机一般默认mss为536字节
端口号:
表示同一个计算机上的不同进程
源端口号和目标端口号都是占用了两个字节
TCP的源端口号和目标端口号预计IP报文中的源IP和目标IP确认一条唯一的TCP连接
序号:4个字节
确认序号:ack,4个字节
控制位:URG、ACK、PSH、RST、SYN、FIN
TCP 的数据是通过名为 IP 分组( 或 IP 数据报) 的小数据块来发送的。HTTP 就是“HTTP over TCP over IP” 这个“协议栈” 中的最顶层了。 其安全版本 HTTPS 就是在 HTTP 和 TCP 之间插入了一个( 称为 TLS 或 SSL的) 密码加密层。
HTTP 要传送一条报文时, 会以流的形式将报文数据的内容通过一条打开的 TCP 连接按序传输。 TCP 收到数据流之后, 会将数据流砍成被称作段的小数据块, 并将段封装在 IP 分组中, 通过因特网进行传输。 所有这些工作都是由 TCP/IP 软件来处理的, HTTP 程序员什么都看不到。
每个 TCP 段都是由 IP 分组承载, 从一个 IP 地址发送到另一个 IP 地址的。 每个 IP分组中都包括:
一个 IP 分组首部(通常为 20 字节):包含源和目的 IP 地址、 长度和其他一些标记
一个 TCP 段首部(通常为 20 字节):包含了TCP端口号、 TCP 控制标记, 以及用于数据排序和完整性检查的一些数字值
一个 TCP 数据块(0 个或多个字节)
TCP 连接是通过 4 个值来识别的:< 源 IP 地址、 源端口号、 目的 IP 地址、 目的端口号 > 这 4 个值一起唯一地定义了一条连接。 两条不同的 TCP 连接不能在同一时刻拥有 4 个完全相同的地址组件值
滑动窗口协议
将TCP与UDP这样的简单传输协议区分开来的两种协议,不同的传输数据的质量。 TCP对于发送数据进行跟踪,这种数据管理需要协议有以下两大关键功能:
可靠性:保证数据确实到达目的地。如果未到达,能够发现并重传。
数据流控:管理数据的发送速率,以使接收设备不致于过载。
要完成这些任务,整个协议操作是围绕滑动窗口确认机制来进行的。因此,理解了滑动窗口,也就是理解了TCP。
(1)在我们滑动窗口协议之前,我们如何来保证发送方与接收方之间,每个包都能被收到,并且是按次序的呢?
问题:吞吐量非常的低。我们发完包1,一定要等确认包1,我们才能发送第二个包。
(2)那么我们就不能先连发几个包等他一起确认吗?这样的话速度更快,吞吐量更高
问题:如果过多的源同时以很快的速度发送大量的数据包,而此时接收方并没有如此高的接收数据的能力,因此极易导致网络的拥塞。
(3)滑动窗口协议(Sliding Window Protocol)
该协议是 TCP协议 的一种应用,用于网络数据传输时的流量控制,以避免拥塞的发生。该协议允许发送方在停止并等待确认前发送多个数据分组。由于发送方不必每发一个分组就停下来等待确认。因此该协议可以加速数据的传输,提高网络吞吐量。滑动窗口算法其实和这个是一样的,只是用的地方场景不一样。
如果我们在任一时间点对于这一过程做一个“快照”,那么我们可以将TCP buffer中的数据分为以下四类,并把它们看作一个时间轴:
已发送已确认 数据流中最早的字节已经发送并得到确认。这些数据是站在发送设备的角度来看的。
已发送但尚未确认 已发送但尚未得到确认的字节。发送方在确认之前,不认为这些数据已经被处理。
未发送而接收方已Ready 设备尚未将数据发出,但接收方根据最近一次关于发送方一次要发送多少字节确认自己有足够空间。发送方会立即尝试发送。
未发送而接收方Not Ready 由于接收方not ready,还不允许将这部分数据发出
丢包问题:比如4号发送了,一直收不到ack,那么10号因为窗口是满的,不能被发送。如何解决?
超时重发/重传:原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的ACK报文,那么就重新发送数据,直到发送成功为止。影响超时重传机制协议效率的一个关键参数是重传超时时间(RTO, Retransmission TimeOut)。RTO的值被设置过大过小都会对协议造成不利影响。
- RTO设长了,重发就慢,没有效率,性能差。
- RTO设短了,重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
- 连接往返时间(RTT,Round Trip Time),指发送端从发送TCP包开始到接收它的立即响应所消耗的时间。
在 Unix 以及 Windows 系统中,最初其重发超时的默认值一般设置为6秒(重发时间必须是0.5秒的倍数)左右。数据被重发之后若还是收不到确认应答,则进行再次发送。此时,等待确认应答的时间将会以2倍、4倍的指数函数延长。 此外,数据也不会被无限、反复地重发。达到一定重发次数之后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接,并且通知应用通信异常强行终止。
2.3 tcp性能
HTTP 紧挨着 TCP, 位于其上层, 所以 HTTP 事务的性能在很大程度上取决于底层TCP 通道的性能。
HTTP 事务的时延有以下几种主要原因:
- 通过 DNS 解析系统将 URI 中的主机名转换成一个 IP地址要花费对应的时间
- 每条新的 TCP 连接都会有连接建立时延,但如果有数百个 HTTP 事务的话, 这个时间消耗值会快速地叠加上去。
- 网络传输请求报文及服务器处理请求报文都需要时间
- Web 服务器会回送 HTTP 响应的花费时间
这些网络时延的大小取决于硬件速度、 网络和服务器的负载, 请求和响应报文的尺寸, 以及客户端和服务器之间的距离。 TCP 协议的技术复杂性也会对时延产生巨大的影响。
TCP连接的握手时延
延迟确认(ack)
由于网络自身无法确保可靠的分组传输( 如果网络设备超负荷的话, 可以随意丢弃分组), 所以 TCP 实现了自己的确认机制来确保数据的成功传输。每个 TCP 段都有一个序列号和数据完整性校验和。 服务端收到完好的TCP段时, 都会向发送者回送小的确认报文。 如果发送者没有在指定的窗口时间内收到确认信息, 发送者就认为分组已损毁或丢失, 并重发数据。
由于确认报文很小, 所以 TCP 允许在发往相同方向的输出数据分组中对其进行“捎带”。 TCP 将返回的确认信息与输出的数据分组结合在一起, 可以更有效地利用网络。 为了增加确认报文找到同向传输数据分组的可能性, 很多 TCP 栈都实现了一种“延迟确认” 算法。 延迟确认算法会在一个特定的窗口时间( 通常是 100 ~ 200 毫秒) 内将输出确认存放在缓冲区中, 以寻找能够捎带它的输出数据分组。 如果在那个时间段内没有输出数据分组, 就将确认信息放在单独的分组中传送。
但是当希望有相反方向回传分组的时候, 偏偏没有那么多。 通常, 延迟确认算法会引入相当大的时延。
tcp慢启动
主机开发发送数据报时,如果立即将大量的数据注入到网络中,可能会出现网络的拥塞。慢启动算法就是在主机刚开始发送数据报的时候先探测一下网络的状况,如果网络状况良好,发送方每发送一次文段都能正确的接受确认报文段。那么就从小到大的增加拥塞窗口的大小,即增加发送窗口的大小, 用于防止因特网的突然过载和拥塞。
TCP 慢启动限制了一个 TCP 端点在任意时刻可以传输的分组数。 简单来说, 每成功接收一个分组, 发送端就有了发送另外两个分组的权限。 如果某个 HTTP 事务有大量数据要发送, 是不能一次将所有分组都发送出去的。 必须发送一个分组, 等待确认; 然后可以发送两个分组, 每个分组都必须被确认, 这样就可以发送四个分组了,以此类推。 这种方式被称为“打开拥塞窗口”。
Nagle算法与TCP_NODELAY
TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同 时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽, TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此, TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。如果 TCP 发送了大量包含少量数据的分组, 网络的性能就会严重下降。
Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
Nagle 算法鼓励发送全尺寸的数据段:Nagle 算法试图在发送一个分组之前, 将大量TCP 数据绑定在一起, 以提高网络效率。
一个 TCP 连接上最多只能有一个未被确认的未完成的小分组,在该分组 ACK 到达之前不能发送其他的小分组。如果其他分组仍然在传输过程中, 就将那部分数据缓存起来。
只有当挂起分组被确认, 或者缓存中积累了足够发送一个全尺寸分组的数据时, 才会将缓存的数据发送出去。
Nagle 算法会引发几种 HTTP 性能问题。 首先, 小的 HTTP 报文可能无法填满一个分组, 可能会因为等待那些永远不会到来的额外数据而产生时延。 其次, Nagle 算法与延迟确认之间的交互存在问题——Nagle 算法会阻止数据的发送, 直到有确认分组抵达为止, 但确认分组自身会被延迟确认算法延迟 100 ~ 200 毫秒。
HTTP 应用程序常常会在自己的栈中设置参数 TCP_NODELAY, 禁用 Nagle 算法,提高性能。(Tomcat通过server.xml进行设置,默认为true)
2.4 串行事务处理
如果只对连接进行简单的管理, TCP 的性能时延可能会叠加起来。 比如, 假设有一 个包含了 3 个嵌入图片的 Web 页面。 浏览器需要发起 4 个 HTTP 事务来显示此页面:1 个用于顶层的 HTML 页面, 3 个用于嵌入的图片。 如果每个事务都需要(串行地建立) 一条新的连接, 那么连接时延和慢启动时延就会叠加起来 。
除了串行加载引入的实际时延之外, 加载一幅图片时, 页面上其他地方都没有动静也会让人觉得速度很慢。 用户更希望能够同时加载多幅图片。
串行加载的另一个缺点是, 有些浏览器在对象加载完毕之前无法获知对象的尺寸,而且它们可能需要尺寸信息来决定将对象放在屏幕的什么位置上, 所以在加载了足够多的对象之前, 无法在屏幕上显示任何内容。 在这种情况下, 可能浏览器串行装载对象的进度很正常, 但用户面对的却是一个空白的屏幕, 对装载的进度一无所知。
提高http连接性能方法:并行连接、持久连接、管道化连接
2.5 并行连接
HTTP 允许客户端(浏览器)打开多条TCP连接, 并行地执行多个 HTTP 事务(每个TCP连接处理一个HTTP事务)。
在以下例子中, 并行加载了四幅嵌入式图片, 每个事务都有自己的 TCP 连接。 页面上的每个组件都包含一个独立的http事务
并行连接不一定更快 :打开大量连接会消耗很多内存资源, 从而引发自身的性能问题。 复杂的 Web页面可能会有数十或数百个内嵌对象。 客户端可能可以打开数百个连接, 但 Web 服务器通常要同时处理很多其他用户的请求, 所以很少有 Web 服务器希望出现这样的情况。 一百个用户同时发出申请, 每个用户打开 100 个连接, 服务器就要负责处理10 000 个连接。 这会造成服务器性能的严重下降。 对高负荷的代理来说也同样如此。
实际上, 浏览器确实使用了并行连接, 但它们会将向同一个域名请求的并行连接的总数限制为一个较小的值。
浏览器同域名请求的最大并发数限制:
2.6 持久连接
Web 客户端经常会打开到同一个站点的连接。 比如, 一个 Web 页面上的大部分内嵌图片通常都来自同一个 Web 站点, 而且相当一部分指向其他对象的超链通常都指向同一个站点。 因此, 初始化了对某服务器 HTTP 请求的应用程序很可能会在不久的将来对那台服务器发起更多的请求(比如, 获取在线图片)。 这种性质被称为站点局部性(site locality)。
HTTP/1.1(以及 HTTP/1.0 的各种增强版本) 允许 HTTP 设备在事务处理结束之后将 TCP 连接保持在打开状态, 以便为未来的 HTTP 请求重用现存的连接。 在事务处理结束之后仍然保持在打开状态的 TCP 连接被称为持久连接。 非持久连接会在每个事务结束之后关闭。 持久连接会在不同事务之间保持打开状态, 直到客户端或服务器决定将其关闭为止。
重用已对目标服务器打开的空闲持久连接, 就可以避开缓慢的连接建立阶段。 而且,已经打开的连接还可以避免慢启动的拥塞适应阶段, 以便更快速地进行数据的传输。
Connection: Keep-Alive
在持久连接中,每一个HTTP请求都是串行的。每个请求的发送都必须等待上一个请求的响应。
2.7 管道化连接
HTTP/1.1 允许在持久连接上使用**管道化(pipeline)**技术。 这是相对于 keep-alive 连接的又一性能优化。
在响应到达之前, 可以将多条请求放入队列。 当第一条请求通过网络流向另一端的服务器时, 第二条和第三条请求也可以开始发送了。 在高时延网络条件下, 这样做可以降低网络的环回时间, 提高性能。
对管道化连接有几条限制:
- 必须按照与请求相同的顺序回送 HTTP 响应(如果顺序发送了请求1/2/3,那么无论服务器处理哪个请求更快,相应的时候必须按照请求的顺序响应)。 HTTP 报文中没有序列号标签, 因此如果收到的响应失序了, 就没办法将其与请求匹配起来了。此时容易引起一个问题:头部阻塞。
- HTTP 客户端必须做好连接会在任意时刻关闭的准备。 如果客户端打开了一条持久连接, 并立即发出了 10 条请求,服务器可能在只处理了5 条请求之后关闭连接,剩下的 5 条请求会失败,客户端必须能够应对这些过早关闭连接的情况, 重新发出这些请求。
- 只有幂等的请求能够被管线化。HTTP 客户端不应该用管道化的方式发送会产生副作用的请求(比如 POST)。 由于无法安全地重试 POST 这样的非幂等请求, 所以出错时, 就存在某些方法永远不会被执行的风险。
3.发展历程
http/0.9
只有一个 get,没有hader等描述信息,发送完毕,就关闭tcp连接。一个http事务使用一个tcp连接
http/1.0
增加很多命令、增加status code和header、多字符集支持、多部分发送、权限、缓存等
缺点:无法实现真正意义上的复用(connection:keep-alive)、head-of-line blocking(队头、线头、头部阻塞)
http/1.1
持久连接、长连接
pipline管道化技术
头部增加host
spdy 会话层
SPDY 没有完全改写 HTTP 协议,而是在 TCP/IP 的应用层与运输层之 间通过新加会话层的形式运作。同时考虑到安全性问题,SPDY 规定通信中使用 SSL。 SPDY 以会话层的形式加入,控制对数据的流动,但还是采用 HTTP 建立通信连接,该层工作在SSL层之上、HTTP层之下
二进制分帧、首部压缩、多路复用、对请求划分优先级、服务器推送流
http/2
二进制分帧、首部压缩、多路复用、对请求划分优先级、服务器推送流
HTTP Working-Group 以 SPDY/2 为基础,开发 HTTP/2
但是,HTTP/2跟 SPDY 仍有不同的地方,主要是以下两点:
1.HTTP/2 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS
2.HTTP/2 消息头的压缩算法采用 HPACK,而非 SPDY 采用的 DEFLATE
4.http2新特性
对1.x协议语意的完全兼容、性能的大幅提升
4.1 二进制分帧
在二进制分帧层上,HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。帧是数据传输的最小单位,以二进制传输代替原本的明文传输。
HTTP 2.0 所有的通信都在一个连接(TCP连接)上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。
HTTP性能的关键在于低延迟而不是高带宽!大多数HTTP 连接的时间都很短,而且是突发性的,但TCP 只在长时间连接传输大块数据时效率才最高。
HTTP 2.0 通过让所有数据流共用同一个连接,可以更有效地使用TCP 连接,让高带宽也能真正的服务于HTTP的性能提升。
好处:
可以减少服务连接压力,内存占用少了,连接吞吐量大了
由于 TCP 连接减少而使网络拥塞状况得以改观;
慢启动时间减少,拥塞和丢包恢复速度更快
http2四个概念:
Connection
1 个 TCP 连接,包含 1 个或者多个 stream。所有通信都在一个 TCP 连接上完成,此连接可以承载任意数量的双向数据流
Stream
一个双向通信的数据流,包含 1 条或者多条 Message。每个数据流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息。流是连接中的一个虚拟信道,可以承载双向消息传输。每个流有唯一整数标识符。为了防止两端流ID冲突,客户端发起的流具有奇数ID,服务器端发起的流具有偶数ID。
Message
消息是指逻辑上的HTTP消息(请求/响应)。一系列数据帧组成了一个完整的消息。比如一系列DATA帧和一个HEADERS帧组成了请求消息。
Frame
最小通信单位,以二进制压缩格式存放内容。来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
Frame 由 Frame Header 和 Frame Payload 两部分组成。所有帧都以固定的 9 字节大小的头作为帧开始,后跟可变长度的有效载荷 payload。
帧头的字段定义:
Length
帧有效负载的长度(数据的长度,不包含头部),注意:帧头的 9 个八位字节不包含在此长度值中。
Type
这 8 位用来表示帧类型的。帧类型确定帧的格式和语义。实现方必须忽略并丢弃任何类型未知的帧。
DATA: 用于传输HTTP消息体 HEADERS:用户传输关于流的额外的首部字段 PRIORITY:用户指定或者重新指定引用资源的优先级RST_STRING:用于通知流的非正常终止 SETTINGS:用于通知两端通信方式的数据配置 PUSH_PROMISE:用于发出创建流和服务器引用资源的要约PING:用于计算往返时间,执行“活性”检查 GOAWAY:用于通知对端停止在当前连接的创建流 WINDOW_UPDATE:用于针对个别流或个别连接实现流量控制CONTINUATION:用于继续一系列首部块片段
Flags
标志位,常用的标志位有 END_HEADERS 表示头数据结束,相当于 HTTP/1 里头后的空行(“\r\n”)。
R
保留的 1 位。该位的语义未定义,发送时必须保持未设置 (0x0),接收时必须忽略。
Stream Identifier
流标识符,表示为无符号 31 位整数。由客户端发起的流必须使用奇数编号的流标识符;那些由服务器发起的必须使用偶数编号的流标识符。DATA 帧必须与某一个流相互关联。
stream ID 的作用:
- 实现多路复用的关键。接收端的实现可以根据这个 ID 并发、组装消息。同一个 stream 内 frame 必须是有序的。
- 推送依赖性请求的关键。客户端发起的流是奇数编号,服务端发起的流是偶数编号。
4.2 头部压缩(hpack)
在 HTTP/1 中,HTTP 请求和响应都是由「状态行、请求 / 响应头部、消息主体」三部分组成。一般而言,消息主体都会经过 gzip 压缩,或者本身传输的就是压缩过后的二进制文件(例如图片、音频),但状态行和头部却没有经过任何压缩,直接以纯文本传输。
根据 HTTP Archive 的统计,当前平均每个页面都会产生上百个请求。越来越多的请求导致消耗在头部的流量越来越多,尤其是每次都要传输 UserAgent、 Cookie 这类不会频繁变动的内容,完全是一种浪费。
HTTP/2协议中定义了 HPACK,这是一种新的压缩方法,它消除了多余的 header 字段,将漏洞限制到已知的安全攻击,并且在受限的环境中具有有限的内存需求。HPACK 格式特意被设计成简单且不灵活的形式:两种特性都降低了由于实现错误而引起的互操作性或安全性问题的风险;没有定义扩展机制,只能通过定义完整的替换来更改格式。
需要注意的是,http 2.0关注的是首部压缩,而我们常用的gzip等是报文内容(body)的压缩,二者不仅不冲突,且能够一起达到更好的压缩效果。
如何进行头部压缩
HPACK头部压缩需要在支持 HTTP/2 的浏览器和服务端之间
- 维护一份相同的静态表(Static Table),包含常见的头部名称,以及常见的头部名称与值的组合(静态表内容共61项,索引号1-61)
- 维护一份相同的动态表(Dynamic Table),当一个header name 或者header value在静态表中不存在,会被插入动态表中,可以动态地添加内容(动态表索引从62开始)
- 客户端和服务端会共同维护一份动态表
- 第一次发送的时候需要明文发送(要经过Huffman编码),第二次及第N次发送索引号
- 对不存在的头部使用哈夫曼编码(Huffman Coding),并动态缓存到索引(动态表)
静态表
一个预定义且不可更改的 header 字段列表。先定义好的内容,只有固定的几十个值,如果要发送的值符合静态表时,用对应的 Index 替换即可,这样就大大压缩了头部的大小,如果遇到不在静态表中的值,就会用到动态表。
动态索引
动态表是一个由先进先出的队列维护的有空间限制的表,同样维护的是头部与对应的索引。
每个动态表只针对一个连接(TCP),每个连接的压缩解压缩的上下文有且仅有一个动态表。
那么动态表就是,当一个头部没有出现过的时候,会把他插入动态表中,索引从62开始。
4.3 多路利用
http1.1中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量的限制,超过限制数目的请求会被阻塞。这也是为何一些站点会有多个静态资源 CDN 域名的原因之一。
http 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接(每个域名一个连接)即可。http2连接可以承载数十或数百个流的复用,多路复用意味着来自很多流的数据包能够混合在一起通过同样连接传输。当到达终点时,再根据不同帧首部的流标识符重新连接将不同的数据流进行组装。
websocket 原生协议由于没有这个 stream ID 类似的字段,所以它原生不支持多路复用。在同一个 stream 内部的 frame 由于没有其他的 ID 编号了,所以无法乱序,必须有序,无法并发。
单个 HTTP/2 连接可以包含多个并发打开的 stream 流,任一一个端点都可能交叉收到来自多个 stream 流的帧。
stream 流可以单方面建立和使用,也可以由客户端或服务器共享。
任何一个端都可以关闭 stream 流。
在 stream 流上发送帧的顺序非常重要。收件人按照收到的顺序处理帧。特别是,HEADERS 和 DATA 帧的顺序在语义上是重要的。
stream 流由整数标识。stream 流标识符是由发起流的端点分配给 stream 流的。
4.4 服务器推送
服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确地请求。并且,服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。
正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。Server Push 让 http1.x 时代使用内嵌资源的优化手段变得没有意义;如果一个请求是由你的主页发起的,服务器很可能会响应主页内容、logo 以及样式表,因为它知道客户端会用到这些东西,这相当于在一个 HTML 文档内集合了所有的资源。
不过与之相比,服务器推送还有一个很大的优势:可以缓存!也让在遵循同源的情况下,不同页面之间可以共享缓存资源成为可能
注意两点: 1、推送遵循同源策略; 2、这种服务端的推送是基于客户端的请求响应来确定的。
当服务端需要主动推送某个资源时,便会发送一个 Frame Type 为 PUSH_PROMISE 的 Frame,里面带了 PUSH 需要新建的 StreamID。
意思是告诉客户端:接下来我要用这个 ID 向你发送东西,客户端准备好接着。客户端解析 Frame 时,发现它是一个 PUSH_PROMISE 类型,便会准备接收服务端要推送的流。
4.5 http2.0性能瓶颈
启用http2.0后会给性能带来很大的提升,但同时也会带来新的性能瓶颈。因为现在所有的压力集中在底层一个TCP连接之上,TCP很可能就是下一个性能瓶颈,比如单个TCP packet丢失导致整个连接阻塞,无法逃避,此时所有消息都会受到影响。
4.6 安全风险
- 窃听风险:通信使用明文,明文报文不具备保密性,内容可能被窃听
- 冒充风险:不验证通信方的身份(不进行身份验证),有可能遇到伪装
- 篡改风险:无法证明报文的完整性,有可能已遭篡改