BIO,NIO和AIO三者的原理及区别

1 BIO (BlockingIO)

1.1同步阻塞式I/O

服务器起一个监听端口,每次有新客户端接入,服务器便会起一个线程进行处理。由于传统IO是面向字节流/字符流进行操作,因此每次通信过程进行读写操作都会发生阻塞。优点是API容易理解上手,适合非高并发场景。缺点是缺乏弹性伸缩能力。由于服务器的线程数和客户端个数是1:1,在高并发环境下,线程开启过多,CPU在多个线程之间无意义轮询,系统性能下降。当开启线程过多时候,服务器就会发生宕机。












                                     图 1 一请求一应答通信模型

public class MyServer {

	public static void main(String[] args) throws IOException {
		int port = 8090;
		ServerSocket server = null;
		Socket socket = null;
		try {
			server = new ServerSocket(port);
			System.out.println("服务器启动了,监听端口" + port);
			while(true){
				socket = server.accept();
				new Thread(new MyServerHandler(socket)).start();
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally{
			if(server != null){
				System.out.println("Myserver close");
				try {
					server.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

public class MyServerHandler implements Runnable {

	private Socket socket;
	
	public MyServerHandler(Socket socket) {
		 this.socket = socket;
	}

	@Override
	public void run() {
		BufferedReader reader = null;
		PrintWriter writer = null;
		try {
			// 读操作
			reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
			String rece = null;
			rece = reader.readLine();
			System.out.println("服务器收到客户端发送过来的信息: " + rece);
			// 写操作
			writer = new PrintWriter(this.socket.getOutputStream(), true);
			System.out.println("请输入要发送给服务端的信息:");
			Scanner scanner = new Scanner(System.in);
			String resp = scanner.next();
			writer.println(resp);
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (writer != null) {
					writer.close();
				}
				if (reader != null) {
				}
				if (socket != null) {
					socket.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

public class MyClient {

	public static void main(String[] args) {
		String host = "192.168.0.101";
		int port = 8090;
		Socket socket = null;
		PrintWriter writer = null;
		BufferedReader reader = null;
		try {
			socket = new Socket(host, port);
			// 写操作
			System.out.println("请输入要发送给服务端的内容:");
			Scanner scanner = new Scanner(System.in);
			String str = scanner.nextLine();
			// true:表示自动刷新
			writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
			writer.println(str);
			// 读操作
			reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			String rece = null;
			// 如果读取到最后行,返回null
			rece = reader.readLine();
			System.out.println("客户端收到服务器发送过来的信息: " + rece);

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (reader != null) {
					reader.close();
				}
				if (writer != null) {
					writer.close();
				}
				if (socket != null) {
					socket.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

1.2伪异步I/O

同1.1最大区别就是在服务端增加了线程池技术。即客户端个数和IO线程的个数比是M:N(M >=N),当客户端个数超过线程池核心池大小时,多余线程会被放在阻塞队列中。优点:当客户端高并发访问时候,由于服务端起的线程数是可控的,因此不会导致服务器资源耗尽和宕机。缺点:本质上还是同步阻塞IO,处理效率低。当通信一方网络发生拥塞时,读取输入流的另一方也将被长时间阻塞。

public class MyServer {

	public static void main(String[] args) throws IOException {
		int port = 8090;
		ServerSocket server = null;
		Socket socket = null;
		try {
			server = new ServerSocket(port);
			System.out.println("服务器启动了,监听端口" + port);
			//线程池
			//当客户端的个数超过核心线程池个数,多余的放入阻塞队列中进行等待
			ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 100, 120L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
			while(true){
				socket = server.accept();
				executor.execute(new MyServerHandler(socket));
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally{
			if(server != null){
				System.out.println("Myserver close");
				try {
					server.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
public class MyServerHandler implements Runnable {

	private Socket socket;
	
	public MyServerHandler(Socket socket) {
		 this.socket = socket;
	}

	@Override
	public void run() {
		BufferedReader reader = null;
		PrintWriter writer = null;
		try {
			// 读操作
			reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
			String rece = null;
			rece = reader.readLine();
			System.out.println("服务器收到客户端发送过来的信息: " + rece);
			// 写操作
			writer = new PrintWriter(this.socket.getOutputStream(), true);
			System.out.println("请输入要发送给服务端的信息:");
			Scanner scanner = new Scanner(System.in);
			String resp = scanner.next();
			writer.println(resp);
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (writer != null) {
					writer.close();
				}
				if (reader != null) {
				}
				if (socket != null) {
					socket.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

public class MyClient {

	public static void main(String[] args) {
		String host = "192.168.0.101";
		int port = 8090;
		Socket socket = null;
		PrintWriter writer = null;
		BufferedReader reader = null;
		try {
			socket = new Socket(host, port);
			// 写操作
			System.out.println("请输入要发送给服务端的内容:");
			Scanner scanner = new Scanner(System.in);
			String str = scanner.nextLine();
			// true:表示自动刷新
			writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
			writer.println(str);
			// 读操作
			reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			String rece = null;
			// 如果读取到最后行,返回null
			rece = reader.readLine();
			System.out.println("客户端收到服务器发送过来的信息: " + rece);

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (reader != null) {
					reader.close();
				}
				if (writer != null) {
					writer.close();
				}
				if (socket != null) {
					socket.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

2 NIO (NonBlockingIO)

同步非阻塞IO是从JDK1.4开始新增的IO操作API,是面向通道和缓冲区进行操作。在NIO库中,数据是从通道读取到缓冲区中,或者从缓冲区写入到通道中。通过设置通道为非阻塞机制,可以同时进行读写双向操作而不发生阻塞。

public class ServerSocketChannelDemo {
	public static void main(String[] args) throws IOException {
		ServerSocketChannel channel = ServerSocketChannel.open();
		channel.socket().bind(new InetSocketAddress(8090));
		channel.configureBlocking(true);  //默认为true,是阻塞
		SocketChannel sc = channel.accept();//发生阻塞
		System.out.println("通道为阻塞");
	}
}






                                 图2 NIO通道为阻塞情况


public class ServerSocketChannelDemo {
	public static void main(String[] args) throws IOException {
		ServerSocketChannel channel = ServerSocketChannel.open();
		channel.socket().bind(new InetSocketAddress(8090));
		channel.configureBlocking(false);  
		SocketChannel sc = channel.accept();
		System.out.println("通道为非阻塞");
	}
}




                                  图3 NIO通道为非阻塞情况


NIO中另外一个重要概念是多路复用器(Selector)。Selector通过轮询检查每个注册在其上的NIO通道,如果某个通道上面发生读(SelectionKey.OP_READ)或者写(SelectionKey.OP_WRITE)操作,则这些通道会被Selector检测出来,并且存放在一个Set集合中,进行后续操作。因为JDK底层采用epoll()代替传统select实现,因此只需要一个线程负责Selector的轮询,可以管理多个channel,从而管理多个网络连接。 优点是在高并发场景下,可以有效减少服务端起的线程数量和CPU时间片的浪费,降低了内存的消耗。缺点是代码比较复杂。

public class Server implements Runnable{
	//1 多路复用器(管理所有的通道)
	private Selector seletor;
	//2 接受数据缓冲区
	private ByteBuffer readBuf = ByteBuffer.allocate(1024);
	//3 发送数据缓冲区
	private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
	//4 标识数字
	private int flag = 0;
	
	/**
	 * @param port
	 */
	public Server(int port){
		try {
			//1 打开路复用器
			this.seletor = Selector.open();
			//2 打开服务器通道
			ServerSocketChannel ssc = ServerSocketChannel.open();
			//3 设置服务器通道为非阻塞模式
			ssc.configureBlocking(false);
			//4 绑定地址
			ssc.bind(new InetSocketAddress(port));
			//5 把服务器通道注册到多路复用器上,并且监听阻塞事件
			ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
			System.out.println("Server start, port :" + port);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void run() {
		while(true){
			try {
				//1 必须要让多路复用器开始监听
				this.seletor.select();
				//2 返回多路复用器已经选择的结果集
				Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
				//3 进行遍历
				while(keys.hasNext()){
					//4 获取一个选择的元素
					SelectionKey key = keys.next();
					//5 直接从容器中移除就可以了
					keys.remove();
					//6 如果是有效的
					if(key.isValid()){
						//7 如果为阻塞状态
						if(key.isAcceptable()){
							this.accept(key);
						}
						//8 如果为可读状态
						if(key.isReadable()){
							this.read(key);
						}
						//9 如果为可写状态
						if(key.isWritable()){
							this.write(key);
						}
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * @descriptions 写方法
	 * @param key
	 */
	private void write(SelectionKey key){
		try {
			// 返回为之创建此键的通道。
			SocketChannel sc = (SocketChannel) key.channel();
			//清空缓冲区数据
			writeBuf.clear();
			//向缓冲区中输入数据  
			String sendText="你好,客户端!" + flag++; 
			//把数据放到缓冲区中
			writeBuf.put(sendText.getBytes());
			//对缓冲区进行复位
			writeBuf.flip();
			//写出数据
			sc.write(writeBuf);
			System.out.println("服务器端向客户端发送数据: "+sendText); 
			sc.register(this.seletor, SelectionKey.OP_READ);
//			Thread.sleep(1000);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * @descriptions 读方法
	 * @param key
	 */
	private void read(SelectionKey key) {
		try {
			//1 清空缓冲区旧的数据
			this.readBuf.clear();
			//2 获取之前注册的socket通道对象
			SocketChannel sc = (SocketChannel) key.channel();
			//3 读取数据
			int count = sc.read(this.readBuf);
			//4 如果该通道已到达流的末尾,则返回 -1 
			if(count == -1){
				key.channel().close();
				key.cancel();
				return;
			}
			//5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
			this.readBuf.flip();
			//6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
			byte[] bytes = new byte[this.readBuf.remaining()];
			//7 接收缓冲区数据
			this.readBuf.get(bytes);
			//8 打印结果
			String body = new String(bytes).trim();
			System.out.println("服务器收到客户端发过来的消息: " + body);
			//9 注册到多路复用器上,并设置读写标识
			sc.register(this.seletor, SelectionKey.OP_WRITE);
			Thread.sleep(1000);
		} catch (IOException | InterruptedException e) {
			e.printStackTrace();
		}
	}

	/**@descriptions 接受方法
	 * @param key
	 */
	private  void accept(SelectionKey key) {
		try {
			System.out.println("有客户端连接进来客户端");
			//1 获取服务通道
			ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
			//2 执行阻塞方法
			SocketChannel sc = ssc.accept();
			//3 设置阻塞模式
			sc.configureBlocking(false);
			//4 注册到多路复用器上,并设置读取标识
			sc.register(this.seletor, SelectionKey.OP_READ );
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		
		new Thread(new Server(8090)).start();;
	}
	
	
}
public class NIOClient {
    // 缓冲区大小
    private static int block = 1024;  
    // 接受数据缓冲区 
    private static ByteBuffer sendbuffer = ByteBuffer.allocate(block);  
    //发送数据缓冲区* 
    private static ByteBuffer receivebuffer = ByteBuffer.allocate(block);  
    //服务器端地址
    private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(  
            "192.168.0.101", 8090);  
 
    public static void main(String[] args) throws IOException {  
        // 打开socket通道  
        SocketChannel socketChannel = SocketChannel.open();  
        // 设置为非阻塞方式  
        socketChannel.configureBlocking(false);  
        // 打开选择器  
        Selector selector = Selector.open();  
        // 注册连接服务端socket动作  
        socketChannel.register(selector, SelectionKey.OP_CONNECT);  
        // 连接  
        socketChannel.connect(SERVER_ADDRESS);  
        // 分配缓冲区大小内存  
          
        Set<SelectionKey> selectionKeys;  
        Iterator<SelectionKey> iterator;  
        SelectionKey selectionKey;  
        SocketChannel client;  
        String receiveText;  
        String sendText;  
        int count=0;  
 
        while (true) {  
            //选择一组键,其相应的通道已为 I/O 操作准备就绪。  
            //此方法执行处于阻塞模式的选择操作。  
            selector.select();  
            //返回此选择器的已选择键集。  
            selectionKeys = selector.selectedKeys();  
            iterator = selectionKeys.iterator();  
            while (iterator.hasNext()) {  
                selectionKey = iterator.next();  
                if (selectionKey.isConnectable()) {  
                    System.out.println("客户端尝试连接服务端");  
                    client = (SocketChannel) selectionKey.channel();  
                    // 判断此通道上是否正在进行连接操作。  
                    // 完成套接字通道的连接过程。  
                    if (client.isConnectionPending()) {  
                    	 client.finishConnect();  
                         System.out.println("客户端和服务端连接成功");  
                         sendbuffer.clear();  
                         sendbuffer.put("Hello,Server".getBytes());  
                         sendbuffer.flip();  
                         client.write(sendbuffer); 
                    }  
                    client.register(selector, SelectionKey.OP_READ| SelectionKey.OP_WRITE);
                }  else if (selectionKey.isReadable()) { 
                    client = (SocketChannel) selectionKey.channel();  
                    //将缓冲区清空以备下次读取  
                    receivebuffer.clear();  
                    //读取服务器发送来的数据到缓冲区中  
                    count=client.read(receivebuffer);  
                    if(count>0){  
                        receiveText = new String( receivebuffer.array(),0,count);  
                        System.out.println("客户端接受服务器端数据--:"+receiveText); 
                        client.register(selector, SelectionKey.OP_WRITE);
                    } 
                    try {
						Thread.sleep(10000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
                } else if (selectionKey.isWritable()) {  
                    sendbuffer.clear();  
                    client = (SocketChannel) selectionKey.channel(); 
                    System.out.println("请输入要发送给服务器的信息:");  
                    //定义一个字节数组,然后使用系统录入功能:
    				Scanner scanner = new Scanner(System.in);
    				sendText = scanner.nextLine();
                    sendbuffer.put(sendText.getBytes());  
                     //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位  
                    sendbuffer.flip();  
                    client.write(sendbuffer);
                    client.register(selector, SelectionKey.OP_READ);
                }  
            }  
            selectionKeys.clear();
        }  
    }  
}

3 AIO (AsynchronousIO)

步非阻塞是从JDK7新增的IO操作API,使用回调的方式,真正实现了高效异步IO。当进行读写操作时,只须直接调用API的read或write方法即可,不需要再使用Selector。由于这两种方法均为异步的,所以均由底层操作系统进行处理,完成后主动通知应用程序。缺点是代码复杂,比较难理解。

public class Server {
	// 线程池
	private ExecutorService executorService;
	// 线程组
	private AsynchronousChannelGroup threadGroup;
	// 服务器通道
	public AsynchronousServerSocketChannel assc;
	
	public Server(int port){
		try {
			//创建一个缓存池
			executorService = Executors.newCachedThreadPool();
			//创建一个线程组
			threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
			//创建服务通道
			assc = AsynchronousServerSocketChannel.open(threadGroup);
			//进行绑定
			assc.bind(new InetSocketAddress(port));
			System.out.println("Server start, port : " + port);
			//进行阻塞
			ServerCompletionHandler completionHandler = new ServerCompletionHandler();
			assc.accept(this, completionHandler);
			//一直阻塞不让服务器停止
			Thread.sleep(Integer.MAX_VALUE);
		} catch (IOException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		Server server = new Server(8090);
	}
}

public class ServerCompletionHandler implements  CompletionHandler<AsynchronousSocketChannel,Server>{

	@Override
	public void completed(AsynchronousSocketChannel asc, Server attachment) {
		//当有下一个客户端接入的时候直接调用Server的accept方法,这样反复执行下去,保证多个客户端	都可以阻塞
		attachment.assc.accept(attachment, this);
		read(asc);
	}

	@Override
	public void failed(Throwable exc, Server attachment) {
		exc.printStackTrace();
	}
	
	private void read(final AsynchronousSocketChannel asc){
		//读取数据
		ByteBuffer buf = ByteBuffer.allocate(1024);
		asc.read(buf, buf, new  CompletionHandler<Integer, ByteBuffer>(){

			@Override
			public void completed(Integer resultSize, ByteBuffer attachment) {
				//进行读取后,重置标识符
				attachment.flip();
				//获取读写的字符数
				System.out.println("Server ->" + "收到客户段的数据长度为:" +  resultSize);
				String resultData = new String(attachment.array()).trim();
				System.out.println("Server ->" + "收到客户段的信息为:" +  resultData);
				String respond = "服务器响应,收到了客户发过来的数据" + resultData;
				write(asc, respond);
			}

			@Override
			public void failed(Throwable exc, ByteBuffer attachment) {
				exc.printStackTrace();
			}
		});
	}
	
	private void write(AsynchronousSocketChannel asc, String respond){
		try {
			ByteBuffer buf = ByteBuffer.allocate(1024);
			buf.put(respond.getBytes());
			buf.flip();
			asc.write(buf).get();
		} catch (InterruptedException | ExecutionException e) {
			e.printStackTrace();
		}
	}

}

public class Client  {

	private AsynchronousSocketChannel asc;
	
	public Client() {
		try {
			asc = AsynchronousSocketChannel.open();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
//	@Override
//	public void run() {
//		while(true){
//			
//		}
//	}
	
	public void connect(){
		asc.connect(new InetSocketAddress("127.0.0.1", 8090));
	}
	
	public void write(String request){
		try {
			asc.write(ByteBuffer.wrap(request.getBytes())).get();
			read();
		} catch (InterruptedException | ExecutionException e) {
			e.printStackTrace();
		}
	}
	
	public void read(){
		ByteBuffer buf = ByteBuffer.allocate(1024);
		try {
			asc.read(buf).get();
			buf.flip();
			byte[] respByte = new byte[buf.remaining()];
			buf.get(respByte);
			System.out.println(new String(respByte, "utf-8").trim());
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		} catch (ExecutionException e1) {
			e1.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		ExecutorService executorService = Executors.newCachedThreadPool();
		for(int i = 0; i < 5; i++){
			executorService.execute(new Runnable() {
				@Override
				public void run() {
					Client client = new Client();
					client.connect();
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					client.write(System.currentTimeMillis() + "");
				}
			});
		}
	}
}


4 参考文献

[1] 李林锋, Netty权威指南 第2版. 2014, 电子工业出版社.

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
原创文章 32 获赞 12 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_35469756/article/details/77151378
今日推荐