【网络】套接字 -- UDP

news2024/11/27 6:25:36

🥁作者华丞臧.
📕​​​​专栏:【网络】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 👉 LeetCode刷题网站


文章目录

  • 一、网络编程套接字
    • 1.1 认识端口号
    • 1.2 认识UDP和TCP协议
    • 1.3 网络字节序
  • 二、UDP套接字
    • 2.1 sockaddr结构
    • 2.2 简单的UDP网络程序
    • 2.1 socket 常见API
      • recvform
      • bind
      • inet_addr
    • 2.1 封装 UdpSocket
      • server
      • init
      • start
    • 2.2 udpclient
    • 2.4 日志显示
    • 2.5 测试


一、网络编程套接字

在数据包的头部中,有两个IP地址,分别叫做源IP地址和目的IP地址;但是光有这两个IP地址不能实现网路通信;在计算机上进行通信的时候,实际上是用户和用户在进行网络通信,而用户通常是用程序体现的,也就是说用户是通过计算机上的某一个进程(软件)来进行数据的交互。

结论:网络通信的本质就是进程间的通信。

因此在实现网络通信时,不仅需要IP地址来确保主机的唯一性,还需要一个标记来确保主机上进程的唯一性。我们将这个表示主机上进程唯一性的标记称为端口号

1.1 认识端口号

端口号(port)是传输层协议的内容。

  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
  • 一个端口号只能被一个进程占用。
  • 端口号可以将操作系统的进程管理和网络解耦。
  • 进程不一定都需要网络通信,端口号可以标识需要进行网络通信的进程。
  • 0~1023的端口号是已经被使用了的,用户的端口号只能从1024 ~ 65536。

理解端口号和进程PID:

  1. PID表示唯一一个进程;
  2. 端口号也表示唯一一个进程;
  3. 一个进程可以绑定多个端口号;
  4. 一个端口号只能绑定唯一一个进程。

1.2 认识UDP和TCP协议

传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号,就是在描述 “数据是谁发的, 要发给谁”。

对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识,此处先了解TCP:

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识,此处先了解UDP:

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

UDP实现足够简单,TCP实现较为复杂;两个协议各有优缺点,不同场景选择合适的协议即可。

1.3 网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分, 那么如何定义网络数据流的地址呢?

两台主机进行网络通信,发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
世界各地的计算机生产厂商非常多, 因此对于发送方和接收方的大小端字节序是无法确定的。无法控制通信双方主机,但是我们可以规定网络中的字节序;TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略, 直接发送即可。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络
字节序和主机字节序的转换。

在这里插入图片描述

  • 这些函数名很好记,h表示hostn表示networkl表示32位长整数s表示16位短整数
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

二、UDP套接字

2.1 sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。用于本地通信和用于网络通信的套接字接口需要的数据都是不同的,为了能够使用统一的接口来实现网络通信和本地通信,接口设计者给出了一个 抽象结构,如下图:
在这里插入图片描述

系统中存在三种结构体,struct sockaddr(抽象类型)、struct sockaddr_in(INET网路套接字)、struct sockaddr_un(Unix域套接字套接字)。struct sockaddr内部会处理判断数据的前16位进行类型识别,然后强转成对应的类型实现切片,可以理解为C++的中多态。

  • 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结构体指针做为参数。

sockaddr 结构
在这里插入图片描述

sockaddr_in 结构
在这里插入图片描述
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in;这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。

in_addr结构
在这里插入图片描述
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。

2.2 简单的UDP网络程序

2.1 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);

说明:

  • domain:域,进行本地通信还是网络通信。

    • AF_UNIX:本地通信;
    • AF_INET:IPV4;
  • type:套接字类型,决定通信对应的报文类型,常用的为流式和用户数据报。

  • protocol:协议类型,网络应用中直接设为0即可。

  • socket套接字返回的是文件描述符,可以理解所有网络操作最终都是文件描述符级的操作。

recvform

用于从套接字文件描述符中读取数据。

 #include <sys/types.h>
 #include <sys/socket.h>
 
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
					struct sockaddr *src_addr, socklen_t *addrlen);

说明:

  • sockfd:套接字
  • buf:用于接收数据的指针,从sockfd文件中读取的数据放入指针指向的空间中;
  • len:读取数据的字节数;
  • flags:模式;
  • src_addr:用于接收读取对方的port和ip地址;
  • addrlen:src_addr数据的字节大小;

bind

用于绑定网络信息,指明ip和port。

#include <sys/types.h>         
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  • sockfd:套接字文件描述符;
  • addr:需要绑定的sockaddr结构;
  • addrlen:对应sockaddr结构的字节大小;

inet_addr

用于将字符串中的点分十进制转换成32位4字节的表示形式。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);

in_addr_t inet_addr(const char *cp);

