Reactor线程模型的实现

一、Selector&Channel

1.1:各种channel

写这个模型需要提前了解Selector以及Channel,之前记录过FileChannel,除此之外还有以下几种Channel:
ServerSocketChannel:用于监听新的TCP连接的通道,负责读取&响应,通常用于服务端的实现。
SocketChannel:用于发起TCP连接,读写网络中的数据,通常用于客户端的实现。
DatagramChannel:上述两个通道基于TCP传输协议,而这个通道则基于UDP,用于读写网络中的数据。
FileChannel:从文件读取数据。

本篇重点放在ServerSocketChannel和SocketChannel上,大部分客户端/服务端为了保证数据准确性,都是基于TCP传输协议实现的,由于使用Selector注册必须要求被注册的Channel是非阻塞模式的,因此FileChannel由于没有非阻塞模式(无法设置configureBlocking(false)),没办法和注册到selector。

1.2:selector

Selector是个通道注册器(用法会在程序里标注),是实现Reactor模型的关键,多个通道均可以注册到Selector,Selector负责监听每个Channel的几个事件:连接就绪、写就绪、读就绪,当某个channel注册感兴趣就绪事件到selector时,若发生兴趣事件就绪,则Selector.select()方法不再阻塞,返回兴趣事件集合(可能包含多个channel的),然后按照事件不同进行分发处理。

Selector返回对应的就绪事件,封装为SelectionKey,每个Channel对应一个SelectionKey,这个对象还可以通过attach方法附着处理类(HandlerAcceptor等)。

1.3:一个简单的例子

先来看个简单使用Selector做处理的服务端实现,可以简单对Selector和SelectionKey的用法做个了解:

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

        Selector selector = Selector.open(); //打开选择器

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //打开通道
        serverSocketChannel.configureBlocking(false); //设置通道为非阻塞模式
        serverSocketChannel.bind(new InetSocketAddress(2333)); //绑定端口
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //注册channel到选择器,指定监听该Channel的哪些事件,初始化都是对连接事件监听(因为是入口)

        while (selector.select() > 0) { // 若收到就绪事件select返回“感兴趣”事件集合,否则阻塞当前线程
            Set keys = selector.selectedKeys(); //获取本次拿到的事件集合
            Iterator iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) { //当前就绪事件为连接事件
                    ServerSocketChannel skc = (ServerSocketChannel) key.channel(); //连接就绪触发,说明已经有客户端通道连了过来,这里需要拿服务端通道去获取客户端通道
                    SocketChannel socketChannel = skc.accept(); //获取客户端通道(连接就绪,说明客户端接下来可能还有别的动作,比如读和写)
                    socketChannel.configureBlocking(false); //同样的需要设置非阻塞模式
                    System.out.println(String.format("收到来自 %s 的连接", socketChannel.getRemoteAddress()));
                    socketChannel.register(selector, SelectionKey.OP_READ); //将该客户端注册到选择器,感兴趣事件设置为读(客户端连接完毕,很肯能会往服务端写数据,因此这里要注册读事件用以接收这些数据)
                } else if (key.isReadable()) { //当前事件为读就绪
                    SocketChannel socketChannel = (SocketChannel) key.channel(); //能触发读就绪,说明客户端已经开始往服务端写数据,通过SelectionKey拿到当前客户端通道
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int count = socketChannel.read(buffer); //从通道读入数据
                    if (count < 0) { //若本次读就绪拿到-1,则认为客户端主动断开了连接
                        socketChannel.close(); //服务端关闭客户端通道
                        key.cancel(); //断连后就将该事件从选择器的SelectionKey集合中移除(这里说一下,这里不是真正意义上的移除,这里是取消,会将该key放入取消队列里,在下次select函数调用时才负责清空)
                        System.out.println("连接关闭");
                        continue;
                    }
                    System.out.println(String.format("收到来自 %s 的消息: %s",
                            socketChannel.getRemoteAddress(),
                            new String(buffer.array())));
                }
                keys.remove(key);
            }
        }
    }

上面是一个简单的例子,接下来,就利用选择器、通道来实现Reactor单线程模型。

二、单Reactor单线程模型的服务端实现

