Netty源码一:服务端启动

示例

public class Server {

    public static void main(String[] args) throws InterruptedException {
        // todo  创建两个 eventGroup boss 接受客户端的连接, 底层就是一个死循环, 不断的监听事件 处理事件
        // new NioEventLoopGroup(1); todo 入参1 表示设置boss设置为1个线程, 默认  = 计算机的 核数*2
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // todo  worker处理客户端的请求
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // todo 创建NettyServer的启动辅助类
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap
            // todo 到目前为止,group()就是把 上面创建的两个 事件循环组一个给了父类(AbstractBootStrap),一个给了自己
            .group(bossGroup, workerGroup)

            // todo  在每个netty服务器启动的时候,都会执行这个方法 ,接收了 NioServerSocketChannel.class 去反射;
            // todo  channel 是它父类的方法
            // todo  到目前为止仍然是赋值的操作, 把它赋值给 ServerBootstrap的父类 AbstractServerBootstrap
            .channel(NioServerSocketChannel.class)

            // todo 为客户端的连接设置相应的配置属性
            .childOption(ChannelOption.TCP_NODELAY,true)

            // todo 为每一个新添加进来的 属性信息, 可以理解成是跟业务逻辑有关 信息
            .childAttr(AttributeKey.newInstance("MyChildAttr"),"MyChildAttValue")

            // todo 添加handler
            .handler(new ServerHandler())

            // todo 添加自定义的子处理器, 处理workerGroup 的请求事件
            .childHandler(new MyServerInitializer()); // 添加自己的Initializer

            // sync() 可以当netty一直在这里等待
            // todo 启动!!!  实际上前面的准备工作都是为了Bind()方法准备的    bind()是它父类的方法, 这里有必要sync同步的等待,毕竟是服务端启动的步奏
            ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();

            Channel channel = channelFuture.channel();

            channel.closeFuture().sync(); // todo 确保程序执行完closeFuture后,再往下进行

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}


这里主要做了几件事:

  1. 设置boss、worker事件循环组
  2. 设置服务端channel
  3. 设置服务端handler
  4. 设置子处理器 childHandler
  5. 进行绑定操作bind, 这一块是核心

image.png

ServerBootstrap.bind

image.png
最终会调用到abstractBootstrap.doBind操作

AbstractBootstrap.doBind

private ChannelFuture doBind(final SocketAddress localAddress) {
    // todo  初始化 和 注册  带 Future 字眼的表示异步!!!  它本身返回的就是一个ChannelFuture
    final ChannelFuture regFuture = initAndRegister();

    final Channel channel = regFuture.channel();


    if (regFuture.cause() != null) {
        return regFuture;
    }

    if (regFuture.isDone()) {
        // At this point we know that the registration was complete and successful.
        ChannelPromise promise = channel.newPromise();
        // TODO  继续绑定端口 doBind0
        doBind0(regFuture, channel, localAddress, promise);
        return promise;
    } else {
        // Registration future is almost always fulfilled already, but just in case it's not.
        final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                Throwable cause = future.cause();
                if (cause != null) {
                    // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                    // IllegalStateException once we try to access the EventLoop of the Channel.
                    promise.setFailure(cause);
                } else {
                    // Registration was successful, so set the correct executor to use.
                    // See https://github.com/netty/netty/issues/2586
                    promise.registered();

                    doBind0(regFuture, channel, localAddress, promise);
                }
            }
        });
        return promise;
    }
}

�先看initAndRegister方法

final ChannelFuture  initAndRegister() {
    Channel channel = null;
    try {
        // todo 这个 channelFactory是那个 反射工厂ReflectiveChannelFactory  对服务端来说, 可以创建   NioServerSocketChannel  对象
        // todo 而这个对象又是 Selector的一种实现  就是SelectorProvider.providor()方法
        // todo 实例化 NioServerSocketChannel, 通过反射走的是无参的构造, 我们去追踪它的无参构造去
        channel = channelFactory.newChannel();


        // todo 初始化Channel, 好几轮赋值, 以及添加 handler 等组件
        init(channel);

    } catch (Throwable t) {
        if (channel != null) {
            // channel can be null if newChannel crashed (eg SocketException("too many open files"))
            channel.unsafe().closeForcibly();
        }
        // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
        return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
    }

    // todo  注册 group  == BOSS EventLoopGroup , -- > 暂时以为, 他是想确保把通过反射创建出来的NioServerSocketChannel注册进 BossGroup
    // todo  目的是,让通过这个NioServerSocketChannel中的ServerSocketChannel 去 accept客户端的连接, 进而把连接通过Acceptor 扔给 WorkerGroup
    // todo   config()--> ServerBootstrapConfig
    // todo   group()--> NioEventLoopGroup -- workerGroup
    // todo   我们用户点进去  进入 EventLoopGroup. 而 Debug 进入的是 MultithreadEventLoopGroup类 , 因为我这里的是 NioEventLoopGroup 是 MultithreadEventLoopGroup类的子类
    // todo  !!! 忽略的一个重点, group是 MultithreadEventLoopGroup类  我们知道这个类中维护的是 BossGroup, 即将channel注册进bossgroup中
    ChannelFuture regFuture = config().group().register(channel);

    if (regFuture.cause() != null) { // todo 非空表示注册失败了
        if (channel.isRegistered()) {
            channel.close();
        } else {
            channel.unsafe().closeForcibly();
        }
    }

    // If we are here and the promise is not failed, it's one of the following cases:
    // todo 如果我们期待的结果并没有失败, 就会出现下面几种情况

    // 1) If we attempted registration from the event loop, the registration has been completed at this point.
    //    i.e. It's safe to attempt bind() or connect() now because the channel has been registered.
    // todo 如果我们 企图往事件循环中注册通道, 因为现在这个通道晶注册完毕了,所以 bind() 和 connet()是安全的

    // 2) If we attempted registration from the other thread, the registration request has been successfully
    //    added to the event loop's task queue for later execution.
    //    i.e. It's safe to attempt bind() or connect() now:
    //         because bind() or connect() will be executed *after* the scheduled registration task is executed
    //         because register(), bind(), and connect() are all bound to the same thread.
    return regFuture;
}

channelFactory.newChannel(反射创建channel)

channelFactory.newChannel() 会最终调用到 外面配置的�NioServerSocketChannel.newInstance,也就是最终会走到它的构造方法
image.png
image.png
记住jdk的是 serverSocketChannel,接下来是调用带ServerSocketChannel的构造方法
image.png
super()一路最终调用AbstractNioChannel的构造方法,下面就是创建了一个NioServerSocketChannelConfig
image.png
这里AbstractNioChannel的构造方法里面做了几件事:

  1. 继续调用super,调用到AbstractChannel的构造方法,里面设置channelId、创建NioMessageUnsafe对象、创建Pipline对象
  2. 保存传入进来的jdk原生的ServerSocketChannel
  3. 设置上感兴趣的事件
  4. 设置非阻塞

最后用一张图总结一下:
image.png

init(channel):初始化channel

@Override
void init(Channel channel) throws Exception {
    // todo ChannelOption 是在配置 Channel 的 ChannelConfig 的信息
    final Map<ChannelOption<?>, Object> options = options0();
    synchronized (options) {
        // todo 把 NioserverSocketChannel 和 options Map传递进去, 给Channel里面的属性赋值
        // todo 这些常量值全是关于和诸如TCP协议相关的信息
        setChannelOptions(channel, options, logger);
    }
    // todo 再次一波 给Channel里面的属性赋值  attrs0()是获取到用户自定义的业务逻辑属性 --  AttributeKey
    final Map<AttributeKey<?>, Object> attrs = attrs0();
    // todo 这个map中维护的是 程序运行时的 动态的 业务数据 , 可以实现让业务数据随着netty的运行原来存进去的数据还能取出来
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e : attrs.entrySet()) {
            @SuppressWarnings("unchecked")
            AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
            channel.attr(key).set(e.getValue());
        }
    }
    // todo-------   options   attrs :   都可以在创建BootStrap时动态的传递进去


    // todo ChannelPipeline   本身 就是一个重要的组件, 他里面是一个一个的处理器, 说他是高级过滤器,交互的数据 会一层一层经过它
    // todo 下面直接就调用了 p , 说明,在channel调用pipeline方法之前, pipeline已经被创建出来了!,
    // todo 到底是什么时候创建出来的 ?  其实是在创建NioServerSocketChannel这个通道对象时,在他的顶级抽象父类(AbstractChannel)中创建了一个默认的pipeline对象
    /// todo 补充: ChannelHandlerContext 是 ChannelHandler和Pipeline 交互的桥梁
    ChannelPipeline p = channel.pipeline();

    // todo  workerGroup 处理IO线程
    final EventLoopGroup currentChildGroup = childGroup;
    // todo 我们自己添加的 Initializer
    final ChannelHandler currentChildHandler = childHandler;

    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;


    // todo 这里是我们在Server类中添加的一些针对新连接channel的属性设置, 这两者属性被acceptor使用到!!!
    synchronized (childOptions) {
        currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
    }
    synchronized (childAttrs) {
        currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
    }

    // todo 下面的代码中是Netty原生默认会往NioServerSocketChannel的管道里面添加了一个 ChannelInitializer  ,
    // todo 通过这个ChannelInitializer可以实现大批量的往 pipeline中添加处理器
    // todo  ( 后来我们自己添加的ChildHandler 就继承了的这个ChannelInitializer , 而这个就继承了的这个ChannelInitializer 实现了ChannelHandler)
    p.addLast(new ChannelInitializer<Channel>() { // todo 进入addlast

        // todo 这是个匿名内部类, 一旦new ,就去执行它的构造方法群, 完事后再回来看下面的代码,

        // todo  这个ChannelInitializer 方便我们一次性往pipeline中添加多个处理器
        @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            // todo  获取Bootstrap的handler 对象, 没有则返回空
            // todo  这个handler 针对BossGroup的Channel  , 给他添加上我们在server类中添加的handler()里面添加处理器
            ChannelHandler handler = config.handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }

            // todo ServerBootstrapAcceptor 接收器, 是一个特殊的chanelHandler
            ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        // todo !!! --   这个很重要,在ServerBootStrap里面,netty已经为我们生成了接收器  --!!!
                        // todo 专门处理新连接的接入, 把新连接的channel绑定在 workerGroup中的某一条线程上
                        // todo 用于处理用户的请求, 但是还有没搞明白它是怎么触发执行的
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                // todo 这些参数是用户自定义的参数
                                // todo NioServerSocketChannel, worker线程组  处理器   关系的事件
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
        }
    });
    System.out.println("哈哈哈哈.....");
}

�代码看着一大堆,其实就做了几件事:

  1. 将ServerBootStrap配置上的参数应用到channel上
  2. 然后在channel的pipline的链表上增加了一个ChannelInitializer
  3. 但是这个时候其实是没有执行ChannelInitializer里面的方法的

具体流程如图所示:
image.png


EventLoopGroup.register(channel):

由于EventLoopGroup是继承MutithreadEventLoopGroup,最终会调用到它
image.png
这里的next(一组EventLoopGroup里面有很多EventLoop,使用轮训算法给你找一个EventLoop)返回的是NioEventLoop:
image.png
由于EventLoop继承的是SingleThreadEventLoop,最终调用到它的register方法
image.png
image.png

最终会调用AbstractChannel.register方法

@Override
// todo 入参 eventLoop == SingleThreadEventLoop   promise == NioServerSocketChannel + Executor
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    if (eventLoop == null) {
        throw new NullPointerException("eventLoop");
    }
    if (isRegistered()) {
        promise.setFailure(new IllegalStateException("registered to an event loop already"));
        return;
    }
    if (!isCompatible(eventLoop)) {
        promise.setFailure(
            new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
        return;
    }
    // todo 赋值给自己的 事件循环, 把当前的eventLoop赋值给当前的Channel上  作用是标记后续的所有注册的操作都得交给我这个eventLoop处理, 正好对应着下面的判断
    // todo 保证了 即便是在多线程的环境下一条channel 也只能注册关联上唯一的eventLoop,唯一的线程
    AbstractChannel.this.eventLoop = eventLoop;

    // todo 下面的分支判断里面执行的代码是一样的!!, 为什么? 这是netty的重点, 它大量的使用线程, 线程之间就会产生同步和并发的问题
    // todo 下面的分支,目的就是把线程可能带来的问题降到最低限度
    // todo 进入inEventLoop() --> 判断当前执行这行代码的线程是否就是 SingleThreadEventExecutor里面维护的那条唯一的线程
    // todo 解释下面分支的必要性, 一个eventLoop可以注册多个channel, 但是channel的整个生命周期中所有的IO事件,仅仅和它关联上的thread有关系
    // todo 而且,一个eventLoop在他的整个生命周期中,只和唯一的线程进行绑定,
    //
    // todo 当我们注册channel的时候就得确保给他专属它的thread,
    // todo 如果是新的连接到了,
    if (eventLoop.inEventLoop()) {
        // todo 进入regist0()
        register0(promise);
    } else {
        try {
            // todo 如果不是,它以一个任务的形式提交  事件循环 , 新的任务在新的线程开始,  规避了多线程的并发
            // todo 他是SimpleThreadEventExucutor中execute()实现的,把任务添加到执行队列执行
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            safeSetFailure(promise, t);
        }
    }
}

�总结一下一共做了几件事:

  1. 将EventLoop赋值给channel
  2. 判断执行当前代码是不是EventLoop里面那个唯一线程在执行,如果是由它在执行,那就直接执行register0,如果不是就创建一个任务然后 eventLoop.execute执行, 由于EventLoop继承了SingleThreadEventExecutor,最终会调用到SingleThreadEventExecutor.execute方法

image.png
image.png
这个executor.execute具体如下:
image.png
结合上面所有的内容总结:

  1. 利用ThreadPerTaskExecutor创建了一个线程,并启动
  2. 这个线程执行的是SingleThreadEventExecutor.this.run(); 最终又会跑到执行 NioEventLoop的run方法,如图所示:

image.png

  1. 上面其实就是创建了一个线程,然后不断的执行NioEventLoop的run方法
  2. 将传进来的task放到队列中,有点类似于线程池的设计,后续这个NioEventLoop会从对列中取出任务执行

这个流程如图所示:
image.png

NioEventLoop.run (类似于线程池)

image.png
它不断的run,最终会执行到我们往里面提交的register0方法,我们来看register0方法
image.png
具体做了几件事:

  1. 向Selector注册上Jdk层面上的channel:doRegister

image.png

  1. 回调触发之前配置的childHandler:

image.png
回调我们之前设置的Channelnitializer:
image.png
如果说我们配置了image.png,在这一步会加入到Pipline中,具体长这样:
image.png
接下来image.png又往NioEventLoop中提交了一个任务,

完整的流程图如下:
image.png

  1. 通知主线程register0已经完成,主线程会被唤醒做其他事

doBind0: 绑定端口

image.png
这个比较简单,往NioEventLoop 提交一个 channel.bind的task
最终会调用到AbstractChannel.bind方法
image.png
这里主要做了几件事:

  1. 使用nio原生Jdk绑定端口
  2. pipeline.fireChannelActive 会传播事件,它会触发channel的read,最终为已经注册到select的channel绑定感兴趣的Accept事件
  3. 唤醒主线程

doBind0完整的流程图如下:
image.png

完整的启动图

image.png

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

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

相关文章

Linux系统——点菜名

Linux系统可以点菜啦&#xff01; [rootlocalhost ~]#vim menu1.sh #!/bin/bash sum0 PS3"请输入(1-6):" MENU" 宫保鸡丁 酸菜鱼 鱼香肉丝 佛跳墙 水煮肉片 点菜结束 "select menu in $MENU do case $REPLY in 1) echo $menu 价格是20 let sum20 ;; 2) ec…

一个基于electron自动化桌面应用-流程图构建

前期工作已搞定&#xff0c;现在可以搭建桌面应用了。这个阶段可以结合前面定义好的数据格式构建流程图。 模板 还是使用熟悉的技术栈vite react electron&#xff0c;模板 流程图 官方文档 自定义 节点样式 因为配置化的操作类型较多&#xff0c;因此可以利用自定义节…

数据结构和算法笔记5:堆和优先队列

今天来讲一下堆&#xff0c;在网上看到一个很好的文章&#xff0c;不过它实现堆是用Golang写的&#xff0c;我这里打算用C实现一下&#xff1a; Golang: Heap data structure 1. 基本概念 满二叉树&#xff08;二叉树每层节点都是满的&#xff09;&#xff1a; 完全二叉树&a…

JAVA_Set系列集合:HashSet、LinkedHashSet、TreeSet底层详解

先看看 Set 系列集合的位置&#xff1a; Set 系列集合的特点&#xff1a; 无序&#xff1a;存取顺序不一致 如存入张三、李四、王五。而遍历获取到的是李四, 张三, 王五 不重复&#xff1a;可以去除重复无索引&#xff1a;没有带索引的方法&#xff0c;所以不能使用普通for循…

Redis缓存设计与性能优化

文章目录 多级缓存架构缓存设计缓存穿透缓存失效(击穿)缓存雪崩热点缓存key重建优化缓存与数据库双写不一致 开发规范与性能优化一、键值设计1. key名设计2. value设计bigkey的危害&#xff1a;bigkey的产生&#xff1a;如何优化bigkey 二、命令使用三、客户端使用Redis对于过期…

SpringBoot系列之MybatisPlus实现分组查询

SpringBoot系列之MybatisPlus实现分组查询 我之前博主曾记写过一篇介绍SpringBoot2.0项目怎么集成MybatisPlus的教程&#xff0c;不过之前的博客只是介绍了怎么集成&#xff0c;并没有做详细的描述各种业务场景&#xff0c;本篇博客是对之前博客的补充&#xff0c;介绍在mybat…

GitHub 一周热点汇总第7期(2024/01/21-01/27)

GitHub一周热点汇总第7期 (2024/01/21-01/27) &#xff0c;梳理每周热门的GitHub项目&#xff0c;离春节越来越近了&#xff0c;不知道大家都买好回家的票没有&#xff0c;希望大家都能顺利买到票&#xff0c;一起来看看这周的项目吧。 #1 rustdesk 项目名称&#xff1a;rust…

3个精美的wordpress律师网站模板

暗红色WordPress律师事务所网站模板 演示 https://www.zhanyes.com/qiye/23.html 暗橙色WordPress律师网站模板 演示 https://www.zhanyes.com/qiye/18.html 红色WordPress律所网站模板 演示 https://www.zhanyes.com/qiye/22.html

最新国内GPT4.0使用教程,AI绘画-Midjourney绘画V6 ALPHA绘画模型,GPT语音对话使用,DALL-E3文生图+思维导图一站式解决方案

一、前言 ChatGPT3.5、GPT4.0、GPT语音对话、Midjourney绘画&#xff0c;文档对话总结DALL-E3文生图&#xff0c;相信对大家应该不感到陌生吧&#xff1f;简单来说&#xff0c;GPT-4技术比之前的GPT-3.5相对来说更加智能&#xff0c;会根据用户的要求生成多种内容甚至也可以和…

STM32实现软件IIC协议操作OLED显示屏(2)

时间记录&#xff1a;2024/1/27 一、OLED相关介绍 &#xff08;1&#xff09;显示分辨率128*64点阵 &#xff08;2&#xff09;IIC作为从机的地址0x78 &#xff08;3&#xff09;操作步骤&#xff1a;主机先发送IIC起始信号S&#xff0c;然后发送OLED的地址0x78&#xff0c;然…

Unity 光照

光照烘培 光照模式切换为 Baked 或 Mixed&#xff0c;Baked 模式完全使用光照贴图模拟光照&#xff0c;运行时修改光照颜色不生效&#xff0c;Mixed 模式也使用光照贴图&#xff0c;并且进行一些实时运算&#xff0c;运行时修改光照颜色会生效 受光照影响的物体勾选 Contribute…

【RH850U2A芯片】Reset Vector和Interrupt Vector介绍

目录 前言 正文 1. 什么是Reset Vector 1.1 S32K144芯片的Reset Vector 1.2 RH850芯片的Reset Vector 2. 什么是Interrupt Vector 2.1 S32K144芯片的Interrupt Vector 2.2 RH850芯片的Interrupt Vector 3. Reset Vector等价于Interrupt Vector吗 4. 总结 前言 最近在…

MongoDB实战

1.MongoDB介绍 1.1 什么是MongoDB MongoDB是一个文档数据库&#xff08;以JSON 为数据模型&#xff09;&#xff0c;由C语言编写&#xff0c;旨在为WEB应用提供可扩展的高性能数据存储解决方案。 文档来自于"JSON Document"&#xff0c;并非我们一般理解的 PDF&…

【RTP】webrtc 学习3: webrtc对h264的rtp解包

rtp_rtcp\source\video_rtp_depacketizer_h264.cc【RTP】webrtc 学习2: webrtc对h264的rtp打包 中分析了打包过程的代码,这样再来看解析过程的源码就容易多了:本代码主要基于m79,m98类似。解析ParseFuaNalu 第一个字节只取 FNRI第二个字节取 原始的nalu type识别第一个分片…

【机器学习笔记】1 线性回归

回归的概念 二分类问题可以用1和0来表示 线性回归&#xff08;Linear Regression&#xff09;的概念 是一种通过属性的线性组合来进行预测的线性模型&#xff0c;其目的是找到一条直线或者一个平面或者更高维的超平面&#xff0c;使得预测值与真实值之间的误差最小化&#x…

网络安全视野:2024 年的人工智能、弹性和协作

在不断发展的网络安全环境中&#xff0c;确保公司运营安全并保障客户体验是一项复杂而关键的挑战&#xff0c;特别是对于在边缘运营的大型组织而言。当我们展望未来时&#xff0c;必须承认人工智能 (AI) 对网络安全领域的深远影响。本文深入研究了2024 年的预测&#xff0c;将其…

接口自动化测试问题汇总

本篇文章分享几个接口自动化用例编写过程遇到的问题总结&#xff0c;希望能对初次探索接口自动化测试的小伙伴们解决问题上提供一小部分思路。 sql语句内容出现错误 空格&#xff1a;由于有些字段判断是变量&#xff0c;需要将sql拼接起来&#xff0c;但是在拼接字符串时没有…

OpenCV-27 Canny边缘检测

一、概念 Canny边缘检测算法是John F.Canny与1986年开发出来的一个多级边缘检测算法&#xff0c;也被很多人认为是边缘检测的最优算法。最优边缘检测的三个主要评价标准是&#xff1a; 低错频率&#xff1a;表示出尽可能多的实际边缘&#xff0c;同时尽可能的减小噪声产生的误…

【QT+QGIS跨平台编译】之十二:【libpng+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

文件目录 一、libpng介绍二、文件下载三、文件分析四、pro文件五、编译实践一、libpng介绍 PNG(Portable Network Graphics,便携式网络图形),是一种采用无损压缩算法的位图格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。 PNG使用从LZ77派生的无损数据压缩算…

文心一言 VS ChatGPT :谁是更好的选择?

前言 目前各种大模型、人工智能相关内容覆盖了朋友圈已经各种媒体平台&#xff0c;对于Ai目前来看只能说各有千秋。GPT的算法迭代是最先进的&#xff0c;但是它毕竟属于国外产品&#xff0c;有着网络限制、注册限制、会员费高昂等弊端&#xff0c;难以让国内用户享受。文心一言…
最新文章