【Linux】网络编程套接字Scoket:UDP网络编程

目录

一、了解UDP协议

二、了解端口和IP地址 

三、套接字概述与Socket的概念

四、Socket的类型

五、 Socket的信息数据结构

 六、网络字节序与主机字节序的互相转换

七、地址转换函数 

八、UDP网络编程流程及相关函数

socket函数

bind函数

recvfrom函数 

sendto函数 

九、创建服务端与客户端之间的网络通信 


注意:本文的重点为网络编程,并不会对TCP、UDP、IP等协议做详细介绍,仅做简要说明。

一、了解UDP协议

UDP(User Datagram Protocol,用户数据报协议)是一个简单的传输层协议,用于在网络中进行快速且无连接的数据传输,无需像TCP那样通过三次握手来建立一个连接。同时,一个UDP应用可同时作为应用的客户或服务器方。

由于UDP不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多,因此我们先以UDP网络编程来导入对网络编程的学习。

下面是UDP协议的是主要特点:

主要特点

  1. 无连接

    • UDP 是无连接的协议,这意味着在数据传输之前,不需要建立和维护连接。每个数据包(称为数据报)是独立的,发送和接收之间不需要进行握手或维护连接状态。
  2. 不可靠传输

    • UDP 不提供可靠的数据传输保证。数据包可能丢失、重复或乱序到达。它不会对数据包的丢失进行重传,也不会进行数据的错误校验和修正。应用层需要自行处理这些问题(如果需要的话)。
  3. 面向数据报

    • UDP 以数据报的形式传输信息,每个数据报包含了目标地址和端口号。数据报大小受到限制,通常最大为 65,535 字节(包括头部和数据部分)。
  4. 低开销

    • UDP 的头部开销较小(仅 8 字节),因为它省略了许多 TCP 中的控制信息(如序列号、确认号、流量控制、重传等)。这使得 UDP 在网络中具有较低的延迟和开销,适用于需要高速传输的应用。
  5. 适用于实时应用

    • 由于其低延迟和简单的机制,UDP 常用于实时应用,如语音通话、视频会议、流媒体等。即使丢失了一些数据包,这些应用也可以继续运行,而不会对整体体验造成显著影响。

二、了解端口和IP地址 

IP 地址和端口号在网络通信中扮演着重要角色,它们各自标识了网络中的不同层次。

  1. IP 地址:这是一个唯一的标识符,用于确定网络中的主机或设备。每台联网的设备都有一个唯一的 IP 地址,这样它们才能在网络中相互识别和通信。IP 地址能够标识网络上的唯一设备,但并不能唯一标识设备上的具体应用或服务。

  2. 端口号:这是一个用于标识设备上特定应用或服务的数字。每个设备可以运行多个应用或服务,每个服务都通过不同的端口号来区分。端口号的范围为0~65 535,一类是由互联网指派名字和号码公司ICANN 负责分配给一些常用的应用程序固定使用的“周知的端口”,其值一般为0~1023,例如,http的端口号是80,fp为21,ssh为 22,telnet为23等;还有一类是用户自己定义的,通常是大于1024的整型值。端口号与 IP 地址一起,能够唯一标识网络上的具体服务或应用。

因此,IP 地址和端口号的组合可以唯一标识网络上的具体服务或应用(进程)。例如,一个 IP 地址为 192.168.1.1 的设备上的 HTTP 服务可以通过 192.168.1.1:80 来访问。

三、套接字概述与Socket的概念

        套接字是操作系统内核中的一个数据结构,它是网络中的节点进行相互通信的门户,是网络进程的ID。网络通信归根到底还是进程间的通信(不同计算机上的进程间通信)。在网络中,每一个节点(计算机或路由)都有一个网络地址,也就是IP地址。在两个进程进行通信时,首先要确定各自所在的网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上很可能同时运行着多个进程,所以仅凭网络地址还不能确定到底要和网络中的哪一个进程进行通信,因此套接字中还需要包括其他的信息,也就是端口号(PORT)。在一台计算机中,一个端口号一次只能分配给一个进程。也就是说,在一台计算机中,端口号和进程之间是一一对应的关系,所以,使用端口号和网络地址的组合可以唯一地确定整个网络中的一个网络进程。
例如,假设网络中某一台计算机的IP地址为10.92.20.160,操作系统分配给计算机中某应用程序进程的端口号为1500,则此时10.92.20.160,1500就构成了一个套接字。

Linux中的网络编程是通过Socket来进行的。Socket 是一种特殊的IO接口,也是一种文件描述符。它是一种常用的进程之间的通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。
每一个Socket都用一个半相关描述“{协议、本地地址、本地端口}”来表示:一个完整的套接字则用一个相关描述“(协议、本地地址、本地端口、远程地址、远程端口)”来表示。Socket也有一个类似于打开文件的函数调用,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过Socket来实现的。套接字(socket)是计算机网络中用于进行通信的一个端点。

四、Socket的类型

流式Socket(SOCK STREAM)用于TCP通信。流式套接字提供可靠的、面向连接的通信流;它使用传输控制协议TCP,从而保证数据传输的正确性和顺序性。

数据报Socket(SOCK DGRAM)用于UDP通信。数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的,它使用数据报协议UDP。

原始Socket(SOCK RAW)用于新的网络协议实现的测试等。原始套接字允许对底层协数据报协议 UDP。议如PP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。

五、 Socket的信息数据结构

socket 常见 API(应用程序编程接口)
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr* address, socklen_t address_len);// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,UNIX Domain Socket。 然而,,各种网络协议的地址格式并不相同,这些地址格式需要通过 Socket API 进行适配。
•IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位地址类型,16 位端口号和 32 位 IP 地址。

