一个 Reactor 模型的实现

  Reactor 模型不再介绍,网上有很多。在实现的过程中有一些收获:

  1. Reactor 广义上说,事件发生时便会触发为事件注册的处理函数。在注册感兴趣的事件时,需要将处理函数一并绑定到事件上,在 NIO 中我们通过 SelectionKey 的 attachment 携带函数对象。

  2. 使用 NIO ,应该摒弃 BIO 编程时的阻塞思维。如:

            while(writeBuffer.remaining()>0){
                socketChannel.write(writeBuffer);
            }

  这种繁忙等待的写法,放在以 NIO 为内核的代码中会比较另类,白白占用了 CPU 资源。

  完全可以依赖其事件通知机制,在写事件未就绪时直接返回让线程做其它事,等待写事件到来再触发写入数据的方法。

  当然这种处理方式需要我们自己判断是否写入已完成,因为一次写入可能需要多次写事件。

  并且一旦写事件完成,我们必须取消 selector 对该 channel 写事件的关注。因为写事件就绪是指 socket 在内核空间的输出缓冲区还有剩余空间可写,如果不取消关注,即使没有数据需要发送,只要该 socket 的内核输出缓冲区还有空间,就会不断的触发写事件。这可能浪费大量 CPU 资源。

  这样,在发送数据量较大且网速较慢时,CPU 不会将时间浪费在等待 socket 输出缓冲区有空余 上。充分发挥 NIO 的性能优势。

  3. 使用 TCP 协议进行数据传输需要我们自己实现报文的定界,避免 分包 与 粘包 的问题。在 NIO 的处理中,我们可以将多读的或者需要补齐的数据放在 SelectionKey 的 attachment 中,等待该 key 下次被激活再接着处理。

  另外牵扯四次挥手,如果我们使用短连接,服务端响应后直接 close 掉连接,客户端接收响应报文时不需要定界。因为服务端发送了关闭请求,客户端在读完接收缓冲区数据后 read 会返回 -1。否则 read 可能返回 0 而不是 -1,也就是说长连接下不能通过 read 的返回值进行定界。

  4. 本次实现采用报文头两个字节为 16 进制报文长度,靠该长度进行定界的策略。也可以采用其他策略定界,可以在凑齐一条完整报文后将报文直接转交给业务处理函数处理。也可以将凑齐的报文保存在有序队列中,采用其他线程顺序的处理这些报文,将报文的接收与处理异步化。

  5. 本类实现仅用于学习,生产时应采用 Netty 等成熟框架,避免自己直接开发,除非项目中有成员深谙此领域的弯弯绕绕。

  下面是实现:

/**
 * @Author Niuxy
 * @Date 2020/5/31 8:15 下午
 * @Description 最基础的 reactor 模型
 * 本模型不负责 serverSocket 与 selector 的初始化
 * 入口处的并发需要通过负载策略将 socketChannel 的事件注册到不同的 selector 上,待验证
 * selector 底层使用的是 OS 提供给我们的事件通知器
 */
public class BasicReator implements Runnable {
    ServerSocketChannel serverSocketChannel;
    Selector selector;

    public BasicReator(ServerSocketChannel serverSocketChannel, Selector selector)
            throws ClosedChannelException {
        this.serverSocketChannel = serverSocketChannel;
        this.selector = selector;
        SelectionKey key = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        key.attach(new Acceptor());
    }

