本文试图回答一个古老的面试问题:当你在浏览器中输入Google.com
并且按下回车之后发生了什么?
不过我们不再局限于平常的回答,而是想办法回答地尽可能具体,不遗漏任何细节。
这将是一个协作的过程,所以深入挖掘吧,并且帮助我们一起完善它。仍然有大量的细节等待着你来添加,欢迎向我们发送Pull Requset
!
我们从回车键按下开始。
回车键按下
为了从头开始,我们选择键盘上的回车键被按到最低处作为起点。在这个时刻,一个专用于回车键的电流回路被直接或者通过电容器闭合了,使得少量的电流进入了键盘的逻辑电路系统。这个系统会扫描每个键的状态,对于按键开关的电位弹跳变化进行噪音消除(debounce
),并将其转化为键盘码值。在这里,回车的码值是13。键盘控制器在得到码值之后,将其编码,用于之后的传输。现在这个传输过程几乎都是通过通用串行总线(USB
)或者蓝牙(Bluetooth
)来进行的,以前是通过PS/2
或者ADB
连接进行。
USB键盘:
- 键盘的
USB
元件通过计算机上的USB
接口与USB
控制器相连接,USB
接口中的第一号针为它提供了5V
的电压 - 键码值存储在键盘内部电路一个叫做
endpoint
的寄存器内 USB
控制器大概每隔10ms
便查询一次endpoint
以得到存储的键码值数据,这个最短时间间隔由键盘提供- 键值码值通过
USB
串行接口引擎被转换成一个或者多个遵循低层USB
协议的USB
数据包 - 这些数据包通过
D+
针或者D-
针(中间的两个针),以最高1.5Mb/s
的速度从键盘传输至计算机。速度限制是因为人机交互设备总是被声明成”低速设备”(USB 2.0 compliance
) - 这个串行信号在计算机的
USB
控制器处被解码,然后被人机交互设备通用键盘驱动进行进一步解释。之后按键的码值被传输到操作系统的硬件抽象层
虚拟键盘(触屏设备):
- 在现代电容屏上,当用户把手指放在屏幕上时,一小部分电流从传导层的静电域经过手指传导,形成了一个回路,使得屏幕上触控的那一点电压下降,屏幕控制器产生一个中断,报告这次“点击”的坐标
- 然后移动操作系统通知当前活跃的应用,有一个点击事件发生在它的某个
GUI
部件上了,现在这个部件是虚拟键盘的按钮 - 虚拟键盘引发一个软中断,返回给
OS
一个“按键按下”消息 - 这个消息又返回来向当前活跃的应用通知一个“按键按下”事件
产生中断[非USB键盘]
键盘在它的中断请求线(IRQ
)上发送信号,信号会被中断控制器映射到一个中断向量,实际上就是一个整型数。CPU
使用中断描述符表(IDT
)把中断向量映射到对应函数,这些函数被称为中断处理器,它们由操作系统内核提供。当一个中断到达时,CPU
根据IDT
和中断向量索引到对应的中端处理器,然后操作系统内核出场了。
(Windows)一个 WM_KEYDOWN 消息被发往应用程序
HID
把键盘按下的事件传送给 KBDHID.sys
驱动,把HID
的信号转换成一个扫描码(Scancode
),这里回车的扫描码是VK_RETURN(0x0d)
。 KBDHID.sys
驱动和 KBDCLASS.sys
(键盘类驱动,keyboard class driver
)进行交互,这个驱动负责安全地处理所有键盘和小键盘的输入事件。之后它又去调用 Win32K.sys
,在这之前有可能把消息传递给安装的第三方键盘过滤器。这些都是发生在内核模式。
Win32K.sys
通过 GetForegroundWindow() API
函数找到当前哪个窗口是活跃的。这个API
函数提供了当前浏览器的地址栏的句柄。Windows
系统的message pump
机制调用 SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam)
函数, lParam
是一个用来指示这个按键的更多信息的掩码,这些信息包括按键重复次数(这里是0),实际扫描码(可能依赖于OEM
厂商,不过通常不会是 VK_RETURN
),功能键(alt
, shift
, ctrl
)是否被按下(在这里没有),以及一些其他状态。
Windows
的 SendMessage API
直接将消息添加到特定窗口句柄 hWnd
的消息队列中,之后赋给 hWnd
的主要消息处理函数 WindowProc
将会被调用,用于处理队列中的消息。
当前活跃的句柄 hWnd
实际上是一个edit control
控件,这种情况下,WindowProc
有一个用于处理WM_KEYDOWN
消息的处理器,这段代码会查看 SendMessage
传入的第三个参数 wParam
,因为这个参数是 VK_RETURN
,于是它知道用户按下了回车键。
(Mac OS X)一个 KeyDown NSEvent被发往应用程序
中断信号引发了I/O Kit Kext
键盘驱动的中断处理事件,驱动把信号翻译成键码值,然后传给OS X
的WindowServer
进程。然后, WindowServer
将这个事件通过Mach
端口分发给合适的(活跃的,或者正在监听的)应用程序,这个信号会被放到应用程序的消息队列里。队列中的消息可以被拥有足够高权限的线程使用 mach_ipc_dispatch
函数读取到。这个过程通常是由 NSApplication
主事件循环产生并且处理的,通过 NSEventType
为 KeyDown
的 NSEvent
。
(GNU/Linux)Xorg 服务器监听键码值
当使用图形化的 X Server
时,X Server
会按照特定的规则把键码值再一次映射,映射成扫描码。当这个映射过程完成之后, X Server
把这个按键字符发送给窗口管理器(DWM
,metacity
, i3
等等),窗口管理器再把字符发送给当前窗口。当前窗口使用有关图形API
把文字打印在输入框内。
解析URL
浏览器通过URL
能够知道下面的信息:
Protocol ”http”
使用HTTP
协议
Resource ”/”
请求的资源是主页(index
)
输入的是URL还是搜索的关键字?
当协议或主机名不合法时,浏览器会将地址栏中输入的文字传给默认的搜索引擎。大部分情况下,在把文字传递给搜索引擎的时候,URL
会带有特定的一串字符,用来告诉搜索引擎这次搜索来自这个特定浏览器。
检查HSTS列表···
- 浏览器检查自带的“预加载
HSTS
(HTTP
严格传输安全)”列表,这个列表里包含了那些请求浏览器只使用HTTPS
进行连接的网站 - 如果网站在这个列表里,浏览器会使用
HTTPS
而不是HTTP
协议,否则,最初的请求会使用HTTP
协议发送 - 注意,一个网站哪怕不在
HSTS
列表里,也可以要求浏览器对自己使用HSTS
政策进行访问。浏览器向网站发出第一个HTTP
请求之后,网站会返回浏览器一个响应,请求浏览器只使用HTTPS
发送请求。然而,就是这第一个HTTP
请求,却可能会使用户收到downgrade attack
的威胁,这也是为什么现代浏览器都预置了HSTS
列表。
转换非ASCII的Unicode字符
- 浏览器检查输入是否含有不是
a-z
,A-Z
,0-9
,-
或者.
的字符 - 这里主机名是
google.com
,所以没有非ASCII
的字符,如果有的话,浏览器会对主机名部分使用Punycode
编码
首先,对于http
肯定是有客户端和服务器的,在这个语境中,客户端和服务器本质上也都是一个软件,实现了http
协议相关标准的软件。客户端一般由都是由浏览器充当,也就是说,在浏览器中实现了http
客户端的相关功能。而服务器的实现就多种多样啦,我们可以用java
写servlet
,c#
写ASP.net
,还有php
,ruby
,Python
,nodejs
等。实际上我想,http
服务在操作系统底层应该有实现,而这些语言只不过是利用操作系统的http
服务封装成自己的接口供开发人员编写web
服务器程序。而我们熟悉的IIS
,Tomcat
,Apache
,Web logic
,都是能够作为某些web
服务器容器的大型服务器平台,它们都会包括很多更为强大的功能。一般来说,我们这里所说的服务器指的是自己用特定语言写的web
应用服务器程序。nodejs
不需要web
容器,本身就有对http
的直接应用模块,所以用nodejs
创建一个web
服务器是很方便的。
整体通信
有了客户端和服务器,就可以开始通信了,整体上分为3个步骤:
- 因为
http
是构建在TCP
之上,那么自然是要经过3次握手创建连接。 - 创建连接后,服务器会根据
url
请求中的信息进行处理,作出响应,一般来说是找到一个html
文件返回给客户端。 - 客户端即浏览器得到
html
,进行渲染。
下面详细说下这3个步骤
创建连接
这个跟网络关联多一些,只能大体说一下。对于http
的客户端,它的输入就是一个url
,而对于创建连接,它需要的只是url
的host
(主机)部分,而主机地址一般是网站的域名,所以第一步肯定是是域名解析,也就是要通过DNS
服务器进行域名解析得到网站的ip
地址,然后向这个ip
地址发送一个连接建立的请求,如果服务器接收到请求会返回一个确认,客户端得到确认再次发送确认,连接建立成功。当然在这个过程中还会涉及到很多细节。
DNS查询···
- 浏览器检查域名是否在缓存当中
- 如果缓存中没有,就去调用
gethostbynme
库函数(操作系统不同函数也不同)进行查询 gethostbyname
函数在试图进行DNS
解析之前首先检查域名是否在本地Hosts
里,Hosts
的位置 不同的操作系统有所不同- 如果
gethostbyname
没有这个域名的缓存记录,也没有在hosts
里找到,它将会向DNS
服务器发送一条DNS
查询请求。DNS
服务器是由网络通信栈提供的,通常是本地路由器或者ISP
的缓存DNS
服务器。 - 查询本地
DNS
服务器 - 如果
DNS
服务器和我们的主机在同一个子网内,系统会按照下面的ARP
过程对DNS
服务器进行ARP
查询 - 如果
DNS
服务器和我们的主机在不同的子网,系统会按照下面的ARP
过程对默认网关进行查询
ARP
要想发送ARP
广播,我们需要有一个目标IP
地址,同时还需要知道用于发送ARP
广播的接口的Mac
地址。
- 首先查询
ARP
缓存,如果缓存命中,我们返回结果:目标IP = MAC
如果缓存没有命中:
- 查看路由表,看看目标
IP
地址是不是在本地路由表中的某个子网内。是的话,使用跟那个子网相连的接口,否则使用与默认网关相连的接口。 - 查询选择的网络接口的
MAC
地址 - 我们发送一个二层
ARP
请求:
ARP Request:
Sender MAC: interface:mac:address:here
Sender IP: interface.ip.goes.here
Target MAC: FF:FF:FF:FF:FF:FF (Broadcast)
Target IP: target.ip.goes.here
根据连接主机和路由器的硬件类型不同,可以分为以下几种情况:
- 直连:
如果我们和路由器是直接连接的,路由器会返回一个 ARP Reply
(见下面)。
- 集线器:
如果我们连接到一个集线器,集线器会把ARP
请求向所有其它端口广播,如果路由器也“连接”在其中,它会返回一个 ARP Reply
。
- 交换机:
如果我们连接到了一个交换机,交换机会检查本地 CAM/MAC
表,看看哪个端口有我们要找的那个MAC
地址,如果没有找到,交换机会向所有其它端口广播这个ARP
请求。
如果交换机的MAC/CAM
表中有对应的条目,交换机会向有我们想要查询的MAC
地址的那个端口发送ARP
请求
如果路由器也“连接”在其中,它会返回一个 ARP Reply
ARP Reply:
Sender MAC: target:mac:address:here
Sender IP: target.ip.goes.here
Target MAC: interface:mac:address:here
Target IP: interface.ip.goes.here
现在我们有了DNS
服务器或者默认网关的IP
地址,我们可以继续DNS
请求了:
- 使用53端口向
DNS
服务器发送UDP
请求包,如果响应包太大,会使用TCP
- 如果本地
/ISP DNS
服务器没有找到结果,它会发送一个递归查询请求,一层一层向高层DNS
服务器做查询,直到查询到起始授权机构,如果找到会把结果返回
使用套接字
当浏览器得到了目标服务器的IP
地址,以及URL
中给出来端口号(http
协议默认端口号是80
, https
默认端口号是443
),它会调用系统库函数 socket
,请求一个 TCP
流套接字,对应的参数是 AF_INET
和SOCK_STREAM
。
这个请求首先被交给传输层,在传输层请求被封装成TCP segment
。目标端口会会被加入头部,源端口会在系统内核的动态端口范围内选取(Linux
下是ip_local_port_range
)
TCP segment
被送往网络层,网络层会在其中再加入一个IP
头部,里面包含了目标服务器的IP
地址以及本机的IP
地址,把它封装成一个TCP packet
。
这个TCP packet
接下来会进入链路层,链路层会在封包中加入frame
头部,里面包含了本地内置网卡的MAC
地址以及网关(本地路由器)的MAC
地址。像前面说的一样,如果内核不知道网关的MAC
地址,它必须进行ARP
广播来查询其地址。
到了现在, TCP
封包已经准备好了,可是使用下面的方式进行传输:
- 以太网
- WiFi
- 蜂窝数据网络
对于大部分家庭网络和小型企业网络来说,封包会从本地计算机出发,经过本地网络,再通过调制解调器把数字信号转换成模拟信号,使其适于在电话线路,有线电视光缆和无线电话线路上传输。在传输线路的另一端,是另外一个调制解调器,它把模拟信号转换回数字信号,交由下一个 网络节点 处理。节点的目标地址和源地址将在后面讨论。
大型企业和比较新的住宅通常使用光纤或直接以太网连接,这种情况下信号一直是数字的,会被直接传到下一个 网络节点 进行处理。
最终封包会到达管理本地子网的路由器。在那里出发,它会继续经过自治区域的边界路由器,其他自治区域,最终到达目标服务器。一路上经过的这些路由器会从IP
数据报头部里提取出目标地址,并将封包正确地路由到下一个目的地。IP
数据报头部TTL
域的值每经过一个路由器就减1,如果封包的TTL
变为0,或者路由器由于网络拥堵等原因封包队列满了,那么这个包会被路由器丢弃。
上面的发送和接受过程在TCP连接期间会发生很多次:
- 客户端选择一个初始序列号(
ISN
),将设置了SYN
位的封包发送给服务器端,表明自己要建立连接并设置了初始序列号 - 服务器端接受到
SYN
包,如果它可以建立连接:
服务器端选择它自己的初始序列号
服务器端设置SYN
位,表明自己选择了一个初始序列号
服务器端把 (客户端ISN + 1
)复制到ACK
域,并且设置ACK
位,表明自己接收到了客户端的第一个封包 - 客户端通过发送下面一个封包来确认这次连接:
自己的序列号+1
接收端ACK+1
设置ACK
位 - 数据通过下面的方式传输:
当一方发送了N
个Bytes
的数据之后,将自己的SEQ
序列号也增加N
另一方确认接收到这个数据包(或者一系列数据包)之后,它发送一个ACK
包,ACK
的值设置为接收到的数据包的最后一个序列号 - 关闭连接时:
要关闭连接的一方发送一个FIN
包
另一方确认这个FIN
包,并且发送自己的FIN
包
要关闭的一方使用ACK包来确认接收到了FIN
UDP 数据包 (TLS 握手)
- 客户端发送一个
Client hello
消息到服务器端,消息中同时包含了它的TLS
版本,可用的加密算法和压缩算法。 - 服务器端向客户端返回一个
Server hello
消息,消息中包含了服务器端的TLS
版本,服务器选择了哪个加密和压缩算法,以及服务器的公开证书,证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程,直到协商生成一个新的对称密钥 - 客户端根据自己的信任
CA
列表,验证服务器端的证书是否有效。如果有效,客户端会生成一串伪随机数,使用服务器的公钥加密它。这串随机数会被用于生成新的对称密钥 - 服务器端使用自己的私钥解密上面提到的随机数,然后使用这串随机数生成自己的对称主密钥
- 客户端发送一个
Finished
消息给服务器端,使用对称密钥加密这次通讯的一个散列值 - 服务器端生成自己的
hash
值,然后解密客户端发送来的信息,检查这两个值是否对应。如果对应,就向客户端发送一个Finished
消息,也使用协商好的对称密钥加密 - 从现在开始,接下来整个
TLS
会话都使用对称秘钥进行加密,传输应用层(HTTP
)内容
TCP 数据包 (HTTP 协议···)
如果浏览器是Google
出品的,它不会使用HTTP
协议来获取页面信息,而是会与服务器端发送请求,商讨使用SPDY
协议。
如果浏览器使用HTTP
协议,它会向服务器发送这样的一个请求:
GET / HTTP/1.1
Host: google.com
[其他头部]
“其他头部”包含了一系列的由冒号分割开的键值对,它们的格式符合HTTP
协议标准,它们之间由一个换行符分割开来。这里我们假设浏览器没有违反HTTP
协议标准的bug
,同时浏览器使用 HTTP/1.1
协议,不然的话头部可能不包含 Host
字段,同时 GET
请求中的版本号会变成 HTTP/1.0
或者 HTTP/0.9
。
HTTP/1.1
定义了“关闭连接”的选项 close
,发送者使用这个选项指示这次连接在响应结束之后会断开:
Connection:close
不支持持久连接的 HTTP/1.1
必须在每条消息中都包含 close
选项。
在发送完这些请求和头部之后,浏览器发送一个换行符,表示要发送的内容已经结束了。
服务器端返回一个响应码,指示这次请求的状态,响应的形式是这样的:
200 OK
[response headers]
然后是一个换行,接下来有效载荷(payload
),也就是 www.google.com
的HTML
内容。服务器下面可能会关闭连接,如果客户端请求保持连接的话,服务器端会保持连接打开,以供以后的请求重用。
如果浏览器发送的HTTP
头部包含了足够多的信息(例如包含了 Etag
头部,以至于服务器可以判断出,浏览器缓存的文件版本自从上次获取之后没有再更改过,服务器可能会返回这样的响应:
304 Not Modified
[response headers]
这个响应没有有效载荷,浏览器会从自己的缓存中取出想要的内容。
在解析完HTML
之后,浏览器和客户端会重复上面的过程,直到HTML
页面引入的所有资源(图片,CSS
,favicon.ico
等等)全部都获取完毕,区别只是头部的 GET / HTTP/1.1
会变成 GET /$(相对www.google.com的URL) HTTP/1.1
。
如果HTML
引入了 www.google.com
域名之外的资源,浏览器会回到上面解析域名那一步,按照下面的步骤往下一步一步执行,请求中的 Host
头部会变成另外的域名。
服务器处理
HTTPD(HTTP Daemon)
在服务器端处理请求/相应。最常见的 HTTPD
有 Linux
上常用的 Apache
和 nginx
,与 Windows
上的 IIS
。
HTTPD
接收请求- 服务器把请求拆分为以下几个参数:
HTTP
请求方法(GET
,POST
,HEAD
,PUT
和DELETE
)。在访问Google
这种情况下,使用的是GET
方法
域名:google.com
请求路径/页面:/ (我们没有请求google.com
下的指定的页面,因此 / 是默认的路径) - 服务器验证其上已经配置了
google.com
的虚拟主机 - 服务器验证
google.com
接受GET
方法 - 服务器验证该用户可以使用
GET
方法(根据IP
地址,身份信息等) - 如果服务器安装了
URL
重写模块(例如Apache
的mod_rewrite
和IIS
的URL Rewrite
),服务器会尝试匹配重写规则,如果匹配上的话,服务器会按照规则重写这个请求 - 服务器根据请求信息获取相应的响应内容,这种情况下由于访问路径是 “/” ,会访问首页文件。(你可以重写这个规则,但是这个是最常用的)
- 服务器会使用指定的处理程序分析处理这个文件,比如假设
Google
使用PH
P,服务器会使用PHP
解析index
文件,并捕获输出,把PHP
的输出结果给请求者
建立好连接后,客户端就会发送http
请求,请求信息包含一个头部和一个请求体,一般的web
技术都会把请求进行封装然后交给我们的服务器进行处理,比如servlet
会把请求封装成httpservletrequest
对象,把响应封装成httpsevletresponse
对象。nodejs
的http
模块,当你创建服务器的时候会写一个回调函数,回调的参数用来接受http
请求对象和响应对象,然后在回调函数中对请求进行处理。
在请求对象中我们可以得到path
(路径),queryString
(查询字符串),body
(post
请求中提交的数据)等。对请求的处理就可以很复杂,也可以很简单。我们可以根据path
找到客户端想要的文件,读取这个文件,然后通过响应对象把内容返回给客户端,这个过程,不同的技术提供的api
可能不同,尤其是用惯了MVC
框架的人,可能只是指定一个文件,或者在配置文件中设置一下就好了。但是最终的实现肯定是符合http
响应标准的,也就是要有一个响应头和一个响应体。我一般接触到的设置响应头就是设置content-type
来决定MIME
类型,设置Cache-Control
,last-modify
等缓存内容。一般来说返回给客户端的内容是一个html
字符串,然后content-type
设为text/html
。当然也可能客户端请求的是一个image
文件,那么就是读取image
文件后,content-type
可能设为image/png
,image/jpg
等,然后把内容返回给客户端。这样一次对请求的处理就结束了。
当然这个过程太单一,而且处理过程也可能很复杂,又有数据的操作,又有页面的构建,又有路径的查找匹配,又有文件的读取等等,于是就出现了MVC
框架以及后来演变出的各种MV*
框架。只是概述一下MVC
主要做了什么,在我看来最重要的就是解耦和模块化。我认为MVC
实现最重要的有两点:
- 路由匹配,
http
请求的path
中就不需要指定到具体的视图位置,而是按照我们制定的规则进行匹配,这样就有了很大的灵活性,可编程性。 - 模板技术,一般来说我们最后返回给客户端的是一个
html
字符串,而有时候这个字符串往往不是静态单一的,有的时候需要和数据进行结合,需要拼接。这就带来了很大的麻烦,模板技术为解决这个问题带来很大的便利性,同时又能够把视图和数据进行解耦。
当服务器提供了资源之后(HTML
,CSS
,JS
,图片等),浏览器会执行下面的操作:
解析HTML,CSS,JS
渲染——构建 DOM
树 -> 渲染 -> 布局 -> 绘制
客户端渲染
客户端接收到服务器传来的响应对象,从中得到html
字符串和MIME
,根据MIME
知道了要用页面渲染引擎来处理内容即html
字符串,于是进入页面渲染阶段,这又是一个很庞杂的体系。
从浏览器的角度讲,它包含几大组件,网络功能(比如http
的实现)算是其中之一,渲染引擎也是其中之一,还有其它的一些比如自己UI
界面,javascript
解释器,客户端数据存储等等。在这里我们主要关注渲染引擎和javascript
解释器,对于web
开发者来说,这才是浏览器的核心。
我们能够在浏览器中看到一个页面,那么这个页面是怎么出现的呢?实际上就是调用底层绘图API
给画出来的。不同的渲染引擎,它的实现也不同,主流的引擎包括IE
的Trident
,chrome
和safary
的webkit
,firefox
的Gecko
,chrome
又出了一个Blink
,放弃webkit
。于是乎才有了让人头疼的各种兼容性问题。而这些兼容性问题就是由于主流浏览器渲染引擎不同而导致的。
浏览器的功能是从服务器上取回你想要的资源,然后展示在浏览器窗口当中。资源通常是 HTML
文件,也可能是 PDF
,图片,或者其他类型的内容。资源的位置通过用户提供的 URI(Uniform Resource Identifier)
来确定。
浏览器解释和展示 HTML
文件的方法,在 HTML
和 CSS
的标准中有详细介绍。这些标准由Web
标准组织 W3C(World Wide Web Consortium)
维护。
不同浏览器的用户界面大都十分接近,有很多共同的 UI
元素:
- 一个地址栏
- 后退和前进按钮
- 书签选项
- 刷新和停止按钮
- 主页按钮
浏览器高层架构
组成浏览器的组件有:
- 用户界面 用户界面包含了地址栏,前进后退按钮,书签菜单等等,除了请求页面之外所有你看到的内容都是用户界面的一部分
- 浏览器引擎 浏览器引擎负责让
UI
和渲染引擎协调工作 - 渲染引擎 渲染引擎负责展示请求内容。如果请求的内容是
HTML
,渲染引擎会解析HTML
和CSS
,然后将内容展示在屏幕上 - 网络组件 网络组件负责网络调用,例如
HTTP
请求等,使用一个平台无关接口,下层是针对不同平台的具体实现 UI
后端UI
后端用于绘制基本UI
组件,例如下拉列表框和窗口。UI
后端暴露一个统一的平台无关的接口,下层使用操作系统的UI
方法实现
Javascript
解释器Javascript
解释器用于解析和执行Javascript
代码- 数据存储 数据存储组件是一个持久层。浏览器可能需要在本地存储各种各样的数据,例如
Cookie
等。浏览器也需要支持诸如localStorage
,IndexedDB
,WebSQL
和FileSystem
之类的存储机制
整体上页面渲染的过程大致是这样的:
渲染引擎得到html
字符串作为输入,然后对html
进行转换,转化成能够被DOM
处理的形式,接着转换成一个dom
树,在解析html
的过程,解析到<link>
,<script>
,<img>
等一些请求标签时,会发送请求把对应的内容获取到。这时又会同步进行css
的解析,构建出css
样式规则应用到dom
树上,然后进行一定的布局处理,比如标记节点块在浏览器中的坐标等形成最终的渲染树,最后根据这棵渲染树在浏览器窗口中进行绘制。最终我们就看到了页面的样子。
当然在页面渲染过程中还会同步进行javascript
的解析,而且这两者是在同一个线程中的,所以一旦javascript
死循环,页面的渲染也就进行不下去了。
以上是从一个web
开发者的角度思考的整个过程。如果从别的角度更细化的去想,还包括许多内容:
比如整个网络通信中协议的封装:
在本机中,把要传输的内容即请求对象在应用层上加上App
首部,传递到传输层加上TCP
首部,到网络层加上IP
首部,数据链路层加上以太网的首部和尾部,然后转换成bit
流进入网络环境中。到达主机后在一层层解封装,最后把内容交给服务器程序。
再比如这个过程中的认证,加密,安全,编码等问题都会有一定的处理,不过这些内容我就不是很了解。
从数据传输的角度:
- 在浏览器中输入地址
- 浏览器查找域名的
Ip
地址—DNS
查找
浏览器缓存–由浏览器自己决定,而不是操作系统决定
系统缓存–系统调用,查看系统缓存的记录
路由器缓存
ISP DNS
缓存–如果还找不到就递归,知道顶级域名服务器 - 浏览器给
Web
服务器发送HTTP
请求
GET http://facebook.com/ HTTP/1.1
Accept: application/x-ms-application, image/jpeg, application/xaml+xml, [...]
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; [...]
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Host: facebook.com
Cookie: datr=1265876274-[...]; locale=en_US; lsd=WW[...]; c_user=2101[...]
GET
这个请求定义了要读取的URL: http://facebook.com/
。 浏览器自身定义 (User-Agent
头), 和它希望接受什么类型的相应 (Accept andAccept-Encoding
头). Connection
头要求服务器为了后边的请求不要关闭TCP
连接。
请求中也包含浏览器存储的该域名的cookies
。可能你已经知道,在不同页面请求当中,cookies
是与跟踪一个网站状态相匹配的键值。这样cookies
会 存储登录用户名,服务器分配的密码和一些用户设置等。Cookies
会以文本文档形式存储在客户机里,每次请求时发送给服务器。
除了获取请求,还有一种是发送请求,它常在提交表单用到。发送请求通过URL
传递其参数(e.g.
: http://robozzle.com/puzzle.aspx?id=85
)。发送 请求在请求正文头之后发送其参数。
像http://facebook.com/
中的斜杠是至关重要的。这种情况下,浏览器能安全的添加斜杠。而像http: //example.com/folderOrFile
这样的 地址,因为浏览器不清楚folderOrFile
到底是文件夹还是文件,所以不能自动添加 斜杠。这时,浏览器就不加斜杠直接访问地址,服务器会响应一个重 定向,结果造成一次不必要的握手。
4. facebook
的永久重定向相应
服务器给浏览器响应一个301
永久重定向响应,这样浏览器就会访问http://www.facebook.com/
而非http://facebook.com/
。
为什么服务器一定要重定向而不是直接发会用户想看的网页内容呢?这个问题有好多有意思的答案。
其中一个原因跟搜索引擎排名有 关。你看,如果一个页面有两个地址,就像http://www.igoro.com/
和http://igoro.com/
,搜索引擎会认为它们是 两个网站,结果造成每一个的搜索链接都减少从而降低排名。而搜索引擎知道301
永久重定向是 什么意思,这样就会把访问带www
的和不带www
的地址归 到同一个网站排名下。
还有一个是用不同的地址会造成缓存友好性变差。当一个页面有好几个名字时,它可能会在缓存里出现好几次。
5. 浏览器跟踪永久重定向地址
现在,浏览器知道了http://www.facebook.com/
才是要访问的正确地址,所以它会发送另一个获取请求
6. 服务器“处理请求”
服务器接收到获取请求,然后处理并返回一个响应。
这表面上看起来是一个顺向的任务,但其实这中间发生了很多有意思的东西- 就像简单的网站,何况像facebook
那样访问量大的网站呢!
Web 服务器软件
web
服务器软件(像IIS
和阿帕奇)接收到HTTP
请求,然后确定执行什么请求处理来处理它。请求处理就是一个能够读懂请求并且能生成HTML
来进行响应 的程序(像ASP.NET
,PHP
,RUBY
…)。
举个最简单的例子,需求处理可以以映射网站地址结构的文件层次存储。像http://example.com/folder1/page1.aspx
这个地址会映射/httpdocs/folder 1/page1.aspx
这个文件。web
服务器软件可设置成为地址人工的对应请求处理这样page1.aspx
的发布地址就可以是http://example.com/folder1/page1
。
请求处理
请求处理例程阅读请求及它的参数和cookies
。它会读取也可能更新一些数据,并讲数据存储在服务器上。然后,需求处理会生成一个HTML
响应。
所有动态网站都面临一个有意思的难点-如何存储数据。小网站一半都会有一个SQL
数据库来存储数据,存储大量数据和(或)访问量大的网站不得不找一些办 法把数据库分配到多台机器上。解决方案 有:sharding
(基于主键值讲数据表分散到多个数据库中),复制,利用弱语义一致性的简化数据库。
- 服务器发回
HTML
响应 - 浏览器显示
HTML
在浏览器没有完整接受全部HTML
文档时,它就已经开始显示这个页面了
HTML 解析
浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成8kB
大小的分块传输。
HTML
解析器的主要工作是对HTML
文档进行解析,生成解析树。
解析树是以DOM
元素以及属性为节点的树。DOM
是文档对象模型(Document Object Model
)的缩写,它是HTML
文档的对象表示,同时也是HTML
元素面向外部(如Javascript
)的接口。树的根部是Document
对象。整个DOM
和HTML
文档几乎是一对一的关系。
解析算法
HTML
不能使用常见的自顶向下或自底向上方法来进行分析。主要原因有以下几点:
- 语言本身的“宽容”特性
HTML
本身可能是残缺的,对于常见的残缺,浏览器需要有传统的容错机制来支持它们- 解析过程需要反复。对于其他语言来说,源码不会在解析过程中发生变化,但是对于
HTML
来说,动态代码,例如脚本元素中包含的document.write()
方法会在源码中添加内容,也就是说,解析过程实际上会改变输入的内容
由于不能使用常用的解析技术,浏览器创造了专门用于解析HTML
的解析器。解析算法在 HTML5
标准规范中有详细介绍,算法主要包含了两个阶段:标记化(tokenization
)和树的构建。
解析结束之后
浏览器开始加载网页的外部资源(CSS
,图像,Javascript
文件等)。
此时浏览器把文档标记为“可交互的”,浏览器开始解析处于“推迟”模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本。之后文档的状态会变为“完成”,浏览器会进行“加载”事件。
注意解析 HTML
网页时永远不会出现“语法错误”,浏览器会修复所有错误,然后继续解析。
执行同步 Javascript 代码。
9. 浏览器发送获取嵌入在HTML
中的对象
在浏览器显示HTML
时,它会注意到需要获取其他地址内容的标签。这时,浏览器会发送一个获取请求来重新获得这些文件。
下面是几个我们访问facebook.com
时需要重获取的几个URL
:
图片
http://static.ak.fbcdn.net/rsrc.php/z12E0/hash/8q2anwu7.gif
http://static.ak.fbcdn.net/rsrc.php/zBS5C/hash/7hwy7at6.gif
…
CSS
式样表
http://static.ak.fbcdn.net/rsrc.php/z448Z/hash/2plh8s4n.css
http://static.ak.fbcdn.net/rsrc.php/zANE1/hash/cvtutcee.css
…
CSS 解析
- 根据
CSS
词法和句法 分析CSS
文件和<style>
标签包含的内容 - 每个
CSS
文件都被解析成一个样式表对象,这个对象里包含了带有选择器的CSS
规则,和对应CSS
语法的对象 CSS
解析器可能是自顶向下的,也可能是使用解析器生成器生成的自底向上的解析器
JavaScript
文件
http://static.ak.fbcdn.net/rsrc.php/zEMOA/hash/c8yzb6ub.js
http://static.ak.fbcdn.net/rsrc.php/z6R9L/hash/cq2lgbs8.js
…
这些地址都要经历一个和HTML
读取类似的过程。所以浏览器会在DNS
中查找这些域名,发送请求,重定向等等…
但不像动态页面那样,静态文件会允许浏览器对其进行缓存。有的文件可能会不需要与服务器通讯,而从缓存中直接读取。服务器的响应中包含了静态文 件保存的期限信息,所以浏览器知道要把它们缓存多长时间。还有,每个响应都可能包含像版本号一样工作的ETag
头(被请求变量的实体值),如果浏览 器观察到文件的版本 ETag
信息已经存在,就马上停止这个文件的传输。
试着猜猜看fbcdn.net
在地址中代表什么?聪明的答案是”Facebook
内容分发网络”。Facebook
利用内容分发网络(CDN
)分发像图片,CSS
表和Java Script
文件这些静态文件。所以,这些文件会在全球很多CDN
的数据中心中留下备份。
举例来讲,当你试着ping static.ak.fbcdn.net
的时候,可能会从某个akamai.net
服务器上获得响应。有意思的是,当你同样再ping
一次的时候,响 应的服务器可能就不一样,这说明幕后的负载平衡开始起作用了
页面渲染
- 通过遍历
DOM
节点树创建一个Frame
树或“渲染树”,并计算每个节点的各个CSS
样式值 - 通过累加子节点的宽度,该节点的水平内边距(
padding
)、边框(border
)和外边距(margin
),自底向上的计算Frame 树
中每个节点首的选(preferred
)宽度 - 通过自顶向下的给每个节点的子节点分配可行宽度,计算每个节点的实际宽度
通过应用文字折行、累加子节点的高度和此节点的内边距(padding
)、边框(border
)和外边距(margin
),自底向上的计算每个节点的高度 - 使用上面的计算结果构建每个节点的坐标
- 当存在元素使用
floated
,位置有absolutely
或relatively
属性的时候,会有更多复杂的计算,详见http://dev.w3.org/csswg/css2/ 和 http://www.w3.org/Style/CSS/current-work - 创建
layer
(层)来表示页面中的哪些部分可以成组的被绘制,而不用被重新栅格化处理。每个帧对象都被分配给一个层 - 页面上的每个层都被分配了纹理(?)
- 每个层的帧对象都会被遍历,计算机执行绘图命令绘制各个层,此过程可能由
CPU
执行栅格化处理,或者直接通过D2D/SkiaGL
在GPU
上绘制 上面所有步骤都可能利用到最近一次页面渲染时计算出来的各个值,这样可以减少不少计算量
计算出各个层的最终位置,一组命令由Direct3D/OpenGL
发出,GPU
命令缓冲区清空,命令传至GPU
并异步渲染,帧被送到Window Server
。- 浏览器发送异步(
AJAX
)请求
- 浏览器发送异步(
在Web 2.0
伟大精神的指引下,页面显示完成后客户端仍与服务器端保持着联系。
以 Facebook
聊天功能为例,它会持续与服务器保持联系来及时更新你那些亮亮灰灰的好友状态。为了更新这些头像亮着的好友状态,在浏览器中执行 的JavaScript
代码会给服务器发送异步请求。这个异步请求发送给特定的地址,它是一个按照程式构造的获取或发送请求。还是在Facebook
这个例子中, 客户端发送给http://www.facebook.com/ajax/chat/buddy_list.php
一个发布请求来获取你好友里哪个在线的状态信息。
提起这个模式,就必须要讲讲AJAX
– “异步JavaScript
和 XML
”,虽然服务器为什么用XML
格式来进行响应也没有个一清二白的原因。
再举个例子吧,对于异步请求,Facebook
会返回一些JavaScript
的代码片段
除了其他,fiddler
这个工具能够让你看到浏览器发送的异步请求。事实上,你不仅可以被动的做为这些请求的看客,还能主动出击修改和重新发送它 们。AJAX
请求这么容易被蒙,可着实让那些计分的在线游戏开发者们郁闷的了。
Facebook
聊天功能提供了关于AJAX
一个有意思的问题案例:把数据从服务器端推送到客户端。因为HTTP
是一个请求-响应协议,所以聊天服务器不能把 新消息发给客户。取而代之的是客户端不得不隔几秒就轮询下服务器端看自己有没有新消息。
这些情况发生时长轮询是个减轻服务器负载挺有趣的技术。如果当被轮询时服务器没有新消息,它就不理这个客户端。而当尚未超时的情况下收到了 该客户的新消息,服务器就会找到未完成的请求,把新消息做为响应返回给客户端。
GPU 渲染
在渲染过程中,图形处理层可能使用通用用途的CPU
,也可能使用图形处理器GPU
当使用GPU
用于图形渲染时,图形驱动软件会把任务分成多个部分,这样可以充分利用GPU
强大的并行计算能力,用于在渲染过程中进行大量的浮点计算。
后期渲染与用户引发的处理
渲染结束后,浏览器根据某些时间机制运行JavaScript
代码(比如Google Doodle
动画)或与用户交互(在搜索栏输入关键字获得搜索建议)。类似Flash
和Java
的插件也会运行,尽管Google
主页里没有。这些脚本可以触发网络请求,也可能改变网页的内容和布局,产生又一轮渲染与绘制。