This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
<audio id="audio" title="34 | Nginx高性能的Web服务器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c2/b4/c244add2f3ad05a959d875e667c336b4.mp3"></audio>
经过前面几大模块的学习你已经完全掌握了HTTP的所有知识那么接下来请收拾一下行囊整理一下装备跟我一起去探索HTTP之外的广阔天地。
现在的互联网非常发达用户越来越多网速越来越快HTTPS的安全加密、HTTP/2的多路复用等特性都对Web服务器提出了非常高的要求。一个好的Web服务器必须要具备稳定、快速、易扩展、易维护等特性才能够让网站“立于不败之地”。
那么,在搭建网站的时候,应该选择什么样的服务器软件呢?
在开头的几讲里我也提到过Web服务器就那么几款目前市面上主流的只有两个Apache和Nginx两者合计占据了近90%的市场份额。
今天我要说的就是其中的Nginx它是Web服务器的“后起之秀”虽然比Apache小了10岁但增长速度十分迅猛已经达到了与Apache“平起平坐”的地位而在“Top Million”网站中更是超过了Apache拥有超过50%的用户([参考数据](https://w3techs.com/technologies/cross/web_server/ranking))。
<img src="https://static001.geekbang.org/resource/image/c5/0b/c5df0592cc8aef91ba961f7fab5a4a0b.png" alt="unpreview">
在这里必须要说一下Nginx的正确发音它应该读成“Engine X”但我个人感觉“X”念起来太“拗口”还是比较倾向于读做“Engine ks”这也与UNIX、Linux的发音一致。
作为一个Web服务器Nginx的功能非常完善完美支持HTTP/1、HTTPS和HTTP/2而且还在不断进步。当前的主线版本已经发展到了1.17正在进行HTTP/3的研发或许一年之后就能在Nginx上跑HTTP/3了。
Nginx也是我个人的主要研究领域我也写过相关的书按理来说今天的课程应该是“手拿把攥”但真正动笔的时候还是有些犹豫的很多要点都已经在书里写过了这次的专栏如果再重复相同的内容就不免有“骗稿费”的嫌疑应该有些“不一样的东西”。
所以我决定抛开书本换个角度结合HTTP协议来讲Nginx带你窥视一下HTTP处理的内幕看看Web服务器的工作原理。
## 进程池
你也许听说过Nginx是个“轻量级”的Web服务器那么这个所谓的“轻量级”是什么意思呢
“轻量级”是相对于“重量级”而言的。“重量级”就是指服务器进程很“重”占用很多资源当处理HTTP请求时会消耗大量的CPU和内存受到这些资源的限制很难提高性能。
而Nginx作为“轻量级”的服务器它的CPU、内存占用都非常少同样的资源配置下就能够为更多的用户提供服务其奥秘在于它独特的工作模式。
<img src="https://static001.geekbang.org/resource/image/3e/c1/3e94fbd78ed043e88c443f6416f99dc1.png" alt="">
在Nginx之前Web服务器的工作模式大多是“Per-Process”或者“Per-Thread”对每一个请求使用单独的进程或者线程处理。这就存在创建进程或线程的成本还会有进程、线程“上下文切换”的额外开销。如果请求数量很多CPU就会在多个进程、线程之间切换时“疲于奔命”平白地浪费了计算时间。
Nginx则完全不同“一反惯例”地没有使用多线程而是使用了“**进程池+单线程**”的工作模式。
Nginx在启动的时候会预先创建好固定数量的worker进程在之后的运行过程中不会再fork出新进程这就是进程池而且可以自动把进程“绑定”到独立的CPU上这样就完全消除了进程创建和切换的成本能够充分利用多核CPU的计算能力。
在进程池之上还有一个“master”进程专门用来管理进程池。它的作用有点像是supervisor一个用Python编写的进程管理工具用来监控进程自动恢复发生异常的worker保持进程池的稳定和服务能力。
不过master进程完全是Nginx自行用C语言实现的这就摆脱了外部的依赖简化了Nginx的部署和配置。
## I/O多路复用
如果你用Java、C等语言写过程序一定很熟悉“多线程”的概念使用多线程能够很容易实现并发处理。
但多线程也有一些缺点,除了刚才说到的“上下文切换”成本,还有编程模型复杂、数据竞争、同步等问题,写出正确、快速的多线程程序并不是一件容易的事情。
所以Nginx就选择了单线程的方式带来的好处就是开发简单没有互斥锁的成本减少系统消耗。
那么疑问也就产生了为什么单线程的Nginx处理能力却能够超越其他多线程的服务器呢
这要归功于Nginx利用了Linux内核里的一件“神兵利器”**I/O多路复用接口**“大名鼎鼎”的epoll。
“多路复用”这个词我们已经在之前的HTTP/2、HTTP/3里遇到过好几次如果你理解了那里的“多路复用”那么面对Nginx的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的流非常相似每个请求处理单独来看是分散、阻塞的但因为都复用到了一个线程里所以资源的利用率非常高。
<img src="https://static001.geekbang.org/resource/image/4c/59/4c6832cdce34133c9ed89237fb9d5059.png" alt="">
epoll还有一个特点大量的连接管理工作都是在操作系统内核里做的这就减轻了应用程序的负担所以Nginx可以为每个连接只分配很小的内存维护状态即使有几万、几十万的并发连接也只会消耗几百M内存而其他的Web服务器这个时候早就“Memory not enough”了。
## 多阶段处理
有了“进程池”和“I/O多路复用”Nginx是如何处理HTTP请求的呢
Nginx在内部也采用的是“**化整为零**”的思路把整个Web服务器分解成了多个“功能模块”就好像是乐高积木可以在配置文件里任意拼接搭建从而实现了高度的灵活性和扩展性。
Nginx的HTTP处理有四大类模块
1. handler模块直接处理HTTP请求
1. filter模块不直接处理请求而是加工过滤响应报文
1. upstream模块实现反向代理功能转发请求到其他服务器
1. balance模块实现反向代理时的负载均衡算法。
因为upstream模块和balance模块实现的是代理功能Nginx作为“中间人”运行机制比较复杂所以我今天只讲handler模块和filter模块。
不知道你有没有了解过“设计模式”这方面的知识,其中有一个非常有用的模式叫做“**职责链**”。它就好像是工厂里的流水线,原料从一头流入,线上有许多工人会进行各种加工处理,最后从另一头出来的就是完整的产品。
Nginx里的handler模块和filter模块就是按照“职责链”模式设计和组织的HTTP请求报文就是“原材料”各种模块就是工厂里的工人走完模块构成的“流水线”出来的就是处理完成的响应报文。
下面的这张图显示了Nginx的“流水线”在Nginx里的术语叫“阶段式处理”Phases一共有11个阶段每个阶段里又有许多各司其职的模块。
<img src="https://static001.geekbang.org/resource/image/41/30/41318c867fda8a536d0e3db6f9987030.png" alt="">
我简单列几个与我们的课程相关的模块吧:
- charset模块实现了字符集编码转换[第15讲](https://time.geekbang.org/column/article/104024)
- chunked模块实现了响应数据的分块传输[第16讲](https://time.geekbang.org/column/article/104456)
- range模块实现了范围请求只返回数据的一部分[第16讲](https://time.geekbang.org/column/article/104456)
- rewrite模块实现了重定向和跳转还可以使用内置变量自定义跳转的URI[第18讲](https://time.geekbang.org/column/article/105614)
- not_modified模块检查头字段“if-Modified-Since”和“If-None-Match”处理条件请求[第20讲](https://time.geekbang.org/column/article/106804)
- realip模块处理“X-Real-IP”“X-Forwarded-For”等字段获取客户端的真实IP地址[第21讲](https://time.geekbang.org/column/article/107577)
- ssl模块实现了SSL/TLS协议支持读取磁盘上的证书和私钥实现TLS握手和SNI、ALPN等扩展功能[安全篇](https://time.geekbang.org/column/article/108643)
- http_v2模块实现了完整的HTTP/2协议。[飞翔篇](https://time.geekbang.org/column/article/112036)
在这张图里你还可以看到limit_conn、limit_req、access、log等其他模块它们实现的是限流限速、访问控制、日志等功能不在HTTP协议规定之内但对于运行在现实世界的Web服务器却是必备的。
如果你有C语言基础感兴趣的话可以下载Nginx的源码在代码级别仔细看看HTTP的处理过程。
## 小结
1. Nginx是一个高性能的Web服务器它非常的轻量级消耗的CPU、内存很少
1. Nginx采用“master/workers”进程池架构不使用多线程消除了进程、线程切换的成本
1. Nginx基于epoll实现了“I/O多路复用”不会阻塞所以性能很高
1. Nginx使用了“职责链”模式多个模块分工合作自由组合以流水线的方式处理HTTP请求。
## 课下作业
1. 你是怎么理解进程、线程上下文切换时的成本的为什么Nginx要尽量避免
1. 试着自己描述一下Nginx用进程、epoll、模块流水线处理HTTP请求的过程。
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/4c/3d/4c7bceb80a8027389705e9d6ec9eb43d.png" alt="unpreview">

View File

@@ -0,0 +1,130 @@
<audio id="audio" title="35 | OpenResty更灵活的Web服务器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/68/7d395f67094c2bfa140ee2d100996168.mp3"></audio>
在上一讲里我们看到了高性能的Web服务器Nginx它资源占用少处理能力高是搭建网站的首选。
虽然Nginx成为了Web服务器领域无可争议的“王者”但它也并不是没有缺点的毕竟它已经15岁了。
“一个人很难超越时代而时代却可以轻易超越所有人”Nginx当初设计时针对的应用场景已经发生了变化它的一些缺点也就暴露出来了。
Nginx的服务管理思路延续了当时的流行做法使用磁盘上的静态配置文件所以每次修改后必须重启才能生效。
这在业务频繁变动的时候是非常致命的(例如流行的微服务架构),特别是对于拥有成千上万台服务器的网站来说,仅仅增加或者删除一行配置就要分发、重启所有的机器,对运维是一个非常大的挑战,要耗费很多的时间和精力,成本很高,很不灵活,难以“随需应变”。
那么有没有这样的一个Web服务器它有Nginx的优点却没有Nginx的缺点既轻量级、高性能又灵活、可动态配置呢
这就是我今天要说的OpenResty它是一个“更好更灵活的Nginx”。
## OpenResty是什么
其实你对OpenResty并不陌生这个专栏的实验环境就是用OpenResty搭建的这么多节课程下来你应该或多或少对它有了一些印象吧。
OpenResty诞生于2009年到现在刚好满10周岁。它的创造者是当时就职于某宝的“神级”程序员**章亦春**网名叫“agentzh”。
OpenResty并不是一个全新的Web服务器而是基于Nginx它利用了Nginx模块化、可扩展的特性开发了一系列的增强模块并把它们打包整合形成了一个**“一站式”的Web开发平台**。
虽然OpenResty的核心是Nginx但它又超越了Nginx关键就在于其中的ngx_lua模块把小巧灵活的Lua语言嵌入了Nginx可以用脚本的方式操作Nginx内部的进程、多路复用、阶段式处理等各种构件。
脚本语言的好处你一定知道它不需要编译随写随执行这就免去了C语言编写模块漫长的开发周期。而且OpenResty还把Lua自身的协程与Nginx的事件机制完美结合在一起优雅地实现了许多其他语言所没有的“**同步非阻塞**”编程范式能够轻松开发出高性能的Web应用。
目前OpenResty有两个分支分别是开源、免费的“OpenResty”和闭源、商业产品的“OpenResty+”运作方式有社区支持、OpenResty基金会、OpenResty.Inc公司还有其他的一些外界赞助例如Kong、CloudFlare正在蓬勃发展。
<img src="https://static001.geekbang.org/resource/image/9f/01/9f7b79c43c476890f03c2c716a20f301.png" alt="unpreview">
顺便说一下OpenResty的官方logo是一只展翅飞翔的海鸥选择海鸥是因为“鸥”与OpenResty的发音相同。另外这个logo的形状也像是左手比出的一个“OK”姿势正好也是一个“O”。
## 动态的Lua
刚才说了OpenResty里的一个关键模块是ngx_lua它为Nginx引入了脚本语言Lua。
Lua是一个比较“小众”的语言虽然历史比较悠久但名气却没有PHP、Python、JavaScript大这主要与它的自身定位有关。
<img src="https://static001.geekbang.org/resource/image/4f/d5/4f24aa3f53969b71baaf7d9c7cf68fd5.png" alt="unpreview">
Lua的设计目标是嵌入到其他应用程序里运行为其他编程语言带来“脚本化”能力所以它的“个头”比较小功能集有限不追求“大而全”而是“小而美”大多数时间都“隐匿”在其他应用程序的后面是“无名英雄”。
你或许玩过或者听说过《魔兽世界》《愤怒的小鸟》吧它们就在内部嵌入了Lua使用Lua来调用底层接口充当“胶水语言”glue language编写游戏逻辑脚本提高开发效率。
OpenResty选择Lua作为“工作语言”也是基于同样的考虑。因为Nginx C开发实在是太麻烦了限制了Nginx的真正实力。而Lua作为“最快的脚本语言”恰好可以成为Nginx的完美搭档既可以简化开发性能上又不会有太多的损耗。
作为脚本语言Lua还有一个重要的“**代码热加载**”特性不需要重启进程就能够从磁盘、Redis或者任何其他地方加载数据随时替换内存里的代码片段。这就带来了“**动态配置**”让OpenResty能够永不停机在微秒、毫秒级别实现配置和业务逻辑的实时更新比起Nginx秒级的重启是一个极大的进步。
你可以看一下实验环境的“www/lua”目录里面存放了我写的一些测试HTTP特性的Lua脚本代码都非常简单易懂就像是普通的英语“阅读理解”这也是Lua的另一个优势易学习、易上手。
## 高效率的Lua
OpenResty能够高效运行的一大“秘技”是它的“**同步非阻塞**”编程范式如果你要开发OpenResty应用就必须时刻铭记于心。
“同步非阻塞”本质上还是一种“**多路复用**”我拿上一讲的Nginx epoll来对比解释一下。
epoll是操作系统级别的“多路复用”运行在内核空间。而OpenResty的“同步非阻塞”则是基于Lua内建的“**协程**”,是应用程序级别的“多路复用”,运行在用户空间,所以它的资源消耗要更少。
OpenResty里每一段Lua程序都由协程来调度运行。和Linux的epoll一样每当可能发生阻塞的时候“协程”就会立刻切换出去执行其他的程序。这样单个处理流程是“阻塞”的但整个OpenResty却是“非阻塞的”多个程序都“复用”在一个Lua虚拟机里运行。
<img src="https://static001.geekbang.org/resource/image/9f/c6/9fc3df52df7d6c11aa02b8013f8e9bc6.png" alt="">
下面的代码是一个简单的例子读取POST发送的body数据然后再发回客户端
```
ngx.req.read_body() -- 同步非阻塞(1)
local data = ngx.req.get_body_data()
if data then
ngx.print(&quot;body: &quot;, data) -- 同步非阻塞(2)
end
```
代码中的“ngx.req.read_body”和“ngx.print”分别是数据的收发动作只有收到数据才能发送数据所以是“同步”的。
但即使因为网络原因没收到或者发不出去OpenResty也不会在这里阻塞“干等着”而是做个“记号”把等待的这段CPU时间用来处理其他的请求等网络可读或者可写时再“回来”接着运行。
假设收发数据的等待时间是10毫秒而真正CPU处理的时间是0.1毫秒那么OpenResty就可以在这10毫秒内同时处理100个请求而不是把这100个请求阻塞排队用1000毫秒来处理。
除了“同步非阻塞”OpenResty还选用了**LuaJIT**作为Lua语言的“运行时Runtime进一步“挖潜增效”。
LuaJIT是一个高效的Lua虚拟机支持JITJust In Time技术可以把Lua代码即时编译成“本地机器码”这样就消除了脚本语言解释运行的劣势让Lua脚本跑得和原生C代码一样快。
另外LuaJIT还为Lua语言添加了一些特别的增强比如二进制位运算库bit内存优化库table还有FFIForeign Function Interface让Lua直接调用底层C函数比原生的压栈调用快很多。
## 阶段式处理
和Nginx一样OpenResty也使用“流水线”来处理HTTP请求底层的运行基础是Nginx的“阶段式处理”但它又有自己的特色。
Nginx的“流水线”是由一个个C模块组成的只能在静态文件里配置开发困难配置麻烦相对而言。而OpenResty的“流水线”则是由一个个的Lua脚本组成的不仅可以从磁盘上加载也可以从Redis、MySQL里加载而且编写、调试的过程非常方便快捷。
下面我画了一张图列出了OpenResty的阶段比起NginxOpenResty的阶段更注重对HTTP请求响应报文的加工和处理。
<img src="https://static001.geekbang.org/resource/image/36/df/3689312a970bae0e949b017ad45438df.png" alt="">
OpenResty里有几个阶段与Nginx是相同的比如rewrite、access、content、filter这些都是标准的HTTP处理。
在这几个阶段里可以用“xxx_by_lua”指令嵌入Lua代码执行重定向跳转、访问控制、产生响应、负载均衡、过滤报文等功能。因为Lua的脚本语言特性不用考虑内存分配、资源回收释放等底层的细节问题可以专注于编写非常复杂的业务逻辑比C模块的开发效率高很多即易于扩展又易于维护。
OpenResty里还有两个不同于Nginx的特殊阶段。
一个是“**init阶段**”它又分成“master init”和“worker init”在master进程和worker进程启动的时候运行。这个阶段还没有开始提供服务所以慢一点也没关系可以调用一些阻塞的接口初始化服务器比如读取磁盘、MySQL加载黑白名单或者数据模型然后放进共享内存里供运行时使用。
另一个是“**ssl阶段**”这算得上是OpenResty的一大创举可以在TLS握手时动态加载证书或者发送“OCSP Stapling”。
还记得[第29讲](https://time.geekbang.org/column/article/111940)里说的“SNI扩展”吗Nginx可以依据“服务器名称指示”来选择证书实现HTTPS虚拟主机但静态配置很不灵活要编写很多雷同的配置块。虽然后来Nginx增加了变量支持但它每次握手都要读磁盘效率很低。
而在OpenResty里就可以使用指令“ssl_certificate_by_lua”编写Lua脚本读取SNI名字后直接从共享内存或者Redis里获取证书。不仅没有读盘阻塞而且证书也是完全动态可配置的无需修改配置文件就能够轻松支持大量的HTTPS虚拟主机。
## 小结
1. Nginx依赖于磁盘上的静态配置文件修改后必须重启才能生效缺乏灵活性
1. OpenResty基于Nginx打包了很多有用的模块和库是一个高性能的Web开发平台
1. OpenResty的工作语言是Lua它小巧灵活执行效率高支持“代码热加载”
1. OpenResty的核心编程范式是“同步非阻塞”使用协程不需要异步回调函数
1. OpenResty也使用“阶段式处理”的工作模式但因为在阶段里执行的都是Lua代码所以非常灵活配合Redis等外部数据库能够实现各种动态配置。
## 课下作业
1. 谈一下这些天你对实验环境里OpenResty的感想和认识。
1. 你觉得Nginx和OpenResty的“阶段式处理”有什么好处对你的实际工作有没有启发
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/c5/9f/c5b7ac40c585c800af0fe3ab98f3449f.png" alt="unpreview">

View File

@@ -0,0 +1,182 @@
<audio id="audio" title="36 | WAF保护我们的网络服务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f5/0f/f53e1775cb784eeb10197f3bd6fa1b0f.mp3"></audio>
在前些天的“安全篇”里我谈到了HTTPS它使用了SSL/TLS协议加密整个通信过程能够防止恶意窃听和窜改保护我们的数据安全。
但HTTPS只是网络安全中很小的一部分仅仅保证了“通信链路安全”让第三方无法得知传输的内容。在通信链路的两端也就是客户端和服务器它是无法提供保护的。
因为HTTP是一个开放的协议Web服务都运行在公网上任何人都可以访问所以天然就会成为黑客的攻击目标。
而且黑客的本领比我们想象的还要大得多。虽然不能在传输过程中做手脚,但他们还可以“假扮”成合法的用户访问系统,然后伺机搞破坏。
## Web服务遇到的威胁
黑客都有哪些手段来攻击Web服务呢我给你大概列出几种常见的方式。
第一种叫“**DDoS**”攻击distributed denial-of-service attack有时候也叫“洪水攻击”。
黑客会控制许多“僵尸”计算机向目标服务器发起大量无效请求。因为服务器无法区分正常用户和黑客只能“照单全收”这样就挤占了正常用户所应有的资源。如果黑客的攻击强度很大就会像“洪水”一样对网站的服务能力造成冲击耗尽带宽、CPU和内存导致网站完全无法提供正常服务。
“DDoS”攻击方式比较“简单粗暴”虽然很有效但不涉及HTTP协议内部的细节“技术含量”比较低不过下面要说的几种手段就不一样了。
网站后台的Web服务经常会提取出HTTP报文里的各种信息应用于业务有时会缺乏严格的检查。因为HTTP报文在语义结构上非常松散、灵活URI里的query字符串、头字段、body数据都可以任意设置这就带来了安全隐患给了黑客“**代码注入**”的可能性。
黑客可以精心编制HTTP请求报文发送给服务器服务程序如果没有做防备就会“上当受骗”执行黑客设定的代码。
“**SQL注入**”SQL injection应该算是最著名的一种“代码注入”攻击了它利用了服务器字符串拼接形成SQL语句的漏洞构造出非正常的SQL语句获取数据库内部的敏感信息。
另一种“**HTTP头注入**”攻击的方式也是类似的原理它在“Host”“User-Agent”“X-Forwarded-For”等字段里加入了恶意数据或代码服务端程序如果解析不当就会执行预设的恶意代码。
在之前的[第19讲](https://time.geekbang.org/column/article/106034)里也说过一种利用Cookie的攻击手段“**跨站脚本**”XSS攻击它属于“JS代码注入”利用JavaScript脚本获取未设防的Cookie。
## 网络应用防火墙
面对这么多的黑客攻击手段,我们应该怎么防御呢?
这就要用到“**网络应用防火墙**”Web Application Firewall简称为“**WAF**”。
你可能对传统的“防火墙”比较熟悉。传统“防火墙”工作在三层或者四层隔离了外网和内网使用预设的规则只允许某些特定IP地址和端口号的数据包通过拒绝不符合条件的数据流入或流出内网实质上是**一种网络数据过滤设备**。
WAF也是一种“防火墙”但它工作在七层看到的不仅是IP地址和端口号还能看到整个HTTP报文所以就能够对报文内容做更深入细致的审核使用更复杂的条件、规则来过滤数据。
说白了WAF就是一种“**HTTP入侵检测和防御系统**”。
<img src="https://static001.geekbang.org/resource/image/e8/a3/e8369d077454e5b92e3722e7090551a3.png" alt="">
WAF都能干什么呢
通常一款产品能够称为WAF要具备下面的一些功能
- IP黑名单和白名单拒绝黑名单上地址的访问或者只允许白名单上的用户访问
- URI黑名单和白名单与IP黑白名单类似允许或禁止对某些URI的访问
- 防护DDoS攻击对特定的IP地址限连限速
- 过滤请求报文,防御“代码注入”攻击;
- 过滤响应报文,防御敏感信息外泄;
- 审计日志,记录所有检测到的入侵操作。
听起来WAF好像很高深但如果你理解了它的工作原理其实也不难。
它就像是平时编写程序时必须要做的函数入口参数检查拿到HTTP请求、响应报文用字符串处理函数看看有没有关键字、敏感词或者用正则表达式做一下模式匹配命中了规则就执行对应的动作比如返回403/404。
如果你比较熟悉Apache、Nginx、OpenResty可以自己改改配置文件写点JS或者Lua代码就能够实现基本的WAF功能。
比如说在Nginx里实现IP地址黑名单可以利用“map”指令从变量$remote_addr获取IP地址在黑名单上就映射为值1然后在“if”指令里判断
```
map $remote_addr $blocked {
default 0;
&quot;1.2.3.4&quot; 1;
&quot;5.6.7.8&quot; 1;
}
if ($blocked) {
return 403 &quot;you are blocked.&quot;;
}
```
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解决方案
这里我就要“隆重”介绍一下WAF领域里的最顶级产品了ModSecurity它可以说是WAF界“事实上的标准”。
ModSecurity是一个开源的、生产级的WAF工具包历史很悠久比Nginx还要大几岁。它开始于一个私人项目后来被商业公司Breach Security收购现在则是由TrustWave公司的SpiderLabs团队负责维护。
ModSecurity最早是Apache的一个模块只能运行在Apache上。因为其品质出众大受欢迎后来的2.x版添加了Nginx和IIS支持但因为底层架构存在差异不够稳定。
所以这两年SpiderLabs团队就开发了全新的3.0版本移除了对Apache架构的依赖使用新的“连接器”来集成进Apache或者Nginx比2.x版更加稳定和快速误报率也更低。
ModSecurity有两个核心组件。第一个是“**规则引擎**”它实现了自定义的“SecRule”语言有自己特定的语法。但“SecRule”主要基于正则表达式还是不够灵活所以后来也引入了Lua实现了脚本化配置。
ModSecurity的规则引擎使用C++11实现可以从[GitHub](https://github.com/SpiderLabs/ModSecurity)上下载源码然后集成进Nginx。因为它比较庞大编译很费时间所以最好编译成动态模块在配置文件里用指令“load_module”加载
```
load_module modules/ngx_http_modsecurity_module.so;
```
只有引擎还不够要让引擎运转起来还需要完善的防御规则所以ModSecurity的第二个核心组件就是它的“**规则集**”。
ModSecurity源码提供一个基本的规则配置文件“**modsecurity.conf-recommended**”使用前要把它的后缀改成“conf”。
有了规则集就可以在Nginx配置文件里加载然后启动规则引擎
```
modsecurity on;
modsecurity_rules_file /path/to/modsecurity.conf;
```
“modsecurity.conf”文件默认只有检测功能不提供入侵阻断这是为了防止误杀误报把“SecRuleEngine”后面改成“On”就可以开启完全的防护
```
#SecRuleEngine DetectionOnly
SecRuleEngine On
```
基本的规则集之外ModSecurity还额外提供一个更完善的规则集为网站提供全面可靠的保护。这个规则集的全名叫“**OWASP ModSecurity 核心规则集**”Open Web Application Security Project ModSecurity Core Rule Set因为名字太长了所以有时候会简称为“核心规则集”或者“CRS”。
<img src="https://static001.geekbang.org/resource/image/ad/48/add929f8439c64f29db720d30f7de548.png" alt="">
CRS也是完全开源、免费的可以从GitHub上下载
```
git clone https://github.com/SpiderLabs/owasp-modsecurity-crs.git
```
其中有一个“**crs-setup.conf.example**”的文件它是CRS的基本配置可以用“Include”命令添加到“modsecurity.conf”里然后再添加“rules”里的各种规则。
```
Include /path/to/crs-setup.conf
Include /path/to/rules/*.conf
```
你如果有兴趣可以看一下这些配置文件里面用“SecRule”定义了很多的规则基本的形式是“SecRule 变量 运算符 动作”。不过ModSecurity的这套语法“自成一体”比较复杂要完全掌握不是一朝一夕的事情我就不详细解释了。
另外ModSecurity还有强大的审计日志Audit Log功能记录任何可疑的数据供事后离线分析。但在生产环境中会遇到大量的攻击日志会快速增长消耗磁盘空间而且写磁盘也会影响Nginx的性能所以一般建议把它关闭
```
SecAuditEngine off #RelevantOnly
SecAuditLog /var/log/modsec_audit.log
```
## 小结
今天我们一起学习了“网络应用防火墙”也就是WAF使用它可以加固Web服务。
1. Web服务通常都运行在公网上容易受到“DDoS”、“代码注入”等各种黑客攻击影响正常的服务所以必须要采取措施加以保护
1. WAF是一种“HTTP入侵检测和防御系统”工作在七层为Web服务提供全面的防护
1. ModSecurity是一个开源的、生产级的WAF产品核心组成部分是“规则引擎”和“规则集”两者的关系有点像杀毒引擎和病毒特征库
1. WAF实质上是模式匹配与数据过滤所以会消耗CPU增加一些计算成本降低服务能力使用时需要在安全与性能之间找到一个“平衡点”。
## 课下作业
1. HTTPS为什么不能防御DDoS、代码注入等攻击呢
1. 你还知道有哪些手段能够抵御网络攻击吗?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/b9/24/b9e48b813c98bb34b4b433b7326ace24.png" alt="unpreview">

View File

@@ -0,0 +1,120 @@
<audio id="audio" title="37 | CDN加速我们的网络服务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/44/4f/44e3c0a62c765e9df59d0447ec226a4f.mp3"></audio>
在正式开讲前我们先来看看到现在为止HTTP手头都有了哪些“武器”。
协议方面HTTPS强化通信链路安全、HTTP/2优化传输效率应用方面Nginx/OpenResty提升网站服务能力WAF抵御网站入侵攻击讲到这里你是不是感觉还少了点什么
没错在应用领域还缺一个在外部加速HTTP协议的服务这个就是我们今天要说的CDNContent Delivery Network或Content Distribution Network中文名叫“内容分发网络”。
## 为什么要有网络加速?
你可能要问了HTTP的传输速度也不算差啊而且还有更好的HTTP/2为什么还要再有一个额外的CDN来加速呢是不是有点“多此一举”呢
这里我们就必须要考虑现实中会遇到的问题了。你一定知道光速是有限的虽然每秒30万公里但这只是真空中的上限在实际的电缆、光缆中的速度会下降到原本的三分之二左右也就是20万公里/秒,这样一来,地理位置的距离导致的传输延迟就会变得比较明显了。
比如北京到广州直线距离大约是2000公里按照刚才的20万公里/秒来算的话发送一个请求单程就要10毫秒往返要20毫秒即使什么都不干这个“硬性”的时延也是躲不过的。
另外不要忘了, 互联网从逻辑上看是一张大网,但实际上是由许多小网络组成的,这其中就有小网络“互连互通”的问题,典型的就是各个电信运营商的网络,比如国内的电信、联通、移动三大家。
<img src="https://static001.geekbang.org/resource/image/41/b9/413605355db69278cb137b318b70b3b9.png" alt="">
这些小网络内部的沟通很顺畅但网络之间却只有很少的联通点。如果你在A网络而网站在C网络那么就必须“跨网”传输和成千上万的其他用户一起去“挤”连接点的“独木桥”。而带宽终究是有限的能抢到多少只能看你的运气。
还有,网络中还存在许多的路由器、网关,数据每经过一个节点,都要停顿一下,在二层、三层解析转发,这也会消耗一定的时间,带来延迟。
把这些因素再放到全球来看,地理距离、运营商网络、路由转发的影响就会成倍增加。想象一下,你在北京,访问旧金山的网站,要跨越半个地球,中间会有多少环节,会增加多少时延?
最终结果就是如果仅用现有的HTTP传输方式大多数网站都会访问速度缓慢、用户体验糟糕。
## 什么是CDN
这个时候CDN就出现了它就是专门为解决“长距离”上网络访问速度慢而诞生的一种网络应用服务。
从名字上看CDN有三个关键词“**内容**”“**分发**”和“**网络**”。
先看一下“网络”的含义。CDN的最核心原则是“**就近访问**”如果用户能够在本地几十公里的距离之内获取到数据那么时延就基本上变成0了。
所以CDN投入了大笔资金在全国、乃至全球的各个大枢纽城市都建立了机房部署了大量拥有高存储高带宽的节点构建了一个专用网络。这个网络是跨运营商、跨地域的虽然内部也划分成多个小网络但它们之间用高速专有线路连接是真正的“信息高速公路”基本上可以认为不存在网络拥堵。
有了这个高速的专用网之后CDN就要“分发”源站的“内容”了用到的就是在[第22讲](https://time.geekbang.org/column/article/108313)说过的“**缓存代理**”技术。使用“推”或者“拉”的手段,把源站的内容逐级缓存到网络的每一个节点上。
于是用户在上网的时候就不直接访问源站而是访问离他“最近的”一个CDN节点术语叫“**边缘节点**”edge node其实就是缓存了源站内容的代理服务器这样一来就省去了“长途跋涉”的时间成本实现了“网络加速”。
<img src="https://static001.geekbang.org/resource/image/46/5b/46d1dbbb545fcf3cfb53407e0afe9a5b.png" alt="">
那么CDN都能加速什么样的“内容”呢
在CDN领域里“内容”其实就是HTTP协议里的“资源”比如超文本、图片、视频、应用程序安装包等等。
资源按照是否可缓存又分为“**静态资源**”和“**动态资源**”。所谓的“静态资源”是指数据内容“静态不变”,任何时候来访问都是一样的,比如图片、音频。所谓的“动态资源”是指数据内容是“动态变化”的,也就是由后台服务计算生成的,每次访问都不一样,比如商品的库存、微博的粉丝数等。
很显然只有静态资源才能够被缓存加速、就近访问而动态资源只能由源站实时生成即使缓存了也没有意义。不过如果动态资源指定了“Cache-Control”允许缓存短暂的时间那它在这段时间里也就变成了“静态资源”可以被CDN缓存加速。
套用一句广告词来形容CDN吧我觉得非常恰当“**我们不生产内容,我们只是内容的搬运工。**”
CDN正是把“数据传输”这件看似简单的事情“做大做强”“做专做精”就像专门的快递公司一样在互联网世界里实现了它的价值。
## CDN的负载均衡
我们再来看看CDN是具体怎么运行的它有两个关键组成部分**全局负载均衡**和**缓存系统**对应的是DNS[第6讲](https://time.geekbang.org/column/article/99665))和缓存代理([第21讲](https://time.geekbang.org/column/article/107577)、[第22讲](https://time.geekbang.org/column/article/108313))技术。
全局负载均衡Global Sever Load Balance一般简称为GSLB它是CDN的“大脑”主要的职责是当用户接入网络的时候在CDN专网中挑选出一个“最佳”节点提供服务解决的是用户如何找到“最近的”边缘节点对整个CDN网络进行“负载均衡”。
<img src="https://static001.geekbang.org/resource/image/6c/ca/6c39e76d58d9f17872c83ae72908faca.png" alt="">
GSLB最常见的实现方式是“**DNS负载均衡**”,这个在[第6讲](https://time.geekbang.org/column/article/99665)里也说过不过GSLB的方式要略微复杂一些。
原来没有CDN的时候权威DNS返回的是网站自己服务器的实际IP地址浏览器收到DNS解析结果后直连网站。
但加入CDN后就不一样了权威DNS返回的不是IP地址而是一个CNAME( Canonical Name )别名记录指向的就是CDN的GSLB。它有点像是HTTP/2里“Alt-Svc”的意思告诉外面“我这里暂时没法给你真正的地址你去另外一个地方再查查看吧。”
因为没拿到IP地址于是本地DNS就会向GSLB再发起请求这样就进入了CDN的全局负载均衡系统开始“智能调度”主要的依据有这么几个
1. 看用户的IP地址查表得知地理位置找相对最近的边缘节点
1. 看用户所在的运营商网络,找相同网络的边缘节点;
1. 检查边缘节点的负载情况,找负载较轻的节点;
1. 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等。
GSLB把这些因素综合起来用一个复杂的算法最后找出一台“最合适”的边缘节点把这个节点的IP地址返回给用户用户就可以“就近”访问CDN的缓存代理了。
## CDN的缓存代理
缓存系统是CDN的另一个关键组成部分相当于CDN的“心脏”。如果缓存系统的服务能力不够不能很好地满足用户的需求那GSLB调度算法再优秀也没有用。
但互联网上的资源是无穷无尽的不管CDN厂商有多大的实力也不可能把所有资源都缓存起来。所以缓存系统只能有选择地缓存那些最常用的那些资源。
这里就有两个CDN的关键概念“**命中**”和“**回源**”。
“命中”就是指用户访问的资源恰好在缓存系统里,可以直接返回给用户;“回源”则正相反,缓存里没有,必须用代理的方式回源站取。
相应地也就有了两个衡量CDN服务质量的指标“**命中率**”和“**回源率**”。命中率就是命中次数与所有访问次数之比回源率是回源次数与所有访问次数之比。显然好的CDN应该是命中率越高越好回源率越低越好。现在的商业CDN命中率都在90%以上相当于把源站的服务能力放大了10倍以上。
怎么样才能尽可能地提高命中率、降低回源率呢?
首先最基本的方式就是在存储系统上下功夫硬件用高速CPU、大内存、万兆网卡再搭配TB级别的硬盘和快速的SSD。软件方面则不断“求新求变”各种新的存储软件都会拿来尝试比如Memcache、Redis、Ceph尽可能地高效利用存储存下更多的内容。
其次,缓存系统也可以划分出层次,分成一级缓存节点和二级缓存节点。一级缓存配置高一些,直连源站,二级缓存配置低一些,直连用户。回源的时候二级缓存只找一级缓存,一级缓存没有才回源站,这样最终“扇入度”就缩小了,可以有效地减少真正的回源。
第三个就是使用高性能的缓存服务据我所知目前国内的CDN厂商内部都是基于开源软件定制的。最常用的是专门的缓存代理软件Squid、Varnish还有新兴的ATSApache Traffic Server而Nginx和OpenResty作为Web服务器领域的“多面手”凭借着强大的反向代理能力和模块化、易于扩展的优点也在CDN里占据了不少的份额。
## 小结
CDN发展到现在已经有二十来年的历史了早期的CDN功能比较简单只能加速静态资源。随着这些年Web 2.0、HTTPS、视频、直播等新技术、新业务的崛起它也在不断进步增加了很多的新功能比如SSL加速、内容优化数据压缩、图片格式转换、视频转码、资源防盗链、WAF安全防护等等。
现在再说CDN是“搬运工”已经不太准确了它更像是一个“无微不至”的“网站保姆”让网站只安心生产优质的内容其他的“杂事”都由它去代劳。
1. 由于客观地理距离的存在直连网站访问速度会很慢所以就出现了CDN
1. CDN构建了全国、全球级别的专网让用户就近访问专网里的边缘节点降低了传输延迟实现了网站加速
1. GSLB是CDN的“大脑”使用DNS负载均衡技术智能调度边缘节点提供服务
1. 缓存系统是CDN的“心脏”使用HTTP缓存代理技术缓存命中就返回给用户否则就要回源。
## 课下作业
1. 网站也可以自建同城、异地多处机房构建集群来提高服务能力为什么非要选择CDN呢
1. 对于无法缓存的动态资源你觉得CDN也能有加速效果吗
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/bc/51/bc1805a7c49977c7838b29602f3bba51.png" alt="unpreview">

View File

@@ -0,0 +1,151 @@
<audio id="audio" title="38 | WebSocket沙盒里的TCP" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5e/e2/5e9e27f590f3fd65f21975e334447ee2.mp3"></audio>
在之前讲TCP/IP协议栈的时候我说过有“TCP Socket”它实际上是一种功能接口通过这些接口就可以使用TCP/IP协议栈在传输层收发数据。
那么你知道还有一种东西叫“WebSocket”吗
单从名字上看“Web”指的是HTTP“Socket”是套接字调用那么这两个连起来又是什么意思呢
所谓“望文生义”大概你也能猜出来“WebSocket”就是运行在“Web”也就是HTTP上的Socket通信规范提供与“TCP Socket”类似的功能使用它就可以像“TCP Socket”一样调用下层协议栈任意地收发数据。
<img src="https://static001.geekbang.org/resource/image/ee/28/ee6685c7d3c673b95e46d582828eee28.png" alt="">
更准确地说“WebSocket”是一种基于TCP的轻量级网络通信协议在地位上是与HTTP“平级”的。
## 为什么要有WebSocket
不过已经有了被广泛应用的HTTP协议为什么要再出一个WebSocket呢它有哪些好处呢
其实WebSocket与HTTP/2一样都是为了解决HTTP某方面的缺陷而诞生的。HTTP/2针对的是“队头阻塞”而WebSocket针对的是“请求-应答”通信模式。
那么,“请求-应答”有什么不好的地方呢?
“请求-应答”是一种“**半双工**”的通信模式,虽然可以双向收发数据,但同一时刻只能一个方向上有动作,传输效率低。更关键的一点,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。
虽然后来的HTTP/2、HTTP/3新增了Stream、Server Push等特性但“请求-应答”依然是主要的工作方式。这就导致HTTP难以应用在动态页面、即时消息、网络游戏等要求“**实时通信**”的领域。
在WebSocket出现之前在浏览器环境里用JavaScript开发实时Web应用很麻烦。因为浏览器是一个“受限的沙盒”不能用TCP只有HTTP协议可用所以就出现了很多“变通”的技术“**轮询**”polling就是比较常用的的一种。
简单地说轮询就是不停地向服务器发送HTTP请求问有没有数据有数据的话服务器就用响应报文回应。如果轮询的频率比较高那么就可以近似地实现“实时通信”的效果。
但轮询的缺点也很明显反复发送无效查询请求耗费了大量的带宽和CPU资源非常不经济。
所以为了克服HTTP“请求-应答”模式的缺点WebSocket就“应运而生”了。它原来是HTML5的一部分后来“自立门户”形成了一个单独的标准RFC文档编号是6455。
## WebSocket的特点
WebSocket是一个真正“**全双工**”的通信协议与TCP一样客户端和服务器都可以随时向对方发送数据而不用像HTTP“你拍一我拍一”那么“客套”。于是服务器就可以变得更加“主动”了。一旦后台有新的数据就可以立即“推送”给客户端不需要客户端轮询“实时通信”的效率也就提高了。
WebSocket采用了二进制帧结构语法、语义与HTTP完全不兼容但因为它的主要运行环境是浏览器为了便于推广和应用就不得不“搭便车”在使用习惯上尽量向HTTP靠拢这就是它名字里“Web”的含义。
服务发现方面WebSocket没有使用TCP的“IP地址+端口号”而是延用了HTTP的URI格式但开头的协议名不是“http”引入的是两个新的名字“**ws**”和“**wss**”分别表示明文和加密的WebSocket协议。
WebSocket的默认端口也选择了80和443因为现在互联网上的防火墙屏蔽了绝大多数的端口只对HTTP的80、443端口“放行”所以WebSocket就可以“伪装”成HTTP协议比较容易地“穿透”防火墙与服务器建立连接。具体是怎么“伪装”的我稍后再讲。
下面我举几个WebSocket服务的例子你看看是不是和HTTP几乎一模一样
```
ws://www.chrono.com
ws://www.chrono.com:8080/srv
wss://www.chrono.com:445/im?user_id=xxx
```
要注意的一点是WebSocket的名字容易让人产生误解虽然大多数情况下我们会在浏览器里调用API来使用WebSocket但它不是一个“调用接口的集合”而是一个通信协议所以我觉得把它理解成“**TCP over Web**”会更恰当一些。
## WebSocket的帧结构
刚才说了WebSocket用的也是二进制帧有之前HTTP/2、HTTP/3的经验相信你这次也能很快掌握WebSocket的报文结构。
不过WebSocket和HTTP/2的关注点不同WebSocket更**侧重于“实时通信”**而HTTP/2更侧重于提高传输效率所以两者的帧结构也有很大的区别。
WebSocket虽然有“帧”但却没有像HTTP/2那样定义“流”也就不存在“多路复用”“优先级”等复杂的特性而它自身就是“全双工”的也就不需要“服务器推送”。所以综合起来WebSocket的帧学习起来会简单一些。
下图就是WebSocket的帧结构定义长度不固定最少2个字节最多14字节看着好像很复杂实际非常简单。
<img src="https://static001.geekbang.org/resource/image/29/c4/29d33e972dda5a27aa4773eea896a8c4.png" alt="">
开头的两个字节是必须的,也是最关键的。
第一个字节的第一位“**FIN**”是消息结束的标志位相当于HTTP/2里的“END_STREAM”表示数据发送完毕。一个消息可以拆成多个帧接收方看到“FIN”后就可以把前面的帧拼起来组成完整的消息。
“FIN”后面的三个位是保留位目前没有任何意义但必须是0。
第一个字节的后4位很重要叫**“Opcode**”操作码其实就是帧类型比如1表示帧内容是纯文本2表示帧内容是二进制数据8是关闭连接9和10分别是连接保活的PING和PONG。
第二个字节第一位是掩码标志位“**MASK**”表示帧内容是否使用异或操作xor做简单的加密。目前的WebSocket标准规定客户端发送数据必须使用掩码而服务器发送则必须不使用掩码。
第二个字节后7位是“**Payload len**”表示帧内容的长度。它是另一种变长编码最少7位最多是7+64位也就是额外增加8个字节所以一个WebSocket帧最大是2^64。
长度字段后面是“**Masking-key**”掩码密钥它是由上面的标志位“MASK”决定的如果使用掩码就是4个字节的随机数否则就不存在。
这么分析下来其实WebSocket的帧头就四个部分“**结束标志位+操作码+帧长度+掩码**”只是使用了变长编码的“小花招”不像HTTP/2定长报文头那么简单明了。
我们的实验环境利用OpenResty的“lua-resty-websocket”库实现了一个简单的WebSocket通信你可以访问URI“/38-1”它会连接后端的WebSocket服务“ws://127.0.0.1/38-0”用Wireshark抓包就可以看到WebSocket的整个通信过程。
下面的截图是其中的一个文本帧因为它是客户端发出的所以需要掩码报文头就在两个字节之外多了四个字节的“Masking-key”总共是6个字节。
<img src="https://static001.geekbang.org/resource/image/c9/94/c91ee4815097f5f9059ab798bb841594.png" alt="">
而报文内容经过掩码不是直接可见的明文但掩码的安全强度几乎是零用“Masking-key”简单地异或一下就可以转换出明文。
## WebSocket的握手
和TCP、TLS一样WebSocket也要有一个握手过程然后才能正式收发数据。
这里它还是搭上了HTTP的“便车”利用了HTTP本身的“协议升级”特性“伪装”成HTTP这样就能绕过浏览器沙盒、网络防火墙等等限制这也是WebSocket与HTTP的另一个重要关联点。
WebSocket的握手是一个标准的HTTP GET请求但要带上两个协议升级的专用头字段
- “Connection: Upgrade”表示要求协议“升级”
- “Upgrade: websocket”表示要“升级”成WebSocket协议。
另外为了防止普通的HTTP消息被“意外”识别成WebSocket握手消息还增加了两个额外的认证用头字段所谓的“挑战”Challenge
- Sec-WebSocket-Key一个Base64编码的16字节随机数作为简单的认证密钥
- Sec-WebSocket-Version协议的版本号当前必须是13。
<img src="https://static001.geekbang.org/resource/image/8f/97/8f007bb0e403b6cc28493565f709c997.png" alt="">
服务器收到HTTP请求报文看到上面的四个字段就知道这不是一个普通的GET请求而是WebSocket的升级请求于是就不走普通的HTTP处理流程而是构造一个特殊的“101 Switching Protocols”响应报文通知客户端接下来就不用HTTP了全改用WebSocket协议通信。有点像TLS的“Change Cipher Spec”
WebSocket的握手响应报文也是有特殊格式的要用字段“Sec-WebSocket-Accept”验证客户端请求报文同样也是为了防止误连接。
具体的做法是把请求头里“Sec-WebSocket-Key”的值加上一个专用的UUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”再计算SHA-1摘要。
```
encode_base64(
sha1(
Sec-WebSocket-Key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' ))
```
客户端收到响应报文,就可以用同样的算法,比对值是否相等,如果相等,就说明返回的报文确实是刚才握手时连接的服务器,认证成功。
握手完成后续传输的数据就不再是HTTP报文而是WebSocket格式的二进制帧了。
<img src="https://static001.geekbang.org/resource/image/84/03/84e9fa337f2b4c2c9f14760feb41c903.png" alt="">
## 小结
浏览器是一个“沙盒”环境有很多的限制不允许建立TCP连接收发数据而有了WebSocket我们就可以在浏览器里与服务器直接建立“TCP连接”获得更多的自由。
不过自由也是有代价的WebSocket虽然是在应用层但使用方式却与“TCP Socket”差不多过于“原始”用户必须自己管理连接、缓存、状态开发上比HTTP复杂的多所以是否要在项目中引入WebSocket必须慎重考虑。
1. HTTP的“请求-应答”模式不适合开发“实时通信”应用效率低难以实现动态页面所以出现了WebSocket
1. WebSocket是一个“全双工”的通信协议相当于对TCP做了一层“薄薄的包装”让它运行在浏览器环境里
1. WebSocket使用兼容HTTP的URI来发现服务但定义了新的协议名“ws”和“wss”端口号也沿用了80和443
1. WebSocket使用二进制帧结构比较简单特殊的地方是有个“掩码”操作客户端发数据必须掩码服务器则不用
1. WebSocket利用HTTP协议实现连接握手发送GET请求要求“协议升级”握手过程中有个非常简单的认证机制目的是防止误连接。
## 课下作业
1. WebSocket与HTTP/2有很多相似点比如都可以从HTTP/1升级都采用二进制帧结构你能比较一下这两个协议吗
1. 试着自己解释一下WebSocket里的”Web“和”Socket“的含义。
1. 结合自己的实际工作你觉得WebSocket适合用在哪些场景里
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/4b/5b/4b81de6b5c57db92ed7808344482ef5b.png" alt="unpreview">