NIO 簡介
JDK1.4中引入了新的Java I/O類,在package java.nio.*中,目的是提高速度。
NIO一開始是"New Input/Output"的縮寫。不過,已經(jīng)過了那么長時(shí)間了,已經(jīng)不再"New"了。目前,普遍認(rèn)可的觀念是,NIO是"No-Blocking Input/Output"的縮寫。
NIO的核心是什么?
Channel, Buffer, Selector組成了NIO的核心API。
三者的協(xié)作關(guān)系是:
Channel如同煤礦,存儲(chǔ)著資源(在程序中就是數(shù)據(jù))
Buffer如同運(yùn)煤的卡車(即緩存)
Selector如同一個(gè)調(diào)度中心
怎么理解三者關(guān)系?
我們假設(shè)挖出來的煤最小運(yùn)輸單位是“框”,NIO出現(xiàn)之前的IO是每挖出一“框”煤,就運(yùn)輸一次。很顯然,這樣很耗費(fèi)資源,效率很低。NIO的做法是每挖出一“框”煤,先放到卡車(即Buffer)中,卡車滿了才統(tǒng)一運(yùn)送一次,這樣效率就提高了。
一般情況下,會(huì)有很多煤礦在同時(shí)挖煤。在主干道(線程)只有一個(gè)的情況下,我們不希望某個(gè)煤礦在不需要運(yùn)輸?shù)臅r(shí)候占用主干道(阻塞的IO會(huì)一直占用線程,即主干道)。這時(shí),需要所有的煤礦(Channel)都到Selector處注冊(cè)。Selector會(huì)挨個(gè)詢問所有的煤礦(Channel),有沒有煤要運(yùn)輸,如果有,則允許使用主干道運(yùn)輸。
可見,Channel總是跟Buffer打交道。要read的數(shù)據(jù)從Buffer中讀取,要write的數(shù)據(jù)先寫入到Buffer中。而Selector則監(jiān)控著所有的Channel。

Channel
簡介
NIO中的所有IO操作要從Channel開始。Channel有點(diǎn)像BIO中的Stream(即“流”),但是又有點(diǎn)區(qū)別:
- Stream是單向的,只能讀或者只能寫。Channel是雙向的。
- Stream是阻塞的,Channel可以是阻塞的,也可以是非阻塞的。
- Stream中的數(shù)據(jù)可以選擇性的讀入到Buffer中,但是Channel中的數(shù)據(jù)必須先讀入到Buffer中。
Channel接口只有兩個(gè)方法
public interface Channel extends Closeable {
//Channel是否打開
public boolean isOpen();
//關(guān)閉Channel
public void close() throws IOException;
}
常見Channel
- FileChannel - 文件IO
- DatagramChannel - UDP
- ServerSocketChannel - TCP Server
- SocketChannel - TCP Client
實(shí)際上,Channel大致可以分為兩類:
- 負(fù)責(zé)文件讀寫的
FileChannel - 負(fù)責(zé)網(wǎng)絡(luò)讀寫的
SelectableChannel
SelectableChannel的常見實(shí)現(xiàn)類有:
DatagramChannelServerSocketChannelSocketChannel
其中DatagramChannel用來進(jìn)行UDP通信,ServerSocketChannel和SocketChannel分別用在TCP通信的Server端和Client端。
FileChannel
FileChannel的繼承關(guān)系:

FileChannel的底層實(shí)現(xiàn)參見深入淺出NIO Channel和Buffer
FileChannel的典型用法示例:
//打開一個(gè)文件
FileOutputStream aFile = new FileOutputStream("data/nio-data.txt", "rw");
//獲取FileChannel
FileChannel inChannel = aFile.getChannel();
//讀取數(shù)據(jù)到ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
//開始寫入數(shù)據(jù)
//準(zhǔn)備數(shù)據(jù)
String newData = "New String to write to file";
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
//向文件中寫入數(shù)據(jù)
while(buf.hasRemaining()) {
channel.write(buf);
}
//關(guān)閉FileCHannel
channel.close();
FileChannel的其他使用詳情請(qǐng)參見Java NIO系列教程(七) FileChannel
ServerSocketChannel
首先看類ServerSocketChannel中的成員:

從中可以發(fā)現(xiàn),ServerSocketChannel并沒有read和write方法。也就是說ServerSocketChannel不負(fù)責(zé)數(shù)據(jù)讀寫。
accept()方法返回一個(gè)SocketChannel類型,根據(jù)經(jīng)驗(yàn)我們猜測,SocketChannel類才是真正負(fù)責(zé)數(shù)據(jù)讀寫的類。這個(gè)我們會(huì)在后面驗(yàn)證。
ServerSocketChannel的繼承關(guān)系:

ServerSocketChannel的創(chuàng)建是通過靜態(tài)方法open():
ServerSocketChannel srvChannel = ServerSocketChannel.open();
SocketChannel
類成員:

可以看出,SocketChannel中有read和write方法,很顯然,能夠執(zhí)行數(shù)據(jù)的讀寫操作。
通過分析其繼承關(guān)系(如下圖)發(fā)現(xiàn),

SocketChannel實(shí)現(xiàn)了ReadableByteChannel接口和WritableByteChannel接口。從名稱上就能看出,這兩個(gè)接口分別負(fù)責(zé)數(shù)據(jù)的讀和寫。因此,SocketChannel會(huì)負(fù)責(zé)數(shù)據(jù)從網(wǎng)絡(luò)中讀取和寫入到網(wǎng)絡(luò)中的功能。
SocketChannel的創(chuàng)建是通過靜態(tài)方法open():
SocketChannel srvChannel = SocketChannel.open();
DatagramChannel

DatagramChannel典型使用示例
int port = 8080;
//打開channel
DatagramChannel channel = DatagramChannel.open();
//綁定本地地址
channel.socket().bind(new InetSocketAddress(port));
//準(zhǔn)備接收數(shù)據(jù)到ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
//準(zhǔn)備發(fā)送數(shù)據(jù)
String newData = "New String to write to file";
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
//發(fā)送數(shù)據(jù)
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
關(guān)于connect,由于UDP是無連接的,連接到特定地址并不會(huì)像TCP通道那樣創(chuàng)建一個(gè)真正的連接。而是鎖住DatagramChannel,讓其只能從特定地址收發(fā)數(shù)據(jù)。
channel.connect(new InetSocketAddress("jenkov.com", 80));
Buffer
簡介
Buffer是NIO和BIO的一個(gè)重要區(qū)別。
BIO是面向Stream的,可以將數(shù)據(jù)直接寫入或者讀出到Stream中
NIO是面向Buffer的,所有數(shù)據(jù)的讀取都需要經(jīng)過Buffer。
《Thinking in Java》中是這么描述的:
我們可以將NIO想象成一個(gè)煤礦,Channel是包含煤(即數(shù)據(jù))的庫礦藏,Buffer則是運(yùn)送礦藏的卡車。我們并沒有直接和Channel打交道,我們只是和Buffer交互,并把Buffer派送到Channel。
Buffer本質(zhì)上是一個(gè)數(shù)組。很顯然,它不可能僅僅是個(gè)數(shù)組,還提供了對(duì)數(shù)據(jù)的結(jié)構(gòu)化訪問,以及維護(hù)讀寫位置信息。這些額外的功能是通過Buffer中的幾個(gè)變量來輔助實(shí)現(xiàn)的:
- capacity:緩存數(shù)組大小
- position:初始值為0。position表示當(dāng)前可以寫入或讀取數(shù)據(jù)的位置。當(dāng)寫入或讀取一個(gè)數(shù)據(jù)后, position向前移動(dòng)到下一個(gè)位置。
-
limit:
- 寫模式下,limit表示最多能往Buffer里寫多少數(shù)據(jù),等于capacity值。
- 讀模式下,limit表示最多可以讀取多少數(shù)據(jù)。
- mark:初始值為-1,用于備份當(dāng)前的position
Buffer上述部分成員移動(dòng)示意圖如下:

原理
Buffer是個(gè)抽象類,只定義了數(shù)據(jù)緩存的部分功能和接口,并不負(fù)責(zé)實(shí)際的數(shù)據(jù)存儲(chǔ)。實(shí)際數(shù)據(jù)存儲(chǔ)在其派生類中實(shí)現(xiàn):

