前言

网上经常看到各种IO:BIO、NIO、AIO…

花了点时间研究了下,大致了解其模型和原理,特整理一份笔记。

学习不同的IO模型之前,有几个概念必须先理清楚。

同步、异步

同步异步关注的是消息的通信机制,也是相对而言的。

同步:程序有序性,第二步的执行必须依赖第一步,只有当第一步执行完了,第二步才能开始执行。

异步:程序无序,第二步的执行不依赖于第一步,即使第一步没完成,第二步照样执行。
JS中的setTimeout函数和setInterval函数就是典型的异步函数。

例子:

我雇了个保姆,让保姆帮我煮碗面,然后通知我。

同步:对于保姆而言,煮面是同步的,他必须亲自等水烧开,然后煮面,最后通知我。

异步:对于我而言,煮面是异步的,我通知保姆煮面,写好回调(煮好通知我),然后我就可以干我自己的活了。

阻塞、非阻塞

阻塞、非阻塞关注的是等待消息时的状态。

等待消息时什么都不干,即使获得了CPU的执行权,也只是空转,就是阻塞。

等待消息的同时可以干别的活,就是非阻塞。

阻塞:保姆干等着,等水烧开,期间什么事也不干。

非阻塞:保姆等水烧开的期间可以打扫卫生。

BIO

全称:Blocking IO(阻塞IO)。

//服务端
public class Server {
	//处理请求的线程池 5
	final static ExecutorService pool = Executors.newFixedThreadPool(5);

	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket = new ServerSocket(8888);
		System.out.println("服务启动、等待连接...");
		while (true) {
			Socket socket = serverSocket.accept();
			/**
			 * 连接请求放到线程池中处理,同时最多只能处理5个请求,其他请求会被阻塞
			 * BIO容易有性能瓶颈:开启线程需要开销,CPU在线程之间切换需要开销
			 * CPU的资源得不到有效利用,IO的速度是很慢的,但是CPU的运算速度是很快的
			 * 完全可以让单个线程处理多个请求
			 */
			pool.execute(new ServerHandler(socket));
		}
	}
}
//服务端处理器
class ServerHandler extends Thread{
	private Socket socket;

