NIO核心Buffer、Selector、Channel分析

NIO核心Buffer、Selector、Channel分析

​ 上一篇文章中我們簡單的說明了BIO、NIO、AIO之間的關係和區別, 本篇文章主要講解NIO核心buffer、selector、channel原理

一、包含知識點

  • Buffer的基本原理
  • 緩衝區的分配
  • 緩衝區分片
  • 只讀緩衝區
  • 直接緩衝區
  • 內存映射
  • 選擇器Selector
  • 通道Channel

二、緩衝區Buffer

2.1 基本原理

​ NIO主要包含Buffer、Selector、Channel三個核心組件, 本小節主要講解buffer。首先看下buffer的類繼承圖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jifh3ale-1592446301659)(./Buffer類繼承圖.jpg)]

​ 緩衝區是一個容器對象, 底層通過數組來實現, 在NIO中, 無論數據的讀取還是寫入都需要先經過緩衝區, 其中最常用的是ByteBuffer, 從上面類繼承圖, Java常用的基本類型都有具體的ByteBuffer和它對應。

​ 緩衝區Buffer底層是通過數組來實現的, 那麼在進行數據的讀取、寫入操作時,它是怎麼記錄緩衝區狀態變化的呢 ?查看Buffer類, 有下面幾個重要字段, 0 <= position <= limit <= capacity

private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
  • position: 數據索引位置,初始化爲0
    • get() 讀取數據時, position表示讀取數據的位置
    • put() 寫入數據時, position表示可以寫入數據的起始位置
  • limit: 剩餘可讀取的數據量或剩餘可插入數據的空間
    • 讀取操作, 表示最大可讀取的數據量
    • 查詢操作, 表示最大可插入數據量
  • capcity: 緩衝區容量, 即底層數組長度

2.2 緩衝區基本操作

​ 首先看下緩衝區基本操作的邏輯代碼

ByteBuffer buffer = ByteBuffer.allocate(10);
//寫入數據
buffer.put((byte)i);
//獲取數據
buffer.get() ;
//刷新緩存區
buffer.flip() ;

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QiGfCb8S-1592446301661)(buffer基本操作position、limit、capacity變化.jpg)]

  • 緩衝區分配

    創建緩衝區對象時, 通過allocate()方法指定緩存的容量, 實際是創建了指定大小的數組, 將其包裝成緩衝區對象

    //1. ByteBuffer.allocate
    public static ByteBuffer allocate(int capacity) {
      if (capacity < 0)
        throw createCapacityException(capacity);
      return new HeapByteBuffer(capacity, capacity);
    }
    
    //2. HeapByteBuffer
    HeapByteBuffer(int cap, int lim) {            // package-private
      super(-1, 0, lim, cap, new byte[cap], 0); // new byte[cap] 創建數組, 作爲緩衝區對象
      /*
            hb = new byte[cap];
            offset = 0;
            */
      this.address = ARRAY_BASE_OFFSET;
    }
    
    //3. ByteBuffer
    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                     byte[] hb, int offset){
      super(mark, pos, lim, cap);
      this.hb = hb; // 緩存區對象
      this.offset = offset;
    }
    
  • put操作

    當通過put()方法向緩衝區添加數據時, 每次添加操作position的值都會自增1,position < limit, 如果position >= limit會拋出BufferOverflowException異常, 添加過程limit、capacity值不變化

    //1. put操作
    public ByteBuffer put(byte x) {
      hb[ix(nextPutIndex())] = x;
      return this;
    }
    
    //2. 獲取下個可添加數據的下標
    final int nextPutIndex() {                          // package-private
      if (position >= limit)
        throw new BufferOverflowException();
      return position++;
    }
    
  • flip操作

    flip有種切換的意思,當需要從寫入操作轉換爲讀取操作時, 需要修改position、limit的值, 前面我們提到了Buffer讀取、寫入是共用position、limit、capacity字段, 不同操作時表示不同意思。

    public Buffer flip() {
      limit = position;
      position = 0;
      mark = -1;
      return this;
    }
    
  • get操作

    在執行flip操作後, position、limit發生了變化, 分別表示可讀取數據的起始位置、緩衝區中可讀數據數量, 每次執行get操作, 都會使position的值增1, position < limit, 如果position >= limit, 會拋出BufferOverflowException異常, 獲取過程中limit、capacity值不發生變化

    //1. get操作
    public byte get() {
      return hb[ix(nextGetIndex())];
    }
    
    //2. 獲取下個可添加數據的下標
    final int nextPutIndex() {                          // package-private
      if (position >= limit)
        throw new BufferOverflowException();
      return position++;
    }
    
  • clear操作

    如果需要對position、limit進行復位操作, 可以執行clear方法

    //1. ByteBuffer.clear()
    ByteBuffer clear() {
      super.clear();
      return this;
    }
    
    //2. Buffer.clear()
    public Buffer clear() {
      position = 0;
      limit = capacity;
      mark = -1;
      return this;
    }
    

