基本特点
- 应用层协议
- 用来在两点之间传输数据,不能用于广播、寻址或路由
- 可以传输文字、图片、音频、视频等超文本数据
无状态明文传输请求应答
报文结构
1. 基本结构
- 起始行(start line):描述请求或响应的基本信息;
- 头部字段集合(header):使用
key-value形式更详细地说明报文; - 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据
报文必须有 header,但可以没有 body,而且在 header 之后必须要有一个“空行”,也就是“CRLF”,十六进制的“0D0A”
2. 请求行(请求报文的起始行)
由三部分组成
- 请求方法:是一个动词,如
GET/POST,表示对资源的操作; - 请求目标:通常是一个
URI,标记了请求方法要操作的资源; - 版本号:表示报文使用的
HTTP协议版本 - 这三个部分通常使用空格(space)来分隔,最后要用 CRLF 换行表示结束
3. 状态行(响应报文的起始行)
由三部分组成
- 版本号:表示报文使用的 HTTP 协议版本;
- 状态码:一个三位数,用代码的形式表示处理的结果,比如 200 是成功,500 是服务器错误;
- 原因:作为数字状态码补充,是更详细的解释文字,帮助人理解原因
4. 头部字段
请求行或状态行再加上头部字段集合就构成了 HTTP 报文里完整的请求头或响应头
- 用“
:”分隔, - 不区分大小写
- 顺序任意
- 字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线“_”
HTTP/1.1里唯一要求必须提供的头字段是Host,它必须出现在请求头里,标记虚拟主机名- 除了规定的标准头,也可以任意添加自定义字段,实现功能扩展
标准请求方法
GET:获取资源,可以理解为读取或者下载数据;HEAD:获取资源的元信息;POST:向资源提交数据,相当于写入或上传数据;PUT:类似POST;DELETE:删除资源;CONNECT:建立特殊的连接隧道;OPTIONS:列出可对资源实行的方法;TRACE:追踪请求 - 响应的传输路径
URI
scheme叫“方案名”或者“协议名”,表示资源应该使用哪种协议来访问;- “
host:port”表示资源所在的主机名和端口号; path标记资源所在的位置;query表示对资源附加的额外要求;- 在
URI里对“@&/”等特殊字符和汉字必须要做编码,否则服务器收到HTTP报文后会无法正确处理
HTTP优缺点
HTTP最大的优点是简单、灵活和易于扩展;HTTP是无状态的,可以轻松实现集群化,扩展性能,但有时也需要用Cookie技术来实现“有状态”;HTTP是明文传输,数据完全肉眼可见,能够方便地研究分析,但也容易被窃听;HTTP是不安全的,无法验证通信双方的身份,也不能判断报文是否被窜改;HTTP的性能不算差,但不完全适应现在的互联网,还有很大的提升空间
实体数据
数据类型使用的头字段
- 客户端用
Accept头告诉服务器希望接收什么样的数据
Accept字段标记的是客户端可理解的MIMEtype,可以用“,”做分隔符列出多个类型Accept: text/html,application/xml,image/webp,image/png告诉服务器:“我能够看懂 HTML、XML 的文本,还有webp和png的图片,请给我这四类格式的数据
- 服务器用
Content头告诉客户端实际发送了什么样的数据
- 响应报文里用头字段
Content-Type告诉实体数据的真实类型 Content-Type: image/png浏览器看到报文里的类型是一个 PNG 文件,就会在页面上显示出图像
Accept-Encoding字段标记的是客户端支持的压缩格式,用“,”列出多个,服务器可以选择其中一种来压缩数据,实际使用的压缩格式放在响应头字段Content-Encoding里- 如果请求报文里没有
Accept-Encoding字段,就表示客户端不支持压缩数据;如果响应报文里没有Content-Encoding字段,就表示响应数据没有被压缩
语言类型使用的头字段
Accept-Language字段标记了客户端可理解的自然语言,也允许用“,”做分隔符列出多个类型- 服务器在响应报文里用头字段
Content-Language告诉客户端实体数据使用的实际语言类型 - 字符集在
HTTP里使用的请求头字段是Accept-Charset,响应头里却没有对应的Content-Charset,而是在Content-Type字段的数据类型后面用“charset=xxx”来表示,这点需要特别注意
内容协商的质量值
- 在 HTTP 协议里用
Accept、Accept-Encoding、Accept-Language等请求头字段进行内容协商的时候,还可以用一种特殊的“q”参数表示权重来设定优先级 - 权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0 就表示拒绝。具体的形式是在数据类型或语言代码后面加一个“;”,然后是“q=value”
表示浏览器最希望使用的是 HTML 文件,权重是 1,其次是 XML 文件,权重是 0.9,最后是任意数据类型,权重是0.8。服务器收到请求头后,就会计算权重,再根据自己的实际情况优先输出 HTML 或者 XML
内容协商的结果
- 服务器会在响应头里多加一个Vary字段,记录服务器在内容协商时参考的请求头字段,给出一点信息
表示服务器依据了 Accept-Encoding、User-Agent 和 Accept 这三个头字段,然后决定了发回的响应报文
- 每当
Accept等请求头变化时,Vary也会随着响应报文一起变化。也就是说,同一个URI可能会有多个不同的“版本”,主要用在传输链路中间的代理服务器实现缓存服务
传输大文件
1. 数据压缩
- 使用:浏览器在发送请求时都会带着“Accept-Encoding”头字段,里面是浏览器支持的压缩格式列表,例如 gzip、deflate、br 等,这样服务器就可以从中选择一种压缩算法,放进“Content-Encoding”响应头里,再把原数据压缩后发给浏览器
- 缺点:gzip 等压缩算法通常只对文本文件有较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高度压缩的,再用 gzip 处理也不会变小
2. 分块传输
分块传输的头部参数
- 分块传输编码,在
响应报文里用头字段Transfer-Encoding: chunked来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送 Transfer-Encoding: chunked和Content-Length这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知
分块传输的规则
- 每个分块包含两个部分,长度头和数据块;
- 长度头是以 CRLF(回车换行,即
\r\n)结尾的一行明文,用 16 进制数字表示长度; - 数据块紧跟在长度头后,最后也用
CRLF结尾,但数据不包含 CRLF; - 最后用一个长度为 0 的块表示结束,即“
0\r\n\r\n”
3.范围请求
基本概念
当在看某穿越剧,想跳过片头,直接看正片,想拖动进度条快进几分钟,这实际上是想获取一个大文件其中的片段数据,而分块传输并没有这个能力,HTTP 协议为了满足这样的需求,提出了范围请求(range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分
- 范围请求不是 Web 服务器必备的功能,可以实现也可以不实现
- 要实现的话,服务器必须在响应头里使用字段
Accept-Ranges: bytes明确告知客户端:“我是支持范围请求的” - 如果不支持的话,服务器可以发送
Accept-Ranges: none,或者干脆不发送Accept-Ranges字段
请求过程
1. 客户端请求头携带Range,格式是bytes=x-y,其中的 x 和 y 是以字节为单位的数据范围
- Range 的格式灵活,起点 x 和终点 y 可以省略,能够很方便地表示正数或者倒数的范围
- x、y 表示的是
偏移量,范围必须从 0 计数,前 10 个字节表示为“0-9”,第二个 10 字节表示为“10-19”,而“0-10”实际上是前 11 个字节
假设文件是 100 个字节
- “0-”表示从文档起点到文档终点,相当于“0-99”,即整个文件;
- “10-”是从第 10 个字节开始到文档末尾,相当于“10-99”;
- “-1”是文档的最后一个字节,相当于“99-99”;
- “-10”是从文档末尾倒数 10 个字节,相当于“90-99”
2. 服务端收到Range 字段, 会进行下面4个步骤
- 检查范围是否合法,比如文件只有 100 个字节,但请求“200-300”,这就是
范围越界了。服务器就会返回状态码416,意思是“你的范围请求有误,我无法处理,请再检查一下” 范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码“206 Partial Content”, 表示 body 只是原数据的一部分- 服务器要添加一个响应头字段
Content-Range,告诉片段的实际偏移量和资源的总大小,格式是bytes x-y/length,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes 0-10/100”。 - 发送数据
多段数据
- 支持在 Range 头里使用多个“x-y”,一次性获取多个片段数据
- 需要
使用一种特殊的 MIME 类型:multipart/byteranges,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数“boundary=xxx”给出段之间的分隔标记 每一个分段必须以“- -boundary”开始(前面加两个“-”),之后要用“Content-Type”和“Content-Range”标记这段数据的类型和所在范围,然后就像普通的响应头一样以回车换行结束,再加上分段数据,最后用一个“- -boundary- -”(前后各有两个“-”)表示所有的分段结束
连接管理
1. 短连接
HTTP 协议底层的数据传输基于 TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接
短连接有个很明显的缺陷:
在 TCP协议里,建立连接和关闭连接都是非常“昂贵”的操作。TCP 建立连接要有“三次握手”,发送 3 个数据包,需要 1 个 RTT;关闭连接是“四次挥手”,4 个数据包需要 2 个 RTT。每次连接和断开连接都要握手操作,效率太低
2. 长连接
既然 TCP 的连接和关闭非常耗时间,那么就把这个时间成本由原来的一个“请求 - 应答”均摊到多个“请求 - 应答”上
3. 连接相关的头字段
在 HTTP/1.1 中的连接都会默认启用长连接,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 连接,在这个连接上收发数据
明确使用长连接机制
请求头里使用字段Connection,值是keep-alive- 不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在
响应报文里放一个Connection: keep-alive字段
长连接的问题
因为 TCP 连接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务器无法为真正有需要的用户提供服务
客户端关闭长连接
- 在
请求头里加上Connection: close字段 - 服务器看到这个字段,就知道客户端要主动关闭连接,于是在
响应报文里也加上这个字段,发送之后就调用 Socket API 关闭 TCP 连接
服务端关闭长连接
- 假设服务端用nginx
- 使用
keepalive_timeout指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。 - 使用
keepalive_requests指令,设置长连接上可发送的最大请求次数。比如设置成 1000,那么当 Nginx 在这个连接上处理了 1000 个请求后,也会主动断开连接。
- 客户端和服务器都可以在报文里附加通用头字段
Keep-Alive: timeout=value,限定长连接的超时时间。
4. 队头阻塞
“队头阻塞”与短连接和长连接无关,而是由 HTTP 基本的“请求 - 应答”模型所导致的
产生的原因
- HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理
- 如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待
解决办法
- 并发连接
对一个域名发起多个长连接,用数量来解决质量的问题
- 同一域名建立多个长连接,分散请求(并发处理)
- 太多的长连接会占用服务器资源,所以浏览器限制同一域名最多只能同时建立
6-8个长连接
- 域名分片
使用多个域名指向同一台服务器,每个域名建立最大可建立数的长连接
上面一个域名可以建立6-8个长连接,那么n个域名就是 n * 6 ~ n * 8
重定向
重定向的过程
301 是“永久重定向”,302 是“临时重定向”,浏览器收到这两个状态码就会跳转到新的 URI,这里有个响应头Location很重要
- “重定向”实际上发送了两次
HTTP请求,第一个请求返回了302,然后第二个请求就被重定向到了Location头的值“/index.html” Location字段属于响应字段,必须出现在响应报文里。只有配合 301/302 状态码才有意义,它标记了服务器要求重定向的 URI,这里就是要求浏览器跳转到“index.html”
重定向状态码
永久重定向301
- 原 URI 已经“永久”性地不存在了,今后的所有请求都必须改用新的 URI
- 历史记录、更新书签,下次可能就会直接用新的 URI 访问,省去了再次跳转的成本。
- 搜索引擎的爬虫看到 301,也会更新索引库,不再使用老的 URI
临时重定向302
- 原 URI 处于“临时维护”状态,新的 URI 是起“顶包”作用的“临时工”
- 原来的 URI 仍然有效,但暂时不可用,所以只会执行简单的跳转页面,不记录新的 URI,也不会有其他的多余动作,下次访问还是用原 URI
应用场景
- 301永久重定向
- 域名、服务器、网站架构发生了大幅度的改变,比如启用了新域名、服务器切换到了新机房、网站目录层次重构,原来的 URI 已经不能用了,必须用 301“永久重定向”,通知浏览器和搜索引擎更新到新地址,这也是搜索引擎优化(SEO)要考虑的因素之一
- 302临时重定向
- 原来的 URI 在将来的某个时间点还会恢复正常,常见的应用场景就是系统维护,把网站重定向到一个通知页面,告诉用户过一会儿再来访问。另一种用法就是“服务降级”,比如在双十一促销的时候,把订单查询、领积分等不重要的功能入口暂时关闭,保证核心服务能够正常运行
重定向的问题
1. 性能损耗
- 重定向的机制决定了一个跳转会有两次请求 - 应答,比正常的访问多了一次,所以重定向应当适度使用,决不能滥用
2. 循环跳转
- 如果重定向的策略设置欠考虑,可能会出现“A=>B=>C=>A”的无限循环,不停地在这个链路里转圈圈,HTTP 协议特别规定,浏览器必须具有检测“循环跳转”的能力,在发现这种情况时应当停止发送请求并给出错误提示
HTTP中的COOKIE
HTTP是无状态协议,服务端如何区分不同用户,用的就是cookie
工作过程
- 浏览器首次发起请求,服务器通过在
响应头添加Set-Cookie字段,格式是key=value,跟着响应报文一起返回 - 浏览器收到响应报文看到里面有
Set-Cookie,就保存起来,下次再请求的时候就自动把这个值放进 Cookie 字段里发给服务器 - 服务器看到请求头有
Cookie字段,拿出里面的值,识别出用户的身份 - 服务器
可以在响应头里添加多个 Set-Cookie,存储多个key=value。浏览器发送时不需要用多个 Cookie 字段,只要在一行里用“;”隔开就行
- 注意,
Cookie是由浏览器负责存储的,如果换浏览器或者换台电脑,新的浏览器里没有服务器对应的Cookie,只能再走一遍Set-Cookie流程
相关属性
1. 有效期
有效期就是为了让cookie在一定时间内有效,超出这个时间马上失效过期,有两个相关属性Expires和Max-Age,两者同时出现的情况下,浏览器会优先采用 Max-Age 计算失效期
- Expires
绝对时间点
- Max-Age
相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间
Expires 标记的过期时间是“GMT 2019 年 6 月 7 号 8 点 19 分”
而 Max-Age 则只有 10 秒,如果现在是 6 月 6 号零点,那么 Cookie 的实际有效期就是“6 月 6 号零点过 10 秒”
2. 作用域
Domain和Path指定了 Cookie 所属的域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不一致,就不会在请求头里发送 Cookie
- 这两个属性可以为不同的域名和路径分别设置各自的 Cookie,比如“/19-1”用一个 Cookie,“/19-2”再用另外一个 Cookie,两者互不干扰
- Path 用
/或者直接省略,表示域名下的任意路径都允许使用 Cookie,让服务器自己去挑
3. 安全性
HttpOnly
JS 脚本里可以用 document.cookie 来读写 Cookie 数据,这就带来了安全隐患,有可能会导致“跨站脚本”(XSS)攻击窃取数据
HttpOnly会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问,浏览器的 JS 引擎就会禁用 document.cookie 等一切相关的 API
SameSite
- 可以
防范跨站请求伪造(XSRF)攻击 - 设置成
SameSite=Strict可以严格限定 Cookie 不能随着跳转链接跨站发送 - 设置成
SameSite=Lax,允许 GET/HEAD等安全方法,但禁止 POST 跨站发送
Secure
- 表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送
Cookie 本身不是加密的,浏览器里还是以明文的形式存在
Cookie的应用
身份识别
- 用户点击登录就会向服务器发起一个登录请求
- 服务器响应报文添加
Set-Cookie,携带用户信息参数 - 以后请求其它接口都会带上这个
身份标识Cookie,就完成了身份识别
广告跟踪
当访问某些网页时,会看到一些广告,这背后是一些广告商,这些广告商会添加一些cookie到浏览器,但是这些cookie不属于你访问的网页的,而是这个广告商的,当你再去访问其它网页,一样能读取到广告商的cookie,然后又可以推荐广告了
缓存控制
基本流程
- 浏览器发现缓存无数据,于是发送请求,向服务器获取资源;
- 服务器响应请求,返回资源,同时标记资源的有效期;
- 浏览器缓存资源,等待下次重用。
服务端缓存控制
Cache-Control
可以有以下的值存在
max-age:生存时间,单位是s,响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻no_store:不允许缓存no_cache:可以缓存,使用前必须要去服务器验证是否过期,是否有最新的版本must-revalidate:缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证
客户端缓存控制
浏览器也可以发Cache-Control
- 点
刷新按钮的时候,浏览器会在请求头里加一个Cache-Control: max-age=0 Ctrl+F5 的“强制刷新,发了一个Cache-Control: no-cache
条件请求
流程
第一次的响应报文预先提供Last-modified和ETag,第二次请求时就可以带上缓存里的原值,验证资源是否是最新的,如果资源没有变,服务器就回应一个304 Not Modified,表示缓存依然有效
Last-modified和if-Modified-Since
Last-modified是文件的最后修改时间, 这种方式存在两个问题
- 一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分
- 一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽
ETag和If-None-Match
ETag是资源的一个唯一标识,可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存
- ETag 有
强``弱的区分 - 强 ETag 要求资源在
字节级别必须完全相符 - 弱 ETag 在值前有个
W/标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变
HTTP代理
代理相关头字段
Via
请求头或响应头里都可以出现,每当报文经过一个代理节点,代理服务器就会把自身的代理主机名(或者域名)追加到字段的末尾- Via 字段
只解决了客户端和源服务器判断是否存在代理的问题,不能知道对方的真实信息
X-Forwarded-For
- 每经过一个代理节点就会在字段里追加
请求方的 IP 地址。在字段里最左边的 IP 地址就是客户端的地址
X-Real-IP
- 记录客户端 IP 地址
- 如果客户端和源服务器之间只有一个代理,
X-Forwarded-For的值等于X-Real-IP
缓存代理
HTTP 传输链路上,不只是客户端有缓存,服务器也有缓存,所以代理服务器也有缓存
缓存代理服务
- 在没有缓存的时候,代理服务器每次都是直接转发客户端和服务器的报文,中间不会存储任何数据,只有最简单的中转功能
- 在有缓存的时候,代理服务器把报文转发给客户端,然后把报文存入自己的 Cache 里,下一次再有相同的请求,代理服务器就可以直接发送 304 或者缓存数据,不必再从源服务器那里获取。降低了客户端的等待时间,节约了源服务器的网络带宽
源服务器的缓存控制相关头部字段
private和public
- 客户端的缓存只是用户自己使用,而代理的缓存可能会为非常多的客户端提供服务
private表示缓存只能在客户端保存, 不能放在代理上与别人共享public的意思就是缓存完全开放,谁都可以存,谁都可以用
proxy-revalidate
- 要求代理的缓存过期后必须验证,客户端不必到源服务器获取缓存信息,只需到代理获取缓存信息
s-maxage
- 缓存生效时间,类似于
max-age,只针对代理服务器
no-transform
- 禁止代理对缓存下来的数据做一些优化,比如把图片生成 png、webp 等几种格式
注意:源服务器在设置完Cache-Control后必须要为报文加上Last-modified或ETag字段。否则,客户端和代理后面就无法使用条件请求来验证缓存是否有效,也就不会有 304 缓存重定向
客户端的缓存控制相关头部字段
max-age、no_store、no_cache 同之前
max-stale
如果代理上的缓存过期了也可以接受,但不能超过max-stale的值
min-fresh
缓存必须有效,而且必须在min-fresh的值后依然有效
only-if-cached
表示只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个 504(Gateway Timeout)