绿色健康小清新

耐得住寂寞,守得住繁华

HTTP

HTTP

概况

HTTP 超文本传输协议,由客户程序和服务器程序实现,客户程序和服务器程序通过交换 HTTP 报文进行会话。HTTP 定义了这些报文的结构以及报文交换的方式,当用户请求一个 Web 页面时,浏览器向服务器发出对该页面中所包含对象的 HTTP 请求报文,服务器接收请求并返回包含这些对象的 HTTP 响应报文。

HTTP 使用 TCP 作为运输协议,它基于 TCP/IP 来传输文本、图片、视频、音频等,HTTP 并不提供数据包的传输功能,而仅仅是客户端和服务端约定好的一种通信格式。HTTP 客户首先发起一个与服务器的 TCP 连接,一旦连接建立,浏览器和服务器进程就可以通过套接字访问 TCP。客户向它的套接字接口发送请求报文,服务器从它的套接字接口接收请求报文。

HTTP 是一种无状态的协议,服务器不存储任何关于该客户的状态信息。假如某个客户在短时间内连续两次请求同一个对象,服务器并不会因为刚刚为该客户做出了响应就不再响应,而是重新进行响应。


URI / URL

我们可以通过输入 www.google.com 地址来访问谷歌的官网,那么这个地址有什么规定吗?我怎么输都可以?AAA.BBB.CCC 是不是也行?当然不是的,你输入的地址格式必须要满足 URI 的规范。

URI的全称是(Uniform Resource Identifier),中文名称是统一资源标识符,使用它就能够唯一地标记互联网上资源。

URL的全称是(Uniform Resource Locator),中文名称是统一资源定位符,也就是我们俗称的网址,它实际上是 URI 的一个子集。

URL是URI的一种,还有一种是 URN(统一资源名称),通过资源名称就能定位到资源。它们之间的关系如下:


非持续连接和持续连接

非持续连接必须为每个请求维护一个连接,对于每个连接,在客户和服务器中都要分配 TCP 缓冲区,给服务器造成很大负担。每次请求到响应大约需要花费两个 RTT 加上服务器传输文件的时间,RTT 指分组从客户到服务器再返回客户的时间。三次握手的前两部分占用一个 RTT,第三部分向服务器发送请求报文,服务器收到后做出响应,占用另一个 RTT。

HTTP1.1 使用持续连接,服务器响应后保持连接打开。在相同客户与服务器之间,后续的请求和响应 报文能够通过相同的连接进行传送。


报文格式

请求报文

请求报文包括请求行、首部行和实体

  • 请求行包括方法、URL 和 HTTP 版本。方法包括了 GET、POST、HEAD、PUT 和 DELETE 等。HEAD 类似于 GET,当服务器收到一个 HEAD 请求时,会用一个 HTTP 报文进行响应,但并不返回请求对象,通常使用 HEAD 进行调试;PUT 常用于上传对象到 Web 服务器;DELETE 用于删除 Web 服务器上的对象。

  • 首部行可以携带信息,例如 Connection:close 可以告诉服务器不要使用持续连接;User-agent 可以指明浏览器类型,服务器可以为不同类型的用户代理发送对象的不同版本。

  • 在首部行后有一个空行,后面跟着的是实体。使用 GET 时实体为空,而使用 POST 时才会使用实体。

HTTP 请求方法一般分为 8 种,它们分别是

  • GET 获取资源,GET 方法用来请求访问已被 URI 识别的资源。指定的资源经服务器端解析后返回响应内容。也就是说,如果请求的资源是文本,那就保持原样返回;

  • POST 传输实体,虽然 GET 方法也可以传输主体信息,但是便于区分,我们一般不用 GET 传输实体信息,反而使用 POST 传输实体信息,

  • PUT 传输文件,PUT 方法用来传输文件。就像 FTP 协议的文件上传一样,要求在请求报文的主体中包含文件内容,然后保存到请求 URI 指定的位置。

    但是,鉴于 HTTP 的 PUT 方法自身不带验证机制,任何人都可以上传文件 , 存在安全性问题,因此一般的 W eb 网站不使用该方法。若配合 W eb 应用程序的验证机制,或架构设计采用REST(REpresentational State Transfer,表征状态转移)标准的同类 Web 网站,就可能会开放使用 PUT 方法。

  • HEAD 获得响应首部,HEAD 方法和 GET 方法一样,只是不返回报文主体部分。用于确认 URI 的有效性及资源更新的日期时间等。

  • DELETE 删除文件,DELETE 方法用来删除文件,是与 PUT 相反的方法。DELETE 方法按请求 URI 删除指定的资源。

  • OPTIONS 询问支持的方法,OPTIONS 方法用来查询针对请求 URI 指定的资源支持的方法。

  • TRACE 追踪路径,TRACE 方法是让 Web 服务器端将之前的请求通信环回给客户端的方法。

  • CONNECT 要求用隧道协议连接代理,CONNECT 方法要求在与代理服务器通信时建立隧道,实现用隧道协议进行 TCP 通信。主要使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加 密后经网络隧道传输。

响应报文

响应报文包括状态行、首部行和实体:

  • 状态行包括协议版本、状态码和对应的状态信息。

  • 首部行中,Date 是服务器发送响应报文的时间;Server 指明了服务器类型,类似于请求报文中的 User-agent

  • 实体是报文的主要部分,即所请求的对象本身。