• IPv4、 IPv6 地址类型分别定义为常数 AF_INET、 AF_INET6。这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容。

• socket API 可以都用 struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性,可以接收 IPv4, IPv6, 以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数。

1. 地址类型

在 netinet/in.h 头文件中,IPv4 和 IPv6 的地址格式分别用 struct sockaddr_in 和 struct sockaddr_in6 结构体表示。sockaddr 结构体是一个通用的地址结构体,用于在编程中处理不同的地址类型:

  • IPv4 地址 (struct sockaddr_in)

    struct sockaddr_in {short int          sin_family;  // 地址族,通常为 AF_INETunsigned short int sin_port;    // 端口号struct in_addr     sin_addr;    // IP 地址char               sin_zero[8]; // 填充,以保证结构体大小为 16 字节
    };struct in_addr {in_addr_t s_addr;  // 32位 IPv4 地址,网络字节序,in_addr_t (unsigned long int )
    };
    
  • IPv6 地址 (struct sockaddr_in6)

    struct sockaddr_in6 {u_int16_t          sin6_family;   // 地址族,通常为 AF_INET6u_int16_t          sin6_port;     // 端口号u_int32_t          sin6_flowinfo; // 流量信息struct in6_addr    sin6_addr;     // IPv6 地址u_int32_t          sin6_scope_id; // 作用域 ID
    };struct in6_addr {unsigned char s6_addr[16];  // IPv6 地址的 16 字节表示,网络字节序
    };
    

2. 地址族常量

地址族常量定义了不同的协议族:

  • AF_INET:表示 IPv4 地址族。
  • AF_INET6:表示 IPv6 地址族。
  • AF_UNIX:表示 UNIX Domain Socket 地址族。

这些常量用于指示 sockaddr 结构体中存储的地址类型,使得程序可以在运行时根据地址族来选择和处理合适的结构体。

3. 使用 struct sockaddr

struct sockaddr 是一个通用的结构体,用于存储所有类型的地址。实际使用时,程序可以通过强制类型转换将 struct sockaddr 转换为具体的地址结构体(如 struct sockaddr_in 或 struct sockaddr_in6),以访问特定的字段

// 创建一个 IPv4 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);// 定义 IPv4 地址
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(12345); // 设置端口号
inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr); // 设置 IP 地址// 使用 struct sockaddr * 作为参数
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));// 定义 IPv6 地址
struct sockaddr_in6 addr6;
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(12345); // 设置端口号
inet_pton(AF_INET6, "2001:0db8:85a3:0000:0000:8a2e:0370:7334", &addr6.sin6_addr); // 设置 IPv6 地址// 使用 struct sockaddr * 作为参数
bind(sockfd, (struct sockaddr *)&addr6, sizeof(addr6));

 六、网络字节序与主机字节序的互相转换

网络序列和主机序列的转化涉及到数据在网络上传输时的字节顺序问题。

字节序(Endianess):

  1. 大端序(Big-Endian):数据的高位字节存在低地址处,低位字节在高地址处。
  2. 小端序(Little-Endian):数据的低位字节存在低地址处,高位字节在高地址处。

不同的计算机系统可能使用不同的字节序来存储数据,这会导致同一数据在不同系统之间的表示方式不同。例如,一台使用大端序的机器和一台使用小端序的机器在数据传输时会有不同的解释。

网络序列和主机序列:

  1. 网络序列:网络协议通常采用大端序来表示数据,这种顺序被称为网络字节序。网络字节序是为了保证不同平台之间的数据能够一致地解释。

  2. 主机序列:是指计算机本身使用的字节序(可能是大端序也可能是小端序)。

转化的必要性:

在网络通信中,数据从一个主机发送到另一个主机时,数据的字节顺序必须统一,以确保接收方能够正确解析数据。由于不同主机可能使用不同的字节序,数据在网络上传输时需要进行转化:

  • 从主机序列到网络序列:在发送数据之前,发送方需要将数据从主机字节序转化为网络字节序。这通常使用系统提供的函数来实现,如 htonl()(将主机字节序的长整型转化为网络字节序)、htons()(将主机字节序的短整型转化为网络字节序)。

  • 从网络序列到主机序列:在接收数据时,接收方需要将数据从网络字节序转化为主机字节序。这通常使用函数如 ntohl()(将网络字节序的长整型转化为主机字节序)、ntohs()(将网络字节序的短整型转化为主机字节序)。

示例:

假设一个大端序主机发送一个整数 0x12345678(十进制的305419896),在网络中,数据以大端序 0x12 0x34 0x56 0x78 传输。如果接收方是小端序主机,它接收到的字节顺序是 0x78 0x56 0x34 0x12,需要进行转化以得到正确的整数值 0x12345678

字节序转换相关函数:

这些函数在 <arpa/inet.h> 头文件中声明:

以下函数中,h代表host(主机),n代表network(网络),s代表short(16位字节序),l代表long(32位字节序)

1. htons

  • 函数原型
    uint16_t htons(uint16_t hostshort);
    
  • 功能:将主机字节序的 uint16_t 类型数值转换为网络字节序。

2. ntohs

  • 函数原型
    uint16_t ntohs(uint16_t netshort);
    
  • 功能:将网络字节序的 uint16_t 类型数值转换为主机字节序。

3. htonl

  • 函数原型
    uint32_t htonl(uint32_t hostlong);
    
  • 功能:将主机字节序的 uint32_t 类型数值转换为网络字节序。

4. ntohl

  • 函数原型
    uint32_t ntohl(uint32_t netlong);
    
  • 功能:将网络字节序的 uint32_t 类型数值转换为主机字节序。

以下是一个示例程序,演示了如何使用以上函数进行字节序转换:

#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>int main() {uint16_t port = 8080;uint32_t ip = 3232235776; // 192.168.1.0// 主机到网络字节序转换uint16_t net_port = htons(port);uint32_t net_ip = htonl(ip);// 网络字节序到主机字节序转换uint16_t host_port = ntohs(net_port);uint32_t host_ip = ntohl(net_ip);printf("Original port: %u\n", port);printf("Network port: %u\n", net_port);printf("Host port: %u\n", host_port);printf("Original IP: %u\n", ip);printf("Network IP: %u\n", net_ip);printf("Host IP: %u\n", host_ip);return 0;
}

在上述示例中:

  • htons 将主机字节序的端口号 port 转换为网络字节序。
  • ntohs 将网络字节序的端口号 net_port 转换回主机字节序。
  • htonl 将主机字节序的 IP 地址 ip 转换为网络字节序。
  • ntohl 将网络字节序的 IP 地址 net_ip 转换回主机字节序。

七、地址转换函数 

地址转换函数用于在不同格式之间转换 IP 地址。它们在网络编程中非常重要,特别是在处理 IP 地址和域名时。以下是常见的地址转换函数的详细介绍:

1. inet_addr

  • 函数原型

    in_addr_t inet_addr(const char *cp);
    
  • 功能:将点分十进制的 IP 地址(如 “192.168.1.1”)转换为网络字节序的 in_addr_t 类型(通常为 uint32_t)的二进制格式。

  • 返回值:成功时返回 IP 地址的网络字节序格式;失败时返回 INADDR_NONE(通常为 0xFFFFFFFF)。

  • 示例

    #include <stdio.h>
    #include <arpa/inet.h>int main() {const char *ip_str = "192.168.1.1";in_addr_t ip = inet_addr(ip_str);if (ip == INADDR_NONE) {printf("Invalid IP address\n");} else {printf("IP address in network byte order: %u\n", ip);}return 0;
    }
    

2. inet_ntoa

  • 函数原型

    char *inet_ntoa(struct in_addr in);
    
  • 功能:将网络字节序的 struct in_addr 结构体转换为点分十进制的 IP 地址字符串(如 “192.168.1.1”)。

  • 返回值:返回指向字符串的指针。如果失败,返回 NULL。需要注意,返回的字符串是静态分配的,因此不适合多线程环境或并发使用。

  • 示例

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

3. inet_pton

  • 函数原型

    int inet_pton(int af, const char *src, void *dst);
    
  • 功能:将 IP 地址的文本表示形式(如 “192.168.1.1” 或 “2001:db8::1”)转换为网络字节序的二进制格式。

  • 参数

    • af:地址族,通常为 AF_INET(IPv4)或 AF_INET6(IPv6)。
    • src:指向包含 IP 地址的字符串的指针。
    • dst:指向存储转换结果的内存地址。
  • 返回值:成功时返回 1,无效地址时返回 0,出错时返回 -1

  • 示例

    #include <stdio.h>
    #include <arpa/inet.h>int main() {struct in_addr addr;if (inet_pton(AF_INET, "192.168.1.1", &addr) == 1) {printf("Address converted successfully\n");} else {printf("Invalid address\n");}return 0;
    }
    

4. inet_ntop

  • 函数原型

    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    
  • 功能:将网络字节序的二进制格式(如 struct in_addr 或 struct in6_addr)转换为 IP 地址的文本表示形式。

  • 参数

    • af:地址族,通常为 AF_INET(IPv4)或 AF_INET6(IPv6)。
    • src:指向包含网络字节序 IP 地址的内存地址。
    • dst:指向存储转换结果的缓冲区。
    • size:缓冲区大小。
  • 返回值:成功时返回指向结果字符串的指针;失败时返回 NULL

  • 示例

    #include <stdio.h>
    #include <arpa/inet.h>int main() {struct in_addr addr;addr.s_addr = htonl(0xC0A80101); // 192.168.1.1char ip_str[INET_ADDRSTRLEN];if (inet_ntop(AF_INET, &addr, ip_str, sizeof(ip_str))) {printf("IP address: %s\n", ip_str);} else {printf("Conversion failed\n");}return 0;
    }
    

总结

这些地址转换函数使得在处理 IP 地址时,可以在字符串表示和网络字节序之间进行转换。可以简化对 IP 地址的操作,并保证在网络通信中使用的地址格式正确。 

八、UDP网络编程流程及相关函数

socket函数

函数功能:用于创建一个新的套接字,套接字(socket)是计算机网络中用于进行通信的一个端点。

1. 函数原型

在不同的编程语言和平台中,socket 函数的原型可能有所不同,但在 C 语言和 POSIX 标准下,它的原型通常是:

int socket(int domain, int type, int protocol);

2. 参数说明

  • domain:指定套接字的域或协议族,常见的有:

    • AF_INET:IPv4 网络协议。
    • AF_INET6:IPv6 网络协议。
    • AF_UNIX:用于本地进程间通信的 Unix 域套接字。
  • type:指定套接字的类型,常见的有:

    • SOCK_STREAM:面向连接的流式套接字,通常用于 TCP 协议。
    • SOCK_DGRAM:数据报套接字,通常用于 UDP 协议。
    • SOCK_RAW:原始套接字,通常用于访问底层网络协议。
  • protocol:指定协议类型。一般情况下,你可以设置为 0,这样系统会根据 domain 和 type 自动选择合适的协议。例如,对于 SOCK_STREAM 类型的套接字,系统会默认使用 TCP 协议;对于 SOCK_DGRAM 类型的套接字,系统会默认使用 UDP 协议。

3. 返回值

  • 成功时,socket 函数返回一个非负整数,这个整数是套接字的描述符。
  • 失败时,返回 -1,并设置 errno 以指示错误类型。

4. 错误处理

当 socket 函数失败时,你可以通过 errno 获取错误代码。常见的错误代码有:

  • EAFNOSUPPORT:不支持指定的地址族。
  • EINVAL:提供了无效的参数。
  • PROTONOSUPPORT:不支持指定的协议。

5. 示例代码

以下是一个简单的 C 语言示例,演示如何创建一个 IPv4 和 UDP 的套接字:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>int main() {int sockfd;// 创建套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket");exit(EXIT_FAILURE);}printf("Socket created successfully.\n");// 关闭套接字close(sockfd);return 0;
}

bind函数

函数功能:bind 函数在网络编程中用于将一个套接字(socket)与一个本地地址(IP 地址和端口)绑定起来。

1. 函数原型

在 C 语言和 POSIX 标准下,bind 函数的原型如下:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2. 参数说明

  • sockfd:套接字描述符,这是由 socket 函数创建并返回的套接字。

  • addr:指向 sockaddr 结构体的指针,用于指定要绑定的本地地址。通常会使用具体的结构体如 sockaddr_in(用于 IPv4 地址)或 sockaddr_in6(用于 IPv6 地址)。

  • addrlenaddr 指向的地址结构体的长度。对于 sockaddr_in,通常是 sizeof(struct sockaddr_in)

3. sockaddr 结构体

  • sockaddr_in(用于 IPv4):

    struct sockaddr_in {sa_family_t    sin_family; // 地址族,通常为 AF_INETuint16_t       sin_port;   // 端口号(网络字节顺序)struct in_addr sin_addr;   // IP 地址
    };struct in_addr {uint32_t s_addr; // IP 地址(网络字节顺序)
    };
    
  • sockaddr_in6(用于 IPv6):

    struct sockaddr_in6 {sa_family_t     sin6_family;   // 地址族,通常为 AF_INET6uint16_t        sin6_port;     // 端口号(网络字节顺序)uint32_t        sin6_flowinfo; // 流量信息struct in6_addr sin6_addr;     // IPv6 地址uint32_t        sin6_scope_id; // 范围 ID
    };struct in6_addr {unsigned char s6_addr[16]; // IPv6 地址
    };
    

4. 返回值

  • 成功时,返回 0
  • 失败时,返回 -1,并设置 errno 以指示错误类型。

5. 错误处理

常见的错误代码包括:

  • EADDRINUSE:地址已经在使用中,通常是端口被占用。
  • EADDRNOTAVAIL:提供的地址在本地不可用。
  • EINVAL:提供了无效的参数。
  • ENOTSOCK:描述符不是一个套接字。

6. 示例代码

以下是一个简单的 C 语言示例,演示如何将一个套接字绑定到一个特定的本地地址和端口:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>int main() {int sockfd;struct sockaddr_in server_addr;// 创建 UDP 套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket");exit(EXIT_FAILURE);}// 配置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用接口server_addr.sin_port = htons(12345);       // 设置端口号(转换为网络字节顺序)// 绑定套接字到本地地址if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("bind");close(sockfd);exit(EXIT_FAILURE);}printf("UDP socket successfully bound to port 12345.\n");// 关闭套接字close(sockfd);return 0;
}

7. 使用场景

bind 函数通常在服务器端使用,绑定套接字到特定的 IP 地址和端口,以便监听来自客户端的连接请求。在客户端,通常不需要显式地调用 bind,除非你需要绑定到特定的本地地址和端口。

recvfrom函数 

函数功能:recvfrom函数是用于接收数据的系统调用,适用于面向无连接的协议,如UDP(用户数据报协议)。

1、函数原型

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

2、参数解释

  1. sockfd:已经创建并绑定的套接字的文件描述符。
  2. buf:指向缓冲区的指针,用于存储接收到的数据。
  3. len:缓冲区buf的大小,以字节为单位。
  4. flags:标志位,用于修改recvfrom的行为,通常情况下设置为0,默认为阻塞读取。其他参数设置例如,MSG_DONTWAIT表示非阻塞模式;MSG_WAITALL表示等待直到接收到指定长度的数据。
  5. src_addr:指向sockaddr结构体的指针,用于存储发送方的地址信息。如果不需要获取发送方的地址,可以设置为NULL
  6. addrlen:指向整型的指针,用于指定src_addr结构体的大小。在调用recvfrom之前,应将其设置为src_addr结构体的大小;调用后,它将被设置为新接收到的地址的实际大小。

3、返回值

  • 成功时,recvfrom返回接收到的字节数。
  • 如果连接被对方优雅地关闭,则返回0。
  • 如果发生错误,返回-1,并设置全局变量errno来指示错误的原因。

4、注意事项

  • 使用recvfrom时,需要处理可能出现的各种错误情况。例如,如果套接字处于非阻塞模式并且没有数据可读,recvfrom可能会返回EAGAINEWOULDBLOCK错误。
  • recvfrom函数可以同时接收数据并获取数据发送方的地址信息,这是它与recv函数的一个主要区别。
  • 对于UDP协议的套接字,由于UDP数据包可能会被分片,因此需要多次读取才能将一个完整的数据包接收完毕。

示例代码

以下是一个简单的 UDP 服务器示例,展示如何使用 recvfrom 函数接收数据并获取发送方的地址信息:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>#define PORT 12345
#define BUF_SIZE 1024int main() {int sockfd;struct sockaddr_in server_addr, client_addr;socklen_t client_len;char buf[BUF_SIZE];ssize_t recv_len;// 创建 UDP 套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket");exit(EXIT_FAILURE);}// 配置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用接口server_addr.sin_port = htons(PORT);        // 设置端口号// 绑定套接字到本地地址if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("bind");close(sockfd);exit(EXIT_FAILURE);}printf("UDP server listening on port %d\n", PORT);// 接收数据client_len = sizeof(client_addr);recv_len = recvfrom(sockfd, buf, BUF_SIZE - 1, 0, (struct sockaddr *)&client_addr, &client_len);if (recv_len < 0) {perror("recvfrom");close(sockfd);exit(EXIT_FAILURE);}// Null-terminate and print received databuf[recv_len] = '\0';printf("Received message: %s\n", buf);printf("From address: %s\n", inet_ntoa(client_addr.sin_addr));printf("From port: %d\n", ntohs(client_addr.sin_port));// 关闭套接字close(sockfd);return 0;
}

代码说明

  1. 创建套接字:使用 socket 函数创建一个 UDP 套接字。
  2. 配置服务器地址:设置服务器地址信息,包括协议族(AF_INET)、地址(INADDR_ANY)、端口号(使用 htons 转换为网络字节顺序)。
  3. 绑定套接字:将套接字绑定到指定的本地地址和端口。
  4. 接收数据
    • 使用 recvfrom 函数接收数据。
    • recvfrom 填充 buf 缓冲区并将客户端的地址信息填充到 client_addr 结构体中。
    • recv_len 保存接收到的字节数。
  5. 处理数据
    • 将接收到的数据缓冲区 buf 以 null 字符终止,并打印出来。
    • 使用 inet_ntoa 函数将客户端的 IP 地址转换为字符串格式。
    • 使用 ntohs 函数将客户端的端口号转换为主机字节顺序。
  6. 关闭套接字:结束时关闭套接字以释放资源。

sendto函数 

函数功能:sendto 函数是网络编程中用于发送数据的一个函数,通常用于 UDP 协议。它的主要功能是将数据包发送到指定的目标地址。sendto 函数常用于 Socket 编程,特别是在处理无连接的 UDP 套接字时。

函数原型

在不同的编程语言和平台中,sendto 的函数原型可能有所不同,但在 C 语言的 POSIX 标准中,sendto 的函数原型如下:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明

  1. sockfd: 套接字描述符,是通过 socket 函数创建的套接字的文件描述符。该套接字应当是一个有效的、已创建的 UDP 套接字。

  2. buf: 指向要发送数据的缓冲区的指针。这个缓冲区中的数据会被发送到目标地址。

  3. len: 要发送的数据的字节数。它指定了 buf 中的有效数据长度。

  4. flags: 发送数据的标志。通常情况下,这个参数设置为 0,但也可以使用不同的标志来控制发送行为(如 MSG_CONFIRMMSG_DONTROUTE 等)。

  5. dest_addr: 指向 sockaddr 结构体的指针,这个结构体包含了目标地址的信息。对于 UDP 套接字,这通常是一个 sockaddr_in 结构体,用于指定目标主机的 IP 地址和端口号。

  6. addrlendest_addr 指向的地址结构体的长度。对于 sockaddr_in 结构体,通常是 sizeof(struct sockaddr_in)

返回值

  • 成功时,sendto 返回发送的字节数(即 len),如果数据包部分发送成功,那么返回值可能小于 len
  • 失败时,返回 -1,并设置 errno 以指示错误原因。

示例代码

以下是一个使用 sendto 函数的简单示例,演示了如何通过 UDP 发送数据:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>int main() {int sockfd;struct sockaddr_in servaddr;char *message = "Hello, UDP server!";// 创建 UDP 套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 配置目标地址memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(12345); // 目标端口servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 目标 IP 地址// 发送数据ssize_t n = sendto(sockfd, message, strlen(message), 0,(struct sockaddr *)&servaddr, sizeof(servaddr));if (n < 0) {perror("sendto failed");close(sockfd);exit(EXIT_FAILURE);}printf("Sent %zd bytes to server\n", n);// 关闭套接字close(sockfd);return 0;
}

上述代码创建了一个 UDP 套接字,配置了目标地址,并通过 sendto 函数将一条消息发送到指定的 IP 和端口。

九、创建服务端与客户端之间的网络通信 

为什么客户端不需要bind? 

  1. 临时性端口:客户端通常由操作系统自动为其分配一个临时的本地端口。客户端只需要指定目标服务器的 IP 地址和端口号,操作系统会为客户端选择一个合适的本地IP地址及本地端口来完成通信。

  2. 简单的连接模型:客户端主要是发起连接请求并接收响应。由于客户端是发起方,网络栈会自动处理本地端口的分配和管理。

基于对上述知识的了解以及UDP编程相关函数的认识,接下来我们依照上述UDP编程流程图来创建一个简易的服务端与客户端之间的网络通信。

首先,我们需要封装一个服务端类,该类的功能包括:初始化服务端、启动服务端、关闭服务端;而服务端通常是不允许被拷贝的,所以我们需要禁止编译器默认生成拷贝构造和赋值运算符重载。

服务端方法设计接口如下: 

#define BUFFER_SIZE 1024const int temp_socketfd = -1;
const std::string temp_IP = "127.0.0.1"; //回环地址,表示计算机自身的网络接口,作为测试使用
const uint16_t temp_port = 8888;    //自行设置的端口号,大小必须在1024~65535之间enum {SOCKET_ERROR = 1,BIND_ERROR,RECVFROM_ERROR,SENDTO_ERROR
};class Socket_Server
{
private:int _socketfd;  //套接字描述符bool _isrunning;//服务器运行标志uint16_t _port; //16位端口号std::string _ip;//服务器IP地址【注意:暂时设置,后续可以进行更改】public:Socket_Server(const Socket_Server&) = delete;Socket_Server& operator=(const Socket_Server&) = delete;//构造函数Socket_Server(const std::string& IP = temp_IP, const uint16_t port = temp_port):_socketfd(temp_socketfd), _isrunning(false), _port(port), _ip(IP){}//初始化服务器void Init(){//1、使用scoket函数建立套接字。AF_INET代表IPv4网络协议,SOCK_DGRAM指定为数据报套接字,通常用于 UDP 协议。//2、使用bind 函数将一个套接字(socket)与一个本地地址(IP 地址和端口)绑定起来。}//启动服务器void Start(){//1、保持服务器不退出,使用recvfrom接收客户端信息//2、信息接收成功后使用sendto向客户端回复信息}//析构函数~Socket_Server(){//1、关闭套接字描述符}
};

