【网络是怎样连接的】第1章:浏览器生成消息——探索浏览器内部

URL,Web服务器,FTP服务器,域名,端口号,HTTP协议,URI,CGI程序,IP地址,路由器,集线器,子网掩码,DNS,Socket库,协议栈,TCP协议,套接字

Posted by x-jeff on September 11, 2023

博客为参考《网络是怎样连接的》一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.前言

👉热身问答,下列说法是正确的还是错误的:

  1. http://www.nikkeibp.co.jp/中的www代表World Wide Web协议(对通信操作规则所作的定义)。
  2. 个人也可以申请注册互联网中的域名。
  3. 浏览器等网络应用程序实际上并不具备网络控制功能。

👉答案:

  1. 错误。http://www.nikkeibp.co.jp/中的www只是Web服务器上的一种命名。而且,World Wide Web也不是一个协议的名字,而是Web的提出者最早开发的浏览器兼HTML编辑器的名字。
  2. 正确。如果是“.com”、“.net”、“.org”、“.jp”(除“co.jp”、“ne.jp”等“xx.jp”格式的域名外)$^1$等没有对注册对象范围进行限制的域名,任何个人都可以申请注册。此外,也有一种“.name”域名是专门为个人申请者准备的。
  3. 正确。应用程序并不是自己去控制网络,而是委托操作系统来控制网络。
  1. 中国的情况类似,个人可以申请“.cn”域名,但“.com.cn”、“.net.cn”等域名则是不开放给个人注册的。此外,日本的域名体系中,“.jp”下级的域名用的是两个字母的命名,例如“.co.jp”、“ne.jp”,而中国使用的是三个字母的命名,例如“.com.cn”、“.net.cn”。

2.生成HTTP请求消息

2.1.探索之旅从输入网址开始

我们的探索之旅从在浏览器中输入网址开始$^1$,在介绍浏览器的工作方式之前,让我们先来介绍一下网址。网址,准确来说应该叫URL$^2$,如果我说它就是以http://开头的那一串东西,恐怕大家一下子就明白了,但实际上除了“http:”,网址还可以以其他一些文字开头,例如“ftp:”、“file:”、“mailto:”$^3$等。

  1. 某些情况下,浏览器的工作是从点击网页中的一个链接开始,大家可以认为这种情况与将链接中所包含的网址输入到浏览器的地址栏中是一样的。
  2. URL:Uniform Resource Locator,统一资源定位符。
  3. 如果没有正确配置电子邮件软件,则即使在地址栏中输入“mailto:”也是无法正常工作的。

之所以有各种各样的URL,是因为尽管我们通常是使用浏览器来访问Web服务器的,但实际上浏览器并不只有这一个功能,它也可以用来在FTP$^1$服务器上下载和上传文件,同时也具备电子邮件客户端的功能。可以说,浏览器是一个具备多种客户端功能的综合性客户端软件,因此它需要一些东西来判断应该使用其中哪种功能来访问相应的数据,而各种不同的URL就是用来干这个的,比如访问Web服务器时用“http:”,而访问FTP服务器时用“ftp:”。

  1. FTP:File Transfer Protocol,文件传送协议。这是一种在上传、下载文件时使用的协议。使用FTP协议来传送文件的程序也被叫作FTP。

图1.1列举了现在互联网中常见的几种URL,根据访问目标的不同,URL的写法也会不同。例如在访问Web服务器和FTP服务器时,URL中会包含服务器的域名$^1$和要访问的文件的路径名等,而发邮件的URL则包含收件人的邮件地址。此外,根据需要,URL中还会包含用户名、密码、服务器端口号$^2$等信息。

  1. 域名:就是像www.glasscom.com这样以句点(.)分隔的名称。后文会有详细说明。
  2. 端口号:一个用来识别要连接的服务器程序的编号。不同的服务器程序会使用不同的编号,例如Web是80,邮件是25等。后文会有详细说明。

尽管URL有各种不同的写法,但它们有一个共同点,那就是URL开头的文字,即“http:”、“ftp:”、“file:”、“mailto:”这部分文字都表示浏览器应当使用的访问方法。比如当访问Web服务器时应该使用HTTP$^1$协议,而访问FTP服务器时则应该使用FTP协议。因此,我们可以把这部分理解为访问时使用的协议$^2$类型$^3$。尽管后面部分的写法各不相同,但开头部分的内容决定了后面部分的写法,因此并不会造成混乱。

  1. HTTP:Hypertext Transfer Protocol,超文本传送协议。
  2. 协议:通信操作的规则定义称为协议(protocol)。
  3. 像“file:”这样的URL在访问时是不使用网络的,因此说URL的开头部分表示的是协议类型并不完全准确,也许理解为“访问方法”会更好一些。

2.2.浏览器先要解析URL

浏览器要做的第一步工作就是对URL进行解析,从而生成发送给Web服务器的请求消息。URL的格式会随着协议的不同而不同,因此下面我们以访问Web服务器的情况为例来进行讲解。

根据HTTP的规格,URL包含图1.2(a)中的这几种元素。当对URL进行解析时,首先需要按照图1.2(a)的格式将其中的各个元素拆分出来,例如图1.2(b)中的URL会拆分成图1.2(c)的样子。然后,通过拆分出来的这些元素,我们就能够明白URL代表的含义。例如,我们来看拆分结果图1.2(c),其中包含Web服务器名称www.lab.glasscom.com,以及文件的路径名/dir1/file1.html,因此我们就能够明白,图1.2(b)中的URL表示要访问www.lab.glasscom.com这个Web服务器上路径名为/dir1/file1.html的文件,也就是位于/dir1/目录$^1$下的file1.html这个文件(图1.3)。

  1. 目录(directory)这个词的意思相当于Windows中的文件夹(folder)。

2.3.省略文件名的情况

图1.2(b)是一个以“http:”开头的典型URL,但有时候我们也会见到一些不太一样的URL,例如下面这个URL是以“/”来结尾的。

我们可以这样理解,以“/”结尾代表/dir/后面本来应该有的文件名被省略了。根据URL的规则,文件名可以像前面这样省略。

不过,没有文件名,服务器怎么知道要访问哪个文件呢?其实,我们会在服务器上事先设置好文件名省略时要访问的默认文件名。这个设置根据服务器不同而不同,大多数情况下是index.html或者default.htm之类的文件名。因此,像前面这样省略文件名时,服务器就会访问/dir/index.html或者/dir/default.htm。

还有一些URL是像下面这样只有Web服务器的域名的,这也是一种省略了文件名的形式。

这个URL也是以“/”结尾的,也就是说它表示访问一个名叫“/”的目录$^1$。而且,由于省略了文件名,所以结果就是访问/index.html或者/default.htm这样的文件了。

  1. “/”目录表示目录层级中最顶层的“根目录”。

