Netty使用及事件傳遞

LemonNan發表於2019-11-02

本文地址: juejin.im/post/5dbcde…

Netty 使用

首先到官網看一下Netty Server 和 Client的demo, netty.io/wiki/user-g…, 我用的是4.1.xx,一般來說不是大版本變更, 變化不會很大.下面是 Netty Server 的demo,跟官網的是一樣的.

public class Main {

    // 下面是一個接收執行緒, 3個worker執行緒 ,所以這裡是 Reactor多執行緒模式
    // 用 Netty 的預設執行緒工廠,可以不傳這個引數
    private final static ThreadFactory threadFactory = new DefaultThreadFactory("Netty學習之路");
    // Boss 執行緒池,用於接收客戶端連線
    private final static NioEventLoopGroup boss = new NioEventLoopGroup(1,threadFactory);
    // Worker執行緒池,用於處理客戶端操作
    private final static NioEventLoopGroup worker = new NioEventLoopGroup(3,threadFactory);
    /*
     * 下面是在構造方法中, 如果不傳執行緒數量,預設是0, super 到 MultithreadEventLoopGroup 這裡後, 最終會用 CPU核數*2 作為執行緒數量, Reactor多執行緒模式的話,就指定 boss 執行緒數量=1
     *  private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
     *  protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
     *  super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
     *  }
     */

    public static void main(String[] args) throws Exception{
        try {
            new NettyServer(8888).start();
//            NIOTest();
            System.out.println(1<<0);
        }catch(Exception e){
            System.out.println("netty server啟動失敗");
            e.printStackTrace();
        }
    }

    static class NettyServer{

        private int port;

        NettyServer(int port){
            this.port = port;
        }

        void start()throws Exception{
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                ChannelFuture future = serverBootstrap
                        .group(boss, worker)
                        .channel(NioServerSocketChannel.class)
                        // 客戶端連線等待佇列大小
                        .option(ChannelOption.SO_BACKLOG, 1024)
                        // 接收緩衝區
                        .option(ChannelOption.SO_RCVBUF, 32*1024)
                        // 連線超時
                        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10*1000)
                        .childHandler(new ChildChannelHandle())
                        .bind(this.port)
                        .sync();
                future.channel().closeFuture().sync();

            }catch(Exception e){
                throw e;
            }finally {
                boss.shutdownGracefully();
                worker.shutdownGracefully();
            }
        }
    }

    static class ChildChannelHandle extends ChannelInitializer<SocketChannel> {

        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            ChannelPipeline pipeline = socketChannel.pipeline();
            // 字串編碼
            pipeline.addLast(worker,new StringEncoder());
            // 字串解碼
            pipeline.addLast(worker,new StringDecoder());
            // 自定義的handle, 狀態變化後進行處理的 handle
            pipeline.addLast(worker,new StatusHandle());
            // 自定義的handle, 現在是對讀取到的訊息進行處理
            pipeline.addLast(worker,new CustomHandle());
        }
    }
}
複製程式碼

客戶端的操作就簡單的使用終端來操作了

Netty使用及事件傳遞
這裡對 inactive 和 active 進行了狀態的輸出, 輸出接收資料並且原樣返回給客戶端

接下來看一下程式碼

  • CustomHandle

這裡對接收到的客戶端的資料進行處理

public class CustomHandle extends ChannelInboundHandlerAdapter {

    private Thread thread = Thread.currentThread();
  
		// 讀取到客戶端資料的事件
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
	      // 這裡簡單輸出一下,然後原樣返回給客戶端
        System.out.println(thread.getName()+": channelRead content : "+msg);
        ctx.writeAndFlush(msg);
    }
}
複製程式碼
  • StatusHandle

對狀態變化後進行處理的Handle(客戶端上下線事件)

public class StatusHandle extends ChannelInboundHandlerAdapter {
    private Thread thread = Thread.currentThread();
    private String ip;

		// 客戶端上線事件
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        this.ip = ctx.channel().remoteAddress().toString();
        System.out.println(thread.getName()+": ["+this.ip+"] channelActive -------");
    }

		// 客戶端下線事件
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(thread.getName()+": ["+this.ip+"] channelInactive -------");
    }
}
複製程式碼

