相较于传统的IO基于字节流和字符流的阻塞式操作,NIO则是基于通道(channel)和缓冲区(buffer)的非阻塞式操作。数据总是从通道读取到缓冲区或者从缓冲区写入到通道。NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中(map()方法),这样就可以像访问内存一样来访问文件了,也可以采用“用竹筒多次重复取水”的方式,创建一个固定大小的ByteBuff,每次从Channel中读取、写入的也都是固定大小的数据。
Channel:Channel类似流,程序不能直接访问Channel中的数据,Channel只能与Buffer交互,也就是Channel中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。Java NIO中最重要的Channel的实现:
- FileChannel从文件中读写数据。
- DatagramChannel能通过UDP读写网络中的数据。
- SocketChannel能通过TCP读写网络中的数据。
- ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样,对每一个新进来的连接都会创建一个SocketChannel。
Buffer:Buffer的三个属性capacity、position、limit;position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。
- capacity:作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
- position:当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向后移动到下一个可插入数据的Buffer单元。position最大值可为 capacity – 1. 当读取数据时,也是从某个特定位置读,当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向后移动到下一个可读的位置。
- limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。方法flip()将Buffer从写模式切换到读模式
Selector:选择器是JavaNIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。所有希望采用非阻塞方式通信的Channel都应该注册到Selector上,通过SelectableChannel.register()方法来实现,register()方法的第二个参数是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣,如果对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来。可以监听四种不同类型的事件:
- Read(1)
- Write(4)
- Connect(8)
- Accept(16)
一个Selector实例有三个SelectionKey集合:
- 所有的SelectionKey集合:代表了注册在该Selector上的Channel,通过keys()方法返回
- 被选择的SelectionKey集合:代表了所有可通过select()方法获取的、需要进行IO处理的Channel,通过selectedKeys()方法返回
- 被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKeys会被彻底删除
下面是一个读取文件的简单例子:
public class NIODemo {
public static void main(String[] args) {
try {
RandomAccessFile aFile = new RandomAccessFile("D:\\test.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//创建一个capacity为48 bytes 的Buffer
ByteBuffer buf = ByteBuffer.allocate(48);
// 将字节序列从channel写到给定buffer,buffer的大小即每次能写入的最大值,这里则是上面定义的48b。
while(inChannel.read(buf) != -1) {
buf.flip(); //反转Buffer,接着再从Buffer中读取数据,将Buffer从写模式切换到读模式
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
//这里注意:当输入字节序列对于给定 charset 来说是不合法的,或者输入字符序列不是合法的 16 位 Unicode 序列时,
//这里会抛出java.nio.charset.MalformedInputException
//例如:当在这块ByteBuffer空间中末尾的字符不符合UTF-8编码。
//可以选择初始化一块很大的ByteBuffer空间或者使用map()方法将文件全部映射到内存;或者以指定字符读取文件
CharBuffer cbuff = decoder.decode(buf);
System.out.println(cbuff);
buf.clear(); //clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。
//Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。
}
aFile.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
下面模拟一个客户端和服务器通过Socket连接使用NIO实现的过程:
//服务端
public class Server {
public static void main(String[] args) {
try {
Selector selector = Selector.open(); //创建一个selector
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress("127.0.0.01",30000));
channel.configureBlocking(false); //与Selector一起使用时,Channel必须处于非阻塞模式下。
//将ServerSocketChannel注册到selector上,返回一个SelectionKey对象
//这里ServerSocketChannel只能注册SelectionKey.OP_ACCEPT
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// select方法会一直阻塞,直到IO事件到达或者设置超时返回;
//当需要处理IO操作,对应的SelectionKey加入被选择的SelectionKey集合
int readyChannels = selector.select();//返回的是Channel的个数
if (readyChannels == 0) {
continue;
}
Set<SelectionKey> selectedKeys = selector.selectedKeys();//返回此Selector的已选择键集。
System.out.println("被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = "+selectedKeys.size());
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 检测channel中什么事件或操作已经就绪
if (key.isAcceptable()) { //1.对应的Channel包含客户端的连接
ServerSocketChannel sscTemp = (ServerSocketChannel) key.channel();
//得到一个连接好的SocketChannel
SocketChannel socketChannel = sscTemp.accept();
socketChannel.configureBlocking(false);
//将得到的SocketChannel注册到Selector上,并对该SocketChannel添加兴趣
//SocketChannel中可以注册SelectionKey.OP_READ、SelectionKey.OP_WRITE、SelectionKey.OP_CONNECT
socketChannel.register(selector, SelectionKey.OP_READ);// ......(1)
System.out.println("注册在该Selector上的Channel个数:" + selector.keys().size());
} else if (key.isReadable()) { //2.读取数据,满足Readable条件,则此Channel已准备好进行读取
//读取通道中的数据
SocketChannel readchannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(128);
buffer.clear();
try {
while (readchannel.read(buffer) > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println("server收到的数据 ..."+ new String(bytes));
}
} catch (IOException e) {
e.printStackTrace();
}
//......(2)
// if(readchannel.read(buffer) < 0) {
// key.cancel();
// }
} else if (key.isConnectable()) { //3.连接
System.out.println("connect ...");
} else if (key.isWritable()) { //4.写
System.out.println("write .. ");
}
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端代码如下:
public class Client extends Thread{
String threadname;
public Client(String threadname){
this.threadname = threadname;
}
@Override
public void run(){
SocketChannel socketChannel;
Selector selector;
try {
selector = Selector.open();
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);
socketChannel = SocketChannel.open(isa);
socketChannel.configureBlocking(false);
//将SocketChannel对象注册到指定Selector
//......(3)
socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
String data = "这是内容,发给服务端,来自 -- " + threadname;
ByteBuffer bd = ByteBuffer.allocate(64);
bd.clear();//开始往Buffer里写数据。
bd.put(data.getBytes());//写入Buffer
bd.flip();//写模式切换为读模式
//Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止
while(bd.hasRemaining()){ //当且仅当此缓冲区中至少还有一个元素时返回 true
socketChannel.write(bd);
}
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
es.submit(new Client("线程1"));
es.submit(new Client("线程2"));
es.shutdown();
}
}
debug运行客户端和服务端输出结果如下:
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 1
注册在该Selector上的Channel个数:2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
注册在该Selector上的Channel个数:3
server收到的数据 ...这是内容,发给服务端,来自 -- 线程2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
server收到的数据 ...这是内容,发给服务端,来自 -- 线程1
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2
...
结果解析:一开始服务端的Selector注册了一个ServerSocketChannel监听新进来的TCP连接,因此只有一个Channel,当连接进来后,对新进来的连接都会创建一个SocketChannel,并注册新的事件(read事件 (1)处),这时注册在该Selector上的Channel个数就 +1,而任何对 Key所关联的兴趣操作集的改变,都只在下次调用了 select()方法后才会生效,所以read事件在下次select()方法之后进行,而接下来需要进行IO处理的Channel +1。
总共有两个线程创建了两个SocketChannel,加上ServerSocketChannel,因此服务端Selector上共注册了3个Channel。处理完数据之后,发现一直在输出外层循环中的“被选择的SelectionKey集合个数 = 需要进行IO处理的Channel = 2”,且循环会一直进入key.isReadable(),可是client中的Channel已经关闭了,这是为什么呢?当客户端主动切断连接时,read仍然起作用,也就是说,状态仍然是有东西可读,不过读出来的字节是0,所以需要进一步判断一下读取的字节的数目,把(2)处的注释打开即可删除该Channel的注册关系,当读完Channel中的数据之后,客户端的两个Channel就没有了,就只剩下了ServerSocketChannel这一个Channel了。可以运行新的Client,Server仍可以正常工作。
还有一个坑!:
- 在NIO中通过Selector的轮询当前是否有IO事件,根据JDK NIO api描述,Selector的select方法会一直阻塞,直到IO事件达到或超时,但是在Linux平台上这里有时会出现问题,在某些场景下select()方法会直接返回,即使没有超时并且也没有IO事件到达,这就是著名的epoll bug,这是一个比较严重的bug,它会导致线程陷入死循环,会让CPU飙到100%,极大地影响系统的可靠性,到目前为止,JDK都没有完全解决这个问题。
参考文章: