标签列表

everest33

自制力

导航

HTTP学习笔记

※,平时记录

eTLD+1:实际指的就是一级域名。参考此文

  • a.com中com是顶级域名,a是一级域名。
  • b.ac.cn中,ac.cn是eTLD(effective Top-Level Domain,有效顶级域名,即应该被视为顶级域名),b是一级域名。


教程:geektime 透视HTTP协议【此教程时间:2019年】

https://github.com/chronolaw/http_study

※,01、HTTP的前世今生

HTTP 协议始于三十年前蒂姆·伯纳斯 - 李的一篇论文(1989年)

http/0.9: 20 世纪 90年代初期的互联网世界非常简陋,计算机处理能力低,这一时期的 HTTP 被定义为 0.9版,结构比较简单,为了便于服务器和客户端处理,它也采用了纯文本格式。蒂姆·伯纳斯 -李最初设想的系统里的文档都是只读的,所以只允许用“GET”动作从服务器上获取 HTML文档,并且在响应请求之后立即关闭连接,功能非常有限

HTTP/1.0: 1996 年,HTTP/1.0版本在 正式发布。HTTP/1.0并不是一个“标准”,只是记录已有实践和模式的一份参考文档,不具有实际的约束力,相当于一个“备忘录”。RFC编号:1945.

HTTP/1.1: 1999 年,HTTP/1.1 发布了 RFC 文档,编号为RFC2616。它是一个“正式的标准”,而不是一份可有可无的“参考文档”。是目前互联网上使用最广泛的协议,功能也非常完善。HTTP/1.1主要的变更点有:

  • 增加了 PUT、DELETE 等新的方法;
  • 增加了缓存管理和控制;
  • 明确了连接管理,允许持久连接;
  • 允许响应数据分块(chunked),利于传输大文件;
  • 强制要求 Host 头,让互联网主机托管成为可能

2014年:由于 HTTP/1.1 太过庞大和复杂,所以在 2014年又做了一次修订,原来的一个大文档被拆分成了六份较小的文档,编号为RFC7230-RFC7235,优化了一些细节,但此外没有任何实质性的改动。

HTTP/2: 2015年:HTTP/1.1发布之后,整个互联网世界呈现出了爆发式的增长,度过了十多年的“快乐时光”,更涌现出了Facebook、Twitter、淘宝、京东等互联网新贵。这期间也出现了一些对 HTTP不满的意见,主要就是连接慢,无法跟上迅猛发展的互联网。第二次的“浏览器大战”后(chrome与IE),互联网标准化组织以Google 的SPDY协议为基础开始制定新版本的 HTTP协议,最终在 2015 年发布了HTTP/2,RFC 编号 RFC7540

HTTP/2的制定充分考虑了现今互联网的现状:宽带、移动、不安全,在高度兼容 HTTP/1.1的同时在性能改善方面做了很大努力,主要的特点有:

  • 二进制协议,不再是纯文本;
  • 可发起多个请求(多路复用能力),废弃了 1.1 里的管道;
  • 使用专用算法压缩头部,减少数据传输量;
  • 允许服务器主动向客户端推送数据;
  • 增强了安全性,“事实上”要求加密通信。

虽然 HTTP/2 到今天已经四岁,也衍生出了 gRPC 等新协议,但由于 HTTP/1.1实在是太过经典和强势,目前它的普及率还比较低,大多数网站使用的仍然还是 20 年前的 HTTP/1.1。

HTTP/3: 2018: 在 HTTP/2 还处于草案之时,Google又发明了一个新的协议,叫做QUIC。2018 年,互联网标准化组织 IETF提议将“HTTP over QUIC”更名为“HTTP/3”并获得批准,HTTP/3正式进入了标准化制订阶段。

※,与HTTP相关的概念/协议

代理(Proxy)是 HTTP协议中请求方和应答方中间的一个环节,作为“中转站”,既可以转发客户端的请求,也可以转发服务器的应答。代理有很多的种类,常见的有:

  • 匿名代理:完全“隐匿”了被代理的机器,外界看到的只是代理服务器;
  • 透明代理:顾名思义,它在传输过程中是“透明开放”的,外界既知道代理,也知道客户端;
  • 正向代理:靠近客户端,代表客户端向服务器发送请求;
  • 反向代理:靠近服务器端,代表服务器响应客户端的请求;

上一讲提到的CDN,实际上就是一种代理,它代替源站服务器响应客户端的请求,通常扮演着透明代理和反向代理的角色。由于代理在传输过程中插入了一个“中间层”,所以可以在这个环节做很多有意思的事情,比如:

  • 负载均衡:把访问请求均匀分散到多台机器,
  • 实现访问集群化;
  • 内容缓存:暂存上下行的数据,减轻后端的压力;
  • 安全防护:隐匿 IP, 使用 WAF等工具抵御网络攻击,保护被代理的机器;
  • 数据处理:提供压缩、加密等额外的功能。
  • 关于 HTTP 的代理还有一个特殊的“代理协议”(proxy protocol),它由知名的代理软件 HAProxy 制订,但并不是RFC 标准,我也会在之后的课程里专门讲解.

MAC 层的传输单位是帧(frame),IP层的传输单位是包(packet),TCP层的传输单位是段(segment),HTTP的传输单位则是消息或报文(message)。但这些名词并没有什么本质的区分,可以统称为数据包。

※,DNS

★,DNS的核心系统是一个三层的树状、分布式服务,基本对应域名的结构:

  • 根域名服务器(Root DNS Server):管理顶级域名服务器,返回“com”“net”“cn”等顶级域名服务器的 IP 地址;【根域名服务器告诉请求方com等这种顶级域名在哪个服务器上】
  • 顶级域名服务器(Top-level DNS Server):管理各自域名下的权威域名服务器,比如 com 顶级域名服务器可以返回 apple.com 域名服务器的 IP地址;
  • 权威域名服务器(Authoritative DNS Server):管理自己域名下主机的 IP 地址,比如 apple.com权威域名服务器可以返回 www.apple.com 的 IP 地址。

★,在核心 DNS 系统之外,还有两种手段用来减轻域名解析的压力

  • ★,许多大公司、网络运行商都会建立自己的 DNS服务器,作为用户 DNS 查询的代理,代替用户访问核心 DNS系统。这些“野生”服务器被称为“非权威域名服务器”,可以缓存之前的查询结果,如果已经有了记录,就无需再向根服务器发起查询,直接返回对应的 IP 地址。比较知名的 DNS
    • Google 的“8.8.8.8”
    • Microsoft 的“4.2.2.1”
    • CloudFlare的“1.1.1.1”等等。
  • ★,操作系统里也会对 DNS 解析结果做缓存。另外,操作系统里还有一个特殊的“主机映射”文件,通常是一个可编辑的文本,在 Linux 里是“/etc/hosts”,在Windows里是C:\WINDOWS\system32\drivers\etc\hosts”,
    如果操作系统在缓存里找不到 DNS 记录,就会找这个文件。

★,可以使用 bind9等开源软件搭建一个在内部使用的DNS,作为名字服务器。这样我们开发的各种内部服务就都用域名来标记。

★,基于域名实现负载均衡:域名解析可以返回多个 IP地址,所以一个域名可以对应多台主机。有两种方式实现负载均衡,两种方式可以混用

  • 客户端收到多个 IP地址后,就可以自己使用轮询算法依次向服务器发起请求,实现负载均衡。
  • 域名解析可以配置内部的策略,返回离客户端最近的主机,或者返回当前服务质量最好的主机,这样在 DNS 端把请求分发到不同的服务器,实现负载均衡。

★,

※,自己动手,搭建HTTP实验环境

★,“最小化”环境用到的应用软件:Wireshark、Chrome/Firefox、Telnet、OpenResty

Telnet使用方法

Telnet是一个经典的虚拟终端,基于 TCP协议远程登录主机,我们可以使用它来模拟浏览器的行为,连接服务器后手动发送 HTTP请求,把浏览器的干扰也彻底排除,能够从最原始的层面去研究 HTTP 协议。telnet使用方法(比较原始):

  • `telnet localhost 80` //连接web服务器
  • 连接上之后按组合键·ctrl+]·
  • 然后按回车键,此时就进入了编辑模式
  • 在编辑模式中可以手动输入HTTP请求(包括请求行、请求头、请求体等),也可以使用鼠标右键粘贴文本 // 注:亲测可用,如果不可用,可能是粘贴的文本中含有一些特殊字符!!!
    • 这里注意!!!:如果输入了随意的字符(非HTTP请求格式),Telnet会直接报错400 Bad Request,然后断开连接(遗失对主机的连接)。POST请求中的Content-Length如果设置的值比实际输入的body短,那么就会出现这种情况。
  • 然后按两下回车就会发送数据,也就是模拟了一次HTTP请求!

Telnet示例:

  • -- 正常GET请求
    GET /09-1 HTTP/1.1
    Host:   www.chrono.com
    
    -- 格式异常的GET请求,到的会是一个“400 BadRequest”,表示请求报文格式有误,服务器无法正确处理
    GET /09-1 HTTP/1.1
    Host : www.chrono.com
    
    -- POST请求。
    POST /10-2 HTTP/1.1
    Host: www.chrono.com
    Content-Length: 3
    
    abc
    // 注意:发送POST请求时,Content-Length(即空行后的body长度,如abc长度为3)一定要设置正确。如果不正确会出现两种情况:
    // 1. Content-Length设置的值A小于实际输入的body字符串的长度B:此时Telnet只会读取B中前A个字符,剩下的字符就相当于下一次Telnet的输入,
    //    当Telnet中发现输入的是随意字符时(非HTTP请求格式)就会报错400 Bad Request,然后断开与服务器的连接。
    // 2. Content-Length设置的值A大于实际输入的body字符串的长度B:此时Telnet会继续等待输入,直至输入的字符串的实际长度等于A

★,Telnet也可以用nc命令或socat命令代替!参考另一篇博文

※,HTTP协议报文解析

★,HTTP 报文结构就像是“大头儿子”,由“起始行 + 头部 + 空行 + 实体”组成,简单地说就是“header+body”;HTTP 报文可以没有 body,但必须要有 header,而且header后也必须要有空行(就像人的脖子),形象地说就是“大头”必须要带着“脖子”。

  • 请求头:由“请求行 + 头部字段”构成,响应头由“状态行 + 头部字段”构成;
  • 请求行有三部分:请求方法,请求目标和版本号;
  • 状态行也有三部分:版本号,状态码和原因字符串;
  • 头部字段是 key-value的形式,用“:”分隔,不区分大小写,顺序任意,除了规定的标准头,也可以任意添加自定义字段,实现功能扩展

★,头字段格式需要注意下面几点:

  • 字段名不区分大小写,例如“Host”也可以写成“host”,但首字母大写的可读性更好;
  • 字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线“_”。例如,“test-name”是合法的字段名,而“test name”“test_name”是不正确的字段名;
  • 字段名后面必须紧接着“:”,不能有空格,而“:”后的字段值前可以有多个空格;
  • 字段的顺序是没有意义的,可以任意排列不影响语义;
  • 字段原则上不能重复,除非这个字段本身的语义允许,例如Set-Cookie。

★,一次HTTP请求抓包分析

  • 前三个包为TCP三次握手
  • 第四个包(No.7)为浏览器向服务端发送的HTTP请求(此次HTTP请求中包含了TCP传输,即基于TCP协议)
  • 第五个包(No.8)为服务端在TCP协议层面向浏览器发送的ACK包,告诉浏览器:"刚才的报文我已经收到了",不过这个TCP包HTTP协议是看不到的
  • 第六个包(No.9)为服务端发送给浏览器的HTTP包,底层走的还是TCP协议。
  • 第七个包(No.10)为浏览器回复给服务器的一个TCP的ACK确认:"你的响应报文收到了,多谢!"
  • TCP关闭连接的“四次挥手”在抓包里没有出现,这是因为 HTTP/1.1总是默认启用keepalive长连接机制,默认不会立即关闭连接。

★,【扩展】TCP三次握手和四次挥手以及相关的状态

四次挥手:四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

为什么连接的时候是三次握手,关闭的时候却是四次握手?

建立连接时因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。所以建立连接只需要三次握手。

由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式。这就意味着,关闭连接时,当Client端发出FIN报文段时,只是表示Client端告诉Server端数据已经发送完毕了。当Server端收到FIN报文并返回ACK报文段,表示它已经知道Client端没有数据发送了,但是Server端还是可以发送数据到Client端的,所以Server很可能并不会立即关闭SOCKET,直到Server端把数据也发送完毕。当Server端也发送了FIN报文段时,这个时候就表示Server端也没有数据要发送了,就会告诉Client端,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。

★,常用头字段:

协议规定了非常多的头部字段,实现各种各样的功能,但基本上可以分为四大类:

  • 通用字段:在请求头和响应头里都可以出现;
  • 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
  • 响应字段:仅能出现在响应头里,补充说明响应报文的信息;
  • 实体字段:它实际上属于通用字段,但专门描述 body的额外信息。

Host字段:

  • 属于请求字段,只能出现在请求头里.
  • HTTP/1.0不支持Host请求头(HTTP/1.0的所有请求头都是可选的)。而在HTTP/1.1中,Host请求头部必须存在(HTTP/1.1中唯一要求必须存在的请求头),否则会返回400 Bad Request。
  • Host字段告诉服务器这个请求应该由哪个主机来处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host字段来选择使用哪个虚拟主机。

User-Agent:是请求字段,只出现在请求头里。

Date字段:是一个通用字段,但通常出现在响应头里,表示HTTP报文创建的时间,客户端可以使用这个时间再搭配其他字段决定缓存策略。

Server字段:是响应字段,只能出现在响应头里。它告诉客户端当前正在提供 Web服务的软件名称和版本号,Server字段也不是必须要出现的,因为这会把服务器的一部分信息暴露给外界,如果这个版本恰好存在 bug,那么黑客就有可能利用 bug攻陷服务器。所以,有的网站响应头里要么没有这个字段,要么就给出一个完全无关的描述信息。比如 GitHub,它的 Server 字段里就看不出是使用了 Apache还是 Nginx,只是显示为“GitHub.com”。

Content-Length字段:属于实体字段(通用字段)。表示报文里body的长度(以字节为单位的十进制数),也就是请求头或响应头空行后面数据的长度。服务器看到这个字段,就知道了后续有多少数据,可以直接接收。如果没有这个字段,那么body就是不定长的,需要使用chunked方(Transfer-Encoding:chunked)式分段传输。

  • Content-Length如果存在并且有效地话,则必须和消息内容的传输长度完全一致,否则就会导致异常。
  • 这个大小是包含了所有内容编码的, 比如,对文本文件进行了gzip压缩的话,Content-Length首部指的就是压缩后的大小而不是原始大小。
  • GET请求报文一般不携带body实体,所以不会有Content-Length,但是GET请求的响应报文中包含实体,需要有Content-Length(非分段传输场景)。
  • POST请求报文和响应报文都需要有Content-Length(非分段传输场景)。

★,理解请求头里的请求方法:

请求头的实际含义是:户端发出了一个“动作指令”,要求服务器端对 URI 定位的资源执行这个动作。服务器掌控着所有资源,也就有绝对的决策权力。它收到HTTP请求报文后,看到里面的请求方法,可以执行也可以拒绝,或者改变动作的含义。比如,你发起了一个 GET请求,想获取“/orders”这个文件,但这个文件保密级别比较高,不是谁都能看的,服务器就可以有如下的几种响应方式:

  • 假装这个文件不存在,直接返回一个 404 Not found 报文;
  • 稍微友好一点,明确告诉你有这个文件,但不允许访问,返回一个 403 Forbidden;
  • 再宽松一些,返回 405 Method Not Allowed,然后用 Allow头告诉你可以用 HEAD 方法获取文件的元信息。【注:405表示请求方法不被允许!】

目前 HTTP/1.1 规定了八种方法,单词都必须是大写的形式:

  • GET:获取资源,可以理解为读取或者下载数据;
  • HEAD:获取资源的元信息;
  • POST:向资源提交数据,相当于写入或上传数据;
  • PUT:类似 POST;
  • DELETE:删除资源;
  • CONNECT:建立特殊的连接隧道;
  • OPTIONS:列出可对资源实行的方法;
  • TRACE:追踪请求 - 响应的传输路径。

GET:

  • GET 方法虽然基本动作比较简单,但搭配 URI和其他头字段就能实现对资源更精细的操作。例如,在 URI后使用“#”,就可以在获取页面后直接定位到某个标签所在的位置;使用If-Modified-Since字段就变成了“有条件的请求”,仅当资源被修改时才会执行获取动作;使用 Range字段就是“范围请求”,只获取资源的一部分数据。

HEAD:

  • HEAD 方法可以看做是 GET方法的一个“简化版”或者“轻量版”。因为它的响应头与 GET完全相同,所以可以用在很多并不真正需要资源的场合。比如想要检查一个文件是否存在,再比如,要检查文件是否有最新版本,
    同样也应该用HEAD,服务器会在响应头里把文件的修改时间传回来

POST/PUT:

PUT 的作用与 POST 类似,也可以向服务器提交数据,但与POST 存在微妙的不同,通常 POST表示的是“新建”“create”的含义,而 PUT则是“修改”“update”的含义。在实际应用中,PUT 用到的比较少。而且,因为它与 POST的语义、功能太过近似,有的服务器甚至就直接禁止使用 PUT方法,只用 POST 方法上传数据。

DELETE:
DELETE方法指示服务器删除资源,因为这个动作危险性太大,所以通常服务器不会执行真正的删除操作,而是对资源做一个删除标记。当然,更多的时候服务器就直接不处理 DELETE 请求

CONNECT:
CONNECT是一个比较特殊的方法,要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道,这时 Web 服务器在中间充当了代理的角色

OPTIONS:
OPTIONS方法要求服务器列出可对资源实行的操作方法,在响应头的Allow字段里返回。它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支持,但nginx可以使用配置指令、自定义模块或Lua脚本实现。

TRACE:
TRACE方法多用于对 HTTP链路的测试或诊断,可以显示出请求 -响应的传输路径。它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。

请求方法的扩展方法:
虽然 HTTP/1.1里规定了八种请求方法,但它并没有限制我们只能用这八种方法,这也体现了 HTTP协议良好的扩展性,我们可以任意添加请求动作,只要请求方和响应方都能理解就行
例如著名的愚人节玩笑 RFC2324,它定义了协议HTCPCP,即“超文本咖啡壶控制协议”,为 HTTP协议增加了用来煮咖啡的 BREW 方法,要求添牛奶的 WHEN方法。此外,还有一些得到了实际应用的请求方法(WebDAV),例如 MKCOL、COPY、MOVE、LOCK、UNLOCK、PATCH等。如果有合适的场景,你也可以把它们应用到自己的系统里,比如用 LOCK 方法锁定资源暂时不允许修改,或者使用PATCH方法给资源打个小补丁,部分更新数据。但因为这些方法是非标准的,所以需要为客户端和服务器编写额外的代码才能添加支持。当然了,你也完全可以根据实际需求,自己发明新的方法,比如“PULL”拉取某些资源到本地,“PURGE”清理某个目录下的所有缓存数据。

请求方法的安全与幂等

“安全”与“幂等”是描述请求方法的两个重要属性,具有理论指导意义,可以帮助我们设计系统。

谓的“安全”是指请求方法不会“破坏”服务器上的资源,即不会对服务器上的资源造成实质的修改。按照这个定义,只有 GET 和 HEAD方法是“安全”的,因为它们是“只读”操作,只要服务器不故意曲解请求方法的处理方式,无论 GET 和HEAD 操作多少次,服务器上的数据都是“安全的”。而 POST/PUT/DELETE操作会修改服务器上的资源,增加或删除数据,所以是“不安全”的。

“幂等”实际上是一个数学用语,被借用到了 HTTP协议里,意思是多次执行相同的操作,结果也都是相同的,即多次“幂”后结果“相等”。GET 和 HEAD 既是安全的也是幂等的,DELETE可以多次删除同一个资源,效果都是“资源不存在”,所以也是幂等的。POST 和 PUT 的幂等性质就略费解一点。按照 RFC 里的语义,POST是“新增或提交数据”,多次提交数据会创建多个资源,所以不是幂等的;而 PUT是“替换或更新数据”,多次更新一个资源,资源还是会第一次更新的状态,所以是幂等的。我对你的建议是,你可以对比一下 SQL 来加深理解:把POST 理解成 INSERT,把 PUT 理解成UPDATE,这样就很清楚了。多次 INSERT会添加多条记录,而多次 UPDATE只操作一条记录,而且效果相同。

※,11 URI/URL

URI的完整形式:其中`host:port`部分称为authority,path必须是以反斜杠`/`开头。

示例:

·file:///D:/http_study/www/·   //这里file是scheme,·://·是分隔符,属于固定格式,·/D:/http_study/www/·是path,中间的authority(host:path)被省略了,这是file类型URI的特例,它允许省略主机名,默认是本机localhost

★,URI的编码

刚才我们看到了,在 URI 里只能使用 ASCII 码,但如果要在URI 里使用英语以外的汉语、日语等其他语言该怎么办呢?还有,某些特殊的 URI,会在 path、query里出现“@&?"等起界定符作用的字符,会导致 URI解析错误,这时又该怎么办呢?所以URI 引入了编码机制,对于 ASCII码以外的字符集和特殊字符做一个特殊的操作,把它们转换成与 URI 语义不冲突的形式。这在 RFC规范里称为“escape”和“unescape”,俗称“转义”。URI 转义的规则有点“简单粗暴”,直接把非 ASCII码或特殊字符转换成十六进制字节值,然后前面再加上一个“%”。例如,空格被转义成“%20”,“?”被转义成“%3F”。而中文、日文等则通常使用 UTF-8编码后再转义,例如“银河”会被转义成“%E9%93%B6%E6%B2%B3”。

※,12 响应状态码

状态行的结构,有三部分

★,RFC标准将状态码分成了五类:

  • 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作;
    • 101 Switching Protocols”。它的意思是客户端使用 Upgrade 头字段,要求在 HTTP协议的基础上改成其他的协议继续通信,比如WebSocket。而如果服务器也同意变更协议,就会发送状态码101,但这之后的数据传输就不会再使用 HTTP 了
  • 2××:成功,报文已经收到并被正确处理;
    • “200 OK”是最常见的成功状态码,表示一切正常。服务器如客户端所期望的那样返回了处理结果,如果是非HEAD 请求,通常在响应头后都会有 body 数据
    • “204 No Content”是另一个很常见的成功状态码,它的含义与“200OK”基本相同,但响应头后没有 body 数据。
    • “206 Partial Content”是 HTTP分块下载或断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200一样,也是服务器成功处理了请求,但 body里的数据不是资源的全部,而是其中的一部分。状态码 206 通常还会伴随着头字段“Content-Range”,表示响应报文里 body数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/2000”,意思是此次获取的是总计 2000 个字节的前 100
      个字节。
  • 3××:重定向,资源位置发生变动,需要客户端重新发送请求;
    • “301 Moved Permanently”俗称“永久重定向”,含义是此次请求的资源已经不存在了,需要改用改用新的 URI 再次访问。
    • “302 Found”,曾经的描述短语是“Moved Temporarily”,俗称“临时重定向”,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。
    • 301 和 302 都会在响应头里使用字段Location指明后续要跳转的URI。301和302还有两个等价的状态码:“308 Permanent Redirect”和“307 Temporary Redirect”,但这两个状态码不允许后续的请求更改请求方法。
    • “304 Not Modified” 是一个比较有意思的状态码,它用于If-Modified-Since等条件请求,表示资源未修改,用于缓存控制。它不具有通常的跳转含义,但可以理解成“重定向已到缓存的文件”(即“缓存重定向”)。
  • 4××:客户端错误,请求报文有误,服务器无法处理;
    • “400 Bad Request”是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI超长它没有明确说,只是一个笼统的错误,客户端看到 400只会是“一头雾水”“不知所措”。所以,在开发 Web应用时应当尽量避免给客户端返回400,而是要用其他更有明确含义的状态码。
    • “403 Forbidden”实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等
    • “404 Not Found”可能是我们最常看见也是最不愿意看到的一个状态码,它的原意是资源在本服务器上未找到,所以无法提供给客户端。但现在已经被“用滥了”,只要服务器“不高兴”就可以给出个404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因
    • 405 Method NotAllowed:不允许使用某些方法操作资源,例如不允许 POST只能 GET;
    • 406 Not Acceptable:资源无法满足客户端请求的条件,例如请求中文但只有英文;
    • 408 Request Timeout:请求超时,服务器等待了过长的时间;
    • 409 Conflict:多个请求发生了冲突,可以理解为多线程并发时的竞态;
    • 413 Request Entity Too Large:请求报文里的 body 太大;
    • 414 Request-URI Too Long:请求行里的 URI 太大;
    • 429 Too Many Requests:客户端发送了太多的请求,通常是由于服务器的限连策略;
    • 431 Request Header Fields Too Large:请求头某个字段或总体太大;
  • 5××:表示客户端请求报文正确,但服务器在处理时内部发生了错误,无法返回应有的响应数据,是服务器端的“错误码”。
    • “500 Internal Server Error”与 400类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。
    • “501 Not Implemented”表示客户端请求的功能还不支持
    • “502 Bad Gateway”通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器【自注:作为代理的服务器需要访问的上游服务器】时发生了错误,但具体的错误原因也是不知道的。
    • “503 Service Unavailable”表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503。503是一个“临时”的状态,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求

目前 RFC 标准里总共有 41个状态码,但状态码的定义是开放的,允许自行扩展。所以Apache、Nginx 等 Web服务器都定义了一些专有的状态码。如果你自己开发 Web应用,也完全可以在不冲突的前提下定义新的代码。

★,

※,13 HTTP有哪些特点

  • 灵活可扩展:可以任意添加头字段实现任意功能
  • 可靠传输:HTTP 是可靠传输协议,基于 TCP/IP协议“尽量”保证数据的送达;我们必须正确地理解“可靠”的含义,HTTP 并不能 100%保证数据一定能够发送到另一端,在网络繁忙、连接质量差等恶劣的环境下,也有可能收发失败。“可靠”只是向使用者提供了一个“承诺”,会在下层用多种手段“尽量”保证数据的完整送达。
    • 如果要100%保证数据收发成功就不能使用HTTP或者TCP协议了,而是要用各种消息中间件(MQ),如RabbitMQ、RocketMQ、kafka等。
    • 【参考文章:为什么消息中间件不直接使用HTTP协议
      • 这个问题问得很准确,并不是说所有消息中间件一定不使用 http 协议,而是“不直接”使用 http 协议。以 RocketMQ 为例,使用的是 grpc 协议,本质是 protobuf(编码协议) + http2.0(传输协议),底层其实使用了 http 协议。当然还有一些消息中间件,是完全没有使用 http 协议,比如 kafka,直接使用了 TCP 协议。消息中间件关注的一个核心点是吞吐量,所以对性能的要求比较高,而 http 协议,特别是 http1.x 版的协议,不具备多路复用的能力(http 2.0 具备),且消息头和消息体都比较大(http2.0 会做头部压缩,protobuf 则直接对消息做了编码压缩)。此外,在消息传输过程中,我们更看重数据的压缩率,网络连接的复用情况,以及特定的优化手段。而 http 协议作为应用层协议,包含很多 MQ 场景可能并不需要的功能,但这些功能(比如额外的头部信息)可能会带来额外的开销;且作为应用层协议,http 缺乏一定的灵活性(越底层越灵活)。综上,http 协议不太适合用来直接作为消息中间件的通信协议。

  • 应用层协议:HTTP 是应用层协议,比 FTP、SSH等更通用功能更多,能够传输任意数据
  • 请求-应答:HTTP 使用了请求 - 应答模式,客户端主动发起请求,服务器被动回复请求;
  • 无状态:
    • TCP协议有状态,ESTABLISHED,CLOSED等状态。
    • udp协议无连接无状态,顺序发包乱序收包,数据包发出去后就不管了,收到后也不会顺序整理。
    • HTTP是有连接无状态,顺序发包顺序收包,按照收发的顺序管理报文。HTTP的无状态即每个请求都是互相独立、毫无关联的,协议不要求客户端或服务器记录请求相关的信息。
    • 无状态”对于 HTTP来说既是优点也是缺点。“无状态”有什么好处呢?因为服务器没有“记忆能力”,所以就不需要额外的资源来记录状态信息,不仅实现上会简单一些,而且还能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。而且,“无状态”也表示服务器都是相同的,没有“状态”的差异,所以可以很容易地组成集群,让负载均衡把请求转发到任意一台服务器,不会因为状态不一致导致处理出错,使用“堆机器”的“笨办法”轻松实现高并发高可用。
      那么,“无状态”又有什么坏处呢
      既然服务器没有“记忆能力”,它就无法支持需要连续多个步骤的“事务”操作。例如电商购物,首先要登录,然后添加购物车,再下单、结算、支付,这一系列操作都需要知道用户的身份才行,但“无状态”服务器是不知道这些请求是相互关联的,每次都得问一遍身份信息,不仅麻烦,而且还增加了不必要的数据传输量。
      所以,HTTP协议最好是既“无状态”又“有状态”,不过还真有“鱼和熊掌”两者兼得这样的好事,这就是“小甜饼”Cookie 技术

★,

※,15 | 海纳百川:HTTP的实体数据

在 TCP/IP协议栈里,传输数据基本上都是“header+body”的格式。但TCP、UDP 因为是传输层的协议,它们不会关心 body数据是什么(但是关心header里的数据,TCP、udp协议里的header已经被规定了固定的格式,接收到之后按照规定好的格式解析即可。HTTP的body没有规定固定的格式,所以需要通过头字段让 客户端和服务器进行“内容协商”),只要把数据发送到对方就算是完成了任务。而 HTTP协议则不同,它是应用层的协议,数据到达之后工作只能说是完成了一半,还必须要告诉上层应用这是什么数据才行。

★,数据类型与编码:解决计算机理解 body数据的问题

  • 客户端用 ·Accept·头告诉服务器希望接收什么样的数据,服务器用·Content-Type·头告诉客户端实际发送了什么样的数据。
    • Accept: text/html,application/xml,image/webp,image/png
    • Content-Type: text/html
  • ·Accept-Encoding·字段标记的是客户端支持的压缩格式,服务器实际使用的压缩格式放在响应头字段·Content-Encoding·里。这两个字段是可以省略的,如果请求报文里没有Accept-Encoding字段,就表示客户端不支持压缩数据;如果响应报文里没有Content-Encoding 字段,就表示响应数据没有被压缩。
    • Accept-Encoding: gzip, deflate, br
    • Content-Encoding: gzip
  •  

★,语言类型与编码:解决“国际化”问题

·Accept-Language·字段标记了客户端可理解的自然语言,也允许用“,”;服务器应该在响应报文里用头字段·Content-Language·告诉客户端实体数据使用的实际语言类型。

  • Accept-Language: zh-CN, zh, en
  • Content-Language: zh-CN

编码字符集在 HTTP 里使用的请求头字段是Accept-Charset,但响应头里却没有对应的 Content-Charset,而是在Content-Type字段的数据类型后面用“charset=xxx”来表示。

  • Accept-Charset: gbk, utf-8
  • Content-Type: text/html; charset=utf-8

现在的浏览器都支持多种字符集,通常不会发送Accept-Charset,而服务器也不会发送Content-Language,因为使用的语言完全可以由字符集推断出来,所以在请求头里一般只会有 Accept-Language字段,响应头里只会有 Content-Type 字段。

★,内容协商的质量值

在 HTTP 协议里用 Accept、Accept-Encoding、Accept-Language等请求头字段进行内容协商的时候,还可以用一种特殊的“q”参数表示权重来设定优先级,这里的“q”是“qualityfactor”的意思。权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0就表示拒绝。具体的形式是在数据类型或语言代码后面加一个“;”,然后是“q=value”。这里要提醒的是“;”的用法,大多数编程语言里“;”的断句语气要强于“,”,而在 HTTP的内容协商里却恰好反了过来,“;”的意义是小于“,”的。例如下面的 Accept 字段:Accept: text/html,application/xml;q=0.9,*/*;q=0.8它表示浏览器最希望使用的是 HTML 文件,权重是1,其次是 XML 文件,权重是0.9,最后是任意数据类型,权重是0.8。

★,内容协商的结果

有的时候,服务器会在响应头里多加一个Vary字段,记录服务器在内容协商时参考的请求头字段,给出一点信息,例如:Vary: Accept-Encoding,User-Agent,Accept这个 Vary 字段表示服务器依据了Accept-Encoding、User-Agent 和 Accept这三个头字段,然后决定了发回的响应报文。

★,总结

 

※,16 |把大象装进冰箱:HTTP传输大文件的方法

★,数据压缩

★,分块传输: 【注:传输的是整个大文件,区分与多段数据的传输格式】

分块传输在响应报文里用头字段“Transfer-Encoding:chunked”来表示,意思是报文里的 body部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。

分块传输时 body数据的长度是未知的,无法在头字段“Content-Length”里给出确切的长度,所以也只能用 chunked 方式分块发送。“Transfer-Encoding: chunked”和“Content-Length”这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(hunked),这一点你一定要记住。

分块传输的编码规则:采用了明文的方式,很类似响应头。每个分块包含两个部分,长度头和数据块;长度头是以CRLF(回车换行,即\r\n)结尾的一行明文,用 16进制数字表示长度;数据块紧跟在长度头后,最后也用 CRLF结尾,但数据不包含 CRLF;最后用一个长度为 0 的块表示结束,即“0\r\n\r\n”,如下图所示:

问题:

  • Q: 分块传输数据的时候,如果数据里含有回车换行(\r\n)是否会影响分块的处理呢?
    • A: 分块传输中数据里含有回车换行(\r\n)不影响分块处理,因为分块前有数据长度说明

★,范围请求:分块传输是把整个文件分成若干小块进行传输,如果客户端想获取一个大文件其中的片段数据,分块传输并没有这个能力。HTTP 协议为了满足这样的需求,提出了“范围请求”(range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分。

范围请求不是 Web服务器必备的功能,可以实现也可以不实现,所以服务器必须在响应头里使用字段“·Accept-Ranges: bytes·”明确告知客户端:“我是支持范围请求的”。
如果不支持的话该怎么办呢?服务器可以发送“Accept-Ranges:none”,或者干脆不发送“Accept-Ranges”字段,这样客户端就认为服务器没有实现范围请求功能,只能老老实实地收发整块文件了。

  • 请求头·Range·是 HTTP 范围请求的专用字段,格式是“bytes=x-y”,其中的 x 和 y 是以字节为单位的数据范围,从0计数。如:·Range: bytes=0-31·

服务器收到 Range 字段后,需要做四件事。

  • 第一,它必须检查范围是否合法,比如文件只有 100个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码416,意思是“你的范围请求有误,我无法处理,请再检查一下”。
  • 第二,如果范围正确,服务器就可以根据 Range头计算偏移量,读取文件的片段了,返回状态码“206 Partial Content”,和 200 的意思差不多,但表示 body只是原数据的一部分。
  • 第三,服务器要添加一个响应头字段·Content-Range·,告诉片段的实际偏移量和资源的总大小,格式是“bytes x-y/length”(如:·Content-Range: bytes 0-31/96·),与 Range头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes 0-10/100”。
  • 第四:最后剩下的就是发送数据了,直接把片段用 TCP发给客户端,一个范围请求就算是处理完了

有了范围请求之后,HTTP处理大文件就更加轻松了,看视频时可以根据时间点计算出文件的Range,不用下载整个文件,直接精确获取片段所在的数据内容。不仅看视频的拖拽进度需要范围请求,常用的下载工具里的多段下载、断点续传也是基于它实现的,要点是:
先发个HEAD,看服务器是否支持范围请求,同时获取文件的大小;开 N 个线程,每个线程使用 Range字段划分出各自负责下载的片段,发请求传输数据;下载意外中断也不怕,不必重头再来一遍,只要根据上次的下载记录,用 Range请求剩下的那一部分就可以了。

  • Q:  如果对一个被 gzip 的文件执行范围请求,比如“Range: bytes=10-19”,那么这个范围是应用于原文件还是压缩后的文件呢?
  • A:  range是针对原文件的。

★,多段数据:范围请求一次只获取一个片段,其实它还支持在Range 头里使用多个“x-y”,一次性获取多个片段数据。这种情况需要使用一种特殊的 MIME 类型:“
multipart/byteranges”,表示报文的 body是由多段字节序列组成的,并且还要用一个参数“boundary=xxx”给出段之间的分隔标记。多段数据的传输格式如下图所示:

每一个分段必须以“--boundary”开始(前面加两个“-”),之后要用“Content-Type”和“Content-Range”标记这段数据的类型和所在范围,然后就像普通的响应头一样以回车换行结束,再加上分段数据,最后用一个“- -boundary--”(前后各有两个“-”)表示所有的分段结束。

示例:

例如,我们在实验环境里用 Telnet 发出有两个范围的请求:
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-9, 20-29
得到的就会是下面这样:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=
00000000001
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
 
 
--00000000001
Content-Type: text/plain
Content-Range: bytes 0-10/96

hello world
--00000000001
Content-Type: text/plain
Content-Range: bytes 20-29/96
 
ext json d
--00000000001--
报文里的“--00000000001”就是多段的分隔符,使用它客户端就可以很容易地区分出多段 Range 数据。

 

※,17 | 排队也要讲效率:HTTP的连接管理

★,短连接

HTTP协议最初(0.9/1.0)是个非常简单的协议,通信过程也采用了简单的“请求 - 应答”方式。它底层的数据传输基于TCP/IP,每次发送请求前需要先与服务器建立连接,
收到响应报文后会立即关闭连接。因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为“短连接”(short-lived connections)。早期的 HTTP 协议也被称为是“无连接”的协议。

短连接的缺点相当严重,因为在 TCP协议里,建立连接和关闭连接都是非常“昂贵”的操作。TCP建立连接要有“三次握手”,发送 3 个数据包,需要 1 个RTT;关闭连接是“四次挥手”,4 个数据包需要 2 个 RTT。而 HTTP 的一次简单“请求 - 响应”通常只需要 4个包,如果不算服务器内部的处理时间,最多是 2 个RTT。这么算下来,浪费的时间就是“3÷5=60%”,有三分之二的时间被浪费掉了,传输效率低得惊人.

★,长连接

长连接对性能的改善效果非常显著,所以在 HTTP/1.1中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP连接,也就是长连接,在这个连接上收发数据。当然,我们也可以在请求头里明确地要求使用长连接机制,使用的字段是Connection,值是“keep-alive”。不过不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在响应报文里放一个“`Connection: keep-alive`”字段,告诉客户端:“我是支持长连接的,接下来就用这个TCP 一直收发数据吧”。

  • Connection还有一个取值:·Connection: Upgrade·,配合状态码101表示协议升级,例如从HTTP协议切换到WebSocket。

长连接也有缺点:因为 TCP连接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务器无法为真正有需要的用户提供服务。所以,长连接也需要在恰当的时间关闭,不能永远保持与服务器的连接,这在客户端或者服务器都可以做到。

  • 在客户端,可以在请求头里加上“Connection: close”字段,告诉服务器:“这次通信后就关闭连接”。服务器看到这个字段,就知道客户端要主动关闭连接,于是在响应报文里也加上·Connection: close·这个字段,发送之后就调用 SocketAPI 关闭 TCP 连接。
  • 服务器端通常不会主动关闭连接,但也可以使用一些策略。拿Nginx 来举例,它有两种方式:
    • 用“keepalive_timeout”指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。
    • 使用“keepalive_requests”指令,设置长连接上可发送的最大请求次数。比如设置成1000,那么当 Nginx 在这个连接上处理了 1000个请求后,也会主动断开连接。
  • 另外:客户端和服务器都可以在报文里附加通用头字段“Keep-Alive:timeout=value”,限定长连接的超时时间。但这个字段的约束力并不强,通信的双方可能并不会遵守,所以不太常见。

★,队头阻塞(Head-of-line blocking,也叫“队首阻塞”):一个HTTP的TCP连接中,多个请求组成一个“串行”队列,这就可能会导致队头阻塞问题。

“队头阻塞”与短连接和长连接无关,而是由 HTTP基本的“请求 - 应答”模型所导致的。因为 HTTP规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。

队头阻塞优化方案:

因为“请求 - 应答”模型不能变,所以“队头阻塞”问题在HTTP/1.1 里无法解决,只能缓解。方法有二:

  • “并发连接”(concurrent connect):,也就是同时对一个域名发起多个长连接,用数量来解决质量的问题。
    • 但这种方式也存在缺陷。如果每个客户端都想自己快,建立很多个连接,用户数×并发数就会是个天文数字。服务器的资源根本就扛不住,或者被服务器认为是恶意攻击,反而会造成“拒绝服务”。所以,HTTP协议建议客户端使用并发,但不能“滥用”并发。RFC2616里明确限制每个客户端最多并发 2个连接。不过实践证明这个数字实在是太小了,众多浏览器都“无视”标准,把这个上限提高到了 6~8。后来修订的 RFC7230也就“顺水推舟”,取消了这个“2”的限制。
    • 利用HTTP的长连接特性对服务器发起大量的请求,导致服务器最终耗尽资源“拒绝服务”,这就是常说的DDos。
  • “域名分片”(domain shard):,还是用数量来解决质量的思路。HTTP协议和浏览器不是限制并发连接数量吗?好,那我就多开几个域名,比如shard1.chrono.com、shard2.chrono.com,而这些域名都指向同一台服务器www.chrono.com,这样实际长连接的数量就又上去了,真是“美滋滋”。不过实在是有点“上有政策,下有对策”的味道。

★,

※,18 | 四通八达:HTTP的重定向和跳转

★,用于标识重定向的“Location”字段属于响应字段,必须出现在响应报文里。但只有配合301/302 状态码才有意义,它标记了服务器要求重定向的URI。

★,重定向报文里还可以用Refresh字段实现延迟重定向,如·Refresh: 5;url=xxx·告诉浏览器5秒钟后再跳转。

★,与跳转有关的还有一个·Referer·和·Referrer-Policy·(注意前者是个拼写错误,但已经将错就错!),表示浏览器跳转的来源(即引用地址),用于统计分析和防盗链。

★,如果重定向的策略设置欠考虑,可能会出现“A=>B=>C=>A”的无限循环,不停地在这个链路里转圈圈。HTTP协议特别规定,浏览器必须具有检测“循环跳转”的能力,在发现这种情况时应当停止发送请求并给出错误提示。

★,

※,19 |让我知道你是谁:HTTP的Cookie机制

★,Cookie 的工作过程

Cookie是服务器委托浏览器存储的一些数据,让服务器有了“记忆能力”;

客户端和服务器之间进行cookie的传递需要用到两个字段:响应头字段·Set-Cookie·和请求头字段·Cookie·。cookie是由浏览器负责存储的,而不是操作系统。所以,它是“浏览器绑定”的,只能在本浏览器内生效。

★,Cookie 的属性

Cookie就是服务器委托浏览器存储在客户端里的一些数据,而这些数据通常都会记录用户的关键识别信息。所以,就需要在“key=value”外再用一些手段来保护,防止外泄或窃取,这些手段就是 Cookie 的属性。

一个cookie的实例如下:·Set-Cookie: favorite=hamburger; Max-Age=10; Expires=Wed, 21-Feb-24 09:55:57 GMT; Domain=localhost; Path=/; HttpOnly; SameSite=Strict·,cookie属性介绍如下:

  • Cookie 的生存周期:也就是它的有效期,让它只能在一段时间内可用,就像是食品的“保鲜期”,一旦超过这个期限浏览器就认为是Cookie 失效,在存储里删除,也不会发送给服务器。Cookie 的有效期可以使用 Expires 和 Max-Age两个属性来设置。Expires 和 Max-Age可以同时出现,两者的失效时间可以一致,也可以不一致,但浏览器会优先采用 Max-Age 计算失效期。
    • ·Expires·俗称“过期时间”,用的是绝对时间点,可以理解为“截止日期”(deadline)。
    • ·Max-Age·用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上Max-Age,就可以得到失效的绝对时间(Cookie 的 max-age 是从浏览器拿到响应报文时开始计算的)。
    • 如果不指定Expires或Max-Age 属性,那么Cookie 仅在浏览器运行时有效,一旦浏览器关闭就会失效,这被称为会话 Cookie (sessioncookie)或内存 Cookie(in-memory cookie)在 Chrome 里过期时间会显示为“Session”或N/A
  • Cookie 的作用域:让浏览器仅发送给特定的服务器和URI,避免被其他网站盗用。·Domain·和·Path·指定了 Cookie所属的域名和路径,浏览器在发送 Cookie 前会从 URI中提取出 host 和 path 部分,对比 Cookie
    的属性。如果不满足条件,就不会在请求头里发送 Cookie。现实中为了省事,通常 Path就用一个“/”或者直接省略,表示域名下的任意路径都允许使用Cookie,让服务器自己去挑。
  • Cookie 的安全性:尽量不要让服务器以外的人看到。在 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本身不是加密的,浏览器里还是以明文的形式存在。Chrome 开发者工具是查看 Cookie的有力工具

★,Cookie 的应用

  • 身份识别:实现有状态的会话“事务”(如登录、购物、付款三个活动构成的一个“事务”)。
  • 广告跟踪:你上网的时候肯定看过很多的广告图片,这些图片背后都是广告商网站(例如Google),它会“偷偷地”给你贴上 Cookie小纸条,这样你上其他的网站,别的广告就能用 Cookie读出你的身份,然后做行为分析,再推给你广告。这种 Cookie 不是由访问的主站存储的,所以又叫“第三方Cookie”(third-party cookie)

★,虽然现在已经出现了多种 Local Web Storage技术,能够比 Cookie 存储更多的数据,但 Cookie仍然是最通用、兼容性最强的客户端数据存储手段。

★,因为 Cookie 并不属于 HTTP标准(RFC6265,而不是RFC2616/7230),所以语法上与其他字段不太一致,使用的分隔符是“;”,与 Accept等字段的“,”不同,小心不要弄错了。

★,小贴士:

  • Cookie 这个词来源于计算机编程里的术语“Magic Cookie”,意思是不透明的数据,并不是“小甜饼”的含义 (虽然字面意如此)
  • 早期 Cookie 直接就是磁盘上的一些小文本文件,现在基本上都是以数据库记录的形式存放的 (通常使用的是 Sqlite)。浏览器对 Cookie的数量和大小也都有限制,不允许无限存储一般总大小不能超过4K
  •  

※,20 | 生鲜速递:HTTP的缓存控制

缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。HTTP传输的每一个环节基本上都会有缓存,非常复杂。基于“请求 -应答”模式的特点,可以大致分为客户端缓存和服务器端缓存,因为服务器端缓存经常与代理服务“混搭”在一起,所以今天先讲客户端——也就是浏览器的缓存。

·Cache-Control: max-age=0· //Cache 的 max-age 是从响应报文的生成时间(Date 头字段)开始计算而不是浏览器接受报文的时间(注意与cookie的区别)

服务器可以发“Cache-Control”头,浏览器也可以发“Cache-Control”,也就是说请求 -应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。

当点击浏览器“刷新”按钮的时候,浏览器会在请求头里加一个“Cache-Control: max-age=0”。浏览器就不会使用缓存,而是向服务器发请求。服务器看到max-age=0,也就会用一个最新生成的报文回应浏览器。

★,除了“Cache-Control”,服务器也可以用“Expires”字段来标记资源的有效期,它的形式和 Cookie 的差不多,同样属于“过时”的属性,优先级低于“Cache-Control”。

★,还有一个历史遗留字段“Pragma: no-cache”,它相当于“Cache-Control: no-cache”,除非为了兼容 HTTP/1.0 否则不建议使用。

★,

 

※,21 | 良心中间商:HTTP的代理服务

客户端---代理服务器---源服务器

★,代理的作用

你也许听过这样一句至理名言:“计算机科学领域里的任何问题,都可以通过引入一个中间层来解决”(在这句话后面还可以再加上一句“如果一个中间层解决不了问题,那就再加一个中间层”)。

  • 负载均衡: 
  • 健康检查:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用;
  • 安全防护:保护被代理的后端服务器,限制 IP地址或流量,抵御网络攻击和过载;
  • 加密卸载:对外网使用 SSL/TLS加密通信认证,而在安全的内网不加密,消除加解密成本;
  • 数据过滤:拦截上下行的数据,任意指定策略修改请求或者响应;
  • 内容缓存:暂存、复用服务器响应

★,代理相关头字段

`Via` : Via是一个通用字段,请求头或响应头里都可以出现。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾。

  • “Via”是HTTP 协议里规定的标准头字段,但有的服务器返回的响应报文里会使用“X-Via”含义是相同的

·X-Forwarded-For·的字面意思是“为谁而转发”,形式上和“Via”差不多,也是每经过一个代理节点就会在字段里追加一个信息。但“Via”追加的是代理主机名(或者域名),而“X-Forwarded-For”追加的是请求方的 IP 地址。所以,在字段里最左边的 IP地址就客户端的地址。有了“X-Forwarded-For”等头字段,源服务器就可以拿到准确的客户端信息了。

  • 因为 HTTP 是明文传输,请求头很容易被窜改所以“X-Forwarded-For”也不是完全可信的

·X-Real-IP·是另一种获取客户端真实 IP的手段,它的作用很简单,就是记录客户端 IP地址,没有中间的代理信息,相当于是“X-Forwarded-For”的简化版。如果客户端和源服务器之间只有一个代理,那么这两个字段的值就是相同的。

·X-Forwarded-Host· 和 ·X-Forwarded-Proto·,它们的作用与“X-Real-IP”类似,只记录客户端的信息,分别是客户端请求的原始域名和原始协议名。

★,代理协议:

专门的“代理协议”可以在不改动原始报文的情况下传递客户端的真实 IP。,它由知名的代理软件 HAProxy所定义,也是一个“事实标准”,被广泛采用(注意并不是RFC)。

★,tips

知名的代理软件有 HAProxy、Squid、Varnish等,而 Nginx 虽然是 Web 服务器,但也可以作为代理服务器,而且功能毫不逊色

★,

※,22 | 冷链周转:HTTP的缓存代理

HTTP传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,“就近”获得响应结果。

HTTP的服务器缓存功能主要由代理服务器来实现(即缓存代理),而源服务器系统内部虽然也经常有各种缓存(如Memcache、Redis、Varnish 等),但与 HTTP没有太多关系。

★,源服务器的缓存控制

服务器端的“Cache-Control”属性:max-age、no_store、no_cache 和 must-revalidate,这 4 种缓存属性可以约束客户端,也可以约束代理。但客户端和代理是不一样的,客户端的缓存只是用户自己使用,而代理的缓存可能会为非常多的客户端提供服务。所以,需要对它的缓存再多一些限制条件(也就是会多一些属性)

区分客户端上的缓存和代理上的缓存,可以使用两个新属性“private”和“public”:“private”表示缓存只能在客户端保存,是用户“私有”的,不能放在代理上与别人共享。而“public”的意思就是缓存完全开放,谁都可以存,谁都可以用。

★,客户端的缓存控制

客户端在 HTTP缓存体系里要面对的是代理和源服务器(之前在没有代理的情形下是只面对源服务器),也必须区别对待,

★,清理缓存
清理缓存的方法有很多,比较常用的一种做法是使用自定义请求方法“PURGE”,发给代理服务器,要求删除 URI 对应的缓存数据。

★,小结:

计算机领域里最常用的性能优化手段是“时空转换”,也就是“时间换空间”或者“空间换时间”,HTTP 缓存属于后者;缓存代理是增加了缓存功能的代理服务,缓存源服务器的数据,分发给下游的客户端;
“Cache-Control”字段也可以控制缓存代理,常用的有“private”“s-maxage”“no-transform”等,同样必须配合“Last-modified”“ETag”等字段才能使用;

★,

※,23 |HTTPS是什么?SSL/TLS又是什么?

之前讲了HTTP的一些缺点,其中的“无状态”在加入 Cookie后得到了解决,而另两个缺点——“明文”和“不安全”仅凭 HTTP。自身是无力解决的,需要引入新的 HTTPS 协议

★,通信安全必须同时具备机密性、完整性,身份认证和不可否认这四个特性;

★,什么是 HTTPS

HTTPS的全称是“HTTP over SSL/TLS”,也就是运行在 SSL/TLS 协议上的 HTTP。注意它的名字,这里是 SSL/TLS,而不是TCP/IP,它是一个负责加密通信的安全协议,建立在 TCP/IP之上,所以也是个可靠的传输协议,可以被用作 HTTP的下层。因为 HTTPS相当于“HTTP+SSL/TLS+TCP/IP”。

HTTPS抓包实际就是筛选出tls协议内容(tls && tcp.port==xxxx)

HTTPS 其实是一个“非常简单”的协议,RFC文档很小,只有短短的 7 页,里面规定了新的协议名“https”,默认端口号 443,至于其他的什么请求-应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。也就是说,除了协议名“http”和端口号 80 这两点不同,HTTPS协议在语法、语义上和 HTTP完全一样,优缺点也“照单全收”(当然要除去“明文”和“不安全”)。

然没有新东西,HTTPS凭什么就能做到机密性、完整性这些安全特性呢?秘密就在于 HTTPS 名字里的“S”,它把 HTTP下层的传输协议由 TCP/IP 换成了 SSL/TLS,由“HTTP over TCP/IP”变成了“HTTP over SSL/TLS”,让 HTTP运行在了安全的 SSL/TLS 协议上,收发报文不再使用 Socket API,而是调用专门的安全接口。

★,TLS: TLS1.0 实际上就是 SSLv3.1

TLS由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术。浏览器和服务器在使用 TLS建立连接时需要选择一组恰当的加密算法来实现安全通信,这些算法的组合被称为“密码套件”(ciphersuite,也叫加密套件)。

TLS的密码套件命名非常规范,格式很固定。基本的形式是“密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法”,比如`ECDHE-RSA-AES256-GCM-SHA384`密码套件的意思就是:“握手时使用 ECDHE 算法进行密钥交换,用 RSA签名和身份认证,握手后的通信使用 AES对称算法,密钥长度 256 位,分组模式是 GCM,摘要算法SHA384 用于消息认证和产生随机数。”

除了 HTTP,SSL/TLS 也可以承载其他的应用协议,例如 FTP=>FTPS,LDAP=>LDAPS 等

★,OpenSSL

说到 TLS,就不能不谈到OpenSSL,它是一个著名的开源密码学程序库和工具包,几乎支持所有公开的加密算法和协议,已经成为了事实上的标准,许多应用软件都会使用它作为底层库来实现 TLS功能,包括常用的 Web 服务器 Apache、Nginx 等。

OpenSSL里的密码套件定义与 TLS略有不同TLS里的形式是“TLS ECDHE_RSAWITH AES_256_GCM_SHA384”,加了前缀“TLS”,并用“WITH”分开了握手和通信的算法。

另一个比较著名的开源密码库是 NSS(Network Security Services),由 Mozilla 开发。

★,

※,24 |固若金汤的根本(上):对称加密与非对称加密

所有的加密算法都是公开的,任何人都可以去分析研究,而算法使用的“密钥”则必须保密。那么,这个关键的“密钥”又是什么呢?由于 HTTPS、TLS都运行在计算机上,所以“密钥”就是一长串的数字,但约定俗成的度量单位是“位”(bit),而不是“字节”(byte)。比如,说密钥长度是 128,就是 16字节的二进制串,密钥长度 1024,就是 128字节的二进制串。按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。

★,对称加密

对称加密只使用一个密钥,运算速度快,密钥必须保密。

TLS 里有非常多的对称加密算法可供选择,比如RC4、DES、3DES、AES、ChaCha20等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20。·AES· 的意思是“高级加密标准”(Advanced Encryption Standard),密钥长度可以是 128、192 或 256。它是 DES算法的替代者,安全强度很高,性能也很好,而且有的硬件还会做特殊优化,所以非常流行,是应用最广泛的对称加密算法。

加密分组模式:对称算法还有一个“分组模式”的概念,它可以让算法用固定长度的密钥加密任意长度的明文。最新的分组模式被称为AEAD(Authenticated Encryption with ssociated Data),在加密的同时增加了认证的功能,常用的是GCM、CCM 和 Poly1305。

★,非对称加密

对称加密看上去好像完美地实现了机密性,但其中有一个很大的问题:如何把密钥安全地传递给对方,术语叫“密钥交换”。因为在对称加密算法中只要持有密钥就可以解密。
如果你和网站约定的密钥在传递途中被黑客窃取,那他就可以在之后随意解密收发的数据,通信过程也就没有机密性可言了。

只用对称加密算法,是绝对无法解决密钥交换的问题的。所以,就出现了非对称加密(也叫公钥加密算法)。它有两个密钥,一个叫“公钥”(public key),一个叫“私钥
”(private key)。两个密钥是不同的,“不对称”,公钥可以公开给任何人使用,而私钥必须严格保密。公钥和私钥有个特别的“单向”性,虽然都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。非对称加密可以解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。

非对称加密算法的设计要比对称算法难得多,在 TLS里只有很少的几种,比如 DH、DSA、·RSA·、·ECC· 等。RSA可能是其中最著名的一个,几乎可以说是非对称加密的代名词。ECC(Elliptic Curve Cryptography)是非对称加密里的“后起之秀。比起 RSA,ECC 在安全强度和性能上都有明显的优势。160位的 ECC 相当于 1024 位的 RSA,而 224 位 ECC则相当于 2048 位的RSA。

ECC 虽然定义了公钥和私钥,但不能直接实现密钥交换和身份认证,需要搭配 DH、DSA等算法,形成专门的 ECDHE、ECDSA。RSA 比较特殊,本身既支持密钥交换也支持身份认证。

★,混合加密:

然非对称加密没有“密钥交换”的问题,但因为它们都是基于复杂的数学难题,运算速度很慢,即使是ECC 也要比 AES差上好几个数量级。如果仅用非对称加密,虽然保证了安全,但通信速度有如乌龟、蜗牛,实用性就变成了零。

TLS 里使用的混合加密方式,其实说穿了也很简单:在通信刚开始的时候使用非对称算法,比如RSA、ECDHE,首先解决密钥交换的问题。然后用随机数产生对称加密算法使用的“会话密钥”(session key),再用公钥加密。因为会话密钥很短,通常只有 16字节或 32 字节,所以慢一点也无所谓。对方拿到密文后用私钥解密,取出会话密钥。这样,
双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。

这样混合加密就解决了对称加密算法的密钥交换问题,而且安全和性能兼顾,完美地实现了机密性。不过这只是“万里长征的第一步”,后面还有完整性、身份认证、不可否认等特性没有实现,所以现在的通信还不是绝对安全。

★,

※,25 |固若金汤的根本(下):数字签名与证书

上一讲中我们学习了对称加密和非对称加密,以及两者结合起来的混合加密,实现了机密性。但仅有机密性,离安全还差的很远。黑客虽然拿不到会话密钥,无法破解密文,但可以通过窃听收集到足够多的密文,再尝试着修改、重组后发给网站。因为没有完整性保证,服务器只能“照单全收”,然后他就可以通过服务器的响应获取进一步的线索,最终就会破解出明文。另外,黑客也可以伪造身份发布公钥。如果你拿到了假的公钥,混合加密就完全失效了。你以为自己是在和“某宝”通信,实际上网线的另一端却是黑客,行卡号、密码等敏感信息就在“安全”的通信过程中被窃取了。所以,在机密性的基础上还必须加上完整性、身份认证等特性,才能实现真正的安全。

★,摘要算法

实现完整性的手段主要是摘要算法(Digest Algorithm),也就是常说的散列函数、哈希函数(Hash Function)。可以把摘要算法近似地理解成一种特殊的压缩算法,它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串。

摘要算法实际上是把数据从一个“大空间”映射到了“小空间”,所以就存在“冲突”(collision,也叫碰撞)的可能性,就如同现实中的指纹一样,可能会有两份不同的原文对应相同的摘要。好的摘要算法必须能够“抵抗冲突”,让这种可能性尽量地小。

MD5(Message-Digest5)、SHA-1(Secure Hash Algorithm1),它们就是最常用的两个摘要算法,能够生成 16 字节和20字节长度的数字摘要。但这两个算法的安全强度比较低,不够安全,在 TLS 里已经被禁止使用了。目前 TLS 推荐使用的是 SHA-1 的后继者:SHA-2。SHA-2 实际上是一系列摘要算法的统称,总共有 6种,常用的有 SHA224、SHA256、SHA384,分别能够生成 28字节、32 字节、48 字节的摘要。

摘要算法除了用于 TLS 安全通信,还有很多其他的用途,比如散列表、数据校验、大文件比较等。

★,完整性

摘要算法保证了“数字摘要”和原文是完全等价的。所以,我们只要在原文后附上它的摘要,就能够保证数据的完整性。比如,你发了条消息:“转账 1000 元”,然后再加上一个SHA-2的摘要。网站收到后也计算一下消息的摘要,把这两份“指纹”做个对比,如果一致,就说明消息是完整可信的,没有被修改。如果黑客在中间哪怕改动了一个标点符号,摘要也会完全不同,网站计算比对就会发现消息被窜改,是不可信的。不过摘要算法不具有机密性,如果明文传输,那么黑客可以修改消息后把摘要也一起改了,网站还是鉴别不出完整性。所以,真正的完整性必须要建立在机密性之上,在混合加密系统里用会话密钥加密消息和摘要,这样黑客无法得知明文,也就没有办法动手脚了。这有个术语,叫哈希消息认证码(HMAC)。

★,数字签名

加密算法结合摘要算法,我们的通信过程可以说是比较安全了。但这里还有漏洞,就是通信的两个端点(endpoint)。就像一开始所说的,黑客可以伪装成网站来窃取信息。而反过来,他也可以伪装成你,向网站发送支付、转账等消息,网站没有办法确认你的身份,钱可能就这么被偷走了。

非对称加密里的“私钥”只能由本人持有,能够在数字世界里证明你的身份。使用私钥再加上摘要算法,就能够实现“数字签名”,同时实现“身份认证”和“不可否认”。数字签名的原理其实很简单,就是把公钥私钥的用法反过来,之前是公钥加密、私钥解密,现在是私钥加密、公钥解密。但又因为非对称加密效率太低,所以私钥只加密原文的摘要,这样运算量就小的多,而且得到的数字签名也很小,方便保管和传输。签名和公钥一样完全公开,任何人都可以获取。但这个签名只有用私钥对应的公钥才能解开,拿到摘后,再比对原文验证完整性,就可以像签署文件一样证明消息确实是你发的。

刚才的这两个行为也有专用术语,叫做“签名”和“验签”。只要你和网站互相交换公钥,就可以用“签名”和“验签”来确认消息的真实性,因为私钥保密,黑客不能伪造签,就能够保证通信双方的身份。比如,你用自己的私钥签名一个消息“我是小明”。网站收到后用你的公钥验签,确认身份没问题,于是也用它的私钥签名消息“我是某宝”。你收到后再用它的公钥验一下,也没问题,这样你和网站就都知道对方不是假冒的,后面就可以用混合加密进行安全通信了。

★,数字证书和 CA

综合使用对称加密、非对称加密和摘要算法,我们已经实现了安全的四大特性,是不是已经完美了呢?不是的,这里还有一个“公钥的信任”问题。因为谁都可以发布公钥,我们还缺少防止黑客伪造公钥的手段,也就是说,怎么来判断这个公钥就是你或者某宝的公钥呢?

我们可以用类似密钥交换的方法来解决公钥认证问题,用别的私钥来给公钥签名,显然,这又会陷入“无穷递归”。但这次实在是“没招”了,要终结这个“死循环”,就必须入“外力”,找一个公认的可信第三方,让它作为“信任的起点,递归的终点”,构建起公钥的信任链。这个“第三方”就是我们常说的CA(Certificate Authority,证书认证机构)。它就像网络世界里的公安局、教育部、公证中心,具有极高的可信度,由它来给各个公钥签名,用自身的信誉来保证公钥无法伪造,是可信的。

有了证书体系,操作系统和浏览器都内置了各大 CA的根证书,上网的时候只要服务器发过来它的证书,就可以验证证书里的签名,顺着证书链(CertificateChain)一层层地验证,直到找到根证书,就能够确定证书是可信的,从而里面的公钥也是可信的。

证书的格式遵循X509 v3 标准,有两种编码方式,一种是二进制的 DER,另一种是 ASCII 码的 PEM。

★,

★,

★,

※,26 | 信任始于握手:TLS1.2连接过程解析

HTTPS 协议会先与服务器执行 TCP 握手,然后执行 TLS握手,才能建立安全连接;TLS握手的目标是安全地交换对称密钥,需要三个随机数,第三个随机数“Pre-Master”必须加密传输,绝对不能让黑客破解;“Hello”消息交换随机数,“Key Exchange”消息交换“Pre-Master”;“Change Cipher Spec”之前传输的都是明文,之后都是对称密钥加密的密文。

★,TLS的握手过程

★,双向认证

“单向认证”握手过程,只认证了服务器的身份,而没有认证客户端的身份。这是因为通常单向认证通过后已经建立了安全通信,用账号、密码等简单的手段就能够确认用户的真实身份。但为了防止账号、密码被盗,有的时候(比如网上银行)还会使用 U 盾给用户颁发客户端证书,实现“双向认证”,这样会更加安全。双向认证的流程也没有太多变化,只是在“Server HelloDone”之后,“Client Key Exchange”之前,客户端要发送“Client Certificate”消息,服务器收到后也把证书链走一遍,验证客户端的身份。

★,

★,

※,27 | 更好更快的握手:TLS1.3特性解析

★,

※,28 | 连接太慢该怎么办:HTTPS的优化

★,

※,29 | 我应该迁移到HTTPS吗

“迁移到HTTPS”已经不是“要不要做”的问题,而是“要怎么做”的问题了.

★,如何迁移至HTTPS

  • 申请证书:
    • 要把网站从 HTTP 切换到HTTPS,首先要做的就是为网站申请一张证书。大型网站出于信誉、公司形象的考虑,通常会选择向传统的CA 申请证书,例如DigiCert、GlobalSign,而中小型网站完全可以选择使用“Let’s Encrypt”这样的免费证书,效果也完全不输于那些收费的证书。“Let’s Encrypt”一直在推动证书的自动化部署,为此还实现了专门的 ACME协议(RFC8555)。有很多的客户端软件可以完成申请、验证、下载、更新的“一条龙”操作,比如 Certbot、acme.sh等等,都可以在“Let’s Encrypt”网站上找到,用法很简单,相关的文档也很详细,几分钟就能完成申请。
    • =============注意事项==============
    • 第一,申请证书时应当同时申请 RSA 和 ECDSA两种证书,在 Nginx里配置成双证书验证,这样服务器可以自动选择快速的椭圆曲线证书,同时也兼容只支持 RSA 的客户端。
    • 第二,如果申请 RSA 证书,私钥至少要 2048位,摘要算法应该选用 SHA-2,例如 SHA256、SHA384 等。
    • 第三,出于安全的考虑,“Let’s Encrypt”证书的有效期很短,只有 90天,时间一到就会过期失效,所以必须要定期更新。你可以在crontab 里加个每周或每月任务,发送更新请求,不过很多ACME 客户端会自动添加这样的定期任务,完全不用你操心。
  • 配置 HTTPS
    • 搞定了证书,接下来就是配置 Web 服务器,在 443端口上开启 HTTPS 服务了。这在 Nginx上非常简单,只要在“listen”指令后面加上参数“ssl”,再配上刚才的证书文件就可以实现最基本的 HTTPS。
      listen            443 ssl;
       
      ssl_certificate      xxx_rsa.crt;  #rsa2048 cert
      ssl_certificate_key   xxx_rsa.key;  #rsa2048 private key
      
      ssl_certificate         xxx_ecc.crt;  #ecdsa cert
      ssl_certificate_key   xxx_ecc.key;  #ecdsa private key
      
      # 为了提高 HTTPS 的安全系数和性能,你还可以强制 Nginx只支持 TLS1.2 以上的协议,打开“Session Ticket”会话复用:
      ssl_protocols              TLSv1.2 TLSv1.3;
       
      ssl_session_timeout         5m;
      ssl_session_tickets         on;
      ssl_session_ticket_key      ticket.key;
    •  

★,服务器名称指示

配置 HTTPS 服务时还有一个“虚拟主机”的问题需要解决。在 HTTP 协议里,多个域名可以同时在一个 IP地址上运行,这就是“虚拟主机”,Web服务器会使用请求头里的 Host 字段(参见第 9 讲)来选择。但在 HTTPS 里,因为请求头只有在 TLS握手之后才能发送,而在握手时就必须选择“虚拟主机”对应的证书,TLS 无法得知域名的信息,就只能用 IP地址来区分。所以,最早的时候每个 HTTPS域名必须使用独立的 IP 地址,非常不方便。

那么怎么解决这个问题呢?
这还是得用到 TLS 的“扩展”,给协议加个SNI(Server Name Indication)的“补充条款”。它的作用和 Host字段差不多,客户端会在“ClientHello”时带上域名信息,这样 服务器就可以根据名字而不是 IP地址来选择证书。

Extension: server_name (len=19)
    Server Name Indication extension
        Server Name Type: host_name (0)
        Server Name: www.chrono.com

Nginx 很早就基于 SNI 特性支持了 HTTPS 的虚拟主机(proxy_pass配置中·proxy_ssl_server_name·和·proxy_ssl_name·指令),但在OpenResty 里可还以编写 Lua 脚本,利用 Redis、MySQL等数据库更灵活快速地加载证书。

★,重定向跳转: 将HTTP请求跳转到HTTPS

return 301 https://$host$request_uri; # 永久重定向
rewrite ^  https://$host$request_uri permanent;  

重定向有两个问题。一个是重定向增加了网络成本,多出了一次请求;另一个是存在安全隐患,重定向的响应可能会被“中间人”窜改,实现“会话劫持”,跳转到恶意网站。有一种“HSTS”(HTTP 严格传输安全,HTTP Strict Transport Security)的技术可以消除这种安全隐患。

★,

※,30 | 时代之风(上):HTTP/2特性概览

HTTP有两个主要的缺点:安全不足和性能不高。通过引入 SSL/TLS在安全上达到了“极致”,但在性能提升方面却是乏善可陈,只优化了握手加密的环节,对于整体的数据传输没有提出更好的改进方案,还只能依赖于“长连接”这种“落后”的技术。在 HTTPS 逐渐成熟之后,HTTP就向着性能方面开始“发力”,走出了另一条进化的道路。google
率先发明了 SPDY 协议,并应用于自家的浏览器Chrome,打响了 HTTP 性能优化的“第一枪”。随后互联网标准化组织 IETF 以 SPDY为基础,综合其他多方的意见,终于推出了 HTTP/1的继任者,也就是今天的主角“HTTP/2”,在性能方面有了一个大的飞跃。

由于 HTTPS 已经在安全方面做的非常好了,所以 HTTP/2的唯一目标就是改进性能。

★,兼容 HTTP/1

★,头部压缩

★,二进制格式

HTTP/2把原来的“Header+Body”的消息“打散”为数个小片的二进制“帧”(Frame),用“HEADERS”帧存放头数据、“DATA”帧存放实体数据。这种做法有点像是“Chunked”分块编码的方式(参见第 16 讲),也是“化整为零”的思路,但 HTTP/2数据分帧后“Header+Body”的报文结构就完全消失了,协议看到的只是一个个的“碎片”。下图中的HEADERS Frame可以有多个。

★,虚拟的“流”

消息的“碎片”到达目的地后应该怎么组装起来呢?
HTTP/2 为此定义了一个“流”(Stream)的概念,它二进制帧的双向传输序列同一个消息往返的帧会分配一个唯一的流
ID。你可以想象把它成是一个虚拟的“数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是 HTTP/1里的请求报文和响应报文。
因为“流”是虚拟的,实际上并不存在,所以 HTTP/2就可以在一个 TCP 连接上用“流”同时发送多个“碎片化”的消息,这就是常说的“多路复用”(Multiplexing)——多个往返通信都复用一个连接来处理(注:HTTP/1.1中的长连接也可以将多个往返通信使用一个连接来处理,区别在于HTTP/1.1中的多个通信是串行的,HTTP/2中的多个往返通信是并行的在“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。多个请求 /响应之间没有了顺序关系,不需要排队等待,也就不会再出现“队头阻塞”问题,降低了延迟,大幅度提高了连接的利用率。

为了更好地利用连接,加大吞吐量,HTTP/2还添加了一些控制帧来管理虚拟的“流”,实现了优先级和流量控制,这些特性也和 TCP协议非常相似。
HTTP/2 还在一定程度上改变了传统的“请求 -应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的JS、CSS 文件发给客户端,减少等待的延迟,这被称为“服务器推送”(Server Push,也叫 Cache Push)。

★,强化安全

出于兼容的考虑,HTTP/2 延续了 HTTP/1的“明文”特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。但由于 HTTPS 已经是大势所趋,而且主流的浏览器Chrome、Firefox 等都公开宣布只支持加密的HTTP/2,所以“事实上”的 HTTP/2是加密的。也就是说,互联网上通常所能见到的 HTTP/2都是使用“https”协议名,跑在 TLS 上面。为了区分“加密”和“明文”这两个不同的版本,HTTP/2协议定义了两个字符串标识符:“·h2·”表示加密的 HTTP/2,“·h2c·”表示明文的 HTTP/2,多出的那个字母“c”的意思是“clear text”。

★,协议栈:HTTP/2是建立在“HPack”“Stream”“TLS1.2”基础之上的,比HTTP/1、HTTPS 复杂了一些。

★,

※,31 | 时代之风(下):HTTP/2内核剖析

★,连接前言(connection preface)

由于 HTTP/2“事实上”是基于TLS,所以在正式收发数据之前,会有 TCP 握手和 TLS握手,TLS 握手成功之后,客户端必须要发送一个“连接前言”(connection  preface),用来确认建立 HTTP/2 连接。这个“连接前言”是标准的 HTTP/1 请求报文,使用纯文本的ASCII码格式,请求方法是特别注册的一个关键字“PRI”,全文只有
24 个字节:`PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`在 Wireshark 里,HTTP/2 的“连接前言”被称为“Magic”,意思就是“不可知的魔法”。所以,就不要问“为什么会是这样”了,只要服务器收到这个“有魔力的字符串”,就知道客户端在 TLS 上想要的是 HTTP/2协议,而不是其他别的协议,后面就会都使用 HTTP/2的数据格式。

★,头部压缩

★,二进制帧

★,流与多路复用

流与多路复用是HTTP/2 最核心的部分。

再重复一遍:流是二进制帧的双向传输序列。
要搞明白流,关键是要理解帧头里的流 ID。在 HTTP/2连接上,虽然帧是乱序收发的,但只要它们都拥有相同的流ID,就都属于一个流,而且在这个流里帧不是无序的,而是有着严格的先后顺序。

比如在这次的 Wireshark抓包里,就有“0、1、3”一共三个流,实际上就是分配了三个流ID 号,把这些帧按编号分组,再排一下队,就成了流

在概念上,一个 HTTP/2 的流就等同于一个 HTTP/1里的“请求 - 应答”。在 HTTP/1 里一个“请求 -响应”报文来回是一次 HTTP 通信,在 HTTP/2里一个流也承载了相同的功能。
你还可以对照着 TCP 来理解。TCP 运行在 IP 之上,其实从MAC 层、IP 层的角度来看,TCP的“连接”概念也是“虚拟”的。但从功能上看,无论是 HTTP/2的流,还是 TCP
的连接,都是实际存在的,所以你以后大可不必再纠结于流的“虚拟”性,把它当做是一个真实存在的实体来理解就好。
HTTP/2 的流有哪些特点呢?我给你简单列了一下:

  • 流是可并发的,一个 HTTP/2连接上可以同时发出多个流传输数据,也就是并发多请求,实现“多路复用”;
  • 客户端和服务器都可以创建流,双方互不干扰;
  • 流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个“请求 - 应答”来回;
  • 流之间没有固定关系,彼此独立,但流内部的帧是有严格顺序的;
  • 流可以设置优先级,让服务器优先处理,比如先传HTML/CSS,后传图片,优化用户体验;
  • 流 ID 不能重用,只能顺序递增,客户端发起的 ID是奇数,服务器端发起的 ID 是偶数;
    • 【注意“发起”的含义:客户端发起的流ID是奇数,针对此次请求服务端响应的流ID也是奇数】
  • 在流上发送“RST_STREAM”帧可以随时终止流,取消接收或发送;
  • 第 0号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制。

下图显示了连接中无序的帧是如何依据流 ID 重组成流的。

从这些特性中,我们还可以推理出一些深层次的知识点。
比如说,HTTP/2在一个连接上使用多个流收发数据,那么它本身默认就会是长连接,所以永远不需要“Connection”头字段(keepalive 或 close)。

又比如,下载大文件的时候想取消接收,在 HTTP/1里只能断开 TCP 连接重新“三次握手”,成本很高,而在HTTP/2里就可以简单地发送一个“RST_STREAM”中断流,
而长连接会继续保持。

再比如,因为客户端和服务器两端都可以创建流,而流 ID有奇数偶数和上限的区分,所以大多数的流 ID都会是奇数(客户端发起请求占了绝大部分),而且客户端在一个连接里最多只能发出2^30,也就是 10 亿个请求。所以就要问了:ID用完了该怎么办呢?这个时候可以再发一个控制帧“GOAWAY”,真正关闭 TCP 连接。

★,流状态转换

★,

※,32 | 未来之路:HTTP/3展望

★,QUIC 选定了 UDP,在它之上把 TCP的那一套连接管理、拥塞窗口、流量控制等“搬”了过来,“去其糟粕,取其精华”,打造出了一个全新的可靠传输协议,可以认为是“新时代的 TCP”。

★,

※,33 | 我应该迁移到HTTP/2吗?

★,主要看流量情况。tmall.com,qq.com等已经使用了HTTP/2。

※,34 | Nginx:高性能的Web服务器

本讲结合 HTTP 协议来讲Nginx,带你窥视一下 HTTP 处理的内幕,看看 Web服务器的工作原理。

★,进程池

nginx 是个“轻量级”的 Web 服务器,那么这个所谓的“轻量级”是什么意思呢?“轻量级”是相对于“重量级”而言的。“重量级”就是指服务器进程很“重”,占用很多资源,当处理 HTTP请求时会消耗大量的 CPU和内存,受到这些资源的限制很难提高性能。而 Nginx 作为“轻量级”的服务器,它的CPU、内存占用都非常少,同样的资源配置下就能够为更多的用户提供服务,其奥秘在于它独特的工作模式.

在 Nginx 之前,Web服务器的工作模式大多是“Per-Process”或者“Per-Thread”,对每一个请求使用单独的进程或者线程处理。这就存在创建进程或线程的成本,还会有进程、线程“上下文切换”的额外开销。如果请求数量很多,CPU就会在多个进程、线程之间切换时“疲于奔命”,平白地浪费了计算时间。Nginx则完全不同,“一反惯例”地没有使用多线程,而是使用了“`进程池 + 单线程`”的工作模式。【总结:Nginx采用“master/workers”进程池架构,不使用多线程,消除了进程、线程切换的成本】

Nginx 在启动的时候会预先创建好固定数量的 worker进程,在之后的运行过程中不会再 fork出新进程,这就是进程池,而且可以自动把进程“绑定”到独立的 CPU上,这样就完全消除了进程创建和切换的成本,能够充分利用多核 CPU 的计算能力。在进程池之上,还有一个“master”进程,专门用来管理进程池。它的作用有点像是 supervisor(一个用 Python编写的进程管理工具),用来监控进程,自动恢复发生异常的worker,保持进程池的稳定和服务能力。不过 master 进程完全是 Nginx 自行用 C语言实现的,这就摆脱了外部的依赖,简化了 Nginx的部署和配置。

Nginx 自1.7.11 开始引入了“多线程”,但只是作为辅助手段,卸载阻塞的磁盘I/O 操作,主要的HTTP请求处理使用的还是单线程里的epoll。

★,I/O 多路复用

使用多线程能够很容易实现并发处理。但多线程也有一些缺点,除了刚才说到的“上下文切换”成本,还有编程模型复杂、数据竞争、同步等问题,写出正确、快速的多线程程序并不是一件容易的事情。所以 Nginx就选择了单线程的方式,带来的好处就是开发简单,没有互斥锁的成本,减少系统消耗。那么,疑问也就产生了:为什么单线程的
Nginx,处理能力却能够超越其他多线程的服务器呢?

这要归功于 Nginx 利用了 Linux 内核里的一件“神兵利器”,I/O 多路复用接口,“大名鼎鼎”的 epoll。Web 服务器从根本上来说是“I/O 密集型”而不是“CPU密集型”,处理能力的关键在于网络收发而不是 CPU计算(这里暂时不考虑 HTTPS 的加解密),而网络 I/O会因为各式各样的原因不得不等待,比如数据还没到达、对端没有响应、缓冲区满发不出去等等。这种情形就有点像是 HTTP里的“队头阻塞”。对于一般的单线程来说 CPU就会“停下来”,造成浪费。多线程的解决思路有点类似“并发连接”,虽然有的线程可能阻塞,但由于多个线程并行,总体上看阻塞的情况就不会太严重了。Nginx 里使用的 epoll,就好像是 HTTP/2里的“多路复用”技术,它把多个 HTTP请求处理打散成碎片,都“复用”到一个单线程里,不按照先来后到的顺序处理,而是只当连接上真正可读、可写的时候才处理,如果可能发生阻塞就立刻切换出去,处理其他的请求。通过这种方式,Nginx 就完全消除了 I/O 阻塞,把 CPU利用得“满满当当”,又因为网络收发并不会消耗太多 CPU计算能力,也不需要切换进程、线程,所以整体的 CPU负载是相当低的。这里我画了一张 Nginx“I/O多路复用”的示意图,你可以看到,它的形式与 HTTP/2的流非常相似,每个请求处理单独来看是分散、阻塞的,但因为都复用到了一个线程里,所以资源的利用率非常高。

epoll还有一个特点,大量的连接管理工作都是在操作系统内核里做的,这就减轻了应用程序的负担,所以 Nginx可以为每个连接只分配很小的内存维护状态,即使有几万、
几十万的并发连接也只会消耗几百 M 内存,而其他的 Web服务器这个时候早就“Memory not enough”了。

★,多阶段处理

有了“进程池”和“I/O 多路复用”,Nginx 是如何处理 HTTP请求的呢?

Nginx 在内部也采用的是“化整为零”的思路,把整个 Web服务器分解成了多个“功能模块”,就好像是乐高积木,可以在配置文件里任意拼接搭建,从而实现了高度的灵活性和扩展性。Nginx 的 HTTP 处理有四大类模块:

  • handler 模块:直接处理 HTTP 请求;
  • filter 模块:不直接处理请求,而是加工过滤响应报文;
  • upstream模块:实现反向代理功能,转发请求到其他服务器;
  • balance 模块:实现反向代理时的负载均衡算法。

因为 upstream 模块和 balance 模块实现的是代理功能,Nginx作为“中间人”,运行机制比较复杂,所以我今天只讲 handler模块和 filter 模块。

nginx 里的 handler 模块和 filter模块是按照“职责链”模式设计和组织的,HTTP请求报文就是“原材料”,各种模块就是工厂里的工人,走完模块构成的“流水线”,出来的就是处理完成的响应报文。下面的这张图显示了 Nginx 的“流水线”,在 Nginx里的术语叫“阶段式处理”(Phases),一共有 11个阶段,每个阶段里又有许多各司其职的模块。

注:

  • 此“流水线”图没有画出 flter 模块所在的位置,它其实是在 CONTENT 阶段的末尾专门“过”响应数据。
  • Nginx的“PRECONTENT”阶段在1.13.3 之前叫“TRY_FILES”,仅供Nginx 内部使用,用户不可介入。

简单列举几个模块:

  • charset 模块实现了字符集编码转换;(第 15 讲)
  • chunked 模块实现了响应数据的分块传输;(第 16 讲)
  • range 模块实现了范围请求,只返回数据的一部分;(第16 讲)
  • rewrite模块实现了重定向和跳转,还可以使用内置变量自定义跳转的 URI;(第 18 讲)
  • not_modified模块检查头字段“if-Modified-Since”和“If-None-Match”,处理条件请求;(第 20 讲)
  • realip模块处理“X-Real-IP”“X-Forwarded-For”等字段,获取客户端的真实 IP 地址;(第 21 讲)
  • ssl 模块实现了 SSL/TLS协议支持,读取磁盘上的证书和私钥,实现 TLS 握手和SNI、ALPN 等扩展功能;(安全篇)
  • http_v2 模块实现了完整的 HTTP/2 协议。(飞翔篇)

在这张图里,你还可以看到 limit_conn、limit_req、access、log等其他模块,它们实现的是限流限速、访问控制、日志等功能,不在 HTTP 协议规定之内,但对于运行在现实世界的 Web服务器却是必备的。

★,tips

如何让 Web 服务器能够高效地处理 10K 以上的并发请求(Concurrent 10K),这就是著名的“C10K 问题”,当然它早已经被 epoll/kqueue 等解决了,现在的新问题是“C10M”

※,35 | OpenResty:更灵活的Web服务器

★,OpenResty 是什么

nginx依赖于磁盘上的静态配置文件,修改后必须重启才能生效,缺乏灵活性;特别是对于拥有成千上万台服务器的网站来说,仅仅增加或者删除一行配置就要分发、重启所有的机器,对运维是一个非常大的挑战,要耗费很多的时间和精力,成本很高,很不灵活,难以“随需应变”。

那么,有没有这样的一个 Web 服务器,它有 Nginx的优点却没有 Nginx的缺点,既轻量级、高性能,又灵活、可动态配置呢?这就是我今天要说的 OpenResty,它是一个“更好更灵活的Nginx”。OpenResty 基于Nginx,打包了很多有用的模块和库,是一个高性能的 Web开发平台;

OpenResty 的核心是 Nginx,但它又超越了Nginx,关键就在于其中的 ngx_lua 模块,把小巧灵活的 Lua语言嵌入了 Nginx,可以用脚本的方式操作 Nginx内部的进程、多路复用、阶段式处理等各种构件。OpenResty 还把 Lua 自身的协程与 Nginx的事件机制完美结合在一起,优雅地实现了许多其他语言所没有的“同步非阻塞”编程范式,能够轻松开发出高性能的 Web 应用。目前 OpenResty有两个分支,分别是开源、免费的“OpenResty”和闭源、商业产品的“OpenResty+"。

★,动态的 Lua

★,高效率的 Lua

OpenResty 能够高效运行的一大“秘技”是它的“同步非阻塞”编程范式,如果你要开发 OpenResty应用就必须时刻铭记于心。
“同步非阻塞”本质上还是一种“多路复用”,我拿上一讲的Nginx epoll 来对比解释一下。
epoll 是操作系统级别的“多路复用”,运行在内核空间。而OpenResty 的“同步非阻塞”则是基于 Lua 内建的“协程”,是应用程序级别的“多路复用”,运行在用户空间,
所以它的资源消耗要更少。
OpenResty 里每一段 Lua 程序都由协程来调度运行。和 Linux的 epoll一样,每当可能发生阻塞的时候“协程”就会立刻切换出去,执行其他的程序。这样单个处理流程是“阻塞”的,但整个OpenResty 却是“非阻塞的”,多个程序都“复用”在一个 Lua虚拟机里运行

下面的代码是一个简单的例子,读取 POST 发送的 body数据,然后再发回客户端:

ngx.req.read_body()                  -- 同步非阻塞 (1)
local data = ngx.req.get_body_data()
if data then
    ngx.print("body: ", data)        -- 同步非阻塞 (2)
end

代码中的“ngx.req.read_body”和“ngx.print”分别是数据的收发动作,只有收到数据才能发送数据,所以是“同步”的。但即使因为网络原因没收到或者发不出去,OpenResty也不会在这里阻塞“干等着”,而是做个“记号”,把等待的这段CPU时间用来处理其他的请求,等网络可读或者可写时再“回来”接着运行。

【扩展:同步、异步 、阻塞、非阻塞】

参考此文

同步与异步关注的是消息通信机制(synchronous communication/ asynchronous communication)。

  • 同步异步是对于被调用方而言。
  • 同步:调用方发起调用后,被调用方必须计算出真实结果之后才返回。
  • 异步:调用方发起调用后,被调用方立即返回给调用方一个结果,真实的结果慢慢计算,算出真实结果后通过“状态”,“通知”,“回调”这三种方式告诉调用方。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

  • 阻塞和非阻塞是对于调用方而言。
  • 阻塞:调用法发起调用后,不管被调用方是同步返回结果还是异步返回结果,调用方都一直挂起等待被调用方返回真实的结果。
    • 异步阻塞:尽管被调用方立即返回给调用方一个结果,但是调用方还是一直等待被调用方返回真实的结果。这种情况很少使用
  • 非阻塞:调用法发起调用后,不管被调用方是同步返回结果还是异步返回结果,调用方都不会将自己挂起等待,而是去做其他的事情。
    • 同步非阻塞:调用法发起调用后,被调用方采取同步方式,在被调用方计算真实结果的过程中,被调用方可以去做其他事情,但是会周期性的向被调用方发起轮询查询真实结果是否已经计算出来。即:同步非阻塞时调用方获取真实结果的方式一般是采用轮询。

★,

※,36 | WAF:保护我们的网络服务

★,Web 服务遇到的威胁

  • “DDoS”攻击(distributed denial-of-service attack),有时候也叫“洪水攻击”。
    • “CC 攻击”(Challenge Collapsar) 是“DDoS”的一种,它使用代理服务器发动攻击
  • SQL 注入 (SQL inject)
  • HTTP 头注入:“User-Agent”“X-Forwarded-For”等字段里加入了恶意数据或代码,服务端程序如果解析不当,就会执行预设的恶意代码
  • 跨站脚本”(XSS)攻击:利用 Cookie的攻击手段,属于“JS 代码注入”,利用 JavaScript 脚本获取未设防的 Cookie。

★,网络应用防火墙

传统的“防火墙”工作在三层或者四层,隔离了外网和内网,使用预设的规则,只允许某些特定 IP地址和端口号的数据包通过,拒绝不符合条件的数据流入或流出内网,实质上是一种网络数据过滤设备。

网络应用防火墙 (Web Application Firewall)了,简称为“WAF",也是一种“防火墙”,但它工作在七层,看到的不仅是 IP地址和端口号,还能看到整个 HTTP报文,所以就能够对报文内容做更深入细致的审核,使用更复杂的条件、规则来过滤数据。说白了,WAF 就是一种“HTTP 入侵检测和防御系统”。

通常一款产品能够称为 WAF,要具备下面的一些功能:

  • IP黑名单和白名单,拒绝黑名单上地址的访问,或者只允许白名单上的用户访问;
  • URI 黑名单和白名单,与 IP黑白名单类似,允许或禁止对某些 URI 的访问;
  • 防护 DDoS 攻击,对特定的 IP 地址限连限速;
  • 过滤请求报文,防御“代码注入”攻击;
  • 过滤响应报文,防御敏感信息外泄;
  • 审计日志,记录所有检测到的入侵操作。

好像很高深,但如果你理解了它的工作原理,其实也不难。如果你比较熟悉Apache、Nginx、OpenResty,可以自己改改配置文件,写点 JS或者 Lua 代码,就能够实现基本的 WAF 功能。
比如说,在 Nginx 里实现 IP地址黑名单,可以利用“map”指令,从变量 $remote_addr 获取IP 地址,在黑名单上就映射为值 1,然后在“if”指令里判断:

map $remote_addr $blocked {
    default       0;
    "1.2.3.4"     1;
    "5.6.7.8"     1;
}
 
 
if ($blocked) {
    return 403 "you are blocked.";  
}

Nginx的配置文件只能静态加载,改名单必须重启,比较麻烦。如果换成 OpenResty 就会非常方便,在 access阶段进行判断,IP 地址列表可以使用 cosocket 连接外部的Redis、MySQL 等数据库,实现动态更新:

local ip_addr = ngx.var.remote_addr
 
local rds = redis:new()
if rds:get(ip_addr) == 1 then 
    ngx.exit(403) 
end

虽然自己实现难度不是很大,但是网络安全领域必须时刻记得“木桶效应”(也叫“短板效应”),使用 WAF 最好“不要重新发明轮子”,而是使用现有的、比较成熟的、经过实际考验的 WAF产品。

★,全面的 WAF 解决方案

这里要“隆重”介绍一下 WAF 领域里的最顶级产品了:ModSecurity,它可以说是 WAF 界“事实上的标准”。

ModSecurity可以以模块的形式集成到NGINX中。

在Nginx 上还可以使用另一个 WAF 模块Naxsi,虽然它的功能也很强大,但与ModSecurity 并不兼容。

OpenResty 生态系统里也已经有了多个比较成熟的纯 Lua WAF,可以在 GitHub 上搜索关键词“ngx lua waf”,而商业版的 OpenResty+则基于ModSecurity 的核心规则集,使用 Lua重新实现了一套自有引擎

★,

※,37 | CDN:加速我们的网络服务

协议方面,HTTPS 强化通信链路安全、HTTP/2优化传输效率;应用方面,Nginx/OpenResty提升网站服务能力,WAF抵御网站入侵攻击,在应用领域,还缺一个在外部加速 HTTP协议的服务:CDN(Content Delivery Network 或 Content Distribution Network),中文名叫“内容分发网络”。它是专门为解决“长距离”上网络访问速度慢而诞生的一种网络应用服务。

由于客观地理距离的存在,直连网站访问速度会很慢,所以就出现了 CDN;CDN构建了全国、全球级别的专网,让用户就近访问专网里的边缘节点,降低了传输延迟,实现了网站加速;

CDN 的最核心原则是“就近访问”,如果用户能够在本地几十公里的距离之内获取到数据,那么时延就基本上变成 0 了。

★,CDN 的负载均衡

CDN有两个关键组成部分:全局负载均衡和缓存系统,对应的是 DNS(第 6 讲)和缓存代理(第21 讲、第 22 讲)技术。

  • CDN 里除了核心的负载均和缓存系统,还有其他的辅助系统,比如管理、监控、日志、统计、计费等。

全局负载均衡(Global Sever Load Balance)一般简称为GSLB,它是 CDN的“大脑”,主要的职责是当用户接入网络的时候在 CDN专网中挑选出一个“最佳”节点提供服务,解决的是用户如何找到“最近的”边缘节点,对整个 CDN网络进行“负载均衡”。

GSLB 最常见的实现方式是“DNS 负载均衡”,这个在第 6讲里也说过,不过 GSLB 的方式要略微复杂一些。原来没有 CDN 的时候,权威 DNS返回的是网站自己服务器的实际 IP 地址,浏览器收到 DNS解析结果后直连网站。但加入 CDN 后就不一样了,权威 DNS 返回的不是 IP地址,而是一个 CNAME( Canonical Name )别名记录,指向的就是 CDN 的 GSLB。它有点像是 HTTP/2里“Alt-Svc”的意思,告诉外面:“我这里暂时没法给你真正的地址,你去另外一个地方再查查看吧。”因为没拿到 IP 地址,于是本地 DNS 就会向 GSLB再发起请求,这样就进入了 CDN的全局负载均衡系统,开始“智能调度”,主要的依据有这么几个:

看用户的 IP地址,查表得知地理位置,找相对最近的边缘节点;
看用户所在的运营商网络,找相同网络的边缘节点;
检查边缘节点的负载情况,找负载较轻的节点;
其他,比如节点的“健康状况”、服务能力、带宽、响应时间等。GSLB把这些因素综合起来,用一个复杂的算法,最后找出一台“最合适”的边缘节点,把这个节点的 IP地址返回给用户,用户就可以“就近”访问 CDN的缓存代理了。

CDN 的缓存代理:缓存系统是 CDN 的另一个关键组成部分,相当于 CDN的“心脏”。如果缓存系统的服务能力不够,不能很好地满足用户的需求,那 GSLB调度算法再优秀也没有用。

目前国内的CDN厂商内部都是基于开源软件定制的。最常用的是专门的缓存代理软件 Squid、Varnish,还有新兴的ATS(Apache Traffic Server),而 Nginx 和 OpenResty 作为Web服务器领域的“多面手”,凭借着强大的反向代理能力和模块化、易于扩展的优点,也在 CDN 里占据了不少的份额。

★,小结

CDN 发展到现在已经有二十来年的历史了,早期的 CDN功能比较简单,只能加速静态资源。随着这些年 Web2.0、HTTPS、视频、直播等新技术、新业务的崛起,它也在不断进步,增加了很多的新功能,比如 SSL加速、内容优化(数据压缩、图片格式转换、视频转码)、资源防盗链、WAF 安全防护等等。现在,再说 CDN是“搬运工”已经不太准确了,它更像是一个“无微不至”的“网站保姆”,让网站只安心生产优质的内容,其他的“杂事”都由它去代劳。

目前应用最广泛的 DNS 软件是开源的 BIND9(Berkeley Internet Name Domain),而OpenResty 则使用stream_lua 实现了纯 Lua的 DNS 服务。

CDN大厂 CloudFlare 的系统就都是由Nginx/OpenResty 驱动的,而 OpenResty 公司的主要商业产品“OpenResty Edge”也是CDN。

当前的 CDN 也有了“云化”的趋势,很多云厂商都把 CDN 作为一项“标配”服务。

★,

※,38 | WebSocket:沙盒里的TCP

★,websocket

  • webSocket”是一种基于 TCP的轻量级网络通信协议,在地位上是与 HTTP“平级”的。
  • WebSocket 与 HTTP/2 一样,都是为了解决 HTTP某方面的缺陷而诞生的。HTTP/2 针对的是“队头阻塞”,而WebSocket 针对的是“请求 - 应答”通信模式。
  • WebSocket 是一个“全双工”的通信协议,相当于对 TCP做了一层“薄薄的包装”,让它运行在浏览器环境里;WebSocket 使用兼容 HTTP 的 URI来发现服务,但定义了新的协议名“ws”和“wss”,端口号也沿用了 80 和 443;
  • 浏览器是一个“沙盒”环境,有很多的限制,不允许建立 TCP连接收发数据,而有了WebSocket,我们就可以在浏览器里与服务器直接建立“TCP连接”,获得更多的自由。不过自由也是有代价的,WebSocket虽然是在应用层,但使用方式却与“TCPSocket”差不多,过于“原始”,用户必须自己管理连接、缓存、状态,开发上比 HTTP 复杂的多,所以是否要在项目中引入
    WebSocket 必须慎重考虑

★,websocket的握手

websocket利用了 HTTP身的“协议升级”特性,“伪装”成HTTP,这样就能绕过浏览器沙盒、网络防火墙等等限制,

WebSocket 的握手是一个标准的 HTTP GET请求,但要带上两个协议升级的专用头字段:“Connection: Upgrade”,表示要求协议“升级”;“Upgrade: websocket”,表示要“升级”成 WebSocket 协议。另外,为了防止普通的 HTTP 消息被“意外”识别成WebSocket,握手消息还增加了两个额外的认证用头字段(所谓的“挑战”,Challenge):Sec-WebSocket-Key:一个 Base64 编码的 16字节随机数,作为简单的认证密钥;Sec-WebSocket-Version:协议的版本号,当前必须是 13。

服务器收到 HTTP请求报文,看到上面的四个字段,就知道这不是一个普通的GET 请求,而是 WebSocket 的升级请求,于是就不走普通的HTTP 处理流程,而是构造一个特殊的“101 Switching
Protocols”响应报文,通知客户端,接下来就不用 HTTP了,全改用 WebSocket 协议通信。

WebSocket的握手响应报文也是有特殊格式的,要用字段“Sec-WebSocket-Accept”验证客户端请求报文,同样也是为了防止误连接.

※,39 | HTTP性能优化面面观(上)

性能优化是一个复杂的概念,在 HTTP里可以分解为服务器性能优化、客户端性能优化和传输链路优化

★,HTTP服务器性能

衡量服务器性能的主要指标有三个:吞吐量(requests per second)、并发数(concurrency)和响应时间(time per request)。

  • 吞吐量就是我们常说的 RPS,每秒的请求次数,也有叫TPS、QPS,它是服务器最基本的性能指标,RPS越高就说明服务器的性能越好。
  • 并发数反映的是服务器的负载能力,也就是服务器能够同时支持的客户端数量,当然也是越多越好,能够服务更多的用户。
  • 响应时间反映的是服务器的处理能力,也就是快慢程度,响应时间越短,单位时间内服务器就能够给越多的用户提供服务,提高吞吐量和并发数。
  • 除了上面的三个基本性能指标,服务器还要考虑CPU、内存、硬盘和网卡等系统资源的占用程度,利用率过高或者过低都可能有问题。

在 Linux 上,最常用的性能测试工具可能就是 ab(Apache Bench)了,比如,下面的命令指定了并发数 100,总共发送10000 个请求:·ab -c 100 -n 10000 'http://www.xxx.com'·
系统资源监控方面,Linux 自带的工具也非常多,常用的有uptime、top、vmstat、netstat、sar等等,可能你比我还要熟悉,我就列几个简单的例子吧:
top             # 查看 CPU 和内存占用情况
vmstat  2       # 每 2 秒检查一次系统状态
sar -n DEV 2    # 看所有网卡的流量,定时 2 秒检查
理解了这些性能指标,我们就知道了服务器的性能优化方向:合理利用系统资源,提高服务器的吞吐量和并发数,降低响应时间。

更高级的服务器性能测试工具有 LoadRunner、JMeter 等,很多云服务商也会提供专业的测试平台。

★,HTTP 客户端性能

HTTP客户端基本的性能指标就是“延迟”(latency)。 影响因素有地理距离、带宽、DNS 查询、TCP 握手等;

之前讲 HTTPS 时介绍过一个专门的网站“SSLLabs”(https://www.ssllabs.com/),而对于HTTP 性能优化,也有一个专门的测试网站“WebPageTest”(https://www.webpagetest.org/)。它的特点是在世界各地建立了很多的测试点,可以任意选择地理位置、机型、操作系统和浏览器发起测试,非常方便,用法也很简单。网站测试的最终结果是一个直观的“瀑布图”(WaterfallChart),清晰地列出了页面中所有资源加载的先后顺序和时间消耗。

★,

※,40 | HTTP性能优化面面观(下)

上一讲里我说到了,在整个 HTTP系统里有三个可优化的环节,分别是服务器、客户端和传输链路(“第一公里”和“中间一公里”)。但因为我们是无法完全控制客户端的,所以实际上的优化工作通常是在服务器端。这里又可以细分为后端和前端,后端是指网站的后台服务,而前端就是HTML、CSS、图片等展现在客户端的代码和数据。

★,花钱的

投资购买现成的硬件最简单的优化方式。

花钱购买外部的软件或者服务。也是一种行之有效的优化方式,最“物有所值”的应该算是 CDN了。CDN专注于网络内容交付,帮助网站解决“中间一公里”的问题,还有很多其他非常专业的优化功能。把网站交给 CDN运营,就好像是“让网站坐上了喷气飞机”,能够直达用户,几乎不需要费什么力气就能够达成很好的优化效果。

★,网站内部、“不花钱”的软件优化

这方面的 HTTP 性能优化概括为三个关键词:开源、节流、缓存。

开源:是指抓“源头”,开发网站服务器自身的潜力,在现有条件不变的情况下尽量挖掘出更多的服务能力

  • 选用高性能的 Web服务器,最佳选择当然就是 Nginx/OpenResty了,尽量不要选择基于 Java、Python、Ruby的其他服务器,它们用来做后面的业务逻辑服务器更好。利用Nginx 强大的反向代理能力实现“动静分离”,动态页面交给Tomcat、Django、Rails,图片、样式表等静态资源交给 Nginx。Nginx 或者 OpenResty自身也有很多配置参数可以用来进一步调优,举几个例子,比如说禁用负载均衡锁、增大连接池,绑定 CPU等等。特别要说的是,对于 HTTP 协议一定要启用长连接。另外,在现代操作系统上都已经支持 TCP 的新特性“TCP Fast Open”(Win10、iOS9、Linux 4.1),它的效果类似 TLS的“False Start”,可以在初次握手的时候就传输数据,也就是o-RTT,所以我们应该尽可能在操作系统和 Nginx里开启这个特性,减少外网和内网里的握手延迟。下面给出一个简短的 Nginx配置示例,启用了长连接等优化参数,实现了动静分离:
    server {
      listen 80 deferred reuseport backlog=4096 fastopen=1024; 
     
     
      keepalive_timeout  60;
      keepalive_requests 10000;
      
      location ~* \.(png)$ {
            root /var/images/png/;
      }
      
      location ~* \.(php)$ {
        proxy_pass http://php_back_end;
      }
    }

节流:“节流”是指减少客户端和服务器之间收发的数据量,在有限的带宽里传输更多的内容。“节流”最基本的做法就是使用 HTTP协议内置的“数据压缩”编码,不仅可以选择标准的gzip,还可以积极尝试新的压缩算法br,它有更好的压缩效果。不过在数据压缩的时候应当注意选择适当的压缩率,不要追求最高压缩比,否则会耗费服务器的计算资源,增加响应时间,降低服务能力,反而会“得不偿失”。

gzip 和 br 是通用的压缩算法,对于 HTTP协议传输的各种格式数据,我们还可以有针对性地采用特殊的压缩方式。

  • HTML/CSS/JS属于纯文本,就可以采用特殊的“压缩”,去掉源码里多余的空格、换行、注释等元素。这样“压缩”之后的文本虽然看起来很混乱,对“人类”不友好,但计算机仍然能够毫无障碍地阅读
  • 图片在 HTTP传输里占有非常高的比例,虽然它本身已经被压缩过了,不能被 gzip、br处理,但仍然有优化的空间。比如说,去除图片里的拍摄时间、地点、机型等元数据,适当降低分辨率,缩小尺寸。图片的格式也很关键,尽量选择高压缩率的格式,有损格式应该用 JPEG,无损格式应该用 Webp 格式。对于小文本或者小图片,还有一种叫做“资源合并”(Concatenation)的优化方式,就是把许多小资源合并成一个大资源,用一个请求全下载到客户端,然后客户端再用 JS、CSS切分后使用,好处是节省了请求次数,但缺点是处理比较麻烦。

压缩之外,“节流”还有两个优化点,就是域名和重定向。

  • 应当适当“收缩”域名,限制在两三个左右,减少解析完整域名所需的时间,让客户端尽快从系统缓存里获取解析结果。
  • 重定向引发的客户端延迟也很高,它不仅增加了一次请求往返,还有可能导致新域名的 DNS 解析,是 HTTP前端性能优化的“大忌”。除非必要,应当尽量不使用重定向,或者使用 Web 服务器的“内部重定向”。

缓存:缓存是无论何时都不能忘记的性能优化利器,应该总使用Etag 或 Last-modified 字段标记资源;

HTTP/2:  在“开源”“节流”和“缓存”这三大策略之外,HTTP性能优化还有一个选择,那就是把协议由 HTTP/1 升级到HTTP/2。通过“飞翔篇”的学习,你已经知道了 HTTP/2的很多优点,它消除了应用层的队头阻塞,拥有头部压缩、二进制帧、多路复用、流量控制、服务器推送等许多新特性,大幅度提升了 HTTP 的传输效率。实际上这些特性也是在“开源”和“节流”这两点上做文章,但因为这些都已经内置在了协议内,所以只要换上HTTP/2,网站就能够立刻获得显著的性能提升。不过你要注意,一些在 HTTP/1 里的优化手段到了 HTTP/2里会有“反效果”。对于 HTTP/2 来说,一个域名使用一个 TCP连接才能够获得最佳性能,如果开多个域名,就会浪费带宽和服务器资源,也会降低 HTTP/2的效率,所以“域名收缩”在 HTTP/2 里是必须要做的。“资源合并”在 HTTP/1 里减少了多次请求的成本,但在HTTP/2里因为有头部压缩和多路复用,传输小文件的成本很低,所以合并就失去了意义。而且“资源合并”还有一个缺点,就是降低了缓存的可用性,只要一个小文件更新,整个缓存就完全失效,必须重新下载。所以在现在的大带宽和 CDN应用场景下,应当尽量少用资源合并(JS、CSS图片合并,数据内嵌),让资源的粒度尽可能地小,才能更好地发挥缓存的作用。

★,

★,

※,1

★,

★,

★,

posted on 2024-02-02 18:13  everest33  阅读(13)  评论(0编辑  收藏  举报