阅读 1455

HTTP面试无非就是这些东西


前言

HTTP,超文本传输协议,就是浏览器和服务器通信的一种规定。废话不多说直接开搞,本文参考拉勾网课程。


HTTP各版本

1991年HTTP诞生,版本为0.9,主要内容单纯是传输超文本内容HTML

1996年,随着互联网发展,浏览器希望通过HTTP传输脚本、样式、图片、音频和视频等不同类型的文件,推出HTTP1.0,增加了头部设定,指定了文件类型,然后这个头部也可以解决其他问题,例如缓存。

1999年,为了解决每次通信都要建立连接、传输连接、断开连接三个阶段,推出HTTP1.1,推出了一个创建持久连接的方法(长连接)。

2015年,为了解决HTTP1.1同一时刻只能处理一个请求阻塞情况,推出HTTP2,采用2进制分帧的机制提升传输效率。

2018年,HTTP3将底层依赖的TCP改成UDP,解决了传输中出现丢包导致整个TCP连接断开问题,但是缺点就是没有确认机制保证了对方是否一定接收到数据。

三次握手

在建立tcp连接之前,客户端要和服务器互相确认对方是否有接收和发送的能力,被称为三次握手。

第一次握手:刚开始客户端处于 CLOSED 的状态,服务端处于 LISTEN 状态。客户端给服务端发送一个 SYN 报文,并指明客户端的初始化序列号 ISN,此时客户端处于 SYN_SEND 状态。

第二次握手:当服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。

第三次握手:当客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也同样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方成功建立起了连接。

第一次握手成功让服务端知道了客户端具有发送能力,第二次握手成功让客户端知道了服务端具有接收和发送能力,但此时服务端并不知道客户端是否接收到了自己发送的消息,所以第三次握手就起到了这个作用。经过三次通信后,服务端和客户端都确认了双方的接收和发送能力。


四次挥手

当客户端和服务端断开连接时要发送四次数据,这个过程称之为四次挥手。

第一次挥手:在挥手之前服务端与客户端都处于 ESTABLISHED 状态。客户端发送一个 FIN 报文,用来关闭客户端到服务器的数据传输,此时客户端处于 FIN_WAIT_1 状态。  

第二次挥手:当服务端收到 FIN 之后,会发送 ACK 报文,并且把客户端的序列号值加 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。 

第三次挥手:如果服务端同意关闭连接,则会向客户端发送一个 FIN 报文,并且指定一个序列号,此时服务端处于 LAST_ACK 的状态。

 第四次挥手:当客户端收到 ACK 之后,处于 FIN_WAIT_2 状态。待收到 FIN 报文时发送一个 ACK 报文作为应答,并且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。等待一段时间后会进入 CLOSED 状态,当服务端收到 ACK 报文之后,也会变为 CLOSED 状态,此时连接正式关闭。


HTTPS原理

HTTP 虽然能满足客户端与服务端的通信需求,但这种使用明文发送数据的方式存在一定的安全隐患,因为通信内容很容易被通信链路中的第三方截获甚至篡改。

对称加密 

当然是对通信数据进行加密传输。加密方式分为对称加密和非对称加密,最大的区别在于,对称加密在加/解密过程中使用同一个密钥,而非对称加密使用不同的密钥进行加/解密。在性能方面,对称密钥更胜一筹,所以可以使用对称密钥。 但是肯定不能在每次通信中都使用同一个对称密钥,因为如果使用同一个密钥,任何人只要与服务端建立通信就能获得这个密钥,也就可以轻松解密其他通信数据了。所以应该是每次通信都要随机生成。 

非对称加密 

由于不可能保证客户端和服务端同时生成一个相同的随机密钥,所以生成的随机密钥需要被传输,这样的话在传输过程中也会存在被盗取的风险。 要解决这个问题还需要通过将密钥加密来进行传输。除了前面提到的对称加密,我们只有非对称加密这个选项了,比如客户端通过公钥来加密,服务端利用私钥来解密。 

