Netty学习——源码篇3 服务端Bootstrap(一) 备份

1 介绍

        在分析客户端的代码中,已经对Bootstrap启动Netty有了一个大致的认识,接下来在分析服务端时,就会相对简单。先看一下服务端简单的启动代码。

public class ChatServer {
    public void start(int port) throws Exception{
        NioEventLoopGroup boosGroup = new NioEventLoopGroup();
        NioEventLoopGroup workersGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boosGroup,workersGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //业务代码
                            
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG,128)
                    .childOption(ChannelOption.SO_KEEPALIVE,true);
            System.out.println("服务已启动,监听端口是:" + port);
            //绑定端口
            ChannelFuture channelFuture = bootstrap.bind(port).sync();
            //等待服务器socket关闭
            channelFuture.channel().closeFuture().sync();
        }finally {
            workersGroup.shutdownGracefully();
            boosGroup.shutdownGracefully();
            System.out.println("服务已关闭");
        }
    }

    public static void main(String[] args) {
        try {
            new ChatServer().start(8080);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

        服务端基本写法跟客户端相比,差别不大,基本也是进行几个部分的初始化。

        1、Event:无论是客户端还是服务端,都必须指定EventLoopGroup。在上面的代码中,指定了NioEventLoopGroup,表示一个NIO的EventLoopGroup,不过服务端需要指定两个EventLoopGroup,一个是boosGroup,用户处理客户端的连接请求;一个是workerGroup,用于处理与各个客户端连接的I/O操作。

        2、指定Channel的类型。这里是服务端,所以使用了NioServerSocketChannel。

        3、配置自定义的业务处理器Handler。

2、NioServerSocketChannel的创建

        在分析客户端Channel的初始化过程时已经提到,Channel是对Java 底层Socket连接的抽象,并且知道客户端Channel的具体类型是NioSocketChannel,由此可知,服务端Channel的类型就是NioServerSocketChannel。

        在客户端中,Channel类型的指定是在初始化时通过Bootstrap的channel的方法设置的,服务端也是同样的方式。

        再看服务端代码,调用ServerBootstrap的channel(NioServerSocketChannel.class)方法,传入的参数是NioServerSocketChannel对象。可以确定NioServerSocketChannel的实例化是通过ReflectiveChannelFactory工厂类来完成的,而ReflectiveChannelFactory中的clazz属性被赋值为NioServerSocketChannel.class,因此当调用ReflectiveChannelFactory的newChannel方法时,就能获取一个NioServerSocketChannel的实例。newChannel方法的代码如下:

public T newChannel() {
        try {
            return (Channel)this.clazz.newInstance();
        } catch (Throwable var2) {
            throw new ChannelException("Unable to create Channel from class " + this.clazz, var2);
        }
    }

        总结一下。

        1、ServerBootstrap中的ChannelFactory的实现类是 ReflectiveChannelFactory类。

        2、创建Channel具体类型是NioServerSocketChannel。

        Channel的实例化过程,其实就是调用ChannelFactory的newChannel方法,而实例化的Channel具体类型就是初始化ServerBootstrap时传给channel方法的实参。因此,上面代码案例中的服务端ServerBootstrap创建的Channel实例就是NioServerSocketChannel的实例。

3 服务端Channel的实例化

        下面分析NioServerSocketChannel的实例化过程,先看一下NioServerSocketChannel的类层次结构图。

        首先,来看一下NioServerSocketChannel的默认构造器。与NioSocketChannel类似,构造器都是调用newSocket方法来打开一个Java NIO Socket。不过需要注意的是,客户端的newSocket方法调用的是openSocketChannel,而服务端的newSocket调用的是openServerSocketChannel。。顾名思义,一个是客户端的Java SocketChannel,一个是服务端的Java ServerSocketChannel。代码如下:

    private static java.nio.channels.ServerSocketChannel newSocket(SelectorProvider provider) {
        try {
            return provider.openServerSocketChannel();
        } catch (IOException var2) {
            throw new ChannelException("Failed to open a server socket.", var2);
        }
    }

    public NioServerSocketChannel() {
        this(newSocket(DEFAULT_SELECTOR_PROVIDER));
    }

         然后调用重载构造方法,代码如下:

    public NioServerSocketChannel(java.nio.channels.ServerSocketChannel channel) {
        super((Channel)null, channel, 16);
        this.config = new NioServerSocketChannelConfig(this, this.javaChannel().socket());
    }

           在这个构造方法中,调用父类构造方法时传入的参数是SelectionKey.OP_ACCEPT。作为对比回顾一下,在客户端Channel初始化时,传入的参数是SelectionKey.OP_READ。在服务启动后需要监听客户端的连接请求,因此在这里设置SelectionKey.OP_ACCEPT,也就是通知Selector监听客户端的连接请求。

        接下俩,和客户单对比分析,逐级调用父类的构造方法,首先调用NioServerSocketChannel的构造器,其次调用AbstractNioMessageChannel的构造器,最后调用AbstractChannel的构造器。同样的,在AbstractChannel中实例化一个Unsafe和Pipeline,代码如下:

    protected AbstractChannel(Channel parent) {
        this.parent = parent;
        this.id = this.newId();
        this.unsafe = this.newUnsafe();
        this.pipeline = this.newChannelPipeline();
    }

        不过这里需要注意的是,客户端的Unsafe是AbstractNioByteChannel.NioByteUnsafe的实例,而服务端的Unsafe是AbstractNioMessageChannel.AbstractNioUnsafe的实例。 AbstractNioMessageChannel重写了newUnsafe方法,代码如下:

protected AbstractNioChannel.AbstractNioUnsafe newUnsafe() {
        return new NioMessageUnsafe();
    }

     总结一下在NioServerSocketChannel实例化过程中执行的逻辑。        

        1、调用NioServerSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER) 方法创建一个新的Java NIO原生的ServerSocketChannel对象。

        2、实例化AbstractChannel对象并给属性赋值,具体赋值属性如下:

        (1)parent:设置为默认null

        (2)unsafe:通过调用newUnsafe方法,实例化一个Unsafe对象,其类型是AbstractNioMessageChannel.AbstractNioUnsafe内部类。

        (3)pipeline:赋值的是DefaultChannelPipeline的实例。

        3、实例化AbstractNioChannel对象并给属性赋值,具体赋值的属性如下:

        (1)ch:被赋值为Java NIO原生的ServerSocketChannel对象,通过调用NioSERverSocketChannel的newSocket方法获取。

        (2)readInterestOp:被赋值为默认值SelectionKey.OP_ACCEPT。

        (3)ch:被设置为非阻塞,也就是调用ch.configureBlocking(false)方法。

        4、给NioServerSocketChannel对象的config属性赋值为new NioServerSocketChannelConfig(this,javaChannel().socket())。

4 ChannelPipeline初始化与Channel注册到Selector

        与客户端相同,参考:Netty学习——源码篇2 客户端Bootstrap(一)

5 bossGroup与workerGroup

        在客户端初始化的时候,初始化了一个EventLoopGroup对象,而在服务端初始化的时候,设置了两个EventLoopGroup:一个是bossGroup,另一是workerGroup。这两个EventLoopGroup的作用是什么?

        其实,bossGroup只用于服务端的accept,也就是用户处理客户端新连接接入的请求。可以把Netty比做一个饭店,bossGroup就像一个大堂经理,当客户来吃饭时,大堂经理就会引导客户就做。而workerGroup就像实际干活的厨师,客户可以稍作休息,而此时厨师(workerGroup)就开始工作了。bossGroup与workerGroup的关系如下图:

             首先,服务端的bossGroup不断的监听是否有客户端的连接,当发现有一个新的客户端连接到来时,bossGroup就会为此连接初始化各项资源;然后,从workerGroup中选出一个EventLoop绑定到此客户端连接中;接下来,服务端与客户端的交互过程将全部在此分配的EventLoop中完成。

        在ServerBootstrap初始化时调用了bootstrap.group(bossGroup,workerGroup),并设置了两个EventLoopGroup,代码如下:

    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
        super.group(parentGroup);
        if (childGroup == null) {
            throw new NullPointerException("childGroup");
        } else if (this.childGroup != null) {
            throw new IllegalStateException("childGroup set already");
        } else {
            this.childGroup = childGroup;
            return this;
        }
    }

        显然,这个方法初始化了两个属性,一个是group = parentGroup,它是在super.group(parentGroup)中完成初始化的。另一个是this.childGroup = childGroup。接着从应用程序的启动代码看,调用了bootstrap.bind()方法来监听一个本地端口,bind方法会触发如下调用链:

AbstractBootstrap.bind()->AbstractBootstrap.doBind()->initAndRegister()

        代码看到这里,发现对于AbstractBootstrap的initAndRegister方法已经很熟悉了,再来看一下这个方法:

final ChannelFuture initAndRegister() {
        Channel channel = null;

        try {
            channel = this.channelFactory.newChannel();
            this.init(channel);
        } catch (Throwable var3) {
            if (channel != null) {
                channel.unsafe().closeForcibly();
            }

            return (new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE)).setFailure(var3);
        }

        ChannelFuture regFuture = this.config().group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }

        return regFuture;
    }

        这里的group()方法返回的是上面提到的bossGroup,而这里的Channel其实就是NioServerSocketChannel的实例,因此可以猜测group.register(channel)将bossGroup和NioServerSocketChannel关联起来。那么workerGroup具体是在哪里与NioServerSocketChannel关联的呢?继续看init(channel)方法:

void init(Channel channel) throws Exception {
        Map<ChannelOption<?>, Object> options = this.options0();
        synchronized(options) {
            channel.config().setOptions(options);
        }

        Map<AttributeKey<?>, Object> attrs = this.attrs0();
        synchronized(attrs) {
            Iterator i$ = attrs.entrySet().iterator();

            while(true) {
                if (!i$.hasNext()) {
                    break;
                }

                Map.Entry<AttributeKey<?>, Object> e = (Map.Entry)i$.next();
                AttributeKey<Object> key = (AttributeKey)e.getKey();
                channel.attr(key).set(e.getValue());
            }
        }

        ChannelPipeline p = channel.pipeline();
        final EventLoopGroup currentChildGroup = this.childGroup;
        final ChannelHandler currentChildHandler = this.childHandler;
        final Map.Entry[] currentChildOptions;
        synchronized(this.childOptions) {
            currentChildOptions = (Map.Entry[])this.childOptions.entrySet().toArray(newOptionArray(this.childOptions.size()));
        }

        final Map.Entry[] currentChildAttrs;
        synchronized(this.childAttrs) {
            currentChildAttrs = (Map.Entry[])this.childAttrs.entrySet().toArray(newAttrArray(this.childAttrs.size()));
        }

        p.addLast(new ChannelHandler[]{new ChannelInitializer<Channel>() {
            public void initChannel(Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = ServerBootstrap.this.config.handler();
                if (handler != null) {
                    pipeline.addLast(new ChannelHandler[]{handler});
                }

                ch.eventLoop().execute(new Runnable() {
                    public void run() {
                        pipeline.addLast(new ChannelHandler[]{new ServerBootstrapAcceptor(currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)});
                    }
                });
            }
        }});
    }

        实际上,init()方法在ServerBootstrap中被重写了,从上面的代码中看到,它为Pipeline添加了一个ChannelInitializer,而这个ChannelInitializer中添加了一个非常关键的ServerBootstrapAccept的Handler。先来关注ServerBootstrapAcceptor类。在ServerBootstrapAcceptor中重写了channelRead()方法,其代码如下:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel)msg;
            child.pipeline().addLast(new ChannelHandler[]{this.childHandler});
            Map.Entry[] arr$ = this.childOptions;
            int len$ = arr$.length;

            int i$;
            Map.Entry e;
            for(i$ = 0; i$ < len$; ++i$) {
                e = arr$[i$];

                try {
                    if (!child.config().setOption((ChannelOption)e.getKey(), e.getValue())) {
                        ServerBootstrap.logger.warn("Unknown channel option: " + e);
                    }
                } catch (Throwable var10) {
                    ServerBootstrap.logger.warn("Failed to set a channel option: " + child, var10);
                }
            }

            arr$ = this.childAttrs;
            len$ = arr$.length;

            for(i$ = 0; i$ < len$; ++i$) {
                e = arr$[i$];
                child.attr((AttributeKey)e.getKey()).set(e.getValue());
            }

            try {
                this.childGroup.register(child).addListener(new ChannelFutureListener() {
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            ServerBootstrap.ServerBootstrapAcceptor.forceClose(child, future.cause());
                        }

                    }
                });
            } catch (Throwable var9) {
                forceClose(child, var9);
            }

        }

        ServerBootstrapAcceptor中的childGroup是构造此对象时传入的currentChildGroup,也就是workerGroup对象。而这里的Channel是NioSocketChannel的实例,因此childGroup的register方法就是将workerGroup中的某个EventLoop和NioSocketChannel关联。那么,ServerBootstrapAcceptor的channelRead()方法是在哪里被调用的呢?其实当一个Client连接到Server时,Java 底层 NIO的ServerSocketChannel就会有一个SelectionKe.OP_ACCEPT的事件就绪,接着会调用NioServerSocketChannel的doReadMessage方法,代码如下:

protected int doReadMessages(List<Object> buf) throws Exception {
        SocketChannel ch = this.javaChannel().accept();

        try {
            if (ch != null) {
                buf.add(new NioSocketChannel(this, ch));
                return 1;
            }
        } catch (Throwable var6) {
            logger.warn("Failed to create a new channel from an accepted socket.", var6);

            try {
                ch.close();
            } catch (Throwable var5) {
                logger.warn("Failed to close a socket.", var5);
            }
        }

        return 0;
    }

        在doReadMessage方法中,通过调用javaChannel().accept方法获取客户端新连接的SocketChannel对象,紧接着实例化一个NioSocketChannel,并且传入NioServerSocketChannel对象。由此可知,创建的NioSocketChannel的父类Channel就是NioServerSocketChannel实例。接下来利用Netty的ChannelPipeline机制,将读取时间逐级发送给各个Handler中,于是就会触发ServerBootstrapAccept的channelRead()方法。

6 服务端Selector事件轮询

        回到服务端ServerBootstrap的启动代码,它是从bind方法开始。ServerBootstrap的bind方法实际上就是其父类AbstractBootstrap的bind方法,来看代码:

private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {

        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (regFuture.isSuccess()) {
                    channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
                } else {
                    promise.setFailure(regFuture.cause());
                }
            }
        });
    }

        在doBind0方法中,调用EventLoop的execute方法,代码如下:

    public void execute(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }

        boolean inEventLoop = inEventLoop();
        if (inEventLoop) {
            addTask(task);
        } else {
            startThread();
            addTask(task);
            if (isShutdown() && removeTask(task)) {
                reject();
            }
        }

        if (!addTaskWakesUp && wakesUpForTask(task)) {
            wakeup(inEventLoop);
        }
    }

        execute()方法主要就是创建线程,将线程添加到EventLoop的无锁化串行任务队列。重点关注startThread()方法,代码如下:

private void startThread() {
        if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                doStartThread();
            }
        }
    }

        发现startThread方法最终调用的就是SingleThreadEventExecutor.this.run()方法,这个this就是NioEventLoop对象,代码如下:

protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));

                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                    default:
                        // fallthrough
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        runAllTasks();
                    }
                } else {
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        final long ioTime = System.nanoTime() - ioStartTime;
                        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
            // Always handle shutdown even if the loop processing threw an exception.
            try {
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }

        上面代码主要就是用一个死循环不断地轮询SelectionKey。select()方法主要用来解决JDK空轮询bug,而processSelectedKeys就是针对不同的轮询事件进行处理。如果客户端有数据写入,最终也会调用AbstractNioMessageChannel的doReadMessages方法。下面总结一下Selector的轮询流程。

        1、Selector事件轮询是从EventLoop的execute方法开始的。

        2、在EventLoop 的execute方法中,会为每一个任务都创建一个独立的线程,并保存到无锁化串行任务队列。

        3、线程任务队列的每个任务实际调用的是NioEventLoop的run方法。

        4、在run方法中调用processSelectedKeys处理轮询事件。

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

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

相关文章

解锁鸿蒙小程序开发新姿势

如今&#xff0c;鸿蒙开发日益受到广大开发者的关注&#xff0c;而小程序开发也早已成为互联网领域的热门话题。那么&#xff0c;我们不禁要问&#xff1a;是否有可能将这两者融为一体&#xff0c;将小程序开发的便捷与高效带入鸿蒙生态中呢&#xff1f;本文将首先带你回顾小程…

SpringCloud alibaba入门简介

SpringCloud alibaba入门简介 1、简介 SpringCloud alibaba官网&#xff1a;SpringCloudAlibaba | Spring Cloud Alibaba (aliyun.com) Spring官网&#xff1a;Spring Cloud Alibaba GitHub中文文档&#xff1a;spring-cloud-alibaba/README-zh.md at 2022.x alibaba/spri…

数据库基本介绍及编译安装mysql

目录 数据库介绍 数据库类型 数据库管理系统&#xff08;DBMS&#xff09; 数据库系统 DBMS的工作模式 关系型数据库的优缺点 编译安装mysql 数据库介绍 数据&#xff1a;描述事物的的符号纪录称为数据&#xff08;Data&#xff09; 表&#xff1a;以行和列的形式组成…

公众号怎么更换主体

公众号账号迁移的作用是什么&#xff1f;只能变更主体吗&#xff1f;1.可合并多个公众号的粉丝、文章&#xff0c;打造超级大V2.可变更公众号主体&#xff0c;更改公众号名称&#xff0c;变更公众号类型——订阅号、服务号随意切换3.可以增加留言功能4.个人订阅号可迁移到企业名…

零知识玩转AVH(8)—— 门槛任务(3)所遇错误及解决(2)

接前一篇文章&#xff1a;零知识玩转AVH&#xff08;7&#xff09;—— 门槛任务&#xff08;2&#xff09;所遇错误及解决&#xff08;1&#xff09; 上一回说到在尝试完成门槛任务 https://github.com/ArmDeveloperEcosystem/Paddle-examples-for-AVH &#xff08;推荐&#…

阿里G6 树状图使用 Iconfont

官网&#xff1a;使用 Iconfont | G6 效果&#xff1a; 完整代码&#xff1a;index.html: <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width…

Python矩阵计算

文章目录 求积求逆最小二乘法特征值 Python科学计算&#xff1a;数组&#x1f4af;数据生成&#x1f4af;数据交互&#x1f4af;微积分&#x1f4af;插值&#x1f4af;拟合&#x1f4af;FFT&#x1f4af;卷积&#x1f4af;滤波&#x1f4af;统计 求积 矩阵是线性代数的核心对…

开发CodeSys可视化控件

文章目录 背景解决方案HTML5 elementsUsing a Visualization as an Element 背景 目前接公司需求&#xff0c;需要开发一套视觉检测系统&#xff0c;并将其集成到codesys中。 编程端基本是采用之前说得的C接口来实现【CodeSys中调用C语言写的动态库】&#xff0c;但是检测画面…

算法笔记p251队列循环队列

目录 队列循环队列循环队列的定义初始化判空判满入队出队获取队列内元素的个数取队首元素取队尾元素 队列 队列是一种先进先出的数据结构&#xff0c;总是从队尾加入元素&#xff0c;从队首移除元素&#xff0c;满足先进先出的原则。队列的常用操作包括获取队列内元素的个数&a…

打造精美响应式CSS日历:从基础到高级样式

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

【09】进阶JavaScript事件循环Promise

一、事件循环 浏览器的进程模型 何为进程? 程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为进程 每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。 何为线程? 有了进程后,就可以运行程序的代码了。 运行代码的「人」称之…

Makefile的基本知识

文章目录 一、使用Makefile 的引入1.GCC的编译流程2.Makefile的引入 二、Makefile的语法规则三、Makefile中的变量1.全局变量2.赋值符“”&#xff0c;“&#xff1a;”&#xff0c;“&#xff1f;”区别 四、Makefile中的自动化变量四、Makefile中伪目标五、Makefile中条件判断…

安防监控视频汇聚平台EasyCVR接入海康Ehome设备,设备在线但视频无法播放是什么原因?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…

Elastic 线下 Meetup 将于 2024 年 3 月 30 号在武汉举办

2024 Elastic Meetup 武汉站活动&#xff0c;由 Elastic、腾讯、新智锦绣联合举办&#xff0c;现诚邀广大技术爱好者及开发者参加。 活动时间 2024年3月30日 13:30-18:00 活动地点 中国武汉 武汉市江夏区腾讯大道1号腾讯武汉研发中心一楼多功能厅 13:30-14:00 入场 活动流程…

微信小程序获取手机号(Java后端)

最近在做小程序后端的时候&#xff0c;需要拿到手机号进行角色校验&#xff0c;小白也是第一次获取小程序的手机号&#xff0c;所以功能完毕后总结一下本次操作咯。 根据微信小程序官方文档&#xff1a;获取手机号 | 微信开放文档 调用的接口是getPhoneNumber 请求参数 从伤处…

C语言数据结构-二叉树基础练习

繁霜尽是心头血 洒向千峰秋叶丹 目录 二叉树最大的深度 思路 代码展示 单值二叉树 思路 代码展示 相同的树 思路 代码展示 对称二叉树 思路 代码展示 另一颗树的子树 思路 代码展示 二叉树最大的深度 题目链接&#xff1a;二叉树最大的深度 给定一个二叉树 root &#xff0…

osgEarth学习笔记3-第二个Osg QT程序

原文链接 打开QT Creator&#xff0c;新建一个窗口项目。 QT版本如下&#xff1a; 修改pro文件 QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 DEFINES QT_DEPRECATED_WARNINGS SOURCES \main.cpp \mainwindow.cpp HEADERS \mainwindow…

释放创造力,Nik Collection 6 by DxO 点亮你的视觉世界

在数字摄影时代&#xff0c;后期处理是提升摄影作品品质的重要环节。而Nik Collection 6 by DxO作为一套优秀的滤镜插件套装&#xff0c;不仅为摄影师提供了丰富的后期处理工具&#xff0c;更让他们能够释放无限的创造力&#xff0c;打造出惊艳的视觉作品。 Nik Collection 6 …

Unity定时播放音乐

一、需求 需要定时在早上8:50&#xff0c;中午12:00&#xff0c;下午13:10定时播放音乐 二、实现步骤 依次在unity创建背景图、主文字提示、时间文字提示、音量控制器及音量文字提示、退出按钮、播放按钮&#xff0c;暂停按钮 在Canvas下创建一个Script脚本&#xff1a;获取…

路由器里如何设置端口映射?

在互联网时代&#xff0c;我们经常需要将内部网络的服务暴露到公网以便其他人访问。直接将内部网络暴露在公网上存在一定的安全风险。为了解决这个问题&#xff0c;我们可以利用路由器里设置端口映射来实现将特定端口的访问请求转发到内部网络的特定设备上。 端口映射的原理 端…
最新文章