大多IP定位的功能都是基于一个IP库文件来实现,通过将目标IP与IP库文件中的数据作对比,查找出对应的IP范围,从而查找到其位置数据,对于频繁查找,显得力不从心了,因此,作者本人想通过将ip库数据放于内存,通过在内存中查找数据的方式来提高其查找性能。但是在实现过程中,遇到一些问题。
一、主要问题
1、问:IP库数据何处来,如果找到完整而相对准确的IP库?
答:目前市面上有qqwry纯真数据库,有ipip.net提供的,有ip138在线查询的,有qqzeng的IP库
因为是在制作IP库,因此不可能去使用在线的,只能把它用来矫正一些数据,另外,这是一个开源库,不可能去花钱买相关的数据,因此,qqzeng的库也排除掉,最终我选择了ipip.net提供的免费库,qqwry的库,关于中国的部分,结构比较混乱,具体表现在country字段。因此在ipip.net的库的基础上,我对一些数据进行了矫正(主要表现在如欧盟、非洲地区、北美地区、亚太地区、拉美地区等范围比较大的区域),通过手工修正了大约近两万条记录。另外,添加了国家英文简写代码,中国区域添加了省份拼音简称。
2、问:在内存中如何存储IP数据
答:将每条记录通过ffi定义C结构体,将记录存储到结构体中,每条记录对应一个key,而key是一个索引位置。将转换后的结构体存储到openresty中的共享字典中。
3、问:如何查找内存中的IP数据
答:存储最大的索引值,并通过二分法来查找目标IP对应的范围。
二、使用到的技术
1、文件操作
在初始化的时候,还是需要读文件,将IP库数据读入到内存中。因此需要用到io文件操作库
2、共享内存操作
主要存储是使用到openresty的ngx.shared.DICT,因此用到共享字典的操作
3、FFI库
openresty中的存储只能存储简单的数据,不能存储像table这样的结构,因此只能借助c的结构体与ffi,将每行IP数据转换成cdata存入到内存中
三、实现步骤
1、下载ipip.net官方提供的免费IP库,生成txt文件
官网下载一个将ipip.net官方IP库转化为qqwry.dat格式库的软件,并通过iplook将qqwry.dat解压成qqwry.txt
2、通过mysql的load file的功能,将txt导入到mysql中。
load file的使用请参考:mysql中导入大容量csv或txt文件
3、添加相关字段,国家英文简写、省、省拼音简写、城市等,并通过ip138等在线查询工具将部分数据补全,并将国家编号、省、省拼音等数据补全
国家英文简写,可以参照geo-ip官网提供的地区数据进行补全,省份和城市是根据IP库本身的数据,对其进行分割(可以使用mysql的left,right,substring,substring_index等来分割)
4、将mysql中的数据导出txt,格式如下:
16777216,16777471,APNIC,亚太互联网络信息中心,,,,
16777472,16778239,CN,中国,FJ,福建,,
16778240,16779007,AU,澳大利亚,,,,
16779008,16779263,AU,澳大利亚,,,,
第一个字段是起始IP,是将IP转化为整型后的数据;
第二个字段是终止IP,也是将IP转化为整型后的数据;
第三个字段是国家英文简称;
第四个字段是国家名称;
第五个字段是省英文简称;
第六个字段是省名称;
第七个字段是市名称;
第八个字段是详细地址。
5、写lua操作库
(1)定义数据结构,用于存储IP数据:
ffi.cdef[[
struct in_addr {
uint32_t s_addr;
};
int inet_aton(const char *cp, struct in_addr *inp);
uint32_t ntohl(uint32_t netlong);
char *inet_ntoa(struct in_addr in);
uint32_t htonl(uint32_t hostlong);
size_t strlen(const char *string);
typedef struct iplocation {
long start_ip;
long end_ip;
char country_code[7];
char country_name[30];
char province_code[2];
char province_name[30];
char city_name[30];
char detail[100];
} iplocation;
]]
注:
- in_addr结构体、strlen等是系统自带
- iplocation是自定义的结构体
(2)读取文件内容:
function _M.loadfile(self)
local path = self.path or 'lib/resty/location/ipdat.txt'
local fd, err = io.open(path)
if fd == nil then
return nil, err
end
local data = {}
local i = 0;
for v in fd:lines() do
local m, err = regx.match(v, '([0-9]+),([0-9]+),([A-Za-z]+),([^,]+),([^,]{0,2}),([^,]{0,30}),([^,]{0,30}),([^,]{0,100})')
i = i+1
if m then
data[#data+1] = m
end
end
fd:close()
return data
end
注:
- regx是ngx.re的别名
- 通过io库将文件读入到一个table中(将每行的数据通过正则表达式来检查,存入到一个数组中)
(3)将文件数据存入内存:
function _M.reload(self)
local data = self:loadfile()
if #data == 0 then
return nil, 'load file failed'
end
local res = false
for i, v in pairs(data) do
location_data.start_ip = tonumber(v[1])
location_data.end_ip = tonumber(v[2])
location_data.country_code = tostring(v[3])
location_data.country_name = tostring(v[4])
location_data.province_code = v[5] ~= nil and tostring(v[5]) or ''
location_data.province_name = v[6] ~= nil and tostring(v[6]) or ''
location_data.city_name = v[7] ~= nil and tostring(v[7]) or ''
location_data.detail = v[8] ~= nil and tostring(v[8]) or ''
local str = ffi.string(location_data, size)
res = self.dict:set('ip_data:'..i, str)
end
self.dict:set('ip_data_last', #data)
return res
end
(4)查找数据
查找某条数据将cdata解析成一个table
function _M.offset(self, start, last)
local index = math.ceil(tonumber((start+last)/2))
local data = self.dict:get('ip_data:'..index)
if data then
local location_data = ffi.cast(ptr_data, data)
local ip_data = {}
ip_data.start_ip = tonumber(location_data.start_ip)
ip_data.end_ip = tonumber(location_data.end_ip)
ip_data.country_code = tostring(ffi.string(location_data.country_code, tonumber(C.strlen(location_data.country_code))))
ip_data.country_name = tostring(ffi.string(location_data.country_name, tonumber(C.strlen(location_data.country_name))))
ip_data.province_code = tostring(ffi.string(location_data.province_code, ffi.sizeof('char[2]')))
ip_data.province_name = tostring(ffi.string(location_data.province_name, tonumber(C.strlen(location_data.province_name))))
ip_data.city_name = tostring(ffi.string(location_data.city_name, tonumber(C.strlen(location_data.city_name))))
ip_data.detail = tostring(ffi.string(location_data.detail, tonumber(C.strlen(location_data.detail))))
return ip_data,index
end
end
二分法查找:
function _M.search(self, ip)
local start = 0
local location_data
local count = 0
local last = self.dict:get('ip_data_last')
if last == nil then
self:reload()
last = self.dict:get('ip_data_last')
end
last = last and tonumber(last) or 0
if last < 1 then
return nil, 'cannot find the last ip index data'
end
local data, index = self:offset(start, last)
if data == nil then
return nil, 'cannot query the data with the index '..index
end
while location_data == nil do
location_data = data
if ip >= data.start_ip and ip<= data.end_ip then
break
elseif ip < data.start_ip then
last = index
else
start = index
end
data, index = self:offset(start, last)
if data == nil then
break
end
count = count + 1
location_data = nil
if count > 200 then break end
end
if location_data then
return location_data
else
return nil, 'can not find the data'
end
end
四、用法
local iplocation = require 'resty.iplocation.iplocation';
local loc = iplocation:new({path = "/the/path/to/your/project/lib/iplocation/iplocation/ipdat.txt"});
local data = loc:location('202.108.22.5');
if data then
ngx.say(data.country_name)
end
--[[
{
country_code = "CN",
country_name = "中国",
province_code = "BJ",
province_name = "北京",
city_name = "北京",
start_ip = 3396075520,
end_ip = 3396141055,
detail = nil
}
--]]