Netty小白入门教程

news2024/11/25 20:22:01

一、概述

1.1 概念

Netty是一个异步的基于事件驱动(即多路复用技术)的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端。

1.2 地位

Netty在Java网络应用框架中的地位就好比,Spring框架在JavaEE开发中的地位。

以下的框架都使用了Netty,因为他们有网络通信需求。

  • Cassandra:非关系型数据库
  • Spark:大数据分布式计算框架
  • Hadoop:大数据分布式存储框架
  • RocketMQ:阿里开源的消息队列
  • ElasticSearch:搜索引擎
  • gRPC:RPC框架
  • Dubbo:RPC框架
  • Spring 5.x:flux api完全抛弃了tomcat,使用netty作为服务器端
  • Zookeeper:分布式协调框架

1.3 优势

  • Netty同样是基于java nio开发。如果自己使用nio开发,工作量大,bug 多,这是因为Netty已经做好了基础部分
    • 构建协议
    • 解决 TCP 传输问题,如粘包、半包
    • Linux多路复用的底层是epoll,会存在空轮询导致 CPU 100%(对应nio中Linux下不阻塞),Netty兼容并解决该问题
    • 对 API 进行增强,使之更易用,如 ThreadLocal => FastThreadLocal ,ByteBuffer => ByteBuf
  • Netty vs 其它网络应用框架
    • Mina 由 apache 维护,将来 3.x 版本可能会有较大重构,破坏 API 向下兼容性,Netty 的开发迭代更迅速,API 更简洁、文档更优秀
    • 久经考验,Netty 版本
      • 2.x 2004
      • 3.x 2008
      • 4.x 2013
      • 5.x 已废弃(使用了AIO,但是Linux的是伪AIO,只有Win真正实现了AIO。实际没有明显的性能提升,却导致维护成本高)

二、入门

2.1 需求

首先需要引入依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.39.Final</version>
</dependency>

使用Netty开发一个简单的服务器端和客户端

  • 客户端向服务器端发送 hello, world
  • 服务器仅接收,不返回

2.2 实现

服务端

public class HelloServer {
    public static void main(String[] args) {
        new ServerBootstrap()//启动器,负责组装netty组件,协调工作
                //一个selector和一个thread就叫EventLoop。EventLoopGroup里面,既有Boss处理连接,也有Worker处理读写
                .group(new NioEventLoopGroup())
                //选择服务器netty的ServerSocketChannel具体实现,有4种自行看源码
                .channel(NioServerSocketChannel.class)
                .childHandler(//child即Worker,负责读写。决定了Worker能执行哪些操作(Handler)
                        new ChannelInitializer<NioSocketChannel>() {//代表和客户端进行数据读写的通道,主要负责在initChannel里面添加其他的handler
                            @Override
                            protected void initChannel(NioSocketChannel channel) throws Exception {
                                //添加具体的handler
                                channel.pipeline().addLast(new StringDecoder());//将netty的ByteBuf转换为字符串
                                channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {//自定义handler
                                    @Override
                                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                        //此时这个msg就是上一步StringDecoder解码后的结果
                                        System.out.println(msg);
                                    }
                                });
                            }
                        })
                //监听端口
                .bind(8080);
    }
}

客户端

public class HelloClient {

    public static void main(String[] args) throws Exception {
        //创建启动器
        new Bootstrap()
                //添加EventLoop
                .group(new NioEventLoopGroup())
                //选择客户端channel实现
                .channel(NioSocketChannel.class)
                //添加处理器,连接建立后该初始化器被调用
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override//在连接建立后(accept)被调用
                    protected void initChannel(NioSocketChannel channel) throws Exception {
                        channel.pipeline()
                                .addLast(new StringEncoder());
                    }
                })
                //连接到服务器
                .connect("localhost", 8080)
                //阻塞方法,直到连接建立
                .sync()
                //代表着连接对象
                .channel()
                //向服务器发送数据
                .writeAndFlush("hello world");
    }
}

针对上述代码对Netty的流程理解

  • channel 是数据的通道,与 jdk nio 中 channel 作用一致
  • msg 是流动的数据。输入是 ByteBuf ,输出也是 ByteBuf 。但是中间会经过 pipeline 加工,变成其他的类型对象。
  • handler是数据的处理工序
    • 工序有多道,合在一起就是 pipeline 。 pipeline 负责发布事件(读、读完成等)传播给各个 handler , handler 对自己感兴趣的事件进行处理
    • handler分为Inbound(数据输入时走入站handler)和Outbound(数据输出时走出站handler)两类
  • eventLoop(底层就是一个线程)是处理数据的工人
    • 工人可以管理多个 channel 的 io 操作。并且工人和 channel 针对io操作是绑定的(这也是从线程安全的角度考虑,如果一个 channel 可以被多个线程管理,就会存在多个线程一起读写的情况,防止出问题可能还要做串行操作)
    • 工人既可以执行 io 操作,也可以进行任务的处理。每位工人有任务队列,队列里可以存储该工人绑定的多个 channel 的待处理任务,任务分为普通任务、定时任务
    • 工人按照 pipeline 顺序,依次按照 handler 的代码处理数据,可以为每道工序指定不同的工人(只适用非io操作)

三、组件

3.1 EventLoop

EventLoop,事件循环对象

EventLoop 本质是一个单线程执行器(同时维护了一个 Selector),里面有 run 方法处理 Channel 上源源不断的 io 事件。

它的继承关系比较复杂

  • 一条线是继承自 java.util.concurrent.ScheduledExecutorService 因此包含了线程池中所有的方法

  • 另一条线是继承自io.netty.util.concurrent.OrderedEventExecutor

    • 提供了 boolean inEventLoop() 方法判断当前执行的线程是否属于此EventLoop

    • 提供了 boolean inEventLoop(Thread thread) 方法判断指定线程是否属于此 EventLoop

    • 提供了 parent 方法来看看自己属于哪个 EventLoopGroup

一般我们不会直接使用EventLoop,而是使用EventLoopGroup

3.2 EventLoopGroup

EventLoopGroup,事件循环组

EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)

  • 继承自 io.netty.util.concurrent.EventExecutorGroup
    • 实现了 Iterable 接口提供遍历 EventLoop 的能力
    • 另有 next 方法获取集合中下一个 EventLoop, next 底层是轮询

使用示例

@Slf4j
public class TestEventLoop {

    public static void main(String[] args) {
        //1. 创建事件循环组。默认线程数跟下源码, 什么都不配置, 默认是cpu线程*2
        NioEventLoopGroup group = new NioEventLoopGroup(2);//可以执行io事件、普通任务、定时任务
        //new DefaultEventLoopGroup();//执行普通任务、定时任务
        //2. 获取下一个事件循环对象,这个next底层就是轮询
        System.out.println(group.next());
        System.out.println(group.next());
        System.out.println(group.next());

        //3. 执行普通任务, 在netty中的意义就是执行一些比较耗时的任务
        group.next().execute(() -> {
            log.info("普通任务");
        });

        //4. 执行定时任务。用于keepalive时连接的保活
        group.next().scheduleAtFixedRate(() -> {
            log.info("定时任务");
        }, 0L, 2L, TimeUnit.SECONDS);
    }
}

3.2.1 完善2.1

将2.1的需求,进一步完善,代码如下。

服务端

@Slf4j
public class EventLoopServer {

    public static void main(String[] args) {
        /**
         * 细分2: 如果处理io的操作比较耗时, 这时候是不应该让nioEventLoop阻塞在那里,否则会影响后续其他channel的读写
         * 所以创建一个独立的EventLoopGroup用来执行那些耗时操作
         */
        DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup(2, new ThreadFactory() {
            private final AtomicInteger atomicInteger = new AtomicInteger(0);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "calc-" + atomicInteger.incrementAndGet());
            }
        });
        new ServerBootstrap()
                //.group(new NioEventLoopGroup())
                //细分1: netty建议将group划分的更细, 划分为parent和child。parent负责accept, child负责read and write
                .group(new NioEventLoopGroup(1, new ThreadFactory() {
                    private final AtomicInteger atomicInteger = new AtomicInteger(0);

                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "boss-" + atomicInteger.incrementAndGet());
                    }
                }), new NioEventLoopGroup(2, new ThreadFactory() {

                    private final AtomicInteger atomicInteger = new AtomicInteger(0);

                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "worker-" + atomicInteger.incrementAndGet());
                    }
                }))
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline()
                                .addLast("handler1", new ChannelInboundHandlerAdapter() {
                                    @Override//没有编解码,那就是ByteBuf
                                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                        ByteBuf buf = (ByteBuf) msg;
                                        log.info(buf.toString(Charset.defaultCharset()) + "_" + ctx.channel().remoteAddress());
                                        
                                        ctx.fireChannelRead(msg);//将消息传递给下一个handler
                                        //上行做法,通过直接调用父级该方法,一样可以实现往下传递
                                        //super.channelRead(ctx, msg);
                                    }
                                })
                                .addLast(defaultEventLoopGroup, "handler2", new ChannelInboundHandlerAdapter() {
                                    @Override
                                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                        ByteBuf buf = (ByteBuf) msg;
                                        TimeUnit.SECONDS.sleep(2L);
                                        log.info(buf.toString(Charset.defaultCharset()) + "_" + ctx.channel().remoteAddress());
                                    }
                                });
                    }
                })
                .bind(8080);
    }
}

客户端

public class EventLoopClient {

    public static void main(String[] args) throws Exception {
        ChannelFuture future = new Bootstrap()//创建启动器
                //添加EventLoop
                .group(new NioEventLoopGroup())
                //选择客户端channel实现
                .channel(NioSocketChannel.class)
                //添加处理器,连接建立后该初始化器被调用
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override//在连接建立后(accept)被调用
                    protected void initChannel(NioSocketChannel channel) throws Exception {
                        channel.pipeline()
                                .addLast(new StringEncoder());
                    }
                })
                //连接到服务器
                .connect("localhost", 8080);
        Channel channel = future
                //阻塞方法,直到连接建立
                .sync()
                .channel();//代表着连接对象
        while (true) {
            System.in.read();
            channel.writeAndFlush("hello world");
            //channel.write("hello world");
        }
    }
}

建立三个Client,每个Client发送一次消息。运行结果如图

由上图可知,channel第一次创建时,就与线程绑定了,不管是处理读写的worker,还是处理耗时的calc,都是绑定的。

h1与h2对应服务端的handler1与handler2

至于head与tail,后面的会提到

3.2.2 切换线程原理

查看源码io.netty.channel.AbstractChannelHandlerContext中的invokeChannelRead

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    // 返回下一个handler的eventLoop,这里是使用了多态的写法
    EventExecutor executor = next.executor();
    //判断当前handler的线程,是否和下一个handler的eventLoop是同一个线程
    //如果是,直接调用;否则, 将要处理的消息提交给下一个eventLoop
    if (executor.inEventLoop()) {
        next.invokeChannelRead(m);
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}

3.3 Channel

channel 的主要作用

  • close() 可以用来异步关闭 channel
  • closeFuture() 用来执行 channel 关闭后的善后操作
    • sync 方法作用是同步等待 channel 关闭
    • 而 addListener 方法是异步等待 channel 关闭
  • pipeline() 方法添加处理器
  • write() 方法将数据写到缓冲区,但并不是立即写出
    • 可以直接将3.2.1的代码,修改成write方法,尝试即可
  • writeAndFlush() 方法立即将数据写出

