网络IO模型的发展

134 阅读7分钟

关于IO

印象中的IO分为文件 IO与网络IO。文件IO比如读取某个磁盘地址的文件(此次IO发生在磁盘),或者直接获取内存中的某个地址的数据(此次IO发生在内存);网络IO比如某次请求的数据读去与写回

关于阻塞

阻塞:指执行当前方法的线程在调用结果返回之前,执行过程中,被挂起,直到该方法执行完毕返回

非阻塞:指执行当前方法的线程在调用结果不能立即返回的情况下,当前线程不会阻塞,不会被挂起

关于同步

同步:发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了

异步:调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用

Redis网络IO选型

  • Blocking IO 阻塞IO
  • Non-blocking IO 非阻塞IO
  • IO multiplexing model 多路复用IO
  • 单线程Reactor模型
  • 多单线程Reactor模型

举例网络IO

Blocking IO

public class BIOServerSocket {

    public static void main(String[] args) {
        ServerSocket serverSocket=null;

        try {
            serverSocket=new ServerSocket(8099);
            while(true) {
                //accept() 阻塞直到有客户端连接进来,它会等当前连有数据发过来处理完成,才能处理下一个连接
                Socket socket = serverSocket.accept(); 
                System.out.println("所连客户端端口号:" + socket.getPort());
                System.err.println("该连接数据在未就绪时候,出现阻塞,只能等该连接发送数据才能继续执行下去");
              	//socket.getInputStream()方法会导致程序阻塞,直到收到连接发过来的数据(本地IO 网卡缓冲区),程序才会继续往下执行
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String clientStr = bufferedReader.readLine();
                System.out.println("所连客户端发送的消息:" + clientStr);
                BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                bufferedWriter.write("rcv a msg :" + clientStr + "\n");
                bufferedWriter.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(serverSocket!=null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

# 开启终端1
 telnet 192.168.1.103 8099
#此时输入一些请求数据,请勿回车 
#此时开启终端2 执行终端2操作
#此时你会发现服务端并没有输出终端2的数据
#此时终端1回车,服务端先回打印来自终端1的数据然后是终端2的数据
# 开启终端2
 telnet 192.168.1.103 8099
#此时输入一些请求数据,回车

Non-blocking IO

public class NIOServerSocket {

    public static void main(String[] args) {
        try {
            ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
            //设置连接非阻塞,没有客户端连接进来也不阻塞
            serverSocketChannel.configureBlocking(false); 
            serverSocketChannel.socket().bind(new InetSocketAddress(8099));
            while(true){
                //accept() 是非阻塞的,一直循环等有客户端连接进来
                SocketChannel socketChannel=serverSocketChannel.accept();
                if(socketChannel!=null){
                    ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                    socketChannel.read(byteBuffer);
                    socketChannel.read(byteBuffer);
                    Thread.sleep(5000);
                    byteBuffer.flip();
                    socketChannel.write(byteBuffer);
                }else{
                    Thread.sleep(500);
                    System.out.println("不阻塞,等待客户端连接就绪 :" + System.currentTimeMillis());
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

    }
}


# 开启终端1
 telnet 192.168.1.103 8099
#此时输入一些请求数据,请勿回车 
#此时开启终端2 执行终端2操作
#此时你会发现服务端并没有输出终端2的数据
#此时终端1回车,服务端先回打印来自终端1的数据然后是终端2的数据
# 开启终端2
 telnet 192.168.1.103 8099
#此时输入一些请求数据,回车

IO multiplexing model

public class NIOSelectorServerSocket implements Runnable{

    Selector selector;
    ServerSocketChannel serverSocketChannel;

    public NIOSelectorServerSocket(int port) throws IOException {
       	//不同操作系统对Selector的实现不一样
      	selector=Selector.open();
        serverSocketChannel=ServerSocketChannel.open();
        //如果采用selector模型,必须要设置非阻塞
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    @Override
    public void run() {
        while(!Thread.interrupted()){
            try {
              	//不断轮询多路复用器里面是否有事件就绪
                selector.select();
              	//获取事件列表
                Set selected=selector.selectedKeys(); 
                Iterator it=selected.iterator();
                while(it.hasNext()){
                    //说明有连接进来
                    dispatch((SelectionKey) it.next());
                  	//移除当前就绪的事件
                    it.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
   private void dispatch(SelectionKey key) throws IOException {
        if(key.isAcceptable()){//连接事件
            register(key);
        }else if(key.isReadable()){ //读事件
            read(key);
        }else if(key.isWritable()){ //写事件
            write(key);
        }
    }

   private void write(SelectionKey key) throws IOException {
        SocketChannel channel= (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.wrap("hello world\n".getBytes());
        channel.write(buffer);
        buffer.clear();
        channel.register(selector,SelectionKey.OP_READ);
    }

    private void register(SelectionKey key) throws IOException {
      	//得到客户端连接
        ServerSocketChannel channel= (ServerSocketChannel) key.channel(); 
      	//获得客户端连接
        SocketChannel socketChannel=channel.accept(); 
        socketChannel.configureBlocking(false);
      	//注册读事件,该客户端一旦有数据发来就触发事件
        socketChannel.register(selector,SelectionKey.OP_READ);
    }
    private void read(SelectionKey key) throws IOException {
        //得到的是socketChannel
        SocketChannel channel= (SocketChannel) key.channel();
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        channel.read(byteBuffer);
        System.out.println("Server Receive Msg:"+new String(byteBuffer.array()));
      	//注册写事件,给当前连接注册写回事件,服务端写会该客户端
        channel.register(selector,SelectionKey.OP_WRITE);
    }

    public static void main(String[] args) throws IOException {
        NIOSelectorServerSocket selectorServerSocket=new NIOSelectorServerSocket(8099);
        new Thread(selectorServerSocket).start();
    }
}

# 开启终端1
 telnet 192.168.1.103 8099
#此时输入一些请求数据,请勿回车 
#此时开启终端2 执行终端2操作
#此时你会发现服务端并输出终端2的数据
#此时终端1回车,服务端先回打印来自终端1的数据
# 开启终端2
 telnet 192.168.1.103 8099
#此时输入一些请求数据,回车

单线程Reactor模型(Redis 6.0之前)

基于NIO的高性能网络IO设计模式,其内在还是使用了多路复用器,Reactor模型只是将其实现变得更加高效,将响应事件与业务事件进行分离,进行职能拆分


public class Reactor implements Runnable{

    private final Selector selector;
    private final ServerSocketChannel serverSocketChannel;

    public Reactor(int port) throws IOException {
        selector=Selector.open();
        serverSocketChannel= ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,new Acceptor(selector,serverSocketChannel));
    }
    @Override
    public void run() {
        while(!Thread.interrupted()){
            try {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while(iterator.hasNext()){
                    dispatch(iterator.next());
                    iterator.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    private void dispatch(SelectionKey key){
        Runnable runnable=(Runnable)key.attachment();
        if(runnable!=null){
            runnable.run(); //
        }
    }
}


public class Acceptor implements Runnable{

    private final Selector selector;
    private final ServerSocketChannel serverSocketChannel;

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

    @Override
    public void run() {
        SocketChannel channel;
        try {
            //得到一个客户端连接
            channel=serverSocketChannel.accept();
            System.out.println(channel.getRemoteAddress()+":收到一个客户端连接");
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ,new Handler(channel));
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

public class Handler implements Runnable{
    SocketChannel channe;

    public Handler(SocketChannel channe) {
        this.channe = channe;
    }

    @Override
    public void run() {
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        int len=0,total=0;
        String msg="";
        try {
            do {
                len = channe.read(buffer);
                if(len>0){
                    total+=len;
                    msg+=new String(buffer.array());
                }
            } while (len > buffer.capacity());
            System.out.println(channe.getRemoteAddress()+": Server receive Msg:"+msg);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(channe!=null){
                try {
                    channe.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class ReactorMain {
    public static void main(String[] args) throws IOException {
        new Thread(new Reactor(8099),"Reactor-Thread").start();
    }
}

多单线程Reactor模型(Redis 6.0之后)

这里只对业务事件处理Handler.calss 进行改造

public class Handler implements Runnable {

    SocketChannel channel;

    private Executor executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public MutilDispatchHandler(SocketChannel channel) {
        this.channel = channel;
    }

    @Override
    public void run() {
        processor();
    }

    private void processor() {
        executor.execute(new ReaderHandler(channel));
    }

    static class ReaderHandler implements Runnable {
        private SocketChannel channel;

        public ReaderHandler(SocketChannel channel) {
            this.channel = channel;
        }

        @Override
        public void run() {
         	ByteBuffer buffer = ByteBuffer.allocate(1024);
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int len = 0, total = 0;
            String msg = "";
            try {
                do {
                    len = channel.read(buffer);
                    if (len > 0) {
                        total += len;
                        msg += new String(buffer.array());
                    }
                } while (len > buffer.capacity());
                System.out.println(channel.getRemoteAddress() + ": Server receive Msg:" + msg);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (channel != null) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

分析网络IO

Blocking IO

优点:用在客户端连接数量已知,数量不多,比如:早起tomcat 使用的就是阻塞式IO,zookeeper选举

瓶颈:连接阻塞,IO阻塞;服务端会一直等客户端连接,并且处理完当前客户端发送的数据再去处理另一个连接

方案:使用线程池来接解决,将请求的socket对象交给线程并发处理但是,线程资源占用将会成又一个瓶颈

Non-blocking IO

优点:连接非阻塞,IO阻塞

瓶颈:服务端会一直轮询数据是否准备好,直到有数据再返回,比起BIO它不会再accept()就直接阻塞,而是会不断轮询

方案:基于事件机制,采取注册形式,当事件触发回调告诉服务端

IO multiplexing model

优点:单线程基于事件机制不会出现阻塞问题

瓶颈:文件描述符的容量存储,多轮询(1.不断轮询是否有事件就绪,若就绪将其加入执行列表 2.轮询执行事件列表完成事件处理)

方案:多路复用器的实现有select、poll,选取epoll模型

单线程Reactor模型

优点:基于NIO事件机制,进行职能分离

瓶颈:单线程,在处理业务事件的时候是串行的,另外单线程一旦出现阻塞,后面的处理只能等待

方案:对业务事件改为异步多线

总结

Redis采用的多线程体现在某些后台事件,如备份、键的过期清除;在网络IO方面Redis只是将客户端连接的接入与接入后的数据在网卡缓冲区读取与解码这些公共重复行的操作。但是事务型的操作(执行命令)还是有单线程,这里只是将之前由单线程所重复事件用多线程处理