《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
- 《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
- 分配给套接字的IP地址和端口号
- 网络地址
- 网络地址分类和主机地址边界
- 用于区分套接字的端口号
- 数据传输过程示例
- 地址信息的表示
- 表示IPv4地址的结构体
- 结构体sockaddr_in的成员分析
- 成员sin_family
- 成员sin_port
- 成员sin_addr
- 成员sin_zero
- 网络字节序与地址变换
- 字节序与网络字节序
- 字节序转换
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
分配给套接字的IP地址和端口号
IP是为收发网络数据而分配给计算机的值。端口号是为区分程序中创建的套接字而分配给套接字的序号。
网络地址
IP地址有两种表达形式:
- IPv4:4字节地址族
- IPv6:16字节地址族
一定要记住IPv4和IPv6不只是在地址长度不同,在其具体的协议实现上是有很大程度的不同的。IPv6出现的主要目的是为了解决由于计算机数量的暴增导致IP地址可能出现不足的问题的,现在IPv6的地址范围可以让地球上任何一个沙子都拥有IP地址。
让我们继续说回到IPv4上,IPv4标准的4字节IP地址分为网络地址和主机(指计算机)地址,且分为A、B、C、D、E等类型。
类型 | 地址范围 | 网络地址位数 | 主机地址位数 | 可分配的网络数量 | 每个网络可分配的主机数量 |
---|---|---|---|---|---|
A | 1.0.0.0 - 126.255.255.255 | 8 | 24 | 128 | 16,777,216 |
B | 128.0.0.0 - 191.255.255.255 | 16 | 16 | 16,384 | 65,536 |
C | 192.0.0.0 - 223.255.255.255 | 24 | 8 | 2,097,152 | 256 |
D | 224.0.0.0 - 239.255.255.255 | 未分配 | 未分配 | 未分配 | 未分配 |
网络地址是用来标识一个特定网络的。它告诉路由器和其他网络设备,数据应该被发送到哪个网络。
主机地址是用来标识网络中具体设备的。它必须在网络中是唯一的,以确保数据能够被正确地发送到正确的设备。
现在我来举个例子来具体理解一下网络地址和主机地址的含义。
假设向WWW.SEMI.COM公司传输数据,该公司内部构建了局域网,把所有计算机连接起来。因此,首先应向SEMI.COM网络传输数据,也就是说,并非一开始就浏览所有4字节IP地址,进而找到目标主机;而是仅浏览4字节IP地址的网络地址,先把数据传到SEMI.COM的网络。SEMI.COM网络(构成网络的路由器)接收到数据后,浏览传输数据的主机地址(主机ID)并将数据传给目标计算机。
网络地址分类和主机地址边界
只需通过地址的第一个字节即可判断网络地址占用的字节数,因为我们根据地址的边界区分网络地址,如下所示:
- A类地址的首字节范围:0~127
- B类地址的首字节范围:128-191
- C类地址的首字节范围:192~223
还有如下这种表述方式:
- A类地址的首位以0开始
- B类地址的前2位以10开始
- C类地址的前3位以110开始
正因如此,通过套接字收发数据时,数据传到网络后即可轻松找到正确的主机。
用于区分套接字的端口号
端口号是计算机为了区分程序中创建的不同套接字,而分配给套接字的序号,由16位组成,端口号唯一,可配分的范围在0~ 65535,其中0~10223是知名端口,一般分配给特定应用程序,所以应当分配范围之外的值。
端口号与套接字是一一对应关系,端口号与程序的不同通信功能是一一对应关系。
TCP套接字和UDP套接字不会共用端口号,所以允许重复。
数据传输过程示例
下面是基于IP地址的数据传输过程图:
主要步骤:
- 主机向203.211.217.202和203.211.172.103传输数据。
- 其中203.211.217和203.211.172是网络ID,通过网络ID可以把数据传输到指定的网络(路由器或交换机)。
- 202和103是主机ID,网络(路由器或交换机)通过主机ID将数据传输到指定的设备上。
- 操作系统收到数据后,根据数据包里的端口号,将数据传输到对应的程序上。
地址信息的表示
应用程序中使用的IP地址和端口号以结构体的形式给出了定义。
表示IPv4地址的结构体
此结构体将作为地址信息传递给bind函数。
struct sockaddr_in
{
sa_family_t sin_family; // 地址族
uint16_t sin_port; // 16位TCO/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地址
};
这些数据类型可以参考 POSIX,它是为 UNIX 系列操作系统设立的标准,它定义了一些其他数据类型,如下表所示:
数据类型名称 | 数据类型说明 | 声明的头文件 |
---|---|---|
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 | 地址族 | sys/socket.h |
socklen_t | 长度 | sys/socket.h |
in_addr_t | IP地址,声明为uint32_t | netinet/in.h |
in_port_t | 端口号,声明为uint16_t | netinet/in.h |
看到这么长的类型表,有人不禁会问,为什么要搞出这么长的类型名呢?
其中一个很大的原因就是移植性的问题,如果适用于一个32位计算机的代码搬到64位的计算机上运行可定会出现由于位数不同导致的int被解释为不同的字节大小,这种问题是万万不可发生的。因此如果使用int32_t类型的数据,就能保证任何时候都占用4字节,即使转到不同字节的计算机上。
结构体sockaddr_in的成员分析
成员sin_family
每种协议族适用的地址族均不同。比如,IPv4使用4字节地址族,IPv6使用16字节地址族。
地址族(Address Family) | 含义 |
---|---|
AF_INET | IPv4网络协议中使用的地址族 |
AF_INET6 |IPv6网络协议中使用的地址族
AF_LOCAL| 本地通信中采用的UNIX协议的地址族
AF_LOCAL是为了说明具有多种地址族而添加的。
成员sin_port
该成员保存16位端口号,重点在于,它以网络字节序保存。
成员sin_addr
该成员保存32位地址信息,且也以网络字节序保存。为理解好该成员,应同时观察结构体in_addr。但结构体in_addr明为uint32_t,因此只需当作32位整数型即可。
成员sin_zero
无特殊含义。只是为使结构体sockaddr_in的大小和sockaddr结构体保持一致而插入的成员。必需填充为0,否则无法得到想要的结果。后面会另外讲解sockaddr。
从之前介绍的代码也可看出,sockaddr_in结构体变量地址值将以如下方式传递给bind函数。稍后将给出关于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_t sin_family; // 地址族
char sa_data[14]; // 地址信息
};
这个结构体结构相对于sockaddr_in来说,它将后三个成员都放入sa_data之中。而这对于包含地址信息非常麻烦,继而有了新的结构体sockaddr_in。但是最后还是要转换为sockaddr型的结构体变量,再传递给bind函数即可。
网络字节序与地址变换
字节序与网络字节序
CPU内存保存数据有两种方式:
- 大端序: 高位字节存放到低位地址
- 小端序: 高位字节存放到高位地址
示例:
0x1234567中,0x12是最高位字节,0x67是最低位字节,大端序中先保存最高位。
0x1234567中,0x12是最高位字节,0x67是最低位字节,小端序中先保存最低位。
0x12和0x34构成的大端序系统值与0x34和0x12构成的小端序系统值相同。换言之,只有改变数据保存顺序才能被识别为同一值。
如果大端序系统传输数据0x1234时未考虑字节序问题,而直接以0x12、0x34的顺序发送。结果接收端以小端序方式保存数据,因此小端序接收的数据变成0x3412,而非0x1234,就会出现问题。
正因如此,在通过网络传输数据时约定统一方式,这种约定称为网络字统一节序(Network Byte Order),非常简单——统一为大端序。因此,所有计算机接受数据时应识别该数据时网络字节格式,小端序系统传输数据时应转换为大端序的排列方式。
字节序转换
为了统一标准,在网络传输前,得先把主机数据数组转化为大端序的网络字节序格式,下面是四种转换字节序的函数:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
s指short,l指long,h指主机(host)字节序,n指网络(network)字节序。
因此,htons指,把short类型数据从主机字节序转换为网络字节序;ntohl指,把long类型数据从网络字节序转换为主机字节序。
示例程序:
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
unsigned short host_port = 0x1234;
unsigned short net_port;
unsigned long host_addr = 0x12345678;
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;
}