把套接字比喻成电话,那么目前只安装了电话机,本章讲解给电话机分配号码的方法,即给套接字分配 IP 地址和端口号。

3.1 分配给套接字的 IP 与端口号

IP 是 Internet Protocol(网络协议)的简写,是为手法网络数据而分配给计算机的值。端口号并非赋予计算机的值,而是为了区分程序中创建的套接字而分配给套接字的端口号。

3.1.1 网络地址(Internet Address)

为使计算机连接到网络并收发数据,必须为其分配 IP 地址。IP 地址分为两类。

  • IPV4(Internet Protocol version 4)4 字节地址族
  • IPV6(Internet Protocol version 6)6 字节地址族

两者之间的主要差别是 IP 地址所用的字节数,目前通用的是 IPV4 , IPV6 的普及还需要时间。

IPV4 标准的 4 字节 IP 地址分为 网络地址 + 主机地址,且分为 A、B、C、D、E 等类型。

C03 地址族与数据序列 - 图1

数据传输过程:

C03 地址族与数据序列 - 图2

某主机向 203.211.172.103 和 203.211.217.202 传递数据,其中 203.211.172 和 203.211.217 为该网络的网络地址,所以「向相应网络传输数据」实际上是向构成网络的路由器或者交换机传输数据,然后又路由器或者交换机根据数据中的主机地址向目标主机传递数据。

3.1.2 网络地址分类与主机地址边界

只需通过IP地址的第一个字节即可判断网络地址占用的总字节数,因为我们根据IP地址的边界区分网络地址,如下所示:

  • A 类地址的首位以 0 开始
  • B 类地址的前2位以 10 开始
  • C 类地址的前3位以 110 开始

相当于:

  • A 类地址的首字节范围为:0~127
  • B 类地址的首字节范围为:128~191
  • C 类地址的首字节范围为:192~223

因此套接字收发数据时,数据传到网络后即可轻松找到主机。

3.1.3 用于区分套接字的端口号

IP地址用于区分计算机,只要有IP地址就能向目标主机传输数据,但是只有这些还不够,我们需要把信息传输给具体的应用程序。

所以计算机一般有 NIC(网络接口卡)数据传输设备。通过 NIC 接受的数据内有端口号,操作系统参考端口号把信息传给相应的应用程序。

端口号由 16 位构成,可分配的端口号范围是 0~65535 。但是 0~1023 是知名端口,一般分配给特定的应用程序,所以应当分配给此范围之外的值。

虽然端口号不能重复,但是 TCP 套接字和 UDP 套接字不会共用端接口号,所以允许重复。如果某 TCP 套接字使用了 9190 端口号,其他 TCP 套接字就无法使用该端口号,但是 UDP 套接字可以使用。

3.2 地址信息的表示

本节围绕如何以结构体的方式表示目标地址。

3.2.1 表示 IPV4 地址的结构体

  1. struct sockaddr_in{
  2. sa_family_t sin_family; // 地址族(Address Family)
  3. uint16_t sin_port; // 16 位 TCP/UDP 端口号
  4. struct in_addr sin_addr; // 32位 IP 地址
  5. char sin_zero[8]; // 不使用
  6. };
  7. struct in_addr
  8. {
  9. in_addr_t s_addr; // 32位IPV4地址
  10. };

关于以上两个结构体的一些数据类型:
image.png
为什么要额外定义这些数据类型呢?
这是考虑扩展性的结果:如果使用int32_t类型,就能保证数据在任何时候都占用4字节,即便将来使用64位表示int类型也是如此。

3.2.2 结构体 sockaddr_in 的成员分析

  • 成员 sin_family

每种协议适用的地址族不同,比如,IPV4 使用 4 字节的地址族,IPV6 使用 16 字节的地址族。

地址族(Address Family) 含义
AF_INET IPV4用的地址族
AF_INET6 IPV6用的地址族
AF_LOCAL 本地通信中采用的 Unix 协议的地址族

