Netty-入門

善良的吉他手發表於2021-05-20

Hello World

目標

開發一個簡單的伺服器端和客戶端

  • 客戶端向伺服器端傳送 hello, world
  • 伺服器僅接收,不返回

依賴

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

伺服器端

@Slf4j
public class HelloServer {

    public static void main(String[] args) {
        new ServerBootstrap()
                // 1. 建立 NioEventLoopGroup
                .group(new NioEventLoopGroup())
                // 2. 選擇 Socket 實現類,NioServerSocketChannel 是基於 nio 實現的
                .channel(NioServerSocketChannel.class)
                // 3. 子執行緒處理類 handler
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        // 5. socketChannel 解碼器
                        nioSocketChannel.pipeline().addLast(new StringDecoder());
                        // 6. SocketChannel 的業務處理,使用上一個處理器的處理結果
                        nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
                                log.info("msg = " + msg);
                            }
                        });
                    }
                })
                // 4. 繫結監聽埠
                .bind(8080);
    }
}

程式碼解讀

  • 1 處,建立 NioEventLoopGroup,可以簡單理解為 執行緒池 + Selector 後面會詳細展開
  • 2 處,選擇服務 Scoket 實現類,其中 NioServerSocketChannel 表示基於 NIO 的伺服器端實現
  • 3 處,為啥方法叫 childHandler,是接下來新增的處理器都是給 SocketChannel 用的,而不是給 ServerSocketChannel。ChannelInitializer 處理器(僅執行一次),它的作用是待客戶端 SocketChannel 建立連線後,執行 initChannel 以便新增更多的處理器
  • 4 處,ServerSocketChannel 繫結的監聽埠
  • 5 處,SocketChannel 的處理器,解碼 ByteBuf => String
  • 6 處,SocketChannel 的業務處理器,使用上一個處理器的處理結果

客戶端

@Slf4j
public class HelloClient {
    public static void main(String[] args) throws InterruptedException {
        // 1. 啟動類
        new Bootstrap()
                // 2. 新增 EventLoop
                .group(new NioEventLoopGroup())
                // 3. 選擇客戶端 channel 實現
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        // 8
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                // 4. 連線到伺服器
                .connect(new InetSocketAddress("localhost",8080))
                // 5
                .sync()
                // 6
                .channel()
                // 7. 向伺服器傳送資料
                .writeAndFlush("hello word");
    }
}

程式碼解讀

  • 1 處,建立 NioEventLoopGroup,同 Server
  • 2 處,選擇客戶 Socket 實現類,NioSocketChannel 表示基於 NIO 的客戶端實現
  • 3 處,新增 SocketChannel 的處理器,ChannelInitializer 處理器(僅執行一次),它的作用是待客戶端 SocketChannel 建立連線後,執行 initChannel 以便新增更多的處理器
  • 4 處,指定要連線的伺服器和埠
  • 5 處,Netty 中很多方法都是非同步的,如 connect,這時需要使用 sync 方法等待 connect 建立連線完畢
  • 6 處,獲取 channel 物件,它即為通道抽象,可以進行資料讀寫操作
  • 7 處,寫入訊息並清空緩衝區
  • 8 處,訊息會經過通道 handler 處理,這裡是將 String => ByteBuf 發出
  • 資料經過網路傳輸,到達伺服器端,伺服器端 5 和 6 處的 handler 先後被觸發,走完一個流程

元件

EventLoop

事件迴圈物件

EventLoop 本質是一個單執行緒執行器(同時維護了一個 Selector),裡面有 run 方法處理 Channel 上源源不斷的 io 事件。

它的繼承關係比較複雜

  • 一條線是繼承自 j.u.c.ScheduledExecutorService 因此包含了執行緒池中所有的方法
  • 另一條線是繼承自 netty 自己的 OrderedEventExecutor,
    • 提供了 boolean inEventLoop(Thread thread) 方法判斷一個執行緒是否屬於此 EventLoop
    • 提供了 parent 方法來看看自己屬於哪個 EventLoopGroup

事件迴圈組

EventLoopGroup 是一組 EventLoop,Channel 一般會呼叫 EventLoopGroup 的 register 方法來繫結其中一個 EventLoop,後續這個 Channel 上的 io 事件都由此 EventLoop 來處理(保證了 io 事件處理時的執行緒安全)

  • 繼承自 netty 自己的 EventExecutorGroup
    • 實現了 Iterable 介面提供遍歷 EventLoop 的能力
    • 另有 next 方法獲取集合中下一個 EventLoop
