第三章:地址族与数据序列
3、1 分配给套接字的IP地址与端口号
- IP(Internet Protocol)网络协议:为收发网络数据而分配给计算机的值。端口号是为区分程序中创建的套接字而分配给套接字的序号。
- 网络地址:
- IP地址分为 IPv4(4字节地址族)、IPv6(16字节地址族),主要差别是表示IP地址所用的字节数。
- IPv4标准的4字节IP地址分为网络地址 和主机(计算机)地址,且分为A 、 B、C、D、E类。
- 网络地址: 为区分网络而设置的一部分IP地址。
- 传输数据时并不是一开始就浏览所有4字节IP地址,进而找到目标主机,而是仅浏览4字节IP地址的网络地址,先将数据传输到网络上,网络(构成网络的路由器)接收到数据后,浏览传输数据的主机地址,将数据传输给目标主机计算机。
- 向相应网络传输数据实际上是向构成网络的路由器(Router)或交换机(Switch)传输数据,由接收数据的路由器根据数据中的主机地址向目标主机传输数据。
- 网络地址分类和主机地址边界
-
只需要通过IP地址的第一个字节即可判断网络地址占用的字节数,因为通常根据IP地址的边界区分网络地址。
- A类地址的首字节范围 : 0~127
- B类地址的首字节范围: 128~191
- C类地址的首字节范围: 192~233
-
或者另一种表达方式:
- A类地址的首位以0开始
- B类地址的前2位以10开始
- C类地址的前3位以110开始
-
IP地址分为A类、B类、C类、D类的依据是根据IP地址的二进制位数划分。具体如下:
-
A类IP地址:第一个二进制位固定为0,后面7个二进制位用于网络部分,剩余24个二进制位用于主机部分。因此,A类IP地址范围为0.0.0.0~127.255.255.255。
-
B类IP地址:前两个二进制位固定为10,后面14个二进制位用于网络部分,剩余16个二进制位用于主机部分。因此,B类IP地址范围为128.0.0.0~191.255.255.255。
-
C类IP地址:前三个二进制位固定为110,后面21个二进制位用于网络部分,剩余8个二进制位用于主机部分。因此,C类IP地址范围为192.0.0.0~223.255.255.255。
-
D类IP地址:前四个二进制位固定为1110,后面28个二进制位用于组播地址。因此,D类IP地址范围为224.0.0.0~239.255.255.255。
-
-
-
- 用于区分套接字的端口号
- IP用于区分计算机,有了IP地址就能向目标主机传输数据。还需要端口号来区分套接字应用程序。
- 计算机配有NIC(Network Interface Card)网卡数据传输设备。通过NIC向计算机内部传输数据会用到IP,操作系统将传递到内部的数据适当分配给套接字,此时需要用到端口号。通过NIC接收的数据内有端口号,操作系统参考此端口号将数据传输到对应的端口的套接字。
- 端口号是操作系统为区分不同套接字设置的,无法将1个端口号分配给不同套接字。端口号由16位构成,可供分配的端口号范围是0~65535, 0~1023是知名端口号(well-known Port),一般分配给特定应用程序。
- 虽然端口号不能重复,但是TCP套接字和UDP套接字不会共用端口号,允许重复。
- 数据传输目的地址同时包含IP地址和端口号,如此数据才会传输到最终的目的应用程序(应用程序套接字)。
3、2 地址信息的表示
-
表示IPv4地址的结构体:
-
考虑哪一种地址族、IP地址是多少、端口号是多少。
-
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]; // 不使用 }; // 用来存放32位IP地址 struct in_addr{ In_addr_t s_addr; // 32位IPv4地址 };
-
-
结构体sockaddr_in的成员分析
-
成员sin_family:
-
地址族(Address Family) 含 义 AF_INET IPv4网络协议中使用的地址族 AF_INET6 IPv6网络协议使用的地址族 AF_LOCAL 本地通信中采用的UNIX协议地址族
-
-
成员sin_port:
- 保存16位端口号,以网络字节序保存。
-
成员sin_addr:
- 保存32位IP地址信息,以网络字节序保存。in_addr可以当作32位整数型。
-
成员 sin_zero
- 无实际意义,为了使得sockaddr_in的大小保持与sockaddr一致而插入的成员。
-
-
** 结构体sockaddr:**是bind函数第二个参数期望得到的地址值。
-
struct sockaddr { sa_family_t sin_family; // 地址族 char sa_data[14]; // 地址信息 }
-
sa_data保存的地址信息中需包含IP地址和端口号,剩余部分填充0。因为直接向sockaddr中写入地址族、端口号、IP地址等会很麻烦,所以有了sockaddr_in结构体。填写了sockaddr_in结构体后,会生成符合bind函数要求的字节流,最后转化为sockaddr型的结构体变量,再传递给bind函数。
-
3、3 网络字节序与地址变换:
-
不同CPU对4字节整形值1的保存方式不同,所以需要考虑这些差异以正确解析接发数据信息。
-
字节序(Order)与网络字节序:
- CPU保存数据的方式有两种,其解析数据的方式也有两种:
- 大端序(Big Ending): 高位字节 存放在 低位地址。
- 小端序(Little Ending): 高位字节 存放 高位地址。
- CPU的数据保存方式不同,所以代表CPU数据保存方式的主机字节序(Host Byte Order)在不同CPU中也不同。
- 所以在通过网络传输数据时约定统一方式,称约定为网络字节序(Network Byte Order)–统一为大端序。
- 先将数据数组转化为大端序格式再进行网络传输。计算机接收数据视为网络字节序格式,小端序系统传输数据时应转化为大端序排列方式。
- CPU保存数据的方式有两种,其解析数据的方式也有两种:
-
字节序转换(Endian Conversions)
-
由于要将主机字节序转换为网络字节序进行数据传输,所以在填充sockadr_in 结构体前要将数据转换为网络字节序。
-
一些帮助转换字节序的函数:
-
unsigned short htons(unsigned short); // 把short类型数据从主机字节序转化为网络字节序(port) unsigned short ntohs(unsigned short); // 把short类型数据从网络字节序转化为主机字节序(port) unsigned short htonl(unsigned short); // 把long类型数据从主机字节序转化为网络字节序(IP) unsigned short ntohl(unsigned short); // 把long类型数据从网络字节序转化为主机字节序。
-
htons中 h代表主机(host)字节序。
-
htons中的n 代表 网络(network)字节序。
-
s指的是short, l 指 long,Linux中long类型占用4个字节。
-
-
#include <stdio.h> #include <arpa/inet.h> int main(int argc, char *argv[]){ // 保存2字节的数据 unsigned short host_port = 0x1234; unsigned short net_port; unsigned long host_addr=0x12345678; unsigned long net_addr; // htons将host_port的数据转换为网络字节序 net_port = htons(host_port); net_addr = htonl(host_addr); // %#x是带格式输出, 效果为在输出前加0x. printf("Host ordered port : %#x \n",host_port); printf("Network ordered port : %#x \n",net_port); printf("Host ordered port : %#x \n",host_addr); printf("Network ordered port : %#x \n",net_addr); return 0; }
-
-
除了向sockaddr_in结构体变量填充数据外,其他情况无需考虑字节序问题。
-
3、4 网络地址的初始化与分配:
-
将字符串信息转换为网络字节序的整数型:
-
sockaddr_in 保存地址信息的成员 struct in_addr sin_addr是32位整数型。为了分配IP地址,需要将点分十进制表示法(Dotted Decimal Notation)表示的IP地址转换为32位整数型数据。该整型值满足网络字节序。
-
#include <arpa/inet.h> // 将字符串形式的IP地址转换成32位整型数据。 in_addr_t inet_addr(const char * string); // 成功时返回32位大端序整数型值,失败返回INADDR_NONE
-
#include <stdio.h> #include <arpa/inet.h> int main(int argc, char *argv[]){ char *addr1 = "1.2.3.4"; // 1个字节能表示的最大整数是255,addr2是错误的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; }
-
-
可以看出,inet_addr()函数不仅可以将点分十进制的IP地址转换位32位整数型,还可以检测无效的IP地址。
-
inet_aton函数与inet_addr功能上相同,也将字符串形式的IP地址转换为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函数不用如此,因inet_aton函数需要传入 in_addr结构体变量的地址值,函数会自动将结果带入结构体变量。
-
#include <stdlib.h> #include <stdio.h> #include <arpa/inet.h> void error_handling(char *message); int main(int argc,char * argv[]){ char *addr1 = "127.232.124.79"; struct sockaddr_in addr_inet; // 转换后的IP地址保存在sockaddr_in的in_addr型变量来才有意义,第二个参数要求得到in_addr变量地址值,可以省略保存IP地址信息的过程。 if(!inet_aton(addr1,&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); }
-
-
-
函数inet_ntoa() 将网络字节序整数型IP地址转换为点分十进制的字符串形式。
-
#include <arpa/inet.h> // 函数将通过参数传入的整数型IP地址转换为字符串格式并返回。 char * inet_ntoa(struct in_addr adr); // 成功返回转换的字符串地址值,失败返回-1。
-
调用完该函数,应立即将字符串信息复制到其他内存空间,如果再次调用inet_ntoa()函数,有可能会覆盖之前保存的字符串信息。
-
#include <stdio.h> #include <stdlib.h> #include <arpa/inet.h> #include <string.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); strcpy(str_arr,str_ptr); printf("Dotted-Decimal notation1: %s \n",str_ptr); // 再次调用inet_ntoa函数,返回的地址会覆盖新的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; // 指定地址族 add.sin_addr.s_addr = inet_addr(serv_ip); // 基于字符串的IP地址初始化 addr.sin_port = htos(atoi(serv_port)); // 基于字符串的端口号初始化
-
memset将addr对象的所有字节均初始化为0,是为了将sockaddr_in结构体成员sin_zero初始化为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函数。
-
INADDR_ANY
-
struct sockaddr_in addr; char *serv_port = "9190"; // 声明端口字符串 memset(&addr,0,sizeof(addr)); // 结构体变量addr所有成员初始化为0 addr.sin_family = AF_INET; // 指定地址族 add.sin_addr.s_addr = inet_addr(INADDR_ANY); // 基于字符串的IP地址初始化 addr.sin_port = htos(atoi(serv_port)); // 基于字符串的端口号初始化
-
每次创建服务器端套接字都要输入IP地址很繁琐,可以利用常数 INADDR_ANY 分配服务器端的IP地址。这样可以自动获取运行服务器端的计算机IP地址,而不必手动输入。若同一计算机中已分配多个IP地址(多宿主(Muti-homed)计算机,一般路由器属于此类,)只要端口号一致就可以从不同IP地址接收数据。一般服务器端优先使用这种方法,客户端除了带有一部分服务器端功能,一般不会采用。
-
创建服务器端套接字需要IP地址的原因
- 同一计算机中可以分配多个IP地址,实际IP地址的个数与计算机安装的NIC的数量相等,即使是服务器端套接字,也需要决定接收哪个IP传来的数据,所以服务器端套接字初始化时要求IP地址信息。
-
-
向套接字分配网络地址:
-
bind函数负责将初始化的地址信息分配给套接字。
-
#include <sys/socket.h> int bind(int sockfd,struct sockaddr * myaddr, socklen_t addrlen); // 成功返回0,失败返回-1
- sockfd : 要分配地址信息的套接字文件描述符。
- 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));
-
3、5 基于Windows的实现:
-
函数 htons , htonl函数的应用:
-
除了加入库初始化的WSAStartup函数和winsock2.h的头文件,与linux上的没有区别。
-
#include <stdio.h> #include <winsock2.h> void ErrorHandling(const char* message); int main(int agrc, 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() error"); 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 port : %#x \n", host_addr); printf("Network ordered port : %#x \n", net_addr); } void ErrorHandling(const char* message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
-
-