數(shù)據(jù)在不同的派生類中是怎么存儲(chǔ)的?
經(jīng)過源碼得知,每個(gè)派生類中都有一個(gè)數(shù)組,數(shù)組類型與派生類對(duì)應(yīng)。如ByteBuffer中有byte[] hb;數(shù)組,CharBuffer中有char[] hb;數(shù)組,DoubleBuffer中有double[] hb數(shù)組。
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{
final byte[] hb; // Non-null only for heap buffers
}
public abstract class CharBuffer extends Buffer{
final char[] hb; // Non-null only for heap buffers
}
使用
如何讀數(shù)據(jù)?
對(duì)于只讀操作,必須顯示地使用靜態(tài)allocate()方法來分配ByteBuffer。
代碼示例:
//sc是SocketChannel的一個(gè)實(shí)例
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(buffer);
buffer.flip();
注意:調(diào)用完成read()方法后,須要調(diào)用Buffer的flip()方法。這是為何?
剛才有講Buffer中的position變量會(huì)在read()調(diào)用的時(shí)候向下移動(dòng)。但是write或者復(fù)制數(shù)據(jù)的時(shí)候,選取的數(shù)據(jù)是position和limit之間的數(shù)據(jù)。這時(shí)就需要將position賦值給limit,同時(shí)position重置為0。flip()方法就是做這件事的:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
如何寫數(shù)據(jù)?
寫數(shù)據(jù)時(shí),首先需要通過Buffer派生類中的put()方法放入數(shù)據(jù)。
代碼示例:
String response = "Hello World";
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(response.getBytes());
buffer.flip();
channel.write(buffer);
注意:這里調(diào)用put()方法后也須調(diào)用flip()方法,原理同上。
clear()方法
clear()方法能對(duì)緩沖區(qū)中的內(nèi)部指針重排,從而復(fù)用Buffer。需要復(fù)用時(shí),須調(diào)用。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
get()方法
get()方法存在于部分派生類中,如ByteBuffer。目的是數(shù)據(jù)的復(fù)制。
//sc是SocketChannel的一個(gè)實(shí)例
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(buffer);
buffer.flip();
byte[] bytes = new byte(buffer.remaining());
//將數(shù)據(jù)復(fù)制到bytes中
buffer.get(bytes);
Selector
簡介
Selector是NIO的核心。
我們知道,在阻塞IO中,等待數(shù)據(jù)的時(shí)間相對(duì)于實(shí)際數(shù)據(jù)操作的時(shí)間是非常非常長的。如下圖所示:

阻塞IO中,大部分時(shí)間沒有被利用起來,白白占用著線程寶貴的資源。Selector的思想就是去除這些無用的等待。
Java Selector借鑒了Linux中的select/poll/epoll模型。其特點(diǎn)如下圖所示:

Selector維護(hù)了一個(gè)數(shù)組,數(shù)組中元素是跟Channel對(duì)應(yīng)的封裝類型SelectionKey。使用時(shí),需要不斷遍歷數(shù)組,如果其中某個(gè)或者某幾個(gè)Key有數(shù)據(jù)讀寫的需求,會(huì)在遍歷的時(shí)候被檢測到,然后進(jìn)行實(shí)際的數(shù)據(jù)讀寫操作。這樣一來,等待數(shù)據(jù)的時(shí)間就被去除了。
使用
創(chuàng)建Selector
Selector通過靜態(tài)函數(shù)open()創(chuàng)建,JDK注釋為:
Opens a selector.
The new selector is created by invoking the SelectorProvider.provider().openSelector() method
代碼示例:
Selector selector = Selector.open();
遍歷Selector
Selector遍歷代碼示例:
Set selectionKeys = selector.selectedKeys();
Iterator it = selectionKeys.iterator();
while(it.hasNext()){
SelectionKey key = (SelectionKey)it.next();
ServerSocketChannel channel = (ServerSocketChannel)key.channel();
或
SocketChannel channel = (SocketChannel)key.channel();
}
Channel加入到Selector的數(shù)組中?
ServerSocketChannel和SocketChannel中有register()函數(shù),可注冊(cè)到Selector的數(shù)組中:
public final SelectionKey register(Selector sel, int ops);
Selector sel: Selector的一個(gè)對(duì)象
int ops: 可取值有:
- OP_READ: 表示當(dāng)有數(shù)據(jù)要讀時(shí),激活Channel
- OP_WRITE: 表示當(dāng)有數(shù)據(jù)要寫時(shí),激活Channel
- OP_CONNECT: 表示連接到了Server時(shí),激活Channel
- OP_ACCEPT: 表示有Client請(qǐng)求連接時(shí),激活Channel
代碼示例:
SocketChannel sc = (ServerSocketChannel)serverSocketChannel.accept();
sc.register(selector, SelectionKey.OP_READ);
select/poll還是epoll
linux操作系統(tǒng)方面多路復(fù)用技術(shù)有三種常用的機(jī)制:select、poll和epoll。
三者的介紹在這里select/poll/epoll...
epoll無輪詢,使用callback機(jī)制,比select/poll的效率要高。但是使用時(shí),究竟是使用的epoll還是select/poll?這個(gè)是跟操作系統(tǒng)相關(guān)的。
一般來說,select有最大fd限制,默認(rèn)1024,很小被使用。常用的是poll和epoll,因此我們可暫不考慮select。
究竟使用poll還是epoll,是由sun.nio.ch.DefaultSelectorProvider類中的create()函數(shù)定義的。
Java NIO根據(jù)操作系統(tǒng)不同, 針對(duì)nio中的Selector有不同的實(shí)現(xiàn)
所以毋須特別指定, Oracle jdk會(huì)自動(dòng)選擇合適的Selector。
如果想設(shè)置特定的Selector,可以屬性:
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
Linux
Linux 在2.6之后才支持epoll。在create()函數(shù)中,檢測了Linux內(nèi)核的版本,只有不小于2.6的時(shí)候才使用epoll,即EPollSelectorProvider,否則使用poll,即PollSelectorProvider或DevPollSelectorProvider
public static SelectorProvider create() {
String osname = AccessController.doPrivileged(
new GetPropertyAction("os.name"));
if ("SunOS".equals(osname)) {
return new sun.nio.ch.DevPollSelectorProvider();
}
// use EPollSelectorProvider for Linux kernels >= 2.6
if ("Linux".equals(osname)) {
String osversion = AccessController.doPrivileged(
new GetPropertyAction("os.version"));
String[] vers = osversion.split("\\.", 0);
if (vers.length >= 2) {
try {
int major = Integer.parseInt(vers[0]);
int minor = Integer.parseInt(vers[1]);
if (major > 2 || (major == 2 && minor >= 6)) {
return new sun.nio.ch.EPollSelectorProvider();
}
} catch (NumberFormatException x) {
// format not recognized
}
}
}
return new sun.nio.ch.PollSelectorProvider();
}
MAC
MAC中epoll是使用其替代品kqueue,即KQueueSelectorProvider
public static SelectorProvider create() {
return new KQueueSelectorProvider();
}
Windows
Windows不支持epoll,因此只能使用poll
NIO編程示例
Server端示例
Server端序列圖(出自《Netty權(quán)威指南》)

Selector selector = Selector.open();
//創(chuàng)建服務(wù)端接收Channel
ServerSocketChannel servChannel = ServerSocketChannel.open();
//設(shè)置成非阻塞的
servChannel.configureBlocking(false);
int port = 8080;
//綁定地址
servChannel.socket().bind(new InetSocketAddress(port), 1024);
//將servChannel注冊(cè)到selector
servChannel.register(selector, SelectionKey.OP_ACCETP);
while(true){
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey = key = null;
while(it.hasNext()){
key = it.next();
if(key.isValid()){
//如果服務(wù)端接收Channel就緒,則開始accept請(qǐng)求
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//將新加入的channel注冊(cè)到selector
sc.registrer(selector, SelectionKey.OP_READ);
}
//如果數(shù)據(jù)channel可讀
if(key.isReadable()){
//開始讀
SocketChannel sc = (SocketChannel)key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);//將數(shù)據(jù)讀取到緩沖區(qū)readBuffer。
if(readBytes > 0){
readBuffer.flip();
}
}
}
}
}
Client端示例
Client端序列圖(出自《Netty權(quán)威指南》)

SocketChannel client = SocketChannel.open();
client.connet(new InetSocketAddress(host, port));
client.register(selector, SelectionKey.OP_READ);
//Write data -- 略
Selector selector = Selector.open();
while(true){
selector.select();
//遍歷selector -- 同Server,略
}
NIO框架
大多數(shù)情況下,不建議直接使用JDK NIO類庫,而是使用一些已有的NIO框架。
為什么不使用原生的NIO編程?
- NIO類庫API繁多,都需要熟練掌握
- 需要其他額外的技能,如多線程技術(shù)
- 需要解決各種可靠性問題,如斷線重連、半包讀寫、網(wǎng)絡(luò)擁塞等
- JDK NIO bug。如epoll bug,會(huì)導(dǎo)致Selector空輪訓(xùn),導(dǎo)致CPU 100%。
常見NIO框架
- Netty
- Vert.x
- Xnio
- Grizzly
- Apache Mina
NIO框架不少,Netty是其中的佼佼者!Netty在工程中被廣泛應(yīng)用,其中包含大型公司如Apple,F(xiàn)acebook,Google,Instagram等。Netty介紹請(qǐng)見Netty...
引申
NIO編程困難
epoll bug
網(wǎng)絡(luò)可靠性問題
- TCP半包/粘包問題