http/1的出生
1989年3月,HTTP诞生了。
CERN(欧洲核子研究组织)的蒂姆 • 伯纳斯 - 李,提出了一种能让远隔两地的研究者们共享知识的设想。最初设想的基本理念是:借助多文档之间相互关联形成的超文本(HyperText),连成可相互参阅的 WWW(World Wide Web,万维网)。
1990 年 11 月,CERN 成功研发了世界上第一台 Web 服务器和 Web 浏览器。
http/0.9
1990年问世,没有依据这一版建立标准,该版本其实是代表了1.0之前的意思而已,可以理解为beta版。
http/1.0
1996年5月问世,并记载于RFC1949,第一个正式标准版本,目前还有一些服务器再用。
http/1.1
1997年1月问世,并记载于RFC2068,修订版记录在RFC2016.
h2是2015年正式通过,能看出来h1迭代缓慢。目前h/1.1还是大多数使用的版本。
简单理解http
为了区分开h2,以下模块先以http/1.1为主,理解http协议。
理解URI和URL
URI是统一资源标识符,URI有两种形式,分别是URL统一资源定位符和URN统一资源名。
在网络服务上每一个资源都有URI,就像身份证一样,URL就是身份证上的地址,通过这个地址就能找到这个资源。
一般咱们经常用的就是URL,URN没有流行起来。
绝对URI格式
[协议名]://[用户名]:[密码]@[主机名]:[端口]/[路径]?[查询参数]#[片段ID]
用户名和密码是登录服务器的用户名和密码,主机名也就是服务器地址,其他的可以随便拿一个地址去套用。对一对就差不多了,不扩展了。
理解http是什么
http协议跟TCP/IP协议族中众多的协议类似,都是用于客户端和服务端的通信。发请求的叫做客户端,回应请求的叫服务端。
不保存状态协议
HTTP 是一种不保存状态,即无状态(stateless)协议。HTTP 协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。
这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。不保存状态,可减少服务器压力和内存消耗。
但是就登录态来说,不可能没打开一个页面就要重新登陆一次,太不友好了。
于是,在http/1.1中引入了cookie。
理解cookie
如何保持无状态,还能让服务端认识区分客户端呢?
客户端每次请求的时候把自己的身份证带着是不是就可以了呢?
就像做核酸一样,护士不知道你是谁,她不认识也不记得。需要你带着身份证去,她扫你然后处理做核酸的事务。
cookie在每次请求就是这个身份证的作用。
设置cookie
Cookie 会根据从服务器端发送的响应报文内的一个叫做 Set-Cookie(Set-cookie: [name]=[value];) 的首部字段信息,通知客户端保存Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去。
expires
这个属性指的是cookie的有效期,可以用这个属性区分出两种cookie,会话级cookie和持久性cookie。
会话级就是换了浏览器就没了,持久性cookie是存储在硬盘里,退出浏览器,重启设备之后还是存在的,前提是没过期。
在浏览器cookie里可以看到这一项,如果是会话级,会有标识【会话】。
过期对比的时间是服务器时间,过了之后浏览器会自动删除失效cookie。
path
将服务器上的文件目录作为Cookie的适用对象(若不指定则默认为文档所在的文件目录)。
在浏览器cookie里可以看到这一项,一般都是默认【/】。
domain
通过 Cookie 的 domain 属性指定的域名可做到与结尾匹配一致。
如domain='xxx.com',那么对a.xxx.com和b.xxx.com发起请求时都会带上这个cookie。
secure
这个属性规定仅在https安全链接下才发送该cookie。
HttpOnly
这个属性规定不可用js脚本获取到该cookie。其主要目的为防止跨站脚本攻击(Cross-site-scripting,XSS)对 Cookie 的信息窃取。
理解HTTP报文
http报文分为请求报文和响应报文。这两种总的概括结构就是四部分,起始行、头部、空行和实体。
请求报文扩展开如下结构
响应报文扩展开如下结构
这么看结构差异只在起始行。
理解方法
方法的作用在于,可以指定请求的资源按期望产生某种行为。就是说,用对应的方法干对应的事儿。就像夹菜用筷子,喝汤用勺子。你要是说勺子也能吃菜,也没毛病,但是用筷子确实喝不了汤。
Get
代表获取资源,请求服务端下发某些资源。是安全方法,安全方法代表着执行这个操作不会对服务造成什么影响。
Head
跟get类似,但是响应报文中没有响应实体。是安全方法。
不获取资源可以干什么呢?
- 通过头部信息,了解资源类型。
- 查看状态码,了解访问状态。
- 通过头部信息,了解资源最近是否被更改过。
这两个方法就能说明夹菜喝汤的问题,Get其实可以把Head的事儿干了。如果是上述的三件事,明显Head更合适,没有了响应实体,各方面运算消耗肯定都会减少。
剩余方法列举
| 方法 | 说明 | 支持版本 |
|---|---|---|
| POST | 传输实体主体 | 1.0、1.1 |
| PUT | 传输文件 | 1.0、1.1 |
| DELETE | 删除文件 | 1.0、1.1 |
| OPTIONS | 询问支持的方法 | 1.1 |
| TRACE | 追踪路径 | 1.1 |
| CONNECT | 要求用隧道协议连接代理 | 1.1 |
| LINK | 建立和资源之间的联系 | 1.0 |
| UNLINK | 断开连接关系 | 1.0 |
理解状态码
状态码就是状态码,一个状态对应一个码。可以把现有的状态码分成5类,每一类挑出几个常见的学习一下。
1xx -- 信息性状态码
100 continue,说明服务端已经收到了初始请求,后边可以继续了。比如想避免客户端发送一个服务端无法处理的大实体的时候,可以使用的状态码。
需要服务端返回这个状态码的时候,客户端头部也需要有相应的操作,设置Expect: 100 continue。Expect设置的是期待服务端的特定行为,100 continue是其中一个值。
如果服务端收到了头部带Expect: 100 continue的请求,需要返回100 continue 或一条错误码来相应。没有收到头部标识,服务端是不能返回100的。
2xx -- 成功状态码
200 Ok,表示这个请求过来之后,服务端处理的很顺利,一切正常。
206 Partial Content,表示请求一部分,需要在请求头中设置Content-Range,指定范围。请求音视频会出现206.
3xx -- 重定向
301 Moved Permanently,永久性重定向,表示之前的完全换了新的地址了,之前的不会再用到了,这时会返回一个301 Moved Permanently 加上一个Location,浏览器会自动跳转到Location并且会查询本地是否有书签,如果有的话会自动更新。
302 Found,跟301不同的是,这是个临时性的重定向。不会更新书签,返回Location,只做重定向跳转。
304 Not Modified,虽然被划分到重定向中,其实跟重定向关系不大。请求指采用 GET方法的请求报文中包含 If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since 中任一首部。如果服务端发现手中的资源跟你的条件匹配不上,就会返回304,没有响应主体。
4xx -- 客户端错误
400 Bad Request,前端发出的请求导致服务端无法解析和响应,比如常见的参数格式传错了。
401 Unauthorized,请求这个资源需要验证身份。
403 Forbidden,服务端拒绝访问。
404 Not Found,找不到该资源。
5xx -- 服务端错误
500 Internal Server Error。后端bug了。
503 Service Unavailable。服务器反应不过来了,或停机了。
理解缓存
http缓存理解为两种:强制缓存和协商缓存。
强制缓存
在响应header中看到Expires或Catch-Control,就表示走的强制缓存。Catch-Control比Expires的优先级要高,Expires是http/1.0的产物,有点过时了,Catch-Control是http/1.1的。
Expires
在响应头中类似这样的信息 Expires:Sat,13 May 2022 07:00:00 GMT, 也就是在这个时间之前不用发起请求,直接走本地缓存,超过了这个时间再去重新请求。
Catch-Control: max-age=xxx
Catch-Control不提供绝对时间,而是设置一个相对时间,设置一个max-age=xxx(单位是秒),用这个时间与响应头的Date相加再与本地时间进行比较。
这两个强制缓存的判断都会受本地时间的影响,因为判断是否过期最终都是跟本地时间进行对比,max-age比Expires好的点就是不用一直去改动这个绝对值而已。我没想到还有什么别的有点,觉得工作方式大差不差。
协商缓存
协商缓存,顾名思义就是客户端跟服务端商量着来。
Last-Modified / If-Modified-Since(HTTP/1.0)
这一对是文件的修改时间,单位是秒。第一次响应之后,服务端会把这个这个时间从Last-Modified下发下去,下次请求在请求头带上If-Modified-Since这个时间值,服务端做对比,一样的话就返回304,走本地缓存,不一样的话下发新资源和新的修改时间。
Etag / If-Non-Match(HTTP/1.1)
正因为上边的修改时间单位是秒级,如果在一秒内修改多次的话,就会有问题了。Etag / If-Non-Match就可以解决一秒内多次修改的问题,Etag是根据文件生成哈西值,他的对比交互跟修改时间是一样的。只是Etag更精确。
如果以上头在响应中都包含,并且客户端均支持,那么优先级如下: Cache-Control > Expires > Etag > Last-Modified。
理解HTTP连接
一个连接
在http初始版本的通信非常简单粗暴,每通信一次,需要重新建立tcp连接。
在那时候传输量本来就小,重建就重建了,没啥大毛病。随着http大量普及,前端技术突飞猛进,web页面花里胡哨,需要加载的资源越来越多。初始版本的http就跟不上了。
并行连接
http允许客户端同时打开多个连接,并行执行http事务。打开多个连接是解决HTTP/1.1阻塞问题的最简单方法,这样可以。大多数浏览器默认最多6个。
这么看起来感觉确实变快了,之前请求6件事儿一个人干,现在6个人干6件事儿,但是雇六个人成本肯定会变大。客户端和服务器都有额外的开销:维护连接需要更多的内存和CPU资源。
重点是,在某些情况下并行连接不一定会变快,甚至没什么提升。比如在网络带宽比较小的情况下,当六个连接对象会平分带宽去已较慢的速度传输时就是这样的。在管理成本上升的情况下,没有提升传输效率,那就是负优化。
持久化连接
持久连接(HTTP Persistent Connections,也称为 HTTP keep-alive 或 HTTP connection reuse)的方法。持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。 持久连接是在1.1和部分1.0+出现的。
持久连接比并行连接好在降低了建立链接和慢启动拥塞的开销,把tcp连接保持在已经调谐的状态,时刻为http通信准备着。类似于,本来没有路,走着走着变成了路。
实现1.0的持久连接是客户端通过一条头部带有Connection: Keep-Alive请求保持连接打开状态的,如果服务端返回的头部也有Connection: Keep-Alive,则可以认为服务端支持并同意了保持这条连接打开状态。如果相应头没有,则会在响应结束后客户端会断开连接。
1.1的持久连接跟keep-alive工作机制不同的是,默认持久连接是激活状态的,如果在响应中明确带了Connection:close,客户端会把这个连接关闭,如果没有带会默认支持持久连接。
管道化连接
管道化连接,听到管道两字,顾名思义把请求在管道中依次发出,然后依次处理,依次响应。不需要等待上一个请求回来之后再将新请求发出。
管道化连接是在持久连接基础上进行的优化。减少了等待时间。
管道化实现过程比较坎坷,没怎么发展起来。
使用管道有几个限制:
- 在没确认该连接是持久连接时,不能使用管道
- 必须按照请求顺序发送响应,如果没有报文中没有序列标签,就没法跟请求匹配起来。
- 不能发送对服务有副作用的请求,如Post,Delete等。上一个请求刚删除了,下一个请求要修改,那么意味着第二个请求报错或永远不会执行。
https
先说一下【http】有哪些缺点
- 通信使用明文,可能被窃听
- 不验证通信方的身份,可能遭遇伪装
- 无法证明报文的完整性,有可能遭遇篡改
再说一下【s】
- 加了s就变成了超文本传输安全协议。
- s是TLS/SSL层,介于http和tcp中间。
https流程简述
简单理解一下流程。
客户端和服务端之间的通信怎么才能不被恶意窃听或篡改呢?
第一个办法,对称加密。两端用同样的密码加解密内容。这样第三方就不知道他俩在干啥了。
问题来了,这个共享密码在客户端和服务端之间怎么传递呢?如果在最开始密码就被劫持了,那么这次通信也就全公开了。
第二个办法,非对称加密。服务端发布一个公开密钥给客户端,自己手里留一把私有密钥。用这一对密钥进行加密通话,这样就不怕偷看了。
但也有问题,非对称加密比较慢,每次通信加解密非常耗时,获取到了公钥虽不能看到客户端说什么,但是能看到服务端说什么。
两个方法结合一下,用非对称加密通话商量出一个对称加密的密钥,然后用商量出来的这个对称加密的密钥再进行通信。这样只有他俩自己知道这个对称加密的密钥,既有了对称加密的速度,还保证了密钥的安全。
还有一个问题,怎么验证初始下发的公钥是可信任的呢?如果初始请求就已经被劫持了,那么连公钥都是第三方的了,更不要说后边的通信了。
解决这个问题需要引入权威的第三方机构CA。CA 负责核实公钥的拥有者的信息,并颁发认证"证书",同时能够为使用者提供证书验证服务。
基本的原理如下
- 数字证书认证机构用自己的私钥对服务端要下发的公钥进行签名并颁发公钥证书。
- 服务端把这个有签名有证书的公钥下发至客户端。
- 客户端用内置的数字认证机构的公钥对证书和签名进行验证。
- 验证没问题,用这个公钥进行后续的共享密钥协商通信。
Https通信建立步骤
步骤 1: 客户端通过发送 Client Hello 报文开始 SSL通信。报文中包含客户端支持的 SSL的指定版本、加密组件(我理解就是加密所需要的东西,没有深究)。
步骤 2: 服务器可进行 SSL通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的(保持一致)。
步骤 3: 之后服务器发送 Certificate 报文。报文中包含公开密钥证书。
步骤 4: 最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的 SSL握手协商部分结束。
步骤 5: SSL第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。报文中包含通信加密中使用的一种被称为 Pre-master-secret 的随机密码串。该报文已用步骤 3 中的公开密钥进行加密。
步骤 6: 接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。
步骤 7: 客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确解密该报文作为判定标准。
步骤 8: 服务器同样发送 Change Cipher Spec 报文。
步骤 9: 服务器同样发送 Finished 报文。
步骤 10: 服务器和客户端的 Finished 报文交换完毕之后,SSL连接就算建立完成。当然,通信会受到 SSL的保护。从此处开始进行应用层协议的通信,即发送 HTTP 请求。
TIPS:在两次hello中,客户端和服务端都会个字生成一个随机数发送给对方,这两个随机数加上Pre-master-secret共同生成通话密钥,就是那个对称加密的密钥。
关于https会不会变慢可以查看https的七个误解。
http2
千呼万唤始出来,2015年2月,http2.0被批准为正式标准。
http1.x存在的问题
队头阻塞
在请求应答过程中,如果出现任何状况,剩下所有的工作都会被阻塞在那次请求应答之后。这就是“队头阻塞”,它会阻碍网络传输和 Web 页面渲染。
h1的一个连接,它需要发起请求、等待响应,之后才能发起下一个请求。
管道化允许一次发送一组请求,但是只能按照发送顺序依次接收响应。而且之前也说过了,管道化限制挺多的,也没怎么用起来。
为了防止这个问题的发生,才有了之前说过的并行连接,但是每个连接还是会有对头阻塞这个问题,只不过是发生的概率变小了。
低效的TCP利用
tcp的设计思想是小心试探,保证公平。
tcp有两个概念:拥塞窗口和慢启动。
拥塞窗口指的是发送方一次能够发送的包的数量。
慢启动是计算这个窗口的过程,发送者在收到每个确认回复后额外发送 1 个未确认包。表示新连接在收到 1 个确认回复之后,可以发送 2 个数据包;在收到 2 个确认回复之后,可以发 4 个。
这个慢启动虽然可靠,但是比较耗时。
h1不支持多路复用,并行的6个连接每个都要走一遍这个慢启动的过程。
臃肿的header
h1 压缩了请求内容,但是消息首部却无法压缩。有时候消息头可比请求体大得多。而且好多都是每次都有的重复内容。
这些问题在h2中都得到了解决。
SPDY
在学习http2之前,有必要了解一下SPDY,是它推动了http的更新。(1.1从99年之后,没有真正意义上的改变)。
2009年,Google的Mike Belshe和Robert Peon宣布,他们在开发 一个叫作SPDY的新协议。他们已经在实验环境中验证了这个协议,结果很好,页面加载时间改善了65%。
SPDY是http的封装,最大的包装是把http基于文本封装成了SDPY的基于二进制。才能提出下面的主要优化点。
- 流多路利用 —— 请求和响应使用单个TCP连接传输数据,它们被分成不同的数据包,以流的方式分组。
- 请求优先级 —— 在同时发送所有请求时,为了避免引入新的性能问题,引入了请求优先级的概念。
- HTTP首部压缩 —— HTTP体早就可以压缩了,现在首部也可以压缩了。
2011年SDPY的正式发布证明了HTTP还有很大的优化空间,http工作组开始基于SDPY开展http2的规划。
核心改动
HTTP/2变成了一个完全的二进制协议,HTTP消息被分成清晰定义的数据帧。这个改动是http2的核心。当收到所有的数据帧后,可以将它们组合为完整的HTTP消息。
理解帧
帧是http2协议传输的最小单位,把报文分成了header frame和data frame。
Length: 表示帧负载的长度,Type 表示当前帧类型,Flags表示具体帧类型的标识(好像没啥用),R 保留位不需设置Stream Identififier 表示每个流的唯一 ID,Frame Payload 长度可变,表示真实的帧内容,长度是在 Length 字段中设置的。
多路复用
HTTP/2允许在单个连接上同时执行多个请求,每个请求或相应都是不同的流,每当有新的请求或相应的时候就会新建一个流,请求是奇数流ID,响应是偶数流ID,这个ID会记录在帧中并且是自增的,用完一个就抛弃一个,不会有重复的。
其实流是虚拟的,指的就是这些可双向流动的帧。多路指的就是就这这多个虚拟流。复用指的就是一条TCP连接。
流量控制
流量控制就是在接收方没准备好接受数据时或不准备接受数据时,要对发送方发送数据做一些限制。当手机下载处理速度很慢,但是网速和服务端处理速度比较快时。或者在把某一个视频播放暂停时。都需要慢慢降低发送数据速度甚至是停止。
在http1.x中,就一条连接在发送或接受,流量控制的事儿TCP就帮他做了,直接控制这条连接即可。 但是在h2中,多路中找到某一条需要控制的该怎么做呢?
当接收方收到数据并消费时,会发送一个WINDOW_UPDATE帧,用来控制窗口大小。发送方会根据这个窗口大小来决定发多少数据。
数据流优先级
在带宽有限的情况下,设置数据流的优先级会极大提高用户的体验。
客户端和服务端可以利用HEADERS帧提前告知或用PRIORITY后续沟通优先发送那些内容。但是这些对前端来说是不透明的,每个浏览器实现也是不一样的。
大概优先级是这样的HTML > CSS > Blocking Script > Font >= Image >= Async Script。
首部压缩
HPACK是首部压缩的方法, 是一种表查找压缩方案,它利用霍夫曼编码获得接近 GZIP 的压缩率。
简单理解HPACK
HPACK充分利用了头部大部分都是重复的这个特性。客户端为发出请求所带的头部信息创建一张表,服务端收到这些头部也创建一张表,这两张表是一样的。
当发送表里已有的头部信息时会直接映射成表中索引发出。
实际维护有两张表,一张静态的和一张静态的。静态里面有61个常用头部,比如固定的一些请求方法等。动态就如上添加的这些,所以是从62开始的。
服务端推送
提升单个对象性能的最佳方式,就是在它被用到之前就放到浏览器的缓存里面。这正是HTTP/2 的服务端推送的目的。
在客户端请求一个html回去之后,发现它可能还要跟这个html相关联的js和css,然后主动推送给客户端,并不是直接推送到页面,而是推送到客户端缓存中,等到客户端用的时候直接从缓存中拿到,性能也就提高了。
服务端推送跟socket还是有区别的,一旦stock建立起来,服务端和客户端可以随意通信,谁主动都行。
但是服务端推送是必须依赖客户端的某一个初始请求,在客户端没有初始请求的时候是不能主动推送的。
在推送的时候需要依赖某一个初始请求的流ID,包装一个PUSH_PROMISE帧,客户端在收到这个帧的时候可以进行验证,如果不满意是可以发送一个RST_STREAM帧来拒绝这个这个主动推送。也可以在发送请求的时候直接设置禁止主动推送。
一般可以在Nginx上配置,推送一些不经常改变的文件,比如通用样式,通用工具包等。
其实还可以再写写quic等! 再说吧!!
参考引用
《图解Http》上野 宣
《Http2 in action》Barry Pollard
《http权威指南》Gourlay.D
《http2基础教程》Stephen Ludin Javier Garza
SSL/TLS协议运行机制的概述--阮一峰