输入网址按下回车发生了什么

使用 IP 地址访问 Web 服务器

HTTP 协议是运行在 TCP/IP 基础上的,依靠 TCP/IP 协议来实现数据的可靠传输。
所以浏览器要用 HTTP 协议收发数据,首先要做的就是建立 TCP 连接。

简要叙述一下一次最简单的浏览器 HTTP 请求过程:

  1. 浏览器从地址栏的输入中获得服务器的 IP 地址和端口号;
  2. 浏览器用 TCP 的三次握手与服务器建立连接;
  3. 浏览器向服务器发送拼好的报文;
  4. 服务器收到报文后处理请求,同样拼好报文再发给浏览器;浏览器解析报文,渲染输出页面。
  5. 浏览器解析报文,渲染输出页面;

使用域名访问 Web 服务器

浏览器看到了网址里的“www.chrono.com”,发现它不是数字形式的 IP 地址,那就肯定是域名了,于是就会发起域名解析动作,通过访问一系列的域名解析服务器,试图把这个域名翻译成 TCP/IP 协议里的 IP 地址。

在域名解析的过程中会有多级的缓存,浏览器首先看一下自己的缓存里有没有,如果没有就向操作系统的缓存要,还没有就检查本机域名解析文件 hosts,也就是上一讲中我们修改的“C:\WINDOWS\system32\drivers\etc\hosts”。

刚好,里面有一行映射关系“127.0.0.1 www.chrono.com”,于是浏览器就知道了域名对应的 IP 地址,就可以愉快地建立 TCP 连接发送 HTTP 请求了。

建立 TCP/IP 连接

标志位(数据包)

SYN:Synchronize Sequence Numbers 同步序列编号
ACK:Acknowledgement 确认字符

状态

ISTEN:侦听TCP端口的连接请求(等待对方发送连接请求)
SYN-SENT:发送连接请求后等待匹配的连接需求(已发送请求,等待回复)
SYN-RECEIVED:收到和发送一个连接请求后等待对连接请求的确认(收到连接请求,等待回复)
ESTABLISHED:一个已经打开的连接,数据可以传送给用户(已经建立连接)

tcp.png

总结

  1. 第一次握手:客户端向服务器发送 SYN 标志位(序号是J),并且进入 SYN_SEND 状态(等待服务器确认状态)。
  2. 第二次握手:服务器收到客户端的 SYN J 后,服务端会确认改数据包已收到并发送 ACK 标志位(序号是J+1)和 SYN 标志位(序号是K),服务器进入 SYN_RECV 状态(请求接收并等待客户端确认状态)。
  3. 第三次握手:客户端进入连接建立状态,向服务端发送 ACK 标志位(序号是K+1),确认客户端已收到建立连接确认,服务端收到 ACK 标志位后,服务端进入连接已建立状态。

关闭 TCP/IP 连接

标志位

FIN:finsh 关闭连接

状态:

FIN-WAIT-1:等待远程TCP的连接中断请求,或先前的连接中断请求的确认
FIN-WAIN-2:从远程TCP等待连接中断请求
CLOSE-WAIT:等待从本地用户发来的连接中断请求
LAST-ACK:等待原来发向远程TCP的连接中断请求的确认
TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认CLOSED:没有任何连接状态

tcp_close.png

总结

  1. 客户端发送连接关闭报文(此时已停止发送数据),第一次挥手。
    报文首部:FIN=1(序列号seq=u)
    此刻客户端进入终止等待1(FIN-WAIT-1)状态。

  2. 服务器收到连接关闭报文,并发送确认报文, 第二次挥手。
    报文首部:ACK=1 ack=u+1(确认FIN)(序列号squ=v) 。
    此刻服务端进入关闭等待(CLOSE-WAIT)状态。
    连接半关闭状态,客户端没有数据要发送,但服务器如果还要发送数据,客户端依然需要接受。

  3. 客户端收到服务器的确认请求后,客户端进入终止等待 2(FIN-WAIT-2)状态
    服务器在这期间还要确认客户端所需要的数据是否真的发送完毕了,如果还没有发送完,继续发送数据。

  4. 服务器确认数据已发送完毕,向客户端发送连接关闭报文 第三次挥手。
    报文首部:FIN=1 ACK=1 ack=u+1(确认上次数据包) 序列号 seq=w。
    此刻进入最后确认状态(LAST-ACK)。

  5. 客户端收到服务端的连接关闭报文后,发出接收确认报文 ,第四次挥手。
    报文首部:ACK=1 ack=w+1(确认上次数据包) 序列号 seq=u+1
    此刻客户端进入等待(TIME-WAIT)状态。

  6. 服务器收到客户端发出的确认,立即进入TCP关闭状态(CLOSE),TCP连接结束。
    TCP关闭,服务端要比客户端早一些。

