引言
小伙伴们最近在刷抖音、今日头条、快手等,在评论区或者主要,都能看到IP属地
,如下图所示:
那么,这种是怎么实现的呢?在实现之前,我们先实现获取ip地址的代码
获取ip地址
我们可以通过以下方式,来获取ip完整地址,比如127.0.0.1
等等,但在获取ip完整地址时:
-
页面报出了这个错误:
This application has no explicit mapping for /error, so you are seeing this as a fallback.
。 -
服务器端报出这个错误:
java.lang.IllegalStateException: No primary or single unique constructor found for interface org.springframework.http.server.ServerHttpRequest
如果想了解他们解决方案,可以参考这篇文章:https://blog.csdn.net/lvoelife/article/details/126101890
使用ServerHttpRequest获取ip完整地址:
如果你用WebFlux时
,获取请求头的信息时使用ServerHttpRequest
实现的代码如下:
获取ip地址的源代码
/**
* @author zs
* @datetime 2022/8/1:14:39
* @desc 获取ip地址
*/
public static String getIpAddressByServerHttpRequest(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
String ipAddress = headers.getFirst("X-Forwarded-For");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = headers.getFirst("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = headers.getFirst("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddress().getAddress().getHostAddress();
if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
// 根据网卡取本机配置的IP
try {
InetAddress inet = InetAddress.getLocalHost();
ipAddress = inet.getHostAddress();
} catch (UnknownHostException e) {
log.error("根据网卡获取本机配置的IP异常", e);
}
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.split(",")[0];
}
return ipAddress;
}
接口调用源代码
package com.example.demo.controller;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.example.demo.IpAddress.getIpAddress;
/**
* @author zs
* @datetime 2022/7/22 10:14
* @desc
*/
@RestController
@RequestMapping("/test")
public class IndexController {
@GetMapping("/getIpAddress")
public Object get(ServerHttpRequest request) {
String ipAddress = getIpAddressByServerHttpRequest(request);
return ipAddress;
}
}
使用HttpServletRequest获取ip完整地址
如果你用MVC时(或WebFlux、MVC依赖同时存在时)
,获取http请求头的信息使用HttpServletRequest
实现的代码如下:
获取ip地址的源代码
/**
* @author zs
* @datetime 2022/8/1:14:39
* @desc 获取ip地址
*/
public static String getIpAddressByHttpServletRequest(HttpServletRequest request) {
String ipAddress = request.getHeader("X-Forwarded-For");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
// 根据网卡取本机配置的IP
try {
InetAddress inet = InetAddress.getLocalHost();
ipAddress = inet.getHostAddress();
} catch (UnknownHostException e) {
log.error("根据网卡获取本机配置的IP异常", e);
}
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.split(",")[0];
}
return ipAddress;
}
接口调用源代码
package com.example.demo.controller;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.example.demo.IpAddress.getIpAddress;
/**
* @author zs
* @datetime 2022/7/22 10:14
* @desc
*/
@RestController
@RequestMapping("/test")
public class IndexController {
@GetMapping("/getIpAddress")
public Object get(HttpServletRequest request) {
String ipAddress = IpAddress.getIpAddressByHttpServletRequest(request);
return ipAddress;
}
}
分析请求头
X-Forwarded-For
一个 HTTP 扩展头部,主要是为了让 Web 服务器获取访问用户的真实 IP 地址。
每个 IP 地址,每个值通过逗号+空格分开,最左边是最原始客户端的 IP 地址,中间如果有多层代理,每⼀层代理会将连接它的客户端 IP 追加在 X-Forwarded-For 右边。
X-Real-IP
一般只记录真实发出请求的客户端IP。
Proxy-Client-IP
这个一般是经过 Apache http 服务器的请求才会有,用 Apache http 做代理时一般会加上 Proxy-Client-IP 请求头。
WL-Proxy-Client-IP
也是通过 Apache http 服务器,在 weblogic 插件加上的头。
分析方法
request.getRemoteAddr()
这种方法在大部分情况下都是有效的。
但是在通过了Apache,Squid等反向代理软件就不能获取到客户端的真实IP地址了。
如果使用了反向代理软件,用request.getRemoteAddr()
方法获取的IP地址是:127.0.0.1或192.168.1.110或公网IP
,而并不是客户端的真实IP。
获取ip属地
我们通过以上方法获取了ip完整地址
后,该如何使用ip完整地址
获取ip属地
呢?
在浏览Gitee时,发现了Ip2region
这个库,我们可以使用Ip2region
来获取ip属地。
Ip2region是什么
ip2region – 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现。
Ip2region的特性
标准化的数据格式
每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP
。
只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,后前的选项全部是0。
数据去重和压缩
xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。
极速查询响应
即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:
-
vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。
-
xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。
IP 数据管理框架
v2.0 格式的 xdb 支持亿级别的 IP 数据段行数,region 信息也可以完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。
内置的三种查询算法
之所以全部的查询客户端单次查询都在 0.x 毫秒级别,是因为内置了三种查询算法:
-
memory 算法:整个数据库全部载入内存,单次查询都在0.1x毫秒内,C语言的客户端单次查询在0.00x毫秒级别。
-
binary 算法:基于二分查找,基于ip2region.db文件,不需要载入内存,单次查询在0.x毫秒级别。
-
b-tree 算法:基于btree算法,基于ip2region.db文件,不需要载入内存,单词查询在0.x毫秒级别,比binary算法更快。
使用ip2region
安装依赖
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.5</version>
</dependency>
下载 ip2region.xdb文件
我们可以登录ip2region的源代码,找到目录 ip2region/ data / ip2region.xdb
,点击下载源文件,如下图所示:
配置ip2region.xdb文件
我们把下载好的ip2region.xdb
文件,放置在resources
目录下,如下图所示:
实现代码
完全基于文件查询
/**
* @author zs
* @datetime 2022/8/1:17:05
* @param ipAddress ip地址,比如127.0.0.1
* @desc 获取ip属地
*/
public static String getIpBelong(String ipAddress) {
// 1、 获取文件地址
String dbPath = "./src/main/resources/ip2region.xdb";
// 2、 创建 searcher 对象
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 null;
}
// 3、查询地址
String region = null;
try {
long sTime = System.nanoTime();
region = searcher.search(ipAddress);
long cost = TimeUnit.NANOSECONDS.toMicros((System.nanoTime() - sTime));
System.out.printf(
"输出日志 = {ip属地: %s, io次数: %d, 耗时: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ipAddress, e);
}
// 4、关闭资源
try {
searcher.close();
} catch (IOException e) {
e.printStackTrace();
}
return region;
}
重要提醒
并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
使用缓存 VectorIndex 索引查询
我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。
/**
* @author zs
* @datetime 2022/8/1:17:15
* @param ipAddress ip地址,比如127.0.0.1
* @desc 使用缓存 VectorIndex 索引
*/
public static String vectorIndexCache(String ipAddress) {
// 1、获取文件地址
String dbPath = "./src/main/resources/ip2region.xdb";
// 2、从 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 null;
}
// 3、使用全局的 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 null;
}
// 4、查询
String region = null;
try {
long sTime = System.nanoTime();
region = searcher.search(ipAddress);
long cost = TimeUnit.NANOSECONDS.toMicros((System.nanoTime() - sTime));
System.out.printf(
"输出日志 = {ip属地: %s, io次数: %d, 耗时: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ipAddress, e);
}
// 5、关闭资源
try {
searcher.close();
} catch (IOException e) {
e.printStackTrace();
}
return region;
}
重要提醒
- 每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
使用缓存整个 xdb 数据
/**
* @author zs
* @datetime 2022/8/1:17:15
* @param ipAddress ip地址,比如127.0.0.1
* @desc 使用缓存整个 xdb 数据
*/
public static String cacheXdb(String ipAddress) {
// 1、获取文件地址
String dbPath = "./src/main/resources/ip2region.xdb";
// 2、从 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;
}
// 3、使用上述的 cBuff 创建一个完全基于内存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
System.out.printf("failed to create content cached searcher: %s\n", e);
return null;
}
// 4、查询
String region = null;
try {
long sTime = System.nanoTime();
region = searcher.search(ipAddress);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf(
"输出日志 = {ip属地: %s, io次数: %d, 耗时: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ipAddress, e);
}
// 5、关闭资源
// searcher.close();
return region;
}
重要提醒
-
关闭资源 – 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher。
-
并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
获取具体属地
因为上述代码获取到的属地是原始属地,比如中国|0|上海|上海市|联通
,因而,我们需要将其拆分,获取真正的属地,如下代码所示:
/**
* @author zs
* @datetime 2022/8/1:17:31
* @param cityInfo ip原始属地,比如:中国|0|上海|上海市|联通
* @desc 将ip原始属地拆分,比如中国|0|上海|上海市|联通拆分成上海
*/
public static String getIpPossession(String cityInfo) {
if (null == cityInfo || cityInfo.equals("")) {
return "未知";
}
cityInfo = cityInfo.replace("|", " ");
String[] cityList = cityInfo.split(" ");
if (cityList.length <= 0) {
return "未知";
}
// 国内的显示到具体的省
if ("中国".equals(cityList[0]) && cityList.length > 1) {
return cityList[1];
}
// 国外显示到国家
return cityList[0];
}
测试代码
测试国内ip
@Test
public void testGetIpBelong() {
String ipBelong = getIpBelong("220.248.12.158");
System.out.println("\n输出ip原始属地:" + ipBelong);
System.out.println("输出ip属地:" + getIpPossession(ipBelong));
}
输出结果如下图:
测试国外ip
@Test
public void testGetIpBelong() {
String ipBelong = getIpBelong("67.220.12.158");
System.out.println("\n输出ip原始属地:" + ipBelong);
System.out.println("输出ip属地:" + getIpPossession(ipBelong));
}
输出结果如下图:
总结
源代码地址
因为代码时不断更新的,如果想要获取最新的maven,可以打开其源代码地址:https://gitee.com/lionsoul/ip2region
failed to create searcher: java.io.FileNotFoundException
复现问题
在获取文件地址时,报出了如下错误:
failed to create searcher with `./resources/ip2region.xdb`:
java.io.FileNotFoundException: .\resources\ip2region.xdb (
系统找不到指定的路径。
)
分析问题
因为我使用文件的相对位置,即./resources/ip2region.xdb
,但ip2region
代码是从项目的根路径,去查找ip2region.xdb
的文件的相对路径。
ip2region.xdb
的完整路径是D:/project/demo/src/main/resources/ip2region.xdb
,而我只写了./resources/ip2region.xdb
,肯定找不到这个文件,就报出找不到文件异常。
如果我们不知道项目的位置,可以使用如下代码来查找,即:
File file=new File("");
try {
System.out.println(file.getCanonicalPath());
} catch (IOException e) {
e.printStackTrace();
}
输出结果是:D:\project\demo
,便可以知道项目的根路径。
解决问题
既然知道了错误原因,便将文件的相对路径修改如下即可:
// 1. 获取文件地址
String dbPath = "./src/main/resources/ip2region.xdb";
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/99193.html