mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
mod
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
<audio id="audio" title="01 | 网络互联的昨天、今天和明天:HTTP 协议的演化" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cc/de/cc72b9a8088d86f0d30604385494e7de.mp3"></audio>
|
||||
|
||||
你好,我是四火。
|
||||
|
||||
HTTP 协议是互联网基础中的基础,和很多技术谈具体应用场景不同的是,几乎所有的互联网服务都是它的应用,没有它,互联网的“互联”将无从谈起,因此我们把它作为正式学习的开篇。
|
||||
|
||||
说到其原理和协议本身,我相信大多数人都能说出个大概来,比如,有哪些常见的方法,常见 HTTP 头,返回码的含义等等。但你是否想过,这个古老而富有生命力的互联网“基石”是怎样发展演化过来的呢?从它身上,我们能否管中窥豹,一叶知秋,找到互联网成长和演进的影子?
|
||||
|
||||
今天,我想带你从实践的角度,亲身感受下这个过程,相信除了 HTTP 本身,你还可以发现网络协议发展过程中的一些通用和具有共性的东西。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/2a/ac90bdd14bced3d81e314a7eddf6972a.png" alt="">
|
||||
|
||||
## HTTP/0.9
|
||||
|
||||
和很多其它协议一样,1991年,HTTP 在最开始的 0.9 版就定义了协议最核心的内容,虽说从功能上看只是具备了如今内容的一个小小的子集。比如,确定了客户端、服务端的这种基本结构,使用域名/IP 加端口号来确定目标地址的方式,还有换行回车作为基本的分隔符。
|
||||
|
||||
它非常简单,不支持请求正文,不支持除了 GET 以外的其它方法,不支持头部,甚至没有版本号的显式指定,而且整个请求只有一行,因而也被称为“The One-line Protocol”。比如:
|
||||
|
||||
```
|
||||
GET /target.html
|
||||
|
||||
```
|
||||
|
||||
虽说 0.9 版本如今已经极少见到了,但幸运的是 Google 还依然支持(Bing 和 Baidu 不支持)。我们不妨自己动手,实践一下!虽然不能使用浏览器,但别忘了,我们还有一个更古老的工具 telnet。在命令行下建立连接:
|
||||
|
||||
```
|
||||
telnet www.google.com 80
|
||||
|
||||
```
|
||||
|
||||
你会看到类似这样的提示:
|
||||
|
||||
```
|
||||
Trying 2607:f8b0:400a:803::2004...
|
||||
Connected to www.google.com.
|
||||
Escape character is '^]'.
|
||||
|
||||
```
|
||||
|
||||
好,现在输入以下请求:
|
||||
|
||||
```
|
||||
GET /
|
||||
|
||||
```
|
||||
|
||||
(请注意这里没有版本号,并不代表 HTTP 协议没有版本号,而是 0.9 版本的协议定义的请求中就是不带有版本号,这其实是该版本的一个缺陷)
|
||||
|
||||
接着,你会看到 Google 把首页 HTML 返回了:
|
||||
|
||||
```
|
||||
HTTP/1.0 200 OK
|
||||
...(此处省略多行HTTP 头)
|
||||
|
||||
...(此处省略正文)
|
||||
|
||||
```
|
||||
|
||||
## HTTP/1.0
|
||||
|
||||
到了1996年,HTTP 1.0 版本就稳定而成熟了,也是如今浏览器广泛支持的最低版本 HTTP 协议。引入了返回码,引入了 header,引入了多字符集,也终于支持多行请求了。
|
||||
|
||||
当然,它的问题也还有很多,支持的特性也远没有后来的 1.1 版本多样。比如,方法只支持 GET、HEAD、POST 这几个。但是,麻雀虽小五脏俱全,这是第一个具备广泛实际应用价值的协议版本。
|
||||
|
||||
你一样可以用和前面类似的方法来亲自动手实践一下,不过,HTTP 1.0 因为支持多行文本的请求,单纯使用 telnet 已经无法很好地一次发送它们了,其中一个解决办法就是使用 [netcat](http://netcat.sourceforge.net/)。
|
||||
|
||||
好,我们先手写一份 HTTP/1.0 的多行请求,并保存到一个文件 request.txt 中:
|
||||
|
||||
```
|
||||
GET / HTTP/1.0
|
||||
User-Agent: Mozilla/1.22 (compatible; MSIE 2.0; Windows 3.1)
|
||||
Accept: text/html
|
||||
|
||||
|
||||
```
|
||||
|
||||
(根据协议,无论请求还是响应,在 HTTP 的头部结束后,必须增加一个额外的换行回车,因此上述代码最后这个空行是必须的,如果是 POST 请求,那么通常在这个空行之后会有正文)
|
||||
|
||||
你看上面的 User-Agent,我写入了一个[假的浏览器和操作系统版本](https://developers.whatismybrowser.com/useragents/parse/2868-internet-explorer-windows-trident),假装我穿越来自 Window 3.1 的年代,并且用的是 IE 2.0,这样一来,我想不会有人比我更“老”了吧。
|
||||
|
||||
好,接着用类似的方法,使用 netcat 来发送这个请求:
|
||||
|
||||
```
|
||||
netcat www.google.com 80 < ~/Downloads/request.txt
|
||||
|
||||
```
|
||||
|
||||
一样从 Google 收到了成功的报文。
|
||||
|
||||
不知这样的几次动手是否能给你一个启示:懂一点特定的协议,使用简单的命令行和文本编辑工具,我们就已经可以做很多事情了。比如上面这样改变 UA 头的办法,可以模拟不同的浏览器,就是用来分析浏览器适配(指根据不同浏览器的兼容性返回不同的页面数据)的常用方法。
|
||||
|
||||
## HTTP/1.1
|
||||
|
||||
1999 年,著名的 RFC2616,在 1.0 的基础上,大量帮助传输效率提升的特性被加入。
|
||||
|
||||
你可能知道,从网络协议分层上看, TCP 协议在 HTTP 协议的下方(TCP 是在 OSI 7 层协议的第 4 层,而 HTTP 则是在最高的第 7 层应用层,因此,前者更加“底层”一点)。
|
||||
|
||||
在 HTTP 1.0 版本时,每一组请求和响应的交互,都要完成一次 TCP 的连接和关闭操作,这在曾经的互联网资源比较贫瘠的时候并没有暴露出多大的问题,但随着互联网的迅速发展,这种通讯模式显然过于低效了。
|
||||
|
||||
于是这个问题的解决方案——HTTP 的长连接,就自然而然地出现了,它指的是打开一次 TCP 连接,可以被连续几次报文传输重用,这样一来,我们就不需要给每次请求和响应都创建专门的连接了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/f9/2e5a9e7cdc1560967168e96c642517f9.jpg" alt="">(上图来自 [Evolution of HTTP — HTTP/0.9, HTTP/1.0, HTTP/1.1, Keep-Alive, Upgrade, and HTTPS](https://medium.com/platform-engineer/evolution-of-http-69cfe6531ba0))
|
||||
|
||||
可以看到,**通过建立长连接,中间的几次 TCP 连接开始和结束的握手都省掉了。**
|
||||
|
||||
那好,我们还是使用 netcat,这次把版本号改成 1.1,同时打开长连接:
|
||||
|
||||
```
|
||||
GET / HTTP/1.1
|
||||
Host: www.google.com
|
||||
User-Agent: Mozilla/1.22 (compatible; MSIE 2.0; Windows 3.1)
|
||||
Connection: keep-alive
|
||||
Accept: text/html
|
||||
|
||||
|
||||
```
|
||||
|
||||
(别忘了上面那个空行)
|
||||
|
||||
相信你也注意到了上面客户端要求开启长连接的 HTTP 头:
|
||||
|
||||
```
|
||||
Connection: keep-alive
|
||||
|
||||
```
|
||||
|
||||
再按老办法运行:
|
||||
|
||||
```
|
||||
netcat www.google.com 80 < ~/Downloads/request.txt
|
||||
|
||||
```
|
||||
|
||||
我们果然得到了 Google 的响应:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Date: ...
|
||||
Expires: -1
|
||||
Cache-Control: private, max-age=0
|
||||
Content-Type: text/html; charset=ISO-8859-1
|
||||
Transfer-Encoding: chunked
|
||||
...(此处省略多行HTTP 头)
|
||||
|
||||
127a
|
||||
...(此处省略 HTML)
|
||||
0
|
||||
|
||||
|
||||
```
|
||||
|
||||
但是在响应中,值得注意的有两点:
|
||||
|
||||
1. 在 HTTP 头部,有这样一行:
|
||||
|
||||
```
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
```
|
||||
|
||||
1. 正文的内容是这样的:
|
||||
|
||||
```
|
||||
127a
|
||||
...
|
||||
0
|
||||
|
||||
|
||||
```
|
||||
|
||||
同时,之前我们见到过头部的 Content-Length 不见了。这是怎么回事呢?
|
||||
|
||||
事实上,如果协议头中存在上述的 chunked 头,表示将采用分块传输编码,响应的消息将由若干个块分次传输,而不是一次传回。刚才的 127a,指的是接下去这一块的大小,在这些有意义的块传输完毕后,会紧跟上一个长度为 0 的块和一个空行,表示传输结束了,这也是最后的那个 0 的含义。
|
||||
|
||||
值得注意的是,实际上在这个 0 之后,协议还允许放一些额外的信息,这部分会被称作“[Trailer](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer)”,这个额外的信息可以是用来校验正确性的 checksum,可以是数字签名,或者传输完成的状态等等。
|
||||
|
||||
在长连接开启的情况下,使用 Content-Length 还是 chunked 头,必须具备其中一种。**分块传输编码大大地提高了HTTP 交互的灵活性**,服务端可以在还不知道最终将传递多少数据的时候,就可以一块一块将数据传回来。在 [第 03 讲] 中,你还会看到藉由分块传输,可以实现一些模拟服务端推送的技术,比如 [Comet](https://en.wikipedia.org/wiki/Comet_(programming))。
|
||||
|
||||
事实上 HTTP/1.1 还增加了很多其它的特性,比如更全面的方法,以及更全面的返回码,对指定客户端缓存策略的支持,对 content negotiation 的支持(即通过客户端请求的以 Accept 开头的头部来告知服务端它能接受的内容类型),等等。
|
||||
|
||||
## HTTP/2
|
||||
|
||||
现在最广泛使用的 HTTP 协议还是 1.1 ,但是 HTTP/2 已经提出,在保持兼容性的基础上,包含了这样几个重要改进:
|
||||
|
||||
- 设计了一种机制,允许客户端来选择使用的 HTTP 版本,这个机制被命名为 ALPN;
|
||||
- HTTP 头的压缩,在 HTTP/2 以前,HTTP 正文支持多种方式的压缩,但是 HTTP 头部却不能;
|
||||
- 多路复用,允许客户端同时在一个连接中同时传输多组请求响应的方法;
|
||||
- 服务端的 push 机制,比方说客户端去获取一个网页的时候,下载网页,分析网页内容,得知还需要一个 js 文件和一个 css 文件,于是再分别下载,而服务端的 push 机制可以提前就把这些资源推送到客户端,而不需要客户端来索取,从而节约网页加载总时间。
|
||||
|
||||
在 HTTP/2 之后,我们展望未来,HTTP/3 已经箭在弦上。如同前面的版本更新一样,依旧围绕传输效率这个协议核心来做进一步改进,其承载协议将从 TCP 转移到基于 UDP 的 [QUIC](https://en.wikipedia.org/wiki/QUIC) 上面来。
|
||||
|
||||
最后,我想说的是,**HTTP 协议的进化史,恰恰是互联网进化史的一个绝佳缩影**,从中你可以看到互联网发展的数个特质。比方说,长连接和分块传输很大程度上增强了 HTTP 交互模型上的灵活性,使得 B/S 架构下的消息即时推送成为可能。
|
||||
|
||||
## 总结思考
|
||||
|
||||
今天我们了解了 HTTP 协议的进化史,并且用了动手操作的方法来帮助你理解内容,还分析了其中两个重要的特性,长连接和分块传输。希望经过今天的实践,除了知识本身的学习,你还能够**在快速的动手验证中,强化自己的主观认识,并将这种学习知识的方式培养成一种习惯,这是学习全栈技能的一大法宝**。
|
||||
|
||||
现在,让我们来进一步思考这样两个问题:
|
||||
|
||||
- 文中介绍了分块传输的 HTTP 特性,你觉得它可以应用到哪些具体场景?
|
||||
- 如果让你去设计一个新的网络协议,你能否举例设计的过程中需要遵循哪些原则?
|
||||
|
||||
好,今天的分享就到这里,欢迎提出你的疑问,也期待你留言与我交流!
|
||||
|
||||
## 选修课堂:抓一段 HTTP 的包
|
||||
|
||||
如果你对于使用 tcpdump 进行网络抓包这个技能已经了解了,就可以跳过下面的内容。反之,推荐你动动手。因为在学习任何网络协议的时候,网络抓包是一个非常基本的实践前置技能;而在实际定位问题的时候,也时不时需要抓包分析。这也是我在第一讲就放上这堂选修课的原因。
|
||||
|
||||
俗话说,耳听为虚,眼见为实,下面让我们继续动手实践。你当然可以尝试抓访问某个网站的包,但也可以在本机自己启动一个 web 服务,抓一段 HTTP GET 请求的报文。
|
||||
|
||||
利用 Python,在任意目录,一行命令就可以在端口 8080 上启动一个完备的 HTTP 服务(这大概是世界上最简单的启动一个 HTTP 服务的方式了):
|
||||
|
||||
```
|
||||
python -m SimpleHTTPServer 8080
|
||||
|
||||
```
|
||||
|
||||
启动成功后,你应该能看到:
|
||||
|
||||
```
|
||||
Serving HTTP on 0.0.0.0 port 8080 ...
|
||||
|
||||
```
|
||||
|
||||
接着使用 tcpdump 来抓包,注意抓的是 loopback 的包(本地发送到本地),因此执行:
|
||||
|
||||
```
|
||||
sudo tcpdump -i lo0 -v 'port 8080' -w http.cap
|
||||
|
||||
```
|
||||
|
||||
这里的 -i 参数表示指定 interface,而因为客户端和服务端都在本地,因此使用 lo0(我使用的是 Mac,在某些Linux操作系统下可能是 lo,具体可以通过 ifconfig 查看)指定 loopback 的接口,这里我们只想捕获发往 8080 端口的数据包,结果汇总成 http.cap 文件。
|
||||
|
||||
打开浏览器敲入 [http://localhost:8080](http://localhost:8080) 并回车,应该能看到启动 HTTP 服务路径下的文件(夹)列表。这时候你也应该能看到类似下面这样的文字,标志着多少包被捕获,多少包被过滤掉了:
|
||||
|
||||
```
|
||||
24 packets captured
|
||||
232 packets received by filter
|
||||
|
||||
```
|
||||
|
||||
好,现在我们使用 CTRL + C 结束这个抓包过程。
|
||||
|
||||
抓包后使用 [Wireshark](https://www.wireshark.org/) 打开该 http.cap 文件,在 filter 里面输入 http 以过滤掉别的我们不关心的数据包,我们应该能看到请求和响应至少两条数据。于是接下去的内容就是我们非常关心的了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/d8/2bdc949865ae703a08ffd528d44e3ad8.jpeg" alt="">
|
||||
|
||||
如果你看到这里,我想请你再思考下,在不设置上面的 http filter 的时候,我们会看到比这多得多的报文,它们不是 HTTP 的请求响应所以才被过滤掉了,那么,它们都有什么呢?
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
- 【基础】如果你对 HTTP 还不熟悉的话,推荐你阅读一篇系统性介绍 HTTP 的教程,比如 [MDN 的这篇教程](https://developer.mozilla.org/zh-CN/docs/Web/HTTP)。
|
||||
- 【基础】[The OSI model explained: How to understand (and remember) the 7 layer network model](https://www.networkworld.com/article/3239677/the-osi-model-explained-how-to-understand-and-remember-the-7-layer-network-model.html):如果你对网络的 OSI 7 层模型还不清楚的话,建议阅读。如果你想知道那些鼎鼎大名的网络协议在这个模型中的哪个位置,那么请从 [List of network protocols (OSI model)](https://en.wikipedia.org/wiki/List_of_network_protocols_(OSI_model)) 里面找。基于聚焦主题的关系,我们在这个专栏中不会详细介绍呈现层(Presentation Layer)之下的网络协议。
|
||||
- HTTP [1.0](https://tools.ietf.org/html/rfc1945)、[1.1](https://tools.ietf.org/html/rfc2616) 和 [2.0](https://tools.ietf.org/html/rfc7540):它们是 RFC 文档,看起来似乎枯燥乏味,通常我们不需要去仔细阅读它们,但是当我们想知道对协议的理解是否正确,它们是我们最终的参考依据。
|
||||
- [Key differences between HTTP 1.0 and HTTP 1.1](http://www.ra.ethz.ch/cdstore/www8/data/2136/pdf/pd1.pdf):文中总结了从 HTTP 1.0 到 1.1 的 9 大改进;而 [HTTP/2 Complete Tutorial](http://qnimate.com/post-series/http2-complete-tutorial/) 是一篇比较系统的 HTTP/2 的介绍。
|
||||
|
||||
|
||||
183
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/02 | 为HTTP穿上盔甲:HTTPS.md
Normal file
183
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/02 | 为HTTP穿上盔甲:HTTPS.md
Normal file
@@ -0,0 +1,183 @@
|
||||
<audio id="audio" title="02 | 为HTTP穿上盔甲:HTTPS" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fa/5c/fadbe6e6eda3ae2baec666ce96f0b45c.mp3"></audio>
|
||||
|
||||
你好,我是四火。
|
||||
|
||||
在上一讲中,我介绍了互联网最重要的 HTTP 协议。可是随着互联网的发展,你会发现 HTTP 越来越无法满足复杂的需求,比如数据加密传输的安全性需求,再比如服务器消息即时推送的交互模式的需求,而这些不适性是由 HTTP 的基本特性所造成的。
|
||||
|
||||
因此,我们需要在传统 HTTP 领域以外开疆拓土,这就包括要引入其它的网络协议,或增强、或填补 HTTP 协议所不擅长的空白领域,这也是今天这一讲和下一讲的核心内容。今天我们重点学习 SSL/TLS ,看看它是如何让 HTTP 传输变得安全可靠的。
|
||||
|
||||
## HTTP,SSL/TLS 和 HTTPS
|
||||
|
||||
在一开始的时候,HTTP 的设计者并没有把专门的加密安全传输放进协议设计里面。因此单独使用 HTTP 进行明文的数据传输,一定存在着许多的安全问题。比方说,现在有一份数据需要使用 HTTP 协议从客户端 A 发送到服务端 B,而第三方 C 尝试来做点坏事,于是就可能产生如下四大类安全问题:
|
||||
|
||||
- Interception:拦截。传输的消息可以被中间人 C 截获,并泄露数据。
|
||||
- Spoofing:伪装。A 和 B 都可能被 C 冒名顶替,因此消息传输变成了 C 发送到 B,或者 A 发送到 C。
|
||||
- Falsification:篡改。C 改写了传输的消息,因此 B 收到了一条被篡改的消息而不知情。
|
||||
- Repudiation:否认。这一类没有 C 什么事,而是由于 A 或 B 不安好心。A 把消息成功发送了,但之后 A 否认这件事发生过;或者 B 其实收到了消息,但是否认他收到过。
|
||||
|
||||
但是,与其重新设计一套安全传输方案,倒不如发挥一点拿来主义的精神,把已有的和成熟的安全协议直接拿过来套用,最好它位于呈现层(Presentation Layer),因此正好加塞在 HTTP 所在的应用层(Application Layer)下面,**这样这个过程对于 HTTP 本身透明,也不影响原本 HTTP 以下的协议(例如 TCP)**。
|
||||
|
||||
这个协议就是 SSL/TLS,它使得上面四大问题中,和传输本身密切相关的前三大问题都可以得到解决(第四个问题还需要引入数字签名来解决)。于是,HTTP 摇身一变成了 HTTPS:
|
||||
|
||||
>
|
||||
HTTP + SSL/TLS = HTTPS
|
||||
|
||||
|
||||
这里涉及到的两个安全协议,SSL 和 TLS,下面简要说明下二者关系。
|
||||
|
||||
SSL 指的是 Secure Socket Layer,而 TLS 指的是 Transport Layer Security,事实上,一开始只有 SSL,但是在 3.0 版本之后,SSL 被标准化并通过 [RFC 2246](https://tools.ietf.org/html/rfc2246) 以 SSL 为基础建立了 TLS 的第一个版本,因此可以简单地认为 SSL 和 TLS 是具备父子衍生关系的同一类安全协议。
|
||||
|
||||
## 动手捕获 TLS 报文
|
||||
|
||||
介绍了最基本的概念,我们再来看看 HTTPS 是怎样安全工作,让客户端和服务端相互信任的, TLS 连接又是怎样建立起来的。还记得上一讲的选修课堂吗?我们学了怎样抓包。今天我们就能让所学派上用场!自己动手,我们抓 TLS 连接握手的报文来分析。
|
||||
|
||||
命令行执行抓包命令,指明要抓 [https://www.google.com](https://www.google.com) 的包(当然,你也可以使用其他 HTTPS 网站地址),注意 HTTPS 的默认端口是 443(-i 指定的 interface 可能因为不同的操作系统有所区别,在我的 Mac 上是 en0):
|
||||
|
||||
```
|
||||
sudo tcpdump -i en0 -v 'host www.google.com and port 443' -w https.cap
|
||||
|
||||
```
|
||||
|
||||
再新建一个命令行窗口,使用 curl 命令来访问 Google 主页:
|
||||
|
||||
```
|
||||
curl https://www.google.com
|
||||
|
||||
```
|
||||
|
||||
于是在看到类似如下抓包后 CTRL + C 停止:
|
||||
|
||||
```
|
||||
tcpdump: listening on en0, link-type EN10MB (Ethernet), capture size 262144 bytes
|
||||
^C49 packets captured
|
||||
719 packets received by filter
|
||||
0 packets dropped by kernel
|
||||
|
||||
```
|
||||
|
||||
接着使用 Wireshark 打开刚才抓的 https.cap,在 filter 中输入 tls,得到如下请求和响应报文:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6a/3b/6a0269e1c2cdc7c768fc11c8bcc20e3b.jpg" alt="">
|
||||
|
||||
可以看到,这里有五个重要的握手消息,在它们之后的所有消息都是用于承载实际数据的“Application Data”了。握手的过程略复杂,接下来我会尽可能用通俗的语言把最主要的流程讲清楚。
|
||||
|
||||
## 对称性和非对称性加密
|
||||
|
||||
这里我先介绍两个概念,“对称性加密”和“非对称性加密”,这是学习后面内容的重要基础。
|
||||
|
||||
对称性加密(Symmetric Cryptography),指的是加密和解密使用相同的密钥。这种方式相对简单,加密解密速度快,但是由于加密和解密需要使用相同的密钥,如何安全地传递密钥,往往成为一个难题。
|
||||
|
||||
非对称性加密(Asymmetric Cryptography),指的是数据加密和解密需要使用不同的密钥。通常一个被称为公钥(Public Key),另一个被称为私钥(Private Key),二者一般同时生成,但是**公钥往往可以公开和传播,而私钥不能。经过公钥加密的数据,需要用私钥才能解密**;反之亦然。这种方法较为复杂,且性能较差,好处就是由于加密和解密的密钥具有相对独立性,公钥可以放心地传播出去,不用担心安全性问题。
|
||||
|
||||
>
|
||||
<p>原始数据 + 公钥 → 加密数据<br>
|
||||
加密数据 + 私钥 → 原始数据</p>
|
||||
|
||||
|
||||
## TLS 连接建立原理
|
||||
|
||||
有了上述基础,下面我们就可以结合图示,看看整个连接建立的握手过程了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/94/63/941574c15cea2cc2d66c66ab492fea63.jpeg" alt="">
|
||||
|
||||
**Step 1: Client Hello.** 客户端很有礼貌,先向服务端打了个招呼,并携带以下信息:
|
||||
|
||||
- 客户端产生的随机数 A;
|
||||
- 客户端支持的加密方法列表。
|
||||
|
||||
**Step 2: Server Hello.** 服务端也很有礼貌,向客户端回了个招呼:
|
||||
|
||||
- 服务端产生的随机数 B;
|
||||
- 服务端根据客户端的支持情况确定出的加密方法组合(Cipher Suite)。
|
||||
|
||||
**Step 3: Certificate, Server Key Exchange, Server Hello Done.** 服务端在招呼之后也紧跟着告知:
|
||||
|
||||
- Certificate,证书信息,证书包含了服务端生成的公钥。这个公钥有什么用呢?别急,后面会说到。
|
||||
|
||||
客户端收到消息后,验证确认证书真实有效,那么这个证书里面的公钥也就是可信的了。
|
||||
|
||||
接着客户端再生成一个随机数 C(Pre-master Secret),于是现在共有随机数 A、B 和 C,根据约好的加密方法组合,三者可生成新的密钥 X(Master Secret),而由 X 可继续生成真正用于后续数据进行加密和解密的对称密钥。因为它是在本次 TLS 会话中生成的,所以也被称为会话密钥(Session Secret)。简言之:
|
||||
|
||||
>
|
||||
客户端随机数 A + 服务端随机数 B + 客户端 Pre-master Secret C → 会话密钥
|
||||
|
||||
|
||||
需要注意的是,实际这个 Pre-master Secret 的生成方法不是固定的,而会根据加密的具体算法不同而不同:
|
||||
|
||||
- 上述我介绍的是传统 RSA 方式,即 Pre-master Secret 由客户端独立生成,加密后再通过 Client Key Exchange 发回服务端。
|
||||
- 还有一种是 [ECDHE](https://zh.wikipedia.org/wiki/%E6%A9%A2%E5%9C%93%E6%9B%B2%E7%B7%9A%E8%BF%AA%E8%8F%B2-%E8%B5%AB%E7%88%BE%E6%9B%BC%E9%87%91%E9%91%B0%E4%BA%A4%E6%8F%9B) 方式,这种方式下无论在客户端还是服务端,Pre-master Secret 需要通过 Client Key Exchange 和 Server Key Exchange 两者承载的参数联合生成。
|
||||
|
||||
**Step 4: Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message.** 接着客户端告诉服务端:
|
||||
|
||||
- Client Key Exchange,本质上它就是上面说的这个 C,但使用了服务端通过证书发来的公钥加密;
|
||||
- Change Cipher Spec,客户端同意正式启用约好的加密方法和密钥了,后面的数据传输全部都使用密钥 X 来加密;
|
||||
- Encrypted Handshake Message,快速验证:这是客户端对于整个对话进行摘要并加密得到的串,如果经过服务端解密,和原串相等,就证明整个握手过程是成功的。
|
||||
|
||||
服务端收到消息后,用自己私钥解密上面的 Client Key Exchange,得到了 C,这样它和客户端一样,也得到了 A、B 和 C,继而到 X,以及最终的会话密钥。
|
||||
|
||||
于是,客户端和服务端都得到了能够加密解密传输数据的对称密钥——会话密钥。
|
||||
|
||||
因此,我们可以看到:**TLS是通过非对称加密技术来保证握手过程中的可靠性(公钥加密,私钥解密),再通过对称加密技术来保证数据传输过程中的可靠性的**。
|
||||
|
||||
这种通过较严格、较复杂的方式建立起消息交换渠道,再通过相对简单且性能更高的方式来实际完成主体的数据传输,并且前者具有长效性(即公钥和私钥相对稳定),后者具有一过性(密钥是临时生成的),这样的模式,我们还将在全栈的知识体系中,继续见到。
|
||||
|
||||
**Step 5: Change Cipher Spec, Encrypted Handshake Message.** 服务端最后也告知客户端:
|
||||
|
||||
- Change Cipher Spec,服务端也同意要正式启用约好的加密方法和密钥,后面的数据传输全部都使用 X 来加密。
|
||||
- Encrypted Handshake Message,快速验证:这是服务端对于整个对话进行摘要并加密得到的串,如果经过客户端解密,和原串相等,就证明整个握手过程是成功的。
|
||||
|
||||
## 总结思考
|
||||
|
||||
今天我们了解了关于数据传输的四大类安全问题,了解了 HTTPS 和 SSL/TLS 的概念和它们之间的关系,还通过自己动手抓包的方式,详细学习了 TLS 连接建立的步骤。
|
||||
|
||||
TLS 连接的步骤是今天的重点,也是比较难理解的部分,希望你能牢牢地掌握它。现在就来检验一下今天的学习成果吧!请你思考这样两个问题:
|
||||
|
||||
- 有位程序员朋友注意到,自己在使用在线支付功能时,网站访问是使用 HTTPS 加密的,因此他觉得,支付的过程中是不可能出现安全问题的,你觉得这种想法对吗?
|
||||
- 在介绍 TLS/SSL 连接建立的过程当中,我提到了,握手过程是使用非对称加密实现的,而真正后续的数据传输部分却是由对称加密实现的。为什么要这么麻烦,全部都使用对称或非对称加密一种不好吗?
|
||||
|
||||
你能回答上面的问题吗?如果可以,我相信你已经理解了 HTTPS 安全机制建立的原理。
|
||||
|
||||
## 选修课堂:证书有效验证的原理
|
||||
|
||||
在讲解“握手过程”的 step 3 时,我提到了客户端在收到服务端发送过来的证书时,需要校验证书的有效性。这个过程其实也是至关重要的,因为只有确认了证书的有效性,客户端才能放心地使用其中的公钥。如果你对它的理解比较模糊,那就一定要看看今天的选修课堂了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/a4/2e5095cb129d58b5bfa325c5e0d9b9a4.jpg" alt="">
|
||||
|
||||
这就是我们抓包中,服务器发来的证书部分的截图。我们可以看到,这不是单个证书,而是一个证书链,包含了两个证书,每个证书都包含版本、发布机构、有效期、数字签名等基本内容,以及一个公钥。实际上,这两个服务端传回来的证书,和浏览器内置的根证书联合起来,组成了一个单向、完整的证书链:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1f/29/1f8d1f510e83084b2161d8e07b43b629.jpeg" alt="">
|
||||
|
||||
上图中的第三行,就是携带着服务器公钥的证书,它是从证书发布机构(CA, Certificate Authority)申请得来的,也就是图中第二行的 GTS CA 1O1。证书在申请的时候,我们提到的服务器公钥就已经是该证书的一部分了,因此我们才说,如果证书是有效的,那么它携带的公钥就是有效的。
|
||||
|
||||
在当时申请的时候,**证书发布机构对证书做摘要生成指纹,并使用它自己的私钥为该指纹加密,生成数字签名(Digital Signature),而这个数字签名也随证书一起发布**。这个发布机构的私钥是它内部自己管理的,不会外泄。
|
||||
|
||||
>
|
||||
指纹 + 私钥 → 数字签名
|
||||
|
||||
|
||||
验证过程则正好是发布过程的反向,即在客户端要对这个被检测证书做两件事:
|
||||
|
||||
- 对它用指定算法进行摘要,得到指纹 P1;
|
||||
- 使用证书发布机构的公钥对它的数字签名进行解密,得到指纹 P2。
|
||||
|
||||
>
|
||||
数字签名 + 公钥 → 指纹
|
||||
|
||||
|
||||
如果 P1 和 P2 一致,就说明证书未被篡改过,也说明这个服务端发来的证书是真实有效的,而不是仿冒的。
|
||||
|
||||
好,问题来了,证书发布机构使用非对称性加密和数字签名保证了证书的有效性,那么谁来保证证书发布机构的有效性?
|
||||
|
||||
答案就是它的上一级证书发布机构。
|
||||
|
||||
CA 是分级管理的,每一级 CA 都根据上述同样的原理,由它的上一级 CA 来加密证书和生成数字签名,来保证其真实性,从而形成一个单向的信任链。同时,标志着最高级别 CA 的根证书数量非常少,且一般在浏览器或操作系统安装的时候就被预置在里面了,因此它们是被我们完全信任的,这就使得真实性的鉴别递归有了最终出口。也就是说,递归自下而上验证的过程,如果一直正确,直至抵达了顶端——浏览器内置的根证书,就说明服务端送过来的证书是安全有效的。
|
||||
|
||||
总结一下今天选修课堂的内容。证书有效性的验证,需要使用依赖于证书发布机构的公钥去解密被检测证书的数字签名,如果顺利解密,并且得到的指纹和被检测证书做摘要得到的指纹一致,就说明证书真实有效。
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
- [HOW HTTPS WORKS](https://howhttps.works/):漫画版介绍 HTTPS 前前后后,很有趣。
|
||||
- [The First Few Milliseconds of an HTTPS Connection](http://www.moserware.com/2009/06/first-few-milliseconds-of-https.html):如果你想深究你抓到的 TLS 连接建立的包中每一段报文的意思,这篇文章是一个很好的参考。
|
||||
- 文中介绍了两种生成 Pre-master Secret 的方法,其中第二种的方法是 Diffie–Hellman 密钥交换的变种,这里蕴含的数学原理很有意思,如果你感兴趣,请参阅[维基百科链接](https://zh.wikipedia.org/wiki/%E8%BF%AA%E8%8F%B2-%E8%B5%AB%E7%88%BE%E6%9B%BC%E5%AF%86%E9%91%B0%E4%BA%A4%E6%8F%9B#%E6%8F%8F%E8%BF%B0)。
|
||||
|
||||
|
||||
150
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/03 | 换个角度解决问题:服务端推送技术.md
Normal file
150
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/03 | 换个角度解决问题:服务端推送技术.md
Normal file
@@ -0,0 +1,150 @@
|
||||
<audio id="audio" title="03 | 换个角度解决问题:服务端推送技术" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/56/a8/565b7275a10af1c204a717aa03814aa8.mp3"></audio>
|
||||
|
||||
你好,我是四火。
|
||||
|
||||
今天我们继续和 HTTP“过不去”。在上一讲,我们讲到了 HTTP 在安全传输方面的局限,并介绍了怎样使用经过 TLS 加密的 HTTPS 连接来解决这样的弊端。
|
||||
|
||||
今天,我要给你讲讲传统 HTTP的另一个在交互模式上的局限,就是只能由客户端主动发起消息传递,而服务端只能被动响应消息的局限,并介绍它的解决办法。
|
||||
|
||||
## Pull 模型的问题
|
||||
|
||||
让我们来思考这样一个场景,假设你设计了一款网页版的即时聊天工具,现在你使用浏览器打开了聊天页面,正在和朋友愉快地聊天。这时有朋友给你发送了一条消息,可是由于 HTTP 本身机制的限制,服务端无法主动推送消息,告知浏览器上的聊天页面“你有一条消息”,进而影响到了消息的即时送达。那么,这个问题怎么解决?
|
||||
|
||||
你可能会立即想到**轮询(Poll)**,比如浏览器每隔十秒钟去问一下服务端是不是有新消息不就完了嘛。这看起来是个好思路,但明显存在这样两个问题:
|
||||
|
||||
- 消息还是不够即时。换言之,假如正好在某次询问之后服务器收到了消息,那么这条消息的获取延迟可能达到至少十秒。
|
||||
- 大量的请求-响应,带宽和服务器资源浪费。如果你开着聊天工具页面一个小时,除了这一条消息,却没有进一步的聊天行为,于是按照每十秒发送一次请求计算,一共发起了 360 次请求,而其中居然只有 1 次返回了聊天消息是有实际意义的。
|
||||
|
||||
显然,轮询这个方案不好。说到底,其实我们并没有抛开对 HTTP 的已有印象,从问题本身出发去思考解决问题的最佳方式,而是潜意识地受限于 HTTP 的传统交互模式,考虑其中的变通方法。
|
||||
|
||||
在进一步分析之前,我们先来看两个容易弄混的概念:Pull 和 Poll。
|
||||
|
||||
“Pull”指的是去主动发起行为获取消息,一般在客户端/服务器(C/S,Client/Server)或浏览器/服务器(B/S,Browser/Server)交互中,客户端或浏览器主动发起的网络请求数据的行为。
|
||||
|
||||
而“Poll”,尽管在某些场景下也和 Pull 通用了,但在计算机网络的领域里,通常把它解释为“轮询”,或者“周期性查询”,在 Pull 的基础上增加了“周期性”的概念,这也是它和 Pull 相比最本质的区别。
|
||||
|
||||
相应地,和 Pull 行为相对的,从服务端主动发起,发送数据到客户端的行为叫做“Push”。Push 相比 Pull 而言,具备这样两个明显的优势:
|
||||
|
||||
- **高效性**。如果没有更新发生,就不会有任何更新消息推送的动作,即每次消息推送都发生在确确实实的更新事件之后,都是有意义的,不会出现请求和响应的资源浪费。
|
||||
- **实时性**。事件发生后的第一时间即可触发通知操作,理论上不存在任何可能导致通知延迟的硬伤。
|
||||
|
||||
可是,有趣的是,事实上 Pull 的应用却远比 Push 更广泛,特别是在分布式系统中。这里有多个原因,其中很重要的一条是:
|
||||
|
||||
服务端不需要维护客户端的列表,不需要知晓客户端的情况,不需要了解客户端查询的策略。**这有助于把服务端从对客户端繁重的管理工作中解放出来,而成为无状态的简单服务,变得具备幂等性(idempotent,指执行多次和执行一次的结果一样),更容易横向扩展。**
|
||||
|
||||
尤其在分布式系统中,状态经常成为毒药,有了状态,就不得不考虑状态的保存、丢失、一致性等问题,因此这种无状态往往可以很大程度地简化系统的设计。
|
||||
|
||||
## 服务端推送技术
|
||||
|
||||
有了这些基础知识,我们就可以来谈谈实际的服务端推送技术了,这些都从一定程度上解决了 HTTP 传统方式 Pull 的弊端。
|
||||
|
||||
### 1. Comet
|
||||
|
||||
严格说,Comet 是一种 Web 应用客户端和服务端交互的模型,它有几种服务端推送的具体实现,但是,它们的大致原理是一样的:**客户端发送一个普通的 HTTP 请求到服务端以后,服务端不像以往一样在处理后立即返回数据,而是保持住连接不释放,每当有更新事件发生,就使用分块传输的方式返回数据**(如果你忘记了块传输的方式,请回看 [[第1讲]](https://time.geekbang.org/column/article/134752))。
|
||||
|
||||
若干次数据返回以后可以完成此次请求响应过程(分块传输返回长度为0的块,表示传输结束),等待客户端下一次请求发送。这种过程看起来也属于轮询,但是每个周期可包含多次服务端数据返回,因而也被形象地称为“长轮询”(Long Polling)。
|
||||
|
||||
在服务端推送技术中,Comet 最大的好处是,它 100% 由 HTTP 协议实现,当然,分块传输要求 HTTP 至少是 1.1 版本。但也正因为这点,它也存在一些弊端,比如,客户端必须在服务端结束当次传输后才能向服务端发送消息;HTTP 协议限制了它在每次请求和响应中必须携带完整的头部,这在一定程度上也造成了浪费(这种为了传输实际数据而使用的额外开销叫做 overhead)。
|
||||
|
||||
下面我给出了一个 Comet 实现的示例图。浏览器在发出 1 号请求要求数据,连接保持,接着陆续收到几个不同大小的响应数据,并且最后一个大小为0,浏览器被告知此次传输完成。过了一会儿,浏览器又发出 2 号请求,开始第二轮的类似交互。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f1/9d/f148cfbe65a6a0e98214095c46e0169d.png" alt="">
|
||||
|
||||
在 Comet 方式下,**看起来服务端有了推送行为,其实只是对于客户端请求有条件、讲时机的多次返回**,因此我们把它称为服务端“假 Push”。
|
||||
|
||||
### 2. WebSocket
|
||||
|
||||
HTML 5 规范定义了 WebSocket 协议,它可以通过 HTTP 的端口(或者 HTTPS 的端口)来完成,从而最大程度上对 HTTP 协议通透的防火墙保持友好。但是,**它是真正的双向、全双工协议,也就是说,客户端和服务端都可以主动发起请求,回复响应,而且两边的传输都互相独立。**
|
||||
|
||||
和上文的 Comet 不同,WebSocket 的服务端推送是完全可以由服务端独立、主动发起的,因此它是服务端的“真 Push”。
|
||||
|
||||
WebSocket 是一个可谓“科班出身”的二进制协议,也没有那么大的头部开销,因此它的传输效率更高。同时,和 HTTP 不一样的是,它是一个带有状态的协议,双方可以约定好一些状态,而不用在传输的过程中带来带去。而且,WebSocket 相比于 HTTP,它没有同源的限制,服务端的地址可以完全和源页面地址无关,即不会出现臭名昭著的浏览器“跨域问题”。
|
||||
|
||||
另外,它和我们之前学习的加密传输也丝毫不冲突,由于它在网络分层模型中位于 TLS 上方,因此他可以使用和 HTTP 一样的加密方式传输:
|
||||
|
||||
>
|
||||
<p>HTTP → WS<br>
|
||||
HTTPS → WSS</p>
|
||||
|
||||
|
||||
最后,最有意思的事情在于,和我们之前的认识不同,WebSocket 是使用 HTTP 协议“升级”的方法来帮助建立连接的,下面我们动手来试一试。
|
||||
|
||||
首先,我们需要找到一个可以支持 WebSocket 测试的网站,比如 websocket.org,然后我们将使用 Chrome 的网络工具来捕获和显示通过浏览器发送和接收的消息。如果这是你第一次使用 Chrome 的开发者工具,那么你需要好好熟悉它了,因为它将在你今后全栈的道路上派上大用场。
|
||||
|
||||
使用 Chrome 打开 [Echo Test](https://www.websocket.org/echo.html) 页面,在这里你可以发送建立一个 WebSocket 连接。但是别急,我们先打开 Chrome 的开发者工具,并选中 Network 标签,接着点击左上角的清除按钮,把已有页面加载的网络消息清除掉,以获得一个清爽的网络报文监视界面:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/ae/ea65ac03047615ba8aafe1d7118d5dae.png" alt="">
|
||||
|
||||
接着,确保页面上建立 WebSocket 连接的对端地址和传递的信息都已经填写,比如:
|
||||
|
||||
```
|
||||
Location:
|
||||
wss://echo.websocket.org
|
||||
Message:
|
||||
Rock it with HTML5 WebSocket
|
||||
|
||||
```
|
||||
|
||||
于是就可以点击“Connect”按钮了,旁边的日志框将出现“CONNECTED”字样,同时,Chrome 开发者工具将捕获这样的请求(如果在开发者工具中网络监视界面上,选中消息的消息头处于“parsed”展示模式,你需要点击 Request Headers 右侧的 “view source” 链接来查看原始消息头):
|
||||
|
||||
```
|
||||
GET wss://echo.websocket.org/?encoding=text HTTP/1.1
|
||||
Host: echo.websocket.org
|
||||
Origin: https://www.websocket.org
|
||||
Connection: Upgrade
|
||||
Upgrade: websocket
|
||||
Sec-WebSocket-Version: 13
|
||||
Sec-WebSocket-Key: xxx
|
||||
... (省略其它 HTTP 头)
|
||||
|
||||
```
|
||||
|
||||
好,你可以看到,这是一个普普通通的 HTTP GET 请求,但是 URL 是以加密连接“wss”开头的,并且有几个特殊的 HTTP 头:Origin 指出了请求是从哪个页面发起的,Connection: Upgrade 和 Upgrade: websocket 这两个表示客户端要求升级 HTTP 协议为 WebSocket。
|
||||
|
||||
好,再来看响应,消息的头部为:
|
||||
|
||||
```
|
||||
HTTP/1.1 101 Web Socket Protocol Handshake
|
||||
Connection: Upgrade
|
||||
Sec-WebSocket-Accept: xxx
|
||||
Upgrade: websocket
|
||||
... (省略其它 HTTP 头)
|
||||
|
||||
```
|
||||
|
||||
嗯,返回码是 101,描述是“Web Socket Protocol Handshake”,并且,它确认了连接升级为“websocket”的事实。
|
||||
|
||||
### 3. 更多推送技术
|
||||
|
||||
到这里,我已经介绍了几种服务端的推送技术,事实上还有更多,但是,**如果你依次了解以后认真思考,就会发现,这些原理居然都在某种程度上和我介绍的 Comet 和 WebSocket 这两种类似,有的甚至来自于它们。**
|
||||
|
||||
这些技术包括:
|
||||
|
||||
- SSE,即 Server-Sent Events,又叫 EventSource,是一种已被写入 HTML 5 标准的服务端事件推送技术,它允许客户端和服务端之间建立一个单向通道,以让服务端向客户端单方向持续推送事件消息;
|
||||
- 为了提高性能,HTTP/2 规范中新添加的服务端推送机制,我们在 [[第 01 讲]](https://time.geekbang.org/column/article/134752) 中提到过,并在该讲的扩展阅读中有它的原理介绍;
|
||||
- WebRTC,即 Web Real-Time Communication,它是一个支持网页进行视频、语音通信的协议标准,不久前已被加入 W3C 标准,最新的 Chrome 和 Firefox 等主流浏览器都支持;
|
||||
- 还有一些利用浏览器插件和扩展达成的服务端推送技术,比如使用 Flash 的 XMLSocket,比如使用 Java 的 Applet,但这些随着 HTML 5 的普及,正慢慢被淘汰。
|
||||
|
||||
你看,通过学习一两个典型的技术,再拓展开,去类比了解和分析思考同一领域内的其它技术,就能掌握到最核心的东西,这就是我推荐的一种学习全栈技术的方式。
|
||||
|
||||
## 总结思考
|
||||
|
||||
今天我们从 HTTP 的交互局限性引出了网络交互中 Pull 和 Push 的两大模型,比较了它们的优劣。服务端 Push 的方式具备高效性和实时性的优势,而客户端 Pull 的方式令服务端免去状态的维护,从根本上简化了系统。
|
||||
|
||||
之后我们以 Comet 和 WebSocket 为重点,介绍了服务端推送的不同方式,尤其是用了实际抓包分析,介绍了通过 HTTP “升级”的方式来建立 WebSocket 连接的原理。
|
||||
|
||||
今天学习得怎样呢?来看这样两个问题:
|
||||
|
||||
- 文中介绍了 Push 和 Pull 在原理上的不同,在你的实际项目中,是否应用了 Push 或 Pull 的模型呢?
|
||||
- 文中介绍了 Push 比 Pull 具备高效性和实时性的优势,而 Pull 比 Push 则具备使得服务变得无状态的优势,除了最重要的这几个,你还能说出更多它们各自的优势吗?
|
||||
|
||||
今天的内容就到这里。以 HTTP 协议为核心,介绍网络协议的三讲文章已经更新完毕了,你是否对于全栈技术本身,还有适合自己的学习方法,有了新的理解呢?欢迎留言和我讨论。
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
- 文中提到了跨域问题,如果感兴趣,推荐你阅读 MDN 的 [HTTP访问控制(CORS)](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS)这篇文章。
|
||||
- TutorialsPoint 的 [WebSocket 系统教程](https://www.tutorialspoint.com/websockets/),对于本文介绍的 WebSocket 协议,需要进一步了解的一个好去处。
|
||||
- 关于 HTTP Update 头的 [RFC 2616 协议片段](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.42)和 WebSocket 的 [RFC 6445](https://tools.ietf.org/html/rfc6455),你也许对响应和请求中的其它 HTTP 头心存疑问,和之前介绍的 HTTP 的 RFC 协议一样,你通常不需要仔细阅读,但它是对协议有问题时的最终去处。
|
||||
- [Stream Updates with Server-Sent Events](https://www.html5rocks.com/en/tutorials/eventsource/basics/),一篇非常好的介绍 SSE 基础,和同类技术比较优劣,并给出代码示例的文章;如果你对 WebRTC 感兴趣,那么可以先看看这个[胶片](http://io13webrtc.appspot.com/#1),再阅读这篇基础知识 [Getting Started with WebRTC](https://www.html5rocks.com/en/tutorials/webrtc/basics/)。
|
||||
|
||||
|
||||
217
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/04 | 工整与自由的风格之争:SOAP和REST.md
Normal file
217
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/04 | 工整与自由的风格之争:SOAP和REST.md
Normal file
@@ -0,0 +1,217 @@
|
||||
<audio id="audio" title="04 | 工整与自由的风格之争:SOAP和REST" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9b/b6/9bbaaa35ff03c02e6a6898bd2b6e90b6.mp3"></audio>
|
||||
|
||||
你好,我是四火。
|
||||
|
||||
今天我要邀请两位风格迥异的主角登上舞台,一位西装革履,另一位随性洒脱。前面那位,代表着工整、严谨和细致;后面那位,代表着自由、灵活和简约。
|
||||
|
||||
它们来自两个不同的时代,却同时活跃于当今的互联网,并担当着重量级的角色,影响了一批新技术的诞生。今天,就让我们来认识下它们,它们的名字,分别叫做 SOAP 和 REST。
|
||||
|
||||
## 概念
|
||||
|
||||
SOAP,Simple Object Access Protocol,即简单对象访问协议,定义了数据对象传输的格式,以便在网络的节点之间交换信息。你可能会问,HTTP 不就是干这事的吗?其实,它们都在 OSI 7 层模型的应用层上,但却互不冲突,SOAP 并不依赖于 HTTP 而存在,而且它们可以互相配合。
|
||||
|
||||
HTTP 负责信息的传输,比如传递文本数据,关心的是消息的送达,但却不关心这个数据代表着什么。这个数据可能本来是一个内存里的对象,是一篇文章,或者是一张图片。但是 SOAP 恰恰相反,它关心的就是怎样把这个数据给序列化成 XML 格式的文本,在传输到对端以后,再还原回来。
|
||||
|
||||
用一个形象的比喻就是,**消息传输就像快递,HTTP 主要关心的是信封,而 SOAP 主要关心的是信封里的物件。**今天我们讨论的 SOAP,不仅仅是协议本身,更主要的是它的风格。
|
||||
|
||||
REST,Representational State Transfer,即表现层状态转换,指的是一种为了信息能在互联网上顺利传递而设计的软件架构风格。对,请注意,**SOAP 是协议,但 REST 是风格,而非协议或标准**,至于 HTTP,它是 REST 风格的重要载体。重要,但不是唯一,因为载体并不只有 HTTP 一个,比如还有 HTML 和 XML,它们恰当地互相配合,组合在一起,来贯彻和体现 REST 的风格。
|
||||
|
||||
SOAP 和 REST,由于概念层次上的不同,其实原本是无法放到一起比较的,但是当我们旨在风格层面上讨论 SOAP 和 REST 的时候,这件事却变得可行而有趣了。
|
||||
|
||||
现在让我们用一个实际例子来进一步认识它们。这个例子很常见,假设我们要设计一个图书馆,馆中的书可以增删改查(CRUD),特别是要添加一本书的时候,我们分别来看看,应用 SOAP 该怎么做,应用 REST 又该怎么做。
|
||||
|
||||
## SOAP
|
||||
|
||||
这是一个最简单的给图书馆添加一本书的 XML 消息体:
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope
|
||||
xmlns:soap="https://www.w3.org/2003/05/soap-envelope/"
|
||||
soap:encodingStyle="https://www.w3.org/2003/05/soap-encoding">
|
||||
<soap:Body xmlns:b="...">
|
||||
<b:CreateBook>
|
||||
<b:Name>...</m:Name>
|
||||
<b:Author>...</m:Author>
|
||||
...
|
||||
</b:CreateBook>
|
||||
</soap:Body>
|
||||
</soap:Envelope>
|
||||
|
||||
```
|
||||
|
||||
让我来简单解释一下:
|
||||
|
||||
1.第一行指明了这个消息本身格式是 XML,包括它的版本号和编码格式。
|
||||
|
||||
2.这里的很多标签都带有“soap:”的前缀,其实,这里的“soap”就是 XML 的命名空间(其中“xmlns”就是指“xml namespace”),并且通过 XML schema 的方式预先定义好了如下两个 SOAP 消息必须要遵从的规则:
|
||||
|
||||
<li>
|
||||
一个是代码片段第 3 行的 [soap-envelope](https://www.w3.org/2003/05/soap-envelope/),它定义了基本的语法规则,比如标签的从属关系,比如同级某标签的个数限制等等,举例来说,你可以看到例子中有一个 Body 标签位于 Envelope 内部,这就是它定义的;
|
||||
</li>
|
||||
<li>
|
||||
另一个是代码片段第 4 行的 [soap-encoding](https://www.w3.org/2003/05/soap-encoding),它定义了编码和数据类型等规则。
|
||||
</li>
|
||||
|
||||
3.在 Body 标签内部,有一个 CreateBook 标签,这是我们的业务标签,命名空间 b 也是我们自己定义的。通过在内部封装姓名(Name)和作者(Author)等书本信息,实现了在图书馆中添加书本的需求。
|
||||
|
||||
上面是一个最简单的例子,实际在 Envelope 中还可以添加 Head 标签,用于存放头部信息,在 Body 中可以添加 Fault 标签,用于存放错误信息。关于这些,都在 XML schema 中做了严格的定义,通过它可以帮助验证一个 XML 是否符合格式,从而可以在最短的时间内验证并发现消息中的格式问题。
|
||||
|
||||
SOAP 通常是通过 HTTP POST 的方式发送到对端的,这个图书馆对书本的增删改查操作,URL 可以是同一个,这是因为 SOAP 消息的具体内容写明了具体要干什么(上述的 CreateBook 标签)。比如下面这个例子,请注意其中的 Content-Type,它是令响应具备自我描述特性的重要组成部分:
|
||||
|
||||
```
|
||||
POST /books HTTP/1.1
|
||||
Host: xxx
|
||||
Content-Type: application/soap+xml; charset=utf-8
|
||||
Content-Length: xxx
|
||||
|
||||
... (省略前述的 SOAP 消息体)
|
||||
|
||||
```
|
||||
|
||||
最后,谈谈经常和 SOAP 放在一起谈论的 WSDL,Web Service Description Language。
|
||||
|
||||
WSDL 用于描述一个 Web Service,说白了,就是用来说明某个 Web 服务该怎样使用,有怎样的接口方法,支持怎样的参数,会有怎样的返回。由于支持 SOAP 的服务端接口是经常使用 WSDL 来描述,因此我们才看到它们总被放在一起讨论,于是在这种情况下,**WSDL 常常被形容成 SOAP 服务的使用说明书**,但是请注意,本质上它们之间不存在依赖关系。
|
||||
|
||||
**这种将服务和服务的描述解耦开设计的方式非常普遍**,希望你可以去类比和联想。在软件的世界里,我们经常谈论这个“描述”的行为,以及描述者和被描述者。比如元属性描述数据,方法签名描述方法,类描述对象等等。
|
||||
|
||||
## REST
|
||||
|
||||
现在,我们再来看 REST 的做法。**REST 的核心要素包括资源、表现层和状态转换这三个部分。**我们把前面客户端发送请求的过程使用 REST 风格再来实现一遍,你将看到这三个要点是怎样体现出来的:
|
||||
|
||||
### 1. 协议
|
||||
|
||||
我们将使用 HTTP 协议,在加密的情况下,协议是 HTTPS,但这对我们的实现来说没有什么区别。
|
||||
|
||||
### 2. URL
|
||||
|
||||
通常来说,这个 URL 要包括域名、版本号以及实体名称,而这个 URL 整体,代表了 REST 中的一类或一项“资源”。比如说:
|
||||
|
||||
```
|
||||
https://xxx/v1/books
|
||||
|
||||
```
|
||||
|
||||
请注意其中的实体名称,它往往是一个单纯的名词,并且以复数形式出现。
|
||||
|
||||
这里提到了 URL,我想给经常混用的 URL、URI 做个简要的说明:URL 指的是 Uniform Resource Locator,URI 指的是 Uniform Resource Identifier,前者是统一资源定位符,后者是统一资源标识符。**Identifier 可以有多种形式,而 locator 只是其中一种,因此 URI 更宽泛,URL 只是 URI 的其中一种形式**。
|
||||
|
||||
当我们提到一个完整的地址,例如 [https://www.google.com](https://www.google.com), 它就是 URL,因为它可以被“定位”,它当然也是 URI;但是如果我们只提到上面地址的一个片段,例如 www.google.com,那么由于缺少了具体协议,我们无法完成完整的定位,因此这样的东西只能被叫做一个标识符,故而只能算 URI,而非 URL。
|
||||
|
||||
### 3. 方法
|
||||
|
||||
HTTP 的方法反映了这个接口上将执行的行为,如果用自然语言描述,它将是一个动词。比如说,给图书馆添加一本图书,那么这个行为将是“添加”。在以 REST 风格主导的设计中,我们将使用这样的 HTTP 方法来代表增删改查(CRUD)的不同动作:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2f/6c/2f80b22c23fb5382e359ece6082c2f6c.jpg" alt="">
|
||||
|
||||
重点解释下表格的最后两列:
|
||||
|
||||
- **幂等性指的是对服务端的数据状态,执行多次操作是否和执行一次产生的结果一样。**从表格中你可以看到,创建资源单位不是幂等的,执行多次就意味着创建了多个资源单位,而其它操作都是幂等的。通常来说,**幂等操作是可以被重试而不产生副作用的。**
|
||||
- **安全性指的是该操作是否对服务端的数据状态产生了影响。**从表格中可以看到,获取资源的操作是安全的,不会对资源数据产生影响,但是其它操作都是不安全的。一定条件下,**安全操作是可以被缓存的**,而不安全的操作,必定对服务端的状态产生了影响,这体现了 REST 的“状态转换”这一要素。
|
||||
|
||||
全栈系统的设计和优化都需要紧密围绕幂等性和安全性进行,这是两个非常重要的概念,在我们后续的学习中,你还会反复见到它们,并和它们打交道。
|
||||
|
||||
你看,通过这样的办法,就把 HTTP 的方法和实际对资源的操作行为绑定起来了。当然,还有一些其它方法,比较常见的有:
|
||||
|
||||
- PATCH:和 PUT 类似,也用于资源更新,但支持的是资源单位的部分更新,并且在资源不存在的时候,能够自动创建资源,这个方法实际使用比较少。
|
||||
- HEAD:这个方法只返回资源的头部,避免了资源本身获取和传输的开销。这种方法很有用,经常用来检查资源是否存在,以及有无修改。
|
||||
- OPTIONS:这个方法常用来返回服务器对于指定资源所支持的方法列表。
|
||||
|
||||
### 4. 正文
|
||||
|
||||
POST 和 PUT 请求都是有 HTTP 正文的,正文的类型和 Content-Type 的选取有关,比如 JSON 就是最典型的一种格式。请不要轻视这里的 Content-Type,从本质上说,它代表了资源的表现形式,从而体现了 REST 定义中的“表现层”这一要素。
|
||||
|
||||
最后,回到我们实际的图书馆添加图书的问题。SOAP 添加一本书的消息,用 REST 风格的 POST 请求实现就会变成这样:
|
||||
|
||||
```
|
||||
POST /v1/books HTTP/1.1
|
||||
HOST: ...
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "...",
|
||||
"author": "...",
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 风格之争
|
||||
|
||||
看到这儿,你应该已经感受到了,SOAP 和 REST 代表了两种迥异的风格。在我们取舍技术的时候,如果没有给出具体场景和限制,我们往往是很难讲出谁更“好”,而是需要进行比较,权衡利弊的。
|
||||
|
||||
SOAP 明显是更“成熟”的那一个。它在数据传输的协议层面做了更多的工作,藉由 XML schema,它具备更严格的检查和校验,配合 WSDL,在真正发送请求前,几乎就对所有远程接口事无巨细的问题有了答案。但是,它的复杂度令人望而生畏,也是它最受人诟病的地方。
|
||||
|
||||
REST 则相反,新接口的学习成本很低,只需要知道资源名称,根据我们熟知的规约,就可以创建出 CRUD 的请求来。但是直到真正发送请求去测试为止,并没有办法百分百确定远程接口的调用是否能工作,或者说,并不知道接口定义上是否有不规范、不合常规的坑在里面。
|
||||
|
||||
对于互联网来说,SOAP 已经是一项“古老”的技术了,晚辈 REST 似乎更切合互联网的潮流。在大多数情况下,REST 要易用和流行得多,于是很多人都不喜欢繁琐的 SOAP 协议。**技术的发展总是有这样的规律,一开始无论问题还是办法都很简单,可是随着需求的进一步增加,解决的方法也缓慢演化,如 SOAP 一般强大而复杂,直到某一天突然掉到谷底,如 REST 一般返璞归真。**
|
||||
|
||||
但是别忘了,有利必有弊。首先,正是因为 REST 只是一个缺乏限制的风格,而非一个严谨的规范,有太多不明确、不一致的实现导致的问题,这些问题轻者给接口调用者带来困惑,重者导致接口调用错误,甚至服务端数据错误。
|
||||
|
||||
其次,REST 通过 HTTP 方法实现本身,也受到了 HTTP 方法的局限性制约。比如最常见的 GET 请求,有时需要一个复杂的查询条件集合,因此参数需要结构化,而 GET 只支持一串键值对组合的参数传递,无法适应业务需要。对于这样的问题,有一些 workaround,比如使用 POST 消息体来传递查询条件的结构体,但那已经偏离了 REST 的最佳实践,丢失了 GET 本身的优势,也带来了实现不一致等一系列问题。
|
||||
|
||||
最后,REST 还存在的一个更本质的问题,资源是它的核心概念,这原本带来了抽象和简洁的优势,但如今也成为了它的桎梏。或者说,前面反复提到的增删改查是它最拿手的本事,可是互联网的需求是千变万化的,远不只有简单的增删改查。有时需要一个复杂的多步操作,有时则需要一个异步事务(需要回调或者二次查询等等方式来实现),这些都没有一个完美统一的 REST 风格的解决方案。即便实现出来了,也可谓五花八门,同时失去了以往我们所熟知的 REST 的简洁优势。
|
||||
|
||||
**互联网总在变复杂,但矛盾的是,人们却希望互联网上的交互会不断变简单。**于是这引发了 REST 的流行,可即便 REST 再流行,依旧有它不适合的场景;SOAP 虽古老,依然有它的用武之地。
|
||||
|
||||
对于全栈工程师或者期待修炼全栈技能的你我来说,trade-off 是永恒的话题。另外,除了 SOAP 和 REST,其实我们还有其它选择。我将在下一讲,结合实例具体介绍如何选择技术,并设计和实现远程接口。
|
||||
|
||||
## 总结思考
|
||||
|
||||
今天我们认识并学习了 SOAP 和 REST 这样两种截然不同的风格,前者工整、严谨和细致,后者自由、灵活和简约。两道思考题如下:
|
||||
|
||||
- 在做技术比较的时候,文中已经简单介绍了 REST 和 SOAP 的优劣,你觉得,它们各自适合怎样的业务场景呢?
|
||||
- 有位程序员朋友在应用 RESTful 风格设计用户管理系统的接口时,“删除单个用户”功能的 URL 举例如下,你觉得有哪些问题?
|
||||
|
||||
```
|
||||
http://xxx/deleteUser?userName=James
|
||||
|
||||
```
|
||||
|
||||
今天的内容就到这里,希望你已经享受到了技术学习的快乐,如果你还有余力,请继续学习下面的选修课堂和扩展阅读。最后,对于上面的问题,或者你对今天的学习有什么感受,欢迎在留言区和我讨论!
|
||||
|
||||
## 选修课堂:动手调用 RESTful API
|
||||
|
||||
学习全栈怎么能不动手实践呢,现在就让我们开始吧。有一些在线工具,预置了 REST 风格的接口服务,我们可以使用命令行去指定不同的 HTTP 方法,发送一些不同的 HTTP 请求,观察返回,通过实际的练习,相信你能够更好地理解 REST。这样的工具有很多,你可以自行搜索,也可以直接选择 [REQ | RES](https://reqres.in/):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2d/b8/2d216d1910348056c1631d46f0f099b8.png" alt="">
|
||||
|
||||
你可以使用网站上预置的请求,但我更推荐你自己写 curl 命令。比如发送一个 GET 请求,列出所有用户:
|
||||
|
||||
```
|
||||
curl -v https://reqres.in/api/users | jq
|
||||
|
||||
```
|
||||
|
||||
其中的 -v 参数可以帮助输出详尽的信息,包括请求和响应的完整信息,当然也可以不用;后面的 “| jq” 是为了让返回的 JSON 数据展示更美观,当然,你需要安装 [jq](https://stedolan.github.io/jq/)。如果你没有安装,不使用 jq 管道也是完全可以的。
|
||||
|
||||
再比如,使用 POST 请求创建一个用户:
|
||||
|
||||
```
|
||||
curl -X POST -d '{"name":"xxx", "job":"yyy"}' -H "Content-Type: application/json" https://reqres.in/api/users | jq
|
||||
|
||||
```
|
||||
|
||||
这里使用了 -x 参数指定其为 POST 请求,之后的 Content-Type 是必不可少的,而 JSON 形式的 user 对象则通过参数 -d 传了过去。
|
||||
|
||||
最后得到了这样的结果:
|
||||
|
||||
```
|
||||
... (省略请求统计信息)
|
||||
{
|
||||
"name": "xxx",
|
||||
"job": "yyy",
|
||||
"id": "585",
|
||||
"createdAt": "2019-07-20T22:19:49.825Z"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
- W3Cschool 上的 [SOAP 教程](https://www.w3cschool.cn/soap/?),如果你对 SOAP 不够熟悉,那么你可以参考这个简明扼要的教程。
|
||||
- 【基础】[REST API Tutorial](https://www.restapitutorial.com/),REST 的教程很多,这是我觉得非常简洁和清晰的一个。
|
||||
- ProgrammableWeb 上的 [Web API 列表](https://www.programmableweb.com/apis/directory),排名最靠前的 10 个 API,其中有 9 个的架构风格都是 REST,这也从侧面应证了 REST 在互联网的趋势。
|
||||
- [REST 和 SOAP:谁更好,或者都好?](https://www.infoq.cn/article/rest-soap-when-to-use-each)这是一篇内容精悍的译文,分别介绍了适合 REST 和 SOAP 的场景。
|
||||
|
||||
|
||||
204
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/05 | 权衡的艺术:漫谈Web API的设计.md
Normal file
204
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/05 | 权衡的艺术:漫谈Web API的设计.md
Normal file
@@ -0,0 +1,204 @@
|
||||
<audio id="audio" title="05 | 权衡的艺术:漫谈Web API的设计" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/14/79/140e75686512e05585f889aaab9c6379.mp3"></audio>
|
||||
|
||||
你好,我是四火。
|
||||
|
||||
今天,我们该根据之前所学,来谈谈具体怎样设计 Web API 接口了。我们围绕的核心,是**“权衡”(trade-off)**这两个字,事实上,它不只是 Web API 接口设计的核心,还是软件绝大多数设计问题的核心。
|
||||
|
||||
我们说“没有银弹”,是因为没有一种技术可以百搭,没有一种解决方案是完美的,但一个优秀的全栈工程师,是可以从琳琅满目的同类技术中,因地制宜地选择出最适合的那一个。
|
||||
|
||||
## 概念
|
||||
|
||||
在一切开始之前,我们先来明确概念。什么是 Web API?
|
||||
|
||||
你应该很熟悉 API,即 Application Programming Interface,应用程序的接口。它指的就是一组约定,不同系统之间的沟通必须遵循的协议。使用者知道了 API,就知道该怎样和它沟通,使用它的功能,而不关心它是怎么实现的。
|
||||
|
||||
Web API 指的依然是应用程序接口,只不过它现在暴露在了 Web 的环境里。并且,我们通常意义上讲 Web API 的时候,无论是在 B/S(浏览器/服务器)模型还是 C/S(客户端/服务器)模型下,往往都心照不宣地默认它在服务端,并被动地接受请求消息,返回响应。
|
||||
|
||||
通常一个 Web API 需要包括哪些内容呢?
|
||||
|
||||
回答这个问题前,让我们先闭上眼想一想,如果没有“Web”这个修饰词,普通的 API 要包括哪些内容呢?嗯,功能、性能、入参、返回值……它们都对,看起来几乎是所有普通 API 的特性,在 Web API 中也全都存在。而且,因为 Web 的特性,它还具备我们谈论普通 API 时不太涉及的内容:
|
||||
|
||||
- 比如承载协议。这里可以有多个协议,因为协议是分层的。HTTP 协议和 TCP 协议就是并存的。
|
||||
- 再比如请求和响应格式。Web API 将普通 API 的方法调用变成了网络通信,因此参数的传入变成了请求传入,结果返回变成了响应传出。
|
||||
|
||||
正是有了 Web API,网络中的不同应用才能互相协作,分布式系统才能正常工作,互联网才能如此蓬勃发展。而我们,不能只停留在“知道”的层面,还要去深入了解它们。
|
||||
|
||||
## Web API 的设计步骤
|
||||
|
||||
关于Web API 的设计步骤,不同人有不同的理解,争论不少,涉及到的内容也非常广泛。这里我综合了自己的经验和观点进行介绍,希望你能有所启发。
|
||||
|
||||
### 第一步:明确核心问题,确定问题域
|
||||
|
||||
和普通的 API 设计、程序的库设计一样,Web API 并不是东打一枪,西打一炮的。想想写代码的时候,我们还要让同类型的方法,以某种方式组织在类和对象中,实现功能上的内聚呢,一个类还要遵循单一职责的原则呢。
|
||||
|
||||
因此,一组 Web API,就是要专注于一类问题,核心问题必须是最重要的一个。
|
||||
|
||||
在上一讲中我举了个图书管理系统的例子,那么可以想象,图书的增删改查 API 就可以放到一起,而如果有一个新的 API 用于查询图书馆内部员工的信息,那么它显然应该单独归纳到另外的类别中,甚至是另外的系统中。
|
||||
|
||||
### 第二步:结合实际需求和限制,选择承载技术
|
||||
|
||||
这里有两件事情需要你考虑,一个是需求,一个是限制。我们虽然经常这样分开说,但严格来说,限制也是需求的一种。比方说,如果对网络传输的效率要求很高,时延要求很短,这就是需求,而且是非功能性的需求。
|
||||
|
||||
大多数功能性的需求大家都能意识到,但是一些非功能性的需求,或者一些“限制”就容易被忽略了。比如说,向前的兼容性,不同版本同时运行,鉴权和访问控制,库依赖限制,易测试性和可维护性,平滑发布(如新老接口并行),等等。
|
||||
|
||||
再来说说承载技术。承载技术指的是实现接口,以及它的请求响应传输所需要使用到的技术集合,比如 HTTP + JSON。我们前面提到的要求网络传输效率高、时延短,[Protobuf](https://developers.google.com/protocol-buffers/) 就是一个值得考察的技术;但有时候,我们更需要消息直观、易读,那么显然 Protobuf 就不是一个适合的技术。这里我们通过分析技术优劣来做选择,这就是权衡。
|
||||
|
||||
虽说 Web API 主要的工作在服务端,但在技术分析时还需要考虑客户端。特别是一些技术要求自动生成客户端,而有些技术则允许通过一定方式“定制”客户端(例如使用 DSL,Domain Specific Language,领域特定语言)。
|
||||
|
||||
### 第三步:确定接口风格
|
||||
|
||||
技术的选择将很大程度地影响接口的风格。
|
||||
|
||||
还记得我在上一讲介绍的 SOAP 和 REST 的例子吗?那就是接口风格比较的一个典型示例。请不要小看这两个字,“风格”包含的内容很多,大到怎样划分功能,小到接口的命名,都包括在内。在实际设计中,我们很少正面地去谈论具体的风格,但我们都有意无意地将其考虑在内。这里我举几个比较重要的例子,通过它,你会了解到权衡其实无处不在。
|
||||
|
||||
角度一:易用性和通用性的平衡,或者说是设计“人本接口”还是“最简接口”。
|
||||
|
||||
比如一个图书管理的接口,一种设计是让其返回“流行书籍”,实际的规则是根据出版日期、借阅人数、引进数量等等做了复杂的查询而得出;而另一种设计则是让用户来自行决定和传入这几个参数,服务端不理解业务含义,接口本身保持通用。
|
||||
|
||||
**前者偏向“易用”,更接近人的思维;后者偏向“通用”,提供了最简化的接口。**虽说多数情况下我们还是会见到后者多一些,但二者却不能说谁对谁错,它们实际代表了不同的风格,各有优劣。
|
||||
|
||||
角度二:接口粒度的划分。
|
||||
|
||||
比如用户还书的过程包括:还书排队登记、检查书本状况、图书入库,这一系列过程是设计成一个大的接口一并完成,还是设计成三个单独的接口分别调用完成?
|
||||
|
||||
其实,这二者各有优劣。**设计成大接口往往可以增加易用性,便于内部优化提高性能(而且只需调用一次);设计成小接口可以增加可重用性,便于功能的组合。**
|
||||
|
||||
你可能会想,两种方式都保留,让用户去选择不行吗?
|
||||
|
||||
行,但那样给双方带来好处的同时,也带来了更多的问题,除了风格的不一致,接口也不再是正交的,而是有一定重叠性的,并且更多的接口意味着更多的开发和维护工作。这些接口要像是一个人设计出来的,而不是简单的组合添加,**风格统一也是一致性的一种表现**。因此,多数情况下我们不那么做。你看,这又是权衡。
|
||||
|
||||
但是,我说的是“多数情况下”我们不那么做。在一些极端情况下,我们是会牺牲掉一致性,保留冗余的。
|
||||
|
||||
我举一个 JDK 的例子。JDK 的 HashTable 有一个 containsValue 方法,还有一个 contains 方法,二者功能上完全一样,之所以搞这样两个完全一样的方法,正是由于历史原因造成的。JDK 1.2 才正式引入 Java Collections Framework,抽象了 Map 接口,也才有了 containsValue 方法,而之前的方法因为需要保持向下兼容而无法删除,也是无可奈何。同样,这也是权衡。
|
||||
|
||||
### 第四步:定义具体接口形式
|
||||
|
||||
在上面这三步通用和共性的步骤完成之后,我们就可以正式跳进具体的接口定义中,去确定 URL、参数、返回和异常等通用而具体的形式了。还记得上一讲中对 REST 请求发送要点的分解吗?在它的基础上,我们将继续以 REST 风格为例,进行更深刻的讨论。
|
||||
|
||||
**1. 条件查询**
|
||||
|
||||
我们在上一讲的例子中使用 HTTP GET 请求从图书馆获取书本信息,从而完成增删改查中的“查”操作:
|
||||
|
||||
```
|
||||
/books/123
|
||||
/books/123/price
|
||||
|
||||
```
|
||||
|
||||
分别查询了 ID 为 123 的图书的全部属性,和该图书的价格信息。
|
||||
|
||||
但是,实际的查所包含的内容可远比这个例子多,比如不是通过 ID 查询,而是通过条件查询:
|
||||
|
||||
```
|
||||
/books?author=Smith&page=2&pageSize=10&sortBy=name&order=desc
|
||||
|
||||
```
|
||||
|
||||
你看条件查询书籍,查询条件通过参数传入,指定了作者,要求显示第二页,每页大小为10条记录,按照书名降序排列。
|
||||
|
||||
除了使用 Query String(问号后的参数)来传递查询条件,多级路径也是一种常见的设计,这种设计让条件的层级关系更清晰。比如:
|
||||
|
||||
```
|
||||
/category/456/books?author=Smith
|
||||
|
||||
```
|
||||
|
||||
它表示查询图书分类为“艺术”(编号为 456)的图书,并且作者是 Smith。看到这里,你可能会产生这样两个疑问。
|
||||
|
||||
疑问一:使用 ID 多不直观啊,我们能使用具体名称吗?
|
||||
|
||||
当然可以!**可以使用具备业务意义的字段来代替没有可读性的 ID,但是这个字段不可重复,也不宜过长**,比如例子中的 category 就可以使用名称,而图书,则可以使用国际标准书号 ISBN。于是 URI 就变成了:
|
||||
|
||||
```
|
||||
/category/Arts/books?author=Smith
|
||||
|
||||
```
|
||||
|
||||
疑问二:category 可以通过 Query String 传入吗?比如下面这样:
|
||||
|
||||
```
|
||||
/books?author=Smith&category=Arts
|
||||
|
||||
```
|
||||
|
||||
当然可以!“category”可以放置在路径中,也可以放置在查询参数串中。**这是 REST 设计中的一个关于设计上合理冗余的典型例子,可以通过不同的方式来完成相同的查询**。如果你学过 Perl,你可能听过[“There’s more than one way to do it”](https://zh.wikipedia.org/wiki/%E4%B8%8D%E6%AD%A2%E4%B8%80%E7%A7%8D%E6%96%B9%E6%B3%95%E5%8E%BB%E5%81%9A%E4%B8%80%E4%BB%B6%E4%BA%8B)这样的俗语,这是一样的道理,也是 REST 风格的一部分。
|
||||
|
||||
当然,从这也可以看出上一讲我们提到过的,REST 在统一性、一致性方面的约束力较弱。
|
||||
|
||||
**2. 消息正文封装**
|
||||
|
||||
有时候我们还需要传递消息正文,比如当我们使用 POST 请求创建对象,和使用 PUT 请求修改对象的时候,我们可以选择使用一种技术来封装它,例如 JSON 和 XML。通常来说,既然我们选择了 REST 风格,我们在相关技术的选择上也可以继续保持简约的一致性,因此 JSON 是更为常见的那一个。
|
||||
|
||||
```
|
||||
{
|
||||
"name": "...",
|
||||
"category": "Arts",
|
||||
"authorId": 999,
|
||||
"price": {
|
||||
"currency": "CNY",
|
||||
"value": 12.99
|
||||
},
|
||||
"ISBN": "...",
|
||||
"quantity": 100,
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面的消息体内容就反映了一本书的属性,但是,在设置属性的时候,往往牵涉到对象关联,上面这个小小的例子就包含了其中三种典型的方式:
|
||||
|
||||
- 传递唯一业务字段:例如上面的 category 取值是具备实际业务意义的“Arts”;
|
||||
- 传递唯一 id:例如上面的 authorId,请注意,这里不能传递实际作者名,因为作者可能会重名;
|
||||
- 传递关联对象:例如上面的 price,这个对象通常可以是一个不完整的对象,这里指定了货币为人民币 CNY,也指定了价格数值为 12.99。
|
||||
|
||||
**3. 响应和异常设计**
|
||||
|
||||
HTTP 协议中规定了返回的状态码,我想你可能知道一些常见的返回码,大致上,它们分为这样五类:
|
||||
|
||||
- 1xx:表示请求已经被接受,但还需要继续处理。这时你可能还记得在 [[第 03 讲]](https://time.geekbang.org/column/article/136587) 中,我们将普通的 HTTP 请求升级成为 WebSocket 的过程,101 就是确认连接升级的状态码。
|
||||
- 2xx:表示请求已经被接受和成功处理。最常见的就是 204,表示请求成功处理,且返回中没有正文内容。
|
||||
- 3xx:表示重定向,请客户端使用重定向后的新地址继续请求。其中,301 是永久重定向,而 302 是临时重定向,新地址一般在响应头“Location”字段中指定。
|
||||
- 4xx:表示客户端错误。服务端已经接到了请求,但是处理失败了,并且这个锅服务端不背。这可能是我们最熟悉的返回码了,比如最常见的 404,表示页面不存在。常见的还有 400,表示请求格式错误,以及 401,鉴权和认证失败。
|
||||
- 5xx:表示服务端错误。这回这个处理失败的锅在服务端这边。最常见的是 500,通用的和未分类的服务端内部错误,还有 503,服务端暂时不可用。
|
||||
|
||||
错误处理是 Web API 设计中很重要的一部分,我们需要告知用户是哪个请求出错了,什么时间出错了,以及为什么出错。比如:
|
||||
|
||||
```
|
||||
{
|
||||
"errorCode": 543,
|
||||
"timeStamp": 12345678,
|
||||
"message": "The requested book is not found.",
|
||||
"detailedInfomation": "...",
|
||||
"reference": "https://...",
|
||||
"requestId": "..."
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这个例子中,你可以看到上面提到的要素都具备了,注意这里的 errorCode 不是响应中的 HTTP 状态码,而是一个具备业务意义的内部定义的错误码。在有些设计里面,也会把 HTTP 状态码放到这个正文中,以方便客户端处理,这种冗余的设计当然也是可以的。
|
||||
|
||||
## 总结思考
|
||||
|
||||
还记得我们是通过怎样的步骤来设计 Web API 的吗?其实可以总结为八个字:**问题、技术、风格和定义**,由问题到实现,由概要到细节。
|
||||
|
||||
问题域往往比较好确定,技术选型在需求和限制分析清楚的情况下也不难做出选择,但是接口风格往往就考验 API 的设计功底了。在这部分中,易用性和通用性的平衡,接口粒度的控制,是非常重要的两个方面,这是需要通过不断地“权衡”来确定的。至于在接口定义的步骤中,细节很多,更多的内容需要我们在实践中多参考一些优秀的接口实现案例,逐渐积累经验。
|
||||
|
||||
这一讲通篇都不断地提到了“权衡”,现在我来提一个关于权衡的小问题:
|
||||
|
||||
- 在介绍 REST 的参数传递的时候,我们讲了 category 参数传递的两种方式,一种是通过路径传递,一种是通过 Query String 的参数传递。你觉得哪些参数适合使用第一种,哪些参数更适合使用第二种?
|
||||
|
||||
如果你还有余力,那我再提一个接口设计方面的问题:
|
||||
|
||||
- 我们提到了 REST 风格下,我们使用 HTTP 的不同方法来应对增删改查这样不同的行为。但是,互联网的业务是很复杂的,有时候操作并非简单的增删改查,这种情况会考验我们的 REST 设计功底。比如说,我们要给银行转账,即钱从一个人的账下转移到另一个人的账下,这样的复杂行为不属于增删改查中的任何一项,我们是否能使用 REST 风格来设计这样的转账接口呢?
|
||||
|
||||
到今天为止,第一章,也就是“网络协议和 Web 接口“的内容我们就讲完了。网络协议部分,我们以 HTTP 为核心,介绍了它的特性和发展进程,展示了 TLS 连接建立和证书验证的原理,深入了 Comet 和 WebSocket 等服务端消息推送技术,并通过抓包分析等实践,进一步加深了理解。Web 接口部分,我们结合图书馆的实例,学习和比较了 SOAP 和 REST 的实现和风格,并一步一步梳理了 Web 接口设计的过程。
|
||||
|
||||
最后,对于上面的问题你有什么答案,或是对于这一章的内容有什么思考和疑问,欢迎你在留言区中畅所欲言,我们一起探讨,相信能碰撞出很多新的火花。
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
- 【基础】[HTTP状态码](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81) 和 [HTTP头字段](https://zh.wikipedia.org/wiki/HTTP%E5%A4%B4%E5%AD%97%E6%AE%B5),我们在工作中会反复和各式各样的状态码和请求、响应中的头部字段打交道,因此通读并熟知一些常见的状态码是很有必要的。关于 HTTP 状态码,有人把一些常见的状态码形象地对应到[猫的照片](https://http.cat/),或许能帮助你记忆,当然,如果你喜欢狗,那你可以看看[这个](https://httpstatusdogs.com/)。
|
||||
- [Any API](https://any-api.com/),我们不能光闭门造车,还要去学习其它网站的 Web API 设计,了解互联网上大家都是怎么做的。我们学习它们的实现,但请不要盲目,有不少接口由于种种原因,设计有一些亟待商榷的地方,请带上你批判的眼光。
|
||||
- [Richardson Maturity Model](https://martinfowler.com/articles/richardsonMaturityModel.html),这篇会有一些深度,大名鼎鼎的马丁·福勒(Martin Fowler)的文章,讲 REST 的一种成熟度模型,里面划分了从 0 级到 3 级这样 4 种成熟度级别,这种分级方式被一些人奉为圭臬。
|
||||
|
||||
|
||||
120
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/06 | 特别放送:北美大厂如何招聘全栈工程师?.md
Normal file
120
极客时间专栏/全栈工程师修炼指南/第一章 网络协议和 Web 接口/06 | 特别放送:北美大厂如何招聘全栈工程师?.md
Normal file
@@ -0,0 +1,120 @@
|
||||
<audio id="audio" title="06 | 特别放送:北美大厂如何招聘全栈工程师?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5f/4e/5f81e8b9e29c15e60ed8436c60b2f24e.mp3"></audio>
|
||||
|
||||
你好,我是四火。
|
||||
|
||||
在第一章技术内容的末尾,我们来换换脑子,聊一些略轻松的话题。我曾在开篇词中讲过,全栈工程师的市场需求量很大,今天我就来介绍一下北美大厂,特别是那些大名鼎鼎的互联网巨头们,都是怎样招聘全栈工程师的。
|
||||
|
||||
这些大公司在全世界不同的国家内往往都会建立基地聚敛人才,当然包括 Top 2 的互联网超级大国——中国(你可能还不知道,[互联网十大企业中,中国占了四大,美国占了六大](https://zh.wikipedia.org/zh-hans/%E6%9C%80%E5%A4%A7%E7%9A%84%E4%BA%92%E8%81%94%E7%BD%91%E5%85%AC%E5%8F%B8%E5%88%97%E8%A1%A8))。我想,了解一下他们的做法,对于程序员的你来说,既能拓宽眼界,也能更好地清楚自己在市场上的定位,从而更好地成长,这应当是很有价值的。
|
||||
|
||||
## 招人理念
|
||||
|
||||
首先,招聘这个事儿,其重要性毋庸置疑,这几乎是所有的互联网公司都认可的一点。对某些互联网公司来说,例如 Google,则是[“最重要”的事情](https://rework.withgoogle.com/blog/Google-hiring-is-the-most-important-thing-you-do/),连“之一”这两个字都省了。
|
||||
|
||||
招到一个优秀的工程师,你的团队和产品,都将获得巨大的收益;而招到一个不合格的工程师,不但会拉低团队的水准,还要花费其他同事大量的时间精力来帮助其成长。因此,招聘可以说是壮大一家公司最快的方法,但同时也是毁掉一家公司最快的方法。于是,面试,对于很多大型互联网公司的工程师来说,就是日常工作的一个重要组成部分。
|
||||
|
||||
通常来说,这些公司在招聘的时候,最关心这样两件事情。
|
||||
|
||||
一个是非技术能力,很多公司把其中重要的内容归纳成了领导力准则(Leadership Principles),比如[亚马逊的这十几条](https://www.amazon.jobs/zh/principles);另一个则是技术能力,主要包括编程能力、问题解决(Problem Solving)能力和架构、设计能力。其中,不同级别的工程师,对于系统设计等内容的要求不同,但是对于编程能力的要求基本是一样的。例如,初级工程师可能在技术能力上 90% 考察的是编程能力和问题解决能力,而高级工程师这部分往往会掉到 60%,剩下的 30% 考察架构和设计能力。
|
||||
|
||||
我们可以把北美和国内不同的工程师岗位考察来做一个比较,它们都立足于基础,但还是有所区别的。国内大厂的面试我认为更具备实战性,即知识性更强,技术面较广,不同用人单位对于不同技术的考察也更具体;但北美(也包括在国内的北美外企)大厂的面试则更偏向于具体技术的问题解决能力、编码能力,以及架构设计能力等等。至于全栈工程师岗位,其实并没有特别显著的特殊性,候选者考察的理念基本是一样的,只是对于问题领域,以及技术栈等方面的考察,会更有针对性。
|
||||
|
||||
## 招聘流程
|
||||
|
||||
### 1. 简历阶段
|
||||
|
||||
招聘的流程有时很短,一周内就可以完成所有的事情,有时也会很长,数月、甚至超过一年之久。
|
||||
|
||||
大多数程序员还是习惯于使用招聘网站来投递简历,但是也有许多程序员们,在找工作的时间窗口内,是以点对点的方式来找下家的,即答复 E-mail、LinkedIn 上主动来联系的 Recruiter(招聘人员)或 Manager(经理),主动投递目标公司,或者找朋友推荐,而不是将自己的简历无差别地挂到招聘网站上,这样可以有效避免过多的电话和消息骚扰,还可以针对特定的公司优化简历,做进一步准备。
|
||||
|
||||
但无论你是用招聘网站,还是朋友推荐,有一天,有一位自称是某公司的招聘人员用着客气的语气发来邮件 ,她介绍了自己的来历,并和你约了时间电话聊天,这就意味着“简历关”已过,进入了互相了解的阶段。
|
||||
|
||||
在把公司、团队、项目、薪酬等等这些事情都介绍完之后,如果互相还有进一步的意愿,通常就要进入电话面试环节了。对于全栈工程师这一岗位来说,**有时招聘人员会问一点非常简单的 Web 相关的基础知识,这其实并非为了考察候选人的能力,而是为了过滤掉那些明显不靠谱的候选人**,给后面的面试团队节约时间成本。之后,Recruiter 会根据工作的时间长短给出候选人的最低应聘级别,比如说,已经工作五年的工程师,最低也得是中级工程师的职位了,此时如果候选人达不到要求,那就不要了。
|
||||
|
||||
还有公司招聘是统招(General Hire),即招聘完毕之后统一双向选择去哪个小组工作,比如 Facebook,但绝大多数公司还是会采用项目组自己招聘的形式。对于前者,招聘人员会着重介绍公司的文化和公司的发展方向;但对于后者,则可以具体很多,即可以和候选人交流目标团队、项目和技术栈等。
|
||||
|
||||
### 2. 电话(视频)面试
|
||||
|
||||
电话面试(Phone Screen)通常有一轮或两轮,每轮 45 分钟到一个小时,有时候也会通过视频面试,当然,对于特别优秀或者他人强力推荐的候选人,甚至可以免试。
|
||||
|
||||
电话面试一般由一线工程师来担任面试官,如果是两轮面试,那么第二轮有时也由一线经理来担任面试官。电话面试至少有一轮必须要考编码问题,这个问题一般不会很难,**通常是一个较为简单到中等偏下难度的算法题,但是需要在电话沟通的基础上,通过多人在线文档工具将代码写下来**,面试官可以看到文本上的编码全过程。对于全栈工程师来说,面试官还可能会花几分钟的时间,问几个全栈技术范畴内的问题,但是总的流程是一样的。
|
||||
|
||||
电话面试出结果很快,一般 Hiring Manager(招聘经理)和参与面试的工程师一商量,就可以确定要不要进行下一步了。电话面试的通过率对于每个公司都不太一样 ,但是根据我的观察,一般这个通过率是在 30% ~ 50% 左右。如果电话面试挂掉的话,通常就可以认为候选人距离公司和团队的要求还有较大差距。
|
||||
|
||||
### 3. 现场面试
|
||||
|
||||
如果电话面试结果不错,候选人往往会有一个和团队核心成员或是经理见面的机会,比如 OCI(Oracle 的云基础设施部门,它是独立运作的,文化和流程都和传统的 Oracle 有较大不同),这主要是在进一步的面试前,给候选者和团队继续互相了解和深入的机会。毕竟,面试是双向的。
|
||||
|
||||
接下来,招聘人员会和候选人约时间进入 On-site(现场面试)环节,这个环节需要到项目组所在的城市去,且一般需要持续一天的时间。**比较常见的是 5 轮面试的方式,每轮一个小时,中午会和招聘经理或是团队的工程师吃饭**,这顿饭有可能作为一轮面试算入考察过程,也可能不算。像 Facebook 和 Google 都不算,候选人可以放松地在食堂饱餐一顿;Amazon、OCI 则往往会请候选人选择一份外卖,送来以后和招聘经理边吃边聊,考察非技术能力。
|
||||
|
||||
现场面试的招聘经理一般由一线工程师团队的经理担任。从这里你也可以看到,招聘一个人的成本,包括人力、时间、场地、差旅等等开销,是非常高昂的。
|
||||
|
||||
在除去吃饭以外的几轮面试中,编码能力是重点,通常最少有三轮是必须要涉及高强度的编码问题,我们把它简称为“主要问题”。这个编码通常在白板上进行。通常**面试官会努力将候选人带入到团队合作解决问题的氛围中,然后给出一个较为模糊的问题,再来一起沟通交流解决问题,最后代码必须落到白板上面**,这个解题过程要占据每轮面试的绝大部分时间。
|
||||
|
||||
对于初级工程师以上的职位,还有至少一轮的问题需要重点考察系统设计能力。每轮面试还有十多分钟的项目和问题挖掘时间,这部分的执行相对较为自由,往往是基于候选人的工作经历往深挖项目和技术,越具体越好。有经验的面试官会抓住一两个点往下深挖,挖到非常细节的部分,从而判断候选人是在夸夸其谈,还是一个真正做事的人。对于全栈工程师来说,项目和技术问题可能大量涉及全栈领域,例如以 Web 网站或应用为背景的题目。
|
||||
|
||||
这里我要再次强调一下白板代码的重要性,这里的白板代码不仅仅包括最后落笔写代码,还包括写代码前大量的确认、分析、讨论、架构、设计等等过程,这些占据技术面试的“主要问题”,所有的内容都是在白板上进行的,从中可以全方位地考察候选人的技术和非技术能力。
|
||||
|
||||
就如同那个“没有 jQuery 不会写 JavaScript”这样略带戏谑、但又透露着些许无奈的说法一样,现在很多程序员朋友都忽视了基础能力的修炼,没有了 IDE 就不会写代码了。白板肯定不像 IDE,有方法提示,写错了还可以随意修改,所以你现在明白为什么用白板了吗?那是因为白板要求你的思路非常清晰,代码组织能力也要很高。由于空间的限制,在白板上修改代码总是件不那么容易的事儿。
|
||||
|
||||
另外,还要补充一点,上面谈到的每轮面试中,最重要的那道面试题,公司的要求是,**题目在一开始必须要足够模糊,从而激发和考察候选人合作解决问题的能力,在沟通中逐步细化问题的时候,必须要达到中等偏上到困难的难度,以保证足够的区分度**。
|
||||
|
||||
这样的问题会考察到候选人的多项素质,特别是编程能力和问题解决能力。但也不要觉得,面试官是在刻意为难,这样的题目设计起来其实并不容易,他们要尽量避免使用互联网上很容易找得到的“常见题”,这个过程往往比解题本身要难得多。**题目不得涉及技术本身的奇技淫巧,不得对候选人使用的编程语言有限制,更要避免“知识性问题”。**
|
||||
|
||||
综合来看,我觉得是不是主要问知识性问题,是北美软件工程师(包括全栈工程师)的面试,和国内的面试比起来,在技术层面最本质区别。
|
||||
|
||||
那什么是“知识性问题”呢?知识性问题,就是那些直接的、较容易通过搜索和文档获取到的知识性内容。比如,Spring 怎样配置 Bean,Tomcat 怎么修改最大连接数等等,这些问题,手册一翻就是分分钟的事情。
|
||||
|
||||
但这绝不是说这些知识不实用,面试官通常不问这类问题最大的原因是,**知识性问题的随机性太强**,如果候选人恰巧刚刚遇到过,或者记性不错,就很容易回答上来了。而这些**却并不能反映候选者分析问题、思考、判断和权衡的能力**。但话说回来,知识性问题也是考察候选人基础技能的一种方式,有的面试官也会问,但肯定不是每轮面试中占据时间最多的那个“主要问题”。
|
||||
|
||||
### 4. 讨论会
|
||||
|
||||
在面试之后,所有的面试官都要写下对于候选人的反馈,这包括候选人的优势和劣势。在很快而来的 Debrief Meeting(讨论会)中,所有的面试官会根据自己的判断评价候选人,分为四档,分别是“强烈建议录用”“建议录用”“建议不录用”和“强烈建议不录用”。
|
||||
|
||||
当然,他们可以说服别人,也可以被说服而改变评价。每一个人,都会负责一项技术考察项(比如数据结构和算法)和非技术考察项(比如是否能拥抱变化,逐步改进),这些考察项在不同的公司会有不同,通常来说五轮里面针对算法和数据结构的考察至少有三轮。另外,有一些非技术的“Red Flag”(即所谓的“红线”),是绝对不能触碰的,比如说对于现有的职位或年限说谎。
|
||||
|
||||
在一组面试的面试官中,有两个角色值得一提,**一个是招聘经理**,上面已经提到了;**还有一个是技术负责人**(例如在 Amazon 叫做 Bar Raiser,OCI 叫做 Bartender),负责保证招聘质量,**他们拥有一票否决权,也就是说,哪怕其他所有人都同意,但这两人只要有一个不同意就不能通过**。
|
||||
|
||||
对于其他情况,直接投票且多数获胜。少数情况下,针对某个候选人,讨论会可能会有截然相反的意见,这时候面试官们就会摆出事实依据进行争辩了。这个讨论会的录用结果,还要包括职位级别。另外,有少数公司在这方面采用的方式略有不同,比如微软,候选人的最终录用决策是由一个特殊的“大人物”(叫做 As Appropriate)决定的,这和国内某些互联网公司很像。
|
||||
|
||||
除了我刚刚说的讨论会,某些公司为了从更高层面上把控招聘需求和质量,还可能会有额外的环节,比如 Google 和 Facebook 在讨论会以后,Hiring Committee(招聘委员会)拥有下一步的决策权,他们可以对讨论会通过的候选人做进一步筛选。总的来说,不同的公司差异较大,但即便是同一家公司,现场面试的总体通过率也非常不确定,并且这个波动较大,高的时候可能达到接近一半,低的时候可能只有十分之一 。
|
||||
|
||||
如果这一步也过了,就可以根据面试反馈的结果适当调整候选者的职位级别,再往后就是商量并给出 offer,确定入职时间,进行背景调查等等众所周知的步骤了。
|
||||
|
||||
## 进一步思考
|
||||
|
||||
众所周知,在一家公司中,软件工程师的未来发展方向,通常包括技术路线和管理路线两个。但是据我了解,大多数程序员还是更钟情于技术路线的,可对于程序员的编程技术,你一定听说过“青春饭”的说法。
|
||||
|
||||
我也听到过不少程序员谈论自己的职业现状,表示随着工作经验的增加,公司似乎更爱招刚毕业不久的年轻人,因为他们更有精力,薪水也更低。于是,大家看着互联网大潮是越来越汹涌,可东西却是越来越学不动了,工作也越来越难找了……
|
||||
|
||||
这是怎么回事?
|
||||
|
||||
首先,我想说的是,找工作总是希望自己是往上走的,薪水越来越高,职位也越来越高,责任也越来越大,高级职位在市场上的需求却越来越小。因此,**从这个角度看,工作本来就是越来越难找的**。因此,如果是这个原因,这未必是一件坏事。
|
||||
|
||||
其次,随着年纪的增加,你觉得你的核心竞争力是什么?如果只是重复地写 Spring 的配置,只是照猫画虎写写 CRUD 的样板代码,那么,比你年轻、能吃苦、能加班的,且薪水更低的程序员当然可以分分钟打败你。因为,你这不是有了工作多年的经验,而是工作一年的经验重复了多遍。从这个角度看,无论这个招聘的职位是不是全栈,学的技术是不是在 Web 方面,道理都是一样的。
|
||||
|
||||
**你的竞争力,在具备扎实基础的前提下,应该是经验、思路、眼界等等这些东西。技术是相通的,技术本质是不容易改变的,在新技术到来的时候,你有基础,无论是深度还是广度的积累,应该让你学得更快,而不是学得更慢。**
|
||||
|
||||
我想,今天介绍的关于工程师招聘流程的内容,也恰好反映了上面这两点:
|
||||
|
||||
- 扎实的基础不可或缺,这是前提。老实说,我经常遇到工作了好多年的一线程序员,连一个简单的二叉树广度优先遍历算法都写不出来。
|
||||
- 经验、思路、眼界,都有高度,才是更高级别技术职位的要求,这也反映在上述面试系统设计、问题解决等等方面。
|
||||
|
||||
这样的招聘方式当然有它的弊端,例如招到的人可能未必对所用技术都熟悉,未必能立马干活,但通过这样的招聘方式,确确实实可以过滤掉那些“一年工作经验重复多遍”的候选人。
|
||||
|
||||
## 总结思考
|
||||
|
||||
作为特别放送,今天的思考题,我想换个形式。如文中介绍的那样,设计一道合理的面试题其实并不容易,需要综合考虑多个因素。下面我列出了几道面试题,假设今天你就是面试官,你能说说它们中哪些适合作为全栈工程师岗位面试的“主要问题”,哪些不适合吗?
|
||||
|
||||
A:设计一个算法,把一个小于一百万的正整数,使用罗马数字来表示;<br>
|
||||
B:对一个 Web API,设计一个流量控制系统;<br>
|
||||
C:写一个 C++ 算法,实现 atoi 算法,即将字符串转换为数字;<br>
|
||||
D:设计一个网约车系统;<br>
|
||||
E:完成一个 HTML 页面,能够在网页上显示一个表示当前时间的数字时钟。
|
||||
|
||||
最后,我想强调一件事,单个应聘经历永远只能代表单次经验,如果有好的结果,那么恭喜你,但请不要意得志满,这其实并不代表你的整体水准;如果结果不好,也请不要灰心丧气,它并不代表你就真的达不到那家公司的要求。毕竟,招聘也好,面试也罢,其中的随机性太强,冷静、淡定分析自己的情况,再采取合理的措施,才是王道。
|
||||
|
||||
今天的特别放送就到这里,希望你在阅读后能有所收获。如果你在应聘和面试方面有什么困惑,或者想分享分享你的面试经历,欢迎在留言区一起讨论。
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/86/c8/860b223d5805a9a080294e9e56bbc0c8.jpg" alt="unpreview">](https://time.geekbang.org/column/intro/254?utm_term=zeus1LKX2&utm_source=app&utm_medium=geektime)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user