应用层
本章主要介绍应用程序所需的网络服务、客户和服务器、进程和运输层接口。其中会涉及到的几种网络应用程序,包括Web、DNS、对等文件分发和视频流。
一、应用层协议
研发网络应用程序的核心是写出能够运行在不同端系统和通过网络彼此通信的程序。
1.1 网络应用程序体系结构
应用程序体系结构不同于网络的体系结构:对于应用研发者来说,网络体系结构是固定的,它为应用程序提供特定的服务集,而应用程序体系结构(application architecture)规定了如何在各种端系统上组织该应用程序。目前所使用的两种主流体系结构分别是:客户-服务器体系结构和对等(P2P)体系结构。
1.1.1 客户-服务器体系结构
在客户-服务器体系结构(client-server architecture)中,有一个总是打开的主机称为服务器,它服务来自许多其他称为客户的主机的请求。在该体系结构中,客户直接是不直接通信的;服务器具有固定的、周知的地址(IP地址)。常用的Web、FTP、Telnet和电子邮件都具有该体系结构。
在一个客户-服务器体系结构应用中,常会出现一台服务器主机跟不上它所有客户的请求。为此,就需要配备大量主机的数据中心(data center),它被用于创建强大的虚拟服务器,来处理来自客户的请求。
1.1.2 P2P体系结构
在一个P2P体系结构(P2P architecture)中,对位于数据中心的专用服务器有最小的(或没有)依赖。应用程序在间断连接的主机对之间使用直接通信,这些主机对被称为对等方。目前流行的、流量密集型应用都是P2P体系结构,如文件共享(BitTorrent)、对等方协助下载加速器(迅雷)、因特网电话和视频会议。也有一些应用采用了混合的体系结构,如对即时讯息应用,服务器被用于跟踪用户的IP地址,但用户到用户的报文在用户主机之间直接发送。
1.2 进程通信
运行在多个端系统上的程序是如何互相通信的呢?用操作系统术语来讲,进行通信的实际上是进程(process)而不是程序。一个进程可以被认为是运行在端系统中的一个程序。当多个进程运行在相同的端系统上时,他们使用进程间通信机制相互通信。
在两个不同端系统上的进程,通过跨越计算机网络交换报文(message)而相互通信。发送进程生成并向网络中发送报文;接收进程接收这些报文并可能通过回送报文进行响应。
1.2.1 客户和服务器进程
网络应用程序由成对的进程组成,这些进程通过网络相互发送报文。对每对通信进程,通常将这两个进程之一标识为客户(client),另一个进程标识为服务器(server)。对Web而言,浏览器是客户进程,Web服务器是服务器进程。对P2P文件共享,下载文件的对等方标识为客户,上载文件的对等方标识为服务器,当然在P2P系统中,一个进程既可以上载文件,又可以下载文件,但仍可以进行这样的标识。
我们对客户和服务器进程进行定义:在一对进程之间的通信会话场景中,发起通信(即在该会话开始时发起与其他进程的联系)的进程被标识为客户,在会话开始时等待联系的进程是服务器。
1.2.2 进程与计算机网络之间的接口
进程通过一个称为套接字(socket)的软件接口向网络发送报文和从网络接收报文。这里我们可以把进程类比为一座房子,而套接字相当于门,当一个进程向另一个主机的另一个进程发送报文时,它把报文推出“门”,门外有基础运输设施将报文运送到目的进程“门口”,它通过接收进程的“门”接收该报文。
图1.1 应用进程、套接字和下面的运输层协议
图1.1显示了两个经过因特网通信的进程之间的套接字通信,套接字是同一台主机内应用层与运输层之间的接口。由于该套接字是建立网络应用程序的可编程接口,因此套接字也称为应用程序和网络之间的应用程序编程接口(Application Programming Interface,API)。应用程序开发者可以控制套接字在应用层端的一切,但是对该套接字的运输层端几乎没有控制权。应用程序的开发者对于运输层的控制仅限于:选择运输层协议;设定几个运输层参数。
1.2.3 进程寻址
一个进程向运行在另一台主机上的进程发送分组,接受进程需要有一个分组,该分组需要包括:主机的地址;在目的主机中指定接收进程的标识符。因特网中,主机由IP地址(IP address)标识,但一台主机上运行着许多网络应用,端口号(port number)则用来标识不同进程。
1.3 可供应用程序使用的运输服务
一个运输层协议能够为调用它的应用程序提供什么样的服务,我们大致能从四个方面对应用程序服务要求进行分类:可靠数据传输、吞吐量、定时和安全性。
1.3.1 可靠数据传输
分组在计算机网络中可能丢失,如果一个协议能确保由应用程序的一端发送的数据正确、完全地交付给该应用程序的另一端,就认为提供了可靠数据传输(reliable data transfer)。当一个运输层协议提供这种服务时,发送进程只要将其数据传递进套接字,就可以完全相信该数据将能无差错地到达接受进程。如果运输层协议不提供可靠数据传输,发送进程发送的某些数据可能到达不了接收进程,但如多媒体一类的容忍丢失的应用(loss-tolerant application)是可以承受一定量的数据丢失。
1.3.2 吞吐量
由于网络带宽是共享的,随着不同会话的到达和离开,可用吞吐量将会随着时间波动。像一些对吞吐量有要求的带宽敏感的应用(bandwidth-sensitive application),就需要运输层协议能够以某种特定的速率提供确保的可用吞吐量,如许多的多媒体应用,不过某些多媒体应用可能采用自适应编码技术对语音视频以当前可用带宽相匹配的速率进行编码。但弹性应用(elastic application)能够根据当时可用的带宽或多或少地利用可使用的吞吐量,如电子邮件、文件传输以及Web传送。
1.3.3 定时
运输层协议也能提供定时保证,这种保证能够以多种形式实现。例如,发送方注入进套接字中的每个比特到达接收方的套接字不迟于100ms。
1.3.4 安全
运输协议能够为应用程序提供一种或多种安全性服务,例如在发送主机中,运输协议能够加密由发送进程传输的所有数据,在接收主机中,运输层协议能够在将数据交付给接收进程之前解密数据。
1.4 因特网提供的运输服务
因特网为应用程序提供两个运输层协议,即UDP和TCP,每个协议为调用它们的应用程序提供不同的服务集合。
图1.2 选择的网络应用的要求
1.4.1 TCP服务
TCP服务模型包括面向连接服务和可靠数据传输服务。当某个应用程序调用TCP作为其运输协议时,该应用就能获得来自TCP的这两种服务。
- 面向连接的服务:在应用层数据报文开始流动之前,TCP让客户和服务器互相交换运输层控制信息。这个所谓的握手过程提醒客户和服务器,让它们为大量分组的到来做好准备。在握手阶段后,一个TCP连接就在两个进程的套接字之间建立了,这条连接是全双工的,即连接双方的进程可以在此连接上同时进行报文收发。当应用程序结束报文发送时,必须拆除该连接。
- 可靠的数据传送服务:通信进程能够依靠TCP,无差错、按适当顺序交付所有发送的数据。当应用程序的一端将字节流传进套接字时,它能依靠TCP将相同的字节流交付给接收方的套接字,而没有字节的丢失和冗余。
TCP还拥有拥塞控制机制。当发送方和接收方之间的网络出现拥塞时,TCP的拥塞控制机制会抑制发送进程(客户或服务器),TCP拥塞控制也试图限制每个TCP连接,使它们达到公平共享网络带宽的目的。
TCP安全问题:无论是TCP还是UDP都没有提供任何加密机制,这就是说发送进程传进其套接字的数据,与经网络传送到目的进程的数据相同,为此因特网界研制了TCP的加强版本,称为安全套接字(Secure Sockets Layer,SSL)。用SSL加强版本后的TCP不仅能够做传统的TCP所能做的一切,而且提供了关键的进程到进程的安全性服务,包括加密、数据完整性和端点鉴别。SSL不是第三种运输协议,而是对TCP的加强,特别是,如果一个应用程序要使用SSL的服务,它需要在该应用程序的客户端和服务器端包括SSL代码(利用现有的、高度优化的类和库)。SSL有它自己的套接字API,这类似于传统的TCP套接字API。当一个应用使用SSL时,发送进程向SSL套接字传递明文数据;在发送主机中的SSL则加密该数据并将加密的数据传递给TCP套接字。加密的数据经由因特网到达接受进程的TCP套接字,该接收套接字将加密数据传递给SSL进行解密。
1.4.2 UDP服务
UDP是一种不提供不必要服务的轻量级运输协议,它仅提供最小服务。UDP是无连接的,因此在两个进程通信前没有握手过程。UDP协议提供一种不可靠数据传输服务,即当进程将一个报文发送进UDP套接字时,UDP协议并不能保证该报文将到达接受进程,不仅如此,到达接收进程的报文也可能是乱序到达的。
UDP没有拥塞控制机制,所以UDP的发送端可以用它选定的任何速率向其下层注入数据,不过由于中间链路的带宽受限或拥塞问题,实际端到端吞吐量可能小于该速率。
1.4.3 因特网运输协议所不提供的服务
在4个方面组织的运输层协议服务:可靠数据传输、吞吐量、定时和安全性,其中TCP提供了可靠的端到端数据传输,并且通过SSL加强提供安全服务,但协议中并没有提供吞吐量或定时保证的讨论。不过现在的因特网通常能够为时间敏感一类的应用提供满意的服务,但它不能提供任何定时或带宽的保证,不过它们已经被设计成尽最大可能对付这种保证的缺乏。
图1.3 流行的因特网应用及其应用协议和支撑的运输协议
1.5 应用层协议
网络进程间的通信是通过把报文发送进套接字实现的,不过报文是如何构造的,报文中各字段是什么含义,进程何时发送报文?应用层协议(application-layer protocal)定义了运行在不同端系统上的应用程序进程是如何相互传递报文。特别是应用层协议定义了:
- 交换的报文类型,例如请求报文和相应报文
- 各种报文类型的语法,如报文中的各个字段及这些字段是如何描述
- 字段的语义,即这些字段中的信息的含义
- 确定一个进程何时以及如何发送报文,对报文进行相应的规则
有些应用层协议是由RFC文档定义的,因此它们位于公共域中,如Web的应用层协议HTTP,如果浏览器开发者遵从HTTP RFC规划,所开发出的浏览器就能访问任何遵从该文档标准的Web服务器。不过还有很多应用层协议是专用的,有意不为公共域使用。应用层协议只是网络应用的一部分。
二、Web和HTTP
2.1 HTTP概况
Web的应用层协议是超文本传输协议(HyperText Transfer Protocal,HTTP),它是Web的核心。HTTP由两个程序实现:一个客户程序和一个服务器程序。客户程序和服务器程序运行在不同端系统中,通过交换HTTP报文进行会话。HTTP定义了这些报文的结构以及客户和服务器进行报文交换的方式。
Web页面(Web page)(也叫文档)是由对象组成。一个对象(object)只是一个文件,诸如HTML文件、一个JPEG图形、一个Java小程序或一个视频片段这样的文件,且它们可通过一个URL地址寻址。多数的Web页面含有一个HTML基本文件(base HTML file)以及几个引用对象。如一个Web页面包含HTML文本和5个JPEG图形,那么这个页面包括6个对象。HTML基本文件通过对象的URL地址引用页面中的其他对象,每个URL地址由两部分组成:存放对象的服务器主机名和对象的路径名,如http://www.xaut.edu/somefiles/picture.png,其中www.xaut.edu就是主机名,/somefiles/picture.png就是路径名。Web浏览器实现了HTTP客户端,Web服务器实现了HTTP服务器端,它由于存储Web对象,每个对象由URL寻址。
HTTP使用TCP作为它的支撑运输协议,因此一个客户进程发出的每个HTTP请求报文最终能完整到达服务器,服务器发出的每个响应报文能完整到达客户。
一个很重要现象:服务器向客户发送被请求的文件,而不存储任何关于该客户的状态信息。例如某特定客户在很短一段时间内两次请求同一个对象,服务器并不会因为刚刚为该客户提供了对象就不再做出反应,而是重新发送该对象。因为服务器不会保存关于客户的任何信息,所以HTTP是一个无状态协议(stateless protocal)。
2.2 非持续连接和持续连接
在许多因特网应用程序中,客户和服务器在相当长的时间范围内通信,其中客户发出一系列请求并且服务器对每个请求进行响应。依据应用功能程序以及该应用程序的使用方式,这一系列请求可以以规则的间隔周期性地或间断性地一个接一个发出。当交互是经TCP进行的,就需要决定每个请求/响应对是经一个单独的TCP连接发送,还是所有的请求及其响应经相同的TCP连接发出。采用前一种方法,该应用程序被称为使用非持续连接(non-persistent connection);采用后一种方法,该应用程序被称为使用持续连接(persistent connection)。两种连接方式HTTP都能使用,默认状态下是持续连接。
2.2.1 采用非持续连接的HTTP
在非持续连接情况下,从服务器向客户传送一个Web页面的步骤。假设该页面含有一个HTML基本文件和10个JPEG图形,并且这11个对象位于同一台服务器,该HTML文件URL为:http://www.xaut.edu/somefiles/welcome.index。整个过程如下:
- HTTP客户进程在端口号80发起一个到服务器www.xaut.edu的TCP连接,该端口号是HTTP的默认端口。
- HTTP客户经它的套接字向该服务器发送一个HTTP请求报文,请求报文中包含了路径名/somefiles/welcome.index。
- HTTP服务器经它的套接字接收该请求报文,从其存储器中检索出welcome.index对象,在一个HTTP相应报文中封装对象,并通过其套接字向客户发送响应报文。
- HTTP服务器进程通知TCP断开该连接,但实际直到TCP确认客户已经完整收到响应报文为止,它才会实际断开。
- HTTP客户接收到响应报文,TCP连接关闭。该报文指出封装的对象是一个HTML文件,客户从响应报文中提取出该文件,检查该HTML文件,得到对10个JPEG图形到的引用。
- 对每个引用的JPEG图形对象重复前4个步骤。
上面步骤,其中每个TCP连接在服务器发送一个对象后关闭,即该连接并不为其他对象而持续下来。需要注意的是每个TCP连接只传输一个请求报文和响应报文,因此本例共产生了11个TCP连接。
本例中没有明确客户获取10个JPEG图形是使用10个串行的TCP连接,还是某些JPEG对象使用一些并行的TCP连接。事实上用户能够配置现代浏览器控制连接的并行度,在默认方式下,大部分浏览器打开5~10个并行的TCP连接,而每条连接处理一个请求响应事务。
2.2.2 采用持续连接的HTTP
非持续连接的缺点:必须为每一个请求的对象建立和维护一个全新的连接。对于每个这样的连接,在客户和服务器中都要分配TCP的缓冲区和保持TCP变量,给Web服务器带来了严重的负担;每个对象经受两倍往返时间(Round-Trip Time,RTT)(三次握手+一次请求和响应传输)的交付时延。
采用持续连接,服务器在发送响应后保持该TCP连接打开。在相同的客户与服务器你之间,后续的请求和响应报文能够通过相同的连接传送。特别是一个完整的Web页面(上例HTML文件加上10个JPEG图形)可以用单个持续TCP连接进行传送。更有甚者,位于同一台服务器的多个Web页面在从该服务器发送给同一客户时,可以在单个持续TCP连接上进行。一般来说如果一条连接经过一定时间间隔仍未被使用,HTTP服务器就关闭该连接。
2.3 HTTP报文格式
HTTP报文有两种:请求报文和响应报文。
2.3.1 HTTP请求报文
一个典型的请求报文:
1 GET /somedir/page.html HTTP/1.1 2 Host: www.xaut.edu 3 Connection: close 4 User-agent: Mozilla/5.0 5 Accept-language:fr
该报文是用ASCII文本编写的,一共有5行,每行由一个回车和换行符结束。最后一行后再附加上一个回车换行符。事实上一个请求报文能够具有更多的行或者至少为一行。HTTP请求报文的第一行叫做请求行(Request line),其后继的行叫做首部行(header line)。
请求行一共有3个字段:方法字段、URL字段和HTTP版本字段。方法字段可以取几种不同的值,包括GET、POST、HEAD、PUT、和DELETE等。绝大部分的HTTP请求报文使用GET方法。当浏览器请求一个对象时,使用GET方法,在URL字段带有请求对象的标识。最后版本字段是自解释的,这个例子中浏览器实现的是HTTP/1.1版本。
首部行Host: www.xaut.edu 指明了对象所在的主机。事实上该主机中已经有一条TCP连接存在,该首部行看起来是不必要的,但该首部行提供的信息是Web代理高速缓存所要求的。通过包含Connection: close首部行,该浏览器告诉服务器不要麻烦地使用持续连接,它要求服务器在发送完被请求的对象后就关闭这条连接。User-agent: 首部行用来指明用户代理,即向服务器发送请求的浏览器的类型,这里的Mozilla/5.0,即FireFox浏览器。这个首部行让服务器可以有效地为不同类型用户代理实际发送相同对象的不同版本。Accept-language:fr首部行表示用户想得到该对象的法语版本(如果服务器中有的话);否则服务器应当发送它的默认版本。Accept-language: 首部行是HTTP中可用的众多内容协商首部之一。
结合上例,我们看如图2.1一个请求报文的通用格式。在首部行后的附加的回车和换行后面有一个“实体体”,也就是请求数据,使用GET方法时实体体为空,使用POST方法时才有该实体体。与请求数据相关的最常使用的请求头是Content-Type和Content-Length。当用户提交表单时,HTTP客户端常常使用POST方法,例如用户向搜索引擎提供搜索关键词时。使用POST报文时,用户仍可以向服务器请求一个Web页面,但Web页面的特定内容依赖于用户在表单字段中输入的内容。如果方法字段的值为POST时,则实体体中包含的就是用户在表单字段中的输入值。
图2.1 一个HTTP请求报文的通用格式
用表单生成的请求报文不是必须使用POST方法,HTML表单经常使用GET方法,并在所请求的URL中包括输入的数据。
HEAD方法类似于GET方法。当服务器收到一个使用HEAD方法的请求时,将会用一个HTTP报文进行响应,但是并不返回请求对象。应用程序开发者常用HEAD方法进行调试跟踪。PUT方法常与Web发行工具联合使用,它允许用户上传对象到指定的Web服务器上指定的路径。DELETE方法允许用户或者应用程序删除Web服务器上的对象。
比较GET和POST:
1 <form action="${pageContext.request.contextPath }/myServlet?name=Aidan" method="GET"> 2 <input type="text" name="password"/> 3 <input type="submit" value="提交"/> 4 </form>
这样一个表单,我们第一次使用GET方法提交,地址栏从C:/Users/test.html变成C:/Users/$%7BpageContext.request.contextPath%20%7D/myServlet?password=123。我们再使用POST提交一次,地址栏是C:/Users/$%7BpageContext.request.contextPath%20%7D/myServlet?name=Aidan。
这里我们注意两个问题:一是,一个GET方法提交时,是在URL中包括提交的数据,因此在URL中可以直接看到,因此安全性不高而且有长度限制为1024字节;而POST方法是将提交的数据放在请求数据(实体体)中,提交时看不到。此情况两种报文如下:(另外对这两种方式,服务端的获取也不一样)
GET请求报文:
1 GET /myServlet?password=123 HTTP/1.1 2 Accept: */* 3 Accept-Language: zh-cn 4 host: localhost 5 6 Content-Type: application/x-www-form-urlencoded 7 Content-Length: 12 8 Connection:close
POST请求报文:
1 POST /myServlet?name=Aidan HTTP/1.1 2 Accept: */* 3 Accept-Language: zh-cn 4 host: localhost 5 6 Content-Type: application/x-www-form-urlencoded 7 Content-Length: 12 8 Connection:close 9 password=123
另一个有趣的问题,当用GET提交时,提交的内容是直接附在?后面的,因此表单使用GET提交时,myServlet?name=Aidan中?后面的内容直接被覆盖了,而POST没有。
2.3.2 HTTP响应报文
一条典型的HTTP响应报文:
1 HTTP/1.1 200 ok 2 Connection: close 3 Date: Tue,18, Aug 2018 15:22:09 GMT 4 Server: Apache/2.2.3 (CentOS) 5 Last-Modified: Tue,18, Aug 2018 15:22:18 GMT 6 Content-Length: 6821 7 Content-Type: text/html 8 (data…………)
这条响应报文有三部分:一个初始状态行(status line),6个首部行(header line),然后是实体类(entity body)。实体部分是报文的主要部分,即它包含了所请求的对象本身(报文中data……部分)。状态行有3个字段:协议版本字段、状态码和相应状态信息。
首部行中,服务器用Connection: close首部行告诉客户,发送完报文后将关闭该TCP连接。Date: 首部行指示服务器产生并发送该响应报文的日期和时间。这个时间不是指对象创建或者最后修改的时间,而是服务器从它的文件系统中检索到该对象,将对象插入响应报文,并发送该响应报文的时间。Server: 首部行指示该报文是由一台Apache Web服务器产生的,它类似于请求报文中的User-agent: 首部行。Last-Modified: 首部行指示了对象创建或者最后修改的日期和时间。它对既可能在本地客户也可能在网络缓存服务器上的对象来说非常重要。Content-Length: 首部行指示了被发送对象中的字节数。Content-Type: 首部行指示了实体体中的对象是HTML文本(该对象类型应该正式地由Content-Type: 指示,而不是用文件扩展名来指示)。
如图2.2所示,是一个响应报文的通用格式,它与上面例子相匹配。
图2.2 一个HTTP响应报文的通用格式
一些常见的状态码和相关短语含义:
- 200 OK:请求成功,信息在返回的响应报文中。
- 301 Moved Permanently:请求的对象已经被永久转移了,新的URL定义在响应报文的Location:首部行中。客户软件将自动获取新的URL。
- 400 Bad Request:一个通用差错代码,只是该请求不能被服务器理解。
- 404 Not Found:被请求的文档不在服务器上。
- 505 HTTP Version Not Supported:服务器不支持请求报文所使用的的HTTP协议版本。
HTTP规范中有很多首部行,浏览器产生的首部行和很多因素有关。
2.4 用户与服务器的交互:cookie
HTTP服务器是无状态的,如果Web站点希望识别用户,就需要使用cookie。cookie允许站点对用户进行跟踪。如图2.3,cookie技术有4个组件:
- 在HTTP响应报文中的一个cookie首部行
- 在HTTP请求报文中的一个cookie首部行
- 在用户端系统中保留有一个cookie文件,并由用户的浏览器进行管理
- 位于Web站点的一个后端数据库
图2.3 用cookie跟踪用户状态
cookie工作过程:
假设用户A总是在同一台PC上使用Chrome上网,她首次与Amazon.com联系,当请求报文到达该Amazon Web服务器时,该Web站点将产生一个唯一识别码,并以此作为索引在它的后端数据库产生一个表项。接下来Amazon Web服务器用一个包含Set-cookie:首部的HTTP响应报文对A的浏览器进行响应,其中Set-cookie:首部含有该识别码(如首部行可能是Set-cookie: 1278)。
当A的浏览器收到了该HTTP响应报文时,它会看到该Set-cookie: 首部。该浏览器在它管理的特定cookie文件中添加一行,该行包含服务器的主机名和Set-cookie: 首部中的识别码。当A继续浏览Amazon网站时,每次请求一个Web页面,其浏览器就会查询该cookie文件并抽取她对这个网站的识别码,并放到HTTP请求报文中包括识别码的cookie首部行中,特别是发往Amazon服务器的每个HTTP请求报文都包括首部行Cookie: 1278。
此方式下,Amazon服务器虽然不知道A的确切身份,但可以跟踪A在其站点的活动,确切知道用户1278以什么顺序、什么时间访问地哪些页面。以此,Amazon能够利用它对A进行定向的网页推送。如果A曾经在Amazon站点也注册过,那么Amazon能将A的身份信息与识别码相关联。这就是一些电子商务网站实现“点击购物”的道理。
上述过程说明,cookie可以用于标识一个用户。用户首次访问一个站点时,可能需要提供一个用户标识,在后续会话中,浏览器向服务器传递一个cookie首部,从而向该服务器标识了用户。因此cookie可以在无状态的HTTP上建立一个用户会话层。
2.5 Web缓存
Web缓存器(Web cache)也叫代理服务器(proxy server),它是能够代表初始Web服务器来满足HTTP请求的网络实体。Web缓存器有自己的磁盘存储空间,并在存储空间中保存最近请求过的对象的副本。
图2.4 客户通过Web缓存器请求对象
如图2.4,可以配置浏览器,使得用户的所有HTTP请求首先指向Web缓存器。如果浏览器被配置,假设浏览器正在请求:http://www.xaut.edu/picture.png,则会发生:
- 浏览器创建一个到Web缓存器的TCP连接,并向Web缓存器中对象发送一个HTTP请求。
- Web缓存器进行检查,看看本地是否存储了该对象的副本。有,则用响应报文返回该对象。
- 如果缓存器中没有,它就打开一个与该对象的初始服务器的TCP连接,发送一个对该对象的HTTP请求。在收到该请求后,初始服务器返回具有该对象的响应。
- Web缓存器收到该对象时,它在本地存储一份副本,并向客户发送含有该副本的响应(通过现有的浏览器到缓存器之间的TCP连接)。
Web缓存器既是服务器又是客户,通常由ISP购买并安装。部署Web缓存器有两个原因,一是可以大大减少对客户请求的响应时间,特别是在初始服务器和客户之间的瓶颈带宽远低于缓存器与客户的瓶颈带宽时,通常客户和Web缓存器之间是一个高速连接。二是减少了一个机构的接入链路到因特网的通信量,用户可以直接在缓存器中获取,不必接入因特网,这样就能暂时不用对接入因特网链路增加带宽,降低了费用。
2.6 条件GET方法
高速缓存能减少用户感受到的响应时间,但是在缓存存放的对象副本可能是陈旧的,即该副本缓存后可能就在服务器上被修改了。不过HTTP协议有一种机制,允许缓存器证实它的对象是最新的,即条件GET(conditional GET)方法。如果:请求报文使用GET方法;并且请求报文中包含一个“If-Modified-Since:”首部行。那么这个HTTP请求报文就是一个条件GET请求报文。
例如,一个代理缓存器向Web服务器发送一条请求报文,服务器收到报文并发送具有请求的对象响应报文,缓存器在本地缓存该对象,同时将对象转发给用户。缓存在在缓存该对象时,也存储了最后修改时间。
一段时间后,另一个用户经由该缓存器请求同一个对象,该对象仍在该缓存器上,不过对象可能已经被修改过,因此缓存器通过发送一个条件GET执行最新检查。
1 GET /picture.png HTTP/1.1 2 Host: www.xaut.edu 3 If-Modified-Since: wed, 9 oct 2018 09:16:54
这里If-Modified-Since行时间正是上一次缓存响应报文中的Last-Modified:首部行的值。该条件GET告诉服务器,如果在该日期后对象被修改过,才发送一个具有该对象最新值的响应报文。如果没有被修改过,Web服务器会发送一个空实体体的响应报文,如下:
1 HTTP/1.1 304 Not Modified 2 Date:wed, 9 oct 2018 09:16:54 3 Server: Apache/1.3.0 (Unix) 4 (empty entity body)
状态行中304 Not Modified告诉缓存器可以使用该对象,可以将此副本转发给浏览器。
三、DNS:因特网的目录服务
主机的标识方法有多种,其中一种是用主机名(hostname),如www.xaut.org,此种方式便于记忆,但并没有提供关于主机在因特网中的位置信息,并且主机名由不定长的字母数字组成,路由器难以处理。因此,主机也可以使用IP地址(IP address)进行标识。
3.1 DNS提供的服务
域名系统(Domain Name System,DNS)的主要任务是提供一种能进行主机名到IP地址转换的目录服务。DNS是:一个由分层的DNS服务器(DNS server)实现的分布式数据库;一个使得主机能够查询分布式数据库的应用层协议。DNS服务器通常运行在BIND软件的UNIX机器,DNS协议运行在UDP之上,使用53号端口。
DNS是应用层协议,其原因在于:使用客户-服务器模式运行在通信的端系统之间;在通信的端系统之间通过下面的端到端运输协议来传送DNS报文。不过DNS不是一个直接和用户打交道的应用,它是为用户应用程序和其他软件提供一种目录服务。
DNS通常是由其他应用层协议所使用的,包括HTTP、SMTP和FTP,将用户提供的主机名解析为IP地址。例如,用户主机上的一个浏览器请求www.xaut.edu/index.html页面,为了使用户主机能找到Web服务器www.xaut.edu,用户主机需要获取到服务器IP地址,其做法是:
- 同一台用户主机上运行着DNS应用的客户端。
- 浏览器从上述URL中抽取出主机名www.xaut.edu,并将这台主机名传给DNS应用的客户端。
- DNS客户向DNS服务器发送一个包含主机名的请求。
- DNS客户最终会收到一份回答报文,其中含有对应于该主机名的IP地址。
- 一旦浏览器接收到来自DNS的该IP地址,它能够向位于该IP地址80端口的HTTP服务器进程发起一个TCP连接。
从上例可以看到使用DNS会带来时延,不过一般情况,想获得的IP地址通常就缓存在一个“附近的”DNS服务器中。除了提供主机名到IP地址的转换服务,DNS还提供其他的一些服务:
- 主机别名(host aliasing):有着复杂主机名的主机能拥有一个或者多个别名,如名为relay1,west-coast.enterprise.com的主机,可能还有www.enterprise.com的别名。这样情况,前面第一个被称为规范主机名(canonical hostname)。应用程序可以调用DNS来获得主机别名对应的规范主机名以及主机的IP地址。
- 邮件服务器别名(mail server aliasing):电子邮件应用程序可以调用DNS,对提供的主机名别名进行解析,以获得该主机的规范主机名和其IP地址。
- 负载分配(load distribution):DNS也用于在冗余的服务器之间进行负载分配。繁忙的站点被冗余分布在多台服务器上,每台服务器均运行在不同的端系统上,每个都有着不同的IP地址。由于这些冗余的Web服务器,一个IP地址集合因此与同一个规范主机名相联系。DNS数据库中存储着这些IP地址集合,当客户对映射到某地址集合的名字发出一个DNS请求时,该服务器用IP地址的整个集合进行响应,但在每个回答中循环这些地址次序。因为客户通常总是向IP地址排在最前面的服务器发送HTTP请求报文,所以DNS就在所有这些冗余的Web服务器之间循环分配了负载。
3.2 DNS工作机理概述
DNS工作过程:用户主机上的某些应用程序需要将主机名转换为IP地址,这些应用程序将调用DNS的客户端,并指明需要被转换的主机名。用户主机上的DNS客户端接收到后,向网络中发送一个DNS查询报文,所有的DNS请求和回答报文使用UDP数据报经端口53发送。经过一段时延后,用户主机上的DNS接收到一个提供所希望映射的DNS回答报文,这个映射结果则被传递到调用DNS的应用程序。
DNS的一个简单设计是在因特网上使用一个DNS服务器,该服务器包含所有的映射,但这种集中式设计不适合现在庞大的因特网,它会产生很多问题:
- 单点故障(a single point of failure):如果DNS服务器崩溃,整个因特网都瘫痪。
- 通信容量(traffic volume):单个DNS服务器不得不处理所有DNS查询。
- 远距离的集中式数据库(distant centralized database):单个DNS服务器不可能“邻近”所有查询用户。
- 维护(maintenance):单个服务器不得不为所有因特网主机保留记录,这将使得中央数据库十分庞大。
3.2.1 分布式、层次数据库
DNS使用了大量的DNS服务器,这些服务器以层次方式组织,分布在全世界;没有一台DNS服务器拥有所以主机的映射,这些映射分布在所有的DNS服务器上。大致上,DNS服务器有3种类型:根DNS服务器、顶级域(Top-Level Domain,TLD)DNS服务器和权威DNS服务器。
图3.1 部分DNS服务器的层次结构
- 根DNS服务器:有400多个根名字服务器,这些服务器由13个不同的组织管理。根名字服务器提供TLD服务器的IP地址。
- 顶级域DNS服务器:对于每个顶级域(如com、org、net、edu)和所有国家的顶级域(如uk、fr、ca),都有TLD服务器,Verisign Global Registry Services 公司维护com顶级域的TLD服务器。TLD服务器提供了权威DNS服务器的IP地址。
- 权威DNS服务器:在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的DNS记录,这些记录将这些主机的名字映射为IP地址。一个组织机构的权威DNS服务器收藏了这些DNS记录。
结合图3.1,例如DNS客户要www.amazon.com的IP地址,则首先客户先与根服务器之一联系,它将返回顶级域名com的TLD服务器的IP地址。该客户则与这些TLD服务器之一联系,它将为amazon.com返回权威服务器的IP地址。最后该客户与amazon.com权威服务器之一联系,它为主机名ww.amazon.com返回其IP地址。
除以上三类DNS服务器,还有本地DNS服务器(local DNS server),它并不属于该DNS层次结构,但它对该结构很重要。每个ISP都有一台本地DNS服务器,当主机与某个ISP相连时,该ISP提供一台主机的IP地址,该主机具有一台或多台其本地DNS服务器IP地址,通过访问Windows或UNIX的网络状态窗口,用户能够容易地确定他的本地DNS服务器的IP地址。主机的本地DNS服务器通常“邻近”本主机,当主机发出DNS请求时,该请求被发往本地DNS服务器,它起着代理作用,并将该请求转发到DNS服务器层次结构中。
一个查询的例子:假设主机cse.nyu.edu想知道主机gaia.cs.umass.edu的IP地址,cse.nyu.edu主机的本地DNS服务器为dns.nyu.edu,并且gaia.cs.umass.edu的权威服务器为dns.umass.edu。如图3.2所示,主机cse.nyu.edu首先向它的本地DNS服务器dns.nyu.edu发送一个查询报文。该查询报文含有被转换的主机名gaia.cs.umass.edu。本地DNS服务器将该报文转发到根DNS服务器。该根DNS服务器注意到其edu前缀并向本地DNS服务器返回负责edu的TLD的IP地址列表。该本地DNS服务器则再次向这些TLD服务器之一发送查询报文。该TLD服务器注意到umass.edu前缀,并用权威服务器的IP地址进行响应,该权威DNS服务器是dns.umass.edu。最后,本地DNS服务器直接向dns.umass.edu重发查询报文,dns.umass.edu用gaia.cs.umass.edu的IP地址进行响应呢。本例中一种发送了8份DNS报文:4份查询和4份回答。
图3.2 各种DNS服务器的交互
上面的例子是假设TLD知道用于主机的权威DNS服务器的IP地址,实际上可能TLD只是知道中间的某个DNS服务器,该中间DNS服务器依次才能知道用于该主机的权威DNS服务器。因此可能需要发送的DNS报文更多,这时就需要利用DNS缓存减少这种查询流量。
如图3.2所示利用了递归查询(recursive query)和迭代查询(iterative query)。从cse.nyu,edu到dns.nyu.edu发出的是递归查询,因为该查询是以自己的名义请求本地服务器获得映射;而后面的三个查询是迭代查询的,因为所有的回答都是直接返回dns.nyu.edu。当然也可以如图3.3,整个过程都是递归的,但实践上通常采用图3.2的方式。
图3.3 DNS中的递归查询
3.2.2 DNS缓存
DNS缓存(DNS caching)原理是,在一个请求链中,当某DNS服务器接收到一个DNS回答时,它能将映射缓存在本地存储器中,则在下一次对相同主机名的查询到达时,该DNS能够提共所要求的主机的IP地址。由于主机和主机名与IP地址间的映射不是永久的,DNS服务器在一段时间后将丢弃缓存的信息。因为有了缓存,很多查询本地DNS服务器就立即返回了,这样就绕过了根DNS服务器、TLD服务器等。
3.3 DNS记录和报文
DNS服务器存储了资源记录(Resource Record,RR),RR提供了主机名到IP地址的映射。每个DNS回答报文包含了一条或多条资源记录。
资源记录是一个包含了下列字段的4元组:(Name,Value,Type,TTL)
TTL是该记录的生存时间,它决定了资源记录应当从缓存中删除的时间。Name和Value的值取决于Type:
- 如果Type = A,则Name是主机名,Value是该主机名对应的IP地址(如www.xaut.edu, 202.200.112.202, A)。
- 如果Type = NS,则Name是个域,而Value是个知道如何获得该域中主机IP地址的权威DNS服务器的主机名。这个记录用于沿着查询链路来路由DNS查询(如foo.com, dns.foo.com, NS)。
- 如果Type = CNAME,则Value是别名为Name的主机对应的规范主机名。该记录能够向查询的主机提供一个主机名对应的规范主机名(如foo.com, real.bar.foo.com, CNAME)。
- 如果Type = MX,则Value是个别名为Name的邮件服务器的规范主机名。
如果一台DNS服务器是用于某特定主机名的权威服务器,那么该DNS服务器会有一条包含用于该主机名的类型A记录(即使不是权威服务器,也可能在缓存中有一条类型A记录);如果服务器不是用于某主机的权威服务器,那么该服务器将包含一条类型NS类型记录,该记录对应于包含主机名的域;它还将包括一条类型A记录,该记录提供了在NS记录的Value字段中的DNS服务器的IP地址。例如一台TLD服务器,如果有一个请求gaia.cs.umass.edu,它根据请求首先在NS记录中找到它的权威服务器(umass.edu, dns.umass.edu, NS),然后TLD服务器中还要有找到的这个权威服务器的IP地址(dns.umass,edu, 128.119.40.111, A)。
3.3.1 DNS报文
DNS只有查询和回答报文两种报文,并且格式相同,如图3.1,各字段语义如下:
图3.1 DNS报文格式
- 前12个字节是首部区域,其中有几个字段。第一个字段(标志符)是一个16比特的数,用于标识该查询。这个标识符会被复制到对查询的回答报文中,以便让客户用它来匹配发送的请求和接收到的回答。标志符字段中含有若干标志符。1比特的“查询/回答”标志位指出是查询报文(0)还是回答报文(1)。当某DNS服务器是所请求名字的权威服务器时,1比特的“权威的”标志位被置在回答报文中。如果客户在该DNS服务器没有某记录时希望它执行递归查询,将设置1比特的“希望递归”标志位。如果该DNS服务器支持递归查询,在它的回答报文中会对1比特的“递归可用”标志位置位。在首部还有4个有关数量的字段,这些字段指出了在首部后的4类数据区域出现的数量。
- 问题区域包含着正在进行的查询信息。该区域包括:名字字段,包含正在被查询的主机名字;类型字段,指出有关该名字的正在被查询的问题类型(A,MX,NS,CNAME)。
- 在来自DNS服务器的回答中,回答区域还包含了对最初请求的名字的资源记录。在回答报文的回答区域中可以包含多条RR,因此一个主机名能够有多个IP地址。
- 权威区域包含了其他权威服务器的记录。
- 附加区域包含了其他有帮助的记录,比如对MX请求的回答报文中,回答区域中包含的一条RR提供了邮件服务器的规范主机名,该附加区域则包含的一条RR提供用于该邮件服务器的规范主机名的IP地址。
四、P2P文件分发
P2P体系结构对总是打开的基础设施服务器有最小的(或没有)依赖,它是成对间歇连接的主机(对等方)彼此直接通信。
4.1 P2P体系结构的扩展性
假设一个场景,现在要将一个大文件分发给一个对等方集合(一定数量的主机),不考虑其他因素,采用客户-服务器体系,那么服务器就需要向每台主机发送该文件到的副本;而采用P2P体系结构,当一个对等方接收到某些文件数据后,它也能利用自己的上载能力向其他对等方分发数据。事实上采用定量分析,此种情况下可以得到每个主机下载完该文件时,P2P体系结构所用时间是要小于客户-服务器体系结构的。具有P2P体系结构的应用能够是字扩展的,其原因在于每个对等方既是消费者同时也是重新分发者。BitTorrent是一种用于文件分发的流行P2P协议;分布式散列表(DHT)是一种简单的数据库,也是一种P2P应用,其数据库记录分布在一个P2P系统的多个对等方上。
五、套接字编程:生成网络应用
典型网络应用是由一对程序组成,它们位于两个不同端系统,当运行这两个程序时,创建了一个客户进程和一个服务者进程,同时它们通过从套接字读出和写入数据在彼此之间进行通信。网络应用程序有两类,一类是由协议标准所定义的操作实现的,客户程序和服务程序必须遵守由该RFC所规定的规则;另一类是专用的网络应用程序,这种情况下客户和服务器程序的应用层协议没有公开,其他开发者不能开发出和该应用程序交互的代码。
5.1 UDP套接字编程
使用UDP套接字的两个通信进程,在发送进程能够将数据分组推出套接字之前,必须先将目的地址附在该分组上。在该分组传过发送方的套接字之后,因特网将使用该目的地址通过因特网为该分组选路到接收进程的套接字。当分组到达接收套接字时,接收进程将通过该套接字取回分组,然后检查分组执行接下来的动作。
附在分组上地址由目的主机的IP地址、目的地套接字的端口号、源主机IP地址和源套接字的端口号,不过源地址和端口号通常不是由UDP应用程序代码完成,而是由底层操作系统自动完成。
设计这样一个简单的客户-服务器应用程序演示UDP和TCP的套接字编程:
- 客户从其键盘读取一行字符并将该数据向服务器发送。
- 服务器接收该数据并将这些字符转换为大写。
- 服务器将修改的数据发送给客户。
- 客户接收修改的数据并在其监视器上将该行显示出来。
图5.1 使用UDP的客户-服务器应用程序
UDPCilent:
1 //指明服务器的地址和端口号 2 String serverName = "10.97.18.22"; 3 int port = 12000; 4 5 //创建UDP套接字,可以不用指定客户套接字,操作系统会完成该操作 6 DatagramSocket client = new DatagramSocket(); 7 8 System.out.println("键入小写:"); 9 Scanner scan = new Scanner(System.in); 10 String lowercase = scan.next(); 11 12 //生成数据报包,为该报文附上目的地址和端口 13 //源地址也会附到报文上,不过由操作系统自动完成 14 DatagramPacket clientPacket = new DatagramPacket(lowercase.getBytes(), 15 lowercase.length(), 16 InetAddress.getByName(serverName), 17 port); 18 //向客户端进程的套接字发送结果分组 19 client.send(clientPacket); 20 21 DatagramPacket serverPacket = new DatagramPacket(new byte[1024], 1024); 22 client.receive(serverPacket); 23 System.out.println("客户端收到:"+new String(serverPacket.getData())); 24 25 client.close();
UDPServer:
1 int port = 12000; 2 //创建UDP套接字,并将分配的端口号与服务器套接字绑定 3 //以这种方式,任何向该服务器的IP地址的端口12000发送一个分组,该分组都将导向该套接字 4 DatagramSocket server = new DatagramSocket(port); 5 6 DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024); 7 server.receive(receivePacket); 8 String s = new String(receivePacket.getData()); 9 System.out.println("服务端收到:"+s); 10 11 //接收数据报包包含客户端的地址和端口号 12 //将该地址和端口号附在报文上,发送到服务器套接字中 13 DatagramPacket sendPacket = new DatagramPacket(s.toUpperCase().getBytes(), 14 s.toUpperCase().getBytes().length, 15 receivePacket.getAddress(), 16 receivePacket.getPort()); 17 System.out.println("客户端口号"+receivePacket.getPort()); 18 server.send(sendPacket); 19 20 server.close();
5.2 TCP套接字编程
TCP是一个面向连接的协议,这意味着在客户和服务器能够开始互相发送数据之前,它们首先要握手和创建一个TCP连接,TCP连接的一端与客户套接字相联系,另一端要与服务器套接字相联系。当创建该TCP套接字时,我们将其与客户套接字地址和服务器套接字地址关联起来。使用该连接时,当一侧要向另一侧发送数据时,它只需经过其套接字将数据丢进TCP连接,这与UDP连接不同,UDP服务器在将分组丢进套接字之前必须为其附上一个目的地址。
TCP中客户具有向服务器发起接触的任务,这里服务器有一扇特殊的门,精确地说是一个特殊套接字,该门欢迎来自运行在任意主机上的客户进程的某种初始接触。使用房子与门比喻进程与套接字,则这个初始接触应称为“敲欢迎之门”。
随着服务器进程的运行,客户进程能够向服务器发起一个TCP连接。这是由客户程序通过创建一个TCP套接字完成的,当该客户生成其TCP套接字时,它指定了服务器中欢迎套接字的地址,即服务器主机的IP地址和套接字端口号。生成其套接字后,该客户发起了一个三次握手并创建与服务器的一个TCP连接,发生在运输层的三次握手,对于该客户和服务器程序是完全透明的。
在三次握手操作期间,客户进程敲服务器进程的欢迎之门。当该服务器收到敲门声时,它将生成一扇新门(精确讲是一个新套接字),它专门用于特定的客户。下例中,这个欢迎之门是serverSocket的TCP套接字对象;它是专门对客户进行连接的新生成的套接字,称为连接套接字(connectionSocket)。如图5.2,这里欢迎套接字是要与服务器通信的客户的起始接触点;连接套接字是为与每个客户通信而生成的套接字。
图5.2 TCPServer进程有两个套接字
我们仍使用和5.1中同样的客户-服务器程序展示TCP套接字编程:
图5.3 使用TCP的客户-服务端应用程序
TCPCilent:
1 String serverName = "10.97.18.22"; 2 int serverPort = 12000; 3 4 //创建TCP套接字并将其连接到指定地址的指定端口 5 //不用指定客户的端口号,操作系统会自动完成 6 //这句代码执行完后,执行三次握手,创建起一条TCP连接 7 Socket client = new Socket(serverName,serverPort); 8 9 //获取套接字的输出流 10 //这里并没有显示地创建一个分组并为该分组附上目的地址 11 System.out.println("键入小写:"); 12 Scanner scanner = new Scanner(System.in); 13 OutputStream out = client.getOutputStream(); 14 out.write(scanner.next().getBytes()); 15 16 //获取套接字输入流 17 InputStream inputStream = client.getInputStream(); 18 byte[] bytes1 = new byte[1024]; 19 inputStream.read(bytes1); 20 System.out.println("客户端收到:"+new String(bytes1)); 21 22 //关闭TCP连接,它引起客户中的TCP向服务器中的TCP发送一条TCP报文 23 out.close(); 24 inputStream.close(); 25 client.close();
TCPServer:
1 int serverPort = 12000; 2 //创建绑定到特定端口的套接字,请求连接的最大数量限制在1个 3 //这个serverSocket就是欢迎套接字 4 ServerSocket serverSocket = new ServerSocket(serverPort,1); 5 6 //创建完欢迎套接字之后,accept就阻塞等待某个客户的TCP连接请求 7 //accept创建了一个新的套接字,即连接套接字,它给特定的客户专用 8 //这是客户和服务器完成了握手,在clientSocket和serverSocket之间创建了一个TCP连接 9 Socket connectionSocket = serverSocket.accept(); 10 11 //获取套接字入流 12 InputStream inputStream = connectionSocket.getInputStream(); 13 byte[] bytes = new byte[1024]; 14 inputStream.read(bytes); 15 String s = new String(bytes); 16 System.out.println("服务器收到:"+s); 17 18 //获取套接字出流 19 OutputStream outputStream = connectionSocket.getOutputStream(); 20 outputStream.write(s.toUpperCase().getBytes()); 21 22 //这里要注意关闭顺序,如果流在其他操作还没执行完就关闭,会导致整个连接套接字关闭 23 //实际上每次只关闭connectionSocket,serverSocket保持打开等待新的请求 24 inputStream.close(); 25 outputStream.close(); 26 connectionSocket.close(); 27 serverSocket.close();
从上面的代码还可以明显地看到,使用UDP套接字,每次发送数据会将数据封装成数据报包,进行发送;而TCP套接字则是流式的,获取的是套接字中的输入输出流。