在上一篇文章中對于I/O模型已經講的比較清楚了,在I/O密集型應用中使用Reactor模式可以明顯提高系統的性能(我們這里談到的性能很大程度上指的是吞吐量),但是在具體的開發過程中模式還是要落地成真實的代碼,使用傳統的I/O庫肯定是不行的,在Java中需要使用java.nio
包下的庫。
雖然是講NIO的實現,但本文將不會把所有Java NIO中的主要API全部過一遍,而是通過例子理清NIO到底可以做什么事情。
本文中提到的JDK源代碼都可以在
%JAVA_HOME%/jre/lib/rt.jar
中看到。
Java NIO最初在Java4中被引入,但是到今天還是有很大部分的開發者從來沒使用過NIO的API,因為基礎I/O已經能滿足了我們日常的開發需求。但如果要開發I/O密集型應用的場景下,NIO可以明顯的提升程序的性能,另外NIO與基礎I/O有本質思想上的區別。
本文主要講Java中的NIO,內容包含:
- Oracle官方對NIO的說法
- Java中NIO的歷史進程
- NIO和NIO.2的區別在哪里
- NIO中的主要類的介紹
- 使用NIO的API構建一個Socket服務器
Oracle官方對NIO的說法
首先看看Oracle的官方文檔中是怎么說的:
Java中對于I/O的支持主要包括
java.io
和java.nio
兩個包的內容,它們共同提供了如下特性:
- 通過數據流和序列化從文件系統中讀取和寫數據。
- 提供Charsets,解碼器和編碼器,用于在字節和Unicode字符之間的翻譯。
- 訪問文件、文件的屬性、文件系統。
- 提供異步的或者非阻塞多路復用I/O的API,用于構建可擴展的服務器程序。
這里并沒有提到網絡I/O的東西,在Java1.4以前,網絡I/O的API都是被放在java.net
包下,在NIO中才被一起放入了java.nio
包下。
Java中NIO的歷史進程
- 最開始Java中使用I/O來訪問文件系統只有通過
java.io.File
類來做,其中包含了一些對文件和目錄基本的操作。對于開發中常碰到的I/O需求一般都能覆蓋到,所以這也是日常開發工作中最常使用的I/O API。官方文檔中稱之為基礎I/O(Basic I/O)。
基礎I/O是基于各種流的概念的,其基本模型就是上一篇中講到的阻塞I/O。 - 為了進一步豐富I/O操作的API,也是為了提升在I/O密集型應用中的性能,基于Reactor模式,在Java1.4中引入了
java.nio
包,其中重點包含幾個類:
-
java.nio.Buffer
,用來存儲各種緩沖數據的容器。 -
java.nio.channels.Channel
,用于連接程序和I/O設備的數據通道。 -
java.nio.channels.Selector
,多路復用選擇器,在上一篇中講到過。 -
java.nio.charset.Charset
,用來編解碼。
- 在Java7中引入了NIO.2,引入了一系列新的API(主要在新加入的包
Java.nio.file
),對于訪問文件系統提供了更多的API實現,更加豐富的文件屬性類,增加了一些異步I/O的API。同時,還添加了很多實用方法。
例如:以前簡單的拷貝一個文件就必須要寫一大堆的代碼,現在實用
java.nio.file.Files.copy(Path, Path, CopyOption...)
就可以很輕松的做到了
NIO和NIO.2的區別在哪里
在上一節中已經簡單介紹了這兩個概念的不同,這里再簡單羅列一下。NIO中引入的一個重要概念就是Reactor模式,而NIO.2對NIO本身不是一次升級,而是一次擴充,NIO.2中新增了很多實用方法(utilities),以支持更多的功能需求,并不是說能夠提升多少的性能。主要增加了如下兩點:
- 新的訪問文件的API。
訪問文件從簡單到復雜的方法
在Java.nio.file
包和其子包中新增了大量的與訪問文件相關的類,其中比較重要的有以下幾個,更完整的更新可以在Oracle的官網文檔中查看。
-
java.nio.file.Path
,它可以用來取代早期的java.io.File
用來訪問文件。 -
java.nio.file.Files
,其中包含了大量的對文件操作的API。
-
異步I/O的API
在NIO原來的API的基礎上,增加了對Proactor模式的支持,可以在包java.nio.channels
中看到新加入的java.nio.channels.AsynchronousChannel
和java.nio.channels.CompletionHandler<V, A>
。使用這些類可以實現異步編程,如代碼1中所示://代碼1 //定義一個處理文件內容的函數式接口 @FunctionalInterface static interface ProcessBuffer{ void process(int result, ByteBuffer bb); } //遞歸地讀取文件的全部內容 static void readFileThrough(AsynchronousFileChannel ch, ProcessBuffer runn, int position) { ByteBuffer bb = ByteBuffer.allocate(512); ch.read(bb, position, null, new CompletionHandler<Integer, Object>() { @Override public void completed(Integer result, Object attachment) { System.out.println("成功了"); bb.flip(); runn.process(result, bb); bb.clear(); if (result == bb.capacity()) readFileThrough(ch, runn, position + result); } @Override public void failed(Throwable exc, Object attachment) { System.err.println("失敗了!!!"); } }); } //讀取文件內容,并打印 static void testAIOReadFile() throws IOException { Path p = Paths.get(fileDir, fileName); AsynchronousFileChannel channel = AsynchronousFileChannel.open(p, StandardOpenOption.READ); Thread daemon = new Thread(() -> { try { System.out.println("守護"); Thread.sleep(10000); } catch (Exception e) { } }); readFileThrough(channel, (result, bb) -> { if (result < bb.capacity()) { System.out.println(new String(Arrays.copyOf(bb.array(), result))); System.out.println("已讀完。。。"); daemon.interrupt(); }else { System.out.print(new String(bb.array())); } }, 0); daemon.start(); }
NIO中的主要類的介紹
NIO的基本思想是要構建一個Reactor模式的實現,具體落實到API,在Java中主要有以下幾個類:
1. java.nio.Buffer
這是一個容器類,用來存儲「基礎數據類型」,所有從Channel中讀取出來的數據都要使用Buffer的子類來作為存儲單元,可以把它想象成一個帶著很多屬性的數組(和ArrayList很類似,其實它的實現機制也差不多就是這樣)。
第一次看到介紹Buffer是在一本書上,書上畫了好多方框和指向這些方框的屬性值,看著就頭暈。其實很簡單,Buffer就是一個數組。
在讀寫交換時,必不可少的要批量地去讀取并寫入到目標對象,這個道理是不變的。在基礎I/O中如果我們要把一個輸入流寫入一個輸出流,可能會這么做:
//代碼2
public static void copy(File src, File dest) throws IOException {
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest);
byte[] buffer = new byte[1024];
int bytes = 0;
while ((bytes = in.read(buffer)) > -1){
out.write(buffer, 0, bytes);
}
out.close();
in.close();
}
以上代碼中使用了一個真實的數組用來做讀寫切換,從而達到批量(緩沖)讀寫的目標。
而在NIO中(如代碼1),讀寫切換也同樣是使用了一個數組進行暫存(緩沖),只不過在這個數組之上,封裝了一些屬性(java.nio.Buffer
源碼中的一些屬性如代碼3所示)和操作。
//代碼3 - Buffer類中定義的一些屬性
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
關于Buffer類詳細的繼承關系和其主要方法,可以參考下圖:
2. java.nio.channels.Channel
Channel可以看做是代碼2中InputStream和OutStream的合體,在實際使用中,我們往往針對同一個I/O設備同時存在讀和寫的操作,在基礎I/O中我們就需要針對同一個目標對象生成一個輸入流和輸出流的對象,可是在NIO中就可以只建立一個Channel對象了。
Channel抽象的概念是對于某個I/O設備的「連接」,可以使用這個連接進行一些I/O操作,java.nio.channels.Channel
本身是一個接口,只有兩個方法,但是在Java的的環境中,往往最簡單的接口最煩人,因為它的實現類總是會異常的多。
//代碼4 - 去除了所有注釋的Channel類
package java.nio.channels;
import java.io.IOException;
import java.io.Closeable;
public interface Channel extends Closeable {
public boolean isOpen();
public void close() throws IOException;
}
當然,這是享受多態帶來的好處的同時必須承受的。詳細的Channel繼承和實現關系如下:
3. java.nio.channels.Selector
如果你是使用NIO來做網絡I/O,Selector是JavaNIO中最重要的類,正如它的注釋里第一句說的,Selector是SelectableChannel的「多路復用器」。
多路復用,這是在上一篇介紹過的概念,在不同的操作系統也有不同的底層實現。用戶也可以自己實現自己的Selector(通過類
java.nio.channels.spi.SelectorProvider
)
//代碼5 - provider構造方法
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
if (loadProviderFromProperty())
//如果設置了屬性java.nio.channels.spi.SelectorProvider,則會載入響應的類
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
如果你不實現自己的SelectorProvidor,在代碼5中可以看到JDK會使用類sun.nio.ch.DefaultSelectorProvider
來創建,這里會根據你的操作系統的類別不同而選擇不同的實現類。openJDK中也有相應的實現,有興趣的可以去GrepCode查看一下,Mac OS下是使用KQueueSelectorProvider
。
Selector的使用比較簡單,同時要配合SelectionKey使用,它們的繼承結構圖也比較簡單,如下:
4. 其他
其他一些類如Charset個人感覺屬于實用性很強的類,但是在NIO與基礎I/O的比較中就顯得不那么重要了。
使用NIO的API構建一個Socket服務器
Java1.4引入的NIO中已經可以實現Reactor模式,在NIO.2中又引入了AIO的API,所以本節將分別使用兩種模式來實現一個Socket服務器,這里重點介紹Java中NIO API的使用,至于NIO和基礎I/O的性能對比,網上有很多,這里就不再做比較了。
首先定義一些基礎類,將從Socket中獲取的數據解析成TestRequest對象,然后再找到響應的Handler。看代碼:
我這里為了偷懶,將很多基礎類和方法定義在了一個類中,這種方法其實十分不可取。
//代碼6
/**
* 執行計算工作的線程池
*/
private static ExecutorService workers = Executors.newFixedThreadPool(10);
/**
* 解析出來的請求對象
* @author lk
*
*/
public static class TestRequest{
/**
* 根據解析到的method來獲取響應的Handler
*/
String method;
String args;
public static TestRequest parseFromString(String req) {
System.out.println("收到請求:" + req);
TestRequest request = new TestRequest();
request.method = req.substring(0, 512);
request.args = req.substring(512, req.length());
return request;
}
}
/**
* 具體的邏輯需要實現此接口
* @author lk
*
*/
public static interface SockerServerHandler {
ByteBuffer handle(TestRequest req);
}
主要的邏輯其實就是使用ServerSocketChannel
的實例監聽本地端口,并且設置其為非阻塞(默認為阻塞模式)。代碼7中的parse()
函數是一個典型的「使用Buffer讀取Channel中數據」的方法,這里為了簡(tou)單(lan),默認只讀取1024個字節,所以并沒有實際去循環讀取。
//代碼7
private static void useNIO() {
Selector dispatcher = null;
ServerSocketChannel serverChannel = null;
try {
dispatcher = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().setReuseAddress(true);
serverChannel.socket().bind(LOCAL_8080);
//ServerSocketChannel只支持這一種key,因為server端的socket只能去accept
serverChannel.register(dispatcher, SelectionKey.OP_ACCEPT);
while (dispatcher.select() > 0) {
operate(dispatcher);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 在分發器上循環獲取連接事件
* @param dispatcher
* @throws IOException
*/
private static void operate(Selector dispatcher) throws IOException {
//Set<SelectionKey> keys = dispatcher.keys();
Set<SelectionKey> keys = dispatcher.selectedKeys();
Iterator<SelectionKey> ki = keys.iterator();
while(ki.hasNext()) {
SelectionKey key = ki.next();
ki.remove();
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//針對此socket的IO就是BIO了
final SocketChannel socket = channel.accept();
workers.submit(() -> {
try {
TestRequest request = TestRequest.parseFromString(parse(socket));
SockerServerHandler handler = (SockerServerHandler) Class.forName(getClassNameForMethod(request.method)).newInstance();
socket.write(handler.handle(request));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
}
}
private static String parse(SocketChannel socket) throws IOException {
String req = null;
try {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
byte[] bytes;
int count = 0;
if ((count = socket.read(buffer)) >= 0) {
buffer.flip();
bytes = new byte[count];
buffer.get(bytes);
req = new String(bytes, Charset.forName("utf-8"));
buffer.clear();
}
} finally {
socket.socket().shutdownInput();
}
return req;
}
Java的程序有個通病,寫出來的程序又臭又長,同樣是使用JavaNIO的API實現一個非阻塞的Socket服務器,使用NIO.2中AIO(異步I/O)的API就很簡單了,但是卻陷入了回調地獄(當然可以通過別的方式避免回調,但是其本質還是一樣的)。和上邊介紹的Reactor模式相比,簡直就是拿核武器比步槍,有點降維攻擊的意味了。Reactor中那么復雜的概念和邏輯所實現的功能,使用AIO的API很輕松就搞定了,而且概念比較少,邏輯更清晰。
//代碼8
private static void useAIO() {
AsynchronousServerSocketChannel server;
try {
server = AsynchronousServerSocketChannel.open();
server.bind(LOCAL_8080);
while (true) {
Future<AsynchronousSocketChannel> socketF = server.accept();
try {
final AsynchronousSocketChannel socket = socketF.get();
workers.submit(() -> {
try {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
socket.read(buffer, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer count, Object attachment) {
byte[] bytes;
if (count >= 0) {
buffer.flip();
bytes = new byte[count];
buffer.get(bytes);
String req = new String(bytes, Charset.forName("utf-8"));
TestRequest request = TestRequest.parseFromString(req);
try {
SockerServerHandler handler = (SockerServerHandler) Class.forName(getClassNameForMethod(request.method)).newInstance();
ByteBuffer bb = handler.handle(request);
socket.write(bb, null, null);
} catch (InstantiationException | IllegalAccessException
| ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
buffer.clear();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
// TODO Auto-generated method stub
}
});
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
}
});
} catch (InterruptedException | ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
最后是測試用的客戶端程序,NIO在客戶端同樣也可以發揮很重要的作用,這里就先略過了,代碼9中客戶端測試使用的是基礎I/O:
//代碼9
private volatile static int succ = 0;
public static void main(String[] args) throws UnknownHostException, IOException {
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
new Thread( () -> {
Socket soc;
try {
soc = new Socket("localhost", 8080);
if (soc.isConnected()) {
OutputStream out = soc.getOutputStream();
byte[] req = "hello".getBytes("utf-8");
out.write(Arrays.copyOf(req, 1024));
InputStream in = soc.getInputStream();
byte[] resp = new byte[1024];
in.read(resp, 0, 1024);
String result = new String(resp, "utf-8");
if (result.equals("haha")) {
succ++;
}
System.out.println(Thread.currentThread().getName() + "收到回復:" + result);
out.flush();
out.close();
in.close();
soc.close();
}
try {
System.out.println(Thread.currentThread().getName() + "去睡覺等待。。。");
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
latch.countDown();
}
Runnable hook = () -> {
System.out.println("成功個數:" + succ);
};
Runtime.getRuntime().addShutdownHook(new Thread(hook));
}
總結
原本只是想寫一篇Netty在RPC框架中的使用,寫著寫著就寫多了。本文從Java中引入NIO的歷史講起,梳理了Java對NIO支持的具體的API,最后通過一個典型的Socket服務器的例子具體的展示了Java中NIO相關API的使用,將Reactor模式和Proactor模式從理論落地到實際的代碼。
由于作者比較懶,貼圖全部都是在網上找的(代碼大部分是自己寫的),如侵刪。下一篇將講到比較火的一個NIO框架Netty的實現與使用。