Serenader

Learning by sharing

HTTP Protocol

HTTP/0.9

  • 1991 年发布
  • 只有一个 GET 命令
  • 服务器只能返回 HTML 格式的字符串
  • 服务器传输数据结束即关闭TCP连接

HTTP/1.0

  • 1996年5月发布
  • 除了可以传输字符串之外,还可以传输图像、视频、二进制文件等。
  • 除了 GET 命令之外,还添加了 POST 命令 和 HEAD 命令
  • 服务器的响应除了内容之外还有包含响应头。
  • 头信息必须是 ASCII 编码,后面的内容可以是任意格式。因此有了 Content-Type 头来指定body里面的数据类型。
  • 也因此可以先将响应内容先经过处理压缩后再发送给客户端,客户端接收到之后再解压得到内容。这时候也就有了 Content-Encoding 响应头。客户端请求的时候一般都会带上 Accept-Encoding 来表示该客户端支持哪种压缩算法。
  • 缺点是每个TCP连接只能发送一个请求,请求完成之后连接就关闭。
GET / HTTP/1.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*
HTTP/1.0 200 OK 
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

<html>
  <body>Hello World</body>
</html>

HTTP/1.1

  • 1997年1月发布
  • 引入持久连接,即TCP连接默认不关闭,无须手动声明 Connection: keep-alive
  • 当客户端和服务端发现对方一段时间内没有活动时,就主动关闭连接。即发送 Connection: close 来关闭连接。
  • 增加了更多的HTTP方法,如PUT、PATCH、OPTIONS、DELETE等
  • 请求头有了 Host 字段,对于一台服务器来说可以托管多个网站了

HTTP/2

  • 2015年发布
  • 头部信息和数据内容均是二进制,并且统称为“帧”:头信息帧和数据帧
  • 多工:在一个连接里面,服务器和客户端可以同时发送多个请求和响应,而且不用按照顺序一一对应。这样就避免了对头堵塞。
  • 每个请求或响应的数据包都称为“数据流”。因为多工的原因,每个数据流都拥有一个独一无二的标识,用来区分当前数据哪个数据流。客户端发出的数据流ID为奇数,服务器发出的一律为偶数。数据流发送到一半时可以通过发送 “RST_STREAM” 帧来取消数据流的传送,同时不用关闭当前连接。客户端还可以指定数据流的优先级,优先级越高,服务器会越早响应。
  • 头信息也经过gzip或compress压缩。另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
  • 服务器可以主动向客户端推送资源。这个叫服务器推送。

一些常见的问题

GET VS POST

Here is the key point of GET request
  • GET requests can be cached
  • GET requests remain in the browser history
  • GET requests can be bookmarked
  • GET requests should never be used when dealing with sensitive data
  • GET requests have length restrictions
  • GET requests should be used only to retrieve data
and Here is the key point of POST request
  • POST requests are never cached
  • POST requests do not remain in the browser history
  • POST requests cannot be bookmarked
  • POST requests have no restrictions on data length
比较容易出问题的是有些后端开发者为了贪图方便,在编写接口的时候往往把获取数据以及修改数据的接口都使用了 GET 请求的方式来处理。但是这样有个很严重的问题,就是,GET 请求会被缓存,而且传参只能通过 URL的 Querystring 来传,而 URL 通常有长度限制,所以一旦需要传参的内容过大时,容易造成 URL 太长而请求失败的情况。(HTTP status code: 414. The request URL is too long)
而且在一些敏感数据的传输时,由于 GET 请求只能在 Query string 里面传参,所以会造成你的所有参数都显示地体现在 URL 上,一旦有人不小心把URL复制给其他人时,那么这些敏感数据就会被他人给获取到了。
POST 与之相反,数据的参数传递除了可以通过 Querystring 来传参,也可以把参数内容都放在 request body 里面。敏感数据一般都会放在 request body,这样对于用户来说是隐式的,即使复制链接也不会把敏感数据也复制出来。(尽管如此,要获取 request body 的内容还是很轻松的,只需要使用代理程序就可以一目了然地看到整个请求内容和响应内容)

