数据管理(八)--CD程序
CD程序
我们已经了解了环境以及管理数据的相关内容了,现在是更新程序的时候了。dbm数据库看起来对于存储我们的CD信息是十分合适的,所以我们会将dbm数据用作我们的新实现的基础。
更新设计
因为这次更新涉及到一个重要的代码重写,所以现在我们需要看一下我们的设计描述以确定是否需要修改。使用以逗号分隔的可以变化的文件来存储信息,尽管在Shell中很容易实现,但是已经证明是十分严格的了。大量的CD标题以及音轨信息在其中需要大量的逗号。如果我们使用dbm就可以放弃这种分隔方法,所以这是我们的设计需要修改的一个元素。
使用单独的文件来分离标题与音轨之间的信息,看起来是一个好主意,所以我们也会使用这个逻辑安排。
前面的实现看起来在某种程序上将程序的访问部分与用户接口部分相混合,至少是因为他们都在一个单独的文件中实现。在这个实现中,我们使用一个头文件来描述数据访问所需要的数据以及方法,并且将用户接口与数据操作实现在另一个单独的文件中。
尽管我们可以继续保持用户接口的curses实现,但是我们会回归到一个简单的基于命令行的系统。这会使得程序的用户接口部分更为矮小和简单,并且允许我们关注于其他的实现部分。
尽管我们在dbm代码中不能使用SQL,但是我们可以使用SQL术语以及更为通常的方式来表达我们的新数据库。如果我们不熟悉SQL也不要担心;我们会解释这些定义。我们将会在第八章了解更多有关SQL的内容。在代码中,数据表可以用下面的方法来描述:
CREATE TABLE cdc_entry (
catalog CHAR(30) PRIMARY KEY REFERENCES cdt_entry(catalog),
title CHAR(70),
type CHAR(30),
artist CHAR(70)
);
CREATE TABLE cdt_entry (
catalog CHAR(30) REFERENCES cdc_entry(catalog),
track_no INTEGER,
track_txt CHAR(70),
PRIMARY KEY(catalog, track_no)
);
这个简短的描述告诉我们数据域的名字与尺寸。对于cdc_entry表,他告诉我们对于每一个实现记录都有一个唯一的类别。对于cdc_entry表,他告诉我们音轨信息号不可以为0,而且catalog与track_no的组合是唯一的。
使用dbm的CD数据库程序
我们现在将要使用dbm数据来实现我们的程序从而存储我们需要的信息,使用的文件为cd_data.h,app_ui.c以及cd_access.c。
我们同时要将我们的用户界面重写为一个命令行程序。在这本书的后面部分,当我们探讨使用不同的客户/服务器机制来实现在我们的程序,以及最后使用Web浏览器跨越网络来访问程序时,会重用这个程序的数据库接口以及部分用户接口。将接口转换为一个简单的命令行驱动的接口可以很容易的使得我们的关注于程序的重要部分,而不是接口。
下面我们将要探讨的是头文件cd_data.h以及在后面的章节中cd_access.c中几次重用的函数。
试验--cd_data.h
我们会由头文件开始,定义我们数据的结构,以及我们将会用于访问数据的函数。
1 这是为CD数据库而定义的数据结构。他定义了组成数据库的两个数据表的结构以及尺寸。我们将会由定义我们将会用到的数据域的尺寸以及两个结构开始:一个结构用户类别记录,而另一个用于音轨记录。
/* The catalog table */
#define CAT_CAT_LEN 30
#define CAT_TITLE_LEN 70
#define CAT_TYPE_LEN 30
#define CAT_ARTIST_LEN 70
typedef struct {
char catalog[CAT_CAT_LEN + 1];
char title[CAT_TITLE_LEN + 1];
char type[CAT_TYPE_LEN + 1];
char artist[CAT_ARTIST_LEN + 1];
} cdc_entry;
/* The tracks table, one entry per track */
#define TRACK_CAT_LEN CAT_CAT_LEN
#define TRACK_TTEXT_LEN 70
typedef struct {
char catalog[TRACK_CAT_LEN + 1];
int track_no;
char track_txt[TRACK_TTEXT_LEN + 1];
} cdt_entry;
2 现在我们有了一些数据结构,我们可以定义我们将会用到的访问函数了。以cdc_开始的函数用于类别记录;而以cdt_开始的函数则用于音轨记录。
/* Initialization and termination functions */
int database_initialize(const int new_database);
void database_close(void);
/* two for simple data retrieval */
cdc_entry get_cdc_entry(const char *cd_catalog_ptr);
cdt_entry get_cdt_entry(const char *cd_catalog_ptr, const int track_no);
/* two for data addition */
int add_cdc_entry(const cdc_entry entry_to_add);
int add_cdt_entry(const cdt_entry entry_to_add);
/* two for data deletion */
int del_cdc_entry(const char *cd_catalog_ptr);
int del_cdt_entry(const char *cd_catalog_ptr, const int track_no);
/* one search function */
cdc_entry search_cdc_entry(const char *cd_catalog_ptr, int *first_call_ptr);
试验--app_ui.c
现在我们开始探讨用户接口。这会给我们一个相对简单的程序,通过他可以访问数据库函数。我们将会在一个单独的文件中实现这个接口。
1 如平时一样,我们由头文件开始:
#define _XOPEN_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include “cd_data.h”
#define TMP_STRING_LEN 125 /* this number must be larger than the biggest
single string in any database structure */
2 我们定义我们的菜单选项。在这里使用#defined常量定义的形式,因为他会允许编译器检查菜单选项变量的类型。
typedef enum {
mo_invalid,
mo_add_cat,
mo_add_tracks,
mo_del_cat,
mo_find_cat,
mo_list_cat_tracks,
mo_del_tracks,
mo_count_entries,
mo_exit
} menu_options;
3 现在我们编写局部函数的原型。要记住,实际访问数据的原型包含在cd_data.h中。
static int command_mode(int argc, char *argv[]);
static void announce(void);
static menu_options show_menu(const cdc_entry *current_cdc);
static int get_confirm(const char *question);
static int enter_new_cat_entry(cdc_entry *entry_to_update);
static void enter_new_track_entries(const cdc_entry *entry_to_add_to);
static void del_cat_entry(const cdc_entry *entry_to_delete);
static void del_track_entries(const cdc_entry *entry_to_delete);
static cdc_entry find_cat(void);
static void list_tracks(const cdc_entry *entry_to_use);
static void count_all_entries(void);
static void display_cdc(const cdc_entry *cdc_to_show);
static void display_cdt(const cdt_entry *cdt_to_show);
static void strip_return(char *string_to_strip);
4 最后我们进行main函数。这个函数由以保证我们用来保持当前选中的CD类别记录的音轨信息的current_cdc_entry已经进行了初始化而开始的。同时我们也要分析命令行,从而得到所运行的程序,并且初始化数据库。
void main(int argc, char *argv[])
{
menu_options current_option;
cdc_entry current_cdc_entry;
int command_result;
memset(¤t_cdc_entry, ‘/0’, sizeof(current_cdc_entry));
if (argc > 1) {
command_result = command_mode(argc, argv);
exit(command_result);
}
announce();
if (!database_initialize(0)) {
fprintf(stderr, “Sorry, unable to initialize database/n”);
fprintf(stderr, “To create a new database use %s -i/n”, argv[0]);
exit(EXIT_FAILURE);
}
5 现在我们已经准备好处理用户输入了。我们在一个循环中,提示菜单选项,并且进行处理,直到用户选择了退出选项。注意,在这里我们将current_cdc_entry结构传递给show_menu函数。我们这样做就要使得如果当前选中了一个类别时菜单选项可以发生变化。
while(current_option != mo_exit) {
current_option = show_menu(¤t_cdc_entry);
switch(current_option) {
case mo_add_cat:
if (enter_new_cat_entry(¤t_cdc_entry)) {
if (!add_cdc_entry(current_cdc_entry)) {
fprintf(stderr, “Failed to add new entry/n”);
memset(¤t_cdc_entry, ‘/0’,
sizeof(current_cdc_entry));
}
}
break;
case mo_add_tracks:
enter_new_track_entries(¤t_cdc_entry);
break;
case mo_del_cat:
del_cat_entry(¤t_cdc_entry);
break;
case mo_find_cat:
current_cdc_entry = find_cat();
break;
case mo_list_cat_tracks:
list_tracks(¤t_cdc_entry);
break;
case mo_del_tracks:
del_track_entries(¤t_cdc_entry);
break;
case mo_count_entries:
count_all_entries();
break;
case mo_exit:
break;
case mo_invalid:
break;
default:
break;
} /* switch */
} /* while */
6 当main循环退出时,我们关闭数据并且退回到环境。通过announce函数来打印欢迎界面:
database_close();
exit(EXIT_SUCCESS);
} /* main */
static void announce(void)
{
printf(“/n/nWelcome to the demonstration CD catalog database /
program/n”);
}
7 在这里我们实现了show_menu函数。这个函数会检测当前类别是否使用类别名的第一个字母被选中。如果一个类别被选中就会出现更多的选项。
static menu_options show_menu(const cdc_entry *cdc_selected)
{
char tmp_str[TMP_STRING_LEN + 1];
menu_options option_chosen = mo_invalid;
while (option_chosen == mo_invalid) {
if (cdc_selected->catalog[0]) {
printf(“/n/nCurrent entry: “);
printf(“%s, %s, %s, %s/n”, cdc_selected->catalog,
cdc_selected->title,
cdc_selected->type,
cdc_selected->artist);
printf(“/n”);
printf(“1 - add new CD/n”);
printf(“2 - search for a CD/n”);
printf(“3 - count the CDs and tracks in the database/n”);
printf(“4 - re-enter tracks for current CD/n”);
printf(“5 - delete this CD, and all its tracks/n”);
printf(“6 - list tracks for this CD/n”);
printf(“q - quit/n”);
printf(“/nOption: “);
fgets(tmp_str, TMP_STRING_LEN, stdin);
switch(tmp_str[0]) {
case ‘1’: option_chosen = mo_add_cat; break;
case ‘2’: option_chosen = mo_find_cat; break;
case ‘3’: option_chosen = mo_count_entries; break;
case ‘4’: option_chosen = mo_add_tracks; break;
case ‘5’: option_chosen = mo_del_cat; break;
case ‘6’: option_chosen = mo_list_cat_tracks; break;
case ‘q’: option_chosen = mo_exit; break;
}
}
else {
printf(“/n/n”);
printf(“1 - add new CD/n”);
printf(“2 - search for a CD/n”);
printf(“3 - count the CDs and tracks in the database/n”);
printf(“q - quit/n”);
printf(“/nOption: “);
fgets(tmp_str, TMP_STRING_LEN, stdin);
switch(tmp_str[0]) {
case ‘1’: option_chosen = mo_add_cat; break;
case ‘2’: option_chosen = mo_find_cat; break;
case ‘3’: option_chosen = mo_count_entries; break;
case ‘q’: option_chosen = mo_exit; break;
}
}
} /* while */
return(option_chosen);
}
8 有多个地方我们希望询问用户他是否确定所请求的动作。我们并不是在代码中的多个地方询问用户,相反,我们会将其作为一个单独的get_confirm函数来实现:
static int get_confirm(const char *question)
{
char tmp_str[TMP_STRING_LEN + 1];
printf(“%s”, question);
fgets(tmp_str, TMP_STRING_LEN, stdin);
if (tmp_str[0] == ‘Y’ || tmp_str[0] == ‘y’) {
return(1);
}
return(0);
}
9 函数enter_new_cat_entry允许用户可以输入新的类别记录。但是并不希望存储由fgets函数所返回的回车,所以我们要去掉回车。
static int enter_new_cat_entry(cdc_entry *entry_to_update)
{
cdc_entry new_entry;
char tmp_str[TMP_STRING_LEN + 1];
memset(&new_entry, ‘/0’, sizeof(new_entry));
printf(“Enter catalog entry: “);
(void)fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
strncpy(new_entry.catalog, tmp_str, CAT_CAT_LEN - 1);
printf(“Enter title: “);
(void)fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
strncpy(new_entry.title, tmp_str, CAT_TITLE_LEN - 1);
printf(“Enter type: “);
(void)fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
strncpy(new_entry.type, tmp_str, CAT_TYPE_LEN - 1);
printf(“Enter artist: “);
(void)fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
strncpy(new_entry.artist, tmp_str, CAT_ARTIST_LEN - 1);
printf(“/nNew catalog entry entry is :-/n”);
display_cdc(&new_entry);
if (get_confirm(“Add this entry ?”)) {
memcpy(entry_to_update, &new_entry, sizeof(new_entry));
return(1);
}
return(0);
}
10 现在我们要来讨论输入音轨信息的enter_new_track_entries函数。相比类别记录函数,这个函数要复杂一些,因为我们允许保存已经存在的音轨记录。
static void enter_new_track_entries(const cdc_entry *entry_to_add_to)
{
cdt_entry new_track, existing_track;
char tmp_str[TMP_STRING_LEN + 1];
int track_no = 1;
if (entry_to_add_to->catalog[0] == ‘/0’) return;
printf(“/nUpdating tracks for %s/n”, entry_to_add_to->catalog);
printf(“Press return to leave existing description unchanged,/n”);
printf(“ a single d to delete this and remaining tracks,/n”);
printf(“ or new track description/n”);
while(1) {
11 首先,我们需要检测是否存在当前音轨号的音轨记录。依据我们需要查找的内容,我们可以改变提示符。
memset(&new_track, ‘/0’, sizeof(new_track));
existing_track = get_cdt_entry(entry_to_add_to->catalog,
track_no);
if (existing_track.catalog[0]) {
printf(“/tTrack %d: %s/n”, track_no,
existing_track.track_txt);
printf(“/tNew text: “);
}
else {
printf(“/tTrack %d description: “, track_no);
}
fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
12 如果并不存在所要查找的音轨信息,而用户并没有添加音轨信息,我们假设用户并没有音轨信息需要添加。
if (strlen(tmp_str) == 0) {
if (existing_track.catalog[0] == ‘/0’) {
/* no existing entry, so finished adding */
break;
}
else {
/* leave existing entry, jump to next track */
track_no++;
continue;
}
}
13 如果用户输入了一个d字符,这会删除当前以及更高记录号的音轨信息。如果并没有查找到要删除的音轨信息,del_cat_entry函数会返回false。
if ((strlen(tmp_str) == 1) && tmp_str[0] == ‘d’) {
/* delete this and remaining tracks */
while (del_cdt_entry(entry_to_add_to->catalog, track_no)) {
track_no++;
}
break;
}
14 现在我们要来编码添加一个新的音轨信息或是更新一个已经存在的音轨信息。我们使用cdt_entry构成new_track,然后调用数据库函数add_cdt_entry将其添加到数据库中。
strncpy(new_track.track_txt, tmp_str, TRACK_TTEXT_LEN - 1);
strcpy(new_track.catalog, entry_to_add_to->catalog);
new_track.track_no = track_no;
if (!add_cdt_entry(new_track)) {
fprintf(stderr, “Failed to add new track/n”);
break;
}
track_no++;
} /* while */
}
15 函数del_cat_entry删除一个类别记录。我们绝不会允许一个不存在类别的音轨记录存在。
static void del_cat_entry(const cdc_entry *entry_to_delete)
{
int track_no = 1;
int delete_ok;
display_cdc(entry_to_delete);
if (get_confirm(“Delete this entry and all it’s tracks? “)) {
do {
delete_ok = del_cdt_entry(entry_to_delete->catalog,
track_no);
track_no++;
} while(delete_ok);
if (!del_cdc_entry(entry_to_delete->catalog)) {
fprintf(stderr, “Failed to delete entry/n”);
}
}
}
16 下一个函数是删除一个类别的所有音轨信息的程序:
static void del_track_entries(const cdc_entry *entry_to_delete)
{
int track_no = 1;
int delete_ok;
display_cdc(entry_to_delete);
if (get_confirm(“Delete tracks for this entry? “)) {
do {
delete_ok = del_cdt_entry(entry_to_delete->catalog, track_no);
track_no++;
} while(delete_ok);
}
}
17 下面,我们创建一个非常简单的类别查找程序。我们允许用户输入一个字符串,然后检测包含这个字符串的类别。因为也许会匹配多个记录,我们只是简单的依次用户提供匹配的记录:
static cdc_entry find_cat(void)
{
cdc_entry item_found;
char tmp_str[TMP_STRING_LEN + 1];
int first_call = 1;
int any_entry_found = 0;
int string_ok;
int entry_selected = 0;
do {
string_ok = 1;
printf(“Enter string to search for in catalog entry: “);
fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
if (strlen(tmp_str) > CAT_CAT_LEN) {
fprintf(stderr, “Sorry, string too long, maximum %d /
characters/n”, CAT_CAT_LEN);
string_ok = 0;
}
} while (!string_ok);
while (!entry_selected) {
item_found = search_cdc_entry(tmp_str, &first_call);
if (item_found.catalog[0] != ‘/0’) {
any_entry_found = 1;
printf(“/n”);
display_cdc(&item_found);
if (get_confirm(“This entry? “)) {
entry_selected = 1;
}
}
else {
if (any_entry_found) printf(“Sorry, no more matches found/n”);
else printf(“Sorry, nothing found/n”);
break;
}
}
return(item_found);
}
18 list_tracks是一个打印一个指定的类别内的所音轨的函数:
static void list_tracks(const cdc_entry *entry_to_use)
{
int track_no = 1;
cdt_entry entry_found;
display_cdc(entry_to_use);
printf(“/nTracks/n”);
do {
entry_found = get_cdt_entry(entry_to_use->catalog,
track_no);
if (entry_found.catalog[0]) {
display_cdt(&entry_found);
track_no++;
}
} while(entry_found.catalog[0]);
(void)get_confirm(“Press return”);
} /* list_tracks */
19 count_all_entries函数统计所有的音轨信息记录数:
static void count_all_entries(void)
{
int cd_entries_found = 0;
int track_entries_found = 0;
cdc_entry cdc_found;
cdt_entry cdt_found;
int track_no = 1;
int first_time = 1;
char *search_string = “”;
do {
cdc_found = search_cdc_entry(search_string, &first_time);
if (cdc_found.catalog[0]) {
cd_entries_found++;
track_no = 1;
do {
cdt_found = get_cdt_entry(cdc_found.catalog, track_no);
if (cdt_found.catalog[0]) {
track_entries_found++;
track_no++;
}
} while (cdt_found.catalog[0]);
}
} while (cdc_found.catalog[0]);
printf(“Found %d CDs, with a total of %d tracks/n”, cd_entries_found,
track_entries_found);
(void)get_confirm(“Press return”);
}
20 现在我们编写display_cdc函数,一个用于显示一个类别记录的函数:
static void display_cdc(const cdc_entry *cdc_to_show)
{
printf(“Catalog: %s/n”, cdc_to_show->catalog);
printf(“/ttitle: %s/n”, cdc_to_show->title);
printf(“/ttype: %s/n”, cdc_to_show->type);
printf(“/tartist: %s/n”, cdc_to_show->artist);
}
以及用于显示单一音轨记录的display_cdt函数:
static void display_cdt(const cdt_entry *cdt_to_show)
{
printf(“%d: %s/n”, cdt_to_show->track_no, cdt_to_show->track_txt);
}
21 函数strip_return用于移除字符串末尾的回车符。记住,与Unix类似,在Linux中,使用一个回车来代表一行的结束:
static void strip_return(char *string_to_strip)
{
int len;
len = strlen(string_to_strip);
if (string_to_strip[len - 1] == ‘/n’) string_to_strip[len - 1] = ‘/0’;
}
22 command_mode是一个用于分析命令行参数的函数。getopt函数是一个很好的方法,可以用来保证我们的程序接受符合标准Linux约定的参数。
static int command_mode(int argc, char *argv[])
{
int c;
int result = EXIT_SUCCESS;
char *prog_name = argv[0];
/* these externals used by getopt */
extern char *optarg;
extern optind, opterr, optopt;
while ((c = getopt(argc, argv, “:i”)) != -1) {
switch(c) {
case ‘i’:
if (!database_initialize(1)) {
result = EXIT_FAILURE;
fprintf(stderr, “Failed to initialize database/n”);
}
break;
case ‘:’:
case ‘?’:
default:
fprintf(stderr, “Usage: %s [-i]/n”, prog_name);
result = EXIT_FAILURE;
break;
} /* switch */
} /* while */
return(result);
}
试验--cd_access.c
现在我们来讨论访问dbm数据的函数。
1 如平时一样,我们由#include语句开始。然后我们使用#define语句来指定我们将会用于存储数据文件。
#define _XOPEN_SOURCE
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <ndbm.h>
#include “cd_data.h”
#define CDC_FILE_BASE “cdc_data”
#define CDT_FILE_BASE “cdt_data”
#define CDC_FILE_DIR “cdc_data.dir”
#define CDC_FILE_PAG “cdc_data.pag”
#define CDT_FILE_DIR “cdt_data.dir”
#define CDT_FILE_PAG “cdt_data.pag”
2 我们使用下面的两个局部变量来保存当前数据库的信息:
static DBM *cdc_dbm_ptr = NULL;
static DBM *cdt_dbm_ptr = NULL;
3 默认情况下,database_initialize函数会打开一个已经存在数据库,但是通过传递一个非零的参数new_database,我们可以强制其创建一个新的数据库,但是移除当前已存在的数据库。如果数据库成功的进行初始化,两个数据库也会进行初始,来表明已打开了一个数据库。
int database_initialize(const int new_database)
{
int open_mode = O_CREAT | O_RDWR;
/* If any existing database is open then close it */
if (cdc_dbm_ptr) dbm_close(cdc_dbm_ptr);
if (cdt_dbm_ptr) dbm_close(cdt_dbm_ptr);
if (new_database) {
/* delete the old files */
(void) unlink(CDC_FILE_PAG);
(void) unlink(CDC_FILE_DIR);
(void) unlink(CDT_FILE_PAG);
(void) unlink(CDT_FILE_DIR);
}
/* Open some new files, creating them if required */
cdc_dbm_ptr = dbm_open(CDC_FILE_BASE, open_mode, 0644);
cdt_dbm_ptr = dbm_open(CDT_FILE_BASE, open_mode, 0644);
if (!cdc_dbm_ptr || !cdt_dbm_ptr) {
fprintf(stderr, “Unable to create database/n”);
cdc_dbm_ptr = cdt_dbm_ptr = NULL;
return (0);
}
return (1);
}
4 database_close函数只是简单的关闭所打开的数据库,并且设置两个数据库指针指向null来表明当前没有数据库打开。
void database_close(void)
{
if (cdc_dbm_ptr) dbm_close(cdc_dbm_ptr);
if (cdt_dbm_ptr) dbm_close(cdt_dbm_ptr);
cdc_dbm_ptr = cdt_dbm_ptr = NULL;
}
5 接下来我们会编写一个函数,当向这个函数传递一个类别字符串时,此函数会取回此类别记录。如果没有找到此记录,返回的数据会有一个空的类别区域。
cdc_entry get_cdc_entry(const char *cd_catalog_ptr)
{
cdc_entry entry_to_return;
char entry_to_find[CAT_CAT_LEN + 1];
datum local_data_datum;
datum local_key_datum;
memset(&entry_to_return, ‘/0’, sizeof(entry_to_return));
6 我们会在开始时进行一些检测,来保证数据库已经打开,并且我们传递了合理的参数--也就是说,查找关键字只包含可用的字符串以及null。
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (entry_to_return);
if (!cd_catalog_ptr) return (entry_to_return);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (entry_to_return);
memset(&entry_to_find, ‘/0’, sizeof(entry_to_find));
strcpy(entry_to_find, cd_catalog_ptr);
7 我们设置dbm函数所需要的datum结构,然后使用dbm_fetch函数来取出数据。如果没有取回任何数据,我们就会返回我们先前所初始化的空的entry_to_return结构。
local_key_datum.dptr = (void *) entry_to_find;
local_key_datum.dsize = sizeof(entry_to_find);
memset(&local_data_datum, ‘/0’, sizeof(local_data_datum));
local_data_datum = dbm_fetch(cdc_dbm_ptr, local_key_datum);
if (local_data_datum.dptr) {
memcpy(&entry_to_return, (char *)local_data_datum.dptr,
local_data_datum.dsize);
}
return (entry_to_return);
} /* get_cdc_entry */
8 我们最好也可以得到一个单一的音轨信息,与get_cdc_entry函数相类似,而这也正是下一个函数所要做的,但是需要一个指向类别字符的指针以及一个音轨序号作为参数。
cdt_entry get_cdt_entry(const char *cd_catalog_ptr, const int track_no)
{
cdt_entry entry_to_return;
char entry_to_find[CAT_CAT_LEN + 10];
datum local_data_datum;
datum local_key_datum;
memset(&entry_to_return, ‘/0’, sizeof(entry_to_return));
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (entry_to_return);
if (!cd_catalog_ptr) return (entry_to_return);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (entry_to_return);
/* set up the search key, which is a composite key of catalog entry
and track number */
memset(&entry_to_find, ‘/0’, sizeof(entry_to_find));
sprintf(entry_to_find, “%s %d”, cd_catalog_ptr, track_no);
local_key_datum.dptr = (void *) entry_to_find;
local_key_datum.dsize = sizeof(entry_to_find);
memset(&local_data_datum, ‘/0’, sizeof(local_data_datum));
local_data_datum = dbm_fetch(cdt_dbm_ptr, local_key_datum);
if (local_data_datum.dptr) {
memcpy(&entry_to_return, (char *) local_data_datum.dptr,
local_data_datum.dsize);
}
return (entry_to_return);
}
9 下一个函数,add_cdc_entry,添加一个新的类别记录:
int add_cdc_entry(const cdc_entry entry_to_add)
{
char key_to_add[CAT_CAT_LEN + 1];
datum local_data_datum;
datum local_key_datum;
int result;
/* check database initialized and parameters valid */
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);
if (strlen(entry_to_add.catalog) >= CAT_CAT_LEN) return (0);
/* ensure the search key contains only the valid string and nulls */
memset(&key_to_add, ‘/0’, sizeof(key_to_add));
strcpy(key_to_add, entry_to_add.catalog);
local_key_datum.dptr = (void *) key_to_add;
local_key_datum.dsize = sizeof(key_to_add);
local_data_datum.dptr = (void *) &entry_to_add;
local_data_datum.dsize = sizeof(entry_to_add);
result = dbm_store(cdc_dbm_ptr, local_key_datum, local_data_datum,
DBM_REPLACE);
/* dbm_store() uses 0 for success */
if (result == 0) return (1);
return (0);
}
10 add_cdt_entry添加一新的音轨记录。访问关键字是类别字符串,并且音轨序号作为组合。
int add_cdt_entry(const cdt_entry entry_to_add)
{
char key_to_add[CAT_CAT_LEN + 10];
datum local_data_datum;
datum local_key_datum;
int result;
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);
if (strlen(entry_to_add.catalog) >= CAT_CAT_LEN) return (0);
memset(&key_to_add, ‘/0’, sizeof(key_to_add));
sprintf(key_to_add, “%s %d”, entry_to_add.catalog,
entry_to_add.track_no);
local_key_datum.dptr = (void *) key_to_add;
local_key_datum.dsize = sizeof(key_to_add);
local_data_datum.dptr = (void *) &entry_to_add;
local_data_datum.dsize = sizeof(entry_to_add);
result = dbm_store(cdt_dbm_ptr, local_key_datum, local_data_datum,
DBM_REPLACE);
/* dbm_store() uses 0 for success and -ve numbers for errors */
if (result == 0)
return (1);
return (0);
}
11 如果我们可以添加一些东西,我们最好也可以删除了他们。下面这个函数删除类别记录:
int del_cdc_entry(const char *cd_catalog_ptr)
{
char key_to_del[CAT_CAT_LEN + 1];
datum local_key_datum;
int result;
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (0);
memset(&key_to_del, ‘/0’, sizeof(key_to_del));
strcpy(key_to_del, cd_catalog_ptr);
local_key_datum.dptr = (void *) key_to_del;
local_key_datum.dsize = sizeof(key_to_del);
result = dbm_delete(cdc_dbm_ptr, local_key_datum);
/* dbm_delete() uses 0 for success */
if (result == 0) return (1);
return (0);
}
12 下面这个函数等同于删除一个音轨信息。记住,音轨关键字是类别记录字符串以及一个音轨序号的组合:
int del_cdt_entry(const char *cd_catalog_ptr, const int track_no)
{
char key_to_del[CAT_CAT_LEN + 10];
datum local_key_datum;
int result;
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (0);
memset(&key_to_del, ‘/0’, sizeof(key_to_del));
sprintf(key_to_del, “%s %d”, cd_catalog_ptr, track_no);
local_key_datum.dptr = (void *) key_to_del;
local_key_datum.dsize = sizeof(key_to_del);
result = dbm_delete(cdt_dbm_ptr, local_key_datum);
/* dbm_delete() uses 0 for success */
if (result == 0) return (1);
return (0);
}
13 最后但不并不是不重要的一点,我们需要一个简单的搜索函数。他并不是非常高级,但是他确实演示了如果在不知道更多关键字的情况下如何搜索dbm记录。
因为我们并不知道存在多少记录,我们实现这个函数,在每次调用时只返回一个记录。如果没有查找到任何记录,记录就会空。要搜索整个数据库,我们通过使用一个指向整数的指针*first_call_ptr来调用这个函数,在第一次调用时此参数值应为1。然后此函数就会知道他应由数据的起始处开始搜索。在接下来的调用中,变量为0,而函数就会上一次他所查找的记录之后恢复查找。
当我们需要重新启动我们的查找时,很可能是查找另一个不同的类别记录,我们必须再一次将*first_call_ptr设置为真来调用这个函数,将参数设置为真用来初始化搜索。
在函数调用之间,函数会维护一些内部状态信息。这会隐藏客户继续查找的复杂性,并且保持查找函数实现的细节。
如果搜索字符串指向一个null字符,此时就会匹配所有的记录。
cdc_entry search_cdc_entry(const char *cd_catalog_ptr, int *first_call_ptr)
{
static int local_first_call = 1;
cdc_entry entry_to_return;
datum local_data_datum;
static datum local_key_datum; /* notice this must be static */
memset(&entry_to_return, ‘/0’, sizeof(entry_to_return));
14 如平时一样,我们由必要的检测开始:
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (entry_to_return);
if (!cd_catalog_ptr || !first_call_ptr) return (entry_to_return);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (entry_to_return);
/* protect against never passing *first_call_ptr true */
if (local_first_call) {
local_first_call = 0;
*first_call_ptr = 1;
}
15 如果这个函数已经由设置为真的*first_call_ptr参数进行了调用,我们需要由开始(或是重新开始)搜索数据库的超始处。如果*first_call_ptr并不会为真,我们只是简单的移到数据中的下一个关键字上:
if (*first_call_ptr) {
*first_call_ptr = 0;
local_key_datum = dbm_firstkey(cdc_dbm_ptr);
}
else {
local_key_datum = dbm_nextkey(cdc_dbm_ptr);
}
do {
if (local_key_datum.dptr != NULL) {
/* an entry was found */
local_data_datum = dbm_fetch(cdc_dbm_ptr, local_key_datum);
if (local_data_datum.dptr) {
memcpy(&entry_to_return, (char *) local_data_datum.dptr,
local_data_datum.dsize);
16 我们的搜索程序进行简单的检测以确定查找字符串是否包含在当前的类别记录中。
/* check if search string occurs in the entry */
if (!strstr(entry_to_return.catalog, cd_catalog_ptr))
{
memset(&entry_to_return, ‘/0’,
sizeof(entry_to_return));
local_key_datum = dbm_nextkey(cdc_dbm_ptr);
}
}
}
} while (local_key_datum.dptr &&
local_data_datum.dptr &&
(entry_to_return.catalog[0] == ‘/0’));
return (entry_to_return);
} /* search_cdc_entry */
现在我们就可以所有的内容放在一个makefile文件中。现在不要太担心,因为我们会在下一章讨论他是如何工作的。就目前而言,输入下面的内容,并且将其保存为Makefile。
all: application
INCLUDE=/usr/include/gdbm
LIBS=gdbm
CFLAGS=
app_ui.o: app_ui.c cd_data.h
gcc $(CFLAGS) -c app_ui.c
access.o: access.c cd_data.h
gcc $(CFLAGS) -I$(INCLUDE) -c access.c
application: app_ui.o access.o
gcc $(CFLAGS) -o application app_ui.o access.o -l$(LIBS)
clean:
rm -f application *.o
nodbmfiles:
rm -f *.dir *.pag
要编译我们的CD程序,在提示符下输入下面的命令:
$ make
如果一切顺利,就会在当前目录下编译成功application可执行程序。
总结
在这一章,我们已经了解了数据管理的三个方面。首先,我们了解了Linux内存系统,以及其使用是如何简单,尽管按需调度分页虚拟内存的实现非常复杂。我们同时也会发现Linux系统通过合法的内存访问来保护操作系统与其他程序。
然后我们探讨了文件锁如何使得多个程序合作访问数据。我们首先了解了一个简单的二进制信号量,然而了解一个更为复杂的情况,此是我们锁住文件的不同部分用于共享或是排他访问。接下来我们了解了dbm库,以及其存储和使用一个非常灵活的索引机制读取数据的能力。
最后,我们使用dbm库作为存储技术重新设计并且实现了我们的CD数据库程序。
我们已经了解了环境以及管理数据的相关内容了,现在是更新程序的时候了。dbm数据库看起来对于存储我们的CD信息是十分合适的,所以我们会将dbm数据用作我们的新实现的基础。
更新设计
因为这次更新涉及到一个重要的代码重写,所以现在我们需要看一下我们的设计描述以确定是否需要修改。使用以逗号分隔的可以变化的文件来存储信息,尽管在Shell中很容易实现,但是已经证明是十分严格的了。大量的CD标题以及音轨信息在其中需要大量的逗号。如果我们使用dbm就可以放弃这种分隔方法,所以这是我们的设计需要修改的一个元素。
使用单独的文件来分离标题与音轨之间的信息,看起来是一个好主意,所以我们也会使用这个逻辑安排。
前面的实现看起来在某种程序上将程序的访问部分与用户接口部分相混合,至少是因为他们都在一个单独的文件中实现。在这个实现中,我们使用一个头文件来描述数据访问所需要的数据以及方法,并且将用户接口与数据操作实现在另一个单独的文件中。
尽管我们可以继续保持用户接口的curses实现,但是我们会回归到一个简单的基于命令行的系统。这会使得程序的用户接口部分更为矮小和简单,并且允许我们关注于其他的实现部分。
尽管我们在dbm代码中不能使用SQL,但是我们可以使用SQL术语以及更为通常的方式来表达我们的新数据库。如果我们不熟悉SQL也不要担心;我们会解释这些定义。我们将会在第八章了解更多有关SQL的内容。在代码中,数据表可以用下面的方法来描述:
CREATE TABLE cdc_entry (
catalog CHAR(30) PRIMARY KEY REFERENCES cdt_entry(catalog),
title CHAR(70),
type CHAR(30),
artist CHAR(70)
);
CREATE TABLE cdt_entry (
catalog CHAR(30) REFERENCES cdc_entry(catalog),
track_no INTEGER,
track_txt CHAR(70),
PRIMARY KEY(catalog, track_no)
);
这个简短的描述告诉我们数据域的名字与尺寸。对于cdc_entry表,他告诉我们对于每一个实现记录都有一个唯一的类别。对于cdc_entry表,他告诉我们音轨信息号不可以为0,而且catalog与track_no的组合是唯一的。
使用dbm的CD数据库程序
我们现在将要使用dbm数据来实现我们的程序从而存储我们需要的信息,使用的文件为cd_data.h,app_ui.c以及cd_access.c。
我们同时要将我们的用户界面重写为一个命令行程序。在这本书的后面部分,当我们探讨使用不同的客户/服务器机制来实现在我们的程序,以及最后使用Web浏览器跨越网络来访问程序时,会重用这个程序的数据库接口以及部分用户接口。将接口转换为一个简单的命令行驱动的接口可以很容易的使得我们的关注于程序的重要部分,而不是接口。
下面我们将要探讨的是头文件cd_data.h以及在后面的章节中cd_access.c中几次重用的函数。
试验--cd_data.h
我们会由头文件开始,定义我们数据的结构,以及我们将会用于访问数据的函数。
1 这是为CD数据库而定义的数据结构。他定义了组成数据库的两个数据表的结构以及尺寸。我们将会由定义我们将会用到的数据域的尺寸以及两个结构开始:一个结构用户类别记录,而另一个用于音轨记录。
/* The catalog table */
#define CAT_CAT_LEN 30
#define CAT_TITLE_LEN 70
#define CAT_TYPE_LEN 30
#define CAT_ARTIST_LEN 70
typedef struct {
char catalog[CAT_CAT_LEN + 1];
char title[CAT_TITLE_LEN + 1];
char type[CAT_TYPE_LEN + 1];
char artist[CAT_ARTIST_LEN + 1];
} cdc_entry;
/* The tracks table, one entry per track */
#define TRACK_CAT_LEN CAT_CAT_LEN
#define TRACK_TTEXT_LEN 70
typedef struct {
char catalog[TRACK_CAT_LEN + 1];
int track_no;
char track_txt[TRACK_TTEXT_LEN + 1];
} cdt_entry;
2 现在我们有了一些数据结构,我们可以定义我们将会用到的访问函数了。以cdc_开始的函数用于类别记录;而以cdt_开始的函数则用于音轨记录。
/* Initialization and termination functions */
int database_initialize(const int new_database);
void database_close(void);
/* two for simple data retrieval */
cdc_entry get_cdc_entry(const char *cd_catalog_ptr);
cdt_entry get_cdt_entry(const char *cd_catalog_ptr, const int track_no);
/* two for data addition */
int add_cdc_entry(const cdc_entry entry_to_add);
int add_cdt_entry(const cdt_entry entry_to_add);
/* two for data deletion */
int del_cdc_entry(const char *cd_catalog_ptr);
int del_cdt_entry(const char *cd_catalog_ptr, const int track_no);
/* one search function */
cdc_entry search_cdc_entry(const char *cd_catalog_ptr, int *first_call_ptr);
试验--app_ui.c
现在我们开始探讨用户接口。这会给我们一个相对简单的程序,通过他可以访问数据库函数。我们将会在一个单独的文件中实现这个接口。
1 如平时一样,我们由头文件开始:
#define _XOPEN_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include “cd_data.h”
#define TMP_STRING_LEN 125 /* this number must be larger than the biggest
single string in any database structure */
2 我们定义我们的菜单选项。在这里使用#defined常量定义的形式,因为他会允许编译器检查菜单选项变量的类型。
typedef enum {
mo_invalid,
mo_add_cat,
mo_add_tracks,
mo_del_cat,
mo_find_cat,
mo_list_cat_tracks,
mo_del_tracks,
mo_count_entries,
mo_exit
} menu_options;
3 现在我们编写局部函数的原型。要记住,实际访问数据的原型包含在cd_data.h中。
static int command_mode(int argc, char *argv[]);
static void announce(void);
static menu_options show_menu(const cdc_entry *current_cdc);
static int get_confirm(const char *question);
static int enter_new_cat_entry(cdc_entry *entry_to_update);
static void enter_new_track_entries(const cdc_entry *entry_to_add_to);
static void del_cat_entry(const cdc_entry *entry_to_delete);
static void del_track_entries(const cdc_entry *entry_to_delete);
static cdc_entry find_cat(void);
static void list_tracks(const cdc_entry *entry_to_use);
static void count_all_entries(void);
static void display_cdc(const cdc_entry *cdc_to_show);
static void display_cdt(const cdt_entry *cdt_to_show);
static void strip_return(char *string_to_strip);
4 最后我们进行main函数。这个函数由以保证我们用来保持当前选中的CD类别记录的音轨信息的current_cdc_entry已经进行了初始化而开始的。同时我们也要分析命令行,从而得到所运行的程序,并且初始化数据库。
void main(int argc, char *argv[])
{
menu_options current_option;
cdc_entry current_cdc_entry;
int command_result;
memset(¤t_cdc_entry, ‘/0’, sizeof(current_cdc_entry));
if (argc > 1) {
command_result = command_mode(argc, argv);
exit(command_result);
}
announce();
if (!database_initialize(0)) {
fprintf(stderr, “Sorry, unable to initialize database/n”);
fprintf(stderr, “To create a new database use %s -i/n”, argv[0]);
exit(EXIT_FAILURE);
}
5 现在我们已经准备好处理用户输入了。我们在一个循环中,提示菜单选项,并且进行处理,直到用户选择了退出选项。注意,在这里我们将current_cdc_entry结构传递给show_menu函数。我们这样做就要使得如果当前选中了一个类别时菜单选项可以发生变化。
while(current_option != mo_exit) {
current_option = show_menu(¤t_cdc_entry);
switch(current_option) {
case mo_add_cat:
if (enter_new_cat_entry(¤t_cdc_entry)) {
if (!add_cdc_entry(current_cdc_entry)) {
fprintf(stderr, “Failed to add new entry/n”);
memset(¤t_cdc_entry, ‘/0’,
sizeof(current_cdc_entry));
}
}
break;
case mo_add_tracks:
enter_new_track_entries(¤t_cdc_entry);
break;
case mo_del_cat:
del_cat_entry(¤t_cdc_entry);
break;
case mo_find_cat:
current_cdc_entry = find_cat();
break;
case mo_list_cat_tracks:
list_tracks(¤t_cdc_entry);
break;
case mo_del_tracks:
del_track_entries(¤t_cdc_entry);
break;
case mo_count_entries:
count_all_entries();
break;
case mo_exit:
break;
case mo_invalid:
break;
default:
break;
} /* switch */
} /* while */
6 当main循环退出时,我们关闭数据并且退回到环境。通过announce函数来打印欢迎界面:
database_close();
exit(EXIT_SUCCESS);
} /* main */
static void announce(void)
{
printf(“/n/nWelcome to the demonstration CD catalog database /
program/n”);
}
7 在这里我们实现了show_menu函数。这个函数会检测当前类别是否使用类别名的第一个字母被选中。如果一个类别被选中就会出现更多的选项。
static menu_options show_menu(const cdc_entry *cdc_selected)
{
char tmp_str[TMP_STRING_LEN + 1];
menu_options option_chosen = mo_invalid;
while (option_chosen == mo_invalid) {
if (cdc_selected->catalog[0]) {
printf(“/n/nCurrent entry: “);
printf(“%s, %s, %s, %s/n”, cdc_selected->catalog,
cdc_selected->title,
cdc_selected->type,
cdc_selected->artist);
printf(“/n”);
printf(“1 - add new CD/n”);
printf(“2 - search for a CD/n”);
printf(“3 - count the CDs and tracks in the database/n”);
printf(“4 - re-enter tracks for current CD/n”);
printf(“5 - delete this CD, and all its tracks/n”);
printf(“6 - list tracks for this CD/n”);
printf(“q - quit/n”);
printf(“/nOption: “);
fgets(tmp_str, TMP_STRING_LEN, stdin);
switch(tmp_str[0]) {
case ‘1’: option_chosen = mo_add_cat; break;
case ‘2’: option_chosen = mo_find_cat; break;
case ‘3’: option_chosen = mo_count_entries; break;
case ‘4’: option_chosen = mo_add_tracks; break;
case ‘5’: option_chosen = mo_del_cat; break;
case ‘6’: option_chosen = mo_list_cat_tracks; break;
case ‘q’: option_chosen = mo_exit; break;
}
}
else {
printf(“/n/n”);
printf(“1 - add new CD/n”);
printf(“2 - search for a CD/n”);
printf(“3 - count the CDs and tracks in the database/n”);
printf(“q - quit/n”);
printf(“/nOption: “);
fgets(tmp_str, TMP_STRING_LEN, stdin);
switch(tmp_str[0]) {
case ‘1’: option_chosen = mo_add_cat; break;
case ‘2’: option_chosen = mo_find_cat; break;
case ‘3’: option_chosen = mo_count_entries; break;
case ‘q’: option_chosen = mo_exit; break;
}
}
} /* while */
return(option_chosen);
}
8 有多个地方我们希望询问用户他是否确定所请求的动作。我们并不是在代码中的多个地方询问用户,相反,我们会将其作为一个单独的get_confirm函数来实现:
static int get_confirm(const char *question)
{
char tmp_str[TMP_STRING_LEN + 1];
printf(“%s”, question);
fgets(tmp_str, TMP_STRING_LEN, stdin);
if (tmp_str[0] == ‘Y’ || tmp_str[0] == ‘y’) {
return(1);
}
return(0);
}
9 函数enter_new_cat_entry允许用户可以输入新的类别记录。但是并不希望存储由fgets函数所返回的回车,所以我们要去掉回车。
static int enter_new_cat_entry(cdc_entry *entry_to_update)
{
cdc_entry new_entry;
char tmp_str[TMP_STRING_LEN + 1];
memset(&new_entry, ‘/0’, sizeof(new_entry));
printf(“Enter catalog entry: “);
(void)fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
strncpy(new_entry.catalog, tmp_str, CAT_CAT_LEN - 1);
printf(“Enter title: “);
(void)fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
strncpy(new_entry.title, tmp_str, CAT_TITLE_LEN - 1);
printf(“Enter type: “);
(void)fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
strncpy(new_entry.type, tmp_str, CAT_TYPE_LEN - 1);
printf(“Enter artist: “);
(void)fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
strncpy(new_entry.artist, tmp_str, CAT_ARTIST_LEN - 1);
printf(“/nNew catalog entry entry is :-/n”);
display_cdc(&new_entry);
if (get_confirm(“Add this entry ?”)) {
memcpy(entry_to_update, &new_entry, sizeof(new_entry));
return(1);
}
return(0);
}
10 现在我们要来讨论输入音轨信息的enter_new_track_entries函数。相比类别记录函数,这个函数要复杂一些,因为我们允许保存已经存在的音轨记录。
static void enter_new_track_entries(const cdc_entry *entry_to_add_to)
{
cdt_entry new_track, existing_track;
char tmp_str[TMP_STRING_LEN + 1];
int track_no = 1;
if (entry_to_add_to->catalog[0] == ‘/0’) return;
printf(“/nUpdating tracks for %s/n”, entry_to_add_to->catalog);
printf(“Press return to leave existing description unchanged,/n”);
printf(“ a single d to delete this and remaining tracks,/n”);
printf(“ or new track description/n”);
while(1) {
11 首先,我们需要检测是否存在当前音轨号的音轨记录。依据我们需要查找的内容,我们可以改变提示符。
memset(&new_track, ‘/0’, sizeof(new_track));
existing_track = get_cdt_entry(entry_to_add_to->catalog,
track_no);
if (existing_track.catalog[0]) {
printf(“/tTrack %d: %s/n”, track_no,
existing_track.track_txt);
printf(“/tNew text: “);
}
else {
printf(“/tTrack %d description: “, track_no);
}
fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
12 如果并不存在所要查找的音轨信息,而用户并没有添加音轨信息,我们假设用户并没有音轨信息需要添加。
if (strlen(tmp_str) == 0) {
if (existing_track.catalog[0] == ‘/0’) {
/* no existing entry, so finished adding */
break;
}
else {
/* leave existing entry, jump to next track */
track_no++;
continue;
}
}
13 如果用户输入了一个d字符,这会删除当前以及更高记录号的音轨信息。如果并没有查找到要删除的音轨信息,del_cat_entry函数会返回false。
if ((strlen(tmp_str) == 1) && tmp_str[0] == ‘d’) {
/* delete this and remaining tracks */
while (del_cdt_entry(entry_to_add_to->catalog, track_no)) {
track_no++;
}
break;
}
14 现在我们要来编码添加一个新的音轨信息或是更新一个已经存在的音轨信息。我们使用cdt_entry构成new_track,然后调用数据库函数add_cdt_entry将其添加到数据库中。
strncpy(new_track.track_txt, tmp_str, TRACK_TTEXT_LEN - 1);
strcpy(new_track.catalog, entry_to_add_to->catalog);
new_track.track_no = track_no;
if (!add_cdt_entry(new_track)) {
fprintf(stderr, “Failed to add new track/n”);
break;
}
track_no++;
} /* while */
}
15 函数del_cat_entry删除一个类别记录。我们绝不会允许一个不存在类别的音轨记录存在。
static void del_cat_entry(const cdc_entry *entry_to_delete)
{
int track_no = 1;
int delete_ok;
display_cdc(entry_to_delete);
if (get_confirm(“Delete this entry and all it’s tracks? “)) {
do {
delete_ok = del_cdt_entry(entry_to_delete->catalog,
track_no);
track_no++;
} while(delete_ok);
if (!del_cdc_entry(entry_to_delete->catalog)) {
fprintf(stderr, “Failed to delete entry/n”);
}
}
}
16 下一个函数是删除一个类别的所有音轨信息的程序:
static void del_track_entries(const cdc_entry *entry_to_delete)
{
int track_no = 1;
int delete_ok;
display_cdc(entry_to_delete);
if (get_confirm(“Delete tracks for this entry? “)) {
do {
delete_ok = del_cdt_entry(entry_to_delete->catalog, track_no);
track_no++;
} while(delete_ok);
}
}
17 下面,我们创建一个非常简单的类别查找程序。我们允许用户输入一个字符串,然后检测包含这个字符串的类别。因为也许会匹配多个记录,我们只是简单的依次用户提供匹配的记录:
static cdc_entry find_cat(void)
{
cdc_entry item_found;
char tmp_str[TMP_STRING_LEN + 1];
int first_call = 1;
int any_entry_found = 0;
int string_ok;
int entry_selected = 0;
do {
string_ok = 1;
printf(“Enter string to search for in catalog entry: “);
fgets(tmp_str, TMP_STRING_LEN, stdin);
strip_return(tmp_str);
if (strlen(tmp_str) > CAT_CAT_LEN) {
fprintf(stderr, “Sorry, string too long, maximum %d /
characters/n”, CAT_CAT_LEN);
string_ok = 0;
}
} while (!string_ok);
while (!entry_selected) {
item_found = search_cdc_entry(tmp_str, &first_call);
if (item_found.catalog[0] != ‘/0’) {
any_entry_found = 1;
printf(“/n”);
display_cdc(&item_found);
if (get_confirm(“This entry? “)) {
entry_selected = 1;
}
}
else {
if (any_entry_found) printf(“Sorry, no more matches found/n”);
else printf(“Sorry, nothing found/n”);
break;
}
}
return(item_found);
}
18 list_tracks是一个打印一个指定的类别内的所音轨的函数:
static void list_tracks(const cdc_entry *entry_to_use)
{
int track_no = 1;
cdt_entry entry_found;
display_cdc(entry_to_use);
printf(“/nTracks/n”);
do {
entry_found = get_cdt_entry(entry_to_use->catalog,
track_no);
if (entry_found.catalog[0]) {
display_cdt(&entry_found);
track_no++;
}
} while(entry_found.catalog[0]);
(void)get_confirm(“Press return”);
} /* list_tracks */
19 count_all_entries函数统计所有的音轨信息记录数:
static void count_all_entries(void)
{
int cd_entries_found = 0;
int track_entries_found = 0;
cdc_entry cdc_found;
cdt_entry cdt_found;
int track_no = 1;
int first_time = 1;
char *search_string = “”;
do {
cdc_found = search_cdc_entry(search_string, &first_time);
if (cdc_found.catalog[0]) {
cd_entries_found++;
track_no = 1;
do {
cdt_found = get_cdt_entry(cdc_found.catalog, track_no);
if (cdt_found.catalog[0]) {
track_entries_found++;
track_no++;
}
} while (cdt_found.catalog[0]);
}
} while (cdc_found.catalog[0]);
printf(“Found %d CDs, with a total of %d tracks/n”, cd_entries_found,
track_entries_found);
(void)get_confirm(“Press return”);
}
20 现在我们编写display_cdc函数,一个用于显示一个类别记录的函数:
static void display_cdc(const cdc_entry *cdc_to_show)
{
printf(“Catalog: %s/n”, cdc_to_show->catalog);
printf(“/ttitle: %s/n”, cdc_to_show->title);
printf(“/ttype: %s/n”, cdc_to_show->type);
printf(“/tartist: %s/n”, cdc_to_show->artist);
}
以及用于显示单一音轨记录的display_cdt函数:
static void display_cdt(const cdt_entry *cdt_to_show)
{
printf(“%d: %s/n”, cdt_to_show->track_no, cdt_to_show->track_txt);
}
21 函数strip_return用于移除字符串末尾的回车符。记住,与Unix类似,在Linux中,使用一个回车来代表一行的结束:
static void strip_return(char *string_to_strip)
{
int len;
len = strlen(string_to_strip);
if (string_to_strip[len - 1] == ‘/n’) string_to_strip[len - 1] = ‘/0’;
}
22 command_mode是一个用于分析命令行参数的函数。getopt函数是一个很好的方法,可以用来保证我们的程序接受符合标准Linux约定的参数。
static int command_mode(int argc, char *argv[])
{
int c;
int result = EXIT_SUCCESS;
char *prog_name = argv[0];
/* these externals used by getopt */
extern char *optarg;
extern optind, opterr, optopt;
while ((c = getopt(argc, argv, “:i”)) != -1) {
switch(c) {
case ‘i’:
if (!database_initialize(1)) {
result = EXIT_FAILURE;
fprintf(stderr, “Failed to initialize database/n”);
}
break;
case ‘:’:
case ‘?’:
default:
fprintf(stderr, “Usage: %s [-i]/n”, prog_name);
result = EXIT_FAILURE;
break;
} /* switch */
} /* while */
return(result);
}
试验--cd_access.c
现在我们来讨论访问dbm数据的函数。
1 如平时一样,我们由#include语句开始。然后我们使用#define语句来指定我们将会用于存储数据文件。
#define _XOPEN_SOURCE
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <ndbm.h>
#include “cd_data.h”
#define CDC_FILE_BASE “cdc_data”
#define CDT_FILE_BASE “cdt_data”
#define CDC_FILE_DIR “cdc_data.dir”
#define CDC_FILE_PAG “cdc_data.pag”
#define CDT_FILE_DIR “cdt_data.dir”
#define CDT_FILE_PAG “cdt_data.pag”
2 我们使用下面的两个局部变量来保存当前数据库的信息:
static DBM *cdc_dbm_ptr = NULL;
static DBM *cdt_dbm_ptr = NULL;
3 默认情况下,database_initialize函数会打开一个已经存在数据库,但是通过传递一个非零的参数new_database,我们可以强制其创建一个新的数据库,但是移除当前已存在的数据库。如果数据库成功的进行初始化,两个数据库也会进行初始,来表明已打开了一个数据库。
int database_initialize(const int new_database)
{
int open_mode = O_CREAT | O_RDWR;
/* If any existing database is open then close it */
if (cdc_dbm_ptr) dbm_close(cdc_dbm_ptr);
if (cdt_dbm_ptr) dbm_close(cdt_dbm_ptr);
if (new_database) {
/* delete the old files */
(void) unlink(CDC_FILE_PAG);
(void) unlink(CDC_FILE_DIR);
(void) unlink(CDT_FILE_PAG);
(void) unlink(CDT_FILE_DIR);
}
/* Open some new files, creating them if required */
cdc_dbm_ptr = dbm_open(CDC_FILE_BASE, open_mode, 0644);
cdt_dbm_ptr = dbm_open(CDT_FILE_BASE, open_mode, 0644);
if (!cdc_dbm_ptr || !cdt_dbm_ptr) {
fprintf(stderr, “Unable to create database/n”);
cdc_dbm_ptr = cdt_dbm_ptr = NULL;
return (0);
}
return (1);
}
4 database_close函数只是简单的关闭所打开的数据库,并且设置两个数据库指针指向null来表明当前没有数据库打开。
void database_close(void)
{
if (cdc_dbm_ptr) dbm_close(cdc_dbm_ptr);
if (cdt_dbm_ptr) dbm_close(cdt_dbm_ptr);
cdc_dbm_ptr = cdt_dbm_ptr = NULL;
}
5 接下来我们会编写一个函数,当向这个函数传递一个类别字符串时,此函数会取回此类别记录。如果没有找到此记录,返回的数据会有一个空的类别区域。
cdc_entry get_cdc_entry(const char *cd_catalog_ptr)
{
cdc_entry entry_to_return;
char entry_to_find[CAT_CAT_LEN + 1];
datum local_data_datum;
datum local_key_datum;
memset(&entry_to_return, ‘/0’, sizeof(entry_to_return));
6 我们会在开始时进行一些检测,来保证数据库已经打开,并且我们传递了合理的参数--也就是说,查找关键字只包含可用的字符串以及null。
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (entry_to_return);
if (!cd_catalog_ptr) return (entry_to_return);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (entry_to_return);
memset(&entry_to_find, ‘/0’, sizeof(entry_to_find));
strcpy(entry_to_find, cd_catalog_ptr);
7 我们设置dbm函数所需要的datum结构,然后使用dbm_fetch函数来取出数据。如果没有取回任何数据,我们就会返回我们先前所初始化的空的entry_to_return结构。
local_key_datum.dptr = (void *) entry_to_find;
local_key_datum.dsize = sizeof(entry_to_find);
memset(&local_data_datum, ‘/0’, sizeof(local_data_datum));
local_data_datum = dbm_fetch(cdc_dbm_ptr, local_key_datum);
if (local_data_datum.dptr) {
memcpy(&entry_to_return, (char *)local_data_datum.dptr,
local_data_datum.dsize);
}
return (entry_to_return);
} /* get_cdc_entry */
8 我们最好也可以得到一个单一的音轨信息,与get_cdc_entry函数相类似,而这也正是下一个函数所要做的,但是需要一个指向类别字符的指针以及一个音轨序号作为参数。
cdt_entry get_cdt_entry(const char *cd_catalog_ptr, const int track_no)
{
cdt_entry entry_to_return;
char entry_to_find[CAT_CAT_LEN + 10];
datum local_data_datum;
datum local_key_datum;
memset(&entry_to_return, ‘/0’, sizeof(entry_to_return));
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (entry_to_return);
if (!cd_catalog_ptr) return (entry_to_return);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (entry_to_return);
/* set up the search key, which is a composite key of catalog entry
and track number */
memset(&entry_to_find, ‘/0’, sizeof(entry_to_find));
sprintf(entry_to_find, “%s %d”, cd_catalog_ptr, track_no);
local_key_datum.dptr = (void *) entry_to_find;
local_key_datum.dsize = sizeof(entry_to_find);
memset(&local_data_datum, ‘/0’, sizeof(local_data_datum));
local_data_datum = dbm_fetch(cdt_dbm_ptr, local_key_datum);
if (local_data_datum.dptr) {
memcpy(&entry_to_return, (char *) local_data_datum.dptr,
local_data_datum.dsize);
}
return (entry_to_return);
}
9 下一个函数,add_cdc_entry,添加一个新的类别记录:
int add_cdc_entry(const cdc_entry entry_to_add)
{
char key_to_add[CAT_CAT_LEN + 1];
datum local_data_datum;
datum local_key_datum;
int result;
/* check database initialized and parameters valid */
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);
if (strlen(entry_to_add.catalog) >= CAT_CAT_LEN) return (0);
/* ensure the search key contains only the valid string and nulls */
memset(&key_to_add, ‘/0’, sizeof(key_to_add));
strcpy(key_to_add, entry_to_add.catalog);
local_key_datum.dptr = (void *) key_to_add;
local_key_datum.dsize = sizeof(key_to_add);
local_data_datum.dptr = (void *) &entry_to_add;
local_data_datum.dsize = sizeof(entry_to_add);
result = dbm_store(cdc_dbm_ptr, local_key_datum, local_data_datum,
DBM_REPLACE);
/* dbm_store() uses 0 for success */
if (result == 0) return (1);
return (0);
}
10 add_cdt_entry添加一新的音轨记录。访问关键字是类别字符串,并且音轨序号作为组合。
int add_cdt_entry(const cdt_entry entry_to_add)
{
char key_to_add[CAT_CAT_LEN + 10];
datum local_data_datum;
datum local_key_datum;
int result;
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);
if (strlen(entry_to_add.catalog) >= CAT_CAT_LEN) return (0);
memset(&key_to_add, ‘/0’, sizeof(key_to_add));
sprintf(key_to_add, “%s %d”, entry_to_add.catalog,
entry_to_add.track_no);
local_key_datum.dptr = (void *) key_to_add;
local_key_datum.dsize = sizeof(key_to_add);
local_data_datum.dptr = (void *) &entry_to_add;
local_data_datum.dsize = sizeof(entry_to_add);
result = dbm_store(cdt_dbm_ptr, local_key_datum, local_data_datum,
DBM_REPLACE);
/* dbm_store() uses 0 for success and -ve numbers for errors */
if (result == 0)
return (1);
return (0);
}
11 如果我们可以添加一些东西,我们最好也可以删除了他们。下面这个函数删除类别记录:
int del_cdc_entry(const char *cd_catalog_ptr)
{
char key_to_del[CAT_CAT_LEN + 1];
datum local_key_datum;
int result;
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (0);
memset(&key_to_del, ‘/0’, sizeof(key_to_del));
strcpy(key_to_del, cd_catalog_ptr);
local_key_datum.dptr = (void *) key_to_del;
local_key_datum.dsize = sizeof(key_to_del);
result = dbm_delete(cdc_dbm_ptr, local_key_datum);
/* dbm_delete() uses 0 for success */
if (result == 0) return (1);
return (0);
}
12 下面这个函数等同于删除一个音轨信息。记住,音轨关键字是类别记录字符串以及一个音轨序号的组合:
int del_cdt_entry(const char *cd_catalog_ptr, const int track_no)
{
char key_to_del[CAT_CAT_LEN + 10];
datum local_key_datum;
int result;
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (0);
memset(&key_to_del, ‘/0’, sizeof(key_to_del));
sprintf(key_to_del, “%s %d”, cd_catalog_ptr, track_no);
local_key_datum.dptr = (void *) key_to_del;
local_key_datum.dsize = sizeof(key_to_del);
result = dbm_delete(cdt_dbm_ptr, local_key_datum);
/* dbm_delete() uses 0 for success */
if (result == 0) return (1);
return (0);
}
13 最后但不并不是不重要的一点,我们需要一个简单的搜索函数。他并不是非常高级,但是他确实演示了如果在不知道更多关键字的情况下如何搜索dbm记录。
因为我们并不知道存在多少记录,我们实现这个函数,在每次调用时只返回一个记录。如果没有查找到任何记录,记录就会空。要搜索整个数据库,我们通过使用一个指向整数的指针*first_call_ptr来调用这个函数,在第一次调用时此参数值应为1。然后此函数就会知道他应由数据的起始处开始搜索。在接下来的调用中,变量为0,而函数就会上一次他所查找的记录之后恢复查找。
当我们需要重新启动我们的查找时,很可能是查找另一个不同的类别记录,我们必须再一次将*first_call_ptr设置为真来调用这个函数,将参数设置为真用来初始化搜索。
在函数调用之间,函数会维护一些内部状态信息。这会隐藏客户继续查找的复杂性,并且保持查找函数实现的细节。
如果搜索字符串指向一个null字符,此时就会匹配所有的记录。
cdc_entry search_cdc_entry(const char *cd_catalog_ptr, int *first_call_ptr)
{
static int local_first_call = 1;
cdc_entry entry_to_return;
datum local_data_datum;
static datum local_key_datum; /* notice this must be static */
memset(&entry_to_return, ‘/0’, sizeof(entry_to_return));
14 如平时一样,我们由必要的检测开始:
if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (entry_to_return);
if (!cd_catalog_ptr || !first_call_ptr) return (entry_to_return);
if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (entry_to_return);
/* protect against never passing *first_call_ptr true */
if (local_first_call) {
local_first_call = 0;
*first_call_ptr = 1;
}
15 如果这个函数已经由设置为真的*first_call_ptr参数进行了调用,我们需要由开始(或是重新开始)搜索数据库的超始处。如果*first_call_ptr并不会为真,我们只是简单的移到数据中的下一个关键字上:
if (*first_call_ptr) {
*first_call_ptr = 0;
local_key_datum = dbm_firstkey(cdc_dbm_ptr);
}
else {
local_key_datum = dbm_nextkey(cdc_dbm_ptr);
}
do {
if (local_key_datum.dptr != NULL) {
/* an entry was found */
local_data_datum = dbm_fetch(cdc_dbm_ptr, local_key_datum);
if (local_data_datum.dptr) {
memcpy(&entry_to_return, (char *) local_data_datum.dptr,
local_data_datum.dsize);
16 我们的搜索程序进行简单的检测以确定查找字符串是否包含在当前的类别记录中。
/* check if search string occurs in the entry */
if (!strstr(entry_to_return.catalog, cd_catalog_ptr))
{
memset(&entry_to_return, ‘/0’,
sizeof(entry_to_return));
local_key_datum = dbm_nextkey(cdc_dbm_ptr);
}
}
}
} while (local_key_datum.dptr &&
local_data_datum.dptr &&
(entry_to_return.catalog[0] == ‘/0’));
return (entry_to_return);
} /* search_cdc_entry */
现在我们就可以所有的内容放在一个makefile文件中。现在不要太担心,因为我们会在下一章讨论他是如何工作的。就目前而言,输入下面的内容,并且将其保存为Makefile。
all: application
INCLUDE=/usr/include/gdbm
LIBS=gdbm
CFLAGS=
app_ui.o: app_ui.c cd_data.h
gcc $(CFLAGS) -c app_ui.c
access.o: access.c cd_data.h
gcc $(CFLAGS) -I$(INCLUDE) -c access.c
application: app_ui.o access.o
gcc $(CFLAGS) -o application app_ui.o access.o -l$(LIBS)
clean:
rm -f application *.o
nodbmfiles:
rm -f *.dir *.pag
要编译我们的CD程序,在提示符下输入下面的命令:
$ make
如果一切顺利,就会在当前目录下编译成功application可执行程序。
总结
在这一章,我们已经了解了数据管理的三个方面。首先,我们了解了Linux内存系统,以及其使用是如何简单,尽管按需调度分页虚拟内存的实现非常复杂。我们同时也会发现Linux系统通过合法的内存访问来保护操作系统与其他程序。
然后我们探讨了文件锁如何使得多个程序合作访问数据。我们首先了解了一个简单的二进制信号量,然而了解一个更为复杂的情况,此是我们锁住文件的不同部分用于共享或是排他访问。接下来我们了解了dbm库,以及其存储和使用一个非常灵活的索引机制读取数据的能力。
最后,我们使用dbm库作为存储技术重新设计并且实现了我们的CD数据库程序。