1、初始化服务器

//初始化服务器void Init(){//1、使用scoket函数建立套接字。AF_INET代表IPv4网络协议,SOCK_DGRAM指定为数据报套接字,通常用于 UDP 协议。_socketfd = ::socket(AF_INET, SOCK_DGRAM, 0);//第三个参数指定为0即可,标识根据前面两个参数选择指定协议if(_socketfd < 0){perror("Create Socket False!!!");exit(SOCKET_ERROR);}std::cout << "Socket Success!!!" <<std::endl;//2、使用bind 函数将一个套接字(socket)与一个本地地址(IP 地址和端口)绑定起来。struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;//地址族,设置为IPv4local.sin_port = htons(_port);//16位主机序端口号转换为网络序local.sin_addr.s_addr = inet_addr(_ip.c_str());//local.sin_addr.s_addr = INADDR_ANY; //也可以这样设置,后续单独解释int ret = ::bind(_socketfd, (struct sockaddr*)&local, sizeof(local));if(ret == -1){perror("Bind IP And Port False!!!");exit(BIND_ERROR);}std::cout << "Bind Success!!!" <<std::endl;}

 INADDR_ANY宏:

  • INADDR_ANY 的值为 0.0.0.0,表示任意地址。它的具体定义通常是一个 32 位的整数,表示 IPv4 地址中的任意地址。

在进行套接字绑定IP地址的操作时,我们可以不绑定具体的地址。

当绑定具体的地址时,服务器仅接受从该特定 IP 地址发来的连接请求。

当不绑定到具体的 IP 地址,而使用INADDR_ANY 时,服务器会监听来自主机所有网络接口的连接请求。

local.sin_addr.s_addr = INADDR_ANY;

对于一台主机来讲,它可能有多个网卡(网络接口),可能是物理网卡或虚拟网卡。每个网卡都有各自的独立的IP地址。当客户端想访问服务端时,它需要拿到服务端进程绑定的IP地址和端口号。

        但是对于一台主机而言,服务端进程的端口号是唯一的,但是IP地址却不止一个。当需要接收来自多个接口的连接请求时,将绑定地址设置为INADDR_ANY ,如此,客户端发送的信息只要发送的目的IP地址属于服务端主机,并且目的端口号也是服务端进程的端口号,那么服务端就可以接收到向这台主机发送的所有的数据和请求。

2、运行服务器

//启动服务器void Start(){//1、保持服务器不退出,使用recvfrom接收客户端信息,并使用sendto向客户端回复信息_isrunning = true;char buffer[BUFFER_SIZE];memset(buffer, 0, sizeof(buffer));while(_isrunning){struct sockaddr_in from_client;memset(&from_client, 0, sizeof(from_client));socklen_t client_len = sizeof(from_client);size_t ret = ::recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&from_client, &client_len);if(ret == 0){_isrunning = false;std::cout << "Server Exit!!!" << std::endl;}else if(ret < 0){perror("Recvfrom False!!!"); exit(RECVFROM_ERROR);}else{buffer[ret] = '\0';//将接收的信息打印出来:std::string client_ip_addr = inet_ntoa(from_client.sin_addr);std::string inf = "Receive Information From Client IP " + client_ip_addr + " : " + buffer;std::cout << inf << std::endl;//向客户端反馈已经收到信息std::string res = "Server Echo : ";res += buffer;size_t ret = ::sendto(_socketfd, res.c_str(), res.size(), 0, (struct sockaddr*)&from_client, client_len);if(ret == -1){perror("Sendto False!!!"); exit(SENDTO_ERROR);}}}}

3、关闭服务器

在main函数中,对服务器对象的管理我们可以采取两种方式:1、使用智能指针管控,当服务端接到信号进行退出时,智能指针会及时合理释放服务端对象占用的内存空间。2、在栈上创建对象,当程序退出后,函数的栈帧销毁,服务器对象资源也随之被清理。

我们要做的就是确保对象销毁时将套接字的描述符进行关闭,以确保文件描述符表中的无用资源被清理,这一步在析构函数中进行:

//析构函数~Socket_Server(){//关闭套接字描述符if(_socketfd >= 0)::close(_socketfd);}

需要注意的是,套接字描述符实际也属于文件描述符,同样遵守文件描述符的规则,即:从文件描述符表中最小的空闲位置开始为其分配文件描述符。由于文件描述符表是一个指针数组,而文件描述符实际是这个数组的下标,这也注定了套接字描述符不会小于0!

客户端代码: 

