2.6 视频流和内容分发网
对如何在因特网中实现流行的视频流服务 进行概述。它们的实现方式是使用应用层协议 和以像高速缓存那样方式运行的服务器。
2.6.1 因特网视频
在流式存储视频应用 中,基础的媒体是预先录制的视频 ,例如电影、电视节目、录制好的体育事件或录制好的用户生成的视频(如通常在 YouTube 上可见的那些)。这些预先录制好的视频放置在服务器上 ,用户按需 向这些服务器发送请求来观看视频。许多因特网公司现在提供流式视频,这些公司包括 Netflix YouTube (谷歌)、亚马逊和优酷。
视频媒体自身。视频是一系列的图像,通常以一种恒定的速率 (如每秒24 或 30 张图像)来展现。一幅未压缩 、数字编码 的图像由像素阵列 组成,其中每个像素 是由一些比特编码 来展示亮度和颜色 。视频的一个重要特征是能够被压缩 ,因而可用比特率 来权衡视频质量。现成的压缩算法能够将一个视频压缩成所希望的任何比特率。当然,比特率越高,图像质量也好,用户总体视觉感受也越好。
从网络的观点看,视频最为突出的特征 是它的高比特率 。压缩的因特网视频的比特率范围通常从用于低质量视频的 100kbps ,到用于流式高分辨率电影的超过 3Mbps ,再到用于4K 流式展望的超过 10Mbps。 这能够转换为巨大的流量 和存储 ,特别是对高端视频。例如,单一 2Mbps 视频在 67 分钟期间将耗费 1GB 的存储和流量。到目前为止,对流式视频 的最为重要的性能度量 是平均端到端吞吐量 。为了提供连续不断的布局,网络必须为流式应用提供平均吞吐量,这个流式应用至少与压缩视频的比特率一样大。
也能使用压缩生成相同视频的多个版本,每个版本由不同的质量等级。例如,使用压缩生成相同视频的 3 个版本,比特率分别为 300kbps、1Mbps 和 3Mbps。用户则能够根据当前可用带宽来决定观看哪个版本。
2.6.2 HTTP 流 和 DASH
在HTTP 流 中 ,视频只是存储在 HTTP 服务器中作为一个普通的文件 、每个文件有一个特的 URL 。当用户要看该视频时,客户与服务器创建一个 TCP 连接 并发送对该 URLHTTP GET 请求。 服务器则以底层网络协议 和流量条件允许 的尽可能快的速率 ,在一个HTTP 响应报文中发送该视频文件 。在客户一侧,字节被收集在客户应用缓存中。一旦该缓存中的字节数量超过预先设定的门限,客户应用程序就开始播放,特别是,流式视频用程序周期性地从客户应用程序缓存中抓取帧 ,对这些帧解压缩 并且在用户屏幕上展现。 因此,流式视频应用接收到视频就进行播放,同时缓存该视频后面部分的帧。
HTTP 流具有严重缺陷,即所有客户接收到相同编码的视频 ,尽管对不同用户或对于相同用户的不同时间而言,客户可用的带宽大小有很大不同。导致一种新型基于 HTTP 的流的研发,称为经 HTTP 的动态适应性流 (DASH )。DASH中,视频编码为几个不同的版本,其中每个版本具有不同的比特率,对应于不同的质量水平。客户动态地请求来自不同版本且长度为几秒的视频段数据块 。当可用带宽量较高时,客户自然地选择来自高速率版本的块;当可用带宽量较低时,客户自然地选择来自低速率版本的块。客户用 HTTP GET 请求报文一次选择一个不同的块。
使用 DASH 后,每个视频版本存储在 HTTP 服务器中 ,每个版本都有一个不同的URL。 HTTP 服务器也有一个告示文件 ( manifest file) ,为每个版本提供了一个URL 及其比特率 。客户首先请求该告示文件并且得知各种各样的版本。然后客户通过在 HTTP GET 请求报文中对每块指定 URL 和 一个字节范围 ,一次选择一块。在下载块的同时,客户也测量接收带宽 并运行一个速率决定算法 来选择下次请求的块。自然地,如果客户缓存的视频很多,并且测量的接收带宽较高,它将选择一个高速率的版本。同样,如果客户缓存的视频很少,并且测量的接收带宽较低 ,它将选择一个低速率的版本。因此DASH 允许客户自由地在不同的质量等级之间切换。
2.6.3 内容分发网
对于一个因特网视频公司,或许提供流式视频服务最为直接的方法是建立单一的大规模数据中心 ,在数据中心中存储其所有视频,并直接从该数据中心向世界范围的客户传输流式视频。但是这种方法存在三个问题:
① 如果客户远离数据中心,服务器到客户的分组将跨越许多通信链路并很可能通过许多 ISP ,其中某些 ISP 可能位于不同的大洲。如果这些链路之一提供的吞吐量小于视频消耗速率,端到端吞吐量也将小于该消耗速率,给用户带来恼人的停滞时延。出现这种事件的可能性随着端到端路径中链路数量的增加而增加。
②流行的视频很可能经过相同的通信链路发送许多次 。这不仅浪费了网络带宽,因特网视频
公司自己也将为向因特网反复发送相同的字节而向其 ISP 运营商(连接到数据中心)支付费用。
③ 单个数据中心代表一个单点故障,如果数据中心或其通向因特网的链路崩溃,将不能够分发任何视频流了。
为了应对向分布于全世界的用户分发巨量视频数据的挑战,几乎所有主要的视频流公司都利用内容分发网(CDN) 。 CDN 管理分布在多个地理位置上的服务器,在它的服务器中存储视频 (和其他类型的 Web 内容,包括文档、图片和音频)的副本 ,并且所有试图将每个用户请求定向到一个将提供最好的用户体验的 CDN 位置。 CDN 可以是专用 CDN ,即它由内容提供商自己拥有;例如,谷歌的 CDN 分发 Youtube 视频和其他类型的内容。另一种 CDN 可以是第三方CDN ,代表多个内容提供商分发内容;Akamai、Limelight 和 level-3 都运行第三方 CDN。
CDN 通常采用两种不同的服务器安置原则:
① 深入 。第一个原则由 Akamai 首创, 该原则是通过在遍及全球的接入 ISP 中部署服务器集群 来深入到 ISP 的接入网中 (在 1.3 节中描述了接入网 。) Akamai 在大约 1700 个位置采用这种方法部署集群。其目标是靠近端用户 ,通过减少端用户和CDN 集群之间 (内容从这里收到)链路和路由器的数量,从而改善了用户感受的时延和吞吐量。
② 邀请做客 。第二个设计原则由 Limelight 和许多其他 CDN 公司所采用,该原则是通过在少量(例如 10 个)关键位置建造大集群来邀请到 ISP 做客 。不是将集群放在接入 ISP 中 ,这些 CDN 通常将它们的集群放置在因特网交换点 (lXP) - (参见 1. 3 节) 与深入设计原则相比,邀请做客设计通常产生较低的维护和管理开销 ,可能以对端用户的较高时延和较低吞吐量为代价。
一旦 CDN 的集群准备就绪,它就可以跨集群复制内容 。CDN 可能不希望将每个视频的副本放置在每个集群巾,因为某些视频很少观看或仅在某些国家中流行。事实上,许多CDN 没有将视频推入它们的集群,而是使用一种简单的拉策略 :如果客户向一个未存储该视频的集群请求某视频,则该集群检索该视频 (从某中心仓库或者从另一个集群) ,向客户流式传输视频时的同时在本地存储一个副本。类似于因特网缓存(参见 2.2.5 节) ,当某集群存储器变满时,它删除不经常请求的视频。
- CDN 操作
当用户主机 中的一个浏览器指令检索一个特定的视频( 由 URL 标识)时,CDN 必须截获该请求 ,以便能够:①确定此时适合用于该客户的 CDN 服务器集群;②将客户的请求重定向到该集群的某台服务器 。很快将讨论 CDN 是如何能够确定一个适当的集群的。但是首先考察截获和重定向请求所依赖的机制。
大多数 CDN 利用 DNS 来截获和重定向请求 :考虑用一个简单的例子来说明通常是怎样设计DNS 的。假定一个内容提供商 NetCinema,雇佣了第三方 CDN 公司 KingCDN 来向客户分发视频。在 NetCinema 的 Web 网页上,它的每个视频都被指派了一个 URL ,该 URL 包括了字符串" video "以及该视频本身的独特标识符;例如,转换器 7 可以指派为 http://video.netcinema.com /6Y7B23V。接下来出现如图 2-24 所示的 6 个步骤。

1)用户访问位于 NetCinema 的 Web 网页。
2) 当用户点击链接 http://video.netcinema.com/6Y7B23V 时,该用户主机发送了一个对于 video.netcinema.com 的 DNS 请求。
3)用户的本地 DNS 服务器(LDNS)将该 DNS 请求中继到一台用于 NetCinema 的权威服务器 ,该服务器观察到主机名 video.netcinema.com 中的字符串"video "。为了将该 DNS 请求移交给 KingCDN ,NetCinema 权威 DNS 服务器并不返回一个 IP 地址,而是向 LDNS 返回一个 KingCDN 域的主机名 ,如 a1105.kingcdn.com。
4) 从这时起,DNS 请求进入了 KingCDN 专用 DNS 基础设施。用户的 LDNS 则发送第
二个请求,此时是对 all05.kingcdn.com DNS 请求 , KingCDN DNS 系统最终向 LDNS 返回 KingCDN 内容服务器的 IP 地址。所以正是在这里,在 KingCDN 的 DNS 系统中, 指定 DNS 服务器、客户将能够从这台服务器接收到它的内容。
5)LDNS 向用户主机转发内容服务 CDN 节点的 IP 地址。
6)一旦客户收到 KingCDN 内容服务器的 IP 地址,它与具有该 IP 地址的服务器创建了一条直接的 TCP 连接,并且发出对该视频的 HTTP GET 请求。如果使用了 DASH ,服务器将首先向客户发送具有 URL 列表的告示文件,每个 URL 对应视频的每个版本,并且客户将动态地选择来自不同版本的块。
2. 集群选择策略
任何 CDN 部署 ,其核心是集群选择策略 (cluster selection strategy) ,即动态地将客户定向 CDN 中的某个服务器集群或数据中心的机制。 如我们刚才所见,经过客户的 DNS查找,CDN 得知了该客户的 LDNS 服务器的 IP 地址。在得知该 IP 地址之后, CDN 需要基于该 lP 地址选择一个适当的集群。 CDN 一般采用专用的集群选择策略。我们现在简单地介绍一些策略,每种策略都有其优点和缺点。
一种简单的策略是指派客户到地理上最为邻近的集群 。使用商用地理位置数据库,每个 LDNS IP 地址都映射到一个地理位置。 当从一个特殊的 LDNS 接收到 DNS 请求时,CDN 选择地理上最为接近的集群。 这样的解决方案对于众多用户来说能够工作的相当好。但对某些用户,该解决方案可能执行的效果差,因为就网络路径的长度或跳数而言,地理最邻近的集群可能并不是最近的集群。 此外,一种所有基于 DNS 的方法都内在具有的问题是,某些段用户配置使用位于远地的 LDNS ,在这种情况下,LDNS 位置可能远离客户的位置。此外,这种简单的策略忽略了时延和可用带宽随因特网路径时间而变化 ,总是为特定的客户指派相同的集群。
为了基于当前流量条件为客户决定最好的集群,CDN 能够对其集群和客户之间的时延和丢包性能执行周期性的实时测量 ( real-time measurement)。 例如, CDN 能够让它的每个集群周期性地向位于全世界的所有 LDNS 发送探测分组 (例如, ping 报文或 DNS 请求)。这种方法的一个缺点是许多 LDNS 被配置为不会响应这些探测。
2.6.4 学习案例:Netflix、Youtube 和 "看看"
- Netflix
Netflix 视频分发具有两个主要部件:亚马逊云和它自己的专用 CDN 基础设施。
Netflix 有一个 Web 网站来处理若干功能 ,这些功能包括用户注册和登录、计费、用于浏览和搜索的电影目录以及一个电影推荐系统。如图 2-25 所示,这个 Web 网站(以及它关联的后端数据库) 完全运行在亚马逊云中的亚马逊服务器上。此外,亚马逊云处理下列关键功能:
① 内容摄取 。Netflix 能够向它的用户分发某电影之前,它必须首先获取和处理该电影 。Netflix 接收制片厂电影的母带,并且将其上载到亚马逊云的主机 上。
② 内容处理 。亚马逊云中的机器为每部电影生成许多不同格式 ,以适合在桌面计算机、智能手机和与电视机相连的游戏机上运行的不同类型的客户视频播放器。为每种格式和比特率都生成一种不同的版本,允许使用 DASH HTTP 适应性播放流。
③ 向其 CDN 上载版本。一旦某电影的所有版本均已生成,在亚马逊云中的主机向其CDN 上载这些版本。

Netflix 创建了自己专用的 CDN ,它从这些专用 CDN 发送它所有的视频。为了创建它自己的 CDN,Netflix 在 IXP 和它自己的住宅 ISP 中安装了服务器机架 。其中 Netflix 向潜在的 ISP 合作伙伴提供了在其网络中安装一个(免费)Netflix 机架的操作指南。在机架中每台服务器具有几个 Gbps 以太网接口和超过 100TB 的存储 。在一个机架中服务器的数量是变化的:IXP 安装通常有数十台服务器并包含整个 Netflix 流式视频库(包括多个版本的视频以支持 DASH);本地 IXP 也许仅有一台服务器并仅包含最为流行的视频。Netflix 不使用拉高速缓存以在 IXP 和 ISP 中扩展它的 CDN 服务器,反而在非高峰时段通过推将这些视频分发给它的 CDN 服务器。
客户与各台服务器之间的交互 ,这些服务器与电影交付有关。浏览 Netflix 视频库的 Web 网页由亚马逊云中的服务器提供服务。 当用户选择一个电影准备播放时,运行在亚马逊云中的 Netflix 软件首先确定它的哪个 CDN 服务器具有该电影的拷贝 。在具有拷贝的服务器中,该软件决定客户请求的" 最好的 "服务器。如果该客户正在使用一个住宅 ISP ,它具有安装在该 ISP 中Netflix CDN 服务器机架并且该机架具有所请求电影的拷贝,则通常选择这个机架中的一台服务器。倘若不是,通常选择邻近 IXP 的一台服务器。
一旦 Netflix 确定了交付内容的 CDN 服务器 ,它向该客户发送特定服务器的 IP 地址 以及资源配置文件 ,该文件具有所请求电影的不同版本的 URL。该客户和那台 CDN 服务器则使用专用版本 的 DASH 进行交互。 具体而言,如 2.6.2 节,该客户使用 HTTP GET 请求报文中的字节范围首部,以请求来自电影的不同板本的块。Netflix 使用大约 4 秒长的块。随着这些块的下载,客户测量收到的吞吐量并且允许一个速率确定算法来确定下一个请求块的质量。
2. Youtube
Youtube 广泛地利用CDN 技术来分发它的视频 ,谷歌使用其专用 CDN 来分发 Youtube 视频,并且已经在几百个不同的 IXP 和 ISP 位置安装了服务器集群 。从这些位置以及从它的巨大数据中心,谷歌分发 Youtube 视频。谷歌使用如 2.2.5 节中描述的拉高速缓存 和如 2.6.3 节中描述的 DNS 重定向。 在大部分时间,谷歌的集群选择策略将客户定向到某个集群 ,使得客户与集群之间的 RTT 是最低的。然而,为了平衡流经集群的负载,有时客户被定向(经 DNS)到一个更远的集群。
YouTube 应用 HTTP 流 ,经常使少量的不同版本为一个视频可用,每个具有不同的比特率和对应的质量等级。Youtube 没有应用适应性流(例如 DASH),而要求用户人工选择一个版本。为了节省那些将被重定位或提前终止而浪费的带宽和服务器资源, YouTube 获取视频的目标量之后,使用 HTTP 字节范围请求来限制传输的数据流。
- 看看
因特网大规模地按需提供视频 。这是一种允许服务提供商极大地减少其基础设施和带宽成本的方法。如猜测那样,这种方法使用 P2P 交付而不是 客户 - 服务器 交互。
从高层看,P2P 流式视频类似于 BitTorrent 文件下载 。当一个对等方要看一个视频时,它联系一个追踪器 ,以发现在系统中具有该视频副本的其他对等方 。这个请求的对等方则并行地从具有该文件的其他对等方请求该视频的块。 然而,不同于使用 BitTorrent 下载,请求被优先地给予那些即将播放的块,以确保连续播放。
近期向混合 CDN-P2P 流式系统 迁移。看看目前在中国部署了数以百计的服务器并且将视频内容推向这些服务器。这个看看 CDN 在流式视频的启动阶段起着主要作用。在大多数场合,客户请求来自 CDN 服务器的内容的开头部分,并且并行地从对等方请求内容 。当 P2P 总流量满足 视频播放时,该客户将从 CDN 停止流并仅从对等方获得流 。但如果P2P 流的流量不充分 ,该客户重新启动 CDN 连接并且返回到混合 CDN-P2P 流模式。以这种方式,看看能够确保短启动时延,与此同时最小地依赖成本高的基础设施服务器和带宽。
2.7 套接字编程:生成网络应用
探讨网络应用程序是如何实际编写 的。2.1节讲过,典型的网络应用是由一对程序(客户程序和服务器程序 )组成的,它们位于两个不同的端系统 中。当运行这两个程序时,创建了一个客户进程和一个服务器进程 ,同时它们通过从套接字读出和写入数据在彼此之间进行通信 。开发者创建一个网络应用时,主要任务就是编写客户程序和服务器程序的代码。
网络应用程序有两类。一类是协议标准 (如一个 RFC 或某种其他标准文档)中所定义的操作的实现 ;对于这样的实现,客户程序和服务器程序必须遵守由该 RFC 所规定的规则。例如,某客户程序可能是 HTTP 协议客户端的一种实现 ,如在 2.2 节所述,该协议由 RFC2616 明确定义;类似的,其服务器程序能够是 HTTP 服务器协议的一种实现,也由 RFC 2616 明确定义。如果一个开发者编写客户端的代码,另一个开发者编写服务器程序的代码,并且两者都完全遵从该 RFC 的各种规则,那么这两个程序能够交互操作。
另一类网络应用程序是专用的网络应用程序 。在这种情况下,由客户和服务器程序应用的应用层协议没有公开发布在某 RFC 中或其他地方。某单独的开发者产生了客户和服务器程序,并且该开发者用它的代码完全控制该代码的功能。但是因为这些代码没有实现一个开放的协议,其他独立的开发者将不能开发出和该应用程序交互的代码。
实现一个非常简单的客户-服务器应用程序代码 。研发阶段,开发者必须最先做出决定:应用程序是运行在TCP 上还是运行在UDP 上。TCP 是面向连接的,并且为两个端系统之间的数据流动提供可靠的字节流通道 。UDP 是无连接的 ,从一个端系统向另一个端系统发送独立的数据分组,不对交付提供任何保证。当客户或服务器程序实现了一个由某 RFC 定义的协议时,它应当使用与该协议关联的周知端口号;与之相反,当研发一个专用应用程序时,研发者必须注意避免使用这些周知端口号。
2.7.1 UDP 套接字编程
编写使用 UDP 的简单客户-服务器程序
运行在不同机器上的进程彼此通过向套接字发送报文来进行通信。每个进程好比一座房子,进程的套接字比作一扇门,应用程序位于房子中门的一侧,运输层位于门外的另一侧。应用程序开发者在套接字的应用层一侧可以控制所有东西,几乎无法控制运输层一侧。
使用 UDP 套接字的两个通信进程之间的交互 。在发送进程能够将数据分组推出套接字 之门之前,当使用 UDP 时,必须先将目的地址附在该分组之上 。在该分组传过发送方的套接字后,因特网使用该目的地址通过因特网为该分组选路到接收进程的套接字 。当分组到达接收套接字时,接收进程通过该套接字取回分组,然后检查分组的内容并采取适当的动作。
目的主机的 IP 地址是目的地址的一部分 ,通过在分组中包括目的地的 IP 地址,因特网中的路由器将能够通过因特网将分组选路到目的主机。但是因为一台主机可能运行许多网络应用迸程,每个进程具有一个或多个套接字 ,所以在目的主机指定特定的套接字 也是必要的。当生成一个套接字时- ,就为它分配一个称为端口号 (port nunlber) 的标识符。因此,分组的目的地址也包括该套接字的端口号。总的来说,发送进程为分组附上目的地址 ,由目的主机的IP地址 和目的地套接字的端口号 组成的。此外,发送方的源地址也是由源主机的 IP 地址和源套接字的端口号组成 ,附在分组上。然而,源地址负载分组上不是由 UDP 应用程序代码所为,而是由底层操作系统自动完成。
使用下列简单的客户-服务器应用程序来演示对于 UDP 和 TCP 的套接字编程:
1)客户从其键盘读取一行字符(数据)并将该数据向服务器发送
2)服务器接收该数据并将这些字符转换为大写
3)服务器将修改的数据发送给客户
4)客户接收修改的数据并在其监视器上将该行显式出来
图 2-26 显示了客户和服务器的主要与套接字相关的活动,两者通过 UDP 运输服务进行通信

