计网自顶向下读书笔记
个人笔记, 主要记录计算机网络自顶向下方法, 没有详细记录全部内容, 存在一些补充或者个人理解. 存在额外内容, 额外内容会特别标注.
一. 计算机网络和因特网
网络核心
网络核心由由各种路由/转发设备组成. 通过路由设备将各个不同的网络进行相连,使各个端系统连接.
通过网络链路和交换机传输数据有两种基本方法:分组交换和电路交换.
分组交换
在各种网络应用中、端系统彼此交换报文 . 报文能够包含协议设计者需要的任何东西. 报文可以执行一种控制功能,也可以包含数据,例如电子邮件数据、 JPEG 图像或 MP3 音频文件.
为了从源端系统向目的端系统发送一个报文,源将长报文划分为较小的数据块,称之为分组. 在源和目的地之间,每个分组都通过通信链路和分组交换机传送. 分组传输时会以其链路的最大传输速率通过.
交换机主要有两类,路由器和链路层交换机
1.存储转发传输
多数分组交换机在链路的输入端使用存储转发传输机制. 存储转发传输是指在交换机能够开始向输出链路传输该分组的第一个比特之前,必须接收到整个分组.
2.排队时延和分组丢失
每台分组交换机与多条链路相连. 对于每条相连的链路,该分组交换机具有一个输出缓存 (也称为输出队列) , 用于存储路由器准备发往那条链路的分组. 该输出缓存在分组交换中起着重要的作用.
如果到达的分组需要传输到某条链路,但发现该链路正忙于传输其他分组,该到达分组必须在输出缓存中等待. 因此,除了存储转发时延以外,分组还要承受输出缓存的排队时延. 这些时延是变化的,变化的程度取决于网络的拥塞程度. 因为缓存空间的大小是有限的,一个到达的分组可能发现该缓存已被其他等待传输的分组完全充满了. 在此情况下,将出现分组丢失(即丢包),到达的分组或已经排队的分组之一将被丢弃.
3.转发表和路由选择协议
路由器从与它相连的一条通信链路得到分组,然后向与它相连的另一 条通信链路转发该分组. 但是路由器怎样决定它应当向哪条链路进行转发呢?
在因特网中,每个端系统具有一个称为IP地址的地址. 当源主机要向目的端系统发送一个分组时,源在该分组的首部包含了目的地的IP地址. 当一个分组到达网络中的路由器时,路由器检查该分组的目的地址的一部分,并向一台相邻路由器转发该分组.
每台路由器具有一个转发表, 用于将目的地址(或目的地址的一部分)映射成为输出链路. 当某分组到达一台路由器时,路由器检查该地址,并用这个目的地址搜索其转发表,以发现适当的出链路. 路由器则将分组导向该出链路.
因特网具有一些特殊的路由选择协议用于自动地设置这些转发表. 具体内容在之后的章节讨论.
电路交换
在电路交换网络中,在端系统间通信会话期间,预留了端系统间沿路径通信所需要的资源(缓存,链路传输速率). 在分组交换网络中.这些资源则不是预留的. 会话的报文按需使用这些资源,其后果可能是不得不等待(即排队)接入通信线路.
传统的电话网络是电路交换网络的例子. 考虑当一个人通过电话网向另一个人发送信息(语音或传真)时所发生的情况. 在发送方能够发送信息之前,该网络必须在发送方和接收方之间建立一条连接. 这是一个名副其实的连接,因为此时沿着发送方和接收方之间路径上的交换机都将为该连接维护连接状态. 用电话的术语来说,该连接被称为一条电路. 当网络创建这种电路时,它也在连接期间在该网络链路上预留了恒定的传输速率(表示为每条链路传输容量的一部分)既然已经为该发送方–接收方连接预留了带宽,则发送方能够以确保的恒定速率向接收方传送数据.
以下是一个例子:

考虑一台主机要经过分组交换网络(如因特网)向另一台主机发送分组所发生的情况. 与使用电路交换相同,该分组经过一系列通信链路传输. 但与电路交换不同的是,该分组被发送进网络,而不预留任何链路资源之类的东西. 如果因为此时其他分组也需要经该链路进行传输而使链路之一出现拥塞,则该分组将不得不在传输链路发送侧的缓存中等待而产生时延. 因特网尽最大努力以实时方式交付分组,但它不做任何保证.
1.电路交换网络中的复用
复用是为了将链路划分为多个电路以满足多个用户数据传输的需求. 如上图1-13中,每条链路被划分为4条电路,就是链路的复用.
链路中的电路是通过频分复用 (FDM) 或时分复用 (TDM)来实现的.
对于 FDM, 链路的频谱由跨越链路创建的所有连接共享. 特别是,在连接期间链路为每条连接专用一个频段. 在电话网络中,这个频段的宽度通常为4kHz (即每秒4000 周期). 毫无疑问,该频段的宽度称为带宽. 调频无线电台也使用 FDM来共享88MHz ~ 108MHz 的频谱、其中每个电台被分配 一个特定的频段.
对于一条TDM链路,时间被划分为固定期间的帧,并且每个帧又被划分为固定数量的时隙. 当网络跨越一条链路创建一条连接(电路)时,网络在每个帧中为该连接指定一个时隙, 这些时隙专门由该连接单独使用,一个时隙(在每个帧内)可用于传输该连接的数据.

