Java - ip2region - 使用篇

Java - ip2region - 使用篇

本篇主要介绍 ip2regionip2region 支持很多客户端,本次主要以Java来介绍

在进行系统开发时,我们一般会涉及到获取到用户的具体位置信息,一般有两个方法:

  • 根据GPS 定位的信息 (一般用于手机端)
  • 用户的 IP 地址解析

每个手机都不一定会打开 GPS,而且有时并不太需要太精确的位置(到城市这个级别即可),所以根据 IP 地址入手来分析用户位置是个不错的选择。

下面就介绍一个分析 IP 地址一个比较好的东西 ip2region

接着上篇 Java - ip2region - 基础篇(你知道ip2region吗?),这篇主要介绍使用与一些原理

ip2region使用

1、导包

使用首先导包,从1.8版本开始,ip2region开源了ip2region.db生成程序的java实现,下面涉及到的源码以2.5.6版本说明

<dependency>
    <groupId>net.dreamlu</groupId>
    <artifactId>mica-ip2region</artifactId>
    <version>2.5.6</version>
</dependency>

2、简单使用

这里举个例子来简单使用:

  • 写了一个 ipaddress 接口,请求接口返回位置信息
  • ipaddress 接口 用了 getIP() 方法获取请求的ip字符串地址
  • 根据 memorySearch()(memory算法)获取位置信息,返回
@RestController
public class UserController {
    private static final String UNKNOWN = "unknown";
    @Autowired
    Ip2regionSearcher ip2regionSearcher; // 核心处理类,具体为什么可以直接 @Autowired,下面原理来说

    @GetMapping("/ipaddress")
    public ResponseEntity<Object> ipTest(HttpServletRequest request) throws IOException {
        String ip = getIp(request); // 获取到请求的ip地址
        IpInfo ipInfo = ip2regionSearcher.memorySearch(ip); // 根据ip地址获取到address
        String address = UNKNOWN;
        assert ipInfo != null;
        if(ipInfo != null){
            address = ipInfo.getAddress();
        }
        return new ResponseEntity<>(address, HttpStatus.OK);
    }
    /**
     * 根据请求获取ip地址
     */
    private static String getIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        String comma = ",";
        String localhost = "127.0.0.1";
        if (ip.contains(comma)) {
            ip = ip.split(",")[0];
        }
        if (localhost.equals(ip)) {
            // 获取本机真正的ip地址
            try {
                ip = InetAddress.getLocalHost().getHostAddress();
            } catch (UnknownHostException e) {
            }
        }
        return ip;
    }
}

请求返回:

请添加图片描述

3、方法与原理介绍

总体介绍图:(这个包很少东西,而且通俗易懂)
请添加图片描述

说明:

  • ip2region.db:基础篇介绍的ip2region.db文件
  • Ip2regionConfiguration:配置类(可直接@Autowired的主要原因)
  • Ip2regionProperties:配置文件读取类
  • DBxxxx主要涉及与 ip2region.db文件相关的处理/配置类
    • DataBlock
      DbConfig
      DbMakerConfigException
      DBReader
      DbSearcher
  • IndexBlock:对应 ip2region.db 中的 indexBlock 的结构,起始ip 终止ip 数据信息(数据地址与数据长度)
  • Ip2regionSearcher / Ip2regionSearcherImpl:核心接口与实现类,里面的核心方法为主要业务处理方法
  • IpInfo:获取到的ip信息,格式为: 城市ip,国家,区域,省份,城市,运营商
方法介绍

Ip2regionSearcher / Ip2regionSearcherImpl:核心接口与实现类,里面的核心方法为主要业务处理方法

  • net.dreamlu.mica.ip2region.core.Ip2regionSearcher
package net.dreamlu.mica.ip2region.core;
import net.dreamlu.mica.ip2region.utils.IpInfoUtil;
import org.springframework.lang.Nullable;
import java.util.function.Function;
/**
 * ip 搜索器
 *
 * @author dream.lu
 */
