URL 在浏览器被被输入到页面展现的过程中发生了什么

前置问题

HTTP请求完成后是否会断开TCP连接

短连接

HTTP 最早期的模型,也是 HTTP/1.0 的默认模型,是短连接。每一个 HTTP 请求都由它自己独立的连接完成;这意味着发起每一个 HTTP 请求之前都会有一次 全新的TCP 握手,而且是连续不断的。 TCP 协议握手本身就是耗费时间的,所以 TCP 可以保持更多的热连接来适应负载。短连接破坏了 TCP 具备的能力,新的冷连接降低了其性能。 这是 HTTP/1.0 的默认模型(如果没有指定 Connection 协议头,或者是值被设置为 close)。但在HTTP/1.0中,也有某些服务器对 Connection: keep-aliveHeader 进行了支持。 即HTTP/1.0默认是短连接,可以设置成长连接。 在 HTTP/1.1 中,只有当 Connection 被设置为 close 时才会用到这个短连接模型。

除非是要兼容一个非常古老的,不支持长连接的系统,没有一个令人信服的理由继续使用这个模型。

长连接

短连接有两个比较大的问题:创建新连接耗费的时间尤为明显,另外 TCP 连接的性能只有在该连接被使用一段时间后(热连接)才能得到改善。为了缓解这些问题,长连接 的概念便被设计出来了,甚至在 HTTP/1.1 之前。或者这被称之为一个 keep-alive 连接。 一个长连接会保持一段时间,重复用于发送一系列请求,节省了新建 TCP 连接握手的时间,还可以利用 TCP 的性能增强能力,同时https的SSL连接也可以复用。当然这个连接也不会一直保留着:连接在空闲一段时间后会被关闭(服务器可以使用 Keep-Alive 协议头来指定一个最小的连接保持时间)。 长连接也还是有缺点的;就算是在空闲状态,它还是会消耗服务器资源,而且在重负载时,还有可能遭受 DoS attacks 攻击。这种场景下,可以使用非长连接,即尽快关闭那些空闲的连接,也能对性能有所提升。通常ngnix会帮我们做这一部分操作。 HTTP/1.0 里默认并不使用长连接。把 Connection 设置成 close 以外的其它参数都可以让其保持长连接,通常会设置为 retry-after。 在 HTTP/1.1 里,默认就是长连接的,协议头都不用再去声明它(但我们还是会把它加上,万一某个时候因为某种原因要退回到 HTTP/1.0 呢)。

要注意的一个重点是 HTTP 的连接管理适用于两个连续节点之间的连接,如 hop-by-hop,而不是 end-to-end。当模型用于从客户端到第一个代理服务器的连接和从代理服务器到目标服务器之间的连接时(或者任意中间代理)效果可能是不一样的。HTTP 协议头受不同连接模型的影响,比如 Connection 和 Keep-Alive,就是 hop-by-hop 协议头,它们的值是可以被中间节点修改的。

一个 TCP 连接可以对应几个 HTTP 请求

如果是长连接模式,也就是 Connection: keep-alive下,一个TCP连接可以发送多个HTTP请求。

http能不能一次连接多次请求

默认情况下,HTTP 请求是按顺序发出的。下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。

HTTP/1.1里引入了流水线标识:Pipelining

A client that supports persistent connections MAY "pipeline" its requests (i.e., send multiple requests without waiting for each response). A server MUST send its responses to those requests in the same order that the requests were received. 一个支持持久连接的客户端可以在一个连接中发送多个请求(不需要等待任意请求的响应)。收到请求的服务器必须按照请求收到的顺序发送响应。

流水线是在同一条长连接上发出连续的请求,而不用等待应答返回。这样可以避免连接延迟。理论上讲,性能还会因为两个 HTTP 请求有可能被打包到一个 TCP 消息包中而得到提升。就算 HTTP 请求不断的继续,尺寸会增加,但设置 TCP 的 MSS(Maximum Segment Size) 选项,仍然足够包含一系列简单的请求。 不过并不是所有类型的 HTTP 请求都能用到流水线:只有 GET、HEAD、PUT 和 DELETE 能够被安全的重试:如果有故障发生时,流水线的内容要能被轻易的重试。 Pipelining 这种设想看起来比较美好,但是在实践中会出现许多问题:

  • 一些代理服务器不能正确的处理 HTTP Pipelining。

  • 正确的流水线实现是复杂的。

  • Head-of-line Blocking 连接头阻塞: 在建立起一个 TCP 连接之后,假设客户端在这个连接连续向服务器发送了几个请求。按照标准,服务器应该按照收到请求的顺序返回结果,假设服务器在处理首个请求时花费了大量时间,那么后面所有的请求都需要等着首个请求结束才能响应。 所以现代浏览器默认是不开启 HTTP Pipelining 的。 HTTP/2定义了一个流的概念,实现了多路复用,但是因为实现过于复杂依然应用范围不广,具体可以参考HTTP/2笔记之流和多路复用

浏览器对同一 Host 建立 TCP 连接到数量有没有限制

Chrome 最多允许对同一个 Host 建立六个 TCP 连接。不同的浏览器有一些区别。

URL 在浏览器被被输入到页面展现的过程中发生了什么

DNS 解析:将域名解析成 IP 地址

DNS解析是一种递归查询,主机会先查询本地DNS服务器是否有网址和IP的映射关系,如果有就返回IP,没有则向上级请求,直到找到这个IP并返回。 DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。DNS解析会优先从缓存查询。 如果是http请求,端口号默认是80,https则默认是443,这个时候请求的地址就变成了:IP_ADDR:80/443

TCP 连接:TCP 三次握手

  • 客户端发送一个带 SYN=1,Seq=X 的数据包到服务器端口(第一次握手,由浏览器发起,告诉服务器我要发送请求了)

  • 服务器发回一个带 SYN=1, ACK=X+1, Seq=Y 的响应包以示传达确认信息(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)

  • 客户端再回传一个带 ACK=Y+1, Seq=Z 的数据包,代表“握手结束”(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧) 注意:如果服务端使用的是ngnix反向代理机制,那么这里TCP连接的IP是ngnix服务器的IP,然后ngnix会保持这个连接并做转发。

发送 HTTP 请求

HTTP请求将发送到ngnix服务器然后转发到服务器,ngnix配置如下:

服务器处理请求并返回 HTTP 报文

这里通常是各种http服务模型,比如我用的就是golang的gin框架以及GRPC框架。

浏览器解析渲染页面

前端部分不熟悉,暂留。

断开连接:TCP 四次挥手

  • 发起方向被动方发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态。 (第一次挥手:由浏览器发起的,发送给服务器,我请求报文发送完了,你准备关闭吧)

  • 被动方发送报文,Ack、Seq,表示同意关闭请求。此时主机发起方进入 FIN_WAIT_2 状态。 (第二次挥手:由服务器发起的,告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧)

  • 被动方向发起方发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态。 (第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧)

  • 发起方向被动方发送报文段,Ack、Seq。然后进入等待 TIME_WAIT 状态。被动方收到发起方的报文段以后关闭连接。发起方等待一定时间未收到回复,则正常关闭。 (第四次挥手:由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)

四次挥手过程会触发TCP的TIME_WAIT状态。

最后更新于