54、C语言与MongoDB基础:MongoDB C Driver、mongoc_client_new、mongoc_collection_find_with_opts

说实话,做了这么多年嵌入式开发,我接触最多的数据库一直是 SQLite 这种轻量级选手。但最近几年,物联网项目越来越复杂,数据量也上来了,很多场景下需要把设备数据上传到云端做分析。这时候,MongoDB 就进入了我的视野。

嗯,今天我们就来聊聊怎么在 C 语言里操作 MongoDB。说白了,就是用官方提供的 C Driver 库,把我们的嵌入式设备跟 MongoDB 数据库连起来。

MongoDB C Driver 是什么?

MongoDB C Driver 是 MongoDB 官方提供的 C 语言客户端库。它允许我们用 C 代码连接 MongoDB 服务器,执行增删改查操作。

我个人习惯把它叫做「胶水层」—— 它把 MongoDB 的 wire protocol 封装成了 C 函数,我们只需要调用几个 API 就能完成数据库操作。

核心要点:MongoDB C Driver 不是 MongoDB 自带的,需要单独安装。在嵌入式 Linux 环境下,通常用交叉编译的方式把它集成到项目中。

环境准备:安装 MongoDB C Driver

在开始写代码之前,得先把环境搭好。我一般在 Ubuntu 或 Debian 系统上开发,安装命令如下:

# 安装依赖
sudo apt-get install cmake libssl-dev libsasl2-dev

# 下载源码
wget https://github.com/mongodb/mongo-c-driver/releases/download/1.24.0/mongo-c-driver-1.24.0.tar.gz
tar -xzf mongo-c-driver-1.24.0.tar.gz
cd mongo-c-driver-1.24.0

# 编译安装
mkdir cmake-build
cd cmake-build
cmake -DENABLE_AUTOMATIC_INIT_AND_CLEANUP=OFF ..
make
sudo make install

小提示:我在项目中遇到过一个问题 —— 如果目标板是 ARM 架构,记得用交叉编译工具链。否则编译出来的库在板子上跑不起来。

核心 API 详解

今天重点讲三个 API:mongoc_client_newmongoc_collection_find_with_opts,以及配套的初始化与清理函数。

1. mongoc_client_new —— 创建客户端连接

这个函数用来创建一个 MongoDB 客户端对象。说白了,就是告诉驱动:「我要连哪台服务器,端口是多少」。

#include <mongoc/mongoc.h>

int main() {
    mongoc_client_t *client;
    
    // 初始化驱动(必须调用)
    mongoc_init();
    
    // 创建客户端连接
    client = mongoc_client_new("mongodb://localhost:27017");
    if (!client) {
        fprintf(stderr, "创建客户端失败\n");
        return -1;
    }
    
    // ... 后续操作 ...
    
    // 清理
    mongoc_client_destroy(client);
    mongoc_cleanup();
    
    return 0;
}

你想想看,这个连接字符串其实挺灵活的。可以指定用户名密码、认证数据库、副本集等参数。比如:

"mongodb://user:pass@192.168.1.100:27017/admin?replicaSet=myReplSet"

注意:我曾经踩过一个坑 —— 忘记调用 mongoc_init() 就直接创建客户端。结果程序跑起来就段错误。嗯,这个初始化函数必须最先调用,而且每个线程只需要调用一次。

2. mongoc_collection_find_with_opts —— 带选项的查询

这个函数是 MongoDB C Driver 里最常用的查询接口。它比老版的 mongoc_collection_find 更灵活,支持传入各种查询选项。

先看一个最简单的例子:

mongoc_collection_t *collection;
mongoc_cursor_t *cursor;
bson_t *filter;
bson_t *opts;
const bson_t *doc;

// 获取集合对象
collection = mongoc_client_get_collection(client, "mydb", "sensors");

// 构造查询条件:查找温度大于30的数据
filter = BCON_NEW("temperature", BCON_INT32(30));

// 构造选项:限制返回10条,按时间降序
opts = BCON_NEW("limit", BCON_INT32(10),
                "sort", "{", "timestamp", BCON_INT32(-1), "}");

// 执行查询
cursor = mongoc_collection_find_with_opts(collection, filter, opts, NULL);

// 遍历结果
while (mongoc_cursor_next(cursor, &doc)) {
    char *str = bson_as_json(doc, NULL);
    printf("%s\n", str);
    bson_free(str);
}

// 清理
mongoc_cursor_destroy(cursor);
bson_destroy(filter);
bson_destroy(opts);
mongoc_collection_destroy(collection);

这里有几个关键点:

  • filter:查询条件,用 BSON 格式构造。说白了就是 JSON 的二进制版本。
  • opts:查询选项,比如 limit、sort、projection 等。
  • 第四个参数:可以传一个 read preference,一般传 NULL 用默认值。

BSON 构造技巧

BSON 是 MongoDB 的「母语」。在 C 语言里,我们通常用 BCON_NEW 宏来快速构造 BSON 对象。

