IP库-IP段生成Trie树过程的剪枝实现

背景

上篇文章分析了ip2region的结构和实现,相对简单。且ip2region的方式仅仅支持IPv4,无法支持IPv6。

APP需支持IPv6 访问,所以需要研究下怎样能同时支持IPv4和IPv6的高并发访问。

查找资料的过程中发现多篇论文提到Trie树,结合之前实现禁词库的经验,深入研究之后发现可行。该处就将IP段生成Trie树过程的剪枝实现进行简单的讨论。

实现

前置知识

前置知识需自行百度,此处不表。

  • IPv4/IPv6基础
  • IPv4转为IPv6
  • 位运算知识
  • Trie树的基础

Trie树和IP映射

IPv6是128位(此处IPv4转化为IPv6),即有128位0或1构成。

例如:

####IPV4
1.92.255.255    转换为IPV6       => ::ffff:015c:ffff
#二进制 为了方便展示 每组64位
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000111111111111111100000001010111001111111111111111


####IPV6
2001:250:6031:ffff:ffff:ffff:ffff:ffff  
#二进制 为了方便展示 每组64位
0010000000000001000000100101000001100000001100011111111111111111
1111111111111111111111111111111111111111111111111111111111111111

可以将所有IP看成一个深度129(根节点为独立节点)的Trie树。

暴力生成IP库

IPv4的数量约42.9亿,IPv6的数量。。。(此处写不下)。如果使用穷举的方式,那生成的Trie树将是任何计算机都不能完成的。

IP段优化

通过对IP地址信息的观察发现,大量的相邻 IP 拥有相同的位置信息,也就是按照IP段进行出现的。

例如:

1.0.0.0  1.0.0.255

我们截取其中一段IP 1.0.0.2  1.0.0.15,生成Trie树进行观察。

将startIP (1.0.0.2),endIP (1.0.0.15)生成Trie树。前面相同节点都省略,只保留不相同的部分。

左边标黑数字为 00010,代表startIP 1.0.0.2;

右边标黑数字为01111,代表endIP 1.0.0.15。

观察上图就会发现在黄色填充的节点内记录数据,即可完整覆盖到startIP(1.0.0.2)和endIP(1.0.0.15)之间的所有IP。

我们假定每个IP段内都存在少量特定的节点就可以完整的覆盖整个IP段,那么现在我们需要用程序找出这些节点,进行相关的剪枝操作就可以大大减小树的体积。

逻辑流程图

代码实现


//处理数据
func deal(startIP, endIP string) error {
  startIPByte := net.ParseIP(startIP)
  if startIPByte == nil {
    return errors.New(fmt.Sprintf("invalid startIP:%s --deal", startIP))
  }
  startIPSet := IP2Bit(startIPByte)
  fmt.Println(fmt.Sprintf("%s startIPSet", startIPSet.DumpAsBits()))

  //结束IP
  endIPByte := net.ParseIP(endIP)
  if endIPByte == nil {
    return errors.New(fmt.Sprintf("invalid endIP:%s --deal", endIP))
  }
  endIPSet := IP2Bit(endIPByte)
  fmt.Println(fmt.Sprintf("%s endIPSet", endIPSet.DumpAsBits()))
  return dealRow(startIPSet, endIPSet)
}

//dealRow 处理单行数据
func dealRow(startIPSet, endIPSet *bitset.BitSet) error {
  tStartIPSet := startIPSet.Clone()
  //比较大小
  cap0 := tStartIPSet.Cap128(endIPSet)
  //当startIP>endIP,直接报错返回,结束
  if cap0 == 1 {
    return errors.New(fmt.Sprintf("invalid startIP:%s --dealRow", "111"))
  }
  //当startIP==endIP
  if cap0 == 0 {
    //startIP的右1位写入,结束。
    fmt.Println(fmt.Sprintf("%s 的从右到左第 %d 位", tStartIPSet.DumpAsBits(), 1))
    return nil
  }
  //当startIP<endIP,继续逻辑。
  isIBitLatest := tStartIPSet.Test(0)
  if isIBitLatest {
    //最后一位为1
    //tStartIPSet的右1位写入,继续。
    fmt.Println(fmt.Sprintf("%s 的从右到左第 %d 位", tStartIPSet.DumpAsBits(), 1))

    //newStartIPSet=startIPSet+1
    newStartIPSet := tStartIPSet.Incr128()
    if newStartIPSet == nil {
      return errors.New(fmt.Sprintf("invalid newStartIP:%s  --dealRow", "222"))
    }
    err := dealRow(newStartIPSet, endIPSet)
    if err != nil {
      return err
    }
  } else {
    //最后一位为0
    //startIP从右到左第一位非0位,非下标
    m := tStartIPSet.FirstSet()
    err := dealRow0(tStartIPSet, endIPSet, m)
    if err != nil {
      return err
    }
  }
  return nil
}

