侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 297 篇文章
  • 累计创建 134 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

Spring Boot集成IP地址解析工具库ip2region

孔子说JAVA
2022-08-18 / 0 评论 / 1 点赞 / 155 阅读 / 12,006 字 / 正在检测是否收录...

在移动互联网的应用中,经常会涉及获取用户具体位置信息的功能。一般有两个方法:根据GPS 定位的信息 (一般用于手机端)和用户 IP 地址的解析。GPS定位需要手机打开 GPS 功能(用户不一定开启),且有时并不需要太精确的位置(到城市这个级别即可),所以根据 IP 地址解析用户位置是个不错的选择。ip解析需要一个 IP 和地理位置的映射关系库,并依赖这个库启动一个 IP 转地理位置的服务,ip2region 是一个不错的选择。

1、介绍

ip2region v2.0 - 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言(java, php, c, python, nodejs, golang, c#)的 xdb 数据生成和查询客户端实现。支持Binary,B树,内存三种查询算法,速度很快也不占内存。

特性如下:

1)标准化的数据格式

每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,后面的选项全部是0。

2)数据去重和压缩

xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。

3)极速查询响应

速度快,内置三种查询算法,支持毫秒级查询。即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:

  • vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。
  • xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。

在查询IP时有三种算法,分别是:

  • memory算法:整个数据库全部载入内存,单次查询都在0.1x毫秒内,C语言的客户端单次查询在0.00x毫秒级别。
    method = searcher.getClass().getMethod("memorySearch", String.class);
    
  • binary算法:基于二分查找,基于ip2region.db文件,不需要载入内存,单次查询在0.x毫秒级别。
    method = searcher.getClass().getMethod("binarySearch", String.class);
    
  • b-tree算法:基于btree算法,基于ip2region.db文件,不需要载入内存,单词查询在0.x毫秒级别,比binary算法更快。
    method = searcher.getClass().getMethod("btreeSearch", String.class);
    

任何客户端b-tree都比binary算法快,当然memory算法是最快的!

4)IP 数据管理框架

v2.0 格式的 xdb 支持亿级别的 IP 数据段行数,region 信息也可以完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。

5)准确率高

数据聚合多个供应商的数据。

6)支持多客户端

支持很多客户端,已经集成的客户端有:java、C#、php、c、python、nodejs、php扩展(php5和php7)、golang、rust、lua、lua_c, nginx。

2、Spring Boot集成ip2region

2.1 pom.xml引入库

<properties>
    <ip2region.version>1.7.2</ip2region.version>
</properties>

<!-- ip2region -->
<dependency>
    <groupId>org.lionsoul</groupId>
    <artifactId>ip2region</artifactId>
    <version>${ip2region.version}</version>
</dependency>

2.2 下载ip2region.db文件

方式一:直接下载

最新版本下载地址: https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region.db
V1.0下载地址: https://gitee.com/lionsoul/ip2region/blob/master/v1.0/data/ip2region.db

方式二:clone git项目

也可以通过clone git项目后在data文件夹下找到这个文件。

$ git clone https://gitee.com/lionsoul/ip2region.git

下载这个项目之后到data/文件夹下面可以找到ip2region.db文件。

2.3 ip2region.db文件添加到springboot项目

将下载的ip2region.db文件添加到springboot项目的resoures路径下。

image-1660695740625

2.4 ip2region 工具类

使用如下工具类可以方便的定位和解析ip地址。输出内容实例:|中国|华南|广东省|广州市|电信,分别代表国家、区域、省份、市和运营商。无数据区域默认为0。

  • 注:如果项目打jar包,需要调用方法 getLocation(HttpServletRequest request) 或 getLocation(String ip)。同时需要将 ip2region.db 打包到jar包内。
import cn.hutool.core.io.IoUtil;
import com.founder.xhjz.dto.IpAreaDTO;
import org.apache.commons.lang3.StringUtils;
import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbSearcher;
import org.springframework.core.io.ClassPathResource;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * IP解析工具类
 *
 * @Author kongzi
 * @Date 2022/8/4
 * @Version 1.0
 */