这次连结尾的“/”都省略了。这种写法也是允许的。当没有路径名时,就代表访问根目录下事先设置的默认文件$^1$,也就是/index.html或者/default.htm这些文件,这样就不会发生混乱了。

  1. 最早的时候这个文件被叫作“主页”(home page),意思就是当省略文件名时访问的那个默认的页面。随着Web的普及,这个词的意义似乎并没有被正确理解,现在不光是默认页面,似乎随便什么网页都可以被叫作主页了。

一般来说,这种情况会按照下面的惯例进行处理:如果Web服务器上存在名为whatisthis的文件,则将whatisthis作为文件名来处理;如果存在名为whatisthis的目录,则将whatisthis作为目录名来处理$^1$。

  1. 我们无法创建两个名字相同的文件和目录,因此不可能既有一个名为whatisthis的文件,同时又有一个名为whatisthis的目录。只要查询一下磁盘中的文件和目录,就可以知道whatisthis究竟是一个文件还是一个目录了,并不会产生歧义。

浏览器的第一步工作就是对URL进行解析。

2.4.HTTP的基本思路

解析完URL之后,我们就知道应该要访问的目标在哪里了。接下来,浏览器会使用HTTP协议来访问Web服务器,不过在介绍这一环节之前,我们先来讲一讲HTTP协议到底是怎么回事。

HTTP协议定义了客户端和服务器之间交互的消息内容和步骤,其基本思路非常简单。首先,客户端会向服务器发送请求消息(图1.4)。请求消息中包含的内容是“对什么”和“进行怎样的操作”两个部分。其中相当于“对什么”的部分称为URI$^1$。一般来说,URI的内容是一个存放网页数据的文件名或者是一个CGI程序$^2$的文件名,例如“/dir1/file1.html”、“/dir1/program1.cgi”等$^3$。不过,URI不仅限于此,也可以直接使用“http:”开头的URL来作为URI。换句话说就是,这里可以写各种访问目标,而这些访问目标统称为URI。

  1. URI:Uniform Resource Identifier,统一资源标识符。
  2. CGI程序:对Web服务器程序调用其他程序的规则所做的定义就是CGI,而按照CGI规范来工作的程序就称为CGI程序。
  3. 实际上,这个文件在Web服务器上未必是真实存在的,因为Web服务器可以通过重写规则对虚拟的URI进行映射。

相当于接下来“进行怎样的操作”的部分称为方法$^1$。方法表示需要让Web服务器完成怎样的工作,其中典型的例子包括读取URI表示的数据、将客户端输入的数据发送给URI表示的程序等。表1.1列举了主要的方法,通过这张表大家应该能够理解通过方法可以执行怎样的操作。

  1. 也叫HTTP谓词,或者HTTP动词。

除了图1.4中的内容之外,HTTP消息中还有一些用来表示附加信息的头字段。客户端向Web服务器发送数据时,会先发送头字段,然后再发送数据。不过,头字段属于可有可无的附加信息。

收到请求消息之后,Web服务器会对其中的内容进行解析,通过URI和方法来判断“对什么”“进行怎样的操作”,并根据这些要求来完成自己的工作,然后将结果存放在响应消息中。在响应消息的开头有一个状态码,它用来表示操作的执行结果是成功还是发生了错误。当我们访问Web服务器时,遇到找不到的文件就会显示出404 Not Found的错误消息,其实这就是状态码。状态码后面就是头字段和网页数据。响应消息会被发送回客户端,客户端收到之后,浏览器会从消息中读出所需的数据并显示在屏幕上。到这里,HTTP的整个工作就完成了。

表1.1列出的方法中,最常用的一个就是GET方法了。一般当我们访问Web服务器获取网页数据时,使用的就是GET方法。所谓一般的访问过程大概就是这样的:首先,在请求消息中写上GET方法,然后在URI中写上存放网页数据的文件名“/dir1/file1.html”,这就表示我们需要获取/dir1/file1.html文件中的数据。当Web服务器收到消息后,会打开/dir1/file1.html文件并读取出里面的数据,然后将读出的数据存放到响应消息中,并返回给客户端。最后,客户端浏览器会收到这些数据并显示在屏幕上。

还有一个经常使用的方法就是POST。我们在表单$^1$中填写数据并将其发送给Web服务器时就会使用这个方法。当我们在网上商城填写收货地址和姓名,或者是在网上填写问卷时,都会遇到带有输入框的网页,而这些可以输入信息的部分就是表单。使用POST方法时,URI会指向Web服务器中运行的一个应用程序$^2$的文件名,典型的例子包括“index.cgi”、“index.php”等。然后,在请求消息中,除了方法和URI之外,还要加上传递给应用程序和脚本的数据。这里的数据也就是用户在输入框里填写的信息。当服务器收到消息后,Web服务器会将请求消息中的数据发送给URI指定的应用程序。最后,Web服务器从应用程序接收输出的结果,会将它存放到响应消息中并返回给客户端。

  1. 表单:网页中的文本框、复选框等能够输入数据的部分。
  2. 用于处理购物订单数据或者问卷数据的程序。

前面两个方法属于HTTP的典型用法,除此之外的其他方法在互联网上几乎见不到使用的例子。但如果只有GET和POST方法,我们就只能从Web服务器中获取网页数据,以及将网页输入框中的信息发送给Web服务器,而有了PUT和DELETE方法,就能够从客户端修改或者删除Web服务器上的文件。

2.5.生成HTTP请求消息

对URL进行解析之后,浏览器确定了Web服务器和文件名,接下来就是根据这些信息来生成HTTP请求消息了。实际上,HTTP消息在格式上是有严格规定的,因此浏览器会按照规定的格式来生成请求消息(图1.5)。

23.$\ $准确来说,消息体的格式会通过消息头中的Content-Type字段来定义(MIME类型),关于MIME类型后续会有详细介绍。

浏览器和Web服务器根据此格式来生成消息。

首先,请求消息的第一行称为请求行。这里的重点是最开头的方法,方法可以告诉Web服务器它应该进行怎样的操作。不过这里必须先解决一个问题,那就是方法有很多种,我们必须先判断应该选用其中的哪一种。

解决这个问题的关键在于浏览器的工作状态。这次探索之旅是从在浏览器顶部的地址栏中输入网址开始的,但浏览器并非只有在这一种场景下才会向Web服务器发送请求消息。比如点击网页中的超级链接$^1$,或者在表单中填写信息后点击“提交”按钮,这些场景都会触发浏览器的工作,而选用哪种方法也是根据场景来确定的。

  1. 在HTML文档中写上<a href="......">标签,其中"......"部分为URL,这就是一个超级链接。

我们的场景是在地址栏中输入网址并显示网页,因此这里应该使用GET方法。点击超级链接的场景中也是使用GET方法。如果是表单,在HTML源代码中会在表单的属性中指定使用哪种方法来发送请求,可能是GET也可能是POST(图1.6)$^1$。

  1. GET方法能够发送的数据只有几百个字节,如果表单中的数据超过这一长度,则必须使用POST方法来发送。

