从浏览器输入URL到页面加载展示,中间发生了什么

页面加载整体流程分析,主要包括浏览器处理URL、DNS解析、获取TCP连接、发送HTTP请求、服务器处理请求、服务器返回响应、浏览器渲染页面这几大步骤。

这是一个面试中常会遇到的问题,其中涉及的问题很多,接下来会试着一一分析一下。

具体过程主要分为浏览器处理URL、DNS解析、获取TCP连接、发送HTTP请求、服务器处理请求、服务器返回响应、浏览器渲染页面这几大步骤。

本文以输入www.google.com 为例进行分析,浏览器以chrome为例。


浏览器处理URL

1. 校验URL格式

首先,浏览器会校验用户输入的URL格式是否正确,如果不是URL地址,那么浏览器会自动调用默认的搜索引擎进行搜索。

合法的URL地址如下

1
<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>

URL 为了在不同协议不同传输机制都可以安全的运送信息,采用的字符都是符合 ASCII 集的。这其中还包含一些保留字符,比如上面的 URL 协议中的分割字符,等特殊含义的字符。
而不安全的字符,非 ASCII 的 Unicode(中文等)字符,就会通过转义去处理,使用% 。

此部分就是检查 url 的合法性,并对不合法的进行转义。浏览器还会检查 HSTS 列表。

2. 检查HSTS 列表

HSTS 就是一种安全策略的机制,是为了让浏览器强制使用 HTTPS 访问的,详细可以去这篇文章看,介绍的比较清楚。当你的网站均采用 HTTPS,并符合它的安全规范,就可以申请加入 HSTS 列表,之后用户不加 HTTPS 协议再去访问你的网站,浏览器都会定向到 HTTPS。

例如用户输入google.com ,最终会定向到https://www.google.com/

1549786677960

DNS解析

我们浏览器中输入的地址只是一个代号,服务器是不认识这个名称的,服务器需要IP地址来进行定位。

通过DNS(Domain Name System)域名系统解析,我们可以把url转换为IP地址

1. 查询缓存

1.1 查看浏览器内部缓存

检测域名是否存在于浏览器缓存中,如果有缓存直接使用,没有则下一步。打开 chrome://net-internals/#dns 即可查看本机浏览器的 dns 缓存。(chrome中有效期为1分钟)

1.2 系统缓存

浏览器会调用一个库函数,此函数会先去检测本地 hosts文件,查看是否有对应ip。

1.3. 路由器缓存、ISP 缓存

如果浏览器和系统缓存都没有,系统的库函数就会像 DNS 服务器发送请求。而网络服务一般都会先经过路由器以及网络服务商(电信),所以会先查询路由器缓存,然后再查询 ISP 的 DNS 缓存。

2. 本地 DNS 服务器

windows下利用ipconfig命令可以查看本机DNS服务器地址

1549802306452

3. 域名服务器

如果本机DNS没有找到的情况下,本地域名服务器会向根域名服务器发送一个请求,如果根域名服务器也不存在该域名时,本地域名会向com顶级域名服务器发送一个请求,依次类推下去。直到最后本地域名服务器得到google的IP地址并把它缓存到本地,供下次查询使用。

关于根域名服务器的介绍以及为什么是13台,可以去这篇文章 看,讲的很明白。

到此处的过程为:根域服务器(.) -> 顶级域名服务器(.com)-> 主域名服务器(google.com)

经过以上这些查询,如果域名正常,在某一步骤应该就会返回 IP 地址,如果直到最后一步都没有查询到,浏览器就会提示找不到服务器地址。

得到目标服务器地址后,可以进行TCP连接了。

TCP连接

TCP协议(Transmission Control Protocol,传输控制协议)是一种面向连接,确保数据在端到端之间可靠传输的协议。由于TCP协议十分复杂,这里仅作简单介绍。

TCP header

根据TCP/IP协议,需要本机和目标服务器的ipport才能进行传输。

本机ip:由操作系统分配 本机port:由操作系统分配

目标服务器ip:通过DNS解析 目标服务器port:http协议默认80,https协议默认443

建立TCP连接

TCP有6个标志位,分别为SYN, ACK, FIN, URG, PSH, RST。置一有效。

这里介绍SYN,ACK, FIN这3个标志位。

SYN(Synchronize Sequence Number): 建立连接的同步信号

ACK(Acknowledgement): 对收到的数据进行确认

FIN(Finish): 表示后面没有数据要发送,连接需要关闭

还需要用到两个序号,序列号seq和确认序号ack

seq(sequence number):表示要发送的数据的第一个字节的序号,后面按这个逻辑递增

