HTTP详解

158 阅读14分钟

前言

http/https协议是前端最常用的应用层协议,没有之一。不过我通常并不会去非常详尽的去了解一个http协议是如何从浏览器发出,并经历了哪些阶段,并最终获取到服务器端的内容。其实如果不是为了面试,详细了解有个锤子用,面试造火箭,干活拧螺丝,现状如此也是没有办法的事。。。

http流程概览

graph TD
发起http ---> 构建请求 ---> 检查缓存(无强缓存) ---> DNS解析 ---> TCP传输层3次握手建立客户端与服务器端连接通道 ---> IP网络层进行数据分包与发包 ---> 服务器端接受数据包并组装 ---> 服务器处理请求 ---> 服务器返回响应数据 ---> IP数据分包发包 ---> TCP传输数据与组装 ---> 断开http连接

发起http

通常发起http的方式分为下面几类:

  1. 浏览器地址栏写入httpurl(统一资源定位符)发起GET请求
  2. 解析html中遇到的link(加载css)、script(加载js)、img(加载图片)、video(加载视频)、audio(加载音频)等等标签,发起GET请求。
  3. js主动发起ajax请求,发起GET、POST、HEAD、PUT、DELETE、OPTION等等请求。(浏览器专门 有网络线程用于处理异步http请求)

构建http请求

首先,浏览器构建请求行信息(如下所示),构建好后,浏览器由浏览器的网络进程准备发起网络请求。以一个html请求为例:

GET /index.html HTTP1.1

检查缓存

浏览器在发起一个http请求之前会首先检查缓存,缓存分为强缓存协商缓存

强缓存

Expires: http1.0规定了Expires字段来规定缓存逻辑。
字段由服务器在设置响应头时设定,采用的也是服务器时间。浏览器再次发起此url请求时候会采用本地的时间来比对服务器设定的Expires时间,本地时间超过此时间则强缓存失效,本地时间未超过此时间,则直接使用本地缓存内容,不会向服务器发起http请求。

Cache-Control: http1.1规定了cache-control字段来规定缓存逻辑。
此字段在请求和响应头中都有使用。但是缓存主要是用于浏览器使用判断,因此此字段由服务器的响应头中来设定,用于浏览器来进行缓存判断。字段值主要分为以下几类:

  • max-age: 单位是秒,示例:max-age=1000;缓存的计算逻辑是计算上次请求发起的时间距离当前的时间间隔是否超过max-age设定的值,超过则判定缓存失效,未超过则直接使用缓存内容,不会向服务器发起http请求。
  • public: 响应头设定此值标识代理服务器、CDN服务器可缓存响应内容,下次请求的时候经过代理服务器/CDN服务器时候,由代理服务器/CDN服务器来判定是否直接返回缓存内容。
  • private: 响应头设定此值,标识代理服务器、CDN服务器不可缓存内容。
  • no-cache: 不缓存,响应头设置此值则标志浏览器再次请求时候必须向服务器询问,由浏览器返回结果告知浏览器是否可使用本地缓存。
  • no-store: 不缓存,响应头设置此值标志浏览器直接不可把响应内容缓存到本地,因此本地不会缓存响应内容,因此理论安琪必须向服务器拉取服务器响应的内容。

Pragma:http1.0中规定,值仅有一个为no-cache,效果其实与cache-control中的no-cache是一致的,即为不使用强缓存,要求请求必须询问服务器,由服务器来告知浏览器是否可使用本地缓存。

优先级上Pragma > Cache-Control > Expires,三者/两者同时存在于响应头时候,优先使用Pragma,其次使用Cache-Control,再次使用Expires。
原因也很简单,Pragma是不让使用强缓存的,你就老实去问服务器该不该使用本地缓存,也是为了规避因为强缓存带来的内容无法更新问题,算是保守的方式。
Expires有很明显的缺点:服务器设定时间为服务器时间,但是本地缓存判定时候采用的逻辑是本地时间与设定的服务器时间比对,而本地时间与服务器的时间可能是不准确的,尤其是本地时间;因此缓存比对结果常常是不符合预期的。
cache-control的max-age也有缺点:单位采用的是秒,服务器内容修改恰好在1秒内完成,那这次的修改将永远不会生效,则不会把新内容更新本地缓存。

DNS解析

DNS解析的存在是因为每个网站的访问最终都会指向网站的服务器,每个服务器是有一个唯一的ip地址,但是ip地址以ipv4为例,是由4段每段3位的数字组成,这样的12位数字对于人们的记忆是有困难的,而一段有含义的英文单词或者中文语句来代表一个网站则对于人们记忆是很方便的,也很利于网站的推广,因此需要出现一个服务来管理真实服务器ip与利于记忆带含义的域名映射关系,这就是DNS服务器产生的原因与所负责的内容。