状态码短语含义
200OK成功响应
301Moved Permanently请求对象已被永久转移,新的 URL 定义在响应报文的首部行,客户端将自动获取。
302Found与301类似,但资源只是临时被移动,客户端继续使用原有 URL。
304Not Modified不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。
400Bad Request通用的差错代码,请求不能被服务器理解。
401Unauthorized未认证,缺乏相关权限。
402Payment Required保留,将来使用。
403Forbidden服务器理解客户端的请求,但是拒绝执行。
404Not Found被请求的文档不在服务器上,有可能因为请求 URL 出错。
405Method Not Allowed客户端中请求的方法被禁止,例如限制 POST 方式但使用了 GET 访问。
500Internal Server Error服务器内部错误,无法完成请求。
501Not Implemented服务器不支持请求的功能,无法完成请求。
502Bad Gateway作为网关或代理工作的服务器尝试执行请求时,从远程服务器收到了一个无效响应。
503Service Unavailable由于超载或系统维护,服务器暂时无法处理客户端的请求。
504Gateway Timeout充当网关或代理的服务器,未及时从远端服务器获取请求。
505HTTP Version Not Supported服务器不支持请求报文使用的 HTTP 版本。
  • 1xx 为开头属于提示信息,是协议处理中的一种中间状态,实际用到的比较少;
  • 2xx 为开头的都表示请求成功响应;
  • 3xx 为开头的都表示需要进行附加操作以完成请求,比如重定向;
  • 4xx 的响应结果表明客户端是发生错误的原因所在,比如有错误的语法或不能完成;
  • 5xx 为开头的响应标头都表示服务器本身发生错误;

首部行

首部行分为通用首部、请求首部、响应首部、实体首部。其中请求首部只在请求报文,响应首部只在响应报文。通用首部和实体首部请求和响应都可以。

通用首部:

  • Cache-Control:通过指定首部字段Cache-Control的指令,就能操作缓存的工作机制。
  • Connection:控制不再转发给代理的首部字段(在客户端发送请求和服务器返回响应内,使用Connection首部字段,可控制不再转发给代理的首部字段),管理持久连接keep-alive(1.1以后默认持久连接,当服务器端想明确断开连接时,则指定Connection首部字段的值为Close)。
  • Date:首部字段Date表明创建HTTP报文的日期和时间。
  • Pragma:Pragma是HTTP/1.1之前版本的历史遗留字段,仅作为与HTTP/1.0的向后兼容而定义。 但只用在客户端发送的请求中。客户端会要求所有的中间服务器不返回缓存的资源。
  • Trailer:首部字段Trailer会事先说明在报文主体后记录了哪些首部字段
  • Transfer-Encoding:首部字段Transfer-Encoding规定了传输报文主体时采用的编码方式。
  • Upgrade:首部字段Upgrade用于检测HTTP协议及其他协议是否可使用更高的版本进行通信,其参数值可以用来指定一个完全不同的通信协议。
  • Via:使用首部字段Via是为了追踪客户端与服务器之间的请求和响应报文的传输路径
  • Warning:该首部通常会告知用户一些与缓存相关的问题的警告

请求首部:

  • Accept:通知服务器,用户代理能够处理的媒体类型(比如html)及媒体类型的相对优先级(范围0-1),当服务器提供多种内容时,将会首先返回权重值最高的媒体类型。
  • **Accept-Charset:**可用来通知服务器用户代理支持的字符集及字符集的相对优先顺序
  • Accept-Encoding:Accept-Encoding首部字段用来告知服务器用户代理支持的内容编码及内容编码的优先级顺序
  • Accept-Language:用来告知服务器用户代理能够处理的自然语言集用来告知服务器用户代理能够处理的自然语言集
  • Authorization:用来告知服务器,用户代理的认证信息(证书值)
  • Expect:告知服务器,期望出现的某种特定行为。HTTP/1.1规范只定义了100-continue,等待状态码100响应的客户端在发生请求时,需要指定Expect:100-continue。
  • From:首部字段From用来告知服务器使用用户代理的用户的电子邮件地址
  • Host:请求的资源所处的互联网主机名和端口号。首部字段Host和以单台服务器分配多个域名的虚拟主机的工作机制有很密切的关联,这是首部字段Host必须存在的意义,有了 Host 字段,就可以将请求发往同一台服务器上的不同网站。
  • If-Match:形如If-xxx这种样式的请求首部字段,都可称为条件请求。服务器接收到附带条件的请求后,只有判断指定条件为真时,才会执行请求。服务器会比对If-Match的字段值和资源的ETag值,仅当两者一致时,才会执行请求
  • If-Modified-Since:它会告知服务器若If-Modified-Since字段值早于资源的更新时间,则希望能处理该请求。而在指
  • If-Modified-Since字段值的日期时间之后,如果请求的资源都没有过更新,则返回状态码304Not Modified的响应。
  • If-None-Match:它和首部字段If-Match作用相反。用于指定If-None-Match字段值的实体标记(ETag)值与请求资源的ETag不一致时,它就告知服务器处理该请求。
  • If-Range:它告知服务器若指定的If-Range字段值(ETag值或者时间)和请求资源的ETag值或时间相一致时,则作为范围请求处理。反之,则返回全体资源。
  • Referer:首部字段Referer会告知服务器请求的原始资源的URI。
  • Range:对于只需获取部分资源的范围请求,包含首部字段Range即可告知服务器资源的指定范围
  • Cookie:客户端请求服务器使用的Cookie

响应首部:

  • Accept-Ranges:首部字段Accept-Ranges是用来告知客户端服务器是否能处理范围请求,以指定获取服务器端某个部分的资源。可指定的字段值有两种,可处理范围请求时指定其为bytes,反之则指定其为none。
  • Age:首部字段Age能告知客户端,源服务器在多久前创建了响应。字段值的单位为秒。代理创建响应时必须加上首部字段Age
  • ETag:首部字段ETag能告知客户端实体标识。它是一种可将资源以字符串形式做唯一性标识的方式。服务器会为每份资源分配对应的ETag值
  • Location:使用首部字段Location可以将响应接收方引导至某个与请求URI位置不同的资源。基本上,该字段会配合3xx:Redirection的响应,提供重定向的URI。
  • Proxy-Authenticate:首部字段Proxy-Authenticate会把由代理服务器所要求的认证信息发送给客户端
  • Set-Cookie:设置cookie

