项目中需要根据用户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库中还包含运营商,这个也可以做到数据库中,这样后期也可以判断出该用户是通过哪个网络运营商接入的服务。