原文:http://blog.csdn.net/chwshuang/article/details/78027873?locationNum=10&fps=1php
项目上接到一个需求,按照用户IP地址判断用户省份、城市,来展现不一样的内容。在网上进行选型的时候,有几个选择java
GeoIP2 GeoLite2开源免费的数据库数据库
MaxMind做为一家私营企业,总部设于美国马萨诸塞州的沃尔瑟姆。MaxMind公司成立于2002年,是领先业界的IP智能与在线欺诈检测工具供应商。有兴趣的能够访问官方网站了解。数组
这个IP库的特色是免费,全球支持比较好,国外的IP应该比较全,国内的IP地址获取率不高,获取后的准确率也不高,公司网站上使用过一段时间,我统计过,国内地址获取率80%左右,而这80%里与淘宝的IP进行对比的准确率只有60%~80%,因此,总体的成功率只有65%如下,因此上线一段时间就没有用了。缓存
淘宝IP库安全
淘宝IP库只能经过Http方式查询IP,没有提供本地库的方式,遇到实时处理系统,确定是不行,解决方案是建一个缓存和一个队列,若是缓存中没有的IP,就放到队列,而后用一个线程单独去队列中把要查询地址的IP经过Http的方式获取。markdown
淘宝IP库的特色是准确,官方宣称的省级准确率达到99.14%。缺点就是只提供线上的Rest API,并且线上的Rest API有请求限制,若是你写个程序一直请求,每一个请求返回的间隔是30秒!因此不适合实时的场景用。网络
纯真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库文件,下面再说如何使用
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; } } }
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); } } }
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[