2.分组交换与电路交换的对比
电路连接不适合计算机之间的通信
连接建立的时间成本较高.
计算机之间的通信具有突发性,如果使用电路交换,则浪费的资源较多(即连接建立后就是两个端系统的专用连接,即使空闲,也不能被其他的呼叫(请求)利用.
分组交换网中的时延,丢包和吞吐量
分组交换网中的时延概述
分组从一台主机(源)出发,通过一系列路由器传输,在另一台主机(目的地)中结束它的历程. 当分组从一个节点(主机或路由器)沿着这条路径到后继节点(主机或路由器),该分组在沿途的每个节点经受了几种不同类型的时延. 这些时延最为重要的是**节点处理时延 **、排队时延、发送时延(又称传输时延)和传播时延, 这些时延总体累加起来是节点总时延.
计算:
发送时延 = 数据帧长度(b) / 信道带宽(b/s)
传播时延 = 信道长度(m) / 电磁波在信道上的传播速率(m/s)
1.时延对的类型
处理时延 (nodal processing delay)
检查分组首部和决定将该分组导向何处所需要的时间是处理时延的一部分. 处理时延也能够包括其他因素,如检查比特级别的差错所需要的时间,该差错出现在从上游节点向路由器A传输这些分组比特的过程中. 高速路由器的处理时延通常是微秒或更低的数量级. 在这种节点处理之后,路由器将该分组引向通往路由器B链路之前的队列.
排队时延(queuing delay)
在队列中,当分组在链路上等待传输时,它经受排队时延. 实际的排队时延可以是毫秒到微秒量级.
发送时延(传输时延)( transmission delay)
假定分组以先到先服务方式传输,这在分组交换网中是常见的方式. 仅当所有已经到达的分组被传输后,才能传输刚到达的分组. 用L bit表示该分组的长度,用 R bps表示从路由器A到路由器B的链路传输速率. 例如,对于一条10Mbps的以太网链路,速率R=10Mbps; 对于 100Mhps的以太网链路,速率R=100Mbps. 传输时延是L/R. 这是将所有分组的比特推向链路(即传输,或者说发射)所需要的时间. 实际的传输时延通常在毫秒到微秒量级.
传播时延(propagation delay)
数据从该链路的起点到目的地传播所需要的时间是传播时延. 该传播速率取决于该链路的物理媒体,等于或略小于光速.
排队时延和丢包
1.排队时延
令a表示分组到达队列的平均速率 (a 的单位是分组/秒,即pkt/s) ,R bps是传输速率,为了简单起见,也假定所有分组都是由L bit组成的. 则比特到达队列的平均速率 是La bps. 最后,假定该队列非常大,因此它基本能容纳无限数量的比特. 比率La/R被称为流量强度. 它在估计排队时延的范围方面经常起着重要的作用.
若流量强度>1, 则比特到达队列的平均速率超过从该队列传输出去的速率. 在这种情况下,该队列趋向于无限增加,并且排队时延将趋向无穷大. 因此,流量工程中的一条铁律是:*设计系统时流量强度不能大于1. *
随着流量强度接近1,平均排队时延迅速增加. 该强度的少量增加将导致时延大比例增加.

2.丢包
一条链路前的队列只有有限的容量. 因为该排队容量是有限的,随着流量强度接近1, 到达的分组将发现一个满的队列. 由于没有地方存储这个分组,路由器将丢弃该分组.
计算机网络中的吞吐量
除了时延和丢包,计算机网络中另一个至关重要的性能测度是端到端吞吐量.
为了定义吞吐量,考虑从主机A到主机B跨越计算机网络传送一个大文件,在任何瞬间的瞬时吞吐量是主机B接收到该文件的速率(bps). 如果该文件由 F 比特组成, 主机B接收到所有F比特用时T秒,则文件传送的平均吞吐量是F/T bps.
对于一个链路传输,其吞吐量是瓶颈链路的传输速率,即连接两端的所有链路中,传输速率最小的链路的传输速率.
协议层次及其服务模型
分层的体系结构
1.协议分层
为了给网络协议的设计提供一个结构,网络设计者以分层的方式组织协议以及实现这些协议的网络硬件和软件,每个协议属于这些层次之一. 某层向它的上一层提供的服务, 即所谓一层的服务模型 .
每层通过在该层中执行某些动作或使用直接下层的服务来提供服务. 例如,由第n层提供的服务可能包括报文从网络的 一边到另一边的可靠交付. 这可能是通过使用第n-1层的边缘到边缘的不可靠报文传送服务,加上第n层的检测和重传丢失报文的功能来实现的.
一个协议层能够用软件、硬件或两者的结合来实现. 诸如 HTTP和SMTP这样的应用 层协议几乎总是在端系统中用软件实现,运输层协议也是如此. 物理层和数据链路层负责处理跨越特定链路的通信,它们通常在与给定链路相关联的网络接口卡(例如以太网 或WiFi 接口卡)中实现. 网络层经常是硬件和软件实现的混合体.
还要注意的是,一个第 n 层协议也分布在构成该网络的端系统、分组交换机和其他组件中. 这就是说,第n层协议的不同部分常常位于这些网络组件的各部分中.
协议分层具有概念化和结构化的优点. 如我们看到的那样,分层提供了一种结构化方式来讨论系统组件,模块化使更新系统组件更为容易.
各层的所有协议被称为协议栈.
1.1因特网协议栈
因特网的协议栈由5个层次组成:物理层、链路层、网络层、运输层和应用层.
本书的“自顶向下”既是“以因特网协议栈的层次从上往下”的意思
![]()
- 5.应用层
应用层是网络应用程序及它们的应用层协议存留的地方. 因特网的应用层包括许多协议,例如HTTP (它提供了Web文档的请求和传送)、SMTP (它提供了电子邮件报文的传输)和FTP (它提供两个端系统之间的文件传送). 我们将看到,某些网络功能,如将像 www. ietf. org 这样对人友好的端系统名字转换为32比特的网络地址(域名解析),也是借助于特定的应用层协议即域名系统(DNS)完成的.
应用层协议分布在多个端系统上,而一个端系统中的应用程序使用协议与另一个端系统中的应用程序交换信息分组. 我们把这种位于应用层的信息分组就是常常提及的报文.
- 4.运输层
因特网的运输层在应用程序端点之间传送应用层报文. 在因特网中,有两种运输协议,即TCP和UDP, 利用其中的任一个都能运输应用层报文.
TCP 向它的应用程序提供了面向连接的服务. 这种服务包括了应用层报文向目的地的确保传递和流量控制(即发送方/接收方速率匹配). TCP也将长报文划分为短报文,并提供拥塞控制机制,因此当网络拥塞时,源抑制其传输速率.
UDP协议向它的应用程序提供无连接服务. 这是一种不提供不必要服务的服务,没有可靠性,没有流量控制,也没有拥塞控制.
粗略的说,TCP协议更可靠,相对资源要求也更高(数据大小,时延等). UDP则反之.
在本书中,我们把运输层的分组称为报文段.
- 3.网络层
网络层负责将网络层分组从一台主机移动到另一 台主机,网络层分组称为数据报. 在一台源主机中的运输层协议 (TCP或UDP) 向网络层递交报文段和目的地址.
网络层包括著名的网际协议IP, 该协议定义了在数据报中的各个字段以及端系统和路由器如何作用于这些字段. 一个端系统的IP仅有一个,所有具有网络层的因特网组件必须运行IP. 因特网的网络层也包括决定路由的路由选择协议,它根据该路由将数据报从源传输到目的地.
尽管网络层包括了其他网际协议和一些路由选择协议,但通常把它简单地称为IP层,这反映了 IP是将因特网连接在一起的黏合剂这样的事实.
- 2.链路层
为了将分组从一个 节点(主机或路由器)移动到路径上的下一个节点,网络层必须依靠该链路层的服务. 特别是在每个节点,网络层将数据报下传给链路层,链路层沿着路径将数据报传递给下一个节点. 在该下一个节点,链路层将数据报上传给网络层.
由链路层提供的服务取决于应用于该链路的特定链路层协议. 例如,某些协议基于链路提供可靠传递,从传输节点跨越一条链路到接收节点.
值得注意的是,这种可靠的传递 服务不同于TCP的可靠传递服务,TCP提供从一个端系统到另一个端系统的可靠交付. 链路层的例子包括以太网、 WiFi和电缆接入网的DOCSIS协议. 因为数据报从源到目的地传送通常需要经过几条链路,一个数据报可能被沿途不同链路上的不同链路层协议处理. 例如,一个数据报可能被一段链路上的以太网和下一段链路上的PPP所处理. 网络层将受到 来自每个不同的链路层协议的不同服务. 在本书中,我们把链路层分组称为帧.
- 1.物理层
虽然链路层的任务是将整个帧从一个网络元素移动到邻近的网络元素,而物理层的任务是将该帧中的一个个比特从一个节点移动到下一个节点.
1.2 OSI模型
OSI模型即是开放系统互连模型,其参考模型有七层:应用层、表示层、会话层、运输层、网 络层、数据链路层和物理层.
这些层次中, 5层的功能大致与它们名字类似的因特网对应层的功能相同. 所以,我们来考虑OSI参考模型中附加的两个层,即表示层和会话层. 表示层的作用是使通信的应用程序能够解释交换数据的含义. 这些服务包括数据压缩和数据 加密(它们是自解释的)以及数据描述(这使得应用程序不必担心在各台计算机中表示/存储的内部格式不同的问题). 会话层提供了数据交换的定界和同步功能,包括了建立检查点和恢复方案的方法.
封装

上图显示了这样一条物理路径:数据从发送端系统的协议栈向下,沿着中间的链 路层交换机和路由器的协议栈上上下下,然后向上到达接收端系统的协议栈.
路由器和链路层交换机都是分组交换机,与端系统类似,路由器和链路层交换机以多层次的方式组织它们的网络硬件和软件. 而路由器和链路层交换机并不实现协议栈中的所有层次. 如图所示,链路层交换机实现了第一层和第二层;路由器实现了第一层到第三层. 这意味着因特网路由器能够实现IP协议,而链路层交换机则不能. 尽管链路层交换机不能识别IP地址,但它们能够识别第二层地址,如以太网地址. 值得注意的是,主机实现了所有5个层次,这与因特网体系结构将它的复杂性放在网络边缘的观点是一致的.
上图也说明了一个重要概念:封装. 在发送主机端,一个应用层报文(图中的M) 被传送给运输层. 在最简单的情况下,运输层收取到报文并附上附加信息(所谓运输层首部信息,图中的Ht)该首部将被接收端的运输层使用. 应用层报文和运输层首部信息一道构成了运输层报文段. 运输层报文段因此封装了应用层报文. 附加的信息也许包括了下列信息:允许接收端运输层向上向适当的应用程序交付报文的信息;差错检测位信息,该信息让接收方能够判断报文中的比特是否在途中已被改变. 运输层则向网络层传递该报文段,网络层增加了如源和目的端系统地址等网络层首部信息(图中的Hn)生成了网络层数据报. 该数据报接下来被传递给链路层,链路层增加它自己的链路层首部信息并生成链路层帧. 所以我们看到,在每一 层,一个分组具有两种类型的字段: 首部字段(头)和有效载荷字段. 有效载荷通常是来自上一层的分组.
封装的过程能够比前面描述的更为复杂. 例如,一个大报文可能被划分为多个运输层的报文段(这些报文段每个又可能被划分为多个网络层数据报). 在接收端,则必须从其连续的数据报中重构这样一个报文段.
面对攻击的网络
病毒
病毒是一种需要某种形式的用户交互来感染用户设备的恶意软件.
蠕虫
蠕虫是一种无须任何明显用户交互就能进入设备的恶意软件.
Dos,拒绝服务攻击
DoS攻击使得网络、 主机或其他基础设施部分不能由合法用户使用. 大多数因特网DoS攻击属于下列三种类型之一:
弱点攻击
即针对对方的漏洞进行攻击
带宽洪泛
攻击者向目标主机发送大量的分组,分组数量之多使得目标的接入链路变得拥塞,使得合法的分组无法到达服务器.
连接洪泛
攻击者在目标主机中创建大量的半开或全开TCP连接,该主机因这些伪造的连接而陷入困境,并停止接受合法的连接.
下图所示的即是分布式DoS(DDoS),攻 击者控制多个源并让每个源向目标猛烈发送流量.

分组嗅探
在无线传输设备的附近放置一台被动的接收机,该接收机就能得到传输的每个分组的副本. 这些分组包含了各种敏感信息,包括口令、社会保险号、商业秘密和个人信息等. 记录每个流经的分组副本的被动接收机被称为分组嗅探器(wireshark就是其中之一).
由于分组嗅探是被动的,它并不会在信道中注入其他分组,所以基本无法检测其存在. 所以我们才要加密数据.
IP欺诈
具有虚假源地址的分组注入因特网的能力被称为IP哄骗, 而它只是一个用户能够冒充另一个用户的许多方式中的一种. 为了解决这个问题,我们需要采用端点鉴别,即一种使我们能够确信一个报文源自我 们认为它应当来自的地方的机制(比如http请求中的remote_addr).
二. 应用层
网络应用原理
研发网络应用程序的核心是写出能够运行在不同的端系统和通过网络彼此通信的程序.
例如,在Web应用程序中,有两个互相通信的不同的程序: 一个是运行在用户主机(桌面机、膝上机、平板电脑、智能电话等) 上的浏览器程序,另一个是运行在Web服务器主机.
网络核心设备并不在应用层上起作用,而仅在较低层起作用,特别是在网络层及以下层次起作用. 将应部用软件限制在端系统的方法,促进了大量的网络应用程序的迅速研发和部署(因为不用考虑下层了).
网络应用体系
当进行软件编码之前,应当对应用程序有一个宽泛的体系结构计划. 应用程序的体系结构明显不同于网络的体系结构(例如在第1章中所讨论的5层因特网体系结构).
从应用程序研发者的角度看,网络体系结构是固定的,并为应用程序提供了特定的服务集合
另外,应用体系结构由应用程序研发者设计,规定了如何在各种端系统上组织该应用程序. 在选择应用程序体系结构时,应用程序研发者很可能利用现代网络应用程序中所使用的两种主流体系结构之一:客户-服务器体系结构或对等(P2P)体系结构.
- 客户-服务器体系结构
在客户-服务器体系结构中,有一个总是打开的主机,称为服务器,它服务于来自许多其他称为客户的主机. 值得注意的是,客户-服务器体系结构下,客户相互之间不直接通信.
客户-服务器体系结构的另一个特征是该服务器具有固定的、周知的地址,该地址称为IP地址.
在一个客户-服务器应用中,常常会出现一台单独的服务器主机跟不上它所有客户请求的情况. 例如,一个流行的社交网络站点如果仅有一台服务器来处理所有请求,将很快变得不堪重负. 为此,托管大量主机的数据中心常被用于创建强大的虚拟服务器,用以满足客户需求.

- P2P体系结构
在P2P体系结构中,对位于数据中心的专用服务器有最小的(或者没有)依赖. 相反,应用程序在间断连接的主机对之间使用直接通信,这些主机对被称为对等方. 这些对等方并不为服务提供商所有,而是为用户的台式机和笔记本电脑所控制. 因为这种对等方通信不必通过专门的服务器,该体系结构被称为对等方到对等方的. 流行的P2P应用的例子是文件共享应用BitTorrent.
P2P体系结构的特性之一是其自扩展性. 例如,在一个P2P文件共享应用中,尽管每个对等方都由于请求文件产生工作负载,但每个对等方通过向其他对等方分发文件也为系统整体增加服务能力. P2P体系结构也是有成本效率的,因为它通常不需要庞大的服务器基础设施和服务器带宽. 然而,未来P2P应用由于高度非集中式结构,面临安全性、性能和可靠性等挑战.

进程通信
在构建网络应用程序前,还需要对程序如何运行在多个端系统上以及程序之间如何相互通信有基本了解. 用操作系统的术语来说,进行通信的实际上是进程而不是程序.
- 一个进程可以被认为是运行在端系统中的一个程序.
- 当多个进程运行在相同的端系统上时,它们使用进程间通信机制相互通信.
- 进程间通信的规则由端系统上的操作系统确定.
而在本书中,我们并不特别关注同一台主机上的进程间通信,而关注运行在不同端系统(可能具有不同的操作系统)上的进程间通信.
在两个不同端系统上的进程,通过跨越计算机网络交换报文而相互通信. 发送进程生成并向网络中发送报文;接收进程接收这些报文并可能通过回送报文进行响应.
1.客户和服务器进程
网络应用程序由成对的进程组成,这些进程通过网络相互发送报文.
例如,在Web应用程序,一个客户浏览器进程与一个Web服务器进程交换报文. 在一个P2P文件共享系统, 文件从一个对等方中的进程传输到另一个对等方中的进程. 对每对通信进程,我们通常将这两个进程之一为客户,而另一个进程为服务器. 对于Web而言,浏览器是一个客户进程,Web服务器是一个服务器进程. 对于P2P文件共享,下载文件的对等方为客户,上载文件的对等方为服务器.
客户和服务器进程的定义如下:**在一对进程之间的通信会话场景,发起通信(即在该会话开始时发起与其他进程的联系)的进程为客户进程,在会话开始时等待联系的进程为服务器进程. **
2. 进程与计算机网络之间的接口
如上所述,多数应用程序由通信进程对组成,每对中的两个进程相互发送报文. 从一个进程向另一个进程发送的报文必须通过下面的网络. 进程通过一个称为套接字的软件接口向网络发送报文和从网络接收报文.
下图显示了两个经过因特网通信的进程之间的套接字通信(假定由该进程使用的运输层协议是TCP协议). 如该图所示,套接字是同一台主机内应用层与运输层之间的接口. 由于该套接字是建立网络应用程序的可编程接口,因此套接字也称为应用程序和网络之间的应用编程接口(API). 应用程序开发者可以控制套接字在应用层端的一切,但是对该套接字的运输层端几乎没有控制权.

应用程序开发者对于运输层的控制仅限于:
- 选择运输层协议
- 可能能设定几个运输层参数,如最大缓存和最大报文段长度
3. 进程寻址
为了向特定目的地发送邮政邮件,目的地需要有一个地址. 类似地,在一台主机上运行的进程为了向另一台主机上运行的进程发送分组,接收进程需要有一个地址. 为了标识该接收进程,需要定义两种信息:
- 主机的地址(因特网中就是IP地址)
- 目标主机中指定接收进程的标识符
除了知道报文发送目的地的主机地址,发送进程还必须指定运行在接收主机上的接收进程. 因为一般而言一台主机能够运行许多网络应用,所以这些信息是必要的. 目的地端口号用于这个目的. 常用的应用有固定的端口号,如Web服务器用端口号80来标识. 邮件服务器进程(使用SMTP协议)用端口号25来标识.
用于所有因特网标准协议的周知端口号的列表能够在www.iana.org处找到
可供应用程序使用的运输服务
包括因特网在内的很多网络提供了不止一种运输层协议. 当开发一个应用时,必须选择一种可用的运输层协议. 如何做出这种选择呢?最可能的方式是,通过研究这些可用的运输层协议所提供的服务,选择一个最能为你的应用需求提供恰当服务的协议.
我们大体能够从四个方面对应用程序服务要求进行分类:
- 可靠数据传输
- 吞吐量
- 定时
- 安全性
1. 可靠数据传输
分组在计算机网络中可能丢失. 例如,分组能够使路由器中的缓存溢出,或者当分组中的某些比特损坏后可能被丢弃.
因此,为了支持这些应用,必须做一些工作以确保由应用程序的一端发送的数据正确并完全地交付给该应用程序的另一端. 如果一个协议提供了这样的确保数据交付服务,就认为提供了可靠数据传输.
运输层协议能够潜在地向应用程序提供的一个重要服务就是进程到进程的可靠数据传输. 当一个运输协议提供这种服务,发送进程只要将其数据传递进套接字,就可以完全相信该数据将能无差错地到达接收进程.
当一个运输层协议不提供可靠数据传输时,由发送进程发送的某些数据可能到达不了接收进程. 这可能能被容忍丢失的应用所接受,最值得注意的是多媒体应用,如交谈式音频/视频,它能够承受一定量的数据丢失. 在多媒体应用中,丢失的数据会引起播放的音频/视频出现小干扰,而不是致命的损伤.
2. 吞吐量
具有吞吐量要求的应用程序被称为带宽敏感的应用. 许多当前的多媒体应用是带宽敏感的,尽管某些多媒体应用可能采用自适应编码技术对数字语音或视频以与当前可用带宽相匹配的速率进行编码.
带宽敏感的应用具有特定的吞吐量要求,而弹性应用能够根据当时可用的带宽或多或少地利用可供使用的乔吐量. 电子邮件、文件传输以及Web传送都属于弹性应用.
3. 定时
运输层协议也能提供定时保证. 如同具有吞吐量保证那样,定时保证能够以多种形式实现. 一个保证的例子如:发送方注入套接字中的每个比特到达接收方的套接字不迟于100ms.
交互式实时应用程序对于定时有较高要求,对于非实时的应,较低的时延总比较高的时延好,但对端到端的时延没有严格的约束.
4. 安全性
运输协议能够为应用程序提供一种或多种安全性服务. 例如,在发送主机中,运输协议能够加密由发送进程传输的所有数据;在接收主机中,运输协议能够在将数据交付给接收进程之前解密这些数据. 这种服务将在发送和接收进程之间提供机密性,以防数据以某种方式在这两个进程之间被观察. 运输协议还能提供除了机密性以外的其他安全性服务,包括数据完整性和端点鉴别等.
因特网提供的运输服务
我们已经考虑了计算机网络能够提供的通用运输服务. 现在我们要更为具体地考察由因特网提供的运输服务类型. 因特网(更一般的是TCP/IP网)为应用程序提供两个运输层协议,即TCP和UDP.
1. TCP服务
TCP服务模型包括面向连接服务和可靠数据传输服务. 当某个应用程序调用TCP作为其运输协议时,该应用程序就能获得来自TCP的这两种服务.
- 面向连接的服务:在应用层数据报文开始流动之前,TCP让客户和服务器相互交换运输层控制信息. 这个所谓的握手过程提醒客户和服务器,让它们为大量分组的到来做好准备. 在握手阶段后,一个TCP连接就在两个进程的套接字之间建立了. 这条连接是全双工的,即连接双方的进程可以在此连接上同时进行报文收发. 当应用程序结束报文发送时,必须拆除该连接.
- 可靠的数据传输服务:通信进程能够依靠TCP,无差错、按适当顺序交付所有发送的数据. 当应用程序的一端将字节流传进套接字时,它能够依靠TCP将相同流交付给接收方的套接,而没有字节的丢失和冗余.
TCP还具有拥塞控制机制,这种服务不一定能直接为通信进程带来好处,但对因特网整体有利. 当发送方和接收方之间的网络出现拥塞时,TCP的拥塞控制机制会抑制发送进程(客户或服务器).
EX. TCP安全
无论TCP还是UDP都没有提供任何加密机制,这就是说发送进程传进其套接字的数据,与经网络传送到目的进程的数据相同. 因此,举例来说,如果某发送进程以明文方式发送了一个口令进入它的套接字,该明文口令将经过发送方与接收方之间的所有链路传送,这就可能在任何中间链路被嗅探和发现.
因为隐私和其他安全问题对许多应用而言已经成为至关重要的问题,所以因特网界已经研制了TCP的“强化模块”,称为运输层安全(TLS). 用TLS加强后的TCP不仅能够做传统的TCP所能做的一切,而且提供了关键的进程到进程的安全性服务,包括加密、数据完整性和端点鉴别.
注意,TLS并不是因特网运输层传输协议,它只是一种对TCP的加强,这种加强是在应用层实现的.
TLS有它自己的套接字API,这类似于传统的TCP套接字API. 当一个应用使用TLS时,发送进程向TLS套接字传递明文数据;发送主机中的TLS则加密该数据,并将加密的数据传递给TCP套接字. 加密的数据经因特网传送到接收进程中的TCP套接字. 该接收套接字将加密数据传递给TLS,由其进行解密. 最后,TLS通过它的TLS套接字将明文数据传递给接收进程.
2. UDP服务
UDP是一种不提供不必要服务的轻量级运输协议,它仅提供最低限度的服务.
UDP是无连接的,因此在两个进程通信前没有握手过程. UDP提供一种不可靠数据传输服务,也就是说,当进程将一个报文发送进UDP套接字时,UDP并不保证该报文将到达接收进程. 不仅如此,到达接收进程的报文也可能是乱序到达的.
UDP不包括拥塞控制机制,所以UDP的发送端可以用它选定的任何速率向其下层(网络层)注入数据. (然而,值得注意的是实际端到端吞吐量可能小于该速率,这可能是由中间链路的带宽受限或拥塞而造成的. )
3. 因特网运输协议所不提供的服务
TCP提供了可靠的端到端数据传输. 并且我们也知道TCP在应用层可以很容易地用TLS来加强以提供安全服务. 所以运输协议服务中的可靠数据传输和安全性都可以得到满足
今天的因特网通常能够为时间敏感应用提供满意的服务,但它不能提供任何定时或知吐量保证.
下图给出了一些流行的因特网应用所使用的运输协议. 可以看到,电子邮件、远程终端访问、Web、文件传输都使用了TCP. 这些应用选择TCP的最主要原因是TCP提供了可靠数据传输服务,确保所有数据最终到达目的. 因为因特网电话应用(如Skype)通常能够容忍某些丢失但要求达到一定的最小速率才能有效工,所以因特网电话应用的开发者通常愿意将该应用运行在UDP上,从而设法避开TCP的拥塞控制机制和分组开销. 但因为许多防火墙被配置成阻挡(大多数类型的)UDP流量,所以因特网电话应用通常被设计成如果UDP通信失败就使用TCP作为备选项.

应用层协议
我们刚刚学习了通过把报文发送进套接字实现网络进程间的互相通信. 那么如何构造这些报文?在这些报文中,各个字段的含义是什么?进程何时发送这些报文?这些问题将我们带进应用层协议的范围.
应用层协议(application-layer protocol)定义了运行在不同端系统上的应用程序进程如何相互传递报文. 特别是应用层协议定义了以下内容:
- 交换的报文类型,例如请求报文和响应报文.
- 各种报文类型的语法,如报文中的各个字段及这些字段是如何描述的.
- 字段的语义,即这些字段中信息的含义.
- 确定一个进程何时以及如何发送报文,对报文进行响应的规则.
有些应用层协议是由RFC文档定义的,因此它们位于公共域中. 例如,Web的应用层协议HTTP(超文本传输协议[RFC 7230])就作为一个RFC可供使用. 如果浏览器开发者遵从HTTP RFC规则,所开发出的浏览器就能访问任何遵从该文档标准的Web服务器并获取相应Web页面. 还有很多别的应用层协议是专用的,有意不为公共域所用. 例如,Skype使用了专用的应用层协议.
区分网络应用和应用层协议是很重要的. 应用层协议只是网络应用的一部分(尽管它是应用非常重要的一部分). 例如,Web是一种客户-服务器应用,它允许客户按照需求从Web服务器获得文档. 该Web应用有很多组成部分,包括文档格式的标准(即HIML)、Web浏览器(如Chrome和Microsoft Internet Explorer)、Web服务器(如Apache、Microsoft服务器程序),以及一个应用层协议. Web的应用层协议是HITP,它定义了在浏览器和Web服务器之间传输的报文格式和序列. 因此,HTTP只是Web应用的一个部分(尽管是重要部分).
Web和HTTP
HTTP概述
Web的应用层协议是超文本传输协议(HyperText Transfer Protocol,HTTP),它是Web的核心. HTTP由两个程序实现:一个客户程序和一个服务器程序. 客户程序和服务器程序运行在不同的端系统中通过交换HTTP报文进行会话. HTTP定义了这些报文的结构以及客户和服务行报文交换的方式. 在详细解释HITP之前,先了解一些Web术语.
Web页面(Web page)(也叫文档)是由对象组成的. 一个对象(object)只是一个文件,诸如一个HIML文件、一个JPEG图形、一个JavaScript文件、一个CCS样式表文件或一个视频片段,它们可通过一个URL寻址. 多数Web页面含有一个HTML基本文件(base HTML file)以及几个引用对象. 例如,如果一个Web页面包含HTML文本和5个JPEGC图形,那么这个Web页面有6个对象:一个HTML基本文件和5个图形. HTML基本文件通过对象的URL引用页面中的其他对象. 每个URL由两部分组:存放对象的服务器主机和名和对象的路径. 例如,一个URL为http:www.someSchool.edu/ysomeDepartment/picture.gif
,其中的www.someSchool.edu
就是主机名,/someDepartment/picture.gif
就是路径名. 因为Web浏览器(Web browser)实现了HTTP的客户端,所以在Web环境中我们经常交换使用浏览器和客户这两个术语. Web服务器(Webserver)实现了HTTP的服务器端,它用于存储Web对象,每个对象由URL寻址. 流行的Web服务器有Apache和Nginx等.
HTTP定义了Web客户向Web服务器请求Web页面的方式,以及服务器向客户传送Web页面的方式. 我们稍后详细讨论客户和服务器的交互过程,而其基本思想在下图中进行了图示. 当用户请求一个Web页面(如点击一个超链接)时浏览器向服务器发出对该页面中所包含对象的HTTP请求报文,服务器接收到请求并用包含这些对象的HTTP响应报文进行啊应.

HTTP使用TCP作为它的支撑运输协议(而不是在UDP上运行). HTTP客户首先发起一个与服务器的TCP连接. 一旦连接建立,该浏览器和服务器进程就可以通过套接字接口访问TCP.
客户向它的套接字接口发送HTTP请求报文并从它的套接字接口接收HTTP响应报文. 类似,服务器从它的套接字接口接收HTTP请求报文并向它的套接字接口发送HTTP响应报文. 一旦客户向它的套接字接口发送了一个请求报文,该报文就脱离了客户控制并进入TCP的控制.
TCP为HTTP提供可靠数据传输服务. 这意味着,一个客户进程发出的每个HTTP请求报文最终能完整地到达服务器;类似,服务器进程发出的每个HTTP响应报文最终能完整地到达客户. 这里我们看到了分层体系结构最大的优点,即HTTP不用担心数据丢失,也不关注TCP从网络的数据委失和乱序故障中恢复的细节. 那是TCP以及协议栈较低层协议的工作.
需要注意:服务器向客户发送被请求的文件,而不存储任何关于该客户的状态信息. 假如某个特定的客户在短短的几秒内两次请求同一个对象,服务器并不会因为刚刚为该客户提供了该对象就不再做出反应,而是重新发送该对象. 因为HTTP服务器并不保存关于客户的任何信息,所以我们说HTTP是一个无状态协议(stateless protocol). 我们同时也注意到Web使用了客户服务器应用程序体系结构. Web服务器总是打开的,具有一个固定的IP地址,且它服务于可能来自数以百万计的不同浏览器的请求.
HTTP 的初始版本称为 HTTP/1.0, 其可追溯到20世纪90年代早期 [RFC1945] . 到2020年为止, 绝大部分的 HTTP 事务都采用 HTTP/1.1 [RFC7230]. 然而, 越来越多的浏览器和 Web 服务器也支持新版的 HTTP, 称为 HTTP/2.0 [RFC7540]. 在本节结束时,将给出 HTTP/2.0 的简介.
非持续连接和持续链接
在许多因特网应用程序中,客户和服务器在一个相当长的时间范围内通信,在此期间,客户发出一系列请求,并且服务器对每个请求进行响应. 依据应用程序以及该应用程序的使用方式,这一系列请求可以以规则的间隔周期性地或者间断性地一个接一个发出.
当每个请求/响应对是经一个单独的TCP连接发送,则该应用程序被称为使用非持续连接(non-persistentconnection).
当所有的请求及其响应经相同的TCP连接发送,则该应用程序被称为使用持续连接(persistentconnection).
为了深入地理解该设计问题,我们研究在特定的应用程序即HTTP的情况下持续连接的优点和缺点,HTTP既能够使用非持续连接,也能够使用持续连接. 尽管HTTP默认使用持续连接,但HITP客户和服务器也能配置成使用非持续连接.
1. 采用非持续连接的HTTP
我们看看在非持续连接情况下从服务器向客户传送一个Web页面的步骤. 假设该页面含有1个HTML基本文件和10个JPEG图形,并且这11个对象位于同一台服务器上. 进一步假设该HTML文件的URL为http://www.example.com/index.html,以下是请求发送的情况:
- HTTP客户进程在端口号80发起一个到服务器www.example.com的TCP连接,该端口号是HTTP的默认端口. 在客户和服务器上分别有一个套接字与该连接相关联.
- HTTP客户经它的套接字向该服务器发送一个HTTP请求报文. 请求报文中包含了路径名/index.html.
- HTTP服务器进程经它的套接字接收该请求报文,从其存储器(通常是RAM)中检索出对象index.html(注意工作目录问题),在一个HTTP响应报文中封装对象,并通过其套接字向客户发送响应报文.
- HTTP服务器进程通知TCP断开该TCP连接. (但直到TCP确认客户得到完整的响应报文后,它才会实际中断连接. )
- HTTP客户接收响应报文,TCP连接关闭. 该报文指出封装的对象是一个HTML文件,客户从响应报文中提取出该文件,检查该HTML文件,得到对10个JPEG图形的引用.
- 对每个引用的JPEG图形对象重复前4个步骤.
当浏览器收到Web页面后,向用户显示该页面. 两个不同的浏览器也许会以不同的方式解释(即向用户显示)该页面. HTTP与客户如何解释一个Web页面毫无关系. HTTP规范([RFC1945]
和[RFC7540]
)仅定义了在HTTP客户程序与HTTP服务器程序之间的通信协议.
上面的步骤举例说明了非持续连接的使用,其中每个TCP连接在服务器发送一个对象后关闭,即该连接并不为其他的对象而持续下来. HTTP71.0应用了非持续TCP连接. 值得注意的是每个TCP连接只传输一个请求报文和一个响应报文. 因此在本例,当用户请求该Web页面时,要产生11个TCP连接.
在上面描述的步骤,我们有意没有明确客户获得这10个JPEG图形对象是使用10个串行的TCP连接,还是某些JPEG对象使用了一些并行的TCP连接. 事实上,用户能够配置现代浏览器来控制连接的并行度. 浏览器打开多个TCP连接,并且请求经多个连接请求某Web页面的不同部分. 我们在下一章会看到,使用并行连接可以缩短响应时间.
在继续讨论之前,我们来简单估算一下从客户请求HTML基本文件起到该客户收到整个文件止所花费的时间. 为此,我们给出往返时间(Round-Trip Time,RTT)的定义,该时间是指一个短分组从客户到服务器然后再返回客户所花费的时间. RTT包括分组传播时延、分组在中间路由器和交换机上的排队时延以及分组处理时延. 现在考虑当用户点击超链接时会发生什么现象. 如下图所示,,这引起浏览器在它和Web服务器之间发起一个TCP连接;这涉及一次“三次握手”过程,即客户向服务器发送一个小TCP报文段,服务器用一个小TCP报文段做出确认和响应,最后,客户向服务器返回确认(确认连接建立). 三次握手中前两个部分所耗费的时间占一个RTT. 完成了三次握手的前两个部分后,客户结合三次握手的第三部分向该TCP连接发送一个HTTP请求报文. 一旦该请求报文到达服务器,服务器就在该TCP连接上发送HTML文件. 该HTTP请求/响应用去了另一个RTT. 因此,粗略地讲,总的响应时间就是两个RTT加上服务器传输HTML文件的时间.

非持续性连接的缺点非常明显. 第一,必须为每一个请求的对象建立和维护一个全新的连接. 对于每个这样的连接,在客户和服务器中都要分配TCP的缓冲区和保持TCP变量,这给Web服务器带来了严重的负担,因为一台Web服务器可能同时服务于数以百计不同客户的请求. 第二,每一个对象经受两倍RTT的交付时延,即一个RTT用于创建TCP,另一个RTT用于请求和接收一个对象.
2. 采用持续连接的HTTP
在采用HTTP1.1持续连接的情况,服务器在发送响应后保持该TCP连接打开. 在相同的客户与服务器之,后续的请求和响应报文能够通过相同的连接进行传送. 特别是,一个完整的Web页面(上例中的HTML基本文件加上10个图形)可以用单个持续TCP连接进行传送. 更有甚者,位于同一台服务器的多个Web页面在从该服务器发送给同一个客户时,可以在单个持续TCP连接上进行. 对对象的这些请求可以一个接一个地发出,而不必等待对未决请求(流水线)的回答. 通常,如果一条连接经过一定时间间隔(一个可配置的超时间隔)仍未被使用,HTTP服务器就关闭该连接. HTTP的默认模式是使用带流水线的持续连接.
HTTP 报文格式
HTTP规范[RFC1945,RFC7230,RFC7540]包含了对HTTP报文格式的定义. HTTP报文有两种:请求报文和响应报文. 下面讨论这两种报文.
1. HTTP请求报文
下面提供了一个典型的HTTP请求报文:
GET /soemedir/page.htmL HTTIP/1.1
Host: www.Someschool.edu
Connection: Close
User-agent: Mozilla/5.0
Accept-Language: fr
通过仔细观察这个简单的请求报文,我们就能学到很多东西. 首先,我们看到该报文是用普通的ASCII文本书写的,这样有一定计算机知识的人都能够阅读它. 其次,我们看到该报文由5行组成,每行由一个回车和换行符(/t/n
)结束. 最后一行后再附加一个回车和换行符. 虽然这个特定的报文仅有5行,但一个请求报文能够具有更多的行或者至少为一行.
HTTP请求报文的第一行叫作请求行(requestline),其后继的行叫作首部行(headerline). 请求行有3个字段:方法字段、URL字段和HTTP版本字段. 方法字段可以取几种不同的值,包括GET、POST、HEAD、PUT和DELETE. 绝大部分的HTTP请求报文使用GET方法. 当浏览器请求一个对象时,使用GET方法,在URL字段带有请求对象的标识. 在本例中,该HTTP报文在请求对象/somedirpage.html
. 其版本字段是自解释的,在本例中,浏览器实现的是HTTP/1.1版本.
现在我们看看本例的首部行. 首部行Host:www.someschool.edu
指明了的主机. 你也许认为该首部行是不必要的,因为在该主机中已对象所在经有一条TCP连接存在了. 但是,如我们将在2.2.5节中所见,该首部行提供的信息是Web代理高速缓存所要求的. 通过包含Connection:close
首部行,该浏览吉告诉服务器不要麻烦地使用持续连接,它要求服务吉在发送完被请求的对象后就关闭这条连接. User-agent:
首部行用来指明用户代理,即向服务器发送请求的浏览器的类型. 这里浏览器类型是Mozila/5.0
,即Firefox浏览器. 这个首部行是有用的,因为服务器可以有效地为不同类型的用户代理实际发送相同对象的不同版本(每个版本都由相同的URL寻址). 最后,Accept-language:
首部行表示用户想得到该对象的法语版本(如果服务器中有这样的对象的话);否则,服务器应当发送它的默认版本. Accept-language:
首部行仅是HTTP中可用的众多内容协商首部之一.
看过一个例子之后,我们再来看看如下图所示的一个请求报文的通用格式. 我们看到该通用格式与我们前面的例子密切对应. 然而,在首部行(与附加的回车和换行符)后有一个实体体(entitybody). 使用GET方法时整个实体体为空,而使用POST方法(并不止)时才使用该实体体. 当用户提交表单时,HTTP客户常常使用POST方法,例如当用户向搜索引擎提供搜索关键词时. 使用POST报文时,用户仍可以向服务器请求一个Web页面,但Web页面的特定内容依赖于用户在表单字段中输入的内容. 如果方法字段的值为POST,则实体体中包含的就与用户在表单字段中的输入值有关.
HTML表单将是经常使用GET方法,并在(表单字段)所请求的URL中包括输入的数据. 例如,一个表单使用GET方法,它有两个字段,分别填写的是monkeys和bananas这样,该URL结构为www.somesite.com/animalsearch?monkeys&bananas
.
这里的
monkey
和bananas
就是GET Params.
HEAD方法类似于GET方法. 当服务器收到一个使用HEAD方法的请求时,将会用一个HTTP报文进行响应,但是并不返回请求对象. 应用程序开发者常用HEAD方法进行调试跟踪.
PUT方法常与Web发行工具联合使用,它允许用户上传对象到指定的Web服务需上指定的路径(目录). PUT方法也被那些需要向Web服务器上传对象的应用程序使用.
DELETE方法允许用户或者应用程序删除Web服务器上的对象.
2.HTTP响应报文
下面提供了一条典型的HTTP响应报文. 该响应报文可以是对刚刚讨论的例子中请求报文的响应.
HTTP/1.1 200 OK
Conriection: close
Date: Tue, 18 Aug 2015 15:44:04 GMT
Server: Apache/2.2.3 (CentOS)
Last-Modified: Tue, 18 Aug 2015 15:11:03 GMT
Content-Length: 6821
Content-Type: text/html
(data ...)
仔细看一下这个响应报文. 它有三个部:一个初始状态行(status line),6个首部行(header line),然后是实体体(entity body). 实体体部分是报文的主要部分,即它包含了所请求的对象本身(表示为data…).
状态行有3个字段:协议版本字段、状态码和响应状态信息. 在这个例子中,状态行指示服务器正在使用HTTP/1.1,并且一切正常(状态码200,即服务器已经找到并正在发送所请求的对象).
现在来看看首部行.
Connection: close
首部行告诉客户,发送完报文后将关闭该TCP连接.Date
首部行指示服务器产生并发送该响应报文的日期和时间. 值得一提的是,这个时间不是指对象创建或者最后修改的时间,而是服务器从它的文件系统中检索到该对像,将该对象插入响应报文,并发送该响应报文的时间.Server
首部行指示该报文是由一台Apache Web服务器产生的,它类似于HTTP请求报文中的User-agent
首部行.Last-Modiftied
首部行指示该对象创建或最后修改的时间与日期. “Last-Modified
首部行对既可能在本地客户也可能在网络缓存服务器(即代理服务器)上的对象缓存来说非常重要,下文将更为详细地讨论Last-Modified
首部行.Content-Length
首部行指示了被发送对象中的字节数.Content-Type
首部行指示了实体体中的对象是HTML文本. (该对象类型应该正式地用Content-Type
首部行而不是文件扩展名来指示. )Content-Length也是可以引发安全问题的,比如CVE-2024-21096.
一些常见的状态码和相关的短语包括:
- 200 OK:请求成功,信息在返回的响应报文中.
- 301 Moved Permanently:请求的对象已经被永久转移了,新的URL定义在响应报文的
Location
首部行. 客户软件将自动获取新的URL. - 400 Bad Request: 一个通用差错代码,指示该请求不能被服务器理解.
- 403 Forbidden: 拒绝访问(无权限访问或不合规范等).
- 404 Not Found: 被请求的文档不在服务器上.
- 500 Internal Server Error: 服务器内部错误.
- 505 HTTP Version Not Supported: 服务器不支持请求报文使用的HTTP版本.
在本节中,我们讨论了HTTP请求报文和响应报文中的一些首部行. HTTP规范中定义了许许多多的首部行,这些首部行可以被浏览器、Web服务器和网络缓存服务器插入(当然也可以自己来加). 我们只提到了全部首部行中的少数几个,在2.2.5节中我们讨论网络Web缓存时还会涉及其他几个.
浏览器是如何决定在一个请求报文中包含哪些首部行的呢?Web服务器又是如何决定在一个响应报文中包含哪些首部行呢?浏览器产生的首部行与很多因素有关,包括浏览器的类型和版本、浏览器的用户配置、浏览器当前是否有一个缓存的但可能超期的对象版本. Web服务器的表现也类似:在产品、版本和配置上都有差异,所有这些都会影响响应报文中包含的首部行.
用户与服务器的交互: Cookie
我们前面提到了HTTP服务器是无状态的. 这简化了服务器的设计,并且允许工程师开发可以同时处理数千个TCP连接的高性能Web服务器. 然而一个Web站点通常希望能够识别用户,可能是因为服务器希望限制用户的访问,或者因为它希望把内容与用户身份联系起来. 为此,HTTP使用了Cookie. Cookie在[RFC 6265]中定义,它允许用户进行跟踪. 目前大多数商务Web站点都使用了Cookie.
如下图所示,Cookie技术有4个组件:
在HTTP响应报文中的一个Cookie首部行;
在HTTP请求报文中的一个Cookie首部行;
在用户端系统中保留的一个Cookie文件,并由用户的浏览器进行管理;
位于Web站点的一个后端数据库.
这里省去一个例子.

Cookie可以用于标识一个用户. 用户首次访问一个站点时,可能需要提供一个用户标识(可能是名字[实际上会是根据某个密钥加一些个人信息加密生成的]). 在后继会话中,浏览器向服务器传递一个Cookie首部,从而向该服务器标识了用户. 因此Cookie可以在无状态的HTTP之上建立一个用户会话层. 例如,当用户向一个基于Web的电子邮件系统注册时,浏览需向服务器发送Cookie信息,允许该服务器在用户与应用程序会话的过程标识该用户.
尽管Cookie通常能够简化用户的因特网购物活动,但是其使用仍具有争议,因为它被认为是对用户隐私的一种侵害. 如我们刚才所见,结合Cookie和用户提供的账户信息,Web站点可以得知许多有关用户的信息,并可能将这些信息卖给第三方.
所以现在很多站点会询问是否允许记录Cookie.
Web缓存
Web缓存器(Web cache)也叫代理服务器(proxy server),它是能够代表初始Web服务器来满足HTTP请求的网络实体. Web缓存器有自己的磁盘存储空间,并在存储空间中保存最近请求过的对象的副本. 如下图所示,可以配置用户的浏览器,使得用户的所有HTTP请求首先指向Web缓存器[RFC 7234]. 一且某浏览器被配置,每个对某对象的浏览器请求首先被定向到该Web缓存器. 举例来说,假设浏览器正在请求对象http:www.someschool.edu/campus.gif,将会发生如下情况:
浏览器创建一个到Web缓存器的TCP连接,并向Web缓存HTTP请求.
Web缓存器进行检查,看看本地是否存储了该对象副本. 如果有,Web绥存器就向客户浏览器用HTTP响应报文返回该对象.
如果Web绥存器中没有该对象,它就打开一个与该对象的初始服务器(即www.someschool.edu)的TCP连接. Web缓存器则在这个缓存器到服务器的TCP连接上发送一个对该对象的HTTP请求. 在收到该请求后,初始服务器向该Web缓存器发送具有该对象的HTTP响应.
当Web缓存器接收到该对象时,它在本地存储空间存储一份副本,并向客户的浏览器用HTTP响应报文发送该副本(通过客户浏览器和Web缓存器之间现有的TCP连接).

值得注意的是Web缓存器既是服务器又是客户. 当它接收浏览器的请求并发回响应时,它是一个服务器. 当它向初始服务器发出请求并接收响应时,它是一个客户.
在因特网上部署Web绥存器有两个原因. 首先,Web缓存器可以大大减少对客户请求的响应时间,特别是当客户与初始服务器之间的瓶颈带宽远低于客户与Web缓存器之间的瓶颈带宽时更是如此. 如果在客户与Web缓存器之间有一个高速连接(情况常常如此),并且如果用户所请求的对象在Web缓存器上,则Web缓存器可以迅速将该对象交付给用户. 其次,如我们马上用例子说明的那样,Web缓存器能够大大减少一个机构的接入链路到因特网的通信量. 通过减少通信量,该机构(如一家公司或者一所大学)就不必急于增加带宽,因此降低了费用. 此外,Web缓存器能从整体上大大减少因特网上的Web流量,从而改善了所有应用的性能.
为了深刻理解缓存器带来的好处,我们考虑在下图场景下的一个例子. 该图显示了两个网络,即机构(内部)网络和公共因特网的一部分. 机构网络是一个高速的局域网,它的一台路由器与因特网上的一台路由器通过一条15Mbps的链路连接. 这些初始服务器与因特网相连但位于全世界各地. 假设对象的平均长度为1Mb,从机构内的浏览器对这些初始服务器的平均访问速率为每秒15个请求. 假设HTTP请求报文小到可以忽略,因而不会在网络中以及接入链路(从机构内部路由器到因特网路由器)上产生什么通信量. 我们还假设在图中从因特网接入链路一侧的路由器转发HTTP请求报文(在一个IP数据报中)开始,到它收到其响应报文(通常在多个IP数据报中)为止的时间平均为2s. 我们将该持续时延非正式地称为“因特网时延”.
总的响应时间,即从浏览器请求一个对象到接收到该对象为止的时间,是局域网时延、接入时延(即两台路由器之间的时延)和因特网时延之和. 我们来粗略地估算一下这个时延,局域网上的流量强度为:
(15个请求/s)x(1Mb/请求)/(100Mbps)=0.15
然而接入链路上的流量强度(从因特网路由器到机构路由器)为
(15个请求/s)x(1Mb/请求)/(15Mbps)=1
局域网上强度为0.15的通信量通常最多导致数十毫秒的时延,因此我们可以忽略局域网时延. 然而,如在1.4.2节讨论的那样,如果流量强度接近1(就像图中接入链路的情况那样),链路上的时延会变得非常大并且无限增长. 因此,满足请求的平均响应时间将在分钟的量级上. 显然,必须想办法来改进时间响应特性.
一个可能的解决办法就是增加接入链路的速率,如从15Mbps增加到100Mbps. 这可以将接入链路上的流量强度减少到0.15,这样一来,两台路由器之间的链路时延也可以忽略了. 这时,总的响应时间将大约为2s,即为因特网时延. 但这种解决方案也意味着该机构必须将它的接入链路由15Mbps升级为100Mbps,这是一种代价很高的方案(很贵的).
现在来考虑另一种解决方案,即不升级链路带宽而是在机构网络中安装一个Web缓存器. 这种解决方案如图2-13所示. 现实中的命中率(即由一个缓存器所满足的请求的比率)通常在0.2~0.7之间. 为了便于阐述,我们假设该机构的缓存命中率为0.4. 因为客户和缓存连接在一个相同的高速局域网上,这样40%的请求将几乎立即会由缓存器得到响应,时延约在10ms以内. 然而,剩下的60%的请求仍然要由初始服务器来满足. 但是只有60%的被请求对象通过接入链路,接人链路上的流量强度从1.0减小到0.6. 一般而言在15Mbps链路上,当流量强度小于0.8时对应的时延较小,约为几十毫秒. 这个时延与2s因特网时延相比是微不足道的.
因此,第二种解决方案提供的响应时延甚至比第一种解决方案更低,也不需要该机构升级它到因特网的链路. 该机构理所当然地要购买和安装Web缓存器. 除此之外其成本较低,很多缓存器使用了运行在廉价PC上的公共域软件.
通过使用内容分发网络(Content Distribution Network,CDN),Web缓存器正在因特网中发挥着越来越重要的作用. CDN公司在因特网上安装了许多地理上分散的缓存器,因而使大量流量实现了本地化. 有多个共享的CDN(例如Akamai和Limelight)和专用的CDN(例如谷歌和Netflix).
条件GET方法
尽管高速缓存能减少用户感受到的响应时间,但也引入了一个新的问题,即存放在缓存器中的对象副本可能是陈旧的. 换名话说,保存在服务器中的对象自该副本缓存在客户上以后可能已经被修改了. 幸运的是,HTTP有一种机制,允许缓存器证实它的对象是最新的. 这种机制就是条件GET(conditional GET)[RFC7232]. 如果HTTP请求报文使用GET方法,并且请求报文中包含一个If-modified-since
首部行,那么,这个HTTP请求报文就是一个条件GET请求报文.
为了说明GET方法的操作方式,我们看一个例子. 首先,一个代理缓存器(proxy cache)代表一个请求浏览器,向某Web服务器发送一个请求报文:
GET /fruit/kiwi.gif HTTP/1.1
Host: www.exotiquecuisine.com
其次,该Web服务器向缓存器发送具有被请求的对象的响应报文:
HTTP/1.1 200 OK
Date: Sat, 3 Oct 2015 15:39:29
Server: Apache/1.3.0 (Unix)
Last-Modified: Wed,9 Sep 2015 09:23:24
Content-Type: image/gif
(data...)
该缓存器在将对象转发到请求的浏览器的同时,也在本地缓存了该对象. 重要的是,缓存器在存储该对象时也存储了最后修改日期. 最后,一个星期后,另一个用户经过该缓存器请求同一个对象,该对象仍在这个缓存器中. 由于在过去的一个星期中位于Web服务器上的该对象可能已经被修改了,该缓存器通过发送一个条件GET执行最新检查. 具体来说,该缓存器发送:
GET /fruit/kiwi.gif HTTP/1.1
Host: www.exotiquecuisine,com
If-modified-since: Wed, 9 Sep 2015 09:23:24
值得注意的是If-modified-since
首部行的值正好等于一星期前服务器发送的响应报文中的Last-Modified
首部行的值. 该条件GET报文告诉服务器,仅当自指定日期之后该对象被修改过,才发送该对象. 假设该对象自2015年9月9日09:23:24后没有被修改. 接下来的第四步,Web服务器向该缓存器发送一个响应报文:
HTTP/1.1 304 Not Modified
Date: Sat, 10 Oct 2015 15:39:29
Server: Apache/1.3.0 (Unix)
(empty entity body)
我们看到,作为对条件CET方法的响应,该Web服务器仍发送一个响应报文,但并没有在该响应报文中包含所请求的对象. 包含该对象只会浪费带宽,并增加用户感受到的响应时间,特别是如果该对象很大更是如此. 值得注意的是在最后的响应报文中,状态行中为304 Not Modified,它告诉缓存器可以使用该对象,能向请求的浏览器转发它(该代理缓存器)缓存的对象副本.
HTTP/2
于2015年标准化的 HTTP/2 [RFC 7540] 是自 HTTP/1.1 以后的首个新版本, 而 HTTP/1.1 是1997年标准化的. HTTP/2 公布后, 2020年,在排名前1000万的 Web 站点中, 超过40%的站点支持 HTTP/2. 大多数浏览器(包括Chrome、Internet Explorer、Safari、opera和Firefox)也支持 HITP/2.
HTTP/2 的主要目标是减小感知时延,其手段是经单一TCP连接使请求与响应多路复用,提供请求优先次序和服务推送,并提供 HTTP 首部字段的有效压缩. HTTP/2 不改变 HTTP 方法、状态码、URL 或首部字段,而是改变数据格式化方法以及客户和服务器之间的传输方式.
回想 HTTP/1.1, 其使用持续 TCP 连接,允许经单一 TCP 连接将一个 Web 页面从服务器发送到客户. 由于每个 Web 页面仅用一个 TCP 连接,服务器的套接字数量被压缩,并且所传送的每个 Web 页面平等共享网络带宽(如下面所讨论的). 但 Web 浏览器的开发者很快就发现了经单一 TCP 连接发送一个 Web 页面中的所有对象存在队首阻塞[Head Of Line (HOL) blocking]问题.
为了理解 HOL 阻塞,考虑一个 Web 页面,它包括一个 HTML 基本页面、靠近 Web 页面顶部的一个大视频片段和该视频下面的许多小对象. 进一步假定在服务器和客户之间的通路上有一条低速/中速的瓶颈链路(例如一条低速的无线链路). 使用一条 TCP 连接, 视频片段将花费很长时间来通过该瓶颈链路, 与此同时, 那些小对象将被延迟, 因为它们在视频片段之后等待. 也就是说, 链路前面的视频片段阻塞了后面的小对象. HTTP/1.1 浏览器解决该问题的典型方法是打开多个并行的 TCP 连接, 从而让同一Web页面的多个对象并行地发送给浏览器. 采用这种方法, 小对象到达并呈现在浏览器上的速度要快得多, 因此可减小用户感知时延.
TCP 拥塞控制(将在第3章中详细讨论)也使得浏览器倾向于使用多条并行 TCP 连接而非单一持续连接. 粗略来说, TCP 拥塞控制针对每条共享同一条瓶颈链路的 TCP 连接, 给出一个平等共享该链路的可用带宽. 如果有 n 条 TCP 连接运行在同一条瓶颈链路上, 则每条连接大约得到 1/n 带宽. 通过打开多条并行 TCP 连接来传送一个 Web 页面, 浏览器能够”欺骗”并霸占该链路的大部分人带宽. 许多 HTTP/1.1 打开多达 6 条并行 TCP 连接并非为了避免 HOL 阻塞,而是为了获得更多的带宽.
HTTP/2 的基本目标之一是摆脱(或至少减少其数量)传送单一 Web 页面时的并行 TCP 连接. 这不仅减少了需要服务器打开与维护的套接字数量,而且允许TCP拥塞控制像设计的那样运行. 但与只用一个 TCP 连接来传送一个 Web 页面相比, HTTP/2 要求仔细设计相关机制以避免 HOL 阻塞.
1. HTTP/2 成帧
用于 HOL 阻塞的 HTTP/2 解决方案是将每个报文分成小帧, 并且在相同 TCP 连接上交错发送请求和响应报文. 为了理解这个问题, 再次考虑由一个大视频片段和许多小对象(例如8个)组成的 Web 页面的例子. 此时, 服务器将从希望查看该 Web 页面的浏览器处接收到9个并行的请求. 对于每个请求, 服务器需要向浏览器发送9个相互竞争的报文. 假定所有帧具有固定长度, 该视频片段由1000帧(报文帧)组成,并且每个较小的对象由2帧组成. 使用帧交错技术, 在视频片段发送第一帧后, 发送每个小对象的第一帧. 然后在视频片段发送第二帧后,发送每个小对象的第二帧. 因此,在发送视频片段的18帧后,所有小对象就发送完成了. 如果不采用交错, 则发送完其他小对象共需要发送1016帧. 因此 HTTP/2 成帧机制能够极大地减小用户感知时延.
将一个 HTTP 报文分成独立的帧、交错发送它们并在接收端将其装配起来的能力, 是 HTTP/2 最为重要的改进. 这一成帧过程是通过 HTTP/2 协议的成帧子层来完成的. 当某服务器要发送一个 HTTP 响应, 其响应由成帧子层来处理, 即将响应划分为帧. 响应的首部字段成为一帧, 报文体被划分为一帧以用于更多的附加帧. 通过服务器中的成帧子层, 该响应的帧与其他响应的帧交错并经过单一持续TCP连接发送. 当这些帧到达客户时, 它们先在成帧子层装配成初始的响应报文, 然后像以往一样由浏览器处理. 类似地, 客户的HTTP请求也被划分成帧并交错发送.
除了将每个 HTTP 报文划分为独立的帧外, 成帧子层也对这些帧进行二进制编码. 二进制协议解析更为高效, 会得到略小一些的帧, 并且更不容易出错.
2. 响应报文的优先次序和服务器推(推送)
报文优先次序允许研发者根据用户要求安排请求的相对优先权,从而更好地优化应用的性能. 如前文所述,成帧子层将报文组织为并行数据流发入相同的请求方. 当某客户向服务器发送并发请求时,它能够为正在请求的响应确定优先次序,方法是为每个报文分配1到256之间的权重. 较大的数字表明较高的优先. 通过这些权重,服务器能够为具有最高优先权的响应发送第一帧. 此外,客户也可通过指明相关的报文段ID,来说明每个报文段与其他报文段的相关性.
HTTP/2 的另一个特征是允许服务器为一个客户请求而发送多个响应. 即除了对初始请求的啊应外, 服务器能够向该客户推额外的对象, 而无须客户再进行任何请求. 因为 HTML 基本页指示了需要在页面呈现的全部对象, 所以这一点是可实现的. 因此无须等待对这些对象的 HTTP 请求, 服务器就能够分析该 HTML 页, 识别需要的对象, 并在接收到对这些对象的明确的请求前将它们发送到客户. 服务器推消除了因等待这些请求而产生的额外时延.
HTTP/3
QUIC(在第3章讨论) 是一种新型的“运输”协议,它在应用层中最基本的UDP之上实现. QUIC 具有几个能够满足 HTTP 的特征, 例如报文复用(交错)、每流流控和低时延连接创建. HTTP/3 是一种设计在 QUIC 之上运行的新 HTTP. 到2020年为止, HTTP/3 处于因特网草案阶段, 还没有全面标准化. 许多 HTTP/2 特征(如报文交错)已被收入 QUIC 中, 使得对 HTTP/3 的设计更为简单合理.
因特网中的电子邮件
自从有了因特网,电子邮件就在因特网上流行起来. 当因特网还在襁褓之中时,电子邮件已经成为最流行的应用程序,年复一年,它变得越来越精细,越来越强大. 它仍然是当今因特网上最重要和实用的应用程序之一.
与普通邮件一样,电子邮件是一种异步通信媒介,即人们方便时就可以发送邮件,不必与他人的计划进行协调. 与普通邮件相比,电子邮件更为快速,易于分发,而且价格便宜. 现代电子邮件具有许多强大的功能,包括添加附件,超链接,HTML格式文本和图片.
在本节中,我们将讨论于因特网电子邮件核心地位的应用层协议. 在深入讨论这些应用层协议之前,我们先总体看看因特网电子邮件系统和他的关键组件.
图2-14给出了因特网电子邮件系统的总体情况. 从该图中,我们可以看到它有3个主要组成部分:用户代理(user agent)、邮件服务器(mail server)和简单邮件传输协议〈Simple Mail Transfer Protocol,SMTP). 下面我们结合发送方Alice发电子邮件给接收方Bob的场景,对每个组成部分进行描述. 用户代理允许用户阅读,恢复,转发,保存和撰写报文. 微软的Outlook、Apple Mail、基于Web的Gmail和运行在智能手机上的Gmail客户端等都是电子邮件用户代理. 当Alice完成邮件撰写时,她的邮件代理向其邮件服务器发送邮件,此时邮件放在邮件服务器的外出报文队列. 当Bob要阅读代理在其邮件服务器的邮箱中取得该报文.
邮件服务器形成了电子邮件体系结构的核心. 每个接收方(如Bob)在其中的某个邮件服务器上有一个邮箱(mail box). Bob的邮箱管理和维护着发送给他的报文. 一个典型的邮件发送过程是:从发送方的用户代理开始,传输到发送方的邮件服务器,再传输到接收方的邮件服务器,然后在这里被分发到接收方的邮箱中. 当Bob要在他的邮箱中读取该报文时,包含他邮箱的邮件服务器(使用用户名和口令)鉴别其身份. Alice的邮箱也必须能处理Bob的邮件服务器的故障. 如果Alice的服务器不能将邮件交付给Bob的服务器(比如Bob的邮件服务器发生了故障),Alice的邮件服务器在一个报文队列(message queue)中保持该报文并在以后尝试再次发送. 通常每30分钟左右进行一次和尝试,如果几天后仍不能成功,服务器就删除该报文并以电子邮件的形式通知发送方(Alice).