in_addr_t inet_network(const char *cp);

char *inet_ntoa(struct in_addr in);

struct in_addr inet_makeaddr(int net, int host);

in_addr_t inet_lnaof(struct in_addr in);

in_addr_t inet_netof(struct in_addr in);

2.1 封装 UdpSocket

server

server是提供服务的一端,可以理解为服务器,用来接收用户传输的消息,同时也可以给用户发消息;可以将服务器封装成一个类,类当中包含套接字的初始化以及提供服务的接口,启动服务器时必须将端口号以参数的形式传递给main函数,ip地址可传可不传。

#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>


#include "Log.hpp"

static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}

// 写一个简单的udpSever
// 云服务器有一些特殊情况:
// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意

class UdpServer
{
public:
    UdpServer(int port, std::string ip = "")
    :sockfd_(-1)
    ,port_(port)
    ,ip_(ip)
    {

    }

    ~UdpServer()
    {

    }
public:
	void init()  //初识化server
	{}	
	void start() //server开始服务
	{}   
private:
    // 服务器的fd
    int sockfd_;
    // 服务器必须得有端口号信息
    uint16_t port_;
    // 服务器必须得有IP地址
    std::string ip_;
};

// ./server port ip
int main(int argc, char *argv[])
{
    if(argc != 2 && argc != 3) 
    {
        Usage(argv[0]);
        exit(PARA_ERR);
    }

    uint16_t port = atoi(argv[1]); 
    std::string ip;
    if(argc == 3)
    {
        ip = argv[2];
    }
    UdpServer server(port,ip);
    server.init();  // 创建并配置套接字
    server.start(); // 提供服务

    return 0;
}

init

初识化的任务主要是创建套接字,将套接字绑定网络信息指明ip和port。一般初识化分为以下几个步骤:
在这里插入图片描述

void init()
    {
        // 1. 创建套接字
        // domain -- 域
        // type -- 套接字类型,SOCK_DGRAM--数据报格式
        // protocol -- 协议类型,网络应用中:0
        // 返回值其实是文件描述符
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);  //就是打开一个文件

         if(sockfd_ < 0)
         {
            logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);
            exit(SOCK_ERR);
         }
         logMessage(DEBUG, "socket create success:%d", sockfd_);

        // 2. 绑定网络信息,指明ip+port
        // 2.1 先填充基本信息到 struct sockaddr_in
        struct sockaddr_in local;
        bzero(&local, sizeof(local));  //初始化为0
        local.sin_family = AF_INET;  //填充协议家族,域
        local.sin_port = htons(port_);     //填充服务对应的端口号信息,一定会发给对方,因此port_一定回到网络中
        //local.sin_addr;
        local.sin_addr.s_addr = ip_.empty()? htonl(INADDR_ANY) : inet_addr(ip_.c_str());  // htonl将INADDR_ANY转换成32位网络字节序, inet_addr将字符串中的点分十进制转换成32位比特位

        if(bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) == -1)
        {
            logMessage(FATAL, "bind:%s", strerror(errno));
            exit(BIND_ERR);
        }
        logMessage(DEBUG, "socket bind success:%d", sockfd_);
        //完成
    }

start

start()是server提供服务的接口,因此该函数必须是一个死循环(服务器都是在一个死循环当中)以给用户提供持续的服务;在start函数中,主要完成接收用户发送的消息并且将消息提取出来,其主要步骤如下图:
在这里插入图片描述

void start()
{
        //  服务器都是在一个死循环当中
        char inbuffer[1024];  //将来读取到的数据,都放在这里
        char outbuffer[1024]; //将来发送的数据,都放在这里
        while(true)
        {
            struct sockaddr_in peer;      //输出型参数
            socklen_t len = sizeof(peer); //输入输出型参数
            // UDP是无连接的
            // 对方发消息,你需要接收消息
            ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, \
                (struct sockaddr*)&peer, &len);
            logMessage(DEBUG, "server 提供服务中.....");
            if(s > 0) 
            {
                //接收成功
                inbuffer[s] = '\0';
            }
            else if(s == -1)
            {
                //
                logMessage(WARINING, "recvfrom fialed:%s[%d]", strerror(errno), sockfd_);
                continue;
            }
            // 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]
            std::string peerIp = inet_ntoa(peer.sin_addr);  //拿到对方的ip
            uint32_t peerPort = ntohs(peer.sin_port);  //拿到对方的port
            // 打印出来客户端给服务器发送过来的消息
            logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);

            logMessage(DEBUG, "server 提供服务中.....");
            //sleep(1);
        }
}

2.2 udpclient

客户端用来连接服务器,并且使用服务器提供的服务;用户想要客户端与服务器进行网络通信就必须将服务器使用的端口号和ip传给客户端,客户端通过端口号和ip可以实现与服务器地网络通信。
在这里插入图片描述

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>


static void Usage(std::string name)
{
    std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
} 


// ./udpClient IP port
int main(int argc, char* argv[])
{
    if(argc != 3)   //必须等于3,必须传ip和port
    {
        Usage(argv[0]);
        exit(PARA_ERR);
    }
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    // 2. 创建客户端
    // 2.1 创建socket

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    assert(sockfd > 0);

    struct sockaddr_in ser;
    bzero(&ser, sizeof ser);
    ser.sin_family = AF_INET;   //填充与服务器相同的协议
    ser.sin_port = htons(port); //服务器的端口号
    ser.sin_addr.s_addr = inet_addr(ip.c_str());  //服务器ip

    std::string buffer;
    while(true)
    {
        std::cerr << "Please Enter# ";
        std::getline(std::cin, buffer);
        // 发送消息给server
        sendto(sockfd, buffer.c_str(), buffer.size(), 0,
               (const struct sockaddr *)&ser, sizeof(ser)); // 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
    }

    close(sockfd);

    return 0;
}

2.4 日志显示

在这里插入图片描述

  • vs_start:使用距离可变参数最近的一个参数初始化ap;
  • va_arg:对可变参数列表提参,提参类型为type;
  • va_end:将ap指针置为空;
//将可变参数列表全部转化为某个字符串当中
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

说明:
	str:用于保存数据的字符串数组地址;
	size:最大写入的空间大小;
	format:以什么格式化写入;
	ap:可变参数列表部分;
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>

#define DEBUG     0
#define NOTICE    1
#define WARINING  2
#define FATAL     3
#define SOCK_ERR  4  
#define BIND_ERR  5
#define PARA_ERR  6

const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};

// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);

    char *name = getenv("USER");  //获取用户名

    char logInfo[1024];
    va_list ap; // ap -> char*  //
    va_start(ap, format);  //

    vsnprintf(logInfo, sizeof(logInfo)-1, format, ap); //写入logInfo
    va_end(ap); // ap = NULL

    FILE *out = (level == FATAL) ? stderr:stdout;

    fprintf(out, "%s | %u | %s | %s\n", \
        log_level[level], \
        (unsigned int)time(nullptr),\
        name == nullptr ? "unknow":name,\
        logInfo);
}

2.5 测试

  • 使用127.0.0.1可以进行本地通信。
    在这里插入图片描述
  • 使用云服务器的ip地址可以进行网络通信。
    在这里插入图片描述

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

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

相关文章

远程使用服务器上的Jupyter notebook

记录下如何远程使用服务器上的jupyter notebook。 主要是在服务器端执行以下操作&#xff1a; 激活需要使用的环境使用pip list 或conda list检查是否已经安装notebook。如果没有安装&#xff0c;则使用pip install jupyter notebook进行安装&#xff1b;反之忽略这一步&…

HDMI协议介绍(四)--Video

目录 视频格式 RGB444 YUV444 YUV422 YUV420 Color Depth Video控制信号 Pixel Repetition HDMI支持多种视频格式和分辨率。以hdmi1.4和2.0协议来说&#xff0c;视频格式支持RGB444、YUV444、YUV422和YUV420&#xff0c;其中RGB444和YUV444一般都是要求支持的。 视频格式…

【计算机网络】网络层IP协议

文章目录一、认识IP协议二、IP协议头部格式三、IP地址划分1. IP地址分类2. 子网划分四、IP地址数量危机1. IP地址的数量限制2. NAT技术五、私网IP和公网IP六、路由1. 认识路由2. 路由表生成算法一、认识IP协议 IP协议是Internet Protocol&#xff08;互联网协议&#xff09;的…

Centos7 安装jenkins java1.8版本

1. 首先安装好jdk1.8 2. 安装jenkins 命令&#xff1a;(可以在根目录&#xff0c;创建文件夹 mkdir home 然后在此文件夹下操作 cd /home) a 清华源&#xff0c;获取jenkins安装包 wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat/jenkins-2.346-1.1.noarch.rp…

2023软件测试金三银四常见的软件测试面试题-【测试理论篇】

三、测试理论 3.1 你们原来项目的测试流程是怎么样的? 我们的测试流程主要有三个阶段&#xff1a;需求了解分析、测试准备、测试执行。 1、需求了解分析阶段 我们的SE会把需求文档给我们自己先去了解一到两天这样&#xff0c;之后我们会有一个需求澄清会议&#xff0c; 我…

GO进阶(4) 深入Go的内存管理

Go语言成为高生产力语言的原因之一自己管理内存&#xff1a;Go抛弃了C/C中的开发者管理内存的方式&#xff0c;实现了主动申请与主动释放管理&#xff0c;增加了逃逸分析和GC&#xff0c;将开发者从内存管理中释放出来&#xff0c;让开发者有更多的精力去关注软件设计&#xff…

