一、项目介绍
1. 背景
IP地址是网络通信中的重要标识,通过分析IP地址的归属地信息,可以帮助我们了解访问来源、用户行为和网络安全等关键信息。例如应用于网站访问日志分析:通过分析访问日志中的IP地址,了解网站访问者的地理位置分布和访问行为,优化网站内容和用户体验。
2. 需求
IP分析,返回归属地信息,要求在毫秒内完成。
3. 涉及技术栈
Eclipse的使用,JavaSE中面向对象,IO流,二分法查找,集合。
4. 目的
通过IP归属地查询项目,巩固javaSE部分所学知识,增强实战能力。
需具备以下能力:1.面向对象程序设计。
2.工具类的封装与使用。
3. 文件IO流操作。
4. 字符串处理。
5. 二分法查找。
二、主要思路
1. 读取提供的地址库文件,解析地址库字符串,转换为结构化数据
2. 封装对应的数据结构,进行查询。
3. 封装对应的相关工具类。
4. 对外提供接口,只需要入参和出参(入参:IP地址,出参:归属地)。
三、代码开发
创建utils包,存放工具类
创建pojo包,存放实体类
创建manager包,存储管理类,业务代码
创建controller包,存储程序入口类
1. 文件读取,封装为工具类
编写文件读取工具类,用于读取本地IP地址库文件中的数据。当前类放于Utils包下。
package com.zh.utils;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
public class FileReadUtil {
// 默认编码方式
private static String defaultencoding = "UTF-8";
public static List<String> FileRead(String filePath, String encoding) throws IOException{
// 通过类加载器获取流,防止打包后无法获取IP文件
InputStream is = FileReadUtil.class.getClassLoader().getResourceAsStream(filePath);
// 字节流
// FileInputStream fis = new FileInputStream(filePath);
// 转字符流
Reader read = new InputStreamReader(is,encoding);
// 缓冲流
BufferedReader br = new BufferedReader(read);
String temp = null;
// 集合存储数据
List<String> list = new ArrayList<String>();
// 逐行读取
while((temp = br.readLine()) != null) {
list.add(temp);
}
br.close();
return list;
}
// 提供无参的方法,以默认编码方式为参数,调用有参的方法
public static List<String> FileRead(String filePath) throws IOException{
return FileRead(filePath,defaultencoding);
}
}
2. 抽象Pojo类,用于数据结构化
根据地址库中的IP地址及其归属地的存储格式(1.0.0.0 1.0.0.255 澳大利亚 亚太互联网络信息中心),创建实体类。实体类属性包括IP起始地址,IP结束地址,归属地,IP起始地址的long类型数值,IP结束地址的long类型数值。IP地址转换为long类型,用于后续对IP地址进行排序。
实体类实现Comparable接口,用于自定义比较规则,覆写comparaTo方法,根据起始IP地址进行比较。
实体类实现Serializable接口,可对当前对象进行序列化操作。
package com.zh.pojo;
import java.io.Serializable;
import com.zh.utils.IPUtil;
public class IPAndLocationPojo implements Comparable<IPAndLocationPojo>, Serializable {
private static final long serialVersionUID = 1L;
/**
* 起始IP
*/
private String startIP;
/**
* 结束IP
*/
private String endIP;
/**
* 归属地
*/
private String location;
/**
* 起始地址转long类型
*/
private long startIPLong;
/**
* 结束地址转long类型
*/
private long endIPLong;
public IPAndLocationPojo() {
super();
}
public IPAndLocationPojo(String startIP, String endIP, String location) {
super();
this.startIP = startIP;
this.endIP = endIP;
this.location = location;
this.startIPLong = IPUtil.ipToLong(startIP);
this.endIPLong = IPUtil.ipToLong(endIP);
}
public String getStartIP() {
return startIP;
}
public void setStartIP(String startIP) {
this.startIP = startIP;
}
public String getEndIP() {
return endIP;
}
public void setEndIP(String endIP) {
this.endIP = endIP;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public long getStartIPLong() {
return startIPLong;
}
public long getEndIPLong() {
return endIPLong;
}
@Override
public String toString() {
return "IPAndLocationPojo [startIP=" + startIP + ", endIP=" + endIP + ", location=" + location
+ ", startIPLong=" + startIPLong + ", endIPLong=" + endIPLong + "]";
}
@Override
public int compareTo(IPAndLocationPojo o) {
long result = this.startIPLong - o.getStartIPLong();
if(result > 0) {
return 1; // 正序
}else if(result < 0) {
return -1; // 倒序
}else {
return 0;
}
}
}
3. 结构化数据
调用文件读取工具类,返回一个集合,集合中的每一条数据,即为文件中的一行。
遍历集合,取出每一条数据,根据制表符(\t)分割,存储到数组中,此时数组中下标为0的位置存储的为IP起始地址的字符串,下标为1的位置存储的是IP结束地址的字符串,下标为2的位置存储的为归属地信息。
将数组中的数据封装为对象,后添加到ArrayList集合中,此时ArrayList中每一个元素即为一个IP归属地对象。
/**
* 结构化数据
* @param filePath
* @param encoding
* @return
* @throws IOException
*/
public static List<IPAndLocationPojo> getPojoList(String filePath, String encoding) throws IOException{
// 获取数据
List<String> list = FileReadUtil.FileRead(filePath, encoding);
// 封装结构化后的数据
List<IPAndLocationPojo> pojoList = new ArrayList<IPAndLocationPojo>();
// 逐条遍历取出数据
for(String string : list) {
// 跳过空行
if(string == null || string.trim().equals("")) {
continue;
}
// 分割字符串
String[] strs = string.split("\t");
// 跳过不合规的数组
if(strs.length != 3) {
continue;
}
String startIP = strs[0];
String endIP = strs[1];
String location = strs[2];
IPAndLocationPojo ipPojo = new IPAndLocationPojo(startIP, endIP, location);
pojoList.add(ipPojo);
}
return pojoList;
}
4. 集合转数组并排序
由于实体类中存储的起始IP和结束IP为字符串类型,默认排序规则不符合要求,所以需要提供工具类,将实体类中起始IP地址和结束IP地址的字符串转换为long类型,为实体类中添加long类型的起始IP和结束IP,并实现Comparable接口,覆写comparaTo方法来自定义比较规则。
工具类:
package com.zh.utils;
import java.util.Scanner;
/**
* Ip地址转换
*/
public class IPUtil {
/**
* ip地址字符串转long类型
*
* @param ipString
* @return
*/
public static long ipToLong(String ipString) {
String[] str = ipString.split("\\.");
return (Long.parseLong(str[0]) << 24)
+ (Long.parseLong(str[1]) << 16)
+ (Long.parseLong(str[2]) << 8)
+ Long.parseLong(str[3]);
}
/**
* long 类型数转Ip地址
*
* @param ipLong
* @return
*/
public static String longToIP(long ipLong) {
StringBuffer sb = new StringBuffer("");
sb.append(String.valueOf(ipLong >> 24)).append(".").append(String.valueOf((ipLong & 0x00ffffff) >> 16))
.append(".").append(String.valueOf((ipLong & 0x0000ffff) >> 8)).append(".")
.append(String.valueOf(ipLong & 0x000000ff));
return sb.toString();
}
}
/**
* 将对象集合转换为数组并排序
* @param ipPojoList
* @return
*/
public static IPAndLocationPojo[] IpPojoArray(List<IPAndLocationPojo> ipPojoList) {
// 集合转数组
IPAndLocationPojo[] ipPojoArray = new IPAndLocationPojo[ipPojoList.size()];
ipPojoList.toArray(ipPojoArray);
// 数组排序
Arrays.sort(ipPojoArray);
return ipPojoArray;
}
5. 二分法查找
根据用户输入的IP地址,采用二分法在已经排序好的数组中,查找IP的归属地信息。
由于IP地址库中存储的IP为一段地址范围,所以每次判断中间索引的起始地址小于用户输入地址后,需要在判断中间索引的结束地址是否大于用户输入IP,如果满足条件说明用户输入的IP地址位于当前中间索引起始地址所在的IP地址段,直接返回中间索引的IP归属地即为用户的IP归属地,
/**
* 根据用户输入的地址查寻归属地
* @param userIp
* @param arr
* @return
*/
public static String searchIP(String userIp, IPAndLocationPojo[] arr) {
// 将用户输入的IP转换为long型
long userIP = IPUtil.ipToLong(userIp);
int height = arr.length - 1;
int low = 0;
// 二分查找
while(low <= height) {
int mid =(height + low) / 2;
// 当中间索引的起始IP大于用户输入IP时,结束索引等于中间索引-1
if(arr[mid].getStartIPLong() > userIP) {
height = mid - 1;
// 当中间索引的结束IP地址大于等于用户输入IP时,返回当前归属地信息
}else if(arr[mid].getEndIPLong() >= userIP){
return arr[mid].getLocation();
}else {
low = mid + 1;
}
}
return null;
}
6. 封装接口类
封装接口,对外只提供获取归属地的方法,入参:IP地址, 出参:归属地
静态代码块中为数组中数据的初始化工作,保证每次执行只执行一次,不需要重复结构化数据,提高效率。文中代码对排序好的数组执行了序列化操作,也可以不进行序列化。
序列化工具类:
package com.zh.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class FileSerializationUtil {
// 序列化对象
public static void SerializationObject(String path, Object arr) {
try (
FileOutputStream fos = new FileOutputStream(path);
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream oos = new ObjectOutputStream(bos);
) {
oos.writeObject(arr);
oos.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
// 对象反序列化
public static Object DeserializationObject(String path) {
Object obj = null;
try (
FileInputStream fis = new FileInputStream(path);
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream ois = new ObjectInputStream(bis);
){
obj = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
IP地址校验工具类:用于校验用户输入IP地址是否符合规范。
package com.zh.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// IP地址校验
public class IPCheckUtil {
// 正则表达式IP地址验证规则
private static final String IP_PATTERN = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
// 创建匹配模式
private static final Pattern pattern = Pattern.compile(IP_PATTERN);
// 匹配IP地址
public static boolean checkIP(String ipString) {
Matcher matcher = pattern.matcher(ipString);
return matcher.matches();
}
}
管理类中代码对外提供接口的方法
private static final String IP_ILLEGAL = "IP地址不合法";
private static final String SERIALIZED_FILE_PATH = "./serialized_ip_data.ser";
private static IPAndLocationPojo[] arr = null;
// 静态语句块只加载一次
static {
File serializedPath = new File(SERIALIZED_FILE_PATH);
if(serializedPath.exists()) {
arr = (IPAndLocationPojo[])FileSerializationUtil.DeserializationObject(SERIALIZED_FILE_PATH);
}else {
// 本地IP地址库
String filePath = "ip_location_relation.txt";
// 编码方式
String encoding = "UTF-8";
List<IPAndLocationPojo> list = null;
try {
// 数据结构化
list = DataProcessManager.getPojoList(filePath, encoding);
// 转换为数组并排序
arr = DataProcessManager.IpPojoArray(list);
FileSerializationUtil.SerializationObject(SERIALIZED_FILE_PATH, arr);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取IP归属地
* @param ipString
* @return
* @throws IOException
*/
public static String ipLocationSer(String ipString){
// 校验IP地址是否合法
if (IPCheckUtil.checkIP(ipString)) {
// IP地址合法,查询归属地
String ipuser = DataProcessManager.searchIP(ipString, arr);
return ipuser;
} else {
return IP_ILLEGAL;
}
}
7. 入口类
充当程序入口,执行main方法。
package com.zh.controller;
import java.io.IOException;
import java.util.List;
import java.util.Scanner;
import com.zh.manage.DataProcessManager;
import com.zh.pojo.IPAndLocationPojo;
import com.zh.utils.IPCheckUtil;
//入口类
public class SystemController {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要查询的IP地址:");
String ip = null;
while((ip = scanner.next())!= null) {
// long startTime = System.currentTimeMillis();
String location = DataProcessManager.ipLocationSer(ip);
System.out.println(location);
// System.out.println("耗时: " + (System.currentTimeMillis() - startTime));
System.out.println("请输入要查询的IP地址:");
}
}
}
管理类完整代码:
package com.zh.manage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.zh.pojo.IPAndLocationPojo;
import com.zh.utils.FileReadUtil;
import com.zh.utils.FileSerializationUtil;
import com.zh.utils.IPCheckUtil;
import com.zh.utils.IPUtil;
/**
* 管理类
*/
public class DataProcessManager {
private static final String IP_ILLEGAL = "IP地址不合法";
private static final String SERIALIZED_FILE_PATH = "./serialized_ip_data.ser";
private static IPAndLocationPojo[] arr = null;
// 静态语句块只加载一次
static {
File serializedPath = new File(SERIALIZED_FILE_PATH);
if(serializedPath.exists()) {
arr = (IPAndLocationPojo[])FileSerializationUtil.DeserializationObject(SERIALIZED_FILE_PATH);
}else {
// 本地IP地址库
String filePath = "ip_location_relation.txt";
// 编码方式
String encoding = "UTF-8";
List<IPAndLocationPojo> list = null;
try {
// 数据结构化
list = DataProcessManager.getPojoList(filePath, encoding);
// 转换为数组并排序
arr = DataProcessManager.IpPojoArray(list);
FileSerializationUtil.SerializationObject(SERIALIZED_FILE_PATH, arr);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取IP归属地
* @param ipString
* @return
* @throws IOException
*/
public static String ipLocationSer(String ipString){
// 校验IP地址是否合法
if (IPCheckUtil.checkIP(ipString)) {
// IP地址合法,查询归属地
String ipuser = DataProcessManager.searchIP(ipString, arr);
return ipuser;
} else {
return IP_ILLEGAL;
}
}
/**
* 结构化数据
* @param filePath
* @param encoding
* @return
* @throws IOException
*/
private static List<IPAndLocationPojo> getPojoList(String filePath, String encoding) throws IOException{
// 获取数据
List<String> list = FileReadUtil.FileRead(filePath, encoding);
// 封装结构化后的数据
List<IPAndLocationPojo> pojoList = new ArrayList<IPAndLocationPojo>();
// 逐条遍历取出数据
for(String string : list) {
// 跳过空行
if(string == null || string.trim().equals("")) {
continue;
}
// 分割字符串
String[] strs = string.split("\t");
// 跳过不合规的数组
if(strs.length != 3) {
continue;
}
String startIP = strs[0];
String endIP = strs[1];
String location = strs[2];
IPAndLocationPojo ipPojo = new IPAndLocationPojo(startIP, endIP, location);
pojoList.add(ipPojo);
}
return pojoList;
}
/**
* 将对象集合转换为数组并排序
* @param ipPojoList
* @return
*/
private static IPAndLocationPojo[] IpPojoArray(List<IPAndLocationPojo> ipPojoList) {
// 集合转数组
IPAndLocationPojo[] ipPojoArray = new IPAndLocationPojo[ipPojoList.size()];
ipPojoList.toArray(ipPojoArray);
// 数组排序
Arrays.sort(ipPojoArray);
return ipPojoArray;
}
/**
* 根据用户输入的地址查寻归属地
* @param userIp
* @param arr
* @return
*/
private static String searchIP(String userIp, IPAndLocationPojo[] arr) {
// 将用户输入的IP转换为long型
long userIP = IPUtil.ipToLong(userIp);
int height = arr.length - 1;
int low = 0;
// 二分查找
while(low <= height) {
int mid =(height + low) / 2;
// 当中间索引的起始IP大于用户输入IP时,结束索引等于中间索引-1
if(arr[mid].getStartIPLong() > userIP) {
height = mid - 1;
// 当中间索引的结束IP地址大于等于用户输入IP时,返回当前归属地信息
}else if(arr[mid].getEndIPLong() >= userIP){
return arr[mid].getLocation();
}else {
low = mid + 1;
}
}
return null;
}
}
运行结果:
8. 优化
为提高首次读取效率添加序列化和反序列化,pojo类中startIP和endIP属性无需序列化可添加transient关键字,可减小序列化后文件的大小,减少IO操作耗时。
/**
* 起始IP
*/
private transient String startIP;
/**
* 结束IP
*/
private transient String endIP;
入口类方法可加入多线程,同步加载管理类,加载静态语句块,实现初始化。
//入口类
public class SystemController {
public static void main(String[] args) {
// 创建一个线程,加载管理类
new Thread(() -> {
try {
Class.forName("com.zh.manage.DataProcessManager");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}).start();
// new Thread(() -> new DataProcessManager()).start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要查询的IP地址:");
String ip = null;
while((ip = scanner.next())!= null) {
long startTime = System.currentTimeMillis();
String location = DataProcessManager.ipLocationSer(ip);
System.out.println(location);
System.out.println("耗时: " + (System.currentTimeMillis() - startTime));
System.out.println("请输入要查询的IP地址:");
}
}
}
优化前首次查询耗时:
优化后首次查询耗时: