Java如何获取IP属地 ip2region failed to create searcher with x:java.io.FileNotFoundException:( 系统找不到指定的路径)

引言

小伙伴们最近在刷抖音、今日头条、快手等,在评论区或者主要,都能看到IP属地,如下图所示:

在这里插入图片描述

那么,这种是怎么实现的呢?在实现之前,我们先实现获取ip地址的代码

获取ip地址

我们可以通过以下方式,来获取ip完整地址,比如127.0.0.1等等,但在获取ip完整地址时:

  • 页面报出了这个错误:This application has no explicit mapping for /error, so you are seeing this as a fallback.

  • 服务器端报出这个错误:java.lang.IllegalStateException: No primary or single unique constructor found for interface org.springframework.http.server.ServerHttpRequest

如果想了解他们解决方案,可以参考这篇文章:https://blog.csdn.net/lvoelife/article/details/126101890

使用ServerHttpRequest获取ip完整地址:

如果你用WebFlux时,获取请求头的信息时使用ServerHttpRequest

实现的代码如下:

获取ip地址的源代码

  /**
     * @author zs
     * @datetime 2022/8/1:14:39
     * @desc 获取ip地址
     */
    public static String getIpAddressByServerHttpRequest(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ipAddress = headers.getFirst("X-Forwarded-For");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddress().getAddress().getHostAddress();
            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
                // 根据网卡取本机配置的IP
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    log.error("根据网卡获取本机配置的IP异常", e);
                }
            }
        }
        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.indexOf(",") > 0) {
            ipAddress = ipAddress.split(",")[0];
        }
        return ipAddress;
    }

接口调用源代码

package com.example.demo.controller;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static com.example.demo.IpAddress.getIpAddress;

/**
 * @author zs
 * @datetime 2022/7/22 10:14
 * @desc
 */
@RestController
@RequestMapping("/test")
public class IndexController {

  @GetMapping("/getIpAddress")
  public Object get(ServerHttpRequest request) {
    String ipAddress = getIpAddressByServerHttpRequest(request);
    return ipAddress;
  }
}

使用HttpServletRequest获取ip完整地址

如果你用MVC时(或WebFlux、MVC依赖同时存在时),获取http请求头的信息使用HttpServletRequest

实现的代码如下:

获取ip地址的源代码

  	/**
     * @author zs
     * @datetime 2022/8/1:14:39
     * @desc 获取ip地址
     */
   public static String getIpAddressByHttpServletRequest(HttpServletRequest request) {
    String ipAddress = request.getHeader("X-Forwarded-For");
    if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
      ipAddress = request.getHeader("Proxy-Client-IP");
    }
    if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
      ipAddress = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
      ipAddress = request.getRemoteAddr();
      if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
        // 根据网卡取本机配置的IP
        try {
          InetAddress inet = InetAddress.getLocalHost();
          ipAddress = inet.getHostAddress();
        } catch (UnknownHostException e) {
          log.error("根据网卡获取本机配置的IP异常", e);
        }
      }
    }

    // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
    if (ipAddress != null && ipAddress.indexOf(",") > 0) {
      ipAddress = ipAddress.split(",")[0];
    }

    return ipAddress;
  }

接口调用源代码

package com.example.demo.controller;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static com.example.demo.IpAddress.getIpAddress;

/**
 * @author zs
 * @datetime 2022/7/22 10:14
 * @desc
 */
@RestController
@RequestMapping("/test")
public class IndexController {

  @GetMapping("/getIpAddress")
 public Object get(HttpServletRequest request) {
    String ipAddress = IpAddress.getIpAddressByHttpServletRequest(request);
    return ipAddress;
  }
}

分析请求头

X-Forwarded-For

一个 HTTP 扩展头部,主要是为了让 Web 服务器获取访问用户的真实 IP 地址。

每个 IP 地址,每个值通过逗号+空格分开,最左边是最原始客户端的 IP 地址,中间如果有多层代理,每⼀层代理会将连接它的客户端 IP 追加在 X-Forwarded-For 右边。

X-Real-IP

一般只记录真实发出请求的客户端IP。

Proxy-Client-IP

这个一般是经过 Apache http 服务器的请求才会有,用 Apache http 做代理时一般会加上 Proxy-Client-IP 请求头。

WL-Proxy-Client-IP

也是通过 Apache http 服务器,在 weblogic 插件加上的头。

分析方法

request.getRemoteAddr()