public interface Ip2regionSearcher {
   /**
    * ip 位置 搜索(memory 算法)
    * @param ip ip
    * @return 位置
    */
   @Nullable
   IpInfo memorySearch(long ip);

   /**
    * ip 位置 搜索(memory 算法)
    *
    * @param ip ip
    * @return 位置
    */
   @Nullable
   IpInfo memorySearch(String ip);



   /**
    * ip 位置 搜索(btree搜索 算法)
    *
    * @param ip ip
    * @return 位置
    */
   @Nullable
   IpInfo btreeSearch(long ip);

   /**
    * ip 位置 搜索(btree搜索 算法)
    *
    * @param ip ip
    * @return 位置
    */
   @Nullable
   IpInfo btreeSearch(String ip);

   /**
    * ip 位置 搜索(binary 搜索 算法)
    *
    * @param ip ip
    * @return 位置
    */
   @Nullable
   IpInfo binarySearch(long ip);

   /**
    * ip 位置 搜索(binary 搜索 算法)
    *
    * @param ip ip
    * @return 位置
    */
   @Nullable
   IpInfo binarySearch(String ip);

    /**
    * ip 位置 搜索
    *
    * @param ptr ptr
    * @return 位置
    */
   @Nullable
   IpInfo getByIndexPtr(long ptr);
    
   /**
    * 读取 ipInfo 中的信息
    * @param ip       ip
    * @param function Function
    * @return 地址
    */
   @Nullable
   default String getInfo(long ip, Function<IpInfo, String> function) {
      return IpInfoUtil.readInfo(memorySearch(ip), function);
   }
   /**
    * 读取 ipInfo 中的信息
    *
    * @param ip       ip
    * @param function Function
    * @return 地址
    */
   @Nullable
   default String getInfo(String ip, Function<IpInfo, String> function) {
      return IpInfoUtil.readInfo(memorySearch(ip), function);
   }
   /**
    * 获取地址信息
    *
    * @param ip ip
    * @return 地址
    */
   @Nullable
   default String getAddress(long ip) {
      return getInfo(ip, IpInfo::getAddress);
   }
   /**
    * 获取地址信息
    *
    * @param ip ip
    * @return 地址
    */
   @Nullable
   default String getAddress(String ip) {
      return getInfo(ip, IpInfo::getAddress);
   }
   /**
    * 获取地址信息包含 isp
    *
    * @param ip ip
    * @return 地址
    */
   @Nullable
   default String getAddressAndIsp(long ip) {
      return getInfo(ip, IpInfo::getAddressAndIsp);
   }
   /**
    * 获取地址信息包含 isp
    *
    * @param ip ip
    * @return 地址
    */
   @Nullable
   default String getAddressAndIsp(String ip) {
      return getInfo(ip, IpInfo::getAddressAndIsp);
   }
}

说明:

  • 分别提供了三大搜索算法的两种重载方法,区别就在于ip地址是自己转换为长整型的还是它来转长整型,返回为 IpInfo
  • 提供一个 自己来处理 IpInfo 的方法,getInfo() 根据自己传入的 Function 来处理得到的 IpInfo
  • 根据ip地址直接获取到地址信息 getAddress() / getAddressAndIsp()
原理介绍
1、为什么可以 @Autowired?

这里看到其实它写了一个配置类 Ip2regionConfiguration ,利用 @Configuration 注解,这样的话,SpringBoot 在启动的时候就会扫描注入,而Ip2regionConfiguration中其注入了一个Bean,默认Bean名称为 ip2regionSearcher ,即我们就可以可以根据 @Autowired 来注入Bean

/**
 * ip2region 自动化配置
 *
 * @author L.cm
 */
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(Ip2regionProperties.class)
@NativeHint(resources = @ResourceHint(patterns = "^ip2region/ip2region.db"))
public class Ip2regionConfiguration {

   @Bean
   public Ip2regionSearcher ip2regionSearcher(ResourceLoader resourceLoader,
                                    Ip2regionProperties properties) {
      return new Ip2regionSearcherImpl(resourceLoader, properties);
   }

}
2、算法实现源码