AF_LOACL 只是为了说明具有多种地址族而添加的。

  • 成员 sin_port
    该成员保存 16 位端口号,重点在于,它以网络字节序保存。
  • 成员 sin_addr
    该成员保存 32 为IP地址信息,且也以网络字节序保存
  • 成员 sin_zero
    无特殊含义。只为结构体 sockaddr_in 大小要与sockaddr保持一致(bind中要进行强转,大小都为15B),必须填充为0


之前的代码:

  1. if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
  2. error_handling("bind() error");

此处 bind 第二个参数期望得到的是 sockaddr 结构体变量的地址值,包括地址族、端口号、IP地址等。

  1. struct sockaddr
  2. {
  3. sa_family_t sin_family; //地址族
  4. char sa_data[14]; //地址信息
  5. }

结构体成员 sa_data 保存的地址信息中需要包含IP地址和端口号,剩余部分应该填充 0 ,但是这样对于包含地址的信息非常麻烦,所以出现了 sockaddr_in 结构体,然后强制转换成 sockaddr 类型,则生成符合 bind 条件的参数。

3.3 网络字节序与地址变换

不同的 CPU 中,4 字节整数值1在内存空间保存方式是不同的。

有些 CPU 这样保存:

  1. 00000000 00000000 00000000 00000001

有些 CPU 这样保存:

  1. 00000001 00000000 00000000 00000000

两种一种是顺序保存,一种是倒序保存 。

3.3.1 字节序(Order)与网络字节序

CPU 保存数据的方式有两种,这意味着 CPU 解析数据的方式也有 2 种:

  • 大端序(Big Endian):高位字节存放到低位地址
  • 小端序(Little Endian):高位字节存放到高位地址

C03 地址族与数据序列 - 图4
C03 地址族与数据序列 - 图5

两台字节序不同的计算机在数据传递的过程中可能出现的问题:

C03 地址族与数据序列 - 图6
因为这种原因,所以在通过网络传输数据时必须约定统一的方式,这种约定被称为网络字节序,非常简单,统一为大端序。即,先把数据数组转化成大端序格式再进行网络传输

3.3.2 字节序转换

帮助转换字节序的函数:

  1. unsigned short htons(unsigned short); // htons: host to net short
  2. unsigned short ntohs(unsigned short);
  3. unsigned long htonl(unsigned long);
  4. unsigned long ntohl(unsigned long);

通过函数名称掌握其功能,只需要了解:

  • htons 的第一位 h 代表主机(host)字节序。
  • htons 的倒数第二位 n 代表网络(network)字节序。
  • s 代表 short
  • l 代表 long

下面的代码是示例,说明以上函数调用过程:

  1. #include <stdio.h>
  2. #include <arpa/inet.h>
  3. int main(int argc, char *argv[])
  4. {
  5. unsigned short host_port = 0x1234;
  6. unsigned short net_port;
  7. unsigned long host_addr = 0x12345678;
  8. unsigned long net_addr;
  9. net_port = htons(host_port); //转换为网络字节序
  10. net_addr = htonl(host_addr);
  11. printf("Host ordered port: %#x \n", host_port);
  12. printf("Network ordered port: %#x \n", net_port);
  13. printf("Host ordered address: %#lx \n", host_addr);
  14. printf("Network ordered address: %#lx \n", net_addr);
  15. return 0;
  16. }

3.4 网络地址的初始化与分配

3.4.1 将字符串信息转换为网络字节序的整数型

sockaddr_in 中需要的是 32 位整数型,但是我们只熟悉点分十进制表示法,那么改如何把类似于 201.211.214.36 转换为 4 字节的整数类型数据呢 ?幸运的是,有一个函数可以帮助我们完成它:

  1. #include <arpa/inet.h>
  2. in_addr_t inet_addr(const char *string);

具体示例:

  1. #include <stdio.h>
  2. #include <arpa/inet.h>
  3. int main(int argc, char *argv[])
  4. {
  5. char *addr1 = "1.2.3.4";
  6. char *addr2 = "1.2.3.256";
  7. unsigned long conv_addr = inet_addr(addr1);
  8. if (conv_addr == INADDR_NONE)
  9. printf("Error occured! \n");
  10. else
  11. printf("Network ordered integer addr: %#lx \n", conv_addr);
  12. conv_addr = inet_addr(addr2);
  13. if (conv_addr == INADDR_NONE)
  14. printf("Error occured! \n");
  15. else
  16. printf("Network ordered integer addr: %#lx \n", conv_addr);
  17. return 0;
  18. }

inet_aton 函数与 inet_addr 函数在功能上完全相同,也是将字符串形式的IP地址转换成整数型的IP地址。只不过该函数用了 in_addr 结构体,且使用频率更高。

  1. #include <arpa/inet.h>
  2. int inet_aton(const char *string, struct in_addr *addr);
  3. /*
  4. 成功时返回 1 ,失败时返回 0
  5. string: 含有需要转换的IP地址信息的字符串地址值
  6. addr: 将保存转换结果的 in_addr 结构体变量的地址值
  7. */

函数调用示例:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <arpa/inet.h>
  4. void error_handling(char *message);
  5. int main(int argc, char *argv[])
  6. {
  7. char *addr = "127.232.124.79";
  8. struct sockaddr_in addr_inet;
  9. if (!inet_aton(addr, &addr_inet.sin_addr))
  10. error_handling("Conversion error");
  11. else
  12. printf("Network ordered integer addr: %#x \n", addr_inet.sin_addr.s_addr);
  13. return 0;
  14. }
  15. void error_handling(char *message)
  16. {
  17. fputs(message, stderr);
  18. fputc('\n', stderr);
  19. exit(1);
  20. }

可以看出,已经成功的把转换后的地址放进了 addr_inet.sin_addr.s_addr 中。

还有一个函数,与 inet_aton() 正好相反,它可以把网络字节序整数型IP地址转换成我们熟悉的字符串形式,函数原型如下:

  1. #include <arpa/inet.h>
  2. char *inet_ntoa(struct in_addr adr);

该函数将通过参数传入的整数型IP地址转换为字符串格式并返回。但要小心,返回值为 char 指针,返回字符串地址意味着字符串已经保存在内存空间,但是该函数未向程序员要求分配内存,而是再内部申请了内存保存了字符串。也就是说调用了该函数候要立即把信息复制到其他内存空间。因此,若再次调用 inet_ntoa 函数,则有可能覆盖之前保存的字符串信息。总之,再次调用 inet_ntoa 函数前返回的字符串地址是有效的。若需要长期保存,则应该将字符串复制到其他内存空间。

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <arpa/inet.h>
  4. int main(int argc, char *argv[])
  5. {
  6. struct sockaddr_in addr1, addr2;
  7. char *str_ptr;
  8. char str_arr[20];
  9. addr1.sin_addr.s_addr = htonl(0x1020304);
  10. addr2.sin_addr.s_addr = htonl(0x1010101);
  11. //把addr1中的结构体信息转换为字符串的IP地址形式
  12. str_ptr = inet_ntoa(addr1.sin_addr);
  13. strcpy(str_arr, str_ptr);
  14. printf("Dotted-Decimal notation1: %s \n", str_ptr);
  15. inet_ntoa(addr2.sin_addr);
  16. printf("Dotted-Decimal notation2: %s \n", str_ptr);
  17. printf("Dotted-Decimal notation3: %s \n", str_arr);
  18. return 0;
  19. }

编译运行:

  1. gcc inet_ntoa.c -o ntoa
  2. ./ntoa

输出:

  1. Dotted-Decimal notation1: 1.2.3.4
  2. Dotted-Decimal notation2: 1.1.1.1
  3. Dotted-Decimal notation3: 1.2.3.4

3.4.2 网络地址初始化

结合前面的内容,介绍套接字创建过程中,常见的网络信息初始化方法:

  1. struct sockaddr_in addr;
  2. char *serv_ip = "211.217,168.13"; //声明IP地址族
  3. char *serv_port = "9190"; //声明端口号字符串
  4. memset(&addr, 0, sizeof(addr)); //结构体变量 addr 的所有成员初始化为0
  5. addr.sin_family = AF_INET; //制定地址族
  6. addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化
  7. addr.sin_port = htons(atoi(serv_port)); //基于字符串的IP地址端口号初始化

3.6 习题

  1. IP地址族 IPV4 与 IPV6 有什么区别?在何种背景下诞生了 IPV6?
    答:主要差别是IP地址所用的字节数,目前通用的是IPV4,目前IPV4的资源已耗尽,所以诞生了IPV6,它具有更大的地址空间。

  2. 通过 IPV4 网络 ID 、主机 ID 及路由器的关系说明公司局域网的计算机传输数据的过程
    答:网络ID是为了区分网络而设置的一部分IP地址,假设向www.baidu.com公司传输数据,该公司内部构建了局域网。因为首先要向baidu.com传输数据,也就是说并非一开始就浏览所有四字节IP地址,首先找到网络地址,进而由baidu.com(构成网络的路由器)接收到数据后,传输到主机地址。比如向 203.211.712.103 传输数据,那就先找到 203.211.712 然后由这个网络的网关找主机号为 103 的机器传输数据。

  3. 套接字地址分为IP地址和端口号,为什么需要IP地址和端口号?或者说,通过IP地址可以区分哪些对象?通过端口号可以区分哪些对象?
    答:有了IP地址和端口号,才能把数据准确的传送到某个应用程序中。通过IP地址可以区分具体的主机,通过端口号可以区分主机上的应用程序。

  4. 请说明IP地址的分类方法,并据此说出下面这些IP的分类。

    • 214.121.212.102(C类)
    • 120.101.122.89(A类)
    • 129.78.102.211(B类)

分类方法:A 类地址的首字节范围为:0~127、B 类地址的首字节范围为:128~191(191=126+64)、C 类地址的首字节范围为:192~223

  1. 计算机通过路由器和交换机连接到互联网,请说出路由器和交换机的作用。
    image.png

通过拓扑图可以看到:
每一个路由器与其之下连接的设备,其实构成一个局域网
交换机工作在路由器之下,就是也就是交换机工作在局域网内
交换机用于局域网内网的数据转发
路由器用于连接局域网和外网

  1. 什么是知名端口?其范围是多少?知名端口中具有代表性的 HTTP 和 FTP 的端口号各是多少?
    答:知名端口是要把该端口分配给特定的应用程序,范围是 0~1023 ,HTTP 的端口号是 80 ,FTP 的端口号是20和21
  2. 向套接字分配地址的 bind 函数原型如下:

    1. int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

    而调用时则用:

    1. bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)

    此处 serv_addr 为 sockaddr_in 结构体变量。与函数原型不同,传入的是 sockaddr_in 结构体变量,请说明原因。
    答:因为对于详细的地址信息使用 sockaddr 类型传递特别麻烦,进而有了 sockaddr_in 类型,其中基本与前面的类型保持一致,还有 sa_sata[4] 来保存地址信息,剩余全部填 0,所以强制转换后,不影响程序运行。

  3. 请解释大端序,小端序、网络字节序,并说明为何需要网络字节序。
    答:CPU 向内存保存数据有两种方式,大端序是高位字节存放低位地址,小端序是高位字节存放高位地址,网络字节序是为了方便传输的信息统一性,统一成了大端序。

  4. 大端序计算机希望把 4 字节整数型 12 传递到小端序计算机。请说出数据传输过程中发生的字节序变换过程。
    答:0x12->0x21

  5. 怎样表示回送地址?其含义是什么?如果向会送地址处传输数据将会发生什么情况?
    答:127.0.0.1 表示回送地址,指的是计算机自身的IP地址,无论什么程序,一旦使用回送地址发送数据,协议软件立即返回,不进行任何网络传输。