这种方法在大部分情况下都是有效的。

但是在通过了Apache,Squid等反向代理软件就不能获取到客户端的真实IP地址了。

如果使用了反向代理软件,用request.getRemoteAddr()方法获取的IP地址是:127.0.0.1或192.168.1.110或公网IP,而并不是客户端的真实IP。

获取ip属地

我们通过以上方法获取了ip完整地址后,该如何使用ip完整地址获取ip属地呢?

在浏览Gitee时,发现了Ip2region 这个库,我们可以使用Ip2region 来获取ip属地。

Ip2region是什么

ip2region - 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现。

Ip2region的特性

标准化的数据格式

每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP

只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,后前的选项全部是0。

数据去重和压缩

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

极速查询响应

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

  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。

IP 数据管理框架

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

内置的三种查询算法

之所以全部的查询客户端单次查询都在 0.x 毫秒级别,是因为内置了三种查询算法:

  1. memory 算法:整个数据库全部载入内存,单次查询都在0.1x毫秒内,C语言的客户端单次查询在0.00x毫秒级别。

  2. binary 算法:基于二分查找,基于ip2region.db文件,不需要载入内存,单次查询在0.x毫秒级别。

  3. b-tree 算法:基于btree算法,基于ip2region.db文件,不需要载入内存,单词查询在0.x毫秒级别,比binary算法更快。

使用ip2region

安装依赖

 <dependency>
    <groupId>org.lionsoul</groupId>
         <artifactId>ip2region</artifactId>
     <version>2.6.5</version>
 </dependency>

下载 ip2region.xdb文件

我们可以登录ip2region的源代码,找到目录 ip2region/ data / ip2region.xdb,点击下载源文件,如下图所示:

在这里插入图片描述

配置ip2region.xdb文件

我们把下载好的ip2region.xdb文件,放置在resources目录下,如下图所示:

配置ip2region.xdb文件

实现代码

完全基于文件查询

 /**
   * @author zs
   * @datetime 2022/8/1:17:05
   * @param ipAddress ip地址,比如127.0.0.1
   * @desc 获取ip属地
   */
  public static String getIpBelong(String ipAddress) {
    // 1、 获取文件地址
    String dbPath = "./src/main/resources/ip2region.xdb";

    // 2、 创建 searcher 对象
    Searcher searcher = null;
    try {
      searcher = Searcher.newWithFileOnly(dbPath);
    } catch (IOException e) {
      System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
      return null;
    }

    // 3、查询地址
    String region = null;
    try {
      long sTime = System.nanoTime();
      region = searcher.search(ipAddress);
      long cost = TimeUnit.NANOSECONDS.toMicros((System.nanoTime() - sTime));
      System.out.printf(
          "输出日志 = {ip属地: %s, io次数: %d, 耗时: %d μs}\n", region, searcher.getIOCount(), cost);
    } catch (Exception e) {
      System.out.printf("failed to search(%s): %s\n", ipAddress, e);
    }

    // 4、关闭资源
    try {
      searcher.close();
    } catch (IOException e) {
      e.printStackTrace();
    }

    return region;
  }
重要提醒

并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。

使用缓存 VectorIndex 索引查询