实体首部:

  • Allow:首部字段Allow用于通知客户端能够支持Request-URI指定资源的所有HTTP方法。
  • Content-Encoding:告知客户端服务器对实体的主体部分选用的内容编码方式
  • Content-Language:实体主体使用的自然语言
  • Content-Length:表明了实体主体部分的大小
  • Content-Location:给出与报文主体部分相对应的URI
  • Content-MD5:一串由MD5算法生成的值,其目的在于检查报文主体在传输过程中是否保持完整,以及确认传输到达。
  • Content-Range:针对范围请求,返回响应时使用的首部字段Content-Range,能告知客户端作为响应返回的实体的哪个部分符合范围请求
  • Content-Type:实体主体内对象的媒体类型,比如text/html; charset=utf-8
  • Expires:首部字段Expires会将资源失效的日期告知客户端。
  • Last-Modified:首部字段Last-Modified指明资源最终修改的时间。

缓存

对于一些具有重复性的 HTTP 请求,比如每次请求得到的数据都一样的,我们可以把这对「请求-响应」的数据都缓存在本地,那么下次就直接读取本地的数据,不必在通过网络获取服务器的响应了,这样的话 HTTP/1.1 的性能肯定肉眼可见的提升。

所以,避免发送 HTTP 请求的方法就是通过缓存技术,HTTP 设计者早在之前就考虑到了这点,因此 HTTP 协议的头部有不少是针对缓存的字段。

HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存

Cache-Control

请求首部Cache-Control的值:

  1. no-cache。当客户端请求时携带这个首部字段的时候,通过中间的缓存服务器时,会不去拿缓存资源,而是让中间服务器转发给资源服务器,资源服务器看看一下这个资源过期没有,如果没有就会告知中间服务器,可以使用缓存资源。否则资源服务器就会直接返回新的资源;

  2. no-store。这个字段非常有意思,就是告知服务器或者客户端以及中间服务器,我请求或者响应的内容里面有机密信息,这些响应的内容是永远不会得到响应的;

  3. max-age。max-age指令标示了客户端不愿意接收一个age大于设定时间的响应,这个字段表达是最大缓存时长,请求中单单添加这个字段,实现不了缓存时长,必须结合响应的max-age。一会,会在响应中的max-age 详细说明;

  4. max-stale。这个指令表达的是缓存时长过期以后,还可以有效。比如现在max-age:60秒,那么max-stale:60秒,现在的缓存时长就是120秒;

  5. min-fresh。设定能够容忍的最小新鲜度(缓存时长)min-fresh标示了客户端不愿意接受新鲜度不多于当前的age加上min-fresh设定的时间之和的响应;

  6. no-transfrom。使用 no-transform 指令规定无论是在请求还是响应中,缓存都不能改变实体主体的媒体类型;

  7. only-if-cache。使用 only-if-cached 指令表示客户端仅在缓存服务器本地缓存目标资源的情况下,才会要求其返回。换言之,该指令要求缓存服务器不重新加载响应,也不会再次确认资源有效性。若发生请求缓存服务器的本 地缓存无响应,则返回状态码 504 Gateway Timeout;

  8. cache-extension。通过 cache-extension 标记(token),可以扩展 Cache-Control 首部字 段内的指令。

响应首部Cache-Control的值:

  1. pulic。这个字段和private是相对的,Cache-Control: public时,则表明所有的用户在通过缓存服务器的时候,都可以缓存这个资源;

  2. private。这个字段和pulic是相对的,Cache-Control: private时,则表明只有某个在通过缓存服务器的时候,得到缓存资源;

  3. no-cache。如果服务器返回的响应中包含 no-cache 指令,那么缓存服务器不能对 资源进行缓存。源服务器以后也将不再对缓存服务器请求中提出的资 源有效性进行确认,且禁止其对响应资源进行缓存操作;

  4. no-store。同请求首部的no-store指令一样;

  5. no-transfrom。同请求首部的no-transfrom指令一样;

  6. max-age。在Response中设置max-age的时间信息,可以在客户端生成缓存文件,在缓存不过期的情况下,客户端不会直接向服务器请求数据,在缓存过期的情况下,客户端会向服务器直接请求生成新的缓存。

    如果同时设置了Response和Request中的max-age 缓存时间,如果Request中的max-age时间小于Response中的max-age时间,客户端会根据Request中max-age时间周期去直接进行网络请求,如果碰到断网或者网络请求不通的情况,即使缓存还在有效期内(Response中设置的max-age时间足够大),在Request设置的max-age过期之后,APP也会直接去进行网络请求。 因此可以考虑在客户端的设计中一个和好的网络缓存场景,用Response的max-age控制缓存的时间,用Request中max-age控制刷新的时间和机制

    应用 HTTP/1.1 版本的缓存服务器遇到同时存在 Expires 首部字段的情况时,会优先处理 max-age 指令,而忽略掉 Expires 首部字段。而 HTTP/1.0 版本的缓存服务器的情况却相反,max-age 指令会被忽略

  7. s-max-age。和max-age类似,它们的不同点是 s- maxage 指令只适用于供多位用户使用的公共缓存服务器 ;

  8. must-revalidate。使用 must-revalidate 指令,代理会向源服务器再次验证即将返回的响 应缓存目前是否仍然有效。若代理无法连通源服务器再次获取有效资源的话,缓存必须给客户端 一条 504(Gateway Timeout)状态码。另外,使用 must-revalidate 指令会忽略请求的 max-stale 指令(即使已 经在首部使用了 max-stale,也不会再有效果);

  9. proxy-revalidate。proxy-revalidate 指令要求所有的缓存服务器在接收到客户端带有该指 令的请求返回响应之前,必须再次验证缓存的有效性;

  10. cache-extension。同请求首部的cache-extension指令一样

强制缓存

强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。