SMTP是因特网电子邮件中主要的应用层协议. 它使用TCP可靠数据传输服务,从发送方的邮件服务器向接收方的邮件服务器发送邮件. 像大多数应用层协议一样,SMTP也有两个部分:运行在发送方邮件服务器的客户端和运行在接收方邮件服务器的服务器端. 每台邮件服务器上既运行SMTP的客户端也运行SMTP的服务器端. 当一个邮件其他邮件服务器发送邮件时,它服务器向就表现为SMTP的客户;当一个邮件服务器从其他邮件服务器上接收邮件时,它就表现为SMTP的服务器.
SMTP
RFC 5321给出了SMTP的定义. SMTP是因特网电子邮件的核心. 如前所述,SMTP用于从发送方的邮件服务器发送报文到接收方的邮件服务器. SMTP问世的时间比HTTP要长得多(初始的SMTP的RFC可追溯到1982年,而SMTP在此之前很长一段时间就已经出现了). 尽管电子邮件应用在因特网上的独特地位可以证明SMTP有着众多非常出色的性质,但它所具有的某种陈旧特征表明它仍然是一种继承的技术. 例如,它限制所有邮件报文的体部分(不只是其首部)只能采用简单的7比特ASCII表示. 在20世纪80年代早期,这种限制是明智的,因为当时传输能力不足,没有人会通过电子邮件发送大的附件或大的图片、声音、视频文件然而,在今天的多媒体时代,7比特ASCII的限制的确有点痛苦,即在用SMTP传送邮件之前,需要将二进制多媒体数据编码为ASCI码,并且在使用SMTP传输后要求将相应的ASCII码邮件解码还原为多媒体数据. 2.2节讲过,使用HTTP传送前不需要将多媒体数据编码为ASCI码.
为了描述SMTP的基本操作,我们观察一种常见的情景. 假设Alice想给Bob发送一封简单的ASCII报文.
Alice调用她的邮件代理程序并提供Bob的邮件地址(例如bob@someschool.edu),撰写报文,然后指示用户代理发送该报文.
Alice的用户代理把报文发到她的邮件服务器,在那里该报文被放在报文队列中.
运行在Alice的邮件服务器上的SMTP客户发现了报文队列中的这个报文,它创建一个到运行在Bob的邮件服务器上的SMTP服务器的TCP连接.
在经过一些初始SMTP握手后,SMTP客户通过该TCP连接发送Alice的报文.
在Bob的邮件服务器上,SMTP的服务器接收该报文. Bob的邮件服务器然后将该报文放入Bob的邮箱中.
在Bob方便的时候,他调用用户代理阅读该报文.
需要注意,SMTP一般不使用中间邮件服务器发送邮件,即使这两个邮件服务器位于地球的两端也是这样. 假设Alice的邮件服务器在中国香港,而Bob的服务器在美国圣路易斯,那么这个TCP连接也是从香港服务器到圣路易斯服务器之间的直接相连. 特别是,如果Bob的邮件服务器没有开机,该报文会保留在Alice的邮件服务器上并等待进行新的尝试,这意味着邮件并不在中间的某个邮件服务器中存留.
我们现在仔细观察一下,SMTP是如何将一个报文从发送邮件服务器传送到接收邮件服务器的. 首先,客户端SMTP(运行在发送邮件服务器主机上)在25号端口建立一个到服务器SMTP(运行在接收邮件服务器主机上)的TCP连接. 如果服务器没有开机,客户端SMTP服务会在稍后继续尝试连接. 一旦连接建立,服务器和客户执行某些应用层的握手(就像人们在相互交流前先进行自我介绍一样). SMTP的客户和服务器在传输信息前先相互介绍. 在SMTP握手的阶段,客户端SMTP指示发送方的邮件地址和接收方的邮件地址. 一旦该SMTP客户和服务器彼此介绍之后,客户端SMTP服务发送该报文. SMTP能依赖TCP提供的可靠数据传输无差错地将邮件投递到接收服务器. 该客户如果有另外的报文要发送到该服务器,就在该相同的TCP连接上重复这种处;否则,它指示TCP关闭连接.
接下来我们分析一个在SMTP客户端(C)和SMTP服务器(S)之间交换报文文本的例子. 客户的主机名为crepes.fs,服务器的主机名为hamburger.edu. 以C:
开头的ASCII码文本行正是客户交给其TCP套接字的那些行,以S:
开头的ASCII码文本则是服务器发送给其TCP套接字的那些行. 一旦创建了TCP连接,就开始下列过程:
S: 220 hamburger.edu
C: HELO crepes.fr
S: 250 Hello crepes.fr,Pleased to meet YoU
C: MAIL FROM:<alice@crepes.fs>
S: 250 alice@crepes.fr ... Sender ok
C: RCPT TO: <bob@hamburger.edu>
S: 250 bob@hamburger.edu...Recipient ok
C: DATA
S: 354 Enter mail,end with "." on a line by itself
C: DO you like ketchup?
S: How about Pickles?
C: .
S: 250 Message accepted for delivery
C: QUIT
S: 221 hamburger.edua closing connection
在上例中,客户从邮件服务器crepes.fr在向邮件服务器hamburger.edu发送了一个报文(Do you like ketchup?How about pickles?). 作为对话的一部分,该客户发送了5条命令:HELO(是HELLO的缩写)、MAILFROM、RCPTTO、DATA以及QUIT. 这些命令都是自解释的. 该客户通过发送一个只包含一个句点的行,向服务器指示该报文结束了. (按照ASCII码的表示方法,每个报文以CRLF.CRLF结束,其中的CR和IF分别表示回车和换行. )服务器对每条命令做出回答,其中每个回答含有一个回答码和一些(可选的)英文解释. 我们在这里指出SMTP用的是持续连接:如果发送邮件服务器有几个报文发往同一个接收邮件服务器,它可以通过同一个TCP连接发送所有这些报文. 对每个报文,该客户用一个新的MAIL FROM:crepes.re
开始,用一个独立的句点指示该邮件的结束,并且仅当所有邮件发送完后才发送QUIT.
我们强烈推荐你使用Telnet与一个SMTP服务器进行一次直接对话. 使用的命令是telnet ServerName 25
其中serverName是本地邮件服务器的名称. 当你这么做时,就直接在本地主机与邮件服务器之间建立了一个TCP连接. 输完上述命令后,你立即会从该服务器收到220回答. 接下来,在适当的时机发出HELO、MAIL FROM、RCPT TO、DATA、CRLF.CRLF以及QUIT等SMTP命令.
强烈推荐你做本章后面的编程作业3. 在该作业中,你将在SMTP的客户端实现一个简单的用户代理,它允许你经本地邮件服务器向任意的接收方发送电子邮件报文.
作业3:邮件客户
这个编程作业的目的是创建一个向任何接收方发送电子邮件的简单邮件客户. 你的客户将必须与邮件服务器(如谷歌的电子邮件服务)创建一个TCP连接,使用SMTP协议与邮件服务器进行交谈,经该邮件服务器向某接收方(如你的朋友)发送一个电子邮件报文,最后关闭与该邮件服务器的TCP连接.
对本作业,配套Web站点为你的客户提供了框架代码. 你的任务是完善该代码并通过向不同的用户账户发送电子邮件来测试你的客户. 你也可以尝试通过不同的服务器(例如谷歌的邮件服务器和你所在大学的邮件服务器)进行发送.
邮件报文格式
当Alice给Bob写一封邮寄时间很长的普通信件时,她可能要在信的上部包含各种各样的环境首部信息,如Bob的地址、她自己的回复地址以及日期等. 类似地,当一个人给另一个人发送电子邮件,一个包含环境信息的首部位于报文体前面. 这些环境信息包括在一系列首部行,这些行由RFC 5322
定义. 首部行和该报文的体用空(即\t\n
)进行分隔. RFC 5322
定义了邮件首部行和它们的语义解释的精确格式. 如同HTTP一样,每个首部行包含了可读的文本,是由关键词后跟冒号及其值组成的. 某些关键词是必需的,另一些则是可选的. 每个首部必须含有一个From:
首部行和一个To:
首部行,一个首部也许包含一个Subjeet:
首部行以及其他可选的首部行. 注意:这些首部行不同于我们在2.3.1节所学到的SMTP命令(即使那里包含了某些相同的词汇,如from和to). 那节中的命令是SMTP握手协议的一部分. 本节中考察的首部行则是邮件报文自身的一部分.
一个典型的报文首部如下:
From: alice@crepes.fr
TD: bob@hamburger.edu
Subject: Searching for the meaning of 1ife
在报文首部之后,紧接一个空白行,然后是ASCII格式表示的报文体. 你应当用telnet向邮件服务器发送包含一些首行部的报文,包括Subject:
首部行.
邮件访问协议
一旦SMTP将邮件报文从Alice的邮件服务器交付给Bob的邮件服务器,该报文就被放入了Bob的邮箱中. 假设Bob(接收方)在其本地主机(如智能手机或PC)上运行用户代理程序,考虑在他的本地PC上也放置一个邮件服务器是自然而然的事,在这种情况下,Alice的邮件服务器就能直接与Bob的PC进行对话了. 然而这种方法会有一个问题:前面讲过邮件服务器管理用户的邮箱,并且和运行SMTP的客户端和服务器端. 如果Bob的邮件服务器位于他的PC上,那么为了能够及时接收可能在任何时候到达的新邮件,他的PC必须总是不间断地运行着并一直保持在线. 这对于许多因特网用户而言是不现实的. 相反,典型的用户通常在本地PC上运行一个用户代理程序,它访问存储在总是保持开机的共享邮件服务顺上的邮箱. 该邮件服务顺与其他用户共享.
现在我们考虑当从Alice向Bob发送一个电子邮件报文时所采取的路径. 我们刚才已经知道,在沿着该路径的某些点上,需要将电子邮件报文存放在Bob的邮件服务器上. 通过让Alice的用户代理直接向Bob的邮件服务器发送报文,就能够做到这一点. 然而,通常Alice的用户代理和Bob的邮件服务器之间并没有一个直接的SMTP对话. 相反,如图2-16所示,Alice的用户代理用SMTP或HTTP将电子邮件报文推入她的邮件服务器,接着她的邮件服务器(作为一个SMTP客户)再用SMTP将该邮件中继到Bob的邮件服务需. 为什么该过程要分成两步?主要是因为不通过Alice的邮件服务器进行中继,Alice的用户代理将没有任何办法到达一个不可达的目的地邮件服务器. 通过首先将邮件存放在自己的邮件服务器中,Alice的邮件服务器可以重复地尝试向Bob的邮件服务顺发送该报文,如每30分钟一次直到Bob的邮件服务器变得运行为止. (并且如果Alice的邮件服务器关机,则她能向系统管理员进行申告)
但是对于该难题仍然有一个疏漏的环节,像Bob这样的接收方,是如何通过运行其本地PC上的用户代理,获得位于他的某ISP的邮件服务器上的邮件呢?值得注意的是Bob的用户代理不能使用SMTP得到报文,因为SMTP是一个推协议,取得报文是一个拉操作.
今天,Bob从邮件服务器取回邮件有两种常用方法. 如果Bob使用基于Web的电子邮件或智能手机上的客户端(如Gmail),则用户代理将使用HTTP来取回Bob的电子邮件. 这种情况要求Bob的电子邮件服务器具有HTTP接口和SMTP接口(与Alice的邮件服务器通信). 另一种方法是使用由RFC 3501定义的因特网邮件访问协议(Internet Mail Access Protocol,IMAP),这通常用于微软的Outlook等. HTTP和TMAP方法都支持Bob管理自己邮件服务器中的文件夹,包括将邮件移动到他创建的文件夹中,删除邮件,将邮件标记为重要邮件等.
DNS:因特网的目录服务
因特网上的主机和人类一样,可以使用多种方式进行标识. 主机的一种标识方法是用主机名(hostname),如www.facebook.com、www.google.com、gaia.cs.umass.edu等,这些名字便于记忆也乐于被人们接受. 然而,主机名几乎没有提供(即使有也很少)关于主机在因特网中位置的信息. (一个名为www.eurecom.fr的主机以国家码.fr结束,告诉我们该主机很可能在法国,仅此而已. )况且,主机名可能由不定长的字母数字组成,路由器难以处理. 为此,主机也可以使用所谓的IP地址(IP address)进行标识.
我们将在第4章更为详细地讨论下地址,但现在简略地介绍一下还是有必要的. 一个IP地址(这里仅指IPv4)由4个字节组成,并有着严格的层次结构. 例如121.7.106.83这样一个IP地址,其中的每个字节都被句点分隔开来,表示了0~255的十进制数字. 我们说IP地址具有层次结构,是因为当我们从左至右扫描它时,会得到越来越具体的关于主机位于因特网何处的信息(即在众多网络的哪个网络里). 类似地,当我们从下向上查看邮政地址时,能够获得该地址位于何处的越来越具体的信息.
DNS提供的服务
我们刚刚看到了识别主机有两种方式一一主机名和IP地址. 人们喜欢便于记忆的主机名标识方式,而路由器则喜欢定长的、有着层次结构的IP地址. 为了对这些不同的偏好进行折中,我们需要一种能进行主机名到IP地址转换的目录服务. 这就是域名系统(Domain Name System,DNS)的主要任务. DNS是:
- 一个由分层的DNS服务器(DNS server)实现的分布式数据库.
- 一个使得主机能够查询分布式数据库的应用层协议.
DNS服务器通常是运行了BIND(Berkeley Internet Name Domain)软件[BIND 2020]的UNIX机器. DNS协议运行在UDP之上,使用53号端口.
什么是BIND?
BIND是一款实现DNS服务器的开放源码软件,够提供双向解析,转发,子域授权,view等功能,是世界上使用最为广泛的DNS服务器软件,目前Internet上半数以上的DNS服务器有都是用Bind来架设的.
DNS:通过客户-服务器模式提供的重要网络功能
与HTTP,FTP和SMP一样,DNS协议是应用层协议,其原因在于:
- 使用客户-服务器模式运行在通信的端系统之间
- 在通信的端系统之间通过下面(指下层)的端到端协议来传送DNS报文.
然而,在其他意义上,DNS的作用非常不同于Web应用、文件传输应用以及电子邮件应用. 与这些应用程序的不同之处在于,DNS不是一个直接和用户打交道的应用,而是为因特网上的用户应用程序以及其他软件提供一种核心功能,即将主机名转换为其背后的IP地址. 我们在1.2节就提到,因特网体系结构的复杂性大多数位于网络的“边缘”. DNS通过采用位于网络边缘的客户和服务器,实现了关键的名字到数字的转化功能,他还是这种设计模式的另一个范例.
DNS通常是由其他应用层协议所使用的,包括HTTP和SMTP,将用户提供的主机名解析为下地址. 举一个例子,考虑运行在某用户主机上的一个浏览器(即一个HTTP客户)请求URLwww.someschool.edu/index.html
页面时会发生什么现象. 为了使用户的主机能够将一个HTTP请求报文发送到Web服务器www.someschool.edu
,该用户主机必须获得www.someschool.edu
的IP地址. 其做法如下:
- 同一台用户主机上运行着DNS应用的客户端
- 浏览器从上述URL中抽取出主机名
www.someschool.edu
,并将主机名传给DNS应用的客户端. - DNS客户向DNS服务器发送一个包含主机名的请求.
- DNS客户最终会收到一份回答报文,其中含有对应于该主机名的IP地址.
- 一旦浏览器接收到来自DNS的该IP地址,它就向位于该了IP地址80端口的HTTP服务器进程发起一个TCP连接.
从这个例子中,我们可以看到DNS给使用它的因特网应用带来了额外的时延,有时还相当可观. 幸运的是,如我们下面讨论的那样,想获得的卫IP址通常就缓存在一个“附近的”DNS服务器,这有助于减少DNS的网络流量和DNS的平均时延.
除了进行主机名到IP地址的转换外,DNS还提供了一些重要的服务:
- 主机别名(host aliasing). 有着复杂主机名的主机能拥有一个或者多个别名. 例如,一台名为
relay1.west-coast.enterprise.com
的主机,可能还有两个别名enterprise.com
和www.enterprise.com
. 在这种情况下,relay1.west-coast,enterprise.com也称为规范主机名(canonical hostname). 主机别名(当存在时)比主机规范名更加容易记忆. 应用程序可以调用DNS来获得主机别名对应的规范主机名以及主机的IP地址. - 邮件服务器别名(mail server aliasing). 显而易见,人们也非常希望电子邮件地址好记忆. 例如,如果Bob在雅虎邮件上有一个账户,Bob的邮件地址就像
bob@yahoo.com
这样简单. 然而,雅虎邮件服务器的主机名可能更为复杂,不像yahoo.com
那样简单好记(例如,规范主机名可能像relay1.west-coast.hotmail.com
那样). 电子邮件应用程序可以调用DNS,对提供的主机别名进行解析,以获得该主机的规范主机名及其IP地址. . 事实上,MX记录(参见后面)允许一个公司的邮件服务器和Web服务器使用相同(别名)的主机名,例如,一个公司的Web服务器和邮件服务器都能叫作enterprise.com
. - 负载分配(load distribution). DNS也用于在冗余的服务器(如冗余的Web服务器等)之间进行负载分配. 繁忙的站点(如cnn.com)被冗余分布在多台服务器上(相同服务但是服务器不同),每台服务器均运行在不同的端系统上,每个都有着不同的IP地址. 由于这些冗余的Web服务器,一个IP地址集合与同一个规范主机名相联系. DNS数据库中存储着这些IP地址集合. 当客户对映射到某地址集合的名字发出一个DNS请求时,该服务器用IP地址的整个集合进行响应,但在每个回答中循环这些地址次序. 因为客户通常总是向IP地址排在最前面的服务器发送HTTP请求报文,所以DNS就在所有这些冗余的Web服务器之间循环分配了负载. DNS的循环同样可以用于邮件服务器,因此多个邮件服务器可以具有相同的别名. 一些内容分发公司也以更复杂的方式使用DNS,以提供Web内容分发(参见2.6.3节).
DNS由RFC 1034和RFC 1035定义,并且在几个附加的RFC中进行了更新. DNS是一个复杂的系统,我们在这里只是就其运行的主要方面进行学习. 感兴趣的读者可以参考这些RFC文档以及Albitz和Liu的书 [Albitz 1993] ,亦可参阅文章 [Mockapetris 1998] 和 [Mockapetris 2005] ,其中 [Mockapetris 1998] 是回顾性的文章,它对DNS组成和工作原理进行了细致的讲解.
DNS工作机理概述
下面给出一个DNS工作过程的总体概述,我们的讨论将集中在主机名到IP地址转换服务方面.
假设运行在用户主机上的某些应用程序(如Web浏览器或邮件阅读器)需要将主机名转换为IP地址. 这些应用程序将调用DNS的客户端,并指明需要被转换的主机名(在很多基于UNIX的机器上,应用程序为了执行这种转换需要调用函数gethostbyname()
). 用户主机上的DNS接到后,向网络中发送一个DNS查询报文. 所有的DNS请求和回答报文使用UDP数据报经端口53发送. 经过若干毫秒到若干秒的时延后,用户主机上的DNS接收到一个提供所希望映射的DNS回答报文. 这个映射结果则被传递到调用DNS的应用程序. 因此,从用户主机上调用应用程序的角度看,DNS是一个提供简单、直接的转换服务的黑盒子. 但事实上,实现这个服务的黑盒子非常复杂,它由分布于全球的大量DNS服务器以及定义了DNS服务器与查询主机通信方式的应用层协议组成.
DNS的一种简单设计是在因特网上只使用一个DNS服务器,该服务器包含所有的映射. 在这种集中式设计中,客户直接将所有查询直接发往单一的DNS服务器,同时该DNS服务器直接对所有的查询客户做出响应. 尽管这种设计的简单性非常具有吸引力,但它不适用于当今的因特网,因为因特网有着数量巨大(并持续增长)的主机. 这种集中式设计的问题包括:
- 单点故障〈single point of failure):如果该DNS服务器崩溃,整个因特网随之瘫痪.
- 通信容量(traffic volume): . 单个DNS服务器不得不处理所有的DNS查询(用于为上亿台主机产生的所有HTTP请求报文和电子邮件报文服务).
- 远距离的集中式数据库(distant centralized database):单个DNS服务器不可能“邻近”所有查询客户. 如果我们将单台DNS服务器放在纽约市,那么所有来自澳大利亚的查询必须传播到地球的另一边,中间也许还要经过低速和拥塞的链路. 这将导致严重的时延.
- 维护(maintenance):单个DNS服务器将不得不为所有的因特网主机保留记录. 这不仅将使这个中央数据库无比庞大,而且它还不得不为解决每个新添加的主机而频繁更新.
总的来说,在单一DNS服务器上运行集中式数据库完全没有可扩展能力. 因此,DNS采用了分布式的设计方案. 事实上,DNS是一个在因特网上实现分布式数据库的精彩范例.
1.分布式,层次数据库
为了处理扩展性问题,DNS使用了大量的DNS服务器,它们以层次方式组织并且分布在全世界范围内. 没有一台DNS服务器拥有因特网上所有主机的映射,这些映射分布在所有的DNS服务器上. 大致说来,有3种类型的DNS服务器:根DNS服务器、顶级域(Top-Level Domain,TLD)DNS服务器和权威DNS服务器. 这些服务器以图2-17中所示的层次结构组织起来. 为了理解这3种类型的DNS服务器交互的方式,假定一个DNS客户要确定主机名www.amazon.com
的IP地址. 粗略说来,将发生下列事件.
- 客户首先与根服务器之一联系,它将返回顶级域名com的TLD服务器的IP地址.
- 该客户与这些TLD服务器之一联系,它将为
amazon.com
返回权威服务器的IP地址. - 最后,该客户与
amazon.com
权威服务器之一联系,它为主机名www.amazon.com
返回IP地址. 我们将很快更为详细地考察DNS查找过程. 不过我们先仔细看一下这3种类型的DNS服务器.

