因为至简网格专注于在极其受限的环境下实现复杂的服务,做到在一部安卓手机上运行完整的HTTP服务器功能,实现服务化、分布式部署等。所以互联网上有些东西非常好,却不能用。
因为要统计访问地,所以用到IP转地址的功能,支持IPv4就行了。使用阿里提供的服务,首先要收费,其次网络调用的性能不佳,所以考虑用纯真数据库。纯真数据库11M多一些(2023.5.30),并不大,可以用在安卓上。但是它的地址信息不是UTF8的,部分内容是多余的,且地址格式有点乱。所以基于不折腾一把不死心的精神,将它转换了一下。略去了一些多余信息,比如“XXX网吧”之类的,只留下国家、省、市、区/县、运营商信息;改正了部分错误,比如“四川大学”这样的记录,更改成“四川 成都 武侯区 四川大学”;合并了将近1/3的记录。有了这些信息,就可以实现按地域、运营商做灰度控制了。
优化后的数据不到4.3M。因为体积小,优化就容易了,全部加载到内存中查询。返回的是String[],通常有四个部分:运营商、国家根域名 国家、省/州、详细地址(市、县/区),其中省份、详细地址可能没有,如果没有运营商信息,则返回“*”。比如查询“1.34.236.0”返回“中华电信 中国 台湾 新北”,查询“1.57.0.255”,返回“联通 中国 黑龙江 绥化”,查询“4.0.0.7”,返回“Level3 美国 科罗拉多州 布隆菲尔德”。
使用原来的55万行IP地址记录作为查询输入,做了个性能测试,单线程可以达到500万/秒,性能与功能应该能满足绝大部分使用场景了。
数据文件格式:
Header)ver:1|createAt:4|countryLen:4|provinceLen:4|telcomLen:4|addrLen:4
Countries)len:1|content...
Provinces)len:1|content...
Telcoms)len:1|content...
Addrs)len:1|content...
Index)start:4|segLen:3|addrIdx:2|country:1|province:1|telcom:1,...
使用Java实现,搜索部分使用二分查找法。
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class IP2Addr {
private static final int NULL_PROVINCE = 255;
private static String[] countries;
private static String[] provinces;
private static String[] telcoms;
private static String[] addrs;
private static int[] index;
//segLen:3|addrIdx:2|country:1|province:1|telcom:1
private static long[] segments;
private static int indexNum = -1;
public static void init(InputStream in) throws IOException {
try(DataInputStream dis = new DataInputStream(new BufferedInputStream(in))) {
/*int ver = */dis.read();
/*long createAt = 60000L * */dis.readInt();
int countryLen = dis.readInt();
int provinceLen = dis.readInt();
int telcomLen = dis.readInt();
int addrLen = dis.readInt();
countries = loadStrings(dis, countryLen);
provinces = loadStrings(dis, provinceLen);
telcoms = loadStrings(dis, telcomLen);
addrs = loadStrings(dis, addrLen);
indexNum = loadIndex(dis, addrs.length,
countries.length, provinces.length, telcoms.length);
}
}
public static String[] parse(String ipStr) {
if(ipStr.indexOf(':') > 0) {
return null; //不支持ipv6地址
}
int ip = ipv4ToInt(ipStr);
int i = search(index, ip);
if(i < 0) {
return null;
}
long v = segments[i];
//segLen:3|addrIdx:2|country:1|province:1|telcom:1
int seg_len = ((int)(v>>40)) & 0xffffff;
int end = index[i] + seg_len;
if(ip > end) {
return null;
}
int addrIdx = ((int)(v >>> 24)) & 0xffff;
String addr = addrs[addrIdx];
int country = ((int)(v >>> 16)) & 0xff;
int province = ((int)(v >>> 8)) & 0xff;
int telcom = ((int)v) & 0xff;
if(province != NULL_PROVINCE) { //有省份信息
return new String[] {
telcoms[telcom],
countries[country],
provinces[province],
addr};
} else {
return new String[] {
telcoms[telcom],
countries[country],
"",
addr};
}
}
/**
* 循环实现二分查找算法arr
* @param index 已排好序的数组
* @param ip 待查的ip
* @return -1 无法查到数据
*/
private static int search(int[] index, int ip) {
//定义初始最小、最大索引
int i1 = 0;
int i2 = indexNum - 1;
int ii;
//确保不会出现重复查找,越界
while (i1 < i2) {
if(ip >= index[i1] && ip < index[i1 + 1]) {
return i1;
}
ii = (i1 + i2) >>> 1;//计算出中间索引值
if (ip >= index[ii] && ip < index[ii + 1]) {
return ii;
}
if (ip < index[ii]) { //判断左边
i2 = ii;
} else { //判断右边
i1 = ii;
}
}
return i2; //若没有,则返回-1
}
private static String[] loadStrings(DataInputStream dis, int len) throws IOException {
byte[] buf = new byte[len];
int readLen = dis.read(buf);
if(readLen != len) {
throw new IOException("invalid content length");
}
int l;
int i = 0;
String s;
List<String> list = new ArrayList<>();
while(i < len) {
l = ((int)buf[i]) & 0xff;
s = new String(buf, i + 1, l, StandardCharsets.UTF_8);
i += l + 1;
list.add(s);
}
return list.toArray(new String[0]);
}
private static int loadIndex(DataInputStream dis, int maxAddr,
int maxCountry, int maxProvince, int maxTelcom) throws IOException {
byte[] buf = new byte[12 * 10000];
int start;
int readLen;
List<Integer> index = new ArrayList<>();
List<Long> segments = new ArrayList<>();
int idx;
long v;
int fore = Integer.MIN_VALUE;
while((readLen = dis.read(buf)) > 0) {
//startIp:4|segLen:3|addrIdx:2|country:1|province:1|telcom:1
if((readLen % 12) != 0) {
throw new IOException("Invalid content");
}
for(int i = 0; i < readLen; i += 12) {
start = parseInt(buf, i, 4);
if(start < fore) {
throw new IOException("Invalid content,not sorted @" + i);
}
fore = start;
//加载时判断,使用时就不必每次都判断了
v = parseInt(buf, i + 4, 3); //seg_len
idx = parseInt(buf, i + 7, 2); //addrIdx
if(idx >= maxAddr) {
throw new IOException("Invalid content,too large addr index " + idx + " @" + i);
}
v <<= 16;
v |= idx;
idx = ((int)buf[i + 9]) & 0xff;
if(idx >= maxCountry) {
throw new IOException("Invalid content,too large country index " + idx + " @" + i);
}
v <<= 8;
v |= idx;
idx = ((int)buf[i + 10]) & 0xff;
if(idx >= maxProvince && idx != NULL_PROVINCE) {
throw new IOException("Invalid content,too large province index " + idx + " @" + i);
}
v <<= 8;
v |= idx;
idx = ((int)buf[i + 11]) & 0xff;
if(idx >= maxTelcom) {
throw new IOException("Invalid content,too large telcom index " + idx + " @" + i);
}
v <<= 8;
v |= idx;
index.add(start);
segments.add(v);
}
}
int n = 0;
IP2Addr.index = new int[index.size()];
for(Integer d : index) {
IP2Addr.index[n++] = d;
}
n = 0;
IP2Addr.segments = new long[segments.size()];
for(Long d : segments) {
IP2Addr.segments[n++] = d;
}
return n;
}
private static int parseInt(byte[] buf, int pos, int len) {
int v = 0;
for (int i = 0; i < len; i++) {
v <<= 8;
v |= ((int) buf[pos + i]) & 0xff;
}
return v;
}
/**
* 使用小端序,因为大端序把数字的高位放在内存的低字节,
* 导致在计算分段开始与结束地址差值时出现了错误
* @param ip 地址
* @return 整型值,数字的高位放在内存的高字节
*/
private static int ipv4ToInt(String ip) {
int p1 = 0;
int p2;
int v = 0;
while((p2 = ip.indexOf('.', p1)) > 0) {
v <<= 8;
v |= Integer.parseInt(ip.substring(p1, p2));
p1 = p2 + 1;
}
v <<= 8;
v |= Integer.parseInt(ip.substring(p1));
return v;
}
}