写好方法之后,加一个空格,然后写URI。URI部分的格式如下,一般是文件和程序的路径名。

1
/<目录名>/.../<文件名>

前面已经讲过,路径名一般来说已经包含在URL中了,因此只要从URL中提取出来原封不动地写上去就好了。

第一行的末尾需要写上HTTP的版本号,这是为了表示该消息是基于哪个版本的HTTP规格编写的。到此为止,第一行就结束了。

第二行开始为消息头。尽管通过第一行我们就可以大致理解请求的内容,但有些情况下还需要一些额外的详细信息,而消息头的功能就是用来存放这些信息。消息头的规格中定义了很多项目,如日期、客户端支持的数据类型、语言、压缩格式、客户端和服务器的软件名称和版本、数据有效期和最后更新时间等。这些项目表示的都是非常细节的信息,因此要想准确理解这些信息的意思,就需要对HTTP协议有非常深入的了解。表1.2中列举了主要的头字段。消息头中的内容随着浏览器类型、版本号、设置等的不同而不同,大多数情况下消息头的长度为几行到十几行不等。

写完消息头之后,还需要添加一个完全没有内容的空行,然后写上需要发送的数据。这一部分称为消息体,也就是消息的主体。不过,在使用GET方法的情况下,仅凭方法和URI,Web服务器就能够判断需要进行怎样的操作,因此消息体中不需要填写任何数据。消息体结束之后,整个消息也就结束了。

当使用POST方法时,需要将表单中填写的信息写在消息体中。到此为止,请求消息的生成操作就全部完成了。

2.6.发送请求后会收到响应

当我们将上述请求消息发送出去之后,Web服务器会返回响应消息。关于响应消息我们将在第6章详细介绍,这里先粗略地了解一下。响应消息的格式以及基本思路和请求消息是相同的(图1.5(b)),差别只在第一行上。在响应消息中,第一行的内容为状态码和响应短语,用来表示请求的执行结果是成功还是出错。状态码和响应短语表示的内容一致,但它们的用途不同。状态码是一个数字,它主要用来向程序告知执行的结果(表1.3);相对地,响应短语则是一段文字,用来向人们告知执行的结果。

返回响应消息之后,浏览器会将数据提取出来并显示在屏幕上,我们就能够看到网页的样子了。如果网页的内容只有文字,那么到这里就全部处理完毕了,但如果网页中还包括图片等资源,则还有下文。

当网页中包含图片时,会在网页中的相应位置嵌入表示图片文件的标签$^1$的控制信息。浏览器会在显示文字时搜索相应的标签,当遇到图片相关的标签时,会在屏幕上留出用来显示图片的空间,然后再次访问Web服务器,按照标签中指定的文件名向Web服务器请求获取相应的图片并显示在预留的空间中。这个步骤和获取网页文件时一样,只要在URI部分写上图片的文件名并生成和发送请求消息就可以了。

  1. 标签:编写网页所使用的HTML语言中规定的控制信息。例如,当需要在网页中插入图片时,需要在相应位置嵌入形如<img src="image1.jpg">的标签。

由于每条请求消息中只能写1个URI,所以每次只能获取1个文件,如果需要获取多个文件,必须对每个文件单独发送1条请求。比如1个网页中包含3张图片,那么获取网页加上获取图片,一共需要向Web服务器发送4条请求。

判断所需的文件,然后获取这些文件并显示在屏幕上,这一系列工作的整体指挥也是浏览器的任务之一,而Web服务器却毫不知情。Web服务器完全不关心这4条请求获取的文件到底是1个网页上的还是不同网页上的,它的任务就是对每一条单独的请求返回1条响应而已。

到这里,我们已经介绍了浏览器与Web服务器进行交互的整个过程。作为参考,图1.7展示了浏览器与Web服务器之间交互消息的一个实例。在这个例子中,我们需要获取一张名为sample1.htm的网页,网页中包含一张名为picture.jpg的图片,图中展示了这个过程中产生的消息。

3.向DNS服务器查询Web服务器的IP地址

3.1.IP地址的基本知识

生成HTTP消息之后,接下来我们需要委托操作系统将消息发送给Web服务器。尽管浏览器能够解析网址并生成HTTP消息,但它本身并不具备将消息发送到网络中的功能,因此这一功能需要委托操作系统来实现$^1$。在进行这一操作时,我们还有一个工作需要完成,那就是查询网址中服务器域名对应的IP地址。在委托操作系统发送消息时,必须要提供的不是通信对象的域名,而是它的IP地址。因此,在生成HTTP消息之后,下一个步骤就是根据域名查询IP地址。在讲解这一操作之前,让我们先来简单了解一个IP地址。

  1. 发送消息的功能对于所有的应用程序来说都是通用的,因此让操作系统来实现这一功能,其他应用程序委托操作系统来进行操作,这是一个比较合理的做法。

互联网和公司内部的局域网都是基于TCP/IP的思路来设计的,所以我们先来了解TCP/IP的基本思路。TCP/IP的结构如图1.8所示,就是由一些小的子网,通过路由器$^1$连接起来组成一个大的网络。这里的子网可以理解为用集线器$^2$连接起来的几台计算机$^3$,我们将它看作一个单位,称为子网。将子网通过路由器连接起来,就形成了一个网络$^4$。

  1. 路由器:一种对包进行转发的设备,在第3章有详细介绍。
  2. 集线器:一种对包进行转发的设备,分为中继式集线器和交换式集线器两种,在第3章有详细介绍。
  3. 当计算机数量较少时,可以用一台集线器连接起来;当计算机数量较多时,一台集线器可能无法连接这么多计算机,可以增加集线器数量并将集线器相互连接起来,这时,凡是通过集线器连接起来的所有设备都属于同一个子网。
  4. 一些家用路由器中已经内置了集线器功能,因此大家可以理解为这种路由器内部同时包含路由器和集线器两种设备,它们在里面已经连接起来了。

在网络中,所有的设备都会被分配一个地址。这个地址就相当于现实中某条路上的“xx号xx室”。其中“号”对应的号码是分配给整个子网的,而“室”对应的号码是分配给子网中的计算机的,这就是网络中的地址。“号”对应的号码称为网络号,“室”对应的号码称为主机号,这个地址的整体称为IP地址$^1$。通过IP地址我们可以判断出访问对象服务器的位置,从而将消息发送到服务器。消息传送的具体过程在后面的章节有详细讲解,不过现在我们先简单了解一下。发送者发出的消息首先经过子网中的集线器$^2$,转发到距离发送者最近的路由器上(图1.8①)。接下来,路由器会根据消息的目的地判断下一个路由器的位置,然后将消息发送到下一个路由器,即消息再次经过子网内的集线器被转发到下一个路由器(图1.8②)。前面的过程不断重复,最终消息就被传送到了目的地。

  1. IP地址和现实中的地址含义是相同的,因此就像“xx号xx室”不能有两户人家的号码相同一样,也不能有两台设备使用相同的IP地址。现实中其实存在因为疏漏两台设备被分配了相同的IP地址的情况,但这种情况下网络会发生故障,无法正常工作。
  2. 数据是以包的形式传送的。