域名分层:在了解DNS查询域名ip之前我们先来了解下域名分层的情况

DNS查询分为两类:递归查询迭代查询

递归查询:客户端进行DNS查询,浏览器没有则转向本地DNS服务器,DNS的查询方始终为客户端。
迭代查询:客户端向本地DNS服务器查询不到结果时,便交给本地服务器去查询IP,本地服务器负责向根服务器、顶级服务器、权限服务器轮番查询,并最终查询到结果返回给客户端的方式为迭代查询。

DNS解析的流程主要分为以下过程(有顺序性):

  1. 检查浏览器DNS缓存是否有缓存,有则直接返回缓存的对应ip
  2. 检查系统host文件,host文件中配置了对应的域名与ip映射,则直接返回host配置域名的对应ip
  3. 向本地DNS服务器询问(递归查询),本地DNS有则直接返回对应IP。
  4. 本地服务器对应域名ip则向根服务器询问顶级域名所在位置。
  5. 本地DNS服务器向顶级域名服务器发起询问,询问权限服务器位置。
  6. 本地DNS服务器向权限服务器查询对应域名IP,有则返回给本地服务器IP。
  7. 本地获取到的IP缓存在本地DNS服务器中,本地服务器把IP返回给浏览器DNS,本地浏览器DNS服务器缓存此域名的IP。

DNS查询域名与ip映射过程采用的是UDP通信协议,流程中的DNS向浏览器DNS缓存、本地DNS缓存查询域名IP为递归查询;本地DNS服务器向根服务器、顶级服务器、权限服务器查询为迭代查询。

TCP建立浏览器与服务器的通信通道

TCP依据IP信息包装TCP请求信息

TCP会组装TCP的报文头部,包含源端口、目标端口、序列号(SYN)、确认号(ACK)等等。

TCP通过3次握手与服务器建立通信

  1. 浏览器端的TCP首先向服务器端发送SYN,表达的意思为我想与你建立连接,可以吗?
  2. 服务器端回复SYN+ACK,表达的意思为可以,你发送消息吧。
  3. 浏览器端收到服务器端的ACK后,再次向服务器发送ACK表达意思为我将要发送消息了,请准备好。 经过上面3次握手后,浏览器端正式与服务器端建立通信连接。

http1.0中的头部字段Connection默认为close,标志在建立通信连接并发送消息和响应后随即经过4次握手断开连接,下次如果还想发送消息需要重新3次握手建立连接。
http1.1默认头部字段Connection默认为keep-live;标志在建立通信连接并且浏览器与服务器请求响应后,继续保持TCP连接通道,后续的请求/响应可直接使用此TCP通道,无需进行3次握手重建连接,除非浏览器端/客户端主动关闭TCP连接,否则一直将会保持连接。
建立TCP通信后,浏览器端与服务器端会各自运行一个socket套接字,浏览器端请求,服务器端监听。

IP网络层数据发包

IP组装/发送数据包

  1. IP组装TCP数据与TCP包首部作为IP数据包的数据部分,并增加IP数据包首部,形成IP数据包。
  2. 参考路由控制表来决定接受不了IP数据包的路由/主机。
  3. 链路层由以太网驱动来处理IP传送来的IP数据包作为数据体,并解析mac地址并增加以太网首部由物理层传输给服务器端。

服务器端接受与组装数据包

  1. 服务器物理层网络接口接到浏览器的以太网数据包后解析出IP数据体、以太网首部。
  2. 以太网驱动判断以太网首部的mac地址,与服务器的mac地址匹配,如果不匹配则丢弃此消息,匹配则传递IP数据包给IP协议层。
  3. IP协议层解析IP数据包为ip数据体、IP首部。依据ip数据首部判断ip与当前服务器ip是否匹配,不匹配则丢弃此消息,匹配则传输到TCP层。
  4. TCP层解析TCP数据包为TCP数据体、TCP首部。TCP对数据进行校验验证数据是否损害、是否完整,然后检查是否按照序号接受数据,最后检查端口号确定对应的服务器端应用程序,等到数据按照序号完整的接收完成后,合并组装成数据传输给服务器的应用处理程序。

服务器处理请求/传输响应数据

  1. 服务器应用程序对浏览器的请求处理响应后,返回响应数据。响应数据分为响应头、数据体。缓存就在此响应头中设置,包含强缓存/协商缓存的响应头。
  2. 经历http数据包进行tcp包装、ip包装、以太网包装后传递到客户端层。我们重点关注http、tcp就可以了,关于网络层的ip与链路层的以太网层,甚至物理层了解即可。

