寫在前面
之前在學習 dubbo 源碼和 netty , 在學習到 dubbo 的傳輸層源碼的時候不太理解 dubbo 對 Channel 的設計 , Client , Server 分別都實現了 Channel 接口 , 當時是不太理解的 。又參考了一下 netty 發現 dubbo 在傳輸層的設計上包括 Channel , ChannelHandler 也是很大一部分參考了 netty 的設計實現。不過我對 netty 也不懂,於是我就想從 jdk 中的 Channel 開始進行系統的學習 , 希望能在理解 java nio channel 的基礎上再去對其他成熟的網絡通信框架進行學習。
Channel 簡介
channel 是連接兩個互通的 I/O 實例通道的抽象 , 兩個 I/O 實例發出的數據在 channel 中傳輸。I/O 實例可能是硬盤,內存 , 網絡設備等。
java nio channel 結構簡圖 (當中只選取了部分我自己認爲比較關鍵的) :
AutoCloseable 、 Closeable 接口定義了 close 方法來關閉釋放打開的資源。Channel 接口是一個高度抽象只定義了檢測 channel 是否處於打開狀態的方法。WritableByteChannel 和 ReadableByteChannel 分別定義了寫入和讀取的方法。而 ByteChannel 什麼都沒做只是繼承了 WritableByteChannel 和 ReadableByteChannel ,任何實現了 ByteChannel 接口的 channel 都具備了讀、寫的能力。GatheringByteChannel 可以將多個 Buffer 中的數據寫入 channel 中 , ScatteringByteChannel 可以將 channel 中的數據分片讀取到多個 Buffer 中。FileChannel 和文件系統相關的 channel , 用來操作文件 I/O 。NetworkChannel 網絡 I/O channel 。 SelectableChannel 基於多路複用 I/O 模型的 channel 抽象 。ServerSocketChannel , SocketChannel 基於 TCP 協議的網絡 channel 。MulticastChannel 基於UDP協議的網絡組播 channel。 DatagramChannel 基於 UDP 協議的網絡 channel 。AsynchronousChannel 異步 I/O 模型 channel 。AsynchronousFileChannel 異步的文件系統 I/O channel 。 AsynchronousServerSocketChannel , AsynchronousSocketChannel 基於TCP協議的異步網絡 I/O channel 。個人感覺 java channel 接口的粒度還是設計的比較細的。
從 FileChannel 入門
FileChannel 是專門針對文件系統 I/O 的 channel , FileChannel 總是阻塞式的 I/O 。FileChannel APIs 簡介 :
public abstract class FileChannel
extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
{
protected FileChannel() { }
// 打開一個文件 channel , 根據 path 指定的文件路徑打開該文件 channel , attrs 自定義的文件屬性 , options 該文件 channel 支持的操作
public static FileChannel open(Path path,
Set<? extends OpenOption> options,
FileAttribute<?>... attrs) throws IOException;
// 打開一個文件 channel , 根據 path 指定的文件路徑打開該文件 channel , options 該文件 channel 支持的操作
public static FileChannel open(Path path, OpenOption... options) throws IOException;
// 將 channel 中的數據讀取到 Buffer 中 , 返回讀取的字節個數。
public abstract int read(ByteBuffer dst) throws IOException;
/**
* 將 channel 按照 Buffer 數組的順序將數據讀入到 Buffer 中 , 從 channel 的當前位置開始讀取 , 返回讀取的字節個數。
* offset : Buffer 數組中第一個元素的偏移量。
* length : Buffer 數組中用來接收 channel 中數據的最大 Buffer 數目 , 比如 Buffer 數組中有 3 個 Buffer length
* 爲 2 , 那麼 channel 中的數據只會被讀取到 Buffer 數組中的第 0 , 1 位置的 Buffer 中。
*/
public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
// 從 channel 的當前位置將數據讀取到 Buffer 數組中 , 返回讀取的字節個數。
public final long read(ByteBuffer[] dsts) throws IOException;
// 將 Buffer 中的數據寫入到 channel 中 , 從 channel 的當前位置開始寫入 , 返回寫入的字節個數。
public abstract int write(ByteBuffer src) throws IOException;
/**
* 將一組 Buffer 中的數據寫入到 channel 中 , 從 channel 的當前位置開始寫入 , 返回寫入的字節個數。
* offset : Buffer 數組中第一個 Buffer 的偏移量。
* length : Buffer 數組中寫入 channel 中的最大 Buffer 個數。
*/
public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
// 將一組 Buffer 中的數據寫入到 channel 中 , 從 channel 的當前位置開始寫入 , 返回寫入的字節個數。
public final long write(ByteBuffer[] srcs) throws IOException;
// 獲取到文件 channel 的當前位置 。
public abstract long position() throws IOException;
// 爲 channel 設置一個新的當前位置。
public abstract FileChannel position(long newPosition) throws IOException;
// 獲取文件的大小 , 單位是字節
public abstract long size() throws IOException;
// 將 channel 連接的文件大小截斷爲給定的長度 , 從文件開頭開始計算。
public abstract FileChannel truncate(long size) throws IOException;
// 強制將此通道文件的任何更新寫入包含該通道的存儲設備
public abstract void force(boolean metaData) throws IOException;
// 將 channel 中的數據從指定的位置開始 , 傳輸指定個數的字節數 , 到另一個可寫的 channel 中。
public abstract long transferTo(long position, long count,WritableByteChannel target) throws IOException;
/**
* 將一個可讀 channel 中的數據傳輸到當前調用的 channel 中 。
* src :可讀的源 channel 。
* position : 當前 channel 的位置 。
* count : 從源 channel 中傳輸的總字節數。
*/
public abstract long transferFrom(ReadableByteChannel src,long position, long count) throws IOException;
// 從 channel 中指定的位置開始 , 從 channel 中讀取數據到 Buffer 中 , 返回讀取的字節數。
public abstract int read(ByteBuffer dst, long position) throws IOException;
// 從 channel 中指定的位置開始 , 將 Buffer 中的數據寫入 channel , 返回寫入的字節數。
public abstract int write(ByteBuffer src, long position) throws IOException;
/**
* 從指定位置開始 , 獲取一個指定大小的文件內存映射。
* mode : 內存映射的模式 , READ_ONLY , READ_WRITE ,PRIVATE
* position : channel 中的位置。
* size : channel 中要映射到內存中數據的大小。
*/
public abstract MappedByteBuffer map(MapMode mode,long position, long size) throws IOException;
/**
* 獲取 channel 文件的給定區域的鎖定。 如果鎖定區域已經被鎖定,而且獲取的是一個排它鎖,方法會阻塞,直到鎖被釋放。
* position : channel 中要鎖定區域的起始位置。
* size : 鎖定的區域大小 , 單位字節 。
* shared : 是否是共享鎖 , true - 是共享鎖 , false - 獨佔鎖 。
*/
public abstract FileLock lock(long position, long size, boolean shared) throws IOException;
// 獲取 channel 文件的排它鎖。如果該文件已經被鎖定, 這個方法會阻塞。
public final FileLock lock() throws IOException;
/**
* 嘗試獲取 channel 文件的給定區域的鎖定。 如果鎖定區域已經被鎖定,這個方法不會被鎖定,而是返回 null 。
* position : channel 中要鎖定區域的起始位置。
* size : 鎖定的區域大小 , 單位字節 。
* shared : 是否是共享鎖 , true - 是共享鎖 , false - 獨佔鎖 。
*/
public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
// 獲取 channel 文件的排它鎖。如果該文件已經被鎖定, 這個方法不會被鎖定,而是返回 null 。
public final FileLock tryLock() throws IOException;
}
OpenOption :
OpenOption 定義了文件將被如何開打或者是創建。StandardOpenOption 定義了一些標準的操作類型:
public enum StandardOpenOption implements OpenOption {
// 文件是可讀的
READ,
// 文件是可寫的
WRITE,
// 寫入時從文件末尾開始添加
APPEND,
// 如果文件已經存在並且是以 WRITE 方式打開 , 會將該文件內容清空到 0 字節大小 。
// 如果文件是以 READ 方式打開則不會清空文件 。
TRUNCATE_EXISTING,
// 如果文件不存在就創建一個文件。
CREATE,
// 創建一個新的文件,如果文件已經存在則創建失敗。
CREATE_NEW,
// channel 關閉時刪除文件。
DELETE_ON_CLOSE,
/**
* 稀疏文件,當與CREATE_NEW選項一起使用時,此選項提供了一個提示 ,新文件將是稀疏的。
* 當文件系統不支持創建稀疏文件時,該選項將被忽略。
*
* 稀疏文件就是在文件中留有很多空餘空間,留備將來插入數據使用。
* 如果這些空餘空間被ASCII碼的NULL字符佔據,並且這些空間相當大。
* 那麼,這個文件就被稱爲稀疏文件,而且,並不分配相應的磁盤塊。
*/
SPARSE,
// 要求將文件內容或元數據的每一次更新同步地寫入底層存儲設備。
SYNC,
// 要求將文件內容的每次更新同步地寫入底層存儲設備。
DSYNC;
}
FileChannel 的讀、寫操作是很簡單的只是有一些需要注意的細節, 在進行讀、寫操作之前不要忘記對 Buffer 進行 flip() 。
文件鎖定 :
鎖可以是共享鎖或者是獨佔鎖 , 如果要獲取共享鎖文件需要是可讀的 , 如果要獲取獨佔鎖文件需要是可寫的。在 JDK1.4 之前是不支持文件鎖定的,但是絕大多數的現代操作系統是早就支持文件鎖定的。文件鎖定的特性在很大程度上依賴本地操作系統的實現,並不是所有的操作系統都支持共享的文件鎖。對於不支持共享文件鎖的操作系統,對一個共享鎖的請求會被自動提升爲對獨佔鎖的請求。鎖定對多個 JVM 實例的訪問,以及同一 JVM 中不同線程的訪問都是有效的。這一點與 《Java NIO》 一書中描述的相反 , 《Java NIO》 中的描述是 : "鎖的對象是文件而不是通道或線程,這意味着文件鎖不適用於判優同一臺 Java 虛擬機上的多個線程發起的訪問。如果一個線程在某個文件上獲得了一個獨佔鎖,然後第二個線程利用一個單獨打開的通道來請求該文件的獨佔鎖,那麼第二個線程的請求會被批准。但如果這兩個線程運行在不同的 Java 虛擬機上,那麼第二個線程會阻塞,因爲鎖最終是由操作系統或文件系統來判優的並且幾乎總是在進程級而非線程級上判優。鎖都是與一個文件關聯的,而不是與單個的文件句柄或通道關聯。" 。實際上我在測試過程中發現 , 同一 JVM 內如果一個線程已經鎖定了文件 , 在鎖沒有釋放前另一個線程嘗試鎖定該文件會拋出 OverlappingFileLockException , 不同 JVM 中當有一個 JVM 中已經鎖定了該文件,另一個 JVM 中試圖獲取該文件鎖定則會阻塞,但不會拋出異常。鎖的釋放需要調用 FileLock 的 release() 方法 , 或者是 channel 關閉 , JVM 關閉時文件鎖都會釋放掉。 文件鎖定測試代碼 :
// 不同 JVM 中文件鎖定測試
@Test
public void jvm1LockTest() throws Exception {
Path path = Paths.get("D:\\Documents\\Pictures\\test\\lock test.txt");
FileChannel fChannel = FileChannel.open(path , StandardOpenOption.CREATE_NEW , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
// 嘗試獲取一個排他鎖
FileLock fileLock = fChannel.tryLock();
// FileLock fileLock = fChannel.tryLock(0 , fChannel.size() , true);
try {
if (! fileLock.isValid()) {
System.out.println("file lock is invalid");
return;
}
String s0 = "一聲梧葉一聲秋,一點芭蕉一點愁,三更歸夢三更後。落燈花,棋未收,嘆新豐逆旅淹留。枕上十年事,江南二老憂,都到心頭。";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
Thread.sleep(100000);
} finally {
fileLock.release();
fChannel.close();
}
}
// 不同 JVM 中文件鎖定測試
@Test
public void jvm2LockTest() throws Exception {
Path path = Paths.get("D:\\Documents\\Pictures\\test\\lock test.txt");
FileChannel fChannel = FileChannel.open(path , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
FileLock fileLock = fChannel.lock();
try {
if (Objects.isNull(fileLock) || ! fileLock.isValid()) {
System.out.println("file lock is invalid");
return;
}
String s0 = "@@@@@@@@@@@@@@@@@@@@@@@<<<<M><><><><><><><>>>>>>>>>>>";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
} finally {
if (Objects.nonNull(fileLock)) {
fileLock.release();
}
fChannel.close();
}
}
// 同一 JVM 中多線程文件鎖定測試
@Test
public void MultiThreadingLockTest() throws Exception {
Path path = Paths.get("D:\\Documents\\Pictures\\test\\lock test.txt");
new Thread(() -> {
FileChannel fChannel = null;
FileLock fileLock = null;
try {
fChannel = FileChannel.open(path , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
// 獲取一個排他鎖
fileLock = fChannel.lock();
if (! fileLock.isValid()) {
System.out.println("file lock is invalid");
return;
}
System.out.println(Thread.currentThread().getName() + " Thread locking file");
String s0 = "一聲梧葉一聲秋,一點芭蕉一點愁,三更歸夢三更後。落燈花,棋未收,嘆新豐逆旅淹留。枕上十年事,江南二老憂,都到心頭。";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
Thread.sleep(100000);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(fileLock)) {
try {
fileLock.release();
} catch (IOException e) {
e.printStackTrace();
}
}
if (Objects.nonNull(fChannel)) {
try {
fChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}, "FileLockThread-1").start();
new Thread(() -> {
FileChannel fChannel = null;
FileLock fileLock = null;
try {
fChannel = FileChannel.open(path , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
fileLock = fChannel.lock();
if (Objects.isNull(fileLock) || ! fileLock.isValid()) {
System.out.println("file lock is invalid");
return;
}
System.out.println(Thread.currentThread().getName() + " Thread locking file");
String s0 = "@@@@@@@@@@@@@@@@@@@@@@@<<<<M><><><><><><><>>>>>>>>>>>";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(fileLock)) {
try {
fileLock.release();
} catch (IOException e) {
e.printStackTrace();
}
}
if (Objects.nonNull(fChannel)) {
try {
fChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} , "FileLockThread-2").start();
Thread.sleep(10000);
}
內存映射文件:
將文件直接映射到內存中,就可以在內存中直接操作文件內容。文件映射有三種模式 , READ_ONLY 只讀模式, READ_WRITE 讀寫模式, PRIVATE 寫時拷貝模式 。寫時拷貝意味着通過 put( )方法所做的任何修改都會導致產生一個私有的數據拷貝並且該拷貝中的數據只有MappedByteBuffer 實例可以看到。該過程不會對底層文件做任何修改,而且一旦緩衝區被施以垃圾收集動作(garbage collected),那些修改都會丟失。儘管寫時拷貝的映射可以防止底層文件被修改,您也必須以 read/write 權限來打開文件以建立 MapMode.PRIVATE 映射。只有這樣,返回的MappedByteBuffer 對象才能允許使用 put( )方法。通過內存映射機制來訪問一個文件會比使用常規方法讀寫高效得多,甚至比使用通道的效率都高。因爲不需要做明確的系統調用,那會很消耗時間。更重要的是,操作系統的虛擬內存可以自動緩存內存頁(memory page)。這些頁是用系統內存來緩存的,所以不會消耗 Java 虛擬機內存堆(memory heap)。一旦一個內存頁已經生效(從磁盤上緩存進來),它就能以完全的硬件速度再次被訪問而不需要再次調用系統命令來獲取數據。那些包含索引以及其他需頻繁引用或更新的內容的巨大而結構化文件能因內存映射機制受益非常多。一個映射一旦建立之後將保持有效,直到MappedByteBuffer 對象被施以垃圾收集動作爲止。同鎖不一樣的是,映射緩衝區沒有綁定到創建它們的通道上。關閉相關聯的 FileChannel 不會破壞映射,只有丟棄緩衝區對象本身才會破壞該映射。MemoryMappedBuffer 直接反映它所關聯的磁盤文件。如果映射有效時文件被在結構上修改,就會產生奇怪的行爲(當然具體的行爲是取決於操作系統和文件系統的)。MemoryMappedBuffer有固定的大小,不過它所映射的文件卻是彈性的。具體來說,如果映射有效時文件大小變化了,那麼緩衝區的部分或全部內容都可能無法訪問,並將返回未定義的數據或者拋出未檢查的異常。所有的 MappedByteBuffer 對象都是直接的,這意味着它們佔用的內存空間位於 Java 虛擬機內存堆之外。 內存映射文件測試代碼 :
@Test
public void mapTest() throws Exception {
// 在內存映射緩衝區上做的修改會同步到文件中
Path path = Paths.get("D:\\Documents\\Pictures\\test\\map test.txt");
FileChannel fChannel = FileChannel.open(path , StandardOpenOption.CREATE_NEW , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
String s0 = "一聲梧葉一聲秋,一點芭蕉一點愁,三更歸夢三更後。落燈花,棋未收,嘆新豐逆旅淹留。枕上十年事,江南二老憂,都到心頭。";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
String s1 = ">>>>>>>>>>>>>>>>>>";
MappedByteBuffer buffer1 = fChannel.map(FileChannel.MapMode.READ_WRITE, 0, s1.getBytes().length);
fChannel.close();
buffer1.put(s1.getBytes(Charset.forName("utf-8")));
}
AsynchronousFileChannel 異步的文件 channel
AsynchronousFileChannel APIs 簡介 (異步文件 channel 對文件的讀、寫、鎖定操作都是異步進行的):
public abstract class AsynchronousFileChannel
implements AsynchronousChannel
{
protected AsynchronousFileChannel() {
}
/**
* 打開一個文件 channel , 用指定的線程池和 channel 綁定 ,如果 executor 爲 null 使用默認的線程池
*/
public static AsynchronousFileChannel open(Path file,
Set<? extends OpenOption> options,
ExecutorService executor,
FileAttribute<?>... attrs) throws IOException ;
// 打開一個文件 channel , 使用默認的線程池和 channel 綁定
public static AsynchronousFileChannel open(Path file, OpenOption... options) throws IOException;
// 獲取文件大小,單位字節
public abstract long size() throws IOException;
// 將文件截斷爲指定的大小
public abstract AsynchronousFileChannel truncate(long size) throws IOException;
// 強制將此通道文件的任何更新寫入包含該通道的存儲設備
public abstract void force(boolean metaData) throws IOException;
// 異步的獲取指定文件區域的鎖定 , 無阻塞 , 獲取鎖完成或者失敗後會回調 CompletionHandler
public abstract <A> void lock(long position, long size, boolean shared, A attachment, CompletionHandler<FileLock,? super A> handler);
// 異步的獲取文件區域的排它鎖 , 無阻塞, 獲取鎖完成或者失敗後會回調 CompletionHandler
public final <A> void lock(A attachment,CompletionHandler<FileLock,? super A> handler);
// 異步的獲取指定文件區域的鎖定 , 無阻塞 ,返回 Future
public abstract Future<FileLock> lock(long position, long size, boolean shared);
// 異步的獲取指定文件區域的排它鎖 , 無阻塞 ,返回 Future
public final Future<FileLock> lock();
// 嘗試獲取指定文件區域的鎖定 , 如果該文件區域正被鎖定返回 null , 無阻塞
public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
// 嘗試獲取文件的鎖定 , 如果該文件區域正被鎖定返回 null , 無阻塞
public final FileLock tryLock() throws IOException ;
// 異步的將 channel 中的數據讀取到 Buffer 中 , 無阻塞 , 讀取完成或者失敗後會回調 CompletionHandler
public abstract <A> void read(ByteBuffer dst, long position, A attachment,CompletionHandler<Integer,? super A> handler);
// 異步的將 channel 中的數據讀取到 Buffer 中, 無阻塞 ,返回一個 Future
public abstract Future<Integer> read(ByteBuffer dst, long position);
// 異步的將 Buffer 中的數據寫入到 channel 中 , 無阻塞 , 寫入完成或者失敗後會回調 CompletionHandler
public abstract <A> void write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer,? super A> handler);
// 異步的將 Buffer 中的數據寫入到 channel 中 , 無阻塞 ,返回一個 Future
public abstract Future<Integer> write(ByteBuffer src, long position);
}
多路複用 Stream I/O Channel
多路複用 I/O 是同步非阻塞的 I/O 它的優點是可以通過一個線程來處理大量的網絡連接不會阻塞 , 簡單來說可以提高吞吐量 , 告別阻塞式 I/O 的一請求一線程模式,或者是線程池模式。
SelectableChannel 是所有支持多路複用 I/O channel 的基類 , APIs 簡介 :
public abstract class SelectableChannel
extends AbstractInterruptibleChannel
implements Channel
{
protected SelectableChannel() { }
public abstract SelectorProvider provider();
// 獲取該 channel 支持的操作
public abstract int validOps();
// 檢查該 channel 是否註冊
public abstract boolean isRegistered();
// 獲取該 channel 在 Selector 上註冊的選擇鍵
public abstract SelectionKey keyFor(Selector sel);
// channel 將感興趣的操作註冊到選擇器上
public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException;
// channel 將感興趣的操作註冊到選擇器上
public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException;
// 設置 channel 處於阻塞模式或非阻塞模式 , true - 阻塞, false - 非阻塞
public abstract SelectableChannel configureBlocking(boolean block) throws IOException;
// 檢查 channel 是否是阻塞模式
public abstract boolean isBlocking();
// 檢索configureBlocking和register方法同步的對象
public abstract Object blockingLock();
}
從 SelectableChannel 的 API 反映出來一些特性 :
1. channel 都有支持的操作類型;
2. channel 具有註冊到某個選擇器(也稱爲多路複用器) Selector 的能力,註冊的時候要指定一個事件(操作類型) , 這個事件是 channel 感興趣的事件,但並不代表某一時刻正在發生的事件。
3. channel 可以工作在阻塞模式或者是非阻塞模式下 。多路複用 I/O 模型要求 channel 必須工作在非阻塞模式下。
4. channel 註冊到選擇器上後會得到一個 SelectionKey (選擇鍵)。
順着梳理的邏輯繼續學習 , 多路複用器 Selector , Selector APIs 簡介 :
public abstract class Selector implements Closeable {
protected Selector() { }
// 打開一個多路複用器
public static Selector open() throws IOException ;
// 檢查多路複用器是否處於打開狀態
public abstract boolean isOpen();
// 獲取創建此選擇器的提供者
public abstract SelectorProvider provider();
// 獲取該多路複用器中註冊的所有選擇鍵
public abstract Set<SelectionKey> keys();
// 獲取多路複用器中選定的選擇鍵
public abstract Set<SelectionKey> selectedKeys();
// 選擇一組其 channel 已經處於 I/O 就緒狀態的選擇鍵 , 該方法不會阻塞立即返回 , 返回選中的選擇鍵的個數
public abstract int selectNow() throws IOException;
// 選擇一組其 channel 已經處於 I/O 就緒狀態的選擇鍵 ,在阻塞時間到達指定的 timeout 時間後返回
public abstract int select(long timeout);
// 選擇一組其 channel 已經處於 I/O 就緒狀態的選擇鍵 , 該方法會阻塞直到至少有一個 channel 被選中。
// 返回值不表示選中的處於就緒狀態的通道數量,而是處於就緒狀態的通道中就緒狀態已經更新的通道數量 ,
// 所以不能以這個返回值是否爲 0 來判斷是否有處於就緒狀態的通道被選中
public abstract int select() throws IOException;
// 使得尚未返回的第一個選擇操作立即返回
public abstract Selector wakeup();
// 關閉該多路複用器
public abstract void close() throws IOException;
}
多路複用器中有三個 select 方法 , 它們都是用來選擇一組已經處於 I/O 就緒狀態的 channel 。 通過 selectedKeys 可以獲取到被選中的 channel 的 SelectionKey (選擇鍵)。
SelectionKey APIs 簡介 :
public abstract class SelectionKey {
protected SelectionKey() { }
// 獲取選擇鍵對應的 channel
public abstract SelectableChannel channel();
// 獲取選擇鍵對應的多路複用器
public abstract Selector selector();
// 檢查該選擇鍵是否有效
public abstract boolean isValid();
// 取消該選擇鍵 , 該選擇鍵將被多路複用器移除
public abstract void cancel();
// 獲取 channel 註冊的感興趣的事件
public abstract int interestOps();
// 設置感興趣的事件
public abstract SelectionKey interestOps(int ops);
// channel 處於就緒狀態的事件
public abstract int readyOps();
// 讀操作
public static final int OP_READ = 1 << 0;
// 寫操作
public static final int OP_WRITE = 1 << 2;
// 連接操作
public static final int OP_CONNECT = 1 << 3;
// 接受連接操作
public static final int OP_ACCEPT = 1 << 4;
// channel 當前的就緒事件是否是讀操作
public final boolean isReadable();
// channel 當前的就緒事件是否是寫操作
public final boolean isWritable();
// channel 當前的就緒事件是否是連接操作
public final boolean isConnectable();
// channel 當前的就緒事件是否是接受連接操作
public final boolean isAcceptable();
// 將給定對象附加到此鍵
public final Object attach(Object ob);
// 獲取此選擇鍵的附加對象
public final Object attachment();
}
通過 SelectionKey 可以獲取到處於就緒狀態的 channel , 也可以知道 channel 當前處於那種操作的就緒狀態下 , 就可以進行對應的處理。也可以給 channel 註冊新的感興趣的事件類型。基於 Selector, SelectionKey , SelectableChannel 就可以編寫 多路複用 I/O 模型的服務端和客戶端程序了。具體使用的是 ServerSocketChannel , SocketChannel , ServerSocketChannel 需要綁定本機的某個端口以實現網絡 I/O 。SocketChannel 也需要綁定本機的某個端口進行 I/O 不過這個端口是隨機的不需要自己指定,SocketChannel 需要關心的是與服務端建立連接的過程,因爲 TCP 協議是面向連接的傳輸協議。另外 ServerSocketChannel 只支持 Accept 操作 , SocketChannel 支持 Read 、 Write、 Connect 操作。
SocketChannel
非阻塞模式下的 connect() :
如果此通道處於非阻塞模式,則調用方法啓動非阻塞連接操作。如果連接立即建立,就像本地連接可能發生的那樣此方法返回 true。否則,此方法返回false,連接操作必須在稍後通過調用 finishConnect 方法完成。
阻塞模式下的 connect() :
如果此通道處於阻塞模式,則調用方法將阻塞,直到建立連接或I/O錯誤發生。
finishConnect() :
完成連接套接字通道的過程。通過在非阻塞模式中放置套接字通道,然後調用其連接方法來啓動非阻塞連接操作。一旦建立連接,或者嘗試失敗,套接字通道將成爲可連接的,並且可以調用此方法來完成連接序列。如果連接操作失敗,那麼調用此方法將導致適當的 IOException 被拋出。如果此通道已連接,則此方法不會阻塞並立即返回true。如果該通道處於非阻塞模式,那麼如果連接過程尚未完成,該方法將返回false。如果此通道處於阻塞模式,則該方法將阻塞,直到連接完成或失敗,並且總是返回true或拋出描述失敗的檢查異常。此方法可隨時調用。如果在該方法的調用過程中調用該信道上的讀或寫操作,則該操作將首先阻塞,直到完成該調用。如果連接嘗試失敗,也就是說,如果該方法的調用拋出了檢查異常,則該通道將被關閉。
isConnectionPending() :
檢查該通道上的連接操作是否正在進行。
NIO 客戶端服務端示例代碼 :
package net.j4love.nio.channels;
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
/**
* @author he peng
* @create 2018/6/4 18:14
* @see
*/
public class SocketChannelTest {
final SocketAddress socketAddress = new InetSocketAddress("127.0.0.1" , 9999);
public SocketChannelTest() throws IOException {}
// 開啓客戶端
@Test
public void openClientTest0() throws Exception {
SocketChannel sChannel = SocketChannel.open();
sChannel.configureBlocking(false);
Selector selector = Selector.open();
sChannel.register(selector , SelectionKey.OP_CONNECT);
boolean connected = sChannel.connect(socketAddress);
try {
while (sChannel.isOpen() && selector.isOpen()) {
selector.select();
// select() 返回值不表示選中的處於就緒狀態的通道數量,而是處於就緒狀態的通道中就緒狀態已經更新的通道數量 ,
// 所以不能以這個返回值是否爲 0 來判斷是否有處於就緒狀態的通道被選中
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
if (! sk.isValid()) {
System.out.println("selection key is invalid");
continue;
} else if (sk.isAcceptable()) {
System.out.println("selection key is Acceptable");
} else if (sk.isConnectable()) {
System.out.println("selection key is Connectable");
if (! connected) {
SocketChannel channel = (SocketChannel) sk.channel();
if (! channel.isConnected()) {
channel.finishConnect();
channel.register(selector , SelectionKey.OP_WRITE);
System.out.println("connect finished");
}
}
} else if (sk.isReadable()) {
System.out.println("selection key is Readable");
SocketChannel channel = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
sChannel.read(buffer);
buffer.flip();
System.out.println("receive from server message -> " +
new String(buffer.array() , 0 , buffer.limit() , Charset.forName("utf-8")) +
" ,time -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
channel.register(selector , SelectionKey.OP_WRITE);
} else if (sk.isWritable()) {
System.out.println("selection key is Writable");
SocketChannel channel = (SocketChannel) sk.channel();
if (channel.isConnected()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello server".getBytes());
buffer.flip();
channel.write(buffer);
channel.register(selector , SelectionKey.OP_READ);
}
} else {
throw new IllegalStateException("Unknown readyOps");
}
}
}
} finally {
sChannel.close();
selector.close();
}
}
// 開啓服務端
@Test
public void openServerTest0() throws Exception {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ssChannel.bind(socketAddress);
System.out.println("server start in " + socketAddress);
try {
while (ssChannel.isOpen() && selector.isOpen()) {
System.out.println("Select .......");
selector.select();
// select() 返回值不表示選中的處於就緒狀態的通道數量,而是處於就緒狀態的通道中就緒狀態已經更新的通道數量 ,
// 所以不能以這個返回值是否爲 0 來判斷是否有處於就緒狀態的通道被選中
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
if (! sk.isValid()) {
try {
System.out.println("selection key is invalid");
continue;
} catch (Exception e) {
e.printStackTrace();
}
} else if (sk.isAcceptable()) {
try {
System.out.println("selection key is Acceptable");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) sk.channel();
SocketChannel sChannel = serverSocketChannel.accept();
if (Objects.nonNull(sChannel)) {
sChannel.configureBlocking(false);
sChannel.register(selector , SelectionKey.OP_READ);
}
} catch (Exception e) {
e.printStackTrace();
}
} else if (sk.isConnectable()) {
System.out.println("selection key is Connectable");
} else if (sk.isReadable()) {
try {
System.out.println("selection key is Readable");
SocketChannel sChannel = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
sChannel.read(buffer);
buffer.flip();
System.out.println("receive from client (" + sChannel.getRemoteAddress() + ") message -> " +
new String(buffer.array() , 0 , buffer.limit() , Charset.forName("utf-8")) +
" ,time -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
sChannel.register(selector , SelectionKey.OP_WRITE);
} catch (Exception e) {
e.printStackTrace();
}
} else if (sk.isWritable()) {
try {
System.out.println("selection key is Writable");
SocketChannel channel = (SocketChannel) sk.channel();
if (channel.isConnected()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello client".getBytes());
buffer.flip();
channel.write(buffer);
channel.register(selector , SelectionKey.OP_READ);
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
throw new IllegalStateException("Unknown readyOps");
}
}
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
ssChannel.close();
selector.close();
}
}
}
以上代碼是一個粗略的簡單測試 , 沒有處理客戶端斷開的情況 , 以及當多個客戶端都斷開後服務端有時會產生空輪詢的情況 , 也就是 select() 方法並沒有阻塞, 這是 Java NIO 一直存在的 bug :
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719,
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6403933 ,
https://www.cnblogs.com/JAYIT/p/8241634.html
出現這種情況最終導致了機器 CPU 被跑滿, 機器變得巨慢。查閱了一些資料都說是在 linux 平臺上出現, jdk 1.7 已經修復了。但是我是用的是 windows 系統 , jdk 1.8 還是出現這個問題,看來問題並沒有被修復。
"NIO 空輪詢bug" 內容勘誤
由於本人的技術能力有限,對一些技術理解、掌握上有誤造成了對大家的誤導(關於 NIO 空輪詢 bug 部分的內容),十分抱歉。在這裏及時修復錯誤,和大家一起交流學習。Selector 的 select() 函數是會阻塞的,並且一直會阻塞到至少有一個處於就緒狀態的 channel 爲止。 select() 會返回一個 int 類型的值 , 之前我認爲返回的這個值表示選中的 channel 的個數。我在代碼中判斷了這個值是否爲 0 , 如果爲 0 就繼續外層循環, 於是導致了空輪詢的問題 。問題代碼如下 :
while(true) {
System.out.println("Select ......");
int selectedNum = selector.select();
if (selectedNum == 0) {
continue;
}
}
事實上 select() 函數的返回值並不表示選中的 channel 的個數 , 而是選中的 channel 中就緒狀態更新了的個數。所以不能以這個返回值是否爲 0 來判斷是否有處於就緒狀態的通道被選中。 應該以 selectedKeys() 函數的返回值爲準。 另外 selectedKeys() 函數返回的 Set 是線程不安全的 , 需要自己進行同步的處理 。在通信過程中如果客戶端異常斷開,服務端如果不做任何處理(比如說關閉客戶端的通道 , channel.close()) , 那麼服務端會依然認爲這個客戶端的 channel 是可用的、存活狀態,再每次選擇時依然會選中它,只不過在進行 write 、 read 操作的時就會拋出異常 (因爲客戶端實際上已經斷開了)。
查閱了一些資料,根據 netty , jetty 關於這個問題的解決方案做了嘗試,對測試代碼做了改進,嘗試應用 Reactor 模式編寫 NIO 客戶端和服務端代碼 ,因爲是測試代碼爲了看起來直觀點,我將所有的代碼全都寫在了一起 , 沒有進行拆分。
package net.j4love.nio.test;
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author he peng
* @create 2018/6/5 16:07
* @see
*/
public class NioServerTest {
// nio server 測試
static final SocketAddress socketAddress = new InetSocketAddress("127.0.0.1" , 9999);
final Object serverSelectedKeysLock = new Object();
static final int JVM_BUG_THRESHOLD = 5;
final SelectorHolder serverSelectorHolder = new SelectorHolder();
static class SelectorHolder {
private Selector selector;
public synchronized SelectorHolder setSelector(Selector selector) {
this.selector = selector;
return this;
}
public synchronized Selector getSelector() {
return selector;
}
}
// 打開服務端
@Test
public void reactorModelServerTest() throws Exception {
Set<SocketAddress> connectedClients = new HashSet<>();
AtomicInteger bossThreadCount = new AtomicInteger(1);
AtomicInteger workerThreadCount = new AtomicInteger(1);
final ThreadPoolExecutor bossThreadPool = new ThreadPoolExecutor(1, 2,
30, TimeUnit.SECONDS,
new LinkedBlockingQueue() ,
r -> {
Thread t = new Thread(r , "NioBossThread-" + bossThreadCount.getAndIncrement());
t.setDaemon(true);
return t;
} ,
new ThreadPoolExecutor.AbortPolicy());
final ThreadPoolExecutor workerThreadPool = new ThreadPoolExecutor(4, 6,
30, TimeUnit.SECONDS,
new LinkedBlockingQueue() ,
r -> {
Thread t = new Thread(r , "NioWorkerThread-" + workerThreadCount.getAndIncrement());
t.setDaemon(true);
return t;
} ,
new ThreadPoolExecutor.AbortPolicy());
Selector selector = Selector.open();
serverSelectorHolder.setSelector(selector);
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector , SelectionKey.OP_ACCEPT);
ssChannel.bind(socketAddress);
System.out.println("[" + Thread.currentThread().getName() + "]" + "server start in " + socketAddress);
bossThreadPool.execute(() -> {
int jvmBug = 0;
try {
while (ssChannel.isOpen() && serverSelectorHolder.getSelector().isOpen()) {
System.out.println("[" + Thread.currentThread().getName() + "] Select .........");
// select 是線程安全的
int selectedNum = serverSelectorHolder.getSelector().select();
/*if (selectedNum == 0) {
// 解決 nio 空輪訓 bug
jvmBug++;
if (jvmBug > JVM_BUG_THRESHOLD) {
Selector newSelector = Selector.open();
for (SelectionKey sk : serverSelectorHolder.getSelector().keys()) {
if (! sk.isValid() || sk.interestOps() == 0) {
continue;
}
SelectableChannel channel = sk.channel();
if (Objects.nonNull(channel) && channel.isOpen()) {
channel.register(newSelector, sk.interestOps());
}
}
Selector oldSelector = serverSelectorHolder.getSelector();
serverSelectorHolder.setSelector(newSelector);
if (oldSelector.isOpen()) {
oldSelector.close();
}
System.out.println("[" + Thread.currentThread().getName() + "]" + "Fix Nio epoll empty polling bug .......");
jvmBug = 0;
}
continue;
}*/
/*Set<SelectionKey> selectionKeys;
synchronized (serverSelectedKeysLock) {
// selectedKeys 是線程不安全的
// 仔細考慮了一下這裏對線程安全的理解不夠透徹 , 這是在函數內是否真的涉及到了線程安全的問題?
// 這裏 Selector 會被多個線程併發的訪問 , Selector 中存儲了通道
selectionKeys = serverSelectorHolder.getSelector().selectedKeys();
}*/
Set<SelectionKey> selectionKeys = serverSelectorHolder.getSelector().selectedKeys();
if (Objects.isNull(selectionKeys) && selectionKeys.isEmpty()) {
continue;
}
Iterator<SelectionKey> iterator = serverSelectorHolder.getSelector().selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
synchronized (serverSelectorHolder) {
iterator.remove();
}
// work thread
if (! sk.isValid()) {
workerThreadPool.execute(() -> System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is invalid"));
continue;
} else if (sk.isAcceptable()) {
Thread.sleep(100);
workerThreadPool.execute(() -> {
try {
System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is Acceptable");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) sk.channel();
SocketChannel sChannel = serverSocketChannel.accept();
if (Objects.nonNull(sChannel)) {
connectedClients.add(sChannel.getRemoteAddress());
sChannel.configureBlocking(false);
sChannel.register(serverSelectorHolder.getSelector() , SelectionKey.OP_READ);
System.out.println("[" + Thread.currentThread().getName() + "] Accept" + sChannel.getRemoteAddress() + " Connect");
}
} catch (Exception e) {
e.printStackTrace();
}
});
} else if (sk.isConnectable()) {
workerThreadPool.execute(() -> System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is Connectable"));
} else if (sk.isReadable()) {
workerThreadPool.execute(() -> {
SocketChannel sChannel = (SocketChannel) sk.channel();
if (Objects.isNull(sChannel) || ! sChannel.isOpen()) {
return;
}
SocketAddress remoteAddress = null;
try {
remoteAddress = sChannel.getRemoteAddress();
System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is Readable");
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
sChannel.read(buffer);
buffer.flip();
System.out.println("[" + Thread.currentThread().getName() + "] " +
"receive from client (" + sChannel.getRemoteAddress() + ") message -> " +
new String(buffer.array() , 0 , buffer.limit() , Charset.forName("utf-8")) +
" ,time -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
workerThreadPool.execute(() -> {
SocketChannel channel = (SocketChannel) sk.channel();
if (Objects.isNull(channel) || ! channel.isOpen()) {
return;
}
SocketAddress remoteAddress1 = null;
try {
remoteAddress1 = channel.getRemoteAddress();
System.out.println("[" + Thread.currentThread().getName() + "] " + "write to -> " + remoteAddress1);
if (channel.isConnected()) {
ByteBuffer buffer1 = ByteBuffer.allocate(1000);
buffer1.put("hello client".getBytes());
buffer1.flip();
channel.write(buffer1);
}
} catch (Exception e) {
// 異步關閉有點問題 ,channel 沒有被真正關閉掉 ,應該是對線程使用的有問題
// 異步的關閉通道 , 因爲有可能會阻塞
/*SocketAddress remoteAddress2 = remoteAddress1;
workerThreadPool.execute(() -> {
System.out.println("Client (" + remoteAddress2 + ") Error ");
try {
channel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
});*/
System.out.println("Client (" + remoteAddress2 + ") Error ");
try {
channel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
});
} catch (Exception e) {
/*SocketAddress remoteAddress1 = remoteAddress;
workerThreadPool.execute(() -> {
System.out.println("Client (" + remoteAddress1 + ") Error ");
try {
sChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
});*/
System.out.println("Client (" + remoteAddress1 + ") Error ");
try {
sChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
});
} else if (sk.isWritable()) {
System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is Writable");
} else {
workerThreadPool.execute(() -> System.err.println("Unknown readyOps -> " + sk.readyOps()));
}
}
}
} catch (Exception e) {
System.err.println("[" + Thread.currentThread().getName() + "] Server Exception , " +
"Connected Client -> " + connectedClients.size() +
" Client Address -> " + connectedClients);
e.printStackTrace();
} finally {
if (Objects.nonNull(ssChannel)) {
workerThreadPool.execute(() -> {
try {
System.out.println("server channel close");
ssChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
});
}
if (Objects.nonNull(serverSelectorHolder.getSelector())) {
try {
System.out.println("server selector close");
serverSelectorHolder.getSelector().close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.err.println("Exception Terminates the Java Virtual Machine");
System.exit(1);
}
});
System.out.println(Thread.currentThread().getName() + " blocking");
Thread.sleep(Integer.MAX_VALUE);
}
// 打開客戶端
@Test
public void openClientTest() throws Exception {
final SelectorHolder clientSelectorHolder = new SelectorHolder();
SocketChannel sChannel = SocketChannel.open();
sChannel.configureBlocking(false);
Selector oldSelector = Selector.open();
clientSelectorHolder.setSelector(oldSelector);
sChannel.register(clientSelectorHolder.getSelector() , SelectionKey.OP_CONNECT);
boolean connected = sChannel.connect(socketAddress);
int jvmBug = 0;
try {
while (sChannel.isOpen() && clientSelectorHolder.getSelector().isOpen()) {
// 測試 nio 空輪詢 bug
System.out.println("[" + Thread.currentThread().getName() + "] Select .........");
int selectedNum = clientSelectorHolder.getSelector().select();
/*if (selectedNum == 0) {
// 解決 nio 空輪訓 bug
jvmBug++;
if (jvmBug > JVM_BUG_THRESHOLD) {
Selector newSelector = Selector.open();
for (SelectionKey sk : clientSelectorHolder.getSelector().keys()) {
if (! sk.isValid() || sk.interestOps() == 0) {
continue;
}
SelectableChannel channel = sk.channel();
if (Objects.nonNull(channel) && channel.isOpen()) {
channel.register(newSelector, sk.interestOps());
}
}
oldSelector = clientSelectorHolder.getSelector();
clientSelectorHolder.setSelector(newSelector);
if (oldSelector.isOpen()) {
oldSelector.close();
}
System.out.println("Fix Nio epoll empty polling bug ....... keys -> " + newSelector.keys().size());
jvmBug = 0;
}
continue;
}*/
Iterator<SelectionKey> iterator = clientSelectorHolder.getSelector().selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
if (! sk.isValid()) {
System.out.println("selection key is invalid");
continue;
} else if (sk.isAcceptable()) {
System.out.println("selection key is Acceptable");
} else if (sk.isConnectable()) {
System.out.println("selection key is Connectable");
if (! connected) {
SocketChannel channel = (SocketChannel) sk.channel();
if (! channel.isConnected()) {
channel.finishConnect();
channel.register(clientSelectorHolder.getSelector() , SelectionKey.OP_WRITE);
System.out.println(channel.getLocalAddress() + " connection to -> " + channel.getRemoteAddress());
}
}
} else if (sk.isReadable()) {
System.out.println("selection key is Readable");
SocketChannel channel = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
sChannel.read(buffer);
buffer.flip();
System.out.println("receive from server message -> " +
new String(buffer.array() , 0 , buffer.limit() , Charset.forName("utf-8")) +
" ,time -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
if (channel.isConnected()) {
System.out.println("write message to server");
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
buffer1.put(("(" + channel.getLocalAddress() + ") say hello server").getBytes());
buffer1.flip();
channel.write(buffer1);
}
} else if (sk.isWritable()) {
SocketChannel channel = (SocketChannel) sk.channel();
if (channel.isConnected()) {
System.out.println("write message to server");
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(("(" + channel.getLocalAddress() + ") say hello server").getBytes());
buffer.flip();
channel.write(buffer);
channel.register(clientSelectorHolder.getSelector() , SelectionKey.OP_READ);
}
} else {
System.err.println("Unknown readyOps -> " + sk.readyOps());
}
iterator.remove();
}
}
} finally {
Thread t = new Thread("SocketChannelCloseThread") {
@Override
public void run() {
try {
sChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
};
t.setDaemon(true);
t.start();
clientSelectorHolder.getSelector().close();
}
}
}
老實說我不太確認上面編寫的代碼是否符合 Reactor 模式 , 希望有大佬可以交流,希望發現問題的老師指正。測試代碼中有很多的網絡通信過程中的問題都沒有解決 , 比如 TCP 粘包、拆包問題 , 數據的編碼、解碼問題 , 優雅停機問題 , 合適優化的線程模型 等等問題,可以體會到編寫一個商用級別的網絡通信框架類似 netty , mina 還是很複雜的。
Selector 的併發性 :
選擇器對象是線程安全的,但它們包含的鍵集合不是。通過 keys( )和 selectKeys( )返回的鍵的集合是 Selector 對象內部的私有的 Set 對象集合的直接引用。這些集合可能在任意時間被改變。已注 冊 的 鍵 的 集 合 是 只 讀 的 。 如 果 試 圖 修 改 它 , 那 麼 會 得 到 的 一 個java.lang.UnsupportedOperationException,但是當觀察它們的時候,它們可能發生了改變的話,仍然會遇到麻煩。Iterator 對象是快速失敗的(fail-fast):如果底層的 Set 被改變了,它們將會拋出 java.util.ConcurrentModificationException,因此如果在多個線程間共享選擇器和/或鍵,請對此做好準備。可以直接修改選擇鍵,但請注意這麼做時可能會徹底破壞另一個線程的 Iterator。如果在多個線程併發地訪問一個選擇器的鍵的集合的時候存在任何問題,可以採取一些步驟來合理地同步訪問。在執行選擇操作時,選擇器在 Selector 對象上進行同步,然後是已註冊的鍵的集合,最後是已選擇的鍵的集合,按照這樣的順序。已取消的鍵的集合也在選擇過程的的第 1 步和第 3 步之間保持同步(當與已取消的鍵的集合相關的通道被註銷時)。 Selector 類的 close( )方法與 slect( )方法的同步方式是一樣的,因此也有一直阻塞的可能性。在選擇過程還在進行的過程中,所有對 close( )的調用都會被阻塞,直到選擇過程結束,或者執行選擇的線程進入睡眠。在後面的情況下,執行選擇的線程將會在執行關閉的線程獲得鎖是立即被喚醒,並關閉選擇器。
後記
對 NIO Channel 學習的感受是,理論理解起來或許能夠很簡單,也較爲容易理解,但當根據這個理論去實現一個穩定可用的產品時卻是困難重重,需要考慮到很多問題,排除很多阻礙,攻破自己的技術壁壘。要培養技術的廣度和敏感性,在出了問題時能夠快速的聯想到一些解決方案或者是類似的遇到過的問題。比如我在編寫測試代碼時,遇到 Selector 空輪詢問題的時候,我嘗試了一個早晨的時間也沒有能夠解決問題 , 下午突然想起來好像在那一本書中看到過 nio epoll 空輪詢 bug 的問題 , 隨即查閱這方面的資料,結合嘗試實驗可以確定確實是 nio 空輪詢的bug 導致的。想要構建上層產品一定要對底層技術、知識有很好的掌握程度,比如對多線程編程不熟、對網絡傳輸協議不熟是不可能寫出像 netty 這樣的產品的。所以這就是我們應該重視所謂的基礎技術、知識的訓練和學習。希望通過對 Java NIO 的淺顯學習建立了一點學習 netty 、mina 類似網絡通信框架的基礎。