证书机制 

同样的问题也会出现,密钥对生成后,该怎么分发呢? 如果在客户端生成密钥对,把私钥发给服务端,那么服务端需要为每个客户端保存一个密钥,这显然是不太现实的。所以只能由服务端生成密钥对,将公钥分发给需要建立连接的客户端。 直接发送给客户端还是会被篡改,此时只能借助第三方来实现了,比如证书机制。 具体来说就是把公钥放入一个证书中,该证书包含服务端的信息,比如颁发者、域名、有效期,为了保证证书是可信的,需要由一个可信的第三方来对证书进行签名。这个第三方一般是证书的颁发机构,也称 CA(Certification Authority,认证中心)。 那么这个证书的签名怎么检验真假呢? 要回答这个问题先要理解证书签名的过程。证书签名就是将证书信息进行 MD5 计算,获取唯一的哈希值,然后再利用证书颁发方的私钥对其进行加密生成。 校验过程与之相反,需要用到证书颁发方的公钥对签名进行解密,然后计算证书信息的 MD5 值,将解密后的 MD5 值与计算所得的 MD5 值进行比对,如果两者一致代表签名是可信的。所以要校验签名的真伪,就需要获得证书颁发方的公钥,这个公钥就在颁发方的证书中。 这种通过签名来颁发与校验证书的方式会形成一个可追溯的链,即证书链。处于证书链顶端的证书称为根证书,这些根证书被预置在操作系统的内部。


HTTP 缓存 

使用缓存最大的问题往往不在于将资源缓存在什么位置或者如何读写资源,而在于如何保证缓存与实际资源一致的同时,提高缓存的命中率。也就是说尽可能地让浏览器从缓存中获取资源,但同时又要保证被使用的缓存与服务端最新的资源保持一致。 为了达到这个目的,需要制定合适的缓存过期策略(简称“缓存策略”),HTTP 支持的缓存策略有两种:强制缓存和协商缓存。 

强制缓存 

强制缓存是在浏览器加载资源的时候,先直接从缓存中查找请求结果,如果不存在该缓存结果,则直接向服务端发起请求。 

1. Expires 

HTTP/1.0 中可以使用响应头部字段 Expires 来设置缓存时间,它对应一个未来的时间戳。客户端第一次请求时,服务端会在响应头部添加 Expires 字段。当浏览器再次发送请求时,先会对比当前时间和 Expires 对应的时间,如果当前时间早于 Expires 时间,那么直接使用缓存;反之,需要再次发送请求。 但是使用 Expires 响应头时容易产生一个问题,那就是服务端和浏览器的时间很可能不同,因此这个缓存过期时间容易出现偏差。同样的,客户端也可以通过修改系统时间来继续使用缓存或提前让缓存失效。 为了解决这个问题,HTTP/1.1 提出了 Cache-Control 响应头部字段。 

2. Cache-Control 它的常用值有下面几个: 

no-cache,表示使用协商缓存,即每次使用缓存前必须向服务端确认缓存资源是否更新; 

no-store,禁止浏览器以及所有中间缓存存储响应内容; 

public,公有缓存,表示可以被代理服务器缓存,可以被多个用户共享; 

private,私有缓存,不能被代理服务器缓存,不可以被多个用户共享;

max-age,以秒为单位的数值,表示缓存的有效时间; 

must-revalidate,当缓存过期时,需要去服务端校验缓存的有效性。  

这几个值可以组合使用,比如:  cache-control: public, max-age=31536000 告诉浏览器该缓存为公有缓存,有效期 1 年。 需要注意的是,cache-control 的 max-age 优先级高于 Expires,也就是说如果它们同时出现,浏览器会使用 max-age 的值。 注意,虽然你可能在其他资料中看到可以使用 meta 标签来设置缓存,比如像下面的形式:  

 <meta http-equiv="expires" content="Wed, 20 Jun 2021 22:33:00 GMT"复制代码

 但在 HTML5 规范中,并不支持这种方式,所以尽量不要使用 meta 标签来设置缓存。 