Cookie

Cookie 一般用来携带用户信息,并且有区分域名。对于匹配的域名下 请求才会带上 cookie。但是,一旦有了 cookie 以后,所有的请求都会带上cookie。如果cookie内容过多的话,会造成浪费带宽的情况。所以 cookie 内容应该尽可能少。
而且cookie在跨域请求里面比较麻烦。

HTTP 缓存

Cache-Control 是最为重要的响应头,如果没有这个响应头,那么其他与缓存相关的响应头都不起效果。 也就是说 Cache-Control 是一个开关,指定了是否需要开启缓存。其值由两部分组成:
  • public/private: 如果使用了 public 的话,那么则表示该请求不仅仅在用户终端的客户端会缓存,还会在任何中间代理里面缓存。而 private 则反之,只会在终端客户端缓存,在中间代理里面不缓存。
  • max-age: 这个指定了缓存的具体时间。单位为秒
Cache-Control: public, max-age=60
以上表明该请求可以被中间代理以及终端客户端缓存,并且缓存时间为60秒。
Expires 响应头则是指定缓存过期的具体日期,其值是一个具体的日期,如:
Expires: Wed, 18 Jan 2017 07:33:11 GMT
当响应头同时有 max-age 跟 Expires 时,max-age 会优先生效。通常情况下,响应头应该还会有一个 data 的头,其值表明了当前客户端第一次请求该资源的时间。当有缓存策略存在时,Expires 响应头的值则是 Date 响应头的值加上 max-age 的值,如:
Cache-Control: max-age=86400
Date: Tue, 17 Jan 2017 07:33:11 GMT
Expires: Wed, 18 Jan 2017 07:33:11 GMT
这几个响应头表示第一次无缓存请求是在 Tue, 17 Jan 2017 07:33:11 GMT 这个时候请求的,并且该响应设置了86400秒(1天)缓存时间,所以过期时间则是 Wed, 18 Jan 2017 07:33:11 GMT
前面这种通过指定 max-age 或者 expires 来实现缓存的方法中,当浏览器第一次成功请求完成时则会将响应结果存储到本地缓存中。下次在未过期之前请求相同的资源时则直接从本地缓存里面获取,不会再发送 HTTP 请求。因此,这种情况你可以从 Chrome 的 Network tab 来看到请求响应码都是 200 ,而且 Size 那一栏会显示 from disk cache/from memory cache 。
/image/9dc23031-4304-4961-ad5a-df2178c86070/e9b5ca93-6997-4bc6-bcb5-04d1a4c67b7a_WX20170117-162222.png
除了上面这种缓存之外,还有另外一种叫做 Conditional requests 的请求。 Conditional requests 是浏览器即使有缓存时,下次请求该资源时仍然会再发送一个 HTTP 请求询问当前请求的资源是否有修改过。如果服务器端判断该资源并没有修改过,那么则会返回一个 304 Not modify 的响应,并且响应内容是空的,因为内容本身在客户端有缓存了。浏览器接收到304之后即直接从本地缓存取该资源。
Conditional requests 有两种类型,一种是基于时间的,即 Time-based:
只需要在响应头里面添加一个 Last-Modifed 响应头即可。如:
Cache-Control:public, max-age=31536000
Last-Modified: Mon, 03 Jan 2011 17:45:57 GMT
以上响应头告诉浏览器该资源最后一次修改的时间是 Mon, 03 Jan 2011 17:45:57 GMT ,下次浏览器再次请求该资源的时候会再发出一条请求,并且包含一个 If-Modified-Since 的请求头,如:
If-Modified-Since: Mon, 03 Jan 2011 17:45:57 GMT
服务器端接收到该请求后再判断该资源最后的修改时间是否与 If-Modifed-Since 相同,如果相同的话则返回 304 。
除了基于时间的 Conditional requests 之外,还有一种是基于内容的,即 Content-based:
与 Time-based 不同的是,响应头里面会包含一个 ETag 响应头,ETag是当前资源的内容的一个摘要,类似于 MD5。所以响应头是这样的:
Cache-Control:public, max-age=31536000
ETag: "15f0fff99ed5aae4edffdd6496d7131f"
浏览器下次请求该资源的时候则会带上一个叫 If-None-Match 的请求头,如:
If-None-Match: "15f0fff99ed5aae4edffdd6496d7131f"
如果ETag内容匹配,则返回304。
通过对比可以知道,以上两种类型的缓存的区别说到底是 from local cache VS 304 Not Modified
from local cache 不会发请求,只会在缓存过期后重新拉去新的资源。而 304 Not Modifed 则是需要再次请求服务端,让服务端验证当前资源是否有新的内容,以此来判断是否重新获取新的内容还是直接使用浏览器的本地缓存。
从加载速度来说,from local cache 会更快一些,毕竟不用再发多一次请求。但是对于那些需要及时更新本地缓存文件内容的请求来说,则需要使用 Conditional requests 这种缓存策略。

