总体步骤
1、先查找强缓存,命中直接使用,没有命中往下进行
2、没有命中强缓存,浏览器发送请求,并根据请求头里边的参数判断是否命中协商缓存,命中直接使用,没有命中往下进行
3、没有拿到缓存则进行DNS域名解析
4、建立tcp连接,三次握手
5、发送http请求
6、服务器处理http请求,并返回http报文
7、浏览器解析和渲染页面
8、连接结束,四次挥手
步骤详解
1、查找强缓存
强缓存会根据请求头或响应头的两个参数,分别为Expirse和Cache-Control,来判断是否命中强缓存
Expirse
- 版本:HTTP/1.0
- 来源:存在于服务端返回的响应头中
- 语法:Expires: Wed, 22 Nov 2019 08:41:00 GMT
- 缺点:服务器的时间和浏览器的时间可能并不一致导致失效
Cache-Control
- 版本:HTTP/1.1
- 来源:响应头和请求头
- 语法:Cache-Control:max-age=3600
- 缺点:时间最终还是会失效
2、协商缓存,
当发现没有强缓存或者强缓存过期了,那么客户端就会去请求服务器,寻找协商缓存。协商缓存中有两个重要的参数,etag和last-modified,etag相当于一个唯一标识,last-modified标识文件的最后修改时间,精确到秒。
协商缓存请求头和响应头的名称不同,etag和last-modified都是响应头,对应的请求头名称为if-none-match和if-modified-since
缓存没有过期
发请求–>看资源是否过期–>过期–>请求服务器–>服务器对比资源是否真的过期–>没过期–>返回304状态码–>客户端用缓存的老资源。
缓存过期
发请求–>看资源是否过期–>过期–>请求服务器–>服务器对比资源是否真的过期–>过期–>返回200状态码–>客户端如第一次接收该资源一样,记下它的cache-control中的max-age、etag、last-modified等。
协商缓存步骤总结:
请求资源时,把用户本地该资源的 etag 同时带到服务端,服务端和最新资源做对比。
如果资源没更改,返回304,浏览器读取本地缓存。
如果资源有更改,返回200,返回最新的资源。
etag解决了几个last-modified比较难解决的问题:
有些特定的场合下,一些静态的文件,可能会被频繁的更新, 但是文件内容没有变化,这时候如果使用Last-modified,服务器端始终返回最新的内容给浏览器,而Etag是根据文件内容来的,如果内容没有变化的话,始终会让浏览器使用本地缓存的文件。所以,使使用ETag可以更好的避免一些不必要的服务器相应。
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新get;
某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),if-modified-since能检查到的粒度是秒级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
某些服务器不能精确的得到文件的最后修改时间
3、DNS域名解析
DNS会先去查找DNS的缓存,浏览器、操作系统、路由器都会缓存一些url对应的ip地址,这个过程会使用递归查询
缓存中没有了话,就会开始进行迭代查询,先查找本地DNS服务器,再查找根域名服务器,查找COM顶级域名服务器,最后直接查找当前网站的服务器
这个过程中,查询的服务器如果没有相应的ip,那么它是不会负责帮你往下查找,而是给你提供一个其他可能知道该域名ip的服务器地址,然后让你自己去查找
4、建立tcp连接,三次握手
第一次握手,客户端向服务端发送一个SYN报文,此时客户端处于SYN_SEND状态,报文段中SYN=1,seq=x,表示希望与服务器建立连接
第二次握手,服务端向客户端发送报文作为应答,报文段中SYN=1,ACK=1,ack=x+1,seq=y,此时服务端处于SYN_RCVD
第三次握手,客户端收到SYN报文之后,同样发送一个ACK表示已经收到了服务端的SYN报文,报文段中ACK=1,seq=x+1,ack=y+1,此时客户端处于ESTABLISHED状态,表示已经建立连接
在服务端接收到第三次握手的报文之后,状态也会成为ESTABLISHED,双方建立起了连接
为什么非得要三次握手,两次不行吗?
第一次握手的时候,客户端知道了自己发送功能正常
第二次握手,客户端知道了自己发送和接收功能都正常,也知道服务端发送和接收功能都正常,服务端知道了自己接收和发送的功能都正常,也知道客户端发送的功能正常,但是不知道客户端的接收功能是否正常
第三次握手,主要是为了让服务端知道客户端能够接收到服务端发送的消息
如果使用两次握手,假设说客户端发送请求连接的报文段,第一次请求因为某些原因在网络中形成了滞留,于是客户端重新发送了第二个请求报文段。第二个成功建立了连接并在传输数据之后进行了释放,客户端连接关闭,而这个时候第一次发送的请求到达了服务端,服务端接收到之后就发送第二次握手,而此时客户端已经关闭了连接,已经不再发送数据,所以服务端此时就处于等待状态,浪费资源
半连接队列和SYN攻击
第一次握手之后,服务端接收到客户端的SYN,就会处于SYN_RCVD状态,此时双方还没有完全建立连接,服务器会把这种状态放在一个队列里,这种队列称为半连接队列
全连接队列表示三次握手已经完成,此时的连接状态会放在全连接队列中
第一次握手之后,服务器会发送第二次握手的报文段,如果未收到客户确认包,服务器会进行报文段重传,继续等待如果没有回应则再次重传,如果重传次数超过最大次数则会将当前连接信息从半连接队列中删除,这里每次重传的等待时间一般不同,一般是指数增长,如1s,2s,4s,8s…
那么这样的状态伴随而来的攻击就是SYN攻击,黑客根据半连接队列的特性,通过在短时间内伪造大量不存在的ip地址向服务端不断发送SYN包,以此占用服务端的半连接队列,此时服务端则需要发送二次握手并等待客户端发送第三次握手,由于源地址不存在,服务端就会进入不断的重发和等待状态。最终导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
常见的防御 SYN 攻击的方法有如下几种:
- 缩短超时(SYN Timeout)时间
- 增加最大半连接数
- 过滤网关防护
- SYN cookies技术
5、发送http请求
http各种版本:
http0.9
- 只有get命令
- 响应类型仅超文本
- 没有header等描述数据的信息
- 服务器发送完毕,就关闭tcp连接
http1.0
- 增加了很多命令post head
- 增加了状态码(status code)、header、多字符集支持、多部分发送(multi-part type)、权限(authoration)、缓存(cache)、内容编码(content encoding)等
- 响应不再只限于超文本(头部的content-type提供了可以传输多种类型的能力,如媒体、样式、脚本、html)
- 不支持keepalive,每次tcp连接只能发送一个请求,当服务器响应后就会关闭本次连接,下次请求需要再次建立tcp连接
http1.1
- 持久连接,引入了connection:keep-alive,一个tcp连接可以允许多个http请求
- 支持的方法:get、head、post、put、delete、trace、options、patch
- 进行了性能优化和特性增强,分块传输、压缩/解压、内容缓存磋商、虚拟主机、更快的响应,以及通过增加缓存节省了更多的宽带
- 支持断点续传,新增了多个状态响应码
http2.0
- 所有数据以二进制的格式进行传输,方便且健壮
- 多工,在一个tcp连接里,客户端和服务端可以同时发送或响应多个请求而且不需要按照顺序一一对应,避免了“对头堵塞”
- 头部压缩和服务器推送提高了效率。
请求中很多字段都是重复的,因此引入了头部信息压缩机制,一方面使用gzip或compress压缩后发送,另一方面,客户端和服务端同时维护一张头信息表,生成字段的索引,发送时只发送索引号
服务器推送常见场景是客户端请求一个网页,这个网页里面包含很多静态资源。正常情况下,客户端必须收到网页后,解析HTML源码,发现有静态资源,再发出静态资源请求。其实,服务器可以预期到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发给客户端了。
http3.0
- QUIC“快速UDP互联网连接”(Quick UDP Internet Connections)
HTTP3 的主要改进在传输层上。传输层不会再有我前面提到的那些繁重的 TCP 连接了。现在,一切都会走 UDP
7、浏览器解析与渲染页面
浏览器渲染是自上而下进行渲染的,下载和渲染是同步进行的,如果遇到css文件进行同步css文件请求,遇到图片进行异步请求,遇到js则先进行挂起
后边依次会生成dom树,cssom树,render树,布局树和图层树
其中dom树和cssom树是对html和css通过转换为字节、字符、令牌、节点、对象模型的流程生成的
dom树和cssom树合并后会生成渲染树也就是render树(这一步后来chrome经过升级,其实已经没有了,布局树已经可以提代render树了)
通过遍历生成的dom树节点将它们添加到布局树中并计算相应节点的位置和大小来生成布局树
根据节点进行分层,计算每个图层的节点样式,为每个节点生成图形和位置,将每个节点绘制填充到图层位图中,生成图层树
图层上传到GPU,最终组合多个图层到页面上生成屏幕图像
8、连接结束,四次挥手
终止连接需要四次挥手,这个是由TCP的半关闭造成的,就是TCP提供了连接一段在结束它的发送后还能接收另一端数据的能力
刚开始双方都处于established状态,假如是客户端先发起关闭请求
第一次挥手:客户端发送一个FIN报文,报文段中FIN=1,seq=u,此时客户端处于FIN_WAIT1状态
第二次挥手:服务端收到客户端请求关闭的报文,进行回应表明确认收到,报文段中ACK=1,seq=v,ack=u+1,此时服务端处于CLOSE_WAIT状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放,客户端收到服务端第二次挥手发来的确认信息之后进入FIN_WAIT2状态
第三次挥手:服务端没有什么可发送的了,想要进行断开,发送报文段FIN=1,ACK=1,seq=w,ack=u+1,此时服务端处于LAST_ACK状态,等待客户端的确认
第四次挥手:客户端收到了服务端发送的FIN,发送一个ACK表示已经收到,此时客户端进入TIME_WAIT状态,发送报文段ACK=1,seq=u+1,ack=w+1。
发送完之后,需要等待一段时间来确保服务端收到了自己的ACK报文,之后才会进入CLOSED状态,而服务端收到了最后的挥手之后,就关闭了连接,进入CLOSED状态。
为什么需要四次挥手
这个四次主要是在于服务端先发送了一个ACK又发送了一个FIN,其实这两步是可以同一次发过去,但是服务端在收到客户端发过来的FIN之后,处于半关闭状态,可能不会立即关闭SOCKET,也有可能有些数据还没有发送完毕,所以只能先发送一个ACK表示已经收到了客户端的关闭请求,等服务端所有数据都发送完了,最后再发送FIN
2MSL以及其意义
在第四次挥手之后,服务端进入了TIME_WAIT状态,在这个状态中,服务端要等待2MSL,也就是2倍的报文段最大生存时间,MSL是Maximun Segment Lifetime的缩写。
之所以等待2MSL,是为了保证第四次挥手的报文段能够到达服务器,因为ACK有可能丢失,如果丢失那么服务端接收不到最后的报文段,就进不了最后的关闭状态,这时服务端会重发第三次挥手让客户端重传一次。
如果没有这个等待时间,客户端发送完第四次挥手就直接关闭了,这时如果出现报文段丢失,那么服务端重发第三次挥手就无法收到客户端的回应,造成服务端无法正常关闭的情况。
另外经过2MSL的等待之后,可以让本次连接的所有报文段在网络中消失,使下一个新的连接中不会出现旧连接的请求报文段。