//dealRow0 128位为0的处理逻辑
//m 为从右到左的位数,非下标
func dealRow0(startIPSet, endIPSet *bitset.BitSet, m uint) error {
  tStartIPSet := startIPSet.Clone()
  //特殊处理
  if m == 0 {
    m = 129
  }
  //m-2得到需要处理的最大下标 [0,m-1)
  tIPSet := tStartIPSet.FlipRange(0, m-1)
  cap1 := tIPSet.Cap128(endIPSet)
  //相等
  if cap1 == 0 {
    //tIP的右m位写入,结束。
    fmt.Println(fmt.Sprintf("%s 的从右到左第 %d 位", tIPSet.DumpAsBits(), m))
    return nil
  }
  //大于
  if cap1 == 1 {
    err2 := dealRow0(startIPSet, endIPSet, m-1)
    if err2 != nil {
      return err2
    }
  }
  //小于
  if cap1 == -1 {
    //tIP的右m位写入,继续。
    fmt.Println(fmt.Sprintf("%s 的从右到左第 %d 位", tIPSet.DumpAsBits(), m))

    //newStartIPSet=tIPSet+1
    newStartIPSet := tIPSet.Incr128()
    if newStartIPSet == nil {
      return errors.New(fmt.Sprintf("invalid tIP:%s", "333"))
    }
    err1 := dealRow(newStartIPSet, endIPSet)
    if err1 != nil {
      return err1
    }
  }

  return nil
}

校验

假定测试IP段为startIP (1.0.0.2),endIP(1.0.0.15)。

func Test_deal(t *testing.T) {
  startIP := "1.0.0.2"
  endIP := "1.0.0.15"
  err := deal(startIP, endIP)
  if err != nil {
    t.Log(err)
  }
}

输出如下:

程序输出结果和上图Trie树结果相同(此处可以使用多组数据自行验证)。

Trie树剪枝通过。

总结

使用剪枝后的IPv4和IPv6全量数据文件可以控制在15MB以内,效果显著。

本文是在现有业务需要的背景下进行的研究,仅能满足当下业务的需求。但是其还有很多方面可以进行更加深入的研究。例如在提供的IP数据为单条非连续数据时,应该怎样进行剪枝等等。欢迎大家一起探讨学习。

备注

1、bitset库扩展

在进行扩展使用时,一定要注意库的开源协议。

https://github.com/bits-and-blooms/bitset

//FirstSet b最后为1的所在位置(非下标),从右到左
func (b *BitSet) FirstSet() uint {
  panicIfNull(b)
  for i := uint(0); i < b.length; i++ {
    if b.set[i>>log2WordSize]&(1<<wordsIndex(i)) != 0 {
      return i + 1
    }
  }
  return 0
}

//Incr128 增加1
func (b *BitSet) Incr128() *BitSet {
  panicIfNull(b)
  if (b.set[0] & allBits) == allBits {
    //低位已经为最大数,则设置为0
    b.set[0] = 0
  } else {
    //低位未为最大数,则设置+1;并直接返回b
    b.set[0] += 1
    return b
  }
  if (b.set[1] & allBits) == allBits {
    //低位已经为最大数,则设置为0
    return nil
  } else {
    //低位未为最大数,则设置+1;并直接返回b
    b.set[1] += 1
    return b
  }
}

// Cap128 128位对比b和c的大小。b>c返回1;b==c 返回0;b<c返回-1。
func (b *BitSet) Cap128(c *BitSet) int {
  panicIfNull(b)
  panicIfNull(c)
  if b.set[1] > c.set[1] {
    return 1
  } else if b.set[1] < c.set[1] {
    return -1
  } else {
    if b.set[0] > c.set[0] {
      return 1
    } else if b.set[0] < c.set[0] {
      return -1
    } else {
      return 0
    }
  }
}

2、IP转换为bit

//IP2Bit IP转换为bit
func IP2Bit(IP net.IP) *bitset.BitSet {
  IPBitSet := bitset.New(128)
  for i := 0; i < 128; i++ {
    //该IP在当前位上的值
    iBit := ((0xFF & int(IP[i>>3])) >> uint(7-(i%8))) & 1
    //计算当前位的位置
    off := uint(128 - 1 - i)

    //初始化数字设置位
    if iBit == 1 {
      IPBitSet.Set(off)
    }
  }
  return IPBitSet
}

3、IP信息

IPv4

1.0.0.0  1.0.0.255
1.68.113.27  1.68.113.83
1.68.113.84  1.68.113.84
1.68.113.85  1.68.114.15

IPv6

2001:250:6415::  2001:250:6415:ffff:ffff:ffff:ffff:ffff
2001:250:6416::  2001:250:6416:ffff:ffff:ffff:ffff:ffff
2001:250:6417::  2001:250:641a:ffff:ffff:ffff:ffff:ffff
2001:250:641b::  2001:250:641b:ffff:ffff:ffff:ffff:ffff
展示评论