如图1.9所示,实际的IP地址是一串32比特的数字,按照8比特(1字节)为一组分成4组,分别用十进制表示然后再用圆点隔开。这就是我们平常经常见到的IP地址格式,但仅凭这一串数字我们无法区分哪部分是网络号,哪部分是主机号。在IP地址的规则中,网络号和主机号连起来总共是32比特,但这两部分的具体结构是不固定的。在组建网络时,用户可以自行决定它们之间的分配关系,因此,我们还需要另外的附加信息来表示IP地址的内部结构。

这一附加信息称为子网掩码。子网掩码的格式如图1.10②所示,是一串与IP地址长度相同的32比特数字,其左边一半都是1,右边一半都是0。其中,子网掩码为1的部分表示网络号,子网掩码为0的部分表示主机号。将子网掩码按照和IP地址一样的方式以每8比特为单位用圆点分组后写在IP地址的右侧,这就是图1.9(b)的方法。这种写法太长,我们也可以把1的部分的比特数用十进制表示并写在IP地址的右侧,如图1.9(c)所示。这两种方式只是写法上的区别,含义是完全一样的。

子网掩码表示网络号与主机号之间的边界。在本例中,这个边界与字节的边界是正好吻合的,也就是正好划分在句点的位置上,实际上也可以划分在字节的中间位置。

主机号部分的比特全部为0或者全部为1时代表两种特殊的含义。主机号部分全部为0代表整个子网而不是子网中的某台设备(图1.9(d))。此外,主机号部分全部为1代表向子网上所有设备发送包,即广播(图1.9(e))。

3.2.域名和IP地址并用的理由

TCP/IP网络是通过IP地址来确定通信对象的,因此不知道IP地址就无法将消息发送给对方。因此,在委托操作系统发送消息时,必须要先查询好对方的IP地址。

可能你会问“既然如此,那么在网址中不写服务器的名字,直接写IP地址不就好了吗?”实际上,如果用IP地址来代替服务器名称也是能够正常工作的$^1$。然而,就像你很难记住电话号码一样,要记住一串由数字组成的IP地址也非常困难。因此,相比IP地址来说,网址中还是使用服务器名称比较好。

  1. 如果Web服务器使用了虚拟主机功能,有可能无法通过IP地址来访问。

那么又有人问了:“既然如此,那干脆不要用IP地址,而是用名称来确定通信对象不就好了吗?”这样的想法其实并不奇怪$^1$。

  1. 实际上真的存在以名称来确定通信对象的网络,Windows网络的原型PC-Networks就是其中的一个例子。

不过从运行效率上来看,这并不能算是一个好主意。互联网中存在无数的路由器,它们之间相互配合,根据IP地址来判断应该把数据传送到什么地方。那么如果我们不用IP地址而是改用名称会怎么样呢?IP地址的长度为32比特,也就是4字节,相对地,域名最短也要几十个字节,最长甚至可以达到255字节。换句话说,使用IP地址只需要处理4字节的数字,而域名则需要处理几十个到255个字节的字符,这增加了路由器的负担,传送数据也会花费更长的时间$^1$。可能有人会说:“那使用高性能路由器不就能解决这个问题了吗?”然而,路由器的速度是有极限的,而互联网内部流动的数据量已然让路由器疲于应付了,因此我们不应该再采用效率更低的设计。随着技术的发展,路由器的性能也会不断提升,但与此同时,数据量也在以更快的速度增长,在可预见的未来,这样的趋势应该不会发生变化。出于这样的原因,使用名称本身来确定通信对象并不是一个聪明的设计。

  1. 域名并不仅是长,而且其长度是不固定的。处理长度不固定的数据比处理长度固定的数据要复杂,这也是造成效率低下的重要原因之一。

于是,现在我们使用的方案是让人来使用名称,让路由器来使用IP地址。为了填补两者之间的障碍,需要有一个机制能够通过名称来查询IP地址,或者通过IP地址来查询名称,这样就能够在人和机器双方都不做出牺牲的前提下完美地解决问题。这个机制就是DNS$^1$。

  1. DNS:Domain Name System,域名服务系统。将服务器名称和IP地址进行关联是DNS最常见的用法,但DNS的功能并不仅限于此,它还可以将邮件地址和邮件服务器进行关联,以及为各种信息关联相应的名称。

3.3.Socket库提供查询IP地址的功能

查询IP地址的方法非常简单,只要询问最近的DNS服务器“www.lab.glasscom.com的IP地址是什么”就可以了,DNS服务器会回答说“该服务器的IP地址为xxx.xxx.xxx.xxx”。

向DNS服务器发出查询,也就是向DNS服务器发送查询消息,并接收服务器返回的响应消息。换句话说,对于DNS服务器,我们的计算机上一定有相应的DNS客户端,而相当于DNS客户端的部分称为DNS解析器,或者简称解析器。通过DNS查询IP地址的操作称为域名解析,因此负责执行解析(resolution)这一操作的就叫解析器(resolver)了。

解析器实际上是一段程序,它包含在操作系统的Socket库中,该库包含的程序组件可以让其他的应用程序调用操作系统的网络功能$^1$,而解析器就是这个库中的其中一种程序组件。

  1. Socket库是在加州大学伯克利分校开发的UNIX系操作系统BSD中开发的C语言库,互联网中所使用的大多数功能都是基于Socket库来开发的。因此,BSD之外的其他操作系统以及C语言之外的其他编程语言也参照Socket库开发了相应的网络库。可以说,Socket库是网络开发中的一种标准库。

Socket库中包含很多用于发送和接收数据的程序组件。Socket库是用于调用网络功能的程序组件集合。

3.4.通过解析器向DNS服务器发出查询

解析器的用法非常简单。Socket库中的程序都是标准组件,只要从应用程序中进行调用就可以了。具体来说,在编写浏览器等应用程序的时候,只要像图1.11这样写上解析器的程序名称“gethostbyname”以及Web服务器的域名“www.lab.glasscom.com”就可以了,这样就完成了对解析器的调用$^1$。

  1. 实际上,除此之外还需要编写一些用于分配保存IP地址的内存空间的语句,并在程序开头使用#include命令将其包含进来。

在应用程序中编写上图中的一行代码后就能够调用解析器完成向DNS服务器查询IP地址的操作。

调用解析器后,解析器会向DNS服务器发送查询消息,然后DNS服务器会返回响应消息。响应消息中包含查询到的IP地址,解析器会取出IP地址,并将其写入浏览器指定的内存地址中。只要运行图1.11中的这一行程序,就可以完成前面所有这些工作,我们也就完成了IP地址的查询。接下来,浏览器在向Web服务器发送消息时,只要从该内存地址取出IP地址,并将它与HTTP请求消息一起交给操作系统就可以了。