实现服务端,服务端负责接收客户端的连接,接收客户端的请求数据以及响应客户端。

把上一篇的结构图再拿过来展示下,看看需要做的有哪些模块:
在这里插入图片描述

图1

通过上图,我们需要实现的模块有Reactor、Acceptor、Handler,下面来逐个编写:

2.1:Reactor核心模块

该模块内部包含两个核心方法,select和dispatch,该模块负责监听就绪事件和对事件的分发处理:

public class Reactor implements Runnable {

    private final Selector selector;
    private final ServerSocketChannel serverSocketChannel;

    public Reactor(int port) throws IOException { //Reactor初始化
        selector = Selector.open(); //打开一个Selector
        serverSocketChannel = ServerSocketChannel.open(); //建立一个Server端通道
        serverSocketChannel.socket().bind(new InetSocketAddress(port)); //绑定服务端口
        serverSocketChannel.configureBlocking(false); //selector模式下,所有通道必须是非阻塞的
        //Reactor是入口,最初给一个channel注册上去的事件都是accept
        SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //attach callback object, Acceptor
        sk.attach(new Acceptor(serverSocketChannel, selector));
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                selector.select(); //就绪事件到达之前,阻塞
                Set selected = selector.selectedKeys(); //拿到本次select获取的就绪事件
                Iterator it = selected.iterator();
                while (it.hasNext()) {
                    //这里进行任务分发
                    dispatch((SelectionKey) (it.next()));
                }
                selected.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    void dispatch(SelectionKey k) {
        Runnable r = (Runnable) (k.attachment()); //这里很关键,拿到每次selectKey里面附带的处理对象,然后调用其run,这个对象在具体的Handler里会进行创建,初始化的附带对象为Acceptor(看上面构造器)
        //调用之前注册的callback对象
        if (r != null) {
            r.run();
        }
    }
}

细节已标注。

2.2:实现Acceptor模块

这个模块只负责处理连接就绪事件,有了这个事件就可以拿到客户单的SocketChannel,就可以继续完成接下来的读写任务了:

public class Acceptor implements Runnable {

    private final Selector selector;

    private final ServerSocketChannel serverSocketChannel;

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

