3.4 网络地址的初始化与分配
前面已讨论过网络字节序, 接下来介绍以bind函数为代表的结构体的应用.
将字符串信息转换为网络字节序的整型数
sockaddr_in 中保存地址信息的成员为32位整数型. 因此, 为了分配IP 地址, 需要将其表示为 32 位整数型数据. 这对于只熟悉字符串信息的我们来说实非易事. 各位可以尝试将 IP 地址 201.211.214.36 转换为4字节整数型数据.
对于IP 地址的表示, 我们熟悉的是分十进制表示法(Dotted Decimal Notation), 而非整数型数据表示法. 幸运的是, 有一个函数会帮我们将字符串形式的 IP 地址转换成32位整数. 此函数在转换类型的同时进行网络字节序转换.
如果向该函数传递类似 “211.214.107.99” 的点分十进制格式的字符串, 它会将其转换为32位整数型数据并返回. 当然, 该整数型值满足网络字节序. 另外, 该函数的返回值类型in_addr_t 在内部声明为32位整数型. 下列实例表示该函数的调用过程.
#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";
/* 通过运行结果验证第9行的函数正常调用 */
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 occureded \n");
}
else
{
printf("Network ordered integer addr: %#lx \n\n", conv_addr);
}
return 0;
}
运行结果:
从运行结果可以看出, inet_addr函数不仅可以把IP地址转成32位整数型, 而且可以检测无效的IP地址. 另外, 从输出结果可以验证确实转换为网络字节序.
inet_aton函数与inet_addr函数在功能上完全相同, 也将字符串形式IP地址转换为32位网络字节序整数并返回. 只不过该函数利用了 in_addr 结构体, 且其使用频率更高.
实际编程中若要调用inet_addr 函数, 需将转换后的 IP 地址信息代入sockaddr_in结构体中声明的 in_addr 结构体变量. 而inet_aton函数则不需此过程. 原因在于, 若传递 in_addr结构体变量地址值, 函数会自动把结果填入该结构体变量. 通过实例了解 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) == 0)
{
error_handling("Conversion error");
}
else
{
printf("Network ordered intager addr: %#x \n",
addr_inet.sin_addr.s_addr);
}
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:
上述结果无关紧要, 更重要的是大家要熟练掌握该函数的调用方法. 最后再介绍一个与inet_aton 函数正好相反的函数, 此函数可以把网络字节序整数型IP地址转换成我们熟悉的字符串形式.
该函数将通过参数传入的整数型 IP 地址转换为字符串格式并返回. 但调用时需小心, 返回值类型为char 指针. 返回字符串地址意味着字符串已保存到内存空间, 但该函数未向程序员要求分配内存, 而是在内部申请了内存并保存了字符串. 也就是说, 调用完该函数后, 应立即将字符串信息复制到其他内存空间. 因为, 若再次调用inet_ntoa 函数, 则有可能覆盖之前保存的字符串信息. 总之, 再次调用inet_ntoa 函数前返回的字符串地址值是有效的. 若需要长期保存, 则应将字符串复制到其他内存空间. 下面给出该函数调用实例.
#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 notationl: %s \n", str_ptr);
/* 再次调用inet_ntoa函数. 由此得出, 第15行中返回的地址已覆盖了新的IP地址字符串,
可通过第23行的输出结果进行验证. */
inet_ntoa(addr2.sin_addr);
printf("Dotted-Decimal ntoation2: %s \n", str_ptr);
/* 第17行中复制了字符串, 因此可以正确输出第15行中返回的IP地址字符串. */
printf("Dotted-Decimal notation3: %s \n", str_arr);
return 0;
}
运行结果:
网络地址初始化
结合前面所学的内容, 现在介绍套接字创建过程中常见的网络地址信息初始化方法.
上述代码中, memset 函数将每个字节初始化为同一值: 第一个参数为结构体变量 addr 的地址值, 即初始化对象为addr; 第二个参数为0, 因此初始化为0; 最后一个参数中传入 addr 的长度, 因此addr的所有字节均初始化0, 这么做是为了将 sockaddr_in 结构体的成员 sin_zero 初始化为0. 另外, 最后一行代码调用的 atoi 函数把字符串类型的值转换成整数型. 总之, 上述代码利用字符串格式的IP地址和端口号初始化了 sockaddr_in 结构体变量.
另外, 代码中对IP地址和端口号进行了硬编码, 这并非良策, 因为运行的环境改变就得更改代码. 因此, 我们运行实例main函数时传入IP地址和端口号.
客户端地址信息初始化
上述网络地址信息初始化过程主要针对服务器端而非客户端 .给套接字分配IP地址和端口号主要是为下面这件事做准备:
反观客户端中的连接请求如下:
请求方法不同意味这调用的函数也不同. 服务器端的准备工作通过bind函数完成, 而客户端则通过 connect 函数完成. 因此, 函数调用前需要准备的地址值类型也不同. 服务器端声明sockaddr_in 结构体变量, 将其初始化为赋予服务器端 IP 和套接字的端口号, 然后调用bind 函数; 而客户端则声明sockaddr_in 结构体, 并初始化要为之连接的服务器端套接字的IP 和端口号, 然后调用connect 函数.
INADDR_ANY
每次创建服务器套接字都要输入IP地址会有些繁琐, 此时可如下初始化地址信息.
与之前方式最大的区别在于, 利用常数INADDR_ANY 分配服务器端的IP地址. 若采用这种方式, 则可以自动获取运行的服务器端的计算机IP地址, 不必亲自输入. 而且, 若同一计算机中已分配多个IP地址(多宿主(Multi-homed) 计算机, 一般路由器属于这一种), 则只要端口号一致, 就可以从不同IP地址接收数据. 因此, 服务器端优先考虑这种方式. 而客户端中除非带有一部分服务器端功能, 否则不会采用.
初始化服务器端套机字是应分配所属计算机的IP地址, 应为初始化时使用的IP地址非常明确, 那为何还要进行IP地址初始化呢? 如前所述, 同一计算机中可以分配多个IP地址, 实际IP地址的个数与计算机中安装的NIC数量相等. 即使是服务器端套机字, 也需要决定应接收那个IP 传来的(哪个NIC 传来的) 数据. 因此, 服务器端套接字初始化过程中要求IP地址信息. 另外, 若只有一个NIC, 则直接使用INADDR_ANY.
第一章的 Hello_server.c Hello_client.c 运行过程
第一章中执行以下命令以运行相当于服务器端的hello_server.c
通过代码可知, 先main 函数传递的9190 为端口号. 通过此端口创建服务器端套接字并运行程序, 但未传递IP地址, 因为可以通过INADDR_ANY指定IP地址. 相信各位现在再去读代码会感到简单很多.
执行下列命令以运行相当于客户端的hello_client.c. 与服务器端运行方式相比, 最大的区别是传递了IP地址信息.
127.0.0.1 是回送地址(loopback addrress), 是指计算机自身IP地址. 在第1章的实例中, 服务器端和客户端在同一计算机中运行, 因此, 连接目标服务器端的地址为127.0.0.1. 当然, 若用实际IP地址代替此地址也能正常运行. 如果服务器端和客户端分别在2台计算机中运行, 则可以输入服务器端 IP 地址.
向套接字分配网络地址
既然已讨论了 sockaddr_in 结构体的初始化方法, 接下来就把初始化的地址信息分配给套机字. bind 函数负责这项操作.
如果此函数调用成功, 则将第二个参数指定的地址信息分配给第一个参数中相应套接字.下面给出服务器端常见套接字初始化过程.
服务器端代码结构默认如上, 当然还有未显示的异常处理代码.
3.5 基于Windows 的实现
windows 中同样存在 sockaddr_in 结构体及各种变换函数, 而且名称, 使用方法及含义都相同. 也就无需针对Windows 平台进行太多修改或改用其他函数. 接下来将前面几个程序改成 Windows 版本.
函数 htons, htonl 在 Windows 中的使用
首先给出 Windows 平台下调用 htons 函数的实例. 这两个函数的用法与 Linux 平台下的使用并无区别, 故省略.
#include <stdio.h>
#include <WinSock2.h>
void ErrorHandling(const char* message);
int main(int argc, char argv[])
{
WSADATA wsaData;
unsigned short host_port = 0x1234;
unsigned short net_port;
unsigned long host_addr = 0x12345678;
unsigned long net_addr;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
ErrorHandling("WSAStartup() erro");
}
net_port = htons(host_port);
net_addr = htonl(host_addr);
printf("Host ordered port: %#x \n", host_port);
printf("Network ordered port: %#x \n", net_port);
printf("Host ordered address: %#lx \n", host_addr);
printf("Network ordered address: %#lx \n", net_addr);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:
该程序多了进行库初始化的 WSAStartup 函数调用和 winsock2.h 头文件的#include 语句, 其他部分没有区别.
函数 inet_addr 、inet_ntoa 在 Windows 中的使用
下列实例给出了 inet_addr 函数和 inet_ntoa函数的调用过程. 前面分别给出了 Linux 中这两个函数的调用实例, 而在 Windows 中不存在 inet_aton 函数, 故省略.
#include <stdio.h>
#include <string.h>
#include <WinSock2.h>
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
ErrorHandling("WSAStartup() error");
}
/* inet_addr函数调用示例 */
{
const char* addr = "127.212.124.78";
unsigned long conv_addr = inet_addr(addr);
if (conv_addr == INADDR_ANY)
{
printf("Error occurd! \n");
}
else
{
printf("Network ordered integer addr: %#lx \n", conv_addr);
}
}
/* inet_ntoa函数调用示例 */
{
struct sockaddr_in addr;
char* strPtr;
char strArr[20];
addr.sin_addr.s_addr = htonl(0x1020304);
strPtr = inet_ntoa(addr.sin_addr);
strcpy(strArr, strPtr);
printf("Dotted-Decimal notation3 %s \n", strArr);
}
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:
上述实例在main函数体内使用括号增加变量声明, 同时区分各函数的调用过程. 添加中括号可以在相应区域的初始化部分声明局部变量. 当然, 此类局部变量跳出括号则消失.
在 Windows 环境下向套接字分配网络地址
Windows 中向套接字分配网络地址的过程与 Linux 中完全相同, 因为bind 函数的含义, 参数及返回类型完全一致.
这与Linux 平台下套接字初始化及地址分配过程基本一致, 只不过改了一些变量名.
WSAStringToAddress & WSAAddressToString
下面介绍 Winsock2中增加的2个转换函数. 它们在功能上与 inet_ntoa 和 inet_addr 完全相同, 但优点在于支持多种协议, 在 IPV4 和 IPV6 中均可适用, 当然它们也是有缺点的, 适用 inet_ntoa、inet_addr 可以很容易地在 Linux 和 Windows 之间切换程序. 而将要介绍的这2个函数则依赖于特定平台, 会降低兼容性. 因此本书不会使用它们, 介绍的目的仅在了解更多函数.
先介绍WSAStringToAddress 函数, 它将地址信息字符串适当填入结构体变量.
下面给出这两个函数的使用示例.
书本给的代码我无法实现报错, 也不会改, 代码如下, 等到我以后功力行了再回来改吧!
#undef UNICODE
#undef _UNICODE
#include <stdio.h>
#include <WinSock2.h>
int main(int argc, char* argv[])
{
const char* strAddr = "203,211,218,102:9190";
char strAddrBuf[50];
SOCKADDR_IN servAddr;
int size;
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
size = sizeof(servAddr);
WSAStringToAddress((char*)strAddr, AF_INET, NULL, (SOCKADDR*)&servAddr, &size);
size = sizeof(strAddrBuf);
WSAAddressToString((SOCKADDR*)&servAddr, sizeof(servAddr), NULL, strAddrBuf, &size);
printf("Second conv result: %s \n", strAddrBuf);
WSACleanup();
return 0;
}
书上的运行结果:
3.6 习题
(1) IP地址族 IPV4 和 IPV6 有何区别? 在何种背景下担生了 IPV6?
…
时间: 2020:05:25