上面標記了兩個地方, 是接下來要講的地方

1.NioServerSocketChannel作用相當於NIO ServerSocketChannel 2.ChildChannelHandle extends ChannelInitializer , 實現 initChannel 方法,這個東西延伸到一個重要的概念,Netty的事件傳遞 Pipeline


NioServerSocketChannel

這個類是 Netty 用於服務端的類,用於接收客戶端連線等. 用過NIO的同學都知道, serverSocket開啟的時候,需要註冊 ACCEPT 事件來監聽客戶端的連線

  • (小插曲)下面是Java NIO 的事件(netty基於NIO,自然也會有跟NIO一樣的事件)

  • public static final int OP_READ = 1 << 0; // 讀訊息事件

  • public static final int OP_WRITE = 1 << 2; // 寫訊息事件

  • public static final int OP_CONNECT = 1 << 3; // 連線就緒事件

  • public static final int OP_ACCEPT = 1 << 4; // 新連線事件

先看一下 NioServerSocketChannel 的繼承類圖

Netty使用及事件傳遞
從上面的demo的 channel(NioServerSocketChannel.class) 開始說起吧,可以看到是工廠生成channel.

 public B channel(Class<? extends C> channelClass) {
        if (channelClass == null) {
            throw new NullPointerException("channelClass");
        } else {
            return this.channelFactory((io.netty.channel.ChannelFactory)(new ReflectiveChannelFactory(channelClass)));
        }
    }
複製程式碼

工廠方法生成 NioServerSocketChannel 的時候呼叫的構造方法:

public NioServerSocketChannel(ServerSocketChannel channel) {
        super(null, channel, SelectionKey.OP_ACCEPT);
        config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
複製程式碼

繼續往下跟,跟到 AbstractNioChannel 的構造方法:

    protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
        super(parent);
        this.ch = ch;
        // 記住這個地方記錄了 readInterestOp
        this.readInterestOp = readInterestOp;
        try {
        		// 設定為非阻塞
            ch.configureBlocking(false);
        } catch (IOException e) {
            try {
                ch.close();
            } catch (IOException e2) {
                if (logger.isWarnEnabled()) {
                    logger.warn(
                            "Failed to close a partially initialized socket.", e2);
                }
            }

            throw new ChannelException("Failed to enter non-blocking mode.", e);
        }
    }
複製程式碼

回到 ServerBootstrap 的鏈式呼叫, 接下來看 bind(port) 方法,一路追蹤下去,會看到

private ChannelFuture doBind(final SocketAddress localAddress) {
  			// 初始化和註冊
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        if (regFuture.isDone()) {
            ChannelPromise promise = channel.newPromise();
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
            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) {
                        promise.setFailure(cause);
                    } else {
                        promise.registered();
                        doBind0(regFuture, channel, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }
複製程式碼

看 initAndRegister 方法

final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
            channel = channelFactory.newChannel();
            init(channel);
        } catch (Throwable t) {
            if (channel != null) {
                channel.unsafe().closeForcibly();
                return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
            }
            return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
        }
  			// 看到這裡的註冊, 繼續往下看
        ChannelFuture regFuture = config().group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }
        return regFuture;
    }
複製程式碼

config().group().register(channel); 往下看, 追蹤到 AbstractChannel 的 register --> regist0(promise) (由於呼叫太多,省去了中間的一些呼叫程式碼)