使用建议

  • 对于静态资源来说,应该设置一个非常长的缓存时间,让浏览器尽可能缓存文件,使得下次请求的时候直接从本地缓存取。当静态资源内容有更新时,应该直接更新静态资源的地址(文件名),强制让浏览器请求新的文件,而不是从本地缓存中取。
  • 对于需要及时校验文件内容的资源,应该尽可能采用 conditional requests 的策略来实现缓存。
  • 对于动态内容,管理员应该评估其内容大致更新的速度,再来设置缓存时间。这个并没有一个固定的值。
  • 对于需要禁止使用缓存的资源,应该指定 Cache-Control: no-cache, no-store 来停用任何缓存。这样会使得浏览器每次请求该资源的时候都直接从服务器端取。

同源策略

两个页面满足同源的条件是:协议相同,端口相同,域名相同。
另外,about:blankjavascript: 继承加载这些资源的页面的 origin。data: 的资源不同,自身会拥有一个空的安全的上下文。
另外,子域可以通过JS 设置 document.domain 来通过同源策略。如:
在子域 http://a.example.com/test.html 的页面中,通过 JS 设置 document.domain='example.com' ,则当前页面与 http://example.com/page.html 符合同源策略。

XHR 跨域请求

与图片、css等资源的跨域请求不同,JS的跨域请求由于有安全性问题,会被同源策略限制。如果要实现跨域请求则需要通过 CORS 策略来实现。
通过JS的API来请求资源有两种方式,一种是 XHR,一种是 fetch。
跨域请求一般分两种,一种是简单的请求,即:
  • 请求的方法是 GET POST HEAD 其中之一。
  • 除了浏览器自动带上的请求头(如Connection-Agent等)之外,只允许下面几种请求头:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
  • Content-Type 请求头的值只能是 application/x-www-form-urlencoded multipart/form-data text/plain 其中之一。
只要满足以上三个条件即称为简单的跨域请求。反之,如果有违背上面三条规则中的任意一条,那么即不是简单的跨域请求。非简单的跨域请求相对于简单的跨域请求来说区别在于,请求在发出去之前,浏览器会先发送一个 preflighted 请求,用来向服务器端确认接下来要进行的请求是否是被允许的。当服务器端成功响应该 preflight 请求后,真正的请求才会发出。
跨域请求(无论简单请求还是非简单请求)在发出时都会带上 Origin 请求头,用来表明当前请求资源的域名是哪一个。此时服务器端的响应头里面必须包含一个 Access-Control-Allow-Origin 并且该值匹配 Origin 请求头,这时候该跨域请求才有可能成功。否则一律失败。
Access-Control-Allow-Origin 是第一道门槛。其值的匹配规则是:
  • 如果其值是通配符 * 的话,则允许所有的域名进行跨域请求
  • 如果其值是指定的某个固定域名,那么只允许该域名进行跨域请求,其他域名将会失败
  • 如果其值是带有通配符的域名,如 *.example.com ,那么则允许该域名以及该域名的子域名进行跨域。
