目錄html
NIO 源碼分析(03) 從 BIO 到 NIOjava
Netty 系列目錄(http://www.javashuo.com/article/p-hskusway-em.html)linux
基本上,全部的 IO 在 NIO 中都從一個 Channel 開始。Channel 有點象流。 數據能夠從 Channel 讀到 Buffer 中,也能夠從 Buffer 寫到 Channel 中。這裏有個圖示:編程
總結: Channel 和 Buffer 在 NIO 並非新的東西:Channel 的本質就是 Socket,Buffer 的本質就是 byte[]。在 BIO 時代,BufferedInputStream 就是一個緩衝流。數組
Selector 容許單線程處理多個 Channel。若是你的應用打開了多個鏈接(通道),但每一個鏈接的流量都很低,使用 Selector 就會很方便。例如,在一個聊天服務器中。緩存
這是在一個單線程中使用一個 Selector 處理 3 個 Channel 的圖示:服務器
總結: Selector 在是 NIO 的核心,有了 Selector 模型,一個線程就能夠處理多個 Channel 了。網絡
(1) Linux IO 網絡編程socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0); bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); listen(listenfd, BACKLOG); socklen_t cliaddr_len = sizeof(client_addr); int clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len);
(2) Linux NIO 網絡編程函數
int listenfd = socket(AF_INET, SOCK_STREAM, 0); bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); listen(listenfd, BACKLOG); // select 模型處理過程 // 1. 初始化套接字集合,添加監聽 socket 到這個集合 FD_ZERO(&totalSet); FD_SET(listenfd, &totalSet); maxi = listenfd; while(1) { // 2. 將集合的一個拷貝傳遞給 select 函數。當有事件發生時,select 移除未決的 socket 而後返回。 // 也就是說 select 返回時,集合 readSet 中就是發生事件的 readSet readSet = totalSet; int nready = select(maxi + 1, &readSet, NULL, NULL, NULL); if (nready > 0) { if (FD_ISSET(listenfd, &readSet)) { cliaddr_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &cliaddr_len); printf("client IP: %s\t PORT : %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); FD_SET(connfd, &totalSet); maxi = connfd; if (--nready == 0) { continue; } } } }
總結: 對比 Linux IO 和 NIO 網絡編程能夠發現,NIO 相對 BIO 多出來的部分實際上是 select 部分,其他的(包括 socket 建立,數據讀取等)都是同樣的。因此我說 Channel 和 Buffer 是 JDK 層面概念的轉換,Selector 纔是 NIO 的核心,接下來 NIO 的源碼會更多的關注 Selector 模型的分析,Channel 和 Buffer 點到即止。
Java NIO 和 IO 之間第一個最大的區別是,IO 是面向流的,NIO 是面向緩衝區的。
Java IO 面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。此外,它不能先後移動流中的數據。若是須要先後移動從流中讀取的數據,須要先將它緩存到一個緩衝區。
Java NIO 的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。可是,還須要檢查是否該緩衝區中包含全部您須要處理的數據。並且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏還沒有處理的數據。
面向流,面向緩衝區,這是 Java 中的概念,和操做系統無關。
(1) Java IO 【SocketInputStream】
/** * Java IO 直接對讀取的數據進行操做 * 1. socketRead(native) 函數中獲取至關長度的數據,而後直接對這塊數據進行了操做 */ int read(byte b[], int off, int length, int timeout) throws IOException { // acquire file descriptor and do the read FileDescriptor fd = impl.acquireFD(); try { // native函數,這裏從內核態中讀取數據到數組 b 中 n = socketRead(fd, b, off, length, timeout); if (n > 0) { return n; } } catch (ConnectionResetException rstExc) { } finally { impl.releaseFD(); } }
(2) Java NIO 【DatagramChannelImpl】
/** * Java NIO 每次讀取的數據放在該內存中,而後對該內存進行操做,增長了處理數據的靈活性 * 1. Util.getTemporaryDirectBuffer(newSize) 申請了一塊堆外內存 * 2. receiveIntoNativeBuffer(native) 將數據讀取到堆外內存中 * 3. dst.put(bb) 將數據從該內存中讀取到內存塊 dst 中 * 4. dst 就一個共享的內存塊,能夠對該內存進行各類操做,但也要注意一些問題,如數據覆蓋 */ private int receive(FileDescriptor fd, ByteBuffer dst) throws IOException { int pos = dst.position(); int lim = dst.limit(); assert (pos <= lim); int rem = (pos <= lim ? lim - pos : 0); if (dst instanceof DirectBuffer && rem > 0) return receiveIntoNativeBuffer(fd, dst, rem, pos); // 申請一塊 newSize 大小的緩衝區塊 int newSize = Math.max(rem, 1); ByteBuffer bb = Util.getTemporaryDirectBuffer(newSize); try { // 數據讀取到緩衝區中,buffer 能夠作標記,操做指針等 int n = receiveIntoNativeBuffer(fd, bb, newSize, 0); bb.flip(); if (n > 0 && rem > 0) dst.put(bb); return n; } finally { Util.releaseTemporaryDirectBuffer(bb); } }
能夠看到,第一段代碼中一次性從 native 函數中獲取至關長度的數據,而後直接對這塊數據進行了操做。
而第二段代碼中 Util.getTemporaryDirectBuffer(newSize); 申請了一塊堆外內存,每次讀取的數據放在該內存中,而後對該內存進行操做。
通常說法面向緩存相對於面向流的好處在於增長了處理數據的靈活性,固然也增長了操做的複雜度,好比當更多數據取入時,是否會覆蓋前面的數據等
天天用心記錄一點點。內容也許不重要,但習慣很重要!