网络真的是一个很复杂很复杂的东西。网络编程更是相当复杂,尽管它很复杂,也不过是由人发明的,基础就在那,就看你学不学。
我们使用的网络目前都是 IPv4 (IP version 4) 版本的,所以使用到的 IP 地址也是第 4 版本的,IPv6 版本的不是不讲,是我也还没用过!!!所以,这个博客涉及到的所有的网络编程,都是基于 IPv4 的东东。在学习 IPv4 前,掌握 ip 地址相关的知识那是必须的必。(千万别问我 ip 地址是干嘛的!!!^_^)
1. IP 地址
网络编程中,各种各样表示 ip 地址的类型让人眼花缭乱,所以必须得梳理一下!
1.1 in_addr_t 还是 in_addr?
IPv4 地址,本质上是 32 位无符号整数。在 Linux 操作系统中,使用了 in_addr_t 类型来表示这样一个 4 字节的整数,它是由 typedef 定义的。
typedef uint32_t in_addr_t;
in_addr_t 这个类型保存的数据,到底是按本机字节序保存的,还是网络字节序保存的,这是不确定的!!!
为了解决此问题,Linux 又定义了一个新的结构体类型 struct in_addr,它明确的表示,它保存的 ip 地址就是网络字节序的!!!
struct in_addr {
in_addr_t s_addr;
}
如果说你传了一个本机字节序的 unsigned int 类型的整数给 in_addr 的 s_addr 成员,很抱歉,后面使用到该结构体的函数都会出错。
2. ip 地址表示方法
在第 1 节中也说了,ip 地址实际上就是一个 unsigned int 类型的整数。比方说接下来你和你同学说,你把你的 ip 地址配置成 0xc0a8a605
吧……你同学可能觉得你是从火星来的。。。
所以说,生活中使用整数来表示 ip 地址是相当不方便的,我们平时实际上都是用“点分十进制(dotted-decimal)”这种表示方法,比如 192.168.1.1
等等类似这样的。
点分十进制表示的 ip 地址和整数表示法之间的转换相当容易,就比如说 0xc0a8a605
,先将其拆分成 4 个字节:c0.a8.a6.05
,然后将 4 个部分的 16 进制数变成 10 进制,即 192.168.166.5
,是不是很容易?
3. 制作 in_addr 类型的地址
在掌握了第 2 节中的 ip 地址表示方法后,相信你会非常容易创建一个 in_addr 类型的地址。比方我们需要把 192.168.166.5 保存成 in_addr 类型的地址,编写下面的代码就行了:
struct in_addr addr;
addr.s_addr = htonl(0xc0a8a605);
我知道有同学又要吐槽了,好麻烦!!!还得自己先把点分十进制转成整数,然后再把整数转成网络字节序,是不是心中有一万头草泥马在狂奔……
还好,Linux 提供一些函数,能够相当方便的帮我们把点分十进制的 ip 地址转换成 in_addr 类型。
4. 和 ip 地址转换相关的函数
下面这些函数中,aton 中的 a(address) 表示点分十进制的地址,n 表示网络字节序地址,ntoa 类似。
函数 lnaof 表示 local network address part of the Internet address in,即主机号。
函数 netof 表示 network number part of the Internet address in,即网络号。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 将点分十进制直接转换成 in_addr 类型,我们很喜欢这个函数
int inet_aton(const char *cp, struct in_addr *inp);
// 将点分十进制转换成 in_addr_t 类型,返回值保存的是网络字节序
in_addr_t inet_addr(const char *cp);
// 将点分十进制转换成 in_addr_t 类型,返回值保存的是本机字节序
in_addr_t inet_network(const char *cp);
// 将 in_addr 地址转换成点分十进制,注意这个函数不是线程安全的
char *inet_ntoa(struct in_addr in);
// 根据网络号和主机号制作 in_addr 类型地址
struct in_addr inet_makeaddr(int net, int host);
// 返回 ip 地址的主机号,返回的结果是本机字节序的
in_addr_t inet_lnaof(struct in_addr in);
// 返回 ip 地址的网络号,返回的结果是本机字节序的
in_addr_t inet_netof(struct in_addr in);
5. 相关程序封装
这里的实验是相当有意思的,我把每个函数都单独写在了不同程序中,程序的名字就是函数的名字。每个程序都通过标准输入和标准输出接收参数和打印结果,如此一来,不同的程序就可以通过管道互相连接啦。
5.1 htonl 程序
// htonl.c
#include <arpa/inet.h>
#include <stdio.h>
int main() {
int host;
scanf("%x", &host);
printf("0x%08x\n", htonl(host));
return 0;
}
5.2 inet_aton 程序
// inet_aton.c
#include <arpa/inet.h>
#include <stdio.h>
int main() {
struct in_addr addr;
char ip[16] = { 0 };
scanf("%s", ip);
if (inet_aton(ip, &addr) == 0) {
fprintf(stderr, "Invalid address\n");
return 1;
}
printf("0x%08x\n", addr.s_addr);
return 0;
}
5.3 inet_addr 程序
// inet_addr.c
#include <arpa/inet.h>
#include <stdio.h>
int main() {
char ip[16] = { 0 };
scanf("%s", ip);
printf("0x%08x\n", inet_addr(ip));
return 0;
}
5.4 inet_network 程序
// inet_network.c
#include <arpa/inet.h>
#include <stdio.h>
int main() {
char ip[16] = { 0 };
scanf("%s", ip);
printf("0x%08x\n", inet_network(ip));
return 0;
}
5.5 inet_lnaof 程序
// inet_lnaof.c
#include <arpa/inet.h>
#include <stdio.h>
int main() {
struct in_addr addr;
addr.s_addr = 0;
scanf("%x", &addr.s_addr);
printf("0x%08x\n", inet_lnaof(addr));
return 0;
}
5.6 inet_netof 程序
// inet_netof.c
#include <arpa/inet.h>
#include <stdio.h>
int main() {
struct in_addr addr;
addr.s_addr = 0;
scanf("%x", &addr.s_addr);
printf("0x%08x\n", inet_netof(addr));
return 0;
}
5.7 inet_ntoa 程序
// inet_ntoa.c
#include <arpa/inet.h>
#include <stdio.h>
int main() {
struct in_addr addr;
addr.s_addr = 0;
scanf("%x", &addr.s_addr);
printf("%s\n", inet_ntoa(addr));
return 0;
}
5.8 inet_makeaddr 程序
// inet_makeaddr.c
#include <arpa/inet.h>
#include <stdio.h>
int main() {
int net, host;
struct in_addr addr;
scanf("%x %x", &net, &host);
addr = inet_makeaddr(net, host);
printf("0x%08x\n", addr.s_addr);
return 0;
}
5.9 Makefile 文件
main:inet_addr inet_aton inet_network inet_lnaof inet_netof inet_ntoa inet_makeaddr htonl
inet_addr:inet_addr.c
gcc inet_addr.c -o inet_addr
inet_aton:inet_aton.c
gcc inet_aton.c -o inet_aton
inet_network:inet_network.c
gcc inet_network.c -o inet_network
inet_lnaof:inet_lnaof.c
gcc inet_lnaof.c -o inet_lnaof
inet_netof:inet_netof.c
gcc inet_netof.c -o inet_netof
inet_ntoa:inet_ntoa.c
gcc inet_ntoa.c -o inet_ntoa
inet_makeaddr:inet_makeaddr.c
gcc inet_makeaddr.c -o inet_makeaddr
htonl:htonl.c
gcc htonl.c -o htonl
6. 实验
把第 5 节中的所有程序复制到你的机器中,然后执行 make
命令就可以一次全部编译完成。
图1 使用 make 生成程序
这里我们需要一个文件 ip.txt,这个文件用下面的命令创建:
$ echo "192.168.166.5" > ip.txt
6.1 将点分十进制转换成网络字节序
有两个函数可以做这件事,如图 2.
图2 将点分十进制转换成网络字节序的地址
这两个函数是有区别的,inet_addr 函数如果出错会返回 -1,即 0xffffffff
,因此地址 255.255.255.255
是无法转换的,虽然将此地址转换成的结果也是 0xffffffff
,但是错误的 ip 地址比如 300.200.200.200
转换的结果也是 0xffffffff
,所以 inet_addr 在不十分严谨的情况下才会使用。
inet_aton 是比较正式的,它单独使用返回值判断转换的地址是否有效,如果返回为 0,表示转换的地址是错误的。
图3 演示非法地址的转换情况
6.2 将点分十进制转换成本机字节序
图4 将点分十进制转换成本机字节序
6.3 将网络字节序的地址转换成点分十进制
图5 将网络字节序的地址转换成点分十进制
上面的过程表示:先由程序 inet_addr 将 ip.txt 中的地址转换成网络字节序的整数,再通过管道送入程序 inet_ntoa.
6.4 计算网络字节序 ip 的主机号和网络号
注意,这两个函数返回的结果都是本机字节序。
图6 计算网络字节序 ip 的主机号和网络号
6.5 根据网络号和主机号制作一个 in_addr 类型地址
图7 根据网络号和主机号制作一个 in_addr 类型地址
7. 总结
- in_addr_t 和 in_addr 的区别和联系
- in_addr 是网络字节序的
- in_addr_t 是什么字节序是不确定的
- 掌握相关的转换函数
- inet_addr 和 inet_aton 的区别
- inet_addr 和 inet_network 的区别
有关网络号和主机号,以及更多有关 ip 地址的知识,请参考谢希仁的《计算机网络》第 5 版。