2.3 緩衝區分片

​ 在NIO中, 除了可以分配或着包裝緩衝區之外, 還可以基於現有的緩衝區創建一個子緩衝區, 子緩衝區和原緩衝區在底層數據共享, 當共享部分有數據變化時, 子緩衝區與原緩衝區都會發生變化, 下面是測試代碼

public class BufferSlice {  
    static public void main( String args[] ) throws Exception {
        ByteBuffer buffer = ByteBuffer.allocate( 10 );
          
        // 緩衝區中的數據0-9  
        for (int i=0; i<buffer.capacity(); ++i) {  
            buffer.put( (byte)i );  
        }  
        buffer.flip();
        while (buffer.remaining() > 0) { // limit - position
            System.out.print(buffer.get() + "  ");
        }
        System.out.println();
        // 創建子緩衝區  
        buffer.position( 3 );  
        buffer.limit( 7 );  
        ByteBuffer slice = buffer.slice();  
          
        // 改變子緩衝區的內容  
        for (int i=0; i<slice.capacity(); ++i) {  
            byte b = slice.get( i );  // 直接通過索引獲取數據, 沒有通過position來獲取數據
            b *= 10;  
            slice.put( i, b );  
        }
        // 復位
        buffer.position( 0 );  
        buffer.limit( buffer.capacity() );  
          
        while (buffer.remaining()>0) {  
            System.out.print( buffer.get() + "  ");
        }  
    }  
}

2.4 只讀緩衝區

​ 只讀緩衝區如字面意思, 緩存的內容只能讀取,不能進行寫入操作, 可以使用asReadOnlyBuffer方法創建只讀緩衝區, 有下面幾點需要注意

  1. 創建的只讀緩衝區和舊緩衝區共享底層空間
  2. asReadOnlyBuffer創建只讀緩衝區時,position、limit和原緩存一樣, 如果執行get操作需要手動更新讀取數據的位置
/**
 * 只讀緩衝區
 */
public class ReadOnlyBuffer {  
	static public void main( String args[] ) throws Exception {  
		ByteBuffer buffer = ByteBuffer.allocate( 10 );  
	    
		// 緩衝區中的數據0-9  
		for (int i=0; i<buffer.capacity(); ++i) {  
			buffer.put( (byte)i );  
		}  
	
		// 創建只讀緩衝區
		// 基於原buffer創建新的buffer
		//position、limit、capcitu 和原緩存一致
		ByteBuffer readonly = buffer.asReadOnlyBuffer();
	    
		// 改變原緩衝區的內容  
		for (int i=0; i<buffer.capacity(); ++i) {  
			byte b = buffer.get( i );  
			b *= 10;  
			buffer.put( i, b );  
		}

		//position、limit保持和舊緩存值一致
		System.out.println(readonly.position());
		System.out.println(readonly.limit());
		System.out.println();

		readonly.position(0);  
		readonly.limit(buffer.capacity());  
	    
		// 只讀緩衝區的內容也隨之改變  
		while (readonly.remaining()>0) {  
			System.out.print( readonly.get() + "\t");
		}
		System.out.println();

		//修改制度緩存
		readonly.put(0,(byte)-1); //java.nio.ReadOnlyBufferException
	}
}

2.5 直接緩衝區

​ 在2.2節中,我們知道普通分配緩存的方式是通過靜態方法ByteBuffer.allocate()來創建的,如果需要加快IO速度, 可以創建直接緩衝區, 它會在每次調用底層操作系統進行IO操作時, 1) 避免將直接緩衝區中的數據copy到中間緩衝區, 2) 避免從中間緩衝區copy數據到直接緩衝區

//1. 普通創建緩衝區的方式
ByteBuffer buffer = ByteBuffer.allocate(10);

//2. 創建直接緩存的方式
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

public static ByteBuffer allocateDirect(int capacity) {
  return new DirectByteBuffer(capacity);
}

DirectByteBuffer(int cap) {                   // package-private
  super(-1, 0, cap, cap);
  boolean pa = VM.isDirectMemoryPageAligned();
  int ps = Bits.pageSize();
  long size = Math.max(1L, (long)cap + (pa ? ps : 0));
  Bits.reserveMemory(size, cap);

  long base = 0;
  try {
    base = UNSAFE.allocateMemory(size);
  } catch (OutOfMemoryError x) {
    Bits.unreserveMemory(size, cap);
    throw x;
  }
  UNSAFE.setMemory(base, size, (byte) 0);
  if (pa && (base % ps != 0)) {
    // Round up to page boundary
    address = base + ps - (base & (ps - 1));
  } else {
    address = base;
  }
  cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
  att = null;
}

2.6 內存映射

​ 內存映射是一種**讀和寫文件**數據的方法,內存映射文件IO是通過使文件中的數據爲數組的內容完成的,不會將整個文件內容進行讀取, 只有實際讀取或者寫入的部分纔會映射到內存中, 看下示例代碼

public class MappedBuffer {  
    static private final int start = 0;
    static private final int size = 26;
      
    static public void main( String args[] ) throws Exception {
        String rootPath = MappedBuffer.class.getClassLoader().getResource("").getPath();

        RandomAccessFile raf = new RandomAccessFile( rootPath + "test.txt", "rw" );
        FileChannel fc = raf.getChannel();
        
        //把緩衝區跟文件系統進行一個映射關聯
        //只要操作緩衝區裏面的內容,文件內容也會跟着改變
        MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,start, size );
          
        mbb.put( 0, (byte)97 );  //a
        mbb.put( 25, (byte)122 );   //z

        raf.close();  
    }  
}

三、選擇器Selector

3.1 傳統會話模式TPR(Thread Per Request)

​ 傳統的Server/Client會話模式是TPR(Thread Per Request),

  1. 服務端會爲每個請求創建一個新的Thread來處理邏輯, 如果請求併發過大,會同時創建過多的線程來處理Client的請求, 大量的線程會增加服務器壓力, 容易帶來性能問題
  2. 爲了解決線程的不斷增長, 通常會使用線程池來控制線程數量上限, 但是可能會帶來新的問題, 如果線程池線程都在處理耗時操作, 比如: 文件上傳、下載操作, 如果有個耗時很短的請求過來,會被阻塞不能及時被處理

3.2 Reactor模式

​ NiO通過Reactor模式來實現非阻塞需求, IO調用不會被阻塞, 只會註冊特定的IO操作, 當特定的事件到來時會發出通知, NIO基於Selector實現非阻塞IO,當有讀或者寫等任何註冊事件發生時, 可以從Selector中獲得相應SelectionKey, 通過這個SelectionKey找到發生事件的SelectableChannel, 然後獲得客戶端發送過來的數據。處理的基本順序可以分爲下面幾個步驟

  • 向 Selector 對象註冊感興趣的事件
  • 從 Selector 中獲取感興趣的事件
  • 根據不同的事件進行相應的處理

下面是Reactor模式圖示

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2qHZMZyl-1592446301662)(Reactor模式圖.jpg)]

下面通過下面的示例代碼熟悉Selector

package com.gupaoedu.vip.netty.io.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
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.util.Iterator;
import java.util.Set;

/**
 * NIO的操作過於繁瑣,於是纔有了Netty
 * Netty就是對這一系列非常繁瑣的操作進行了封裝
 *
 * Created by Tom.
 */
public class NIOServerDemo {

    private int port = 8080;

    //輪詢器 Selector 
    private Selector selector;
    //緩衝區 Buffer 
    private ByteBuffer buffer = ByteBuffer.allocate(1024);

    //初始化完畢
    public NIOServerDemo(int port){
        try {
            this.port = port;
            ServerSocketChannel server = ServerSocketChannel.open();
            //IP/Port
            server.bind(new InetSocketAddress(this.port));
            //BIO 升級版本 NIO,爲了兼容BIO,NIO模型默認是採用阻塞式
            server.configureBlocking(false);
            selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void listen(){
        System.out.println("listen on " + this.port + ".");
        try {
            //輪詢主線程
            while (true){
                //大堂經理再叫號
                selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iter = keys.iterator();
                //不斷地迭代,就叫輪詢
                //同步體現在這裏,因爲每次只能拿一個key,每次只能處理一種狀態
                while (iter.hasNext()){
                    SelectionKey key = iter.next();
                    iter.remove();
                    //每一個key代表一種狀態
                    //沒一個號對應一個業務
                    //數據就緒、數據可讀、數據可寫
                    process(key);
                }             
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //每一次輪詢就是調用一次process方法,而每一次調用,只能幹一件事
    //在同一時間點,只能幹一件事
    private void process(SelectionKey key) throws IOException {
        //針對於每一種狀態給一個反應
        if(key.isAcceptable()){
            ServerSocketChannel server = (ServerSocketChannel)key.channel();
            //這個方法體現非阻塞,不管你數據有沒有準備好
            //你給我一個狀態和反饋
            SocketChannel channel = server.accept();
            //一定一定要記得設置爲非阻塞
            channel.configureBlocking(false);
            //當數據準備就緒的時候,將狀態改爲可讀
            key = channel.register(selector,SelectionKey.OP_READ);
        }
        else if(key.isReadable()){
            //key.channel 從多路複用器中拿到客戶端的引用
            SocketChannel channel = (SocketChannel)key.channel();
            int len = channel.read(buffer);
            if(len > 0){
                buffer.flip();
                String content = new String(buffer.array(),0,len);
                key = channel.register(selector,SelectionKey.OP_WRITE);
                //在key上攜帶一個附件,一會再寫出去
                key.attach(content);
                System.out.println("讀取內容:" + content);
            }
        }
        else if(key.isWritable()){
            SocketChannel channel = (SocketChannel)key.channel();

            String content = (String)key.attachment();
            channel.write(ByteBuffer.wrap(("輸出:" + content).getBytes()));

            channel.close();
        }
    }

    public static void main(String[] args) {
        new NIOServerDemo(8080).listen();
    }
}

四、Channel

4.1 channel基本知識

​ 通道是一個對象, 數據的讀取和寫入都需要經過通道(Channel),但是需要注意的是數據的直接操作並不是Channel而是Buffer, NIO中提供了多種通道對象, 請看下圖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-CH2gHQG7-1592446301664)(Channel類繼承關係.jpg)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pKFjDKep-1592446301665)(Selector處理事件.jpg)]

使用 NIO 讀取數據

  • 從 FileInputStream 獲取 Channel
  • 創建 Buffer
  • 將數據從 Channel 讀取到 Buffer 中

使用 NIO 寫入數據

  • 從 FileInputStream 獲取 Channel
  • 創建 Buffer
  • 將數據從 Channel 寫入到 Buffer 中

4.2 IO多路複用

​ 多路複用 IO 技術最適用的是“高併發”場景,以滿足 短時間內至少同時有上千個連接請求準備好。其他情

況下多路複用 IO 技術發揮不出來它的優勢。另一方面,使用 JAVA NIO 進行功能實現,相對於傳統的 Socket 套接字

實現要複雜一些,所以實際應用中,需要根據自己的業務需求進行技術選擇。

​ 常見多路複用技術

IO模型 相對性能 關鍵思路 操作系統 Java支持
select 較高 Reactor Win/Linux 支持,Reactor 模式(反應器設計模式)。Linux 操作系統的 kernels 2.4 內核版本之前,默認使用select;而目前 windows 下對同步 IO 的支持,都是 select 模型
pool 較高 Reactor Linux Linux 下的 JAVA NIO 框架,Linux kernels 2.6 內核版本之前使用 poll 進行支持。也是使用的Reactor 模式
epoll Reactor/Proactor Linux Linux kernels 2.6 內核版本及以後使用 epoll 進行支持;Linux kernels 2.6 內核版本之前使用 poll進行支持;另外一定注意,由於 Linux 下沒有Windows 下的 IOCP 技術提供真正的 異步 IO 支持,所以 Linux 下使用 epoll 模擬異步 IO
Kqueue Proactor Linux

五、NIO源碼分析

​ 在3.2節中我們知道, Selector的創建方式, 那麼它具體實現邏輯是什麼呢 ?

  • 創建Provider
//Selector
public static Selector open() throws IOException {
  return SelectorProvider.provider().openSelector(); // 創建Selector入口
}

//SelectorProvider
public static SelectorProvider provider() {
  synchronized (lock) { // 加鎖避免併發問題
    if (provider != null)
      return provider; // 如果已經創建, 返回創建好的provider , 保證整個Server只有一個provider
    return AccessController.doPrivileged(
      new PrivilegedAction<>() {
        public SelectorProvider run() {
          if (loadProviderFromProperty()) //從配置屬性java.nio.channels.spi.SelectorProvider家在provider
            return provider;
          if (loadProviderAsService()) // 通過SPI機制家在provider
            return provider;
          /** 
           * 這裏以PollSelectorImpl爲例說明, 注意這裏會根據不同操作系統創建不同的Provider
           * 1. windows, WindowSelectorProvider
           * 2. Linux: EpoolSelectorProvider
           * 可以將 ${JAVA_HOME}/lib/rt.jar 進行解壓, 解壓後進入 sun.nio.ch目錄, 查看DefaultSelectorProvider.class文件, 
           * 內容實現會根據系統不一樣創建對應的Provider
           */
          provider = sun.nio.ch.DefaultSelectorProvider.create(); // 創建一個基於當前操作系統的provider
          return provider;
        }
      });
  }
}
  • ​ 創建Selector

    查看DefaultSelectorProvider類, 當需要Selector對象時, provider通過openSelector創建對應的selector對象

//DefaultSelectorProvider
public static SelectorProvider create() {
  /** 
   * 這裏以PollSelectorImpl爲例說明, 注意這裏會根據不同操作系統創建不同的Provider
   */
  return new PollSelectorProvider(); // 這裏以PollSelectorImpl爲例說明
}

//PollSelectorImpl
PollSelectorImpl(SelectorProvider var1) {
  super(var1, 1, 1);
  long var2 = IOUtil.makePipe(false); //native方法, 返回文件兩個描述符, 用long來存儲
  this.fd0 = (int)(var2 >>> 32); //高位, read描述符
  this.fd1 = (int)var2; //低位, write描述符

  try {
    this.pollWrapper = new PollArrayWrapper(10);
    this.pollWrapper.initInterrupt(this.fd0, this.fd1);
    this.channelArray = new SelectionKeyImpl[10];
  } catch (Throwable var8) {
    try {
      FileDispatcherImpl.closeIntFD(this.fd0);
    } catch (IOException var7) {
      var8.addSuppressed(var7);
    }

    try {
      FileDispatcherImpl.closeIntFD(this.fd1);
    } catch (IOException var6) {
      var8.addSuppressed(var6);
    }

    throw var8;
  }
}
  • 創建Pipe

    如果selector需要打開創建pipe, 可以通過openPipe來創建Pipe對象

//SelectorProviderImpl
public Pipe openPipe() throws IOException {
  return new PipeImpl(this);
}

//PipeImpl
PipeImpl(SelectorProvider var1) {
  long var2 = IOUtil.makePipe(true); //native方法, 返回文件兩個描述符, 用long來存儲
  int var4 = (int)(var2 >>> 32); //高位, read描述符
  int var5 = (int)var2; //低位, write描述符
  FileDescriptor var6 = new FileDescriptor();
  IOUtil.setfdVal(var6, var4);
  this.source = new SourceChannelImpl(var1, var6);
  FileDescriptor var7 = new FileDescriptor();
  IOUtil.setfdVal(var7, var5);
  this.sink = new SinkChannelImpl(var1, var7);
}
  • 創建Channel

    這裏以創建ServerSocketChannel說明, 從provider創建流程我們知道, 一個應用只有一個provider, Channel的創建是通過這個provider創建的

//SelectorProviderImpl
public ServerSocketChannel openServerSocketChannel() throws IOException {
  return new ServerSocketChannelImpl(this);
}

//ServerSocketChannelImpl
ServerSocketChannelImpl(SelectorProvider var1) throws IOException {
  super(var1);
  this.fd = Net.serverSocket(true);
  this.fdVal = IOUtil.fdVal(this.fd);
  this.state = 0;
}

//AbstractSelectableChannel
protected AbstractSelectableChannel(SelectorProvider provider) {
	this.provider = provider;
}
  • ServerSocketChannel.register()

    從3.2節示例代碼我們知道, channel和selector通過 channel.register(selector,SelectionKey.OP_READ) 綁定在一起, 即創建ServerSocketChannel時創建的FD和selector綁定在一起。

//AbstractSelectableChannel
public final SelectionKey register(Selector sel, int ops,
                                       Object att)
        throws ClosedChannelException
    {
        synchronized (regLock) {
            if (!isOpen()) // 管道是否已經打開, false拋錯
                throw new ClosedChannelException();
            if ((ops & ~validOps()) != 0) // 是否支持當前 SelectionKey, 如果不支持,拋錯
                throw new IllegalArgumentException();
            if (blocking)
                throw new IllegalBlockingModeException();
            SelectionKey k = findKey(sel); // 遍歷SelectionKey集合, 找到屬於當前selector的SelectorKey
            if (k != null) {// 查詢到key, 更新selector
                k.interestOps(ops);
                k.attach(att);
            }
            if (k == null) { // 沒查詢到, 新註冊,將Channel和Selector綁定
                // New registration
                synchronized (keyLock) {
                    if (!isOpen())
                        throw new ClosedChannelException();
                    k = ((AbstractSelector)sel).register(this, ops, att);
                    addKey(k);
                }
            }
            return k;
        }
    }
  • Selector.doSelect()
protected int doSelect(long var1) throws IOException {
        if (this.channelArray == null) {// 沒有SelectorKey信息, 拋異常
            throw new ClosedSelectorException();
        } else {
            this.processDeregisterQueue();

            try {
                this.begin();
                this.pollWrapper.poll(this.totalChannels, 0, var1); // 核心方法, 輪詢pollWrapper中保存的FD
            } finally {
                this.end();
            }

            this.processDeregisterQueue();
            int var3 = this.updateSelectedKeys(); // 更新SelectedKey
            if (this.pollWrapper.getReventOps(0) != 0) {
                this.pollWrapper.putReventOps(0, 0);
                synchronized(this.interruptLock) {
                    IOUtil.drain(this.fd0);
                    this.interruptTriggered = false;
                }
            }

            return var3;
        }
    }

//PollArrayWrapper
int poll(int var1, int var2, long var3) {
  return this.poll0(this.pollArrayAddress + (long)(var2 * 8), var1, var3);
}

private native int poll0(long var1, int var3, long var4);

這個 poll0()會監聽 pollWrapper 中的 FD 有沒有數據進出,這會造成 IO 阻塞,直到有數據讀寫事件發生。比如,由於 pollWrapper 中保存的也有 ServerSocketChannel 的 FD,所以只要 ClientSocket 發一份數據到 ServerSocket,那麼 poll0() 就會返回;又由於 pollWrapper 中保存的也有 pipe 的 write 端的 FD,所以只要 pipe 的 write 端向 FD 發一份數據,也會造 成 poll0()返回;如果這兩種情況都沒有發生,那麼 poll0()就一直阻塞,也就是 selector.select()會一直阻塞;如果有任 何一種情況發生,那麼 selector.select()就會返回,所有在 OperationServer 的 run()裏要用 while (true) {,這樣就可以保證在 selector 接收到數據並處理完後繼續監聽 poll();

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章