NIO核心之Channel,Buffer和Selector简介
在NIO的API中,Channel就是實現非阻塞的組件,而事件分發(Dispatcher)使用的是Selector組件,在傳統的I/O流(Stream)是有方向的,而NIO支持雙向讀寫,這樣就需要將流中的數據讀取到某個緩沖組件里,即Buffer組件.Buffer組件還有個特殊的實現DirectByteBuffer, 可以申請堆外內存,關于為什么要申請堆外內存后續會談。
一、Channel(對比于原生的javaAPI,channel是可以雙向的通道)
Channel是NIO中用來實現非阻塞數據操作的橋梁,筆者猜測是借鑒的Berkly Socket的設計,代表某種通道,和I/O Stream只支持讀或者寫(單向)不一樣,Channel同時支持讀和寫, 但是只能讀和寫到Buffer中,因為,支持了非阻塞,讀出的數據要找個地方臨時存放.
Channel主要實現有:
基本類圖如下:
以上Channel涵蓋了文件,TCP, UDP網絡的支持, 也是我們用的最多的。
Channel都不是手動new出來的,基本都是用靜態方法Open出來的,或者從BIO的Stream里封裝得到的(本質上也是調用某Channel的open方法)。
比如使用FileChannel來讀寫文件的一個例子:
/*** 測試FileChannel模擬傳統IO用竹筒多次取水的過程* * @author sound2gd**/ public class FileChannelTest2 {public static void main(String[] args) throws Exception{FileInputStream sr = new FileInputStream("src/com/cris/chapter15/f6/FileChannelTest2.java");FileChannel fc = sr.getChannel();ByteBuffer bf = ByteBuffer.allocate(256);//創建Charset對象Charset charset = Charset.forName("UTF-8");CharsetDecoder decoder = charset.newDecoder();while((fc.read(bf))!=-1){//鎖定Buffer的空白區bf.flip();//轉碼CharBuffer cbuff = decoder.decode(bf);System.out.print(cbuff);//buffer初始化,用于下一次讀取bf.clear();}} }用法還是比較簡單的,從Channel讀數據到Buffer用read, 從Buffer寫數據到Channel用write這里的FileChannel就是從FileInputStream上得到的, 查看其源碼:
public FileChannel getChannel() {synchronized (this) {if (channel == null) {channel = FileChannelImpl.open(fd, path, true, false, this);}return channel;} }可以看到,還是調用了FileChannelImpl的open方法
上面的類圖還可以看到ScatteringByteChannel和GatheringByteChannel,它們分別代表Scatter和Gather操作
- Scatter是分散操作,可以將一個Channel里的數據讀取到多個Buffer
- Gather是聚合操作,可以將多個Buffer的數據讀取到一個Channel
在網絡編程中這倆是常用操作,比如http協議的解析通常會將header和body分散到倆Buffer,方便后續處理。Scatter和Gather的細節限于篇幅不展開敘述,感興趣的讀者可以自行了解
二、 Buffer
Buffer是一個容器,本質上就是一個數組.用于接受從Channel里傳過來的數據
Buffer的實現常見有:
看名字就知道是存放什么類型數據的Buffer.
Buffer的創建時通過Buffer類的靜態方法來創建的。 Buffer有三個核心概念:
- position:位置,用于指明下一個可以被讀出的或者寫入的緩沖區位置索引
- limit:界限,第一個不應該被讀出或者寫入的緩沖區位置索引
- capacity:容量,創建后不能改變
Buffer類有一個實例方法:flip()。其作用是將limit設置為position所在的位置,然后將position置為0 ,這就使得Buffer的讀寫指針又回到了開始位置。 clear()方法就是將position置為0,limit置為capacity.
為啥要有這種操作?因為Buffer是支持讀和寫的,寫完了給別的地方用就要flip, 免得數據處理出錯
下面以CharBuffer為例舉個簡單的例子
輸出結果請讀者自行理解下
上面還有個特殊的類,就是這個DirectByteBuffer, 這個是有名的冰山對象,分配DirectByteBuffer的時候,JVM是申請一塊直接內存(堆外), 然后地址關聯到DirectByteBufer里的address。它的回收器sun.misc.Cleaner使用的是虛引用, 當DirectByteBuffer被回收的時候,其關聯的堆外內存也會使用Unsafe釋放掉。雖然在DireactByteBuffer堆內占用內存少,但是可能關聯一塊非常大的堆外內存,和冰山一樣,所以稱為冰山對象。后面還會對DirectByteBuffer進行解析,這個是NIO的一個重要feature之一
三、Selector
-
Selector是NIO中用來實現事件分發的組件,受AWT線程的啟發,用于接收I/O事件并分發到合適的處理器。
-
Selector底層使用的依然是操作系統的select,poll和epoll系統調用,支持使用一個線程來監聽多個fd的I/O事件,也即前面講的I/O多路復用模型.
-
Selector可以同時監控多個SelectableChannel的IO狀況,是非阻塞IO的核心,一個Selector 有三個SelectionKey集合
所有的SelectionKey集合,代表了注冊在該Selector上的Channel。
- 被選擇的SelectionKey集合:代表了所有可以通過select 方法獲取的,需要進行IO處理的Channel。
- 被取消的SelectionKey集合:代表了所有被取消注冊關系的Channel,在下次執行select方法時。這些Channel對應的SelectKey會被徹底刪除
SelectableChannel代表可以支持非阻塞IO操作的Channel對象,它可以被注冊到Selector上, 這種注冊關系由SelectionKey實例表示
下面舉個聊天室的例子
import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets;/*** 使用NIO來實現聊天室*/ public class NServer {// 用于檢測所有Channel狀態的selectorprivate Selector selector = null;// 定義實現編碼,解碼的字符集對象private Charset charset = StandardCharsets.UTF_8;public void init() throws Exception {selector = Selector.open();//通過open方法來打開一個未綁定的ServerSocketChannel實例ServerSocketChannel server = ServerSocketChannel.open();InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 8888);// 綁定到指定地址server.bind(isa);// 設置以非阻塞的方式工作server.configureBlocking(false);// 將Server注冊到指定的Selector對象server.register(selector, SelectionKey.OP_ACCEPT);while (selector.select() > 0) {// 依次處理selector上的已選擇的SelectionKeyfor (SelectionKey sk : selector.selectedKeys()) {//從selector上的已選擇key集中刪除正在處理的SelectionKeyselector.selectedKeys().remove(sk);//如果sk對應的Channel包含客戶端的連接請求if (sk.isAcceptable()) {//調用accept方法接受此連接,產生服務器端的SocketChannelSocketChannel accept = server.accept();//采用非阻塞模式accept.configureBlocking(false);//將該SocketChannel注冊到selectoraccept.register(selector, SelectionKey.OP_READ);//將sk對應的Channel設置成準備接受其他請求sk.interestOps(SelectionKey.OP_ACCEPT);}// 如果sk對應的Channel有數據需要讀取if (sk.isReadable()) {// 獲取該SelctionKey對應的Channel,該Channel有可讀的數據SocketChannel channel = (SocketChannel) sk.channel();//定義準備執行讀取數據的ByteBufferByteBuffer buffer = ByteBuffer.allocate(1024);String content = "";//開始讀取數據try {while (channel.read(buffer) > 0) {buffer.flip();content += charset.decode(buffer);}//打印從該SK對應的Channel讀取到的數據System.out.println("讀取的數據" + content);//將sk對應的channel設置成準備下一次讀取sk.interestOps(SelectionKey.OP_READ);} catch (Exception e) {//如果捕獲到了該SK對應的Channel出現了異常,即表明//該Channel對應的Client出現了問題,所以從selctor中取消該Sk的注冊sk.cancel();if (sk.channel() != null) {sk.channel().close();}}//如果content的長度大于0,即聊天信息不為空,if (content.length() > 0) {//遍歷該selecor里注冊的所有SelectionKeyfor (SelectionKey key : selector.keys()) {//獲取該key對應的channelSelectableChannel target = key.channel();//如果該Channel是SocketChannel對象if (target instanceof SocketChannel) {// 將讀取到的內容寫入該channel中SocketChannel dest = (SocketChannel) target;dest.write(charset.encode(content));}}}}}}}public static void main(String[] args) {try {new NServer().init();} catch (Exception e) {e.printStackTrace();}}}使用nc localhost 8888就可以測試了,多開幾個終端以模擬多個客戶端。
這個例子里使用了ServerSocketChannel,類似于BIO中ServerSocket,用于監聽某個地址和端口, 是TCP服務端的代表.同時還可以看到accept之后得到了一個SocketChannel, 代表一個TCP socket通道.
我們均使用了非阻塞模式, 在read的時候如果讀取的數據不夠,也不會阻塞調用線程。
本文介紹了NIO的核心Channel, Buffer和Selector,它們的設計意圖和解決的問題,同時舉了些簡單的例子來說明用法。NIO的根本還是I/O多路復用, 操作系統告訴你哪個fd可讀可寫,內核幫你做了Event Loop,比在應用層用戶空間做無疑是提升了太多的。
本文轉自
總結
以上是生活随笔為你收集整理的NIO核心之Channel,Buffer和Selector简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分享一篇关于饿了么的需求文档
- 下一篇: 新一代客服系统