private void register0(ChannelPromise promise) {
            try {
                // check if the channel is still open as it could be closed in the mean time when the register
                // call was outside of the eventLoop
                if (!promise.setUncancellable() || !ensureOpen(promise)) {
                    return;
                }
                boolean firstRegistration = neverRegistered;
                // 執行註冊
                doRegister();
                neverRegistered = false;
                registered = true;

                // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
                // user may already fire events through the pipeline in the ChannelFutureListener.
	              // 這裡官方也說得很清楚了,確保我們在使用 promise 的通知之前真正的呼叫了 pipeline 中的 handleAdded 方法, 下面第二點  
                pipeline.invokeHandlerAddedIfNeeded();

                safeSetSuccess(promise);
                // 先呼叫 regist 方法
                pipeline.fireChannelRegistered();
                // Only fire a channelActive if the channel has never been registered. This prevents firing
                // multiple channel actives if the channel is deregistered and re-registered.
                // 只有 channel 之前沒有註冊過才會呼叫 channelActive
                // 這裡防止 channel deregistered(登出) 和 re-registered(重複呼叫 regist) 的時候多次呼叫 channelActive
                if (isActive()) {
                    if (firstRegistration) {
                        // 執行 channelActive 方法
                        pipeline.fireChannelActive();
                    } else if (config().isAutoRead()) {
                        // This channel was registered before and autoRead() is set. This means we need to begin read
                        // again so that we process inbound data.
                        //
                        // channel 已經註冊過 並且 已經設定 autoRead().這意味著我們需要開始再次讀取才能處理 inbound 的資料
                        // See https://github.com/netty/netty/issues/4805
                        beginRead();
                    }
                }
            } catch (Throwable t) {
                // Close the channel directly to avoid FD leak.
                closeForcibly();
                closeFuture.setClosed();
                safeSetFailure(promise, t);
            }
        }

複製程式碼

看到 doRegister() 方法,繼續跟下去, 跟蹤到 AbstractNioChannel 的 doRegister() 方法

protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
                // 這裡呼叫java的 NIO 註冊 
                selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
                return;
            } catch (CancelledKeyException e) {
                if (!selected) {
                    eventLoop().selectNow();
                    selected = true;
                } else {
                    throw e;
                }
            }
        }
    }

複製程式碼

寫過NIO的同學應該熟悉上面的這句話:

selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);

複製程式碼

這裡就是呼叫了java NIO的註冊, 至於為什麼註冊的時候 ops = 0 , 繼續追蹤下去,此處省略一堆呼叫....(實在是過於繁雜)最後發現, 最終都會呼叫 AbstractNioChannel 的 doBeginRead() 方法修改 selectionKey 的 interestOps, 客戶端連線後,註冊的讀事件在這裡也是相同的操作.

protected void doBeginRead() throws Exception {
    // Channel.read() or ChannelHandlerContext.read() was called
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    readPending = true;

    final int interestOps = selectionKey.interestOps();
  	// // 這裡是判斷有沒有註冊過相同的事件,沒有的話才修改 ops
    if ((interestOps & readInterestOp) == 0) {
        // 就是這裡, 記得剛才註冊的時候,ops == 0 嗎, this.readInterestOp 在上面的初始化的時候賦了值
        // 與 0 邏輯或, 所以最終值就是 this.readInterestOp , 註冊事件的數值 不清楚的話可以看一下最上面
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

複製程式碼

上面介紹了服務端的 ACCEPT 事件, 接下來是客戶端連線的過程.

上面介紹的 服務端 ACCEPT 最後呼叫的 NIO 的 register 方法, read也是呼叫nio的register, 但是在 SocketChannel(client) 呼叫register 之前, 服務端是有一個 server.accept() 方法獲取客戶端連線, 以此為契機, 最後在 NioServerSocketChannel 裡面找到了accept 方法.

// 1
    protected int doReadMessages(List<Object> buf) throws Exception {
        // accept 客戶端, 傳入 serverSocketChannel
        SocketChannel ch = SocketUtils.accept(javaChannel());

        try {
            if (ch != null) {
                // 建立新的 Netty 的 Channel , 並設定 ops =1 (read), 最後再呼叫 doBeginRead的時候就會修改 ops 的值 , 跟 server 的一樣
                buf.add(new NioSocketChannel(this, ch));
                return 1;
            }
        } catch (Throwable t) {
            logger.warn("Failed to create a new channel from an accepted socket.", t);

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

        return 0;
    }
    // 2
    public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException {
        try {
            return AccessController.doPrivileged(new PrivilegedExceptionAction<SocketChannel>() {
                @Override
                public SocketChannel run() throws IOException {
                		// nio 的方法
                    return serverSocketChannel.accept();
                }
            });
        } catch (PrivilegedActionException e) {
            throw (IOException) e.getCause();
        }
    }
複製程式碼

客戶端連線的時候,會觸發上面的 server.accept(), 然後會觸發 AbstractChannel 的 register 方法 從而呼叫

AbstractChannel.this.pipeline.fireChannelRegistered();// 這個方法呼叫下面的兩個方法
複製程式碼
static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRegistered();
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelRegistered();
                }
            });
        }
    }

    private void invokeChannelRegistered() {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelRegistered(this);
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireChannelRegistered();
        }
    }
複製程式碼

中間省略掉了一些呼叫(不得不說Netty的呼叫有點複雜..), 最後會呼叫到 我們接下來要講的 ChannelInitializer的 handlerAdded方法.這些都跟一個 pipeline 相關.

Pipeline

pipeline ,事件傳輸管道(傳輸途徑啥的都行),它是一條 handle 訊息傳遞鏈.

先看一下 AbstractChannelHandlerContext 中的 兩個方法

    // 查詢下一個 inboundHandle (從當前位置往後查詢 intBound)
    private AbstractChannelHandlerContext findContextInbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.next; // 往後查詢
        } while (!ctx.inbound);
        return ctx;
    }

    // 查詢下一個 OutboundHandle (從當前位置往前查詢 outBound )
    private AbstractChannelHandlerContext findContextOutbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.prev; // 往前查詢
        } while (!ctx.outbound);
        return ctx;
    }
複製程式碼

為什麼是這樣呢,我們從 AbstractChannelHandleContext 的 read 和 write 兩個方法進入檢視

AbstractChannelHandleContext

read

    @Override
    public ChannelHandlerContext fireChannelRead(final Object msg) {
        // findContextInbound() 就是上面2個方法的第一個,結果是往後找到一個 inbound 的  handle
        // 呼叫的下面的方法
        invokeChannelRead(findContextInbound(), msg);
        return this;
    }
	//當讀資料(入棧)事件的時候, 從當前位置往後查詢 InBoundHandleHandle, 觸發下一個 InBoundHandleHandle 的 channelRead 事件
    static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(new Runnable() {
                public void run() {
                    next.invokeChannelRead(m);
                }
            });
        }
    }
複製程式碼

write

// 當寫資料(出棧)事件的時候, 從當前的 handle 往前查詢到 OutBoundHandle, 觸發前一個 OutBoundHandle 的 write 事件傳遞
  private void write(Object msg, boolean flush, ChannelPromise promise) {
    AbstractChannelHandlerContext next = this.findContextOutbound();
    Object m = this.pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
      if (flush) {
        next.invokeWriteAndFlush(m, promise);
      } else {
        next.invokeWrite(m, promise);
      }
    } else {
      Object task;
      if (flush) {
        task = AbstractChannelHandlerContext.WriteAndFlushTask.newInstance(next, m, promise);
      } else {
        task = AbstractChannelHandlerContext.WriteTask.newInstance(next, m, promise);
      }

      if (!safeExecute(executor, (Runnable)task, promise, m)) {
        ((AbstractChannelHandlerContext.AbstractWriteTask)task).cancel();
      }
    }
  }
複製程式碼

一個平時可能不會注意到到地方:

上面講的是 AbstractChannelHandleContext 中的方法, AbstractChannel 中也有 write 方法 ,接下來我們就講 ctx.write() 和 channel.write() 這兩個方法呼叫的區別

ctx.write vs channel.write

AbstractChannelHandleContext#ctx.write()

    public ChannelFuture write(Object msg, ChannelPromise promise) {
        if (msg == null) {
            throw new NullPointerException("msg");
        } else {
            try {
                if (this.isNotValidPromise(promise, true)) {
                    ReferenceCountUtil.release(msg);
                    return promise;
                }
            } catch (RuntimeException var4) {
                ReferenceCountUtil.release(msg);
                throw var4;
            }
          	// 呼叫下面那個方法
            this.write(msg, false, promise);
            return promise;
        }
    }

    private void write(Object msg, boolean flush, ChannelPromise promise) {
        // 找到下一個 outboundHandle
        AbstractChannelHandlerContext next = this.findContextOutbound();
        Object m = this.pipeline.touch(msg, next);
        EventExecutor executor = next.executor();
        // 後面程式碼太多省略
    ...
    }

複製程式碼

AbstractChannel#channel.write()

    // ctx.channel().write()
    public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
        // 呼叫下面的方法	
        return this.pipeline.writeAndFlush(msg, promise);
    }
    public final ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
        // tail 是最末尾的一個handle
        return this.tail.writeAndFlush(msg, promise);
    }
複製程式碼

總結: ctx.write() 這個方法是在 當前 handle 開始往前查詢 OutBoundHandle進行事件傳遞, 而 channel.write() 是從 pipeline的最後一個handle(tail)往前查詢 OutBoundHandle 進行事件傳遞, 所以 channel.write() 傳遞的事件會經過所有的 OutBoundHandle .


// 字串編碼
pipeline.addLast(worker,new StringEncoder()); // 1.outbound
// 字串解碼
pipeline.addLast(worker,new StringDecoder()); // 2.inbound
// 自定義的handle, 狀態變化後進行處理的 handle
pipeline.addLast(worker,new StatusHandle()); // 3.inbound
// 自定義的handle, 現在是對讀取到的訊息進行處理
pipeline.addLast(worker,new CustomHandle()); // 4.inbound
複製程式碼

我們上面4個 handle 新增的順序為 out, in , in, in , 所以最終呼叫的話,會變成下面這樣(handle前面的數字僅僅是新增進pipeline的順序)

Netty使用及事件傳遞

ChannelInitializer

這個類見上面的 ChildChannelHandle 類.
當 channel(客戶端通道)一旦被註冊,將會呼叫這個方法,這個方法用來初始化客戶端的pipeline, 並且在方法返回的時候, 這個例項(ChannelInitializer)將會被從 ChannelPipeline (客戶端的 pipeline) 中移除.
原文:

public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter

/**
 * This method will be called once the {@link Channel} was registered. After the method returns this instance  
 * will be removed from the {@link ChannelPipeline} of the {@link Channel}.
 *
 * @param ch            the {@link Channel} which was registered.
 * @throws Exception    is thrown if an error occurs. In that case it will be handled by
 *                      {@link #exceptionCaught(ChannelHandlerContext, Throwable)} which will by default close
 *                      the {@link Channel}.
 */
protected abstract void initChannel(C ch) throws Exception;
複製程式碼

initChannel

除了這個抽象方法, 這個類還有一個過載方法 ,具體實現就在這個方法裡.

 private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
        if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance.
            try {
              	// 第二步
              	// 這裡呼叫我們自己實現的那個抽象方法 , 將 我們前面定義的 handle 都加入到 client 的 pipeline 中
                initChannel((C) ctx.channel());
            } catch (Throwable cause) {
                exceptionCaught(ctx, cause);
            } finally {
              	// 第三步,將自己(ChannelInitializer)從pipeline中移除
              	// 所以客戶端的pipeline中就不會有 ChannelInitializer 這個handle(它也是個handle)
              	// 它的作用是用來初始化客戶端的pipeline中的handle
                remove(ctx);
            }
            return true;
        }
        return false;
    }

複製程式碼

remove

// 將自己從客戶端的pipeline中移除
private void remove(ChannelHandlerContext ctx) {
    try {
        ChannelPipeline pipeline = ctx.pipeline();
        if (pipeline.context(this) != null) {
            pipeline.remove(this);
       	}
   	} finally {
            initMap.remove(ctx);
        }
}
複製程式碼

這篇的程式碼有點多, 如果只是demo使用的話, 不需要花費什麼時間, 如果想要深入瞭解一下 Netty 的話, 可以從這裡的事件傳遞開始對原始碼的一點點分析.

最後

這次的內容到這裡就結束了,最後的最後,非常感謝你們能看到這裡!!你們的閱讀都是對作者的一次肯定!!!
覺得文章有幫助的看官順手點個贊再走唄(終於暴露了我就是來騙讚的(◒。◒)),你們的每個贊對作者來說都非常重要(異常真實),都是對作者寫作的一次肯定(double)!!!

相關文章