对于简单的跨域请求来说,通常只需要通过 Access-Control-Allow-Origin 这个响应头则可以请求成功(带 cookie 等情况先不考虑,会在下面讨论)。而当请求不是简单的跨域请求,情况就比较复杂。
首先,会先经过一个 preflighted 请求,具体过程是: 浏览器在发出实际的请求时,比方一个带有特殊的请求头 X-Custom-Header: value 的 GET 请求,浏览器会先发出一个 OPTIONS 请求来向服务器端确认当前要进行的请求是否是被允许的。该 OPTIONS 会带上以下请求头:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-Custom-Header
服务器端则会返回以下响应头来表示该操作是被允许的:
Access-Control-Allow-Headers:X-Custom-Header
Access-Control-Allow-Methods:GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Origin:http://example.com
当通过了 OPTIONS 请求后,浏览器才会继续发送真正的 GET 请求。
假设服务器端返回的响应头里面没有 Access-Control-Allow-Header 或者其值不匹配浏览器发送的 Access-Control-Request-Headers 的值,那么 OPTIONS 请求就会失败,接下来的 GET 请求就不会继续发送。
简单的说,OPTIONS 请求就是向服务器端检验当前请求的特殊条件是否是可以被允许的,如Access-Control-Request-Headers 以及 Access-Control-Request-Method ,然后服务器端返回可被允许的操作,如 Access-Control-Allow-HeadersAccess-Control-Allow-Methods 等。匹配成功的话,则会继续接下来的真正的请求,否则的话则会失败,接下来的请求不会被执行。
OPTIONS 请求是一个比较容易被人忽略的一个关键点,有一些后端人员在编写接口的时候,往往只知道在接口的响应头里面写入 Access-Control-Allow-Origin ,而没有意识到 OPTIONS 请求的存在。而即使有对 OPTIONS 请求做跨域允许的话,那么也很容易因为缺少相应的 Access-Control-Allow-HeadersAccess-Control-Allow-Methods 响应头导致请求仍然失败。
由于OPTIONS请求的存在,对于一个非简单请求来说,实际发出去的请求会有两个。这多多少少会浪费带宽,毕竟这个校验应该只会在第一次发生而已,一旦通过校验,在接下来的一段时间里,再次请求该接口的话,那么实际上OPTIONS 请求则没有必要再发出。好在,有个叫做 Access-Control-Max-Age 的响应头可以实现这样的功能。这个响应头指定了请求一旦通过了 preflight 请求之后,会在多长时间内无须再次触发 preflight 请求。从而达到减少实际请求,减少带宽浪费的问题。
默认情况下, 任何跨域请求都不会带上任何身份凭证的,即 cookie, 与身份认证相关的请求头,以及 TLS 客户端证书。然而,在大多数情况下,我们需要请求带上 cookie ,那么则需要开启跨域请求的 withCredentials 选项。
想要手动开启传输 cookie 的话,有以下方法;
  • XHR:为 XHR对象设置 xhr.withCredentials = true
  • fetch: 传入的参数选项里面开启 withCredentials fetch(url, { credentials: 'include' })
开启了 withCredentials 之后,请求头就会默认加上 Cookie 。除此之外,浏览器在接收到服务器端的响应头时会检查是否有 Access-Control-Allow-Credentials 的响应头,并且是否为真值。如果不是的话,那么会导致请求失败。因此,想要跨域请求带上 cookie 的话,不但要手动设置,还需要服务器端配置相应的响应头。
另外,一旦开启了 withCredentials 选项,服务器端的 Access-Control-Allow-Origin 响应头就不能是通配符,只能是固定的一个域名,否则会请求失败。
https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS