【netty系列-03】深入理解NIO的基本原理和底层实现(详解)

Netty系列整体栏目


内容链接地址
【一】深入理解网络通信基本原理和tcp/ip协议https://zhenghuisheng.blog.csdn.net/article/details/136359640
【二】深入理解Socket本质和BIOhttps://zhenghuisheng.blog.csdn.net/article/details/136549478
【三】深入理解NIO的基本原理和底层实现https://zhenghuisheng.blog.csdn.net/article/details/138451491

深入理解NIO的基本原理和底层实现

  • 一,深入理解NIO的底层原理
    • 1,Reactor反应堆模式
      • 1.1,通过餐厅描述Bio
      • 1.2,通过餐厅引入nio
    • 2,NIO三大核心组件
    • 3,NIO通信原理
    • 4,通过NIO实现简单网络编程

一,深入理解NIO的底层原理

在上一篇中,讲解了bio的底层原理和具体实现,虽然bio在一定场景下也可以进行通信,但是随着互联网越来越多业务的场景,bio会存在阻塞的弊端被暴露无疑,在并发量稍微大点的地方,通过bio实现的网络编程会显得略显吃力。于是在jdk1.4之后,引入了一个新东西 NIO ,由于bio原名叫做 Blocking IO阻塞io,因此新网络编程的取名nio,有着 NoBlocking IO即不阻塞io,当然也有的地方取名为new io。

在讲解nio之前,依旧和以前的学习一样,不能脱离官网进行学习:netty官网地址 ,用户指南可以参考4.1版本
在这里插入图片描述

1,Reactor反应堆模式

网络编程从bio废弃到再到nio的崛起,跟nio的底层实现有着很大的联系,其最主要的设计思想就是这个 reactor 反应堆模式,总结这个reactor模式主要有三点:注册感兴趣的事件,扫描是否有感兴趣的事件发生,在事件发生之后做出相应的处理

在讲解这个反应堆模式之前,先通过一个生活中的案例来讲述这个事情,以我们去线下餐厅点餐为例,首先用户扫码点餐,然后点餐系统返回一个排队的号码,再服务员喊到号码的时候去取餐,就以这个案例来说明一下什么是反应堆模式。

1.1,通过餐厅描述Bio

首先在上面的这个点餐的案例中,bio的实现如下,就有点类似于用户直接和厨师直接进行交流,告诉厨师要什么菜,当没有用户点餐时,那么厨师就会一直等待用户来点餐,直到有用户点餐为止,如果一直没有用户点餐,那么厨师就会一直处于阻塞状态进行等待,这里就对应了bio服务端,没有客户端请求时会长期处于一个阻塞状态;

如果已经有了一个用户点餐,那么厨师会先炒这个用户的菜,当有其他用户来点餐时,那么其他用户会处于阻塞状态,只有等帮第一个用户炒完菜之后,厨师才能和第二个用户进行交流,第二个用户才能把自己需要什么菜告诉厨师,在如果在上一个用户的菜还没炒玩之前,那么下一个用户则会处于一个阻塞等待状态。因此这样效率肯定是非常低下的,那么毫无疑问,bio的这种方式注定是要被淘汰的。(这里服务端默认为在一个cpu里面,就是说一个cpu中只有一个线程去处理请求,上面案例对应的就是服务端对应的就是一个厨师,厨师就是老板,其他顾客就是对应的服务端)

在这里插入图片描述

1.2,通过餐厅引入nio

由于一个厨师对应多个用户效率会十分的低下,而且如果用户量稍微大一点,那么每个用户就不用去干其他的事情,就一直排队阻塞在那里,因此严重的影响整个系统的吞吐量以及严重的影响用户的体验感。随着客户的增加,或者午餐这段高峰期,为了解决用户长时间等待问题,那么就可以做一个点餐系统,用户只需扫码点餐即可,当用户点餐完成之后,可以去做用户自己想做的事情,如出去逛逛等,此时系统会给用户一个点餐号,此时就解决了用户长时间排队阻塞的问题。厨师这边也不需要每次只处理一个请求,如多个用户点同一个菜,那么厨师可以一次性炒多份菜,这样也提高了厨师这边的效率。当厨师将菜炒好之后,只需要服务员通过念号或者通过公众号通知订餐的用户即可。

