第三章 地址族与数据序列
本章讲给套接字分配IP地址和端口号的方法。
3.1 分配给套接字的IP地址和端口号
IP是网络协议(Internet Protocol)的简写,是为收发网络数据而分配给计算机的值。
端口号并非赋予计算机的值,而是为区分程序中创建的套接字而分配给套接字的序号。
网络地址
为了使计算机连接到网络,并收发数据,必须向其分配IP地址。
IP地址分为两类:IPv4和IPv6。主要区别是用来表示IP地址所用的字节数不同,目前通用的地址族是IPv4。
IPv4标准的4字节IP地址 == 网络地址 + 主机地址。且分为ABCDE等类型。一般不会使用已被预约了的E类地址。
- A类地址:网络ID占1字节,主机ID占3字节
- B类地址:网络ID占2字节,主机ID占2字节
- C类地址:网络ID占3字节,主机ID占1字节
- D类地址:网络ID占4字节,主机ID占0字节(这是多播IP地址)
并非一开始就浏览整个4字节的IP地址,而是先浏览网络地址,将数据传到那个网络;网络接到数据后,浏览传输数据的主机地址,将数据传递给那个目标主机。
向相应的网络传输数据,实际上是向构成网络的路由器或交换机传递数据,由接收数据的路由器根据数据中主机地址向目标主机传递数据。
网络地址分类与主机地址边界
首字节范围:
- A类地址:0~127,首位以0开始
- B类地址:128~191,前2位以10开始
- C类地址:192~223,前3位以110开始
用于区分套接字的端口号
计算机中一般配有NIC(网络接口卡)数据传输设备。通过NIC向计算机内部传输数据时会用到IP。
操作系统负责把传递到内部的数据适当分配给套接字,这时会利用端口号。
通过NIC接收的数据内有端口号,操作系统正是参考此端口号把数据传输给相应端口的套接字。
端口号,就是在同一个操作系统内为区分不同套接字而设置的。
端口号由16位构成,可分配的端口号范围0~65535。但0-1023是知名端口,一般分配给特定应用程序。
虽然端口号不能重复,但TCP套接字和UDP套接字不会共用端口号,所以允许重复。
数据传输目标地址,同时包含 IP地址和端口号,这样,数据就会被传输到最终的目的应用程序(应用程序套接字)。
3.2 地址信息的表示
表示IPv4地址的结构体
struct sockaddr_in{
sa_family_t sin_family; //地址族
uint16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位的IP地址
char sin_zero[8];//不使用
}
其中:
struct in_addr{
in_addr_t s_addr; //32位IPv4地址
}
POSIX中定义的数据类型 | 说明 | 声明的头文件 |
---|---|---|
int8_t | signed 8-bit int | sys/types.h |
uint8_t | unsigned 8-bit int(unsigned char) | sys/types.h |
int16_t | signed 16-bit int | sys/types.h |
uint16_t | unsigned 16-bit int(unsigned short) | sys/types.h |
int32_t | signed 32-bit int | sys/types.h |
uint32_t | unsigned 32-bit int(unsigned long) | sys/types.h |
sa_family_t | 地址族(address family) | sys/socket.h |
socklen_t | 长度(length of struct) | sys/socket.h |
in_addr_t | IP地址,声明为uint32_t | netinet/in.h |
in_port_t | 端口号,声明为uint16_t | netinet/in.h |
结构体sockaddr_in的成员分析
成员sin_family:
地址族 | 含义 |
---|---|
AF_INET | IPv4网络协议中使用的地址族(常用) |
AF_INET6 | IPv6网络协议中使用的地址族 |
AF_LOCAL | 本地通信中采用的UNIX协议的地址族 |
成员sin_port:
保存16位端口号(2字节),以网络字节序保存。
成员sin_addr:
保存32位IP地址信息(4字节),也以网络字节序保存。其中的in_addr声明为unit32_t,只需当做32位的证书型即可。
成员sin_zero:
没啥意思。只是为了使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员。必须填充为0。
sockaddr_in结构体变量地址值传递给bind函数:
struct sockaddr_in serv_addr;
...
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
...
bind函数的第二个参数,期望得到sockaddr结构体变量地址值,包括地址族、端口号、IP地址等。
struct sockaddr{
sa_family sin_family; //地址族
char sa_data[14];//地址信息
}
bind函数要求sa_data保存的地址信息中包含IP地址和端口号,剩余部分填充为0。太麻烦了,借助sockaddr_in这个结构体辅助构造,先填好sockaddr_in,然后转换成sockaddr型的结构体变量,再传递给bind函数。
3.3 网络字节序与地址变换
大端序:高位字节存放到低位地址
小端序:高位字节存放到高位地址
将4字节的int型数0x12345678
保存在0x20
开始的地址中,
小端序:
低位地址 0x20 0x21 0x22 0x23 高位地址
高位字节 0x12 0x34 0x56 0x78 低位字节
大端序:
低位地址 0x20 0x21 0x22 0x23 高位地址
低位字节 0x78 0x56 0x34 0x12 高位字节
0x20是低位地址,0x23是高位地址;
0x12是高位字节,0x78是低位字节
每种CPU的数据保存方式均不同,所以,代表CPU数据保存方式的主机字节序在不同CPU中也各不相同。
在通过网络传输数据时约定统一方式,这种约定称为网络字节序——统一为大端序。就是,先把数据数组转化成大端序格式再进行网络传输。
字节序转换
unsigned short htons(unsigned short); 把short型数据从主机字节序转化为网络字节序
unsigned short ntohs(unsigned short); 把short型数据从网络字节序转化为主机字节序
unsigned long htonl(unsigned long);
unsigned long ntohs(unsigned long);
h:主机(host)字节序
n:网络(network)字节序
s:short
l:long
通常,以s
作为后缀的函数中,s代表2
字节short
,用于端口号转换;以l
作为后缀的函数中,l代表4
字节long
,用于IP地址转换。
3.4 网络地址的初始化与分配
(1)inet_addr
inet_addr
函数可以将字符串形式的IP地址转换成32位整数型数据,此函数在转换类型的同时进行字节序转换,就是能将其转换成网络字节序:
#include<arpa/inet.h>
in_addr_t inet_addr(const char* string);
成功时返回32位大端序整数型值,失败时返回INADDR_NONE
应用示范:
char * addr = "1.2.3.4";
unsigned long conv_addr = inet_addr(addr);
if(conv_addr == INADDR_NONE)
printf("Error occureded \n");
else
printf("Network ordered integer addr: %#lx \n\n", conv_addr);
最后输出:
Network ordered integer addr: 0x4030201
(2)inet_aton
inet_aton
函数和inet_addr
函数在功能上完全相同:
#include<arpa/inet.h>
int inet_aton(const char* string, struct in_addr * addr);
string 含有需要转换的IP地址信息的字符串地址值
addr 将保存转换结果的in_addr结构体变量的地址值
成功时返回1(true),失败时返回0(false)
应用示范:
char * addr = "127.232.124.79";
struct sockaddr_in addr_inet;
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);
}
(3)inet_ntoa
与inet_aton
功能相反,将网络字节序整数型IP地址转换为我们熟悉的字符串形式
#include<arpa/inet.h>
char* inet_ntoa(struct in_addr adr);
成功时返回字符串地址值,失败时返回-1
调用完这个函数后,最好立即将字符串信息复制到其他内存空间,放置下次调用该函数时覆盖了这次的结果:
struct sockaddr_in addr;
char * str_ptr;
char str_arr[20];
addr.sin_addr.s_addr = htonl(0x1020304);
str_ptr = inet_ntoa(addr.sin_addr);
strcpy(str_arr,str_ptr);
printf("Dotted_Decimal notation: %s \n",str_prt); 此时输出结果是1.2.3.4,后面再次调用inet_ntoa函数后,会变成新的值
printf("Dotted_Decimal notation: %s \n",str_arr); 此时输出结果是1.2.3.4,后面再次调用inet_ntoa函数后,仍保存这个值
网络地址初始化
主要是针对服务器端:
struct sockaddr_in addr;
char* serv_ip = "211.217.168.13"; //声明IP地址字符串
char* serv_port = "9190"; //声明端口号字符串
memset(&addr, 0, sizeof(addr)); //结构体变量addr的所有成员初始化为0,这样sockaddr_in结构体的成员sin_zero初始化为0
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化
addr.sin_port = htons(atoi(serv_port)); //基于字符串的端口号初始化。atoi函数将字符串类型的端口值转换成整数型
客户端地址信息初始化
服务器端,声明sockaddr_in
结构体变量,将其初始化为赋予服务器端IP和套接字的端口号,然后调用bind
函数;
客户端,声明sockaddr_in
结构体变量,并初始化为要与之连接的服务器端套接字IP和端口号,然后调用connect
函数。
INADDR_ANY
服务器端优先采用INADDR_ANY
这种方式。
char* serv_ip = "211.217.168.13"; //声明IP地址字符串
addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化
可以缩为如下一句话:
addr.sin_addr.s_addr = htonl(INADDR_ANY); //可以自动获取运行服务器端的计算机IP地址
同一计算机中可以分配多个IP地址,实际IP地址的个数与计算机中安装的NIC的数量相等。
即使是服务器端套接字,也需要决定应接收哪个IP传来的(哪个NIC传来的)数据。
若只有1个NIC
,则直接使用INADDR_ANY
。
第一章的hello_server.c和hello_client.c的运行过程
127.0.0.1
是回送地址,指的是计算机自身IP
地址。
向套接字分配网络地址
完成了sockaddr_in结构体的初始化后,接下来要把初始化的地址信息分配给套接字,bind
函数负责此项操作。
#include<sys/socket.h>
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);
sockfd 要分配地址信息(IP地址和端口号)的套接字文件描述符
myaddr 存有地址信息的结构体变量地址值
addrlen 第二个结构体变量的长度
成功时返回0,失败时返回-1
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在Windows中的应用
#include<winsock2.h>
WSADATA wsaData; 库初始化
if(WSAStartup(MAKEWORD(2,2), &wsaData) != 0){
ErrorHandling("WSAStartup() error");
}
unsigned short host_port = 0x1234;
unsigned short net_port = htons(host_port); 将端口号从主机字节序变成网络字节序
unsigned long host_addr = 0x12345678;
unsigned long net_addr = htonl(host_addr); 将IP地址从主机字节序变成网络字节序
WSACleanup(); 关闭库
函数inet_addr、inet_ntoa在Windows中的应用
Windows中不存在inet_aton函数。
#include<stdio.h>
#include<string.h>
#include<winsock2.h>
void ErrorHandling(char * message);
int main(int argc, char * argv[]){
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
{//inet_addr函数调用示例
char* addr = "127.212.124.78";
unsigned long conv_addr = inet_addr(addr);
if(conv_addr == INADDR_NONE)
printf("Error occured! \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(char * message){
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
Windows环境下向套接字分配网络地址
SOCKET servSock;
struct sockaddr_in servAddr;
char * servPort = "9190";
//创建服务器套接字
servSock = socket(PF_INET, SOCK_STREAM, 0);
/*地址信息初始化*/
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(servPort));
//分配地址信息
bind(servSock, (struct sockaddr*)&servAddr, sizeof(servAddr));
...
WSAStringToAddress& WSAAddressToString
这两个转换函数功能与inet_ntoa
和inet_addr
完全相同,但有点在于支持多种协议,在IPv4和IPv6中都可以用。但是,缺点是这俩函数依赖特定平台,只能在Windows上使用,不能在Linux上用,这样会降低兼容性。
3.6 习题
(1)IP地址族IPv4和IPv6有何区别?在何种背景下诞生了IPv6?
IPV4是4字节地址族,IPV6是16字节地址族。IPV6的诞生是为了应对2010年前后IP地址耗尽的问题而提出的标准。
(2)通过IPV4网络ID、主机ID及路由器的关系说明向公司局域网中的计算机传输数据的过程。
首先数据传输的第一个环节是向目标IP所属的网络传输数据。此时使用的是IP地址中的网络ID。传输的数据将被传到管理网络的路由器,接受数据的路由器将参照IP地址的主机号找自己保存的路由表,找到对应的主机发送数据。
(3)套接字地址分为IP地址和端口号。为什么需要IP地址和端口号?或者说,通过IP可以区分哪些对象?通过端口号可以区分哪些对象?
IP地址是为了区分网络上的主机。端口号是区分同一主机下的不同的SOCKET,以确保软件准确收发数据。
(4)说明IP地址的分类方法,据此说出下面这些IP地址的分类
- A类地址首字节范围:0-127;网络ID占1字节,主机ID占3字节
- B类地址首字节范围:128-191;网络ID2字节,主机ID2字节
- C类地址首字节范围:192-223;网络ID3字节,主机ID1字节
下面是题目给的地址:
- 214.121.212.102(C)
- 120.101.122.89(A)
- 129.78.102.211(B)
(5)计算机通过路由器或交换机连接到互联网。请说出路由器和交换机的作用。
路由器是帮助数据传输到目的地的中介。不仅如此,还起到帮助连接本地网络的电脑和互联网的作用。 不太理解
(6)什么是知名端口?其范围是多少?知名端口中具有代表性的HTTP合同FTP端口号各是多少?
“知名端口(Well-known PROT)”是指预定分配给特定操作的端口。其范围是0~1023
,其中最知名的端口是HTTP:80
端口和TCP:21
。
(7)题目大概意思是:为什么bind中第二个参数是sockaddr,但是传入的是sockaddr_in。
bind函数第二个参数类型是sockaddr结构体,很难分配IP地址和端口号,它的IP地址和端口号的分配是通过sockaddr_in完成的。因为该结构体和sockaddr结构体的组成字节序和大小完全相同,所以可以强转。
(8)请解释大端序、小端序、网络字节序,并说明为何需要网络字节序。
小端序是把高位字节存储到高位地址上;大端序是把高位字节存储到低位地址上。因为保存栈的方式有差异,所以对网络传输数据的过程制定了标准,这就是“网路字节序”。而且,在网络字节序中,数据传输的标准是“大端序”。
(9)大端计算机希望将4字节整型数据12传到小端序计算机。请说出数据传输过程中发生的字节序变换过程。
因为网络字节序的顺序标准是“大端序”,所以大端序的计算机在网络传输中不需要先转换字节顺序,直接传输。但是接受数据的是小端序计算机,因此,要经过网络字节序转本地序的过程,再保存到存储设备上。
(10)怎么表示回送地址?其含义是什么?如果向回送地址传输数据将会发生什么情况?
回送地址表示计算机本身,为127.0.0.1
。因此,如果将数据传送到IP地址127.0.0.1,数据不会传输到网络的其他设备上,而是直接返回。