文章目录
前言
最近看到微博和抖音上线了IP定位功能,感觉挺有意思的。然后想了解一下是功能实现方式,也是这篇文档的初衷。
IP 定位服务很常见,而且很多公司都提供了类似的付费服务,比如阿里,高德,百度等,当然也有公开的免费服务,像 GeoIP等。这些服务要么通过 HTML 页面解析,要么通过接口请求,但不管怎样都离不开一次 http 请求,更不用说大部分服务都对 QPS 作了限制。下表枚举了一些常见的通过 IP 获取地址的方式。
开放API服务 | 方式 | 限制 | 样例 |
---|---|---|---|
淘宝IP地址库 | 接口 | 每个用户的QPS要小于1 | curl -d “ip=218.97.9.25&accessKey=alibaba-inc” http://ip.taobao.com/outGetIpInfo |
高德地图 | 接口 | 每个用户每天有10万的访问限制,企业开发者有3000万的访问限制 | https://restapi.amap.com/v3/ip?ip=218.97.9.25&key=f4cf14aca974dfbb0501c582ce3fce77 |
GeoIP | HTML解析 | 每个用户的QPS要小于1 | curl -d “ip=218.97.9.25&submit=提交” https://www.geoip.com |
在日常工作通常需要将大量用户请求日志中 IP 转换成用户位置信息,用作后续的分析。这其中的关键是数据量大,处理要快。显然每次都通过请求 API 公共服务的方式无法满足我们的日常需求。
一、Ip2region 简介
根据它获取一个具体ip的信息,通过IP解析出国家、具体地址、网络服务商等相关信息。
准确率99.9%的离线IP地址定位库,0.0x毫秒级查询,ip2region.db数据库只有数MB,提供了java、php、c、python、 nodejs、golang、c#等查询绑定和Binary,B树,内存三种查询算法。
Ip2region 为什么查询速度快?
全部的查询客户端单次查询都在0.x毫秒级别,内置了三种查询算法:
memory算法:整个数据库全部载入内存,单次查询都在0.1x毫秒内,C语言的客户端单次查询在0.00x毫秒级别。
binary算法:基于二分查找,基于ip2region.db文件,不需要载入内存,单次查询在0.x毫秒级别。
b-tree算法:基于btree算法,基于ip2region.db文件,不需要载入内存,单词查询在0.x毫秒级别,比binary算法更快。
任何客户端b-tree都比binary算法快,当然memory算法固然是最快的!
官网地址:
https://github.com/lionsoul2014/ip2region
二、Ip2region 实际应用:
在开发中,可以记录登陆的日志信息,关于登陆者的ip和位置信息,可以通过ip2region来实现。
三、项目集成Ip2region
1. pom.xml 引入
代码如下(示例):
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>1.7.2</version>
</dependency>
2.下载Ip2region.db
链接:https://pan.baidu.com/s/11SLxYfDnvH-3xVILa_Lutg
提取码:lxnr
将下载好的 Ip2region.db拷贝到 resources目录下
3.写工具类IPUtil
import java.io.File;
import java.lang.reflect.Method;
import org.apache.commons.lang3.StringUtils;
import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbSearcher;
import org.lionsoul.ip2region.Util;
import javax.servlet.http.HttpServletRequest;
public class IPUtil {
/**
* 获取城市信息
*
* @param ip ip
* @return
*/
public static String getCityInfo(String ip){
//db
String dbPath = IPUtil.class.getResource("/city/ip2region.db").getPath();
File file = new File(dbPath);
if ( file.exists() == false ) {
System.out.println("Error: Invalid ip2region.db file");
}
//查询算法
int algorithm = DbSearcher.BTREE_ALGORITHM; //B-tree
//DbSearcher.BINARY_ALGORITHM //Binary
//DbSearcher.MEMORY_ALGORITYM //Memory
try {
DbConfig config = new DbConfig();
DbSearcher searcher = new DbSearcher(config, dbPath);
//define the method
Method method = null;
switch ( algorithm )
{
case DbSearcher.BTREE_ALGORITHM:
method = searcher.getClass().getMethod("btreeSearch", String.class);
break;
case DbSearcher.BINARY_ALGORITHM:
method = searcher.getClass().getMethod("binarySearch", String.class);
break;
case DbSearcher.MEMORY_ALGORITYM:
method = searcher.getClass().getMethod("memorySearch", String.class);
break;
}
DataBlock dataBlock = null;
if ( Util.isIpAddress(ip) == false ) {
System.out.println("Error: Invalid ip address");
}
dataBlock = (DataBlock) method.invoke(searcher, ip);
return dataBlock.getRegion();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取客ip所在省份
*
* @param request 请求
* @return
*/
public static String getIpPossession(String ip) {
String cityInfo = getCityInfo(ip);
if (!StringUtils.isEmpty(cityInfo)) {
cityInfo = cityInfo.replace("|", " ");
String[] cityList = cityInfo.split(" ");
if (cityList.length > 0) {
// 国内的显示到具体的省
if ("中国".equals(cityList[0])) {
if (cityList.length > 1) {
return cityList[2];
}
}
// 国外显示到国家
return cityList[0];
}
}
return "未知";
}
/**
* 获取客户端ip
*
* @param request 请求
* @return
*/
public static String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
} else if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
} else if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
} else if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
} else if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
public static void main(String[] args) throws Exception{
System.err.println(getCityInfo("1.80.0.0"));
}
}
执行结果:
public static void main(String[] args) throws Exception{
System.err.println(getCityInfo("1.80.0.0"));
}
中国|0|陕西省|西安市|电信
public static void main(String[] args) throws Exception{
System.err.println(getIpPossession("1.80.0.0"));
}
陕西省
每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,后前的选项全部是0。
4.写一个 ipaddress 测试接口
import com.example.ipdemo.utils.IPUtil;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 控制器
*
* @author admin
* @email ${email}
* @date 2021-11-12 15:15:16
*/
@RestController
@RequestMapping("ipDemo")
@Log4j2
public class IpDemoController {
private String getRemortIP()throws Exception {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = "";
if (request.getHeader("x-forwarded-for") == null) {
ip = request.getRemoteAddr();
}else{
ip = request.getHeader("x-forwarded-for");
}
return ip;
}
@RequestMapping(value = "ipaddress", method = RequestMethod.GET)
@ResponseBody
public String testTime() {
String s = "";
try {
s = IPUtil.getCityInfo(getRemortIP());
} catch (Exception e) {
e.printStackTrace();
}
return s;
}
}
请求返回:
总结
ip2region 库解决了一个非常常见的 IP 定位问题,但将这个服务做到了又小又快(当然还提供了多语言的客户端),还是不可多得的。