《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列

发布于:2024-11-04 ⋅ 阅读:(130) ⋅ 点赞:(0)

《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地址的数据传输过程图:

在这里插入图片描述

主要步骤:

  1. 主机向203.211.217.202和203.211.172.103传输数据。
  2. 其中203.211.217和203.211.172是网络ID,通过网络ID可以把数据传输到指定的网络(路由器或交换机)。
  3. 202和103是主机ID,网络(路由器或交换机)通过主机ID将数据传输到指定的设备上。
  4. 操作系统收到数据后,根据数据包里的端口号,将数据传输到对应的程序上。

地址信息的表示

应用程序中使用的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;
}

网站公告

今日签到

点亮在社区的每一天
去签到