当前位置: 首页 > news >正文

网络编程核心:套接字绑定(bind函数)与 IP 地址转换处理

目录

一、绑定的核心函数 bind

1、参数详解

1.sockfd

2.addr

3.addrlen

2、返回值说明

二、struct sockaddr_in 结构体的深度解析

1、结构体的查找与平台差异

2、结构体成员详解

2.sin_port

3.sin_addr

4.sin_zero

3、结构体的使用示例

三、两个问题思考

1、为什么将该结构体强制转换为 const struct sockaddr* 类型?

1. 函数参数类型的统一性

2. 通用性和扩展性

3. 类型安全与兼容性

2、这样的强制转换会不会越界访问?

1. 内存布局的兼容性

2. 函数对地址族的处理

3. 长度参数的保障

四、绑定的本质理解

五、服务器类中引入 IP 地址和端口号

六、服务端绑定的详细步骤与代码实现

1、套接字创建

2、填充网络属性信息

3、绑定操作

七、IP 地址的两种表现形式

1、字符串 IP

2、整数 IP

八、整数 IP 存在的意义

九、字符串 IP 和整数 IP 相互转换的方式

十、系统提供的转换函数

1、inet_addr 函数

1. 函数原型及头文件

2. 函数功能

3. 参数说明

4. 返回值说明

5. 底层实现原理(简化理解)

6. 代码示例及详细注释

7. 注意事项

2、inet_ntoa 函数

1. 函数原型及头文件

2. 函数功能

3. 参数说明

4. 返回值说明

5. 底层实现原理(简化理解)

6. 代码示例及详细注释

7. 注意事项

十一、struct sockaddr_in 详解

1、结构体定义及作用

2、成员详解

十二、IP 地址的转换

1、点分十进制字符串转 32 位整数

2、32 位整数转点分十进制字符串


        在服务端成功创建套接字之后,这仅仅意味着在系统层面打开了一个特殊的“网络文件”。然而,此时操作系统尚处于迷茫状态,它并不清楚这个套接字对应的通信目标以及数据传输的方向,即不知道是要将接收到的数据写入磁盘还是刷到网卡,因为这个套接字还未与具体的网络信息关联起来。对于一款功能完备的服务器而言,绑定操作是至关重要的下一步,尤其是在我们编写不面向连接的 UDP 服务器时,绑定操作更是不可或缺。


一、绑定的核心函数 bind

在网络编程中,负责绑定操作的函数是 bind其函数原型如下:

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

1、参数详解

1.sockfd

  • 这个参数代表要绑定的文件的文件描述符。

  • 在之前创建套接字的过程中,我们通过 socket 函数获取到了一个文件描述符,这个文件描述符就如同是这个套接字在进程文件描述符表中的唯一标识。

  • 通过将该文件描述符传递给 bind 函数,我们明确指定了要将哪个套接字与给定的网络属性信息进行绑定。

  • 例如,在之前创建 UDP 套接字的示例中,我们获取到的 _sockfd 就可以作为这里的参数值,告知系统要对这个特定的 UDP 套接字进行绑定操作。

2.addr

  • addr 参数是一个指向 struct sockaddr 结构体的指针,该结构体用于存储网络相关的属性信息。

  • 这些属性信息涵盖了协议家族、IP 地址、端口号等关键内容。

  • struct sockaddr 是一个通用的结构体,在实际使用中,我们通常会根据具体的协议家族使用其对应的特定结构体,如对于 IPv4 协议,我们会使用 struct sockaddr_in 结构体,其定义大致如下:

#include <netinet/in.h>
struct sockaddr_in {sa_family_t    sin_family; // 协议家族,对于 IPv4,值为 AF_INETin_port_t      sin_port;   // 端口号,使用网络字节序表示struct in_addr sin_addr;   // IP 地址
};struct in_addr {uint32_t       s_addr;     // 32 位的 IPv4 地址,使用网络字节序
};

        在设置 addr 参数时,我们需要将 struct sockaddr_in 结构体强制转换为 struct sockaddr 类型,因为 bind 函数的参数要求的是 struct sockaddr 指针。例如,如果我们希望服务器绑定到本机的所有 IP 地址(即监听所有网络接口)的 8888 端口,可以按照如下方式设置:

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8888); // 将主机字节序的端口号转换为网络字节序
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY 表示本机所有 IP 地址,同样转换为网络字节序

然后将 &serv_addr 强制转换为 const struct sockaddr* 类型作为 addr 参数的值传递给 bind 函数。

3.addrlen

  • addrlen 参数用于指定传入的 addr 结构体的长度。

  • 对于 struct sockaddr_in 结构体,其长度可以通过 sizeof(struct sockaddr_in) 来获取。

  • 这个参数让系统能够准确知道 addr 结构体所占用的内存空间大小,从而正确读取其中的网络属性信息。

2、返回值说明

    bind 函数的返回值用于指示绑定操作的结果。如果绑定成功,函数将返回 0;如果绑定失败,函数则返回 -1,并且系统会设置相应的错误码,我们可以通过检查错误码(例如使用 perror 函数或者查看 errno 变量)来了解绑定失败的具体原因。常见的绑定失败原因包括:

  • 指定的端口号已经被其他进程占用。

  • 权限不足,例如试图绑定到低于 1024 的特权端口,而当前进程没有足够的权限。

  • 传入的地址结构体格式不正确等。

        通过绑定操作,服务端的套接字与特定的网络地址和端口建立了关联,使得操作系统能够明确知道该套接字用于接收发往指定地址和端口的数据报,从而为后续的网络通信奠定了基础。


二、struct sockaddr_in 结构体的深度解析

        在网络编程中,当服务端进行绑定操作时,需要精确指定网络相关的属性信息,如协议家族、IP 地址和端口号等。这些信息需要填充到一个特定的结构体中,然后作为 bind 函数的第二个参数传入,这个结构体就是 struct sockaddr_in

1、结构体的查找与平台差异

        我们可以通过在 /usr/include 目录下使用 grep 命令来查找 struct sockaddr_in 的定义。例如,在终端中执行 grep -r "struct sockaddr_in {" /usr/include,系统会遍历该目录下的文件,找出定义该结构体的文件。

grep -r "struct sockaddr_in {" /usr/include

        需要特别注意的是,struct sockaddr_in 属于系统级的概念,不同的操作系统平台在接口设计上可能会存在一些细微的差别。这些差异主要体现在结构体成员的排列顺序、一些扩展字段的使用等方面,但核心成员的功能和含义基本保持一致。

2、结构体成员详解

以下是 struct sockaddr_in 结构体的典型定义(以 Linux 系统为例):

#include <netinet/in.h>struct sockaddr_in {__SO_SA_FAMILY_T  sin_family; // 协议家族in_port_t          sin_port;   // 端口号,16 位整数struct in_addr     sin_addr;   // IP 地址// 以下字段通常不直接处理,但可以进行初始化unsigned char      sin_zero[8]; 
};struct in_addr {uint32_t       s_addr;     // 32 位的 IPv4 地址
};

1.sin_family

  • 该成员表示协议家族。在 IPv4 网络编程中,其值通常设置为 AF_INET

  • 协议家族的定义决定了该套接字所使用的网络协议类型,AF_INET 明确指定了使用 IPv4 协议。

  • 例如,当我们创建一个基于 IPv4 的 UDP 服务器时,在设置 struct sockaddr_in 结构体时,就需要将 sin_family 设置为 AF_INET,以告知系统该套接字将在 IPv4 网络环境中进行通信。

2.sin_port

  • sin_port 成员用于表示端口号,它是一个 16 位的整数。

  • 在网络通信中,端口号用于标识主机上的一个特定服务或应用程序。

  • 需要注意的是,在设置端口号时,要使用网络字节序。

  • 因为不同的计算机系统可能采用不同的字节序(大端序或小端序),而网络传输要求使用统一的字节序,即网络字节序(大端序)。

  • 我们可以使用 htons 函数将主机字节序的端口号转换为网络字节序。例如,如果希望服务器监听 8888 端口,可以这样设置:serv_addr.sin_port = htons(8888);

3.sin_addr

  • sin_addr 成员表示 IP 地址,其类型为 struct in_addr

  • struct in_addr 结构体实际上只包含一个成员 s_addr,它是一个 32 位的整数,用于存储 IPv4 地址。

  • 同样,IP 地址也需要使用网络字节序。我们可以使用 htonl 函数将主机字节序的 IP 地址转换为网络字节序。

  • 如果希望服务器绑定到本机的所有 IP 地址(即监听所有网络接口),可以将 s_addr 设置为 INADDR_ANY,并使用 htonl 函数进行转换:serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

4.sin_zero

  • sin_zero 是一个长度为 8 的无符号字符数组,在大多数情况下,我们一般不直接对其进行处理。

  • 不过,为了保证结构体的正确性和兼容性,在初始化 struct sockaddr_in 结构体时,可以将其所有元素设置为 0。

3、结构体的使用示例

以下是一个简单的代码示例,展示如何正确设置 struct sockaddr_in 结构体,并将其用于服务器的绑定操作:

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>class UdpServer
{
public:bool InitServer(){// 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0) { std::cerr << "socket error" << std::endl;return false;}// 设置服务器地址和端口信息struct sockaddr_in serv_addr;// 初始化结构体,将 sin_zero 字段清零memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(8888);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);// 调用 bind 函数进行绑定if (bind(_sockfd, (const struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {std::cerr << "bind error" << std::endl;close(_sockfd);return false;}std::cout << "server bind success, sockfd: " << _sockfd << std::endl;return true;}~UdpServer(){if (_sockfd >= 0) {close(_sockfd);}}private:int _sockfd; // 文件描述符
};int main()
{UdpServer* svr = new UdpServer();svr->InitServer();delete svr;return 0;
}

        在这个示例中,我们首先使用 memset 函数将 struct sockaddr_in 结构体 serv_addr 的所有字节设置为 0,确保 sin_zero 字段被正确初始化。然后,我们分别设置了 sin_familysin_port 和 sin_addr 成员的值,最后将该结构体强制转换为 const struct sockaddr* 类型,作为参数传递给 bind 函数,完成服务器的绑定操作。

        通过正确理解和使用 struct sockaddr_in 结构体,我们能够准确地指定服务器的网络属性信息,使服务器能够正确地监听指定的地址和端口,为后续的网络通信做好准备。


三、两个问题思考

1、为什么将该结构体强制转换为 const struct sockaddr* 类型?

        在将 struct sockaddr_in 结构体作为参数传递给 bind 函数时,需要将其强制转换为 const struct sockaddr* 类型,这主要基于以下几个关键原因:

1. 函数参数类型的统一性

bind 函数的原型定义如下:

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 从函数定义中可以清晰地看到,bind 函数的第二个参数 addr 的类型明确指定为 const struct sockaddr*

  • 这是系统函数定义的一部分,为了保证代码能够正确编译和调用,我们必须遵循这个参数类型的要求。

  • struct sockaddr 是一个通用的套接字地址结构体,而 struct sockaddr_in 是专门用于 IPv4 地址的特定结构体。

  • 通过强制转换,我们将特定类型的结构体指针转换为通用类型的指针,以满足函数参数的类型匹配。

2. 通用性和扩展性

  • struct sockaddr 被设计为一个通用的地址结构体,它可以表示多种不同类型的套接字地址,包括 IPv4、IPv6 等。

  • 不同的网络协议族(如 AF_INET 用于 IPv4,AF_INET6 用于 IPv6)都有自己对应的特定地址结构体(如 struct sockaddr_in 和 struct sockaddr_in6)。

  • 通过使用 struct sockaddr 作为函数参数类型,bind 函数以及其他相关的套接字函数(如 connectsendtorecvfrom 等)可以以一种统一的方式处理各种不同类型的套接字地址。

        例如,当我们编写一个网络程序,希望它能够同时支持 IPv4 和 IPv6 时,使用通用的 struct sockaddr 指针作为参数,可以使代码更加简洁和灵活。我们可以根据实际需求,将不同类型的地址结构体(struct sockaddr_in 或 struct sockaddr_in6)强制转换为 struct sockaddr*,然后传递给相应的函数,而无需为每种地址类型单独编写一套函数。

3. 类型安全与兼容性

  • 虽然强制转换在某种程度上绕过了类型系统的严格检查,但在这种特定的网络编程场景下,它是安全且兼容的。

  • 因为 struct sockaddr_in 结构体的前几个成员与 struct sockaddr 结构体的成员在布局和含义上是兼容的。

  • 具体来说,struct sockaddr_in 的 sin_family 成员与 struct sockaddr 的 sa_family 成员对应,都用于表示协议家族。这种兼容性保证了在强制转换后,函数能够正确地读取和处理地址信息。

2、这样的强制转换会不会越界访问?

在将 struct sockaddr_in 强制转换为 const struct sockaddr* 时,通常不会导致越界访问,这主要基于以下几点原因:

1. 内存布局的兼容性

struct sockaddr 和 struct sockaddr_in 在内存布局上具有前向兼容性。struct sockaddr 的定义通常如下:

struct sockaddr {sa_family_t sa_family;char        sa_data[14];
};

而 struct sockaddr_in 的定义一般为:

struct sockaddr_in {sa_family_t    sin_family;in_port_t      sin_port;struct in_addr sin_addr;// 其他可能的填充或扩展字段
};
  • struct sockaddr_in 的第一个成员 sin_family 与 struct sockaddr 的第一个成员 sa_family 类型相同,都是 sa_family_t,用于表示地址族(例如 AF_INET 表示 IPv4)。

  • 当进行强制转换时,bind 函数首先会读取这个地址族字段,以确定后续如何解析地址信息。由于这两个结构体的起始部分内存布局一致,所以读取 sa_family 是安全的,不会出现越界访问。

2. 函数对地址族的处理

  • 像 bind 这样的套接字函数,在接收到 const struct sockaddr* 指针后,首先会根据地址族字段的值来决定如何进一步处理地址信息。

  • 如果地址族是 AF_INET,函数就知道后续应该按照 struct sockaddr_in 的布局来读取端口号、IP 地址等信息。

  • 函数内部会有相应的逻辑来正确解析不同地址族对应的地址结构,不会盲目地访问超出实际结构体范围的内存。

3. 长度参数的保障

  • bind 函数的第三个参数 addrlen 用于指定传入的地址结构体的长度。当我们传入 struct sockaddr_in 结构体时,会将 addrlen 设置为 sizeof(struct sockaddr_in)

  • 函数在处理地址信息时,会根据这个长度参数来限制访问的内存范围,确保不会越界访问。

  • 例如,在读取完地址族字段后,如果地址族是 AF_INET,函数会按照 struct sockaddr_in 的大小来读取后续的成员,而不会超出 addrlen 所指定的范围。


四、绑定的本质理解

在网络编程的语境下,绑定操作具有关键的意义:

  • 当我们进行绑定时,本质上是在向对应的“网络文件”(即通过套接字创建所代表的网络通信端点)告知特定的 IP 地址和端口号信息。

  • 这一操作会引发系统底层的一系列变化,其中重要的一点是改变了网络文件中文件操作函数的指向。

  • 在未进行绑定之前,这个“网络文件”的操作函数可能处于默认或未明确指定的状态。而一旦完成绑定,系统会将对应的操作函数设置为与网卡相关的操作方法。

  • 这意味着,后续当进行读数据和写数据操作时,操作的对象就不再是普通的磁盘文件,而是网卡。

  • 从这个角度来看,绑定操作的核心作用就是将文件(套接字所代表的网络通信端点)与网络(通过网卡实现的数据传输)紧密关联起来,使得数据能够通过网络进行正确的收发。


五、服务器类中引入 IP 地址和端口号

        由于绑定操作需要明确指定 IP 地址和端口号,因此在服务器类的设计中,我们需要引入这两个关键参数。在创建服务器对象时,将对应的 IP 地址和端口号传入,以便在后续的绑定过程中使用。以下是改进后的 UdpServer 类定义示例:

#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>class UdpServer
{
public:UdpServer(std::string ip, int port): _sockfd(-1), _port(port), _ip(ip){};~UdpServer(){if (_sockfd >= 0) {close(_sockfd);}};private:int _sockfd; // 文件描述符int _port; // 端口号,虽然定义为整型,但实际仅使用低16位std::string _ip; // IP地址
};

        在这个类定义中,我们通过构造函数接收外部传入的 IP 地址和端口号,并将其存储在类的成员变量 _ip 和 _port 中,为后续的绑定操作做好准备。

注意: 虽然这里端口号定义为整型,但由于端口号是16位的,因此我们实际只会用到它的低16位!!!


六、服务端绑定的详细步骤与代码实现

1、套接字创建

        在完成服务器对象的初始化后,首先需要创建套接字。这是通过网络编程中的 socket 函数实现的。以下是在 UdpServer 类中实现套接字创建的代码:

bool InitServer()
{// 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0) { // 创建套接字失败std::cerr << "socket error" << std::endl;return false;}std::cout << "socket create success, sockfd: " << _sockfd << std::endl;// 后续绑定等操作代码...
}

        在上述代码中,socket 函数的第一个参数 AF_INET 表示使用 IPv4 协议族,第二个参数 SOCK_DGRAM 指定创建的是数据报套接字(用于 UDP 通信),第三个参数 0 表示让系统自动选择合适的协议。如果套接字创建成功,函数返回一个文件描述符,我们将其存储在 _sockfd 成员变量中;如果创建失败,返回值为负数,我们输出错误信息并返回 false

2、填充网络属性信息

        在创建套接字之后,需要定义一个 struct sockaddr_in 结构体,并将网络相关的属性信息填充到该结构体中。由于该结构体可能包含一些选填字段,为了确保数据的正确性,我们最好在填充之前对结构体变量进行清空操作。同时,要注意将端口号转换为网络字节序,并将字符串形式的 IP 地址转换为整数形式。以下是具体的代码实现:

bool InitServer()
{//... 套接字创建代码...// 填充网络通信相关信息struct sockaddr_in local;memset(&local, '\0', sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 将端口号转换为网络字节序local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将字符串IP转换为整数IP//... 后续绑定代码...
}

        在上述代码中,memset 函数用于将 local 结构体的内存空间全部清零。htons 函数将主机字节序的端口号转换为网络字节序,inet_addr 函数将点分十进制表示的字符串 IP 地址转换为 32 位的整数形式,以满足网络传输的要求。(后面会详细讲解!!!)

        c_str() 函数的主要功能是将 std::string 对象中的字符序列转换为一个以空字符('\0')结尾的 C 风格字符串(即 const char* 类型),并返回该指针。例如,如果 _ip 存储的字符串是 "127.0.0.1",调用 _ip.c_str() 后会返回一个指向字符数组 {'1', '2', '7', '.', '0', '.', '0', '.', '1', '\0'} 的指针。

3、绑定操作

        填充完网络属性信息后,就可以调用 bind 函数进行绑定了。由于 bind 函数的参数类型为通用指针类型 struct sockaddr*,因此我们需要将 struct sockaddr_in* 类型的指针强制转换为 struct sockaddr* 类型后再传入。以下是绑定操作的代码实现:

bool InitServer()
{//... 前面代码...// 绑定if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) { // 注意这里sizeof的参数应该是local,原代码sizeof(sockaddr)有误std::cerr << "bind error" << std::endl;return false;}std::cout << "bind success" << std::endl;return true;
}

        在上述代码中,bind 函数的第一个参数是之前创建套接字得到的文件描述符 _sockfd,第二个参数是经过强制转换后的网络属性结构体指针,第三个参数是该结构体的大小。如果绑定成功,函数返回 0;如果绑定失败,返回值为负数,我们输出错误信息并返回 false

以下是完整的 UdpServer 类代码示例:

#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>class UdpServer
{
public:UdpServer(std::string ip, int port): _sockfd(-1), _port(port), _ip(ip){};bool InitServer(){// 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0) { std::cerr << "socket error" << std::endl;return false;}std::cout << "socket create success, sockfd: " << _sockfd << std::endl;// 填充网络通信相关信息struct sockaddr_in local;memset(&local, '\0', sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());// 绑定if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) { std::cerr << "bind error" << std::endl;return false;}std::cout << "bind success" << std::endl;return true;}~UdpServer(){if (_sockfd >= 0) {close(_sockfd);}};private:int _sockfd; int _port; std::string _ip; 
};int main()
{UdpServer svr("127.0.0.1", 8888);svr.InitServer();return 0;
}

通过以上详细的步骤和代码实现,我们完成了服务端的绑定操作,使得服务器能够监听指定的 IP 地址和端口,为后续的网络通信做好了准备。


七、IP 地址的两种表现形式

IP 地址作为网络通信中的重要标识,存在两种不同的表现形式,分别为字符串 IP 和整数 IP。

1、字符串 IP

  • 它是以点分十进制字符串的形式呈现,例如我们常见的 192.168.233.123

  • 这种表示方式直观易懂,符合人类阅读和记录的习惯,方便我们在日常的网络配置、日志查看等场景中使用。

2、整数 IP

  • 在网络传输过程中,IP 地址采用的是一种更为紧凑的表示形式,即用一个 32 位的整数来表示。

  • 这种表示方式虽然对于人类来说不够直观,但在网络通信中具有显著的优势。


八、整数 IP 存在的意义

        在网络传输数据时,资源的高效利用至关重要。若直接以点分十进制的字符串形式传送 IP 地址,一个 IP 地址至少需要 15 个字节(15个char类型,例如 192.168.233.123 包含 4 个数字以及 3 个点分隔符这样的char字符)。

        然而,从本质上分析,IP 地址可以划分为四个区域,每个区域的取值范围是 0 到 255,而这个范围内的数字仅需 8 个比特位(1 字节)就能表示。因此,使用 32 位(4 字节)的整数就可以完整地表示一个 IP 地址,其中这个 32 位整数的每一个字节对应 IP 地址中的一个区域。

        由于采用整数 IP 方案表示一个 IP 地址仅需 4 个字节,与字符串 IP 相比,大大减少了数据量,并且在网络通信中能够准确传达相同的含义。所以,在网络通信过程中,为了提高传输效率,减少数据传输量,通常采用整数 IP 而不是字符串 IP。


九、字符串 IP 和整数 IP 相互转换的方式

字符串 IP 和整数 IP 之间的相互转换有多种实现思路,下面介绍一种基于联合体和位段的方法:

        我们可以定义一个位段结构体 A,该结构体包含四个成员,每个成员大小为 8 个比特位,依次表示 IP 地址的四个区域,总共 32 个比特位。然后定义一个联合体 IP,该联合体有两个成员,一个是 32 位的整数,用于表示整数 IP;另一个是 A 类型的成员,用于表示字符串 IP 对应的各个区域。

以下是具体的代码示例:

#include <iostream>// 位段结构体,表示IP地址的四个区域
struct BitFieldIP {unsigned char p1 : 8;unsigned char p2 : 8;unsigned char p3 : 8;unsigned char p4 : 8;
};// 联合体,用于字符串IP和整数IP的转换
union UnionIP {unsigned int integerIP;BitFieldIP bitFieldIP;
};

通过这个联合体,我们可以按照以下方式进行 IP 地址的设置和读取:

  • 设置整数 IP:直接将 32 位整数赋值给联合体的 integerIP 成员。

  • 设置字符串 IP:先将字符串 IP 按照点分十进制的形式分割成四个部分,然后将每部分转换为对应的二进制序列,依次设置到联合体中 bitFieldIP 成员的 p1p2p3 和 p4 中。

  • 获取整数 IP:直接读取联合体的 integerIP 成员。

  • 获取字符串 IP:依次获取联合体中 bitFieldIP 成员的 p1p2p3 和 p4,将每一部分转换成字符串后拼接起来。

        实际上,在操作系统内部,也是采用类似位段和联合体(或者相关的底层机制)来完成字符串 IP 和整数 IP 之间的相互转换,以确保高效地处理网络地址信息。


十、系统提供的转换函数

为了方便开发者进行字符串 IP 和整数 IP 的转换,系统提供了一些现成的函数,我们无需自己编写复杂的转换逻辑。

1、inet_addr 函数

1. 函数原型及头文件

inet_addr 函数的原型如下:

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

        该函数定义在 <arpa/inet.h> 头文件中,在使用该函数时,需要包含此头文件。in_addr_t 通常是一个无符号 32 位整数类型(uint32_t 的别名),用于表示转换后的整数 IP 地址。

2. 函数功能

  • inet_addr 函数的主要功能是将点分十进制表示的字符串 IP 地址(例如 "192.168.1.1")转换为 32 位的无符号整数形式的 IP 地址。

  • 这种转换在网络编程中非常重要,因为网络协议栈在内部处理 IP 地址时通常使用整数形式,以提高处理效率和节省存储空间。

3. 参数说明

  • 函数的唯一参数 cp 是一个指向以空字符('\0')结尾的字符串的指针,该字符串表示点分十进制的 IP 地址。

  • 字符串的格式应符合标准的 IPv4 点分十进制表示法,即由四个用点分隔的十进制数字组成,每个数字的范围是 0 到 255。例如,"10.0.0.1""172.16.254.1" 等都是有效的输入字符串。

4. 返回值说明

  • 成功转换:如果输入的字符串 IP 地址格式正确且能够成功转换为 32 位无符号整数,函数将返回转换后的整数 IP 地址。

  • 转换失败:如果输入的字符串格式不正确,例如包含非数字字符、数字超出 0 到 255 的范围、点的数量不正确等,函数将返回 INADDR_NONE,其值通常为 (in_addr_t) -1

5. 底层实现原理(简化理解)

从底层实现的角度来看,inet_addr 函数会按照以下步骤进行转换:

  1. 字符串分割:函数首先会以点(.)为分隔符,将输入的字符串分割成四个子字符串。

  2. 数字转换:然后,将每个子字符串尝试转换为无符号整数。在转换过程中,会检查每个数字是否在 0 到 255 的范围内。

  3. 整数组合:如果四个子字符串都成功转换为有效的数字,函数将这四个数字分别存储到 32 位整数的相应字节位置。通常,第一个数字存储在最高字节,第四个数字存储在最低字节,从而形成一个 32 位的整数 IP 地址。

6. 代码示例及详细注释

以下是一个使用 inet_addr 函数的简单示例代码,并附有详细注释:

#include <iostream>
#include <arpa/inet.h>int main() {const char* ipStr = "192.168.1.1"; // 定义点分十进制表示的字符串IP地址in_addr_t ipInt = inet_addr(ipStr); // 调用inet_addr函数进行转换if (ipInt == INADDR_NONE) { // 检查转换是否失败std::cerr << "Invalid IP address format: " << ipStr << std::endl;return 1;}std::cout << "The integer representation of IP address " << ipStr << " is: " << ipInt << std::endl;// 为了更直观地查看整数IP的字节分布,可以将其拆分成四个字节并打印unsigned char* bytes = (unsigned char*)&ipInt;std::cout << "Byte representation: ";for (int i = 0; i < 4; ++i) {std::cout << (int)bytes[i];if (i < 3) {std::cout << ".";}}std::cout << std::endl;return 0;
}

在上述代码中:

  • 首先定义了一个点分十进制表示的字符串 IP 地址 ipStr

  • 然后调用 inet_addr 函数将其转换为整数 IP 地址,并将结果存储在 ipInt 变量中。

  • 接着检查转换是否失败,如果失败则输出错误信息并返回。

  • 如果转换成功,打印出转换后的整数 IP 地址。

  • 为了更直观地理解整数 IP 的字节分布,将整数 IP 地址的内存表示拆分成四个字节,并按照点分十进制的格式打印出来。

7. 注意事项

  • 输入验证:在使用 inet_addr 函数之前,最好对输入的字符串进行一定的验证,确保其格式大致符合点分十进制的要求,以避免不必要的转换失败。

  • 错误处理:由于函数在转换失败时返回 INADDR_NONE,在使用返回值之前,一定要进行检查,以确定转换是否成功。

  • 可移植性:虽然 inet_addr 函数在大多数 Unix-like 系统(如 Linux、macOS)和 Windows 系统中都有提供,但在不同的系统上可能存在一些细微的差异。如果需要编写可移植的代码,可以考虑使用更现代和标准化的函数,如 inet_pton

        通过以上对 inet_addr 函数的详细讲解,相信你已经对该函数的功能、使用方法以及注意事项有了全面的了解,能够在实际的网络编程中正确使用它进行字符串 IP 到整数 IP 的转换。

2、inet_ntoa 函数

1. 函数原型及头文件

inet_ntoa 函数的原型如下:

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

该函数定义在 <arpa/inet.h> 头文件中,在使用该函数时,需要包含此头文件。struct in_addr 是一个结构体,通常定义如下:

struct in_addr {in_addr_t s_addr; // 32-bit IPv4 address, in network byte order
};

其中,s_addr 是一个 32 位的无符号整数,表示网络字节序(大端序)的 IPv4 地址。

2. 函数功能

  • inet_ntoa 函数的主要功能是将 32 位的网络字节序的 IPv4 地址(struct in_addr 类型)转换为点分十进制字符串形式(如 "192.168.1.1")。

  • 这种转换在网络编程中非常常见,因为人类更习惯于阅读点分十进制格式的 IP 地址,而网络协议栈内部通常使用整数形式存储和处理 IP 地址。

3. 参数说明

  • 函数的唯一参数 in 是一个 struct in_addr 类型的结构体,其中 s_addr 成员存储了 32 位的网络字节序的 IPv4 地址。

  • 网络字节序是指高位字节存储在低地址,低位字节存储在高地址,与主机字节序(可能为大端序或小端序)不同。

4. 返回值说明

  • 成功转换:如果输入的 struct in_addr 结构体中的 IP 地址有效,函数将返回一个指向静态分配的字符串的指针,该字符串表示点分十进制格式的 IP 地址。

  • 注意事项

    • 返回的字符串指针指向一个静态分配的缓冲区,这意味着后续调用 inet_ntoa 或其他相关函数可能会覆盖该缓冲区的内容。因此,如果需要保存返回的字符串,应该及时复制(如使用 strcpy)到其他存储空间。

    • 该函数不是线程安全的,因为在多线程环境中,多个线程同时调用 inet_ntoa 可能会导致返回的字符串指针指向的内容被其他线程覆盖。

5. 底层实现原理(简化理解)

从底层实现的角度来看,inet_ntoa 函数会按照以下步骤进行转换:

  1. 提取字节:从 struct in_addr 结构体的 s_addr 成员中提取出四个字节(因为 s_addr 是 32 位的,即 4 个字节)。

  2. 字节序转换:如果当前主机的字节序与网络字节序不同(例如,主机是小端序,而网络字节序是大端序),则需要对这四个字节进行字节序转换,以确保正确的字节顺序。

  3. 数字转换:将每个字节转换为对应的十进制数字字符串。

  4. 字符串拼接:将四个十进制数字字符串用点(.)分隔拼接起来,形成点分十进制格式的 IP 地址字符串。

6. 代码示例及详细注释

以下是一个使用 inet_ntoa 函数的简单示例代码,并附有详细注释:

#include <iostream>
#include <arpa/inet.h>
#include <cstring> // 用于strcpyint main() {// 定义一个32位的网络字节序的IPv4地址(这里以192.168.1.1为例)// 注意:inet_addr返回的是网络字节序的整数,可以直接赋值给in_addr.s_addrstruct in_addr addr;addr.s_addr = inet_addr("192.168.1.1");if (addr.s_addr == INADDR_NONE) {std::cerr << "Invalid IP address conversion." << std::endl;return 1;}// 调用inet_ntoa函数将网络字节序的IPv4地址转换为点分十进制字符串char* ipStr = inet_ntoa(addr);// 由于inet_ntoa返回的是静态分配的缓冲区,如果需要保存结果,应该复制到其他存储空间char savedIpStr[16]; // IPv4地址的最大长度为15个字符(xxx.xxx.xxx.xxx),加上'\0'共16个字节strcpy(savedIpStr, ipStr);std::cout << "The dotted-decimal representation of the IP address is: " << savedIpStr << std::endl;// 再次调用inet_ntoa,观察静态缓冲区是否被覆盖addr.s_addr = inet_addr("10.0.0.1");std::cout << "Another IP address: " << inet_ntoa(addr) << std::endl;std::cout << "The previously saved IP address is now: " << savedIpStr << std::endl; // 仍然正确,因为已经复制return 0;
}