以 UDP 客户开始,该程序将向服务器发送一个简单的应用级报文 。服务器为了能够接收并回答该客户的报文,它必须准备好并已经在运行,这就是说,在客户发送其报文之前,服务器必须作为一个进程正在运行。
客户程序被称为 UDPClient.py,服务器程序被称为 UDPServer.py。
1. UDPClient.py
该应用程序客户端的代码并分析各行代码:
from socket import *
serverName = 'hostname'
serverPort = 12000
clientSocket = socket(AF一工NET SOCK_DGRAM)
message = raw_ínput(' Input lowe.rcase sentence:')
clientSocket.sendto(message.encode() , (serverName, serverPort))
modifiedMessage, serverAddress = clientSocket . recvfrom(2048)
print(modifiedMessage.decocte())
clientSocket.close()
from socket import * :该 socket 模块形成了在 python 中给所有网络通信的基础,包括了这行,才能够在程序中创建套接字。
serverName='hostname' serverPort=12000:第一行将变量 serverName 置为字符串 'hostname'。这里提供了或者包含服务器的 IP地址 (如"128.138.32.126")或者包含服务器的主机名(如"cis.poly.edu")的字符串。如果使用主机名,则将自动执行 DNS lookup 从而得到 IP 地址。第二行将整数变量 serverPort 置为 12000.
clientSocket = socket(AF_INET, SOCK_DGRAM):该行创建了客户的套接字 ,称为 clientSocket。第一个参数指示了地址簇 ;特别是,AF_INET 指示了底层网络使用了 IPv4。第二个参数指示了该套接字是 SOCK_DGRAM 类型的 ,意味着它是 UDP套接字。创建套接字时,并没有指定客户套接字的端口号。既然创建了客户进程的门,将要生成通过该门发送的报文。
message=raw_input('Input lowercase sentence;'):raw_input() 是python中的内置功能。执行这条命令时,客户上的用户将以单词"Input lowercase sentence:" 进行提示,用户则使用它的键盘输入一行,该内容被放入变量 message 中 。既然有了一个套接字和一条报文,将要通过该套接字向目的主机发送报文。
clientSocket.sendto(message.encode() , (serverName, serverPort)) :首先将报文由字符串类型转换为字节类型,向套接字中发送字节 ;这将使用 encode() 方法完成。方法 sendto() 为报文附上目的地址(serverName, serverPort) 并且向进程的套接字 clientSocket 发送结果分组 。(源地址也附到分组上,不是显式地由代码完成的)经一个 UDP 套接字发送一个客户到服务器的报文非常简单!在发送分组后,客户等待接收来自服务器的数据。
modifiedMessage, serverAddress = clientSocket . recvfrom(2048) :当一个来自因特网的分组到达该客户套接字时,该分组的数据 被放置到变量 modifiedMessage 中,其源地址 被放置到变量 serverAddress 中。变量 serverAddress 包含了服务器的 IP 地址和服务器的端口号。程序 UDPClient 实际上并不需要服务器的地址信息,因为它起始就知道了该服务器地址;而这行 python 代码仍然提供了服务器地址。方法 recvfrom 也取缓存长度 2048 作为输入。
print(modifiedMessage.decocte()) clientSocket.close() :将报文从字节转化为字符串后,在用户显示器上打印出 modifiedMessage 。应当是用户键入的原始行,但现在变为大写。close则关闭了套接字,然后关闭进程。
2. 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.rècvfrom(2048)
modifiedMessage=message.decode().upper()
serverSocket.sendto(modifiedMessage.encode(),clientAddress)
UDPServer 的开始部分与 UDPClient 类似。它也是导入套接字模块,也将整数变量 serverPort 设置为 12000,并且也创建套接字类型 SOCK_DGRAM(一种套接字)。与 UDPClient 由很大不同的一行是:
serverSocket.bind((' ' , serverPort)) :将端口号 12000 与该服务器的套接字绑定(即分配)在一起 。因此在 UDPServer 中,(应用程序开发者编写的)代码显式地为该套接字分配一个端口号。以这种方式,当任何人向位于该服务器的 IP 地址的端口 12000 发送一个分组,该分组将导向该套接字 。UDPServer 然后进入一个 while 循环;该 while 循环将允许 UDPServer 无限期地接收并处理来自客户的分组。在该 while 循环中,UDPServer 等待一个分组的到达。
message.clientAddress = serverSocket.rècvfrom(2048) :当某分组到达该服务器的套接字时,该分组的数据被放置到变量 message 中,其源地址被放置到变量 clientAddress 中。变量 clientAddress 包含了客户的 IP 地址和客户的端口号 。这里,UDPServer 将利用该地址信息 ,因为提供了返回地址,类似于普通邮政邮件的返回地址。使用该源地址信息,服务器此时知道了它应当将回答发向何处。
modifiedMessage=message.decode().upper() :关键部分,将报文转化为字符串后 ,获取由客户发送的行并使用 upper() 将其转换为大写。
serverSocket.sendto(modifiedMessage.encode(),clientAddress) :将客户的地址(IP地址和端口号)附到大写的报文上(将字符串转化为字节后) ,并将所得的分组发送到服务器的套接字中 。然后因特网将分组交付到该客户地址。在服务器发送该分组后,它仍然维持在while 循环中,等待(从允许在任一台主机上的任何客户发送的)另一个 UDP 分组到达。

