Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说史上最详细的经典面试题 从输入URL到看到页面发生了什么?「终于解决」,希望能够帮助你!!!。
首先说明,本文很长,请泡一杯咖啡,抽出至少半个小时来慢慢回味。
很多大公司面试喜欢问这样一道面试题,输入URL到看见页面发生了什么?
,今天我们来总结一下。 简单来说,共有以下几个过程
下面我们来看看具体的细节
DNS解析实际上就是寻找你所需要的资源的过程。假设你输入www.baidu.com
,而这个网址并不是百度的真实地址,互联网中每一台机器都有唯一标识的IP地址,这个才是关键,但是它不好记,乱七八糟一串数字谁记得住啊,所以就需要一个网址和IP地址的转换,也就是DNS解析。下面看看具体的解析过程
DNS解析其实是一个递归的过程 输入www.google.com
网址后,首先在本地的域名服务器中查找,没找到去根域名服务器查找,没有再去com
顶级域名服务器查找,,如此的类推下去,直到找到IP地址,然后把它记录在本地,供下次使用。大致过程就是.
-> .com -> google.com.
-> www.google.com.
。 (你可能觉得我多写 .,并木有,这个.对应的就是根域名服务器,默认情况下所有的网址的最后一位都是.,既然是默认情况下,为了方便用户,通常都会省略,浏览器在请求DNS的时候会自动加上)
既然已经懂得了解析的具体过程,我们可以看到上述一共经过了N个过程,每个过程有一定的消耗和时间的等待,因此我们得想办法解决一下这个问题!
DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
在你的chrome浏览器中输入:chrome://dns/,你可以看到chrome浏览器的DNS缓存。
系统缓存主要存在/etc/hosts(Linux系统)中
不知道你们有没有注意这样一件事,你访问baidu.com
的时候,每次响应的并非是同一个服务器(IP地址不同),一般大公司都有成百上千台服务器来支撑访问,假设只有一个服务器,那它的性能和存储量要多大才能支撑这样大量的访问呢?DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡
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就发送一次
请求报头允许客户端向服务器传递请求的附加信息和客户端自身的信息。 从图中可以看出,请求报头中使用了Accept, Accept-Encoding, Accept-Language, Cache-Control, Connection, Cookie等字段。Accept用于指定客户端用于接受哪些类型的信息,Accept-Encoding与Accept类似,它用于指定接受的编码方式。Connection设置为Keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间。
当使用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');
resolve();
}).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等环境不同而发生变化。
我想说的也说完了,不知道您懂了嘛
这篇文章由一个简单的问题扯出了很多前端工程师必学也是很重要的东西,但是由于我本人水平较低,很多地方都是一笔带过,甚至有些地方还有错误,望各位同仁指正批评。