    @Override
    public void run() {
        SocketChannel socketChannel;
        try {
            socketChannel = serverSocketChannel.accept();
            if (socketChannel != null) {
                System.out.println(String.format("收到来自 %s 的连接",
                        socketChannel.getRemoteAddress()));
                new Handler(socketChannel, selector); //这里把客户端通道传给Handler,Handler负责接下来的事件处理(除了连接事件以外的事件均可)
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

细节已标注。

2.3:Handler模块的实现

这个模块负责接下来的读写操作:

public class Handler implements Runnable {

    private final SelectionKey selectionKey;
    private final SocketChannel socketChannel;

    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer sendBuffer = ByteBuffer.allocate(2048);

    private final static int READ = 0;
    private final static int SEND = 1;

    private int status = READ;

    Handler(SocketChannel socketChannel, Selector selector) throws IOException {
        this.socketChannel = socketChannel; //接收客户端连接
        this.socketChannel.configureBlocking(false); //置为非阻塞模式(selector仅允非阻塞模式)
        selectionKey = socketChannel.register(selector, 0); //将该客户端注册到selector,得到一个SelectionKey,以后的select到的就绪动作全都是由该对象进行封装
        selectionKey.attach(this); //附加处理对象,当前是Handler对象,run是对象处理业务的方法
        selectionKey.interestOps(SelectionKey.OP_READ); //走到这里,说明之前Acceptor里的建连已完成,那么接下来就是读取动作,因此这里首先将读事件标记为“感兴趣”事件
        selector.wakeup(); //唤起select阻塞
    }

    @Override
    public void run() {
        try {
            switch (status) {
                case READ:
                    read();
                    break;
                case SEND:
                    send();
                    break;
                default:
            }
        } catch (IOException e) { //这里的异常处理是做了汇总,常出的异常就是server端还有未读/写完的客户端消息,客户端就主动断开连接,这种情况下是不会触发返回-1的,这样下面read和write方法里的cancel和close就都无法触发,这样会导致死循环异常(read/write处理失败,事件又未被cancel,因此会不断的被select到,不断的报异常)
            System.err.println("read或send时发生异常!异常信息:" + e.getMessage());
            selectionKey.cancel();
            try {
                socketChannel.close();
            } catch (IOException e2) {
                System.err.println("关闭通道时发生异常!异常信息:" + e2.getMessage());
                e2.printStackTrace();
            }
        }
    }

    private void read() throws IOException {
        if (selectionKey.isValid()) {
            readBuffer.clear();
            int count = socketChannel.read(readBuffer); //read方法结束,意味着本次"读就绪"变为"读完毕",标记着一次就绪事件的结束
            if (count > 0) {
                System.out.println(String.format("收到来自 %s 的消息: %s",
                        socketChannel.getRemoteAddress(),
                        new String(readBuffer.array())));
                status = SEND;
                selectionKey.interestOps(SelectionKey.OP_WRITE); //注册写方法
            } else {
                //读模式下拿到的值是-1,说明客户端已经断开连接,那么将对应的selectKey从selector里清除,否则下次还会select到,因为断开连接意味着读就绪不会变成读完毕,也不cancel,下次select会不停收到该事件
                //所以在这种场景下,(服务器程序)你需要关闭socketChannel并且取消key,最好是退出当前函数。注意,这个时候服务端要是继续使用该socketChannel进行读操作的话,就会抛出“远程主机强迫关闭一个现有的连接”的IO异常。
                selectionKey.cancel();
                socketChannel.close();
                System.out.println("read时-------连接关闭");
            }
        }
    }

    void send() throws IOException {
        if (selectionKey.isValid()) {
            sendBuffer.clear();
            sendBuffer.put(String.format("我收到来自%s的信息辣:%s,  200ok;",
                    socketChannel.getRemoteAddress(),
                    new String(readBuffer.array())).getBytes());
            sendBuffer.flip();
            int count = socketChannel.write(sendBuffer); //write方法结束,意味着本次写就绪变为写完毕,标记着一次事件的结束

            if (count < 0) {
                //同上,write场景下,取到-1,也意味着客户端断开连接
                selectionKey.cancel();
                socketChannel.close();
                System.out.println("send时-------连接关闭");
            }

            //没断开连接,则再次切换到读
            status = READ;
            selectionKey.interestOps(SelectionKey.OP_READ);
        }
    }
}

细节已标注。

关键模块已实现,下面来启动服务端:

new Thread(new Reactor(2333)).start();

2.4:客户端的编写

接下来同样利用selector编写客户端,客户端需要做的事情就是发送消息到服务端,等待服务端响应,然后再次发送消息,发够10条消息断开连接:

Client入口模块

public class NIOClient implements Runnable {

    private Selector selector;

    private SocketChannel socketChannel;

    NIOClient(String ip, int port) {
        try {
            selector = Selector.open(); //打开一个Selector
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false); //设置为非阻塞模式
            socketChannel.connect(new InetSocketAddress(ip, port)); //连接服务
            //入口,最初给一个客户端channel注册上去的事件都是连接事件
            SelectionKey sk = socketChannel.register(selector, SelectionKey.OP_CONNECT);
            //附加处理类,第一次初始化放的是连接就绪处理类
            sk.attach(new Connector(socketChannel, selector));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                selector.select(); //就绪事件到达之前,阻塞
                Set selected = selector.selectedKeys(); //拿到本次select获取的就绪事件
                Iterator it = selected.iterator();
                while (it.hasNext()) {
                    //这里进行任务分发
                    dispatch((SelectionKey) (it.next()));
                }
                selected.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    void dispatch(SelectionKey k) {
        Runnable r = (Runnable) (k.attachment()); //这里很关键,拿到每次selectKey里面附带的处理对象,然后调用其run,这个对象在具体的Handler里会进行创建,初始化的附带对象为Connector(看上面构造器)
        //调用之前注册的callback对象
        if (r != null) {
            r.run();
        }
    }
}

细节已标注。

Connector模块(建连)

public class Connector implements Runnable {

    private final Selector selector;

    private final SocketChannel socketChannel;

    Connector(SocketChannel socketChannel, Selector selector) {
        this.socketChannel = socketChannel;
        this.selector = selector;
    }

    @Override
    public void run() {
        try {
            if (socketChannel.finishConnect()) { //这里连接完成(与服务端的三次握手完成)
                System.out.println(String.format("已完成 %s 的连接",
                        socketChannel.getRemoteAddress()));
                new Handler(socketChannel, selector); //连接建立完成后,接下来的动作交给Handler去处理(读写等)
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

细节已标注。

客户端Handler模块实现

public class Handler implements Runnable {

    private final SelectionKey selectionKey;
    private final SocketChannel socketChannel;

    private ByteBuffer readBuffer = ByteBuffer.allocate(2048);
    private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);

    private final static int READ = 0;
    private final static int SEND = 1;

    private int status = SEND; //与服务端不同,默认最开始是发送数据

    private AtomicInteger counter = new AtomicInteger();

    Handler(SocketChannel socketChannel, Selector selector) throws IOException {
        this.socketChannel = socketChannel; //接收客户端连接
        this.socketChannel.configureBlocking(false); //置为非阻塞模式(selector仅允非阻塞模式)
        selectionKey = socketChannel.register(selector, 0); //将该客户端注册到selector,得到一个SelectionKey,以后的select到的就绪动作全都是由该对象进行封装
        selectionKey.attach(this); //附加处理对象,当前是Handler对象,run是对象处理业务的方法
        selectionKey.interestOps(SelectionKey.OP_WRITE); //走到这里,说明之前Connect已完成,那么接下来就是发送数据,因此这里首先将写事件标记为“感兴趣”事件
        selector.wakeup(); //唤起select阻塞
    }

    @Override
    public void run() {
        try {
            switch (status) {
                case SEND:
                    send();
                    break;
                case READ:
                    read();
                    break;
                default:
            }
        } catch (IOException e) { //这里的异常处理是做了汇总,同样的,客户端也面临着正在与服务端进行写/读数据时,突然因为网络等原因,服务端直接断掉连接,这个时候客户端需要关闭自己并退出程序
            System.err.println("send或read时发生异常!异常信息:" + e.getMessage());
            selectionKey.cancel();
            try {
                socketChannel.close();
            } catch (IOException e2) {
                System.err.println("关闭通道时发生异常!异常信息:" + e2.getMessage());
                e2.printStackTrace();
            }
        }
    }

    void send() throws IOException {
        if (selectionKey.isValid()) {
            sendBuffer.clear();
            int count = counter.incrementAndGet();
            if (count <= 10) {
                sendBuffer.put(String.format("客户端发送的第%s条消息", count).getBytes());
                sendBuffer.flip(); //切换到读模式,用于让通道读到buffer里的数据
                socketChannel.write(sendBuffer);

                //则再次切换到读,用以接收服务端的响应
                status = READ;
                selectionKey.interestOps(SelectionKey.OP_READ);
            } else {
                selectionKey.cancel();
                socketChannel.close();
            }
        }
    }

    private void read() throws IOException {
        if (selectionKey.isValid()) {
            readBuffer.clear(); //切换成buffer的写模式,用于让通道将自己的内容写入到buffer里
            socketChannel.read(readBuffer);
            System.out.println(String.format("收到来自服务端的消息: %s", new String(readBuffer.array())));

            //收到服务端的响应后,再继续往服务端发送数据
            status = SEND;
            selectionKey.interestOps(SelectionKey.OP_WRITE); //注册写事件
        }
    }
}

细节已标注。

下面启动客户端去连接之前的服务端:

new Thread(new NIOClient("127.0.0.1", 2333)).start();
new Thread(new NIOClient("127.0.0.1", 2333)).start();

上面模拟了两个客户端同时连到服务端,运行结果如下:

服务端运行结果:
在这里插入图片描述

图2

客户端运行结果如下:
在这里插入图片描述

图3

单线程Reactor模型有个致命的缺点,通过上述例子可以看出,整个执行流程都是线性的,客户端请求→服务端读取→服务端响应→客户端收到响应→客户端再次发送请求,那么在这个链路中,如果handler中某个位置存在性能瓶颈,比如我们可以改造下服务端的send方法:

try {
   Thread.sleep(2000L); //响应2s
} catch (InterruptedException e) {
   e.printStackTrace();
}
int count = socketChannel.write(sendBuffer);

在响应客户端之前睡眠2s,当做是性能瓶颈点,同样的再次开两个客户端同时访问服务端,每个客户端发送10条消息,会发现,程序直接运行了40s,这是大多数情况下不愿意看到的,因此,就有了多线程Reactor模式,跟BIO为了提高性能将读操作放到一个独立线程处理一样,Reactor这样做,也是为了解决上面提到的性能问题,只不过NIO比BIO做异步有个最大的优势就是NIO不会阻塞一个线程,类似read这种操作状态都是由selector负责监听的,不像BIO里都是阻塞的,只要被异步出去,那么一定是非阻塞的业务代码(除非是人为将代码搞成阻塞),而BIO由于read本身阻塞,因此会阻塞掉整个线程,这也是同样是异步为什么NIO可以更加高效的原因之一。

那么单线程Reactor适用于什么情况呢?适用于那种程序复杂度很低的系统,例如redis,其大部分操作都是非常高效的,很多命令的时间复杂度直接为O(1),这种情况下适合这种简单的Reactor模型实现服务端。

2.5:单线程Reactor模型的缺点

紧接着上篇Reactor单线程模型的例子来,假设Handler的read那里的处理方式延迟5s,当做是业务性能瓶颈,改变下原来的Handler,让其read方法在处理时延迟5s:

private void read() throws IOException {
        if (selectionKey.isValid()) {
            System.out.println("服务端读取数据前");
            readBuffer.clear();
            int count = socketChannel.read(readBuffer);
            if (count > 0) {
                try {
                    Thread.sleep(5000L); //读取信息后睡眠5s当做业务处理瓶颈
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(String.format("收到来自 %s 的消息: %s",
                        socketChannel.getRemoteAddress(),
                        new String(readBuffer.array())));
                status = SEND;
                selectionKey.interestOps(SelectionKey.OP_WRITE);
            } else {
                selectionKey.cancel();
                socketChannel.close();
                System.out.println("read时-------连接关闭");
            }
        }
    }

现在同样开启两个客户端同时连接到该服务端,然后请求–>收到响应–>再次请求的流程走10次,会发现,客户端每收到一次响应需要10s,同样的如果开启3个客户端,则需要15s,因为单线程的Reactor模型是串行的,业务处理的瓶颈可以影响到全局的事件分发,这种模型下,如果存在类似例子中的瓶颈点是致命的(例子的5s是夸张处理),因为新进来的连接也会排队,整个select都会被Handler的处理给阻塞掉,举个实际点的例子,redis在使用时大部分时候会避免使用类似keys这种重操作,为什么呢?就是因为redis是单线程,这里说的单线程其实并不是说redis服务端就一个线程,而是说redis采用的NIO Reactor模型就是单线程的Reactor模型,跟上面代码里做的改动一样,5s可以理解成重操作,影响整个模型的正常运作,redis之所以采用单线程模式,是因为redis大部分操作实在是太快了,快到使用这种模式也可以提供近十万/秒的并发能力,单线程模型实现起来简单且可控性强,所以redis很自然的选择了这种模式。回到问题本身,我们自己的业务可能并没有redis那样高的处理能力,搞不好几个网络请求就可以造成性能瓶颈,拖慢甚至拖垮整个处理模型,所以大部分RPC框架和web容器并不会采用单线程的Reactor模型实现,那么有没有什么方法可以优化这种模型呢?比如,把这个瓶颈点利用独立线程异步出去处理,这样可以保证不影响select的执行,也就很好的避免了上面的问题了,下面介绍两种多线程的Reactor模型。

三、单Reactor多线程模型

模型图:
在这里插入图片描述

图4

上图与单线程Reactor模型对比可以看出,读入数据后,对数据的业务处理部分被线程池做了异步处理,也就是说,上述5s的那段瓶颈被放到了子线程去处理,select的执行不会受到任何影响,因此对新的连接处理、多个客户端的响应速度都应该可以得到保障。

现在来改写下前篇文章里的单线程处理模式的Handler,更名为AsyncHandler

public class AsyncHandler implements Runnable {

    private final Selector selector;

    private final SelectionKey selectionKey;
    private final SocketChannel socketChannel;

    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer sendBuffer = ByteBuffer.allocate(2048);

    private final static int READ = 0; //读取就绪
    private final static int SEND = 1; //响应就绪
    private final static int PROCESSING = 2; //处理中

    private int status = READ; //所有连接完成后都是从一个读取动作开始的

    //开启线程数为5的异步处理线程池
    private static final ExecutorService workers = Executors.newFixedThreadPool(5);

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

    @Override
    public void run() { //如果一个任务正在异步处理,那么这个run是直接不触发任何处理的,read和send只负责简单的数据读取和响应,业务处理完全不阻塞这里的处理
        switch (status) {
            case READ:
                read();
                break;
            case SEND:
                send();
                break;
            default:
        }
    }

    private void read() {
        if (selectionKey.isValid()) {
            try {
                readBuffer.clear();
                int count = socketChannel.read(readBuffer);
                if (count > 0) {
                    status = PROCESSING; //置为处理中,处理完成后该状态为响应,表示读入处理完成,接下来可以响应客户端了
                    workers.execute(this::readWorker); //异步处理
                } else {
                    selectionKey.cancel();
                    socketChannel.close();
                    System.out.println("read时-------连接关闭");
                }
            } catch (IOException e) {
                System.err.println("处理read业务时发生异常!异常信息:" + e.getMessage());
                selectionKey.cancel();
                try {
                    socketChannel.close();
                } catch (IOException e1) {
                    System.err.println("处理read业务关闭通道时发生异常!异常信息:" + e.getMessage());
                }
            }
        }
    }

    void send() {
        if (selectionKey.isValid()) {
            status = PROCESSING; //置为执行中
            workers.execute(this::sendWorker); //异步处理
            selectionKey.interestOps(SelectionKey.OP_READ); //重新设置为读
        }
    }

    //读入信息后的业务处理
    private void readWorker() {
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(String.format("收到来自客户端的消息: %s",
                new String(readBuffer.array())));
        status = SEND;
        selectionKey.interestOps(SelectionKey.OP_WRITE); //注册写事件
        this.selector.wakeup(); //唤醒阻塞在select的线程,因为该interestOps写事件是放到子线程的,select在该channel还是对read事件感兴趣时又被调用,因此如果不主动唤醒,select可能并不会立刻select该读就绪事件(在该例中,可能永远不会被select到)
    }

    private void sendWorker() {
        try {
            sendBuffer.clear();
            sendBuffer.put(String.format("我收到来自%s的信息辣:%s,  200ok;",
                    socketChannel.getRemoteAddress(),
                    new String(readBuffer.array())).getBytes());
            sendBuffer.flip();

            int count = socketChannel.write(sendBuffer);

            if (count < 0) {
                selectionKey.cancel();
                socketChannel.close();
                System.out.println("send时-------连接关闭");
            } else {
                //再次切换到读
                status = READ;
            }
        } catch (IOException e) {
            System.err.println("异步处理send业务时发生异常!异常信息:" + e.getMessage());
            selectionKey.cancel();
            try {
                socketChannel.close();
            } catch (IOException e1) {
                System.err.println("异步处理send业务关闭通道时发生异常!异常信息:" + e.getMessage());
            }
        }
    }
}

可以看到,read里、send里的逻辑处理被异步出去执行,新增了中间状态“执行中”,主要用来防止事件重复触发,重复执行异步逻辑,当异步逻辑处理完毕才会更改状态值,这时候可以继续处理接下来的事件(读或写)。

把Accptor类里的实现换成AsyncHandler,运行服务端和客户端会发现,两个客户端的响应均为5s,也不会阻塞新增的连接,新增至三个或者更多的客户端基本可以保持客户端响应均为5s(说明:这里5s是夸张比喻,正常瓶颈没这么夸张,若开了n多客户端,每个都阻塞5s,那么线程池也会发生排队,因为子线程个数有限,处理不过来,最后还是阻塞,一定会远超过5s)。

通过多线程Reactor模型,降低了业务代码瓶颈导致影响整个Reactor执行链路的风险,但是即便如此,readsend操作仍然和接收请求**(accept)处于同一个线程**,这就意味着read、send的处理可能会影响到对客户端连接的接收能力,那么有没有一种办法,可以把读写流程彻底异步出去,负责连接的线程就只负责接收连接?于是多Reactor多线程模型就产生了,这种模型也叫主从Reactor模型,该模型下可以分为一个主Reactor专门处理连接事件,而多个从Reactor负责读写、业务处理等,这样服务端可以接收并处理更多的请求,提升服务端的吞吐能力(该模型或者说所有基于NIO的Reactor模型,都是以提升服务端处理能力为基础的,NIO在某些情况下不一定会比BIO处理速度快,但一定比BIO稳,就像NIO可以利用很少的线程处理大量的客户端请求,而BIO在大量客户端请求过来的情况下,由于各种操作均会阻塞线程,会处理不过来)。

四、主从Reactor模型

还是把之前文章的图拿来展示下这种模型的流程,可以与上面图4进行对比,看看发生了哪些变化:
在这里插入图片描述

图5

上图就是主从Reactor模型的一个流程,看下与图4的不同之处,多了SubReactor这样一个角色,这个角色就是用来处理读写操作的Reactor,现在仍然基于之前的例子,进行改写,明确需要改写的点:

①新增SubReactor

②Acceptor那里进行初始化一批SubReactor,进行分发处理

③为了区分客户端分别是被哪个SubReactor处理的读写操作,还需要改写下AsyncHandler,在里面加上SubReactor的序号,打印信息时进行区分。

ok,总结完改动点,现在基于上面的代码(代码初代目版本:Reactor单线程模型的实现)改写一下这几个类:

step1.首先新增SubReactor类

public class SubReactor implements Runnable {
    private final Selector selector;
    private boolean register = false; //注册开关表示,为什么要加这么个东西,可以参考Acceptor设置这个值那里的描述
    private int num; //序号,也就是Acceptor初始化SubReactor时的下标

    SubReactor(Selector selector, int num) {
        this.selector = selector;
        this.num = num;
    }

    @Override
    public void run() {
        while (!Thread.interrupted()) {
            System.out.println(String.format("%d号SubReactor等待注册中...", num));
            while (!Thread.interrupted() && !register) {
                try {
                    if (selector.select() == 0) {
                        continue;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                Set selectedKeys = selector.selectedKeys();
                Iterator it = selectedKeys.iterator();
                while (it.hasNext()) {
                    dispatch(it.next());
                    it.remove();
                }
            }
        }
    }

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

    void registering(boolean register) {
        this.register = register;
    }

}

这个类负责Acceptor交给自己的事件select(例子中实际上就是read、send)。

step2.Acceptor类的更改

public class Acceptor implements Runnable {

    private final ServerSocketChannel serverSocketChannel;

    private final int coreNum = Runtime.getRuntime().availableProcessors(); // 获取CPU核心数

    private final Selector[] selectors = new Selector[coreNum]; // 创建selector给SubReactor使用,个数为CPU核心数(如果不需要那么多可以自定义,毕竟这里会吞掉一个线程)

    private int next = 0; // 轮询使用subReactor的下标索引

    private SubReactor[] reactors = new SubReactor[coreNum]; // subReactor

    private Thread[] threads = new Thread[coreNum]; // subReactor的处理线程

    Acceptor(ServerSocketChannel serverSocketChannel) throws IOException {
        this.serverSocketChannel = serverSocketChannel;
        // 初始化
        for (int i = 0; i < coreNum; i++) {
            selectors[i] = Selector.open();
            reactors[i] = new SubReactor(selectors[i], i); //初始化sub reactor
            threads[i] = new Thread(reactors[i]); //初始化运行sub reactor的线程
            threads[i].start(); //启动(启动后的执行参考SubReactor里的run方法)
        }
    }

    @Override
    public void run() {
        SocketChannel socketChannel;
        try {
            socketChannel = serverSocketChannel.accept(); // 连接
            if (socketChannel != null) {
                System.out.println(String.format("收到来自 %s 的连接",
                        socketChannel.getRemoteAddress()));
                socketChannel.configureBlocking(false); //
                reactors[next].registering(true); // 注意一个selector在select时是无法注册新事件的,因此这里要先暂停下select方法触发的程序段,下面的weakup和这里的setRestart都是做这个事情的,具体参考SubReactor里的run方法
                selectors[next].wakeup(); // 使一個阻塞住的selector操作立即返回
                SelectionKey selectionKey = socketChannel.register(selectors[next],
                        SelectionKey.OP_READ); // 当前客户端通道SocketChannel向selector[next]注册一个读事件,返回key
                selectors[next].wakeup(); // 使一個阻塞住的selector操作立即返回
                reactors[next].registering(false); // 本次事件注册完成后,需要再次触发select的执行,因此这里Restart要在设置回false(具体参考SubReactor里的run方法)
                selectionKey.attach(new AsyncHandler(socketChannel, selectors[next], next)); // 绑定Handler
                if (++next == selectors.length) {
                    next = 0; //越界后重新分配
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

可以跟以前的Acceptor做个对比,做了如下改动:

①接受到连接后不再直接触发handler了

②初始化一堆SubReactor(从反应堆),每个交给一个线程处理,注册读事件后顺序分配给不同的SubReactor去处理自己的selector监听。

以上,就可以把读写处理+业务处理与接受连接的Reactor彻底分开了,接受连接的事件不再受任何读写、业务相关的影响,只负责接收,目前即便是业务线程池用光线程发生排队,也不会影响到连接的接收,很大程度上降低了服务端的接收能力遭遇瓶颈的风险。

step3.改写AsyncHandler的打印
这里就不po代码了,具体就是把SubReactor的序号传给handler,标记触发Handler的Reactor是哪个。

同样的,启动下服务端,再开启两个客户端(跟之前一样,每个客户端发10条消息终止连接),运行结果如下:

服务端:

1号SubReactor等待注册中...
3号SubReactor等待注册中...
0号SubReactor等待注册中...
2号SubReactor等待注册中...
收到来自 /127.0.0.1:60407 的连接
0号SubReactor等待注册中...
收到来自 /127.0.0.1:60410 的连接
1号SubReactor等待注册中...
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第1条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第1条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第2条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第2条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第3条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第3条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第4条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第4条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第5条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第5条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第6条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第6条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第7条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第7条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第8条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第8条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第9条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第9条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第10条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第10条消息                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
0号SubReactor触发:read时-------连接关闭
1号SubReactor触发:read时-------连接关闭

客户端:

已完成 /127.0.0.1:2333 的连接
已完成 /127.0.0.1:2333 的连接
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第1条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第1条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第2条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第2条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第3条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第3条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第4条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第4条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第5条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第5条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第6条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第6条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第7条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第7条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第8条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第8条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第9条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第9条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第10条消息,  200ok;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第10条消息,  200ok;    

到这里,主从Reactor模型就被改写完成了,上面的例子只是简单演示了下这个模型,所有的例子都是从单线程Reactor模型一点点改写来的,客户端没变过,为的是更好的测试服务端在不同模型下的表现。主从Reactor模型应用的比较多,比如著名NIO框架Netty底层模型也是基于主从Reactor模型来实现的。

到这里java nio的东西已经差不多记录完了,后续会开始netty的学习记录,当然上述例子弱化了buffer的使用,而且例子中不存在粘包拆包的问题(因为都是请求+应答的方式进行),如果把上面的例子改成客户端在未收到响应时就连续发送几条信息,服务端这时再次由写模式切换到读模式,就会从Channel里连续拿到这几条消息,这就导致了粘包问题,那么如何解决类似的问题呢?通常是定义一种协议,来区分消息头和尾,中间的消息体是我们真正需要的数据,这种协议也就是我们常说的应用层协议,比如HTTP、FTP等,这里不做赘述,之后会通过一个例子来完成这部分的补充说明。

五、代码地址

单线程Reactor模型
https://github.com/exceting/DemoAll/tree/master/jdk/src/main/java/demo/jdk/reactor/simple
多线程Reactor模型
同上,Acceptor里的Handler改成AsyncHandler即可
主从多线程Reactor模型
https://github.com/exceting/DemoAll/tree/master/jdk/src/main/java/demo/jdk/reactor/mainsub




原文转自:
1、https://www.cnblogs.com/hama1993/p/10611229.html
2、https://www.cnblogs.com/hama1993/p/10640067.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值