1. 从HTTP/0.9 到 HTTP/1.1
HTTP协议作为一个应用层协议,应该是出现频率最高的一个协议了。几乎所有的网站都是HTTP协议。从最初的HTTP,再到后来的HTTPS,其内部的版本主要经历了0.9,1.0,1.1以及未来将会普及的2.0。关于HTTP协议更详细的内容具体可参见每个版本对应的RFC,例如当前普及率最高的HTTP/1.1版本的RFC编号是2616。所谓RFC,即Request For Comments,是一种类似于业内共识的文本。
1.1 HTTP/0.9
- 仅支持方法:
GET
- 仅支持超文本类型
- 连接在得到回复后立即结束
- 不支持HTTP header
- 没有状态码(即400,408这些)
1.2 HTTP/1.0
- 支持方法:
GET
,HEAD
,POST
- 支持更多类型文件,包括脚本和媒体文件等
- 连接在得到回复后立即结束
- 支持HTTP header,包含一些请求/回复的信息,例如文本类型、上次修改时间等
- 支持状态码
至此,可以看到从0.9到1.0,有一个共同的问题没有得到解决,即每一次连接都会在得到回复后立即结束。这里我们默认双方使用的是TCP作为数据传输层协议,那么每一次连接都需要进行建立连接的三次握手阶段,这一阶段需要至少3倍的单向延迟时间(1.5个RTT),很明显当一个客户需要向服务器请求多个实体时这样频繁的建立连接是没有必要且十分耗时的。HTTP/1.1版本可以说主要是解决了这个问题。
1.3 HTTP/1.1
- 支持方法:
GET
,HEAD
,POST
,PUT
,DELETE
,TRACE
,OPTIONS
- 支持持久化的连接(keep-alive)
- 支持串联请求(pipelined requests)
- 支持并发处理请求(I/O多路复用)
2. 从HTTP/1.1 到 HTTP/2.0
虽然HTTP/1.1版本是目前流行的版本,但是它本身也有一些问题待解决。其中一个最主要的问题就是队头阻塞问题(Head-of-line Blocking)。这个问题指的是在持久化连接、尤其是串联请求的时候,如果前面有一个请求话费的时间较长(例如需要传输的文件较大),那么后面所有的请求都需要等待该请求完成后才能进行。
一个简单的例子,假设你在浏览一篇博客,但是这篇博客里面有一张很大的图片,并且你的带宽并不是很高的情况下,我们假设传输这张图片需要2秒。而恰巧,你的浏览器对这篇博客的HTTP串联请求,将这个图片放在了一开始的地方,那么此时,你的浏览器将会处于空白状态。2秒之后这张图片显示出来了,随即这篇博客的正文也显示出来了。但是其实这张图片并不是你关注的重点,你只是想看博客的文字内容。所以在这种情况下,之前等待的2秒是非常影响用户体验的。
HTTP/2.0 版本便主要针对这个问题,提出了一些解决方案。
- TCP连接多路复用
- 支持在同一个TCP连接内,采用多路复用(多个流,multiple streams)的方式,并发地传输串联请求所需要的文件,因此小文件不会被大文件阻塞
- 主要是通过将各个文件切分成小粒度的message,然后进行编号,让客户端能够进行文件重组
- 流优先级管理(Stream Prioritization)
- 这个是建立在多路复用的基础上的,可以使得服务端能够进一步定制页面的加载顺序,以满足定制化的用户需求(例如有的场景需要优先加载图片,有的场景则需要优先加载文本,等等)
- 服务器推送(server push)
- 在这之前,所有的HTTP请求都是以request for A –> response with A的形式存在的,即客户端要什么,服务端就给什么。但是实际应用场景,每个网页都是由很多文件内容组成的,并且会有一些依赖关系(例如一个HTML文件里面有一个图片的link,那么大概率客户端会请求那个图片)
- 服务器端便可以通过这种依赖关系来向客户端主动发送一些文件内容
- 请求头压缩
- HTTP/1.1其实也有对message进行压缩,是对message body,即对实际传输的文件(CSS、js脚本等)进行压缩
- HTTP/2.0引入的是对请求头信息的压缩,采用的是(HPACK)
3. HTTP的扩展
HTTP最初在设计上是去状态化(stateless)的,即HTTP服务器是不需要存储用户的相关状态信息的,去状态化最大的优势即能够大大降低服务器的载荷,一个连接在结束之后,与其相关的所有资源均可释放,使得服务器端的逻辑更简洁,这很符合HTTP协议的定位。但是自从1.1版本开始,随着持久化连接、串联请求以及并发请求的引入,更复杂的网站被建立起来,其中最主要的一个特征就是有着用户账号体系,需要登录的网站服务。因此,这些网站服务就需要跨请求传递信息的支持,例如网站服务需要辨认多个请求是否来自同一个账号(身份认证),或者网站服务需要对用户行为进行跟踪和分析,又或者网站服务需要为用户定制化内容等。Cookie和session的引入正是为了满足这些需求。
3.1 Cookie
Cookie可以理解为是HTTP服务器主动在用户侧存储的一些非敏感信息,以键值对的形式储存。
- 每一条cookie记录有着对应的作用域,当一个请求满足某条cookie的作用域时,浏览器就会自动将该cookie连同请求一起发送(作为请求头的一部分)。
- 每一条cookie记录有对应的生存周期,如果指定生存周期,那么浏览器就会将cookie储存起来(硬盘),否则该cookie会在当前会话结束(浏览器窗口关闭)后被自动删除。
- 不同的浏览器不会共享cookie,即便是同一个作用域下的cookie。
- 一个网站服务只能读取属于它的作用域内的cookie,这一点是由浏览器来保证的。
前面提到cookie的一个主要应用就是进行身份认证,也就通常的用户登录后的界面。而我们知道cookie是储存在客户端的,并且跨作用域访问限制是完全依靠浏览器实现的,因此,可以看出用cookie直接储存敏感信息是不可取的。那么既然如此,cookie又怎么能够实现身份认证这种需要敏感信息参与的任务呢?这就引出了session机制。可以说,session机制是建立在cookie机制之上的,并且session是需要在服务端存储相应信息的,这一点与cookie不同。
3.2 Session
这里用一个具体的例子来阐述session是如何建立在cookie的基础上运作的。
- 用户在浏览器上登录Facebook账号,采用的是POST方法,里面包含了用户名和密码(没有cookie参与)
- Facebook服务器收到该请求后会匹配用户数据库,如果匹配成功则用户成功登录。服务器会记录下该用户的登录状态,并分配一个唯一(对Facebook服务器端唯一)的编号(称为session id)给该用户账号
- 在回复中服务器会加入
Set-Cookie
头让用户端的浏览器将该编号作为cookie内容存储下来,通常会设置生存周期和服务器端的自动登出时间一致 - 这之后,如果用户访问Facebook的相关页面,浏览器就会自动将刚才包含session编号的cookie附加在请求头中
- Facebook再次收到该用户的请求后,会识别该session id,并与自身维护的已登录用户做匹配
- 如果匹配成功,那么Facebook服务器就完成了对用户身份的认证,随即可以为该用户输出定制化的页面,例如好友列表、好友动态等
注意:这里的身份认证其实有一些歧义,因为整个过程中,用户完成登录是不需要cookie或者session的介入的。而后续服务器端仅仅是识别该已登录用户的请求,所以称为身份识别可能会更精确一些。
不过从这一点也可以窥探出cookie或者session机制会带来的问题——即如果我获得了别人的cookie,是否意味着我可以假装别人获取已登录界面?一般来说服务端还会检查用户IP地址等,但是诸如此类也可以被伪装或者绕过。因此这个问题的回答倾向于是肯定的。所以,绝大多数网站都仅仅会依据session的匹配结果给出一些非敏感的个性化信息,例如商品推荐等。但是一旦涉及到类似下单、结算等比较敏感的操作,服务网站会采取更严格的措施,例如重新登录等,而不仅仅依靠cookie或者session信息。