05-趣谈

开篇词 | 想成为技术牛人？先搞定网络协议！

你好，我是刘超，网易研究院云计算技术部的首席架构师。我主要负责两部分工作，对内支撑网易核心业务上云，对外帮助客户搞定容器化与微服务化架构。

当极客时间约我做“趣谈网络协议”专栏的时候，我非常开心，因为网络协议也是我长期研究和关注的点。摸爬滚打 15 年，有了一些收获也溅了一身血，我才能在这里和你分享。

为什么网络协议这么重要呢？为什么“计算机组成与系统结构”“数据结构与算法”“操作系统”“计算机网络”“编译原理”，会成为大学计算机的核心课程呢？至少看起来，这些内容没有“多少天搞定 MFC、Structs”这样的内容更容易帮你找到工作。我毕业的时候，也感到很困惑。

不过当时我抱着一个理想，也可能是大多数程序员的理想：我要做技术牛人，我要搞定大系统。

工作 15 年，我在 EMC 做过类似 GFS 的分布式存储开发，做过基于 Lucene 的搜索引擎，做过 Hadoop 的运维；在 HP 和华为做过 OpenStack 的开发、实施和解决方案；还创业倒腾过 Mesos 容器平台，后来在网易做 Kubernetes。

随着见过的世面越来越多，我渐渐发现，无论是对于大规模系统的架构，还是对于程序员的个人职业生涯，网络和网络协议都是绕不过去的坎儿。

集群规模一大，我们首先想到的就是网络互通的问题；应用吞吐量压不上去，我们首先想到的也是网络互通的问题。不客气地讲，很多情况下，只要搞定了网络，一个大型系统也就搞定了一半。所以，要成为技术牛人，搞定大系统，一定要过网络这一关，而网络协议在网络中占有举足轻重的地位。

相信大部分人都思考过“技术变化太快，容易过时”的问题。毕竟，技术浪潮一浪接一浪，新技术层出不穷。从搜索引擎、大数据、云计算，到人工智能、区块链，简直就是“你方唱罢我登场”。这里面究竟有没有最本质的东西，使得你掌握了它，就能在新技术的滚滚浪潮中，保持快速学习的能力？

通过对大量开源技术的代码进行分析，我发现很多技术看起来轰轰烈烈，扒下外衣，本质的东西其实就是基础知识和核心概念。想要不被滚滚而来的新技术淘汰，就要掌握这些可以长久使用的知识，而网络协议就是值得你学习，而且是到 40 岁之后依然有价值的知识。

但是，要想真正学习和掌握网络协议，也并非易事。下面这些场景，你是不是也感同身受呢？

网络协议知识点太多，学完记不住。我们都学过计算机网络课程，学的时候感觉并不难。尤其这门课没有公式，更像是文科。学了一大堆，也背了一大堆，应付完考试之后，最终都“还给老师”了。

看上去懂了，但是经不住问。没关系，网上有很多的文章嘛。于是，你会搜索很多文章去看。看的时候，你感觉别人说的很有道理，好像理解了，但是经不住问，一问就发现，你只是了解了大概的流程，很多细节还是不知道。所以说，从能看懂到能给别人讲明白，中间还有很长一段距离。

知识学会了，实际应用依旧不会。细节都摸索得差不多了，但是当你自己去应用和调试的时候，发现还是没有思路。比如，当创建出来的虚拟机不能上网的时候，该怎么办呢？学过的东西，怎么还是不会用？

我把这样的网络协议学习过程总结为：一看觉得懂，一问就打鼓，一用就糊涂。

那网络协议究竟该怎么学？基于这个问题，我决定从以下三个角度和你分享我所理解的网络协议。

第一，我会从身边经常见到的事情出发，用故事来讲解各种网络协议，然后慢慢扩展到不熟悉的领域。

例如，每个人都会查看 IP 地址，那我们就从这个命令开始，展开一些概念；很多人都在大学宿舍组过简单的网络来打游戏，我就从宿舍里最简单的网络概念开始讲；然后说到办公室，说到日常上网、购物、视频下载等过程涉及的协议；最后说到最陌生的数据中心。

第二，我会用贴近场景的方式来讲解网络协议，将各个层次的关系串起来，而非孤立地讲解某个概念。

常见的计算机网络课程往往会按照网络分层，一层一层地讲，却很少讲层与层之间的关系。例如，我们学习路由协议的时候，在真实场景中，这么多的算法和二层是什么关系呢？和四层又是什么关系呢？例如，在真实的网络通信中，我们访问一个网站，做一个支付，在 TCP 进行三次握手的时候，IP 层在干嘛？MAC 层又在干嘛？这些你是不是都清楚？

第三，我会在讲解完各个层次的网络协议之后，着重剖析如何在当下热门领域使用这些协议，比如云计算、容器和微服务。

一方面你可以知道网络协议真实应用的地方，另一方面你也可以通过上手使用云计算、容器、微服务来进一步加深对于协议的理解。

千里之行，始于足下。不管何时，我相信，扎实的功底和过硬的技术，都会是你职业发展的助力器。

希望这个专栏，不仅可以帮你理清繁杂的网络协议概念，帮你构建一个精准的网络协议知识框架，帮你在热门领域应用这些底层知识，更重要的是给你一种学习知识的方法和态度：看似最枯燥、最基础的东西往往具有最长久的生命力。

第1讲 | 为什么要学习网络协议？

《圣经》中有一个通天塔的故事，大致是说，上帝为了阻止人类联合起来，就让人类说不同的语言。人类没法儿沟通，达不成“协议”，通天塔的计划就失败了。

但是千年以后，有一种叫“程序猿”的物种，敲着一种这个群体通用的语言，连接着全世界所有的人，打造这互联网世界的通天塔。如今的世界，正是因为互联网，才连接在一起。

当 "Hello World!" 从显示器打印出来的时候，还记得你激动的心情吗？

public class HelloWorld {

  public static void main(String[] args){

    System.out.println("Hello World!");

  }

}

如果你是程序员，一定看得懂上面这一段文字。这是每一个程序员向计算机世界说“你好，世界”的方式。但是，你不一定知道，这段文字也是一种协议，是人类和计算机沟通的协议，只有通过这种协议，计算机才知道我们想让它做什么。

协议三要素

当然，这种协议还是更接近人类语言，机器不能直接读懂，需要进行翻译，翻译的工作教给编译器，也就是程序员常说的 compile。这个过程比较复杂，其中的编译原理非常复杂，我在这里不进行详述。 ﻿

但是可以看得出，计算机语言作为程序员控制一台计算机工作的协议，具备了协议的三要素。

语法，就是这一段内容要符合一定的规则和格式。例如，括号要成对，结束要使用分号等。

语义，就是这一段内容要代表某种意义。例如数字减去数字是有意义的，数字减去文本一般来说就没有意义。

顺序，就是先干啥，后干啥。例如，可以先加上某个数值，然后再减去某个数值。

会了计算机语言，你就能够教给一台计算机完成你的工作了。恭喜你，入门了！

但是，要想打造互联网世界的通天塔，只教给一台机器做什么是不够的，你需要学会教给一大片机器做什么。这就需要网络协议。只有通过网络协议，才能使一大片机器互相协作、共同完成一件事。

这个时候，你可能会问，网络协议长啥样，这么神奇，能干成啥事？我先拿一个简单的例子，让你尝尝鲜，然后再讲一个大事。

当你想要买一个商品，常规的做法就是打开浏览器，输入购物网站的地址。浏览器就会给你显示一个缤纷多彩的页面。

那你有没有深入思考过，浏览器是如何做到这件事情的？它之所以能够显示缤纷多彩的页面，是因为它收到了一段来自 HTTP 协议的“东西”。我拿网易考拉来举例，格式就像下面这样：

HTTP/1.1 200 OK

Date: Tue, 27 Mar 2018 16:50:26 GMT

Content-Type: text/html;charset=UTF-8

Content-Language: zh-CN

<!DOCTYPE html>

<html>

<head>

<base href="https://pages.kaola.com/" />

<meta charset="utf-8"/> <title> 网易考拉 3 周年主会场 </title>

这符合协议的三要素吗？我带你来看一下。

首先，符合语法，也就是说，只有按照上面那个格式来，浏览器才认。例如，上来是状态，然后是首部，然后是内容。

第二，符合语义，就是要按照约定的意思来。例如，状态 200，表述的意思是网页成功返回。如果不成功，就是我们常见的“404”。

第三，符合顺序，你一点浏览器，就是发送出一个 HTTP 请求，然后才有上面那一串 HTTP 返回的东西。

浏览器显然按照协议商定好的做了，最后一个五彩缤纷的页面就出现在你面前了。

我们常用的网络协议有哪些？

接下来揭秘我要说的大事情，“双十一”。这和我们要讲的网络协议有什么关系呢？

在经济学领域，有个伦纳德·里德（Leonard E. Read）创作的《铅笔的故事》。这个故事通过一个铅笔的诞生过程，来讲述复杂的经济学理论。这里，我也用一个下单的过程，看看互联网世界的运行过程中，都使用了哪些网络协议。

你先在浏览器里面输入 https://www.kaola.com ，这是一个URL。浏览器只知道名字是“www.kaola.com”，但是不知道具体的地点，所以不知道应该如何访问。于是，它打开地址簿去查找。可以使用一般的地址簿协议DNS去查找，还可以使用另一种更加精准的地址簿查找协议HTTPDNS。

无论用哪一种方法查找，最终都会得到这个地址：106.114.138.24。这个是IP地址，是互联网世界的“门牌号”。

知道了目标地址，浏览器就开始打包它的请求。对于普通的浏览请求，往往会使用HTTP协议；但是对于购物的请求，往往需要进行加密传输，因而会使用HTTPS协议。无论是什么协议，里面都会写明“你要买什么和买多少”。﻿

DNS、HTTP、HTTPS 所在的层我们称为应用层。经过应用层封装后，浏览器会将应用层的包交给下一层去完成，通过 socket 编程来实现。下一层是传输层。传输层有两种协议，一种是无连接的协议UDP，一种是面向连接的协议TCP。对于支付来讲，往往使用 TCP 协议。所谓的面向连接就是，TCP 会保证这个包能够到达目的地。如果不能到达，就会重新发送，直至到达。

TCP 协议里面会有两个端口，一个是浏览器监听的端口，一个是电商的服务器监听的端口。操作系统往往通过端口来判断，它得到的包应该给哪个进程。

传输层封装完毕后，浏览器会将包交给操作系统的网络层。网络层的协议是 IP 协议。在 IP 协议里面会有源 IP 地址，即浏览器所在机器的 IP 地址和目标 IP 地址，也即电商网站所在服务器的 IP 地址。

操作系统既然知道了目标 IP 地址，就开始想如何根据这个门牌号找到目标机器。操作系统往往会判断，这个目标 IP 地址是本地人，还是外地人。如果是本地人，从门牌号就能看出来，但是显然电商网站不在本地，而在遥远的地方。

操作系统知道要离开本地去远方。虽然不知道远方在何处，但是可以这样类比一下：如果去国外要去海关，去外地就要去网关。而操作系统启动的时候，就会被 DHCP 协议配置 IP 地址，以及默认的网关的 IP 地址 192.168.1.1。

操作系统如何将 IP 地址发给网关呢？在本地通信基本靠吼，于是操作系统大吼一声，谁是 192.168.1.1 啊？网关会回答它，我就是，我的本地地址在村东头。这个本地地址就是MAC地址，而大吼的那一声是ARP协议。

于是操作系统将 IP 包交给了下一层，也就是MAC 层。网卡再将包发出去。由于这个包里面是有 MAC 地址的，因而它能够到达网关。

网关收到包之后，会根据自己的知识，判断下一步应该怎么走。网关往往是一个路由器，到某个 IP 地址应该怎么走，这个叫作路由表。

路由器有点像玄奘西行路过的一个个国家的一个个城关。每个城关都连着两个国家，每个国家相当于一个局域网，在每个国家内部，都可以使用本地的地址 MAC 进行通信。

一旦跨越城关，就需要拿出 IP 头来，里面写着贫僧来自东土大唐（就是源 IP 地址），欲往西天拜佛求经（指的是目标 IP 地址）。路过宝地，借宿一晚，明日启行，请问接下来该怎么走啊？﻿

城关往往是知道这些“知识”的，因为城关和临近的城关也会经常沟通。到哪里应该怎么走，这种沟通的协议称为路由协议，常用的有OSPF和BGP。﻿

城关与城关之间是一个国家，当网络包知道了下一步去哪个城关，还是要使用国家内部的 MAC 地址，通过下一个城关的 MAC 地址，找到下一个城关，然后再问下一步的路怎么走，一直到走出最后一个城关。

最后一个城关知道这个网络包要去的地方。于是，对着这个国家吼一声，谁是目标 IP 啊？目标服务器就会回复一个 MAC 地址。网络包过关后，通过这个 MAC 地址就能找到目标服务器。

目标服务器发现 MAC 地址对上了，取下 MAC 头来，发送给操作系统的网络层。发现 IP 也对上了，就取下 IP 头。IP 头里会写上一层封装的是 TCP 协议，然后将其交给传输层，即TCP 层。

在这一层里，对于收到的每个包，都会有一个回复的包说明收到了。这个回复的包绝非这次下单请求的结果，例如购物是否成功，扣了多少钱等，而仅仅是 TCP 层的一个说明，即收到之后的回复。当然这个回复，会沿着刚才来的方向走回去，报个平安。

因为一旦出了国门，西行路上千难万险，如果在这个过程中，网络包走丢了，例如进了大沙漠，或者被强盗抢劫杀害怎么办呢？因而到了要报个平安。

如果过一段时间还是没到，发送端的 TCP 层会重新发送这个包，还是上面的过程，直到有一天收到平安到达的回复。这个重试绝非你的浏览器重新将下单这个动作重新请求一次。对于浏览器来讲，就发送了一次下单请求，TCP 层不断自己闷头重试。除非 TCP 这一层出了问题，例如连接断了，才轮到浏览器的应用层重新发送下单请求。

当网络包平安到达 TCP 层之后，TCP 头中有目标端口号，通过这个端口号，可以找到电商网站的进程正在监听这个端口号，假设一个 Tomcat，将这个包发给电商网站。﻿

电商网站的进程得到 HTTP 请求的内容，知道了要买东西，买多少。往往一个电商网站最初接待请求的这个 Tomcat 只是个接待员，负责统筹处理这个请求，而不是所有的事情都自己做。例如，这个接待员要告诉专门管理订单的进程，登记要买某个商品，买多少，要告诉管理库存的进程，库存要减少多少，要告诉支付的进程，应该付多少钱，等等。

如何告诉相关的进程呢？往往通过 RPC 调用，即远程过程调用的方式来实现。远程过程调用就是当告诉管理订单进程的时候，接待员不用关心中间的网络互连问题，会由 RPC 框架统一处理。RPC 框架有很多种，有基于 HTTP 协议放在 HTTP 的报文里面的，有直接封装在 TCP 报文里面的。

当接待员发现相应的部门都处理完毕，就回复一个 HTTPS 的包，告知下单成功。这个 HTTPS 的包，会像来的时候一样，经过千难万险到达你的个人电脑，最终进入浏览器，显示支付成功。

小结

看到了吧，一个简简单单的下单过程，中间牵扯到这么多的协议。而管理一大片机器，更是一件特别有技术含量的事情。除此之外，像最近比较火的云计算、容器、微服务等技术，也都需要借助各种协议，来达成大规模机器之间的合作。

我在这里列一下之后要讲的网络协议，之后我会按照从底层到上层的顺序来讲述。﻿

上面的“双十一”故事只是为了给你一个大致的框架，这里面有些协议，我在故事里已经提到了，有些还没有提到。在这门课的最后一章，当所有的协议都讲过之后，我会再重新讲一遍这个故事，到时候你就能明白更多的细节。

最后，学完了这一节，给你留一个问题吧。

当网络包到达一个城关的时候，可以通过路由表得到下一个城关的 IP 地址，直接通过 IP 地址找就可以了，为什么还要通过本地的 MAC 地址呢？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第2讲 | 网络分层的真实含义是什么？

长时间从事计算机网络相关的工作，我发现，计算机网络有一个显著的特点，就是这是一个不仅需要背诵，而且特别需要将原理烂熟于胸的学科。很多问题看起来懂了，但是就怕往细里问，一问就发现你懂得没有那么透彻。

我们上一节列了之后要讲的网络协议。这些协议本来没什么稀奇，每一本教科书都会讲，并且都要求你背下来。因为考试会考，面试会问。可以这么说，毕业了去找工作还答不出这类题目的，那你的笔试基本上也就挂了。

当你听到什么二层设备、三层设备、四层 LB 和七层 LB 中层的时候，是否有点一头雾水，不知道这些所谓的层，对应的各种协议具体要做什么“工作”？

这四个问题你真的懂了吗？

因为教科书或者老师往往会打一个十分不恰当的比喻：为什么网络要分层呀？因为不同的层次之间有不同的沟通方式，这个叫作协议。例如，一家公司也是分“层次”的，分总经理、经理、组长、员工。总经理之间有他们的沟通方式，经理和经理之间也有沟通方式，同理组长和员工。有没有听过类似的比喻？

那么第一个问题来了。请问经理在握手的时候，员工在干什么？很多人听过 TCP 建立连接的三次握手协议，也会把它当知识点背诵。同理问你，TCP 在进行三次握手的时候，IP 层和 MAC 层对应都有什么操作呢？

除了上面这个不恰当的比喻，教科书还会列出每个层次所包含的协议，然后开始逐层地去讲这些协议。但是这些协议之间的关系呢？却很少有教科书会讲。

学习第三层的时候会提到，IP 协议里面包含目标地址和源地址。第三层里往往还会学习路由协议。路由就像中转站，我们从原始地址 A 到目标地址 D，中间经过两个中转站 A->B->C->D，是通过路由转发的。

那么第二个问题来了。A 知道自己的下一个中转站是 B，那从 A 发出来的包，应该把 B 的 IP 地址放在哪里呢？B 知道自己的下一个中转站是 C，从 B 发出来的包，应该把 C 的 IP 地址放在哪里呢？如果放在 IP 协议中的目标地址，那包到了中转站，怎么知道最终的目的地址是 D 呢？

教科书不会通过场景化的例子，将网络包的生命周期讲出来，所以你就会很困惑，不知道这些协议实际的应用场景是什么。

我再问你一个问题。你一定经常听说二层设备、三层设备。二层设备处理的通常是 MAC 层的东西。那我发送一个 HTTP 的包，是在第七层工作的，那是不是不需要经过二层设备？或者即便经过了，二层设备也不处理呢？或者换一种问法，二层设备处理的包里，有没有 HTTP 层的内容呢？

最终，我想问你一个综合的问题。从你的电脑，通过 SSH 登录到公有云主机里面，都需要经历哪些过程？或者说你打开一个电商网站，都需要经历哪些过程？说得越详细越好。

实际情况可能是，很多人会答不上来。尽管对每一层都很熟悉，但是知识点却串不起来。

上面的这些问题，有的在这一节就会有一个解释，有的则会贯穿我们整个课程。好在后面一节中我会举一个贯穿的例子，将很多层的细节讲过后，你很容易就能把这些知识点串起来。

网络为什么要分层？

这里我们先探讨第一个问题，网络为什么要分层？因为，是个复杂的程序都要分层。

理解计算机网络中的概念，一个很好的角度是，想象网络包就是一段 Buffer，或者一块内存，是有格式的。同时，想象自己是一个处理网络包的程序，而且这个程序可以跑在电脑上，可以跑在服务器上，可以跑在交换机上，也可以跑在路由器上。你想象自己有很多的网口，从某个口拿进一个网络包来，用自己的程序处理一下，再从另一个网口发送出去。

当然网络包的格式很复杂，这个程序也很复杂。复杂的程序都要分层，这是程序设计的要求。比如，复杂的电商还会分数据库层、缓存层、Compose 层、Controller 层和接入层，每一层专注做本层的事情。

程序是如何工作的？

我们可以简单地想象“你”这个程序的工作过程。

当一个网络包从一个网口经过的时候，你看到了，首先先看看要不要请进来，处理一把。有的网口配置了混杂模式，凡是经过的，全部拿进来。

拿进来以后，就要交给一段程序来处理。于是，你调用process_layer2(buffer)。当然，这是一个假的函数。但是你明白其中的意思，知道肯定是有这么个函数的。那这个函数是干什么的呢？从 Buffer 中，摘掉二层的头，看一看，应该根据头里面的内容做什么操作。

假设你发现这个包的 MAC 地址和你的相符，那说明就是发给你的，于是需要调用process_layer3(buffer)。这个时候，Buffer 里面往往就没有二层的头了，因为已经在上一个函数的处理过程中拿掉了，或者将开始的偏移量移动了一下。在这个函数里面，摘掉三层的头，看看到底是发送给自己的，还是希望自己转发出去的。

如何判断呢？如果 IP 地址不是自己的，那就应该转发出去；如果 IP 地址是自己的，那就是发给自己的。根据 IP 头里面的标示，拿掉三层的头，进行下一层的处理，到底是调用 process_tcp(buffer) 呢，还是调用 process_udp(buffer) 呢？

假设这个地址是 TCP 的，则会调用process_tcp(buffer)。这时候，Buffer 里面没有三层的头，就需要查看四层的头，看这是一个发起，还是一个应答，又或者是一个正常的数据包，然后分别由不同的逻辑进行处理。如果是发起或者应答，接下来可能要发送一个回复包；如果是一个正常的数据包，就需要交给上层了。交给谁呢？是不是有 process_http(buffer) 函数呢？

没有的，如果你是一个网络包处理程序，你不需要有 process_http(buffer)，而是应该交给应用去处理。交给哪个应用呢？在四层的头里面有端口号，不同的应用监听不同的端口号。如果发现浏览器应用在监听这个端口，那你发给浏览器就行了。至于浏览器怎么处理，和你没有关系。

浏览器自然是解析 HTML，显示出页面来。电脑的主人看到页面很开心，就点了鼠标。点击鼠标的动作被浏览器捕获。浏览器知道，又要发起另一个 HTTP 请求了，于是使用端口号，将请求发给了你。

你应该调用send_tcp(buffer)。不用说，Buffer 里面就是 HTTP 请求的内容。这个函数里面加一个 TCP 的头，记录下源端口号。浏览器会给你目的端口号，一般为 80 端口。

然后调用send_layer3(buffer)。Buffer 里面已经有了 HTTP 的头和内容，以及 TCP 的头。在这个函数里面加一个 IP 的头，记录下源 IP 的地址和目标 IP 的地址。

然后调用send_layer2(buffer)。Buffer 里面已经有了 HTTP 的头和内容、TCP 的头，以及 IP 的头。这个函数里面要加一下 MAC 的头，记录下源 MAC 地址，得到的就是本机器的 MAC 地址和目标的 MAC 地址。不过，这个还要看当前知道不知道，知道就直接加上；不知道的话，就要通过一定的协议处理过程，找到 MAC 地址。反正要填一个，不能空着。

万事俱备，只要 Buffer 里面的内容完整，就可以从网口发出去了，你作为一个程序的任务就算告一段落了。

揭秘层与层之间的关系

知道了这个过程之后，我们再来看一下原来困惑的问题。

首先是分层的比喻。所有不能表示出层层封装含义的比喻，都是不恰当的。总经理握手，不需要员工在吧，总经理之间谈什么，不需要员工参与吧，但是网络世界不是这样的。正确的应该是，总经理之间沟通的时候，经理将总经理放在自己兜里，然后组长把经理放自己兜里，员工把组长放自己兜里，像套娃娃一样。那员工直接沟通，不带上总经理，就不恰当了。

现实生活中，往往是员工说一句，组长补充两句，然后经理补充两句，最后总经理再补充两句。但是在网络世界，应该是总经理说话，经理补充两句，组长补充两句，员工再补充两句。

那 TCP 在三次握手的时候，IP 层和 MAC 层在做什么呢？当然是 TCP 发送每一个消息，都会带着 IP 层和 MAC 层了。因为，TCP 每发送一个消息，IP 层和 MAC 层的所有机制都要运行一遍。而你只看到 TCP 三次握手了，其实，IP 层和 MAC 层为此也忙活好久了。

这里要记住一点：只要是在网络上跑的包，都是完整的。可以有下层没上层，绝对不可能有上层没下层。

所以，对 TCP 协议来说，三次握手也好，重试也好，只要想发出去包，就要有 IP 层和 MAC 层，不然是发不出去的。

经常有人会问这样一个问题，我都知道那台机器的 IP 地址了，直接发给他消息呗，要 MAC 地址干啥？这里的关键就是，没有 MAC 地址消息是发不出去的。

所以如果一个 HTTP 协议的包跑在网络上，它一定是完整的。无论这个包经过哪些设备，它都是完整的。

所谓的二层设备、三层设备，都是这些设备上跑的程序不同而已。一个 HTTP 协议的包经过一个二层设备，二层设备收进去的是整个网络包。这里面 HTTP、TCP、 IP、 MAC 都有。什么叫二层设备呀，就是只把 MAC 头摘下来，看看到底是丢弃、转发，还是自己留着。那什么叫三层设备呢？就是把 MAC 头摘下来之后，再把 IP 头摘下来，看看到底是丢弃、转发，还是自己留着。

小结

总结一下今天的内容，理解网络协议的工作模式，有两个小窍门：

始终想象自己是一个处理网络包的程序：如何拿到网络包，如何根据规则进行处理，如何发出去；

始终牢记一个原则：只要是在网络上跑的包，都是完整的。可以有下层没上层，绝对不可能有上层没下层。

最后，给你留两个思考题吧。

如果你也觉得总经理和员工的比喻不恰当，你有更恰当的比喻吗？

要想学习网络协议，IP 这个概念是最最基本的，那你知道如何查看 IP 地址吗？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第3讲 | ifconfig：最熟悉又陌生的命令行

上一节结尾给你留的一个思考题是，你知道怎么查看 IP 地址吗？

当面试听到这个问题的时候，面试者常常会觉得走错了房间。我面试的是技术岗位啊，怎么问这么简单的问题？

的确，即便没有专业学过计算机的人，只要倒腾过电脑，重装过系统，大多也会知道这个问题的答案：在 Windows 上是 ipconfig，在 Linux 上是 ifconfig。

那你知道在 Linux 上还有什么其他命令可以查看 IP 地址吗？答案是 ip addr。如果回答不上来这个问题，那你可能没怎么用过 Linux。

那你知道 ifconfig 和 ip addr 的区别吗？这是一个有关 net-tools 和 iproute2 的“历史”故事，你刚来到第三节，暂时不用了解这么细，但这也是一个常考的知识点。

想象一下，你登录进入一个被裁剪过的非常小的 Linux 系统中，发现既没有 ifconfig 命令，也没有 ip addr 命令，你是不是感觉这个系统压根儿没法用？这个时候，你可以自行安装 net-tools 和 iproute2 这两个工具。当然，大多数时候这两个命令是系统自带的。

安装好后，我们来运行一下 ip addr。不出意外，应该会输出下面的内容。

root@test:~# ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default

    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

    inet 127.0.0.1/8 scope host lo

       valid_lft forever preferred_lft forever

    inet6 ::1/128 scope host

       valid_lft forever preferred_lft forever

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000

    link/ether fa:16:3e:c7:79:75 brd ff:ff:ff:ff:ff:ff

    inet 10.100.122.2/24 brd 10.100.122.255 scope global eth0

       valid_lft forever preferred_lft forever

    inet6 fe80::f816:3eff:fec7:7975/64 scope link

       valid_lft forever preferred_lft forever

这个命令显示了这台机器上所有的网卡。大部分的网卡都会有一个 IP 地址，当然，这不是必须的。在后面的分享中，我们会遇到没有 IP 地址的情况。

IP 地址是一个网卡在网络世界的通讯地址，相当于我们现实世界的门牌号码。既然是门牌号码，不能大家都一样，不然就会起冲突。比方说，假如大家都叫六单元 1001 号，那快递就找不到地方了。所以，有时候咱们的电脑弹出网络地址冲突，出现上不去网的情况，多半是 IP 地址冲突了。

如上输出的结果，10.100.122.2 就是一个 IP 地址。这个地址被点分隔为四个部分，每个部分 8 个 bit，所以 IP 地址总共是 32 位。这样产生的 IP 地址的数量很快就不够用了。因为当时设计 IP 地址的时候，哪知道今天会有这么多的计算机啊！因为不够用，于是就有了 IPv6，也就是上面输出结果里面 inet6 fe80::f816:3eff:fec7:7975/64。这个有 128 位，现在看来是够了，但是未来的事情谁知道呢？

本来 32 位的 IP 地址就不够，还被分成了 5 类。现在想想，当时分配地址的时候，真是太奢侈了。

在网络地址中，至少在当时设计的时候，对于 A、B、 C 类主要分两部分，前面一部分是网络号，后面一部分是主机号。这很好理解，大家都是六单元 1001 号，我是小区 A 的六单元 1001 号，而你是小区 B 的六单元 1001 号。

下面这个表格，详细地展示了 A、B、C 三类地址所能包含的主机的数量。在后文中，我也会多次借助这个表格来讲解。

这里面有个尴尬的事情，就是 C 类地址能包含的最大主机数量实在太少了，只有 254 个。当时设计的时候恐怕没想到，现在估计一个网吧都不够用吧。而 B 类地址能包含的最大主机数量又太多了。6 万多台机器放在一个网络下面，一般的企业基本达不到这个规模，闲着的地址就是浪费。

无类型域间选路（CIDR）

于是有了一个折中的方式叫作无类型域间选路，简称CIDR。这种方式打破了原来设计的几类地址的做法，将 32 位的 IP 地址一分为二，前面是网络号，后面是主机号。从哪里分呢？你如果注意观察的话可以看到，10.100.122.2/24，这个 IP 地址中有一个斜杠，斜杠后面有个数字 24。这种地址表示形式，就是 CIDR。后面 24 的意思是，32 位中，前 24 位是网络号，后 8 位是主机号。

伴随着 CIDR 存在的，一个是广播地址，10.100.122.255。如果发送这个地址，所有 10.100.122 网络里面的机器都可以收到。另一个是子网掩码，255.255.255.0。

将子网掩码和 IP 地址进行 AND 计算。前面三个 255，转成二进制都是 1。1 和任何数值取 AND，都是原来数值，因而前三个数不变，为 10.100.122。后面一个 0，转换成二进制是 0，0 和任何数值取 AND，都是 0，因而最后一个数变为 0，合起来就是 10.100.122.0。这就是网络号。将子网掩码和 IP 地址按位计算 AND，就可得到网络号。

公有 IP 地址和私有 IP 地址

在日常的工作中，几乎不用划分 A 类、B 类或者 C 类，所以时间长了，很多人就忘记了这个分类，而只记得 CIDR。但是有一点还是要注意的，就是公有 IP 地址和私有 IP 地址。

我们继续看上面的表格。表格最右列是私有 IP 地址段。平时我们看到的数据中心里，办公室、家里或学校的 IP 地址，一般都是私有 IP 地址段。因为这些地址允许组织内部的 IT 人员自己管理、自己分配，而且可以重复。因此，你学校的某个私有 IP 地址段和我学校的可以是一样的。

这就像每个小区有自己的楼编号和门牌号，你们小区可以叫 6 栋，我们小区也叫 6 栋，没有任何问题。但是一旦出了小区，就需要使用公有 IP 地址。就像人民路 888 号，是国家统一分配的，不能两个小区都叫人民路 888 号。

公有 IP 地址有个组织统一分配，你需要去买。如果你搭建一个网站，给你学校的人使用，让你们学校的 IT 人员给你一个 IP 地址就行。但是假如你要做一个类似网易 163 这样的网站，就需要有公有 IP 地址，这样全世界的人才能访问。

表格中的 192.168.0.x 是最常用的私有 IP 地址。你家里有 Wi-Fi，对应就会有一个 IP 地址。一般你家里地上网设备不会超过 256 个，所以 /24 基本就够了。有时候我们也能见到 /16 的 CIDR，这两种是最常见的，也是最容易理解的。

不需要将十进制转换为二进制 32 位，就能明显看出 192.168.0 是网络号，后面是主机号。而整个网络里面的第一个地址 192.168.0.1，往往就是你这个私有网络的出口地址。例如，你家里的电脑连接 Wi-Fi，Wi-Fi 路由器的地址就是 192.168.0.1，而 192.168.0.255 就是广播地址。一旦发送这个地址，整个 192.168.0 网络里面的所有机器都能收到。

但是也不总都是这样的情况。因此，其他情况往往就会很难理解，还容易出错。

举例：一个容易“犯错”的 CIDR

我们来看 16.158.165.91/22 这个 CIDR。求一下这个网络的第一个地址、子网掩码和广播地址。

你要是上来就写 16.158.165.1，那就大错特错了。

/22 不是 8 的整数倍，不好办，只能先变成二进制来看。16.158 的部分不会动，它占了前 16 位。中间的 165，变为二进制为‭10100101‬。除了前面的 16 位，还剩 6 位。所以，这 8 位中前 6 位是网络号，16.158.<101001>，而 <01>.91 是机器号。

第一个地址是 16.158.<101001><00>.1，即 16.158.164.1。子网掩码是 255.255.<111111><00>.0，即 255.255.252.0。广播地址为 16.158.<101001><11>.255，即 16.158.167.255。

这五类地址中，还有一类 D 类是组播地址。使用这一类地址，属于某个组的机器都能收到。这有点类似在公司里面大家都加入了一个邮件组。发送邮件，加入这个组的都能收到。组播地址在后面讲述 VXLAN 协议的时候会提到。

讲了这么多，才讲了上面的输出结果中很小的一部分，是不是觉得原来并没有真的理解 ip addr 呢？我们接着来分析。

在 IP 地址的后面有个 scope，对于 eth0 这张网卡来讲，是 global，说明这张网卡是可以对外的，可以接收来自各个地方的包。对于 lo 来讲，是 host，说明这张网卡仅仅可以供本机相互通信。

lo 全称是loopback，又称环回接口，往往会被分配到 127.0.0.1 这个地址。这个地址用于本机通信，经过内核处理后直接返回，不会在任何网络中出现。

MAC 地址

在 IP 地址的上一行是 link/ether fa:16:3e:c7:79:75 brd ff:ff:ff:ff:ff:ff，这个被称为MAC 地址，是一个网卡的物理地址，用十六进制，6 个 byte 表示。

MAC 地址是一个很容易让人“误解”的地址。因为 MAC 地址号称全局唯一，不会有两个网卡有相同的 MAC 地址，而且网卡自生产出来，就带着这个地址。很多人看到这里就会想，既然这样，整个互联网的通信，全部用 MAC 地址好了，只要知道了对方的 MAC 地址，就可以把信息传过去。

这样当然是不行的。 一个网络包要从一个地方传到另一个地方，除了要有确定的地址，还需要有定位功能。 而有门牌号码属性的 IP 地址，才是有远程定位功能的。

例如，你去杭州市网商路 599 号 B 楼 6 层找刘超，你在路上问路，可能被问的人不知道 B 楼是哪个，但是可以给你指网商路怎么去。但是如果你问一个人，你知道这个身份证号的人在哪里吗？可想而知，没有人知道。

MAC 地址更像是身份证，是一个唯一的标识。它的唯一性设计是为了组网的时候，不同的网卡放在一个网络里面的时候，可以不用担心冲突。从硬件角度，保证不同的网卡有不同的标识。

MAC 地址是有一定定位功能的，只不过范围非常有限。你可以根据 IP 地址，找到杭州市网商路 599 号 B 楼 6 层，但是依然找不到我，你就可以靠吼了，大声喊身份证 XXXX 的是哪位？我听到了，我就会站起来说，是我啊。但是如果你在上海，到处喊身份证 XXXX 的是哪位，我不在现场，当然不会回答，因为我在杭州不在上海。

所以，MAC 地址的通信范围比较小，局限在一个子网里面。例如，从 192.168.0.2/24 访问 192.168.0.3/24 是可以用 MAC 地址的。一旦跨子网，即从 192.168.0.2/24 到 192.168.1.2/24，MAC 地址就不行了，需要 IP 地址起作用了。

网络设备的状态标识

解析完了 MAC 地址，我们再来看 <BROADCAST,MULTICAST,UP,LOWER_UP> 是干什么的？这个叫作net_device flags，网络设备的状态标识。

UP 表示网卡处于启动的状态；BROADCAST 表示这个网卡有广播地址，可以发送广播包；MULTICAST 表示网卡可以发送多播包；LOWER_UP 表示 L1 是启动的，也即网线插着呢。MTU1500 是指什么意思呢？是哪一层的概念呢？最大传输单元 MTU 为 1500，这是以太网的默认值。

上一节，我们讲过网络包是层层封装的。MTU 是二层 MAC 层的概念。MAC 层有 MAC 的头，以太网规定连 MAC 头带正文合起来，不允许超过 1500 个字节。正文里面有 IP 的头、TCP 的头、HTTP 的头。如果放不下，就需要分片来传输。

qdisc pfifo_fast 是什么意思呢？qdisc 全称是queueing discipline，中文叫排队规则。内核如果需要通过某个网络接口发送数据包，它都需要按照为这个接口配置的 qdisc（排队规则）把数据包加入队列。

最简单的 qdisc 是 pfifo，它不对进入的数据包做任何的处理，数据包采用先入先出的方式通过队列。pfifo_fast 稍微复杂一些，它的队列包括三个波段（band）。在每个波段里面，使用先进先出规则。

三个波段（band）的优先级也不相同。band 0 的优先级最高，band 2 的最低。如果 band 0 里面有数据包，系统就不会处理 band 1 里面的数据包，band 1 和 band 2 之间也是一样。

数据包是按照服务类型（Type of Service，TOS）被分配到三个波段（band）里面的。TOS 是 IP 头里面的一个字段，代表了当前的包是高优先级的，还是低优先级的。

队列是个好东西，后面我们讲云计算中的网络的时候，会有很多用户共享一个网络出口的情况，这个时候如何排队，每个队列有多粗，队列处理速度应该怎么提升，我都会详细为你讲解。

小结

怎么样，看起来很简单的一个命令，里面学问很大吧？通过这一节，希望你能记住以下的知识点，后面都能用得上：

IP 是地址，有定位功能；MAC 是身份证，无定位功能；

CIDR 可以用来判断是不是本地人；

IP 分公有的 IP 和私有的 IP。后面的章节中我会谈到“出国门”，就与这个有关。

最后，给你留两个思考题。

你知道 net-tools 和 iproute2 的“历史”故事吗？

这一节讲的是如何查看 IP 地址，那你知道 IP 地址是怎么来的吗？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第4讲 | DHCP与PXE：IP是怎么来的，又是怎么没的？

上一节，我们讲了 IP 的一些基本概念。如果需要和其他机器通讯，我们就需要一个通讯地址，我们需要给网卡配置这么一个地址。

如何配置 IP 地址？

那如何配置呢？如果有相关的知识和积累，你可以用命令行自己配置一个地址。可以使用 ifconfig，也可以使用 ip addr。设置好了以后，用这两个命令，将网卡 up 一下，就可以开始工作了。

使用 net-tools：

$ sudo ifconfig eth1 10.0.0.1/24

$ sudo ifconfig eth1 up

使用 iproute2：

$ sudo ip addr add 10.0.0.1/24 dev eth1

$ sudo ip link set up eth1

你可能会问了，自己配置这个自由度太大了吧，我是不是配置什么都可以？如果配置一个和谁都不搭边的地址呢？例如，旁边的机器都是 192.168.1.x，我非得配置一个 16.158.23.6，会出现什么现象呢？

不会出现任何现象，就是包发不出去呗。为什么发不出去呢？我来举例说明。

192.168.1.6 就在你这台机器的旁边，甚至是在同一个交换机上，而你把机器的地址设为了 16.158.23.6。在这台机器上，你企图去 ping192.168.1.6，你觉得只要将包发出去，同一个交换机的另一台机器马上就能收到，对不对？

可是 Linux 系统不是这样的，它没你想得那么智能。你用肉眼看到那台机器就在旁边，它则需要根据自己的逻辑进行处理。

还记得我们在第二节说过的原则吗？只要是在网络上跑的包，都是完整的，可以有下层没上层，绝对不可能有上层没下层。

所以，你看着它有自己的源 IP 地址 16.158.23.6，也有目标 IP 地址 192.168.1.6，但是包发不出去，这是因为 MAC 层还没填。

自己的 MAC 地址自己知道，这个容易。但是目标 MAC 填什么呢？是不是填 192.168.1.6 这台机器的 MAC 地址呢？

当然不是。Linux 首先会判断，要去的这个地址和我是一个网段的吗，或者和我的一个网卡是同一网段的吗？只有是一个网段的，它才会发送 ARP 请求，获取 MAC 地址。如果发现不是呢？

Linux 默认的逻辑是，如果这是一个跨网段的调用，它便不会直接将包发送到网络上，而是企图将包发送到网关。

如果你配置了网关的话，Linux 会获取网关的 MAC 地址，然后将包发出去。对于 192.168.1.6 这台机器来讲，虽然路过它家门的这个包，目标 IP 是它，但是无奈 MAC 地址不是它的，所以它的网卡是不会把包收进去的。

如果没有配置网关呢？那包压根就发不出去。

如果将网关配置为 192.168.1.6 呢？不可能，Linux 不会让你配置成功的，因为网关要和当前的网络至少一个网卡是同一个网段的，怎么可能 16.158.23.6 的网关是 192.168.1.6 呢？

所以，当你需要手动配置一台机器的网络 IP 时，一定要好好问问你的网络管理员。如果在机房里面，要去网络管理员那里申请，让他给你分配一段正确的 IP 地址。当然，真正配置的时候，一定不是直接用命令配置的，而是放在一个配置文件里面。不同系统的配置文件格式不同，但是无非就是 CIDR、子网掩码、广播地址和网关地址。

动态主机配置协议（DHCP）

原来配置 IP 有这么多门道儿啊。你可能会问了，配置了 IP 之后一般不能变的，配置一个服务端的机器还可以，但是如果是客户端的机器呢？我抱着一台笔记本电脑在公司里走来走去，或者白天来晚上走，每次使用都要配置 IP 地址，那可怎么办？还有人事、行政等非技术人员，如果公司所有的电脑都需要 IT 人员配置，肯定忙不过来啊。

因此，我们需要有一个自动配置的协议，也就是称动态主机配置协议（Dynamic Host Configuration Protocol），简称DHCP。

有了这个协议，网络管理员就轻松多了。他只需要配置一段共享的 IP 地址。每一台新接入的机器都通过 DHCP 协议，来这个共享的 IP 地址里申请，然后自动配置好就可以了。等人走了，或者用完了，还回去，这样其他的机器也能用。

所以说，如果是数据中心里面的服务器，IP 一旦配置好，基本不会变，这就相当于买房自己装修。DHCP 的方式就相当于租房。你不用装修，都是帮你配置好的。你暂时用一下，用完退租就可以了。

解析 DHCP 的工作方式

当一台机器新加入一个网络的时候，肯定一脸懵，啥情况都不知道，只知道自己的 MAC 地址。怎么办？先吼一句，我来啦，有人吗？这时候的沟通基本靠“吼”。这一步，我们称为DHCP Discover。

新来的机器使用 IP 地址 0.0.0.0 发送了一个广播包，目的 IP 地址为 255.255.255.255。广播包封装了 UDP，UDP 封装了 BOOTP。其实 DHCP 是 BOOTP 的增强版，但是如果你去抓包的话，很可能看到的名称还是 BOOTP 协议。

在这个广播包里面，新人大声喊：我是新来的（Boot request），我的 MAC 地址是这个，我还没有 IP，谁能给租给我个 IP 地址！

格式就像这样：

如果一个网络管理员在网络里面配置了DHCP Server的话，他就相当于这些 IP 的管理员。他立刻能知道来了一个“新人”。这个时候，我们可以体会 MAC 地址唯一的重要性了。当一台机器带着自己的 MAC 地址加入一个网络的时候，MAC 是它唯一的身份，如果连这个都重复了，就没办法配置了。

只有 MAC 唯一，IP 管理员才能知道这是一个新人，需要租给它一个 IP 地址，这个过程我们称为DHCP Offer。同时，DHCP Server 为此客户保留为它提供的 IP 地址，从而不会为其他 DHCP 客户分配此 IP 地址。

DHCP Offer 的格式就像这样，里面有给新人分配的地址。

DHCP Server 仍然使用广播地址作为目的地址，因为，此时请求分配 IP 的新人还没有自己的 IP。DHCP Server 回复说，我分配了一个可用的 IP 给你，你看如何？除此之外，服务器还发送了子网掩码、网关和 IP 地址租用期等信息。

新来的机器很开心，它的“吼”得到了回复，并且有人愿意租给它一个 IP 地址了，这意味着它可以在网络上立足了。当然更令人开心的是，如果有多个 DHCP Server，这台新机器会收到多个 IP 地址，简直受宠若惊。

它会选择其中一个 DHCP Offer，一般是最先到达的那个，并且会向网络发送一个 DHCP Request 广播数据包，包中包含客户端的 MAC 地址、接受的租约中的 IP 地址、提供此租约的 DHCP 服务器地址等，并告诉所有 DHCP Server 它将接受哪一台服务器提供的 IP 地址，告诉其他 DHCP 服务器，谢谢你们的接纳，并请求撤销它们提供的 IP 地址，以便提供给下一个 IP 租用请求者。

此时，由于还没有得到 DHCP Server 的最后确认，客户端仍然使用 0.0.0.0 为源 IP 地址、255.255.255.255 为目标地址进行广播。在 BOOTP 里面，接受某个 DHCP Server 的分配的 IP。

当 DHCP Server 接收到客户机的 DHCP request 之后，会广播返回给客户机一个 DHCP ACK 消息包，表明已经接受客户机的选择，并将这一 IP 地址的合法租用信息和其他的配置信息都放入该广播包，发给客户机，欢迎它加入网络大家庭。

最终租约达成的时候，还是需要广播一下，让大家都知道。

IP 地址的收回和续租

既然是租房子，就是有租期的。租期到了，管理员就要将 IP 收回。

如果不用的话，收回就收回了。就像你租房子一样，如果还要续租的话，不能到了时间再续租，而是要提前一段时间给房东说。DHCP 也是这样。

客户机会在租期过去 50% 的时候，直接向为其提供 IP 地址的 DHCP Server 发送 DHCP request 消息包。客户机接收到该服务器回应的 DHCP ACK 消息包，会根据包中所提供的新的租期以及其他已经更新的 TCP/IP 参数，更新自己的配置。这样，IP 租用更新就完成了。

好了，一切看起来完美。DHCP 协议大部分人都知道，但是其实里面隐藏着一个细节，很多人可能不会去注意。接下来，我就讲一个有意思的事情：网络管理员不仅能自动分配 IP 地址，还能帮你自动安装操作系统！

预启动执行环境（PXE）

普通的笔记本电脑，一般不会有这种需求。因为你拿到电脑时，就已经有操作系统了，即便你自己重装操作系统，也不是很麻烦的事情。但是，在数据中心里就不一样了。数据中心里面的管理员可能一下子就拿到几百台空的机器，一个个安装操作系统，会累死的。

所以管理员希望的不仅仅是自动分配 IP 地址，还要自动安装系统。装好系统之后自动分配 IP 地址，直接启动就能用了，这样当然最好了！

这事儿其实仔细一想，还是挺有难度的。安装操作系统，应该有个光盘吧。数据中心里不能用光盘吧，想了一个办法就是，可以将光盘里面要安装的操作系统放在一个服务器上，让客户端去下载。但是客户端放在哪里呢？它怎么知道去哪个服务器上下载呢？客户端总得安装在一个操作系统上呀，可是这个客户端本来就是用来安装操作系统的呀？

其实，这个过程和操作系统启动的过程有点儿像。首先，启动 BIOS。这是一个特别小的小系统，只能干特别小的一件事情。其实就是读取硬盘的 MBR 启动扇区，将 GRUB 启动起来；然后将权力交给 GRUB，GRUB 加载内核、加载作为根文件系统的 initramfs 文件；然后将权力交给内核；最后内核启动，初始化整个操作系统。

那我们安装操作系统的过程，只能插在 BIOS 启动之后了。因为没安装系统之前，连启动扇区都没有。因而这个过程叫做预启动执行环境（Pre-boot Execution Environment），简称PXE。

PXE 协议分为客户端和服务器端，由于还没有操作系统，只能先把客户端放在 BIOS 里面。当计算机启动时，BIOS 把 PXE 客户端调入内存里面，就可以连接到服务端做一些操作了。

首先，PXE 客户端自己也需要有个 IP 地址。因为 PXE 的客户端启动起来，就可以发送一个 DHCP 的请求，让 DHCP Server 给它分配一个地址。PXE 客户端有了自己的地址，那它怎么知道 PXE 服务器在哪里呢？对于其他的协议，都好办，要么人告诉他。例如，告诉浏览器要访问的 IP 地址，或者在配置中告诉它；例如，微服务之间的相互调用。

但是 PXE 客户端启动的时候，啥都没有。好在 DHCP Server 除了分配 IP 地址以外，还可以做一些其他的事情。这里有一个 DHCP Server 的一个样例配置：

ddns-update-style interim;

ignore client-updates;

allow booting;

allow bootp;

subnet 192.168.1.0 netmask 255.255.255.0

{

option routers 192.168.1.1;

option subnet-mask 255.255.255.0;

option time-offset -18000;

default-lease-time 21600;

max-lease-time 43200;

range dynamic-bootp 192.168.1.240 192.168.1.250;

filename "pxelinux.0";

next-server 192.168.1.180;

}

按照上面的原理，默认的 DHCP Server 是需要配置的，无非是我们配置 IP 的时候所需要的 IP 地址段、子网掩码、网关地址、租期等。如果想使用 PXE，则需要配置 next-server，指向 PXE 服务器的地址，另外要配置初始启动文件 filename。

这样 PXE 客户端启动之后，发送 DHCP 请求之后，除了能得到一个 IP 地址，还可以知道 PXE 服务器在哪里，也可以知道如何从 PXE 服务器上下载某个文件，去初始化操作系统。

解析 PXE 的工作过程

接下来我们来详细看一下 PXE 的工作过程。

首先，启动 PXE 客户端。第一步是通过 DHCP 协议告诉 DHCP Server，我刚来，一穷二白，啥都没有。DHCP Server 便租给它一个 IP 地址，同时也给它 PXE 服务器的地址、启动文件 pxelinux.0。

其次，PXE 客户端知道要去 PXE 服务器下载这个文件后，就可以初始化机器。于是便开始下载，下载的时候使用的是 TFTP 协议。所以 PXE 服务器上，往往还需要有一个 TFTP 服务器。PXE 客户端向 TFTP 服务器请求下载这个文件，TFTP 服务器说好啊，于是就将这个文件传给它。

然后，PXE 客户端收到这个文件后，就开始执行这个文件。这个文件会指示 PXE 客户端，向 TFTP 服务器请求计算机的配置信息 pxelinux.cfg。TFTP 服务器会给 PXE 客户端一个配置文件，里面会说内核在哪里、initramfs 在哪里。PXE 客户端会请求这些文件。

最后，启动 Linux 内核。一旦启动了操作系统，以后就啥都好办了。

小结

好了，这一节就到这里了。我来总结一下今天的内容：

DHCP 协议主要是用来给客户租用 IP 地址，和房产中介很像，要商谈、签约、续租，广播还不能“抢单”；

DHCP 协议能给客户推荐“装修队”PXE，能够安装操作系统，这个在云计算领域大有用处。

最后，学完了这一节，给你留两个思考题吧。

PXE 协议可以用来安装操作系统，但是如果每次重启都安装操作系统，就会很麻烦。你知道如何使得第一次安装操作系统，后面就正常启动吗？

现在上网很简单了，买个家用路由器，连上 WIFI，给 DHCP 分配一个 IP 地址，就可以上网了。那你是否用过更原始的方法自己组过简单的网呢？说来听听。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第5讲 | 从物理层到MAC层：如何在宿舍里自己组网玩联机游戏？

上一节，我们见证了 IP 地址的诞生，或者说是整个操作系统的诞生。一旦机器有了 IP，就可以在网络的环境里和其他的机器展开沟通了。

故事就从我的大学宿舍开始讲起吧。作为一个八零后，我要暴露年龄了。

我们宿舍四个人，大一的时候学校不让上网，不给开通网络。但是，宿舍有一个人比较有钱，率先买了一台电脑。那买了电脑干什么呢？

首先，有单机游戏可以打，比如说《拳皇》。两个人用一个键盘，照样打得火热。后来有第二个人买了电脑，那两台电脑能不能连接起来呢？你会说，当然能啊，买个路由器不就行了。

现在一台家用路由器非常便宜，一百多块的事情。那时候路由器绝对是奢侈品。一直到大四，我们宿舍都没有买路由器。可能是因为那时候技术没有现在这么发达，导致我对网络技术的认知是逐渐深入的，而且每一层都是实实在在接触到的。

第一层（物理层）

使用路由器，是在第三层上。我们先从第一层物理层开始说。

物理层能折腾啥？现在的同学可能想不到，我们当时去学校配电脑的地方买网线，卖网线的师傅都会问，你的网线是要电脑连电脑啊，还是电脑连网口啊？

我们要的是电脑连电脑。这种方式就是一根网线，有两个头。一头插在一台电脑的网卡上，另一头插在另一台电脑的网卡上。但是在当时，普通的网线这样是通不了的，所以水晶头要做交叉线，用的就是所谓的1－3、2－6 交叉接法。

水晶头的第 1、2 和第 3、6 脚，它们分别起着收、发信号的作用。将一端的 1 号和 3 号线、2 号和 6 号线互换一下位置，就能够在物理层实现一端发送的信号，另一端能收到。

当然电脑连电脑，除了网线要交叉，还需要配置这两台电脑的 IP 地址、子网掩码和默认网关。这三个概念上一节详细描述过了。要想两台电脑能够通信，这三项必须配置成为一个网络，可以一个是 192.168.0.1/24，另一个是 192.168.0.2/24，否则是不通的。

这里我想问你一个问题，两台电脑之间的网络包，包含 MAC 层吗？当然包含，要完整。IP 层要封装了 MAC 层才能将包放入物理层。

到此为止，两台电脑已经构成了一个最小的局域网，也即LAN。可以玩联机局域网游戏啦！

等到第三个哥们也买了一台电脑，怎么把三台电脑连在一起呢？

先别说交换机，当时交换机也贵。有一个叫作Hub的东西，也就是集线器。这种设备有多个口，可以将宿舍里的多台电脑连接起来。但是，和交换机不同，集线器没有大脑，它完全在物理层工作。它会将自己收到的每一个字节，都复制到其他端口上去。这是第一层物理层联通的方案。

第二层（数据链路层）

你可能已经发现问题了。Hub 采取的是广播的模式，如果每一台电脑发出的包，宿舍的每个电脑都能收到，那就麻烦了。这就需要解决几个问题：

这个包是发给谁的？谁应该接收？

大家都在发，会不会产生混乱？有没有谁先发、谁后发的规则？

如果发送的时候出现了错误，怎么办？

这几个问题，都是第二层，数据链路层，也即 MAC 层要解决的问题。MAC的全称是Medium Access Control，即媒体访问控制。控制什么呢？其实就是控制在往媒体上发数据的时候，谁先发、谁后发的问题。防止发生混乱。这解决的是第二个问题。这个问题中的规则，学名叫多路访问。有很多算法可以解决这个问题。就像车管所管束马路上跑的车，能想的办法都想过了。

比如接下来这三种方式：

方式一：分多个车道。每个车一个车道，你走你的，我走我的。这在计算机网络里叫作信道划分；

方式二：今天单号出行，明天双号出行，轮着来。这在计算机网络里叫作轮流协议；

方式三：不管三七二十一，有事儿先出门，发现特堵，就回去。错过高峰再出。我们叫作随机接入协议。著名的以太网，用的就是这个方式。

解决了第二个问题，就是解决了媒体接入控制的问题，MAC 的问题也就解决好了。这和 MAC 地址没什么关系。

接下来要解决第一个问题：发给谁，谁接收？这里用到一个物理地址，叫作链路层地址。但是因为第二层主要解决媒体接入控制的问题，所以它常被称为MAC 地址。

解决第一个问题就牵扯到第二层的网络包格式。对于以太网，第二层的最开始，就是目标的 MAC 地址和源的 MAC 地址。

接下来是类型，大部分的类型是 IP 数据包，然后 IP 里面包含 TCP、UDP，以及 HTTP 等，这都是里层封装的事情。

有了这个目标 MAC 地址，数据包在链路上广播，MAC 的网卡才能发现，这个包是给它的。MAC 的网卡把包收进来，然后打开 IP 包，发现 IP 地址也是自己的，再打开 TCP 包，发现端口是自己，也就是 80，而 nginx 就是监听 80。

于是将请求提交给 nginx，nginx 返回一个网页。然后将网页需要发回请求的机器。然后层层封装，最后到 MAC 层。因为来的时候有源 MAC 地址，返回的时候，源 MAC 就变成了目标 MAC，再返给请求的机器。

对于以太网，第二层的最后面是CRC，也就是循环冗余检测。通过 XOR 异或的算法，来计算整个包是否在发送的过程中出现了错误，主要解决第三个问题。

这里还有一个没有解决的问题，当源机器知道目标机器的时候，可以将目标地址放入包里面，如果不知道呢？一个广播的网络里面接入了 N 台机器，我怎么知道每个 MAC 地址是谁呢？这就是ARP 协议，也就是已知 IP 地址，求 MAC 地址的协议。

在一个局域网里面，当知道了 IP 地址，不知道 MAC 怎么办呢？靠“吼”。

广而告之，发送一个广播包，谁是这个 IP 谁来回答。具体询问和回答的报文就像下面这样：

为了避免每次都用 ARP 请求，机器本地也会进行 ARP 缓存。当然机器会不断地上线下线，IP 也可能会变，所以 ARP 的 MAC 地址缓存过一段时间就会过期。

局域网

好了，至此我们宿舍四个电脑就组成了一个局域网。用 Hub 连接起来，就可以玩局域网版的《魔兽争霸》了。

打开游戏，进入“局域网选项”，选择一张地图，点击“创建游戏”，就可以进入这张地图的房间中。等同一个局域网里的其他小伙伴加入后，游戏就可以开始了。

这种组网的方法，对一个宿舍来说没有问题，但是一旦机器数目增多，问题就出现了。因为 Hub 是广播的，不管某个接口是否需要，所有的 Bit 都会被发送出去，然后让主机来判断是不是需要。这种方式路上的车少就没问题，车一多，产生冲突的概率就提高了。而且把不需要的包转发过去，纯属浪费。看来 Hub 这种不管三七二十一都转发的设备是不行了，需要点儿智能的。因为每个口都只连接一台电脑，这台电脑又不怎么换 IP 和 MAC 地址，只要记住这台电脑的 MAC 地址，如果目标 MAC 地址不是这台电脑的，这个口就不用转发了。

谁能知道目标 MAC 地址是否就是连接某个口的电脑的 MAC 地址呢？这就需要一个能把 MAC 头拿下来，检查一下目标 MAC 地址，然后根据策略转发的设备，按第二节课中讲过的，这个设备显然是个二层设备，我们称为交换机。

交换机怎么知道每个口的电脑的 MAC 地址呢？这需要交换机会学习。

一台 MAC1 电脑将一个包发送给另一台 MAC2 电脑，当这个包到达交换机的时候，一开始交换机也不知道 MAC2 的电脑在哪个口，所以没办法，它只能将包转发给除了来的那个口之外的其他所有的口。但是，这个时候，交换机会干一件非常聪明的事情，就是交换机会记住，MAC1 是来自一个明确的口。以后有包的目的地址是 MAC1 的，直接发送到这个口就可以了。

当交换机作为一个关卡一样，过了一段时间之后，就有了整个网络的一个结构了，这个时候，基本上不用广播了，全部可以准确转发。当然，每个机器的 IP 地址会变，所在的口也会变，因而交换机上的学习的结果，我们称为转发表，是有一个过期时间的。

有了交换机，一般来说，你接个几十台、上百台机器打游戏，应该没啥问题。你可以组个战队了。能上网了，就可以玩网游了。

这里，给你推荐一个课程，极客时间新上线了《从 0 开始学游戏开发》，由原网易游戏引擎架构师、资深底层技术专家蔡能老师，手把手带你梳理游戏开发的流程和细节，为你剖析热门游戏的成功之道。帮助普通程序员成为游戏开发工程师，步入游戏开发之路。你可以点击文末的图片进入课程。

小结

好了，今天的内容差不多了，我们来总结一下，有三个重点需要你记住：

第一，MAC 层是用来解决多路访问的堵车问题的；

第二，ARP 是通过吼的方式来寻找目标 MAC 地址的，吼完之后记住一段时间，这个叫作缓存；

第三，交换机是有 MAC 地址学习能力的，学完了它就知道谁在哪儿了，不用广播了。

最后，给你留两个思考题吧。

在二层中我们讲了 ARP 协议，即已知 IP 地址求 MAC；还有一种 RARP 协议，即已知 MAC 求 IP 的，你知道它可以用来干什么吗？

如果一个局域网里面有多个交换机，ARP 广播的模式会出现什么问题呢？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第6讲 | 交换机与VLAN：办公室太复杂，我要回学校

上一次，我们在宿舍里组建了一个本地的局域网 LAN，可以愉快地玩游戏了。这是一个非常简单的场景，因为只有一台交换机，电脑数目很少。今天，让我们切换到一个稍微复杂一点的场景，办公室。

拓扑结构是怎么形成的？

我们常见到的办公室大多是一排排的桌子，每个桌子都有网口，一排十几个座位就有十几个网口，一个楼层就会有几十个甚至上百个网口。如果算上所有楼层，这个场景自然比你宿舍里的复杂多了。具体哪里复杂呢？我来给你具体讲解。

首先，这个时候，一个交换机肯定不够用，需要多台交换机，交换机之间连接起来，就形成一个稍微复杂的拓扑结构。

我们先来看两台交换机的情形。两台交换机连接着三个局域网，每个局域网上都有多台机器。如果机器 1 只知道机器 4 的 IP 地址，当它想要访问机器 4，把包发出去的时候，它必须要知道机器 4 的 MAC 地址。

于是机器 1 发起广播，机器 2 收到这个广播，但是这不是找它的，所以没它什么事。交换机 A 一开始是不知道任何拓扑信息的，在它收到这个广播后，采取的策略是，除了广播包来的方向外，它还要转发给其他所有的网口。于是机器 3 也收到广播信息了，但是这和它也没什么关系。

当然，交换机 B 也是能够收到广播信息的，但是这时候它也是不知道任何拓扑信息的，因而也是进行广播的策略，将包转发到局域网三。这个时候，机器 4 和机器 5 都收到了广播信息。机器 4 主动响应说，这是找我的，这是我的 MAC 地址。于是一个 ARP 请求就成功完成了。

在上面的过程中，交换机 A 和交换机 B 都是能够学习到这样的信息：机器 1 是在左边这个网口的。当了解到这些拓扑信息之后，情况就好转起来。当机器 2 要访问机器 1 的时候，机器 2 并不知道机器 1 的 MAC 地址，所以机器 2 会发起一个 ARP 请求。这个广播消息会到达机器 1，也同时会到达交换机 A。这个时候交换机 A 已经知道机器 1 是不可能在右边的网口的，所以这个广播信息就不会广播到局域网二和局域网三。

当机器 3 要访问机器 1 的时候，也需要发起一个广播的 ARP 请求。这个时候交换机 A 和交换机 B 都能够收到这个广播请求。交换机 A 当然知道主机 A 是在左边这个网口的，所以会把广播消息转发到局域网一。同时，交换机 B 收到这个广播消息之后，由于它知道机器 1 是不在右边这个网口的，所以不会将消息广播到局域网三。

如何解决常见的环路问题？

这样看起来，两台交换机工作得非常好。随着办公室越来越大，交换机数目肯定越来越多。当整个拓扑结构复杂了，这么多网线，绕过来绕过去，不可避免地会出现一些意料不到的情况。其中常见的问题就是环路问题。

例如这个图，当两个交换机将两个局域网同时连接起来的时候。你可能会觉得，这样反而有了高可用性。但是却不幸地出现了环路。出现了环路会有什么结果呢？

我们来想象一下机器 1 访问机器 2 的过程。一开始，机器 1 并不知道机器 2 的 MAC 地址，所以它需要发起一个 ARP 的广播。广播到达机器 2，机器 2 会把 MAC 地址返回来，看起来没有这两个交换机什么事情。

但是问题来了，这两个交换机还是都能够收到广播包的。交换机 A 一开始是不知道机器 2 在哪个局域网的，所以它会把广播消息放到局域网二，在局域网二广播的时候，交换机 B 右边这个网口也是能够收到广播消息的。交换机 B 会将这个广播息信息发送到局域网一。局域网一的这个广播消息，又会到达交换机 A 左边的这个接口。交换机 A 这个时候还是不知道机器 2 在哪个局域网，于是将广播包又转发到局域网二。左转左转左转，好像是个圈哦。

可能有人会说，当两台交换机都能够逐渐学习到拓扑结构之后，是不是就可以了？

别想了，压根儿学不会的。机器 1 的广播包到达交换机 A 和交换机 B 的时候，本来两个交换机都学会了机器 1 是在局域网一的，但是当交换机 A 将包广播到局域网二之后，交换机 B 右边的网口收到了来自交换机 A 的广播包。根据学习机制，这彻底损坏了交换机 B 的三观，刚才机器 1 还在左边的网口呢，怎么又出现在右边的网口呢？哦，那肯定是机器 1 换位置了，于是就误会了，交换机 B 就学会了，机器 1 是从右边这个网口来的，把刚才学习的那一条清理掉。同理，交换机 A 右边的网口，也能收到交换机 B 转发过来的广播包，同样也误会了，于是也学会了，机器 1 从右边的网口来，不是从左边的网口来。

然而当广播包从左边的局域网一广播的时候，两个交换机再次刷新三观，原来机器 1 是在左边的，过一会儿，又发现不对，是在右边的，过一会，又发现不对，是在左边的。

这还是一个包转来转去，每台机器都会发广播包，交换机转发也会复制广播包，当广播包越来越多的时候，按照上一节讲过一个共享道路的算法，也就是路会越来越堵，最后谁也别想走。所以，必须有一个方法解决环路的问题，怎么破除环路呢？

STP 协议中那些难以理解的概念

在数据结构中，有一个方法叫作最小生成树。有环的我们常称为图。将图中的环破了，就生成了树。在计算机网络中，生成树的算法叫作STP，全称Spanning Tree Protocol。

STP 协议比较复杂，一开始很难看懂，但是其实这是一场血雨腥风的武林比武或者华山论剑，最终决出五岳盟主的方式。

在 STP 协议里面有很多概念，译名就非常拗口，但是我一作比喻，你很容易就明白了。

Root Bridge，也就是根交换机。这个比较容易理解，可以比喻为“掌门”交换机，是某棵树的老大，是掌门，最大的大哥。

Designated Bridges，有的翻译为指定交换机。这个比较难理解，可以想像成一个“小弟”，对于树来说，就是一棵树的树枝。所谓“指定”的意思是，我拜谁做大哥，其他交换机通过这个交换机到达根交换机，也就相当于拜他做了大哥。这里注意是树枝，不是叶子，因为叶子往往是主机。

Bridge Protocol Data Units （BPDU） ，网桥协议数据单元。可以比喻为“相互比较实力”的协议。行走江湖，比的就是武功，拼的就是实力。当两个交换机碰见的时候，也就是相连的时候，就需要互相比一比内力了。BPDU 只有掌门能发，已经隶属于某个掌门的交换机只能传达掌门的指示。

Priority Vector，优先级向量。可以比喻为实力 （值越小越牛）。实力是啥？就是一组 ID 数目，[Root Bridge ID, Root Path Cost, Bridge ID, and Port ID]。为什么这样设计呢？这是因为要看怎么来比实力。先看 Root Bridge ID。拿出老大的 ID 看看，发现掌门一样，那就是师兄弟；再比 Root Path Cost，也即我距离我的老大的距离，也就是拿和掌门关系比，看同一个门派内谁和老大关系铁；最后比 Bridge ID，比我自己的 ID，拿自己的本事比。

STP 的工作过程是怎样的？

接下来，我们来看 STP 的工作过程。

一开始，江湖纷争，异常混乱。大家都觉得自己是掌门，谁也不服谁。于是，所有的交换机都认为自己是掌门，每个网桥都被分配了一个 ID。这个 ID 里有管理员分配的优先级，当然网络管理员知道哪些交换机贵，哪些交换机好，就会给它们分配高的优先级。这种交换机生下来武功就很高，起步就是乔峰。

既然都是掌门，互相都连着网线，就互相发送 BPDU 来比功夫呗。这一比就发现，有人是岳不群，有人是封不平，赢的接着当掌门，输的就只好做小弟了。当掌门的还会继续发 BPDU，而输的人就没有机会了。它们只有在收到掌门发的 BPDU 的时候，转发一下，表示服从命令。

数字表示优先级。就像这个图，5 和 6 碰见了，6 的优先级低，所以乖乖做小弟。于是一个小门派形成，5 是掌门，6 是小弟。其他诸如 1-7、2-8、3-4 这样的小门派，也诞生了。于是江湖出现了很多小的门派，小的门派，接着合并。

合并的过程会出现以下四种情形，我分别来介绍。

情形一：掌门遇到掌门

当 5 碰到了 1，掌门碰见掌门，1 觉得自己是掌门，5 也刚刚跟别人 PK 完成为掌门。这俩掌门比较功夫，最终 1 胜出。于是输掉的掌门 5 就会率领所有的小弟归顺。结果就是 1 成为大掌门。

情形二：同门相遇

同门相遇可以是掌门与自己的小弟相遇，这说明存在“环”了。这个小弟已经通过其他门路拜在你门下，结果你还不认识，就 PK 了一把。结果掌门发现这个小弟功夫不错，不应该级别这么低，就把它招到门下亲自带，那这个小弟就相当于升职了。

我们再来看，假如 1 和 6 相遇。6 原来就拜在 1 的门下，只不过 6 的上司是 5，5 的上司是 1。1 发现，6 距离我才只有 2，比从 5 这里过来的 5（=4+1）近多了，那 6 就直接汇报给我吧。于是，5 和 6 分别汇报给 1。

同门相遇还可以是小弟相遇。这个时候就要比较谁和掌门的关系近，当然近的当大哥。刚才 5 和 6 同时汇报给 1 了，后来 5 和 6 再比较功夫的时候发现，5 你直接汇报给 1 距离是 4，如果 5 汇报给 6 再汇报给 1，距离只有 2+1=3，所以 5 干脆拜 6 为上司。

情形三：掌门与其他帮派小弟相遇

小弟拿本帮掌门和这个掌门比较，赢了，这个掌门拜入门来。输了，会拜入新掌门，并且逐渐拉拢和自己连接的兄弟，一起弃暗投明。

例如，2 和 7 相遇，虽然 7 是小弟，2 是掌门。就个人武功而言，2 比 7 强，但是 7 的掌门是 1，比 2 牛，所以没办法，2 要拜入 7 的门派，并且连同自己的小弟都一起拜入。

情形四：不同门小弟相遇

各自拿掌门比较，输了的拜入赢的门派，并且逐渐将与自己连接的兄弟弃暗投明。

例如，5 和 4 相遇。虽然 4 的武功好于 5，但是 5 的掌门是 1，比 4 牛，于是 4 拜入 5 的门派。后来当 3 和 4 相遇的时候，3 发现 4 已经叛变了，4 说我现在老大是 1，比你牛，要不你也来吧，于是 3 也拜入 1。

最终，生成一棵树，武林一统，天下太平。但是天下大势，分久必合，合久必分，天下统一久了，也会有相应的问题。

如何解决广播问题和安全问题？

毕竟机器多了，交换机也多了，就算交换机比 Hub 智能一些，但是还是难免有广播的问题，一大波机器，相关的部门、不相关的部门，广播一大堆，性能就下来了。就像一家公司，创业的时候，一二十个人，坐在一个会议室，有事情大家讨论一下，非常方便。但是如果变成了 50 个人，全在一个会议室里面吵吵，就会乱的不得了。

你们公司有不同的部门，有的部门需要保密的，比如人事部门，肯定要讨论升职加薪的事儿。由于在同一个广播域里面，很多包都会在一个局域网里面飘啊飘，碰到了一个会抓包的程序员，就能抓到这些包，如果没有加密，就能看到这些敏感信息了。还是上面的例子，50 个人在一个会议室里面七嘴八舌的讨论，其中有两个 HR，那他们讨论的问题，肯定被其他人偷偷听走了。

那咋办，分部门，分会议室呗。那我们就来看看怎么分。

有两种分的方法，一个是物理隔离。每个部门设一个单独的会议室，对应到网络方面，就是每个部门有单独的交换机，配置单独的子网，这样部门之间的沟通就需要路由器了。路由器咱们还没讲到，以后再说。这样的问题在于，有的部门人多，有的部门人少。人少的部门慢慢人会变多，人多的部门也可能人越变越少。如果每个部门有单独的交换机，口多了浪费，少了又不够用。

另外一种方式是虚拟隔离，就是用我们常说的VLAN，或者叫虚拟局域网。使用 VLAN，一个交换机上会连属于多个局域网的机器，那交换机怎么区分哪个机器属于哪个局域网呢？

我们只需要在原来的二层的头上加一个 TAG，里面有一个 VLAN ID，一共 12 位。为什么是 12 位呢？因为 12 位可以划分 4096 个 VLAN。这样是不是还不够啊。现在的情况证明，目前云计算厂商里面绝对不止 4096 个用户。当然每个用户需要一个 VLAN 了啊，怎么办呢，这个我们在后面的章节再说。

如果我们买的交换机是支持 VLAN 的，当这个交换机把二层的头取下来的时候，就能够识别这个 VLAN ID。这样只有相同 VLAN 的包，才会互相转发，不同 VLAN 的包，是看不到的。这样广播问题和安全问题就都能够解决了。

我们可以设置交换机每个口所属的 VLAN。如果某个口坐的是程序员，他们属于 VLAN 10；如果某个口坐的是人事，他们属于 VLAN 20；如果某个口坐的是财务，他们属于 VLAN 30。这样，财务发的包，交换机只会转发到 VLAN 30 的口上。程序员啊，你就监听 VLAN 10 吧，里面除了代码，啥都没有。

而且对于交换机来讲，每个 VLAN 的口都是可以重新设置的。一个财务走了，把他所在的作为的口从 VLAN 30 移除掉，来了一个程序员，坐在财务的位置上，就把这个口设置为 VLAN 10，十分灵活。

有人会问交换机之间怎么连接呢？将两个交换机连接起来的口应该设置成什么 VLAN 呢？对于支持 VLAN 的交换机，有一种口叫作Trunk 口。它可以转发属于任何 VLAN 的口。交换机之间可以通过这种口相互连接。

好了，解决这么多交换机连接在一起的问题，办公室的问题似乎搞定了。然而这只是一般复杂的场景，因为你能接触到的网络，到目前为止，不管是你的台式机，还是笔记本所连接的网络，对于带宽、高可用等都要求不高。就算出了问题，一会儿上不了网，也不会有什么大事。

我们在宿舍、学校或者办公室，经常会访问一些网站，这些网站似乎永远不会“挂掉”。那是因为这些网站都生活在一个叫做数据中心的地方，那里的网络世界更加复杂。在后面的章节，我会为你详细讲解。

小结

好了，这节就到这里，我们这里来总结一下：

当交换机的数目越来越多的时候，会遭遇环路问题，让网络包迷路，这就需要使用 STP 协议，通过华山论剑比武的方式，将有环路的图变成没有环路的树，从而解决环路问题。

交换机数目多会面临隔离问题，可以通过 VLAN 形成虚拟局域网，从而解决广播问题和安全问题。

最后，给你留两个思考题。

STP 协议能够很好的解决环路问题，但是也有它的缺点，你能举几个例子吗？

在一个比较大的网络中，如果两台机器不通，你知道应该用什么方式调试吗？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第7讲 | ICMP与ping：投石问路的侦察兵

无论是在宿舍，还是在办公室，或者运维一个数据中心，我们常常会遇到网络不通的问题。那台机器明明就在那里，你甚至都可以通过机器的终端连上去看。它看着好好的，可是就是连不上去，究竟是哪里出了问题呢？

ICMP 协议的格式

一般情况下，你会想到 ping 一下。那你知道 ping 是如何工作的吗？

ping 是基于 ICMP 协议工作的。ICMP全称Internet Control Message Protocol，就是互联网控制报文协议。这里面的关键词是“控制”，那具体是怎么控制的呢？

网络包在异常复杂的网络环境中传输时，常常会遇到各种各样的问题。当遇到问题的时候，总不能“死个不明不白”，要传出消息来，报告情况，这样才可以调整传输策略。这就相当于我们经常看到的电视剧里，古代行军的时候，为将为帅者需要通过侦察兵、哨探或传令兵等人肉的方式来掌握情况，控制整个战局。

ICMP 报文是封装在 IP 包里面的。因为传输指令的时候，肯定需要源地址和目标地址。它本身非常简单。因为作为侦查兵，要轻装上阵，不能携带大量的包袱。

ICMP 报文有很多的类型，不同的类型有不同的代码。最常用的类型是主动请求为 8，主动请求的应答为 0。

查询报文类型

我们经常在电视剧里听到这样的话：主帅说，来人哪！前方战事如何，快去派人打探，一有情况，立即通报！

这种是主帅发起的，主动查看敌情，对应 ICMP 的查询报文类型。例如，常用的ping 就是查询报文，是一种主动请求，并且获得主动应答的 ICMP 协议。所以，ping 发的包也是符合 ICMP 协议格式的，只不过它在后面增加了自己的格式。

对 ping 的主动请求，进行网络抓包，称为ICMP ECHO REQUEST。同理主动请求的回复，称为ICMP ECHO REPLY。比起原生的 ICMP，这里面多了两个字段，一个是标识符。这个很好理解，你派出去两队侦查兵，一队是侦查战况的，一队是去查找水源的，要有个标识才能区分。另一个是序号，你派出去的侦查兵，都要编个号。如果派出去 10 个，回来 10 个，就说明前方战况不错；如果派出去 10 个，回来 2 个，说明情况可能不妙。

在选项数据中，ping 还会存放发送请求的时间值，来计算往返时间，说明路程的长短。

差错报文类型

当然也有另外一种方式，就是差错报文。

主帅骑马走着走着，突然来了一匹快马，上面的小兵气喘吁吁的：报告主公，不好啦！张将军遭遇埋伏，全军覆没啦！这种是异常情况发起的，来报告发生了不好的事情，对应 ICMP 的差错报文类型。

我举几个 ICMP 差错报文的例子：终点不可达为 3，源抑制为 4，超时为 11，重定向为 5。这些都是什么意思呢？我给你具体解释一下。

第一种是终点不可达。小兵：报告主公，您让把粮草送到张将军那里，结果没有送到。

如果你是主公，你肯定会问，为啥送不到？具体的原因在代码中表示就是，网络不可达代码为 0，主机不可达代码为 1，协议不可达代码为 2，端口不可达代码为 3，需要进行分片但设置了不分片位代码为 4。

具体的场景就像这样：

网络不可达：主公，找不到地方呀？

主机不可达：主公，找到地方没这个人呀？

协议不可达：主公，找到地方，找到人，口号没对上，人家天王盖地虎，我说 12345！

端口不可达：主公，找到地方，找到人，对了口号，事儿没对上，我去送粮草，人家说他们在等救兵。

需要进行分片但设置了不分片位：主公，走到一半，山路狭窄，想换小车，但是您的将令，严禁换小车，就没办法送到了。

第二种是源站抑制，也就是让源站放慢发送速度。小兵：报告主公，您粮草送的太多了吃不完。

第三种是时间超时，也就是超过网络包的生存时间还是没到。小兵：报告主公，送粮草的人，自己把粮草吃完了，还没找到地方，已经饿死啦。

第四种是路由重定向，也就是让下次发给另一个路由器。小兵：报告主公，上次送粮草的人本来只要走一站地铁，非得从五环绕，下次别这样了啊。

差错报文的结构相对复杂一些。除了前面还是 IP，ICMP 的前 8 字节不变，后面则跟上出错的那个 IP 包的 IP 头和 IP 正文的前 8 个字节。

而且这类侦查兵特别恪尽职守，不但自己返回来报信，还把一部分遗物也带回来。

侦察兵：报告主公，张将军已经战死沙场，这是张将军的印信和佩剑。

主公：神马？张将军是怎么死的（可以查看 ICMP 的前 8 字节）？没错，这是张将军的剑，是他的剑（IP 数据包的头及正文前 8 字节）。

ping：查询报文类型的使用

接下来，我们重点来看 ping 的发送和接收过程。

假定主机 A 的 IP 地址是 192.168.1.1，主机 B 的 IP 地址是 192.168.1.2，它们都在同一个子网。那当你在主机 A 上运行“ping 192.168.1.2”后，会发生什么呢?

ping 命令执行的时候，源主机首先会构建一个 ICMP 请求数据包，ICMP 数据包内包含多个字段。最重要的是两个，第一个是类型字段，对于请求数据包而言该字段为 8；另外一个是顺序号，主要用于区分连续 ping 的时候发出的多个数据包。每发出一个请求数据包，顺序号会自动加 1。为了能够计算往返时间 RTT，它会在报文的数据部分插入发送时间。

然后，由 ICMP 协议将这个数据包连同地址 192.168.1.2 一起交给 IP 层。IP 层将以 192.168.1.2 作为目的地址，本机 IP 地址作为源地址，加上一些其他控制信息，构建一个 IP 数据包。

接下来，需要加入 MAC 头。如果在本节 ARP 映射表中查找出 IP 地址 192.168.1.2 所对应的 MAC 地址，则可以直接使用；如果没有，则需要发送 ARP 协议查询 MAC 地址，获得 MAC 地址后，由数据链路层构建一个数据帧，目的地址是 IP 层传过来的 MAC 地址，源地址则是本机的 MAC 地址；还要附加上一些控制信息，依据以太网的介质访问规则，将它们传送出去。

主机 B 收到这个数据帧后，先检查它的目的 MAC 地址，并和本机的 MAC 地址对比，如符合，则接收，否则就丢弃。接收后检查该数据帧，将 IP 数据包从帧中提取出来，交给本机的 IP 层。同样，IP 层检查后，将有用的信息提取后交给 ICMP 协议。

主机 B 会构建一个 ICMP 应答包，应答数据包的类型字段为 0，顺序号为接收到的请求数据包中的顺序号，然后再发送出去给主机 A。

在规定的时候间内，源主机如果没有接到 ICMP 的应答包，则说明目标主机不可达；如果接收到了 ICMP 应答包，则说明目标主机可达。此时，源主机会检查，用当前时刻减去该数据包最初从源主机上发出的时刻，就是 ICMP 数据包的时间延迟。

当然这只是最简单的，同一个局域网里面的情况。如果跨网段的话，还会涉及网关的转发、路由器的转发等等。但是对于 ICMP 的头来讲，是没什么影响的。会影响的是根据目标 IP 地址，选择路由的下一跳，还有每经过一个路由器到达一个新的局域网，需要换 MAC 头里面的 MAC 地址。这个过程后面几节会详细描述，这里暂时不多说。

如果在自己的可控范围之内，当遇到网络不通的问题的时候，除了直接 ping 目标的 IP 地址之外，还应该有一个清晰的网络拓扑图。并且从理论上来讲，应该要清楚地知道一个网络包从源地址到目标地址都需要经过哪些设备，然后逐个 ping 中间的这些设备或者机器。如果可能的话，在这些关键点，通过 tcpdump -i eth0 icmp，查看包有没有到达某个点，回复的包到达了哪个点，可以更加容易推断出错的位置。

经常会遇到一个问题，如果不在我们的控制范围内，很多中间设备都是禁止 ping 的，但是 ping 不通不代表网络不通。这个时候就要使用 telnet，通过其他协议来测试网络是否通，这个就不在本篇的讲述范围了。

说了这么多，你应该可以看出 ping 这个程序是使用了 ICMP 里面的 ECHO REQUEST 和 ECHO REPLY 类型的。

Traceroute：差错报文类型的使用

那其他的类型呢？是不是只有真正遇到错误的时候，才能收到呢？那也不是，有一个程序 Traceroute，是个“大骗子”。它会使用 ICMP 的规则，故意制造一些能够产生错误的场景。

所以，Traceroute 的第一个作用就是故意设置特殊的 TTL，来追踪去往目的地时沿途经过的路由器。Traceroute 的参数指向某个目的 IP 地址，它会发送一个 UDP 的数据包。将 TTL 设置成 1，也就是说一旦遇到一个路由器或者一个关卡，就表示它“牺牲”了。

如果中间的路由器不止一个，当然碰到第一个就“牺牲”。于是，返回一个 ICMP 包，也就是网络差错包，类型是时间超时。那大军前行就带一顿饭，试一试走多远会被饿死，然后找个哨探回来报告，那我就知道大军只带一顿饭能走多远了。

接下来，将 TTL 设置为 2。第一关过了，第二关就“牺牲”了，那我就知道第二关有多远。如此反复，直到到达目的主机。这样，Traceroute 就拿到了所有的路由器 IP。当然，有的路由器压根不会回这个 ICMP。这也是 Traceroute 一个公网的地址，看不到中间路由的原因。

怎么知道 UDP 有没有到达目的主机呢？Traceroute 程序会发送一份 UDP 数据报给目的主机，但它会选择一个不可能的值作为 UDP 端口号（大于 30000）。当该数据报到达时，将使目的主机的 UDP 模块产生一份“端口不可达”错误 ICMP 报文。如果数据报没有到达，则可能是超时。

这就相当于故意派人去西天如来那里去请一本《道德经》，结果人家信佛不信道，消息就会被打出来。被打的消息传回来，你就知道西天是能够到达的。为什么不去取《心经》呢？因为 UDP 是无连接的。也就是说这人一派出去，你就得不到任何音信。你无法区别到底是半路走丢了，还是真的信佛遁入空门了，只有让人家打出来，你才会得到消息。

Traceroute 还有一个作用是故意设置不分片，从而确定路径的 MTU。要做的工作首先是发送分组，并设置“不分片”标志。发送的第一个分组的长度正好与出口 MTU 相等。如果中间遇到窄的关口会被卡住，会发送 ICMP 网络差错包，类型为“需要进行分片但设置了不分片位”。其实，这是人家故意的好吧，每次收到 ICMP“不能分片”差错时就减小分组的长度，直到到达目标主机。

小结

好了，这一节内容差不多了，我来总结一下：

ICMP 相当于网络世界的侦察兵。我讲了两种类型的 ICMP 报文，一种是主动探查的查询报文，一种异常报告的差错报文；

ping 使用查询报文，Traceroute 使用差错报文。

最后，给你留两个思考题吧。

当发送的报文出问题的时候，会发送一个 ICMP 的差错报文来报告错误，但是如果 ICMP 的差错报文也出问题了呢？

这一节只说了一个局域网互相 ping 的情况。如果跨路由器、跨网关的过程会是什么样的呢？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第8讲 | 世界这么大，我想出网关：欧洲十国游与玄奘西行

前几节，我主要跟你讲了宿舍里和办公室里用到的网络协议。你已经有了一些基础，是时候去外网逛逛了！

怎么在宿舍上网？

还记得咱们在宿舍的时候买了台交换机，几台机器组了一个局域网打游戏吗？可惜啊，只能打局域网的游戏，不能上网啊！盼啊盼啊，终于盼到大二，允许宿舍开通网络了。学校给每个宿舍的网口分配了一个 IP 地址。这个 IP 是校园网的 IP，完全由网管部门控制。宿舍网的 IP 地址多为 192.168.1.x。校园网的 IP 地址，假设是 10.10.x.x。

这个时候，你要在宿舍上网，有两个办法：

第一个办法，让你们宿舍长再买一个网卡。这个时候，你们宿舍长的电脑里就有两张网卡。一张网卡的线插到你们宿舍的交换机上，另一张网卡的线插到校园网的网口。而且，这张新的网卡的 IP 地址要按照学校网管部门分配的配置，不然上不了网。这种情况下，如果你们宿舍的人要上网，就需要一直开着宿舍长的电脑。

第二个办法，你们共同出钱买个家庭路由器（反正当时我们买不起）。家庭路由器会有内网网口和外网网口。把外网网口的线插到校园网的网口上，将这个外网网口配置成和网管部的一样。内网网口连上你们宿舍的所有的电脑。这种情况下，如果你们宿舍的人要上网，就需要一直开着路由器。

这两种方法其实是一样的。只不过第一种方式，让你的宿舍长的电脑，变成一个有多个口的路由器而已。而你买的家庭路由器，里面也跑着程序，和你宿舍长电脑里的功能一样，只不过是一个嵌入式的系统。

当你的宿舍长能够上网之后，接下来，就是其他人的电脑怎么上网的问题。这就需要配置你们的网卡。当然 DHCP 是可以默认配置的。在进行网卡配置的时候，除了 IP 地址，还需要配置一个Gateway的东西，这个就是网关。

你了解 MAC 头和 IP 头的细节吗？

一旦配置了 IP 地址和网关，往往就能够指定目标地址进行访问了。由于在跨网关访问的时候，牵扯到 MAC 地址和 IP 地址的变化，这里有必要详细描述一下 MAC 头和 IP 头的细节。

在 MAC 头里面，先是目标 MAC 地址，然后是源 MAC 地址，然后有一个协议类型，用来说明里面是 IP 协议。IP 头里面的版本号，目前主流的还是 IPv4，服务类型 TOS 在第三节讲 ip addr 命令的时候讲过，TTL 在第 7 节讲 ICMP 协议的时候讲过。另外，还有 8 位标识协议。这里到了下一层的协议，也就是，是 TCP 还是 UDP。最重要的就是源 IP 和目标 IP。先是源 IP 地址，然后是目标 IP 地址。

在任何一台机器上，当要访问另一个 IP 地址的时候，都会先判断，这个目标 IP 地址，和当前机器的 IP 地址，是否在同一个网段。怎么判断同一个网段呢？需要 CIDR 和子网掩码，这个在第三节的时候也讲过了。

如果是同一个网段，例如，你访问你旁边的兄弟的电脑，那就没网关什么事情，直接将源地址和目标地址放入 IP 头中，然后通过 ARP 获得 MAC 地址，将源 MAC 和目的 MAC 放入 MAC 头中，发出去就可以了。

如果不是同一网段，例如，你要访问你们校园网里面的 BBS，该怎么办？这就需要发往默认网关 Gateway。Gateway 的地址一定是和源 IP 地址是一个网段的。往往不是第一个，就是第二个。例如 192.168.1.0/24 这个网段，Gateway 往往会是 192.168.1.1/24 或者 192.168.1.2/24。

如何发往默认网关呢？网关不是和源 IP 地址是一个网段的么？这个过程就和发往同一个网段的其他机器是一样的：将源地址和目标 IP 地址放入 IP 头中，通过 ARP 获得网关的 MAC 地址，将源 MAC 和网关的 MAC 放入 MAC 头中，发送出去。网关所在的端口，例如 192.168.1.1/24 将网络包收进来，然后接下来怎么做，就完全看网关的了。

网关往往是一个路由器，是一个三层转发的设备。啥叫三层设备？前面也说过了，就是把 MAC 头和 IP 头都取下来，然后根据里面的内容，看看接下来把包往哪里转发的设备。

在你的宿舍里面，网关就是你宿舍长的电脑。一个路由器往往有多个网口，如果是一台服务器做这个事情，则就有多个网卡，其中一个网卡是和源 IP 同网段的。

很多情况下，人们把网关就叫作路由器。其实不完全准确，而另一种比喻更加恰当：路由器是一台设备，它有五个网口或者网卡，相当于有五只手，分别连着五个局域网。每只手的 IP 地址都和局域网的 IP 地址相同的网段，每只手都是它握住的那个局域网的网关。

任何一个想发往其他局域网的包，都会到达其中一只手，被拿进来，拿下 MAC 头和 IP 头，看看，根据自己的路由算法，选择另一只手，加上 IP 头和 MAC 头，然后扔出去。

静态路由是什么？

这个时候，问题来了，该选择哪一只手？IP 头和 MAC 头加什么内容，哪些变、哪些不变呢？这个问题比较复杂，大致可以分为两类，一个是静态路由，一个是动态路由。动态路由下一节我们详细地讲。这一节我们先说静态路由。

静态路由，其实就是在路由器上，配置一条一条规则。这些规则包括：想访问 BBS 站（它肯定有个网段），从 2 号口出去，下一跳是 IP2；想访问教学视频站（它也有个自己的网段），从 3 号口出去，下一跳是 IP3，然后保存在路由器里。

每当要选择从哪只手抛出去的时候，就一条一条的匹配规则，找到符合的规则，就按规则中设置的那样，从某个口抛出去，找下一跳 IPX。

IP 头和 MAC 头哪些变、哪些不变？

对于 IP 头和 MAC 头哪些变、哪些不变的问题，可以分两种类型。我把它们称为“欧洲十国游”型和“玄奘西行”型。

之前我说过，MAC 地址是一个局域网内才有效的地址。因而，MAC 地址只要过网关，就必定会改变，因为已经换了局域网。两者主要的区别在于 IP 地址是否改变。不改变 IP 地址的网关，我们称为转发网关；改变 IP 地址的网关，我们称为NAT 网关。

“欧洲十国游”型

结合这个图，我们先来看“欧洲十国游”型。

服务器 A 要访问服务器 B。首先，服务器 A 会思考，192.168.4.101 和我不是一个网段的，因而需要先发给网关。那网关是谁呢？已经静态配置好了，网关是 192.168.1.1。网关的 MAC 地址是多少呢？发送 ARP 获取网关的 MAC 地址，然后发送包。包的内容是这样的：

源 MAC：服务器 A 的 MAC

目标 MAC：192.168.1.1 这个网口的 MAC

源 IP：192.168.1.101

目标 IP：192.168.4.101

包到达 192.168.1.1 这个网口，发现 MAC 一致，将包收进来，开始思考往哪里转发。

在路由器 A 中配置了静态路由之后，要想访问 192.168.4.0/24，要从 192.168.56.1 这个口出去，下一跳为 192.168.56.2。

于是，路由器 A 思考的时候，匹配上了这条路由，要从 192.168.56.1 这个口发出去，发给 192.168.56.2，那 192.168.56.2 的 MAC 地址是多少呢？路由器 A 发送 ARP 获取 192.168.56.2 的 MAC 地址，然后发送包。包的内容是这样的：

源 MAC：192.168.56.1 的 MAC 地址

目标 MAC：192.168.56.2 的 MAC 地址

源 IP：192.168.1.101

目标 IP：192.168.4.101

包到达 192.168.56.2 这个网口，发现 MAC 一致，将包收进来，开始思考往哪里转发。

在路由器 B 中配置了静态路由，要想访问 192.168.4.0/24，要从 192.168.4.1 这个口出去，没有下一跳了。因为我右手这个网卡，就是这个网段的，我是最后一跳了。

于是，路由器 B 思考的时候，匹配上了这条路由，要从 192.168.4.1 这个口发出去，发给 192.168.4.101。那 192.168.4.101 的 MAC 地址是多少呢？路由器 B 发送 ARP 获取 192.168.4.101 的 MAC 地址，然后发送包。包的内容是这样的：

源 MAC：192.168.4.1 的 MAC 地址

目标 MAC：192.168.4.101 的 MAC 地址

源 IP：192.168.1.101

目标 IP：192.168.4.101

包到达服务器 B，MAC 地址匹配，将包收进来。

通过这个过程可以看出，每到一个新的局域网，MAC 都是要变的，但是 IP 地址都不变。在 IP 头里面，不会保存任何网关的 IP 地址。所谓的下一跳是，某个 IP 要将这个 IP 地址转换为 MAC 放入 MAC 头。

之所以将这种模式比喻称为欧洲十国游，是因为在整个过程中，IP 头里面的地址都是不变的。IP 地址在三个局域网都可见，在三个局域网之间的网段都不会冲突。在三个网段之间传输包，IP 头不改变。这就像在欧洲各国之间旅游，一个签证就能搞定。 ﻿﻿

“玄奘西行”型

我们再来看“玄奘西行”型。

这里遇见的第一个问题是，局域网之间没有商量过，各定各的网段，因而 IP 段冲突了。最左面大唐的地址是 192.168.1.101，最右面印度的地址也是 192.168.1.101，如果单从 IP 地址上看，简直是自己访问自己，其实是大唐的 192.168.1.101 要访问印度的 192.168.1.101。

怎么解决这个问题呢？既然局域网之间没有商量过，你们各管各的，那到国际上，也即中间的局域网里面，就需要使用另外的地址。就像出国，不能用咱们自己的身份证，而要改用护照一样，玄奘西游也要拿着专门取经的通关文牒，而不能用自己国家的身份证。

首先，目标服务器 B 在国际上要有一个国际的身份，我们给它一个 192.168.56.2。在网关 B 上，我们记下来，国际身份 192.168.56.2 对应国内身份 192.168.1.101。凡是要访问 192.168.56.2，都转成 192.168.1.101。

于是，源服务器 A 要访问目标服务器 B，要指定的目标地址为 192.168.56.2。这是它的国际身份。服务器 A 想，192.168.56.2 和我不是一个网段的，因而需要发给网关，网关是谁？已经静态配置好了，网关是 192.168.1.1，网关的 MAC 地址是多少？发送 ARP 获取网关的 MAC 地址，然后发送包。包的内容是这样的：

源 MAC：服务器 A 的 MAC

目标 MAC：192.168.1.1 这个网口的 MAC

源 IP：192.168.1.101

目标 IP：192.168.56.2

包到达 192.168.1.1 这个网口，发现 MAC 一致，将包收进来，开始思考往哪里转发。

在路由器 A 中配置了静态路由：要想访问 192.168.56.2/24，要从 192.168.56.1 这个口出去，没有下一跳了，因为我右手这个网卡，就是这个网段的，我是最后一跳了。

于是，路由器 A 思考的时候，匹配上了这条路由，要从 192.168.56.1 这个口发出去，发给 192.168.56.2。那 192.168.56.2 的 MAC 地址是多少呢？路由器 A 发送 ARP 获取 192.168.56.2 的 MAC 地址。

当网络包发送到中间的局域网的时候，服务器 A 也需要有个国际身份，因而在国际上，源 IP 地址也不能用 192.168.1.101，需要改成 192.168.56.1。发送包的内容是这样的：

源 MAC：192.168.56.1 的 MAC 地址

目标 MAC：192.168.56.2 的 MAC 地址

源 IP：192.168.56.1

目标 IP：192.168.56.2

包到达 192.168.56.2 这个网口，发现 MAC 一致，将包收进来，开始思考往哪里转发。

路由器 B 是一个 NAT 网关，它上面配置了，要访问国际身份 192.168.56.2 对应国内身份 192.168.1.101，于是改为访问 192.168.1.101。

在路由器 B 中配置了静态路由：要想访问 192.168.1.0/24，要从 192.168.1.1 这个口出去，没有下一跳了，因为我右手这个网卡，就是这个网段的，我是最后一跳了。

于是，路由器 B 思考的时候，匹配上了这条路由，要从 192.168.1.1 这个口发出去，发给 192.168.1.101。

那 192.168.1.101 的 MAC 地址是多少呢？路由器 B 发送 ARP 获取 192.168.1.101 的 MAC 地址，然后发送包。内容是这样的：

源 MAC：192.168.1.1 的 MAC 地址

目标 MAC：192.168.1.101 的 MAC 地址

源 IP：192.168.56.1

目标 IP：192.168.1.101

包到达服务器 B，MAC 地址匹配，将包收进来。

从服务器 B 接收的包可以看出，源 IP 为服务器 A 的国际身份，因而发送返回包的时候，也发给这个国际身份，由路由器 A 做 NAT，转换为国内身份。

从这个过程可以看出，IP 地址也会变。这个过程用英文说就是Network Address Translation，简称NAT。

其实这第二种方式我们经常见，现在大家每家都有家用路由器，家里的网段都是 192.168.1.x，所以你肯定访问不了你邻居家的这个私网的 IP 地址的。所以，当我们家里的包发出去的时候，都被家用路由器 NAT 成为了运营商的地址了。

很多办公室访问外网的时候，也是被 NAT 过的，因为不可能办公室里面的 IP 也是公网可见的，公网地址实在是太贵了，所以一般就是整个办公室共用一个到两个出口 IP 地址。你可以通过 https://www.whatismyip.com/ 查看自己的出口 IP 地址。

小结

好了，这一节内容差不多了，我来总结一下：

如果离开本局域网，就需要经过网关，网关是路由器的一个网口；

路由器是一个三层设备，里面有如何寻找下一跳的规则；

经过路由器之后 MAC 头要变，如果 IP 不变，相当于不换护照的欧洲旅游，如果 IP 变，相当于换护照的玄奘西行。

最后，给你留两个思考题吧。

当在你家里要访问 163 网站的时候，你的包需要 NAT 成为公网 IP，返回的包又要 NAT 成你的私有 IP，返回包怎么知道这是你的请求呢？它怎么就这么智能的 NAT 成了你的 IP 而非别人的 IP 呢？

对于路由规则，这一节讲述了静态路由，需要手动配置，如果要自动配置，你觉得应该怎么办呢？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第9讲 | 路由协议：西出网关无故人，敢问路在何方

俗话说得好，在家千日好，出门一日难。网络包一旦出了网关，就像玄奘西行一样踏上了江湖漂泊的路。

上一节我们描述的是一个相对简单的情形。出了网关之后，只有一条路可以走。但是，网络世界复杂得多，一旦出了网关，会面临着很多路由器，有很多条道路可以选。如何选择一个更快速的道路求取真经呢？这里面还有很多门道可以讲。

如何配置路由？

通过上一节的内容，你应该已经知道，路由器就是一台网络设备，它有多张网卡。当一个入口的网络包送到路由器时，它会根据一个本地的转发信息库，来决定如何正确地转发流量。这个转发信息库通常被称为路由表。

一张路由表中会有多条路由规则。每一条规则至少包含这三项信息。

目的网络：这个包想去哪儿？

出口设备：将包从哪个口扔出去？

下一跳网关：下一个路由器的地址。

通过 route 命令和 ip route 命令都可以进行查询或者配置。

例如，我们设置 ip route add 10.176.48.0/20 via 10.173.32.1 dev eth0，就说明要去 10.176.48.0/20 这个目标网络，要从 eth0 端口出去，经过 10.173.32.1。

上一节的例子中，网关上的路由策略就是按照这三项配置信息进行配置的。这种配置方式的一个核心思想是：根据目的 IP 地址来配置路由。

如何配置策略路由？

当然，在真实的复杂的网络环境中，除了可以根据目的 ip 地址配置路由外，还可以根据多个参数来配置路由，这就称为策略路由。

可以配置多个路由表，可以根据源 IP 地址、入口设备、TOS 等选择路由表，然后在路由表中查找路由。这样可以使得来自不同来源的包走不同的路由。

例如，我们设置：

ip rule add from 192.168.1.0/24 table 10

ip rule add from 192.168.2.0/24 table 20

表示从 192.168.1.10/24 这个网段来的，使用 table 10 中的路由表，而从 192.168.2.0/24 网段来的，使用 table20 的路由表。

在一条路由规则中，也可以走多条路径。例如，在下面的路由规则中：

ip route add default scope global nexthop via 100.100.100.1 weight 1 nexthop via 200.200.200.1 weight 2

下一跳有两个地方，分别是 100.100.100.1 和 200.200.200.1，权重分别为 1 比 2。

在什么情况下会用到如此复杂的配置呢？我来举一个现实中的例子。

我是房东，家里从运营商那儿拉了两根网线。这两根网线分别属于两个运行商。一个带宽大一些，一个带宽小一些。这个时候，我就不能买普通的家用路由器了，得买个高级点的，可以接两个外网的。

家里的网络呢，就是普通的家用网段 192.168.1.x/24。家里有两个租户，分别把线连到路由器上。IP 地址为 192.168.1.101/24 和 192.168.1.102/24，网关都是 192.168.1.1/24，网关在路由器上。

就像上一节说的一样，家里的网段是私有网段，出去的包需要 NAT 成公网的 IP 地址，因而路由器是一个 NAT 路由器。

两个运营商都要为这个网关配置一个公网的 IP 地址。如果你去查看你们家路由器里的网段，基本就是我图中画的样子。

运行商里面也有一个 IP 地址，在运营商网络里面的网关。不同的运营商方法不一样，有的是 /32 的，也即一个一对一连接。

例如，运营商 1 给路由器分配的地址是 183.134.189.34/32，而运营商网络里面的网关是 183.134.188.1/32。有的是 /30 的，也就是分了一个特别小的网段。运营商 2 给路由器分配的地址是 60.190.27.190/30，运营商网络里面的网关是 60.190.27.189/30。

根据这个网络拓扑图，可以将路由配置成这样：

$ ip route list table main

60.190.27.189/30 dev eth3  proto kernel  scope link  src 60.190.27.190

183.134.188.1 dev eth2  proto kernel  scope link  src 183.134.189.34

192.168.1.0/24 dev eth1  proto kernel  scope link  src 192.168.1.1

127.0.0.0/8 dev lo  scope link

default via 183.134.188.1 dev eth2

当路由这样配置的时候，就告诉这个路由器如下的规则：

如果去运营商二，就走 eth3；

如果去运营商一呢，就走 eth2；

如果访问内网，就走 eth1；

如果所有的规则都匹配不上，默认走运营商一，也即走快的网络。

但是问题来了，租户 A 不想多付钱，他说我就上上网页，从不看电影，凭什么收我同样贵的网费啊？没关系，咱有技术可以解决。

下面我添加一个 Table，名字叫chao。

# echo 200 chao >> /etc/iproute2/rt_tables

添加一条规则：

# ip rule add from 192.168.1.101 table chao

# ip rule ls

0:    from all lookup local

32765:    from 10.0.0.10 lookup chao

32766:    from all lookup main

32767:    from all lookup default

设定规则为：从 192.168.1.101 来的包都查看个 chao 这个新的路由表。

在 chao 路由表中添加规则：

# ip route add default via 60.190.27.189 dev eth3 table chao

# ip route flush cache

默认的路由走慢的，谁让你不付钱。

上面说的都是静态的路由，一般来说网络环境简单的时候，在自己的可控范围之内，自己捣鼓还是可以的。但是有时候网络环境复杂并且多变，如果总是用静态路由，一旦网络结构发生变化，让网络管理员手工修改路由太复杂了，因而需要动态路由算法。

动态路由算法

使用动态路由路由器，可以根据路由协议算法生成动态路由表，随网络运行状况的变化而变化。那路由算法是什么样的呢？

我们可以想象唐僧西天取经，需要解决两大问题，一个是在每个国家如何找到正确的路，去换通关文牒、吃饭、休息；一个是在国家之间，野外行走的时候，如何找到正确的路、水源的问题。

无论是一个国家内部，还是国家之间，我们都可以将复杂的路径，抽象为一种叫作图的数据结构。至于唐僧西行取经，肯定想走得路越少越好，道路越短越好，因而这就转化成为如何在途中找到最短路径的问题。

咱们在大学里面学习计算机网络与数据结构的时候，知道求最短路径常用的有两种方法，一种是 Bellman-Ford 算法，一种是 Dijkstra 算法。在计算机网络中基本也是用这两种方法计算的。

1. 距离矢量路由算法

第一大类的算法称为距离矢量路由（distance vector routing）。它是基于 Bellman-Ford 算法的。

这种算法的基本思路是，每个路由器都保存一个路由表，包含多行，每行对应网络中的一个路由器，每一行包含两部分信息，一个是要到目标路由器，从那条线出去，另一个是到目标路由器的距离。

由此可以看出，每个路由器都是知道全局信息的。那这个信息如何更新呢？每个路由器都知道自己和邻居之间的距离，每过几秒，每个路由器都将自己所知的到达所有的路由器的距离告知邻居，每个路由器也能从邻居那里得到相似的信息。

每个路由器根据新收集的信息，计算和其他路由器的距离，比如自己的一个邻居距离目标路由器的距离是 M，而自己距离邻居是 x，则自己距离目标路由器是 x+M。

这个算法比较简单，但是还是有问题。

第一个问题就是好消息传得快，坏消息传得慢。如果有个路由器加入了这个网络，它的邻居就能很快发现它，然后将消息广播出去。要不了多久，整个网络就都知道了。但是一旦一个路由器挂了，挂的消息是没有广播的。当每个路由器发现原来的道路到不了这个路由器的时候，感觉不到它已经挂了，而是试图通过其他的路径访问，直到试过了所有的路径，才发现这个路由器是真的挂了。

我再举个例子。

原来的网络包括两个节点，B 和 C。A 加入了网络，它的邻居 B 很快就发现 A 启动起来了。于是它将自己和 A 的距离设为 1，同样 C 也发现 A 起来了，将自己和 A 的距离设置为 2。但是如果 A 挂掉，情况就不妙了。B 本来和 A 是邻居，发现连不上 A 了，但是 C 还是能够连上，只不过距离远了点，是 2，于是将自己的距离设置为 3。殊不知 C 的距离 2 其实是基于原来自己的距离为 1 计算出来的。C 发现自己也连不上 A，并且发现 B 设置为 3，于是自己改成距离 4。依次类推，数越来越大，直到超过一个阈值，我们才能判定 A 真的挂了。

这个道理有点像有人走丢了。当你突然发现找不到这个人了。于是你去学校问，是不是在他姨家呀？找到他姨家，他姨说，是不是在他舅舅家呀？他舅舅说，是不是在他姥姥家呀？他姥姥说，是不是在学校呀？总归要问一圈，或者是超过一定的时间，大家才会认为这个人的确走丢了。如果这个人其实只是去见了一个谁都不认识的网友去了，当这个人回来的时候，只要他随便见到其中的一个亲戚，这个亲戚就会拉着他到他的家长那里，说你赶紧回家，你妈都找你一天了。

这种算法的第二个问题是，每次发送的时候，要发送整个全局路由表。网络大了，谁也受不了，所以最早的路由协议 RIP 就是这个算法。它适用于小型网络（小于 15 跳）。当网络规模都小的时候，没有问题。现在一个数据中心内部路由器数目就很多，因而不适用了。

所以上面的两个问题，限制了距离矢量路由的网络规模。

2. 链路状态路由算法

第二大类算法是链路状态路由（link state routing），基于 Dijkstra 算法。

这种算法的基本思路是：当一个路由器启动的时候，首先是发现邻居，向邻居 say hello，邻居都回复。然后计算和邻居的距离，发送一个 echo，要求马上返回，除以二就是距离。然后将自己和邻居之间的链路状态包广播出去，发送到整个网络的每个路由器。这样每个路由器都能够收到它和邻居之间的关系的信息。因而，每个路由器都能在自己本地构建一个完整的图，然后针对这个图使用 Dijkstra 算法，找到两点之间的最短路径。

不像距离距离矢量路由协议那样，更新时发送整个路由表。链路状态路由协议只广播更新的或改变的网络拓扑，这使得更新信息更小，节省了带宽和 CPU 利用率。而且一旦一个路由器挂了，它的邻居都会广播这个消息，可以使得坏消息迅速收敛。

动态路由协议

1. 基于链路状态路由算法的 OSPF

OSPF（Open Shortest Path First，开放式最短路径优先）就是这样一个基于链路状态路由协议，广泛应用在数据中心中的协议。由于主要用在数据中心内部，用于路由决策，因而称为内部网关协议（Interior Gateway Protocol，简称IGP）。

内部网关协议的重点就是找到最短的路径。在一个组织内部，路径最短往往最优。当然有时候 OSPF 可以发现多个最短的路径，可以在这多个路径中进行负载均衡，这常常被称为等价路由。

这一点非常重要。有了等价路由，到一个地方去可以有相同的两个路线，可以分摊流量，还可以当一条路不通的时候，走另外一条路。这个在后面我们讲数据中心的网络的时候，一般应用的接入层会有负载均衡 LVS。它可以和 OSPF 一起，实现高吞吐量的接入层设计。

有了内网的路由协议，在一个国家内，唐僧可以想怎么走怎么走了，两条路选一条也行。

2. 基于距离矢量路由算法的 BGP

但是外网的路由协议，也即国家之间的，又有所不同。我们称为外网路由协议（Border Gateway Protocol，简称BGP）。

在一个国家内部，有路当然选近的走。但是国家之间，不光远近的问题，还有政策的问题。例如，唐僧去西天取经，有的路近。但是路过的国家看不惯僧人，见了僧人就抓。例如灭法国，连光头都要抓。这样的情况即便路近，也最好绕远点走。

对于网络包同样，每个数据中心都设置自己的 Policy。例如，哪些外部的 IP 可以让内部知晓，哪些内部的 IP 可以让外部知晓，哪些可以通过，哪些不能通过。这就好比，虽然从我家里到目的地最近，但是不能谁都能从我家走啊！

在网络世界，这一个个国家成为自治系统AS（Autonomous System）。自治系统分几种类型。

Stub AS：对外只有一个连接。这类 AS 不会传输其他 AS 的包。例如，个人或者小公司的网络。

Multihomed AS：可能有多个连接连到其他的 AS，但是大多拒绝帮其他的 AS 传输包。例如一些大公司的网络。

Transit AS：有多个连接连到其他的 AS，并且可以帮助其他的 AS 传输包。例如主干网。

每个自治系统都有边界路由器，通过它和外面的世界建立联系。

BGP 又分为两类，eBGP 和 iBGP。自治系统间，边界路由器之间使用 eBGP 广播路由。内部网络也需要访问其他的自治系统。边界路由器如何将 BGP 学习到的路由导入到内部网络呢？就是通过运行 iBGP，使得内部的路由器能够找到到达外网目的地的最好的边界路由器。

BGP 协议使用的算法是路径矢量路由协议（path-vector protocol）。它是距离矢量路由协议的升级版。

前面说了距离矢量路由协议的缺点。其中一个是收敛慢。在 BGP 里面，除了下一跳 hop 之外，还包括了自治系统 AS 的路径，从而可以避免坏消息传的慢的问题，也即上面所描述的，B 知道 C 原来能够到达 A，是因为通过自己，一旦自己都到达不了 A 了，就不用假设 C 还能到达 A 了。

另外，在路径中将一个自治系统看成一个整体，不区分自治系统内部的路由器，这样自治系统的数目是非常有限的。就像大家都能记住出去玩，从中国出发先到韩国然后到日本，只要不计算细到具体哪一站，就算是发送全局信息，也是没有问题的。

小结

好了，这一节就到这里了，我来做个总结：

路由分静态路由和动态路由，静态路由可以配置复杂的策略路由，控制转发策略；

动态路由主流算法有两种，距离矢量算法和链路状态算法。基于两种算法产生两种协议，BGP 协议和 OSPF 协议。

最后，再给你留两个思考题：

路由协议要在路由器之间交换信息，这些信息的交换还需要走路由吗？不是死锁了吗？

路由器之间信息的交换使用什么协议呢？报文格式是什么样呢？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第10讲 | UDP协议：因性善而简单，难免碰到“城会玩”

讲完了 IP 层以后，接下来我们开始讲传输层。传输层里比较重要的两个协议，一个是 TCP，一个是 UDP。对于不从事底层开发的人员来讲，或者对于开发应用的人来讲，最常用的就是这两个协议。由于面试的时候，这两个协议经常会被放在一起问，因而我在讲的时候，也会结合着来讲。

TCP 和 UDP 有哪些区别？

一般面试的时候我问这两个协议的区别，大部分人会回答，TCP 是面向连接的，UDP 是面向无连接的。

什么叫面向连接，什么叫无连接呢？在互通之前，面向连接的协议会先建立连接。例如，TCP 会三次握手，而 UDP 不会。为什么要建立连接呢？你 TCP 三次握手，我 UDP 也可以发三个包玩玩，有什么区别吗？

所谓的建立连接，是为了在客户端和服务端维护连接，而建立一定的数据结构来维护双方交互的状态，用这样的数据结构来保证所谓的面向连接的特性。

例如，TCP 提供可靠交付。通过 TCP 连接传输的数据，无差错、不丢失、不重复、并且按序到达。我们都知道 IP 包是没有任何可靠性保证的，一旦发出去，就像西天取经，走丢了、被妖怪吃了，都只能随它去。但是 TCP 号称能做到那个连接维护的程序做的事情，这个下两节我会详细描述。而UDP 继承了 IP 包的特性，不保证不丢失，不保证按顺序到达。

再如，TCP 是面向字节流的。发送的时候发的是一个流，没头没尾。IP 包可不是一个流，而是一个个的 IP 包。之所以变成了流，这也是 TCP 自己的状态维护做的事情。而UDP 继承了 IP 的特性，基于数据报的，一个一个地发，一个一个地收。

还有TCP 是可以有拥塞控制的。它意识到包丢弃了或者网络的环境不好了，就会根据情况调整自己的行为，看看是不是发快了，要不要发慢点。UDP 就不会，应用让我发，我就发，管它洪水滔天。

因而TCP 其实是一个有状态服务，通俗地讲就是有脑子的，里面精确地记着发送了没有，接收到没有，发送到哪个了，应该接收哪个了，错一点儿都不行。而 **UDP 则是无状态服务。** 通俗地说是没脑子的，天真无邪的，发出去就发出去了。

我们可以这样比喻，如果 MAC 层定义了本地局域网的传输行为，IP 层定义了整个网络端到端的传输行为，这两层基本定义了这样的基因：网络传输是以包为单位的，二层叫帧，网络层叫包，传输层叫段。我们笼统地称为包。包单独传输，自行选路，在不同的设备封装解封装，不保证到达。基于这个基因，生下来的孩子 UDP 完全继承了这些特性，几乎没有自己的思想。

UDP 包头是什么样的？

我们来看一下 UDP 包头。

前面章节我已经讲过包的传输过程，这里不再赘述。当我发送的 UDP 包到达目标机器后，发现 MAC 地址匹配，于是就取下来，将剩下的包传给处理 IP 层的代码。把 IP 头取下来，发现目标 IP 匹配，接下来呢？这里面的数据包是给谁呢？

发送的时候，我知道我发的是一个 UDP 的包，收到的那台机器咋知道的呢？所以在 IP 头里面有个 8 位协议，这里会存放，数据里面到底是 TCP 还是 UDP，当然这里是 UDP。于是，如果我们知道 UDP 头的格式，就能从数据里面，将它解析出来。解析出来以后呢？数据给谁处理呢？

处理完传输层的事情，内核的事情基本就干完了，里面的数据应该交给应用程序自己去处理，可是一台机器上跑着这么多的应用程序，应该给谁呢？

无论应用程序写的使用 TCP 传数据，还是 UDP 传数据，都要监听一个端口。正是这个端口，用来区分应用程序，要不说端口不能冲突呢。两个应用监听一个端口，到时候包给谁呀？所以，按理说，无论是 TCP 还是 UDP 包头里面应该有端口号，根据端口号，将数据交给相应的应用程序。
﻿﻿

当我们看到 UDP 包头的时候，发现的确有端口号，有源端口号和目标端口号。因为是两端通信嘛，这很好理解。但是你还会发现，UDP 除了端口号，再没有其他的了。和下两节要讲的 TCP 头比起来，这个简直简单得一塌糊涂啊！

UDP 的三大特点

UDP 就像小孩子一样，有以下这些特点：

第一，沟通简单，不需要一肚子花花肠子（大量的数据结构、处理逻辑、包头字段）。前提是它相信网络世界是美好的，秉承性善论，相信网络通路默认就是很容易送达的，不容易被丢弃的。

第二，轻信他人。它不会建立连接，虽然有端口号，但是监听在这个地方，谁都可以传给他数据，他也可以传给任何人数据，甚至可以同时传给多个人数据。

第三，愣头青，做事不懂权变。不知道什么时候该坚持，什么时候该退让。它不会根据网络的情况进行发包的拥塞控制，无论网络丢包丢成啥样了，它该怎么发还怎么发。

UDP 的三大使用场景

基于 UDP 这种“小孩子”的特点，我们可以考虑在以下的场景中使用。

第一，需要资源少，在网络情况比较好的内网，或者对于丢包不敏感的应用。这很好理解，就像如果你是领导，你会让你们组刚毕业的小朋友去做一些没有那么难的项目，打一些没有那么难的客户，或者做一些失败了也能忍受的实验性项目。

我们在第四节讲的 DHCP 就是基于 UDP 协议的。一般的获取 IP 地址都是内网请求，而且一次获取不到 IP 又没事，过一会儿还有机会。我们讲过 PXE 可以在启动的时候自动安装操作系统，操作系统镜像的下载使用的 TFTP，这个也是基于 UDP 协议的。在还没有操作系统的时候，客户端拥有的资源很少，不适合维护一个复杂的状态机，而是因为是内网，一般也没啥问题。

第二，不需要一对一沟通，建立连接，而是可以广播的应用。咱们小时候人都很简单，大家在班级里面，谁成绩好，谁写作好，应该表扬谁惩罚谁，谁得几个小红花都是当着全班的面讲的，公平公正公开。长大了人心复杂了，薪水、奖金要背靠背，和员工一对一沟通。

UDP 的不面向连接的功能，可以使得可以承载广播或者多播的协议。DHCP 就是一种广播的形式，就是基于 UDP 协议的，而广播包的格式前面说过了。

对于多播，我们在讲 IP 地址的时候，讲过一个 D 类地址，也即组播地址，使用这个地址，可以将包组播给一批机器。当一台机器上的某个进程想监听某个组播地址的时候，需要发送 IGMP 包，所在网络的路由器就能收到这个包，知道有个机器上有个进程在监听这个组播地址。当路由器收到这个组播地址的时候，会将包转发给这台机器，这样就实现了跨路由器的组播。

在后面云中网络部分，有一个协议 VXLAN，也是需要用到组播，也是基于 UDP 协议的。

第三，需要处理速度快，时延低，可以容忍少数丢包，但是要求即便网络拥塞，也毫不退缩，一往无前的时候。记得曾国藩建立湘军的时候，专门招出生牛犊不怕虎的新兵，而不用那些“老油条”的八旗兵，就是因为八旗兵经历的事情多，遇到敌军不敢舍死忘生。

同理，UDP 简单、处理速度快，不像 TCP 那样，操这么多的心，各种重传啊，保证顺序啊，前面的不收到，后面的没法处理啊。不然等这些事情做完了，时延早就上去了。而 TCP 在网络不好出现丢包的时候，拥塞控制策略会主动的退缩，降低发送速度，这就相当于本来环境就差，还自断臂膀，用户本来就卡，这下更卡了。

当前很多应用都是要求低时延的，它们可不想用 TCP 如此复杂的机制，而是想根据自己的场景，实现自己的可靠和连接保证。例如，如果应用自己觉得，有的包丢了就丢了，没必要重传了，就可以算了，有的比较重要，则应用自己重传，而不依赖于 TCP。有的前面的包没到，后面的包到了，那就先给客户展示后面的嘛，干嘛非得等到齐了呢？如果网络不好，丢了包，那不能退缩啊，要尽快传啊，速度不能降下来啊，要挤占带宽，抢在客户失去耐心之前到达。

由于 UDP 十分简单，基本啥都没做，也就给了应用“城会玩”的机会。就像在和平年代，每个人应该有独立的思考和行为，应该可靠并且礼让；但是如果在战争年代，往往不太需要过于独立的思考，而需要士兵简单服从命令就可以了。

曾国藩说哪支部队需要诱敌牺牲，也就牺牲了，相当于包丢了就丢了。两军狭路相逢的时候，曾国藩说上，没有带宽也要上，这才给了曾国藩运筹帷幄，城会玩的机会。同理如果你实现的应用需要有自己的连接策略，可靠保证，时延要求，使用 UDP，然后再应用层实现这些是再好不过了。

基于 UDP 的“城会玩”的五个例子

我列举几种“城会玩”的例子。

“城会玩”一：网页或者 APP 的访问

原来访问网页和手机 APP 都是基于 HTTP 协议的。HTTP 协议是基于 TCP 的，建立连接都需要多次交互，对于时延比较大的目前主流的移动互联网来讲，建立一次连接需要的时间会比较长，然而既然是移动中，TCP 可能还会断了重连，也是很耗时的。而且目前的 HTTP 协议，往往采取多个数据通道共享一个连接的情况，这样本来为了加快传输速度，但是 TCP 的严格顺序策略使得哪怕共享通道，前一个不来，后一个和前一个即便没关系，也要等着，时延也会加大。

而QUIC（全称Quick UDP Internet Connections，快速 UDP 互联网连接）是 Google 提出的一种基于 UDP 改进的通信协议，其目的是降低网络通信的延迟，提供更好的用户互动体验。

QUIC 在应用层上，会自己实现快速连接建立、减少重传时延，自适应拥塞控制，是应用层“城会玩”的代表。这一节主要是讲 UDP，QUIC 我们放到应用层去讲。

“城会玩”二：流媒体的协议

现在直播比较火，直播协议多使用 RTMP，这个协议我们后面的章节也会讲，而这个 RTMP 协议也是基于 TCP 的。TCP 的严格顺序传输要保证前一个收到了，下一个才能确认，如果前一个收不到，下一个就算包已经收到了，在缓存里面，也需要等着。对于直播来讲，这显然是不合适的，因为老的视频帧丢了其实也就丢了，就算再传过来用户也不在意了，他们要看新的了，如果老是没来就等着，卡顿了，新的也看不了，那就会丢失客户，所以直播，实时性比较比较重要，宁可丢包，也不要卡顿的。

另外，对于丢包，其实对于视频播放来讲，有的包可以丢，有的包不能丢，因为视频的连续帧里面，有的帧重要，有的不重要，如果必须要丢包，隔几个帧丢一个，其实看视频的人不会感知，但是如果连续丢帧，就会感知了，因而在网络不好的情况下，应用希望选择性的丢帧。

还有就是当网络不好的时候，TCP 协议会主动降低发送速度，这对本来当时就卡的看视频来讲是要命的，应该应用层马上重传，而不是主动让步。因而，很多直播应用，都基于 UDP 实现了自己的视频传输协议。

“城会玩”三：实时游戏

游戏有一个特点，就是实时性比较高。快一秒你干掉别人，慢一秒你被别人爆头，所以很多职业玩家会买非常专业的鼠标和键盘，争分夺秒。

因而，实时游戏中客户端和服务端要建立长连接，来保证实时传输。但是游戏玩家很多，服务器却不多。由于维护 TCP 连接需要在内核维护一些数据结构，因而一台机器能够支撑的 TCP 连接数目是有限的，然后 UDP 由于是没有连接的，在异步 IO 机制引入之前，常常是应对海量客户端连接的策略。

另外还是 TCP 的强顺序问题，对战的游戏，对网络的要求很简单，玩家通过客户端发送给服务器鼠标和键盘行走的位置，服务器会处理每个用户发送过来的所有场景，处理完再返回给客户端，客户端解析响应，渲染最新的场景展示给玩家。

如果出现一个数据包丢失，所有事情都需要停下来等待这个数据包重发。客户端会出现等待接收数据，然而玩家并不关心过期的数据，激战中卡 1 秒，等能动了都已经死了。

游戏对实时要求较为严格的情况下，采用自定义的可靠 UDP 协议，自定义重传策略，能够把丢包产生的延迟降到最低，尽量减少网络问题对游戏性造成的影响。

“城会玩”四：IoT 物联网

一方面，物联网领域终端资源少，很可能只是个内存非常小的嵌入式系统，而维护 TCP 协议代价太大；另一方面，物联网对实时性要求也很高，而 TCP 还是因为上面的那些原因导致时延大。Google 旗下的 Nest 建立 Thread Group，推出了物联网通信协议 Thread，就是基于 UDP 协议的。

“城会玩”五：移动通信领域

在 4G 网络里，移动流量上网的数据面对的协议 GTP-U 是基于 UDP 的。因为移动网络协议比较复杂，而 GTP 协议本身就包含复杂的手机上线下线的通信协议。如果基于 TCP，TCP 的机制就显得非常多余，这部分协议我会在后面的章节单独讲解。

小结

好了，这节就到这里了，我们来总结一下：

如果将 TCP 比作成熟的社会人，UDP 则是头脑简单的小朋友。TCP 复杂，UDP 简单；TCP 维护连接，UDP 谁都相信；TCP 会坚持知进退；UDP 愣头青一个，勇往直前；

UDP 虽然简单，但它有简单的用法。它可以用在环境简单、需要多播、应用层自己控制传输的地方。例如 DHCP、VXLAN、QUIC 等。

最后，给你留两个思考题吧。

都说 TCP 是面向连接的，在计算机看来，怎么样才算一个连接呢？

你知道 TCP 的连接是如何建立，又是如何关闭的吗？

欢迎你留言和讨论。趣谈网络协议，我们下期见！

第11讲 | TCP协议（上）：因性恶而复杂，先恶后善反轻松

上一节，我们讲的 UDP，基本上包括了传输层所必须的端口字段。它就像我们小时候一样简单，相信“网之初，性本善，不丢包，不乱序”。

后来呢，我们都慢慢长大，了解了社会的残酷，变得复杂而成熟，就像 TCP 协议一样。它之所以这么复杂，那是因为它秉承的是“性恶论”。它天然认为网络环境是恶劣的，丢包、乱序、重传，拥塞都是常有的事情，一言不合就可能送达不了，因而要从算法层面来保证可靠性。

TCP 包头格式

我们先来看 TCP 头的格式。从这个图上可以看出，它比 UDP 复杂得多。

首先，源端口号和目标端口号是不可少的，这一点和 UDP 是一样的。如果没有这两个端口号。数据就不知道应该发给哪个应用。

接下来是包的序号。为什么要给包编号呢？当然是为了解决乱序的问题。不编好号怎么确认哪个应该先来，哪个应该后到呢。编号是为了解决乱序问题。既然是社会老司机，做事当然要稳重，一件件来，面临再复杂的情况，也临危不乱。

还应该有的就是确认序号。发出去的包应该有确认，要不然我怎么知道对方有没有收到呢？如果没有收到就应该重新发送，直到送达。这个可以解决不丢包的问题。作为老司机，做事当然要靠谱，答应了就要做到，暂时做不到也要有个回复。

TCP 是靠谱的协议，但是这不能说明它面临的网络环境好。从 IP 层面来讲，如果网络状况的确那么差，是没有任何可靠性保证的，而作为 IP 的上一层 TCP 也无能为力，唯一能做的就是更加努力，不断重传，通过各种算法保证。也就是说，对于 TCP 来讲，IP 层你丢不丢包，我管不着，但是我在我的层面上，会努力保证可靠性。

这有点像如果你在北京，和客户约十点见面，那么你应该清楚堵车是常态，你干预不了，也控制不了，你唯一能做的就是早走。打车不行就改乘地铁，尽力不失约。

接下来有一些状态位。例如 SYN 是发起一个连接，ACK 是回复，RST 是重新连接，FIN 是结束连接等。TCP 是面向连接的，因而双方要维护连接的状态，这些带状态位的包的发送，会引起双方的状态变更。

不像小时候，随便一个不认识的小朋友都能玩在一起，人大了，就变得礼貌，优雅而警觉，人与人遇到会互相热情的寒暄，离开会不舍的道别，但是人与人之间的信任会经过多次交互才能建立。

还有一个重要的就是窗口大小。TCP 要做流量控制，通信双方各声明一个窗口，标识自己当前能够的处理能力，别发送的太快，撑死我，也别发的太慢，饿死我。

作为老司机，做事情要有分寸，待人要把握尺度，既能适当提出自己的要求，又不强人所难。除了做流量控制以外，TCP 还会做拥塞控制，对于真正的通路堵车不堵车，它无能为力，唯一能做的就是控制自己，也即控制发送的速度。不能改变世界，就改变自己嘛。

作为老司机，要会自我控制，知进退，知道什么时候应该坚持，什么时候应该让步。

通过对 TCP 头的解析，我们知道要掌握 TCP 协议，重点应该关注以下几个问题：

顺序问题 ，稳重不乱；

丢包问题，承诺靠谱；

连接维护，有始有终；

流量控制，把握分寸；

拥塞控制，知进知退。

TCP 的三次握手

所有的问题，首先都要先建立一个连接，所以我们先来看连接维护问题。

TCP 的连接建立，我们常常称为三次握手。

A：您好，我是 A。

B：您好 A，我是 B。

A：您好 B。

我们也常称为“请求 -> 应答 -> 应答之应答”的三个回合。这个看起来简单，其实里面还是有很多的学问，很多的细节。

首先，为什么要三次，而不是两次？按说两个人打招呼，一来一回就可以了啊？为了可靠，为什么不是四次？

我们还是假设这个通路是非常不可靠的，A 要发起一个连接，当发了第一个请求杳无音信的时候，会有很多的可能性，比如第一个请求包丢了，再如没有丢，但是绕了弯路，超时了，还有 B 没有响应，不想和我连接。

A 不能确认结果，于是再发，再发。终于，有一个请求包到了 B，但是请求包到了 B 的这个事情，目前 A 还是不知道的，A 还有可能再发。

B 收到了请求包，就知道了 A 的存在，并且知道 A 要和它建立连接。如果 B 不乐意建立连接，则 A 会重试一阵后放弃，连接建立失败，没有问题；如果 B 是乐意建立连接的，则会发送应答包给 A。

当然对于 B 来说，这个应答包也是一入网络深似海，不知道能不能到达 A。这个时候 B 自然不能认为连接是建立好了，因为应答包仍然会丢，会绕弯路，或者 A 已经挂了都有可能。

而且这个时候 B 还能碰到一个诡异的现象就是，A 和 B 原来建立了连接，做了简单通信后，结束了连接。还记得吗？A 建立连接的时候，请求包重复发了几次，有的请求包绕了一大圈又回来了，B 会认为这也是一个正常的的请求的话，因此建立了连接，可以想象，这个连接不会进行下去，也没有个终结的时候，纯属单相思了。因而两次握手肯定不行。

B 发送的应答可能会发送多次，但是只要一次到达 A，A 就认为连接已经建立了，因为对于 A 来讲，他的消息有去有回。A 会给 B 发送应答之应答，而 B 也在等这个消息，才能确认连接的建立，只有等到了这个消息，对于 B 来讲，才算它的消息有去有回。

当然 A 发给 B 的应答之应答也会丢，也会绕路，甚至 B 挂了。按理来说，还应该有个应答之应答之应答，这样下去就没底了。所以四次握手是可以的，四十次都可以，关键四百次也不能保证就真的可靠了。只要双方的消息都有去有回，就基本可以了。

好在大部分情况下，A 和 B 建立了连接之后，A 会马上发送数据的，一旦 A 发送数据，则很多问题都得到了解决。例如 A 发给 B 的应答丢了，当 A 后续发送的数据到达的时候，B 可以认为这个连接已经建立，或者 B 压根就挂了，A 发送的数据，会报错，说 B 不可达，A 就知道 B 出事情了。

当然你可以说 A 比较坏，就是不发数据，建立连接后空着。我们在程序设计的时候，可以要求开启 keepalive 机制，即使没有真实的数据包，也有探活包。

另外，你作为服务端 B 的程序设计者，对于 A 这种长时间不发包的客户端，可以主动关闭，从而空出资源来给其他客户端使用。

三次握手除了双方建立连接外，主要还是为了沟通一件事情，就是TCP 包的序号的问题。

A 要告诉 B，我这面发起的包的序号起始是从哪个号开始的，B 同样也要告诉 A，B 发起的包的序号起始是从哪个号开始的。为什么序号不能都从 1 开始呢？因为这样往往会出现冲突。

例如，A 连上 B 之后，发送了 1、2、3 三个包，但是发送 3 的时候，中间丢了，或者绕路了，于是重新发送，后来 A 掉线了，重新连上 B 后，序号又从 1 开始，然后发送 2，但是压根没想发送 3，但是上次绕路的那个 3 又回来了，发给了 B，B 自然认为，这就是下一个包，于是发生了错误。

因而，每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的，可以看成一个 32 位的计数器，每 4ms 加一，如果计算一下，如果到重复，需要 4 个多小时，那个绕路的包早就死翘翘了，因为我们都知道 IP 包头里面有个 TTL，也即生存时间。

好了，双方终于建立了信任，建立了连接。前面也说过，为了维护这个连接，双方都要维护一个状态机，在连接建立的过程中，双方的状态变化时序图就像这样。

一开始，客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口，处于 LISTEN 状态。然后客户端主动发起连接 SYN，之后处于 SYN-SENT 状态。服务端收到发起的连接，返回 SYN，并且 ACK 客户端的 SYN，之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后，发送 ACK 的 ACK，之后处于 ESTABLISHED 状态，因为它一发一收成功了。服务端收到 ACK 的 ACK 之后，处于 ESTABLISHED 状态，因为它也一发一收了。

TCP 四次挥手

好了，说完了连接，接下来说一说“拜拜”，好说好散。这常被称为四次挥手。

A：B 啊，我不想玩了。

B：哦，你不想玩了啊，我知道了。

这个时候，还只是 A 不想玩了，也即 A 不会再发送数据，但是 B 能不能在 ACK 的时候，直接关闭呢？当然不可以了，很有可能 A 是发完了最后的数据就准备不玩了，但是 B 还没做完自己的事情，还是可以发送数据的，所以称为半关闭的状态。

这个时候 A 可以选择不再接收数据了，也可以选择最后再接收一段数据，等待 B 也主动关闭。

B：A 啊，好吧，我也不玩了，拜拜。

A：好的，拜拜。

这样整个连接就关闭了。但是这个过程有没有异常情况呢？当然有，上面是和平分手的场面。

A 开始说“不玩了”，B 说“知道了”，这个回合，是没什么问题的，因为在此之前，双方还处于合作的状态，如果 A 说“不玩了”，没有收到回复，则 A 会重新发送“不玩了”。但是这个回合结束之后，就有可能出现异常情况了，因为已经有一方率先撕破脸。

一种情况是，A 说完“不玩了”之后，直接跑路，是会有问题的，因为 B 还没有发起结束，而如果 A 跑路，B 就算发起结束，也得不到回答，B 就不知道该怎么办了。另一种情况是，A 说完“不玩了”，B 直接跑路，也是有问题的，因为 A 不知道 B 是还有事情要处理，还是过一会儿会发送结束。

那怎么解决这些问题呢？TCP 协议专门设计了几个状态来处理这些问题。我们来看断开连接的时候的状态时序图。

断开的时候，我们可以看到，当 A 说“不玩了”，就进入 FIN_WAIT_1 的状态，B 收到“A 不玩”的消息后，发送知道了，就进入 CLOSE_WAIT 的状态。

A 收到“B 说知道了”，就进入 FIN_WAIT_2 的状态，如果这个时候 B 直接跑路，则 A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理，但是 Linux 有，可以调整 tcp_fin_timeout 这个参数，设置一个超时时间。

如果 B 没有跑路，发送了“B 也不玩了”的请求到达 A 时，A 发送“知道 B 也不玩了”的 ACK 后，从 FIN_WAIT_2 状态结束，按说 A 可以跑路了，但是最后的这个 ACK 万一 B 收不到呢？则 B 会重新发一个“B 不玩了”，这个时候 A 已经跑路了的话，B 就再也收不到 ACK 了，因而 TCP 协议要求 A 最后等待一段时间 TIME_WAIT，这个时间要足够长，长到如果 B 没收到 ACK 的话，“B 说不玩了”会重发的，A 会重新发一个 ACK 并且足够时间到达 B。

A 直接跑路还有一个问题是，A 的端口就直接空出来了，但是 B 不知道，B 原来发过的很多包很可能还在路上，如果 A 的端口被一个新的应用占用了，这个新的应用会收到上个连接中 B 发过来的包，虽然序列号是重新生成的，但是这里要上一个双保险，防止产生混乱，因而也需要等足够长的时间，等到原来 B 发送的所有的包都死翘翘，再空出端口来。

等待的时间设为 2MSL，MSL是Maximum Segment Lifetime，报文最大生存时间，它是任何报文在网络上存在的最长时间，超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的，而 IP 头中有一个 TTL 域，是 IP 数据报可以经过的最大路由数，每经过一个处理他的路由器此值就减 1，当此值为 0 则数据报将被丢弃，同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟，实际应用中常用的是 30 秒，1 分钟和 2 分钟等。

还有一个异常情况就是，B 超过了 2MSL 的时间，依然没有收到它发的 FIN 的 ACK，怎么办呢？按照 TCP 的原理，B 当然还会重发 FIN，这个时候 A 再收到这个包之后，A 就表示，我已经在这里等了这么长时间了，已经仁至义尽了，之后的我就都不认了，于是就直接发送 RST，B 就知道 A 早就跑了。

TCP 状态机

将连接建立和连接断开的两个时序状态图综合起来，就是这个著名的 TCP 的状态机。学习的时候比较建议将这个状态机和时序状态机对照着看，不然容易晕。

在这个图中，加黑加粗的部分，是上面说到的主要流程，其中阿拉伯数字的序号，是连接过程中的顺序，而大写中文数字的序号，是连接断开过程中的顺序。加粗的实线是客户端 A 的状态变迁，加粗的虚线是服务端 B 的状态变迁。

小结

好了，这一节就到这里了，我来做一个总结：

TCP 包头很复杂，但是主要关注五个问题，顺序问题，丢包问题，连接维护，流量控制，拥塞控制；

连接的建立是经过三次握手，断开的时候四次挥手，一定要掌握的我画的那个状态图。

最后，给你留两个思考题。

TCP 的连接有这么多的状态，你知道如何在系统中查看某个连接的状态吗？

这一节仅仅讲了连接维护问题，其实为了维护连接的状态，还有其他的数据结构来处理其他的四个问题，那你知道是什么吗？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第12讲 | TCP协议（下）：西行必定多妖孽，恒心智慧消磨难

我们前面说到玄奘西行，要出网关。既然出了网关，那就是在公网上传输数据，公网往往是不可靠的，因而需要很多的机制去保证传输的可靠性，这里面需要恒心，也即各种重传的策略，还需要有智慧，也就是说，这里面包含着大量的算法。

如何做个靠谱的人？

TCP 想成为一个成熟稳重的人，成为一个靠谱的人。那一个人怎么样才算靠谱呢？咱们工作中经常就有这样的场景，比如你交代给下属一个事情以后，下属到底能不能做到，做到什么程度，什么时候能够交付，往往就会有应答，有回复。这样，处理事情的过程中，一旦有异常，你也可以尽快知道，而不是交代完之后就石沉大海，过了一个月再问，他说，啊我不记得了。

对应到网络协议上，就是客户端每发送的一个包，服务器端都应该有个回复，如果服务器端超过一定的时间没有回复，客户端就会重新发送这个包，直到有回复。

这个发送应答的过程是什么样呢？可以是上一个收到了应答，再发送下一个。这种模式有点像两个人直接打电话，你一句，我一句。但是这种方式的缺点是效率比较低。如果一方在电话那头处理的时间比较长，这一头就要干等着，双方都没办法干其他事情。咱们在日常工作中也不是这样的，不能你交代你的下属办一件事情，就一直打着电话看着他做，而是应该他按照你的安排，先将事情记录下来，办完一件回复一件。在他办事情的过程中，你还可以同时交代新的事情，这样双方就并行了。

如果使⽤这种模式，其实需要你和你的下属就不能靠脑⼦了，⽽是要都准备⼀个本⼦，你每交代下属⼀个事情，双方的本子都要记录⼀下。

当你的下属做完⼀件事情，就回复你，做完了，你就在你的本⼦上将这个事情划去。同时你的本⼦上每件事情都有时限，如果超过了时限下属还没有回复，你就要主动重新交代⼀下：上次那件事情，你还没回复我，咋样啦？

既然多件事情可以一起处理，那就需要给每个事情编个号，防止弄错了。例如，程序员平时看任务的时候，都会看 JIRA 的 ID，而不是每次都要描述一下具体的事情。在大部分情况下，对于事情的处理是按照顺序来的，先来的先处理，这就给应答和汇报工作带来了方便。等开周会的时候，每个程序员都可以将 JIRA ID 的列表拉出来，说以上的都做完了，⽽不⽤⼀个个说。

如何实现一个靠谱的协议？

TCP 协议使用的也是同样的模式。为了保证顺序性，每一个包都有一个 ID。在建立连接的时候，会商定起始的 ID 是什么，然后按照 ID 一个个发送。为了保证不丢包，对于发送的包都要进行应答，但是这个应答也不是一个一个来的，而是会应答某个之前的 ID，表示都收到了，这种模式称为累计确认或者累计应答（cumulative acknowledgment）。

为了记录所有发送的包和接收的包，TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的 ID 一个个排列，根据处理的情况分成四个部分。

第一部分：发送了并且已经确认的。这部分就是你交代下属的，并且也做完了的，应该划掉的。

第二部分：发送了并且尚未确认的。这部分是你交代下属的，但是还没做完的，需要等待做完的回复之后，才能划掉。

第三部分：没有发送，但是已经等待发送的。这部分是你还没有交代给下属，但是马上就要交代的。

第四部分：没有发送，并且暂时还不会发送的。这部分是你还没有交代给下属，而且暂时还不会交代给下属的。

这里面为什么要区分第三部分和第四部分呢？没交代的，一下子全交代了不就完了吗？

这就是我们上一节提到的十个词口诀里的“流量控制，把握分寸”。作为项目管理人员，你应该根据以往的工作情况和这个员工反馈的能力、抗压力等，先在心中估测一下，这个人一天能做多少工作。如果工作布置少了，就会不饱和；如果工作布置多了，他就会做不完；如果你使劲逼迫，人家可能就要辞职了。

到底一个员工能够同时处理多少事情呢？在 TCP 里，接收端会给发送端报一个窗口的大小，叫Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分，就是已经交代了没做完的加上马上要交代的。超过这个窗口的，接收端做不过来，就不能发送了。

于是，发送端需要保持下面的数据结构。

LastByteAcked：第一部分和第二部分的分界线

LastByteSent：第二部分和第三部分的分界线

LastByteAcked + AdvertisedWindow：第三部分和第四部分的分界线

对于接收端来讲，它的缓存里记录的内容要简单一些。

第一部分：接受并且确认过的。也就是我领导交代给我，并且我做完的。

第二部分：还没接收，但是马上就能接收的。也即是我自己的能够接受的最大工作量。

第三部分：还没接收，也没法接收的。也即超过工作量的部分，实在做不完。

对应的数据结构就像这样。 ﻿﻿

MaxRcvBuffer：最大缓存的量；

LastByteRead 之后是已经接收了，但是还没被应用层读取的；

NextByteExpected 是第一部分和第二部分的分界线。

第二部分的窗口有多大呢？

NextByteExpected 和 LastByteRead 的差其实是还没被应用层读取的部分占用掉的 MaxRcvBuffer 的量，我们定义为 A。

AdvertisedWindow 其实是 MaxRcvBuffer 减去 A。

也就是：AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)。

那第二部分和第三部分的分界线在哪里呢？NextByteExpected 加 AdvertisedWindow 就是第二部分和第三部分的分界线，其实也就是 LastByteRead 加上 MaxRcvBuffer。

其中第二部分里面，由于受到的包可能不是顺序的，会出现空挡，只有和第一部分连续的，可以马上进行回复，中间空着的部分需要等待，哪怕后面的已经来了。

顺序问题与丢包问题

接下来我们结合一个例子来看。

还是刚才的图，在发送端来看，1、2、3 已经发送并确认；4、5、6、7、8、9 都是发送了还没确认；10、11、12 是还没发出的；13、14、15 是接收方没有空间，不准备发的。

在接收端来看，1、2、3、4、5 是已经完成 ACK，但是没读取的；6、7 是等待接收的；8、9 是已经接收，但是没有 ACK 的。

发送端和接收端当前的状态如下：

1、2、3 没有问题，双方达成了一致。

4、5 接收方说 ACK 了，但是发送方还没收到，有可能丢了，有可能在路上。

6、7、8、9 肯定都发了，但是 8、9 已经到了，但是 6、7 没到，出现了乱序，缓存着但是没办法 ACK。

根据这个例子，我们可以知道，顺序问题和丢包问题都有可能发生，所以我们先来看确认与重发的机制。

假设 4 的确认到了，不幸的是，5 的 ACK 丢了，6、7 的数据包丢了，这该怎么办呢？

一种方法就是超时重试，也即对每一个发送了，但是没有 ACK 的包，都有设一个定时器，超过了一定的时间，就重新尝试。但是这个超时的时间如何评估呢？这个时间不宜过短，时间必须大于往返时间 RTT，否则会引起不必要的重传。也不宜过长，这样超时时间变长，访问就变慢了。

估计往返时间，需要 TCP 通过采样 RTT 的时间，然后进行加权平均，算出一个值，而且这个值还是要不断变化的，因为网络状况不断的变化。除了采样 RTT，还要采样 RTT 的波动范围，计算出一个估计的超时时间。由于重传时间是不断变化的，我们称为自适应重传算法（Adaptive Retransmission Algorithm）。

如果过一段时间，5、6、7 都超时了，就会重新发送。接收方发现 5 原来接收过，于是丢弃 5；6 收到了，发送 ACK，要求下一个是 7，7 不幸又丢了。当 7 再次超时的时候，有需要重传的时候，TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候，都会将下一次超时时间间隔设为先前值的两倍。两次超时，就说明网络环境差，不宜频繁反复发送。

超时触发重传存在的问题是，超时周期可能相对较长。那是不是可以有更快的方式呢？

有一个可以快速重传的机制，当接收方收到一个序号大于下一个所期望的报文段时，就检测到了数据流中的一个间格，于是发送三个冗余的 ACK，客户端收到后，就在定时器过期之前，重传丢失的报文段。

例如，接收方发现 6、8、9 都已经接收了，就是 7 没来，那肯定是丢了，于是发送三个 6 的 ACK，要求下一个是 7。客户端收到 3 个，就会发现 7 的确又丢了，不等超时，马上重发。

还有一种方式称为Selective Acknowledgment （SACK）。这种方式需要在 TCP 头里加一个 SACK 的东西，可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9，有了地图，发送方一下子就能看出来是 7 丢了。

流量控制问题

我们再来看流量控制机制，在对于包的确认中，同时会携带一个窗口的大小。

我们先假设窗口不变的情况，窗口始终为 9。4 的确认来的时候，会右移一个，这个时候第 13 个包也可以发送了。

这个时候，假设发送端发送过猛，会将第三部分的 10、11、12、13 全部发送完毕，之后就停止发送了，未发送可发送部分为 0。

当对于包 5 的确认到达的时候，在客户端相当于窗口再滑动了一格，这个时候，才可以有更多的包可以发送了，例如第 14 个包才可以发送。

如果接收方实在处理的太慢，导致缓存中没有空间了，可以通过确认信息修改窗口的大小，甚至可以设置为 0，则发送方将暂时停止发送。

我们假设一个极端情况，接收端的应用一直不读取缓存中的数据，当数据包 6 确认后，窗口大小就不能再是 9 了，就要缩小一个变为 8。

这个新的窗口 8 通过 6 的确认消息到达发送端的时候，你会发现窗口没有平行右移，而是仅仅左面的边右移了，窗口的大小从 9 改成了 8。

如果接收端还是一直不处理数据，则随着确认的包越来越多，窗口越来越小，直到为 0。

当这个窗口通过包 14 的确认到达发送端的时候，发送端的窗口也调整为 0，停止发送。

如果这样的话，发送方会定时发送窗口探测数据包，看是否有机会调整窗口的大小。当接收方比较慢的时候，要防止低能窗口综合征，别空出一个字节来就赶快告诉发送方，然后马上又填满了，可以当窗口太小的时候，不更新窗口，直到达到一定大小，或者缓冲区一半为空，才更新窗口。

这就是我们常说的流量控制。

拥塞控制问题

最后，我们看一下拥塞控制的问题，也是通过窗口的大小来控制的，前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满，而拥塞窗口 cwnd，是怕把网络塞满。

这里有一个公式 LastByteSent - LastByteAcked <= min {cwnd, rwnd} ，是拥塞窗口和滑动窗口共同控制发送的速度。

那发送方怎么判断网络是不是满呢？这其实是个挺难的事情，因为对于 TCP 协议来讲，他压根不知道整个网络路径都会经历什么，对他来讲就是一个黑盒。TCP 发送包常被比喻为往一个水管里面灌水，而 TCP 的拥塞控制就是在不堵塞，不丢包的情况下，尽量发挥带宽。

水管有粗细，网络有带宽，也即每秒钟能够发送多少数据；水管有长度，端到端有时延。在理想状态下，水管里面水的量 = 水管粗细 x 水管长度。对于到网络上，通道的容量 = 带宽 × 往返延迟。

如果我们设置发送窗口，使得发送但未确认的包为为通道的容量，就能够撑满整个管道。

如图所示，假设往返时间为 8s，去 4s，回 4s，每秒发送一个包，每个包 1024byte。已经过去了 8s，则 8 个包都发出去了，其中前 4 个包已经到达接收端，但是 ACK 还没有返回，不能算发送成功。5-8 后四个包还在路上，还没被接收。这个时候，整个管道正好撑满，在发送端，已发送未确认的为 8 个包，正好等于带宽，也即每秒发送 1 个包，乘以来回时间 8s。

如果我们在这个基础上再调大窗口，使得单位时间内更多的包可以发送，会出现什么现象呢？

我们来想，原来发送一个包，从一端到达另一端，假设一共经过四个设备，每个设备处理一个包时间耗费 1s，所以到达另一端需要耗费 4s，如果发送的更加快速，则单位时间内，会有更多的包到达这些中间设备，这些设备还是只能每秒处理一个包的话，多出来的包就会被丢弃，这是我们不想看到的。

这个时候，我们可以想其他的办法，例如这个四个设备本来每秒处理一个包，但是我们在这些设备上加缓存，处理不过来的在队列里面排着，这样包就不会丢失，但是缺点是会增加时延，这个缓存的包，4s 肯定到达不了接收端了，如果时延达到一定程度，就会超时重传，也是我们不想看到的。

于是 TCP 的拥塞控制主要来避免两种现象，包丢失和超时重传。一旦出现了这些现象就说明，发送速度太快了，要慢一点。但是一开始我怎么知道速度多快呢，我怎么知道应该把窗口调整到多大呢？

如果我们通过漏斗往瓶子里灌水，我们就知道，不能一桶水一下子倒进去，肯定会溅出来，要一开始慢慢的倒，然后发现总能够倒进去，就可以越倒越快。这叫作慢启动。

一条 TCP 连接开始，cwnd 设置为一个报文段，一次只能发送一个；当收到这一个确认的时候，cwnd 加一，于是一次能够发送两个；当这两个的确认到来的时候，每个确认 cwnd 加一，两个确认 cwnd 加二，于是一次能够发送四个；当这四个的确认到来的时候，每个确认 cwnd 加一，四个确认 cwnd 加四，于是一次能够发送八个。可以看出这是指数性的增长。

涨到什么时候是个头呢？有一个值 ssthresh 为 65535 个字节，当超过这个值的时候，就要小心一点了，不能倒这么快了，可能快满了，再慢下来。

每收到一个确认后，cwnd 增加 1/cwnd，我们接着上面的过程来，一次发送八个，当八个确认到来的时候，每个确认增加 1/8，八个确认一共 cwnd 增加 1，于是一次能够发送九个，变成了线性增长。

但是线性增长还是增长，还是越来越多，直到有一天，水满则溢，出现了拥塞，这时候一般就会一下子降低倒水的速度，等待溢出的水慢慢渗下去。

拥塞的一种表现形式是丢包，需要超时重传，这个时候，将 sshresh 设为 cwnd/2，将 cwnd 设为 1，重新开始慢启动。这真是一旦超时重传，马上回到解放前。但是这种方式太激进了，将一个高速的传输速度一下子停了下来，会造成网络卡顿。

前面我们讲过快速重传算法。当接收端发现丢了一个中间包的时候，发送三次前一个包的 ACK，于是发送端就会快速的重传，不必等待超时再重传。TCP 认为这种情况不严重，因为大部分没丢，只丢了一小部分，cwnd 减半为 cwnd/2，然后 sshthresh = cwnd，当三个包返回的时候，cwnd = sshthresh + 3，也就是没有一夜回到解放前，而是还在比较高的值，呈线性增长。

就像前面说的一样，正是这种知进退，使得时延很重要的情况下，反而降低了速度。但是如果你仔细想一下，TCP 的拥塞控制主要来避免的两个现象都是有问题的。

第一个问题是丢包并不代表着通道满了，也可能是管子本来就漏水。例如公网上带宽不满也会丢包，这个时候就认为拥塞了，退缩了，其实是不对的。

第二个问题是 TCP 的拥塞控制要等到将中间设备都填充满了，才发生丢包，从而降低速度，这时候已经晚了。其实 TCP 只要填满管道就可以了，不应该接着填，直到连缓存也填满。

为了优化这两个问题，后来有了TCP BBR 拥塞算法。它企图找到一个平衡点，就是通过不断的加快发送速度，将管道填满，但是不要填满中间设备的缓存，因为这样时延会增加，在这个平衡点可以很好的达到高带宽和低时延的平衡。

小结

好了，这一节我们就到这里，总结一下：

顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的，这其实就相当于你领导和你的工作备忘录，布置过的工作要有编号，干完了有反馈，活不能派太多，也不能太少；

拥塞控制是通过拥塞窗口来解决的，相当于往管道里面倒水，快了容易溢出，慢了浪费带宽，要摸着石头过河，找到最优值。

最后留两个思考题：

TCP 的 BBR 听起来很牛，你知道他是如何达到这个最优点的嘛？

学会了 UDP 和 TCP，你知道如何基于这两种协议写程序吗？这样的程序会有什么坑呢？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第13讲 | 套接字Socket：Talk is cheap, show me the code

前面讲完了 TCP 和 UDP 协议，还没有上手过，这一节咱们讲讲基于 TCP 和 UDP 协议的 Socket 编程。

在讲 TCP 和 UDP 协议的时候，我们分客户端和服务端，在写程序的时候，我们也同样这样分。

Socket 这个名字很有意思，可以作插口或者插槽讲。虽然我们是写软件程序，但是你可以想象为弄一根网线，一头插在客户端，一头插在服务端，然后进行通信。所以在通信之前，双方都要建立一个 Socket。

在建立 Socket 的时候，应该设置什么参数呢？Socket 编程进行的是端到端的通信，往往意识不到中间经过多少局域网，多少路由器，因而能够设置的参数，也只能是端到端协议之上网络层和传输层的。

在网络层，Socket 函数需要指定到底是 IPv4 还是 IPv6，分别对应设置为 AF_INET 和 AF_INET6。另外，还要指定到底是 TCP 还是 UDP。还记得咱们前面讲过的，TCP 协议是基于数据流的，所以设置为 SOCK_STREAM，而 UDP 是基于数据报的，因而设置为 SOCK_DGRAM。

基于 TCP 协议的 Socket 程序函数调用过程

两端创建了 Socket 之后，接下来的过程中，TCP 和 UDP 稍有不同，我们先来看 TCP。

TCP 的服务端要先监听一个端口，一般是先调用 bind 函数，给这个 Socket 赋予一个 IP 地址和端口。为什么需要端口呢？要知道，你写的是一个应用程序，当一个网络包来的时候，内核要通过 TCP 头里面的这个端口，来找到你这个应用程序，把包给你。为什么要 IP 地址呢？有时候，一台机器会有多个网卡，也就会有多个 IP 地址，你可以选择监听所有的网卡，也可以选择监听一个网卡，这样，只有发给这个网卡的包，才会给你。

当服务端有了 IP 和端口号，就可以调用 listen 函数进行监听。在 TCP 的状态图里面，有一个 listen 状态，当调用这个函数之后，服务端就进入了这个状态，这个时候客户端就可以发起连接了。

在内核中，为每个 Socket 维护两个队列。一个是已经建立了连接的队列，这时候连接三次握手已经完毕，处于 established 状态；一个是还没有完全建立连接的队列，这个时候三次握手还没完成，处于 syn_rcvd 的状态。

接下来，服务端调用 accept 函数，拿出一个已经完成的连接进行处理。如果还没有完成，就要等着。

在服务端等待的时候，客户端可以通过 connect 函数发起连接。先在参数中指明要连接的 IP 地址和端口号，然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功，服务端的 accept 就会返回另一个 Socket。

这是一个经常考的知识点，就是监听的 Socket 和真正用来传数据的 Socket 是两个，一个叫作监听 Socket，一个叫作已连接 Socket。

连接建立成功之后，双方开始通过 read 和 write 函数来读写数据，就像往一个文件流里面写东西一样。

这个图就是基于 TCP 协议的 Socket 程序函数调用过程。

说 TCP 的 Socket 就是一个文件流，是非常准确的。因为，Socket 在 Linux 中就是以文件的形式存在的。除此之外，还存在文件描述符。写入和读出，也是通过文件描述符。

在内核中，Socket 是一个文件，那对应就有文件描述符。每一个进程都有一个数据结构 task_struct，里面指向一个文件描述符数组，来列出这个进程打开的所有文件的文件描述符。文件描述符是一个整数，是这个数组的下标。

这个数组中的内容是一个指针，指向内核中所有打开的文件的列表。既然是一个文件，就会有一个 inode，只不过 Socket 对应的 inode 不像真正的文件系统一样，保存在硬盘上的，而是在内存中的。在这个 inode 中，指向了 Socket 在内核中的 Socket 结构。

在这个结构里面，主要的是两个队列，一个是发送队列，一个是接收队列。在这两个队列里面保存的是一个缓存 sk_buff。这个缓存里面能够看到完整的包的结构。看到这个，是不是能和前面讲过的收发包的场景联系起来了？

整个数据结构我也画了一张图。

基于 UDP 协议的 Socket 程序函数调用过程

对于 UDP 来讲，过程有些不一样。UDP 是没有连接的，所以不需要三次握手，也就不需要调用 listen 和 connect，但是，UDP 的的交互仍然需要 IP 和端口号，因而也需要 bind。UDP 是没有维护连接状态的，因而不需要每对连接建立一组 Socket，而是只要有一个 Socket，就能够和多个客户端通信。也正是因为没有连接状态，每次通信的时候，都调用 sendto 和 recvfrom，都可以传入 IP 地址和端口。

这个图的内容就是基于 UDP 协议的 Socket 程序函数调用过程。

﻿﻿

服务器如何接更多的项目？

会了这几个基本的 Socket 函数之后，你就可以轻松地写一个网络交互的程序了。就像上面的过程一样，在建立连接后，进行一个 while 循环。客户端发了收，服务端收了发。

当然这只是万里长征的第一步，因为如果使用这种方法，基本上只能一对一沟通。如果你是一个服务器，同时只能服务一个客户，肯定是不行的。这就相当于老板成立一个公司，只有自己一个人，自己亲自上来服务客户，只能干完了一家再干下一家，这样赚不来多少钱。

那作为老板你就要想了，我最多能接多少项目呢？当然是越多越好。

我们先来算一下理论值，也就是最大连接数，系统会用一个四元组来标识一个 TCP 连接。

{本机 IP, 本机端口, 对端 IP, 对端端口}

服务器通常固定在某个本地端口上监听，等待客户端的连接请求。因此，服务端端 TCP 连接四元组中只有对端 IP, 也就是客户端的 IP 和对端的端口，也即客户端的端口是可变的，因此，最大 TCP 连接数 = 客户端 IP 数×客户端端口数。对 IPv4，客户端的 IP 数最多为 2 的 32 次方，客户端的端口数最多为 2 的 16 次方，也就是服务端单机最大 TCP 连接数，约为 2 的 48 次方。

当然，服务端最大并发 TCP 连接数远不能达到理论上限。首先主要是文件描述符限制，按照上面的原理，Socket 都是文件，所以首先要通过 ulimit 配置文件描述符的数目；另一个限制是内存，按上面的数据结构，每个 TCP 连接都要占用一定内存，操作系统是有限的。

所以，作为老板，在资源有限的情况下，要想接更多的项目，就需要降低每个项目消耗的资源数目。

方式一：将项目外包给其他公司（多进程方式）

这就相当于你是一个代理，在那里监听来的请求。一旦建立了一个连接，就会有一个已连接 Socket，这时候你可以创建一个子进程，然后将基于已连接 Socket 的交互交给这个新的子进程来做。就像来了一个新的项目，但是项目不一定是你自己做，可以再注册一家子公司，招点人，然后把项目转包给这家子公司做，以后对接就交给这家子公司了，你又可以去接新的项目了。

这里有一个问题是，如何创建子公司，并如何将项目移交给子公司呢？

在 Linux 下，创建子进程使用 fork 函数。通过名字可以看出，这是在父进程的基础上完全拷贝一个子进程。在 Linux 内核中，会复制文件描述符的列表，也会复制内存空间，还会复制一条记录当前执行到了哪一行程序的进程。显然，复制的时候在调用 fork，复制完毕之后，父进程和子进程都会记录当前刚刚执行完 fork。这两个进程刚复制完的时候，几乎一模一样，只是根据 fork 的返回值来区分到底是父进程，还是子进程。如果返回值是 0，则是子进程；如果返回值是其他的整数，就是父进程。

进程复制过程我画在这里。

因为复制了文件描述符列表，而文件描述符都是指向整个内核统一的打开文件列表的，因而父进程刚才因为 accept 创建的已连接 Socket 也是一个文件描述符，同样也会被子进程获得。

接下来，子进程就可以通过这个已连接 Socket 和客户端进行互通了，当通信完毕之后，就可以退出进程，那父进程如何知道子进程干完了项目，要退出呢？还记得 fork 返回的时候，如果是整数就是父进程吗？这个整数就是子进程的 ID，父进程可以通过这个 ID 查看子进程是否完成项目，是否需要退出。

方式二：将项目转包给独立的项目组（多线程方式）

上面这种方式你应该也能发现问题，如果每次接一个项目，都申请一个新公司，然后干完了，就注销掉这个公司，实在是太麻烦了。毕竟一个新公司要有新公司的资产，有新的办公家具，每次都买了再卖，不划算。

于是你应该想到了，我们可以使用线程。相比于进程来讲，这样要轻量级的多。如果创建进程相当于成立新公司，购买新办公家具，而创建线程，就相当于在同一个公司成立项目组。一个项目做完了，那这个项目组就可以解散，组成另外的项目组，办公家具可以共用。

在 Linux 下，通过 pthread_create 创建一个线程，也是调用 do_fork。不同的是，虽然新的线程在 task 列表会新创建一项，但是很多资源，例如文件描述符列表、进程空间，还是共享的，只不过多了一个引用而已。

﻿ 新的线程也可以通过已连接 Socket 处理请求，从而达到并发处理的目的。

上面基于进程或者线程模型的，其实还是有问题的。新到来一个 TCP 连接，就需要分配一个进程或者线程。一台机器无法创建很多进程或者线程。有个C10K，它的意思是一台机器要维护 1 万个连接，就要创建 1 万个进程或者线程，那么操作系统是无法承受的。如果维持 1 亿用户在线需要 10 万台服务器，成本也太高了。

其实 C10K 问题就是，你接项目接的太多了，如果每个项目都成立单独的项目组，就要招聘 10 万人，你肯定养不起，那怎么办呢？

方式三：一个项目组支撑多个项目（IO 多路复用，一个线程维护多个 Socket）

当然，一个项目组可以看多个项目了。这个时候，每个项目组都应该有个项目进度墙，将自己组看的项目列在那里，然后每天通过项目墙看每个项目的进度，一旦某个项目有了进展，就派人去盯一下。

由于 Socket 是文件描述符，因而某个线程盯的所有的 Socket，都放在一个文件描述符集合 fd_set 中，这就是项目进度墙，然后调用 select 函数来监听文件描述符集合是否有变化。一旦有变化，就会依次查看每个文件描述符。那些发生变化的文件描述符在 fd_set 对应的位都设为 1，表示 Socket 可读或者可写，从而可以进行读写操作，然后再调用 select，接着盯着下一轮的变化。。

方式四：一个项目组支撑多个项目（IO 多路复用，从“派人盯着”到“有事通知”）

上面 select 函数还是有问题的，因为每次 Socket 所在的文件描述符集合中有 Socket 发生变化的时候，都需要通过轮询的方式，也就是需要将全部项目都过一遍的方式来查看进度，这大大影响了一个项目组能够支撑的最大的项目数量。因而使用 select，能够同时盯的项目数量由 FD_SETSIZE 限制。

如果改成事件通知的方式，情况就会好很多，项目组不需要通过轮询挨个盯着这些项目，而是当项目进度发生变化的时候，主动通知项目组，然后项目组再根据项目进展情况做相应的操作。

能完成这件事情的函数叫 epoll，它在内核中的实现不是通过轮询的方式，而是通过注册 callback 函数的方式，当某个文件描述符发送变化的时候，就会主动通知。

如图所示，假设进程打开了 Socket m, n, x 等多个文件描述符，现在需要通过 epoll 来监听是否这些 Socket 都有事件发生。其中 epoll_create 创建一个 epoll 对象，也是一个文件，也对应一个文件描述符，同样也对应着打开文件列表中的一项。在这项里面有一个红黑树，在红黑树里，要保存这个 epoll 要监听的所有 Socket。

当 epoll_ctl 添加一个 Socket 的时候，其实是加入这个红黑树，同时红黑树里面的节点指向一个结构，将这个结构挂在被监听的 Socket 的事件列表中。当一个 Socket 来了一个事件的时候，可以从这个列表中得到 epoll 对象，并调用 call back 通知它。

这种通知方式使得监听的 Socket 数据增加的时候，效率不会大幅度降低，能够同时监听的 Socket 的数目也非常的多了。上限就为系统定义的、进程打开的最大文件描述符个数。因而，epoll 被称为解决 C10K 问题的利器。

小结

好了，这一节就到这里了，我们来总结一下：

你需要记住 TCP 和 UDP 的 Socket 的编程中，客户端和服务端都需要调用哪些函数；

写一个能够支撑大量连接的高并发的服务端不容易，需要多进程、多线程，而 epoll 机制能解决 C10K 问题。

最后，给你留两个思考题：

epoll 是 Linux 上的函数，那你知道 Windows 上对应的机制是什么吗？如果想实现一个跨平台的程序，你知道应该怎么办吗？

自己写 Socket 还是挺复杂的，写个 HTTP 的应用可能简单一些。那你知道 HTTP 的工作机制吗？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第14讲 | HTTP协议：看个新闻原来这么麻烦

前面讲述完传输层，接下来开始讲应用层的协议。从哪里开始讲呢，就从咱们最常用的 HTTP 协议开始。

HTTP 协议，几乎是每个人上网用的第一个协议，同时也是很容易被人忽略的协议。

既然说看新闻，咱们就先登录 http://www.163.com 。

http://www.163.com 是个 URL，叫作统一资源定位符。之所以叫统一，是因为它是有格式的。HTTP 称为协议，www.163.com 是一个域名，表示互联网上的一个位置。有的 URL 会有更详细的位置标识，例如 http://www.163.com/index.html 。正是因为这个东西是统一的，所以当你把这样一个字符串输入到浏览器的框里的时候，浏览器才知道如何进行统一处理。

HTTP 请求的准备

浏览器会将 www.163.com 这个域名发送给 DNS 服务器，让它解析为 IP 地址。有关 DNS 的过程，其实非常复杂，这个在后面专门介绍 DNS 的时候，我会详细描述，这里我们先不管，反正它会被解析成为 IP 地址。那接下来是发送 HTTP 请求吗？

不是的，HTTP 是基于 TCP 协议的，当然是要先建立 TCP 连接了，怎么建立呢？还记得第 11 节讲过的三次握手吗？

目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面，默认是开启了 Keep-Alive 的，这样建立的 TCP 连接，就可以在多次请求中复用。

学习了 TCP 之后，你应该知道，TCP 的三次握手和四次挥手，还是挺费劲的。如果好不容易建立了连接，然后就做了一点儿事情就结束了，有点儿浪费人力和物力。

HTTP 请求的构建

建立了连接以后，浏览器就要发送 HTTP 的请求。

请求的格式就像这样。

HTTP 的报文大概分为三大部分。第一部分是请求行，第二部分是请求的首部，第三部分才是请求的正文实体。

第一部分：请求行

在请求行中，URL 就是 http://www.163.com ，版本为 HTTP 1.1。这里要说一下的，就是方法。方法有几种类型。

对于访问网页来讲，最常用的类型就是GET。顾名思义，GET 就是去服务器获取一些资源。对于访问网页来讲，要获取的资源往往是一个页面。其实也有很多其他的格式，比如说返回一个 JSON 字符串，到底要返回什么，是由服务器端的实现决定的。

例如，在云计算中，如果我们的服务器端要提供一个基于 HTTP 协议的 API，获取所有云主机的列表，这就会使用 GET 方法得到，返回的可能是一个 JSON 字符串。字符串里面是一个列表，列表里面是一项的云主机的信息。

另外一种类型叫做POST。它需要主动告诉服务端一些信息，而非获取。要告诉服务端什么呢？一般会放在正文里面。正文可以有各种各样的格式。常见的格式也是 JSON。

例如，我们下一节要讲的支付场景，客户端就需要把“我是谁？我要支付多少？我要买啥？”告诉服务器，这就需要通过 POST 方法。

再如，在云计算里，如果我们的服务器端，要提供一个基于 HTTP 协议的创建云主机的 API，也会用到 POST 方法。这个时候往往需要将“我要创建多大的云主机？多少 CPU 多少内存？多大硬盘？”这些信息放在 JSON 字符串里面，通过 POST 的方法告诉服务器端。

还有一种类型叫PUT，就是向指定资源位置上传最新内容。但是，HTTP 的服务器往往是不允许上传文件的，所以 PUT 和 POST 就都变成了要传给服务器东西的方法。

在实际使用过程中，这两者还会有稍许的区别。POST 往往是用来创建一个资源的，而 PUT 往往是用来修改一个资源的。

例如，云主机已经创建好了，我想对这个云主机打一个标签，说明这个云主机是生产环境的，另外一个云主机是测试环境的。那怎么修改这个标签呢？往往就是用 PUT 方法。

再有一种常见的就是DELETE。这个顾名思义就是用来删除资源的。例如，我们要删除一个云主机，就会调用 DELETE 方法。

第二部分：首部字段

请求行下面就是我们的首部字段。首部是 key value，通过冒号分隔。这里面，往往保存了一些非常重要的字段。

例如，Accept-Charset，表示客户端可以接受的字符集。防止传过来的是另外的字符集，从而导致出现乱码。

再如，Content-Type是指正文的格式。例如，我们进行 POST 的请求，如果正文是 JSON，那么我们就应该将这个值设置为 JSON。

这里需要重点说一下的就是缓存。为啥要使用缓存呢？那是因为一个非常大的页面有很多东西。

例如，我浏览一个商品的详情，里面有这个商品的价格、库存、展示图片、使用手册等等。商品的展示图片会保持较长时间不变，而库存会根据用户购买的情况经常改变。如果图片非常大，而库存数非常小，如果我们每次要更新数据的时候都要刷新整个页面，对于服务器的压力就会很大。

对于这种高并发场景下的系统，在真正的业务逻辑之前，都需要有个接入层，将这些静态资源的请求拦在最外面。

这个架构的图就像这样。

其中 DNS、CDN 我在后面的章节会讲。和这一节关系比较大的就是 Nginx 这一层，它如何处理 HTTP 协议呢？对于静态资源，有 Vanish 缓存层。当缓存过期的时候，才会访问真正的 Tomcat 应用集群。

在 HTTP 头里面，Cache-control是用来控制缓存的。当客户端发送的请求中包含 max-age 指令时，如果判定缓存层中，资源的缓存时间数值比指定时间的数值小，那么客户端可以接受缓存的资源；当指定 max-age 值为 0，那么缓存层通常需要将请求转发给应用集群。

另外，If-Modified-Since也是一个关于缓存的。也就是说，如果服务器的资源在某个时间之后更新了，那么客户端就应该下载最新的资源；如果没有更新，服务端会返回“304 Not Modified”的响应，那客户端就不用下载了，也会节省带宽。

到此为止，我们仅仅是拼凑起了 HTTP 请求的报文格式，接下来，浏览器会把它交给下一层传输层。怎么交给传输层呢？其实也无非是用 Socket 这些东西，只不过用的浏览器里，这些程序不需要你自己写，有人已经帮你写好了。

HTTP 请求的发送

HTTP 协议是基于 TCP 协议的，所以它使用面向连接的方式发送请求，通过 stream 二进制流的方式传给对方。当然，到了 TCP 层，它会把二进制流变成一个的报文段发送给服务器。

在发送给每个报文段的时候，都需要对方有一个回应 ACK，来保证报文可靠地到达了对方。如果没有回应，那么 TCP 这一层会进行重新传输，直到可以到达。同一个包有可能被传了好多次，但是 HTTP 这一层不需要知道这一点，因为是 TCP 这一层在埋头苦干。

TCP 层发送每一个报文的时候，都需要加上自己的地址（即源地址）和它想要去的地方（即目标地址），将这两个信息放到 IP 头里面，交给 IP 层进行传输。

IP 层需要查看目标地址和自己是否是在同一个局域网。如果是，就发送 ARP 协议来请求这个目标地址对应的 MAC 地址，然后将源 MAC 和目标 MAC 放入 MAC 头，发送出去即可；如果不在同一个局域网，就需要发送到网关，还要需要发送 ARP 协议，来获取网关的 MAC 地址，然后将源 MAC 和网关 MAC 放入 MAC 头，发送出去。

网关收到包发现 MAC 符合，取出目标 IP 地址，根据路由协议找到下一跳的路由器，获取下一跳路由器的 MAC 地址，将包发给下一跳路由器。

这样路由器一跳一跳终于到达目标的局域网。这个时候，最后一跳的路由器能够发现，目标地址就在自己的某一个出口的局域网上。于是，在这个局域网上发送 ARP，获得这个目标地址的 MAC 地址，将包发出去。

目标的机器发现 MAC 地址符合，就将包收起来；发现 IP 地址符合，根据 IP 头中协议项，知道自己上一层是 TCP 协议，于是解析 TCP 的头，里面有序列号，需要看一看这个序列包是不是我要的，如果是就放入缓存中然后返回一个 ACK，如果不是就丢弃。

TCP 头里面还有端口号，HTTP 的服务器正在监听这个端口号。于是，目标机器自然知道是 HTTP 服务器这个进程想要这个包，于是将包发给 HTTP 服务器。HTTP 服务器的进程看到，原来这个请求是要访问一个网页，于是就把这个网页发给客户端。

HTTP 返回的构建

HTTP 的返回报文也是有一定格式的。这也是基于 HTTP 1.1 的。

状态码会反应 HTTP 请求的结果。“200”意味着大吉大利；而我们最不想见的，就是“404”，也就是“服务端无法响应这个请求”。然后，短语会大概说一下原因。

接下来是返回首部的key value。

这里面，Retry-After表示，告诉客户端应该在多长时间以后再次尝试一下。“503 错误”是说“服务暂时不再和这个值配合使用”。

在返回的头部里面也会有Content-Type，表示返回的是 HTML，还是 JSON。

构造好了返回的 HTTP 报文，接下来就是把这个报文发送出去。还是交给 Socket 去发送，还是交给 TCP 层，让 TCP 层将返回的 HTML，也分成一个个小的段，并且保证每个段都可靠到达。

这些段加上 TCP 头后会交给 IP 层，然后把刚才的发送过程反向走一遍。虽然两次不一定走相同的路径，但是逻辑过程是一样的，一直到达客户端。

客户端发现 MAC 地址符合、IP 地址符合，于是就会交给 TCP 层。根据序列号看是不是自己要的报文段，如果是，则会根据 TCP 头中的端口号，发给相应的进程。这个进程就是浏览器，浏览器作为客户端也在监听某个端口。

当浏览器拿到了 HTTP 的报文。发现返回“200”，一切正常，于是就从正文中将 HTML 拿出来。HTML 是一个标准的网页格式。浏览器只要根据这个格式，展示出一个绚丽多彩的网页。

这就是一个正常的 HTTP 请求和返回的完整过程。

HTTP 2.0

当然 HTTP 协议也在不断地进化过程中，在 HTTP1.1 基础上便有了 HTTP 2.0。

HTTP 1.1 在应用层以纯文本的形式进行通信。每次通信都要带完整的 HTTP 的头，而且不考虑 pipeline 模式的话，每次的过程总是像上面描述的那样一去一回。这样在实时性、并发性上都存在问题。

为了解决这些问题，HTTP 2.0 会对 HTTP 的头进行一定的压缩，将原来每次都要携带的大量 key value 在两端建立一个索引表，对相同的头只发送索引表中的索引。

另外，HTTP 2.0 协议将一个 TCP 的连接中，切分成多个流，每个流都有自己的 ID，而且流可以是客户端发往服务端，也可以是服务端发往客户端。它其实只是一个虚拟的通道。流是有优先级的。

HTTP 2.0 还将所有的传输信息分割为更小的消息和帧，并对它们采用二进制格式编码。常见的帧有Header 帧，用于传输 Header 内容，并且会开启一个新的流。再就是Data 帧，用来传输正文实体。多个 Data 帧属于同一个流。

通过这两种机制，HTTP 2.0 的客户端可以将多个请求分到不同的流中，然后将请求内容拆成帧，进行二进制传输。这些帧可以打散乱序发送， 然后根据每个帧首部的流标识符重新组装，并且可以根据优先级，决定优先处理哪个流的数据。

我们来举一个例子。

假设我们的一个页面要发送三个独立的请求，一个获取 css，一个获取 js，一个获取图片 jpg。如果使用 HTTP 1.1 就是串行的，但是如果使用 HTTP 2.0，就可以在一个连接里，客户端和服务端都可以同时发送多个请求或回应，而且不用按照顺序一对一对应。

HTTP 2.0 其实是将三个请求变成三个流，将数据分成帧，乱序发送到一个 TCP 连接中。

HTTP 2.0 成功解决了 HTTP 1.1 的队首阻塞问题，同时，也不需要通过 HTTP 1.x 的 pipeline 机制用多条 TCP 连接来实现并行请求与响应；减少了 TCP 连接数对服务器性能的影响，同时将页面的多个数据 css、js、 jpg 等通过一个数据链接进行传输，能够加快页面组件的传输速度。

QUIC 协议的“城会玩”

HTTP 2.0 虽然大大增加了并发性，但还是有问题的。因为 HTTP 2.0 也是基于 TCP 协议的，TCP 协议在处理包时是有严格顺序的。

当其中一个数据包遇到问题，TCP 连接需要等待这个包完成重传之后才能继续进行。虽然 HTTP 2.0 通过多个 stream，使得逻辑上一个 TCP 连接上的并行内容，进行多路数据的传输，然而这中间并没有关联的数据。一前一后，前面 stream 2 的帧没有收到，后面 stream 1 的帧也会因此阻塞。

于是，就又到了从 TCP 切换到 UDP，进行“城会玩”的时候了。这就是 Google 的 QUIC 协议，接下来我们来看它是如何“城会玩”的。

机制一：自定义连接机制

我们都知道，一条 TCP 连接是由四元组标识的，分别是源 IP、源端口、目的 IP、目的端口。一旦一个元素发生变化时，就需要断开重连，重新连接。在移动互联情况下，当手机信号不稳定或者在 WIFI 和 移动网络切换时，都会导致重连，从而进行再次的三次握手，导致一定的时延。

这在 TCP 是没有办法的，但是基于 UDP，就可以在 QUIC 自己的逻辑里面维护连接的机制，不再以四元组标识，而是以一个 64 位的随机数作为 ID 来标识，而且 UDP 是无连接的，所以当 IP 或者端口变化的时候，只要 ID 不变，就不需要重新建立连接。

机制二：自定义重传机制

前面我们讲过，TCP 为了保证可靠性，通过使用序号和应答机制，来解决顺序问题和丢包问题。

任何一个序号的包发过去，都要在一定的时间内得到应答，否则一旦超时，就会重发这个序号的包。那怎么样才算超时呢？还记得我们提过的自适应重传算法吗？这个超时是通过采样往返时间 RTT不断调整的。

其实，在 TCP 里面超时的采样存在不准确的问题。例如，发送一个包，序号为 100，发现没有返回，于是再发送一个 100，过一阵返回一个 ACK101。这个时候客户端知道这个包肯定收到了，但是往返时间是多少呢？是 ACK 到达的时间减去后一个 100 发送的时间，还是减去前一个 100 发送的时间呢？事实是，第一种算法把时间算短了，第二种算法把时间算长了。

QUIC 也有个序列号，是递增的。任何一个序列号的包只发送一次，下次就要加一了。例如，发送一个包，序号是 100，发现没有返回；再次发送的时候，序号就是 101 了；如果返回的 ACK 100，就是对第一个包的响应。如果返回 ACK 101 就是对第二个包的响应，RTT 计算相对准确。

但是这里有一个问题，就是怎么知道包 100 和包 101 发送的是同样的内容呢？QUIC 定义了一个 offset 概念。QUIC 既然是面向连接的，也就像 TCP 一样，是一个数据流，发送的数据在这个数据流里面有个偏移量 offset，可以通过 offset 查看数据发送到了哪里，这样只要这个 offset 的包没有来，就要重发；如果来了，按照 offset 拼接，还是能够拼成一个流。

机制三：无阻塞的多路复用

有了自定义的连接和重传机制，我们就可以解决上面 HTTP 2.0 的多路复用问题。

同 HTTP 2.0 一样，同一条 QUIC 连接上可以创建多个 stream，来发送多个 HTTP 请求。但是，QUIC 是基于 UDP 的，一个连接上的多个 stream 之间没有依赖。这样，假如 stream2 丢了一个 UDP 包，后面跟着 stream3 的一个 UDP 包，虽然 stream2 的那个包需要重传，但是 stream3 的包无需等待，就可以发给用户。

机制四：自定义流量控制

TCP 的流量控制是通过滑动窗口协议。QUIC 的流量控制也是通过 window_update，来告诉对端它可以接受的字节数。但是 QUIC 的窗口是适应自己的多路复用机制的，不但在一个连接上控制窗口，还在一个连接中的每个 stream 控制窗口。

还记得吗？在 TCP 协议中，接收端的窗口的起始点是下一个要接收并且 ACK 的包，即便后来的包都到了，放在缓存里面，窗口也不能右移，因为 TCP 的 ACK 机制是基于序列号的累计应答，一旦 ACK 了一个系列号，就说明前面的都到了，所以只要前面的没到，后面的到了也不能 ACK，就会导致后面的到了，也有可能超时重传，浪费带宽。

QUIC 的 ACK 是基于 offset 的，每个 offset 的包来了，进了缓存，就可以应答，应答后就不会重发，中间的空挡会等待到来或者重发即可，而窗口的起始位置为当前收到的最大 offset，从这个 offset 到当前的 stream 所能容纳的最大缓存，是真正的窗口大小。显然，这样更加准确。

另外，还有整个连接的窗口，需要对于所有的 stream 的窗口做一个统计。

小结

好了，今天就讲到这里，我们来总结一下：

HTTP 协议虽然很常用，也很复杂，重点记住 GET、POST、 PUT、DELETE 这几个方法，以及重要的首部字段；

HTTP 2.0 通过头压缩、分帧、二进制编码、多路复用等技术提升性能；

QUIC 协议通过基于 UDP 自定义的类似 TCP 的连接、重试、多路复用、流量控制技术，进一步提升性能。

接下来，给你留两个思考题吧。

QUIC 是一个精巧的协议，所以它肯定不止今天我提到的四种机制，你知道它还有哪些吗？

这一节主要讲了如何基于 HTTP 浏览网页，如果要传输比较敏感的银行卡信息，该怎么办呢？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第15讲 | HTTPS协议：点外卖的过程原来这么复杂

用 HTTP 协议，看个新闻还没有问题，但是换到更加严肃的场景中，就存在很多的安全风险。例如，你要下单做一次支付，如果还是使用普通的 HTTP 协议，那你很可能会被黑客盯上。

你发送一个请求，说我要点个外卖，但是这个网络包被截获了，于是在服务器回复你之前，黑客先假装自己就是外卖网站，然后给你回复一个假的消息说：“好啊好啊，来来来，银行卡号、密码拿来。”如果这时候你真把银行卡密码发给它，那你就真的上套了。

那怎么解决这个问题呢？当然一般的思路就是加密。加密分为两种方式一种是对称加密，一种是非对称加密。

在对称加密算法中，加密和解密使用的密钥是相同的。也就是说，加密和解密使用的是同一个密钥。因此，对称加密算法要保证安全性的话，密钥要做好保密。只能让使用的人知道，不能对外公开。

在非对称加密算法中，加密使用的密钥和解密使用的密钥是不相同的。一把是作为公开的公钥，另一把是作为谁都不能给的私钥。公钥加密的信息，只有私钥才能解密。私钥加密的信息，只有公钥才能解密。

因为对称加密算法相比非对称加密算法来说，效率要高得多，性能也好，所以交互的场景下多用对称加密。

对称加密

假设你和外卖网站约定了一个密钥，你发送请求的时候用这个密钥进行加密，外卖网站用同样的密钥进行解密。这样就算中间的黑客截获了你的请求，但是它没有密钥，还是破解不了。

这看起来很完美，但是中间有个问题，你们两个怎么来约定这个密钥呢？如果这个密钥在互联网上传输，也是很有可能让黑客截获的。黑客一旦截获这个秘钥，它可以佯作不知，静静地等着你们两个交互。这时候你们之间互通的任何消息，它都能截获并且查看，就等你把银行卡账号和密码发出来。

我们在谍战剧里面经常看到这样的场景，就是特工破译的密码会有个密码本，截获无线电台，通过密码本就能将原文破解出来。怎么把密码本给对方呢？只能通过线下传输。

比如，你和外卖网站偷偷约定时间地点，它给你一个纸条，上面写着你们两个的密钥，然后说以后就用这个密钥在互联网上定外卖了。当然你们接头的时候，也会先约定一个口号，什么“天王盖地虎”之类的，口号对上了，才能把纸条给它。但是，“天王盖地虎”同样也是对称加密密钥，同样存在如何把“天王盖地虎”约定成口号的问题。而且在谍战剧中一对一接头可能还可以，在互联网应用中，客户太多，这样是不行的。

非对称加密

所以，只要是对称加密，就会永远在这个死循环里出不来，这个时候，就需要非对称加密介入进来。

非对称加密的私钥放在外卖网站这里，不会在互联网上传输，这样就能保证这个秘钥的私密性。但是，对应私钥的公钥，是可以在互联网上随意传播的，只要外卖网站把这个公钥给你，你们就可以愉快地互通了。

比如说你用公钥加密，说“我要定外卖”，黑客在中间就算截获了这个报文，因为它没有私钥也是解不开的，所以这个报文可以顺利到达外卖网站，外卖网站用私钥把这个报文解出来，然后回复，“那给我银行卡和支付密码吧”。

先别太乐观，这里还是有问题的。回复的这句话，是外卖网站拿私钥加密的，互联网上人人都可以把它打开，当然包括黑客。那外卖网站可以拿公钥加密吗？当然不能，因为它自己的私钥只有它自己知道，谁也解不开。

另外，这个过程还有一个问题，黑客也可以模拟发送“我要定外卖”这个过程的，因为它也有外卖网站的公钥。

为了解决这个问题，看来一对公钥私钥是不够的，客户端也需要有自己的公钥和私钥，并且客户端要把自己的公钥，给外卖网站。

这样，客户端给外卖网站发送的时候，用外卖网站的公钥加密。而外卖网站给客户端发送消息的时候，使用客户端的公钥。这样就算有黑客企图模拟客户端获取一些信息，或者半路截获回复信息，但是由于它没有私钥，这些信息它还是打不开。

数字证书

不对称加密也会有同样的问题，如何将不对称加密的公钥给对方呢？一种是放在一个公网的地址上，让对方下载；另一种就是在建立连接的时候，传给对方。

这两种方法有相同的问题，那就是，作为一个普通网民，你怎么鉴别别人给你的公钥是对的。会不会有人冒充外卖网站，发给你一个它的公钥。接下来，你和它所有的互通，看起来都是没有任何问题的。毕竟每个人都可以创建自己的公钥和私钥。

例如，我自己搭建了一个网站 cliu8site，可以通过这个命令先创建私钥。

openssl genrsa -out cliu8siteprivate.key 1024

然后，再根据这个私钥，创建对应的公钥。

openssl rsa -in cliu8siteprivate.key -pubout -outcliu8sitepublic.pem

这个时候就需要权威部门的介入了，就像每个人都可以打印自己的简历，说自己是谁，但是有公安局盖章的，就只有户口本，这个才能证明你是你。这个由权威部门颁发的称为证书（Certificate）。

证书里面有什么呢？当然应该有公钥，这是最重要的；还有证书的所有者，就像户口本上有你的姓名和身份证号，说明这个户口本是你的；另外还有证书的发布机构和证书的有效期，这个有点像身份证上的机构是哪个区公安局，有效期到多少年。

这个证书是怎么生成的呢？会不会有人假冒权威机构颁发证书呢？就像有假身份证、假户口本一样。生成证书需要发起一个证书请求，然后将这个请求发给一个权威机构去认证，这个权威机构我们称为CA（ Certificate Authority）。

证书请求可以通过这个命令生成。

openssl req -key cliu8siteprivate.key -new -out cliu8sitecertificate.req

将这个请求发给权威机构，权威机构会给这个证书卡一个章，我们称为 ** 签名算法。** 问题又来了，那怎么签名才能保证是真的权威机构签名的呢？当然只有用只掌握在权威机构手里的东西签名了才行，这就是 CA 的私钥。

签名算法大概是这样工作的：一般是对信息做一个 Hash 计算，得到一个 Hash 值，这个过程是不可逆的，也就是说无法通过 Hash 值得出原来的信息内容。在把信息发送出去时，把这个 Hash 值加密后，作为一个签名和信息一起发出去。

权威机构给证书签名的命令是这样的。

openssl x509 -req -in cliu8sitecertificate.req -CA cacertificate.pem -CAkey caprivate.key -out cliu8sitecertificate.pem

这个命令会返回 Signature ok，而 cliu8sitecertificate.pem 就是签过名的证书。CA 用自己的私钥给外卖网站的公钥签名，就相当于给外卖网站背书，形成了外卖网站的证书。

我们来查看这个证书的内容。

openssl x509 -in cliu8sitecertificate.pem -noout -text

这里面有个 Issuer，也即证书是谁颁发的；Subject，就是证书颁发给谁；Validity 是证书期限；Public-key 是公钥内容；Signature Algorithm 是签名算法。

这下好了，你不会从外卖网站上得到一个公钥，而是会得到一个证书，这个证书有个发布机构 CA，你只要得到这个发布机构 CA 的公钥，去解密外卖网站证书的签名，如果解密成功了，Hash 也对的上，就说明这个外卖网站的公钥没有啥问题。

你有没有发现，又有新问题了。要想验证证书，需要 CA 的公钥，问题是，你怎么确定 CA 的公钥就是对的呢？

所以，CA 的公钥也需要更牛的 CA 给它签名，然后形成 CA 的证书。要想知道某个 CA 的证书是否可靠，要看 CA 的上级证书的公钥，能不能解开这个 CA 的签名。就像你不相信区公安局，可以打电话问市公安局，让市公安局确认区公安局的合法性。这样层层上去，直到全球皆知的几个著名大 CA，称为root CA，做最后的背书。通过这种层层授信背书的方式，从而保证了非对称加密模式的正常运转。

除此之外，还有一种证书，称为Self-Signed Certificate，就是自己给自己签名。这个给人一种“我就是我，你爱信不信”的感觉。这里我就不多说了。

HTTPS 的工作模式

我们可以知道，非对称加密在性能上不如对称加密，那是否能将两者结合起来呢？例如，公钥私钥主要用于传输对称加密的秘钥，而真正的双方大数据量的通信都是通过对称加密进行的。

当然是可以的。这就是 HTTPS 协议的总体思路。

当你登录一个外卖网站的时候，由于是 HTTPS，客户端会发送 Client Hello 消息到服务器，以明文传输 TLS 版本信息、加密套件候选列表、压缩算法候选列表等信息。另外，还会有一个随机数，在协商对称密钥的时候使用。

这就类似在说：“您好，我想定外卖，但你要保密我吃的是什么。这是我的加密套路，再给你个随机数，你留着。”

然后，外卖网站返回 Server Hello 消息, 告诉客户端，服务器选择使用的协议版本、加密套件、压缩算法等，还有一个随机数，用于后续的密钥协商。

这就类似在说：“您好，保密没问题，你的加密套路还挺多，咱们就按套路 2 来吧，我这里也有个随机数，你也留着。”

然后，外卖网站会给你一个服务器端的证书，然后说：“Server Hello Done，我这里就这些信息了。”

你当然不相信这个证书，于是你从自己信任的 CA 仓库中，拿 CA 的证书里面的公钥去解密外卖网站的证书。如果能够成功，则说明外卖网站是可信的。这个过程中，你可能会不断往上追溯 CA、CA 的 CA、CA 的 CA 的 CA，反正直到一个授信的 CA，就可以了。

证书验证完毕之后，觉得这个外卖网站可信，于是客户端计算产生随机数字 Pre-master，发送 Client Key Exchange，用证书中的公钥加密，再发送给服务器，服务器可以通过私钥解密出来。

到目前为止，无论是客户端还是服务器，都有了三个随机数，分别是：自己的、对端的，以及刚生成的 Pre-Master 随机数。通过这三个随机数，可以在客户端和服务器产生相同的对称密钥。

有了对称密钥，客户端就可以说：“Change Cipher Spec，咱们以后都采用协商的通信密钥和加密算法进行加密通信了。”

然后发送一个 Encrypted Handshake Message，将已经商定好的参数等，采用协商密钥进行加密，发送给服务器用于数据与握手验证。

同样，服务器也可以发送 Change Cipher Spec，说：“没问题，咱们以后都采用协商的通信密钥和加密算法进行加密通信了”，并且也发送 Encrypted Handshake Message 的消息试试。当双方握手结束之后，就可以通过对称密钥进行加密传输了。

这个过程除了加密解密之外，其他的过程和 HTTP 是一样的，过程也非常复杂。

上面的过程只包含了 HTTPS 的单向认证，也即客户端验证服务端的证书，是大部分的场景，也可以在更加严格安全要求的情况下，启用双向认证，双方互相验证证书。

重放与篡改

其实，这里还有一些没有解决的问题，例如重放和篡改的问题。

没错，有了加密和解密，黑客截获了包也打不开了，但是它可以发送 N 次。这个往往通过 Timestamp 和 Nonce 随机数联合起来，然后做一个不可逆的签名来保证。

Nonce 随机数保证唯一，或者 Timestamp 和 Nonce 合起来保证唯一，同样的，请求只接受一次，于是服务器多次受到相同的 Timestamp 和 Nonce，则视为无效即可。

如果有人想篡改 Timestamp 和 Nonce，还有签名保证不可篡改性，如果改了用签名算法解出来，就对不上了，可以丢弃了。

小结

好了，这一节就到这里了，我们来总结一下。

加密分对称加密和非对称加密。对称加密效率高，但是解决不了密钥传输问题；非对称加密可以解决这个问题，但是效率不高。

非对称加密需要通过证书和权威机构来验证公钥的合法性。

HTTPS 是综合了对称加密和非对称加密算法的 HTTP 协议。既保证传输安全，也保证传输效率。

最后，给你留两个思考题：

HTTPS 协议比较复杂，沟通过程太繁复，这样会导致效率问题，那你知道有哪些手段可以解决这些问题吗？

HTTP 和 HTTPS 协议的正文部分传输个 JSON 什么的还好，如果播放视频，就有问题了，那这个时候，应该使用什么协议呢？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第16讲 | 流媒体协议：如何在直播里看到美女帅哥？

最近直播比较火，很多人都喜欢看直播，那一个直播系统里面都有哪些组成部分，都使用了什么协议呢？

无论是直播还是点播，其实都是对于视频数据的传输。一提到视频，大家都爱看，但是一提到视频技术，大家都头疼，因为名词实在是太多了。

三个名词系列

我这里列三个名词系列，你先大致有个印象。

名词系列一：AVI、MPEG、RMVB、MP4、MOV、FLV、WebM、WMV、ASF、MKV。例如 RMVB 和 MP4，看着是不是很熟悉？

名词系列二：H.261、 H.262、H.263、H.264、H.265。这个是不是就没怎么听过了？别着急，你先记住，要重点关注 H.264。

名词系列三：MPEG-1、MPEG-2、MPEG-4、MPEG-7。MPEG 好像听说过，但是后面的数字是怎么回事？是不是又熟悉又陌生？

这里，我想问你个问题，视频是什么？我说，其实就是快速播放一连串连续的图片。

每一张图片，我们称为一帧。只要每秒钟帧的数据足够多，也即播放得足够快。比如每秒 30 帧，以人的眼睛的敏感程度，是看不出这是一张张独立的图片的，这就是我们常说的帧率（FPS）。

每一张图片，都是由像素组成的，假设为 1024*768（这个像素数不算多）。每个像素由 RGB 组成，每个 8 位，共 24 位。

我们来算一下，每秒钟的视频有多大？

30 帧 × 1024 × 768 × 24 = 566,231,040Bits = 70,778,880Bytes

如果一分钟呢？4,246,732,800Bytes，已经是 4 个 G 了。

是不是不算不知道，一算吓一跳？这个数据量实在是太大，根本没办法存储和传输。如果这样存储，你的硬盘很快就满了；如果这样传输，那多少带宽也不够用啊！

怎么办呢？人们想到了编码，就是看如何用尽量少的 Bit 数保存视频，使播放的时候画面看起来仍然很精美。编码是一个压缩的过程。

视频和图片的压缩过程有什么特点？

之所以能够对视频流中的图片进行压缩，因为视频和图片有这样一些特点。

空间冗余：图像的相邻像素之间有较强的相关性，一张图片相邻像素往往是渐变的，不是突变的，没必要每个像素都完整地保存，可以隔几个保存一个，中间的用算法计算出来。

时间冗余：视频序列的相邻图像之间内容相似。一个视频中连续出现的图片也不是突变的，可以根据已有的图片进行预测和推断。

视觉冗余：人的视觉系统对某些细节不敏感，因此不会每一个细节都注意到，可以允许丢失一些数据。

编码冗余：不同像素值出现的概率不同，概率高的用的字节少，概率低的用的字节多，类似霍夫曼编码（Huffman Coding）的思路。

总之，用于编码的算法非常复杂，而且多种多样，但是编码过程其实都是类似的。

﻿﻿

视频编码的两大流派

能不能形成一定的标准呢？要不然开发视频播放的人得累死了。当然能，我这里就给你介绍，视频编码的两大流派。

流派一：ITU（International Telecommunications Union）的 VCEG（Video Coding Experts Group），这个称为国际电联下的 VCEG。既然是电信，可想而知，他们最初做视频编码，主要侧重传输。名词系列二，就是这个组织制定的标准。

流派二：ISO（International Standards Organization）的 MPEG（Moving Picture Experts Group），这个是ISO 旗下的 MPEG，本来是做视频存储的。例如，编码后保存在 VCD 和 DVD 中。当然后来也慢慢侧重视频传输了。名词系列三，就是这个组织制定的标准。

后来，ITU-T（国际电信联盟电信标准化部门，ITU Telecommunication Standardization Sector）与 MPEG 联合制定了 H.264/MPEG-4 AVC，这才是我们这一节要重点关注的。

经过编码之后，生动活泼的一帧一帧的图像，就变成了一串串让人看不懂的二进制，这个二进制可以放在一个文件里面，按照一定的格式保存起来，这就是名词系列一。

其实这些就是视频保存成文件的格式。例如，前几个字节是什么意义，后几个字节是什么意义，然后是数据，数据中保存的就是编码好的结果。

如何在直播里看到帅哥美女？

当然，这个二进制也可以通过某种网络协议进行封装，放在互联网上传输，这个时候就可以进行网络直播了。

网络协议将编码好的视频流，从主播端推送到服务器，在服务器上有个运行了同样协议的服务端来接收这些网络包，从而得到里面的视频流，这个过程称为接流。

服务端接到视频流之后，可以对视频流进行一定的处理，例如转码，也即从一个编码格式，转成另一种格式。因为观众使用的客户端千差万别，要保证他们都能看到直播。

流处理完毕之后，就可以等待观众的客户端来请求这些视频流。观众的客户端请求的过程称为拉流。

如果有非常多的观众，同时看一个视频直播，那都从一个服务器上拉流，压力太大了，因而需要一个视频的分发网络，将视频预先加载到就近的边缘节点，这样大部分观众看的视频，是从边缘节点拉取的，就能降低服务器的压力。

当观众的客户端将视频流拉下来之后，就需要进行解码，也即通过上述过程的逆过程，将一串串看不懂的二进制，再转变成一帧帧生动的图片，在客户端播放出来，这样你就能看到美女帅哥啦。

整个直播过程，可以用这个的图来描述。

接下来，我们依次来看一下每个过程。

编码：如何将丰富多彩的图片变成二进制流？

虽然我们说视频是一张张图片的序列，但是如果每张图片都完整，就太大了，因而会将视频序列分成三种帧。

I 帧，也称关键帧。里面是完整的图片，只需要本帧数据，就可以完成解码。

P 帧，前向预测编码帧。P 帧表示的是这一帧跟之前的一个关键帧（或 P 帧）的差别，解码时需要用之前缓存的画面，叠加上和本帧定义的差别，生成最终画面。

B 帧，双向预测内插编码帧。B 帧记录的是本帧与前后帧的差别。要解码 B 帧，不仅要取得之前的缓存画面，还要解码之后的画面，通过前后画面的数据与本帧数据的叠加，取得最终的画面。

可以看出，I 帧最完整，B 帧压缩率最高，而压缩后帧的序列，应该是在 IBBP 的间隔出现的。这就是通过时序进行编码。

在一帧中，分成多个片，每个片中分成多个宏块，每个宏块分成多个子块，这样将一张大的图分解成一个个小块，可以方便进行空间上的编码。

尽管时空非常立体的组成了一个序列，但是总归还是要压缩成一个二进制流。这个流是有结构的，是一个个的网络提取层单元（NALU，Network Abstraction Layer Unit）。变成这种格式就是为了传输，因为网络上的传输，默认的是一个个的包，因而这里也就分成了一个个的单元。

﻿﻿

每一个 NALU 首先是一个起始标识符，用于标识 NALU 之间的间隔；然后是 NALU 的头，里面主要配置了 NALU 的类型；最终 Payload 里面是 NALU 承载的数据。

在 NALU 头里面，主要的内容是类型NAL Type。

0x07 表示 SPS，是序列参数集， 包括一个图像序列的所有信息，如图像尺寸、视频格式等。

0x08 表示 PPS，是图像参数集，包括一个图像的所有分片的所有相关信息，包括图像类型、序列号等。

在传输视频流之前，必须要传输这两类参数，不然无法解码。为了保证容错性，每一个 I 帧前面，都会传一遍这两个参数集合。

如果 NALU Header 里面的表示类型是 SPS 或者 PPS，则 Payload 中就是真正的参数集的内容。

如果类型是帧，则 Payload 中才是正的视频数据，当然也是一帧一帧存放的，前面说了，一帧的内容还是挺多的，因而每一个 NALU 里面保存的是一片。对于每一片，到底是 I 帧，还是 P 帧，还是 B 帧，在片结构里面也有个 Header，这里面有个类型，然后是片的内容。

这样，整个格式就出来了，一个视频，可以拆分成一系列的帧，每一帧拆分成一系列的片，每一片都放在一个 NALU 里面，NALU 之间都是通过特殊的起始标识符分隔，在每一个 I 帧的第一片前面，要插入单独保存 SPS 和 PPS 的 NALU，最终形成一个长长的 NALU 序列。

推流：如何把数据流打包传输到对端？

那这个格式是不是就能够直接在网上传输到对端，开始直播了呢？其实还不是，还需要将这个二进制的流打包成网络包进行发送，这里我们使用RTMP 协议。这就进入了第二个过程，推流。

RTMP 是基于 TCP 的，因而肯定需要双方建立一个 TCP 的连接。在有 TCP 的连接的基础上，还需要建立一个 RTMP 的连接，也即在程序里面，你需要调用 RTMP 类库的 Connect 函数，显示创建一个连接。

RTMP 为什么需要建立一个单独的连接呢？

因为它们需要商量一些事情，保证以后的传输能正常进行。主要就是两个事情，一个是版本号，如果客户端、服务器的版本号不一致，则不能工作。另一个就是时间戳，视频播放中，时间是很重要的，后面的数据流互通的时候，经常要带上时间戳的差值，因而一开始双方就要知道对方的时间戳。

未来沟通这些事情，需要发送六条消息：客户端发送 C0、C1、 C2，服务器发送 S0、 S1、 S2。

首先，客户端发送 C0 表示自己的版本号，不必等对方的回复，然后发送 C1 表示自己的时间戳。

服务器只有在收到 C0 的时候，才能返回 S0，表明自己的版本号，如果版本不匹配，可以断开连接。

服务器发送完 S0 后，也不用等什么，就直接发送自己的时间戳 S1。客户端收到 S1 的时候，发一个知道了对方时间戳的 ACK C2。同理服务器收到 C1 的时候，发一个知道了对方时间戳的 ACK S2。

于是，握手完成。

﻿﻿

握手之后，双方需要互相传递一些控制信息，例如 Chunk 块的大小、窗口大小等。

真正传输数据的时候，还是需要创建一个流 Stream，然后通过这个 Stream 来推流 publish。

推流的过程，就是将 NALU 放在 Message 里面发送，这个也称为RTMP Packet 包。Message 的格式就像这样。

﻿﻿

发送的时候，去掉 NALU 的起始标识符。因为这部分对于 RTMP 协议来讲没有用。接下来，将 SPS 和 PPS 参数集封装成一个 RTMP 包发送，然后发送一个个片的 NALU。

RTMP 在收发数据的时候并不是以 Message 为单位的，而是把 Message 拆分成 Chunk 发送，而且必须在一个 Chunk 发送完成之后，才能开始发送下一个 Chunk。每个 Chunk 中都带有 Message ID，表示属于哪个 Message，接收端也会按照这个 ID 将 Chunk 组装成 Message。

前面连接的时候，设置的 Chunk 块大小就是指这个 Chunk。将大的消息变为小的块再发送，可以在低带宽的情况下，减少网络拥塞。

这有一个分块的例子，你可以看一下。

假设一个视频的消息长度为 307，但是 Chunk 大小约定为 128，于是会拆分为三个 Chunk。

第一个 Chunk 的 Type＝0，表示 Chunk 头是完整的；头里面 Timestamp 为 1000，总长度 Length 为 307，类型为 9，是个视频，Stream ID 为 12346，正文部分承担 128 个字节的 Data。

第二个 Chunk 也要发送 128 个字节，Chunk 头由于和第一个 Chunk 一样，因此采用 Chunk Type＝3，表示头一样就不再发送了。

第三个 Chunk 要发送的 Data 的长度为 307-128-128=51 个字节，还是采用 Type＝3。

就这样数据就源源不断到达流媒体服务器，整个过程就像这样。

这个时候，大量观看直播的观众就可以通过 RTMP 协议从流媒体服务器上拉取，但是这么多的用户量，都去同一个地方拉取，服务器压力会很大，而且用户分布在全国甚至全球，如果都去统一的一个地方下载，也会时延比较长，需要有分发网络。

分发网络分为中心和边缘两层。边缘层服务器部署在全国各地及横跨各大运营商里，和用户距离很近。中心层是流媒体服务集群，负责内容的转发。智能负载均衡系统，根据用户的地理位置信息，就近选择边缘服务器，为用户提供推 / 拉流服务。中心层也负责转码服务，例如，把 RTMP 协议的码流转换为 HLS 码流。

﻿﻿

这套机制在后面的 DNS、HTTPDNS、CDN 的章节会更有详细的描述。

拉流：观众的客户端如何看到视频？

接下来，我们再来看观众的客户端通过 RTMP 拉流的过程。

﻿﻿

先读到的是 H.264 的解码参数，例如 SPS 和 PPS，然后对收到的 NALU 组成的一个个帧，进行解码，交给播发器播放，一个绚丽多彩的视频画面就出来了。

小结

好了，今天的内容就到这里了，我们来总结一下：

视频名词比较多，编码两大流派达成了一致，都是通过时间、空间的各种算法来压缩数据；

压缩好的数据，为了传输组成一系列 NALU，按照帧和片依次排列；

排列好的 NALU，在网络传输的时候，要按照 RTMP 包的格式进行包装，RTMP 的包会拆分成 Chunk 进行传输；

推送到流媒体集群的视频流经过转码和分发，可以被客户端通过 RTMP 协议拉取，然后组合为 NALU，解码成视频格式进行播放。

最后，给你留两个思考题：

你觉得基于 RTMP 的视频流传输的机制存在什么问题？如何进行优化？

在线看视频之前，大家都是把电影下载下来看的，电影这么大，你知道如何快速下载吗？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第17讲 | P2P协议：我下小电影，99%急死你

如果你想下载一个电影，一般会通过什么方式呢？

当然，最简单的方式就是通过HTTP进行下载。但是相信你有过这样的体验，通过浏览器下载的时候，只要文件稍微大点，下载的速度就奇慢无比。

还有种下载文件的方式，就是通过FTP，也即文件传输协议。FTP 采用两个 TCP 连接来传输一个文件。

控制连接：服务器以被动的方式，打开众所周知用于 FTP 的端口 21，客户端则主动发起连接。该连接将命令从客户端传给服务器，并传回服务器的应答。常用的命令有：list——获取文件目录；reter——取一个文件；store——存一个文件。

数据连接：每当一个文件在客户端与服务器之间传输时，就创建一个数据连接。

FTP 的两种工作模式

每传输一个文件，都要建立一个全新的数据连接。FTP 有两种工作模式，分别是主动模式（PORT）和被动模式（PASV），这些都是站在 FTP 服务器的角度来说的。

主动模式下，客户端随机打开一个大于 1024 的端口 N，向服务器的命令端口 21 发起连接，同时开放 N+1 端口监听，并向服务器发出 “port N+1” 命令，由服务器从自己的数据端口 20，主动连接到客户端指定的数据端口 N+1。

被动模式下，当开启一个 FTP 连接时，客户端打开两个任意的本地端口 N（大于 1024）和 N+1。第一个端口连接服务器的 21 端口，提交 PASV 命令。然后，服务器会开启一个任意的端口 P（大于 1024），返回“227 entering passive mode”消息，里面有 FTP 服务器开放的用来进行数据传输的端口。客户端收到消息取得端口号之后，会通过 N+1 号端口连接服务器的端口 P，然后在两个端口之间进行数据传输。

P2P 是什么？

但是无论是 HTTP 的方式，还是 FTP 的方式，都有一个比较大的缺点，就是难以解决单一服务器的带宽压力， 因为它们使用的都是传统的客户端服务器的方式。

后来，一种创新的、称为 P2P 的方式流行起来。P2P就是peer-to-peer。资源开始并不集中地存储在某些设备上，而是分散地存储在多台设备上。这些设备我们姑且称为 peer。

想要下载一个文件的时候，你只要得到那些已经存在了文件的 peer，并和这些 peer 之间，建立点对点的连接，而不需要到中心服务器上，就可以就近下载文件。一旦下载了文件，你也就成为 peer 中的一员，你旁边的那些机器，也可能会选择从你这里下载文件，所以当你使用 P2P 软件的时候，例如 BitTorrent，往往能够看到，既有下载流量，也有上传的流量，也即你自己也加入了这个 P2P 的网络，自己从别人那里下载，同时也提供给其他人下载。可以想象，这种方式，参与的人越多，下载速度越快，一切完美。

种子（.torrent）文件

但是有一个问题，当你想下载一个文件的时候，怎么知道哪些 peer 有这个文件呢？

这就用到种子啦，也即咱们比较熟悉的.torrent 文件。.torrent 文件由两部分组成，分别是：announce（tracker URL）和文件信息。

文件信息里面有这些内容。

info 区：这里指定的是该种子有几个文件、文件有多长、目录结构，以及目录和文件的名字。

Name 字段：指定顶层目录名字。

每个段的大小：BitTorrent（简称 BT）协议把一个文件分成很多个小段，然后分段下载。

段哈希值：将整个种子中，每个段的 SHA-1 哈希值拼在一起。

下载时，BT 客户端首先解析.torrent 文件，得到 tracker 地址，然后连接 tracker 服务器。tracker 服务器回应下载者的请求，将其他下载者（包括发布者）的 IP 提供给下载者。下载者再连接其他下载者，根据.torrent 文件，两者分别对方告知自己已经有的块，然后交换对方没有的数据。此时不需要其他服务器参与，并分散了单个线路上的数据流量，因此减轻了服务器的负担。

下载者每得到一个块，需要算出下载块的 Hash 验证码，并与.torrent 文件中的对比。如果一样，则说明块正确，不一样则需要重新下载这个块。这种规定是为了解决下载内容的准确性问题。

从这个过程也可以看出，这种方式特别依赖 tracker。tracker 需要收集下载者信息的服务器，并将此信息提供给其他下载者，使下载者们相互连接起来，传输数据。虽然下载的过程是非中心化的，但是加入这个 P2P 网络的时候，都需要借助 tracker 中心服务器，这个服务器是用来登记有哪些用户在请求哪些资源。

所以，这种工作方式有一个弊端，一旦 tracker 服务器出现故障或者线路遭到屏蔽，BT 工具就无法正常工作了。

去中心化网络（DHT）

那能不能彻底非中心化呢？

于是，后来就有了一种叫作DHT（Distributed Hash Table）的去中心化网络。每个加入这个 DHT 网络的人，都要负责存储这个网络里的资源信息和其他成员的联系信息，相当于所有人一起构成了一个庞大的分布式存储数据库。

有一种著名的 DHT 协议，叫Kademlia 协议。这个和区块链的概念一样，很抽象，我来详细讲一下这个协议。

任何一个 BitTorrent 启动之后，它都有两个角色。一个是peer，监听一个 TCP 端口，用来上传和下载文件，这个角色表明，我这里有某个文件。另一个角色DHT node，监听一个 UDP 的端口，通过这个角色，这个节点加入了一个 DHT 的网络。

在 DHT 网络里面，每一个 DHT node 都有一个 ID。这个 ID 是一个很长的串。每个 DHT node 都有责任掌握一些知识，也就是文件索引，也即它应该知道某些文件是保存在哪些节点上。它只需要有这些知识就可以了，而它自己本身不一定就是保存这个文件的节点。

哈希值

当然，每个 DHT node 不会有全局的知识，也即不知道所有的文件保存在哪里，它只需要知道一部分。那应该知道哪一部分呢？这就需要用哈希算法计算出来。

每个文件可以计算出一个哈希值，而DHT node 的 ID 是和哈希值相同长度的串。

DHT 算法是这样规定的：如果一个文件计算出一个哈希值，则和这个哈希值一样的那个 DHT node，就有责任知道从哪里下载这个文件，即便它自己没保存这个文件。

当然不一定这么巧，总能找到和哈希值一模一样的，有可能一模一样的 DHT node 也下线了，所以 DHT 算法还规定：除了一模一样的那个 DHT node 应该知道，ID 和这个哈希值非常接近的 N 个 DHT node 也应该知道。

什么叫和哈希值接近呢？例如只修改了最后一位，就很接近；修改了倒数 2 位，也不远；修改了倒数 3 位，也可以接受。总之，凑齐了规定的 N 这个数就行。

刚才那个图里，文件 1 通过哈希运算，得到匹配 ID 的 DHT node 为 node C，当然还会有其他的，我这里没有画出来。所以，node C 有责任知道文件 1 的存放地址，虽然 node C 本身没有存放文件 1。

同理，文件 2 通过哈希运算，得到匹配 ID 的 DHT node 为 node E，但是 node D 和 E 的 ID 值很近，所以 node D 也知道。当然，文件 2 本身没有必要一定在 node D 和 E 里，但是碰巧这里就在 E 那有一份。

接下来一个新的节点 node new 上线了。如果想下载文件 1，它首先要加入 DHT 网络，如何加入呢？

在这种模式下，种子.torrent 文件里面就不再是 tracker 的地址了，而是一个 list 的 node 的地址，而所有这些 node 都是已经在 DHT 网络里面的。当然随着时间的推移，很可能有退出的，有下线的，但是我们假设，不会所有的都联系不上，总有一个能联系上。

node new 只要在种子里面找到一个 DHT node，就加入了网络。

node new 会计算文件 1 的哈希值，并根据这个哈希值了解到，和这个哈希值匹配，或者很接近的 node 上知道如何下载这个文件，例如计算出来的哈希值就是 node C。

但是 node new 不知道怎么联系上 node C，因为种子里面的 node 列表里面很可能没有 node C，但是它可以问，DHT 网络特别像一个社交网络，node new 只有去它能联系上的 node 问，你们知道不知道 node C 的联系方式呀？

在 DHT 网络中，每个 node 都保存了一定的联系方式，但是肯定没有 node 的所有联系方式。DHT 网络中，节点之间通过互相通信，也会交流联系方式，也会删除联系方式。和人们的方式一样，你有你的朋友圈，你的朋友有它的朋友圈，你们互相加微信，就互相认识了，过一段时间不联系，就删除朋友关系。

有个理论是，社交网络中，任何两个人直接的距离不超过六度，也即你想联系比尔盖茨，也就六个人就能够联系到了。

所以，node new 想联系 node C，就去万能的朋友圈去问，并且求转发，朋友再问朋友，很快就能找到。如果找不到 C，也能找到和 C 的 ID 很像的节点，它们也知道如何下载文件 1。

在 node C 上，告诉 node new，下载文件 1，要去 B、D、 F，于是 node new 选择和 node B 进行 peer 连接，开始下载，它一旦开始下载，自己本地也有文件 1 了，于是 node new 告诉 node C 以及和 node C 的 ID 很像的那些节点，我也有文件 1 了，可以加入那个文件拥有者列表了。

但是你会发现 node new 上没有文件索引，但是根据哈希算法，一定会有某些文件的哈希值是和 node new 的 ID 匹配上的。在 DHT 网络中，会有节点告诉它，你既然加入了咱们这个网络，你也有责任知道某些文件的下载地址。

好了，一切都分布式了。

这里面遗留几个细节的问题。

DHT node ID 以及文件哈希是个什么东西？

节点 ID 是一个随机选择的 160bits（20 字节）空间，文件的哈希也使用这样的 160bits 空间。

所谓 ID 相似，具体到什么程度算相似？

在 Kademlia 网络中，距离是通过异或（XOR）计算的。我们就不以 160bits 举例了。我们以 5 位来举例。

01010 与 01000 的距离，就是两个 ID 之间的异或值，为 00010，也即为 2。 01010 与 00010 的距离为 01000，也即为 8,。01010 与 00011 的距离为 01001，也即 8+1=9 。以此类推，高位不同的，表示距离更远一些；低位不同的，表示距离更近一些，总的距离为所有的不同的位的距离之和。

这个距离不能比喻为地理位置，因为在 Kademlia 网络中，位置近不算近，ID 近才算近，所以我把这个距离比喻为社交距离，也即在朋友圈中的距离，或者社交网络中的距离。这个和你住的位置没有关系，和人的经历关系比较大。

还是以 5 位 ID 来举例，就像在领英中，排第一位的表示最近一份工作在哪里，第二位的表示上一份工作在哪里，然后第三位的是上上份工作，第四位的是研究生在哪里读，第五位的表示大学在哪里读。

如果你是一个猎头，在上面找候选人，当然最近的那份工作是最重要的。而对于工作经历越丰富的候选人，大学在哪里读的反而越不重要。

DHT 网络中的朋友圈是怎么维护的？

就像人一样，虽然我们常联系人的只有少数，但是朋友圈里肯定是远近都有。DHT 网络的朋友圈也是一样，远近都有，并且按距离分层。

假设某个节点的 ID 为 01010，如果一个节点的 ID，前面所有位数都与它相同，只有最后 1 位不同。这样的节点只有 1 个，为 01011。与基础节点的异或值为 00001，即距离为 1；对于 01010 而言，这样的节点归为“k-bucket 1”。

如果一个节点的 ID，前面所有位数都相同，从倒数第 2 位开始不同，这样的节点只有 2 个，即 01000 和 01001，与基础节点的异或值为 00010 和 00011，即距离范围为 2 和 3；对于 01010 而言，这样的节点归为“k-bucket 2”。

如果一个节点的 ID，前面所有位数相同，从倒数第 i 位开始不同，这样的节点只有 2^(i-1) 个，与基础节点的距离范围为 [2^(i-1), 2^i)；对于 01010 而言，这样的节点归为“k-bucket i”。

最终到从倒数 160 位就开始都不同。

你会发现，差距越大，陌生人越多，但是朋友圈不能都放下，所以每一层都只放 K 个，这是参数可以配置。

DHT 网络是如何查找朋友的？

假设，node A 的 ID 为 00110，要找 node B ID 为 10000，异或距离为 10110，距离范围在 [2^4, 2^5)，所以这个目标节点可能在“k-bucket 5”中，这就说明 B 的 ID 与 A 的 ID 从第 5 位开始不同，所以 B 可能在“k-bucket 5”中。

然后，A 看看自己的 k-bucket 5 有没有 B。如果有，太好了，找到你了；如果没有，在 k-bucket 5 里随便找一个 C。因为是二进制，C、B 都和 A 的第 5 位不同，那么 C 的 ID 第 5 位肯定与 B 相同，即它与 B 的距离会小于 2^4，相当于比 A、B 之间的距离缩短了一半以上。

再请求 C，在它自己的通讯录里，按同样的查找方式找一下 B。如果 C 知道 B，就告诉 A；如果 C 也不知道 B，那 C 按同样的搜索方法，可以在自己的通讯录里找到一个离 B 更近的 D 朋友（D、B 之间距离小于 2^3），把 D 推荐给 A，A 请求 D 进行下一步查找。

Kademlia 的这种查询机制，是通过折半查找的方式来收缩范围，对于总的节点数目为 N，最多只需要查询 log2(N) 次，就能够找到。

例如，图中这个最差的情况。

A 和 B 每一位都不一样，所以相差 31，A 找到的朋友 C，不巧正好在中间。和 A 的距离是 16，和 B 距离为 15，于是 C 去自己朋友圈找的时候，不巧找到 D，正好又在中间，距离 C 为 8，距离 B 为 7。于是 D 去自己朋友圈找的时候，不巧找到 E，正好又在中间，距离 D 为 4，距离 B 为 3，E 在朋友圈找到 F，距离 E 为 2，距离 B 为 1，最终在 F 的朋友圈距离 1 的地方找到 B。当然这是最最不巧的情况，每次找到的朋友都不远不近，正好在中间。

如果碰巧了，在 A 的朋友圈里面有 G，距离 B 只有 3，然后在 G 的朋友圈里面一下子就找到了 B，两次就找到了。

在 DHT 网络中，朋友之间怎么沟通呢？

Kademlia 算法中，每个节点只有 4 个指令。

PING：测试一个节点是否在线，还活着没，相当于打个电话，看还能打通不。

STORE：要求一个节点存储一份数据，既然加入了组织，有义务保存一份数据。

FIND_NODE：根据节点 ID 查找一个节点，就是给一个 160 位的 ID，通过上面朋友圈的方式找到那个节点。

FIND_VALUE：根据 KEY 查找一个数据，实则上跟 FIND_NODE 非常类似。KEY 就是文件对应的 160 位的 ID，就是要找到保存了文件的节点。

DHT 网络中，朋友圈如何更新呢？

每个 bucket 里的节点，都按最后一次接触的时间倒序排列，这就相当于，朋友圈里面最近联系过的人往往是最熟的。

每次执行四个指令中的任意一个都会触发更新。

当一个节点与自己接触时，检查它是否已经在 k-bucket 中，也就是说是否已经在朋友圈。如果在，那么将它挪到 k-bucket 列表的最底，也就是最新的位置，刚联系过，就置顶一下，方便以后多联系；如果不在，新的联系人要不要加到通讯录里面呢？假设通讯录已满的情况，PING 一下列表最上面，也即最旧的一个节点。如果 PING 通了，将旧节点挪到列表最底，并丢弃新节点，老朋友还是留一下；如果 PING 不通，删除旧节点，并将新节点加入列表，这人联系不上了，删了吧。

这个机制保证了任意节点加入和离开都不影响整体网络。

小结

好了，今天的讲解就到这里了，我们总结一下：

下载一个文件可以使用 HTTP 或 FTP，这两种都是集中下载的方式，而 P2P 则换了一种思路，采取非中心化下载的方式；

P2P 也是有两种，一种是依赖于 tracker 的，也即元数据集中，文件数据分散；另一种是基于分布式的哈希算法，元数据和文件数据全部分散。

接下来，给你留两个思考题：

除了这种去中心化分布式哈希的算法，你还能想到其他的应用场景吗？

在前面所有的章节中，要下载一个文件，都需要使用域名。但是网络通信是使用 IP 的，那你知道怎么实现两者的映射机制吗？

我们的专栏马上更新过半了，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第18讲 | DNS协议：网络世界的地址簿

前面我们讲了平时常见的看新闻、支付、直播、下载等场景，现在网站的数目非常多，常用的网站就有二三十个，如果全部用 IP 地址进行访问，恐怕很难记住。于是，就需要一个地址簿，根据名称，就可以查看具体的地址。

例如，我要去西湖边的“外婆家”，这就是名称，然后通过地址簿，查看到底是哪条路多少号。

DNS 服务器

在网络世界，也是这样的。你肯定记得住网站的名称，但是很难记住网站的 IP 地址，因而也需要一个地址簿，就是DNS 服务器。

由此可见，DNS 在日常生活中多么重要。每个人上网，都需要访问它，但是同时，这对它来讲也是非常大的挑战。一旦它出了故障，整个互联网都将瘫痪。另外，上网的人分布在全世界各地，如果大家都去同一个地方访问某一台服务器，时延将会非常大。因而，DNS 服务器，一定要设置成高可用、高并发和分布式的。

于是，就有了这样树状的层次结构。

﻿

根 DNS 服务器 ：返回顶级域 DNS 服务器的 IP 地址

顶级域 DNS 服务器：返回权威 DNS 服务器的 IP 地址

权威 DNS 服务器 ：返回相应主机的 IP 地址

DNS 解析流程

为了提高 DNS 的解析性能，很多网络都会就近部署 DNS 缓存服务器。于是，就有了以下的 DNS 解析流程。

电脑客户端会发出一个 DNS 请求，问 www.163.com 的 IP 是啥啊，并发给本地域名服务器 (本地 DNS)。那本地域名服务器 (本地 DNS) 是什么呢？如果是通过 DHCP 配置，本地 DNS 由你的网络服务商（ISP），如电信、移动等自动分配，它通常就在你网络服务商的某个机房。

本地 DNS 收到来自客户端的请求。你可以想象这台服务器上缓存了一张域名与之对应 IP 地址的大表格。如果能找到 www.163.com，它直接就返回 IP 地址。如果没有，本地 DNS 会去问它的根域名服务器：“老大，能告诉我 www.163.com 的 IP 地址吗？”根域名服务器是最高层次的，全球共有 13 套。它不直接用于域名解析，但能指明一条道路。

根 DNS 收到来自本地 DNS 的请求，发现后缀是 .com，说：“哦，www.163.com 啊，这个域名是由.com 区域管理，我给你它的顶级域名服务器的地址，你去问问它吧。”

本地 DNS 转向问顶级域名服务器：“老二，你能告诉我 www.163.com 的 IP 地址吗？”顶级域名服务器就是大名鼎鼎的比如 .com、.net、 .org 这些一级域名，它负责管理二级域名，比如 163.com，所以它能提供一条更清晰的方向。

顶级域名服务器说：“我给你负责 www.163.com 区域的权威 DNS 服务器的地址，你去问它应该能问到。”

本地 DNS 转向问权威 DNS 服务器：“您好，www.163.com 对应的 IP 是啥呀？”163.com 的权威 DNS 服务器，它是域名解析结果的原出处。为啥叫权威呢？就是我的域名我做主。

权限 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。

本地 DNS 再将 IP 地址返回客户端，客户端和目标建立连接。

至此，我们完成了 DNS 的解析过程。现在总结一下，整个过程我画成了一个图。

负载均衡

站在客户端角度，这是一次DNS 递归查询过程。因为本地 DNS 全权为它效劳，它只要坐等结果即可。在这个过程中，DNS 除了可以通过名称映射为 IP 地址，它还可以做另外一件事，就是负载均衡。

还是以访问“外婆家”为例，还是我们开头的“外婆家”，但是，它可能有很多地址，因为它在杭州可以有很多家。所以，如果一个人想去吃“外婆家”，他可以就近找一家店，而不用大家都去同一家，这就是负载均衡。

DNS 首先可以做内部负载均衡。

例如，一个应用要访问数据库，在这个应用里面应该配置这个数据库的 IP 地址，还是应该配置这个数据库的域名呢？显然应该配置域名，因为一旦这个数据库，因为某种原因，换到了另外一台机器上，而如果有多个应用都配置了这台数据库的话，一换 IP 地址，就需要将这些应用全部修改一遍。但是如果配置了域名，则只要在 DNS 服务器里，将域名映射为新的 IP 地址，这个工作就完成了，大大简化了运维。

在这个基础上，我们可以再进一步。例如，某个应用要访问另外一个应用，如果配置另外一个应用的 IP 地址，那么这个访问就是一对一的。但是当被访问的应用撑不住的时候，我们其实可以部署多个。但是，访问它的应用，如何在多个之间进行负载均衡？只要配置成为域名就可以了。在域名解析的时候，我们只要配置策略，这次返回第一个 IP，下次返回第二个 IP，就可以实现负载均衡了。

另外一个更加重要的是，DNS 还可以做全局负载均衡。

为了保证我们的应用高可用，往往会部署在多个机房，每个地方都会有自己的 IP 地址。当用户访问某个域名的时候，这个 IP 地址可以轮询访问多个数据中心。如果一个数据中心因为某种原因挂了，只要在 DNS 服务器里面，将这个数据中心对应的 IP 地址删除，就可以实现一定的高可用。

另外，我们肯定希望北京的用户访问北京的数据中心，上海的用户访问上海的数据中心，这样，客户体验就会非常好，访问速度就会超快。这就是全局负载均衡的概念。

示例：DNS 访问数据中心中对象存储上的静态资源

我们通过 DNS 访问数据中心中对象存储上的静态资源为例，看一看整个过程。

假设全国有多个数据中心，托管在多个运营商，每个数据中心三个可用区（Available Zone）。对象存储通过跨可用区部署，实现高可用性。在每个数据中心中，都至少部署两个内部负载均衡器，内部负载均衡器后面对接多个对象存储的前置服务器（Proxy-server）。

﻿

当一个客户端要访问 object.yourcompany.com 的时候，需要将域名转换为 IP 地址进行访问，所以它要请求本地 DNS 解析器。

本地 DNS 解析器先查看看本地的缓存是否有这个记录。如果有则直接使用，因为上面的过程太复杂了，如果每次都要递归解析，就太麻烦了。

如果本地无缓存，则需要请求本地的 DNS 服务器。

本地的 DNS 服务器一般部署在你的数据中心或者你所在的运营商的网络中，本地 DNS 服务器也需要看本地是否有缓存，如果有则返回，因为它也不想把上面的递归过程再走一遍。

至 7. 如果本地没有，本地 DNS 才需要递归地从根 DNS 服务器，查到.com 的顶级域名服务器，最终查到 yourcompany.com 的权威 DNS 服务器，给本地 DNS 服务器，权威 DNS 服务器按说会返回真实要访问的 IP 地址。

对于不需要做全局负载均衡的简单应用来讲，yourcompany.com 的权威 DNS 服务器可以直接将 object.yourcompany.com 这个域名解析为一个或者多个 IP 地址，然后客户端可以通过多个 IP 地址，进行简单的轮询，实现简单的负载均衡。

但是对于复杂的应用，尤其是跨地域跨运营商的大型应用，则需要更加复杂的全局负载均衡机制，因而需要专门的设备或者服务器来做这件事情，这就是全局负载均衡器（GSLB，Global Server Load Balance）。

在 yourcompany.com 的 DNS 服务器中，一般是通过配置 CNAME 的方式，给 object.yourcompany.com 起一个别名，例如 object.vip.yourcomany.com，然后告诉本地 DNS 服务器，让它请求 GSLB 解析这个域名，GSLB 就可以在解析这个域名的过程中，通过自己的策略实现负载均衡。

图中画了两层的 GSLB，是因为分运营商和地域。我们希望不同运营商的客户，可以访问相同运营商机房中的资源，这样不跨运营商访问，有利于提高吞吐量，减少时延。

第一层 GSLB，通过查看请求它的本地 DNS 服务器所在的运营商，就知道用户所在的运营商。假设是移动，通过 CNAME 的方式，通过另一个别名 object.yd.yourcompany.com，告诉本地 DNS 服务器去请求第二层的 GSLB。

第二层 GSLB，通过查看请求它的本地 DNS 服务器所在的地址，就知道用户所在的地理位置，然后将距离用户位置比较近的 Region 里面，六个内部负载均衡（SLB，Server Load Balancer）的地址，返回给本地 DNS 服务器。

本地 DNS 服务器将结果返回给本地 DNS 解析器。

本地 DNS 解析器将结果缓存后，返回给客户端。

客户端开始访问属于相同运营商的距离较近的 Region 1 中的对象存储，当然客户端得到了六个 IP 地址，它可以通过负载均衡的方式，随机或者轮询选择一个可用区进行访问。对象存储一般会有三个备份，从而可以实现对存储读写的负载均衡。

小结

好了，这节内容就到这里了，我们来总结一下：

DNS 是网络世界的地址簿，可以通过域名查地址，因为域名服务器是按照树状结构组织的，因而域名查找是使用递归的方法，并通过缓存的方式增强性能；

在域名和 IP 的映射过程中，给了应用基于域名做负载均衡的机会，可以是简单的负载均衡，也可以根据地址和运营商做全局的负载均衡。

最后，给你留两个思考题：

全局负载均衡为什么要分地址和运营商呢？

全局负载均衡使用过程中，常常遇到失灵的情况，你知道具体有哪些情况吗？对应应该怎么来解决呢？

我们的专栏更新过半了，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第19讲 | HTTPDNS：网络世界的地址簿也会指错路

上一节我们知道了 DNS 的两项功能，第一是根据名称查到具体的地址，另外一个是可以针对多个地址做负载均衡，而且可以在多个地址中选择一个距离你近的地方访问。

然而有时候这个地址簿也经常给你指错路，明明距离你 500 米就有个吃饭的地方，非要把你推荐到 5 公里外。为什么会出现这样的情况呢？

还记得吗？当我们发出请求解析 DNS 的时候，首先，会先连接到运营商本地的 DNS 服务器，由这个服务器帮我们去整棵 DNS 树上进行解析，然后将解析的结果返回给客户端。但是本地的 DNS 服务器，作为一个本地导游，往往有自己的“小心思”。

传统 DNS 存在哪些问题？

1. 域名缓存问题

它可以在本地做一个缓存，也就是说，不是每一个请求，它都会去访问权威 DNS 服务器，而是访问过一次就把结果缓存到自己本地，当其他人来问的时候，直接就返回这个缓存数据。

这就相当于导游去过一个饭店，自己脑子记住了地址，当有一个游客问的时候，他就凭记忆回答了，不用再去查地址簿。这样经常存在的一个问题是，人家那个饭店明明都已经搬了，结果作为导游，他并没有刷新这个缓存，结果你辛辛苦苦到了这个地点，发现饭店已经变成了服装店，你是不是会非常失望？

另外，有的运营商会把一些静态页面，缓存到本运营商的服务器内，这样用户请求的时候，就不用跨运营商进行访问，这样既加快了速度，也减少了运营商之间流量计算的成本。在域名解析的时候，不会将用户导向真正的网站，而是指向这个缓存的服务器。

很多情况下是看不出问题的，但是当页面更新，用户会访问到老的页面，问题就出来了。例如，你听说一个餐馆推出了一个新菜，你想去尝一下。结果导游告诉你，在这里吃也是一样的。有的游客会觉得没问题，但是对于想尝试新菜的人来说，如果导游说带你去，但其实并没有吃到新菜，你是不是也会非常失望呢？

再就是本地的缓存，往往使得全局负载均衡失败，因为上次进行缓存的时候，缓存中的地址不一定是这次访问离客户最近的地方，如果把这个地址返回给客户，那肯定就会绕远路。

就像上一次客户要吃西湖醋鱼的事，导游知道西湖边有一家，因为当时游客就在西湖边，可是，下一次客户在灵隐寺，想吃西湖醋鱼的时候，导游还指向西湖边的那一家，那这就绕的太远了。

2. 域名转发问题

缓存问题还是说本地域名解析服务，还是会去权威 DNS 服务器中查找，只不过不是每次都要查找。可以说这还是大导游、大中介。还有一些小导游、小中介，有了请求之后，直接转发给其他运营商去做解析，自己只是外包了出去。

这样的问题是，如果是 A 运营商的客户，访问自己运营商的 DNS 服务器，如果 A 运营商去权威 DNS 服务器查询的话，权威 DNS 服务器知道你是 A 运营商的，就返回给一个部署在 A 运营商的网站地址，这样针对相同运营商的访问，速度就会快很多。

但是 A 运营商偷懒，将解析的请求转发给 B 运营商，B 运营商去权威 DNS 服务器查询的话，权威服务器会误认为，你是 B 运营商的，那就返回给你一个在 B 运营商的网站地址吧，结果客户的每次访问都要跨运营商，速度就会很慢。

﻿﻿

3. 出口 NAT 问题

前面讲述网关的时候，我们知道，出口的时候，很多机房都会配置NAT，也即网络地址转换，使得从这个网关出去的包，都换成新的 IP 地址，当然请求返回的时候，在这个网关，再将 IP 地址转换回去，所以对于访问来说是没有任何问题。

但是一旦做了网络地址的转换，权威的 DNS 服务器，就没办法通过这个地址，来判断客户到底是来自哪个运营商，而且极有可能因为转换过后的地址，误判运营商，导致跨运营商的访问。

4. 域名更新问题

本地 DNS 服务器是由不同地区、不同运营商独立部署的。对域名解析缓存的处理上，实现策略也有区别，有的会偷懒，忽略域名解析结果的 TTL 时间限制，在权威 DNS 服务器解析变更的时候，解析结果在全网生效的周期非常漫长。但是有的时候，在 DNS 的切换中，场景对生效时间要求比较高。

例如双机房部署的时候，跨机房的负载均衡和容灾多使用 DNS 来做。当一个机房出问题之后，需要修改权威 DNS，将域名指向新的 IP 地址，但是如果更新太慢，那很多用户都会出现访问异常。

这就像，有的导游比较勤快、敬业，时时刻刻关注酒店、餐馆、交通的变化，问他的时候，往往会得到最新情况。有的导游懒一些，8 年前背的导游词就没换过，问他的时候，指的路往往就是错的。

5. 解析延迟问题

从上一节的 DNS 查询过程来看，DNS 的查询过程需要递归遍历多个 DNS 服务器，才能获得最终的解析结果，这会带来一定的时延，甚至会解析超时。

HTTPDNS 的工作模式

既然 DNS 解析中有这么多问题，那怎么办呢？难不成退回到直接用 IP 地址？这样显然不合适，所以就有了HTTPDNS。

HTTPNDS 其实就是，不走传统的 DNS 解析，而是自己搭建基于 HTTP 协议的 DNS 服务器集群，分布在多个地点和多个运营商。当客户端需要 DNS 解析的时候，直接通过 HTTP 协议进行请求这个服务器集群，得到就近的地址。

这就相当于每家基于 HTTP 协议，自己实现自己的域名解析，自己做一个自己的地址簿，而不使用统一的地址簿。但是默认的域名解析都是走 DNS 的，因而使用 HTTPDNS 需要绕过默认的 DNS 路径，就不能使用默认的客户端。使用 HTTPDNS 的，往往是手机应用，需要在手机端嵌入支持 HTTPDNS 的客户端 SDK。

通过自己的 HTTPDNS 服务器和自己的 SDK，实现了从依赖本地导游，到自己上网查询做旅游攻略，进行自由行，爱怎么玩怎么玩。这样就能够避免依赖导游，而导游又不专业，你还不能把他怎么样的尴尬。

下面我来解析一下HTTPDNS 的工作模式。

在客户端的 SDK 里动态请求服务端，获取 HTTPDNS 服务器的 IP 列表，缓存到本地。随着不断地解析域名，SDK 也会在本地缓存 DNS 域名解析的结果。

当手机应用要访问一个地址的时候，首先看是否有本地的缓存，如果有就直接返回。这个缓存和本地 DNS 的缓存不一样的是，这个是手机应用自己做的，而非整个运营商统一做的。如何更新、何时更新，手机应用的客户端可以和服务器协调来做这件事情。

如果本地没有，就需要请求 HTTPDNS 的服务器，在本地 HTTPDNS 服务器的 IP 列表中，选择一个发出 HTTP 的请求，会返回一个要访问的网站的 IP 列表。

请求的方式是这样的。

curl http://106.2.xxx.xxx/d?dn=c.m.163.com

{"dns":[{"host":"c.m.163.com","ips":["223.252.199.12"],"ttl":300,"http2":0}],"client":{"ip":"106.2.81.50","line":269692944}}

手机客户端自然知道手机在哪个运营商、哪个地址。由于是直接的 HTTP 通信，HTTPDNS 服务器能够准确知道这些信息，因而可以做精准的全局负载均衡。

当然，当所有这些都不工作的时候，可以切换到传统的 LocalDNS 来解析，慢也比访问不到好。那 HTTPDNS 是如何解决上面的问题的呢？

其实归结起来就是两大问题。一是解析速度和更新速度的平衡问题，二是智能调度的问题，对应的解决方案是 HTTPDNS 的缓存设计和调度设计。

HTTPDNS 的缓存设计

解析 DNS 过程复杂，通信次数多，对解析速度造成很大影响。为了加快解析，因而有了缓存，但是这又会产生缓存更新速度不及时的问题。最要命的是，这两个方面都掌握在别人手中，也即本地 DNS 服务器手中，它不会为你定制，你作为客户端干着急没办法。

而 HTTPDNS 就是将解析速度和更新速度全部掌控在自己手中。一方面，解析的过程，不需要本地 DNS 服务递归的调用一大圈，一个 HTTP 的请求直接搞定，要实时更新的时候，马上就能起作用；另一方面为了提高解析速度，本地也有缓存，缓存是在客户端 SDK 维护的，过期时间、更新时间，都可以自己控制。

HTTPDNS 的缓存设计策略也是咱们做应用架构中常用的缓存设计模式，也即分为客户端、缓存、数据源三层。

对于应用架构来讲，就是应用、缓存、数据库。常见的是 Tomcat、Redis、MySQL。

对于 HTTPDNS 来讲，就是手机客户端、DNS 缓存、HTTPDNS 服务器。

只要是缓存模式，就存在缓存的过期、更新、不一致的问题，解决思路也是很像的。

例如 DNS 缓存在内存中，也可以持久化到存储上，从而 APP 重启之后，能够尽快从存储中加载上次累积的经常访问的网站的解析结果，就不需要每次都全部解析一遍，再变成缓存。这有点像 Redis 是基于内存的缓存，但是同样提供持久化的能力，使得重启或者主备切换的时候，数据不会完全丢失。

SDK 中的缓存会严格按照缓存过期时间，如果缓存没有命中，或者已经过期，而且客户端不允许使用过期的记录，则会发起一次解析，保障记录是更新的。

解析可以同步进行，也就是直接调用 HTTPDNS 的接口，返回最新的记录，更新缓存；也可以异步进行，添加一个解析任务到后台，由后台任务调用 HTTPDNS 的接口。

同步更新的优点是实时性好，缺点是如果有多个请求都发现过期的时候，同时会请求 HTTPDNS 多次，其实是一种浪费。

同步更新的方式对应到应用架构中缓存的Cache-Aside 机制，也即先读缓存，不命中读数据库，同时将结果写入缓存。

异步更新的优点是，可以将多个请求都发现过期的情况，合并为一个对于 HTTPDNS 的请求任务，只执行一次，减少 HTTPDNS 的压力。同时可以在即将过期的时候，就创建一个任务进行预加载，防止过期之后再刷新，称为预加载。

它的缺点是当前请求拿到过期数据的时候，如果客户端允许使用过期数据，需要冒一次风险。如果过期的数据还能请求，就没问题；如果不能请求，则失败一次，等下次缓存更新后，再请求方能成功。

异步更新的机制对应到应用架构中缓存的Refresh-Ahead 机制，即业务仅仅访问缓存，当过期的时候定期刷新。在著名的应用缓存 Guava Cache 中，有个 RefreshAfterWrite 机制，对于并发情况下，多个缓存访问不命中从而引发并发回源的情况，可以采取只有一个请求回源的模式。在应用架构的缓存中，也常常用数据预热或者预加载的机制。

HTTPDNS 的调度设计

由于客户端嵌入了 SDK，因而就不会因为本地 DNS 的各种缓存、转发、NAT，让权威 DNS 服务器误会客户端所在的位置和运营商，而可以拿到第一手资料。

在客户端，可以知道手机是哪个国家、哪个运营商、哪个省，甚至哪个市，HTTPDNS 服务端可以根据这些信息，选择最佳的服务节点返回。

如果有多个节点，还会考虑错误率、请求时间、服务器压力、网络状况等，进行综合选择，而非仅仅考虑地理位置。当有一个节点宕机或者性能下降的时候，可以尽快进行切换。

要做到这一点，需要客户端使用 HTTPDNS 返回的 IP 访问业务应用。客户端的 SDK 会收集网络请求数据，如错误率、请求时间等网络请求质量数据，并发送到统计后台，进行分析、聚合，以此查看不同的 IP 的服务质量。

在服务端，应用可以通过调用 HTTPDNS 的管理接口，配置不同服务质量的优先级、权重。HTTPDNS 会根据这些策略综合地理位置和线路状况算出一个排序，优先访问当前那些优质的、时延低的 IP 地址。

HTTPDNS 通过智能调度之后返回的结果，也会缓存在客户端。为了不让缓存使得调度失真，客户端可以根据不同的移动网络运营商 WIFI 的 SSID 来分维度缓存。不同的运营商或者 WIFI 解析出来的结果会不同。

小结

好了，这节就到这里了，我们来总结一下，你需要记住这两个重点：

传统的 DNS 有很多问题，例如解析慢、更新不及时。因为缓存、转发、NAT 问题导致客户端误会自己所在的位置和运营商，从而影响流量的调度。

HTTPDNS 通过客户端 SDK 和服务端，通过 HTTP 直接调用解析 DNS 的方式，绕过了传统 DNS 的这些缺点，实现了智能的调度。

最后，给你留两个思考题。

使用 HTTPDNS，需要向 HTTPDNS 服务器请求解析域名，可是客户端怎么知道 HTTPDNS 服务器的地址或者域名呢？

HTTPDNS 的智能调度，主要是让客户端选择最近的服务器，而有另一种机制，使得资源分发到离客户端更近的位置，从而加快客户端的访问，你知道是什么技术吗？

我们的专栏更新过半了，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第20讲 | CDN：你去小卖部取过快递么？

上一节，我们看到了网站的一般访问模式。

当一个用户想访问一个网站的时候，指定这个网站的域名，DNS 就会将这个域名解析为地址，然后用户请求这个地址，返回一个网页。就像你要买个东西，首先要查找商店的位置，然后去商店里面找到自己想要的东西，最后拿着东西回家。

那这里面还有没有可以优化的地方呢？

例如你去电商网站下单买个东西，这个东西一定要从电商总部的中心仓库送过来吗？原来基本是这样的，每一单都是单独配送，所以你可能要很久才能收到你的宝贝。但是后来电商网站的物流系统学聪明了，他们在全国各地建立了很多仓库，而不是只有总部的中心仓库才可以发货。

电商网站根据统计大概知道，北京、上海、广州、深圳、杭州等地，每天能够卖出去多少书籍、卫生纸、包、电器等存放期比较长的物品。这些物品用不着从中心仓库发出，所以平时就可以将它们分布在各地仓库里，客户一下单，就近的仓库发出，第二天就可以收到了。

这样，用户体验大大提高。当然，这里面也有个难点就是，生鲜这类东西保质期太短，如果提前都备好货，但是没有人下单，那肯定就坏了。这个问题，我后文再说。

我们先说，我们的网站访问可以借鉴“就近配送”这个思路。

全球有这么多的数据中心，无论在哪里上网，临近不远的地方基本上都有数据中心。是不是可以在这些数据中心里部署几台机器，形成一个缓存的集群来缓存部分数据，那么用户访问数据的时候，就可以就近访问了呢？

当然是可以的。这些分布在各个地方的各个数据中心的节点，就称为边缘节点。

由于边缘节点数目比较多，但是每个集群规模比较小，不可能缓存下来所有东西，因而可能无法命中，这样就会在边缘节点之上。有区域节点，规模就要更大，缓存的数据会更多，命中的概率也就更大。在区域节点之上是中心节点，规模更大，缓存数据更多。如果还不命中，就只好回源网站访问了。

这就是CDN 的分发系统的架构。CDN 系统的缓存，也是一层一层的，能不访问后端真正的源，就不打扰它。这也是电商网站物流系统的思路，北京局找不到，找华北局，华北局找不到，再找北方局。

有了这个分发系统之后，接下来就是，客户端如何找到相应的边缘节点进行访问呢？

还记得我们讲过的基于 DNS 的全局负载均衡吗？这个负载均衡主要用来选择一个就近的同样运营商的服务器进行访问。你会发现，CDN 分发网络也是一个分布在多个区域、多个运营商的分布式系统，也可以用相同的思路选择最合适的边缘节点。

在没有 CDN 的情况下，用户向浏览器输入 www.web.com 这个域名，客户端访问本地 DNS 服务器的时候，如果本地 DNS 服务器有缓存，则返回网站的地址；如果没有，递归查询到网站的权威 DNS 服务器，这个权威 DNS 服务器是负责 web.com 的，它会返回网站的 IP 地址。本地 DNS 服务器缓存下 IP 地址，将 IP 地址返回，然后客户端直接访问这个 IP 地址，就访问到了这个网站。

然而有了 CDN 之后，情况发生了变化。在 web.com 这个权威 DNS 服务器上，会设置一个 CNAME 别名，指向另外一个域名 www.web.cdn.com，返回给本地 DNS 服务器。

当本地 DNS 服务器拿到这个新的域名时，需要继续解析这个新的域名。这个时候，再访问的就不是 web.com 的权威 DNS 服务器了，而是 web.cdn.com 的权威 DNS 服务器，这是 CDN 自己的权威 DNS 服务器。在这个服务器上，还是会设置一个 CNAME，指向另外一个域名，也即 CDN 网络的全局负载均衡器。

接下来，本地 DNS 服务器去请求 CDN 的全局负载均衡器解析域名，全局负载均衡器会为用户选择一台合适的缓存服务器提供服务，选择的依据包括：

根据用户 IP 地址，判断哪一台服务器距用户最近；

用户所处的运营商；

根据用户所请求的 URL 中携带的内容名称，判断哪一台服务器上有用户所需的内容；

查询各个服务器当前的负载情况，判断哪一台服务器尚有服务能力。

基于以上这些条件，进行综合分析之后，全局负载均衡器会返回一台缓存服务器的 IP 地址。

本地 DNS 服务器缓存这个 IP 地址，然后将 IP 返回给客户端，客户端去访问这个边缘节点，下载资源。缓存服务器响应用户请求，将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容，那么这台服务器就要向它的上一级缓存服务器请求内容，直至追溯到网站的源服务器将内容拉到本地。

CDN 可以进行缓存的内容有很多种。

保质期长的日用品比较容易缓存，因为不容易过期，对应到就像电商仓库系统里，就是静态页面、图片等，因为这些东西也不怎么变，所以适合缓存。

还记得这个接入层缓存的架构吗？在进入数据中心的时候，我们希望通过最外层接入层的缓存，将大部分静态资源的访问拦在边缘。而 CDN 则更进一步，将这些静态资源缓存到离用户更近的数据中心外。越接近客户，访问性能越好，时延越低。

但是静态内容中，有一种特殊的内容，也大量使用了 CDN，这个就是前面讲过的流媒体。

CDN 支持流媒体协议，例如前面讲过的 RTMP 协议。在很多情况下，这相当于一个代理，从上一级缓存读取内容，转发给用户。由于流媒体往往是连续的，因而可以进行预先缓存的策略，也可以预先推送到用户的客户端。

对于静态页面来讲，内容的分发往往采取拉取的方式，也即当发现未命中的时候，再去上一级进行拉取。但是，流媒体数据量大，如果出现回源，压力会比较大，所以往往采取主动推送的模式，将热点数据主动推送到边缘节点。

对于流媒体来讲，很多 CDN 还提供预处理服务，也即文件在分发之前，经过一定的处理。例如将视频转换为不同的码流，以适应不同的网络带宽的用户需求；再如对视频进行分片，降低存储压力，也使得客户端可以选择使用不同的码率加载不同的分片。这就是我们常见的，“我要看超清、标清、流畅等”。

对于流媒体 CDN 来讲，有个关键的问题是防盗链问题。因为视频是要花大价钱买版权的，为了挣点钱，收点广告费，如果流媒体被其他的网站盗走，在人家的网站播放，那损失可就大了。

最常用也最简单的方法就是HTTP 头的 refer 字段， 当浏览器发送请求的时候，一般会带上 referer，告诉服务器是从哪个页面链接过来的，服务器基于此可以获得一些信息用于处理。如果 refer 信息不是来自本站，就阻止访问或者跳到其它链接。

refer 的机制相对比较容易破解，所以还需要配合其他的机制。

一种常用的机制是时间戳防盗链。使用 CDN 的管理员可以在配置界面上，和 CDN 厂商约定一个加密字符串。

客户端取出当前的时间戳，要访问的资源及其路径，连同加密字符串进行签名算法得到一个字符串，然后生成一个下载链接，带上这个签名字符串和截止时间戳去访问 CDN。

在 CDN 服务端，根据取出过期时间，和当前 CDN 节点时间进行比较，确认请求是否过期。然后 CDN 服务端有了资源及路径，时间戳，以及约定的加密字符串，根据相同的签名算法计算签名，如果匹配则一致，访问合法，才会将资源返回给客户。

然而比如在电商仓库中，我在前面提过，有关生鲜的缓存就是非常麻烦的事情，这对应着就是动态的数据，比较难以缓存。怎么办呢？现在也有动态 CDN，主要有两种模式。

一种为生鲜超市模式，也即边缘计算的模式。既然数据是动态生成的，所以数据的逻辑计算和存储，也相应的放在边缘的节点。其中定时从源数据那里同步存储的数据，然后在边缘进行计算得到结果。就像对生鲜的烹饪是动态的，没办法事先做好缓存，因而将生鲜超市放在你家旁边，既能够送货上门，也能够现场烹饪，也是边缘计算的一种体现。

另一种是冷链运输模式，也即路径优化的模式。数据不是在边缘计算生成的，而是在源站生成的，但是数据的下发则可以通过 CDN 的网络，对路径进行优化。因为 CDN 节点较多，能够找到离源站很近的边缘节点，也能找到离用户很近的边缘节点。中间的链路完全由 CDN 来规划，选择一个更加可靠的路径，使用类似专线的方式进行访问。

对于常用的 TCP 连接，在公网上传输的时候经常会丢数据，导致 TCP 的窗口始终很小，发送速度上不去。根据前面的 TCP 流量控制和拥塞控制的原理，在 CDN 加速网络中可以调整 TCP 的参数，使得 TCP 可以更加激进地传输数据。

可以通过多个请求复用一个连接，保证每次动态请求到达时。连接都已经建立了，不必临时三次握手或者建立过多的连接，增加服务器的压力。另外，可以通过对传输数据进行压缩，增加传输效率。

所有这些手段就像冷链运输，整个物流优化了，全程冷冻高速运输。不管生鲜是从你旁边的超市送到你家的，还是从产地送的，保证到你家是新鲜的。

小结

好了，这节就到这里了。咱们来总结一下，你记住这两个重点就好。

CDN 和电商系统的分布式仓储系统一样，分为中心节点、区域节点、边缘节点，而数据缓存在离用户最近的位置。

CDN 最擅长的是缓存静态数据，除此之外还可以缓存流媒体数据，这时候要注意使用防盗链。它也支持动态数据的缓存，一种是边缘计算的生鲜超市模式，另一种是链路优化的冷链运输模式。

最后，给你留两个思考题：

这一节讲了 CDN 使用 DNS 进行全局负载均衡的例子，CDN 如何使用 HTTPDNS 呢？

客户端对 DNS、HTTPDNS、CDN 访问了半天，还没进数据中心，你知道数据中心里面什么样吗？

我们的专栏更新到第 20 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第21讲 | 数据中心：我是开发商，自己拿地盖别墅

无论你是看新闻、下订单、看视频、下载文件，最终访问的目的地都在数据中心里面。我们前面学了这么多的网络协议和网络相关的知识，你是不是很好奇，数据中心究竟长啥样呢？

数据中心是一个大杂烩，几乎要用到前面学过的所有知识。

前面讲办公室网络的时候，我们知道办公室里面有很多台电脑。如果要访问外网，需要经过一个叫网关的东西，而网关往往是一个路由器。

数据中心里面也有一大堆的电脑，但是它和咱们办公室里面的笔记本或者台式机不一样。数据中心里面是服务器。服务器被放在一个个叫作机架（Rack）的架子上面。

数据中心的入口和出口也是路由器，由于在数据中心的边界，就像在一个国家的边境，称为边界路由器（Border Router）。为了高可用，边界路由器会有多个。

一般家里只会连接一个运营商的网络，而为了高可用， 为了当一个运营商出问题的时候，还可以通过另外一个运营商来提供服务，所以数据中心的边界路由器会连接多个运营商网络。

既然是路由器，就需要跑路由协议，数据中心往往就是路由协议中的自治区域（AS）。数据中心里面的机器要想访问外面的网站，数据中心里面也是有对外提供服务的机器，都可以通过 BGP 协议，获取内外互通的路由信息。这就是我们常听到的多线 BGP的概念。

如果数据中心非常简单，没几台机器，那就像家里或者宿舍一样，所有的服务器都直接连到路由器上就可以了。但是数据中心里面往往有非常多的机器，当塞满一机架的时候，需要有交换机将这些服务器连接起来，可以互相通信。

这些交换机往往是放在机架顶端的，所以经常称为TOR（Top Of Rack）交换机。这一层的交换机常常称为接入层（Access Layer）。注意这个接入层和原来讲过的应用的接入层不是一个概念。

当一个机架放不下的时候，就需要多个机架，还需要有交换机将多个机架连接在一起。这些交换机对性能的要求更高，带宽也更大。这些交换机称为汇聚层交换机（Aggregation Layer）。

数据中心里面的每一个连接都是需要考虑高可用的。这里首先要考虑的是，如果一台机器只有一个网卡，上面连着一个网线，接入到 TOR 交换机上。如果网卡坏了，或者不小心网线掉了，机器就上不去了。所以，需要至少两个网卡、两个网线插到 TOR 交换机上，但是两个网卡要工作得像一张网卡一样，这就是常说的网卡绑定（bond）。

这就需要服务器和交换机都支持一种协议LACP（Link Aggregation Control Protocol）。它们互相通信，将多个网卡聚合称为一个网卡，多个网线聚合成一个网线，在网线之间可以进行负载均衡，也可以为了高可用作准备。

网卡有了高可用保证，但交换机还有问题。如果一个机架只有一个交换机，它挂了，那整个机架都不能上网了。因而 TOR 交换机也需要高可用，同理接入层和汇聚层的连接也需要高可用性，也不能单线连着。

最传统的方法是，部署两个接入交换机、两个汇聚交换机。服务器和两个接入交换机都连接，接入交换机和两个汇聚都连接，当然这样会形成环，所以需要启用 STP 协议，去除环，但是这样两个汇聚就只能一主一备了。STP 协议里我们学过，只有一条路会起作用。

交换机有一种技术叫作堆叠，所以另一种方法是，将多个交换机形成一个逻辑的交换机，服务器通过多根线分配连到多个接入层交换机上，而接入层交换机多根线分别连接到多个交换机上，并且通过堆叠的私有协议，形成双活的连接方式。

由于对带宽要钱求更大，而且挂了影响也更大，所以两个堆叠可能就不够了，可以就会有更多的，比如四个堆叠为一个逻辑的交换机。

汇聚层将大量的计算节点相互连接在一起，形成一个集群。在这个集群里面，服务器之间通过二层互通，这个区域常称为一个POD（Point Of Delivery），有时候也称为一个可用区（Available Zone）。

当节点数目再多的时候，一个可用区放不下，需要将多个可用区连在一起，连接多个可用区的交换机称为核心交换机。

核心交换机吞吐量更大，高可用要求更高，肯定需要堆叠，但是往往仅仅堆叠，不足以满足吞吐量，因而还是需要部署多组核心交换机。核心和汇聚交换机之间为了高可用，也是全互连模式的。

这个时候还存在那个问题，出现环路怎么办？

一种方式是，不同的可用区在不同的二层网络，需要分配不同的网段。汇聚和核心之间通过三层网络互通的，二层都不在一个广播域里面，不会存在二层环路的问题。三层有环是没有问题的，只要通过路由协议选择最佳的路径就可以了。那为啥二层不能有环路，而三层可以呢？你可以回忆一下二层环路的情况。

如图，核心层和汇聚层之间通过内部的路由协议 OSPF，找到最佳的路径进行访问，而且还可以通过 ECMP 等价路由，在多个路径之间进行负载均衡和高可用。

但是随着数据中心里面的机器越来越多，尤其是有了云计算、大数据，集群规模非常大，而且都要求在一个二层网络里面。这就需要二层互连从汇聚层上升为核心层，也即在核心以下，全部是二层互连，全部在一个广播域里面，这就是常说的大二层。

如果大二层横向流量不大，核心交换机数目不多，可以做堆叠，但是如果横向流量很大，仅仅堆叠满足不了，就需要部署多组核心交换机，而且要和汇聚层进行全互连。由于堆叠只解决一个核心交换机组内的无环问题，而组之间全互连，还需要其他机制进行解决。

如果是 STP，那部署多组核心无法扩大横向流量的能力，因为还是只有一组起作用。

于是大二层就引入了TRILL（Transparent Interconnection of Lots of Link），即多链接透明互联协议。它的基本思想是，二层环有问题，三层环没有问题，那就把三层的路由能力模拟在二层实现。

运行 TRILL 协议的交换机称为RBridge，是具有路由转发特性的网桥设备，只不过这个路由是根据 MAC 地址来的，不是根据 IP 来的。

Rbridage 之间通过链路状态协议运作。记得这个路由协议吗？通过它可以学习整个大二层的拓扑，知道访问哪个 MAC 应该从哪个网桥走；还可以计算最短的路径，也可以通过等价的路由进行负载均衡和高可用性。

TRILL 协议在原来的 MAC 头外面加上自己的头，以及外层的 MAC 头。TRILL 头里面的 Ingress RBridge，有点像 IP 头里面的源 IP 地址，Egress RBridge 是目标 IP 地址，这两个地址是端到端的，在中间路由的时候，不会发生改变。而外层的 MAC，可以有下一跳的 Bridge，就像路由的下一跳，也是通过 MAC 地址来呈现的一样。

如图中所示的过程，有一个包要从主机 A 发送到主机 B，中间要经过 RBridge 1、RBridge 2、RBridge X 等等，直到 RBridge 3。在 RBridge 2 收到的包里面，分内外两层，内层就是传统的主机 A 和主机 B 的 MAC 地址以及内层的 VLAN。

在外层首先加上一个 TRILL 头，里面描述这个包从 RBridge 1 进来的，要从 RBridge 3 出去，并且像三层的 IP 地址一样有跳数。然后再外面，目的 MAC 是 RBridge 2，源 MAC 是 RBridge 1，以及外层的 VLAN。

当 RBridge 2 收到这个包之后，首先看 MAC 是否是自己的 MAC，如果是，要看自己是不是 Egress RBridge，也即是不是最后一跳；如果不是，查看跳数是不是大于 0，然后通过类似路由查找的方式找到下一跳 RBridge X，然后将包发出去。

RBridge 2 发出去的包，内层的信息是不变的，外层的 TRILL 头里面。同样，描述这个包从 RBridge 1 进来的，要从 RBridge 3 出去，但是跳数要减 1。外层的目标 MAC 变成 RBridge X，源 MAC 变成 RBridge 2。

如此一直转发，直到 RBridge 3，将外层解出来，发送内层的包给主机 B。

这个过程是不是和 IP 路由很像？

对于大二层的广播包，也需要通过分发树的技术来实现。我们知道 STP 是将一个有环的图，通过去掉边形成一棵树，而分发树是一个有环的图形成多棵树，不同的树有不同的 VLAN，有的广播包从 VLAN A 广播，有的从 VLAN B 广播，实现负载均衡和高可用。

核心交换机之外，就是边界路由器了。至此从服务器到数据中心边界的层次情况已经清楚了。

在核心交换上面，往往会挂一些安全设备，例如入侵检测、DDoS 防护等等。这是整个数据中心的屏障，防止来自外来的攻击。核心交换机上往往还有负载均衡器，原理前面的章节已经说过了。

在有的数据中心里面，对于存储设备，还会有一个存储网络，用来连接 SAN 和 NAS。但是对于新的云计算来讲，往往不使用传统的 SAN 和 NAS，而使用部署在 x86 机器上的软件定义存储，这样存储也是服务器了，而且可以和计算节点融合在一个机架上，从而更加有效率，也就没有了单独的存储网络了。

于是整个数据中心的网络如下图所示。

这是一个典型的三层网络结构。这里的三层不是指 IP 层，而是指接入层、汇聚层、核心层三层。这种模式非常有利于外部流量请求到内部应用。这个类型的流量，是从外到内或者从内到外，对应到上面那张图里，就是从上到下，从下到上，上北下南，所以称为南北流量。

但是随着云计算和大数据的发展，节点之间的交互越来越多，例如大数据计算经常要在不同的节点将数据拷贝来拷贝去，这样需要经过交换机，使得数据从左到右，从右到左，左西右东，所以称为东西流量。

为了解决东西流量的问题，演进出了叶脊网络（Spine/Leaf）。

叶子交换机（leaf），直接连接物理服务器。L2/L3 网络的分界点在叶子交换机上，叶子交换机之上是三层网络。

脊交换机（spine switch），相当于核心交换机。叶脊之间通过 ECMP 动态选择多条路径。脊交换机现在只是为叶子交换机提供一个弹性的 L3 路由网络。南北流量可以不用直接从脊交换机发出，而是通过与 leaf 交换机并行的交换机，再接到边界路由器出去。

传统的三层网络架构是垂直的结构，而叶脊网络架构是扁平的结构，更易于水平扩展。

小结

好了，复杂的数据中心就讲到这里了。我们来总结一下，你需要记住这三个重点。

数据中心分为三层。服务器连接到接入层，然后是汇聚层，再然后是核心层，最外面是边界路由器和安全设备。

数据中心的所有链路都需要高可用性。服务器需要绑定网卡，交换机需要堆叠，三层设备可以通过等价路由，二层设备可以通过 TRILL 协议。

随着云和大数据的发展，东西流量相对于南北流量越来越重要，因而演化为叶脊网络结构。

最后，给你留两个思考题：

对于数据中心来讲，高可用是非常重要的，每个设备都要考虑高可用，那跨机房的高可用，你知道应该怎么做吗？

前面说的浏览新闻、购物、下载、看视频等行为，都是普通用户通过公网访问数据中心里面的资源。那 IT 管理员应该通过什么样的方式访问数据中心呢？

我们的专栏更新到第 21 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第22讲 | VPN：朝中有人好做官

前面我们讲到了数据中心，里面很复杂，但是有的公司有多个数据中心，需要将多个数据中心连接起来，或者需要办公室和数据中心连接起来。这该怎么办呢？

第一种方式是走公网，但是公网太不安全，你的隐私可能会被别人偷窥。

第二种方式是租用专线的方式把它们连起来，这是土豪的做法，需要花很多钱。

第三种方式是用 VPN 来连接，这种方法比较折中，安全又不贵。

VPN，全名Virtual Private Network，虚拟专用网，就是利用开放的公众网络，建立专用数据传输通道，将远程的分支机构、移动办公人员等连接起来。

VPN 是如何工作的？

VPN 通过隧道技术在公众网络上仿真一条点到点的专线，是通过利用一种协议来传输另外一种协议的技术，这里面涉及三种协议：乘客协议、隧道协议和承载协议。

我们以 IPsec 协议为例来说明。

你知道如何通过自驾进行海南游吗？这其中，你的车怎么通过琼州海峡呢？这里用到轮渡，其实这就用到隧道协议。

在广州这边开车是有“协议”的，例如靠右行驶、红灯停、绿灯行，这个就相当于“被封装”的乘客协议。当然在海南那面，开车也是同样的协议。这就相当于需要连接在一起的一个公司的两个分部。

但是在海上坐船航行，也有它的协议，例如要看灯塔、要按航道航行等。这就是外层的承载协议。

那我的车如何从广州到海南呢？这就需要你遵循开车的协议，将车开上轮渡，所有通过轮渡的车都关在船舱里面，按照既定的规则排列好，这就是隧道协议。

在大海上，你的车是关在船舱里面的，就像在隧道里面一样，这个时候内部的乘客协议，也即驾驶协议没啥用处，只需要船遵从外层的承载协议，到达海南就可以了。

到达之后，外部承载协议的任务就结束了，打开船舱，将车开出来，就相当于取下承载协议和隧道协议的头。接下来，在海南该怎么开车，就怎么开车，还是内部的乘客协议起作用。

在最前面的时候说了，直接使用公网太不安全，所以接下来我们来看一种十分安全的 VPN，IPsec VPN。这是基于 IP 协议的安全隧道协议，为了保证在公网上面信息的安全，因而采取了一定的机制保证安全性。

机制一：私密性，防止信息泄漏给未经授权的个人，通过加密把数据从明文变成无法读懂的密文，从而确保数据的私密性。
前面讲 HTTPS 的时候，说过加密可以分为对称加密和非对称加密。对称加密速度快一些。而 VPN 一旦建立，需要传输大量数据，因而我们采取对称加密。但是同样，对称加密还是存在加密秘钥如何传输的问题，这里需要用到因特网密钥交换（IKE，Internet Key Exchange）协议。

机制二：完整性，数据没有被非法篡改，通过对数据进行 hash 运算，产生类似于指纹的数据摘要，以保证数据的完整性。

机制三：真实性，数据确实是由特定的对端发出，通过身份认证可以保证数据的真实性。

那如何保证对方就是真正的那个人呢？

第一种方法就是预共享密钥，也就是双方事先商量好一个暗号，比如“天王盖地虎，宝塔镇河妖”，对上了，就说明是对的。

另外一种方法就是用数字签名来验证。咋签名呢？当然是使用私钥进行签名，私钥只有我自己有，所以如果对方能用我的数字证书里面的公钥解开，就说明我是我。

基于以上三个特性，组成了IPsec VPN 的协议簇。这个协议簇内容比较丰富。

在这个协议簇里面，有两种协议，这两种协议的区别在于封装网络包的格式不一样。

一种协议称为AH（Authentication Header），只能进行数据摘要 ，不能实现数据加密。

还有一种ESP（Encapsulating Security Payload），能够进行数据加密和数据摘要。

在这个协议簇里面，还有两类算法，分别是加密算法和摘要算法。

这个协议簇还包含两大组件，一个用于 VPN 的双方要进行对称密钥的交换的IKE 组件，另一个是 VPN 的双方要对连接进行维护的SA（Security Association）组件。

IPsec VPN 的建立过程

下面来看 IPsec VPN 的建立过程，这个过程分两个阶段。

第一个阶段，建立 IKE 自己的 SA。这个 SA 用来维护一个通过身份认证和安全保护的通道，为第二个阶段提供服务。在这个阶段，通过 DH（Diffie-Hellman）算法计算出一个对称密钥 K。

DH 算法是一个比较巧妙的算法。客户端和服务端约定两个公开的质数 p 和 q，然后客户端随机产生一个数 a 作为自己的私钥，服务端随机产生一个 b 作为自己的私钥，客户端可以根据 p、q 和 a 计算出公钥 A，服务端根据 p、q 和 b 计算出公钥 B，然后双方交换公钥 A 和 B。

到此客户端和服务端可以根据已有的信息，各自独立算出相同的结果 K，就是对称密钥。但是这个过程，对称密钥从来没有在通道上传输过，只传输了生成密钥的材料，通过这些材料，截获的人是无法算出的。

有了这个对称密钥 K，接下来是第二个阶段，建立 IPsec SA。在这个 SA 里面，双方会生成一个随机的对称密钥 M，由 K 加密传给对方，然后使用 M 进行双方接下来通信的数据。对称密钥 M 是有过期时间的，会过一段时间，重新生成一次，从而防止被破解。

IPsec SA 里面有以下内容：

SPI（Security Parameter Index），用于标识不同的连接；

双方商量好的加密算法、哈希算法和封装模式；

生存周期，超过这个周期，就需要重新生成一个 IPsec SA，重新生成对称密钥。

当 IPsec 建立好，接下来就可以开始打包封装传输了。

左面是原始的 IP 包，在 IP 头里面，会指定上一层的协议为 TCP。ESP 要对 IP 包进行封装，因而 IP 头里面的上一层协议为 ESP。在 ESP 的正文里面，ESP 的头部有双方商讨好的 SPI，以及这次传输的序列号。

接下来全部是加密的内容。可以通过对称密钥进行解密，解密后在正文的最后，指明了里面的协议是什么。如果是 IP，则需要先解析 IP 头，然后解析 TCP 头，这是从隧道出来后解封装的过程。

有了 IPsec VPN 之后，客户端发送的明文的 IP 包，都会被加上 ESP 头和 IP 头，在公网上传输，由于加密，可以保证不被窃取，到了对端后，去掉 ESP 的头，进行解密。

这种点对点的基于 IP 的 VPN，能满足互通的要求，但是速度往往比较慢，这是由底层 IP 协议的特性决定的。IP 不是面向连接的，是尽力而为的协议，每个 IP 包自由选择路径，到每一个路由器，都自己去找下一跳，丢了就丢了，是靠上一层 TCP 的重发来保证可靠性。

因为 IP 网络从设计的时候，就认为是不可靠的，所以即使同一个连接，也可能选择不同的道路，这样的好处是，一条道路崩溃的时候，总有其他的路可以走。当然，带来的代价就是，不断的路由查找，效率比较差。

和 IP 对应的另一种技术称为 ATM。这种协议和 IP 协议的不同在于，它是面向连接的。你可以说 TCP 也是面向连接的啊。这两个不同，ATM 和 IP 是一个层次的，和 TCP 不是一个层次的。

另外，TCP 所谓的面向连接，是不停地重试来保证成功，其实下层的 IP 还是不面向连接的，丢了就丢了。ATM 是传输之前先建立一个连接，形成一个虚拟的通路，一旦连接建立了，所有的包都按照相同的路径走，不会分头行事。

好处是不需要每次都查路由表的，虚拟路径已经建立，打上了标签，后续的包傻傻的跟着走就是了，不用像 IP 包一样，每个包都思考下一步怎么走，都按相同的路径走，这样效率会高很多。

但是一旦虚拟路径上的某个路由器坏了，则这个连接就断了，什么也发不过去了，因为其他的包还会按照原来的路径走，都掉坑里了，它们不会选择其他的路径走。

ATM 技术虽然没有成功，但其屏弃了繁琐的路由查找，改为简单快速的标签交换，将具有全局意义的路由表改为只有本地意义的标签表，这些都可以大大提高一台路由器的转发功力。

有没有一种方式将两者的优点结合起来呢？这就是多协议标签交换（MPLS，Multi-Protocol Label Switching）。MPLS 的格式如图所示，在原始的 IP 头之外，多了 MPLS 的头，里面可以打标签。

在二层头里面，有类型字段，0x0800 表示 IP，0x8847 表示 MPLS Label。

在 MPLS 头里面，首先是标签值占 20 位，接着是 3 位实验位，再接下来是 1 位栈底标志位，表示当前标签是否位于栈底了。这样就允许多个标签被编码到同一个数据包中，形成标签栈。最后是 8 位 TTL 存活时间字段，如果标签数据包的出发 TTL 值为 0，那么该数据包在网络中的生命期被认为已经过期了。

有了标签，还需要设备认这个标签，并且能够根据这个标签转发，这种能够转发标签的路由器称为标签交换路由器（LSR，Label Switching Router）。

这种路由器会有两个表格，一个就是传统的 FIB，也即路由表，另一个就是 LFIB，标签转发表。有了这两个表，既可以进行普通的路由转发，也可以进行基于标签的转发。

有了标签转发表，转发的过程如图所示，就不用每次都进行普通路由的查找了。

这里我们区分 MPLS 区域和非 MPLS 区域。在 MPLS 区域中间，使用标签进行转发，非 MPLS 区域，使用普通路由转发，在边缘节点上，需要有能力将对于普通路由的转发，变成对于标签的转发。

例如图中要访问 114.1.1.1，在边界上查找普通路由，发现马上要进入 MPLS 区域了，进去了对应标签 1，于是在 IP 头外面加一个标签 1，在区域里面，标签 1 要变成标签 3，标签 3 到达出口边缘，将标签去掉，按照路由发出。

这样一个通过标签转换而建立的路径称为 LSP，标签交换路径。在一条 LSP 上，沿数据包传送的方向，相邻的 LSR 分别叫上游 LSR（upstream LSR）和下游 LSR（downstream LSR）。

有了标签，转发是很简单的事，但是如何生成标签，却是 MPLS 中最难修炼的部分。在 MPLS 秘笈中，这部分被称为LDP（Label Distribution Protocol），是一个动态的生成标签的协议。

其实 LDP 与 IP 帮派中的路由协议十分相像，通过 LSR 的交互，互相告知去哪里应该打哪个标签，称为标签分发，往往是从下游开始的。

如果有一个边缘节点发现自己的路由表中出现了新的目的地址，它就要给别人说，我能到达一条新的路径了。

如果此边缘节点存在上游 LSR，并且尚有可供分配的标签，则该节点为新的路径分配标签，并向上游发出标签映射消息，其中包含分配的标签等信息。

收到标签映射消息的 LSR 记录相应的标签映射信息，在其标签转发表中增加相应的条目。此 LSR 为它的上游 LSR 分配标签，并继续向上游 LSR 发送标签映射消息。

当入口 LSR 收到标签映射消息时，在标签转发表中增加相应的条目。这时，就完成了 LSP 的建立。有了标签，转发轻松多了，但是这个和 VPN 什么关系呢？

可以想象，如果我们 VPN 通道里面包的转发，都是通过标签的方式进行，效率就会高很多。所以要想个办法把 MPLS 应用于 VPN。

在 MPLS VPN 中，网络中的路由器分成以下几类：

PE（Provider Edge）：运营商网络与客户网络相连的边缘网络设备；

CE（Customer Edge）：客户网络与 PE 相连接的边缘设备；

P（Provider）：这里特指运营商网络中除 PE 之外的其他运营商网络设备。

为什么要这样分呢？因为我们发现，在运营商网络里面，也即 P Router 之间，使用标签是没有问题的，因为都在运营商的管控之下，对于网段，路由都可以自己控制。但是一旦客户要接入这个网络，就复杂得多。

首先是客户地址重复的问题。客户所使用的大多数都是私网的地址 (192.168.X.X;10.X.X.X;172.X.X.X)，而且很多情况下都会与其它的客户重复。

比如，机构 A 和机构 B 都使用了 192.168.101.0/24 网段的地址，这就发生了地址空间重叠（Overlapping Address Spaces）。

首先困惑的是 BGP 协议，既然 VPN 将两个数据中心连起来，应该看起来像一个数据中心一样，那么如何到达另一端需要通过 BGP 将路由广播过去，传统 BGP 无法正确处理地址空间重叠的 VPN 的路由。

假设机构 A 和机构 B 都使用了 192.168.101.0/24 网段的地址，并各自发布了一条去往此网段的路由，BGP 将只会选择其中一条路由，从而导致去往另一个 VPN 的路由丢失。

所以 PE 路由器之间使用特殊的 MP-BGP 来发布 VPN 路由，在相互沟通的消息中，在一般 32 位 IPv4 的地址之前加上一个客户标示的区分符用于客户地址的区分，这种称为 VPN-IPv4 地址族，这样 PE 路由器会收到如下的消息，机构 A 的 192.168.101.0/24 应该往这面走，机构 B 的 192.168.101.0/24 则应该去另外一个方向。

另外困惑的是路由表，当两个客户的 IP 包到达 PE 的时候，PE 就困惑了，因为网段是重复的。

如何区分哪些路由是属于哪些客户 VPN 内的？如何保证 VPN 业务路由与普通路由不相互干扰？

在 PE 上，可以通过 VRF（VPN Routing&Forwarding Instance）建立每个客户一个路由表，与其它 VPN 客户路由和普通路由相互区分。可以理解为专属于客户的小路由器。

远端 PE 通过 MP-BGP 协议把业务路由放到近端 PE，近端 PE 根据不同的客户选择出相关客户的业务路由放到相应的 VRF 路由表中。

VPN 报文转发采用两层标签方式：

第一层（外层）标签在骨干网内部进行交换，指示从 PE 到对端 PE 的一条 LSP。VPN 报文利用这层标签，可以沿 LSP 到达对端 PE；

第二层（内层）标签在从对端 PE 到达 CE 时使用，在 PE 上，通过查找 VRF 表项，指示报文应被送到哪个 VPN 用户，或者更具体一些，到达哪一个 CE。这样，对端 PE 根据内层标签可以找到转发报文的接口。

我们来举一个例子，看 MPLS VPN 的包发送过程。

机构 A 和机构 B 都发出一个目的地址为 192.168.101.0/24 的 IP 报文，分别由各自的 CE 将报文发送至 PE。

PE 会根据报文到达的接口及目的地址查找 VPN 实例表项 VRF，匹配后将报文转发出去，同时打上内层和外层两个标签。假设通过 MP-BGP 配置的路由，两个报文在骨干网走相同的路径。

MPLS 网络利用报文的外层标签，将报文传送到出口 PE，报文在到达出口 PE 2 前一跳时已经被剥离外层标签，仅含内层标签。

出口 PE 根据内层标签和目的地址查找 VPN 实例表项 VRF，确定报文的出接口，将报文转发至各自的 CE。

CE 根据正常的 IP 转发过程将报文传送到目的地。

小结

好了，这一节就到这里了，我们来总结一下：

VPN 可以将一个机构的多个数据中心通过隧道的方式连接起来，让机构感觉在一个数据中心里面，就像自驾游通过琼州海峡一样；

完全基于软件的 IPsec VPN 可以保证私密性、完整性、真实性、简单便宜，但是性能稍微差一些；

MPLS-VPN 综合和 IP 转发模式和 ATM 的标签转发模式的优势，性能较好，但是需要从运营商购买。

接下来，给你留两个思考题：

当前业务的高可用性和弹性伸缩很重要，所以很多机构都会在自建私有云之外，采购公有云，你知道私有云和公有云应该如何打通吗？

前面所有的上网行为，都是基于电脑的，但是移动互联网越来越成为核心，你知道手机上网都需要哪些协议吗？

我们的专栏更新到第 22 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第23讲 | 移动网络：去巴塞罗那，手机也上不了脸书

前面讲的都是电脑上网的场景，那使用手机上网有什么不同呢？

移动网络的发展历程

你一定知道手机上网有 2G、3G、4G 的说法，究竟这都是什么意思呢？有一个通俗的说法就是：用 2G 看 txt，用 3G 看 jpg，用 4G 看 avi。

2G 网络

手机本来是用来打电话的，不是用来上网的，所以原来在 2G 时代，上网使用的不是 IP 网络，而是电话网络，走模拟信号，专业名称为公共交换电话网（PSTN，Public Switched Telephone Network）。

那手机不连网线，也不连电话线，它是怎么上网的呢？

手机是通过收发无线信号来通信的，专业名称是 Mobile Station，简称 MS，需要嵌入 SIM。手机是客户端，而无线信号的服务端，就是基站子系统（BSS，Base Station SubsystemBSS）。至于什么是基站，你可以回想一下，你在爬山的时候，是不是看到过信号塔？我们平时城市里面的基站比较隐蔽，不容易看到，所以只有在山里才会注意到。正是这个信号塔，通过无线信号，让你的手机可以进行通信。

但是你要知道一点，无论无线通信如何无线，最终还是要连接到有线的网络里。前面讲数据中心的时候我也讲过，电商的应用是放在数据中心的，数据中心的电脑都是插着网线的。

因而，基站子系统分两部分，一部分对外提供无线通信，叫作基站收发信台（BTS，Base Transceiver Station），另一部分对内连接有线网络，叫作基站控制器（BSC，Base Station Controller）。基站收发信台通过无线收到数据后，转发给基站控制器。

这部分属于无线的部分，统称为无线接入网（RAN，Radio Access Network）。

基站控制器通过有线网络，连接到提供手机业务的运营商的数据中心，这部分称为核心网（CN，Core Network）。核心网还没有真的进入互联网，这部分还是主要提供手机业务，是手机业务的有线部分。

首先接待基站来的数据的是移动业务交换中心（MSC，Mobile Service Switching Center），它是进入核心网的入口，但是它不会让你直接连接到互联网上。

因为在让你的手机真正进入互联网之前，提供手机业务的运营商，需要认证是不是合法的手机接入。别你自己造了一张手机卡，就连接上来。鉴权中心（AUC，Authentication Center）和设备识别寄存器（EIR，Equipment Identity Register）主要是负责安全性的。

另外，需要看你是本地的号，还是外地的号，这个牵扯到计费的问题，异地收费还是很贵的。访问位置寄存器（VLR，Visit Location Register）是看你目前在的地方，归属位置寄存器（HLR，Home Location Register）是看你的号码归属地。

当你的手机卡既合法又有钱的时候，才允许你上网，这个时候需要一个网关，连接核心网和真正的互联网。网关移动交换中心（GMSC ，Gateway Mobile Switching Center）就是干这个的，然后是真正的互连网。在 2G 时代，还是电话网络 PSTN。

数据中心里面的这些模块统称为网络子系统（NSS，Network and Switching Subsystem）。

因而 2G 时代的上网如图所示，我们总结一下，有这几个核心点：

手机通过无线信号连接基站；

基站一面朝前接无线，一面朝后接核心网；

核心网一面朝前接到基站请求，一是判断你是否合法，二是判断你是不是本地号，还有没有钱，一面通过网关连接电话网络。

2.5G 网络

后来从 2G 到了 2.5G，也即在原来电路交换的基础上，加入了分组交换业务，支持 Packet 的转发，从而支持 IP 网络。

在上述网络的基础上，基站一面朝前接无线，一面朝后接核心网。在朝后的组件中，多了一个分组控制单元（PCU，Packet Control Unit），用以提供分组交换通道。

在核心网里面，有个朝前的接待员（SGSN，Service GPRS Supported Node）和朝后连接 IP 网络的网关型 GPRS 支持节点（GGSN，Gateway GPRS Supported Node）。

3G 网络

到了 3G 时代，主要是无线通信技术有了改进，大大增加了无线的带宽。

以 W-CDMA 为例，理论最高 2M 的下行速度，因而基站改变了，一面朝外的是 Node B，一面朝内连接核心网的是无线网络控制器（RNC，Radio Network Controller）。核心网以及连接的 IP 网络没有什么变化。

4G 网络

然后就到了今天的 4G 网络，基站为 eNodeB，包含了原来 Node B 和 RNC 的功能，下行速度向百兆级别迈进。另外，核心网实现了控制面和数据面的分离，这个怎么理解呢？

在前面的核心网里面，有接待员 MSC 或者 SGSN，你会发现检查是否合法是它负责，转发数据也是它负责，也即控制面和数据面是合二为一的，这样灵活性比较差，因为控制面主要是指令，多是小包，往往需要高的及时性；数据面主要是流量，多是大包，往往需要吞吐量。

于是有了下面这个架构。

HSS 用于存储用户签约信息的数据库，其实就是你这个号码归属地是哪里的，以及一些认证信息。

MME 是核心控制网元，是控制面的核心，当手机通过 eNodeB 连上的时候，MME 会根据 HSS 的信息，判断你是否合法。如果允许连上来，MME 不负责具体的数据的流量，而是 MME 会选择数据面的 SGW 和 PGW，然后告诉 eNodeB，我允许你连上来了，你连接它们吧。

于是手机直接通过 eNodeB 连接 SGW，连上核心网，SGW 相当于数据面的接待员，并通过 PGW 连到 IP 网络。PGW 就是出口网关。在出口网关，有一个组件 PCRF，称为策略和计费控制单元，用来控制上网策略和流量的计费。

4G 网络协议解析

我们来仔细看一下 4G 网络的协议，真的非常复杂。我们将几个关键组件放大来看。

控制面协议

其中虚线部分是控制面的协议。当一个手机想上网的时候，先要连接 eNodeB，并通过 S1-MME 接口，请求 MME 对这个手机进行认证和鉴权。S1-MME 协议栈如下图所示。

UE 就是你的手机，eNodeB 还是两面派，朝前对接无线网络，朝后对接核心网络，在控制面对接的是 MME。

eNodeB 和 MME 之间的连接就是很正常的 IP 网络，但是这里面在 IP 层之上，却既不是 TCP，也不是 UDP，而是 SCTP。这也是传输层的协议，也是面向连接的，但是更加适合移动网络。 它继承了 TCP 较为完善的拥塞控制并改进 TCP 的一些不足之处。

SCTP 的第一个特点是多宿主。一台机器可以有多个网卡，而对于 TCP 连接来讲，虽然服务端可以监听 0.0.0.0，也就是从哪个网卡来的连接都能接受，但是一旦建立了连接，就建立了四元组，也就选定了某个网卡。

SCTP 引入了联合（association）的概念，将多个接口、多条路径放到一个联合中来。当检测到一条路径失效时，协议就会通过另外一条路径来发送通信数据。应用程序甚至都不必知道发生了故障、恢复，从而提供更高的可用性和可靠性。

SCTP 的第二个特点是将一个联合分成多个流。一个联合中的所有流都是独立的，但均与该联合相关。每个流都给定了一个流编号，它被编码到 SCTP 报文中，通过联合在网络上传送。在 TCP 的机制中，由于强制顺序，导致前一个不到达，后一个就得等待，SCTP 的多个流不会相互阻塞。

SCTP 的第三个特点是四次握手，防止 SYN 攻击。在 TCP 中是三次握手，当服务端收到客户的 SYN 之后，返回一个 SYN-ACK 之前，就建立数据结构，并记录下状态，等待客户端发送 ACK 的 ACK。当恶意客户端使用虚假的源地址来伪造大量 SYN 报文时，服务端需要分配大量的资源，最终耗尽资源，无法处理新的请求。

SCTP 可以通过四次握手引入 Cookie 的概念，来有效地防止这种攻击的产生。在 SCTP 中，客户机使用一个 INIT 报文发起一个连接。服务器使用一个 INIT-ACK 报文进行响应，其中就包括了 Cookie。然后客户端就使用一个 COOKIE-ECHO 报文进行响应，其中包含了服务器所发送的 Cookie。这个时候，服务器为这个连接分配资源，并通过向客户机发送一个 COOKIE-ACK 报文对其进行响应。

SCTP 的第四个特点是将消息分帧。TCP 是面向流的，也即发送的数据没头没尾，没有明显的界限。这对于发送数据没有问题，但是对于发送一个个消息类型的数据，就不太方便。有可能客户端写入 10 个字节，然后再写入 20 个字节。服务端不是读出 10 个字节的一个消息，再读出 20 个字节的一个消息，而有可能读入 25 个字节，再读入 5 个字节，需要业务层去组合成消息。

SCTP 借鉴了 UDP 的机制，在数据传输中提供了消息分帧功能。当一端对一个套接字执行写操作时，可确保对等端读出的数据大小与此相同。

SCTP 的第五个特点是断开连接是三次挥手。在 TCP 里面，断开连接是四次挥手，允许另一端处于半关闭的状态。SCTP 选择放弃这种状态，当一端关闭自己的套接字时，对等的两端全部需要关闭，将来任何一端都不允许再进行数据的移动了。

当 MME 通过认证鉴权，同意这个手机上网的时候，需要建立一个数据面的数据通路。建立通路的过程还是控制面的事情，因而使用的是控制面的协议 GTP-C。

建设的数据通路分两段路，其实是两个隧道。一段是从 eNodeB 到 SGW，这个数据通路由 MME 通过 S1-MME 协议告诉 eNodeB，它是隧道的一端，通过 S11 告诉 SGW，它是隧道的另一端。第二端是从 SGW 到 PGW，SGW 通过 S11 协议知道自己是其中一端，并主动通过 S5 协议，告诉 PGW 它是隧道的另一端。

GTP-C 协议是基于 UDP 的，这是UDP 的“城会玩”中的一个例子。如果看 GTP 头，我们可以看到，这里面有隧道的 ID，还有序列号。

通过序列号，不用 TCP，GTP-C 自己就可以实现可靠性，为每个输出信令消息分配一个依次递增的序列号，以确保信令消息的按序传递，并便于检测重复包。对于每个输出信令消息启动定时器，在定时器超时前未接收到响应消息则进行重发。

数据面协议

当两个隧道都打通，接在一起的时候，PGW 会给手机分配一个 IP 地址，这个 IP 地址是隧道内部的 IP 地址，可以类比为 IPsec 协议里面的 IP 地址。这个 IP 地址是归手机运营商管理的。然后，手机可以使用这个 IP 地址，连接 eNodeB，从 eNodeB 经过 S1-U 协议，通过第一段隧道到达 SGW，再从 SGW 经过 S8 协议，通过第二段隧道到达 PGW，然后通过 PGW 连接到互联网。

数据面的协议都是通过 GTP-U，如图所示。

手机每发出的一个包，都由 GTP-U 隧道协议封装起来，格式如下。

和 IPsec 协议很类似，分为乘客协议、隧道协议、承载协议。其中乘客协议是手机发出来的包，IP 是手机的 IP，隧道协议里面有隧道 ID，不同的手机上线会建立不同的隧道，因而需要隧道 ID 来标识。承载协议的 IP 地址是 SGW 和 PGW 的 IP 地址。

手机上网流程

接下来，我们来看一个手机开机之后上网的流程，这个过程称为Attach。可以看出来，移动网络还是很复杂的。因为这个过程要建立很多的隧道，分配很多的隧道 ID，所以我画了一个图来详细说明这个过程。

手机开机以后，在附近寻找基站 eNodeB，找到后给 eNodeB 发送 Attach Request，说“我来啦，我要上网”。

eNodeB 将请求发给 MME，说“有个手机要上网”。

MME 去请求手机，一是认证，二是鉴权，还会请求 HSS 看看有没有钱，看看是在哪里上网。

当 MME 通过了手机的认证之后，开始分配隧道，先告诉 SGW，说要创建一个会话（Create Session）。在这里面，会给 SGW 分配一个隧道 ID t1，并且请求 SGW 给自己也分配一个隧道 ID。

SGW 转头向 PGW 请求建立一个会话，为 PGW 的控制面分配一个隧道 ID t2，也给 PGW 的数据面分配一个隧道 ID t3，并且请求 PGW 给自己的控制面和数据面分配隧道 ID。

PGW 回复 SGW 说“创建会话成功”，使用自己的控制面隧道 ID t2，回复里面携带着给 SGW 控制面分配的隧道 ID t4 和控制面的隧道 ID t5，至此 SGW 和 PGW 直接的隧道建设完成。双方请求对方，都要带着对方给自己分配的隧道 ID，从而标志是这个手机的请求。

接下来 SGW 回复 MME 说“创建会话成功”，使用自己的隧道 ID t1 访问 MME，回复里面有给 MME 分配隧道 ID t6，也有 SGW 给 eNodeB 分配的隧道 ID t7。

当 MME 发现后面的隧道都建设成功之后，就告诉 eNodeB，“后面的隧道已经建设完毕，SGW 给你分配的隧道 ID 是 t7，你可以开始连上来了，但是你也要给 SGW 分配一个隧道 ID”。

eNodeB 告诉 MME 自己给 SGW 分配一个隧道，ID 为 t8。

MME 将 eNodeB 给 SGW 分配的隧道 ID t8 告知 SGW，从而前面的隧道也建设完毕。

这样，手机就可以通过建立的隧道成功上网了。

异地上网问题

接下来我们考虑异地上网的事情。

为什么要分 SGW 和 PGW 呢，一个 GW 不可以吗？SGW 是你本地的运营商的设备，而 PGW 是你所属的运营商的设备。

如果你在巴塞罗那，一下飞机，手机开机，周围搜寻到的肯定是巴塞罗那的 eNodeB。通过 MME 去查寻国内运营商的 HSS，看你是否合法，是否还有钱。如果允许上网，你的手机和巴塞罗那的 SGW 会建立一个隧道，然后巴塞罗那的 SGW 和国内运营商的 PGW 建立一个隧道，然后通过国内运营商的 PGW 上网。

这样判断你是否能上网的在国内运营商的 HSS，控制你上网策略的是国内运营商的 PCRF，给手机分配的 IP 地址也是国内运营商的 PGW 负责的，给手机分配的 IP 地址也是国内运营商里统计的。运营商由于是在 PGW 里面统计的，这样你的上网流量全部通过国内运营商即可，只不过巴塞罗那运营商也要和国内运营商进行流量结算。

由于你的上网策略是由国内运营商在 PCRF 中控制的，因而你还是上不了脸书。

小结

好了，这一节就到这里了，我们来总结一下：

移动网络的发展历程从 2G 到 3G，再到 4G，逐渐从打电话的功能为主，向上网的功能为主转变；

请记住 4G 网络的结构，有 eNodeB、MME、SGW、PGW 等，分控制面协议和数据面协议，你可以对照着结构，试着说出手机上网的流程；

即便你在国外的运营商下上网，也是要通过国内运营商控制的，因而也上不了脸书。

最后，给你留两个思考题：

咱们上网都有套餐，有交钱多的，有交钱少的，你知道移动网络是如何控制不同优先级的用户的上网流量的吗？

前面讲过的所有的网络都是基于物理机的，随着云计算兴起，无论是电商，还是移动网络都要部署在云中了，你知道云中网络的设计有哪些要点吗？

我们的专栏更新到第 23 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第24讲 | 云中网络：自己拿地成本高，购买公寓更灵活

前面我们讲了，数据中心里面堆着一大片一大片的机器，用网络连接起来，机器数目一旦非常多，人们就发现，维护这么一大片机器还挺麻烦的，有好多不灵活的地方。

采购不灵活：如果客户需要一台电脑，那就需要自己采购、上架、插网线、安装操作系统，周期非常长。一旦采购了，一用就 N 年，不能退货，哪怕业务不做了，机器还在数据中心里留着。

运维不灵活：一旦需要扩容 CPU、内存、硬盘，都需要去机房手动弄，非常麻烦。

规格不灵活：采购的机器往往动不动几百 G 的内存，而每个应用往往可能只需要 4 核 8G，所以很多应用混合部署在上面，端口各种冲突，容易相互影响。

复用不灵活：一台机器，一旦一个用户不用了，给另外一个用户，那就需要重装操作系统。因为原来的操作系统可能遗留很多数据，非常麻烦。

从物理机到虚拟机

为了解决这些问题，人们发明了一种叫虚拟机的东西，并基于它产生了云计算技术。

其实在你的个人电脑上，就可以使用虚拟机。如果你对虚拟机没有什么概念，你可以下载一个桌面虚拟化的软件，自己动手尝试一下。它可以让你灵活地指定 CPU 的数目、内存的大小、硬盘的大小，可以有多个网卡，然后在一台笔记本电脑里面创建一台或者多台虚拟电脑。不用的时候，一点删除就没有了。

在数据中心里面，也有一种类似的开源技术 qemu-kvm，能让你在一台巨大的物理机里面，掏出一台台小的机器。这套软件就能解决上面的问题：一点就能创建，一点就能销毁。你想要多大就有多大，每次创建的系统还都是新的。

我们常把物理机比喻为自己拿地盖房子，而虚拟机则相当于购买公寓，更加灵活方面，随时可买可卖。 那这个软件为什么能做到这些事儿呢？

它用的是软件模拟硬件的方式。刚才说了，数据中心里面用的 qemu-kvm。从名字上来讲，emu 就是 Emulator（模拟器）的意思，主要会模拟 CPU、内存、网络、硬盘，使得虚拟机感觉自己在使用独立的设备，但是真正使用的时候，当然还是使用物理的设备。

例如，多个虚拟机轮流使用物理 CPU，内存也是使用虚拟内存映射的方式，最终映射到物理内存上。硬盘在一块大的文件系统上创建一个 N 个 G 的文件，作为虚拟机的硬盘。

简单比喻，虚拟化软件就像一个“骗子”，向上“骗”虚拟机里面的应用，让它们感觉独享资源，其实自己啥都没有，全部向下从物理机里面弄。

虚拟网卡的原理

那网络是如何“骗”应用的呢？如何将虚拟机的网络和物理机的网络连接起来？

首先，虚拟机要有一张网卡。对于 qemu-kvm 来说，这是通过 Linux 上的一种 TUN/TAP 技术来实现的。

虚拟机是物理机上跑着的一个软件。这个软件可以像其他应用打开文件一样，打开一个称为 TUN/TAP 的 Char Dev（字符设备文件）。打开了这个字符设备文件之后，在物理机上就能看到一张虚拟 TAP 网卡。

虚拟化软件作为“骗子”，会将打开的这个文件，在虚拟机里面虚拟出一张网卡，让虚拟机里面的应用觉得它们真有一张网卡。于是，所有的网络包都往这里发。

当然，网络包会到虚拟化软件这里。它会将网络包转换成为文件流，写入字符设备，就像写一个文件一样。内核中 TUN/TAP 字符设备驱动会收到这个写入的文件流，交给 TUN/TAP 的虚拟网卡驱动。这个驱动将文件流再次转成网络包，交给 TCP/IP 协议栈，最终从虚拟 TAP 网卡发出来，成为标准的网络包。

就这样，几经转手，数据终于从虚拟机里面，发到了虚拟机外面。

虚拟网卡连接到云中

我们就这样有了虚拟 TAP 网卡。接下来就要看，这个卡怎么接入庞大的数据中心网络中。

在接入之前，我们先来看，云计算中的网络都需要注意哪些点。

共享：尽管每个虚拟机都会有一个或者多个虚拟网卡，但是物理机上可能只有有限的网卡。那这么多虚拟网卡如何共享同一个出口？

隔离：分两个方面，一个是安全隔离，两个虚拟机可能属于两个用户，那怎么保证一个用户的数据不被另一个用户窃听？一个是流量隔离，两个虚拟机，如果有一个疯狂下片，会不会导致另外一个上不了网？

互通：分两个方面，一个是如果同一台机器上的两个虚拟机，属于同一个用户的话，这两个如何相互通信？另一个是如果不同物理机上的两个虚拟机，属于同一个用户的话，这两个如何相互通信？

灵活：虚拟机和物理不同，会经常创建、删除，从一个机器漂移到另一台机器，有的互通、有的不通等等，灵活性比物理网络要好得多，需要能够灵活配置。

共享与互通问题

这些问题，我们一个个来解决。

首先，一台物理机上有多个虚拟机，有多个虚拟网卡，这些虚拟网卡如何连在一起，进行相互访问，并且可以访问外网呢？

还记得我们在大学宿舍里做的事情吗？你可以想象你的物理机就是你们宿舍，虚拟机就是你的个人电脑，这些电脑应该怎么连接起来呢？当然应该买一个交换机。

在物理机上，应该有一个虚拟的交换机，在 Linux 上有一个命令叫作 brctl，可以创建虚拟的网桥 brctl addbr br0。创建出来以后，将两个虚拟机的虚拟网卡，都连接到虚拟网桥 brctl addif br0 tap0 上，这样将两个虚拟机配置相同的子网网段，两台虚拟机就能够相互通信了。

那这些虚拟机如何连外网呢？在桌面虚拟化软件上面，我们能看到以下选项。

这里面，host-only 的网络对应的，其实就是上面两个虚拟机连到一个 br0 虚拟网桥上，而且不考虑访问外部的场景，只要虚拟机之间能够相互访问就可以了。

如果要访问外部，往往有两种方式。

一种方式称为桥接。如果在桌面虚拟化软件上选择桥接网络，则在你的笔记本电脑上，就会形成下面的结构。

每个虚拟机都会有虚拟网卡，在你的笔记本电脑上，会发现多了几个网卡，其实是虚拟交换机。这个虚拟交换机将虚拟机连接在一起。在桥接模式下，物理网卡也连接到这个虚拟交换机上，物理网卡在桌面虚拟化软件上，在“界面名称”那里选定。

如果使用桥接网络，当你登录虚拟机里看 IP 地址的时候会发现，你的虚拟机的地址和你的笔记本电脑的，以及你旁边的同事的电脑的网段是一个网段。这是为什么呢？这其实相当于将物理机和虚拟机放在同一个网桥上，相当于这个网桥上有三台机器，是一个网段的，全部打平了。我将图画成下面的样子你就好理解了。

在数据中心里面，采取的也是类似的技术，只不过都是 Linux，在每台机器上都创建网桥 br0，虚拟机的网卡都连到 br0 上，物理网卡也连到 br0 上，所有的 br0 都通过物理网卡出来连接到物理交换机上。

同样我们换一个角度看待这个拓扑图。同样是将网络打平，虚拟机会和你的物理网络具有相同的网段。

在这种方式下，不但解决了同一台机器的互通问题，也解决了跨物理机的互通问题，因为都在一个二层网络里面，彼此用相同的网段访问就可以了。但是当规模很大的时候，会存在问题。

你还记得吗？在一个二层网络里面，最大的问题是广播。一个数据中心的物理机已经很多了，广播已经非常严重，需要通过 VLAN 进行划分。如果使用了虚拟机，假设一台物理机里面创建 10 台虚拟机，全部在一个二层网络里面，那广播就会很严重，所以除非是你的桌面虚拟机或者数据中心规模非常小，才可以使用这种相对简单的方式。

另外一种方式称为NAT。如果在桌面虚拟化软件中使用 NAT 模式，在你的笔记本电脑上会出现如下的网络结构。

在这种方式下，你登录到虚拟机里面查看 IP 地址，会发现虚拟机的网络是虚拟机的，物理机的网络是物理机的，两个不相同。虚拟机要想访问物理机的时候，需要将地址 NAT 成为物理机的地址。

除此之外，它还会在你的笔记本电脑里内置一个 DHCP 服务器，为笔记本电脑上的虚拟机动态分配 IP 地址。因为虚拟机的网络自成体系，需要进行 IP 管理。为什么桥接方式不需要呢？因为桥接将网络打平了，虚拟机的 IP 地址应该由物理网络的 DHCP 服务器分配。

在数据中心里面，也是使用类似的方式。这种方式更像是真的将你宿舍里面的情况，搬到一台物理机上来。

虚拟机是你的电脑，路由器和 DHCP Server 相当于家用路由器或者寝室长的电脑，物理网卡相当于你们宿舍的外网网口，用于访问互联网。所有电脑都通过内网网口连接到一个网桥 br0 上，虚拟机要想访问互联网，需要通过 br0 连到路由器上，然后通过路由器将请求 NAT 成为物理网络的地址，转发到物理网络。

如果是你自己登录到物理机上做个简单配置，你可以简化一下。例如将虚拟机所在网络的网关的地址直接配置到 br0 上，不用 DHCP Server，手动配置每台虚拟机的 IP 地址，通过命令 iptables -t nat -A POSTROUTING -o ethX -j MASQUERADE，直接在物理网卡 ethX 上进行 NAT，所有从这个网卡出去的包都 NAT 成这个网卡的地址。通过设置 net.ipv4.ip_forward = 1，开启物理机的转发功能，直接做路由器，而不用单独的路由器，这样虚拟机就能直接上网了。

隔离问题

解决了互通的问题，接下来就是隔离的问题。

如果一台机器上的两个虚拟机不属于同一个用户，怎么办呢？好在 brctl 创建的网桥也是支持 VLAN 功能的，可以设置两个虚拟机的 tag，这样在这个虚拟网桥上，两个虚拟机是不互通的。

但是如何跨物理机互通，并且实现 VLAN 的隔离呢？由于 brctl 创建的网桥上面的 tag 是没办法在网桥之外的范围内起作用的，于是我们需要寻找其他的方式。

有一个命令vconfig，可以基于物理网卡 eth0 创建带 VLAN 的虚拟网卡，所有从这个虚拟网卡出去的包，都带这个 VLAN，如果这样，跨物理机的互通和隔离就可以通过这个网卡来实现。

首先为每个用户分配不同的 VLAN，例如有一个用户 VLAN 10，一个用户 VLAN 20。在一台物理机上，基于物理网卡，为每个用户用 vconfig 创建一个带 VLAN 的网卡。不同的用户使用不同的虚拟网桥，带 VLAN 的虚拟网卡也连接到虚拟网桥上。

这样是否能保证两个用户的隔离性呢？不同的用户由于网桥不通，不能相互通信，一旦出了网桥，由于 VLAN 不同，也不会将包转发到另一个网桥上。另外，出了物理机，也是带着 VLAN ID 的。只要物理交换机也是支持 VLAN 的，到达另一台物理机的时候，VLAN ID 依然在，它只会将包转发给相同 VLAN 的网卡和网桥，所以跨物理机，不同的 VLAN 也不会相互通信。

使用 brctl 创建出来的网桥功能是简单的，基于 VLAN 的虚拟网卡也能实现简单的隔离。但是这都不是大规模云平台能够满足的，一个是 VLAN 的隔离，数目太少。前面我们学过，VLAN ID 只有 4096 个，明显不够用。另外一点是这个配置不够灵活。谁和谁通，谁和谁不通，流量的隔离也没有实现，还有大量改进的空间。

小结

好了，这一节就到这里了，我们来总结一下：

云计算的关键技术是虚拟化，这里我们重点关注的是，虚拟网卡通过打开 TUN/TAP 字符设备的方式，将虚拟机内外连接起来；

云中的网络重点关注四个方面，共享、隔离、互通、灵活。其中共享和互通有两种常用的方式，分别是桥接和 NAT，隔离可以通过 VLAN 的方式。

接下来，给你留两个思考题。

为了直观，这一节的内容我们以桌面虚拟化系统举例。在数据中心里面，有一款著名的开源软件 OpenStack，这一节讲的网络连通方式对应 OpenStack 中的哪些模型呢？

这一节的最后，我们也提到了，本节提到的网络配置方式比较不灵活，你知道什么更加灵活的方式吗？

我们的专栏更新到第 24 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第25讲 | 软件定义网络：共享基础设施的小区物业管理办法

上一节我们说到，使用原生的 VLAN 和 Linux 网桥的方式来进行云平台的管理，但是这样在灵活性、隔离性方面都显得不足，而且整个网络缺少统一的视图、统一的管理。

可以这样比喻，云计算就像大家一起住公寓，要共享小区里面的基础设施，其中网络就相当于小区里面的电梯、楼道、路、大门等，大家都走，往往会常出现问题，尤其在上班高峰期，出门的人太多，对小区的物业管理就带来了挑战。

物业可以派自己的物业管理人员，到每个单元的楼梯那里，将电梯的上下行速度调快一点，可以派人将隔离健身区、景色区的栅栏门暂时打开，让大家可以横穿小区，直接上地铁，还可以派人将多个小区出入口，改成出口多、入口少等等。等过了十点半，上班高峰过去，再派人都改回来。

软件定义网络（SDN）

这种模式就像传统的网络设备和普通的 Linux 网桥的模式，配置整个云平台的网络通路，你需要登录到这台机器上配置这个，再登录到另外一个设备配置那个，才能成功。

如果物业管理人员有一套智能的控制系统，在物业监控室里就能看到小区里每个单元、每个电梯的人流情况，然后在监控室里面，只要通过远程控制的方式，拨弄一个手柄，电梯的速度就调整了，栅栏门就打开了，某个入口就改出口了。

这就是软件定义网络（SDN）。它主要有以下三个特点。

控制与转发分离：转发平面就是一个个虚拟或者物理的网络设备，就像小区里面的一条条路。控制平面就是统一的控制中心，就像小区物业的监控室。它们原来是一起的，物业管理员要从监控室出来，到路上去管理设备，现在是分离的，路就是走人的，控制都在监控室。

控制平面与转发平面之间的开放接口：控制器向上提供接口，被应用层调用，就像总控室提供按钮，让物业管理员使用。控制器向下调用接口，来控制网络设备，就像总控室会远程控制电梯的速度。这里经常使用两个名词，前面这个接口称为北向接口，后面这个接口称为南向接口，上北下南嘛。

逻辑上的集中控制：逻辑上集中的控制平面可以控制多个转发面设备，也就是控制整个物理网络，因而可以获得全局的网络状态视图，并根据该全局网络状态视图实现对网络的优化控制，就像物业管理员在监控室能够看到整个小区的情况，并根据情况优化出入方案。

OpenFlow 和 OpenvSwitch

SDN 有很多种实现方式，我们来看一种开源的实现方式。

OpenFlow 是 SDN 控制器和网络设备之间互通的南向接口协议，OpenvSwitch 用于创建软件的虚拟交换机。OpenvSwitch 是支持 OpenFlow 协议的，当然也有一些硬件交换机也支持 OpenFlow 协议。它们都可以被统一的 SDN 控制器管理，从而实现物理机和虚拟机的网络连通。

SDN 控制器是如何通过 OpenFlow 协议控制网络的呢？

在 OpenvSwitch 里面，有一个流表规则，任何通过这个交换机的包，都会经过这些规则进行处理，从而接收、转发、放弃。

那流表长啥样呢？其实就是一个个表格，每个表格好多行，每行都是一条规则。每条规则都有优先级，先看高优先级的规则，再看低优先级的规则。

对于每一条规则，要看是否满足匹配条件。这些条件包括，从哪个端口进来的，网络包头里面有什么等等。满足了条件的网络包，就要执行一个动作，对这个网络包进行处理。可以修改包头里的内容，可以跳到任何一个表格，可以转发到某个网口出去，也可以丢弃。

通过这些表格，可以对收到的网络包随意处理。

具体都能做什么处理呢？通过上面的表格可以看出，简直是想怎么处理怎么处理，可以覆盖 TCP/IP 协议栈的四层。

对于物理层：

匹配规则包括由从哪个口进来；

执行动作包括从哪个口出去。

对于 MAC 层：

匹配规则包括：源 MAC 地址是多少？（dl_src），目标 MAC 是多少？（dl_dst），所属 vlan 是多少？（dl_vlan）；

执行动作包括：修改源 MAC（mod_dl_src），修改目标 MAC（mod_dl_dst），修改 VLAN（mod_vlan_vid），删除 VLAN（strip_vlan），MAC 地址学习（learn）。

对于网络层：

匹配规则包括：源 IP 地址是多少？(nw_src)，目标 IP 是多少？（nw_dst）。

执行动作包括：修改源 IP 地址（mod_nw_src），修改目标 IP 地址（mod_nw_dst）。

对于传输层：

匹配规则包括：源端口是多少？（tp_src），目标端口是多少？（tp_dst）。

执行动作包括：修改源端口（mod_tp_src），修改目标端口（mod_tp_dst）。

总而言之，对于 OpenvSwitch 来讲，网络包到了我手里，就是一个 Buffer，我想怎么改怎么改，想发到哪个端口就发送到哪个端口。

OpenvSwitch 有本地的命令行可以进行配置，能够实验咱们前面讲过的一些功能。我们可以通过 OpenvSwitch 的命令创建一个虚拟交换机。然后可以将多个虚拟端口 port 添加到这个虚拟交换机上。

ovs-vsctl add-br ubuntu_br

实验一：用 OpenvSwitch 实现 VLAN 的功能

下面我们实验一下通过 OpenvSwitch 实现 VLAN 的功能，在 OpenvSwitch 中端口 port 分两种。

第一类是 access port：

这个端口配置 tag，从这个端口进来的包会被打上这个 tag；

如果网络包本身带有的 VLAN ID 等于 tag，则会从这个 port 发出；

从 access port 发出的包不带 VLAN ID。

第二类是 trunk port：

这个 port 不配置 tag，配置 trunks；

如果 trunks 为空，则所有的 VLAN 都 trunk，也就意味着对于所有 VLAN 的包，本身带什么 VLAN ID，就是携带者什么 VLAN ID，如果没有设置 VLAN，就属于 VLAN 0，全部允许通过；

如果 trunks 不为空，则仅仅带着这些 VLAN ID 的包通过。

我们通过以下命令创建如下的环境：

ovs-vsctl add-port ubuntu_br first_br

ovs-vsctl add-port ubuntu_br second_br

ovs-vsctl add-port ubuntu_br third_br

ovs-vsctl set Port vnet0 tag=101

ovs-vsctl set Port vnet1 tag=102

ovs-vsctl set Port vnet2 tag=103

ovs-vsctl set Port first_br tag=103

ovs-vsctl clear Port second_br tag

ovs-vsctl set Port third_br trunks=101,102

另外要配置禁止 MAC 地址学习。

ovs-vsctl set bridge ubuntu_br flood-vlans=101,102,103

创建好了环境以后，我们来做这个实验。

从 192.168.100.102 来 ping 192.168.100.103，然后用 tcpdump 进行抓包。first_if 收到包了，从 first_br 出来的包头是没有 VLAN ID 的。second_if 也收到包了，由于 second_br 是 trunk port，因而出来的包头是有 VLAN ID 的，third_if 收不到包。

从 192.168.100.100 来 ping 192.168.100.105, 则 second_if 和 third_if 可以收到包，当然 ping 不通，因为 third_if 不属于某个 VLAN。first_if 是收不到包的。second_if 能够收到包，而且包头里面是 VLAN ID=101。third_if 也能收到包，而且包头里面是 VLAN ID=101。

从 192.168.100.101 来 ping 192.168.100.104， 则 second_if 和 third_if 可以收到包。first_if 是收不到包的。second_br 能够收到包，而且包头里面是 VLAN ID=102。third_if 也能收到包，而且包头里面是 VLAN ID=102。

通过这个例子，我们可以看到，通过 OpenvSwitch，不用买一个支持 VLAN 的交换机，你也能学习 VLAN 的工作模式了。

实验二：用 OpenvSwitch 模拟网卡绑定，连接交换机

接下来，我们来做另一个实验。在前面，我们还说过，为了高可用，可以使用网卡绑定，连接到交换机，OpenvSwitch 也可以模拟这一点。

在 OpenvSwitch 里面，有个 bond_mode，可以设置为以下三个值：

active-backup：一个连接是 active，其他的是 backup，当 active 失效的时候，backup 顶上；

balance-slb：流量安装源 MAC 和 output VLAN 进行负载均衡；

balance-tcp：必须在支持 LACP 协议的情况下才可以，可根据 L2, L3, L4 进行负载均衡。

我们搭建一个测试环境。

我们使用下面的命令，建立 bond 连接。

ovs-vsctl add-bond br0 bond0 first_br second_br

ovs-vsctl add-bond br1 bond1 first_if second_if

ovs-vsctl set Port bond0 lacp=active

ovs-vsctl set Port bond1 lacp=active

默认情况下 bond_mode 是 active-backup 模式，一开始 active 的是 first_br 和 first_if。

这个时候我们从 192.168.100.100 ping 192.168.100.102，以及从 192.168.100.101 ping 192.168.100.103 的时候，tcpdump 可以看到所有的包都是从 first_if 通过。

如果把 first_if 设成 down，则包的走向会变，发现 second_if 开始有流量，对于 192.168.100.100 和 192.168.100.101 似乎没有收到影响。

如果我们通过以下命令，把 bond_mode 设为 balance-slb。然后我们同时在 192.168.100.100 ping 192.168.100.102，在 192.168.100.101 ping 192.168.100.103，我们通过 tcpdump 发现包已经被分流了。

ovs-vsctl set Port bond0 bond_mode=balance-slb

ovs-vsctl set Port bond1 bond_mode=balance-slb

通过这个例子，我们可以看到，通过 OpenvSwitch，你不用买两台支持 bond 的交换机，也能看到 bond 的效果。

那 OpenvSwitch 是怎么做到这些的呢？我们来看 OpenvSwitch 的架构图。

OpenvSwitch 包含很多的模块，在用户态有两个重要的进程，也有两个重要的命令行工具。

第一个进程是 OVSDB 进程。ovs-vsctl 命令行会和这个进程通信，去创建虚拟交换机，创建端口，将端口添加到虚拟交换机上，OVSDB 会将这些拓扑信息保存在一个本地的文件中。

第一个进程是 vswitchd 进程。ovs-ofctl 命令行会和这个进程通信，去下发流表规则，规则里面会规定如何对网络包进行处理，vswitchd 会将流表放在用户态 Flow Table 中。

在内核态，OpenvSwitch 有内核模块 OpenvSwitch.ko，对应图中的 Datapath 部分。在网卡上注册一个函数，每当有网络包到达网卡的时候，这个函数就会被调用。

在内核的这个函数里面，会拿到网络包，将各个层次的重要信息拿出来，例如：

在物理层，in_port 即包进入的网口的 ID；

在 MAC 层，源和目的 MAC 地址；

在 IP 层，源和目的 IP 地址；

在传输层，源和目的端口号。

在内核中，有一个内核态 Flow Table。接下来内核模块在这个内核流表中匹配规则，如果匹配上了，则执行操作、修改包，或者转发或者放弃。如果内核没有匹配上，则需要进入用户态，用户态和内核态之间通过 Linux 的一个机制 Netlink 相互通信。

内核通过 upcall，告知用户态进程 vswitchd 在用户态 Flow Table 里面去匹配规则，这里面的规则是全量的流表规则，而内核 Flow Table 里面的只是为了快速处理，保留了部分规则，内核里面的规则过一阵就会过期。

当在用户态匹配到了流表规则之后，就在用户态执行操作，同时将这个匹配成功的流表通过 reinject 下发到内核，从而接下来的包都能在内核找到这个规则。

这里调用 openflow 协议的，是本地的命令行工具，也可以是远程的 SDN 控制器，一个重要的 SDN 控制器是 OpenDaylight。

下面这个图就是 OpenDaylight 中看到的拓扑图。是不是有种物业管理员在监控室里的感觉？

我们可以通过在 OpenDaylight 里，将两个交换机之间配置通，也可以配置不通，还可以配置一个虚拟 IP 地址 VIP，在不同的机器之间实现负载均衡等等，所有的策略都可以灵活配置。

如何在云计算中使用 OpenvSwitch？

OpenvSwitch 这么牛，如何用在云计算中呢？

我们还是讨论 VLAN 的场景。

在没有 OpenvSwitch 的时候，如果一个新的用户要使用一个新的 VLAN，还需要创建一个属于新的 VLAN 的虚拟网卡，并且为这个租户创建一个单独的虚拟网桥，这样用户越来越多的时候，虚拟网卡和虚拟网桥会越来越多，管理非常复杂。

另一个问题是虚拟机的 VLAN 和物理环境的 VLAN 是透传的，也即从一开始规划的时候，就需要匹配起来，将物理环境和虚拟环境强绑定，本来就不灵活。

而引入了 OpenvSwitch，状态就得到了改观。

首先，由于 OpenvSwitch 本身就是支持 VLAN 的，所有的虚拟机都可以放在一个网桥 br0 上，通过不同的用户配置不同的 tag，就能够实现隔离。例如上面的图，用户 A 的虚拟机都在 br0 上，用户 B 的虚拟机都在 br1 上，有了 OpenvSwitch，就可以都放在 br0 上，只是设置了不同的 tag。

另外，还可以创建一个虚拟交换机 br1，将物理网络和虚拟网络进行隔离。物理网络有物理网络的 VLAN 规划，虚拟机在一台物理机上，所有的 VLAN 都是从 1 开始的。由于一台机器上的虚拟机不会超过 4096 个，所以 VLAN 在一台物理机上如果从 1 开始，肯定够用了。

例如在图中，上面的物理机里面，用户 A 被分配的 tag 是 1，用户 B 被分配的 tag 是 2，而在下面的物理机里面，用户 A 被分配的 tag 是 7，用户 B 被分配的 tag 是 6。

如果物理机之间的通信和隔离还是通过 VLAN 的话，需要将虚拟机的 VLAN 和物理环境的 VLAN 对应起来，但为了灵活性，不一定一致，这样可以实现分别管理物理机的网络和虚拟机的网络。好在 OpenvSwitch 可以对包的内容进行修改。例如通过匹配 dl_vlan，然后执行 mod_vlan_vid 来改进进出出物理机的网络包。

尽管租户多了，物理环境的 VLAN 还是不够用，但是有了 OpenvSwitch 的映射，将物理和虚拟解耦，从而可以让物理环境使用其他技术，而不影响虚拟机环境，这个我们后面再讲。

小结

好了，这一节就到这里了，我们来总结一下：

用 SDN 控制整个云里面的网络，就像小区保安从总控室管理整个物业是一样的，将控制面和数据面进行了分离；

一种开源的虚拟交换机的实现 OpenvSwitch，它能对经过自己的包做任意修改，从而使得云对网络的控制十分灵活；

将 OpenvSwitch 引入了云之后，可以使得配置简单而灵活，并且可以解耦物理网络和虚拟网络。

最后，给你留两个思考题：

在这一节中，提到了通过 VIP 可以通过流表在不同的机器之间实现复杂均衡，你知道怎样才能做到吗？

虽然 OpenvSwitch 可以解耦物理网络和虚拟网络，但是在物理网络里面使用 VLAN，数目还是不够，你知道该怎么办吗？

我们的专栏更新到第 25 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第26讲 | 云中的网络安全：虽然不是土豪，也需要基本安全和保障

在今天的内容开始之前，我先卖个关子。文章结尾，我会放一个超级彩蛋，所以，今天的内容你一定要看到最后哦！

上一节我们看到，做一个小区物业维护一个大家共享的环境，还是挺不容易的。如果都是自觉遵守规则的住户那还好，如果遇上不自觉的住户就会很麻烦。

就像公有云的环境，其实没有你想的那么纯净，各怀鬼胎的黑客到处都是。扫描你的端口呀，探测一下你启动的什么应用啊，看一看是否有各种漏洞啊。这就像小偷潜入小区后，这儿看看，那儿瞧瞧，窗户有没有关严了啊，窗帘有没有拉上啊，主人睡了没，是不是时机潜入室内啊，等等。

假如你创建了一台虚拟机，里面明明跑了一个电商应用，这是你非常重要的一个应用，你会把它进行安全加固。这台虚拟机的操作系统里，不小心安装了另外一个后台应用，监听着一个端口，而你的警觉性没有这么高。

虚拟机的这个端口是对着公网开放的，碰巧这个后台应用本身是有漏洞的，黑客就可以扫描到这个端口，然后通过这个后台应用的端口侵入你的机器，将你加固好的电商网站黑掉。这就像你买了一个五星级的防盗门，卡车都撞不开，但是厕所窗户的门把手是坏的，小偷从厕所里面就进来了。

所以对于公有云上的虚拟机，我的建议是仅仅开放需要的端口，而将其他的端口一概关闭。这个时候，你只要通过安全措施守护好这个唯一的入口就可以了。采用的方式常常是用ACL（Access Control List，访问控制列表）来控制 IP 和端口。

设置好了这些规则，只有指定的 IP 段能够访问指定的开放接口，就算有个有漏洞的后台进程在那里，也会被屏蔽，黑客进不来。在云平台上，这些规则的集合常称为安全组。那安全组怎么实现呢？

我们来复习一下，当一个网络包进入一台机器的时候，都会做什么事情。

首先拿下 MAC 头看看，是不是我的。如果是，则拿下 IP 头来。得到目标 IP 之后呢，就开始进行路由判断。在路由判断之前，这个节点我们称为PREROUTING。如果发现 IP 是我的，包就应该是我的，就发给上面的传输层，这个节点叫作INPUT。如果发现 IP 不是我的，就需要转发出去，这个节点称为FORWARD。如果是我的，上层处理完毕完毕后，一般会返回一个处理结果，这个处理结果会发出去，这个节点称为OUTPUT，无论是 FORWARD 还是 OUTPUT，都是路由判断之后发生的，最后一个节点是POSTROUTING。

整个过程如图所示。

整个包的处理过程还是原来的过程，只不过为什么要格外关注这五个节点呢？

是因为在 Linux 内核中，有一个框架叫 Netfilter。它可以在这些节点插入 hook 函数。这些函数可以截获数据包，对数据包进行干预。例如做一定的修改，然后决策是否接着交给 TCP/IP 协议栈处理；或者可以交回给协议栈，那就是ACCEPT；或者过滤掉，不再传输，就是DROP；还有就是QUEUE，发送给某个用户态进程处理。

这个比较难理解，经常用在内部负载均衡，就是过来的数据一会儿传给目标地址 1，一会儿传给目标地址 2，而且目标地址的个数和权重都可能变。协议栈往往处理不了这么复杂的逻辑，需要写一个函数接管这个数据，实现自己的逻辑。

有了这个 Netfilter 框架就太好了，你可以在 IP 转发的过程中，随时干预这个过程，只要你能实现这些 hook 函数。

一个著名的实现，就是内核模块 ip_tables。它在这五个节点上埋下函数，从而可以根据规则进行包的处理。按功能可分为四大类：连接跟踪（conntrack）、数据包的过滤（filter）、网络地址转换（nat）和数据包的修改（mangle）。其中连接跟踪是基础功能，被其他功能所依赖。其他三个可以实现包的过滤、修改和网络地址转换。

在用户态，还有一个你肯定知道的客户端程序 iptables，用命令行来干预内核的规则。内核的功能对应 iptables 的命令行来讲，就是表和链的概念。

iptables 的表分为四种：raw–>mangle–>nat–>filter。这四个优先级依次降低，raw 不常用，所以主要功能都在其他三种表里实现。每个表可以设置多个链。

filter 表处理过滤功能，主要包含三个链：

INPUT 链：过滤所有目标地址是本机的数据包；

FORWARD 链：过滤所有路过本机的数据包；

OUTPUT 链：过滤所有由本机产生的数据包。

nat 表主要是处理网络地址转换，可以进行 Snat（改变数据包的源地址）、Dnat（改变数据包的目标地址），包含三个链：

PREROUTING 链：可以在数据包到达防火墙时改变目标地址；

OUTPUT 链：可以改变本地产生的数据包的目标地址；

POSTROUTING 链：在数据包离开防火墙时改变数据包的源地址。

mangle 表主要是修改数据包，包含：

PREROUTING 链；

INPUT 链；

FORWARD 链；

OUTPUT 链；

POSTROUTING 链。

将 iptables 的表和链加入到上面的过程图中，就形成了下面的图和过程。

数据包进入的时候，先进 mangle 表的 PREROUTING 链。在这里可以根据需要，改变数据包头内容之后，进入 nat 表的 PREROUTING 链，在这里可以根据需要做 Dnat，也就是目标地址转换。

进入路由判断，要判断是进入本地的还是转发的。

如果是进入本地的，就进入 INPUT 链，之后按条件过滤限制进入。

之后进入本机，再进入 OUTPUT 链，按条件过滤限制出去，离开本地。

如果是转发就进入 FORWARD 链，根据条件过滤限制转发。

之后进入 POSTROUTING 链，这里可以做 Snat，离开网络接口。

有了 iptables 命令，我们就可以在云中实现一定的安全策略。例如我们可以处理前面的偷窥事件。首先我们将所有的门都关闭。

iptables -t filter -A INPUT -s 0.0.0.0/0.0.0.0 -d X.X.X.X -j DROP

-s 表示源 IP 地址段，-d 表示目标地址段，DROP 表示丢弃，也即无论从哪里来的，要想访问我这台机器，全部拒绝，谁也黑不进来。

但是你发现坏了，ssh 也进不来了，都不能远程运维了，可以打开一下。

iptables -I INPUT -s 0.0.0.0/0.0.0.0 -d X.X.X.X -p tcp --dport 22 -j ACCEPT

如果这台机器是提供的是 web 服务，80 端口也应该打开，当然一旦打开，这个 80 端口就需要很好的防护，但是从规则角度还是要打开。

iptables -A INPUT -s 0.0.0.0/0.0.0.0 -d X.X.X.X -p tcp --dport 80 -j ACCEPT

这样就搞定了，其他的账户都封死，就一个防盗门可以进出，只要防盗门是五星级的，就比较安全了。

这些规则都可以在虚拟机里，自己安装 iptables 自己配置。但是如果虚拟机数目非常多，都要配置，对于用户来讲就太麻烦了，能不能让云平台把这部分工作做掉呢？

当然可以了。在云平台上，一般允许一个或者多个虚拟机属于某个安全组，而属于不同安全组的虚拟机之间的访问以及外网访问虚拟机，都需要通过安全组进行过滤。

例如图中，我们会创建一系列的网站，都是前端在 Tomcat 里面，对外开放 8080 端口。数据库使用 MySQL，开放 3306 端口。

为了方便运维，我们创建两个安全组，将 Tomcat 所在的虚拟机放在安全组 A 里面。在安全组 A 里面，允许任意 IP 地址 0.0.0.0/0 访问 8080 端口，但是对于 ssh 的 22 端口，仅仅允许管理员网段 203.0.113.0/24 访问。

我们将 MySQL 所在的虚拟机在安全组 B 里面。在安全组 B 里面，仅仅允许来自安全组 A 的机器访问 3306 端口，但是对于 ssh 的 22 端口，同样允许管理员网段 203.0.113.0/24 访问。

这些安全组规则都可以自动下发到每个在安全组里面的虚拟机上，从而控制一大批虚拟机的安全策略。这种批量下发是怎么做到的呢？你还记得这幅图吗？

两个 VM 都通过 tap 网卡连接到一个网桥上，但是网桥是二层的，两个 VM 之间是可以随意互通的，因而需要有一个地方统一配置这些 iptables 规则。

可以多加一个网桥，在这个网桥上配置 iptables 规则，将在用户在界面上配置的规则，放到这个网桥上。然后在每台机器上跑一个 Agent，将用户配置的安全组变成 iptables 规则，配置在这个网桥上。

安全问题解决了，iptables 真强大！别忙，iptables 除了 filter，还有 nat 呢，这个功能也非常重要。

前面的章节我们说过，在设计云平台的时候，我们想让虚拟机之间的网络和物理网络进行隔离，但是虚拟机毕竟还是要通过物理网和外界通信的，因而需要在出物理网的时候，做一次网络地址转换，也即 nat，这个就可以用 iptables 来做。

我们学过，IP 头里面包含源 IP 地址和目标 IP 地址，这两种 IP 地址都可以转换成其他地址。转换源 IP 地址的，我们称为 Snat；转换目标 IP 地址的，我们称为 Dnat。

你有没有思考过这个问题，TCP 的访问都是一去一回的，而你在你家里连接 WIFI 的 IP 地址是一个私网 IP，192.168.1.x。当你通过你们家的路由器访问 163 网站之后，网站的返回结果如何能够到达你的笔记本电脑呢？肯定不能通过 192.168.1.x，这是个私网 IP，不具有公网上的定位能力，而且用这个网段的人很多，茫茫人海，怎么能够找到你呢？

所以当你从你家里访问 163 网站的时候，在你路由器的出口，会做 Snat 的，运营商的出口也可能做 Snat，将你的私网 IP 地址，最终转换为公网 IP 地址，然后 163 网站就可以通过这个公网 IP 地址返回结果，然后再 nat 回来，直到到达你的笔记本电脑。

云平台里面的虚拟机也是这样子的，它只有私网 IP 地址，到达外网网口要做一次 Snat，转换成为机房网 IP，然后出数据中心的时候，再转换为公网 IP。

这里有一个问题是，在外网网口上做 Snat 的时候，是全部转换成一个机房网 IP 呢，还是每个虚拟机都对应一个机房网 IP，最终对应一个公网 IP 呢？前面也说过了，公网 IP 非常贵，虚拟机也很多，当然不能每个都有单独的机房网和公网 IP 了，于是这种 Snat 是一种特殊的 Snat，MASQUERADE（地址伪装）。

这种方式下，所有的虚拟机共享一个机房网和公网的 IP 地址，所有从外网网口出去的，都转换成为这个 IP 地址。那又一个问题来了，都变成一个公网 IP 了，当 163 网站返回结果的时候，给谁呢，再 nat 成为哪个私网的 IP 呢？

这就是 Netfilter 的连接跟踪（conntrack）功能了。对于 TCP 协议来讲，肯定是上来先建立一个连接，可以用“源 / 目的 IP+ 源 / 目的端口”唯一标识一条连接，这个连接会放在 conntrack 表里面。当时是这台机器去请求 163 网站的，虽然源地址已经 Snat 成公网 IP 地址了，但是 conntrack 表里面还是有这个连接的记录的。当 163 网站返回数据的时候，会找到记录，从而找到正确的私网 IP 地址。

这是虚拟机做客户端的情况，如果虚拟机做服务器呢？也就是说，如果虚拟机里面部署的就是 163 网站呢？

这个时候就需要给这个网站配置固定的物理网的 IP 地址和公网 IP 地址了。这时候就需要显示的配置 Snat 规则和 Dnat 规则了。

当外部访问进来的时候，外网网口会通过 Dnat 规则将公网 IP 地址转换为私网 IP 地址，到达虚拟机，虚拟机里面是 163 网站，返回结果，外网网口会通过 Snat 规则，将私网 IP 地址转换为那个分配给它的固定的公网 IP 地址。

类似的规则如下：

源地址转换 (Snat)：iptables -t nat -A -s 私网 IP -j Snat --to-source 外网 IP

目的地址转换 (Dnat)：iptables -t nat -A -PREROUTING -d 外网 IP -j Dnat --to-destination 私网 IP

到此为止 iptables 解决了非法偷窥隐私的问题。

小结

好了，这一节就讲到这里了，我们来总结一下。

云中的安全策略的常用方式是，使用 iptables 的规则，请记住它的五个阶段，PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING。

iptables 分为四种表，raw、mangle、nat、filter。其中安全策略主要在 filter 表中实现，而虚拟网络和物理网络地址的转换主要在 nat 表中实现。

最后，给你留两个思考题。

这一节中重点讲了 iptables 的 filter 和 nat 功能，iptables 还可以通过 QUEUE 实现负载均衡，你知道怎么做吗？

这一节仅仅讲述了云中偷窥的问题，如果是一个合法的用户，但是不自觉抢占网络通道，应该采取什么策略呢？

我们的专栏更新到第 26 讲，不知你掌握得如何？是不是有很多问题想要跟我面对面探讨呢？这里就有一个机会。

今天晚上 8:30，我会在极客时间 APP 里做一个直播，主题是“技术人如何在技术浪潮中线性成长？”，我会把我们讲过的网络协议作为案例，在直播中展开讲解，也会分享我从业多年来的心得体会。你可以直接在这里留言提问，也可以准备好问题在直播的时候和我交流。

欢迎你来看直播！我们晚上见！

第27讲 | 云中的网络QoS：邻居疯狂下电影，我该怎么办？

在小区里面，是不是经常有住户不自觉就霸占公共通道，如果你找他理论，他的话就像一个相声《楼道曲》说的一样：“公用公用，你用我用，大家都用，我为什么不能用？”。

除此之外，你租房子的时候，有没有碰到这样的情况：本来合租共享 WIFI，一个人狂下小电影，从而你网都上不去，是不是很懊恼？

在云平台上，也有这种现象，好在有一种流量控制的技术，可以实现QoS（Quality of Service），从而保障大多数用户的服务质量。

对于控制一台机器的网络的 QoS，分两个方向，一个是入方向，一个是出方向。

其实我们能控制的只有出方向，通过 Shaping，将出的流量控制成自己想要的模样。而进入的方向是无法控制的，只能通过 Policy 将包丢弃。

控制网络的 QoS 有哪些方式？

在 Linux 下，可以通过 TC 控制网络的 QoS，主要就是通过队列的方式。

无类别排队规则

第一大类称为无类别排队规则（Classless Queuing Disciplines）。还记得我们讲ip addr的时候讲过的pfifo_fast，这是一种不把网络包分类的技术。

pfifo_fast 分为三个先入先出的队列，称为三个 Band。根据网络包里面 TOS，看这个包到底应该进入哪个队列。TOS 总共四位，每一位表示的意思不同，总共十六种类型。

通过命令行 tc qdisc show dev eth0，可以输出结果 priomap，也是十六个数字。在 0 到 2 之间，和 TOS 的十六种类型对应起来，表示不同的 TOS 对应的不同的队列。其中 Band 0 优先级最高，发送完毕后才轮到 Band 1 发送，最后才是 Band 2。

另外一种无类别队列规则叫作随机公平队列（Stochastic Fair Queuing）。

会建立很多的 FIFO 的队列，TCP Session 会计算 hash 值，通过 hash 值分配到某个队列。在队列的另一端，网络包会通过轮询策略从各个队列中取出发送。这样不会有一个 Session 占据所有的流量。

当然如果两个 Session 的 hash 是一样的，会共享一个队列，也有可能互相影响。hash 函数会经常改变，从而 session 不会总是相互影响。

还有一种无类别队列规则称为令牌桶规则（TBF，Token Bucket Filte）。

所有的网络包排成队列进行发送，但不是到了队头就能发送，而是需要拿到令牌才能发送。

令牌根据设定的速度生成，所以即便队列很长，也是按照一定的速度进行发送的。

当没有包在队列中的时候，令牌还是以既定的速度生成，但是不是无限累积的，而是放满了桶为止。设置桶的大小为了避免下面的情况：当长时间没有网络包发送的时候，积累了大量的令牌，突然来了大量的网络包，每个都能得到令牌，造成瞬间流量大增。

基于类别的队列规则

另外一大类是基于类别的队列规则（Classful Queuing Disciplines），其中典型的为分层令牌桶规则（HTB， Hierarchical Token Bucket）。

HTB 往往是一棵树，接下来我举个具体的例子，通过 TC 如何构建一棵 HTB 树来带你理解。

使用 TC 可以为某个网卡 eth0 创建一个 HTB 的队列规则，需要付给它一个句柄为（1:）。

这是整棵树的根节点，接下来会有分支。例如图中有三个分支，句柄分别为（:10）、（:11）、（:12）。最后的参数 default 12，表示默认发送给 1:12，也即发送给第三个分支。

tc qdisc add dev eth0 root handle 1: htb default 12

对于这个网卡，需要规定发送的速度。一般有两个速度可以配置，一个是rate，表示一般情况下的速度；一个是ceil，表示最高情况下的速度。对于根节点来讲，这两个速度是一样的，于是创建一个 root class，速度为（rate=100kbps，ceil=100kbps）。

tc class add dev eth0 parent 1: classid 1:1 htb rate 100kbps ceil 100kbps

接下来要创建分支，也即创建几个子 class。每个子 class 统一有两个速度。三个分支分别为（rate=30kbps，ceil=100kbps）、（rate=10kbps，ceil=100kbps）、（rate=60kbps，ceil=100kbps）。

tc class add dev eth0 parent 1:1 classid 1:10 htb rate 30kbps ceil 100kbps

tc class add dev eth0 parent 1:1 classid 1:11 htb rate 10kbps ceil 100kbps

tc class add dev eth0 parent 1:1 classid 1:12 htb rate 60kbps ceil 100kbps

你会发现三个 rate 加起来，是整个网卡允许的最大速度。

HTB 有个很好的特性，同一个 root class 下的子类可以相互借流量，如果不直接在队列规则下面创建一个 root class，而是直接创建三个 class，它们之间是不能相互借流量的。借流量的策略，可以使得当前不使用这个分支的流量的时候，可以借给另一个分支，从而不浪费带宽，使带宽发挥最大的作用。

最后，创建叶子队列规则，分别为fifo和sfq。

tc qdisc add dev eth0 parent 1:10 handle 20: pfifo limit 5

tc qdisc add dev eth0 parent 1:11 handle 30: pfifo limit 5

tc qdisc add dev eth0 parent 1:12 handle 40: sfq perturb 10

基于这个队列规则，我们还可以通过 TC 设定发送规则：从 1.2.3.4 来的，发送给 port 80 的包，从第一个分支 1:10 走；其他从 1.2.3.4 发送来的包从第二个分支 1:11 走；其他的走默认分支。

tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 match ip dport 80 0xffff flowid 1:10

tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 flowid 1:11

如何控制 QoS？

我们讲过，使用 OpenvSwitch 将云中的网卡连通在一起，那如何控制 QoS 呢？

就像我们上面说的一样，OpenvSwitch 支持两种：

对于进入的流量，可以设置策略 Ingress policy；

ovs-vsctl set Interface tap0 ingress_policing_rate=100000

ovs-vsctl set Interface tap0 ingress_policing_burst=10000

对于发出的流量，可以设置 QoS 规则 Egress shaping，支持 HTB。

我们构建一个拓扑图，来看看 OpenvSwitch 的 QoS 是如何工作的。

首先，在 port 上可以创建 QoS 规则，一个 QoS 规则可以有多个队列 Queue。

ovs-vsctl set port first_br qos=@newqos -- --id=@newqos create qos type=linux-htb other-config:max-rate=10000000 queues=0=@q0,1=@q1,2=@q2 -- --id=@q0 create queue other-config:min-rate=3000000 other-config:max-rate=10000000 -- --id=@q1 create queue other-config:min-rate=1000000 other-config:max-rate=10000000 -- --id=@q2 create queue other-config:min-rate=6000000 other-config:max-rate=10000000

上面的命令创建了一个 QoS 规则，对应三个 Queue。min-rate 就是上面的 rate，max-rate 就是上面的 ceil。通过交换机的网络包，要通过流表规则，匹配后进入不同的队列。然后我们就可以添加流表规则 Flow(first_br 是 br0 上的 port 5)。

ovs-ofctl add-flow br0 "in_port=6 nw_src=192.168.100.100 actions=enqueue:5:0"

ovs-ofctl add-flow br0 "in_port=7 nw_src=192.168.100.101 actions=enqueue:5:1"

ovs-ofctl add-flow br0 "in_port=8 nw_src=192.168.100.102 actions=enqueue:5:2"

接下来，我们单独测试从 192.168.100.100，192.168.100.101，192.168.100.102 到 192.168.100.103 的带宽的时候，每个都是能够打满带宽的。

如果三个一起测试，一起狂发网络包，会发现是按照 3:1:6 的比例进行的，正是根据配置的队列的带宽比例分配的。

如果 192.168.100.100 和 192.168.100.101 一起测试，发现带宽占用比例为 3:1，但是占满了总的流量，也即没有发包的 192.168.100.102 有 60% 的带宽被借用了。

如果 192.168.100.100 和 192.168.100.102 一起测试，发现带宽占用比例为 1:2。如果 192.168.100.101 和 192.168.100.102 一起测试，发现带宽占用比例为 1:6。

小结

好了，这一节就讲到这里了，我们来总结一下。

云中的流量控制主要通过队列进行的，队列分为两大类：无类别队列规则和基于类别的队列规则。

在云中网络 Openvswitch 中，主要使用的是分层令牌桶规则（HTB），将总的带宽在一棵树上按照配置的比例进行分配，并且在一个分支不用的时候，可以借给另外的分支，从而增强带宽利用率。

最后，给你留两个思考题。

这一节中提到，入口流量其实没有办法控制，出口流量是可以很好控制的，你能想出一个控制云中的虚拟机的入口流量的方式吗？

安全性和流量控制大概解决了，但是不同用户在物理网络的隔离还是没有解决，你知道怎么解决吗？

我们的专栏更新到第 27 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第28讲 | 云中网络的隔离GRE、VXLAN：虽然住一个小区，也要保护隐私

对于云平台中的隔离问题，前面咱们用的策略一直都是 VLAN，但是我们也说过这种策略的问题，VLAN 只有 12 位，共 4096 个。当时设计的时候，看起来是够了，但是现在绝对不够用，怎么办呢？

一种方式是修改这个协议。这种方法往往不可行，因为当这个协议形成一定标准后，千千万万设备上跑的程序都要按这个规则来。现在说改就放，谁去挨个儿告诉这些程序呢？很显然，这是一项不可能的工程。

另一种方式就是扩展，在原来包的格式的基础上扩展出一个头，里面包含足够用于区分租户的 ID，外层的包的格式尽量和传统的一样，依然兼容原来的格式。一旦遇到需要区分用户的地方，我们就用这个特殊的程序，来处理这个特殊的包的格式。

这个概念很像咱们第 22 讲讲过的隧道理论，还记得自驾游通过摆渡轮到海南岛的那个故事吗？在那一节，我们说过，扩展的包头主要是用于加密的，而我们现在需要的包头是要能够区分用户的。

底层的物理网络设备组成的网络我们称为Underlay 网络，而用于虚拟机和云中的这些技术组成的网络称为Overlay 网络，这是一种基于物理网络的虚拟化网络实现。这一节我们重点讲两个 Overlay 的网络技术。

GRE

第一个技术是GRE，全称 Generic Routing Encapsulation，它是一种 IP-over-IP 的隧道技术。它将 IP 包封装在 GRE 包里，外面加上 IP 头，在隧道的一端封装数据包，并在通路上进行传输，到另外一端的时候解封装。你可以认为 Tunnel 是一个虚拟的、点对点的连接。

从这个图中可以看到，在 GRE 头中，前 32 位是一定会有的，后面的都是可选的。在前 4 位标识位里面，有标识后面到底有没有可选项？这里面有个很重要的 key 字段，是一个 32 位的字段，里面存放的往往就是用于区分用户的 Tunnel ID。32 位，够任何云平台喝一壶的了！

下面的格式类型专门用于网络虚拟化的 GRE 包头格式，称为NVGRE，也给网络 ID 号 24 位，也完全够用了。

除此之外，GRE 还需要有一个地方来封装和解封装 GRE 的包，这个地方往往是路由器或者有路由功能的 Linux 机器。

使用 GRE 隧道，传输的过程就像下面这张图。这里面有两个网段、两个路由器，中间要通过 GRE 隧道进行通信。当隧道建立之后，会多出两个 Tunnel 端口，用于封包、解封包。

主机 A 在左边的网络，IP 地址为 192.168.1.102，它想要访问主机 B，主机 B 在右边的网络，IP 地址为 192.168.2.115。于是发送一个包，源地址为 192.168.1.102，目标地址为 192.168.2.115。因为要跨网段访问，于是根据默认的 default 路由表规则，要发给默认的网关 192.168.1.1，也即左边的路由器。

根据路由表，从左边的路由器，去 192.168.2.0/24 这个网段，应该走一条 GRE 的隧道，从隧道一端的网卡 Tunnel0 进入隧道。

在 Tunnel 隧道的端点进行包的封装，在内部的 IP 头之外加上 GRE 头。对于 NVGRE 来讲，是在 MAC 头之外加上 GRE 头，然后加上外部的 IP 地址，也即路由器的外网 IP 地址。源 IP 地址为 172.17.10.10，目标 IP 地址为 172.16.11.10，然后从 E1 的物理网卡发送到公共网络里。

在公共网络里面，沿着路由器一跳一跳地走，全部都按照外部的公网 IP 地址进行。

当网络包到达对端路由器的时候，也要到达对端的 Tunnel0，然后开始解封装，将外层的 IP 头取下来，然后根据里面的网络包，根据路由表，从 E3 口转发出去到达服务器 B。

从 GRE 的原理可以看出，GRE 通过隧道的方式，很好地解决了 VLAN ID 不足的问题。但是，GRE 技术本身还是存在一些不足之处。

首先是Tunnel 的数量问题。GRE 是一种点对点隧道，如果有三个网络，就需要在每两个网络之间建立一个隧道。如果网络数目增多，这样隧道的数目会呈指数性增长。

其次，GRE 不支持组播，因此一个网络中的一个虚机发出一个广播帧后，GRE 会将其广播到所有与该节点有隧道连接的节点。

另外一个问题是目前还是有很多防火墙和三层网络设备无法解析 GRE，因此它们无法对 GRE 封装包做合适地过滤和负载均衡。

VXLAN

第二种 Overlay 的技术称为 VXLAN。和三层外面再套三层的 GRE 不同，VXLAN 则是从二层外面就套了一个 VXLAN 的头，这里面包含的 VXLAN ID 为 24 位，也够用了。在 VXLAN 头外面还封装了 UDP、IP，以及外层的 MAC 头。

VXLAN 作为扩展性协议，也需要一个地方对 VXLAN 的包进行封装和解封装，实现这个功能的点称为VTEP（VXLAN Tunnel Endpoint）。

VTEP 相当于虚拟机网络的管家。每台物理机上都可以有一个 VTEP。每个虚拟机启动的时候，都需要向这个 VTEP 管家注册，每个 VTEP 都知道自己上面注册了多少个虚拟机。当虚拟机要跨 VTEP 进行通信的时候，需要通过 VTEP 代理进行，由 VTEP 进行包的封装和解封装。

和 GRE 端到端的隧道不同，VXLAN 不是点对点的，而是支持通过组播的来定位目标机器的，而非一定是这一端发出，另一端接收。

当一个 VTEP 启动的时候，它们都需要通过 IGMP 协议。加入一个组播组，就像加入一个邮件列表，或者加入一个微信群一样，所有发到这个邮件列表里面的邮件，或者发送到微信群里面的消息，大家都能收到。而当每个物理机上的虚拟机启动之后，VTEP 就知道，有一个新的 VM 上线了，它归我管。

如图，虚拟机 1、2、3 属于云中同一个用户的虚拟机，因而需要分配相同的 VXLAN ID=101。在云的界面上，就可以知道它们的 IP 地址，于是可以在虚拟机 1 上 ping 虚拟机 2。

虚拟机 1 发现，它不知道虚拟机 2 的 MAC 地址，因而包没办法发出去，于是要发送 ARP 广播。

ARP 请求到达 VTEP1 的时候，VTEP1 知道，我这里有一台虚拟机，要访问一台不归我管的虚拟机，需要知道 MAC 地址，可是我不知道啊，这该咋办呢？

VTEP1 想，我不是加入了一个微信群么？可以在里面 @all 一下，问问虚拟机 2 归谁管。于是 VTEP1 将 ARP 请求封装在 VXLAN 里面，组播出去。

当然在群里面，VTEP2 和 VTEP3 都收到了消息，因而都会解开 VXLAN 包看，里面是一个 ARP。

VTEP3 在本地广播了半天，没人回，都说虚拟机 2 不归自己管。

VTEP2 在本地广播，虚拟机 2 回了，说虚拟机 2 归我管，MAC 地址是这个。通过这次通信，VTEP2 也学到了，虚拟机 1 归 VTEP1 管，以后要找虚拟机 1，去找 VTEP1 就可以了。

VTEP2 将 ARP 的回复封装在 VXLAN 里面，这次不用组播了，直接发回给 VTEP1。

VTEP1 解开 VXLAN 的包，发现是 ARP 的回复，于是发给虚拟机 1。通过这次通信，VTEP1 也学到了，虚拟机 2 归 VTEP2 管，以后找虚拟机 2，去找 VTEP2 就可以了。

虚拟机 1 的 ARP 得到了回复，知道了虚拟机 2 的 MAC 地址，于是就可以发送包了。

虚拟机 1 发给虚拟机 2 的包到达 VTEP1，它当然记得刚才学的东西，要找虚拟机 2，就去 VTEP2，于是将包封装在 VXLAN 里面，外层加上 VTEP1 和 VTEP2 的 IP 地址，发送出去。

网络包到达 VTEP2 之后，VTEP2 解开 VXLAN 封装，将包转发给虚拟机 2。

虚拟机 2 回复的包，到达 VTEP2 的时候，它当然也记得刚才学的东西，要找虚拟机 1，就去 VTEP1，于是将包封装在 VXLAN 里面，外层加上 VTEP1 和 VTEP2 的 IP 地址，也发送出去。

网络包到达 VTEP1 之后，VTEP1 解开 VXLAN 封装，将包转发给虚拟机 1。

有了 GRE 和 VXLAN 技术，我们就可以解决云计算中 VLAN 的限制了。那如何将这个技术融入云平台呢？

还记得将你宿舍里面的情况，所有东西都搬到一台物理机上那个故事吗？

虚拟机是你的电脑，路由器和 DHCP Server 相当于家用路由器或者寝室长的电脑，外网网口访问互联网，所有的电脑都通过内网网口连接到一个交换机 br0 上，虚拟机要想访问互联网，需要通过 br0 连到路由器上，然后通过路由器将请求 NAT 后转发到公网。

接下来的事情就惨了，你们宿舍闹矛盾了，你们要分成三个宿舍住，对应上面的图，你们寝室长，也即路由器单独在一台物理机上，其他的室友也即 VM 分别在两台物理机上。这下把一个完整的 br0 一刀三断，每个宿舍都是单独的一段。

可是只有你的寝室长有公网口可以上网，于是你偷偷在三个宿舍中间打了一个隧道，用网线通过隧道将三个宿舍的两个 br0 连接起来，让其他室友的电脑和你寝室长的电脑，看起来还是连到同一个 br0 上，其实中间是通过你隧道中的网线做了转发。

为什么要多一个 br1 这个虚拟交换机呢？主要通过 br1 这一层将虚拟机之间的互联和物理机机之间的互联分成两层来设计，中间隧道可以有各种挖法，GRE、VXLAN 都可以。

使用了 OpenvSwitch 之后，br0 可以使用 OpenvSwitch 的 Tunnel 功能和 Flow 功能。

OpenvSwitch 支持三类隧道：GRE、VXLAN、IPsec_GRE。在使用 OpenvSwitch 的时候，虚拟交换机就相当于 GRE 和 VXLAN 封装的端点。

我们模拟创建一个如下的网络拓扑结构，来看隧道应该如何工作。

三台物理机，每台上都有两台虚拟机，分别属于两个不同的用户，因而 VLAN tag 都得打地不一样，这样才不能相互通信。但是不同物理机上的相同用户，是可以通过隧道相互通信的，因而通过 GRE 隧道可以连接到一起。

接下来，所有的 Flow Table 规则都设置在 br1 上，每个 br1 都有三个网卡，其中网卡 1 是对内的，网卡 2 和 3 是对外的。

下面我们具体来看 Flow Table 的设计。

1.Table 0 是所有流量的入口，所有进入 br1 的流量，分为两种流量，一个是进入物理机的流量，一个是从物理机发出的流量。

从 port 1 进来的，都是发出去的流量，全部由 Table 1 处理。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1 in_port=1 actions=resubmit(,1)"

从 port 2、3 进来的，都是进入物理机的流量，全部由 Table 3 处理。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1 in_port=2 actions=resubmit(,3)"

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1 in_port=3 actions=resubmit(,3)"

如果都没匹配上，就默认丢弃。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=0 actions=drop"

2.Table 1 用于处理所有出去的网络包，分为两种情况，一种是单播，一种是多播。

对于单播，由 Table 20 处理。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1 table=1 dl_dst=00:00:00:00:00:00/01:00:00:00:00:00 actions=resubmit(,20)"

对于多播，由 Table 21 处理。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1 table=1 dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=resubmit(,21)"

3.Table 2 是紧接着 Table1 的，如果既不是单播，也不是多播，就默认丢弃。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=0 table=2 actions=drop"

4.Table 3 用于处理所有进来的网络包，需要将隧道 Tunnel ID 转换为 VLAN ID。

如果匹配不上 Tunnel ID，就默认丢弃。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=0 table=3 actions=drop"

如果匹配上了 Tunnel ID，就转换为相应的 VLAN ID，然后跳到 Table 10。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1 table=3 tun_id=0x1 actions=mod_vlan_vid:1,resubmit(,10)"

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1 table=3 tun_id=0x2 actions=mod_vlan_vid:2,resubmit(,10)"

5. 对于进来的包，Table 10 会进行 MAC 地址学习。这是一个二层交换机应该做的事情，学习完了之后，再从 port 1 发出去。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1 table=10  actions=learn(table=20,priority=1,hard_timeout=300,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:0->NXM_OF_VLAN_TCI[],load:NXM_NX_TUN_ID[]->NXM_NX_TUN_ID[],output:NXM_OF_IN_PORT[]),output:1"

Table 10 是用来学习 MAC 地址的，学习的结果放在 Table 20 里面。Table20 被称为 MAC learning table。

NXM_OF_VLAN_TCI 是 VLAN tag。在 MAC learning table 中，每一个 entry 都仅仅是针对某一个 VLAN 来说的，不同 VLAN 的 learning table 是分开的。在学习结果的 entry 中，会标出这个 entry 是针对哪个 VLAN 的。

NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[] 表示，当前包里面的 MAC Source Address 会被放在学习结果的 entry 里的 dl_dst 里。这是因为每个交换机都是通过进入的网络包来学习的。某个 MAC 从某个 port 进来，交换机就应该记住，以后发往这个 MAC 的包都要从这个 port 出去，因而源 MAC 地址就被放在了目标 MAC 地址里面，因为这是为了发送才这么做的。

load:0->NXM_OF_VLAN_TCI[] 是说，在 Table20 中，将包从物理机发送出去的时候，VLAN tag 设为 0，所以学习完了之后，Table 20 中会有 actions=strip_vlan。

load:NXM_NX_TUN_ID[]->NXM_NX_TUN_ID[] 的意思是，在 Table 20 中，将包从物理机发出去的时候，设置 Tunnel ID，进来的时候是多少，发送的时候就是多少，所以学习完了之后，Table 20 中会有 set_tunnel。

output:NXM_OF_IN_PORT[] 是发送给哪个 port。例如是从 port 2 进来的，那学习完了之后，Table 20 中会有 output:2。

所以如图所示，通过左边的 MAC 地址学习规则，学习到的结果就像右边的一样，这个结果会被放在 Table 20 里面。

6.Table 20 是 MAC Address Learning Table。如果不为空，就按照规则处理；如果为空，就说明没有进行过 MAC 地址学习，只好进行广播了，因而要交给 Table 21 处理。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=0 table=20 actions=resubmit(,21)"

7.Table 21 用于处理多播的包。

如果匹配不上 VLAN ID，就默认丢弃。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=0 table=21 actions=drop"

如果匹配上了 VLAN ID，就将 VLAN ID 转换为 Tunnel ID，从两个网卡 port 2 和 port 3 都发出去，进行多播。

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1table=21dl_vlan=1 actions=strip_vlan,set_tunnel:0x1,output:2,output:3"

ovs-ofctl add-flow br1 "hard_timeout=0 idle_timeout=0 priority=1table=21dl_vlan=2 actions=strip_vlan,set_tunnel:0x2,output:2,output:3"

小结

好了，这一节就到这里了，我们来总结一下。

要对不同用户的网络进行隔离，解决 VLAN 数目有限的问题，需要通过 Overlay 的方式，常用的有 GRE 和 VXLAN。

GRE 是一种点对点的隧道模式，VXLAN 支持组播的隧道模式，它们都要在某个 Tunnel Endpoint 进行封装和解封装，来实现跨物理机的互通。

OpenvSwitch 可以作为 Tunnel Endpoint，通过设置流表的规则，将虚拟机网络和物理机网络进行隔离、转换。

最后，给你留两个思考题。

虽然 VXLAN 可以支持组播，但是如果虚拟机数目比较多，在 Overlay 网络里面，广播风暴问题依然会很严重，你能想到什么办法解决这个问题吗？

基于虚拟机的云比较复杂，而且虚拟机里面的网卡，到物理网络转换层次比较多，有一种比虚拟机更加轻量级的云的模式，你知道是什么吗？

我们的专栏更新到第 28 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第29讲 | 容器网络：来去自由的日子，不买公寓去合租

如果说虚拟机是买公寓，容器则相当于合租，有一定的隔离，但是隔离性没有那么好。云计算解决了基础资源层的弹性伸缩，却没有解决 PaaS 层应用随基础资源层弹性伸缩而带来的批量、快速部署问题。于是，容器应运而生。

容器就是 Container，而 Container 的另一个意思是集装箱。其实容器的思想就是要变成软件交付的集装箱。集装箱的特点，一是打包，二是标准。

在没有集装箱的时代，假设要将货物从 A 运到 B，中间要经过三个码头、换三次船。每次都要将货物卸下船来，弄的乱七八糟，然后还要再搬上船重新整齐摆好。因此在没有集装箱的时候，每次换船，船员们都要在岸上待几天才能干完活。

有了尺寸全部都一样的集装箱以后，可以把所有的货物都打包在一起，所以每次换船的时候，一个箱子整体搬过去就行了，小时级别就能完成，船员再也不用耗费很长时间了。这是集装箱的“打包”“标准”两大特点在生活中的应用。

那么容器如何对应用打包呢？

学习集装箱，首先要有个封闭的环境，将货物封装起来，让货物之间互不干扰，互相隔离，这样装货卸货才方便。

封闭的环境主要使用了两种技术，一种是看起来是隔离的技术，称为namespace，也即每个 namespace 中的应用看到的是不同的 IP 地址、用户空间、程号等。另一种是用起来是隔离的技术，称为cgroup，也即明明整台机器有很多的 CPU、内存，而一个应用只能用其中的一部分。

有了这两项技术，就相当于我们焊好了集装箱。接下来的问题就是如何“将这个集装箱标准化”，并在哪艘船上都能运输。这里的标准首先就是镜像。

所谓镜像，就是将你焊好集装箱的那一刻，将集装箱的状态保存下来，就像孙悟空说：“定！”，集装箱里的状态就被定在了那一刻，然后将这一刻的状态保存成一系列文件。无论从哪里运行这个镜像，都能完整地还原当时的情况。

接下来我们就具体来看看，这两种网络方面的打包技术。

命名空间（namespace）

我们首先来看网络 namespace。

namespace 翻译过来就是命名空间。其实很多面向对象的程序设计语言里面，都有命名空间这个东西。大家一起写代码，难免类会起相同的名词，编译就会冲突。而每个功能都有自己的命名空间，在不同的空间里面，类名相同，不会冲突。

在 Linux 下也是这样的，很多的资源都是全局的。比如进程有全局的进程 ID，网络也有全局的路由表。但是，当一台 Linux 上跑多个进程的时候，如果我们觉得使用不同的路由策略，这些进程可能会冲突，那就需要将这个进程放在一个独立的 namespace 里面，这样就可以独立配置网络了。

网络的 namespace 由 ip netns 命令操作。它可以创建、删除、查询 namespace。

我们再来看将你们宿舍放进一台物理机的那个图。你们宿舍长的电脑是一台路由器，你现在应该知道怎么实现这个路由器吧？可以创建一个 Router 虚拟机来做这件事情，但是还有一个更加简单的办法，就是我在图里画的这条虚线，这个就是通过 namespace 实现的。

我们创建一个 routerns，于是一个独立的网络空间就产生了。你可以在里面尽情设置自己的规则。

ip netns add routerns

既然是路由器，肯定要能转发嘛，因而 forward 开关要打开。

ip netns exec routerns sysctl -w net.ipv4.ip_forward=1

exec 的意思就是进入这个网络空间做点事情。初始化一下 iptables，因为这里面要配置 NAT 规则。

ip netns exec routerns iptables-save -c

ip netns exec routerns iptables-restore -c

路由器需要有一张网卡连到 br0 上，因而要创建一个网卡。

ovs-vsctl -- add-port br0 taprouter -- set Interface taprouter type=internal -- set Interface taprouter external-ids:iface-status=active -- set Interface taprouter external-ids:attached-mac=fa:16:3e:84:6e:cc

这个网络创建完了，但是是在 namespace 外面的，如何进去呢？可以通过这个命令：

ip link set taprouter netns routerns

要给这个网卡配置一个 IP 地址，当然应该是虚拟机网络的网关地址。例如虚拟机私网网段为 192.168.1.0/24，网关的地址往往为 192.168.1.1。

ip netns exec routerns ip -4 addr add 192.168.1.1/24 brd 192.168.1.255 scope global dev taprouter

为了访问外网，还需要另一个网卡连在外网网桥 br-ex 上，并且塞在 namespace 里面。

ovs-vsctl -- add-port br-ex taprouterex -- set Interface taprouterex type=internal -- set Interface taprouterex external-ids:iface-status=active -- set Interface taprouterex external-ids:attached-mac=fa:16:3e:68:12:c0

ip link set taprouterex netns routerns

我们还需要为这个网卡分配一个地址，这个地址应该和物理外网网络在一个网段。假设物理外网为 16.158.1.0/24，可以分配一个外网地址 16.158.1.100/24。

ip netns exec routerns ip -4 addr add 16.158.1.100/24 brd 16.158.1.255 scope global dev taprouterex

接下来，既然是路由器，就需要配置路由表，路由表是这样的：

ip netns exec routerns route -n

Kernel IP routing table

Destination   Gateway     Genmask     Flags Metric Ref  Use Iface

0.0.0.0     16.158.1.1  0.0.0.0     UG  0   0    0 taprouterex

192.168.1.0    0.0.0.0     255.255.255.0  U   0   0    0 taprouter

16.158.1.0  0.0.0.0     255.255.255.0  U   0   0    0 taprouterex

路由表中的默认路由是去物理外网的，去 192.168.1.0/24 也即虚拟机私网，走下面的网卡，去 16.158.1.0/24 也即物理外网，走上面的网卡。

我们在前面的章节讲过，如果要在虚拟机里面提供服务，提供给外网的客户端访问，客户端需要访问外网 IP3，会在外网网口 NAT 称为虚拟机私网 IP。这个 NAT 规则要在这个 namespace 里面配置。

ip netns exec routerns iptables -t nat -nvL

Chain PREROUTING

target  prot opt  in  out  source  destination

DNAT  all  --  *  *  0.0.0.0/0 16.158.1.103 to:192.168.1.3

Chain POSTROUTING

target  prot opt  in  out  source   destination

SNAT  all  --  *  *  192.168.1.3  0.0.0.0/0 to:16.158.1.103

这里面有两个规则，一个是 SNAT，将虚拟机的私网 IP 192.168.1.3 NAT 成物理外网 IP 16.158.1.103。一个是 DNAT，将物理外网 IP 16.158.1.103 NAT 成虚拟机私网 IP 192.168.1.3。

至此为止，基于网络 namespace 的路由器实现完毕。

机制网络（cgroup）

我们再来看打包的另一个机制网络 cgroup。

cgroup 全称 control groups，是 Linux 内核提供的一种可以限制、隔离进程使用的资源机制。

cgroup 能控制哪些资源呢？它有很多子系统：

CPU 子系统使用调度程序为进程控制 CPU 的访问；

cpuset，如果是多核心的 CPU，这个子系统会为进程分配单独的 CPU 和内存；

memory 子系统，设置进程的内存限制以及产生内存资源报告；

blkio 子系统，设置限制每个块设备的输入输出控制；

net_cls，这个子系统使用等级识别符（classid）标记网络数据包，可允许 Linux 流量控制程序（tc）识别从具体 cgroup 中生成的数据包。

我们这里最关心的是 net_cls，它可以和前面讲过的 TC 关联起来。

cgroup 提供了一个虚拟文件系统，作为进行分组管理和各子系统设置的用户接口。要使用 cgroup，必须挂载 cgroup 文件系统，一般情况下都是挂载到 /sys/fs/cgroup 目录下。

所以首先我们要挂载一个 net_cls 的文件系统。

mkdir /sys/fs/cgroup/net_cls

mount -t cgroup -onet_cls net_cls /sys/fs/cgroup/net_cls

接下来我们要配置 TC 了。还记得咱们实验 TC 的时候那个树吗？

当时我们通过这个命令设定了规则：从 1.2.3.4 来的，发送给 port 80 的包，从 1:10 走；其他从 1.2.3.4 发送来的包从 1:11 走；其他的走默认。

tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 match ip dport 80 0xffff flowid 1:10

tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 flowid 1:11

这里是根据源 IP 来设定的，现在有了 cgroup，我们按照 cgroup 再来设定规则。

tc filter add dev eth0 protocol ip parent 1:0 prio 1 handle 1: cgroup

假设我们有两个用户 a 和 b，要对它们进行带宽限制。

首先，我们要创建两个 net_cls。

mkdir /sys/fs/cgroup/net_cls/a

mkdir /sys/fs/cgroup/net_cls/b

假设用户 a 启动的进程 ID 为 12345，把它放在 net_cls/a/tasks 文件中。同样假设用户 b 启动的进程 ID 为 12346，把它放在 net_cls/b/tasks 文件中。

net_cls/a 目录下面，还有一个文件 net_cls.classid，我们放 flowid 1:10。net_cls/b 目录下面，也创建一个文件 net_cls.classid，我们放 flowid 1:11。

这个数字怎么放呢？要转换成一个 0xAAAABBBB 的值，AAAA 对应 class 中冒号前面的数字，而 BBBB 对应后面的数字。

echo 0x00010010 > /sys/fs/cgroup/net_cls/a/net_cls.classid

echo 0x00010011 > /sys/fs/cgroup/net_cls/b/net_cls.classid

这样用户 a 的进程发的包，会打上 1:10 这个标签；用户 b 的进程发的包，会打上 1:11 这个标签。然后 TC 根据这两个标签，让用户 a 的进程的包走左边的分支，用户 b 的进程的包走右边的分支。

容器网络中如何融入物理网络？

了解了容器背后的技术，接下来我们来看，容器网络究竟是如何融入物理网络的？

如果你使用 docker run 运行一个容器，你应该能看到这样一个拓扑结构。

是不是和虚拟机很像？容器里面有张网卡，容器外有张网卡，容器外的网卡连到 docker0 网桥，通过这个网桥，容器直接实现相互访问。

如果你用 brctl 查看 docker0 网桥，你会发现它上面连着一些网卡。其实这个网桥和第 24 讲，咱们自己用 brctl 创建的网桥没什么两样。

那连接容器和网桥的那个网卡和虚拟机一样吗？在虚拟机场景下，有一个虚拟化软件，通过 TUN/TAP 设备虚拟一个网卡给虚拟机，但是容器场景下并没有虚拟化软件，这该怎么办呢？

在 Linux 下，可以创建一对 veth pair 的网卡，从一边发送包，另一边就能收到。

我们首先通过这个命令创建这么一对。

ip link add name veth1 mtu 1500 type veth peer name veth2 mtu 1500

其中一边可以打到 docker0 网桥上。

ip link set veth1 master testbr

ip link set veth1 up

那另一端如何放到容器里呢？

一个容器的启动会对应一个 namespace，我们要先找到这个 namespace。对于 docker 来讲，pid 就是 namespace 的名字，可以通过这个命令获取。

docker inspect '--format={{ .State.Pid }}' test

假设结果为 12065，这个就是 namespace 名字。

默认 Docker 创建的网络 namespace 不在默认路径下 ，ip netns 看不到，所以需要 ln 软链接一下。链接完毕以后，我们就可以通过 ip netns 命令操作了。

rm -f /var/run/netns/12065

ln -s /proc/12065/ns/net /var/run/netns/12065

然后，我们就可以将另一端 veth2 塞到 namespace 里面。

ip link set veth2 netns 12065

然后，将容器内的网卡重命名。

ip netns exec 12065 ip link set veth2 name eth0

然后，给容器内网卡设置 ip 地址。

ip netns exec 12065 ip addr add 172.17.0.2/24 dev eth0

ip netns exec 12065 ip link set eth0 up

一台机器内部容器的互相访问没有问题了，那如何访问外网呢？

你先想想看有没有思路？对，就是虚拟机里面的桥接模式和 NAT 模式。Docker 默认使用 NAT 模式。NAT 模式分为 SNAT 和 DNAT，如果是容器内部访问外部，就需要通过 SNAT。

从容器内部的客户端访问外部网络中的服务器，我画了一张图。在虚拟机那一节，也有一张类似的图。

在宿主机上，有这么一条 iptables 规则：

-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

所有从容器内部发出来的包，都要做地址伪装，将源 IP 地址，转换为物理网卡的 IP 地址。如果有多个容器，所有的容器共享一个外网的 IP 地址，但是在 conntrack 表中，记录下这个出去的连接。

当服务器返回结果的时候，到达物理机，会根据 conntrack 表中的规则，取出原来的私网 IP，通过 DNAT 将地址转换为私网 IP 地址，通过网桥 docker0 实现对内的访问。

如果在容器内部属于一个服务，例如部署一个网站，提供给外部进行访问，需要通过 Docker 的端口映射技术，将容器内部的端口映射到物理机上来。

例如容器内部监听 80 端口，可以通 Docker run 命令中的参数 -p 10080:80，将物理机上的 10080 端口和容器的 80 端口映射起来， 当外部的客户端访问这个网站的时候，通过访问物理机的 10080 端口，就能访问到容器内的 80 端口了。

Docker 有两种方式，一种是通过一个进程docker-proxy的方式，监听 10080，转换为 80 端口。

/usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 10080 -container-ip 172.17.0.2 -container-port 80

另外一种方式是通过DNAT方式，在 -A PREROUTING 阶段加一个规则，将到端口 10080 的 DNAT 称为容器的私有网络。

-A DOCKER -p tcp -m tcp --dport 10080 -j DNAT --to-destination 172.17.0.2:80

如此就可以实现容器和物理网络之间的互通了。

小结

好了，这一节就到这里了，我们来总结一下。

容器是一种比虚拟机更加轻量级的隔离方式，主要通过 namespace 和 cgroup 技术进行资源的隔离，namespace 用于负责看起来隔离，cgroup 用于负责用起来隔离。

容器网络连接到物理网络的方式和虚拟机很像，通过桥接的方式实现一台物理机上的容器进行相互访问，如果要访问外网，最简单的方式还是通过 NAT。

最后，给你留两个思考题：

容器内的网络和物理机网络可以使用 NAT 的方式相互访问，如果这种方式用于部署应用，有什么问题呢？

和虚拟机一样，不同物理机上的容器需要相互通信，你知道容器是怎么做到这一点吗？

我们的专栏更新到第 29 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第30讲 | 容器网络之Flannel：每人一亩三分地

上一节我们讲了容器网络的模型，以及如何通过 NAT 的方式与物理网络进行互通。

每一台物理机上面安装好了 Docker 以后，都会默认分配一个 172.17.0.0/16 的网段。一台机器上新创建的第一个容器，一般都会给 172.17.0.2 这个地址，当然一台机器这样玩玩倒也没啥问题。但是容器里面是要部署应用的，就像上一节讲过的一样，它既然是集装箱，里面就需要装载货物。

如果这个应用是比较传统的单体应用，自己就一个进程，所有的代码逻辑都在这个进程里面，上面的模式没有任何问题，只要通过 NAT 就能访问进来。

但是因为无法解决快速迭代和高并发的问题，单体应用越来越跟不上时代发展的需要了。

你可以回想一下，无论是各种网络直播平台，还是共享单车，是不是都是很短时间内就要积累大量用户，否则就会错过风口。所以应用需要在很短的时间内快速迭代，不断调整，满足用户体验；还要在很短的时间内，具有支撑高并发请求的能力。

单体应用作为个人英雄主义的时代已经过去了。如果所有的代码都在一个工程里面，开发的时候必然存在大量冲突，上线的时候，需要开大会进行协调，一个月上线一次就很不错了。而且所有的流量都让一个进程扛，怎么也扛不住啊！

没办法，一个字：拆！拆开了，每个子模块独自变化，减少相互影响。拆开了，原来一个进程扛流量，现在多个进程一起扛。所以，微服务就是从个人英雄主义，变成集团军作战。

容器作为集装箱，可以保证应用在不同的环境中快速迁移，提高迭代的效率。但是如果要形成容器集团军，还需要一个集团军作战的调度平台，这就是 Kubernetes。它可以灵活地将一个容器调度到任何一台机器上，并且当某个应用扛不住的时候，只要在 Kubernetes 上修改容器的副本数，一个应用马上就能变八个，而且都能提供服务。

然而集团军作战有个重要的问题，就是通信。这里面包含两个问题，第一个是集团军的 A 部队如何实时地知道 B 部队的位置变化，第二个是两个部队之间如何相互通信。

第一个问题位置变化，往往是通过一个称为注册中心的地方统一管理的，这个是应用自己做的。当一个应用启动的时候，将自己所在环境的 IP 地址和端口，注册到注册中心指挥部，这样其他的应用请求它的时候，到指挥部问一下它在哪里就好了。当某个应用发生了变化，例如一台机器挂了，容器要迁移到另一台机器，这个时候 IP 改变了，应用会重新注册，则其他的应用请求它的时候，还是能够从指挥部得到最新的位置。

接下来是如何相互通信的问题。NAT 这种模式，在多个主机的场景下，是存在很大问题的。在物理机 A 上的应用 A 看到的 IP 地址是容器 A 的，是 172.17.0.2，在物理机 B 上的应用 B 看到的 IP 地址是容器 B 的，不巧也是 172.17.0.2，当它们都注册到注册中心的时候，注册中心就是这个图里这样子。

这个时候，应用 A 要访问应用 B，当应用 A 从注册中心将应用 B 的 IP 地址读出来的时候，就彻底困惑了，这不是自己访问自己吗？

怎么解决这个问题呢？一种办法是不去注册容器内的 IP 地址，而是注册所在物理机的 IP 地址，端口也要是物理机上映射的端口。

这样存在的问题是，应用是在容器里面的，它怎么知道物理机上的 IP 地址和端口呢？这明明是运维人员配置的，除非应用配合，读取容器平台的接口获得这个 IP 和端口。一方面，大部分分布式框架都是容器诞生之前就有了，它们不会适配这种场景；另一方面，让容器内的应用意识到容器外的环境，本来就是非常不好的设计。

说好的集装箱，说好的随意迁移呢？难道要让集装箱内的货物意识到自己传的信息？而且本来 Tomcat 都是监听 8080 端口的，结果到了物理机上，就不能大家都用这个端口了，否则端口就冲突了，因而就需要随机分配端口，于是在注册中心就出现了各种各样奇怪的端口。无论是注册中心，还是调用方都会觉得很奇怪，而且不是默认的端口，很多情况下也容易出错。

Kubernetes 作为集团军作战管理平台，提出指导意见，说网络模型要变平，但是没说怎么实现。于是业界就涌现了大量的方案，Flannel 就是其中之一。

对于 IP 冲突的问题，如果每一个物理机都是网段 172.17.0.0/16，肯定会冲突啊，但是这个网段实在太大了，一台物理机上根本启动不了这么多的容器，所以能不能每台物理机在这个大网段里面，抠出一个小的网段，每个物理机网段都不同，自己看好自己的一亩三分地，谁也不和谁冲突。

例如物理机 A 是网段 172.17.8.0/24，物理机 B 是网段 172.17.9.0/24，这样两台机器上启动的容器 IP 肯定不一样，而且就看 IP 地址，我们就一下子识别出，这个容器是本机的，还是远程的，如果是远程的，也能从网段一下子就识别出它归哪台物理机管，太方便了。

接下来的问题，就是物理机 A 上的容器如何访问到物理机 B 上的容器呢？

你是不是想到了熟悉的场景？虚拟机也需要跨物理机互通，往往通过 Overlay 的方式，容器是不是也可以这样做呢？

这里我要说 Flannel 使用 UDP 实现 Overlay 网络的方案。

在物理机 A 上的容器 A 里面，能看到的容器的 IP 地址是 172.17.8.2/24，里面设置了默认的路由规则 default via 172.17.8.1 dev eth0。

如果容器 A 要访问 172.17.9.2，就会发往这个默认的网关 172.17.8.1。172.17.8.1 就是物理机上面 docker0 网桥的 IP 地址，这台物理机上的所有容器都是连接到这个网桥的。

在物理机上面，查看路由策略，会有这样一条 172.17.0.0/24 via 172.17.0.0 dev flannel.1，也就是说发往 172.17.9.2 的网络包会被转发到 flannel.1 这个网卡。

这个网卡是怎么出来的呢？在每台物理机上，都会跑一个 flanneld 进程，这个进程打开一个 /dev/net/tun 字符设备的时候，就出现了这个网卡。

你有没有想起 qemu-kvm，打开这个字符设备的时候，物理机上也会出现一个网卡，所有发到这个网卡上的网络包会被 qemu-kvm 接收进来，变成二进制串。只不过接下来 qemu-kvm 会模拟一个虚拟机里面的网卡，将二进制的串变成网络包，发给虚拟机里面的网卡。但是 flanneld 不用这样做，所有发到 flannel.1 这个网卡的包都会被 flanneld 进程读进去，接下来 flanneld 要对网络包进行处理。

物理机 A 上的 flanneld 会将网络包封装在 UDP 包里面，然后外层加上物理机 A 和物理机 B 的 IP 地址，发送给物理机 B 上的 flanneld。

为什么是 UDP 呢？因为不想在 flanneld 之间建立两两连接，而 UDP 没有连接的概念，任何一台机器都能发给另一台。

物理机 B 上的 flanneld 收到包之后，解开 UDP 的包，将里面的网络包拿出来，从物理机 B 的 flannel.1 网卡发出去。

在物理机 B 上，有路由规则 172.17.9.0/24 dev docker0 proto kernel scope link src 172.17.9.1。

将包发给 docker0，docker0 将包转给容器 B。通信成功。

上面的过程连通性没有问题，但是由于全部在用户态，所以性能差了一些。

跨物理机的连通性问题，在虚拟机那里有成熟的方案，就是 VXLAN，那能不能 Flannel 也用 VXLAN 呢？

当然可以了。如果使用 VXLAN，就不需要打开一个 TUN 设备了，而是要建立一个 VXLAN 的 VTEP。如何建立呢？可以通过 netlink 通知内核建立一个 VTEP 的网卡 flannel.1。在我们讲 OpenvSwitch 的时候提过，netlink 是一种用户态和内核态通信的机制。

当网络包从物理机 A 上的容器 A 发送给物理机 B 上的容器 B，在容器 A 里面通过默认路由到达物理机 A 上的 docker0 网卡，然后根据路由规则，在物理机 A 上，将包转发给 flannel.1。这个时候 flannel.1 就是一个 VXLAN 的 VTEP 了，它将网络包进行封装。

内部的 MAC 地址这样写：源为物理机 A 的 flannel.1 的 MAC 地址，目标为物理机 B 的 flannel.1 的 MAC 地址，在外面加上 VXLAN 的头。

外层的 IP 地址这样写：源为物理机 A 的 IP 地址，目标为物理机 B 的 IP 地址，外面加上物理机的 MAC 地址。

这样就能通过 VXLAN 将包转发到另一台机器，从物理机 B 的 flannel.1 上解包，变成内部的网络包，通过物理机 B 上的路由转发到 docker0，然后转发到容器 B 里面。通信成功。

小结

好了，今天的内容就到这里，我来总结一下。

基于 NAT 的容器网络模型在微服务架构下有两个问题，一个是 IP 重叠，一个是端口冲突，需要通过 Overlay 网络的机制保持跨节点的连通性。

Flannel 是跨节点容器网络方案之一，它提供的 Overlay 方案主要有两种方式，一种是 UDP 在用户态封装，一种是 VXLAN 在内核态封装，而 VXLAN 的性能更好一些。

最后，给你留两个问题：

通过 Flannel 的网络模型可以实现容器与容器直接跨主机的互相访问，那你知道如果容器内部访问外部的服务应该怎么融合到这个网络模型中吗？

基于 Overlay 的网络毕竟做了一次网络虚拟化，有没有更加高性能的方案呢？

我们的专栏更新到第 30 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第31讲 | 容器网络之Calico：为高效说出善意的谎言

上一节我们讲了 Flannel 如何解决容器跨主机互通的问题，这个解决方式其实和虚拟机的网络互通模式是差不多的，都是通过隧道。但是 Flannel 有一个非常好的模式，就是给不同的物理机设置不同网段，这一点和虚拟机的 Overlay 的模式完全不一样。

在虚拟机的场景下，整个网段在所有的物理机之间都是可以“飘来飘去”的。网段不同，就给了我们做路由策略的可能。

Calico 网络模型的设计思路

我们看图中的两台物理机。它们的物理网卡是同一个二层网络里面的。由于两台物理机的容器网段不同，我们完全可以将两台物理机配置成为路由器，并按照容器的网段配置路由表。

例如，在物理机 A 中，我们可以这样配置：要想访问网段 172.17.9.0/24，下一跳是 192.168.100.101，也即到物理机 B 上去。

这样在容器 A 中访问容器 B，当包到达物理机 A 的时候，就能够匹配到这条路由规则，并将包发给下一跳的路由器，也即发给物理机 B。在物理机 B 上也有路由规则，要访问 172.17.9.0/24，从 docker0 的网卡进去即可。

当容器 B 返回结果的时候，在物理机 B 上，可以做类似的配置：要想访问网段 172.17.8.0/24，下一跳是 192.168.100.100，也即到物理机 A 上去。

当包到达物理机 B 的时候，能够匹配到这条路由规则，将包发给下一跳的路由器，也即发给物理机 A。在物理机 A 上也有路由规则，要访问 172.17.8.0/24，从 docker0 的网卡进去即可。

这就是Calico 网络的大概思路，即不走 Overlay 网络，不引入另外的网络性能损耗，而是将转发全部用三层网络的路由转发来实现，只不过具体的实现和上面的过程稍有区别。

首先，如果全部走三层的路由规则，没必要每台机器都用一个 docker0，从而浪费了一个 IP 地址，而是可以直接用路由转发到 veth pair 在物理机这一端的网卡。同样，在容器内，路由规则也可以这样设定：把容器外面的 veth pair 网卡算作默认网关，下一跳就是外面的物理机。

于是，整个拓扑结构就变成了这个图中的样子。

Calico 网络的转发细节

我们来看其中的一些细节。

容器 A1 的 IP 地址为 172.17.8.2/32，这里注意，不是 /24，而是 /32，将容器 A1 作为一个单点的局域网了。

容器 A1 里面的默认路由，Calico 配置得比较有技巧。

default via 169.254.1.1 dev eth0

169.254.1.1 dev eth0 scope link

这个 IP 地址 169.254.1.1 是默认的网关，但是整个拓扑图中没有一张网卡是这个地址。那如何到达这个地址呢？

前面我们讲网关的原理的时候说过，当一台机器要访问网关的时候，首先会通过 ARP 获得网关的 MAC 地址，然后将目标 MAC 变为网关的 MAC，而网关的 IP 地址不会在任何网络包头里面出现，也就是说，没有人在乎这个地址具体是什么，只要能找到对应的 MAC，响应 ARP 就可以了。

ARP 本地有缓存，通过 ip neigh 命令可以查看。

169.254.1.1 dev eth0 lladdr ee:ee:ee:ee:ee:ee STALE

这个 MAC 地址是 Calico 硬塞进去的，但是没有关系，它能响应 ARP，于是发出的包的目标 MAC 就是这个 MAC 地址。

在物理机 A 上查看所有网卡的 MAC 地址的时候，我们会发现 veth1 就是这个 MAC 地址。所以容器 A1 里发出的网络包，第一跳就是这个 veth1 这个网卡，也就到达了物理机 A 这个路由器。

在物理机 A 上有三条路由规则，分别是去两个本机的容器的路由，以及去 172.17.9.0/24，下一跳为物理机 B。

172.17.8.2 dev veth1 scope link

172.17.8.3 dev veth2 scope link

172.17.9.0/24 via 192.168.100.101 dev eth0 proto bird onlink

同理，物理机 B 上也有三条路由规则，分别是去两个本机的容器的路由，以及去 172.17.8.0/24，下一跳为物理机 A。

172.17.9.2 dev veth1 scope link

172.17.9.3 dev veth2 scope link

172.17.8.0/24 via 192.168.100.100 dev eth0 proto bird onlink

如果你觉得这些规则过于复杂，我将刚才的拓扑图转换为这个更加容易理解的图。

在这里，物理机化身为路由器，通过路由器上的路由规则，将包转发到目的地。在这个过程中，没有隧道封装解封装，仅仅是单纯的路由转发，性能会好很多。但是，这种模式也有很多问题。

Calico 的架构

路由配置组件 Felix

如果只有两台机器，每台机器只有两个容器，而且保持不变。我手动配置一下，倒也没啥问题。但是如果容器不断地创建、删除，节点不断地加入、退出，情况就会变得非常复杂。

就像图中，有三台物理机，两两之间都需要配置路由，每台物理机上对外的路由就有两条。如果有六台物理机，则每台物理机上对外的路由就有五条。新加入一个节点，需要通知每一台物理机添加一条路由。

这还是在物理机之间，一台物理机上，每创建一个容器，也需要多配置一条指向这个容器的路由。如此复杂，肯定不能手动配置，需要每台物理机上有一个 agent，当创建和删除容器的时候，自动做这件事情。这个 agent 在 Calico 中称为 Felix。

路由广播组件 BGP Speaker

当 Felix 配置了路由之后，接下来的问题就是，如何将路由信息，也即将“如何到达我这个节点，访问我这个节点上的容器”这些信息，广播出去。

能想起来吗？这其实就是路由协议啊！路由协议就是将“我能到哪里，如何能到我”的信息广播给全网传出去，从而客户端可以一跳一跳地访问目标地址的。路由协议有很多种，Calico 使用的是 BGP 协议。

在 Calico 中，每个 Node 上运行一个软件 BIRD，作为 BGP 的客户端，或者叫作 BGP Speaker，将“如何到达我这个 Node，访问我这个 Node 上的容器”的路由信息广播出去。所有 Node 上的 BGP Speaker 都互相建立连接，就形成了全互连的情况，这样每当路由有所变化的时候，所有节点就都能够收到了。

安全策略组件

Calico 中还实现了灵活配置网络策略 Network Policy，可以灵活配置两个容器通或者不通。这个怎么实现呢？

虚拟机中的安全组，是用 iptables 实现的。Calico 中也是用 iptables 实现的。这个图里的内容是 iptables 在内核处理网络包的过程中可以嵌入的处理点。Calico 也是在这些点上设置相应的规则。

当网络包进入物理机上的时候，进入 PREOUTING 规则，这里面有一个规则是 cali-fip-dnat，这是实现浮动 IP（Floating IP）的场景，主要将外网的 IP 地址 dnat 为容器内的 IP 地址。在虚拟机场景下，路由器的网络 namespace 里面有一个外网网卡上，也设置过这样一个 DNAT 规则。

接下来可以根据路由判断，是到本地的，还是要转发出去的。

如果是本地的，走 INPUT 规则，里面有个规则是 cali-wl-to-host，wl 的意思是 workload，也即容器，也即这是用来判断从容器发到物理机的网络包是否符合规则的。这里面内嵌一个规则 cali-from-wl-dispatch，也是匹配从容器来的包。如果有两个容器，则会有两个容器网卡，这里面内嵌有详细的规则“cali-fw-cali 网卡 1”和“cali-fw-cali 网卡 2”，fw 就是 from workload，也就是匹配从容器 1 来的网络包和从容器 2 来的网络包。

如果是转发出去的，走 FORWARD 规则，里面有个规则 cali-FORWARD。这里面分两种情况，一种是从容器里面发出来，转发到外面的；另一种是从外面发进来，转发到容器里面的。

第一种情况匹配的规则仍然是 cali-from-wl-dispatch，也即 from workload。第二种情况匹配的规则是 cali-to-wl-dispatch，也即 to workload。如果有两个容器，则会有两个容器网卡，在这里面内嵌有详细的规则“cali-tw-cali 网卡 1”和“cali-tw-cali 网卡 2”，tw 就是 to workload，也就是匹配发往容器 1 的网络包和发送到容器 2 的网络包。

接下来是匹配 OUTPUT 规则，里面有 cali-OUTPUT。接下来是 POSTROUTING 规则，里面有一个规则是 cali-fip-snat，也即发出去的时候，将容器网络 IP 转换为浮动 IP 地址。在虚拟机场景下，路由器的网络 namespace 里面有一个外网网卡上，也设置过这样一个 SNAT 规则。

至此为止，Calico 的所有组件基本凑齐。来看看我汇总的图。

全连接复杂性与规模问题

这里面还存在问题，就是 BGP 全连接的复杂性问题。

你看刚才的例子里只有六个节点，BGP 的互连已经如此复杂，如果节点数据再多，这种全互连的模式肯定不行，到时候都成蜘蛛网了。于是多出了一个组件 BGP Route Reflector，它也是用 BIRD 实现的。有了它，BGP Speaker 就不用全互连了，而是都直连它，它负责将全网的路由信息广播出去。

可是问题来了，规模大了，大家都连它，它受得了吗？这个 BGP Router Reflector 会不会成为瓶颈呢？

所以，肯定不能让一个 BGP Router Reflector 管理所有的路由分发，而是应该有多个 BGP Router Reflector，每个 BGP Router Reflector 管一部分。

多大算一部分呢？咱们讲述数据中心的时候，说服务器都是放在机架上的，每个机架上最顶端有个 TOR 交换机。那将机架上的机器连在一起，这样一个机架是不是可以作为一个单元，让一个 BGP Router Reflector 来管理呢？如果要跨机架，如何进行通信呢？这就需要 BGP Router Reflector 也直接进行路由交换。它们之间的交换和一个机架之间的交换有什么关系吗？

有没有觉得在这个场景下，一个机架就像一个数据中心，可以把它设置为一个 AS，而 BGP Router Reflector 有点儿像数据中心的边界路由器。在一个 AS 内部，也即服务器和 BGP Router Reflector 之间使用的是数据中心内部的路由协议 iBGP，BGP Router Reflector 之间使用的是数据中心之间的路由协议 eBGP。

这个图中，一个机架上有多台机器，每台机器上面启动多个容器，每台机器上都有可以到达这些容器的路由。每台机器上都启动一个 BGP Speaker，然后将这些路由规则上报到这个 Rack 上接入交换机的 BGP Route Reflector，将这些路由通过 iBGP 协议告知到接入交换机的三层路由功能。

在接入交换机之间也建立 BGP 连接，相互告知路由，因而一个 Rack 里面的路由可以告知另一个 Rack。有多个核心或者汇聚交换机将接入交换机连接起来，如果核心和汇聚起二层互通的作用，则接入和接入之间之间交换路由即可。如果核心和汇聚交换机起三层路由的作用，则路由需要通过核心或者汇聚交换机进行告知。

跨网段访问问题

上面的 Calico 模式还有一个问题，就是跨网段问题，这里的跨网段是指物理机跨网段。

前面我们说的那些逻辑成立的条件，是我们假设物理机可以作为路由器进行使用。例如物理机 A 要告诉物理机 B，你要访问 172.17.8.0/24，下一跳是我 192.168.100.100；同理，物理机 B 要告诉物理机 A，你要访问 172.17.9.0/24，下一跳是我 192.168.100.101。

之所以能够这样，是因为物理机 A 和物理机 B 是同一个网段的，是连接在同一个交换机上的。那如果物理机 A 和物理机 B 不是在同一个网段呢？

例如，物理机 A 的网段是 192.168.100.100/24，物理机 B 的网段是 192.168.200.101/24，这样两台机器就不能通过二层交换机连接起来了，需要在中间放一台路由器，做一次路由转发，才能跨网段访问。

本来物理机 A 要告诉物理机 B，你要访问 172.17.8.0/24，下一跳是我 192.168.100.100 的，但是中间多了一台路由器，下一跳不是我了，而是中间的这台路由器了，这台路由器的再下一跳，才是我。这样之前的逻辑就不成立了。

我们看刚才那张图的下半部分。物理机 B 上的容器要访问物理机 A 上的容器，第一跳就是物理机 B，IP 为 192.168.200.101，第二跳是中间的物理路由器右面的网口，IP 为 192.168.200.1，第三跳才是物理机 A，IP 为 192.168.100.100。

这是咱们通过拓扑图看到的，关键问题是，在系统中物理机 A 如何告诉物理机 B，怎么让它才能到我这里？物理机 A 根本不可能知道从物理机 B 出来之后的下一跳是谁，况且现在只是中间隔着一个路由器这种简单的情况，如果隔着多个路由器呢？谁能把这一串的路径告诉物理机 B 呢？

我们能想到的第一种方式是，让中间所有的路由器都来适配 Calico。本来它们互相告知路由，只互相告知物理机的，现在还要告知容器的网段。这在大部分情况下，是不可能的。

第二种方式，还是在物理机 A 和物理机 B 之间打一个隧道，这个隧道有两个端点，在端点上进行封装，将容器的 IP 作为乘客协议放在隧道里面，而物理主机的 IP 放在外面作为承载协议。这样不管外层的 IP 通过传统的物理网络，走多少跳到达目标物理机，从隧道两端看起来，物理机 A 的下一跳就是物理机 B，这样前面的逻辑才能成立。

这就是 Calico 的IPIP 模式。使用了 IPIP 模式之后，在物理机 A 上，我们能看到这样的路由表：

172.17.8.2 dev veth1 scope link

172.17.8.3 dev veth2 scope link

172.17.9.0/24 via 192.168.200.101 dev tun0 proto bird onlink

这和原来模式的区别在于，下一跳不再是同一个网段的物理机 B 了，IP 为 192.168.200.101，并且不是从 eth0 跳，而是建立一个隧道的端点 tun0，从这里才是下一跳。

如果我们在容器 A1 里面的 172.17.8.2，去 ping 容器 B1 里面的 172.17.9.2，首先会到物理机 A。在物理机 A 上根据上面的规则，会转发给 tun0，并在这里对包做封装：

内层源 IP 为 172.17.8.2；

内层目标 IP 为 172.17.9.2；

外层源 IP 为 192.168.100.100；

外层目标 IP 为 192.168.200.101。

将这个包从 eth0 发出去，在物理网络上会使用外层的 IP 进行路由，最终到达物理机 B。在物理机 B 上，tun0 会解封装，将内层的源 IP 和目标 IP 拿出来，转发给相应的容器。

小结

好了，这一节就到这里，我们来总结一下。

Calico 推荐使用物理机作为路由器的模式，这种模式没有虚拟化开销，性能比较高。

Calico 的主要组件包括路由、iptables 的配置组件 Felix、路由广播组件 BGP Speaker，以及大规模场景下的 BGP Route Reflector。

为解决跨网段的问题，Calico 还有一种 IPIP 模式，也即通过打隧道的方式，从隧道端点来看，将本来不是邻居的两台机器，变成相邻的机器。

最后，给你留两个思考题：

将 Calico 部署在公有云上的时候，经常会选择使用 IPIP 模式，你知道这是为什么吗？

容器是用来部署微服务的，微服务之间的通信，除了网络要互通，还需要高效的传输信息，例如下单的商品、价格、数量、支付的钱等等，这些要通过什么样的协议呢？

我们的专栏更新到第 31 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第32讲 | RPC协议综述：远在天边，近在眼前

前面我们讲了容器网络如何实现跨主机互通，以及微服务之间的相互调用。

网络是打通了，那服务之间的互相调用，该怎么实现呢？你可能说，咱不是学过 Socket。服务之间分调用方和被调用方，我们就建立一个 TCP 或者 UDP 的连接，不就可以通信了？

你仔细想一下，这事儿没这么简单。我们就拿最简单的场景，客户端调用一个加法函数，将两个整数加起来，返回它们的和。

如果放在本地调用，那是简单的不能再简单了，只要稍微学过一种编程语言，三下五除二就搞定了。但是一旦变成了远程调用，门槛一下子就上去了。

首先你要会 Socket 编程，至少先要把咱们这门网络协议课学一下，然后再看 N 本砖头厚的 Socket 程序设计的书，学会咱们学过的几种 Socket 程序设计的模型。这就使得本来大学毕业就能干的一项工作，变成了一件五年工作经验都不一定干好的工作，而且，搞定了 Socket 程序设计，才是万里长征的第一步。后面还有很多问题呢！

如何解决这五个问题？

问题一：如何规定远程调用的语法？

客户端如何告诉服务端，我是一个加法，而另一个是乘法。我是用字符串“add”传给你，还是传给你一个整数，比如 1 表示加法，2 表示乘法？服务端该如何告诉客户端，我的这个加法，目前只能加整数，不能加小数，不能加字符串；而另一个加法“add1”，它能实现小数和整数的混合加法。那返回值是什么？正确的时候返回什么，错误的时候又返回什么？

问题二：如果传递参数？

我是先传两个整数，后传一个操作符“add”，还是先传操作符，再传两个整数？是不是像咱们数据结构里一样，如果都是 UDP，想要实现一个逆波兰表达式，放在一个报文里面还好，如果是 TCP，是一个流，在这个流里面，如何将两次调用进行分界？什么时候是头，什么时候是尾？别这次的参数和上次的参数混了起来，TCP 一端发送出去的数据，另外一端不一定能一下子全部读取出来。所以，怎么才算读完呢？

问题三：如何表示数据？

在这个简单的例子中，传递的就是一个固定长度的 int 值，这种情况还好，如果是变长的类型，是一个结构体，甚至是一个类，应该怎么办呢？如果是 int，不同的平台上长度也不同，该怎么办呢？

在网络上传输超过一个 Byte 的类型，还有大端 Big Endian 和小端 Little Endian 的问题。

假设我们要在 32 位四个 Byte 的一个空间存放整数 1，很显然只要一个 Byte 放 1，其他三个 Byte 放 0 就可以了。那问题是，最后一个 Byte 放 1 呢，还是第一个 Byte 放 1 呢？或者说 1 作为最低位，应该是放在 32 位的最后一个位置呢，还是放在第一个位置呢？

最低位放在最后一个位置，叫作 Little Endian，最低位放在第一个位置，叫作 Big Endian。TCP/IP 协议栈是按照 Big Endian 来设计的，而 X86 机器多按照 Little Endian 来设计的，因而发出去的时候需要做一个转换。

问题四：如何知道一个服务端都实现了哪些远程调用？从哪个端口可以访问这个远程调用？

假设服务端实现了多个远程调用，每个可能实现在不同的进程中，监听的端口也不一样，而且由于服务端都是自己实现的，不可能使用一个大家都公认的端口，而且有可能多个进程部署在一台机器上，大家需要抢占端口，为了防止冲突，往往使用随机端口，那客户端如何找到这些监听的端口呢？

问题五：发生了错误、重传、丢包、性能等问题怎么办？

本地调用没有这个问题，但是一旦到网络上，这些问题都需要处理，因为网络是不可靠的，虽然在同一个连接中，我们还可通过 TCP 协议保证丢包、重传的问题，但是如果服务器崩溃了又重启，当前连接断开了，TCP 就保证不了了，需要应用自己进行重新调用，重新传输会不会同样的操作做两遍，远程调用性能会不会受影响呢？

协议约定问题

看到这么多问题，你是不是想起了我第一节讲过的这张图。

本地调用函数里有很多问题，比如词法分析、语法分析、语义分析等等，这些编译器本来都能帮你做了。但是在远程调用中，这些问题你都需要重新操心。

很多公司的解决方法是，弄一个核心通信组，里面都是 Socket 编程的大牛，实现一个统一的库，让其他业务组的人来调用，业务的人不需要知道中间传输的细节。通信双方的语法、语义、格式、端口、错误处理等，都需要调用方和被调用方开会商量，双方达成一致。一旦有一方改变，要及时通知对方，否则通信就会有问题。

可是不是每一个公司都有这种大牛团队，往往只有大公司才配得起，那有没有已经实现好的框架可以使用呢？

当然有。一个大牛 Bruce Jay Nelson 写了一篇论文Implementing Remote Procedure Calls，定义了 RPC 的调用标准。后面所有 RPC 框架，都是按照这个标准模式来的。

当客户端的应用想发起一个远程调用时，它实际是通过本地调用本地调用方的 Stub。它负责将调用的接口、方法和参数，通过约定的协议规范进行编码，并通过本地的 RPCRuntime 进行传输，将调用网络包发送到服务器。

服务器端的 RPCRuntime 收到请求后，交给提供方 Stub 进行解码，然后调用服务端的方法，服务端执行方法，返回结果，提供方 Stub 将返回结果编码后，发送给客户端，客户端的 RPCRuntime 收到结果，发给调用方 Stub 解码得到结果，返回给客户端。

这里面分了三个层次，对于用户层和服务端，都像是本地调用一样，专注于业务逻辑的处理就可以了。对于 Stub 层，处理双方约定好的语法、语义、封装、解封装。对于 RPCRuntime，主要处理高性能的传输，以及网络的错误和异常。

最早的 RPC 的一种实现方式称为 Sun RPC 或 ONC RPC。Sun 公司是第一个提供商业化 RPC 库和 RPC 编译器的公司。这个 RPC 框架是在 NFS 协议中使用的。

NFS（Network File System）就是网络文件系统。要使 NFS 成功运行，要启动两个服务端，一个是 mountd，用来挂载文件路径；一个是 nfsd，用来读写文件。NFS 可以在本地 mount 一个远程的目录到本地的一个目录，从而本地的用户在这个目录里面写入、读出任何文件的时候，其实操作的是远程另一台机器上的文件。

操作远程和远程调用的思路是一样的，就像操作本地一样。所以 NFS 协议就是基于 RPC 实现的。当然无论是什么 RPC，底层都是 Socket 编程。

XDR（External Data Representation，外部数据表示法）是一个标准的数据压缩格式，可以表示基本的数据类型，也可以表示结构体。

这里是几种基本的数据类型。

在 RPC 的调用过程中，所有的数据类型都要封装成类似的格式。而且 RPC 的调用和结果返回，也有严格的格式。

XID 唯一标识一对请求和回复。请求为 0，回复为 1。

RPC 有版本号，两端要匹配 RPC 协议的版本号。如果不匹配，就会返回 Deny，原因就是 RPC_MISMATCH。

程序有编号。如果服务端找不到这个程序，就会返回 PROG_UNAVAIL。

程序有版本号。如果程序的版本号不匹配，就会返回 PROG_MISMATCH。

一个程序可以有多个方法，方法也有编号，如果找不到方法，就会返回 PROC_UNAVAIL。

调用需要认证鉴权，如果不通过，则 Deny。

最后是参数列表，如果参数无法解析，则返回 GABAGE_ARGS。

为了可以成功调用 RPC，在客户端和服务端实现 RPC 的时候，首先要定义一个双方都认可的程序、版本、方法、参数等。

如果还是上面的加法，则双方约定为一个协议定义文件，同理如果是 NFS、mount 和读写，也会有类似的定义。

有了协议定义文件，ONC RPC 会提供一个工具，根据这个文件生成客户端和服务器端的 Stub 程序。

最下层的是 XDR 文件，用于编码和解码参数。这个文件是客户端和服务端共享的，因为只有双方一致才能成功通信。

在客户端，会调用 clnt_create 创建一个连接，然后调用 add_1，这是一个 Stub 函数，感觉是在调用本地一样。其实是这个函数发起了一个 RPC 调用，通过调用 clnt_call 来调用 ONC RPC 的类库，来真正发送请求。调用的过程非常复杂，一会儿我详细说这个。

当然服务端也有一个 Stub 程序，监听客户端的请求，当调用到达的时候，判断如果是 add，则调用真正的服务端逻辑，也即将两个数加起来。

服务端将结果返回服务端的 Stub，这个 Stub 程序发送结果给客户端，客户端的 Stub 程序正在等待结果，当结果到达客户端 Stub，就将结果返回给客户端的应用程序，从而完成整个调用过程。

有了这个 RPC 的框架，前面五个问题中的前三个“如何规定远程调用的语法？”“如何传递参数？”以及“如何表示数据？”基本解决了，这三个问题我们统称为协议约定问题。

传输问题

但是错误、重传、丢包、性能等问题还没有解决，这些问题我们统称为传输问题。这个就不用 Stub 操心了，而是由 ONC RPC 的类库来实现。这是大牛们实现的，我们只要调用就可以了。

在这个类库中，为了解决传输问题，对于每一个客户端，都会创建一个传输管理层，而每一次 RPC 调用，都会是一个任务，在传输管理层，你可以看到熟悉的队列机制、拥塞窗口机制等。

由于在网络传输的时候，经常需要等待，因而同步的方式往往效率比较低，因而也就有 Socket 的异步模型。为了能够异步处理，对于远程调用的处理，往往是通过状态机来实现的。只有当满足某个状态的时候，才进行下一步，如果不满足状态，不是在那里等，而是将资源留出来，用来处理其他的 RPC 调用。

从这个图可以看出，这个状态转换图还是很复杂的。

首先，进入起始状态，查看 RPC 的传输层队列中有没有空闲的位置，可以处理新的 RPC 任务。如果没有，说明太忙了，或直接结束或重试。如果申请成功，就可以分配内存，获取服务的端口号，然后连接服务器。

连接的过程要有一段时间，因而要等待连接的结果，会有连接失败，或直接结束或重试。如果连接成功，则开始发送 RPC 请求，然后等待获取 RPC 结果，这个过程也需要一定的时间；如果发送出错，可以重新发送；如果连接断了，可以重新连接；如果超时，可以重新传输；如果获取到结果，就可以解码，正常结束。

这里处理了连接失败、重试、发送失败、超时、重试等场景。不是大牛真写不出来，因而实现一个 RPC 的框架，其实很有难度。

服务发现问题

传输问题解决了，我们还遗留一个问题，就是问题四“如何找到 RPC 服务端的那个随机端口”。这个问题我们称为服务发现问题。在 ONC RPC 中，服务发现是通过 portmapper 实现的。

portmapper 会启动在一个众所周知的端口上，RPC 程序由于是用户自己写的，会监听在一个随机端口上，但是 RPC 程序启动的时候，会向 portmapper 注册。客户端要访问 RPC 服务端这个程序的时候，首先查询 portmapper，获取 RPC 服务端程序的随机端口，然后向这个随机端口建立连接，开始 RPC 调用。从图中可以看出，mount 命令的 RPC 调用，就是这样实现的。

小结

好了，这一节就到这里，我们来总结一下。

远程调用看起来用 Socket 编程就可以了，其实是很复杂的，要解决协议约定问题、传输问题和服务发现问题。

大牛 Bruce Jay Nelson 的论文、早期 ONC RPC 框架，以及 NFS 的实现，给出了解决这三大问题的示范性实现，也即协议约定要公用协议描述文件，并通过这个文件生成 Stub 程序；RPC 的传输一般需要一个状态机，需要另外一个进程专门做服务发现。

最后，给你留两个思考题。

在这篇文章中，mount 的过程是通过系统调用，最终调用到 RPC 层。一旦 mount 完毕之后，客户端就像写入本地文件一样写入 NFS 了，这个过程是如何触发 RPC 层的呢？

ONC RPC 是早期的 RPC 框架，你觉得它有哪些问题呢？

我们的专栏更新到第 32 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第33讲 | 基于XML的SOAP协议：不要说NBA，请说美国职业篮球联赛

上一节我们讲了 RPC 的经典模型和设计要点，并用最早期的 ONC RPC 为例子，详述了具体的实现。

ONC RPC 存在哪些问题？

ONC RPC 将客户端要发送的参数，以及服务端要发送的回复，都压缩为一个二进制串，这样固然能够解决双方的协议约定问题，但是存在一定的不方便。

首先，需要双方的压缩格式完全一致，一点都不能差。一旦有少许的差错，多一位，少一位或者错一位，都可能造成无法解压缩。当然，我们可以用传输层的可靠性以及加入校验值等方式，来减少传输过程中的差错。

其次，协议修改不灵活。如果不是传输过程中造成的差错，而是客户端因为业务逻辑的改变，添加或者删除了字段，或者服务端添加或者删除了字段，而双方没有及时通知，或者线上系统没有及时升级，就会造成解压缩不成功。

因而，当业务发生改变，需要多传输一些参数或者少传输一些参数的时候，都需要及时通知对方，并且根据约定好的协议文件重新生成双方的 Stub 程序。自然，这样灵活性比较差。

如果仅仅是沟通的问题也还好解决，其实更难弄的还有版本的问题。比如在服务端提供一个服务，参数的格式是版本一的，已经有 50 个客户端在线上调用了。现在有一个客户端有个需求，要加一个字段，怎么办呢？这可是一个大工程，所有的客户端都要适配这个，需要重新写程序，加上这个字段，但是传输值是 0，不需要这个字段的客户端很“冤”，本来没我啥事儿，为啥让我也忙活？

最后，ONC RPC 的设计明显是面向函数的，而非面向对象。而当前面向对象的业务逻辑设计与实现方式已经成为主流。

这一切的根源就在于压缩。这就像平时我们爱用缩略语。如果是篮球爱好者，你直接说 NBA，他马上就知道什么意思，但是如果你给一个大妈说 NBA，她可能就不知所云。

所以，这种 RPC 框架只能用于客户端和服务端全由一拨人开发的场景，或者至少客户端和服务端的开发人员要密切沟通，相互合作，有大量的共同语言，才能按照既定的协议顺畅地进行工作。

XML 与 SOAP

但是，一般情况下，我们做一个服务，都是要提供给陌生人用的，你和客户不会经常沟通，也没有什么共同语言。就像你给别人介绍 NBA，你要说美国职业篮球赛，这样不管他是干啥的，都能听得懂。

放到我们的场景中，对应的就是用文本类的方式进行传输。无论哪个客户端获得这个文本，都能够知道它的意义。

一种常见的文本类格式是 XML。我们这里举个例子来看。

<?xml version="1.0" encoding="UTF-8"?>

<geek:purchaseOrder xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:geek="http://www.example.com/geek">

    <order>

        <date>2018-07-01</date>

        <className> 趣谈网络协议 </className>

        <Author> 刘超 </Author>

        <price>68</price>

    </order>

</geek:purchaseOrder>

我这里不准备详细讲述 XML 的语法规则，但是你相信我，看完下面的内容，即便你没有学过 XML，也能一看就懂，这段 XML 描述的是什么，不像全面的二进制，你看到的都是 010101，不知所云。

有了这个，刚才我们说的那几个问题就都不是问题了。

首先，格式没必要完全一致。比如如果我们把 price 和 author 换个位置，并不影响客户端和服务端解析这个文本，也根本不会误会，说这个作者的名字叫 68。

如果有的客户端想增加一个字段，例如添加一个推荐人字段，只需要在上面的文件中加一行：

<recommended> Gary </recommended>

对于不需要这个字段的客户端，只要不解析这一行就是了。只要用简单的处理，就不会出现错误。

另外，这种表述方式显然是描述一个订单对象的，是一种面向对象的、更加接近用户场景的表示方式。

既然 XML 这么好，接下来我们来看看怎么把它用在 RPC 中。

传输协议问题

我们先解决第一个，传输协议的问题。

基于 XML 的最著名的通信协议就是SOAP了，全称简单对象访问协议（Simple Object Access Protocol）。它使用 XML 编写简单的请求和回复消息，并用 HTTP 协议进行传输。

SOAP 将请求和回复放在一个信封里面，就像传递一个邮件一样。信封里面的信分抬头和正文。

POST /purchaseOrder HTTP/1.1

Host: www.geektime.com

Content-Type: application/soap+xml; charset=utf-8

Content-Length: nnn

<?xml version="1.0"?>

<soap:Envelope xmlns:soap="http://www.w3.org/2001/12/soap-envelope"

soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">

    <soap:Header>

        <m:Trans xmlns:m="http://www.w3schools.com/transaction/"

          soap:mustUnderstand="1">1234

        </m:Trans>

    </soap:Header>

    <soap:Body xmlns:m="http://www.geektime.com/perchaseOrder">

        <m:purchaseOrder">

            <order>

                <date>2018-07-01</date>

                <className> 趣谈网络协议 </className>

                <Author> 刘超 </Author>

                <price>68</price>

            </order>

        </m:purchaseOrder>

    </soap:Body>

</soap:Envelope>

HTTP 协议我们学过，这个请求使用 POST 方法，发送一个格式为 application/soap + xml 的 XML 正文给 www.geektime.com，从而下一个单，这个订单封装在 SOAP 的信封里面，并且表明这是一笔交易（transaction），而且订单的详情都已经写明了。

协议约定问题

接下来我们解决第二个问题，就是双方的协议约定是什么样的？

因为服务开发出来是给陌生人用的，就像上面下单的那个 XML 文件，对于客户端来说，它如何知道应该拼装成上面的格式呢？这就需要对于服务进行描述，因为调用的人不认识你，所以没办法找到你，问你的服务应该如何调用。

当然你可以写文档，然后放在官方网站上，但是你的文档不一定更新得那么及时，而且你也写的文档也不一定那么严谨，所以常常会有调试不成功的情况。因而，我们需要一种相对比较严谨的Web 服务描述语言，WSDL（Web Service Description Languages）。它也是一个 XML 文件。

在这个文件中，要定义一个类型 order，与上面的 XML 对应起来。

 <wsdl:types>

  <xsd:schema targetNamespace="http://www.example.org/geektime">

   <xsd:complexType name="order">

    <xsd:element name="date" type="xsd:string"></xsd:element>

<xsd:element name="className" type="xsd:string"></xsd:element>

<xsd:element name="Author" type="xsd:string"></xsd:element>

    <xsd:element name="price" type="xsd:int"></xsd:element>

   </xsd:complexType>

  </xsd:schema>

 </wsdl:types>

接下来，需要定义一个 message 的结构。

 <wsdl:message name="purchase">

  <wsdl:part name="purchaseOrder" element="tns:order"></wsdl:part>

 </wsdl:message>

接下来，应该暴露一个端口。

 <wsdl:portType name="PurchaseOrderService">

  <wsdl:operation name="purchase">

   <wsdl:input message="tns:purchase"></wsdl:input>

   <wsdl:output message="......"></wsdl:output>

  </wsdl:operation>

 </wsdl:portType>

然后，我们来编写一个 binding，将上面定义的信息绑定到 SOAP 请求的 body 里面。

 <wsdl:binding name="purchaseOrderServiceSOAP" type="tns:PurchaseOrderService">

  <soap:binding style="rpc"

   transport="http://schemas.xmlsoap.org/soap/http" />

  <wsdl:operation name="purchase">

   <wsdl:input>

    <soap:body use="literal" />

   </wsdl:input>

   <wsdl:output>

    <soap:body use="literal" />

   </wsdl:output>

  </wsdl:operation>

 </wsdl:binding>

最后，我们需要编写 service。

 <wsdl:service name="PurchaseOrderServiceImplService">

  <wsdl:port binding="tns:purchaseOrderServiceSOAP" name="PurchaseOrderServiceImplPort">

   <soap:address location="http://www.geektime.com:8080/purchaseOrder" />

  </wsdl:port>

 </wsdl:service>

WSDL 还是有些复杂的，不过好在有工具可以生成。

对于某个服务，哪怕是一个陌生人，都可以通过在服务地址后面加上“?wsdl”来获取到这个文件，但是这个文件还是比较复杂，比较难以看懂。不过好在也有工具可以根据 WSDL 生成客户端 Stub，让客户端通过 Stub 进行远程调用，就跟调用本地的方法一样。

服务发现问题

最后解决第三个问题，服务发现问题。

这里有一个UDDI（Universal Description, Discovery, and Integration），也即统一描述、发现和集成协议。它其实是一个注册中心，服务提供方可以将上面的 WSDL 描述文件，发布到这个注册中心，注册完毕后，服务使用方可以查找到服务的描述，封装为本地的客户端进行调用。

小结

好了，这一节就到这里了，我们来总结一下。

原来的二进制 RPC 有很多缺点，格式要求严格，修改过于复杂，不面向对象，于是产生了基于文本的调用方式——基于 XML 的 SOAP。

SOAP 有三大要素：协议约定用 WSDL、传输协议用 HTTP、服务发现用 UDDL。

最后，给你留两个思考题：

对于 HTTP 协议来讲，有多种方法，但是 SOAP 只用了 POST，这样会有什么问题吗？

基于文本的 RPC 虽然解决了二进制的问题，但是 SOAP 还是有点复杂，还有一种更便捷的接口规则，你知道是什么吗？

我们的专栏更新到第 33 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第34讲 | 基于JSON的RESTful接口协议：我不关心过程，请给我结果

上一节我们讲了基于 XML 的 SOAP 协议，SOAP 的 S 是啥意思来着？是 Simple，但是好像一点儿都不简单啊！

你会发现，对于 SOAP 来讲，无论 XML 中调用的是什么函数，多是通过 HTTP 的 POST 方法发送的。但是咱们原来学 HTTP 的时候，我们知道 HTTP 除了 POST，还有 PUT、DELETE、GET 等方法，这些也可以代表一个个动作，而且基本满足增、删、查、改的需求，比如增是 POST，删是 DELETE，查是 GET，改是 PUT。

传输协议问题

对于 SOAP 来讲，比如我创建一个订单，用 POST，在 XML 里面写明动作是 CreateOrder；删除一个订单，还是用 POST，在 XML 里面写明了动作是 DeleteOrder。其实创建订单完全可以使用 POST 动作，然后在 XML 里面放一个订单的信息就可以了，而删除用 DELETE 动作，然后在 XML 里面放一个订单的 ID 就可以了。

于是上面的那个 SOAP 就变成下面这个简单的模样。

POST /purchaseOrder HTTP/1.1

Host: www.geektime.com

Content-Type: application/xml; charset=utf-8

Content-Length: nnn

<?xml version="1.0"?>

 <order>

     <date>2018-07-01</date>

      <className> 趣谈网络协议 </className>

       <Author> 刘超 </Author>

       <price>68</price>

  </order>

而且 XML 的格式也可以改成另外一种简单的文本化的对象表示格式 JSON。

POST /purchaseOrder HTTP/1.1

Host: www.geektime.com

Content-Type: application/json; charset=utf-8

Content-Length: nnn

{

 "order": {

  "date": "2018-07-01",

  "className": " 趣谈网络协议 ",

  "Author": " 刘超 ",

  "price": "68"

 }

}

经常写 Web 应用的应该已经发现，这就是 RESTful 格式的 API 的样子。

协议约定问题

然而 RESTful 可不仅仅是指 API，而是一种架构风格，全称 Representational State Transfer，表述性状态转移，来自一篇重要的论文《架构风格与基于网络的软件架构设计》（Architectural Styles and the Design of Network-based Software Architectures）。

这篇文章从深层次，更加抽象地论证了一个互联网应用应该有的设计要点，而这些设计要点，成为后来我们能看到的所有高并发应用设计都必须要考虑的问题，再加上 REST API 比较简单直接，所以后来几乎成为互联网应用的标准接口。

因此，和 SOAP 不一样，REST 不是一种严格规定的标准，它其实是一种设计风格。如果按这种风格进行设计，RESTful 接口和 SOAP 接口都能做到，只不过后面的架构是 REST 倡导的，而 SOAP 相对比较关注前面的接口。

而且由于能够通过 WSDL 生成客户端的 Stub，因而 SOAP 常常被用于类似传统的 RPC 方式，也即调用远端和调用本地是一样的。

然而本地调用和远程跨网络调用毕竟不一样，这里的不一样还不仅仅是因为有网络而导致的客户端和服务端的分离，从而带来的网络性能问题。更重要的问题是，客户端和服务端谁来维护状态。所谓的状态就是对某个数据当前处理到什么程度了。

这里举几个例子，例如，我浏览到哪个目录了，我看到第几页了，我要买个东西，需要扣减一下库存，这些都是状态。本地调用其实没有人纠结这个问题，因为数据都在本地，谁处理都一样，而且一边处理了，另一边马上就能看到。

当有了 RPC 之后，我们本来期望对上层透明，就像上一节说的“远在天边，尽在眼前”。于是使用 RPC 的时候，对于状态的问题也没有太多的考虑。

就像 NFS 一样，客户端会告诉服务端，我要进入哪个目录，服务端必须要为某个客户端维护一个状态，就是当前这个客户端浏览到哪个目录了。例如，客户端输入 cd hello，服务端要在某个地方记住，上次浏览到 /root/liuchao 了，因而客户的这次输入，应该给它显示 /root/liuchao/hello 下面的文件列表。而如果有另一个客户端，同样输入 cd hello，服务端也在某个地方记住，上次浏览到 /var/lib，因而要给客户显示的是 /var/lib/hello。

不光 NFS，如果浏览翻页，我们经常要实现函数 next()，在一个列表中取下一页，但是这就需要服务端记住，客户端 A 上次浏览到 20～30 页了，那它调用 next()，应该显示 30～40 页，而客户端 B 上次浏览到 100～110 页了，调用 next() 应该显示 110～120 页。

上面的例子都是在 RPC 场景下，由服务端来维护状态，很多 SOAP 接口设计的时候，也常常按这种模式。这种模式原来没有问题，是因为客户端和服务端之间的比例没有失衡。因为一般不会同时有太多的客户端同时连上来，所以 NFS 还能把每个客户端的状态都记住。

公司内部使用的 ERP 系统，如果使用 SOAP 的方式实现，并且服务端为每个登录的用户维护浏览到报表那一页的状态，由于一个公司内部的人也不会太多，把 ERP 放在一个强大的物理机上，也能记得过来。

但是互联网场景下，客户端和服务端就彻底失衡了。你可以想象“双十一”，多少人同时来购物，作为服务端，它能记得过来吗？当然不可能，只好多个服务端同时提供服务，大家分担一下。但是这就存在一个问题，服务端怎么把自己记住的客户端状态告诉另一个服务端呢？或者说，你让我给你分担工作，你也要把工作的前因后果给我说清楚啊！

那服务端索性就要想了，既然这么多客户端，那大家就分分工吧。服务端就只记录资源的状态，例如文件的状态，报表的状态，库存的状态，而客户端自己维护自己的状态。比如，你访问到哪个目录了啊，报表的哪一页了啊，等等。

这样对于 API 也有影响，也就是说，当客户端维护了自己的状态，就不能这样调用服务端了。例如客户端说，我想访问当前目录下的 hello 路径。服务端说，我怎么知道你的当前路径。所以客户端要先看看自己当前路径是 /root/liuchao，然后告诉服务端说，我想访问 /root/liuchao/hello 路径。

再比如，客户端说我想访问下一页，服务端说，我怎么知道你当前访问到哪一页了。所以客户端要先看看自己访问到了 100～110 页，然后告诉服务器说，我想访问 110～120 页。

这就是服务端的无状态化。这样服务端就可以横向扩展了，一百个人一起服务，不用交接，每个人都能处理。

所谓的无状态，其实是服务端维护资源的状态，客户端维护会话的状态。对于服务端来讲，只有资源的状态改变了，客户端才调用 POST、PUT、DELETE 方法来找我；如果资源的状态没变，只是客户端的状态变了，就不用告诉我了，对于我来说都是统一的 GET。

虽然这只改进了 GET，但是已经带来了很大的进步。因为对于互联网应用，大多数是读多写少的。而且只要服务端的资源状态不变，就给了我们缓存的可能。例如可以将状态缓存到接入层，甚至缓存到 CDN 的边缘节点，这都是资源状态不变的好处。

按照这种思路，对于 API 的设计，就慢慢变成了以资源为核心，而非以过程为核心。也就是说，客户端只要告诉服务端你想让资源状态最终变成什么样就可以了，而不用告诉我过程，不用告诉我动作。

还是文件目录的例子。客户端应该访问哪个绝对路径，而非一个动作，我就要进入某个路径。再如，库存的调用，应该查看当前的库存数目，然后减去购买的数量，得到结果的库存数。这个时候应该设置为目标库存数（但是当前库存数要匹配），而非告知减去多少库存。

这种 API 的设计需要实现幂等，因为网络不稳定，就会经常出错，因而需要重试，但是一旦重试，就会存在幂等的问题，也就是同一个调用，多次调用的结果应该一样，不能一次支付调用，因为调用三次变成了支付三次。不能进入 cd a，做了三次，就变成了 cd a/a/a。也不能扣减库存，调用了三次，就扣减三次库存。

当然按照这种设计模式，无论 RESTful API 还是 SOAP API 都可以将架构实现成无状态的，面向资源的、幂等的、横向扩展的、可缓存的。

但是 SOAP 的 XML 正文中，是可以放任何动作的。例如 XML 里面可以写 < ADD >，< MINUS > 等。这就方便使用 SOAP 的人，将大量的动作放在 API 里面。

RESTful 没这么复杂，也没给客户提供这么多的可能性，正文里的 JSON 基本描述的就是资源的状态，没办法描述动作，而且能够出发的动作只有 CRUD，也即 POST、GET、PUT、DELETE，也就是对于状态的改变。

所以，从接口角度，就让你死了这条心。当然也有很多技巧的方法，在使用 RESTful API 的情况下，依然提供基于动作的有状态请求，这属于反模式了。

服务发现问题

对于 RESTful API 来讲，我们已经解决了传输协议的问题——基于 HTTP，协议约定问题——基于 JSON，最后要解决的是服务发现问题。

有个著名的基于 RESTful API 的跨系统调用框架叫 Spring Cloud。在 Spring Cloud 中有一个组件叫 Eureka。传说，阿基米德在洗澡时发现浮力原理，高兴得来不及穿上裤子，跑到街上大喊：“Eureka（我找到了）！”所以 Eureka 是用来实现注册中心的，负责维护注册的服务列表。

服务分服务提供方，它向 Eureka 做服务注册、续约和下线等操作，注册的主要数据包括服务名、机器 IP、端口号、域名等等。

另外一方是服务消费方，向 Eureka 获取服务提供方的注册信息。为了实现负载均衡和容错，服务提供方可以注册多个。

当消费方要调用服务的时候，会从注册中心读出多个服务来，那怎么调用呢？当然是 RESTful 方式了。

Spring Cloud 提供一个 RestTemplate 工具，用于将请求对象转换为 JSON，并发起 Rest 调用，RestTemplate 的调用也是分 POST、PUT、GET、 DELETE 的，当结果返回的时候，根据返回的 JSON 解析成对象。

通过这样封装，调用起来也很方便。

小结

好了，这一节就到这里了，我们来总结一下。

SOAP 过于复杂，而且设计是面向动作的，因而往往因为架构问题导致并发量上不去。

RESTful 不仅仅是一个 API，而且是一种架构模式，主要面向资源，提供无状态服务，有利于横向扩展应对高并发。

最后，给你留两个思考题：

在讨论 RESTful 模型的时候，举了一个库存的例子，但是这种方法有很大问题，那你知道为什么要这样设计吗？

基于文本的 RPC 虽然解决了二进制的问题，但是它本身也有问题，你能举出一些例子吗？

我们的专栏更新到第 34 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第35讲 | 二进制类RPC协议：还是叫NBA吧，总说全称多费劲

前面我们讲了两个常用文本类的 RPC 协议，对于陌生人之间的沟通，用 NBA、CBA 这样的缩略语，会使得协议约定非常不方便。

在讲 CDN 和 DNS 的时候，我们讲过接入层的设计，对于静态资源或者动态资源静态化的部分都可以做缓存。但是对于下单、支付等交易场景，还是需要调用 API。

对于微服务的架构，API 需要一个 API 网关统一的管理。API 网关有多种实现方式，用 Nginx 或者 OpenResty 结合 Lua 脚本是常用的方式。在上一节讲过的 Spring Cloud 体系中，有个组件 Zuul 也是干这个的。

数据中心内部是如何相互调用的？

API 网关用来管理 API，但是 API 的实现一般在一个叫作Controller 层的地方。这一层对外提供 API。由于是让陌生人访问的，我们能看到目前业界主流的，基本都是 RESTful 的 API，是面向大规模互联网应用的。

在 Controller 之内，就是咱们互联网应用的业务逻辑实现。上节讲 RESTful 的时候，说过业务逻辑的实现最好是无状态的，从而可以横向扩展，但是资源的状态还需要服务端去维护。资源的状态不应该维护在业务逻辑层，而是在最底层的持久化层，一般会使用分布式数据库和 ElasticSearch。

这些服务端的状态，例如订单、库存、商品等，都是重中之重，都需要持久化到硬盘上，数据不能丢，但是由于硬盘读写性能差，因而持久化层往往吞吐量不能达到互联网应用要求的吞吐量，因而前面要有一层缓存层，使用 Redis 或者 memcached 将请求拦截一道，不能让所有的请求都进入数据库“中军大营”。

缓存和持久化层之上一般是基础服务层，这里面提供一些原子化的接口。例如，对于用户、商品、订单、库存的增删查改，将缓存和数据库对再上层的业务逻辑屏蔽一道。有了这一层，上层业务逻辑看到的都是接口，而不会调用数据库和缓存。因而对于缓存层的扩容，数据库的分库分表，所有的改变，都截止到这一层，这样有利于将来对于缓存和数据库的运维。

再往上就是组合层。因为基础服务层只是提供简单的接口，实现简单的业务逻辑，而复杂的业务逻辑，比如下单，要扣优惠券，扣减库存等，就要在组合服务层实现。

这样，Controller 层、组合服务层、基础服务层就会相互调用，这个调用是在数据中心内部的，量也会比较大，还是使用 RPC 的机制实现的。

由于服务比较多，需要一个单独的注册中心来做服务发现。服务提供方会将自己提供哪些服务注册到注册中心中去，同时服务消费方订阅这个服务，从而可以对这个服务进行调用。

调用的时候有一个问题，这里的 RPC 调用，应该用二进制还是文本类？其实文本的最大问题是，占用字节数目比较多。比如数字 123，其实本来二进制 8 位就够了，但是如果变成文本，就成了字符串 123。如果是 UTF-8 编码的话，就是三个字节；如果是 UTF-16，就是六个字节。同样的信息，要多费好多的空间，传输起来也更加占带宽，时延也高。

因而对于数据中心内部的相互调用，很多公司选型的时候，还是希望采用更加省空间和带宽的二进制的方案。

这里一个著名的例子就是 Dubbo 服务化框架二进制的 RPC 方式。

Dubbo 会在客户端的本地启动一个 Proxy，其实就是客户端的 Stub，对于远程的调用都通过这个 Stub 进行封装。

接下来，Dubbo 会从注册中心获取服务端的列表，根据路由规则和负载均衡规则，在多个服务端中选择一个最合适的服务端进行调用。

调用服务端的时候，首先要进行编码和序列化，形成 Dubbo 头和序列化的方法和参数。将编码好的数据，交给网络客户端进行发送，网络服务端收到消息后，进行解码。然后将任务分发给某个线程进行处理，在线程中会调用服务端的代码逻辑，然后返回结果。

这个过程和经典的 RPC 模式何其相似啊！

如何解决协议约定问题？

接下来我们还是来看 RPC 的三大问题，其中注册发现问题已经通过注册中心解决了。我们下面就来看协议约定问题。

Dubbo 中默认的 RPC 协议是 Hessian2。为了保证传输的效率，Hessian2 将远程调用序列化为二进制进行传输，并且可以进行一定的压缩。这个时候你可能会疑惑，同为二进制的序列化协议，Hessian2 和前面的二进制的 RPC 有什么区别呢？这不绕了一圈又回来了吗？

Hessian2 是解决了一些问题的。例如，原来要定义一个协议文件，然后通过这个文件生成客户端和服务端的 Stub，才能进行相互调用，这样使得修改就会不方便。Hessian2 不需要定义这个协议文件，而是自描述的。什么是自描述呢？

所谓自描述就是，关于调用哪个函数，参数是什么，另一方不需要拿到某个协议文件、拿到二进制，靠它本身根据 Hessian2 的规则，就能解析出来。

原来有协议文件的场景，有点儿像两个人事先约定好，0 表示方法 add，然后后面会传两个数。服务端把两个数加起来，这样一方发送 012，另一方知道是将 1 和 2 加起来，但是不知道协议文件的，当它收到 012 的时候，完全不知道代表什么意思。

而自描述的场景，就像两个人说的每句话都带前因后果。例如，传递的是“函数：add，第一个参数 1，第二个参数 2”。这样无论谁拿到这个表述，都知道是什么意思。但是只不过都是以二进制的形式编码的。这其实相当于综合了 XML 和二进制共同优势的一个协议。

Hessian2 是如何做到这一点的呢？这就需要去看 Hessian2 的序列化的语法描述文件。

看起来很复杂，编译原理里面是有这样的语法规则的。

我们从 Top 看起，下一层是 value，直到形成一棵树。这里面的有个思想，为了防止歧义，每一个类型的起始数字都设置成为独一无二的。这样，解析的时候，看到这个数字，就知道后面跟的是什么了。

这里还是以加法为例子，“add(2,3)”被序列化之后是什么样的呢？

H x02 x00     # Hessian 2.0

C          # RPC call

 x03 add     # method "add"

 x92        # two arguments

 x92        # 2 - argument 1

 x93        # 3 - argument 2

H 开头，表示使用的协议是 Hession，H 的二进制是 0x48。

C 开头，表示这是一个 RPC 调用。

0x03，表示方法名是三个字符。

0x92，表示有两个参数。其实这里存的应该是 2，之所以加上 0x90，就是为了防止歧义，表示这里一定是一个 int。

第一个参数是 2，编码为 0x92，第二个参数是 3，编码为 0x93。

这个就叫作自描述。

另外，Hessian2 是面向对象的，可以传输一个对象。

class Car {

 String color;

 String model;

}

out.writeObject(new Car("red", "corvette"));

out.writeObject(new Car("green", "civic"));

---

C            # object definition (#0)

 x0b example.Car    # type is example.Car

 x92          # two fields

 x05 color       # color field name

 x05 model       # model field name

O            # object def (long form)

 x90          # object definition #0

 x03 red        # color field value

 x08 corvette      # model field value

x60           # object def #0 (short form)

 x05 green       # color field value

 x05 civic       # model field value

首先，定义这个类。对于类型的定义也传过去，因而也是自描述的。类名为 example.Car，字符长 11 位，因而前面长度为 0x0b。有两个成员变量，一个是 color，一个是 model，字符长 5 位，因而前面长度 0x05,。

然后，传输的对象引用这个类。由于类定义在位置 0，因而对象会指向这个位置 0，编码为 0x90。后面 red 和 corvette 是两个成员变量的值，字符长分别为 3 和 8。

接着又传输一个属于相同类的对象。这时候就不保存对于类的引用了，只保存一个 0x60，表示同上就可以了。

可以看出，Hessian2 真的是能压缩尽量压缩，多一个 Byte 都不传。

如何解决 RPC 传输问题？

接下来，我们再来看 Dubbo 的 RPC 传输问题。前面我们也说了，基于 Socket 实现一个高性能的服务端，是很复杂的一件事情，在 Dubbo 里面，使用了 Netty 的网络传输框架。

Netty 是一个非阻塞的基于事件的网络传输框架，在服务端启动的时候，会监听一个端口，并注册以下的事件。

连接事件：当收到客户端的连接事件时，会调用 void connected(Channel channel) 方法。

当可写事件触发时，会调用 void sent(Channel channel, Object message)，服务端向客户端返回响应数据。

当可读事件触发时，会调用 void received(Channel channel, Object message) ，服务端在收到客户端的请求数据。

当发生异常时，会调用 void caught(Channel channel, Throwable exception)。

当事件触发之后，服务端在这些函数中的逻辑，可以选择直接在这个函数里面进行操作，还是将请求分发到线程池去处理。一般异步的数据读写都需要另外的线程池参与，在线程池中会调用真正的服务端业务代码逻辑，返回结果。

Hessian2 是 Dubbo 默认的 RPC 序列化方式，当然还有其他选择。例如，Dubbox 从 Spark 那里借鉴 Kryo，实现高性能的序列化。

到这里，我们说了数据中心里面的相互调用。为了高性能，大家都愿意用二进制，但是为什么后期 Spring Cloud 又兴起了呢？这是因为，并发量越来越大，已经到了微服务的阶段。同原来的 SOA 不同，微服务粒度更细，模块之间的关系更加复杂。

在上面的架构中，如果使用二进制的方式进行序列化，虽然不用协议文件来生成 Stub，但是对于接口的定义，以及传的对象 DTO，还是需要共享 JAR。因为只有客户端和服务端都有这个 JAR，才能成功地序列化和反序列化。

但当关系复杂的时候，JAR 的依赖也变得异常复杂，难以维护，而且如果在 DTO 里加一个字段，双方的 JAR 没有匹配好，也会导致序列化不成功，而且还有可能循环依赖。这个时候，一般有两种选择。

第一种，建立严格的项目管理流程。

不允许循环调用，不允许跨层调用，只准上层调用下层，不允许下层调用上层。

接口要保持兼容性，不兼容的接口新添加而非改原来的，当接口通过监控，发现不用的时候，再下掉。

升级的时候，先升级服务提供端，再升级服务消费端。

第二种，改用 RESTful 的方式。

使用 Spring Cloud，消费端和提供端不用共享 JAR，各声明各的，只要能变成 JSON 就行，而且 JSON 也是比较灵活的。

使用 RESTful 的方式，性能会降低，所以需要通过横向扩展来抵消单机的性能损耗。

这个时候，就看架构师的选择喽！

小结

好了，这节就到这里了，我们来总结一下。

RESTful API 对于接入层和 Controller 层之外的调用，已基本形成事实标准，但是随着内部服务之间的调用越来越多，性能也越来越重要，于是 Dubbo 的 RPC 框架有了用武之地。

Dubbo 通过注册中心解决服务发现问题，通过 Hessian2 序列化解决协议约定的问题，通过 Netty 解决网络传输的问题。

在更加复杂的微服务场景下，Spring Cloud 的 RESTful 方式在内部调用也会被考虑，主要是 JAR 包的依赖和管理问题。

最后，给你留两个思考题。

对于微服务模式下的 RPC 框架的选择，Dubbo 和 SpringCloud 各有优缺点，你能做个详细的对比吗？

到目前为止，我们讲过的 RPC，还没有跨语言调用的场景，你知道如果跨语言应该怎么办吗？

我们的专栏更新到第 35 讲，不知你掌握得如何？每节课后我留的思考题，你都有没有认真思考，并在留言区写下答案呢？我会从已发布的文章中选出一批认真留言的同学，赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第36讲 | 跨语言类RPC协议：交流之前，双方先来个专业术语表

到目前为止，咱们讲了四种 RPC，分别是 ONC RPC、基于 XML 的 SOAP、基于 JSON 的 RESTful 和 Hessian2。

通过学习，我们知道，二进制的传输性能好，文本类的传输性能差一些；二进制的难以跨语言，文本类的可以跨语言；要写协议文件的严谨一些，不写协议文件的灵活一些。虽然都有服务发现机制，有的可以进行服务治理，有的则没有。

我们也看到了 RPC 从最初的客户端服务器模式，最终演进到微服务。对于 RPC 框架的要求越来越多了，具体有哪些要求呢？

首先，传输性能很重要。因为服务之间的调用如此频繁了，还是二进制的越快越好。

其次，跨语言很重要。因为服务多了，什么语言写成的都有，而且不同的场景适宜用不同的语言，不能一个语言走到底。

最好既严谨又灵活，添加个字段不用重新编译和发布程序。

最好既有服务发现，也有服务治理，就像 Dubbo 和 Spring Cloud 一样。

Protocol Buffers

这是要多快好省的建设社会主义啊。理想还是要有的嘛，这里我就来介绍一个向“理想”迈进的 GRPC。

GRPC 首先满足二进制和跨语言这两条，二进制说明压缩效率高，跨语言说明更灵活。但是又是二进制，又是跨语言，这就相当于两个人沟通，你不但说方言，还说缩略语，人家怎么听懂呢？所以，最好双方弄一个协议约定文件，里面规定好双方沟通的专业术语，这样沟通就顺畅多了。

对于 GRPC 来讲，二进制序列化协议是 Protocol Buffers。首先，需要定义一个协议文件.proto。

我们还看买极客时间专栏的这个例子。

syntax = “proto3”;

package com.geektime.grpc

option java_package = “com.geektime.grpc”;

message Order {

  required string date = 1;

  required string classname = 2;

  required string author = 3;

  required int price = 4;

}

message OrderResponse {

  required string message = 1;

}

service PurchaseOrder {

  rpc Purchase (Order) returns (OrderResponse) {}

}

在这个协议文件中，我们首先指定使用 proto3 的语法，然后我们使用 Protocol Buffers 的语法，定义两个消息的类型，一个是发出去的参数，一个是返回的结果。里面的每一个字段，例如 date、classname、author、price 都有唯一的一个数字标识，这样在压缩的时候，就不用传输字段名称了，只传输这个数字标识就行了，能节省很多空间。

最后定义一个 Service，里面会有一个 RPC 调用的声明。

无论使用什么语言，都有相应的工具生成客户端和服务端的 Stub 程序，这样客户端就可以像调用本地一样，调用远程的服务了。

协议约定问题

Protocol Buffers 是一款压缩效率极高的序列化协议，有很多设计精巧的序列化方法。

对于 int 类型 32 位的，一般都需要 4 个 Byte 进行存储。在 Protocol Buffers 中，使用的是变长整数的形式。对于每一个 Byte 的 8 位，最高位都有特殊的含义。

如果该位为 1，表示这个数字没完，后续的 Byte 也属于这个数字；如果该位为 0，则这个数字到此结束。其他的 7 个 Bit 才是用来表示数字的内容。因此，小于 128 的数字都可以用一个 Byte 表示；大于 128 的数字，比如 130，会用两个字节来表示。

对于每一个字段，使用的是 TLV（Tag，Length，Value）的存储办法。

其中 Tag = (field_num << 3) | wire_type。field_num 就是在 proto 文件中，给每个字段指定唯一的数字标识，而 wire_type 用于标识后面的数据类型。

例如，对于 string author = 3，在这里 field_num 为 3，string 的 wire_type 为 2，于是 (field_num << 3) | wire_type = (11000) | 10 = 11010 = 26；接下来是 Length，最后是 Value 为“liuchao”，如果使用 UTF-8 编码，长度为 7 个字符，因而 Length 为 7。

可见，在序列化效率方面，Protocol Buffers 简直做到了极致。

在灵活性方面，这种基于协议文件的二进制压缩协议往往存在更新不方便的问题。例如，客户端和服务器因为需求的改变需要添加或者删除字段。

这一点上，Protocol Buffers 考虑了兼容性。在上面的协议文件中，每一个字段都有修饰符。比如：

required：这个值不能为空，一定要有这么一个字段出现；

optional：可选字段，可以设置，也可以不设置，如果不设置，则使用默认值；

repeated：可以重复 0 到多次。

如果我们想修改协议文件，对于赋给某个标签的数字，例如 string author=3，这个就不要改变了，改变了就不认了；也不要添加或者删除 required 字段，因为解析的时候，发现没有这个字段就会报错。对于 optional 和 repeated 字段，可以删除，也可以添加。这就给了客户端和服务端升级的可能性。

例如，我们在协议里面新增一个 string recommended 字段，表示这个课程是谁推荐的，就将这个字段设置为 optional。我们可以先升级服务端，当客户端发过来消息的时候，是没有这个值的，将它设置为一个默认值。我们也可以先升级客户端，当客户端发过来消息的时候，是有这个值的，那它将被服务端忽略。

至此，我们解决了协议约定的问题。

网络传输问题

接下来，我们来看网络传输的问题。

如果是 Java 技术栈，GRPC 的客户端和服务器之间通过 Netty Channel 作为数据通道，每个请求都被封装成 HTTP 2.0 的 Stream。

Netty 是一个高效的基于异步 IO 的网络传输框架，这个上一节我们已经介绍过了。HTTP 2.0 在第 14 讲，我们也介绍过。HTTP 2.0 协议将一个 TCP 的连接，切分成多个流，每个流都有自己的 ID，而且流是有优先级的。流可以是客户端发往服务端，也可以是服务端发往客户端。它其实只是一个虚拟的通道。

HTTP 2.0 还将所有的传输信息分割为更小的消息和帧，并对它们采用二进制格式编码。

通过这两种机制，HTTP 2.0 的客户端可以将多个请求分到不同的流中，然后将请求内容拆成帧，进行二进制传输。这些帧可以打散乱序发送， 然后根据每个帧首部的流标识符重新组装，并且可以根据优先级，决定优先处理哪个流的数据。

由于基于 HTTP 2.0，GRPC 和其他的 RPC 不同，可以定义四种服务方法。

第一种，也是最常用的方式是单向 RPC，即客户端发送一个请求给服务端，从服务端获取一个应答，就像一次普通的函数调用。

rpc SayHello(HelloRequest) returns (HelloResponse){}

第二种方式是服务端流式 RPC，即服务端返回的不是一个结果，而是一批。客户端发送一个请求给服务端，可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取，直到没有更多消息为止。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}

第三种方式为客户端流式 RPC，也即客户端的请求不是一个，而是一批。客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入，就等待服务端读取这些消息并返回应答。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}

第四种方式为双向流式 RPC，即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的，所以客户端和服务端能按其希望的任意顺序读写，服务端可以在写应答前等待所有的客户端消息，或者它可以先读一个消息再写一个消息，或者读写相结合的其他方式。每个数据流里消息的顺序会被保持。

rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}

如果基于 HTTP 2.0，客户端和服务器之间的交互方式要丰富得多，不仅可以单方向远程调用，还可以实现当服务端状态改变的时候，主动通知客户端。

至此，传输问题得到了解决。

服务发现与治理问题

最后是服务发现与服务治理的问题。

GRPC 本身没有提供服务发现的机制，需要借助其他的组件，发现要访问的服务端，在多个服务端之间进行容错和负载均衡。

其实负载均衡本身比较简单，LVS、HAProxy、Nginx 都可以做，关键问题是如何发现服务端，并根据服务端的变化，动态修改负载均衡器的配置。

在这里我们介绍一种对于 GRPC 支持比较好的负载均衡器 Envoy。其实 Envoy 不仅仅是负载均衡器，它还是一个高性能的 C++ 写的 Proxy 转发器，可以配置非常灵活的转发规则。

这些规则可以是静态的，放在配置文件中的，在启动的时候加载。要想重新加载，一般需要重新启动，但是 Envoy 支持热加载和热重启，这在一定程度上缓解了这个问题。

当然，最好的方式是将规则设置为动态的，放在统一的地方维护。这个统一的地方在 Envoy 眼中被称为服务发现（Discovery Service），过一段时间去这里拿一下配置，就修改了转发策略。

无论是静态的，还是动态的，在配置里面往往会配置四个东西。

第一个是 listener。Envoy 既然是 Proxy，专门做转发，就得监听一个端口，接入请求，然后才能够根据策略转发，这个监听的端口就称为 listener。

第二个是 endpoint，是目标的 IP 地址和端口。这个是 Proxy 最终将请求转发到的地方。

第三个是 cluster。一个 cluster 是具有完全相同行为的多个 endpoint，也即如果有三个服务端在运行，就会有三个 IP 和端口，但是部署的是完全相同的三个服务，它们组成一个 cluster，从 cluster 到 endpoint 的过程称为负载均衡，可以轮询。

第四个是 route。有时候多个 cluster 具有类似的功能，但是是不同的版本号，可以通过 route 规则，选择将请求路由到某一个版本号，也即某一个 cluster。

如果是静态的，则将后端的服务端的 IP 地址拿到，然后放在配置文件里面就可以了。

如果是动态的，就需要配置一个服务发现中心，这个服务发现中心要实现 Envoy 的 API，Envoy 可以主动去服务发现中心拉取转发策略。

看来，Envoy 进程和服务发现中心之间要经常相互通信，互相推送数据，所以 Envoy 在控制面和服务发现中心沟通的时候，就可以使用 GRPC，也就天然具备在用户面支撑 GRPC 的能力。

Envoy 如果复杂的配置，都能干什么事呢？

一种常见的规则是配置路由策略。例如后端的服务有两个版本，可以通过配置 Envoy 的 route，来设置两个版本之间，也即两个 cluster 之间的 route 规则，一个占 99% 的流量，一个占 1% 的流量。

另一种常见的规则就是负载均衡策略。对于一个 cluster 下的多个 endpoint，可以配置负载均衡机制和健康检查机制，当服务端新增了一个，或者挂了一个，都能够及时配置 Envoy，进行负载均衡。

所有这些节点的变化都会上传到注册中心，所有这些策略都可以通过注册中心进行下发，所以，更严格的意义上讲，注册中心可以称为注册治理中心。

Envoy 这么牛，是不是能够将服务之间的相互调用全部由它代理？如果这样，服务也不用像 Dubbo，或者 Spring Cloud 一样，自己感知到注册中心，自己注册，自己治理，对应用干预比较大。

如果我们的应用能够意识不到服务治理的存在，就是直接进行 GRPC 的调用就可以了。

这就是未来服务治理的趋势Serivce Mesh，也即应用之间的相互调用全部由 Envoy 进行代理，服务之间的治理也被 Envoy 进行代理，完全将服务治理抽象出来，到平台层解决。

至此 RPC 框架中有治理功能的 Dubbo、Spring Cloud、Service Mesh 就聚齐了。

小结

好了，这一节就到这里了，我们来总结一下。

GRPC 是一种二进制，性能好，跨语言，还灵活，同时可以进行服务治理的多快好省的 RPC 框架，唯一不足就是还是要写协议文件。

GRPC 序列化使用 Protocol Buffers，网络传输使用 HTTP 2.0，服务治理可以使用基于 Envoy 的 Service Mesh。

最后，给你留一个思考题吧。

在讲述 Service Mesh 的时候，我们说了，希望 Envoy 能够在服务不感知的情况下，将服务之间的调用全部代理了，你知道怎么做到这一点吗？

我们《趣谈网络协议》专栏已经接近尾声了。你还记得专栏开始，我们讲过的那个“双十一”下单的故事吗？

下节开始，我会将这个过程涉及的网络协议细节，全部串联起来，给你还原一个完整的网络协议使用场景。信息量会很大，做好准备哦，我们下期见！

第37讲 | 知识串讲：用双十一的故事串起碎片的网络协议（上）

基本的网络知识我们都讲完了，还记得最初举的那个“双十一”下单的例子吗？这一节开始，我们详细地讲解这个过程，用这个过程串起我们讲过的网络协议。

我把这个过程分为十个阶段，从云平台中搭建一个电商开始，到 BGP 路由广播，再到 DNS 域名解析，从客户看商品图片，到最终下单的整个过程，每一步我都会详细讲解。这节我们先来看前三个阶段。

1. 部署一个高可用高并发的电商平台

首先，咱们要有个电商平台。假设我们已经有了一个特别大的电商平台，这个平台应该部署在哪里呢？假设我们用公有云，一般公有云会有多个位置，比如在华东、华北、华南都有。毕竟咱们的电商是要服务全国的，当然到处都要部署了。我们把主站点放在华东。

为了每个点都能“雨露均沾”，也为了高可用性，往往需要有多个机房，形成多个可用区（Available Zone）。由于咱们的应用是分布在两个可用区的，所以假如任何一个可用区挂了，都不会受影响。

我们来回想数据中心那一节，每个可用区里有一片一片的机柜，每个机柜上有一排一排的服务器，每个机柜都有一个接入交换机，有一个汇聚交换机将多个机柜连在一起。

这些服务器里面部署的都是计算节点，每台上面都有 Open vSwitch 创建的虚拟交换机，将来在这台机器上创建的虚拟机，都会连到 Open vSwitch 上。

接下来，你在云计算的界面上创建一个 VPC（Virtual Private Cloud，虚拟私有网络），指定一个 IP 段，这样以后你部署的所有应用都会在这个虚拟网络里，使用你分配的这个 IP 段。为了不同的 VPC 相互隔离，每个 VPC 都会被分配一个 VXLAN 的 ID。尽管不同用户的虚拟机有可能在同一个物理机上，但是不同的 VPC 二层压根儿是不通的。

由于有两个可用区，在这个 VPC 里面，要为每一个可用区分配一个 Subnet，也就是在大的网段里分配两个小的网段。当两个可用区里面网段不同的时候，就可以配置路由策略，访问另外一个可用区，走某一条路由了。

接下来，应该创建数据库持久化层。大部分云平台都会提供 PaaS 服务，也就是说，不需要你自己搭建数据库，而是采用直接提供数据库的服务，并且单机房的主备切换都是默认做好的，数据库也是部署在虚拟机里面的，只不过从界面上，你看不到数据库所在的虚拟机而已。

云平台会给每个 Subnet 的数据库实例分配一个域名。创建数据库实例的时候，需要你指定可用区和 Subnet，这样创建出来的数据库实例可以通过这个 Subnet 的私网 IP 进行访问。

为了分库分表实现高并发的读写，在创建的多个数据库实例之上，会创建一个分布式数据库的实例，也需要指定可用区和 Subnet，还会为分布式数据库分配一个私网 IP 和域名。

对于数据库这种高可用性比较高的，需要进行跨机房高可用，因而两个可用区都要部署一套，但是只有一个是主，另外一个是备，云平台往往会提供数据库同步工具，将应用写入主的数据同步给备数据库集群。

接下来是创建缓存集群。云平台也会提供 PaaS 服务，也需要每个可用区和 Subnet 创建一套，缓存的数据在内存中，由于读写性能要求高，一般不要求跨可用区读写。

再往上层就是部署咱们自己写的程序了。基础服务层、组合服务层、Controller 层，以及 Nginx 层、API 网关等等，这些都是部署在虚拟机里面的。它们之间通过 RPC 相互调用，需要到注册中心进行注册。

它们之间的网络通信是虚拟机和虚拟机之间的。如果是同一台物理机，则那台物理机上的 OVS 就能转发过去；如果是不同的物理机，这台物理机的 OVS 和另一台物理机的 OVS 中间有一个 VXLAN 的隧道，将请求转发过去。

再往外就是负载均衡了，负载均衡也是云平台提供的 PaaS 服务，也是属于某个 VPC 的，部署在虚拟机里面的，但是负载均衡有个外网的 IP，这个外网的 IP 地址就是在网关节点的外网网口上的。在网关节点上，会有 NAT 规则，将外网 IP 地址转换为 VPC 里面的私网 IP 地址，通过这些私网 IP 地址访问到虚拟机上的负载均衡节点，然后通过负载均衡节点转发到 API 网关的节点。

网关节点的外网网口是带公网 IP 地址的，里面有一个虚拟网关转发模块，还会有一个 OVS，将私网 IP 地址放到 VXLAN 隧道里面，转发到虚拟机上，从而实现外网和虚拟机网络之间的互通。

不同的可用区之间，通过核心交换机连在一起，核心交换机之外是边界路由器。

在华北、华东、华南同样也部署了一整套，每个地区都创建了 VPC，这就需要有一种机制将 VPC 连接到一起。云平台一般会提供硬件的 VPC 互连的方式，当然也可以使用软件互连的方式，也就是使用 VPN 网关，通过 IPsec VPN 将不同地区的不同 VPC 通过 VPN 连接起来。

对于不同地区和不同运营商的用户，我们希望他能够就近访问到网站，而且当一个点出了故障之后，我们希望能够在不同的地区之间切换，这就需要有智能 DNS，这个也是云平台提供的。

对于一些静态资源，可以保持在对象存储里面，通过 CDN 下发到边缘节点，这样客户端就能尽快加载出来。

2. 大声告诉全世界，可以到我这里买东西

当电商应用搭建完毕之后，接下来需要将如何访问到这个电商网站广播给全网。

刚才那张图画的是一个可用区的情况，对于多个可用区的情况，我们可以隐去计算节点的情况，将外网访问区域放大。

外网 IP 是放在虚拟网关的外网网口上的，这个 IP 如何让全世界知道呢？当然是通过 BGP 路由协议了。

每个可用区都有自己的汇聚交换机，如果机器数目比较多，可以直接用核心交换机，每个 Region 也有自己的核心交换区域。

在核心交换外面是安全设备，然后就是边界路由器。边界路由器会和多个运营商连接，从而每个运营商都能够访问到这个网站。边界路由器可以通过 BGP 协议，将自己数据中心里面的外网 IP 向外广播，也就是告诉全世界，如果要访问这些外网 IP，都来我这里。

每个运营商也有很多的路由器、很多的点，于是就可以将如何到达这些 IP 地址的路由信息，广播到全国乃至全世界。

3. 打开手机来上网，域名解析得地址

这个时候，不但你的这个网站的 IP 地址全世界都知道了，你打的广告可能大家也都看到了，于是有客户下载 App 来买东西了。

客户的手机开机以后，在附近寻找基站 eNodeB，发送请求，申请上网。基站将请求发给 MME，MME 对手机进行认证和鉴权，还会请求 HSS 看有没有钱，看看是在哪里上网。

当 MME 通过了手机的认证之后，开始建立隧道，建设的数据通路分两段路，其实是两个隧道。一段是从 eNodeB 到 SGW，第二段是从 SGW 到 PGW，在 PGW 之外，就是互联网。

PGW 会为手机分配一个 IP 地址，手机上网都是带着这个 IP 地址的。

当在手机上面打开一个 App 的时候，首先要做的事情就是解析这个网站的域名。

在手机运营商所在的互联网区域里，有一个本地的 DNS，手机会向这个 DNS 请求解析 DNS。当这个 DNS 本地有缓存，则直接返回；如果没有缓存，本地 DNS 才需要递归地从根 DNS 服务器，查到.com 的顶级域名服务器，最终查到权威 DNS 服务器。

如果你使用云平台的时候，配置了智能 DNS 和全局负载均衡，在权威 DNS 服务中，一般是通过配置 CNAME 的方式，我们可以起一个别名，例如 vip.yourcomany.com ，然后告诉本地 DNS 服务器，让它请求 GSLB 解析这个域名，GSLB 就可以在解析这个域名的过程中，通过自己的策略实现负载均衡。

GSLB 通过查看请求它的本地 DNS 服务器所在的运营商和地址，就知道用户所在的运营商和地址，然后将距离用户位置比较近的 Region 里面，三个负载均衡 SLB 的公网 IP 地址，返回给本地 DNS 服务器。本地 DNS 解析器将结果缓存后，返回给客户端。

对于手机 App 来说，可以绕过刚才的传统 DNS 解析机制，直接只要 HTTPDNS 服务，通过直接调用 HTTPDNS 服务器，得到这三个 SLB 的公网 IP 地址。

看，经过了如此复杂的过程，咱们的万里长征还没迈出第一步，刚刚得到 IP 地址，包还没发呢？话说手机 App 拿到了公网 IP 地址，接下来应该做什么呢？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第38讲 | 知识串讲：用双十一的故事串起碎片的网络协议（中）

上一节我们讲到，手机 App 经过了一个复杂的过程，终于拿到了电商网站的 SLB 的 IP 地址，是不是该下单了？

别忙，俗话说的好，买东西要货比三家。大部分客户在购物之前要看很多商品图片，比来比去，最后好不容易才下决心，点了下单按钮。下单按钮一按，就要开始建立连接。建立连接这个过程也挺复杂的，最终还要经过层层封装，才构建出一个完整的网络包。今天我们就来看这个过程。

4. 购物之前看图片，静态资源 CDN

客户想要在购物网站买一件东西的时候，一般是先去详情页看看图片，是不是想买的那一款。

我们部署电商应用的时候，一般会把静态资源保存在两个地方，一个是接入层 nginx 后面的 varnish 缓存里面，一般是静态页面；对于比较大的、不经常更新的静态图片，会保存在对象存储里面。这两个地方的静态资源都会配置 CDN，将资源下发到边缘节点。

配置了 CDN 之后，权威 DNS 服务器上，会为静态资源设置一个 CNAME 别名，指向另外一个域名 cdn.com ，返回给本地 DNS 服务器。

当本地 DNS 服务器拿到这个新的域名时，需要继续解析这个新的域名。这个时候，再访问的时候就不是原来的权威 DNS 服务器了，而是 cdn.com 的权威 DNS 服务器。这是 CDN 自己的权威 DNS 服务器。

在这个服务器上，还是会设置一个 CNAME，指向另外一个域名，也即 CDN 网络的全局负载均衡器。

本地 DNS 服务器去请求 CDN 的全局负载均衡器解析域名，全局负载均衡器会为用户选择一台合适的缓存服务器提供服务，将 IP 返回给客户端，客户端去访问这个边缘节点，下载资源。缓存服务器响应用户请求，将用户所需内容传送到用户终端。

如果这台缓存服务器上并没有用户想要的内容，那么这台服务器就要向它的上一级缓存服务器请求内容，直至追溯到网站的源服务器，将内容拉到本地。

5. 看上宝贝点下单，双方开始建连接

当你浏览了很多图片，发现实在喜欢某个商品，于是决定下单购买。

电商网站会对下单的情况提供 RESTful 的下单接口，而对于下单这种需要保密的操作，需要通过 HTTPS 协议进行请求。

在所有这些操作之前，首先要做的事情是建立连接。

HTTPS 协议是基于 TCP 协议的，因而要先建立 TCP 的连接。在这个例子中，TCP 的连接是从手机上的 App 和负载均衡器 SLB 之间的。

尽管中间要经过很多的路由器和交换机，但是 TCP 的连接是端到端的。TCP 这一层和更上层的 HTTPS 无法看到中间的包的过程。尽管建立连接的时候，所有的包都逃不过在这些路由器和交换机之间的转发，转发的细节我们放到那个下单请求的发送过程中详细解读，这里只看端到端的行为。

对于 TCP 连接来讲，需要通过三次握手建立连接，为了维护这个连接，双方都需要在 TCP 层维护一个连接的状态机。

一开始，客户端和服务端都处于 CLOSED 状态。服务端先是主动监听某个端口，处于 LISTEN 状态。然后客户端主动发起连接 SYN，之后处于 SYN-SENT 状态。服务端收到发起的连接，返回 SYN，并且 ACK 客户端的 SYN，之后处于 SYN-RCVD 状态。

客户端收到服务端发送的 SYN 和 ACK 之后，发送 ACK 的 ACK，之后处于 ESTABLISHED 状态。这是因为，它一发一收成功了。服务端收到 ACK 的 ACK 之后，处于 ESTABLISHED 状态，因为它的一发一收也成功了。

当 TCP 层的连接建立完毕之后，接下来轮到HTTPS 层建立连接了，在 HTTPS 的交换过程中，TCP 层始终处于 ESTABLISHED。

对于 HTTPS，客户端会发送 Client Hello 消息到服务器，用明文传输 TLS 版本信息、加密套件候选列表、压缩算法候选列表等信息。另外，还会有一个随机数，在协商对称密钥的时候使用。

然后，服务器会返回 Server Hello 消息，告诉客户端，服务器选择使用的协议版本、加密套件、压缩算法等。这也有一个随机数，用于后续的密钥协商。

然后，服务器会给你一个服务器端的证书，然后说：“Server Hello Done，我这里就这些信息了。”

客户端当然不相信这个证书，于是从自己信任的 CA 仓库中，拿 CA 的证书里面的公钥去解密电商网站的证书。如果能够成功，则说明电商网站是可信的。这个过程中，你可能会不断往上追溯 CA、CA 的 CA、CA 的 CA 的 CA，反正直到一个授信的 CA，就可以了。

证书验证完毕之后，觉得这个服务端是可信的，于是客户端计算产生随机数字 Pre-master，发送 Client Key Exchange，用证书中的公钥加密，再发送给服务器，服务器可以通过私钥解密出来。

接下来，无论是客户端还是服务器，都有了三个随机数，分别是：自己的、对端的，以及刚生成的 Pre-Master 随机数。通过这三个随机数，可以在客户端和服务器产生相同的对称密钥。

有了对称密钥，客户端就可以说：“Change Cipher Spec，咱们以后都采用协商的通信密钥和加密算法进行加密通信了。”

然后客户端发送一个 Encrypted Handshake Message，将已经商定好的参数等，采用协商密钥进行加密，发送给服务器用于数据与握手验证。

同样，服务器也可以发送 Change Cipher Spec，说：“没问题，咱们以后都采用协商的通信密钥和加密算法进行加密通信了”，并且也发送 Encrypted Handshake Message 的消息试试。

当双方握手结束之后，就可以通过对称密钥进行加密传输了。

真正的下单请求封装成网络包的发送过程，我们先放一放，我们来接着讲这个网络包的故事。

6. 发送下单请求网络包，西行需要出网关

当客户端和服务端之间建立了连接后，接下来就要发送下单请求的网络包了。

在用户层发送的是 HTTP 的网络包，因为服务端提供的是 RESTful API，因而 HTTP 层发送的就是一个请求。

POST /purchaseOrder HTTP/1.1

Host: www.geektime.com

Content-Type: application/json; charset=utf-8

Content-Length: nnn

{

 "order": {

  "date": "2018-07-01",

  "className": " 趣谈网络协议 ",

  "Author": " 刘超 ",

  "price": "68"

 }

}

HTTP 的报文大概分为三大部分。第一部分是请求行，第二部分是请求的首部，第三部分才是请求的正文实体。

在请求行中，URL 就是 www.geektime.com/purchaseOrder ，版本为 HTTP 1.1。

请求的类型叫作 POST，它需要主动告诉服务端一些信息，而非获取。需要告诉服务端什么呢？一般会放在正文里面。正文可以有各种各样的格式，常见的格式是 JSON。

请求行下面就是我们的首部字段。首部是 key value，通过冒号分隔。

Content-Type 是指正文的格式。例如，我们进行 POST 的请求，如果正文是 JSON，那么我们就应该将这个值设置为 JSON。

接下来是正文，这里是一个 JSON 字符串，里面通过文本的形式描述了，要买一个课程，作者是谁，多少钱。

这样，HTTP 请求的报文格式就拼凑好了。接下来浏览器或者移动 App 会把它交给下一层传输层。

怎么交给传输层呢？也是用 Socket 进行程序设计。如果用的是浏览器，这些程序不需要你自己写，有人已经帮你写好了；如果在移动 APP 里面，一般会用一个 HTTP 的客户端工具来发送，并且帮你封装好。

HTTP 协议是基于 TCP 协议的，所以它使用面向连接的方式发送请求，通过 Stream 二进制流的方式传给对方。当然，到了 TCP 层，它会把二进制流变成一个的报文段发送给服务器。

在 TCP 头里面，会有源端口号和目标端口号，目标端口号一般是服务端监听的端口号，源端口号在手机端，往往是随机分配一个端口号。这个端口号在客户端和服务端用于区分请求和返回，发给那个应用。

在 IP 头里面，都需要加上自己的地址（即源地址）和它想要去的地方（即目标地址）。当一个手机上线的时候，PGW 会给这个手机分配一个 IP 地址，这就是源地址，而目标地址则是云平台的负载均衡器的外网 IP 地址。

在 IP 层，客户端需要查看目标地址和自己是否是在同一个局域网，计算是否是同一个网段，往往需要通过 CIDR 子网掩码来计算。

对于这个下单场景，目标 IP 和源 IP 不会在同一个网段，因而需要发送到默认的网关。一般通过 DHCP 分配 IP 地址的时候，同时配置默认网关的 IP 地址。

但是客户端不会直接使用默认网关的 IP 地址，而是发送 ARP 协议，来获取网关的 MAC 地址，然后将网关 MAC 作为目标 MAC，自己的 MAC 作为源 MAC，放入 MAC 头，发送出去。

一个完整的网络包的格式是这样的。

真不容易啊，本来以为上篇就发送下单包了，结果到中篇这个包还没发送出去，只是封装了一个如此长的网络包。别着急，你可以自己先预想一下，接下来该做什么了？

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第39讲 | 知识串讲：用双十一的故事串起碎片的网络协议（下）

上一节，我们封装了一个长长的网络包，“大炮”准备完毕，开始发送。

发送的时候可以说是重重关隘，从手机到移动网络、互联网，还要经过多个运营商才能到达数据中心，到了数据中心就进入第二个复杂的过程，从网关到 VXLAN 隧道，到负载均衡，到 Controller 层、组合服务层、基础服务层，最终才下单入库。今天，我们就来看这最后一段过程。

7. 一座座城池一道道关，流控拥塞与重传

网络包已经组合完毕，接下来我们来看，如何经过一道道城关，到达目标公网 IP。

对于手机来讲，默认的网关在 PGW 上。在移动网络里面，从手机到 SGW，到 PGW 是有一条隧道的。在这条隧道里面，会将上面的这个包作为隧道的乘客协议放在里面，外面 SGW 和 PGW 在核心网机房的 IP 地址。网络包直到 PGW（PGW 是隧道的另一端）才将里面的包解出来，转发到外部网络。

所以，从手机发送出来的时候，网络包的结构为：

源 MAC：手机也即 UE 的 MAC；

目标 MAC：网关 PGW 上面的隧道端点的 MAC；

源 IP：UE 的 IP 地址；

目标 IP：SLB 的公网 IP 地址。

进入隧道之后，要封装外层的网络地址，因而网络包的格式为：

外层源 MAC：E-NodeB 的 MAC；

外层目标 MAC：SGW 的 MAC；

外层源 IP：E-NodeB 的 IP；

外层目标 IP：SGW 的 IP；

内层源 MAC：手机也即 UE 的 MAC；

内层目标 MAC：网关 PGW 上面的隧道端点的 MAC；

内层源 IP：UE 的 IP 地址；

内层目标 IP：SLB 的公网 IP 地址。

当隧道在 SGW 的时候，切换了一个隧道，会从 SGW 到 PGW 的隧道，因而网络包的格式为：

外层源 MAC：SGW 的 MAC；

外层目标 MAC：PGW 的 MAC；

外层源 IP：SGW 的 IP；

外层目标 IP：PGW 的 IP；

内层源 MAC：手机也即 UE 的 MAC；

内层目标 MAC：网关 PGW 上面的隧道端点的 MAC；

内层源 IP：UE 的 IP 地址；

内层目标 IP：SLB 的公网 IP 地址。

在 PGW 的隧道端点将包解出来，转发出去的时候，一般在 PGW 出外部网络的路由器上，会部署 NAT 服务，将手机的 IP 地址转换为公网 IP 地址，当请求返回的时候，再 NAT 回来。

因而在 PGW 之后，相当于做了一次欧洲十国游型的转发，网络包的格式为：

源 MAC：PGW 出口的 MAC；

目标 MAC：NAT 网关的 MAC；

源 IP：UE 的 IP 地址；

目标 IP：SLB 的公网 IP 地址。

在 NAT 网关，相当于做了一次玄奘西游型的转发，网络包的格式变成：

源 MAC：NAT 网关的 MAC；

目标 MAC：A2 路由器的 MAC；

源 IP：UE 的公网 IP 地址；

目标 IP：SLB 的公网 IP 地址。

出了 NAT 网关，就从核心网到达了互联网。在网络世界，每一个运营商的网络成为自治系统 AS。每个自治系统都有边界路由器，通过它和外面的世界建立联系。

对于云平台来讲，它可以被称为 Multihomed AS，有多个连接连到其他的 AS，但是大多拒绝帮其他的 AS 传输包。例如一些大公司的网络。对于运营商来说，它可以被称为 Transit AS，有多个连接连到其他的 AS，并且可以帮助其他的 AS 传输包，比如主干网。

如何从出口的运营商到达云平台的边界路由器？在路由器之间需要通过 BGP 协议实现，BGP 又分为两类，eBGP 和 iBGP。自治系统之间、边界路由器之间使用 eBGP 广播路由。内部网络也需要访问其他的自治系统。

边界路由器如何将 BGP 学习到的路由导入到内部网络呢？通过运行 iBGP，使内部的路由器能够找到到达外网目的地最好的边界路由器。

网站的 SLB 的公网 IP 地址早已经通过云平台的边界路由器，让全网都知道了。于是这个下单的网络包选择的下一跳是 A2，也即将 A2 的 MAC 地址放在目标 MAC 地址中。

到达 A2 之后，从路由表中找到下一跳是路由器 C1，于是将目标 MAC 换成 C1 的 MAC 地址。到达 C1 之后，找到下一跳是 C2，将目标 MAC 地址设置为 C2 的 MAC。到达 C2 后，找到下一跳是云平台的边界路由器，于是将目标 MAC 设置为边界路由器的 MAC 地址。

你会发现，这一路，都是只换 MAC，不换目标 IP 地址。这就是所谓下一跳的概念。

在云平台的边界路由器，会将下单的包转发进来，经过核心交换，汇聚交换，到达外网网关节点上的 SLB 的公网 IP 地址。

我们可以看到，手机到 SLB 的公网 IP，是一个端到端的连接，连接的过程发送了很多包。所有这些包，无论是 TCP 三次握手，还是 HTTPS 的密钥交换，都是要走如此复杂的过程到达 SLB 的，当然每个包走的路径不一定一致。

网络包走在这个复杂的道路上，很可能一不小心就丢了，怎么办？这就需要借助 TCP 的机制重新发送。

既然 TCP 要对包进行重传，就需要维护 Sequence Number，看哪些包到了，哪些没到，哪些需要重传，传输的速度应该控制到多少，这就是TCP 的滑动窗口协议。

整个 TCP 的发送，一开始会协商一个 Sequence Number，从这个 Sequence Number 开始，每个包都有编号。滑动窗口将接收方的网络包分成四个部分：

已经接收，已经 ACK，已经交给应用层的包；

已经接收，已经 ACK，未发送给应用层；

已经接收，尚未发送 ACK；

未接收，尚有空闲的缓存区域。

对于 TCP 层来讲，每一个包都有 ACK。ACK 需要从 SLB 回复到手机端，将上面的那个过程反向来一遍，当然路径不一定一致，可见 ACK 也不是那么轻松的事情。

如果发送方超过一定的时间没有收到 ACK，就会重新发送。只有 TCP 层 ACK 过的包，才会发给应用层，并且只会发送一份，对于下单的场景，应用层是 HTTP 层。

你可能会问了，TCP 老是重复发送，会不会导致一个单下了两遍？是否要求服务端实现幂等？从 TCP 的机制来看，是不会的。只有收不到 ACK 的包才会重复发，发到接收端，在窗口里面只保存一份，所以在同一个 TCP 连接中，不用担心重传导致二次下单。

但是 TCP 连接会因为某种原因断了，例如手机信号不好，这个时候手机把所有的动作重新做一遍，建立一个新的 TCP 连接，在 HTTP 层调用两次 RESTful API。这个时候可能会导致两遍下单的情况，因而 RESTful API 需要实现幂等。

当 ACK 过的包发给应用层之后，TCP 层的缓存就空了出来，这会导致上面图中的大三角，也即接收方能够容纳的总缓存，整体顺时针滑动。小的三角形，也即接收方告知发送方的窗口总大小，也即还没有完全确认收到的缓存大小，如果把这些填满了，就不能再发了，因为没确认收到，所以一个都不能扔。

8. 从数据中心进网关，公网 NAT 成私网

包从手机端经历千难万险，终于到了 SLB 的公网 IP 所在的公网网口。由于匹配上了 MAC 地址和 IP 地址，因而将网络包收了进来。

在虚拟网关节点的外网网口上，会有一个 NAT 规则，将公网 IP 地址转换为 VPC 里面的私网 IP 地址，这个私网 IP 地址就是 SLB 的 HAProxy 所在的虚拟机的私网 IP 地址。

当然为了承载比较大的吞吐量，虚拟网关节点会有多个，物理网络会将流量分发到不同的虚拟网关节点。同样 HAProxy 也会是一个大的集群，虚拟网关会选择某个负载均衡节点，将某个请求分发给它，负载均衡之后是 Controller 层，也是部署在虚拟机里面的。

当网络包里面的目标 IP 变成私有 IP 地址之后，虚拟路由会查找路由规则，将网络包从下方的私网网口发出来。这个时候包的格式为：

源 MAC：网关 MAC；

目标 MAC：HAProxy 虚拟机的 MAC；

源 IP：UE 的公网 IP；

目标 IP：HAProxy 虚拟机的私网 IP。

9. 进入隧道打标签，RPC 远程调用下单

在虚拟路由节点上，也会有 OVS，将网络包封装在 VXLAN 隧道里面，VXLAN ID 就是给你的租户创建 VPC 的时候分配的。包的格式为：

外层源 MAC：网关物理机 MAC；

外层目标 MAC：物理机 A 的 MAC；

外层源 IP：网关物理机 IP；

外层目标 IP：物理机 A 的 IP；

内层源 MAC：网关 MAC；

内层目标 MAC：HAProxy 虚拟机的 MAC；

内层源 IP：UE 的公网 IP；

内层目标 IP：HAProxy 虚拟机的私网 IP。

在物理机 A 上，OVS 会将包从 VXLAN 隧道里面解出来，发给 HAProxy 所在的虚拟机。HAProxy 所在的虚拟机发现 MAC 地址匹配，目标 IP 地址匹配，就根据 TCP 端口，将包发给 HAProxy 进程，因为 HAProxy 是在监听这个 TCP 端口的。因而 HAProxy 就是这个 TCP 连接的服务端，客户端是手机。对于 TCP 的连接状态、滑动窗口等，都是在 HAProxy 上维护的。

在这里 HAProxy 是一个四层负载均衡，也即它只解析到 TCP 层，里面的 HTTP 协议它不关心，就将请求转发给后端的多个 Controller 层的一个。

HAProxy 发出去的网络包就认为 HAProxy 是客户端了，看不到手机端了。网络包格式如下：

源 MAC：HAProxy 所在虚拟机的 MAC；

目标 MAC：Controller 层所在虚拟机的 MAC；

源 IP：HAProxy 所在虚拟机的私网 IP；

目标 IP：Controller 层所在虚拟机的私网 IP。

当然这个包发出去之后，还是会被物理机上的 OVS 放入 VXLAN 隧道里面，网络包格式为：

外层源 MAC：物理机 A 的 MAC；

外层目标 MAC：物理机 B 的 MAC；

外层源 IP：物理机 A 的 IP；

外层目标 IP：物理机 B 的 IP；

内层源 MAC：HAProxy 所在虚拟机的 MAC；

内层目标 MAC：Controller 层所在虚拟机的 MAC；

内层源 IP：HAProxy 所在虚拟机的私网 IP；

内层目标 IP：Controller 层所在虚拟机的私网 IP。

在物理机 B 上，OVS 会将包从 VXLAN 隧道里面解出来，发给 Controller 层所在的虚拟机。Controller 层所在的虚拟机发现 MAC 地址匹配，目标 IP 地址匹配，就根据 TCP 端口，将包发给 Controller 层的进程，因为它在监听这个 TCP 端口。

在 HAProxy 和 Controller 层之间，维护一个 TCP 的连接。

Controller 层收到包之后，它是关心 HTTP 里面是什么的，于是解开 HTTP 的包，发现是一个 POST 请求，内容是下单购买一个课程。

10. 下单扣减库存优惠券，数据入库返回成功

下单是一个复杂的过程，因而往往在组合服务层会有一个专门管理下单的服务，Controller 层会通过 RPC 调用这个组合服务层。

假设我们使用的是 Dubbo，则 Controller 层需要读取注册中心，将下单服务的进程列表拿出来，选出一个来调用。

Dubbo 中默认的 RPC 协议是 Hessian2。Hessian2 将下单的远程调用序列化为二进制进行传输。

Netty 是一个非阻塞的基于事件的网络传输框架。Controller 层和下单服务之间，使用了 Netty 的网络传输框架。有了 Netty，就不用自己编写复杂的异步 Socket 程序了。Netty 使用的方式，就是咱们讲Socket 编程的时候，一个项目组支撑多个项目（IO 多路复用，从派人盯着到有事通知）这种方式。

Netty 还是工作在 Socket 这一层的，发送的网络包还是基于 TCP 的。在 TCP 的下层，还是需要封装上 IP 头和 MAC 头。如果跨物理机通信，还是需要封装的外层的 VXLAN 隧道里面。当然底层的这些封装，Netty 都不感知，它只要做好它的异步通信即可。

在 Netty 的服务端，也即下单服务中，收到请求后，先用 Hessian2 的格式进行解压缩。然后将请求分发到线程中进行处理，在线程中，会调用下单的业务逻辑。

下单的业务逻辑比较复杂，往往要调用基础服务层里面的库存服务、优惠券服务等，将多个服务调用完毕，才算下单成功。下单服务调用库存服务和优惠券服务，也是通过 Dubbo 的框架，通过注册中心拿到库存服务和优惠券服务的列表，然后选一个调用。

调用的时候，统一使用 Hessian2 进行序列化，使用 Netty 进行传输，底层如果跨物理机，仍然需要通过 VXLAN 的封装和解封装。

咱们以库存为例子的时候，讲述过幂等的接口实现的问题。因为如果扣减库存，仅仅是谁调用谁减一。这样存在的问题是，如果扣减库存因为一次调用失败，而多次调用，这里指的不是 TCP 多次重试，而是应用层调用的多次重试，就会存在库存扣减多次的情况。

这里常用的方法是，使用乐观锁（Compare and Set，简称 CAS）。CAS 要考虑三个方面，当前的库存数、预期原来的库存数和版本，以及新的库存数。在操作之前，查询出原来的库存数和版本，真正扣减库存的时候，判断如果当前库存的值与预期原值和版本相匹配，则将库存值更新为新值，否则不做任何操作。

这是一种基于状态而非基于动作的设计，符合 RESTful 的架构设计原则。这样的设计有利于高并发场景。当多个线程尝试使用 CAS 同时更新同一个变量时，只有其中一个线程能更新变量的值，而其它线程都失败，失败的线程并不会被挂起，而是被告知这次竞争中失败，并可以再次尝试。

最终，当下单更新到分布式数据库中之后，整个下单过程才算真正告一段落。

好了，经过了十个过程，下单终于成功了，你是否对这个过程了如指掌了呢？如果发现对哪些细节比较模糊，可以回去看一下相应的章节，相信会有更加深入的理解。

到此，我带着你用下单过程把网络协议的知识都复习了一遍。授人以鱼不如授人以渔。下一节，我将会带你来搭建一个网络实验环境，配合实验来说明理论。

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

第40讲 | 搭建一个网络实验环境：授人以鱼不如授人以渔

因为这门课是基础课程，而且配合音频的形式发布，所以我多以理论为主来进行讲解。在专栏更新的过程中，不断有同学让我推荐一些网络方面的书籍，还有同学说能不能配合一些实验来说明理论。

的确，网络是一门实验性很强的学科，就像我在开篇词里面说的一样：一看觉得懂，一问就打鼓，一用就糊涂。 在写专栏的过程中，我自己也深深体会到了。这个时候，我常常会拿一个现实的环境，上手操作一下，抓个包看看，这样心里就会有定论。

《TCP/IP 详解》实验环境搭建

对于网络方面的书籍，我当然首推 Rechard Stevens 的《TCP/IP illustrated》（《TCP/IP 详解》）。这本书把理论讲得深入浅出，还配有大量的上手实践和抓包，看到这些抓包，原来不理解的很多理论，一下子就能懂了。

这本书里有个拓扑图，书上的很多实验都是基于这个图的，但是这个拓扑图还是挺复杂的。我这里先不说，一会儿详细讲。

Rechard Stevens，因为工作中有这么一个环境，很方便做实验，最终才写出了这样一本书，而我们一般人学习网络，没有这个环境应该怎么办呢？

时代不同了，咱们现在有更加强大的工具了。例如，这里这么多的机器，我们可以用 Docker 来实现，多个网络可以用 Open vSwitch 来实现。你甚至不需要一台物理机，只要一台 1 核 2G 的虚拟机，就能将这个环境搭建起来。

搭建这个环境的时候，需要一些脚本。我把脚本都放在了Github里面，你可以自己取用。

1. 创建一个 Ubuntu 虚拟机

在你的笔记本电脑上，用 VirtualBox 创建就行。1 核 2G，随便一台电脑都能搭建起来。

首先，我们先下载一个 Ubuntu 的镜像。我是从Ubuntu 官方网站下载的。

然后，在 VirtualBox 里面安装 Ubuntu。安装过程网上一大堆教程，你可以自己去看，我这里就不详细说了。

这里我需要说明的是网络的配置。

对于这个虚拟机，我们创建两个网卡，一个是 Host-only，只有你的笔记本电脑上能够登录进去。这个网卡上的 IP 地址也只有在你的笔记本电脑上管用。这个网卡的配置比较稳定，用于在 SSH 上做操作。这样你的笔记本电脑就可以搬来搬去，在公司里安装一半，回家接着安装另一半都没问题。

这里有一个虚拟的网桥，这个网络可以在管理 > 主机网络管理里面进行配置。

在这里可以虚拟网桥的的 IP 地址，同时启用一个 DHCP 服务器，为新创建的虚拟机配置 IP 地址。

另一个网卡配置为 NAT 网络，用于访问互联网。配置了 NAT 网络之后，只要你的笔记本电脑能上网，虚拟机就能上网。由于咱们在 Ubuntu 里面要安装一些东西，因而需要联网。

你可能会问了，这个配置复杂吗？一点儿都不复杂。咱们讲虚拟机网络的时候，讲过这个。

安装完了 Ubuntu 之后，需要对 Ubuntu 里面的网卡进行配置。对于 Ubuntu 来讲，网卡的配置在 /etc/network/interfaces 这个文件里面。在我的环境里，NAT 的网卡名称为 enp0s3，Host-only 的网卡的名称为 enp0s8，都可以配置为自动配置。

auto lo

iface lo inet loopback

auto enp0s3

iface enp0s3 inet dhcp

auto enp0s8

iface enp0s8 inet dhcp

这样，重启之后，IP 就配置好了。

2. 安装 Docker 和 Open vSwitch

接下来，在 Ubuntu 里面，以 root 用户，安装 Docker 和 Open vSwitch。

你可以按照 Docker 的官方安装文档来做。我这里也贴一下我的安装过程。

apt-get remove docker docker-engine docker.io

apt-get -y update

apt-get -y install apt-transport-https ca-certificates curl software-properties-common

curl -fsSL https://download.docker.com/linux/ubuntu/gpg > gpg

apt-key add gpg

apt-key fingerprint 0EBFCD88

add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

apt-get -y update

apt-cache madison docker-ce

apt-get -y install docker-ce=18.06.0~ce~3-0~ubuntu

之后，还需要安装 Open vSwitch 和 Bridge。

apt-get -y install openvswitch-common openvswitch-dbg openvswitch-switch python-openvswitch openvswitch-ipsec openvswitch-pki openvswitch-vtep

apt-get -y install bridge-utils

apt-get -y install arping

3. 准备一个 Docker 的镜像

每个节点都是一个 Docker，对应要有一个 Docker 镜像。这个镜像我已经打好了，你可以直接使用。

docker pull hub.c.163.com/liuchao110119163/ubuntu:tcpip

当然你也可以自己打这个镜像。Dockerfile 就像这样：

FROM hub.c.163.com/public/ubuntu:14.04

RUN apt-get -y update && apt-get install -y iproute2 iputils-arping net-tools tcpdump curl telnet iputils-tracepath traceroute

RUN mv /usr/sbin/tcpdump /usr/bin/tcpdump

ENTRYPOINT /usr/sbin/sshd -D

4. 启动整个环境

启动这个环境还是比较复杂的，我写成了一个脚本。在 Git 仓库里面，有一个文件 setupenv.sh ，可以执行这个脚本，里面有两个参数，一个参数是 NAT 网卡的名字，一个是镜像的名称。

git clone https://github.com/popsuper1982/tcpipillustrated.git

cd tcpipillustrated

docker pull hub.c.163.com/liuchao110119163/ubuntu:tcpip

chmod +x setupenv.sh

./setupenv.sh enp0s3 hub.c.163.com/liuchao110119163/ubuntu:tcpip

这样，整个环境就搭建起来了，所有的容器之间都可以 ping 通，而且都可以上网。

不过，我写的这个脚本对一些人来说可能会有点儿复杂，我这里也解释一下。

首先每一个节点，都启动一个容器。使用–privileged=true 方式，网络先不配置–net none。有两个二层网络，使用 ovs-vsctl 的 add-br 命令，创建两个网桥。

pipework 是一个很好的命令行工具，可以将容器连接到两个二层网络上。

但是我们上面那个图里有两个比较特殊的网络，一个是从 slip 到 bsdi 的 P2P 网络，需要创建一个 peer 的两个网卡，然后两个 Docker 的网络 namespace 里面各塞进去一个。

有关操作 Docker 的网络 namespace 的方式，咱们在容器网络那一节讲过 ip netns 命令。

这里需要注意的是，P2P 网络和下面的二层网络不是同一个网络。P2P 网络的 CIDR 是 140.252.13.64/27，而下面的二层网络的 CIDR 是 140.252.13.32/27。如果按照 /24，看起来是一个网络，但是 /27 就不是了。至于CIDR 的计算方法，你可以回去复习一下。

另外需要配置从 sun 到 netb 的点对点网络，方法还是通过 peer 网卡和 ip netns 的方式。

这里有个特殊的地方，对于 netb 来讲，不是一个普通的路由器，因为 netb 两边是同一个二层网络，所以需要配置 arp proxy。

为了所有的节点之间互通，要配置一下路由策略，这里需要通过 ip route 命令。

对于 slip 来讲，bsdi 左面 13.66 这个网口是网关。

对于 bsdi 和 svr4 来讲，如果去外网，sun 下面的网口 13.33 是网关。

对于 sun 来讲，上面的网口 1.29 属于上面的二层网络了，它如果去外网，gateway 下面的网口 1.4 就是外网网关。

对于 aix，solaris，gemini 来讲，如果去外网，网关也是 gateway 下面的网口 1.4。如果去下面的二层网口，网关是 sun 上面的网口 1.29。

配置完了这些，图中的所有的节点都能相互访问了，最后还要解决如何访问外网的问题。

我们还是需要创建一个 peer 网卡对。一个放在 gateway 里面，一个放在 gateway 外面。外面的网卡去外网的网关。

在虚拟机上面，还需要配置一个 iptables 的地址伪装规则 MASQUERADE，其实就是一个 SNAT。因为容器里面要访问外网，因为外网是不认的，所以源地址不能用容器的地址，需要 SNAT 成为虚拟机的地址出去，回来的时候再 NAT 回来。

配置这个环境还是挺复杂的，要用到咱们学到的很多知识。如果没有学习前面那些知识，直接就做这个实验，你肯定会很晕。但是只学理论也不行，要把理论都学过一遍，再做一遍实验，这才是一个不断迭代、更新知识库的过程。

有了这个环境，《TCP/IP 详解》里面的所有实验都能做了，而且我打的这个 Docker 镜像里面，tcpdump 等网络工具都安装了，你可以“为所欲为”了。

Open vSwitch 的实验

做了 TCP/IP 详解的实验之后，网络程序设计这部分，你就有了坚实的基础。但是涉及到数据中心内部的一些网络技术，什么 VLAN、VXLAN、STP 等偏运维方向的，学习还是会比较困难。好在我们有 Open vSwitch，也可以做大量的实验。

Open vSwitch 门槛比较高，里面的概念也非常多，可谓千头万绪。不过，通过我这么多年研究的经验，可以告诉你，这里面有一个很好的线索，那就是 Open vSwitch 会将自己对于网络的配置保存在一个本地库里面。这个库的表结构之间的关系就像这样：

这个库其实是一个 JSON，如果把这个 JSON 打印出来，能够看到更加详细的特性。按照这些特性一一实验，可以逐渐把 Open vSwitch 各个特性都掌握。

这里面最重要的概念就是网桥。一个网桥会有流表控制网络包的处理过程，会有控制器下发流表，一个网桥上会有多个端口，可以对端口进行流控，一个端口可以设置 VLAN，一个端口可以包含多个网卡，可以做绑定，网卡可以设置成为 GRE 和 VXLAN。

我写过一个 Open vSwitch 的实验教程，也放在了 Github 里面。这里面有这么几个比较重要的实验，你可以看一看。

实验一：查看 Open vSwitch 的架构。我们在讲 Open vSwitch 的时候，提过 Open vSwitch 的架构，在这个实验中，我们可以查看 Open vSwitch 的各个模块以及启动的参数。

实验五：配置使用 OpenFlow Controller，体验一把作为小区物业在监控室里面管控整个小区道路的样子。

实验八：测试 Port 的 VLAN 功能。看一下 VLAN 隔离究竟是什么样的。

实验十：QoS 功能。体验一把如果使用 HTB 进行网卡限流。

实验十一：GRE 和 VXLAN 隧道功能，看虚拟网络如何进行租户隔离。

实验十五：对 Flow Table 的操作，体验流表对网络包随心所欲的处理。

好了，关于整个环境的搭建我就讲到这里了。

其实到这里，对于网络世界的探索才刚刚开始，只有经过你自己动手和思考产生的内容，才是真正属于你的知识！打开你的电脑，上手去实验吧！

欢迎你留言和我讨论。趣谈网络协议，我们下期见！

“趣谈网络协议”专栏「食用指南」

你好，我是刘超。

“趣谈网络协议”专栏现在已经全部更新完毕。这里有一份「食用指南」，可以帮你找到学习本专栏的最佳姿势。

在这份指南中，我为你整理了专栏的所有学习资料，并告诉你如何更高效地使用这些资料，从而帮助你消化吸收，以期获得更好的学习效果。

不管你是刚刚打开这个专栏，还是进入温故的阶段，我的这份指南，都可以帮你更上一个台阶。一起加油吧！

1. 能力测试

我从常用的网络协议中，精心筛选了核心知识点，编成了 10 道题。这里面的题目和答案都是我精心设计的。希望你一定要先拿出纸笔，认真思考，记录下自己的答案，之后再和文末的详细解析进行对照。

刚刚打开这个专栏的你，可以据此寻找自己的薄弱点，对症下药；已经学习了一段时间的你，可以检测学习成果，查漏补缺。

点击查看：网络协议能力测试题

2. 答疑解惑

每篇文章后，我都会留两个思考题，其中第一个问题意在启发你的思考，是对本节内容的延伸学习；第二个问题是为引出下一节，下一节的内容其实就是答案（所以我就不单独解答啦）。

我希望你能够好好地利用这些思考题，毕竟所有的“知”，只有经过了自己的思考之后，才能成为“识”。

如果你是刚刚加入学习，你可以继续在思考题后的“留言区”写下你的答案，学习过程中遇到的问题和思考也欢迎多多分享，我依然会在这里回复你的留言，和你一起讨论。

我知道你肯定也很好奇我对这些问题的思考是怎样的，因此，我针对每一节课后的第一道思考题及留言区比较有代表性的、有深度的问题，特意写了一系列答疑文章。

我再强调一遍，对于这一系列的答疑文章，你一定要在自己进行深度思考之后，再来看文章对比答案，这样可以更有效地拓展你的知识边界。

点击查看：

第一期：第 1 讲至第 2 讲答疑解惑合辑

第二期：第 3 讲至第 6 讲答疑解惑合辑

第三期：第 7 讲至第 13 讲答疑解惑合辑

第四期：第 14 讲至第 21 讲答疑解惑合辑

第五期：第 22 讲至第 36 讲答疑解惑合辑

3. 知识串讲

在学习完前面 36 讲的内容之后，我详细讲解了一个“下单”的过程。我把这个过程分为十个阶段，从云平台中搭建一个电商开始，到 BGP 路由广播，再到 DNS 域名解析；从客户看商品图片，到最终下单的整个过程，每个步骤我都画了详细的分解图。

你可以用这个过程，串起我们讲过的所有网络协议，还原真实的使用场景，学以致用。我相信，学完前面的详细内容之后，再来看这个串讲内容，你对网络协议一定会有一个全面、深入的把握。

点击查看：

知识串讲（上篇）

知识串讲（中篇）

知识串讲（下篇）

4. 知识图谱

专栏中最精华的内容，我都整理在这张图上了。

点击查看：网络协议知识图谱

5. 实验环境

纸上得来终觉浅。网络是一门实验性很强的学科，我在写专栏的过程中也深深体会到了。有时候，遇到疑问，我常常会拿一个现实的环境，上手操作一下，抓个包看看，这样心里就会有定论。

网络方面最权威的书籍《TCP/IP 详解》（TCP/IP illustrated）的作者斯蒂文森（W. Richard Stevens），也是经过无数次实验，才完成这本巨著。

因此，我在这本书中的实验基础上，带你搭建一个实验环境，希望你能够上手操作一下学过的知识。毕竟，只有经过你自己动手和思考产生的内容，才是真正属于你的知识。

点击查看我搭建实验环境时候的具体操作，希望给你的思维晋升指路：《搭建一个网络实验环境：授人以鱼不如授人以渔》

6. 专栏音频

我在这里想特别提一下专栏音频。我的每篇专栏文章都包含了很多图片，为了帮助你更好的理解文章内容，我在录音的时候，常常会对图片做一些补充解释和说明，所以音频和文字稿并非完全一一对应。不知道你具体的学习习惯是怎样的，我建议你除了阅读文字以外，一定要听一下音频，可以利用“倍速播放”的功能，还可以自由把控播放速度，更高效地学习。

7. 记录，高效；分享，快乐

我们专栏还有不少功能，提醒你好好利用起来，成为高效的学习者。

比如，在学习的过程中，遇到自己不懂的地方，或者是有深刻感受的地方，一定要及时利用“划线笔记”的功能，记录下自己当时的想法。这样在过程中点滴积累，等学完后，还可以回过头来再过一遍。如果有可能，你可以把自己的这些思考梳理成文。相信我，这样做，你的提升速度会快到让自己意外。

再比如“请朋友读”功能。如果你觉得某篇内容对自己很有帮助，不妨把它推荐给身边有同样需求的朋友，这一个动作或许就能帮他解决一个手边的问题。最重要的是，通过这些分享，你会找到那些和你一样热爱学习的伙伴，一起学习更快乐。

最后，还有一个小小的彩蛋。我把自己这半年写专栏的经历，写成了一篇文章。我是如何写专栏中每一篇文章的？每一篇音频又是如何录出来的？创作专栏给我带来了哪些改变？带你走进“极客时间万人专栏”背后的创作故事。

点击查看：我是如何创作“趣谈网络协议”专栏的？

协议专栏特别福利 | 答疑解惑第一期

你好，我是刘超。

首先，感谢大家关注并在留言区写下近 3000 条留言。留言太多，没有及时回复，一是每周写三篇文章压力真的挺大的。为了保质保量的产出，晚上和周末的时间基本上都搭进去了。二是很多人的留言非常有深度，水平很高，提的问题一两句话解释不清楚。

每一节结尾我基本都会留两个思考题，其中第一个问题是启发思考的，是对本节内容的延伸学习；第二个问题是为了引出下一节，下一节的内容其实就是答案。

所以我会回答一下每一节的第一个问题，并列出第一个同我的思路最相近的同学，并对留言中比较有代表性的问题，做一个统一的回答，顺便也实现之前要送知识图谱和奖励礼券的承诺。

当然，这并不能说明我的回答就是一定是正确的或者全面的，有很多同学的留言有非常大的信息量，甚至更广的思路，也对这些同学表示感谢。还有些同学指出了我的错误，也感谢你们。

《第 1 讲 | 为什么要学习网络协议？》

课后思考题

当网络包到达一个城关的时候，可以通过路由表得到下一个城关的 IP 地址，直接通过 IP 地址找就可以了，为什么还要通过本地的 MAC 地址呢？

徐良红同学说的比较接近。在网络包里，有源 IP 地址和目标 IP 地址、源 MAC 地址和目标 MAC 地址。从路由表中取得下一跳的 IP 地址后，应该把这个地址放在哪里呢？如果放在目标 IP 地址里面，到了城关，谁知道最终的目标在哪里呢？所以要用 MAC 地址。

所谓的下一跳，看起来是 IP 地址，其实是要通过 ARP 得到 MAC 地址，将下一跳的 MAC 地址放在目标 MAC 地址里面。

留言问题

1.MAC 地址可以修改吗？

我查了一下，MAC（Media Access Control，介质访问控制）地址，也叫硬件地址，长度是 48 比特（6 字节），由 16 进制的数字组成，分为前 24 位和后 24 位。

前 24 位叫作组织唯一标志符（Organizationally Unique Identifier，OUI），是由 IEEE 的注册管理机构给不同厂家分配的代码，用于区分不同的厂家。后 24 位是厂家自己分配的，称为扩展标识符。同一个厂家生产的网卡中 MAC 地址后 24 位是不同的。

也就是说，MAC 本来设计为唯一性的，但是后来设备越来越多，而且还有虚拟化的设备和网卡，有很多工具可以修改，就很难保证不冲突了。但是至少应该保持一个局域网内是唯一的。

MAC 的设计，使得即便不能保证绝对唯一，但是能保证一个局域网内出现冲突的概率很小。这样，一台机器启动的时候，就能够在没有 IP 地址的情况下，先用 MAC 地址进行通信，获得 IP 地址。

好在 MAC 地址是工作在一个局域网中的，因而即便出现了冲突，网络工程师也能够在自己的范围内很快定位并解决这个问题。这就像我们生成 UUID 或者哈希值，大部分情况下是不会冲突的，但是如果碰巧出现冲突了，采取一定的机制解决冲突就好。

2.TCP 重试有没有可能导致重复下单？

答案是不会的。这个在TCP那一节有详细的讲解。因为 TCP 层收到了重复包之后，TCP 层自己会进行去重，发给应用层、HTTP 层。还是一个唯一的下单请求，所以不会重复下单。

那什么时候会导致重复下单呢？因为网络原因或者服务端错误，导致 TCP 连接断了，这样会重新发送应用层的请求，也即 HTTP 的请求会重新发送一遍。

如果服务端设计的是无状态的，它记不住上一次已经发送了一次请求。如果处理不好，就会导致重复下单，这就需要服务端除了实现无状态，还需要根据传过来的订单号实现幂等，同一个订单只处理一次。

还会有的现象是请求被黑客拦截，发送多次，这在 HTTPS 层可以有很多种机制，例如通过 Timestamp 和 Nonce 随机数联合起来，然后做一个不可逆的签名来保证。

3.TCP 报平安的包是原路返回吗？

谢谢语鬼同学的指正。这里的比喻不够严谨，容易让读者产生误会，这里的原路返回的意思是原样返回，也就是返回也是这个过程，不一定是完全一样的路径。

4.IP 地址和 MAC 地址的关系？

芒果同学的理解非常准确，讲IP 和 MAC 的关系的时候说了这个问题。IP 是有远程定位功能的，MAC 是没有远程定位功能的，只能通过本地 ARP 的方式找到。

我个人认为，即便有了 IPv6，也不会改变当前的网络分层模式，还是 IP 层解决远程定位问题，只不过改成 IPv6 了，到了本地，还是通过 MAC。

5. 如果最后一跳的时候，IP 改变了怎么办？

对于 IP 层来讲，当包到达最后一跳的时候，原来的 IP 不存在了。比如网线拔掉了，或者服务器直接宕机了，则 ARP 就找不到了，所以这个包就会发送失败了。对于 IP 层的工作就结束了。

但是 IP 层之上还有 TCP 层，TCP 会重试的，包还是会重新发送，但是如果服务器没有启动起来，超过一定的次数，最终放弃。

如果服务器重启了，IP 还是原来的 IP 地址，这个时候 TCP 重新发送的一个包的时候，ARP 是能够得到这个地址的，因而会发到这台机器上来，但是机器上面没有启动服务端监听那个端口，于是会发送 ICMP 端口不可达。

如果服务器重启了，服务端也重新启动了，也在监听那个端口了，这个时候 TCP 的服务端由于是新的，Sequence Number 根本对不上，说明不是原来的连接，会发送 RST。

那有没有可能有特殊的场景 Sequence Number 也能对的上呢？按照 Sequence Number 的生成算法，是不可能的。

但是有一个非常特殊的方式，就是虚拟机的热迁移，从一台物理机迁移到另外一台物理机，IP 不变，MAC 不变，内存也拷贝过去，Sequence Number 在内存里面也保持住了，在迁移的过程中会丢失一两个包，但是从 TCP 来看，最终还是能够连接成功的。

6.TCP 层报平安，怎么确认浏览器收到呢？

TCP 报平安，只能保证 TCP 层能够收到，不保证浏览器能够收到。但是可以想象，如果浏览器是你写的一个程序，你也是通过 socket 编程写的，你是通过 socket，建立一个 TCP 的连接，然后从这个连接里面读取数据，读取的数据就是 TCP 层确认收到的。

这个读取的动作是本地系统调用，大部分情况下不会失败的。如果读取失败呢，当然本地会报错，你的 socket 读取函数会返回错误，如果你是浏览器程序的实现者，你有两种选择，一个是将错误报告给用户，另一个是重新发送一次请求，获取结果显示给用户。

7.ARP 协议属于哪一层？

ARP 属于哪个层，一直是有争议的。比如《TCP/IP 详解》把它放在了二层和三层之间，但是既然是协议，只要大家都遵守相同的格式、流程就可以了，在实际应用的时候，不会有歧义的，唯一有歧义的是参加各种考试，让你做选择题，ARP 属于哪一层？平时工作中咱不用纠结这个。

《第 2 讲 | 网络分层的真实含义是什么？》

课后思考题

如果你也觉得总经理和员工的比喻不恰当，你有更恰当的比喻吗？

我觉得，寄快递和寄信这两个比喻都挺好的。关键是有了封装和解封装的过程。有的同学举了爬楼，或者公司各层之间的沟通，都无法体现封装和解封装的过程。

留言问题

1. 为什么要分层？

是的，仅仅用复杂性来解释分层，太过牵强了。

其实这是一个架构设计的通用问题，不仅仅是网络协议的问题。一旦涉及到复杂的逻辑，或者软件需求需要经常变动，一般都会通过分层来解决问题。

假如我们将所有的代码都写在一起，但是产品经理突然想调整一下界面，这背后的业务逻辑变不变，那要不要一起修改呢？所以会拆成两层，把 UI 层从业务逻辑中分离出来，调用 API 来进行组合。API 不变，仅仅界面变，是不是就不影响后台的代码了？

为什么要把一些原子的 API 放在基础服务层呢？将数据库、缓存、搜索引擎等，屏蔽到基础服务层以下，基础服务层之上的组合逻辑层、API 层都只能调用基础服务层的 API，不能直接访问数据库。

比如我们要将 Oracle 切换成 MySQL。MySQL 有一个库，分库分表成为 4 个库。难道所有的代码都要修改吗？当然只要把基础服务层屏蔽，提供一致的接口就可以了。

网络协议也是这样的。有的想基于 TCP，自己不操心就能够保证到达；有的想自己实现可靠通信，不基于 TCP，而使用 UDP。一旦分了层就好办了，定制化后要依赖于下一层的接口，只要实现自己的逻辑就可以了。如果 TCP 的实现将所有的逻辑耦合在了整个七层，不用 TCP 的可靠传输机制都没有办法。

2. 层级之间真实的调用方式是什么样的？

如果文中是一个逻辑图，这个问题其实已经到实现层面上来了，需要看 TCP/IP 的协议栈代码了。这里首先推荐一本书《深入理解 Linux 网络技术内幕》。

其实下层的协议知道上层协议的，因为在每一层的包头里面，都会有上一层是哪个协议的标识，所以不是一个回调函数，每一层的处理函数都会在操作系统启动的时候，注册到内核的一个数据结构里面，但是到某一层的时候，是通过判断到底是哪一层的哪一个协议，然后去找相应的处理函数去调用。

调用的大致过程我这里再讲一下。由于 TCP 比较复杂，我们以 UDP 为例子，其实发送的包就是一个 sk_buff 结构。这个在Socket那一节讲过。

int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4)

接着，UDP 层会调用 IP 层的函数。

int ip_send_skb(struct net *net, struct sk_buff *skb)

然后，IP 层通过路由判断，最终将包发给下一层。

int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)

发送的时候，要进行 ARP。如果有 MAC，则调用二层的函数，neigh 其实就是邻居系统，是二层的意思。

int neigh_output(struct neighbour *n, struct sk_buff *skb)

接收的时候，会调用这里的接收函数。

int netif_receive_skb(struct sk_buff *skb)

这个函数会根据是 ARP 或者 IP 等，选择调用不同的函数。如果是 IP 协议的话，就调用这里的函数。

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)

这里也有路由判断。如果是本地的，则继续往上提交这个结构。

int ip_local_deliver(struct sk_buff *skb)

接着，还是根据 IP 头里面的协议号，来判断是什么协议，从而调用什么函数。下面这个是对 UDP 的调用。

int udp_rcv(struct sk_buff *skb)

3. 什么情况下会有下层没上层？

有时候我们自己写应用的时候，不一定是直接调用应用层协议的接口，例如 HTTP 等，而是自己写 Socket 编程，来约定应用层的协议。再如，ping 也是一个应用，但是它没有用传输层的协议，而是用了 ICMP 的协议。

最后，感谢留言次数前 15 名的同学，谢谢你们持之以恒的学习，相信你们一定有自己的收获。（统计数据截止到 2018 年 8 月 8 日）

同时感谢第 1 讲、第 2 讲中对内容有深度思考和提出问题的同学。我会为你们送上奖励礼券和知识图谱。（稍后运营同学会发送短信通知。）

欢迎你继续提问！

协议专栏特别福利 | 答疑解惑第三期

你好，我是刘超。

第三期答疑涵盖第 7 讲至第 13 讲的内容。我依旧对课后思考题和留言中比较有代表性的问题作出回答。你可以点击文章名，回到对应的章节复习，也可以继续在留言区写下你的疑问，我会持续不断地解答。希望对你有帮助。

《第 7 讲 | ICMP 与 ping：投石问路的侦察兵》

课后思考题

当发送的报文出问题的时候，会发送一个 ICMP 的差错报文来报告错误，但是如果 ICMP 的差错报文也出问题了呢？

我总结了一下，不会导致产生 ICMP 差错报文的有：

ICMP 差错报文（ICMP 查询报文可能会产生 ICMP 差错报文）；

目的地址是广播地址或多播地址的 IP 数据报；

作为链路层广播的数据报；

不是 IP 分片的第一片；

源地址不是单个主机的数据报。这就是说，源地址不能为零地址、环回地址、广播地址或多播地址。

留言问题

1.ping 使用的是什么网络编程接口？

咱们使用的网络编程接口是 Socket，对于 ping 来讲，使用的是 ICMP，创建 Socket 如下：

socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)

SOCK_RAW 就是基于 IP 层协议建立通信机制。

如果是 TCP，则建立下面的 Socket：

socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)

如果是 UDP，则建立下面的 Socket：

socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)

2.ICMP 差错报文是谁发送的呢？

我看留言里有很多人对这个问题有疑惑。ICMP 包是由内核返回的，在内核中，有一个函数用于发送 ICMP 的包。

void icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info);

例如，目标不可达，会调用下面的函数。

icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0);

当 IP 大小超过 MTU 的时候，发送需要分片的 ICMP。

if (ip_exceeds_mtu(skb, mtu)) {

  icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));

  goto drop;

 }

《第 8 讲 | 世界这么大，我想出网关：欧洲十国游与玄奘西行》

课后思考题

当在你家里要访问 163 网站的时候，你的包需要 NAT 成为公网 IP，返回的包又要 NAT 成你的私有 IP，返回包怎么知道这是你的请求呢？它怎么能这么智能的 NAT 成了你的 IP 而非别人的 IP 呢？

这是个比较复杂的事情。在讲云中网络安全里的 iptables 时，我们讲过 conntrack 功能，它记录了 SNAT 一去一回的对应关系。

如果编译内核时开启了连接跟踪选项，那么 Linux 系统就会为它收到的每个数据包维持一个连接状态，用于记录这条数据连接的状态。

根据咱们学过的 Netfilter 的流程图，我们知道，网络包有三种路径：

发给我的，从 PREROUTING 到 INPUT，我就接收了；

我发给别人的，从 OUTPUT 到 POSTROUTING，就发出去的；

从我这里经过的，从 PREROUTING 到 FORWARD 到 POSTROUTING。

如果要跟踪一个网络包，对于每一种路径，都需要设置两个记录点，相当于打两次卡，这样内核才知道这个包的状态。

对于这三种路径，打卡的点是这样设置的：

发给我的，在 PREROUTING 调用 ipv4_conntrack_in，创建连接跟踪记录；在 INPUT 调用 ipv4_confirm，将这个连接跟踪记录挂在内核的连接跟踪表里面。为什么不一开始就挂在内核的连接跟踪表里面呢？因为有 filter 表，一旦把包过滤了，也就是丢弃了，那根本没必要记录这个连接了。

我发给别人的，在 OUTPUT 调用 ipv4_conntrack_local，创建连接跟踪记录，在 POSTROUTING 调用 ipv4_confirm，将这个连接跟踪记录挂在内核的连接跟踪表里面。

从我这里经过的，在 PREROUTING 调用 ipv4_conntrack_in，创建连接跟踪记录，在 POSTROUTING 调用 ipv4_confirm，将这个连接跟踪记录挂在内核的连接跟踪表里面。

网关主要做转发，这里主要说的是 NAT 网关，因而我们重点来看“从我这里经过的”这种场景，再加上要 NAT，因而将 NAT 的过程融入到连接跟踪的过程中来：

如果是 PREROUTING 的时候，先调用 ipv4_conntrack_in，创建连接跟踪记录；

如果是 PREROUTING 的时候，有 NAT 规则，则调用 nf_nat_ipv4_in 进行地址转换；

如果是 POSTROUTING 的时候，有 NAT 规则，则调用 nf_nat_ipv4_out 进行地址转换；

如果是 POSTROUTING 的时候，调用 ipv4_confirm，将这个连接跟踪记录挂在内核的连接跟踪表里面。

接下来，我们来看，在这个过程中涉及到的数据结构：连接跟踪记录、连接跟踪表。

在前面讲网络包处理的时候，我们说过，每个网络包都是一个 struct sk_buff，它有一个成员变量 _nfct 指向一个连接跟踪记录 struct nf_conn。当然当一个网络包刚刚进来的时候，是不会指向这么一个结构的，但是这个网络包肯定属于某个连接，因而会去连接跟踪表里面去查找，之后赋值给 sk_buff 的这个成员变量。没找到的话，就说明是一个新的连接，然后会重新创建一个。

连接跟踪记录里面有几个重要的东西：

nf_conntrack 其实才是 _nfct 变量指向的地址，但是没有关系，学过 C++ 的话应该明白，对于结构体来讲，nf_conn 和 nf_conntrack 的起始地址是一样的；

tuplehash 虽然是数组，但是里面只有两个，IP_CT_DIR_ORIGINAL 为下标 0，表示连接的发起方向，IP_CT_DIR_REPLY 为下标 1，表示连接的回复方向。

struct nf_conn {

 ......

 struct nf_conntrack ct_general;

 ......

 struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];

 ......

 unsigned long status;

 ......

}

在这里面，最重要的是 nf_conntrack_tuple_hash 的数组。nf_conn 是这个网络包对应的一去一回的连接追踪记录，但是这个记录是会放在一个统一的连接追踪表里面的。

连接跟踪表 nf_conntrack_hash 是一个数组，数组中的每一项都是一个双向链表的头，每一项后面都挂着一个双向链表，链表中的每一项都是这个结构。

这个结构的第一项是链表的链，nf_conntrack_tuple 是用来标识是否同一个连接。

从上面可以看出来，连接跟踪表是一个典型的链式哈希表的实现。

每当有一个网络包来了的时候，会将网络包中 sk_buff 中的数据提取出来，形成 nf_conntrack_tuple，并根据里面的内容计算哈希值。然后需要在哈希表中查找，如果找到，则说明这个连接出现过；如果没找到，则生成一个插入哈希表。

通过 nf_conntrack_tuple 里面的内容，可以唯一地标识一个连接：

src：包含源 IP 地址；如果是 TCP 或者 UDP，包含源端口；如果是 ICMP，包含的是 ID；

dst：包含目标 IP 地址；如果是 TCP 或者 UDP，包含目标端口；如果是 ICMP，包含的是 type, code。

有了这些数据结构，我们接下来看这一去一回的过程。

当一个包发出去的时候，到达这个 NAT 网关的时候，首先经过 PREROUTING 的时候，先调用 ipv4_conntrack_in。这个时候进来的包 sk_buff 为： {源 IP：客户端 IP，源端口：客户端 port，目标 IP：服务端 IP，目标端口：服务端 port}，将这个转换为 nf_conntrack_tuple，然后经过哈希运算，在连接跟踪表里面查找，发现没有，说明这是一个新的连接。

于是，创建一个新的连接跟踪记录 nf_conn，这里面有两个 nf_conntrack_tuple_hash：

一去：{源 IP：客户端 IP，源端口：客户端 port，目标 IP：服务端 IP，目标端口：服务端 port}；

一回：{源 IP：服务端 IP，源端口：服务端 port，目标 IP：客户端 IP，目标端口：客户端 port}。

接下来经过 FORWARD 过程，假设包没有被 filter 掉，于是要转发出去，进入 POSTROUTING 的过程，有 NAT 规则，则调用 nf_nat_ipv4_out 进行地址转换。这个时候，源地址要变成 NAT 网关的 IP 地址，对于 masquerade 来讲，会自动选择一个公网 IP 地址和一个随机端口。

为了让包回来的时候，能找到连接跟踪记录，需要修改两个 nf_conntrack_tuple_hash 中回来的那一项为：{源 IP：服务端 IP，源端口：服务端 port，目标 IP：NAT 网关 IP，目标端口：随机端口}。

接下来要将网络包真正发出去的时候，除了要修改包里面的源 IP 和源端口之外，还需要将刚才的一去一回的两个 nf_conntrack_tuple_hash 放入连接跟踪表这个哈希表中。

当网络包到达服务端，然后回复一个包的时候，这个包 sk_buff 为：{源 IP：服务端 IP，源端口：服务端 port，目标 IP：NAT 网关 IP，目标端口：随机端口}。

将这个转换为 nf_conntrack_tuple 后，进行哈希运算，在连接跟踪表里面查找，是能找到相应的记录的，找到 nf_conntrack_tuple_hash 之后，Linux 会提供一个函数。

static inline struct nf_conn *

nf_ct_tuplehash_to_ctrack(const struct nf_conntrack_tuple_hash *hash)

{

 return container_of(hash, struct nf_conn,

       tuplehash[hash->tuple.dst.dir]);

}

可以通过 nf_conntrack_tuple_hash 找到外面的连接跟踪记录 nf_conn，通过这个可以找到来方向的那个 nf_conntrack_tuple_hash，{源 IP：客户端 IP，源端口：客户端 port，目标 IP：服务端 IP，目标端口：服务端 port}，这样就能够找到客户端的 IP 和端口，从而可以 NAT 回去。

留言问题

1.NAT 能建立多少连接？

SNAT 多用于内网访问外网的场景，鉴于 conntrack 是由{源 IP，源端口，目标 IP，目标端口}，hash 后确定的。

如果内网机器很多，但是访问的是不同的外网，也即目标 IP 和目标端口很多，这样内网可承载的数量就非常大，可不止 65535 个。

但是如果内网所有的机器，都一定要访问同一个目标 IP 和目标端口，这样源 IP 如果只有一个，这样的情况下，才受 65535 的端口数目限制，根据原理，一种方法就是多个源 IP，另外的方法就是多个 NAT 网关，来分摊不同的内网机器访问。

如果你使用的是公有云，65535 台机器，应该放在一个 VPC 里面，可以放在多个 VPC 里面，每个 VPC 都可以有自己的 NAT 网关。

其实 SNAT 的场景是内网访问外网，存在端口数量的问题，也是所有的机器都访问一个目标地址的情况。

如果是微信这种场景，应该是服务端在数据中心内部，无论多少长连接，作为服务端监听的都是少数几个端口，是 DNAT 的场景，是没有端口数目问题的，只有一台服务器能不能维护这么多连接，因而在 NAT 网关后面部署多个 nginx 来分摊连接即可。

2. 公网 IP 和私网 IP 需要一一绑定吗？

公网 IP 是有限的，如果使用公有云，需要花钱去买。但是不是每一个虚拟机都要有一个公网 IP 的，只有需要对外提供服务的机器，也即接入层的那些 nginx 需要公网 IP，没有公网 IP，使用 SNAT，大家共享 SNAT 网关的公网 IP 地址，也是能够访问的外网的。

我看留言中的困惑点都在于，要区分内主动发起访问外，还是外主动发起访问内，是访问同一个服务端，还是访问一大批服务端。这里就很明白了。

《第 9 讲 | 路由协议：西出网关无故人，敢问路在何方》

课后思考题

路由协议要在路由器之间交换信息，这些信息的交换还需要走路由吗？不是死锁了吗？

OSPF 是直接基于 IP 协议发送的，而且 OSPF 的包都是发给邻居的，也即只有一跳，不会中间经过路由设备。BGP 是基于 TCP 协议的，在 BGP peer 之间交换信息。

留言问题

1. 多线 BGP 机房是怎么回事儿？

BGP 主要用于互联网 AS 自治系统之间的互联，BGP 的最主要功能在于控制路由的传播和选择最好的路由。各大运营商都具有 AS 号，全国各大网络运营商多数都是通过 BGP 协议与自身的 AS 来实现多线互联的。

使用此方案来实现多线路互联，IDC 需要在 CNNIC（中国互联网信息中心）或 APNIC（亚太网络信息中心）申请自己的 IP 地址段和 AS 号，然后通过 BGP 协议将此段 IP 地址广播到其它的网络运营商的网络中。

使用 BGP 协议互联后，网络运营商的所有骨干路由设备将会判断到 IDC 机房 IP 段的最佳路由，以保证不同网络运营商用户的高速访问。

《第 10 讲 | UDP 协议：因性善而简单，难免碰到“城会玩”》

课后思考题

都说 TCP 是面向连接的，在计算机看来，怎么样才算一个连接呢？

赵强强在留言中回答的是正确的。这是 TCP 的两端为了维护连接所保持的数据结构。

《第 11 讲 | TCP 协议（上）：因性恶而复杂，先恶后善反轻松》

课后思考题

TCP 的连接有这么多的状态，你知道如何在系统中查看某个连接的状态吗？

留言问题

1.TIME_WAIT 状态太多是怎么回事儿？

如果处于 TIMEWAIT 状态，说明双方建立成功过连接，而且已经发送了最后的 ACK 之后，才会处于这个状态，而且是主动发起关闭的一方处于这个状态。

如果存在大量的 TIMEWAIT，往往是因为短连接太多，不断的创建连接，然后释放连接，从而导致很多连接在这个状态，可能会导致无法发起新的连接。解决的方式往往是：

打开 tcp_tw_recycle 和 tcp_timestamps 选项；

打开 tcp_tw_reuse 和 tcp_timestamps 选项；

程序中使用 SO_LINGER，应用强制使用 rst 关闭。

当客户端收到 Connection Reset，往往是收到了 TCP 的 RST 消息，RST 消息一般在下面的情况下发送：

试图连接一个未被监听的服务端；

对方处于 TIMEWAIT 状态，或者连接已经关闭处于 CLOSED 状态，或者重新监听 seq num 不匹配；

发起连接时超时，重传超时，keepalive 超时；

在程序中使用 SO_LINGER，关闭连接时，放弃缓存中的数据，给对方发送 RST。

2. 起始序列号是怎么计算的，会冲突吗？

有同学在留言中问了几个问题。Ender0224 的回答非常不错。

起始 ISN 是基于时钟的，每 4 毫秒加一，转一圈要 4.55 个小时。

TCP 初始化序列号不能设置为一个固定值，因为这样容易被攻击者猜出后续序列号，从而遭到攻击。 RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。

ISN = M + F (localhost, localport, remotehost, remoteport)

M 是一个计时器，这个计时器每隔 4 毫秒加 1。F 是一个 Hash 算法，根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出，用 MD5 算法是一个比较好的选择。

《第 12 讲 | TCP 协议（下）：西行必定多妖孽，恒心智慧消磨难》

课后思考题

TCP 的 BBR 听起来很牛，你知道它是如何达到这个最优点的嘛？

《第 13 讲 | 套接字 Socket：Talk is cheap, show me the code》

课后思考题

epoll 是 Linux 上的函数，那你知道 Windows 上对应的机制是什么吗？如果想实现一个跨平台的程序，你知道应该怎么办吗？

epoll 是异步通知，当事件发生的时候，通知应用去调用 IO 函数获取数据。IOCP 异步传输，当事件发生时，IOCP 机制会将数据直接拷贝到缓冲区里，应用可以直接使用。

如果跨平台，推荐使用 libevent 库，它是一个事件通知库，适用于 Windows、Linux、BSD 等多种平台，内部使用 select、epoll、kqueue、IOCP 等系统调用管理事件机制。

感谢第 7 讲至第 13 讲中对内容有深度思考和提出问题的同学。我会为你们送上奖励礼券和知识图谱。（稍后运营同学会发送短信通知。）

欢迎你继续提问！

协议专栏特别福利 | 答疑解惑第二期

你好，我是刘超。

第二期答疑涵盖第 3 讲至第 6 讲的内容。我依旧对课后思考题和留言中比较有代表性的问题作出回答。你可以点击文章名，回到对应的章节复习，也可以继续在留言区写下你的疑问，我会持续不断地解答。希望对你有帮助。

《第 3 讲 | ifconfig：最熟悉又陌生的命令行》

课后思考题

你知道 net-tools 和 iproute2 的“历史”故事吗？

这个问题的答案，盖同学已经写的比较全面了。具体的对比，我这里推荐一篇文章https://linoxide.com/linux-command/use-ip-command-linux/，感兴趣的话可以看看。

留言问题

1.A、B、C 类地址的有效地址范围是多少？

我在写的时候，没有考虑这么严谨，平时使用地址的时候，也是看个大概的范围。所以这里再回答一下。

A 类 IP 的地址第一个字段范围是 0～127，但是由于全 0 和全 1 的地址用作特殊用途，实际可指派的范围是 1～126。所以我仔细查了一下，如果较真的话，你在答考试题的时候可以说，A 类地址范围和 A 类有效地址范围。

2. 网络号、IP 地址、子网掩码和广播地址的先后关系是什么？

当在一个数据中心或者一个办公室规划一个网络的时候，首先是网络管理员规划网段，一般是根据将来要容纳的机器数量来规划，一旦定了，以后就不好变了。

假如你在一个小公司里，总共就没几台机器，对于私有地址，一般选择 192.168.0.0/24 就可以了。

这个时候先有的是网络号。192.168.0 就是网络号。有了网络号，子网掩码同时也就有了，就是前面都是网络号的是 1，其他的是 0，广播地址也有了，除了网络号之外都是 1。

当规划完网络的时候，一般这个网络里面的第一个、第二个地址被默认网关 DHCP 服务器占用，你自己创建的机器，只要和其他的不冲突就可以了，当然你也可以让 DHCP 服务自动配置。

规划网络原来都是网络管理员的事情。有了公有云之后，一般有个概念虚拟网络（VPC），鼠标一点就能创建一个网络，网络完全软件化了，任何人都可以做网络规划。

3. 组播和广播的意义和原理是什么？

C 类地址的主机号 8 位，去掉 0 和 255，就只有 254 个了。

在《TCP/IP 详解》这本书里面，有两章讲了广播、多播以及 IGMP。广播和组播分为两个层面，其中 MAC 层有广播和组播对应的地址，IP 层也有自己的广播地址和组播地址。

广播相对比较简单，MAC 层的广播为 ff:ff:ff:ff:ff:ff，IP 层指向子网的广播地址为主机号为全 1 且有特定子网号的地址。

组播复杂一些，MAC 层中，当地址中最高字节的最低位设置为 1 时，表示该地址是一个组播地址，用十六进制可表示为 01:00:00:00:00:00。IP 层中，组播地址为 D 类 IP 地址，当 IP 地址为组播地址的时候，有一个算法可以计算出对应的 MAC 层地址。

多播进程将目的 IP 地址指明为多播地址，设备驱动程序将它转换为相应的以太网地址，然后把数据发送出去。这些接收进程必须通知它们的 IP 层，它们想接收的发给定多播地址的数据报，并且设备驱动程序必须能够接收这些多播帧。这个过程就是“加入一个多播组”。

当多播跨越路由器的时候，需要通过 IGMP 协议告诉多播路由器，多播数据包应该如何转发。

4.MTU 1500 的具体含义是什么？

MTU（Maximum Transmission Unit，最大传输单元）是二层的一个定义。以以太网为例，MTU 为 1500 个 Byte，前面有 6 个 Byte 的目标 MAC 地址，6 个 Byte 的源 MAC 地址，2 个 Byte 的类型，后面有 4 个 Byte 的 CRC 校验，共 1518 个 Byte。

在 IP 层，一个 IP 数据报在以太网中传输，如果它的长度大于该 MTU 值，就要进行分片传输。如果不允许分片 DF，就会发送 ICMP 包，这个在ICMP那一节讲过。

在 TCP 层有个 MSS（Maximum Segment Size，最大分段大小），它等于 MTU 减去 IP 头，再减去 TCP 头。即在不分片的情况下，TCP 里面放的最大内容。

在 HTTP 层看来，它的 body 没有限制，而且在应用层看来，下层的 TCP 是一个流，可以一直发送，但其实是会被分成一个个段的。

《第 4 讲 | DHCP 与 PXE：IP 是怎么来的，又是怎么没的》

课后思考题

PXE 协议可以用来安装操作系统，但是如果每次重启都安装操作系统，就会很麻烦。你知道如何使得第一次安装操作系统，后面就正常启动吗？

一般如果咱们手动安装一台电脑的时候，都是有启动顺序的，如果改为硬盘启动，就没有问题了。

好在服务器一般都提供 IPMI 接口，可以通过这个接口启动、重启、设置启动模式等等远程访问，这样就可以批量管理一大批机器。

这里提到 Cobbler，这是一个批量安装操作系统的工具。在 OpenStack 里面，还有一个 Ironic，也是用来管理裸机的。有兴趣的话可以研究一下。

留言问题

1. 在 DHCP 网络里面，手动配置 IP 地址会冲突吗?

在一个 DHCP 网络里面，如果某一台机器手动配置了一个 IP 地址，并且在 DHCP 管理的网段里的话，DHCP 服务器是会将这个地址分配给其他机器的。一旦分配了，ARP 的时候，就会收到两个应答，IP 地址就冲突了。

当发生这种情况的时候，应该怎么办呢？DHCP 的过程虽然没有明确如何处理，但是 DHCP 的客户端和服务器都可以添加相应的机制来检测冲突。

如果由客户端来检测冲突，一般情况是，客户端在接受分配的 IP 之前，先发送一个 ARP，看是否有应答，有就说明冲突了，于是发送一个 DHCPDECLINE，放弃这个 IP 地址。

如果由服务器来检测冲突，DHCP 服务器会发送 ping，来看某个 IP 是否已经被使用。如果被使用了，它就不再将这个 IP 分配给其他的客户端了。

2.DHCP 的 Offer 和 ACK 应该是单播还是广播呢？

没心没肺 回答的很正确。

这个我们来看DHCP 的 RFC，我截了个图放在这儿：

这里面说了几个问题。

正常情况下，一旦有了 IP 地址，DHCP Server 还是希望通过单播的方式发送 OFFER 和 ACK。但是不幸的是，有的客户端协议栈的实现，如果还没有配置 IP 地址，就使用单播。协议栈是不接收这个包的，因为 OFFER 和 ACK 的时候，IP 地址还没有配置到网卡上。

所以，一切取决于客户端的协议栈的能力，如果没配置好 IP，就不能接收单播的包，那就将 BROADCAST 设为 1，以广播的形式进行交互。

如果客户端的协议栈实现很厉害，即便是没有配置好 IP，仍然能够接受单播的包，那就将 BROADCAST 位设置为 0，就以单播的形式交互。

3.DHCP 如何解决内网安全问题?

其实 DHCP 协议的设计是基于内网互信的基础来设计的，而且是基于 UDP 协议。但是这里面的确是有风险的。例如一个普通用户无意地或者恶意地安装一台 DHCP 服务器，发放一些错误或者冲突的配置；再如，有恶意的用户发出很多的 DHCP 请求，让 DHCP 服务器给他分配大量的 IP。

对于第一种情况，DHCP 服务器和二层网络都是由网管管理的，可以在交换机配置只有来自某个 DHCP 服务器的包才是可信的，其他全部丢弃。如果有 SDN，或者在云中，非法的 DHCP 包根本就拦截到虚拟机或者物理机的出口。

对于第二种情况，一方面进行监控，对 DHCP 报文进行限速，并且异常的端口可以关闭，一方面还是 SDN 或者在云中，除了被 SDN 管控端登记过的 IP 和 MAC 地址，其他的地址是不允许出现在虚拟机和物理机出口的，也就无法模拟大量的客户端。

《第 5 讲 | 从物理层到 MAC 层：如何在宿舍里自己组网玩联机游戏？》

课后思考题

1. 在二层中我们讲了 ARP 协议，即已知 IP 地址求 MAC；还有一种 RARP 协议，即已知 MAC 求 IP 的，你知道它可以用来干什么吗？

2. 如果一个局域网里面有多个交换机，ARP 广播的模式会出现什么问题呢？

盖还说出了环路的问题。

没心没肺不但说明了问题，而且说明了方案。

《第 6 讲 | 交换机与 VLAN：办公室太复杂，我要回学校》

课后思考题

STP 协议能够很好地解决环路问题，但是也有它的缺点，你能举几个例子吗？

STP 的主要问题在于，当拓扑发生变化，新的配置消息要经过一定的时延才能传播到整个网络。

由于整个交换网络只有一棵生成树，在网络规模比较大的时候会导致较长的收敛时间，拓扑改变的影响面也较大，当链路被阻塞后将不承载任何流量，造成了极大带宽浪费。

留言问题

1. 每台交换机的武力值是什么样的？

当一台交换机加入或者离开网络的时候，都会造成网络拓扑变化，这个时候检测到拓扑变化的网桥会通知根网桥，根网桥会通知所有的网桥拓扑发生变化。

网桥的 ID 是由网桥优先级和网桥 MAC 地址组成的，网桥 ID 最小的将成为网络中的根桥。默认配置下，网桥优先级都一样，默认优先级是 32768。这个时候 MAC 地址最小的网桥成为根网桥。但是如果你想设置某台为根网桥，就配置更小的优先级即可。

在优先级向量里面，Root Bridge ID 就是根网桥的 ID，Bridge ID 是网桥的 ID，Port ID 就是一个网桥上有多个端口，端口的 ID。

按照 RFC 的定义，ROOT PATH COST 是和出口带宽相关的，具体的数据如下：

2. 图中的 LAN 指的是什么？

在这一节中，这两张图引起了困惑。

本来是为了讲二层的原理，做了个抽象的图，结果引起了大家的疑问，所以这里需要重新阐述一下。

首先，这里的 LAN1、LAN2、LAN 3 的说法的确不准确，因为通过网桥或者交换机连接，它们还是属于一个 LAN，其实这是三个物理网络，通过网桥或者交换机连接起来，形成一个二层的 LAN。

对于一层，也即物理层的设备，主要使用集线器（Hub），这里我们就用 Hub 将物理层连接起来。

于是我新画了两个图。

在这里，我用 Hub 将不同的机器连接在一起，形成一个物理段，而非 LAN。

3. 在 MAC 地址已经学习的情况下，ARP 会广播到没有 IP 的物理段吗？

首先谢谢这两位同学指出错误，这里 ARP 的目标地址是广播的，所以无论是否进行地址学习，都会广播，而对于某个 MAC 的访问，在没有地址学习的时候，是转发到所有的端口的，学习之后，只会转发到有这个 MAC 的端口。

4.802.1Q VLAN 和 Port-based VLAN 有什么区别？

所谓 Port-based VLAN，一般只在一台交换机上起作用，比如一台交换机，10 个口，1、3、5、7、9 属于 VLAN 10。1 发出的包，只有 3、5、7、9 能够收到，但是从这些口转发出去的包头中，并不带 VLAN ID。

而 802.1Q 的 VLAN，出了交换机也起作用，也就是说，一旦打上某个 VLAN，则出去的包都带这个 VLAN，也需要链路上的交换机能够识别这个 VLAN，进行转发。

感谢第 3 讲至第 6 讲中对内容有深度思考和提出问题的同学。我会为你们送上奖励礼券和知识图谱。（稍后运营同学会发送短信通知。）

欢迎你继续提问！

协议专栏特别福利 | 答疑解惑第五期

你好，我是刘超。

第五期答疑涵盖第 22 讲至第 36 讲的内容。我依旧对课后思考题和留言中比较有代表性的问题作出回答。你可以点击文章名，回到对应的章节复习，也可以继续在留言区写下你的疑问，我会持续不断地解答。希望对你有帮助。

《第 22 讲 | VPN：朝中有人好做官》

课后思考题

当前业务的高可用性和弹性伸缩很重要，所以很多机构都会在自建私有云之外，采购公有云，你知道私有云和公有云应该如何打通吗？

留言问题

DH 算法会因为传输随机数被破解吗？

这位同学的笔记特别认真，让人感动。DH 算法的交换材料要分公钥部分和私钥部分，公钥部分和其他非对称加密一样，都是可以传输的，所以对于安全性是没有影响的，而且传输材料远比传输原始的公钥更加安全。私钥部分是谁都不能给的，因此也是不会截获到的。

《第 23 讲 | 移动网络：去巴塞罗那，手机也上不了脸书》

课后思考题

咱们上网都有套餐，有交钱多的，有交钱少的，你知道移动网络是如何控制不同优先级的用户的上网流量的吗？

这个其实是 PCRF 协议进行控制的，它可以下发命令给 PGW 来控制上网的行为和特性。

《第 24 讲 | 云中网络：自己拿地成本高，购买公寓更灵活》

课后思考题

为了直观，这一节的内容我们以桌面虚拟化系统举例。在数据中心里面，有一款著名的开源软件 OpenStack，这一节讲的网络连通方式对应 OpenStack 中的哪些模型呢？

OpenStack 的早期网络模式有 Flat、Flat DHCP、VLAN，后来才有了 VPC，用 VXLAN 和 GRE 进行隔离。

《第 25 讲 | 软件定义网络：共享基础设施的小区物业管理办法》

课后思考题

在这一节中，提到了通过 VIP 可以通过流表在不同的机器之间实现负载均衡，你知道怎样才能做到吗？

可以通过 ovs-ofctl 下发流表规则，创建 group，并把端口加入 group 中，所有发现某个地址的包在两个端口之间进行负载均衡。

sudo ovs-ofctl -O openflow11 add-group br-lb "group_id=100 type=select selection_method=dp_hash bucket=output:1 bucket=output:2"

sudo ovs-ofctl -O openflow11 add-flow br-lb "table=0,ip,nw_dst=192.168.2.0/24,actions=group:100"

留言问题

SDN 控制器是什么东西？

SDN 控制器是一个独立的集群，主要是在管控面，因为要实现一定的高可用性。

主流的开源控制器有 OpenContrail、OpenDaylight 等。当然每个网络硬件厂商都有自己的控制器，而且可以实现自己的私有协议，进行更加细粒度的控制，所以江湖一直没有办法统一。

流表是在每一台宿主机上保存的，大小限制取决于内存，而集中存放的缺点就是下发会很慢。

《第 26 讲 | 云中的网络安全：虽然不是土豪，也需要基本安全和保障》

课后思考题

这一节中重点讲了 iptables 的 filter 和 nat 功能，iptables 还可以通过 QUEUE 实现负载均衡，你知道怎么做吗？

我们可以在 iptables 里面添加下面的规则：

-A PREROUTING -p tcp -m set --match-set minuteman dst,dst -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -j NFQUEUE --queue-balance 50:58

-A OUTPUT -p tcp -m set --match-set minuteman dst,dst -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -j NFQUEUE --queue-balance 50:58

NFQUEUE 的规则表示将把包的处理权交给用户态的一个进程。–queue-balance 表示会将包发给几个 queue。

libnetfilter_queue 是一个用户态库，用户态进程会使用 libnetfilter_queue 连接到这些 queue 中，将包读出来，根据包的内容做决策后，再放回内核进行发送。

《第 27 讲 | 云中的网络 QoS：邻居疯狂下电影，我该怎么办？》

课后思考题

这一节中提到，入口流量其实没有办法控制，出口流量是可以很好控制的，你能想出一个控制云中的虚拟机的入口流量的方式吗？

在云平台中，我们可以限制一个租户的默认带宽，我们仍然可以配置点对点的流量控制。

在发送方的 OVS 上，我们可以统计发送方虚拟机的网络统计数据，上报给管理面。在接收方的 OVS 上，我们同样可以收集接收方虚拟机的网络统计数据，上报给管理面。

当流量过大的时候，我们虽然不能控制接收方的入口流量，但是我们可以在管理面下发一个策略，控制发送方的出口流量。

留言问题

对于 HTB 借流量的情况，借出去的流量能够抢回来吗？

首先，借出去的流量，当自己使用的时候，是能够抢回来的。

有一篇著名的文章《HTB Linux queuing discipline manual》里面很详细，你可以看看。

很多人看不懂，这是一棵 HTB 树，有三个分支。A 用户使用 www 访问网页，SMTP 协议发送邮件，B 用户不限协议。

在时间 0 的时候，0、1、2 都以 90k 的速度发送数据，也即 A 用户在访问网页，同时发送邮件；B 也在上网，干啥都行。

在时间 3 的时候，将 0 的发送停止，A 不再访问网页了，红色的线归零，A 的总速率下来了，剩余的流量按照比例分给了蓝色的和绿色的线，也即分给了 A 发邮件和 B 上网。

在时间 6 的时候，将 0 的发送重启为 90k，也即 A 重新开始访问网页，则蓝色和绿色的流量返还给红色的流量。

在时间 9 的时候，将 1 的发送停止，A 不再发送邮件了，绿色的流量为零，A 的总速率也下来了，剩余的流量按照比例分给了蓝色和红色。

在时间 12，将 1 的发送恢复，A 又开始发送邮件了，红色和蓝色返还流量。

在时间 15，将 2 的发送停止，B 不再上网了，啥也不干了，蓝色流量为零，剩余的流量按照比例分给红色和绿色。

在时间 19，将 1 的发送停止，A 不再发送邮件了，绿色的流量为零，所有的流量都归了红色，所有的带宽都用于 A 来访问网页。

《第 28 讲 | 云中网络的隔离 GRE、VXLAN：虽然住一个小区，也要保护隐私》

课后思考题

虽然 VXLAN 可以支持组播，但是如果虚拟机数目比较多，在 Overlay 网络里面，广播风暴问题依然会很严重，你能想到什么办法解决这个问题吗？

很多情况下，物理机可以提前知道对端虚拟机的 MAC 地址，因而当发起 ARP 请求的时候，不用广播全网，只要本地返回就可以了，在 Openstack 里面称为 L2Population。

《第 29 讲 | 容器网络：来去自由的日子，不买公寓去合租》

课后思考题

容器内的网络和物理机网络可以使用 NAT 的方式相互访问，如果这种方式用于部署应用，有什么问题呢？

_CountingStars 对这个问题进行了补充。

其实就是性能损耗，随机端口占用，看不到真实 IP。

《第 30 讲 | 容器网络之 Flannel：每人一亩三分地》

课后思考题

通过 Flannel 的网络模型可以实现容器与容器直接跨主机的互相访问，那你知道如果容器内部访问外部的服务应该怎么融合到这个网络模型中吗？

Pod 内到外部网络是通过 docker 引擎在 iptables 的 POSTROUTING 中的 MASQUERADE 规则实现的，将容器的地址伪装为 node IP 出去，回来时再把包 nat 回容器的地址。

有的时候，我们想给外部的一个服务使用一个固定的域名，这就需要用到 Kubernetes 里 headless service 的 ExternalName。我们可以将某个外部的地址赋给一个 Service 的名称，当容器内访问这个名字的时候，就会访问一个虚拟的 IP。然后，在容器所在的节点上，由 iptables 规则映射到外部的 IP 地址。

《第 31 讲 | 容器网络之 Calico：为高效说出善意的谎言》

课后思考题

将 Calico 部署在公有云上的时候，经常会选择使用 IPIP 模式，你知道这是为什么吗？

_CountingStars 的回答是部分正确的。

一个原因是中间有路由，如果 VPC 网络是平的，但是公有云经常会有一个限制，那就是器的 IP 段是用户自己定义的，一旦出虚拟机的时候，云平台发现不是它分配的 IP，很多情况下直接就丢弃了。如果是 IPIP，出虚拟机之后，IP 还是虚拟机的 IP，就没有问题。

《第 32 讲 | RPC 协议综述：远在天边，近在眼前》

课后思考题

在这篇文章中，mount 的过程是通过系统调用，最终调用到 RPC 层。一旦 mount 完毕之后，客户端就像写入本地文件一样写入 NFS 了，这个过程是如何触发 RPC 层的呢？

_CountingStars 的回答很有深度。

是的，是通过 VFS 实现的。

在讲Socket那一节的时候，我们知道，在 Linux 里面，很多东西都是文件，因而内核中有一个打开的文件列表 File list，每个打开的文件都有一项。

对于 Socket 来讲，in-core inode 就是内存中的 inode。对于 nfs 来讲，也有一个 inode，这个 inode 里面有一个成员变量 file_operations，这里面是这个文件系统的操作函数。对于 nfs 来讲，因为有 nfs_file_read、nfs_file_write，所以在一个 mount 的路径中读取某个文件的时候，就会调用这两个函数，触发 RPC，远程调用服务端。

《第 33 讲 | 基于 XML 的 SOAP 协议：不要说 NBA，请说美国职业篮球联赛》

课后思考题

对于 HTTP 协议来讲，有多种方法，但是 SOAP 只用了 POST，这样会有什么问题吗？

《第 34 讲 | 基于 JSON 的 RESTful 接口协议：我不关心过程，请给我结果》

课后思考题

在讨论 RESTful 模型的时候，举了一个库存的例子，但是这种方法有很大问题，那你知道为什么要这样设计吗？

这个我在双十一包的例子中已经分析过了。

《第 35 讲 | 二进制类 RPC 协议：还是叫 NBA 吧，总说全称多费劲》

课后思考题

对于微服务模式下的 RPC 框架的选择，Dubbo 和 SpringCloud 各有优缺点，你能做个详细的对比吗？

《第 36 讲 | 跨语言类 RPC 协议：交流之前，双方先来个专业术语表》

课后思考题

在讲述 Service Mesh 的时候，我们说了，希望 Envoy 能够在服务不感知的情况下，将服务之间的调用全部代理了，你知道怎么做到这一点吗？

iptables 规则可以这样来设置：

首先，定义的一条规则是 ISTIO_REDIRECT 转发链。这条链不管三七二十一，都将网络包转发给 envoy 的 15000 端口。但是一开始这条链没有被挂到 iptables 默认的几条链中，所以不起作用。

接下来，在 PREROUTING 规则中使用这个转发链。从而，进入容器的所有流量都被先转发到 envoy 的 15000 端口。而 envoy 作为一个代理，已经被配置好了，将请求转发给 productpage 程序。

当 productpage 往后端进行调用的时候，就碰到了 output 链。这个链会使用转发链，将所有出容器的请求都转发到 envoy 的 15000 端口。

这样，无论是入口的流量，还是出口的流量，全部用 envoy 做成了“汉堡包”。envoy 根据服务发现的配置，做最终的对外调用。

这个时候，iptables 规则会对从 envoy 出去的流量做一个特殊处理，允许它发出去，不再使用上面的 output 规则。

感谢第 22 讲至第 36 讲中对内容有深度思考和提出问题的同学。我会为你们送上奖励礼券和知识图谱。（稍后运营同学会发送短信通知。）

咱们的答疑环节暂时告一段落了，如果你在学习过程中有什么问题，欢迎你继续留言给我！

协议专栏特别福利 | 答疑解惑第四期

你好，我是刘超。

第四期答疑涵盖第 14 讲至第 21 讲的内容。我依旧对课后思考题和留言中比较有代表性的问题作出回答。你可以点击文章名，回到对应的章节复习，也可以继续在留言区写下你的疑问，我会持续不断地解答。希望对你有帮助。

《第 14 讲 | HTTP 协议：看个新闻原来这么麻烦》

课后思考题

QUIC 是一个精巧的协议，所以它肯定不止今天我提到的四种机制，你知道还有哪些吗？

云学讲了一个 QUIC 的特性。

QUIC 还有其他特性，一个是快速建立连接。这个我放在下面 HTTPS 的时候一起说。另一个是拥塞控制，QUIC 协议当前默认使用了 TCP 协议的 CUBIC（拥塞控制算法）。

你还记得 TCP 的拥塞控制算法吗？每当收到一个 ACK 的时候，就需要调整拥塞窗口的大小。但是这也造成了一个后果，那就是 RTT 比较小的，窗口增长快。

然而这并不符合当前网络的真实状况，因为当前的网络带宽比较大，但是由于遍布全球，RTT 也比较长，因而基于 RTT 的窗口调整策略，不仅不公平，而且由于窗口增加慢，有时候带宽没满，数据就发送完了，因而巨大的带宽都浪费掉了。

CUBIC 进行了不同的设计，它的窗口增长函数仅仅取决于连续两次拥塞事件的时间间隔值，窗口增长完全独立于网络的时延 RTT。

CUBIC 的窗口大小以及变化过程如图所示。

当出现丢包事件时，CUBIC 会记录这时的拥塞窗口大小，把它作为 Wmax。接着，CUBIC 会通过某个因子执行拥塞窗口的乘法减小，然后，沿着立方函数进行窗口的恢复。

从图中可以看出，一开始恢复的速度是比较快的，后来便从快速恢复阶段进入拥塞避免阶段，也即当窗口接近 Wmax 的时候，增加速度变慢；立方函数在 Wmax 处达到稳定点，增长速度为零，之后，在平稳期慢慢增长，沿着立方函数的开始探索新的最大窗口。

留言问题

HTTP 的 keepalive 模式是什么样？

在没有 keepalive 模式下，每个 HTTP 请求都要建立一个 TCP 连接，并且使用一次之后就断开这个 TCP 连接。

使用 keepalive 之后，在一次 TCP 连接中可以持续发送多份数据而不会断开连接，可以减少 TCP 连接建立次数，减少 TIME_WAIT 状态连接。

然而，长时间的 TCP 连接容易导致系统资源无效占用，因而需要设置正确的 keepalive timeout 时间。当一个 HTTP 产生的 TCP 连接在传送完最后一个响应后，还需要等待 keepalive timeout 秒后，才能关闭这个连接。如果这个期间又有新的请求过来，可以复用 TCP 连接。

《第 15 讲 | HTTPS 协议：点外卖的过程原来这么复杂》

课后思考题

HTTPS 协议比较复杂，沟通过程太繁复，这样会导致效率问题，那你知道有哪些手段可以解决这些问题吗？

通过 HTTPS 访问的确复杂，至少经历四个阶段：DNS 查询、TCP 连接建立、TLS 连接建立，最后才是 HTTP 发送数据。我们可以一项一项来优化这个过程。

首先如果使用基于 UDP 的 QUIC，可以省略掉 TCP 的三次握手。至于 TLS 的建立，如果按文章中基于 TLS 1.2 的，双方要交换 key，经过两个来回，也即两个 RTT，才能完成握手。但是咱们讲 IPSec 的时候，讲过通过共享密钥、DH 算法进行握手的场景。

在 TLS 1.3 中，握手过程中移除了 ServerKeyExchange 和 ClientKeyExchange，DH 参数可以通过 key_share 进行传输。这样只要一个来回，就可以搞定 RTT 了。

对于 QUIC 来讲，也可以这样做。当客户端首次发起 QUIC 连接时，会发送一个 client hello 消息，服务器会回复一个消息，里面包括 server config，类似于 TLS1.3 中的 key_share 交换。当客户端获取到 server config 以后，就可以直接计算出密钥，发送应用数据了。

留言问题

1.HTTPS 的双向认证流程是什么样的？

2. 随机数和 premaster 的含义是什么？

《第 16 讲 | 流媒体协议：如何在直播里看到美女帅哥？》

课后思考题

你觉得基于 RTMP 的视频流传输的机制存在什么问题？如何进行优化？

Jason 的回答很对。

Jealone 的回答更加具体。

当前有基于自研 UDP 协议传输的，也有基于 QUIC 协议传输的。

留言问题

RTMP 建立连接的序列是什么样的？

的确，这个图我画错了，我重新画了一个。

不过文章中这部分的文字描述是没问题的。

客户端发送 C0、C1、 C2，服务器发送 S0、 S1、 S2。

首先，客户端发送 C0 表明自己的版本号，不必等对方的回复，然后发送 C1 表明自己的时间戳。

服务器只有在收到 C0 的时候，才能返回 S0，表明自己的版本号。如果版本不匹配，可以断开连接。

服务器发送完 S0 后，也不用等什么，就直接发送自己的时间戳 S1。客户端收到 S1 的时候，发一个知道了对方时间戳的 ACK C2。同理服务器收到 C1 的时候，发一个知道了对方时间戳的 ACK S2。

于是，握手完成。

《第 17 讲 | P2P 协议：我下小电影，99% 急死你》

课后思考题

除了这种去中心化分布式哈希的算法，你还能想到其他的应用场景吗？

留言问题

99% 卡住的原因是什么？

《第 18 讲 | DNS 协议：网络世界的地址簿》

课后思考题

全局负载均衡使用过程中，常常遇到失灵的情况，你知道具体有哪些情况吗？对应应该怎么来解决呢？

留言问题

如果权威 DNS 连不上，怎么办？

一般情况下，DNS 是基于 UDP 协议的。在应用层设置一个超时器，如果 UDP 发出没有回应，则会进行重试。

DNS 服务器一般也是高可用的，很少情况下会挂。即便挂了，也会很快切换，重试一般就会成功。

对于客户端来讲，为了 DNS 解析能够成功，也会配置多个 DNS 服务器，当一个不成功的时候，可以选择另一个来尝试。

《第 19 讲 | HTTPDNS：网络世界的地址簿也会指错路》

课后思考题

使用 HTTPDNS，需要向 HTTPDNS 服务器请求解析域名，可是客户端怎么知道 HTTPDNS 服务器的地址或者域名呢？

《第 20 讲 | CDN：你去小卖部取过快递么？》

课后思考题

这一节讲了 CDN 使用 DNS 进行全局负载均衡的例子，CDN 如何使用 HTTPDNS 呢？

《第 21 讲 | 数据中心：我是开发商，自己拿地盖别墅》

课后思考题

对于数据中心来讲，高可用是非常重要的，每个设备都要考虑高可用，那跨机房的高可用，你知道应该怎么做吗？

其实跨机房的高可用分两个级别，分别是同城双活和异地灾备。

同城双活，就是在同一个城市，距离大概 30km 到 100km 的两个数据中心之间，通过高速专线互联的方式，让两个数据中心形成一个大二层网络。

同城双活最重要的是，数据如何从一个数据中心同步到另一个数据中心，并且在一个数据中心故障的时候，实现存储设备的切换，保证状态能够快速切换到另一个数据中心。在高速光纤互联情况下，主流的存储厂商都可以做到在一定距离之内的两台存储设备的近实时同步。数据双活是一切双活的基础。

基于双数据中心的数据同步，可以形成一个统一的存储池，从而数据库层在共享存储池的情况下可以近实时的切换，例如 Oracle RAC。

虚拟机在统一的存储池的情况下，也可以实现跨机房的 HA，在一个机房切换到另一个机房。

SLB 负载均衡实现同一机房的各个虚拟机之间的负载均衡。GSLB 可以实现跨机房的负载均衡，实现外部访问的切换。

如果在两个数据中心距离很近，并且大二层可通的情况下，也可以使用 VRRP 协议，通过 VIP 方式进行外部访问的切换。

下面我们说异地灾备。

异地灾备的第一大问题还是数据的问题，也即生产数据中心的数据如何备份到容灾数据中心。由于异地距离比较远，不可能像双活一样采取近同步的方式，只能通过异步的方式进行同步。可以预见的问题是，容灾切换的时候，数据会丢失一部分。

由于容灾数据中心平时是不用的，不是所有的业务都会进行容灾，否则成本太高。

对于数据的问题，我比较建议从业务层面进行容灾。由于数据同步会比较慢，可以根据业务需求高优先级同步重要的数据，因而容灾的层次越高越好。

例如，有的用户完全不想操心，直接使用存储层面的异步复制。对于存储设备来讲，它是无法区分放在存储上的虚拟机，哪台是重要的，哪台是不重要的，只会完全根据块进行复制，很可能就会先复制了不重要的虚拟机。

如果用户想对虚拟机做区分，则可以使用虚拟机层面的异步复制。用户知道哪些虚拟机更重要一些，哪些虚拟机不重要，则可以先同步重要的虚拟机。

对业务来讲，如果用户可以根据业务层情况，在更细的粒度上区分数据是否重要。重要的数据，例如交易数据，需要优先同步；不重要的数据，例如日志数据，就不需要优先同步。

在有异地容灾的情况下，可以平时进行容灾演练，看容灾数据中心是否能够真正起作用，别容灾了半天，最后用的时候掉链子。

感谢第 14 讲至第 21 讲中对内容有深度思考和提出问题的同学。我会为你们送上奖励礼券和知识图谱。（稍后运营同学会发送短信通知。）

欢迎你继续提问！

我是如何创作“趣谈网络协议”专栏的？

我用将近半年的时间在“极客时间”写了一个专栏“趣谈网络协议”。对于我自己来讲，这真的是个非常特殊而又难忘的经历。

很多人都很好奇，这个专栏究竟是怎么一步步创作出来的，每一篇文章是怎么写出来的？自己录音频又是什么样的感受？写完整个专栏之后，我终于有时间回顾、整理一下这半年的所感所想。对我来说，这是一次难得的体验，也是一次与“极客时间”的深度沟通。

专栏是写给谁的？

和极客时间的编辑谈妥主题之后，他们首先要求我基于约定的主题，写一个 36 节至 50 节的大纲，之后会以每周三篇的频率，文字加音频的方式发布。每篇文章的体量要求在 3000 字左右，录成音频大约就是 10 分钟。

我本来觉得写这么一个专栏根本就不是个事儿。毕竟咱也是在 IT 圈摸爬滚打了许多年的“老司机”，干货积累得也不少。只要是熟悉的领域，不用准备，聊个把小时都没啥问题。况且我原来还写过书、写过博客、写过公众号。所以，我对自己文字方面的能力很有自信。

至于语言方面，咱常年出入各大技术论坛，什么场子没趟过。一个两天的线下培训，咱都能扛过来。每篇 10 分钟，总共 36 篇，那不才是 6 个小时嘛，肯定没问题。

但是，写了之后我发现，自己会是一回事儿，能讲给别人是另一回事儿，而能讲给“看不见的陌生人”听，是这世上最难的事儿。

我知道，很多技术人都有这样一个“毛病”，就是觉得掌握技术本身是最重要的，其他什么产品、市场、销售，都没技术含量。这种思维导致很多技术比较牛的人会以自我为中心，仅站在自己的角度思考问题。所以，常常是自己讲得很爽，完全不管听的人是不是真的接受了。写专栏的时候，这绝对是个大忌。

除此之外，这种思维对职业发展的影响也是很大的。单打独斗，一个人搞定一个软件的时代已经过去了。学会和别人合作，才是现代社会的生存法则，而良好的合作源于沟通。

但沟通不易，高质量的沟通更难。面对的人越多，沟通的难度就越大。因为每个人的背景、知识、基础都不同，想听的内容肯定更是千差万别。况且不是每个人都能准确地表达出自己的需求，加之需求的表达、转述都会因表达方式和传递媒介而发生变形，这样一来，接收信息的一方自然很难把握真实的需求。

写专栏的时候，“极客时间”的编辑不断地告诉我，我的受众只有一个人，就是“你”。我心想，这个简单啊，因为面对的人最少嘛！可是，事实上证明，我又“错”了。

这个抽象的“你”，看起来只有一个，其实却是看不到、摸不着的许许多多的人。所以，这个其实是最难的。协议专栏上线 10 天，就有 10000 多人订阅，而订阅专栏的用户里，只有少数人会留言。所以，对于很多读者的真实情况，我都无从得知，你可能每天都听但是没有留言的习惯，也可能买了之后觉得我讲得不好，骂一句“这钱白花了”，然后再也不听。

所以，如何把控内容，写给广大未知受众，是我写这个专栏面临的最大挑战。而这里面，文章的深度、广度，音频的语调、语气，每一个细节都非常重要。

专栏文章是怎么写的？

经过大纲和前几篇文稿的打磨，我对“极客时间”和专栏创作也有了更深的了解。我私下和很多人交流过一个问题，那就是，咱们平时聊一个话题的时候，有很多话可以说。但是真正去写一篇文章的时候，好像又没有什么可讲的，尤其是那些看起来很基础的内容。

我在写专栏的过程中，仔细思考过这样一个问题：很多人对某一领域或者行业研究得很深入，也有自己长期的实践，但是有多少人可以从感性认识上升到理性认知的高度呢？

现在技术变化这么快，我们每个人的精力都是有限的，不少人学习新知识的方式就是看看书，看看博客、技术文章，或者听同事讲一下，了解个大概就觉得可以直接上手去做了。我也是这样的。可是一旦到写专栏的时候，基础掌握不扎实的问题一下子全都“暴露”出来了。

落到文字上的东西一定要是严谨的。所以，在写到很多细节的时候，我查了大量的资料，找到权威的书籍、官方文档、RFC 里面的具体描述，有时候我甚至要做个实验，或者打开代码再看一下，才放心下笔。

尽管我对自己写文章有很多“完美倾向”的要求，但是这其实依旧是站在我自己的角度去看的。读者究竟想要看什么内容呢？

太深入了，看不懂；太浅显了，也不行。太长了，负担太重；太短了，没有干货；同时，每篇文字还要自成一体，所有文章要是一个完整的知识体系。我发现，原来我不仅是对知识的了解没那么全面、具体，对用户阅读和倾听场景也没有过多的考虑。

除了写文字，专栏还要录音频，所以为了方便“听”，文章内不能放大量代码、实验。如果很多人在通勤路上听，而我把一张图片讲得天花乱坠，听的人却根本看不到，那肯定是不行的，所以写文章的时候，我还要把故事性、画面感都考虑进去，尽量详尽而不啰嗦。

把这些限制条件加起来之后，我发现，写专栏这件事儿，真的太不容易了。每篇文章看起来内容不多，但是都是费了很多心思的，这也是为什么很多老师说，写完专栏就像是过了火焰山。

专栏音频是怎么录的？

说完写文章，我来说说录音频。我平时听播音员说话，感觉非常轻松，所以当时我毫不犹豫地就说，“我要自己录”。但是在录开篇词的时候，我就觉得这完全不是我想的那么回事啊！

专栏的文章在录音的时候一定会有个“音频稿”，我一开始很不理解，我对着发布的稿件直接讲就好了啊，为什么还要特意准备一个供录音频的稿件啊？

我在没有音频稿的情况下，自己试着“发挥”了几次，结果，我发现我的嘴会“吃”字，会反复讲一个内容而且表达不清，但是自己却经常毫无察觉，还会自己讲着讲着就收不住等等。

咱们平时说话的时候，会有很多口头语和重复的词语。面对面交流的时候，我们为什么没有注意这个问题呢？因为我们会更注重对方的表情、手势，但是一旦录成音频，这些“啰嗦”的地方就特别明显。

而有了音频稿之后，整个过程就严谨很多。如果哪句话说错了，看着稿件再说一遍就好了。而且，你会发现录音的时间大大缩短了，原来需要用十分钟，现在五分钟就可以很精炼地讲出来了。

有了稿子，那我是不是对着念就好了？这不是很容易吗？不，我又遇到了新的难题。

录音频的时候，我常常一个人关在密闭的房间里，对着显示器“读”，这和公共演讲肯定是不一样的。加上因为有写好的音频稿，我常常感觉束手束脚，找不到演讲那种有激情的感觉，很容易就变成了念课文。

为了同时满足自然和严谨，一方面我会先熟记“台词”；另一方面，每次录的时候，我都假想对面有个人，我在对着他缓缓地讲出来。讲到某些地方，我还会假想他对这个知识点是不是有疑问，这样就更加有互动感。

录音频这件事对我的改变非常大。我说话、演讲的时候变得更加严谨了。我会下意识地不去重复已经说过的话。一旦想重复，也闭嘴不发音，等想好了下一句再说。后面，我的录音也越来越顺利，一开始要录五六遍才能成功，后面基本一遍就过了。

创作专栏的过程还有许多事情，都是我很难得的记忆。我很佩服“极客时间”的编辑做专栏时的专业和认真。我也很庆幸，我没有固执地按照自己认为正确的方向和方式来做，而是尊重了他们的专业。很显然，他们没有我懂技术，但是他们比我更懂“你”。

专栏结束后，我回看这半年的准备和努力，我发现，无论对自己的领域多么熟悉，写这个专栏都让我又上升了一个新高度。

我知道很多技术人都喜欢分享，而写文章又是最容易实现的方式。写文章的时候，可以检验你对基础知识的掌握是否扎实，是不是有换位思考能力，能不能从感性认识上升到理性认知。

除此之外，我觉得最重要的一点是，在创作专栏文章的过程中，我学到了很多技术之外的东西，比如换位思考能力和细节把控的能力。

我在这里记下与“极客时间”的相识和相知。希望看到更多人在极客时间，分享自己的知识和见解。

测一测 | 这些网络协议你都掌握了吗？

经过三个多月的学习，相信你对网络协议的基础概念和使用场景有了更深入的了解。我从专栏中精心筛选了核心知识点，编成了这 10 道测试题。希望可以帮助你学习自检，消化吸收，以期获得更好的学习效果。

如果你刚刚打开这个专栏，可以用这 10 道题，找到自己的薄弱点，对症下药；如果你已经学习了一段时间，可以用这 10 道题，检测一下学习成果，查漏补缺。

你可以拿出纸笔，写下自己的回答，之后再与文末的答案进行对照。

1.（多选）下列哪种方式可以给网卡配置 IP 地址？

A. 通过 DHCP 协议

B. 通过 ip addr 命令

C. 通过 PXE

D. 通过 Ubuntu 的 Interface 文件配置

2.（多选）下列哪种方式可以配置 VLAN？

A. 通过交换机可以配置某个口的 VLAN

B. 通过网桥的 tag

C. 通过 vconfig 命令

D. 通过 VTEP

3.（单选）对于网关和路由器，下列说法正确的是

A. 网关和路由器是一回事儿

B. 网关是三层的，路由器可以是三层的，也可以是二层的

C. 网关是路由器的一个接口

D. 网关可以做 NAT，路由器不能

4.（多选）对于路由协议，下列描述正确的有

A. 路由协议主要是用来寻找最短路径

B. 最短路径往往只有一条

C. 数据中心内部和外部往往使用不同的路由协议

D. 边界路由器只把部分 IP 告知外面的网络

5.（单选）对于传输层协议，下列描述正确的是

A. 传输层只有两种协议：TCP 和 UDP

B. 由于 UDP 过于简单，因而只在数据中心内部使用

C. TCP 通过 Sequence Number 标识重传和回复的包

D. TCP 通过滑动窗口解决网络拥塞的问题

6.（多选）对于 HTTP 协议，下列描述正确的有

A. HTTP 协议是基于 TCP 协议的

B. TCP 的重传机制会导致 HTTP 服务端收到重复的包，因而需要实现幂等

C. HTTP 的 POST 方法可以用来发送数据，也可以用来请求数据

D. HTTP 协议主要用来传输 HTML 网页

7.（多选）对于 Socket，下列描述正确的有

A. Socket 可以建立 TCP 连接，也可以建立 UDP 连接，所以它在传输层工作

B. 对于 Linux 内核来看，Socket 也是一个文件

C. 启动一个 Socket 来监听，当连接建立之后，会将这个 socket 交给一个线程来处理

D. UDP 的 Socket 可以接收来自多个源的网络包

8.（单选）对于 DNS 协议，下列描述正确的是

A. 如果没有 DNS，服务器之间无法通信

B. 客户端端需要去轮询 DNS 树进行域名解析

C. DNS 既可以做本地负载均衡，也可以做全局负载均衡

D. 只要修改了 DNS 域名对应的 IP 地址，马上就起作用

9.（多选）对于 CDN，下列描述正确的有

A. 使用了 CDN 之后，访问的域名会发生变化

B. CDN 只能用来加速静态数据

C. CDN 厂商的节点分布在各个地区的各个运营商

D. 相同运营商比相同地点有更高优先级

10.（单选）对于移动通信网络，下列描述正确的是

A. 移动通信网络也是在运营商，因而一旦到了有线部分，就都一样了

B. 之所以分 SGW 和 PGW，主要是解决异地上网的问题

C. 手机的 IP 地址是公网 IP 地址，是由互联网上的 DHCP 服务器分配的

D. 手机上网不稳定，因而所有的协议都要基于 TCP，不然一旦丢包就麻烦了

答案与解析

1.（多选）答案. ABD

解析：配置的参数如果是 DHCP，会通过 DHCP 协议自动配置 IP 地址；如果是 Static 会通过 ip addr 命令行进行手动配置。而 PXE 不是用于配置 IP 地址的，是用于物理机初始化的。

2.（多选）答案. ABC

解析：物理交换机可以配置 VLAN，brctl 里面的 tag 可以配置 VLAN，vconfig 可以配置一个带 VLAN ID 的虚拟网卡。VTEP 是用来封装 VXLAN 的。

3.（单选）答案. C

解析：网关和路由器都是三层的，都可以做 NAT，网关往往是路由器在某个局域网里面的口。

4.（多选）答案. CD

解析：数据中心内部和外部使用的路由协议不太一样。内部的协议主要是用来寻找最短路径，外部的协议往往会有一些策略，可以选择将部分的 IP 地址广播出去。

5.（单选）答案. C

解析：传输层常用的两种协议是 TCP 和 UDP，其实还有其他的，例如 SCTP。UDP 比较简单，常用于内网，但是它可以让应用层自己实现可靠连接和拥塞控制，因此也常在数据中心外使用。TCP 通过 Sequence Number 和滑动窗口，解决重传和回复的问题，而拥塞窗口解决的是网络拥塞问题。

6.（多选）答案. AC

解析：HTTP 是基于 TCP 的。TCP 虽然会重传，但是只有一个包可以到达 HTTP 层，所以 HTTP 层不会收到重复的包。为了防止重新发送 HTTP 的请求，需要实现幂等。POST 仅仅是一个方法，实现可以很灵活。SOAP 协议中常用 POST 实现调用 get 函数。HTTP 不仅仅是传输 HTML 的，可以传输很多东西，比如视频、JSON、XML 等。

7. （多选）答案. BD

解析：Socket 可以建立 TCP 和 UDP 连接，但是它处于应用层。Socket 连接后会生成一个文件描述符，所以从内核来看，它只是一个文件。监听的 Socket 和读写的 Socket 并不是同一个。UDP 的 Socket 不是面向连接的，它可以接收来自多个源的网络包。

8.（单选）答案. C

解析：没有 DNS，上网就会很不方便，但是使用 IP 地址，仍然可以互相通信。轮询 DNS 树的不是客户端，而是本地 DNS 服务器。DNS 可以在数据中心里面做本地负载均衡，也可以做跨数据中心的全局负载均衡。修改 DNS 域名对应的 IP 之后，过一段时间才会起作用。

9.（多选）答案. ACD

解析：使用了 CDN 之后，域名会 CNAME 成为 CDN 的域名。CDN 可以加速静态数据，也可以加速动态数据。CDN 节点分布在各个地区和运营商。相同的运营商路径更短一些。有时候相同的地区，不同的运营商不能直接连接，而是要到两个运营商互通的地方转一下，所以相同运营商更加重要一些。而对于同一个运营商，地区比较重要。

10.（单选）答案. B

解析：移动通信网络有无线的部分、核心网的部分、互联网的部分，不是到了有线就都一样了。区分 SGW 和 PGW 可以解决异地上网的问题，SGW 是服务本地 PGW 的登记地结算的。手机的 IP 地址是 PGW 分配的。手机里面的 GTP 协议是基于 UDP 的。

第2季回归 | 这次我们来“趣谈Linux操作系统”

你好，我是你的老朋友刘超。在“趣谈网络协议”结课半年之后，我又给你带来了一个新的基础课程，“趣谈 Linux 操作系统”。

在咱们“趣谈网络协议”的留言里，我和同学们进行了很多互动，同时，我也和其他做基础知识专栏的作者有了不少交流，我发现，无论是从个人的职业发展角度，还是从公司招聘候选人的角度来看，扎实的基础知识是很多人的诉求。这让我更加坚信，我应该在“趣谈基础知识”这条道路上走下去。

在设计“趣谈 Linux 操作系统”专栏的时候，我仍然秉承“趣谈”和“故事化”的方式，将枯燥的基础知识结合某个场景，给你生动、具象地讲述出来，帮你加深理解、巩固记忆、夯实基础。

在我看来，操作系统在计算机中承担着“大管家”的角色，这个“大管家”就好比一家公司的老板，我们的目标就是把这家公司做上市，具体的过程，我用了一张图来表示：

Linux 操作系统中的概念非常多，数据结构也很多，流程也复杂，一般人在学习的过程中很容易迷路。我希望能够将这些复杂的概念、数据结构、流程通俗地讲解出来，争取每篇文章都用一张图串起这篇的知识点。

最终，整个专栏下来，你如果能把这些图都掌握了，你的知识就会形成体系和连接。在此基础上再进行深入学习，就会如鱼得水、易如反掌。

一段新的征途即将开始，期待与你继续同行。为了感谢老同学，我为你送上一张10 元专属优惠券，可以与限时优惠同享，优惠券有效期仅5 天，建议你抓紧使用。点击下方图片，可试读专栏最新文章。

我们新专栏见！

结束语 | 放弃完美主义，执行力就是限时限量认真完成

你好，我是刘超。

从筹备、上线到今天专栏完结，过去了将近半年的时间。200 多天，弹指一挥间。

我原本计划写 36 篇，最后愣是写到了 45 篇。原本编辑让我一篇写两三千字，结果几乎每篇都是四五千字。这里面涉及图片张数我没具体数过，但是据说多到让编辑上传到吐。编辑一篇我的稿件的工作量相当于别的专栏的两倍。

人常说，有多少付出，就有多少回报。但是，写这个“趣谈网络协议”专栏，我收获的东西远超过我的想象。我希望你的收获也是如此。为什么这么说呢？我们把时间放回到这个专栏最开始的时候，我慢慢跟你讲。

我不是最懂的人，但我想尝试成为这样的人

今年年初，极客时间来找我，希望我讲一些偏重基础的知识，比如网络协议。

他们一提到这个主题，我就很兴奋，因为这也触动了我心中长期以来的想法，因为网络这个东西学起来实在是太痛苦。

但是，说实话，接下这个重任，我心里其实是有点“怕”的。我怕自己不够专业，毕竟业内有这么多网络工程师和研究网络理论的教授。我讲这个课会不会贻笑大方啊？

我知道，很多技术人员不敢写博客、写公众号，其实都有这种“怕”的心理：我又不牛，没啥要分享的，要是误导了别人怎么办？

如果你想做出一些成绩，这个心理一定要克服。其实每个人都有自己的相对优势。对于某个东西，你研究的时间不一定是最长的，但是你可能有特殊的角度、表达方式和应用场景。坚定了这个想法之后，我就开始投入热火朝天的专栏写作了。

一旦开始写，我发现，这个事情远没有看上去那么简单。它会花费你非常多的个人时间。写专栏这几个月，晚上两点之后睡，周末全在写专栏，基本成为我的生活常态。但是我想挑战一下自己，我觉得，只要咬牙挺过去，自己的技术就会上升一个层次。

放弃完美主义，执行力就是限时限量认真完成

技术人都有完美主义倾向，觉得什么事情都要钻研个底儿朝天，才拿出来见人。我也一样。

我曾经答应某出版社写一本搜索引擎的书。这本书分为原理篇和实践篇。我总觉得我还没把原理篇写完，就不能写实践篇。但是，仅原理篇我就写了一年。搜索引擎就火了一两年，最后时间窗口过了，书稿没有完成，这件事儿也就这么搁浅了。

所以，完美主义虽然是个很好听的词，但是它往往是和拖延症如影随形的，它常常会给拖延症披上一个华丽的外衣，说，我是因为追求完美嘛。但是，最终的结果往往是，理论研究半天还没动手，执行力很差。时间点过了，就心安理得地说，反正现在也不需要了，那就算了吧。久而久之，你就会发现，自己好像陷入了瓶颈。

我慢慢明白过来，我们不是为了做技术而做技术，做技术是为了满足人类需求的。完美主义是好事儿，但是，坚持完美主义的同时要限时限量地完成，才能形成执行力。

写这个专栏之后，我更加深刻地体会到这一点。每周都要写三篇文章，压力很大，根本容不得任何拖延。如果我还是坚持以前完美主义的做法，读完十本书，用三年时间把网络协议都研究透再来写，那现在就没有这个专栏了。

如果我们要强调执行力，时间点这个因素就至关重要。在固定的时间点上，就要把控范围，不能顾虑太多，要勇于放弃。就像给产品做排期，先做最小闭环的功能集合，其他的放在以后再补充。在这个前提下，以自己最大的限度往完美的方向上努力。比如，我觉得每天 2 点睡是我的身体极限，努力到这个程度，我也就无愧于心了。

所以说，我们做事情的目的并不是完美，而是在固定的时间点，以固定的数量和质量，尽可能认真地满足当时的客户需求，这才是最重要的。

这样做肯定会有不满意的地方，比如很多同学在留言区指出我的错误，甚至有的同学提的问题，我原来都没思考过。但是，我觉得这些都不是事儿。我可以再查资料，再补充、再完善。所以，后来时间宽裕了，我还增加了 5 期答疑，回答了一下之前没来得及回答地问题。高手在民间，咱们一起来讨论和进步。这个过程已经让我受益良多。

保持饥渴，不怕被“鄙视”，勇于脱离舒适区

有人可能会问了，你看你既不是最专业的，还不追求完美，真的不怕被人“鄙视”吗？

被“鄙视”，谁都怕，这也是为什么越大的会议，参加人数越多的演讲，越是没有人提出具体的问题。大家都怕丢人，看上去好像大家都听懂了，就我啥都不懂，我要是问，被大家笑话怎么办？我想很多人都有这样的经历吧？我也来给你讲讲我的亲身经历。

我从 Windows 开发去做 Linux 的存储系统开发时，连 Linux man 都不会看；我在惠普从事 OpenStack 实施工作的时候，对于网络的了解一塌糊涂，一直被甲方骂；我在华为做云计算，支撑运营商项目的时候，面对一大堆核心网词汇，一脸懵；我在网易云对内支撑考拉的时候，在微服务架构方面也是小白……被“鄙视”了这么多次之后，我不怕了。因为这每一次“鄙视”都可以让我发现自己的短板，然后啃下这些东西，这不就是最大的收获吗？

我就是这样一直被“鄙视”着成长起来的人，我就是常常在别人分享的时候坐第一排问很傻的那种问题的人，我就是常常一知半解还愿意和别人讨论的人……

怕被“鄙视”，说明你还不够饥渴，还没有勇气脱离你的舒适区。 在你熟悉的领域里面，你是最最权威的，但是，天下之大，你真的只满足于眼前这一亩三分地吗？

很多人因为怕被“鄙视”，不敢问、不敢做，因而与很多美好的东西都擦肩而过了。直到有一天你用到了，你才后悔，当时自己怎么没去多问一句。

所以，当你看到一个特别好的、突破自己的学习机会，别犹豫，搭上这辆车。等过了十年，你会发现，当年那些嘲笑、轻视，甚至谩骂，都算不了什么，进步本身才是最最重要的。

今天，咱们没有谈具体的知识，我只表达了一下我的观点。我就是那个你在直播里看到的，那个邋遢、搞笑、不装，同时做事认真，愿意和你一起进步的技术大叔。

脱离舒适区吧，希望我们可以一起成长！