根据域名查询IP地址时,浏览器会使用Socket库中的解析器。

3.5.解析器的内部原理

下面来看一看当应用程序调用解析器时,解析器内部是怎样工作的(图1.12)。网络应用程序(在我们的场景中就是指浏览器)调用解析器时,程序的控制流程就会转移到解析器的内部。

解析器会生成要发送给DNS服务器的查询消息,这个过程与浏览器生成要发送给Web服务器的HTTP请求消息的过程类似$^1$。发送消息这个操作并不是由解析器自身来执行,而是要委托给操作系统内部的协议栈$^2$来执行。这是因为和浏览器一样,解析器本身也不具备使用网络收发数据的功能。解析器调用协议栈后,控制流程会再次转移,协议栈会执行发送消息的操作,然后通过网卡将消息发送给DNS服务器。

  1. HTTP消息是用文本编写的,但DNS消息是使用二进制数据编写的。
  2. 协议栈:操作系统内部的网络控制软件,也叫“协议驱动”“TCP/IP驱动”等。

如果要访问的Web服务器已经在DNS服务器上注册,那么这条记录就能够被找到,然后其IP地址会被写入响应消息并返回给客户端。

顺带一提,向DNS服务器发送消息时,我们当然也需要知道DNS服务器的IP地址。只不过这个IP地址是作为TCP/IP的一个设置项目事先设置好的,不需要再去查询了。不同的操作系统中TCP/IP的设置方法也有差异,Windows中的设置如图1.13所示,解析器会根据这里设置的DNS服务器IP地址来发送消息。

4.全世界DNS服务器的大接力

4.1.DNS服务器的基本工作

DNS服务器的基本工作就是接收来自客户端的查询消息,然后根据消息的内容返回响应。

其中,来自客户端的查询消息包含以下3种信息。

  1. 域名
    • 服务器、邮件服务器(邮件地址中@后面的部分)的名称。
  2. Class
    • 在最早设计DNS方案时,DNS在互联网以外的其他网络中的应用也被考虑到了,而Class就是用来识别网络的信息。不过,如今除了互联网并没有其他的网络了,因此Class的值永远是代表互联网的IN。
  3. 记录类型
    • 表示域名对应何种类型的记录。例如,当类型为A时,表示域名对应的是IP地址;当类型为MX时,表示域名对应的是邮件服务器。对于不同的记录类型,服务器向客户端返回的信息也会不同。

DNS服务器上事先保存有前面这3种信息对应的记录数据,如图1.14所示。DNS服务器就是根据这些记录查找符合查询请求的内容并对客户端作出响应的。

Web服务器的域名有很多都是像www.lab.glasscom.com这样以www开头的,但这并不是一定之规,只是因为最早设计Web的时候,很多Web服务器都采用了www这样的命名,后来就形成了一个惯例而已。因此,无论是WebServer1也好,MySrv也好,只要是作为A$^1$记录在DNS服务器上注册的,都可以作为Web服务器的域名$^2$。

  1. A是Address的缩写。
  2. 不仅是Web服务器,像邮件服务器、数据库服务器等,无论任何服务器,只要注册了A类型的记录,都可以作为服务器的域名来使用。准确来说,A类型的记录表示与IP地址所对应的域名,因此与其说是某个服务器的域名,不如说是被分配了某个IP地址的某台具体设备的域名。

在查询IP地址时我们使用A这个记录类型,而查询邮件服务器时则要使用MX$^1$类型。这是因为在DNS服务器上,IP地址是保存在A记录中的,而邮件服务器则是保存在MX记录中的。例如,对于一个邮件地址tone@glasscom.com,当需要知道这个地址对应的邮件服务器时,我们需要提供@后面的那一串名称。查询消息的内容如下。

  1. MX:Mail eXchange,邮件交换。
  • (a)域名=glasscom.com
  • (b)Class=IN
  • (c)记录类型=MX

DNS服务器会返回10和mail.glasscom.com这两条消息。当记录类型为MX时,DNS服务器会在记录中保存两种信息,分别是邮件服务器的域名和优先级$^1$。此外,MX记录的返回消息还包括邮件服务器mail.glasscom.com的IP地址。上表的第三行就是mail.glasscom.com的IP地址,因此只要用mail.glasscom.com的域名就可以找到这条记录。在这个例子中,我们得到的IP地址是192.0.2.227。

  1. 当一个邮件地址对应多个邮件服务器时,需要根据优先级来判断哪个邮件服务器是优先的。优先级数值较小的邮件服务器代表更优先。

综上所述,DNS服务器的基本工作就是根据需要查询的域名和记录类型查找相关的记录,并向客户端返回响应消息。

前面只介绍了A和MX这两个记录类型,实际上还有很多其他的类型。例如根据IP地址反查域名的PTR类型,查询域名相关别名的CNAME类型,查询DNS服务器IP地址的NS类型,以及查询域名属性信息的SOA类型等。

此外,虽然图1.14展示的是表格形式,但实际上这些信息是保存在配置文件中的,表格中的一行信息被称为一条资源记录。

4.2.域名的层次结构

在前面的讲解中,我们假设要查询的信息已经保存在DNS服务器内部的记录中了。如果是在像公司内部网络这样Web和邮件服务器数量有限的环境中,所有的信息都可以保存在一台DNS服务器中,其工作方式也就完全符合我们前面讲解的内容。然而,互联网中存在着不计其数的服务器,将这些服务器的信息全部保存在一台DNS服务器中是不可能的,因此一定会出现在DNS服务器中找不到要查询的信息的情况。下面来看一看此时DNS服务器是如何工作的。

直接说答案的话很简单,就是将信息分布保存在多台DNS服务器中,这些DNS服务器相互接力配合,从而查找出要查询的信息。不过,这个机制其实有点复杂,因此我们先来看一看信息是如何在DNS服务器上注册并保存的。

首先,DNS服务器中的所有信息都是按照域名以分层次的结构来保存的。

DNS中的域名都是用句点来分隔的,比如www.lab.glasscom.com,这里的句点代表了不同层次之间的界限。在域名中,越靠右的位置表示其层级越高,比如www.lab.glasscom.com这个域名如果按照公司里的组织结构来说,大概就是“com事业集团glasscom部lab科的www”这样。其中,相当于一个层级的部分称为域。因此,com域的下一层是glasscom域,再下一层是lab域,再下面才是www这个名字。

这种具有层次结构的域名信息会注册到DNS服务器中,而每个域都是作为一个整体来处理的。换句话说就是,一个域的信息是作为一个整体存放在DNS服务器中的,不能将一个域拆开来存放在多台DNS服务器中。不过,DNS服务器和域之间的关系也并不总是一对一的,一台DNS服务器中也可以存放多个域的信息。为了避免把事情搞得太复杂,这里先假设一台DNS服务器中只存放一个域的信息,后面的讲解也是基于这个前提来进行的。于是,DNS服务器也具有了像域名一样的层次结构,每个域的信息都存放在相应层级的DNS服务器中。例如,这里有一个公司的域,那么就相应地有一台DNS服务器,其中存放了公司中所有Web服务器和邮件服务器的信息$^1$。

  1. 实际上,由于一台DNS服务器可以存放多个域的信息,因此并不是每个域名都有一台与之相对应的DNS服务器。比如网络运营商的DNS服务器中就存放了很多个域的信息。