- 根DNS服务器:有超过1000台根DNS服务器实体遍及全世界. 这些根服务器是13个不同根服务器的副本,由12个不同组织管理,并通过因特网号码分配机构来协调 [IANA 2020]. 根名字服务器的全部清单连同管理它们的组织及其下地址可以在 [Root Servers 2020] 中找到. 根服务器提供 TLD 服务的 IP地址.
- 顶级域(TLD)DNS服务:对于每个顶级域(如com、org、net、edu和gov)和所有国家的顶级域(如uk、fr、cn和jp等),都有TLD服务器(或服务器集群). Verisign Global Registry Services公司维护com顶级域的TLD服务器,Educause公司维护edu项级域的TLD服务器. 支持TLD的网络基础设施可能是大而复杂的, [Osterweil 2012] 对Verisign网络进行了很好的概述. 所有项级域的列表参见[TLD list 2020]. TLD服务器提供了权威DNS服务器的IP地址.
- 权威DNS服务器:在因特网上具有公共可访问主机(如Web服务器和邮件服务器)的每个组织机构必须提供公共可访问的DNS记录,这些记录将这些主机的名字上映射为IP地址. 一个组织机构的权威DNS服务器收藏了这些DNS记录. 一种方法是,一个组织机构可以选择实现自己的权威DNS服务器以保存这些记录;另一种方法是,该组织能够支付费用,让这些记录存储在某个服务提供商的一个权威DNS服务器中. 多数大学和大公司实现并维护它们自己的基本和辅助(备份)的权威DNS服务器.
根、TLD和权威DNS服务器都处在该DNS服务器的层次结构中,如图2-17所示. 还有另一类重要的DNS服务器,称为本地DNS服务器(local DNS server). 严格说来,一个本地DNS服务器并不属于该服务器的层次结构,但它对DNS层次结构是至关重要的. 每个ISP(如一个居民区的ISP或一个机构的ISP)都有一台本地DNS服务器(也叫默认名字服务器). 当主机与某个ISP连接时,该ISP提供一台主机的卫地址,该主机具有一台或多台其本地DNS服务器的IP地址(通常通过DHCP,将在第4章中讨论). 通过访问Windows或UNIX的网络状态窗口,用户能够容易地确定自己的本地DNS服务器的IP地址.主机的本地DNS服务器通常“邻近”本主机. 对某机构ISP而言,本地DNS服务器可能就与主机在同一个局域网中;对于某居民区ISP来说,本地DNS服务器通常与主机相隔不超过几台路由器. 当主机发出DNS请求时,该请求被发往本地DNS服务器,它起着代理的作用,并将该请求转发到DNS服务器层次结构中,下面我们将更为详细地讨论.

我们来看一个简单的例子,假设主机cse.nyu.edu
想知道主机gaia.cs.umass.edu
的 IP地址. 同时假设纽约大学(NYU)的cse.nyu.edu
主机的本地DNS服务器为dns.nyu.edu
,并且gaia.cs.umass.edu
的权威DNS服务器为dns.umass.edu
. 如图2-18所示,主机cse.nyu.edu
首先向它的本地DNS服务器dns.nyu.edu
发送一个DNS查询报文. 该查询报文含有被转换的主机名gaia.cs.umass.edu
. 本地DNS服务器将该报文转发到根DNS服务器. 该根DNS服务器注意到其edu后缀并向本地DNS服务器返回负责edu的TLD服务器的IP地址列表.该本地DNS服务器则再次向这些TLD服务器之一发送查询报文.该TLD服务器注意到umass.edu
后缀,并用权威DNS服务器的IP地址进行响应,该权威DNS服务器是负责马萨诸塞大学的dns.umass.edu
.最后,本地DNS服务器直接向dns.umass.edu
重发查询报文,dns.umass.edu
用gaia.cs.umass.edu
的IP地址进行响应. 注意到在本例中,为了获得一台主机名的映射,共发送了8份DNS报文:4份查询报文和4份回答报文!我们将很快看到利用DNS缓存减少这种查询流量的方法.
前面的例子假设了TLD服务器知道用于主机的权威DNS服务器的IP地址. 一般而言,这种假设并不总是正确. 相反,TLD服务器只是知道中间的某个DNS服务器,该中间DNS服务器才可能能知道用于该主机的权威DNS服务器,若不知道,则接着查询下一个中间服务器. 例如,再次假设马萨诸塞大学有一台用于本大学的DNS服务器,称为dns.umass.edu
. 同时假设该大学的每个系都有自己的DNS服务器,每个系的DNS服务器是本系所有主机的权威服务器. 在这种情况下,当中间DNS服务器dns.umass.edu
收到了对某主机的请求时,该主机名是以cs.umass.edu
结尾,它向dns.nyu.edu
(前面提到的请求者的本地DNS服务器)返回dns.cs.umass.edu
的IP地址,后者是所有以cs.umass.edu
结尾的主机的权威服务器. 本地DNS服务dns.nyu.edu
则向权威DNS服务器发送查询,该权威DNS服务器向本地DNS服务器返回所希望的映射,该本地服务次向请求主机返回该映射. 在这个例子中,共发送了10份DNS报文. 相当于在访问权威DNS服务器时可能存在中间DNS服务器的情况.
图2-18所示的例子利用了递归查询(recursive query)和迭代查询(iterative query). 从cse.nyu.edu
到dns.nyu.edu
发出的查询是递归查询,因为该查询以自己的名义请求dns.nyu.edu来获得该映射. 而后继的3个查询是迭代查询,因为所有的回答都是直接返回给dns.nyu.edu. 从理论上讲,任何DNS查询既可以是迭代的也可以是递归的. 例如,图2-19显示了一条DNS查询链,其中的所有查询都是递归的. 实践中,查询通常遵循图2-18中的模式:从请求主机到本地DNS服务器的查询是递归的,其余的查询是迭代的.

2.DNS缓存
至此我们的讨论一直忽略了DNS系统的一个非常重要的特色:DNS缓存(DNS caching). 实际上,为了改善时延性能并减少在因特网上到处传输DNS报文数量,DNS广泛使用了缓存技术. DNS缓存的原理非常简单. 在一个请求链中,当某DNS服务器接收一个DNS回答(例如,包含某主机名到IP地址的映射)时,它就能将映射缓存在本地存储器中. 例如,在图2-18中,每当DNS服务器dns.nyu.edu从某个DNS服务器接收到一个回答,他就能够缓存包含在该回答中的任何信息.如果在DNS服务器中缓存了一个主机名/IP地址对,另一个对相同主机名的查询到达该DNS服务器时,该DNS服务器就能够提供所要求的IP地址,即使它不是该主机名的权威服务器. 由于主机和主机名与耻地址间的映射并不是永久的,DNS服务器在一段间后将丢弃缓存的信息.
DNS记录和报文
共同实现DNS分布式数据库的所有DNS服务器存储了资源记录(Resource Record,RR),RR提供了主机名到IP地址的映射(或者主机名到另一主机名的映射,后述). 每个DNS回答报文包含了一条或多条资源记录. 在本小节以及后续小节中,我们概要地介绍DNS资源记录和报文,更详细的信息可以在 [Albitz 1993] 或有关DNS的REFC文档 [RFC 1034,RFC 1035] 中找到.
资源记录是一个包含了下列字段的4元组:
(Name, Value, Type, TTL)
TTL(Time To Life)是该记录的生存时间,它决定了资源记录应当从缓存中删除的时间. 在下面给出的记录例子,我们忽略掉TTL字段. Name和Value的意义取决于Type:
如果
Type=A
,则对该主机名而言Name是主机名,Value是该主机名对应的IP地址. 因此,一条类型为A的资源记录提供了标准的主机名到IP地址的映射. 例如(relay1.bar.foo.com, 145.37.93.126, A)
就是一条类型A的记录.如果
Type=NS
,则对该域中的主机而言Name是域(如foo.com),而Value是一个知道如何获得该域中主机IP地址的权威DNS服务器的主机名. 这个记录用于委托域名解析权,即将该域名移交给其他DNS服务器解析. 例如(foo.com, dns.foo.com, NS)
就是一条类型为NS的记录.如果
Type=CNAME
,则Value是主机别名Name对应的规范主机名. 该记录能够向查询的主机提供一个主机名对应的规范主机名,例如(foo.com, relay1.bar.foo.com, CNAME)
就是一条CNAME类型的记录.实际上是把一个域名指向另一个域名.
如果
Type=MX
,则Value是一个别名为Name的邮件服务器的规范主机名. 举例来说,(foo.com, mail.bar.foo.com, MX)
就是一条MX记录. MX记录人允许邮件服务器主机名有具有简单的别名. 值得注意的是,通过使用MX记录,一个公司的邮件服务器和其他服务器(如它的Web服务器)可以使用相同的别名. 为了获得邮件服务器的规范主机名,DNS客户应当请求一条MX记录;而为了获得其他服务器的规范主机名,DNS客户应当请求CNAME记录.也就是说,在以邮件服务为目的检索foo.com时,会请求MX记录,再根据其邮件服务器的规范主机名来查询其IP;以其他服务为目的检索foo.com时,则会请求CNAME记录,得到规范主机名,再查询其IP地址.
如果一台DNS服务器是某特定主机名的权威DNS服务器,那么该DNS服务器会有一条包含用于该主机名的类型A记录(即使该DNS服务器不是其权威DNS服务器,它也可能在缓存中包含几条类型A记录). 如果服务器不是用于某主机名的权威服务器,那么该服务器将包含一条类型NS记录,该记录对应于包含主机名的域;它还将包含一条类型A记录,该记录提供了在NS记录的Value字段中的DNS服务器的IP地址.
举例来说,假设一台edu TLD服务器不是主机gaia.cd.umass.edu
的权威DNS服务器,则该服务器将包含一条主机gaia.cs.umass.edu
的域记录,如(umass.edu, dns.umass.edu, NS)
;该edu TLD服务器还将包含一条类型A记录,如(dns.umass.edu, 128.119.40.111, A)
, 该记录将名字dns.umass.edu
映射为一个IP地址.
1.DNS报文
在本节前面,我们提到了DNS查询和回答报文. DNS只有这两种报文,并且查询和回答报文有着相同的格式,如图2-20所示.

DNS报文中的各个字段语义如下:
前12字节是首部区域,其中有几个字段.
- 第一个字段(标识符/id)是一个16bit的数,用于标识该查询. 这个标识符会被复制到对查询的回答报文中,以便让客户用它来匹配发送的请求和接收到的回答. 标志字段中含有若干标志.
- 标志位有以下几种:
“查询/回答(Response,QR)”
标志位指出报文是查询报文(0)还是回答报文(1).操作码(Opcode)
中,0表示标准查询,1表示反向查询,2表示服务器状态请求.- 当某DNS服务器是所请求名字的权威DNS服务器时,1bit的
“权威的(Authoritative,AA)”
标志位被置于回答报文中,以标志该响应服务器就是查询主机名的权威服务器. TC(Truncated)
表示是否被截断. 值为1时,表示响应已超过512字节并已被截断,只返回前512个字节.- 如果客户(主机或者DNS服务器)在该DNS服务器没有某记录时,它将执行递归查询,并设置1bit的
“希望递归(Recursion Desired, RD)”
标志位. - 如果该DNS服务器支持递归查询,在它的回答报文中会设置1bit的
“递归可用(Recursion Available, RA)”
标志位. Z
是保留字段,在所有的请求和应答报文中,它的值必须为0.rcode(Reply code)
是返回码字段,表示响应的差错状态. 当值为0时,表示没有错误;当值为1时,表示报文格式错误,服务器不能理解请求的报文;当值为2时,表示域名服务器失败,因为服务器的原因导致没办法处理这个请求;当值为3时,表示名字错误,只有对授权域名解析服务器有意义,指出解析的域名不存在;当值为4时,表示查询类型不支持,即域名服务器不支持查询类型;当值为5时,表示拒绝,一般是服务器由于设置的策略拒绝给出应答,如服务器不希望对某些请求者给出应答.
在该首部中还有4个有关数量的字段.这些字段指出了在首部后的4类数据区域出现的数量.
问题区域:包含了正在进行的查询信息. 该区域包括:
- 名字字段,包含正在被查询的主机名字;
- 类型字段,指出有关该名字的正被询问的问题类型
例如主机地址是与个名字相关联(类型A)还是与某个名字的邮件服务器相关联(类型MX).
回答区域:包含了对最初请求的Name的资源记录. 前面讲过每个资源记录中有Type(如A、NS、CNAME和MX)字段、Value字段和TTL字段. 在回答报文的回答区域中可以包含多条RR,因此一个主机名能够有多个IP地址(例如,就像本节前面讨论的冗余Web服务器).
权威区域:包含了其他权威服务器的信息(注意是其他权威服务器的信息不是该权威服务器的其他信息).
附加信息区域:包含了其他有帮助的记录. 例如,对于一个MX请求的回答报文的回答区域包含了一条资源记录,该记录提供了邮件服务器的规范主机名. 该附加信息包含一个类型A记录,该记录提供了用于该邮件服务器的规范主机名的IP地址.
DNS报文的详细解释,实例等可以查看DNS报文格式解析(非常详细)
使用nslookup(nslookup program)可以从正在工作的主机直接向某些DNS服务器发送一个DNS查询. 对于多数Windows和UNIX平台,nslookup程序是可用的. 例如,从一台Windows主机打开命令提示符界面,直接键人nslookup即可调用nslookup程序. 在调用nslookup后,你能够向任何DNS服务器(根、TLD或权威)发送DNS查询. 在接收到来自DNS服务器的回答后,nslookup将显示包括在该回答中的记录(以人可读的格式). 从你自己的主机运行nslookup还有一种方法,即访问允许你远程应用nslookup的许多Web站点之一(在一个搜索引擎中键入nslookup就能够得到这些站点中的一个). 本章最后的DNS Wireshark实验将使你更为详细地研究DNS.
2.在DNS数据库中插入记录
上面的讨论只是关注如何从DNS数据库中取数据. 你可能想知道这些数据最初是怎么进入数据库中的. 我们在一个特定的例子中看看这是如何完成的. 假定你刚刚创建了一个称为网络乌托邦(Network Utopia)的令人兴奋的创业公司. 你必定要做的第一件事是在注册登记机构注册域名networkutopia.com
. 注册登记机构(registrar)是一个商业实体,它验证该域名的唯一性,将该域名输入DNS数据库(如下面所讨论的那样),对提供的服务收取少量费用. 1999年前,唯一的注册登记机构是Nework Solutions,它独家经营对于com、net和org域名的和注册. 但是现在有许多注册登记机构竞争客户,因特网名字和地址分配机构(Internet Corporation for Assigned Names and Numbers, ICANN)向各种注册登记机构授权. 在http://www.internic.net
上可以找到授权的注册登记机构的完整列表.
当你向某些注册登记机构注册域名networkutopia.com
时,需要向该机构提供你的基本、辅助权威DNS服务器的名字和IP地址. 假定该名字和IP地址是dns1.networkutopia.com
和dns2.networkutopia.com
及212.212.212.1和212.212.212.2. 对这两个权威DNS服务器的每一个,该注册登记机构确保将一个 类型NS 和一个 类型A 的记录输入TLD com服务器. 特别是对于用于networkutopia.com
的基本权威服务器,该注册登记机构将下列两条资源记录插和人DNS系统中:
(networkutopia.com, dns1.networkutopia.com, NS)
(dns1.networkutoepia.com, 212.212.212.1, A)
你还必须确保用于Web服务器www.networkutopia.com
的类型A资源记录和用于邮件服务器mail.networkutopia.com
的类型MX资源记录被输入你的权威DNS服务器中. [最近,DNS协议中添加了一个更新(UPDATE)选项,允许通过DNS报文对数据库中的内容进行动态添加或者删除. [RFC 2136]和[RFC 3007]定义了DNS动态更新. ]
一旦完成所有这些步骤,人们将能够访问你的Web站点,并向你公司的雇员发送电子邮件. 我们通过验证该说法的正确性来总结DNS的讨论. 这种验证也有助于充实我们已经学到的DNS知识. 假定在澳大利亚的Alice要观看www.networkutopia.com
的Web页面. 如前面所讨论,她的主机将首先向其本地DNS服务器发送请求. 该本地服务器接着联系一个TLD com服务器. (如果TLD com服务器的地址没有被缓存,该本地DNS服务器也将必须与根DNS服务器相联系. )该TLD服务器包含前面列出的类型NS和类型A资源记录,因为注册登记机构将这些资源记录搬入所有的TLD com服务器. 该TLD com服务器向Alice的本地DNS服务器发送一个回答,该回答包含了这两条资源记录. 本地DNS服务需则加212.212.212.1发送一个DNS查询,请求对应于www.networkutopia.com
的类型A记录. 该记录提供了所和希望的Web服务器的IP地址,如212.212.71.4,本地DNS服务器将该地址回传给Alice的主机. Alice的浏览器此时能够向主机212.212.71.4发起一个TCP连接,并在该连接上发送一个HTTP请求.
DNS脆弱性
我们已经看到DNS是因特网基础设施的一个至关重要的组件,对于包括Web、电子邮件等的许多重要的服务,没有它都不能正常工作. 因此,我们自然要问:DNS会受到攻击吗?DNS是一个易受攻击的目标吗?它是将会被淘汰的服务吗?大多数因特网应用会随之一起无法工作吗?
第一种针对DNS服务的攻击是分布式拒绝服务(DDoS)带宽洪泛攻击. 倒如,某攻击者可能试图向每个DNS根服务器发送大量的分组,使得大多数合法DNS请求得不到回答. 这种对DNS根服务器的DDoS大规模攻击实际发生在2002年10月21日. 在这次攻击中,攻击者利用用一个僵尸网络向13个DNS根服务器中的每个都发送了大批的ICMP ping报文负载. (5.6节中讨论ICMP报文. 此时,知道ICMP分组是特殊类型的IP数据报就可以了.)幸运的是,这种大规模攻击所带来的损害很小,对用户的因特网体验几乎没有或根本没有影响. 攻击者的确成功地将大量的分组指向了根服务器,但许多DNS根服务器受到了分组过滤器的保护,配置的分组过滤器阻挡了所有指向根服务器的ICMP ping报文.这些被保护的服务器因此未受伤并且与平常一样发挥着作用. 此外,大多数本地DNS服务器缓存了顶级域名服务器的IP地址,使得这些请求过程通常为DNS根服务器分流.
对DNS的更为有效的潜在DDoS攻击将是向顶级域名服务器(例如向所有处理.com域的顶级域名服务器)发送大量的DNS请求. 过滤指向DNS服务器的DNS请求将更为困难,并且顶级域名服务器不像根服务器那样容易绕过. 这种对顶级域名服务提供商的攻击发生在2016年10月21日. 该DDoS攻击是通过发送大量的DNS查找请求进行的,这些请求来自一个由十万多个物联网设备组成的僵尸网络,这些设备包括被Miral恶意软件感染的打印机、网络相机、住宅网关和婴儿监视器等. 攻击几乎持续了一整天,亚马逊、推特、Netflix、GitHub和Spotify都受到了干扰.
DNS也可能潜在地以其他方式被攻击. 在中间人攻击中,攻击者截获来自主机的请求并返回伪造的回答. 在DNS投毒攻击中,攻击者向一台DNS服务器发送伪造的回答,诱使服务器在它的缓存中接收伪造的记录. 这些攻击中的任意一种都可能被用于不良用途,例如将没有疑心的Web用户重定向到攻击者的Web站点. DNS安全扩展套件(已经设计并部署了DNSSEC[Gieben 2004;RFC 4033])用于防范这些漏洞. 作为DNS的安全版本,DNSSEC处理了许多类似这样的攻击并在因特网上得到了普及.
P2P文件分发
到目前为止本章中描述的应用(包括Web、电子邮件和DNS)都采用了客户-服务器体系结构,极大地依赖于总是打开的基础设施服务器. 在2.1.1节讲过,使用P2P体系结构,对总是打开的基础设施服务器依赖最少(或者没有依赖). 与之相反,成对间歇连接的主机(称为对等方)彼此直接通信. 这些对等方并不为服务提供商所拥有,而是受用户控制的计算机.
在本节中我们将研究一个非常自然的P2P应用,即从单一服务器向大量主机(称为对等方)分发一个大文件. 该文件也许是一个新版的Linux操作系统,也许是对于现有操作系统或应用程序的一个软件补丁,或一个MPEG视频文件. 在客户-服务器文件分发中,该服务器必须向每个对等方发送该文件的一个副本,即服务器承受了极大的负担,并且消耗了大量的服务器带宽. 在P2P文件分发中,每个对等方能够向任何其他对等方重新分发它已经收到的该文件的任何部分,从而在分发过程中协助该服务器. 到2020年止,最为流行的P2P文件分发协议是BitTorrent. 该应用程序最初由Bram Cohen研发,现不在有许多不同的独立且符合BitTorrent协议的BitTorrent客户,就在有许多像有许多符合HTTP协议的Web浏览器客户一样. 在下面的小节中,我们首先考察在文件分发环境中P2P体系结构的自扩展性. 然后我们更为详细地描述BitTorrent,突出它的最为重要的特性.
1.P2P体系结构的扩展性
为了将客户-服务器体系结构与P2P体系结构进行比较,阐述P2P的内在自扩展性,我们现在考虑一个用于两种体系结构类型的简单定量模型,将一个文件分发给一个固定对等方集合. 如图2-21所示,服务器和对等方使用接入链路与因特网相连. 其中$u_s$表示服务器接入链路的上载速率,$u_i$表示第i对等方接入链路的上载速率,$d_i$表示第i对等方接入链路的下载速率. 用F表示被分发的文件长度(以bit计),N表示要获得该文件副本的对等方的数量. 分发时间(distribution time)是所有N个对等方得到该文件的副本所需要的时间. 在下面分析分发时间的过程,我们对客户-服务器和P2P体系结构做了简化(并且通常是准确的[Akela 2003])的假设,即因特网核心具有足够的带宽,这意味着所有瓶颈都在网络接入链路. 我们还假设服务器和客户没有参与任何其他网络应用,因此它们的所有上传和下载访问带宽能被全部用于分发该文件.

我们首先来确定对于客户-服务器体系结构的分发时间,我们将其表示为$D_{cs}$. 在客户-服务器体系结构,没有对等方帮助分发文件. 那么情况大致如下:
- 服务器必须向N个对等方的每个传输该文件的一个副本. 因此该服务器必须传输
NF bit
量值的数据. 因为该服务器的上载速率是$u_s$,分发该文件的时间必定至少为$NF/u_{s}$. - 令$d_{min}$表示具有最小下载速率的对等方的下载速率,即$d_{min}=min{d_1, d_2, …, d_N}$. 具有最小下载速率的对等方不可能在少于$F/d_{min}$s的时间内获得该文件的所有F bit. 因此最小分发时间至少为$F/d_{min}$s.
将以上两条综合,我们就可以得到:$$D_{cs} \geqslant max\lbrace\frac{NF}{u_s}, \frac{F}{d_{min}}\rbrace$$ 该式提供了对于客户-服务器体系结构的最小分发时间的下界. 因此我们取上面提供的这个下界作为实际发送时间,即下式(2-1):$$D_{cs} = max\lbrace\frac{NF}{u_s},\frac{F}{d_{min}}\rbrace$$ 我们从式(2-1)看到,对于足够大的N,客户-服务器分发时间由$ND/u_s$确定. 所以,该分发时间随着对等方N的数量线性地增加. 因此举例来说,如果从某星期到下星期对等方的数量从1000增加了到了100万,将该文件分发到所有对等方所需要的时间就要增加1000倍.
我们现在来对P2P体系结构进行简单的分析,其中每个对等方能够帮助服务器分发该文件. 特别是当一个对等方接收到某些文件数据,它能够使用自己的上载能力重新将数据分发给其他对等方. 计算P2P体系结构的分发间在某种程度上比计算客户-服务器体系结构的更为复杂,因为分发时间取决于每个对等方如何向其他对等方分发该文件的各个部分. 无论如何,能够得到对该最小分发时间的一个简单表达式[Kumar 2006]. 至此,我们先做如下观察:
- 在分发的开始,只有服务器具有文件. 为了使社区的这些对等方得到该文件,该服务器必须经其接入链路至少发送该文件的额每个bit一次. 因此,最小分发时间至少是$F/u_s$. (与客户-服务器方案不同,由服务器发送过一次的比特可能不必由该服务器再次发送,因为对等方在它们之间可以重新分发这些比特. )
- 与客户-服务器体系结构相同,具有最低下载速率的对等方不能够以小于$F/d_{min}$s的分发时间获得所有F bit. 因此最小分发时间至少为$F/d_{min}$.
- 最后,观察到系统整体的总上载能力等于服务器的上载速率加上每个单独的对等方的上载速率,即$u_{total}=u_s+u_1+…+u_N$. 整个系统必须向这N个对等方交付F bit的数据,因此总共交付NF bit. 这不能以快于$u_{total}$的速率完成. 因此,最小的分发时间也至少是$NF/(u_s+u_1+…+u_N)$.
将这三项观察放在一起,我们获得了对P2P的最小分发时间,表达为$D_{P2P}$(下式记作式2-2). $$D_{cs} \geqslant max\lbrace\frac{F}{u_s},\frac{F}{d_{min}},\frac{NF}{u_s+\sum_{i=1}^Nu_i}\rbrace$$ 式(2-2)提供了对于P2P体系结构的最小分发时间的下界. 这说明,如果我们认为一旦每个对等方接收到一个比特就能够重分发一个比特的话,则存在一个重新分发方案能实际取得这种下界[Kumar 2006]. 实际上,备份发的是文件块而不是一个个bit. 式(2-2)能够作为1实际最小分发时间的近似值.
图2-22比较了客户-服务器和P2P体系结构的最小分发时间,其中假定所有的对等方具有相同的上载速率u. 在图2-22中,我们已经设置了F/u=1小时,$u_s=10u, d_{min} \geqslant u_s$. 即在一个小时中一个对等方能够传输整个文件,该服务器的传输速率是对等方上载速率的10倍,并且对等方的下载速率被设置得足够大,使之不会产生影响. 我们从图2-22中看到,对于客户-服务器体系结构,随着对等方数量的增加,分发时间呈线性增长并且没有界. 然而,对于P2P体系结构,最小分发时间不仅总是小于客户-服务器体系结构的分发时间,并且对于任意的对等方数量N,总是小于1小时. 因此,具有P2P体系结构的应用程序能够是自扩展的. 这种扩展性的直接成因是:对等方除了是比特的消费者外还是它们的重新分发者.

2.BitTorrent
BitTorrent是一种用于文件分发的流行P2P协议[Chao 2011]. 用BitTorment的术语来讲,参与一个特定文件分发的所有对等方的集合被称为一个洪流(torrent). 在一个洪流中的对等方彼此下载等长度的文件块(chunk),典型的块长度为256KB. 当一个对等方首次加入一个洪流时,它没有块. 随着时间的流逝,它累积了越来越多的块. 当它下载块时,也为其他对等方上载了多个块. 一且某对等方获得了整个文件,它也许离开潜流,或留在该洪流中并继续向其他对等方上载块. 同时,任何对等方可能在仅具有块的子集的情况下就离开该洪流,并在以后重新加入该洪流中.
我们现在更为仔细地观察BitTorrent运行的过程. 因为BitTorrent是一个相当复杂的协以,所以我们将仅描述它最重要的机制. 每个洪流具有一个基础设施节点,称为追踪器(tracker). 当一个对等方加入某洪流时,它向追踪器注册自己,并周期性地通知追踪器它仍在该洪流中. 以这种方式,追踪器跟踪参与在洪流中的对等方. 一个给定的洪流可能在任何时刻具有数以百计或数以千计的对等方.
如图2-23所示,当一个新的对等方Alice加入该洪流时,追踪器随机地从参与对等方的集合中选择对等方的一个子集(为了具体起见,设有50个对等方),并将这50个对等方的了IP地址发送给Alice. Alice持有对等方的这张列表,试图与该列表上的所有对等方创建并行的TCP连接. 我们称所有这样与Alice成功地创建一个TCP连接的对等方为“邻近对等方”(在图2-23中,Alice显示了仅有三个邻近对等方. 通常,她应当有更多的对等方). 随着时间的流逝,这些对等方中的某些可能离开,其他对等方(最初50个以外的)可能试图与Alice创建TCP连接. 因此一个对等方的邻近对等方将随时间而波动.

在任何给定的时,每个对等方将具有来自该文件的块的子集,并且不同的对等方具有不同的子集. Alice周期性地(经TCP连接)询问每个邻近对等方它们所具有的块列表. 如果Alice具有L个不同的邻居,她将获得L个块列表. 有了这个信息,Alice将对她当前还没有的块发出请求(仍通过TCP连接).
因此在任何给定的时刻,Alice将具有块的子集并知道它的邻居具有哪些块. 利用这些信息,Alice将做出两个重要决定. 第一,她应当从她的邻居请求哪些块?第二,她应当向哪些向她请求块的邻居发送块?在决定请求哪些块的过程中,Alice使用一种称为最稀缺优先(rarest first)的技术. 这种技术的思路是,针对她没有的块在她的邻居中决定最稀缺的块(最稀缺的块就是那些在她的邻居中副本数量最少的块)并首先请求那些最稀缺的块. 这样,最稀缺块得到更为迅速的重新分发,其目标是(大致地)均衡每个块在洪流中的副本数量.
为了决定她响应哪个请求,BitTorrent使用了一种机灵的对换算法. 其基本想法是,Alice根据当前能够以最高速率向她提供数据的邻居,给出其优先权. 特别是,Alice对于她的每个邻居都持续地测量接收到比特的速率,并确定以最高速率流入的4个邻居. 每过10秒,她重新计算该速率并可能修改这4个对等方的集合. 用BitTorrent术语来说,这4个对等方被称为疏通(unchoked). 重要的是,每过30秒,她也要随机地选择另外一个邻居并向其发送块. 我们将这个被随机选择的对等方称为Bob. 因为Alice正在向Bob发送数据,她可能成为Bob前4位上载者之一,这样的话Bob将开始向Alice发送数据. 如果Bob向Alice发送数据的速率足够高,Bob接下来也能成为Alice的前4位上载者. 换言之,每过30秒Alice将随机地选择一名新的对换伴侣并开始与那位伴侣进行对换. 如果这两名对等方都满足此对换,它们将对方放入其前4位列表中并继续与对方进行对换,直到该对等方之一发现了一个更好的伴侣为止. 这种效果是对等方能够以趋向于找到彼此的协调的速率上载. 随机选择邻居也人允许新的对等方得到块,因此它们能够具有对换的东西. 除了这5个对等方(前4个对等方和一个试探的对等方)的所有其他相邻对等方均被“阻塞”,即它们不能从Alice接收到任何块. BitTorrent有一些有趣的机制没有在这里讨论,包括片(小块)、流水线、随机优先选择、残局模型和反念慢[Cohen 2003].
刚刚描述的关于交换的激励机制常被称为“一报还一报”(tit-for-tat)[Cohen 2003]. 已证实这种激励方案能被回避[Liogkas 2006;Locher 2006;Piatek 2008]. 无论如何,BitTorrent“生态系统”取得了广泛成功,数以百万计的并发对等方在数十万条洪流中积极地共享文件. 如果BitTorrent被设计为不采用一报还一报(或一种变种),然而在别的方面却完全相同的协议,BitTorrent现在将很可能不复存在了,因为大多数用户将成为搭便车者了[Sarouiu 2002].
我们简要地提一下另一种P2P应用——分布式散列表(DHT)来结束我们的讨论. 分布式散列表是一种简单的数据库,其数据库记录分布在一个P2P系统的多个对等方上. DHT得到了广泛实现(如在BitTorrent中),并成为大量研究的主题.
以下内容来自分布式哈希表 (DHT) 和 P2P 技术
早期的一种P2P网络采取了不同的策略,它不设置中央服务器;当用户请求资源时,它会请求它所有的邻接节点,邻接节点再依次请求各自的邻接节点,并使用一些策略防止重复请求,直到找到拥有资源的节点.也就是说,这是一种泛洪搜索(Flooding Search).
这种P2P网络去除了中央服务器, 它的稳定性就强多了. 然而它太慢了. 一次查找可能会产生大量的请求, 可能会有大量的节点卷入其中. 一旦整个系统中的的节点过多, 性能就会变得很差.
为了解决这些问题, 分布式哈希表(即前文提到的分布式散列表)应运而生. 在一个有n个节点的分布式哈希表中, 每个节点仅需存储$O(lgn)$个其他节点, 查找资源时仅需请求$O(lgn)$个节点, 并且无需中央服务器, 是一个完全自组织的系统.
地址管理
首先, 在分布式哈希表中, 每个节点和资源都有一个唯一标识, 通常是一个160位整数. 为方便起见, 我们称节点的唯一标识为ID, 称资源的唯一标识为Key. 我们可以把一个节点的IP地址用SHA-1算法哈希得到这个节点的ID; 同样地, 把一个资源文件用SHA-1算法哈希就能得到这个资源的Key了.
定义好ID和Key之后, 就可以发布和存储资源了. 每个节点都会负责一段特定范围的Key, 其规则取决于具体的算法. 例如, 在Chord算法中, 每个Key总是被第一个ID大于或等于它的节点负责. 在发布资源的的时候, 先通过哈希算法计算出资源文件的Key, 然后联系负责这个Key的节点, 把资源存放在这个节点上. 当有人请求资源的时候, 就联系负责这个Key的节点, 把资源取回即可.
发布和请求资源有两种做法, 一种是直接把文件传输给负责的节点, 由它存储文件资源; 请求资源时再由这个节点将文件传输给请求者. 另一种做法是由发布者自己设法存储资源, 发布文件时把文件所在节点的地址传输给负责的节点, 负责的节点仅存储一个地址; 请求资源的时候会联系负责的节点获取资源文件的地址, 然后再取回资源. 这两种做法各有优劣. 前者的好处是资源的发布者不必在线, 请求者也能获取资源; 坏处是如果文件过大, 就会产生较大的传输和存储成本. 后者的好处是传输和存储成本都比较小, 但是资源的发布者, 或者说资源文件所在的节点必须一直在线.
路由算法
上面我们简述了地址系统,以及如何发布和取回资源.但是现在还有一个大问题:如何找到负责某个特定Key的节点呢? 这里就要用到路由算法了.不同的分布式哈希表实现有不同的路由算法,但它们的思路是一致的.
首先每个节点会路由若干个其他节点的联系方式(IP地址,端口), 称之为路由表. 一般来说一个有着n个节点的分布式哈希表中, 一个节点的路由表的长度为$O(lgn)$.每个节点都会按照特定的规则构建路由表, 最终所有的节点会形成一张网络.从一个节点发出的消息会根据特定的路由规则,沿着网络逐步接近目标节点,最终达到目标节点.在有着n个节点的分布式哈希表中, 这个过程的转发次数通常为$O(lgn)$次.
自我组织(self-organization)
分布式哈希表中的节点都是由各个用户组成,随时有用户加入,离开或失效;并且分布式哈希表没有中央服务器,也就是说着这个系统完全没有管理者.这意味着分配地址,构建路由表,节点加入,节点离开,排除失效节点等操作都要靠自我组织策略实现.
要发布或获取资源,首先要有节点加入.一个节点加入通常有以下几步.首先,一个新节点需要通过一些外部机制联系分布式哈希表中的任意一个已有节点;接着新节点通过请求这个已有节点构造出自己的路由表,并且更新其他需要与其建立连接的节点的路由表;最后这个节点还需要取回它所负责的资源.
此外我们必须认为节点的失效是一件经常发生的事,必须能够正确处理它们.例如,在路由的过程中遇到失效的节点,会有能够替代它的其他节点来完成路由操作;会定期地检查路由表中的节点是否有效;将资源重复存储在多个节点上以对抗节点失效等.另外分布式哈希表中的节点都是自愿加入的,也可以自愿离开.节点离开的处理与节点失效类似,不过还可以做一些更多的操作,比如说立即更新其他节点的路由表,将自己的资源转储到其他节点等.
视频流和内容分发网
众多评估数据显示,包括Netflix、YouTube和亚马逊Prime在内的流式视频,大约占2020年因特网流量的80%[Cisco 2020]. 在本节中,我们将概述流行的视频流式服务在今天的因特网中是如何实现的. 我们将看到,其实现使用了应用层协议,以及以某种方式起到高速缓存作用的服务器.
因特网视频
在流式存储视频应用中,基础的媒体是预先录制的视频,例如电影、电视节目、录制好的体育事件或录制好的用户生成的视频(如通常在YouTube上可见的那些). 这些预先录制好的视频放置在服务器上,用户按需向这些服务器发送请求来观看视频. 许多因特网公司现在提供流式视频,这些公司包括Netflix、YouTube(谷歌)、亚马逊和抖音等.
但在开始讨论视频流之前,我们先迅速感受一下视频媒体. 视频是一系列的图像,通常以一种恒定的速率(如每秒24或30张图像等)来展现. 一幅未压缩、数字编码的图像由像素阵列组成,其中每个像素由一些比特编码来表示亮度和颜色. 视频的一个重要特征是能够被压缩,因而可用比特率来权衡视频质量. 今天现成的压缩算法能够将一个视频压缩成所希望的任何比特率. 当然,比特率越高,图像质量越,用户的总体视觉感受越好.
从网络的观点看,也许视频最为突出的特征是高比特率. 压缩的因特网视频的比特率范围通常从用于低质量视频的100kbps,到用于流式高分辩率电影的超过4Mbps,再于4K在线播放的超过10Mbps. 到用这能够转换为巨大的流量和存储,特别是对高端视频. 例如,单一2Mbps视频在67分钟期间将耗费1GB的存储和流量. 到目前为止,对流式视频的最为重要的性能度量是平均端到端吞吐量. 为了提供连续不断的播放,网络必须为流式应用提供平均吞吐量,这个流式应用至少与压缩视频的比特率一样大.
我们也能使用压缩生成相同视频的多个版,每个版本有不同的质量等级. 例如,我们能够使用压缩生成相同视频的3个版本,比特率分别为300kbps、1Mbps和3Mbps. 用户则能够根据他们当前可用带宽来决定观看哪个版本. 具有高速因特网连接的用户也许选择3Mbps版本,使用智能手机通过3G观看视频的用户可能选择300kbps版本.
HTTP流和DASH
在HTTP流中,视频只是存储在HTTP服务器中作为一个普通的文件,每个文件有一个特定的URL. 当用户要看该视频时,客户与服务器创建一个TCP连接并发送对该URL的HTTP GET请求. 服务器则以底层网络协议和流量条件允许的尽可能快的速率,HTTP响应报文中发送该视频文件. 在客户一侧,字节被收集在客户应用缓存中. 一旦该缓存中的字节超过预先设定的门限,客户应用程序就开始播放,特别是,流式视频应用程序周期性地从客户应用程序缓存中抓取帧,对这些帧解压缩并且在用户屏幕上展现. 因此,流式视频应用接收到视频就进行播放,同时缓存该视频后面部分的帧.
如前一小节所述,尽管HTTP流在实践中已经得到广泛部署(例如,自YouTube发展初期开始),但它有严重缺陷,即所有客户接收到相同编码的视频,尽管对不同的客户或者对于相同客户的不同时间而言,客户可用的带宽大小有很大不同. 这导致了一种新型的基于HTTP的流的研发,它常常被称为经HTTP的动态适应性流(Dynamic Adaptive Steaming over HTTP,DASH). 在DASH中视频编码为几个不同的版本,其中每个版本具有不同的比特率,对应于不同的质量水平. 客户动态地请求来自不同版本且长度为几秒的视频段数据块. 当可用带宽量较高时,客户自然地选择来自高速率版本的块;当可用带宽量较低,客户自然地选择来自低速率版本的块. 客户用HTTP GET请求报文一次选择一个不同的块[Akhshabi 2011]. DASH允许客户使用不同的因特网接入速率来流式播放不同编码速率的视频. 使用低速3G连接的客户能够接收低比特率(和低质量)的版本,使用光纤连接的客户能够接收高质量的版本. 如果端到端带宽在会话过程中改变的话,DASH人允许客户适应可用带宽. 这种特色对于移动用户特别重要,当移动用户相对于基站移动,通常他们能感受到其可用带宽的波动.
使用DASH后,0HTTP服务器,每个版本都有一个不同的URL. HTTP服务器也有一个告示文件(manifest file),为每个版本提供了一个URL及其比特率. 客户首先请求该告示文件并且得知各种各样的版本. 然后客户通过在HTTP GET请求报文中对每块指定一个URL和一个字节范围,一次选择一块. 在下载块的同时,客户也测量接受带宽并运行一个速率决定算法来选择下次请求的块. 自然地,如果客户缓存地视频很多,并且测量到的接受带宽较高,它将选择一个高速率的版本. 同样,如果用户缓存的视频很少,并且测量的接受带宽较低,它将选择一个低速率的版本. 因此DASH允许客户自由地在不同的质量等级之间切换.
内容分发网
今天,许多因特网视频公司日复一日地向数以百万计的用户按需分发每秒数兆比特的流. 向位于全世界的所有用户流式传输所有流量同时提供连续播放和高交互性显然是一项有挑战性的任务.
对于一个因特网视频公司,或许提供流式视频服务最为直接的方法是建立单一的大规模数据中心,在数据中心中存储其所有视频,并直接从该数据中心向世界范围的客户传输流式视频. 但是这种方法存在三个问题.
- 首先,如果客户远离数据中心,服务器到客户的分组将跨越许多通信链路并很可能通过许多ISP,其中某些ISP可能位于不同的大洲,如果这些链路之一提供的春吐量小于视频消耗速率,端到端吞吐量也将小于该消耗速率,给用户带来恼人的停滞时延. (第1章讲过,一条流的端到端吞吐量由瓶颈链路的吞吐量所决定)出现这种事件的可能性随着端到端路径中链路数量的增加而增加.
- 第二个缺陷是流行的视频很可能经过相同的通信链路发送许多次. 这不仅浪费了网络带宽,因特网视频公司自己也将为向因特网反复发送相同的字节而向其ISP支付费用.
- 第三个问题是单个数据中心代表一个单点故障,如果数据中心或其通向因特网的链路崩溃,它将不能够分发任何视频流了.
为了应对向分布于全世界的用户分发巨量视频数据的挑成,几乎所有主要的视频流公司都使用了CDN. CDN管理分布在多个地理位置上的服务器,在它的服务器中存储视频(和其他类型的Web内容,包括文档、图片和音频)的副本,并且试图将所有用户请求定向到一个提供最好用户体验的CDN位置. CDN可以是专用CDN(private CDN),即由内容提供商自己所拥有,例如谷歌的CDN分发YouTube视频和其他类型的内容. CDN还可以是第三方CDN(third-party CDN),它代表多个内容提供商分发内容,Akamai、Limelight和Level-3都选择第三方CDN.
CDN通常采用两种不同的服务器安置原则:
- 深入:第一个原则由Akamai首创,该原则是通过在遍及全球的接入ISP中部署服务器集群来深入到ISP的接入网中. Akamai在数以千计个位置采用这种方法部署集群. 其目标是靠近端用户,通过减少端用户和CDN集群之间的链路和路由器的数量,从而改善了用户感受的时延和吞吐量. 因为这种高度分布式设计,维护和管理集群的任务成为挑战.
- 邀请做客:第二个设计原则由Limelight和许多其他CDN公司所采用,该原则是通过在少量(例如10个)关键位置建造大集群来邀请到ISP做客(即与ISP进行数据的交换,更接近ISP). 不是将集群放在接入ISP中这些CDN通常将它们的集群放置在因特网交换点(IXP). 与深入设计原则相比,邀请做客设计通常产生较低的维护和管理开销,但是可能对端用户造成较高时延和较低吞吐量.
接入网指的是骨干网络到端用户之间的网络连接.
互联网交换中心(Internet Exchange Point,IXP)是一种物理基础设施,用于在不同的互联网服务提供商(ISP)和内容分发网络(CDN)之间交换互联网流量.
一旦CDN的集群准备就绪,它就可以跨集群复制内容. CDN可能不希望将每个视频的副本放置在每个集群,因为某些视频很少被观看或仅在某些国家中流行. 事实上,许多CDN没有将视频推人它们的集群,而是使用一种简单的拉策略:如果客户向一个未存储该视频的集群请求某视频,则该集群(从某中心仓库或者从另一个集群)检索该视频,向客户流式传输视频的同时在本地存储一个副本. 类似于Web缓存,当某集群存储器变满时,它删除不经常请求的视频.
1.CDN操作
在讨论过这两种部署CDN的重要方法后,我们现在深入看看CDN操作的细节. 当用户主机中的一个浏览器指令检索一个特定的视频(由URL标识)时,CDN必须截获该请求,以便能够进行以下操作:
- 确定此时适合用于该客户的CDN服务器集群
- 将客户的请求重定向到该集群的某台服务器
我们很快将讨论CDN是如何能够确定一个适当的集群的. 但是我们首先考察截获和重定向请求所依赖的机制.
大多数CDN利用DNS来截获和重定向请求. 我们考虑用一个简单的例子来说明通常是怎样使用DNS的. 假定有一个内容提供商NetCinema,雇用了第三方CDN公司KingCDN来向其客户分发视频. 在NetCinema的Web网页上,它的每个视频都被指派了一个URL,该URL包括字符串“video”以及该视频本身的独特标识符. 例如,变形金刚7可以指派为http://video.netcinema.com/6Y7B23V. 接下来出现如图2-24所示的6个步骤:

- 用户访问位于NetCinema的Web网页.
- 当用户点击链接http://video.netcinema.com/6Y7B23V时该用户主机发送了一个对于video.netcinema.com的DNS请求.
- 用户的本地DNS服务器(LDNS)将该DNS请求中继到一台用于NetCinema的权威DNS服务器,该服务器观察到主机名video.netcinema.com中的字符串“video”(存在CNAME记录使得该url被映射为指定url). 为了将该DNS请求移交给KingCDN,NetCinema权威DNS服务了将该DNS请求移交给KingCDN,NetCinema权威DNS服务器并不返回一个IP地址,而是向LDNS返回一个KingCDN域的主机和名,如a1105.kingcdn.com.
- 从这时起,DNS请求进入了KingCDN专用DNS基础设施. 用户的LDNS则发送第二个请求,此时是对a1105.kingcdn.com的DNS请求,KingCDN的DNS系统最终向LDNS返回KingCDN内容服务器的IP地址. 所以正是在这里,在KingCDN的DNS系统中,指定了CDN服务器,客户将能够从这人台服务器接收到它的内容.
- LDNS向用户主机转发内容服务CDN节点的IP地址.
- 一旦客户收到KingCDN内容服务器的IP地址,它与具有该IP地址的服务器创建了一条直接的TCP连接,并且发出对该视频的HTTPGET请求. 如果使用了DASH,服务器将首先向客户发送具有URL列表的告示文件,每个URL对应视频的每个版本,并且客户将动态地选择来自不同版本的块.
2.集群选择策略
任何CDN部署,其核心都是集群选择策略(cluster selection strategy),即动态地将客户定向到CDN中的某个服务器集群或数据中心的机制. 如我们刚才所见,经过客户的DNS查找,CDN得知了该客户的LDNS服务器的IP地址. 在得知该IP地址之后,CDN需要基于该IP地址选择一个适当的集群. CDN一般采用专用的集群选择策略. 我们现在简单地介绍一些策略,每种策略都有优缺点.
一种简单的策略是指派用户到地理上最为邻近(geographically closest)的集群. 使用商用地理位置数据库,每个LDNS IP地址都映射到一个地理位置. 当从一个特殊的LDNS接受到一个DNS请求时,CDN选择地理上最为接近的集群,即离LDNS最少几千米远的集群,“就像鸟飞一样”. 这样的解决方案对于众多用户来说能够工作的相当好. 但对某些用户,该解决方案可能执行效果比较差,因为就网络路径的长度或跳数而言,地理最邻近的集群可能并不是最近的集群. 此外,所有基于DNS的方法都具有的问题时,某些端用户配置使用位于远地的LDNS,在这种情况下,LDNS位置可能远离客户的位置. 此外,这种简单的策略忽略了时延和可用带宽随因特网路径时间而变化,总是为特定的客户指派相同的集群.
为了基于当前流量条件为客户确定最好的集群,CDN能够对其集群和客户之间的时延和丢包性能执行周期性的实时测量(real-time measurement). 例如,CDN能够让它的每个集群周期性地向位于全世界的所有LDNS发送探测分组(例如,ping报文或DNS请求). 这种方法的一个缺点是许多LDNS被配置为不响应这些探测.
SSL/TLS
该部分为额外内容,参考:
什么是SSL
SSL(Secure Sockets Layer) 是一种用于在网络上保护信息安全的标准安全技术. 它通过对网络连接进行加密来确保数据在客户端和服务器之间的安全传输. SSL协议使用了非对称加密和对称加密技术,可以防止数据在传输过程中被窃取或篡改.
HTTP 在传输数据时使用的是明文是不安全的,为了解决这一隐患,网景公司(Netscape)推出了 SSL 安全套接字协议层. SSL 是基于HTTP之下,TCP 之上的一个协议层,是基于 HTTP 标准并对 TCP 传输数据时进行加密,所以 HPPTS 即 HTTP+SSL/TLS,Https 默认使用端口443.
SSL协议组成
SSL协议由SSL记录协议和SSL握手协议组成.
SSL记录协议(SSL Record Protocol)
:它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持.SSL握手协议(SSL Handshake Protocol)
:它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等.
什么是TLS
TLS(Transport Layer Security)是IETF在SSL3.0基础上设计的协议,实际上相当于SSL的后续版本. SSL/TLS是一个安全通信框架,上面可以承载HTTP协议或者SMTP/POP3协议等.
TLS协议的架构

TLS主要分为两层,下层是TLS记录协议,主要负责使用对称密码对消息进行加密. 上层是TLS握手协议,主要分为如图的4个部分.
- 握手协议负责在客户端和服务器端商定密码算法和共享密钥,包括证书认证,是4个协议中最最复杂的部分.
- 密码规格变更协议负责向通信对象传达变更密码方式的信号
- 警告协议负责在发生错误的时候将错误传达给对方
- 应用数据协议负责将TLS承载的应用数据传达给通信对象的协议.
TLS握手流程

对图中的流程有如下解释:
Client Hello
:客户端向服务端发送hello消息,包括以下内容:可用版本号
当前时间
客户端随机数
会话ID
可用的密码套件清单
可用的压缩方式清单
之前提到了TLS其实是一套加密框架,其中的有些组件其实是可以替换的,这里的可用版本号,可用的密码套件清单,可用的压缩方式清单就是向服务器询问对方支持哪些服务.
Server Hello
:服务端收到客户端的 hello 后会返回一个 hello,包含以下内容:使用的版本号
当前时间
服务器随机数
会话ID
使用的密码套件
使用的压缩方式
使用的版本号,使用的密码套件,使用的压缩方式是对步骤1的回答. 服务器随机数是一个由服务器端生成的随机数,用来生成对称密钥.
certificate(S2C)
:服务器端发送自己的证书清单,因为证书可能是层级结构的,所以处理服务器自己的证书之外,还需要发送为服务器签名的证书. 客户端将会对服务器端的证书进行验证. 如果是以匿名的方式通信则不需要证书.ServerKeyExchange
:如果certificate
的证书信息不足,则可以发送ServerKeyExchange
用来构建加密通道.ServerKeyExchange
的内容可能包含两种形式:- 如果选择的是RSA协议,那么传递的就是RSA构建公钥密码的参数(E,N).
- 如果选择的是Diff-Hellman密钥交换协议,那么传递的就是密钥交换的参数.
CertificateRequest
:如果是在一个受限访问的环境,比如fabric(区块链框架,可以部署区块链应用程序)中,服务器端也需要向客户端索要证书. 如果并不需要客户端认证,则不需要此步骤.server hello done
:服务器端发送server hello done的消息告诉客户端自己的消息结束了.Certificate(C2S)
:对步骤5的回应,客户端发送客户端证书给服务器.ClientKeyExchange
:- 如果是公钥或者RSA模式情况下,客户端将根据客户端生成的随机数和服务器端生成的随机数,生成预备主密码,通过该公钥进行加密,返回给服务器端.
- 如果使用的是Diff-Hellman密钥交换协议,则客户端会发送自己这一方要生成Diff-Hellman密钥而需要公开的值,这样服务器端可以根据这个公开值计算出预备主密码.
CertificateVerify
:客户端向服务器端证明自己是客户端证书的持有者.ChangeCipherSpec(C2S)
:ChangeCipherSpec
是密码规格变更协议的消息,表示后面的消息将会以前面协商过的密钥进行加密.Finished
:客户端告诉服务器端握手协议结束了.ChangeCipherSpec(S2C)
:服务器端告诉客户端自己要切换密码了.Finished
:服务器端告诉客户端,握手协议结束了.切换到应用数据协议,这之后服务器和客户端就是以加密的方式进行沟通了.
主密码和预备主密码
上面的步骤8生成了预备主密码,主密码是根据密码套件中定义的单向散列函数实现的伪随机数生成器+预备主密码+客户端随机数+服务器端随机数生成的. 主密码主要用来生成称密码的密钥,消息认证码的密钥和对称密码的 CBC 模式所使用的初始化向量.
TLS记录协议
TLS记录协议主要负责消息的压缩,加密及数据的认证:

消息首先将会被分段,然后压缩,再计算其消息验证码(MAC值),然后使用对称密码进行加密,加密使用的是 CBC 模式,CBC 模式的初始向量是通过主密码来生成的. 得到密文之后会附加类型,版本和长度等其他信息,最终组成最后的报文数据.
三. 运输层
运输层位于应用层和网络层之间,是分层的网络体系结构的重要部分. 该层为运行在不同主机上的应用进程提供直接的通信服务起着至关重要的作用. 我们在本章将交替地讨论运输层的原理和这些原理在现有的协议中是如何实现的. 与往常一样,我们将特别关注因特网协议,即 TCP 和 UDP 运输层协议.
我们将从讨论运输层和网络层的关系开始. 这就为研究运输层第一个关键功能打好了基础,即将网络层的在两个端系统之间的交付服务扩展到运行在两个不同端系统上的应用层进程之间的交付服务. 我们将在讨论因特网的无连接运输协议 UDP 时阐述这个功能.
然后我们重新回到原理学习上,面对计算机网络中最为基础性的问题之一,即两个实体怎样才能在一种会丢失或损坏数据的媒介上可靠地通信. 通过一系列复杂性不断增加的场景,我们将逐步建立起一套被运输协议用来解决这些问题的技术. 然后,我们将说明这些原理是如何体现在因特网面向连接的运输协议 TCP 中的.
接下来我们讨论网络中的第二个基础性的重要问题,即控制运输层实体的传输速率以避免网络中的拥塞,或从拥塞中恢复过来. 我们将考虑拥塞的原因和后果,以及常用的拥塞控制技术. 在透彻地理解了拥塞控制问题之后,我们将研究TCP应对拥塞控制的方法.
概述和运输层服务
运输层协议为运行在不同主机上的应用进程之间提供了逻辑通信(logic communication)功能. 从应用程序的角度看,通过逻辑通信,运行不同进程的主机好像直接相连一样;实际上,这些主机也许位于地球的两侧,通过很多路由器及多种不同类型的链路相连. 应用进程使用运输层提供的逻辑通信功能彼此发送报文,而无须考虑承载这些报文的物理基础设施的细节. 图3-1图示了逻辑通信的概念.

运输层和网络层的关系
该部分存在简化
前面讲过,在协议栈中,运输层刚好位于网络层之上. 网络层提供了主机之间的逻辑通信,而运输层为运行在不同主机上的进程之间提供了逻辑通信.
网络层负责主机间的通信.它的主要职责就是确保数据包能够从源主机正确路由到目的主机.它关心的是设备之间的连接,即如何找到通往目标主机的最佳路径.
运输层负责进程间的通信.它在网络层提供的服务基础之上进一步将数据交付给目标主机上正在运行的某个应用程序.
因特网运输层概述
运输层中存在两种截然不同的协议, 即是 TCP和 UDP.
为了简化术语, 我们将运输层分组称为报文段(segment). 有些文献会将TCP的运输层分组称作报文段, 而将UDP的分组称作数据报(data-gram). 在本书中, 我们将运输层的分组统称为报文段, 而将网络层的分组称作数据包.
UDP 和 TCP 最基本的责任是, 将两个端系统间IP的交付服务扩展为运行在端系统上的两个进程之间的交付服务. 这一过程成为运输层的多路复用(multiplexing)和多路分解(demutiplexing). UDP 和 TCP 还可以通过在其报文段首部中包含差错检查字段而提供完整性检查. 进程到进程的数据交付和差错检查是两种最低限度的运输层服务, 也是 UDP 所能提供的仅有的两种服务. 特别是, 与 IP 一样, UDP 也是一种不可靠的服务, 不能保证数据能够完整地到达目的进程.
TCP为应用程序提供了几种附加服务. 首先, 它提供了可靠数据传输(reliable data transfer). 通过使用流量控制, 序号, 确认和定时器, TCP确保正确地将数据从发送进程交付给接受进程. 这样, TCP九江两个端系统间地不可靠IP服务转换成了一种进程间地可靠数据传输服务. TCP还提供拥塞控制(congesion control). 拥塞控制与其说是一种提供给它的应用程序的服务, 不如说是一种提供给整个因特网的服务, 这是一种带来通用好处的服务.不太严格地说, TCP拥塞控制防止任何一条TCP连接用过多流量来淹没通信主机之间的链路和交换设备.TCP力求为每个通过一条阻塞网络链路的连接平等的共享网络链路带宽. 这可以通过调节TCP连接的发送端发送仅=进网络的流量速率来做到. 在另一方面, UDP流量是不可调节的. 使用UDP传输的应用程序可以根据其需要以任意速率发送数据(当然存在带宽等限制).
一个能提供可靠数据传输和拥塞控制的协议必定是复杂的. 我们将用几节的篇幅来介绍可靠数据传输和拥塞控制的原理, 用另外几节去介绍TCP协议本身.但在全面介绍这些内容之前, 我们先学习运输层的多路复用与多路分解.
多路复用与多路分解
在本节中,我们讨论运输层的多路复用与多路分解,也就是将由网络层提供的主机间交付服务延伸到运行在主机上的进程间交付服务. 为了使讨论具体,我们将在因特网环境中讨论这种基本的运输层服务. 然而,多路复用与多路分解服务是所有计算机网络都需要的.
在目的主机, 运输层从紧邻其下的网络层接收报文段. 运输层负责将这些报文段中的数据交付给主机上运行的适当应用程序进程.
一个进程(作为网络应用的一部分)有一个或多个套接字, 它相当于从网络向进程传递数据和从进程向网络传递数据的门户. 因此,如 图3-2 所示,在接收主机中的运输层实际上并没有直接将数据交付给进程,而是将数据交给了一个中间的套接字. 由于在任一时刻, 在接收主机上可能有不止一个套接字, 所以每个套接字都有唯一的标识符. 标识符的格式取决于它是 UDP 还是 TCP 套接字,我们将很快对它们进行讨论.

现在我们考虑接收主机怎样将一个到达的运输层报文段定向到适当的套接字. 为此, 每个运输层报文段中具有几个字段. 在接收端, 运输层检查这些字段, 标识出接收套接字, 进而将报文段定向到该套接字. 将运输层报文段中的数据交付到正确的套接字的工作称为多路分解(demultiplexing). 在源主机从不同套接字中收集数据块, 并为每个数据块封装上首部信息(后续用于接收端分解)从而生成报文段, 然后将报文段传递到网络层, 所有这些工作称为多路复用(multiplexing).
值得注意的是,图3-2 中的中间那台主机的运输层必须将从其下的网络层收到的报文段分解后交给其上的$P_1$,或$P_2$进程; 这一过程是通过将到达的报文段数据定向到对应进程的套接字来完成的. 中间主机中的运输层也必须收集这些套接字输出的数据, 形成运输层报文段, 然后将其向下传递给网络层. 尽管我们在因特网运输层协议的环境下引入了多路复用和多路分解, 但是我们要知道, 它们与在某层(在运输层或别处)的单一协议何时被位于接下来的较高层的多个协议使用有关.
既然我们理解了运输层多路复用和多路分解的作用, 那就再来看看在主机中它们实际是怎样工作的. 通过上述讨论, 我们知道运输层多路复用的要求:
套接字有唯一标识符;
每个报文段有特殊字段来指示该报文段所要交付到的套接字. 这些特殊的字段是源端口字段(source port number field)和目的端口字段(dextination number field).(还存在一些其他字段,后述)
端口号是一个 16bit 的数, 其大小在0-65535之间. 0-1023范围的端口号称为周知端口号(well-known port number), 是受限制的, 这是它们保留给诸如 HTTP(port:80) 和 FTP(port:21)之类的周知应用层协议使用的. 周知端口列表在 [RFC 1700] 中给出.当我们开发一个新的应用程序时, 必须为其分配一个端口号.
现在应该清楚运输层是怎样能够实现分解服务的了: 在主机上的每个套接字能够分配一个端口号, 当报文段到达主机时, 运输层检查报文段中的目的端口号, 并将其定向到相应的套接字. 然后报文段中的数据通过套接字进入其所连接的进程. 如我们将看到的那江样, UDP大体上是这样做的. 然而, 也将如我们所见, TCP中的多路复用与多路分解更为复杂.

1. 无连接的多路复用和多路分解
无连接指的就是由 UDP 传输数据. 通过为 UDP套接字 分配端口号, 我们现在能够精确地描述 UDP 的复用与分解. 假定在主机A中的一个进程具有 UDP端口 19157, 它要发送一个应用程序数据块给位于 主机B 中的另一进程, 该进程具有 UDP端口 46428. 主机A 中的运输层创建一个运输层报文段,其中包括应用程序数据、源端口号(19157)、目的端口号(46428)和两个其他值(后述). 然后,运输层将得到的报文段传递到网络层. 网络层将该报文段封装到一个IP数据报中, 并尽力而为地将报文段交付给接收主机. 如果该报文段到达接收 主机B, 接收主机运输层就检查该报文段中的目的端口号(46428)并将该报文段交付给端口号46428所标识的套接字. 值得注意的是, 主机B 能够运行多个进程,每个进程有自己的 UDP套接字 及相应的端口号. 当 UDP报文段 从网络到达时, 主机B 通过检查该报文段中的目的端口号,将每个报文段定向(分解)到相应的套接字.
一个 UDP套接字 是由一个二元组全面标识的, 该二元组包含 目的IP地址 和 目的端口号.因此, 如果两个 UDP报文段 有不同的源 IP地址 和/或源端口号,但具有相同的目的 IP地址 和目的端口号,那么这两个报文段将通过相同的目的套接字被定向到相同的目的进程.
UDP报文段不携带源IP地址么?
是的, 因为在网络层中, IP数据包的头部数据包含了源IP地址, 不需要UDP来解决.
2. 面向连接的多路复用与多路分解
为了理解 TCP多路分解,我们必须更为仔细地研究 TCP套接字 和 TCP连接创建. TCP套接字 和 UDP套接字 之间的一个细微差别是, TCP套接字 是由一个四元组(源IP地址, 源端口号, 目的IP地址, 目的端口号)来标识的. 因此,当一个 TCP报文段 从网络到达一台主机时, 该主机使用全部 4个值 来将报文定向(分解)到相应的套接字. 特别与 UDP 不同的是,两个具有不同源IP地址或源端口号的到达 TCP报文段 将被定向到两个不同的套接字,除非 TCP报文段 携带了初始创建连接的请求(携带SYN报文段,后述).
故而TCP创建连接的过程比UDP复杂得多, 大致有以下流程:
- 一个服务器程序会创建一个监听套接字,并将其绑定到一个特定的本地端口.
- 当一个客户端发起连接请求(SYN报文段)时,这个监听套接字会接收到它.
- 服务器会为这个新连接创建一个新的、专用的套接字,专门用于处理与该特定客户端(由其唯一的源IP和源端口标识)的通信.
服务器主机可以支持很多并行的 TCP套接字,每个套接字与一个进程相联系,并由其四元组来标识每个套接字. 当一个 TCP报文段 到达主机时,所有4个字段(源IP地址,源端口,目的IP地址,目的端口)被用来将报文段定向(分解)到相应的套接字.
3. Web服务器与TCP
在结束这个讨论之前,再多说几句Web服务器以及它们如何使用端口号. 考虑一人台运行 Web服务器 的主机,例如在端口80上运行一个 Apache Web服务器. 当客户(如浏览器)向该服务器发送报文段时,所有报文段的目的端口都将为80. 特别是, 初始连接建立报文段和承载 HTTP请求 的报文段都有80的目的端口. 如我们刚才描述的那样, 该服务器能够根据源下地址和源端口号来区分来自不同客户的报文段.

图3-5显示了一台 Web服务器 为每条连接生成一个新进程. 如图3-5所示,每个这样的进程都有自己的套接字, 通过这些套接字可以收到 HTTP请求 和发送 HTTP响应. 然而,我们要提及的是, 连接套接字与进程之间并非总是有着一一对应的关系. 事实上, 当今的高性能 Web服务器 通常只使用一个进程, 但是为每个新的客户连接创建一个具有新连接套接字的新线程. 对于这样一台服务器,在任意给定的时间内都可能有(具有不同标识的)许多连接套接字连接到相同的进程.
如果客户与服务器使用 持续HTTP, 则在整条连接持续期间, 客户与服务器之间经由同一个服务器套接字交换HTTP报文. 然而, 如果客户与服务器使用 非持续HTTP,则对每一对请求/响应都创建一个新的TCP连接,并在随后关闭.这种套接字的频繁创建和关闭会严重地影响一个繁忙的 Web服务器 的性能(尽管有许多操作系统技巧可用来减轻这个问题的影响).
由于我们已经讨论过了运输层多路复用与多路分解问题,下面我们就继续讨论因特网运输层协议之一,即 UDP. 在下一节中,我们将看到 UDP 无非就是对网络层协议增加了一点(多路)复用/(多路)分解服务而已.
无连接运输: UDP
在本节中,我们要仔细地研究一下UDP,看它是怎样工作的,能做些什么.
假如你对设计一个不提供不必要服务的最简化的运输层协议感兴趣. 你将打算怎样做呢? 你也许会首先考虑使用一个无所事事的运输层协议. 特别是在发送方一侧, 你可能会考虑将来自应用进程的数据直接交给网络; 在接收方一侧, 你可能会考虑将从网络层到达的报文直接交给应用进程. 而正如我们在前一节所学的, 我们必须做一点点事, 而不是什么都不做! 运输层最低限度必须提供一种复用/分解服务, 以便在网络层与正确的应用级进程之间传递数据.
由 [RFC768] 定义的 UDP 只是做了运输协议能够做的最少工作. 除了复用/分解功能及少量的差错检测外, 它几乎没有对 IP 增加别的东西. 实际上,如果应用程序开发人员选择 UDP 而不是 TCP,则该应用程序差不多就是直接与 IP 打交道. UDP 从应用进程得到数据, 附加上用于多路复用/分解服务的源端口号, 目的端口号字段, 报文长度和校验和, 然后将形成的报文段交给网络层. 网络层将该运输层报文段封装到一个 IP数据报中, 然后尽力而为地尝试将此报文段交付给接收主机. 如果该报文段到达接收主机, UDP使用目的端口号将报文段中的数据交付给正确的应用进程. 值得注意的是, 使用 UDP 时,在发送报文段之前, 发送方和接收方的运输层实体之间没有握手. 正因为如此, UDP被称为无连接的.
DNS 是一个通常使用 UDP 的应用层协议. 当一台主机中的 DNS应用程序 想要进行一次查询, 它构造了一个 DNS查询报文 并将其交给 UDP. 无须与运行在目的端系统中的 UDP实体 之间握手, 主机端的 UDP 为此报文添加首部字段, 然后将形成的报文段交给网络层. 网络层将此UDP报文段封装进一个 IP数据报 中, 然后将其发送给一个名字服务器. 在查询主机中的 DNS应用程序 则等待对该查询的响应. 如果它没有收到响应(可能是由于底层网络丢失了查询或响应), 则要么试图向另一个名字服务器发送该查询, 要么通知调用的应用程序它不能获得响应.
现在你也许想知道,为什么应用开发人员宁愿在 UDP 之上构建应用, 而不选择在 TCP 上构建应用? 既然 TCP 提供了可靠数据传输服务,而 UDP 不能提供,那么 TCP 是否总是首选的呢? 答案是否定的, 因为有许多应用更适合用 UDP,原因主要以下几点;
- 关于发送什么数据以及何时发送的应用层控制更为精细: 采用UDP时,只要应用进程将数据传递给 UDP, UDP 就会将此数据打包进 UDP报文段 并立即将其传递给网络层. 在另一方面,TCP 有一个拥塞控制机制, 以便当源和目的主机间的一条或多条链路变得极度拥塞时来遏制运输层 TCP 发送方. TCP 仍将继续重新发送数据报文段直到目的主机收到此报文并加以确认, 而不管可靠交付需要用多长时间. 因为实时应用通常要求最小的发送速率, 不希望过分地延迟报文段的传送, 且能容忍一些数据丢失, TCP服务模型 并不是特别适合这些应用的需要.
- 无须连接建立: 如我们后面所讨论的, TCP 在开始数据传输之前要经过三次握手. UDP 却不需要任何准备即可进行数据传输. 因此 UDP 不会引入建立连接的时延, 这可能是 DNS 运行在 UDP 之上而不是运行在TCP之上的主要原因(如果运行在 TCP 上,则 DNS 会慢得多). HTTP 使用 TCP 而不是 UDP, 因为对于有具有文本数据的 Web 网页来说, 可靠性是至关重要的. 但是, HTTP 中的 TCP 连接建立时延对于与下载 Web 文档相关的时延来说是一个重要因素. 用于和谷歌的 Chrome浏览器 中的 QUIC 协议(快速 UDP 因特网连接 [ETF QUIC 2020] )的确将 UDP 作为其支撑运输协议并在 UDP 之上的应用层协议中实现可靠性. 我们将在之后更为仔细地讨论 QUIC.
- 无连接状态: TCP 需要在端系统中维护连接状态. 此连接状态包括接收和发送缓存、拥塞控制参数以及序号与确认号的参数. 我们将在之后看到,要实现 TCP 的可靠数据传输服务并提供拥塞控制,这些状态信息是必要的. 另一方面, UDP 不维护连接状态, 也不跟踪这些参数. 因此, 当应用程序运行在 UDP 之上而不是运行在 TCP 上时, 某些专门用于某种特定应用的服务器一般都能支持更多的活跃客户.
- 分组首部开销小: 每个 TCP报文段 都有20字节的首部开销,而 UDP 仅有8字节的开销.

图3-6 列出了流行的因特网应用及其所使用的运输协议. 如我们所期望的那样, 电子邮件、远程终端访问、Web 及文件传输都运行在 TCP 之上. 因为所有这些应用都需要 TCP 的可靠数据传输服务. 无论如何, 有很多重要的应用是运行在 UDP 上而不是 TCP 上. 例如, UDP 用于承载网络管理数据(SNMP). 在这种场合下, UDP 要优于 TCP, 因为网络管理应用程序通常必须在该网络处于重压状态时运行, 而正是在这个时候可靠的、拥塞受控的数据传输难以实现. 此外,如我们前面所述,DNS运行在UDP之上,从而避免了TCP的连接创建时延.
图3-6 所示, UDP 和 TCP 现在都用于多媒体应用, 如因特网电话、实时视频会议、流式存储音频与视频. 我们将在之后仔细学习这些应用. 我们刚说, 既然所有这些应用都能容忍少量的分组丢失, 因此可靠数据传输对于这些应用的成功并不是至关重要的. 此外, TCP 的拥塞控制会导致如因特网电话、视频会议之类的实时应用性能变得很差. 由于这些原因, 多媒体应用开发人员通常将这些应用运行在 UDP 之上而不是 TCP 之上. 当分组丢包率低时, 并且为了安全原因, 某些机构阻蹇UDP流量, 对于流式媒体传输来说, TCP 变得越来越有吸引力了.
虽然目前通常这样做, 但在 UDP 之上运行多媒体应用需要小心处理. 如我们前面所述, UDP 没有拥塞控制. 但是, 需要拥塞控制来预防网络进入一种拥塞状态, 在拥塞状态中可做的有用工作非常少. 如果每个人都启动流式高比特率视频而不使用任何拥塞控制的话, 就会使路由器中有大量的分组溢出, 以至于非常少的 UDP 分组能成功地通过源到目的的路径传输. 况且, 由无控制的 UDP 发送方引入的高丢包率将引起 TCP 发送方(如我们将看到的那样, TCP遇到拥塞将减小它们的发送速率)大大地减小它们的速率. 因此,UDP 中缺乏拥塞控制会导致 UDP 发送方和接收方之间的高丢包率, 并挤垮 TCP 会话, 这是一个潜在的严重问题 [Floyd 1999]. 很多研究人员已提出了一些新机制, 以促使所有的数据源(包括UDP源)执行自适应的拥塞控制[Mahdavi 1997; Floyd2000; Kohler 2006; RFC 4340].
在讨论 UDP 报文段结构之前, 我们要提一下, 使用 UDP 的应用是可能实现可靠数据传输的. 这可通过在应用程序自身中建立可靠性机制来完成(例如, 可通过增加确认与重传机制来实现, 如采用我们将在下一节学习的一些机制). 我们前面讲过在谷歌的 Chrome浏览器 中所使用的 QUIC协议 在 UDP 之上的应用层协议中实现了可靠性. 但这并不是无足轻重的任务, 它会使应用开发人员长时间地忙于调试. 无论如何, 将可靠性直接构建于应用程序中可以使其“左右逢源”. 也就是说应用进程可以进行可靠通信,而无须受制于由 TCP拥塞控制机制 强加的传输速率限制.
UDP 报文段结构
UDP 报文段结构如图3-7所示,它由 [RFC 768] 定义. 应用层数据占用 UDP报文段 的数据字段. 例如,对于 DNS应用, 数据字段要么包含一个查询报文, 要入包含一个响应报文. 对于流式音频应, 音频抽样数据填充到数据字段. UDP首部 只有4个字段, 每个字段由两个字节组成. 如前一节所讨论的, 通过端口号可以使目的主机将应用数据交给运行在目的端系统中的相应进程(即执行分解功能). 长度字段指示了在 UDP报文段 中的字节数(首部加数据部分).
因为数据字段的长度在一个 UDP 段中不同于在另一个段中, 故需要一个明确的长度. 接收方使用检验和来检查在该报文段中是否出现了差错. 实际上, 计算检验和时, 除了UDP报文段以外还包括了IP首部的一些字段. 但是我们忽略这些细节, 以便能从整体上看问题. 下面我们将讨论检验和的计算. 在之后将描述差错检测的基本原理. 长度字段指明了包括首部在内的UDP报文段长度(以字节为单位).
UDP 校验和
UDP检验和提供了差错检测功能. 这就是说, 检验和用于确定当 UDP报文段 从源到达目的地移动时, 其中的比特是否发生了改变(例如, 由于链路中的噪声干扰或者存储在路由器中时引入问题). 发送方的 UDP 对报文段中的所有16比特字的和进行反码运算, 求和时遇到的任何溢出都被回卷(将溢出的1当作最低位在加到结果数上). 得到的结果被放在UDP报文段中的检验和字段. 下面给出一个计算检验和的简单例子. 在 [RFC 1071] 中可以找到有效实现的细节, 还可在 [Stone 1998;Stone 2000] 中找到它处理真实数据的性能. 举例来说,假定我们有下面 3个16bit 的字:
0110011001100000
0101010101010101
1000111100001100
将三个字加和, 得到结果为0100101011000001
. 注意到这样的加算是存在溢出的, 它要被回卷, 所以成为0100101011000010
. 其反码为1011010100111101
, 这个反码就是校验和.
在接收方,全部的4个16比特字(包括检验和)加在一起. 如果该分组中没有引入差错, 则显然在接收方处该和将是1111111111111111. 如果这些比特之一是0, 那么我们就知道该分组中已经出现了差错.
你可能想知道为什么 UDP 首先提供了检验和, 就像许多链路层协议(包括流行的以太网协议)也提供了差错检测那样. 其原因是不能保证源和目的之间的所有链路都提供差错检测; 这就是说, 也许这些链路中的一条可能使用没有差错检测的协议. 此外, 即使报文段经链路正确地传输, 当报文段存储在某台路由器的内存中, 也可能引入比特差错. 在既无法确保逐链路的可靠性, 又无法确保内存中的差错检测的情况, 如果端到端数据传输服务要提供差错检测, UDP 就必须在端到端基础上在运输层提供差错检测. 这是一个在系统设计中被称颂的端到端原则(end-end principle)的例子 [Saltzer 1984], 该原则表述为因为某种功能(在此时为差错检测)必须基于端到端实现: “与在较高级别提供这些功能的代价相比, 在较低级别上设置的功能可能是冗余的或几乎没有价值的. ”
因为假定IP是可以运行在任何第二层协议之上的,运输层提供差错检测作为一种保险措施是非常有用的. 虽然 UDP 提供差错检测, 但它对差错恢复无能为力. UDP 的某种实现只是丢弃受损的报文段; 其他实现是将受损的报文段交给应用程序并给出警告.
可靠数据传输原理
在本节中, 我们在一般场景下考虑可靠数据传输的问题. 因为可靠数据传输的实现问题不仅在运输层出现, 也会在链路层以及应用层出现, 这时讨论它是恰当的. 因此, 一般性问题对网络来说更为重要. 如果的确要将所有网络中最为重要的“前10个”问题排名的话, 可靠数据传输将是名列榜首的候选者. 在下一节中, 我们将学习 TCP, 尤其要说明 TCP 所采用的许多原理, 而这些正是我们打算描述的内容.
图3-8 说明了我们学习可靠数据传输的框架. 为上层实体提供的服务抽象是: 数据可以通过一条可靠的信道进行传输. 借助于可靠信道, 传输数据比特就不会受到损坏(由0变为1,或者相反)或丢失, 而且所有数据都是按照其发送顺序进行交付. 这恰好就是 TCP 向调用它的因特网应用所提供的服务模型.

实现这种服务抽象是可靠数据传输协议(reliable data transfer protocol)的责任. 由于可靠数据传输协议的下层协议也许是不可靠的, 因此这是一项困难的任务. 例如, TCP 是在不可靠的的(IP)端到端网络层之上实现的可靠数据传输协议. 更一般的情况是, 两个可靠通信端点的下层可能是由一条物理链路(如在链路级数据传输协议的场合下)组成或是由一个全球互联网络(如在运输级协议的场合下)组成. 然而, 就我们而言, 我们可将较低层直接视为不可靠的点对点信道.
在本节中, 考虑到底层信道模型越来越复杂, 我们将不断地开发一个可靠数据传输协议的发送方一侧和接收方一侧. 例如, 我们将考虑当底层信道能够损坏比特或丢失整个分组时, 需要什么样的协议机制. 这里贯穿讨论始终的一个假设是分组将以它们发送的次序进行交付, 某些分组可能会丢失; 也就是说, 底层信道将不会对分组进行重排序. 图3-8b 说明了用于数据传输协议的接口. 通过调用rdt_send()
函数, 上层可以调用数据传输协议的发送方. 它将要发送的数据交付给位于接收方的较高层. (rdt 表示可靠数据传输协议,_send 指示 rdt 的发送端正在被调用)在接收端, 当分组从信道的接收端到达时, 将调用rdt_rev()
. 当 rdt协议 想要向较高层交付数据时,将通过调用deliver_data()
来完成. 后面, 我们将使用术语“分组”而不用运输层的“报文段”. 因为本节研讨的理论适用于一般的计算机网络, 而不只是用于因特网运输层, 所以这时采用通用术语“分组”也许更为合适.
在本节中, 我们仅考虑单向数据传输(unidirectional data transfer)的情况,即数据传输是从发送端到接收端的. 可靠的双向数据传输(bidirectional data transfer)(即全双工数据传输)情况从概念上讲不会更难, 但解释起来更为单调乏味. 虽然我们只考虑单向数据传输, 注意到下列事实是重要的, 我们的协议也需要在发送端和接收端两个方向上传输分组, 如 图3-8 所示. 我们很快会看到, 除了交换含有待传送的数据的分组之外, rdt的发送端和接收端还需往返交换控制分组. rdt 的发送端和接收端都要通过调用udt_send()
发送分组给对方(其中udt表示不可靠数据传输).
构造可靠数据传输协议
我们现在一步步地研究一系列协议, 他们一个比一个复杂, 最后得到一个完美, 可靠地数据传输协议.
1. 经完全可靠信道的可靠数据传输: rdt1.0
首先, 我们考虑最简单地情况, 即底层信道是完全可靠的. 我们称该协议为rdt1.0, 该协议本身是简单的. 图3-9 显示了 rdt1.0 发送方和接收方的有限状态机(Finite-State Machine, FSM)的定义. 图3-9a 中的 FSM 定义了发送方的操作. 图3-9b中的FSM定义了接收方的操作. 值得注意的是, 发送方和接收方有各自的 FSM. 图3-9 中发送方和接收方的 FSM 每个都只有一个状态. FSM描述图中的箭头指示了协议从一个状态变迁到另一个状态(因为 图3-9 中的每个FSM都只有一个状态, 因此变迁必定是从一个状态返回自身)引起变迁的事件表示在横线上方, 事件发生梭采取的操作表示在横线下方(可以参考数电中的状态转移).如果对一个事件没有操作, 或没有就事件发生而采取操纵, 我们将在横线上方或下方使用符号A
, 以分别明确地表示缺少操作或事件. FSM的初始状态用虚线表示. 尽管 图3-9 中的FSM只有一个状态, 但马上我们就将看到多状态的FSM, 因此标识每个FSM的初始状态是非常重要的.

rdt 的发送端只通过rdt_send(data)
事件接受来自较高层的数据, 产生一个包含该数据的分组(经由make_pkt(data)
操作), 并将分组发送到信道中。实际上,rdt_send(data)
事件是由较高层应用的过程调用产生的(例如,rdt_send()
)。
在接收端,rdt 通过rdt_rev(packet)
事件从底层信道接收一个分组, 从分组中取出数据(经由extract(packet,data)
操作), 并将数据上传给较高层(通过deliver_data(data)
操作)。实际上, rdt_rcv(packet)
事件是由较低层协议的过程调用产生的(例如,rdt_rev()
)。
在这个简单的协议中, 一个单元数据与一个分组没差别。而且, 所有分组是从发送方流向接收方, 有了完全可靠的信道, 接收端就不需要提供任何反馈信息给发送方, 因为不必担心出现差错! 注意到我们也已经假定了接收方接收数据的速率能够与发送方发送数据一样快. 因此, 接收方没有必要请求发送速度慢一点.
2. 经具有比特差错信道的可靠数据传输: rdt2.0
底层信道更为实际的模型是分组中的比特可能受损的模型. 在分组的传输, 传播或缓存的过程中, 这种比特差错通常会出现在网络的物理部件中. 我们眼下还将继续假定所有发送的分组(虽然有些比特可能受损)将按其发送的顺序被接收.
在研发一种经这种信道进行可靠通信的协议之前, 首先考虑一下人们会怎样处理这类情形。考虑一下你自己是怎样通过电话口述一条长报文的。在通常情况, 报文接收者在听到、理解并记下每句话后可能会说“OK”。如果报文接收者听到一名含糊不清的话时, 他可能要求你重复那句容易误解的话。这种口述报文协议使用了肯定确认(positive acknowledgment)与否定确认(negative acknowledgment)。这些控制报文使得接收方可以让发送方知道哪些内容被正确接收, 哪些内容接收有误并因此需要重复。在计算机网络环境, 基于这样重传机制的可靠数据传输协议称为自动重传请求(Automatic Repeat reQuest,ARQ)协议。
重要的是, ARQ协议 中还需要另外三种协议功能来处理存在比特差错的情况:
- 差错检测: 首先, 需要一种机制以使接收方检测到何时出现了比特差错。前一节讲到, UDP 使用因特网检验和字段正是为了这个目的。在第6章中, 我们将更详细地学习差错检测和纠错技术。这些技术使接收方可以检测并可能纠正分组中的比特差错。此刻, 我们只需知道这些技术要求有额外的比特(除了等发送的初始数据比特之外的比特)从发送方发送到接收方; 这些比特将被汇集在 rdt2.0 数据分组的分组检验和字段中。
- 接收方反馈: 因为发送方和接收方通常在不同端系统上执行, 可能相隔数千英里, 发送方要了解接收方情况(此时为分组是否被正确接收)的唯一途径就是让接收方提供明确的反馈信息。在口述报文情况下回答的“肯定确认”(ACK)和“否定确认”(NAK)就是这种反馈的例子。类似的, rdt2.0 协议将从接收方向发送方回送 ACK 与 NAK 分组。理论上, 这些分组只需要一个比特长度, 如用0表示 NAK, 用1表示 ACK。
- 重传: 接收方收到有差错的分组时, 发送方将重传该分组文。
图3-10 说明了表示 rdt2.0 的 FSM,该数据传输协议采用了差错检测、肯定确认与和否定确认。

rdt2.0 的发送端有两个状态。在最左边的状态中, 发送端协议正等待来自上层传下来的数据。当rdt_send(data)
事件出现时,发送方将产生一个包含待发送数据的分组(sndp-kt)
, 带有检验和(就像在对UDP报文段使用的方法),然udt_send(sndpkt)
操作发送该分组。在最右边后经由的状态中,发送方协议等待来自接收方的 ACK 或 NAK分组。如果收到一个 ACK分组 [ 图3-10 中符号rdt_rev(revpkt)&&isSACK(reypkt)
对应该事件], 则发送方知道最近发送的分组已被正确接收, 因此协议返回到等待来自上层的数据的状态。如果收到一个 NAK分组, 该协议重传上一个分组并等待接收方为响应重传分组而回送的 ACK 和 NAK。注意到下列事实很重要: 当发送方处于等待 ACK 或 NAK 的状态时, 它不能从上层获得更多的数据; 这就是说,rdt_send()
事件不可能出现; 仅当接收到 ACK 并离开该状态时才能发生这样的事件。因此, 发送方将不会发送一块新数据, 除接受方确信接收方已正确接收当前分组。由于这种行为, rdt2.0 这样的协议被称为停等(stop-and-wait)协议.
rdt2.0 接收方的 FSM 仍然只有单一状态。当分组到达时, 接收方要么回答一个ACK, 要么回答一个 NAK, 这取决于收到的分组是否受损。在 图3-10 中, 符号rdt_rev(revpkt)&&corrupt(rcevpkt)
对应于收到一个分组并发现有错的事件.
rdt2.0 协议看起来似乎可以运行, 但遗憾的是, 它存在一个致命的缺聊。尤其是我们没有考虑到 ACK 或 NAK 分组受损的可能性. 遗憾的是, 我们细小的疏忽并非像它看起来那么无关紧要。至少, 我们需要在 ACK/NAK 分组中添加检验和比特以检测这样的差错。更难的问题是协议应该怎样纠正 ACK 或 NAK 分组中的差错。这里的难点在于, 如果一个 ACK 或 NAK 分组受损, 发送方无法知道接收方是否正确接收了上一块发送的数据。
考虑处理受损ACK和NAK时的三种可能性:
- 对于第一种可能性, 考虑在口述报文情况下人可能的做法。如果说话者不理解来自接收方回答的 “OK” 或 “请重复一遍”, 说话者将可能问 “你说什么?” (因此在我们的协议中引入了一种新型发送方到接收方的分组)。接收方则将复述其回答。但是如果说话者的 “你说什么?” 产生了差错, 情况又会怎样呢? 接收者不明白那句混淆的话是口述内容的一部分还是一个要求重复上次回答的请求, 很可能回一句“你说什么?”。于是, 该回答可能含糊不清了。显然, 我们走上了一条困难重重之路。
- 第二种可能性是增加足够的检验和比特, 使发送方不仅可以检测差错, 还可恢复差错。对于会产生差错但不丢失分组的信道, 这就可以直接解决问题。
- 第三种可能性是, 当发送方收到含糊不清的 ACK 或 NAK 分组时, 只需重传当前数据分组即可。然而, 这种方法在发送方到接收方的信道中引入了冗余分组(duplicate packet)。冗余分组的根本困难在于接收方不知道它上次所发送的 ACK 或 NAK 是否被发送方正确地收到。因此它无法事先知道接收到的分组是新的还是一次重传.
解决这个新问题的一种简单方法(几乎所有现有的数据传输协议都采用了这种方法)是在数据分组中添加一新字段, 让发送方对其数据分组编号, 即将发送数据分组的序号(sequence number)放在该字段。于是, 接收方只需要检查序号即可确定收到的分组是否为重传。对于停等协议这种简单情况, 1比特序号就足够了, 因为它可让接收方知道发送方是否正在重传前一个发送分组(接收到的分组序号与最近收到的分组序号相同), 或是一个新分组(序号变化了,用模2运算“前向”移动)。因为目前我们假定信道不丢分组, ACK 和 NAK 分组本身不需要指明它们要确认的分组序号。发送方知道所接收到的 ACK 和 NAK 分组(无论是否是含糊不清的)是为响应其最近发送的数据分组而生成的。
图3-11 和 图3-12 给出了对 rdt2.1 的 FSM 描述,这是 rdt2.0 的修订版。rdt2.1 的发送方和接收方 FSM 的状态数都是以前的两倍。这是因为协议状态此时必须反映出目前(由发送方)正发送的分组或(在接收方)希望接收的分组的序号是0还是1。值得注意的是, 发送或期望接收0号分组的状态中的操作与发送或期望接收1号分组的状态中的操作是相似的; 唯一的不同是序号处理的方法不同.
协议rdt2.1使用了从接收方到发送方的 ACK 和 NAK。当接收到失序的分组时, 接收方对所接收的分组发送一个肯定确认。如果收到受损的分组, 则接收方将发送一个和否定确认。如果不发送NAK, 而是对上次正确接收的分组发送一个ACK, 我们也能实现与 NAK 一样的效果, 因为发送方接收到对同一个分组的两个 ACK[即接收冗余ACK(duplicate ACK)] 后, 就知道接收方没有正确接收到跟在被确认两次的分组后面的分组(即后续的分组产生了乱序)。
对 rdt2.1 的工作流程举例讲解
- 假设发送方发送了分组1、2、3, 然后因为某些原因, 3号分组抵达时2号分组仍然未抵达.
- 1号分组如期抵达并无错误. 接收方返回ACK1
- 3号分组到达, 但是序号不对, 接收方会再次返回ACK1
- 发送方发现存在重复的ACK1, 判定1号分组之后的分组出现了乱序.并从2号分组开始重传
- 注意, 1, 2, 3这样的编号是为了方便理解. 具体实现上只需一个bit的数据量就可以编号了.因为当0号分组后不是1号分组就可以立即判定为失序, 触发上述行为.


rdt2.2 是在有比特差错信道上实现的一个无 NAK 的可靠数据传输协议, 如 图3-13 和 图3-14 所示。rdt2.1 和 rdt2.2 之间的细微变化在于, 接收方此时必须包括由一个 ACK 报文所确认的分组序号[这可以通过在接收方 FSM 中,在make_pkt()
中包括参数 ACK0 或 ACK1 来实现], 发送方此时必须检查接收到的 ACK 报文中被确认的分组序号[这可通过在发送方 FSM 中,在isACK()
中包括参数 0 或 1 来实现]。


3. 经具有比特差错的丢包信道的可靠数据传输: rdt3.0
现在假定除了比特受损外, 底层信道还会丢包, 这在今天的计算机网络(包括因特网)中并不罕见。协议现在必须处理另外两个问题: 怎样检测丢包以及发生丢包后该做些什么。在 rdt2.2 中已经研发的技术, 如使用检验和、序号、ACK 分组和重传等, 使我们能给出后一个问题的答案。为解决第一个关注的问题,还需增加一种新的协议机制。
有很多可能的方法用于解决丢包问题(在本章结尾的习题中研究了几种其他方法)。这里, 我们让发送方负责检测和恢复丢包工作。假定发送方传输一个数据分组, 该分组或者接收方对该分组发出的 ACK 发生了丢失。在这两种情况下, 发送方都收不到应当到来的接收方的响应。如果发送方愿意等待足够长的时间以便确定分组已丢失, 则它只需重传该数据分组即可。你应该相信该协议确实有效。
但是发送方需要等待多久才能确定已丢失了某些东西呢? 很明显发送方至少需要等待这样长的时间: 发送方与接收方之间的一个往返时延(可能会包括在中间路由器的缓冲时延)加上接收方处理一个分组所需的时间。在很多网络, 最坏情况下的最大时延是很难估算的, 确定的因素非常少。此外, 理想的协议应尽可能快地从丢包中恢复出来; 等待一个最坏情况的时延可能意味着要等待一段较长的时间, 直到启动差错恢复为止。因此实践中采取的方法是发送方明智地选择一个时间值, 以判定可能发生了丢包(尽管不能确保)。如果在这个时间内没有收到 ACK, 则重传该分组。注意到如果一个分组经历了一个特别大的时延, 发送方可能会重传该分组, 即使该数据分组及其 ACK 都没有丢失。这就在发送方到接收方的信道中引入了冗余数据分组(duplicate data packet)的可能性。幸运的是, rdt2.2 协议已经有足够的功能(即序号)来处理冗余分组情况。
从发送方的观点来看, 重传是一种万能灵药。发送方不知道是一个数据分组丢失, 还是一个 ACK 丢失, 或者只是该分组或 ACK 过度延时。在所有这些情况, 操作是同样的: 重传。为了实现基于时间的重传机制需要一个倒计数定时器(count down timer), 在一个给定的时间量过期后, 可中断发送方。因此, 发送方需要能做到:
- 每次发送一个分组(包括第一次分组和重传分组)时,便启动一个定时器;
- 响应定时器中断(采取适当的操作);
- 终止定时器。

