• Reactor模型-单线程版


    Reactor模型是典型的事件驱动模型。在网络编程中,所谓的事件当然就是read、write、bind、connect、close等这些动作了。Reactor模型的实现有很多种,下面介绍最基本的三种:

    • 单线程版
    • 多线程版
    • 主从多线程版
    Key Word:Java NIO,Reactor模型,Java并发编程,Event-Driven

    单线程版本

    结构图(引用自Doug Lea的Scalable IO in Java)如下:

     
    Reactor模型图

    上图中Reactor是一个典型的事件驱动中心,客户端发起请求并建立连接时,会触发注册在多路复用器Selector上的SelectionKey.OP_ACCEPT事件,绑定在该事件上的Acceptor对象的职责就是接受请求,为接下来的读写操作做准备。

    Reactor设计如下:

    /**
     * Reactor
     * 
     * @author wqx
     *
     */
    public class Reactor implements Runnable {
    
        private static final Logger LOG = LoggerFactory.getLogger(Reactor.class);
        
        private Selector selector;
        
        private ServerSocketChannel ssc;
    
        private Handler DEFAULT_HANDLER = new Handler(){
            @Override
            public void processRequest(Processor processor, ByteBuffer msg) {
                //NOOP
            }
        };
        private Handler handler = DEFAULT_HANDLER;
        
        
        /**
         * 启动阶段
         * @param port
         * @throws IOException
         */
        public Reactor(int port, int maxClients, Handler serverHandler) throws IOException{
            selector = Selector.open();
            ssc = ServerSocketChannel.open();
            ssc.configureBlocking(false);
            ssc.socket().bind(new InetSocketAddress(port));
            
            this.handler = serverHandler;
            SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT);
            sk.attach(new Acceptor());
        }
        /**
         * 轮询阶段
         */
        @Override
        public void run() {
            while(!ssc.socket().isClosed()){
                try {
                    selector.select(1000);
                    Set<SelectionKey> keys;
                    synchronized(this){
                        keys = selector.selectedKeys();
                    }
                    Iterator<SelectionKey> it = keys.iterator();
                    while(it.hasNext()){
                        SelectionKey key = it.next();
                        dispatch(key);
                        it.remove();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            close();
        }
        
        public void dispatch(SelectionKey key){
            Runnable r = (Runnable)key.attachment();
            if(r != null)
                r.run();
        }
        /**
         * 用于接受TCP连接的Acceptor
         * 
         */
        class Acceptor implements Runnable{
    
            @Override
            public void run() {
                SocketChannel sc;
                try {
                    sc = ssc.accept();
                    if(sc != null){
                        new Processor(Reactor.this,selector,sc);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        
        public void close(){
            try {
                selector.close();
                if(LOG.isDebugEnabled()){
                    LOG.debug("Close selector");
                }
            } catch (IOException e) {
                LOG.warn("Ignoring exception during close selector, e=" + e);
            }
        }
        public void processRequest(Processor processor, ByteBuffer msg){
            if(handler != DEFAULT_HANDLER){
                handler.processRequest(processor, msg);
            }
        }
    }
    
    

    上面是典型的单线程版本的Reactor实现,实例化Reactor对象的过程中,在当前多路复用器Selector上注册了OP_ACCEPT事件,当OP_ACCEPT事件发生后,Reactor通过dispatch方法执行Acceptor的run方法,Acceptor类的主要功能就是接受请求,建立连接,并将代表连接建立的SocketChannel以参数的形式构造Processor对象。

    Processor的任务就是进行I/O操作。

    下面是Processor的源码:

    /**
     * Server Processor
     * 
     * @author wqx
     */
    public class Processor implements Runnable {
    
        private static final Logger LOG = LoggerFactory.getLogger(Processor.class);
    
        Reactor reactor;
    
        private SocketChannel sc;
    
        private final SelectionKey sk;
    
        private final ByteBuffer lenBuffer = ByteBuffer.allocate(4);
    
        private ByteBuffer inputBuffer = lenBuffer;
    
        private ByteBuffer outputDirectBuffer = ByteBuffer.allocateDirect(1024 * 64);
    
        private LinkedBlockingQueue<ByteBuffer> outputQueue = new LinkedBlockingQueue<ByteBuffer>();
    
        public Processor(Reactor reactor, Selector sel,SocketChannel channel) throws IOException{
            this.reactor = reactor;
            sc = channel;
            sc.configureBlocking(false);
            sk = sc.register(sel, SelectionKey.OP_READ);
            sk.attach(this);
            sel.wakeup();
        }
    
        @Override
        public void run() {
            if(sc.isOpen() && sk.isValid()){
                if(sk.isReadable()){
                    doRead();
                }else if(sk.isWritable()){
                    doSend();
                }
            }else{
                LOG.error("try to do read/write operation on null socket");
                try {
                    if(sc != null)
                        sc.close();
                } catch (IOException e) {}
            }
        }
        private void doRead(){
            try {
                int byteSize = sc.read(inputBuffer);
                
                if(byteSize < 0){
                    LOG.error("Unable to read additional data");
                }
                if(!inputBuffer.hasRemaining()){
                    
                    if(inputBuffer == lenBuffer){
                        //read length
                        inputBuffer.flip();
                        int len = inputBuffer.getInt();
                        if(len < 0){
                            throw new IllegalArgumentException("Illegal data length");
                        }
                        //prepare for receiving data
                        inputBuffer = ByteBuffer.allocate(len);
                    }else{
                        //read data
                        if(inputBuffer.hasRemaining()){
                            sc.read(inputBuffer);
                        }
                        if(!inputBuffer.hasRemaining()){
                            inputBuffer.flip();
                            processRequest();
                            //clear lenBuffer and waiting for next reading operation 
                            lenBuffer.clear();
                            inputBuffer = lenBuffer;
                        }
                    }
                }
            } catch (IOException e) {
                LOG.error("Unexcepted Exception during read. e=" + e);
                try {
                    if(sc != null)
                        sc.close();
                } catch (IOException e1) {
                    LOG.warn("Ignoring exception when close socketChannel");
                }
            }
        }
    
        /**
         * process request and get response
         * 
         * @param request
         * @return
         */
        private void processRequest(){
            reactor.processRequest(this,inputBuffer);
        }
        private void doSend(){
            try{
                /**
                 * write data to channel:
                 * step 1: write the length of data(occupy 4 byte)
                 * step 2: data content
                 */
                if(outputQueue.size() > 0){
                    ByteBuffer directBuffer = outputDirectBuffer;
                    directBuffer.clear();
                    
                    for(ByteBuffer buf : outputQueue){
                        buf.flip();
                        
                        if(buf.remaining() > directBuffer.remaining()){
                            //prevent BufferOverflowException
                            buf = (ByteBuffer) buf.slice().limit(directBuffer.remaining());
                        }
                        //transfers the bytes remaining in buf into  directBuffer
                        int p = buf.position();
                        directBuffer.put(buf);
                        //reset position
                        buf.position(p);
    
                        if(!directBuffer.hasRemaining()){
                            break;
                        }
                    }
                    directBuffer.flip();
                    int sendSize = sc.write(directBuffer);
                    
                    while(!outputQueue.isEmpty()){
                        ByteBuffer buf = outputQueue.peek();
                        int left = buf.remaining() - sendSize;
                        if(left > 0){
                            buf.position(buf.position() + sendSize);
                            break;
                        }
                        sendSize -= buf.remaining();
                        outputQueue.remove();
                    }
                }
                synchronized(reactor){
                    if(outputQueue.size() == 0){
                        //disable write
                        disableWrite();
                    }else{
                        //enable write
                        enableWrite();
                    }
                }
            } catch (CancelledKeyException e) {
                LOG.warn("CancelledKeyException occur e=" + e);
            } catch (IOException e) {
                LOG.warn("Exception causing close, due to " + e);
            }
        }
        public void sendBuffer(ByteBuffer bb){
            try{
                synchronized(this.reactor){
                    if(LOG.isDebugEnabled()){
                        LOG.debug("add sendable bytebuffer into outputQueue");
                    }
                    //wrap ByteBuffer with length header
                    ByteBuffer wrapped = wrap(bb);
                    
                    outputQueue.add(wrapped);
                    
                    enableWrite();
                }
            }catch(Exception e){
                LOG.error("Unexcepted Exception: ", e);
            }
        }
        
        private ByteBuffer wrap(ByteBuffer bb){
            bb.flip();
            lenBuffer.clear();
            int len = bb.remaining();
            lenBuffer.putInt(len);
            ByteBuffer resp = ByteBuffer.allocate(len+4);
            lenBuffer.flip();
            
            resp.put(lenBuffer);
            resp.put(bb);
            return resp;
        }
        private void enableWrite(){
            int i = sk.interestOps();
            if((i & SelectionKey.OP_WRITE) == 0){
                sk.interestOps(i | SelectionKey.OP_WRITE);
            }
        }
        private void disableWrite(){
            int i = sk.interestOps();
            if((i & SelectionKey.OP_WRITE) == 1){
                sk.interestOps(i & (~SelectionKey.OP_WRITE));           
            }
        }
    }
    
    

    其实Processor要做的事情很简单,就是向selector注册感兴趣的读写时间,OP_READ或OP_WRITE,然后等待事件触发,做相应的操作。

        @Override
        public void run() {
            if(sc.isOpen() && sk.isValid()){
                if(sk.isReadable()){
                    doRead();
                }else if(sk.isWritable()){
                    doSend();
                }
            }else{
                LOG.error("try to do read/write operation on null socket");
                try {
                    if(sc != null)
                        sc.close();
                } catch (IOException e) {}
            }
        }
    

    而doRead()和doSend()方法稍微复杂了一点,这里其实处理了用TCP协议进行通信时必须要解决的问题:TCP粘包拆包问题

    TCP粘包拆包问题

    我们都知道TCP协议是面向字节流的,而字节流是连续的,无法有效识别应用层数据的边界。如下图:


     
    粘包拆包示意图

    上图显示的应用层有三个数据包,D1,D2,D3.当应用层数据传到传输层后,可能会出现粘包拆包现象。

    TCP协议的基本传输单位是报文段,而每个报文段最大有效载荷是有限制的,一般以太网MTU为1500,去除IP头20B,TCP头20B,那么剩下的1460B就是传输层最大报文段的有效载荷。如果应用层数据大于该值(如上图中的数据块D2),那么传输层就会进行拆分重组。

    解决方案

    1. 消息定长(通信双方发送的消息固定长度,缺点很明显:浪费可耻!!!)
    2. 每个消息之间加分割符(缺点:消息编解码耗时,并且如果消息体中本省就包含分隔字符,需要进行转义,效率低)
    3. 每个数据包加个Header!!!(header中指定后面数据的长度,这不就是Tcp、Ip协议通用的做法么。。。哈哈)

    采用方案三

    示意图如下:


     
    数据包结构

    header区占用4B,内容为数据的长度。too simple。。。-_-

    理论有了,下面具体分析下Read、Write的实现过程:

    doRead
    inputBuffer负责接受数据,lenBuffer负责接受数据长度,初始化的时候,将lenBuffer赋给inputBuffer,定义如下:

    private final ByteBuffer lenBuffer = ByteBuffer.allocate(4);
    private ByteBuffer inputBuffer = lenBuffer;
    
    1. 如果inputBuffer == lenBuffer,那么从inputBuffer中读取出一个整型值len,这个值就是接下来要接受的数据的长度。同时分配一个大小为len的内存空间,并复制给inputBuffer,准备接受数据!!!
        private void doRead(){
            try {
                int byteSize = sc.read(inputBuffer);
                
                if(byteSize < 0){
                    LOG.error("Unable to read additional data");
                }
                if(!inputBuffer.hasRemaining()){
                    
                    if(inputBuffer == lenBuffer){
                        //read length
                        inputBuffer.flip();
                        int len = inputBuffer.getInt();
                        if(len < 0){
                            throw new IllegalArgumentException("Illegal data length");
                        }
                        //prepare for receiving data
                        inputBuffer = ByteBuffer.allocate(len);
                    else{...}
    
    1. 如果inputBuffer != lenBuffer,那么开始接受数据吧!
    if(inputBuffer == lenBuffer){
            //。。。
    }else{
        //read data
        if(inputBuffer.hasRemaining()){
            sc.read(inputBuffer);
        }
        if(!inputBuffer.hasRemaining()){
            inputBuffer.flip();
            processRequest();
            //clear lenBuffer and waiting for next reading operation 
            lenBuffer.clear();
            inputBuffer = lenBuffer;
        }
    }
    

    注意

    1. 必须保证缓冲区是满的,即inputBuffer.hasRemaining()=false
    2. processRequest后,将inputBuffer重新赋值为lenBuffer,为下一次读操作做准备。

    doWrite

    用户调用sendBuffer方法发送数据,其实就是将数据加入outputQueue,这个outputQueue就是一个发送缓冲队列。

    public void sendBuffer(ByteBuffer bb){
            try{
                synchronized(this.reactor){
                    if(LOG.isDebugEnabled()){
                        LOG.debug("add sendable bytebuffer into outputQueue");
                    }
                    //wrap ByteBuffer with length header
                    ByteBuffer wrapped = wrap(bb);
                    
                    outputQueue.add(wrapped);
                    
                    enableWrite();
                }
            }catch(Exception e){
                LOG.error("Unexcepted Exception: ", e);
            }
        }
    

    doSend方法就很好理解了,无非就是不断从outputQueue中取数据,然后写入channel中即可。过程如下:

    将发送队列outputQueue中的数据写入缓冲区outputDirectBuffer:

    1. 清空outputDirectBuffer,为发送数据做准备
    2. 将outputQueue数据写入outputDirectBuffer
    3. 调用socketChannel.write(outputDirectBuffer);将outputDirectBuffer写入socket缓冲区

    执行步骤2的时候,我们可能会遇到这么几种情况:

    1.某个数据包大小超过了outputDirectBuffer剩余空间大小

    2.outputDirectBuffer已被填满,但是outputQueue仍有待发送的数据

    执行步骤3的时候,也可能出现下面两种情况:

    1.outputDirectBuffer被全部写入socket缓冲区

    2.outputDirectBuffer只有部分数据或者压根就没有数据被写入socket缓冲区

    实现过程可以结合源码,这里重点分析下面几个点:

    为什么需要重置buf的position

    int p = buf.position();
    directBuffer.put(buf);
    //reset position
    buf.position(p);
    

    写入directBuffer的数据是即将被写入SocketChannel的数据,问题就在于:当我们调用

    int sendSize = sc.write(directBuffer);
    

    的时候,directBuffer中的数据都被写入Channel了吗?明显是不确定的(具体可以看java.nio.channels.SocketChannel.write(ByteBuffer src)的doc文档)

    上面的问题如何解决

    思路很简单,根据write方法返回值sendSize,遍历outputQueue中的ByteBuffer,根据buf.remaining()和sendSize的大小,才可以确定buf是否真的被发送了。如下所示:

    while(!outputQueue.isEmpty()){
        ByteBuffer buf = outputQueue.peek();
        int left = buf.remaining() - sendSize;
        if(left > 0){
            buf.position(buf.position() + sendSize);
            break;
        }
        sendSize -= buf.remaining();
        outputQueue.remove();
    }
    

    网络通信基本解决,上面的处理思路是参照Zookeeper网络模块的实现,有兴趣可以看Zookeeper相应源码。

    测试

    Server端:

    public class ServerTest {
    
        private static int PORT = 8888;
        
        public static void main(String[] args) throws IOException, InterruptedException {
            
            Thread t = new Thread(new Reactor(PORT,1024,new MyHandler()));
            t.start();
            System.out.println("server start");
            t.join();
        }
    }
    

    用户自定义Handler:

    public class MyHandler implements Handler {
        
        @Override
        public void processRequest(Processor processor, ByteBuffer msg) {
            byte[] con = new byte[msg.remaining()];
            msg.get(con);
            
            String str = new String(con,0,con.length);
            
            String resp = "";
            switch(str){
            case "request1":resp = "response1";break;
            case "request2":resp = "response2";break;
            case "request3":resp = "response3";break;
            default :resp = "";
            }
            
            ByteBuffer buf = ByteBuffer.allocate(resp.getBytes().length);
            buf.put(resp.getBytes());
            
            processor.sendBuffer(buf);
        }
    }
    

    client端

    public class ClientTest {
    
        private static String HOST = "localhost";
        private static int PORT = 8888;
    
        public static void main(String[] args) throws IOException {
            
            Client client = new Client();
            client.socket().setTcpNoDelay(true);
            
            client.connect(
                    new InetSocketAddress(HOST,PORT));
            
            ByteBuffer msg;
            for(int i = 1; i <= 3; i++){
                msg = ByteBuffer.wrap(("request" + i).getBytes());
                System.out.println("send-" + "request" + i);
                
                ByteBuffer resp = client.send(msg);
                byte[] retVal = new byte[resp.remaining()];
                resp.get(retVal);
    
                System.out.println("receive-" + new String(retVal,0,retVal.length));
                
            }
        }
    }
    

    输出:

    send-request1
    receive-response1
    send-request2
    receive-response2
    send-request3
    receive-response3
    

    Client是一个客户端工具类,简单封装了发送ByteBuffer前,添加header的逻辑。详见源码。Client.java

    总结

    在这种实现方式中,dispatch方法是同步阻塞的!!!所有的IO操作和业务逻辑处理都在NIO线程(即Reactor线程)中完成。如果业务处理很快,那么这种实现方式没什么问题,不用切换到用户线程。但是,想象一下如果业务处理很耗时(涉及很多数据库操作、磁盘操作等),那么这种情况下Reactor将被阻塞,这肯定是我们不希望看到的。解决方法很简单,业务逻辑进行异步处理,即交给用户线程处理。

    下面分析下单线程版的Reactor模型的缺点:

    • 自始自终都只有一个Reactor线程,缺点很明显:Reactor意外挂了,整个系统也就无法正常工作,可靠性太差。
    • 单线程的另外一个问题是在大负载的情况下,Reactor的处理速度必然会成为系统性能的瓶颈。


    作者:TopGun_Viper
    链接:https://www.jianshu.com/p/eb28811421e3
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    poj 3613(经过N条边的最短路)
    poj 3328(多起点多终点的最短路)
    poj 3311(floyd+状态压缩)
    新CCIE笔记-IP网络基础
    新CCIE笔记-IP网络基础
    算法之【冒泡排序法】
    算法之【冒泡排序法】
    算法之【冒泡排序法】
    算法之【辗转相除法】
    算法之【辗转相除法】
  • 原文地址:https://www.cnblogs.com/wuer888/p/10144404.html
Copyright © 2020-2023  润新知