3.3.1 ChannelFuture

保证获取到的channel是成功连接后的,两种方式

  1. sync 阻塞,本线程阻塞,直到channel成功建立连接
  2. addListener(回调对象) 添加回调,其他线程执行,channel监听到连接成功后执行回调对象

服务端代码保持不变,客户端代码修改

@Slf4j
public class EventLoopClient {

    public static void main(String[] args) throws Exception {
        //带有Future、Promise的都是和异步方法配套使用,目的是提高效率且正确处理结果
        ChannelFuture future = new Bootstrap()//创建启动器
                //添加EventLoop
                .group(new NioEventLoopGroup())
                //选择客户端channel实现
                .channel(NioSocketChannel.class)
                //添加处理器,连接建立后该初始化器被调用
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override//在连接建立后(accept)被调用
                    protected void initChannel(NioSocketChannel channel) throws Exception {
                        channel.pipeline()
                                .addLast(new StringEncoder());
                    }
                })
                //连接到服务器
                /**
                 * 异步非阻塞
                 * 异步:我(当前线程)只负责发起连接,至于连接后的结果,让别人(NioEventLoopGroup中线程)取
                 * 非阻塞:不等待直接下一步
                 */
                .connect("localhost", 8080);

        //方法一:使用sync阻塞方法,直到连接建立
        //future.sync();
        //直接获取channel,其实获取到的是个尚未连接的channel
        //Channel channel = future.channel();//代表着连接对象
        //for (int i = 0; i < 2; i++) {
        //    if (channel.isActive()) {
        //        log.info("已连接");
        //        channel.writeAndFlush("hello world");
        //    } else {
        //        log.info("未连接");
        //    }
        //    TimeUnit.SECONDS.sleep(1);
        //}

        //方法二:使用addListener(回调对象)方法异步处理结果
        future.addListener(new ChannelFutureListener() {
            @Override//在nio线程连接建立好之后,会调用operationComplete
            public void operationComplete(ChannelFuture future) throws Exception {
                Channel channel = future.channel();
                for (int i = 0; i < 2; i++) {
                    if (channel.isActive()) {
                        log.info("已连接");
                        channel.writeAndFlush("hello world");
                    } else {
                        log.info("未连接");
                    }
                    TimeUnit.SECONDS.sleep(1);
                }
            }
        });
    }
}

3.3.2 CloseFuture

Channel通过 closeFuture() 来进行善后操作

  • sync 方法作用是同步等待 channel 关闭
  • 而 addListener 方法是异步等待 channel 关闭
@Slf4j
public class CloseFutureClient {

    public static void main(String[] args) throws Exception {
        NioEventLoopGroup group = new NioEventLoopGroup();
        ChannelFuture future = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline()
                                .addLast(new LoggingHandler(LogLevel.DEBUG))
                                .addLast(new StringEncoder());
                    }
                })
                .connect("localhost", 8080);
        Channel channel = future.sync().channel();
        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String line = scanner.nextLine();
                if ("q".equals(line)) {
                    //close是个异步方法, 通过添加LoggingHandler可以监视关闭的线程
                    channel.close();
                    log.info("调用close方法");
                    break;
                }
                channel.writeAndFlush(line);
            }
        }, "input").start();

        //方式一:sync
        //ChannelFuture closeFuture = channel.closeFuture();
        //closeFuture.sync();
        //log.info("成功关闭");
        //group.shutdownGracefully();

        //方式二:监听
        channel.closeFuture().addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                log.info("成功关闭后的回调");
                group.shutdownGracefully();
            }
        });
    }
}

Netty可以通过LoggingHandler打印日志,直观的查看Channel连接、收发、断开的过程

3.3.3 为何异步

疑问:为什么不在一个线程中去执行建立连接、去执行关闭 channel,那样不是也可以吗?非要用这么复杂的异步方式:比如一个线程发起建立连接,另一个线程去真正建立连接;一个线程去关闭连接,另一个线程真正去关闭连接。

这个问题也很简单,就比如多路复用的做法。我只有4个线程来发起长连接,如果一线程一长连接那种,撑死只能建立4个长连接。但是使用多路复用技术,就能处理更多的长连接了。这也就是Netty异步的核心思想了。

3.4 Future&&Promise

3.4.1 比较

在异步处理时,经常用到这 Future Promise 两个接口

首先要说明 netty 中的 Future 与 jdk 中的 Future 同名,但是是两个接口,netty 的 Future 继承自 jdk 的 Future,而 Promise 又对 netty Future 进行了扩展

  • jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果。比如get方法,就是只能同步等待获取结果。
  • netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果(比如CloseFuture的addListener),但都是要等任务结束
  • netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
功能/名称jdk Futurenetty FuturePromise
cancel取消任务--
isCanceled任务是否取消--
isDone任务是否完成,不能区分成功失败--
get获取任务结果,阻塞等待--
getNow-获取任务结果,非阻塞,还未产生结果时返回 null-
await-等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断-
sync-等待任务结束,如果任务失败,抛出异常-
isSuccess-判断任务是否成功-
cause-获取失败信息,非阻塞,如果没有失败,返回null-
addLinstener-添加回调,异步接收结果-
setSuccess--设置成功结果
setFailure--设置失败结果

3.4.2 示例

jdk Future

@Slf4j
public class TestJDKFuture {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future<Integer> future = executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.info("calc..");
                TimeUnit.SECONDS.sleep(2);
                return ThreadLocalRandom.current().nextInt(1, 10);
            }
        });
        log.info("waiting..");
        Integer integer = future.get();
        log.info("received..{}", integer);
    }
}

netty Future

@Slf4j
public class TestNettyFuture {

