網路程式設計-Netty-Reactor模型

Yangsc_o發表於2020-06-08



# 摘要

在前兩篇《快速理解Linux網路I_O》、《java的I_O模型-BIO&NIO&AIO》兩邊中介紹了Linux下的I/O模型和java中的I/O模型,今天我們介紹Reactor模型,並探究Netty的實現

高效能伺服器

在網際網路時代,我們使用的軟體基本上全是C/S架構,C/S架構的軟體一個明顯的好處就是:只要有網路,你可以在任何地方幹同一件事。C/S架構可以抽象為如下模型:

image-20200608122739089

  • C就是Client(客戶端),上面的B是Browser(瀏覽器)
  • S就是Server(伺服器):伺服器管理某種資源,並且通過操作這種資源來為它的客戶端提供某種服務

那伺服器如何能快速的處理使用者的請求呢?在我看來高效能伺服器至少要滿足如下幾個需求:

  • 效率高:既然是高效能,那處理客戶端請求的效率當然要很高了
  • 高可用:不能隨便就掛掉了
  • 程式設計簡單:基於此伺服器進行業務開發需要足夠簡單
  • 可擴充套件:可方便的擴充套件功能
  • 可伸縮:可簡單的通過部署的方式進行容量的伸縮,也就是服務需要無狀態

而滿足如上需求的一個基礎就是高效能的IO!

Reactor模式

什麼是Reactor模式?

兩種I/O多路複用模式:Reactor和Proactor,兩個與事件分離器有關的模式是Reactor和Proactor。Reactor模式採用同步IO,而Proactor採用非同步IO。

在Reactor中,事件分離器負責等待檔案描述符或socket為讀寫操作準備就緒,然後將就緒事件傳遞給對應的處理器,最後由處理器負責完成實際的讀寫工作。

在Proactor模式中,處理器--或者兼任處理器的事件分離器,只負責發起非同步讀寫操作。IO操作本身由作業系統來完成。傳遞給作業系統的引數需要包括使用者定義的資料緩衝區地址和資料大小,作業系統才能從中得到寫出操作所需資料,或寫入從socket讀到的資料。事件分離器捕獲IO操作完成事件,然後將事件傳遞給對應處理器。

說人話的方式理解:

  • reactor:能收了你跟俺說一聲。
  • proactor: 你給我收十個位元組,收好了跟俺說一聲。

Doug Lea是這樣類比的

  • Reactor通過排程適當的處理程式來響應IO事件;
  • 處理程式執行非阻塞操作
  • 通過將處理程式繫結到事件來管理;

image-20200608173303651

Reactor單執行緒模型設計

image-20200608175038780

單執行緒版本Java NIO的支援:

  • Channels:與支援非阻塞讀取的檔案,套接字等的連線

  • Buffers:類似於陣列的物件,可由Channels直接讀取或寫入

  • Selectors:通知一組通道中哪一個有IO事件

  • SelectionKeys:維護IO事件狀態和繫結

  • Reactor 程式碼如下

public class Reactor implements Runnable {
    final Selector selector;
    final ServerSocketChannel serverSocketChannel;

    public Reactor(int port) throws IOException {
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        SelectionKey key = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        key.attach(new Acceptor());
    }

    @Override
    public void run() {
        while (!Thread.interrupted()) {
            try {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey selectionKey : selectionKeys) {
                    dispatch(selectionKey);
                }
                selectionKeys.clear();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void dispatch(SelectionKey selectionKey) {
        Runnable run = (Runnable) selectionKey.attachment();
        if (run != null) {
            run.run();
        }
    }

    class Acceptor implements Runnable {
        @Override
        public void run() {
            try {
                SocketChannel channel = serverSocketChannel.accept();
                if (channel != null) {
                    new Handler(selector, channel);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new Thread(
                new Reactor(1234)
        ).start();
    }

}
  • Handler程式碼如下:
public class Handler implements Runnable{
    private final static int DEFAULT_SIZE = 1024;
    private final SocketChannel socketChannel;
    private final SelectionKey seletionKey;
    private static final int READING = 0;
    private static final int SENDING = 1;
    private int state = READING;

    ByteBuffer inputBuffer = ByteBuffer.allocate(DEFAULT_SIZE);
    ByteBuffer outputBuffer = ByteBuffer.allocate(DEFAULT_SIZE);

    public Handler(Selector selector, SocketChannel channel) throws IOException {
        this.socketChannel = channel;
        socketChannel.configureBlocking(false);
        this.seletionKey = socketChannel.register(selector, 0);
        seletionKey.attach(this);
        seletionKey.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    @Override
    public void run() {
        if (state == READING) {
            read();
        } else if (state == SENDING) {
            write();
        }
    }


    private void write() {
        try {
            socketChannel.write(outputBuffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        while (outIsComplete()) {
            seletionKey.cancel();
        }
    }

    private void read() {
        try {
            socketChannel.read(inputBuffer);
            if (inputIsComplete()) {
                process();
                System.out.println("接收到來自客戶端(" + socketChannel.socket().getInetAddress().getHostAddress()
                        + ")的訊息:" + new String(inputBuffer.array()));
                seletionKey.attach(new Sender());
                seletionKey.interestOps(SelectionKey.OP_WRITE);
                seletionKey.selector().wakeup();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public boolean inputIsComplete() {
        return true;
    }
    public boolean outIsComplete() {
        return true;
    }


    public void process() {
        // do something...
    }

    class Sender implements Runnable {
        @Override
        public void run() {
            try {
                socketChannel.write(outputBuffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (outIsComplete()) {
                seletionKey.cancel();
            }
        }
    }

}

這個模型和上面的NIO流程很類似,只是將訊息相關處理獨立到了Handler中去了!雖然說到NIO一個執行緒就可以支援所有的IO處理。但是瓶頸也是顯而易見的!如果這個客戶端多次進行請求,如果在Handler中的處理速度較慢,那麼後續的客戶端請求都會被積壓,導致響應變慢!所以引入了Reactor多執行緒模型!

Reactor多執行緒模型設計

Reactor多執行緒模型就是將Handler中的IO操作和非IO操作分開,操作IO的執行緒稱為IO執行緒,非IO操作的執行緒稱為工作執行緒!這樣的話,客戶端的請求會直接被丟到執行緒池中,客戶端傳送請求就不會堵塞!

image-20200608190105063

Reactor保持不變,僅需要改動Handler程式碼:

public class Handler implements Runnable{
    private final static int DEFAULT_SIZE = 1024;
    private final SocketChannel socketChannel;
    private final SelectionKey seletionKey;
    private static final int READING = 0;
    private static final int SENDING = 1;
    private int state = READING;

    ByteBuffer inputBuffer = ByteBuffer.allocate(DEFAULT_SIZE);
    ByteBuffer outputBuffer = ByteBuffer.allocate(DEFAULT_SIZE);

    private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());
    private static final int PROCESSING = 3;
    private Selector selector;


    public Handler(Selector selector, SocketChannel channel) throws IOException {
        this.selector = selector;
        this.socketChannel = channel;
        socketChannel.configureBlocking(false);
        this.seletionKey = socketChannel.register(selector, 0);
        seletionKey.attach(this);
        seletionKey.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    @Override
    public void run() {
        if (state == READING) {
            read();
        } else if (state == SENDING) {
            write();
        }
    }

    private void write() {
        try {
            socketChannel.write(outputBuffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        while (outIsComplete()) {
            seletionKey.cancel();
        }
    }

    private void read() {
        try {
            socketChannel.read(inputBuffer);
            if (inputIsComplete()) {
                process();
                executorService.execute(new Processer());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public boolean inputIsComplete() {
        return true;
    }
    public boolean outIsComplete() {
        return true;
    }


    public void process() {
        // do something...
    }

    class Sender implements Runnable {
        @Override
        public void run() {
            try {
                socketChannel.write(outputBuffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (outIsComplete()) {
                seletionKey.cancel();
            }
        }
    }

    synchronized void processAndHandOff() {
        process();
        // or rebind attachment
        state = SENDING;
        seletionKey.interestOps(SelectionKey.OP_WRITE);
        selector.wakeup();
    }

    class Processer implements Runnable {
        @Override
        public void run() {
            processAndHandOff();
        }
    }

}

主從Reactor多執行緒模型設計

主從Reactor多執行緒模型是將Reactor分成兩部分,mainReactor負責監聽server socket,accept新連線,並將建立的socket分派給subReactor。subReactor負責多路分離已連線的socket,讀寫網路資料,對業務處理功能,其扔給worker執行緒池完成。通常,subReactor個數上可與CPU個數等同:

image-20200608222654773

Handler保持不變,僅需要改動Reactor程式碼:

public class Reactor {
    // also create threads
    Selector[] selectors;
    AtomicInteger next = new AtomicInteger(0);
    final ServerSocketChannel serverSocketChannel;

    private static ExecutorService sunReactors = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());
    private static final int PROCESSING = 3;

    public Reactor(int port) throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        selectors = new Selector[4];
        for (int i = 0; i < selectors.length; i++) {
            Selector selector = selectors[i];
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            serverSocketChannel.configureBlocking(false);
            SelectionKey key = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            key.attach(new Acceptor());
            new Thread(()->{
                while (!Thread.interrupted()) {
                    try {
                        selector.select();
                        Set<SelectionKey> selectionKeys = selector.selectedKeys();
                        for (SelectionKey selectionKey : selectionKeys) {
                            dispatch(selectionKey);
                        }
                        selectionKeys.clear();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }



    private void dispatch(SelectionKey selectionKey) {
        Runnable run = (Runnable) selectionKey.attachment();
        if (run != null) {
            run.run();
        }
    }

    class Acceptor implements Runnable {
        @Override
        public void run() {
            try {
                SocketChannel channel = serverSocketChannel.accept();
                if (channel != null) {
                    sunReactors.execute(new Handler(selectors[next.getAndIncrement() % selectors.length], channel));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new Reactor(1234);
    }

}

以上是三種不同的設計思路,接下來看一下Netty這個一個高效能NIO框架,其是如何實現Reactor模型的!

Netty Reactor模型設計

  • 看一個最簡單的Netty服務端程式碼
public final class EchoServer {
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
    public static void main(String[] args) throws Exception {
        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        final EchoServerHandler serverHandler = new EchoServerHandler();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     p.addLast(serverHandler);
                 }
             });
            ChannelFuture f = b.bind(PORT).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
  • Netty Server Handler
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

我們從Netty伺服器程式碼來看,與Reactor模型進行對應!

  • EventLoopGroup就相當於是Reactor,bossGroup對應主Reactor,workerGroup對應從Reactor
  • TimeServerHandler就是Handler
  • child開頭的方法配置的是客戶端channel,非child開頭的方法配置的是服務端channel

參考

Scalable IO in Java

高效能Server---Reactor模型

NIO技術概覽


你的鼓勵也是我創作的動力

打賞地址

相關文章