计算机网络自顶向下 第二章 应用层
2.1 应用层序协议原理
2.1.1 网络应用程序体系结构
客户-服务器体系结构
这种体系结构中,有一个总是打开的主机称为服务器,它服务于来自许多其他称为客户的主机的请求。在Web应用程序的例子中,总是打开的Web服务器服务于来自浏览器(运行在客户主机上)的请求。这种体系结构,客户相互之间不直接通信。如,在Web应用中两个浏览器并不直接通信。
P2P体系结构
例子:文件共享(BitTorrent)、对等方协助下载加速器(迅雷)、因特网电话(Skype)、IPTV(迅雷、PPstream)。应用程序在间断连接的主机对之间使用直接通信,这些主机对被称为对等方。因为这种对等方通信不必通过专门的服务器,所以被称为对等方到对等方的。目前许多流行的、流量密集型应用都是P2P体系结构。
混合体系结构
某些应用具有以上两者混合的结构体系。如即时讯息应用,服务器被用于跟踪用户的IP地址,但用户到用户的报文在用户主机之间(无需通过中间服务器)直接发送。
2.1.2 进程通信
在两个不同的端系统上两个进程,通过计算机网络交换报文互相通信。发送进程生成并向网络之中发送报文;接收进程接收报文并通过回送报文进行响应。
客户和服务器进程
网络应用程序由成对的进程组成,这些进程通过网络相互发送报文。
- 在给定的一对进行之间的通信会话场景中,发起通信的进程被标识为客户,在会话开始时等待联系的进程是服务器。
进程与计算机之间的的接口
进程与计算机网络之间的接口:进程通过套接字(socket)软件接口向网络发送报文和从网络接收报文。套接字相当于一个通道:发送进程将报文交给套接字,套接字将这些报文传输到接收进程的套接字。套接字的运输层端几乎没有控制权。控制权仅限于:选择运输层协议,设定参数。
进程寻址
为了向另一台主机运行的进程发送分组,接收进程要有一个地址。为了表示地址需要定义两种信息:1.主机的地址(IP地址),2.在目标主机中指定接收进程的标识符(端口号)。
2.1.3 可供应用进程使用的运输服务
运输服务的衡量标准:可靠数据传输、吞吐量、定时、安全性
选择的网络的应用的要求
TCP服务
- 面向连接的服务:需要执行“3次握手”建立两个进程的套接字之间的TCP连接。这条连接是全双工的,即连接双方的进程可以在此连接上同时进行报文收发。当应用程序结束报文发送时,必须拆除该连接。
- 可靠的数据传送服务:通信进程能依靠TCP,无差错、按适当顺序交付所有发送的数据,没有字节的丢失和冗余。
- 拥塞控制机制:为因特网带来整体好处。当发送方和接收方之间的网络出现拥塞时,TCP拥塞控制机制会抑制发送进程。该机制也会试图限制每个TCP连接,使它们达到公平共享网络带宽的目的。
UDP服务
- 无连接的服务:进程通信前不需要建立连接
- 不可靠的数据传送服务:当进程将一个报文发送进UDP套接字时,UDP协议不保证该报文将到达接收进程,而且到达接收进程的报文也可能是乱序到达的。
- 没有拥塞控制机制:所以UDP的发送端可以用它选定的任何速率向其下层(网络层)注入数据。(实际端到端吞吐量可能小于这种速率,可能是因为中间链路的带宽受限或因为拥塞而造成。)
应用层协议定义了运行在不同端系统上的应用程序进程如何相互传递报文。应用层协议定义了:
- 交换报文类型,例如请求报文和响应报文
- 各种报文类型的语法,如报文之间的字段是如何描述的
- 字段的语义,这些字段的信息含义
- 确定一个进程何时以及如何发送报文,对报文响应的规则。
2.2 Web和HTTP
2.2.1 HTTP概况
Web 页面由一些对象组成;HTML文件是Web页面的基础,它可以包括各种各样的对象,是一个容器对象;任何一个对象都可以用 URL来定位。URL地址由两部分组成:存放对象的服务器主机名和对象的路径名。
web采取 客户端/服务器模式,客户端启动TCP连接(创建套接字) 到服务器, 端口;服务器接受来自客户端的TCP 连接;http报文(应用层协议报文) 在浏览器 (http client) 和Web服务器(http server)之间进行交换;关闭TCP 连接。
HTTP服务器不保存关于客户的任何信息,所以说HTTP是一个无状态协议。
2.2.2非持续连接和持续连接
非持续连接:每个请求/响应对是经一个单独的TCP连接发送。
持续连接:所有请求及其响应经相同的TCP连接发送。
采用非持续连接的HTTP
RTT(Round-Trip Time)。RTT指的是,一个短分组从客户端到服务器,然后再返回客户端所用的时间。RTT包括分组的传播时延、排队时延、处理时延(因为是短分组,所以其传输时延可不计)。
采用非持续连接的缺点
- 需要为每一个请求的对象建立和维护一个全新的连接。对于每个这样的连接,在客户和服务器中都要分配TCP的缓冲区和保持TCP变量,这给Web服务器带来严重负担。
- 每一个对象要经受两倍的RTT(往返时间,Round-Trip Time)交付延迟。
采用持续连接的HTTP
持久连接又可以分为:非流水线方式:一个对象传输完成方能传输下一个;流水线方式:可以一次性发送所有请求,慢慢接收。
2.2.3 HTTP的报文格式
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
- 方法:可填入GET、POST、HEAD、PUT、DELETE等,绝大部分HTTP请求报文使用GET方法
- URL:如www.baidu.com
- 版本:HTTP版本
- 首部字段名:有好几种首部字段名,如Host、Connection、User-agent
值:首部字段名对应的值 - sp:空格
- cr:回车(carriage return)
- lf:换行(line feed)
- 实体主体:使用GET方法时,实体主体为空;使用POST方法才会有实体主体
HTTP响应报文
HTTP/1.1 200 OK
Connection: close
Date: Tue, 09 Aug 2011 15:44:04 GMT
Server: Apache/2.2.3 (CentOS)
Last-Modified: Tue, 09 Aug 2011 15:11:03 GMT
Content-Length: 6821
Content-Type: text/html
(data data data data data ...)
版本:HTTP版本
状态码:
- 200(OK,请求成功,信息在返回的响应报文中)
- 301(Moved Permanently,请求对象已被永久转移,新的URL定义在响应报文的Location:首部行中。客户软件将自动获取新的URL——自动跳转到新URL)
- 400(Bad Request,一个通用差错代码,指示该请求不能被服务器理解)
- 404(Not Found,被请求的文档不在服务器上)
- 505(HTTP Version Not Supported,服务器不支持
2.2.4 cookie
前文说HTTP服务器是无状态的,不保存客户信息。但是有时候一个Web站点希望能够识别用户,可能是出于服务器希望限制用户的访问,或者是出于希望把内容与用户身份联系起来。
为此HTTP可以使用cookie。它允许站点对用户进行跟踪。目前大多数商务Web站点都使用了cookie。
cookie技术的4个组件
- 在HTTP响应报文中的一个cookie首部行。
- 在HTTP请求报文中的一个cookie首部行。
- 在用户端系统中保留有一个cookie文件,并由用户的浏览器进行管理
- 位于Web站点的一个后端数据库。用于保存cookie。
2.2.5web缓存
顾名思义就是服务器的代理了,它同样可以处理客户发送的HTTP请求报文,如果它缓存着请求报文所需对象的副本的话。如果没有,Web缓存器会先向本体Web服务器获取所需对象的副本,再提供服务给客户。
通过“条件GET方法”,保证缓存器的对象是最新的。
采用Web缓存器的好处
- 大大减少对客户请求的响应时间
- 大大减少一个机构的接入链路到因特网的通信量。通过减少通信量,该机构就不必急于增加带宽,从而降低费用
- 能从整体上大大减低因特网上的Web流量,从而改善所有应用的性能
2.3简单邮件协议 SMTP
电子邮件系统的3个主要组成部分
- 用户代理
- 邮件服务器
- STMP
STMP是运用在邮件服务器之间传输的协议。
用户代理负责把用户发送的报文发送给邮件服务器,以及接收邮件服务器发送给用户的报文。
STMP一般不适用中间邮件服务器发送邮件,即使这两个邮件服务器位于地球的两端。如Alice的邮件服务器在中国香港,而Bob的服务器在美国圣路易斯,那么这个TCP连接也是从香港服务器到圣路易斯服务器之间的直接相连。特别是,如果Bob的邮件服务器没有开机,该报文会保留在Alice的邮件服务器上并等待进行新的尝试,这意味着邮件并不在中间的某个邮件服务器存留。
SMTP与HTTP的对比
1、两者都用于从一台主机向另一台主机传送文件,都使用持续连接。HTTP从Web服务器向Web客户传送文件;SMTP从一个邮件服务器向另一个邮件服务器传送文件。
2、HTTP是一个拉协议(pull protocol)。即在方便时,用户使用HTTP从服务器拉取信息;SMTP是一个推协议(push protocol),发送邮件服务器把文件推向接收邮件服务器。
3、SMTP要求每个报文(包括它们的体)使用7比特ASCII码格式。如果报文包含非7比特ASCII字符(如有重音的法文字符)或二进制数据(如图形文件),则报文必须按照7特比ASCII码进行编码。HTTP数据不受这种限制。
4、处理一个既包含文本又包含图形的文档的方式不同。SMTP把所有报文对象放在一个报文中;HTTP把每个对象封装到它自己的HTTP响应报文中。
2.4 DNS :因特网的目录服务
因特网上的主机的标识有多种方式
- 主机名,如www.baidu.com
- IP地址,如xxx.xxx.xxx.xxx
2.4.1 DNS提供的服务
DNS是
- 一个由分层的DNS服务器实现的分布式数据库
- 一个使得主机能够查询分布式数据库的应用层协议
DNS协议运行在UDP之上,使用53号端口。
DNS除了将主机名转换为IP地址,还有以下服务
- 识别主机别名(用于HTTP、FTP)
- 识别邮件服务器别名(用于SMTP)
- 负载分配
2.4.2 DNS工作机理概述
为了处理扩展性问题,DNS使用了大量的DNS服务器。大致来说,有3种类型的服务器:根DNS服务器、顶级域(Top-Level Domain.TLD)DNS服务器和权威DNS服务器
-
根DNS服务器:因特网上有13个根DNS服务器,这13个服务器实际上是一个冗余的计算机网络以提供安全性和可靠性;它主要提供TLD服务器的IP地址
-
顶级域DNS服务器:负责顶级域名,如com,org,net,edu,gov以及各个国家的顶级域名,主要提供权威DNS服务器的IP地址
-
权威DNS服务器:互联网上的每一个能够公共访问的主机都具有一个DNS记录,这些记录由某些组织机构的权威服务器保存。例如常见的服务提供商以及学校和公司都会实现和维护自己的权威服务器来保存一些主机DNS记录
本地DNS服务器:该DNS服务器不属于上述的层次结构。但它发挥着极其重要的作用
每个ISP(如一个居民区的ISP或者一个机构的ISP)都有一台本地DNS服务器,这些本地DNS服务器通常“邻近”用户主机。当主机发出DNS请求时,该请求被发往本地DNS服务器,它起着代理的作用,并将该请求转发到DNS服务器层次结构中。
上述例子中利用了递归查询和迭代查询:
-
递归查询:DNS服务器收到一个域名解析请求时,如果所要检索的资源记录不在本地,DNS服务器将和自己的上一层服务器交互,获得最终的答案,并将其返回给客户(例如上述的cse.sicnu.edu到dns.sicnu.edu的查询)
-
迭代查询:DNS服务器收到解析请求,首先在本地的数据库中查找是否有相应的资源记录,如果没有,则向客户(本地服务器)提供另外一个DNS服务器的地址,客户(本地服务器)负责把解析请求发送给新的DNS服务器地址。
实际上,常常是采用上述的方式进行解析。(只有从请求主机到本地DNS服务器的查询是递归的,其余的查询都是迭代的)
2.4.3 DNS缓存
DNS服务器在接收到一个DNS回答(包含主机名到IP地址的映射)时,将映射缓存在本地存储器中。
-
由于主机和主机名与IP地址间的映射并不是永久的,DNS服务器在一段时间后,将丢弃缓存的信息。
-
本地DNS服务器也可以缓存TLD服务器的IP地址,因而允许本地DNS服务器绕过查询链中的根DNS服务器,事实上,因为缓存的存在,大多数DNS查询中,根服务器都被绕过了。
2.4.3DNS记录和报文
共同实现DNS数据库的所有DNS服务器存储了资源记录,RR提供了主机名到IP的映射。
资源记录是一条包含下列字段的4元组((Name, Value, Type, TTL)。
Name和Value的意义取决于Type
- if Type = A, Name就是主机名,Value就是主机的IP 地址。因此,一个A类型的记录提供了一个标准的hostname-to-IP地址的映射
- If Type = NS, Name是域名(domain,例如foo.com), 值是这个域名的权威DNS服务器。例如(foo.com, dns.foo.com, NS)
- If Type = CNAME, Name是别称,Value是规范名称(canonical name),用来查询别称的规范主机名,例如(foo.com, relay1.bar.foo.com, CNAME) is a CNAME record
- If Type = MX, Name是邮件服务器的别称,Value是邮件服务器的规范名称,例如(foo.com, mail.bar.foo.com, MX)
2.4.4报文格式
2.5 P2P文件分发
P2P和客户-服务器体系结构的分发时间
2.7 套接字编程:生成网络应用
我们用下列简单的客户-服务器应用程序来演示对于UDP和TCP的套接字编程
- 客户从键盘读取一行字符并向服务器发送。
- 服务器接收该数据并将这些字符转换为大写
- 服务器将修改的数据发给客户
- 客户接收的数据并在监视器上将改行显示出来
2.7.1 UDP编程
UDPclient.py
from socket import *
serverName = "hostname"
serverPort = 12000
clientsocket = socket(AF_INET, SOCK_DGRAM)
message = raw_input("Input lowercase sentence:")
clientSocket.sendto(message.encode(),(serverName,serverPort))
modifiedMessage, serverAddress = clientsocket.recvfrom(2048)
print (modifiedMessage.decode())
clientSocket.close ()
serverName = "hostname"
serverPort = 12000
在这里我们可以提供包含服务器IP地址或者主机名的字符串,如果是主机名的话将自动执行DNS lookup从而获得IP地址。将整数变量serverPort,即服务器端口设置为12000
clientSocket.sendto(message.encode(),(serverName,serverPort))
该行创建了客户的套接字,称为clientSocket第一个参数指示了地址簇;第二个参数指示了该套接字是SOCK.DGRAM类型(数据报datagram)的,这意味着它是一个UDP套接字
clientSocket.sendto(message.encode(),(serverName,serverPort))
在这行中,我们首先将报文由字符串类型转换为字节类型,因为我们需要向套接字中发送字节;这将使用encode ()方法完成。方法sendto ()为报文附上目的地址 (serverName, serverPort)并且向进程的套接字clientSocket发送结果分组。
print (modifiedMessage.decode () )
这行将报文从字节转化为字符串后,在用户显示器上打印出modifiedMessage0它应当 是用户键入的原始行,但现在变为大写的了
clientSocket.close ()
该行关闭了套接字。然后关闭了该进程
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)
serversocket.bind(('',serverPort))
上面行将端口号与该服务器的套接字绑定在一起。因此在UDPServer中,代码显式地为该套接字分配一个端口号。以这种方式,当任何人向位于该服务器的IP地址的端口发送一个分组,该分组将导向该套接字。
message, clientAddress = serverSocket.recvfrom(2048)
这行代码类似于我们在UDPClient中看到的。当某分组到达该服务器的套接字时,该分组的数据被放置到变量message中,其源地址被放置到变量clientAddress中。变量clientAddress包含了客户的IP地址和客户的端口号。这里,UDPServer将利用该地址信息,因为它提供了返回地址,类似于普通邮政邮件的返回地址。使用该源地址信息,服务器此时知道了它应当将回答发向何处。
serverSocket.sendto(modifiedMessage.encode(), clientAddress)
最后一行将该客户的地址(IP地址和端口号)附到大写的报文上(在将字符串转化为字节后),并将所得的分组发送到服务器的套接字中。然后因特网将分组交付到该客户地址。在服务器发送该分组后,它仍维持在while循环中,等待(从运行在任一台主机上的任何客户发送的)另一个UDP分组到达。
2.7.2 TCP套接字编程
与UDP不同,TCP是一个面向连接的协议。这意味着在客户和服务器能够开始互相发送数据之前,它们先要握手和创建一个TCP连接。TCP连接的一端与客户套接字相联系,另一端与服务器套接字相联系。当创建该TCP连接时,我们将其与客户套接字地址 ( IP地址和端口号)和服务器套接字地址(IP地址和端口号)关联起来。使用创建的TCP 连接,当一侧要向另一侧发送数据时,它只需经过其套接字将数据丢进TCP连接。这与 UDP不同,UDP服务器在将分组丢进套接字之前必须为其附上一个目的地地址。
TCPClient.py
from socket import *
serverName = 'servername'
serverPort = 114514
clientSocket = socket(AF_INET,SOCK_STREAM)
#有客户端套接字隐式捆绑到本地port
clientsocket.connect((serverName,serverPort))
sentence = raw_input('Input lowercase sentence:')
clientsocket.send(sentence.encode())
modifiedSentence = clientsocket.recv(1024)
print('From Server:' , modifiedSentence.decode())
clientSocket.close ()
clientSocket = socket(AF_INET,SOCK_STREAM)
该行创建了客户的套接字,称为clientSocket,第一个参数仍指示底层网络使用的是 IPv4。第二个参数指示该套接字是SOCK_STREAM类型。这表明它是一个TCP套接字。
clientsocket.connect((serverName,serverPort))
前面讲过在客户能够使用一个TCP套接字向服务器发送数据之前(反之亦然),必须在客户与服务器之间创建一个TCP连接。上面这行就发起了客户和服务器之间的这条TCP连接。connect()方法的参数是这条连接中服务器端的地址。这行代码执行完后,执行三次握手,并在客户和服务器之间创建起一条TCP连接
clientsocket.send(sentence.encode())
通过该客户的套接字并进入TCP连接发送字符串sentence,值得注意的是,该程序并未显式地创建一个分组并为该分组附上目的地址,而使用UDP套接字却要那样做。 相反,该客户程序只是将字符串sentence中的字节放入该TCP连接中去。
modifiedSentence = clientsocket.recv(1024)
当字符到达客户端时,它们被放置在字符串modifiedSentence中。字符继续积累在 modifiedSentence中,直到该行以回车符结束为止。
TCPServer. py
from socket import *
serverPort = 114514
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 ()
serverSocket.listen(1)
但对TCP而言,serverSocket是我们的欢迎套接字。在创建这扇欢迎之门后,我们将等待并聆听某个客户敲门(即聆听某个客户端发出TCP连接请求)。其中参数定义了请求连接的最大数 (至少为1)。
connectionSocket,addr = serverSocket.accept()
当客户敲该门时,程序为serverSocket调用accept()方法,这在服务器中创建了一个称为connectionSocket的新套接字,由这个特定的客户专用。客户和服务器则完成了握手,在客户的clientSocket和服务器的serverSocket之间创建了一个TCP连接。借助于创建的TCP连接,客户与服务器现在能够通过该连接相互发送字节。使用TCP,从一侧发送的所有字节不仅确保到达另一侧,而且确保按序到达。
connectionSocket.close ()
在此程序中,在向客户发送修改的句子后,我们关闭了该连接套接字。但由于serverSocket保持打开,所以另一个客户此时能够敲门并向该服务器发送一个句子要求修改。