    public static void main(String[] args) throws Exception {
        NioEventLoopGroup group = new NioEventLoopGroup(1);
        EventLoop eventLoop = group.next();

        Future<Integer> future = eventLoop.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.info("calc..");
                TimeUnit.SECONDS.sleep(2);
                return ThreadLocalRandom.current().nextInt(1, 10);
            }
        });

        //方式一:同步方式
        //log.info("waiting..");
        //Integer integer = future.get();
        //log.info("received..{}", integer);

        //方式二:异步方式
        future.addListener(new GenericFutureListener<Future<Integer>>() {
            @Override
            public void operationComplete(Future<Integer> future) throws Exception {
                log.info("waiting..");
                Integer integer = future.getNow();
                log.info("received..{}", integer);
            }
        });
    }
}

netty Promise

@Slf4j
public class TestNettyPromise {

    public static void main(String[] args) throws Exception {
        EventLoop eventLoop = new NioEventLoopGroup(1).next();
        //与future不同的是,可以主动创建promise对象。而不用像future一样通过提交任务获取对象。
        //结果容器
        Promise<Integer> promise = new DefaultPromise<>(eventLoop);

        new Thread(() -> {
            log.info("calc..");
            try {
                TimeUnit.SECONDS.sleep(2L);
                //int i = 2 / 0;
                //线程执行完毕后,向promise填充结果
                promise.setSuccess(ThreadLocalRandom.current().nextInt(1, 10));
            } catch (InterruptedException e) {
                e.printStackTrace();
                promise.setFailure(e);
            }

        }).start();

        //接收结果
        log.info("waiting..");
        Integer integer = promise.get();
        log.info("received..{}", integer);
    }
}

3.5 Handler&&Pipeline

3.5.1 Handler在Pipeline中执行顺序

ChannelHandler 用来处理 Channel 上的各种事件,分为入站、出站两种。所有 ChannelHandler 被连成一串,就是 Pipeline

  • 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据、写回结果
  • 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工

打个比喻,每个 Channel 是一个产品的加工车间,Pipeline 是车间中的流水线,ChannelHandler 就是流水线上的各道工序,而后面要讲的 ByteBuf 是原材料,经过很多工序的加工:先经过一道道入站工序,再经过一道道出站工序最终变成产品

@Slf4j
public class TestPipeline {

    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        //通过channel拿到pipeline
                        ChannelPipeline pipeline = ch.pipeline();
                        //添加处理器。netty会自动添加两个handler分别为head和tail
                        //addLast并不是加到最后,而是加到tail之前
                        //channel的执行流程:head->In_1->In_2->In_3->Out_4->Out_5->Out_6->tail
                        pipeline.addLast("In_1", new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.info("In_1");
                                super.channelRead(ctx, msg);//唤醒下一个入站处理器
                            }
                        });
                        pipeline.addLast("In_2", new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.info("In_2");
                                super.channelRead(ctx, msg);//唤醒下一个入站处理器
                            }
                        });
                        pipeline.addLast("In_3", new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.info("In_3");
                                super.channelRead(ctx, msg);//唤醒下一个入站处理器。此处已经结尾了,所以无所谓。
                                //ctx.writeAndFlush("hello world");//表示从当前处理器往前找 写出 处理器
                                ctx.channel().writeAndFlush("hello world");//表示从channel的tail往前找 写出 处理器
                            }
                        });


                        pipeline.addLast("Out_4", new ChannelOutboundHandlerAdapter() {
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.info("Out_4");
                                super.write(ctx, msg, promise);
                            }
                        });
                        pipeline.addLast("Out_5", new ChannelOutboundHandlerAdapter() {
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.info("Out_5");
                                super.write(ctx, msg, promise);
                            }
                        });
                        pipeline.addLast("Out_6", new ChannelOutboundHandlerAdapter() {
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.info("Out_6");
                                super.write(ctx, msg, promise);
                            }
                        });

                    }
                })
                .bind(8080);
    }
}

@Slf4j
class TestPipelineClient {
    public static void main(String[] args) throws Exception {
        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline()
                                .addLast(new StringEncoder())
                                .addLast(new StringDecoder())
                                .addLast(new ChannelInboundHandlerAdapter() {
                                    @Override
                                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                        log.info("received==>{}", msg);
                                    }
                                })
                                .addLast(new ChannelOutboundHandlerAdapter() {
                                    @Override
                                    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                        log.info("writed==>{}", msg);
                                        super.write(ctx, msg, promise);
                                    }
                                });
                    }
                })
                .connect("localhost", 8080)
                .sync().channel();

        while (true) {
            System.in.read();
            channel.writeAndFlush("hello world");
        }
    }
}

可以看到,ChannelInboundHandlerAdapter 是按照 addLast 的顺序执行的,而 ChannelOutboundHandlerAdapter 是按照 addLast 的逆序执行的。

ChannelPipeline 的实现是一个 ChannelHandlerContext(包装了 ChannelHandler) 组成的双向链表

3.5.2 更方便地测试Handler执行顺序

@Slf4j
public class TestEmbeddedChannel {

    public static void main(String[] args) {
        ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.info("h1");
                super.channelRead(ctx, msg);
            }
        };

        ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.info("h2");
                super.channelRead(ctx, msg);
            }
        };


        ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter() {
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.info("h3");
                super.write(ctx, msg, promise);
            }
        };

        ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter() {
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.info("h4");
                super.write(ctx, msg, promise);
            }
        };


        EmbeddedChannel embeddedChannel = new EmbeddedChannel(h1, h2, h3, h4);
        //测试入站
        log.info("测试入站");
        embeddedChannel.writeInbound("inbound");
        Object o = embeddedChannel.readInbound();
        log.info("测试出站");
        embeddedChannel.writeOutbound("outbound");
        Object o1 = embeddedChannel.readOutbound();
    }
}

