Java IO,BIO,NIO
1.Linux的五种I/O模型(转自Netty权威指南)
1.1阻塞I/O模型(转自Netty权威指南)
最常用的I/O模型就是阻塞I/O模型,缺省情形下,所有文件操作都是阻塞的。我们以套接字借口为例来讲解此模型:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直会等待,进程从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞I/O模型,如图1所示。
图1
1.2非阻塞式I/O模型
recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来,如图2所示。
图2
1.3I/O复用模型
Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到一些制约。Linux还提供一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback,如图3所示。
图3
1.4信号驱动I/O模型
首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,九尾该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循坏函数处理数据,如图4所示
图4
1.5异步I/O模型
告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成,如图5。
图5
2.同步与阻塞
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
-
状态值。
一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO -
同步阻塞IO:
在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式! -
同步非阻塞IO:
在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。 -
异步阻塞IO:
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?
因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性! -
异步非阻塞IO:
在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。Java1.7 后的NIO2.0实现文件和网络的异步非阻塞IO -
举一个例子,如下:
小帅和小美聊天:- 同步阻塞式聊天:小帅向小美打招呼后,一直在聊天窗口等着小美的回应。一对一聊天,不干别的事。
- 同步非阻塞式聊天:小帅向小美打招呼后,一直等着无聊,看看视频和新闻。但是生怕错过小美回应,小美生气。就要在看视频和新闻的同时,查看小美有没有回消息。
- 异步阻塞式聊天:小帅开启消息通知,只要小美的消息到了,立即会有语音提示。但是小帅很想收到小美的消息,一刻也不想错过,虽然有语音提醒。但还是在电脑前等着,不干其他的事情。
- 异步非阻塞式聊天:小帅开启消息通知,向小美打招呼后。不傻等,而是看视频刷微博。听到语音提醒后,再来回小美的消息。
你们喜欢怎么聊?
搞清楚了以上概念以后,我们再回过头来看看,Reactor模式和Proactor模式。(其实阻塞与非阻塞都可以理解为同步范畴下才有的概念,对于异步,就不会再去分阻塞非阻塞。对于用户进程,接到异步通知后,就直接操作进程用户态空间里的数据好了。)
首先来看看Reactor模式,Reactor模式应用于同步I/O的场景。我们以读操作为例来看看Reactor中的具体步骤:
读取操作:
-
应用程序注册读就绪事件和相关联的事件处理器
-
多路复用选择器等待事件的发生
-
当发生读就绪事件的时候,多路复用选择器调用第一步注册的事件处理器
-
事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
写入操作类似于读取操作,只不过第一步注册的是写就绪事件。
下面我们来看看Proactor模式中读取操作的过程:
读取操作:
-
应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
-
事件分离器等待读取操作完成事件
-
在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作(异步IO都是操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作,操作系统扮演了重要角色),并将读取的内容放入用户传递过来的缓存区中。
这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。 -
事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
Proactor中写入操作也类似于读取操作,只不过感兴趣的事件是写入完成事件。
从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,
它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.
综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步 需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
Reactor模式对Java的NIO(同步非阻塞式IO),Selector实现的就是多路复用选择器,各种就绪事件及对应的处理程序就注册在这对象中。Proactor模式对应Java的NIO 2.0(即AIO),实现异步非阻塞式IO。
3.Java NIO
Java NIO的核心类为java.nio.channels.Selector。首先Java NIO实现的是I/O复用模型,而Selector扮演的角色如同多路复用选择器。以打印回显服务为例,客户端从控制台获取消息发送给服务端,而服务端则回显消息给客户端。EchoServer创建了ServerSocketChannel通道,绑定相应的端口,设置非阻塞模式。而后需要向Selector注册isAcceptable(接收就绪)事件和isReadable(读就绪)事件。开启一个新的线程,不断的获取seletor中已经就绪的事件,并进行相应的处理。
public class EchoServer {
/*
实现非阻塞式 nonbolcking EchoServer
监听8080端口,向多路复用选择器Selector注册 acceptable 和 readable 事情
*/
public static void main(String[] args) {
//端口 8080
int port = 8080;
MultiplexerEchoServer echoServer = new MultiplexerEchoServer(port);
new Thread(echoServer, "EchoServer001").start();
}
}
public class MultiplexerEchoServer implements Runnable {
//多路复用选择器
Selector selector;
//ServerSocketChannel EchoServer非阻塞式通道
ServerSocketChannel serverSocketChannel;
volatile boolean isStoped = false;
public MultiplexerEchoServer(int port) {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(port), 1024);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run() {
System.out.println("The EchoServer start");
//多路复用选择器 阻塞一秒 之后处理 acceptable 事件 和 readable 事件
while (!isStoped) {
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
SelectionKey selectionKey = null;
//获取
while (iterator.hasNext()) {
selectionKey = iterator.next();
iterator.remove();
handInput(selectionKey);
}
} catch (Exception e) {
e.printStackTrace();
System.exit(0);
}
}
//服务器关闭 关闭资源
if (selector != null) {
try {
selector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//处理客户端请求
private void handInput(SelectionKey selectionKey) throws Exception {
if (selectionKey.isValid()) {
//客户端发起连接
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
//设置非阻塞模式
socketChannel.configureBlocking(false);
//向选择器注册 readable 事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
//读取客户端消息
if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(byteBuffer);
if (readBytes > 0) {
//读取到数据
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String echoMessage = new String(bytes, "UTF-8");
System.out.println("recive client message:" + echoMessage);
//停止服务器
if ("STOP".equals(echoMessage)) {
this.stop();
return;
}
doWrite(socketChannel, echoMessage);
} else if (readBytes < 0) {
//链路关闭
selectionKey.cancel();
socketChannel.close();
} else {
//没有读取到数据
}
}
}
}
//向客户端发送消息
private void doWrite(SocketChannel socketChannel, String echoMessage) throws Exception {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byte[] bytes = echoMessage.getBytes("UTF-8");
byteBuffer.put(bytes);
byteBuffer.flip();
socketChannel.write(byteBuffer);
}
public void stop() {
this.isStoped = true;
}
}
EchoClient与EchoSever逻辑上并无太大区别,只是EchoClient关注的是isConnectable(连接就绪)事件和isReadable(读就绪)事件。
public class EchoClient {
public static void main(String[] args) {
//端口
int port = 8080;
//ip 地址
String address = "127.0.0.1";
EchoClientHandle echoClientHandle = new EchoClientHandle(address, port);
new Thread(echoClientHandle).start();
}
}
public class EchoClientHandle implements Runnable {
//选择器
Selector selector;
SocketChannel socketChannel;
volatile boolean isStop = false;
String host;
int port;
public EchoClientHandle(String host, int port) {
this.host = host;
this.port = port;
try {
selector = Selector.open();
this.socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run() {
//直接链接客户端
try {
doConnect();
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
while (!isStop) {
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
SelectionKey selectionKey;
while (iterator.hasNext()) {
selectionKey = iterator.next();
iterator.remove();
handInput(selectionKey);
}
} catch (Exception e) {
}
}
}
//处理事件
private void handInput(SelectionKey selectionKey) throws Exception {
SocketChannel sc = (SocketChannel) selectionKey.channel();
;
if (selectionKey.isValid()) {
//链接到服务端
if (selectionKey.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else {
//链接失败 程序退出
System.exit(1);
}
}
}
//读取服务端数据
if (selectionKey.isReadable()) {
//缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(byteBuffer);
if (readBytes > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String echoMessage = new String(bytes, "UTF-8");
System.out.println("recive server message:" + echoMessage);
}
//读取服务端消息 再发送消息给服务端
doWrite(sc);
}
}
//链接服务器
private void doConnect() {
try {
boolean connected = socketChannel.connect(new InetSocketAddress(host, port));
if (connected) {
//
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
//向服务端 发送消息
private void doWrite(SocketChannel socketChannel) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String line = bufferedReader.readLine();
if ("stop".equals(line)) {
this.stop();
return;
}
byte[] bytes = line.getBytes("UTF-8");
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(bytes);
byteBuffer.flip();
socketChannel.write(byteBuffer);
}
//停止客户端
private void stop() {
this.isStop = true;
}
}
4.Java AIO
AIO关注的事件完成后的处理,而不是就绪。还是以打印回显为例。服务端通过AsynchronousServerSocketChannel类的open方法创建异步通道。而后注册accept(接收请求完成)事件。需要两个参数,一个是附件attachment,另一个是完成处理器CompletionHandler,在事件完成后被调用。在accept的完成处理器中,需要再注册accept(接收请求完成)事件。从而形成一个环,循环处理客户端的请求。
以EchoClient代码为例:
public class EchoClient {
public static void main(String[] args) {
int port = 8080;
String host = "127.0.0.1";
EchoClientHandler echoClientHandle = new EchoClientHandler(host, port);
new Thread(echoClientHandle).start();
}
}
public class EchoClientHandler implements CompletionHandler<Void, EchoClientHandler>, Runnable {
private AsynchronousSocketChannel client;
private String host;
private int port;
private CountDownLatch latch;
public EchoClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
client = AsynchronousSocketChannel.open();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
client.connect(new InetSocketAddress(host, port), this, this);
try {
//线程阻塞
latch.await();
} catch (Exception e) {
e.printStackTrace();
}
try {
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void completed(Void result, EchoClientHandler attachent) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
//缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
String line;
try {
line = reader.readLine();
byte[] bytes = line.getBytes("UTF-8");
byteBuffer.put(bytes);
} catch (Exception e) {
e.printStackTrace();
latch.countDown();
return;
}
byteBuffer.flip();
//循环写入
client.write(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
if (attachment.hasRemaining()) {
client.write(attachment, attachment, this);
} else {
//写完成后 设置读完成触发事件
ByteBuffer readByteBuffer = ByteBuffer.allocate(1024);
client.read(readByteBuffer, readByteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] bytes = new byte[attachment.remaining()];
attachment.get(bytes);
String echoMessage = null;
try {
echoMessage = new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println("client recive message:" + echoMessage);
latch.countDown();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
latch.countDown();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
//释放线程
latch.countDown();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc, EchoClientHandler attachment) {
latch.countDown();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端创建AsynchronousSocketChannel对象,通过connect方法注册连接完成事件。在连接完成处理中,读取控制台输入,再通过write方法注册写完成事件,循环判断是否写完。写完成后再通过read方法注册读完成事件,将接收的消息打印出来。本例使用了很多内部类,略显复杂。