IP 是 Internet Protocol(网络协议)的简写,是为收发网络数据而分配给计算机的值。端口号并非赋予计算机的值,而是为了区分程序中创建的套接字而分配给套接字的序号。
为使计算机连接到网络并收发数据,必须为其分配 IP 地址。IP 地址分为两类。
- IPV4(Internet Protocol version 4)4 字节地址族
- IPV6(Internet Protocol version 6)6 字节地址族
两者之间的主要差别是 IP 地址所用的字节数,目前通用的是 IPV4 , IPV6 的普及还需要时间。
IP地址用于区分计算机,只要有IP地址就能向目标主机传输数据,但是只有这些还不够,我们需要把信息传输给具体的应用程序。 端口号就是在同一操作系统内为区分不同套接字而设置的。 端口号由 16 位构成,可分配的端口号范围是 0~65535 。但是 0~1023 是知名端口,一般分配给特定的应用程序,所以应当分配给此范围之外的值。 虽然端口号不能重复,但是 TCP 套接字和 UDP 套接字不会共用端接口号,所以允许重复。 总之,数据传输目标地址同时包含IP地址和端口号,只有这样,数据才会被传输到最终的目的应用程序。
表示 IPV4 地址的结构体:
struct sockaddr_in
{
sa_family_t sin_family; //地址族(Address Family)
uint16_t sin_port; //16 位 TCP/UDP 端口号
struct in_addr sin_addr; //32位 IP 地址
char sin_zero[8]; //不使用
};
该结构体中提到的另一个结构体 in_addr 定义如下,它用来存放 32 位IP地址:
struct in_addr
{
in_addr_t s_addr; //32位IPV4地址
}
结构体 sockaddr_in 的成员分析:
不同的 CPU 中,4 字节整数值1在内存空间保存方式是不同的。
有些 CPU 这样保存:
00000000 00000000 00000000 00000001
有些 CPU 这样保存:
00000001 00000000 00000000 00000000
两种一种是顺序保存,一种是倒序保存 。若不考虑顺序就收发数据容易出问题。
CPU 保存数据的方式有两种,这意味着 CPU 解析数据的方式也有 2 种:
在通过网络传输数据时必须约定统一的方式,这种约定被称为网络字节序,非常简单,统一为大端序。即,先把数据数组转化成大端序格式再进行网络传输。
介绍一下帮助转换字节序的函数:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
例如:htons(host to network short):把short型数据从主机字节序转换为网络字节序。
通常,以s作为后缀的函数中,s代表2字节的short,因此用于端口号转换;以l作为后缀的函数中,l代表4个字节,因此用于IP地址转换。
实验:
#include
#include
int main(int argc, char *argv[])
{
unsigned short host_port = 0x1234;//保存2个字节的数据
unsigned short net_port;
unsigned long host_addr = 0x12345678;//保存4个字节的数据
unsigned long net_addr;
//转换为网络字节序
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);
return 0;
}
实验结果:
这是在小端 CPU 的运行结果。大部分人会得到相同的结果,因为 Intel 和 AMD 的 CPU 都是小端序为标准。
本节介绍三个函数用于在字符串信息和网络字节序的整数型之间进行转换。
第一个函数将字符串形式的 IP 地址转换为网络字节序形式的 32 位整数型数据:
#include
in_addr_t inet_addr(const char *string);
//成功时返回 32 位大端序整数型值,失败时返回 INADDR_NONE
实验:
#include
#include
int main(int argc, char *argv[])
{
char *addr1 = "1.2.3.4";
//1个字节能表示的最大整数是255,所以代码中 addr2 是错误的IP地址
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;
}
从运行结果看,inet_addr 不仅可以转换地址,还可以检测有效性。
第二个函数inet_aton与 inet_addr 函数在功能上完全相同,也是将字符串形式的IP地址转换成整数型的IP地址。只不过该函数用了 in_addr 结构体,且使用频率更高。
#include
int inet_aton(const char *string, struct in_addr *addr);
/*
成功时返回 1 ,失败时返回 0
string: 含有需要转换的IP地址信息的字符串地址值
addr: 保存转换结果的 in_addr 结构体变量的地址值
*/
实验:
#include
#include
#include
void error_handling(char *message);
int main(int argc, char *argv[])
{
char *addr = "127.232.124.79";
struct sockaddr_in addr_inet;
//调用inet_aton函数将IPv4地址字符串转换为网络字节序的无符号长整型,并将结果存储在addr_inet.sin_addr的s_addr字段中
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);
}
可以看出,已经成功的把转换后的地址放进了 addr_inet.sin_addr.s_addr 中。
最后一个函数,与 inet_aton() 正好相反,它可以把网络字节序整数型IP地址转换成我们熟悉的字符串形式,函数原型如下:
#include
char *inet_ntoa(struct in_addr adr);
//成功时返回保存转换结果的字符串地址值,失败时返回 NULL 空指针
ps:返回值为 char 指针,返回字符串地址意味着字符串已经保存在内存空间,但是该函数未向程序员要求分配内存,而是再内部申请了内存保存了字符串。也就是说调用了该函数后要立即把信息复制到其他内存空间。因为,若再次调用 inet_ntoa 函数,则有可能覆盖之前保存的字符串信息。总之,再次调用 inet_ntoa 函数前返回的字符串地址是有效的。若需要长期保存,则应该将字符串复制到其他内存空间。
实验:
#include
#include
#include
int main(int argc, char *argv[])
{
//定义了两个struct sockaddr_in类型的变量addr1和addr2,以及一个指向字符的指针str_ptr和一个字符数组str_arr。
struct sockaddr_in addr1, addr2;
char *str_ptr;
char str_arr[20];
//通过调用htonl函数将两个十六进制数0x1020304和0x1010101转换为网络字节序的无符号长整型,并将结果存储在addr1.sin_addr.s_addr和addr2.sin_addr.s_addr中,分别表示两个IPv4地址。
addr1.sin_addr.s_addr = htonl(0x1020304);
addr2.sin_addr.s_addr = htonl(0x1010101);
//把addr1中的结构体信息转换为字符串的IP地址形式
str_ptr = inet_ntoa(addr1.sin_addr);
//调用strcpy函数将str_ptr指向的字符串复制到str_arr字符数组中。
strcpy(str_arr, str_ptr);
printf("Dotted-Decimal notation1: %s \n", str_ptr);
//验证了:新的IP地址覆盖了旧IP地址
inet_ntoa(addr2.sin_addr);
printf("Dotted-Decimal notation2: %s \n", str_ptr);
printf("Dotted-Decimal notation3: %s \n", str_arr);
return 0;
}
下面介绍套接字创建过程中,常见的网络信息初始化方法:
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)); //基于字符串的IP地址端口号初始化
上述网络地址信息初始化过程主要针对服务器端而非客户端。
请求方法不同意味着调用的函数也不同。服务器端的准备工作通过bind函数完成,而客户端则通过connect函数完成。因此,函数调用前需准备的地址值类型也不同。服务器端声明sockaddr_in结构体变量,将其初始化为赋予服务器端IP和套接字的端口号,然后调用bind函数;而客户端则声明sockaddr_in结构体,并初始化为要与之连接的服务器端套接字的IP和端口号,然后调用connect函数。
1、通过 IPV4 网络 ID 、主机 ID 及路由器的关系说明公司局域网的计算机传输数据的过程
IP地址分配:每台计算机在公司局域网中被分配了一个唯一的IPv4地址。IPv4地址由网络ID和主机ID组成。网络管理员负责分配IP地址,确保网络ID在公司局域网范围内唯一。
数据发送:当一台计算机想要向另一台计算机发送数据时,它会创建一个数据包并将目标IP地址设置为接收方计算机的IP地址。数据包还包含发送方计算机的IP地址作为源IP地址。
路由选择:发送方计算机通过查找其本地的路由表来确定应该将数据包发送到哪个路由器。路由器是网络中的关键设备,负责转发数据包。根据目标IP地址和路由表的匹配规则,发送方计算机选择一个合适的路由器。
数据包转发:选择的路由器接收到数据包后,它会检查目标IP地址,并根据自身的路由表将数据包转发到下一个目标。这个过程可能涉及多个路由器,每个路由器都负责将数据包传递给下一个目标,直到达到目标计算机所在的局域网。
目标计算机接收:当数据包到达目标计算机所在的局域网时,该计算机会通过比较目标IP地址和自身的IP地址来判断是否为自己的数据包。如果是,则该计算机接收数据包并进行后续处理。
路由器表示连接到互联网的网络 ID,用于在不同网络间转发数据。交换机用于组织一个局域网内部的主机,局域网内部的主机可以通过交换机直接通信。如果局域网内的主机想要和其他网络的主机通信,需要通过路由器转发到目的网络,接收到的其他网络发来的数据先通过路由器接收,再由路由器根据主机号转发到交换机寻找具体的主机。
CPU 向内存保存数据有两种方式,大端序是高位字节存放低位地址,小端序是高位字节存放高位地址,网络字节序是为了方便传输的信息统一性,统一成了大端序。