	public ServerHandler(Socket socket) {
		this.socket = socket;
	}
	@Override
	public void run() {
		try {
			//sleep 3秒,突出连接阻塞
			SleepUtil.sleep(3000);
			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
			while (true) {
				String req = in.readLine();
				if (StrUtil.isBlank(req)) {
					break;
				}
				String result;
				switch (req) {
					case "date":
						result = DateUtil.formatDate(new Date());
						break;
					case "time":
						result = DateUtil.formatDateTime(new Date());
						break;
					default:
						result = "请输入口令";
				}
				System.out.println("服务端接收到口令:"+req);
				out.println(Thread.currentThread().getName()+"线程处理返回:"+result);
			}
			in.close();
			out.close();
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

//客户端
class Client{
	public static void main(String[] args) throws IOException {
		//启动10个线程,发出请求
		for (int i = 0; i < 5; i++) {
			new ClientHandler("date").start();
		}
		for (int i = 0; i < 5; i++) {
			new ClientHandler("time").start();
		}
	}
}
//客户端处理器
class ClientHandler extends Thread{
	private String command;

	public ClientHandler(String command) {
		this.command = command;
	}

	@Override
	public void run() {
		try {
			//向服务端发送 hello
			Socket socket = new Socket("127.0.0.1", 8888);
			OutputStream out = socket.getOutputStream();
			out.write((command+"\n\n").getBytes());
			out.flush();
			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			while (true) {
				String temp = in.readLine();
				if (StrUtil.isBlank(temp)) {
					break;
				}
				//输出服务端响应数据
				System.out.println(temp);
				out.flush();
			}
			out.close();
			in.close();
			socket.close();
		}catch (Exception e){}
	}
}

客户端启动10个线程请求服务端。

服务端输出如下:

服务启动、等待连接...
服务端接收到口令:date
服务端接收到口令:time
服务端接收到口令:date
服务端接收到口令:time
服务端接收到口令:time
服务端接收到口令:date
服务端接收到口令:date
服务端接收到口令:time
服务端接收到口令:time
服务端接收到口令:date

客户端输出如下:

pool-1-thread-4线程处理返回:2019-11-03
pool-1-thread-1线程处理返回:2019-11-03 10:13:36
pool-1-thread-3线程处理返回:2019-11-03 10:13:36
pool-1-thread-5线程处理返回:2019-11-03
pool-1-thread-2线程处理返回:2019-11-03 10:13:36
pool-1-thread-2线程处理返回:2019-11-03
pool-1-thread-3线程处理返回:2019-11-03
pool-1-thread-5线程处理返回:2019-11-03 10:13:39
pool-1-thread-4线程处理返回:2019-11-03 10:13:39
pool-1-thread-1线程处理返回:2019-11-03

服务端最多同时处理5个连接请求,多个请求中间会有阻塞。

BIO不适用于处理大量的请求,容易有性能瓶颈,优点是代码编写简单。

每个请求都单独用一个线程去处理,使用线程池可以减少开启线程的开销,但是CPU频繁的在多个线程中切换也会增加额外的开销。线程池的个数是有限的,意味着大多数连接需要等待。

网络IO的速度是很慢的,但是内存和CPU处理数据的能力是很快的。
BIO在网络读写数据时是阻塞的,意味着CPU大部分时间处于空闲状态,不仅浪费了CPU资源,而且程序的性能也很受影响。

NIO

JDK1.4之后,Java提供了一个全新的IO API——NIO。

全称:non-blocking IO,非阻塞IO。

  • Channel
    类似于BIO中的Stream,Stream是单向的,Channel是双向的,可读又可写。
  • Buffer
    NIO中提供了缓存,数据读写时不直接和Channel打交道,数据读写在Buffer中完成,最后由Channel从Buffer读写到磁盘中。
//服务端
public class Server {
	private final static int BUFFER_SIZE = 1024;

	public static void main(String[] args) throws Exception {
		new Server().start();
	}

	void start() throws Exception{
		//创建通道管理器对象selector
		Selector selector=Selector.open();

		//创建一个通道对象channel
		ServerSocketChannel channel = ServerSocketChannel.open();
		//将通道设置为非阻塞
		channel.configureBlocking(false);
		channel.socket().bind(new InetSocketAddress(8888));

		//将上述的通道管理器和通道绑定,并为该通道注册OP_ACCEPT事件
		//注册要监听的事件,事件没到达时,select()会一直阻塞。
		channel.register(selector, SelectionKey.OP_ACCEPT);

		while (true){
			//这是一个阻塞方法,直到有请求接入
			selector.select();
			Set keys = selector.selectedKeys();
			Iterator iterator = keys.iterator();
			while (iterator.hasNext()){
				SelectionKey key = (SelectionKey) iterator.next();
				iterator.remove();
				//判断当前key所代表的channel状态,进行不同的处理
				if (key.isAcceptable()){
					doAccept(key);
				} else if (key.isReadable()) {
					doProcess(key);
				}
			}
		}
	}

	//数据处理
	void doProcess(SelectionKey key) throws IOException {
		SocketChannel channel = (SocketChannel) key.channel();
		ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
		int read = channel.read(byteBuffer);
		String req = new String(byteBuffer.array(), 0, read);
		String result;
		switch (req) {
			case "date":
				result = DateUtil.formatDate(new Date());
				break;
			case "time":
				result = DateUtil.formatDateTime(new Date());
				break;
			default:
				result = "请输入口令";
		}
		System.out.println("服务端接收到口令:"+req);
		byteBuffer.clear();
		byteBuffer.put((Thread.currentThread().getName()+"线程处理返回:"+result).getBytes());
		byteBuffer.flip();
		channel.write(byteBuffer);
		//关闭连接
		channel.close();
	}

	//连接处理
	void doAccept(SelectionKey key) throws IOException {
		System.out.println("有请求接入...");
		ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
		SocketChannel channel = serverChannel.accept();
		//非阻塞
		channel.configureBlocking(false);
		//监听 读
		channel.register(key.selector(),SelectionKey.OP_READ);
	}
}

//客户端
class Client{
	public static void main(String[] args) throws IOException, InterruptedException {
		for (int i = 0; i < 5; i++) {
			new ClientHandler("date").start();
		}
		for (int i = 0; i < 5; i++) {
			new ClientHandler("time").start();
		}
	}
}
class ClientHandler extends Thread{
	private String command;

	public ClientHandler(String command) {
		this.command = command;
	}

	@Override
	public void run() {
		try {
			Socket socket = new Socket("127.0.0.1", 8888);
			OutputStream out = socket.getOutputStream();
			out.write(command.getBytes());
			out.flush();
			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			while (true) {
				String temp = in.readLine();
				if (StrUtil.isBlank(temp)) {
					break;
				}
				System.out.println(temp);
				out.flush();
			}
			out.close();
			in.close();
			socket.close();
		}catch (Exception e){
			e.printStackTrace();
		}
	}
}

客户端输出:

main线程处理返回:2019-11-03 15:28:38
main线程处理返回:2019-11-03 15:28:38
main线程处理返回:2019-11-03
main线程处理返回:2019-11-03
main线程处理返回:2019-11-03 15:28:38
main线程处理返回:2019-11-03 15:28:38
main线程处理返回:2019-11-03
main线程处理返回:2019-11-03
main线程处理返回:2019-11-03
main线程处理返回:2019-11-03 15:28:38

NIO下,即使是单线程,也可以处理大量请求。
适用于请求密集型,但是IO数据量少的场景。
例如:聊天室。

AIO

Java1.7开始引入,全称:Asynchronous IO(异步IO)。

提前写好处理请求的钩子函数,当有请求接入时,由程序自动调用,对于主线程来说,监听请求接入和IO读写都是异步非阻塞的。

//服务端
public class Server {
	private final static int BUFFER_SIZE = 1024;

	public static void main(String[] args) throws Exception {
		new Server().start();
	}

	synchronized void start() throws Exception{
		AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
		serverSocketChannel.bind(new InetSocketAddress(8888));
		serverSocketChannel.accept(null,new CompletionHandler<AsynchronousSocketChannel, Object>() {

			@Override
			public void completed(AsynchronousSocketChannel channel, Object attachment) {
				//继续监听下一个请求
				serverSocketChannel.accept(attachment, this);
				ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
				try {
					//读取请求数据
					Integer len = channel.read(byteBuffer).get();
					byteBuffer.flip();
					String req = new String(byteBuffer.array(), 0, len);
					System.out.println(req);
					String result;
					switch (req) {
						case "date":
							result = DateUtil.formatDate(new Date());
							break;
						case "time":
							result = DateUtil.formatDateTime(new Date());
							break;
						default:
							result = "请输入口令";
					}
					Integer integer = channel.write(ByteBuffer.wrap((Thread.currentThread().getName() + "线程处理返回:" + result).getBytes())).get();
					System.out.println(integer);
					channel.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}

			@Override
			public void failed(Throwable exc, Object attachment) {
				System.err.println("处理失败...");
			}
		});
		wait();
	}
}

//客户端
class Client{
	public static void main(String[] args) throws IOException, InterruptedException, ExecutionException {
		for (int i = 0; i < 5; i++) {
			new ClientHandler("date").start();
		}
		for (int i = 0; i < 5; i++) {
			new ClientHandler("time").start();
		}
		System.out.println("10个请求线程开启结束...");
		SleepUtil.sleep(10000);
	}
}

class ClientHandler extends Thread{
	private String command;

	public ClientHandler(String command) {
		this.command = command;
	}

	@Override
	public void run() {
		AsynchronousSocketChannel socketChannel = null;
		try {
			socketChannel = AsynchronousSocketChannel.open();
		} catch (IOException e) {
			e.printStackTrace();
		}
		AsynchronousSocketChannel channel = socketChannel;
		channel.connect(new InetSocketAddress("127.0.0.1", 8888),null,new CompletionHandler() {
			@Override
			public void completed(Object result, Object attachment) {
				ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
				try {
					//发送数据
					channel.write(ByteBuffer.wrap(command.getBytes())).get();
					//读取响应
					Integer len = channel.read(byteBuffer).get();
					byteBuffer.flip();
					String response = new String(byteBuffer.array(), 0, len);
					channel.close();
					//输出结果
					System.out.println(response);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}

			@Override
			public void failed(Throwable exc, Object attachment) {

			}
		});
	}
}

客户端输出如下:

10个请求线程开启结束...
Thread-3线程处理返回:2019-11-03 21:01:39
Thread-5线程处理返回:2019-11-03
Thread-9线程处理返回:2019-11-03
Thread-2线程处理返回:2019-11-03
Thread-4线程处理返回:2019-11-03 21:01:39
Thread-7线程处理返回:2019-11-03
Thread-6线程处理返回:2019-11-03 21:01:39
Thread-1线程处理返回:2019-11-03 21:01:39
Thread-8线程处理返回:2019-11-03
Thread-11线程处理返回:2019-11-03 21:01:39

为什么Netty使用NIO而不是AIO?

Windows系统很好的实现了AIO,Linux对其却没有很好的实现。
Netty看重的是在服务器上的运行,而服务器系统绝大多数都是Linux。
在Linux中AIO的性能可能反而不如NIO,所以Netty采用NIO而非AIO。