第二章 应用层
目录:
- 目标:
- 网络应用层原理:网络应用协议的概念和实现方面
- 传输层的服务模型
- 客户——服务模型
- 对等模型
- 内容分发网络
- 网络应用实例:互联网留校的应用层协议
- HTTP
- FTP
- SMTP/POP3/IMAP
- DNS
- 网络编程Socket API
- 提纲:
- 应用层协议原理
- Web and HTTP
- FTP*
- Email、SMTP、POP3、IMAP
- DNS
- P2P应用
- CDN
- TCP套接字编程
- UDP套接字编程
1 应用层协议原理
1.1 网络应用的体系结构
- C/S模式:客户端——服务器模式
- 服务器:
- 一直运行
- 固定的IP地址和周知的端口号(约定)
- 扩展性:数据中心进行扩展、扩展性差
- 客户端:
- 主动与服务器通信
- 与互联网有间歇性的连接
- 可能是动态IP 地址
- 不直接与其它客户端通信
- 服务器:
- P2P模式:对等体模式
- (几乎)没有一直运行的服务器
- 任意端系统之间可以进行通信
- 每一个节点既是客户端又是服务器(自扩展性强)
- 参与的主机间歇性连接且可以改变IP 地址(难以管理)
- C/S和P2P体系结构的混合体
- Napster
- 文件搜索:集中
- 主机在中心服务器上注册其资源
- 主机向中心服务器查询资源位置
- 文件传输:P2P
- 任意Peer节点之间
- 文件搜索:集中
- 即时通信
- 在线检测:集中
- 当用户上线时,向中心服务器注册其IP地址
- 用户与中心服务器联系,以找到其在线好友的位置
- 两个用户之间聊天:P2P
- 在线检测:集中
- Napster
1.2 进程通信
进程:在主机上运行的应用程序
在同一个主机内,使用进程间通信机制通信(操作系统定义)
不同主机,通过交换报文(Message)来通信
客户端进程:发起通信的进程
服务器进程:等待连接的进程
进程间通信形式:应用程序接口API (TCP/IP :socket API)
层间接口必须要携带的信息:
- 要传输的报文(对于本层来说:SDU)
- 谁传的:对方的应用进程的标示:IP+TCP(UDP) 端口
- 传给谁:对方的应用进程的标示:对方的IP+TCP(UDP)端口号
如果Socket API 每次传输报文,都携带如此多的信息,太繁琐易错,不便于管理,用个代号标示通信的双方或者单方:socket
TCP socket:
- 对于使用面向连接服务(TCP)的应用而言,套接字是4元组的一个具有本地意义的标示
- TCP服务,两个进程之间的通信需要之前要建立连接,两个进程通信会持续一段时间,通信关系稳定
- 可以用一个整数表示两个应用实体之间的通信关系,本地标示
- 穿过层间接口的信息量最小
- TCP socket:源IP,源端口,目标IP,目标端口
- 唯一的指定了一个会话(2个进程之间的会话关系)
UDP socket:
- 对于使用无连接服务(UDP)的应用而言,套接字是2元组的一个具有本地意义的标示
- UDP服务,两个进程之间的通信需要之前无需建立连接
- 每个报文都是独立传输的
- 前后报文可能给不同的分布式进程
- 因此,只能用一个整数表示本应用实体的标示,因为这个报文可能传给另外一个分布式进程
- 穿过层间接口的信息大小最小
- UDP套接字指定了应用所在的一个端节点(end point)
- 在发送数据报时,采用创建好的本地套接字(标示ID),就不必在发送每个报文中指明自己所采用的ip和port
- UDP socket:本IP,本端口
- 但是传输报文时:必须要提供对方IP,port,接收报文时: 传输层需要上传对方的IP,port
2 Web and HTTP
Web页:由一些对象组成,对象可以是HTML文件、JPEG图像、Java小程序、声音剪辑文件等。
Web页含有一个基本的HTML文件,该基本HTML文件又包含若干对象的引用(链接),通过URL对每个对象进行引用
2.1 HTTP概况
HTTP: 超文本传输协议
- Web的应用层协议
- 基于TCP
- 客户/服务器模式
- 客户: 请求、接收和显示Web对象的浏览器
- 服务器: 对请求进行响应,发送对象的Web服务器
- 无状态协议
2.2 HTTP连接
2.2.1 非持久HTTP
- HTTP1.0
- 最多只有一个对象在TCP连接上发送
- 下载多个对象需要多个TCP连接
- 每次连接都要关掉
- HTTP/1.0使用非持久连接
2.2.2 持久HTTP
- 多个对象可以在一个(在客户端和服务器之间的)TCP连接上传输
- 每次连接都会持久一段时间
- HTTP/1.1 默认使用持久连接
2.3 HTTP请求报文
- 请求行:
- 方法:GET、POST、HEAD、PUT、DELETE等
- URL:请求对象标识
- 版本:HTTP版本
- 首部行:
- Host:对象所在的主机
- User-agent:指明用户代理,即向服务器发送请求的浏览器的类型
- Connection:标识服务器发送完被请求对象后是否关闭这条连接
- Accept-language:浏览器支持的语言
2.4 HTTP响应报文
- 状态行:
- 版本:HTTP协议版本
- 状态码:HTTP响应状态码
- 短语:状态码对应状态信息
- 首部行:
- Connection:标识服务器发送完被请求对象后是否关闭这条连接
- Date:服务器产生并发送该响应报文的日期时间(服务器搜索到对象的时刻)
- Server:服务器的类型(类似User-agent)
- last-Modified:对象创建或者最后修改的时间(对象存储时要用到)
- Content-Length:被发送对象的字节数
- Content-Type:指示实体中对象的类型如text/html、image/webp
2.5 HTTP响应状态码
- 200 OK:请求成功,请求对象包含在响应报文的后续部分
- 301 Moved Permanently:请求的对象已经被永久转移了;新的URL在响应报文的Location:首部行中指定。客户端软件自动用新的URL去获取对象
- 400 Bad Request:一个通用的差错代码,表示该请求不能被服务器解读
- 404 Not Found:请求的文档在该服务上没有找到
- 505 HTTP Version Not Supported:服务器不支持请求报文使用的HTTP协议版本
2.6 用户-服务器状态:cookies
HTTP协议是无状态协议,故HTTP1.1 提供了cookies来维护状态
维护状态的协议很复杂!
- 必须维护历史信息(状态)
- 如果服务器/客户端死机,它们的状态信息可能不一致,二者的信息必须是一致
- 无状态的服务器能够支持更多的客户端
cookie组件:
- HTTP响应报文中的一个cookie首部
- HTTP请求报文中的一个cookie首部
- 客户端系统中保留一个cookie文件
- 位于web站点的一个后端数据库
2.7 Web缓存
缓存既是客户端又是服务器
通常缓存是由ISP安装(大学、公司、居民区ISP)
作用:
- 降低客户端的请求响应时间
- 可以大大减少一个机构内部网络与Internent接入链路上的流量
- 互联网大量采用了缓存:可以使较弱的ICP也能够有效提供内容
2.8 条件GET方法
目标:如果缓存器中的对象拷贝是最新的,就不要发送对象
缓存器: 在HTTP请求中指定缓存拷贝的日期
If-modified-since:<date>
服务器: 如果缓存拷贝陈旧,则响应报文没包含对象:
HTTP/1.0 304 Not
Modified
3 FTP: 文件传输协议
- 向远程主机上传输文件或从远程主机接收文件
- 客户/服务器模式
- 客户端:发起传输的一方
- 服务器:远程主机
- ftp: RFC 959
- ftp服务器:端口号为21
4 EMail
3个主要组成部分:
- 用户代理
- 邮件服务器
- 简单邮件传输协议:SMTP
4.1 用户代理
- 又名“邮件阅读器”
- 撰写、编辑和阅读邮件
- 如Outlook、Foxmail
- 输出和输入邮件保存在服务器上
4.2 邮件服务器
- 邮箱中管理和维护发送给用户的邮件
- 输出报文队列保持待发送邮件报文
- 邮件服务器之间的SMTP协议:发送email报文
- 客户:发送方邮件服务器
- 服务器:接收端邮件服务器
4.3 SMTP
- 使用TCP在客户端和服务器之间传送报文,端口号为25
- 直接传输:从发送方服务器到接收方服务器
- 传输的3个阶段
- 握手
- 传输报文
- 关闭
- 命令/响应交互
- 命令:ASCII文本
- 响应:状态码和状态信息
- 报文必须为7位ASCII码
工作流程:
- Alice使用用户代理撰写邮件并发送给
bob@someschool.edu
- Alice的用户代理将邮件发送到她的邮件服务器;邮件放在报文队列中
- SMTP的客户端打开到Bob邮件服务器的TCP连接
- SMTP客户端通过TCP连接发送Alice的邮件
- Bob的邮件服务器将邮件放到Bob的邮箱
- Bob调用他的用户代理阅读邮件
与HTTP比较:
相同点:
- 都用于一台主机向另一台主机传送文件:HTTP从Web服务器向Web客户传送文件;SMTP从一个邮件服务器向另一个邮件服务器传送文件。
- 当进行文件传送时,持续的HTTP和SMTP都是同持续TCP连接。
不同点:
- HTTP是一个拉协议,用户使用HTTP从服务器拉取信息;SMTP是一个推协议,一个邮件服务器把文件推向另一个邮件服务器。
- SMTP要求每个报文使用7比特ASCII格式,HTTP不受这种限制。
- 在处理包含图像等某提类型的文件,HTTP把每个对象封装到它自己的HTTP响应报文中,而SMTP则把所有报文对象放在一个报文中。
4.4 邮件报文格式
4.5 邮件访问协议
- SMTP: 传送到接收方的邮件服务器
- 邮件访问协议:从服务器访问邮件
- POP3:邮局访问协议(Post Office Protocol)[RFC 1939]
- 用户身份确认(代理<–>服务器) 并下载
- IMAP:Internet邮件访问协议(Internet Mail AccessProtocol)[RFC 1730]
- 更多特性(更复杂)
- 在服务器上处理存储的报文
- HTTP:Hotmail , Yahoo! Mail等
- 方便
- POP3:邮局访问协议(Post Office Protocol)[RFC 1939]
4.5.1 POP3(第三版邮局协议)
当用户代理打开一个到邮件服务器端口上的TCP连接后,POP3就开始工作了。工作流程分为三个阶段:特许、事务处理和更新。
- 特许:用户代理发送用户名和口令(明文形式)以鉴别用户。
- 事务处理:用户代理取回报文。还可以做以下操作:报文删除标记、取消报文删除标记以及获取邮件的统计信息。
- 更新:出现在用户发出quit命令后,目的是结束POP3会话。这时邮件服务器会删除那些被标记的报文。
4.5.2 IMAP(因特网邮件访问协议)
POP3协议只能将文件和报文下载存放在本地主机上管理;IMAP协议可以在服务器上创建文件夹并把报文存放在文件夹中。
- 当报文第一次到达服务器时,它与收件人的INBOX文件夹相关联。收件人可以将其移动到自己创建的文件夹进行阅读或删除。此外IMAP还提了远程文件夹中查询邮件的命令,IMAP服务器维护IMAP会话的用户状态信息。
- IMAP另一个重要特性是它具有允许用户代理获取报文组件的命令。只获取报文的某一部分。
4.5.3 基于Web(HTTP)的电子邮件
- 浏览器作为用户代理。
- 用户从邮件服务器获取邮件通过HTTP协议进行。
- 用户发送邮件到自己的邮件服务器时用的时HTTP协议。
- 发送邮件服务器发送邮件给接受邮件服务器用SMTP协议。
5 DNS(Domain Name System)
DNS协议则是用来将域名转换为IP地址(也可以将IP地址转换为相应的域名地址)。
- 问题1:如何命名设备
- 用有意义的字符串:好记,便于人类用使用
- 解决一个平面命名的重名问题:层次化命名
- 问题2:如何完成名字到IP地址的转换
- 分布式的数据库维护和响应名字查询
- 问题3:如何维护
- 增加或者删除一个域,需要在域名系统中做哪些工作
5.1 DNS(Domain Name System)总体思路和目标
- DNS的主要思路
- 分层的、基于域的命名机制
- 若干分布式的数据库完成名字到IP地址的转换
- 运行在UDP之上端口号为53的应用服务(事务性强,没必要TCP)
- 核心的Internet功能,但以应用层协议实现
- 在网络边缘处理复杂性
- DNS主要目的:
- 实现主机名-IP地址的转换(name/IP translate)
- 其它目的
- 主机别名到规范名字的转换:Host aliasing
- 邮件服务器别名到邮件服务器的正规名字的转换:Mail server aliasing
- 负载均衡:Load Distribution
5.2 DNS 工作机制
最简单的实现方式:集中式地使用一个DNS服务器
问题:
- 单点故障:如果这个服务器崩溃,整个互联网崩溃
- 通信容量:单个服务器不得不处理所有DNS查询
- 远距离的集中式数据库:如果只有一个服务器那么其他地方的主机离着就很远
- 维护:单个服务器存放着所有的数据,数据库庞大,而且新增数据时还得频繁更新
解决方式:
- 分布式、层次数据库
- DNS缓存
5.2.1 分布式、层次数据库
大致来说分为三类DNS服务器:
- 根DNS服务器:根名字服务器提供顶级域服务器的IP地址
- 顶级域DNS服务器:顶级域DNS服务器提供了权威服务器的IP地址。负责顶级域名(如com, org, net,edu和gov)和所有国家级的顶级域名(如cn, uk, fr, ca,jp )
- 权威服务器:组织机构的DNS服务器, 提供组织机构服务器(如Web和mail)可访问的主机和IP之间的映射。组织机构可以选择实现自己维护或由某个服务提供商来维护
本地DNS服务器:严格来说本地DNS服务器不属于服务器层次结构;由ISP提供;当主机与某个ISP连接时,该ISP提供一台DNS服务器的IP地址。
5.2.2 解析域名
主要有两种方式:
- 递归查询
- 迭代查询
5.2.2.1 递归查询
名字解析负担都放在当前联络的名字服务器上
问题:根服务器的负担太重
解决: 迭代查询(iterated queries)
5.2.2.2 迭代查询
根(及各级域名)服务器返回的不是查询结果,而是下一个NS的地址,最后由权威名字服务器给出解析结果
当前联络的服务器给出可以联系的服务器的名字
我不知道这个名字,但可以向这个服务器请求
5.2.3 DNS缓存
核心:缓存时为了性能,删除是为了一致
- 一旦名字服务器学到了一个映射,就将该映射缓存起来
- 根服务器通常都在本地服务器中缓存着,使得根服务器不用经常被访问
- 目的:提高效率
- 可能存在的问题:如果情况变化,缓存结果和权威资源记录不一致
- 解决方案:TTL(默认2天)
5.3 DNS记录和报文
资源记录(resource records)
- 作用:维护域名-IP地址(其它)的映射关系
- 位置:Name Server的分布式数据库中
RR格式: (domain_name, ttl, type,class,Value)
- Domain_name: 域名
- TTL: time to live : 生存时间(权威,缓冲记录)
- Class 类别:对于Internet,值为IN
- Value 值:可以是数字,域名或ASCII串
- Type 类别:资源记录的类型
- Type=A:域名到IP
- Name为主机
- Value为IP地址
- Type=NS:域名到权威服务器域名
- Name域名(如foo.com)
- Value为该域名的权威服务器的域名
- Type=CNAME
- Name为规范名字的别名
www.ibm.com
的规范名字为
servereast.backup2.ibm.com - value 为规范名字
- Name为规范名字的别名
- Type=MX
- Value为name对应的邮件服务器的名字
- Type=A:域名到IP
DNS协议:查询和响应报文的报文格式相同
5.4 维护问题:新增一个域
- 在上级域的名字服务器中增加两条记录,指向这个新增的子域的域名和域名服务器的地址,例:
(networkutopia.com, dns1.networkutopia.com, NS)
(dns1.networkutopia.com, 212.212.212.1, A) - 在新增子域的名字服务器上运行名字服务器,负责本域的名字解析: 名字==>IP地址
6 P2P 应用
没有(或极少)一直运行的服务器
任意端系统都可以直接通信
利用peer的服务能力
Peer节点间歇上网,每次IP地址都有可能变化
6.1 文件分发: C/S vs P2P
6.2 P2P文件分发: BitTorrent
文件被分为一个个块256KB
网络中的这些peers发送接收文件块,相互服务
6.2.1 工作过程
假设有一个新的对等方Alice加入一个洪流
- 追踪器(tracker)随机地从参与对等方的集合中选择对等方的一个子集(比如50个)并将这50个对等方的IP地址发送给Alice,Alice拥有了一个IP地址的列表
- Alice与列表上的所有对等方创建TCP连接,成为邻近对等方
- Alice周期性的询问每个邻近对等方他们所具有的块列表。
- 对当前自身没有的块信息,Alice发送请求获取(最稀缺的块,优先级最高请求)
- 向那些向她请求的块的邻居发送邻居没有的而自己有的块信息。
两个问题:
- 应该向邻居请求哪些块呢?
最稀缺优先 - 如何决定响应哪个邻居的请求?
BitTorrent
使用了一种对换算法(一报还一报)。该对换算法的基本思想是Alice根据当前能够以最高速率向她提供数据的邻居,给出其优先权。
操作过程如下:
- Alice对于她的每个邻居持续的测试接收到比特的速率,并确定以最高速率流入的4个邻居
- 每过10秒,她重新计算该速率并可能修改这4个对等方的集合
- 每隔30秒,Alice要随机选择另外一个邻居并向其发送块,也即Alie随机选择一名新的对换伴侣。
- 这种效果是对等方能够趋于找到彼此的协调的速率上载。随机选择邻居也允许新的对等方得到块,因此他们能够具有对换的东西,除了上诉5个对等方,其它对等方均被阻塞。
7 视频流和内容分发
7.1 因特网视频流
- 视频流量:占据着互联网大部分的带宽
- 挑战:规模性-如何服务者~1B 用户?不同用户拥有不同的能力(例如:有线接入和移动用户;带宽丰富和受限用户)
- 解决方案: 分布式的,应用层面的基础设施
- 编码:使用图像内和图像间的冗余来降低编码的比特数
- 空间冗余(图像内)
- 时间冗余(相邻的图像间)
7.2 DASH
DASH: Dynamic, Adaptive Streaming over HTTP
- 服务器:
- 将视频文件分割成多个块
- 每个块独立存储,编码于不同码率(8-10种)
- 告示文件(manifest file): 提供不同块的URL
- 客户端:
- 先获取告示文件
- 周期性地测量服务器到客户端的带宽
- 查询告示文件,在一个时刻请求一个块,HTTP头部指定字节范围
- 如果带宽足够,选择最大码率的视频块
- 会话中的不同时刻,可以切换请求不同的编码块(取决于当时的可用带宽)
7.3 CDN
Content Distribution Networks内容分发网络
通过CDN,全网部署缓存节点,存储服务内容,就近为用户提供服务,提高用户体验
通常CDN采取两种服务器安置原则:
- 深入(enter deep):将CDN服务器深入到许多接入网,更接近用户,数量多,离用户近,管理困难
- 邀请做客(bring home):部署在少数(10个左右)关键位置,如将服务器簇安装于IXP附近
7.3.1 操作过程
- 用户访问位于NetCinema的视频网站(
www.NetCinema.com
)。当用户点击视频链接http://video.netcinema.com/6Y7B23V
时,该用户主机发送了一个对于video.netcinema.com
的DNS 请求。 - DNS请求先请求本地DNS服务器LDNS
- 然后找到NetCinema权威DNS服务器,此时权威服务器并不会返回ip而是KingCDN域的主机名
al105.kingcdn.com
- 此时LDNS向
al105.kingcdn.com
发出DNS请求,KingCDN权威服务器再返回KingCDN内容服务器的IP地址给LDNS - LDNS告诉主机要访问的IP地址
- 主机与这个IP地址创建TCP连接
8 TCP套接字编程
8.1 请求流程
8.1.1 服务器端流程:
- 创建服务器套接字(ServerSocket)
- 将套接字绑定到一个本地地址和端口上(bind)
- 将套接字设定为监听模式,准备接受客户端请求(listen)
- 阻塞等待客户端请求到来。当请求到来后,接受连接请求,返迥-个新的对应于此客户端连接的套接字socketClient (accept)
- 用返回的套接字socketClient和客户端进行通信(I0流操作)
- 返回,等待另一个客户端请求(accept)
- 关闭套接字(close)
8.1.2 客户端流程:
- 创建客户端套接字(Socket)
- 向服务器发出连接请求(connect)
- 和服务器进行通信(I0流操作)
- 关闭套接字(close)
8.1.3 代码
package com.suo.webServer.tcp;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
public class MySocketClient {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("Usage: SimpleSocketClientExample <server> <path>");
System.exit(0);
}
String server = args[0];
String path = args[1];
System.out.println("Loading contents of URL: " + server);
try {
// Connect to the server
Socket socket = new Socket(server, 80);
// Create input and output streams to read from and write to the server
PrintStream out = new PrintStream(socket.getOutputStream());
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// Follow the HTTP protocol of GET <path> HTTP/1.0 followed by an empty line
// 发送请求
out.println("GET " + path + " HTTP/1.0");
out.println();
// Read data from the server until we finish reading the document
// 得到服务器响应的请求
String line = in.readLine();
while (line != null) {
System.out.println(line);
line = in.readLine();
}
// Close our streams
in.close();
out.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.suo.webServer.tcp;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class MyWebServer extends Thread {
private ServerSocket serverSocket;
private int port;
private boolean running = false;
public MyWebServer(int port) {
this.port = port;
}
public void startServer() {
try {
//绑定ServerSocket也就是连接套接字
serverSocket = new ServerSocket(port);
this.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public void stopServer() {
running = false;
this.interrupt();
}
@Override
public void run() {
running = true;
while (running) {
try {
System.out.println("Listening for a connection");
// Call accept() to receive the next connection
// 等待接受客户端的请求
Socket socket = serverSocket.accept();
// Pass the socket to the RequestHandler thread for processing
// 响应客户端的请求
RequestHandler requestHandler = new RequestHandler(socket);
requestHandler.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("Usage: SimpleSocketServer <port>");
System.exit(0);
}
int port = Integer.parseInt(args[0]);
System.out.println("Start server on port: " + port);
MyWebServer server = new MyWebServer(port);
// 开启服务器
server.startServer();
// Automatically shutdown in 1 minute
try {
Thread.sleep(60000);
} catch (Exception e) {
e.printStackTrace();
}
server.stopServer();
}
}
package com.suo.webServer.tcp;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
class RequestHandler extends Thread {
private Socket socket;
RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
System.out.println("Received a connection");
// Get input and output streams
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream());
// Write out our header to the client
out.println("Echo Server 1.0");
out.flush();
// Echo lines back to the client until the client closes the connection or we receive an empty line
String line = in.readLine();
// 规定只有”/“路径有资源其他路径没有资源
if(line != null && line.length() > 0) {
String[] s = line.split(" ");
if (!s[1].equals("/")) {
out.println("Echo: 404 Not Found");
out.flush();
line = null;
}
}
while (line != null && line.length() > 0) {
out.println("Echo: " + line);
out.flush();
line = in.readLine();
}
// Close our connection
in.close();
out.close();
socket.close();
System.out.println("Connection closed");
} catch (Exception e) {
e.printStackTrace();
}
}
}