1. HTTP发展史
HTTP/0.9 - 单行协议
http 0.9
版本很简单,因为请求指令只由单行构成,所以被称之为单行协议
,请求命令由:GET
+url
构成,例如:
GET /index.html
没有请求头等描述信息,服务器接收或者发送完数据后立马关闭TCP
连接。
HTTP/1.0 - 构建可扩展性
增加了其他的请求方式,状态码,请求头等信息,增加了多字符集支持,多部分内容发送(发送多个文件时会对内容进行拆分),权限验证,缓存控制等功能。另外,在请求头的帮助下,http
能够传输除了纯文本之外其他类型文档的能力。这时,一个典型的请求如下:
// request
GET /index.html HTTP/1.0
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)
//response
200 OK
Date: Tue, 15 Nov 1994 08:12:31 GMT
Server: CERN/3.0 libwww/2.17
Content-Type: text/html
HTTP/1.1 - 标准化的协议
1997年初,http 1.1
标准发布,该标准消除了大量的歧义,并且引入了多项改进:
- 增加了
connection
,使得tcp
连接持久化,节省了频繁建立tcp
连接的开销 - 增加了
pipelining
技术,允许在同一tcp
连接里面按发送多个http
请求,并且按照请求顺序返回内容 - 增加了缓存机制
- 增加了内容协商机制
- 增加了
host
头,使得同一服务器上可以部署不同的服务
下面是一个http 1.1
的典型请求:
GET /en-US/docs/Glossary/Simple_header HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://developer.mozilla.org/en-US/docs/Glossary/Simple_header
200 OK
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Wed, 20 Jul 2016 10:55:30 GMT
Etag: "547fa7e369ef56031dd3bff2ace9fc0832eb251a"
Keep-Alive: timeout=5, max=1000
Last-Modified: Tue, 19 Jul 2016 00:59:33 GMT
Server: Apache
Transfer-Encoding: chunked
Vary: Cookie, Accept-Encoding
HTTP/2 - 为了更优异的表现
http 2
主要解决了一些安全性问题和优化了管道复用技术,与之前的版本相比较:
-
http 2
是二进制协议而非文本协议,所有的报文内容被封装成帧,不再可读,也不能无障碍的手工创建 - 同一
tcp
连接可以并行发送和接受http
请求,移除了顺序和阻塞的约束 - 压缩了
headers
,因为headers
在一系列的请求中往往是相似的 - 允许服务端主动推送数据到客户端
TCP三次握手和四次挥手
因为http
协议是应用层协议,是建立在tcp
协议之上的,所以我们有必要了解下tcp
连接的创建过程。
三次握手
假定 A
为客户端,也就是请求的发起者,B
为服务端,一次tcp
建立的过程应该是:
// seq, ack(acknowledge,确认码) 均存在TCP报文的首部中,占4个字节,
// seq(sequence number,随机码) 是请求时随机生成的,
// syn = 1的意思是 syn(synchronous,建立码)这个标志放在了第一位
A (syn=1, seq=x) ==> B
A <== B (ack=x+1, syn=1, seq=y)
A (ack=y+1, seq=z) ==> B
四次挥手
因为tcp
连接是全双工的,所以断开连接时得双方都确认关闭,具体流程如下:
// fin(finish,结束码)
A (fin=m) ==> B
A <== B (ack=m+1)
A <== B (fin=n)
A (ack=n+1) ==> B
2. CORS和CORS预请求
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
出于安全考虑,浏览器会限制从脚本内发起的跨域 http
请求,例如xhr
和fetch
,这意味着一个web应用程序只能加载来自同一个域的资源。
解决跨域的办法
解决跨域有很多种办法,例如jsonP
,或者代理,但是这些和http
协议并不相关,这里主要介绍如何配置headers
来实现跨域。**跨域资源共享标准新增了一组http
首部字段,可以声明允许跨域的源和方法等。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET
以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST
请求),浏览器必须首先使用 OPTIONS
方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。
CORS预请求
简单请求不会触发预请求,对于简单请求,这里是这样定义的:
- 请求方法仅限于
get
,post
,head
- 请求头内不应该除了以下字段(
Accept
,Accept-Language
,Content-Language
,Content-type
,DPR
,DownLink
,Viewport-Width
,Width
)之外的头信息 -
Content-type
仅限于text/plain
,multipart/form-data
,application/x-www-form-urlencoded
预请求得到正确的响应后,浏览器才会发送真正的跨域请求,响应结果示例:
其中Access-Control-*
类的头信息可以在服务端设置,Access-Control-Allow-Headers
表示服务器可接受的头信息中的字段,Access-Control-Allow-Methods
表示可接受的请求的方法,Access-Control-Allow-Origin
表示可以接受访问的域名(这里不建议写*
,会埋下隐患),Access-Control-Max-Age
表示该预请求的缓存有效时间,可以避免重复发送预请求。这里需要补充几点: - 每个浏览器可支持的最大的
Access-Control-Max-Age
可能都不同,FireFox
支持最长24h
,而Chrome
只支持最长10min
- 每个预请求的缓存的内容只对该请求的
url
有效,如果发送其它请求,尽管上个预请求没过期,这里仍然会发送预请求
3. Cache-Control和验证头
重用已获取的资源能够有效的提升网站与应用的性能。Web 缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间。借助 HTTP 缓存,Web 站点变得更具有响应性。
Cache-Control头
这个头是服务器用来声明该资源的可缓存性的,它的值有以下几种情况:
-
public
,公有缓存 表示任何请求节点,包括发起请求的客户端,代理服务器等都可以缓存该资源 -
private
,私有缓存 表示该资源是私有的,只有发起请求的客户端才能缓存该资源 -
no-cache
,强制确认缓存表示在此种方式下,每次发出该请求,浏览器都会带有该缓存的验证头想服务器确认是否可以使用该缓存,如果服务器返回304
,则表示可以使用该缓存副本。 -
no-store
,禁止缓存 任何请求的节点不得缓存该资源。
但其实,这些头都是一些声明性的头,不具有强制约束力,主流浏览器都会遵从该协议,但是一些中间代理可就不一定了~
缓存的生命周期
-
Max-Age = 1000
,设置有效时间1000秒 -
S-MaxAge = 1000
,这是专门给代理节点设置的,如果同时存在该值和上述值,则该值对代理生效,而上述值对客户端生效 -
Max-Stale = 1000
,请求头内携带该内容,表示尽管服务端设置的缓存有效期到了,客户端依旧会使用该缓存
举个栗子:
// node语法
response.writeHeader(200, {
'Cache-Control': 'Max-Age=86400, public' // 设置有效期24小时,公共缓存
})
验证头信息
验证头一般出现在缓存方式为Cache-control: must-revalidate
的情况下,因为该类资源可能会频繁改变,所以为了确保客户端的资源是具有时效性的,服务器会要求客户端在使用该缓存副本的时候向浏览器确认该缓存是否可用。
-
Last-Modified
直译就是上次修改的时间,这个是在response header
里面的,针对该缓存,浏览器再再次请求该内容时会携带头信息If-Modified-Since
或者If-Unmodified-Since
,来想服务器确认该资源是否修改了。 -
ETag
即一个类似于数字签名的标志,服务器在响应该请求时会返回资源的ETag
,下次请求时会在请求头中加入If-Match
或者If-None-Match
在上述请求得到200 OK
或者304 NotModified
的返回时表示浏览器可以使用本地缓存,同时304
的响应头还可以更新cache
的有效时间。
4. Cookie和Session
HTTP Cookie(也叫Web Cookie或浏览器Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie使基于无状态的HTTP协议记录稳定的状态信息成为了可能。
我们一般在服务端设置Cookie
,例如:
res.writeHeader(200,{
'Set-Cookie': ['token=123; max-age=10', 'name=bing; HttpOnly', 'id=123; domain=test.com'] // 多个cookie元素使用数组来表示(node js)
})
在Node.js
中我们一般通过数组来一次性设置多个Cookie
,max-age=10
表示该Cookie
的有效时间为10s
,如果不设置过期时间,浏览器会默认该Cookie
的过期时间就是浏览器关闭的时间;HttpOnly
表示该Cookie
仅能被Http
请求读取与使用,无法通过JavaScript
读取,提供了一定的安全性保证;domain=test.com
表示该Cookie
所属的域是test.com
,我们不能跨域访问Cookie
;另外,如果我们想要达到这样的效果:同一个一级域名下的二级域名都可以访问到主域名的Cookie
。那我们只需要在访问主域名的时候设置了Cookie
,在访问二级域名的时候就可以获取Cookie
了。另外补充一点就是POST
请求是不会携带Cookie
的。
Session
的意思就是会话,我们可以通过Cookie
来记录当前登录状态,当我们意外关闭网站后,下次打开该网站,仍然能够回到上次登录的地方,就好像没有离开过一样,这就是一种Session
的实现方式。
5. Http长链接以及数据协商
持久连接
曾经见到过一个很有意思的面试题:当一个页面需要同时加载20张图片时,浏览器是如何处理的?看了这部分内容相信你会有答案的。我们前面讲过,从http/1.1
开始支持持久连接,即一个tcp
连接在完成一次http
请求后不会立即被服务器关闭,后面的http
请求可以复用该连接,这里涉及到一个请求头中间的关键字Connection
,它只有两个值:
-
keep-alive
在一定时间内不会关闭连接 -
close
在一次请求后立马关闭连接
所以,如果浏览器需要同时请求20张图片,那么此时会并发尽可能多的http
请求,而http
请求是建立在tcp
连接之上的,所以我们会尽可能多建立tcp
连接,但这样对服务器很不友好,所以浏览器规定对某一个域产生的tcp
连接不得超过一定数量,Chrome
规定的是6个连接,也就是说我们会同时和服务器建立6个tcp
连接(如果之前不存在tcp连接),然后同时发送6个http
请求,然后浏览器会等待这6个请求的返回,当某一个请求返回之后也就是当前tcp
连接空闲了之后才会发起新的http
请求,这样说可能会和http/1.1
的管线复用技术相悖,这是因为一些阻塞和时序性问题,代理服务器很难处理类似请求,所以一般情况下浏览器默认关闭该功能!
数据协商
顾名思义,数据协商就是客户端和服务端交互数据时会定一些规则,来确保交互的信息都满足双方的要求,该功能主要通过Content-Type
实现:
请求头
-
Accept
,告诉服务端我需要什么样的数据 -
Accept-Encoding
,支持的压缩方式 -
Accept-Language
,支持的语言 -
User-Agent
,可以通过该字段来知道客户端的设备,操作系统,浏览器内核,渲染引擎等
响应头(与请求头对应的)
-
Content-Type
,返回内容的数据格式,比如 text/plain, text/html, text/javascript, application/json (主类型 / 分类型) -
Content-Encoding
,编码方式 -
Content-Language
, 语言例如 zh-CN -
x-Content-Type-Options
:noSniff
, 这个是告诉浏览器不要去猜测数据格式,按照返回的格式来解析内容,主要是为了安全性。
这里有一个压缩方式的例子:
const zlib = require('zlib')
const html = fs.readFileSync('test.html')
res.writeHeader(200, {
'Content-Type': 'text/html',
'Content-Encoding': 'gzip'
)
res.end(zlib.gzipsync(html)) // 把这个html压缩后发送, 可以节省传输时间,提高性能
6. 重定向
当我们在网络上的资源更换位置后,原有的请求应该被重定向到新的地址,比如:
res.writeHead(302, { // 注意这个status code 一定要写对,302 指的是我们临时跳转到这个url,301表示永久跳转到搞路由
'Location': '/new' ,// 因为是同域跳转,这里写新的路由就可以了
})
这里需要注意的就是要合理使用状态码,如果状态码为301
,那么这个时候浏览器会把新的地址缓存起来,接下来每次访问该地址都会被重定向到被缓存的地址,除非用户手动清除缓存;如果状态码为302
,表示临时重定向,下次浏览器请求该资源时还是会先请求重定向之前的旧地址。