前面提到过,因特网是分布式网络应用的平台,作为为应用提供服务的联网基础设施。那么什么是网络应用呢?网络应用又是怎么使用因特网作为基础设置的呢?
研发网络应用的程序的核心是写出能够运行在不同的端系统和通过网络彼此通信的程序。
前面学到的5层因特网分层结构,可以称之为网络的体系结构,从应用程序开发人员来看,网络体系结构是固定的,它为应用程序提供了特定的服务集合。
从程序运行的端系统角度看,应用程序开发者更为关注的是应用程序体系结构。主流的应用程序体系结构分为两种:客户-服务器体系和对等(P2P)体系结构
A. 客户-服务器体系:
B. P2P体系结构:
前面提到应用需要在不同端系统上运行程序,实际上程序是静态的,真正运行的是进程,可以理解为进程是程序的运行时,进行通信的实际上是进程,而不是程序。
当多个进程运行在相同的端系统上时,它们使用进程间通信机制进行通信,这里不涉及网络。对于两个不同端系统上的进程,需要跨越计算机网络交换报文来进行通信。
客户和服务器进程
套接字
进程寻址
进程之间通过套接字传输报文,套接字之下是运输层,对于运输层,能够为应用程序提供的运输服务包括:可靠数据传输、吞吐量、定时和安全性
以上是运输层理论上可以提供的运输服务,而应用程序是通过选择特定的运输层协议,从而能够享受这些运输服务的。
因特网提供的运输层协议有两个:TCP和UDP
TCP协议
UDP协议
通过因特网提供的仅有的两个运输层协议来看,我们期望运输层所能提供的服务,即可靠数据传输、吞吐量、定时和安全性,并没有完全被实现,如吞吐量、定时等。实际上,这些遗漏的服务目前的因特网并没有提供,而是通过将应用设计成尽最大可能对付这种保证缺失。
进程之间通过将报文发送进套接字实现网络进程之间的通信,而应用层协议定义了运行在不同端系统上的应用程序进程如何相互传递报文
具体的,应用层协议定义了:
应用层协议 & 应用:
应用层协议是应用的一部分,而且是非常重要的一部分。一个应用,如Web引用,它包括文档格式的标准(即HTML)、Web浏览器、Web服务器以及一个应用层协议(HTTP协议)
Web应用概述:
超文本传输协议(HTTP)概述:
非持续连接:每个请求/响应对是经一个单独的TCP连接发生
持续连接:所有的请求及其响应经过相同的TCP连接发生
HTTP既可以使用非持续连接,也可以使用非持续连接,默认使用持续连接
非持续连接的缺点:
在HTTP1.1持续连接的情况下,服务器在发送响应后保持该TCP连接打开,在相同的客户与服务器之间,后续的请求和相应报文能够通过相同的连接进行传送。
在HTTP/2中,允许相同连接中多个请求和回答交错,并增加了该连接中优化HTTP报文请求和回答的机制
HTTP报文分为:请求报文和响应报文。
以下是一个典型的HTTP请求报文:
GET /somedir/page.html HTTP/1.1
Host: www.someshcool.edu
Connection: close
User-agent: Mozilla/5.0
Accept-language: fr
其中,第一行称为请求行,包含三个字段:方法字段、URL字段和HTTP版本字段。方法字段包括:GET、POST、HEAD、PUT、DELETE。/somedir/page.html为对象的URL。浏览器实现的是HTTP/1.1版本。
余下的称为首部行。
上述例子中,由于使用GET方法,实体体为空,而在使用POST方法时才使用实体体。
通用的请求报文格式包括:状态行、首部行和实体体。
以下是一个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 data...)
第一行为状态行,包含三个字段:协议版本字段、状态码和相应状态信息
对于状态码,常见的状态码和状态信息包括:
以上响应报文中,首部行有6个:
前面提到,HTTP协议是无状态的,服务器不会存储任何该用户的状态信息。但是在一个Web站点,通常希望能够识别用户,可能是因为服务器希望限制用户的访问,或者因为它希望把内容与用户身份联系起来。这是就需要用到cookie对用户进行追踪。
cookie技术有四个组件:
通过cookie字段,Web站点可以实现标识用户,从而在无状态的HTTP之上建立一个用户会话层。
Web缓存器,又称代理服务器,是能够代表初始服务器来满足HTTP请求的网络实体。
Web缓存器有自己的磁盘存储空间,并在存储空间中保存最近请求过的对象的副本。通过配置用户浏览器,可以使得用户的所有HTTP请求首先指向Web缓存器。当请求在Web缓存器中命中,直接返回请求对象;否则,由Web缓存器再向初始服务器请求对象并返回给用户,请求对象在Web缓存中存储副本。
Web缓存器即使服务器又是客户,当接受浏览器的请求并返回响应时是一个服务器;当向初始服务器发送请求并接受响应时是一个客户。
部署Web缓存器的好处有:
使用Web缓存器可以带来诸多好处,但是也引入了一个新的问题,即存放在Web缓存器中的对象副本可能是陈旧的,在初始服务器上的对象可能已经被修改了。
为了解决这个问题,HTTP协议引入了条件GET方法。条件GET方法有以下特征:
当Web缓存器向初始服务器请求一个对象时,初始服务器会通过报文返回该对象,同时首部行中包含Last-Modified,Web缓存器存储对象副本以及最后修改时间。当过了一段时间,用户请求该对象,Web缓存器会发送条件GET报文,if-Modified-Since首部行的值即为上次Last-Modified的值。当初始服务器收到报文后,检查对象是否自if-Modified-Since有修改过,如果修改过则返回最新对象,没有修改也会发送一个响应报文,但是报文中不包含对象。
电子邮件系统主要组成部分包括:用户代理、邮件服务器和简单邮件传输协议(SMTP)
SMTP一般不使用中间邮件服务器发送邮件,即使这两台服务器位于地球两端也是如此。
客户SMTP在与服务器SMTP建立TCP连接后,将会执行某些应用层的握手,相当于人类在互相交流前的自我介绍。彼此介绍之后,客户端才发送报文。如下所示为SMTP客户 ©和SMTP服务器(S)之间的应用层握手示例,其中客户的主机名为crepes.fr,服务器的主机名为:hamburger.edu。
S: 220 hamburger.edu
C: HELO crepes.fr
S: 250 Hello crepes.fr, pleased to meet you
C: MAIL FROM:
S: 250 [email protected] ... Sender OK
C: RCPT TO :
S: 250 [email protected] ... Recipent 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
SMTP vs HTTP
相同点:SMPT和持续的HTTP都是用持续连接
不同点:
SMTP报文格式
SMTP报文格式包含:首部行、报文体,首部行与报文体之间是空白行。每个首部必须含有一个From:首部行和To:首部行,其他首部行可选。
以下是一个典型的报文首部
From: [email protected]
To: [email protected]
Subject: Serching for the meaning of lifr
前面提到了因特网电子邮件系统的整体组成,现在我们来思考几个问题:
问题1:为什么要有用户代理,发送方和接收方在自己的PC上放置邮件服务器不好吗?这样彼此之间可以直接进行对话
问题2: 发送方PC运行用户代理,为什么该用户代理不直接通过SMTP协议直接与接收方对应的邮件服务器通信?而是要先将报文发送到自己对应的邮件服务器上,再由该邮件服务器与接收方的邮件服务器进行通信?
结合之前学到的,SMTP是一个推协议,结合电子邮件系统的结构,SMTP既可以用来将邮件从发送方的邮件服务器传输到接收方的邮件服务器,也可以用来将邮件从发送方的用户代理传送到发送方的邮件服务器。
那么问题来了,作为接收方,如何通过用户代理获取对应服务器上的邮件呢?要知道取邮件是一个拉的操作,而SMTP是一个推协议,因此不能用SMTP来实现。
基于这种情况,就需要用到邮件访问协议,目前流行的包括:第三版的邮局协议(POP3)、因特网邮件访问协议(IMAP)和HTTP
该协议非常简单,也因此功能相当有限。POP3按照三个阶段进行:特许、事务处理以及更新
特许
user
和pass
事务处理
更新
下载并删除 vs 下载并保留
在用户代理和邮件服务器之间的POP3会话期间,服务器会保留一些状态信息,但是并不会在会话过程中携带状态信息
POP3协议是通过将邮件下载到本地主机,而没有给用户提供任何创建远程文件夹并为报文指派文件夹的方法
IMAP协议为用户提供了创建文件夹以及将邮件从一个文件夹移动到另一个文件夹的命令,同时还为用户提供了在远程文件夹中查询邮件的命令
IMAP服务器还维护了会话的用户状态信息
IMAP协议另一个特性是,它具有允许用户代理获取报文某些部分的命令。如只读取报文的首部,这在用户代理与邮件服务器之间使用低带宽连接时非常有用
人类可以用很多方法进行标识,比如出生证明上的姓名、社会保险号、驾照上的号码等等。而主机同样有很多标识方法,常见的有主机名和IP地址,主机名便于记忆,但是几乎没有提供关于主机在互联网中位置的信息;IP地址可以表示主机位置,但是又不方便记忆。DNS提供了主机名到IP地址的转换。
域名系统(Domain Name System, DNS)是:
DNS服务器通常是运行在BIND软件的UNIX机器,DNS协议运行在UDP之上。
除了前面提到的提供了主机名到IP地址的转换,DNS还提供以下服务:
就提供主机名到IP地址的转换服务而言,从用户主机调用应用程序来看,DNS是一个提供简单、直接的转换服务的黑盒子。
DNS最简单的设计思路是,在因特网上只使用一个DNS服务器,所有的映射关系都包含在该服务器中。这样会带来以下的问题
实际中的DNS是一个在因特网上实现的分布式、层次数据库。
DNS存在三种类型的DNS服务器,分别是:根DNS服务器、顶级域(Top-Level Domain, TLD)DNS服务器和权威DNS服务器,没有一台DNS服务器拥有因特网上所有主机的映射。
根DNS服务器
顶级域(Top-Level Domain, TLD)DNS服务器:
权威DNS服务器
除此之外,还有一类称为本地DNS服务器,一般每个ISP(如一个居民区的ISP或一个机构的ISP)都有一台本地DNS服务器,当用户主机发出DNS请求时,该请求被发送给本地DNS服务器,它起着代理的作用,并将该请求转发到DNS服务器层次结构中。
DNS缓存用于改善时延,并减少DNS的报文数量。
在一个请求链中,当某DNS服务器接收到一个DNS回答(例如,包含主机名到IP地址的映射)时,它能将映射缓存到本地储存器中。下次遇到相同的请求可以直接返回结果。
为了保持记录的实时性,DNS服务器在一段时间后(通常设置为2天)将丢弃缓存的信息。
实现DNS分布式数据库的所有DNS服务器存储的实际上是资源记录(Resource Record, RR),资源记录是一个4元组:(Name, Value, Type, TTL)
如果一台DNS服务器是用于某特定主机名的权威DNS服务器,该服务器会有一条包含用于该主机名的类型A记录,如(relay1.bar.foo.com, 145.37.93.126, A)
如果一台DNS服务器不是用于某特定主机名的权威DNS服务器,是顶级域DNS服务器,该服务器将包含一条类型NS的记录,对应包含某特定主机名的域,如(umass.edu, dns.umass.edu, NS);还将包含一条类型A的记录,对应包含NS记录中Value字段的DNS服务器IP地址,如( dns.umass.edu, 128.119.40.111,A)
DNS协议只有查询和回答两种报文,且这两种报文有着相同的格式
前面提到DNS是一个分布式、分层的数据库,那数据库中的数据最初是怎么进入数据库的呢?
假设需要有一个新的创业公司,公司名为Network Utopia,该公司需要付费让注册登录机构登记机构注册域名networkutopia.com。注册登录机构会验证该域名的唯一性,并将该域名输入DNS数据库。
具体的,在注册该域名时,该公司需要向注册登录结构提供基本和辅助权威DNS服务器的名字和IP地址,对这两个权威DNS服务器的每一个,该注册登记结构确保将一个类型NS和一个类型A的记录输入TLD com服务器。
同时,还需要将用于Web服务器www.networkutopia.com的类型A资源记录和用于邮件服务器mail.networkutopia.com的类型MX资源记录输入权威DNS服务器。
完成以上步骤,就可以访问该公司的Web站点,并向公司的雇员发送电子邮件。
前面提到的应用(Web、电子邮件和DNS)都采用的客户-服务器体系,该体系极大依赖于总是打开的基础设施服务器。
P2P体系结构则对基础设施基础具有最小(或者没有)依赖,而是成对间歇连接的主机(称为对等方)彼此直接通信。
以文件分发为例,客户-服务器体系中,服务器需要向每个主机发送文件的一个副本,而P2P体系结构中,这些主机(对等方)能够向其他主机(对等方)重新分发它已经收到的该文件的任何部分,从而协助服务器。
还是以文件分发为例,服务器和对等方使用接入链路与因特网相连,并定义:
分发时间是所有N个对等方得到该文件的副本所需要的时间,以分发时间衡量文件分发的速度。
同时假设:因特网核心有足够的带宽,也因此所有的瓶颈都在网络接入链路。服务器和客户没有参与任何其他网络应用。
对于客户-服务器体系:
对于P2P体系结构:
BitTorrent是一个用于文件分发的P2P协议。
相关概念:
运行机制:
视频是一系列的图像,以一种恒定的速率(如每秒24或30张图像来展示)来展现。
视频一个重要的特征是能够被压缩,因此可以用**比特率(bit/s)**来衡量视频质量,比特率越高,图像质量越好,视觉感受越好。
对于流式视频最重要的性能度量是端到端吞吐量,为了连续不断地布局,网络必须为流式应用提供平均吞吐量,这个流式应用至少与压缩视频的比特利率一样大。
HTTP流
DASH
对于一个因特网视频公司,提供流式视频最直接的方法是建立一个单一的大规模数据中心,在数据中心中存储所有视频。但这样做有以下问题
当今主流的视频流公司都利用了内容分发网(Content Distribution Network, CDN),CDN管理多个地理位置上的服务器,并在这些服务器中存储视频副本,同时试图将每个用户请求定向到一个能提供最好用户体验的CDN位置。
CDN服务器的部署原则有两种:
并不是每个视频都会存在每个CDN集群中,CDN采用一种拉策略:如果客户像一个未存储请求视频的集群发送请求时,该集群检索该视频(从中心仓库或者另一个集群),向客户传输视频的同时在本地存储副本。
用户主机浏览器检索一个特定视频(由URL标识)时,CDN必须拦截请求,从而可以:
CDN实现的请求的拦截以及重定向的方法是DNS。
所谓集群选择策略,即动态地将客户定向到CDN中某个服务器集群或数据中心的机制。
常见的策略有:
Netflix视频分发具有两个主要部件:亚马逊云和自己专有的CDN基础设施
Netflix有一个完全运行在亚马逊云中的亚马逊服务器上的Web网站,同时亚马逊云还有以下关键功能:
Netflix有自己专用的的CDN,分别在IXP和它们自己的住宅ISP中安装了服务器机架。
Netflix不需要DNS来做重定向,而是依靠Netflix软件(运行在亚马逊云中)直接告诉客户使用一台特定的CDN服务器。
Netflix CDN使用推高速缓存而不是拉高速缓存,在非高峰时间段的预定时间将内容推入服务器。
类似于Netflix,使用谷歌专用的CDN来分发视频。同时使用拉高速缓存和DNS重定向技术。
Youtube使用的是HTT流,而不是DASH流,要求用户人工选择一个版本。
使用的是P2P交付而不是客户-服务器交付。当对等方请求一个视频时,联系跟踪器,一发现具有该视频副本的其他对等方,之后并行地从这些对等方请求该视频的快。
目前已经向混合CDN-P2P流逝系统迁移。大多数时候,客户请求来自CDN服务器的内容的开头部分,并且并行地从对等方请求内容。当P2P总流量满足视频播放,客户将从CDN停止流并只从对等方获得流。而当P2P流的流不够,则重新启动CDN连接。
前面提到过,网络应用程序有一对客户程序和服务器程序组成,它们运行在不同的端系统中。运行这两个程序时,创建了一个客户进程和服务器进程,两个进程之间通过从套接字读取和写入数据进行通信。
典型的网络应用成有两大类:
现在我们自己来实现一个专用的客户-服务器网络应用程序,并分别以运行在TCP和UDP上来实现该应用程序。该应用程序的功能很简单:
这里采用Pyhton3来实现该程序。
首先,需要说明的是,对于发送进程而言,需要为分组附上目的地地址(目的地主机IP和目的地套接字的端口号组成),同时发送方的源地址(源主机的IP地址和源套接字的端口组成)也要附在分组上,但这一步并不需要UDP程序代码实现,而是由底层操作系统实现。
现在具体看代码,我们称客户程序为UDPClient.py,称服务器程序为UDPServer.py。
以下是UDPClient.py的代码:
# 导入socket模块
from socket import *
# 这里的hostname可以是具体的IP地址,也可以是服务器的主机名
serverName = 'hostname'
# 选择12000作为服务器程序套接字的端口号
serPort = 12000
# 创建客户套接字,AF_INET指示底层网络使用IPv4,SOCK_DGRAM意味着是一个UDP套接字
# 这里没有指示客户端套接字的端口,这个由操作系统实现
clientSocket = socket(AF_INET, SOCK_DGRAM)
# 用户输入字符串
message = raw_input('Input lowercase sentence:')
# 将字符串转为字节,并指定目的地地址向进程的套接字clientSocket发送分组
clientSocket.sendto(message.encode(), (serverName, serverPort))
# 接受服务器的数据,当分组到达客户时,分组数据被放置在变量modifiedMessage中,其源地址被放置到serverAddress
modifiedMessage, serverAddress = clientSocket.recvfrom(2048)
# 将字节转为字符后打印
print(modifiedMessage.decode())
# 关闭套接字,关闭进程
clientSocket.close()
以下是UDPServer.py的代码:
from socket import *
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_DGRAM)
# 将12000端口号与服务器的套接字绑定
serverSocket.bind('', serverPort)
print('The server is ready to receive')
# 无限期接收并处理来自客户的分组
while(true)
# 当分组到达时,分组数据被放置在变量message中,其源地址被放置到clientAddress
message, clientAddress = serverSocket.recvfrom(2048)
# 字符串转换
modifiedMessage = message.decode().upper()
# 将子符串转为字节,并指定目的地地址向进程的套接字serverSocket发送分组
serverSocket.sendto(modifiedMessage.encode(), clientAddress)
首先说明TCP编程与UDP编程的区别:
现在具体看代码,我们称客户程序为TCPClient.py,称服务器程序为TCPServer.py。
以下是TCPClient.py的代码:
# 导入socket模块
from socket import *
# 这里的hostname可以是具体的IP地址,也可以是服务器的主机名
serverName = 'hostname'
# 选择12000作为服务器程序套接字的端口号
serPort = 12000
# 创建客户套接字,AF_INET指示底层网络使用IPv4,SOCK_DGRAM意味着是一个UDP套接字
# 这里没有指示客户端套接字的端口,这个由操作系统实现
clientSocket = socket(AF_INET, SOCK_DGRAM)
# 创建TCP连接
clientSocket.connect(serverName, serverPort)
# 用户输入字符串
message = raw_input('Input lowercase sentence:')
# 将字符串转为字节,并向进程的套接字clientSocket发送分组,不再需要指定目的地地址
clientSocket.sendto(message.encode())
# 接受服务器的数据,当分组到达客户时,分组数据被放置在变量modifiedMessage中
modifiedMessage = clientSocket.recv(1024)
# 将字节转为字符后打印
print(modifiedMessage.decode())
# 关闭套接字,关闭进程
clientSocket.close()
以下是TCPServer.py的代码:
from socket import *
serverPort = 12000
# 这里的serverSocket是欢迎套接字
serverSocket = socket(AF_INET, SOCK_DGRAM)
# 将12000端口号与服务器的套接字绑定
serverSocket.bind('', serverPort)
# 监听客户端TCP连接,定义请求连接的最大数
serverSocket.listen(1);
print('The server is ready to receive')
# 无限期接收并处理来自客户的分组
while(true)
# 创建一个连接套接字,由特定客户专用
connectionSocket, addr = serverSocket.accept()
sentence = serverSocket.recv(1024).decode().upper();
connectionSocket.send(sentence.encode())
# 关闭连接套接字
connectionSocket.close()