JavaNIO--3.Reactor模式实现echo服务器

一、什么是Reactor模式

1.1概念

The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers

这是维基百科上对于Reactor的定义。

大致意思是说,reactor设计模式是一种事件处理模式,这种模式针对同时有一个或多个请求发送到事件处理器(service handler),这个事件处理器会采用多路复用(demultiplexes )方法,同步的将这些请求分发到请求处理器(request handlers)

这里写图片描述

听起来可能有些拗口,但其中的精髓部分就是多路复用的模式进行请求的分发,而对于服务器来说,这些请求其实就是Socket通信中的I/O请求。

说到这里,就和我们前面所说到的JavaIO模型中的I/O多路复用,建立关系了。
没错,Reactor模式其实就是依靠这种I/O模型来实现请求的分发。

1.2NIO如何实现Reactor模式

根据上面提到的,Reactor模式要依靠多路复用的分发模式,联想到Java中的多路复用I/O是采用基于NIOSelector实现,所以要实现Reactor模式,还是要依靠JavaNIO的核心组件。

这里写图片描述

这就是一个基础的Reactor模式的模型图,可以和定义中的Reactor模式图进行对比,client相当于inputer,而Reactor本身相当于ServiceHandler,而其通过Selector实现的多路复用进行分发请求到具体的Request Handler,用于进行读取消息,编码解码,返回消息等操作。

二、JavaNIO实现基于Reactor模式的ECHO服务器

2.1初始化事件处理器(service handler)

public class NioReactor {
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;

    public NioReactor(int port) throws IOException {
        // 初始化Selector和Channel,并完成注册
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 这里注意绑定到key上
        selectionKey.attach(new Acceptor());
        serverSocketChannel.bind(new InetSocketAddress(port));
    }

    /**
     * 内部类Accept,实际TCP连接的建立和SocketChannel的获取在这个类中实现
     * 根据类的实现,可以发现一个Accept类对应一个ServerSocketChannel
     * 
     * @author CringKong
     *
     */
    private class Acceptor implements Runnable{

        @Override
        public void run() {
            try {
                SocketChannel socketChannel = serverSocketChannel.accept();
                if (socketChannel != null) {
                    // 创建一个新的处理类
                    new Handler(socketChannel, selector);
                }
            } catch (Exception e) {
                // TODO: handle exception
                e.printStackTrace();
            }
        }
    }
    ****/
}

2.2多路复用+循环分发

    /**
     * 多路复用,循环分发任务
     * @throws IOException
     */
    private void dispatchLoop() throws IOException {
        while(true) {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()) {
                // 任务分派器,无差别的分派任务
                dispatchTask(iterator.next());
            }
            selectionKeys.clear();
        }
    }

可以看到,这个dispatchLoop和我们之前实现的NIOserver服务器中的selectLoop实现很相似。其实原理都是一致的,只不过在Reactor模式中,他就是事件处理器会采用多路复用(demultiplexes )方法

2.3同步任务派发器(dispatch)

    /**
     * 任务分派器的进阶版,耦合性降低,拓展性增强
     * 子类只需要实现Runnable接口,并重写run()方法,就可以实现多种任务的无差别分派
     * 
     * @param selectionKey
     */
    private void dispatchTask(SelectionKey taskSelectionKey) {
        Runnable runnable = (Runnable)taskSelectionKey.attachment();
        if (runnable != null) {
            runnable.run();
        }
    }

    /**
     * 任务分派器基础版本,根据通道内准备好的操作类型进行任务分派
     * 
     * @param selectionKey
     * @param selector
     */
    private void taskDispatcherBasicVersion(SelectionKey taskSelectionKey,Selector selector) {
        if (taskSelectionKey.isAcceptable()) {
            // 执行自己实现的接收方法,建立连接创建SocketChannl并且注册到Selector
        }else if (taskSelectionKey.isReadable()) {
            // 执行自己实现的读方法,读取通道内容并做进一步处理
        }
    }