我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。

 /**
   * @author zs
   * @datetime 2022/8/1:17:15
   * @param ipAddress ip地址,比如127.0.0.1
   * @desc 使用缓存 VectorIndex 索引
   */
  public static String vectorIndexCache(String ipAddress) {
    // 1、获取文件地址
    String dbPath = "./src/main/resources/ip2region.xdb";

    // 2、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
    byte[] vIndex;
    try {
      vIndex = Searcher.loadVectorIndexFromFile(dbPath);
    } catch (Exception e) {
      System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
      return null;
    }

    // 3、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
    Searcher searcher;
    try {
      searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
    } catch (Exception e) {
      System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
      return null;
    }

    // 4、查询
    String region = null;
    try {
      long sTime = System.nanoTime();
      region = searcher.search(ipAddress);
      long cost = TimeUnit.NANOSECONDS.toMicros((System.nanoTime() - sTime));
      System.out.printf(
          "输出日志 = {ip属地: %s, io次数: %d, 耗时: %d μs}\n", region, searcher.getIOCount(), cost);
    } catch (Exception e) {
      System.out.printf("failed to search(%s): %s\n", ipAddress, e);
    }

    // 5、关闭资源
    try {
      searcher.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return region;
  }
重要提醒
  • 每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。

使用缓存整个 xdb 数据


 /**
   * @author zs
   * @datetime 2022/8/1:17:15
   * @param ipAddress ip地址,比如127.0.0.1
   * @desc 使用缓存整个 xdb 数据
   */
  public static String cacheXdb(String ipAddress) {
    // 1、获取文件地址
    String dbPath = "./src/main/resources/ip2region.xdb";

    // 2、从 dbPath 加载整个 xdb 到内存。
    byte[] cBuff;
    try {
      cBuff = Searcher.loadContentFromFile(dbPath);
    } catch (Exception e) {
      System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
      return;
    }

    // 3、使用上述的 cBuff 创建一个完全基于内存的查询对象。
    Searcher searcher;
    try {
      searcher = Searcher.newWithBuffer(cBuff);
    } catch (Exception e) {
      System.out.printf("failed to create content cached searcher: %s\n", e);
      return null;
    }

    // 4、查询
    String region = null;
    try {
      long sTime = System.nanoTime();
      region = searcher.search(ipAddress);
      long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
      System.out.printf(
          "输出日志 = {ip属地: %s, io次数: %d, 耗时: %d μs}\n", region, searcher.getIOCount(), cost);
    } catch (Exception e) {
      System.out.printf("failed to search(%s): %s\n", ipAddress, e);
    }

    // 5、关闭资源
    // searcher.close();
    return region;
  }
重要提醒
  1. 关闭资源 - 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher。

  2. 并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。

获取具体属地

因为上述代码获取到的属地是原始属地,比如中国|0|上海|上海市|联通,因而,我们需要将其拆分,获取真正的属地,如下代码所示:

 /**
   * @author zs
   * @datetime 2022/8/1:17:31
   * @param cityInfo ip原始属地,比如:中国|0|上海|上海市|联通
   * @desc 将ip原始属地拆分,比如中国|0|上海|上海市|联通拆分成上海
   */
  public static String getIpPossession(String cityInfo) {
    if (null == cityInfo || cityInfo.equals("")) {
      return "未知";
    }
    cityInfo = cityInfo.replace("|", " ");
    String[] cityList = cityInfo.split(" ");
    if (cityList.length <= 0) {
      return "未知";
    }
    // 国内的显示到具体的省
    if ("中国".equals(cityList[0]) && cityList.length > 1) {
      return cityList[1];
    }
    // 国外显示到国家
    return cityList[0];
  }

测试代码

测试国内ip

@Test
  public void testGetIpBelong() {
    String ipBelong = getIpBelong("220.248.12.158");
    System.out.println("\n输出ip原始属地:" + ipBelong);
    System.out.println("输出ip属地:" + getIpPossession(ipBelong));
  }

输出结果如下图:

在这里插入图片描述

测试国外ip

 @Test
  public void testGetIpBelong() {
    String ipBelong = getIpBelong("67.220.12.158");
    System.out.println("\n输出ip原始属地:" + ipBelong);
    System.out.println("输出ip属地:" + getIpPossession(ipBelong));
  }

输出结果如下图:

在这里插入图片描述

总结

源代码地址

因为代码时不断更新的,如果想要获取最新的maven,可以打开其源代码地址:https://gitee.com/lionsoul/ip2region

failed to create searcher: java.io.FileNotFoundException

复现问题

在获取文件地址时,报出了如下错误:

failed to create searcher with `./resources/ip2region.xdb`: 	
	java.io.FileNotFoundException: .\resources\ip2region.xdb (
		系统找不到指定的路径。
	)

分析问题

因为我使用文件的相对位置,即./resources/ip2region.xdb,但ip2region代码是从项目的根路径,去查找ip2region.xdb的文件的相对路径。

ip2region.xdb的完整路径是D:/project/demo/src/main/resources/ip2region.xdb,而我只写了./resources/ip2region.xdb,肯定找不到这个文件,就报出找不到文件异常。

如果我们不知道项目的位置,可以使用如下代码来查找,即:

 File file=new File("");
 try {
    System.out.println(file.getCanonicalPath());
  } catch (IOException e) {
    e.printStackTrace();
  }

输出结果是:D:\project\demo,便可以知道项目的根路径。

解决问题

既然知道了错误原因,便将文件的相对路径修改如下即可:

// 1. 获取文件地址
String dbPath = "./src/main/resources/ip2region.xdb";
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网全栈开发实战

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值