从 0 到 1 ,手把手教你编写《消息队列》项目(Java实现) —— 编写客户端

news2024/10/7 6:38:49

文章目录

  • 一、设计核心类
    • Connection 类
    • Channel 类
    • ConnectionFactory 类
  • 二、代码编写
    • Connection 类
    • Connection 类
    • Channel 类


一、设计核心类

Connection 类

Connection类有以下特点与功能

  • 表示一个TCP连接
  • 该类持有 Socket对象
  • 可以写入请求,读取响应
  • 管理多个 Channel 对象

Channel 类

Channel类有以下特点与功能

  • 表示一个逻辑上的连接
  • 内部有多个方法去构造请求调用服务器端对应的API

ConnectionFactory 类

ConnectionFactory类有以下特点与功能

该类持有服务器的ip地址与端口号,主要功能是 实例化Connection 类.


二、代码编写

在这里插入图片描述


Connection 类

/**
 * 一个TCP连接类
 */
public class Connection {
    private Socket socket = null;
    // 需要管理多个 channel 使用一个 hash表 把若干个 channel 组织起来
    private ConcurrentHashMap<String,Channel> channelMap = new ConcurrentHashMap<>();

    private InputStream inputStream;
    private OutputStream outputStream;
    private DataInputStream dataInputStream;
    private DataOutputStream dataOutputStream;

    // 线程池
    private ExecutorService callbackPool = null;

    public Connection(String host,int port) throws IOException {
        socket = new Socket(host, port);
        inputStream = socket.getInputStream();
        outputStream = socket.getOutputStream();
        dataInputStream = new DataInputStream(inputStream);
        dataOutputStream = new DataOutputStream(outputStream);
        callbackPool = Executors.newFixedThreadPool(5);

        // 创建一个扫描线程,由这个线程负责不停的从 socket 中读取响应数据,把这个响应数据再交给对应的 channel 负责处理
        Thread thread = new Thread(() -> {
            try {
                while (!socket.isClosed()) {
                    Response response = readResponse();
                    dispatchResponse(response);
                }
            } catch (SocketException e) {
                // 连接正常断开的,此时这个异常直接忽略
                System.out.println("[Connection] 连接正常断开!");
            } catch (IOException | ClassNotFoundException | MqException e) {
                System.out.println("[Connection] 连接异常断开!");
                e.printStackTrace();
            }

        });
        thread.start();
    }

