1、浏览器生成消息 — 探索浏览器内部

1.1 生成HTTP请求消息

1.1.1 探索之旅从输入网址开始

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

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

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

1.1.2 浏览器先要解析URL

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

1.1.3 省略文件名的情况

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

1.1.4 HTTP的基本思路

HTTP协议定义了客户端和服务器之间交互的消息内容和步骤,其基本思路非常简单。

首先,客户端会向服务器发送请求消息。请求消息中包含的内容是“对什么”和“进行怎样的操作”两个部分。其中相当于“对什么”的部分称为URI。换句话说就是,这里可以写各种访问目标,而这些访问目标统称为URI。

接下来“进行怎样的操作”的部分称为方法,方法表示需要让Web服务器完成怎样的工作,其中典型的例子包括读取URI表示的数据、将客户端输入的数据发送给URI表示的程序等。

1.1.5 生成HTTP请求消息

理解了HTTP的基本知识之后,让我们回到对浏览器本身的探索中来。

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

首先,请求消息的第一行称为请求行。这里的重点是最开头的方法,方法可以告诉Web服务器它应该进行怎样的操作。

HTTP消息的格式如下:

HTTP中主要头字段如下:

1.1.6 发送请求后收到响应

当我们将上述请求消息发送出去之后,Web服务器会返回响应消息。

在响应消息中,第一行的内容为状态码和响应短语,用来表示请求的执行结果是成功还是出错。状态码和响应短语表示的内容一致,但它们的用途不同。状态码是一个数字,它主要用来向程序告知执行的结果;相对地,响应短语则是一段文字,用来向人们告知执行的结果。

HTTP消息示例如下:

1.2 向DNS服务器查询Web服务器IP地址

1.2.1 IP地址的基本知识

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

互联网和公司内部的局域网都是基于TCP/IP的思路来设计的,所以我们先来了解TCP/IP的基本思路。TCP/IP的结构如图1.8所示,就是由一些小的子网,通过路由器连接起来组成一个大的网络。这里的子网可以理解为用集线器连接起来的几台计算机,我们将它看作一个单位,称为子网。将子网通过路由器连接起来,就形成了一个网络。在网络中,所有的设备都会被分配一个地址。这个地址就相当于现实中某条路上的“××号××室”。其中“号”对应的号码是分配给整个子网的,而“室”对应的号码是分配给子网中的计算机的,这就是网络中的地址。“号”对应的号码称为网络号,“室”对应的号码称为主机号,这个地址的整体称为IP地址。通过IP地址我们可以判断出访问对象服务器的位置,从而将消息发送到服务器。消息传送的具体过程在后面的章节有详细讲解,不过现在我们先简单了解一下。发送者发出的消息首先经过子网中的集线器,转发到距离发送者最近的路由器上。接下来,路由器会根据消息的目的地判断下一个路由器的位置,然后将消息发送到下一个路由器,即消息再次经过子网内的集线器被转发到下一个路由器。前面的过程不断重复,最终消息就被传送到了目的地。

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

1.2.2 域名和IP地址并用的理由

TCP/IP网络是通过IP地址来确定通信对象的,因此不知道IP地址就无法将消息发送给对方,这和我们打电话的时候必须要知道对方的电话号码是一个道理。因此,在委托操作系统发送消息时,必须要先查询好对方的IP地址。

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

不过从运行效率上来看,这并不能算是一个好主意。互联网中存在无数的路由器,它们之间相互配合,根据IP地址来判断应该把数据传送到什么地方。那么如果我们不用IP地址而是改用名称会怎么样呢?IP地址的长度为32比特,也就是4字节,相对地,域名最短也要几十个字节,最长甚至可以达到255字节。换句话说,使用IP地址只需要处理4字节的数字,而域名则需要处理几十个到255个字节的字符,这增加了路由器的负担,传送数据也会花费更长的时间。

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

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

解析器实际上是一段程序,它包含在操作系统的Socket库中,在介绍解析器之前,我们先来简单了解一下Socket库。首先,库到底是什么东西呢?库就是一堆通用程序组件的集合,其他的应用程序都需要使用其中的组件。库有很多好处。首先,使用现成的组件搭建应用程序可以节省编程工作量;其次,多个程序使用相同的组件可以实现程序的标准化。除此之外还有很多其他的好处,因此使用库来进行软件开发的思路已经非常普及,库的种类和数量也非常之多。Socket库也是一种库,其中包含的程序组件可以让其他的应用程序调用操作系统的网络功能,而解析器就是这个库中的其中一种程序组件。

Socket库是用于调用网络功能的程序组件集合。

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

调用解析器后,解析器会向DNS服务器发送查询消息,然后DNS服务器会返回响应消息。响应消息中包含查询到的IP地址,解析器会取出IP地址,并将其写入浏览器指定的内存地址中。

1.2.5 解析器的内部原理

下面来看一看当应用程序调用解析器时,解析器内部是怎样工作的.

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

1.3 全世界DNS服务器的大接力

1.3.1 DNS服务器的基本工作

DNS服务器就是根据这些记录查找符合查询请求的内容并对客户端作出响应的。

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

1.3.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.3.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个,而且这些地址几乎不发生变化,因此将这些地址保存在所有的DNS服务器中也并不是一件难事。

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

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

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

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

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

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

1.4 委托协议栈发送消息

1.4.1 数据收发操作概览

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

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

使用Socket库来收发数据的操作过程如下图。简单来说,收发数据的两台计算机之间连接了一条数据通道,数据沿着这条通道流动,最终到达目的地。我们可以把数据通道想象成一条管道,将数据从一端送入管道,数据就会到达管道的另一端然后被取出。数据可以从任何一端被送入管道,数据的流动是双向的。不过,这并不是说现实中真的有这么一条管道,只是为了帮助大家理解数据收发操作的全貌。

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

我们再来看一看收发数据操作结束时的情形。当数据全部发送完毕之后,连接的管道将会被断开。管道在连接时是由客户端发起的,但在断开时可以由客户端或服务器任意一方发起。其中一方断开后,另一方也会随之断开,当管道断开后,套接字也会被删除。到此为止,通信操作就结束了。

综上所述,收发数据的操作分为若干个阶段,可以大致总结为以下4个

(1)创建套接字(创建套接字阶段)

(2)将管道连接到服务器端的套接字上(连接阶段)

(3)收发数据(通信阶段)

(4)断开管道并删除套接字(断开阶段)

在每个阶段,Socket库中的程序组件都会被调用来执行相关的数据收发操作。不过,在探索其具体过程之前,我们来补充一点内容。前面这4个操作都是由操作系统中的协议栈来执行的,浏览器等应用程序并不会自己去做连接管道、放入数据这些工作,而是委托协议栈来代劳。

1.4.2 创建套接字阶段

下面我们就来探索一下应用程序(浏览器)委托收发数据的过程。这个过程的关键点就是像对DNS服务器发送查询一样,调用Socket库中的特定程序组件。访问DNS服务器时我们调用的是一个叫作gethostbyname的程序组件(也就是解析器),而这一次则需要按照一定的顺序调用若干个程序组件,其过程如下图所示,请大家边看图边继续看下面的讲解。其中,调用Socket库中的程序组件的思路和图1.11旁边关于调用解析器的说明是一样的,请大家回忆一下。

首先是套接字创建阶段。客户端创建套接字的操作非常简单,只要调用Socket库中的socket程序组件就可以了。

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

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

1.4.3 连接阶段:把管道接上去

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

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

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

第3个参数,即端口号,这个需要稍微解释一下。可能大家会觉得,IP地址就像电话号码,只要知道了电话号码不就可以联系到对方了吗?其实,网络通信和电话还是有区别的,我们先来看一看IP地址到底能用来干什么。

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

1.4.4 通信阶段:传递消息

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

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

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

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

1.4.5 断开阶段:收发数据结束

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

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

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

2、用电信号传输TCP/IP — 探索协议栈和网卡

第1章,我们从解析浏览器中输入的网址开始,探索了生成HTTP请求消息、委托操作系统发送消息等步骤。本章,我们将讲解操作系统中的协议栈是如何处理数据发送请求的。

(1)创建套接字

从应用程序收到委托后,协议栈通过TCP协议收发数据的操作可以分为4个阶段。首先是创建套接字,在这个阶段,我们将介绍协议栈的内部结构、套接字的实体,以及创建套接字的操作过程。到这里,大家应该可以对套接字到底是什么样的一个东西有一个比较具体的理解。

(2)连接服务器

接下来是客户端套接字向服务器套接字进行连接的阶段。我们将介绍“连接”具体是进行怎样的操作,在这个过程中协议栈到底是如何工作的,以及客户端和服务器是如何进行交互的。

(3)收发数据

两端的套接字完成连接之后,就进入收发消息的阶段了。在这个阶段,协议栈会将从应用程序收到的数据切成小块并发送给服务器,考虑到通信过程中可能会出错导致网络包丢失,协议栈还需要确认切分出的每个包是否已经送达服务器,对于没有送达的包要重新发送一次。这里我们将对收发数据的情形加以说明。

(4)从服务器断开连接并删除套接字

收发消息的操作全部结束之后,接下来要断开服务器的连接并删除套接字。断开操作的本质是当消息收发完成后客户端和服务器相互进行确认的过程,但这个过程并不只是相互确认并删除套接字那么简单,其中有些地方是很有意思的。

(5)IP与以太网的包收发操作

在介绍TCP协议收发消息的操作之后,我们再来看看实际的网络包是如何进行收发的。协议栈会与网卡进行配合,将数据切分成小块并封装成网络包,再将网络包转换成电信号或者光信号发送出去。介绍完这个过程之后,大家应该就可以对计算机网络功能有一个完整的概念了。

(6)用UDP协议收发数据的操作

TCP协议有很多方便的功能,比如网络包出错丢失时可以重发,因此很多应用程序都是使用TCP协议来收发数据的,但这些方便的功能也有帮倒忙的时候,在这种情况下我们还有另外一种叫UDP的协议。这里我们将介绍UDP的必要性以及它与TCP的差异。

2.1 创建套接字

2.1.1 协议栈的内部结构

本章我们将探索操作系统中的网络控制软件(协议栈)和网络硬件(网卡)是如何将浏览器的消息发送给服务器的。和浏览器不同的是,协议栈的工作我们从表面上是看不见的,可能比较难以想象。因此,在实际探索之前,我们先来对协议栈做个解剖,看看里面到底有些什么。

协议栈的内部如图下图所示,分为几个部分,分别承担不同的功能。这张图中的上下关系是有一定规则的,上面的部分会向下面的部分委派工作,下面的部分接受委派的工作并实际执行,这一点大家在看图时可以参考一下。当然,这一上下关系只是一个总体的规则,其中也有一部分上下关系不明确,或者上下关系相反的情况,所以也不必过于纠结。此外,对于图中的每个部分以及它们的工作方式,本章将按顺序进行介绍,因此对于里面的细节现在看不明白也没关系,只要大体上看出有哪些组成要素就可以了。

上层会向下层逐层委派工作。

下面我们从上到下来看一遍。图中最上面的部分是网络应用程序,也就是浏览器、电子邮件客户端、Web服务器、电子邮件服务器等程序,它们会将收发数据等工作委派给下层的部分来完成。当然,除了浏览器之外,其他应用程序在网络上收发数据的操作也都是类似上面这样的,也就是说,尽管不同的应用程序收发的数据内容不同,但收发数据的操作是共通的。因此,下面介绍的内容不仅适用于浏览器,也适用于各种应用程序。

应用程序的下面是Socket库,其中包括解析器,解析器用来向DNS服务器发出查询,它的工作过程我们在第1章已经介绍过了。

再下面就是操作系统内部了,其中包括协议栈。协议栈的上半部分有两块,分别是负责用TCP协议收发数据的部分和负责用UDP协议收发数据的部分,它们会接受应用程序的委托执行收发数据的操作。关于TCP和UDP我们将在后面讲解,现在大家只要先记住下面这句话就可以了:像浏览器、邮件等一般的应用程序都是使用TCP收发数据的,而像DNS查询等收发较短的控制数据的时候则使用UDP。

下面一半是用IP协议控制网络包收发操作的部分。在互联网上传送数据时,数据会被切分成一个一个的网络包,而将网络包发送给通信对象的操作就是由IP来负责的。此外,IP中还包括ICMP协议和ARP协议。ICMP用于告知网络包传送过程中产生的错误以及各种控制消息,ARP用于根据IP地址查询相应的以太网MAC地址。

IP下面的网卡驱动程序负责控制网卡硬件,而最下面的网卡则负责完成实际的收发操作,也就是对网线中的信号执行发送和接收的操作。

2.1.2 套接字的实体就是通信控制信息

我们已经了解了协议栈的内部结构,而对于在数据收发中扮演关键角色的套接字,让我们来看一看它具体是个怎样的东西。

在协议栈内部有一块用于存放控制信息的内存空间,这里记录了用于控制通信操作的控制信息,例如通信对象的IP地址、端口号、通信操作的进行状态等。本来套接字就只是一个概念而已,并不存在实体,如果一定要赋予它一个实体,我们可以说这些控制信息就是套接字的实体,或者说存放控制信息的内存空间就是套接字的实体。

讲了这么多抽象的概念,可能大家还不太容易理解,所以下面来看看真正的套接字。在Windows中可以用netstat命令显示套接字内容(下图),图中每一行相当于一个套接字,当创建套接字时,就会在这里增加一行新的控制信息,赋予“即将开始通信”的状态,并进行通信的准备工作,如分配用于临时存放收发数据的缓冲区空间。

既然有图,我们就来讲讲图上这些到底都是什么意思。比如第8行,它表示PID为4的程序正在使用IP地址为10.10.1.16的网卡与IP地址为10.10.1.18的对象进行通信。此外我们还可以看出,本机使用1031端口,对方使用139端口,而139端口是Windows文件服务器使用的端口,因此我们就能够看出这个套接字是连接到一台文件服务器的。我们再来看第1行,这一行表示PID为984的程序正在135端口等待另一方的连接,其中本地IP地址和远程IP地址都是0.0.0.0,这表示通信还没开始,IP地址不确定。

2.1.3 调用socket时的操作

看过套接字的具体样子之后,我们的探索之旅将继续前进,看一看当浏览器调用socket、connect等Socket库中的程序组件时,协议栈内部是如何工作的。

首先,我们再来看一下浏览器通过Socket库向协议栈发出委托的一系列操作(下图)。这张图和介绍浏览器时用的那张图的内容大体相同,只作了少许修改。正如我们之前讲过的那样,浏览器委托协议栈使用TCP协议来收发数据,因此下面的讲解都是关于TCP的。

首先是创建套接字的阶段。如图下图①所示,应用程序调用socket申请创建套接字,协议栈根据应用程序的申请执行创建套接字的操作。

接下来,需要将表示这个套接字的描述符告知应用程序。描述符相当于用来区分协议栈中的多个套接字的号码牌。

收到描述符之后,应用程序在向协议栈进行收发数据委托时就需要提供这个描述符。由于套接字中记录了通信双方的信息以及通信处于怎样的状态,所以只要通过描述符确定了相应的套接字,协议栈就能够获取所有的相关信息,这样一来,应用程序就不需要每次都告诉协议栈应该和谁进行通信了。

2.2 连接服务器

2.2.1 连接是什么意思

创建套接字之后,应用程序(浏览器)就会调用connect,随后协议栈会将本地的套接字与服务器的套接字进行连接。话说,以太网的网线都是一直连接的状态,我们并不需要来回插拔网线,那么这里的“连接”到底是什么意思呢?连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作,在讲解具体的过程之前,我们先来说一说“连接”到底代表什么意思。

网线是一直连接着的,随时都有信号从中流过,如果通信过程只是将数据转换为电信号,那么这一操作随时都可以进行。不过,在这个时间点,也就是套接字刚刚创建完成时,当应用程序委托发送数据的时候,协议栈会如何操作呢?

套接字刚刚创建完成的时候,里面并没有存放任何数据,也不知道通信的对象是谁。在这个状态下,即便应用程序要求发送数据,协议栈也不知道数据应该发送给谁。浏览器可以根据网址来查询服务器的IP地址,而且根据规则也知道应该使用80号端口,但只有浏览器知道这些必要的信息是不够的,因为在调用socket创建套接字时,这些信息并没有传递给协议栈。因此,我们需要把服务器的IP地址和端口号等信息告知协议栈,这是连接操作的目的之一。

那么,服务器这边又是怎样的情况呢?服务器上也会创建套接字,但服务器上的协议栈和客户端一样,只创建套接字是不知道应该和谁进行通信的。而且,和客户端不同的是,在服务器上,连应用程序也不知道通信对象是谁,这样下去永远也没法开始通信。于是,我们需要让客户端向服务器告知必要的信息,比如“我想和你开始通信,我的IP地址是xxx.xxx.xxx.xxx,端口号是yyyy。”可见,客户端向服务器传达开始通信的请求,也是连接操作的目的之一。

之前我们讲过,连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作,像上面提到的客户端将IP地址和端口号告知服务器这样的过程就属于交换控制信息的一个具体的例子。所谓控制信息,就是用来控制数据收发操作所需的一些信息,IP地址和端口号就是典型的例子。除此之外还有其他一些控制信息,我们后面会逐一进行介绍。连接操作中所交换的控制信息是根据通信规则来确定的,只要根据规则执行连接操作,双方就可以得到必要的信息从而完成数据收发的准备。此外,当执行数据收发操作时,我们还需要一块用来临时存放要收发的数据的内存空间,这块内存空间称为缓冲区,它也是在连接操作的过程中分配的。上面这些就是“连接”这个词代表的具体含义。

2.2.2 负责保存控制信息的头部

TCP头部格式如下图:

客户端与服务器之间交换的控制信息如下图:

通信操作中使用的控制信息分为两类。

(1)头部中记录的信息

(2)套接字(协议栈中的内存空间)中记录的信息

2.2.3 连接操作的实际过程

我们已经了解了连接操作的含义,下面来看一下具体的操作过程。这个过程是从应用程序调用Socket库的connect开始的。

1
connect(<描述符>,<服务器IP地址和端口号>,…)

上面的调用提供了服务器的IP地址和端口号,这些信息会传递给协议栈中的TCP模块。然后,TCP模块会与该IP地址对应的对象,也就是与服务器的TCP模块交换控制信息,这一交互过程包括下面几个步骤。首先,客户端先创建一个包含表示开始数据收发操作的控制信息的头部。如上表所示,头部包含很多字段,这里要关注的重点是发送方和接收方的端口号。到这里,客户端(发送方)的套接字就准确找到了服务器(接收方)的套接字,也就是搞清楚了我应该连接哪个套接字。然后,我们将头部中的控制位的SYN比特设置为1,大家可以认为它表示连接。此外还需要设置适当的序号和窗口大小,这一点我们会稍后详细讲解。

当TCP头部创建好之后,接下来TCP模块会将信息传递给IP模块并委托它进行发送。IP模块执行网络包发送操作后,网络包就会通过网络到达服务器,然后服务器上的IP模块会将接收到的数据传递给TCP模块,服务器的TCP模块根据TCP头部中的信息找到端口号对应的套接字,也就是说,从处于等待连接状态的套接字中找到与TCP头部中记录的端口号相同的套接字就可以了。当找到对应的套接字之后,套接字中会写入相应的信息,并将状态改为正在连接。上述操作完成后,服务器的TCP模块会返回响应,这个过程和客户端一样,需要在TCP头部中设置发送方和接收方端口号以及SYN比特。此外,在返回响应时还需要将ACK控制位设为1,这表示已经接收到相应的网络包。网络中经常会发生错误,网络包也会发生丢失,因此双方在通信时必须相互确认网络包是否已经送达,而设置ACK比特就是用来进行这一确认的。接下来,服务器TCP模块会将TCP头部传递给IP模块,并委托IP模块向客户端返回响应。

然后,网络包就会返回到客户端,通过IP模块到达TCP模块,并通过TCP头部的信息确认连接服务器的操作是否成功。如果SYN为1则表示连接成功,这时会向套接字中写入服务器的IP地址、端口号等信息,同时还会将状态改为连接完毕。到这里,客户端的操作就已经完成,但其实还剩下最后一个步骤。刚才服务器返回响应时将ACK比特设置为1,相应地,客户端也需要将ACK比特设置为1并发回服务器,告诉服务器刚才的响应包已经收到。当这个服务器收到这个返回包之后,连接操作才算全部完成。

现在,套接字就已经进入随时可以收发数据的状态了,大家可以认为这时有一根管子把两个套接字连接了起来。当然,实际上并不存在这么一根管子,不过这样想比较容易理解,网络业界也习惯这样来描述。这根管子,我们称之为连接。只要数据传输过程在持续,也就是在调用close断开之前,连接是一直存在的。

2.3 收发数据

2.3.1 将HTTP请求消息交给协议栈

当控制流程从connect回到应用程序之后,接下来就进入数据收发阶段了。数据收发操作是从应用程序调用write将要发送的数据交给协议栈开始的,协议栈收到数据后执行发送操作,这一操作包含如下要点。