这里再补充一点。对于公司域来说,例如现在需要为每一个事业集团配备一台DNS服务器,分别管理各事业集团自己的信息,但我们之前也说过一个域是不可分割的,这该怎么办呢?没关系,我们可以在域的下面创建下级域$^1$,然后再将它们分别分配给各个事业集团。比如,假设公司的域为example.co.jp,我们可以在这个域的下面创建两个子域,即sub1.example.co.jp和sub2.example.co.jp,然后就可以将这两个下级域分配给不同的事业集团来使用。如果公司下级的组织不是事业部而是子公司,对于域来说也是没有区别的。因为域并不代表“事业集团”这一特定组织,无论是子公司还是什么别的组织名称,都可以分配相应的域。实际上,互联网中的域也是一样,通过创建下级的域来分配给不同的国家、公司和组织使用。通过实际的域名可能更容易理解,比如www.nikkeibp.co.jp这个域名,最上层的jp代表分配给日本这个国家的域;下一层的co是日本国内进行分类的域,代表公司;再下层的nikkeibp就是分配给某个公司的域;最下层的www就是服务器的名称。

  1. 下级的域称为“子域”。

4.3.寻找相应的DNS服务器并获取IP地址

下面再来看一看如何找到DNS服务器中存放的信息。这里的关键在于如何找到我们要访问的Web服务器的信息归哪一台DNS服务器管。

互联网中有数万台DNS服务器,肯定不能一台一台挨个去找。我们可以采用下面的办法。首先,将负责管理下级域的DNS服务器的IP地址注册到它们的上级DNS服务器中,然后上级DNS服务器的IP地址再注册到更上一级的DNS服务器中,以此类推。也就是说,负责管理lab.glasscom.com这个域的DNS服务器的IP地址需要注册到glasscom.com域的DNS服务器中,而glasscom.com域的DNS服务器的IP地址又需要注册到com域的DNS服务器中。这样,我们就可以通过上级DNS服务器查询出下级DNS服务器的IP地址,也就可以向下级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服务器(图1.15)。分配给根域DNS服务器的IP地址在全世界仅有13个$^1$,而且这些地址几乎不发生变化,因此将这些地址保存在所有的DNS服务器中也并不是一件难事。实际上,根域DNS服务器的相关信息已经包含在DNS服务器程序的配置文件中了,因此只要安装了DNS服务器程序,这些信息也就被自动配置好了。

  1. 根域DNS服务器在运营上使用多台服务器来对应一个IP地址,因此尽管IP地址只有13个,但其实服务器的数量是很多的。

到这里所有的准备工作就都完成了。当我们配置一台DNS服务器时,必须要配置好上面这些信息,这样DNS服务器就能够从上万台DNS服务器中找到目标服务器。下面就来看一看这个过程是如何进行的。

如图1.16所示,客户端首先会访问最近的一台DNS服务器(也就是客户端的TCP/IP设置中填写的DNS服务器地址),假设我们要查询www.lab.glasscom.com这台Web服务器的相关信息(图1.16①)。由于最近的DNS服务器中没有存放www.lab.glasscom.com这一域名对应的信息,所以我们需要从顶层开始向下查找。最近的DNS服务器中保存了根域DNS服务器的信息,因此它会将来自客户端的查询消息转发给根域DNS服务器(图1.16②)。根域服务器中也没有www.lab.glasscom.com这个域名,但根据域名结构可以判断这个域名属于com域,因此根域DNS服务器会返回它所管理的com域中的DNS服务器的IP地址,意思是“虽然我不知道你要查的那个域名的地址,但你可以去com域问问看”。接下来,最近的DNS服务器又会向com域的DNS服务器发送查询消息(图1.16③)。com域中也没有www.lab.glasscom.com这个域名的信息,和刚才一样,com域服务器会返回它下面的glasscom.com域的DNS服务器的IP地址。以此类推,只要重复前面的步骤,就可以顺藤摸瓜找到目标DNS服务器(图1.16⑤),只要向目标DNS服务器发送查询消息,就能够得到我们需要的答案,也就是www.lab.glasscom.com的IP地址了。

收到客户端的查询消息之后,DNS服务器会按照前面的方法来查询IP地址,并返回给客户端(图1.16⑥)。这样,客户端就知道了Web服务器的IP地址,也就能够对其进行访问了(图1.16⑦)。

图1.16中的①和⑥分别相当于图1.12中的⑤和⑥。

4.4.通过缓存加快DNS服务器的响应

图1.16展示的是基本原理,与真实互联网中的工作方式还是有一些区别的。在真实的互联网中,一台DNS服务器可以管理多个域的信息,因此并不是像图1.16这样每个域都有一台自己的DNS服务器。图中,每一个域旁边都写着一台DNS服务器,但现实中上级域和下级域有可能共享同一台DNS服务器。在这种情况下,访问上级DNS服务器时就可以向下跳过一级DNS服务器,直接返回再下一级DNS服务器的相关信息。

此外,有时候并不需要从最上级的根域开始查找,因为DNS服务器有一个缓存$^1$功能,可以记住之前查询过的域名。如果要查询的域名和相关信息已经在缓存中,那么就可以直接返回响应,接下来的查询可以从缓存的位置开始向下进行。相比每次都从根域找起来说,缓存可以减少查询所需的时间。

  1. 缓存:指的是将使用过的数据存放在离使用该数据的地方较近的高速存储装置中,以便提高后续访问速度的技术。这一技术有很多应用,如CPU和内存之间的缓存、磁盘和内存之间的缓存等,在网络中缓存也是一种用来提高访问速度的普遍性技术。

并且,当要查询的域名不存在时,“不存在”这一响应结果也会被缓存。这样,当下次查询这个不存在的域名时,也可以快速响应。

这个缓存机制中有一点需要注意,那就是信息被缓存后,原本的注册信息可能会发生改变,这时缓存中的信息就有可能是不正确的。因此,DNS服务器中保存的信息都设置有一个有效期,当缓存中的信息超过有效期后,数据就会从缓存中删除。而且,在对查询进行响应时,DNS服务器也会告知客户端这一响应的结果是来自缓存中还是来自负责管理该域名的DNS服务器。

5.委托协议栈发送消息

5.1.数据收发操作概览

