GeoIP的使用-C語言版


0x00. 簡介

GeoIP庫可以根據IP地址(支持IPv4 和 IPv6), 定位該IP所在的 洲、經緯度、國家、省市、ASN 等信息。

GeoIP目前已經升級到GeoIP2,GeoIP2有兩個版本,一個免費版(GeoLite2),一個收費版本(GeoIP2, 200$起步)。

收費版本的准確率稍高一些,更新頻率為每周二更新一次, 免費版是每月第一個周二更新一次。

兩者對比可以參考官網說明 https://www.maxmind.com/en/geoip2-city-accuracy-comparison

對於大部分項目來說免費版已經足夠使用了.

除了GeoIP外, 其實還有 ip2location、Quova等也提供類似的功能, 但都是收費的.

 

0x01. 資源下載

很多linux版本支持這個庫, 可以使用yum 或 apt 進行下載, windows上使用的話就需要自己編譯了.

源碼下載:

https://dev.maxmind.com/geoip/geoip2/downloadable/

GeoIP2提供了多種語言的API接口供選擇.

這里我需要使用C語言接口, 所以下載C語言版的源碼.

https://github.com/maxmind/libmaxminddb/releases

GeoIP數據庫下載:

https://dev.maxmind.com/geoip/geoip2/geolite2/

可以看到官網提供三種庫,2種格式, 首先 官網API是需要使用二進制庫文件, CSV格式的庫可以導入其他程序 或 供你簡單瀏覽。

三種庫的區別可以從名字上就可以看出來:

  City       精確到城市(大小70M左右),

  Country 精確到國家(4M左右),

  ASN       用於產看IP地址的擁有者(7M左右). 需要注意的是 City 和 Country 庫中不含ASN信息

對於ASN的理解可以通過知乎了解一下 https://www.zhihu.com/question/21024981

根據業務需求選擇. 這里我們下載精確到城市的數據庫文件.

https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz

由於數據庫經常更新, 官網還提供了更新的方案:

https://dev.maxmind.com/geoip/geoipupdate/#For_Free_GeoLite2_Databases

 

0x02.接口說明

http://maxmind.github.io/libmaxminddb/

/* ------------------數據庫的關閉與打開
 * 這里 MMDB_open 的 flags 參數需要說明一下, 
 * 目前代碼實現db文件都是使用mmap()映射到內存的,
 * 所以flags其實設置什么都無所謂,所以默認使用 MMDB_MODE_MMAP就好.
 */
int MMDB_open(const char *const filename, uint32_t flags, MMDB_s *const mmdb);
void MMDB_close(MMDB_s *const mmdb);

/* ------------------數據搜索
 * 網絡字節序地址搜索API, 即傳入sockaddr參數是網絡字節序的地址 */
MMDB_lookup_result_s MMDB_lookup_sockaddr(MMDB_s *const mmdb,
    const struct sockaddr *const sockaddr, int *const mmdb_error);

/* 字符串IP地址搜索API, 即傳入ipstr參數的null結尾字符串,如"114.240.52.162" 
 * 這個API其實就是調用 getaddrinfo() 將字符串轉換為網絡地址, 
 * 然后再調用 MMDB_lookup_sockaddr() 實現的, 
 * 所以不要將網絡地址轉成字符串,再調用這個函數, 直接使用MMDB_lookup_sockaddr()
 * gai_error參數 是用來返回 getaddrinfo() 錯誤碼的.
 */
MMDB_lookup_result_s MMDB_lookup_string(MMDB_s *const mmdb, const char *const ipstr,
    int *const gai_error, int *const mmdb_error);
    
/* ------------------從搜索到數據中提取指定數據
 * 下面 3 個函數意義相同, 只不過傳入參數的方法不同.
 * 3個函數前2個參數相同, 第一個是 搜索結果中搜到的entry,具體看例子程序
 * 第二個參數是 獲取數據的存放處, 剩下的參數都是搜索用的數據.
 * 其實這3個函數實現是層層調用的關系:
 * MMDB_get_value()是可變參數函數, 函數內部把 可變參數 用 va_list 封裝起來,
 * 接着調用 MMDB_vget_value(), 這個 vget函數把所有可變參數 再分離開來,
 * 放到一個 字符串指針數組里面, 再把這個數組傳遞給 MMDB_aget_value()函數,
 * 最終就是 aget 函數完成搜索功能. 
 * 所以如果考慮到性能, 那個最好直接調用 aget 函數.
 * 另外, 需要注意 傳入的參數 最后一個必須是 NULL, 否則會導致程序崩潰
 */
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_lookup_sockaddr/MMDB_lookup_string 后獲取所有該IP地址相關信息
 * 具體使用看官方例子, 用處不大, 主要是可以用來了解一下結果數據的組成
 */
