常见HTTP问题浅析

601 阅读21分钟

三次握手

关于TCP协议

TCP(Transmission Control Protocol, 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。与之对应的是UDP(User Datagram Protocol ,用户数据报协议),是不可靠的传输层协议。

TCP报文格式

img

知识预备

  • SYN 同步位 SYN=1 表示进行一个链接请求;当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。
  • ACK 确认位 ACK=1 确认有效 ACK=0 确认无效;
  • seq 序号 随机的
  • ack 应答号 对方发送的 序号+1
  • FIN 终止 FIN= 1数据已经发送完毕,并且要求释放

简单小对话,基础了解三次握手

C:我要给你发信息了(SYN=1,seq=100 )

S:好的,我准备好了,你发((ACK=1, ack=101. SYN=1, seq=200)

C:好的,收到(ACK=1, ack=201)

image-20210714091843734

  1. 客户端发送 SYN=1的询问报文给服务器端,seq是x,进入 SYN_SENT 状态。

  2. 服务器端回应一个ACK=1、SYN=1 的应答+询问报文。应答号ack是x+1,询问号seq是y,进入 SYN_RCVD 状态。

  3. 客户端收到后,回应一个 ACK=1的应答报文,应答号是y+1,进入 Established 状态。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。

为什么要三步握手

假设是两步握手。客户端发送请求报文A,因网络延时服务器没收到。又发了一遍报文A,服务器收到后建立链接等待客户端发送数据。客户端正常发送数据。 过了一会第一次发送的报文A也到达服务器,服务器再次建立链接等待客户端发送数据,而客户端并不知情。浪费服务器资源。

经过三次挥手后确认建立连接后,发送HTTP请求

HTTP请求

发送HTTP请求的过程就是构建HTTP请求报文并通过TCP协议中发送到服务器指定端口 请求报文由请求行请求头请求体组成。

POST /auth/login HTTP/1.1 // 请求行
// 请求头
Host: blog-server.hunger-valley.com
Connection: keep-alive
Content-Length: 41
Accept: application/json, text/plain, */*
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imh1bmdlciIsImlkIjoxLCJpYXQiOjE2MTExMjc1MjMsImV4cCI6MTYxMTM4NjcyM30.U-CkNW7WU0zprsjI23eK-0TE5wS_gD-2ZTFW8wE31FU
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Content-Type: application/json;charset=UTF-8
Origin: https://jirengu-inc.github.io
Referer: https://jirengu-inc.github.io/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

{"username":"hunger","password":"123456"} // 请求体

请求行

包含请求方法、URL、协议版本

  • 请求方法包含 8 种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE。
  • URL 即请求地址,由 <协议>://<主机>:<端口>/<路径>?<参数> 组成
  • 协议版本即 http 版本号

请求头

请求头部通知服务器有关于客户端请求的信息。它包含许多有关的客户端环境和请求正文的有用信息。其中比如:Host,表示主机名,虚拟主机;Connection,HTTP/1.1 增加的,使用 keepalive,即持久连接,一个连接可以发多个请求;User-Agent,请求发出者,兼容性以及定制化需求。

请求体

可以承载多个请求参数的数据,包含回车符、换行符和请求数据,并不是所有请求都具有请求数据。上面图片,承载着 name、password、realName 三个请求参数。

响应请求

每台服务器上都会安装处理请求的应用——web server。常见的 web server 产品有 apache、nginx、IIS 或 Lighttpd 等。web server 担任管控的角色,对于不同用户发送的请求,会结合配置文件,把不同请求委托给服务器上处理相应请求的程序进行处理(例如 CGI 脚本,JSP 脚本,servlets,ASP 脚本,服务器端 JavaScript,或者一些其它的服务器端技术等),然后返回后台程序处理产生的结果作为响应。

HTTP的响应报文也由三部分组成(响应行+响应头+响应体

HTTP/1.1 200 OK // 响应行
// 响应头
Server: nginx/1.4.6 (Ubuntu)
Date: Wed, 20 Jan 2021 07:28:09 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 406
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, PUT, POST, DELETE, PATCH, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
ETag: W/"196-Ay8U/71Rt0EbDzvYIuK2YtXe7xE"

{"status":"ok","msg":"登录成功","data": //响应体 {"id":1,"username":"hunger","avatar":"https://avatars.dicebear.com/api/human/hunger.svg?mood[]=happy","createdAt":"2020-09-17T03:03:55.803Z","updatedAt":"2020-09-17T03:03:55.803Z"},"token":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imh1bmdlciIsImlkIjoxLCJpYXQiOjE2MTExMjc2ODksImV4cCI6MTYxMTM4Njg4OX0.dcO4DTvWAVYPPL5do3j9zyfa48-69j157iAiXae5yrw"}

常见的HTTP状态码

信息响应

100 Continue

这个临时响应表明,迄今为止的所有内容都是可行的,客户端应该继续请求,如果已经完成,则忽略它。

2xx成功响应

200 OK

请求成功

201 Created

该请求已成功,并因此创建了一个新的资源。这通常是在POST请求,或是某些PUT请求之后返回的响应。

202 Accepted

请求已经接收到,但还未响应,没有结果。意味着不会有一个异步的响应去表明当前请求的结果,预期另外的进程和服务去处理请求,或者批处理。

206 Partial Content

服务器已经成功处理了部分 GET 请求。类似于 FlashGet 或者迅雷这类的 HTTP 下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。该请求必须包含 Range 头信息来指示客户端希望得到的内容范围,并且可能包含 If-Range 来作为请求条件。

3xx重定向

这类状态码代表需要客户端采取进一步的操作才能完成请求。

301 Moved Permanently

被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。

302 Found

请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。

304 Not Modified

如果客户端发送了一个带条件的 GET 请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。304 响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。

305 Use Proxy

被请求的资源必须通过指定的代理才能被访问。Location 域中将给出指定的代理所在的 URI 信息,接收者需要重复发送一个单独的请求,通过这个代理才能访问相应资源。只有原始服务器才能建立305响应。

4xx客户端错误

这类的状态码代表了客户端看起来可能发生了错误,妨碍了服务器的处理

400 Bad Request

1、语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求。

2、请求参数有误。

401 Unauthorized

当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。

403 Forbidden

服务器已经理解请求,但是拒绝执行它。与 401 响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。

404 Not Found

请求失败,请求所希望得到的资源未被在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下。

405 Method Not Allowed

请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow 头信息用以表示出当前资源能够接受的请求方法的列表。 鉴于 PUT,DELETE 方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回405错误。

服务端响应

表示服务器无法完成明显有效的请求。这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理

500 Internal Server Error

服务器遇到了不知道如何处理的情况。

501 Not Implemented

服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求。

502 Bad Gateway

作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。

503 Service Unavailable

服务器没有准备好处理请求。 常见原因是服务器因维护或重载而停机。 请注意,与此响应一起,应发送解释问题的用户友好页面。 这个响应应该用于临时条件和 Retry-After:如果可能的话,HTTP头应该包含恢复服务之前的估计时间。 网站管理员还必须注意与此响应一起发送的与缓存相关的标头,因为这些临时条件响应通常不应被缓存。

504 Gateway Timeout

当服务器作为网关,不能及时得到响应时返回此错误代码。

505 HTTP Version Not Supported

服务器不支持请求中所使用的HTTP协议版本。

四步挥手

image-20210714092256880

  1. 客户端发送一个 FIN ,seq是u,告诉服务器想关闭连接。

  2. 服务器收到这个 FIN ,发回一个 ACK,seq是v,应答号ack是u+1。

  3. 服务器通知应用程序关闭网络连接,应用程序关闭后通知服务器。服务器发送一个 FIN (FIN=1,ACK=1,seq=w,ack=u+1)给客户端 。

  4. 客户端发回 ACK ,ACK = 1 seq=u+1 ack = w+1报文确认。

为什么挥手要四步

这是因为服务端的 LISTEN 状态下的 SOCKET 当收到客户端建立连接请求的SYN 报文后,它可以把 ACK 和 SYN ( ACK 起应答作用,而 SYN 起同步作用)放在一个报文里来发送。但关闭连接时,当服务器收到客户端的 FIN 报文通知时,服务器只能发一个回应报文ACK:“哦,我知道了”,然后通知应用程序。应用程序完成全部数据发送并确定可以终止了,服务器才能发送FIN告诉客户端可以真正断开连接了。所以这一步ACK报文和FIN报文需要分开发送,因此多了一个步骤。

HTTP 缓存有哪几种?

缓存流程图

缓存_副本.png

HTPP缓存有2中缓存方式:协商缓存强制缓存

强制缓存不用发请求直接用本地缓存,协商缓存要发请求去问服务器有没有更新。

协商缓存

ETag和If-None-Match

ETag是URL的Entity Tag,就是一个URL资源的标识符,类似于文件的md5,计算方式也类似,当服务器返回时,可以根据返回内容计算一个hash值或者就是一个数字版本号,具体返回什么值要看服务器的计算策略。然后将它加到responseheader里面,可能长这样:

ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

客户端拿到后会将这个ETag和返回值一起存下来,等下次请求时,使用配套的If-None-Match,将这个放到requestheader里面,可能长这样:

If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

然后服务端拿到请求里面的If-None-Match跟当前版本的ETag比较下:

  1. 如果是一样的话,直接返回304,语义为Not Modified,不返回内容(body),只返回header,告诉浏览器直接用缓存。
  2. 如果不一样的话,返回200和最新的内容

ETag配套的还有一个不太常用的request header ----If-Match,这个和前面If-None-Match的语义是相反的。前面If-None-Match的语义是如果不匹配就下载。而If-Match通常用于post或者put请求中,语义为如果匹配才提交,比如你在编辑一个商品,其他人也可能同时在编辑。当你提交编辑时,其他人可能已经先于你提交了,这时候服务端的ETag就已经变了,If-Match就不成立了,这时候服务端会给你返回412错误,也就是Precondition Failed,前提条件失败。如果If-Match成立,就正常返回200

小总结:

Etag相当于给资源打个标记生成“独一无二”的指纹。当文件在服务端被修改时,Etag就会改变。其作用和Last-Modify类似。在现实环境中,这个独一无二并不严谨。

Last-Modified & If-Modified-Since

Last-ModifiedIf-Modified-Since也是配套使用的,类似于ETagIf-None-Match的关系。只不过ETag放的是一个版本号或者hash值,Last-Modified放的是资源的最后修改时间。Last-Modified是放到responseheader里面的,当浏览器向服务器请求资源,服务器给出响应时会带上资源的修改时间,可能长这样:

Last-Modified: Wed, 21 Oct 2000 07:28:00 GMT

而客户端浏览器在使用时,应该将配套的If-Modified-Since放到**requestheader**里面,浏览器下次向服务器请求该图片时会带上 If-Modified-Since: Wed, 21 Oct 2000 07:28:00 GMT 。服务器可根据请求的文件修改时间和真实的文件修改时间做比较,来判断资源是否过期。长这样:

If-Modified-Since: Wed, 21 Oct 2000 07:28:00 GMT

服务端拿到这个头后,会跟当前版本的修改时间进行比较:

  1. 当前版本的修改时间比这个晚,也就是这个时间后又改过了,返回200和新的内容
  2. 当前版本的修改时间和这个一样,也就是没有更新,返回304,不返回内容,只返回头,客户端直接使用缓存

If-Modified-Since对应的还有If-Unmodified-SinceIf-Modified-Since可以理解为有更新才下载,那If-Unmodified-Since就是没有更新才下载。如果客户端传了If-Unmodified-Since,像这样:

If-Unmodified-Since: Wed, 21 Oct 2000 07:28:00 GMT

服务端拿到这个头后,也会跟当前版本的修改时间进行比较:

  1. 如果这个时间后没有更新,服务器返回200,并返回内容。
  2. 如果这个时间后有更新,其实就是这个if不成立,会返回错误代码412,语义为Precondition Failed

ETag和Last-Modified优先级

ETagLast-Modified都是协商缓存,都需要服务器进行计算和比较,那如果这两个都存在,用哪个呢?答案是ETagETag的优先级比Last-Modified。因为Last-Modified在设计上有个问题,那就是Last-Modified的精度只能到秒,如果一个资源频繁修改,在同一秒进行多次修改,你从Last-Modified上是看不出来区别的。但是ETag每次修改都会生成新的,所以他比Last-Modified精度高,更准确。但是ETag也不是完全没问题的,你的ETag如果设计为一个hash值,每次请求都要计算这个值,需要额外耗费服务器资源。具体使用哪一个,需要根据自己的项目情况来进行取舍。

强制缓存

上面扯蛋那里的第三个例子和第四个例子就是强制缓存,就是我知道在某个时间段完全不用去问服务端,直接去用缓存就行。这两个例子里面提到的Expires是一个单独的headermax-ageimmutable同属于Cache-Control这个header

Expires

Expires代表资源的过期时间,就是服务器responseheader带上这个字段:

Expires: Wed, 21 Oct 2000 07:28:00 GMT

然后在这个时间前,客户端浏览器都不会再发起请求,而是直接用缓存资源。

Cache-control: max-age=过期秒数,Expires会被忽略

Cache-Control

Cache-Control相对比较复杂,可设置属性也比较多,max-age只是其中一个属性,长这样:

Cache-Control: max-age=20000

这表示当前资源在20000秒内都不用再请求了,直接使用缓存。

上面提到的immutable也是Cache-Control的一个属性,但是是个实验性质的,各个浏览器兼容并不好。设置了Cache-control: immutable表示这辈子都用缓存了,再请求是不可能的了。

其他常用属性还有:

no-cache:使用缓存前,强制要求把请求提交给服务器进行验证(协商缓存验证)。

no-store:不存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。

另外Cache-Control还有很多属性,大家可以参考MDN的文档

Expires和Cache-Control的优先级

就一句话:如果在Cache-Control响应头设置了 max-age 或者 s-maxage 指令,那么 Expires 头会被忽略。

协商缓存和强制缓存优先级

这个其实很好理解,协商缓存需要发请求跟服务器协商,强制缓存如果生效,根本就不会发请求。所以这个优先级就是:先判断强制缓存,如果强制缓存生效,直接使用缓存;如果强制缓存失效,再发请求跟服务器协商,看要不要使用缓存

HTTP是如何控制缓存的

  • 浏览器第一次向服务器发请求获取资源,服务器响应报文的状态码是200,响应头会带上 Cache-Control、Etag字段,响应体是原始资源。浏览器收到响应后把资源缓存在本地。

  • 当浏览器再次发送请求获取该资源时,浏览器先检查该资源是否过期(通过之前响应报文的Cache-Control:max-age=过期时间来判断)。如果在过期时间以内,直接使用该资源。

  • 如果时间过期,则发请求询问该资源是否依旧可用。请求包含头字段 If-None-Match ,是之前响应报文里的Etag。

  • 服务器收到请求后通过If-None-Match里的Etag和新计算的Etag做对比,如果匹配,则直接返回一个状态码为304,不包含响应体的报文,告诉浏览器该资源依旧可用。如果不匹配,则返回一个状态码为200带Cache-Control、Etag和原始资源的新报文。

  • 如果不存在Etag,则用Last-Modified和If-Modified-Since做类似的判断。

总结

HTTP缓存机制要点如下:

  1. HTTP缓存机制分为强制缓存协商缓存两类。
  2. 强制缓存的意思就是不要问了(不发起请求),直接用缓存吧。
  3. 强制缓存常见技术有ExpiresCache-Control
  4. Expires的值是一个时间,表示这个时间前缓存都有效,都不需要发起请求。
  5. Cache-Control有很多属性值,常用属性max-age设置了缓存有效的时间长度,单位为,这个时间没到,都不用发起请求。
  6. immutable也是Cache-Control的一个属性,表示这个资源这辈子都不用再请求了,但是他兼容性不好,Cache-Control其他属性可以参考MDN的文档
  7. Cache-Controlmax-age优先级比Expires高。
  8. 协商缓存常见技术有ETagLast-Modified
  9. ETag其实就是给资源算一个hash值或者版本号,对应的常用request headerIf-None-Match
  10. Last-Modified其实就是加上资源修改的时间,对应的常用request headerIf-Modified-Since,精度为
  11. ETag每次修改都会改变,而Last-Modified的精度只到,所以ETag更准确,优先级更高,但是需要计算,所以服务端开销更大。
  12. 强制缓存协商缓存都存在的情况下,先判断强制缓存是否生效,如果生效,不用发起请求,直接用缓存。如果强制缓存不生效再发起请求判断协商缓存

HTTP2.0好在哪里

Http1.x存在的问题

  • pipeling 传输方式浏览器在处理时有各自问题和bug,所以一般默认也未开启支持。另外对于大文件依旧会存在服务器阻塞。
  • 主流用的还是keep-alive,在一个连接里资源的请求是串行的。为了加快并行速度浏览器会开多个连接,一个域名默认最多开约6个连接,超过限制数目的请求会被阻塞。(所以一些网站静态资源使用了多个域名,但域名太多管理不便且域名解析也需要时间)
  • 只能客户端主动发起请求,不能服务器主动发起
  • 请求/响应首部太大了,未经压缩就发送,浪费
  • 每次请求/响应的首部大都是冗余的重复的内容
  • 数据压缩非强制,可能存在未经压缩的情况
  • 请求顺序没优先级,只能听天命(HTML资源顺序)
  • 客户端可以解析html发送一个个的资源请求,服务器也能啊

Http2.0的改进

  • 基于二进制流。 将一个TCP连接分为若干个流(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。
  • 多路复用(Multiplexing)。一个TCP连接,可以无限制处理多个请求
  • 请求可以设置优先级
  • 压缩Http首部
  • 服务器推送(Server Push) 。客户端发送获取HTML的请求,服务器把HTML以及HTML里需要的资源一起发过去
  • 服务器提示(Server Hints),preload 和prefetch。 浏览器会在空闲的时间加载这个大的图片,下次请求可能会用到

小知识 Preload与 Server Push

  • preload 预加载,告诉浏览器下一步立即要加载什么资源。
<link rel="preload" href="https://example.com/images/large-background.jpg">
  • prefetch 预加载,告诉浏览器下一步要加载什么资源。在空闲时加载。
<link rel="preload" href="https://example.com/images/music.mp3">

参考文章


前端剑指offer · 语雀 (yuque.com)

从URL输入到页面展现到底发生什么?

轻松理解HTTP缓存策略 - SegmentFault 思否