知道了IP地址之后,就可以委托操作系统内部的协议栈向这个目标IP地址,也就是我们要访问的Web服务器发送消息了。要发送给Web服务器的HTTP消息是一种数字信息(digital data),因此也可以说是委托协议栈来发送数字信息。收发数字信息这一操作不仅限于浏览器,对于各种使用网络的应用程序来说都是共通的。因此,这一操作的过程也不仅适用于Web,而是适用于任何网络应用程序$^1$。下面就来一起探索这一操作的过程。

  1. 通过DNS服务器查询IP地址的操作也同样适用于所有网络应用程序。

和向DNS服务器查询IP地址的操作一样,这里也需要使用Socket库中的程序组件。不过,查询IP地址只需要调用一个程序组件就可以了,而这里需要按照指定的顺序调用多个程序组件,这个过程有点复杂。发送数据是一系列操作相结合来实现的,如果不能理解这个操作的全貌,就无法理解其中每个操作的意义。因此,我们先来介绍一下收发数据操作的整体思路。

向操作系统内部的协议栈发出委托时,需要按照指定的顺序来调用Socket库中的程序组件。

使用Socket库来收发数据的操作过程如图1.17所示$^1$。简单来说,收发数据的两台计算机之间连接了一条数据通道,数据沿着这条通道流动,最终到达目的地。我们可以把数据通道想象成一条管道,将数据从一端送入管道没数据就会到达管道的另一端然后被取出。数据可以从任何一端被送入管道,数据的流动是双向的。

  1. 图1.17中展示的是用TCP协议来收发数据的过程,还有另外一种名为UDP(User Datagram Protocol,用户数据报协议)的协议,其收发数据的过程将在后面进行讲解。

收发数据的整体思路就是这样,但还有一点也非常重要。光从图上来看,这条管道好像一开始就有,实际上并不是这样,在进行收发数据操作之前,双方需要先建立起这条管道才行。建立管道的关键在于管道两端的数据出入口,这些出入口称为套接字。我们需要先创建套接字,然后再将套接字连接起来形成管道。实际的过程是下面这样的。首先,服务器一方先创建套接字,然后等待客户端向该套接字连接管道$^1$。当服务器进入等待状态时,客户端就可以连接管道了。具体来说,客户端也会先创建一个套接字,然后从该套接字延伸出管道,最后管道连接到服务器端的套接字上。当双方的套接字连接起来之后,通信准备就完成了。接下来,就像我们刚刚讲过的一样,只要将数据送入套接字就可以收发数据了。

  1. 服务器程序一般会在启动后就创建好套接字并等待客户端连接管道。

我们再来看一看收发数据操作结束时的情形。当数据全部发送完毕之后,连接的管道将会被断开。管道在连接时是由客户端发起的,但在断开时可以由客户端或服务器任意一方发起$^1$。其中一方断开后,另一方也会随之断开,当管道断开后,套接字也会被删除。到此为止,通信操作就结束了。综上所述,收发数据的操作分为若干个阶段,可以大致总结为以下4个。

  1. 实际上,管道切断的顺序是根据应用程序的规则来决定的。在Web中,断开顺序根据HTTP版本的不同而不同,在HTTP1.0中,当服务器向客户端发送完所有Web数据之后,服务器一方会断开管道。
  1. 创建套接字(创建套接字阶段)
  2. 将管道连接到服务器端的套接字上(连接阶段)
  3. 收发数据(通信阶段)
  4. 断开管道并删除套接字(断开阶段)

在每个阶段,Socket库中的程序组件都会被调用来执行相关的数据收发操作。不过,在探索其具体过程之前,我们来补充一点内容。前面这4个操作都是由操作系统中的协议栈来执行的,浏览器等应用程序并不会自己去做连接管道、放入数据这些工作,而是委托协议栈来代劳。本章将要介绍的只是这个“委托”的操作。关于协议栈收到委托之后具体是如何连接管道和放入数据的,我们将在第2章介绍。此外,这些委托的操作都是通过调用Socket库中的程序组件来执行的,但这些数据通信用的程序组件其实仅仅充当了一个桥梁的角色,并不执行任何实质性的操作,应用程序的委托内容最终会被原原本本地传递给协议栈。因此,我们无法形象地展示这些程序组件到底完成了怎样的工作,与其勉强强调Socket库的存在,还不如将Socket库和协议栈看成一个整体并讲解它们的整体行为让人更容易理解。因此,后文将会采用这样的讲法。不过,请大家不要忘记Socket库这一桥梁的存在,正如图1.12中所示的一样。

5.2.创建套接字阶段

下面我们就来探索一下应用程序(浏览器)委托收发数据的过程。这个过程的关键点就是像对DNS服务器发送查询一样,调用Socket库中的特定程序组件。访问DNS服务器时我们调用的是一个叫作gethostbyname的程序组件(也就是解析器),而这一次则需要按照一定的顺序调用若干个程序组件,其过程如图1.18所示。

内部分为创建套接字、连接Web服务器、发送数据、接收数据、断开连接几个阶段。

首先是套接字创建阶段。客户端创建套接字的操作非常简单,只要调用Socket库中的socket程序组件$^1$就可以了(图1.18①)。和调用解析器一样,调用socket之后,控制流程会转移到socket内部并执行创建套接字的操作,完成之后控制流程又会被移交回应用程序。只不过,socket的内部操作并不像解析器那样简单,因此我们将在第2章为大家详细讲解这部分内容$^2$。现在大家只要知道调用socket后套接字就创建好了就可以了。

  1. 书中出现了Socket、socket、套接字(英文也是socket)等看起来非常容易混淆的词,其中小写的socket表示程序组件的名称,大写字母开头的Socket表示库,而汉字的“套接字”则表示管道两端的接口。
  2. 后面将提到的connect、write、read、close等程序组件的内部操作也将在第2章讲解。

套接字创建完成后,协议栈会返回一个描述符,应用程序会将收到的描述符存放在内存中。描述符是用来识别不同的套接字的,大家可以作如下理解。我们现在只关注了浏览器访问Web服务器的过程,但实际上计算机中会同时进行多个数据的通信操作,比如可以打开两个浏览器窗口,同时访问两台Web服务器。这时,有两个数据收发操作在同时进行,也就需要创建两个不同的套接字。这个例子说明,同一台计算机上可能同时存在多个套接字,在这样的情况下,我们就需要一种方法来识别出某个特定的套接字,这种方法就是描述符。我们可以将描述符理解成给某个套接字分配的编号。当创建套接字后,我们就可以使用这个套接字来执行收发数据的操作了。这时,只要我们出示描述符,协议栈就能够判断出我们希望用哪一个套接字来连接或者收发数据了。

5.3.连接阶段:把管道接上去

接下来,我们需要委托协议栈将客户端创建的套接字与服务器那边的套接字连接起来。应用程序通过调用Socket库中的名为connect的程序组件来完成这一操作。这里的要点是当调用connect时,需要指定描述符、服务器IP地址和端口号这3个参数(图1.18②)。

第1个参数,即描述符,就是在创建套接字的时候由协议栈返回的那个描述符。connect会将应用程序指定的描述符告知协议栈,然后协议栈根据这个描述符来判断到底使用哪一个套接字去和服务器端的套接字进行连接,并执行连接的操作$^1$。

  1. 当调用Socket库中的程序组件时,应用程序所指定的参数会通过Socket库的程序组件传递给协议栈,并由协议栈来实际执行相应的操作。在后面的内容中,这一过程都是相同的,因此不再赘述。

第2个参数,即服务器IP地址,就是通过DNS服务器查询得到的我们要访问的服务器的IP地址。在进行数据收发操作时,双方必须知道对方的IP地址并告知协议栈。这个参数就是那个IP地址了。

第3个参数,即端口号。IP地址是为了区分网络中的各个计算机而分配的数值$^1$。因此,只要知道了IP地址,我们就可以识别出网络上的某台计算机。但是,连接操作的对象是某个具体的套接字,因此必须要识别到具体的套接字才行,而仅凭IP地址是无法做到这一点的。当同时指定IP地址和端口号时,就可以明确识别出某台具体的计算机上的某个具体的套接字。

  1. 准确地说,IP地址不是分配给每一台设备的,而是分配给设备中安装的网络硬件的。因此,如果一台设备中安装了多个网络硬件,那么就会有多个IP地址。

“能不能用前面创建套接字时提到的那个描述符来识别套接字呢?”这种方法其实是行不通的,因为描述符是和委托创建套接字的应用程序进行交互时使用的,并不是用来告诉网络连接的另一方的,因此另一方并不知道这个描述符。同样地,客户端也无法知道服务器上的描述符。因此,客户端也无法通过服务器端的描述符去确定位于服务器上的某一个套接字。所以,我们需要另外一个对客户端也同样适用的机制,而这个机制就是端口号。如果说描述符是用来在一台计算机内部识别套接字的机制,那么端口号就是用来让通信的另一方能够识别出套接字的机制$^1$。

  1. 既然可以用端口号来识别套接字,那为什么还需要描述符呢?要回答这个问题,我们需要对端口号进行更深入的了解,请见后续博客。

既然需要通过端口号来确定连接对象的套接字,那么到底应该使用几号端口呢?网址中好像并没有端口号$^1$,也不能像IP地址一样去问DNS服务器$^2$。其实,服务器上所使用的端口号是根据应用的种类事先规定好的,仅此而已。比如Web是80号端口,电子邮件是25号端口$^3$。只要指定了事先规定好的端口号,就可以连接到相应的服务器程序的套接字。也就是说,浏览器访问Web服务器时使用80号端口,这是已经规定好的。

  1. 看图1.1的前两张图,实际上根据网址的规则,是有用来写端口号的地方的,但实际的网址中很少出现端口号,大部分情况下都省略了。
  2. 实际上存在通过DNS服务器查询端口号的机制,只能说并没有广泛普及。
  3. 端口号的规则是全球统一的,为了避免重复和冲突,端口号和IP地址一样都是由IANA(Internet Assigned Number Authority,互联网编号管理局)这一组织来统一管理的。

既然确定连接对象的套接字需要使用端口号,那么服务器也得知道客户端的套接字号码才行吧,这个问题是怎么解决的呢?首先,客户端在创建套接字时,协议栈会为这个套接字随便分配一个端口号$^1$。接下来,当协议栈执行连接操作时,会将这个随便分配的端口号通知给服务器。这部分内容我们会在第2章探索协议栈内部工作时进行介绍。

  1. 在创建套接字时,服务器也可以自行指定端口号,但一般并不常用。

总而言之,就是当调用connect时,协议栈就会执行连接操作。当连接成功后,协议栈会将对方的IP地址和端口号等信息保存在套接字中,这样我们就可以开始收发数据了。

描述符:应用程序用来识别套接字的机制。

IP地址和端口号:客户端和服务器之间用来识别对方套接字的机制。

5.4.通信阶段:传递消息

当套接字连接起来之后,剩下的事情就简单了。只要将数据送入套接字,数据就会被发送到对方的套接字中。当然,应用程序无法直接控制套接字,因此还是要通过Socket库委托协议栈来完成这个操作。这个操作需要使用write这个程序组件,具体过程如下。

首先,应用程序需要在内存中准备好要发送的数据。根据用户输入的网址生成的HTTP请求消息就是我们要发送的数据。接下来,当调用write时,需要指定描述符和发送数据(图1.18③),然后协议栈就会将数据发送到服务器。由于套接字中已经保存了已连接的通信对象的相关信息,所以只要通过描述符指定套接字,就可以识别出通信对象,并向其发送数据。接着,发送数据会通过网络到达我们要访问的服务器。

接下来,服务器执行接收操作,解析收到的数据内容并执行相应的操作,向客户端返回响应消息。

当消息返回后,需要执行的是接收消息的操作。接收消息的操作是通过Socket库中的read程序组件委托协议栈来完成的(图1.18③’)。调用read时需要指定用于存放接收到的响应消息的内存地址,这一内存地址称为接收缓冲区。于是,当服务器返回响应消息时,read就会负责将接收到的响应消息存放到接收缓冲区中。由于接收缓冲区是一块位于应用程序内部的内存空间,因此当消息被存放到接收缓冲区中时,就相当于已经转交给了应用程序。

5.5.断开阶段:收发数据结束

当浏览器收到数据之后,收发数据的过程就结束了。接下来,我们需要调用Socket库的close程序组件进入断开阶段(图1.18④)。最终,连接在套接字之间的管道会被断开,套接字本身也会被删除。

断开的过程如下。Web使用的HTTP协议规定,当Web服务器发送完响应消息之后,应该主动执行断开操作$^1$,因此Web服务器会首先调用close来断开连接。断开操作传达到客户端之后,客户端的套接字也会进入断开阶段。接下来,当浏览器调用read执行接收数据操作时,read会告知浏览器收发数据操作已结束,连接已经断开。浏览器得知后,也会调用close进入断开阶段。

  1. 根据应用种类不同,客户端和服务器哪一方先执行close都有可能。有些应用中是客户端先执行close,而另外一些应用中则是服务器先执行close。

这就是HTTP的工作过程。HTTP协议将HTML文档和图片都作为单独的对象来处理,每获取一次数据,就要执行一次连接、发送请求消息、接收响应消息、断开的过程。因此,如果一个网页中包含很多张图片,就必须重复进行很多次连接、收发数据、断开的操作。对于同一台服务器来说,重复连接和断开显然是效率很低的,因此后来人们又设计出了能够在一次连接中收发多个请求和响应的方法。在HTTP版本1.1中就可以使用这种方法,在这种情况下,当所有数据都请求完成后,浏览器会主动触发断开连接的操作。

本章我们探索了浏览器与Web服务器之间收发消息的过程,但实际负责收发消息的是协议栈、网卡驱动和网卡,只有这3者相互配合,数据才能够在网络中流动起来。下一章我们将对这一部分进行探索。