2.7.2 TCP 套接字编程
与 UDP 不同,TCP 是一个面向连接 的协议。 这意味着在客户和服务器能够开始互相发送数据之前,它们先要握手和创建一个 TCP 连接 。TCP 连接的一端与客户套接字 相联系,另一端与服务器套接字 相联系。当创建该 TCP 连接时,我们将其与客户套接字地址(IP 地址和端口号)和服务器套接字地址(IP 地址和端口号)关联起来 。使用创建的 TCP连接,当一侧要向另一侧发送数据时,它只需经过其套接字将数据丢进 TCP 连接 。这与 UDP 不同,UDP 服务器在将分组丢进套接字之前必须为其附上一个目的地地址。
观察 TCP 中客户 程序和服务器 程序的交互 。客户 具有向服务器发起接触 的任务。服务器为了能够对客户的初始接触做出反应,服务器必须已经准备好。这意味着两件事:第一,与在 UDP 中的情况一样,TCP 服务器在客户试图发起接触前必须作为进程运行起来 。第二,服务器程序必须具有一扇特殊的门,具体说是一个特殊的套接字,欢迎来自运行在任意主机上的客户进程的某种初始接触。
随着服务器进程的运行,客户 进程能够向服务器 发起一个TCP 连接 。这是由客户 程序通过创建一个 TCP 套接字 完成的。当该客户生成其 TCP 套接字时,它指定 了服务器中的欢迎套接字地址,即服务器主机的 IP 地址和套接字的端口号 。生成其套接字之后,该客户发起了一个三次握手并创建与服务器的一个 TCP 连接。发生在运输层的三次握手,对于客户和服务器是完全透明的。
在三次握手期间,客户进程敲服务器进程的门。当服务器"听"到敲门声时,生成一扇新门(一个新套接字 ),专门用于特定的客户。在下面例子中,欢迎之门是称为serverSocket 的TCP 套接字对象 ;专门对客户进行连接的新生成的套接字 ,称为连接套接字 。初次容易混淆欢迎套接字 (所有要与服务器通信的客户的起始接触点)和每个新生成 的服务器侧 的连接套接字(随后为每个客户通信而生成的套接字)。
从应用程序观点,客户套接字 和服务器连接套接字 直接通过一根管道 连接。 如图 2-27 所示,客户进程可以向它的套接字发送任意字节 ,并且 TCP 保证服务器进程能够按照发送的顺序接收(通过连接套接字)每个字节 。TCP 因此在客户和服务器进程之间提供可靠服务。此外,客户进程 不仅能向它的套接字发送字节 ,也能从中接收字节 ;类似的,服务器进 程不仅从它的连接套接字接收字节 ,也能向其发送字节。

