前言
這篇文章讀不懂的沒關系厢漩,可以先收藏一下待错。筆者準備介紹完epoll和NIO等知識點褐澎,然后寫一篇Java網(wǎng)絡IO模型的介紹会钝,這樣可以使Java網(wǎng)絡IO的知識體系更加地完整和嚴謹。初學者也可以等看完IO模型介紹的博客之后工三,再回頭看這些博客迁酸,會更加有收獲。
NIO相比BIO的優(yōu)勢
NIO(Non-blocking I/O俭正,在Java領域奸鬓,也稱為New I/O),是一種同步非阻塞的I/O模型掸读,也是I/O多路復用的基礎串远,已經(jīng)被越來越多地應用到大型應用服務器,成為解決高并發(fā)與大量連接儿惫、I/O處理問題的有效方式澡罚。
面向流與面向緩沖
Java NIO和BIO之間第一個最大的區(qū)別是,BIO是面向流的肾请,NIO是面向緩沖區(qū)的留搔。 JavaIO面向流意味著每次從流中讀一個或多個字節(jié),直至讀取所有字節(jié)铛铁,它們沒有被緩存在任何地方隔显。此外,它不能前后移動流中的數(shù)據(jù)饵逐。如果需要前后移動從流中讀取的數(shù)據(jù)括眠,需要先將它緩存到一個緩沖區(qū)。Java NIO的緩沖讀取方法略有不同梳毙。數(shù)據(jù)讀取到一個緩沖區(qū)哺窄,需要時可在緩沖區(qū)中前后移動捐下。這就增加了處理過程中的靈活性账锹。但是,還需要檢查是否該緩沖區(qū)中包含所有需要處理的數(shù)據(jù)坷襟。而且奸柬,需確保當更多的數(shù)據(jù)讀入緩沖區(qū)時,不要覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)婴程。
有關面向緩沖讀取數(shù)據(jù)的示例和注意點廓奕,可以點擊查看
阻塞IO與非阻塞IO
Java IO的各種流是阻塞的。這意味著,當一個線程調(diào)用read() 或write()時桌粉,該線程被阻塞蒸绩,直到有數(shù)據(jù)被讀取或者數(shù)據(jù)寫入。該線程在阻塞期間不能做其他事情铃肯。而Java NIO的非阻塞模式患亿,如果通道沒有東西可讀,或不可寫押逼,讀寫函數(shù)馬上返回步藕,而不會阻塞,這個線程可以去做別的事情挑格。 線程通常將非阻塞IO的空閑時間用于在其它通道上執(zhí)行IO操作咙冗,所以一個單獨的線程可以管理多個輸入和輸出通道(channel),即IO多路復用的原理漂彤。
零拷貝
在傳統(tǒng)的文件IO操作中雾消,我們都是調(diào)用操作系統(tǒng)提供的底層標準IO系統(tǒng)調(diào)用函數(shù)read()、write() 挫望,此時調(diào)用此函數(shù)的進程(在JAVA中即java進程)由當前的用戶態(tài)切換到內(nèi)核態(tài)仪或,然后OS的內(nèi)核代碼負責將相應的文件數(shù)據(jù)讀取到內(nèi)核的IO緩沖區(qū),然后再把數(shù)據(jù)從內(nèi)核IO緩沖區(qū)拷貝到進程的私有地址空間中去士骤,這樣便完成了一次IO操作范删。
而NIO的零拷貝與傳統(tǒng)的文件IO操作最大的不同之處就在于它雖然也是要從磁盤讀取數(shù)據(jù),但是它并不需要將數(shù)據(jù)讀取到OS內(nèi)核緩沖區(qū)拷肌,而是直接將進程的用戶私有地址空間中的一部分區(qū)域與文件對象建立起映射關系到旦,這樣直接從內(nèi)存中讀寫文件,速度大幅度提升巨缘。
詳細的解析添忘,之后會有單獨的博客進行講解
NIO的核心部分
Java NIO主要由以下三個核心部分組成:
- Channel
- Buffer
- Selector
Channel
基本上,所有的IO在NIO中都從一個Channel開始若锁。數(shù)據(jù)可以從Channel讀到Buffer中搁骑,也可以從Buffer寫到Channel中。這里有個圖示:
Channel和Buffer有好幾種類型又固。下面是Java NIO中的一些主要Channel的實現(xiàn):
- FileChannel(file)
- DatagramChannel(UDP)
- SocketChannel(TCP)
- ServerSocketChannel(TCP)
這些通道涵蓋了UDP和TCP網(wǎng)絡IO以及文件IO仲器。
最后兩個channel的關系。通過 ServerSocketChannel.accept() 方法監(jiān)聽新進來的連接仰冠。當 accept()方法返回的時候,它返回一個包含新進來的連接的 SocketChannel乏冀。因此, accept()方法會一直阻塞到有新連接到達。通常不會僅僅只監(jiān)聽一個連接,在while循環(huán)中調(diào)用 accept()方法.
//打開 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
//關閉ServerSocketChannel
serverSocketChannel.close();
Buffer
緩沖區(qū)本質(zhì)上是一塊可以寫入數(shù)據(jù)洋只,然后可以從中讀取數(shù)據(jù)的內(nèi)存辆沦。這塊內(nèi)存被包裝成NIO Buffer對象昼捍,并提供了一組方法,用來方便的訪問該塊內(nèi)存肢扯。
Java NIO里關鍵的Buffer實現(xiàn):
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
這些Buffer覆蓋了你能通過IO發(fā)送的基本數(shù)據(jù)類型:byte妒茬、short、int蔚晨、long郊闯、float、double和char蛛株。
為了理解Buffer的工作原理团赁,需要熟悉它的三個屬性:
- capacity
- position
- limit
position和limit的含義取決于Buffer處在讀模式還是寫模式。不管Buffer處在什么模式谨履,capacity的含義總是一樣的欢摄。
capacity
作為一個內(nèi)存塊,Buffer有個固定的最大值笋粟,就是capacity怀挠。Buffer只能寫capacity個byte、long害捕、char等類型绿淋。一旦Buffer滿了,需要將其清空(通過讀數(shù)據(jù)或者清除數(shù)據(jù))才能繼續(xù)寫數(shù)據(jù)往里寫數(shù)據(jù)尝盼。
position
當寫數(shù)據(jù)到Buffer中時吞滞,position表示當前的位置。初始的position值為0盾沫。當一個byte裁赠、long等數(shù)據(jù)寫到Buffer后, position會向前移動到下一個可插入數(shù)據(jù)的Buffer單元赴精。position最大可為capacity – 1.
當讀取數(shù)據(jù)時佩捞,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式蕾哟,position會被重置為0一忱。 當從Buffer的position處讀取數(shù)據(jù)時,position向前移動到下一個可讀的位置谭确。
limit
在寫模式下帘营,Buffer的limit表示最多能往Buffer里寫多少數(shù)據(jù)。 寫模式下琼富,limit等于capacity仪吧。
當切換Buffer到讀模式時, limit表示你最多能讀到多少數(shù)據(jù)鞠眉。因此薯鼠,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值械蹋。
Selector
Selector允許單線程處理多個 Channel出皇。如果你的應用打開了多個連接(通道),但每個連接的流量都很低哗戈,使用Selector就會很方便郊艘。例如,在一個聊天服務器中唯咬。
這是在一個單線程中使用一個Selector處理3個Channel的圖示:
要使用Selector纱注,得向Selector注冊Channel,然后調(diào)用它的select()方法胆胰。這個方法會一直阻塞到某個注冊的通道有事件就緒狞贱。一旦這個方法返回,線程就可以處理這些事件蜀涨,事件例如有新連接進來瞎嬉,數(shù)據(jù)接收等。
NIO與epoll的關系
Java NIO根據(jù)操作系統(tǒng)不同厚柳, 針對NIO中的Selector有不同的實現(xiàn):
- macosx:KQueueSelectorProvider
- solaris:DevPollSelectorProvider
- Linux:EPollSelectorProvider (Linux kernels >= 2.6)或PollSelectorProvider
- windows:WindowsSelectorProvider
所以不需要特別指定氧枣,Oracle JDK會自動選擇合適的Selector。
如果想設置特定的Selector别垮,可以設置屬性便监,例如:
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
JDK在Linux已經(jīng)默認使用epoll方式,但是JDK的epoll采用的是水平觸發(fā)碳想,所以Netty自4.0.16起, Netty為Linux通過JNI的方式提供了native socket transport茬贵。Netty重新實現(xiàn)了epoll機制,
- 采用邊緣觸發(fā)方式
- netty epoll transport暴露了更多的nio沒有的配置參數(shù)移袍,如 TCP_CORK, SO_REUSEADDR等等解藻。
- C代碼,更少GC葡盗,更少synchronized
使用native socket transport的方法很簡單螟左,只需將相應的類替換即可。
NioEventLoopGroup → EpollEventLoopGroup
NioEventLoop → EpollEventLoop
NioServerSocketChannel → EpollServerSocketChannel
NioSocketChannel → EpollSocketChannel
有關epoll的詳細講解觅够,可以點擊查看
NIO處理消息的核心思路
結合示例代碼胶背,總結NIO的核心思路:
- NIO 模型中通常會有兩個線程,每個線程綁定一個輪詢器 selector 喘先,在上面例子中serverSelector負責輪詢是否有新的連接钳吟,clientSelector負責輪詢連接是否有數(shù)據(jù)可讀
- 服務端監(jiān)測到新的連接之后,不再創(chuàng)建一個新的線程窘拯,而是直接將新連接綁定到clientSelector上红且,這樣就不用BIO模型中1w 個while循環(huán)在阻塞坝茎,參見(1)
- clientSelector被一個 while 死循環(huán)包裹著,如果在某一時刻有多條連接有數(shù)據(jù)可讀暇番,那么通過clientSelector.select(1)方法可以輪詢出來嗤放,進而批量處理,參見(2)
- 數(shù)據(jù)的讀寫面向 Buffer壁酬,參見(3)
NIO的示例代碼
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 對應IO編程中服務端啟動
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 監(jiān)測是否有新的連接次酌,這里的1指的是阻塞的時間為 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// (1) 每來一個新連接,不需要創(chuàng)建一個線程舆乔,而是直接注冊到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
while (true) {
// (2) 批量輪詢是否有哪些連接有數(shù)據(jù)可讀岳服,這里的1指的是阻塞的時間為 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
}