协商缓存 

协商缓存的更新策略是不再指定缓存的有效时间了,而是浏览器直接发送请求到服务端进行确认缓存是否更新,如果请求响应返回的 HTTP 状态为 304,则表示缓存仍然有效。控制缓存的难题就是从浏览器端转移到了服务端。 

1. Last-Modified 和 If-Modified-Since 

服务端要判断缓存有没有过期,只能将双方的资源进行对比。若浏览器直接把资源文件发送给服务端进行比对的话,网络开销太大,而且也会失去缓存的意义,所以显然是不可取的。有一种简单的判断方法,那就是通过响应头部字段 Last-Modified 和请求头部字段 If-Modified-Since 比对双方资源的修改时间。

具体工作流程如下: 

  • 浏览器第一次请求资源,服务端在返回资源的响应头中加入 Last-Modified 字段,该字段表示这个资源在服务端上的最近修改时间; 
  • 当浏览器再次向服务端请求该资源时,请求头部带上之前服务端返回的修改时间,这个请求头叫 If-Modified-Since; 
  • 服务端再次收到请求,根据请求头 If-Modified-Since 的值,判断相关资源是否有变化,如果没有,则返回 304 Not Modified,并且不返回资源内容,浏览器使用资源缓存值;否则正常返回资源内容,且更新 Last-Modified 响应头内容。  

这种方式虽然能判断缓存是否失效,但也存在两个问题: 

  • 精度问题,Last-Modified 的时间精度为秒,如果在 1 秒内发生修改,那么缓存判断可能会失效; 
  • 准度问题,考虑这样一种情况,如果一个文件被修改,然后又被还原,内容并没有发生变化,在这种情况下,浏览器的缓存还可以继续使用,但因为修改时间发生变化,也会重新返回重复的内容。 

2. ETag 和 If-None-Match 

为了解决精度问题和准度问题,HTTP 提供了另一种不依赖于修改时间,而依赖于文件哈希值的精确判断缓存的方式,那就是响应头部字段 ETag 和请求头部字段 If-None-Match。  

具体工作流程如下: 

  • 浏览器第一次请求资源,服务端在返响应头中加入 Etag 字段,Etag 字段值为该资源的哈希值; 
  • 当浏览器再次跟服务端请求这个资源时,在请求头上加上 If-None-Match,值为之前响应头部字段 ETag 的值;  
  • 服务端再次收到请求,将请求头 If-None-Match 字段的值和响应资源的哈希值进行比对,如果两个值相同,则说明资源没有变化,返回 304 Not Modified;否则就正常返回资源内容,无论是否发生变化,都会将计算出的哈希值放入响应头部的 ETag 字段中。  

这种缓存比较的方式也会存在一些问题,具体表现在以下两个方面。 

  • 计算成本。生成哈希值相对于读取文件修改时间而言是一个开销比较大的操作,尤其是对于大文件而言。如果要精确计算则需读取完整的文件内容,如果从性能方面考虑,只读取文件部分内容,又容易判断出错。
  •  计算误差。HTTP 并没有规定哈希值的计算方法,所以不同服务端可能会采用不同的哈希值计算方式。这样带来的问题是,同一个资源,在两台服务端产生的 Etag 可能是不相同的,所以对于使用服务器集群来处理请求的网站来说,使用 Etag 的缓存命中率会有所降低。 

需要注意的是,强制缓存的优先级高于协商缓存,在协商缓存中,Etag 优先级比 Last-Modified 高。既然协商缓存策略也存在一些缺陷,那么我们转移到浏览器端看看 ServiceWorker 能不能给我们带来惊喜。

总结

HTTP面试过程中无非就是这些东西,兄弟们背起来。