IO、NIO、AIO

 同步和异步是针对应用程序和内核的交互而言的。 阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式

同步   指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪

异步   执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作

阻塞   当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止

非阻塞    非阻塞状态下, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待

 

Java BIO : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
      服务端提供IP和监听端口,客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。
      传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信
     

 

      用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。

      BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通信模型。

      BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程来处理这条链路,在需要满足高性能、高并发的场景是没法应用的

package socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    private static int DEFAULT_PORT=8089;
    private ServerSocket serverSocket;
    
    public void start() {
    
        try {
            serverSocket = new ServerSocket(DEFAULT_PORT);
            System.out.println("服务端启动!端口号"+DEFAULT_PORT);
        
            while(true) {
                Socket socket = serverSocket.accept();
                new Thread(new Handler(socket)).start();
                
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally {
                try {
                    if(serverSocket!=null) {
                         serverSocket.close();
                    }
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
        }
    }
    
}


class Handler implements Runnable{
    BufferedReader reader = null;
    PrintWriter writer=null;
    private Socket socket=null;
    
    public Handler(Socket socket) {
        this.socket=socket;
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            while(true) {
                reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
                writer=new PrintWriter(socket.getOutputStream());
                String expression = reader.readLine();
                System.out.println(expression);
                writer.println("ok");
                writer.flush();
            }
            
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally {
            if(reader!=null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                reader=null;
            }
            
            if(writer!=null) {
                writer.close();
                writer=null;
            }
        }
    }
    
}
View Code

     为了改进这种一连接一线程的模型,可以使用线程池来管理这些线程,实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型“。

正因为限制了线程数量,如果发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。而对Socket的输入流就行读取时,会一直阻塞,直到发生:

  •     有数据可读
  •     可用数据以及读取完毕
  •     发生空指针或I/O异常

    所以在读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这就是最大的弊端。

 

Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
    由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。
 
    
   IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
   

 

    用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行
    从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
 
    

IO多路复用模型使用了Reactor设计模式实现了这一机制。

图4 Reactor设计模式

如图4所示,EventHandler抽象类表示IO事件处理器,它拥有IO文件句柄Handle(通过get_handle获取),以及对Handle的操作handle_event(读/写等)。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理EventHandler(注册、删除等),并使用handle_events实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数select,只要某个文件句柄被激活(可读/写等),select就返回(阻塞),handle_events就会调用与文件句柄关联的事件处理器的handle_event进行相关操作。

    对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。
    NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。

    缓冲区 Buffer

    Buffer是一个对象,包含一些要写入或者读出的数据。

    在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

    具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer。

    

    通道 Channel

    对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。

    Channel主要分两大类:

  •     SelectableChannel:用户网络读写
  •     FileChannel:用于文件操作

     多路复用器 Selector

     Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

    一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

   服务端的demo:

package socket;

import java.io.IOException;
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.Iterator;
import java.util.Set;

public class ServerChannel {

    private static int DEFAULT_PORT = 8089;

    public void start() {
        new Thread(new ServerHandle(DEFAULT_PORT)).start();
    }

}

class ServerHandle implements Runnable {

    private int port;
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;
    private volatile boolean started = false;

    public ServerHandle(int p) {
        this.port = p;
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(port));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            started = true;
            System.out.println("服务器已近启动!");
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public void stop() {
        started = false;
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub

        try {
            while (started) {
                selector.select();
                // 阻塞,只有当注册的事件发生时才会进行
                // selector.select(1000);
                // 无论是否有读写事件,每隔1s再唤醒一次
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iter = keys.iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    iter.remove();
                    handleInput(key);
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {

            if (selector != null) {
                try {
                    selector.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            if (key.isAcceptable()) {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel socketChannel = ssc.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SelectionKey.OP_READ);
            }
            if (key.isReadable()) {
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int size = sc.read(buffer);
                if (size > 0) {
                    
                    buffer.flip();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    System.out.println(new String(data));
                    
                    
                    ByteBuffer writeBuffer = ByteBuffer.allocate(data.length);
                    writeBuffer.put(data);
                    writeBuffer.flip();
                    sc.write(writeBuffer);
                    writeBuffer.clear();
                } else {
                    key.cancel();
                    sc.close();
                }

            }
        }
    }

}
View Code

   客户端的demo:

package socket;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

public class ClientChannel {
   private static int PORT=8089;
   private static String SERVER_ADDRESS="127.0.0.1";
   private ClientHandle clientHandler;
   
   public void start() {
       clientHandler= new ClientHandle(SERVER_ADDRESS,PORT);
       new Thread(clientHandler).start();;  
   }
   
}

class ClientHandle implements Runnable{
    private int port;
    private String address;
    private SocketChannel socketChannel=null;
    private Selector selector=null;
    private boolean started=false;
    
    public ClientHandle(String addr,int port) {
        this.address=addr;
        this.port=port;
        
        try {
            selector=Selector.open();
            socketChannel = socketChannel.open();
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress(address,port));
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
            started=true;
            
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {                
            
            while(started) {
                selector.select();
                
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iter = keys.iterator();
                while(iter.hasNext()) {
                    SelectionKey key = iter.next();
                    iter.remove();
                    
                    handleData(key);
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally {
            try {
                selector.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    
    private void handleData(SelectionKey key) throws IOException {
        if(key.isValid()) {
            SocketChannel channel = (SocketChannel) key.channel();
             if(key.isConnectable()) {
                 if(channel.finishConnect()) {
                     if(sendMsg()) {
                         started=false;
                     }
                 }
             }
             if(key.isReadable()) {
                 ByteBuffer buffer = ByteBuffer.allocate(1024);
                 int readSize = channel.read(buffer);
                 
                 if(readSize>0) {
                      buffer.flip();
                      byte[] data=new byte[buffer.remaining()];
                      buffer.get(data);
                      String exp = new String(data);
                      System.out.println(exp+"------");
                 }else {
                     key.cancel();
                     channel.close();
                 }
             }
            
        }
    }
    
    public boolean sendMsg() {
        System.out.println("please input quit to close");
        boolean quit=false;
        try {
            socketChannel.register(selector, SelectionKey.OP_READ);
            while(!quit) {
                String msg = new Scanner(System.in).nextLine();
                
                if(msg.equals("quit"))
                    quit=false;
                byte[] bytes = msg.getBytes("utf-8");
                ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
                buffer.put(bytes);
                buffer.flip();
                socketChannel.write(buffer);
                buffer.clear();
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return true;
    }
    
}
View Code

   测试demo

package socket;

import java.util.Scanner;

public class TestChannel {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ServerChannel serverChannel = new ServerChannel();
        serverChannel.start();
        
        ClientChannel clientChannel = new ClientChannel();
        clientChannel.start();
            
    }

}
View Code

 

Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成再通知服务器应用去启动线程进行处理,
NIO的IO行为还是同步的。对于NIO来说,线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的,对AIO来说,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。
 
在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

异步IO模型使用了Proactor设计模式实现了这一机制。

图6 Proactor设计模式

如图6,Proactor模式和Reactor模式在结构上比较相似,不过在用户(Client)使用方式上差别较大。Reactor模式中,用户线程通过向Reactor对象注册感兴趣的事件监听,然后事件触发时调用事件处理函数。而Proactor模式中,用户线程将AsynchronousOperation(读/写等)、Proactor以及操作完成时的CompletionHandler注册到AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade模式提供了一组异步操作API(读/写等)供用户使用,当用户线程调用异步API后,便继续执行自己的任务。AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步。当异步IO操作完成时,AsynchronousOperationProcessor将用户线程与AsynchronousOperation一起注册的Proactor和CompletionHandler取出,然后将CompletionHandler与IO操作的结果数据一起转发给Proactor,Proactor负责回调每一个异步操作的事件完成处理函数handle_event。虽然Proactor模式中每个异步操作都可以绑定一个Proactor对象,但是一般在操作系统中,Proactor被实现为Singleton模式,以便于集中化分发操作完成事件。

图7 异步IO

如图7所示,异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。

 

在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供accept()用于接收用户连接请求。在客户端使用的通道是AsynchronousSocketChannel,这个通道处理提供open静态工厂方法外,还提供read和write方法。

在AIO编程中,发出一个事件(accept read write等)之后要指定事件处理类(回调函数),AIO中的事件处理类是CompletionHandler<V,A>,这个接口定义两个方法,分别在异步操作成功和失败时被回调。

     void completed(V result, A attachment);

     void failed(Throwable exc, A attachment);

异步通道提供两种方式获取获取操作结果。

  1. 通过java.util.concurrent.Future类来表示异步操作的结果;
  2. 在执行异步操作的时候传入一个java.nio.channels。

CompletionHandler接口的实现类作为操作完成的回调。

AIO 服务端demo

package socket;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CountDownLatch;

public class AIOServer {
    private static int port = 8089;

    public void start() {
        AsyncServerHandler asyncServerHandler = new AsyncServerHandler(port);
        new Thread(asyncServerHandler).start();
    }
}

class AsyncServerHandler implements Runnable {
    private int port;
    AsynchronousServerSocketChannel asynchronousServerSocketChannel;
    CountDownLatch latch;

    public AsyncServerHandler(int port) {
        // TODO Auto-generated constructor stub
        this.port = port;
        try {
            // 在构造方法中,我们首先创建一个异步的服务端通道AsynchronousServerSocketChannel,
            // 然后调用它的bind方法绑定监听端口,如果端口合法且没被占用,绑定成功,打印启动成功提示到控制台。
            asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
            asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
            System.out.println("服务端已经启动!");
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        // 可以传递一个CompletionHandler<AsynchronousSocketChannel,? super
        // A>类型的handler实例接收accept操作成功的通知消息,
           latch=new CountDownLatch(1);
           asynchronousServerSocketChannel.accept(this, new AcceptHandler());
           
           try {
            latch.await();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncServerHandler> {

    /**
     * 我们从attachment获取成员变量AsynchronousServerSocketChannel,然后继续调用它的accept方法。
     * 在此可能会心存疑惑:既然已经接收客户端成功了,为什么还要再次调用accept方法呢?
     * 原因是这样的:当我们调用AsynchronousServerSocketChannel的accept方法后,
     * 如果有新的客户端连接接入,系统将回调我们传入的CompletionHandler实例的completed方法,
     * 表示新的客户端已经接入成功,因为一个AsynchronousServerSocketChannel可以接收成千上万个客户端,
     * 所以我们需要继续调用它的accept方法,接收其他的客户端连接,最终形成一个循环。 每当接收一个客户读连接成功之后,再异步接收新的客户端连接。
     */
    @Override
    public void completed(AsynchronousSocketChannel channel, AsyncServerHandler attachment) {
        // TODO Auto-generated method stub

        attachment.asynchronousServerSocketChannel.accept(attachment, this);
        // 链路建立成功后,服务端需要接受客户端的请求消息
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 调用AsynchronousSocketChannel的read方法进行异步读操作,异步read方法的参数:
        // ByteBuffer dst:接收缓冲区,用于从异步Channel中读取数据包
        // A attachment:异步Channel携带的附件,通知回调的时候作为入参使用
        // CompletionHandler<Integer,? super
        // A>:接收通知回调的业务handler,本例程中为ReadCompletionHandler
        channel.read(buffer, buffer, new ReaderHandler(channel));
    }

    @Override
    public void failed(Throwable exc, AsyncServerHandler attachment) {
        // TODO Auto-generated method stub
        attachment.latch.countDown();
    }

}

class ReaderHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel socketChannel;

    public ReaderHandler(AsynchronousSocketChannel socketChannel) {
        // 将AsynchronousSocketChannel通过参数传递到ReadCompletion Handler中当作成员变量来使用
        // 主要用于读取半包消息和发送应答。
            this.socketChannel = socketChannel;
    }

    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        // TODO Auto-generated method stub
        attachment.flip();
        byte[] bytes = new byte[attachment.remaining()];
        attachment.get(bytes);

        String exp = new String(bytes);
        System.out.println(exp);

        doWrite(exp + "---------");
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        // TODO Auto-generated method stub
        try {
            socketChannel.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public void doWrite(String result) {
        if (result != null && result.trim().length() > 0) {
            byte[] bytes = result.getBytes();
            ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
            buffer.put(bytes);
            buffer.flip();

            socketChannel.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {

                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    // TODO Auto-generated method stub
                    if (buffer.hasRemaining()) {
                        socketChannel.write(buffer,buffer,this);
                    } else {
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        socketChannel.read(byteBuffer, byteBuffer, new ReaderHandler(socketChannel));
                    }
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    // TODO Auto-generated method stub
                    //关注下failed方法,它的实现很简单,就是当发生异常的时候,对异常Throwable进行判断,
                    //如果是I/O异常,就关闭链路,释放资源,
                    //如果是其他异常,按照业务自己的逻辑进行处理,如果没有发送完成,继续发送.
                    //本例程作为简单demo,没有对异常进行分类判断,只要发生了读写异常,就关闭链路,释放资源
                    try {
                        socketChannel.close();
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }

            });
        }
    }
}
View Code

AIO Client demo:

package socket;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;

public class AIOClient {
    AsyncClient asyncClient;
    
    public void start() {
        asyncClient = new AsyncClient();
        new Thread(asyncClient).start();
    }
    
    public void sendMessage() {
        asyncClient.sendMsg();
    }
}

class AsyncClient implements Runnable, CompletionHandler<Void, AsyncClient> {

    private int port = 8089;
    private String addr = "127.0.0.1";
    private AsynchronousSocketChannel asynchronousSocketChannel;
    private CountDownLatch latch;

    public AsyncClient() {
        // TODO Auto-generated constructor stub

        try {
            // 通过AsynchronousSocketChannel的open方法创建一个新的AsynchronousSocketChannel对象
            asynchronousSocketChannel = AsynchronousSocketChannel.open();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        // 发起异步连接操作,回调参数就是这个类本身,如果连接成功会回调completed方法
        latch = new CountDownLatch(1);
        asynchronousSocketChannel.connect(new InetSocketAddress(addr, port), this, this);

        try {
            latch.await();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    @Override
    public void completed(Void result, AsyncClient attachment) {
        // TODO Auto-generated method stub
        System.out.println("客户端成功连接到服务器...");
        System.out.println("please input data");
        // String msg = new Scanner(System.in).nextLine();
        // byte[] bytes = msg.getBytes();
        // ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        // buffer.put(bytes);
        // buffer.flip();
        // asynchronousSocketChannel.write(buffer, buffer, new
        // WriteHandler(asynchronousSocketChannel, latch));
    }

    @Override
    public void failed(Throwable exc, AsyncClient attachment) {
        // TODO Auto-generated method stub
        System.err.println("连接服务器失败...");
        try {
            asynchronousSocketChannel.close();
            latch.countDown();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public void sendMsg() {
        String msg = null;
        while ((msg = new Scanner(System.in).nextLine()) != null) {
            byte[] bytes = msg.getBytes();
            ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
            buffer.put(bytes);
            buffer.flip();
            asynchronousSocketChannel.write(buffer, buffer, new WriteHandler(asynchronousSocketChannel, latch));
        }
    }

}

class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel socketChannel;
    private CountDownLatch latch;

    public WriteHandler(AsynchronousSocketChannel socketChannel, CountDownLatch latch) {
        // TODO Auto-generated constructor stub
        this.socketChannel = socketChannel;
        this.latch = latch;
    }

    @Override
    public void completed(Integer result, ByteBuffer buffer) {
        // TODO Auto-generated method stub
        if (buffer.hasRemaining())
            socketChannel.write(buffer, buffer, this);
        else {
            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
            socketChannel.read(readBuffer, readBuffer, new ReadHandler(socketChannel, latch));
        }
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        // TODO Auto-generated method stub
        System.err.println("数据发送失败...");
        try {
            socketChannel.close();
            latch.countDown();
        } catch (IOException e) {
        }
    }

}

class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel clientChannel;
    private CountDownLatch latch;

    public ReadHandler(AsynchronousSocketChannel clientChannel, CountDownLatch latch) {
        this.clientChannel = clientChannel;
        this.latch = latch;
    }

    @Override
    public void completed(Integer result, ByteBuffer buffer) {
        buffer.flip();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        String body;
        try {
            body = new String(bytes, "UTF-8");
            System.out.println("客户端收到结果:" + body);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.err.println("数据读取失败...");
        try {
            clientChannel.close();
            latch.countDown();
        } catch (IOException e) {
        }
    }
}
View Code

测试代码

package socket;

import java.util.concurrent.TimeUnit;

public class AIOTest {

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        AIOServer aioServer = new AIOServer();
        aioServer.start();
        
        TimeUnit.SECONDS.sleep(3);
        AIOClient aioClient = new AIOClient();
        
        aioClient.start();
        aioClient.sendMessage();
    }

}
View Code

先以一张表来直观的对比一下:

    03

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持

异步I/O模型(AIO, asynchronous I/O)

  进程发起read操作之后,立刻就可以开始去做其它的事。从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成。

  这个模型工作机制是:告诉内核启动某个操作,并让内核在整个操作(包括第二阶段,即将数据从内核拷贝到进程缓冲区中)完成后通知我们。

       

Java IO和NIO之间的主要区别:

IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
选择器

 

 

 

 

1、面向流与面向缓冲

      Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。

      Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

      在BIO(阻塞IO)中,等待IO的线程必须被阻塞,以全天候地执行IO操作。为了解决这种IO方式低效的问题,引入了缓冲区的概念,当数据到达时,可以预先被写入缓冲区,再由缓冲区交给线程,因此线程无需阻塞地等待IO。

2、阻塞与非阻塞IO

     Java IO的各种流是阻塞的。意味着当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

     Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 同时,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

3、选择器(Selectors)

     Java NIO的选择器允许一个单独的线程来监视多个输入通道,可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

     通道和缓冲区的机制,使得线程无需阻塞地等待IO事件的就绪,但是总是要有人来监管这些IO事件。这个工作就交给了selector来完成,这就是所谓的同步。

数据处理

    在IO设计中,我们从InputStream或 Reader逐字节读取数据。假设你正在处理一基于行的文本数据流,例如:

Name: Anna 
Age: 25
Email: anna@mailserver.com 
Phone: 1234567890 

该文本行的流可以这样处理:

InputStream input = ... ; // get the InputStream from the client socket   

BufferedReader reader = new BufferedReader(new InputStreamReader(input));   

String nameLine   = reader.readLine(); 
String ageLine    = reader.readLine(); 
String emailLine  = reader.readLine(); 
String phoneLine  = reader.readLine();

请注意处理状态由程序执行多久决定。换句话说,一旦reader.readLine()方法返回,你就知道肯定文本行就已读完, readline()阻塞直到整行读完。

而一个NIO的实现会有所不同,下面是一个简单的例子:

ByteBuffer buffer = ByteBuffer.allocate(48); 
int bytesRead = inChannel.read(buffer); 

注意第二行,从通道读取字节到ByteBuffer。当这个方法调用返回时,你不知道你所需的所有数据是否在缓冲区内。其结果是,在你知道所有数据都在缓冲区里之前,你必须检查几次缓冲区的数据。这不仅效率低下,而且可以使程序设计方案杂乱不堪。例如:

ByteBuffer buffer = ByteBuffer.allocate(48);   

int bytesRead = inChannel.read(buffer);   

while(! bufferFull(bytesRead) ) {   
       bytesRead = inChannel.read(buffer);   
}

bufferFull()方法必须跟踪有多少数据读入缓冲区,并返回真或假,这取决于缓冲区是否已满。换句话说,如果缓冲区准备好被处理,那么表示缓冲区满。

下图展示了“缓冲区数据循环就绪”:

NIO和epoll:

(1)select==>时间复杂度O(n)

     有I/O事件发生,却并不知道是哪那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(2)poll==>时间复杂度O(n)

    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

(3)epoll==>时间复杂度O(1)

    epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以说epoll实际上是事件驱动(每个事件关联上fd)的,此时对这些流的操作都是有意义的。(复杂度降低到了O(1))

    select,poll,epoll是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。  

 

参考:

http://www.cnblogs.com/aspirant/p/8630283.html

https://www.cnblogs.com/hujihon/p/6686363.html

https://blog.csdn.net/x_i_y_u_e/article/details/52223406

https://www.cnblogs.com/aspirant/p/6877350.html?utm_source=itdadao&utm_medium=referral(推荐)

https://www.cnblogs.com/aspirant/p/9166944.html

posted on 2018-09-26 16:10  溪水静幽  阅读(316)  评论(0)    收藏  举报