Loading

Java Reactor通信模型

前言

NettyJava网络通信中高性能框架,但它为什么是高性能的呢?其中很大一个原因在于Netty采用Reactor通信模型。Reactor是同步IO的线程模型,效率高且编码易于理解,和它相对应的是基于异步IOProactor线程模型,Proactor同样高效,但因为是异步的处理方式,所以编码不易。

下面就来介绍Reactor通信模型的原理和实现。

BIO 和 NIO

首先,介绍两种最常见的通信方式,BIONIO

1、BIO
Blocking IO,阻塞IO。阻塞指的是调用acceptreadwrite等方法时,线程是阻塞的,必须一直等待相应的事件发生,而不能做其他的任何事情。

BIO下,每一个客户端连接后,都需要创建一个线程来处理连接和读写操作。所以,效率十分低下,只适用于连接数且读写操作比较少的场景。

image

2、NIO
Non-blocking IO,非阻塞IO。非阻塞指的是调用acceptreadwrite等方法时,即使没有相应的事件发生,线程也不用一直在这等着,可以继续往下执行。

NIO下,客户端的连接和读写请求都是通过Selecor大管家进行处理的,即一个线程处理多个客户端的连接。所以,NIO的效率要比BIO高许多。并且,Reactor模型就是基于NIO通信方式实现的。

image

如果对BIONIO通信的源码有兴趣,请看文章末尾的链接。

Reactor模型

Reactor模型也主要有三种实现方式,单Reactor单线程、单Reactor多线程和主从Reactor多线程。

1、单 Reactor 单线程
该模式和NIO的通信方式一致,单线程处理客户端的连接和读写操作。缺点很明显,就是连接和读写多的情况下,效率低下。

2、单 Reactor 多线程
该模式与NIO的区别在于,通过多线程的方式来处理客户端的读写操作,这样既减轻了Selector的压力,又保证了处理读写操作的能力。这个缺点在于Reactor既要负责客户端的连接,又要负责读写操作,连接操作比较快,但读写可能很多且比较耗时。

image

WorkHandler.java

package main.java.io.reactor.single;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;

// 处理客户端读写操作
public class WorkHandler {
    private final SelectionKey key;
    private final ExecutorService executor;
    private final ByteBuffer buffer;

    public WorkHandler(SelectionKey sk, ExecutorService es) {
        key = sk;
        executor = es;
        buffer = ByteBuffer.allocate(1024);
    }

    public void handle() {
        SocketChannel channel = (SocketChannel) key.channel();
        try {
            int n = channel.read(buffer); // 执行 read 之后,selector.select() 才不会再次监听到同一个读事件
            executor.execute(() -> {
                try {
                    System.out.println("work handle call");
                    if (n > 0) {
                        String tn = Thread.currentThread().getName();
                        String str = String.format("%s %s %s",
                                "[SingleReactor]", tn, new String(buffer.array()).trim());
                        channel.write(ByteBuffer.wrap(str.getBytes()));
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3、主从 Reactor 多线程
基于单 Reactor 多线程的缺点,于是有了主从Reactor模式,主 Reactor 只负责处理客户端的连接,从 Reactor 负责客户端的读写操作,并且是一主多从的模型。主从模型下,不仅遵循了单一职责的原则,还提高了承受读写负载的能力。

说了这么多,还是来看下这个的完整源码:

(1)MSReactor.java

package main.java.io.reactor.ms;

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

/**
 * 主从 Reactor,多工作线程
 * Master-Slave Reactor,Multiple Worker Threads
 */
public class MainReactor {
    private static final String ADDRESS = "127.0.0.1";
    private static final int PORT = 8000;

    public static void main(String[] args) {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            InetSocketAddress address = new InetSocketAddress(ADDRESS, PORT);
            serverSocketChannel.bind(address);
            System.out.println("MSReactor server bind " + address);

            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            for (;;) {
                selector.select();
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    if (key.isAcceptable()) {
                        Acceptor acceptor = new Acceptor(serverSocketChannel, selector);
                        acceptor.accept();
                    }else if (key.isReadable()) {
                        SubReactor subReactor = new SubReactor(key);
                        subReactor.process();
                    }
                    it.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

(2)Acceptor.java

package main.java.io.reactor.ms;

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

/**
 * 处理客户端连接
 */
public class Acceptor {
    private final ServerSocketChannel serverSocketChannel;
    private final Selector selector;

    public Acceptor(ServerSocketChannel ssc, Selector s) {
        serverSocketChannel = ssc;
        selector = s;
    }

    public void accept() {
        try {
            SocketChannel channel = serverSocketChannel.accept();
            System.out.println("connected from " + channel.getRemoteAddress());
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

(3)SubReactor.java

package main.java.io.reactor.ms;

import java.nio.channels.SelectionKey;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 从 Reactor
 */
public class SubReactor {
    private final SelectionKey key;
    private final ExecutorService executor;

    public SubReactor(SelectionKey k) {
        key = k;
        executor = Executors.newCachedThreadPool();
    }

    public void process() {
        WorkHandler workHandler = new WorkHandler(key, executor);
        workHandler.handle();
    }
}

(4)WorkHandler.java

package main.java.io.reactor.ms;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;

public class WorkHandler {
    private final SelectionKey key;
    private final ExecutorService executor;
    private final ByteBufferPool pool;

    public WorkHandler(SelectionKey sk, ExecutorService es) {
        key = sk;
        executor = es;
        pool = new ByteBufferPool();
    }

    public void handle() {
        SocketChannel channel = (SocketChannel) key.channel();
        try {
            if (!channel.isConnected()) {
                System.out.println(channel.getRemoteAddress() + " close connect");
                return;
            }

            ByteBuffer b1 = pool.borrowObject();
            int n = channel.read(b1); // 执行 read 之后,selector.select() 才不会再次监听到同一个读事件
            executor.execute(() -> {
                try {
                    if (n > 0) {
                        String tn = Thread.currentThread().getName();
                        String str = String.format("%s %s ", "[MSReactor]", tn);
                        ByteBuffer b2 = pool.borrowObject();
                        b2.put(str.getBytes());
                        b1.flip();  // reset read pos
                        b2.put(b1);
                        b2.flip();  // reset read pos
                        channel.write(b2);
                        b1.clear();
                        b2.clear();
                        pool.returnObject(b1);
                        pool.returnObject(b2);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        } catch (IOException e) {
            try {
                channel.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
            e.printStackTrace();
        }
    }
}

(5)ByteBufferPool.java

package main.java.io.reactor.ms;

import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Queue;

public class ByteBufferPool {
    private final int cap;
    private final int bufferSize = 256;
    private final Queue<ByteBuffer> pool;

    public ByteBufferPool() {
        this(10);
    }

    public ByteBufferPool(int initialCap) {
        cap = initialCap;
        pool = new LinkedList<>();
        initPool();
    }

    private void initPool() {
        for (int i=0; i<cap; i++) {
            pool.offer(ByteBuffer.allocate(bufferSize));
        }
    }

    public boolean returnObject(ByteBuffer buffer) {
        return pool.offer(buffer);
    }

    public ByteBuffer borrowObject() {
        ByteBuffer buffer = pool.poll();
        if (buffer == null) {
            buffer = ByteBuffer.allocate(bufferSize);
        }
        return buffer;
    }
}

看了源码,你是否发现ByteBuffer使用起来有点不方便。是的,JDK提供的ByteBuffer使用起来很不方便,甚至容易出错,需要开发人员注意向ByteBuffer写完数据,程序要读取数据之前要flip进行读写转换,其实也就是reset read/write position

ByteBuffer不易使用在于它使用一个变量实现了读写指针,从而在写完之后读取数据时,要手动地进行读写转换。而如果使用过Netty地同学会发现,Netty提供的ByteBuf就不需要这个操作,因为ByteBuf使用了两个变量来表示读和写指针,从而避免了程序要手动地读写转换,这或许就是设计的魅力吧。

此外,为了提高读写效率,我用队列简单对BytBuffer实现了池化。

实现代码

代码请看GitHub仓库:
github - flowers-bloom - io

如果访问速度太慢,可访问Gitee
gitee - flowers-bloom - io

参考

posted @ 2021-09-02 20:55  flowers-bloom  阅读(395)  评论(0编辑  收藏  举报