Reactor反应器模式

什么是reactor反应器模式

反应器模式由Reactor反应器线程、Handler处理器两个角色组成:

  1. Reactor反应器线程:负责响应IO事件,并且分发到Handler处理器。
  2. Handler处理器:非阻塞的执行业务处理逻辑。

反应器模式从简单到复杂,有很多版本,根据上面的定义,仅仅是最简单的一个版本。如果要了解反应器模式,还要从OIO编程开始说起。

while(true)}{
    socket = accept();//阻塞,接收链接
    handle(socket);//读取数据,处理业务,返回结果
}

这种方式最大的问题是:前一个网络链接的业务处理还没完成,那么后面的连接请求没法被接受,于是后面的请求会被阻塞住,服务器的吞吐量就低了。

为了解决连接阻塞问题,出现了一个Connection Per Thread模式。一个线程处理一个连接。

class ConnectionPerThread implements Runnable{
    public void run(){
        try{
            ServerSocket serverSocket = new ServerSocket(2222);
            while(!Thread.interrupted){
                Socket socket = serverSocket.accpet;
                //接收一个连接后,为socket连接,新建一个线程
                Handler handler = new Handler(socket);
                new Thread(handler).start();
            }
        }
    }
    
    static class Handler implements Runable{
        final Socket socket;
        Handler(Socket s){
            this.socket = s;
        }
        public void run(){
            while(true){
                try{
                    byte[] input = new byte[1024];
                    //读取数据
                    socket.getInputStream().read(input);
                    byte[] output = new byte[1024];
                    socket.getOutputStream().write(output);
                }catcah(Exception e){
                    //异常处理
                }
            }
        }
    }
}

对于每一个新的网络链接都分配一个线程。每个线程都独自处理自己负责的输入和输出。服务器的监听线程也是独立,任务的socket连接的输入和输出处理,不会阻塞到后面新socket连接的监听和建立。

Connection Per Thread模式的优点:解决了新连接被严重阻塞的问题。

一个问题:一个线程同时负责处理多个socket连接的逻辑,可以吗?当然可以,但是传统的OIO编程中每个socket的IO读写操作都是阻塞的。在同一时刻,一个线程里只能处理一个socket,前一个socket被阻塞了,后面连接的IO操作是无法被并发执行的。

Connection Per Thread缺点:对应于大量的链接,需要耗费大量的线程资源,对线程资源要求太高。在系统中,线程是昂贵的系统资源。如果线程数太多,系统无法承受。而且,线程的反复创建、销毁、线程的切换也需要代价。

使用Reactor反应器模式可以解决上述问题。用反应器模式对线程的数量进行控制,做到一个线程处理大量的连接。

单线程的Reactor反应器模式

反应器模式的两个重要组件:

Reactor反应器:负责查询IO事件,当检测到一个IO事件,将其发送给相应的Handler处理器去处理。这里的IO事件,就是NIO中选择器监控的通道IO事件。

Handler处理器:与IO事件绑定,负责IO事件的处理。完成真正的连接建立、通道的读取、处理业务逻辑、负责将结果写出到通道等。

基于java NIO实现简单的单线程版本的反应器模式,的核心方法是attach(Object o)和attachment(),

两个方法分别是SelecttionKey选择键添加附件和取出附件,附件就是Handle处理器,当IO事件发生,选择键被select方法选到,就可以将附件取出,完成业务处理。

class Reactor implements Runable{
    Selector selector;
    ServerSocketChannel serverSocketChannel;
    EchoServerReactor() throws IOException{
        // 打开选择器、连接监听通道
        。。。
        //注册serverSocket的accpet事件
        SelectionKey sk = serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
        //将新连接处理器作为附件,绑定到sk选择器
        sk.attach(new AcceptorHandler());
    }
    
    public void run(){
        try{
            while(!Thread.interrupted()){
                selector.select();
                Set selected = selector.selectedKeys();
                Iterator it = selected.iterator();
                while(it.hasNext()){
                    //反应器负责dispatch收到的事件
                    SelectionKey sk = it.next();
                    dispatch(sk);
                }
                selected.clear;
            }
        }catch(IOException ex){}
    }
    
