NIO入门和使用
阻塞和非阻塞,同步和异步
阻塞和非阻塞:程序在访问数据的时候,阻塞往往需要等待缓冲区中数据准备好才能继续处理,否则等待;非阻塞则是不论数据是否准备好都会返回。
同步和异步:同步应用程序直接参与IO读写操作,同步IO方式必须阻塞在某个方法上(阻塞IO或非阻塞轮询IO的方式);而异步的IO操作交给了操作系统去处理,当IO完成后给应用程序一个通知即可。
Java IO模型
BIO:JDK1.4以前都是BIO,消耗线程性能开销
NIO:JDK1.4后,实现IO事件的轮询方式(Linux多路复用技术select模式),如Netty,Mina。
AIO:JDK1.7(NIO2.0),Linux epoll模式,真正实现了异步。服务端:AsynchronousServerSocketChannel
客户端:AsynchronousSocketChannel。用户处理器:CompletionHandler接口,这个接口实现应用程序向操作系统发起IO请求(由系统内核完成IO操作),当完成后处理具体的逻辑,否则可以做其他事情。
NIO概念
java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO)。Sun 官方标榜的特性如下:为所有的原始类型提供(Buffer)缓存支持。字符集编码解码解决方案。Channel:一个新的原始I/O 抽象。支持锁和内存映射文件的文件访问接口。提供多路(non-bloking)非阻塞式的高伸缩性网络I/O。
为什么要使用NIO?NIO的创建目的是为了让Java程序员可以实现高速I/O而无需编写自定义的本机代码。NIO将最耗时的I/O操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。
原来的I/O库(在 java.io.*中)与NIO最重要的区别是数据打包和传输的方式。正如前面提到的,原来的I/O以流的方式处理数据,而NIO以块的方式处理数据。
面向流的I/O系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的I/O通常相当慢。
面向块的I/O系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的I/O缺少一些面向流的I/O所具有的优雅性和简单性。
NIO包括:通道 Channels、缓冲区 Buffers、选择器 Selectors
Buffer
Buffer(缓冲区)是一个对象,它包含一些要写入或者刚读出的数据。
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区主要是三个变量:position、limit、capacity。
这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。
Position:position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。
Limit:limit 变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。position 总是小于或者等于 limit。
Capacity:表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小(容量)。limit 决不能大于 capacity。
Channel
Channel(通道)是一个对象,可以通过它读取和写入数据。拿NIO与原来的I/O做个比较,通道就像是流。所有数据都通过 Buffer 对象来处理。通过通道将数据写入缓冲区或通过通道从缓冲区读出数据。通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是InputStream 或者OutputStream 的子类), 而通道可以用于读、写或者同时用于读写,因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。
Selector
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
NIO的读写
读取文件涉及三个步骤:(1)从FileInputStream获取Channel,(2)创建Buffer,(3) 数据从Channel读到Buffer中。同样,写文件也是这三个类似的步骤,只是将读改为写。
下面的例子说明的读写文件实现文件的复制。其中内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。
read()和write()调用得到了极大的简化,因为许多工作细节都由缓冲区完成了。
clear()和flip()方法用于让缓冲区在读和写之间切换。
allocate()方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中
String infile = "F:\\file_to_copy.txt";
String outfile = "F:\\copied_file.txt";
//文件输入输出流
FileInputStreamfin = new FileInputStream(infile);
FileOutputStream fout= new FileOutputStream(outfile);
// 获取读的通道
FileChannelfcin = fin.getChannel();
// 获取写的通道
FileChannelfcout = fout.getChannel();
// 定义缓冲区,并指定大小
ByteBufferbuffer = ByteBuffer.allocate(1024);
while (true) {
// 清空缓冲区
buffer.clear();
//从通道读取一个数据到缓冲区
int r = fcin.read(buffer);
//判断是否有从通道读到数据
if (r == -1) {
break;
}
//将buffer指针指向头部
buffer.flip();
//把缓冲区数据写入通道
fcout.write(buffer);
}
连网和异步 I/O
异步 I/O 是一种没有阻塞地读写数据的方法。通常,在代码进行read()调用时,代码会阻塞直至有可供读取的数据。同样,write()调用将会阻塞直至数据能够写入。而异步 I/O 调用不会阻塞。当可读的数据的到达、新的套接字连接,等等,系统将会发出通知。
异步I/O的一个优势在于,它允许同时根据大量的输入和输出执行I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。
异步 I/O 中的核心对象名为 Selector,一个单独的线程可以管理多个channel,从而管理多个网络连接。
服务端接收
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
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;
/**
* Created by SunLin on 2018.3.13
*/
public class NioReceiver {
public static void main(String[] args) throws Exception {
//申请缓冲区
ByteBuffer echoBuffer = ByteBuffer.allocate(100);
//创建一个selector,便于注册一些I/O事件
Selector selector = Selector.open();
//创建ServerSocketChannel对象监听不同端口
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置为非阻塞监听
ssc.configureBlocking(false);
//将通道绑定到服务器套接字上
ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress("localhost", 8081);
ss.bind(address);
//将新打开的ServerSocketChannels注册到Selector上
//register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件
SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("开始监听……");
while (true) {
// Selector的select()方法,这个方法会阻塞,直到至少有一个已注册的事件发生。
int num = selector.select();
System.out.println("selector.select()=" + num);
//Selector的selectedKeys()方法返回发生了事件的SelectionKey对象的一个集合。
Set selectedKeys = selector.selectedKeys();
//迭代这个集合
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey sKey = (SelectionKey) it.next();
SocketChannel channel = null;
if (sKey.isAcceptable()) {
ServerSocketChannel sc = (ServerSocketChannel) key.channel();
//接受连接请求
channel = sc.accept();
channel.configureBlocking(false);
//注册监听读取事件
channel.register(selector, SelectionKey.OP_READ);
it.remove();
} else if (sKey.isReadable()) {
channel = (SocketChannel) sKey.channel();
while (true) {
echoBuffer.clear();
int r = channel.read(echoBuffer);
if (r <= 0) {
channel.close();
System.out.println("接收完毕,断开连接");
break;
}
System.out.println("内容长度:" + r + "<--->内容:" + new String(echoBuffer.array(), 0, echoBuffer.position()));
echoBuffer.flip();
}
//删除处理过的 SelectionKey
it.remove();
} else {
channel.close();
}
}
}
}
}
客户端发送
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.Set;
/**
* Created by SunLin on 2018.3.13
*/
public class NioSend {
public static void main(String[] args) throws Exception {
ByteBuffer echoBuffer = ByteBuffer.allocate(1024);
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
// 请求连接
channel.connect(new InetSocketAddress("localhost", 8081));
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT);
int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
it.remove();
if (key.isConnectable()) {
if (channel.isConnectionPending()) {
if (channel.finishConnect()) {
// 只有当连接成功后才能注册OP_READ事件
key.interestOps(SelectionKey.OP_READ);
echoBuffer.clear();
echoBuffer.put("你好,我发送了一些数据".getBytes());
echoBuffer.flip();
System.out.println("##" + new String(echoBuffer.array()));
channel.write(echoBuffer);
System.out.println("写入完毕");
} else {
key.cancel();
}
}
}
}
}
}
总结
因为java nio使用比较繁琐,netty对java nio进行了大量的封装,了解了NIO的原理和使用,对于Netty的理解和使用会有很大的帮助。
Netty百科词条:
Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。“快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议的实现经验,这些协议包括FTP,SMTP,HTTP,各种二进制,文本协议,并经过相当精心设计的项目,最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。
参考:https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html