第二章 应用层
第二章 应用层
网络应用程序是计算机网络的基础。如果我们想不出任何有用的应用程序,就不需要网络基础设施和协议来支持它们。自从互联网诞生以来,确实出现了许多实用而有趣的应用程序。这些应用程序一直是互联网成功背后的推动力,激励家庭、学校、政府和企业将互联网作为他们日常活动的一部分。
Internet应用程序包括在20世纪70年代和80年代流行起来的经典的基于文本的应用程序:文本电子邮件、对计算机的远程访问、文件传输和新闻组。它们包括20世纪90年代中期的杀手级应用——万维网,包括网上冲浪、搜索和电子商务。自从新千年开始,新的和高度引人注目的应用程序不断出现,包括IP语音和视频会议,如Skype, Facetime和谷歌Hangouts;用户生成的视频(如YouTube)和点播电影(如Netflix);以及多人在线游戏,如《第二人生》和《魔兽世界》。在此期间,Facebook、Instagram、Twitter等新一代社交网络应用程序在因特网或路由器和通信链路上建立了人际网络。最近,随着智能手机的普及和4G/5G无线互联网接入的普及,出现了大量基于位置的移动应用,包括流行的签到、约会和道路交通预测应用(如Yelp、Tinder和Waz),移动支付应用(如微信、Apple Pay)和即时通讯应用(如微信、WhatsApp)。显然,新的、令人兴奋的互联网应用程序并没有放慢速度。也许本文的一些读者将创建下一代杀手级互联网应用程序。
在本章中,我们将研究网络应用程序的概念和实现方面。我们首先定义关键的应用层概念,包括应用程序、客户端和服务器、进程和传输层接口所需的网络服务。我们详细研究了几个网络应用程序,包括Web、电子邮件、DNS、点对点(P2P)文件分发和视频流。然后我们讨论网络应用程序开发,包括TCP和UDP。特别是,我们将研究套接字接口,并使用Python粗略实现一些简单的客户端-服务器应用程序。在本章的最后,我们还提供了几个有趣的套接字编程作业。
应用层是我们开始研究协议的一个特别好的地方。我们会知晓许多依赖于我们将要研究的协议的应用程序。它会让我们很好地了解协议是关于什么的,还会在我们再次学习传输、网络、链路层协议时引入许多相同的问题。
2.1 网络应用原理
假设您有一个新的网络应用程序的想法。也许这个应用程序将是一个伟大的服务于人类,或将取悦你的教授,或将给你带来巨大的财富,或仅仅是开发的乐趣。不管动机是什么,现在让我们看看如何将这个想法转化为现实世界的网络应用程序。
网络应用程序开发的核心是编写运行在不同端系统上并通过网络相互通信的程序。例如,在Web应用程序中有两个相互通信的不同程序:运行在用户主机(台式机、笔记本电脑、平板电脑、智能手机等)中的浏览器程序;以及运行在Web服务器主机上的Web服务器程序。另一个例子是,在Netflix等视频点播应用程序(参见章节2.6)中,有一个Netflix提供的程序运行在用户的智能手机、平板电脑或计算机上;以及在Netflix服务器主机上运行的Netflix服务器程序。服务器通常(但肯定不总是)位于数据中心中,如图2.1所示。
2.1.1网络应用架构
在开始编写软件代码之前,您应该为您的应用程序制定一个广泛的架构计划。请记住,应用程序架构与网络架构是截然不同的(例如,在第一章中讨论的五层Internet架构)。从应用程序开发人员的角度来看,网络架构是固定的,并为应用程序提供了一组特定的服务。另一方面, 应用程序架构 是由应用程序开发人员设计的,并规定了应用程序在各种端系统上的结构。在选择应用程序架构时,应用程序开发人员可能会使用现代网络应用程序中使用的两种主要架构范式之一:客户端-服务器架构或点对点(P2P)架构。
在 客户端-服务器 架构中,有一个始终在线的主机,称为服务器,它为来自许多其他主机(称为客户端)的请求提供服务。一个经典的例子是Web应用程序,它的Web服务器为来自客户端主机上运行的浏览器的请求提供服务。当Web服务器从客户端主机接收到一个对象的请求时,它的响应方式是将请求的对象发送给客户端主机。注意,在客户端-服务器架构中,客户端之间不直接通信;例如,在Web应用程序中,两个浏览器不直接通信。客户端-服务器架构的另一个特征是,服务器有一个固定的、众所周知的地址,称为IP地址(我们将很快讨论这个地址)。因为服务器有一个固定的、众所周知的地址,而且服务器总是处于打开状态,所以客户端可以通过发送数据包到服务器的IP地址来联系服务器。一些比较知名的具有客户端-服务器架构的应用程序包括Web、FTP、Telnet和电子邮件。客户端-服务器架构如图2.2(a)所示。
通常在客户端-服务器应用程序中,单服务器主机无法跟上来自客户端的所有请求。例如,一个流行的社交网站如果只有一个服务器处理它所有的请求,那么它很快就会不堪重负。因此, 数据中心 (包含大量主机)通常用于创建功能强大的虚拟服务器。最流行的互联网服务,如搜索引擎(如谷歌、Bing、百度)、互联网商务(如Amazon、eBay、Alibaba)、基于web的电子邮件(如Gmail和Yahoo Mail)、社交媒体(如Facebook、Instagram、Twitter和微信)都运行在一个或多个数据中心。如1.3.3节所述,谷歌有19个分布在世界各地的数据中心,它们共同处理搜索、YouTube、Gmail和其他服务。一个数据中心可以有数十万台服务器,这些服务器必须提供电源并进行维护。此外,服务提供商必须支付从其数据中心发送数据的再发互连(recurring interconnection)和带宽费用。
在 P2P架构 中,对数据中心专用服务器的依赖最小(或不依赖)。相反,应用程序利用间歇连接的主机对之间的直接通信,称为对等点(peers)。对等点不属于服务提供商,而是由用户控制的台式机和笔记本电脑,大多数对等点在家庭、大学或办公室里。因为对等交流这种架构称为无需经过专用服务器。一个流行的P2P应用程序的例子是文件共享应用程序BitTorrent。
P2P架构最引人注目的特征之一是其 自扩展性(self-scalability) 。例如,在P2P文件共享应用程序中,虽然每个对等点通过请求文件产生工作负载,但每个对等点也通过将文件分发给其他对等点来增加系统的服务容量。P2P架构也具有成本效益,因为它们通常不需要大量的服务器基础设施和服务器带宽(与数据中心的客户端-服务器设计相反)。然而,P2P应用由于其高度分散的结构而面临着安全、性能和可靠性的挑战。
2.1.2 进程间通信
在构建您的网络应用程序之前,您还需要基本了解在多个端系统中运行的程序如何相互通信。在操作系统的行话中,它实际上不是程序通信,而是 进程 通信。进程可以被看作是一个运行在端系统中的程序。当进程运行在同一个端系统上时,它们可以使用由端系统操作系统控制的规则,通过进程间通信相互通信。但是在本书中,我们对同一主机上的进程如何通信不是特别感兴趣,而是对不同主机(可能使用不同的操作系统)上运行的进程如何通信感兴趣。
两个不同端系统上的进程通过计算机网络交换 消息(message) 来相互通信。发送进程创建消息并将消息发送到网络中;接收进程接收这些消息,并可能通过返回消息进行响应。图2.1说明了进程之间的通信位于五层协议栈的应用层。
客户端和服务器进程
网络应用程序由成对的进程组成,这些进程通过网络互相发送消息。例如,在Web应用程序中,客户端浏览器进程与Web服务器进程交换消息。在P2P文件共享系统中,文件从一个对等点的进程传输到另一个对等点的进程。对于每一对通信进程,我们通常将两个进程中的一个标记为 客户端 ,另一个标记为 服务器 。对于Web,浏览器是一个客户端进程,而Web服务器是一个服务器进程。在P2P文件共享中,下载文件的对等点被标记为客户端,上传文件的对等点被标记为服务器。
您可能已经注意到,在某些应用程序中,例如在P2P文件共享中,一个进程可以同时是客户端和服务器。事实上,P2P文件共享系统中的进程可以上传和下载文件。然而,在一对进程之间的任何给定通信会话的环境中,我们仍然可以将一个进程标记为客户端,而将另一个进程标记为服务器。我们将客户端和服务器进程定义如下:
在一对进程之间的通信会话的环境中,发起通信的进程(即在会话开始时最初与另一个进程接触)被标记为客户端。等待被联系以开始会话的进程是服务器。
在Web中,浏览器进程初始化与Web服务器进程的联系;因此,浏览器进程是客户端,Web服务器进程是服务器。在P2P文件共享中,当对等点A请求对等点B发送特定的文件时,对等点A是该特定通信会话的客户端,对等点B是该特定通信会话的服务器。在没有混淆的情况下,我们有时还会使用应用程序的客户端和服务器端这两个术语。在本章的最后,我们将逐步介绍网络应用程序的客户端和服务器端的简单代码。
进程和计算机网络间的接口
如上所述,大多数应用程序由成对的通信进程组成,每对进程中的两个进程互相发送消息。从一个进程发送到另一个进程的任何消息都必须经过底层网络。进程通过一个称为 套接字(socket) 的软件接口向网络发送消息,并从网络接收消息。让我们考虑一个类比来帮助我们理解进程和套接字。进程类似于房子,它的套接字类似于它的门。当一个进程想要向另一个主机上的另一个进程发送消息时,它会将消息推出它的门(套接字)。此发送进程假定在其门的另一侧有一个传输基础设施,用于将消息传输到目标进程的门。一旦消息到达目标主机,该消息将通过接收进程的门(套接字),然后接收进程对该消息进行操作。
图2.3说明了两个通过Internet进行通信的进程之间的套接字通信。(图2.3假设进程使用的底层传输协议是Internet的TCP协议。)如图所示,套接字是主机内应用层和传输层之间的接口。它也被称为应用程序和网络之间的 应用程序编程接口( API) ,因为套接字是用来构建网络应用程序的编程接口。应用程序开发人员可以控制套接字的应用层端的所有内容,但对套接字的传输层几乎没有控制权。应用程序开发人员在传输层方面拥有的唯一控制是(1)传输协议的选择,以及(2)可能修改一些传输层参数的能力,如最大缓冲区和最大段(segment)大小(将在第三章讨论)。一旦应用程序开发者选择了一个传输协议(如果有选择),应用程序将使用该协议提供的传输层服务来构建。我们将在第2.7节详细探讨套接字。
寻址进程
为了将邮件发送到特定的目的地,目的地需要有一个地址。类似地,为了让运行在一台主机上的进程向运行在另一台主机上的进程发送数据包,接收进程需要有一个地址。要识别接收进程,需要指定两条信息:(1)主机的地址和(2)指定目标主机中接收进程的标识符。
在Internet中,主机由其 IP地址 标识。我们将在第4章详细讨论IP地址。现在,我们只需要知道IP地址是一个32位的数字,我们可以认为它是唯一标识主机的。除了知道消息要发送到的主机的地址外,发送进程还必须标识主机中运行的接收进程(更确切地说,是接收套接字)。这一信息是必需的,因为通常一个主机可能运行许多网络应用程序。目标 端口号 用于此目的。流行的应用程序被分配了特定的端口号。例如,Web服务器通过端口号80来标识。邮件服务器进程(使用SMTP协议)通过端口号25来标识。所有互联网标准协议的众所周知的端口号列表可以在www.iana.org上找到。我们将在第3章详细探讨端口号。
2.1.3 应用可使用的传输服务
回想一下,套接字是应用程序进程和传输层协议之间的接口。发送方的应用程序通过套接字推送消息。在套接字的另一端,传输层协议负责将消息发送到接收进程的套接字。
许多网络,包括Internet,提供一个以上的传输层协议。在开发应用程序时,必须选择可用的传输层协议之一。你如何做出选择?最可能的情况是,您将研究可用的传输层协议所提供的服务,然后选择具有最适合您的应用程序需求的服务的协议。这种情况类似于在两个城市之间选择火车或飞机运输。你必须选择其中一种,而且每种交通方式都提供不同的服务。(例如,火车提供市中心的接送服务,而飞机提供更短的旅行时间)。传输层协议可以为调用它的应用程序提供哪些服务?我们可以将可能的服务大致分为四个方面:可靠的数据传输、吞吐量、时间和安全性。
可靠数据传输
正如在第一章中所讨论的,数据包可能会在计算机网络中丢失。例如,一个数据包可能会溢出路由器的缓冲区,或者在它的一些比特损坏后被主机或路由器丢弃。对于许多应用程序,如电子邮件、文件传输、远程主机访问、Web文档传输和金融应用程序,数据丢失可能会造成灾难性的后果(在后一种情况下,对银行或客户都是如此!)因此,为了支持这些应用程序,必须做一些事情来保证应用程序一端发送的数据被正确地、完全地交付到应用程序的另一端。如果协议提供了这样一种有保证的数据交付服务,就称为提供了 可靠数据传输 。传输层协议可能为应用程序提供的一个重要服务是进程到进程的可靠数据传输。当传输协议提供此服务时,发送进程可以将其数据传递到套接字,并完全确信数据将在接收进程中到达,不会出现错误。
当传输层协议不能提供可靠的数据传输时,发送进程发送的一些数据可能永远不会到达接收进程。对于 容错应用程序(loss-tolerant applications) 来说,这是可以接受的,尤其是多媒体应用程序,比如可以容忍一些数据丢失的对话音/视频。在这些多媒体应用程序中,丢失的数据可能会导致音频/视频的一个小故障,而不是一个关键的缺陷。
吞吐量
在第一章中,我们介绍了可用吞吐量的概念,在网络路径上两个进程之间的通信会话的环境中,可用吞吐量是指发送进程向接收进程传送比特的速率。由于其他会话将沿着网络路径共享带宽,而且这些其他会话将来来往往,因此可用吞吐量可能随时间波动。这些观察结果导致了传输层协议可以提供的另一种自然服务,即以特定速率保证可用吞吐量。使用这样的服务,应用程序可以请求r比特/秒的保证吞吐量,然后传输协议将确保可用吞吐量总是至少为r比特/秒。这种有保证的吞吐量服务将吸引许多应用程序。例如,如果一个Internet电话应用程序以32kbps的速度编码语音,那么它需要将数据发送到网络并以这个速度将数据发送到接收应用程序。如果传输协议不能提供这种吞吐量,应用程序需要更低的编码率编码(和接收足够的吞吐量来维持这个更低的编码率)或可能不得不放弃。具有吞吐量要求的应用程序称为 带宽敏感应用程序(bandwidth-sensitive applications) 。目前许多多媒体应用程序对带宽敏感,尽管有些多媒体应用程序可能使用自适应编码技术,以匹配当前可用吞吐量的速度对数字化的声音或视频进行编码。
虽然对带宽敏感的应用程序有特定的吞吐量要求,但 弹性应用程序 可以尽可能多地或尽可能少地利用吞吐量。电子邮件、文件传输和Web传输都是弹性应用程序。当然,吞吐量越大越好。俗话说,“不可能太富,不可能太瘦,也不可能有太多的吞吐量!“
时间
传输层协议还可以提供时间(timing)保证。与吞吐量保证一样,时间保证可以有多种形式。一个例子可能是,发送方泵入套接字的每一个比特到达接收方的套接字都不会超过100毫秒。这样的服务将吸引交互式实时应用程序,如互联网电话、虚拟环境、电话会议和多人游戏,所有这些都需要严格的数据交付时间限制才能有效,参见[Gauthier 1999;Ramjee 1994]。例如,网络电话的长时间延迟往往会导致谈话中出现不自然的停顿;在多人游戏或虚拟交互环境中,在采取行动和看到环境的响应(例如,从端到端连接末端的另一个玩家)之间的长时间延迟使应用程序感觉不那么真实。对于非实时应用程序,较低的延迟总是优于较高的延迟,但是对端到端延迟没有严格的限制。
安全
最后,传输协议可以为应用程序提供一个或多个安全服务。例如,在发送主机中,传输协议可以对发送进程传输的所有数据进行加密;在接收主机中,传输层协议可以在将数据交付给接收进程之前对数据进行解密。这样的服务将在两个进程之间提供保密性,即使在发送和接收进程之间以某种方式观察到数据。除了保密性之外,传输协议还可以提供其他安全服务,包括数据完整性和端点身份验证,这些主题我们将在第8章中详细讨论。
2.1.4 Internet提供的传输服务
到目前为止,我们认为计算机网络一般可以提供传输服务。现在让我们更具体地了解一下Internet提供的传输服务的类型。Internet(以及更普遍的TCP/ IP网络)为应用程序提供了两种传输协议,UDP和TCP。当您(作为应用程序开发人员)创建一个网络应用程序时,你必须做的第一个决定是使用UDP还是TCP。这些协议中的每一个都为调用应用程序提供一组不同的服务。图2.4显示了某些选定应用程序的业务需求。
在互联网上,你首先要做的决定之一就是使用UDP还是TCP。这些协议中的每一个都为调用应用程序提供一组不同的服务。图2.4显示了某些选定应用程序的业务需求。
TCP服务
TCP服务模型包括一个面向连接的服务和一个可靠的数据传输服务。当应用程序调用TCP作为其传输协议时,应用程序将从TCP接收到这两个服务:
- 面向连接的服务 TCP在应用程序级消息开始流动之前,让客户端和服务器相互交换传输层控制信息。这个所谓的握手过程会提醒客户端和服务器,让它们为大量的数据包做好准备。握手阶段结束后,两个进程的套接字之间就存在一个 TCP连接 。该连接是全双工(full-duplex)连接,因为两个进程可以同时通过该连接向对方发送消息。当应用程序完成发送消息时,它必须关闭连接。在第三章中,我们将详细讨论面向连接的服务,并研究它是如何实现的。
- 可靠的数据传输服务 通信进程可以依赖于TCP,以正确的顺序、无错误地交付所有发送的数据。当应用程序的一端将字节流交付给套接字时,它可以依靠TCP将相同的字节流交付给接收套接字,而不丢失或重复字节。
TCP还包括一个拥塞控制机制,这是一种为Internet的普遍福祉而不是通信进程的直接利益提供的服务。当发送方和接收方之间发生网络拥塞时,TCP拥塞控制机制会限制发送进程(客户端或服务器)。正如我们将在第三章中看到的,TCP拥塞控制也试图限制每个TCP连接在网络带宽的公平份额。
UDP服务
UDP是一种简单、轻量级的传输协议,提供最小的服务。UDP是无连接的,所以在两个进程开始通信之前没有握手。UDP提供了一个不可靠的数据传输服务,也就是说,当一个进程将消息发送到UDP套接字时,UDP不能保证消息始终到达接收进程。此外,到达接收进程的消息可能会无序到达。
聚焦安全之——安全TCP
TCP和UDP都不提供任何加密——发送进程传送到套接字的数据与通过网络传送到目标进程的数据是一样的。因此,例如,如果发送进程将密码明文(即未加密的)发送到套接字中,明文密码将在发送方和接收方之间的所有链路上传播,可能会在任何中间链接上被嗅探和发现。由于隐私和其他安全问题已经成为许多应用程序的关键,互联网社区已经开发了一种TCP增强,称为传输层安全(Transport Layer security, TLS) [RFC 5246]。用TLS增强的TCP不仅完成传统TCP所做的一切,而且还提供关键的进程到进程的安全服务,包括加密、数据完整性和端点身份验证。我们强调TLS不是与TCP和UDP在同一级别上的第三个Internet传输协议,而是TCP的一个增强,这些增强在应用层实现。特别是,如果应用程序想要使用TLS的服务,它需要在应用程序的客户端和服务器端都包含TLS代码(现有的、高度优化的库和类)。TLS有自己的套接字API,类似于传统的TCP套接字API。当应用程序使用TLS时,发送进程将明文数据传递给TLS套接字,然后发送主机中的TLS对数据进行加密,并将加密后的数据传递给TCP套接字。在接收进程中,加密的数据通过Internet传输到TCP套接字。接收套接字将加密的数据传递给TLS,后者对数据进行解密。最后,TLS通过其TLS套接字将明文数据传递给接收进程。我们将在第8章中详细讨论TLS。
UDP不包括拥塞控制机制,所以UDP的发送端可以以任何它喜欢的速率将数据泵入下面的层(网络层)。(但是,请注意,由于中间链路的传输能力有限或由于拥塞,实际的端到端吞吐量可能低于这个速率)。
Internet传输协议不提供的服务
我们按照四个维度组织了传输协议服务:可靠的数据传输、吞吐量、时间和安全性。哪些服务是由TCP和UDP提供的?我们已经注意到TCP提供可靠的端到端数据传输。我们也知道,TCP可以很容易地在应用层用TLS进行增强来提供安全服务。但是在我们对TCP和UDP的简短描述中,明显没有提到吞吐量或时间保证服务,而这些服务并不是由今天的互联网传输协议提供的。这是否意味着像网络电话这样对时间敏感的应用程序不能在今天的互联网上运行?答案显然是否定的,因为Internet已经托管了对时间敏感的应用程序很多年了。这些应用程序通常工作得相当好,因为它们被设计为在最大程度上应对这种缺乏保证的情况。然而,当延迟过大或端到端吞吐量受到限制时,聪明的设计也有其局限性。总之,今天的Internet通常可以为时间敏感的应用程序提供满意的服务,但它不能提供任何时间或吞吐量保证。
图2.5显示了一些流行的Internet应用程序所使用的传输协议。我们看到电子邮件、远程终端访问、Web和文件传输都使用TCP。这些应用程序之所以选择TCP,主要是因为TCP提供可靠的数据传输,保证所有数据最终都会到达目的地。因为Internet电话应用程序(如Skype)通常可以容忍一些丢包,但需要最小的速率才能有效,所以Internet电话应用程序的开发人员通常更喜欢在UDP上运行他们的应用程序,从而绕过TCP的拥塞控制机制和数据包开销。但由于许多防火墙被配置为阻止(大多数类型)UDP流量,互联网电话应用程序通常被设计为在UDP通信失败时使用TCP作为备份。
图2.5 流行的Internet应用程序、它们的应用层协议以及它们的底层传输协议
2.1.5 应用层协议
我们刚刚学习了网络进程通过将消息发送到套接字来相互通信。但是这些消息是如何构建的呢?消息中各个字段的含义是什么?进程何时发送消息?这些问题将我们带入应用层协议领域。 应用层协议 定义了运行在不同端系统上的应用程序进程如何相互传递消息。具体来说,应用层协议定义了:
- 交换的消息类型,例如请求消息和响应消息。
- 各种消息类型的语法,比如消息中的字段以及字段的描述方式。
- 字段的语义,即字段中信息的含义。
- 用于确定进程何时以及如何发送消息和响应消息的规则。
一些应用层协议是在RFC中指定的,因此属于公共领域。例如,Web的应用层协议,HTTP(超文本传输协议[RFC7230])就是一个RFC。如果浏览器开发人员遵循HTTP RFC规则,那么浏览器将能够从任何也遵循HTTP RFC规则的Web服务器检索Web页面。许多其他应用层协议是专有的,有意不在公共领域中使用。例如,Skype使用专有的应用层协议。
区分网络应用和应用层协议很重要。应用层协议只是网络应用程序的一部分(尽管从我们的角度来看,它是应用程序非常重要的一部分!)让我们看几个例子。Web是一个客户端-服务器应用程序,允许用户根据需要从Web服务器获取文档。Web应用程序由许多组件组成,包括文档格式标准(即HTML)、Web浏览器(例如,Chrome和IE)、Web服务器(例如,Apache和Microsoft服务器)和应用层协议。Web的应用层协议HTTP定义了浏览器和Web服务器之间交换消息的格式和顺序。因此,HTTP只是Web应用程序的一部分(重要的一部分)。作为另一个例子,我们将在2.6节中看到Netflix的视频服务也有很多组件,包括存储和传输视频的服务器,管理账单和其他客户端功能的服务器,客户端(例如,您的智能手机、平板电脑或计算机上的Netflix应用程序),以及应用程序级的DASH协议,定义了Netflix服务器和客户端之间交换的消息的格式和顺序。因此,DASH只是Netflix应用程序的一部分(重要的一部分)。
2.1.6 本书涉及的网络应用
每天都有新的应用程序被开发出来。我们没有以百科全书的方式覆盖大量的Internet应用程序,而是选择集中于少数普遍且重要的应用程序。在本章中,我们将讨论五个重要的应用:Web、电子邮件、目录服务、视频流和P2P应用。我们首先讨论Web,不仅因为它是一个非常流行的应用程序,还因为它的应用层协议HTTP简单易懂。接着,我们将讨论因特网的第一个杀手级应用——电子邮件。电子邮件比Web更复杂,因为它使用的不是一个而是几个应用层协议。在电子邮件之后,我们将介绍DNS,它为Internet提供目录服务。大多数用户不直接与DNS进行交互;相反,用户通过其他应用程序(包括Web、文件传输和电子邮件)间接地调用DNS。DNS很好地说明了如何在Internet的应用层实现核心网络功能(网络名称到网络地址的转换)。然后我们讨论P2P文件共享应用程序,并通过讨论视频流,包括通过内容分发网络分发存储的视频来完成我们的应用研究。
2.2 Web和HTTP
直到20世纪90年代早期,因特网主要被研究人员、学者和大学生用来登录到远程主机上,在本地主机和远程主机之间传输文件,反之亦然,接收和发送新闻,以及接收和发送电子邮件。尽管这些应用程序非常有用(并且将继续如此),但在学术和研究领域之外,互联网基本上还是未知的。然后,在20世纪90年代早期,一个重要的新应用程序出现在了舞台上——万维网[Berners-Lee 1994]。Web是第一个被大众所关注的互联网应用程序。它极大地改变了人们在工作环境内外的互动方式。它将互联网从众多数据网络中的一个提升到本质上唯一的数据网络。
也许最吸引用户的是Web按需运行。当用户需要的时候,他们就会得到他们想要的东西。这与传统的广播和电视不同,当内容提供商提供内容时,迫使用户收听。除了随需随用之外,Web还有许多人们喜爱和珍惜的精彩功能。对于任何个人来说,在网络上发布信息都是非常容易的,每个人都可以以极低的成本成为发布者。超链接和搜索引擎帮助我们在信息的海洋中导航。照片和视频刺激我们的感官。表单、JavaScript、视频和许多其他设备使我们能够与页面和站点进行交互。网络及其协议为YouTube、基于Web的电子邮件(如Gmail)以及包括Instagram和谷歌地图在内的大多数移动互联网应用提供了平台。
2.2.1 HTTP概述
Web的应用层协议—— 超文本传输协议(HTTP) 是Web的核心。它在[RFC 1945]、[RFC 7230]和[RFC 7540]中有定义。HTTP在两个程序中实现:一个客户端程序和一个服务器程序。在不同的端系统上执行的客户端程序和服务器程序通过交换HTTP消息来相互交谈。HTTP定义了这些消息的结构,以及客户端和服务器如何交换消息。在详细解释HTTP之前,我们应该回顾一些Web术语。
Web页面 (也称为文档)由对象组成——对象是一个简单的文件——如HTML文件、JPEG图像、javascript文件、CCS样式表文件或视频剪辑——通过单个URL寻址。大多数Web页面由一个 基础HTML文件(base HTML file) 和几个引用对象组成。例如,如果一个Web页面包含HTML文本和5个JPEG图像,那么该Web页面有6个对象:基础HTML文件和5个图像。基础HTML文件使用对象的URL引用页面中的其他对象。每个URL都有两个组件:存放对象的服务器主机名和对象的路径名。例如,URL: http://www.someSchool.edu/someDepartment/picture.gif 其主机名为www.someSchool.edu和路径名/someDepartment/picture.gif。因为Web浏览器(如IE和Chrome)实现了HTTP的客户端,所以在Web环境中,我们将交替使用浏览器和客户端这两个词。实现HTTP服务器端的Web服务器存放Web对象,每个对象都可以通过URL寻址。流行的Web服务器包括Apache和Microsoft Internet Information Server。
HTTP定义了Web客户端如何从Web服务器请求Web页面,以及服务器如何向客户端传输Web页面。稍后我们将详细讨论客户端和服务器之间的交互,但总体思想在图2.6中说明。当用户请求一个Web页面(例如,单击一个超链接)时,浏览器向服务器发送页面中对象的HTTP请求消息。服务器接收请求并使用包含对象的HTTP响应消息进行响应。
HTTP使用TCP作为其底层传输协议(而不是运行在UDP之上)。HTTP客户端首先发起与服务器的TCP连接。一旦建立了连接,浏览器和服务器进程就会通过它们的套接字接口访问TCP。如2.1节所述,客户端socket接口是客户端进程与TCP连接之间的门;在服务器端,它是服务器进程和TCP连接之间的门。客户端向自己的套接字接口发送HTTP请求消息,并从自己的套接字接口接收HTTP响应消息。类似地,HTTP服务器从其套接字接口接收请求消息,并将响应消息发送到其套接字接口。一旦客户端将消息发送到它的套接字接口,该消息就从客户端手中转移到了TCP手中。回想一下,在2.1节中,TCP向HTTP提供了可靠的数据传输服务。这意味着客户端进程发送的每个HTTP请求消息最终会完整地到达服务器;类似地,服务器进程发送的每个HTTP响应消息最终也会完整地到达客户端。在这里,我们看到了分层架构的巨大优势之一。HTTP不需要担心丢失的数据,也不需要担心TCP如何从丢失中恢复或重新排序网络中的数据的细节。这是TCP和协议栈底层协议的工作。
重要的是要注意,服务器将请求的文件发送到客户端,而不存储关于客户端的任何状态信息。如果一个特定的客户端在几秒钟内两次请求同一个对象,服务器不会说它刚为客户端服务发送过这个对象;相反,服务器重新发送对象,因为它完全忘记了之前做的事情。因为HTTP服务器没有维护关于客户端的信息,HTTP被称为无状态协议。我们还注意到Web使用客户端-服务器应用程序架构,如第2.1节所述。Web服务器总是打开的,具有固定的IP地址,它服务于来自潜在的数百万个不同浏览器的请求。
HTTP的最初版本叫做HTTP/1.0,可以追溯到20世纪90年代早期[RFC 1945]。截至2020年,大多数HTTP处理都发生在HTTP/1.1上[RFC 7230]。然而,越来越多的浏览器和Web服务器也支持新版本的HTTP,即HTTP/2 [RFC 7540]。在本节的最后,我们将介绍HTTP/2。
2.2.2 非持久和持久连接
在许多Internet应用程序中,客户端和服务器之间的通信要持续一段时间,客户端发出一系列请求,服务器对每个请求进行响应。根据应用程序的使用方式,可以连续地、定期地或间歇地发出一系列请求。当这个客户端-服务器通过TCP进行交互时,应用程序开发人员需要做出一个重要的决定:每个请求/响应对是通过一个单独的TCP连接发送,还是所有的请求及其相应的响应都通过同一个TCP连接发送?在前一种方法中,应用程序被称为使用 非持久连接(non-persistent connections) ;而后者,则是 持久连接(persistent connections) 。为了深入理解这个设计问题,让我们研究一下持久连接在特定应用程序(即HTTP)环境中的优缺点,HTTP既可以使用非持久连接,也可以使用持久连接。尽管HTTP在默认模式下使用持久连接,但可以将HTTP客户端和服务器配置为使用非持久连接。
非持久连接的HTTP
让我们来看看在非持久连接的情况下,如何将Web页面从服务器传输到客户端。让我们假设页面由一个基础HTML文件和10个JPEG图像组成,并且这11个对象都在同一台服务器上。进一步假设基础HTML文件的URL为 http://www.someSchool.edu/someDepartment/home.index。下面是发生的情况:
- HTTP客户端进程发起一个到服务器www.someSchool.edu的TCP连接,端口号为80,这是HTTP的默认端口号。与TCP连接相关联的是,客户端和服务器端分别有一个套接字。
- HTTP客户端通过其套接字向服务器发送HTTP请求消息。请求消息包括路径名/someDepartment/home.index。(我们将在下面详细讨论HTTP消息)
- HTTP服务器进程通过套接字接收请求消息,从其内存(RAM或磁盘)中检索对象/someDepartment/home.index,将对象封装在HTTP响应消息中,并通过套接字将响应消息发送给客户端。
- HTTP服务器进程通知TCP关闭TCP连接。(但是TCP在确定客户端收到完整的响应消息之前不会真正终止连接)
- HTTP客户端收到响应消息。TCP连接终止。该消息表明被封装的对象是一个HTML文件。客户端从响应消息中提取文件,检查HTML文件,并查找对10个JPEG对象的引用。
- 然后对每个引用的JPEG对象重复前四个步骤。
当浏览器接收到Web页面时,它将页面显示给用户。两种不同的浏览器可能以不同的方式解释(即显示给用户)一个Web页面。HTTP与客户端如何解释Web页面无关。HTTP规范([RFC 1945]和[RFC 7540])只定义了客户端HTTP程序和服务器HTTP程序之间的通信协议。
上面的步骤说明了非持久连接的使用,其中每个TCP连接在服务器发送对象后关闭,而该连接不为其他对象持久。HTTP/1.0采用非持久TCP连接。注意,每个非持久TCP连接只传输一个请求消息和一个响应消息。因此,在本例中,当用户请求Web页面时,将生成11个TCP连接。
在上面描述的步骤中,对于客户端是通过10个连续的TCP连接获得10张jpeg,还是通过并行TCP连接获得一些jpeg,我们故意含糊其词。实际上,用户可以配置一些浏览器来控制并行程度。浏览器可能会打开多个TCP连接,并通过多个连接请求Web页面的不同部分。我们将在下一章中看到,并行连接的使用缩短了响应时间。
在继续之前,让我们做一个粗略的计算,以估算从客户端请求基础HTML文件到客户端接收到整个文件所需的时间。为此,我们定义了 往返时间(RTT) ,这是一个小数据包从客户端传输到服务器,然后再返回到客户端所花费的时间。RTT包括数据包传播延迟、中间路由器和交换机中的数据包排队延迟以及数据包处理延迟。(这些延迟已在第1.4节中讨论。)现在,考虑一下当用户单击超链接时会发生什么。如图2.7所示,这导致浏览器在浏览器和Web服务器之间发起一个TCP连接;这涉及到三次握手,客户端向服务器发送一个小的TCP段,服务器确认并响应一个小的TCP段,最后,客户端向服务器返回确认。三次握手的前两部分需要一个RTT。在完成握手的前两部分后,客户端将HTTP请求消息与三次握手的第三部分(确认)一起发送到TCP连接。请求消息到达服务器后,服务器将HTML文件发送到TCP连接中。这个HTTP请求/响应会消耗另一个RTT。因此,总的响应时间大致为两个RTT加上服务器上HTML文件的传输时间。
HTTP与持久连接
非持久连接有一些缺点。首先,必须为每个请求的对象建立和维护一个全新的连接。对于每一个连接,都必须分配TCP缓冲区,并且TCP变量必须同时保存在客户端和服务器中。这可能会给Web服务器带来很大的负担,因为它可能要同时处理来自数百个不同客户端的请求。第二,正如我们刚才描述的,每个对象都有两个RTT的交付延迟,一个RTT用于建立TCP连接,另一个RTT用于请求和接收对象。
使用HTTP/1.1持久连接,服务器在发送响应后保持TCP连接打开。同一客户端和服务器之间的后续请求和响应可以通过相同的连接发送。特别是,整个Web页面(在上面的示例中,基础HTML文件和10个图像)可以通过单个持久TCP连接发送。此外,在同一服务器上的多个Web页面可以通过单个持久TCP连接从服务器发送到同一客户端。这些对对象的请求可以背靠背地进行,而不需要等待挂起请求的答复。通常,当HTTP服务器在一段时间内(可配置的超时时间间隔)没有使用它时,它会关闭连接。当服务器接收到背靠背的请求时,它会背靠背地发送对象。HTTP的默认模式使用带有管道的(pipelining)持久连接。我们将在第二章和第三章的作业问题中定量地比较非持久连接和持久连接的性能。也可以查看[Heidemann 1997;Nielsen1997;RFC 7540]。
2.2.3 HTTP消息格式
HTTP规范[RFC 1945;RFC 7230;RFC 7540]包括了HTTP消息格式的定义。HTTP消息有两种类型,请求消息和响应消息,下面将讨论这两种类型。
HTTP请求消息
下面我们提供了一个典型的HTTP请求消息:
GET /somedir/page.html HTTP/1.1
Host: www.someschool.edu
Connection: close
User-agent: Mozilla/5.0
Accept-language: fr
通过仔细研究这个简单的请求消息,我们可以学到很多东西。首先,我们看到该消息是用普通ASCII文本编写的,因此能被熟练使用计算机的普通人类阅读它。其次,我们看到消息由5行组成,每一行后面跟着一个回车符和一个换行符。最后一行后面是一个额外的回车和换行。尽管这个特定的请求消息有5行,但请求消息可以有更多的行,也可以只有一行。HTTP请求消息的第一行称为 请求行(request line) ;后面的行称为 头行(header line) 。请求行有三个字段:方法字段、URL字段和HTTP版本字段。方法字段可以采用几种不同的值,包括GET、POST、HEAD、PUT和DELETE。绝大多数HTTP请求消息使用GET方法。GET方法在浏览器请求对象时使用,所请求的对象在URL字段中标识。在这个例子中,浏览器请求对象/somedir/page.html。版本字段不言自明;在这个例子中,浏览器实现了HTTP/1.1版本。
现在让我们看看例子中的头行。头行Host: www.someschool.edu指定对象所在的主机。您可能认为这个头行是不必要的,因为已经有一个到主机的TCP连接了。但是,正如我们将在2.2.5节中看到的,Web代理缓存需要主机头行提供的信息。通过包含Connection: close头行,浏览器告诉服务器它不想麻烦持久连接;它希望服务器在发送请求对象后关闭连接。User-agent:头行指定用户代理,即向服务器发出请求的浏览器类型。这里的用户代理是Mozilla/5.0,一个Firefox浏览器。这个头行非常有用,因为服务器实际上可以将相同对象的不同版本发送给不同类型的用户代理。(每个版本都使用相同的URL。)最后,Accept-language头行表明用户更愿意接收法语版本的对象,如果服务器上存在这样的对象;否则,服务器应该发送它的默认版本。Accept-language头行只是HTTP中可用的众多内容协商头行之一。
看了一个例子之后,现在让我们看看请求消息的一般格式,如图2.8所示。我们看到通用格式与前面的示例非常相似。但是,您可能已经注意到,在头行(以及额外的回车和换行)之后有一个 实体主体(entity body) 。实体主体在GET方法中为空,但在POST方法中使用。HTTP客户端通常在用户填写表单时使用POST方法,例如,当用户向搜索引擎提供搜索词时。使用POST消息,用户仍然从服务器请求Web页面,但是Web页面的具体内容取决于用户在表单字段中输入的内容。如果方法字段的值是POST,那么实体主体包含用户在表单字段中输入的内容。
表单生成的请求不一定要使用POST方法。HTML表单通常使用GET方法,并在请求的URL中包含输入的数据(在表单字段中)。例如,如果一个表单使用GET方法,有两个字段,两个字段的输入是monkeys和bananas,那么URL的结构将是www.somesite.com/animalsearch?monkeys&bananas。在您的日常Web浏览中,您可能已经注意到这类扩展URL。
HEAD方法类似于GET方法。当服务器接收到带有HEAD方法的请求时,它会响应一条HTTP消息,但会忽略被请求的对象。应用程序开发人员经常使用HEAD方法进行调试。PUT方法经常与Web发布工具一起使用。它允许用户将对象上传到特定Web服务器上的特定路径(目录)。需要将对象上传到Web服务器的应用程序也使用PUT方法。DELETE方法允许用户或应用程序删除Web服务器上的对象。
HTTP响应消息
下面我们提供了一个典型的HTTP响应消息。此响应消息可以是对刚才讨论的示例请求消息的响应。
HTTP/1.1 200 OK
Connection: 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 data data data data ...)
让我们仔细看看这个响应消息。它有三个部分:初始 状态行(status line) 、六个 头行(header line) 和 实体主体(entity body) 。实体主体是消息的可食用部分,它包含请求对象本身(data data data data data ...)。状态行有三个字段:协议版本字段、状态码和对应的状态消息。在这个例子中,状态行表明服务器正在使用HTTP/1.1并且一切正常(也就是说,服务器已经找到并正在发送被请求的对象)。
现在让我们看看头行。服务器使用Connection: close头行来告诉客户端它将在发送消息后关闭TCP连接。Date头行表示服务器创建并发送HTTP响应的时间和日期。注意,这不是对象创建或最后修改的时间;这是服务器从其文件系统中检索对象、将对象插入到响应消息中并发送响应消息的时间。Server头行表示该消息是由Apache Web服务器生成的;它类似于HTTP请求消息中的User-agent头行。Last-Modified头行表示创建或最后修改对象的时间和日期。Last-Modified头行,我们很快会详细介绍,它对于对象缓存至关重要,无论是在本地客户端还是在网络缓存服务器(也称为代理服务器)。Content-Length头行表示发送对象的字节数。Content-Type头行表示实体主体中的对象是HTML文本。(对象类型是由Content-Type头行正式指示的,而不是由文件扩展名。)
看了一个例子之后,现在让我们来看看响应消息的一般格式,如图2.9所示。此响应消息的通用格式与前面的响应消息示例相匹配。让我们再说一些关于状态码和它们的短语的单词。状态码和相关联的短语表示请求的结果。一些常见的状态码和相关短语包括:
- 200 OK:请求成功,响应返回信息。
- 301 Moved permanent:请求对象已被永久移动;新的URL在响应消息的Location头行中指定。客户端软件将自动检索新的URL。
- 400 Bad Request:这是一个通用的错误代码,表明请求不能被服务器理解。
- 404 Not Found:请求的文档在此服务器上不存在。
- 505 HTTP Version Not Supported:服务器不支持请求的HTTP协议版本。
2.2.4 用户-服务器交互:cookie
上面我们提到过,HTTP服务器是无状态的。这简化了服务器设计,并允许工程师开发能够处理数千个并发TCP连接的高性能Web服务器。但是,Web站点通常希望识别用户,因为服务器希望限制用户访问或者因为它想根据用户身份服务内容。出于这些目的,HTTP使用cookie。cookie在[RFC 6265]中定义,允许站点跟踪用户。今天,大多数主要的商业网站都使用cookie。
如图2.10所示,cookie技术有四个组成部分:(1)HTTP响应消息中的cookie头行;(2) HTTP请求消息中的cookie头行;(3)保存在用户端系统上并由用户浏览器管理的cookie文件;(4)网站的后端数据库。使用图2.10,让我们通过一个例子来了解cookie是如何工作的。假设Susan,她总是在家里用IE浏览器上网,第一次接触亚马逊网站。让我们假设过去她已经访问过eBay网站。当请求进入Amazon Web服务器时,服务器创建一个惟一的标识号,并在其后端数据库中创建一个按标识号索引的条目。然后Amazon Web服务器响应Susan的浏览器,在HTTP响应中包含一个Set-cookie头行,其中包含标识号。例如,头行可能是:
Set-cookie: 1678
当Susan的浏览器接收到HTTP响应消息时,它会看到Set-cookie头行信息。然后浏览器会在它所管理的特殊cookie文件后追加一行。这一行包括服务器的主机名和Set-cookie头行中的标识号。请注意,cookie文件中已经有一个eBay条目,因为Susan过去访问过该站点。当Susan继续浏览Amazon站点时,每次她请求一个Web页面时,她的浏览器都会询问她的cookie文件,提取她的该站点的标识号,并在HTTP请求中放置一个包含标识号的cookie头行。具体来说,她对Amazon服务器的每个HTTP请求都包含头行:
Cookie: 1678
通过这种方式,Amazon服务器能够跟踪Susan在Amazon站点上的活动。虽然Amazon Web站点不一定知道Susan的名字,但它确切地知道用户1678访问了哪些页面、访问顺序和访问时间!亚马逊使用cookie来提供购物车服务。亚马逊可以维护一个Susan所有计划购买的清单,这样她就可以在会话结束时一起支付。
如果Susan一周后返回到Amazon的站点,她的浏览器将继续在请求消息中显示头行Cookie: 1678。亚马逊还根据Susan过去访问过的网页向她推荐产品。如果Susan注册亚马逊:提供姓名、电子邮件地址、邮寄地址,和信用卡信息,亚马逊可以包含这些信息的数据库,从而将Susan姓名和身份证号码关联(她过去访问过该站点所有的页面!)。这就是亚马逊等电子商务网站提供“一键式购物”服务的方式。如果Susan在以后的访问中选择购买某件商品,就不需要重新输入姓名、信用卡号码或地址。
从本文的讨论中,我们可以看到可以使用cookie来标识用户。用户第一次访问网站时,可以提供用户身份(可能是他或她的名字)。在随后的会话中,浏览器将cookie头行传递给服务器,从而向服务器标识用户。因此,可以使用cookie在无状态HTTP之上创建用户会话层。例如,用户登录一个基于web的电子邮件应用程序(如Hotmail),浏览器向服务器发送cookie信息,允许服务器在用户与应用程序的整个会话中识别用户。
虽然cookie通常简化了用户的网上购物体验,但它们也有争议,因为它们也可能被视为侵犯隐私。正如我们刚才看到的,使用cookie和用户提供的帐户信息的组合,Web站点可以了解用户的很多信息,并可能将这些信息出售给第三方。
2.2.5 Web高速缓存
Web缓存 也称为 代理服务器 ,它是代表原始Web服务器满足HTTP请求的网络实体。Web缓存有自己的磁盘存储,并在该存储中保存最近请求的对象的副本。如图2.11所示,用户的浏览器可以配置为所有用户的HTTP请求首先被定向到Web缓存[RFC 7234]。一旦配置了浏览器,每个浏览器对对象的请求首先被定向到Web缓存。例如,假设浏览器请求对象http://www.someschool.edu/campus.gif。情况是这样的:
- 浏览器建立到Web缓存的TCP连接,并为对象向Web缓存发送HTTP请求。
- Web缓存检查是否在本地存储了对象的副本。如果是,Web缓存将HTTP响应消息中的对象返回给客户端浏览器。
- 如果Web缓存没有对象,则Web缓存打开到原始服务器(即www.someschool.edu)的TCP连接。然后Web缓存为该对象发送一个HTTP请求到缓存到服务器(cache-to-server)的TCP连接。在接收到这个请求后,源服务器将HTTP响应中的对象发送到Web缓存。
- 当Web缓存接收到对象时,它在本地存储中存储一份副本,并在HTTP响应消息中向客户端浏览器发送一份副本(通过客户端浏览器和Web缓存之间现有的TCP连接)。
请注意,缓存同时是服务器和客户端。当它接收到来自浏览器的请求并向浏览器发送响应时,它就是一个服务器。当它向源服务器发送请求并从源服务器接收响应时,它就是客户端。
通常Web缓存是通过ISP购买和安装的。例如,一所大学可能在其校园网上安装一个缓存,并将所有校园浏览器配置为指向该缓存。或者一个主要的本地ISP(如Comcast)可能会在其网络中安装一个或多个缓存,并预先配置其附带的浏览器以指向安装的缓存。
Web缓存出现在Internet上有两个原因。首先,Web缓存可以大大减少客户端请求的响应时间,特别是当客户端和原始服务器之间的带宽瓶颈远远小于客户端和缓存之间的带宽瓶颈时。如果客户端和缓存之间有高速连接(通常是这样),并且缓存中有所请求的对象,那么缓存将能够快速地将对象交付给客户端。第二,正如我们很快会用一个例子说明的那样,Web缓存可以大大减少机构接入链路到Internet的流量。通过减少流量,机构(例如,公司或大学)不必快速升级带宽,从而降低成本。此外,Web缓存可以整体上减少Internet中的Web流量,从而提高所有应用程序的性能。
为了更深入地理解缓存的好处,让我们探讨图2.12环境中的一个例子。这张图显示了两个网络,机构网络和公共互联网。机构网络是一个高速局域网。机构网络中的路由器和Internet中的路由器通过15Mbps的链路连接。原始服务器连接到Internet,但位于全球各地。假设平均对象大小为1Mbits,从机构的浏览器到原始服务器的平均请求速率为每秒15个请求。假设HTTP请求消息可以忽略不计,因此在网络或接入链路(从机构路由器到Internet路由器)中不会产生流量。再假设从图2.12中接入链路的Internet端路由器转发HTTP请求(在一个IP数据报内)到接收到响应(通常在多个IP数据报内)所花费的时间平均为2秒。非正式地,我们把这个延迟称为互联网延迟。
从浏览器请求对象到接收对象的总响应时间是局域网延迟(两个路由器之间的延迟)和因特网延迟的总和。现在就做一个非常粗略的计算来估算这个延迟。局域网的流量强度(见章节1.4.2):
(15 requests/sec) * (1 Mbits/request)/(100 Mbps) = 0.15
而接入链路(从Internet路由器到机构路由器)上的流量强度为:
(15 requests/sec) * (1 Mbits/request)/(15 Mbps) = 1
局域网的流量强度为0.15通常会导致至多数十毫秒的延迟;因此,我们可以忽略局域网延迟。然而,正如1.4.2节所讨论的,当流量强度接近1时(如图2.12中接入链路的情况),链路上的延迟会变得非常大,并且无限制地增长。因此,满足请求的平均响应时间至少是分钟级的,这对机构的用户是不可接受的。显然必须采取一些措施。
一个可能的解决方案是将接入速率从15Mbps提高到100Mbps。这将使接入链路上的流量强度降低到0.15,这意味着两台路由器之间的延迟可以忽略不计。在本例中,总的响应时间大约为2秒,即Internet延迟。但这个解决方案也意味着该机构必须将其接入链路从15Mbps升级到100Mbps,这是一个昂贵的提议。
现在考虑另一种解决方案,即不升级接入链路,而是在机构网络中安装一个Web缓存。这个解决方案如图2.13所示。命中率(由缓存满足的请求的比例)通常在0.2到0.7之间。为了便于说明,我们假设缓存为该机构提供了0.4的命中率。因为客户端和缓存都连接到同一个高速局域网,40%的请求几乎会立即被缓存满足,比如说,在10毫秒内。然而,剩下的60%的请求仍然需要原始服务器来满足。但是,由于只有60%的请求的对象通过接入链路,接入链路上的流量强度从1.0降低到0.6。通常,小于0.8的流量强度对应的是一个很小的延迟,比如说,在一个15Mbps的链路上有几十毫秒的延迟。与两秒的Internet延迟相比,这个延迟可以忽略不计。考虑到这些因素,因此平均延迟是:
0.4 * (0.01 seconds) + 0.6 * (2.01 seconds)
也就是略大于1.2秒。因此,第二个解决方案比第一个解决方案提供更低的响应时间,而且它不需要机构升级其与互联网的链路。当然,机构必须购买并安装一个网络缓存。但这一成本很低,许多缓存使用的是运行在廉价个人电脑上的公共域名软件(public-domain software)。
通过使用 内容分发网络(CDN Content Distribution Networks) , Web缓存在互联网中扮演着越来越重要的角色。CDN公司在整个互联网上安装许多地理分布的缓存,从而使大部分流量本地化。有共享的CDN(如Akamai和Limelight)和专用的CDN(如谷歌和Netflix)。我们将在2.6节中更详细地讨论CDN。
条件 GET
虽然缓存可以减少用户感知的响应时间,但它引入了一个新问题:驻留在缓存中的对象副本可能已经过时。换句话说,副本缓存到客户端上之后,存放于Web服务器中的对象可能已经被修改。幸运的是,HTTP有一种机制,允许缓存验证其对象是不是最新的。这种机制被称为 条件GET(Conditional GET) [RFC 7232]。如果一条HTTP请求消息满足:(1)请求消息使用GET方法,(2)请求消息包含一个if-modified-since头行,则称之为条件GET消息。
为了说明条件GET是如何操作的,让我们看一个例子。第一,代理缓存代表请求浏览器向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 data data data 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)
我们看到在条件GET的响应中,Web服务器仍然发送响应消息,但在响应消息中不包含所请求的对象。包括请求的对象只会浪费带宽并增加用户感知到的响应时间,特别是在对象很大的情况下。请注意,最后一条响应消息的状态行中有304 Not Modified,它告诉缓存它可以继续并将它的(代理缓存)对象的缓存副本转发给请求浏览器。
2.2.6 HTTP/2
HTTP/2 [RFC 7540], 2015年标准化,是自1997年标准化HTTP/1.1以来的第一个新版本。自标准化以来,HTTP/2得到了长足发展,在2020年排名前1000万的网站中有超过40%支持HTTP/2 [W3Techs]。大多数浏览器包括谷歌Chrome、IE、Safari、Opera和Firefox也支持HTTP/2。
HTTP/2的主要目标是通过在单个TCP连接上启用请求和响应多路复用,提供请求优先级和服务器推送,并提供高效的HTTP头字段压缩,从而减少感知到的延迟。HTTP/2不会改变HTTP方法、状态码、URL或头字段。而是改变了数据的格式以及在客户端和服务器之间的传输方式。
为了激发对HTTP/2的需求,回想一下HTTP/1.1使用持久TCP连接,允许通过单个TCP连接将Web页面从服务器发送到客户端。通过每个Web页面只有一个TCP连接,服务器上的套接字数量减少了,每个被传输的Web页面都可以公平地分享网络带宽(如下所述)。但是Web浏览器的开发人员很快发现,通过单个TCP连接发送Web页面中的所有对象存在 Head of Line (HOL)阻塞 问题。要理解HOL阻塞,请考虑一个包含HTML基础页面、靠近Web页面顶部的一个大视频和视频下方的许多小对象的Web页面。进一步假设在服务器和客户端之间的路径上有一个低速到中速的瓶颈链路(例如,一个低速无线链路)。使用单一TCP连接时,视频片段通过瓶颈链路的时间会较长,而小对象则会在视频片段后面等待,从而产生延时;也就是说,排在前面的视频剪辑挡住了排在后面的小对象。HTTP/1.1浏览器通常通过打开多个并行的TCP连接来解决这个问题,从而将同一网页中的对象并行发送给浏览器。通过这种方式,小对象可以更快地到达浏览器并在浏览器中渲染,从而减少用户感知到的延迟。
第三章详细讨论的TCP拥塞控制,也为浏览器提供了一个意想不到的动机,即使用多个并行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阻塞。
HTTP/2 组帧(framing)
用于HOL阻塞的HTTP/2解决方案是将每个消息分解成小的帧,并在同一TCP连接上交错发送请求和响应消息。为了理解这一点,再来考虑一下这个Web页面的例子,这个网页包含一个大视频和8个小对象。因此,服务器将从任何希望查看此Web页面的浏览器接收9个并发请求。对于这些请求中的每一个,服务器需要向浏览器发送9个竞争性的HTTP响应消息。假设所有的帧都是固定长度,视频由1000帧组成,每个较小的对象由2帧组成。通过帧交错,在发送视频中的一帧后,每个小对象的第一个帧被发送。然后在发送视频的第二帧后,每个小对象的最后一帧被发送。因此,所有较小的对象都是在发送了总共18帧之后完成。如果不使用交错,较小的对象将在发送1016帧之后才完成。因此,HTTP/2帧机制可以显著减少用户感知的延迟。
将HTTP消息分解成独立的帧,交错它们,然后在另一端重新组合它们的能力是HTTP/2最重要的一个增强。组帧是由HTTP/2协议的组帧子层(framing sub-layer)完成的。当服务器想要发送HTTP响应时,响应由组帧子层处理,在那里它被分解成帧。响应的头字段成为一个帧,消息的主体被分解成更多的帧。响应的帧然后由服务器中的组帧子层与其他响应的帧交错,并通过单个持久TCP连接发送。当帧到达客户端时,它们首先在组帧子层被重新组装成原始的响应消息,然后由浏览器照常处理。类似地,客户端的HTTP请求被分解成帧并交错处理。
除了将每个HTTP消息分解成独立的帧外,组帧子层还对帧进行二进制编码。二进制协议更有效地解析,帧更小,并且更不容易出错。
响应消息优先级和服务器推送
消息优先级允许开发人员定制请求的相对优先级,以更好地优化应用程序性能。正如我们刚刚了解到的,组帧子层将消息组织成发送给同一请求者的并行数据流。当客户端向服务器发送并发请求时,它可以通过为每个消息分配1到256之间的权重来对请求的响应进行优先级排序。数字越大,优先级越高。使用这些权重,服务器可以首先发送具有最高优先级的响应帧。除此之外,客户端还通过指定它所依赖的消息的ID来声明每个消息对其他消息的依赖。
HTTP/2的另一个功能是服务器为单个客户端请求发送多个响应的能力。也就是说,除了对原始请求的响应之外,服务器还可以将其他对象推送给客户端,而不必让客户端逐个请求。这是可能的,因为HTML基础页面指示了完全渲染Web页面所需的对象。因此,服务器无需等待针对这些对象的HTTP请求,而是可以分析HTML页面,识别所需的对象,并在接收到针对这些对象的显式请求之前将它们发送给客户端。服务器推送消除了由于等待请求而产生的额外延迟。
HTTP/3
在第三章中讨论的QUIC是一种新的传输协议,它是在基本的UDP协议之上在应用层实现的。QUIC具有HTTP所需要的几个特性,例如消息多路复用(交错)、逐流(per-stream)流控和低延迟连接建立。HTTP/3是一种新的HTTP协议,设计用于在QUIC上运行。截至2020年,HTTP/3在互联网草案中被描述,尚未完全标准化。许多HTTP/2特性(如消息交错)都被归入QUIC,允许为HTTP/3进行更简单、更流畅的设计。
2.3 互联网上的电子邮件
电子邮件从互联网出现的时候就已经存在了。当互联网还处于婴儿期时,它是最流行的应用程序(Segaller 1998),随着时间的推移,它变得更加复杂和强大。它仍然是互联网最重要和最有用的应用之一。
与普通邮件一样,电子邮件是不需要与他人的日程协调,只要方便就可以发送和阅读的非同步通信媒介。与邮政邮件相比,电子邮件速度快,易于分发,而且价格便宜。现代电子邮件有许多强大的功能,包括带有附件的消息、超链接、html格式化的文本和嵌入的照片。
在本节中,我们将研究Internet电子邮件的核心——应用层协议。但是,在深入讨论这些协议之前,让我们从高层视角(high-level view)来看Internet邮件系统及其关键组件。
图2.14给出了Internet邮件系统的高层视图。从这个图中我们可以看到它有三个主要组件: 用户代理、邮件服务器和简单邮件传输协议(SMTP Simple Mail Transfer Protocol) 。现在,我们在发送者Alice向接收者Bob发送电子邮件消息的环境中描述这些组件。用户代理允许用户读取、回复、转发、保存和撰写消息。电子邮件用户代理的例子包括Microsoft Outlook、Apple Mail、基于web的Gmail、运行在智能手机上的Gmail应用程序等等。当Alice完成她的消息的编写后,她的用户代理将消息发送到她的邮件服务器,在邮件服务器的传出消息队列中放置该消息。当Bob想要读取消息时,他的用户代理从邮件服务器中的邮箱中检索消息。
邮件服务器构成了电子邮件基础结构的核心。每个收件人(如Bob)都有一个位于邮件服务器中的 邮箱 。Bob的邮箱管理和维护发送给他的消息。典型的消息在发件人的用户代理中开始传递,然后传递到发件人的邮件服务器,然后传送到收件人的邮件服务器,并将邮件存储在收件人的邮箱中。当Bob希望访问其邮箱中的消息时,包含其邮箱的邮件服务器将对Bob进行身份验证(使用他的用户名和密码)。Alice的邮件服务器还必须处理Bob的邮件服务器的故障。如果Alice的服务器不能将邮件发送到Bob的服务器,Alice的服务器将消息保存在 消息队列 中,然后尝试稍后传输消息。通常每隔30分钟左右就会重新尝试一次;如果几天后仍然没有成功,服务器将删除该消息并用电子邮件通知发送者(Alice)。
使用TCP可靠的数据传输服务,将邮件从发送方的邮件服务器传输到接收方的邮件服务器。与大多数应用层协议一样,SMTP有两个方面:在发送方的邮件服务器上执行的客户端和在接收方的邮件服务器上执行的服务器端。SMTP的客户端和服务器端都运行在每个邮件服务器上。当邮件服务器向其他邮件服务器发送邮件时,它充当SMTP客户端。当邮件服务器从其他邮件服务器接收邮件时,它充当SMTP服务器的角色。
2.3.1 SMTP
RFC 5321中定义的SMTP是Internet电子邮件的核心。如上所述,SMTP将消息从发件人邮件服务器传输到收件人邮件服务器。SMTP比HTTP早得多。(最初的SMTP RFC可以追溯到1982年,而SMTP在那之前很久就已经存在了。)虽然SMTP有许多奇妙的特性,它在互联网上的无处不在证明了这一点,但它仍然是一种具有某些古老特性的遗留技术。例如,它将所有邮件消息的主体(而不仅仅是头部)限制为简单的7位ASCII。这一限制在20世纪80年代早期很有意义,当时传输容量不足,没有人通过电子邮件发送大型附件或大型图像、音频或视频文件。但是今天,在多媒体时代,7位ASCII码的限制有点麻烦,它要求二进制多媒体数据在通过SMTP发送之前要先被编码成ASCII码;它要求相应的ASCII消息在SMTP传输后被解码成二进制。在第2.2节中,HTTP不要求多媒体数据在传输之前进行ASCII编码。
为了说明SMTP的基本操作,让我们通过一个常见的场景。假设Alice想给Bob发送一个简单的ASCII信息。
- Alice为电子邮件调用她的用户代理,提供Bob的电子邮件地址(例如,[email protected]),编写消息,并指示用户代理发送消息。
- Alice的用户代理将消息发送到她的邮件服务器,邮件服务器将消息放置在消息队列中。
- 在Alice的邮件服务器上运行的SMTP的客户端在消息队列中看到消息。它打开与运行在Bob邮件服务器上的SMTP服务器的TCP连接。
- 在一些初始的SMTP握手之后,SMTP客户端将Alice的消息发送到TCP连接。
- 在Bob的邮件服务器上,SMTP服务器端接收消息。然后,Bob的邮件服务器将消息放入Bob的邮箱中。
- Bob调用他的用户代理在他方便的时候读取消息。
这个场景在图2.15中进行了总结。
需要注意的是,SMTP通常不使用中间邮件服务器发送邮件,即使两个邮件服务器位于世界的两端。如果Alice的服务器在香港,Bob的服务器在圣路易斯,则TCP连接是香港和圣路易斯服务器之间的直接连接。尤其是,如果Bob的邮件服务器宕机了,消息将留在Alice的邮件服务器中并等待新的尝试——消息不会被放置在某个中间邮件服务器中。
现在让我们仔细看看SMTP是如何将消息从发送邮件服务器传输到接收邮件服务器的。我们将看到SMTP协议与用于面对面的人类交互的协议有许多相似之处。首先,客户端SMTP(运行在发送邮件服务器主机上)通过TCP与服务器SMTP(运行在接收邮件服务器主机上)的端口25建立连接。如果服务器宕机,客户端稍后会再次尝试。一旦建立了这个连接,服务器和客户端就会执行一些应用层握手,就像人们在彼此之间传递信息之前经常介绍自己一样,SMTP客户端和服务器在传递信息之前也会介绍自己。在这个SMTP握手阶段,SMTP客户端指出发件人(生成消息的人)的电子邮件地址和收件人的电子邮件地址。一旦SMTP客户端和服务器彼此进行了自我介绍,客户端就发送这条消息。SMTP可以依靠TCP可靠的数据传输服务将消息发送到服务器,而不会出现错误。如果客户端有其他消息要发送给服务器,则在相同的TCP连接上重复这个过程;否则,它指示TCP关闭连接。
接下来让我们看一个示例,SMTP客户端(C)和SMTP服务器(S)之间的消息交换,客户端的主机名是crepes.fr,服务器的主机名是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: <[email protected]>
S:!!250 [email protected] ... Sender ok
C:!!RCPT TO: <[email protected]>
S:!!250 [email protected] ... Recipient ok
C:!!DATA
S:!!354 Enter mail, end with ”.” on a line by itself
C:!!Do you like ketchup?
C:!!How about pickles?
C:!!.
S:!!250 Message accepted for delivery
C:!!QUIT
S:!!221 hamburger.edu closing connection
在上面的例子中,客户端发送了一条消息(你喜欢番茄酱吗?泡菜怎么样?)从邮件服务器crepes.fr到邮件服务器hamburger.edu。作为对话的一部分,客户端发出了5个命令:HELO (HELLO的缩写)、MAIL FROM、RCPT TO、DATA和QUIT。这些命令是不言自明的。客户端还向服务器发送由单个句点组成的行,表示消息的结束。(在ASCII行话中,每条消息以CRLF结尾。CRLF,其中CR和LF分别代表回车和换行。)服务器对每个命令发出应答(replay),每个应答都有应答代码和一些(可选的)英语解释。我们在这里提到SMTP使用持久连接:如果发送邮件服务器有多个消息要发送到同一个接收邮件服务器,那么它可以通过同一个TCP连接发送所有消息。对于每个消息,客户端用一个新的MAIL FROM: crepes.fr开始,用一个单独的时间段指定消息的结束,只有在所有消息都发送完毕后才发出QUIT。
强烈建议您使用Telnet与SMTP服务器进行直接对话。要做到这一点,请发
telnet serverName 25
其中serverName是本地邮件服务器的名称。当您这样做时,您只是在您的本地主机和邮件服务器之间建立了TCP连接。输入这一行之后,您应该立即收到来自服务器的220回复。然后发出SMTP命令HELO、MAIL FROM、RCPT TO、DATA、CRLF。并在适当的时候退出。我还强烈建议你在本章的最后做编程作业3。在该任务中,您将构建一个简单的用户代理,它实现了SMTP的客户端。它允许您通过本地邮件服务器向任意收件人发送电子邮件。
2.3.2 邮件消息格式
当Alice向Bob写一封普通的邮寄信件(snail-mail)时,她可能会在信的顶部包含各种外围(peripheral)头信息,比如Bob的地址、她自己的回信地址和日期。类似地,当电子邮件从一个人发送到另一个人时,包含外围信息的消息头在消息主体之前。这些外围数据包含在RFC 5322中定义的一系列头行中。消息的头行和主体由一个空白行(即由CRLF)分隔。RFC 5322指定了邮件头行的确切格式及其语义解释。与HTTP一样,每个头行包含可读的文本,由关键字、冒号和值组成。有些关键字是必需的,有些是可选的。每个头必须有一个From:头行和一个To:头行;头可以包括Subject:头行以及其他可选的头行。需要注意的是,这些头行不同于我们在2.3.1节中学习的SMTP命令(尽管它们包含一些常见的单词,如from和to)。该部分中的命令是SMTP握手协议的一部分;本节中检查的头行是邮件消息本身的一部分。
典型的消息头如下所示:
From: [email protected]
To: [email protected]
Subject: Searching for the meaning of life.
消息头之后,跟一个空行;然后是消息主体(用ASCII格式)。您应该使用Telnet将包含一些头行(包括Subject:头行)的消息发送到邮件服务器。为此,发出telnet serverName 25,如2.3.1节所述。
2.3.3邮件访问协议
一旦SMTP将消息从Alice的邮件服务器发送到Bob的邮件服务器,该消息就被放置在Bob的邮箱中。假设Bob(收件人)在其本地主机(例如,智能手机或PC)上执行其用户代理,那么自然会考虑在其本地主机上也放置一个邮件服务器。通过这种方法,Alice的邮件服务器可以直接与Bob的PC进行对话。然而,这种方法有一个问题。回想一下,邮件服务器管理邮箱并运行SMTP的客户端和服务器端。如果Bob的邮件服务器驻留在他的本地主机上,那么Bob的主机必须始终处于打开状态,并连接到Internet,以便接收随时可能到达的新邮件。这对许多互联网用户来说是不切实际的。相反,典型的用户在本地主机上运行用户代理,但访问存储在共享邮件服务器上的邮箱。该邮件服务器与其他用户共享。
现在让我们考虑一封电子邮件从Alice发送到Bob时所经过的路径。我们刚刚了解到,在这条路径的某个时刻,电子邮件需要存储在Bob的邮件服务器中。这可以简单地通过让Alice的用户代理将消息直接发送到Bob的邮件服务器来完成。但是,通常情况下,发件人的用户代理不直接与收件人的邮件服务器进行对话。相反,如图2.16所示,Alice的用户代理使用SMTP或HTTP将电子邮件消息发送到她的邮件服务器,然后Alice的邮件服务器使用SMTP(作为一个SMTP客户端)将电子邮件消息转发到Bob的邮件服务器。为什么要分两步?这主要是因为如果不通过Alice的邮件服务器进行中继,Alice的用户代理对不可达的目标邮件服务器没有任何追索权(recourse)。通过让Alice先将电子邮件存入自己的邮件服务器,Alice的邮件服务器可以每隔30分钟重复向Bob的邮件服务器发送邮件,直到Bob的邮件服务器开始工作。(如果Alice的邮件服务器宕机,那么她可以向她的系统管理员投诉!)
但这个谜题还缺一块!像Bob这样在其本地主机上运行用户代理的收件人如何获取位于邮件服务器中的消息?请注意,Bob的用户代理不能使用SMTP来获取消息,因为获取消息是一个拉操作,而SMTP是一个推送协议。
现在,Bob有两种从邮件服务器检索电子邮件的常用方法。如果Bob正在使用基于web的电子邮件或智能手机应用程序(如Gmail),那么用户代理将使用HTTP检索Bob的电子邮件。在这种情况下,Bob的邮件服务器需要有HTTP接口和SMTP接口(用于与Alice的邮件服务器通信)。另一种方法(通常用于Microsoft Outlook等邮件客户端)是使用RFC 3501中定义的 Internet邮件访问协议(IMAP Internet Mail Access Protocol) 。HTTP和IMAP方法都允许Bob管理Bob邮件服务器中维护的文件夹。Bob可以将消息移动到他创建的文件夹中,删除消息,将消息标记为重要,等等。
2.4 DNS——因特网的目录服务
我们人类可以在很多方面被识别。例如,我们可以通过出生证明上出现的名字来识别。我们可以通过社会安全号码被识别。我们可以通过我们的驾照号码来识别。尽管每个标识符都可以用来标识人,但在给定的环境中,一个标识符可能比另一个更合适。例如,美国国税局(美国臭名昭著的税务机构)的计算机更喜欢使用固定长度的社会安全号码,而不是出生证明上的名字。另一方面,普通人更喜欢更容易记忆的出生证明名字,而不是社会安全号码。(事实上,你能想象说,嗨。我的名字是132-67-9875。请见见我的丈夫,178-87-1146。)
就像人类可以通过许多方式被识别一样,互联网主机也可以。主机的标识符之一是它的 主机名 。像www.facebook.com、www.google.com、gaia.c.s.umass .edu这样的主机名便于记忆,因此被人们所欣赏。但是,主机名提供的关于该主机在Internet中的位置的信息很少(如果有的话)。(主机名,例如www.eurecom.fr,以国家代码。fr结尾,告诉我们主机可能在法国,但没有说更多。)此外,由于主机名可以由可变长度的字母数字字符组成,它们将很难被路由器处理。由于这些原因,主机也是通过所谓的 IP地址 来标识的。
我们将在第4章中详细讨论了IP地址,但是现在对它们做一些简短的介绍是很有用的。IP地址由四个字节组成,具有严格的层次结构。IP地址看起来像121.7.106.83,其中每个句点分隔从0到255的十进制表示的一个字节。IP地址是分层的,因为当我们从左到右扫描地址时,我们获得关于主机在Internet中的位置的越来越具体的信息(即,在哪个网络中,在网络中的网络中)。类似地,当我们从下到上扫描邮寄地址时,我们会获得关于收件人所在位置的越来越具体的信息。
2.4.1 DNS提供的服务
我们已经看到了通过主机名和IP地址标识主机的两种方法。人们更喜欢便于记忆的主机名标识符,而路由器更喜欢固定长度、层次结构的IP地址。为了协调这些偏好,我们需要一个将主机名转换为IP地址的目录服务。这是因特网 域名系统(DNS,domain name system) 的主要任务。DNS是一种基于DNS服务器层次结构实现的分布式数据库,是一种允许主机查询分布式数据库的应用层协议。DNS服务器通常是运行Berkeley Internet Name Domain (BIND)软件[BIND 2020]的UNIX机器。DNS协议采用UDP协议,端口为53。
其他应用层协议(包括HTTP和SMTP)通常使用DNS将用户提供的主机名转换为IP地址。例如,当运行在某些用户主机上的浏览器(即HTTP客户端)请求URL www.someschool.edu/index.html时,会发生什么情况。为了使用户的主机能够向Web服务器www.someschool.edu发送HTTP请求消息,用户的主机必须首先获得www.someschool.edu的IP地址。这是按如下方式完成的:
- 同一用户机器运行DNS应用程序的客户端。
- 浏览器从URL提取主机名www.someschool.edu,并将主机名传递给DNS应用程序客户端。
- DNS客户端向DNS服务器发送一个包含主机名的查询。
- DNS客户端最终收到一个回复,其中包括主机名的IP地址。
- 一旦浏览器从DNS接收到IP地址,它就可以发起一个TCP连接到位于该IP地址80端口的HTTP服务器进程。
我们从这个例子中看到,DNS增加了额外的延迟,有时对使用它的Internet应用程序来说是相当大的延迟。幸运的是,正如我们下面讨论的,所需的IP地址通常缓存在附近的DNS服务器上,这有助于减少DNS网络流量和平均DNS延迟。
除了将主机名转换为IP地址之外,DNS还提供了一些其他重要的服务:
- 主机别名 。主机名复杂的主机可以有一个或多个别名。例如,像relay1.west-coast.enterprise.com这样的主机名。可以有两个别名,比如enterprise.com和www.enterprise.com。在本例中,主机名relay1 .west-coast.enterprise.com被认为是一个 规范主机名(canonical hostname) 。别名主机名,当出现时,通常比规范主机名更容易记忆。应用程序可以调用DNS来获取所提供的别名主机名的规范主机名以及主机的IP地址。
- 邮件服务器别名 。由于显而易见的原因,电子邮件地址很容易记忆。例如,如果Bob拥有一个雅虎邮箱帐户,那么Bob的电子邮件地址就可以简单地设置为[email protected]。但是,与简单的yahoo.com相比,Yahoo邮件服务器的主机名要复杂得多,而且不易记忆(例如,规范主机名可能是relay1.westcoast .yahoo.com之类的东西)。邮件应用程序可以调用DNS来获取提供的别名主机名的规范主机名以及主机的IP地址。实际上,MX记录(见下文)允许公司的邮件服务器和Web服务器拥有相同的(别名)主机名;例如,一个公司的Web服务器和邮件服务器都可以叫做enterprise.com。
- 负载分配(Load distribution) DNS还用于在重复的(replicated)服务器(如重复的Web服务器)之间执行负载分配。繁忙的站点(如bilibili.com)被复制到多个服务器上,每个服务器运行在不同的端系统上,每个服务器都有不同的IP地址。对于重复的Web服务器,一组IP地址因此与一个别名主机名相关联。DNS数据库包含这组IP地址。当客户端对映射到一组地址的名称进行DNS查询时,服务器会响应整组IP地址,但在每个应答中会轮换地址的顺序。由于客户端通常将其HTTP请求消息发送到集合中列出的第一个IP地址,DNS轮换将在重复的服务器之间分配流量。DNS轮换也用于电子邮件,以便多个邮件服务器可以具有相同的别名。此外,像Akamai这样的内容分发公司已经以更复杂的方式使用DNS [Dilley 2002]来提供Web内容分发(参见2.6.3节)。
实践原则之——DNS:通过客户端-服务器范例实现关键的网络功能
如HTTP, FTP和SMTP, DNS协议是一个应用层协议,因为它(1)使用客户端-服务器范例的通信终端系统和(2)依赖于底层的端到端传输协议来在通信的端系统之间传输DNS消息。然而,从另一种意义上说,DNS的角色与Web、文件传输和电子邮件应用程序截然不同。DNS不是用户直接与之交互的应用程序。相反,DNS为Internet中的用户应用程序和其他软件提供了一个核心的Internet功能,即将主机名转换为其底层IP地址。我们在第1.2节中注意到,Internet体系结构中的大部分复杂性都位于网络的边缘。DNS使用位于网络边缘的客户端和服务器实现关键的名称到地址转换过程,它是这种设计理念的另一个例子。
DNS在RFC 1034和RFC 1035中指定,并在其他几个RFC中更新。这是一个复杂的系统,我们在这里只涉及其运作的关键方面。感兴趣的读者可以参考这些RFC和Albitz和Liu的书[Albitz 1993];另请参阅回顾论文[Mockapetris 1988],它很好地描述了DNS的内容和原因,以及[Mockapetris 2005]。
2.4.2 DNS工作原理简介
现在,我们将对DNS的工作方式进行概述。我们将重点讨论主机名到ip地址的转换服务。
假设在用户主机上运行的某个应用程序(如Web浏览器或邮件客户端)需要将主机名转换为IP地址。应用程序将调用DNS的客户端,指定需要转换的主机名。(在许多基于unix的机器上,gethostbyname()是应用程序为了执行转换而调用的函数。)然后用户主机中的DNS接管,向网络发送查询消息。所有DNS查询和应答消息在UDP数据报中发送到端口53。延迟后,从毫秒开始到秒,用户主机中的DNS收到DNS应答消息,提供所需的映射。然后将该映射传递给调用方应用程序。因此,从用户主机中调用应用程序的角度来看,DNS是黑盒提供简单、直接的翻译服务。但事实上,黑盒实现该服务的DNS服务器比较复杂,由大量的DNS服务器组成分布在全球各地,以及指定DNS服务器和查询主机如何通信的应用层协议。
一个简单的DNS设计应该有一个包含所有映射的DNS服务器。在这种集中式设计中,客户端直接将所有查询发送到单个DNS服务器,而DNS服务器直接响应查询的客户端。虽然这种设计的简单性很有吸引力,但它不适用于当今主机数量庞大(且不断增长)的互联网。集中式设计的问题包括:
- 单点故障 如果DNS服务器崩溃,整个互联网也会崩溃。
- 流量容量 一个单一的DNS服务器必须处理所有的DNS查询(对于从数亿台主机生成的所有HTTP请求和电子邮件消息)。
- 远距离的集中式数据库 单个DNS服务器不能靠近所有查询的客户端。如果我们把一个DNS服务器放在纽约市,那么来自澳大利亚的所有查询必须传送到地球的另一端,可能是通过缓慢而拥挤的链路。这可能会导致严重的延迟。
- 维护 单一的DNS服务器必须保存所有互联网主机的记录。这个集中的数据库不仅非常庞大,而且还必须经常更新,以适应每台新主机。
总之,一个集中的数据库在单个DNS服务器上是无法扩展的。因此,DNS按设计进行分配。事实上,DNS是如何在Internet中实现分布式数据库的一个很好的例子。
一个分布式、分层的数据库
为了解决规模问题,DNS使用了大量的服务器,这些服务器以分层的方式组织起来,分布在世界各地。没有一个DNS服务器具有Internet中所有主机的所有映射。相反,映射分布在整个DNS服务器上。粗略地说,有三类DNS服务器:根DNS服务器、顶级域名(TLD top-level domain) DNS服务器和权威DNS服务器,它们按照如图2.17所示的层次结构组织。为了了解这三类服务器是如何交互的,假设DNS客户端希望确定主机名www.amazon.com的IP地址。大致说来,将发生下列事件。客户端首先联系一个根服务器,返回顶级域名com的TLD服务器的IP地址。客户端然后联系这些TLD服务器之一,它返回amazon.com的权威服务器的IP地址。最后,客户端联系amazon.com的一个权威服务器,它返回主机名www.amazon.com的IP地址。我们将很快更详细地研究这个DNS查找过程。但让我们先仔细看看这三类DNS服务器:
- 根DNS服务器 。有超过1000个根服务器实例分布在世界各地,如图2.18所示。这些根服务器是13台不同的根服务器的副本,由12个不同的组织管理,并通过Internet Assigned Numbers Authority进行协调[IANA 2020]。根名服务器(root name servers)的完整列表,以及管理它们的组织和它们的IP地址可以在[Root Servers 2020]中找到。根名服务器提供TLD服务器的IP地址。
- 顶级域服务器 。对于每个顶级域名,顶级域名如com, org, net, edu,和gov,以及所有国家顶级域名如uk, fr, ca, jp,都有TLD服务器(或服务器集群)。Verisign Global Registry Services公司为com顶级域名维护TLD服务器,Educause公司为edu顶级域名维护TLD服务器。支持TLD的网络基础设施可能庞大而复杂;请参阅[Osterweil 2012]了解Verisign网络的详细概况。查看[TLD list 2020]的所有顶级域名列表。TLD服务器为权威DNS服务器提供IP地址。
- 权威DNS服务器 。在Internet上拥有可公开访问主机(如Web服务器和邮件服务器)的每个组织都必须提供可公开访问的DNS记录,这些记录将这些主机的名称映射到IP地址。组织的权威DNS服务器存放这些DNS记录。一个组织可以选择实现自己的权威DNS服务器来保存这些记录;或者,组织可以付费将这些记录存储在某个服务提供商的权威DNS服务器上。大多数大学和大型公司都实施和维护自己的主、备(备份)权威DNS服务器。
根DNS服务器、TLD DNS服务器和权威DNS服务器都属于DNS服务器的层次结构,如图2.17所示。还有一种重要的DNS服务器,称为 本地DNS服务器 。本地DNS服务器并不严格地属于服务器的层次结构,但仍然是DNS体系结构的核心。每个ISP(如住宅ISP或机构ISP)都有一个本地DNS服务器(也称为默认名称服务器)。当主机与ISP连接时,ISP会向主机提供一个或多个本地DNS服务器的IP地址(通常通过DHCP,第四章中讨论)。通过访问windows或UNIX中的网络状态窗口,您可以很容易地确定本地DNS服务器的IP地址。主机的本地DNS服务器通常离主机很近。对于机构ISP,本地DNS服务器可能与主机在同一局域网内;对于本地ISP来说,它与主机之间的间隔通常不超过几个路由器。当主机进行DNS查询时,查询被发送到充当代理的本地DNS服务器,将查询转发到DNS服务器层次结构,我们将在下面更详细地讨论。
让我们看一个简单的例子。假设主机cse.nyu.edu需要gaia.c.s.umass.edu的IP地址。再假设NYU的cse.nyu.edu的本地DNS服务器为DNS.nyu.edu, gaia.c.s.umass.edu的权威DNS服务器为DNS.umass.edu,如图2.19所示。主机cse.nyu.edu首先向其本地DNS服务器DNS.nyu.edu发送DNS查询消息。查询消息包含要翻译的主机名,即gaia.c.s.umass.edu。本地DNS服务器将查询消息转发给根DNS服务器。根DNS服务器注意到edu后缀,并返回给本地DNS服务器负责edu的TLD服务器的IP地址列表。然后,本地DNS服务器将查询消息重新发送给其中一个TLD服务器。TLD服务器记录下umass.edu的后缀,并响应马萨诸塞州大学的权威DNS服务器的IP地址,即DNS.umass.edu。最后,本地DNS服务器将查询消息直接重发给DNS.umass.edu,后者以gaia.cs.umass.edu的IP地址响应。注意,在本例中,为了获得一个主机名的映射,发送了8个DNS消息:4个查询消息和4个应答消息!我们将很快看到DNS缓存如何减少这个查询流量。
我们前面的例子假设TLD服务器知道权威DNS服务器的主机名。一般来说,这并不总是正确的。相反,TLD服务器可能只知道中间DNS服务器,而中间DNS服务器又知道权威DNS服务器的主机名。例如,假设马萨诸塞大学大学有一个DNS服务器,名为DNS.umass.edu。还假设马萨诸塞大学的每个系都有自己的DNS服务器,并且每个系的DNS服务器对系中的所有主机都具有权威性。在这种情况下,当中间DNS服务器DNS.umass.edu收到一个主机名以cs.umass.edu结尾的查询时,它将DNS.cs.umass.edu的IP地址返回给DNS.nyu.edu,这对所有以cs.umass.edu结尾的主机名是权威的。本地DNS服务器DNS.nyu.edu然后将查询发送到权威DNS服务器,后者将所需的映射返回给本地DNS服务器,而本地DNS服务器又将映射返回给请求主机。在这种情况下,总共发送10个DNS消息。
图2.19所示的例子使用了 递归查询 和 迭代查询 。从cse.nyu.edu发送到DNS.nyu.edu的查询是一个递归查询,因为查询请求DNS.nyu.edu来代表它获取映射。但是,后面的三个查询是迭代的,因为所有的答复都直接返回到DNS.nyu.edu。理论上,任何DNS查询都可以是迭代的或递归的。例如,图2.20显示了一个DNS查询链,其中所有查询都是递归的。在实践中,查询通常遵循图2.19中的模式:从请求主机到本地DNS服务器的查询是递归的,其余的查询是迭代的。
DNS 缓存
到目前为止,我们的讨论忽略了 DNS缓存 ,这是DNS系统的一个非常重要的功能。事实上,DNS广泛地利用DNS缓存来提高延迟性能,并减少在Internet上跳跃的DNS消息的数量。DNS缓存背后的思想非常简单。在查询链中,当DNS服务器收到DNS应答(例如,包含主机名到IP地址的映射)时,它可以将映射缓存到本地内存中。例如,在图2.19中,每次本地DNS服务器DNS.nyu.edu收到来自某个DNS服务器的应答时,它可以缓存应答中包含的任何信息。如果一个主机名/IP地址对缓存在DNS服务器中,并且另一个对相同主机名的查询到达DNS服务器,DNS服务器可以提供所需的IP地址,即使它不是主机名的权威机构。因为主机以及主机名和IP地址之间的映射绝不是永久的,DNS服务器在一段时间后(通常设置为2天)就会丢弃缓存信息。
例如,假设主机apricot.nyu.edu向DNS.nyu.edu查询主机名bilibili.com的IP地址。此外,假设几个小时后,另一个NYU主机,比如kiwi.nyu.edu,向DNS.nyu.edu查询相同的主机名。由于缓存,本地DNS服务器将能够立即返回bilibili.com的IP地址到第二台请求主机!无需查询任何其他DNS服务器。本地DNS服务器还可以缓存TLD服务器的IP地址,从而绕过查询链中的根DNS服务器。事实上,由于缓存,除了非常小的一部分DNS查询,其余被根服务器绕过了。
2.4.3 DNS记录和消息
共同实现DNS分布式数据库存储 资源记录(RRs,resource records) 的DNS服务器,包括提供主机名到IP地址映射的RRs。每个DNS应答消息携带一个或多个资源记录。在本节和下面的小节中,我们将简要概述DNS资源记录和消息;更多细节可以在[Albitz 1993]或DNS RFC [RFC 1034;RFC 1035]中发现。
资源记录是一个包含以下字段的四元组:
(Name, Value, Type, TTL)
TTL是资源记录的存活时间;它决定何时从缓存中移除资源。在下面给出的示例记录中,我们忽略TTL字段。Name和Value的含义与Type有关:
- 如果Type=A,则Name为主机名,Value为主机名的IP地址。因此,一个Type A记录提供了标准的主机名到IP地址的映射。例如,(relay1.bar.foo.com, 145.37.93.126, A)是Type A记录。
- 如果Type=NS (Name Server),则Name是一个域名(例如 foo.com),Value是权威DNS服务器的主机名,该服务器知道如何获取该域名中主机的IP地址。此记录用于在查询链中进一步路由DNS查询。例如,(foo.com, DNS.foo.com, NS)是Type NS记录。
- 如果Type=CNAME (Canonical NAME),则Name是主机名别名,Value是规范主机名。此记录可以为查询主机提供主机名的规范名称。例如,(foo.com, relay1.bar.foo.com, CNAME)是一条CNAME记录。
- 如果Type=MX (Mail Exchanger),则Name是主机名别名,Value是邮件服务器的规范名称。例如,(foo.com, mail.bar.foo.com, MX)是一个MX记录。MX记录允许邮件服务器的主机名使用简单的别名。注意,通过使用MX记录,公司可以为其邮件服务器和其他服务器(如Web服务器)使用相同的别名。为了获得邮件服务器的规范名称,DNS客户端将查询MX记录;要获得另一个服务器的规范名称,DNS客户端将查询CNAME记录。
如果DNS服务器对特定主机名具有权威性,那么该DNS服务器将包含主机名的Type A记录。(即使DNS服务器不是权威的,它可能在其缓存中包含Type A记录。)如果服务器对主机名没有权威性,那么服务器将包含主机名相关域名的Type NS记录;它还将包含一条Type A记录,在NS记录的Value字段中提供DNS服务器的IP地址。例如,假设一个edu TLD服务器对主机gaa.c.s.umass.edu没有权威性。然后该服务器将包含一个包含主机gaia.c.s.umass.edu的域名的记录,例如(umass.edu, DNS.umass.edu, NS)。edu TLD服务器也会包含一个Type A记录,它将映射DNS服务器DNS.umass.edu到一个IP地址,例如,(DNS.umass.edu, 128.119.40.111, A)。
DNS 消息
在本节的前面,我们提到了DNS查询和应答消息。这是仅有的两种DNS消息。此外,查询和应答消息都具有相同的格式,如图2.21所示。DNS消息中各字段的语义如下所示:
- 前12个字节是消息头区域(header section),它有许多字段。第一个字段是标识查询的16位数字。这个标识符被复制到查询的应答消息中,允许客户端将收到的应答与发送的查询匹配。在flag字段中有许多标志。1位的查询/应答标志表示该消息是查询(0)还是应答(1)。当DNS服务器是被查询名称的权威服务器时,在应答消息中设置1位的权威标志。当客户端(主机或DNS服务器)在没有记录时希望DNS服务器执行递归时,设置1位递归期望(recursion-desired)标志。如果DNS服务器支持递归,则在应答中设置一个1位递归可用字段。在消息头中,还有四个number-of字段。这些字段指示消息头后面的四种类型的数据区域(data sections)的出现次数。
- 问题区域(question section)包含关于正在进行的查询的信息。该区域包括(1)一个名称字段,其中包含查询的名称,和(2)一个问题类型的字段,例如,一个关联名称的主机地址(Type A)或关联名称的邮件服务器(Type MX)。
- 在来自DNS服务器的应答中,答案区域(answer section)包含最初查询的名称的资源记录。回想一下,在每个资源记录中都有Type(例如,A、NS、CNAME和MX)、Value和TTL。应答可以在答案中返回多个RRs,因为主机名可以有多个IP地址(例如,对于重复的Web服务器,如本节前面所讨论的)。
- 权威区域(authority section)包含其他权威服务器的记录。
- 附加区域(additional section)包含其他有用的记录。例如,对MX查询的应答中的答案字段包含提供邮件服务器规范主机名的资源记录。附加区域包含Type A记录,提供邮件服务器规范主机名的IP地址。
您希望如何将DNS查询消息直接从您正在工作的主机发送到某些DNS服务器?这可以通过 nslookup程序 轻松完成,该程序可在大多数Windows和UNIX平台上使用。例如,在Windows主机上,打开命令提示符并通过键入nslookup调用nslookup程序。调用nslookup后,可以向任意DNS服务器(根服务器、TLD服务器或权威服务器)发送DNS查询。在接收到DNS服务器的应答消息后,nslookup将显示应答中包含的记录(以人类可读的格式)。作为从自己的主机上运行nslookup的替代方法,您可以访问允许远程使用nslookup的众多Web站点之一。(只需在搜索引擎中输入nslookup,你就会被带到这些网站之一。)本章末尾的DNS Wireshark实验室将允许您更详细地探索DNS。
插入记录到DNS数据库
上面的讨论集中于如何从DNS数据库检索记录。您可能首先想知道记录是如何进入数据库的。让我们看看在一个特定示例的环境中是如何做到这一点的。假设你刚刚创建了一家令人兴奋的新公司,名为Network Utopia。您肯定要做的第一件事是在注册商处注册域名networkutopia.com。 注册商 是一个商业实体,它验证域名的唯一性,将域名输入到DNS数据库中(如下所述),并为其服务向您收取少量费用。在1999年之前,一个单一的注册商,Network Solutions,垄断了com, net和org域名的注册。但现在有许多注册商在争夺客户,互联网名称和数字地址分配机构(ICANN)对各种注册商进行了认证。认证注册人员的完整名单可在http://www.internic.net上找到。
当您向某些注册商注册域名networkutopia.com时,您还需要向注册商提供您的主要和次要权威DNS服务器的名称和IP地址。假设名称和IP地址分别为DNS1.networkutopia.com、DNS2.networkutopia.com、212.2.212.1和212.212.212.2。对于这两个权威DNS服务器中的每一个,注册商将确保Type NS和Type A记录输入TLD com服务器。具体来说,对于networkutopia.com的主要权威服务器,注册商将向DNS系统插入以下两个资源记录:
(networkutopia.com, DNS1.networkutopia.com, NS)
(DNS1.networkutopia.com, 212.212.212.1, A)
您还必须确保将Web服务器www.networkutopia.com的Type A资源记录和邮件服务器mail.networkutopia.com的Type MX资源记录输入到权威DNS服务器中。(直到最近,每个DNS服务器的内容都是静态配置的,例如,从系统管理员创建的配置文件中配置。稍后,在DNS协议中添加了一个UPDATE选项,允许通过DNS消息从数据库中动态地添加或删除数据。[RFC 2136]和[RFC 3007]指定DNS动态更新。)
完成所有这些步骤后,人们就可以访问您的Web站点并向您公司的员工发送电子邮件。让我们通过验证这一说法的正确性来结束对DNS的讨论。这种验证还有助于巩固我们对DNS的了解。假设澳大利亚的Alice想要查看Web页面www.networkutopia.com。如前所述,她的主机将首先向她的本地DNS服务器发送一个DNS查询。本地DNS服务器将联系TLD com服务器。(如果TLD com服务器的地址没有缓存,本地DNS服务器必须联系根DNS服务器。)这个TLD服务器包含上面列出的Type NS和Type A资源记录,因为注册商将这些资源记录插入到所有的TLD com服务器中。TLD com服务器向Alice的本地DNS服务器发送一个应答,该应答包含两个资源记录。然后,本地DNS服务器向212.212.212.1发送DNS查询,请求www.networkutopia.com对应的Type A记录。该记录提供所需Web服务器的IP地址,例如,212.212.71.4,本地DNS服务器将该地址传回Alice的主机。Alice的浏览器现在可以发起到主机212.212.71.4的TCP连接,并通过该连接发送HTTP请求。唷!在网上浏览的时候,会有比眼前看到的更多的东西。
聚焦安全之——DNS漏洞
我们已经看到,DNS是Internet基础设施的关键组件,许多重要的服务——包括Web和电子邮件——没有它就无法正常工作。
首先想到的攻击类型是针对DNS服务器的DDoS带宽洪水攻击(见章节1.6)。例如,攻击者可能试图向每个DNS根服务器发送大量的数据包,以至于大多数合法的DNS查询都得不到答案。针对DNS根服务器的大规模DDoS攻击在2002年10月21日发生过。在这种攻击中,攻击者利用僵尸网络向13个DNS根IP地址中的每个地址发送大量ICMP ping消息。(ICMP消息将在第5.6节中讨论。现在,只要知道ICMP数据包是特殊类型的IP数据报就足够了。)幸运的是,这种大规模的攻击造成的损害很小,对用户的互联网体验影响很小或没有影响。攻击者确实成功地将大量数据包定向到根服务器上。但是,许多DNS根服务器都受到数据包过滤器的保护,数据包过滤器被配置为总是阻止指向根服务器的所有ICMP ping消息。因此,这些受保护的服务器得以保留并正常运行。此外,大多数本地DNS服务器缓存顶级域名服务器的IP地址,允许查询过程在大多数情况下绕过DNS根服务器。
一个潜在的更有效的DDoS攻击是将大量的DNS请求发送到顶级域名服务器,例如,发送到处理.com域的顶级域名服务器。更难以过滤指向DNS服务器的DNS查询;而且顶级域名服务器不像根服务器那样容易被绕过。2016年10月21日,顶级域名服务提供商Dyn遭到此类攻击。这种DDoS攻击是通过来自僵尸网络的大量DNS查询请求完成的,该僵尸网络由大约10万台被Mirai恶意软件感染的物联网设备(如打印机、IP摄像头、住宅网关和婴儿监视器)组成。整整一天,亚马逊(Amazon)、推特(Twitter)、Netflix、Github和Spotify都被扰乱了。
DNS可能会以其他方式受到攻击。在中间人攻击中,攻击者拦截来自主机的查询并返回伪造的应答。DNS投毒攻击是指攻击者向DNS服务器发送虚假应答,诱使DNS服务器接受虚假记录进入其缓存。例如,可以使用这两种攻击将毫无戒心的Web用户重定向到攻击者的Web站点。DNS安全扩展(DNSSEC [Gieben 2004;RFC 4033]的设计和部署就是为了防止此类攻击。DNSSEC是DNS的一个安全版本,它解决了许多可能的攻击,并在互联网上越来越受欢迎。
2.5 P2P 文件分发
到目前为止,本章中描述的应用程序——包括Web、电子邮件和DNS——都采用客户端-服务器架构,极大地依赖于始终在线的基础架构服务器。回想一下第2.1.1节,在P2P架构中,对基础架构服务器的依赖最小(或不依赖)。相反,成对的间歇连接的主机(称为对等点)直接相互通信。这些对等点不属于服务提供商,而是由用户控制的个人电脑、笔记本电脑和智能手机。
在本节中,我们探讨一个非常自然的P2P应用程序,即将一个大文件从单个服务器分发到大量主机(称为对等点)。该文件可能是Linux操作系统的新版本、现有操作系统的软件补丁或MPEG视频文件。在客户端-服务器文件分发中,服务器必须向每个对等点发送文件的副本,这给服务器带来了巨大的负担,并消耗了大量的服务器带宽。在P2P文件分发中,每个对等点可以将收到的文件的任何部分重新分发给其他对等点,从而帮助服务器进行分发。截至2020年,最流行的P2P文件分发协议是BitTorrent。最初由Bram Cohen开发,现在有许多不同的独立的BitTorrent客户端符合BitTorrent协议,就像有许多Web浏览器客户端符合HTTP协议一样。在这个小节中,我们首先检查在文件分发的背景下P2P架构的自伸缩性。然后我们详细描述BitTorrent,突出其最重要的特点和功能。
P2P架构的可扩展性
为了比较客户端-服务器架构和点对点架构,并说明P2P固有的自扩展性(self-scalability),我们现在考虑一个简单的定量模型,用于为这两种架构类型将文件分发到固定的对等点集合。如图2.22所示,服务器和对等点通过接入链路连接到Internet。用us表示服务器接入链路的上传速率,ui表示第i个对等点接入链路的上传速率,di表示第i个对等点接入链路的下载速率。另外,F表示要分发的文件的大小(以比特为单位),N表示希望获得文件副本的对等点的数量。 分发时间 是将文件的副本发送到所有N个对等点所需要的时间。在下面我们对客户端-服务器和P2P架构的分发时间的分析中,我们做了一个简化的(通常是准确的[Akella 2003])假设,即Internet核心有充足的带宽,这意味着所有的瓶颈都在接入网络中。我们还假设服务器和客户端不参与任何其他网络应用程序,因此他们所有的上传和下载接入带宽可以完全用于分发这个文件。
让我们首先确定客户端-服务器架构的分发时间,我们用Dcs表示。在客户端-服务器架构中,没有一个对等点帮助分发文件。我们作出以下观察:
服务器必须向N个对等点发送一个文件副本。因此,服务器必须传输NF比特。因为服务器的上传速率是us,所以分发文件的时间必须至少是NF/us。
令dmin表示下载速率最低的对等点的下载速率,即dmin = min{d1, d2,…, dN}。下载速率最低的对等点无法在小于F/dmin秒内获得文件的所有比特。因此,最小分发时间至少为F/dmin。
把这两个观测结果放在一起,我们得到:
>= max {, }
这为客户端-服务器架构提供了最小分发时间的下限。在家庭作业的问题中,你将被要求证明服务器可以调度它的传输,从而使下限更加完美。我们把上面给出的下限作为实际分发时间:
Dcs = max {, } (2.1)
从公式2.1中我们可以看出,当N足够大时,客户端-服务器分发时间由NF/us给出。因此,分发时间随对等点N的数量线性增加。例如,如果对等点的数量从这周到下周增加了1000倍,从1000增加到100万,那么将文件分发给所有对等点所需的时间就增加1000倍。
现在让我们对P2P架构进行类似的分析,其中每个对等点可以帮助服务器分发文件。特别是当对等点接收到一些文件数据时,可以使用自己的上传能力将数据重新分发给其他对等点。计算P2P体系结构的分发时间比计算客户端-服务器架构的分发时间要复杂一些,因为分发时间取决于每个对等点如何将文件的部分分发给其他对等点。然而,最小分配时间可以得到一个简单的表达式[Kumar 2006]。为此,我们首先提出以下观察结果:
- 在分发开始时,只有服务器拥有该文件。为了将该文件放入对等点社区,服务器必须将文件的每个比特至少发送一次到其接入链路中。因此,最小分发时间至少为F/us。(与客户端-服务器方案不同,服务器发送一次的比特不一定要再次发送,因为对等点可能会在它们自己之间重新分配比特。)
- 与客户端-服务器架构一样,下载速率最低的对等点无法在小于F/dmin秒内获得文件的所有F比特。因此,最小分配时间至少为F/dmin。
- 最后,观察整个系统的总上传容量等于服务器的上传速率加上每个独立对等点的上传速率,即utotal = us + u1 + ... + uN。系统必须向N个对等点发送(上传)F比特,总共发送NF比特。这不能比utotal更快的速度完成。因此,最小分发时间也至少为NF/(us + u1 + ... + uN)。
将这三种观测结果放在一起,我们得到了P2P的最小分发时间,用DP2P表示:
式2.2给出了P2P架构最小分发时间的下限。结果是,如果我们想象每个对等点一旦收到比特就可以重新分发。其实就有这么一个重新分发方案,实际上达到了这个下限[Kumar 2006]。(我们将在作业中证明这个结果的一个特例。)在现实中,文件是以块(chunk)重新分发的,而不是单独的比特,方程2.2是实际最小分发时间的一个很好的近似值。因此,我们取公式2.2提供的下界作为实际的最小分配时间,即:
图2.23比较了客户端-服务器和P2P架构的最小分发时间,假设所有的对等点都有相同的上传速率u。在图2.23中,我们设置F/u = 1小时,us = 10u, dmin >= us。因此,对等点可以在一小时内传输整个文件,服务器的传输速率是对等点上传速率的10倍,并且(为了简单起见)对等点的下载速率设置得足够大,以免产生影响。从图2.23中我们可以看出,对于客户端-服务器架构来说,随着对等点数量的增加,分发时间线性增加,没有上限。然而,对于P2P体系结构,最小的分发时间不仅总是小于客户端-服务器架构的分发时间,对于任意数量的对等点n,它也不超过1小时。因此,具有P2P架构的应用程序是自扩展的。
BitTorrent
BitTorrent是一种流行的P2P文件分发协议[Chao 2011]。在BitTorrent术语中,所有参与分发特定文件的对等点的集合称为torrent。torrent中的对等点从彼此下载相同大小的文件块,典型的块大小为256kbytes。当对等点第一次加入torrent时,它没有块。随着时间的推移,它积累了越来越多的块。在下载块的同时,它也向其他对等点上传块。一旦某个对等点获得了整个文件,它可能(自私地)离开torrent,或(利他地)留在torrent中,并继续向其他对等点上传块。同样,任何对等点可以在任何时候只带着一个子集的块离开torrent,然后再重新加入torrent。
图2.24 BitTorrent文件分发
现在让我们来仔细看看BitTorrent是如何运作的。由于BitTorrent是一个相当复杂的协议和系统,我们将只描述它最重要的机制。每个torrent都有一个基础设施节点,称为跟踪器(tracker)。当一个对等点加入一个torrent时,它向跟踪器注册自己,并周期性地通知跟踪器它仍然在这个torrent中。通过这种方式,跟踪器跟踪参与torrent的对等点。在任何时刻,一个特定的torrent可能会有少于10个或超过1000个对等点参与。
如图2.24所示,当一个新的对等点Alice加入torrent时,跟踪器随机从一组参与的对等点中选择一个子集(具体来说,比如50个),并将这50个对等点的IP地址发送给Alice。拥有这个对等点列表后,Alice尝试与该列表上的所有对等点建立并发TCP连接。我们将Alice成功建立TCP连接的所有对等点称为邻居(neighboring peers)。(在图2.24中,Alice只有三个邻居。正常情况下,她会有更多。)随着时间的推移,这些对等点中的一些可能会离开,而其他的对等点(在最初的50之外)可能会尝试与Alice建立TCP连接。所以对等点的邻居会随着时间的推移而波动。
在任何给定的时间,每个对等点都有一个文件块的子集,不同的对等点有不同的子集。Alice会周期性地(通过TCP连接)向她的每一个邻居询问它们拥有的块的列表。如果Alice有L个不同的邻居,她将得到L个块列表。有了这些信息,Alice将(再次通过TCP连接)对她当前没有的块发出请求。
所以在任何给定的时刻,Alice会有一个块的子集并且知道她的邻居有哪些块。有了这些信息,Alice将做出两个重要的决定。首先,她应该先向邻居请求哪些块?其次,她应该向她哪个邻居请求块?在决定请求哪些数据块时,Alice使用了一种称为稀有优先(rarest first)的技术。这个想法是,从她没有的块中,确定在她的邻居中最稀有的块(也就是说,在她的邻居中有最少重复拷贝的块),然后首先请求那些最稀有的块。通过这种方式,最稀有的块可以更快地重新分配,目的是(大致地)使torrent中每个块的拷贝数量相等。
BitTorrent使用了一种聪明的交易算法来决定她会回应哪些请求。其基本思想是,Alice优先考虑当前以最高速率提供给她数据的邻居。具体来说,对于她的每一个邻居,爱丽丝不断地测量她接收比特的速度,并确定四个以最高速度喂给她比特的对等点。然后,她通过向这四个对等点发送chunk作为回报。每隔10秒,她就会重新计算速率,并可能修改4个对等点的集合。用BitTorrent的行话来说,这四个人都是 畅通无阻(unchoked) 的。重要的是,每隔30秒,她还会随机选择一个额外的邻居,并向它发送数据块。我们称随机选择的对等点为Bob。在BitTorrent的行话中,Bob是 乐观的不被阻塞的(optimistically unchoked) 。因为Alice正在向Bob发送数据,她可能成为Bob的四大上传者之一,在这种情况下Bob将开始向Alice发送数据。如果Bob向Alice发送数据的速率足够高,那么Bob就可以成为Alice的四大上传者之一。换句话说,每隔30秒,Alice将随机选择一个新的交易伙伴,并开始与该伙伴进行交易。如果两个对等点对交易感到满意,他们会把对方放在前四名名单中,并继续彼此交易,直到其中一个对等点找到更好的伙伴。其结果是,能够以兼容速率上传的对等点往往会找到彼此。随机邻居选择也允许新邻居获得区块,这样他们就可以有东西进行交易。除了这5个对等点(4个顶级对等点和1个探测对等点)之外,其他所有的相邻对等点都被阻塞,也就是说,它们没有从Alice那里收到任何块。BitTorrent有许多有趣的机制,这里没有讨论,包括pieces,(mini-chunks),pipelining,random first selection,残局模式(endgame mode),和anti-snubbing [Cohen 2003]。
刚才描述的交易激励机制通常被称为tit-for-tat(以彼之道还施彼身) [Cohen 2003]。已经证明,这种激励机制是可以规避的[Liogkas 2006;Locher 2006;Piatek 2008]。尽管如此,BitTorrent生态系统还是非常成功的,成千上万的用户同时分享着成千上万的torrent文件。如果BitTorrent没有被设计成tit-for-tat(或一种变体),但在其他方面完全相同,BitTorrent可能甚至不会存在,因为大多数用户将会是白嫖怪(freeriders)[Saroiu 2002]。
在结束对P2P的讨论时,我们简要地提到了P2P的另一个应用,即分布式哈希表(DHT Distributed Hash Table)。分布式哈希表是一种简单的数据库,数据库记录分布在P2P系统中的对等点上。DHT已经被广泛应用(例如,在BitTorrent中),并且已经成为广泛研究的主题。在配套网站的视频说明中提供了一个概述。
2.6 视频流和内容分发网络
据估计,到2020年,包括Netflix、YouTube和亚马逊Prime在内的流媒体视频约占互联网流量的80%(思科2020)。本节我们将概述当今互联网上流行的视频流服务是如何实现的。我们将看到它们是通过应用程序级协议和服务器实现的,这些服务器的功能在某些方面类似于缓存。
2.6.1 网络视频
在流媒体存储的视频应用程序中,底层媒体是预先录制的视频,如电影、电视节目、预先录制的体育赛事或预先录制的用户生成的视频(如在YouTube上常见的那些视频)。这些预先录制好的视频放在服务器上,用户向服务器发送请求,按需查看视频。如今,许多互联网公司都提供流媒体视频,包括Netflix、YouTube、Amazon和TikTok。
但是在开始讨论视频流之前,我们应该先对视频媒体本身有一个快速的感觉。视频是一个图像序列,通常以恒定的速度显示,例如,每秒显示24或30张图像。一个未压缩的数字编码图像由一个像素数组组成,每个像素被编码成若干比特来表示亮度和颜色。视频的一个重要特征是它可以被压缩,从而用比特率来权衡视频质量。目前现有的压缩算法基本上可以将视频压缩到任何想要的比特率。当然,比特率越高,图像质量就越好,用户的整体观看体验也就越好。
从网络的角度来看,视频最显著的特点可能是它的高比特率。压缩的互联网视频通常在100kbps(低质量视频)到4Mbps(高清电影流媒体)之间;4K流媒体的比特率预计将超过10Mbps。这可以转化为巨大的流量和存储,特别是对于高端视频。例如,一个2Mbps的视频时长67分钟将消耗1g的存储和流量。到目前为止,对视频流最重要的性能衡量是平均端到端吞吐量。为了提供连续播放,网络必须为流应用程序提供至少与压缩视频的比特率一样大的平均吞吐量。
我们也可以使用压缩来创建同一个视频的多个版本,每个版本都有不同的质量级别。例如,我们可以使用压缩来创建相同视频的三个版本,速率分别为300kbps、1Mbps和3Mbps。用户可以根据当前可用带宽来决定他们想看哪个版本。拥有高速互联网连接的用户可以选择3Mbps的版本;用智能手机通过3G观看视频的用户可能会选择300kbps的版本。
2.6.2 HTTP流媒体和DASH
在HTTP流中,视频只是作为一个带有特定URL的普通文件存储在HTTP服务器上。当用户想要看视频时,客户端与服务器建立TCP连接,并为该URL发出HTTP GET请求。然后,服务器在HTTP响应消息中以底层网络协议和流量条件允许的最快速度发送视频文件。在客户端,字节在客户端应用程序缓冲区中收集。一旦这个缓冲区中的字节数超过预定的阈值,客户端应用程序就开始播放。流媒体视频应用程序周期性地从客户端应用程序缓冲区中抓取视频帧,解压缩这些帧,并在用户屏幕上显示它们。因此,视频流应用程序在接收视频时显示视频,并缓冲与视频的后面部分相对应的帧。
尽管如之前所述,HTTP流媒体已经在实践中被广泛部署(例如,YouTube从一开始就部署了),但它有一个主要的缺点:所有客户端接收相同的视频编码,尽管客户端可用的带宽量有很大的变化,无论是在不同的客户端,还是在同一客户端。这导致了一种新型的基于HTTP的流媒体的开发,通常称为 基于HTTP的动态自适应流媒体(DASH, Dynamic Adaptive Streaming over HTTP) 。在DASH中,视频被编码成几个不同的版本,每个版本都有不同的比特率,相应的,也有不同的质量级别。客户端动态地请求长度为几秒的视频片段块。当可用带宽高时,客户端自然会从高速率的版本中选择块;当可用带宽较低时,它自然会从低速率的版本中选择。客户端使用HTTP GET请求消息一次选择一个不同的块[Akhshabi 2011]。
DASH允许具有不同互联网接入速率的客户端以不同的编码速率传输视频。低速3G连接的客户端可以接收到低比特率(低质量)的版本,光纤连接的客户端可以接收到高质量的版本。如果会话期间可用的端到端带宽发生变化,DASH还允许客户端适应可用带宽。这个功能对于移动用户特别重要,他们通常看到自己的带宽可用性随着它们相对于基站的移动而波动。
使用DASH,每个视频版本都存储在HTTP服务器中,每个版本都有不同的URL。HTTP服务器也有一个 清单文件(manifest file) ,它提供了每个版本的URL及其比特率。客户端首先请求清单文件并了解各种版本。然后,客户端通过在HTTP GET请求消息中为每个块指定URL和字节范围,每次选择一个块。在下载块时,客户端还测量接收到的带宽,并运行一个速率确定算法来选择接下来要请求的块。当然,如果客户端有很多缓冲的视频,如果测量的接收带宽很高,它会从高比特率版本中选择一个块。当然,如果客户端有很少的视频缓冲和测量的接收带宽很低,它会从一个低比特率版本中选择一个块。因此,DASH允许客户在不同的质量水平之间自由切换。
2.6.3 内容分发网络
今天,许多互联网视频公司每天都在向数百万用户分发多Mbps(multi-Mbps)的点播流。例如,YouTube拥有数以亿计的视频库,每天向世界各地的用户分发数以亿计的视频流。将所有这些流量流到世界各地,同时提供持续的播放和高交互性,显然是一项具有挑战性的任务。
对于互联网视频公司来说,提供流媒体视频服务的最直接的方法可能是建立一个单独的大型数据中心,将其所有的视频存储在数据中心,然后将视频直接从数据中心传输到全球的客户端。但是这种方法有三个主要问题。首先,如果客户端离数据中心很远,服务器到客户端数据包将会跨越许多通信链路,并且很可能通过许多ISP,其中一些ISP可能位于不同的大陆。如果其中一个链路提供的吞吐量小于视频消耗速率,端到端吞吐量也将低于视频消耗速率,从而导致用户讨厌的冻结延迟。(回想第1章,流的端到端吞吐量由瓶颈链路的吞吐量控制。)随着端到端路径中链路数量的增加,这种情况发生的可能性也会增加。第二个缺点是,一个受欢迎的视频可能会通过相同的通信链路被多次发送。这不仅浪费了网络带宽,而且互联网视频公司本身也要为其提供商ISP(连接到数据中心)一次又一次地向互联网发送相同的字节而支付费用。这种解决方案的第三个问题是,单个数据中心代表一个故障点——如果数据中心或其与Internet的链路中断,它将无法分发任何视频流。
为了应对向分布在世界各地的用户分发大量视频数据的挑战,几乎所有主要的视频流媒体公司都使用 内容分发网络(CDN Content Distribution Networks) 。CDN管理多个地理位置上分布的服务器,在其服务器中存储视频(和其他类型的Web内容,包括文档、图像和音频)的副本,并试图将每个用户请求导向一个CDN位置,以提供最佳的用户体验。CDN可以是私有CDN,即由内容提供商本身拥有;例如,谷歌的CDN分发YouTube视频和其他类型的内容。该CDN也可以是代表多个内容提供者分发内容的第三方CDN;Akamai、Limelight和Level-3都运营着第三方CDN。一个非常易读的现代CDN概述是[Leighton 2009;Nygren 2010]。
CDN通常采用两种不同的服务器布局原则中的一种[Huang 2008]:
- Enter Deep 由Akamai开创的一种理念是,通过在世界各地的接入ISP中部署服务器集群,深入进入互联网服务提供商的接入网络。(接入网请参见1.3章节。)Akamai在数千个位置的集群中采用了这种方法。其目标是接近终端用户,通过减少终端用户和接收内容的CDN服务器之间的链路和路由器的数量,从而提高用户感知的延迟和吞吐量。由于这种高度分布式的设计,维护和管理集群的任务变得非常具有挑战性。
- Bring Home Limelight和许多其他CDN公司采用的第二种设计理念是,通过在数量较少(比如几十个)的站点上建立大型集群,将ISP bring-home。这些CDN通常将它们的集群放置在互联网交换点(IXP)中,而不是进入接入ISP内部(见章节1.3)。与前面的设计理念相比,bring-home的设计通常会导致更低的维护和管理开销,可能会以终端用户更高的延迟和更低的吞吐量为代价。
一旦它的集群就位,CDN就会在它的集群之间复制内容。CDN可能不希望在每个集群中放置每个视频的副本,因为有些视频很少被观看或只在一些国家流行。事实上,很多CDN不推(push)视频到他们的集群,而是使用一个简单的拉(pull)策略:如果一个客户端从一个集群请求视频而视频不存在,那么该集群将检索视频(从中央存储库或从另一个集群)并存储为本地副本,同时将视频传输给客户端。类似Web缓存(参见2.2.5节),当集群存储空间满时,它会删除不经常请求的视频。
CDN 操作
在确定了部署CDN的两种主要方法之后,现在让我们深入了解CDN如何运行的具体细节。当用户主机的浏览器被命令检索一个特定的视频(通过一个URL识别),CDN必须拦截请求,以便它可以(1)为该客户端确定一个合适的CDN服务器集群,和(2)重定向客户端请求到服务器集群。我们将简要讨论CDN如何确定合适的集群。但首先让我们来看看拦截和重定向请求背后的机制。
大多数CDN利用DNS拦截和重定向请求。让我们考虑一个简单的示例来说明是如何涉及到DNS的。假设一个内容提供商,NetCinema,雇佣了第三方CDN公司,KingCDN,把它的视频分发给它的客户。在NetCinema Web页面上,它的每个视频都被分配一个URL,该URL包含“video”字符串和视频本身的唯一标识符;例如,《变形金刚7》可能被分配到http://video.netcinema.com/6Y7B23V。然后出现6个步骤,如图2.25所示:
- 用户访问NetCinema的网页。
- 当用户单击链接http://video.netcinema.com/6Y7B23V时,用户的主机发送一个针对video.netcinema.com的DNS查询。
- 用户的本地DNS服务器(LDNS)将DNS查询转发给权威的NetCinema DNS服务器,该服务器观察主机名video.netcinema.com中的字符串video。为了将DNS查询移交给KingCDN, NetCinema授权DNS服务器不返回IP地址,而是返回给LDNS一个在KingCDN域的主机名,例如a1105.kingCDN.com。
- 至此,DNS查询进入KingCDN的私有DNS基础设施。然后用户的LDNS发送第二个查询,现在是针对a1105.kingCDN。最终,KingCDN的DNS系统将KingCDN内容服务器的IP地址返回给LDNS。因此,在KingCDN的DNS系统中,指定了客户端接收内容的CDN服务器。
- LDNS将提供内容服务的CDN节点的IP地址转发给用户的主机。
- 一旦客户端收到KingCDN内容服务器的IP地址,它就会与该IP地址的服务器建立直接的TCP连接,并为视频发出HTTP GET请求。如果使用了DASH,服务器首先会向客户端发送一个包含URL列表的清单文件,每个版本的视频都有一个URL列表,客户端会动态地从不同版本中选择块。
案例学习之--谷歌的网络基础设施
为了支持其大量的服务——包括搜索、Gmail、日历、YouTube视频、地图、文档和社交网络——谷歌已经部署了一个广泛的私有网络和CDN基础设施。谷歌的CDN基础结构有三层服务器集群:
- 北美、欧洲和亚洲共有19个大型数据中心[谷歌Locations 2020],每个数据中心拥有约10万台服务器。这些大型数据中心负责提供动态(通常是个性化的)内容,包括搜索结果和Gmail消息。
- IXP中大约有90个集群分布在世界各地,每个集群由数百台服务器组成[Adhikari 2011a][谷歌CDN 2020]。这些集群负责提供静态内容,包括YouTube视频。
- 位于接入ISP内的数百个“Enter Deep”集群。这里的集群通常由一个机架内的数十台服务器组成。这些enter-deep服务器执行TCP分割(splitting,见3.7节)并提供静态内容[Chen 2011],包括嵌入搜索结果的Web页面的静态部分。
所有这些数据中心和集群位置都与谷歌自己的专用网络连接在一起。当用户进行搜索查询时,通常该查询首先通过本地ISP发送到附近的enter-deep缓存中,从那里获取静态内容;在向客户端提供静态内容的同时,附近的缓存也通过谷歌的私有网络将查询转发到一个大型数据中心,从那里获得个性化的搜索结果。对于YouTube视频,视频本身可能来自一个bring-home的缓存,而围绕视频的Web页面的部分可能来自附近的enter-deep缓存,围绕视频的广告来自数据中心。综上所述,除了本地ISP,谷歌云服务主要是由独立于公共互联网的网络基础设施提供的。
集群的选择策略
任何CDN部署的核心都是 集群选择策略(cluster selection strategy) ,即动态地将客户端指向CDN内的服务器集群或数据中心的机制。正如我们刚才看到的,CDN通过客户端的DNS查找来获知客户端的LDNS服务器的IP地址。CDN获知该IP地址后,需要根据该IP地址选择合适的集群。CDN通常采用专有的集群选择策略。我们现在简要地调查了几种方法,每一种都有自己的优点和缺点。
使用商业 地理位置(geographically closest) 数据库(如Quova [Quova 2020]和MaxMind [MaxMind 2020]),每个LDNS IP地址都被映射到一个地理位置。当CDN接收到一个特定的LDNS请求时,CDN会选择地理上最近的集群。这样的解决方案可以很好地应用于大部分客户[Agarwal 2009]。但是,对于某些客户端,该解决方案的性能可能很差,因为地理上最近的集群可能在网络路径的长度或跳数方面不是最近的集群。此外,所有基于DNS的方法都存在一个固有的问题,即一些终端用户被配置为使用远程定位的lDNS [Shaikh 2001;Mao 2002],在这种情况下,LDNS的位置可能离客户的位置很远。此外,这个简单的策略忽略了延迟和可用带宽随时受Internet路径变化影响,而总是将相同的集群分配给特定的客户端。
为了根据当前的流量情况为客户端确定最佳的集群,CDN可以在其集群和客户端之间执行周期性的延迟和丢包性能的 实时测量(real-time measurements) 。例如,一个CDN可以让它的每个集群周期性地发送探测(例如,ping消息或DNS查询)发送到世界各地的所有LDNS。这种方法的一个缺点是,许多LDNS被配置为不响应此类探测。
2.6.4 案例分析:Netflix和YouTube
我们通过看两个非常成功的大规模部署来结束对流媒体存储视频的讨论:Netflix和YouTube。我们将看到,每个系统都采用非常不同的方法,但使用了本节中讨论的许多基本原则。
Netflix
截至2020年,Netflix是北美领先的在线电影和电视剧服务提供商。正如我们下面所讨论的,Netflix视频分发有两个主要组件:Amazon云和它自己的私有CDN基础设施。
Netflix拥有一个可以处理许多功能的网站,包括用户注册和登录、计费、用于浏览和搜索的电影目录,以及电影推荐系统。如图2.26所示,这个Web站点(及其关联的后端数据库)完全运行在Amazon云中的Amazon服务器上。此外,Amazon云还处理以下关键功能:
- 内容吸收(Content ingestion) 在Netflix将一部电影分发给客户之前,它必须首先吸收和处理这部电影。Netflix接收电影制片厂的主版本,并将它们上传到亚马逊云的主机上。
- 内容处理 Amazon云中的机器为每部电影创建了许多不同的格式,适用于运行在台式机、智能手机和连接到电视的游戏机上的各种视频播放器客户端。每个格式都创建了不同的版本,并且有多个比特率,允许使用DASH在HTTP上自适应流。
- 上传版本到CDN 一旦创建了一部电影的所有版本,Amazon云中的主机就会将这些版本上传到它的CDN中。
当Netflix在2007年首次推出视频流媒体服务时,它雇佣了三家第三方CDN公司来分发其视频内容。此后,Netflix创建了自己的私有CDN,现在所有的视频都是从CDN上播放的。为了创建自己的CDN, Netflix已经在IXP和住宅ISP内部安装了服务器机架。Netflix目前拥有超过200个IXP站点的服务器机架;请参阅[Bottger 2018] [Netflix Open Connect 2020],以获取Netflix机架的当前IXP列表。还有数百家ISP提供Netflix服务;也见[Netflix Open Connect 2020],其中Netflix向潜在的ISP合作伙伴提供有关为其网络安装(免费)Netflix机架的说明。机架中的每台服务器都有几个10gbps以太网端口和超过100tb的存储空间。机架中的服务器数量各不相同:IXP安装通常有数十台服务器,并包含整个Netflix流媒体视频库,包括支持DASH的多个版本的视频。Netflix没有使用拉式缓存(pull-caching 章节2.2.5)在IXP和ISP中填充其CDN服务器。相反,Netflix通过在非高峰时段将视频推送到CDN服务器进行分发。对于那些不能容纳整个视频库的地方,Netflix只推送最受欢迎的视频,这些视频是按天确定的。Netflix CDN设计在YouTube视频[Netflix Video 1]和[Netflix Video 2]中有一些详细的描述;也见[Bottger 2018]。
在描述了Netflix体系结构的组件之后,让我们进一步了解在电影交付过程中涉及到的客户端和各种服务器之间的交互。如前所述,用于浏览Netflix视频库的Web页面是从Amazon云中的服务器提供的。当用户选择要播放的电影时,在Amazon云上运行的Netflix软件首先确定哪些CDN服务器拥有该电影的副本。在拥有该影片的服务器中,软件将决定该客户端请求的最佳服务器。如果客户端使用的是在该ISP中安装了Netflix CDN服务器机架的住宅ISP,并且该机架具有所请求的电影的副本,则通常会选择该机架中的服务器。如果不是,通常会选择附近的IXP服务器。
一旦Netflix确定了交付内容的CDN服务器,它就向客户端发送特定服务器的IP地址以及一个清单文件,其中包含所请求电影的不同版本的URL。客户端和CDN服务器然后直接使用专有版本的DASH进行交互。具体来说,如2.6.2节所述,客户端使用HTTP GET请求消息中的byte-range头来请求来自电影不同版本的块。Netflix使用大约4秒长的块[Adhikari 2012]。当块被下载时,客户端测量接收到的吞吐量,并运行一个速率确定算法来确定下一个请求块的质量。
Netflix体现了本节前面讨论的许多关键原则,包括自适应流媒体和CDN分发。然而,由于Netflix使用它自己的私有CDN,它只分发视频(而不是网页),Netflix已经能够简化和定制它的CDN设计。特别是,Netflix不需要使用DNS重定向,如2.6.3节所讨论的,以连接特定的客户端到CDN服务器;相反,Netflix软件(运行在Amazon云上)直接告诉客户端使用特定的CDN服务器。此外,Netflix CDN使用的是推式缓存而不是拉式缓存(章节2.2.5):内容在非高峰时间被推送到服务器,而不是在缓存缺失时动态推送。
YouTube
每分钟有数百个小时的视频被上传到YouTube上,每天有数十亿次的视频点击量,YouTube无疑是世界上最大的视频分享网站。YouTube于2005年4月开始服务,2006年11月被谷歌收购。虽然谷歌/YouTube的设计和协议是专有的,但通过一些独立的测量工作,我们可以大致理解关于YouTube是如何运作的[Zink 2009;Torres 2011;Adhikari 2011]。与Netflix一样,YouTube广泛使用CDN技术来分发视频[Torres 2011]。与Netflix类似,谷歌使用自己的私有CDN来分发YouTube视频,并在数百个不同的IXP和ISP地点安装了服务器集群。谷歌从这些地方直接从其庞大的数据中心分发YouTube视频[Adhikari 2011a]。然而,与Netflix不同的是,谷歌使用拉式缓存(如2.2.5节所述)和DNS重定向(如2.6.3节所述)。在大多数情况下,谷歌的集群选择策略将客户端导向RTT最低的集群;然而,为了平衡集群之间的负载,有时客户端(通过DNS)被定向到一个更遥远的集群[Torres 2011]。
YouTube使用HTTP流媒体,通常为一个视频提供少量不同的版本,每个版本都有不同的比特率和相应的质量级别。YouTube不采用自适应流媒体(如DASH),而是要求用户手动选择一个版本。为了节省因重新定位或提前终止而浪费的带宽和服务器资源,YouTube使用HTTP字节范围请求来限制在预先载入视频之后的数据流的传输。
2.7 套接字编程:创建网络应用程序
现在我们已经了解了许多重要的网络应用程序,让我们来看看网络应用程序实际上是如何创建的。在第2.1节中,一个典型的网络应用程序由一对程序组成——一个客户端程序和一个服务器程序——驻留在两个不同的终端系统中。当这两个程序执行时,会创建一个客户端进程和一个服务端进程,这两个进程通过读取和写入套接字来相互通信。因此,在创建网络应用程序时,开发人员的主要任务是编写客户端和服务器程序的代码。
网络应用程序有两种类型。一种类型是在协议标准(如RFC或其他标准文档)中指定操作的实现;这种应用程序有时被称为开放的,因为指定其操作的规则是众所周知的。对于这样的实现,客户端和服务器程序必须遵守RFC规定的规则。例如,客户端程序可以是HTTP协议的客户端实现,在章节2.2中描述并在RFC 2616中精确定义;类似地,服务器程序可以是HTTP服务器协议的实现,也在RFC 2616中精确地定义了。如果一个开发人员为客户端程序编写代码,另一个开发人员为服务器程序编写代码,并且双方都仔细地遵循RFC的规则,那么这两个程序将能够互操作。事实上,今天的许多网络应用程序都涉及到由独立开发者创建的客户端和服务器程序之间的通信,例如,谷歌Chrome浏览器与Apache Web服务器通信,或者BitTorrent客户端与BitTorrent跟踪器通信。
另一种类型的网络应用程序是专有网络应用程序。在这种情况下,客户端和服务器程序使用了一个没有在RFC或其他地方公开发布的应用层协议。一个单独的开发人员(或开发团队)创建客户端和服务器程序,并且开发人员完全控制代码中的内容。但是,由于代码没有实现开放协议,其他独立开发人员将无法开发与应用程序互操作的代码。
在本节中,我们将研究开发客户端-服务器应用程序中的关键问题,并通过查看实现非常简单的客户端-服务器应用程序的代码来深入了解。在开发阶段,开发人员必须做出的第一个决定是应用程序是在TCP上运行还是在UDP上运行。回想一下,TCP是面向连接的,它提供了一个可靠的字节流通道,数据通过它在两端系统之间流动。UDP是无连接的,从一端系统发送独立的数据包到另一端系统,没有任何关于传输的保证。回想一下,当客户端或服务器程序实现由RFC定义的协议时,它应该使用与该协议相关的已知端口号;相反,在开发专有应用程序时,开发人员必须小心避免使用这种众所周知的端口号。(在2.1节中简要讨论了端口号。它们将在第3章中详细介绍。)
我们通过一个简单的UDP应用程序和一个简单的TCP应用程序来介绍UDP和TCP套接字编程。我们介绍了Python3中简单的UDP和TCP应用程序。我们可以用Java、C或c++编写代码,但我们选择Python主要是因为Python清楚地暴露了套接字的关键概念。使用Python,代码行更少,并且每一行都可以轻松地向初学者解释。但是,如果您不熟悉Python,也没有必要感到害怕。如果您有Java、C或c++编程经验,您应该能够轻松地理解这些代码。
2.7.1 使用UDP的Socket编程
在本小节中,我们将使用UDP编写简单的客户端-服务器程序,在下一节中,我们将编写使用TCP的类似程序。
回顾2.1节,在不同机器上运行的进程通过将消息发送到套接字来相互通信。我们说过,每个进程类似于一个房子,而进程的套接字类似于一扇门。该应用程序位于房屋中门的一侧;传输层协议位于门的另一边,在外面的世界。应用程序开发人员可以控制套接字的应用层端的所有内容;然而,它对传输层几乎没有控制权。
现在让我们仔细看看两个使用UDP套接字的通信进程之间的交互。当使用UDP时,在发送进程将数据包推出套接字门之前,它必须先将目标地址附加到数据包上。当数据包通过发送方的套接字后,Internet将使用这个目标地址将数据包通过Internet路由到接收进程的套接字。当数据包到达接收套接字时,接收进程将通过套接字检索数据包,然后检查数据包的内容并采取相应的行动。
因此,您现在可能想知道,附加到数据包的目标地址中有什么内容?如您所料,目标主机的IP地址是目标地址的一部分。通过在数据包中包含目标IP地址,Internet中的路由器能够通过Internet将数据包路由到目标主机。但是,由于主机可能运行许多网络应用程序进程,每个进程都有一个或多个套接字,因此还需要在目标主机中识别特定的套接字。当一个套接字被创建时,一个被称为端口号(port number)的标识符被分配给它。因此,如您所料,数据包的目标地址也包括套接字的端口号。总之,发送进程给数据包附加一个目标地址,这个地址由目标主机的IP地址和目标套接字的端口号组成。此外,我们很快就会看到,发送方的源地址(由源主机的IP地址和源套接字的端口号组成)也被附加到数据包上。然而,将源地址附加到数据包通常不是由UDP应用程序代码完成的;相反,它由底层操作系统自动完成。
我们将使用以下简单的客户端-服务器应用程序来演示UDP和TCP的套接字编程:
- 客户端从其键盘中读取一行字符(数据),并将数据发送到服务器。
- 服务器接收数据并将其转换为大写字母。
- 服务器将修改后的数据发送给客户端。
- 客户端接收到修改后的数据,并在其屏幕上显示该行。
图2.27突出显示了通过UDP传输服务进行通信的客户端和服务器的主要socket相关活动。
现在让我们动手看看这个由UDP实现的客户端-服务器程序对。我们还在每个程序之后提供详细的逐行分析。我们将从UDP客户端开始,它将向服务器发送一个简单的应用程序级消息。为了使服务器能够接收和应答客户端的消息,它必须准备好并运行,也就是说,它必须在客户端发送消息之前作为一个进程运行。
客户端程序称为UDPClient.py,服务器端程序称为UDPServer.py。为了强调关键问题,我们有意提供最少的代码。好的代码当然会有更多的辅助行,特别是用于处理错误的情况。对于这个应用程序,我们任意选择12000作为服务器端口号。
UDPClient.py
下面是应用程序的客户端代码:
from socket import *
serverName = 'hostname'
serverPort = 12000
clientSocket = socket(AF_INET, SOCK_DGRAM)
message = input('Input lowercase sentence:')
clientSocket.sendto(message.encode(),(serverName, serverPort))
modifiedMessage, serverAddress = clientSocket.recvfrom(2048)
print(modifiedMessage.decode())
clientSocket.close()
现在让我们看一下UDPClient.py中的各种代码行:
from socket import *
socket模块构成了Python中所有网络通信的基础。通过包含这行代码,我们能够在程序中创建套接字。
serverName = 'hostname'
serverPort = 12000
第一行将变量serverName设置为字符串’hostname’。这里,我们提供了一个字符串,它包含服务器的IP地址(例如128.138.32.126)或者服务器的主机名(例如cis.poly.edu)。如果我们使用主机名,那么将自动执行DNS查询来获取IP地址。)第二行将整形变量serverPort设置为12000。
clientSocket = socket(AF_INET, SOCK_DGRAM)
这一行创建客户端的套接字,即clientSocket。第一个参数表示地址族;特别是,AF_INET表示底层网络使用IPv4。(现在不要担心这个问题,我们将在第4章讨论IPv4。)第二个参数表明套接字的类型是SOCK_DGRAM,这意味着它是UDP套接字(而不是TCP套接字)。注意,我们在创建客户端套接字时没有指定端口号;相反,我们让操作系统为我们做这些。现在已经创建了客户端进程的门,我们需要创建一条通过该门发送的消息。
message = input('Input lowercase sentence:')
input()是Python中的内置函数。当执行此命令时,客户端上的用户会收到“Input lowercase sentence:”的提示:然后用户使用键盘输入一行,该行被放入变量消息中。现在我们有了套接字和消息,我们希望通过套接字将消息发送到目标主机。
clientSocket.sendto(message.encode(),(serverName,serverPort))
在上一行中,我们首先将消息从字符串类型转换为字节类型,因为我们需要将字节发送到套接字;这是通过encode()方法完成的。sendto()方法将目标地址(serverName, serverPort)附加到消息上,并将结果数据包发送到进程的套接字clientSocket中。(如前所述,源地址也附加到数据包上,尽管这是自动完成的,而不是由代码显式完成的。)通过UDP套接字发送一个从客户端到服务器的消息就是这么简单!发送数据包后,客户端等待从服务器接收数据。
modifiedMessage, serverAddress = clientSocket.recvfrom(2048)
使用上面这行代码,当一个数据包从Internet到达客户端套接字时,数据包的数据被放入变量modifiedMessage中,数据包的源地址被放入变量serverAddress中。变量serverAddress包含服务器的IP地址和服务器的端口号。程序UDPClient实际上并不需要这个服务器地址信息,因为它从一开始就知道服务器地址;但这一行Python仍然提供了服务器地址。recvfrom方法也将缓冲区大小2048作为输入。(这个缓冲区大小适用于大多数用途。)
print(modifiedMessage.decode())
在将消息从字节转换为字符串后,这一行将在用户的显示器上打印出modifiedMessage。它应该是用户输入的原始行,但现在大写了。
clientSocket.close()
这一行关闭了套接字。然后进程终止。
UDPServer.py
现在让我们看看应用程序的服务器端代码:
# UDPServer.py
from socket import *
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_DGRAM)
serverSocket.bind(('', serverPort))
print("The server is ready to receive")
while True:
message, clientAddress = serverSocket.recvfrom(2048)
modifiedMessage = message.decode().upper()
serverSocket.sendto(modifiedMessage.encode(),clientAddress)
注意,UDPServer的开头类似于UDPClient。导入socket模块,还将整型变量serverPort设置为12000,并创建一个类型为SOCK_DGRAM (UDP套接字)的套接字。与UDPClient明显不同的第一行代码是:
serverSocket.bind(('', serverPort))
上面的行绑定(即分配)端口号12000到服务器的套接字。因此,在UDPServer中,代码(由应用程序开发人员编写)显式地将端口号分配给套接字。通过这种方式,当任何人向服务器IP地址的端口12000发送数据包时,该数据包将被定向到这个套接字。UDPServer然后进入一个while循环;while循环将允许UDPServer接收和处理来自客户端的数据包。在while循环中,UDPServer等待数据包到达。
message, clientAddress = serverSocket.recvfrom(2048)
这行代码类似于我们在UDPClient中看到的代码。当一个数据包到达服务器的套接字时,数据包的数据被放入变量message中,数据包的源地址被放入变量clientAddress中。变量clientAddress包含客户端的IP地址和客户端的端口号。在这里,UDPServer将利用这个地址信息,因为它提供了一个返回地址,类似于普通邮政邮件的返回地址。有了这个源地址信息,服务器现在知道它应该把它的回复指向哪里。
modifiedMessage = message.decode().upper()
这一行是我们简单应用程序的核心。它接受客户端发送的行,并在将消息转换为字符串后,使用upper()方法将其大写。
serverSocket.sendto(modifiedMessage.encode(), clientAddress)
最后一行将客户端地址(IP地址和端口号)附加到大写的消息(在将字符串转换为字节后),并将结果数据包发送到服务器的套接字。(如前所述,服务器地址也附加到数据包上,尽管这是自动完成的,而不是通过代码显式完成的。)然后,因特网将数据包发送到这个客户端地址。服务器发送数据包后,它仍然在while循环中,等待另一个UDP数据包到达(来自任何主机上运行的任何客户端)。
要测试这两个程序,您需要在一台主机上运行UDPServer,另一台主机上运行UDPClient.py。确保在UDPClient.py中包含正确的服务器主机名或IP地址。接下来,在服务器主机中执行编译后的服务器程序UDPServer.py。这将在服务器中创建一个进程,该进程一直处于空闲状态,直到某个客户端联系到它。然后在客户端中执行编译后的客户端程序UDPClient.py。这将在客户端中创建一个进程。最后,要在客户端上使用应用程序,需要输入一个句子,后面跟着一个回车符。
要开发自己的UDP客户端-服务器应用程序,可以从稍微修改客户端或服务器程序开始。例如,服务器可以计算字母s出现的次数并返回这个数字,而不是将所有的字母都转换成大写。或者,您可以修改客户端,以便在接收到大写的句子后,用户可以继续向服务器发送更多的句子。
2.7.2 使用TCP进行Socket编程
与UDP不同,TCP是一种面向连接的协议。这意味着在客户端和服务器可以开始互相发送数据之前,它们首先需要握手并建立TCP连接。TCP连接的一端连接到客户端套接字,另一端连接到服务器套接字。在创建TCP连接时,我们将客户端套接字地址(IP地址和端口号)和服务器套接字地址(IP地址和端口号)关联起来。TCP连接建立后,当一方希望向另一方发送数据时,它只需通过socket将数据放入TCP连接中。这与UDP不同,服务器必须在将数据包放入套接字之前将其连接到目标地址。
现在让我们仔细看看TCP中客户端和服务器程序的交互。客户端的工作是发起与服务器的联系。为了使服务器能够对客户端的初次接触做出反应,服务器必须做好准备。这意味着两件事。首先,与UDP的情况一样,在客户端尝试发起联系之前,TCP服务器必须作为一个进程运行。其次,服务器程序必须有一个特殊的门,更精确的说是一个特殊的套接字,用于欢迎运行在任意主机上的客户端进程的一些初始接触。用我们的房子/门来比喻一个进程/套接字,我们有时会把客户端的第一次接触称为敲门。
随着服务器进程的运行,客户端进程可以发起到服务器的TCP连接。这是通过在客户端程序中创建TCP套接字完成的。当客户端创建自己的TCP套接字时,它指定了服务器端套接字的地址,即服务器主机的IP地址和套接字的端口号。客户端创建套接字后,会发起三次握手,并与服务器建立TCP连接。在传输层中进行的三次握手对客户端和服务器程序是完全不可见的。
在三次握手过程中,客户端进程敲服务器进程的欢迎门(welcoming door)。当服务器听到敲门声时,它会创建一个新门,准确的说是一个专门为特定客户端创建的新套接字。在下面的例子中,欢迎门是一个TCP套接字对象,我们称之为serverSocket;专用于客户端建立连接的新创建的套接字称为connectionSocket。第一次遇到TCP套接字的学生有时会混淆欢迎套接字(它是所有想要与服务器通信的客户端的初始接触点),以及随后为与每个客户端通信而创建的服务器端连接套接字。
从应用程序的角度来看,客户端的套接字和服务器的连接套接字是通过管道直接连接的。如图2.28所示,客户端进程可以向它的套接字发送任意字节,TCP保证服务端进程将按照发送顺序(通过连接套接字)接收到每个字节。因此,TCP在客户端和服务器进程之间提供了可靠的服务。此外,正如人们可以进出同一扇门,客户端进程不仅发送字节,也从它的套接字接收字节;类似地,服务器进程不仅接收字节,而且将字节发送到它的连接套接字中。
我们使用同一个简单的客户端-服务器应用程序来演示使用TCP进行套接字编程:客户端向服务器发送一行数据,服务器将这行代码大写,然后将其发送回客户端。图2.29突出显示了通过TCP传输服务进行通信的客户端和服务器的主要套接字相关活动。
TCPClient.py
下面是应用程序的客户端代码:
from socket import *
serverName = 'servername'
serverPort = 12000
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((serverName,serverPort))
sentence = input('Input lowercase sentence:')
clientSocket.send(sentence.encode())
modifiedSentence = clientSocket.recv(1024)
print('From Server: ', modifiedSentence.decode())
clientSocket.close()
现在让我们看看代码中的不同行,它们与UDP实现有很大的不同。第一行是创建客户端套接字。
clientSocket = socket(AF_INET, SOCK_STREAM)
这一行创建客户端的套接字,称为clientSocket。第一个参数再次表示底层网络使用IPv4。第二个参数表示socket类型为SOCK_STREAM,这意味着它是一个TCP套接字(而不是UDP套接字)。请注意,我们在创建客户端套接字时并没有指定端口号;相反,我们让操作系统为我们做这些。现在,下一行代码与我们在UDPClient中看到的非常不同:
clientSocket.connect((serverName,serverPort))
回想一下,在客户端可以使用TCP套接字向服务器发送数据(或反之亦然)之前,必须首先在客户端和服务器之间建立TCP连接。上面的代码行启动客户端和服务器之间的TCP连接。connect()方法的参数是连接的服务器端地址。在这行代码执行之后,将执行三次握手,并在客户端和服务器之间建立TCP连接。
sentence = input(’Input lowercase sentence:’)
与UDPClient一样,上面的sentence从用户处获得一个句子。字符串语句继续收集字符,直到用户输入回车符结束行。下一行代码也与UDPClient非常不同:
clientSocket.send(sentence.encode())
上面这行代码通过客户端套接字发送sentence到TCP连接。请注意,这个程序并不像UDP套接字那样显式地创建一个数据包并将目标地址附加到数据包上。客户端程序只是将sentence字符串中的字节放入TCP连接中。然后客户端等待从服务器接收字节。
modifiedSentence = clientSocket.recv(2048)
当字符从服务器回来时,它们被放置到字符串modifiedsentence中。字符在modifiedSentence中继续累积,直到行以回车字符结束。打印大写的句子之后,我们关闭客户端的套接字:
clientSocket.close()
最后一行关闭套接字,因此关闭客户端和服务器之间的TCP连接。它导致客户端中的TCP向服务器中的TCP发送一条TCP消息(请参见3.5节)。
TCPServer.py
现在让我们看看服务器程序。
from socket import *
serverPort = 12000
serverSocket = socket(AF_INET,SOCK_STREAM)
serverSocket.bind(('',serverPort))
serverSocket.listen(1)
print('The server is ready to receive')
while True:
connectionSocket, addr = serverSocket.accept()
sentence = connectionSocket.recv(1024).decode()
capitalizedSentence = sentence.upper()
connectionSocket.send(capitalizedSentence.encode())
connectionSocket.close()
现在让我们看一下与UDPServer和TCPClient有明显区别的行。与TCPClient一样,服务器创建一个TCP套接字:
serverSocket=socket(AF_INET,SOCK_STREAM)
与UDPServer类似,我们将服务器端口号serverPort与这个套接字相关联:
serverSocket.bind(('',serverPort))
但是对于TCP, serverSocket将是我们的欢迎套接字。在建立了这扇欢迎的门之后,我们会等待并倾听一些客户来敲门:
serverSocket.listen(1)
这一行让服务器侦听来自客户端的TCP连接请求。该参数指定排队连接的最大数量(至少1个)。
connectionSocket, addr = serverSocket.accept()
当客户端敲这个门时,程序调用serverSocket的accept()方法,该方法在服务器中创建一个名为connectionSocket的新套接字,专用于这个特定的客户端。然后客户端和服务器完成握手,在客户端的clientSocket和服务器的connectionSocket之间创建一个TCP连接。TCP连接建立后,客户端和服务器现在可以通过连接向彼此发送字节。使用TCP,从一端发送的所有字节保证到达另一端,而且保证按顺序到达。
connectionSocket.close()
在这个程序中,在将修改后的语句发送给客户端之后,我们关闭连接套接字。但是由于serverSocket保持打开状态,现在另一个客户端可以敲门并向服务器发送一个句子来修改。
这就完成了我们对TCP套接字编程的讨论。我们鼓励您在两个独立的主机上运行这两个程序,并对它们进行修改,以实现略微不同的目标。您应该比较UDP程序对和TCP程序对,看看它们有什么不同。您还应该完成第二章、第四章和第九章末尾描述的许多套接字编程任务。最后,我们希望有一天,在掌握了这些和更高级的套接字程序之后,您将编写自己的流行网络应用程序,变得非常丰富和有名,并记住这本教科书的作者(哈哈哈,一定做到!)。
2.8 总结
在本章中,我们研究了网络应用程序的概念和实现方面。我们已经了解了许多Internet应用程序所采用的普遍存在的客户端-服务器架构,并看到了它在HTTP、SMTP和DNS协议中的使用。我们详细地研究了这些重要的应用程序级协议,以及它们相应的相关应用程序(Web、文件传输、电子邮件和DNS)。我们已经了解了P2P架构,并将其与客户端-服务器架构进行了对比。我们还了解了流媒体视频,以及现代视频分发系统如何利用CDN。我们已经研究了如何使用套接字API构建网络应用程序。我们已经介绍了面向连接(TCP)和无连接(UDP)端到端传输服务的套接字的使用。我们沿着分层网络架构的旅程的第一步现在已经完成。
在这本书的一开始,在1.1节中,我们给一个相当模糊,贫乏的一个协议的定义:两个或两个以上的通信实体之间交换消息的格式和顺序,以及传输和/或接收消息或其他事件所采取的行动。本章的内容,特别是我们对HTTP、SMTP和DNS协议的详细研究,已经为这个定义增加了相当多的内容。协议是网络中的一个关键概念;我们对应用协议的研究现在使我们有机会更直观地了解协议是关于什么的。
在2.1节中,我们描述了TCP和UDP提供给调用它们的应用程序的服务模型。在第2.7节中,当我们开发运行在TCP和UDP上的简单应用程序时,我们更仔细地研究了这些服务模型。然而,我们很少谈到TCP和UDP如何提供这些服务模型。例如,我们知道TCP提供可靠的数据服务,但我们还没有说它是如何做到的。在下一章中,我们将不仅仔细研究传输协议的内容,而且还将研究传输协议的方式和原因。
有了关于Internet应用程序结构和应用层协议的知识,我们现在准备在第3章中进一步深入了解协议栈并检查传输层。
作业
之后补充