自己动手写 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 加入
代码是门手艺活,也是门艺术活
评论