文章目录
1.将字符串信息转换为网络字节序的整数型
1.1 inet_addr 函数
sockaddr_in
中保存地址信息的成员为
32
32
32 位整数型。因此,为了分配IP地址,需要将其表示为
32
32
32 位整数型数据。但是,对于IP地址的表示,我们熟悉的是点分十进制表示法(Dotted Decimal Notation),而非整数型数据表示法。幸运的是,inet_addr
函数会帮我们将字符串形式的IP地址转换成
32
32
32 位整数型数据。inet_addr
函数在转换类型的同时进行网络字节序转换。
#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);
// 成功时返回32位大端序整数型值,失败时返回INADDR_NONE
// 返回值类型in_addr_t在内部声明为32位整数型
下面通过示例代码 inet_addr.c 说明该函数的调用过程:
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
char *addr1 = "1.2.3.4";
// 1个字节能表示的最大整数为255,也就是说,它是错误的IP地址。
// 利用该错误地址验证inet_addr函数的错误检测能力。
char *addr2 = "1.2.3.256";
// 函数正常调用
unsigned long conv_addr = inet_addr(addr1);
if (conv_addr == INADDR_NONE)
printf("Error occured!\n");
else
printf("Network ordered integer addr: %#lx\n", conv_addr);
// 函数调用出现异常
conv_addr = inet_addr(addr2);
if (conv_addr == INADDR_NONE)
printf("Error occured!\n");
else
printf("Network ordered integer addr: %#lx\n", conv_addr);
return 0;
}
编译运行:
gcc inet_addr.c -o addr
./addr
输出结果:
Network ordered integer addr: 0x4030201
Error occured!
从运行结果可以看出,inet_addr
函数不仅可以把IP地址转成
32
32
32 位整数型,而且可以检测无效的IP地址。另外,从输出结果可以验证确实转换为网络字节序。
1.2 inet_aton 函数
inet_aton
函数与 inet_addr
函数在功能上完全相同,也将字符串形式IP地址转换为
32
32
32 位网络字节序整数并返回。只不过该函数利用了 in_addr
结构体,且其使用频率更高。
#include <arpa/inet.h>
int inet_aton(const char *string, struct in_addr *addr);
// 成功时返回1(true),失败时返回0(false)
// string:含有需转换的IP地址信息的字符串地址值
// addr:将保存转换结果的in_addr结构体变量的地址值
在实际编程中,若要调用 inet_addr
函数,需将转换后的IP地址信息代入 sockaddr_in
结构体中声明的 in_addr
结构体变量。而 inet_aton
函数则不需此过程。原因在于,若传递 in_addr
结构体变量地址值,函数会自动把结果填入该结构体变量。
下面通过示例代码 inet_aton.c 了解 inet_aton
函数的调用过程:
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
char *addr = "127.232.124.79";
struct sockaddr_in addr_inet;
// 转换后的IP地址信息需保存到sockaddr_in的in_addr型变量才有意义。
// 因此,inet_aton函数的第二个参数要求得到in_addr型的变量地址值。这就省去了手动保存IP地址信息的过程。
if (!inet_aton(addr, &addr_inet.sin_addr))
error_handling("Conversion error");
else
printf("Network ordered integer addr: %#x\n", addr_inet.sin_addr.s_addr);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
编译运行:
gcc inet_aton.c -o aton
./aton
运行结果:
Network ordered integer addr: 0x4f7ce87f
1.3 inet_ntoa 函数
与 inet_aton
函数正好相反,inet_ntoa
函数可以把网络字节序整数型IP地址转换成我们熟悉的字符串形式。
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr adr);
// 成功时返回转换的字符串地址值,失败时返回-1
inet_ntoa
函数将通过参数传入的整数型IP地址转换为字符串格式并返回。但调用时需小心,返回值类型为 char 指针。返回字符串地址意味着字符串已保存到内存空间,但该函数未向程序员要求分配内存,而是在内部申请了内存并保存了字符串。也就是说,调用完该函数后,应立即将字符串信息复制到其他内存空间。因为,若再次调用 inet_ntoa
函数,则有可能覆盖之前保存的字符串信息。总之,再次调用 inet_ntoa
函数前返回的字符串地址值是有效的。若需要长期保存,则应将字符串复制到其他内存空间。
下面通过示例代码 inet_ntoa.c 说明该函数的调用过程:
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
struct sockaddr_in addr1, addr2;
char *str_ptr;
char str_arr[20];
addr1.sin_addr.s_addr = htonl(0x1020304);
addr2.sin_addr.s_addr = htonl(0x1010101);
// 向inet_ntoa函数传递结构体变量addr1中的IP地址信息并调用该函数,返回字符串形式的IP地址
str_ptr = inet_ntoa(addr1.sin_addr);
// 浏览并复制第15行中返回的IP地址信息
strcpy(str_arr, str_ptr);
printf("Dotted-Decimal notation1: %s\n", str_ptr);
// 再次调用inet_ntoa函数。由此得出,第15行中返回的地址已覆盖了新的IP地址字符串,可通过第23行的输出结果进行验证。
inet_ntoa(addr2.sin_addr);
printf("Dotted-Decimal notation2: %s\n", str_ptr);
// 第18行中复制了字符串,因此可以正确输出第15行中返回的IP地址字符串
printf("Dotted-Decimal notation3: %s\n", str_arr);
return 0;
}
编译运行:
gcc inet_ntoa.c -o ntoa
./ntoa
输出结果:
Dotted-Decimal notation1: 1.2.3.4
Dotted-Decimal notation2: 1.1.1.1
Dotted-Decimal notation3: 1.2.3.4
2.网络地址初始化
结合前面所学的内容,现在介绍套接字创建过程中常见的网络地址信息初始化方法。
struct sockaddr_in addr;
char *serv_ip = "211.217.168.13"; // 声明IP地址字符串
char *serv_port = "9190"; // 声明端口号字符串
memset(&addr, 0, sizeof(addr)); // 结构体变量addr的所有成员初始化为0
addr.sin_family = AF_INET; // 指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); // 基于字符串的IP地址初始化
addr.sin_port = htons(atoi(serv_port)); // 基于字符串的端口号初始化
上述代码中,memset
函数将每个字节初始化为同一值:
- 第一个参数为结构体变量 addr 的地址值,即初始化对象为 addr;
- 第二个参数为 0 0 0,因此初始化为 0 0 0;
- 最后一个参数中传入 addr 的长度,因此 addr 的所有字节均初始化为
0
0
0。这么做是为了将
sockaddr_in
结构体的成员sin_zero
初始化为 0 0 0。
另外,最后一行代码调用的 atoi
函数把字符串类型的值转换成整数型。总之,上述代码利用字符串格式的IP地址和端口号初始化了 sockaddr_in
结构体变量。
上述网络地址信息初始化过程主要针对服务器端而非客户端。给套接字分配IP地址和端口号主要是为下面这件事做准备:“请把进入IP 211.217.168.13、9190 端口的数据传给我!”
反观客户端中连接请求如下:“请连接到IP 211.217.168.13、9190 端口!”
请求方法不同意味着调用的函数也不同。服务器端的准备工作通过 bind 函数完成,而客户端则通过 connect 函数完成。因此,函数调用前需准备的地址值类型也不同。
- 服务器端声明 sockaddr_in 结构体变量,将其初始化为赋予服务器端IP和套接字的端口号,然后调用 bind 函数;
- 而客户端则声明 sockaddr_in 结构体,并初始化为要与之连接的服务器端套接字的IP和端口号,然后调用 connect 函数。
3.INADDR_ANY
每次创建服务器端套接字都要输入IP地址会有些繁琐,此时可如下初始化地址信息。
struct sockaddr_in addr;
char *serv_port = "9190";
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));
与之前方式最大的区别在于,利用常数 INADDR_ANY 分配服务器端的IP地址。若采用这种方式,则可自动获取运行服务器端的计算机IP地址,不必亲自输入。而且,若同一计算机中已分配多个IP地址(多宿主 Multi-homed 计算机,一般路由器属于这一类),则只要端口号一致,就可以从不同IP地址接收数据。因此,服务器端中优先考虑这种方式。而客户端中除非带有一部分服务器端功能,否则不会采用。
问:创建服务器端套接字时需要IP地址的原因?
答:同一计算机中可以分配多个IP地址,实际IP地址的个数与计算机中安装的NIC的数量相等。即使是服务器端套接字,也需要决定应接收哪个IP传来的(哪个NIC传来的)数据。因此,服务器端套接字初始化过程中要求IP地址信息。另外,若只有
1
1
1 个NIC,则直接使用 INADDR_ANY
。
4.向套接字分配网络地址(bind 函数)
既然已讨论了 sockaddr_in
结构体的初始化方法,接下来就把初始化的地址信息分配给套接字,bind 函数负责这项操作。
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
// 成功时返回0,失败时返回-1
// sockfd:要分配地址信息(IP地址和端口号)的套接字文件描述符
// myaddr:存有地址信息的结构体变量地址值
// addrlen:第二个结构体变量的长度
如果此函数调用成功,则将第二个参数指定的地址信息分配给第一个参数中的相应套接字。
下面给出服务器端常见套接字初始化过程。
int serv_sock;
struct sockaddr_in serv_addr;
char *serv_port = "9190";
// 创建服务器端套接字(监听套接字)
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
// 地址信息初始化
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(serv_port));
// 分配地址信息
bind(serv_sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
// ......
服务器端代码结构默认如上,当然还有未显示的异常处理代码。