当前位置:   article > 正文

《TCP/IP网络编程》(第三章)地址族和数据序列

《TCP/IP网络编程》(第三章)地址族和数据序列

1.IP地址

IP地址是Internet Protocol(网络协议)的缩写,是为了收发网络数据而分配给计算机的值,分为两类:IPv4IPv6,差别主要是表示IP地址所用的字节数,前者用四字节地址族,后者用16字节地址族。IPv6是为了应对IPv4地址耗尽而提出的标准,2019 年 11 月 25 日已分配完公网 IPv4 地址,但本书还是以IPv4为例子进行展示。
在这里插入图片描述

下图展示IPv4地址族,分为A、B、C、D、E(忽略)等类型:
在这里插入图片描述
可以通过IP地址的第一个字节即可判断网络地址占用的字节数,进而区分网络地址的类型:

  1. A类地址:
    首字节范围:0到127
    首位:0
    网络ID占用:1个字节
    主机ID占用:3个字节
  2. B类地址:
    首字节范围:128到191
    首位:10
    网络ID占用:2个字节
    主机ID占用:2个字节
  3. C类地址:
    首字节范围:192到223
    首位:110
    网络ID占用:3个字节
    主机ID占用:1个字节

例如,如果一个IP地址的首字节是192(二进制为11000000),我们立即知道这是一个C类地址,网络ID占用3个字节,主机ID占用1个字节.

网络ID是用来标识一个特定网络的。它告诉路由器和其他网络设备,数据应该被发送到哪个网络。
主机ID是用来标识网络中具体设备的。它必须在网络中是唯一的,以确保数据能够被正确地发送到正确的设备。

2.端口号

端口号是计算机为了区分程序中创建的不同套接字,而分配给套接字的序号,由16位组成,端口号唯一,可配分的范围在0~ 65535,其中0~10223是知名端口,一般分配给特定应用程序,所以应当分配范围之外的值。
其次TCP套接字和UDP套接字不会共用端口号,所以允许重复。

3.数据传输过程

下面是基于IP地址的数据传输过程图:
在这里插入图片描述

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

4.IPv4的地址结构体表示

结构体定义如下:

struct sockaddr_in ipv4_address {
    sa_family_t     sin_family;   // 地址族,对于IPv4通常是AF_INET
    in_port_t       sin_port;     // 端口号,网络字节序
    struct in_addr  sin_addr;     // IPv4地址,具体结构见下
    char            sin_zero[8];  // 填充字符,用于与sockaddr结构体兼容
};
//其中的struct 定义如下
struct in_addr {
    in_addr_t       s_addr; // 存储IPv4地址的32位整数,网络字节序
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

sin_family
表示地址族,对于IPv4地址,它通常被设置为AF_INET,具体分类如下:

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

sin_port
是一个网络字节序的端口号,用于标识特定的服务或应用程序。

sin_addr
是一个32位的无符号整数,用来存储IPv4地址,且必须以网络字节序(大端序)表示。

sin_zero
是一个填充字段,用于确保sockaddr_in结构体的大小与sockaddr结构体兼容,因为某些系统调用可能同时处理IPv4和IPv6地址。

5.网络字节序

字节序: 是指计算机处理器在内存中存储多字节数据类型(如整数、长整型、双精度浮点数等)时所采用的字节排列顺序。分为两种:

  1. 大端序: 高位字节存放到低位地址
    在这里插入图片描述
    0x1234567中,0x12是最高位字节,0x67是最低位字节,大端序中先保存最高位。

  2. 小端序: 高位字节存放到高位地址
    在这里插入图片描述
    0x1234567中,0x12是最高位字节,0x67是最低位字节,小端序中先保存最低位。

网络字节序:
是一种在互联网上进行通信时使用的字节序,它保证了不同计算机架构之间数据的一致性。网络字节序遵循大端序),即最高有效字节(MSB)存储在最低的内存地址处。

6.字节序转换

为了统一标准,在网络传输前,得先把主机数据数组转化为大端序的网络字节序格式,下面是四种转换字节序的函数:

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类型数据从网络字节序转换为主机字节序
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

7.将字符串信息转化为网络字节序

因为在IPv4的结构体定义中,IPv4地址为32位整数型,所以我们可以通过inet_addr()函数进行转化,而且该函数还可以检测无效的IP地址,代码如下:

in_addr_t inet_addr(const char *cp);
//cp 是一个指向包含 IPv4 地址点分十进制字符串的字符指针,例如 "192.168.1.1"。
  • 1
  • 2

Linux系统下运行代码:

#include <arpa/inet.h> // 包含inet_addr函数的头文件
#include <stdio.h>

int main() {
    const char *ip_str = "1.2.3.4"; // 这是要转换的IP地址字符串
    in_addr_t ip_addr; // 存储转换后的网络字节序整数型

    // 使用inet_addr函数将字符串转换为网络字节序的32位整数
    ip_addr = inet_addr(ip_str);

	// 检查转换是否成功
    if (ip_addr == INADDR_NONE) {
        fprintf(stderr, "Invalid IP address\n");
        return 1;
    }

    // 打印转换后的网络字节序整数
    printf("The IP address in network byte order is: %u\n", (unsigned int)ip_addr);
	//输出结果为0x4030201
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Windows系统下运行代码:
因为inet_addr()是Windows Sockets的一部分,所以需要初始化Winsock

#include <winsock2.h> // 包含Winsock的头文件
#include <stdio.h>
#include <stdlib.h>

int main() {

    // 因为inet_addr()是Windows Sockets的一部分,所以需要初始化Winsock
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (result != 0) {
        fprintf(stderr, "WSAStartup failed: %d\n", result);
        return 1;
    }

    const char *ip_str = "1.2.3.4"; // 要转换的IP地址字符串
    unsigned long ip_addr; // 存储转换后的网络字节序整数型

    // 使用inet_addr函数将字符串转换为网络字节序的32位整数
    ip_addr = inet_addr(ip_str);
    
	// 检查转换是否成功
    if (ip_addr == INADDR_NONE) {
        fprintf(stderr, "Invalid IP address\n");
        WSACleanup(); // 清理Winsock
        return 1;
    }

    // 打印转换后的网络字节序整数
    printf("The IP address in network byte order is: %#x\n", ip_addr);
    //输出结果为0x4030201
    system("pause");
    // 清理Winsock
    WSACleanup();

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

也可以使用inet_aton()函数,与inet_addr()函数不同,inet_aton()可以将转化后的32位网络字节序自动代入in_addr结构体,所以使用更加频繁

int inet_aton(const char *cp, struct in_addr *inp);
//cp:是一个指向点分十进制IPv4地址字符串的指针。
//inp:是一个指向 in_addr 结构的指针,该结构用于接收转换后的网络字节序的32位整数。
  • 1
  • 2
  • 3

8.将网络字节序转化为字符串信息

使用inet_aton()函数,将网络字节序ip地址转化为字符串形式,语法如下:

char *inet_ntoa(struct in_addr in);
//in 是一个 struct in_addr 类型的变量,它包含了要转换的网络字节序的IPv4地址。
  • 1
  • 2

函数返回值是一个指向静态分配的字符数组的指针,该数组包含了转换后的点分十进制IPv4地址字符串。注意, 返回的字符串是一个指向静态存储的指针,这意味着该字符串不应被修改,并且在每次 inet_ntoa 调用后都可能改变,如果你需要保留这个字符串,应该立即复制它到安全的存储位置。

Linux系统下运行代码:

#include <arpa/inet.h>
#include <stdio.h>

int main() {
    struct in_addr ip_addr;
    // 假设我们有一个网络字节序的32位整数
    ip_addr.s_addr = htonl(0x4030201);

    // 将网络字节序的IPv4地址转换为点分十进制的字符串
    char *ip_str = inet_ntoa(ip_addr);

    // 打印转换后的IP地址字符串
    printf("The IP address is: %s\n", ip_str);
	//输出结果为4.3.2.1
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

Windows系统下运行代码:
因为inet_aton()是Windows Sockets的一部分,所以需要初始化Winsock

#include <winsock2.h> // 包含Winsock的头文件
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 初始化Winsock
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (result != 0) {
        fprintf(stderr, "WSAStartup failed: %d\n", result);
        return 1;
    }

    // 十六进制数转换为网络字节序的整数
    struct sockaddr_in addr;
    addr.sin_addr.s_addr= htonl(0x4030201);

    // 将网络字节序的整数转换回点分十进制的IPv4地址字符串
    char *ip_str = inet_ntoa(addr.sin_addr);

    // 打印转换后的IP地址
    printf("The IP address is: %s\n", ip_str);
    system("pause");
    // 清理Winsock
    WSACleanup();

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

Windows系统补充说明: WSAStringToAddress()WSAAddressToString()函数,是Windows特有的函数

  • WSAStringToAddress() :是 Windows Sockets的一部分,它用于将字符串形式的网络地址转换为相应的 socket 地址结构,且自动填充到sockaddr 中
int WSAStringToAddress(
  const char *AddressString,
  int AddressFamily,
  const struct sockaddr *lpProtocolInfo,
  struct sockaddr *lpSocketAddress,
  int *lpAddressLength
);
//AddressString:指向包含地址字符串的指针,例如 "192.168.1.1" 或 "example.com"。
//AddressFamily:指定地址族,对于 IPv4 使用 AF_INET,对于 IPv6 使用 AF_INET6。
//lpProtocolInfo:指向协议特定信息的可选指针,默认为 NULL。
//lpSocketAddress:指向接收转换结果的 sockaddr 结构的指针。
//lpAddressLength:在调用时,指向 sockaddr 结构的大小的指针。在函数返回后,它表示实际填充到结构中的数据的大小。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • WSAAddressToString():是 Windows Sockets API (Winsock) 的一部分,自动读取sockaddr 结构体,将其中的 socket 地址转换为它的字符串表示形式。
int WSAAddressToStringA(
  LPSOCKADDR lpsaAddress,
  DWORD dwAddressLength,
  LPWSAPROTOCOL_INFOW lpProtocolInfo,
  LPSTR lpszAddressString,
  LPDWORD lpdwAddressStringLength
);
//lpsaAddress:指向 sockaddr 结构的指针,包含要转换的地址信息。
//dwAddressLength:sockaddr 结构的大小。
//lpProtocolInfo:指向 WSAPROTOCOL_INFOW 结构的指针,可以为 NULL。
//lpszAddressString:指向缓冲区的指针,该缓冲区接收转换后的地址字符串。
//lpdwAddressStringLength:指向一个 DWORD 的指针,该 DWORD 指定 lpszAddressString 缓冲区的长度。函数返回时,它表示实际存储在缓冲区中的字符数,包括空终止符。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

9.套接字初始化

当学习之后,我们再来看之前的代码,就会发现分为几个部分,这里展示套接字初始化部分,,Linux系统和Windows系统初始化部分代码几乎完全一致:
PS: 使用INADDR_ANY可以自动获取本地计算机的IP地址。

Linux系统

int serv_sock; // 定义服务器套接字
struct sockaddr_in serv_addr; // 定义服务器地址结构体
char* serv_port="9190";//定义端口号

//创建套接字
serv_sock=socket(PF_INET, SOCK_STREAM, 0);

//地址信息初始化
memset(&servAddr, 0, sizeof(servAddr));// 清空servAddr结构
//设置结构体
servAddr.sin_family = AF_INET;// 设置地址族为IPv4
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置监听的IP地址
servAddr.sin_port = htons(atoi(serv_port));// 设置监听端口

//把设置好的地址信息分配给套接字,使用bind函数绑定
if (bind(serv_sock, (struct sockadd* )&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
		error_handling("bind() error");//绑定失败则返回异常
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

Windows系统

SOCKET serv_sock; // 定义服务器套接字
struct sockaddr_in serv_addr; // 定义服务器地址结构体
char* serv_port="9190";//定义端口号

//创建套接字
serv_sock=socket(PF_INET, SOCK_STREAM, 0);

//地址信息初始化
memset(&servAddr, 0, sizeof(servAddr));// 清空servAddr结构
//设置结构体
servAddr.sin_family = AF_INET;// 设置地址族为IPv4
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置监听的IP地址
servAddr.sin_port = htons(atoi(serv_port));// 设置监听端口

//把设置好的地址信息分配给套接字,使用bind函数绑定
if (bind(serv_sock, (struct sockadd* )&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
		error_handling("bind() error");//绑定失败则返回异常
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

10.回顾运行过程

回顾第一章里面的代码运行部分,Linux平台Windows平台,我们便可以理解为什么那样运行。

在这里插入图片描述
上图是开启服务器命令,意味着通过9190端口创建服务器套接字并运行程序,这里之所以没有输入IP地址,是因为通过INADDR_ANY已经自动获取了本机的IP地址。
在这里插入图片描述
上图是开启客户端命令,意味着尝试连接IP地址127.0.0.1(这里是本机地址,因为运行程序的服务器和客户端在一台电脑上,正常情况下应该是服务器端IP地址),并连接到服务器端的9190端口

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/608859
推荐阅读
相关标签
  

闽ICP备14008679号