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

news2024/9/23 21:28:12

目录

一、了解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_INET
        unsigned 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_INET6
        u_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.1
    
        char *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.1
    
        char 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_INET
        uint16_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_INET6
        uint16_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 1024

int 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 data
    buf[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 1024

const 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;//地址族,设置为IPv4
        local.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 1024

const 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;//地址族,设置为IPv4
            server_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 client

server:Server_Main.cc
	g++ -o $@ $^ -std=c++14

client:Client_Main.cc
	g++ -o $@ $^ -std=c++14

.PHONY:clean 
clean:
	rm -f server client 

效果展示:

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网站开发涉及到的技术内容介绍——后端PHP(2)

网站开发涉及到的技术内容介绍——后端PHP(1)https://blog.csdn.net/xiaochenXIHUA/article/details/141000752?spm=1001.2014.3001.5501 一、PHP的常用函数 1.1、PHP文件夹的常用函数 PHP的目录常用函数 序号目录常用函数说明1$_SERVER[DOCUMENT_ROOT]获取到PHP项目的根目…

C++ -- 负载均衡式在线OJ (一)

一、项目宏观结构 1.项目功能 本项目的功能为一个在线的OJ&#xff0c;实现类似leetcode的题目列表、在线提交、编译、运行等功能。 2.项目结构 该项目一共三个模块&#xff1a; comm : 公共模块compile_server : 编译与运行模块oj_server : 获取题目列表&#xff0c;查看题…

Spring Boot项目缺少配置文件的解决方法:IDEA

本文介绍在IntelliJ IDEA软件中&#xff0c;为Spring Boot项目添加配置文件的操作方法。 最近&#xff0c;在IntelliJ IDEA软件中新创建了一个Spring Boot项目&#xff0c;是通过如下图所示的方法直接新建的。 但是&#xff0c;随后发现这样创建的Spring Boot项目没有配置文件。…

Threejs实现鼠标控制相机+键盘控制模型+点击指定点控制模型移动

1.前言 Threejs实现鼠标控制相机功能,键盘控制模型功能,点击指定点控制模型移动功能 键盘使用WASD控制模型移动效果图: 鼠标移动可控制相机的位置控制模型移动到指定点效果图: 2.功能拆分 根据以上效果图,可以得到以下三个主要实现的功能 鼠标移动可以使相机跟随通过键…

leetcode-121-买卖股票的最佳时机

原理&#xff1a; 核心原理&#xff1a; 如果我们真的在买卖股票&#xff0c;我们肯定会想&#xff1a;如果我是在历史最低点买入就好了&#xff01;该历史最低点是指卖出当天之前的历史最低点而不是全局最低点。 实现步骤&#xff1a; 1、初始化变量preprices[0]表示历史股…

20240809 每日AI必读资讯

乒乓球AI机器人赢了人类&#xff01;正反手灵活转换&#xff0c;擦网球高球都能接 - 谷歌发布首个达到人类竞技水平的机器人Agent&#xff0c;挑战乒乓球赛场。 - 机器人通过学习大量乒乓球状态数据&#xff0c;掌握了正手上旋球、反手瞄准等技能&#xff0c;展现出高速运动…

CTFHUB | web进阶 | PHP | Bypass disable_function | bypass iconv 2

开启题目 查看源码&#xff0c;发现可以蚁剑连接 进入之后无发现&#xff0c;使用插件 iconv 上传脚本 进入之后发现多了一个 .antproxy.php&#xff0c;复制文件名重新拼接连接 进入终端&#xff0c;查看根目录之后发现了有两个 flag 文件&#xff0c;之后发现了本题的 flag

STM32CUBEMX+PWM多一个尖峰的问题

问题描述&#xff1a;使用TIM2的通道3产生PWM波形&#xff0c;产生n个数量的波形后&#xff0c;在停止的时候会有一个尖峰。 怀疑是自动重载值临界的时候有问题&#xff0c;对重载值多减一个值&#xff0c;但还是有这个问题。 解决&#xff1a;电路是默认低电平&#xff0c;我…

skynet 连接redis

文章目录 概述main.luaagent.luaredis.lua 小结 概述 之前写过skynet 入门篇&#xff0c;还有skynet实操篇&#xff1b;这2篇&#xff0c;主要写了skynet如何使用&#xff0c;还有些skynet的调用流程之类。 其实&#xff0c;看过skynet的demo之后&#xff0c;发现skynet中没有…

L1-书生·浦语大模型全链路开源体系介绍

视频观看地址&#xff1a;书生浦语大模型全链路开源开放体系_哔哩哔哩_bilibili 本视频介绍了书生葡语大模型的开源开放体系&#xff0c;包括技术发展、性能提升、模型架构、开源生态等。 要点: - &#x1f31f; 开源开放体系涵盖数据收集、标注、训练、微调、评测、部署等全…

Ubuntu 系统的部署和基础操作(使用)

​ 大家好&#xff0c;我是程序员小羊&#xff01; 前言 Ubuntu 是一款基于 Debian 的开源 Linux 操作系统&#xff0c;以其易用性和强大的社区支持而广受欢迎。对于许多初次接触 Linux 的用户来说&#xff0c;Ubuntu 是理想的入门选择。本文将介绍 Ubuntu 系统的基本操作和使用…

cordova打包后请求不到接口(接口请求失败)

原因&#xff1a;CORS跨域问题导致 解决方法&#xff1a; 将根目录下的config.xml打开&#xff0c;添加 preference 即可

10分钟学会docker安装与使用

文章目录 1、docker简介2、docker的基本组成3、docker的安装与配置4、docker的常用命令 1、docker简介 什么是容器&#xff1f; 它是一种虚拟化的方案&#xff0c;是操作系统级别的虚拟化&#xff0c;只能运行相同或相似内核的操作系统&#xff0c;依赖于Linux内核特性&#x…

Qt实现圆形窗口

重新实现paintEvent()函数。 效果如下&#xff1a; 效果为蓝色区域&#xff0c;背景是vs接面&#xff0c;代码直接复制可用&#xff0c;留给有需要的人。 #ifndef CircleWidget_h__ #define CircleWidget_h__#include <QWidget>class CCircleWidget : public QWidget {Q…

MySQL安装以及配置

目录 1. MySQL安装包下载 2. 安装 3. 配置 4. 使用MySQL 5. 配置环境变量 1. MySQL安装包下载 1.1 迅雷下载 分享文件&#xff1a;MySQL安装包.zip 链接&#xff1a;https://pan.xunlei.com/s/VO3llUOt6rFFWl9TdrTrJI-cA1?pwdxere# 1.2 官网下载 MySQL :: Download MyS…

如何从戴尔笔记本电脑硬盘恢复数据

“如何从坏掉的戴尔笔记本电脑硬盘中恢复数据&#xff1f;我无法访问硬盘&#xff0c;但我确实需要从硬盘中检索数据。我有很多重要文件被困在那里。” 人们学习如何从戴尔笔记本电脑硬盘恢复数据的原因有很多&#xff0c;例如有意或无意删除、硬盘格式化、安全警告隔离受病毒…

springboot社区旧物回收系统-计算机毕业设计源码94813

目 录 摘要 1 绪论 1.1 研究背景 1.2研究意义 1.3论文结构与章节安排 2 社区旧物回收系统系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据流程 3.3.2 业务流程 2.3 系统功能分析 2.3.1 功能性分析 2.3.2 非功能性分析 2.4 系统用例分析 2.5本章小结 3 社…

适合出行的蓝牙耳机推荐有吗?盘点4款开放式耳机排行版前十名

如果说出行想要佩戴耳机&#xff0c;但是又不知道选什么样的耳机&#xff0c;那其实你可以看看我的建议。因为我自己其实是个比较爱玩爱出去乱逛且选择恐惧症的耳机重度患者&#xff0c;所以就平时经常会跑去公园散步戴耳机听歌&#xff0c;或者是自己去野炊之类的&#xff1b;…

二叉树的遍历与根据遍历序列求二叉树

二叉树的遍历&#xff1a; 1、先序遍历&#xff1a;DLR 2、中序遍历: LDR 3、后序遍历: LRD (L表示遍历左子树&#xff0c;D表示遍历根结点&#xff0c;R表示遍历右子树&#xff09; 以下图举例说明&#xff1a; 以先序遍历为例&#xff1a; 1、因为先序遍历的规则为D…

直击Vue2/3watch的底层逻辑,字符串长度对侦听效率的影响

目录 直击Vue2/3watch的底层逻辑&#xff0c;字符串长度对侦听效率的影响 一、Vue 2的底层原理 二、Vue 3的底层原理 三、基础类型性能消耗 四、数据变化比较原理 1、Vue 2 中的引用类型比较 2、Vue 3 中的引用类型比较 3、字符串比较&#xff08;基础类型比较&#xf…