网络编程-网段计算

Posted by 周思进 on August 25, 2019

问题:知道当前子网的一个ip地址,还有网络号,获取其网段ip地址,怎么计算?

比如IPV4地址为192.168.10.123,网络号为24,则网段ip是192.168.10.0。

比如IPV6地址为fe80:f123:f456:f789:f559:fdc7:af26:a210,其前缀为64,则对应的网段ip是fe80:f123:f456:f789::。


要得到一个网段ip地址,方法就是主机ip地址和其子网掩码进行与操作。

子网掩码的特性就是网络号部分全是1,主机号部分全是0。常见的子网掩码如255.255.255.0,表示前24位是网络号,后8位是主机号。

所以这个问题的核心其实是通过网络号来计算得到对应的子网掩码。

从子网掩码构成来看,左边网络号全是1,右边主机号全是0。该特性比较容易想到通过移位操作来计算。

移位操作,向左移位,则右边补0;向右移位,左边补0(这里只考虑无符号数)。所以子网掩码获取则可以通过将一个全是1的32位无符号数进行左移来实现。

对应实现代码如下:

int get_ipv4_net(void)
{
	int ipv4_bits = 32;
	int net_bits = 24;

	// 需要注意本地存储转换字节序
	uint32_t ip = ntohl(inet_addr("192.168.10.123"));
	uint32_t netmask = 0;
	uint32_t ip_net = 0;
	struct in_addr addr;

	// 计算操作后以本地主机序存储
	netmask = 0xfffffffff << (ipv4_bits - net_bits);
	ip_net = ip & netmask;

	// 需要转换成网络字节序处理
	addr.s_addr = htonl(ip_net);
	printf("ip_net:%s\n", inet_ntoa(addr));

	return 0;
}

上面的难点个人觉得主要在于理清是否要进行字节序转换,这个后面单独写文章梳理大小端问题。

初版实现将几个地址都存储到无符号整型变量中,这和正常的地址表示方式不符,遂进行了修改如下:

int get_ipv4_net_v2(void)
{
	int ipv4_bits = 32;
	int net_bits = 24;
	struct in_addr ip = {0};
	struct in_addr netmask = {0};
	struct in_addr ip_net = {0};

	ip.s_addr = inet_addr("192.168.10.123");

	netmask.s_addr = htonl(0xfffffffff << (ipv4_bits - net_bits));
	ip_net.s_addr = ip.s_addr & netmask.s_addr;

	printf("ip_net:%s\n", inet_ntoa(ip_net));

	return 0;
}

这样代码相对易于理解,且只需要将移位操作计算结果进行网络字节序转换存储到网络地址结构体中。

这里记一点,网络结构体存储的地址形式都应该是网络字节序存储,这样网络相关接口使用就不需要做字节序转换处理,网络接口说明里都会要求是以网络字节序存储。


上面代码只解决了IPV4网段计算,那IPV6呢?
可惜IPV6并不像IPV4一样,可以有个32位的整型变量进行移位操作(大部分系统不支持),没有对应128位的整型变量。

先对ipv6地址定义有个了解,如下:

/*
 * IPv6 address
 */
typedef struct in6_addr {
	union {
		__uint8_t   __u6_addr8[16];
		__uint16_t  __u6_addr16[8];
		__uint32_t  __u6_addr32[4];
	} __u6_addr;                    /* 128-bit IP6 address */
} in6_addr_t;

#define s6_addr   __u6_addr.__u6_addr8  // 这种处理方式可以学习
#define s6_addr16 __u6_addr.__u6_addr16
#define s6_addr32 __u6_addr.__u6_addr32

#define INET6_ADDRSTRLEN        46

这里就需要用到常用的memset接口了,该接口日常工作都是用来初始化变量做清0操作。实际上该接口可以将指定字节数内容初始化为指定字节,比如:

memset(buf, 0xff, MIN(sizeof(buf), 64));

上述代码就是将buf至多前64个字节的每个字节都初始化为0xff。

memset操作的基本单位是字节,而网络号可能不并是8位的整数倍,那么对于多出来的那几位需要单独处理,剩下的字节就置成0。实现方式如下:

int get_ipv6_net(void)
{
	int net_bits = 69;
	struct in6_addr addr = {0};
	struct in6_addr netmask = {0};
	char ipv6_netmask[INET6_ADDRSTRLEN] = {0};
	char ipv6_net[INET6_ADDRSTRLEN] = {0};
	int  i = 0;

	// 低字节存放高位,所以netmask已经是大端存储了,后面不需要做字节序转换
	memset(netmask.s6_addr, 0xff, net_bits/8);

	// 注意这里数组下标是69/8,而不是(69+8)/8; 移位数是 8-69%8,而不是69%8
	netmask.s6_addr[net_bits/8] = (char)(0xff << (8 - net_bits % 8));

	// inet_ntop支持IPV6
	inet_ntop(AF_INET6, &netmask, ipv6_netmask, sizeof(ipv6_netmask));	

	printf("ipv6_netmask	%s\n", ipv6_netmask);

	// 得到IPV6掩码地址后,进行与操作计算得到ipv6网段地址
	inet_pton(AF_INET6, "fe80:f123:f456:f789:f559:fdc7:af26:a210", &addr);

	for (i = 0; i < 4; i++)
	{
		addr.s6_addr32[i] &= netmask.s6_addr32[i];
	}

	inet_ntop(AF_INET6, &addr, ipv6_net, sizeof(ipv6_net));	

	printf("ipv6_net	%s\n", ipv6_net);

	return 0;
}

回顾:
1、inet_addr返回的地址是网络字节序,inet_ntoa操作的入参需是网络字节序,这些网络接口都需要注意,目前来看网络编程接口都是操作的网络字节序数据,如不清楚可以通过man手册进行查看。

2、结构体成员变量如果取值操作路径过长,可以像IPV6地址结构体那样宏定义方式实现。