2.4请求处理器(Request Handler)

    /**
     * 内部类Handler,实际的I/O操作和编码解码都要在这其中是实现
     * 可以发现,一个Handler类对应一个Socket连接
     * 
     * @author CringKong
     *
     */
    private class Handler implements Runnable{

        private SocketChannel socketChannel;
        private SelectionKey selectionKey;
        private ByteBuffer oldBuffer;

        public Handler(SocketChannel socketChannel , Selector selector) throws IOException {
            // 初始化的oldBuffer为null
            oldBuffer = null;
            this.socketChannel = socketChannel;
            socketChannel.configureBlocking(false);

            // 在构造函数里就注册通道到Selector
            this.selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
            // attach(this)将自身对象绑定到key上,作用是使dispatch()函数正确使用
            selectionKey.attach(this);
            // Selector.wakeup()方法会使阻塞中的Selector.select()方法立刻返回
            selector.wakeup();
        }

        @Override
        public void run() {
            try {
                readDate(selectionKey);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        /**
         * 读取数据并进行操作的函数
         * @param selectionKey
         * @throws IOException
         */
        private void readDate(SelectionKey selectionKey) throws IOException {

            ByteBuffer newBuffer = ByteBuffer.allocate(64);

            int read;
            while((read = socketChannel.read(newBuffer))<=0) {
                return;
            }

            newBuffer.flip();
            String line = readLine(newBuffer);
            if (line != null) {

                // 如果这次读到了行结束符,就将原来不含有行结束符的buffer合并位一行
                String sendData = readLine(mergeBuffer(oldBuffer, newBuffer));
                if (readLineContent(sendData).equalsIgnoreCase("exit")) { // 如果这一行的内容是exit就断开连接
                    socketChannel.close();
                    return;
                }
                // 然后直接发送回到客户端
                ByteBuffer sendBuffer = ByteBuffer.wrap(sendData.getBytes("utf-8"));
                while (sendBuffer.hasRemaining()) {
                    socketChannel.write(sendBuffer);
                }
                oldBuffer = null;
            }else {
                // 如果这次没读到行结束付,就将这次读的内容和原来的内容合并
                oldBuffer = mergeBuffer(oldBuffer, newBuffer);
            }

        }

        /**
         * 读取ByteBuffer直到一行的末尾
         * 返回这一行的内容,包括换行符
         * 
         * @param buffer
         * @return String 读取到行末的内容,包括换行符 ; null 如果没有换行符
         * @throws UnsupportedEncodingException
         */
        private String readLine(ByteBuffer buffer) throws UnsupportedEncodingException {
            // windows中的换行符表示手段 "\r\n"
            // 基于windows的软件发送的换行符是会是CR和LF
            char CR = '\r';
            char LF = '\n';

            boolean crFound = false;
            int index = 0;
            int len = buffer.limit();
            buffer.rewind();
            while(index < len) {
                byte temp = buffer.get();
                if (temp == CR) {
                    crFound = true;
                }
                if (crFound && temp == LF) {
                    // Arrays.copyOf(srcArr,length)方法会返回一个 源数组中的长度到length位 的新数组
                    return new String(Arrays.copyOf(buffer.array(), index+1),"utf-8");
                }
                index ++;
            }
            return null;
        }


        /**
         * 获取一行的内容,不包括换行符
         * @param buffer
         * @return String 行的内容
         * @throws UnsupportedEncodingException
         */
        private String readLineContent(String line) throws UnsupportedEncodingException {
            System.out.print(line);
            System.out.print(line.length());
            return line.substring(0, line.length() - 2);
        }

        /**
         * 对传入的Buffer进行拼接
         * @param oldBuffer
         * @param newBuffer
         * @return ByteBuffer 拼接后的Buffer
         */
        public ByteBuffer mergeBuffer(ByteBuffer oldBuffer,ByteBuffer newBuffer) {
            // 如果原来的Buffer是null就直接返回
            if (oldBuffer == null) {
                return newBuffer;
            }
            // 如果原来的Buffer的剩余长度可容纳新的buffer则直接拼接
            newBuffer.rewind();
            if (oldBuffer.remaining() > (newBuffer.limit()-newBuffer.position())) {
                return oldBuffer.put(newBuffer);
            }

            // 如果不是以上两种情况就构建新的Buffer进行拼接
            int oldSize = oldBuffer != null?oldBuffer.limit():0;
            int newSize = newBuffer != null?newBuffer.limit():0;
            ByteBuffer result = ByteBuffer.allocate(oldSize+newSize);

            result.put(Arrays.copyOfRange(oldBuffer.array(), 0, oldSize));
            result.put(Arrays.copyOfRange(newBuffer.array(), 0, newSize));

            return result;
        }
    }

这里的Handler 是根据我们之前实现的ECHO服务器直接改写的,具体实现请跳转参考。

三、完整代码

package reactor;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Set;

public class NioReactor {

    private Selector selector;
    private ServerSocketChannel serverSocketChannel;


    public NioReactor(int port) throws IOException {
        // 初始化Selector和Channel,并完成注册
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        selectionKey.attach(new Acceptor());
        serverSocketChannel.bind(new InetSocketAddress(port));
    }

    /**
     * 多路复用,循环分发任务
     * @throws IOException
     */
    private void dispatchLoop() throws IOException {
        while(true) {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()) {
                // 任务分派器,无差别的分派任务
                dispatchTask(iterator.next());
            }
            selectionKeys.clear();
        }
    }

    /**
     * 任务分派器的进阶版,耦合性降低,拓展性增强
     * 子类只需要实现Runnable接口,并重写run()方法,就可以实现多种任务的无差别分派
     * 
     * @param selectionKey
     */
    private void dispatchTask(SelectionKey taskSelectionKey) {
        Runnable runnable = (Runnable)taskSelectionKey.attachment();
        if (runnable != null) {
            runnable.run();
        }
    }

    /**
     * 任务分派器基础版本,根据通道内准备好的操作类型进行任务分派
     * 
     * @param selectionKey
     * @param selector
     */
    private void taskDispatcherBasicVersion(SelectionKey taskSelectionKey,Selector selector) {
        if (taskSelectionKey.isAcceptable()) {
            // 执行自己实现的接收方法,建立连接创建SocketChannl并且注册到Selector
            new Acceptor().run();
        }else if (taskSelectionKey.isReadable()) {
            // 执行自己实现的读方法,读取通道内容并做进一步处理
        }
    }

    /**
     * 内部类Accept,实际TCP连接的建立和SocketChannel的获取在这个类中实现
     * 根据类的实现,可以发现一个Accept类对应一个ServerSocketChannel
     * 
     * @author CringKong
     *
     */
    private class Acceptor implements Runnable{

        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {
                SocketChannel socketChannel = serverSocketChannel.accept();
                if (socketChannel != null) {
                    // 创建一个新的处理类
                    new Handler(socketChannel, selector);
                }
            } catch (Exception e) {
                // TODO: handle exception
                e.printStackTrace();
            }
        }
    }

    /**
     * 内部类Handler,实际的I/O操作和编码解码都要在这其中是实现
     * 可以发现,一个Handler类对应
     * 
     * @author CringKong
     *
     */
    private class Handler implements Runnable{

        private SocketChannel socketChannel;
        private SelectionKey selectionKey;
        private ByteBuffer oldBuffer;

        public Handler(SocketChannel socketChannel , Selector selector) throws IOException {
            // 初始化的oldBuffer为null
            oldBuffer = null;
            this.socketChannel = socketChannel;
            socketChannel.configureBlocking(false);

            // 在构造函数里就注册通道到Selector
            this.selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
            // attach(this)将自身对象绑定到key上,作用是使dispatch()函数正确使用
            selectionKey.attach(this);
            // Selector.wakeup()方法会使阻塞中的Selector.select()方法立刻返回
            selector.wakeup();
        }

        @Override
        public void run() {
            try {
                readDate(selectionKey);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        /**
         * 读取数据并进行操作的函数
         * @param selectionKey
         * @throws IOException
         */
        private void readDate(SelectionKey selectionKey) throws IOException {

            ByteBuffer newBuffer = ByteBuffer.allocate(64);

            int read;
            while((read = socketChannel.read(newBuffer))<=0) {
                return;
            }

            newBuffer.flip();
            String line = readLine(newBuffer);
            if (line != null) {

                // 如果这次读到了行结束符,就将原来不含有行结束符的buffer合并位一行
                String sendData = readLine(mergeBuffer(oldBuffer, newBuffer));
                if (readLineContent(sendData).equalsIgnoreCase("exit")) { // 如果这一行的内容是exit就断开连接
                    socketChannel.close();
                    return;
                }
                // 然后直接发送回到客户端
                ByteBuffer sendBuffer = ByteBuffer.wrap(sendData.getBytes("utf-8"));
                while (sendBuffer.hasRemaining()) {
                    socketChannel.write(sendBuffer);
                }
                oldBuffer = null;
            }else {
                // 如果这次没读到行结束付,就将这次读的内容和原来的内容合并
                oldBuffer = mergeBuffer(oldBuffer, newBuffer);
            }

        }

        /**
         * 读取ByteBuffer直到一行的末尾
         * 返回这一行的内容,包括换行符
         * 
         * @param buffer
         * @return String 读取到行末的内容,包括换行符 ; null 如果没有换行符
         * @throws UnsupportedEncodingException
         */
        private String readLine(ByteBuffer buffer) throws UnsupportedEncodingException {
            // windows中的换行符表示手段 "\r\n"
            // 基于windows的软件发送的换行符是会是CR和LF
            char CR = '\r';
            char LF = '\n';

            boolean crFound = false;
            int index = 0;
            int len = buffer.limit();
            buffer.rewind();
            while(index < len) {
                byte temp = buffer.get();
                if (temp == CR) {
                    crFound = true;
                }
                if (crFound && temp == LF) {
                    // Arrays.copyOf(srcArr,length)方法会返回一个 源数组中的长度到length位 的新数组
                    return new String(Arrays.copyOf(buffer.array(), index+1),"utf-8");
                }
                index ++;
            }
            return null;
        }


        /**
         * 获取一行的内容,不包括换行符
         * @param buffer
         * @return String 行的内容
         * @throws UnsupportedEncodingException
         */
        private String readLineContent(String line) throws UnsupportedEncodingException {
            System.out.print(line);
            System.out.print(line.length());
            return line.substring(0, line.length() - 2);
        }

        /**
         * 对传入的Buffer进行拼接
         * @param oldBuffer
         * @param newBuffer
         * @return ByteBuffer 拼接后的Buffer
         */
        public ByteBuffer mergeBuffer(ByteBuffer oldBuffer,ByteBuffer newBuffer) {
            // 如果原来的Buffer是null就直接返回
            if (oldBuffer == null) {
                return newBuffer;
            }
            // 如果原来的Buffer的剩余长度可容纳新的buffer则直接拼接
            newBuffer.rewind();
            if (oldBuffer.remaining() > (newBuffer.limit()-newBuffer.position())) {
                return oldBuffer.put(newBuffer);
            }

            // 如果不是以上两种情况就构建新的Buffer进行拼接
            int oldSize = oldBuffer != null?oldBuffer.limit():0;
            int newSize = newBuffer != null?newBuffer.limit():0;
            ByteBuffer result = ByteBuffer.allocate(oldSize+newSize);

            result.put(Arrays.copyOfRange(oldBuffer.array(), 0, oldSize));
            result.put(Arrays.copyOfRange(newBuffer.array(), 0, newSize));

            return result;
        }
    }

    // 测试
    public static void main(String[] args) {
        NioReactor reactor;
        try {
            reactor = new NioReactor(12345);
            reactor.dispatchLoop();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

四、测试结果

这个服务器和原来实现的ECHO服务器逻辑上没有任何区别,只是实现了Reactor模式。

这里写图片描述


参考资料:
- Reactor模式详解
- Scalable IO in Java – Doug Lea大神之作
- 细说Reactor模式
- Java进阶(五)Java I/O模型从BIO到NIO和Reactor模式

猜你喜欢

转载自blog.csdn.net/cringkong/article/details/80185198
今日推荐