概述
BIO是面向字節(jié)流和字符流的辉巡,數(shù)據(jù)從流中順序獲取
NIO是面向通道和緩沖區(qū)的溅固,數(shù)據(jù)總是從通道中讀到buffer緩沖區(qū)內(nèi)募书,或者從buffer緩沖區(qū)內(nèi)寫入通道
Channel通道和Buffer緩沖區(qū)是NIO的核心异雁,幾乎在每一個(gè)IO操作中使用它們华临,Selector選擇器則允許單個(gè)線程操作多個(gè)通道,對(duì)于高并發(fā)多連接很有幫助饥追。
操作系統(tǒng)的IO一般分為兩個(gè)階段图仓,等待和就緒操作,比如讀可以分為等待系統(tǒng)可讀和真正的讀但绕,寫可以分為等待系統(tǒng)可寫和真正的寫救崔,在傳統(tǒng)的BIO中是這兩個(gè)階段都會(huì)阻塞,在NIO中第一個(gè)階段不是阻塞的捏顺,第二個(gè)階段是阻塞的六孵,如下圖,BIO是阻塞IO幅骄,NIO是非阻塞IO
Buffer(緩沖區(qū))
常用Buffer類型
ByteBuffer,MappedByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer劫窒。對(duì)應(yīng)了幾大基本數(shù)據(jù)類型
ByteBuffer可以選擇實(shí)例化為DirectByteBuffer和HeapByteBuffer,如果數(shù)據(jù)量比較小的中小應(yīng)用情況下拆座,可以考慮使用heapBuffer主巍;反之可以用directBuffer。一般來(lái)說(shuō)DirectByteBuffer可以減少一次系統(tǒng)空間到用戶空間的拷貝挪凑。但Buffer創(chuàng)建和銷毀的成本更高孕索,更不宜維護(hù),通常會(huì)用內(nèi)存池來(lái)提高性能躏碳。其他buffer類似
利用Buffer讀寫數(shù)據(jù)步驟
將數(shù)據(jù)寫入buffer
調(diào)用flip將寫模式改為讀模式
從buffer中讀取數(shù)據(jù)搞旭,進(jìn)行操作
調(diào)用clear()清除整個(gè)buffer數(shù)據(jù)或者調(diào)用compact()清空已讀數(shù)據(jù)
buffer的屬性
capacity容量,該buffer最多存儲(chǔ)的字節(jié)數(shù)
position位置菇绵,寫模式下选脊,position從0到capacity-1,變更為讀模式后脸甘,position歸零恳啥,邊讀邊移動(dòng)
limit限制,寫模式下丹诀,代表我們能寫的最大量為capacity钝的,讀模式下,變更為原position位置铆遭,即有數(shù)據(jù)的位置
buffer api
通過(guò)allocate()方法為buffer分配內(nèi)存大小硝桩,如開(kāi)辟一個(gè)48字節(jié)的ByteBuffer buffer:ByteBuffer.allocate(48)
寫數(shù)據(jù)可以通過(guò)通道寫,如:FileChannel.read(buffer)枚荣;也可以通過(guò)put方法來(lái)寫數(shù)據(jù)碗脊,如:buffer.put(127)
flip()翻轉(zhuǎn)方法,將寫模式切換到讀模式橄妆,position歸零衙伶,設(shè)置limit為之前的position位置
讀數(shù)據(jù)可以讀到通道祈坠,如:FileChannel.write(buffer);也可以調(diào)用get()方法讀取矢劲,byte aByte=buffer.get()
buffer.rewind()將position置0赦拘,limit不變,這樣我們就可以重復(fù)讀取數(shù)據(jù)啦
buffer.clear()將position置為0芬沉,limit設(shè)置為capacity躺同,這里并沒(méi)有刪除buffer里面的數(shù)據(jù),只是把標(biāo)記位置改了丸逸;
buffer.compact()清除已讀數(shù)據(jù)蹋艺,這里也沒(méi)有刪除數(shù)據(jù),將position設(shè)置為未讀的個(gè)數(shù)黄刚,將后面幾個(gè)未讀的字節(jié)順序的復(fù)制到前面的幾個(gè)字節(jié)捎谨,limit設(shè)置為capacity,比如buffer容量3個(gè)字節(jié)隘击,讀取hello,在讀取了2個(gè)字節(jié)后我就調(diào)用了compact()方法研铆,那么此時(shí)position為1埋同,limit為3,buffer內(nèi)部存儲(chǔ)的數(shù)據(jù)buff[0]='l',buff[1]='e',buff[2]='l'棵红,因?yàn)橛幸粋€(gè)'l'沒(méi)有讀完凶赁,將'l'提取到最前面供下次讀取
mark()可以標(biāo)記當(dāng)前的position位置,通過(guò)reset來(lái)恢復(fù)mark位置逆甜,可以用來(lái)實(shí)現(xiàn)重復(fù)讀取滿足條件的數(shù)據(jù)塊
equals()兩個(gè)buffer相等需滿足虱肄,類型相同,buffer剩余(未讀)字節(jié)數(shù)相同交煞,所有剩余字節(jié)數(shù)相同
compareTo()比較buffer中的剩余元素咏窿,只不過(guò)此方法適合排序
Channel(通道)
Channel的重要實(shí)現(xiàn)
FileChannel用于文件數(shù)據(jù)的讀寫,transferTo()方法可以將通道的數(shù)據(jù)傳送至另外一個(gè)通道素征,完成數(shù)據(jù)的復(fù)制
DatagramChannel用于UDP數(shù)據(jù)的讀寫
SocketChannel用于TCP的數(shù)據(jù)讀寫集嵌,通常我們所說(shuō)的客戶端套接字通道
ServerSocketChannel允許我們監(jiān)聽(tīng)TCP鏈接請(qǐng)求,通常我們所說(shuō)的服務(wù)端套接字通道御毅,每一個(gè)請(qǐng)求都會(huì)創(chuàng)建一個(gè)SocketChannel
Scatter和Gather
java nio在Channel實(shí)現(xiàn)類也實(shí)現(xiàn)了Scatter和Gather相關(guān)類
Scatter.read()是從通道讀取的操作能把數(shù)據(jù)寫入多個(gè)buffer根欧,即一個(gè)通道向多個(gè)buffer寫數(shù)據(jù)的過(guò)程,但是read必須寫滿一個(gè)buffer后才會(huì)向后移動(dòng)到下一個(gè)buffer端蛆,因此read不適合大小會(huì)動(dòng)態(tài)改變的數(shù)據(jù)凤粗。代碼如下:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
- Gather.write()是從可以把多個(gè)buffer的數(shù)據(jù)寫入通道,write是只會(huì)寫position到limit之間的數(shù)據(jù)今豆,因此寫是可以適應(yīng)大小動(dòng)態(tài)改變的數(shù)據(jù)嫌拣。代碼如下:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
在有些場(chǎng)景會(huì)非常有用柔袁,比如處理多份需要分開(kāi)傳輸?shù)臄?shù)據(jù),舉例來(lái)說(shuō)亭罪,假設(shè)一個(gè)消息包含了header和body瘦馍,我們可能會(huì)把header和body分別放在不同的buffer
Selector(選擇器)
Selector用于檢查一個(gè)或多個(gè)NIO Channel的狀態(tài)是否可讀可寫,這樣就可以實(shí)現(xiàn)單線程管理多個(gè)Channels应役,也就是可以管理多個(gè)網(wǎng)絡(luò)連接情组,NIO非阻塞主要就是通過(guò)Selector注冊(cè)事件監(jiān)聽(tīng),監(jiān)聽(tīng)通道將數(shù)據(jù)就緒后箩祥,就進(jìn)行實(shí)際的讀寫操作院崇,因此前面說(shuō)的IO兩個(gè)階段,一階段NIO僅僅是異步監(jiān)聽(tīng)袍祖,二階段就是同步實(shí)際操作數(shù)據(jù)
Selector監(jiān)聽(tīng)事件類別
SelectionKey.OP_CONNECT是Channel和server連接成功后底瓣,連接就緒
SelectionKey.OP_ACCEPT是server Channel接收請(qǐng)求連接就緒
SelectionKey.OP_READ是Channel有數(shù)據(jù)可讀時(shí),處于讀就緒
SelectionKey.OP_WRITE是Channel可以進(jìn)行數(shù)據(jù)寫入是蕉陋,寫就緒
使用Selector的步驟
創(chuàng)建一個(gè)Selector捐凭,Selector selector = Selector.open();
注冊(cè)Channel到Selector上面,將Channel切換為非阻塞的:channel.configureBlocking(false)凳鬓,然后綁定Selector:SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
SelectionKey.OP_READ為監(jiān)聽(tīng)的事件類別茁肠,如果要監(jiān)聽(tīng)多個(gè)事件,可利用位的或運(yùn)算結(jié)合多個(gè)常量如:int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
需要使用Selector的Channel必須是非阻塞的缩举,F(xiàn)ileChannel不能切換為非租塞的垦梆,因此FileChannel不適用于Selector
API
Selector.select()方法是阻塞的,因此可以放心的將它寫在while(true)中仅孩,不用擔(dān)心cpu會(huì)空轉(zhuǎn)
Selector.wakeup()喚醒select()造成的阻塞托猩,可能是有新的事件注冊(cè),優(yōu)先級(jí)更高的事件觸發(fā)(如定時(shí)器事件)辽慕,希望及時(shí)處理京腥。其原理是向通道或者連接中寫入一個(gè)字節(jié),阻塞的select因?yàn)橛蠭O事件就緒溅蛉,立即返回
Issues
在使用TcpClient和TcpServer時(shí)绞旅,最開(kāi)始客戶端結(jié)束之后,服務(wù)端catch住了異常温艇,但是并沒(méi)有處理因悲,那個(gè)客戶端的Channel還是可以從Selector出來(lái),原因是注冊(cè)了這個(gè)事件沒(méi)有取消注冊(cè)勺爱,在catch中取消注冊(cè)該事件就行晃琳,SelectionKey.cancel()
localhost是一個(gè)域名,通常指向127.0.0.1可在host文件配置
127.0.0.1指本機(jī)地址環(huán)回地址,只有自己的機(jī)器可以訪問(wèn)
0.0.0.0表示網(wǎng)絡(luò)中的本機(jī)地址卫旱,即如果本機(jī)器連接了兩個(gè)網(wǎng)絡(luò)人灼,在注冊(cè)服務(wù)端時(shí)使用0.0.0.0,這兩個(gè)網(wǎng)絡(luò)的其他主機(jī)都可以訪問(wèn)這個(gè)服務(wù)端
我們可以理解為本機(jī)有三塊網(wǎng)卡顾翼,一塊網(wǎng)卡叫做 loopback(這是一塊虛擬網(wǎng)卡)127.*整個(gè)網(wǎng)段用作loopback網(wǎng)絡(luò)接口的默認(rèn)地址投放,按照慣例通常設(shè)置為127.0.0.1這個(gè)地址和網(wǎng)絡(luò)無(wú)關(guān),這個(gè)是環(huán)回地址适贸,直接找本地地址灸芳,如果服務(wù)端綁定這個(gè)地址,那就只有本地服務(wù)器才可以訪問(wèn)這個(gè)服務(wù)端拜姿,另外一塊網(wǎng)卡叫做 ethernet (這是你的有線網(wǎng)卡)烙样,還有一塊網(wǎng)卡叫做 wlan(這是你的無(wú)線網(wǎng)卡)
實(shí)例源代碼如下圖:前往下載