闽公网安备 35020302035485号
项目中需要根据用户IP地址获取到实际的省份,城市。虽然网络上有一些在线api接口(比如淘宝ip.taobao.com/),可以获取到实际的地址,且比较准确。但是会有QPS的限制,不能支持大量请求,且走了http请求返回相对会比较慢。
因此决定采用本地化方案,将IP端的解析放到本地中。再将匹配数据放到内存中,进而可以快速查找到IP对应的省份,城市。这个时候就需要用到一个比较牛逼的IP库了,IP2Region。这个库也会定时更新,我们可以自己编写程序定时同步库到本地即可。
-- 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段从小到大来进行的排列。public static long convertIPToLong(String ip) {
// 堆代码 duidaima.com
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 0 L;
} else {
byte[] bytes = IPAddr.getAddress();
if (bytes.length < 4) {
return 0 L;
} 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;
}
}
}
我们处理完成后,就可以得到一份地域的表。@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;
}
四. 页面显示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库中还包含运营商,这个也可以做到数据库中,这样后期也可以判断出该用户是通过哪个网络运营商接入的服务。