在上述代码中:

  • 首先定义了一个 struct in_addr 结构体 addr,并使用 inet_addr 函数将字符串 IP 地址 "192.168.1.1" 转换为网络字节序的整数,存储在 addr.s_addr 中。

  • 然后调用 inet_ntoa 函数将 addr 转换为点分十进制字符串,并将结果复制到 savedIpStr 数组中,以避免后续调用覆盖静态缓冲区的内容。

  • 接着打印转换后的点分十进制字符串。

  • 为了验证静态缓冲区的问题,再次调用 inet_ntoa 函数转换另一个 IP 地址,并观察之前保存的字符串是否被覆盖(由于已经复制,所以不会被覆盖)。

7. 注意事项

  • 静态缓冲区问题:由于 inet_ntoa 返回的字符串指针指向静态分配的缓冲区,因此在多线程环境中或需要多次调用该函数时,应该及时复制返回的字符串,以避免数据被覆盖。

  • 线程安全性inet_ntoa 不是线程安全的函数。在多线程程序中,应该考虑使用线程安全的替代函数,如 inet_ntop

  • 替代函数inet_ntop 是 inet_ntoa 的更现代、更安全的替代函数,它支持 IPv4 和 IPv6,并且允许用户提供缓冲区,从而避免了静态缓冲区的问题。其函数原型如下:

    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    其中,af 是地址族(如 AF_INET 表示 IPv4),src 是指向要转换的地址的指针(对于 IPv4,可以转换为 struct in_addr*),dst 是用户提供的缓冲区,size 是缓冲区的大小。

        通过以上对 inet_ntoa 函数的详细讲解,相信你已经对该函数的功能、使用方法以及注意事项有了全面的了解,能够在实际的网络编程中正确使用它进行整数 IP 到字符串 IP 的转换。

通过这些系统提供的函数,我们可以轻松地在字符串 IP 和整数 IP 之间进行转换,满足不同场景下对 IP 地址表示和处理的需求。


十一、struct sockaddr_in 详解

1、结构体定义及作用

    struct sockaddr_in 是网络编程中用于存储 IPv4 地址和端口号等信息的关键结构体。在网络通信里,为了统一不同类型地址结构的处理,它遵循一定的规范和填充规则。其典型定义如下:

#include <netinet/in.h>
#include <sys/socket.h>// 通用套接字地址宏定义,用于包含地址族等信息
#define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##familystruct sockaddr_in
{__SOCKADDR_COMMON(sin_);  // 通用部分,存储地址族in_port_t sin_port;       // 16 位端口号struct in_addr sin_addr;  // 存储 IPv4 地址的结构体// 填充字段,确保 struct sockaddr_in 大小与 struct sockaddr 一致unsigned char sin_zero[sizeof(struct sockaddr) -__SOCKADDR_COMMON_SIZE -sizeof(in_port_t) -sizeof(struct in_addr)];
};

2、成员详解

  • 地址族相关(__SOCKADDR_COMMON(sin_)

    • __SOCKADDR_COMMON 是一个宏,它通过特殊的符号拼接操作(##),将传入的参数与 family 拼接成一个新的符号。例如,__SOCKADDR_COMMON(sin_) 展开后为 sa_family_t sin_family

    • sa_family_t 是地址族类型,通常定义为 typedef unsigned short int sa_family_t。在 IPv4 网络编程中,sin_family 的值一般设置为 AF_INET,用于标识该地址结构为 IPv4 地址。

  • 端口号(in_port_t sin_portin_port_t 用于表示端口号,定义为 typedef uint16_t in_port_t,即 16 位无符号整数。端口号在网络通信中用于标识主机上的特定服务或应用程序。由于网络字节序和主机字节序可能不同,在设置端口号时,通常需要使用 htons 函数将主机字节序的端口号转换为网络字节序。

  • IPv4 地址(struct in_addr sin_addrstruct in_addr 是专门用于存储 IPv4 地址的结构体,定义如下:

typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};

    in_addr_t 是 32 位无符号整数类型。IPv4 地址通常以点分十进制字符串形式(如 `"192.168.0.123"`)表示,但在网络通信中,需要将其转换为 32 位整数形式存储在 `s_addr` 中。可以使用 `inet_addr` 函数将点分十进制字符串转换为 32 位整数,例如 `in_addr_t ip_int = inet_addr("192.168.0.123");`。

  • 填充字段(sin_zerosin_zero 是一个无符号字符数组,其作用是填充 struct sockaddr_in 结构体,使其大小与通用的 struct sockaddr 结构体一致。这样可以保证在函数调用等操作中,不同类型的地址结构能够以统一的方式进行传递和处理。在初始化 struct sockaddr_in 结构体时,通常需要将 sin_zero 字段全部设置为 0。


十二、IP 地址的转换

1、点分十进制字符串转 32 位整数

如前面所述,使用 inet_addr 函数可以将点分十进制表示的 IPv4 地址字符串转换为 32 位无符号整数。例如:

#include <stdio.h>
#include <arpa/inet.h>int main() {const char *ip_str = "192.168.1.1";in_addr_t ip_int = inet_addr(ip_str);if (ip_int == INADDR_NONE) {printf("Invalid IP address format.\n");return 1;}printf("The 32-bit integer representation of %s is: %u\n", ip_str, (unsigned int)ip_int);return 0;
}

2、32 位整数转点分十进制字符串

可以通过自定义方法或使用标准库函数将 32 位整数转换回点分十进制字符串。以下是自定义方法的示例:

#include <stdio.h>
#include <stdint.h>
#include <string.h>// 自定义结构体用于演示 IP 地址各部分的存储
struct ip {unsigned char part1;unsigned char part2;unsigned char part3;unsigned char part4;
};int main() {uint32_t ip_int = 0xC0A80101; // 192.168.1.1 的 32 位整数表示struct ip *p = (struct ip *)&ip_int;// 由于字节序问题,直接按此方式解析可能在不同平台有差异,这里仅作演示// 正确做法应考虑字节序转换,或者使用标准库函数printf("The dotted - decimal representation is: %d.%d.%d.%d\n",p->part1, p->part2, p->part3, p->part4);// 实际开发中,可以使用 inet_ntoa 函数// 示例代码(需包含 <arpa/inet.h> 和 <netinet/in.h>)/*struct in_addr addr;addr.s_addr = ip_int;char *ip_str = inet_ntoa(addr);printf("Using inet_ntoa: %s\n", ip_str);*/return 0;
}

        在实际开发中,更推荐使用标准库函数 inet_ntoa 或更通用的 inet_ntop 来进行 32 位整数到点分十进制字符串的转换,以确保代码的可移植性和正确性。inet_ntoa 函数的使用示例如下:

#include <stdio.h>
#include <arpa/inet.h>
#include <netinet/in.h>int main() {struct in_addr addr;addr.s_addr = inet_addr("192.168.1.1");char *ip_str = inet_ntoa(addr);printf("The IP address is: %s\n", ip_str);return 0;
}

        综上所述,struct sockaddr_in 结构体在网络编程中起着关键作用,而 IP 地址在不同形式之间的转换也是网络通信中常见的操作,合理使用相关结构体和转换函数能够确保网络编程的正确性和高效性。

http://www.dtcms.com/a/597719.html

相关文章:

  • 百度建站东莞著名网站建设
  • 如何选择邯郸网站制作做外贸网站维护费是多少
  • 【SCI复现】高比例可再生能源并网如何平衡灵活性与储能成本?虚拟电厂多时间尺度调度及衰减建模
  • CodeBuddy AI IDE:全栈AI开发平台实战
  • 购物网站开发教程 视频大流量网站 文章点击
  • 研究人员诱导ChatGPT对自身实施提示注入攻击
  • 数据结构与算法实验(黑龙江大学)
  • 孤客截图工具 Pro - 从开发到打包的完整指南
  • 山东德州最大的网站建设教学学校网站php源码|班级主页教师博客学生博客|学校网站织梦仿
  • 基于librespot的定制化Spotify客户端开发:开源替代方案的技术实践与优化
  • 主从同步配置的步骤
  • 个人使用网站wordpress用户设置
  • vps网站目录是灰色的生活中实用的产品设计
  • mysql主备配置(对比postgresql)
  • mysql tidb like查询有换行符内容问题解决
  • 【工具变量】上市公司是否获得ZF采购DID(2000-2025年)
  • 【AI学习-comfyUI学习-LCM lora八步生成 工作流-各个部分学习-第八节】
  • 转轮机加密(攻防世界)
  • 微信小程序实现长按复制选中文字的效果
  • SQL Server 驱动 和 TLS 版本不兼容 的问题
  • 【低空安全】低空无人机集群侦测与反制概述
  • 制作网站的原因是计算机网页制作工具
  • 机器学习聚类k均值簇数的调优方法
  • 批量格式化XML与JSON文件小工具
  • TensorFlow深度学习实战(41)——TensorFlow生态系统
  • 网站空格 教程宁波龙山建设有限公司网站
  • 4-ARM-PEG-COOH(2),多功能羧基PEG的结构特性与反应特点
  • 东昌府区网站建设公司铜川网站建设公司电话
  • 大模型如何处理不同格式的文档?
  • GCPC总决赛(牛客)