《网络是怎样连接的》——在读
约 5812 字大约 19 分钟
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 三次握手建立连接