基于openresty共享内存实现一个IP定位的模块

大多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,格式如下:
  1. 16777216,16777471,APNIC,亚太互联网络信息中心,,,,
  2. 16777472,16778239,CN,中国,FJ,福建,,
  3. 16778240,16779007,AU,澳大利亚,,,,
  4. 16779008,16779263,AU,澳大利亚,,,,
  • 第一个字段是起始IP,是将IP转化为整型后的数据;

  • 第二个字段是终止IP,也是将IP转化为整型后的数据;

  • 第三个字段是国家英文简称;

  • 第四个字段是国家名称;

  • 第五个字段是省英文简称;

  • 第六个字段是省名称;

  • 第七个字段是市名称;

  • 第八个字段是详细地址。

5、写lua操作库
(1)定义数据结构,用于存储IP数据:
  1. ffi.cdef[[
  2. struct in_addr {
  3. uint32_t s_addr;
  4. };
  5. int inet_aton(const char *cp, struct in_addr *inp);
  6. uint32_t ntohl(uint32_t netlong);
  7. char *inet_ntoa(struct in_addr in);
  8. uint32_t htonl(uint32_t hostlong);
  9. size_t strlen(const char *string);
  10. typedef struct iplocation {
  11. long start_ip;
  12. long end_ip;
  13. char country_code[7];
  14. char country_name[30];
  15. char province_code[2];
  16. char province_name[30];
  17. char city_name[30];
  18. char detail[100];
  19. } iplocation;
  20. ]]

注:

  • in_addr结构体、strlen等是系统自带
  • iplocation是自定义的结构体
(2)读取文件内容:
  1. function _M.loadfile(self)
  2. local path = self.path or 'lib/resty/location/ipdat.txt'
  3. local fd, err = io.open(path)
  4. if fd == nil then
  5. return nil, err
  6. end
  7. local data = {}
  8. local i = 0;
  9. for v in fd:lines() do
  10. local m, err = regx.match(v, '([0-9]+),([0-9]+),([A-Za-z]+),([^,]+),([^,]{0,2}),([^,]{0,30}),([^,]{0,30}),([^,]{0,100})')
  11. i = i+1
  12. if m then
  13. data[#data+1] = m
  14. end
  15. end
  16. fd:close()
  17. return data
  18. end

注:

  • regx是ngx.re的别名
  • 通过io库将文件读入到一个table中(将每行的数据通过正则表达式来检查,存入到一个数组中)
(3)将文件数据存入内存:
  1. function _M.reload(self)
  2. local data = self:loadfile()
  3. if #data == 0 then
  4. return nil, 'load file failed'
  5. end
  6. local res = false
  7. for i, v in pairs(data) do
  8. location_data.start_ip = tonumber(v[1])
  9. location_data.end_ip = tonumber(v[2])
  10. location_data.country_code = tostring(v[3])
  11. location_data.country_name = tostring(v[4])
  12. location_data.province_code = v[5] ~= nil and tostring(v[5]) or ''
  13. location_data.province_name = v[6] ~= nil and tostring(v[6]) or ''
  14. location_data.city_name = v[7] ~= nil and tostring(v[7]) or ''
  15. location_data.detail = v[8] ~= nil and tostring(v[8]) or ''
  16. local str = ffi.string(location_data, size)
  17. res = self.dict:set('ip_data:'..i, str)
  18. end
  19. self.dict:set('ip_data_last', #data)
  20. return res
  21. end
(4)查找数据

查找某条数据将cdata解析成一个table

  1. function _M.offset(self, start, last)
  2. local index = math.ceil(tonumber((start+last)/2))
  3. local data = self.dict:get('ip_data:'..index)
  4. if data then
  5. local location_data = ffi.cast(ptr_data, data)
  6. local ip_data = {}
  7. ip_data.start_ip = tonumber(location_data.start_ip)
  8. ip_data.end_ip = tonumber(location_data.end_ip)
  9. ip_data.country_code = tostring(ffi.string(location_data.country_code, tonumber(C.strlen(location_data.country_code))))
  10. ip_data.country_name = tostring(ffi.string(location_data.country_name, tonumber(C.strlen(location_data.country_name))))
  11. ip_data.province_code = tostring(ffi.string(location_data.province_code, ffi.sizeof('char[2]')))
  12. ip_data.province_name = tostring(ffi.string(location_data.province_name, tonumber(C.strlen(location_data.province_name))))
  13. ip_data.city_name = tostring(ffi.string(location_data.city_name, tonumber(C.strlen(location_data.city_name))))
  14. ip_data.detail = tostring(ffi.string(location_data.detail, tonumber(C.strlen(location_data.detail))))
  15. return ip_data,index
  16. end
  17. end

二分法查找:

  1. function _M.search(self, ip)
  2. local start = 0
  3. local location_data
  4. local count = 0
  5. local last = self.dict:get('ip_data_last')
  6. if last == nil then
  7. self:reload()
  8. last = self.dict:get('ip_data_last')
  9. end
  10. last = last and tonumber(last) or 0
  11. if last < 1 then
  12. return nil, 'cannot find the last ip index data'
  13. end
  14. local data, index = self:offset(start, last)
  15. if data == nil then
  16. return nil, 'cannot query the data with the index '..index
  17. end
  18. while location_data == nil do
  19. location_data = data
  20. if ip >= data.start_ip and ip<= data.end_ip then
  21. break
  22. elseif ip < data.start_ip then
  23. last = index
  24. else
  25. start = index
  26. end
  27. data, index = self:offset(start, last)
  28. if data == nil then
  29. break
  30. end
  31. count = count + 1
  32. location_data = nil
  33. if count > 200 then break end
  34. end
  35. if location_data then
  36. return location_data
  37. else
  38. return nil, 'can not find the data'
  39. end
  40. end

四、用法

  1. local iplocation = require 'resty.iplocation.iplocation';
  2. local loc = iplocation:new({path = "/the/path/to/your/project/lib/iplocation/iplocation/ipdat.txt"});
  3. local data = loc:location('202.108.22.5');
  4. if data then
  5. ngx.say(data.country_name)
  6. end
  7. --[[
  8. {
  9. country_code = "CN",
  10. country_name = "中国",
  11. province_code = "BJ",
  12. province_name = "北京",
  13. city_name = "北京",
  14. start_ip = 3396075520,
  15. end_ip = 3396141055,
  16. detail = nil
  17. }
  18. --]]

完整源码和IP库:https://github.com/shixinke/lua-resty-iplocation