首先,协议栈并不关心应用程序传来的数据是什么内容。应用程序在调用write时会指定发送数据的长度,在协议栈看来,要发送的数据就是一定长度的二进制字节序列而已。

其次,协议栈并不是一收到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区中,并等待应用程序的下一段数据。这样做是有道理的。应用程序交给协议栈发送的数据长度是由应用程序本身来决定的,不同的应用程序在实现上有所不同,有些程序会一次性传递所有的数据,有些程序则会逐字节或者逐行传递数据。总之,一次将多少数据交给协议栈是由应用程序自行决定的,协议栈并不能控制这一行为。在这样的情况下,如果一收到数据就马上发送出去,就可能会发送大量的小包,导致网络效率下降,因此需要在数据积累到一定量时再发送出去。至于要积累多少数据才能发送,不同种类和版本的操作系统会有所不同,不能一概而论,但都是根据下面几个要素来判断的。

第一个判断要素是每个网络包能容纳的数据长度,协议栈会根据一个叫作MTU的参数来进行判断。MTU表示一个网络包的最大长度,在以太网中一般是1500字节。MTU是包含头部的总长度,因此需要从MTU减去头部的长度,然后得到的长度就是一个网络包中所能容纳的最大数据长度,这一长度叫作MSS。当从应用程序收到的数据长度超过或者接近MSS时再发送出去,就可以避免发送大量小包的问题了。

1
2
3
MTU:一个网络包的最大长度,以太网中一般为1500字节。

MSS:除去头部之后,一个网络包所能容纳的TCP数据的最大长度。

另一个判断要素是时间。当应用程序发送数据的频率不高的时候,如果每次都等到长度接近MSS时再发送,可能会因为等待时间太长而造成发送延迟,这种情况下,即便缓冲区中的数据长度没有达到MSS,也应该果断发送出去。为此,协议栈的内部有一个计时器,当经过一定时间之后,就会把网络包发送出去。

判断要素就是这两个,但它们其实是互相矛盾的。如果长度优先,那么网络的效率会提高,但可能会因为等待填满缓冲区而产生延迟;相反地,如果时间优先,那么延迟时间会变少,但又会降低网络的效率。因此,在进行发送操作时需要综合考虑这两个要素以达到平衡。不过,TCP协议规格中并没有告诉我们怎样才能平衡,因此实际如何判断是由协议栈的开发者来决定的,也正是由于这个原因,不同种类和版本的操作系统在相关操作上也就存在差异。

2.3.2 对较大的数据进行拆分

HTTP请求消息一般不会很长,一个网络包就能装得下,但如果其中要提交表单数据,长度就可能超过一个网络包所能容纳的数据量,比如在博客或者论坛上发表一篇长文就属于这种情况。

这种情况下,发送缓冲区中的数据就会超过MSS的长度,这时我们当然不需要继续等待后面的数据了。发送缓冲区中的数据会被以MSS长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中。

根据发送缓冲区中的数据拆分的情况,当判断需要发送这些数据时,就在每一块数据前面加上TCP头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号,然后交给IP模块来执行发送数据的操作(如下图)

应用程序数据拆分发送, 如上图。

应用程序的数据一般都比较大,因此TCP会按照网络包的大小对数据进行拆分。

2.3.3 使用ACK号确认网络包已收到

到这里,网络包已经装好数据并发往服务器了,但数据发送操作还没有结束。TCP具备确认对方是否成功收到网络包,以及当对方没收到时进行重发的功能,因此在发送网络包之后,接下来还需要进行确认操作。

我们先来看一下确认的原理(如下图)。首先,TCP模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节,接下来在发送这一块数据时,将算好的字节数写在TCP头部中,“序号”字段就是派在这个用场上的。然后,发送数据的长度也需要告知接收方,不过这个并不是放在TCP头部里面的,因为用整个网络包的长度减去头部的长度就可以得到数据的长度,所以接收方可以用这种方法来进行计算。有了上面两个数值,我们就可以知道发送的数据是从第几个字节开始,长度是多少了。

通过这些信息,接收方还能够检查收到的网络包有没有遗漏。例如,假设上次接收到第1460字节,那么接下来如果收到序号为1461的包,说明中间没有遗漏;但如果收到的包序号为2921,那就说明中间有包遗漏了。像这样,如果确认没有遗漏,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入TCP头部的ACK号中发送给发送方。简单来说,发送方说的是“现在发送的是从第××字节开始的部分,一共有××字节哦!”而接收方则回复说,“到第××字节之前的数据我已经都收到了哦!”这个返回ACK号的操作被称为确认响应,通过这样的方式,发送方就能够确认对方到底收到了多少数据。

然而,下图的例子和实际情况还是有些出入的。在实际的通信中,序号并不是从1开始的,而是需要用随机数计算出一个初始值,这是因为如果序号都从1开始,通信过程就会非常容易预测,有人会利用这一点来发动攻击。但是如果初始值是随机的,那么对方就搞不清楚序号到底是从多少开始计算的,因此需要在开始收发数据之前将初始值告知通信对象。大家应该还记得在我们刚才讲过的连接过程中,有一个将SYN控制位设为1并发送给服务器的操作,就是在这一步将序号的初始值告知对方的。实际上,在将SYN设为1的同时,还需要同时设置序号字段的值,而这里的值就代表序号的初始值。

序号和ACK号的用法, 如下图

前面介绍了通过序号和ACK号来进行数据确认的思路,但仅凭这些还不够,因为我们刚刚只考虑了单向的数据传输,但TCP数据收发是双向的,在客户端向服务器发送数据的同时,服务器也会向客户端发送数据,因此必须要想办法应对这样的情况。不过,这其实也不难,图2.7中展示的客户端向服务器发送数据的情形,我们只要增加一种左右相反的情形就可以了,如图2.8所示。首先客户端先计算出一个序号,然后将序号和数据一起发送给服务器,服务器收到之后会计算ACK号并返回给客户端;相反地,服务器也需要先计算出另一个序号,然后将序号和数据一起发送给客户端,客户端收到之后计算ACK号并返回给服务器。此外,如图所示,客户端和服务器双方都需要各自计算序号,因此双方需要在连接过程中互相告知自己计算的序号初始值。

明白原理之后我们来看一下实际的工作过程下图)。首先,客户端在连接时需要计算出与从客户端到服务器方向通信相关的序号初始值,并将这个值发送给服务器(下图①)。接下来,服务器会通过这个初始值计算出ACK号并返回给客户端(下图②)。初始值有可能在通信过程中丢失,因此当服务器收到初始值后需要返回ACK号作为确认。同时,服务器也需要计算出与从服务器到客户端方向通信相关的序号初始值,并将这个值发送给客户端(下图②)。接下来像刚才一样,客户端也需要根据服务器发来的初始值计算出ACK号并返回给服务器(下图③)。到这里,序号和ACK号都已经准备完成了,接下来就可以进入数据收发阶段了。数据收发操作本身是可以双向同时进行的,但Web中是先由客户端向服务器发送请求,序号也会跟随数据一起发送(下图④)。然后,服务器收到数据后再返回ACK号(下图⑤)。从服务器向客户端发送数据的过程则正好相反(下图⑥⑦)。

数据双向传输时的情况, 如下图

序号和ACK号的交互, 如下图:

TCP采用这样的方式确认对方是否收到了数据,在得到对方确认之前,发送过的包都会保存在发送缓冲区中。如果对方没有返回某些包对应的ACK号,那么就重新发送这些包。

这一机制非常强大。通过这一机制,我们可以确认接收方有没有收到某个包,如果没有收到则重新发送,这样一来,无论网络中发生任何错误,我们都可以发现并采取补救措施(重传网络包)。反过来说,有了这一机制,我们就不需要在其他地方对错误进行补救了。

因此,网卡、集线器、路由器都没有错误补偿机制,一旦检测到错误就直接丢弃相应的包。应用程序也是一样,因为采用TCP传输,即便发生一些错误对方最终也能够收到正确的数据,所以应用程序只管自顾自地发送这些数据就好了。不过,如果发生网络中断、服务器宕机等问题,那么无论TCP怎样重传都不管用。这种情况下,无论如何尝试都是徒劳,因此TCP会在尝试几次重传无效之后强制结束通信,并向应用程序报错。

1
通过“序号”和“ACK号”可以确认接收方是否收到了网络包。

2.3.4 根据网络包平均往返时间调整ACK号等待时间

前面说的只是一些基本原理,实际上网络的错误检测和补偿机制非常复杂。下面来说几个关键的点,首先是返回ACK号的等待时间(这个等待时间叫超时时间)。

当网络传输繁忙时就会发生拥塞,ACK号的返回会变慢,这时我们就必须将等待时间设置得稍微长一点,否则可能会发生已经重传了包之后,前面的ACK号才姗姗来迟的情况。这样的重传是多余的,看上去只是多发一个包而已,但它造成的后果却没那么简单。因为ACK号的返回变慢大多是由于网络拥塞引起的,因此如果此时再出现很多多余的重传,对于本来就很拥塞的网络来说无疑是雪上加霜。那么等待时间是不是越长越好呢?也不是。如果等待时间过长,那么包的重传就会出现很大的延迟,也会导致网络速度变慢。

看来等待时间需要设为一个合适的值,不能太长也不能太短,但这谈何容易。根据服务器物理距离的远近,ACK号的返回时间也会产生很大的波动,而且我们还必须考虑到拥塞带来的影响。例如,在公司里的局域网环境下,几毫秒就可以返回ACK号,但在互联网环境中,当遇到拥塞时需要几百毫秒才能返回ACK号也并不稀奇。

正因为波动如此之大,所以将等待时间设置为一个固定值并不是一个好办法。因此,TCP采用了动态调整等待时间的方法,这个等待时间是根据ACK号返回所需的时间来判断的。具体来说,TCP会在发送数据的过程中持续测量ACK号的返回时间,如果ACK号返回变慢,则相应延长等待时间;相对地,如果ACK号马上就能返回,则相应缩短等待时间。

2.3.5 使用窗口有效管理ACK号

如下图所示,每发送一个包就等待一个ACK号的方式是最简单也最容易理解的,但在等待ACK号的这段时间中,如果什么都不做那实在太浪费了。为了减少这样的浪费,TCP采用下图这样的滑动窗口方式来管理数据发送和ACK号的操作。所谓滑动窗口,就是在发送一个包之后,不等待ACK号返回,而是直接发送后续的一系列包。这样一来,等待ACK号的这段时间就被有效利用起来了。

一来一回方式和滑动窗口方式:

虽然这样做能够减少等待ACK号时的时间浪费,但有一些问题需要注意。在一来一回方式中,接收方完成接收操作后返回ACK号,然后发送方收到ACK号之后才继续发送下一个包,因此不会出现发送的包太多接收方处理不过来的情况。但如果不等返回ACK号就连续发送包,就有可能会出现发送包的频率超过接收方处理能力的情况。

下面来具体解释一下。当接收方的TCP收到包后,会先将数据存放到接收缓冲区中。然后,接收方需要计算ACK号,将数据块组装起来还原成原本的数据并传递给应用程序,如果这些操作还没完成下一个包就到了也不用担心,因为下一个包也会被暂存在接收缓冲区中。如果数据到达的速率比处理这些数据并传递给应用程序的速率还要快,那么接收缓冲区中的数据就会越堆越多,最后就会溢出。缓冲区溢出之后,后面的数据就进不来了,因此接收方就收不到后面的包了,这就和中途出错的结果是一样的,也就意味着超出了接收方处理能力。我们可以通过下面的方法来避免这种情况的发生。首先,接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制,这就是滑动窗口方式的基本思路。

关于滑动窗口的具体工作方式,还是看图更容易理解(下图)。在这张图中,接收方将数据暂存到接收缓冲区中并执行接收操作。当接收操作完成后,接收缓冲区中的空间会被释放出来,也就可以接收更多的数据了,这时接收方会通过TCP头部中的窗口字段将自己能接收的数据量告知发送方。这样一来,发送方就不会发送过多的数据,导致超出接收方的处理能力了。

此外,单从图上看,大家可能会以为接收方在等待接收缓冲区被填满之前似乎什么都没做,实际上并不是这样。这张图是为了讲解方便,故意体现一种接收方来不及处理收到的包,导致缓冲区被填满的情况。实际上,接收方在收到数据之后马上就会开始进行处理,如果接收方的性能高,处理速度比包的到达速率还快,缓冲区马上就会被清空,并通过窗口字段告知发送方。

还有,下图中只显示了从右往左发送数据的操作,实际上和序号、ACK号一样,发送操作也是双向进行的。

前面提到的能够接收的最大数据量称为窗口大小,它是TCP调优参数中非常有名的一个。

滑动窗口与接收缓冲区:

2.3.6 ACK与窗口的合并

要提高收发数据的效率,还需要考虑另一个问题,那就是返回ACK号和更新窗口的时机。如果假定这两个参数是相互独立的,分别用两个单独的包来发送,结果会如何呢?

首先,什么时候需要更新窗口大小呢?当收到的数据刚刚开始填入缓冲区时,其实没必要每次都向发送方更新窗口大小,因为只要发送方在每次发送数据时减掉已发送的数据长度就可以自行计算出当前窗口的剩余长度。因此,更新窗口大小的时机应该是接收方从缓冲区中取出数据传递给应用程序的时候。这个操作是接收方应用程序发出请求时才会进行的,而发送方不知道什么时候会进行这样的操作,因此当接收方将数据传递给应用程序,导致接收缓冲区剩余容量增加时,就需要告知发送方,这就是更新窗口大小的时机。

那么ACK号又是什么情况呢?当接收方收到数据时,如果确认内容没有问题,就应该向发送方返回ACK号,因此我们可以认为收到数据之后马上就应该进行这一操作。

如果将前面两个因素结合起来看,首先,发送方的数据到达接收方,在接收操作完成之后就需要向发送方返回ACK号,而再经过一段时间,当数据传递给应用程序之后才需要更新窗口大小。但如果根据这样的设计来实现,每收到一个包,就需要向发送方分别发送ACK号和窗口更新这两个单独的包。这样一来,接收方发给发送方的包就太多了,导致网络效率下降。

因此,接收方在发送ACK号和窗口更新时,并不会马上把包发送出去,而是会等待一段时间,在这个过程中很有可能会出现其他的通知操作,这样就可以把两种通知合并在一个包里面发送了。举个例子,在等待发送ACK号的时候正好需要更新窗口,这时就可以把ACK号和窗口更新放在一个包里发送,从而减少包的数量。当需要连续发送多个ACK号时,也可以减少包的数量,这是因为ACK号表示的是已收到的数据量,也就是说,它是告诉发送方目前已接收的数据的最后位置在哪里,因此当需要连续发送ACK号时,只要发送最后一个ACK号就可以了,中间的可以全部省略。当需要连续发送多个窗口更新时也可以减少包的数量,因为连续发生窗口更新说明应用程序连续请求了数据,接收缓冲区的剩余空间连续增加。这种情况和ACK号一样,可以省略中间过程,只要发送最终的结果就可以了。

2.3.7 接收HTTP响应消息

到这里,我们已经讲解完协议栈接到浏览器的委托后发送HTTP请求消息的一系列操作过程了。

不过,浏览器的工作并非到此为止。发送HTTP请求消息后,接下来还需要等待Web服务器返回响应消息。对于响应消息,浏览器需要进行接收操作,这一操作也需要协议栈的参与。按照探索之旅的思路,本来是应该按照访问Web服务器的顺序逐一讲解其中的每一步操作,也就是说接收HTTP响应消息应该放在最后再讲,但这样一来大家可能容易忘记前面的部分,所以我们就把这部分内容放在这里讲一讲。

首先,浏览器在委托协议栈发送请求消息之后,会调用read程序(之前的图2.3④)来获取响应消息。然后,控制流程会通过read转移到协议栈,然后协议栈会执行接下来的操作。和发送数据一样,接收数据也需要将数据暂存到接收缓冲区中,这里的操作过程如下。首先,协议栈尝试从接收缓冲区中取出数据并传递给应用程序,但这个时候请求消息刚刚发送出去,响应消息可能还没返回。响应消息的返回还需要等待一段时间,因此这时接收缓冲区中并没有数据,那么接收数据的操作也就无法继续。这时,协议栈会将应用程序的委托,也就是从接收缓冲区中取出数据并传递给应用程序的工作暂时挂起,等服务器返回的响应消息到达之后再继续执行接收操作。

协议栈接收数据的具体操作过程已经在发送数据的部分讲解过了,因此这里我们就简单总结一下。首先,协议栈会检查收到的数据块和TCP头部的内容,判断是否有数据丢失,如果没有问题则返回ACK号。然后,协议栈将数据块暂存到接收缓冲区中,并将数据块按顺序连接起来还原出原始的数据,最后将数据交给应用程序。具体来说,协议栈会将接收到的数据复制到应用程序指定的内存地址中,然后将控制流程交回应用程序。将数据交给应用程序之后,协议栈还需要找到合适的时机向发送方发送窗口更新。

2.4 从服务器断开并删除套接字

2.4.1 数据发送完毕后断开连接

既然我们已经讲解到了这里,那么索性把数据收发完成后协议栈要执行的操作也讲一讲吧。这样一来,从创建套接字到连接、收发数据、断开连接、删除套接字这一系列关于收发数据的操作就全部讲完了。

毫无疑问,收发数据结束的时间点应该是应用程序判断所有数据都已经发送完毕的时候。这时,数据发送完毕的一方会发起断开过程,但不同的应用程序会选择不同的断开时机。以Web为例,浏览器向Web服务器发送请求消息,Web服务器再返回响应消息,这时收发数据的过程就全部结束了,服务器一方会发起断开过程。当然,可能也有一些程序是客户端发送完数据就结束了,不用等服务器响应,这时客户端会先发起断开过程。这一判断是应用程序作出的,协议栈在设计上允许任何一方先发起断开过程。

无论哪种情况,完成数据发送的一方会发起断开过程,这里我们以服务器一方发起断开过程为例来进行讲解。首先,服务器一方的应用程序会调用Socket库的close程序。然后,服务器的协议栈会生成包含断开信息的TCP头部,具体来说就是将控制位中的FIN比特设为1。接下来,协议栈会委托IP模块向客户端发送数据(下图)。同时,服务器的套接字中也会记录下断开操作的相关信息。

断开连接的交互过程:

接下来轮到客户端了。当收到服务器发来的FIN为1的TCP头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态。然后,为了告知服务器已收到FIN为1的包,客户端会向服务器返回一个ACK号(上图)。这些操作完成后,协议栈就可以等待应用程序来取数据了。

过了一会儿,应用程序就会调用read来读取数据。这时,协议栈不会向应用程序传递数据,而是会告知应用程序(浏览器)来自服务器的数据已经全部收到了。根据规则,服务器返回请求之后,Web通信操作就全部结束了,因此只要收到服务器返回的所有数据,客户端的操作也就随之结束了。因此,客户端应用程序会调用close来结束数据收发操作,这时客户端的协议栈也会和服务器一样,生成一个FIN比特为1的TCP包,然后委托IP模块发送给服务器(上图)。一段时间之后,服务器就会返回ACK号(上图)。到这里,客户端和服务器的通信就全部结束了。

2.4.2 删除套接字

和服务器的通信结束之后,用来通信的套接字也就不会再使用了,这时我们就可以删除这个套接字了。不过,套接字并不会立即被删除,而是会等待一段时间之后再被删除。

等待这段时间是为了防止误操作,引发误操作的原因有很多,这里无法全部列举,下面来举一个最容易理解的例子。假设和上图的过程相反,客户端先发起断开,则断开的操作顺序如下。

(1)客户端发送FIN

(2)服务器返回ACK号

(3)服务器发送FIN

(4)客户端返回ACK号

如果最后客户端返回的ACK号丢失了,结果会如何呢?这时,服务器没有接收到ACK号,可能会重发一次FIN。如果这时客户端的套接字已经删除了,会发生什么事呢?套接字被删除,那么套接字中保存的控制信息也就跟着消失了,套接字对应的端口号就会被释放出来。这时,如果别的应用程序要创建套接字,新套接字碰巧又被分配了同一个端口号,而服务器重发的FIN正好到达,会怎么样呢?本来这个FIN是要发给刚刚删除的那个套接字的,但新套接字具有相同的端口号,于是这个FIN就会错误地跑到新套接字里面,新套接字就开始执行断开操作了。之所以不马上删除套接字,就是为了防止这样的误操作。

至于具体等待多长时间,这和包重传的操作方式有关。网络包丢失之后会进行重传,这个操作通常要持续几分钟。如果重传了几分钟之后依然无效,则停止重传。在这段时间内,网络中可能存在重传的包,也就有可能发生前面讲到的这种误操作,因此需要等待到重传完全结束。协议中对于这个等待时间没有明确的规定,一般来说会等待几分钟之后再删除套接字。

2.4.3 数据收发操作小结

到这里,用TCP协议收发应用程序数据的操作就全部结束了。这部分内容的讲解比较长,所以最后我们再整理一下。

数据收发操作的第一步是创建套接字。一般来说,服务器一方的应用程序在启动时就会创建好套接字并进入等待连接的状态。客户端则一般是在用户触发特定动作,需要访问服务器的时候创建套接字。在这个阶段,还没有开始传输网络包。

创建套接字之后,客户端会向服务器发起连接操作。首先,客户端会生成一个SYN为1的TCP包并发送给服务器(下图①)。这个TCP包的头部还包含了客户端向服务器发送数据时使用的初始序号,以及服务器向客户端发送数据时需要用到的窗口大小。当这个包到达服务器之后,服务器会返回一个SYN为1的TCP包(下图②)。和下图①一样,这个包的头部中也包含了序号和窗口大小,此外还包含表示确认已收到包①的ACK号。当这个包到达客户端时,客户端会向服务器返回一个包含表示确认的ACK号的TCP包(下图③)。到这里,连接操作就完成了,双方进入数据收发阶段。

TCP的整体流程:

数据收发阶段的操作根据应用程序的不同而有一些差异,以Web为例,首先客户端会向服务器发送请求消息。TCP会将请求消息切分成一定大小的块,并在每一块前面加上TCP头部,然后发送给服务器(上图④)。TCP头部中包含序号,它表示当前发送的是第几个字节的数据。当服务器收到数据时,会向客户端返回ACK号(上图⑤)。在最初的阶段,服务器只是不断接收数据,随着数据收发的进行,数据不断传递给应用程序,接收缓冲区就会被逐步释放。这时,服务器需要将新的窗口大小告知客户端。当服务器收到客户端的请求消息后,会向客户端返回响应消息,这个过程和刚才的过程正好相反(上图⑥⑦)。

服务器的响应消息发送完毕之后,数据收发操作就结束了,这时就会开始执行断开操作。以Web为例,服务器会先发起断开过程。在这个过程中,服务器先发送一个FIN为1的TCP包(上图⑧),然后客户端返回一个表示确认收到的ACK号(上图⑨)。接下来,双方还会交换一组方向相反的FIN为1的TCP包(上图⑩)和包含ACK号的TCP包(上图⑪)。最后,在等待一段时间后,套接字会被删除。

2.5 IP与以太网的包收发操作

2.5.1 包的基本知识

TCP模块在执行连接、收发、断开等各阶段操作时,都需要委托IP模块将数据封装成包发送给通信对象。我们在TCP的讲解中也经常提到IP,下面就来讨论一下IP模块是如何将包发送给对方的。

正式开始这个话题之前,我们先来介绍一下关于网络包的一些基本知识。首先,包是由头部和数据两部分构成的(下图网络包的结构)。头部包含目的地址等控制信息,大家可以把它理解为快递包裹的面单;头部后面就是委托方要发送给对方的数据,也就相当于快递包裹里的货物。一个包发往目的地的过程如下图所示(发送方、接收方和转发设备)。

首先,发送方的网络设备会负责创建包,创建包的过程就是生成含有正确控制信息的头部,然后再附加上要发送的数据。接下来,包会被发往最近的网络转发设备。当到达最近的转发设备之后,转发设备会根据头部中的信息判断接下来应该发往哪里。这个过程需要用到一张表,这张表里面记录了每一个地址对应的发送方向,也就是按照头部里记录的目的地址在表里进行查询,并根据查到的信息判断接下来应该发往哪个方向。比如,如果查表的结果是“目标地址为××××的包应该发到××××号线路”,那么转发设备就会把这个包发到××××号线路去。接下来,包在向目的地移动的过程中,又会到达下一个转发设备,然后又会按照同样的方式被发往下一个转发设备。就这样,经过多个转发设备的接力之后,包最终就会到达接收方的网络设备。当然,发送方向接收方发送一个包,接收方可能也会向发送方返回一个包,此时的发送方到了接下来的某个时刻就会变成接收方。因此,我们不需要把发送方和接收方明确区分开来,在这里我们把发送方和接收方统称为终端节点。

网络包的结构:

发送方、接收方和转发设备:

前面介绍的这些基本知识,对于各种通信方式都是适用的,当然也适用于TCP/IP网络。不过,TCP/IP包的结构是在这个基本结构的基础上扩展出来的,因此更加复杂。在第1章1.2.1节,我们讲过子网的概念,还讲过网络中有路由器和集线器两种不同的转发设备,它们在传输网络包时有着各自的分工。

(1)路由器根据目标地址判断下一个路由器的位置

(2)集线器在子网中将网络包传输到下一个路由

实际上,集线器是按照以太网规则传输包的设备,而路由器是按照IP规则传输包的设备,因此我们也可以作如下理解。

(1)IP协议根据目标地址判断下一个IP转发设备的位置

(2)子网中的以太网协议将包传输到下一个转发设备

具体来说,如上图一所示,TCP/IP包包含如下两个头部。

(a)MAC头部(用于以太网协议)

(b)IP头部(用于IP协议)

这两个头部分别具有不同的作用。首先,发送方将包的目的地,也就是要访问的服务器的IP地址写入IP头部中。这样一来,我们就知道这个包应该发往哪里,IP协议就可以根据这一地址查找包的传输方向,从而找到下一个路由器的位置,也就是下图的路由器R1。接下来,IP协议会委托以太网协议将包传输过去。这时,IP协议会查找下一个路由器的以太网地址(MAC地址),并将这个地址写入MAC头部中。这样一来,以太网协议就知道要将这个包发到哪一个路由器上了。

网络包在传输过程中(下图)会经过集线器,集线器是根据以太网协议工作的设备。为了判断包接下来应该向什么地方传输,集线器里有一张表(用于以太网协议的表),可根据以太网头部中记录的目的地信息查出相应的传输方向。这张图中只有一个集线器,当存在多个集线器时,网络包会按顺序逐一通过这些集线器进行传输。

接下来,包会到达下一个路由器(下图)。路由器中有一张IP协议的表,可根据这张表以及IP头部中记录的目的地信息查出接下来应该发往哪个路由器。为了将包发到下一个路由器,我们还需要查出下一个路由器的MAC地址,并记录到MAC头部中,大家可以理解为改写了MAC头部。这样,网络包就又被发往下一个节点了。

再往后的过程图上就没有画出来了。网络包会通过路由器到达下一个路由器R2。这个过程不断重复,最终网络包就会被送到目的地,当目的地设备成功接收之后,网络包的传输过程就结束了。

前面介绍的就是在TCP/IP网络中,一个网络包从出发到到达目的地的全过程。虽然看起来有点复杂,不过设计这样的分工是有原因的。前面讲了IP和以太网的分工,其中以太网的部分也可以替换成其他的东西,例如无线局域网、ADSL、FTTH等,它们都可以替代以太网的角色帮助IP协议来传输网络包。因此,将IP和负责传输的网络分开,可以更好地根据需要使用各种通信技术。像互联网这样庞大复杂的网络,在架构上需要保证灵活性,这就是设计这种分工方式的原因。

IP网络包的传输方式:

2.5.2 包收发操作概览

了解了整体流程之后,下面来讲一讲在协议栈中IP模块是如何完成包收发操作的。尽管我们说IP模块负责将包发给对方,但实际上将包从发送方传输到接收方的工作是由集线器、路由器等网络设备来完成的,因此IP模块仅仅是整个包传输过程的入口而已。即便如此,IP模块还是有很多工作需要完成,首先我们先粗略地整理一下。

包收发操作的起点是TCP模块委托IP模块发送包的操作(下图中的“①发送”)。这个委托的过程就是TCP模块在数据块的前面加上TCP头部,然后整个传递给IP模块,这部分就是网络包的内容。与此同时,TCP模块还需要指定通信对象的IP地址,也就是需要写清楚“将什么内容发给谁”。

图2.17 包收发操作的整体过程:

收到委托后,IP模块会将包的内容当作一整块数据,在前面加上包含控制信息的头部。刚才我们讲过,IP模块会添加IP头部和MAC头部这两种头部。IP头部中包含IP协议规定的、根据IP地址将包发往目的地所需的控制信息;MAC头部包含通过以太网的局域网将包传输至最近的路由器所需的控制信息。关于IP头部和MAC头部的区别以及其中包含的控制信息的含义,我们将稍后介绍。总之,加上这两个头部之后,一个包就封装好了,这些就是IP模块负责的工作。

IP模块负责添加如下两个头部。

(1)MAC头部:以太网用的头部,包含MAC地址

(2)IP头部:IP用的头部,包含IP地址

接下来,封装好的包会被交给网络硬件(图2.17中的“②发送”),例如以太网、无线局域网等。网络硬件可能是插在计算机主板上的板卡,也可能是笔记本电脑上的PCMCIA卡,或者是计算机主板上集成的芯片,不同形态的硬件名字也不一样,本书将它们统称为网卡。传递给网卡的网络包是由一连串0和1组成的数字信息,网卡会将这些数字信息转换为电信号或光信号,并通过网线(或光纤)发送出去,然后这些信号就会到达集线器、路由器等转发设备,再由转发设备一步一步地送达接收方。

包送达对方之后,对方会作出响应。返回的包也会通过转发设备发送回来,然后我们需要接收这个包。接收的过程和发送的过程是相反的,信息先以电信号的形式从网线传输进来,然后由网卡将其转换为数字信息并传递给IP模块(图2.17中的“③接收”)。接下来,IP模块会将MAC头部和IP头部后面的内容,也就是TCP头部加上数据块,传递给TCP模块。接下来的操作就是我们之前讲过的TCP模块负责的部分了。

在这个过程中,有几个关键的点。TCP模块在收发数据时会分为好几个阶段,并为各个阶段设计了实现相应功能的网络包,但IP的包收发操作都是相同的,并不会因包本身而有所区别。因为IP模块会将TCP头部和数据块看作一整块二进制数据,在执行收发操作时并不关心其中的内容,也不关心这个包是包含TCP头部和数据两者都有呢,还是只有TCP头部而没有数据。当然,IP模块也不关心TCP的操作阶段,对于包的乱序和丢失也一概不知。总之,IP的职责就是将委托的东西打包送到对方手里,或者是将对方送来的包接收下来,仅此而已。因此,接下来我们要讲的这些关于IP的工作方式,可适用于任何TCP委派的收发操作。

2.5.3 生成包含接收方IP地址的IP头部

下面来看一看IP模块的具体工作过程。IP模块接受TCP模块的委托负责包的收发工作,它会生成IP头部并附加在TCP头部前面。IP头部包含的内容如表2.2所示,其中最重要的内容就是IP地址,它表示这个包应该发到哪里去。这个地址是由TCP模块告知的,而TCP又是在执行连接操作时从应用程序那里获得这个地址的,因此这个地址的最初来源就是应用程序。IP不会自行判断包的目的地,而是将包发往应用程序指定的接收方,即便应用程序指定了错误的IP地址,IP模块也只能照做。当然,这样做肯定会出错,但这个责任应该由应用程序来承担。

表2.2 IP头部格式

IP头部中还需要填写发送方的IP地址,大家可以认为是发送方计算机的IP地址,实际上“计算机的IP地址”这种说法并不准确。一般的客户端计算机上只有一块网卡,因此也就只有一个IP地址,这种情况下我们可以认为这个IP地址就是计算机的IP地址,但如果计算机上有多个网卡,情况就没那么简单了。IP地址实际上并不是分配给计算机的,而是分配给网卡的,因此当计算机上存在多块网卡时,每一块网卡都会有自己的IP地址。很多服务器上都会安装多块网卡,这时一台计算机就有多个IP地址,在填写发送方IP地址时就需要判断到底应该填写哪个地址。这个判断相当于在多块网卡中判断应该使用哪一块网卡来发送这个包,也就相当于判断应该把包发往哪个路由器,因此只要确定了目标路由器,也就确定了应该使用哪块网卡,也就确定了发送方的IP地址。

1
2
3
IP头部的“接收方IP地址”填写通信对象的IP地址。

发送方IP地址需要判断发送所使用的网卡,并填写该网卡的IP地址。

那么,我们应该如何判断应该把包交给哪块网卡呢?其实和图2.16中路由器使用IP表判断下一个路由器位置的操作是一样的。因为协议栈的IP模块与路由器中负责包收发的部分都是根据IP协议规则来进行包收发操作的,所以它们也都用相同的方法来判断把包发送给谁。

这个“IP表”叫作路由表,我们将在第3章探索路由器时详细介绍它的用法,这里先简单讲个大概。如图2.18所示,我们可以通过route print命令来显示路由表,下面来边看边讲。首先,我们对套接字中记录的目的地IP地址与路由表左侧的Network Destination栏进行比较,找到对应的一行。例如,TCP模块告知的目标IP地址为192.168.1.21,那么就对应图2.18中的第6行,因为它和192.168.1的部分相匹配。如果目标IP地址为10.10.1.166,那么就和10.10.1的部分相匹配,所以对应第3行。以此类推,我们需要找到与IP地址左边部分相匹配的条目,找到相应的条目之后,接下来看从右边数第2列和第3列的内容。右起第2列,也就是Interface列,表示网卡等网络接口,这些网络接口可以将包发送给通信对象。此外,右起第2列,也就是Interface列,表示网卡等网络接口,这些网络接口可以将包发送给通信对象。此外,右起第3列,即Gateway列表示下一个路由器的IP地址,将包发给这个IP地址,该地址对应的路由器就会将包转发到目标地址。路由表的第1行中,目标地址和子网掩码都是0.0.0.0,这表示默认网关,如果其他所有条目都无法匹配,就会自动匹配这一行。

图2.18 路由表示例

这样一来,我们就可以判断出应该使用哪块网卡来发送包了,然后就可以在IP头部的发送方IP地址中填上这块网卡对应的IP地址。

接下来还需要填写协议号,它表示包的内容是来自哪个模块的。例如,如果是TCP模块委托的内容,则设置为06(十六进制),如果是UDP模块委托的内容,则设置为17(十六进制),这些值都是按照规则来设置的。在现在我们使用的浏览器中,HTTP请求消息都是通过TCP来传输的,因此这里就会填写表示TCP的06(十六进制)。

2.5.4 生成以太网用的MAC头部

生成了IP头部之后,接下来IP模块还需要在IP头部的前面加上MAC头部(表2.3)。IP头部中的接收方IP地址表示网络包的目的地,通过这个地址我们就可以判断要将包发到哪里,但在以太网的世界中,TCP/IP的这个思路是行不通的。以太网在判断网络包目的地时和TCP/IP的方式不同,因此必须采用相匹配的方式才能在以太网中将包发往目的地,而MAC头部就是干这个用的。

IP模块在生成IP头部之后,会在它前面再加上MAC头部。MAC头部是以太网使用的头部,它包含了接收方和发送方的MAC地址等信息。

关于以太网的结构我们稍后会进行介绍,但下面的内容需要一些MAC头部的相关知识才能理解,因此先介绍一些最基础的。MAC头部的开头是接收方和发送方的MAC地址,大家可以认为它们和IP头部中的接收方和发送方IP地址的功能差不多,只不过IP地址的长度为32比特,而MAC地址为48比特。此外,IP地址是类似多少弄多少号这种现实中地址的层次化的结构,而MAC地址中的48比特可以看作是一个整体。尽管有上述差异,但从表示接收方和发送方的意义上来说,MAC地址和IP地址是没有区别的,因此大家可以暂且先把它们当成是一回事。第3个以太类型字段和IP头部中的协议号类似。在IP中,协议号表示IP头部后面的包内容的类型;而在以太网中,我们可以认为以太网类型后面就是以太网包的内容,而以太类型就表示后面内容的类型。以太网包的内容可以是IP、ARP等协议的包,它们都有对应的值,这也是根据规则来确定的。

表2.3 MAC头部的字段

在生成MAC头部时,只要设置表2.3中的3个字段就可以了。方便起见,我们按照从下往上的顺序来对表进行讲解。首先是“以太类型”,这里填写表示IP协议的值0800(十六进制)。接下来是发送方MAC地址,这里填写网卡本身的MAC地址。MAC地址是在网卡生产时写入ROM里的,只要将这个值读取出来写入MAC头部就可以了。对于多块网卡的情况,请大家回想一下设置发送方IP地址的方法。设置发送方IP地址时,我们已经判断出了从哪块网卡发送这个包,那么现在只要将这块网卡对应的MAC地址填进去就好了。

前面这些还比较简单,而接收方MAC地址就有点复杂了。只要告诉以太网对方的MAC的地址,以太网就会帮我们把包发送过去,那么很显然这里应该填写对方的MAC地址。然而,在这个时间点上,我们还没有把包发送出去,所以先得搞清楚应该把包发给谁,这个只要查一下路由表就知道了。在路由表中找到相匹配的条目,然后把包发给Gateway列中的IP地址就可以了。

既然已经知道了包应该发给谁,那么只要将对方的MAC地址填上去就好了,但到这里为止根本没有出现对方的MAC地址,也就是说我们现在根本不知道对方的MAC地址是什么。因此,我们还需要执行根据IP地址查询MAC地址的操作。

1
IP模块根据路由表Gateway栏的内容判断应该把包发送给谁。

2.5.5 通过ARP查询目标路由器的MAC地址

这里我们需要使用ARP,它其实非常简单。在以太网中,有一种叫作广播的方法,可以把包发给连接在同一以太网中的所有设备。ARP就是利用广播对所有设备提问:“××这个IP地址是谁的?请把你的MAC地址告诉我。”然后就会有人回答:“这个IP地址是我的,我的MAC地址是××××。”(图2.19)

图2.19 用ARP查询MAC地址

如果对方和自己处于同一个子网中,那么通过上面的操作就可以得到对方的MAC地址。然后,我们将这个MAC地址写入MAC头部,MAC头部就完成了。

不过,如果每次发送包都要这样查询一次,网络中就会增加很多ARP包,因此我们会将查询结果放到一块叫作ARP缓存的内存空间中留着以后用。也就是说,在发送包时,先查询一下ARP缓存,如果其中已经保存了对方的MAC地址,就不需要发送ARP查询,直接使用ARP缓存中的地址,而当ARP缓存中不存在对方MAC地址时,则发送ARP查询。显示ARP缓存的方法和MAC地址的写法如图2.20和图2.21所示,供大家参考。

图2.20 ARP缓存的内容

图2.21 MAC地址

有了ARP缓存,我们可以减少ARP包的数量,但如果总是使用ARP缓存中保存的地址也会产生问题。例如当IP地址发生变化时,ARP缓存的内容就会和现实发生差异。为了防止这种问题的发生,ARP缓存中的值在经过一段时间后会被删除,一般这个时间在几分钟左右。这个删除的操作非常简单粗暴,不管ARP缓存中的内容是否有效,只要经过几分钟就全部删掉,这样就不会出问题了。当地址从ARP缓存中删除后,只要重新执行一次ARP查询就可以再次获得地址了。

上面这个策略能够在几分钟后消除缓存和现实的差异,但IP地址刚刚发生改变的时候,ARP缓存中依然会保留老的地址,这时就会发生通信的异常。

1
查询MAC地址需要使用ARP。

将MAC头部加在IP头部的前面,整个包就完成了。到这里为止,整个打包的工作是由IP模块负责的。有人认为,MAC头部是以太网需要的内容,并不属于IP的职责范围,但从现实来看,让IP负责整个打包工作是有利的。如果在交给网卡之前,IP模块能够完成整个打包工作,那么网卡只要将打好的包发送出去就可以了。对于除IP以外的其他类型的包也是一样,如果在交给网卡之前完成打包,那么对于网卡来说,发送的操作和发送IP包是完全相同的。这样一来,同一块网卡就可以支持各种类型的包。至于接收操作,我们到后面会讲,但如果接收的包可以原封不动直接交给IP模块来处理,网卡就只要负责接收就可以了。这样一来,一块网卡也就能支持各种类型的包了。与其机械地设计模块和设备之间的分工,导致网卡只能支持IP包,不如将分工设计得现实一些,让网卡能够灵活支持各种类型的包。

2.5.6 以太网的基本知识

完成IP模块的工作之后,下面就该轮到网卡了,不过在此之前,我们先来了解一些以太网的基本知识。

以太网是一种为多台计算机能够彼此自由和廉价地相互通信而设计的通信技术,它的原型如图2.22(a)所示。从图上不难看出,这种网络的本质其实就是一根网线。图上还有一种叫作收发器的小设备,它的功能只是将不同网线之间的信号连接起来而已。因此,当一台计算机发送信号时,信号就会通过网线流过整个网络,最终到达所有的设备。这就好像所有人待在一个大房间里,任何一个人说话,所有人都能够听到,同样地,这种网络中任何一台设备发送的信号所有设备都能接收到。不过,我们无法判断一个信号到底是发给谁的,因此需要在信号的开头加上接收者的信息,也就是地址。这样一来就能够判断信号的接收者了,与接收者地址匹配的设备就接收这个包,其他的设备则丢弃这个包,这样我们的包就送到指定的目的地了。为了控制这一操作,我们就需要使用表2.3中列出的MAC头部。通过MAC头部中的接收方MAC地址,就能够知道包是发给谁的;而通过发送方MAC地址,就能够知道包是谁发出的;此外,通过以太类型就可以判断包里面装了什么类型的内容。以太网其实就这么简单。

图2.22 以太网的基本结构:

这个原型后来变成了图2.22(b)中的结构。这个结构是将主干网线替换成了一个中继式集线器[插图],将收发器网线替换成了双绞线。不过,虽然网络的结构有所变化,但信号会发送给所有设备这一基本性质并没有改变。

后来,图2.22(c)这样的使用交换式集线器的结构普及开来,现在我们说的以太网指的都是这样的结构。这个结构看上去和(b)很像,但其实里面有一个重要的变化,即信号会发送给所有设备这一性质变了,现在信号只会流到根据MAC地址指定的设备,而不会到达其他设备了。当然,根据MAC地址来传输包这一点并没有变,因此MAC头部的设计也得以保留。

尽管以太网经历了数次变迁,但其基本的3个性质至今仍未改变,即将包发送到MAC头部的接收方MAC地址代表的目的地,用发送方MAC地址识别发送方,用以太类型识别包的内容。因此,大家可以认为具备这3个性质的网络就是以太网

以太网中的各种设备也是基于以太网规格来工作的,因此下面的内容不仅适用于客户端计算机,同样也适用于服务器、路由器等各种设备。

此外,以太网和IP一样,并不关心网络包的实际内容,因此以太网的收发操作也和TCP的工作阶段无关,都是共通的。

2.5.7 将IP包转换成电或者光信号发送出去

下面来看看以太网的包收发操作。IP生成的网络包只是存放在内存中的一串数字信息,没有办法直接发送给对方。因此,我们需要将数字信息转换为电或光信号,才能在网线上传输,也就是说,这才是真正的数据发送过程。

负责执行这一操作的是网卡,但网卡也无法单独工作,要控制网卡还需要网卡驱动程序。驱动程序不只有网卡才有,键盘、鼠标、显卡、声卡等各种硬件设备都有。当然,不同厂商和型号的网卡在结构上有所不同,因此网卡驱动程序也是厂商开发的专用程序。

网卡的内部结构如图2.23所示,这是一张网卡主要构成要素的概念图,并不代表硬件的实际结构,但依然可以看清大体的思路。记住这一内部结构之后,我们再来介绍包收发的操作过程,现在,我们先来讲讲网卡的初始化过程。

网卡并不是通上电之后就可以马上开始工作的,而是和其他硬件一样,都需要进行初始化。也就是说,打开计算机启动操作系统的时候,网卡驱动程序会对硬件进行初始化操作,然后硬件才进入可以使用的状态。这些操作包括硬件错误检查、初始设置等步骤,这些步骤对于很多其他硬件也是共通的,但也有一些操作是以太网特有的,那就是在控制以太网收发操作的MAC模块中设置MAC地址。

图2.23 网卡:

网卡的ROM中保存着全世界唯一的MAC地址,这是在生产网卡时写入的,将这个值读出之后就可以对MAC模块进行设置,MAC模块就知道自己对应的MAC地址了。也有一些特殊的方法,比如从命令或者配置文件中读取MAC地址并分配给MAC模块。这种情况下,网卡会忽略ROM中的MAC地址。有人认为在网卡通电之后,ROM中的MAC地址就自动生效了,其实不然,真正生效的是网卡驱动进行初始化时在MAC模块中设置的那个MAC地址。在操作系统启动并完成这些初始化操作之后,网卡就可以等待来自IP的委托了。

1
网卡的ROM中保存着全世界唯一的MAC地址,这是在生产网卡时写入的。

网卡中保存的MAC地址会由网卡驱动程序读取并分配给MAC模块。

2.5.8 给网络包再加3个控制数据

好了,下面来看一看网卡是如何将包转换成电信号并发送到网线中的。网卡驱动从IP模块获取包之后,会将其复制到网卡内的缓冲区中,然后向MAC模块发送发送包的命令。接下来就轮到MAC模块进行工作了。

首先,MAC模块会将包从缓冲区中取出,并在开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列(图2.24)

图2.24 网卡发送出去的包:

图中显示了协议栈和网卡对包的处理过程。MAC头部很容易被误解为是由网卡来处理的,实际上它是由TCP/IP软件来负责的。

报头是一串像10101010…这样1和0交替出现的比特序列,长度为56比特,它的作用是确定包的读取时机。当这些1010的比特序列被转换成电信号后,会形成如图2.25这样的波形。接收方在收到信号时,遇到这样的波形就可以判断读取数据的时机。关于这一块内容,我们得先讲讲如何通过电信号来读取数据。

2.5.9 向集线器发送网络包

加上报头、起始帧分界符和FCS之后,我们就可以将包通过网线发送出去了(图2.24)。发送信号的操作分为两种,一种是使用集线器的半双工模式,另一种是使用交换机的全双工模式。

在半双工模式中,为了避免信号碰撞,首先要判断网线中是否存在其他设备发送的信号。如果有,则需要等待该信号传输完毕,因为如果在有信号时再发送一组信号,两组信号就会发生碰撞。当之前的信号传输完毕,或者本来就没有信号在传输的情况下,我们就可以开始发送信号了。

另一种全双工模式我们会在第3章探索交换机时进行介绍,在全双工模式中,发送和接收可以同时进行,不会发生碰撞。因此,全双工模式中不需要像半双工模式这样考虑这么多复杂的问题,即便接收线路中有信号进来,也可以直接发送信号。

2.5.10 接收返回包

网卡将包转换为电信号并发送出去的过程到这里就结束了,既然讲到了以太网的工作方式,那我们不妨继续看看接收网络包时的操作过程。

在使用集线器的半双工模式以太网中,一台设备发送的信号会到达连接在集线器上的所有设备。这意味着无论是不是发给自己的信号都会通过接收线路传进来,因此接收操作的第一步就是不管三七二十一把这些信号全都收进来再说。

信号的开头是报头,通过报头的波形同步时钟,然后遇到起始帧分界符时开始将后面的信号转换成数字信息。这个操作和发送时是相反的,即PHY(MAU)模块先开始工作,然后再轮到MAC模块。


通知计算机的操作会使用一个叫作中断的机制。在网卡执行接收包的操作的过程中,计算机并不是一直监控着网卡的活动,而是去继续执行其他的任务。因此,如果网卡不通知计算机,计算机是不知道包已经收到了这件事的。网卡驱动也是在计算机中运行的一个程序,因此它也不知道包到达的状态。在这种情况下,我们需要一种机制能够打断计算机正在执行的任务,让计算机注意到网卡中发生的事情,这种机制就是中断

2.5.11 将服务器的响应包从IP传递给TCP

下面我们假设Web服务器返回了一个网络包,那么协议栈会进行哪些处理呢?服务器返回的包的以太类型应该是0800,因此网卡驱动会将其交给TCP/IP协议栈来进行处理。接下来就轮到IP模块先开始工作了,第一步是检查IP头部,确认格式是否正确。如果格式没有问题,下一步就是查看接收方IP地址。如果接收网络包的设备是一台Windows客户端计算机,那么服务器返回的包的接收方IP地址应该与客户端网卡的地址一致,检查确认之后我们就可以接收这个包了。

如果接收方IP地址不是自己的地址,那一定是发生了什么错误。客户端计算机不负责对包进行转发,因此不应该收到不是发给自己的包。当发生这样的错误时,IP模块会通过ICMP消息将错误告知发送方(图2.1)。ICMP规定了各种类型的消息,如表2.4所示。当我们遇到这个错误时,IP模块会通过表2.4中的Destination unreachable消息通知对方。从这张表的内容中我们可以看到在包的接收和转发过程中能够遇到的各种错误,因此希望大家看一看这张表。

表2.4 主要的ICMP消息:

如果接收方IP地址正确,则这个包会被接收下来,这时还需要完成另一项工作。IP协议有一个叫作分片的功能,具体的内容我们将在第3章探索路由器时进行介绍。简单来说,网线和局域网中只能传输小包,因此需要将大的包切分成多个小包。如果接收到的包是经过分片的,那么IP模块会将它们还原成原始的包。分片的包会在IP头部的标志字段中进行标记,当收到分片的包时,IP模块会将其暂存在内部的内存空间中,然后等待IP头部中具有相同ID的包全部到达,这是因为同一个包的所有分片都具有相同的ID。此外,IP头部还有一个分片偏移量(fragment offset)字段,它表示当前分片在整个包中所处的位置。根据这些信息,在所有分片全部收到之后,就可以将它们还原成原始的包,这个操作叫作分片重组。

到这里,IP模块的工作就结束了,接下来包会被交给TCP模块。TCP模块会根据IP头部中的接收方和发送方IP地址,以及TCP头部中的接收方和发送方端口号来查找对应的套接字。找到对应的套接字之后,就可以根据套接字中记录的通信状态,执行相应的操作了。例如,如果包的内容是应用程序数据,则返回确认接收的包,并将数据放入缓冲区,等待应用程序来读取;如果是建立或断开连接的控制包,则返回相应的响应控制包,并告知应用程序建立和断开连接的操作状态。

2.6 UDP协议的收发操作

2.6.1 不需要重发的数据用UDP发送更高效

跟着第1章的脚步,本章我们探索了通过套接字收发数据的整个过程,这个过程到这里已经告一段落了。接下来,网络包会从计算机出来跑向集线器,这个过程我们将在下一章来介绍,现在先来说点题外话。

大多数的应用程序都像之前介绍的一样使用TCP协议来收发数据,但当然也有例外。有些应用程序不使用TCP协议,而是使用UDP协议来收发数据。向DNS服务器查询IP地址的时候我们用的也是UDP协议。下面就简单介绍一下UDP协议。

其实TCP中就包含了UDP的一些要点。TCP的工作方式十分复杂,如果我们能够理解TCP为什么要设计得如此复杂,也就能够理解UDP了。那么,为什么要设计得如此复杂呢?因为我们需要将数据高效且可靠地发送给对方。为了实现可靠性,我们就需要确认对方是否收到了我们发送的数据,如果没有还需要再发一遍。

要实现上面的要求,最简单的方法是数据全部发送完毕之后让接收方返回一个接收确认。这样一来,如果没收到直接全部重新发送一遍就好了,根本不用像TCP一样要管理发送和确认的进度。但是,如果漏掉了一个包就要全部重发一遍,怎么看都很低效。为了实现高效的传输,我们要避免重发已经送达的包,而是只重发那些出错的或者未送达的包。TCP之所以复杂,就是因为要实现这一点。

不过,在某种情况下,即便没有TCP这样复杂的机制,我们也能够高效地重发数据,这种情况就是数据很短,用一个包就能装得下。如果只有一个包,就不用考虑哪个包未送达了,因为全部重发也只不过是重发一个包而已,这种情况下我们就不需要TCP这样复杂的机制了。而且,如果不使用TCP,也不需要发送那些用来建立和断开连接的控制包了。此外,我们发送了数据,对方一般都会给出回复,只要将回复的数据当作接收确认就行了,也不需要专门的接收确认包了。

2.6.2 控制用的短数据

这种情况就适合使用UDP。像DNS查询等交换控制信息的操作基本上都可以在一个包的大小范围内解决,这种场景中就可以用UDP来代替TCP。 UDP没有TCP的接收确认、窗口等机制,因此在收发数据之前也不需要交换控制信息,也就是说不需要建立和断开连接的步骤,只要在从应用程序获取的数据前面加上UDP头部,然后交给IP进行发送就可以了(表2.5)。接收也很简单,只要根据IP头部中的接收方和发送方IP地址,以及UDP头部中的接收方和发送方端口号,找到相应的套接字并将数据交给相应的应用程序就可以了。除此之外,UDP协议没有其他功能了,遇到错误或者丢包也一概不管。因为UDP只负责单纯地发送包而已,并不像TCP一样会对包的送达状态进行监控,所以协议栈也不知道有没有发生错误。但这样并不会引发什么问题,因此出错时就收不到来自对方的回复,应用程序会注意到这个问题,并重新发送一遍数据。这样的操作本身并不复杂,也并不会增加应用程序的负担。

表2.5 UDP头部中的控制信息:

2.6.3 音频和视频数据

还有另一个场景会使用UDP,就是发送音频和视频数据的时候。音频和视频数据必须在规定的时间内送达,一旦送达晚了,就会错过播放时机,导致声音和图像卡顿。如果像TCP一样通过接收确认响应来检查错误并重发,重发的过程需要消耗一定的时间,因此重发的数据很可能已经错过了播放的时机。一旦错过播放时机,重发数据也是没有用的,因为声音和图像已经卡顿了,这是无法挽回的。当然,我们可以用高速线路让重发的数据能够在规定的时间内送达,但这样一来可能要增加几倍的带宽才行。

此外,音频和视频数据中缺少了某些包并不会产生严重的问题,只是会产生一些失真或者卡顿而已,一般都是可以接受的。

在这些无需重发数据,或者是重发了也没什么意义的情况下,使用UDP发送数据的效率会更高。

本章我们探索了在收发数据时,操作系统中的协议栈是如何工作的,以及网卡是如何将包转换成电信号通过网线发送出去的。到这里,我们的网络包已经沿着网线流出了客户端计算机,下一章,我们将探索网络包如何经过集线器、交换机、路由器等设备,最终到达互联网。

问题

1.表示网络包收件人的接收方IP地址是位于IP头部还是TCP头部中呢?–> IP头部

2.端口号用来指定服务器程序的种类,那么它位于TCP头部还是IP头部中呢?–> TCP头部

3.会对包是否正确送达进行确认的是TCP还是IP呢?–> TCP

4.根据IP地址查询MAC地址的机制叫什么?–> ARP

5.在收到ACK号之前继续发送下一个包的方式叫什么? –> 滑动窗口方式

3、从网络到网络设备 — 探索集线器、交换机和路由器

问题

  1. 问题:路由器比交换机问世时间更早。

答案:√。交换机比路由器更加简单,因此可能有人以为交换机应该比路由器出现得更早,其实是路由器先问世的。

  1. 问题:对于路由器和交换机,如果包在传输过程中发生错误,会直接丢弃错误的包而不会尝试修复。

答案:√。不过操作系统中的网络控制软件(协议栈)会对丢弃的包进行重发,数据不会因此丢失。

上一章,我们探索了客户端中的协议栈和网卡,介绍了发送网络包,也就是将网络包转换成电信号通过网线传输出去的过程。本章我们将继续跟着上一章的脚步,看一看通过网线传输出去的包是如何经过集线器、交换机和路由器等网络设备,最终进入互联网的。

(1)信号在网线和集线器中传输

信号从计算机中流出之后,会在网线中经过集线器等设备前进。此时,信号是如何在网线和集线器传输的,就是我们的第一个看点。信号在传输过程中会衰减,还会受到噪声干扰而失真,如何抑制这些影响是我们的另一个看点。

(2)交换机的包转发操作

交换机的工作方式也是本章看点之一。交换机并不只是简单地让信号流过,而是先接收信号并将其还原为数字信息,然后再重新转换成信号并发送出去的过程。这里我们将详细探索这一过程。

(3)路由器的包转发操作

路由器和交换机一样也负责对包进行转发,但它们的工作方式有一些差异。交换机是基于以太网规格工作的设备,而路由器是基于IP工作的,它们之间的差异也是本章看点之一。

(4)路由器的附加功能

位于互联网接入端的路由器通常还会提供一些附加功能,例如将私有地址转换为公有地址的地址转换功能,以及阻止危险网络包的包过滤功能等。本章最后将介绍一下这些功能,这样我们就会对路由器有较全面的认识。

3.1 信号在网线和集线器中传输

3.1.1 每个包都是独立传输的

从计算机发送出来的网络包会通过集线器、路由器等设备被转发,最终到达目的地。我们在第2章的2.5.1节和2.5.2节讲过,转发设备会根据包头部中的控制信息,在转发设备内部一个写有转发规则的表中进行查询,以此来判断包的目的地,然后将包朝目的地的方向进行转发。邮递员在送信的时候只看信封,不看里面的内容,同样地,转发设备在进行转发时也不看数据的内容。因此,无论包里面装的是应用程序的数据或者是TCP协议的控制信息,都不会对包的传输操作本身产生影响。换句话说,HTTP请求的方法,TCP的确认响应和序号,客户端和服务器之间的关系,这一切都与包的传输无关。因此,所有的包在传输到目的地的过程中都是独立的,相互之间没有任何关联。

3.1.2 防止网线中的信号衰减很重要

信号到达集线器的时候并不是跟刚发送出去的时候一模一样。集线器收到的信号有时会出现衰减(图3.3)。信号在网线的传输过程中,能量会逐渐损失。网线越长,信号衰减就越严重。

图3.3 接收方信号变得难以识别

在发送方一端还十分清晰的矩形信号波形,在传输过程不断衰减,波形也会失真,导致接收方难以读取。

3.1.3 “双绞”是为了抑制噪声

局域网网线使用的是双绞线,其中“双绞”的意思就是以两根信号线为一组缠绕在一起,这种拧麻花一样的设计是为了抑制噪声的影响。

那么双绞线为什么能够抑制噪声呢?首先,我们来看看噪声是如何产生的。产生噪声的原因是网线周围的电磁波,当电磁波接触到金属等导体时,在其中就会产生电流。因此,如果网线周围存在电磁波,就会在网线中产生和原本的信号不同的电流。由于信号本身也是一种带有电压变化的电流,其本质和噪声产生的电流是一样的,所以信号和噪声的电流就会混杂在一起,导致信号的波形发生失真,这就是噪声的影响。

影响网线的电磁波分为两种。一种是由电机、荧光灯、CRT显示器等设备泄漏出来的电磁波,这种电磁波来自网线之外的其他设备,我们来看看双绞线如何抑制这种电磁波的影响。首先,信号线是用金属做成的,当电磁波接触到信号线时,会沿电磁波传播的右旋方向产生电流,这种电流会导致波形发生失真。如果我们将信号线缠绕在一起,信号线就变成了螺旋形,其中两根信号线中产生的噪声电流方向就会相反,从而使得噪声电流相互抵消,噪声就得到了抑制(图3.4(a))。当然,即便信号线变成螺旋形,里面的信号依然可以原样传输,也就是说,信号没有变,只是噪声被削弱了。

另一种电磁波是从网线中相邻的信号线泄漏出来的。由于传输的信号本身就是一种电流,当电流流过时就会向周围发出电磁波,这些电磁波对于其他信号线来说就成了噪声。这种内部产生的噪声称为串扰(crosstalk)。

这种噪声的强度其实并不高,但问题是噪声源的距离太近了。距离发生源越远,电磁波就会因扩散而变得越弱,但在同一根网线中的信号线之间距离很近,这些电磁波还没怎么衰减就已经接触到了相邻的信号线。因此,尽管信号线产生的电磁波十分微弱,也能够在相邻的信号线中产生感应电流。

要抑制这种噪声,关键在于双绞线的缠绕方式。在一根网线中,每一对信号线的扭绞间隔(节距)都有一定的差异,这使得在某些地方正信号线距离近,另一些地方则是负信号线距离近。由于正负信号线产生的噪声影响是相反的,所以两者就会相互抵消(图3.4(b))。从网线整体来看,正负的分布保持平衡,自然就会削弱噪声的影响。

图3.4 双绞线对噪声的抑制

(a)通过两根信号线的缠绕抵消外源性噪声;(b)通过改变节距抑制内源性噪声。

3.1.4 集线器将信号发往所有线路

当信号到达集线器后,会被广播到整个网络中。以太网的基本架构就是将包发到所有的设备,然后由设备根据接收方MAC地址来判断应该接收哪些包,而集线器就是这一架构的忠实体现,它就是负责按照以太网的基本架构将信号广播出去。下面来看看它的工作方式。

图3.5 交叉网线的使用

图3.6 交叉网线

1
集线器将信号发送给所有连接在它上面的线路。

由于集线器只是原封不动地将信号广播出去,所以即便信号受到噪声的干扰发生了失真,也会原样发送到目的地。这时,接收信号的设备,也就是交换机、路由器、服务器等,会在将信号转换成数字信息后通过FCS校验发现错误,并将出错的包丢弃。当然,丢弃包并不会影响数据的传输,因为丢弃的包不会触发确认响应。因此协议栈的TCP模块会检测到丢包,并对该包进行重传。

3.2 交换机的包转发操作

3.2.1 交换机根据地址表进行转发

下面来看一下包是如何通过交换机的。交换机的设计是将网络包原样转发到目的地,图3.7就是它的内部结构,

首先,信号到达网线接口,并由PHY(MAU)模块进行接收,这一部分和集线器是相同的。也就是说,它的接口和PHY(MAU)模块也是以MDI-X模式进行连接的,当信号从双绞线传入时,就会进入PHY(MAU)模块的接收部分。

接下来,PHY(MAU)模块会将网线中的信号转换为通用格式,然后传递给MAC模块。MAC模块将信号转换为数字信息,然后通过包末尾的FCS校验错误,如果没有问题则存放到缓冲区中。

图3.7 交换机的结构

1
交换机根据MAC地址表查找MAC地址,然后将信号发送到相应的端口。

3.2.2 MAC地址表的维护

交换机在转发包的过程中,还需要对MAC地址表的内容进行维护,维护操作分为两种。

