第二章 第一个Netty程序

79 阅读10分钟

1. 第一个Netty程序

废话少说,直接上代码。
客户端:

public class MyNettyClient {  
    public static void main(String[] args) throws InterruptedException {  
        Bootstrap bootstrap = new Bootstrap();  

        bootstrap.channel(NioSocketChannel.class);  

        bootstrap.group(new NioEventLoopGroup());  

        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {  
            @Override  
            protected void initChannel(NioSocketChannel ch) throws Exception {  
                ch.pipeline().addLast(new StringEncoder());  
            }  
        });  

        ChannelFuture connect = bootstrap.connect(new InetSocketAddress(8000));  
        connect.sync();  
       //创建了新的线程 进行写操作  
        Channel channel = connect.channel();  
        channel.writeAndFlush("hello world!");  
    }  
}

服务端:

public class MyNettyServer {  
    public static void main(String[] args) {  
        ServerBootstrap serverBootstrap = new ServerBootstrap();  
        serverBootstrap.channel(NioServerSocketChannel.class);  

        //创建了一组线程 通过死循环 监控状态 accept read write  
        serverBootstrap.group(new NioEventLoopGroup());  

        // SocketChannel  
        serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {  
            /*  
            channel 接通 监控 accept rw 处理 通过流水线 用handler进行处理 。  
            */  
            @Override  
            protected void initChannel(NioSocketChannel ch) throws Exception {  
                //ByteBuf 字节--->字符  
                ch.pipeline().addLast(new StringDecoder());  
                // 自定义处理器 handle  
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){  
                @Override  
                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
                    System.out.println("msg = " + msg);  
                }  
                });  
            }  
        });  
        serverBootstrap.bind(8000);  
    }  
}

首先来解释用到的Netty组件。

2. EventLoop

在Netty中,EventLoop是一个用于处理事件的循环。每个EventLoop都运行在一个单独的线程中,并负责处理多个Channel上的事件。EventLoop的实例被用于驱动异步操作,例如处理接收到的数据、执行用户的业务逻辑等。
通俗的说EventLoop就相当于一个线程,监控accept read write等状态,不用像NIO那样显示的写while(true)监测事件,还可以区分开来boss线程和work线程。

public static void main ( String[] args ) {  
  
    // 主  
    EventLoopGroup bossGroup = new NioEventLoopGroup(1);  
    // 从  
    EventLoopGroup workerGroup = new NioEventLoopGroup(3);  

    // 专门用来处理某一个handler 可以在这里处理复杂的操作 释放nioEventLoop,处理别的请求连接操作  
    DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();  

    ServerBootstrap serverBootstrap = new ServerBootstrap();  
    serverBootstrap.channel(NioServerSocketChannel.class);  

    // 线程池 EventLoop  
    // serverBootstrap.group(new NioEventLoopGroup());  
    serverBootstrap.group(bossGroup, workerGroup);  
    // 在这里处理接收到的数据  
    serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {  
        // read write channel  
        // channel --> accept 建立连接的操作  
        @Override  
        protected void initChannel ( NioSocketChannel ch ) throws Exception {  
        ch.pipeline().addLast(new StringDecoder());  
        ch.pipeline().addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){  
        @Override  
        public void channelRead ( ChannelHandlerContext ctx, Object msg ) throws Exception {  
            System.out.println("服务端收到消息:" + msg);  
        }  
        });  
        }  
    });  
    serverBootstrap.bind(8000);  
}

EventLoop :
worker 线程 监听 ---> READ WRITE
boss 线程 监听 ---> Accept
EventLoop的创建通常是通过EventLoopGroup来管理的。EventLoopGroup是一组EventLoop的容器,负责创建和管理它们。在创建EventLoopGroup时,通常有两种主要的实现类:NioEventLoopGroupEpollEventLoopGroup

3. EventLoopGroup

EventLoopGroup 是 Netty 中的一个重要概念,用于管理和维护多个 EventLoop 实例。它通常用于处理连接、接受连接、处理I/O等任务。EventLoopGroup 负责创建和管理一组 EventLoop 实例,并确保这些实例在整个应用程序的生命周期内得到正确的使用和维护。

  1. 定义: EventLoopGroup 是一个用于管理 EventLoop 实例的容器。它充当了线程池的角色,负责处理并发的I/O事件。

  2. EventLoop: EventLoopGroup 中包含多个 EventLoop,每个 EventLoop 都运行在独立的线程中,负责处理特定的连接和事件。它们负责执行任务、处理I/O操作、管理定时任务等。

  3. 两种类型: 在 Netty 中,有两种常见的 EventLoopGroup 实现:

    • NioEventLoopGroup: 基于Java的NIO实现,适用于大多数场景,如TCP连接。
    • EpollEventLoopGroup: 基于Linux的Epoll实现,对于高并发的网络应用,性能可能更好。

4. NioEventLoop和DefaultEventLoop区别

  1. NioEventLoop 是一个线程 主要对IO中 Write Read 事件的监控
  2. DefaultEventLoop 就是一个普通的线程,内容工作可以由程序员决定,他不做 IO监控 读写的处理.

注意:后续在Netty进行多线程开发,推荐大家优先考虑DefaultEventLoop -->普通线程。 DefaultEventLoop,更多的是辅助NIOEventLoop完成普通业务操作。

5. 异步

都说NIO是异步非阻塞的,那么,在Netty中是怎么体现的呢?
在client中,

// 开始新的线程 异步进行网络连接  
// 异步 阻塞的方式 与服务器进行连接  
ChannelFuture future = bootstrap.connect(new InetSocketAddress(8000));  
future.sync(); // 阻塞 当前线程 等待上一步 连接真正的建立起来 --> 阻塞 直到连接成功

future.sync();在这里是阻塞住的,等连接建立起来,所以用一个新的线程来等待连接的建立.
还可以不通过阻塞,通过异步监听回调的方式,监听连接的建立

// 不通过阻塞 通过异步回调方式  
future.addListener(new ChannelFutureListener() {  
    @Override  
    public void operationComplete ( ChannelFuture channelFuture ) throws Exception {  
        Channel channel = future.channel();  
        channel.writeAndFlush("hello !");  
    }  
});

JDK中是怎么实现的?

public static void main ( String[] args ) throws ExecutionException, InterruptedException {  
    ExecutorService executorService = Executors.newFixedThreadPool(2);  

    Future<Integer> future = executorService.submit(new Callable<Integer>() {  
        // 异步化的工作使用future 启用一个新的线程进行处理 最后把结果返回给主线程(调用者线程)  
        @Override  
        public Integer call () throws Exception {  
        log.info("异步的工作处理.......");  
        TimeUnit.SECONDS.sleep(1);  
        return 1;  
        }  
    });  
    log.info("返回结果 {}",future.get()); // 阻塞  
    log.info("----------------");  
}

很简单,是通过Future实现的。
在Netty中,也封装了Future的实现,io.netty.util.concurrent.Future;

public static void main ( String[] args ) throws ExecutionException, InterruptedException {  
    // netty 对线程池进行封装就是 EventLoopGroup  
    DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup(2);  

    EventLoop eventLoop = defaultEventLoopGroup.next();  
    Future<Integer> future = eventLoop.submit(new Callable<Integer>() {  
        @Override  
        public Integer call () throws Exception {  
            log.info("异步的工作处理.......");  
            TimeUnit.SECONDS.sleep(1);  
            return 1;  
        }  
    });  
    // log.info("返回结果 {}",future.get()); // 阻塞  
    // log.info("----------------");  

    // 异步监听  
    // 异步处理的2个问题  
    // 1 runnable 接口 不能返回结果  
    // 2 callable 接口 不能准确的表达返回的结果是 成功还是 失败的  
    future.addListener(new GenericFutureListener<Future<? super Integer>>() {  
        @Override  
        public void operationComplete ( Future<? super Integer> future ) throws Exception {  
            log.info("返回结果 {}",future.get());  
        }  
    });  
}

跟在JDK中的Future的用法是同样的,不过,在Netty中还多了异步监听的方式,获取返回结果。

不过这样并不能保证我们返回的结果正确的表达成功还是失败的,所以Netty还封装了Promise。Netty中大多数的异步操作都是使用的Promise.
Promise 是一个与 Future 结合使用的接口,用于处理异步操作的结果。Promise 表示一个操作的最终状态(成功或失败),并提供了一种方式来注册回调,以便在操作完成时执行相应的逻辑。Promise 继承自 Java 标准库中的 java.util.concurrent.Future 接口。

public interface Promise<V> extends Future<V> { 
    Promise<V> setSuccess(V result);
    Promise<V> setFailure(Throwable cause); 
}
  • setSuccess(V result) 方法表示操作成功,并设置操作的结果。
  • setFailure(Throwable cause) 方法表示操作失败,并设置失败的原因。