使用同样的客户-服务器应用程序来展示TCP 套接字编程 :客户向服务器发送一行数据 ,服务器将这行改为大写并回送给客户 。图2-28 着重显示了客户和服务器的主要与套接字相关的活动 ,两者通过 TCP 运输服务进行通信。

1. TCPClient.py
应用程序客户端代码:
from socket import *
serverName = 'servername'
serverPort = 12000
clientSocket = socket (AF_INET, SOCK_ STREAM)
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 套接字(而不是UDP)。创建该客户套接字时仍未指定其端口号;相反,让操作系统做此事。
clientSocket.connect((serverName, serverPort)) :发起客户和服务器之间的 TCP 连接。connect() 方法的参数是这条连接中服务器端的地址,执行后,执行三次握手,并在客户和服务器之间创建一条 TCP 连接。
sentence = raw_input (' Input lowercase sentence :' ) :如 UDPClient 一样,从用户获得了一个句子,字符串 sentence 连续收集字符直到用户键入回车以终止该行为止。
clientSocket. send(sentence . encode()) :通过客户的套接字进入 TCP 连接发送字符串 sentence。该程序并未显式地创建一个分组并为该分组附上目的地址(使用UDP才需这么做),相反,该客户程序只是将字符串 sentence 中的字节放入 TCP 连接中,然后客户等待接收来自服务器的字节。
modifiedSentence = clientSocket . recv(1024) :字符到达服务器时,被放置在字符串 modifiedSentence 中。字符继续累积在 modifiedSentence 中,直到该行以回车符结束为止。
clientSocket.close():打印完大写句子后,关闭套接字,因此关闭了客户和服务器之间的 TCP 连接,引起客户中的 TCP 向服务器的 TCP 发送一条 TCP 报文。
服务器程序
from socket import *
serverPort = 12000
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()
与 TCPClient 相同的是,服务器创建一个 TCP 套接字,执行:
serverSocket=socket(AF_INET,SOCK_STREAM)
severSocket.bind((' ',serverPort)):将服务器的端口号 serverPort 与该套接字关联
severSocket.listen(1) :serverSocket是欢迎套接字 ,创建欢迎之门后,将等待并聆听某个客户敲门。让服务器聆听来自客户的 TCP 连接请求。参数定义了请求连接的最大数(至少为1)
connectionSocket, addr= serverSocket . accept() :客户敲门时,程序为 serverSocket 调用 accept() 方法 ,在服务器中创建了一个称为 connectionSocket 的新套接字 ,由这个特定的客户专用 。客户和服务器则完成了握手,在客户的 clientSocket 和 服务器的 serverSocket 之间创建了一个 TCP 连接 。借助于创建的 TCP 连接,客户与服务器现在能够通过该连接相互发送字节。使用 TCP,从一侧发送的所有字节不仅确保到达另一侧,而且确保按序到达。
connectionSocket . close():客户发送修改的句子后,关闭了该连接套接字。但由于 serverSocket 保持打开,所以另一个客户此时也能够敲门并向该服务器发送一个句子要求修改。
