找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4212

积分

0

好友

554

主题
发表于 2 小时前 | 查看: 4| 回复: 0

在嵌入式开发中,轻量级的键值对存储往往比直接引入MySQL或Redis更切实际。本文将用数据库(Key-Value)的思想,从头用C语言实现一个基于二进制文件的简易数据库,并完成增、删、改、查全部功能。

1 简易数据库介绍

数据库(Database)是按特定结构组织、存储和管理数据的集合。在嵌入式软件开发中,如果不想引入重量级数据库,又想方便地管理少量数据,自己动手实现一个简易数据库是很好的选择。

数据可以直接保存在文件中,基本操作无非增、删、改、查。简单数据库最常见的存储形式是键值对(key-value),通过一个键(key)来对应一个具体值(value)。为了让value更实用,可以支持多种类型,编程中最常用的是整型、浮点型和字符串。

2 代码实现

我们将用C语言定义数据结构和数据库接口。

2.1 数据结构

数据存储采用二进制文件,既节省空间,又提高读写效率。每条记录包含键名、值类型、值长度和值数据。支持的三种基本类型:int、float和string。

首先用枚举定义值类型:

// 定义值类型
typedef enum {
    TYPE_INT,
    TYPE_FLOAT,
    TYPE_STRING
} ValueType;

// 定义值的联合体
typedef union {
    int int_val;
    float float_val;
    char* str_val;
} Value;

// 定义键值对结构
typedef struct {
    char* key;      // key
    ValueType type; // 数据类型
    Value value;    // 数据的值
} KVNode;

// 数据库结构
typedef struct {
    FILE* file;     // 文件句柄
    char* filename; // 文件名
} KVDB;

每条数据在文件中的存储结构如下:

#define KEY_NAME_LEN   (64)
#define VALUE_DATA_LEN (128)

// 每条记录在文件中的存储格式
typedef struct {
    char key[KEY_NAME_LEN];             // 键名,最长63个字符
    ValueType type;                     // 值类型
    int value_len;                      // 值的长度(主要用于字符串)
    unsigned char data[VALUE_DATA_LEN]; // 存储值的数据
} FileRecord;

C语言Key-Value数据库内存与文件存储数据结构设计图

2.2 访问接口

数据库需要提供基本的操作接口:

  • 打开/关闭数据库
  • 插入/更新键值对(kvdb_set)
  • 查询键值对(kvdb_get)
  • 删除键值对(kvdb_delete)
  • 遍历所有键值对(kvdb_iterate)
// 打开数据库
KVDB* kvdb_open(const char* filename);

// 关闭数据库
void kvdb_close(KVDB* db);

// 插入或更新键值对
bool kvdb_set(KVDB* db, const char* key, ValueType type, Value value);

// 获取键对应的值
bool kvdb_get(KVDB* db, const char* key, KVNode* result);

// 删除键值对
bool kvdb_delete(KVDB* db, const char* key);

// 遍历所有键值对
void kvdb_iterate(KVDB* db);

2.2.1 打开/关闭数据库

打开数据库的步骤很简单:

  • 根据传入文件名构建KVDB信息
  • 尝试fopen打开db文件,如果不存在则先创建一个空文件
  • 将KVDB返回供后续使用
// 打开数据库(传入DB文件的名称)
KVDB* kvdb_open(const char* filename)
{
    KVDB* db = (KVDB*)malloc(sizeof(KVDB));
    if (!db) 
    {
        return NULL;
    }

    db->filename = (char*)malloc(strlen(filename) + 1);
    if (!db->filename) 
    {
        free(db);
        return NULL;
    }
    strcpy(db->filename, filename);

    // 以读写方式打开文件,如果不存在则创建
    db->file = fopen(filename, "rb+");
    if (!db->file) 
    {
        db->file = fopen(filename, "wb+");
        if (!db->file) 
        {
            printf("[%s] db file:%s not exist, create fail\n", __func__, filename);
            free(db->filename);
            free(db);
            return NULL;
        }
        printf("[%s] db file:%s not exist, create ok\n", __func__, filename);
    }
    printf("[%s] db file:%s exist, open ok\n", __func__, filename);

    return db;
}

关闭数据库就简单了,释放资源即可:

