好用的IP解析库
我正在参加「掘金·启航计划」
项目中需要根据用户IP地址获取到实际的省份,城市。虽然网络上有一些在线api接口(比如淘宝ip.taobao.com/),可以获取到实际的地址,且比较准确。但是会有QPS的限制,不能支持大量请求,且走了http请求返回相对会比较慢。
1. 方案
因此决定采用本地化方案,将IP端的解析放到本地中。再将匹配数据放到内存中,进而可以快速查找到IP对应的省份,城市。这个时候就需要用到一个比较牛逼的IP库了,IP2Region。这个库也会定时更新,我们可以自己编写程序定时同步库到本地即可。
2. 接入方式
2.1. 急速查询
完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别。
- vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。
- xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。
缺点:只能查询具体的城市,如果需要进行城市过滤,就无法做到,有限制。
2.2. 自定义查询
针对上面继续查询的缺点,这个时候我们就需要把整个数据都解析匹配到数据库中。IP2Region 也提供了手动处理的方式。
可以基于 ip2region 自带的 ./data/ip.merge.txt 原始 IP 数据用 ip2region 提供的编辑工具来自己修改。
2.2.1. 解析数据入库
原始数据格式为国家|区域|省份|城市|ISP
,项目只需要用到国家,省份,城市这三个字段。定义地域表,表结构如下:
-- auto-generated definition
create table t_area
(
id int auto_increment
primary key,
name varchar(64) null comment '名称',
father_id int null comment '父一级的id',
level int null comment '1-国家 2-省份 3-城市'
)
comment '区域';
解析每一行数据到数据库中,针对其他国家,进行过滤,只匹配到国家一级即可。
/**
* 从文件中解析出本地库
*/
public void parseIpFromMergeTxt() {
ClassPathResource pathResource = new ClassPathResource("/ip.merge.txt");
Map<String, Area> areaMap = new HashMap<>();
// 处理等级为1的
Map<String, Area> levelOneMap = list(new LambdaQueryWrapper<Area>()
.eq(Area::getLevel, 1))
.stream().collect(Collectors.toMap(Area::getName, a -> a));
Map<String, Area> levelTwoMap = list(new LambdaQueryWrapper<Area>()
.eq(Area::getLevel, 2))
.stream().collect(Collectors.toMap(Area::getName, a -> a));
Map<String, Area> levelThreeMap = list(new LambdaQueryWrapper<Area>()
.eq(Area::getLevel, 3))
.stream().collect(Collectors.toMap(Area::getName, a -> a));
Path path = Paths.get("ip.new.txt");
// 按行读取文件
try {
BufferedWriter bw = Files.newBufferedWriter(path);
Files.lines(Paths.get(pathResource.getURI())).forEach(line -> {
String[] strArr = line.split("\|");
log.info("国家:{},省份:{},城市:{}",strArr[2],strArr[4],strArr[5]);
StringJoiner sj = new StringJoiner("|");
sj.add(String.valueOf(IPUtil.convertIPToLong(strArr[0])));
sj.add(String.valueOf(IPUtil.convertIPToLong(strArr[1])));
// 查询是否有该地域,没有则插入
// 不是中国,只记录国家
if ("中国".equals(strArr[2])) {
sj.add(String.valueOf(levelOneMap.get(strArr[2]).getId()));
sj.add(String.valueOf(levelTwoMap.get(strArr[4]).getId()));
sj.add(String.valueOf(levelThreeMap.get(strArr[5]).getId()));
} else {
sj.add(String.valueOf(levelOneMap.get(strArr[2]).getId()));
sj.add("");
sj.add("");
}
try {
bw.write(sj.toString());
bw.newLine();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
bw.flush();
} catch (Exception e) {
log.error("read ip file error => {}", e);
}
}
前提: IP库文件是按IP段从小到大来进行的排列。
存入数据库中的IP不能是IP段,这样后期查询的时候匹配非常慢,这个时候就需要把IP转化为long数值。转化方法:
public static long convertIPToLong(String ip) {
String subIP = null;
// 当ip地址有多条时,只取第一条
if (ip.contains(",")) {
String[] ips = ip.split(",");
subIP = ips[0].trim();
} else if (ip.contains(",")) {
String[] ips = ip.split(",");
subIP = ips[0].trim();
} else {
subIP = ip;
}
InetAddress IPAddr = null;
try {
IPAddr = InetAddress.getByName(subIP);
} catch (UnknownHostException e) {
logger.error("ip could not parse, please check the ip is spell correct: + [{}]", ip);
}
if (IPAddr == null) {
return 0L;
} else {
byte[] bytes = IPAddr.getAddress();
if (bytes.length < 4) {
return 0L;
} else {
long l0 = (long) (bytes[0] & 255);
long l1 = (long) (bytes[1] & 255);
long l2 = (long) (bytes[2] & 255);
long l3 = (long) (bytes[3] & 255);
return l0 << 24 | l1 << 16 | l2 << 8 | l3;
}
}
}
我们处理完成后,就可以得到一份地域的表。
3. 数据使用
基于上面的前提,我们已经把数据入库了,这个时候就需要把数据展示到页面上。根据level进行区分不同地域类型。这个时候我们就需要用到递归算法来将整个地域以tree的形式展示到页面上。前端使用的antd,所以tree的格式采用:
@Data
public class Tree {
private int key;
private String title;
private List<Tree> children;
}
整个树结构获取:
/**
* 获取中国地域列表
* @return
*/
public List<Tree> chinaAreaList() {
// 查询所有城市
List<Area> areaList = list(new LambdaQueryWrapper<Area>()
.ne(Area::getName, "0")
.orderByAsc(Area::getName)
);
// 从中国往下递归
return childrenTree(areaList, 235);
}
private List<Tree> childrenTree(List<Area> areaList, int fatherId) {
return areaList.stream()
.filter(a -> a.getFatherId() == fatherId)
.map(a -> areaToTree(a))
.peek(t -> t.setChildren(childrenTree(areaList, t.getKey())))
.collect(Collectors.toList());
}
/**
* 树对象转换
* @param a
* @return
*/
private Tree areaToTree(Area a) {
Tree tree = new Tree();
tree.setTitle(a.getName());
tree.setKey(a.getId());
return tree;
}
4. 页面显示
这个时候页面上就可以进行地域配置了,对请求进行地域包含或者排除操作。当用户请求过来时,获取用户IP
public static String parseIp(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();
}
return ip;
}
获取到IP后,通过上面的方法转换成long的数字。这个时候就需要跟加载到内存中的数据进行匹配。这个时候就需要用到二分查找进行匹配(基于数据都是排序好的)。
就可以非常快速的查找到对应IP是哪个城市哪个省份。也可以用于用户IP解析入库。IP库中还包含运营商,这个也可以做到数据库中,这样后期也可以判断出该用户是通过哪个网络运营商接入的服务。
5. 总结
自此,一个完整的IP解析库已经完成。也可以定期,半年或者一年去github上下载最新的ip库,更新我们自有库,达到更精准的控制。后续准备把这个独立成一个单独的微服务,供所有业务系统调用。
转载自:https://juejin.cn/post/7247027696822632507