#pragma once
#include <string>
#include <iostream>
#include <signal.h>
#include <sys/types.h>        
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>#define BUFFER_SIZE 1024const int temp_socketfd = -1;
const std::string temp_IP = "127.0.0.1"; //回环地址,表示计算机自身的网络接口,作为测试使用
const uint16_t temp_port = 8888;    //自行设置的端口号,大小必须在1024~65535之间enum{SOCKET_ERROR = 1,BIND_ERROR,RECVFROM_ERROR,SENDTO_ERROR
};class Client_Server 
{
private:int _socketfd;  //套接字描述符bool _isrunning;//服务器运行标志uint16_t server_port; //服务器16位端口号std::string server_ip;//服务器IP地址【注意:暂时设置,后续有更改】public:Client_Server(const Client_Server&) = delete;Client_Server& operator=(const Client_Server&) = delete;//构造函数Client_Server(const std::string& IP = temp_IP, const uint16_t port = temp_port):_socketfd(temp_socketfd), _isrunning(false), server_port(port), server_ip(IP){}//初始化客户端void Init(){//1、使用scoket函数建立套接字。AF_INET代表IPv4网络协议,SOCK_DGRAM指定为数据报套接字,通常用于 UDP 协议。_socketfd = ::socket(AF_INET, SOCK_DGRAM, 0);//第三个参数指定为0即可,标识根据前面两个参数选择指定协议if(_socketfd < 0){perror("Client Create Socket False!!!");exit(SOCKET_ERROR);}std::cout << "Socket Success!!!" << std::endl;}//启动服务器void Start(){_isrunning = true;char buffer[BUFFER_SIZE];memset(buffer, 0, sizeof(buffer));while(_isrunning){//1、客户端输入信息std::cout << "Please Enter : ";std::string enter;getline(std::cin, enter);//2、设置服务端的套接字的信息结构,使用sendto向目的IP和端口号发送信息struct sockaddr_in server_sock;memset(&server_sock, 0, sizeof(server_sock));server_sock.sin_family = AF_INET;//地址族,设置为IPv4server_sock.sin_port = htons(server_port);//16位主机序端口号转换为网络序server_sock.sin_addr.s_addr = inet_addr(server_ip.c_str());size_t send_num = sendto(_socketfd, enter.c_str(), enter.size(), 0, (struct sockaddr*)&server_sock, sizeof(server_sock));if(send_num < 0){perror("Client Sendto False!!!");exit(SENDTO_ERROR);}//3、使用recvfrom获取服务端的反馈信息,并获取服务端的套接字的信息结构struct sockaddr_in from_server;memset(&from_server, 0, sizeof(from_server));socklen_t len = sizeof(from_server);char buffer[BUFFER_SIZE];size_t recv_num = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&from_server, &len);if(recv_num < 0){perror("Client Recvfrom False!!!");exit(RECVFROM_ERROR);}buffer[recv_num] = '\0';//4、将接收的信息打印出来:std::string client_ip_addr = inet_ntoa(from_server.sin_addr);std::string inf = "Receive Information From Server IP " + client_ip_addr + " : " + buffer;std::cout << inf << std::endl;}}//析构函数~Client_Server(){//关闭套接字描述符if(_socketfd >= 0)::close(_socketfd);}
};

因为客户端向服务端发送信息时需要指定服务端的IP地址和端口号,因此,在客户端的main函数中可以采取命令行参数的方式从命令行输入中获取用户主动输入的IP地址和端口号。

由于格式不同,使用相应的格式转换函数进行转换即可。

#include "UDP_Client.hpp"int main(int argc, char* argv[])
{if(argc < 3){std::cerr << "Usage: " << argv[0] << " server-ip and server-port not provided" << std::endl;exit(0);}Client_Server client(argv[1], std::stoi(argv[2]));client.Init();client.Start();return 0;
}

Makefile: 

.PHONY:all
all:server clientserver:Server_Main.ccg++ -o $@ $^ -std=c++14client:Client_Main.ccg++ -o $@ $^ -std=c++14.PHONY:clean 
clean:rm -f server client 

效果展示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/399557.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UIAbility组件基础(一)

一、概述 UIAbility组件是一种包含UI的应用组件&#xff0c;主要用于和用户交互。UIAbility组件是系统调度的基本单元&#xff0c;为应用提供绘制界面的窗口。一个应用可以包含一个或多个UIAbility组件。每一个UIAbility组件实例都会在最近任务列表中显示一个对应的任务。 U…

C语言 ——— 学习、使用memmove函数 并模拟实现

目录 memmvoe函数的功能 学习memmove函数​编辑 模拟实现memmove函数 memmvoe函数的功能 memmvoe函数的功能类似于memcpy函数&#xff0c;都是内存拷贝&#xff0c;唯一的区别是memcpy函数不能成功拷贝原数据&#xff0c;而memmvoe函数可以 举例来说&#xff1a; [1, 2, 3…

单元测试注解:@ContextConfiguration

ContextConfiguration注解 ContextConfiguration注解主要用于在‌Spring框架中加载和配置Spring上下文&#xff0c;特别是在测试场景中。 它允许开发者指定要加载的配置文件或配置类的位置&#xff0c;以便在运行时或测试时能够正确地构建和初始化Spring上下文。 基本用途和工…

全国首个数据要素人才标准,亿信华辰携76家单位共同起草

在数字化浪潮汹涌的今天&#xff0c;数据已跃升为社会经济发展的核心引擎。如何精准、高效地评估数据要素领域人才的专业能力&#xff0c;成为了亟待解决的关键议题。亿信华辰积极响应国家战略布局&#xff0c;依托自身在大数据管理与应用领域的深厚底蕴&#xff0c;携手业界76…

CUDA Programming - (1) CUDA简介

1. GPU 简介 处理器一般包含以下几部分&#xff1a;Cache 缓存&#xff0c;ALU 计算单元&#xff0c;Control 控制中心&#xff0c;RAM 内存。 CPU&#xff08;Central Processing Unit&#xff09;&#xff1a;中央处理器。适合进行逻辑&#xff0c;ALU计算核心较少。适合控…

企业源代码加密软件有哪些?2024最好用的十款源代码加密软件

在当今快速发展的技术环境中&#xff0c;企业源代码的安全性至关重要。源代码不仅包含着企业的核心知识产权&#xff0c;还可能涉及敏感的商业数据。因此&#xff0c;选择一款合适的源代码加密软件&#xff0c;对于保护企业资产和避免数据泄露风险至关重要。随着安全技术的不断…

