Java - ip2region - 使用篇

Java - ip2region - 使用篇,第1张

Java - ip2region - 使用篇

本篇主要介绍 ip2region, ip2region 支持很多客户端,本次主要以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 blockheader 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

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/992305.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存