1633_xv6 book PC硬件与BootLoader

全部学习汇总&#xff1a; GreyZhang/g_unix: some basic learning about unix operating system. (github.com) 按照课程的建议&#xff0c;先去读了一下xv6 book的附录&#xff0c;感觉还是有一些收获的。这中间去扫盲增补各种概念的过程就已经收获不少。 1. 这里介绍了一下计…

C#:Krypton控件使用方法详解(第九讲) ——kryptonRadioButton

今天介绍的Krypton控件中的kryptonRadioButton&#xff0c;这是一个单选按钮控件。下面开始介绍这个控件的属性&#xff1a;首先介绍的是外观属性&#xff0c;如下图所示&#xff1a;Cheacked属性&#xff1a;表示设置kryptonRadioButton控件的初始选中状态是什么样的&#xff…

Buuctf [ACTF新生赛2020]Universe_final_answer 题解

1.程序逻辑 程序逻辑并不复杂: 首先输入字符串,然后对字符串进行一个判断是否满足条件的操作 如果满足则对字符串进行处理并输出,输出的就是flag 2.judge_860函数 显然根据这十个条件可以通过矩阵解线性方程组,这里对变量的命名做了一些调整,让Vi对应flag[i]方便读 ​​​​…

2018年蓝桥杯省赛试题-5道(Python)

文章目录一、日志统计思考二、递增三元组思考三、螺旋折线思考四、乘积最大思考五、全球变暖思考尾声提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、日志统计 题目描述 小明维护着一个程序员论坛。 现在他收集了一份"点赞"日志&#xf…

mysql数据库表的多条件查询

mysql数据库表的多条件查询 一、select语句基本查询 SELECT 字段1,字段2....FROM 表名[WHERE 条件] [LIMIT N][ OFFSET M]select可以返回多条数据也可以返回一条数据如果要查询所有的字段可以用 *****代替where后面跟的是筛选条件&#xff08;可选&#xff09;N 是返回的数据…

1632_x86中几种地址概念的理解

全部学习汇总&#xff1a; GreyZhang/g_unix: some basic learning about unix operating system. (github.com) 在看xv6的资料的时候发现有几个概念没弄清楚&#xff0c;结果让我理解资料的时候感觉比较模糊。这几个概念名词也倒是简单&#xff1a;逻辑地址、线性地址、物理地…

vmware创建虚拟机centor7

右键 选择下好的centos7 设置密码 登录

Spring是怎么解决循环依赖的

1.什么是循环依赖&#xff1a; 这里给大家举个简单的例子&#xff0c;相信看了上一篇文章大家都知道了解了spring的生命周期创建流程。那么在Spring在生命周期的哪一步会出现循环依赖呢&#xff1f; 第一阶段&#xff1a;实例化阶段 Instantiation 第二阶段&#xff1a;属性赋…

代码随想录 NO52 | 动态规划_leetcode 647. 回文子串 516.最长回文子序列

动态规划_leetcode 647. 回文子串 516.最长回文子序列今天是动态规划最后一天的题了&#xff0c;整个过程已经接近尾声了&#xff01; 647. 回文子串 确定dp数组&#xff08;dp table&#xff09;以及下标的含义 本题如果我们定义&#xff0c;dp[i] 为 下标i结尾的字符串有 dp…

【改进灰狼优化算法】改进收敛因子和比例权重的灰狼优化算法【期刊论文完美复现】(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密…

【Hello Linux】进程概念

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;简单介绍下进程的概念 进程基本概念PCB 程序控制块task_struct是什么task_struct里面有什么查看进程通过系统目录查看进程通过ps指令查…

22.2.26打卡 Codeforces Round #853 (Div. 2)

A题极端考虑, 只要存在一个前缀数组的最大公约数小于等于2, 将其放在数组最前端, 那么保证能够满足题目要求数据范围这么小, 果断暴力Serval and Mochas Array题目描述Mocha likes arrays, and Serval gave her an array consisting of positive integers as a gift.Mocha thin…

ARM Context synchronization event和Instruction Synchronization Barrier

在Arm architecture里&#xff0c;经常提到Context synchronization event(CSE)和Explicit synchronization&#xff0c;Context synchronization events在之前是叫作context synchronization operations。Explicit synchronization是Context synchronization event的结果&…

C++9:优先级队列以及仿函数和反向迭代器

目录 优先级队列的基本增删查改实现 仿函数 反向迭代器 优先级队列的本质其实是一个堆&#xff0c;具体到底层的数据结构其实是有数学关系所形成的一个类似二叉树的结构 至于其优先级的这个特性&#xff0c;跟大堆小堆的性质是相同的&#xff0c;只不过它使用了仿函数来控制…