地址转换函数
上一章中,我们已经了可以分配与初始化各种类型的套接口。这些是由一个常量进行初始化的简单例子。由一个使用变化地址的C字符串设置一个地址需要更多的编程努力。在这一章,我们将会关注建立网络地址的传统问题,以及了解可以在这一领域帮助我们的函数。
在这一章,我们了解到如下内容:
网络地址分类
IP网络掩码
私有的以及保留的IP地址
IP转换函数
然而在我们开始之前,这是一个很好的机会来回顾一下IP地址的设计。这样我们就会更为理解我们将要进行的工作。
网络IP地址
IP地址是由四个十进制数组成的,其中由十进制的点来分隔,通常为点。每一个十进制值以网络字节顺序来表示一个字节的无符号值。在这里我们记住网络字节顺序要求最重要的字节先出现(大端顺序)。
每一个字节都作为一个无符号的8位值。这将每一个字节的值限制在0到255之间。因为这个值是无符号的,这个值不可以是负的,加号也是不允许的。例如,考虑下地址192.168.0.1,网络顺序的第一个字节必须为十进制的192。
当我们看到一个电影在屏幕上显示192.168.300.5的IP地址,我们就会知道这个制作者对于TCP/IP编程了解较少。虽然这个地址在句法上是正确的,但是十进制的300超过了最大的无符号数255。
网络地址分类
网络地址是由下面的两个组件构成的:
网络号(最重要位)
主机号(次重要位)
网络号标识主机可以连接到的网络。主机号标识一个特定网络中的一个主机(例如我们的PC)。
正如我们已经知道的,IP地址是32位的(或者是4个8位字节)。然而,网络号与主机号组件之间的分隔并不是固定的位置。分隔线取决于网络地址的分类,这是由地址的最重要的字节的检测来决定的。下表显示了IP地址是如何分类的:
Table 3.1: Internet Address Classes
Class Lowest Highest Network Bits Host Bits
A 0.0.0.0 127.255.255.255 7 24
B 128.0.0.0 191.255.255.255 14 16
C 192.0.0.0 223.255.255.255 21 8
D 224.0.0.0 239.255.255.255 28 N/A
E 240.0.0.0 247.255.255.255 27 N/A
A,B,C类定义了主机的特定IP地址。对于D类与E类地址,在地址没有主机位可用。D类地址用于多播,其中28位用于描述一个多播组。E类地址的27位保留的。
下图描述了32IP地址的分隔。下图显示的常用的A,B,C类地址:
理解网络掩码
有时我们必须决定一个地址的网络掩码。如是要我们设置我们的网络时这尤为正确。所以,什么是一个网络掩码。
如 果我们将一个IP地址作为32位,网络ID是由地址的最重要的位来标识的。另外,同一个地址的主机ID是由次重要的位来决定的。网络掩码是一个简单的值, 我们可以用来与一个地址进行按位与,从而只保留网络ID。下图显示了IP地址192.168.9.1是如何进行掩码从而得到网络ID位的。
结果就得到了IP地址中表示网络部分的最重要的位,而没有主机ID。下图演示了一个网络掩码如何转换为一个十进制IP地址的:
如果我们必须设置我们的网络,我们就必须确定我们的网络掩码是多少。下表列出A,B,C类地址的网络掩码:
Class Lowest Highest Netmask
A 0.0.0.0 127.255.255.255 255.0.0.0
B 128.0.0.0 191.255.255.255 255.255.0.0
C 192.0.0.0 223.255.255.255 255.255.255.0
有时,在一个网络软件中,我们的软件必须可以分类一个网络地址。有时,这是通过确定一个默认的网络掩码来简单完成的。
下面提供了一个简单的例子程序来演示如何由一个套接口地址分类一个IP地址。
在这个程序中,在一个网络套接口地址结构中设置了四个不同的IP地址。然后对这个地址进行检测与分类。这就演示了如何分类连接到我们服务器的远程客户端的IP地址。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc,char **argv)
{
int x;
struct sockaddr_in adr_inet;
int len_inet;
unsigned msb;
char class;
char *netmask;
static struct
{
unsigned char ip[4];
}addresses[]={
{{44,135,86,12}},
{{127,0,0,1}},
{{172,16,23,95}},
{{192,168,9,1}}
};
for (x=0;x<4;x++)
{
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
memcpy(&adr_inet.sin_addr.s_addr,addresses[x].ip,4);
len_inet = sizeof adr_inet;
msb = *(unsigned char*)&adr_inet.sin_addr.s_addr;
if((msb &0x80) == 0x00)
{
class = 'A';
netmask = "255.0.0.0";
}
else if((msb &0xc0) == 0x80)
{
class = 'B';
netmask = "255.255.0.0";
}
else if((msb &0xe0) == 0xc0)
{
class = 'C';
netmask = "255.255.255.0";
}
else if((msb &0xf0) == 0xe0)
{
class = 'D';
netmask = "255.255.255.255";
}
else
{
class = 'E';
netmask = "255.255.255.255";
}
printf("Address %u.%u.%u.%u is class %c "
"netmask %s/n",
addresses[x].ip[0],
addresses[x].ip[1],
addresses[x].ip[2],
addresses[x].ip[3],
class,
netmask);
}
return 0;
}
通过这个程序例子,我们就可以了解如何来分类一个正在处理的IP地址。
分配IP地址
在前一个例子中,我们已经了解了如何来分类一个IP地址。IP地址是由一个名InterNIC的组织分配给各种个人或是组织的。然而一些范围的IP地址是设置为私有的,而其他的一些保留为特殊的用途。
私有IP地址
通常IP地址必须由InterNICd rs.internic.net进行注册。然而,如果我们的系统并没有直接连接到网络,我们并不需要一个全球唯一的地址。相反,我们可以使用私用的IP地址。
紧随着的第一个问题就是"我们应使用什么IP地址?"。在这一节,我们就会解答这个问题。
RFC 1597是一个描述私有IP地址是如何分配的网络标准文档。下表是一个简要的描述:
Class Lowest Highest Netmask
A 10.0.0.0 10.255.255.255 255.0.0.0
B 172.16.0.0 172.31.255.255 255.255.0.0
C 192.168.0.0 192.168.255.255 255.255.255.0
A,B,C类IP地址的选择在很大程度上取决于单个的网络数量以及我们要建立的主机数。如果网络以及主机数很小,那个一个C类就足够了。相对于,一个A类地址允许一个网络,但是却有大量的主机数目。B类地址提供了大量的网络数与主机数。
保留IP地址
存在大量的保留IP地址,而这些内容位RFC 1166中。作为保留系列地址的一个例子,在下表中将Amateur广播IP地址系列作为例子。现在AX.25协议已经构建进入Linux内核,将会有更多的广播业余爱好者使用这些IP地址。
Class Lowest Highest Netmask
A 44.0.0.0 44.255.255.255 255.0.0.0
处理IP地址
为了简化将字符串格式的IP地址转换为可用的套接口地址的编程负担,提供了一些函数来完成这样的工作。这些函数将会在下面的部分中进行描述。
使用inet_addr(3)函数
我们首先要了解的函数是一个较老的函数,这在新的代码中将不会再使用。然而,我们仍可以在已存在的网络代码中看到这个函数,所以我们应了解他,并且知道他的限制。
inet_addr(3)函数概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_addr(const char *string);
这个函数接受一个输入的C字符串参数string,并且将其解析为一个32位的网络地址值。32位地址值是以网络字节顺序返回的。
如果输入参数string并不是一个可用的地址值,则会返回INADDR_NONE。其他的返回值则代表转换的值。
下面的这个例子程序演示了如何使用这个函数。当这个程序运行时,会将包含一个IP地址的C字符串转换为一个网络顺序的32位IP地址。然后把这个值放入AF_INET套接口地址,并且绑定到这个套接口。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static void bail(const char *on_what)
{
fputs(on_what,stderr);
fputs("/n",stderr);
exit(1);
}
int main(int argc,char **argv)
{
int z;
struct sockaddr_in adr_inet;
int len_inet;
int sck_inet;
sck_inet = socket(AF_INET,SOCK_STREAM,0);
if(sck_inet == -1)
bail("socket()");
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
adr_inet.sin_addr.s_addr = inet_addr("127.0.0.95");
if(adr_inet.sin_addr.s_addr == INADDR_NONE)
bail("bad address");
len_inet = sizeof adr_inet;
z = bind(sck_inet,(struct sockaddr *)&adr_inet,len_inet);
if(z == -1)
bail("bind()");
system("netstat -pa --tcp 2>/dev/null"
" | grep inetaddr");
return 0;
}
程序运行结果如下:
$ ./inetaddr
tcp 0 0 127.0.0.95:9000 *:* CLOSE 992/inetaddr
$
inet_aton(3)函数
inet_aton是一个改进的方法来将一个字符串IP地址转换为一个32位的网络序列IP地址。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *string, struct in_addr *addr);
inet_aton函数接受两个参数。参数描述如下:
1 输入参数string包含ASCII表示的IP地址。
2 输出参数addr是将要用新的IP地址更新的结构。
如果这个函数成功,函数的返回值非零。如果输入地址不正确则会返回零。使用这个函数并没有错误码存放在errno中,所以他的值会被忽略。
对于这个函数有一点迷惑的就是这个函数调用所需要的两个参数。如果我们定义了一个AF_INET套接口地址:
struct sockaddr_in adr_inet;
提供给inet_aton函数调用的参数指针为 &adr_inet.sin_addr
下面这个程序使用inet_aton函数,而不是我们在前面所谈到的in_addr函数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static void bail(const char *on_what)
{
fputs(on_what,stderr);
fputs("/n",stderr);
}
int main(int argc,char **argv)
{
int z;
struct sockaddr_in adr_inet;
int len_inet;
int sck_inet;
sck_inet = socket(AF_INET,SOCK_STREAM,0);
if(sck_inet == -1)
bail("Socket()");
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
if( !inet_aton("127.0.0.1",&adr_inet.sin_addr))
bail("bad address");
len_inet = sizeof adr_inet;
z = bind(sck_inet,(struct sockaddr *)&adr_inet,len_inet);
if(z == -1)
bail("bind()");
system("netstat -pa --tcp 2>/dev/null"
" | grep inetaton");
return 0;
}
程序的运行结果如下:
S$ ./inetaton
tcp 0 0 127.0.0.23:9000 *:* CLOSE 1007/inetaton
使用inet_ntoa(3)函数
有时一个套接口地址代表一个连接到我们服务器的用户的地址,或者是一个UDP包。将一个网络顺序的32位值转换为一个点分隔的IP地址值是不方便的。从而提供了inet_ntoa函数。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr addr);
这个函数仅需要一个输入参数addr。注意struct in_addr是网络套接口地址的一个内部部分。这个地址被转换为函数个内部的的static缓冲区。字符数组的指针作为返回值返回。只有当下一次调用这个函数时结果才会可用。
如果在我们的程序中addr作为一个sockaddr_in结构而存在,那么下面的代码就显示了如何使用inet_ntoa函数来执行这个转换。IP地址转换为一个字符串,并且使用printf函数进行输出。
struct sockaddr_in addr;
printf("IP ADDR: %s/n",
inet_ntoa(addr.sin_addr));
下面提供一个完整的程序例子。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char **argv)
{
struct sockaddr_in adr_inet;
int len_inet;
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
if(!inet_aton("127.0.0.23",&adr_inet.sin_addr))
puts("bad address");
len_inet = sizeof adr_inet;
printf("The IP Address is %s/n",
inet_ntoa(adr_inet.sin_addr));
return 0;
}
这个程序的运行结果如下:
$ ./inetntoa
The IP Address is 127.0.0.23
使用inet_network(3)
有时会有这样的情况,将一个点分隔的IP地址转换为一个32位的主机顺序的值是比较方便的。当我们要执行掩码值从地址中得到主机位或是网络位是更为方便。
inet_network的函数概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_network(const char *addr);
这个函数需要一个在参数addr中包含一个点分隔的地址输入字符串。返回值是IP地址的32位值,但是以主机顺序的格式而存在。然而,如果输入值不合法,返回结果就会为0xFFFFFFFF。
拥有一个主机端顺序的返回值意味着我们可以安全的执行掩码或是位操作。如果返回值为网络端顺序,那么对于不同的CPU平台就会有不同的操作。
下面的例子程序演示了如何使用inet_network函数。下面显示了如何从一个C地址得到一个网络地址:
unsigned long net_addr;
net_addr =
inet_network("192.168.9.1") & 0xFFFFFF00;
赋给net_addr的值应为)0xC0A80900(或者是点分隔的192.168.9.0)。与操作屏蔽掉低8位从而得到网络ID,而没有主机ID。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char **argv)
{
int x;
const char *addr[]={
"44.135.86.12",
"127.0.0.1",
"172.16.23.95",
"192.168.9.1"
};
unsigned long net_addr;
for(x=0;x<4;x++)
{
net_addr = inet_network(addr[x]);
printf("%14s = 0x%08lX net 0x%08lX/n",
addr[x],net_addr,(unsigned long)htonl(net_addr));
}
return 0;
}
程序的运行结果如下:
$ ./network
44.135.86.12 = 0x2C87560C net 0x0C56872C
127.0.0.1 = 0x7F000001 net 0x0100007F
172.16.23.95 = 0xAC10175F net 0x5F1710AC
192.168.9.1 = 0xC0A80901 net 0x0109A8C0
$
使用inet_lnaof(3)函数
inet_lnaof函数将一个包含在套接口地址中的网络字节顺序的IP地址转换为一个主机ID,而没有网络ID。返回值为主机端顺序。
这个函数省去我们确定IP地址然后得到主机ID部分的繁琐操作。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_lnaof(struct in_addr addr);
输入参数必须为我们通常操作所用的套接口地址的struct in_addr成员。这个值必须为网络字节顺序,而这也正是这个函数所要求的。下面的例子演示了如何使用一个sockaddr_in地址为调用这个函数:
struct sockaddr_in addr;
unsigned long host_id;
host_id = inet_lnaof(addr.sin_addr);
下表列出了一些可以应用inet_lnaof函数的值以及返回结果。
IP Number Class Hexadecimal Dotted-Quad
44.135.86.12 A 0087560C 0.135.86.12
127.0.0.1 A 00000001 0.0.0.1
172.16.23.95 B 0000175F 0.0.23.95
192.168.9.1 C 00000001 0.0.0.1
我们可以注意到在上表中A类地址在反回结果中只有第一个字节为0,而B类地址在返回结果高十六位为0.最后C类地址前三个字节为0,只保留最后一位的主机号。
使用inet_netof(3)函数
inet_netof函数是与inet_lnaof函数相对的。inet_netof函数返回网络ID而不是主机ID。在其他方面,这两个函数是相同的。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_netof(struct in_addr addr);
如下面的例子所示:
struct sockaddr_in addr;
unsigned long net_id;
net_id = inet_netof(addr.sin_addr);
下表列出这个函数的一些例子返回值:
IP Number Class Hexadecimal Dotted-Quad
44.135.86.12 A 0000002C 0.0.0.44
127.0.0.1 A 0000007F 0.0.0.127
172.16.23.95 B 0000AC10 0.0.172.16
192.168.9.1 C 00C0A809 0.192.168.9
使用inet_makeaddr(3)函数
使用inet_netof与inet_lnaof函数我们可以得到主机ID与网络ID。要使用网络ID与主机ID重新组合为IP地址,我们可以使用inet_makeaddr函数。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
struct in_addr inet_makeaddr(int net,int host);
这个函数的参数描述如下:
1 net参数是网络ID,右对齐并且是主机端顺序。这也由函数inet_netof函数的返回值相同。
2 host参数是主机ID,主机端顺序。这也由函数inet_lnaof返回值相同。
返回值存放在sockaddr_in套接口地址中的struct in_addr成员中。这个值是网络字节顺序。
下面所演示的例子程序使用了inet_netof,inet_lnaof,inet_makeaddr三个函数。sockaddr_in结构中的IP地址将会被分解为主机ID与网络ID。然后套接口地址清零,并且由刚才得到的网络部分与主机部分重新进行组合。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char **argv)
{
int x;
struct sockaddr_in adr_inet;
const char *addr[] =
{
"44.135.86.12",
"127.0.0.1",
"172.16.23.95",
"192.168.9.1"
};
unsigned long net,hst;
for(x=0;x<4;x++)
{
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
if(!inet_aton(addr[x],&adr_inet.sin_addr))
puts("bad address");
hst = inet_lnaof(adr_inet.sin_addr);
net = inet_netof(adr_inet.sin_addr);
printf("%14s : net=0x%08lx host=0x%08lx/n",
inet_ntoa(adr_inet.sin_addr),net,hst);
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
adr_inet.sin_addr = inet_makeaddr(net,hst);
printf("%14s : %s/n/n",
"inet_makeaddr",
inet_ntoa(adr_inet.sin_addr));
}
return 0;
}
程序的运行结果如下:
$ ./makeaddr
44.135.86.12 : net=0x0000002C host=0x0087560C
inet_makeaddr : 44.135.86.12
127.0.0.1 : net=0x0000007F host=0x00000001
inet_makeaddr : 127.0.0.1
172.16.23.95 : net=0x0000AC10 host=0x0000175F
inet_makeaddr : 172.16.23.95
192.168.9.1 : net=0x00C0A809 host=0x00000001
inet_makeaddr : 192.168.9.1
$