    public void close() {
        // 关闭 Connection 释放上述资源

        try {
            callbackPool.shutdownNow();
            channelMap.clear();
            inputStream.close();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 使用这个方法来分别处理,当前这个响应是一个针对控制请求的响应,还是服务器推送的消息
    private void dispatchResponse(Response response) throws IOException, ClassNotFoundException, MqException {
        if (response.getType() == 0xc) {
            // 服务器推送来的消息数据
            SubScribeReturns subScribeReturns = (SubScribeReturns) BinaryTool.fromBytes(response.getPayload());
            // 根据 channelId 找到对应的 channel 对象
            // 执行该 channel 对象内部的回调函数
            Channel channel = channelMap.get(subScribeReturns.getChannelId());
            if (channel == null) {
                throw new MqException("[Connection] 该消息对应的 channel 在客户端中不存在! channelId=" + subScribeReturns.getChannelId());
            }
            // 执行该 channel 内部的回调函数
            callbackPool.execute(() -> {
                try {
                    channel.getConsumer().handleDelivery(subScribeReturns.getConsumerTag(),subScribeReturns.getBasicProperties(),
                            subScribeReturns.getBody());
                } catch (MqException | IOException e) {
                    e.printStackTrace();
                }
            });

        } else {
            // 当前响应是针对刚才的控制请求的响应
            BasicReturns basicReturns = (BasicReturns) BinaryTool.fromBytes(response.getPayload());
            // 把这个结果给他放到对应的 channel 的 哈希表中
            Channel channel = channelMap.get(basicReturns.getChannelId());
            if (channel == null) {
                throw new MqException("[Connection] 该消息对应的 channel 在客户端中不存在! channelId=" + basicReturns.getChannelId());
            }
            channel.putReturns(basicReturns);

        }
    }

    // 发送请求
    public void writeRequest(Request request) throws IOException {
        dataOutputStream.writeInt(request.getType());
        dataOutputStream.writeInt(request.getLength());
        dataOutputStream.write(request.getPayload());
        dataOutputStream.flush();
        System.out.println("[Connection 发送请求! type=" + request.getType() + ",length=" + request.getLength());
    }

    // 读取响应
    public Response readResponse() throws IOException {
        Response response = new Response();
        response.setType(dataInputStream.readInt());
        response.setLength(dataInputStream.readInt());
        byte[] payload = new byte[response.getLength()];
        int length = dataInputStream.read(payload);
        if (response.getLength() != length) {
            throw new IOException("读取的数据格式异常");
        }
        response.setPayload(payload);
        System.out.println("[Connection 接收响应! type=" + response.getType() + ",length=" + response.getLength());
        return response;
    }

    // 创建Channel(信道)
    public Channel createChannel() {
        String channelId = "C-" + UUID.randomUUID().toString();
        Channel channel = new Channel(channelId,this);
        // 同时也需要把 "创建 channel" 的这个消息也告诉服务器
        boolean ok = false;
        // 把这个 channel 对象 放到 Connection 管理 channel 的 哈希表 中
        channelMap.put(channelId,channel);
        System.out.println("Channel 创建成功!");
        try {
            ok = channel.createChannel();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (!ok) {
            // 服务器这里创建失败了,整个这次创建 channel 操作不顺利
            channelMap.remove(channelId);
            return null;
        }

        return channel;
    }
}

Connection 类

/**
 * Connection工厂类
 */
@Data
public class ConnectionFactory {
    // broker server 的 ip 地址
    private String host;
    // broker server 的端口号
    private int port;

//    // 访问 broker server 的哪个虚拟主机
//    private String virtualHostName;
//    private String username;
//    private String password;


    public Connection newConnection() throws IOException {
        Connection connection = new Connection(host,port);
        return connection;
    }
}

Channel 类

以下方法中对应服务器的API中的请求类型,一定要与之前定义的协议保持一致.

/**
 * 逻辑连接类
 */
@Data
public class Channel {
    private String channelId;

    // 当前这个 channel 属于哪个连接
    private Connection connection;

    // 用来存储后续客户端收到的服务器的响应
    private ConcurrentHashMap<String, BasicReturns> basicReturnsMap = new ConcurrentHashMap<>();

    // 如果当前 Channel 订阅了某个队列,就需要在此处记录下对应回调是啥,当该队列的消息返回回来的时候,调用回调
    // 此处约定一个 Channel 中只能有一个回调
    private Consumer consumer = null;

    public Channel(String channelId, Connection connection) {
        this.channelId = channelId;
        this.connection = connection;
    }

    // 期望使用这个方法来阻塞等待服务器的响应
    private BasicReturns waitResult(String rid) {
        BasicReturns basicReturns = null;
        while ((basicReturns = basicReturnsMap.get(rid)) == null) {
            // 查询结果为 null,证明 响应 还没到
            // 此时就需要阻塞等待
            synchronized (this) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        // 读取到响应后,删除 哈希表 中的存储的这个 响应
        basicReturnsMap.remove(rid);
        return basicReturns;
    }

    private String generateRid() {
        return "R-" + UUID.randomUUID().toString();
    }


    public void putReturns(BasicReturns basicReturns) {
        basicReturnsMap.put(basicReturns.getRid(),basicReturns);
        synchronized (this) {
            // 当前也不知道有多少个线程在等待上述的这个响应
            // 全都唤醒
            notifyAll();
        }
    }

    // 在以下方法中,和服务器交互,告知服务器,此处客户端 要进行的操作

    // 创建 channel
    public boolean createChannel() throws IOException {
        // 对于创建 Channel 操作来说,payload 就是一个 basicArguments 对象
        BasicArguments basicArguments = new BasicArguments();
        basicArguments.setChannelId(channelId);
        basicArguments.setRid(generateRid());
        byte[] payload = BinaryTool.toBytes(basicArguments);

        Request request = new Request();
        request.setType(0x1);
        request.setLength(payload.length);
        request.setPayload(payload);
        // 构造出完整请求后,发送请求
        connection.writeRequest(request);

        // 等待服务器响应
        BasicReturns basicReturns = waitResult(basicArguments.getRid());
        return basicReturns.isOk();
    }

    // 销毁 channel
    public boolean close() throws IOException {
        // 对于创建 Channel 操作来说,payload 就是一个 basicArguments 对象
        BasicArguments basicArguments = new BasicArguments();
        basicArguments.setChannelId(channelId);
        basicArguments.setRid(generateRid());

        byte[] payload = BinaryTool.toBytes(basicArguments);

        Request request = new Request();
        request.setType(0x2);
        request.setLength(payload.length);
        request.setPayload(payload);
        // 构造出完整请求后,发送请求
        connection.writeRequest(request);

        // 等待服务器响应
        BasicReturns basicReturns = waitResult(basicArguments.getRid());
        return basicReturns.isOk();
    }


    // 创建交换机
    public boolean exchangeDeclare(String exchangeName, ExchangeType exchangeType, boolean durable, boolean autoDelete,
                                   Map<String,Object> arguments) throws IOException {
        ExchangeDeclareArguments exchangeDeclareArguments = new ExchangeDeclareArguments();
        exchangeDeclareArguments.setRid(generateRid());
        exchangeDeclareArguments.setChannelId(channelId);
        exchangeDeclareArguments.setExchangeName(exchangeName);
        exchangeDeclareArguments.setExchangeType(exchangeType);
        exchangeDeclareArguments.setDurable(durable);
        exchangeDeclareArguments.setAutoDelete(autoDelete);
        exchangeDeclareArguments.setArguments(arguments);


        byte[] payload = BinaryTool.toBytes(exchangeDeclareArguments);


        Request request = new Request();
        request.setType(0x3);
        request.setLength(payload.length);
        request.setPayload(payload);
        connection.writeRequest(request);


        BasicReturns basicReturns = waitResult(exchangeDeclareArguments.getRid());
        return basicReturns.isOk();

    }

    // 删除交换机
    public boolean exchangeDelete(String exchangeName) throws IOException {
        ExchangeDeleteArguments exchangeDeleteArguments = new ExchangeDeleteArguments();
        exchangeDeleteArguments.setRid(generateRid());
        exchangeDeleteArguments.setChannelId(channelId);
        exchangeDeleteArguments.setExchangeName(exchangeName);

        byte[] payload = BinaryTool.toBytes(exchangeDeleteArguments);

        Request request = new Request();
        request.setType(0x4);
        request.setLength(payload.length);
        request.setPayload(payload);

        connection.writeRequest(request);

        BasicReturns basicReturns = waitResult(exchangeDeleteArguments.getRid());
        return basicReturns.isOk();
    }

    // 创建队列
    public boolean queueDeclare(String queueName,boolean durable,boolean exclusive,boolean autoDelete,
                                Map<String,Object> arguments) throws IOException {
        QueueDeclareArguments queueDeclareArguments = new QueueDeclareArguments();
        queueDeclareArguments.setRid(generateRid());
        queueDeclareArguments.setChannelId(channelId);
        queueDeclareArguments.setQueueName(queueName);
        queueDeclareArguments.setDurable(durable);
        queueDeclareArguments.setExclusive(exclusive);
        queueDeclareArguments.setAutoDelete(autoDelete);
        queueDeclareArguments.setArguments(arguments);

        byte[] payload = BinaryTool.toBytes(queueDeclareArguments);

        Request request = new Request();
        request.setType(0x5);
        request.setLength(payload.length);
        request.setPayload(payload);

        connection.writeRequest(request);

        BasicReturns basicReturns = waitResult(queueDeclareArguments.getRid());
        return basicReturns.isOk();
    }

    // 删除队列
    public boolean queueDelete(String queueName) throws IOException {
        QueueDeleteArguments queueDeleteArguments = new QueueDeleteArguments();
        queueDeleteArguments.setRid(generateRid());
        queueDeleteArguments.setChannelId(channelId);
        queueDeleteArguments.setQueueName(queueName);

        byte[] payload = BinaryTool.toBytes(queueDeleteArguments);

        Request request = new Request();
        request.setType(0x6);
        request.setLength(payload.length);
        request.setPayload(payload);

        connection.writeRequest(request);


        BasicReturns basicReturns = waitResult(queueDeleteArguments.getRid());

        return basicReturns.isOk();
    }

    // 创建绑定
    public boolean bindingDeclare(String exchangeName,String queueName,String bindingKey) throws IOException {
        BindingDeclareArguments bindingDeclareArguments = new BindingDeclareArguments();
        bindingDeclareArguments.setRid(generateRid());
        bindingDeclareArguments.setChannelId(channelId);
        bindingDeclareArguments.setExchangeName(exchangeName);
        bindingDeclareArguments.setQueueName(queueName);
        bindingDeclareArguments.setBindingKey(bindingKey);

        byte[] payload = BinaryTool.toBytes(bindingDeclareArguments);

        Request request = new Request();

        request.setType(0x7);
        request.setLength(payload.length);
        request.setPayload(payload);

        connection.writeRequest(request);

        BasicReturns basicReturns = waitResult(bindingDeclareArguments.getRid());
        return basicReturns.isOk();

    }

    // 解除绑定
    public boolean bindingDelete(String exchangeName,String queueName) throws IOException {
        BindingDeleteArguments bindingDeleteArguments = new BindingDeleteArguments();
        bindingDeleteArguments.setRid(generateRid());
        bindingDeleteArguments.setChannelId(channelId);
        bindingDeleteArguments.setExchangeName(exchangeName);
        bindingDeleteArguments.setQueueName(queueName);

        byte[] payload = BinaryTool.toBytes(bindingDeleteArguments);

        Request request = new Request();

        request.setType(0x8);
        request.setLength(payload.length);
        request.setPayload(payload);

        connection.writeRequest(request);

        BasicReturns basicReturns = waitResult(bindingDeleteArguments.getRid());
        return basicReturns.isOk();

    }

    // 发送消息
    public boolean basicPublish(String exchangeName, String routingKey, BasicProperties basicProperties, byte[] body) throws IOException {
        BasicPublishArguments basicPublishArguments = new BasicPublishArguments();
        basicPublishArguments.setRid(generateRid());
        basicPublishArguments.setChannelId(channelId);
        basicPublishArguments.setExchangeName(exchangeName);
        basicPublishArguments.setRoutingKey(routingKey);
        basicPublishArguments.setBasicProperties(basicProperties);
        basicPublishArguments.setBody(body);

        byte[] payload = BinaryTool.toBytes(basicPublishArguments);

        Request request = new Request();
        request.setType(0x9);
        request.setLength(payload.length);
        request.setPayload(payload);

        connection.writeRequest(request);

        BasicReturns basicReturns = waitResult(basicPublishArguments.getRid());
        return basicReturns.isOk();
    }

    // 订阅消息
    public boolean basicConsume(String queueName, boolean autoAck, Consumer consumer) throws MqException, IOException {
        // 先设置回调
        if (this.consumer != null) {
            throw new MqException("该 channel 已经设置过消费消息的回调函数了,不能重复设置");
        }
        this.consumer = consumer;

        BasicConsumeArguments basicConsumeArguments = new BasicConsumeArguments();
        basicConsumeArguments.setRid(generateRid());
        basicConsumeArguments.setChannelId(channelId);
        basicConsumeArguments.setConsumerTag(channelId); // 此处 ConsumerTag 也使用 channelId 表示
        basicConsumeArguments.setQueueName(queueName);
        basicConsumeArguments.setAutoAck(autoAck);

        byte[] payload = BinaryTool.toBytes(basicConsumeArguments);

        Request request = new Request();
        request.setType(0xa);
        request.setLength(payload.length);
        request.setPayload(payload);

        connection.writeRequest(request);

        BasicReturns basicReturns = waitResult(basicConsumeArguments.getRid());
        return basicReturns.isOk();
    }

    // 确认消息
    public boolean basicAck(String queueName,String messageId) throws IOException {
        BasicAckArguments basicAckArguments = new BasicAckArguments();
        basicAckArguments.setRid(generateRid());
        basicAckArguments.setChannelId(channelId);
        basicAckArguments.setQueueName(queueName);
        basicAckArguments.setMessageId(messageId);

        byte[] payload = BinaryTool.toBytes(basicAckArguments);

        Request request = new Request();
        request.setType(0xb);
        request.setLength(payload.length);
        request.setPayload(payload);

        connection.writeRequest(request);

        BasicReturns basicReturns = waitResult(basicAckArguments.getRid());
        return basicReturns.isOk();
    }


}

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

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

相关文章

万万没想到!| 三代宏病毒组研究还能这么干!

书接上回&#xff0c;我也是一个万万没想到啊&#xff0c;陈卫华&#xff0c;赵兴明老师的三代宏病毒组研究&#xff0c;居然让我追到续集了&#xff01; 前一回中&#xff0c;利用三代单分子测序技术&#xff0c;科研团队成功构建了中国人肠道噬菌体目录&#xff08;CHGV&…

云安全—K8S API Server 未授权访问

0x00 前言 master节点的核心就是api服务&#xff0c;k8s通过REST API来进行控制&#xff0c;在k8s中的一切都可以抽象成api对象&#xff0c;通过api的调用来进行资源调整&#xff0c;分配和操作。 通常情况下k8s的默认api服务是开启在8080端口&#xff0c;如果此接口存在未授…

【验证码系列】用逆向思维深度分析滑动验证码(含轨迹算法)

文章目录 1. 写在前面2. 抓包分析3. 接口分析4. 滑动验证码弹出分析5. 滑动验证分析6. 轨迹生成算法实现7. 生成W参数值算法 1. 写在前面 验证码是机器人防护&#xff08;即爬虫&#xff09;常用重要手段之一&#xff01;在爬虫这个领域内专精某一项&#xff08;验证码识别、JS…

七人拼团模式:颠覆你的购物观念,499元产品让你赚翻天!

七人拼团模式是一种创新的消费模式&#xff0c;通过聚集消费者的购买力&#xff0c;让消费者能够以更优惠的价格购买到优质的商品。下面我们以499元的产品为例&#xff0c;详细介绍七人拼团模式的玩法规则和收益计算。 玩法规则&#xff1a; 消费者购买499元的指定产品后&…

wireshark捕获DNS

DNS解析&#xff1a; 过滤项输入dns&#xff1a; dns查询报文 应答报文&#xff1a; 事务id相同&#xff0c;flag里 QR字段1&#xff0c;表示响应&#xff0c;answers rrs变成了2. 并且响应报文多了Answers 再具体一点&#xff0c;得到解析出的ip地址&#xff08;最底下的add…

write_edif 生成 AD9361 配置的自定义IP核

将AD9361配置文件设置为顶层 设置里&#xff1b; -mode out_of_context 替换文字 综合 导出 IP 核 write_edif -security_mode all D:/tops.edfD:/tops.edf write_verilog -mode synth_stub D:/tops_stub.vD:/tops_stub.v 调用 AD9361 IP 核

OpenAI将推出ChatGPT Plus会员新功能,有用户反馈将支持上传文件和多模态

&#x1f989; AI新闻 &#x1f680; OpenAI将推出ChatGPT Plus会员新功能&#xff0c;有用户反馈将支持上传文件和多模态 摘要&#xff1a;OpenAI为ChatGPT Plus会员推出了一些新功能&#xff0c;包括上传文件、处理文件和多模态支持。用户不再需要手动选择模式&#xff0c;…

视频直播与制作软件 Wirecast Pro mac中文版软件功能

Wirecast Pro mac是一款专业的视频直播和流媒体软件&#xff0c;由Telestream公司开发和发布&#xff0c;适用于各种场景&#xff0c;包括企业会议、体育赛事、音乐演出、教育培训等。 Wirecast Pro mac软件功能 支持多摄像头连接&#xff0c;实现多角度拍摄和切换。 可导入图片…

电脑文件夹怎么压缩?分享三个简单的方法!

为了节省存储空间和便于管理&#xff0c;压缩文件夹可以将多个文件或文件夹整合成一个压缩文件&#xff0c;从而节省存储空间。此外&#xff0c;压缩文件夹还可以方便地管理文件&#xff0c;那么电脑文件夹怎么压缩呢&#xff1f;一起来看看吧~ 一、电脑自带的压缩功能 1、找到…

虹科新闻 | 高性能超声波测距仪来袭,虹科与MaxBotix正式建立合作伙伴关系

近日&#xff0c;虹科与MaxBotix正式建立合作关系&#xff0c;将共同致力于提供高精度、工业级耐用性、功能先进和有防水保护的超声波传感器&#xff0c;重新定义距离测量技术。 虹科CEO陈秋苑表示&#xff1a;“我们非常自豪地宣布&#xff0c;与MaxBotix合作&#xff0c;为我…

在WEB应用使用MyBatis(使用MVC架构模式)

2023.10.30 本章将在web应用中使用MyBatis&#xff0c;实现一个银行转账的功能。整体架构采用MVC架构模式。 数据库表的初始化 环境的初始化配置 web.xml文件的配置&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"h…

Linux多线程服务端编程:使用muduo C++网络库 学习笔记 第六章 muduo网络库简介

2010年3月作者写了一篇《学之者生&#xff0c;用之者死——ACE历史与简评》&#xff08;http://blog.csdn.net/Solstice/archive/2010/03/10/5364096.aspx&#xff0c;ACE是&#xff08;Adaptive Communication Environment&#xff09;是一个C编写的开源框架&#xff0c;用于开…

如何使用IP归属地查询API加强网络安全

引言 在当今数字化时代&#xff0c;网络安全对于个人和组织来说至关重要。恶意网络活动的威胁不断增加&#xff0c;因此采取有效的措施来加强网络安全至关重要。其中之一是利用IP归属地查询API。这个工具可以为您的网络安全策略提供宝贵的信息&#xff0c;帮助您更好地保护自己…

微软bing大声朗读文档或网页卡顿老是中断,用离线的huihui就很流畅但没那么自然

默认的xiaoxiao_online好听&#xff0c;但卡顿&#xff0c;朗读功能确实受到了网络状态的影响。 大概率是网络问题。

概念解析 | 动态非线性系统 VS 非线性系统 VS 线性系统

KaTeX parse error: \newcommand{\blue} attempting to redefine \blue; use \renewcommand 注1:本文系“概念解析”系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:动态非线性系统 VS 非线性系统 VS 线性系统。 概念解析 | 动态非线性系统 VS 非线性…

国产射频功率放大器技术指标有哪些内容

射频功率放大器是一种广泛应用于通信、雷达、卫星等领域的高频电子设备&#xff0c;其作用是将微弱的电磁信号放大到足以传输或检测的强度。射频功率放大器技术指标是衡量其性能优劣的重要标准&#xff0c;其主要内容包括以下几个方面。 频率范围&#xff1a;射频功率放大器需要…

接雨水 DP 双指针

力扣 接雨水 public class 接雨水 {public static int trap(int[] height){int res 0;int len height.length;int[] maxLeft new int[len];//存 i 左边最高的高度int[] maxRight new int[len];//存 i 右边最高的高度maxLeft[0] 0;maxRight[len - 1] 0; // DPfor (int i…

leetcode每日一题复盘(10.30~11.5)

leetcode 93 复原ip地址 这一题和上一次的回文串那题差不多&#xff0c;都是给一串数据&#xff0c;在数据中挑出符合要求的放进结果集中 整个题目可以分成三部分&#xff0c;用来判断是否符合条件的函数&#xff0c;回溯函数&#xff0c;主函数 首先是判断函数&#xff0c;这…

ubuntu安装python以及conda

切换国内镜像源 备份下 sudo cp /etc/apt/sources.list /etc/apt/sources_init.list 更换源 sudo vi /etc/apt/sources.list 开头加 deb http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ trusty-sec…

项目解读_v2

1. 项目介绍 如果使用task2-1作为示例时&#xff0c; 运行process.py的过程中需要确认 process调用的是函数 preprocess_ast_wav2vec(wav, fr) 1.1 任务简介 首个开源的儿科呼吸音数据集&#xff0c; 通过邀请11位医师标注&#xff1b; 数字听诊器的采样频率和量化分辨率分…