基础篇介绍了其内置了三种查询算法,基于二分查找的binary算法、基于btree算法的b-tree算法、与binary算法类似的memory算法(整个数据库全部载入内存)

三种方法的本质 都是通过不同的方法先找到 index,然后根据二分查找找到 ip地址 处于的 index bolck(12个字节),根据后四个字节存储的数据信息找到数据,然后根据数据信息中的前三个字节找到对应的数据地址

binary搜索

基于二分查询方法,步骤如下:

  • 把 ip值 通过 ip2long 方法转为长整型
  • 通过 SUPER BLOCK 拿到 INDEX 的起始位置和结束位置
  • 两个位置相减,然后 +1 得出index block 总数
  • 采用二分法直接求解,比较 index block 和当前 ip 的大小,即可找到该ip属于的 index block(经典二分)
  • 拿到该 index block 的后面四个字节(数据信息), 分别得到数据长度和数据地址
  • 从数据地址读取拿到的所得长度的字节,即是搜索结果

源码:(基本上源码就是根据上面的步骤编写的,很容易看懂)

  • net.dreamlu.mica.ip2region.core.DbSearcher#binarySearch
public DataBlock binarySearch(long ip) throws IOException {
   int blen = IndexBlock.getIndexBlockLength();
   if (totalIndexBlocks == 0) {
      byte[] superBytes = new byte[8]; // super (memeory 这里是根据整个dbStr获取的,即这点区别)
      reader.readFully(0L, superBytes, 0, superBytes.length);
      firstIndexPtr = Ip2regionUtil.getIntLong(superBytes, 0);
      lastIndexPtr = Ip2regionUtil.getIntLong(superBytes, 4);
       // 1、根据index 获取到 index block 个数
      totalIndexBlocks = (int) ((lastIndexPtr - firstIndexPtr) / blen) + 1;
   }
   //2、搜索ip地址在的index block(典型二分)
   int l = 0, h = totalIndexBlocks;
   byte[] buffer = new byte[blen];
   long sip, eip, dataptr = 0;
   while (l <= h) {
      int m = (l + h) >> 1;
      reader.readFully(firstIndexPtr + m * blen, buffer, 0, buffer.length);
      sip = Ip2regionUtil.getIntLong(buffer, 0);
      if (ip < sip) {
         h = m - 1;
      } else {
         eip = Ip2regionUtil.getIntLong(buffer, 4);
         if (ip > eip) {
            l = m + 1;
         } else {
            dataptr = Ip2regionUtil.getIntLong(buffer, 8);
            break;
         }
      }
   }
   if (dataptr == 0) {
      return null;
   }
   //4. 根据index block 获取数据
   int dataLen = (int) ((dataptr >> 24) & 0xFF);
   int dataPtr = (int) ((dataptr & 0x00FFFFFF));
   byte[] data = new byte[dataLen];
   reader.readFully(dataPtr, data, 0, data.length);
   int cityId = (int) Ip2regionUtil.getIntLong(data, 0);
   String region = new String(data, 4, data.length - 4, StandardCharsets.UTF_8);

   return new DataBlock(cityId, region, dataPtr);
}

b-tree 搜索

b-tree 搜索用到了 HEADER INDEX,第一步先在 HEADER INDEX 中搜索,再定位到 INDEX 中的某个 4k index分区搜索。

步骤:

  • 把 ip值 通过 ip2long 转为长整型
  • 使用二分法在 HEADER INDEX 中搜索,比较得到对应的 header index block
  • header index block 指向 INDEX 中的一个 4K 分区,所以直接把搜索范围降低到 4K
  • 采用二分法在获取到的 4K 分区搜索,得到对应的 index block
  • 拿到该 index block 的后面四个字节(数据信息), 分别得到数据长度和数据地址
  • 从数据地址读取拿到的所得长度的字节,即是搜索结果

源码:(基本上源码就是根据上面的步骤编写的,很容易看懂)

  • net.dreamlu.mica.ip2region.core.DbSearcher#btreeSearch
