本文大綱如下:
- 1、什么是NIO
- 2、為什么使用NIO
- 3藏斩、NIO的基本使用
- 4嫂便、BIO、NIO丢烘、AIO區(qū)別以及總結(jié)
一、什么是NIO
NIO是JDK1.4新加入的,為了解決BIO(原始IO的不足)的非阻塞IO模型导披。原始的IO流是單向的,如InputStream只能讀取不能寫入埃唯,而NIO是面向通道(Channel)和緩沖區(qū)(Buffer)撩匕,NIO可以輸入也可以輸出。
二墨叛、為什么使用NIO
探討NIO之前止毕,肯定先說明BIO的不足模蜡,只有在BIO滿足不了需求的時候,才會出現(xiàn)新的技術(shù)代替扁凛。如下圖所示忍疾,因為BIO是一個阻塞的IO模型,所以需要為每個IO配備一個線程去監(jiān)聽調(diào)度谨朝,如果在高并發(fā)的情況下卤妒,開辟新的線程以及切換線程會帶來巨大的損耗。但是對于比較簡單的文件讀寫字币,還是使用BIO则披。
NIO的出現(xiàn)解決了BIO的低效率不能高并發(fā)的問題,所以NIO不是阻塞的IO模型洗出。他只需要調(diào)用一個線程去輪詢Selector士复,是否有新的事件出現(xiàn)即可。再通過事件找到具體的Channel翩活,這樣就可以將N的線程降低為一個線程了阱洪。如下圖所示,只需要單個線程監(jiān)聽Selector隅茎,當對應(yīng)的Channel有消息到達時候澄峰,再調(diào)用工作線程去處理,這樣可以減少了數(shù)據(jù)未到達之前的等待時間辟犀。
無論是輸入還是輸出俏竞,NIO中Channel和Buffer是配對使用的。在輸入(Input)時堂竟,Channel的數(shù)據(jù)讀取到Buffer中魂毁,使用者再從Buffer中讀取數(shù)據(jù)。輸出反之出嘹。調(diào)度者直接調(diào)度的只能是Buffer席楚。
三、NIO的基本用法
1)税稼、Channel烦秩、Buffer、Selector組件的介紹
① 郎仆、Channel通道
Channel通道是 I/O 傳輸發(fā)生時通過的入口只祠。當Buffer數(shù)據(jù)寫入到Channel時,他就開始傳輸了扰肌。反之當Channel接收到數(shù)據(jù)時抛寝,Buffer就可以讀取了。Channel是真正開始數(shù)據(jù)傳輸?shù)摹?/p>
對應(yīng)的Channel主要有:
- FileChannel 讀寫文件
- DatagramChannel 讀寫UDP數(shù)據(jù)
- SocketChannel 讀寫TCP數(shù)據(jù)
- ServerSocketChannel 監(jiān)聽TCP鏈接,并為服務(wù)端創(chuàng)建一個SocketChannel
②盗舰、Buffer緩沖區(qū)
Buffer即緩沖區(qū)晶府。在NIO模式下,數(shù)據(jù)并非立刻到達的钻趋,在數(shù)據(jù)沒到達之前都是阻塞的川陆,所以在NIO中才引入緩沖區(qū)概念,數(shù)據(jù)到達時先寫入到Buffer中爷绘,再交給調(diào)用者處理緩沖區(qū)书劝。這樣處理者線程就不需要阻塞了进倍。
對應(yīng)的Buffer有:
- ByteBuffer 以字節(jié)為單位
- CharBuffer 以Char為單位
- DoubleBuffer 以Double為單位
- FloatBuffer 以Float為單位
- IntBuffer 以Int為單位
- LongBuffer 以Long為單位
- ShortBuffer 以Short為單位
Buffer的屬性:
- private int mark = -1;
- private int position = 0;
- private int limit;
- private int capacity;
Buffer中共有mask土至、position、limit猾昆、capacity屬性陶因,其中主要的是position、limit垂蜗、capacity楷扬。
capacity: capacity是容器的大小,即緩沖區(qū)的大小贴见。
limit : capacity意味著緩沖區(qū)的大小烘苹,但并不是所有的緩沖區(qū),使用者都能用的上片部,使用者可以根據(jù)需要镣衡,劃分出自己所需要的大小,但不能大于了capacity档悠,這就是limit的意義廊鸥。寫的模式下,limit代表了辖所,最多可以寫多少惰说。讀模式下,意味著最多可以讀到多少缘回。當從寫模式切換至讀模式吆视,即調(diào)用flip()函數(shù)后,limit=position酥宴,position=0啦吧;這樣就可以開始讀buffer了。
position:在寫的模式下幅虑,position為0丰滑,position代表下一個寫入的位置,所以position最多不能大于limit-1;在讀模式下褒墨,position代表的是下一個讀取的位置炫刷,大小同樣不能大于limit-1。在初始狀態(tài)下或者調(diào)用flip函數(shù)時郁妈,position會重新賦值等于0浑玛;
mark: mark從字面意思就是標記的意思,它就是當前position的一個快照噩咪。通過reset函數(shù)會讓position重新賦值為mark顾彰。
Buffer中核心API:
Buffer既然是個內(nèi)存,它的API就是對該內(nèi)存位置的管理標記胃碾,即對屬性管理涨享。下面只介紹部分方法
- clear() 當該buffer中的數(shù)據(jù)處理完的時候,就可以調(diào)用該方法仆百,他會使buffer變成初始狀態(tài)厕隧。
- compact() 于clear一樣,compact()也是清除的作用俄周,但是他只清除讀取完的數(shù)據(jù)吁讨,把未讀取的數(shù)據(jù)copy到首部,移動position和limit
- hasRemaining() 代表是否有下一個位置可以處理峦朗。讀時是不是讀完了建丧,寫時為是不是寫完了。通常是在這操作之前判斷波势。
③翎朱、Selector選擇器
當應(yīng)用中有大量的Channel的時候,即IO連接時艰亮。還得有大量的線程去監(jiān)聽數(shù)據(jù)狀態(tài)闭翩。為此NIO加入了Selector,由Channel向Selector注冊迄埃,并支持一個Selector可以注冊多個Channel疗韵。將監(jiān)聽 Channel換成監(jiān)聽Selector,從而減少線程使用侄非。
由于Channel可以進行讀或者寫蕉汪,所以向Selector注冊時,需要表明是監(jiān)聽哪一種事件逞怨。最終輪詢Selector即可知道哪種事件到達了者疤。
Selector事件如下:
- OP_ACCEPT TCP服務(wù)端接收到客戶端連接事件
- OP_CONNECT 該連接是否已經(jīng)斷開事件
- OP_READ 可以讀取該數(shù)據(jù)事件
- OP_WRITE 可以寫入數(shù)據(jù)事件
2)、NIO基本用法
① 叠赦、FileChannel
FileChannel是讀取文件時的Channel
讀取文件例子如下:
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile("D:\\proguard-rules.pro", "rw");
FileChannel channel = file.getChannel();
// 分配緩存空間
ByteBuffer buf = ByteBuffer.allocate(48);
// buf讀取通道的數(shù)據(jù) length為讀到的數(shù)據(jù)長度
int length = channel.read(buf);
byte[] bytes = new byte[48];
while (length != -1) {
// buf由寫的狀態(tài) 切換至讀狀態(tài)
buf.flip();
while(buf.hasRemaining()){
buf.get(bytes,0,length);
System.out.print(new String(bytes));
}
// 清空buf數(shù)據(jù)
buf.clear();
length = channel.read(buf);
}
file.close();
}
上訴代碼只是簡單的讀取文件驹马,雖然簡單,但包含了NIO的基本用法流程。生產(chǎn)Buffer對象有兩種方法:
- ByteBuffer buf = ByteBuffer.allocate(48);
- ByteBuffer buf = ByteBuffer.wrap(bytes,0,length);
transferTo(long position, long count,
WritableByteChannel target)
現(xiàn)代的操作系統(tǒng)分為內(nèi)核態(tài)和用戶態(tài)糯累,文件的copy是從內(nèi)核態(tài) ->用戶態(tài) ->內(nèi)核態(tài)算利,它需要經(jīng)過用戶態(tài)才能copy。FileChannel中的拷貝transferTo(transferTo類似)泳姐,直接從內(nèi)核態(tài)到內(nèi)核態(tài)效拭,效率比較高。
② 胖秒、DatagramChannel
DatagramChannel是接收發(fā)送UDP的缎患,而UDP是面向數(shù)據(jù)包,而非數(shù)據(jù)流的阎肝。
發(fā)送數(shù)據(jù)包:
DatagramChannel channel = DatagramChannel.open();
// 發(fā)送數(shù)據(jù)
String data = "DatagramChannel...";
ByteBuffer writeBuffer = ByteBuffer.allocate(48);
writeBuffer.clear();
writeBuffer.put(data.getBytes());
writeBuffer.flip();
int len = channel.send(writeBuffer, new InetSocketAddress("127.0.0.1", 5520));
System.out.println(len);
接收數(shù)據(jù)包:
DatagramChannel channel = DatagramChannel.open();
// 接收綁定端口
channel.socket().bind(new InetSocketAddress(5521));
ByteBuffer readBuffer = ByteBuffer.allocate(48);
channel.receive(readBuffer);
readBuffer.flip();
byte[] bytes = new byte[48];
while (readBuffer.hasRemaining()) {
readBuffer.get(bytes,0,readBuffer.limit());
System.out.print(new String(bytes));
}
③ 挤渔、ServerSocketChannel
利用ServerSocketChannel監(jiān)聽TCP鏈接
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
// 設(shè)置為非阻塞,沒有連接時也會返回null
server.configureBlocking(false);
// 綁定本地端口
server.socket().bind(new InetSocketAddress(5510));
// 注冊客戶端連接到達監(jiān)聽
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
try {
if (selector.select() == 0) {
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 客戶端到達狀態(tài)
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
// 非阻塞狀態(tài)拿到客戶端連接
SocketChannel socketChannel = serverSocketChannel.accept();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
首先盗痒,生成ServerSocketChannel對象蚂蕴,并綁定本地端口低散。再用ServerSocketChannel向Selector注冊SelectionKey.OP_ACCEPT俯邓。輪詢Selector返回的事件,當有TCP連接時熔号,則生成SocketChannel進行網(wǎng)絡(luò)數(shù)據(jù)傳輸稽鞭。
④ 、SocketChannel
SocketChannel專門就是接收發(fā)送TCP數(shù)據(jù)的引镊,打開連接朦蕴,將buffer中的數(shù)據(jù)寫入到channel或者從channel讀取數(shù)據(jù)到buffer中。舉個簡單的例子弟头。
// 客戶端打開SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 5510));
// 開啟非阻塞模式 異步模式下調(diào)用connect(), read() 和write() 可以立即返回無阻塞
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 連接還在
while (!socketChannel.finishConnect()) {
// 從channel中讀取數(shù)據(jù)
socketChannel.read(byteBuffer);
// buffer切換到讀模式
byteBuffer.flip();
// channel發(fā)送buffer中數(shù)據(jù)
socketChannel.write(byteBuffer);
}
在高并發(fā)的情況下吩抓,需要借助Selector,利用輪詢Selector方式赴恨,找到SocketChannel疹娶,再根據(jù)需求做特定的處理。
Selector selector = Selector.open();
// 客戶端打開SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 5510));
// 開啟非阻塞模式 異步模式下調(diào)用connect(), read() 和write() 可以立即返回無阻塞
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 連接還在
while (true) {
if (selector.select() == 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isValid()) {
// 取消對讀事件的繼續(xù)監(jiān)聽
selectionKey.interestOps(selectionKey.readyOps() & ~SelectionKey.OP_READ);
// 從channel中讀取數(shù)據(jù)
socketChannel.read(byteBuffer);
}
iterator.remove();
}
}
}
channel向Selector注冊是SelectionKey key = channel.register(selector, SelectionKey.OP_READ);結(jié)合上述的介紹可以看出SelectionKey與Channel是一對一的關(guān)系伦连,而Selector和Channel是一對多關(guān)系雨饺。
將SelectionKey作為key,value是處理數(shù)據(jù)的Runnable,注冊的時候惑淳,存入Runnable额港。事件到達時,根據(jù)key取出Runnable歧焦,這樣就避免為每個連接創(chuàng)建線程執(zhí)行了移斩。
四、BIO、NIO向瓷、AIO區(qū)別
BIO忍宋,同步阻塞的IO模型。當用InputStream去讀取數(shù)據(jù)的時候风罩,必須等到數(shù)據(jù)操作完成才能進行下一步操作糠排,否則會阻塞當前的線程。這種方式比較簡單超升,但短流量高并發(fā)的情況下入宦,會造成效率低下∈易粒基本用于本地文件操作等簡單業(yè)務(wù)乾闰。
NIO,同步非阻塞IO模型盈滴。NIO還不是異步涯肩,它將一個線程操作一次請求,只是有效的減少了線程巢钓。即每一個連接注冊到多路復用器Selector中病苗,輪詢Selector中是否有事件到達,有事件就代表一次請求到達症汹,再用一個線程處理該請求硫朦。相比較BIO,它只是將多個線程去等待數(shù)據(jù)到達減低為一個線程去等待背镇。多用于低流量多連接的場景咬展,如聊天服務(wù)器。
AIO瞒斩,異步非阻塞IO模型破婆。當進行讀寫操作時,只須直接調(diào)用API的read或write方法胸囱。該方法都是異步的祷舀,方法執(zhí)行完成會回調(diào)回來。從方法簽名中可以看出旺矾,read時候會注入一個CompletionHandler回調(diào)方法蔑鹦,通知該次操作的狀態(tài)。調(diào)用者發(fā)送一個read或者write操作請求箕宙,操作完成后會得到一個通知嚎朽,真正的IO操作由操作系統(tǒng)內(nèi)核進行處理。AIO多用于長連接業(yè)務(wù)柬帕。
public abstract <A> void read(ByteBuffer dst,
long timeout,
TimeUnit unit,
A attachment,
CompletionHandler<Integer,? super A> handler);