    @Override
    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                selector.select();
                Set keySet = selector.selectedKeys();
                Iterator iterator = keySet.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = (SelectionKey) iterator.next();
                    iterator.remove();
                    dispatch(key);
                }
            }
        } catch (RuntimeException re) {
            // 接收策略层面没有正确解析报文会抛出 RuntimeException,为了测试,遇到该异常立即终止
            throw re;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @Author Niuxy
     * @Date 2020/5/31 7:23 下午
     * @Description 核心的部分
     * 无论什么事件被触发,我们均执行与事件绑定的方法
     * 在注册感兴趣的事件时,就将处理该事同步件的方法绑定到了对应的 selectionKey 中
     */
    private void dispatch(SelectionKey key) {
        Runnable att = (Runnable) key.attachment();
        try {
            if (att == null) {
                return;
            }
            att.run();
        } catch (RuntimeException re) {
            throw re;
        } catch (Exception e) {
            //dispatch 层面面向唯一连接,具有处理异常的能力,捕获处理不影响其它连接
            //TO DO
            e.printStackTrace();
        }
    }

    /**
     * @Author Niuxy
     * @Date 2020/5/31 7:41 下午
     * @Description 处理连接请求,注册读写事件并绑定读写事件处理器
     */
    class Acceptor implements Runnable {
        @Override
        public void run() {
            try {
                SocketChannel socketChannel = serverSocketChannel.accept();
                if (socketChannel == null) {
                    return;
                }
                socketChannel.configureBlocking(false);
                SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);
                //报文接收策略,与连接一一绑定
                MessageHandler messageHandler = new PrintMessageHandlerImpl();
                ReciveRegister reciveRegister = new HLRegisterImpl(2, messageHandler);
                //注册 key 的同时 将事件处理的 "回调" 函数绑定到 key 上
                key.attach(new Handler(socketChannel, key, reciveRegister));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @Author Niuxy
     * @Date 2020/5/31 11:44 下午
     * @Description 读写处理器
     */
    class Handler implements Runnable {
        public static final int READING = 0, WRITING = 1;
        SocketChannel socketChannel;
        SelectionKey key;
        /**
         * @Author Niuxy
         * @Date 2020/6/2 9:29 下午
         * @Description
         * 在响应上一个请求前,我们不希望处理下一个请求,因此在 Handler 维护一个状态位,标识目前应当
         * 处理读事件还是写事件
         * 我们必须保证接收和回复的顺序性,保证客户端可以对响应做出正确的处理
         * 当然也有其它的处理方式,我们将响应数据装入一个有序队列,并顺序的处理这些响应。或者通过令牌将请求和响应
         * 进行对应。
         */
        int state = READING;
        ReciveRegister reciveRegister;
        String readResult = null;
        ByteBuffer writeBuffer = null;

        Handler(SocketChannel channel, SelectionKey key, ReciveRegister reciveRegister) {
            this.socketChannel = channel;
            this.key = key;
            this.reciveRegister = reciveRegister;
        }

        @Override
        public void run() {
            try {
                if (state == READING) {
                    read();
                } else {
                    write();
                }
                //TO DO :write() method
            } catch (RuntimeException rex) {
                throw rex;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private void read() throws Exception {
            String re = reciveRegister.doRecive(socketChannel);
            if (re != null && re != "") {
                readResult = re;
                state = WRITING;
                key.interestOps(SelectionKey.OP_WRITE);
            }
        }

        private void write() throws IOException {
            if (this.readResult == null || readResult == "") {
                return;
            }
            //如果不是第一次触发写事件,接着上次的写
            if (writeBuffer == null) {
                writeBuffer = ByteBuffer.wrap(this.readResult.getBytes());
            }

            //该循环处理发送缓冲区处理速度小于网卡发送速度,无法一次性将 buffer 中数据写入发送缓冲区的情况
            socketChannel.write(writeBuffer);
            if (writeBuffer.position() != writeBuffer.limit()) {
                return;
            }
            writeBuffer = null;
            readResult = null;
            state = READING;
            //写完将兴趣移除,否则会将 CPU 跑满
       key.interestOps(SelectionKey.OP_READ); } } }

  定界策略与服务分发通过策略模式进行了解耦,定界策略的实现:

public interface ReciveRegister {
    public String doRecive(SocketChannel socketChannel) throws Exception;
}
/**
 * @Author Niuxy
 * @Date 2020/5/28 8:36 下午
 * @Description 报文头标示报文长度的定界策略,由于存在类成员缓冲区的原因需要与连接一一绑定
 */
public class HLRegisterImpl implements ReciveRegister {
    //报文头长度
    private int headLength = 0;
    //报文内容长度
    private int messageLength = 0;

    private MessageHandler messageHandler;

    boolean isCache = false;
    ByteBuffer headCacheBuffer;
    ByteBuffer messageCacheBuffer;

    public HLRegisterImpl(int headLength, MessageHandler messageHandler) {
        this.messageHandler = messageHandler;
        this.headLength = headLength;
        headCacheBuffer = ByteBuffer.allocate(headLength);
    }

    @Override
    public String doRecive(SocketChannel socketChannel) throws Exception {
        //判断是否已读取报文头
        if (messageLength == 0) {
            int readLen = socketChannel.read(headCacheBuffer);
            if (Util.isFullBuffer(headCacheBuffer)) {
                headCacheBuffer.flip();
                String messageLenthStr = Util.bytes2HexString(headCacheBuffer.array());
//                messageLength = headCacheBuffer.getInt();
                messageLength = Integer.parseInt(messageLenthStr, 16);
                System.out.println(messageLength);
                messageCacheBuffer = ByteBuffer.allocate(messageLength);
                headCacheBuffer.clear();
                return null;
            }
        } else {
            int readLen = socketChannel.read(messageCacheBuffer);
            String re = "";
            if (Util.isFullBuffer(messageCacheBuffer)) {
                re = messageHandler.doHandler(socketChannel, messageCacheBuffer);
                messageLength = 0;
                headLength = 0;
                messageCacheBuffer = null;
                System.gc();
                return re;
            }
            return null;
        }
        return null;
    }

}

  报文处理:

public interface MessageHandler {
    public String doHandler(SocketChannel socketChannel,ByteBuffer messageBuffer) throws Exception;
}
public class PrintMessageHandlerImpl implements MessageHandler {
    String target = "hellow server!hellow server!hellow server!hellow server!hellow server!hellow server!hellow server!hellowhellow server!hellow server!hellow server!hellow server!hellow server!hellow server!hellow";

    @Override
    public String doHandler(SocketChannel socketChannel, ByteBuffer messageBuffer) throws Exception {
        String message = new String(messageBuffer.array());
        String re = "";
        if (!target.equals(message)) {
            System.out.println("error!: " + message);
            re="error&/n/*";
        } else {
            System.out.println("success!");
            re="successsuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssucc" +
                    "esssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssuccesssucces"
                    + "successsuccesssuccesssuccesssuccesssuccesssuccesssuccessssuccesssuccesssuccesssuccesssuccesss" +
                    "uccesssuccesssuccesssuccesssuccesssuccesssuccess!&/n/*";
        }
        messageBuffer = null;
        return re;
    }
}

  测试客户端,直接使用 BIO 编写:

public class TestClient {
    public static void main(String[] args) throws Exception {
        final String msg = "hellow server!hellow server!hellow server!hellow server!hellow server!hellow server!hellow server!hellowhellow server!hellow server!hellow server!hellow server!hellow server!hellow server!hellow";
        Thread thread0 = new Thread(() -> {
            sendMsg(msg);
        });
        Thread thread1 = new Thread(() -> {
            sendMsg(msg);
        });
        Thread thread2 = new Thread(() -> {
            sendMsg(msg);
        });
        Thread thread3 = new Thread(() -> {
            sendMsg(msg);
        });
        thread0.start();
        thread1.start();
        thread2.start();
        thread3.start();

    }

    private static void sendMsg(String msg) {
        try {
            for (int i = 0; i < 100; i++) {
                send(msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void send(String message) throws Exception {
        Socket socket = new Socket("127.0.0.1", 8000);
        byte[] messageBytes = message.getBytes();
        Integer length = messageBytes.length;
        System.out.println(length);
        for (int i0 = 0; i0 < 10; i0++) {
            OutputStream outputStream = socket.getOutputStream();
            String lenStr16 = Util.numToHex16(length);
            outputStream.write(Util.hexTobytes(lenStr16));
            Thread.sleep(100);
            outputStream.write(messageBytes);
            outputStream.flush();
            BufferedInputStream in = new BufferedInputStream(socket.getInputStream());
            byte[] buffer = new byte[100];
            int len = 0;
            StringBuilder sb = new StringBuilder();
            while ((len = in.read(buffer)) != -1) {
                //重要!为了复用 buffer,我们必须知道哪些是新写入的数据,哪些是已经处理过的数据
                String data = new String(buffer, 0, len);
                sb.append(data);
                //必须有自己的定界方式。否则只要服务端不关闭连接,len 永远不会是 -1,必须用特定的方式标识某次读取已结束!
                if (data != null && data.contains("&/n/*")) {
                    break;
                }
            }
            System.out.println(sb.substring(0));
        }
    }

    private static void printArr(byte[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i]);
        }
    }

}

 

posted @ 2020-06-02 23:38  牛有肉  阅读(603)  评论(0编辑  收藏  举报