利用纯真ip库搭建ip查询服务

admin 2025-10-03 阅读:8 评论:0
如何使用 我们的ip地址必在0.0.0.0-255.255.255.255之间(这里不考虑ipv6的地址),所以只要判断某个ip是否在ipstart和ipend之间就行了(即一个ip段内),就知道它属于哪里了。 sql查询方式即为: 注:...

在这里插入图片描述

如何使用

我们的ip地址必在0.0.0.0-255.255.255.255之间(这里不考虑ipv6的地址),所以只要判断某个ip是否在ipstart和ipend之间就行了(即一个ip段内),就知道它属于哪里了。

sql查询方式即为:

注:INET_ATON函数 会将一个有效的ip地址转成整数,所以不用自己手动转换ip地址了,直接传递点分式ip地址就行了。

在这里插入图片描述

但是不推荐这样使用,前面那种方式是直接将ip转为整数存入数据库,下面这种方式是直接存入ip的点分式,然后查询时转换。这样的话,速度非常慢,因为它相当于要对每一个ip地址做转换才能查询,这是错误的使用方式。

在这里插入图片描述

数据准备

首先,需要准备qqwry.dat这个文件,因为数据在它里面。但是,我发现了一个更好用的软件,这里就不直接使用这个文件了。

注意:鉴于网络上面资源下载容易踩坑,这里推荐到这个网站去下载,点击页面中的下载即可。如果有能力的话,也可以选择捐助作者。

在这里插入图片描述

下载完成之后,解压安装这个软件,就可以使用ip查询了。但是它是一个桌面软件,我们这里的目的是做成web服务,即像这个网站一样的查询服务。这样的ip查询服务有很多,但是如果使用量大的话,自己搭建一个还是非常好的,成本低、可靠!并且,这个数据库是一直在更新的,而且更新频率也还高whatsapp网页版,说明它的效果还是很好的。

在这里插入图片描述

测试一下,结果同上面的web页面

在这里插入图片描述

这里我们不直接使用这个软件,而是使用它的一个功能,在主界面上有一个解压按钮,点击将ip数据导出成文本(我这里命名成qqwry.txt)。

在这里插入图片描述

qqwry.txt的内容格式如下

在这里插入图片描述

因为这个qqwry.txt的格式,直接在数据库中查询不方便,所以需要处理一下,它目前的格式是:4列(ip段起始地址 ip段结束地址 area地区 location位置)。

如果直接将它们存入数据库中,那么查询就会变得麻烦,而且基本上也无法使用索引了,对于性能影响很大。这里我们采用网上别人的方式,将ip起始地址和ip结束地址转成数字。

所以需要先使用程序对上面的文本数据进行处理一下,我看别人的博客处理数据都是基于php的,但是我并不会世界上最好的语言,所以我就采用java来处理了。

注意:由于java语言没有无符号数的概念,所以ip地址并不能转成java的int型。ipv4地址是32位,int型也是32位,但是java的int中第一位是符号位,所以实际可以表示的大小是其它语言中int的一半。这也导致了,如果将ip地址转换为int的话,会有许多数据溢出,变成负数,所以这里需要将ip地址转成long型。

数据处理 数据处理代码

package re;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.stream.Collectors;
public class IPRecord {
	private static final String regex = "\\s+";
	private static final int limit = 4;   // split方法的这个参数的作用很值得深入学习一下
	private static final String ipRegex = "\\.";
	
	private static class Record {
		String ipStartStr;
		String ipEndStr;
		long ipStart;
		long ipEnd;
		String area;
		String location;
		
		public Record(String[] record) {
			this.ipStartStr = record[0];
			this.ipEndStr = record[1];
			this.ipStart = ip2Long(record[0]);   // 将ip地址字符串转换成long
			this.ipEnd = ip2Long(record[1]);     // 将ip地址字符串转换成long
			this.area = record[2];
			this.location = record[3];
		}
		
		@Override
		public String toString() {
			return ipStart + "," + ipEnd + "," + area + "," + location + "," + ipStartStr + "," + ipEndStr;  // 转成csv格式
		}
		
		public String toRedis() {
			return "ZADD\tqqwry\t" + ipEnd + "\t" + "{\"ipstart\":" + ipStart 
					+ ",\"ipend\":" + ipEnd
					+ ",\"area\":" + "\"" + area + "\""
					+ ",\"location\":" + "\"" + location + "\""
					+ ",\"ipstartstr\":" + "\"" + ipStartStr + "\""
					+ ",\"ipstartend\":" + "\"" + ipEndStr
					+ "\"}";
		}
	}
	
	public static void main(String[] args) throws IOException {
		// qqwry.tx 的文件路径
		Path path = Paths.get("C:/Users/Alfred/Desktop/data/test/qqwry.txt");
		// 使用指定字符集(UTF-8)读取文件
		List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
    //  输出测试,注意不要省略 limit,因为总共有 52万+行数据,直接输出的话,压力比较大。
//		lines.stream().map(IPRecord::split).limit(10).map(Record::toJson).forEach(System.out::println);
		// 将文件转为csv文件存放
		
		long start = System.currentTimeMillis();
		lines = lines.stream()
				     .map(IPRecord::split)
				     .map(Record::toString)
				     .limit(528129)     // 有效数据行数,剩下的是一些空行和最后的统计信息
				     .collect(Collectors.toList());
		
//	    String csvHeader = "ipstart,ipend, area, location, ipstartstr, ipendstr\n";
//		Files.write(Paths.get("C:/Users/Alfred/Desktop/data/test/qqwry.csv"), csvHeader.getBytes(), StandardOpenOption.CREATE);            //写入csv文件的头部
		Files.write(Paths.get("C:/Users/Alfred/Desktop/data/test/qqwry_redis.csv"), lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE);  //写入csv文件的主体部分
		System.out.println("Execute Success! Time: " + (System.currentTimeMillis()-start) + " ms");
		
	}
	
	public static Record split(String record) {
		return new Record(record.split(regex, limit));
	}
	
	/**
	 * int类型是32位的,但是无法直接将ip和int类型进行转换,因为可能会溢出。
	 * int类型是有符号的,除非这里有无符号整型,但是Java是没有这个的,所以
	   *    这里需要转换成long型。
	 * */
	public static long ip2Long(String ip) {
		String[] strs = ip.split(ipRegex);
		int[] bs = new int[4];
		for (int i = 0; i < 4; i++) {
			bs[i] = Integer.parseInt(strs[i]);  // Byte.parseByte(strs[i]); 这个方法转成的字节范围是 -128-127,真是太坑了!
		}
		return ((bs[3] & 0xFFL)      ) +        // 这个转换方法是从jdk源码里面偷来的,挺好用的!
	               ((bs[2] & 0xFFL) <<  8) +
	               ((bs[1] & 0xFFL) << 16) +
	               ((bs[0] & 0xFFL) << 24);
	}
}
// 默认的split(regex)方法会以split(regex,0)调用
// 当limit N 大于0时,会匹配N-1次,数组长度最大为N,返回数组的最后一个元素会把数组最后的所有字符包括进来
// 当limit N 小于0时,会匹配尽可能多次,返回最大长度的数组
// 当limit N 等于0时,会匹配尽可能多次,返回最大长度的数组,并且末尾的空串不会包含进来
//推荐使用正则表达式,速度飞起
//String record = "0.0.0.0         0.255.255.255   IANA 保留地址";
//String regex = "\\s+";
//long start = System.currentTimeMillis();
//for (String s : record.split(regex)) {
//	System.out.println(s);
//}
//System.out.println(System.currentTimeMillis()-start);
//这种方式太慢了,简直难以忍受!
//List strList = new ArrayList<>();
//boolean flag = true;
//StringBuilder sb = new StringBuilder();
//
//char[] chs = record.toCharArray();
//for (int i = 0; i < chs.length; i++) {
//	if (chs[i] != ' ') {
//		sb.append(chs[i]);
//		if (i == chs.length - 1) {
//			strList.add(sb.toString());
//			break;
//		}
//		flag = true;
//	} else {
//		if (flag) {
//			strList.add(sb.toString());
//			sb.delete(0, sb.length());
//		}
//		flag = false;
//	}
//}
//strList.forEach(System.out::println);

注意: toString方法是转成符合数据库导入格式的数据,toRedis是转成符号redis导入的格式(我不太确定这种方式的效率,并且我最终没有采用这种方法)。

说明:

1.这里的处理逻辑很简单,就是将文件分行读入内存,然后对每一行切分成4块,然后调用编写的ip2Long方法进行处理即可。

2.如果可以使用正则表达式,尽量用正则表达式,特别是这种数据处理,速度差距非常之大!

3.这个ip2Long方法,是我从jdk的ObjectInpustream里面拿来的,随便修改了一下,原来的代码使用的是byte,但是这里如果传入一个尝试把一个大于127的数转成int会抛出异常,所以使用int数组代替了byte数组,并且由于我这里只有32位,所以我就删除了后32位的处理代码。

执行结果:

这个notepad++打开大文件感觉还行,40多m也能流畅使用。我这里使用excel打开就比较慢了,而且中文乱码了,就不放截图了。如果使用excel打开的话和普通的excel文件显示没有什么区别。我这里转成csv的原因是:

a).csv文件本身就是文本格式,使用程序直接输出即可。

b).我使用的sqlyog社区版无法导入excel数据,只能导入csv格式的数据。

csv格式数据

在这里插入图片描述

redis格式的数据,直接就是一条命令,但是它总共是52万+的数据,想必一条一条的执行,时间上也会接受不了,但是这个给我了启发whatsapp登录,我后来在redis中使用了这种格式来存储值(json数据)。

redis格式数据(没有使用)

在这里插入图片描述

数据入库

因为我使用的是sqlyog,数据导入有一点麻烦,如果使用其它的数据库客户端应该会简单一些。

建库建表

这个建表语句参考了别人的博客,做了一些修改(添加了原始的ip起始地址和结束地址、修改了表的字符编码)

CREATE TABLE `ip_data` (
`ipstart`  INT(10) UNSIGNED NOT NULL,                                                # 开始ip地址
`ipend`  INT(10) UNSIGNED NOT NULL,                                                  # 结束ip地址
`area`  VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,       # 区域
`location`  VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,  # 位置
`ipstartstr` VARCHAR(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,  # 开始ip地址的点分表示方式
`ipendstr` VARCHAR(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,    # 结束ip地址的点分表示方式
PRIMARY KEY (`ipstart`),
INDEX `ip` (`ipstart`, `ipend`) USING BTREE 
)
ENGINE=INNODB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci;

导入数据

我直接使用sqlyog的导入csv,但是没有成功。它的图形界面导入功能感觉就很鸡肋,我就没成功过,最后没办法,从网上查找到了如下命令,然后导入了csv文件,速度还行!

LOAD DATA LOCAL INFILE "C:/Users/Alfred/Desktop/data/test/qqwry.csv" 
INTO TABLE  `poem_land`.`qqwry_ip`  FIELDS TERMINATED BY ',' LINES TERMINATED BY '\r\n';

数据查询

数据行数量

在这里插入图片描述

简单查看几行数据

在这里插入图片描述

查询数据

在这里插入图片描述

好了,还算不错!

数据存入redis

关系型数据库的查询效果可能不够理想,这里使用redis这个nosql来提高查询性能,首先要做的还是数据导入。我发现这个数据导入真的是麻烦哎,昨天这个东西搞了我好久,当然也因为我对redis并不熟悉。

上面说了,我没有采用那种一条一条执行命令的方式,而是使用了一种更加快速的方式——redis的管道技术。网上看了好多资料才搞成,第一次听说这个东西,效果确实很好!这里我们要做的是使用redis的管道技术将mysql数据导入到redis。

导入数据命令

sudo mysql -uroot -p'password' poem_land --skip-column-names --raw < data.sql | ./redis-4.0.11/src/redis-cli -h host -p port -a password --pipe

参数:

password:mysql数据库密码、redis密码

host:redis所在主机

port:redis所用端口

data.sql:一个sql文件,执行它会将数据库数据转成适合于redis导入的形式。

data.sql 文件

注意: redis_member 就是存入redis的值whatsapp网页版,这里我采用数据库的CONCAT函数把所有列拼成了一个json字符串的形式,这样的话查询就可以获取到全部数据了,我是第一次这样用,不知道其它更好的方式(不过,导入时间却耗费了好几分钟,估计是拼接字符串影响了性能)。

SELECT CONCAT(
    '*4\r\n',
    '$',LENGTH(redis_cmd),'\r\n',redis_cmd,'\r\n',
    '$',LENGTH(redis_key),'\r\n',redis_key,'\r\n',
    '$',LENGTH(redis_score),'\r\n',redis_score,'\r\n',
    '$',LENGTH(redis_member),'\r\n',redis_member,'\r'
) FROM (
    SELECT 'ZADD' AS redis_cmd,
    'qqwry' AS redis_key,
    ipend AS redis_score,
    CONCAT('{"ipstart":', ipstart, ',"ipend":', ipend,
    ',"area":"', `area`, '","location":"', location, '"}') AS redis_member 
    FROM `poem_land`.`qqwry_ip`
) AS NAME

导入结果

在这里插入图片描述

查询数据

Springboot结合使用

我最终的目的,还是要提供一个查询的接口来使用。现在就将它作为服务提供了,让我们开始吧!

控制器代码

这里我使用的是mybatis_plus和它的生成器,所以代码是自动生成的,基本上只需要看这个Controller的代码就行了。

package org.dragon.ip_seek.controller;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.dragon.ip_seek.entity.QqwryIp;
import org.dragon.ip_seek.service.IQqwryIpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
 * 

* 前端控制器 *

* * @author Alfred * @since 2020-12-08 */
@RestController @RequestMapping("/seeker/") public class QqwryIpController { private static final Logger logger = LoggerFactory.getLogger(QqwryIpController.class); private static final String ipRegex = "\\."; // 匹配ip的正则表达式 @Autowired private IQqwryIpService iqqwryIpService; // mysql 服务调用 @Autowired private RedisTemplate<String, String> redisTemplate; // redis 服务调用 @GetMapping(value = "/poem1", produces = MediaType.APPLICATION_JSON_VALUE) public String test01(HttpServletRequest request) { // 本地局域网测试,ip地址是内网地址,无法使用,这里我先传递一个模拟的ip地址。 String host = request.getRemoteHost(); logger.info("当前用户的ip地址为:{}", host); Map<String, String> ipInfo = iqqwryIpService.getIpInfo("60.174.231.10"); ipInfo.forEach((k, v) -> { logger.info("K:{} V:{}", k, v); }); return "{\"poem\": \"一日不见兮,思之如狂。\"}"; } @GetMapping(value = "/poem_mysql", produces = MediaType.APPLICATION_JSON_VALUE) public Map<String, Object> test02(HttpServletRequest request) { String host = request.getRemoteHost(); // 本地局域网测试,ip地址是内网地址,无法使用,这里我先传递一个模拟的ip地址。 // 如果部署到云服务器上,去掉此行即可。 host = "60.174.231.10"; LambdaQueryWrapper<QqwryIp> wrapper = Wrappers.lambdaQuery(); // 忘记了,eclipse自己的编译器不支持这个 SFunction接口 wrapper.select(QqwryIp::getArea, QqwryIp::getLocation) .apply("INET_ATON({0}) between ipstart and ipend", host); Map<String, Object> info = iqqwryIpService.getMap(wrapper); Map<String, Object> ipInfoMap = new java.util.LinkedHashMap<>(); // 我想要指定map序列化的键值对顺序为:ip area location ipInfoMap.put("ip", host); if (Objects.nonNull(info)) { ipInfoMap.putAll(info); } logger.info("通过mysql查询 ip: {}", ipInfoMap.get("ip")); return ipInfoMap; // "{\"poem\": \"一日不见兮,思之如狂。\"}"; } /** * 采用 Redis 缓存的方式进行服务调用 * @param * */ @GetMapping(value = "/poem_redis", produces = MediaType.APPLICATION_JSON_VALUE) public Map<String, Object> test03(HttpServletRequest request) { String host = request.getRemoteHost(); host = "116.62.188.176"; // 本来想测试一个错误数据的:256.256.266.255,结果long都溢出了,直接变成了0 // 从redis中查询数据 Set<String> resultSet = redisTemplate.opsForZSet() .rangeByScore("qqwry", ip2Long(host), Double.MAX_VALUE, 0, 1); logger.info("通过reids查询 ip: {}", host); return transfer(resultSet, host); } /** * 对redis查询数据做一个转换,使之格式更加友好 * */ @SuppressWarnings("unchecked") private Map<String, Object> transfer(Set<String> resultSet, String host) { Map<String, Object> ipInfoMap = new java.util.LinkedHashMap<>(); // 我想要指定map序列化的键值对顺序为:ip area location ipInfoMap.put("ip", host); if (resultSet.isEmpty()) { ipInfoMap.put("area", "NAN"); ipInfoMap.put("location", "NAN"); return ipInfoMap; } String result = resultSet.toArray(new String[0])[0]; Map<String, Object> info = null; ObjectMapper mapper = new ObjectMapper(); try { info = mapper.readValue(result, Map.class); // 这里泛型会被擦除,直接赋值会报编译器警告,但是我也不知道Class这种参数该如何传递了,就先抑制一下编译器警告吧 if (Objects.nonNull(info)) { ipInfoMap.putAll(info); } } catch (JsonMappingException e) { e.printStackTrace(); } catch (JsonProcessingException e) { e.printStackTrace(); } return ipInfoMap; } private long ip2Long(String ip) { String[] strs = ip.split(ipRegex); int[] bs = new int[4]; for (int i = 0; i < 4; i++) { bs[i] = Integer.parseInt(strs[i]); // Byte.parseByte(strs[i]); 这个方法转成的字节范围是 -128-127,真是太坑了! } return ((bs[3] & 0xFFL) ) + // 这个转换方法是从jdk源码里面偷来的,挺好用的! ((bs[2] & 0xFFL) << 8) + ((bs[1] & 0xFFL) << 16) + ((bs[0] & 0xFFL) << 24); } }

说明:

test01是传统的mybatis的写法,后来发现mybatis_plus可以使用更加简单的方法,就换成了test02,但是保留了test01。test03是从redis中进行数据查询。 效果演示

启动项目

在这里插入图片描述

test01——mysql

在这里插入图片描述

这里查询的结果,我是使用在日志上了,本来是打算在后台使用的,但是暂时也没啥想法,就算了。

在这里插入图片描述

test02——mysql

在这里插入图片描述

test03——redis

在这里插入图片描述

版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

分享:

扫一扫在手机阅读、分享本文

3502文章数 0评论数
热门文章
  • 详细指南:WhatsApp国内使用方法全攻略-下载安装、注册登录及功能使用攻略

    详细指南:WhatsApp国内使用方法全攻略-下载安装、注册登录及功能使用攻略
    一、下载与安装WhatsApp WhatsApp是一款全球使用范围极广的即时通讯软件,不仅可以实现文本聊天,还能进行语音、视频通话WhatsApp中文版,发送文件等功能。由于众所周知的原因,国内用户想要下载和安装WhatsApp可能会遇到一些困难。不过,只要按照以下步骤操作,就能顺利完成WhatsApp的下载和安装。 首先,我们需要解决的是下载问题。由于WhatsApp在国内的应用商店无法直接下载,所以我们需要寻找其他的下载途径。Android用户可以选择到各大安卓市场搜索下...
  • 苹果IOS5.1.1机完美越狱

    苹果IOS5.1.1机完美越狱
    越狱工具下载:(需手动复制: http://bbs./read-htm-tid-4804612.html ) 详细的越狱教程: Absinthe 2.0.4的越狱方法与Absinthe 1.0的越狱方法完全一样,非常简单的“傻瓜一键式”。不过这里绿毒有说明,最好重新刷固件。支持越狱设备IOS5.1.1的固件下载地址请点击本站的IOS固件下载,看下图: 下载完固件之后,把设备连接iTunes,开始刷固件。你也可以选择不刷,但是在越狱的过程之中,新的系统在越狱时会比较的顺利,...
  • 电脑微信的聊天照片文件在哪里微信图片保存电脑哪个文件夹

    电脑微信的聊天照片文件在哪里微信图片保存电脑哪个文件夹
    1. 怎么找到微信聊天图片在哪个文件夹里 图片保存路径:/storage/emulated/tencent/MicroMsg/WeiXin/文件夹,这个是完整路径。而在文件夹中只需要找到/tencent/MicroMsg/WeiXinWhatsApp中文版,就可以了,前面两个文件夹是系统根目录。以下是保存及查找图片的方法步骤介绍。 第一步、找到桌面的微信APP,直接点击打开这个微信的APP。 2. 电脑微信图片保存在哪个文件夹 个人微信文件夹。 电脑版微信聊天的图片,都保存到...
  • Windows 11版WhatsApp将从UWP/Native切换回WebView2打包模式

    Windows 11版WhatsApp将从UWP/Native切换回WebView2打包模式
    Meta(最近更名为 Meta AI)悄然宣布,WhatsApp 将在 Windows 11 上放弃 UWP(WinUI)WhatsApp中文版,退回采用基于 Chromium 的容器。这意味着 WhatsApp 又回到了几年前的样子。由于 web.whatsapp.com 一直领先于 Windows 应用开发,它确实拥有一些新功能,但速度更慢,占用更多内存。 如果独立开发者因为无力维护所有平台的代码库而选择 Web 应用程序,那是一回事,但当像 Meta 这样价值万亿美元...
  • 小米下載WhatsApp的完整指南:安装与使用注意事项

    小米下載WhatsApp的完整指南:安装与使用注意事项
    综上所述,确保您的小米手机具备以上条件,将有助于您顺利下载和安装WhatsApp,享受便捷的通讯服务。在满足这些条件后,您就可以按照后续步骤进行WhatsApp的下载和安装。 2. 下载和安装WhatsApp的具体步骤 在小米手机上下载和安装WhatsApp的具体步骤相对简单,但需注意一些特殊情况。首先,确保你的手机系统已更新到最新版本,以保证最佳的兼容性。由于安卓手机商店内无法直接找到WhatsApp,你可以通过APKPure等第三方应用商店进行下载。打开APKPure应用...