函数db_fetch为用户接口,根据传入的键值寻找对应的数据,如果找到,则返回指向数据的指针,否则返回NULL,代码如下:
/* 根据给定的键读取一条记录 */
char *db_fetch(DBHANDLE h, const char *key)
{
DB *db = h;
char *ptr;
/* 参数三:0表示读,1表示写,根据此函数决定加什么锁 */
if (_db_find_and_lock(db, key, 0) < 0)
{
/* 返回-1表示未找到记录 */
ptr = NULL;
db->cnt_fetcherr++;
}
else /* 返回0表示查找成功 */
{
ptr = _db_readdat(db); /* 返回找到的数据 */
db->cnt_fetchok++;
}
if (un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
return ptr;
}
此函数的最后用到了un_lock宏函数来解锁,因为在_db_find_and_lock函数中获得了一个锁。db_fetch函数的大致流程是:根据传入的key调用_db_find_and_lock查找对应的索引记录,如果找到,则继续调用_db_readdat读取对应的数据记录,未找到,则不会调用_db_readdat函数,而是直接返回NULL。 所以,用户在调用db_fetch时一定要判断返回值。我在刚开使用时由于未判断返回值而直接打印返回的指针,结果导致在未找到的情况下出现Segmentation fault。
函数_db_find_and_lock是关键所在,下面是它的代码:
/* writelock:0表示读,1表示写
* 返回值:0查找成功,-1查找失败
*/
static int _db_find_and_lock(DB *db, const char *key, int writelock)
{
off_t offset, nextoffset;
/* 根据散列函数定位桶的起始字符,这里要跳过第一个空闲链表 */
db->chainoff = (_db_hash(db, key) * PTR_SZ) + db->hashoff;
db->ptroff = db->chainoff; /* 同样指向某个桶 */
if (writelock) /* 写锁 */
{
/* 这里只锁一个链的开头一个字节,其它链仍然可用 */
if (writew_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
}
else /* 读锁 */
{
/* 这里只锁一个链的开头一个字节,其它链仍然可用 */
if (readw_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
}
/* 知道偏移量,知道指针宽度,atol将指针转换成数字类型 */
offset = _db_readptr(db, db->ptroff); /* 直接读取桶中的元素,即第一个节点的偏移量 */
/* 沿着桶向下查找 */
while (offset != 0) /* 如果为0则该散列链为空 */
{
nextoffset = _db_readidx(db, offset); /* 读取一条索引记录存入db->idxbuf */
/* db->idxbuf = 键值 '\0' 数据记录偏移量 '\0' 数据记录长度 '\0' */
if (strcmp(db->idxbuf, key) == 0)
break;
db->ptroff = offset; /* ptroff记录前一索引记录的地址 */
offset = nextoffset;
}
return (offset == 0 ? -1 : 0); /* offset = 0则没有找到记录 */
}
_db_find_and_lock函数根据传入的key计算出散列值,然后定位到散列表中的某个链表,从链表头向后遍历每条记录,直到找出某条记录保存的key和传入的key相等或到链表结尾为止。散列函数如下:
/* 散列函数 */
static DBHASH _db_hash(DB *db, const char *key)
{
DBHASH hval = 0;
char c;
int i;
/* 以下为散列函数 */
for (i = 1; (c = *key++) != 0; i++)
hval += c * i;
return (hval % db->nhash); /* db->nhash = 137 */
}
下面介绍_db_find_and_lock调用到的相关函数。首先是_db_readptr,代码如下:
/* 读取一个指针并转换为数值型,凡是要获取指针值都要调用这个函数
* 根据offset知道指针所在位置,指针所占字节宽度也已知,就能读取一个指针的值了
* 注意:此函数未进行加锁操作,需要手动进行
*/
static off_t _db_readptr(DB *db, off_t offset)
{
char asciiptr[PTR_SZ + 1];
if (lseek(db->idxfd, offset, SEEK_SET) == -1)
{
printf("%d\n", __LINE__);
exit(-1);
}
if (read(db->idxfd, asciiptr, PTR_SZ) != PTR_SZ) /* PTR_SZ = 6,一次读6个字符 */
{
printf("%d\n", __LINE__);
exit(-1);
}
asciiptr[PTR_SZ] = 0; /* 追加一个空字符,为了atol函数辨识结尾处 */
/* atol会跳过前面的空格,遇到非数字或字符串结束时停止转换 */
return (atol(asciiptr));
}
由于所有的记录都是以字符的形式保存在文件中的,所以如果想要获得指针值,则必须将字符串类型转换成数值类型,上述函数就是这个作用。根据偏移量,就能知道指针所在位置,然后读取这个字符串,把字符串转换成数值类型,然后返回。
接下来是_db_readidx函数:
/* 根据索引记录的偏移值offset读取一条索引记录,填充DB结构许多成员
* 返回值:下一条记录的偏移量
*/
static off_t _db_readidx(DB *db, off_t offset)
{
ssize_t i;
char *ptr1, *ptr2;
char asciiptr[PTR_SZ + 1], asciilen[IDXLEN_SZ + 1];
struct iovec iov[2];
/* 保存当前索引记录偏移量
* 如果offset为0,表示从当前偏移量处读
*/
if ((db->idxoff = lseek(db->idxfd, offset, offset == 0 ? SEEK_CUR : SEEK_SET)) == -1)
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 散布读,第一部分放asciiptr中,第二部分放asciilen中 */
iov[0].iov_base = asciiptr;
iov[0].iov_len = PTR_SZ; /* 6 */
iov[1].iov_base = asciilen;
iov[1].iov_len = IDXLEN_SZ; /* 4 */
if ((i = readv(db->idxfd, &iov[0], 2)) != PTR_SZ + IDXLEN_SZ)
{
/* 返回读取的字节数,结尾返回-1 */
if (i == 0 && offset == 0)
return -1; /* 这个返回值是给函数db_nextrec使用的,_db_find_and_lock永远不可能返回-1 */
else
{
printf("%d\n", __LINE__);
exit(-1);
}
}
/* 下一条索引记录偏移量 */
asciiptr[PTR_SZ] = 0;
db->ptrval = atol(asciiptr);
/* 当前索引记录长度 */
asciilen[IDXLEN_SZ] = 0;
if ((db->idxlen = atoi(asciilen)) < IDXLEN_MIN || db->idxlen > IDXLEN_MAX)
{
/* 索引记录长度必须在6~1024字节之间 */
printf("%d\n", __LINE__);
exit(-1);
}
/* 下面读取实际的索引记录,文件指针已经在调用readv后指向正确位置,即key开头 */
if ((i = read(db->idxfd, db->idxbuf, db->idxlen)) != db->idxlen)
{
printf("%d\n", __LINE__);
exit(-1);
}
if (db->idxbuf[db->idxlen -1] != NEWLINE)
{
printf("%d\n", __LINE__);
exit(-1);
}
db->idxbuf[db->idxlen - 1] = 0; /* 把换行符替换为空字符,为了atol函数做准备 */
/* 找出分隔符 */
if ((ptr1 = strchr(db->idxbuf, SEP)) == NULL)
{
printf("%d\n", __LINE__);
exit(-1);
}
*ptr1++ = 0; /* 分隔符替换为空字符,ptr1现在指向数据记录的偏移量 */
if ((ptr2 = strchr(ptr1, SEP)) == NULL)
{
printf("%d\n", __LINE__);
exit(-1);
}
*ptr2++ = 0; /* 分隔符替换为空字符,ptr2现在指向数据记录的长度 */
if (strchr(ptr2, SEP) != NULL) /* 只有两个分隔符 */
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 保存实际数据的偏移量 */
if ((db->datoff = atol(ptr1)) < 0) /* atol遇到空字符结束 */
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 保存实际数据的长度,范围必须在2~1024字节之间 */
if ((db->datlen = atol(ptr2)) <= 0 || db->datlen > DATLEN_MAX)
{
printf("%d\n", __LINE__);
exit(-1);
}
return (db->ptrval);
}
由_db_readptr知道索引记录偏移量,在调用_db_readidx函数从文件中读取一条索引记录到一个buffer中,这个buffer实际上就是db->idxbuf。在调用完这个函数后,DB结构体中的许多成员都记录了某条记录的信息,例如数据偏移量,数据长度,索引记录长度等。知道了数据偏移量和数据长度,就可以读取数据文件中实际的数据了,此即函数_db_readdat的职责:
/* 读取实际数据到缓存db->datbuf中
* 返回值:NULL结尾的实际数据
*/
static char* _db_readdat(DB *db)
{
if (lseek(db->datfd, db->datoff, SEEK_SET) == -1) /* 定位 */
{
printf("%d\n", __LINE__);
exit(-1);
}
if (read(db->datfd, db->datbuf, db->datlen) != db->datlen) /* 读入缓存 */
{
printf("%d\n", __LINE__);
exit(-1);
}
if (db->datbuf[db->datlen - 1] != NEWLINE) /* 数据文件中的一个条必以换行符结尾 */
{
printf("%d\n", __LINE__);
exit(-1);
}
db->datbuf[db->datlen - 1] = 0; /* 换行符替换成空字符 */
return (db->datbuf);
}
由于存放在文件中时,每条数据记录都是以换行符结尾,此函数读取一条数据记录后,把换行符修改为结束符,这方便了用户代码直接打印返回的数据。