我常用的几种构造方式:

场景 代码示例
简单等值查询 BCON_NEW("status", BCON_UTF8("active"))
范围查询 BCON_NEW("age", "{", "$gte", BCON_INT32(18), "$lte", BCON_INT32(60), "}")
嵌套文档 BCON_NEW("address", "{", "city", BCON_UTF8("Beijing"), "}")
数组查询 BCON_NEW("tags", "[", BCON_UTF8("sensor"), BCON_UTF8("iot"), "]")

经验之谈:我在调试时经常用 bson_as_json() 把 BSON 转成可读的 JSON 字符串打印出来。这样能快速确认构造的查询条件对不对。

完整示例:查询传感器数据

下面是一个完整的实战例子。假设我们有一个物联网项目,传感器每5秒上报一次数据,我们要查询最近1小时内温度超过35度的记录。

#include <stdio.h>
#include <mongoc/mongoc.h>
#include <time.h>

int main() {
    mongoc_client_t *client;
    mongoc_collection_t *collection;
    mongoc_cursor_t *cursor;
    bson_t *filter, *opts;
    const bson_t *doc;
    bson_error_t error;
    
    // 初始化
    mongoc_init();
    
    // 连接 MongoDB
    client = mongoc_client_new("mongodb://localhost:27017");
    if (!client) {
        fprintf(stderr, "连接失败\n");
        return -1;
    }
    
    // 获取集合
    collection = mongoc_client_get_collection(client, "iot_db", "sensor_data");
    
    // 构造查询条件
    // 条件1:温度 > 35
    // 条件2:时间在最近1小时内
    time_t now = time(NULL);
    time_t one_hour_ago = now - 3600;
    
    filter = BCON_NEW("temperature",
                      "{", "$gt", BCON_INT32(35), "}",
                      "timestamp",
                      "{", "$gte", BCON_INT64(one_hour_ago), "}");
    
    // 选项:按时间降序,只返回20条
    opts = BCON_NEW("sort", "{", "timestamp", BCON_INT32(-1), "}",
                    "limit", BCON_INT32(20));
    
    // 执行查询
    cursor = mongoc_collection_find_with_opts(collection, filter, opts, NULL);
    
    // 打印结果
    printf("=== 高温传感器数据(最近1小时)===\n");
    while (mongoc_cursor_next(cursor, &doc)) {
        char *json = bson_as_json(doc, NULL);
        printf("%s\n", json);
        bson_free(json);
    }
    
    // 检查是否有错误
    if (mongoc_cursor_error(cursor, &error)) {
        fprintf(stderr, "查询出错: %s\n", error.message);
    }
    
    // 清理
    mongoc_cursor_destroy(cursor);
    bson_destroy(filter);
    bson_destroy(opts);
    mongoc_collection_destroy(collection);
    mongoc_client_destroy(client);
    mongoc_cleanup();
    
    return 0;
}

知识体系结构图

下面这张图展示了 MongoDB C Driver 的核心调用流程:

MongoDB C Driver 核心调用流程 mongoc_init() mongoc_client_new(uri) mongoc_client_get_collection() mongoc_collection_find_with_opts() 关键参数 filter: 查询条件(BSON) opts: 选项(limit/sort等) 返回值: mongoc_cursor_t* 遍历结果: while(cursor_next) { bson_as_json() } 清理顺序: cursor → filter → opts → collection → client

避坑指南

我总结了几条实战中容易踩的坑:

  • 内存泄漏:BSON 对象、cursor、collection 都需要手动释放。我曾经因为忘记释放 cursor,导致程序跑了几天后内存爆了。
  • 线程安全:MongoDB C Driver 的 client 对象是线程安全的,但 cursor 不是。多线程环境下,每个线程应该有自己的 cursor。
  • 连接池:默认情况下,每个 client 内部维护了一个连接池。不需要手动管理连接,但要注意不要频繁创建销毁 client。
  • 错误处理mongoc_cursor_error() 一定要检查。网络波动、服务器超时都会导致查询失败,不检查的话程序可能静默地返回空结果。

特别提醒:在嵌入式设备上,MongoDB C Driver 编译出来的体积不小。如果 flash 空间紧张,可以考虑裁剪掉不需要的功能(比如 TLS、SASL 认证等)。

总结

MongoDB C Driver 的核心就三个步骤:初始化、创建客户端、执行查询。其中 mongoc_collection_find_with_opts 是最常用的查询接口,配合 BSON 构造器可以完成各种复杂的查询需求。

我个人觉得,C 语言操作 MongoDB 最大的门槛不是 API 本身,而是 BSON 的构造和内存管理。只要把这两个点搞明白了,剩下的就是熟练度的问题了。

嗯,今天就聊到这里。代码已经贴出来了,建议你动手跑一遍,看看实际效果。


公众号:蓝海资料掘金营,微信 deep3321