3.6 ByteBuf

3.6.1 优势

io.netty.buffer.ByteBuf是对java.nio.ByteBuffer的增强。

  1. 支持动态扩容。最大容量不超过Integer最大值。
  2. 池化思想。对直接内存影响最大,保证享受了直接内存的高读写的同时,又能有效避免重复开辟内存造成的性能损失。
  3. 读写指针分离。内部使用两套指针,标识读和写。与ByteBuffer相比,就能减少不必要的来回切换。
  4. 零拷贝。比如slice/duplicate/compositeByteBuf
  5. 方便开发者高效编写。比如链式调用。

3.6.2 组成

ByteBuf 由四部分组成

  1. 废弃字节
  2. 可读字节
  3. 可写字节
  4. 可扩容字节

该组成结构,使得ByteBuf在使用上,比ByteBuffer(如下图所示)方便许多,因为节省了人为频繁切换指针位置的操作。

3.6.3 使用

池化 VS 非池化

池化的最大意义在于可以重用 ByteBuf,优点有

  • 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
  • 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
  • 高并发时,池化功能更节约内存,减少内存溢出的可能

池化功能是否开启,可以通过下面的系统环境变量来设置

-Dio.netty.allocator.type={unpooled|pooled}
  • 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现
  • 4.1 之前,池化功能还不成熟,默认是非池化实现

直接内存 VS 堆内存

可以使用下面的代码来创建池化基于堆内存的 ByteBuf

ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);

也可以使用下面的代码来创建池化基于直接内存的 ByteBuf

ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);

直接内存 与 堆内存 的比较

  • 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
  • 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放

调试工具类

首先创建一个调试工具类

import io.netty.buffer.ByteBuf;

import static io.netty.buffer.ByteBufUtil.appendPrettyHexDump;
import static io.netty.util.internal.StringUtil.NEWLINE;

public class DebugByteBuf {

    public static void log(ByteBuf buffer) {
        log(buffer, false);
    }

    public static void log(ByteBuf buffer, boolean pretty) {
        int length = buffer.readableBytes();
        int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
        StringBuilder buf = new StringBuilder(rows * 80 * 2)
                .append("read index:").append(buffer.readerIndex())
                .append(" write index:").append(buffer.writerIndex())
                .append(" capacity:").append(buffer.capacity())
                .append(NEWLINE);
        if (pretty) {
            appendPrettyHexDump(buf, buffer);
        }
        System.out.println(buf.toString());
    }
}

创建

ByteBuf能自动扩容,初始值256,最大值为Integer最大范围

public class TestByteBuf {
    public static void main(String[] args) {
        //创建ByteBuf, 默认创建大小256个字节的,最大为Integer最大值
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        System.out.println(buffer.getClass());
        DebugByteBuf.log(buffer);//初始256
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 300; i++) {
            sb.append("a");
        }
        buffer.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8));
        DebugByteBuf.log(buffer);//由于超过了容量256,自动扩容到512
    }
}

写入

大小端存储

Big Endian(大端存储)Little Endian(小端存储)是两种不同的字节存储方式,用于表示一个多字节数据类型在内存中的存储顺序。

不要将字节与位的关系混淆。

计算机中用来表示内存储器容量大小的基本单位是字节(Byte),此处是讲多字节数据类型的存储顺序。

Big Endian(大端存储)是指内存的低地址,存储高位字节。

Little Endian(小端存储)是指内存的低地址,存储低位字节。

操作系统都采用小端存储模式

通讯协议则采用大端存储模式

测试大小端存储时顺序

public class DebugEndian {

    public static void debug(long num) {
        // 将 long 变量转换为大端顺序的字节数组
        byte[] bigEndian = ByteBuffer.allocate(Long.BYTES).order(ByteOrder.BIG_ENDIAN).putLong(num).array();

        // 将 long 变量转换为小端顺序的字节数组
        byte[] littleEndian = ByteBuffer.allocate(Long.BYTES).order(ByteOrder.LITTLE_ENDIAN).putLong(num).array();

        // 打印大端和小端字节数组
        System.out.println("Big-endian dec: " + Arrays.toString(bigEndian));
        System.out.println("Little-endian dec: " + Arrays.toString(littleEndian));


        String[] bigEndianHex = new String[bigEndian.length];
        for (int i = 0; i < bigEndian.length; i++) {
            bigEndianHex[i] = Integer.toHexString(bigEndian[i]);
        }
        String[] littleEndianHex = new String[littleEndian.length];
        for (int i = 0; i < bigEndian.length; i++) {
            littleEndianHex[i] = Integer.toHexString(littleEndian[i]);
        }
        System.out.println("Big-endian hex: " + Arrays.toString(bigEndianHex));
        System.out.println("Little-endian hex: " + Arrays.toString(littleEndianHex));
    }

    public static void main(String[] args) {
        // 声明一个值为 0x112345678 的 long 变量
        //long num = 0x112345678L;
        long num = 0x250L;
        debug(num);
    }
}

方法列表

方法列表,省略一些不重要的方法

方法含义备注
writeBoolean(boolean value)写入 boolean 值,占1字节非零为真
writeByte(int value)写入 byte 值,占1字节
writeShort(int value)写入 short 值,占2字节
writeInt(int value)写入 int 值,占4字节Big Endian(大端写入),如 250,写入后16进制表示 00 00 00 fa
writeIntLE(int value)写入 int 值,占4字节Little Endian(小端写入),如 250,写入后16进制表示 fa 00 00 00
writeLong(long value)写入 long 值,占8字节
writeChar(int value)写入 char 值,占2字节
writeFloat(float value)写入 float 值,占4字节
writeDouble(double value)写入 double 值,占8字节
writeBytes(ByteBuf src)写入 netty 的 ByteBuf
writeBytes(byte[] src)写入 byte[]
writeBytes(ByteBuffer src)写入 nio 的 ByteBuffer
int writeCharSequence(CharSequence sequence, Charset charset)写入字符串

