拓冰建站拓冰建站
首页 / 资讯中心 / 正文

libmaxminddb 与 Fluent Bit geoip2 过滤器:MaxMind DB 文件读取 C API 全解析

libmaxminddb 与 Fluent Bit geoip2 过滤器MaxMind DB 文件读取 C API 全解析【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bitlibmaxminddb 是 MaxMind 官方提供的 C 语言库用于读取 MaxMind DB 格式的地理数据库文件如 GeoLite2、GeoIP2 系列。本文以当前仓库中随 filter_geoip2 插件一同打包的 libmaxminddb 1.12.2 官方文档 为主体完整梳理其公开数据结构、状态码与全部导出函数并结合 Fluent Bit 中 geoip2 过滤器源码 展示该库在真实数据处理管线中的落地方式。读完本文你将掌握 MaxMind DB 文件从打开、IP 查询、数据提取到资源释放的完整 C 编程范式。libmaxminddb 是什么以及它如何进入 Fluent Bitlibmaxminddb 提供了一组以MMDB_为前缀的函数、结构与宏用于读取 MaxMind DB 格式的二进制数据库。数据库与查询结果都以不同的 C 数据结构表示通过MMDB_open()打开数据库句柄通过MMDB_lookup_string()字符串 IP或MMDB_lookup_sockaddr()已解析的sockaddr执行 IP 查询再通过MMDB_get_value()或MMDB_get_entry_data_list()提取关联数据最后以MMDB_close()释放资源。在 Fluent Bit 中该库被 filter_geoip2 过滤器插件直接复用插件的 CMakeLists.txt 以add_subdirectory(libmaxminddb-1.12.2 EXCLUDE_FROM_ALL)引入该子库并以FLB_PLUGIN(filter_geoip2 ${src} maxminddb)链接。也就是说下面讲解的每一个 API 都真实运行在 Fluent Bit 的每一条日志记录上——理解它们就等于理解了 geoip2 过滤器的内部工作原理。核心数据结构一览所有由maxminddb.h导出的数据结构都以typedef struct foo_s { ... } foo_s形式定义使用时可省略struct前缀。该头文件位于 include/maxminddb.h。MMDB_s——数据库句柄typedef struct MMDB_s { uint32_t flags; const char *filename; ... MMDB_metadata_s metadata; } MMDB_s;公开字段含义flags打开数据库时传入的标志详见MMDB_open()filename调用MMDB_open()时传入的文件路径metadata数据库元数据。其余字段仅供内部使用可能随版本变化。MMDB_metadata_s与MMDB_description_s——元数据typedef struct MMDB_metadata_s { uint32_t node_count; uint16_t record_size; uint16_t ip_version; const char *database_type; struct { size_t count; const char **names; } languages; uint16_t binary_format_major_version; uint16_t binary_format_minor_version; uint64_t build_epoch; struct { size_t count; MMDB_description_s **descriptions; } description; } MMDB_metadata_s; typedef struct MMDB_description_s { const char *language; const char *description; } MMDB_description_s;ip_version取值恒为4或6binary_format_major_version恒为2libmaxminddb 仅支持主版本 2 的数据库languages与description的count允许为 0数据库元数据不强制包含语言与描述其余字段应全部填充。除直接读取该结构外也可调用MMDB_get_metadata_as_entry_data_list()以链表形式获取元数据往往更便捷。MMDB_lookup_result_s——查询结果typedef struct MMDB_lookup_result_s { bool found_entry; MMDB_entry_s entry; uint16_t netmask; } MMDB_lookup_result_s;found_entry为 false 时其余成员无意义必须首先检查该字段entry用于进一步提取与该 IP 关联的数据netmask表示该 IP 在数据库中归属的子网。例如在 IPv4 数据库中查询1.1.1.1返回 netmask 16则说明该地址属于1.1.0.0/16子网。关于 netmask 的注意事项如果数据库是 IPv6 数据库返回的 netmask 始终是 IPv6 前缀长度0–128即使该库同时包含 IPv4 网络。此时若查询的是 IPv4 地址并希望换算成 IPv4 掩码只需将值减去96。MMDB_entry_data_s——单条数据条目typedef struct MMDB_entry_data_s { bool has_data; union { uint32_t pointer; const char *utf8_string; double double_value; const uint8_t *bytes; uint16_t uint16; uint32_t uint32; int32_t int32; uint64_t uint64; {mmdb_uint128_t or uint8_t[16]} uint128; bool boolean; float float_value; }; ... uint32_t data_size; uint32_t type; } MMDB_entry_data_s;has_data为 true 表示查询到了数据为 false 时其余成员无意义联合体中哪个成员被填充取决于type成员pointer成员在 API 返回的数据中绝不会被填充——指针在库内部始终被解析为实际数据data_size仅对utf8_string与bytes有意义utf8_string不是以\0结尾的必须用data_size确定其长度type可与MMDB_DATA_TYPE_*宏比较。128 位整数uint128的特殊处理uint128的处理取决于平台是否支持 128 位整数GCC 4.4/4.5 下使用unsigned int __attribute__ ((__mode__ (TI)))GCC 4.6 与 clang 3.2 可直接使用unsigned __int128更老的编译器无法使用整数类型此时退化为 16 字节的uint8_t数组直接对应数据库中的原始字节。为屏蔽差异库在maxminddb.h中定义了mmdb_uint128_t类型并导出公开宏MMDB_UINT128_IS_BYTE_ARRAY若其值为 1则uint128以字节数组返回为 0 则以mmdb_uint128_t整数返回。数据类型宏对 MaxMind DB 规范中的每一种数据类型库都提供了对应宏MMDB_DATA_TYPE_UTF8_STRING、MMDB_DATA_TYPE_DOUBLE、MMDB_DATA_TYPE_BYTES、MMDB_DATA_TYPE_UINT16、MMDB_DATA_TYPE_UINT32、MMDB_DATA_TYPE_MAP、MMDB_DATA_TYPE_INT32、MMDB_DATA_TYPE_UINT64、MMDB_DATA_TYPE_UINT128、MMDB_DATA_TYPE_ARRAY、MMDB_DATA_TYPE_BOOLEAN、MMDB_DATA_TYPE_FLOAT。另有仅供内部使用的类型MMDB_DATA_TYPE_EXTENDED、MMDB_DATA_TYPE_POINTER、MMDB_DATA_TYPE_CONTAINER、MMDB_DATA_TYPE_END_MARKER。如果返回数据中出现这些类型说明数据库已损坏、生成有误或库本身存在 bug。指针值与MMDB_close()utf8_string、bytes以及可能的uint128成员是指向数据库数据区calloc或mmap的内存块的直接指针。调用MMDB_close()之后这些指针即失效如需在关闭后继续引用数据必须先用strdup、memcpy等函数复制出来。MMDB_entry_data_list_s——条目链表typedef struct MMDB_entry_data_list_s { MMDB_entry_data_s entry_data; struct MMDB_entry_data_list_s *next; } MMDB_entry_data_list_s;该结构把 map 或 array 的整体数据组织成链表可逐项遍历。MMDB_search_node_s——搜索树节点typedef struct MMDB_search_node_s { uint64_t left_record; uint64_t right_record; uint8_t left_record_type; uint8_t right_record_type; MMDB_entry_s left_record_entry; MMDB_entry_s right_record_entry; } MMDB_search_node_s;该结构主要用于遍历整个搜索树而非查询单个 IP。两个 record 的类型取值MMDB_RECORD_TYPE_SEARCH_NODE指向下一个搜索节点MMDB_RECORD_TYPE_EMPTY占位符表示该 IP 无数据搜索应在此结束MMDB_RECORD_TYPE_DATA指向数据区中的数据使用该 record 的 entry 提取数据MMDB_RECORD_TYPE_INVALID节点无效或数据库损坏。MMDB_entry_s成员仅在类型为MMDB_RECORD_TYPE_DATA时有效其他类型下使用会得到错误或无效数据。状态码Status Codes多数函数返回或填充以下状态码所有状态码都应视为int值状态码含义MMDB_SUCCESS一切正常MMDB_FILE_OPEN_ERROR打开 MaxMind DB 文件失败MMDB_IO_ERRORIO 操作失败需查看errno获取详情MMDB_CORRUPT_SEARCH_TREE_ERROR搜索树查询得到不可能的结果数据库损坏或生成有误MMDB_INVALID_METADATA_ERROR元数据缺失键或含非法值如ip_version为 7MMDB_UNKNOWN_DATABASE_FORMAT_ERROR数据库主版本不是 2本库只支持主版本 2MMDB_OUT_OF_MEMORY_ERROR内存分配malloc等失败MMDB_INVALID_DATA_ERROR数据区条目包含非法数据如uint16字段声称超过 2 字节MMDB_INVALID_LOOKUP_PATH_ERROR传给MMDB_get_value/MMDB_vget_value/MMDB_aget_value的查找路径中数组偏移超过LONG_MAX或小于LONG_MINMMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR查找路径与条目的数据结构不匹配map 中无该键、数组索引越界、路径期望的位置不是 map/array 等MMDB_strerror()const char *MMDB_strerror(int error_code)输入状态码返回对应的英文错误说明字符串。Fluent Bit 的 geoip2 过滤器在打开数据库失败、查询失败时正是用该函数生成日志信息。函数 API 详解MMDB_open()——打开数据库int MMDB_open( const char *const filename, uint32_t flags, MMDB_s *const mmdb);典型用法MMDB_s mmdb; int status MMDB_open(/path/to/file.mmdb, MMDB_MODE_MMAP, mmdb); if (MMDB_SUCCESS ! status) { ... } ... MMDB_close(mmdb);返回值为状态码必须检查Windows 下filename必须是 UTF-8 编码MMDB_s可以放在栈上或堆上打开成功后其内部持有堆分配数据必须用MMDB_close()释放若返回非MMDB_SUCCESS库会保证在返回前释放所有已分配内存当前唯一提供的标志是MMDB_MODE_MMAP以mmap()打开数据库传入其他值可能产生不可预知结果传0则使用默认标志当前默认为MMDB_MODE_MMAP但未来可能变化。在 Fluent Bit 的 configure() 中可以看到真实用法ctx-mmdb由flb_malloc分配随后调用MMDB_open(ctx-database, MMDB_MODE_MMAP, ctx-mmdb)返回非MMDB_SUCCESS时记录Cannot open geoip2 database: %s: %s错误日志并释放内存。MMDB_close()——关闭数据库void MMDB_close(MMDB_s *const mmdb);释放MMDB_s持有的已分配或已 mmap 的内存。注意它不会释放MMDB_s结构体本身——如果该结构体是堆分配的需要自行释放。这正是 Fluent Bit 在 cb_geoip2_exit() 中先MMDB_close(ctx-mmdb)再flb_free(ctx-mmdb)的原因。MMDB_lookup_string()——按字符串 IP 查询MMDB_lookup_result_s MMDB_lookup_string( MMDB_s *const mmdb, const char *const ipstr, int *const gai_error, int *const mmdb_error);int gai_error, mmdb_error; MMDB_lookup_result_s result MMDB_lookup_string(mmdb, 1.2.3.4, gai_error, mmdb_error); if (0 ! gai_error) { ... } if (MMDB_SUCCESS ! mmdb_error) { ... } if (result.found_entry) { ... }内部先调用getaddrinfo()将字符串解析为二进制形式再调用MMDB_lookup_sockaddr()若地址已经解析过应直接调用后者避免重复解析函数总是返回MMDB_lookup_result_s但必须同时检查gai_error与mmdb_error任一指示错误则返回结构无意义无错误时仍需确认found_entry为 true否则表示该 IP 在数据库中没有条目对同时含 IPv4/IPv6 数据的数据库IPv4 地址按::xxx.xxx.xxx.xxx查询而非重映射到::ffff:xxx.xxx.xxx.xxx的 IPv4-mapped 段向仅含 IPv4 数据的数据库传入 IPv6 地址时found_entry为 false但mmdb_error仍为MMDB_SUCCESS。MMDB_lookup_sockaddr()——按已解析地址查询MMDB_lookup_result_s MMDB_lookup_sockaddr( MMDB_s *const mmdb, const struct sockaddr *const sockaddr, int *const mmdb_error);除不调用getaddrinfo()外与MMDB_lookup_string()完全一致。数据查询三兄弟MMDB_get_value()/MMDB_vget_value()/MMDB_aget_value()int MMDB_get_value( MMDB_entry_s *const start, MMDB_entry_data_s *const entry_data, ...); int MMDB_vget_value( MMDB_entry_s *const start, MMDB_entry_data_s *const entry_data, va_list va_path); int MMDB_aget_value( MMDB_entry_s *const start, MMDB_entry_data_s *const entry_data, const char *const *const path);三个函数行为一致仅调用风格不同第一个参数是MMDB_entry_s通常来自MMDB_lookup_string()/MMDB_lookup_sockaddr()的返回结果第二个参数是MMDB_entry_data_s的引用找到数据时被填充找不到时其has_data为 false为 true 时查看type成员最后一个参数是查找路径一组字符串每个字符串代表一个 map 键如city或数组索引如0、1、-1。负索引表示从数组末尾偏移-1指数组最后一个元素。路径必须以NULL结尾三个函数都如此不提供路径时返回顶层 map 对应的条目。例如对于如下数据{ names: { en: Germany, de: Deutschland }, cities: [ Berlin, Frankfurt ] }查询英文名MMDB_lookup_result_s result MMDB_lookup_sockaddr(mmdb, address-ai_addr, mmdb_error); MMDB_entry_data_s entry_data; int status MMDB_get_value(result.entry, entry_data, names, en, NULL); if (MMDB_SUCCESS ! status) { ... } if (entry_data.has_data) { ... }查询第一个城市则路径为cities, 0。三个函数的返回值都是状态码。在 Fluent Bit 中的落地geoip2 过滤器的record参数支持点分路径如country.iso_codeadd_geoip_fields() 会把该路径按.拆分成字符串数组再调用MMDB_aget_value(entry, entry_data, path)取值与上面三个函数的语义完全对应。MMDB_get_entry_data_list()——一次性获取全部条目数据int MMDB_get_entry_data_list( MMDB_entry_s *start, MMDB_entry_data_list_s **const entry_data_list);一次性取出复杂数据结构的全部数据避免反复调用MMDB_get_value()MMDB_lookup_result_s result MMDB_lookup_sockaddr(mmdb, address-ai_addr, mmdb_error); MMDB_entry_data_list_s *entry_data_list, *first; int status MMDB_get_entry_data_list(result.entry, entry_data_list); if (MMDB_SUCCESS ! status) { ... } // 保存 first 以便稍后释放 first entry_data_list; while (1) { MMDB_entry_data_list_s *next entry_data_list entry_data_list-next; if (NULL next) { break; } switch (next-entry_data.type) { case MMDB_DATA_TYPE_MAP: { ... } case MMDB_DATA_TYPE_UTF8_STRING: { ... } ... } } MMDB_free_entry_data_list(first);链表按深度优先遍历顺序组织。对上面的示例数据链表依次为MAP——顶层 mapUTF8_STRING——names键MAP——names对应的 mapUTF8_STRING——en键UTF8_STRING——en的值UTF8_STRING——de键UTF8_STRING——de的值UTF8_STRING——cities键ARRAY——cities的值UTF8_STRING——array[0]UTF8_STRING——array[1]MMDB_free_entry_data_list()void MMDB_free_entry_data_list( MMDB_entry_data_list_s *const entry_data_list);MMDB_get_entry_data_list()与MMDB_get_metadata_as_entry_data_list()会在堆上分配链表需调用本函数释放。MMDB_get_metadata_as_entry_data_list()——以链表形式取元数据int MMDB_get_metadata_as_entry_data_list( MMDB_s *const mmdb, MMDB_entry_data_list_s **const entry_data_list);将数据库元数据转为MMDB_entry_data_list_s链表比直接操作MMDB_metadata_s结构更灵活MMDB_entry_data_list_s *entry_data_list, *first; int status MMDB_get_metadata_as_entry_data_list(mmdb, entry_data_list); if (MMDB_SUCCESS ! status) { ... } first entry_data_list; ... // do something with the data MMDB_free_entry_data_list(first);MMDB_dump_entry_data_list()——格式化输出int MMDB_dump_entry_data_list( FILE *const stream, MMDB_entry_data_list_s *const entry_data_list, int indent);把链表 stringify 到指定stream如stdout。indent是起始缩进级别嵌套结构map、array会递增缩进。输出是“类 JSON”格式但值带类型标注map/array 分别显示为{}/[]。该函数的输出格式可能随版本变化不应被程序化依赖它主要用于向用户展示与调试。若平台支持 GNUopen_memstream()也可用它把输出捕获为字符串。MMDB_read_node()——读取搜索树节点int MMDB_read_node( MMDB_s *const mmdb, uint32_t node_number, MMDB_search_node_s *const node);读取搜索树中指定编号的节点填充MMDB_search_node_s。传入的node_number大于数据库节点总数时返回MMDB_INVALID_NODE_NUMBER_ERROR否则返回MMDB_SUCCESS。搜索树第一个节点永远是节点 0要遍历整棵树从节点 0 开始依据每个 record 的类型跟随跳转类型为MMDB_RECORD_TYPE_SEARCH_NODE时 record 内含下一个节点编号。MMDB_lib_version()const char *MMDB_lib_version(void)返回库版本字符串形如2.0.0。完整示例程序以下示例程序取自官方文档演示了完整的生命周期打开 → 查询 → 提取 → 清理#include errno.h #include maxminddb.h #include stdlib.h #include string.h int main(int argc, char **argv) { char *filename argv[1]; char *ip_address argv[2]; MMDB_s mmdb; int status MMDB_open(filename, MMDB_MODE_MMAP, mmdb); if (MMDB_SUCCESS ! status) { fprintf(stderr, \n Cant open %s - %s\n, filename, MMDB_strerror(status)); if (MMDB_IO_ERROR status) { fprintf(stderr, IO error: %s\n, strerror(errno)); } exit(1); } int gai_error, mmdb_error; MMDB_lookup_result_s result MMDB_lookup_string(mmdb, ip_address, gai_error, mmdb_error); if (0 ! gai_error) { fprintf(stderr, \n Error from getaddrinfo for %s - %s\n\n, ip_address, gai_strerror(gai_error)); exit(2); } if (MMDB_SUCCESS ! mmdb_error) { fprintf(stderr, \n Got an error from libmaxminddb: %s\n\n, MMDB_strerror(mmdb_error)); exit(3); } MMDB_entry_data_list_s *entry_data_list NULL; int exit_code 0; if (result.found_entry) { int status MMDB_get_entry_data_list(result.entry, entry_data_list); if (MMDB_SUCCESS ! status) { fprintf( stderr, Got an error looking up the entry data - %s\n, MMDB_strerror(status)); exit_code 4; goto end; } if (NULL ! entry_data_list) { MMDB_dump_entry_data_list(stdout, entry_data_list, 2); } } else { fprintf( stderr, \n No entry for this IP address (%s) was found\n\n, ip_address); exit_code 5; } end: MMDB_free_entry_data_list(entry_data_list); MMDB_close(mmdb); exit(exit_code); }注意错误处理的三层结构打开失败 →getaddrinfo失败gai_error→ 库查询失败mmdb_error→found_entry为 false以及goto end统一完成链表释放与句柄关闭。Fluent Bit 的 geoip2 过滤器在 mmdb_lookup() 中也遵循同样的分层错误检查模式gai_error ! 0时记录getaddrinfo failedmmdb_error ! MMDB_SUCCESS时记录lookup failed。在 Fluent Bit geoip2 过滤器中的实战形态Fluent Bit 的geoip2过滤器把 libmaxminddb 封装成一条声明式配置。其 config_map 暴露三个参数databaseMaxMind DB 文件路径必填对应MMDB_open()的filenamelookup_key可多次指定声明从记录中取哪个字段作为待查询 IPrecord可多次指定格式为KEY LOOKUP_KEY VALUE即把查询结果中的VALUE路径点分形式如country.iso_code写入输出字段KEY。一个典型的配置[FILTER] Name geoip2 Match * database /path/to/GeoLite2-City.mmdb lookup_key ip record city country.names.en record country_code country.iso_code record latitude location.latitude record longitude location.longitude处理时过滤器先把每条记录中lookup_key命中的字段收集进哈希表prepare_lookup_keys()随后对每个record执行MMDB_lookup_string()MMDB_aget_value()最后依据entry_data.type分派到对应的 msgpack 编码器写出字段。从 add_geoip_fields() 的 switch 可以看到当前实现支持的类型映射UTF8_STRING/BYTES写字符串DOUBLE/FLOAT写双精度浮点UINT16/UINT32/INT32/UINT64写整数BOOLEAN写布尔而MAP、ARRAY、UINT128、EXTENDED、POINTER、CONTAINER等类型则输出null日志提示Not supported MAP and ARRAY或Not supported uint128这也印证了前面提到的数据类型宏与联合体语义在实际工程中的取舍。配套的命令行工具 mmdblookup 可以离线验证数据库内容mmdblookup --file FILE --ip IP [DATA PATH]其中路径同样支持names en、cities 1数组从 0 编号这样的逐级导航非常适合在配置过滤器前确认字段路径是否正确。运行要求与线程安全编译要求libmaxminddb 至少需要 POSIX.1-2001 支持未在编译期显式指定时默认请求 POSIX.1-2008。线程安全当库与线程安全的malloc/free实现一起编译链接时本库是线程安全的。小结libmaxminddb 通过一组设计清晰的 C 接口把 MaxMind DB 的二进制格式细节完全封装打开MMDB_open/MMDB_close、查询MMDB_lookup_string/MMDB_lookup_sockaddr、取值MMDB_get_value三兄弟与MMDB_get_entry_data_list各司其职MMDB_strerror与状态码让错误定位一目了然。在 Fluent Bit 中它作为 filter_geoip2 的底层引擎把同样的能力以database/lookup_key/record三个配置项开放给使用者——无论是直接写 C 程序还是通过 Fluent Bit 配置做流式 IP 地理信息增强掌握本库的数据结构与调用约定都是关键的第一步。【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门