《TCP/IP详解卷3:TCP事务协议、HTTP、NNTP和UNIX域协议》 —1.2 UDP上的客户-服务器
1.2 UDP上的客户-服务器
我们先来看一个简单的UDP客户-服务器应用程序的例子,其客户程序源代码如图1-1所示。在这个例子中,客户向服务器发出一个请求,服务器处理该请求,然后发回一个应答。
图1-1 UDP上的简单客户程序
图1-1 (续)
本书中所有源代码的格式都是这样。每一非空行前面都标有行号。正文中叙述某段源代码时,这段源代码的起始和结束行号标记于正文段落的左边,如下面的正文所示。有时这些段落前面会有一小段说明,对所描述的源代码进行概要说明。源代码段开头和结尾处的水平线标明源代码段所在的文件名。这些文件名通常都是指我们在1.9节中将介绍的4.4版BSD-Lite中发布的文件。
我们来讨论这个程序的一些有关特性,但不详细描述插口函数,因为我们假设读者对这些函数有一些基本的认识。关于插口函数的细节在参考书[Stevens 1990]的第6章中可以找到。图1-2给出了头文件cliserv.h。
1. 创建UDP插口
10-11 socket函数用于创建一个UDP插口,并将一个非负的插口描述符返回给调用进程。出错处理函数err_sys参见参考书[Stevens 1992]的附录B.2。这个函数可以接受任意数目的参数,但要用vsprintf函数对它们格式化,然后这个函数会打印出系统调用所返回的errno值所对应的Unix出错信息,然后终止进程。
2. 填写服务器地址
12-15 首先用memset函数将Internet插口地址结构清零,然后填入服务器的IP地址和端口号。为简明起见,我们要求用户在程序运行中通过命令行输入一个点分十进制数形式的IP地址(argv[1])。服务器端口号(UDP_SERV_PORT)在头文件cliserv.h中用#define定义,在本章的所有程序首部中都包含了该头文件。这样做是为了使程序简洁,并避免使调用gethostbyname和getservbyname函数的源代码复杂化。
3. 构造并向服务器发送请求
16-19 客户程序构造一个请求(只用一行注释来表示),并用sendto函数将其发出,这样就有一个UDP数据报发往服务器。同样是为了简明起见,我们假设请求(REQUEST)和应答(REPLY)的报文长度为固定值。实用的程序应当按照请求和应答的最大长度来分配缓存空间,但实际的请求和应答报文长度是变化的,而且一般都比较小。
4. 读取和处理服务器的应答
20-23 调用recvfrom函数将使进程阻塞(即置为睡眠状态),直至收到一个数据报。接着客户进程处理应答(用一行注释来表示),然后进程终止。
由于recvfrom函数中没有超时机制,请求报文或应答报文中任何一个丢失都将造成该进程永久挂起。事实上,UDP客户-服务器应用的一个基本问题就是对现实世界中的此类错误缺少健壮性。在本节的末尾将对这个问题做更详细的讨论。
在头文件cliserv.h中,我们将SA定义为struct sockaddr*,即指向一般的插口地址结构的指针。每当有一个插口函数需要一个指向插口地址结构的指针时,该指针必须被置为指向一个一般性插口地址结构的指针。这是由于插口函数先于ANSI C标准出现,在20世纪80年代早期开发插口函数的时候,void*(空类型)指针类型尚不可用。问题是,“struct sockaddr*”总共有17个字符,这经常使这一行源代码超出屏幕(或书本页面)的右边界,因此我们将其缩写成SA。这个缩写是从BSD内核源代码中借用过来的。
图1-2给出了在本章所有程序中都包含的头文件cliserv.h。
图1-2 本章各程序中均包含的头文件cliserv.h
图1-3给出了相应的UDP服务器程序。
图1-3 与图1-1的UDP客户程序对应的UDP服务器程序
图1-3 (续)
5. 创建UDP插口和绑定本机地址
8-15 调用socket函数创建一个UDP插口,并在其Internet插口地址结构中填入服务器的本机地址。这里本机地址设置为通配符(INADDR_ANY),这意味着服务器可以从任何一个本机接口接收数据报(假设服务器是多宿主的,即可以有多个网络接口)。端口号设为服务器的知名端口(UDP_SERV_PORT),该常量也在前面讲过的头文件cliserv.h中定义。本机IP地址和知名端口用bind函数绑定到插口上。
6. 处理客户请求
16-25 接下来,服务器程序就进入一个无限循环:等待客户程序的请求到达(recvfrom),处理该请求(我们只用一行注释来表示处理动作),然后发出应答(sendto)。
这只是最简单的UDP客户-服务器应用。实际中常见的例子是域名服务系统(DNS)。DNS客户(称作解析器)通常是一般客户应用程序(例如,Telnet客户、FTP客户或WWW浏览器)的一个部分。解析器向DNS服务器发出一个UDP数据报,查询某一域名对应的IP地址。服务器发回的应答通常也是一个UDP数据报。
如果观察客户向服务器发送请求时双方交换的分组,我们就会得到图1-4这样的时序图,页面上时间自上而下递增。服务器程序先启动,其行为过程给在图1-4的右半部,客户程序稍后启动。
我们分别来看客户和服务器程序中调用的函数及其相应内核执行的动作。在对socket函数的两次调用中,上下紧挨着的两个箭头表示内核执行请求的动作并立即返回。在调用sendto函数时,尽管内核也立即返回,但实际上已经发出了一个UDP数据报。为简明起见,我们假设客户程序的请求和服务器程序的应答所生成的IP数据报的长度都小于网络的最大传输单元(MTU),IP数据报不必分段。
在这个图中,有两次调用recvfrom函数使进程睡眠,直到有数据报到达才被唤醒。我们把内核中相应的例程记为sleep和wakeup。
最后,我们还在图中标出了事务所耗费的时间。图1-4的左侧标示的是客户端测得的事务时间:从客户发出请求到收到服务器的应答所经历的时间。组成这段事务时间的数值标在图的右侧:RTT + SPT,其中RTT是网络往返时间,SPT是服务器处理客户请求的时间。UDP客户-服务器事务的最短时间就是RTT + SPT。
图1-4 UDP客户-服务器事务的时序图
尽管没有明确说明,但我们已经假设从客户到服务器的路径需要1/2RTT时间,返回的路径又需1/2RTT时间。但实际情况并非总是如此。据对大约600条Internet路径的研究[Paxson 1995b]发现:30%的路径呈现明显的不对称性,这说明两个方向上的路由经过了不同的站点。
我们的UDP客户-服务器看起来非常简洁(每个程序只有大约30行有关网络的源代码),但在实际环境中应用还不够健壮。由于UDP是不保证可靠的协议,数据报可能会丢失、失序或重复,因此实用的应用程序必须处理这些问题。这通常是在客户程序调用recvfrom时设置一个超时定时器,用以检测数据报的丢失,并重传请求。如果要使用超时定时器,客户程序就要测量RTT并动态更新,因为互联网上的RTT会在很大范围内变化,并且变化很快。但如果是服务器的应答丢失,而不是请求,那么服务器就要再次处理同一个请求,这可能会给某些服务带来问题。解决这个问题的办法之一是,让服务器将每个客户最近一次请求的响应暂存起来,必要时重传这个应答,而不需要再次处理这个请求。最后,典型的情况是,客户向服务器发送的每个请求中都有一个不同的标识,服务器把这个标识在响应中传回来,使客户能把请求和响应匹配起来。在参考书[Stevens 1990]的 8.4节中给出了UDP上的客户-服务器处理这些问题的源代码细节,但这将在程序中增加大约500行源代码。
一方面,许多UDP应用程序都通过执行所有这些额外步骤(超时机制、RTT值测量、请求标识,等等)来增加可靠性;另一方面,随着新的UDP应用程序不断出现,这些步骤也在不断地推陈出新。参考书[Patridge 1990b]中指出,“为了开发‘可靠的UDP应用程序’,你要有状态信息(序列号、重传计数器和往返时间估计器),原则上你要用到当前TCP连接块中的全部信息。因此,构筑一个‘可靠的UDP’,本质上和开发TCP一样难”。
有些应用程序并不实现上面所述的所有步骤:例如在接收时使用超时机制,但并不测量RTT值,当然更不会动态地更新RTT值。这样,当应用程序从一个环境(比如局域网)移植到另一个环境(比如广域网)中应用时,就可能会引发一些问题。比较好的解决办法是用TCP 而不是用UDP,这样就可以利用TCP提供的所有可靠传输特性。但是这种办法会使客户端测得的事务时间由RTT + SPT增加到2×RTT + SPT(见下一节),而且还会大大增加两个系统之间交换的分组数目。对付这些新的问题也有一个办法,即用T/TCP取代TCP,我们将在1.4节中对此进行讨论。
- 点赞
- 收藏
- 关注作者
评论(0)