public class IpUtils {

    private static final String UNKNOWN_ADDRESS = "未知";

    private static final String SPLIT = "\\|";

    private static final String CHN = "中国";

    private static final String[] GATS = {"香港", "澳门", "台湾"};//港澳台

    /**
     * 根据IP获取地址
     *
     * @return IpAreaDTO
     */
    public static IpAreaDTO getLocation(HttpServletRequest request) {
        String ip = getIpAddress(request);
        IpAreaDTO areaDTO = getLocation(ip);
        return areaDTO;
    }

    /**
     * 根据IP获取地址
     *
     * @return IpAreaDTO
     */
    public static IpAreaDTO getLocation(String ip) {
        IpAreaDTO areaDTO = new IpAreaDTO();
        areaDTO.setIp(ip);
        String area = null;
        try {
            area = getLocationByIPMemory(ip);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if(StringUtils.isNotBlank(area)) {
            String[] areas = area.split("\\|");
            areaDTO.setCountry(areas[0]);
            if(areas.length >= 3) {
                areaDTO.setProvince(areas[1]);
                if(areas.length == 3) {
                    areaDTO.setNetworkISP(areas[2]);
                } else if(areas.length == 4) {
                    areaDTO.setCity(areas[2]);
                    areaDTO.setNetworkISP(areas[3]);
                }
            }
        }
        return areaDTO;
    }

    /**
     * 根据IP获取地址
     *
     * @return 国家|区域|省份|城市|ISP
     */
    public static String getLocationByIP(String ip) {
        try {
//            return getLocationByIP(ip, DbSearcher.BTREE_ALGORITHM);
            return getLocationByIP(ip, DbSearcher.MEMORY_ALGORITYM);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 根据IP获省份,国外则获取国家
     *
     * @return 国家|区域|省份|城市|ISP
     */
    public static String getProvinceByIP(String ip) {
        String location = getLocationByIP(ip);
        if (StringUtils.isEmpty(location))
            return UNKNOWN_ADDRESS;
        String[] locations = location.split(SPLIT);
        if (!CHN.equals(locations[0])) {
            return locations[0];
        } else if (locations.length > 1) {
            String province = locations[1].replace("省", "");
            if (Arrays.asList(GATS).contains(province)) {
                return CHN + province;
            }
            return locations[1].replace("省", "");
        }
        return UNKNOWN_ADDRESS;
    }

    /**
     * 根据IP获省份,国外则获取国家 - 从内存获取,适用于项目打jar包的情况
     *
     * @return 国家|区域|省份|城市|ISP
     */
    public static String getLocationByIPMemory(String ip) {
        DbSearcher searcher = null;
        try {
            // jar包内需要通过该方式获取
            // 获取ip库路径
            ClassPathResource classPathResource = new ClassPathResource("ip2region.db");
            if (classPathResource.getClassLoader() == null){
                System.out.println("存储路径发生错误,没有被发现");
                return null;
            }
            InputStream inputStream = classPathResource.getInputStream();
            byte[] bytes = IoUtil.readBytes(inputStream);
            DbConfig dbConfig = new DbConfig();
            searcher = new DbSearcher(dbConfig, bytes);
            Method method = searcher.getClass().getMethod("memorySearch", String.class);
            // address1--中国|0|北京|北京市|联通
            String address = ((DataBlock) method.invoke(searcher, ip)).getRegion();
            // address1--中国|北京|北京市|联通
            address = address.replace("0|", "");
            char symbol = '|';
            if (address.charAt(address.length() - 1) == symbol) {
                address = address.substring(0, address.length() - 1);
            }
            return address.equals("内网IP|内网IP") ? UNKNOWN_ADDRESS : address;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return UNKNOWN_ADDRESS;
    }

    /**
     * 根据IP获取地址 - jar包内需要通过该方式获取
     *
     * @param ip
     * @param algorithm 查询算法
     * @return 国家|区域|省份|城市|ISP
     * @see DbSearcher DbSearcher.BTREE_ALGORITHM; //B-tree
     * DbSearcher.BINARY_ALGORITHM //Binary DbSearcher.MEMORY_ALGORITYM
     * //Memory
     */
    public static String getLocationByIP(String ip, int algorithm) {
        DbSearcher searcher = null;
        try {
            //
//            String dbPath = IpUtils.class.getResource("/ip2region.db").getPath();
//            searcher = new DbSearcher(new DbConfig(), dbPath);
            // jar包内需要通过该方式获取
            // 获取ip库路径
            ClassPathResource classPathResource = new ClassPathResource("ip2region.db");
            if (classPathResource.getClassLoader() == null){
                System.out.println("存储路径发生错误,没有被发现");
                return null;
            }
            InputStream inputStream = classPathResource.getInputStream();
            byte[] bytes = IoUtil.readBytes(inputStream);
            searcher = new DbSearcher(new DbConfig(), bytes);
            DataBlock dataBlock;
            switch (algorithm) {
                case DbSearcher.BTREE_ALGORITHM:
                    dataBlock = searcher.btreeSearch(ip);
                    break;
                case DbSearcher.BINARY_ALGORITHM:
                    dataBlock = searcher.binarySearch(ip);
                    break;
                case DbSearcher.MEMORY_ALGORITYM:
                    dataBlock = searcher.memorySearch(ip);
                    break;
                default:
                    return UNKNOWN_ADDRESS;
            }
            String address = dataBlock.getRegion().replace("0|", "");
            char symbol = '|';
            if (address.charAt(address.length() - 1) == symbol) {
                address = address.substring(0, address.length() - 1);
            }
            return address.equals("内网IP|内网IP") ? UNKNOWN_ADDRESS : address;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (searcher != null) {
                try {
                    searcher.close();
                } catch (IOException ignored) {
                    ignored.printStackTrace();
                }
            }
        }
        return UNKNOWN_ADDRESS;
    }

    public static String getLocationByIP2(String ip, int algorithm) {
        DbSearcher searcher = null;
        try {
            // 通过类路径指定
            String dbPath = IpUtils.class.getResource("/ip2region.db").getPath();
            searcher = new DbSearcher(new DbConfig(), dbPath);
            DataBlock dataBlock;
            switch (algorithm) {
                case DbSearcher.BTREE_ALGORITHM:
                    dataBlock = searcher.btreeSearch(ip);
                    break;
                case DbSearcher.BINARY_ALGORITHM:
                    dataBlock = searcher.binarySearch(ip);
                    break;
                case DbSearcher.MEMORY_ALGORITYM:
                    dataBlock = searcher.memorySearch(ip);
                    break;
                default:
                    return UNKNOWN_ADDRESS;
            }
            String address = dataBlock.getRegion().replace("0|", "");
            char symbol = '|';
            if (address.charAt(address.length() - 1) == symbol) {
                address = address.substring(0, address.length() - 1);
            }
            return address.equals("内网IP|内网IP") ? UNKNOWN_ADDRESS : address;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (searcher != null) {
                try {
                    searcher.close();
                } catch (IOException ignored) {
                    ignored.printStackTrace();
                }
            }
        }
        return UNKNOWN_ADDRESS;
    }

    /**
     * 通过request获取IP
     *
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        // X-Forwarded-For:Squid 服务代理
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            // Proxy-Client-IP:apache 服务代理
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            // WL-Proxy-Client-IP:weblogic 服务代理
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            // HTTP_CLIENT_IP:有些代理服务器
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            // X-Real-IP:nginx服务代理
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("X-Real-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
        } else if (ip.length() > 15) {
            // 有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
            String[] ips = ip.split(",");
            for (int index = 0; index < ips.length; index++) {
                String strIp = (String) ips[index];
                if (!("unknown".equalsIgnoreCase(strIp))) {
                    ip = strIp;
                    break;
                }
            }
        }
        return ip;
    }

    public static void main(String[] args) {
        System.out.println(IpUtils.getProvinceByIP("114.44.227.87"));
        System.out.println(IpUtils.getLocationByIP2("127.0.0.87", DbSearcher.BTREE_ALGORITHM));
        System.out.println(IpUtils.getLocationByIP2("114.44.227.87", DbSearcher.BTREE_ALGORITHM));
        System.out.println(IpUtils.getLocationByIP2("119.44.227.87", DbSearcher.BTREE_ALGORITHM));
    }
}

ip地区对象IpAreaDTO

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

/**
 * ip地区对象
 *
 * @Author kongzi
 * @Date 2022/8/4
 * @Version 1.0
 */
@JsonPropertyOrder({"country", "province", "city", "networkISP"})
public class IpAreaDTO {
    // ip
    private String ip;
    // 国家
    private String country;
    // 省
    private String province;
    // 市
    private String city;
    // 运营商
    private String networkISP;

    // getter、setter方法
}

如果 ip2region.db 没有被打包到jar包内,可以参考如下代码。

<build>
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<includes>
					<include>**/*.xml</include>
				</includes>
			</resource>
			<resource>
				<directory>src/main/resources</directory>
				<includes>
					<include>**.*</include>
					<include>**/*.*</include><!-- i18n能读取到 -->
					<include>**/*/*.*</include>
				</includes>
			</resource>
		</resources>
</build>

3、扩展

3.1 注意事项

ip2region 的并发使用

  1. 全部binding的各个search接口都不是线程安全的实现,不同线程可以通过创建不同的查询对象来使用,并发量很大的情况下,binary和b-tree算法可能会打开文件数过多的错误,请修改内核的最大允许打开文件数(fs.file-max=一个更高的值),或者使用持久化的memory算法。
  2. memorySearch接口,在发布对象前进行一次预查询(本质上是把ip2region.db文件加载到内存),可以安全用于多线程环境。

3.2 ip2region.db的生成

从1.8版本开始,ip2region开源了ip2region.db生成程序的java实现,提供了ant编译支持,编译后会得到以下提到的dbMaker-{version}.jar,对于需要研究生成程序的或者更改自定义生成配置的请参考${ip2region_root}/maker/java内的java源码。

从ip2region 1.2.2版本开始里面提交了一个dbMaker-{version}.jar的可以执行jar文件,用它来完成这个工作:

  1. 确保你安装好了java环境(不玩Java的童鞋就自己谷歌找找拉,临时用一用,几分钟的事情)
  2. cd到${ip2region_root}/maker/java,然后运行如下命令:
java -jar dbMaker-{version}.jar -src 文本数据文件 -region 地域csv文件 [-dst 生成的ip2region.db文件的目录]

# 文本数据文件:db文件的原始文本数据文件路径,自带的ip2region.db文件就是/data/ip.merge.txt生成而来的,你可以换成自己的或者更改/data/ip.merge.txt重新生成
# 地域csv文件:该文件目的是方便配置ip2region进行数据关系的存储,得到的数据包含一个city_id,这个直接使用/data/origin/global_region.csv文件即可
# ip2region.db文件的目录:是可选参数,没有指定的话会在当前目录生成一份./data/ip2region.db文件
  1. 获取生成的ip2region.db文件覆盖原来的ip2region.db文件即可
  2. 默认的ip2region.db文件生成命令:
cd ${ip2region_root}/java/
java -jar dbMaker-1.2.2.jar -src ./data/ip.merge.txt -region ./data/global_region.csv

# 会看到一大片的输出

3.3 其他环境ip2region安装

nodejs

npm install node-ip2region --save

nuget安装

Install-Package IP2Region

php composer

# 插件来自:https://github.com/zoujingli/ip2region
composer require zoujingli/ip2region

3.4 附录

ip2region.db文件:ip2region

1

评论区