一、浏览器生成消息
1.1 生成HTTP请求消息
1.1.1 什么是URL
我们的网络体验从在浏览器中输入网址开始。网址,即URL,URL通过开头不同的标识区分不同的服务,例如访问Web服务器时使用"http://",访问FTP服务器时使用"ftp://"。
下图列举了几种常用的互联网URL:
URL的写法不同,但它们有一个共同点,就是URL开头的文字,即“http:”“ftp:”“file:”“mailto:”这部分文字都表示浏览器应 当使用的访问方法。比如当访问 Web 服务器时应该使用 HTTP协议,而 访问 FTP 服务器时则应该使用 FTP 协议。因此,我们可以把这部分理解为 访问时使用的协议类型。
1.1.2 浏览器解析URL
浏览器要先解析URL,才能生成发送给Web服务器的消息。下面我们以访问Web服务器为例介绍浏览器解析URL的过程:
- 假设浏览器访问"www.lovewujunli.com/love/day.ht…";
- 浏览器首先对URL进行拆解,得到服务器名称(地址)、文件地址。
- 浏览器得到:要访问"www.lovewujunli.com"这个服务器上的"love"文件夹下的"dai.html"文件。
1.1.3 什么是HTTP协议
解析完URL之后,浏览器会使用HTTP协议来访问Web服务器,下面介绍一下什么是HTTP协议。
如上图,HTTP协议定义了客户端和服务器之间交互的消息内容和步骤。首先,客户端向服务器发送请求消息,请求消息包括方法和URI。其中,URI叫作统一资源标识符,简单来说URI标识了要访问的目标。方法表示要让Web服务器完成怎样的操作,下表列举了主要的方法:
客户端发送的请求消息除了请求方法和URI之外,还有一些用来表示附加信息的头字段。客户端向Web服务器发送数据时,会先发送头字段,然后再发送数据,后面会详细介绍头字。
收到请求消息之后,Web 服务器会对其中的内容进行解析,通过 URI 和方法来判断“对什么”“进行怎样的操作”,并根据这些要求来完成自己 的工作,然后将结果存放在响应消息中。在响应消息的开头有一个状态码, 它用来表示操作的执行结果是成功还是发生了错误。
响应消息会被发送回客户端,客户端收到之后,浏览器会从消息中读出所需的数据并显示在屏幕 上。到这里,HTTP 的整个工作就完成了。
1.1.4 生成HTTP请求消息
对URL进行解析之后浏览器知道要访问的服务器和内容了,接下来就浏览器就要按照规定的格式来生成请求消息了,请求消息的格式如下图所示:
首先,请求消息的第一行称为请求行。这里的重点是最开头的方法,方法可以告诉 Web 服务器它应该进行怎样的操作。浏览器通过提交请求的方式判断需要用什么方法,比如在地址栏输入URL回车之后浏览器就知道应该用GET方法,如果是在表单处发送请求,浏览器就知道应该用POST方法。
尽管通过第一行我们就可以大致理解请求的内容,但有些情况下还需要一些额外的详细信息,而消息头的功能就是用来存放这些信息。消息头的规格中定义了很多项目,如日期、客户端支持的数据类型、语言、压缩格式、客户端和服务器的软件名称和版本、数据有 效期和最后更新时间等。
1.1.5 发送请求后收到响应
当浏览器向服务器发送请求后,会接受到服务器发送过来的响应消息,响应消息的格式在上面的图中已经给出了。下表向我们介绍了状态码大概的意思:
由于每条请求消息中只能写 1 个 URI,所以每次只能获取 1 个文件, 如果需要获取多个文件,必须对每个文件单独发送 1 条请求。比如 1 个网页中包含 3 张图片,那么获取网页加上获取图片,一共需要向 Web 服务器发送 4 条请求。
判断所需的文件,然后获取这些文件并显示在屏幕上,这一系列工作 的整体指挥是浏览器的任务之一,而 Web 服务器却毫不知情。Web 服务器完全不关心这 4 条请求获取的文件到底是 1 个网页上的还是不同网页上 的,它的任务就是对每一条单独的请求返回 1 条响应而已。
到这里,我们已经知道了了浏览器与 Web 服务器进行交互的整个过程。 《网络是怎样连接的》书中第一章最后展示了浏览器与 Web 服务器之间交互消息的一个实例,具体见该书第47页。
1.2 向DNS服务器查询Web服务器的IP地址
1.2.1 什么是IP地址
浏览器并不具备发送请求信息的功能,而是通过委托操作系统来发送请求消息,但在这之前要先通过解析URL得到的域名查询服务器的IP地址。在介绍这一操作之前,我们先了解一下什么是IP地址。
互联网和公司内部的局域网都是基于 TCP/IP 的思路来设计的,所以我们先来了解 TCP/IP 的基本思路。TCP/IP 的结构如图 1.8 所示,就是由一些 小的子网,通过路由器连接起来组成一个大的网络。这里的子网可以理解为用集线器连接起来的几台计算机,我们将它看作一个单位,称为子网。 将子网通过路由器连接起来,就形成了一个网络。
在网络中,所有的设备都会被分配一个地址。这个地址就相当于现实中某条路上的“×× 号 ×× 室”。其中“号”对应的号码是分配给整个子网的,而“室”对应的号码是分配给子网中的计算机的,这就是网络中的地址。“号”对应的号码称为网络号,“室”对应的号码称为主机号,这个地址的整体称为 IP 地址。
发送者发出的消息首先经过子网中的集线器,转发到距离发送者最近的路由器上(图 1.8 ①)。接下来,路由器会根据消息的目的地判断下一个路由器的位置,然后将消息发送到下一个路由器,即消息再次经过子网内的集线器被转发到下一个路由器(图 1.8 ②)。前面的过程不断重复,最终消息就被传送到了目的地。
前面这些就是 TCP/IP 中IP地址的基本思路。了解了这些知识之后,让我们再来看一下实际的 IP 地址。如图1.10所示,实际的IP地址是一串32比特的数字,按照8比特(1字节)为一组分成4组,分别用十进制表示然后再用圆点隔开。这就是我们平常经常见到的 IP 地址格式,但仅凭这一串数字我们无法区分哪部分是网络号,哪部分是主机号。在IP地址的规则 中,网络号和主机号连起来总共是32比特,但这两部分的具体结构是不固定的。在组建网络时,用户可以自行决定它们之间的分配关系,因此,我们还需要另外的附加信息来表示 IP 地址的内部结构。
这里的附加信息就是子网掩码,子网掩码是一 串与IP地址长度相同的32比特数字,其左边一半都是1,右边一半都是0。其中,子网掩码为1的部分表示网络号,子网掩码为0的部分表示主机号。第一种写法是将子网掩码按照和IP地址一样的方式以每8bit为单位用圆点分组后写在IP地址的右侧,如下图所示。
上面的写法太长,我们还可以把子网掩码中1的部分的位数用十进制表示,并写在IP地址的右侧,比如上图的例子,子网掩码中一共有24位二进制的1,就可以像下图一样简写子网掩码。
顺带一提,如下图所示,主机号有两种特别形式,即主机号部分全0或全1。
1.2.2 Socket库提供查询IP地址的功能
查询 IP 地址的方法非常简单,只要询问最近的DNS服务器“www. lab.glasscom.com的IP地址是什么”就可以了,DNS服务器会回答说“该服务器的 IP 地址为 xxx.xxx.xxx.xxx”。这一过程很简单,那么浏览器是如何向DNS服务器发起查询的呢?让我们先来探索一下DNS。
对于DNS,用户的计算机上有相应的DNS客户端——DNS解析器,解析器实际上是一段程序,包含在操作系统的Socket库中,调用的是一个叫作 gethostbyname 的程序组件。Socket库可以让其他的应用程序调用操作系统的网络功能,解析器就是这个库中的一种程序组件。
下面我们来具体了解一下解析器。
1.2.3 通过解析器向DNS服务器发出查询
调用解析器后,解析器会向DNS服务器发送查询消息,然后DNS服务器会返回响应消息。响应消息中包含查询到的IP地址,解析器会取出IP地址,并将其写入浏览器指定的内存地址中。接 下来,浏览器在向Web服务器发送消息时,只要从该内存地址取出IP地址,并将它与HTTP请求消息一起交给操作系统就可以了。
1.2.4 解析器的原理
网络应用程序(这里是浏览器)调用解析器时,程序的控制流程程就会转移到解析器的内部。随后解析器会生成一条要发送给DNS服务器的查询消息,DNS服务器会返回一条响应消息。解析器本身不能发送消息,而是要委托给操作系统内部的协议栈(操作系统内部的网络控制软件,也叫“协议驱动”“TCP/IP 驱动”等)发送查询消息。协议栈会执行发送消息的操作,然后通过网卡将消息发送给DNS服务器。DNS服务器接收到查询消息之后就会返回一条响应消息,包含域名对应的IP地址,这一整个过程称为域名解析。
1.3 全世界DNS服务器的大接力
1.3.1 DNS服务器的基本工作
来自客户端的查询消息包含下面三个信息:
- 域名:服务器、邮件服务器(邮件地址中 @ 后面的部分)的名称。
- Class:在最早设计 DNS 方案时,DNS 在互联网以外的其他网络中的应用也被考虑到了,而 Class 就是用来识别网络的信息。不过,如今除了 互联网并没有其他的网络了,因此 Class 的值永远是代表互联网的 IN。
- 记录类型:表示域名对应何种类型的记录。例如,当类型为 A 时,表示域名对应的是 IP 地址;当类型为 MX时,表示域名对应的是邮件服务器。对于不同的记录类型,服务器向客户端返回的信息也会不同。
1.3.2 域名的层次结构
DNS 服务器中的所有信息都是按照域名以分层次的结构来保存的。DNS中的域名都是用句点来分隔的,比如www.lab.glasscom.com,这里的句点代表了不同层次之间的界限,在域名中,越靠右的位置表示其层级越高。其中,相当于一个层级的部分称为域。例如前面的域名,com域的下一层是glasscom域,再下一层是lab域,再下面才是www这个名字。
于是,DNS服务器也具有了像域名一样的层次结构,每个域的信息都存放在相应层级的DNS服务器中。
再补充一点,在一个域下面可以创建下级域,比如,假设某公司的域为 example.cn,我们可以在这个域的下面创建两个子域,例如sub1. example.cn 和 sub2.example.cn,然后就可以将这两个下级域分配给公司不同的部门来使用。
1.3.3 寻找相应的DNS服务器并获取IP地址
从DNS服务器获取域名对应的IP地址的关键是如何找到我们要访问的Web服务器信息归哪一台DNS服务器管。
首先,将负责管理下级域的DNS服务器的IP地址注册到它们的上级DNS服务器中,然后上级 DNS 服务器的IP地址再注册到更上一级的 DNS 服务器中,以此类推。这样,我们就可以通过上级DNS服务器查询出下级DNS服务器的IP地址,也就可以向下级 DNS 服务器发送查询请求了。
在互联网中,com和cn的上面还有一级域,称为根域,书写域名时常被省略。根域的DNS服务器中保管着com、cn等的 DNS 服务器的信息。根域的DNS服务器信息保存在互联网中所有的DNS服务器中。这样一来,任何DNS服务器就都可以找到并访问根域DNS服务器了。因此,客户端只要能够找到任意一台DNS服务器,就可以通过它找到根域DNS服务器,然后再一路顺藤摸瓜找到位于下层的某台目标 DNS 服务器。这个过程如下图所示。
1.3.4 通过缓存加快DNS服务器的响应
有时候并不需要从最上级的根域开始查找,因为 DNS 服务器有一 个缓存功能,可以记住之前查询过的域名。如果要查询的域名和相关信息已经在缓存中,那么就可以直接返回响应,接下来的查询可以从缓存的位置开始向下进行。相比每次都从根域找起来说,缓存可以减少查询所需的时间。
并且,当要查询的域名不存在时,“不存在”这一响应结果也会被缓存。这样,当下次查询这个不存在的域名时,也可以快速响应。
这个缓存机制中有一点需要注意,那就是信息被缓存后,原本的注册信息可能会发生改变,这时缓存中的信息就有可能是不正确的。因此,DNS服务器中保存的信息都设置有一个有效期,当缓存中的信息超过有效期后,数据就会从缓存中删除。
1.4 委托协议栈发送消息
1.4.1 数据收发操作概览
知道了 IP 地址之后,就可以委托操作系统内部的协议栈向这个目标 IP 地址,也就是我们要访问的 Web 服务器发送消息了。这里需要按照指定的顺序调用多个程序组件,这个过程有点复杂,下图简要概述这个过程。
进行收发数据操作之前,双方需要先建立起收发数据的管道才行。建立管道的关键在于管道两端的数据出入口,这些出入口称为套接字。
首先,服务器一方先创建套接字,然后等待客户端向该套接字连接管道 。当服务器进入等待状态时,客户端就可以连接管道了。具体来说,客户端也会先创建一个套接字,当双方的套接字连接起来之后,通信准备就完成了。接下来,就像我们刚刚讲过的一样,只要将数据送入套接字就可以收发数据了。
当数据全部发送完毕之后,连接的管道将会被断开。管道在连接时是由客户端发起的,但在断开时可以由客户端或服务器任意一方发起。其中一方断开后,另一方也会随之断开,当管道断开后,套接字也会被删除。到此为止,通信操作就结束了。
综上所述,收发数据的操作分为若干个阶段,可以大致总结为以下 4 个:
- 创建套接字(创建套接字阶段) 。
- 将管道连接到服务器端的套接字上(连接阶段) 。
- 收发数据(通信阶段) 。
- 断开管道并删除套接字(断开阶段)
这4个操作都是由操作系统中的协议栈来执行的,浏览器等应用程序并不会自己去做连接管道、放入数据这些工作,而是委托协议栈来代劳。
下面分阶段来介绍一下收发数据的具体过程。
1.4.2 创建套接字阶段
应用程序(浏览器)委托收发数据过程的关键点就是像对 DNS 服务器发送查询一样,调用 Socket 库中的socket 程序组件。
套接字创建完成后,协议栈会返回一个描述符,应用程序会将收到的描述符存放在内存中。描述符是用来识别计算机内协议栈中不同的套接字的,不是用来识别另一方的套接字。比如浏览器打开多个网页,就会在客户端协议栈内创建多个套接字,这里的描述符就是用来识别客户端协议栈内多个套接字的。只要我们出示描述符, 协议栈就能够判断出我们希望用哪一个套接字来连接或者收发数据了。
1.4.3 连接阶段
接下来,我们需要委托协议栈将客户端创建的套接字与服务器那边的套接字连接起来。应用程序通过调用Socket库中的connect的程序组件来完成这一操作。当调用 connect 时,需要指定描述符、 服务器 IP 地址和端口号这 3 个参数,下面介绍一下这三个参数的作用。
- 描述符:就是在创建套接字的时候由协议栈返回的那 个描述符。connect 会将应用程序指定的描述符告知协议栈,然后协议栈根据这个描述符来判断到底使用哪一个套接字去和服务器端的套接字进行连接,并执行连接的操作。
- 服务器 IP 地址,就是通过 DNS 服务器查询得到的我们要访问的服务器的 IP 地址。在 DNS 服务器的部分已经讲过,在进行数据收发操作时,双方必须知道对方的 IP 地址并告知协议栈。
- 端口号:描述符是用来在一台机器中选择协议栈某一个套接字进行连接操作的,端口号则是用来在服务器中识别具体的套接字的,IP地址用来连接到某个服务器,端口号则是具体选择对应服务器的某个套接字。服务器上所使用的端口号是根据应用的种类事先规定好的,比如 Web 是 80 号端口,电子邮件是 25 号端口。
当连接成功后,协议栈会将对方的 IP 地址和端口号等信息保存在套接字中,这样我们就可以开始收发数据了。
1.4.4 通信阶段:传递消息
当套接字连接起来之后,剩下的事情就简单了。只要将数据送入套接字,数据就会被发送到对方的套接字中。这个操作需 要使用Socket库中write这个程序组件,具体过程如下。
首先,应用程序需要在内存中准备好要发送的数据。根据用户输入的网址生成的 HTTP 请求消息就是我们要发送的数据。接下来,当调用 write 时,需要指定描述符和发送数据,然后协议栈就会将数据发送到服务器。由于在完成上一个连接阶段之后,套接字中已经保存了已连接的通信对象的相关信息(包括端口号),所以只要通过描述符指定套接字,就可以识别出通信对象,并向其发送数据。 接着,发送数据会通过网络到达我们要访问的服务器。
接下来,服务器执行接收操作,解析收到的数据内容并执行相应的操作,向客户端返回响应消息。
当消息返回后,需要执行的是接收消息的操作。接收消息的操作是通过 Socket 库中的 read 程序组件委托协议栈来完成的。调用 read 时需要指定用于存放接收到的响应消息的内存地址,这一内存地址称为接收缓冲区。接收缓冲区是一块位于应用程序内部的内存空间,因此当消息被存放到接收缓冲区中时,就相当于已经转交给了应用程序。
1.4.5 断开阶段:收发数据结束
当浏览器收到数据之后,收发数据的过程就结束了。接下来,我们需要调用 Socket 库的 close 程序组件进入断开阶段,之后协议栈内对应的套接字会被删除,数据通路(前面说的管道)也随之消失。
断开的过程如下。Web 使用的 HTTP 协议规定,当 Web 服务器发送完响应消息之后,应该主动执行断开操作 ,因此 Web 服务器会首先调用 close 来断开连接。断开操作传达到客户端之后,客户端的套接字也会进入 断开阶段。接下来,当浏览器调用 read 执行接收数据操作时,read 会告知浏览器收发数据操作已结束,连接已经断开。浏览器得知后,也会调用close 进入断开阶段。
要注意的是,根据应用种类不同,客户端和服务器哪一方先执行close都有可能。有些应用中是客户端先执行close。
这就是 HTTP 的工作过程。HTTP 协议将 HTML 文档和图片都作为单独的对象来处理,每获取一次数据,就要执行一次连接、发送请求消息、 接收响应消息、断开的过程。因此,如果一个网页中包含很多张图片,就必须重复进行很多次连接、收发数据、断开的操作。
第一章结语
到这里第一章就结束了,本章了解了浏览器与 Web 服务器之间收发消息的过程,但实际负责收发消息的是协议栈、网卡驱动和网卡,只有这 3 者相互配合,数据才能够在网络中流动起来。下一章将对这一部分进行探索。