13.7 CD数据库应用程序

在看过如何用命名管道来实现一个简单的客户/服务器系统后,我们将重新阅读CD数据库应用程序,并据此对它进行改进。我们还将添加一些信号处理内容,使我们可以在进程被中断时执行一些清理工作。为了使代码尽可能简单,我们将使用早期的只有一个命令行接口的dbm版本。

在深入研究这个新版本之前,先来编译这个新的应用程序。如果你已经从网站上下载了源代码,就可以用makefile将它编译为server和client这两个程序。

第7章讲过,不同的Linux发行版命名和安装dbm文件的方式略微不同。如果我们提供的文件不能在你的系统中成功编译,请回顾第7章有关dbm文件命名和位置的内容。

键入命令server -i,将使程序初始化一个新的CD数据库。

不用说,如果服务器未启动运行,客户程序是不会运行的。下面是makefile文件的内容,它显示了程序是如何组织在一起的:

13.7 CD数据库应用程序 - 图1

13.7 CD数据库应用程序 - 图2

13.7.1 目标

我们的目标是把这个应用程序中处理数据库的部分和用户界面部分分开。我们还希望只运行一个服务器进程,但允许存在许多并发的客户进程。我们将尽量减少对已有代码的修改,只要有可能,就保留原有的代码。

为了简化应用,我们还希望能够在应用程序中创建(和删除)管道,这样就无需让系统管理员在运行程序之前为我们创建命名管道了。

还有一点非常重要,就是我们决不能“忙等待”某个事件的发生,从而减少CPU时间的浪费。正如我们看到的,Linux允许我们阻塞以等待事件的发生,从而避免消耗很多系统资源。我们可以利用管道的阻塞特性来确保对CPU的有效使用。总之,服务器至少在理论上可以在客户请求到来之前等待许多个小时。

13.7.2 实现

在第7章这个应用程序的早期单进程版本中,我们用一组数据访问例程来处理数据,它们是:

13.7 CD数据库应用程序 - 图3

这些函数提供了一个方便的起点,让我们可以把客户和服务器两部分清楚地分开。

这个应用程序的单进程实现版本虽然被编译为一个单独的程序,但我们可以把它看作是由两部分组成的,如图13-6所示。

13.7 CD数据库应用程序 - 图4

图 13-6

在客户/服务器实现版本中,我们想在这个应用程序的两个主要部分之间插入一些命名管道和相应的支持代码。图13-7显示了我们需要的结构。

13.7 CD数据库应用程序 - 图5

图 13-7

在具体实现中,我们选择把客户和服务器的接口例程都放在同一个文件pipe_imp.c中。这就把在客户/服务器实现版本中依赖命名管道使用的所有代码都集中到一个文件中。而将传递数据的格式和打包方式与实现命名管道的例程分离开。新版本中所包含的源文件更多了,但它们之间的区分也更符合逻辑了。这个应用程序的调用结构如图13-8所示。

13.7 CD数据库应用程序 - 图6

图 13-8

文件app_ui.c、client_if.c和pipe_imp.c将被编译和链接在一起构成客户端程序。而文件cd_dbm.c、server.c和pipe_imp.c将被编译和链接在一起构成服务器程序。头文件cliserv.h将以一个公共定义头文件的形式把这两者联系在一起。

文件app_ui.c和cd_dbm.c只做了少许改动,主要是为了把它分离为两个程序。由于这个应用程序现在已变得很大了,而代码中的绝大部分和以前的版本相比并无改动,所以我们在这里只显示文件cliserv.h、client_if.c和pipe_imp.c中的代码。

这个文件的某些部分依赖于客户/服务器的具体实现,在本例中就是命名管道。在第14章的结尾,我们还将改用另一种不同的客户/服务器模型。

头文件cliserv.h

我们首先来看头文件cliserv.h。这个文件定义了客户/服务器接口。客户和服务器的实现中都要用到它。

(1)首先是需要包含的头文件:

13.7 CD数据库应用程序 - 图7

(2)接着是命名管道的定义。我们为服务器设置一个管道,为每个客户分别设置一个管道。因为可能会有多个客户,所以客户管道的名字中要加上它的进程ID,来确保管道名字的唯一性:

13.7 CD数据库应用程序 - 图8

(3)我们将命令实现为枚举类型,而不是#define常量。

使用枚举类型是个好方法,它允许编译器进行更多的类型检查并且有利于软件调试。因为许多调试器可以显示枚举常量的名字,但对由#define指令定义的名字就不行。

第一个typedef给出了发送给服务器的请求类型,第二个给出了服务器返回给客户的响应类型。

13.7 CD数据库应用程序 - 图9

(4)接下来,我们声明了一个结构,用来在两个进程之间进行双向传递消息。

因为我们无需在同一个响应中同时返回cdc_entry和cdt_entry,所以也可以用联合变量的形式将它们结合在一起。但出于简化问题的考虑,我们还是将它们分离开来,这也使得代码更易于维护。

13.7 CD数据库应用程序 - 图10

13.7 CD数据库应用程序 - 图11

(5)最后是执行数据传输工作的管道接口函数,它的具体实现在文件pipe_imp.c中。它们分为服务器端函数和客户端函数两组,分别列在下面的第一部分和第二部分:

13.7 CD数据库应用程序 - 图12

我们将下面的讨论分为两部分,一部分介绍客户接口函数,另一部分介绍在文件pipe_imp.c中的服务器端和客户端函数的实现细节,我们会在必要时给出源代码。

13.7.3 客户接口函数

现在我们来看文件client_if.c。它提供了“假”版本的数据库访问例程。这些例程对请求进行编码并将它放入message_db_t结构,然后使用pipe_imp.c中的例程将请求传输给服务器。这样可以尽量减少对原来的app_ui.c文件的改动。

1.客户命令解释器

(1)这个文件实现了在头文件cd_data.h中定义的9个数据库函数。它的作用如同是一个中转站,先把请求传递给服务器,然后从函数返回服务器的响应。它的开始部分是#include语句和常量的定义:

13.7 CD数据库应用程序 - 图13

(2)静态变量mypid减少了对getpid函数的调用次数。为了消除重复代码,我们使用了局部函数read_one_response:

13.7 CD数据库应用程序 - 图14

(3)函数database_initialize和close仍被使用,但与以往不同,它们一个用来初始化管道接口的客户端,一个用来删除当客户退出时多余的命名管道:

13.7 CD数据库应用程序 - 图15

(4)用一个给定的CD唱片标题调用get_cdc_entry例程,将从数据库中取出对应的标题数据项。我们将请求编码到一个message_db_t结构中并把它传递给服务器,然后将服务器的响应读回到另一个message_db_t结构中。如果在数据库中找到了对应的数据项,它将被存放在message_db_t结构的cdc_entry结构中,我们把该结构作为函数的返回值:

13.7 CD数据库应用程序 - 图16

(5)下面是函数read_one_response的源代码,我们用它来避免重复代码:

13.7 CD数据库应用程序 - 图17

13.7 CD数据库应用程序 - 图18

(6)其他get_xxx、del_xxx和add_xxx形式的例程与get_cdc_entry函数的实现方式类似。为了代码的完整性,我们也把它们列在下面,首先是用来检索CD曲目的函数get_cdt_entry:

13.7 CD数据库应用程序 - 图19

(7)接下来是两个添加数据的函数,第一个用于标题数据库,第二个用于曲目数据库:

13.7 CD数据库应用程序 - 图20

13.7 CD数据库应用程序 - 图21

(8)最后是两个用于删除数据的函数:

13.7 CD数据库应用程序 - 图22

13.7 CD数据库应用程序 - 图23

2.搜索数据库

根据CD唱片关键字进行搜索的函数非常复杂。调用者希望每调用它一次就开始一次搜索。在第7章中,为了满足这种需求,在第一次调用该函数时将first_call_ptr设置为true,这样它将返回第一个匹配记录。在后续对搜索函数的调用中,我们将first_call_ptr设置为false,这样它返回的是后续的匹配记录,每次调用返回一个。

现在,由于我们已将应用程序划分为两个进程,在服务器中就不能再允许每次搜索只处理一个数据项了,因为在前一次搜索正在进行时,可能会有另一个客户开始请求服务器进行另外一次搜索。我们也不能让服务器端分别保存每个客户搜索的上下文(即搜索已到达的位置),因为用户可能会在搜索进行到一半时,由于找到了想找的CD唱片或因为客户突然中断而停止这次搜索。

我们可以改变搜索的执行方式,也可以像我们在这里选择的那样把这些复杂性隐藏在接口例程中。我们的做法是,让服务器把搜索的可能匹配结果全部返回并保存在一个临时文件中,直到客户请求它们。

(1)这个函数看上去很复杂,但实际并非如此。它调用了3个管道函数(我们将在下一节中介绍它们):send_mess_to_server、start_resp_from_server和read_resp_from_server。

13.7 CD数据库应用程序 - 图24

(2)第一次调用这个函数进行搜索时,*first_call_ptr被设置为true。我们最好现在就将它设置为false,以免后面忘记修改它。然后创建临时文件work_file并初始化客户消息结构。

13.7 CD数据库应用程序 - 图25

(3)接下来是三重条件判断,它将调用pipe_imp.c文件中的函数。如果消息被成功发送给服务器,客户就开始等待服务器的响应。成功读取了服务器返回的响应后,就将搜索的匹配结果保存到客户的临时文件work_file中,同时增加匹配计数器entries_matching的值。

13.7 CD数据库应用程序 - 图26

(4)接下来的测试检查搜索是否找到匹配数据。然后通过fseek调用设置work_file的下一个数据写入位置。

13.7 CD数据库应用程序 - 图27

(5)如果这不是本次搜索操作中第一次调用搜索函数,代码将检查是否还有其他匹配。最后,把下一个匹配数据项读到ret_val结构中。此前的检查用来确保还有匹配项存在。

13.7 CD数据库应用程序 - 图28

13.7.4 服务器接口server.c

如同客户端有个用于app_ui.c程序的接口,服务器端也需要一个程序用来控制cd_dbm.c(在以前的版本中名字是cd_access.c)。下面是服务器的main函数代码。

(1)首先声明一些全局变量、process_command函数的原型和一个用来完成退出清理工作的catch_signals函数。

13.7 CD数据库应用程序 - 图29

13.7 CD数据库应用程序 - 图30

(2)下面是main函数的代码。在检查完信号捕获例程可以正常工作后,程序检查用户是否在命令行上输入了-i选项。如果有,它就创建一个新数据库。如果调用cd_dbm.c中的database_initialize函数失败,就给出一条错误消息。如果一切正常则服务器开始运行,来自客户的任何请求都将被发往process_command函数,我们后面将会讲到这个函数。

13.7 CD数据库应用程序 - 图31

(3)所有客户的消息都将被发往process_command函数,在那里它们被放入一个case语句,进而调用cd_dbm.c中相应的函数。

13.7 CD数据库应用程序 - 图32

13.7 CD数据库应用程序 - 图33

在介绍管道的具体实现之前,我们先来看看,在客户和服务器进程之间传递数据时各种事件发生的先后顺序。图13-9显示客户和服务器进程在各自启动之后,双方在处理命令和响应时的循环情况。

13.7 CD数据库应用程序 - 图34

图 13-9

在具体实现中,情况要更复杂一些。因为在搜索请求中,客户向服务器传递一条命令,然后等待从服务器中接收一个或多个响应。这就使得情况更复杂了,但主要是在客户端。

13.7.5 管道

下面是实现管道功能的pipe_imp.c文件,它同时包含客户端和服务器端的函数。

在第10章中我们见到过DEBUG_TRACE标志,我们可以通过定义该标志来显示,客户和服务器进程在互相传递消息时,各个调用的执行顺序。

1.管道实现的开始部分

(1)首先是#include语句:

13.7 CD数据库应用程序 - 图35

(2)我们还定义了一些在此文件里的函数中会用到的值:

13.7 CD数据库应用程序 - 图36

2.服务器端函数

接下来,我们来看服务器端的函数。第一部分显示打开、关闭命名管道和读取来自客户的消息的函数。第二部分显示用于打开、发送和关闭客户管道的代码,客户管道名基于客户包含在其请求消息中的进程ID来确定。

● 服务器函数

(1)server_starting例程先为服务器创建一个它将从中读取命令的命名管道,然后以只读方式打开这个管道。这个open调用将阻塞到有客户以写方式打开这个管道为止。使用阻塞模式可以使服务器在等待发送过来的命令时对管道执行阻塞式读取。

13.7 CD数据库应用程序 - 图37

(2)当服务器结束时,它删除命名管道,这样客户就可以检测出没有服务器在运行:

13.7 CD数据库应用程序 - 图38

(3)下面给出的read_request_from_client函数会阻塞在对服务器管道的读操作上,直到有客户向其中写入一条消息为止:

13.7 CD数据库应用程序 - 图39

13.7 CD数据库应用程序 - 图40

13.7 CD数据库应用程序 - 图41

(4)如果出现没有任何客户以写方式打开这个管道的特殊情况,read调用将返回0。也就是说,它检测到一个EOF,此时服务器会关闭管道并重新打开它,这样服务器就可以阻塞到有客户打开这个管道为止。这与服务器第一次启动时的情况完全一样,等于我们重新初始化了服务器。把下面这些代码插到上面的函数中去:

13.7 CD数据库应用程序 - 图42

服务器是一个进程,它可能同时为许多客户服务。因为每个客户用不同的管道接收响应,所以服务器需要使用不同的管道来给不同的客户发送响应。而由于文件描述符是一种有限资源,所以服务器只有在需要发送数据时才会以写方式打开一个客户管道。

我们将打开、写入和关闭客户管道分离为3个独立的函数。这是为了适应数据库搜索返回多个搜索结果的情况,这样我们可以只打开管道一次,写入多个响应,然后再关闭它。

● 探测管道

(1)首先打开客户管道:

13.7 CD数据库应用程序 - 图43

(2)消息都是通过调用这个函数发送出去的。我们后面就会看到对应的用于接收消息的客户端函数。

13.7 CD数据库应用程序 - 图44

(3)最后,关闭客户管道:

13.7 CD数据库应用程序 - 图45

3.客户端函数

pipe_imp.c文件中与服务器端函数互补的是客户端函数,除了那个名为send_mess_to_server的函数,它们都与服务器端函数很相似。

● 客户函数

(1)在检查到服务器可访问后,client_starting函数初始化客户端管道:

13.7 CD数据库应用程序 - 图46

13.7 CD数据库应用程序 - 图47

(2)client_ending函数的作用是关闭文件描述符并删除目前多余的命名管道:

13.7 CD数据库应用程序 - 图48

(3)send_mess_to_server函数的作用是通过服务器管道传递请求:

13.7 CD数据库应用程序 - 图49

与我们前面看到的服务器端函数相对应,为了能够处理多个搜索结果,客户在从服务器取回结果时也使用了3个函数。

● 取得服务器返回的结果

(1)这个客户函数开始监听服务器的响应。它先以只读方式打开一个客户管道,然后又以只写方式重新打开这个管道。我们将在本节的稍后部分解释这样做的原因。

13.7 CD数据库应用程序 - 图50

13.7 CD数据库应用程序 - 图51

(2)下面是具体负责从服务器读取响应的read调用,它将取回匹配的数据库条目:

13.7 CD数据库应用程序 - 图52

(3)最后这个客户函数标记服务器响应的结束:

13.7 CD数据库应用程序 - 图53

在start_resp_from_server函数中第二个以写方式打开客户管道的调用是:

13.7 CD数据库应用程序 - 图54

它用来防止一个竞争条件的出现,这个竞争条件会在服务器需要响应来自同一个客户的快速、连续的多个请求时发生。

为了将这个问题解释得更清楚,我们来看看这个事件发生的过程。

(1)客户发送一个请求给服务器。

(2)服务器读取请求,打开客户管道并发回响应,但在关闭客户管道之前被挂起。

(3)客户以读方式打开自己的管道,读取第一个响应并关闭管道。

(4)客户然后发送一个新命令并再次以读方式打开客户管道。

(5)此时服务器恢复运行,关闭它那端的客户管道。

糟糕的是,此时客户正尝试从这个管道读取数据,等待自己下一个请求的响应,但因为已无进程以写方式打开这个客户管道,所以read调用将返回0字节。

通过允许客户以读写两种方式打开它自己的管道,就消除了反复重新打开这个管道的需要,从而避免了竞争条件的产生。因为客户永远也不会向这个管道写数据,所以不会有读到错误数据的危险。

13.7.6 对CD数据库应用程序的总结

现在,我们已经把CD数据库应用程序分为客户和服务器两部分了,这使我们可以对用户界面和底层的数据库技术分别进行独立的开发。我们可以看到,一个精心定义的数据库接口可以让应用程序的每个主要部分充分地使用计算机资源。进一步地,我们还可以把管道实现方案改进为网络实现方案,并使用一个专用的数据库服务器。我们将在第15章学习更多的网络编程。