首先说明,本文很长,请泡一杯咖啡,抽出至少半个小时来慢慢回味。html
不少大公司面试喜欢问这样一道面试题,输入URL到看见页面发生了什么?
,今天咱们来总结一下。 简单来讲,共有如下几个过程前端
下面咱们来看看具体的细节node
DNS解析实际上就是寻找你所须要的资源的过程。假设你输入www.baidu.com
,而这个网址并非百度的真实地址,互联网中每一台机器都有惟一标识的IP地址,这个才是关键,可是它很差记,乱七八糟一串数字谁记得住啊,因此就须要一个网址和IP地址的转换,也就是DNS解析。下面看看具体的解析过程面试
DNS解析实际上是一个递归的过程 算法
www.google.com
网址后,首先在本地的域名服务器中查找,没找到去根域名服务器查找,没有再去
com
顶级域名服务器查找,,如此的类推下去,直到找到IP地址,而后把它记录在本地,供下次使用。大体过程就是
.
-> .com ->
google.com.
->
www.google.com.
。 (你可能以为我多写 .,并木有,这个.对应的就是根域名服务器,默认状况下全部的网址的最后一位都是.,既然是默认状况下,为了方便用户,一般都会省略,浏览器在请求DNS的时候会自动加上)
既然已经懂得了解析的具体过程,咱们能够看到上述一共通过了N个过程,每一个过程有必定的消耗和时间的等待,所以咱们得想办法解决一下这个问题!chrome
DNS存在着多级缓存,从离浏览器的距离排序的话,有如下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。数据库
在你的chrome浏览器中输入:chrome://dns/,你能够看到chrome浏览器的DNS缓存。json
系统缓存主要存在/etc/hosts(Linux系统)中数组
不知道大家有没有注意这样一件事,你访问baidu.com
的时候,每次响应的并不是是同一个服务器(IP地址不一样),通常大公司都有成百上千台服务器来支撑访问,假设只有一个服务器,那它的性能和存储量要多大才能支撑这样大量的访问呢?DNS能够返回一个合适的机器的IP给用户,例如能够根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡promise
TCP提供一种可靠的传输,这个过程涉及到三次握手,四次挥手,下面咱们详细看看 TCP提供一种面向链接的,可靠的字节流服务。 其首部的数据格式以下
源端口:源端口和IP地址的做用是标识报文的返回地址。
目的端口:端口指明接收方计算机上的应用程序接口。
TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP惟一肯定一条TCP链接。
序号:是TCP可靠传输的关键部分。序号是该报文段发送的数据组的第一个字节的序号。在TCP传送的流中,每个字节都有一个序号。好比一个报文段的序号为300,报文段数据部分共有100字节,则下一个报文段的序号为400。因此序号确保了TCP传输的有序性。
确认号:即ACK,指明下一个期待收到的字节序号,代表该序号以前的全部数据已经正确无误的收到。确认号只有当ACK标志为1时才有效。好比创建链接时,SYN报文的ACK标志位为0。
首部长度/数据偏移:占4位,它指出TCP报文的数据距离TCP报文段的起始处有多远。因为首部可能含有可选项内容,所以TCP报头的长度是不肯定的,报头不包含任何任选字段则长度为20字节,4位首部长度字段所能表示的最大值为1111,转化为10进制为15,15*32/8=60,故报头最大长度为60字节。首部长度也叫数据偏移,是由于首部长度实际上指示了数据区在报文段中的起始偏移值。
保留:占6位,保留从此使用,但目前应都位0。
控制位:URG ACK PSH RST SYN FIN,共6个,每个标志位表示一个控制功能。
紧急URG:当URG=1,代表紧急指针字段有效。告诉系统此报文段中有紧急数据
确认ACK:仅当ACK=1时,确认号字段才有效。TCP规定,在链接创建后全部报文的传输都必须把ACK置1。
推送PSH:当两个应用进程进行交互式通讯时,有时在一端的应用进程但愿在键入一个命令后当即就能收到对方的响应,这时候就将PSH=1。
复位RST:当RST=1,代表TCP链接中出现严重差错,必须释放链接,而后再从新创建链接。
同步SYN:在链接创建时用来同步序号。当SYN=1,ACK=0,代表是链接请求报文,若赞成链接,则响应报文中应该使SYN=1,ACK=1。
终止FIN:用来释放链接。当FIN=1,代表此报文的发送方的数据已经发送完毕,而且要求释放。
窗口:滑动窗口大小,用来告知发送端接受端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。窗口大小时一个16bit字段,于是窗口大小最大为65535。
校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。由发送端计算和存储,并由接收端进行验证。
紧急指针:只有当 URG 标志置 1 时紧急指针才有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。 TCP 的紧急方式是发送端向另外一端发送紧急数据的一种方式。
选项和填充:最多见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每一个链接方一般都在通讯的第一个报文段(为创建链接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。选项长度不必定是32位的整数倍,因此要加填充位,即在这个字段中加入额外的零,以保证TCP头是32的整数倍。
数据部分: TCP 报文段中的数据部分是可选的。在一个链接创建和一个链接终止时,双方交换的报文段仅有 TCP 首部。若是一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多状况中,也会发送不带任何数据的报文段。
须要注意的是: (A)不要将确认序号Ack与标志位中的ACK搞混了。 (B)确认方Ack=发起方Req+1,两端配对。
客户端发送syn包(Seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认;
服务器收到syn包,必须确认客户的SYN(ack=x+1),同时本身也发送一个SYN包(Seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程当中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP链接一旦创建,在通讯双方中的任何一方主动关闭链接以前,TCP 链接都将被一直保持下去。
创建链接的过程是利用客户服务器模式,假设主机A为客户端,主机B为服务器端。
采用三次握手是为了防止失效的链接请求报文段忽然又传送到主机B,于是产生错误。失效的链接请求报文段是指:主机A发出的链接请求没有收到主机B的确认,因而通过一段时间后,主机A又从新向主机B发送链接请求,且创建成功,顺序完成数据传输。考虑这样一种特殊状况,主机A第一次发送的链接请求并无丢失,而是由于网络节点致使延迟达到主机B,主机B觉得是主机A又发起的新链接,因而主机B赞成链接,并向主机A发回确认,可是此时主机A根本不会理会,主机B就一直在等待主机A发送数据,致使主机B的资源浪费。
采用两次握手不行,缘由就是上面说的失效的链接请求的特殊状况。而在三次握手中, client和server都有一个发syn和收ack的过程, 双方都是发后能收, 代表通讯则准备工做OK.
为何不是四次握手呢? 你们应该知道通讯中著名的蓝军红军约定, 这个例子说明, 通讯不可能100%可靠, 而上面的三次握手已经作好了通讯的准备工做, 再增长握手, 并不能显著提升可靠性, 并且也没有必要。
数据传输完毕后,双方均可释放链接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,假设客户端主动关闭,服务器被动关闭。
客户端发送一个FIN,用来关闭客户端到服务器的数据传送,也就是客户端告诉服务器:我已经不 会再给你发数据了(固然,在fin包以前发送出去的数据,若是没有收到对应的ack确认报文,客户端依然会重发这些数据),可是,此时客户端还可 以接受数据。 FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即便不携带数据,也要消耗一个序号。
服务器收到FIN包后,发送一个ACK给对方而且带上本身的序列号seq,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号)。此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,可是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送链接释放报文(在这以前还须要接受服务器发送的最后的数据)。
服务器发送一个FIN,用来关闭服务器到客户端的数据传送,也就是告诉客户端,个人数据也发送完了,不会再给你发数据了。因为在半关闭状态,服务器极可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP链接尚未释放,必须通过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,当即进入CLOSED状态。一样,撤销TCB后,就结束了此次的TCP链接。能够看到,服务器结束TCP链接的时间要比客户端早一些。
至此,完成四次挥手。
MSL(Maximum Segment Lifetime),TCP容许不一样的实现能够设置不一样的MSL值。
创建链接的时候, 服务器在LISTEN状态下,收到创建链接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。 而关闭链接时,服务器收到对方的FIN报文时,仅仅表示对方再也不发送数据了可是还能接收数据,而本身也未必所有数据都发送给对方了,因此己方能够当即关闭,也能够发送一些数据给对方后,再发送FIN报文给对方来表示赞成如今关闭链接,所以,己方ACK和FIN通常都会分开发送,从而致使多了一次。
首先科补一个小知识,HTTP的端口为80/8080,而HTTPS的端口为443
发送HTTP请求的过程就是构建HTTP请求报文并经过TCP协议中发送到服务器指定端口 请求报文由请求行,请求抱头,请求正文组成。
请求行的格式为Method Request-URL HTTP-Version CRLF
eg: GET index.html HTTP/1.1
经常使用的方法有: GET
,POST
, PUT
, DELETE
, OPTIONS
, HEAD
。
这里主要展现POST
和GET
的区别
常见的区别
注意一点你也能够在GET里面藏body,POST里面带参数
重点区别
GET
会产生一个TCP
数据包,而POST
会产生两个TCP
数据包。
详细的说就是:
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
注意一点,并非全部的浏览器都会发送两次数据包,Firefox就发送一次
请求报头容许客户端向服务器传递请求的附加信息和客户端自身的信息。
当使用POST, PUT等方法时,一般须要客户端向服务器传递数据。这些数据就储存在请求正文中。在请求包头中有一些与请求正文相关的信息,例如: 如今的Web应用一般采用Rest架构,请求的数据格式通常为json。这时就须要设置Content-Type: application/json
。
HTTP属于客户端缓存,咱们常认为浏览器有一个缓存数据库,用来保存一些静态文件,下面咱们分为如下几个方面来简单介绍HTTP缓存
缓存规则分为强制缓存和协商缓存
当缓存数据库中有客户端须要的数据,客户端直接将数据从其中拿出来使用(若是数据未失效),当缓存服务器没有须要的数据时,客户端才会向服务端请求。
又称对比缓存。客户端会先从缓存数据库拿到一个缓存的标识,而后向服务端验证标识是否失效,若是没有失效服务端会返回304,这样客户端能够直接去缓存数据库拿出数据,若是失效,服务端会返回新的数据
强制缓存的优先级高于协商缓存,若两种缓存皆存在,且强制缓存命中目标,则协商缓存再也不验证标识。
上面的内容让咱们大概了解了缓存机制是怎样运行的,可是,服务器是如何判断缓存是否失效呢?咱们知道浏览器和服务器进行交互的时候会发送一些请求数据和响应数据,咱们称之为HTTP报文。报文中包含首部header和主体部分body。与缓存相关的规则信息就包含在header中。boby中的内容是HTTP请求真正要传输的部分。举个HTTP报文header部分的例子以下:
对于强制缓存,服务器响应的header中会用两个字段来代表——Expires和Cache-Control。
Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但因为服务端时间和客户端时间可能有偏差,这也将致使缓存命中的偏差,另外一方面,Expires是HTTP1.0的产物,故如今大多数使用Cache-Control替代。
Cache-Control有不少属性,不一样的属性表明的意义也不一样。
协商缓存须要进行对比判断是否可使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一块儿响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就能够直接使用缓存数据了。
对于协商缓存来讲,缓存标识咱们须要着重理解一下,下面咱们将着重介绍它的两种缓存方案。
Last-Modified:服务器在响应请求时,会告诉浏览器资源的最后修改时间。
if-Modified-Since
:浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中得到的最后修改时间。服务端收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,若是一致则返回304和响应报文头,浏览器只须要从缓存中获取信息便可。 从字面上看,就是说:从某个时间节点算起,是否文件被修改了
if-Unmodified-Since
:从字面上看, 就是说: 从某个时间点算起, 是否文件没有被修改
这两个的区别是一个是修改了才下载一个是没修改才下载。
Last-Modified 说好却也不是特别好,由于若是在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会由于Last-Modified时间匹配不上而返回了整个实体给客户端(即便客户端缓存里有个如出一辙的资源)。为了解决这个问题,HTTP1.1推出了Etag。
Etag:服务器响应请求时,经过此字段告诉浏览器当前资源在服务器生成的惟一标识(生成规则由服务器决定)
If-None-Match:再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现If-None-Match则与被请求资源的惟一标识进行对比。
可是实际应用中因为Etag的计算是使用算法来得出的,而算法会占用服务端计算的资源,全部服务端的资源都是宝贵的,因此就不多使用Etag了。
它会对TCP链接进行处理,对HTTP协议进行解析,并按照报文格式进一步封装成HTTP Request对象,供上层使用。这一部分工做通常是由Web服务器去进行,我使用过的Web服务器有Tomcat, Nginx和Apache等等 HTTP报文也分红三份,状态码 ,响应报头和响应报文
状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:
1xx:指示信息–表示请求已接收,继续处理。
2xx:成功–表示请求已被成功接收、理解、接受。
3xx:重定向–要完成请求必须进行更进一步的操做。
4xx:客户端错误–请求有语法错误或请求没法实现。
5xx:服务器端错误–服务器未能实现合法的请求。 平时遇到比较常见的状态码有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500
请求成功,一般服务器提供了须要的资源。
服务器成功处理了请求,但没有返回任何内容。
请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
服务器目前从不一样位置的网页响应请求,但请求者应继续使用原有位置来进行之后的请求。
自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
服务器不理解请求的语法。
请求要求身份验证。 对于须要登陆的网页,服务器可能返回此响应。
服务器拒绝请求。
服务器找不到请求的网页。
请求格式正确,可是因为含有语义错误,没法响应
服务器遇到错误,没法完成请求。
常见的响应报头字段有: Server, Connection...。
你从服务器请求的HTML,CSS,JS文件就放在这里面
Webkit
解析渲染页面的过程。
当Render Tree中部分或所有元素的尺寸、结构、或某些属性发生改变时,浏览器从新渲染部分或所有文档的过程称为回流。
会致使回流的操做:
一些经常使用且会致使回流的属性和方法:
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并从新绘制它,这个过程称为重绘。
JS的解析是由浏览器的JS引擎完成的。因为JavaScript是单进程运行,也就是说一个时间只能干一件事,干这件事情时其余事情都有排队,可是有些人物比较耗时(例如IO操做),因此将任务分为同步任务和异步任务,全部的同步任务放在主线程上执行,造成执行栈,而异步任务等待,当执行栈被清空时才去看看异步任务有没有东西要搞,有再提取到主线程执行,这样往复循环(冤冤相报什么时候了,阿弥陀佛),就造成了Event Loop事件循环,下面来看看大人物
先看一段代码
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('立刻执行for循环啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
复制代码
结果我想你们都应该知道。主要来介绍JavaScript的解析,至于Promise等下一节再说
JavaScript是一门单线程语言,尽管H5中提出了Web-Worker
,可以模拟实现多线程,但本质上仍是单线程,说它是多线程就是扯淡。
既然是单线程,每一个事件的执行就要有顺序,好比你去银行取钱,前面的人在进行,后面的就得等待,要是前面的人弄个一两个小时,估计后面的人都疯了,所以,浏览器的JS引擎处理JavaScript时分为同步任务和异步任务
js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。 估计看完这些你对事件循环有必定的了解,可是事实上咱们看对的没这么简单,一般咱们会看到Promise,setTimeout,process.nextTick(),这个时候你和我就懵逼。
除了同步任务和异步任务,咱们还分为宏任务和微任务,常见的有如下几种
script里面先执行,不过我喜欢把它拎出来,直接称其进入执行栈
),当主线程执行栈所有任务被清空后去微任务看看,若是有等待执行的任务,执行所有的微任务(其实将其回调函数推入执行栈来执行),再去宏任务找最早进入队列的任务执行,执行这个任务后再去主线程执行任务(例如执行```console.log("hello world")这种任务),执行栈被清空后再去微任务,这样往复循环(冤冤相报什么时候了)Tip:微任务会所有执行,而宏任务会一个一个来执行
下面来看一段代码
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
复制代码
咱们看看它的执行状况
promise
,遇到then
,将其分发到微任务console.log("console")
,直接输出console
then
setTimeout
,总体执行完毕。 具体的执行过程大体就是这样,可能我有疏忽的地方,还望指正。
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
复制代码
咱们来分析一下
console.log('1')
,直接输出setTimeout
,将其回调函数分发到宏任务事件队列,暂时标记为setTimeout1process.nextTick()
,将其回调函数分发到微任务事件队列,标记为process.nextTick1
(这个地方有点出入,我通常认为```process.nextTick()推入主线程执行栈栈底,做为执行栈最后一个任务执行)7
,then函数分发的微任务事件队列,标记为Promise1。1,7
,宏任务和微任务的事件队列 状况以下
process.nextTick1
,输出6
,接着执行Promise1
,输出8
。至此,第一轮循环已经结束,输出了
1,7,6,8
,接下来执行第二轮循环 ,先从宏任务的setTimeout1
开始
console.log('2')
,执行输出。process.nextTick()
,将其回调函数分发到微任务,标记为process.nextTick2
,又遇到 Promise
,当即执行,输出4
,将then函数推入微任务事件队列,标记为Promise2
2,4
,来看看事件队列
process.nextTick2
,输出3
,接着再来执行Promise2
,输出5
。 第二轮循环执行完毕。如今一共输出了1,7,6,8,2,4,3,5
setTimeout2
开始第三轮循环 ,先直接输出9
,遇到process.nextTick()
,将其回调函数分发到微任务事件队列,标记为process.nextTick3
,又遇到恶心的Promise,当即执行输出11
,将then函数分发到微任务,标记为Promise3。10,12
。 至此,所有任务执行完毕,输出顺序为1,7,6,8,2,4,3,5,9,11,10,12
.注意,这段代码执行结果可能与node等环境不一样而发生变化。
我想说的也说完了,不知道您懂了嘛
这篇文章由一个简单的问题扯出了不少前端工程师必学也是很重要的东西,可是因为我本人水平较低,不少地方都是一笔带过,甚至有些地方还有错误,望各位同仁指正批评。