网络
# 1. 计算机网络的体系结构
- 4 层协议:应用层->运输层->网际层->网络接口层
- 5 层协议:应用层(FTP、SMTP)-> 运输层(TCP、UDP)-> 网络层(IP)-> 数据链路层 -> 物理层
- 应用层:应用层通过应用进程间的交互来完成特定的网络应用。对于不同的网络应用需要有不同的应用层协议。如域名系统 DNS,支持万维网应用的 http 协议,支持电子邮件的 SMTP 协议,应用层之间的交互数据单元为报文。
- 运输层:负责向两台主机中进程之间的通信提供通用的数据传输协服务,主要使用以下两种协议;传输控制协议 TCP:面向连接的、可靠的数据传输服务,单位为报文段。用户数据报协议 UDP:无连接的、尽最大努力的数据传输服务,不保证可靠性,单位为用户数据报。传输层的作用是指出具体该把数据包发给哪个应用,通过端口来分辨应用。同一个端口不会同时出现,传输层通过辨认端口号来确认应用。但是只靠端口号识别通信是不够的。需要采取五个信息来识别一个通信,分别是源 IP 地址,目标 IP 地址,协议号,源端口号,目标端口号。两个包中只要任何一个信息不同就不是同一个通信。
- 网络层:为分组交换网上的不同主机提供通信服务。在发送数据时。网络层把运输层产生的报文段或用户数据报封装成分组或包进行传送。在 tcp/ip 体系中,使用 ip 协议,因此分组也叫做 IP 数据报,简称数据报。由于互联网是由大量异构网络通过路由器相互连接起来的,因此互联网使用的网络层协议是无连接的网际协议 IP 和多种路由选择协议。
- 数据链路层:将网络层传下来的 ip 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制等)控制信息可以在链路层阶段发现收到的帧是否有差错,如果有则在该阶段丢弃这个出错的帧,避免继续在网络中传送下去白白浪费网络资源。
- 物理层:利用物理媒体传输,单位为比特。
# 2. TCP/UDP/IP 协议
协议是控制两个对等实体(或多个实体)进行通信的规则的集合。通信的真正端点并不是主机而是主机中的进程。两个计算机进程想要相互通信,不仅需要知道对方的 IP 地址,而且要知道对方的端口号,从而找到相应计算机的应用进程,熟知的端口号有 http80,https443,dns53
# UDP:用户数据报协议
- UDP 是无连接的,发送数据之前、结束后不需要建立、释放连接,较少了开销和发送数据之前的时延。
- UDP 是尽最大努力交付,即不保证可靠交付。因此主机不需要维持复杂的连接状态表。
- UDP 是面向报文的。应用层交下来的报文,UDP 既不合并也不拆分,而是保留这些报文的边界。UDP 为报文增加首部后就向下交付 IP 层。也就是说,UDP 一次交付一个完整的报文。所以若报文过长或者过短都会降低 IP 层的效率。
- UDP 没有拥塞控制,网络上出现的拥塞不会使源主机发送效率降低。
- UDP 支持一对一,一对多,多对一,多对多的交互通信。
- UDP 首部开销小,只有 8 个字节,而 TCP 有 20 个字节。
- UDP 的首部格式: 由四个字段组成,每个字段的长度都是两个字节。
- 源端口:源端口号,需要对方回信时选用,不需要时可全 0
- 目的端口:目的端口号,在终点交付报文时必须使用。
- 长度:UDP 用户数据报的长度,最短为 8 字节(仅有头部)
- 检验和:检验 UDP 用户数据报在传输过程中是否有错。有错就丢弃
# TCP 传输控制协议
- TCP 是面向连接的运输层协议。在使用 TCP 协议之前,必须先建立连接(3 次握手),结束之后必须释放已经建立的 TCP 链接(4 次分手)。
- 每一条 TCP 链接只能由两个端点,每一条 TCP 链接只能是点对点的。
- TCP 提供可靠交付的服务。通过 TCP 连接传送的数据,无差错,不丢失,不重复,并且按序到达。
- TCP 提供全双工通信。允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。在发送时,应用程序在把数据传送给 TCP 的缓存后,就可以做自己的事,而 TCP 在合适的时候把数据发送出去。在接收时,TCP 把收到的数据放入缓存,上层的应用进程在合适的时候读取缓存中的数据。
- 面向字节流。TCP 不保证接收方应用程序所受到的数据块和发送方的应用程序所发出的数据块具有对应大小的关系,但接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样。
# IP
- IP 地址 每块网卡需配置至少一个 IP 地址 IP 地址由 32 位正整数组成,为二进制,但是为了人类更好的阅读,将他每 8 位分为一组,共 4 组 IP 地址由网络和主机两标识组成 网络标识在数据链路的每个段配置不同的值,必须保证相互连接的端的地址不重复 主机标识不允许在同一网段内重复
IP 地址分为四个级别,分别为 A, B, C, D A 类地址是首位为 0 开头,前八位是网络标识, 0.0.0.0 ~ 127.0.0.0 属于 A 类 B 类地址是前两位由 10 组成,前 16 位是网络标识,128.0.0.0 ~ 191.255.0.0 属于 B 类 C 类地址前三位是 110, 前 24 位是网络标识,192.0.0.0 ~ 239.255.255.0 属于 B 类 D 类前四位是 1110,32 位全是网络标识,224.0.0.0 ~ 239.255.255.255 属于 D 类
但是以上的分类已经不用,改为使用子网掩码定位网络标识长度。 子网标识同一个网关,255.255.255.0 和 255.255.255.1 是同一个子网 子网掩码也是 32 位组成 掩码中有几个 1 就代码几位网络标识,其他为主机标识 假如掩码前 24 位为 1,就代表前 24 位都为网络标识,用 IP 地址标识就是 255.255.255.0,后面的 0 代表主机标识,理论上有 256 台主机可连接
- 路由控制 仅有 IP 地址还不足以将数据包发送到对端,还需指明路由器或主机。保存这种信息的就是路由控制表。 路由控制表中记录着地址与下一步要发送至路由器的地址。在发送 IP 包时,先确定 IP 包首部目标地址,然后在表中找到与该地址具有相同网络地址的记录,根据记录将 IP 包转发给相应的下一个路由器。
# 3.DNS 网域名称系统 (opens new window)
它实质上是一个 域名 和 IP 相互映射的分布式数据库,有了它,我们就可以通过域名更方便的访问互联网。 DNS 有以下特点:
- 分布式的
- 协议支持 TCP 和 UDP, 常用端口是 53
- 每一级域名的长度限制是 63
- 域名总长度限制是 253
# 4.HTTP 常见状态码
2 开头 (请求成功)表示成功处理了请求的状态代码
- 200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
- 201 (已创建) 请求成功并且服务器创建了新的资源。
- 202 (已接受) 服务器已接受请求,但尚未处理。
- 203 (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。
- 204 (无内容) 服务器成功处理了请求,但没有返回任何内容。
- 205 (重置内容) 服务器成功处理了请求,但没有返回任何内容。
- 206 (部分内容) 服务器成功处理了部分 GET 请求。
3 开头 (请求被重定向)表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。
- 300 (多种选择) 针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent)选择一项操作,或提供操作列表供请求者选择。
- 301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
- 302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
- 303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。
- 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
- 305 (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。
- 307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
4 开头 (请求错误)这些状态代码表示请求可能出错,妨碍了服务器的处理。
- 400 (错误请求) 服务器不理解请求的语法。
- 401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
- 403 (禁止) 服务器拒绝请求。
- 404 (未找到) 服务器找不到请求的网页。
- 405 (方法禁用) 禁用请求中指定的方法。
- 406 (不接受) 无法使用请求的内容特性响应请求的网页。
- 407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
- 408 (请求超时) 服务器等候请求时发生超时。
- 409 (冲突) 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。
- 410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。
- 411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。
- 412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。
- 413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
- 414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。
- 415 (不支持的媒体类型) 请求的格式不受请求页面的支持。
- 416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。
- 417 (未满足期望值) 服务器未满足"期望"请求标头字段的要求。
5 开头(服务器错误)这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。
- 500 (服务器内部错误) 服务器遇到错误,无法完成请求。
- 501 (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。
- 502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。
- 503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
- 504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。
- 505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。
# 5. GET 和 POST 的区别
# 6. HTTP1.0~3 的历程 (opens new window)
# 7. 从输入域名到渲染出页面都经历了什么
# 浏览器的多进程架构
在 chrome 中,一个页面 浏览器就会为其分配一个进程,每个页面的进程相对独立,单独页面的崩溃并不会影响到其他页面的正常运行。
- 浏览器进程:负责浏览器的用户交互、子进程管理和文件存储功能。比如 tab 的前进后退、处理一些不可见的底层操作如网络请求等。
- 网络进程: 网络进程是面向渲染进程和浏览器进程等提供网络下载功能。
- 渲染进程: 渲染进程的主要职责是把从网络下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。因为渲染进程所有的内容都是通过网络获取的,会存在一些恶意代码利用浏览器漏洞对系统进行攻击,所以运行在渲染进程里面的代码是不被信任的。这也是为什么 Chrome 会让渲染进程运行在安全沙箱里,就是为了保证系统的安全。
# 网页加载的过程
- 首先,用户从浏览器进程里输入请求信息;
- 然后,网络进程发起 URL 请求;
- 服务器响应 URL 请求之后,浏览器进程就又要开始准备渲染进程了;
- 渲染进程准备好之后,需要先向渲染进程提交页面数据,我们称之为提交文档阶段;
- 渲染进程接收完文档信息之后,便开始解析页面和加载子资源,完成页面的渲染。
# 用户输入
当用户在地址栏中输入查询的关键字时, 浏览器回去根据正则规则去判断它是一个网站链接还是一个搜索内容。同时会根据输入内容是否符合 url 协议去将输入的链接整合成一个完整的 url。
# url 请求过程
- 浏览器进程会通过进程间通信(IPC)把 URL 请求发送给网络进程,网络进程接收到请求后,会先去查找本地缓存,如果有本地缓存资源,则直接返回资源给浏览器进程。如果没有找到,那么直接 进入网络请求流程。第一步是进行 DNS 域名解析,将域名的所属的 IP 解析出来,如果一个域名已经解析过,那会把解析的结果缓存下来,下次处理直接走缓存,不需要经过 DNS 解析。如果是 HTTPS 协议,还需要建立 TLS 链接。
- 建立 TCP 连接,Chrome 在同一个域名下要求同时最多只能有 6 个 TCP 连接,超过 6 个的话剩下的请求就得等待。当通过 IP 地址和服务器建立了 TCP 连接之后,浏览器端会构建请求行、请求头和请求体,并把域名相对应的 cookie 等数据附加在请求头中,然后向服务器发送构建的请求信息。
- 服务器接收到请求信息后,根据请求信息生成响应数据(包括响应头、响应行、响应体等信息),并发给网络进程。网络进程接受到了响应行和 响应头后,开始解析响应头的内容。
- 重定向:在接收到服务器返回的响应头后,网络进程开始解析响应头。 如果发现返回的状态码是 301 或是 302,说明服务器需要浏览器重定向至其他的 URL,这时候网络进程会去响应头中拿到重定向的数据 Location,然后重新发起 HTTP 或者 HTTPS 请求,然后重新走 url 的请求过程。直到响应行的状态码是 200 的时候,则表示浏览器可以继续处理该请求。
- 响应数据类型处理:当处理了跳转信息之后,浏览器会根据响应头中的 Content-Type 来决定如何显示响应体的内容。若返回的是字节流类型的,浏览器一般会按照下载类型来处理该需求, 该请求会被提交给下载管理器,同时结束这次请求。如果是 HTML,那么浏览器则会继续进行导航流程。准备渲染进程。
- 准备渲染进程:默认情况下,chrome 会为每一个页面分配一个渲染进程,当然也有例外,如果多个页面都是相同域名下的子域名,或是不同的端口,都认为他们是同一站点。Chrome 的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 process-per-site-instance。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
- 提交文档:这里的“文档”是指 URL 请求的响应体数据。 “提交文档”的消息是由浏览器进程发出的,渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。 这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿才会更新页面。
到这里,一个完整的导航流程就“走”完了,这之后就要进入渲染阶段了。
- 渲染阶段:文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML 解析完成后顺序执行。 如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。 CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了。 一个完整的渲染流程大致可总结为如下:
- 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
相关概念
- 构建 DOM 树:因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
- 构建 CSSOM 树: 首先,将 css 转换为浏览器能够理解的结构(styleSheets)(其结构同时具备了查询和修改的功能),并将属性里所有的值转换为渲染引擎更容易理解的、标准化的计算值(1em = 32px),从而计算出 dom 树中每个节点的具体样式。生成一个完整的布局树。
- 布局: render tree。这个时候我们已经构建好了响应的 DOM、CSSOM styleSheets 树,接下来就需要计算出 DOM 树中可见元素的几何位置,这个计算的过程就叫做布局。Chrome 在布局阶段需要完成两个任务:创建布局树和布局计算。由于 DOM 树上还有许多例如 script、head 这样不需要展示的标签,也有使用了 display:none 的元素,所以在显示之前,我们需要额外的构建一棵只包含可见元素的布局树。
- 分层:当有了布局树之后,还要进行分层。因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。所以说浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。在形成了层叠上下文 (opens new window)和需要剪裁的地方创建起图层,一步步从布局树->图层树,随后渲染进程会对图层树中的每个图层生成绘制列表,再把它转交给渲染进程中的合成线程。合成线程会根据当前想要渲染的图层划分为图块,然后根据浏览器视口的位置优先使用视口附近的图块生成位图。实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块(一般大小为 256x256 或是 512x512)转换为位图。
- 栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。
- 合成。一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
- 重排(更新了元素的几何属性)如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
- 重绘(更新元素的绘制属性)如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
# CDN 与 DNS
浏览器访问了某个域名,首先会查找浏览器缓存、本地 hosts 文件、DNS 缓存,没有找到的话再去请求本地 DNS 服务器,由它负责完成域名的解析。
本地 DNS 会依次请求根域名服务器拿到对应的顶级域名服务器的地址,然后请求顶级域名服务器,拿到权威域名服务器的地址,之后权威域名服务器会返回最终的 IP 给本地 DNS 服务器,由它再返给浏览器。
比如说 baidu.com 这个域名,根域名是 .,顶级域名(也叫一级域名)是 com,而二级域名是 baidu.com,那会先向根域名服务器查找 com 的顶级域名服务器的地址,然后再向 com 的顶级域名服务器查找 baidu.com 的权威域名服务器的地址。
有的同学可能会问,那 image.baidu.com 或者 xx.yy.zz.baidu.com 呢?
二级域名和更多级的域名都在权威域名服务器解析,域名服务器只有三级。
因为域名服务器之所以这样分级是为了通过负载均衡来分散压力,具体的域名解析都是由各自的权威域名服务器来处理的,根域名和顶级域名服务器只是做了个转发。
三级就已经能达成目的了,更多级可以自己分,比如后面会讲的 CDN 服务就是自己做了更多级的负载均衡。
说到了 CDN,那 CDN 与 DNS 是啥关系呢?
CDN 不是一种协议,只是基于 DNS 协议实现的一种分布式网络。CDN 是基于 DNS 的,在权威域名服务器做了 CNAME 的转发,然后根据请求 IP 的所在地来返回就近区域的服务器的 IP。
# 8. 跨域攻击
- CSRF 跨站请求伪造。通过钓鱼网站,嵌入真实网站的请求。比如用户在进入钓鱼网站之前登陆了真实的银行等站点并把 COOKIE 保存了起来,这时候钓鱼网站把请求发出去的时候就会把 cookie 也一并带上,另一端以为是正常的请求,就执行。
# 9. 跨域
- jsonp
function sendJsonp(params) {
const { url, callback, timeout, data } = params;
let timer,
str = "";
sendJsonp.id = sendJsonp.id || 1;
let id = sendJsonp.id;
function cleanup() {
if (script.parentNode) {
script.parentNode.removeChild(script);
window[name] = null;
}
if (timer) {
clearTimeout(timer);
}
}
if (timeout) {
timer = setTimeout(() => {
callback("超时");
cleanup();
}, timeout);
}
let name = `cd_${id}`;
window[name] = (res) => {
if (window[name]) {
cleanup();
}
callback("成功", res);
};
for (const key in data) {
const value = data[key] !== undefind ? data[key] : "";
str += `${key}=${encodeURIComponent(value)}&`;
}
url = url + "?" + str + `callback=${name}`;
const script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
document.head.appendChild(script);
// id自增
sendJsonp.id++;
}
sendJsonp({
url: "http://abc.com/api/get",
data: {
name: "cd",
password: "cd真TM帅",
},
callback: (err, data) => {
console.log("callback;", err, data);
},
timeout: 10000, // 单位是毫秒
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- cors 跨域资源共享(Cross-origin resource sharing) (opens new window)在发生跨域请求之前,发送了一个 OPTIONS 请求去询问服务器是否允许接下来的跨域请求 如果是简单请求,则不会触发预检,直接发出正常请求。 OPTIONS 里有几个字段:
- Origin 发起请求原来的域
- Access-Control-Request-Method: 将要发起跨域的请求方式
- Access-Control-Request-Headers: 将要发起的跨域请求中包含的请求头字段
服务器在响应字段中来表明是否允许这个跨域请求,浏览器收到后检查如果不符合要求,就拒绝后面的请求。
- Access-Control-Allow-Origin:允许哪些域来访问(*为所有域名下的请求)
- Access-Control-Allow-Methods:允许哪些请求方式
- Access-Control-Allow-Headers: 允许那些请求头字段
- Access-Control-Allow-Credentials:它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可。
- Access-Control-Max-Age: 服务器返回两者可通讯的有效期,在有效期内不需要再调用 OPTIONS 请求询问。 对此,chrome 还做了优化: 如果是一个简单请求,那就直接发起请求,只需在请求中加入 Origin 字段表明自己来源,在响应中检查 Access-Control-Allow-Origin,如果不符合要求就报错,不需要再单独询问了。 简单请求就是请求方式属于 HEAD、GET、POST 三者之一,请求头只有下面这些,不符合要求的就是非简单请求,就得询问了”
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:(application/x-www-form-urlencoded、multipart/form-data、text/plain)
withCredentials 属性
上面说到,CORS 请求默认不发送 Cookie 和 HTTP 认证信息。如果要把 Cookie 发到服务器,一方面要服务器同意,指定 Access-Control-Allow-Credentials 字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在 AJAX 请求中打开 withCredentials 属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
2
否则,即使服务器同意发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。
但是,如果省略 withCredentials 设置,有的浏览器还是会一起发送 Cookie。这时,可以显式关闭 withCredentials。需要注意的是,如果要发送 Cookie,Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨源)原网页代码中的 document.cookie 也无法读取服务器域名下的 Cookie。
cors 简单跨域请求代码示例
var xhr = XMLHttpRequest();
xhr.withCredentials = true; // 设置withCredentials 为true获得的第三方cookies,将会依旧享受同源策略
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send("user=adsad");
xhr.onreadystatechenge = function() {
if (xhr.readyState == 4 && xhr.status === 200) {
alert(xhr.responseText);
}
};
2
3
4
5
6
7
8
9
cors 跨域的优点
- 支持所有类型的 http 请求,功能完善
- 通过 onerror 事件监听进行调用错误处理
- 通过 Access-Control-Allow-Origin 进行资源访问授权
cors 跨域缺点
- 主流(ie10)浏览器都支持 cors,但 ie8 和 ie9 需要使用 XDomainRequest 对象进行兼容。i7 以下不兼容
- 服务器代理 顾名思义在发送跨域请求时,后端进行代理中转请求到服务器端,然后获取数据返回给前端,适用以下场景:
- 针对 IE7 及以下浏览器摒弃 flash 插件的情况,配置代理接口与前端页面同源,并中转目标服务器接口,则 ajax 请求不存在跨域
- 外网前端页面无法访问内网接口,配置代理接口允许前端页面访问,并中转内网接口,则外网前端页面可以跨域访问内网接口
缺点就是后端需要一定的改造工作量
# 前端跨域通信解决方案
- document.domain + iframe 适用于主域相同,子域不同的前端通信跨域场景,比如 a.cd.top 和 b.cd.top 两者有着相同的主域 cd.top. a 嵌套 b,再通过 js 设置 document.domain 为主域 cd.top,则两个页面满足了同源策略,从而实现了跨域通信。
A页面
<iframe id="iframe" src="b.cd.top"></iframe>
<script>
document.domain = "cd.top";
var windowB = document.getElementById("iframe").contentWindow;
alert("B页面变量" + windowB.user);
</script>
B页面
<script>
document.domain = "cd.top";
var user = "CD";
</script>
2
3
4
5
6
7
8
9
10
11
12
13
优点:实现逻辑简单,不需额外的中转页面 缺点:仅适用于主域相同,子域不同的前端通信跨域场景。
- location.hash + iframe
- window.name+iframe
# 10. 浏览器的缓存机制
深入理解缓存机制 (opens new window) 深入理解缓存机制 2 (opens new window)
通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。
# 强缓存
不会向服务器发送请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且 Size 显示 from disk cache 或 from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。
Expires 缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和 Last-modified 结合使用。Expires 是 Web 服务器响应消息头字段,在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。 Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。Expires: Wed, 22 Oct 2018 08:41:00 GMT 表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。
Cache-Control 在 HTTP/1.1 中,Cache-Control 是最重要的规则,主要用于控制网页缓存。比如当 Cache-Control:max-age=300 时,则代表在这个请求正确返回时间(浏览器也会记录下来)的 5 分钟内再次加载资源,就会命中强缓存。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令:
public:所有内容都将被缓存(客户端和代理服务器都可缓存)。具体来说响应可被任何中间节点缓存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中间的 proxy 可以缓存资源,比如下次再请求同一资源 proxy1 直接把自己缓存的东西给 Browser 而不再向 proxy2 要。
private:所有内容只有客户端可以缓存,Cache-Control 的默认取值。具体来说,表示中间节点不允许缓存,对于 Browser <-- proxy1 <-- proxy2 <-- Server,proxy 会老老实实把 Server 返回的数据发送给 proxy1,自己不缓存任何数据。当下次 Browser 再次请求时 proxy 会做好请求转发而不是自作主张给自己缓存的数据。
no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control 的缓存控制方式做前置验证,而是使用 Etag 或者 Last-Modified 字段来控制缓存。需要注意的是,no-cache 这个名字有一点误导。设置了 no-cache 之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。
no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
max-age:max-age=xxx (xxx is numeric)表示缓存内容将在 xxx 秒后失效
s-maxage(单位为 s):同 max-age 作用一样,只在代理服务器中生效(比如 CDN 缓存)。比如当 s-maxage=60 时,在这 60 秒中,即使更新了 CDN 的内容,浏览器也不会进行请求。max-age 用于普通缓存,而 s-maxage 用于代理缓存。s-maxage 的优先级高于 max-age。如果存在 s-maxage,则会覆盖掉 max-age 和 Expires header。
max-stale:能容忍的最大过期时间。max-stale 指令标示了客户端愿意接收一个已经过期了的响应。如果指定了 max-stale 的值,则最大容忍时间为对应的秒数。如果没有指定,那么说明浏览器愿意接收任何 age 的响应(age 表示响应由源站生成或确认的时间与当前时间的差值)。
min-fresh:能够容忍的最小新鲜度。min-fresh 标示了客户端不愿意接受
在浏览器请求一个页面的数据之后,再次进入浏览器时,打开 network 面板中灰色的部分,采用的是强缓存,我们可以看到他的 size 有两种类型:memory cache 和 disk cache
- memory cache: 内存缓存,特点是高效性和时效性:当浏览器将文件内容编译解析之后存直接存到浏览器的内存中,需要的时候直接进行读取,十分高效。当这个页面的进程被关闭之后,内存缓存也将被清除,这是时效性。
- disk cache: 硬盘缓存,则是将浏览器读取的文件存入硬盘缓存中,每一次需要读取该文件的时候都需要从缓存中读取并重新编译这个文件,读取复杂且速度相对内存缓存来说慢。
在浏览器中,浏览器会在 js 和图片等文件解析之后直接存到内存缓存中,那么当页面刷新的时候只需要从内存缓存中读取,而 css 文件则会放入到硬盘文件中,所以每次刷新都要从硬盘读取缓存。
# 协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
- 协商缓存生效,返回 304 和 Not Modified 资源无更新,从缓存中取过去缓存的资源;
- 协商缓存失效,返回 200 和请求结果,重新将返回的结果和缓存标识存入浏览器缓存中;
于此同时,协商缓存的标识也是在响应报文的 HTTP 头中和请求结果一起返回给浏览器的。协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
# 缓存机制
强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match)其中 Etag / If-None-Match 的优先级比 Last-Modified / If-Modified-Since 高。协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。
# Last-Modified / If-Modified-Since
Last-Modified 是服务器响应请求时,返回该资源文件在服务器最后被修改的时间。
If-Modified-Since 则是客户端再次发起该请求时,携带上次请求返回的 Last-Modified 值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有 If-Modified-Since 字段,则会根据 If-Modified-Since 的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于 If-Modified-Since 的字段值,则重新返回资源,状态码为 200;否则则返回 304,代表资源无更新,可继续使用缓存文件,如下。
# Etag / If-None-Match
Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),如下。
If-None-Match 是客户端再次发起该请求时,携带上次请求返回的唯一标识 Etag 值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有 If-None-Match,则会根据 If-None-Match 的字段值与该资源在服务器的 Etag 值做对比,一致则返回 304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为 200,如下。
注:Etag / If-None-Match 优先级高于 Last-Modified / If-Modified-Since,同时存在则只有 Etag / If-None-Match 生效。
# 延申
网站的缓存设置一般是这样的:入口设置 no-cache 其他资源设置 max-age,这样入口文件会缓存但是每次都协商,保证能及时更新,而其他资源不发请求,减轻服务端压力。
注意,入口 html 文件是绝对不能强缓存的,不然就更新不了了。
当我们对页面进行强刷,或者设置Disabled cache的时候,其实谷歌浏览器内部帮我将请求投的cache-control设置为了no-cache,也就是和服务端协商决定用本地的缓存还是下载新的,从而实现了强刷。
# 11. 说一说浏览器的本地存储
# cookie
cookie 的诞生本身是为了弥补 http 在状态管理上的不足。HTTP 协议是一个无状态协议,客户端向服务器发请求,服务器返回响应,故事就这样结束了,但是下次发请求如何让服务端知道客户端是谁呢?这种情况下,就有了 cookie Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储。当在同一个域名下发送请求时,都会携带相同的 cookie,服务器可以根据拿到的 cookie 进行解析,从而拿到客户端的状态。可以说 cookie 的作用是用来做状态存储的。cookie 设置时可以为其设置过期时间。 缺点:
- 容量小,只有 4kb,只能用来存储少量的信息。
- 性能可能会有浪费。cookie 对应域名存储,不管域名下面的某一个地址是否需要这些 cookie,都会携带上完整的 cookie,这样随着请求数量的增多,其实会造成巨大的性能浪费的,因为携带和许多不必要的内容。
- 安全问题。由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,在 Cookie 的有效期内重新发送给服务器,这是相当危险的。另外,在 HttpOnly(是否可通过客户端脚本访问)为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。
cookie 分为两种类型:一种是 session cookies 一种是 Persistent cookies,如果 cookie 不包含到期时间,则视为绘画 cookie,存放于内存中,永远不会写入磁盘,当浏览器关闭后,会清除该会话 cookie。如果 cookie 有有效期,则视为持久性 cookie 存入磁盘,在指定的日期,才会从磁盘清楚。
cookie 的作用域:Domain 和 Path 标识了 cookie 的作用域:即 Cookie 应该发送哪些给 URL。Domain 标识了哪些主机可以接受 cookie,如果不指定,默认为当前主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。
# localStorage
localStorage 和 cookie 一样,针对在同一个域名下,会存储一段相同的 localStorage。 localStorage 的容量上限为 5M,并且为持久存储。且只存在于客户端,不参与服务器端的通信,避免了 cookie 那样带来的安全性能问题。 可以使用 localStorage.setItem getItem 等方法进行操作。
# sessionStorage
容量上限也为 5M,但是其为会话级别存储,页面关闭之后,这部分的 sessionStorage 就不存在了 只存在客户端,默认不参与与服务端的通信。 当服务器第一次接收到请求时,开辟了一块 session 空间(创建了 session 对象),同时生成一个 sessionID,并通过响应头的 Set-Cookie:aaa=xxxxxx 向客户端发送要求设置 Cookie 的响应。客户端收到响应后,在本机客户端设置了一个 aaa=xxxxxx 的 cookie 信息,该 cookie 的过期时间为浏览器会话结束。
session 的缺点:只针对同时的相同客户端和服务端,若该服务器做了负载均衡,当该服务器爆满时,就会把请求换到另外的服务器进行访问,但是另外的服务器并没有存储当前服务器的 session,就会导致 session 失效。
# IndexDB
IndexDB 是运行在浏览器中的非关系型数据库, 本质上是数据库,绝不是和刚才 WebStorage 的 5M 一个量级,理论上这个容量是没有上限的。
关于它的使用,本文侧重原理,而且 MDN 上的教程文档已经非常详尽,这里就不做赘述了,感兴趣可以看一下使用文档。
接着我们来分析一下 IndexDB 的一些重要特性,除了拥有数据库本身的特性,比如支持事务,存储二进制数据,还有这样一些特性需要格外注意:
- 键值对存储。内部采用对象仓库存放数据,在这个对象仓库中数据采用键值对的方式来存储。
- 异步操作。数据库的读写属于 I/O 操作, 浏览器中对异步 I/O 提供了支持。
- 受同源策略限制,即无法访问跨域的数据库。
# 12. 图片懒加载
方案一:clientHeight、scrollTop 和 offsetTop 首先给图片一个占位资源:
<img src="default.jpg" data-src="http://www.xxx.com/target.jpg" /></img>
接着,通过监听 scroll 事件来判断图片是否到达视口:
let img = document.document.getElementsByTagName("img");
let count = 0;//计数器,从第一张图片开始计
lazyload();//首次加载别忘了显示图片
window.addEventListener('scroll', lazyload);
function lazyload() {
let viewHeight = document.documentElement.clientHeight;//视口高度
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;//滚动条卷去的高度
for(let i = count; i <num; i++) {
// 元素现在已经出现在视口中
if(img[i].offsetTop < scrollHeight + viewHeight) {
if(img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count ++;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
当然,最好对 scroll 事件做节流处理,以免频繁触发:
// throttle 函数我们上节已经实现 window.addEventListener('scroll', throttle(lazyload, 200)); #方案二:getBoundingClientRect 现在我们用另外一种方式来判断图片是否出现在了当前视口, 即 DOM 元素的 getBoundingClientRect API。
上述的 lazyload 函数改成下面这样:
function lazyload() {
for (let i = count; i < num; i++) {
// 元素现在已经出现在视口中
if (
img[i].getBoundingClientRect().top < document.documentElement.clientHeight
) {
if (img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count++;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
# 方案三: IntersectionObserver
这是浏览器内置的一个 API,实现了监听 window 的 scroll 事件、判断是否在视口中以及节流三大功能。
我们来具体试一把:
let img = document.document.getElementsByTagName("img");
const observer = new IntersectionObserver((changes) => {
//changes 是被观察的元素集合
for (let i = 0, len = changes.length; i < len; i++) {
let change = changes[i];
// 通过这个属性判断是否在视口中
if (change.isIntersecting) {
const imgElement = change.target;
imgElement.src = imgElement.getAttribute("data-src");
observer.unobserve(imgElement);
}
}
});
observer.observe(img);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这样就很方便地实现了图片懒加载,当然这个 IntersectionObserver 也可以用作其他资源的预加载,功能非常强大。
# 13. 接口如何防刷
- 网关控制流量洪峰。
- 源 IP 请求个数限制。
- http 请求头信息检验。
- 对用户唯一身份 uid 进行限制和校验。例如基本的长度,组合方式,甚至有效性进行判断。或者 uid 具有一定的时效性。
- 前后端协议采用二进制方式进行交互或者协议采用签名机制。
- 人机验证,验证码,短信验证码,滑动图片形式,12306 形式
# 14. HTTPS 握手过程中,客户端如何验证证书的合法性
- 首先什么是 HTTP 协议? http 协议是超文本传输协议,位于 tcp/ip 四层模型中的应用层;通过请求/响应的方式在客户端和服务器之间进行通信;但是缺少安全性,http 协议信息传输是通过明文的方式传输,不做任何加密,相当于在网络上裸奔;容易被中间人恶意篡改,这种行为叫做中间人攻击;
- 加密通信: 为了安全性,双方可以使用对称加密的方式 key 进行信息交流,但是这种方式对称加密秘钥也会被拦截,也不够安全,进而还是存在被中间人攻击风险; 于是人们又想出来另外一种方式,使用非对称加密的方式;使用公钥/私钥加解密;通信方 A 发起通信并携带自己的公钥,接收方 B 通过公钥来加密对称秘钥;然后发送给发起方 A;A 通过私钥解密;双发接下来通过对称秘钥来进行加密通信;但是这种方式还是会存在一种安全性;中间人虽然不知道发起方 A 的私钥,但是可以做到偷天换日,将拦截发起方的公钥 key;并将自己生成的一对公/私钥的公钥发送给 B;接收方 B 并不知道公钥已经被偷偷换过;按照之前的流程,B 通过公钥加密自己生成的对称加密秘钥 key2;发送给 A; 这次通信再次被中间人拦截,尽管后面的通信,两者还是用 key2 通信,但是中间人已经掌握了 Key2;可以进行轻松的加解密;还是存在被中间人攻击风险;
- 解决困境:权威的证书颁发机构 CA 来解决; 3.1 制作证书:作为服务端的 A,首先把自己的公钥 key1 发给证书颁发机构,向证书颁发机构进行申请证书;证书颁发机构有一套自己的公私钥,CA 通过自己的私钥来加密 key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样使用机构的私钥进行加密;制作完成后,机构将证书发给 A; 3.2 校验证书真伪:当 B 向服务端 A 发起请求通信的时候,A 不再直接返回自己的公钥,而是返回一个证书; 说明:各大浏览器和操作系统已经维护了所有的权威证书机构的名称和公钥。B 只需要知道是哪个权威机构发的证书,使用对应的机构公钥,就可以解密出证书签名;接下来,B 使用同样的规则,生成自己的证书签名,如果两个签名是一致的,说明证书是有效的; 签名验证成功后,B 就可以再次利用机构的公钥,解密出 A 的公钥 key1;接下来的操作,就是和之前一样的流程了; 3.3:中间人是否会拦截发送假证书到 B 呢? 因为证书的签名是由服务器端网址等信息生成的,并且通过第三方机构的私钥加密中间人无法篡改; 所以最关键的问题是证书签名的真伪;
- https 主要的思想是在 http 基础上增加了 ssl 安全层,即以上认证过程。
# 15. 一个 tcp 连接能发送几个 http 请求
- 如果是 http1.0 版本协议,一般情况下不支持长连接,因此在每次请求发送完毕之后,tcp 连接就会断开,因此一个 tcp 发送一个 http 请求,但是有一种情况可以将一条 tcp 连接保持在活跃状态,就是通过 Connection 和 Keep-Alive 首部,再请求头带上 Connection:Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过方法这个方法也有限制。
- 如果是 HTTP1.1 版本协议,支持了长连接,因此只需要 TCP 连接不断开,就可以一直发送 http 请求,持续不断,没有上限。
- 如果是 HTTP2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 http 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限的持续发送。
# 16. 浏览器内核
浏览器 | 内核 | js 引擎 | |
---|---|---|
Chrome | webkit\blink | v8 处理器 |
FireFox | Gecko | SpiderMonkey |
Safari | webkit | javascriptCore |
Edge | EdgeHTML | Chakra(for JavaScript) |
IE | Trident | JScript(ie3-8) |
Opera | Presto\blink | Linear A(4.0-6.1)/ Linear B(7.0-9.2)/ Futhark(9.5-10.2)/ Carakan(10.5-) |
# 17. 浏览器的组成部分主要是什么?
- 「用户界面」 - 包括地址栏、前进/后退按钮、书签菜单等。
- 「浏览器引擎」 - 在用户界面和呈现引擎之间传送指令。
- 「呈现引擎」 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- 「网络」 - 用于网络调用,比如 HTTP 请求。
- 「用户界面后端」 -用于绘制基本的窗口小部件,比如组合框和窗口。
- 「JavaScript 解释器」- 用于解析和执行 JavaScript 代码。
- 「数据存储」 - 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
值得注意的是,和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个呈现引擎实例。每个标签页都是一个独立的进程。
# 18. CSS 的加载是否会造成阻塞
答案是不。详情可以从上面输入 URL 到渲染可以得知: CSS 加载的快慢只会影响到 DOM 元素的渲染,即是上面说的生成 CSSOM 树、布局树的过程收到了限制。CSSOM 提供给 JS 操作样式表的能力,它会阻塞到 JS 的执行,JS 需要等到 HTML 样式的生成之后执行,但是并不会影响到 JS 文件的下载。 我们可以知道,DOM 和 CSSOM 通常是并行构建的,CSS 的加载不会影响到 DOM 的解析。然而布局树是依赖 DOM tree 和 CSSOM tree 的,所以它必须要等待两者都加载完毕之后,完成相应的构建,才开始渲染,因此 CSS 会阻塞 DOM 渲染。
# 19. 为什么 JS 会阻塞页面加载
原因和上面是一样的。js 阻塞 DOM 解析,会阻塞页面。JS 会影响到 render tree 的构建。如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码。
# 20. defer 和 async 的区别 ?
- 两者都是异步去加载外部 JS 文件,不会阻塞 DOM 解析
- Async 是在外部 JS 加载完成后,浏览器空闲时,Load(事件触发时,页面上所有的 DOM,样式表,脚本,图片等资源已经加载完毕)事件触发前执行,标记为 async 的脚本并不保证按照指定他们的先后顺序执行,该属性对于内联脚本无作用 (即没有「src」属性的脚本)。
- defer 是在 JS 加载完成后,整个文档解析完成后,触发 DOMContentLoaded(仅当 DOM 解析完成后,不包括样式表,图片等资源。) 事件前执行,如果缺少 src 属性(即内嵌脚本),该属性不应被使用,因为这种情况下它不起作用
这两者会发生的情况: 带 async:
带 async 的脚本一定会在 load 事件之前执行,可能会在 DOMContentLoaded 之前或之后执行。
- HTML 还没有被解析完的时候,async 脚本已经加载完了,那么 HTML 停止解析,去执行脚本,脚本执行完毕后触发 DOMContentLoaded 事件
- HTML 解析完了之后,async 脚本才加载完,然后再执行脚本,那么在 HTML 解析完毕、async 脚本还没加载完的时候就触发 DOMContentLoaded 事件
带 defer:
如果 script 标签中包含 defer,那么这一块脚本将不会影响 HTML 文档的解析,而是等到 HTML 解析完成后才会执行。而 DOMContentLoaded 只有在 defer 脚本执行结束后才会被触发。
- HTML 还没解析完成时,defer 脚本已经加载完毕,那么 defer 脚本将等待 HTML 解析完成后再执行。defer 脚本执行完毕后触发 DOMContentLoaded 事件
- HTML 解析完成时,defer 脚本还没加载完毕,那么 defer 脚本继续加载,加载完成后直接执行,执行完毕后触发 DOMContentLoaded 事件
# 21. 深入理解 HTTPS 原理、过程与实践 (opens new window)
# 安全的 HTTP 的需求
对 HTTP 的安全需求:
- 加密(客户端和服务器的对话是私密的,无须担心被窃听)
- 服务端认证(客户端知道它们是在与真正的而不是伪造的服务器通信)
- 客户端认证(服务器知道它们是在与真正的而不是伪造的客户端通信)
- 完整性(客户端和服务器的数据不会被修改)
- 效率(一个运行足够快的算法,一遍低端的客户端和服务器使用)
- 普适性(基本上所有的客户端和服务器都支持这些协议)
- 管理的可扩展性(在任何地方的任何人都可以立即进行安全通信)
- 适应性(能够支持当前最知名的安全方法)
- 在社会上的可行性(满足社会的政治文化需要),要有公众受信能力
在这里面最重要的是前面几条
- 数据加密 传输内容进行混淆
- 身份验证 通信双方验证对方的身份真实性
- 数据完整性保护 检测传输的内容是否被篡改或伪造
# 安全 HTTP 的实现
# 加密方式的选择
共享密钥加密 对称密钥加密 共享密钥加密方式使用相同的密钥进行加密解密,通信双方都需要接收对方的加密密钥进行数据解密,这种方式在通信过程中必须交互共享的密钥,同样无法避免被网络监听泄漏密钥的问题;同时对于众多客户端的服务器来说还需要分配和管理密钥,对于客户端来说也需要管理密钥,增加设计和实现的复杂度,同时也降低了通信的效率;不用看都不靠谱。
公开密钥加密 公开密钥加密方式使用一对非对称的密钥对(私钥和公钥),不公开的作为私钥,随意分发的作为公钥;公钥和私钥都能进行数据加密和解密,公钥能解密私钥加密的数据,私钥也能解密公钥加密的数据;这样只需要一套密钥就能处理服务端和众多客户端直接的通信被网络监听泄漏密钥的问题,同时没有额外的管理成本;看起来挺合适。虽然公开密钥加密安全性高,但伴随着加密方式复杂,会产生处理速度慢的问题。如果我们的通信都是用公开密钥的方式加密,那么通信效率会很低。
HTTPS 采用共享密钥加密和公开密钥加密混合的加密方式,在交换密钥对环节使用公开密钥加密方式(防止被监听泄漏密钥)加密共享的密钥,在随后的通信过程中使用共享密钥的方式使用共享的密钥进行解密。
# 认证方式实现
数字证书 数字签名是附加在报文上的特殊加密校验码,可以证明是作者编写了这条报文,前提是作者才会有私钥,才能算出这些校验码。如果传输的报文被篡改,则校验码不会匹配,因为校验码只有作者保存的私钥才能产生,所以前面可以保证报文的完整性。
数字证书认证机构(Certificate Authority CA)是客户端和服务器双方都可信赖的第三方机构。
服务器的运营人员向数字证书认证机构提出证书认证申请,数字证书认证机构在判明申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公钥证书(也叫数字证书或证书)后绑定在一起。服务器将这份有数字认证机构颁发的公钥证书发送给客户端,以进行公开密钥加密方式通信。
EV SSL(Extended Validation SSL Certificate)证书是基于国际标准的认证指导方针办法的证书,通过认证的 Web 网站能获得更高的认可度。持有 EV SSL 证书的 Web 网站的浏览器地址栏的背景色是绿色的,同时在地址栏的左侧显示了 SSL 证书中记录的组织名称及办法证书的认证机构的名称。
使用 OpenSSL,每个人都可以构建一套认证机构文件,同时可以用来给自己的证书请求进行签名,这种方式产生的证书称为自签名证书,这种证书通常是 CA 自己的证书,用户开发测试的证书,也可以像 12306 这样的,信不信由你。
证书信任的方式
操作系统和浏览器内置 每个操作系统和大多数浏览器都会内置一个知名证书颁发机构的名单。因此,你也会信任操作系统及浏览器提供商提供和维护的可信任机构。 受信认证机构(也有不受信的,比如赛门铁克,沃通,或者像 2011 年被入侵的 DigiNotar 等)的证书一般会被操作系统或者浏览器在发行或者发布时内置。
证书颁发机构 CA( Certificate Authority,证书颁发机构)是被证书接受者(拥有者)和依赖证书的一方共同信任的第三方。
手动指定证书 所有浏览器和操作系统都提供了一种手工导入信任证书的机制。至于如何获得证书和验证完整性则完全由你自己来定。
PKI(Public Key Infrastructure),即公开密钥基础设施,是国际上解决开放式互联网络信息安全需求的一套体系。PKI 支持身份认证,信息传输,存储的完整性,消息传输,存储的机密性以及操作的不可否认性。
# 数据完整性
数字签名是只有信息发送者才能产生的别人无法伪造的一段文本,这段文本是对信息发送者发送信息真实性的一个有效证明,具有不可抵赖性。
报文的发送方从报文文本生成一个 128 位的散列值(或称为报文摘要活哈希值),发送方使用自己的私钥对这个摘要值进行加密来形成发送方的数字签名。然后这个数字签名将作为报文的附件一起发送给报文的接收方。报文的接收方首先从接收到的原始报文中计算出 128 位的散列值,再用发送方的公钥来对报文附加的数字签名进行解密。如果两次得到的结果是一致的那么接收方可以确认该数字签名是发送方的,同时确认信息是真实的 。
# HTTPS 数据交互过程
HTTP 中没有加密机制,可以通过 SSL(Secure Socket Layer 安全套接层)或 TLS(Transport Layer Security 安全层传输协议)的组合使用,加密 HTTP 的通信内容。
HTTPS 是 HTTP Secure 或 HTTP over SSL。
SSL(Security Socket Layer)是最初由网景公司(NetScape)为了保障网上交易安全而开发的协议,该协议通过加密来保护客户个人资料,通过认证和完整性检查来确保交易安全。网景公司开发过 SSL3.0 之前的版本;目前主导权已转移给 IETF(Internet Engineering Task Force),IETF 以 SSL3.0 为原型,标准化并制定了 TSL1.0,TLS1.1,TLS1.2。但目前主流的还是 SSL3.0 和 TSL1.0。
SSL 工作在 OSI 七层模型中的表示层,TCP/IP 四层模型的应用层。
SSL 和 TLS 可以作为基础协议的一部分(对应用透明),也可以嵌入在特定的软件包中(比如 Web 服务器中的实现)。
SSL 基于 TCP,SSL 不是简单地单个协议,而是两层协议;SSL 记录协议(SSL Record Protocol)为多种高层协议(SSL 握手协议,SSL 修改密码参数协议,SSL 报警协议)提供基本的安全服务。HTTP 是为 Web 客户端/服务器交互提供传输服务的,它可以在 SSL 的顶层运行;SSL 记录协议为 SSL 链接提供两种服务,机密性:握手协议定义了一个共享密钥,用于 SSL 载荷的对称加密。 消息完整性:握手协议还定义了一个共享密钥,它用来产生一个消息认证码(Message Authentication Code,MAC)。
# SSL 记录协议操作
- 分段 将每个上层消息分解成不大于 2^14(16384)位,然后有选择的进行压缩
- 添加 MAC 在压缩数据的基础上计算 MAC
- 加密 消息加上 MAC 用对称加密方法加密
- 添加 SSL 记录头 内容类型(8 位),主版本(8 位),副版本(8 位),压缩长度(16 位)
# SSL 握手过程
- 第一阶段 建立安全能力 包括协议版本 会话 Id 密码构件 压缩方法和初始随机数
- 第二阶段 服务器发送证书 密钥交换数据和证书请求,最后发送请求-相应阶段的结束信号
- 第三阶段 如果有证书请求客户端发送此证书 之后客户端发送密钥交换数据 也可以发送证书验证消息
- 第四阶段 变更密码构件和结束握手协议
SSL 协议两个重要概念,SSL 会话,SSL 连接;SSL 连接是点到点的连接,而且每个连接都是瞬态的,每一个链接都与一个会话关联。SSL 会话是一个客户端和一个服务器之间的一种关联,会话由握手协议(Handshake Protocol)创建,所有会话都定义了一组密码安全参数,这些安全参数可以在多个连接之间共享,会话可以用来避免每一个链接需要进行的代价高昂的新的安全参数协商过程。