xm_ip_service
相关技术栈:golang、MySQL、PostgreSQL、linux、Web
项目需求
通过IP地址(目前仅支持IPV4),进行IP地址相关地理信息的查询,查询过程分为,国内信息查询与国际信息查询两个查询方式。
接口定义
接口内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| struct GeoInfo { 1: string ip, 2: string geoip_country_code, 3: string geoip_country_name, 4: string geoip_city_code, 5: string geoip_city_name, 6: i32 error_no, 7: string geoip_province_code, 8: string geoip_province_name, 9: string geoip_carrier, 10: string error_msg, 11: i64 geoip_start_ip, 12: i64 geoip_end_ip }
struct GeoRequest { 1: list<string> ipList, 2: string lang }
service IPService { list<GeoInfo> queryGeoInfoByIP(1:list<string> ipList);
list<GeoInfo> queryGeoInfo(1:GeoRequest req);
list<GeoInfo> queryI18nGeoInfoByIP(1:list<string> ipList); }
|
业务逻辑说明
hook.go
描述:
定义并初始化了IP相关地理信息的原始数据结构体,并从文件当中读取所需地理信息数据,给其赋值,该结构体作为IP地理信息参照表,亦或查询表。
流程:
在加载配置文件之后,服务正式运行之前,通过New函数初始化该地理信息结构体;
然后,通过Open函数,读取配置里的路径信息,将该路径下文件里的地理信息通过信息的处理,依次读取到GeoModel的中文地理信息(cnGeos)和英文地理信息(enGeos)两个切片中,此时的GeoModel就成为了存储有完整IP相关地理信息的一张参照表;
GeoI18nModel类似,不过后者用作存储国际化的全球IP地理信息,没有中英文的区分,而是利用了命名为Names的Map基本结构扩展了所有所需的语言包,所以无需特意进行语言的区分,查询时打上语言标识即可。
IPservice.go
定义新建控制器。
main.go
描述:
定义并初始化了控制器及其进程,以及hook函数的定义以及运行。
Handler
描述:
IP_Service_Handler |
功能 |
queryGeoInfo_Handler |
通过发来的请求里包含的IP地址,参照GeoModel查询对应的地理信息; 如果查询不到或地理位置不在中国,则交给GeoI18nModel来继续查询对比相关的地理信息。 进行合适的日志处理之后,打包返回相关地理信息以及错误信息。 |
queryGeoInfoByIP_Handler |
与上面不同的是,输入当中直接为IP_list,并没有相关的中英文标识,语言统一预设为中文识别, 当查询到地理信息不是中国的时候,则交给GeoI18nModel来继续查询对比相关的地理信息。 |
queryI18nGeoInfoByIP_Handler |
与第二条相同,输入也直接为IP_list,不同的是,查询语言预设为英文。 并且是国际IP地理信息的查询服务,所以并不区分是否在“中国”国内。 |
流程:
见Model里的业务逻辑描述。
Model
定义了Geo和GeoModel结构体。及其New函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| type Geo struct { startIp uint32 endIp uint32 countryName string provinceName string cityName string districtName string carrierName string }
type GeoModel struct { enDataPath string cnDataPath string cnGeos []*Geo enGeos []*Geo }
|
- open函数用来从文件中读取IP地理信息到geoModel结构体。该函数在hook.go中调用,用来初始化参照表GeoModel。
- charToLong和charTolittle函数用来将IP地址转化为整数(ipValue),好进行区间比较,以便找到IP的归属地。
- search函数和compare函数结合起来,找到ipValue的对应区间,用来查找IP的归属地。
- GetInfoByIP函数则整合以上代码,进行查表,将一个IP地址信息,和其语言标识,来查询对应的地理信息表单,返回一个地理信息和相关错误信息。
定义了Geo和GeoModel结构体。及其New函数。
1 2 3 4 5
| type GeoI18nModel struct { dataPath string Data *geoip2.Reader }
|
- open函数用来从文件中读取IP地理信息到GeoI18nModel结构体。该函数在hook.go中调用,用来初始化参照表GeoI18nModel。
- GetI18nInfoByIP函数,进行查表操作,查询国际IP地理信息表单,将一个IP地址信息进行解析,然后查询IP地址的所属城市信息,然后依次赋值给需要返回的单个地理信息,最好返回该地理信息和相关错误信息。
重构项目
对这个项目进行重构,数据存储层迁移到 PG, 功能上支持 ipv6
数据存储层迁移到PgSQL
此处数据存储层指的地理位置信息的参照表,原本是以data
形式存储在代码包文件当中的,如下。
需要将该文件存储到PG中,以查表的方式来查询IP_geoInfo。
所以需要设计两张表来分别存储这两个文件。命名分别以ip_service_cn/en命名。
设计结果如下:
其中注意
1 2
| nextval('ip_service_cn_id_seq'::regclass) nextval('ip_service_en_id_seq'::regclass)
|
1
| INSERT INTO public.ip_service ( start_ip , end_ip , country_name , province_name , city_name , district_name , carrier_name) VALUES (16777216,16777471,'澳大利亚','','','',''),(16777472,16778239,'中国','福建','','','电信')
|
1 2 3 4
| TRUNCATE TABLE ip_service_en; TRUNCATE TABLE ip_service_cn; ALTER SEQUENCE ip_service_cn_id_seq RESTART WITH 1; ALTER SEQUENCE ip_service_en_id_seq RESTART WITH 1;
|
因为查询IP_GEOINFO时,最主要是通过比对ip所处的区间,来得知,查询的IP属于哪一个地址范围,且,start_ip,end_ip有明显的连续性,所以可以由此建立索引。建表语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| CREATE TABLE "public"."ip_service_en"( "id" serial8 NOT NULL PRIMARY KEY, "create_time" timestamptz(6) NOT NULL DEFAULT now(), "update_time" timestamptz(6) NOT NULL DEFAULT now(), "start_ip" int8 NOT NULL DEFAULT 0, "end_ip" int8 NOT NULL DEFAULT 0, "country_name" varchar(100) NOT NULL DEFAULT '', "province_name" varchar(100) NOT NULL DEFAULT '', "city_name" varchar(100) NOT NULL DEFAULT '', "district_name" varchar(100) NOT NULL DEFAULT '', "carrier_name" varchar(100) NOT NULL DEFAULT '' ); CREATE INDEX "idx_iprange_en" ON "public"."ip_service_en" (start_ip,end_ip); CREATE TABLE "public"."ip_service_cn"( "id" serial8 NOT NULL PRIMARY KEY, "create_time" timestamptz(6) NOT NULL DEFAULT now(), "update_time" timestamptz(6) NOT NULL DEFAULT now(), "start_ip" int8 NOT NULL DEFAULT 0, "end_ip" int8 NOT NULL DEFAULT 0, "country_name" varchar(100) NOT NULL DEFAULT '', "province_name" varchar(100) NOT NULL DEFAULT '', "city_name" varchar(100) NOT NULL DEFAULT '', "district_name" varchar(100) NOT NULL DEFAULT '', "carrier_name" varchar(100) NOT NULL DEFAULT '' ); CREATE INDEX "idx_iprange_cn" ON "public"."ip_service_cn" (start_ip,end_ip);
|
因为涉及到的数据量有近五百万条IP地址段,所以需要考虑插入的效率问题。于是结合了两个方案:
1 2 3 4 5 6 7
| var wg sync.WaitGroup wg.Add(1) go NewInsert(p, "en", &wg) wg.Add(1) go NewInsert(p, "cn", &wg) wg.Wait() Db.Close()
|
- 结合sql语句,1000条数据提交一次。(见上文PG插入多个数据的语句格式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| func NewInsert(p *GeoModel, tableName string, wg *sync.WaitGroup) {
var data, sql, sqlHead, sqlData, oneData string sqlHead = "INSERT INTO public.ip_service_%s ( start_ip , end_ip , country_name , province_name , city_name , district_name , carrier_name)" sqlData = "(%v,%v,'%v','%v','%v','%v','%v')"
if tableName == "en" {
for k, vousmevoyez := range p.enGeos {
if vousmevoyez.StartIP != 0 && vousmevoyez.EndIP != 0 {
oneData = fmt.Sprintf(sqlData, v.StartIP, v.EndIP, v.CountryName, v.ProvinceName, v.CityName, v.DistrictName, v.CarrierName)
if k%1000 != 0 && k < len(p.cnGeos) { data += oneData data += "," } else { data += oneData sql = sqlHead + " VALUES " + data fmt.Println(tableName + "***********:**********" + strconv.Itoa(k)) _, err := Db.Exec(sql) if err != nil { fmt.Println(sql, err) TableHandler(0) return } } }
if k%1000 == 0 { data = "" } } } else { } wg.Done() return }
|
其间碰到的一个坑:在英文IP地理信息中,名称中有的时候有单引号如Yi'an City
这个时候放在sql语句中就会报错,这个时候,如果想要在数据库中正确插入单引号,就要把一个单引号变成两个。所以,增加了一个排查单引号的功能,例子如下:
1 2 3 4 5 6
| if len(tokens) >= 3 { geo.CountryName = tokens[2] if strings.Contains(geo.CountryName, "'") { geo.CountryName = strings.Replace(geo.CountryName, "'", "''", -1) } }
|
后续只需要将源代码中关于geoModel的结构体做一次重新的适配,搜索的入口做一个重定向就行了,这里不做赘述。
如何支持IPV6
由于Leader说了,I18N的重构暂时不需要管IPV6的模块,所以就不动,并且,现阶段只需要做出一个能用的IPV6地址查询即可,于是,我首先要下载数据源,然后做地理查询前,同样要先将数据源转移到PG,然后再以类似的逻辑做地理查询,问题的难点在于,数据如何处理,和如何转移数据,以及数据库如何设计等等。
IPV6地理信息数据集的特性