ack(Acknowledgement number):期望收到的下一个报文段数据的第一个字节的序号

3次握手的过程如下图所示

1549789979776

为什么要三次挥手来建立连接?

主要是要初始化Sequence Number(seq) 的初始值。seq要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。

为什么是三次而不是两次

主要因为信息对等防止超时

信息对等:如果只进行2次握手,B机器无法确认A收到了自己的信息,只有通过第3次握手,才能确认自己的发报能力和收报能力都是正常的

1549791850506

防止超时:TTL网络报文的生存时间往往都会超过TCP请求超时时间,如果只用2次握手建立连接,那么当A机器发送第一个请求超时,第二个请求才和B机器建立连接,当A发送的第一 个超时请求这个时候才到达B时,B机器会建立一个新的连接,然而,由于A的状态不是SYN_SENT,会直接丢弃B的确认数据,导致B单方面创建连接完毕。

1549791947646

断开TCP连接

建立连接需要3次,而断开连接需要4次挥手。主要过程见下图。

1549794267158

这里主要介绍一下CLOSE_WAITTIME_WAIT这两个状态。

  • CLOSE_WAIT: 该状态表示等待关闭,并通知应用程序发送剩余数据,处理现场信息,关闭相关资源。

  • TIME_WAIT: 主动要求关闭的机器在收到对方的FIN报文后,发送ACK报文,并进入TIME_WAIT状态,等待2MSL后即可进入CLOSED关闭状态。

MSL(Maximum Segment Lifetime),等待2MSL是报文在网络上生存的最长时间,超过阈值报文则被丢弃,一般来说MSL大于TTL衰减至0的时间。在RFC793中规定MSL为2分钟,然而在当前告诉网络中,2分钟等待时间会造成极大的资源浪费,在高并发服务器上通常会使用更小的值。

既然TIME_WAIT这个状态看起很鸡肋,为何不直接进入CLOSED状态呢?

主要有两点原因:

第一,确认被动关闭方能够顺利进入CLOSED状态。如果最后一个ACK没有到达B机器,B机器会重发FIN+ACK报文,当A机器收到了第二次FIN+ACK报文,会重发一次ACK,并重新计时。如果A没有等待时间而是发送完ACK后直接关闭,可能会导致B机器无法确保收到最后的ACK指令。

第二,防止失效请求。防止已失效连接的请求数据包与正常连接的请求数据包混淆而发生异常。

RFC793定义了MSL为2分钟,Linux设置成了30s,在服务器上通过变更/etc/sysctl.conf文件修改

1
net.ipv4.tcp_fin_timeout = 30

服务器上TIME_WAIT状态过多的原因?

如果在大并发的短链接下,此状态可能过多,可通过优化服务器参数得到解决vim /etc/sysctl.conf
编辑文件,选择添加以下内容:

1
2
3
4
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30

然后执行 /sbin/sysctl -p 让参数生效。

net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为,syncookies是妥协版的TCP协议,并不严谨。默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。官方文档上说tcp_tw_reuse 加上tcp_timestamps(又叫PAWS, for Protection Against Wrapped Sequence Numbers)可以保证协议的角度上的安全,但是你需要tcp_timestamps在两边都被打开(你可以读一下tcp_twsk_unique的源码 )。默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收。如果是tcp_tw_recycle被打开了话,会假设对端开启了tcp_timestamps,然后会去比较时间戳,如果时间戳变大了,就可以重用。但是,如果对端是一个NAT网络的话(如:一个公司只用一个IP出公网)或是对端的IP被另一台重用了,这个事就复杂了。建连接的SYN可能就被直接丢掉了(你可能会看到connection time out的错误)。默认为0,表示关闭。
使用tcp_tw_reuse和tcp_tw_recycle来解决TIME_WAIT的问题是非常非常危险的,因为这两个参数违反了TCP协议(RFC 1122)

服务器上CLOSE_WAIT状态过多的原因?

对方关闭socket连接,我方忙于读或写,没有及时关闭连接。

检查代码,特别是释放资源的代码

检查配置,特别是处理请求的线程配置

1
2
3
4
# 查看443端口上CLOSE_WAIT状态的命令
netstat -ant | grep -i "443" | grep CLOSE_WAIT | wc-1
# 统计各个状态的连接数
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

为什么需要四次挥手才能断开连接?

因为TCP建立的是全双工连接,发送方和接收方都需要FIN报文和ACK报文。

TCP滑动窗口

The TCP/IP Guide中有详细的解说

TCP对于发送数据进行跟踪,这种数据管理需要协议有以下两大关键功能:

可靠性:保证数据确实到达目的地。如果未到达,能够发现并重传。

数据流控:管理数据的发送速率,以使接收设备不致于过载。

要完成这些任务,整个协议操作是围绕滑动窗口确认机制来进行的。因此,理解了滑动窗口,也就是理解了TCP。

  • 发送方的滑动窗口示意图

图中分为4个部分,其中黑色方框框出的部分就是滑动窗口

1. 已发送已确认 数据流中最早的字节已经发送并得到确认。这些数据是站在发送设备的角度来看的。如上图所示,31个字节已经发送并确认。

2. 已发送但尚未确认 已发送但尚未得到确认的字节。发送方在确认之前,不认为这些数据已经被处理。上图所示14字节为第2类。

3. 未发送而接收方已Ready 设备尚未将数据发出,但接收方根据最近一次关于发送方一次要发送多少字节确认自己有足够空间。发送方会立即尝试发送。如图,第3类有6字节。

4. 未发送而接收方Not Ready 由于接收方not ready,还不允许将这部分数据发出。

  • 滑动后窗口示意图

当发送方接收到一系列字节的确认时,它知道数据被接收方成功地接收了,发送方将这些数据从“已发送但未确认”移动到“已发送已确认”。这样使得滑动窗口向右滑动,允许发送发发送更多数据。

img

发送HTTPS请求

TCP连接建立后,客户端向浏览器发送HTTP请求。

现在很多网站都使用https协议替代了http协议。

HTTPS全称为HTTP over SSL。其中,SSL(Secure Socket Layer)安全套接字层,是一个工作于应用层和传输层之间,为应用提供加密传输的协议。顾名思义,HTTPS就是在HTTP上增加了SSL协议的加密能力。

关于数字加密

这里需要补充一点数字加密的相关知识,在另一篇文章中提及。

HTTPS建立连接

HTTPS建立连接步骤大致如下:

  1. 客户端向服务端发送请求,客户端会告诉服务端自己支持哪些加密套件
  2. 服务端收到请求后,会返回一系列协议数据,并以一个没有数据内容的Server Hello Done 作为结束。
  3. 客户端在收到服务端的握手信息后,也会发送一系列协议。
  4. 服务端在接收客户端的确认信息和验证信息后,会对客户端发送的数据进行确认。
  5. 最后,如果双方都确认加密无误后,各自按照之前约定的Session Secret对应用数据进行加密传输。

具体的协议数据等,可以通过抓包工具进行查看,这里就不做展开了。

1549807179453

服务器处理请求

WEB服务器

如果后端采用WEB服务器和应用服务器的架构,请求先到达WEB服务器,然后在由WEB服务器转发到某个应用服务器。

常见的WEB服务器有Nginx

Nginx利用epoll的方式读取请求,判断请求类型。

对于静态请求:读取服务器硬盘上的相关文件,直接返回

对于动态请求:转发到应用服务器(这里假设为Tomcat),如果有多个应用服务器,需要采用策略(负载均衡)

应用服务器

常用的应用服务器有Tomcat

Tomcat是一个由Java编写的可以运行Servlet/JSP的容器,Javaweb的代码运行在这个容器上

Tomcat会采用阻塞I/O(Blocking I/O)或者I/O多路复用技术(NIO)

  • BIO: 为每个请求分配一个线程去处理
  • NIO: 监听所有的连接,当连接状态发生变化,才用一个线程/进程对那个连接进行处理,处理完继续监视

服务器返回响应

Http请求到达应用服务器后,会被交给某个Servlet处理。

如果使用框架(SpringMVC),DispatcherServlet会处理收到的请求。经过处理器映射器,处理器适配器交给Controller执行,返回的模型视图(ModelAndView)对象会交给视图解析器(ViewResolver)

浏览器渲染页面

视图解析器内部调用render方法,将Model数据填充到View中,最终将View包装成Response传给浏览器。

浏览器从HTTPResponse中读取数据,准备显示页面。

由于HTML中可能引用大量其他资源,例如js,css,图片等,浏览器会下载这些资源(从DNS获取IP地址开始)

当服务器发给客户端资源文件时,会告知何时过期(使用Cache-control或者Expire),客户端可以把文件缓存到本地,下次再有请求时,如果没有过期,可以直接从本地缓存读取。如果过期,客户端会询问服务器资源是否被修改(依据上一次服务器发送的Last-Modified),如果未被修改(状态码304 Not Modified),可以继续使用缓存,否则服务器会发送最新的资源到客户端。


参考