// 关闭数据库
void kvdb_close(KVDB* db)
{
    if (db) 
    {
        if (db->file) 
        {
            fclose(db->file); // 关闭文件 
        }
        free(db->filename); // 释放文件名
        free(db); // 释放KVDB结构
    }
}

2.2.2 插入/更新

插入或更新一条记录,核心思路是:

  • 根据key、type和value构建一条FileRecord
  • 遍历db文件中的每一行,匹配key是否存在
    • 若key已存在,则更新该条记录的值
    • 若key不存在,则在文件末尾追加新记录
// 插入或更新键值对
bool kvdb_set(KVDB* db, const char *key, ValueType type, Value value)
{
    if (!db || !db->file || !key) 
    {
        printf("[%s] NULL ptr\n", __func__);
        return false;
    }

    // 检查键长度
    if (strlen(key) >= KEY_NAME_LEN) 
    {
        printf("[%s] key:%s len > %d\n", __func__, key, KEY_NAME_LEN);
        return false;
    }

    FileRecord record;
    strcpy(record.key, key);
    record.type = type;
    memset(record.data, 0, sizeof(record.data));

    // 根据类型处理值
    switch (type) 
    {
        case TYPE_INT:
        {
            record.value_len = sizeof(int);
            memcpy(record.data, &value.int_val, sizeof(int));
            break;
        }
        case TYPE_FLOAT:
        {
            record.value_len = sizeof(float);
            memcpy(record.data, &value.float_val, sizeof(float));
            break;
        }
        case TYPE_STRING:
        {
            if (!value.str_val) return false;
            record.value_len = strlen(value.str_val) + 1; // 包含终止符
            if (record.value_len > sizeof(record.data)) return false;
            strcpy((char*)record.data, value.str_val);
            break;
        }
        default:
            return false;
    }

    // 先尝试找到并替换现有记录
    fseek(db->file, 0, SEEK_SET);
    long pos;
    while ((pos = ftell(db->file)) != EOF) 
    {
        FileRecord existing;
        if (fread(&existing, sizeof(FileRecord), 1, db->file) != 1) 
        {
            break;
        }

        if (strcmp(existing.key, key) == 0) 
        {
            // 找到相同的键,替换记录
            fseek(db->file, pos, SEEK_SET);
            if (fwrite(&record, sizeof(FileRecord), 1, db->file) == 1) 
            {
                print_db_set_info(__func__, key, type, value, 0, 1);
                fflush(db->file);
                return true;
            }
            print_db_set_info(__func__, key, type, value, 0, 0);
            return false;
        }
    }

    // 如果没找到,则在文件末尾添加新记录
    fseek(db->file, 0, SEEK_END);
    if (fwrite(&record, sizeof(FileRecord), 1, db->file) == 1) 
    {
        fflush(db->file);
        print_db_set_info(__func__, key, type, value, 1, 1);
        return true;
    }
    print_db_set_info(__func__, key, type, value, 1, 0);

    return false;
}

配套的打印辅助函数:

void print_db_set_info(const char *func, const char *key, ValueType type, Value value, int is_insert, int is_success)
{
    char *state = is_success ? "ok" : "fail";
    char *insertOrUpdate = is_insert ? "insert" : "update";

    if (TYPE_INT == type)
    {
        printf("[%s] %s key:%s value:%d [%s]\n", func, insertOrUpdate, key, value.int_val, state);
    }
    else if (TYPE_FLOAT == type)
    {
        printf("[%s] %s key:%s value:%f [%s]\n", func, insertOrUpdate, key, value.float_val, state);
    }
    else if (TYPE_STRING == type)
    {
        printf("[%s] %s key:%s value:%s [%s]\n", func, insertOrUpdate, key, value.str_val, state);
    }
}

2.2.3 查询

查询操作就是遍历文件记录,找到匹配的key后把数据拷出来:

  • 遍历db文件的每一行(FileRecord)
  • 如果key匹配,则将数据填充到KVNode的result中并返回
  • 如果遍历完还没找到,返回失败