// btreeSearch 算法
public DataBlock btreeSearch(long ip) throws IOException {
   //1、找到 HEADER(index的二级索引)
   if (HeaderSip == null) {
      byte[] b = new byte[8 * 1024];
      reader.readFully(8L, b, 0, b.length);
      int len = b.length >> 3, idx = 0;  //b.lenght / 8
      long[] headerSip = new long[len];
      int[] headerPtr = new int[len];
      long startIp, dataPtr;
      for (int i = 0; i < b.length; i += 8) {
         startIp = Ip2regionUtil.getIntLong(b, i);
         dataPtr = Ip2regionUtil.getIntLong(b, i + 4);
         if (dataPtr == 0) {
            break;
         }
         headerSip[idx] = startIp;
         headerPtr[idx] = (int) dataPtr;
         idx++;
      }
      headerLength = idx;
      this.HeaderPtr = headerPtr;
      this.HeaderSip = headerSip;
   }
   
   if (ip == HeaderSip[0]) {
      return getByIndexPtr(HeaderPtr[0]);
   } else if (ip == HeaderSip[headerLength - 1]) {
      return getByIndexPtr(HeaderPtr[headerLength - 1]);
   }
   //2、根据 Header 找到 index
   int l = 0, h = headerLength, sptr = 0, eptr = 0;
   while (l <= h) {
      int m = (l + h) >> 1;
      //perfetc matched, just return it
      if (ip == HeaderSip[m]) {
         if (m > 0) {
            sptr = HeaderPtr[m - 1];
            eptr = HeaderPtr[m];
         } else {
            sptr = HeaderPtr[m];
            eptr = HeaderPtr[m + 1];
         }
         break;
      }
      //less then the middle value
      if (ip < HeaderSip[m]) {
         if (m == 0) {
            sptr = HeaderPtr[m];
            eptr = HeaderPtr[m + 1];
            break;
         } else if (ip > HeaderSip[m - 1]) {
            sptr = HeaderPtr[m - 1];
            eptr = HeaderPtr[m];
            break;
         }
         h = m - 1;
      } else {
         if (m == headerLength - 1) {
            sptr = HeaderPtr[m - 1];
            eptr = HeaderPtr[m];
            break;
         } else if (ip <= HeaderSip[m + 1]) {
            sptr = HeaderPtr[m];
            eptr = HeaderPtr[m + 1];
            break;
         }
         l = m + 1;
      }
   }
   if (sptr == 0) {
      return null;
   }
   //3、典型的二分查找,根据index 找到 index block
   int blockLen = eptr - sptr, blen = IndexBlock.getIndexBlockLength();
   byte[] iBuffer = new byte[blockLen + blen];
   reader.readFully(sptr, iBuffer, 0, iBuffer.length);
   l = 0;
   h = blockLen / blen;
   long sip, eip, dataptr = 0;
   while (l <= h) {
      int m = (l + h) >> 1;
      int p = m * blen;
      sip = Ip2regionUtil.getIntLong(iBuffer, p);
      if (ip < sip) {
         h = m - 1;
      } else {
         eip = Ip2regionUtil.getIntLong(iBuffer, p + 4);
         if (ip > eip) {
            l = m + 1;
         } else {
            dataptr = Ip2regionUtil.getIntLong(iBuffer, p + 8);
            break;
         }
      }
   }
   if (dataptr == 0) {
      return null;
   }

   //4. 根据index block 获取数据
   int dataLen = (int) ((dataptr >> 24) & 0xFF);
   int dataPtr = (int) ((dataptr & 0x00FFFFFF));
   byte[] data = new byte[dataLen];
   reader.readFully(dataPtr, data, 0, data.length);
   int cityId = (int) Ip2regionUtil.getIntLong(data, 0);
   String region = new String(data, 4, data.length - 4, StandardCharsets.UTF_8);
   return new DataBlock(cityId, region, dataPtr);
}

memory算法

该方法和 binary搜索 方法类似,区别就是它将 ip2region.db 全部读进内存中再进行查找

相关链接

GitHub ip2region

  • 4
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值