    //反应器的分发方法
    void dispatch(SelectionKey k){
        Runable handler = (Runable) (k.attachment());
        if(handler!=null){
            handler.run();
        }
    }
    
    //新连接处理器
    class AcceptorHandler implements Runable{
        public void run(){
            //接收新连接
            //需要为新连接,创建一个输入输出的handler处理器
        }
    }
}

上面代码中,设计了一个Handler处理器,叫做AcceptorHandler处理器。在注册serverSocket服务监听连接的接收事件之后,创建一个AcceptorHandler新连接处理器的实例,作为附件,被设置到了SelectionKey中。

当连接事件发生后,取出了之前attach到SelectionKey中的Handler业务处理器,进行socket的各种IO处理。

AcceptorHandler处理器的两大职责:一是接收新连接,二是在为新连接创建一个输入输出的Handler处理器,叫做IOHandler。

IOHandler就是负责socket的数据输入、业务处理,结果输出。

class IOHandler implements Runable{
    final SocketChannel channel;
    final SelectionKey sk;
    IOHandler(Selector selector,SocketChannel c) throws IOException{
        channel = c;
        c.configureBlocking(false);
        //取得选择键
        sk = channel.register(selector,0);
        //设置附件
        sk.attach(this);
        //注册读写就绪事件
        sk.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
    }
    
    public void run(){
        //处理输入和输出
    }
}

在IOHandler的构造器中,有两点比较重要:

  1. 将新的SocketChannel传输通道,注册到了反应器Reactor类的同一个选择器中。
  2. Chanel传输通道注册完成后,将IOHandler自身作为附件,attach到了选择键中。这样,在Reactor类分发事件时,能执行到IOHandler的run方法。

代码示例

package com.io.test;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @ClassName EchoServerReactor
 * @Description TODO
 * @Author djl
 * @Date 2020-02-08 21:04
 * @Version 1.0
 */
public class EchoServerReactor implements Runnable {

    Selector selector;
    ServerSocketChannel serverSocketChannel;

    EchoServerReactor() throws IOException {
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 2222));
        serverSocketChannel.configureBlocking(false);
        SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        sk.attach(new AcceptorHandler());
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                selector.select();
                Set selected = selector.selectedKeys();
                Iterator it = selected.iterator();
                while (it.hasNext()) {
                    SelectionKey sk = (SelectionKey) it.next();
                    dispatch(sk);
                }
                selected.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

    class AcceptorHandler implements Runnable {

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

    public static void main(String[] args) throws IOException {
        new Thread(new EchoServerReactor()).start();
    }
}
package com.io.test;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;

/**
 * @ClassName EchoHandler
 * @Description TODO
 * @Author djl
 * @Date 2020-02-18 11:58
 * @Version 1.0
 */
public class EchoHandler implements Runnable {

    final SocketChannel channel;
    final SelectionKey sk;
    final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    final int RECIVING = 0, SENDING = 1;
    int state = RECIVING;

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

    @Override
    public void run() {
        try {
            if (state == SENDING) {
                channel.write(byteBuffer);
                byteBuffer.clear();
                sk.interestOps(SelectionKey.OP_READ);
                state = RECIVING;
            } else if (state == RECIVING) {
                int length = 0;
                while ((length = channel.read(byteBuffer)) > 0) {
                    System.out.println(new String(byteBuffer.array(),0,length));
                }
                byteBuffer.flip();
                sk.interestOps(SelectionKey.OP_WRITE);
                state = SENDING;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端示例

package com.io.test;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @ClassName NioDiscardClient
 * @Description TODO
 * @Author djl
 * @Date 2020-02-09 11:15
 * @Version 1.0
 */
public class NioDiscardClient {

    public static void startClient() throws IOException {
        InetSocketAddress address = new InetSocketAddress(InetAddress.getLocalHost(),2222);
        SocketChannel socketChannel = SocketChannel.open(address);
        socketChannel.configureBlocking(false);
        while (!socketChannel.finishConnect()){

        }
        System.out.println("connect success");
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("hello world".getBytes());
        byteBuffer.flip();
        socketChannel.write(byteBuffer);
        socketChannel.shutdownOutput();
        socketChannel.close();
    }

    public static void main(String[] args) throws IOException {
        startClient();
    }
}

JlDang
34 声望3 粉丝