// 获取键对应的值
bool kvdb_get(KVDB* db, const char* key, KVNode* result)
{    
    if (!db || !db->file || !key || !result) 
    {
        printf("[%s] NULL ptr\n", __func__);
        return false;
    }

    fseek(db->file, 0, SEEK_SET);
    FileRecord record;

    while (fread(&record, sizeof(FileRecord), 1, db->file) == 1) 
    {
        // 找到了key
        if (strcmp(record.key, key) == 0) 
        {
            result->key = (char*)malloc(strlen(record.key) + 1);
            if (!result->key) 
            {
                printf("[%s] db no key:%s\n", __func__, key);
                return false;
            }
            // 赋值result->key
            strcpy(result->key, record.key);
            // 赋值result->type
            result->type = record.type;
            // 赋值result->value
            switch (record.type) 
            {
                case TYPE_INT:
                {
                    memcpy(&result->value.int_val, record.data, sizeof(int));
                    break;
                }
                case TYPE_FLOAT:
                {
                    memcpy(&result->value.float_val, record.data, sizeof(float));
                    break;
                }
                case TYPE_STRING:
                {
                    result->value.str_val = (char*)malloc(record.value_len);
                    if (!result->value.str_val) 
                    {
                        print_db_get_info(__func__, key, result, 0);
                        free(result->key);
                        return false;
                    }
                    strcpy(result->value.str_val, (char*)record.data);
                    break;
                }
                default:
                {
                    print_db_get_info(__func__, key, result, 0);
                    free(result->key);
                    return false;
                }
            }

            print_db_get_info(__func__, key, result, 1);
            return true;
        }
    }

    // 未找到键
    return false;
}

查询结果的打印函数:

void print_db_get_info(const char *func, const char *key, const KVNode* result, int is_success)
{
    if (is_success)
    {
        if (TYPE_INT == result->type)
        {
            printf("[%s] get key:%s value:%d [ok]\n", func, key, result->value.int_val);
        }
        else if (TYPE_FLOAT == result->type)
        {
            printf("[%s] get key:%s value:%f [ok]\n", func, key, result->value.float_val);
        }
        else if (TYPE_STRING == result->type)
        {
            printf("[%s] get key:%s value:%s [ok]\n", func, key, result->value.str_val);
        }
    }
    else
    {
        if (TYPE_INT == result->type)
        {
            printf("[%s] get key:%s [fail]\n", func, key);
        }
        else if (TYPE_FLOAT == result->type)
        {
            printf("[%s] get key:%s [fail]\n", func, key);
        }
        else if (TYPE_STRING == result->type)
        {
            printf("[%s] get key:%s [fail]\n", func, key);
        }
    }
}

2.2.4 删除

删除操作稍微复杂一点,采用“临时文件”方案:

  • 创建一个临时文件
  • 遍历原db文件的每一行(FileRecord),如果不是要删除的key,就拷贝到临时文件;如果是,直接跳过
  • 处理完所有记录后,删除原db文件
  • 将临时db文件重命名为原db文件名,并重新打开db文件

数据库删除操作流程:临时文件与重命名

// 删除键值对
bool kvdb_delete(KVDB* db, const char* key)
{
    if (!db || !db->file || !key) 
    {
        printf("[%s] NULL ptr\n", __func__);
        return false;
    }

    // 创建一个临时文件
    char temp_filename[256];
    sprintf(temp_filename, "%s.tmp", db->filename);
    FILE* temp_file = fopen(temp_filename, "wb+");
    if (!temp_file) 
    {
        printf("[%s] fopen tmp file:%s fail\n", __func__, temp_filename);
        return false;
    }

    bool found = false;
    fseek(db->file, 0, SEEK_SET);
    FileRecord record;

    // 复制所有不匹配的记录到临时文件
    while (fread(&record, sizeof(FileRecord), 1, db->file) == 1) 
    {
        if (strcmp(record.key, key) != 0) 
        {
            fwrite(&record, sizeof(FileRecord), 1, temp_file);
        } 
        else
        {
            printf("[%s] delete key:%s\n", __func__, key);
            found = true;
        }
    }

    // 关闭文件
    fclose(db->file);
    fclose(temp_file);

    // 删除原文件,重命名临时文件
    printf("[%s] remove old db, then rename tmp db to norml db\n", __func__);
    remove(db->filename);
    rename(temp_filename, db->filename);

    // 重新打开数据库文件
    db->file = fopen(db->filename, "rb+");
    if (!db->file) 
    {
        return false;
    }

    return found;
}

2.2.5 遍历数据库

为了方便查看db中存储了哪些数据,可以写一个遍历函数,把所有键值对打印出来。