注意

  • 这些方法的未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用
  • 网络传输,默认习惯是 Big Endian (大端存储)
  • CharSequence 是个接口,像 String/StringBuilder 都实现该接口
扩容
public class TestByteBufWrite {
    public static void main(String[] args) {
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(3);
        buffer.writeBytes(new byte[]{1,2});
        DebugByteBuf.log(buffer,true);
        buffer.writeInt(250);
        DebugByteBuf.log(buffer,true);
        buffer.writeIntLE(250);
        DebugByteBuf.log(buffer,true);
    }
}

由上图,可验证大小端存储相关知识。

不过由上图,也发现进行了自动扩容, ByteBuf 的扩容规则是

  • 若容量小于16,则扩容后16
  • 若容量大于16小于64,则扩容后64
  • 若容量大于64,则每次扩容为当前容量的2倍
  • 扩容不能超过 max capacity 会报错

读取

方法列表

方法名含义备注
int readByte()读取一个字节会向后移动读指针
ByteBuf markReaderIndex()将当前位置定义为读标记。默认是0
ByteBuf resetReaderIndex()重置到读标记
getXXX读取不会改变读指针

3.6.4 内存释放

释放原理

由于 Netty 中有多种内存的 ByteBuf 实现,因此要灵活处理

  • UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
  • UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存,手动释放
  • PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存,手动释放还给内存池

不过,Netty 为了方便开发者手动释放内存,采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 io.netty.util.ReferenceCounted 接口

  • 每个 ByteBuf 对象初始后,计数为 1
  • 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
  • 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
  • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用

计数为0后的释放逻辑,在io.netty.buffer.AbstractReferenceCountedByteBuf#deallocate方法中

谁来负责 release 呢?

不是我们想象的(一般情况下)

ByteBuf buf = ...
try {
    ...
} finally {
    buf.release();
}

请思考,因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性(当然,如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递)

基本规则是,谁是最后使用者,谁负责 release

在pipeline中,head与tail两个处理器可以自动做收尾工作

  • 入站msg,tail 对 ByteBuf 进行释放
  • 出站msg,head 对 ByteBuf 进行释放

开发者不能完全依赖head与tail

如果用户在某个handler中,并没有将ByteBuf往后面的处理器传,这时候,收尾的head与tail就失去了作用,因为你根本没把资源传递给我,我咋释放啊

tail源码

tail只处理入站,写出都是通过outHandler执行的,所以跟tail也没啥关系。因此代码中也只实现入站处理器。

final class TailContext extends AbstractChannelHandlerContext 
    implements ChannelInboundHandler {}

查看io.netty.channel.DefaultChannelPipeline.TailContext#channelRead方法,往下跟,找到如下代码

protected void onUnhandledInboundMessage(Object msg) {
    try {
        logger.debug(
                "Discarded inbound message {} that reached at the tail of the pipeline. " +
                        "Please check your pipeline configuration.", msg);
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
//ReferenceCountUtil
public static boolean release(Object msg) {
    if (msg instanceof ReferenceCounted) {
        return ((ReferenceCounted) msg).release();
    }
    return false;
}

head源码

head既处理入站,也处理出站,因此两个处理器都实现

final class HeadContext extends AbstractChannelHandlerContext
    implements ChannelOutboundHandler, ChannelInboundHandler{}

查看io.netty.channel.DefaultChannelPipeline.HeadContext#write方法,往下跟,找到如下代码

public final void write(Object msg, ChannelPromise promise) {
    assertEventLoop();

    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null) {
        // If the outboundBuffer is null we know the channel was closed and so
        // need to fail the future right away. If it is not null the handling of the rest
        // will be done in flush0()
        // See https://github.com/netty/netty/issues/2362
        safeSetFailure(promise, newClosedChannelException(initialCloseCause));
        // release message now to prevent resource-leak
        ReferenceCountUtil.release(msg);
        return;
    }

    int size;
    try {
        msg = filterOutboundMessage(msg);
        size = pipeline.estimatorHandle().size(msg);
        if (size < 0) {
            size = 0;
        }
    } catch (Throwable t) {
        safeSetFailure(promise, t);
        ReferenceCountUtil.release(msg);
        return;
    }

    outboundBuffer.addMessage(msg, size, promise);
}

3.6.5 深拷贝

会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关

public class TestCopy {
    public static void main(String[] args) {
        ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
        buf1.writeBytes(new byte[]{
                1, 2, 3, 4, 5
        });
        ByteBuf buf2 = buf1.copy();

        //验证深拷贝
        buf2.setByte(0, 10);
        log(buf1, true);
        log(buf2, true);
    }
}

3.6.6 零拷贝

零拷贝不进行内存复制,使用原有内存,其相关应用分为两大类

    • slice
    • duplicate
    • composite

切片slice

【零拷贝】的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read 和 write 指针

注意:

切片后的ByteBuf底层是SlicedByteBuf,再写内容,对原始数据有影响,因此,SlicedByteBuf禁止写入。

同样地,原内容发生变化,SlicedByteBuf也受到影响

所以,实际编写代码时,需要添加引用和手动释放

验证并没有发生数据复制

public class TestSlice {
    public static void main(String[] args) {
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
        buf.writeBytes(new byte[]{
                1, 2, 3, 4, 5, 6, 7, 8, 9, 10
        });
        log(buf, true);

        //在切片过程中,并未发生数据复制
        ByteBuf buf1 = buf.slice(0, 3);
        buf1.retain();//引用一次
        ByteBuf buf2 = buf.slice(3, 3);
        buf2.retain();
        ByteBuf buf3 = buf.slice(6, 4);
        buf3.retain();
        log(buf1, true);
        log(buf2, true);
        log(buf3, true);
        //使用如下代码,验证并没有进行数据复制
        //注意toString中的hashCode并非表示地址值,只是哈希值而已
        buf1.setByte(0, 10);
        log(buf1, true);
        log(buf, true);
        System.out.println("引用次数"+buf.refCnt());//4
    }
}

浅拷贝duplicate

【零拷贝】的体现之一,就好比截取了原始 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的

实际编写代码时,需要添加引用和手动释放

组合compositeBuffer

【零拷贝】的体现之一,可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免拷贝

public class TestComposite {
    public static void main(String[] args) {
        ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
        buf1.writeBytes(new byte[]{
                1, 2, 3, 4, 5
        });
        ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
        buf2.writeBytes(new byte[]{
                6, 7, 8, 9, 10
        });

        发生数据复制的合并
        //ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
        //buf.writeBytes(buf1).writeBytes(buf2);
        //log(buf, true);

        //不发生数据复制的合并
        CompositeByteBuf buf = ByteBufAllocator.DEFAULT.compositeBuffer(10);
        buf.addComponents(true, buf1, buf2);
        buf1.retain();//添加引用,防止buf1和buf2被释放
        buf2.retain();//添加引用,防止buf1和buf2被释放
        log(buf, true);

        //验证未发生数据合并
        buf1.setByte(0, 10);
        log(buf, true);
        System.out.println(buf1.refCnt());
        System.out.println(buf2.refCnt());
    }
}

实际编写代码时,需要添加引用和手动释放

Unpooled 是一个工具类,类如其名,提供了非池化的 ByteBuf 创建、组合、复制等操作

这里仅介绍其跟【零拷贝】相关的 wrappedBuffer 方法(底层即CompositeByteBuf),可以用来包装 ByteBuf

ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});

