Java使用纯真IP库获取IP对应省份和城市

原文:http://blog.csdn.net/chwshuang/article/details/78027873?locationNum=10&fps=1php

Java使用纯真IP库获取IP对应省份和城市

项目上接到一个需求,按照用户IP地址判断用户省份、城市,来展现不一样的内容。在网上进行选型的时候,有几个选择java

开源免费的IP库选型


  1. GeoIP2 GeoLite2开源免费的数据库数据库

    MaxMind做为一家私营企业,总部设于美国马萨诸塞州的沃尔瑟姆。MaxMind公司成立于2002年,是领先业界的IP智能与在线欺诈检测工具供应商。有兴趣的能够访问官方网站了解。数组

    这个IP库的特色是免费,全球支持比较好,国外的IP应该比较全,国内的IP地址获取率不高,获取后的准确率也不高,公司网站上使用过一段时间,我统计过,国内地址获取率80%左右,而这80%里与淘宝的IP进行对比的准确率只有60%~80%,因此,总体的成功率只有65%如下,因此上线一段时间就没有用了。缓存

    这里写图片描述

  2. 淘宝IP库安全

    淘宝IP库只能经过Http方式查询IP,没有提供本地库的方式,遇到实时处理系统,确定是不行,解决方案是建一个缓存和一个队列,若是缓存中没有的IP,就放到队列,而后用一个线程单独去队列中把要查询地址的IP经过Http的方式获取。markdown

    淘宝IP库的特色是准确,官方宣称的省级准确率达到99.14%。缺点就是只提供线上的Rest API,并且线上的Rest API有请求限制,若是你写个程序一直请求,每一个请求返回的间隔是30秒!因此不适合实时的场景用。网络

    这里写图片描述

  3. 纯真IP库多线程

    纯真IP库是国人开源的一个IP库,支持多语言的,它的格式是公开的,因此,你能够将它用在目前主流的开发语言的项目中。纯真IP库的获取率是99.99%,除非是本地内网IP,好比以192.168开头的一些IP会不认,而这个是没有影响的。而淘宝IP库比较有意思的是会返回本地IP这个地区描述。纯真库还有一个最大的好处,就是它在按期更新,且比较频繁,最新更新时间是几天前2017年9月15号。使用过程当中库更新也比较方便,直接更新项目上的库文件,而后重启服务便可。固然,你也可让程序手动或者自动定时触发更新。并发

    纯真IP库须要在Window的操做系统上安装程序,而后在安装目录(默认是 ?\cz88.net\ip\)找到qqwry.dat这个文件, 这个就是压缩后的IP本地库

    这里写图片描述



.

纯真IP库使用


上面纯真IP介绍里已经说过怎么获取IP库文件,下面再说如何使用

  1. 建立一个本地对象
public class IPLocation { /** * 国家 */ private String country; /** * 区域 - 省份 + 城市 */ private String area; public IPLocation() { country = area = ""; } public synchronized IPLocation getCopy() { IPLocation ret = new IPLocation(); ret.country = country; ret.area = area; return ret; } public String getCountry() { return country; } public String getCity() { String city = ""; if(country != null){ String[] array = country.split("省"); if(array != null && array.length > 1){ city = array[1]; } else { city = country; } if(city.length() > 3){ city.replace("内蒙古", ""); } } return city; } public void setCountry(String country) { this.country = country; } public String getArea() { return area; } public void setArea(String area) { //若是为局域网,纯真IP地址库的地区会显示CZ88.NET,这里把它去掉 if(area.trim().equals("CZ88.NET")){ this.area="本机或本网络"; }else{ this.area = area; } } } 

 

 

  1. 建立工具类
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.util.StringTokenizer; /** * 工具类,提供IP字符串转数组的方法 */ public class Util { private static final Logger log = LoggerFactory.getLogger(CZIPUtils.class); private static StringBuilder sb = new StringBuilder(); /** * 从ip的字符串形式获得字节数组形式 * * @param ip 字符串形式的ip * @return 字节数组形式的ip */ public static byte[] getIpByteArrayFromString(String ip) { byte[] ret = new byte[4]; StringTokenizer st = new StringTokenizer(ip, "."); try { ret[0] = (byte) (Integer.parseInt(st.nextToken()) & 0xFF); ret[1] = (byte) (Integer.parseInt(st.nextToken()) & 0xFF); ret[2] = (byte) (Integer.parseInt(st.nextToken()) & 0xFF); ret[3] = (byte) (Integer.parseInt(st.nextToken()) & 0xFF); } catch (Exception e) { log.error("从ip的字符串形式获得字节数组形式报错" + e.getMessage(), e); } return ret; } /** * 字节数组IP转String * @param ip ip的字节数组形式 * @return 字符串形式的ip */ public static String getIpStringFromBytes(byte[] ip) { sb.delete(0, sb.length()); sb.append(ip[0] & 0xFF); sb.append('.'); sb.append(ip[1] & 0xFF); sb.append('.'); sb.append(ip[2] & 0xFF); sb.append('.'); sb.append(ip[3] & 0xFF); return sb.toString(); } /** * 根据某种编码方式将字节数组转换成字符串 * * @param b 字节数组 * @param offset 要转换的起始位置 * @param len 要转换的长度 * @param encoding 编码方式 * @return 若是encoding不支持,返回一个缺省编码的字符串 */ public static String getString(byte[] b, int offset, int len, String encoding) { try { return new String(b, offset, len, encoding); } catch (UnsupportedEncodingException e) { return new String(b, offset, len); } } } 

 

  1. 建立工具类
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * IP地址服务 */ public class IPAddressUtils { private static Logger log = LoggerFactory.getLogger(IPAddressUtils.class); /** * 纯真IP数据库名 */ private String IP_FILE="qqwry.dat"; /** * 纯真IP数据库保存的文件夹 */ private String INSTALL_DIR="/test/"; /** * 常量,好比记录长度等等 */ private static final int IP_RECORD_LENGTH = 7; /** * 常量,读取模式1 */ private static final byte REDIRECT_MODE_1 = 0x01; /** * 常量,读取模式2 */ private static final byte REDIRECT_MODE_2 = 0x02; /** * 缓存,查询IP时首先查询缓存,以减小没必要要的重复查找 */ private Map<String, IPLocation> ipCache; /** * 随机文件访问类 */ private RandomAccessFile ipFile; /** * 内存映射文件 */ private MappedByteBuffer mbb; /** * 起始地区的开始和结束的绝对偏移 */ private long ipBegin, ipEnd; /** * 为提升效率而采用的临时变量 */ private IPLocation loc; /** * 为提升效率而采用的临时变量 */ private byte[] buf; /** * 为提升效率而采用的临时变量 */ private byte[] b4; /** * 为提升效率而采用的临时变量 */ private byte[] b3; /** * IP地址库文件错误 */ private static final String BAD_IP_FILE = "IP地址库文件错误"; /** * 未知国家 */ private static final String UNKNOWN_COUNTRY = "未知国家"; /** * 未知地区 */ private static final String UNKNOWN_AREA = "未知地区"; public void init() { try { // 缓存必定要用ConcurrentHashMap, 避免多线程下获取为空 ipCache = new ConcurrentHashMap<>(); loc = new IPLocation(); buf = new byte[100]; b4 = new byte[4]; b3 = new byte[3]; try { ipFile = new RandomAccessFile(IP_FILE, "r"); } catch (FileNotFoundException e) { // 若是找不到这个文件,再尝试再当前目录下搜索,此次所有改用小写文件名 // 由于有些系统可能区分大小写致使找不到ip地址信息文件 String filename = new File(IP_FILE).getName().toLowerCase(); File[] files = new File(INSTALL_DIR).listFiles(); for(int i = 0; i < files.length; i++) { if(files[i].isFile()) { if(files[i].getName().toLowerCase().equals(filename)) { try { ipFile = new RandomAccessFile(files[i], "r"); } catch (FileNotFoundException e1) { log.error("IP地址信息文件没有找到,IP显示功能将没法使用:{}" + e1.getMessage(), e1); ipFile = null; } break; } } } } // 若是打开文件成功,读取文件头信息 if(ipFile != null) { try { ipBegin = readLong4(0); ipEnd = readLong4(4); if(ipBegin == -1 || ipEnd == -1) { ipFile.close(); ipFile = null; } } catch (IOException e) { log.error("IP地址信息文件格式有错误,IP显示功能将没法使用"+ e.getMessage(), e); ipFile = null; } } } catch (Exception e) { log.error("IP地址服务初始化异常:" + e.getMessage(), e); } } /** * 查询IP地址位置 - synchronized的做用是避免多线程时获取区域信息为空 * @param ip * @return */ public synchronized IPLocation getIPLocation(final String ip) { IPLocation location = new IPLocation(); location.setArea(this.getArea(ip)); location.setCountry(this.getCountry(ip)); return location; } /** * 从内存映射文件的offset位置开始的3个字节读取一个int * @param offset * @return */ private int readInt3(int offset) { mbb.position(offset); return mbb.getInt() & 0x00FFFFFF; } /** * 从内存映射文件的当前位置开始的3个字节读取一个int * @return */ private int readInt3() { return mbb.getInt() & 0x00FFFFFF; } /** * 根据IP获得国家名 * @param ip ip的字节数组形式 * @return 国家名字符串 */ public String getCountry(byte[] ip) { // 检查ip地址文件是否正常 if(ipFile == null) return BAD_IP_FILE; // 保存ip,转换ip字节数组为字符串形式 String ipStr = Util.getIpStringFromBytes(ip); // 先检查cache中是否已经包含有这个ip的结果,没有再搜索文件 if(ipCache.containsKey(ipStr)) { IPLocation ipLoc = ipCache.get(ipStr); return ipLoc.getCountry(); } else { IPLocation ipLoc = getIPLocation(ip); ipCache.put(ipStr, ipLoc.getCopy()); return ipLoc.getCountry(); } } /** * 根据IP获得国家名 * @param ip IP的字符串形式 * @return 国家名字符串 */ public String getCountry(String ip) { return getCountry(Util.getIpByteArrayFromString(ip)); } /** * 根据IP获得地区名 * @param ip ip的字节数组形式 * @return 地区名字符串 */ public String getArea(final byte[] ip) { // 检查ip地址文件是否正常 if(ipFile == null) return BAD_IP_FILE; // 保存ip,转换ip字节数组为字符串形式 String ipStr = Util.getIpStringFromBytes(ip); // 先检查cache中是否已经包含有这个ip的结果,没有再搜索文件 if(ipCache.containsKey(ipStr)) { IPLocation ipLoc = ipCache.get(ipStr); return ipLoc.getArea(); } else { IPLocation ipLoc = getIPLocation(ip); ipCache.put(ipStr, ipLoc.getCopy()); return ipLoc.getArea(); } } /** * 根据IP获得地区名 * @param ip IP的字符串形式 * @return 地区名字符串 */ public String getArea(final String ip) { return getArea(Util.getIpByteArrayFromString(ip)); } /** * 根据ip搜索ip信息文件,获得IPLocation结构,所搜索的ip参数从类成员ip中获得 * @param ip 要查询的IP * @return IPLocation结构 */ private IPLocation getIPLocation(final byte[] ip) { IPLocation info = null; long offset = locateIP(ip); if(offset != -1) info = getIPLocation(offset); if(info == null) { info = new IPLocation(); info.setCountry ( UNKNOWN_COUNTRY); info.setArea(UNKNOWN_AREA); } return info; } /** * 从offset位置读取4个字节为一个long,由于java为big-endian格式,因此没办法 * 用了这么一个函数来作转换 * @param offset * @return 读取的long值,返回-1表示读取文件失败 */ private long readLong4(long offset) { long ret = 0; try { ipFile.seek(offset); ret |= (ipFile.readByte() & 0xFF); ret |= ((ipFile.readByte() << 8) & 0xFF00); ret |= ((ipFile.readByte() << 16) & 0xFF0000); ret |= ((ipFile.readByte() << 24) & 0xFF000000); return ret; } catch (IOException e) { return -1; } } /** * 从offset位置读取3个字节为一个long,由于java为big-endian格式,因此没办法 * 用了这么一个函数来作转换 * @param offset 整数的起始偏移 * @return 读取的long值,返回-1表示读取文件失败 */ private long readLong3(long offset) { long ret = 0; try { ipFile.seek(offset); ipFile.readFully(b3); ret |= (b3[0] & 0xFF); ret |= ((b3[1] << 8) & 0xFF00); ret |= ((b3[2] << 16) & 0xFF0000); return ret; } catch (IOException e) { return -1; } } /** * 从当前位置读取3个字节转换成long * @return 读取的long值,返回-1表示读取文件失败 */ private long readLong3() { long ret = 0; try { ipFile.readFully(b3); ret |= (b3[0] & 0xFF); ret |= ((b3[1] << 8) & 0xFF00); ret |= ((b3[2] << 16) & 0xFF0000); return ret; } catch (IOException e) { return -1; } } /** * 从offset位置读取四个字节的ip地址放入ip数组中,读取后的ip为big-endian格式,可是 * 文件中是little-endian形式,将会进行转换 * @param offset * @param ip */ private void readIP(long offset, byte[] ip) { try { ipFile.seek(offset); ipFile.readFully(ip); byte temp = ip[0]; ip[0] = ip[3]; ip[3] = temp; temp = ip[1]; ip[1] = ip[2]; ip[2] = temp; } catch (IOException e) { log.error(e.getMessage(), e); } } /** * 从offset位置读取四个字节的ip地址放入ip数组中,读取后的ip为big-endian格式,可是 * 文件中是little-endian形式,将会进行转换 * @param offset * @param ip */ private void readIP(int offset, byte[] ip) { mbb.position(offset); mbb.get(ip); byte temp = ip[0]; ip[0] = ip[3]; ip[3] = temp; temp = ip[1]; ip[1] = ip[2]; ip[2] = temp; } /** * 把类成员ip和beginIp比较,注意这个beginIp是big-endian的 * @param ip 要查询的IP * @param beginIp 和被查询IP相比较的IP * @return 相等返回0,ip大于beginIp则返回1,小于返回-1。 */ private int compareIP(byte[] ip, byte[] beginIp) { for(int i = 0; i < 4; i++) { int r = compareByte(ip[i], beginIp[i]); if(r != 0) return r; } return 0; } /** * 把两个byte看成无符号数进行比较 * @param b1 * @param b2 * @return 若b1大于b2则返回1,相等返回0,小于返回-1 */ private int compareByte(byte b1, byte b2) { if((b1 & 0xFF) > (b2 & 0xFF)) // 比较是否大于 return 1; else if((b1 ^ b2) == 0)// 判断是否相等 return 0; else return -1; } /** * 这个方法将根据ip的内容,定位到包含这个ip国家地区的记录处,返回一个绝对偏移 * 方法使用二分法查找。 * @param ip 要查询的IP * @return 若是找到了,返回结束IP的偏移,若是没有找到,返回-1 */ private long locateIP(byte[] ip) { long m = 0; int r; // 比较第一个ip项 readIP(ipBegin, b4); r = compareIP(ip, b4); if(r == 0) return ipBegin; else if(r < 0) return -1; // 开始二分搜索 for(long i = ipBegin, j = ipEnd; i < j; ) { m = getMiddleOffset(i, j); readIP(m, b4); r = compareIP(ip, b4); // log.debug(Utils.getIpStringFromBytes(b)); if(r > 0) i = m; else if(r < 0) { if(m == j) { j -= IP_RECORD_LENGTH; m = j; } else j = m; } else return readLong3(m + 4); } // 若是循环结束了,那么i和j一定是相等的,这个记录为最可能的记录,可是并不是 // 确定就是,还要检查一下,若是是,就返回结束地址区的绝对偏移 m = readLong3(m + 4); readIP(m, b4); r = compareIP(ip, b4); if(r <= 0) return m; else return -1; } /** * 获得begin偏移和end偏移中间位置记录的偏移 * @param begin * @param end * @return */ private long getMiddleOffset(long begin, long end) { long records = (end - begin) / IP_RECORD_LENGTH; records >>= 1; if(records == 0) records = 1; return begin + records * IP_RECORD_LENGTH; } /** * 给定一个ip国家地区记录的偏移,返回一个IPLocation结构 * @param offset 国家记录的起始偏移 * @return IPLocation对象 */ private IPLocation getIPLocation(long offset) { try { // 跳过4字节ip ipFile.seek(offset + 4); // 读取第一个字节判断是否标志字节 byte b = ipFile.readByte(); if(b == REDIRECT_MODE_1) { // 读取国家偏移 long countryOffset = readLong3(); // 跳转至偏移处 ipFile.seek(countryOffset); // 再检查一次标志字节,由于这个时候这个地方仍然多是个重定向 b = ipFile.readByte(); if(b == REDIRECT_MODE_2) { loc.setCountry ( readString(readLong3())); ipFile.seek(countryOffset + 4); } else loc.setCountry ( readString(countryOffset)); // 读取地区标志 loc.setArea( readArea(ipFile.getFilePointer())); } else if(b == REDIRECT_MODE_2) { loc.setCountry ( readString(readLong3())); loc.setArea( readArea(offset + 8)); } else { loc.setCountry ( readString(ipFile.getFilePointer() - 1)); loc.setArea( readArea(ipFile.getFilePointer())); } return loc; } catch (IOException e) { return null; } } /** * 给定一个ip国家地区记录的偏移,返回一个IPLocation结构,此方法应用与内存映射文件方式 * @param offset 国家记录的起始偏移 * @return IPLocation对象 */ private IPLocation getIPLocation(int offset) { // 跳过4字节ip mbb.position(offset + 4); // 读取第一个字节判断是否标志字节 byte b = mbb.get(); if(b == REDIRECT_MODE_1) { // 读取国家偏移 int countryOffset = readInt3(); // 跳转至偏移处 mbb.position(countryOffset); // 再检查一次标志字节,由于这个时候这个地方仍然多是个重定向 b = mbb.get(); if(b == REDIRECT_MODE_2) { loc.setCountry ( readString(readInt3())); mbb.position(countryOffset + 4); } else loc.setCountry ( readString(countryOffset)); // 读取地区标志 loc.setArea(readArea(mbb.position())); } else if(b == REDIRECT_MODE_2) { loc.setCountry ( readString(readInt3())); loc.setArea(readArea(offset + 8)); } else { loc.setCountry ( readString(mbb.position() - 1)); loc.setArea(readArea(mbb.position())); } return loc; } /** * 从offset偏移开始解析后面的字节,读出一个地区名 * @param offset 地区记录的起始偏移 * @return 地区名字符串 * @throws IOException */ private String readArea(long offset) throws IOException { ipFile.seek(offset); byte b = ipFile.readByte(); if(b == REDIRECT_MODE_1 || b == REDIRECT_MODE_2) { long areaOffset = readLong3(offset + 1); if(areaOffset == 0) return UNKNOWN_AREA; else return readString(areaOffset); } else return readString(offset); } /** * @param offset 地区记录的起始偏移 * @return 地区名字符串 */ private String readArea(int offset) { mbb.position(offset); byte b = mbb.get(); if(b == REDIRECT_MODE_1 || b == REDIRECT_MODE_2) { int areaOffset = readInt3(); if(areaOffset == 0) return UNKNOWN_AREA; else return readString(areaOffset); } else return readString(offset); } /** * 从offset偏移处读取一个以0结束的字符串 * @param offset 字符串起始偏移 * @return 读取的字符串,出错返回空字符串 */ private String readString(long offset) { try { ipFile.seek(offset); int i; for(i = 0, buf[i] = ipFile.readByte(); buf[i] != 0; buf[++i] = ipFile.readByte()); if(i != 0) return Util.getString(buf, 0, i, "GBK"); } catch (IOException e) { log.error(e.getMessage(), e); } return ""; } /** * 从内存映射文件的offset位置获得一个0结尾字符串 * @param offset 字符串起始偏移 * @return 读取的字符串,出错返回空字符串 */ private String readString(int offset) { try { mbb.position(offset); int i; for(i = 0, buf[i] = mbb.get(); buf[i] != 0; buf[++i] = mbb.get()); if(i != 0) return Util.getString(buf, 0, i, "GBK"); } catch (IllegalArgumentException e) { log.error(e.getMessage(), e); } return ""; } public String getCity(final String ipAddress){ try { if(ipAddress.startsWith("192.168.")){ log.error("此IP[{}]段不进行处理!", ipAddress); return null; } return getIPLocation(ipAddress).getCity(); }catch (Exception e){ log.error("根据IP[{}]获取省份失败:{}", ipAddress, e.getMessage()); return null; } } public static void main(String[] args){ IPAddressUtils ip = new IPAddressUtils(); ip.init(); String address = "112.225.35.70"; System.out.println("IP地址["+address + "]获取到的区域信息:" + ip.getIPLocation(address).getCountry() + ", 获取到的城市:" + ip.getIPLocation(address).getCity() + ", 运营商:"+ip.getIPLocation(address).getArea()); } }

总结

其实我也是从网络上找的解析纯真库代码[http://blog.csdn.net/rockstar541/article/details/7161505] , 固然,个人代码是在他的基础上进行优化的,主要的在高并发的状况下,会出现获取城市为空的状况,因此在如下几个地方有改进:

  • (1)ipCache初始化的时候使用并发Map
ipCache = new HashMap<>(); 替换为 ipCache = new ConcurrentHashMap<>();
  • (2)获取IPLocation对象时进行同步处理,我尝试过将synchronized关键字加到更深或者更浅的方法上,在getIPLocation(String ip) 上加目前最安全,最高效
public IPLocation getIPLocation(String ip) { IPLocation location = new IPLocation(); location.setArea(this.getArea(ip)); location.setCountry(this.getCountry(ip)); return location; } 替换为 public synchronized IPLocation getIPLocation(final String ip) { IPLocation location = new IPLocation(); location.setArea(this.getArea(ip)); location.setCountry(this.getCountry(ip)); return location; }
相关文章
相关标签/搜索