前言
Java NIO,被称为新 IO(New IO),是 Java 1.4 引入的。与IO API相比,它加入了很多新的东西。
那么,为什么要引入NIO呢,还是以前的答案:
不管是什么,只要是新入的东西,基本都有三个目的:
1.使得开发维护更便捷,减少程序员的开发工作量。
2.提高程序运行效率。
3.更加安全。
NIO的引入目的主要在于上面的2(提高程序运行效率),但是也不是说它就是完美的,就可以完全可以取代IO,世上毕竟没有完美的东西。具体的提高效率和不完美,请看下面详解的分析。
NIO概述
NIO的核心主要由如下几部分组成:
- Channels
- Buffers
- Selectors
NIO当然还有一些其他的工具类,但是最最核心的就是这三个类。学习和掌握了这三个类的原理,后面的一些工具类的使用也就是水到渠成啦。
Channel
什么是Channel?
Channel就是一个通道,用于传输数据,两端分别是缓冲区和实体(文件或者套接字)。所以通道有以下几个特点:
- 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。(同步和异步一般可以用阻塞和非阻塞来理解。
事实上经常可以看到同步和异步的说法。在FileStream的例子里,异步读写意味着不需要专门开设用户线程去读写数据,而且能保证主程序在BeginRead()之后能不等待read完成就继续往下执行。) - 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
Channel有哪些?
- FileChannel:从文件中读写数据。
- DatagramChannel:能通过UDP读写网络中的数据。
- SocketChannel:能通过TCP读写网络中的数据。
- ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
正如你所看到的,这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。
源码解读:
Channel源码:
public interface Channel extends Closeable {
public boolean isOpen();
public void close() throws IOException;
}
从这里我们可以看到,Channel接口只提供了关闭通道和检测通道是否打开这两个方法,剩下方法的都是由子接口和实现类来定义提供。
public interface WritableByteChannel
extends Channel
{
public int write(ByteBuffer src) throws IOException;
}
public interface ReadableByteChannel extends Channel
{
public int read(ByteBuffer dst) throws IOException;
}
public interface ByteChannel
extends ReadableByteChannel, WritableByteChannel
{
}
通道可以只读、只写或者同时读写,因为Channel类可以只实现只读接口ReadableByteChannel或者只实现只写接口WritableByteChannel,而我们常用的Channel类FileChannel、SocketChannel、DatagramChannel是双向通信的, 因为实现了ByteChannel接口。
Channel的获取:
IO在广义上可以分为:文件IO和网络IO。文件IO对应的通道为FileChannel,而网络IO对应的通道则有三个:SocketChannel、ServerSoketChannel和DatagramChannel。
文件通道
FileChannel对象不能直接创建,只能通过FileInputStream、OutputStream、RandomAccessFile对象的getChannel()来获取,如:
FileInputStream fis = new FileInputStream("c:/in.txt");
FileChannel fic = fis.getChannel();
FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。
1)使用通道读取文件:
public static void wirteByte() throws IOException{
RandomAccessFile aFile = new RandomAccessFile("D:/图片/8ed63b936f261c026285b5b41e06730f.jpg", "rw");
FileChannel fileChannel = aFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(48);
fileChannel.read(buffer);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
buffer.clear();
fileChannel.close();
aFile.close();
}
执行结果:
-1-40-1-3201674707370011114414400-1-370670866765877799810122013121111122518
2)使用通道写入文件:
public static void wirteByte() throws IOException{
FileOutputStream outputStream = new FileOutputStream("d:/test.txt");
FileChannel fileChannel = outputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(48);
byteBuffer.clear();
String string = "I,m superman!";
byteBuffer.put(string.getBytes());
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
fileChannel.write(byteBuffer);
}
fileChannel.close();
outputStream.close();
}
执行结果:
在这里总是要记住channel是要关闭的。
通道只能使用ByteBuffer,不管是读还是写,通道都要对接缓冲区。
3)通道的常用方法:
position();返回通道的文件位置
position(long newPosition):设置通道的文件位置
将上面读文件的程序修改下,来观察这几个方法:
public static void readByte() throws IOException{
RandomAccessFile aFile = new RandomAccessFile("D:/图片/8ed63b936f261c026285b5b41e06730f.jpg", "rw");
FileChannel fileChannel = aFile.getChannel();
System.out.println("此通道文件的总长度" + fileChannel.size() +"byte");
long position = fileChannel.position(); //通道当前位置
System.out.println("通道的当前位置为:"+position);
fileChannel.position(8); //设置新的通道位置,从该位置开始读取
ByteBuffer buffer = ByteBuffer.allocate(48);
fileChannel.read(buffer);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
buffer.clear();
fileChannel.close();
aFile.close();
}
执行结果:
此通道文件的总长度76752byte
通道的当前位置为:0
7370011114414400-1-3706708667658777998101220131211111225181915202926313029
文件大小:
FileChannel是线程安全的,可以多个线程在同一个实例上并发操作,但是其中有些方法(改变文件通道位置或者文件大小的方法)必须是单线程操作。
网络通道
一.SocketChannel:
SocketChannel是一个连接到TCP套接字的通道,获取的方式有两种:
1、打开一个SocketChannel并连接到互联网上某台服务器。
2、一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
上面这两种模式跟IO的Socket、ServerSocket类似,下面分别来看看客户端和服务器端:
一.SocketChannel:
从通道中读取数据:
public static void readByteFromSocketChannel() throws IOException, InterruptedException {
SocketChannel socketChannel = SocketChannel.open(); 获取socket通道
socketChannel.configureBlocking(false); 设置为非阻塞模式
socketChannel.connect(new InetSocketAddress("wap.cmread.com", 80)); // 建立连接,非阻塞模式下,该方法可能在连接建立之前就返回了
while (!socketChannel.finishConnect()) {
System.out.println("连接未建立");
Thread.sleep(5);
}
ByteBuffer buffer = ByteBuffer.allocate(48);
int readByte = socketChannel.read(buffer);
System.out.println(readByte);
socketChannel.close();
buffer.clear();
}
执行结果:
连接未建立
连接未建立
连接未建立
连接未建立
连接未建立
连接未建立
连接未建立
连接未建立
0
1.由于是非阻塞模式,通道在调用方法connect/read/writer这三个方法时,会出现这些情况:连接未建立,connect方法就返回了;尚未读取任何数据时,read方法就返回;尚未写出任何内容时,writer就返回。
2.由于只是建立了连接,通道里面其实没有任何的数据。
3.调用read方法,由于是非阻塞模式,所以在并未读取任何数据的情况下就返回0(尽管通道里面没有数据)。
4.循环代码中,是判断连接是否建立,从执行结果来看,循环执行了几次连接才建立(在循环里线程还有休眠)。
向通道中写入数据:
public static void writeToSocketChannel() throws IOException, InterruptedException{
SocketChannel socketChannel = SocketChannel.open(); // 获取socket通道
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("wap.cmread.com", 80)); // 建立连接,非阻塞模式下,该方法可能在连接建立之前就返回了
while (!socketChannel.finishConnect()) {
System.out.println("连接未建立");
Thread.sleep(5);
}
String string = "non-blocking socket channel";
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.put(string.getBytes());
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
buffer.clear();
socketChannel.close();
}
1、SocketChannel.write()方法的调用是在一个while循环中的。Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止。
二.ServerSocketChannel:
ServerSocketChannel是一个可以监听新进来的TCP连接的通道。重点API如下:
public abstract class ServerSocketChannel extends AbstractSelectableChannel
{
public static ServerSocketChannel open() throws IOException;
public abstract ServerSocket socket();
public abstract ServerSocket accept()throws IOException;
public final int validOps();
}
实例如下:
public static SocketChannel accept() throws IOException, InterruptedException{
ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(10000));
ssc.configureBlocking(false);
while (true) {
SocketChannel sc = ssc.accept();
if(sc != null)
return sc;
Thread.sleep (200);
}
}
1、获取一个ServerSocketChannel,并且监听10000端口,设置为非阻塞模式。
2、通过accept方法监听新接入进来的连接,这个方法会返回一个包含新进来的连接的SocketChannel(服务器端的通道的获取方式)。如果是阻塞模式,该方法会一直阻塞直到有新的连接进来。如果是非阻塞模式,则accept方法会立刻返回,返回值是null。
3、是因为在非阻塞模式下,需要检查SocketChannel是否为null。
socket通道与socket
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket socket = ssc.socket();
ServerSocketChannel ssc1 = socket.getChannel();
1、从这代码片段可以大概看到这样一种关系:所有socket通道(SocketChannel/ServerSocketChanne/DatagramSocketChannel)在被实例化之后,都是伴随生成对应的socket对象,就是前面IO章节介绍的java.net类(Socket/ServerSocket/DatagramSocket)。通过通道类的socket方法来获取。
2、java.net类(Socket/ServerSocket/DatagramSocket)现在可以通过getChannel方法来获取对应的通道。前提是这些socket对象不是使用传统方式(直接实例化)创建的。否则它就没有关联的socket通道,调用getChannel方法返回总是null。
DatagramChannel
正如SocketChannel对应Socket,ServerSocketChannel对应ServerSocket,每一个DatagramChannel对象也有一个关联的DatagramSocket对象。不过原命名模式在此并未适用:“DatagramSocketChannel”显得有点笨拙,因此采用了简洁的“DatagramChannel”名称。
正如SocketChannel模拟连接导向的流协议(如TCP/IP),DatagramChannel则模拟包导向的无连接协议(如UDP/IP)。
DatagramChannel是无连接的。每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载。与面向流的的socket不同,DatagramChannel可以发送单独的数据报给不同的目的地址。同样,DatagramChannel对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)。
示例如下:
public class DatagramChannelLearn {
public static void main(String args[]) throws IOException, InterruptedException {
recive();
Thread.sleep(1000);
send();
}
public static void send() throws IOException {
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.wrap("滴滴答答".getBytes("utf-8"));
datagramChannel.send(buffer, new InetSocketAddress("localhost", 10000));
datagramChannel.close();
}
public static void recive() {
Thread aThread = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.socket().bind(new InetSocketAddress(10000));
ByteBuffer buffer = ByteBuffer.allocate(48);
datagramChannel.configureBlocking(false);
while (datagramChannel.receive(buffer) == null) {
try {
System.out.println("无消息,继续监听中");
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
buffer.flip();
String recStr = Charset.forName("utf-8").newDecoder().decode(buffer).toString();
System.out.println(recStr);
datagramChannel.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
aThread.start();
}
}
结果如下:
无消息,继续监听中
无消息,继续监听中
无消息,继续监听中
滴滴答答
Channel中的一些方法:
Buffer
buffer是什么,它就是一个读取和存入数据缓冲区,它就像那一辆拖拉机。从通道中拉出煤炭(数据),或者给通道中拉去煤炭(数据)。没错,这就是它,一辆方便你我他的拖拉机。
buffer的核心
它的核心很简单。主要由三个东西构成:
- capacity(拖拉机容量)
- position(拖拉机装东西的一个标记,装东西时指的是装到哪了,下东西时,指的是下到哪了)
- limit(装和下的极限,装的时候指的就是容量,下的时候,指的就是上次装了多少东西)
来,看图:
左图是指写(装煤),右图是读(下煤)。所以,聪明的骚年,你肯定看懂了吧。
Buffer的基本用法
使用Buffer读写数据一般遵循以下四个步骤:
- 写入数据到Buffer
- 调用flip()方法
- 从Buffer中读取数据
- 调用clear()方法或者compact()方法
当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区并转换到写模式:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
示例:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip(); //make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
Buffer的类型
ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
Buffer中的一些方法
- ByteBuffer.allocate(48):分配
- 向Buffer中写数据:
1.从Channel写到Buffer的例子
int bytesRead = inChannel.read(buf); //read into buffer.
2.通过put方法写Buffer的例子:
buf.put(127); - flip():flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。
- 从Buffer中读取数据:
1.从Buffer读取数据到Channel的例子:
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
2.使用get()方法从Buffer中读取数据的例子
byte aByte = buf.get(); - rewind()方法: Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。
- clear()与compact()方法:都可以将读切换到写,具体前面有讲到。
- mark()与reset()方法:通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。
- equals()与compareTo()方法
equals()
当满足下列条件时,表示两个Buffer相等:
1.有相同的类型(byte、char、int等)。
2.Buffer中剩余的byte、char等的个数相等。
3.Buffer中所有剩余的byte、char等都相同。
如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。
compareTo()方法
compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:
第一个不相等的元素小于另一个Buffer中对应的元素 。
所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。
Scatter/Gather
这里很简单。(就是可以直接读到一个buffer的数组,或者用一个buffer数组写入,都是按照数组的顺序进行的。有时候就会很方便,比如分开一个消息的消息头和消息体)
直接参看如下链接:
Scatter/Gather
Selector
NIO VS IO
IO | NIO |
---|---|
面向流 | 面向缓冲 |
阻塞IO | 非阻塞IO |
无 | 选择器 |
区别如上所示,如果你用过NIO那么你肯定一下子就能理解上面是什么意思,但是如果你没有用过的话,可能对你来说理解起来还是有些困难。但是不要着急,理解起来有困难是很正常的,毕竟第一次接触,慢慢的不要着急向后看,你就会理解了。
这些区别的影响
无论您选择IO或NIO工具箱,可能会影响您应用程序设计的以下几个方面:
- 对NIO或IO类的API调用。
- 数据处理。
- 用来处理数据的线程数。
对NIO或IO类的API调用:
面向的东西都不一样了,想都不用想,API调用肯定不一样嘛。
数据处理
在IO设计中,我们从InputStream或 Reader逐字节读取数据。假设你正在处理一基于行的文本数据流,例如:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
该文本行的流可以这样处理:
InputStream input = … ; // get the InputStream from the client socket
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();
一旦reader.readLine()方法返回,你就知道肯定文本行就已读完, readline()阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个readline()调用返回的时候,你知道这行包含年龄等。 正如你可以看到,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。
Java IO: 从一个阻塞的流中读数据) 而一个NIO的实现会有所不同,下面是一个简单的例子:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
注意第二行,从通道读取字节到ByteBuffer。当这个方法调用返回时,你不知道你所需的所有数据是否在缓冲区内。你所知道的是,该缓冲区包含一些字节,这使得处理有点困难。
假设第一次 read(buffer)调用后,读入缓冲区的数据只有半行,例如,“Name:An”,你能处理数据吗?显然不能,需要等待,直到整行数据读入缓存,在此之前,对数据的任何处理毫无意义。
所以,你怎么知道是否该缓冲区包含足够的数据可以处理呢?好了,你不知道。发现的方法只能查看缓冲区中的数据。其结果是,在你知道所有数据都在缓冲区里之前,你必须检查几次缓冲区的数据。这不仅效率低下,而且可以使程序设计方案杂乱不堪。例如:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
bufferFull()方法必须跟踪有多少数据读入缓冲区,并返回真或假,这取决于缓冲区是否已满。换句话说,如果缓冲区准备好被处理,那么表示缓冲区满了。
bufferFull()方法扫描缓冲区,但必须保持在bufferFull()方法被调用之前状态相同。如果没有,下一个读入缓冲区的数据可能无法读到正确的位置。这是不可能的,但却是需要注意的又一问题。
如果缓冲区已满,它可以被处理。如果它不满,并且在你的实际案例中有意义,你或许能处理其中的部分数据。但是许多情况下并非如此。
用来处理数据的线程数
NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如P2P网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。
Selector
这里直接上代码,自己看吧。
package com.newIO.learn;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
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;
public class SelectorLearn {
public static void main(String args[]) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
serverChannelUsingSelector();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
socketChannelTest();
} catch (IOException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
thread1.start();
Thread.sleep(1000);
thread2.start();
}
public static void serverChannelUsingSelector() throws IOException{
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(10000));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //注册自己感兴趣的事件
System.out.println("等待链接中");
while (true) {
if(selector.select() > 0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
if(selectionKey.isAcceptable()){
serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println(socketChannel);
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(48);
while (socketChannel.read(buffer) > 0) {
buffer.flip();
byte datas[] = new byte[48];
while(buffer.hasRemaining()){
buffer.get(datas,0,buffer.limit()); //读取哪里到哪里
System.out.print(new String(datas));
}
buffer.clear();
}
System.out.println();
}
iterator.remove();
}
}
}
}
public static void socketChannelTest() throws IOException, InterruptedException{
System.out.println("准备链接中");
int i = 1;
while(true){
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(10000));
if(socketChannel.finishConnect()){
ByteBuffer buffer = ByteBuffer.allocate(48);
String helloString = "Hello,I,m "+i + "嘻嘻嘻";
buffer.put(helloString.getBytes());
buffer.flip();
socketChannel.write(buffer);
}
i++;
Thread.sleep(500);
}
}
}