《网络是怎样连接的》
约 15385 字大约 51 分钟
2025-04-01
《网络是怎样连接的》 - [日] 户根勤
本书以探索之旅的形式,从在浏览器中输入网址开始,一路追踪了到显示出网页的内容为止的整个过程,以图配文,讲解了网络的全貌,并重点介绍了实际的网络设备和软件是如何工作的.
第一章 浏览器生成消息——探索浏览器内部
1.1 生成 HTTP 请求消息
URL:Uniform Resource Locator,统一资源定位符。以 http:// 开头的那一串东西,除了“http:”, 网址还可以以其他一些文字开头, 例如“ftp:”“file:” “mailto:” 等。
URI:Uniform Resource Identifier,统一资源标识符。URI 的内容是一个存放网页 数据的文件名或者是一个 CGI 程序的文件名,例如“/dir1/file1.html” “/dir1/program1.cgi”等 。这里可以写各种访问目标,而这些访问目标统称为 URI。
HTTP 常用方法:
- GET 方法:当我们访问 Web 服务器获取网页数据时,使用的就是 GET 方法。
- POST 方法:我们在表单(文本框、复选框等能够输入数据的部分)中填写数据并将其发送给 Web 服务器时就会使用这个方法。
由于每条请求消息中只能写 1 个 URI,所以每次只能获取 1 个文件,如果需要获取多个文件,必须对每个文件单独发送 1 条请求。比如 1 个网页中包含 3 张图片,那么获取网页加上获取图片,一共需要向 Web 服务器发送 4 条请求。
判断所需的文件,然后获取这些文件并显示在屏幕上,这一系列工作的整体指挥也是浏览器的任务之一,而 Web 服务器却毫不知情。Web 服务器完全不关心这 4 条请求获取的文件到底是 1 个网页上的还是不同网页上的,它的任务就是对每一条单独的请求返回 1 条响应而已。
1.2 向 DNS 服务器查询 Web 服务器的 IP 地址
生成 HTTP 消息之后,接下来我们需要委托操作系统将消息发送给 Web 服务器。尽管浏览器能够解析网址并生成 HTTP 消息,但它本身并不具备将消息发送到网络中的功能,因此这一功能需要委托操作系统来实现 。
在进行这一操作时,我们还有一个工作需要完成,那就是查询网址中服务器域名对应的 IP 地址。在委托操作系统发送消息时,必须要提供的不是通信对象的域名,而是它的 IP 地址。因此,在生成 HTTP 消息之后,下一个步骤就是根据域名查询 IP 地址。
TCP/IP 的结构:由一些小的子网,通过路由器连接起来组成一个大的网络。这里的子网可以理解为用集线器连接起来的几台计算机,我们将它看作一个单位,称为子网。将子网通过路由器连接起来,就形成了一个网络。
IP 地址的主机号
- 全 0:表示整个子网
- 全 1:表示向子网上所有设备发送包,即“广播”
现在我们使用的方案是让人来使用名称,让路由器来使用 IP 地址。为了填补两者之间的障碍,需要有一个机制能够通过名称来查询 IP 地址,或者通过 IP 地址来查询名称,这样就能够在人和机器双方都不做出牺牲的前提下完美地解决问题。这个机制就是 DNS。
DNS:Domain Name System,域名服务系统。将服务器名称和 IP 地址进行关联是 DNS 最常见的用法,但 DNS 的功能并不仅限于此,它还可以将邮件地址和邮件服务器进行关联,以及为各种信息关联相应的名称。
首先,库到底是什么东西呢?
- 库就是一堆通用程序组件的集合,其他的应用程序都需要使用其中的组件。
库有很多好处:
- 首先,使用现成的组件搭建应用程序可以节省编程工作量;
- 其次,多个程序使用相同的组件可以实现程序的标准化。
- 除此之外还有很多其他的好处,因此使用库来进行软件开发的思路已经非常普及,库的种类和数量也非常之多。
Socket 库是用于调用网络功能的程序组件集合。
计算机的内部结构就是这样一层一层的。也就是说,很多程序组成不同的层次,彼此之间分工协作。当接到上层委派的操作时,本层的程序并不会完成所有的工作,而是会完成一部分工作,再将剩下的部分委派到下层来完成。
顺带一提,向 DNS 服务器发送消息时,我们当然也需要知道 DNS 服务器的 IP 地址。只不过这个 IP 地址是作为 TCP/IP 的一个设置项目事先设置好的,不需要再去查询了。
1.3 全世界 DNS 服务器的大接力
例如,如果要查询 www.lab.glasscom.com 这个域名对应的 IP 地址,客户端会向 DNS 服务器发送包含以下信息的查询消息。
- (a)域名 = www.lab.glasscom.com
- (b)Class = IN (代表互联网)
- (c)记录类型 = A (A 是 Address 的缩写)
然后,DNS 服务器会从已有的记录中查找域名、Class 和记录类型全 部匹配的记录。假如 DNS 服务器中的记录如图 1.14 所示,那么第一行记 录与查询消息中的 3 个项目完全一致。于是,DNS 服务器会将记录中的 192.0.2.226 这个值返回给客户端。
当类型为 A 时,表示域名对应的是 IP 地址;
当类型为 MX 时,表示域名对应的是邮件服务器,DNS 服务器会在记录中保存两种信息,分别是邮件服务器的域名和优先级。(当一个邮件地址对应多个邮件服务器时,需要根据优先级来判断哪个邮件服务器是优先的。优先级数值较小的邮件服务器代表更优先。)
前面只介绍了 A 和 MX 这两个记录类型,实际上还有很多其他的类型。例如根据 IP 地址反查域名的 PTR 类型,查询域名相关别名的 CNAME 类型,查询 DNS 服务器 IP 地址的 NS 类型,以及查询域名属性信息的 SOA 类型等。
尽管 DNS 服务器的工作原理很简单,不过是根据查询消息中的域名和记录类型来进行查找并返回响应的信息而已,但通过组合使用不同的记录类型,就可以处理各种各样的信息。
在前面的讲解中,似乎 com、jp 这些域(称为顶级域)就是最顶层了,它们各自负责保存下级 DNS 服务器的信息,但实际上并非如此。在互联网中,com 和 jp 的上面还有一级域,称为根域。根域不像 com、jp 那样有自己的名字,因此在一般书写域名时经常被省略,如果要明确表示根域,应该像 www.lab.glasscom.com. 这样在域名的最后再加上一个句点,而这个最后的句点就代表根域。不过,一般都不写最后那个句点,因此根域的存在往往被忽略,但根域毕竟是真实存在的,根域的 DNS 服务器中保管着 com、jp 等的 DNS 服务器的信息。由于上级 DNS 服务器保管着所有下级 DNS 服务器的信息,所以我们可以从根域开始一路往下顺藤摸瓜找到任意一个域的 DNS 服务器。
现实中上级域和下级域有可能共享同一台 DNS 服务器。在这种情况下,访问上级 DNS 服务器时就可以向下跳过一级 DNS 服务器,直接返回再下一级 DNS 服务器的相关信息。
有时候并不需要从最上级的根域开始查找,因为 DNS 服务器有一个缓存功能,可以记住之前查询过的域名。如果要查询的域名和相关信息已经在缓存中,那么就可以直接返回响应,接下来的查询可以从缓存的位置开始向下进行。相比每次都从根域找起来说,缓存可以减少查询所需的时间。
这个缓存机制中有一点需要注意,那就是信息被缓存后,原本的注册信息可能会发生改变,这时缓存中的信息就有可能是不正确的。因此,DNS 服务器中保存的信息都设置有一个有效期,当缓存中的信息超过有效期后,数据就会从缓存中删除。而且,在对查询进行响应时,DNS 服务器也会告知客户端这一响应的结果是来自缓存中还是来自负责管理该域名的 DNS 服务器。
1.4 委托协议栈发送消息
收发数据的整体思路就是这样,但还有一点也非常重要。光从图上来看,这条管道好像一开始就有,实际上并不是这样,在进行收发数据操作之前,双方需要先建立起这条管道才行。建立管道的关键在于管道两端的数据出入口,这些出入口称为套接字。我们需要先创建套接字,然后再将套接字连接起来形成管道。实际的过程是下面这样的。首先,服务器一方先创建套接字,然后等待客户端向该套接字连接管道(服务器程序一般会在启动后就创建好套接字并等待客户端连接管道)。当服务器进入等待状态时,客户端就可以连接管道了。具体来说,客户端也会先创建一个套接字,然后从该套接字延伸出管道,最后管道连接到服务器端的套接字上。当双方的套接字连接起来之后,通信准备就完成了。接下来,就像我们刚刚讲过的一样,只要将数据送入套接字就可以收发数据了。
管道在连接时是由客户端发起的,但在断开时可以由客户端或服务器任意一方发起。其中一方断开后,另一方也会随之断开,当管道断开后,套接字也会被删除。到此为止,通信操作就结束了。
综上所述,收发数据的操作分为若干个阶段,可以大致总结为以下 4 个。
- (1)创建套接字(创建套接字阶段)
- (2)将管道连接到服务器端的套接字上(连接阶段)
- (3)收发数据(通信阶段)
- (4)断开管道并删除套接字(断开阶段)
首先是套接字创建阶段。客户端创建套接字的操作非常简单,只要调用 Socket 库中的 socket 程序组件就可以了。
描述符是用来识别不同的套接字的,大家可以作如下理解。我们现在只关注了浏览器访问 Web 服务器的过程,但实际上计算机中会同时进行多个数据的通信操作,比如可以打开两个浏览器窗口,同时访问两台 Web 服务器。这时,有两个数据收发操作在同时进行,也就需要创建两个不同的套接字。
应用程序是通过“描述符”这一类似号码牌的东西来识别套接字的。
接下来,我们需要委托协议栈将客户端创建的套接字与服务器那边的套接字连接起来。应用程序通过调用 Socket 库中的名为 connect 的程序组 件来完成这一操作。这里的要点是当调用 connect 时,需要指定描述符、 服务器 IP 地址和端口号这 3 个参数。
第 1 个参数,即描述符,就是在创建套接字的时候由协议栈返回的那个描述符。connect 会将应用程序指定的描述符告知协议栈,然后协议栈根据这个描述符来判断到底使用哪一个套接字去和服务器端的套接字进行连接,并执行连接的操作 。
第 2 个参数,即服务器 IP 地址,就是通过 DNS 服务器查询得到的我们要访问的服务器的 IP 地址。在 DNS 服务器的部分已经讲过,在进行数据收发操作时,双方必须知道对方的 IP 地址并告知协议栈。这个参数就是那个 IP 地址了。
第 3 个参数,即端口号,这个需要稍微解释一下。可能大家会觉得,IP 地址就像电话号码,只要知道了电话号码不就可以联系到对方了吗?其 实,网络通信和电话还是有区别的,我们先来看一看 IP 地址到底能用来干什么。IP 地址是为了区分网络中的各个计算机而分配的数值 。因此,只要知道了 IP 地址,我们就可以识别出网络上的某台计算机。但是,连接操作的对象是某个具体的套接字,因此必须要识别到具体的套接字才行,而仅凭 IP 地址是无法做到这一点的。我们打电话的时候,也需要通过“请帮我找一下某某某”这样的方式来找到具体的某个联系人,而端口号就是这样一种方式。当同时指定 IP 地址和端口号时,就可以明确识别出某台具体的计算机上的某个具体的套接字。
如果说描述符是用来在一台计算机内部识别套接字的机制,那么端口号就是用来让通信的另一方能够识别出套接字的机制。
只要指定了事先规定好的端口号,就可以连接到相应的服务器
程序的套接字。
描述符:应用程序用来识别套接字的机制
IP 地址和端口号:客户端和服务器之间用来识别对方套接字的机制
HTTP 协议将 HTML 文档和图片都作为单独的对象来处理,每获取一次数据,就要执行一次连接、发送请求消息、接收响应消息、断开的过程。因此,如果一个网页中包含很多张图片,就必须重复进行很多次连接、收发数据、断开的操作。对于同一台服务器来说,重复连接和断开显然是效率很低的,因此后来人们又设计出了能够在一次连接中收发多个请求和响应的方法。在 HTTP 版本 1.1 中就可以使用这种方法,在这种情况下,当所有数据都请求完成后,浏览器会主动触发断开连接的操作。
本章我们探索了浏览器与 Web 服务器之间收发消息的过程,但实际负责收发消息的是协议栈、网卡驱动和网卡,只有这 3 者相互配合,数据才能够在网络中流动起来。
第二章 用电信号传输 TCP/IP 数据——探索协议栈和网卡
2.1 创建套接字
应用程序调用 socket 申请创建套接字,协议栈根据应用程序的申请执行创建套接字的操作。
在这个过程中,协议栈首先会分配用于存放一个套接字所需的内存空间。用于记录套接字控制信息的内存空间并不是一开始就存在的,因此我们先要开辟出这样一块空间来,这相当于为控制信息准备一个容器。但光一个容器并没有什么用,还需要往里面存入控制信息。套接字刚刚创建时,数据收发操作还没有开始,因此需要在套接字的内存空间中写入表示这一初始状态的控制信息。到这里,创建套接字的操作就完成了。
接下来,需要将表示这个套接字的描述符告知应用程序。描述符相当于用来区分协议栈中的多个套接字的号码牌。
收到描述符之后,应用程序在向协议栈进行收发数据委托时就需要提供这个描述符。由于套接字中记录了通信双方的信息以及通信处于怎样的状态,所以只要通过描述符确定了相应的套接字,协议栈就能够获取所有的相关信息,这样一来,应用程序就不需要每次都告诉协议栈应该和谁进行通信了。
2.2 连接服务器
创建套接字之后,应用程序(浏览器)就会调用 connect,随后协议栈会将本地的套接字与服务器的套接字进行连接。
套接字刚刚创建完成的时候,里面并没有存放任何数据,也不知道通信的对象是谁。在这个状态下,即便应用程序要求发送数据,协议栈也不知道数据应该发送给谁。浏览器可以根据网址来查询服务器的 IP 地址,而且根据规则也知道应该使用 80 号端口,但只有浏览器知道这些必要的信息是不够的,因为在调用 socket 创建套接字时,这些信息并没有传递给协议栈。因此,我们需要把服务器的 IP 地址和端口号等信息告知协议栈,这是连接操作的目的之一。
那么,服务器这边又是怎样的情况呢?服务器上也会创建套接字(服务器程序一般会在系统启动时就创建套接字并等待客户端连接),但服务器上的协议栈和客户端一样,只创建套接字是不知道应该和谁进行通信的。而且,和客户端不同的是,在服务器上,连应用程序也不知道通信对象是谁,这样下去永远也没法开始通信。于是,我们需要让客户端向服务器告知必要的信息,比如“我想和你开始通信,我的 IP 地址是 xxx.xxx.xxx.xxx,端口号是 yyyy。”可见,客户端向服务器传达开始通信的请求,也是连接操作的目的之一。
连接实际上是通信双方交换控制信息
首先,客户端先创建一个包含表示开始数据收发操作的控制信息的头部。如表 2.1 所示,头部包含很多字段,这里要关注的重点是发送方和接收方的端口号。到这里,客户端(发送方)的套接字就准确找到了服务器(接收方)的套接字,也就是搞清楚了我应该连接哪个套接字。然后,我们将头部中的控制位的 SYN 比特设置为 1,大家可以认为它表示连接。
当 TCP 头部创建好之后,接下来 TCP 模块会将信息传递给 IP 模块并委托它进行发送。IP 模块执行网络包发送操作后,网络包就会通过网络到达服务器,然后服务器上的 IP 模块会将接收到的数据传递给 TCP 模块,服务器的 TCP 模块根据 TCP 头部中的信息找到端口号对应的套接字,也就是说,从处于等待连接状态的套接字中找到与 TCP 头部中记录的端口号相同的套接字就可以了。当找到对应的套接字之后,套接字中会写入相应的信息,并将状态改为正在连接。上述操作完成后,服务器的 TCP 模块会返回响应,这个过程和客户端一样,需要在 TCP 头部中设置发送方和接收方端口号以及 SYN 比特。此外,在返回响应时还需要将 ACK 控制位设为,这表示已经接收到相应的网络包。网络中经常会发生错误,网络包也会发生丢失,因此双方在通信时必须相互确认网络包是否已经送达,而设置 ACK 比特就是用来进行这一确认的。接下来,服务器 TCP 模块会将 TCP 头部传递给 IP 模块,并委托 IP 模块向客户端返回响应。
然后,网络包就会返回到客户端,通过 IP 模块到达 TCP 模块,并通过 TCP 头部的信息确认连接服务器的操作是否成功。如果 SYN 为 1 则表示连接成功,这时会向套接字中写入服务器的 IP 地址、端口号等信息,同时还会将状态改为连接完毕。到这里,客户端的操作就已经完成,但其实还剩下最后一个步骤。刚才服务器返回响应时将 ACK 比特设置为 1,相应地,客户端也需要将 ACK 比特设置为 1 并发回服务器,告诉服务器刚才的响应包已经收到。当这个服务器收到这个返回包之后,连接操作才算全部完成。
现在,套接字就已经进入随时可以收发数据的状态了,大家可以认为这时有一根管子把两个套接字连接了起来。当然,实际上并不存在这么一根管子,不过这样想比较容易理解,网络业界也习惯这样来描述。这根管子,我们称之为连接。只要数据传输过程在持续,也就是在调用 close 断开之前,连接是一直存在的。建立连接之后,协议栈的连接操作就结束了,也就是说 connect 已经执行完毕,控制流程被交回到应用程序。
这就是 TCP 三次握手建立连接
2.3 收发数据
当控制流程从 connect 回到应用程序之后,接下来就进入数据收发阶段了。数据收发操作是从应用程序调用 write 将要发送的数据交给协议栈开始的,协议栈收到数据后执行发送操作。
2.4 从服务器断开并删除套接字
总结:
数据收发操作的第一步是创建套接字。一般来说,服务器一方的应用程序在启动时就会创建好套接字并进入等待连接的状态。客户端则一般是在用户触发特定动作,需要访问服务器的时候创建套接字。在这个阶段,还没有开始传输网络包。创建套接字之后,客户端会向服务器发起连接操作。首先,客户端会生成一个 SYN 为 1 的 TCP 包并发送给服务器(图 2.13 ①)。这个 TCP 包的头部还包含了客户端向服务器发送数据时使用的初始序号,以及服务器向客户端发送数据时需要用到的窗口大小。当这个包到达服务器之后,服务器会返回一个 SYN 为 1 的 TCP 包(图 2.13 ②)。和图 2.13 ①一样,这个包的头部中也包含了序号和窗口大小,此外还包含表示确认已收到包①的 ACK 号 。当这个包到达客户端时,客户端会向服务器返回一个包含表示确认的 ACK 号的 TCP 包(图 2.13 ③)。到这里,连接操作就完成了,双方进入数据收发阶段。
数据收发阶段的操作根据应用程序的不同而有一些差异,以 Web 为例,首先客户端会向服务器发送请求消息。TCP 会将请求消息切分成一定大小的块,并在每一块前面加上 TCP 头部,然后发送给服务器(图 2.13 ④)TCP 头部中包含序号,它表示当前发送的是第几个字节的数据。当服务器收到数据时,会向客户端返回 ACK 号(图 2.13 ⑤)。在最初的阶段,服务器只是不断接收数据,随着数据收发的进行,数据不断传递给应用程序,接收缓冲区就会被逐步释放。这时,服务器需要将新的窗口大小告知客户端。当服务器收到客户端的请求消息后,会向客户端返回响应消息,这个过程和刚才的过程正好相反(图 2.13 ⑥⑦)
服务器的响应消息发送完毕之后,数据收发操作就结束了,这时就会开始执行断开操作。以 Web 为例,服务器会先发起断开过程。在这个过程中,服务器先发送一个 FIN 为 1 的 TCP 包(图 2.13 ⑧),然后客户端返回一个表示确认收到的 ACK 号(图 2.13 ⑨)。接下来,双方还会交换一组方向相反的 FIN 为 1 的 TCP 包(图 2.13 ⑩)和包含 ACK 号的 TCP 包(图 2.13k)。最后,在等待一段时间后,套接字会被删除。
2.5 IP 与以太网的包收发操作
具体来说,如图 2.14(b)所示,TCP/IP 包包含如下两个头部。
- (a)MAC 头部(用于以太网协议)
- (b)IP 头部(用于 IP 协议)
将要访问的服务器的 IP 地址写入 IP 头部中,下一个路由器的以太网地址(MAC 地址)写入 MAC 头部中。
每经过一个服务器,收到包的时候 MAC 头部会被舍弃,而当再次发送的时候又会加上包含新 MAC 地址的新 MAC 头部,前往下一个路由器。
尽管以太网经历了数次变迁,但其基本的 3 个性质至今仍未改变,即将包发送到 MAC 头部的接收方 MAC 地址代表的目的地,用发送方 MAC地址识别发送方,用以太类型识别包的内容。因此,大家可以认为具备这 3 个性质的网络就是以太网
网卡的 ROM 中保存着全世界唯一的 MAC 地址,这是在生产网卡时写入的。
网卡中保存的 MAC 地址会由网卡驱动程序读取并分配给 MAC 模块。
简单来说,网线和局域网中只能传输小包,因此需要将大的包切分成多个小包。如果接收到的包是经过分片的,那么 IP 模块会将它们还原成原始的包。分片的包会在 IP 头部的标志字段中进行标记,当收到分片的包时,IP 模块会将其暂存在内部的内存空间中,然后等待 IP 头部中具有相同 ID 的包全部到达,这是因为同一个包的所有分片都具有相同的 ID。此外,IP 头部还有一个分片偏移量(fragment offset)字段,它表示当前分片在整个包中所处的位置。根据这些信息,在所有分片全部收到之后,就可以将它们还原成原始的包,这个操作叫作分片重组。
到这里,IP 模块的工作就结束了,接下来包会被交给 TCP 模块。TCP 模块会根据 IP 头部中的接收方和发送方 IP 地址,以及 TCP 头部中的接收 方和发送方端口号来查找对应的套接字。找到对应的套接字之后,就可以根据套接字中记录的通信状态,执行相应的操作了。例如,如果包的内容是应用程序数据,则返回确认接收的包,并将数据放入缓冲区,等待应用程序来读取;如果是建立或断开连接的控制包,则返回相应的响应控制包,并告知应用程序建立和断开连接的操作状态。
严格来说,TCP 模块和 IP 模块有各自的责任范围,TCP 头部属于 TCP 的责任范围,而 IP 头部属于 IP 模块的责任范围。根据这样的逻辑,当包交给 TCP 模块之后,TCP 模块需要查询 IP 头部中的接收方和发送方 IP 地址来查找相应的套接字,这个过程就显得有点奇怪。因为 IP 头部是 IP 模块负责的,TCP 模块去查询它等于是越权了。如果要避免越权,应该对两者进行明确的划分,IP 模块只向 TCP 模块传递 TCP 头部以及它后面的数据,而对于 IP 头部中的重要信息,即接收方和发送方的 IP 地址,则由 IP 模块以附加参数的形式告知 TCP 模块。然而,如果根据这种严格的划分来开发程序的话,IP 模块和 TCP 模块之间的交互过程必然会产生成本,而且 IP模块和 TCP 模块进行类似交互的场景其实非常多,总体的交互成本就会很高,程序的运行效率就会下降。因此,就像之前提过的一样,不妨将责任范围划分得宽松一些,将 TCP 和 IP 作为一个整体来看待,这样可以带来更大的灵活性。
2.6 UDP 协议的收发操作
要实现上面的要求,最简单的方法是数据全部发送完毕之后让接收方返回一个接收确认。这样一来,如果没收到直接全部重新发送一遍就好了,根本不用像 TCP 一样要管理发送和确认的进度。但是,如果漏掉了一个包就要全部重发一遍,怎么看都很低效。为了实现高效的传输,我们要避免重发已经送达的包,而是只重发那些出错的或者未送达的包。TCP 之所以复杂,就是因为要实现这一点。
不过,在某种情况下,即便没有 TCP 这样复杂的机制,我们也能够高效地重发数据,这种情况就是数据很短,用一个包就能装得下。如果只有一个包,就不用考虑哪个包未送达了,因为全部重发也只不过是重发一个包而已,这种情况下我们就不需要 TCP 这样复杂的机制了。而且,如果不使用 TCP,也不需要发送那些用来建立和断开连接的控制包了。此外,我们发送了数据,对方一般都会给出回复,只要将回复的数据当作接收确认就行了,也不需要专门的接收确认包了。
这种情况就适合使用 UDP。像 DNS 查询等交换控制信息的操作基本上都可以在一个包的大小范围内解决,这种场景中就可以用 UDP 来代替 TCP 。UDP 没有 TCP 的接收确认、窗口等机制,因此在收发数据之前也不需要交换控制信息,也就是说不需要建立和断开连接的步骤,只要在从应用程序获取的数据前面加上 UDP 头部,然后交给 IP 进行发送就可以了(表 2.5)。接收也很简单,只要根据 IP 头部中的接收方和发送方 IP 地址,以及 UDP 头部中的接收方和发送方端口号,找到相应的套接字并将数据交给相应的应用程序就可以了。除此之外,UDP 协议没有其他功能了,遇到错误或者丢包也一概不管。因为 UDP 只负责单纯地发送包而已,并不像 TCP 一样会对包的送达状态进行监控,所以协议栈也不知道有没有发生错误。但这样并不会引发什么问题,因此出错时就收不到来自对方的回复,应用程序会注意到这个问题,并重新发送一遍数据。这样的操作本身并不复杂,也并不会增加应用程序的负担。
第三章 从网线到网络设备——探索集线器、交换机和路由器
3.1 信号在网线和集线器中传输
集线器将信号发送给所有连接在它上面的线路。
3.2 交换机的包转发操作
交换机根据 MAC 地址表查找 MAC 地址,然后将信号发送到相应的端口。
注
第一种是收到包时,将发送方 MAC 地址以及其输入端口的号码写入 MAC 地址表中。由于收到包的那个端口就连接着发送这个包的设备,所以只要将这个包的发送方 MAC 地址写入地址表,以后当收到发往这个地址的包时,交换机就可以将它转发到正确的端口了。交换机每次收到包时都会执行这个操作,因此只要某个设备发送过网络包,它的 MAC 地址就会被记录到地址表中。
注
另一种是删除地址表中某条记录的操作,这是为了防止设备移动时产生问题。比如,我们在开会时会把笔记本电脑从办公桌拿到会议室,这时设备就发生了移动。从交换机的角度来看,就是本来连接在某个端口上的笔记本电脑消失了。这时如果交换机收到了发往这台已经消失的笔记本电脑的包,那么它依然会将包转发到原来的端口,通信就会出错,因此必须想办法删除那些过时的记录。然而,交换机没办法知道这台笔记本电脑已经从原来的端口移走了。因此地址表中的记录不能永久有效,而是要在一段时间不使用后就自动删除。
当交换机发现一个包要发回到原端口时,就会直接丢弃这个包。
地址表中找不到指定的 MAC 地址。这可能是因为具有该地址的设备还没有向交换机发送过包,或者这个设备一段时间没有工作导致地址被从地址表中删除了。这种情况下,交换机无法判断应该把包转发到哪个端口,只能将包转发到除了源端口之外的所有端口上,无论该设备连接在哪个端口上都能收到这个包。这样做不会产生什么问题,因为以太网的设计本来就是将包发送到整个网络的,然后只有相应的接收者才接收包,而其他设备则会忽略这个包。
3.3 路由器的包转发操作
路由器在转发包时,首先会通过端口将发过来的包接收进来,这一步的工作过程取决于端口对应的通信技术。对于以太网端口来说,就是按照以太网规范进行工作,而无线局域网端口则按照无线局域网的规范工作,总之就是委托端口的硬件将包接收进来。接下来,转发模块会根据接收到的包的 IP 头部中记录的接收方 IP 地址,在路由表中进行查询,以此判断转发目标。然后,转发模块将包转移到转发目标对应的端口,端口再按照硬件的规则将包发送出去,也就是转发模块委托端口模块将包发送出去的意思。
刚才我们讲到端口模块会根据相应通信技术的规范来执行包收发的操作,这意味着端口模块是以实际的发送方或者接收方的身份来收发网络包的。以以太网端口为例,路由器的端口具有 MAC 地址,因此它就能够成为以太网的发送方和接收方。端口还具有 IP 地址,从这个意义上来说,它和计算机的网卡是一样的。当转发包时,首先路由器端口会接收发给自己的以太网包,然后查询转发目标,再由相应的端口作为发送方将以太网包发送出去。这一点和交换机是不同的,交换机只是将进来的包转发出去而已,它自己并不会成为发送方或者接收方。
路由器的各个端口都具有 MAC 地址和 IP 地址。
在“查表判断转发目标”这一点上,路由器和交换机的大体思路是类似的,不过具体的工作过程有所不同。交换机是通过 MAC 头部中的接收方 MAC 地址来判断转发目标的,而路由器则是根据 IP 头部中的 IP 地址来判断的。由于使用的地址不同,记录转发目标的表的内容也会不同。
路由器会将接收到的网络包的接收方 IP 地址与路由表中的目标地址进行比较,并找到相应的记录。交换机在地址表中只匹配完全一致的记录,而路由器则会忽略主机号部分,只匹配网络号部分。打个比方,路由器在转发包的时候只看接收方地址属于哪个区,×× 区发往这一边,×× 区发往那一边。
路由器会忽略主机号,只匹配网络号。
我们现在有 3 个子网,分别为 10.10.1.0/24、10.10.2.0/24、10.10.3.0/24,路由器 B 需要将包发往这 3 个子网。在这种情况下,路由器 B 的路由表中原本应该有对应这 3 个子网的 3 条记录,但在这个例子中,无论发往任何一个子网,都是通过路由器 A 来进行转发,因此我们可以在路由表中将这 3 个子网合并成 10.10.0.0/16,这样也可以正确地进行转发,但我们减少了路由表中的记录数量,这就是路由聚合。经过路由聚合,多个子网会被合并成一个子网,子网掩码会发生变化,同时,目标地址列也会改成聚合后的地址。
首先,信号到达网线接口部分,其中的 PHY(MAU)模块和 MAC 模块将信号转换为数字信息,然后通过包末尾的 FCS 进行错误校验,如果没问题则检查 MAC 头部中的接收方 MAC 地址,看看是不是发给自己的包,如果是就放到接收缓冲区中,否则就丢弃这个包。如果包的接收方 MAC 地址不是自己,说明这个包是发给其他设备的,如果接收这个包就违反了以太网的规则。
路由器的端口都具有 MAC 地址,只接收与自身地址匹配的包,遇到不匹配的包则直接丢弃。
完成包接收操作之后,路由器就会丢弃包开头的 MAC 头部。MAC 头部的作用就是将包送达路由器,其中的接收方 MAC 地址就是路由器端口的 MAC 地址。因此,当包到达路由器之后,MAC 头部的任务就完成了,于是 MAC 头部就会被丢弃。
通过路由器转发的网络包,其接收方 MAC 地址为路由器端口的 MAC 地址。
接下来,路由器会根据 MAC 头部后方的 IP 头部中的内容进行包的转发操作。转发操作分为几个阶段,首先是查询路由表判断转发目标。我们可能会匹配到多条候选记录。在这个例子中,第 3、4、5 行都可以匹配。其中,路由器首先寻找网络号比特数最长的一条记录。网络号比特数越长,说明主机号比特数越短,也就意味着该子网内可分配的主机数量越少,即子网中可能存在的主机数量越少,这一规则的目的是尽量缩小范围,所以根据这条记录判断的转发目标就会更加准确。
如果在路由表中无法找到匹配的记录,路由器会丢弃这个包,并通过 ICMP 消息告知发送方。这里的处理方式和交换机不同,原因在于网络规模的大小。交换机连接的网络最多也就是几千台设备的规模,这个规模并不大。如果只有几千台设备,遇到不知道应该转发到哪里的包,交换机可以将包发送到所有的端口上,虽然这个方法很简单粗暴,但不会引发什么问题。然而,路由器工作的网络环境就是互联网,它的规模是远远大于以太网的,全世界所有的设备都连接在互联网上,而且规模还在持续扩大,未来的互联网里到底会有多少设备,我们谁都说不准。在如此庞大的网络中,如果将不知道应该转发到哪里的包发送到整个网络上,那就会产生大量的网络包,造成网络拥塞。因此,路由器遇到不知道该转发到哪里的包,就会直接丢弃。
ICMP:Internet Control Message Protocol,Internet 控制报文协议。当包传输过程中发生错误时,用来发送控制消息。
子网掩码 0.0.0.0 的意思是网络包接收方 IP 地址和路由表目标地址的匹配中需要匹配的比特数为 0,换句话说,就是根本不需要匹配。只要将子网掩码设置为 0.0.0.0,那么无论任何地址都能匹配到这一条记录,这样就不会发生不知道要转发到哪里的问题了。
路由表中子网掩码为0.0.0.0的记录表示“默认路由”。
TTL 字段表示包的有效期,包每经过一个路由器的转发,这个值就会减 1,当这个值变成 0 时,就表示超过了有效期,这个包就会被丢弃。
这个机制是为了防止包在一个地方陷入死循环。如果路由表中的转发目标都配置正确,应该不会出现这样的情况,但如果其中的信息有问题,或者由于设备故障等原因切换到备用路由时导致暂时性的路由混乱,就会出现这样的情况。
一般来说都是可以分片的,但下面两种情况不能分片:1)发送方应用程序等设置了不允许分片;2)这个包已经是经过分片后的包。
在分片中,TCP 头部及其后面的部分都是可分片的数据,尽管 TCP 头部不属于用户数据,但从 IP 来看也是 TCP 请求传输的数据的一部分。数据被拆分后,每一份数据前面会加上 IP 头部,其大部分内容都和原本的 IP 头部一模一样,但其中有部分字段需要更新,这些字段用于记录分片相关的信息。
路由器判断下一个转发目标的方法如下。
- 如果路由表的网关列内容为 IP 地址,则该地址就是下一个转发目标。
- 如果路由表的网关列内容为空,则 IP 头部中的接收方 IP 地址就是下一个转发目标。
路由器也会使用 ARP 来查询下一个转发目标的 MAC 地址。
提示
IP (路由器)负责将包发送给通信对象这一整体过程,而其中将包传输到下一个路由器的过程则是由以太网(交换机)来负责的。
3.4 路由器的附加功能
在内网中可用作私有地址的范围仅限以下这些:
10.0.0.0 ~ 10.255.255.255
172.16.0.0 ~ 172.31.255.255
192.168.0.0 ~ 192.168.255.255
端口号是一个 16 比特的数值,总共可以分配出几万个端口,因此如果用公有地址加上端口的组合对应一个私有地址,一个公有地址就可以对应几万个私有地址,这种方法提高了公有地址的利用率。
换个角度来看,这意味着对于没有在访问互联网的内网设备,是无法从互联网向其发送网络包的。而且即便是正在访问的设备,也只能向和互联网通信中使用的那个端口发送网络包,无法向其他端口发送包。也就是说,除非公司主动允许,否则是无法从互联网向公司内网发送网络包的。这种机制具有防止非法入侵的效果。
第四章 通过接入网进入互联网内部——探索接入网和网络运营商
个人感觉这章的内容不是很重要,随便看看就好。
4.1 ADSL 接入网的结构和工作方式
ADSL:Asymmetric Digital Subscriber Line,不对称数字用户线。它是一种利用架设在电线杆上的金属电话线来进行高速通信的技术,它的上行方向(用户到互联网)和下行方向(互联网到用户)的通信速率是不对称的。
提示
互联网接入路由器会在网络包前面加上 MAC 头部、PPPoE 头部、PPP 头 部 总 共 3 种 头 部, 然 后 发 送 给 ADSL Modem(PPPoE 方式下)。
提示
ADSL Modem 将包拆分成信元,并转换成电信号发送给分离器。
提示
DSLAM 具有 ATM 接口,和后方路由器收发数据时使用的是原始网络包拆分后的 ATM 信元形式。
提示
BAS 负责将 ATM 信元还原成网络包并转发到互联网内部。
4.2 光纤接入网(FTTH)
单模和多模实际上表示相位一致的角度有一个还是多个
单模光纤和多模光纤在光的传导方式上有所不同,这决定了它们的特性也有所不同。多模光纤中可以传导多条光线,这意味着能通过的光线较多,对光源和光敏元件的性能要求也就较低,从而可以降低光源和光敏元件的价格。相对地,单模光纤的纤芯中只能传导一条光线,能通过的光线较少,相应地对于光源和光敏元件的性能要求就较高,但信号的失真会比较小。
多模光纤中,多条反射角不同的光线同时传导,其中反射角越大的光线反射次数越多,走过的距离也就越长;相对地,反射角越小的光线走过的距离越短。光通过的距离会影响其到达接收端的时间,也就是说,通过的距离越长,到达接收端的时间越长。结果,多条光线到达的时间不同,信号的宽度就会被拉伸,这就造成了失真。因此,光纤越长,失真越大,当超过允许范围时,通信就会出错。
相对地,单模光纤则不会出现这样的问题。因为在纤芯传导的光线只有一条,不会因为行进距离的差异产生时间差,所以即便光纤很长,也不会产生严重的失真。
光纤的最大长度也是由上述性质决定的。单模光纤的失真小,可以比多模光纤更长,因此多模光纤主要用于一座建筑物里面的连接,单模光纤则用于距离较远的建筑物之间的连接。FTTH 属于后者,因此主要使用单模光纤。
4.3 接入网中使用的 PPP 和隧道
提示
PPPoE 是将 PPP 消息装入以太网包进行传输的方式。
提示
互联网接入路由器通过 PPPoE 的发现机制查询 BAS 的 MAC 地址。
提示
BAS 下发的 TCP/IP 参数会被配置到互联网接入路由器的 BAS 端的端口上,这样路由器就完成接入互联网的准备了。
提示
BAS 在收到用户路由器发送的网络包之后,会去掉 MAC 头部和 PPPoE 头部,然后用隧道机制将包发送给网络运营商的路由器。
提示
一对一连接的端口可以不分配 IP 地址,这种方式称为无编号。
提示
PPPoA 方式不添加 MAC 头部和 PPPoE 头部,而是直接将包装入信元中。
提示
还有一种 DHCP 方式,它不使用 PPP,而是将以太网包直接转换成 ADSL 信号发送给 DSLAM。
4.4 网络运营商的内部
提示
网络包通过接入网之后,到达运营商 POP 的路由器。
4.5 跨越运营商的网络包
提示
互联网内部使用 BGP(边界网关协议) 机制在运营商之间交换路由信息。
第五章 服务器端的局域网中有什么玄机
5.1 Web 服务器的部署地点
在公司里部署 Web 服务器
将 Web 服务器部署在数据中心:数据中心通过高速线路直接连接到互联网的核心部分,因此将服务器部署在这里可以获得很高的访问速度。当服务器访问量很大时这是非常有效的。此外,数据中心一般位于具有抗震结构的大楼内,还具有自主发电设备,并实行 24 小时门禁管理,可以说比放在公司里具有更高的安全性。此外,数据中心不但提供安放服务器的场地,还提供各种附加服务,如服务器工作状态监控、防火墙的配置和运营、非法入侵监控等,从这一点来看,其安全性也更高。
5.2 防火墙的结构和原理
无论服务器部署在哪里,现在一般都会在前面部署一个防火墙,如果包无法通过防火墙,就无法到达服务器。
提示
包过滤方式的防火墙可根据接收方 IP 地址、发送方 IP 地址、接收方端口号、发送方端口号、控制位等信息来判断是否允许某个包通过。
5.3 通过将请求平均分配给多台服务器来平衡负载
轮询:每次更换返回的服务器ip顺序,可以将访问平均分配给所有的服务器。
缺点:
假如多台 Web 服务器中有一台出现了故障,这时我们希望在返回 IP 地址时能够跳过故障的 Web 服务器,然而普通的 DNS 服务器并不能确认 Web 服务器是否正常工作,因此即便 Web 服务器宕机了,它依然可能会返回这台服务器的 IP 地址。
在通过 CGI 等方式动态生成网页的情况下,有些操作是要跨多个页面的,如果这期间访问的服务器发生了变化,这个操作就可能无法继续。例如在购物网站中,可能会在第一个页面中输入地址和姓名,在第二个页面中输入信用卡号,这就属于刚才说的那种情况。
改进:使用负载均衡器分配访问。
5.4 使用缓存服务器分担负载
5.5图
缓存服务器会根据上述规则来转发请求消息,在这个过程中,缓存服务器会以客户端的身份向目标 Web 服务器发送请求消息。也就是说,它会先创建套接字,然后连接到 Web 服务器的套接字,并发送请求消息。从 Web 服务器来看,缓存服务器就相当于客户端。于是,缓存服务器会收到来自 Web 服务器的响应消息(图 5.5(a)③、图 5.6(c)),接收消息的过程也是以客户端的身份来完成的。
刚才讲的是在 Web 服务器一端部署一个代理,然后利用其缓存功能来改善服务器的性能,还有一种方法是在客户端一侧部署缓存服务器,称为正向代理。
正向代理刚刚出现的时候,其目的之一就是缓存,这个目的和服务器端的缓存服务器相同。不过,当时的正向代理还有另外一个目的,那就是用来实现防火墙。
在使用正向代理时,一般需要在浏览器的设置窗口中的“代理服务器” 一栏中填写正向代理的 IP 地址,浏览器发送请求消息的过程也会发生相应的变化。在没有设置正向代理的情况下,浏览器会根据网址栏中输入的 http://... 字符串判断 Web 服务器的域名,并向其发送请求消息;当设置了正向代理时,浏览器会忽略网址栏的内容,直接将所有请求发送给正向代理。请求消息的内容也会有一些不同。没有正向代理时,浏览器会从网址中提取出 Web 服务器域名后面的文件名或目录名,然后将其作为请求的 URI 进行发送;而有正向代理时,浏览器会像图 5.9 这样,在请求的 URI 字段中填写完整的 http://... 网址。
正如前面讲过的,使用正向代理需要在浏览器中进行设置,这可以说是识别正向代理的一个特征。但是,设置浏览器非常麻烦,如果设置错误还可能导致浏览器无法正常工作。需要设置浏览器这一点除了麻烦、容易发生故障之外,还有其他一些限制。如果我们想把代理放在服务器端,那么服务器不知道谁会来访问,也没办法去设置客户端的浏览器,因此无法采用这种方法来实现。于是,我们可以对这种方法进行改良,使得不需要在浏览器中设置代理也可以使用。也就是说,我们可以通过将请求消息中的 URI 中的目录名与 Web 服务器进行关联,使得代理能够转发一般的不包含完整网址的请求消息。我们前面介绍的服务器端的缓存服务器采用的正是这种方式,这种方式称为反向代理(reverse proxy)。
缓存服务器判断转发目标的方法还有一种,那就是查看请求消息的包头部。因为包的卫头部中包含接收方地址,只要知道了这个地址,就知道用户要访问哪台服务器了。这种方法称为透明代理(transparent proxy)。
5.5 内容分发服务
为了解决这个问题,一些专门从事相关服务的厂商出现了,他们来部署缓存服务器,并租借给 Web 服务器运营者。这种服务称为内容分发服务
提供这种服务的厂商称为 CDSP ,他们会与主要的供应商签约,并部署很多台缓存服务器。另一方面,CDSP 会与 Web 服务器运营者签约,使得 CDSP 的缓存服务器配合 Web 服务器工作。具体的方法我们后面会介绍,只要 Web 服务器与缓存服务器建立关联,那么当客户端访问 Web 服务器时,实际上就是在访问 CDSP 的缓存服务器了。
缓存服务器可以缓存多个网站的数据,因此 CDSP 的缓存服务器就可以提供给多个 Web 服务器的运营者共享。这样一来,每个网站运营者的平均成本就降低了,从而减少了网站运营者的负担。而且,和运营商之间的签约工作也由 CDSP 统一负责,网站运营者也节省了精力。
第六章 请求到达 Web 服务器,响应返回浏览器——短短几秒的“漫长旅程”迎来终点
6.1 服务器概览
注
使用描述符来指代套接字的原因如下。
- (1)等待连接的套接字中没有客户端 IP 地址和端口号
- (2)使用描述符这一种信息比较简单
6.2 服务器的接收操作
注
网卡的 MAC 模块将网络包从信号还原为数字信息,校验 FCS 并存入缓冲区。
注
网卡驱动会根据 MAC 头部判断协议类型,并将包交给相应的协议栈。
注
协议栈的 IP 模块会检查 IP 头部,(1)判断是不是发给自己的;(2)判断网络包是否经过分片;(3)将包转交给 TCP 模块或 UDP 模块。
注
如果收到的是发起连接的包,则 TCP 模块会(1)确认 TCP 头部的控制位 SYN;(2)检查接收方端口号;(3)为相应的等待连接套接字复制一个新的副本;(4)记录发送方IP地址和端口号等 信息。
注
收到数据包时,TCP 模块会(1)根据收到的包的发送方 IP 地址、发送方端口号、接收方 IP 地址、接收方端口号找到相对应的套接字;(2)将数据块拼合起来并保存在接收缓冲区中;(3)向 客户端返回 ACK。
6.3 Web 服务器程序解释请求消息并作出响应
注
如果完全按照 URI 中的路径和文件名读取,那就意味着磁盘上所有的文件都可以访问,Web 服务器的磁盘内容就全部暴露了,这很危险。
注
客户端看到的目录结构和实际目录结构是不同的:客户端看到的 Web 服务器目录是虚拟的,和实际的目录结构不同。Web 服务器内部会将实际的目录名和供外部访问的虚拟目录名进行关联。
注
正如我们前面讲的,Web 服务器的基本工作方式就是根据请求消息的内容判断数据源,并从中获取数据返回给客户端,不过在执行这些操作之前,Web 服务器还可以检查事先设置的一些规则,并根据规则允许或禁止访问。这种根据规则判断是否允许访问的功能称为访问控制,一些会员制的信息服务需要限制用户权限的时候会使用这一功能,公司里也可以利用访问控制只允许某些特定部门访问。Web 服务器的访问控制规则主要有以下 3 种。
- (1)客户端 IP 地址
- (2)客户端域名
- (3)用户名和密码
6.4 浏览器接收响应消息并显示内容
注
要显示内容,首先需要判断响应消息中的数据属于哪种类型。Web 可以处理的数据包括文字、图像、声音、视频等多种类型,每种数据的显示方法都不同,因此必须先要知道返回了什么类型的数据,否则无法正确显示。原则上可以根据响应消息开头的 Content-Type 头部字段的值来进行判断。