在移动互联网的应用中,经常会涉及获取用户具体位置信息的功能。一般有两个方法:根据GPS 定位的信息 (一般用于手机端)和用户 IP 地址的解析。GPS定位需要手机打开 GPS 功能(用户不一定开启),且有时并不需要太精确的位置(到城市这个级别即可),所以根据 IP 地址解析用户位置是个不错的选择。ip解析需要一个 IP 和地理位置的映射关系库,并依赖这个库启动一个 IP 转地理位置的服务,ip2region 是一个不错的选择。
1、介绍
ip2region v2.0 - 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言(java, php, c, python, nodejs, golang, c#)的 xdb 数据生成和查询客户端实现。支持Binary,B树,内存三种查询算法,速度很快也不占内存。
特性如下:
1)标准化的数据格式
每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,后面的选项全部是0。
2)数据去重和压缩
xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。
3)极速查询响应
速度快,内置三种查询算法,支持毫秒级查询。即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:
- vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。
- xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。
在查询IP时有三种算法,分别是:
- memory算法:整个数据库全部载入内存,单次查询都在0.1x毫秒内,C语言的客户端单次查询在0.00x毫秒级别。
method = searcher.getClass().getMethod("memorySearch", String.class);
- binary算法:基于二分查找,基于ip2region.db文件,不需要载入内存,单次查询在0.x毫秒级别。
method = searcher.getClass().getMethod("binarySearch", String.class);
- b-tree算法:基于btree算法,基于ip2region.db文件,不需要载入内存,单词查询在0.x毫秒级别,比binary算法更快。
method = searcher.getClass().getMethod("btreeSearch", String.class);
任何客户端b-tree都比binary算法快,当然memory算法是最快的!
4)IP 数据管理框架
v2.0 格式的 xdb 支持亿级别的 IP 数据段行数,region 信息也可以完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。
5)准确率高
数据聚合多个供应商的数据。
- 淘宝IP地址库, http://ip.taobao.com/
- GeoIP, https://geoip.com/
- 纯真IP库, http://www.cz88.net/
6)支持多客户端
支持很多客户端,已经集成的客户端有:java、C#、php、c、python、nodejs、php扩展(php5和php7)、golang、rust、lua、lua_c, nginx。
2、Spring Boot集成ip2region
2.1 pom.xml引入库
<properties>
<ip2region.version>1.7.2</ip2region.version>
</properties>
<!-- ip2region -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>${ip2region.version}</version>
</dependency>
2.2 下载ip2region.db文件
方式一:直接下载
最新版本下载地址: https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region.db
V1.0下载地址: https://gitee.com/lionsoul/ip2region/blob/master/v1.0/data/ip2region.db
方式二:clone git项目
也可以通过clone git项目后在data文件夹下找到这个文件。
$ git clone https://gitee.com/lionsoul/ip2region.git
下载这个项目之后到data/文件夹下面可以找到ip2region.db文件。
2.3 ip2region.db文件添加到springboot项目
将下载的ip2region.db文件添加到springboot项目的resoures路径下。
2.4 ip2region 工具类
使用如下工具类可以方便的定位和解析ip地址。输出内容实例:|中国|华南|广东省|广州市|电信,分别代表国家、区域、省份、市和运营商。无数据区域默认为0。
- 注:如果项目打jar包,需要调用方法 getLocation(HttpServletRequest request) 或 getLocation(String ip)。同时需要将 ip2region.db 打包到jar包内。
import cn.hutool.core.io.IoUtil;
import com.founder.xhjz.dto.IpAreaDTO;
import org.apache.commons.lang3.StringUtils;
import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbSearcher;
import org.springframework.core.io.ClassPathResource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* IP解析工具类
*
* @Author kongzi
* @Date 2022/8/4
* @Version 1.0
*/
public class IpUtils {
private static final String UNKNOWN_ADDRESS = "未知";
private static final String SPLIT = "\\|";
private static final String CHN = "中国";
private static final String[] GATS = {"香港", "澳门", "台湾"};//港澳台
/**
* 根据IP获取地址
*
* @return IpAreaDTO
*/
public static IpAreaDTO getLocation(HttpServletRequest request) {
String ip = getIpAddress(request);
IpAreaDTO areaDTO = getLocation(ip);
return areaDTO;
}
/**
* 根据IP获取地址
*
* @return IpAreaDTO
*/
public static IpAreaDTO getLocation(String ip) {
IpAreaDTO areaDTO = new IpAreaDTO();
areaDTO.setIp(ip);
String area = null;
try {
area = getLocationByIPMemory(ip);
} catch (Exception e) {
e.printStackTrace();
}
if(StringUtils.isNotBlank(area)) {
String[] areas = area.split("\\|");
areaDTO.setCountry(areas[0]);
if(areas.length >= 3) {
areaDTO.setProvince(areas[1]);
if(areas.length == 3) {
areaDTO.setNetworkISP(areas[2]);
} else if(areas.length == 4) {
areaDTO.setCity(areas[2]);
areaDTO.setNetworkISP(areas[3]);
}
}
}
return areaDTO;
}
/**
* 根据IP获取地址
*
* @return 国家|区域|省份|城市|ISP
*/
public static String getLocationByIP(String ip) {
try {
// return getLocationByIP(ip, DbSearcher.BTREE_ALGORITHM);
return getLocationByIP(ip, DbSearcher.MEMORY_ALGORITYM);
} catch (Exception e) {
return null;
}
}
/**
* 根据IP获省份,国外则获取国家
*
* @return 国家|区域|省份|城市|ISP
*/
public static String getProvinceByIP(String ip) {
String location = getLocationByIP(ip);
if (StringUtils.isEmpty(location))
return UNKNOWN_ADDRESS;
String[] locations = location.split(SPLIT);
if (!CHN.equals(locations[0])) {
return locations[0];
} else if (locations.length > 1) {
String province = locations[1].replace("省", "");
if (Arrays.asList(GATS).contains(province)) {
return CHN + province;
}
return locations[1].replace("省", "");
}
return UNKNOWN_ADDRESS;
}
/**
* 根据IP获省份,国外则获取国家 - 从内存获取,适用于项目打jar包的情况
*
* @return 国家|区域|省份|城市|ISP
*/
public static String getLocationByIPMemory(String ip) {
DbSearcher searcher = null;
try {
// jar包内需要通过该方式获取
// 获取ip库路径
ClassPathResource classPathResource = new ClassPathResource("ip2region.db");
if (classPathResource.getClassLoader() == null){
System.out.println("存储路径发生错误,没有被发现");
return null;
}
InputStream inputStream = classPathResource.getInputStream();
byte[] bytes = IoUtil.readBytes(inputStream);
DbConfig dbConfig = new DbConfig();
searcher = new DbSearcher(dbConfig, bytes);
Method method = searcher.getClass().getMethod("memorySearch", String.class);
// address1--中国|0|北京|北京市|联通
String address = ((DataBlock) method.invoke(searcher, ip)).getRegion();
// address1--中国|北京|北京市|联通
address = address.replace("0|", "");
char symbol = '|';
if (address.charAt(address.length() - 1) == symbol) {
address = address.substring(0, address.length() - 1);
}
return address.equals("内网IP|内网IP") ? UNKNOWN_ADDRESS : address;
} catch (Exception e) {
e.printStackTrace();
}
return UNKNOWN_ADDRESS;
}
/**
* 根据IP获取地址 - jar包内需要通过该方式获取
*
* @param ip
* @param algorithm 查询算法
* @return 国家|区域|省份|城市|ISP
* @see DbSearcher DbSearcher.BTREE_ALGORITHM; //B-tree
* DbSearcher.BINARY_ALGORITHM //Binary DbSearcher.MEMORY_ALGORITYM
* //Memory
*/
public static String getLocationByIP(String ip, int algorithm) {
DbSearcher searcher = null;
try {
//
// String dbPath = IpUtils.class.getResource("/ip2region.db").getPath();
// searcher = new DbSearcher(new DbConfig(), dbPath);
// jar包内需要通过该方式获取
// 获取ip库路径
ClassPathResource classPathResource = new ClassPathResource("ip2region.db");
if (classPathResource.getClassLoader() == null){
System.out.println("存储路径发生错误,没有被发现");
return null;
}
InputStream inputStream = classPathResource.getInputStream();
byte[] bytes = IoUtil.readBytes(inputStream);
searcher = new DbSearcher(new DbConfig(), bytes);
DataBlock dataBlock;
switch (algorithm) {
case DbSearcher.BTREE_ALGORITHM:
dataBlock = searcher.btreeSearch(ip);
break;
case DbSearcher.BINARY_ALGORITHM:
dataBlock = searcher.binarySearch(ip);
break;
case DbSearcher.MEMORY_ALGORITYM:
dataBlock = searcher.memorySearch(ip);
break;
default:
return UNKNOWN_ADDRESS;
}
String address = dataBlock.getRegion().replace("0|", "");
char symbol = '|';
if (address.charAt(address.length() - 1) == symbol) {
address = address.substring(0, address.length() - 1);
}
return address.equals("内网IP|内网IP") ? UNKNOWN_ADDRESS : address;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (searcher != null) {
try {
searcher.close();
} catch (IOException ignored) {
ignored.printStackTrace();
}
}
}
return UNKNOWN_ADDRESS;
}
public static String getLocationByIP2(String ip, int algorithm) {
DbSearcher searcher = null;
try {
// 通过类路径指定
String dbPath = IpUtils.class.getResource("/ip2region.db").getPath();
searcher = new DbSearcher(new DbConfig(), dbPath);
DataBlock dataBlock;
switch (algorithm) {
case DbSearcher.BTREE_ALGORITHM:
dataBlock = searcher.btreeSearch(ip);
break;
case DbSearcher.BINARY_ALGORITHM:
dataBlock = searcher.binarySearch(ip);
break;
case DbSearcher.MEMORY_ALGORITYM:
dataBlock = searcher.memorySearch(ip);
break;
default:
return UNKNOWN_ADDRESS;
}
String address = dataBlock.getRegion().replace("0|", "");
char symbol = '|';
if (address.charAt(address.length() - 1) == symbol) {
address = address.substring(0, address.length() - 1);
}
return address.equals("内网IP|内网IP") ? UNKNOWN_ADDRESS : address;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (searcher != null) {
try {
searcher.close();
} catch (IOException ignored) {
ignored.printStackTrace();
}
}
}
return UNKNOWN_ADDRESS;
}
/**
* 通过request获取IP
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
// X-Forwarded-For:Squid 服务代理
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
// Proxy-Client-IP:apache 服务代理
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
// WL-Proxy-Client-IP:weblogic 服务代理
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
// HTTP_CLIENT_IP:有些代理服务器
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
// X-Real-IP:nginx服务代理
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} else if (ip.length() > 15) {
// 有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
String[] ips = ip.split(",");
for (int index = 0; index < ips.length; index++) {
String strIp = (String) ips[index];
if (!("unknown".equalsIgnoreCase(strIp))) {
ip = strIp;
break;
}
}
}
return ip;
}
public static void main(String[] args) {
System.out.println(IpUtils.getProvinceByIP("114.44.227.87"));
System.out.println(IpUtils.getLocationByIP2("127.0.0.87", DbSearcher.BTREE_ALGORITHM));
System.out.println(IpUtils.getLocationByIP2("114.44.227.87", DbSearcher.BTREE_ALGORITHM));
System.out.println(IpUtils.getLocationByIP2("119.44.227.87", DbSearcher.BTREE_ALGORITHM));
}
}
ip地区对象IpAreaDTO
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
/**
* ip地区对象
*
* @Author kongzi
* @Date 2022/8/4
* @Version 1.0
*/
@JsonPropertyOrder({"country", "province", "city", "networkISP"})
public class IpAreaDTO {
// ip
private String ip;
// 国家
private String country;
// 省
private String province;
// 市
private String city;
// 运营商
private String networkISP;
// getter、setter方法
}
如果 ip2region.db 没有被打包到jar包内,可以参考如下代码。
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**.*</include>
<include>**/*.*</include><!-- i18n能读取到 -->
<include>**/*/*.*</include>
</includes>
</resource>
</resources>
</build>
3、扩展
3.1 注意事项
ip2region 的并发使用
- 全部binding的各个search接口都不是线程安全的实现,不同线程可以通过创建不同的查询对象来使用,并发量很大的情况下,binary和b-tree算法可能会打开文件数过多的错误,请修改内核的最大允许打开文件数(fs.file-max=一个更高的值),或者使用持久化的memory算法。
- memorySearch接口,在发布对象前进行一次预查询(本质上是把ip2region.db文件加载到内存),可以安全用于多线程环境。
3.2 ip2region.db的生成
从1.8版本开始,ip2region开源了ip2region.db生成程序的java实现,提供了ant编译支持,编译后会得到以下提到的dbMaker-{version}.jar,对于需要研究生成程序的或者更改自定义生成配置的请参考${ip2region_root}/maker/java内的java源码。
从ip2region 1.2.2版本开始里面提交了一个dbMaker-{version}.jar的可以执行jar文件,用它来完成这个工作:
- 确保你安装好了java环境(不玩Java的童鞋就自己谷歌找找拉,临时用一用,几分钟的事情)
- cd到${ip2region_root}/maker/java,然后运行如下命令:
java -jar dbMaker-{version}.jar -src 文本数据文件 -region 地域csv文件 [-dst 生成的ip2region.db文件的目录]
# 文本数据文件:db文件的原始文本数据文件路径,自带的ip2region.db文件就是/data/ip.merge.txt生成而来的,你可以换成自己的或者更改/data/ip.merge.txt重新生成
# 地域csv文件:该文件目的是方便配置ip2region进行数据关系的存储,得到的数据包含一个city_id,这个直接使用/data/origin/global_region.csv文件即可
# ip2region.db文件的目录:是可选参数,没有指定的话会在当前目录生成一份./data/ip2region.db文件
- 获取生成的ip2region.db文件覆盖原来的ip2region.db文件即可
- 默认的ip2region.db文件生成命令:
cd ${ip2region_root}/java/
java -jar dbMaker-1.2.2.jar -src ./data/ip.merge.txt -region ./data/global_region.csv
# 会看到一大片的输出
3.3 其他环境ip2region安装
nodejs
npm install node-ip2region --save
nuget安装
Install-Package IP2Region
php composer
# 插件来自:https://github.com/zoujingli/ip2region
composer require zoujingli/ip2region
3.4 附录
ip2region.db文件:
评论区