前言
网上经常看到各种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:38NIO下,即使是单线程,也可以处理大量请求。
适用于请求密集型,但是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。
















