两个月前(2020年11月12日)发布的macOS Big Sur中safari 14默认支持了HTTP/3,有没有突然觉得自己的ToDoList又悄悄变长了一大截。
下面我们对HTTP协议及相关进行一下完整讨论。
概述
Hypertext Transfer Protocol (HTTP) 中的hypertext这个术语是1965提出的,指的是展示在计算机或者其他电子设备上的文本,这些文本包含指向其他文本的引用(即超链接),后来也被用来表示带有超链接的表格、图片等资源。
说到这里我们就知道了超文本传输协议的真正意义,即用来传输各种资源的的协议,类似的还有Hypertext Markup Language (HTML),一种表示超文本的标记语言。
Tim Berners-Lee在1989年提出WorldWideWeb项目,包含HTTP、HTML、URL、web浏览器、Web服务器等技术和程序,后来成了我们熟知的world wide web,并且为了标准化相关技术组建了World Wide Web Consortium (W3C) 。
第一个版本的http协议是http/0.9,只有一个方法get,用来获取html文档。
1996年的http/1.0通过引入header和新的method对其进行了扩展。第二年http/1.1发布(正式发布前已经有超过65%的浏览器实现了对应功能),2007年http工作组对http/1.1规范进行了修正和阐明,并公布了以下六部分来代替之前的规范。
- RFC 7230 HTTP/1.1: Message Syntax and Routing
- RFC 7231 HTTP/1.1: Semantics and Content
- RFC 7232 HTTP/1.1: Conditional Requests
- RFC 7233 HTTP/1.1: Range Requests
- RFC 7234 HTTP/1.1: Caching
- RFC 7235 HTTP/1.1: Authentication http/1.1规定了我们使用的主要语法和语义,之后的版本都是对该版本的补充。
2015年5月HTTP/2作为 RFC 7540发布(后一个月发布了ES6)。
HTTP/3当前处于草案阶段,不过已经被多个浏览器实现。
本文除了http协议本身,还包括
- 加密版本的http,即HTTP Over TLS,也就是我们熟悉的https。
- 为http加密而使用的TLS协议
除非另外提供链接,本文内容参考mdn
1 术语
通过一些术语来了解一下http相关的架构,参考rfc7230#section-2
1.1 Client/Server Messaging
http是一种无状态 请求/响应 协议,通过在可靠的传输层连接交换信息来操作。一个http client是一个向server建立连接发送request的程序。一个http server是一个接受http连接发送响应的程序。
client和server两个术语只表示一次特定连接的角色,一个程序可以在一些连接做client,另一些连接做server。术语user agent表示各种任何发起请求的client程序,比如浏览器、爬虫、命令行工具、移动app等。术语origin server指的是可以为指定target resource提供可信的响应。术语"sender" and "recipient"表示任何发送和接收消息的实现。
http使用Uniform Resource Identifier (URI) standard表示target resource和resource之间的关系。被传递的message的格式类似于邮件传输的Multipurpose
Internet Mail Extensions (MIME),但略有区别。
一个请求消息以一个请求行开始,包含一个method,URI,和协议版本;后面跟着headers字段和一个空行表示header部分的结束;最后是一个body包含可能的payload。
一个响应以一个状态行开始,包含协议版本、成功或失败code、描述性短语;后面跟着headers字段和一个空行;最后是一个body.
一次连接可能被多次请求/响应使用,一次典型的会话如
// Client request:
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
// Server response:
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
Hello World! My payload includes a trailing CRLF.
1.2 Intermediaries
有三种http中介,proxy,gateway和tunnel。http通过中介的使用可以使一个请求通过一个链式连接。有时候一个单独的中介可以作为origin server,proxy或tunnel,会基于每个请求的情况切换不同的表现。
> > > >
UA =========== A =========== B =========== C =========== O
< < < <
上图在user agent和origin server中有三个中介(A,B,C),每个请求或相应信息都会通过四个单独的连接,有些通信选项可能只适用于其中部分连接。尽管这个图是线性的,每个参与者可能参与多个同时进行的通信,比如B可能收到很多不是来自A的请求,然后转发给其他服务器,而不是C;同样,后续的请求可能按照不同的连接路径发送,这种情况经常是基于负载均衡的动态配置。
术语upstream 和 downstream被用来描述信息流的方向,术语inbound和outbound用来描述与请求路由有关的方向,其中前者是指向origin server,后者是指向user agent。
一个proxy是一个信息转发agent(可以用来转换各种应用层协议),作为一个中介去代替user agent发出请求,可以用来做缓存或者隐蔽个人信息(图源)。
gateway(也叫reverse proxy)是一个为outbound连接扮演origin server的中介,而且会将收到的请求传递到其他server。gateway经常用来封装遗留的或者不信任的服务、通过增加缓存提高服务器性能以及partitioning or load balancing。
一个tunnel在两个连接中进行盲转发,不会修改消息的任何内容。当两端的连接关闭,tunnel也就不存在。tunnel被用于扩展一个虚拟连接,比如在tls中用来通过防火墙代理。
1.3 caches
cache是之前响应信息的本地存储和控制信息存储、获取和删除的子系统。一个cache会缓存可缓存的响应来减少响应时间和网络带宽消耗。任何client或server都可以使用缓存(除了作为tunnel时)。
如果一个请求/响应连接的某个参与者保存的缓存应用于请求,整个连接就会变短。比如B中保存着之前O相应的内容,且A和UA中没有缓存,就会如图所示
> >
UA =========== A =========== B - - - - - - C - - - - - - O
< <
如果一个响应的副本可以被缓存来响应后续的请求,则称该请求是可缓存的。即使一个响应是可缓存的,仍然可能有另外的限制影响缓存的应用。
2 连接管理
在http/1.1 有多个连接模型:短连接、持久连接和http pipelining。
http主要依赖tcp作为传输协议来提供client和server之间的连接。一开始http使用单一模型来处理连接,这些连接是短连接,每当一个请求需要发送时都会创建一个新连接,一旦收到响应就会关闭连接,开启每个tcp连接是一个很耗资源的操作。
http/1.1 引入了两个新的模式,持久连接模式会保持成功的连接。http pipeling 不需要等响应直接发送多个请求
注意连接管理只用于两个节点之间的,而不是端到端的。
三种模式中第一种浪费性能,第三种在实践中具有局限性,因此在浏览器中一般默认关闭。第二种用Connection header表示,默认长连接(keep-alive)
3 CORS
Cross-Origin Resource Sharing (CORS)是一个基于http header的机制,服务器可以用来指示其他origin的浏览器可以加载资源。想了解跨域更多可以参考之前html系列文章中对origin的讨论。
cors也需要浏览器向目标服务器做一个option方法的preflight请求来检查是否允许实际的请求,在这个preflight中浏览器发送header来表明在实际的请求中方法和header。
3.1 相关响应header
- Access-Control-Allow-Origin: | * 指定允许访问该资源的origin,如果是*,则不允许携带身份凭证
- Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header 在跨域访问时,客户端只能获取一些基本的响应头,包括Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要让客户端获取其他的需要用这个header加入白名单。
- Access-Control-Max-Age: <delta-seconds> 指定preflight请求结果可以被缓存多久。
- Access-Control-Allow-Credentials: <boolean> 表示是否允许客户端带有认证信息,如果用于preflight,表明实际请求是否可以使用credentials,这里的认证信息指的是cookie,根据实际经验authorization等不受影响。
- Access-Control-Allow-Methods: [, ]* 用于preflight,表明实际请求允许的方法
- Access-Control-Allow-Headers: [, ]* 用于preflight,表示允许的header
3.2 相关请求header
- Origin: 表示preflight或实际请求的origin
- Access-Control-Request-Method: 用于preflight,将实际的请求方法告诉服务器
- Access-Control-Request-Headers: [, ]* 用于preflight,将实际的header告诉服务器
3.3 简单请求
某些请求不会触发preflight,这类请求被称为简单请求。这类请求满足以下条件
- 使用以下方法
- get
- post
- head
- 只能用部分header
- Content-Type只能取值
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- XMLHttpRequestUpload对象没有注册事件监听,但是可以通过XMLHttpRequest.upload 访问。
- 请求中没使用ReadableStream对象
4 HTTP authentication
身份验证定义在rfc7235。
http定义了为访问控制和身份认证定义了一个通用架构。
4.1 相关状态码
- 401 Unauthorized 缺少请求目标资源的身份认证信息
- 407 Proxy Authentication Required 缺少通过代理服务器的身份认证信息
- 403 Forbidden 在rfc7231定义的相关状态码,指的是权限不足
4.2 相关header
- WWW-Authenticate: realm=[, charset="UTF-8"] 响应header,用来定义用来获取资源时实用的认证方法,会和401一起发送,其中type表示认证类型,常用Basic
- Authorization: 在收到401后client携带的认证信息,比如
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
- Proxy-Authenticate: realm= 用来定义通过代理访问资源时的认证方法,会和407一起发送
- Proxy-Authenticate: realm= 在收到407后client携带的header
5 HTTP caching
缓存定义在rfc7234,另外参考
回顾前面(§1.3)的示意图,一次请求的响应内容可以在origin server之外的节点保存,当对应请求再次发生时,就会直接从缓存中读取。
> >
UA =========== A =========== B - - - - - - C - - - - - - O
< <
缓存一般可分为私有缓存和共享缓存。其中浏览器缓存属于私有缓存,可用于back按钮或者访问直接访问过的页面,而不需要再次向服务器请求。代理缓存属于共享缓存,典型的如cdn。
当我们使用缓存时要注意两点,一点是决定是否要缓存,一点是缓存的内容何时被更新。这两点的控制要通过http header,最终是否用的缓存要根据状态。
5.1 相关header
- Date 报文的创建时间
- Age 报文在代理缓存中存储的时长,单位秒
- Expires 响应header,表示过期的时间,如果和cache-control设置了max-age,则此header无效
- Pragma http/1.0中规定的首部,只用于兼容,日常不要用
- ETag 响应header,是指定版本的标识符,结合If-Match和if-none-match作为验证器
- If-None-Match 条件请求,服务器将客户端的etag和最新版本的etag进行比较决定客户端缓存是否可用
- Last-Modified 响应header,包含上次修改的时间,精度一秒,如果1秒内修改多个版本可能不准,因此作为备用验证器,在请求中使用If-Modified-Since进行验证
- If-Modified-Since 指定时间以后的资源才有效
- If-Unmodified-Since
- Cache-Control 通用header,多个指令可以用逗号隔开,当做为请求header时可取值
Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: only-if-cached
响应header可取值
Cache-Control: must-revalidate
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: public
Cache-Control: private
Cache-Control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-Control: s-maxage=<seconds>
分别表示
- public 可以被任何节点缓存,即使响应本身是不可缓存的
- private 只能在浏览器缓存,即使是不可缓存的
- no-cache 可以被任何节点缓存,即使本身是不可缓存的,但是每次使用前一定要向origin server确认
- no-store 不会被缓存,但不会对之前的缓存有影响,可以设置max-age=0清理当前的缓存
- max-age=<seconds> 多长时间内有效
- s-maxage=<seconds> 覆盖max-age或expire header,只对共享缓存有效
- max-stale[=] 可以接受过期缓存,参数表示过期最大限度
- min-fresh=<seconds> 只接受指定秒数后仍有效的响应
- must-revalidate 缓存一旦过期,必须验证通过后才能使用
- proxy-revalidate 类似must-revalidate,用于代理缓存
- no-transform 不得对资源进行转换
5.2 相关status
以浏览器看到的响应状态为例
- 200 OK (from cache) 没有和origin server确认直接使用浏览器缓存
- 304 Not Modified 经过origin server确认使用浏览器缓存
- 200 OK 没使用浏览器缓存
5.3 meta标签
对于html文件的缓存,可以在meta标签携带缓存指令,可用
<meta http-equiv="Cache-Control" content="no-cache">
这种用法非标准化,不推荐使用。控制缓存直接在origin server处添加对应header即可。
5.4 工作原理
这里将http连接简化为浏览器和origin server的直接连接,对于浏览器来讲,其他节点都是server。 前端提到两个验证器ETag和Last-Modified,如果没有验证器则无法验证缓存是否有效,因此没有这两个响应header的话资源就不会被缓存,其他情况会根据相应header的指令选择对资源是否缓存。
-
当浏览器首次发起get请求a时,浏览器中没有对应缓存,origin server在响应中返回资源并携带相关header,其中Expires和Pragma不推荐使用,这里只讨论Cache-Control和验证器。如果cache-control不是no-store,且带有验证器,则会在浏览器端进行缓存。
-
当浏览器再次发起请求时
- 如果前一次没有缓存,则重复第一步
- 如果有缓存
- 如果是一次需要验证的请求(比如max-age过期或者cache-control:no-catch)则使用etag或Last-Modified验证器对当前缓存进行验证
- 如果验证通过,则返回304,更新缓存的信息,结束本次请求
- 如果不通过,则返回200,使用新的响应,结束本次请求
- 否则直接使用缓存,返回200,结束本次请求
- 如果是一次需要验证的请求(比如max-age过期或者cache-control:no-catch)则使用etag或Last-Modified验证器对当前缓存进行验证
-
如果当前有缓存而且可用,但是想要设为不可用,有两种方法
- 在响应头设置cache-control:max-age=0
- 在请求头设置cache-control,比如no-cache,当执行ctrl+f5时即使用的该方法,当f5时会将缓存置为过期然后去验证缓存,其他场景比如在地址栏输入网址或者点击超链接等会按照正常缓存规范执行。
5.5 缓存方案
对于版本化控制的文件,比如css或js可以设置长时间的缓存,需要更新时更改对应url,比如加hash值或时间戳
比如非版本化文件,比如index.html,则直接设为每次都验证,如果更改频繁,可以不缓存。
6 Compression in HTTP
在http协议相关的压缩包括end-to-end(ua到origin server)和hop-by-hop(http连接中的两个节点)两种。
第一种是由服务器压缩,到达浏览器之前不会改变。
现在最常用的两种压缩算法是gzip和较新的br,为了确定最终的压缩算法,浏览器需要和服务器协商,浏览器会添加Accept-Encoding header来告诉服务器自己支持的压缩算法列表,服务器会选一个进行压缩,然后利用Content-Encoding header告诉浏览器选择的算法,然后再加一个vary header至少包含'Accept-Encoding'然后便于缓存不同压缩算法的内容
第二种是连接中两个节点之间传输时的压缩,任何两段连接之间的压缩算法可能是不一样的
为了实现压缩,两个节点的请求方会携带TE header表示想要的压缩算法,响应方会选择一个算法进行压缩并携带Transfer-Encoding header表示选择的具体算法
7 HTTP conditional requests
条件请求指的是根据请求中带的header 响应不同内容。对于安全方法(相关定义参考Method部分),比如get,用来响应仅满足条件的请求;对于非安全方法,比如put,只有在条件满足时才可以操作成功。
这部分内容和缓存一节密切相关,为了判断是否符合某个条件需要验证器,即etag和last-modified。
验证时分强验证和弱验证两种,强验证逐个字节验证,例如断点续传时,弱验证可以接受细微差别。
相关条件header 包括
- If-Match 如果远程资源etag和If-Match列出的一种符合则验证成功,默认强验证,如果etag以
W/
为前缀则为弱验证 - If-None-Match 和If-Match相反
- If-Modified-Since 如果远程资源的Last-Modified比If-Modified-Since的值更晚则验证成功
- If-Unmodified-Since 和If-Modified-Since相反
- If-Range 用验证器之一来验证,如果验证成功则range header有效
使用场景
7.1 缓存
前面cache部分已经讨论过,即使用If-Modified-Since 和 If-Match 两个header验证两个验证器,这个验证过程是浏览器自发的。
7.2 增量下载验证
支持增量下载的服务器会添加一个Accept-Ranges 响应头,客户端就可以添加range请求头进行增量下载,但是两次下载之间远程资源可能变了,因此需要下载之前利用if-range做进一步验证。
7.3 使用乐观锁(optimistic locking)避免更新丢失问题
当在本地编辑远程资源时,需要先拉下远程副本,修改后提交。如果同时也有别人进行编辑,依次提交时就会出现互相覆盖的问题,
这时候要引入乐观锁,即只允许基于最新的远程资源进行修改,用If-Match 或 If-Unmodified-Since 来验证,如果源文件在本地获取副本后有修改,提交时会返回412 Precondition Failed拒绝提交,参考git的冲突处理机制。
当第一次上传时使用If-None-Match:*,当之前有人上传过时拒绝上传。
8 Content negotiation
当请求一种资源时,该资源可能有多种表现形式,使用方式有两种,一种是主动协商机制,浏览器主动要求某种,一种是响应式协商机制,即服务器在响应中提供选择。
8.1 主动协商机制
当浏览器发起请求时会带有一系列对内容描述的headers,比如Accept、Accept-Charset、 Accept-Encoding、Accept-Language,服务器会使用vary header表明哪些消息头是作用于内容协商方便缓存
这种方式需要为每一种不同的方面设计一种header
8.2 响应式协商机制
当服务器接到不明确的资源访问请求时会返回一个选择页面供用户选择。但是对应页面并没有指定(因此要退化到前一种机制,要么进行重定向),且每次获取资源需要两次请求。
9 HTTP cookies
http本身是无状态协议,服务器可以通过Set-Cookie header将指定内容保存在浏览器,下一次访问时被携带发送回服务器。在浏览器中保存的数据就叫做cookie。 同一个响应中可以包含多个set-cookie header从而设置多个cookie,每个header除了cookie内容外还有其他参数,多个参数用分号分隔
Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
// Multiple directives are also possible, for example:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
其中分别表示
- <cookie-name>=<cookie-value> 一个表示cookie的键值对,键名可以包含两种前缀
- __Secure- 前缀,要和secure参数一起设置,且用于https页面
- __Host- 前缀,与secure参数一起设置,用于https页面,不能设置domain,path值为'/'
- Expires=<date> 失效时间
- Max-Age=<non-zero-digit> 指定秒数后失效,优先级高于expires
- Domain=<domain-value> 指定域名下的请求携带cookies,默认当前域名,如果显式指定则包含各个子域,
- Path=<path-value> 指定路径下的请求才会携带cookies
- Secure 在https使用
- HttpOnly 设置后不能使用js获取 SameSite=Strict|Lax 跨域时是否携带
10 HTTP range requests
可以允许服务器只发送资源的一部分到浏览器,如果服务器在一次响应header包含Accept-Ranges且不能none,则说明支持range请求。用于范围请求大文件和断点续传。
在请求中使用range header表示想要获取的范围,可以使用if-range验证后再下载。
Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
cookie api
11 Redirections in HTTP
url重定向,也就是url转发。可以用来给一个页面、一个网站或应用多个url。
重定向由服务器发送特定状态码触发,浏览器接收到相关响应后会立即请求新的url.
重定向分为三种
- 永久重定向
- 临时重定向
- 特殊重定向
11.1 永久重定向
表示原url不被使用,会被搜索引擎更新为新的url
- 301 Moved Permanently 所有请求方法会在重定向时采用get;
- 308 Permanent Redirect时重定向的请求不会发生变化。
11.2 临时重定向
不会被搜索引擎更新
- 302 Found 所有请求方法在重定向时有可能使用get;
- 303 See Other 所有方法变成get,body丢失;
- 307 Temporary Redirect 重定向时不会发生变化。
11.3 特殊重定向
- 300 Multiple Choice用于内容协商提供多个可使用的链接
- 304 Not Modified 用于验证缓存时表示缓存依然可用
11.4 其他重定向方法
除了使用http header进行重定向,还可以在html中使用meta标签
<meta http-equiv="Refresh" content="0; URL=http://example.com/" />
在js中
window.location = "http://example.com/";
优先级从高到低为,http-meta-js
11.5 使用场景
比如
- 使用多个域名
- 强制使用https
12 HTTP headers
前面介绍了一些header,所有header用法参考mdn
13 HTTP request methods
http定义了一系列request methods来表示对给定资源执行什么操作。
http/1.0定义了get,post,head 三种方法,http/1.1添加了options,put,delete,trace,connect五种方法,以及RFC 5789添加的patch方法。
一个请求方法有三个需要注意的特性,即Safe,Idempotent,cacheable。
- Safe 安全方法表示只用来获取资源而不修改服务器的资源
- Idempotent 幂等表示多次请求应该结果一样
- cacheable 可缓存就是其响应可以缓存
其中
- get 表示请求一个特定资源
- head 和get获得一样的响应,但是没有响应body
- post 提交指定内容
- put 修改或创建资源
- delete 删除资源
- trace 查http连接中中介做了哪些修改
- connect 为请求建立隧道,通常用来加ssl加密的通信(https)通过未加密的http代理
- patch 应用资源的部分修改
14 HTTP response status codes
在响应中表示响应是否成功,包含五组
- Informational responses (100–199)
- Successful responses (200–299)
- Redirects (300–399)
- Client errors (400–499)
- Server errors (500–599)
14.1 Informational responses
- 100 Continue 一切正常,可以忽略这个响应
- 101 Switching Protocol 用来响应一个带upgrade请求头的请求,表示服务器切换协议成功
14.2 Successful responses
- 200 OK 请求成功
- 206 Partial Content 部分内容,用于range请求
14.3 Redirection messages
前面介绍的各种重定向,见redirect
- 300 Multiple Choice
- 301 Moved Permanently
- 302 Found
- 303 See Other
- 304 Not Modified
- 307 Temporary Redirect
- 308 Permanent Redirect
14.4 Client error responses
- 400 Bad Request 服务器不能理解的语法
- 401 Unauthorized 没授权
- 403 Forbidden 权限不足
- 404 Not Found 找不到对应资源
- 405 Method Not Allowed 不允许该方法
14.5 Server error responses
- 500 Internal Server Error 服务器内部操作
- 504 Gateway Timeout 超时
15 Protocol upgrade mechanism
http/1.1可以使用upgrade请求头来将已建立的连接升级未其他协议(比如websocket或http/2.0),注意这种机制只能在http/1.1使用,不能在2.0及以上使用。
协议升级总是由浏览器发起,一个升级的请求要另外携带两个header,Connection: Upgrade
表示这是一个升级请求的连接,Upgrade: protocols
表示想要升级为的协议
GET /index.html HTTP/1.1
Host: www.example.com
Connection: upgrade
Upgrade: example/1, foo/2
服务器一旦升级成功,就会返回101的status code,此时可以使用新协议通信。
16 HTTPS
即http over tls,在rfc2818定义,通过tls来对http 连接进行加密。主要细节参考下一章tls,这里做一下基本介绍
16.1 HTTP Over TLS
从概念上说,https很简单,使用http over tls就像使用http over tcp一样。
16.1.1 连接初始化
作为http client的agent也应该作为tls client,需要在一个合适的端口发送tls的 clentHello来开启 tls handshke。当handshake结束后,client需要初始化第一个http请求,所有http数据一定要作为tls的应用数据进行发送。
16.1.2 连接关闭
当收到一个有效的关闭alert,一个实现应该认为不会再在连接上收到任何数据了。一个tls实现一定要在关闭之前初始化一个可交换的关闭alert,在发送完关闭alert后可以不用等待对方发送回应就直接关闭连接,从而生成不完全关闭,这个不完全关闭可以重新启用。
16.1.3 端口号
http服务器希望收到的第一个数据就是请求行,tls服务器(即https服务器)希望收到的第一个数据是clientHello。https会在一个单独的端口运行,可以此分清使用的什么协议,如果是建立在tcp/ip之上的连接默认端口是443,而tls需要的只是一个可靠连接。
16.1.4 uri格式
https和http的的uri协议部分不同,前者是https,后者是http
16.2 Endpoint Identification
16.2.1 服务端标识
通常一个https请求是通过dereferencing一个uri(通过uri获取对应资源)来生成的,因此客户端是知道服务端的hostname的,如果hostname可用,client一定要按照服务端的认证消息对其进行验证来防止中间人(man-in-the-middle)攻击。
如果客户端有另外的信息作为服务端的标识,hostname验证可以忽略。
16.2.2 客户端标识
通常服务端并不知道应该怎么验证客户端,除非客户端有一个基于CA的certificate chain。
17 TLS
涉及很多加密学的知识,这里只做简要分析(而且不一定对)。更多内容可参考TLS协议分析 与 现代加密通信协议设计
当前版本是 Transport Layer Security (TLS) Protocol/1.3,定义在rfc8446,tls可以使c/s应用的通信避免窃听(eavesdropping),篡改(tampering), and 消息伪造(message forgery)。
tls是通过在网络连接的两端建立安全通道实现的,唯一需要的就是可靠、有序的数据流传输。这个安全通道应该有以下特性:
- 认证(Authentication):通道的服务端总是认证过的,客户端选择性认证。认证可以通过非对称加密(比如RSA,ECDSA或EdDSA)或对称加密的PSK
- 保密性(Confidentiality):通道建立后的数据传输只在两端可见。tls并不会隐藏数据传输的长度,尽管端点可以填充长度来隐藏真实的长度。
- 完整性(Integrity):通道建立后的数据传输只要被修改都可以监测到。
tls有三个子协议
- handshake protocol,用来验证双方,协商加密模式和参数,建立共享密钥材料。
- record protocol,使用握手协议建立的参数来保护双方的流量。记录协议将流量分成一系列记录,每一个都使用一个独立的流量密钥来保护。
- Alert Protocol,用来提供警告信息
tls和应用协议无关,其上可以有更高层的协议透明传输。但是tls标准并没有指定一个协议怎么使用tls增加安全性、怎么发起tls握手、以及如何解释交换的证书,这些都取决于这些协议本身。
17.1 协议概述
安全通道使用的加密参数由tls握手协议产生,这个tls的子协议被服务端和客户端首次通信时使用,用来协商协议版本、选择加密算法、可选的互相认证以及创建共享密钥材料。一旦握手完成,双方使用产生的密钥来保护应用层的流量。
tls支持三种基本的密钥交换模式
- (EC)DHE (Diffie-Hellman over either finite fields or elliptic curves)
- PSK-only
- PSK with (EC)DHE
17.2 Handshake Protocol
可结合TLS1.3握手流程以及参数详解阅读rfc8446相关部分。
17.2.1 tls加密的一些分析
当使用http时,明文传输数据,因此消息易受截获,需要加密处理。
这里涉及到的加密分为对称加密和非对称加密,其中
- 对称加密,密钥(Shared key )只有一个,用来加密和解密,存在的问题是密钥易泄露
- 非对称加密,包含公钥(public key)和私钥(private key),其中一种加密的数据只能另一种密钥解密,比如服务端保留私钥,公钥对外公开,存在的问题是响应报文无法做到加密(因为公钥公开)
- 两者结合,客户端请求时利用非对称加密将后续使用的对称加密的密钥发送给服务端,然后服务端接收到密钥后两者接下来的通信便可以进行对称加密,存在的问题是存在中间人攻击,即第一次通信时就有中间人参与
- 引入第三方ca发布数字证书来标记一个域名,ca证书也是通过非对称加密,其中的公钥客户端可以直接获取且保证为真(比如内置在浏览器中),私钥在相应服务端,当服务端利用ca 私钥将证书和相关数据(比如对称加密使用的密钥)发送给客户端时,客户端可以对证书进行验证,防止中间人的存在,这也是当前tls加密的主要手段
17.2.2 握手过程
握手的最终是要获取一个可靠的、用于后续通信对称加密的密钥(之所以后续通信不用非对称加密是性能原因)
下图展示的整个tls的握手过程,需要一个来回,注意在tls握手之前需要三次握手建立tcp连接
这个握手过程包括
- 客户端发送一个ClientHello消息,包含协议版本、random nonce(使用一次的随机字符串,用于生成shared key)、加密套件等
- 服务端计算出sharedkey,回复serverHello,包含选定的加密套件,证书,并用证书对应的私钥加密
- 客户端对收到的证书验证,用证书公钥解密后根据相关参数算出sharedkey
此时握手结束,client和server获取了record层保护数据传输所用的shared key,除非特别指定,否则在发送Finished之前不要发送应用数据。
18 HTTP/2
定义在rfc7540,是对http/1.1的补充,之前的语法和语义保持不变。可参考Introduction to HTTP/2。
http最开始设计就是为world wide web获取资源,于是http/0.9仅用一行实现了这个目的,http/1.0和http/1.1对其进行了扩展,进而成为应用最广泛的互联网应用协议。然而却出现了性能问题:
- 需要多个连接才能实现并发和缩短延迟(每个tcp连接同时只能处理一个http请求)
- header 重复且冗长,从而导致不必要的网络流量
- 不支持资源优先级,导致连接利用率低下
http/2的出现就是为了解决这些问题
这一切的核心是在应用层引入了一个binary framing and multiplexing layer ,定义了如何封装http消息
通过对http传输信息修改编码,从1.x版本的纯文本,改为更小的的二进制帧。
18.1 术语
- Stream 流,在一个已建立连接中的双向比特流,可以携带一个或多个message。
- Message 消息,一个完整的frame序列,对应一个逻辑请求或逻辑响应
- Frame 帧,http/2通信的最小单位,每个包含一个header
这些术语的关系是
- 所有通信都在一个tcp连接上进行,这个连接可以携带任何数量的双向流
- 每个流都包含一个独特的标识和一个可选的优先级用于携带双向消息
- 每个消息是一个逻辑上的请求或响应,会包含一个或多个帧
- 帧是最小的通信单位,携带特定类型的数据,比如http header,message payload等,不同流的帧可能交错发送,再根据header重新组装
18.2 http/2 功能改进
借助二进制帧的机制实现了以下功能
- 请求和响应复用
- 流优先级
- 每个origin共用一个连接
- 流控制
- 服务器推送
- header压缩 具体为
18.2.1 Request and response multiplexing
http2之前每个tcp请求同时只能处理一个http通信,且并发的tcp连接通常是有限制的,因此如果需要多个请求需要排队,只有前一个请求响应结束才能开始下一个请求。http2将请求和响应消息拆分为互不依赖的帧,交错发送,到另一端重新组装,实现了请求和响应的复用,所有http通信只需要建立一个tcp连接即可。
18.2.2 Stream prioritization
通过为每个数据流添加优先级,可以进一步提升传输效率。优先级根据数据流依赖关系和权重计算得来。
18.2.3 One connection per origin
这一点前面提到了,就是每个origin只需要建立一个tcp连接
18.2.4 Flow control
在tcp 流控制的基础上,在http协议中进行更细粒度的控制。
18.2.5 Server push
http2之前的http是严格的一问一答式,每个响应对应一个请求,在http2中每个请求可以对应多个响应,比如请求一个html文件,可以把对应的css一起推送给客户端
18.2.6 Header compression
在http2之前,每个消息都包含很多header作为纯文本发送,http2使用HPACK对header进行压缩,包括修改header格式和维护一个header列表及索引两种方式
19 HTTP/3
目前是草案阶段,定义在draft-ietf-quic-http,可以参考HTTP/3 explained。
http/3的产生当然是解决http/2存在的问题。
我们在使用http/2时每个origin建立一个tcp连接,因为tcp连接是按序传输,如果其中有数据包丢失,被丢失的数据就会重传,其后的内容就需要等待,这种阻塞被称为head of line blocking。如果一个网络丢包率大于2%,性能甚至不及http/1.1,因为http/1.1一般最多会有6个tcp连接同时工作。
这个问题是tcp的问题,要想解决这个问题就要从传输层协议入手。http3最终采用的是在udp基础上创造一个新的协议QUIC(QUIC并不是什么缩写,是一个完整的名字,是一个新的传输层协议,http/3是基于该传输协议的第一个应用层协议,在draft-ietf-quic-transport-34作为草案定义)。http3和http2的协议栈对照如下
http3是在http2已有使用的基础上添加了新的功能,除了解决了tcp的阻塞问题,还有
- quic的握手需要0或1个来回,其中如果之前连接过并有对应缓存可以实现0-RTT
- http3不提供不安全版本
http3的发展遇到了一些问题
- 为了防止攻击,很多主机对dns端口以外的udp流量进行拦截或限流
- 内核处理udp比较慢且更消耗cpu
完结撒花