int MMDB_get_entry_data_list(MMDB_entry_s *start, MMDB_entry_data_list_s **const entry_data_list);
int MMDB_dump_entry_data_list(FILE *const stream,MMDB_entry_data_list_s *const entry_data_list, int indent);
void MMDB_free_entry_data_list(MMDB_entry_data_list_s *const 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_read_node例子可以看源碼的 read_node_t.c,個人覺得沒什么用 */
const char *MMDB_strerror(int error_code);
const char *MMDB_lib_version(void);
int MMDB_read_node(MMDB_s *const mmdb, uint32_t node_number, MMDB_search_node_s *const node);

 

 

0x03. 例子

例子1:使用 City 或 Country 庫 查詢IP所屬位置信息

/* this file must be utf8 encode */
#include <errno.h>
#include <maxminddb.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv)
{
    char *filename = "./GeoLite2-City.mmdb";
    char *ip_address = "114.240.52.162";

    MMDB_s mmdb;
    MMDB_entry_data_s entry_data;
     MMDB_lookup_result_s result;
    int gai_error, mmdb_error;
    
    int status = MMDB_open(filename, MMDB_MODE_MMAP, &mmdb);

    if (MMDB_SUCCESS != status) {
        fprintf(stderr, "Can't open %s - %s\n", filename, MMDB_strerror(status));

        if (MMDB_IO_ERROR == status) {
            fprintf(stderr, "IO error: %s\n", strerror(errno));
        }
        exit(1);
    }

    result = MMDB_lookup_string(&mmdb, ip_address, &gai_error, &mmdb_error);
    if (0 != gai_error) {
        fprintf(stderr, "Error from getaddrinfo for %s - %s\n", ip_address, gai_strerror(gai_error));
        exit(2);
    }

    if (MMDB_SUCCESS != mmdb_error) {
        fprintf(stderr, "Got an error from libmaxminddb: %s\n", MMDB_strerror(mmdb_error));
        exit(3);
    }

    if (result.found_entry) {
        /* 獲取國家名稱(簡體中文) */
        status = MMDB_get_value(&result.entry, &entry_data, "country", "names", "zh-CN", NULL);
/* 獲取國家簡稱(CN/AU/JP等) */ // status = MMDB_get_value(&result.entry, &entry_data, "country", "iso_code", NULL); /* 獲取城市名稱(簡體中文) */ // status = MMDB_get_value(&result.entry, &entry_data, "city", "names", "zh-CN", NULL); if (MMDB_SUCCESS == status) {/* MMDB_get_value 成功 */ if (entry_data.has_data) {/* 找到了想要的數據 */ if (entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) { /* entry_data.utf8_string 返回的不是null-terminated 字符串,需要根據長度自己截取數據 */ fwrite(entry_data.utf8_string, entry_data.data_size, 1, stdout); printf("\n"); } else { printf("data_type = %d\n", entry_data.type); } } else { fprintf(stderr, "MMDB_get_value not found\n"); } } else { fprintf(stderr, "MMDB_get_value failed,%s\n", MMDB_strerror(status)); } } MMDB_close(&mmdb); exit(EXIT_SUCCESS); }

例子2: 使用ASN庫查詢IP地址的ASN

/* this file must be utf8 encode */
#include <errno.h>
#include <maxminddb.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv)
{
    char *filename = "./GeoLite2-ASN.mmdb";
    char *ip_address = "114.240.52.162";

    MMDB_s mmdb;
    MMDB_entry_data_s entry_data;
     MMDB_lookup_result_s result;
    int gai_error, mmdb_error;
    
    int status = MMDB_open(filename, MMDB_MODE_MMAP, &mmdb);

    if (MMDB_SUCCESS != status) {
        fprintf(stderr, "Can't open %s - %s\n", filename, MMDB_strerror(status));

        if (MMDB_IO_ERROR == status) {
            fprintf(stderr, "IO error: %s\n", strerror(errno));
        }
        exit(1);
    }

    result = MMDB_lookup_string(&mmdb, ip_address, &gai_error, &mmdb_error);
    if (0 != gai_error) {
        fprintf(stderr, "Error from getaddrinfo for %s - %s\n", ip_address, gai_strerror(gai_error));
        exit(2);
    }

    if (MMDB_SUCCESS != mmdb_error) {
        fprintf(stderr, "Got an error from libmaxminddb: %s\n", MMDB_strerror(mmdb_error));
        exit(3);
    }

/* ASN DB INFO Example
{
   "autonomous_system_number": 
     4808 <uint32>
   "autonomous_system_organization": 
     "China Unicom Beijing Province Network" <utf8_string>
 }
*/
    if (result.found_entry) {
        /* 獲取ASN */
        status = MMDB_get_value(&result.entry, &entry_data, "autonomous_system_organization", NULL);
        
        if (MMDB_SUCCESS == status) {/* MMDB_get_value 成功 */
            if (entry_data.has_data) {/* 找到了想要的數據 */
                if (entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) {
                    /* entry_data.utf8_string 返回的不是null-terminated 字符串,需要根據長度自己截取數據 */
                    fwrite(entry_data.utf8_string, entry_data.data_size, 1, stdout);
                    printf("\n");
                }
                else {
                    printf("data_type = %d\n", entry_data.type);
                }
            }
            else {
                fprintf(stderr, "MMDB_get_value not found\n");
            }
        }
        else {
            fprintf(stderr, "MMDB_get_value failed,%s\n", MMDB_strerror(status));
        }
    }


    MMDB_close(&mmdb);
    exit(EXIT_SUCCESS);
}

 

0x04. 題外話

從源代碼來看, 官方提供的庫並不適合高速大量查詢, 所以有很多第三方實現,例如nginx等都自己實現了數據庫的搜索模塊.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM