Reactor模式

shigp1發表於2024-09-28

Reactor模式

許多高效能的伺服器軟體離不開Reactor模式.像高效能快取Redis,高效能web伺服器Nginx,高效能的網路元件Netty,高效能的訊息中介軟體Kafka,RocketMQ等.

那什麼是Reactor模式呢?借用Doug Lea大師的話來說,就是:

Reactor模式由Reactor執行緒,Handles處理器兩大角色組成,它們的職責分別是:

1.Reactor執行緒負責響應IO事件,並且將IO事件分發到Handles處理器
2.Handles執行緒:IO的讀取,業務邏輯處理,寫入

那為什麼會產生Reactor模式呢?這就不得不說起OIO了.OIO又叫BIO,阻塞IO,像ServerScoket,Socket的read和write都會阻塞執行緒,直到完成IO操作. 系統的吞吐量特別低,每個IO操作都會阻塞其他的操作.為了解決這個問題,後面引入了每個連線一個執行緒.由每個執行緒處理一個連線的IO讀,業務處理,IO寫.這樣每個連線在IO操作阻塞時不會影響其他的執行緒.

在系統的連線數少時沒有問題,當連線數越來越多時,執行緒的數量就越來越多.對系統的資源消耗太高.

為了解決以上的問題,需要控制執行緒的數量.假設一個執行緒處理大量的連線,就可以控制系統資源的使用,同時提高系統的吞吐量.

單執行緒的Reactor模式

如果Reactor執行緒和Handles執行緒是同一個執行緒,就是最簡單的Reactor模式了.

來看個例子,實現個Echo服務,簡單返回客戶端傳送的資料.

服務端:

public interface Handler {
    void handle() throws IOException;
}


public class AcceptorHandler implements Handler{

    ServerSocketChannel serverSocketChannel;

    Selector selector;

    public AcceptorHandler(ServerSocketChannel serverSocketChannel,
                           Selector selector) {
        this.serverSocketChannel = serverSocketChannel;
        this.selector = selector;
    }

    @Override
    public void handle() {
        try {
            SocketChannel channel = serverSocketChannel.accept();

            channel.configureBlocking(false);

            SelectionKey selectionKey = channel.register(selector, 0);
            selectionKey.attach(new IOHandler(channel, selector,selectionKey));
            selectionKey.interestOps(SelectionKey.OP_READ);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}

public class IOHandler implements Handler {

    Selector selector;

    SocketChannel socketChannel;

    SelectionKey selectionKey;
    ByteBuffer byteBuffer = ByteBuffer.allocate(2048);

    public IOHandler(SocketChannel socketChannel,Selector selector,
                     SelectionKey selectionKey) {
        this.selector = selector;
        this.socketChannel = socketChannel;
        this.selectionKey = selectionKey;
    }

    @Override
    public void handle() {
        if (selectionKey.isReadable()) {
            try {
                while (socketChannel.read(byteBuffer) > 0) {
                    byteBuffer.flip();
                    System.out.println("讀的內容是"+ new String(byteBuffer.array(),byteBuffer.position(),byteBuffer.limit()));
                }

                SelectionKey selectionKey1 = selectionKey.interestOps(SelectionKey.OP_WRITE);
                selectionKey1.attach(this);
            } catch (IOException e) {
                try {
                    socketChannel.close();
                } catch (IOException ex) {

                }
            }
        }else if (selectionKey.isWritable()) {
            try {
                while (socketChannel.write(byteBuffer) > 0) {

                }

                byteBuffer.clear();

                SelectionKey selectionKey1 = selectionKey.interestOps(SelectionKey.OP_READ);
                selectionKey1.attach(this);
            } catch (Exception e) {
                try {
                    socketChannel.close();
                } catch (IOException ex) {

                }
            }
        }
     }
}

public class EchoServer {

    public static void main(String[] args) {
        try {
            startServer();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void startServer() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        serverSocketChannel.configureBlocking(false);

        serverSocketChannel.bind(new InetSocketAddress("localhost",10700));

        Selector selector = Selector.open();

        SelectionKey selectionKey = serverSocketChannel.register(selector, 0);
        selectionKey.attach(new AcceptorHandler(serverSocketChannel,selector));
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        while (!Thread.interrupted()) {
            int select = selector.select();
            if (select > 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();

                Iterator<SelectionKey> iterator = selectionKeys.iterator();

                while (iterator.hasNext()) {
                    SelectionKey selectionKey1 = iterator.next();

                    Handler handler = (Handler) selectionKey1.attachment();
                    handler.handle();
                }
                selectionKeys.clear();
            }
        }

        serverSocketChannel.close();
        selector.close();
    }
}

客戶端:

public class EchoClient {

    private static final ByteBuffer byteBuffer = ByteBuffer.allocate(2048);

    public static void main(String[] args) {
        try {
            startClient();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void startClient() throws Exception {
        SocketChannel socketChannel = SocketChannel.open();

        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("localhost",10700));

        while (!socketChannel.finishConnect()) {

        }

        System.out.println("成功與伺服器建立連線");

        Scanner scanner = new Scanner(System.in);
        System.out.print("請輸入一行:");
        while (scanner.hasNext()) {
            String s = scanner.nextLine();

            byteBuffer.clear();
            byteBuffer.put(s.getBytes(StandardCharsets.UTF_8));

            byteBuffer.flip();

            while(socketChannel.write(byteBuffer)>0) {

            }


            byteBuffer.clear();
            while (socketChannel.read(byteBuffer) == 0) {

            }

            byteBuffer.flip();
            System.out.println("從伺服器接收資料:"+new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()));

            System.out.print("請輸入一行:");
        }

        socketChannel.close();
    }
}

可以看到一個執行緒管理了很多連線,解決了每個連線一個執行緒的系統資源消耗的問題.可以看出,缺點就是進行IO操作時還會阻塞其他的執行緒.

多執行緒Reactor模式

在單執行緒的Reactor模式加上多執行緒來改進阻塞問題:
1.Handle加多執行緒,考慮使用執行緒池
2.Reactor加多執行緒,引入多個Selector

現在看下多執行緒版本的Reactor實現的Echo服務:

伺服器

public class MultiAcceptorHandler implements Handler {

    ServerSocketChannel serverSocketChannel;

    Selector selector;

    ExecutorService executorService;

    public MultiAcceptorHandler(ServerSocketChannel serverSocketChannel,
                                Selector selector,ExecutorService executorService) {
        this.serverSocketChannel = serverSocketChannel;
        this.selector = selector;
        this.executorService = executorService;
    }

    @Override
    public void handle() {
        try {
            SocketChannel channel = serverSocketChannel.accept();

            channel.configureBlocking(false);

            new MultiIOHandler(channel, selector,executorService);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}

public class MultiIOHandler implements Handler {

    final Selector selector;

    final SocketChannel socketChannel;

    final SelectionKey selectionKey;

    final ExecutorService executorService;
    final ByteBuffer byteBuffer = ByteBuffer.allocate(2048);

    int read = 0;
    public MultiIOHandler(SocketChannel socketChannel, Selector selector,
                           ExecutorService executorService) {
        this.selector = selector;
        this.socketChannel = socketChannel;
        this.executorService = executorService;

        try {
            this.selectionKey = socketChannel.register(selector, 0);
            this.socketChannel.configureBlocking(false);
            this.selectionKey.attach(this);
            this.selectionKey.interestOps(SelectionKey.OP_READ);
            selector.wakeup();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void handle() {
        executorService.submit(this::syncRun);
     }

     public synchronized void syncRun() {
         if (read==0) {
             try {
                 while (socketChannel.read(byteBuffer) > 0) {
                     System.out.println("讀的內容是"+ new String(byteBuffer.array(),0,byteBuffer.position()));
                 }
                 byteBuffer.flip();
                 System.out.println("讀取"+byteBuffer.position()+","+byteBuffer.limit());
                 selectionKey.interestOps(SelectionKey.OP_WRITE);

                 read=1;
                 selector.wakeup();
             } catch (IOException e) {
                 try {
                     socketChannel.close();
                 } catch (IOException ex) {

                 }
             }
         }else {
             try {
                 while (socketChannel.write(byteBuffer) > 0) {

                 }

                 byteBuffer.clear();

                 selectionKey.interestOps(SelectionKey.OP_READ);
                 read=0;
                 selector.wakeup();
             } catch (Exception e) {
                 try {
                     socketChannel.close();
                 } catch (IOException ex) {

                 }
             }
         }
     }
}

public class SelectorThread implements Runnable {

    Selector selector;

    int index;

    public SelectorThread(Selector selector,int index) {
        this.selector = selector;
        this.index = index;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
//                System.out.println(index+"正在執行");
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();

                Iterator<SelectionKey> iterator = selectionKeys.iterator();

                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();

                    Handler handler = (Handler) selectionKey.attachment();

                    if (handler != null) {
                        handler.handle();
                    }
                }

                selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}


public class MultiEchoServer {

    public static void main(String[] args) {
        try {
            startServer();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void startServer() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        serverSocketChannel.configureBlocking(false);

        serverSocketChannel.bind(new InetSocketAddress("localhost",10700));

        Selector[] selectors = new Selector[2];

        SelectorThread[] selectorThreads = new SelectorThread[2];

        for (int i = 0; i < 2; i++) {
            selectors[i] = Selector.open();
        }

        ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

        SelectionKey selectionKey = serverSocketChannel.register(selectors[0], 0);
        selectionKey.attach(new MultiAcceptorHandler(serverSocketChannel,selectors[1],executorService));
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        for (int i = 0; i < 2; i++) {
            selectorThreads[i] = new SelectorThread(selectors[i],i);
            new Thread(selectorThreads[i]).start();
        }

		//        Thread closeThread = new Thread(() -> {
		//            try {
		//                executorService.awaitTermination(5, TimeUnit.SECONDS);
		//
		//                executorService.shutdown();
		//                serverSocketChannel.close();
		//
		//                for (int i= 0; i < 2; i++) {
		//                    selectors[i].close();
		//                }
		//            } catch (Exception e) {
		//                e.printStackTrace();
		//            }
		//        });
		//
		//        Runtime.getRuntime().addShutdownHook(closeThread);
			}
		}

Selector[] selectors = new Selector[2]使用了兩個Selector,第一個用於接收客戶端的連線,註冊的Handle是MultiAcceptorHandler;接收連線後將處理IO註冊到第二個Selector, 第二個用於處理IO讀取和寫入,註冊的Handle是MultiIOHandler.每個連線的IO處理也是用執行緒池處理的.

注意:在MultiIOHandler不能用selectionKey.isReadable()來判斷是否是可讀,增加了read標誌來判斷.

優缺點

優點

1.響應快,雖然Reactor執行緒是同步的,但是不會被IO操作所阻塞
2.程式設計簡單
3.可擴充套件,可以加多執行緒充分利用CPU資源

缺點

1.有一定複雜性
2.依賴作業系統支援
3.同一個Handle中出現長時間讀寫,會造成Reactor執行緒的其他通道的IO處理

相關文章