在这里插入图片描述

反应堆模式就是,不能一直等着客户端去等待服务端的响应,而是通过某个中间层,客户端先向中间层注册一个事件,当服务端有空做出响应的时候再通过定时任务去扫描这个中间层,当中间层发现有注册的事件之后,再去通知客户端,这样就可以减少客户端的等待时间。换句话就是说,通过请求响应的模式来说,客户端向服务端发送一个请求之后,如果服务端长时间没有响应,那么客户端可以结束此次请求,服务端来不及响应,但是服务端得记录这个请求的记录,当服务端有空的时候,再去扫描这个记录,再去响应这个请求,再通过通知异步的去响应对应的请求。

2,NIO三大核心组件

在nio编程中,里面有三大核心组件,分别是 Selector、Channel、Buffer 三大组件。

在上面讲解了通过餐厅系统去了解nio的内部实现,在这三大组件中,扮演的角色分别如下:

  • 由于在网络编程中,基本是基于tcp协议去实现客户端和服务端之间的通信,因此通过socket将tcp协议封装,而这里的channel,是对这个socket进行了再次的封装。也就是说,只需要创建这个channel实例就可以完成双端之间的通信,因此点餐系统里面的用户和厨师之间的交流就是通过这个channel去实现的,那么channel扮演的角色就是完成客户和厨师之间的最终交流
  • Selector就是一个选择器,通过这个餐厅系统,可以发现引入了一个新的点餐系统,用于注册客户的订单以及在订单完成之后给予响应,就是通知下单的客户,因此这个Selector选择器扮演的角色就是这个点餐系统,也是这个反应堆模式的核心,用于注册客户端事件,扫描这些注册的事件,并对这些事件做出具体的响应
  • 而这个Buffer,就是nio和bio之间的重大区别,因为这个Buffer就是一个Nio的一个重要的特性,用于面向缓冲流进行编程,这个Buffer指的是应用层之间的buffer,就是已经建立好连接之后,在服务端内部的一个缓冲区,如在这个餐厅系统中,在准备食材的时候也是需要大量时间的,如果先点餐的用户需要准备的食材要久一些,那么厨师可以优先炒后面用户下的单,那么这个Buffer就起到重要的作用了。由于这个bio是串行执行,那么就不存在这个Buffer的说法,但是在这个nio里面,通过这个Buffer让整个系统更加的灵活,即使先建立的请求,也可以后响应,从而提高整个系统的吞吐量。还有比如说可以重复的读取数据,来不及处理的优先放在这个buffer缓冲区,某个buffer缓冲区如果字节数没达到要求可以先去处理其他的缓冲区等,主要是让整个系统更加的灵活多变,从而提高整个系统的吞吐量和响应。同时也是与BIO最大的差异化之一

在这里插入图片描述

3,NIO通信原理

通过上面的餐厅事例和讲解NIO内部的三大组件,接下来通过一个发送和接收数据的事例讲解NIO底层到底是如何进行网络通信和数据传输的。

  • 首先客户端先向服务端发送一个请求,然后服务端在接收到这个请求之后,服务端首先会通过这个Selector先向本地注册一个连接事件,然后再扫描Channel事件列表,查看是否有感兴趣的Channel事件
  • 在Channel中找到这个对连接感兴趣的事件之后,随后通知这个感兴趣的事件,创建一个ServerSocketChannel对象,用于服务端和客户端通过三次握手建立可靠的连接
  • 完成建立连接之后,又会去Selector中扫描是否有对读数据感兴趣的事件,如果找到有服务端对读数据感兴趣的事件,又会通知对这个事件感兴趣的具体事件,用于实例化SocketChannel对象,这里的SocketChannel就是建立好连接的Socket对象,用于真正的去读取数据以及发送数据
  • socket读取的数据并不是发送给服务端的应用程序,而是将数据先存入到Buffer中,让应用程序去读取buffer里面的数据,从而提高整个架构的吞吐量和效率
  • 最后将要响应的数据也存到Buffer中,然后通过感兴趣的写事件,将数据返回给对应的客户端即可

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4,通过NIO实现简单网络编程

上面讲解了大量的理论,接下来通过具体的编码,来讲述NIO的底层到底是怎么实现的。首先创建一个服务端的线程,用于接收客户端的请求以及内部做出的响应,接下来创建一个 NioServerTask 的任务类,并且实现一个 Runnable 方法,在该方法中去创建 selector,ServerSocketChannel,SockerChannel、Buffer等对象

package com.zhs.netty.nio.nio;

import com.zhs.netty.nio.Const;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * 服务端线程具体代码实现
 */
@Slf4j
public class NioServerTask implements Runnable{

    private volatile boolean started;
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;

    /**
     * 构造方法
     * @param port 指定要监听的端口号
     */
    public NioServerTask(int port) {
        try {
            //创建一个选择器
            selector = Selector.open();
            //创建ServerSocketChannel的实例
            serverSocketChannel = ServerSocketChannel.open();
            //通道实例设置为非阻塞模式
            serverSocketChannel.configureBlocking(false);
            //绑定端口
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            //注册事件到selector之上,监听客户端连接
            serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
            started = true;
            log.info("服务器已启动,端口号:" + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while(started){
            try {
                //selector每隔1s被唤醒一次
                selector.select(1000);
                //获取全部已经注册的本地事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    //将处理过的本地注册事件给删除
                    iterator.remove();
                    handleInput(key);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //处理具体的事件
    private void handleInput(SelectionKey key) throws IOException {
        if(key.isValid()){
            //处理新接入的客户端的请求
            if(key.isAcceptable()){
                //获取channels全部事件中对此感兴趣的事件
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                //获取到感兴趣的事件之后,创建一个socket实例,用于发送和读取数据
                SocketChannel sc = ssc.accept();
                //设置为非阻塞
                sc.configureBlocking(false);
                //注册一个感兴趣的读事件
                sc.register(selector,SelectionKey.OP_READ);
            }
            //处理对端的发送的数据
            if(key.isReadable()){
                SocketChannel sc = (SocketChannel) key.channel();
                //创建ByteBuffer,开辟一个缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(buffer);
                if(readBytes>0){
                    //缓冲区中存在指针,记录有效位置
                    buffer.flip();
                    //根本有效位置的指针处创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String message = new String(bytes,"UTF-8");
                    log.info("服务器收到消息:" + message);
                    String result = Const.response(message);
                    doWrite(sc,result);

                }else if(readBytes<0){
                    //将channels集合的数据取消
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    /*发送应答消息*/
    private void doWrite(SocketChannel sc,String response) throws IOException {
        byte[] bytes = response.getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        buffer.put(bytes);
        buffer.flip();
        sc.write(buffer);
    }
}

从上面的代码中可以发现,在服务端中只关注了读的事件,并没有关注写的事件。并且在这个Buffer中,存在一个指针,用于记录buffer的有效位置,这样在读数据时,只需要读取到有效的数据即可。

服务端代码写好之后,接下来编写客户端的代码,代码和客户端基本一样,但是由于客户端不需要提供服务,因此在客户端这边是不需要 ServerSocketChannel 这个组件的。其他的 SocketChannel,Selector,Buffer 还是需要的

package com.zhs.netty.nio.nio;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @author zhenghuisheng
 * nio客户端请求
 */
@Slf4j
public class NioClientTask implements Runnable{
    private String host;
    private int port;
    private volatile boolean started;
    private Selector selector;
    private SocketChannel socketChannel;

    public NioClientTask(String ip, int port) {
        this.host = ip;
        this.port = port;
        try {
            //创建选择器的实例
            selector = Selector.open();
            //创建ServerSocketChannel的实例
            socketChannel = SocketChannel.open();
            //设置通道为非阻塞模式
            socketChannel.configureBlocking(false);
            started = true;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        try{
            doConnect();
        }catch(IOException e){
            e.printStackTrace();
            System.exit(1);
        }

        //循环遍历selector
        while(started){
            try{
                //无论是否有读写事件发生,selector每隔1s被唤醒一次
                selector.select(1000);
                //获取全部已经注册的本地事件
                Set<SelectionKey> keys = selector.selectedKeys();
                //转换为迭代器
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while(it.hasNext()){
                    key = it.next();
                    it.remove();
                    try{
                        handleInput(key);
                    }catch(Exception e){
                        if(key != null){
                            key.cancel();
                            if(key.channel() != null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
                System.exit(1);
            }
        }
        //selector关闭后会自动释放里面管理的资源
        if(selector != null)
            try{
                selector.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
    }

    //具体的事件处理方法
    private void handleInput(SelectionKey key) throws IOException{
        if(key.isValid()){
            //获得关心当前事件的channel
            SocketChannel sc = (SocketChannel) key.channel();
            //连接事件
            if(key.isConnectable()){
                if(sc.finishConnect()){
                    socketChannel.register(selector,
                        SelectionKey.OP_READ);}
                else System.exit(1);
            }
            //有数据可读事件
            if(key.isReadable()){
                //创建ByteBuffer,并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //读取请求码流,返回读取到的字节数
                int readBytes = sc.read(buffer);
                //读取到字节,对字节进行编解码
                if(readBytes>0){
                    //将缓冲区当前的limit设置为position,position=0,
                    // 用于后续对缓冲区的读取操作
                    buffer.flip();
                    //根据缓冲区可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String result = new String(bytes,"UTF-8");
                    log.info("客户端收到消息:" + result);
                }
                //链路已经关闭,释放资源
                else if(readBytes<0){
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    private void doWrite(SocketChannel channel,String request)
            throws IOException {
        //将消息编码为字节数组
        byte[] bytes = request.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        //flip操作
        writeBuffer.flip();
        //发送缓冲区的字节数组
        channel.write(writeBuffer);
    }

    private void doConnect() throws IOException{
        //非阻塞的连接,这里需要注意,因为客户端和服务端都是无阻塞的,因此可能在三次握手建立连接之前,
        //这段注册读的代码就已经走完了,因此在else中增加一个注册连接的代码
        if(socketChannel.connect(new InetSocketAddress(host,port))){
            socketChannel.register(selector,SelectionKey.OP_READ);
        }else{
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
        }
    }
    //写数据对外暴露的API
    public void sendMsg(String msg) throws Exception{
        doWrite(socketChannel, msg);
    }
}

接下来进行一个数据的测试,先创建一个服务端的Main方法,然后启动这个Main方法,并且设置端口号为8881

public class NioServer {
    private static NioServerTask nioServerTask;
    public static void main(String[] args){
        nioServerTask = new NioServerTask(8881);
        new Thread(nioServerTask,"NioServer").start();
    }
}

再创建一个客户端的Main方法,ip设置成本地,端口号设置成服务端设置的端口号

/**
 * @author zhenghuisheng
 */
public class NioClient {
    private static NioClientTask nioClientTask;
    public static void main(String[] args) throws Exception {
        nioClientTask = new NioClientTask("127.0.0.1",8881);
        new Thread(nioClientTask,"nioClient").start();
        //控制台输入
        Scanner scanner = new Scanner(System.in);
        String message = scanner.next();
        while(!StringUtils.isEmpty(message)){
            nioClientTask.sendMsg(message);
        }
    }
}

客户端发送消息:

132432
21:58:41.118 [nioClient] INFO com.zhs.netty.nio.nio.NioClientTask - 客户端收到消息:Hello,132432,Now is Sat May 04 21:58:41 CST 2024

服务端接收到的消息:

21:58:30.767 [main] INFO com.zhs.netty.nio.nio.NioServerTask - 服务器已启动,端口号:8881
21:58:41.114 [NioServer] INFO com.zhs.netty.nio.nio.NioServerTask - 服务器收到消息:132432

到此为止,通过NIO的方式将服务端发送消息和客户端接收消息的代码实现

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringCloud Alibaba Nacos简单应用(三)

文章目录 SpringCloud Alibaba Nacos创建Nacos 的服务消费者需求说明/图解创建member-service-nacos-consumer-80 并注册到NacosServer8848创建member-service-nacos-consumer-80修改pom.xml创建application.yml创建主启动类业务类测试 SpringCloud Alibaba Nacos 创建Nacos 的…

鸿蒙通用组件Image简介

鸿蒙通用组件Image简介 图片----Image图片支持三种引用方式设置图片宽高设置图片缩放模式设置图片占位图设置图片重复样式设置图片插值效果 图片----Image Image主要用于在应用中展示图片 Image($r(app.media.app_icon)).width(150) // 设置宽.height(150) // 设置高.objectF…

使用docker-compose编排lnmp(dockerfile)完成wordpress

文章目录 使用docker-compose编排lnmp&#xff08;dockerfile&#xff09;完成wordpress1、服务器环境2、Docker、Docker-Compose环境安装2.1 安装Docker环境2.2 安装Docker-Compose 3、nginx3.1 新建目录&#xff0c;上传安装包3.2 编辑Dockerfile脚本3.3 准备nginx.conf配置文…

redis集群-主从机连接过程

首先从机需要发送自身携带的replid和offset向主机请求连接 replid&#xff1a;replid是所有主机在启动时会生成的一个固定标识&#xff0c;它表示当前复制流的id&#xff0c;当从机第一次请求连接时&#xff0c;主机会将自己的replid发送给从机&#xff0c;从机在接下来的请求…

docker部署nginx并配置https

1.准备SSL证书&#xff1a; 生成私钥&#xff1a;运行以下命令生成一个私钥文件。 生成证书请求&#xff08;CSR&#xff09;&#xff1a;运行以下命令生成证书请求文件。 生成自签名证书&#xff1a;使用以下命令生成自签名证书。 openssl genrsa -out example.com.key 2048 …

【Java探索之旅】内部类 静态、实例、局部、匿名内部类全面解析

文章目录 &#x1f4d1;前言一、内部类1.1 概念1.2 静态内部类1.3 实例内部类1.4 局部内部类1.5 匿名内部类 &#x1f324;️全篇总结 &#x1f4d1;前言 在Java编程中&#xff0c;内部类是一种强大的特性&#xff0c;允许在一个类的内部定义另一个类&#xff0c;从而实现更好的…

Vue3-element-plus表格

一、element-plus 1.用组件属性实现跳转路由 <el-menu active-text-color"#ffd04b" background-color"#232323" :default-active"$route.path" //高亮 text-color"#fff"router><el-menu-item index"/article/channe…

第十篇:深入文件夹:Python中的文件管理和自动化技术

深入文件夹&#xff1a;Python中的文件管理和自动化技术 1 文件系统基础操作 在今天的技术博客中&#xff0c;我们将深入探讨Python中的文件系统基础操作。文件系统对于任何操作系统都是不可或缺的组成部分&#xff0c;它管理着数据的存储、检索以及维护。Python通过其标准库中…

节能洗车房车牌识别项目实战

项目背景 学电子信息的你加入了一家节能环保企业&#xff0c;公司的主营产品是节能型洗车房。由于节水节电而且可自动洗车&#xff0c;产品迅速得到了市场和资本的认可。公司决定继续投入研发新一代产品&#xff1a;在节能洗车房的基础上实现无人值守的功能。新产品需要通过图…

Java高阶私房菜:JVM性能优化案例及讲解

目录 核心思想 优化思考方向 压测环境准备 堆大小配置调优 调优前 调优后 分析结论 垃圾收集器配置调优 调优前 调优后 分析结论 JVM性能优化是一项复杂且耗时的工作&#xff0c;该环节没办法一蹴而就&#xff0c;它需要耐心雕琢&#xff0c;逐步优化至理想状态。“…

Qt服务器端与客户端交互

Qt做客户端与服务器端交互第一步引入network 第一步引入network后继续编程首先界面设计 创建server和socket 引入QTcpServer&#xff0c;QTcpSocket MainWindow.h代码如下 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QTcpServer&…

EPAI手绘建模APP演示板、材质编辑器、样式编辑器

(11) 更多 图 74 更多工具栏 ① 演示板&#xff1a;打开关闭演示板。演示板用来显示从设备导入的模型图纸图片或者打开模型建模教程网页&#xff0c;是建模过程中一个辅助功能。有些设备有小窗口功能有些没有&#xff0c;对于没有小窗口功能的设备&#xff0c;通过演示板能够在…

智慧旅游引领旅游行业创新发展:借助智能科技的力量,实现旅游资源的优化配置和高效利用,推动旅游行业的转型升级和可持续发展

目录 一、引言 二、智慧旅游的定义与特点 1、信息化程度高 2、智能化服务丰富 3、互动性强 4、个性化服务突出 5、可持续性发展 三、智慧旅游在旅游行业创新发展中的作用 &#xff08;一&#xff09;优化旅游资源配置 &#xff08;二&#xff09;提升旅游服务质量 &…

【吃透Java手写】- Spring(上)-启动-扫描-依赖注入-初始化-后置处理器

【吃透Java手写】Spring&#xff08;上&#xff09;启动-扫描-依赖注入-初始化-后置处理器 1 准备工作1.1 创建自己的Spring容器类1.2 创建自己的配置类 ComponentScan1.3 ComponentScan1.3.1 Retention1.3.2 Target 1.4 用户类UserService Component1.5 Component1.6 测试类 2…

HCIA-题目解析1

0x00 前言 遇到这样一道题,这种题目对于我来说还是比较复杂的,所以记录一下。主要还是和熟练度有关系。 0x01 题目 路由器RouterID邻居关系如下,下列说法正确的是 A:本路由器和Router-lD为10.0.3.3的路由器不能直接交换链路状态信息 B:DR路由器的Router-lD为10.0.1.2 C:…

机器学习:基于K-近邻(KNN)、高斯贝叶斯(GaussianNB)、SVC、随机森林(RF)、梯度提升树(GBDT)对葡萄酒质量进行预测

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…

Finder Windows for Mac:双系统窗口,一键切换!

Finder Windows for Mac是一款专为Mac用户设计的实用工具&#xff0c;它模拟了Windows系统的窗口管理功能&#xff0c;让Mac用户也能享受到类似Windows的窗口操作体验。这款软件的主要功能是提供一个浮动面板&#xff0c;帮助用户随时即时访问打开的Finder窗口列表&#xff0c;…

力扣每日一题106:从中序与后序遍历序列构造二叉树

题目 中等 相关标签 相关企业 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1a;inorder [9,3,15,20,7], postorder …

在线协作,开源的设计和原型创作平台:penpot

penpot&#xff1a;面向团队&#xff0c;设计自由- 精选真开源&#xff0c;释放新价值。 概览 Penpot 是一款专为跨职能团队量身定制的开源设计软件&#xff0c;与行业领先的 Figma 齐名&#xff0c;提供了一个强大而灵活的在线设计解决方案。其最大的亮点在于&#xff0c;用户…

Qt与MySQL连接

QT连接Mysql数据库&#xff08;详细成功版&#xff09;-CSD N博客 我的MySQL是64位的&#xff0c;所以我的Qt的套件也需要是64位的 遇到的问题&#xff1a; &#xff08;available drivers中已经有QMYSQL QMYSQL3&#xff0c;还是not loaded&#xff09; QSqlDatabase: QMYS…