作为网友,我们可能只是看看戏,但是作为一个努力学习的码农,我们肯定要来看一下这个功能是怎么实现的,本日这篇文章,就用几分钟的韶光来讲述一下这个功能是怎么实现的。

1获取用户 IP 地址HttpServletRequest 获取 IP

首先我们来看一下,在 Java 中,是如何获取到 IP 属地的,紧张有以下两步:

通过 HttpServletRequest 工具,获取用户的 「IP」 地址通过 IP 地址,获取对应的省份、城市]

首先,我们这里写一个工具类用于获取 IP 地址,由于用户的每次 Request 要求都会携带要求的 IP 地址放到要求头中,以是我们可以通过截取要求中的 IP 来获取 IP 地址;

归属地源码php全网上线 IP 归属地功效一文教你若何实现 CSS

/ 网络工具类 @author <a href="https://github.com/liyupi">程序员鱼皮</a> @from <a href="https://yupi.icu">编程导航知识星球</a> /public class NetUtils { / 获取客户端 IP 地址 @param request @return / public static String getIpAddress(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(); if (ip.equals("127.0.0.1")) { // 根据网卡取本机配置的 IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (Exception e) { e.printStackTrace(); } if (inet != null) { ip = inet.getHostAddress(); } } } // 多个代理的情形,第一个IP为客户端真实IP,多个IP按照','分割 if (ip != null && ip.length() > 15) { if (ip.indexOf(",") > 0) { ip = ip.substring(0, ip.indexOf(",")); } } // 本机访问 if ("localhost".equalsIgnoreCase(ip) || "127.0.0.1".equalsIgnoreCase(ip) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(ip)){ // 根据网卡取本机配置的IP InetAddress inet; try { inet = InetAddress.getLocalHost(); ip = inet.getHostAddress(); } catch (UnknownHostException e) { e.printStackTrace(); } } // 如果查找不到 IP,可以返回 127.0.0.1,可以做一定的处理,但是这里不考虑 // if (ip == null) { // return "127.0.0.1"; // } return ip; } / 获取mac地址 / public static String getMacAddress() throws Exception { // 取mac地址 byte[] macAddressBytes = NetworkInterface.getByInetAddress(InetAddress.getLocalHost()).getHardwareAddress(); // 下面代码是把mac地址拼装成String StringBuilder sb = new StringBuilder(); for (int i = 0; i < macAddressBytes.length; i++) { if (i != 0) { sb.append("-"); } // mac[i] & 0xFF 是为了把byte转化为正整数 String s = Integer.toHexString(macAddressBytes[i] & 0xFF); sb.append(s.length() == 1 ? 0 + s : s); } return sb.toString().trim().toUpperCase(); }}2获取用户的 IP 地址属地淘宝库获取用户 IP 地址属地

通过这个方法,就可以紧张求头中获取到用户的 IP 地址了,然后接下来便是 IP 地址归属地省份、城市的获取了,这里可以用很多 IP 地址查询的库进行查询,这里用一个库来测试一下。

淘宝 IP 地址库:ip.taobao.com/

不过淘宝的 IP 地址查询库已经在 2022 年 3 月 31 日下线了,这里我们就不能利用它了,只能另辟路子了。

这里我们截取一段之前淘宝货期 IP 地址的源码,然后一起来看一下。

这里可以看到,在日志文件中,涌现了大量的 the request over max qps for user 问题。

虽然这个方法已经寄了,但是我们求知的道路可以停吗?肯定不可以啊,这里我们就来整一个新的获取 IP 地址属地的方法,也便是我们本日文章的主角:Ip2region

Ip2region 先容

这个是在之前的一篇文章看到的,他是一个 Gthub 的开源项目,即 Ip2region 开源项目

地址如下:https://github.com/lionsoul2014/ip2region

这个开源库目前已经更新到了 V2 的版本,现在的它是一个强大的离线IP地址定位库和IP定位数据管理框架,其达到了微秒级别的查询效率,还供应了浩瀚主流编程措辞的 xdb 数据天生和查询客户端实现,可以说是非常得好用,本日这篇文章我们紧张针对其 V2 版本进行讲解,如果想要查询 1.0 版本的内容的话,可以去 Github 上面进行查看。

3Ip2region 详解高达 99.9 % 的查询准确率

数据聚合了一些有名 ip 到地名查询供应商的数据,这些是他们官方的的准确率,经测试其实比经典的纯洁 IP 定位准确一些。

ip2region 的数据聚合自以下做事商的开放 API 或者数据(升级程序每秒要求次数 2 到 4 次),比例如下:

80%, 淘宝 IP 地址库, ip.taobao.com/%5C≈10%, GeoIP, geoip.com/%5C≈2%, 纯洁 IP 库, www.cz88.net/%5CIp2region V2.0 特性1、IP 数据管理框架

xdb 支持亿级别的 IP 数据段行数,默认的 region 信息都固定了格式:国家|区域|省份|城市|ISP,缺省的地域信息默认是0。

region 信息支持完备自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。
也便是你完备可以利用 ip2region 来管理你自己的 IP 定位数据。

2、数据去重和压缩

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

3、极速查询相应

纵然是完备基于 xdb 文件的查询,单次查询相应韶光在十微秒级别,可通过如下两种办法开启内存加速查询:

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

已经客户端有:Java、C#、php、C、Python、Node.js、PHP 拓展(PHP 5 和 PHP 7)等,紧张如下:

Ip2region xdb Java 查询客户端实现

这里大略展示一下 Java 的实现,这里利用开拓中常用的 Maven 实现的办法:

1. 引入 Maven 仓库

由于项目利用Spring 的办法构建,这里可以选择利用引入 Spring 的 starter 的办法进行

<dependency> <groupId>com.github.hiwepy</groupId> <artifactId>ip2region-spring-boot-starter</artifactId> <version>2.0.1.RELEASE</version></dependency>

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

在引入 Maven 依赖之后,我们这里引入几种实现的办法:

2. 实现办法 1:【基于文件查询】

import org.lionsoul.ip2region.xdb.Searcher;import java.io.;import java.util.concurrent.TimeUnit;public class SearcherTest { public static void main(String[] args) { // 1、创建 searcher 工具 String dbPath = "ip2region.xdb file path"; 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; } // 2、查询 try { String ip = "1.2.3.4"; long sTime = System.nanoTime(); String region = searcher.search(ip); long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime)); System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost); } catch (Exception e) { System.out.printf("failed to search(%s): %s\n", ip, e); } // 3、备注:并发利用,每个线程须要创建一个独立的 searcher 工具单独利用。
}}
3. 实现办法 2:【缓存VectorIndex索引】

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

import org.lionsoul.ip2region.xdb.Searcher;import java.io.;import java.util.concurrent.TimeUnit;public class SearcherTest { public static void main(String[] args) { String dbPath = "ip2region.xdb file path"; // 1、从 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; } // 2、利用全局的 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; } // 3、查询 try { String ip = "1.2.3.4"; long sTime = System.nanoTime(); String region = searcher.search(ip); long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime)); System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost); } catch (Exception e) { System.out.printf("failed to search(%s): %s\n", ip, e); } // 备注:每个线程须要单独创建一个独立的 Searcher 工具,但是都共享全局的制度 vIndex 缓存。
}}
4. 实现办法 3:「缓存全体 xdb 数据」

我们也可以预先加载全体 ip2region.xdb 的数据到内存,然后基于这个数据创建查询工具来实现完备基于文件的查询,类似之前的 memory search。

import org.lionsoul.ip2region.xdb.Searcher;import java.io.;import java.util.concurrent.TimeUnit;public class SearcherTest { public static void main(String[] args) { String dbPath = "ip2region.xdb file path"; // 1、从 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; } // 2、利用上述的 cBuff 创建一个完备基于内存的查询工具。
Searcher searcher; try { searcher = Searcher.newWithBuffer(cBuff); } catch (Exception e) { System.out.printf("failed to create content cached searcher: %s\n", e); return; } // 3、查询 try { String ip = "1.2.3.4"; long sTime = System.nanoTime(); String region = searcher.search(ip); long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime)); System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost); } catch (Exception e) { System.out.printf("failed to search(%s): %s\n", ip, e); } // 备注:并发利用,用全体 xdb 数据缓存创建的查询工具可以安全的用于并发,也便是你可以把这个 searcher 工具做玉成局工具去跨线程访问。
}}
5. 编译测试程序

通过 maven 来编译测试程序。

# cd 到 java binding 的根目录cd binding/java/mvn compile package

然后会在当前目录的 target 目录下得到一个 ip2region-{version}.jar 的打包文件。

6. 查询测试

可以通过 java -jar ip2region-{version}.jar search 命令来测试查询:

➜ java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar searchjava -jar ip2region-{version}.jar search [command options]options: --db string ip2region binary xdb file path --cache-policy string cache policy: file/vectorIndex/content

例如:利用默认的 data/ip2region.xdb 文件进行查询测试:

➜ java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar search --db=../../data/ip2region.xdbip2region xdb searcher test program, cachePolicy: vectorIndextype 'quit' to exitip2region>> 1.2.3.4{region: 美国|0|华盛顿|0|谷歌, ioCount: 7, took: 82 μs}ip2region>>

输入 ip 即可进行查询测试,也可以分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。

4bench 测试

可以通过 java -jar ip2region-{version}.jar bench 命令来进行 bench 测试,一方面确保 xdb 文件没有缺点,一方面可以评估查询性能:

➜ java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar benchjava -jar ip2region-{version}.jar bench [command options]options: --db string ip2region binary xdb file path --src string source ip text file path --cache-policy string cache policy: file/vectorIndex/content

例如:通过默认的 data/ip2region.xdb 和 data/ip.merge.txt 文件进行 bench 测试:

➜ java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar bench --db=../../data/ip2region.xdb --src=../../data/ip.merge.txtBench finished, {cachePolicy: vectorIndex, total: 3417955, took: 8s, cost: 2 μs/op}

可以通过分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的效果。

@Note: 把稳 bench 利用的 src 文件假如天生对应 xdb 文件相同的源文件。