如下图中,返回的是 200 状态码,但在 size 项中标识的是 from disk cache,就是使用了强制缓存。

强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:

  • Cache-Control, 是一个相对时间;
  • Expires,是一个绝对时间;

如果 HTTP 响应头部同时有 Cache-Control 和 Expires 字段的话,Cache-Control的优先级高于 Expires

Cache-control 选项更多一些,设置更加精细,所以建议使用 Cache-Control 来实现强缓存。具体的实现流程如下:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 Cache-Control,Cache-Control 中设置了过期时间大小;
  • 浏览器再次请求访问服务器中的该资源时,会先通过请求资源的时间与 Cache-Control 中设置的过期时间大小,来计算出该资源是否过期,如果没有,则使用该缓存,否则重新请求服务器;
  • 服务器再次收到请求后,会再次更新 Response 头部的 Cache-Control。

协商缓存

当我们在浏览器使用开发者工具的时候,你可能会看到过某些请求的响应码是 304,这个是告诉浏览器可以使用本地缓存的资源,通常这种通过服务端告知客户端是否可以使用缓存的方式被称为协商缓存。

上图就是一个协商缓存的过程,所以协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存

协商缓存可以基于两种头部来实现。

第一种:请求头部中的 If-Modified-Since 字段与响应头部中的 Last-Modified 字段实现,这两个字段的意思是:

  • 响应头部中的 Last-Modified:表示这个响应资源的最后修改时间;
  • 请求头部中的 If-Modified-Since:当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;如果最后修改时间较旧(小),说明资源无新修改,响应 HTTP 304 走缓存。

第二种:请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段,这两个字段的意思是:

  • 响应头部中 Etag:唯一标识响应资源,是第一次请求的响应头部中的摘要;
  • 请求头部中的 If-None-Match:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 304,如果资源变化了返回 200。

第一种实现方式是基于时间实现的,第二种实现方式是基于一个唯一标识实现的,相对来说后者可以更加准确地判断文件内容是否被修改,避免由于时间篡改导致的不可靠问题。

如果在第一次请求资源的时候,服务端返回的 HTTP 响应头部同时有 Etag 和 Last-Modified 字段,那么客户端再下一次请求的时候,如果带上了 ETag 和 Last-Modified 字段信息给服务端,这时 Etag 的优先级更高,也就是服务端先会判断 Etag 是否变化了,如果 Etag 有变化就不用在判断 Last-Modified 了,如果 Etag 没有变化,然后再看 Last-Modified。

**为什么 ETag 的优先级更高?**这是因为 ETag 主要能解决 Last-Modified 几个比较难以解决的问题:

  1. 在没有修改文件内容情况下文件的最后修改时间可能也会改变,这会导致客户端认为这文件被改动了,从而重新请求;
  2. 可能有些文件是在秒级以内修改的,If-Modified-Since 能检查到的粒度是秒级的,使用 Etag就能够保证这种需求下客户端在 1 秒内能刷新多次;
  3. 有些服务器不能精确获取文件的最后修改时间。

强制缓存+协商缓存

注意,协商缓存这两个字段都需要配合强制缓存中 Cache-control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求

下图是强制缓存和协商缓存的工作流程:

当使用 ETag 字段实现的协商缓存的过程:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 ETag 唯一标识,这个唯一标识的值是根据当前请求的资源生成的;

  • 当浏览器再次请求访问服务器中的该资源时,首先会先检查强制缓存是否过期:

    • 如果没有过期,则直接使用本地缓存;
    • 如果缓存过期了,会在 Request 头部加上 If-None-Match 字段,该字段的值就是 ETag 唯一标识;
  • 服务器再次收到请求后,

    会根据请求中的 If-None-Match 值与当前请求的资源生成的唯一标识进行比较:

    • 如果值相等,则返回 304 Not Modified,不会返回资源
    • 如果不相等,则返回 200 状态码和返回资源,并在 Response 头部加上新的 ETag 唯一标识;
  • 如果浏览器收到 304 的请求响应状态码,则会从本地缓存中加载资源,否则更新资源。

GET 和 POST 的区别

  • GET 是幂等的,可以多次执行没有副作用,读取一个资源,可以将 GET 数据缓存在浏览器、代理或服务端。(反复 GET 不应该对访问有副作用,没有副作用被称为幂等。 )

    POST 不是幂等的,意味着不能随意多次执行,因此不能缓存,如果尝试重新执行 POST 请求,浏览器会弹出提示框询问是否重新提交表单。

  • GET 请求由 url 触发,想携带参数就只能在 url 后附加。

    POST 请求来自表单提交,表单数据被浏览器编码到 HTTP 请求报文的请求体中。主要有两种编码格式,一种是 application/..,用来传输简单数据;另一种是 multipart/form-data格式,用来传输文件,对二进制数据传输效率高。

  • 从攻击的角度说,无论 GET 还是 POST 都不安全,因为 HTTP 是明文协议,只要抓个包就都能看到了;

  • GET 长度受限于 url,而 url 的长度由浏览器和服务器决定。

    POST 没有大小限制,起限制作用的是服务器的处理能力。


HTTP 的无状态性简化了服务器设计,提高了性能,使其可以同时处理大量 TCP 连接。但无状态也导致服务器不能识别用户,为解决该问题 HTTP 使用 cookie 客户端会话技术对用户进行追踪。

工作流程

① 当客户通过浏览器第一次访问站点时,该站点将产生一个唯一识别码,并以此作为索引,在后端数据库中产生一个表项。

② 服务器用一个包含 Set-cookie 首部的 HTTP 响应报文对浏览器进行响应,浏览器收到后将其添加到自己管理的 cookie 文件。

③ 在下次访问该站点时,请求报文的首部行会包括这个识别码,尽管浏览器不知道客户是谁,但可以确定是同一个客户。

cookie 和 session 的区别

① cookie 只能存储 ASCII 码,而 session 可以存储任何类型的数据。

