一、Java NIO簡介
Java NIO(New IO | Non Blocking IO)是從java1.4版本開始引入的一個新的IO API,可以替代標準的Java IO API.NIO與原來的IO有同樣的作用和目的,但是使用的方式完全不同,NIO支持面向緩衝區的、基於通道的IO操作。NIO將以更加高效的方式進行文件的讀寫操作。
二、Java NIO和IO的主要區別
傳統的IO是面向流的,數據是放在流裏面的,並且流是單向的
NIO是面向緩衝區的,通道只負責連接,可以把通道理解爲鐵路,緩衝區理解爲火車,數據是放在緩衝區當中的,並且是雙向的 .
三、通道與緩衝區
● Java NIO系統的核心在於:通道(Channel)和緩衝區(Buffer).通道表示打開到IO設備(例如:文件、套接字)的連接。若需要使用NIO系統,需要獲取用於連接IO設備的通道以及用於容納數據的緩衝區。然後操作緩衝區,對數據進行處理。
簡而言之,Channel負責傳輸、Buffer負責存儲
● 緩衝區(Buffer):一個用於特定基本數據類型的容器。由java.nio包定義的,所有緩衝區都是Buffer抽象類的子類
● Java NIO中的Buffer主要用於與NIO通道進行交互,數據是從通道讀入緩衝區,從緩衝區寫入通道中的。
初始化一個容量爲10的緩衝區,初始化狀態如下
向緩衝區存放五個字節的數據,狀態如下
調用filp()方法 從寫數據模式就變爲讀數據模式,相應的limit值發生變化,狀態如下
下面有一段程序來理解Buffer裏面的四個核心屬性以及常用的方法
package com.buffer;
import java.nio.ByteBuffer;
import org.junit.Test;
/**
*
* 一、緩衝區(Buffer): 在java NIO 中負責數據的存儲。 緩衝區就是數組。用於存儲不同數據類型的數據
*
* 根據數據類型不同(boolean 除外),提供了相應的緩衝區:
* ByteBuffer
* CharBuffer
* ShortBuffer
* IntBuffer
* LongBuffer
* FloatBuffer
* DoubleBuffer
*
*上述緩衝區的管理方式幾乎一致,通過allocate()獲取緩衝區
*
*二、緩衝區存取數據的核心方法:
* put():存入數據到緩衝區中
* get(): 獲取緩衝區中的數據
*
*
*三、緩衝區Buffer的四個核心屬性
* capacity :容量,表示緩衝區中最大存儲數據的容量。一旦聲明,不可改變
* limit : 界限,表示緩衝區中可以操作數據的大小(limit 後面的數據是不能進行讀取的)
* position: 位置,表示緩衝區中正在操作數據的位置。
*
*
* mark: 標記,用戶記錄當前positiond的位置。可以通過reset()恢復到mark標記的位置
*
* 0<= mark <= position <= limit <=capacity
*
*/
public class TestBuffer {
@Test
public void test2()
{
String str = "abcde";
//1.分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(str.getBytes());
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst,0,2);
System.out.println(new String(dst,0,2));
System.out.println("此時position的位置"+buf.position());
//mark()標記一下
buf.mark();
buf.get(dst,2,2);
System.out.println(new String(dst,2,2));
System.out.println("此時position的位置"+buf.position());
//reset() 恢復到mark標記的位置
buf.reset();
System.out.println("此時position的位置"+buf.position());
//判斷緩衝區中是否還有剩餘的數據
if(buf.hasRemaining())
{
//獲取緩衝區中可以操作的s數量
System.out.println(buf.remaining());
}
}
@Test
public void test1()
{
String str = "abcde";
//1.分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("---------allocate()------------");
System.out.println(buf.position()); //0
System.out.println(buf.limit()); //1024
System.out.println(buf.capacity()); //1024
//2.利用put()方法存入數據到緩衝區中
buf.put(str.getBytes());
System.out.println("---------put()------------");
System.out.println(buf.position()); // 5
System.out.println(buf.limit()); //1024
System.out.println(buf.capacity()); //1024
//3.切換成讀取數據的模式
buf.flip();
System.out.println("---------flip()------------");
System.out.println(buf.position()); // 0
System.out.println(buf.limit()); //5
System.out.println(buf.capacity()); //1024
//4.利用get()方法讀取緩衝區中的數據
byte[] dst = new byte[buf.limit()];
buf.get(dst);
System.out.println(new String(dst,0,dst.length));
System.out.println("---------get()------------");
System.out.println(buf.position()); // 5
System.out.println(buf.limit()); //5
System.out.println(buf.capacity()); //1024
//5.rewind() :可重複讀數據
buf.rewind();
System.out.println("---------rewind()------------");
System.out.println(buf.position()); //0
System.out.println(buf.limit()); //5
System.out.println(buf.capacity()); //1024
//6.clear():清空緩衝區. 但是緩衝區中的數據依然存在,但是處於被遺忘狀態,指針(limit position)回去到了最初狀態
buf.clear();
System.out.println("---------clear()------------");
System.out.println(buf.position()); //0
System.out.println(buf.limit()); //1024
System.out.println(buf.capacity()); //1024
System.out.println((char)buf.get());
}
}
直接緩衝區和非直接緩衝區的區別
* 非直接緩衝區:通過allocate() 方法分配緩衝區,將緩衝區建立在JVM的內存中。
* 直接緩衝區:通過 allocateDirect() 方法分配直接緩衝區,將緩衝區建立在操作系統的物理內存中。某種情況是可以提高效率的
當應用程序要讀取數據,那麼要向操作系統底層發起讀取數據的操作。由於數據不能直接傳輸,首先讀取到內核地址空間,copy到用戶地址空間(jvm內存),最後讀取到應用程序當中。那麼那段copy操作時耗費時間和資源的。
當應用程序要寫數據到磁盤的時候也是一樣,先寫入到用戶地址空間,然後複製到內核空間,最後寫入到磁盤當中
當應用程序對直接緩衝區操作的時候,面對的是操作系統的物理內存,因爲直接緩衝區是分配在物理內存中的,在讀寫數據的時候,不需要進行copy操作。但是呢,我們將數據寫入到物理內存的映射文件中呢,數據多會寫入到磁盤是由os系統來控制的。
並且直接緩衝區的分配和銷燬是耗資源的和時間的,銷燬的時候需要由垃圾回收機制來銷燬,但是又不確定垃圾回收機制多會進行。
package com.buffer;
import java.nio.ByteBuffer;
import org.junit.Test;
/*
* 一、直接緩衝區和非直接緩衝區
*
* 非直接緩衝區:通過allocate() 方法分配緩衝區,將緩衝區建立在JVM的內存中。
* 直接緩衝區:通過 allocateDirect() 方法分配直接緩衝區,將緩衝區建立在操作系統的物理內存中。某種情況是可以提高效率的
*
*/
public class TestBuffer2 {
@Test
public void testBuffer()
{
//分配直接緩衝區
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
//判斷是不是直接緩衝區
System.out.println(buf.isDirect());
}
}
看一下allocate()方法和allocateDirect()方法的源碼。
很明顯,allocate分配時分配到jvm的堆上面。
allocateDirect分配是在操作系統的物理內存上分配的。
● 通道:由java.nio.chaneels包定義的,Channel表示IO源與目標打開的連接。Channel類似於傳統的“流”。只不過Channel本身不能直接訪問數據,Channel只能與Buffer進行交互。
package com.buffer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import org.junit.Test;
/**
*
* 一、通道:用於源節點與目標節點的連接。在Java NIO中,負責緩衝區中數據的傳輸,通道本身不存儲任何數據
* 因此需要配合緩衝區進行傳輸。
*
* 二、通道的一些主要實現類
* java.nio.chaneels.Chaneel 接口:
* |--FileChannel :用於本地
* |--SocketChannel :用於tcp
* |--ServerSocketChannel :用於tcp
* |--DatagramChannel :用於udp傳輸
*
* 三、獲取通道
* 1、java 針對支持通道的類提供了getChannel()方法
* 本地io:
* FileInputStream/FileOutputStream
* RandomAccessFile
*
* 網絡io:
* Socket
* ServerSocket
* DatagramScoket
*
* 2、在JDK1.7中的NIO.2針對各個通道提供了一個靜態方法open()
* 3、在JDK1.7中的NIO.2的Files工具類的newByteChannel()
*
*四、通道之間的數據傳輸
*transferFrom()
*transferto()
*
*
*五、分散(Scatter)與聚集(Gather)
*
*分散讀取(Scatter Reads) : 將通道中的數據分散到多個緩衝區中
*聚集寫入(Gather writes) : 將多個緩衝區中的數據聚集到通道中
*
*
*六、字符集 :CharSet
*
*編碼: 字符串- 》字節數組
*解碼: 字節數組 -》 字符串
*/
public class TestChaneel {
//字符集
@Test
public void test6() throws CharacterCodingException
{
Charset cs1 = Charset.forName("GBK");
//獲取編碼器
CharsetEncoder ce = cs1.newEncoder();
//獲取解碼器
CharsetDecoder de = cs1.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(1024);
charBuffer.put("威威");
charBuffer.flip();
//編碼
ByteBuffer bbuf = ce.encode(charBuffer);
for(int i = 0;i<4;i++)
{
System.out.println(bbuf.get());
}
//解碼
bbuf.flip();
CharBuffer dbuf = de.decode(bbuf);
System.out.println(dbuf.toString());
}
@Test
public void test5()
{
SortedMap<String,Charset> availableCharsets = Charset.availableCharsets();
Set<Entry<String,Charset>> entrySet = availableCharsets.entrySet();
for(Entry<String,Charset> entry : entrySet)
{
System.out.println(entry.getKey()+"="+entry.getValue());
}
}
@Test
public void test4() throws IOException
{
RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
//1.獲取通道
FileChannel channel1 = raf1.getChannel();
//2.分配指定大小的緩衝區
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//3.分散讀取
ByteBuffer[] bufs = {buf1,buf2};
channel1.read(bufs);
//
for (ByteBuffer byteBuffer : bufs) {
byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(),0,bufs[0].limit()));
System.out.println("-------------------------------------");
System.out.println(new String(bufs[1].array(),0,bufs[1].limit()));
//聚集寫入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
}
//通道之間的數據傳輸(直接緩衝區的方式)
@Test
public void test3() throws IOException
{
FileChannel inChannel = FileChannel.open(Paths.get("d:/1.avi"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:/2.avi"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
}
//2.使用直接緩衝區完成文件的複製(內存映射文件)
@Test
public void test2() throws IOException //耗費的時間125
{
long start = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("d:/1.avi"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:/2.avi"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//內存映射文件
MappedByteBuffer inMappedBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接對緩衝區進行數據的讀寫操作
byte[] dst = new byte[inMappedBuffer.limit()];
inMappedBuffer.get(dst);
outMappedBuffer.put(dst);
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println("耗費的時間"+(end-start));
}
//1.利用通道完成文件的複製(非直接緩衝區)
@Test
public void test1() //耗費的時間1396
{
long start = System.currentTimeMillis();
FileInputStream fis=null;
FileOutputStream fos=null;
//1.獲取通道
FileChannel inChannel =null;
FileChannel outChannel =null;
try {
fis = new FileInputStream("d:/1.avi");
fos = new FileOutputStream("d:/2.avi");
inChannel = fis.getChannel();
outChannel = fos.getChannel();
//2.分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
//3.將通道中的數據存入到緩衝區中
while(inChannel.read(buf) != -1)
{
buf.flip(); //切換成讀取數據的模式
//4.將緩衝區中的數據寫入到通道
outChannel.write(buf);
buf.clear(); //清空緩衝區
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
if(outChannel!=null)
{
try {
outChannel.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(inChannel !=null)
{
try {
inChannel.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(fos!=null)
{
try {
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(fis !=null)
{
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
long end = System.currentTimeMillis();
System.out.println("耗費的時間"+(end-start));
}
}
四、NIO的非阻塞式網絡通信
● 傳統的IO流都是阻塞式的。也就是說,當一個線程調用read()或write()時,該線程阻塞,直到有一些數據被讀取或寫入,該線程在此期間不能執行其他任務。因此在完成網絡通信進行IO操作時,由於線程會阻塞,所以服務器端必須爲每個客戶端都提供一個獨立的線程進行處理,當服務器端需要處理大量客戶端時,性能急劇下降。
● Java NIO時非阻塞式的,當線程從某通道進行讀寫數據時,若沒有數據可用時,該線程進行其他的任務。線程通常將非阻塞IO的空閒時間用於在其他通道上執行IO操作,所以單獨的線程可以管理多個輸入和輸出通道。因此,NIO可以讓服務區端使用一個或有限個線程來同時處理連接到服務器端的所有客戶端。
每一個通道都會註冊到選擇器當中,選擇器會監控每一個通道,讓發現通道的數據準備好的時候,纔會讓服務器端分配給一個或者多個線程去執行這個通道的任務。當這個通道數據沒有準備好的時候,其他線程可以幹其他的事情。這樣就大大的提高了CPU的利用率
接下來先來一個阻塞的NIO程序
package com.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import org.junit.Test;
public class TestBlockingNIO2 {
//客戶端
@Test
public void Client() throws IOException{
//1.獲取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
FileChannel inChanner = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
//2.分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
//3.讀取本地文件併發送到服務端去
while(inChanner.read(buf)!=-1)
{
buf.flip();
sChannel.write(buf);
buf.clear();
}
sChannel.shutdownOutput();
//4.接收服務端的反饋
int len = 0;
while((len = sChannel.read(buf))!=-1)
{
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
//關閉通道
inChanner.close();
sChannel.close();
}
//服務區端
@Test
public void Server() throws IOException{
//獲取通道
ServerSocketChannel ss = ServerSocketChannel.open();
FileChannel outChanner = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//綁定連接
ss.bind(new InetSocketAddress(9898));
//獲取客戶端連接的通道
SocketChannel socketChannel = ss.accept();
//分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
//讀取客戶端傳過來的數據 並保存到本地
while(socketChannel.read(buf)!=-1)
{
buf.flip();
outChanner.write(buf);
buf.clear();
}
//發送反饋給客戶端
buf.put("服務端接收數據成功".getBytes());
buf.flip();
socketChannel.write(buf);
//關閉通道
socketChannel.close();
outChanner.close();
ss.close();
}
}
非阻塞式的NIO
package com.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
import org.junit.Test;
/**
*
* 一、使用NIO完成網絡通信的三個核心
*
* 1.通道:負責連接
*
* java.nio.channels.Channel 接口:
* |--SelectableChannel
* |--SocketChannel
* |--ServerSocketChannel
* |--DatagramChannel
*
* |--pipe.SinkChannel
* |--pipe.SourceChannel
*
*
*
*
* 2.緩衝區:負責存儲數據
*
* 3.選擇器:是SelectableChannel的多路複用器,用於監控SelectableChannel的IO狀況
*
*/
public class TestNonBlockingNIO {
//客戶端
@Test
public void Client() throws IOException{
//1.獲取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
//2.切換成非阻塞模式
sChannel.configureBlocking(false);
//3.分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
//4.發送數據到服務器端
buf.put(new Date().toString().getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
//關閉通道
sChannel.close();
}
//服務區端
@Test
public void Server() throws IOException{
//1.獲取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2.切換成非阻塞模式
ssChannel.configureBlocking(false);
//3.綁定連接
ssChannel.bind(new InetSocketAddress(9898));
//4.獲取選擇器
Selector selector = Selector.open();
//5.將通道註冊到選擇器上 ,並且指定監聽接收事件, 第二個參數 是監測狀態 有四種常量
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6.輪詢式的獲取選擇器上已經準備就緒的事件
while(selector.select()>0)
{
//7.獲取當前選擇器中所有註冊的選擇鍵(已就緒的監聽事件)
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext())
{
//8.獲取準備就緒的事件
SelectionKey sk = it.next();
//9.判斷具體是什麼事件準備就緒
if(sk.isAcceptable())
{
//10.若接收就緒,獲取客戶端連接
SocketChannel sChannel = ssChannel.accept();
//11. 切換成非阻塞模式
sChannel.configureBlocking(false);
//12.將該通道註冊到選擇器上
sChannel.register(selector, SelectionKey.OP_READ);
}else if(sk.isReadable())
{
//13.獲取當前選擇器上 “讀就緒”狀態的通道
SocketChannel socketChannel = (SocketChannel)sk.channel();
//14.讀取數據
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = socketChannel.read(buf))>0)
{
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
}
//取消選擇鍵 SelectionKey
it.remove();
}
}
}
}