Codeforces Pinely Round 4 (Div. 1 + Div. 2) A~G

A.Maximize the Last Element&#xff08;枚举&#xff09; 题意&#xff1a; 给你一个由 n n n个整数组成的数组 a a a&#xff0c;其中 n n n是奇数。 在一次操作中&#xff0c;你将从数组 a a a中删除两个相邻的元素&#xff0c;然后将数组的剩余部分连接起来。例如&…

git常见命令和常见问题解决

文章目录 常见命令问题问题1&#xff08;git push相关&#xff09;问题2&#xff08;git push相关&#xff09;问题3&#xff08;git push相关&#xff09;删除github的仓库github新创建本地仓库的操作…or create a new repository on the command line…or push an existing …

led台灯对眼睛好不好?揭秘使用护眼台灯能不能有效预防近视

在当前社会&#xff0c;近视的影响不容小视&#xff0c;除了对视觉健康的影响外&#xff0c;近视还可能对个人的心理健康产生负面影响。视力不佳可能导致自卑感和社会交往障碍&#xff0c;尤其是在儿童和青少年时期。保护视力健康要从小做起&#xff0c;家长们可以关注孩子的用…

[openSSL]TLS 1.3握手分析

文章目录 前言一、ECDHE密钥交换二、TLS单向身份认证三、TLS双向身份认证 前言 关于TLS握手网上资料很多&#xff0c;但是有一些写的很不清楚&#xff0c;导致学习时对概念和流程出现混淆&#xff0c;以下是我觉得写得比较清晰和准确的供学习参考。 浅析 TLS&#xff08;ECDHE…

超算互联网-Stable Diffusion 2.1文生图教程

一、名词简介 1. 超算互联网 超算互联网是一种基于云计算的高性能计算平台&#xff0c;用户可以通过互联网接入超级计算资源。它集成了大量的计算节点&#xff0c;提供强大的计算能力&#xff0c;适用于科学计算、深度学习、人工智能等领域。用户可以利用超算互联网平台运行复…

老阳推荐的temu选品师项目能不能做成?

在不断变化的电商领域&#xff0c;temU选品师项目作为一种新兴职业&#xff0c;受到了越来越多的关注。老阳的推荐使得这一项目引起了不少人的兴趣&#xff0c;那么&#xff0c;temU选品师项目究竟能否成功呢?让我们从一个新的角度来探讨这一问题。 新兴市场的机遇与挑战 temU…

基于VEH的无痕HOOK

这里的无痕HOOK指的是不破坏程序机器码,这样就可以绕过CRC或MD5的校验。 VEH利用了Windows的调试机制和异常处理,人为抛出异常,从异常的上下文中获取寄存器信息。 DLL入口 // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" #include "CHoo…

jenkins一键推送到远程服务器并用docker容器启动

1.安装jenkins 我后端使用的是宝塔面板来安装的容器化jenkins,要选中允许外部访问&#xff0c;安装完之后没有那个选项了&#xff0c;一开始安装的时候要选中不使用域名和后面的允许外部访问。Jenkins 版本为&#xff1a; 2.462.1 2.配置Jenkins 2.1 Git plugin 安装完毕之…

100 Exercises To Learn Rust 挑战!构文・整数・变量

前一篇文章 【0】准备 【1】构文・整数・变量 ← 本次全部文章列表 《100 Exercise To Learn Rust》第2回&#xff0c;也就是实际演习的第1回&#xff01;从这次开始&#xff0c;我们会适度减少前置说明&#xff0c;直接进入问题的解决&#xff01; 本次的相关页面 1.1. Syn…

Java:接口interface

文章目录 接口interface好处为什么要用接口 接口案例需求思路代码Student.javaClassManage.javaStudentOperator 接口StudentOperatorImpl1.javaStudentOperatorImpl2.javaTest.java 黑马程序员学习笔记 接口interface 接口中&#xff1a;变量默认为常量&#xff0c;方法默认为…

全网最详细HAProxy入门小知识

目录 一. 负载均衡 负载均衡的意义&#xff1a; 负载均衡的类型&#xff1a; 二. HAProxy 简介 HAProxy 的特点&#xff1a; 社区版和企业版&#xff1a; 三. HAProxy 的安装和服务信息 1、实验环境 1&#xff09;安装并配置 Nginx 2&#xff09;在客户端测试 2、安装…

使用C语言构建Lua库

Lua 本身是用 C 语言编写的&#xff0c;因此使用 C 编写扩展可以更好地与 Lua 引擎集成&#xff0c;减少性能瓶颈&#xff0c;同时C 语言提供了对底层硬件和操作系统功能的直接访问能力&#xff0c;让 Lua 可以通过 C 扩展来实现对文件系统、网络等高级功能的支持。因为C 语言非…

大语言模型的模型量化(INT8/INT4)技术

目录 一、LLM.in8 的量化方案 1.1 模型量化的动机和原理1.2 LLM.int8 量化的精度和性能1.3 LLM.int8 量化的实践 二、SmoothQuant 量化方案 2.1 SmoothQuant 的基本原理2.2 SmoothQuant 的实践 三、GPTQ 量化训练方案 3.1 GPTQ 的基本原理3.2 GPTQ 的实践 参考资料 一、LLM.i…

SD-WAN企业组网:与传统组网有何不同?

很多企业已经尝试过使用SD-WAN来进行组网。SD-WAN代表着一种新兴的网络连接技术&#xff0c;与传统的网络架构相比&#xff0c;它在许多方面都有明显的不同。 SD-WAN基于软件定义网络&#xff08;SDN&#xff09;的概念&#xff0c;提供集中化的网络控制和智能优化&#xff0c;…