DefaultPromise 实现: 在实际使用中,通常使用 DefaultPromise 类来创建 Promise 实例。

    public static void main ( String[] args ) throws ExecutionException, InterruptedException {  
    EventLoop eventLoop = new DefaultEventLoopGroup(2).next();  

    DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);  

    new Thread(()->{  
        log.info("异步的工作处理.......");  
        try {  
            TimeUnit.SECONDS.sleep(1);  
        } catch ( InterruptedException e ) {  
            promise.setFailure(e);  
            throw new RuntimeException(e);  
        }  
            promise.setSuccess(10);  
        }).start();  

        // log.info("返回结果 {}",promise.get());  

        promise.addListener(new GenericFutureListener<Future<? super Integer>>() {  
        @Override  
        public void operationComplete ( Future<? super Integer> future ) throws Exception {  
            log.info("返回结果 {}",promise.get());  
        }  
    });  
}

6. Channel

在Netty中,Channel 是网络操作的一个抽象,它代表了一个开放的连接,可以进行数据的读取和写入。Channel 是Netty中与网络交互的主要接口,提供了统一的API用于处理底层的网络细节。

核心API:

  • channel.writeAndFlush("hello");
    • 写出数据后,刷新缓冲区,直接把数据发送出去
  • channel.write()
    • 数据写在缓冲区不会立即发出去,需要调用.flush()
  • channel.close()
    • 关闭channel 是一个异步化操作
ChannelFuture close = channel.close();//异步化操作 启动一个新的线程  
//其他资源的释放,其他事,close()方法执行完之后,运行后面这些代码  
//main主线程完成  
close.sync();

Handler

在 Netty 中,ChannelHandler 是处理网络事件的基本单元,用于实现自定义的业务逻辑。ChannelHandler 接口定义了一系列的方法,通过重写这些方法,可以在不同的阶段处理入站(inbound)和出站(outbound)的事件。
主要就是在这里处理接收或者要发送的数据,通过Pipeline 把多个handler有机整合成了一个整体。 Pipleline中 执行Handler有固定顺序 (类似一个栈)双向链表。
读取数据 ChannleInboundHandler 子类
写出数据 ChannelOutboundHandler 子类

serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {  
    /*  
    channel 接通 监控 accept rw 处理 通过流水线 用handler进行处理 。  
    */  
    @Override  
    protected void initChannel(NioSocketChannel ch) throws Exception {  
        //ByteBuf 字节--->字符  
        ChannelPipeline pipeline = ch.pipeline();  
        pipeline.addLast(new StringDecoder());  
        pipeline.addLast("handel1",new ChannelInboundHandlerAdapter() {  
            @Override  
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
                log.info("服务端接收到消息:{}",msg);  
                super.channelRead(ctx,msg);  
            }  
        });  
        pipeline.addLast("handel2",new ChannelInboundHandlerAdapter() {  
            @Override  
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
                log.info("服务端接收到消息:{}",msg);  
                // super.channelRead(ctx,msg); 
                // 最后一个Handler不需要传递数据时,那么上述方法 也就无需调用
            }  
        });  
    }  
});

那么,上述的代码中一个 pipeline 包含了多少个 handle ?2个?
答案是一共4个,在pipeline中还隐藏了2个,Head Tail
head ---> StingDecoder ---> handler1 --> tail
OutboundHandler 特性 编码顺序 和 运行顺序 相反

ByteBuf

ByteBuf 是 Netty 中用于表示字节序列的数据缓冲区的抽象类。它提供了一种灵活的方式来处理字节数据,支持动态扩容和直接内存访问,是 Netty 中用于处理网络数据的核心组件之一。
Netty在网络通信过程中,底层数据存储在ByteBuf中,是对ByteBuffer的封装:

  • 自动扩容
  • 拥有读写指针,方便操作
  • 内存的池化
  • 零拷贝,尽量少占用内存

ByteBuf的使用

ByteBuf还分为堆内存和直接内存。
怎么获得:
直接内存:
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10); ByteBuf buf1 = ByteBufAllocator.DEFAULT.directBuffer();
默认的是创建一个直接内存的。
堆内存:
ByteBuf buf2 = ByteBufAllocator.DEFAULT.heapBuffer();
创建一个大小为10的 ByteBuf,默认大小为256,最大值为 Integer.max
自动扩容: 有个规律 -> 4的n次方

public static void main(String[] args) {  
    // 会自动扩容  
    ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);  

    for (int i = 0; i < 11; i++) {  
    buffer.writeByte(1);  
    }  

    System.out.println( buffer);  
    System.out.println(ByteBufUtil.prettyHexDump(buffer));  
}