第一种是收到包时,将发送方MAC地址以及其输入端口的号码写入MAC地址表中。由于收到包的那个端口就连接着发送这个包的设备,所以只要将这个包的发送方MAC地址写入地址表,以后当收到发往这个地址的包时,交换机就可以将它转发到正确的端口了。交换机每次收到包时都会执行这个操作,因此只要某个设备发送过网络包,它的MAC地址就会被记录到地址表中。

另一种是删除地址表中某条记录的操作,这是为了防止设备移动时产生问题。比如,我们在开会时会把笔记本电脑从办公桌拿到会议室,这时设备就发生了移动。从交换机的角度来看,就是本来连接在某个端口上的笔记本电脑消失了。这时如果交换机收到了发往这台已经消失的笔记本电脑的包,那么它依然会将包转发到原来的端口,通信就会出错,因此必须想办法删除那些过时的记录。然而,交换机没办法知道这台笔记本电脑已经从原来的端口移走了。因此地址表中的记录不能永久有效,而是要在一段时间不使用后就自动删除。

那么当笔记本电脑被拿到会议室之后,会议室里的交换机又会如何工作呢?只要笔记本电脑连接到会议室的交换机,交换机就会根据笔记本电脑发出的包来更新它的地址表。因此,对于目的地的交换机来说,不需要什么特别的措施就可以正常工作了。

综合来看,为了防止终端设备移动产生问题,只需要将一段时间不使用的过时记录从地址表中删除就可以了。

过时记录从地址表中删除的时间一般为几分钟,因此在过时记录被删除之前,依然可能有发给该设备的包到达交换机。这时,交换机会将包转发到老的端口,通信就会发生错误,这种情况尽管罕见,但的确也有可能发生。不过大家不必紧张,遇到这样的情况,只要重启一下交换机,地址表就会被清空并更新正确的信息,然后网络就又可以正常工作了。

总之,交换机会自行更新或删除地址表中的记录,不需要手动维护。当地址表的内容出现异常时,只要重启一下交换机就可以重置地址表,也不需要手动进行维护。

3.2.3 特殊操作

上面介绍了交换机的基本工作方式,下面来看一些特殊情况下的操作。比如,交换机查询地址表之后发现记录中的目标端口和这个包的源端口是同一个端口。当像图3.9这样用集线器和交换机连接在一起时就会遇到这样的情况,那么这种情况要怎么处理呢?

图3.9 不向源端口转发网络包

3.2.4 全双工模式可以同时进行发送和接收

全双工模式是交换机特有的工作模式,它可以同时进行发送和接收操作,集线器不具备这样的特性。

使用集线器时,如果多台计算机同时发送信号,信号就会在集线器内部混杂在一起,进而无法使用,这种现象称为碰撞,是以太网的一个重要特征。不过,只要不用集线器,就不会发生碰撞。

而使用双绞线时,发送和接收的信号线是各自独立的,因此在双绞线中信号不会发生碰撞。网线连接的另一端,即交换机端口和网卡的PHY(MAU)模块以及MAC模块,其内部发送和接收电路也是各自独立的,信号也不会发生碰撞。因此,只要不用集线器,就可以避免信号碰撞了。

如果不存在碰撞,也就不需要半双工模式中的碰撞处理机制了。也就是说,发送和接收可以同时进行。然而,以太网规范中规定了在网络中有信号时要等该信号结束后再发送信号,因此发送和接收还是无法同时进行。于是,人们对以太网规范进行了修订,增加了一个无论网络中有没有信号都可以发送信号的工作模式,同时规定在这一工作模式下停用碰撞检测(图3.10)。这种工作模式就是全双工模式。在全双工模式下,无需等待其他信号结束就可以发送信号,因此它比半双工模式速度要快。由于双方可以同时发送数据,所以可同时传输的数据量也更大,性能也就更高。

1
交换机的全双工模式可以同时发送和接收信号。

3.2.5 自动协商:确定最优的传输速率

随着全双工模式的出现,如何在全双工和半双工模式之间进行切换的问题也产生了。在全双工模式刚刚出现的时候,还需要手动进行切换,但这样实在太麻烦,于是后来出现了自动切换工作模式的功能。这一功能可以由相互连接的双方探测对方是否支持全双工模式,并自动切换成相应的工作模式。此外,除了能自动切换工作模式之外,还能探测对方的传输速率并进行自动切换。这种自动切换的功能称为自动协商。

图3.10 全双工模式的工作方式

3.2.6 交换机可同时执行多个转发操作

交换机只将包转发到具有特定MAC地址的设备连接的端口,其他端口都是空闲的。如图3.7中的例子所示,当包从最上面的端口发送到最下面的端口时,其他端口都处于空闲状态,这些端口可以传输其他的包,因此交换机可以同时转发多个包。

相对地,集线器会将输入的信号广播到所有的端口,如果同时输入多个信号就会发生碰撞,无法同时传输多路信号,因此从设备整体的转发能力来看,交换机要高于集线器。

3.3 路由器的包转发操作

3.3.1 路由器的基本知识

网络包经过集线器和交换机之后,现在到达了路由器,并在此被转发到下一个路由器。这一步转发的工作原理和交换机类似,也是通过查表判断包转发的目标。不过在具体的操作过程上,路由器和交换机是有区别的。因为路由器是基于IP设计的,而交换机是基于以太网设计的。IP和以太网的区别在很多地方都会碰到,我们稍后再具体讲,现在先来看看路由器的概况。

首先,路由器的内部结构如图3.12所示。这张图已经画得非常简略了,大家只要看明白路由器包括转发模块和端口模块两部分就可以了。其中转发模块负责判断包的转发目的地,端口模块负责包的收发操作。这一分工模式在第2章介绍计算机内部结构的时候也出现过,换句话说,路由器转发模块和端口模块的关系,就相当于协议栈的IP模块和网卡之间的关系。因此,大家可以将路由器的转发模块想象成IP模块,将端口模块想象成网卡。

图3.12 路由器的结构

通过更换网卡,计算机不仅可以支持以太网,也可以支持无线局域网,路由器也是一样。如果路由器的端口模块安装了支持无线局域网的硬件,就可以支持无线局域网了。此外,计算机的网卡除了以太网和无线局域网之外很少见到支持其他通信技术的品种,而路由器的端口模块则支持除局域网之外的多种通信技术,如ADSL、FTTH,以及各种宽带专线等,只要端口模块安装了支持这些技术的硬件即可。

1
路由器的各个端口都具有MAC地址和IP地址。

3.3.2 路由表中的信息

在“查表判断转发目标”这一点上,路由器和交换机的大体思路是类似的,不过具体的工作过程有所不同。交换机是通过MAC头部中的接收方MAC地址来判断转发目标的,而路由器则是根据IP头部中的IP地址来判断的。由于使用的地址不同,记录转发目标的表的内容也会不同。

1
路由器根据“IP地址”判断转发目标。

图3.13 路由器根据路由表对包进行转发

1
路由器会忽略主机号,只匹配网络号。

图3.14 路由聚合

对路由表进行维护的方法有几种,大体上可分为以下两类。

(a)由人手动维护路由记录

(b)根据路由协议机制,通过路由器之间的信息交换由路由器自行维护路由表的记录

其中(b)中提到的路由协议有很多种,例如RIP、OSPC、BGP等都属于路由协议。

3.3.3 路由器的包接收操作

下面我们来看一看路由器的整个工作过程。首先,路由器会接收网络包。路由器的端口有各种不同的类型,这里我们只介绍以太网端口是如何接收包的。以太网端口的结构和计算机的网卡基本相同,接收包并存放到缓冲区中的过程也和网卡几乎没有区别。

首先,信号到达网线接口部分,其中的PHY(MAU)模块和MAC模块将信号转换为数字信息,然后通过包末尾的FCS进行错误校验,如果没问题则检查MAC头部中的接收方MAC地址,看看是不是发给自己的包,如果是就放到接收缓冲区中,否则就丢弃这个包。如果包的接收方MAC地址不是自己,说明这个包是发给其他设备的,如果接收这个包就违反了以太网的规则。

1
路由器的端口都具有MAC地址,只接收与自身地址匹配的包,遇到不匹配的包则直接丢弃。

3.3.4 查询路由表确定输出端口

完成包接收操作之后,路由器就会丢弃包开头的MAC头部。MAC头部的作用就是将包送达路由器,其中的接收方MAC地址就是路由器端口的MAC地址。因此,当包到达路由器之后,MAC头部的任务就完成了,于是MAC头部就会被丢弃。

1
通过路由器转发的网络包,其接收方MAC地址为路由器端口的MAC地址。

接下来,路由器会根据MAC头部后方的IP头部中的内容进行包的转发操作。转发操作分为几个阶段,首先是查询路由表判断转发目标。关于具体的工作过程,我们还是来看一个实际的例子,如图3.13的情况,假设地址为10.10.1.101的计算机要向地址为192.168.1.10的服务器发送一个包,这个包先到达图中的路由器。判断转发目标的第一步,就是根据包的接收方IP地址查询路由表中的目标地址栏,以找到相匹配的记录。

3.3.5 找不到匹配路由时选择默认路由

既然如此,那么是不是所有的转发目标都需要配置在路由表中才行呢?如果是公司或者家庭网络,这样的做法也没什么问题,但互联网中的转发目标可能超过20万个,如果全部要配置在路由表中实在是不太现实。

其实,大家不必担心,因为之前的图3.13路由表中的最后一行的作用就相当于把所有目标都配置好了。这一行的子网掩码为0.0.0.0,关键就在这里,子网掩码0.0.0.0的意思是网络包接收方IP地址和路由表目标地址的匹配中需要匹配的比特数为0,换句话说,就是根本不需要匹配。只要将子网掩码设置为0.0.0.0,那么无论任何地址都能匹配到这一条记录,这样就不会发生不知道要转发到哪里的问题了。

只要在这一条记录的网关列中填写接入互联网的路由器地址,当匹配不到其他路由时[插图],网络包就会被转发到互联网接入路由器。因此这条记录被称为默认路由,这一行配置的网关地址被称为默认网关。在计算机的TCP/IP设置窗口中也有一个填写默认网关的框,意思是一样的。计算机上也有一张和路由器一样的路由表,其中默认网关的地址就是我们在设置窗口中填写的地址。

1
路由表中子网掩码为0.0.0.0的记录表示“默认路由”。

这样一来,无论目标地址是表示一个子网还是表示某台设备,都可以用相同的方法查找出转发目标,而且也避免了不知道转发到哪里的问题。

3.3.6 包的有效期

从路由表中查找到转发目标之后,网络包就会被转交给输出端口,并最终发送出去,但在此之前,路由器还有一些工作要完成。

第一个工作是更新IP头部中的TTL(Time to Live,生存时间)字段(参见第2章的表2.2)。TTL字段表示包的有效期,包每经过一个路由器的转发,这个值就会减1,当这个值变成0时,就表示超过了有效期,这个包就会被丢弃。

3.3.7 通过分片功能拆分大网络包

路由器的端口并不只有以太网一种,也可以支持其他局域网或专线通信技术。不同的线路和局域网类型各自能传输的最大包长度也不同,因此输出端口的最大包长度可能会小于输入端口。。即便两个端口的最大包长度相同,也可能会因为添加了一些头部数据而导致包的实际长度发生变化,ADSL、FTTH等宽带接入技术中使用的PPPoE协议就属于这种情况。无论哪种情况,一旦转发的包长度超过了输出端口能传输的最大长度,就无法直接发送这个包了。

遇到这种情况,可以使用IP协议中定义的分片功能对包进行拆分,缩短每个包的长度。需要注意的是,这里说的分片和第2章介绍的TCP对数据进行拆分的机制是不同的。TCP拆分数据的操作是在将数据装到包里之前进行的,换句话说,拆分好的一个数据块正好装进一个包里。从IP分片的角度来看,这样一个包其实是一个未拆分的整体,也就是说,分片是对一个完整的包再进行拆分的过程。

分片操作的过程如图3.15所示。首先,我们需要知道输出端口的MTU,看看这个包能不能不分片直接发送。最大包长度是由端口类型决定的,用这个最大长度减掉头部的长度就是MTU,将MTU与要转发的包长度进行比较。如果输出端口的MTU足够大,那么就可以不分片直接发送;如果输出端口的MTU太小,那么就需要将包按照这个MTU进行分片,但在此之前还需要看一下IP头部中的标志字段,确认是否可以分片。

图3.15 对包进行拆分的分片功能

3.3.8 路由器的发送操作和计算机相同

到这里,发送前的准备工作就完成了,接下来就会进入包的发送操作。这一步操作取决于输出端口的类型。如果是以太网端口,则按照以太网的规则将包转换为电信号发送出去;如果是ADSL则按照ADSL的规则来转换,以此类推。在家庭网络中,路由器后面一般连接ADSL等线路接入互联网,因此路由器会根据接入网的规则来发送包。

路由器判断下一个转发目标的方法如下。

● 如果路由表的网关列内容为IP地址,则该地址就是下一个转发目标。

● 如果路由表的网关列内容为空,则IP头部中的接收方IP地址就是下一个转发目标。

3.3.9 路由器与交换机的关系

关于路由器的基本工作,也就是包转发,到这里就全部讲完了,下面来整理一下路由器与交换机的关系。

要理解两者之间的关系,关键点在于计算机在发送网络包时,或者是路由器在转发网络包时,都需要在前面加上MAC头部。之前的讲解都是说在开头加上MAC头部,如果看图3.16大家可以发现,准确的说法应该是将IP包装进以太网包的数据部分中。也就是说,给包加上MAC头部并发送,从本质上说是将IP包装进以太网包的数据部分中,委托以太网去传输这些数据。IP协议本身没有传输包的功能,因此包的实际传输要委托以太网来进行。路由器是基于IP设计的,而交换机是基于以太网设计的,因此IP与以太网的关系也就是路由器与交换机的关系。换句话说,路由器将包的传输工作委托给交换机来进行。当然,这里讲的内容只适用于原原本本实现IP和以太网机制的纯粹的路由器和交换机,实际的路由器有内置交换机功能的,比如用于连接互联网的家用路由器就属于这一种,对于这种路由器,上面内容可能就不适用了。但是,如果把这种“不纯粹”的路由器拆分成“纯粹”的路由器和“纯粹”的交换机,则它们各自都适用上面的内容。

图3.16 将IP包装进以太网包的数据部分

从包的转发目标也可以看出路由器和交换机之间的委托关系。IP并不是委托以太网将包传输到最终目的地,而是传输到下一个路由器。在创建MAC头部时,也是从IP的路由表中查找出下一个路由器的IP地址,并通过ARP查询出MAC地址,然后将MAC地址写入MAC头部中的,这表示IP对以太网的委托只是将包传输到下一个路由器就行了。当包到达下一个路由器后,下一个路由器又会重新委托以太网将包传输到再下一个路由器。随着这一过程反复执行,包就会最终到达IP的目的地,也就是通信的对象。

到这里我们已经梳理了路由器与交换机之间的关系。简单来说,IP(路由器)负责将包发送给通信对象这一整体过程,而其中将包传输到下一个路由器的过程则是由以太网(交换机)来负责的。

当然,网络并非只有以太网一种,还有无线局域网,以及接入互联网的通信线路,它们和IP之间的关系又是什么样的呢?其实只要将以太网替换成无线局域网、互联网线路等通信规格就可以了。也就是说,如果和下一个路由器之间是通过无线局域网连接的,那么就委托无线局域网将包传输过去;如果是通过互联网线路连接的,那么就委托它将包传输过去。除了这里列举的例子之外,世界上还有很多其他类型的通信技术,它们之间的关系也是一样的,都是委托所使用的通信技术将包传输过去。

IP本身不负责包的传输,而是委托各种通信技术将包传输到下一个路由器,这样的设计是有重要意义的,即可以根据需要灵活运用各种通信技术,这也是IP的最大特点。正是有了这一特点,我们才能够构建出互联网这一规模巨大的网络。

1
IP(路由器)负责将包送达通信对象这一整体过程,而其中将包传输到下一个路由器的过程则是由以太网(交换机)来负责的。

3.4 路由器的附加功能

3.4.1 通过地址转换有效利用IP地址

刚才我们介绍了路由器的基本工作过程,现在的路由器除了这些基本功能之外,还有一些附加功能。下面来介绍两种最重要的功能——地址转换和包过滤。

首先,我们先了解一下地址转换功能出现的背景。所谓地址,就是用来识别每一台设备的标志,因此每台设备都应该有一个唯一不重复的地址,就好像如果很多人的地址都一样,那么快递员就不知道该把包裹送给谁了。网络也是一样,本来互联网中所有的设备都应该有自己的固定地址,而且最早也确实是这样做的。比如,公司内网需要接入互联网的时候,应该向地址管理机构申请IP地址,并将它们分配给公司里的每台设备。换句话说,那个时候没有内网和外网的区别,所有客户端都是直接连接到互联网的。

尽管互联网原本是这样设计的,但进入20世纪90年代之后,互联网逐步向公众普及,接入互联网的设备数量也快速增长,如此一来,情况就发生了变化。如果还用原来的方法接入,过不了多久,可分配的地址就用光了。如果不能保证每台设备有唯一不重复的地址,就会从根本上影响网络包的传输,这是一个非常严重的问题。如果任由这样发展下去,不久的将来,一旦固定地址用光,新的设备就无法接入了,互联网也就无法继续发展了。

解决这个问题的关键在于固定地址的分配方式。举个例子,假如有A、B两家公司,它们的内网是完全独立的。这种情况下,两家公司的内网之间不会有网络包流动,即使A公司的某台服务器和B公司的某台客户端具有相同的IP地址也没关系,因为它们之间不会进行通信。只要在每家公司自己的范围内,能够明确判断网络包的目的地就可以了,是否和其他公司的内网地址重复无关紧要,只要每个公司的网络是相互独立的,就不会出现问题。

解决地址不足的问题,利用的就是这样的性质,即公司内部设备的地址不一定要和其他公司不重复。这样一来,公司内部设备就不需要分配固定地址了,从而大幅节省了IP地址。当然,就算是公司内网,也不是可以随便分配地址的,因此需要设置一定的规则,规定某些地址是用于内网的,这些地址叫作私有地址,而原来的固定地址则叫作公有地址

1
2
3
10.0.0.010.255.255.255
172.16.0.0172.31.255.255
192.168.0.0192.168.255.255

图3.17 私有地址和公有地址分别管理

3.4.2 地址转换的基本原理

地址转换的基本原理是在转发网络包时对IP头部中的IP地址和端口号进行改写。具体的过程我们来看一个实际的例子,假设现在要访问Web服务器,看看包是如何传输的。

首先,TCP连接操作的第一个包被转发到互联网时,会像图3.18这样,将发送方IP地址从私有地址改写成公有地址。这里使用的公有地址是地址转换设备的互联网接入端口的地址。与此同时,端口号也需要进行改写,地址转换设备会随机选择一个空闲的端口。然后,改写前的私有地址和端口号,以及改写后的公有地址和端口号,会作为一组相对应的记录保存在地址转换设备内部的一张表中。

图3.18 利用端口号改写IP地址

在对外只能使用一个公有地址的情况下,可以用不同的端口号来区别内网中的不同终端。

改写发送方IP地址和端口号之后,包就被发往互联网,最终到达服务器,然后服务器会返回一个包。服务器返回的包的接收包是原始包的发送方,因此返回的包的接收方就是改写后的公有地址和端口号。这个公有地址其实是地址转换设备的地址,因此这个返回包就会到达地址转换设备。

接下来,地址转换设备会从地址对应表中通过公有地址和端口号找到相对应的私有地址和端口号,并改写接收方信息,然后将包发给公司内网,这样包就能够到达原始的发送方了。

在后面的包收发过程中,地址转换设备需要根据对应表查找私有地址和公有地址的对应关系,再改写地址和端口号之后进行转发。当数据收发结束,进入断开阶段,访问互联网的操作全部完成后,对应表中的记录就会被删除。

通过这样的机制,具有私有地址的设备就也可以访问互联网了。从互联网一端来看,实际的通信对象是地址转换设备(这里指的是路由器)。

3.4.3 改写端口号的原因

现在我们使用的地址转换机制是同时改写地址和端口号的,但早期的地址转换机制是只改写地址,不改写端口号的。用这种方法也可以让公司内网和互联网进行通信,而且这种方法更简单。

但是,使用这种方法的前提是私有地址和公有地址必须一一对应,也就是说,有多少台设备要上互联网,就需要多少个公有地址。当然,访问动作结束后可以删除对应表中的记录,这时同一个公有地址可以分配给其他设备使用,因此只要让公有地址的数量等于同时访问互联网的设备数量就可以了。然而公司人数一多,同时访问互联网的人数也会增加。一个几千人的公司里,有几百人同时访问互联网是很正常的,这样就需要几百个公有地址。

改写端口号正是为了解决这个问题。客户端一方的端口号本来就是从空闲端口中随机选择的,因此改写了也不会有问题。端口号是一个16比特的数值,总共可以分配出几万个端口,因此如果用公有地址加上端口的组合对应一个私有地址,一个公有地址就可以对应几万个私有地址,这种方法提高了公有地址的利用率。

