CD数据库程序
现在我们已经了解了我们可以如何使用有名管道来实现一个简单的客户端/服务器系统,我们可以重新查看我们的CD数据库程序并且进行相应的修改。我们同时组合了一些信号处理从而允许我们在进程被中断时进行一些清理动作。我们会使用我们前面的具有一个命令行接口的dbm版本,从而进可能直接的查看代码。
在我们更为详细的讨论新版本的代码之前,让我们先编译这个程序。如果我们有由Web站点所获得的源代码,就可以使用makefile来编译生成server与client程序。
输入server -i使得程序初始化一个新的CD数据库。
无需多言,客户端直到服务器启动并运行时才运行。下面是makefile文件,显示程序是如何组合在一起的。
all: server client CC=cc CFLAGS= -pedantic -Wall # For debugging un-comment the next line # DFLAGS=-DDEBUG_TRACE=1 -g # Where, and which version, of dbm are we using. # This assumes gdbm is pre-installed in a standard place, but we are # going to use the gdbm compatibility routines, that make it emulate ndbm. # We do this because ndbm is the ‘most standard’ of the dbm versions. # Depending on your distribution, these may need changing. DBM_INC_PATH=/usr/include/gdbm DBM_LIB_PATH=/usr/lib DBM_LIB_FILE=gdbm .c.o: $(CC) $(CFLAGS) -I$(DBM_INC_PATH) $(DFLAGS) -c $< app_ui.o: app_ui.c cd_data.h cd_dbm.o: cd_dbm.c cd_data.h client_f.o: clientif.c cd_data.h cliserv.h pipe_imp.o: pipe_imp.c cd_data.h cliserv.h server.o: server.c cd_data.h cliserv.h client: app_ui.o clientif.o pipe_imp.o $(CC) -o client $(DFLAGS) app_ui.o clientif.o pipe_imp.o server: server.o cd_dbm.o pipe_imp.o $(CC) -o server -L$(DBM_LIB_PATH) $(DFLAGS) server.o cd_dbm.o pipe_imp.o - l$(DBM_LIB_FILE) clean: rm -f server client_app *.o *~
目标
我们的目标是将程序中处理数据库的部分与与处理用户界面的部分分离开来。我们同时希望运行一个服务器进程,但是允许多个并发客户端。我们同时希望在已有的代码上进行最小的修改。如果可能,我们保持已有的代码不变。
为了使得事情简单,我们同时希望可以在程序内部创建与删除管道,所以不需要系统管理员在我们可以使用这些程序之前创建有名管道。
另外保证我们绝不会在一件事情上"忙等待"浪费CPU时间也是很重要的。正如我们所看到的,Linux允许我们阻塞,等待事件而不是使用重要的资源。我们应使用管道的阻塞特性来保证我们可以更为有效的使用CPU。总之,在理论上,服务器应可以为一个请求的到达等待几个小时。
实现
在前面第7章中单进程版本的程序中,我们使用了一个数据访问函数集合用于数据操作。他们是:
int database_initialize(const int new_database); void database_close(void); cdc_entry get_cdc_entry(const char *cd_catalog_ptr); cdt_entry get_cdt_entry(const char *cd_catalog_ptr, const int track_no); int add_cdc_entry(const cdc_entry entry_to_add); int add_cdt_entry(const cdt_entry entry_to_add); int del_cdc_entry(const char *cd_catalog_ptr); int del_cdt_entry(const char *cd_catalog_ptr, const int track_no); cdc_entry search_cdc_entry(const char *cd_catalog_ptr, int *first_call_ptr);
这些函数为了在客户端与服务器之间进行区分提供了一个合适的场所。
在单进程的实现中,我们可以将这个程序看作具有两部分,尽管他们被编译在一起作为一个程序,如图13-6所示。
在客户端-服务器的实现中,我们希望在程序的两个主要部分之间插入一些合理的有名管道并且提供代码。图13-7显示我们所需要的结构。
在我们的实现中,我们选择将客户端与服务器接口例程放在同一个文件中,pipe_imp.c。这会将所有依赖于用于客户端/服务器实现所用的有名管道的代码放在一个文件中。要传递的数据的格式化与打包与实现有名管道的例程相分离。程序中的调用结构如图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。
首先我们来看一下cliserv.h。这个文件定义了客户端/服务器接口。这是客户端与服务器实现所需要的。
试验--头文件cliserv.h
1 下面是所需要的#include头文件声明:
#include #include #include #include #include #include #include
2 然后我们定义有名管道。我们使用一个管道用于服务器,另一个管道用于所有的客户端。因为也许会有多个客户端,客户端在其名字中组合一个进程ID来保证他的管道是唯一的:
#define SERVER_PIPE “/tmp/server_pipe” #define CLIENT_PIPE “/tmp/client_%d_pipe” #define ERR_TEXT_LEN 80
3 我们将命令实现为枚举类型,而不是#define定义。
这是一个好办法,允许编译器进行更多的类型检测工作,同时有助于程序的调试,因为许多调试器可以显示枚举常量的名字,但是不能显示由#define指令所定义的名字。
第一个typdef指定了将要发送给服务器的请求类型;第二个指定了服务器向客户端的响应类型。
typedef enum { s_create_new_database = 0, s_get_cdc_entry, s_get_cdt_entry, s_add_cdc_entry, s_add_cdt_entry, s_del_cdc_entry, s_del_cdt_entry, s_find_cdc_entry } client_request_e; typedef enum { r_success = 0, r_failure, r_find_no_more } server_response_e;
4 接下来我们声明了一个构成可以在两个进程之间双向传递的消息的结构。
typedef struct { pid_t client_pid; client_request_e request; server_response_e response; cdc_entry cdc_entry_data; cdt_entry cdt_entry_data; char error_text[ERR_TEXT_LEN + 1]; } message_db_t;
5 最后,我们来了解执行数据传输的管道接口函数,实现在pipe_imp.c中。这些函数分为服务器端与客户端函数,分别在第一个与第二个块中:
int server_starting(void); void server_ending(void); int read_request_from_client(message_db_t *rec_ptr); int start_resp_to_client(const message_db_t mess_to_send); int send_resp_to_client(const message_db_t mess_to_send); void end_resp_to_client(void); int client_starting(void); void client_ending(void); int send_mess_to_server(message_db_t mess_to_send); int start_resp_from_server(void); int read_resp_from_server(message_db_t *rec_ptr); void end_resp_from_server(void);
我们将讨论的其余部分分为客户端接口函数与定义在pipe_imp.c中的服务器端与客户端函数的详细讨论,而且我们会讨论必须的源代码。
客户端接口函数
现在我们来探讨client_if.c。他为数据库访问例程提供了一个虚拟版本,他将请求编码进入message_db_t结构中,然后使用pipe_imp.c中的例程将这些请求传递给服务器。这会使得我们在原始的app_ui.c上进行最小的修改。
试验--客户解释器
1 这个文件实现了在cd_data.h中定义的9个数据库函数。他通过向服务器传递请求并且由函数中返回服务器响应来实现操作,类似于一个中间人。这个文件由#include与常量定义开始:
#define _POSIX_SOURCE #include #include #include #include #include #include #include #include "cd_data.h" #include "cliserv.h"
2 静态变量mypid减少了所需要的getpid函数的调用次数。我们使用一个局部函数,read_one_response,来减少重复的代码:
static pid_t mypid; static int read_one_response(message_db_t *rec_ptr);
3 database_initialize与close例程仍然被调用,但是现在分别用来初始化管道接口的客户端和当客户端退出时移除多余的有名管道。
int database_initialize(const int new_database) { if (!client_starting()) return(0); mypid = getpid(); return(1); } /* database_initialize */ void database_close(void) { client_ending(); }
4 get_cdc_entry例被调用用来在指定一个CD类别标题的情况下由数据中获取一个类别实体。在这里我们将请求编码进message_db_t结构中,并且将其传递给服务器。然后我们读取返回的响应放入另一个不同的message_db_t结构中。如果找到一个实体,他就会被作为一个 cdc_entry结构被包含进入message_db_t结构中,所以我们需要介绍结构的相关部分:
cdc_entry get_cdc_entry(const char *cd_catalog_ptr) { cdc_entry ret_val; message_db_t mess_send; message_db_t mess_ret; ret_val.catalog[0] = ‘\0’; mess_send.client_pid = mypid; mess_send.request = s_get_cdc_entry; strcpy(mess_send.cdc_entry_data.catalog, cd_catalog_ptr); if (send_mess_to_server(mess_send)) { if (read_one_response(&mess_ret)) { if (mess_ret.response == r_success) { ret_val = mess_ret.cdc_entry_data; } else { fprintf(stderr, “%s”, mess_ret.error_text); } } else { fprintf(stderr, “Server failed to respond\n”); } } else { fprintf(stderr, “Server not accepting requests\n”); } return(ret_val); }
5 下面是我们用来避免重复代码的read_one_response函数源码:
static int read_one_response(message_db_t *rec_ptr) { int return_code = 0; if (!rec_ptr) return(0); if (start_resp_from_server()) { if (read_resp_from_server(rec_ptr)) { return_code = 1; } end_resp_from_server(); } return(return_code); }
6 其他的get_xxx,del_xxx与add_xxx例程的实现方式与get_cdc_entry函数类似,为了完整,我们在这里进行介绍。首先是用来读取CD音轨的函数源码:
cdt_entry get_cdt_entry(const char *cd_catalog_ptr, const int track_no) { cdt_entry ret_val; message_db_t mess_send; message_db_t mess_ret; ret_val.catalog[0] = ‘\0’; mess_send.client_pid = mypid; mess_send.request = s_get_cdt_entry; strcpy(mess_send.cdt_entry_data.catalog, cd_catalog_ptr); mess_send.cdt_entry_data.track_no = track_no; if (send_mess_to_server(mess_send)) { if (read_one_response(&mess_ret)) { if (mess_ret.response == r_success) { ret_val = mess_ret.cdt_entry_data; } else { fprintf(stderr, “%s”, mess_ret.error_text); } } else { fprintf(stderr, “Server failed to respond\n”); } } else { fprintf(stderr, “Server not accepting requests\n”); } return(ret_val); }
7 下面是两个是用来添加数据的函数,第一个向类别中添加,而第二个向音轨数据中添加:
int add_cdc_entry(const cdc_entry entry_to_add) { message_db_t mess_send; message_db_t mess_ret; mess_send.client_pid = mypid; mess_send.request = s_add_cdc_entry; mess_send.cdc_entry_data = entry_to_add; if (send_mess_to_server(mess_send)) { if (read_one_response(&mess_ret)) { if (mess_ret.response == r_success) { return(1); } else { fprintf(stderr, “%s”, mess_ret.error_text); } } else { fprintf(stderr, “Server failed to respond\n”); } } else { fprintf(stderr, “Server not accepting requests\n”); } return(0); } int add_cdt_entry(const cdt_entry entry_to_add) { message_db_t mess_send; message_db_t mess_ret; mess_send.client_pid = mypid; mess_send.request = s_add_cdt_entry; mess_send.cdt_entry_data = entry_to_add; if (send_mess_to_server(mess_send)) { if (read_one_response(&mess_ret)) { if (mess_ret.response == r_success) { return(1); } else { fprintf(stderr, “%s”, mess_ret.error_text); } } else { fprintf(stderr, “Server failed to respond\n”); } } else { fprintf(stderr, “Server not accepting requests\n”); } return(0); }
8 最后是两个用来数据删除的函数:
int del_cdc_entry(const char *cd_catalog_ptr) { message_db_t mess_send; message_db_t mess_ret; mess_send.client_pid = mypid; mess_send.request = s_del_cdc_entry; strcpy(mess_send.cdc_entry_data.catalog, cd_catalog_ptr); if (send_mess_to_server(mess_send)) { if (read_one_response(&mess_ret)) { if (mess_ret.response == r_success) { return(1); } else { fprintf(stderr, “%s”, mess_ret.error_text); } } else { fprintf(stderr, “Server failed to respond\n”); } } else { fprintf(stderr, “Server not accepting requests\n”); } return(0); } int del_cdt_entry(const char *cd_catalog_ptr, const int track_no) { message_db_t mess_send; message_db_t mess_ret; mess_send.client_pid = mypid; mess_send.request = s_del_cdt_entry; strcpy(mess_send.cdt_entry_data.catalog, cd_catalog_ptr); mess_send.cdt_entry_data.track_no = track_no; if (send_mess_to_server(mess_send)) { if (read_one_response(&mess_ret)) { if (mess_ret.response == r_success) { return(1); } else { fprintf(stderr, “%s”, mess_ret.error_text); } } else { fprintf(stderr, “Server failed to respond\n”); } } else { fprintf(stderr, “Server not accepting requests\n”); } return(0); } |