② session 存储在服务器,而 cookie 存储在客户浏览器中,容易被恶意查看。

③ session 的运行依赖 session id,而 session id 存在 cookie 中,叫做 JSESSIONID。如果浏览器禁用了 cookie ,同时 session 也会失效(可以通过其它方式实现,比如在 url 中传递 session_id)。


输入一个 url 发生的事

判断 url 是否合法,如果不合法会使用默认的搜索引擎进行搜索。如果输入的是一个域名,默认会加上一个 http 前缀。

先检查浏览器的 DNS 缓存,没有则检查本地 hosts 文件的缓存,如果仍然没有会向本地 DNS 服务器发送请求,最终本地 DNS 服务器得到域名和 IP 地址的映射关系,把结果返回给用户并进行缓存。

获取 IP 地址后,通过 TCP 三次握手建立连接,发送请求报文。

服务器收到请求报文后进行响应,主进程进行监听,创建子进程处理,先判断是否是重定向,如果是重定向则返回重定向地址。如果是静态资源则直接返回,否则通过 REST URL 在代码层面处理,最后返回响应报文。

浏览器收到 HTTP 响应报文后进行解析,首先查看响应报文的状态码,根据不同的状态码做不同处理。之后解析 HTML、CSS、JS 等文件,构建 DOM 树,构建CSS树,构建渲染树,布局渲染树,页面渲染。

通过 TCP 的四次挥手断开连接,如果是 HTTP1.1 则会将连接保持一小段时间。


HTTP版本

HTTP 0.9

HTTP问世之初并没有作为标准建立,被正式制定为标准是在1996年公布的HTTP/1.0协议。因此,在这之前的协议被称为HTTP/0.9。

  • 只支持GET请求方式:由于不支持其他请求方式,因此客户端是没办法向服务端传输太多的信息;

  • 没有请求头概念:所以不能在请求中指定版本号,服务端返回仅包含文件内容本身:

    1
    2
    3
    <html>
    <body>HELLO WORLD!</body>
    </html>
  • 服务端相响应之后,立即关闭TCP连接。

HTTP 1.0

随着互联网技术的飞速发展,HTTP协议被使用的越来越广泛,协议本身的局限性已经不能满足互联网功能的多样性。因此,1996年5月HTTP/1.0诞生,其内容和功能都大大增加了。对比与HTTP/0.9,新的版本包含了以下功能:

  • 请求方式新增了POST,DELETE,PUT,HEADER等方式;
  • 增添了请求头和响应头的概念,在通信中指定了 HTTP 协议版本号,以及其他的一些元信息 (比如: 状态码、权限、缓存、内容编码content-encoding);
  • 扩充了传输内容格式,在header中添加content-type以此可以传输html之外类型的文件,图片、音视频资源、二进制等都可以进行传输。

在这个版本主要的就是对请求和响应的元信息进行了扩展,客户端和服务端有更多的获取当前请求的所有信息,进而更好更快的处理请求相关内容。

1
2
3
4
5
6
7
8
9
10
11
GET /index.html HTTP/1.0
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)

200 OK
Date: Tue, 15 Nov 1994 08:12:31 GMT
Server: CERN/3.0 libwww/2.17
Content-Type: text/html;charset=utf-8 // 类型,编码。
<HTML>
A page with an image
<IMG src="/image.gif">
<HTML>

HTTP 1.1

仅仅在HTTP/1.0公布后的几个月,HTTP/1.1发布了,到目前为止HTTP1.1协议都是作为主流的版本,以至于随后的近10年时间里都没有新的HTTP协议版本发布。

对比之前的版本,其主要更新如下:

  • 引入了持续连接(keep-alive),在之前,非持续连接每获取一次资源都需要进行DNS解析过程以及TCP的三次握手,但在同服务器获取资源不断的建立和断开链接需要消耗的资源和时间是巨大的,为了提升连接的效率,持久连接使得服务器响应后保持连接打开,在相同客户与服务器之间,后续的请求和响应报文能够通过相同的连接进行传送

  • 添加了Pipeline,客户端可以以流水线的方式发送请求,也就是允许在第一个请求的答案完全传输之前发送第二个请求这降低了通信的延迟,但pipeline要求服务端必须按照请求发送的顺序返回响应,那如果一个响应返回延迟了,那么其后续的响应都会被延迟,直到队头的响应送达,造成http的队头阻塞

  • chunked机制,分块响应。用户通常会通过response header中返回的Content-Length来判断服务端返回数据的大小。但随着网络技术的不断发展,越来越多的动态资源被引入进来,这时候服务端就无法在传输之前知道待传递资源的大小,也就无法通过Content-Length来告知用户资源大小。服务器可以一边动态产生资源,一边传递给用户,这种机制称为“分块传输编码”(Chunkded Transfer Encoding),允许服务端发送给客户端的数据分为多个部分,此时服务器端需要在header中添加“Transfer-Encoding: chunked”头域来替代传统的“Content-Length。

    1
    Transfer-Encoding: chunked

    通过分块响应可以实现断点传输,在上传/下载资源时,如果资源过大,将其分割为多个部分,分别上传/下载,如果遇到网络故障,可以从已经上传/下载好的地方继续请求,不用从头开始,提高效率。

  • 引入了cache机制,当浏览器请求资源时,先看是否有缓存的资源,如果有缓存,直接取,不会再发请求,如果没有缓存,则发送请求。 通过设置字段cache-control来控制缓存;

  • 引入了内容协商,包括语言、编码和类型,客户端和服务器现在可以就交换哪些内容达成一致;

  • 请求首部新增Host首部行。在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname),1.1中新增的host用来处理一个 IP 地址上面多个虚拟主机的情况。在请求头域中新增了Host字段,其用来指定同一个ip服务器的不同域名。有了Host字段,在同一台服务器上就可以搭建不同的网站了,这也为后来虚拟化的发展建好啦地基。

    1
    Host: www.alibaba-inc.com

HTTP 2.0

根据时代的发展网页变得更加复杂。其中一些甚至本身就是应用程序。显示了更多的视觉媒体,增加了交互性的脚本的数量和大小也增加了。更多的数据通过更多的 HTTP 请求传输,这为 HTTP/1.1 连接带来了更多的复杂性和开销。为此,谷歌在 2010 年代初实施了一个实验性协议 SPDY。鉴于SPDY的成功,HTTP/2也采用了SPDY作为整个方案的蓝图进行开发。HTTP/2 于 2015 年 5 月正式标准化

  • 二进制分帧。HTTP 1.x 的解析是基于文本,HTTP 2之后将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,多个帧之间可以乱序发送,根据帧首部的流(比如每个流都有自己的id)表示可以重新组装

  • 多路复用。在共享TCP链接的基础上同时发送请求和响应,将一个request/response作为一个stream,每个流都有独一无二的标志和优先级,并将一个stream根据负载分为多种类型的frame(例如 header frame,data frame等),在同一条connection之上可以混合发送分属于不同stream的frame,来自不同流的帧可以通过帧头的标志来关联和组装起来。多路复用解决了 HTTP 层的队头阻塞,但仍然存在 TCP 层的队头阻塞

    TCP队头阻塞(head-of-line blocking)发生在一个TCP分节丢失,导致其后续分节不按序到达接收端的时候。该后续分节将被接收端一直保持直到丢失的第一个分节被发送端重传并到达接收端为止

    HTTP队头阻塞是由pipeline引起的,pipeline要求服务端必须按照请求发送的顺序返回响应,那如果一个响应返回延迟了,那么其后续的响应都会被延迟,直到队头的响应送达。

  • 头部压缩HPACK,由于一些请求在一组请求中通常是相似的,因此这消除了传输数据的重复和开销。使用一份索引表来定义常用的 HTTP Header,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小:

  • 服务器推送,服务器可以额外的向客户端推送资源,而无需客户端明确的请求。在浏览器刚请求 HTML 的时候,服务端会把某些资源存在一定的关联性 JS、CSS 等文件等静态资源主动发给客户端,这样客户端可以直接从本地加载这些资源,不用再通过网络再次请求,以此来达到节省浏览器发送request请求的过程。

    使用服务器推送:

    1
    2
    3
    4
    5
    6
    # 静态 Link 标签法
    <link rel="preload" href="/css/styles.css" as="style">

    # HTTP 头表示法
    Link: </css/styles.css>; rel=preload; as=style,
    </img/example.png>; rel=preload; as=image

    可以看到服务器initiator中的push状态表示这是服务端进行主动推送:

    对于主动推送的文件势必会带来多余或已经浏览器已有一份的文件,客户端使用一个简洁的 Cache Digest 来告诉服务器,哪些东西已经在缓存,因此服务器也就会知道哪些是客户端所需要的。

HTTP 3.0

在限定条件下,TCP下解决队头阻塞的问题相当困难,但是随着互联网的爆炸式发展,更高的稳定性和安全性需要得到满足,谷歌在2016年11月国际互联网工程任务组(IETF)召开了第一次QUIC(Quick UDP Internet Connections)工作组会议,制定的一种基于UDP的低时延的互联网传输层协议,HTTP-over-QUIC于2018年11月更名为HTTP/3。

(1)0-RTT 握手

tcp中 客户端发送syn包(syn seq=x)到服务器, 服务器接收并且需要发送(SYN seq =y; ACK x+1)包给客户端,客户端向服务器发送确认包ACK(seq = x+1; ack=y+1),至此客户端和服务器进入ESTABLISHED状态,完成三次握手。

而HTTP3.0只需要进行一次握手就可以进行通信,如果缓存ServerConfig,甚至可以不用握手就直接通信。

1-RTT

  1. 客服端使用私钥 a 然后选择一个公开的加密数 X ,通过计算得出 a*X = A, 将X 和 A发送给服务端;
  2. 服务端使用私钥 b,通过计算得出 b*X = B, 将B发送给客户端;
  3. 客户端使用ECDH生成通讯密钥 key = a*B = a*(b*X);
  4. 服务器使用ECDH生成通讯密钥 key = b*A = b*(a*X)。

0-RTT

0-RTT则是客户端缓存了 ServerConfig(B=b*X),下次建连直接使用缓存数据计算通信密钥:

  • 客户端:使用私钥,选择公开的大数 X,计算 A=c*X,将 A 和 X 发送给服务器,也就是 Client Hello 消息后,客户端直接使用缓存的 B 计算通信密钥 KEY = c*B = c*b*X,加密发送应用数据;
  • 服务器:根据 Client Hello 消息计算通信密钥 key = b*A = b*(c*X)。

客户端不需要经过握手直接通过缓存的B生成key就可以发送应用数据。

再来思考一个问题:假设攻击者记录下所有的通信数据和公开参数A1,A2,一旦服务器的随机数 b(私钥)泄漏了,那之前通信的所有数据就都可以破解了。为了解决这个问题,需要为每次会话都创建一个新的通信密钥,来保证前向安全性

(2)有序交付

QUIC 是基于 UDP 协议的,而 UDP 是不可靠传输协议,QUIC 在每个数据包都设有一个 offset 字段(偏移量),接收端根据 offset 字段就可以对异步到达的数据包进行排序了,保证了有序性。

(3)队头堵塞

HTTP/2 之所以存在 TCP 层的队头阻塞,是因为所有请求流都共享一个滑动窗口,TCP中的队头阻塞的产生是由TCP自身的实现机制决定的,无法避免。想要在应用程序当中避免TCP队头阻塞带来的影响,只有舍弃TCP协议。QUIC中给每个请求流都分配一个独立的滑动窗口

