自己动手写 Docker 系列 -- 6.1 ip 分配管理
 作者:萧
- 2022 年 4 月 15 日
- 本文字数:3198 字 - 阅读完需:约 10 分钟 
简介
在前面的文章中,我们完成了基本的容器操作,容器已经能在无网络情况下运行,接下来的文章中,将开始网络相关的部分,本地是编写一个 ip 分配工具
源码说明
同时放到了 Gitee 和 Github 上,都可进行获取
本章节对应的版本标签是:5.2,防止后面代码过多,不好查看,可切换到标签版本进行查看
代码实现
在本篇中将实现一个 ip 分配管理工具
主要的思路如下:
1 给定一个网段,能自动分配未使用的 IP 地址
2 能回收 IP 地址,提供给下次使用
IP 地址需要一个存储,目前如书中所说,简单采用字符串进行记录,一个位置就是一个 IP 标识位
IP 地址分配管理
下面的代码包含了 IP 地址的分配,回收和再分配
const ipamDefaultAllocatorPath = "/var/run/mydocker/network/ipam/subnet.json"
// IPAM 存放IP地址分配信息type IPAM struct {  // 分配文件存放位置  SubnetAllocatorPath string  // 网段和位图算法的数组map,key是网段,value是分配的位图数组  Subnets *map[string]string}
// 初始化一个IPAMd对象var ipAllocator = &IPAM{  SubnetAllocatorPath: ipamDefaultAllocatorPath,}
// 加载网段地址分配信息func (ipam *IPAM) load() error {  if _, err := os.Stat(ipam.SubnetAllocatorPath); err != nil {    if os.IsNotExist(err) {      return nil    }    return err  }  subnetConfigFile, err := os.Open(ipam.SubnetAllocatorPath)  defer subnetConfigFile.Close()  if err != nil {    return err  }  subnetJson := make([]byte, 2000)  n, err := subnetConfigFile.Read(subnetJson)  if err != nil {    return err  }
  err = json.Unmarshal(subnetJson[:n], ipam.Subnets)  if err != nil {    return fmt.Errorf("dump allocation info err: %v", err)  }  log.Infof("load ipam file from: %s", subnetConfigFile.Name())  return nil}
// 存储网段地址分配信息func (ipam *IPAM) dump() error {  ipamConfigFileDir, _ := path.Split(ipam.SubnetAllocatorPath)  if _, err := os.Stat(ipamConfigFileDir); err != nil {    if os.IsNotExist(err) {      os.MkdirAll(ipamConfigFileDir, 0644)    } else {      return err    }  }
  subnetConfigFile, err := os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)  defer subnetConfigFile.Close()  if err != nil {    return err  }
  ipamConfigJson, err := json.Marshal(ipam.Subnets)  if err != nil {    return err  }  _, err = subnetConfigFile.Write(ipamConfigJson)  if err != nil {    return err  }  log.Infof("dump ipam file from: %s", ipamConfigFileDir)  return nil}
// Allocate 在网段中分配一个可用的IP地址func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) {  // 存储网段中地址分配信息的数组  ipam.Subnets = &map[string]string{}
  // 从文件中加载已经分配了的网段信息  err = ipam.load()  if err != nil {    return nil, fmt.Errorf("load subnet file err: %v", err)  }
  // net.ipnet.nask.size() 返回网段的子网掩码的总长度和网段前面的固定位的长度  // 比如 127.0.0.0/8 网段的子网掩码是 255.0.0.0  // 返回的是前面255所对应的位数和总位数,即8和24  one, size := subnet.Mask.Size()
  // 如果之前没有分配过这个网段,则初始化网段的分配配置  if _, exist := (*ipam.Subnets)[subnet.String()]; !exist {    // 用0填满这个网段的配置, 1<<uint8(size-one)表示这个网段中有多少个可用的地址    // size - one 是子网掩码后面的网络位数,2^(size-one)(即1<<uint8(size-one))表示可用的IP数    (*ipam.Subnets)[subnet.String()] = strings.Repeat("0", 1<<uint8(size-one))  }
  // 遍历网段的位图数组  for c := range (*ipam.Subnets)[subnet.String()] {    // 找到网段中为0的项和数组序号,即可分配的IP    if (*ipam.Subnets)[subnet.String()][c] == '0' {      // 设置当前的序号值为1,即分配这个IP      ipalloc := []byte((*ipam.Subnets)[subnet.String()])
      // Go中字符串不能修改,通过转成byte数组,再转成字符串赋值      ipalloc[c] = '1'      (*ipam.Subnets)[subnet.String()] = string(ipalloc)
      // 这里的IP为初始IP,比如192.168.0.0/16,这里就是192.168.0.0      ip = subnet.IP
      // 通过网段的IP与上面的偏移相加计算出分配的IP地址,由于IP地址是uint的一个数组      // 需要通过数组中的每一项加所需要的值,比如网段172.16.0.0/12,数组序号是65555      // 那么在[172,16,0,0]上依次加[uint8(65555 >> 24)、[uint8(65555 >> 16)、[uint8(65555 >> 8)、[uint8(65555 >> 8)      // 即[0, 1, 0, 19],那么最后得到的172.17.0.19      for t := uint(4); t > 0; t -= 1 {        []byte(ip)[4-t] += uint8(c >> ((t - 1) * 8))      }
      // 由于IP是从1开始的,所以最后加1      ip[3] += 1      break    }  }
  // 通过dump将分配结果保存到文件中  return ip, ipam.dump()}
// Release 地址释放func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error {  ipam.Subnets = &map[string]string{}
  // 从文件中加载网段分配信息  err := ipam.load()  if err != nil {    return fmt.Errorf("load subnet file err: %v", err)  }
  // 计算IP地址在网段位图数组中的索引位置  index := 0  // 将IP地址转换成4个字节的表现形式  releaseIp := ipaddr.To4()  // 由于IP是从1开始分配的,所以转换成索引应减一  releaseIp[3] -= 1  // 与分配IP相反,释放IP获得索引的方式是将IP地址的每一位相减后分别左移将对应的数值加到索引上  for t := uint(4); t > 0; t -= 1 {    index += int(releaseIp[t-1]-subnet.IP[t-1]) << ((4 - t) * 8)  }  log.Infof("release index: %d", index)
  // 将分配的位图索引中的位置的值置为0  ipalloc := []byte((*ipam.Subnets)[subnet.String()])  ipalloc[index] = '0'  (*ipam.Subnets)[subnet.String()] = string(ipalloc)
  // 保存释放IP后的配置信息  return ipam.dump()}
复制代码
 测试
写一个测试验证下
func TestIPAM_Allocate(t *testing.T) {  // 首先把文件删除了,清理重置下环境  _ = os.RemoveAll(ipamDefaultAllocatorPath)
  // 每次释放和分配ip时,都需要重新调用下面的函数进行IPNet的获取,因为函数调用后,IPNet的值会发生变化  _, ipNet, _ := net.ParseCIDR("192.168.0.0/24")  // 第一次分配  ip1, err := ipAllocator.Allocate(ipNet)  assert.Equal(t, nil, err)  assert.Equal(t, "192.168.0.1", ip1.String())
  // 第二个ip分配  _, ipNet, _ = net.ParseCIDR("192.168.0.0/24")  ip2, err := ipAllocator.Allocate(ipNet)  assert.Equal(t, nil, err)  assert.Equal(t, "192.168.0.2", ip2.String())
  // 释放调第一个IP  _, ipNet, _ = net.ParseCIDR("192.168.0.0/24")  assert.Equal(t, nil, ipAllocator.Release(ipNet, &ip1))
  // 能分配得第一个IP  _, ipNet, _ = net.ParseCIDR("192.168.0.0/24")  ip3, err := ipAllocator.Allocate(ipNet)  assert.Equal(t, nil, err)  assert.Equal(t, "192.168.0.1", ip3.String())
  // 分配第三个IP  _, ipNet, _ = net.ParseCIDR("192.168.0.0/24")  ip4, err := ipAllocator.Allocate(ipNet)  assert.Equal(t, nil, err)  assert.Equal(t, "192.168.0.3", ip4.String())}
复制代码
 划线
评论
复制
发布于: 刚刚阅读数: 3
版权声明: 本文为 InfoQ 作者【萧】的原创文章。
原文链接:【http://xie.infoq.cn/article/dfc9e0bd1a50594220e4feccb】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。

萧
关注
还未添加个人签名 2018.09.09 加入
代码是门手艺活,也是门艺术活










 
    
评论