断开HTTP连接

其实断开的是TCP连接,TCP连接断开分为4次握手。

  1. 由浏览器发起FIN给服务器端,告知服务器我要断开和你的连接了。
  2. 服务器返回ACK给浏览器端,告知浏览器端我收到你的断开连接通知了。
  3. 服务器再次返回FIN给浏览器端,告知浏览器我准备好关闭连接了,你关闭吧。
  4. 浏览器发送ACK给服务器端,告知服务器端我关闭连接了。

浏览器响应数据处理

缓存

强缓存:具体见上面强缓存部分。
协商缓存:协商缓存主要由Last-modified/if-modified-since、ETag/if-none-match来在服务器端进行比对逻辑,最终确定是否使用缓存,使用缓存则返回304状态码并不返回响应数据体,不使用缓存则返回200并返回响应式数据体。

Last-Modified/if-modified-since

  1. 服务器在初次的响应头中增加Last-modified字段,字段值为UTC的时间字符串,代表服务响应体资源最后修改的时间。
  2. 浏览器端在接到有Last-modified响应头的响应后,会缓存响应体数据,再下次再次请求同路径的请求时在请求头中会自动带上if-modified-since字段,此字段的值就是上次同请求路径中响应头的Last-modified值。
  3. 服务器端在解析请求数据包的时候,取出请求头中的if-modified-since字段的值,并比对当前服务响应资源的最新修改时间是否与此值一致,一致则返回304状态码并不带响应体数据,不一致则返回200并返回响应体数据。
  4. 浏览器再接到304状态码后则直接使用本地缓存作为请求响应体数据,如果是200则更新本地响应体数据为服务器的响应体数据,并把服务器的响应体数据作为请求响应数据。

ETag/if-none-match

  1. 服务器在初次响应头中增加Etag字段,字段值为响应体数据的MD5字符串,并将此字符串值赋给ETag头部字段。
  2. 浏览器在接到有ETag字段的响应头响应后,会缓存响应体数据,再下次再次请求同路径的请求时会自动在请求头中带上if-none-match字段,字段值为上次同路径的响应头中的ETag字段值。
  3. 服务器在接到有if-none-match的请求头字段后,取出此字段值并与对应响应体资源的md5进行比对,如果一致则返回304并不返回响应体,如果不一致则返回200并返回响应体数据。
  4. 浏览器再接到304状态码后则直接使用本地缓存作为请求响应体数据,如果是200则更新本地响应体数据为服务器的响应体数据,并把服务器的响应体数据作为请求响应数据。

ETag/if-none-match优先级高于Last-Modified/if-modified-since,因此在两者都存在的时候以浏览器在决定使用缓存的逻辑以ETag/if-none-match为准。

HTML响应数据处理

前置知识:现代浏览器为多进程模式,主要包含浏览器主进程渲染进程插件进程网络进程GPU进程

  • 浏览器主进程:主要负责子进程管理、界面显示、用户操作等功能。

  • 渲染进程:主要将html、css、JavaScript渲染成用户看到的内容并响应用户的交互行为。主要分为以下几个线程:

    • 渲染线程:负责html、css解析以及布局、绘制到浏览器页面展示区。
    • JavaScript执行线程:负责js的执行。
    • 事件线程:负责响应事件被触发时候把事件处理函数添加到js处理队列的队尾。
    • 定时器线程:负责js中的定时任务(settimeout,setInterval等)的函数处理。
    • http异步请求线程:负责ajax的请求处理。
  • 插件进程:负责浏览器插件的管理。

  • 网络进程:负责浏览器的网络请求,上面的http就是网络进程负责发送与接收,依据响应头的content-type字段来区分对待,对于值为type/html的则把接收到的数据传递给对应的渲染进程来进行文档渲染,对于application/octet-stream字节流类型的则进行下载。

  • GPU进程:负责3d绘制。

HTML解析与渲染:HTNL解析与渲染由渲染进程的渲染线程负责,并最终渲染到浏览器的页面展示区。html的解析与渲染分为以下几类和步骤。

  1. 解析html生成html语法树,处理语法树生成dom树
  2. 解析css生成css语法树,处理语法树生成css规则树
  3. 合成dom树与css规则树生成渲染树
  4. 对渲染树进行布局计算各个元素位置与大小生成布局树。
  5. 渲染到页面展示区。

AJAX响应数据处理

ajax请求也是http请求,不过为异步的http请求(也有同步ajax请求,不常用)。ajax请求由浏览器的渲染进程的http异步线程去发起,并在接收到服务器响应数据后把ajax的回调函数(含响应数据)添加到js的执行队列的队尾,并轮到执行时执行ajax的回调。