3.4.4 从互联网访问公司内网

对于从公司内网访问互联网的包,即便其发送方私有地址和端口号没有保存在对应表中也是可以正常转发的,因为用来改写的公有地址就是地址转换设备自身的地址,而端口号只要随便选一个空闲的端口就可以了,这些都可以由地址转换设备自行判断。然而,对于从互联网访问公司内网的包,如果在对应表中没有记录就无法正常转发。因为如果对应表中没有记录,就意味着地址转换设备无法判断公有地址与私有地址之间的对应关系。

换个角度来看,这意味着对于没有在访问互联网的内网设备,是无法从互联网向其发送网络包的。而且即便是正在访问的设备,也只能向和互联网通信中使用的那个端口发送网络包,无法向其他端口发送包。也就是说,除非公司主动允许,否则是无法从互联网向公司内网发送网络包的。这种机制具有防止非法入侵的效果。

不过,有时候我们希望能够从互联网访问公司内网,这需要进行一些设置才能实现。之所以无法从互联网访问内网,是因为对应表里没有相应的记录,那么我们只要事先手动添加这样的记录就可以了(图3.19)。一般来说,用于外网访问的服务器可以放在地址转换设备的外面并为它分配一个公有地址,也可以将服务器的私有地址手动添加到地址转换设备中,这样就可以从互联网访问到这台具有私有地址的服务器。

图3.19 从互联网访问公司内网

只要事先将地址和端口的关联信息添加到地址转换设备的对应表中,就可以从互联网访问内网中的设备了。

3.4.5 路由器的包过滤功能

下面来介绍一下包过滤功能。包过滤也是路由器的一个重要附加功能,刚才的地址转换看起来有点复杂,不过包过滤的机制并不复杂。包过滤就是在对包进行转发时,根据MAC头部、IP头部、TCP头部的内容,按照事先设置好的规则决定是转发这个包,还是丢弃这个包。我们通常说的防火墙设备或软件,大多数都是利用这一机制来防止非法入侵的。

包过滤的原理非常简单,但要想设置一套恰当的规则来区分非法访问和正常访问,只阻止非法入侵而不影响正常访问,是非常不容易的。举个例子,为了防止从互联网非法入侵内网,我们可以将来自互联网的所有包都屏蔽掉,但是这会造成什么结果呢?正如我们第2章介绍过的TCP的工作过程一样,网络包是双向传输的,如果简单地阻止来自互联网的全部包,那么从内网访问互联网的操作也会无法正常进行。

问题

1.局域网中使用的双绞线中为什么要将信号线缠绕在一起?

—> 为了抑制噪声的影响

2.将输入的信号广播到所有端口上的设备是交换机还是集线器?

—> 集线器

3.用来指定网络号和主机号比特数的值叫什么?

—> 子网掩码

4.将大网络包进行拆分的功能叫什么?

—> 分片

5.路由器的路由表中有时可以看到子网掩码为0.0.0.0的记录,这代表什么意思?

—> 默认路由

4、通过接入网进入互联网内部 — 探索接入网和网络运营商

热身问题

1、光纤的通信速率之所以更快,是因为光信号的传播速度比电信号要快。

—> ×。电信号和光信号传播的速度大体上相同,之所以电缆不如光纤通信速率高,是因为电信号在提升通信速率的同时,其衰减率也会提高(信号在传播过程中减弱),导致信号无法传到目的地。相对地,光信号本来的衰减率就很低,提高通信速率也并不会提高衰减率。此外,光纤还不受电磁噪声的影响,因此光纤能够进行高速通信。

上一章,我们探索了从客户端计算机发送的网络包通过家庭和公司局域网中的集线器和路由器前往目的地的过程。本章,我们来看一看网络包是如何通过互联网接入路由器,最终进入互联网内部的。

(1) ADSL接入网的结构和工作方式

家庭和公司的内网是通过接入网连接到网络运营商的。接入网有很多类型,这里我们将介绍ADSL接入网的知识,重点包括ADSL接入网的结构、电话线中传输的信号以及与电话共用的方式。

(2) 光纤接入网(FTTH)

我们还会介绍另一种常用的接入网技术——与ADSL技术的利用率不相上下的光纤技术,重点包括光纤结构、单模和多模的区别之类的光纤性质,以及光纤用作接入网时的工作方式。

(3) 接入网中使用的PPP和隧道

接入网需要通过用户名和密码验证用户的身份,然后由网络运营商向用户分配公有地址。此外,从接入网向网络运营商传输网络包时还使用了隧道技术,这些都是本章的看点。

(4) 网络运营商的内部

接入网后面连接着网络运营商的网络,运营商网络也是以路由器为核心组成的,这一点和家庭、公司网络是一样的,包转发的工作原理也没有区别。不过,运营商网络也使用了一些和家庭、公司网络不同的技术,比如运营商之间可以自动交换路由信息和更新路由表,这些都是本章的看点。

(5) 跨越运营商的网络包

互联网是由多个运营商网络相互连接形成的巨大网络,而多个运营商之间相互连接的部分可以说就是互联网的核心部分,这里也是本章的看点。

4.1 ADSL接入网的结构和工作方式

4.1.1 互联网的基本结构和家庭、公司网络是相同的

互联网是一个遍布世界的巨大而复杂的系统,但其基本工作方式却出奇地简单。和家庭、公司网络一样,互联网也是通过路由器来转发包的,而且路由器的基本结构和工作方式也并没有什么不同(图4.1)​。因此,我们可以将互联网理解为家庭、公司网络的一个放大版。

4.1.2 连接用户与互联网的接入网

上一章已经讲过,网络包通过交换机和路由器的转发一步一步地接近它的目的地,在通过互联网接入路由器之后,就进入了互联网。本章的探索之旅就从这里开始。

刚才讲过,路由器的转发操作都是相同的,因此互联网接入路由器的包转发操作也和第3章讲过的以太网路由器几乎是一样的。简单来说,就是根据包IP头部中的接收方IP地址在路由表的目标地址中进行匹配,找到相应的路由记录后将包转发到这条路由的目标网关。不过,互联网接入路由器发送网络包的操作和以太网路由器有一点不同,互联网接入路由器是按照接入网规则来发送包的。

所谓接入网,就是指连接互联网与家庭、公司网络的通信线路。一般家用的接入网方式包括ADSL、FTTH、CATV、电话线、ISDN等,公司则还可能使用专线。接入网的线路有很多种类,我们无法探索所有这些线路,因此下面先介绍一个比较有代表性的例子——ADSL。

4.1.3 ADSL Modem将包拆分成信元

待补充…

4.1.4 ADSL将信元“调制”成信号

待补充…

4.1.5 ADSL通过使用多个波来提高速率

待补充…

4.1.6 分离器的作用

待补充…

4.1.7 从用户到电话局

待补充…

4.1.8 噪声的干扰

待补充…

4.1.9 通过DSLAM到达BAS

待补充…

4.2 光纤接入网(FTTH)

4.2.1 光纤的基本知识

通过ADSL接入网和BAS之后,网络包就到达了互联网内部,在继续探索之前,我们再来介绍另一种接入网技术,它的名字叫FTTH,是一种基于光纤的接入网技术。FTTH的关键点在于对光纤的使用,所以我们先来介绍一些光纤的基本知识。

4.2.2 单模与多模

光纤通信的关键技术就是能够传导光信号的光纤。光在透明材质中传导似乎听起来很简单,但实际上光的传导方式是非常复杂的,不同材质的光纤其透光率和折射率也不同,纤芯的直径等因素也会影响光的传导。其中,纤芯的直径对光的传导影响很大,要理解这一点,我们得先来看看光在光纤中是如何传导的。

4.2.3 通过光纤分路来降低成本

用光纤来代替ADSL将用户端接入路由器和运营商的BAS连接起来的接入方式就是FTTH[插图],从形态上可大致分为两种。

一种是用一根光纤直接从用户端连接到最近的电话局(图4.16(a))​。这种类型的FTTH中,用户和电话局之间通过光纤直接连接,网络包的传输方式如下。首先,用户端的光纤收发器将以太网的电信号转换成光信号。这一步只进行电信号到光信号的转换,而不会像ADSL一样还需要将包拆分成信元,大家可以认为是将以太网包原原本本地转换成了光信号。接下来,光信号通过连接到光纤收发器的光纤直接到达BAS前面的多路光纤收发器。FTTH一般使用单模光纤,因此其纤芯中只有特定角度的光信号能够反射并前进。然后,多路光纤收发器将光信号转换成电信号,BAS的端口接收之后,将包转发到互联网内部。

把网络包发送到互联网之后,服务器会收到响应,响应包的光信号也是沿着同一条光纤传输到用户端的。这里,前往互联网的上行光信号和前往用户的下行光信号在光纤中混合在一起,信号会变得无法识别,因此我们需要对它们进行区分,办法是上行和下行信号采用不同波长的光。波长不同的光混合后可通过棱镜原理进行分离,因此光纤中的上行和下行信号即便混合起来也可以识别。像这样在一条光纤中使用不同的波长传输多个光信号的方式叫作波分复用。

另一种光纤的接入方式是在用户附近的电线杆上安装一个名为分光器的设备,通过这个设备让光纤分路,同时连接多个用户​。

图4.16 FTTH接入网的结构

4.3 接入网中使用ppp和隧道

4.3.1 用户认证和配置下发

刚才已经简单讲过,用户发送的网络包会通过ADSL和FTTH等接入网到达运营商的BAS。

互联网本来就是由很多台路由器相互连接组成的,因此原则上应该是将接入网连接到路由器上。随着接入网发展到ADSL和FTTH,接入网连接的路由器也跟着演进,而这种进化型的路由器就叫作BAS。下面我们来具体讲一讲。

首先是用户认证和配置下发功能。ADSL和FTTH接入网中,都需要先输入用户名和密码,登录之后才能访问互联网,而BAS就是登录操作的窗口。BAS使用PPPoE方式来实现这个功能。PPPoE是由传统电话拨号上网上使用的PPP协议发展而来的,所以我们先来看一看PPP拨号上网的工作方式。

4.3.2 在以太网上传输PPP消息

ADSL和FTTH接入方式也需要为计算机分配公有地址才能上网,这一点和拨号上网是相同的。不过,ADSL和FTTH中,用户和BAS之间是通过电缆或光纤固定连接在一起的,因此没有必要验证用户身份,所以实际上并不需要PPP的所有这些功能。然而,通过用户名和密码登录的步骤可以根据用户名来切换不同的运营商,这很方便。因此,接入运营商在ADSL和FTTH中一般也会使用PPP。

4.3.3 通过隧道将网络包发送给运营商

BAS除了作为用户认证的窗口之外,还可以使用隧道方式来传输网络包。所谓隧道,就类似于套接字之间建立的TCP连接。在TCP连接中,我们从一侧的出口(套接字)放入数据,数据就会原封不动地从另一个出口出来,隧道也是如此。也就是说,我们将包含头部在内的整个包从隧道的一头扔进去,这个包就会原封不动地从隧道的另一头出来,就好像在网络中挖了一条地道,网络包从这个地道里穿过去一样。

像这样,如果在BAS和运营商路由器之间的ADSL/FTTH接入服务商的网络中建立一条隧道,将用户到BAS的接入网连接起来,就形成了一条从用户一直到运营商路由器的通道,网络包通过这条通道,就可以进入互联网内部了,这样的机制就类似于将接入网一直延伸到运营商路由器。

隧道有几种实现方式,刚才提到的TCP连接就是其中一种实现方式(图4.19(a))​。这种方式中,首先需要在网络上的两台隧道路由器之间建立TCP连接,然后将连接两端的套接字当作是路由器的端口,并从这个端口来收发数据。换句话说,在路由器收发包时,是基于隧道的规则向隧道中放入或取出网络包,这时,TCP连接就好像变成了一根网线,包从这里穿过到达另一端。

图4.19(b)中还介绍了另一种基于封装(encapsulation)的隧道实现方式,这种方式是将包含头部在内的整个包装入另一个包中传输到隧道的另一端。在这种方式中,包本身可以原封不动地到达另一端的出口,从结果上看和基于TCP连接的方式是一样的,都实现了一个可供包进行穿梭的通道。

通过前面的介绍大家可以发现,无论任何机制,只要能够将包原封不动搬运到另一端,从原理上看就都可以用来建立隧道。

图4.19 隧道的结构

4.3.4 接入网的整体工作过程

理解了PPPoE和隧道的原理之后,下面来看看接入网的整体工作过程。接入网的工作从用户端的互联网接入路由器进行连接操作开始。首先,接入路由器中需要配置运营商分配的用户名和密码。然后,接入路由器会根据PPPoE的发现机制来寻找BAS。这一机制和ARP一样是基于广播来实现的,过程如下,很简单。

用户询问:​“BAS在不在?在的话请报告MAC地址

BAS回答:​“我在这里,我的MAC地址是xx:xx:xx:xx:xx:xx。​”

这样用户端就知道了BAS的MAC地址,也就可以和BAS进行通信了。

1
互联网接入路由器通过PPPoE的发现机制查询BAS的MAC地址。
1
BAS下发的TCP/IP参数会被配置到互联网接入路由器的BAS端的端口上,这样路由器就完成接入互联网的准备了。
1
BAS在收到用户路由器发送的网络包之后,会去掉MAC头部和PPPoE头部,然后用隧道机制将包发送给网络运营商的路由器。

4.3.5 不分配IP地址的无编号端口

前面介绍了PPPoE的工作过程,这里面有一个有趣的问题,就是互联网接入路由器在发送包的时候为什么要加上那些头部呢?头部里面的值基本上都是事先定好的,跟路由表里面的默认网关地址根本没什么关系。当采用一对一连接,也就是两台路由器的端口用一根线直接连起来的情况下,一端发送的包肯定会到达另一端,那么这种情况下就没有必要按照路由表查询默认网关来判断转发目标地址了。如果没有必要判断转发地址,那么网关的地址也就没什么用了;如果网关地址没用,那么目标路由器的端口也用不着分配IP地址了。上面的性质对于所有一对一连接都是适用的。

以前,即便是在这样的场景中,还是会为每个端口分配IP地址,这是因为有一条规则规定所有的端口都必须具有IP地址。然而,当公有地址越来越少时,就提出了一个特例,即一对一连接的端口可以不分配IP地址。现在,在这种场景中按惯例都是不为端口分配IP地址的,这种方式称为无编号(unnumbered)。这种情况下,BAS下发配置信息时就不会下发默认网关的IP地址。

1
一对一连接的端口可以不分配IP地址,这种方式称为无编号。

4.3.6 互联网接入路由器将私有地址转换成公有地址

前面的介绍里面其实遗漏了一个地方,那就是互联网接入路由器在转发包时需要进行地址转换。刚才我们讲过,BAS会向用户端下发TCP/IP的配置信息,如果将这些信息配置在计算机上,就相当于计算机拥有了公有地址,这种情况下不需要进行地址转换也可以访问互联网。其实TCP/IP原本的设计就是这样的。然而,如果使用路由器来上网,BAS下发的参数就会被配置在路由器上,而且公有地址也是分配给路由器的。这样一来,计算机就没有公有地址了。

这时,计算机会被分配一个私有地址,计算机发送的包需要通过路由器进行地址转换然后再转发到互联网中。Web和电子邮件等应用程序不会受到地址转换的影响,但有些应用程序会因为地址转换无法正常工作,这一点需要大家注意。这是因为有些应用程序需要将自己的IP地址告知通信对象或者告知控制服务器,但在有地址转换的情况下这些操作无法完成。

遇到应用程序因地址转换无法正常工作的情况时,我们可以不使用路由器,而是直接让计算机接收来自BAS的PPPoE消息,也就是采用最原始的上网方法。这样一来,计算机就具有了公有地址,不需要地址转换也可以上网了。

不过,不用路由器上网也有一点需要注意,因为上网的计算机拥有公有地址,这意味着来自互联网的包可以直接到达计算机,这可能导致计算机被攻击。因此,对于直接上网的客户端计算机,我们应该采取安装防火墙软件等防御手段。

4.3.7 除PPPoE之外的其他方式

1
PPPoA方式不添加MAC头部和PPPoE头部,而是直接将包装入信元中。

由于PPPoA没有MAC头部,所以PPP消息是无法通过以太网来传输的,这就意味着需要和BAS收发PPP消息的设备,也就是计算机和路由器,必须和ADSL Modem是一体的,否则PPP机制就无法工作了。这个一体化的方式主要有以下两种。

第一种是将计算机和ADSL Modem用USB接口连接起来,这样ADSL Modem就和计算机成为一体了。不过,这种方式最终并没有普及。另一种方式是像图4.21所示的这样,将ADSL Modem和路由器整合成一台设备。这种方式和PPPoE中使用路由器上网的方式基本没什么区别,因此得到了广泛的普及。不过,正如我们刚才提到的,当由于地址转换产生问题时,这种方式就不容易处理了,因为我们无法抛开路由器用计算机直接上网。

图4.21 用ATM信元装载PPP消息的PPPoA

DHCP经常用于通过公司网络向客户端计算机下发TCP/IP配置信息,其原理如图4.22所示,首先客户端请求配置信息(图4.22①)​,然后DHCP服务器下发配置信息(图4.22②)​,非常简单,不需要像PPP(图4.17)那样需要多个步骤,也不需要验证用户名和密码。没有用户名和密码,就意味着无法通过用户名来切换运营商网络,但这种方式也有优势,它可以单纯地直接传输以太网包,不需要添加额外的PPP头部,因此不会占用MTU。

图4.22 DHCP的原理

此外,采用DHCP的运营商使用的ADSL Modem也和PPPoE、PPPoA方式不同,这种ADSL Modem不使用信元,而是直接将以太网包调制成ADSL信号,因此没有ADSL Modem和路由器无法分离的问题。

1
还有一种DHCP方式,它不使用PPP,而是将以太网包直接转换成ADSL信号发送给DSLAM。

4.4 网络运营商的内部

4.4.1 POP和NOC

下面回到正题,现在网络包已经通过接入网,到达了网络运营商的路由器。这里是互联网的入口,网络包会从这里进入互联网内部。

互联网的实体并不是由一个组织运营管理的单一网络,而是由多个运营商网络相互连接组成的(图4.23)​。ADSL、FTTH等接入网是与用户签约的运营商设备相连的,这些设备称为POP,互联网的入口就位于这里。

图4.23 互联网内部概览

1
网络包通过接入网之后,到达运营商POP的路由器。

那么,POP里面是什么样的呢?POP的结构根据接入网类型以及运营商的业务类型不同而不同,大体上是图4.24中的这个样子。POP中包括各种类型的路由器,路由器的基本工作方式是相同的,但根据其角色分成了不同的类型。图4.24中,中间部分列出了连接各种接入网的路由器,这里的意思就是根据接入网的类型需要分别使用不同类型的路由器。

图4.24 POP概览

4.4.2 室外通信线路的连接

POP和NOC遍布全国各地,它们各自的规模有大有小,但看起来跟公司里的机房没什么太大区别,都是位于一幢建筑物中的,其中的路由器或者通过线路直接连接,或者通过交换机进行连接,这些和公司以及家庭网络都是相同的。只不过,公司的机房一般使用双绞线来连接设备,但运营商的网络中需要传输大量的包,已经超过了双绞线能容纳的极限,因此一般还是更多地使用光纤。

4.5 跨越运营商的网络包

4.5.1 运营商之间的连接

让我们重新回到运营商内部,看一看到达POP路由器之后,网络包是如何前往下一站的。首先,如果最终目的地Web服务器和客户端是连接在同一个运营商中的,那么POP路由器的路由表中应该有相应的转发目标。运营商的路由器可以和其他路由器交换路由信息,从而自动更新自己的路由表,通过这一功能,路由信息就实现了自动化管理。于是,路由器根据路由表中的信息判断转发目标,这个转发目标可能是NOC,也可能是相邻的POP,无论如何,路由器都会把包转发出去,然后下一个路由器也同样根据自己路由表中的信息继续转发。经过几次转发之后,网络包就到达了Web服务器所在的POP的路由器,然后从这里被继续转发到Web服务器。

那么,如果服务器的运营商和客户端的运营商不同又会怎样呢?这种情况下,网络包需要先发到服务器所在的运营商,这些信息也可以在路由表中找到,这是因为运营商的路由器和其他运营商的路由器也在交换路由信息。这个信息交换的过程稍后再讲,我们暂且认为路由表中能找到对方运营商的路由信息,这时网络包会被转发到对方运营商的路由器。

总之,对于互联网内部的路由器来说,无论最终目的地是否属于同一家运营商,都可以从路由表中查到,因此只要一次接一次按照路由表中的目标地址来转发包,最终一定可以到达Web服务器所在的POP。这样一来,我们就可以把包发到任何地方,包括地球的另一面。

4.5.2 运营商之间的路由信息交换

只要路由表中能够查到,我们当然可以把包发到任何地方,包括地球的另一面,但这些路由信息是如何写入路由表的呢?如果路由表中没有相应的路由信息,路由器就无法判断某个网络的位置,也就无法对包进行转发,也就是说,仅仅用线路将路由器连起来,是无法完成包转发的。下面我们来看看运营商之间是如何交换路由信息,并对路由器进行自动更新的。

其实方法并不难。如图4.25所示,只要让相连的路由器告知路由信息就可以了。只要获得了对方的路由信息,就可以知道对方路由器连接的所有网络,将这些信息写入自己的路由表中,也就可以向那些网络发送包了。

图4.25 运营商之间的路由信息交换

获得对方的路由信息之后,我们也需要将自身的路由信息告知对方。这样一来,对方也可以将发往我们所在子网的包转发过来。这个路由信息交换的过程是由路由器自动完成的,这里使用的机制称为BGP

根据所告知的路由信息的内容,这种路由交换可分为两类。一类是将互联网中的路由全部告知对方。例如图4.26中,如果运营商D将互联网上所有路由都告知运营商E,则运营商E不但可以访问运营商D,还可以访问运营商D后面的运营商B、A和C。然后,通过运营商D就可以向所有的运营商发送包。像这样,通过运营商D来发送网络包的方式称为转接。

另一种类型是两个运营商之间仅将与各自网络相关的路由信息告知对方。这样,只有双方之间的网络可以互相收发网络包,这种方式称为非转接,也叫对等

1
互联网内部使用BGP机制在运营商之间交换路由信息。

4.5.3 与公司网络中自动更新路由表机制的区别

路由器之间相互交换信息自动更新路由表的方式在公司网络中也会用到,不过公司内部和运营商之间在路由交换方式上是有区别的。

公司中使用的方式是寻找与目的地之间的最短路由,并按照最短路由来转发包,因此,周围的所有路由器都是平等对待的。

公司内部采用这样的方式没问题,但运营商之间就不行了。假设某个运营商拥有一条连接日本和美国的高速线路,那么要访问美国的地址时,可能这条线路是最短路由。如果单纯采用最短路由的方式,那么其他运营商的包就都会走这条线路,这时,该运营商需要向其他运营商收取相应的费用,否则就成义务劳动了。在这种情况下,如果使用最短路由的方式,就无法区分哪个运营商交了费,哪个运营商没交费,也就是说无法阻止那些没交费的运营商使用这条线路,这样就很难和对方进行交涉了。

正是出于这样的原因,互联网中不能单纯采用最短路由,而是需要一种能够阻止某些来源的网络包的机制,互联网的路由交换机制就具有这样的功能。

首先,互联网中可以指定路由交换的对象。公司中,路由信息是在所有路由器间平等交换的,但运营商之间的路由交换是在特定路由器间一对一进行的。这样一来,运营商就可以只将路由信息提供给那些交了费的运营商,那些没交费的运营商也就无法将网络包发送过来了。

其次,在判断路由时,该机制不仅可以判断是否是最短路由,还可以设置其他一些判断因素。例如当某个目的地有多条路由时,可以对每条路由设置优先级。

4.5.4 IX的必要性

图4.26中有一个叫作IX的东西,我们来说说它是干什么用的。对于两个运营商来说,像图4.26中运营商D和运营商C这样一对一的连接是最基本的一种连接方式,现在也会使用这种方式。但这种方式有个不方便的地方,如果运营商之间只能一对一连接,那么就需要像图4.27(a)这样将所有的运营商都用通信线路连接起来。现在光日本国内就有数千家运营商,这样连接非常困难。对于这种情况,我们可以采用图4.27(b)的方式,设置一个中心设备,通过连接到中心设备的方式来减少线路数量,这个中心设备就称为IX。

现在日本国内有几个这样的设备,其中具有代表性的包括JPIX、NSPIXP-2、JPNAP。经过这3个IX的数据总量约为200 Gbit/s,而且还在持续增加。

4.5.5 运营商如何通过IX互相连接

下面我们来探索一下IX。首先是IX的部署场所。为了保证在遇到停电、火灾等事故,以及地震等自然灾害时,路由器等网络设备还能继续工作,IX所在的大楼都装有自主发电设备,并具有一定的抗震能力。其实这样的要求也不仅限于IX,运营商的NOC也是一样。现在在日本,拥有如此高安全性的大楼其实并不多,因此符合这样要求的大楼里面都可能会有NOC和IX。运营商和IX运营机构会租下大楼中的一块地方用于放置NOC和IX的设备,换句话说,IX就在这些大楼中某一层的某个角落中。

IX的核心是具有大量高速以太网端口的二层交换机(图4.28)。二层交换机的基本原理和一般交换机相同,大家可以认为IX的核心就是大型的、高速的交换机。

接下来就是将各个运营商的路由器连接到IX核心交换机上,连接方法有几种。首先,当运营商NOC和IX位于同一幢大楼里时,只要从NOC中将光纤延长出来接到IX交换机就可以了(图4.28①)​。这种情况和公司、家庭网络中的路由器与交换机的连接方法是相同的。这种方法很简单,但如果NOC和IX不在同一幢大楼里又该怎么办呢?我们可以用通信线路将路由器和交换机连起来。这种情况下有两种连法,一种是从路由器延伸出一根通信线路并连接到IX交换机上(图4.28②)​,另一种是将路由器搬到IX机房里,用通信线路将路由器和NOC连起来,再将路由器连到IX交换机上(图4.28③)​。

以前IX交换机都是放在一个地方的,也就是呈点状分布的。现在这些点状设施已经逐步扩张,在数据中心等网络流量集中的地方一般都会设置IX终端交换机,各运营商的路由器在这里连接到终端交换机上(图4.28④)​。IX已经从点扩张到线,甚至到面了。

下面我们来看一看网络包具体是如何传输的。其实这里并没有什么特别需要解释的,因为IX的交换机和一般的交换机在工作方式上没有区别,路由器发送网络包时,先通过ARP查询下一个路由器的MAC地址,然后将其写入MAC头部发送出去即可。只要填写了正确的MAC地址,就可以向任何运营商的路由器发送包。不过实际上,要成功发送包还需要正确的路由信息,对于没有进行路由交换的运营商,我们是无法向其发送包的。这需要运营商之间通过谈判签订合约,然后按照合约来交换路由信息,实现网络包的收发。

运营商之间可以直接连接,也可以通过IX连接,无论是哪种方式,最终网络包都会到达服务器所在的运营商,然后通过POP进入服务器端的网络。后面的内容我们下一章继续讲。

图4.28 IX的实体是高性能交换机

问题

1.什么是接入网? —> 用于连接网络运营商的线路

2.要使用ADSL服务,需要安装一个将电话信号和ADSL信号分开的设备,这个设备叫什么名字? —> 分离器

3.和电话局距离越远,ADSL的通信速率越低,为什么? —> 因为离电话局越远,信号越弱

4.BAS(宽带接入服务器)与一般的路由器有什么不同? —-> BAS具有身份认证、向客户端下发IP地址等配置信息的功能

5.将多个运营商汇聚在一起相互连接的设备叫什么? —-> IX(Internet eXchange,互联网交换中心)​

5、服务器端的局域网中有什么玄机

问题

1.当使用浏览器访问Web服务器时,浏览器的通信对象不仅限于Web服务器。

—- √。浏览器有时候是和Web服务器通信,有时候是和缓存服务器以及负载均衡器等进行通信。

2.没有防火墙就不能连接到互联网。

—– ×。防火墙并不是必需的,但是没有防火墙会增加风险。

3.也有防火墙无法抵御的攻击。

—– √。防火墙不会检查通信数据的具体内容,因此无法抵御隐藏在通信数据内容中的攻击。

上一章,我们探索了网络包在进入互联网之后,通过通信线路和运营商网络到达服务器POP端的过程。接下来,网络包将继续朝服务器前进,并通过服务器前面的防火墙、缓存服务器、负载均衡器等。本章我们将对这一部分进行探索。

(1)Web服务器的部署地点

客户端计算机一般都放在家庭、公司网络上,但服务器的部署不仅限于家庭和公司中。那么服务器到底放在哪里呢?这是我们的第一个看点。

(2)防火墙的结构和原理

一般在Web服务器前面都会部署防火墙,那么防火墙是通过怎样的机制保护服务器的呢?这是我们的第二个看点。

(3)通过将请求平均分配给多台服务器来平衡负载

随着访问量的增加,Web服务器的处理能力会不够用,对于访问量很大的大型网站来说,必须要考虑到这一点。如何应对这个问题,也是我们的看点之一。有很多方案可以应对这个问题,我们先介绍其中一种方法,即通过多台Web服务器来分担负载。

(4)利用缓存服务器分担负载

另一种减轻Web服务器负担的方法是将访问过的数据保存在缓存服务器中,当再次访问时直接使用缓存的数据。除了在服务器端部署缓存服务器之外,在客户端也可以部署缓存服务器,缓存服务器有各种用法,这也是我们的看点之一。

(5)内容分发服务

内容分发服务是从缓存服务器发展而来的,它在互联网中部署很多缓存服务器,并将用户的访问引导到最近的缓存服务器上。那么如何才能找到离用户最近的缓存服务器呢?如何将用户的访问引导到这台服务器上呢?内容分发服务的结构还是非常耐人寻味的。

5.1 Web服务器的部署地点

5.1.1 在公司里部署Web服务器

网络包从互联网到达服务器的过程,根据服务器部署地点的不同而不同。最简单的是图5.1(a)中的这种情况,服务器直接部署在公司网络上,并且可以从互联网直接访问。这种情况下,网络包通过最近的POP中的路由器、接入网以及服务器端路由器之后,就直接到达了服务器。其中,路由器的包转发操作,以及接入网和局域网中包的传输过程都和我们之前讲过的内容没有区别。

以前这样的服务器部署方式很常见,但现在已经不是主流方式了。这里有几个原因。

第一个原因是IP地址不足。这样的方式需要为公司网络中的所有设备,包括服务器和客户端计算机,都分配各自的公有地址。然而现在公有地址已经不够用了,因此采用这种方式已经不现实了。

另一个原因是安全问题。这种方式中,从互联网传来的网络包会无节制地进入服务器,这意味着服务器在攻击者看来处于“裸奔”状态。当然,我们可以强化服务器本身的防御来抵挡攻击,这样可以一定程度上降低风险。但是,任何设置失误都会产生安全漏洞,而裸奔状态的服务器,其安全漏洞也都会暴露出来。人工方式总会出错,安全漏洞很难完全消除,因此让服务器裸奔并不是一个稳妥的办法。

图5.1 服务器的所在地

5.1.2 将Web服务器部署在数据中心

如果Web服务器部署在数据中心里,那么网络包会从互联网核心部分直接进入数据中心,然后到达服务器。如果数据中心有防火墙,则网络包会先接受防火墙的检查,放行之后再到达服务器。无论如何,网络包通过路由器的层层转发,最终到达服务器的这个过程都是相同的。

5.2 防火墙的结构和原理

5.2.1 主流的包过滤方式

无论服务器部署在哪里,现在一般都会在前面部署一个防火墙,如果包无法通过防火墙,就无法到达服务器。因此,让我们先来探索一下包是如何通过防火墙的。

5.2.2 如何设置包过滤的规则

表5.1 地址转换和包过滤中用于设置规则的字段

图5.2 包过滤的典型示例

5.2.3 通过端口号限定应用程序

不过,按照前面的设置,相当于允许了互联网和Web服务器之间所有的包通过,这个状态很危险。假如服务器上还有一个文件服务器程序在工作,那么这些文件就可能会被非法访问从而造成信息泄露。有风险的还不仅是文件服务器,现在每天都会发布若干安全漏洞,可以说随处都隐藏着风险。因此,我们最好是阻止除了必需服务(也就是本例中的Web服务)以外的所有应用程序的包。

当我们要限定某个应用程序时,可以在判断条件中加上TCP头部或者UDP头部中的端口号。Web服务器的端口号为80,因此我们在刚才的接收方IP地址和发送方IP地址的基础上再加上80端口作为条件就可以了。也就是说,当包的接收方IP地址为Web服务器地址,且接收方端口号为80时,允许这些包通过(图5.2中表的第1行)​;或者当包的发送方IP地址为Web服务器地址,且发送方端口号为80时,允许这些包通过(图5.2中的表的第3行)​。如果要允许访问除Web之外的其他应用程序,则只要将该应用程序的端口号设置到防火墙中并允许通过就可以了。

5.2.4 通过控制位判断连接方向

5.2.5 从公司内网访问公开区域的规则

5.2.6 从外部无法访问公司内网

5.2.7 通过防火墙

1
包过滤方式的防火墙可根据接收方IP地址、发送方IP地址、接收方端口号、发送方端口号、控制位等信息来判断是否允许某个包通过。

5.2.8 防火墙无法抵御的攻击

防火墙可以根据包的起点和终点来判断是否允许其通过,但仅凭起点和终点并不能筛选出所有有风险的包。比如,假设Web服务器在收到含有特定数据的包时会引起宕机。但是防火墙只关心包的起点和终点,因此即便包中含有特定数据,防火墙也无法发现,于是包就被放行了。然后,当包到达Web服务器时,就会引发服务器宕机。通过这个例子大家可以看出,只有检查包的内容才能识别这种风险,因此防火墙对这种情况无能为力。

5.3 通过将请求平均分配给多台服务器平衡负载

5.3.1 性能不足时需要负载均衡

当服务器的访问量上升时,增加服务器线路的带宽是有效的,但并不是网络变快了就可以解决所有的问题。高速线路会传输大量的网络包,这会导致服务器的性能跟不上。尤其是通过CGI等应用程序动态生成数据的情况下,对服务器CPU的负担更重,服务器性能的问题也会表现得越明显。

要解决这个问题,大家可能首先想到的是换一台性能更好的服务器,但当很多用户同时访问时,无论服务器的性能再好,仅靠一台服务器还是难以胜任的。在这种情况下,使用多台服务器来分担负载的方法更有效。这种架构统称为分布式架构,其中对于负载的分担有几种方法,最简单的一种方法就是采用多台Web服务器,减少每台服务器的访问量。假设现在我们有3台服务器,那么每台服务器的访问量会减少到三分之一,负载也就减轻了。要采用这样的方法,必须有一个机制将客户端发送的请求分配到每台服务器上。具体的做法有很多种,最简单的一种是通过DNS服务器来分配。当访问服务器时,客户端需要先向DNS服务器查询服务器的IP地址,如果在DNS服务器中填写多个名称相同的记录,则每次查询时DNS服务器都会按顺序返回不同的IP地址。例如,对于域名www.lab.glasscom.com,如果我们给它分配如下3个IP地址。

当第1次查询这个域名时,服务器会返回如下内容。

1
192.0.2.60  192.0.2.70  192.0.2.80

当第2次查询时,服务器会返回如下内容。

1
192.0.2.70  192.0.2.80  192.0.2.60

当第3次查询时,服务器会返回如下内容。

1
192.0.2.80  192.0.2.60  192.0.2.70

当第4次查询时就又回到第1次查询的结果(图5.3)​。这种方式称为轮询(round-robin),通过这种方式可以将访问平均分配给所有的服务器。

图5.3 DNS轮询

但这种方式是有缺点的。假如多台Web服务器中有一台出现了故障,这时我们希望在返回IP地址时能够跳过故障的Web服务器,然而普通的DNS服务器并不能确认Web服务器是否正常工作,因此即便Web服务器宕机了,它依然可能会返回这台服务器的IP地址.

此外,轮询分配还可能会引发一些问题。在通过CGI等方式动态生成网页的情况下,有些操作是要跨多个页面的,如果这期间访问的服务器发生了变化,这个操作就可能无法继续。例如在购物网站中,可能会在第一个页面中输入地址和姓名,在第二个页面中输入信用卡号,这就属于刚才说的那种情况。

5.3.2 使用负载均衡器分配访问

为了避免出现前面的问题,可以使用一种叫作负载均衡器的设备。使用负载均衡器时,首先要用负载均衡器的IP地址代替Web服务器的实际地址注册到DNS服务器上。假设有一个域名www.lab.glasscom.com,我们将这个域名对应的IP地址设置为负载均衡器的IP地址并注册到DNS服务器上。于是,客户端会认为负载均衡器就是一台Web服务器,并向其发送请求,然后由负载均衡器来判断将请求转发给哪台Web服务器(图5.4)。这里的关键点不言而喻,那就是如何判断将请求转发给哪台Web服务器。

图5.4 用于对多台Web服务器分配访问的负载均衡器

判断条件有很多种,根据操作是否跨多个页面,判断条件也会有所不同。如果操作没有跨多个页面,则可以根据Web服务器的负载状况来进行判断。负载均衡器可以定期采集Web服务器的CPU、内存使用率,并根据这些数据判断服务器的负载状况,也可以向Web服务器发送测试包,根据响应所需的时间来判断负载状况。当然,Web服务器的负载可能会在短时间内上下波动,因此无法非常准确地把握负载状况,反过来说,如果过于密集地去查询服务器的负载,这个查询操作本身就会增加Web服务器的负载。因此也有一种方案是不去查询服务器的负载,而是根据事先设置的服务器性能指数,按比例来分配请求。无论如何,这些方法都能够避免负载集中在某一台服务器上。

当操作跨多个页面时,则不考虑Web服务器的负载,而是必须将请求发送到同一台Web服务器上。要实现这一点,关键在于我们必须要判断一个操作是否跨了多个页面。HTTP的基本工作方式是在发送请求消息之前先建立TCP连接,当服务器发送响应消息后断开连接,下次访问Web服务器的时候,再重新建立TCP连接。因此,在Web服务器看来,每一次HTTP访问都是相互独立的,无法判断是否和之前的请求相关。

之所以会这样,是因为Web中使用的HTTP协议原本就是这样设计的。如果要判断请求之间的相关性,就必须在Web服务器一端保存相应的信息,这会增加服务器的负担。此外,Web服务器最早并不是用来运行CGI程序的,而是主要用来提供静态文件的,而静态文件不需要判断请求之间的相关性,因此最早设计HTTP规格的时候,就有意省略了请求之间相关性的判断。

那么在不知道请求之间的相关性时,能不能根据一系列请求的发送方IP地址相同这一点来判断呢?也不行。如果使用了我们后面要讲的代理机制,所有请求的发送方IP地址都会变成代理服务器的IP地址,无法判断实际发送请求的客户端是哪个。此外,如果使用了地址转换,发送方IP地址则会变成地址转换设备的IP地址,也无法判断具体是哪个客户端。

于是,人们想出了一些方案来判断请求之间的相关性。例如,可以在发送表单数据时在里面加上用来表示关联的信息,或者是对HTTP规格进行扩展,在HTTP头部字段中加上用来判断相关性的信息。这样,负载均衡器就可以通过这些信息来作出判断,将一系列相关的请求发送到同一台Web服务器,对于不相关的请求则发送到负载较低的服务器了。

5.4 使用缓存服务器分担负载

5.4.1 如何使用缓存服务器

除了使用多台功能相同的Web服务器分担负载之外,还有另外一种方法,就是将整个系统按功能分成不同的服务器,如Web服务器、数据库服务器。缓存服务器就是一种按功能来分担负载的方法。

缓存服务器是一台通过代理机制对数据进行缓存的服务器。代理介于Web服务器和客户端之间,具有对Web服务器访问进行中转的功能。当进行中转时,它可以将Web服务器返回的数据保存在磁盘中,并可以代替Web服务器将磁盘中的数据返回给客户端。这种保存的数据称为缓存,缓存服务器指的也就是这样的功能。

Web服务器需要执行检查网址和访问权限,以及在页面上填充数据等内部操作过程,因此将页面数据返回客户端所需的时间较长。相对地,缓存服务器只要将保存在磁盘上的数据读取出来发送给客户端就可以了,因此可以比Web服务器更快地返回数据。

不过,如果在缓存了数据之后,Web服务器更新了数据,那么缓存的数据就不能用了,因此缓存并不是永久可用的。此外,CGI程序等产生的页面数据每次都不同,这些数据也无法缓存。无论如何,在来自客户端的访问中,总有一部分访问可以无需经过Web服务器,而由缓存服务器直接处理。即便只有这一部分操作通过缓存服务器提高了速度,整体性能也可以得到改善。此外,通过让缓存服务器处理一部分请求,也可以减轻Web服务器的负担,从而缩短Web服务器的处理时间。

5.4.2 缓存服务器通过更新时间管理内容

图5.5 临时保存内容并代替Web服务器返回内容的缓存服务器

5.4.3 最原始的代理——正向代理

5.4.4 正向代理的改良版——反向代理

5.4.5 透明代理

5.5 内容分发服务

5.5.1 利用内容分发服务分担负载

图5.10 缓存服务器的3种部署方式

5.5.2 如何找到最近的缓存服务器

在使用内容分发服务时,如图5.11所示,互联网中有很多缓存服务器,如何才能从这些服务器中找到离客户端最近的一个,并让客户端去访问那台服务器呢?

图5.11 访问目标服务器的所在地

第一种方法:

就是像负载均衡一样用DNS服务器来分配访问。也就是说,我们可以在DNS服务器返回Web服务器IP地址时,对返回的内容进行一些加工,使其能够返回距离客户端最近的缓存服务器的IP地址。在解释这种方法的具体原理之前,我们先来复习一下DNS的基本工作方式。

互联网中有很多台DNS服务器,它们通过相互接力来处理DNS查询,这个过程从客户端发送查询消息开始,也就是说客户端会用要访问的Web服务器域名生成查询消息,并发送给自己局域网中的DNS服务器[插图](图5.12①)​。然后,客户端DNS服务器会通过域名的层次结构找到负责管理该域名的DNS服务器,也就是Web服务器端的那个DNS服务器,并将查询消息发送给它(图5.12②)​。Web服务器端的DNS服务器收到查询消息后,会查询并返回域名相对应的IP地址。在这台DNS中,有一张管理员维护的域名和IP地址的对应表,只要按照域名查表,就可以找到相应的IP地址(图5.12③)​。接下来,响应消息回到客户端的DNS服务器,然后再返回给客户端(图5.12④)​。

图5.12 DNS服务器的一般工作方式

上面讲的是Web服务器的域名只对应一个IP地址的情况,如果一个域名对应多个IP地址,则按照前面图5.3的轮询方式按顺序返回所有的IP地址。

如果按照DNS服务器的一般工作方式来看,它只能以轮询方式按顺序返回IP地址,完全不考虑客户端与缓存服务器的远近,因此可能会返回离客户端较远的缓存服务器IP地址。

第二种方法:

如果要让用户访问最近的缓存服务器,则不应采用轮询方式,而是应该判断客户端与缓存服务器的距离,并返回距离客户端最近的缓存服务器IP地址。这里的关键点不言自明,那就是到底该怎样判断客户端与缓存服务器之间的距离呢?

首先,作为准备,需要事先从缓存服务器部署地点的路由器收集路由信息(图5.13)​。例如,在图5.13的例子中,一共有4台缓存服务器,在这4台服务器的部署地点又分别有4台路由器,则我们需要分别获取这4台路由器的路由表,并将4张路由表集中到DNS服务器上。

图5.13 DNS服务器参照路由信息时的工作方式

接下来,DNS服务器根据路由表查询从本机到DNS查询消息的发送方,也就是客户端DNS服务器的路由信息。例如,根据图5.13路由器A的路由表,可以查出路由器A到客户端DNS服务器的路由。通过互联网内部的路由表中的路由信息可以知道先通过运营商X,然后通过运营商Y,最后到达运营商Z这样的信息,通过这样的信息可以大致估算出距离。依次查询所有路由器的路由表之后,我们就可以通过比较找出哪一台路由器距离客户端DNS服务器最近。提供路由表的路由器位于缓存服务器的位置,而客户端DNS服务器也应该和客户端在同一位置,这样就等于估算出了缓存服务器与客户端之间的距离,从而能够判断出哪台缓存服务器距离客户端最近了。实际上,客户端DNS服务器不一定和客户端在同一位置,因此可能无法得出准确的距离,但依然可以达到相当的精度。

5.5.3 通过重定向服务器分配访问目标

忽略, 此方法没意义

还有另一个让客户端访问最近的缓存服务器的方法。HTTP规格中定义了很多头部字段,其中有一个叫作Location的字段。当Web服务器数据转移到其他服务器时可以使用这个字段,它的意思是“您要访问的数据在另一台服务器上,请访问那台服务器吧。​”这种将客户端访问引导到另一台Web服务器的操作称为重定向,通过这种方法也可以将访问目标分配到最近的缓存服务器。

5.5.4 缓存的更新方法会影响性能

还有一个因素会影响缓存服务器的效率,那就是缓存内容的更新方法。缓存本来的思路是像图5.5那样,将曾经访问过的数据保存下来,然后当再次访问时拿出来用,以提高访问操作的效率。不过,这种方法对于第一次访问是无效的,而且后面的每次访问都需要向原始服务器查询数据有没有发生变化,如果遇到网络拥塞,就会使响应时间恶化。

要改善这一点,有一种方法是让Web服务器在原始数据发生更新时,立即通知缓存服务器,使得缓存服务器上的数据一直保持最新状态,这样就不需要每次确认原始数据是否有变化了,而且从第一次访问就可以发挥缓存的效果。内容分发服务采用的缓存服务器就具备这样的功能。

6、请求到达web服务器,响应返回浏览器 — 短短几秒“漫长旅程”迎来终点

上一章,我们探索了Web服务器前面的防火墙、缓存服务器、负载均衡器等设备,现在网络包已经通过这些设备,到达了Web服务器中。本章的探索之旅就从这里开始。

(1)服务器概览

服务器的职责是响应客户端的请求,但仅从如何响应请求这一点是无法看清服务器的全貌的,这样是无法理解服务器的。因此,我们会先介绍一下服务器程序的整体结构,以及启动后要做的一些准备工作,这样大家就能够了解服务器到底是怎么一回事了。

(2)服务器的接收操作

搞清楚服务器的全貌之后,我们来探索一下服务器的协议栈是如何接收数据的。首先我们看一看服务器如何接收信号并将信号还原成数字形式的网络包,然后从中提取出HTTP消息。在第1章、第2章介绍发送操作的时候我们也提了一些关于接收操作的内容,但那些介绍都比较零散,本章我们将对接收操作做一个整体性的探索。然后,我们将探索协议栈是如何将接收的消息通过Socket库传递给Web服务器程序的。

(3)Web服务器程序解释请求消息并作出响应

Web服务器程序收到消息后,会查询其中的内容,并按照请求进行处理,将结果返回给客户端。例如,如果请求内容是获取某个网页的数据,那么就读取该文件并取出数据;如果请求某个CGI程序,就将相关参数传递给该程序并执行,然后获取程序输出的数据。接下来,这些数据会以响应消息的形式返回给客户端。我们将对上面这一系列操作进行探索。

(4)浏览器接收响应消息并显示内容

Web服务器返回的响应消息会通过互联网到达客户端计算机的浏览器。接下来,浏览器会将消息的内容显示在屏幕上。当客户端计算机上显示出网页的内容时,访问Web服务器的操作就全部完成了,这也是我们本次探索之旅的终点。

6.1 服务器概览

6.1.1 客户端与服务器的区别

首先,服务器和客户端有什么区别呢?根据用途,服务器可以分为很多种类,其硬件和操作系统与客户端是有所不同的。但是,网络相关的部分,如网卡、协议栈、Socket库等功能和客户端却并无二致。无论硬件和OS如何变化,TCP和IP的功能都是一样的,或者说这些功能规格都是统一的。

不过,它们的功能相同,不代表用法也相同。在连接过程中,客户端发起连接操作,而服务器则是等待连接操作,因此在Socket库的用法上还是有一些区别的,即应用程序调用的Socket库的程序组件不同。

此外,服务器的程序可以同时和多台客户端计算机进行通信,这也是一点区别。因此,服务器程序和客户端程序在结构上是不同的。

6.1.2 服务器程序的结构

服务器需要同时和多个客户端通信,但一个程序来处理多个客户端的请求是很难的,因为服务器必须把握每一个客户端的操作状态。因此一般的做法是,每有一个客户端连接进来,就启动一个新的服务器程序,确保服务器程序和客户端是一对一的状态。

具体来说,服务器程序的结构如图6.1所示。首先,我们将程序分成两个模块,即等待连接模块(图6.1(a))和负责与客户端通信的模块(图6.1(b))[插图]。当服务器程序启动并读取配置文件完成初始化操作后,就会运行等待连接模块(a)。这个模块会创建套接字,然后进入等待连接的暂停状态。接下来,当客户端连发起连接时,这个模块会恢复运行并接受连接,然后启动客户端通信模块(b),并移交完成连接的套接字。接下来,客户端通信模块(b)就会使用已连接的套接字与客户端进行通信,通信结束后,这个模块就退出了。

图6.1 用不同的模块与每个客户端进行通信

每次有新的客户端发起连接,都会启动一个新的客户端通信模块(b),因此(b)与客户端是一对一的关系。这样,(b)在工作时就不必考虑其他客户端的连接情况,只要关心自己对应的客户端就可以了。通过这样的方式,可以降低程序编写的难度。服务器操作系统具有多任务、多线程功能,可以同时运行多个程序,服务器程序的设计正是利用了这一功能。

当然,这种方法在每次客户端发起连接时都需要启动新的程序,这个过程比较耗时,响应时间也会相应增加。因此,还有一种方法是事先启动几个客户端通信模块,当客户端发起连接时,从空闲的模块中挑选一个出来将套接字移交给它来处理。

6.1.3 服务器端的套接字和端口号

刚才我们介绍了服务器程序的大体结构,但如果不深入挖掘调用Socket库的具体过程,我们还是无法理解服务器是如何使用套接字来完成通信的。因此,下面就来看一看服务器程序是如何调用Socket库的。

首先,我们再来回忆一下客户端与服务器的区别。从数据收发的角度来看,区分“客户端”和“服务器”这两个固定的角色似乎不是一个好办法。现在大多数应用都是由客户端去访问服务器,但其实应用的形态不止这一种。为了能够支持各种形态的应用,最好是在数据收发层面不需要区分客户端和服务器,而是能够以左右对称的方式自由发送数据。TCP也正是在这样的背景下设计出来的。

不过,这其中还是存在一个无法做到左右对称的部分,那就是连接操作。连接这个操作是在有一方等待连接的情况下,另一方才能发起连接,如果双方同时发起连接是不行的,因为在对方没有等待连接的状态下,无法单方面进行连接。因此,只有这个部分必须区分发起连接和等待连接这两个不同的角色。从数据收发的角度来看,这就是客户端与服务器的区别,也就是说,发起连接的一方是客户端,等待连接的一方是服务器。

这个区别体现在如何调用Socket库上。首先,客户端的数据收发需要经过下面4个阶段。

(1)创建套接字(创建套接字阶段)

(2)用管道连接服务器端的套接字(连接阶段)

(3)收发数据(收发阶段)

(4)断开管道并删除套接字(断开阶段)

相对地,服务器是将阶段(2)改成了等待连接,具体如下。

(1)创建套接字(创建套接字阶段)

(2-1)将套接字设置为等待连接状态(等待连接阶段)

(2-2)接受连接(接受连接阶段)

(3)收发数据(收发阶段)

(4)断开管道并删除套接字(断开阶段)

图6.4 分配接收到的包

这个问题可以用下面的方法来解决,即要确定某个套接字时,不仅使用服务器端套接字对应的端口号,还同时使用客户端的端口号再加上IP地址,总共使用下面4种信息来进行判断

· 客户端IP地址

· 客户端端口号

· 服务器IP地址

· 服务器端口号

服务器上可能存在多个端口号相同的套接字,但客户端的套接字都是对应不同端口号的,因此我们可以通过客户端的端口号来确定服务器上的某个套接字。不过,使用不同端口号的规则仅限一台客户端的内部,当有多个客户端进行连接时,它们之间的端口号是可以重复的。因此,我们还必须加上客户端的IP地址才能进行判断。例如,IP地址为198.18.203.154的客户端的1025端口,就和IP地址为198.18.142.86的客户端的1025端口对应不同的套接字。如果能够理解上面这些内容,那么关于套接字和端口号的知识就已经掌握得差不多了。

说句题外话,既然通过客户端IP地址、客户端端口号、服务器IP地址、服务器端口号这4种信息可以确定某个套接字,那么要指代某个套接字时用这4种信息就好了,为什么还要使用描述符呢?这个问题很好,不过我们无法用上面4种信息来代替描述符。原因是,在套接字刚刚创建好,还没有建立连接的状态下,这4种信息是不全的。此外,为了指代一个套接字,使用一种信息(描述符)比使用4种信息要简单。出于上面两个原因,应用程序和协议栈之间是使用描述符来指代套接字的。

使用描述符来指代套接字的原因如下。

1
2
3
(1)等待连接的套接字中没有客户端IP地址和端口号

(2)使用描述符这一种信息比较简单

6.2 服务器的接受操作

6.2.1 网卡将接收到的信号转换成数字信息

了解了服务器的整体结构之后,下面我们重新回到探索之旅。现在,客户端发送的网络包已经到达了服务器。

到达服务器的网络包其本质是电信号或者光信号,接收信号的过程和客户端是一样的。关于这个过程我们在第2章介绍客户端包收发操作时已经讲过了,不过这里还是简单复习一下,顺便从整体上看一看接收操作的全过程。

接收操作的第一步是网卡接收到信号,然后将其还原成数字信息。局域网中传输的网络包信号是由1和0组成的数字信息与用来同步的时钟信号叠加而成的,因此只要从中分离出时钟信号,然后根据时钟信号进行同步,就可以读取并还原出1和0的数字信息了。

图6.5 服务器将接收到的电信号还原为数字信息

服务器接收电信号的过程和客户端发送的过程相反,是从模拟信息转换为数字信息。

图6.6 根据信号还原的数字信息

以太网的基本工作方式是将数据广播到整个网络上,只有指定的接收者才接收数据,因此网络中还有很多发给其他设备的数据在传输,如果包的接收者不是自己,那么就需要丢弃这个包。

到这里,接收信号并还原成数字信息的操作就完成了,还原后的数字信息被保存在网卡内部的缓冲区中。上面这些操作都是由网卡的MAC模块来完成的。

1
网卡的MAC模块将网络包从信号还原为数字信息,校验FCS并存入缓冲区

6.2.2 IP模块的接收操作

当网络包转交到协议栈时,IP模块会首先开始工作,检查IP头部。IP模块首先会检查IP头部的格式是否符合规范,然后检查接收方IP地址,看包是不是发给自己的。当服务器启用类似路由器的包转发功能时,对于不是发给自己的包,会像路由器一样根据路由表对包进行转发。

确认包是发给自己的之后,接下来需要检查包有没有被分片。检查IP头部的内容就可以知道是否分片,如果是分片的包,则将包暂时存放在内存中,等所有分片全部到达之后将分片组装起来还原成原始包;如果没有分片,则直接保留接收时的样子,不需要进行重组。到这里,我们就完成了包的接收。

接下来需要检查IP头部的协议号字段,并将包转交给相应的模块。例如,如果协议号为06(十六进制)​,则将包转交给TCP模块;如果是11(十六进制)​,则转交给UDP模块。这里我们假设这个包被交给TCP模块处理,然后继续往下看。

1
协议栈的IP模块会检查IP头部,(1)判断是不是发给自己的;(2)判断网络包是否经过分片;(3)将包转交给TCP模块或UDP模块

6.2.3 TCP模块如何处理连接包

前面的步骤对于任何包都是一样的,但后面的TCP模块的操作则根据包的内容有所区别。首先,我们来看一下发起连接的包是如何处理的。

当TCP头部中的控制位SYN为1时,表示这是一个发起连接的包(图6.7①)​。这时,TCP模块会执行接受连接的操作,不过在此之前,需要先检查包的接收方端口号,并确认在该端口上有没有与接收方端口号相同且正在处于等待连接状态的套接字。如果指定端口号没有等待连接的套接字,则向客户端返回错误通知的包。

图6.7 服务器的工作过程

6.2.4 TCP模块如何处理数据包

接下来我们来看看进入数据收发阶段之后,当数据包到达时TCP模块是如何处理的(图6.7②)​。

首先,TCP模块会检查收到的包对应哪一个套接字。在服务器端,可能有多个已连接的套接字对应同一个端口号,因此仅根据接收方端口号无法找到特定的套接字。这时我们需要根据IP头部中的发送方IP地址和接收方IP地址,以及TCP头部中的接收方端口号和发送方端口号共4种信息,找到上述4种信息全部匹配的套接字

找到4种信息全部匹配的套接字之后,TCP模块会对比该套接字中保存的数据收发状态和收到的包的TCP头部中的信息是否匹配,以确定数据收发操作是否正常。具体来说,就是根据套接字中保存的上一个序号和数据长度计算下一个序号,并检查与收到的包的TCP头部中的序号是否一致。如果两者一致,就说明包正常到达了服务器,没有丢失。这时,TCP模块会从包中提出数据,并存放到接收缓冲区中,与上次收到的数据块连接起来。这样一来,数据就被还原成分包之前的状态了。

当收到的数据进入接收缓冲区后,TCP模块就会生成确认应答的TCP头部,并根据接收包的序号和数据长度计算出ACK号,然后委托IP模块发送给客户端。

收到的数据块进入接收缓冲区,意味着数据包接收的操作告一段落了。接下来,应用程序会调用Socket库的read(图6.7③)来获取收到的数据,这时数据会被转交给应用程序。如果应用程序不来获取数据,则数据会被一直保存在缓冲区中,但一般来说,应用程序会在数据到达之前调用read等待数据到达,在这种情况下,TCP模块在完成接收操作的同时,就会执行将数据转交给应用程序的操作。

然后,控制流程会转移到服务器程序,对收到的数据进行处理,也就是检查HTTP请求消息的内容,并根据请求的内容向浏览器返回相应的数据。这一部分已经超出了TCP模块的范围,我们将在稍后探索服务器程序内部时进行介绍。

1
收到数据包时,TCP模块会(1)根据收到的包的发送方IP地址、发送方端口号、接收方IP地址、接收方端口号找到相对应的套接字;(2)将数据块拼合起来并保存在接收缓冲区中;(3)向客户端返回ACK。

6.2.5 TCP模块的断开操作

在TCP协议的规则中,断开操作可以由客户端或服务器任何一方发起,具体的顺序是由应用层协议决定的。Web中,这一顺序随HTTP协议版本不同而不同,在HTTP1.0中,是服务器先发起断开操作。

这时,服务器程序会调用Socket库的close,TCP模块会生成一个控制位FIN为1的TCP头部,并委托IP模块发送给客户端。当客户端收到这个包之后,会返回一个ACK号。接下来客户端调用close,生成一个FIN为1的TCP头部发给服务器,服务器再返回ACK号,这时断开操作就完成了。HTTP1.1中,是客户端先发起断开操作,这种情况下只要将客户端和服务器的操作颠倒一下就可以了。

无论哪种情况,当断开操作完成后,套接字会在经过一段时间后被删除。

6.3 Web服务器程序解释请求消息并作出响应

6.3.1 将请求的URI转换为实际的文件名

6.3.2 运行CGI程序

6.3.3 Web服务器的访问控制

6.3.4 返回响应消息

6.4 浏览器接收响应消息并显示内容

6.4.1 通过响应的数据类型判断其中的内容

Web服务器发送的响应消息会被分成多个包发送给客户端,然后客户端需要接收数据。首先,网卡将信号还原成数字信息,协议栈将拆分的网络包组装起来并取出响应消息,然后将消息转交给浏览器。这个过程和服务器的接收操作相同。接下来,我们来看一看浏览器是如何显示内容的。

要显示内容,首先需要判断响应消息中的数据属于哪种类型。Web可以处理的数据包括文字、图像、声音、视频等多种类型,每种数据的显示方法都不同,因此必须先要知道返回了什么类型的数据,否则无法正确显示。

这时,我们需要一些信息才能判断数据类型,原则上可以根据响应消息开头的Content-Type头部字段的值来进行判断。这个值一般是下面这样的字符串。

1
Content-Type:text/html

其中“/”左边的部分称为“主类型”​,表示数据的大分类;右边的“子类型”表示具体的数据类型。在上面的例子中,主类型是text,子类型是html。主类型和子类型的含义都是事先确定好的,表6.1列出了其中主要的一些类型。上面例子中的数据类型表示遵循HTML规格的HTML文档。

表6.1 消息的Content-Type定义的数据类型

此外,当数据类型为文本时,还需要判断编码方式,这时需要用charset附加表示文本编码方式的信息,内容如下。

1
Content-Type:text/html;charset=utf-8

这里的utf-8表示编码方式为Unicode,如果是euc-jp就表示EUC编码,iso-2022-jp表示JIS编码,shift_jis表示JIS编码。

Encoding头部字段。如果消息中存放的内容是通过压缩或编码技术对原始数据进行转换得到的,那么Content-Encoding的值就表示具体的转换方式,通过这个字段的值,我们可以知道如何将消息中经过转换的数据还原成原始数据。

Content-Type字段使用的表示数据类型的方法是在MIME规格中定义的,这个规格不仅用于Web,也是邮件等领域中普遍使用的一种方式。不过这种方式也只不过是一种原则性的规范,要通过Content-Type准确判断数据类型,就需要保证Web服务器正确设置Content-Type的值,但现实中并非总是如此。如果Web服务器管理员不当心,就可能会因为设置错误导致Content-Type的值不正确。因此,根据原则检查Content-Type并不能确保总是能够准确判断数据类型。

因此,有时候我们需要结合其他一些信息来综合判断数据类型,例如请求文件的扩展名、数据内容的格式等。比如,我们可以检查文件的扩展名,如果为.html或.htm则看作是HTML文件,或者也可以检查数据的内容,如果是以开头的则看作是HTML文档。不仅是HTML这样的文本文件,图片也是一样。图片是经过压缩的二进制数据,但其开头也有表示内容格式的信息,我们可以根据这些信息来判断数据的类型。不过,这部分的逻辑并没有一个统一的规格,因此不同的浏览器以及不同的版本都会有所差异。

6.4.2 浏览器显示网页内容!访问完成!

不介绍了

7、总结 网络包的旅程