// 当包装 ByteBuf 个数超过一个时, 底层使用了 CompositeByteBuf
ByteBuf buf3 = Unpooled.wrappedBuffer(buf1, buf2);
System.out.println(ByteBufUtil.prettyHexDump(buf3));

输出

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 0a                   |..........      |
+--------+-------------------------------------------------+----------------+

也可以用来包装普通字节数组,底层也不会有拷贝操作

ByteBuf buf4 = Unpooled.wrappedBuffer(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
System.out.println(buf4.getClass());
System.out.println(ByteBufUtil.prettyHexDump(buf4));

输出

class io.netty.buffer.CompositeByteBuf
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06                               |......          |
+--------+-------------------------------------------------+----------------+

T.buffer(5);
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});

// 当包装 ByteBuf 个数超过一个时, 底层使用了 CompositeByteBuf
ByteBuf buf3 = Unpooled.wrappedBuffer(buf1, buf2);
System.out.println(ByteBufUtil.prettyHexDump(buf3));


输出

     +-------------------------------------------------+
     |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |

±-------±------------------------------------------------±---------------+
|00000000| 01 02 03 04 05 06 07 08 09 0a |… |
±-------±------------------------------------------------±---------------+


也可以用来包装普通字节数组,底层也不会有拷贝操作

```java
ByteBuf buf4 = Unpooled.wrappedBuffer(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
System.out.println(buf4.getClass());
System.out.println(ByteBufUtil.prettyHexDump(buf4));

输出

class io.netty.buffer.CompositeByteBuf
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06                               |......          |
+--------+-------------------------------------------------+----------------+

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

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

相关文章

C++017-C++文件读写应用

文章目录 C017-C文件读写应用C文件读写应用CSP-J目标1. 文件的基本概念、文本文件的基本操作关闭文件 文件操作-写入文本文件2.文件读写操作基本案例seekg() 和 tellg() 函数来读取文件中的数据 2. 文本文件类型与二进制文件类型3. 文件重定向、文件读写等操作 在练习&#xff…

56.网页设计规则#4_图标

使用好的图标 使用一个好的图标包&#xff0c;有大量的免费和付费图标包 图标工具推荐&#xff1a; ● Phosphor icons ● ionicons ● ICONS8 只使用一个图标包。不要混合不同图标包中的图标 使用SVG图标或图标字体。不要使用位图图像格式(.jpg and .png)! 调整网站个性!圆…

Android 中的跨进程数据块传递

Android 的 Binder 进程间通信机制主要用于实现远程过程调用 RPC&#xff0c;Android 系统中进程之间的大块数据传递&#xff0c;如音频数据&#xff0c;出于效率等原因&#xff0c;一般不直接用 Binder 机制。Binder 库提供了基于共享内存外加 Binder 机制的跨进程文件描述符传…

每月一书(202304)《RxJava2.x实战》

[TOC] 又到了每月一书的时间&#xff0c;本月阅读的是技术相关书籍《RxJava2.x实战》&#xff0c;下面分享一下我阅读完后的体会。 主要内容 本书主要介绍了RxJava这个框架&#xff0c;框架版本是2.x。主要内容包含三大部分&#xff1a; 框架的原理和使用方法框架中各类操…

git alias

git alias 其实之前就用过一些 alias&#xff0c;比如说 git reflog show 就是 git log -g --abbrev-commit --prettyoneline 的 alias&#xff0c;一般 alias 可以存储到 git 的 config 文件&#xff0c;repo 等级的在 .git 下&#xff0c;global 的一般在 ~/.gitconfig 或者…

【flask】三种路由和各自的比较配置文件所有的字母必须大写if __name__的作用核心对象循环引用的几种解决方式--难Flask的经典错误上下文管理器

三种路由 方法1&#xff1a;装饰器 python C#, java 都可以用这种方式 from flask import Flask app Flask(__name__)app.route(/hello) def hello():return Hello world!app.run(debugTrue)方法2: 注册路由 php python from flask import Flask app Flask(__name__)//app…

【前端面经】JS-事件循环

什么是事件循环(Event Loop)? 众所周知, Javascript是一门单线程的语言, 单线程即同一时间只能做一件事, 但这并不意味着JavaScript在执行代码的过程中就会一直阻塞,而解决单线程不阻塞的这个机制就叫做事件循环(Event Loop), 也就是同步和异步的概念. 任务执行流程 在JS中…

PS滤镜插件-Nik Collection介绍

PS滤镜插件-Nik Collection介绍 什么是Nik CollectionNik Collection都包含什么&#xff1f; 什么是Nik Collection Nik Collection是一款PS滤镜插件套装&#xff0c;其包含了八款PS插件&#xff0c;功能涵盖修图、调色、降噪、胶片滤镜等方面。Nik Collection 作为很多摄影师…

redhat 安装oracle 11g

这里写目录标题 1、数据库下载和安装文档1.2、安装文档1.3、license种类解释&#xff08; XE版 标准本 个人版 企业版&#xff09;1.4、在安装完oracle后再创建数据库1.5、DBA的文档1.6、Automatic Storage Management Administrators Guide1.7、数据库备份恢复手册1.8、Overvi…

系统集成项目管理工程师 笔记(第15章 信息(文档)和配置管理)

文章目录 软件文档的分类&#xff08;1&#xff09;开发文档&#xff1a;描述开发过程 本身&#xff08;2&#xff09;产品文档&#xff1a;描述开发过程的 产物&#xff08;3&#xff09;管理文档&#xff1a;记录项目管理的信息 文档的质量可以分为四级&#xff08;1&#xf…

03_Uboot网络命令与MMC和文件操作命令

目录 网络操作命令 ping命令 nfs 命令 tftp 命令 EMMC和SD卡操作命令 mmc info命令 mmc rescan命令 mmc list 命令 mmc dev 命令 mmc part命令 mmc read 命令 mmc write 命令 mmc erase 命令 FAT 格式文件系统操作命令 fatinfo 命令 fatls 命令 fstype 命令 …

好的代码风格,如同书法,让你的代码更加漂亮

很多初学者的代码其实都不够“漂亮”&#xff0c;那是因为没有养成好的编码习惯。本篇博客以C语言为例&#xff0c;总结一些好习惯。其实&#xff0c;很多习惯都是肌肉记忆&#xff0c;举个例子&#xff1a;请你写一个程序&#xff0c;输入2个整数并输出它们的和。有些朋友可能…

java中的\t说明

阅读前请看一下&#xff1a;我是一个热衷于记录的人&#xff0c;每次写博客会反复研读&#xff0c;尽量不断提升博客质量。文章设置为仅粉丝可见&#xff0c;是因为写博客确实花了不少精力。希望互相进步谢谢&#xff01;&#xff01; 文章目录 阅读前请看一下&#xff1a;我是…

SQLIntegrityConstraintViolationException: Column ‘create_time‘ cannot be null

概述 在使用MySQL MyBatis时遇到的问题&#xff0c;记录一下。 问题 在测试环境里&#xff0c;往MySQL数据表里插入数据时报错&#xff1a;SQLIntegrityConstraintViolationException: Column create_time cannot be null 表结构字段定义&#xff1a; create_time dateti…

【Canvas入门】从零开始在Canvas上绘制简单的动画

这篇文章是观看HTML5 Canvas Tutorials for Beginners教程做的记录&#xff0c;所以代码和最后的效果比较相似&#xff0c;教程的内容主要关于这四个部分&#xff1a; 创建并设置尺寸添加元素让元素动起来与元素交互 设置Canvas的大小 获取到canvas并设置尺寸为当前窗口的大…

Lesson13 IP协议

IP: 提供一种能力,将数据从A主机送到B主机的能力,但不一定会成功 主机 : 配有 IP 地址 , 但是不进行路由控制的设备 ; 路由器: 即配有 IP 地址 , 又能进行路由控制 ; 节点 : 主机和路由器的统称; 协议头格式 如何封装和解包: 定长报头 自描述字段 如何交付(分用) : 8…

Linux驱动之input输入子系统

输入子系统用于实现Linux系统输入设备&#xff08;鼠标 键盘 触摸屏 游戏杆&#xff09;驱动的一种框架。Linux内核将其中的固定部分放入内核&#xff0c;驱动开发时只需要实现其中的不固定部分&#xff08;主要还是和硬件相关的部分&#xff09;&#xff0c;这和platform设备…

离散数学下--- 代数系统

代数系统 定义&#xff1a; 代数系统是用代数运算构造数学模型的方法。 • 通过构造手段生成&#xff0c;所以也称代数结构 • 代数运算&#xff1a;在集合上建立满足一定规则的运算系统 &#xff08;一&#xff09;二元运算 二元运算的定义 二元运算需要满足的两个条件&a…

【P1】Jmeter 准备工作

文章目录 一、Jmeter 介绍1.1、Jmeter 有什么样功能1.2、Jmeter 与 LoadRunner 比较1.3、常用性能测试工具1.4、性能测试工具如何选型1.5、学习 Jmeter 对 Java 编程的要求 二、Jmeter 软件安装2.1、官网介绍2.2、JDK 安装及环境配置2.3、Jmeter 三种模式2.4、主要配置介绍2.4.…

数据结构——二叉树层序遍历

数据结构——二叉树层序遍历 107. 二叉树的层序遍历 II199. 二叉树的右视图思路&#xff1a; 637. 二叉树的层平均值 107. 二叉树的层序遍历 II 107. 二叉树的层序遍历 II 给你二叉树的根节点 root &#xff0c;返回其节点值 自底向上的层序遍历 。 &#xff08;即按从叶子节…