// 遍历所有键值对
void kvdb_iterate(KVDB* db)
{
    if (!db || !db->file) 
    {
        return;
    }

    fseek(db->file, 0, SEEK_SET);
    FileRecord record;
    KVNode node;

    while (fread(&record, sizeof(FileRecord), 1, db->file) == 1) 
    {
        node.key = record.key;
        node.type = record.type;

        switch (record.type) 
        {
            case TYPE_INT:
            {
                memcpy(&node.value.int_val, record.data, sizeof(int));
                break;
            }
            case TYPE_FLOAT:
            {
                memcpy(&node.value.float_val, record.data, sizeof(float));
                break;
            }
            case TYPE_STRING:
            {
                node.value.str_val = (char*)record.data;
                break;
            }
        }

        print_node(&node);
    }
}

对应的打印函数:

// 打印键值对
void print_node(KVNode* node)
{
    if (!node) 
    {
        return;
    }

    printf("Key: %s,\t Type: ", node->key);
    switch (node->type) 
    {
        case TYPE_INT:
        {
            printf("int,\t Value: %d\n", node->value.int_val);
            break;
        }
        case TYPE_FLOAT:
        {
            printf("float,\t Value: %.2f\n", node->value.float_val);
            break;
        }
        case TYPE_STRING:
        {
            printf("string,\t Value: %s\n", node->value.str_val);
            break;
        }
        default:
            printf("unknown\n");
    }
}

2.3 测试代码

写一个简单的main函数来验证功能:

int main()
{
    char *db_file = "test_kv.db";
    // 打开数据库
    KVDB* db = kvdb_open(db_file);
    if (!db) 
    {
        printf("[%s] 无法打开数据库文件\n", __func__);
        return 1;
    }
    printf("[%s] open db file:%s ok\n", __func__, db_file);

    Value val;

    // 插入一些数据
    val.int_val = 20;
    kvdb_set(db, "count", TYPE_INT, val);

    val.float_val = 3.14159f;
    kvdb_set(db, "pi", TYPE_FLOAT, val);

    val.str_val = "25-09-13 13:48";
    kvdb_set(db, "time", TYPE_STRING, val);

    // 查询并打印数据
    KVNode result;
    if (kvdb_get(db, "count", &result)) 
    {
        printf("[%s] 查询到 age: %d\n", __func__, result.value.int_val);
        free(result.key); // 释放分配的内存
    }

    if (kvdb_get(db, "pi", &result)) 
    {
        printf("[%s] 查询到 pi: %.2f\n", __func__, result.value.float_val);
        free(result.key);
    }

    if (kvdb_get(db, "time", &result)) 
    {
        printf("[%s] 查询到 time: %s\n", __func__, result.value.str_val);
        free(result.value.str_val); // 释放字符串内存
        free(result.key);
    }

    // 更新数据
    val.int_val = 30;
    kvdb_set(db, "count", TYPE_INT, val);
    if (kvdb_get(db, "count", &result)) 
    {
        printf("[%s] 更新后 count: %d\n", __func__, result.value.int_val);
        free(result.key);
    }

    // 遍历所有数据
    printf("\n所有数据:\n");
    kvdb_iterate(db);

    // 删除数据
    kvdb_delete(db, "pi");
    printf("\n删除 pi 后所有数据:\n");
    kvdb_iterate(db);

    // 关闭数据库
    kvdb_close(db);
    return 0;
}

3 运行测试

在Ubuntu下编译运行,输出效果如下:

C语言简易数据库增删改查测试运行结果

可以看到,程序正确完成了插入、查询、更新、删除和遍历的全部流程。

4 总结

通过C语言从零实现这个Key-Value数据库,我们深入理解了文件I/O、二进制存储以及数据结构的实际应用。这种轻量级方案非常适合嵌入式或资源受限的环境,也为进一步扩展(如索引、事务)打下了基础。

本文由云栈社区整理提供,更多C/C++、数据库等底层技术干货,欢迎访问 yunpan.plus 一起交流学习。




上一篇:Shell脚本实战:高效文件检查与增量拷贝(cmp对比法)
下一篇:AES加密C语言实现详解:纯代码手写、OpenSSL与在线工具验证
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-3 05:19 , Processed in 1.771749 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表