TIME-WAIT

TIME-WAIT时长:2MSL Maximum Segment Lifetime 最大报文生存时间。
MSL的值根据不同的情况而不同,一般是30秒,1分钟,2分钟。

保证客户端发送的最后一个报文能够发到服务器,一旦报文丢失,服务器会认为,自己最后一次发送的FIN+ACK包,客户端并没有收到,此时,服务器会重新发送一次FIN+ACK包(第三次挥手),而客户端可以在2MSL的TIME-WAIT时间内收到重新传输的FIN+ACK包,接着重新进行第四次挥手,并重启2MSL计时器。

为什么是四次挥手

第一次挥手的时候发送了FIN包,服务器接收到以后,表示客户端不再发送数据了,但还能接收数据。这时服务器先向客户端先发送确认包,并且确认自己是否还有数据没有发送给客户端,这个确认的阶段是COLSE-WAIT,所以在终止等待1(CLOSE-WAIT)的开始和结束需要各发送一个包,状态开始时向客户端发送的包是确认收到客户端的FIN包,状态结束后向客户端发送的是确认数据已经发送完毕,所以是四次挥手。

TCP 连接建立后,客户端出现故障

TCP保活计时器:客户端如果出现故障,服务器每收到一次客户端的请求都会重新复位保活计时器,时间通常是2小时,如2小时还没有收到客户端的数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍无反应,服务器就认为客户端出了故障,此时将关闭连接。

真实的网络世界

真实的互联网世界要比这两个场景要复杂的多,我利用下面的这张图来做一个详细的说明。

true.png

如果你用的是电脑台式机,那么你可能会使用带水晶头的双绞线连上网口,由交换机接入固定网络。如果你用的是手机、平板电脑,那么你可能会通过蜂窝网络、WiFi,由电信基站、无线热点接入移动网络。

假设你要访问的是 Apple 网站,显然你是不知道它的真实 IP 地址的,在浏览器里只能使用域名“www.apple.com”访问,那么接下来要做的必然是域名解析。这就要用 DNS 协议开始从操作系统、本地 DNS、根 DNS、顶级 DNS、权威 DNS 的层层解析,当然这中间有缓存,可能不会费太多时间就能拿到结果。

别忘了互联网上还有另外一个重要的角色 CDN,它也会在 DNS 的解析过程中“插上一脚”。DNS 解析可能会给出 CDN 服务器的 IP 地址,这样你拿到的就会是 CDN 服务器而不是目标网站的实际地址。

因为 CDN 会缓存网站的大部分资源,比如图片、CSS 样式表,所以有的 HTTP 请求就不需要再发到 Apple,CDN 就可以直接响应你的请求,把数据发给你。

由 PHP、Java 等后台服务动态生成的页面属于“动态资源”,CDN 无法缓存,只能从目标网站获取。于是你发出的 HTTP 请求就要开始在互联网上的“漫长跋涉”,经过无数的路由器、网关、代理,最后到达目的地。

目标网站的服务器对外表现的是一个 IP 地址,但为了能够扛住高并发,在内部也是一套复杂的架构。通常在入口是负载均衡设备,例如四层的 LVS 或者七层的 Nginx,在后面是许多的服务器,构成一个更强更稳定的集群。

负载均衡设备会先访问系统里的缓存服务器,通常有 memory 级缓存 Redis 和 disk 级缓存 Varnish,它们的作用与 CDN 类似,不过是工作在内部网络里,把最频繁访问的数据缓存几秒钟或几分钟,减轻后端应用服务器的压力。