@Slf4j
public class EventLoopServer {
    public static void main(String[] args) {
        /**
         * 細分1:
         * boss 只負責 ServerSocketChannel 上的 accept 事件
         * worker 只負責 SocketChannel 上的讀寫事件
         */
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        /**
         * 細分2:
         * 建立一個獨立的 EventLoopGroup
         */
        EventLoopGroup dfGroup = new DefaultEventLoopGroup();
        new ServerBootstrap()
                .group(boss,worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel sc) throws Exception {
                        sc.pipeline().addLast("handler1",new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf) msg;
                                log.info(buf.toString(Charset.defaultCharset()));
                                ctx.fireChannelRead(msg); // 把訊息傳遞給下一個 handler
                            }
                        }).addLast(dfGroup,"handler2",new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf)msg;
                                log.info(buf.toString(Charset.defaultCharset()));
                            }
                        });
                    }
                })
                .bind(8085);
    }
}
@Slf4j
public class EventLoopClient {
    public static void main(String[] args) throws InterruptedException {

        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel sc) throws Exception {
                        sc.pipeline().addLast(new StringEncoder());
                    }
                })
                .connect(new InetSocketAddress("localhost", 8085))
                .sync()
                .channel();

        log.info(channel.toString());
        System.out.println();
    }
}

優雅關閉

優雅關閉 shutdownGracefully 方法。該方法會首先切換 EventLoopGroup 到關閉狀態從而拒絕新的任務的加入,然後在任務佇列的任務都處理完成後,停止執行緒的執行。從而確保整體應用是在正常有序的狀態下退出的

Channel

channel 的主要作用

  • close() 可以用來關閉 channel
  • closeFuture() 用來處理 channel 的關閉
    • sync 方法作用是同步等待 channel 關閉
    • 而 addListener 方法是非同步等待 channel 關閉
  • pipeline() 方法新增處理器
  • write() 方法將資料寫入
  • writeAndFlush() 方法將資料寫入並刷出

ChannelFuture

@Slf4j
public class EventLoopServer {
    public static void main(String[] args) {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        new ServerBootstrap()
                .group(boss,worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel sc) throws Exception {
                        sc.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf) msg;
                                log.info(buf.toString(Charset.defaultCharset()));
                            }
                        });
                    }
                })
                .bind(8086);
    }
}
@Slf4j
public class EventLoopClient {
    public static void main(String[] args) throws InterruptedException {

        // 2. 帶有 Future ,Promise 的型別都是和非同步方法配套使用,用來處理結果
        ChannelFuture channelFuture = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel sc) throws Exception {
                        sc.pipeline().addLast(new StringEncoder());
                    }
                })
                // 1. 連線到伺服器
                // 非同步阻塞,main 執行緒發起了呼叫,真正執行 connect 連線的是 nio 執行緒
                .connect(new InetSocketAddress("localhost", 8086));

        // 2.1 使用 sync 方法同步處理結果
//        channelFuture.sync();  // 阻塞住當前執行緒,直到 nio 執行緒連線建立完畢
//        Channel channel = channelFuture.channel();
//        log.info(channel.toString());
//        channel.writeAndFlush("hello word;");

        // 2.2 使用 addListener(回撥物件)方法非同步處理結果
        channelFuture.addListener(new ChannelFutureListener() {
            // 在 nio 執行緒連線建立好之後,會呼叫 operationComplete
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                Channel channel = channelFuture.channel();
                log.info(channel.toString());
                channel.writeAndFlush("hello word");
            }
        });
    }
}

CloseFuture

@Slf4j
public class EventLoopClient2 {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
        ChannelFuture channelFuture = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
                        socketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                .connect(new InetSocketAddress("localhost", 8086));
        Channel channel = channelFuture.sync().channel();
        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String str = scanner.nextLine();
                if("q".equals(str)){
                    channel.close();
                    break;
                }
                channel.writeAndFlush(str);
            }
        },"input").start();

        // 1. 獲取 CloseFuture 物件同步處理關閉
        ChannelFuture closeFuture = channel.closeFuture();
//        closeFuture.sync();  // 阻塞
//        log.info("處理關閉後的操作...");

        // 2. 非同步處理關閉
        closeFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                log.info("處理關閉之後的操作...");
                // 優雅的停止接受任務
                group.shutdownGracefully();
            }
        });
    }
}