tags:
- Network
Chapter 2: Application Layer
Network applications are the raisons d’être of a computer network—if we couldn’t conceive of any useful applications, there wouldn’t be any need for networking infrastructure and protocols to support them.
当我们有一个网络应用的点子时,我们得先明白现实世界中的网络应用是如何运行的。在网络应用最核心的开发任务就是编写能够跑在不同终端并使得它们可以经由网络核心(network core)互相通信的程序代码。在Web应用中,有两类应用不断地互相通信,一类是运行在用户host上的浏览程序(browser program),另一类是运行在Web服务器上的Web服务程序(Web server program)。
应用程序开发者的角度来看,网络架构是固定的(fixed),并且为应用程序提供了一套特定的服务。换句话说,当开发者创建应用程序时,他们可以依赖这个预先定义好的网络架构来构建功能,而不需要关心网络的底层细节。
从一个进程向另一个进程传输的报文都要经由下层网络。我们之前提到:计算机网络的下层是为上层的服务而存在的。在这里我们会有疑问,网络栈中 应用层 和 传输层 之间的接口是什么呢?这个处于应用和网络之间的软件接口API就叫做Socket——应用进程与传输层协议间的接口。
前文提到Socket是应用进程与传输层协议间的接口,应用进程的发送方将报文信息经由socket传给传输层协议——which has the responsibility of getting the messages to the socket of the receiving process. 因此,开发者在开发应用时必须选择一个传输层协议来保证报文从网络层向传输层的正常传递。
我们之前了解过,数据包packet在进程到进程传输过程中可能发生丢失、某些bit位出错的情况。在一些如e-mail、金融软件和文件传输的情况时,数据丢失可能造成严重的后果。为保证数据传输时的包完整度,一些协议提供这种可靠的信息传输。一旦传输层协议提供这种服务,发送进程即可将信息包传给socket且保证在信息传输过程中的不出错。
一些协议不提供这样的可靠性,这样的协议可能为那些loss-tolerant applications所用。比如一些音视频的多媒体应用,它们容许在信息传出中一定量的数据丢失。
吞吐率是用来衡量sending process能够向receiving process传输bit的速率大小。我们已经学习过在网络路径上可能不止一条通信会话,可能会有多个会话共享网络路径的带宽,随着其他会话的上线下线,可用带宽会随时间发生变化。这种现象自然地引出另一种传输层提供的服务——guaranteed available throughput at some specified rate。有这种服务的保证,应用可用要求一个最低r bits/sec的最小吞吐率,这对那些带宽敏感的应用是极为需要的。一些对吞吐率变化不敏感的应用——弹性应用(elastic application) 对这种保证性服务就没有这么需要了。
传输层协议也可以提供时间保证(time guaranteed)的服务。Timing guarantees 可以多种方式提供给应用进程,如guarantee might be that every bit that the sender pumps into the socket arrives at the receiver’s socket no more than 100 msec later.
传输层也会向应用提供一个或多个安全服务(Security services)。例如,在发送端,传输层可以加密(encrypt)发送进程所发送的所有信息,并在接收端的传输层解密(decrypt)后将信息传给接收进程。
The Internet(通常上是TCP/IP协议族网络),向应用提供两种传输协议——TCP和UDP。开发人员在开发网络应用时,最先考虑的问题之一就是应用应当使用TCP协议还是UDP协议。这两个协议中的每种协议都提供了不同的服务集。
TCP服务模型涵括了面向连接的服务和可靠数据传输的服务,当选用TCP作为其传输层协议时,应用就获得了TCP所提供的这两种服务。
相比TCP,UDP是”轻便“简洁的,它没有一些附加功能,是一种轻量的传输协议。
*Note that neither of these two protocol provides any securing encryption.Transport Layer Security (TLS) is Internet community developing for TCP enhancement. It's not provided by TCP
or UDP itself.
我们之前通过四个维度总结传输层协议提供的服务:可靠数据传输、吞吐率、timing、安全性。在前面谈论TCP和UDP所提供服务时都漏掉时间和吞吐率保证,这是因为现今的传输层协议不提供时间和吞吐率的保证服务。但这不意为着不能使用Internet拨打电话或视频通话。这是因为如今的互联网可以为time-sensitive application提供满意的服务,但不提供任何timing和throughput的保证。
我们已经了解到网络进程间的通信是通过socket这个API实现的。但我们仍不了解关于messages报文是如何构造的?报文中不同fields的含义是什么?进程何时发送报文?Application-layer protocol会告诉我们答案。一个应用层协议的具体定义如下:
一些应用层协议在RFC中能够找到详细定义,如HTTP;但也有一些应用层协议作为财产专利不公之于众,如Skype。
弄明白网络应用和应用层协议的区别很重要,应用层协议知识网络应用的一部分,我们举一个例子:
网络应用日新月异,第二章将以其中一小部分切入讲解:the Web, electronic mail, directory service, video streaming, and P2P applications。它们应用最广泛且易于理解。
90年代初期之前,网络只能做一些很简单的应用。后来,现象级的应用横空出世——World Wide Web。Web应用是第一个进入大众视线的Internet应用。Web的出现极大地改变了人们在工作环境内外的互动方式,使得互联网从众多数据网络中脱颖而出,成为了基本上唯一的全球性数据网络。
超文本传输协议(HyperText Tranfer Protocol) 是Web应用在应用层中的核心协议。HTTP完成两份程序——客户端程序和服务器程序,这些程序运行在不同的end system上并通过HTTP报文相互通信。HTTP规定了应用报文的格式和这些报文传输的方式。
在许多Internet应用中,client和server会彼此相互通信很长一段时间。在这一大段时间里,client发送一系列请求,server回应这一系列请求。因此,我们产生了一个疑问:这些请求-响应是应该通过多个不同的(separate)TCP连接发送?还是通过一个保持连接的(same)TCP连接发送?
HTTP的规定了HTTP报文格式(HTTP massage formats) 。HTTP报文有两种格式:(1)HTTP请求报文格式(2)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
。本例中请求操作方式是GET
,表示浏览器向服务器请求一个对象。Host: www.someschool.edu
指明了对象所在的host位置。Connection: close
表示浏览器告诉服务器在自己接收到目标报文后关闭TCP连接。User-agent: Mozilla/5.0
是发起此请求浏览器的种类。Accept-language: fr
向服务器指明用户期望得到一个法语版本的html对象。POST
操作时才会用到下面的entity body。下面我们给出一个简单的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 ...)
HTTP/1.1 200 OK
表示协议版本是HTTP/1.1,并且everything is OK。Connection: close
告诉客户机进程在发送完报文后就要关闭TCP连接了。Date:
头部行表示了服务器响应并发出对象报文的时间。Server:
头部行类似于User-agent
头部行,指出报文是被Apache/2.2.3 (CentOS)
所发出。Last-Modified:
头部行表示源服务器认为资源最近一次被修改的日期。Content-Length:
头部行声明了对象的大小,单位为byte。Content-Type:
头部行声明了对象的格式,在本例中是html格式的文本。我们前文提到,HTTP协议提供的是一个无状态的服务,这简化了服务器设计,提高了服务器的性能。然而由于user identification的需求,HTTP使用Cookies使网站能够保存用户的轨迹,以便提供面向用户的内容。
Web cache—也叫代理服务器(proxy server),是一种代表源Web服务器为HTTP请求提供服务。下图中演示了一个代理服务器的工作方式,当用户浏览器发送HTTP请求报文时,会先在Web cache中寻找是否存有请求对象,因此Web cache需要拥有自己的磁盘存储来拷贝保存最近被请求的对象。
If-Modified-Since:
头部行。则称这样的HTTP请求报文为conditional GET message。GET /fruit/kiwi.gif HTTP/1.1
Host: www.exotiquecuisine.com
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 ...)
GET /fruit/kiwi.gif HTTP/1.1
Host: www.exotiquecuisine.com
If-modified-since: Wed, 9 Sep 2015 09:23:24
HTTP/1.1 304 Not Modified
Date: Sat, 10 Oct 2015 15:39:29
Server: Apache/1.3.0 (Unix)
(empty entity body)
HTTP/2对于HOL的解决方案是将报文”打碎“为多个帧(frame),然后多个对象轮番发送,按一定顺序发送第一帧接着第二帧等。这样能够极大的缩短用户的感知延迟。将HTTP报文成帧并在在client端重组报文是HTTP/2最大的改进之一。头部字段划分成一个帧,entity body被划分成一个或多个帧。
在client向server发送多个请求时,它可以在权重1-256之间设置请求报文的响应优先级。数字越大表示优先级越高。
HTTP/2还有能力向一个client的一个请求发送多个响应报文,在发送完源请求对应的响应报文后server将额外的对象push给client。
□ 向用户主机上传输文件或从远程主机上接收文件
□ 客户机/服务器模式
在TCP/IP协议中, 需要两个端口,一个是数据端口,一个是控制端口。
控制端口一般为21,而数据端口不一定是20,这和FTP的应用模式有关,如果是主动模式,应该为20,如果为被动模式,由服务器端和客户端协商而定。相比于HTTP,FTP协议要复杂得多。复杂的原因,是因为FTP协议要用到两个TCP连接,一个是命令链路,用来在FTP客户端与服务器之间传递命令;另一个是数据链路,用来上传或下载数据。
□ FTP客户端与FTP服务器通过端口21联系,使用TCP作为传输协议
□ 客户端通过控制连接获得身份确认
□ 客户端通过控制连接发送命令浏览远程目录
□ 收到一个问价传输命令时,服务器打开一个到客户端的数据连接
□ 一个文件传输完成后,服务器关闭连接
□ 服务器打开第二个TCP数据连接用来传输另一个文件
□ 控制连接:带外(out of band)传送
□ FTP服务器维护用户的状态信息:当前路径、用户账户与控制连接对应
(即FTP服务器维护client的状态)
□ FTP 在控制连接上,信息以ASCII文本的方式传送
USER username
PASS password
LIST
:请服务器返回远程主机当前目录检索文件(gets)STOR filename
:向远程主机的当前目录存放文件(puts)331 Username OK,
password required
125 data connection already open;
transfer starting
425 Can't open data connection
452 Error writing file
Electronic mail has been around since the beginning of the Internet. It was the most popular application when the Internet was in its infancy.
在本节,我们将介绍Internet e-mail中最核心的应用层协议SMTP(Simple Mail Transfer Protocol) 。
SMTP(Simlple mail trasfer protocol)是互联网最早出现的应用层协议之一(它出现于1982年,比HTTP还早)。由于它出现的时间很早,考虑的事情不完全,也因此,它在当今是一种legacy technology。例如:SMTP规定在发送E-mail报文时要先将报文的body部分编码成7-bits ASCII,7位的ASCII显然不适用于现代多媒体信息的传送,只适用文本的传输,也体现了其过时的性质。
bob@someschool.edu
)和信件并操作user agent发送报文。S: 220 hamburger.edu
C: HELO crepes.fr
S: 250 Hello crepes.fr, pleased to meet you
C: MAIL FROM: <alice@crepes.fr>
S: 250 alice@crepes.fr ... Sender ok
C: RCPT TO: <bob@hanburger.edu>
S: 250 bob@hamburger.edu ... 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
HELO
、MAIL FROM
、RCPT TO
、DATA
、QUIT
。这些自解释性的命令我们很容易看明白。与HTTP一样,SMTP报文的每一行也是由CRLF结尾的(Carriage Return and Line Feed)。SMTP提供持久性的连接,因此,在我们没有发起QUIT请求前,我们可以一直用DATA命令向server发送多个email。telnet
命令来直接与server建立直接的会话,像这样:telnet servername 25
HELO yourDomain.com
MAIL FROM: <yourEmail@yourDomain.com>
RCPT TO:<recipientEmail@recipientDomain.com>
DATA
Subject: Test Email
This is a test email sent from Telnet.
.
QUIT
在向别人写信时,我们一般会在信纸上写上“给某某”和“爱你的某某”。SMTP中mail message的格式也一样,在真正写信之前,必须先写上头部行。每个message头都必须包含From:
头部行和To:
头部行,Subject
头部行时可以舍去的。
SMTP的massage header可能是这样的:
From: alice@crepes.fr
To: bob@hamburger.edu
Subject: Searching for the meaning of life.
通过之前对SMTP的学习,我们可能会有mail server存在意义何在的疑问。确实,信件总是先被传送到mail server后再转发给用户,总会有让用户有信息安全方面的担忧。但是,让用户时时刻刻保持在线连网的状态对很多人而言是不现实的。因此,我们在发电邮时会先将email发到一个always-on shared mail server中。
Just as humans can be identified in many ways, so too can Internet hosts.
在Internet中,有两种方式来识别一个主机:(1)通过hostname;(2)通过IP地址;可以理解为human name和human ID number。人们当然更喜欢便于记忆的hostname,但是router路由时当然喜欢定长结构化的IP地址进行路由。这也就是DNS的主要工作。
在Internet中,DNS是(1)不同层次的DNS服务器组成的分布式数据库;(2)允许hosts查询分布式数据库的的应用层协议。
通常情况下,DNS运行在BIND(Berkeley Internet Name Domain)软件上,DNS一般采用UDP传输层协议,使用53号端口。
DNS作为一个工具,常常被同为应用层的协议如:HTTP、SMTP所使用。下面举例简单说明一下DNS是怎么工作的。
假设browser要请求URL
我们从上面例子中看到,DNS提供IP地址解析的服务为应用添加了额外的时延。但和Proxy Server会缓存HTTP报文一样,结构化的DNS服务器也同样会将DNS的相关数据cache到临近client host的DNS server。
从此我们可以看到,DNS提供的服务不仅仅只是IP地址和hostname之间的转换。还有:
bob@yahoo.com
。我们看到的yahoo的邮件服务器主机名是简单的yahoo.com
,但它的规范主机名可能会是relay1.west-coast.yahoo.com
。显然,yahoo.com
更容易记忆。我们已经概况的了解了DNS提供服务的,本节我们来看看DNS所提供的 主机名-IP地址 映射服务。假设某些应用需要用到这样的服务,他会先将需要转换的信息给客户端的DNS,然后DNS完成转换工作。
在很多UNIX-based的机器上,系统会提供gethostbyname()
函数封装转换服务。随后用户机上的DNS会接管,向网络送出query message。所有的DNS问询和其收到的回复报文都会被53号端口通过UDP数据报的方式传输。经过一段时间的时延后DNS收到映射后的内容。之后,这段内容会被传给应用。应用所看到的只是一个黑盒子,将主机名(IP地址)丢进去,黑盒会返回应用想要的IP地址(主机名)。但事实上,这”个“黑盒子涵括了分布在全球成千上万的DNS服务器和指定这些服务器通讯的应用层协议。
对于DNS,最简单的设计思想可能就是中心化的设计了!即让一个DNS服务器记录所有的主机名-IP地址映射关系。但我们也很容易发现其中的弊端:
- A single point of failure
- Traffic volume
- Distant certralized database
- Maintenance
而DNS数据库设计是分布式的、层次化的。但中心化的单一DNS服务器just doesn't scale。
DNS是一个浩大的工程,分布式、层次化的DNS服务器遍布世界各地。得益于这种扩展性(scale),没有一个DNS服务器需要记录全部的 主机名-IP地址 映射关系,主机名-IP地址的映射分布交叉地存放在各个DNS服务器中。
我们用下图来近似地描述这种分布式、层次化的关系,我们可以看到三类DNS服务器:root DNS 服务器、top-level domain(TLD) DNS服务器 和 authoritative DNS服务器。
根DNS服务器:世界上有13个IPv4的根服务器,为12个组织所管理,但是为了保证根域名服务器的可用性,会部署多个节点。因此也有说全球根服务器的数量在1000+左右(截至2020)。根域名服务器能够提供 TLD服务器的 IP地址。
顶级域服务器:TDL服务器管理并提供特定TLD的DNS服务。这些顶级域名有:com
、org
、net
、edu
、gov
,还有国家TLD如:uk
、cn
、jp
等。顶级域名由不同的公司或组织所拥有和管理,如Educause管理edu
TLD。TLD服务器提供权威DNS服务器的IP地址。
权威DNS服务器:权威DNS服务器记录存储特定域名的官方DNS,记录主机名和IP地址之间的映射关系。任何提供公开访问主机的组织都需要应用权威DNS服务器。对于这些组织来说,它们可以自己搭建服务器或交给特定的服务提供商。大多数的大学和大型公司都有自己的一级和二级权威DNS服务器(备份)。
从DNS服务器层次结构中我们看到root、TLD还有authoritative DNS服务器。但在DNS服务器层次结构外还有一种十分重要的DNS服务器,叫做local DNS server。尽管严格来说local DNS server并不属于architecture,但却处在architecture的C位。每个ISP都会有这样的local DNS server(也叫default name server)。下面我们演示local DNS server是怎么工作的(假设主机cse.nyu.edu
想要获得主机gaia.cs.umass.edu
的IP地址):
cse.nyu.edu
向local DNS server发送解析gaia.cs.umass.edu
的查询报文;(query)edu
返回负责该顶级域名TLD服务器的IP地址列;(reply)umass.edu
后缀返回authoritative DNS 服务器的IP地址;(reply)dns.umass.edu
发生请求报文;(query)gaia.cs.umass.edu
的IP地址;(reply)cse.nyu.edu
。(reply)之前,我们假设TDL 服务器默认知道目标hostname在哪个authoritative DNS 服务器的情况,但现实中不全是这样的。可能的情况是,TLD服务器只认得一些知道authoritative DNS 服务器hostname的intermediate DNS 服务器在哪。这样,local DNS 服务器就需要额外的DNS通讯才能找到最终的authoritative DNS 服务器。(额外 N个intermediate DNS 服务器产生额外 2N个DNS messages)
Recurive queries and iterative queries
The example shown in Figure 2.19 makes use of both recursive queries and iterative queries. The query sent from cse.nyu.edu
to dns.nyu.edu
is a recursive query, since the query asks dns.nyu.edu
to obtain the mapping on its behalf. However, the subsequent three queries are iterative since all of the replies are directly returned to dns.nyu.edu
. In theory, any DNS query can be iterative or recursive. For example, Figure 2.20 shows a DNS query chain for which all of the queries are recursive. In practice, the queries typically follow the pattern in Figure 2.19: The query from the requesting host to the local DNS server is recursive, and the remaining queries are iterative
在上面的例子中,我们看到,请求DNS服务的client向知道目标主机 hostname/IP 映射关系的authoritative server请求DNS服务时,DNS messages的数量会随着intermediate DNS 服务器结点的增加而增加:
DNS caching的思想十分简单,和HTTP中的proxy server(Web cache)的思想类似,都是通过保存近期请求的数据来减少未来请求的处理时间和网络流量。这种缓存机制在提高DNS解析速度和减少网络拥塞方面起着至关重要的作用。
在下面的例子中,在Requesting host首次请求查询gaia.cs.umass.edu
的 IP 时,local DNS 服务器会将主机gaia.cs.umass.edu
的 hostname/IP 映射关系缓存到本地。如果另一台主机也有查询gaia.cs.umass.edu
的 IP 的话,local DNS server会直接将缓存在本地的 IP 地址转发给requesting host,即只需要2步。
DNS 服务器共同组成了存放Resource Records(RRs) 的DNS 分布式数据库。每个DNS reply message 都包含着至少一个的资源记录(RRs)。
RRs是一个包含以下字段(fields)四元组(four-tuple):
下面,我们看看 RRs 的其余三个字段的含义。其中,字段Name
和字段Value
的含义取决于Type
字段:
Type=A
,这时,Name
就是一个主机名 并且 Value
是主机的 IP 地址。因而,Type A 记录提供 标准的 hostname/IP 映射关系, (relay1.bar.foo.com, 145.37.93.126, A) 就是一个Type A的记录。(当Type=AAAA
表示正在查询域名的IPv6地址)Type=NS
,这时,Name
就是一个域名(domain) 并且 Value
是知道如何获得域名内主机 IP 地址的authoritative DNS server的主机名。(foo.com, dns.foo.com, NS)就是一个Type NS的记录。这种记录告诉其他DNS服务器如果需要解析foo.com
域内的任何主机名,应该查询dns.foo.com
这个权威DNS服务器。Type=CNAME
,那么 Value
就是一个规范主机名(canonical hostname) ,这时的 Name
字段用来表示主机别名 这种记录向querying hosts提供主机的规范主机名。(foo.com, relay1.bar.foo.com, CNAME)
就是一种CNAME记录。Type=MX
,则 Value
表示mail server的规范名 而 Name
表示mail server的别名。(foo.com, relay1.bar.foo.com, CNAME)
就是一种 CNAME 记录。RRs 不同的Type代表着不同的含义,不同服务器中记录的RRs的种类也不尽相同。在authoritative DNS 服务器中,存储的是Type A的RRs;在其他DNS 服务器如TLD服务器中存放着Type为NS的RRs。
在之前的Figure 2.19中,我们知道DNS query messages 和 reply DNS messages 是如何传输的。我们看到两种DNS message,但事实上,不论是query messages 还是 reply messages 都使用同一DNS报文格式。DNS报文格式如下:
举一个DNS querying message的例子:
Header | Questions |
---|---|
Transaction ID: 0x9ad0 | Name: www.baidu.com |
Flags: 0x0100 (Standard query) | Type: A (Host Address) |
Questions: 1 | Class: IN (Internet) |
Answer RRs: 0 | |
Authority RRs: 0 | |
Additional RRs: 0 |
我们现在再看看DNS reply message的例子:
Header Sect. | Questions Sect. | Answers Sect. |
---|---|---|
Transaction ID: 0x9ad0 | Name: www.baidu.com | Name: www.baidu.com |
Flags: 0x8180 (Standard query response, No error) | Type: A (Host Address) | Type: A (Host Address) |
Questions: 1 | Class: IN (Internet) | Class: IN (Internet) |
Answer RRs: 1 | TTL: 86400 | |
Authority RRs: 0 | Data length: 4 | |
Additional RRs: 0 | Address: 123.125.114.144 |
nslookup program
nslookup
来获取域名和IP地址之间的映射。我们已经了解了如何从DNS distributed database中获取我们想要的信息。但当我们创立一个公司后,我们需要拥有自己的网站来宣传自己的产品。我们应该怎么做呢?(假设我们创立商业公司,也就是TLD为com
)
com
的服务器中。在之前的章节中,我们已经学习过的应用层应用有:Web应用、e-mail和DNS,他们都是基于C/S 体系结构。这些应用对于一个时刻响应的服务器的需求是时时刻刻都存在的。但Peer-to-peer网络体系结构对这样的服务器的依赖是很少的,由于每个终端设备(也叫节点)都可以处理请求且资源分散在各个节点上的,因此每个节点既可以作为客户端,也可以做服务器。
在本节中,我们将了解当今最流行的P2P file distribution protocol——BitTorrent。我们之前已经了解过,在C/S网络体系结构中,资源只保存在服务器端,资源的传输方向只能是 server->client。这样,服务器频繁的传输资源会对服务器带来严重的负担。而在P2P网络体系结构中,资源可以分布地存储在各个peer中,每个peer都可以是资源的请求者,也可以是资源的提供者。
为了展现P2P体系结构在传输文件上的优越性,我们用下图的例子来演示并计算每种architecture的分发时间。
我们假设服务器接入链路(access link)的上传速率为
假设采用C/S体系结构的分发时间是
当网络采用P2P体系结构时,情况则大有不同!因为每个对等体peer都可以辅助服务器分发文件,本着人多力量大的原则,随着对等体越来越多,分发时间不会像C/S网络那样线性增加。
下面,我们用计算来进一步观察在P2P网络下的分发时间的变化。
将这三部分合在一块,我们就会得到P2P的分发时间:
BitTorrent 是一个流行的 P2P(点对点)文件分发协议。在 BitTorrent 的术语中,参与特定文件分发的所有节点(peers)的集合被称为一个 “torrent”。在 torrent 中的节点互相下载文件的等大小块(chunks),典型的块大小是 256 KB。当一个节点首次加入 torrent 时,它没有任何块。随着时间的推移,它会逐渐积累越来越多的块。在下载块的同时,它也会上传块给其他节点。一旦一个节点获得了整个文件,它可以选择离开 torrent,或者留在 torrent 中继续向其他节点上传块。此外,任何节点都可以在只有部分块的情况下随时离开 torrent,并且之后可以重新加入 torrent。
BitTorrent 是一个复杂的系统,但我们可以通过其最重要的机制来理解它的基本原理。每个 torrent 都有一个叫做 tracker 的基础设施节点。当一个 peer 加入 torrent 时,它会在 tracker 中注册,并定期通知 tracker 它仍然在 torrent 中。这样,tracker 就知道 peers 的数量。
当一个新的 peer,比如 Alice,加入 torrent 时,tracker 会从参与的 peers 中随机选择一部分(比如说50个)并将这些 peers 的 IP 地址发送给 Alice。Alice 拥有了这个 peers 列表后,会尝试与列表上的所有 peers 建立并发的 TCP 连接。我们称 Alice 成功建立 TCP 连接的 peers 为 “neighboring peers”。随着时间的推移,一些 peers 可能会离开,而其他的 peers(不在最初的50个之内)可能会尝试与 Alice 建立 TCP 连接。因此,一个 peer 的neighboring peers 的数量会随时间波动。
在任何给定的时间点,每个 peer 都会拥有文件的一部分 chunks,不同的 peers 拥有不同的 chunks 子集。Alice 会定期向她的neighboring peers 请求他们拥有的 chunks 列表。如果 Alice 有 L 个不同的neighbor,她将获得 L 个 chunks 列表。知道这些后,Alice 可以请求她当前没有的 chunks。
因此,在任何时候,Alice 都会拥有一部分 chunks,而且知道她的邻居拥有哪些 chunks。有了这些信息,Alice 需要做出两个重要的决定:首先,她应该首先向邻居请求哪些 chunks? 其次,她应该向哪些邻居发送请求的 chunks?在决定请求哪些 chunks 时,Alice 使用了一种叫做 “rarest first” 的技术。Rearest first 用来确定在她没有的 chunks 中,哪些 chunks 是在她的邻居中重复次数最少的 ,然后最先请求这些最稀有的 chunks。这样,最稀有的 chunks 能更快地被重新分配,目的是(大致)使 torrent 中每个 chunks 的副本数量均衡。
为了确定她应该回应哪些请求,BitTorrent 使用了一个巧妙的“交易”算法。基本思想是 Alice 优先回应那些当前以最高速率向她提供数据的邻居。具体来说,对于她的每个邻居,Alice 持续测量她接收比特的速率,并确定四个以最高速率向她提供比特的 peers。然后她通过向这四个相同的 peers 发送 chunks 来回报他们。每10秒,她会重新计算速率,并可能修改这四个 peers 的集合。在 BitTorrent 术语中,这四个 peers 被称为 unchoked peer。重要的是,每30秒,她还会随机选择一个额外的邻居并向其发送 chunks。我们称这个随机选择的 peer 为 Bob。这时的 Bob 就是 optimistically unchoked peer。因为 Alice 向 Bob 发送数据,她可能会成为 Bob 的前四个上传者之一,这样 Bob 就会开始向 Alice 发送数据。如果 Bob 向 Alice 发送数据的速率足够高,Bob 可能会反过来成为 Alice 的前四个上传者之一。换句话说,每30秒,Alice 会随机选择一个新的交易伙伴并开始与该伙伴交易。如果两个 peers 对交易感到满意,他们会将对方放入他们的前四个列表中,并继续彼此交易,直到其中一个 peer 找到更好的伙伴。这样,能够以兼容速率上传的 peers 往往能够找到彼此。随机邻居选择也允许新的 peers 获得 chunks,这样他们就有东西可以交易。除了这五个 peers(四个“顶级” peers 和一个探测 peer)之外的所有其他邻居 peers 都被 “choked”,即他们不会从 Alice 那里接收任何 chunks。BitTorrent 还有许多其他有趣的机制,这里没有讨论,包括 pieces(小 chunks)、pipelining、random first selection、endgame mode 和 anti-snubbing。
刚才描述的交易激励机制通常被称为 tit-for-tat。尽管已经有研究表明这种激励方案可以被规避,但 BitTorrent 生态系统仍然非常成功,有数百万个同时在线的 peers 在数十万个 torrents 中积极分享文件。如果 BitTorrent 没有设计 tit-for-tat(或其变体),即使其他方面完全相同,BitTorrent 都可能不会存在,因为大多数用户只想作为数据的接收者。
最后,我们简要提及 P2P 的另一个应用,即分布式哈希表(DHT)。分布式哈希表是一个简单的数据库,数据库记录分布在 P2P 系统的 peers 中。DHT 已被广泛实施(例如,在 BitTorrent 中)并且是广泛研究的主题。在配套网站的视频笔记中提供了一个概述。
By many estimates, streaming video account for about 80% of Internet traffic in 2020. [Cisco 2020]
本节课中,我们会简单了解当今网络世界中的视频流服务是怎么实现的。我们会看看这些 streaming video 是如何用应用层协议和一些 cache-like 的服务实现的。
在流存储视频应用(streaming stored video applications)中,underlying medium 就是提前录制好的视频。这些视频会被放置在服务器中,如何client向服务器发送需要视频的请求报文。
那么什么是 video medium 呢?我们要先明白视频就是一连串的图片,我们常常听到24帧、30帧60帧的视频。这表示视频每秒钟会显示60帧(张)的图片。而这些图片又是由一连串的像素构成的,这些像素由可以用表示亮度和颜色的 bits 来表示。这是压缩(compression)的过程,也是视频的特点之一 —— 将视频文件压缩成任何所需的比特率(bit rate)。比特率越高视频的质量越高。
从网络的视角来看,video 最重要的特征可能就是比特率了。一般来说,压缩后网络视频的比特率通常在100Kbps 到 4Mbps。如果想要传输4K以上的视频,那么至少需要10Mbps的比特率,这样会占用大量的网络和存储资源。为保证视频持续稳定的传输,网络就必须提供比压缩视频比特率还大的网络吞吐量(throughput)。
如果端到端的吞吐量小于压缩视频的比特率,我们还可以将源视频压缩成不同比特率的版本,中断设备根据当前网络吞吐量来选择多大比特率的版本。这也就是为什么视频网站上会存在144p、480p、1080p、2k、4k多个版本的视频供用户选择。
当视频采用HTTP媒体流(HTTP streaming)传输时,视频会存放在HTTP服务器的一个URL处。当用户想要观看视频时,client会建立与server的TCP连接并通过HTTP GET
方式来获取这个视频。之后,server会将视频文件包含在一个响应报文中发给client。在client端,压缩后的视频(bits)会先存放在client application buffer中。一旦buffer中的字节数量到预设阈值的时候,client端的应用就会开始播放视频。
尽管HTTP streaming 得到极为广泛地应用,但它仍然有一个缺点:即所有的client都会收到同样比特率的视频。这样的特点没有充分考虑到不同client目前可用带宽的大小。因此也促成 DASH(Dynamic Adaptive Streaming HTTP)技术 的发展。动态自适应流媒体技术将视频编码成多个比特率的版本,client会动态地请求一段长度的视频片段。根据目前的带宽选择响应比特率版本的视频。
DASH技术的出现使不同网络状况的clients能够在不同编码率下享受流媒体视频中的内容。同时DASH也支持在会话中根据网络状况动态地选择不同比特率版本。
有了DASH,每种不同比特率版本的视频都将存放在HTTP的服务器中,每种视频都与单一的URL相对应。而这些URL信息可以在HTTP服务器中的 清单文件(manifest file) 中看到。
我们最后再来在宏观上看看video是如何在server-client上传输的:
GET
方法请求想要的video文件的一个chunk;当今,许多流媒体视频公司需要将这些视频内容分布式地放在世界各地来时时刻刻为百万计的用户提供稳定的视频播放。
那么如何为这些用户提供稳定的视频播放呢?我们可以建造一个巨型的数据中心(mega data center),将所有的视频都存放在数据中心里。但这样做会造成很多问题:
为应对百万计clients的视频流请求,现在大部分公司都会采用Content Distribution Networks(CDNs)。内容分发网络通过管理分布在不同位置的服务器,将video、其他Web内容拷贝到不同的服务器(服务器农场)中。每当有用户发起内容请求,CDN就会让用户与最合适的data center进行通信,以获得最好的体验。CDN可以是私有(Private CDN)的,只为内容提供商所有。CDN也可以是第三方的(third-party CDN),由多个内容提供商分发内容。
A very readable overview of modern CDNs is [Leighton 2009; Nygren 2010].
CDNs的分布有两种不同的设计哲学:
我们做假设来理解这两种策略:假设圆的内部是network core,圆上是network edge。那么在enter deep方式中,CDN靠近一个圆的圆心,它平等的到每个client都近。而bring home更像是将CDN放在圆上,这样对一部分用户更近,但是对另外一些用户来说却更远。
了解完CDNs的两种分布策略之后,我们现在看看CDN是任何发挥作用的。当client host想要获得一个video时,CDN必须拦截(intercept)该请求并根据请求(1)决定此时刻最适合该client的CDN服务器集群;(2)将client的请求引导发送到那个集群中的服务器。
下面,我们用一张图直观地理解CDN的作用。我们假设有一个内容提供商(NetCinema)从 KingCDN 租用了一个第三方的CDN来完成其视频分发。在 NetCinema 网页中,每个视频都对应一个URL与其对应。在用户想要观看某个视频且视频流被送到用户的host上前,发生了以下事件:
video.netcinema.com
DNS query 报文;xxxxx.kingcdn.com
交给 LDNS。xxxxx.kingcdn.com
的 DNS query 之后 LDNS 收到 KingCDN 的内容服务器的IP地址。GET
来获得想要的内容。如果使用 DASH ,服务器还会先将包含一连串URLs的清单文件发送给 client host ,然后 client host 通过目前的网络状态动态地选择需要的视频chunk文件。在CND分发内容的时候,往往要考虑怎么选择合适的 cluster selection strategy。集群选择策略是在client host 进行查询时,CDN通过 LDNS 的IP地址来为其推荐最合适的服务器集群(cluster)。
CDN往往采用专有的集群选择策略。一种笨想的策略就是 geographically closest,就是根据物理距离的长短让距离 client host 最近的服务器集群(也就是数据中心)负责向 client host 传输内容信息。Content provider 会使用商业性的 geo-location databases 来获取地理位置上的信息。这种方式对与大多数用户而言都是相对较优的,但对 “Geographically close, driven far” 的情况来说,这种策略的体验可能会很差。因为地理距离上的cluster可能在跳数(number of hops)上可能不是最小的。
Tip*在下图的例子就很好地说明了这个问题,虽然两地相距120+km,但是开车确需要1300+km。
为了解决这种问题,基于client host当前的网络状况来选择最佳的集群,CDNs可以使用 real-time measurements 策略周期性地检查与client host之间的延时(如使用ping
命令)等来选择cluster。
2.6.4小节作为了解性内容。
回顾2.1节,我们讨论了由两部分组成的网络应用——客户端程序和服务器程序——运行在两台end systems上。当程序一旦被运行,客户端进程和服务器进程就会被创建,然后这些进程靠sockets进行客户端-服务器之间的读、写操作。要进行这样的客户机-服务器间的通信,程序员就要编写客户端程序和服务器端程序。
我们可以使用现成的[RFC]文档或其他开源的标准文档提供的协议标准来完成我们的网络应用程序(如使用HTTP[RFC 2616]来作为服务器和客户端程序的指导文档)。依这种方式,客户端程序和服务器程序的读、写操作的实现就需要安装文档规定的方式实现。
之外,我们还可以使用自己规定的读写接口协议来实现我们的客户端程序和服务器程序。这种方式下实现的网络应用往往是闭源私有的。
我们再回顾TCP和UDP的特点。我们知道,TCP是面向连接(connection oriented) 的网络层协议,为两台终端设备之间提供可靠的字节流信道。
而UDP是无连接的,它提供不可靠的、以独立传输包(independent packets)方式传输的,对传输过程中发生的意外不做任何保证。
我们将主机比做街道,应用进程比作房子,进程的端口号/sockets用房子的门牌号来表示,模拟数据包传输为快递运送的过程。
我们来模拟一次数据包传输的过程:现在有一个快递包需要从房子A邮递到房屋B,我们需要在快递包上写很多邮寄信息,如送到哪个街道(主机)的哪个房子(进程)门牌号(端口号)是多少。快递员会根据这些信息将快递包送到指定的房子。
作为生活在房子里面的程序员,我们完全有随意布置房间和发送什么快递包的权力。但是我们不能影响到房间外快递员是如何配送包裹的。这也就是我们所说的开发者只对socket接口的application-layer side有完全的掌握,但对传输层一侧几乎没有控制。
如果还是不理解,不妨从网络视角再看看数据包传输的问题。在网络中,路由器会解析IP地址并将数据转发到正确的主机上,而主机上有许多进程,一个进程又可以有多个sockets,因此我们需要端口号来对这些socket进行标识。因而,有了IP地址和端口号,我们就可以将数据包送去目标主机了。
我们用下面的客户机-服务器应用的规则来演示一下UDP和TCP上的socket编程:
我们在下图中用高亮表示客户端-服务器基于UDP传输服务上的相关socket活动:
原书用 python 进行应用端对传输层协议的选择配置编程。相比 C++更容易理解和学习。
#include <iostream> // 引入输入输出流库,用于控制台输入输出。
#include <cstring> // 引入字符串处理库,提供字符串操作函数。
#include <sys/socket.h> // 引入套接字接口库,用于网络通信。
#include <arpa/inet.h> // 引入用于IP地址转换的库。
#include <unistd.h> // 引入POSIX操作系统API库。
#define PORT 8080 // 定义服务器监听的端口号为8080。
#define BUFFER_SIZE 1024 // 定义缓冲区大小为1024字节。
int main() {
int sockfd; // 套接字文件描述符,用于标识创建的套接字。
char buffer[BUFFER_SIZE]; // 缓冲区数组,用于存储输入的消息和服务器的响应。
struct sockaddr_in server_addr; // 服务器地址结构体,用于存储服务器的地址信息。
// 创建UDP socket
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed"); // 如果创建套接字失败,打印错误信息。
exit(EXIT_FAILURE); // 退出程序。
}
// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr)); // 初始化服务器地址结构体。
server_addr.sin_family = AF_INET; // 设置地址族为IPv4。
server_addr.sin_port = htons(PORT); // 设置端口号,htons用于将主机字节序转换为网络字节序。
server_addr.sin_addr.s_addr = INADDR_ANY; // 设置IP地址为任意地址,即本机的任意IP。
while (true) { // 无限循环,直到程序被手动停止。
std::cout << "Enter a message: "; // 提示用户输入消息。
std::cin.getline(buffer, BUFFER_SIZE); // 从标准输入读取一行数据到缓冲区。
std::cout << "Message has been loaded! "<< std::endl;
// 发送数据到服务器
sendto(sockfd, buffer, strlen(buffer), 0, (const struct sockaddr *)&server_addr, sizeof(server_addr));
std::cout << "Message has been sent! "<<std::endl;
// 接收服务器的响应
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, nullptr, nullptr); // 接收服务器响应的数据。
buffer[n] = '\0'; // 在消息末尾添加字符串结束符。
std::cout << "Server response: " << buffer << std::endl; // 打印服务器的响应。
}
close(sockfd); // 关闭套接字。
return 0; // 程序正常退出。
}
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <algorithm>
#define PORT 8080
#define BUFFER_SIZE 1024
void to_uppercase(char* str) {
std::transform(str, str + strlen(str), str, ::toupper);
}
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
// 创建UDP socket
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 绑定socket到地址
if (bind(sockfd, (const struct sockaddr *)&
server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
while (true) {
memset(buffer, 0, BUFFER_SIZE);
// 接收数据
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len);
buffer[n] = '\0';
std::cout << "Received: " << buffer << std::endl;
// 转换为大写
to_uppercase(buffer);
// 发送修改后的数据
sendto(sockfd, buffer, strlen(buffer), 0, (const struct sockaddr *)&client_addr, addr_len);
std::cout << "Sent: " << buffer << std::endl;
}
close(sockfd);
return 0;
}
与UDP提供的无连接服务不同,TCP提供有连接的服务。所以,在客户端和服务器端进程相互传递信息前,我们需要先通过握手(handshakes)建立TCP连接。然后用客户端connection socket和服务器connection socket建立起TCP连接。因为是面向连接的,因此在一端想向另一端发送信息时,只需要将信息送给connection socket,而不需要向UDP那样在数据包中额外加入IP地址和端口号等信息。
我们还要明确,在客户进程初始化TCP连接前需要:
在服务进程上,它会一开始一直监听是否有客户进程与它建立TCP连接,一旦连接建立,服务器进程就会创建一个属于当前客户进程的connection socket。如下图所示,连接建立后,客户进程和服务器进程就会通过一个管道(pipe)来传输信息。TCP保证服务器进程/客户进程按序收到客户进程/服务器进程发送的全部字节,所以我们说TCP在客户进程和服务器进程间提供可靠的服务。
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
std::string message;
// 创建socket文件描述符
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
std::cerr << "Socket creation error\n";
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将地址转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
std::cerr << "Invalid address/ Address not supported\n";
return -1;
}
// 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
std::cerr << "Connection Failed\n";
return -1;
}
// 从键盘读取数据并发送到服务器
std::cout << "Enter message: ";
std::getline(std::cin, message);
send(sock, message.c_str(), message.length(), 0);
// 接收服务器返回的数据并显示
read(sock, buffer, BUFFER_SIZE);
std::cout << "Modified message from server: " << buffer << std::endl;
close(sock);
return 0;
}
#include <iostream>
#include <cstring>
#include <cctype>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define PORT 8080
#define BUFFER_SIZE 1024
void to_uppercase(char* str) {
for (int i = 0; str[i]; i++) {
str[i] = toupper(str[i]);
}
}
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 绑定端口
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 读取数据并转换为大写
read(new_socket, buffer, BUFFER_SIZE);
to_uppercase(buffer);
send(new_socket, buffer, strlen(buffer), 0);
std::cout << "Modified message sent back to client\n";
close(new_socket);
close(server_fd);
return 0;
}