NIO是非阻塞的IO,Java NIO由一下幾個核心部分組成:Buffers
、Channels
、Selectors
。
Java NIO中有很多類和組件,但是Buffers
、Channels
、Selectors
構成了核心的API。其他組件如Pipe
和FileLock
,只不過是與其他三個核心組件共同使用的工具類。
- Channel:基本上所有的IO在NIO中都從一個
Channel
開始,Channel有點像流。數據可以從Channel讀到Buffer中,也可以從Buffer寫到Channel中。Channel和Buffer有好多類型,Channel主要有:FileChannel、DataGramChannel、SocketChannel、ServerSocketChannel
。涵蓋了UDP和TCP網絡的IO以及文件IO。
-
Buffer:NIO主要的Buffer有:
ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer
,這些Buffer涵蓋了你能通過IO發送的基本數據類型。 -
Selector:允許單線程處理多個
Channel
。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如一個聊天服務器中。要使用Selector,得先向Selector注冊Channel然后調用它的select()
方法。這個方法會一直堵塞知道某個注冊的通道有事件就緒。一旦這個方法返回線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。
緩沖區&Buffer
在整個Java新IO中,所有的操作都是以緩沖區進行的,使用緩沖區,則操作的性能將是最高的。
在Buffer中存在一系列的狀態變量,隨著寫入或讀取都有可能被改變,在緩沖區可以使用三個值表示緩沖區狀態
- position 位置
- limit 界限
- capacity 容量
ByteBuffer的使用
- 創建ByteBuffer
(1)使用allocate()靜態方法
ByteBuffer buffer=ByteBuffer.allocate(256);
以上方法將創建一個容量為256字節的ByteBuffer,如果發現創建的緩沖區容量太小,唯一的選擇就是重新創建一個大小合適的緩沖區.
(2)通過包裝一個已有的數組來創建
如下,通過包裝的方法創建的緩沖區保留了被包裝數組內保存的數據.
ByteBuffer buffer=ByteBuffer.wrap(byteArray);
如果要將一個字符串存入ByteBuffer,可以如下操作:
String sendString="你好,服務器. "; ByteBuffer sendBuffer=ByteBuffer.wrap(sendString.getBytes("UTF-16"));
- 緩沖區
- buffer.flip();
這個方法用來將緩沖區準備為數據傳出狀態,執行以上方法后,輸出通道會從數據的開頭而不是末尾開始.回繞保持緩沖區中的數據不變,只是準備寫入而不是讀取.
- buffer.clear();
這個方法實際上也不會改變緩沖區的數據,而只是簡單的重置了緩沖區的主要索引值.不必為了每次讀寫都創建新的緩沖區,那樣做會降低性能.相反,要重用現在的緩沖區,在再次讀取之前要清除緩沖區.
ByteBuffer轉為其他的Buffer,如:CharBuffer、DoubleBuffer、IntBuffer、LongBuffer、ShortBuffer,都有對應的asXXXBuffer()方法。
- 創建子緩沖區 buf.slice()
子緩沖區可以修改數據
import java.nio.IntBuffer ;
public class IntBufferDemo02{
public static void main(String args[]){
IntBuffer buf = IntBuffer.allocate(10) ; // 準備出10個大小的緩沖區
IntBuffer sub = null ; // 定義子緩沖區
for(int i=0;i<10;i++){
buf.put(2 * i + 1) ; // 在主緩沖區中加入10個奇數
}
// 需要通過slice() 創建子緩沖區
buf.position(2) ;
buf.limit(6) ;
sub = buf.slice() ;
for(int i=0;i<sub.capacity();i++){
int temp = sub.get(i) ;
sub.put(temp-1) ;
}
buf.flip() ; // 重設緩沖區
buf.limit(buf.capacity()) ;
System.out.print("主緩沖區中的內容:") ;
while(buf.hasRemaining()){
int x = buf.get() ;
System.out.print(x + "、") ;
}
}
}
- 創建只讀緩沖區 buf.asReadOnlyBuffer()
import java.nio.IntBuffer ;
public class IntBufferDemo03{
public static void main(String args[]){
IntBuffer buf = IntBuffer.allocate(10) ; // 準備出10個大小的緩沖區
IntBuffer read = null ; // 定義子緩沖區
for(int i=0;i<10;i++){
buf.put(2 * i + 1) ; // 在主緩沖區中加入10個奇數
}
read = buf.asReadOnlyBuffer() ;// 創建只讀緩沖區
read.flip() ; // 重設緩沖區
System.out.print("主緩沖區中的內容:") ;
while(read.hasRemaining()){
int x = read.get() ;
System.out.print(x + "、") ;
}
read.put(30) ; // 修改,錯誤
}
}
- 創建直接緩沖區
public static ByteBuffer allocateDirect(int capacity)
只能提高一些盡可能的性能。
import java.nio.ByteBuffer ;
public class ByteBufferDemo01{
public static void main(String args[]){
ByteBuffer buf = ByteBuffer.allocateDirect(10) ; // 準備出10個大小的緩沖區
byte temp[] = {1,3,5,7,9} ; // 設置內容
buf.put(temp) ; // 設置一組內容
buf.flip() ;
System.out.print("主緩沖區中的內容:") ;
while(buf.hasRemaining()){
int x = buf.get() ;
System.out.print(x + "、") ;
}
}
}
通道
- 可讀、可寫。程序不會直接操作通道,所有的內容讀到或者寫入緩沖區,再通過緩沖區中取得或寫入。
- 傳統的流操作分為輸入或輸出流,而通道本身是雙向操作的,可以完成輸入也可以完成輸出。
讀入方式
- RandomAccessFile:較慢
- FileInputStream:較慢
- 緩存讀取:速度較快
- 內存映射MapByteBuffer:最快!
FileChannel的三種內存映射模式
NO. | 常量 | 類型 | 描述 |
---|---|---|---|
1 | public static final FileChannel.MapMode.READ_ONLY | 常量 | 只讀映射模式 |
2 | public static final FileChannel.MapMode.READ_WRITE | 常量 | 讀取/寫入映射模式 |
3 | public static final FileChannel.MapMode.PRIVATE | 常量 | 專用(寫入時拷貝)映射模式 |
import java.nio.ByteBuffer ;
import java.nio.MappedByteBuffer ;
import java.nio.channels.FileChannel ;
import java.io.File ;
import java.io.FileOutputStream ;
import java.io.FileInputStream ;
public class FileChannelDemo03{
public static void main(String args[]) throws Exception{
File file = new File("d:" + File.separator + "mldn.txt") ;
FileInputStream input = null ;
input = new FileInputStream(file) ;
FileChannel fin = null ; // 定義輸入的通道
fin = input.getChannel() ; // 得到輸入的通道
MappedByteBuffer mbb = null ;
mbb = fin.map(FileChannel.MapMode.READ_ONLY,0,file.length()) ;
byte data[] = new byte[(int)file.length()] ; // 開辟空間接收內容
int foot = 0 ;
while(mbb.hasRemaining()){
data[foot++] = mbb.get() ; // 讀取數據
}
System.out.println(new String(data)) ; // 輸出內容
fin.close() ;
input.close() ;
}
}
內存映射在讀取時速度快,但是如果在使用以上操作代碼的時候,執行的是寫入操作則有可能非常危險,因為僅僅是改變數組中單個元素這種簡單的操作,就可能直接修改磁盤上的文件,因為修改數據與將數據保存在磁盤上是一樣的。
文件鎖FileLock
當一個線程將文件鎖定之后,其它線程無法操作此文件,要想進行文件鎖定操作,需要使用FileLock
類完成,此類的對象需要依靠FileChannel
進行實例化操作。
鎖定方式
共享鎖:允許多個線程進行文件的讀/寫操作
-
獨占鎖:只允許一個線程進行文件的讀/寫操作
字符集 Charset
整個NIO中,對于不同平臺的編碼操作,java都可以進行自動適應,因為可以使用字符集進行字符編碼的轉換。
在java中,所有信息都是以UNICODE進行編碼,計算機中存在多種編碼,NIO中提供了charset類來處理編碼問題,包括了創建編碼器(
CharsetEndoder
)和創建解碼器(CharsetDecoder
)的操作。
Charset latin1 = Charset.forName("ISO-8859-1") ; // 只能表示的英文字符
CharsetEncoder encoder = latin1.newEncoder() ; // 得到編碼器
CharsetDecoder decoder = latin1.newDecoder() ; // 得到解碼器
// 通過CharBuffer類中的
CharBuffer cb = CharBuffer.wrap("啊哈哈哈哈") ;
ByteBuffer buf = encoder.encode(cb) ; // 進行編碼操作
System.out.println(decoder.decode(buf)) ; // 進行解碼操作
Selector
解決服務器端的通訊性能。
- 使用
Selector
可以構建一個非阻塞的網絡服務 - 在NIO中實現網絡程序需要依靠
ServerSocketChannel
類與SocketChannel
類
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.Set;
import java.util.Iterator;
import java.util.Date;
import java.nio.channels.ServerSocketChannel;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
public class DateServer {
public static void main(String args[]) throws Exception {
int ports[] = { 8000, 8001, 8002, 8003, 8005, 8006 }; // 表示五個監聽端口
Selector selector = Selector.open(); // 通過open()方法找到Selector
for (int i = 0; i < ports.length; i++) {
ServerSocketChannel initSer = null;
initSer = ServerSocketChannel.open(); // 打開服務器的通道
initSer.configureBlocking(false); // 服務器配置為非阻塞
ServerSocket initSock = initSer.socket();
InetSocketAddress address = null;
address = new InetSocketAddress(ports[i]); // 實例化綁定地址
initSock.bind(address); // 進行服務的綁定
initSer.register(selector, SelectionKey.OP_ACCEPT); // 等待連接
System.out.println("服務器運行,在" + ports[i] + "端口監聽。");
}
// 要接收全部生成的key,并通過連接進行判斷是否獲取客戶端的輸出
int keysAdd = 0;
while ((keysAdd = selector.select()) > 0) { // 選擇一組鍵,并且相應的通道已經準備就緒
Set<SelectionKey> selectedKeys = selector.selectedKeys();// 取出全部生成的key
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next(); // 取出每一個key
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept(); // 接收新連接
client.configureBlocking(false);// 配置為非阻塞
ByteBuffer outBuf = ByteBuffer.allocateDirect(1024); //
outBuf.put(("當前的時間為:" + new Date()).getBytes()); // 向緩沖區中設置內容
outBuf.flip();
client.write(outBuf); // 輸出內容
client.close(); // 關閉
}
}
selectedKeys.clear(); // 清楚全部的key
}
}
}