网络地址的初始化与分配

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));

// ......

服务器端代码结构默认如上,当然还有未显示的异常处理代码。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值