A 请求流上的丢包不会影响 B 请求流上的数据发送。但是,对于每个请求流而言,也是存在队头阻塞问题的,也就是说,虽然 QUIC 解决了 TCP 层的队头阻塞,但仍然存在单条流上的队头阻塞。这就是 QUIC 声明的无队头阻塞的多路复用。

(4)连接迁移

当客户端切换网络时,和服务器的连接并不会断开,仍然可以正常通信,对于 TCP 协议而言,这是不可能做到的。因为 TCP 的连接基于 4 元组:源 IP、源端口、目的 IP、目的端口,只要其中 1 个发生变化,就需要重新建立连接。但 QUIC 的连接是基于 64 位的 Connection ID,网络切换并不会影响 Connection ID 的变化,连接在逻辑上仍然是通的。

假设客户端先使用 IP1 发送了 1 和 2 数据包,之后切换网络,IP 变更为 IP2,发送了 3 和 4 数据包,服务器根据数据包头部的 Connection ID 字段可以判断这 4 个包是来自于同一个客户端。QUIC 能实现连接迁移的根本原因是底层使用 UDP 协议就是面向无连接的。

对比

HTTP 1.1有点慢,优化方法

优化方法主要有三种:

第一个思路是,通过缓存技术来避免发送 HTTP 请求。客户端收到第一个请求的响应后,可以将其缓存在本地磁盘,下次请求的时候,如果缓存没过期,就直接读取本地缓存的响应数据。如果缓存过期,客户端发送请求的时候带上响应数据的摘要,服务器比对后发现资源没有变化,就发出不带包体的 304 响应,告诉客户端缓存的响应仍然有效。

第二个思路是,减少 HTTP 请求的次数,有以下的方法:

  • 将原本由客户端处理的重定向请求,交给代理服务器处理,这样可以减少重定向请求的次数;
  • 将多个小资源合并成一个大资源再传输,能够减少 HTTP 请求次数以及 头部的重复传输,再来减少 TCP 连接数量,进而省去 TCP 握手和慢启动的网络消耗;
  • 按需访问资源,只访问当前用户看得到/用得到的资源,当客户往下滑动,再访问接下来的资源,以此达到延迟请求,也就减少了同一时间的 HTTP 请求次数。

第三思路是,通过压缩响应资源,降低传输资源的大小,从而提高传输效率,所以应当选择更优秀的压缩算法。

避免发送 HTTP 请求-cache

客户端要向服务器发送请求的。但是,对于一些具有重复性的 HTTP 请求,比如每次请求得到的数据都一样的,我们可以把这对「请求-响应」的数据都缓存在本地,那么下次就直接读取本地的数据,不必在通过网络获取服务器的响应了,这样的话 HTTP/1.1 的性能肯定肉眼可见的提升。

所以,避免发送 HTTP 请求的方法就是通过缓存技术,HTTP 设计者早在之前就考虑到了这点,因此 HTTP 协议的头部有不少是针对缓存的字段。

那缓存是如何做到的呢?

客户端会把第一次请求以及响应的数据保存在本地磁盘上,其中将请求的 URL 作为 key,而响应作为 value,两者形成映射关系。

这样当后续发起相同的请求时,就可以先在本地磁盘上通过 key 查到对应的 value,也就是响应,如果找到了,就直接从本地读取该响应。毋庸置疑,读取本次磁盘的速度肯定比网络请求快得多,如下图:

服务器在发送 HTTP 响应时,会估算一个过期的时间,并把这个信息放到响应头部中,这样客户端在查看响应头部的信息时,一旦发现缓存的响应是过期的,则就会重新发送网络请求

如果客户端从第一次请求得到的响应头部中发现该响应过期了,客户端重新发送请求,假设服务器上的资源并没有变更,还是老样子,那么你觉得还要在服务器的响应带上这个资源吗?

很显然不带的话,可以提高 HTTP 协议的性能,那具体如何做到呢?

只需要客户端在重新发送请求时,在请求的 Etag 头部带上第一次请求的响应头部中的摘要,这个摘要是唯一标识响应的资源,当服务器收到请求后,会将本地资源的摘要与请求中的摘要做个比较。

  • 如果不同,那么说明客户端的缓存已经没有价值,服务器在响应中带上最新的资源;
  • 如果相同,说明客户端的缓存还是可以继续使用的,那么服务器仅返回不含有包体的 304 Not Modified 响应,告诉客户端仍然有效,这样就可以减少响应资源在网络中传输的延时。

缓存真的是性能优化的一把万能钥匙,小到 CPU Cache、Page Cache、Redis Cache,大到 HTTP 协议的缓存。

减少 HTTP 请求次数

减少 HTTP 请求次数自然也就提升了 HTTP 性能,可以从这 3 个方面入手:

  • 减少重定向请求次数
  • 合并请求
  • 延迟发送请求
减少重定向请求次数

服务器上的一个资源可能由于迁移、维护等原因从 url1 移至 url2 后,而客户端不知情,它还是继续请求 url1,这时服务器不能粗暴地返回错误,而是通过 302 响应码和 Location 头部,告诉客户端该资源已经迁移至 url2 了,于是客户端需要再发送 url2 请求以获得服务器的资源。

那么,如果重定向请求越多,那么客户端就要多次发起 HTTP 请求,每一次的 HTTP 请求都得经过网络,这无疑会越降低网络性能。

另外,服务端这一方往往不只有一台服务器,比如源服务器上一级是代理服务器,然后代理服务器才与客户端通信,这时客户端重定向就会导致客户端与代理服务器之间需要 2 次消息传递,如下图:

如果重定向的工作交由代理服务器完成,就能减少 HTTP 请求次数了,如下图:

而且当代理服务器知晓了重定向规则后,可以进一步减少消息传递次数,如下图:

除了 302 重定向响应码,还有其他一些重定向的响应码,你可以从下图看到:

其中,301308 响应码是告诉客户端可以将重定向响应缓存到本地磁盘,之后客户端就自动用 url2 替代 url1 访问服务器的资源。

合并请求

如果把多个访问小文件的请求合并成一个大的请求,虽然传输的总资源还是一样,但是减少请求,也就意味着减少了重复发送的 HTTP 头部。

另外由于 HTTP/1.1 是请求响应模型,如果第一个发送的请求,未收到对应的响应,那么后续的请求就不会发送,于是为了防止单个请求的阻塞,所以一般浏览器会同时发起 5-6 个请求,每一个请求都是不同的 TCP 连接,那么如果合并了请求,也就会减少 TCP 连接的数量,因而省去了 TCP 握手和慢启动过程耗费的时间

接下来,具体看看合并请求的几种方式。

有的网页会含有很多小图片、小图标,有多少个小图片,客户端就要发起多少次请求。那么对于这些小图片,我们可以考虑使用 CSS Image Sprites 技术把它们合成一个大图片,这样浏览器就可以用一次请求获得一个大图片,然后再根据 CSS 数据把大图片切割成多张小图片。

这种方式就是通过将多个小图片合并成一个大图片来减少 HTTP 请求的次数,以减少 HTTP 请求的次数,从而减少网络的开销

除了将小图片合并成大图片的方式,还可以使用精灵图,通过移动精灵图的位置显示不同的图片。

还有服务端使用 webpack 等打包工具将 js、css 等资源合并打包成大文件,也是能达到类似的效果。

另外,还可以将图片的二进制数据用 base64 编码后,以 URL 的形式潜入到 HTML 文件,跟随 HTML 文件一并发送

1
<image src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPoAAAFKCAIAAAC7M9WrAAAACXBIWXMAA ... />

这样客户端收到 HTML 后,就可以直接解码出数据,然后直接显示图片,就不用再发起图片相关的请求,这样便减少了请求的次数。

可以看到,合并请求的方式就是合并资源,以一个大资源的请求替换多个小资源的请求。

但是这样的合并请求会带来新的问题,当大资源中的某一个小资源发生变化后,客户端必须重新下载整个完整的大资源文件,这显然带来了额外的网络消耗。

延迟发送请求

不要一口气吃成大胖子,一般 HTML 里会含有很多 HTTP 的 URL,当前不需要的资源,我们没必要也获取过来,于是可以通过「按需获取」的方式,来减少第一时间的 HTTP 请求次数。

请求网页的时候,没必要把全部资源都获取到,而是只获取当前用户所看到的页面资源,当用户向下滑动页面的时候,再向服务器获取接下来的资源,这样就达到了延迟发送请求的效果。

减少 HTTP 响应的数据大小

对于 HTTP 的请求和响应,通常 HTTP 的响应的数据大小会比较大,也就是服务器返回的资源会比较大。

于是,我们可以考虑对响应的资源进行压缩,这样就可以减少响应的数据大小,从而提高网络传输的效率。

压缩的方式一般分为 2 种,分别是:

  • 无损压缩;

  • 有损压缩;

无损压缩

无损压缩是指资源经过压缩后,信息不被破坏,还能完全恢复到压缩前的原样,适合用在文本文件、程序可执行文件、程序源代码

首先,我们针对代码的语法规则进行压缩,因为通常代码文件都有很多换行符或者空格,这些是为了帮助程序员更好的阅读,但是机器执行时并不要这些符,把这些多余的符号给去除掉。

接下来,就是无损压缩了,需要对原始资源建立统计模型,利用这个统计模型,将常出现的数据用较短的二进制比特序列表示,将不常出现的数据用较长的二进制比特序列表示,生成二进制比特序列一般是「霍夫曼编码」算法。

gzip 就是比较常见的无损压缩。客户端支持的压缩算法,会在 HTTP 请求中通过头部中的 Accept-Encoding 字段告诉服务器:

1
Accept-Encoding: gzip, deflate, br

服务器收到后,会从中选择一个服务器支持的或者合适的压缩算法,然后使用此压缩算法对响应资源进行压缩,最后通过响应头部中的 content-encoding 字段告诉客户端该资源使用的压缩算法。

1
content-encoding: gzip

gzip 的压缩效率相比 Google 推出的 Brotli 算法还是差点意思,也就是上文中的 br,所以如果可以,服务器应该选择压缩效率更高的 br 压缩算法。

有损压缩

与无损压缩相对的就是有损压缩,经过此方法压缩,解压的数据会与原始数据不同但是非常接近

有损压缩主要将次要的数据舍弃,牺牲一些质量来减少数据量、提高压缩比,这种方法经常用于压缩多媒体数据,比如音频、视频、图片

可以通过 HTTP 请求头部中的 Accept 字段里的「 q 质量因子」,告诉服务器期望的资源质量

1
Accept: audio/*; q=0.2, audio/basic

关于图片的压缩,目前压缩比较高的是 Google 推出的 WebP 格式,它与常见的 Png 格式图片的压缩比例对比如下图:

可以发现,相同图片质量下,WebP 格式的图片大小都比 Png 格式的图片小,所以对于大量图片的网站,可以考虑使用 WebP 格式的图片,这将大幅度提升网络传输的性能。

关于音视频的压缩,音视频主要是动态的,每个帧都有时序的关系,通常时间连续的帧之间的变化是很小的。

比如,一个在看书的视频,画面通常只有人物的手和书桌上的书是会有变化的,而其他地方通常都是静态的,于是只需要在一个静态的关键帧,使用增量数据来表达后续的帧,这样便减少了很多数据,提高了网络传输的性能。对于视频常见的编码格式有 H264、H265 等,音频常见的编码格式有 AAC、AC3。

本文一些部分来源小林的文章,受益匪浅。

-------------本文结束感谢您的阅读-------------
六经蕴籍胸中久,一剑十年磨在手

欢迎关注我的其它发布渠道