image.png 可以看到第一次扩容到16,就是4的2次方。
设置条件 i < 17

image.png 第二次扩容到64,就是4的3次方。
重新设置条件 i < 65
第三次扩容 4的4次方 256对吧 来看一下打印结果

image.png 为什么是128,不应该是256吗?
Netty在这里做了优化,4的3次方之后的数据扩容为原数据的2倍,也就是 64*2 = 128
重新设置条件 i < 129

image.png 128 * 2 = 256,没有问题。

读写指针

获取容量: buf.capacity()
读指针: buf.readerIndex()
写指针:buf.writerIndex()

public static void main(String[] args) {  
    ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);  

    buffer.writeByte(1);  
    buffer.writeByte(2);  
    System.out.println( buffer);  
    System.out.println(ByteBufUtil.prettyHexDump(buffer));  

    byte b = buffer.readByte();  
    System.out.println("b = " + b);  
    System.out.println("buffer = " + buffer);  
    System.out.println("readIndex = "+ buffer.readerIndex());  
    System.out.println("writerIndex = "+ buffer.writerIndex());  
    System.out.println(ByteBufUtil.prettyHexDump(buffer));  
  
}

image.png 如上图可以看到刚开始读指针为0,写指针为2,容量为10,把1读出来后读指针前进变为1,写指针还是2,再读取2的话,读指针再往前移,为2,2,缓冲区的元素都被读取完了,再往下读会发生什么呢?

image.png 报错了,读指针的长度超过了写指针的长度,已经没有数据了,就不能再往下读了。
那如果我还想从头重新读数据怎么办,那就要把读指针重置为0了。

public static void main(String[] args) {  
    ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);  

    buffer.writeByte(1);  
    System.out.println( buffer);  
    System.out.println(ByteBufUtil.prettyHexDump(buffer));  

    // 标记索引  
    buffer.markReaderIndex();  
    byte b = buffer.readByte();  
    System.out.println("b = " + b);  
    System.out.println("buffer = " + buffer);  
    System.out.println("readIndex = "+ buffer.readerIndex());  
    System.out.println("writerIndex = "+ buffer.writerIndex());  
    System.out.println(ByteBufUtil.prettyHexDump(buffer));  

    // 想要重复读  
    buffer.resetReaderIndex();  
    byte b1 = buffer.readByte();  
    System.out.println("b1 = " + b1);  
    System.out.println(ByteBufUtil.prettyHexDump(buffer));  
}

重要的就这两个方法:
buffer.markReaderIndex();
buffer.resetReaderIndex();
标记索引,再重置索引。

池化

使用堆内存,创建和销毁代价小,但GC压力大,使用直接内存,创建和销毁代价大,但GC压力小。
Netty引入了池化思想,提高了创建的效率,合理的利用资源,减少内存溢出的可能,
ByteBuf创建时会尝试从池子里面取,ByteBuf在使用完毕后,需要通过调用release()方法来释放。当release()方法被调用时,ByteBuf的引用计数会减少,当引用计数为零时,内存将被返回到内存池。

  • 需要小心使用release()方法,确保在不再使用ByteBuf时及时释放。
  • 在使用ByteBuf时,应当遵循引用计数的规则,以防止内存泄漏。

ByteBuf一定只能应用在pipeline中,在handler中进行释放数据最为理想且稳妥。tailContext 会对读到的数据进行ByteBuf释放。headContext 会对写的数据进行ByteBuf的释放。在最后一次在handler中使用ByteBuf时候,做ByteBuf释放。
4.1之后池化是默认开启的,4.1之前是关闭的 -Dio.netty.allocator.type=pooled unpooled

分片

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','0'});  

    System.out.println( buf);  
    System.out.println(ByteBufUtil.prettyHexDump(buf));  

    // 分片  
    ByteBuf s1 = buf.slice(0, 6);  
    s1.retain();  
    System.out.println(s1);  
    ByteBuf s2 = buf.slice(6, 4);  
    s2.retain();  
    System.out.println(s2);  

    buf.release();// buf被释放 会导致下面的分片读取错误,因为引用的是同一个buf 可以使用s1.retain()  
    System.out.println(ByteBufUtil.prettyHexDump(s1));  
    System.out.println(ByteBufUtil.prettyHexDump(s2));  
}

至此,Netty的基础使用先到这。

码文不易,给个赞支持一下吧。

文章参考:孙帅suns Netty应用