如果缓存服务器里也没有,那么负载均衡设备就要把请求转发给应用服务器了。这里就是各种开发框架大显神通的地方了,例如 Java 的 Tomcat/Netty/Jetty,Python 的 Django,还有 PHP、Node.js、Golang 等等。它们又会再访问后面的 MySQL、PostgreSQL、MongoDB 等数据库服务,实现用户登录、商品查询、购物下单、扣款支付等业务操作,然后把执行的结果返回给负载均衡设备,同时也可能给缓存服务器里也放一份。

应用服务器的输出到了负载均衡设备这里,请求的处理就算是完成了,就要按照原路再走回去,还是要经过许多的路由器、网关、代理。如果这个资源允许缓存,那么经过 CDN 的时候它也会做缓存,这样下次同样的请求就不会到达源站了。

最后网站的响应数据回到了你的设备,它可能是 HTML、JSON、图片或者其他格式的数据,需要由浏览器解析处理才能显示出来,如果数据里面还有超链接,指向别的资源,那么就又要重走一遍整个流程,直到所有的资源都下载完。

总结

  1. HTTP 协议基于底层的 TCP/IP 协议,所以必须要用 IP 地址建立连接;

  2. 如果不知道 IP 地址,就要用 DNS 协议去解析得到 IP 地址,否则就会连接失败;

  3. 建立 TCP 连接后会顺序收发数据,请求方和应答方都必须依据 HTTP 规范构建和解析报文;

  4. 为了减少响应时间,整个过程中的每一个环节都会有缓存,能够实现“短路”操作;

  5. 虽然现实中的 HTTP 传输过程非常复杂,但理论上仍然可以简化成实验里的“两点”模型;

知识拓展

1. 你能试着解释一下在浏览器里点击页面链接后发生了哪些事情吗?

首先判断是不是一个域名。如果是一个域名,走 DNS 解析流程。过程如下。

浏览器缓存 -> 操作系统缓存 -> 本地 host 文件 -> 非权威域名服务器
-> 根域名服务器 -> 顶级域名服务器 -> 权威域名服务器

获取到IP 地址后,首先要建立 TCP/IP 连接(三次握手)。

详见 建立 TCP/IP 连接。

连接建立完成后,发起 HTTP 请求。

可能 DNS 解析返回的不是源站的 IP 地址,而是 CDN 服务器 的 IP 地址。
这时,会直接从 CDN 服务器返回所需要的资源。

发起的 HTTP 请求,经过无数的路由器、网关、代理后最终到达源站。

如果目标服务器存在负载均衡。
负载均衡设备还会先访问系统里的缓存(memory 级缓存 Redis 和 disk 级缓存 Varnish)。
如果缓存服务器也没有,就转发给应用服务器。那就是各类框架应用,操作数据库后,返回给负载均衡设备。

获取到资源后,按照原路返回。浏览器获取到资源后,进行后续处理。

如果这个资源允许缓存,经过 CDN 时也会做缓存。
如果返回的资源还指向别的资源,就会重走一遍流程,知道所有的数据都下载完。
由于HTTP/1.1 长连接特性,默认不会立即关闭连接。

如请求完毕,一段时间后,会关闭 TCP/IP 连接。

详见 关闭 TCP/IP 连接。

2. 如果是一个不存在的域名,那么浏览器的工作流程会是怎么样的呢?

走 DNS 解析过程。

浏览器缓存 -> 操作系统缓存 -> 本地 host 文件 -> 非权威域名服务器
-> 根域名服务器 -> 顶级域名服务器 -> 权威域名服务器

如果获取到 IP 地址,正常建立 TCP/IP 连接,发起 HTTP 请求。否则就会报出 dns 解析错误,流程结束。

3. 减少 HTTP 请求的方法?

  1. 能做雪碧图就做雪碧图(小图标、装饰用的东西),CSS雪碧图;
  2. base64编码图片,webpack 配置 url-loader;
  3. 合并脚本与样式表代码 HTML/JS/CSS;
  4. 配置多个域名,CDN加速;
  5. 尽量使用浏览器的缓存机制;

CDN加速:CDN具有并发请求能力,本身速度就比较快。即用自己的域名在第三方服务器上进行解析,从而生成CDN加速域名。