图3-15 给出了 rdt3.0 的发送方 FSM, 这是一个在可能出错和丢包的信道传输数据的协议; 图3-16 显示了在没有丢包和延迟分组情况下协议运作的情况, 以及它是如何处理数据分组丢失的。在 图3-16 中, 时间从图的顶部朝底部移动; 注意到一个分组的接收时间必定迟于一个分组的发送时间, 这是因为发送时延与传播时延之故。在 图3-16 b~d 中,发送方括号部分表明了定时器的设置时刻以及随后的超时。本章后面的习题探讨了该协议几个更细微的方面。因为分组序号在0和1之间交替, 因此 rdt3.0 有时被称为比特交替协议(alternating-bit protocol)。

在检验和, 序号, 定时器, 肯定和否定确认分组这些技术中, 每种机制都在协议的运行中起到了必不可少的作用. 至此, 我们得到了一个可靠的传输协议.
流水线可靠数据传输协议
rdt3.0 是一个功能正确的协议, 但它的性能不尽人意, 特别是在今天的高速网络中更是如此。rdt3.0 性能问题的核心在于它是一个停等协议。
为了评价该停等行为对性能的影响, 可考虑一种具有两台主机的理想化场合, 一台主机位于美国西海岸, 另一台位于美国东海岸, 如图3-17所示。在这两个端系统之间的RTT大约为30ms。假定彼此通过一条发送速率民为 1Gbps(每秒$10^9$bit) 的信道相连。包括首部字段和数据的分组长L为1000字节(8000bit), 发送一个分组进入 1Gbps 链路实际所需时间是:$$t_{trans} = \frac{L}{R} = \frac{8000bit}{10^9 bit/s} = 8 \mu s$$
图3-18a 显示了对于该停等协议, 如果发送方在上 $t=0$ 时刻开始发送分组,则在 $t=L/R=8 \mu s$ 后,最后 1bit 数据进入了发送端信道。该分组经过 15ms 的穿越国家的旅途后到达接收端, 该分组的最后 1比特 在时刻上 $t = RTT/2 + L/R =15.008ms$ 时到达接收方。为了简化起见, 假设 ACK分组 很小(以便我们可以忽略其发送时间), 接收方一旦收到一个数据分组的最后 1bit 后立即发送 ACK, ACK 在时刻 $t = RTT+L/R = 30.008ms$ 时到达发送方。此时, 发送方可以发送下一个报文。因此, 在 30.008ms 内, 发送方的发送只用了 0.008ms。如果我们定义发送方(或信道)的利用率(utilization)为发送方实际忙于将发送比特送进信道的那部分时间与发送时间之比, 图3-18a 中的分析表明了停等协议有着非常低的发送方利用率 $U_{sender}$:$$U_{sender} = \frac{L/R}{RTT + L/R} = \frac{0.008}{30.008} = 0.00027$$ 这就是说, 发送方只有 0.027% 时间是忙的. 从其他角度来看, 发送方在 30.008ms 内只能发送 1000字节, 有效的吞吐量仅为267kbps, 即使有 1Gbps 的链路可用也是如此! 想象一个不幸的网络经理购买了一条千兆比容量的链路, 但他仅能得到267kbps吞吐量的情况, 这是一个形象的网络协议限制底层网络硬件所提供的能力的图例。而且, 我们还忽略了在发送方和接收方的底层协议处理时间, 以及可能出现在发送方与接收方之间的任何中间路由器上的处理与排队时延。考虑到这些因素, 将进一步增加时延, 使其性能更糟糕.
这种特殊的性能问题的一个简单解决方法是: 不以停等方式运行, 允许发送方发送多个分组而无须等待确认, 如 图3-17b 所示。图3-18b 显示了如果发送方可以在等待确认之前发送 3个报文, 其利用率也基本上提高3倍。因为许多从发送方向接收方输送的分组可以被看成是填充到一条流水线, 故这种技术被称为流水线(Pipe lining). 流水线技术对可靠数据传输协议可带来如下影响:
- 必须增加序号范围, 因为每个输送中的分组(不计算重传的)必须有一个唯一的序号, 而且也许有多个在输送中的未确认报文。
- 协议的发送方和接收方两端也许不得不缓存多个分组。发送方最低限度应当能缓冲那些已发送但没有确认的分组。如下面讨论的那样, 接收方或许也需要缓存那些已正确接收的分组。
- 所需序号范围和对缓冲的要求取决于数据传输协议如何处理丢失、损坏及延时过大的分组。解决流水线的差错恢复有两种基本方法:回退N步(Go-Back-N, GBN)和选择重传(Selective Repeat, SR)。

回退N步
在回退N步(CBN)协议, 允许发送方发送多个分组(当有多个分组可用时)而不需等待确认,但它也受限于在流水线中未确认的分组数不能超过某个最大允许数N。在本节中我们较为详细地描述CBN。
图3-19 显示了发送方看到的 CBN协议 的序号范围。如果我们将 基序号(base) 定义为最早未确认分组的序号, 将 下一个序号(nextseqnum) 定义为最小的未使用序号(即下一个待发分组的序号), 则可将序号范围分割成4段。在 [0, base-1]
段内的序号对应于已经发送并被确认的分组。 [base, nextseqnum-1]
段内对应已经发送但未被确认的分组。[nextseqnum, base+N-1]
段内的序号能用于那些要被立即发送的分组, 如果有数据来自上层的话。最后, 大于或等于base+N
的序号是不能使用的, 直到当前流水线中未被确认的分组(特别是序号为base的分)已得到确认为止。

如 图3-19 所示, 那些已被发送但还未被确认的分组的许可序号范围可以被看成是一个在序号范围内长度为N的窗口。随着协议的运行, 该窗口在序号空间向前滑动。因此, N常被称为窗口长度(window size), GBN协议也常被称为滑动窗口协议(sliding-window protocol)。你也许想知道, 我们为什么先要限制这些被发送的、未被确认的分组的数目为N呢? 为什么不允许这些分组为无限制的数目呢? 我们将在之后看到, 流量控制是对发送方施加限制的原因之一。并在学习TCP拥塞控制时分析另一个原因。
在实践中, 一个分组的序号承载在分组首部的一个国定长度的字段中。如果分组序号字段的比特数是 $k$, 则该序号范围是 $[0, 2^k-1]$。在一个有限的序号范围内, 所有涉及序号的运算必须使用模 $2^k$ 运算。(即序号空间可被看作一个长度为 $2^k$ 的环,其中序号 $2^k-1$ 紧接着序号 0。)前面讲过, rdt3.0 有一个 1比特 的序号, 序号范围是[0,1]。在本章末的几道习题中探讨了一个有限的序号范围所产生的结果。我们将在之后看到, TCP 有一个 32比特 的序号字段, 其中的 TCP序号 是按字节流中的字节进行计数的, 而不是按分组计数。
图3-20 和 图3-21 给出了一个基于 ACK、无 NAK 的 GBN协议 的发送方和接收方这两端的 扩展FSM 描述。我们称该 FSM 描述为 扩展FSM, 是因为我们已经增加了变量 base
和 nextseqnum
, 还增加了对这些变量的操作以及与这些变量有关的条件操作。注意到该扩展的 FSM 规约现在变得有点像编程语言规约。[Bochman1984] 对 FSM扩展技术 做了很好的综述, 也提供了用于定义协议的其他基于编程语言的技术。


GBN 发送方必须响应三种类型的事件:
- 上层的调用: 当上层调用
rdt_send()
时, 发送方首先检查发送窗口是否已满, 即是否有 N 个已发送但未被确认的分组。如果窗口未满, 则产生一个分组并将其发送, 并相应地更新变量。如果窗口已满, 发送方只需将数据返回给上层, 隐式地指示上层该窗口已满。然后上层可能会过一会儿再试。在实际实现中, 发送方更可能缓存(并不立刻发)这些数据或者使用同步机制(如一个信号量或标志)允许上层在仅当窗口不满时才调用rdt_send()
。 - 收到一个ACK:在GBN协议中, 对序号为 n 的分组的确认采取累积确认(cumulative acknowledgment)的方式, 表明接收方已正确接收到序号为 n 及之前的所有分组。稍后讨论GBN接收方一端, 我们将再次研究这个主题。
- 超时事件: 协议的名字 “回退N步” 来源于出现丢失和时延过长分组时发送方的行为。就像在停等协议中那样, 定时器将再次用于恢复数据或确认分组的丢失。如果出现超时, 发送方重传所有已发送但还未被确认过的分组。图3-20 中的发送方仅使用一个定时器, 它可被当作最早的已发送但未被确认的分组所使用的定时器。如果收到一个 ACK, 但仍有已发送但未被确认的分组, 则定时器被重新启动。如果没有已发送但未被确认的分组, 停止该定时器。
在 GBN 中, 接收方的操作也很简单。如果一个序号为 n 的分组被正确且有序地接受到(即上次交付给上层的数据是序号为 n-1 的分组), 则接收方为分组发送一个 ACK, 并将该分组中的数据部分交付到上层。在所有其他情况下, 接收方丢弃该分组, 并为最近按序接收的分组重新发送 ACK。注意到因为一次交付给上层一个分组, 如果分组 k 已接收并交付, 则所有序号比 k 小的分组也已经交付。因此, 使用累积确认是GBN一个自然的选择。
在 GBN 协议中, 接收方丢弃所有失序分组。尽管丢弃一个正确接收(但失序)的分组有点浪费, 但这样做是有理由的。前面讲过, 接收方必须按序将数据交付给上层。假定现在期望接收分组 n, 而分组 n+1 却到了。因为数据必须按序交付, 接收方可能缓存分组 n+1, 然后在它收到并交付分组 n 后, 再将该分组交付到上层。然而, 如果分组 n 丢失, 则该分组及分组 n+1 最终将在发送方根据GBN重传规则而被重传。因此, 接收方只需丢弃分组 n+1 即可。这种方法的优点是接收缓存简单, 即接收方不需要缓存任何失序分组。因此, 虽然发送方必须维护窗口的上下边界及 nextseqnum
在该窗口地位置, 但是接收方需要维护地唯一信息就是下一个按序接收的分组的序号。该值保存在 expectedseqnum
变量中, 如 图3-21中接收方 FSM 所示。当然, 丢弃一个正确接收的分组的缺点是随后对该分组的重传也许会丢失或出错, 因此可能需要更多次的重传。

图3-22 给出了窗口长度为 4 个分组的 GBN 协议的运行情况。因为该窗口长度的限制, 发送方发送分组 0~3, 然后在继续发送之前, 必须等待直到一个或者多个分组被确认。当接收到每一个连续的 ACK时, 该窗口便向前滑动, 发送方可以发送新的分组。在接收方, 分组 2 丢失, 因此分组3, 4, 5被发现时失序分组并被丢弃。
在结束对 GBN 的讨论之前, 需要注意, 在协议栈中实现该协议可能与 图3-20 中的扩展 FSM 有着相似的结构。该实现也可能是以各种过程形式出现, 每个过程实现了在响应各种可能出现的事件时要采取的操作。在这种基于事件的编程(event-based programming)方式中, 这些过程要么被协议栈中的其他过程调用, 要么作为一次中断的结果。在发送方, 这些事件包括:
- 来自上层实体的调用而调用
rdt_send()
- 定时器中断
- 报文到达时,来自下层的调用而调用
rdt_rev()
。
本章后面的编程作业会使你有机会在一个模拟网络环境中实际实现这些例程, 但该环境却是真实的。这里我们注意到, GBN 协议中综合了我们将在之后学习 TCP 可靠数据传输构件时遇到的所有技术。这些技术包括使用序号、累积确认、检验和以及超时/重传操作。
选择重传
在 图3-17 中, GBN 协议潜在地允许发送方用多个分组“填充流水线”, 因此避免了停等协议中所提到的信道利用率问题。然而, GBN 本身也有一些情况存在着性能问题。尤其是当窗口长度和带宽时延积(链路带宽*RTT)都很大时, 在流水线中会有很多分组更是如此。单个分组的差错就能够引起CBN重传大量分组, 许多分组根本没有必要重传。随着信道差错率的增加, 流水线可能会被这些不必要重传的分组所充斥。想象一下, 在我们口述消息的例子中, 如果每次有一个单词含糊不清, 其前后1000个单词(例如,窗口长度为1000个单词)不得不被重传的情况。此次口述会由于这些反复述说的单词而变慢。
顾名思义, 选择重传(SR)协议通过让发送方仅重传那些它怀疑在接收方出错(即丢失或受损)的分组而避免了不必要的重传。这种个别的、按需的重传要求接收方逐个确认正确接收的分组。再次用窗口长度 N 来限制流水线中未完成、未被确认的分组数. 然而, 与GBN不同的是, 发送方已经收到了对窗口中某些分组的 ACK。图3-23 显示了 SR 发送方看到的序号空间。图3-24 详细描述了 SR 发送方所采取的操作。

SR 接收方将确认一个正确接收的分组而不管其是和否按序。失序的分组将被缓存直到所有丢失分组(即序号更小的分组)皆被收到为止, 这时才可以将一批分组按序交付给上层。图3-25 详细列出了 SR 接收方所采用的各种操作。图3-26 给出了一个例子以说明出现丢包时SR的操作。值得注意的是, 在 图3-26 中接收方初始时缓存了分组3、4、5,并在最终收到分组2时, 才将它们一并上交给上层。


注意到 图3-25 中的第二步很重要, 接收方重新确认(而不是忽略)已收到过的那些序号小于当前窗口基序号的分组。你应该理解这种重新确认确实是需要的。例如, 给定在 图3-23 中所示的发送方和接收方的序号空间, 如果分组 send_base
的 ACK 没有从接收方传播回发送方, 则发送方最终将重传分组 send_base
, 即使显然接收方已经收到了该分组。如果接收方不确认该分组, 则发送方窗口将永远不能向前滑动! 这个例子说明了 SR 协议(和很多其他协议一样)的一个重要方面。对于哪些分组已经被正确接收, 哪些没有, 发送方和接收方并不总是能看到相同的结果。对 SR 协议而言, 这就意味着发送方和接收方的窗口并不总是一致(图3-26 中就显然不一致).
在序号范围(指给各个分组的编号)有限的情况下, 发送方和接收方窗口间缺乏同步会产生严重的后果。考虑下面例子中可能发生的情况, 该例有包括 4 个分组序号 0、1、2、3 的有限序号范围且窗口长度为 3。假定发送了分组 0 至 2, 并在接收方被正确接收且确认了。此时, 接收方窗口落在第 4、5、6 个分组上, 其序号分别为 3、0、1。现在考虑两种情况。在第一种情况下, 如 图3-27a 所示, 对前 3 个分组的 ACK 丢失, 因此发送方重传这些分组。因此, 接收方下一步要接收序号为 0 的分组, 即第一个发送分组的副本。

在第二种情况下, 如 图3-27b 所示, 对前 3 个分组的 ACK 都被正确交付。因此发送方向前移动窗口并发送第 4、5、6 个分组, 其序号分别为 3、0、1。序号为 3 的分组丢失, 但序号为 0 的分组到达(一个包含新数据的分组).
现在考虑一下 图3-27 中接收方的观点, 在发送方和接收方之间有一个假想的帘子, 因为接收方不能“看见”发送方采取的操作。接收方所能观察到的是它从信道中收到的以及它向信道中发出报文序列。就所关注的而言, 图3-27 中的两种情况是等同的。没有办法区分是第 1 个分组的重传还是第 5 个分组的初次传输。显然, 窗口长度比序号空间小 1 时协议无法工作。但窗口必须多小呢? 对于 SR 协议而言, 窗口长度必须小于或等于序号空间大小的一半。
为什么窗口长度须小于等于序号空间的一般一半?
在窗口长度为 2, 序号空间大小为 4 的情况下:
- 若发出了 0, 1, 则使用了 0, 1 连个空间, 接下来的发送窗口序号与接收窗口序号就是 2, 3; 而要发出的是 0, 1, 不会发生冲突。
- 若发出了 0, 1, 而 0 收到了 ACK, 1 超时了, 接下来的发送序号为 1, 2, 接收窗口为 1, 2; 此时也不存在重复, 不会冲突。
- 若发出了 0, 1, 而 0 收到了 ACK, 1 的接收 ACK 丢包了, 则需要重传 1 号分组, 接收窗口为 2, 3; 此时也不会存在冲突。
总结来说, 不论丢包与否, 何时丢什么包, 都必然不可能发生窗口和序号重叠的情况。
至此我们结束了对可靠数据传输协议的讨论。我们已涵盖许多基础知识, 并介绍了多种机制, 这些机制可一起提供可靠数据传输。表3-1 总结这些机制。既然我们已经学习了所有这些运行中的机制, 并能看到“全景”, 我们建议你再复习一遍本节内容, 看看这些机制是怎样逐步被添加进来, 以涵盖复杂性渐增的(也就是越发现实的)连接发送方与接收方的各种信道模型的, 或者如何改善协议性能的。

我们通过考虑在底层信道模型中的一个遗留假设来结束对可靠数据传输协议的讨论。前面讲过, 我们曾假定分组在发送方与接收方之间的信道中不能被重新排序。这在发送方与接收方由单段物理线路相连的情况下, 通常是一个合理的假设。然而, 当连接两端的“信道”是一个网络, 分组重新排序是可能会发生的。分组重新排序的一个表现就是, 一个具有序号或确认号 x 的分组的旧副本可能会出现, 即使发送方或接收方的窗口中都没有包含 x。对于分组重新排序, 信道可被看成基本上是在缓存分组, 并在将来任意时刻自然地释放出这些分组。由于序号可以被重新使用, 那么必须小心, 以免出现这样的冗余分组。实际应用中采用的方法是, 确保一个序号不被重新使用, 直到发送方“确信”任何先前发送的序号为 x 的分组都不再在网络中为止。通过假定一个分组在网络中的“存活”时间不会超过某个固定最大时间量来做到这一点。在高速网络的 TCP 扩展中, 最长的分组寿命被假定为大约3分钟 [RFC 7323]。 [Sunshine 1978] 描述了一种使用序号的方法, 它能够完全避免重新排序问题。
面向连接的运输: TCP
既然我们已经学习了可靠数据传输的基本原理, 我们就可以转而学习 TCP 了。TCP 是因特网运输层的面向连接的可靠的运输协议。我们在本节中将看到, 为了提供可靠数据传输, TCP 依赖于前一节所讨论的许多基本原理, 其中包括差错检测、重传、累积确认、定时器以及用于序号和确认号的首部字段。TCP 定义在 RFC793、RFC1122、RFC2018、RFC5681 和 REFC7323 中。
TCP 连接
TCP 被称为是面向连接的(connection-oriented), 这是因为在一个应用进程可以开始向另一个应用进程发送数据之前, 这两个进程必须先相互“握手”, 即它们必须相互发送某些预备报文段, 以建立确保数据传输的参数。作为 TCP 连接建立的一部分, 连接的双方都将初始化与 TCP 连接相关的许多 TCP 状态变量。
历史事件: Vinton Cerf, Robert Kahn 和 TCP/IP
在 20 世纪 70 年代早期, 分组交换网开始飞速增长, 而因特网的前身 ARPAnet 也只是当时众多分组交换网中的一个。这些网络都有它们各自的协议。两个研究人员Vinton Cerf 和 Robert Kahn 认识到互联网这些网络的重要性, 发明了沟通网络的 TCP/IP, 该协议代表传输控制协议/网际协议(Transmission Control Protocol/Internet Protocol)。 虽然Cerf 和 Kahn 开始时把该协会看成单一的实体, 但是后来将它分成单独运行的两个部分: TCP 和 IP。Cerf 和 Kahn 在1974年5月的IEEE Transactions on Commurnications Technrology 杂志上发表了一篇关于 TCP/IP 的论文 [Cerf 1974].
TCP/IP 是当今因特网的支柱性协议, 但它的发明先于PC、工作站、智能手机和平板电脑, 先于以太网、电缆、DSL、WiFi和其他接入网技术的激增, 先于Web、社交媒体和流式视频等。Cerf 和 Kahn 预见到了对于联网协议的需求, 一方面为行将定义的应用提供广泛的支持, 另一方面允许任何主机与链路层协议互操作。
2004年, Cerf 和 Kahn 由于“联网方面的开创性工作(包括因特网的基本通信协议TCP/IP 的设计和实现)以及联网方面富有才能的领导”而获得 ACM 图灵奖, 该奖项被认为是“计算机界的诺贝尔奖”。
这种 TCP “连接”不是一条像在电路交换网络中的端到端 TDM 或 FDM 电路。相反, 该“连接”是一条逻辑连接, 其共同状态仅保留在两个通信端系统的 TCP 程序中。前面讲过, 由于 TCP 协议只在端系统中运行, 而不在中间的网络元素(路由器和链路层交换机)中运行, 所以中间的网络元素不会维持 TCP 连接状态。事实上, 中间路由器对 TCP 连接完全视而不见, 它们看到的是数据报, 而不是连接完全视而不见, 它们看到的是数据报, 而不是连接。
TCP 连接提供的是全双工服务(full-duplex service): 如果一台主机上的进程 A 与另一台主机上的进程 B 存在一条 TCP 连接, 那么应用层数据就可以从进程 B 流向进程 A 的同时, 也从进程 A 流向进程 B。TCP 连接也总是点对点(Point-to-Point Protocol, PPP)的, 即在单个发送方与单个接收方之间的连接。所谓“多播”, 即在一次发送操作中, 从一个发送方将数据传送给多个接收方, 这种情况对 TCP 来说是不可能的。对于 TCP 而言,两台主机是一对, 而 3 台主机则太多了!
我们现在来看看 TCP 连接是怎样建立的。假设运行在某台主机上的一个进程想与另一台主机上的一个进程建立一条连接。前面讲过, 发起连接的这个进程被称为客户进程, 而另一个进程被称为服务器进程。该客户应用进程首先要通知客户运输层, 它想与服务器上的一个进程建立一条连接。一个 Python 客户程序通过发出下面的命令来实现此目的:
clientSocket.connect((serverName, serverPort))
其中 serverName
是服务器的名字, serverPort
标识了服务器上的进程。客户上的 TCP 便开始与服务器上的 TCP 建立一条 TCP 连接。我们将在本节后面更为详细地讨论连接建立的过程。现在知道下列事实就可以了:
- 客户首先发送一个特殊的 TCP 报文段
- 服务器用另一个特殊的 TCP 报文段来响应
- 客户再用第三个特殊报文段作为响应。
前两个报文段不承载“有效载荷”, 也就是不包含应用层数据; 而第三个报文段可以承载有效载荷。由于在这两台主机之间发送了 3 个报文段, 所以这种连接建立过程常被称为三次握手(three-wayhandshake)。
一旦建立起一条 TCP 连接, 两个应用进程之间就可以相互发送数据了。我们考虑一下从客户进程向服务器进程发送数据的情况。客户进程通过套接字(socket)传递数据流。数据一旦通过该 socket, 它就由客户中运行的 TCP 控制了。如 图3-28 所示, TCP 将这些数据引导到该连接的发送缓存(send buffer)里, 发送缓存是发起三次握手期间设置的缓存之一。接下来 TCP 就会不时从发送缓存里取出一块数据, 并将数据传递到网络层。有趣的是, 在 TCP 规范 [RFC793] 中却没提及 TCP 应何时实际发送缓存里的数据, 只是描述为“TCP 应该在它方便的时候以报文段的形式发送数据”。TCP可从缓存中取出并放入报文段中的数据数量受限于最大报文段长度(Maximum Segment Size, MSS)。MSS 通常根据最初确定的由本地发送主机发送的最大链路层帧长度 [即所谓的最大传输单元(Maximum Transmission Unit, MTU)]来设置。设置该 MSS 要保证一个 TCP 报文段(当封装在一个了数据报)加上 TCP/IP 首部长度(通常 40 字节)将适合单个链路层帧。以太网和 PPP 链路层协议都具有 1500 字节的 MTU, 因此 MSS 的典型值为 1460 字节。已经提出了多种发现路径 MTU 的方法, 并基于路径 MTU 值设置 MSS (路径 MTU 是指能在从源到目的地的所有链路上发送的最大链路层帧 [RFC1191])。注意到 MSS 是指在报文段里应用层数据的最大长度, 而不是指包括首部的 TCP 报文段的最大长度。(该术语很容易混淆, 但是我们不得不采用它, 因为它已经根深蒂固了。)
TCP 为每块客户数据配上一个 TCP 首部, 从而形成多个TCP报文段(TCP segment)。这些报文段被下传给网络层, 网络层将其分别封装在网络层 IP 数据报中。然后这些 IP 数据报被发送到网络中。当 TCP 在另一端接收到一个报文段后, 该报文段的数据就被放和人该 TCP 连接的接收缓存中, 如 图3-28 中所示。应用程序从此缓存中读取数据流。该连接的每一端都有各自的发送缓存和接收缓存。

从以上讨论中我们可以看出, TCP 连接的组成包括: 一台主机上的缓存、变量和与进程连接的套接字, 以及另一台主机上的另一组缓存、变量和与进程连接的套接字。如前面讲过的那样, 在这两台主机之间的网络元素(路由、交换机和中继融)中, 没有为该连接分配任何缓存和变量。
TCP 报文段结构