细心的朋友们可能已经发现了,先在抖音、知乎、快手、小红书等这些平台已经上线了“网络用户显示 IP 的功能”,境外用户显示的是国家,国内的用户显示的省份,而且此项显示无法关闭,归属地强制显示。
首先我们来看一下,在 Java 中,是如何获取到 IP 属地的,主要有以下两步:
首先,我们这里写一个工具类用于获取 IP 地址,因为用户的每次 Request 请求都会携带请求的 IP 地址放到请求头中,所以我们可以通过截取请求中的 IP 来获取 IP 地址;
/**
?*?网络工具类
?*
?*?@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();
????}
}
通过这个方法,就可以重请求头中获取到用户的 IP 地址了,然后接下来就是 IP 地址归属地省份、城市的获取了,这里可以用很多 IP 地址查询的库进行查询,这里用一个库来测试一下。
淘宝 IP 地址库:ip.taobao.com/
不过淘宝的 IP 地址查询库已经在 2022 年 3 月 31 日下线了,这里我们就不能使用它了,只能另辟蹊径了。
这里我们截取一段之前淘宝货期 IP 地址的源码,然后一起来看一下。
这里可以看到,在日志文件中,出现了大量的 the request over max qps for user 问题。
虽然这个方法已经寄了,但是我们求知的道路可以停吗?肯定不可以啊,这里我们就来整一个新的获取 IP 地址属地的方法,也就是我们今天文章的主角:Ip2region。
这个开源库目前已经更新到了 V2 的版本,现在的它是一个强大的离线IP地址定位库和IP定位数据管理框架,其达到了微秒级别的查询效率,还提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,可以说是非常得好用????,今天这篇文章我们主要针对其 V2 版本进行讲解
数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真 IP 定位准确一些。
ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次),比例如下:
xdb 支持亿级别的 IP 数据段行数,默认的 region 信息都固定了格式:国家|区域|省份|城市|ISP,缺省的地域信息默认是0。
region 信息支持完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。
xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。
即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:
已经客户端有:Java、C#、php、C、Python、Node.js、PHP 拓展(PHP 5 和 PHP 7)等,主要如下:
这里简单展示一下 Java 的实现,这里使用开发中常用的 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 依赖之后,我们这里引入几种实现的方式:
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 对象单独使用。
????}
}
我们可以提前从 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 缓存。
????}
}
我们也可以预先加载整个 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 对象做成全局对象去跨线程访问。
????}
}
通过 maven 来编译测试程序。
#?cd?到?java?binding?的根目录
cd?binding/java/
mvn?compile?package
然后会在当前目录的 target 目录下得到一个?ip2region-{version}.jar?
的打包文件。
可以通过?java -jar ip2region-{version}.jar search
?命令来测试查询:
???java?git:(v2.0_xdb)???java?-jar?target/ip2region-2.6.0.jar?search
java?-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.xdb
ip2region?xdb?searcher?test?program,?cachePolicy:?vectorIndex
type?'quit'?to?exit
ip2region>>?1.2.3.4
{region:?美国|0|华盛顿|0|谷歌,?ioCount:?7,?took:?82?μs}
ip2region>>
输入 ip 即可进行查询测试,也可以分别设置 cache-policy 为?file/vectorIndex/content
?来测试三种不同缓存实现的查询效果。
可以通过?java -jar ip2region-{version}.jar bench
?命令来进行 bench 测试,一方面确保 xdb 文件没有错误,一方面可以评估查询性能:
???java?git:(v2.0_xdb)???java?-jar?target/ip2region-2.6.0.jar?bench
java?-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.txt
Bench?finished,?{cachePolicy:?vectorIndex,?total:?3417955,?took:?8s,?cost:?2?μs/op}
可以通过分别设置?cache-policy
?为?file/vectorIndex/content
?来测试三种不同缓存实现的效果。
@Note: 注意 bench 使用的 src 文件要是生成对应 xdb 文件相同的源文件。
最后说一句(求关注!别白嫖!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。