1. 程式人生 > 實用技巧 >IO工作機制

IO工作機制

一,磁碟IO工作機制

1.1 訪問檔案的方式

1.標註訪問:通過系統呼叫read和write函式;從磁碟複製到核心空間,在複製到使用者空間,非常耗費時間,因此在核心空間中存在一個快取機制。

2.直接iO方式:應用程式直接訪問磁碟,而不經過核心空間快取區。如果程式快取中沒有,在訪問核心空間的快取,這樣速度非常低。通常是直接和非同步相結合,會取得比較好的效能。

3.同步訪問檔案的方式:就是資料的讀寫是同步的,與方式一不同的是,只有資料被成功寫入磁碟時才返回給應用成功的標誌。這種方式效能較差,一般用於安全性比較高的場合。

4.非同步訪問方式:當訪問資料的執行緒放出請求後,會接著處理其他的事情,不會阻塞等待,當返回資料時才會繼續處理接下來的操作

5.記憶體對映的方式:作業系統將一塊記憶體區域與磁碟中的檔案關聯起來,當腰訪問記憶體中的一段資料時轉換為訪問檔案的某一段資料。目的還是減少核心空間到使用者空間資料的複製次數,因為這兩個空間時共享的

1.2 java訪問磁碟檔案

在java中File通常並不代表一個真實存在的檔案,當你指定一個路徑描述符是,返回一個代表這個路徑的虛擬物件,這可能是一個真實存在的檔案或者包含多個檔案的目錄。

1.3 物件序列化技術

java序列化就是指講一個物件轉換為一串二進位制表示的位元組陣列,通過儲存或者轉義這些位元組資料來達到持久化的目的。

在純java環境下,java序列化能夠很好的進行工作,但是在多語言環境下,用java序列化儲存後,很難用其他語言還原出來,因此應該儲存通用的資料結構,如json

或者xml

二,網路IO工作機制

2.1 TCP狀態轉化

2.2 影響網路傳輸的因素:網路頻寬,傳輸距離,TCP擁塞控制

2.3 Java Socket的工機制

Socket比作城市之間的交通工具。大部分使用的是流套接字,是一種穩定的通訊協議。通過Socket唯一代表一個主機上的應用程式的通訊鏈路了

2.4 建立通訊鏈路

2.5 傳輸資料

三,NIO的工作方式

3.1 BIO : 即阻塞IO,不管是磁碟或者網路IO,資料在寫入OutputStream或者從InputStream讀取時,都有可能會阻塞,一旦阻塞就會失去CPU的使用權,這在當前的大規模訪問量和有效能要求的情況下,是不能接受的,因此需要新的方式

3.2 NIO的工作機制

Channel和Selector是NIO的兩個核心概念。可以把Channel比作某種具體的交通工具,而Selector負責鍵控每個交通工具的當前執行狀態,也及時說它可以輪詢每個Channel的狀態。

Selector一般稱為選擇器,也可以翻譯為多路複用器,主要功能是用於檢查一個或者多個NIO Channel(通道)的狀態是否處於可讀、可寫。如此可以實現單執行緒管理多個Channel(通道),當然也可以管理多個網路連線。使用Selector的好處在於,可以使用更少的執行緒來處理更多的通道,相比使用更多的執行緒,避免了執行緒上下文切換帶來的開銷等。

  1.建立:通過呼叫靜態工廠方法Selector.open()方法建立一個Selector物件,open()方法實際上是向SPI1發出請求,通過預設的SelectorProvider物件獲取一個新的Selector例項,如:

Selector selector = Selector.open();
  2.註冊Channel到Selector
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

  程式碼的第一句就是讓這個Channel(通道)是非阻塞的。它是SelectableChannel抽象類裡的方法,用於使通道處於阻塞模式或非阻塞模式,false表示非阻塞,true表示阻塞

  要想Channel註冊到Selector中,那麼這個Channel必須是非阻塞的。所以FileChannel不適合Selector,因為FileChannel不能切換為非阻塞模式,更準確的說是因為FileChannel沒有繼承SelectableChannel。但是SocketChannel可以正常使用。

  程式碼的第二行,register()方法就是將通道註冊到Selector中,並且讓Selector監聽感興趣的事件(第二個引數)。

  著重講一下第二個引數,它是一個“interest集合”,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽四種不同型別的事件:Connect、Accept、Read、Write。

  ❤ Connect:成功連線到另一個伺服器稱為“連線就緒”;

  ❤ Accept:ServerSocketChannel準備好接收新進入的連線稱為“接收就緒”;

  ❤ Read:有資料可讀的通道稱為“讀就緒”;

  ❤ Write:等待寫資料的通道稱為“寫就緒”;

  上面這四種事件用SelectionKey的四個常量來表示:

SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

如果你對不止一種事件感興趣,可以使用或( | )運算子來操作:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

  3.SelectionKey

  一個SelectionKey鍵表示了一個特定的通道物件和一個特定的選擇器物件之間的註冊關係。

key.attachment(); //返回SelectionKey的attachment,attachment可以在註冊channel的時候指定。
key.channel(); // 返回該SelectionKey對應的channel。
key.selector(); // 返回該SelectionKey對應的Selector。
key.interestOps(); //返回代表需要Selector監控的IO操作的bit mask
key.readyOps(); // 返回一個bit mask,代表在相應channel上可以進行的IO操作。

  (1)key.interestOps():

   通過這個方法來判斷Selector是否對Channel的某種事件感興趣;

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

  (2)key.readyOps():

   ready set是通道已經準備就緒的操作的集合。在一次選Selection之後,你應該會首先訪問這個ready set。Java中定義了以下幾個方法來檢查這些操作是否就緒:

   //建立ready集合的方法
    int readySet = selectionKey.readyOps();
    //檢查這些操作是否就緒的方法
    selectionKey.isAcceptable();//等價於selectionKey.readyOps()&SelectionKey.OP_ACCEPT
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();

  (3)key.attachment():

   可以將一個物件或者更多資訊附著到SelectionKey上,這樣就能方便的識別某個特定的通道。例如,可以附加與通道一起使用的Buffer,或者包含聚集資料的某個物件。如:

    key.attach(theObject);
    Object attachedObj = key.attachment();

    還可以在register()方法使用的時候(即Selector註冊Channel的時候)附加物件:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

  (4)key.channel()和key.selector() :

   取出SelectionKey關聯的Channel和Selector;

    Channel channel = key.channel();
    Selector selector = key.selector();

  4.Selector中的Channel

  選擇器維護註冊過的通道,這種選擇器與通道的註冊關係被封裝在SelectionKey中

public abstract class Selector
{
    ...
    public abstract Set keys();
    public abstract Set selectedKeys();
    public abstract int select() throws IOException;
    public abstract int select(long timeout) throws IOException;
    public abstract int selectNow() throws IOException;
    public abstract void wakeup();
    ...   
}

  Selector維護的三種類型SelectionKey集合:

  (1)已註冊的鍵的集合(Registered key set)

    所有與選擇器關聯的通道所生成的鍵的集合稱為已註冊鍵的集合。這個集合通過keys()方法返回,並且有可能是空的。

    注意:並不是所有註冊過的鍵都有效。同時已註冊鍵的集合是不可以直接修改的,若這麼做的話,將會丟擲ava.lang.UnsupportedOperationException 異常。

  (2)已選擇鍵的集合(Selected key set)

    已註冊鍵的集合的子集,這個集合的每個成員都是相關的通道被選擇器判斷為已經準備好的並且包含於鍵的interest集合中的操作。這個集合通過selectedKeys()方法返回(有可能是空的)。

    注意:這些鍵可以直接從這個集合中移除,但是不能新增。若這麼做的話將會丟擲java.lang.UnsupportedOperationException異常。

  (3)已取消鍵的集合(Cancelled key set)

    已註冊鍵的集合的子集,這個集合包含了cancel()方法被呼叫過的鍵(這個鍵已經被無效化),但他們還沒有被登出。這個集合是選擇器物件的私有成員,因而無法直接訪問。

    注意:當鍵被取消(可以通過isValid()方法來判斷)時,它將被放在相關的選擇器的已取消的鍵的集合裡。註冊不會立即被取消,但鍵會立即失效。當再次呼叫select()方法時(或者一個正在進行的select()呼叫結束時),已取消的鍵的·集合中的被取消的鍵將會被清理掉,並且相應的登出也將會完成。通道會被登出,新的SelectionKey將被返回。當通道關閉時,所有相關的鍵會自動取消(一個通道可以被註冊到多個選擇器上)。當選擇器關閉時,所有被註冊到該選擇器的通道都將被登出,並且相應的鍵將立即被無效化(取消),一旦鍵被無效化,呼叫它的與相關的方法就將丟擲CancelledKeyException 異常。

  5.select()方法

  在剛初始化的Selector物件中,上面講述的三個集合都是空的。通過Selector的select()方法可以選擇已經準備就緒的通道(這些通道包含你感興趣的事件)。比如你對讀就緒的通道感興趣,那麼select()方法就會返回讀事件已經就緒的那些通道。下面是Selector過載的幾個select()方法:

    ❤ int select():阻塞,至少有一個通道在你註冊的事件上就緒了;

    ❤ int select(long timeout):和select()一樣,但最長阻塞時間為timeout毫秒;

    ❤ int selectNow():非阻塞,執行就緒檢查過程,但不阻塞,如果當前沒有通道就緒,立刻返回0;

  select()方法返回的int值表示有多少通道已經就緒,是自上次呼叫select()方法後有多少通道變成就緒狀態。之前在呼叫select()時進入就緒的通道不會在本次呼叫中被計入,

  而在前一次select()呼叫進入就緒但現在已經不在於就緒狀態的通道也不會被計入。例如:首次呼叫select()方法,如果有一個通道變成了就緒狀態,返回了1,若再次呼叫select()方法,

  如果一個另一個通道就緒了,它會再次返回1.如果對第一個就緒的Channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法呼叫之間,只有一個通道就緒了。

  一旦呼叫了select()方法,並且返回值不為0時,則可以通過呼叫Selector的selectedKeys()方法來訪問已選擇鍵的集合。如下:

 1 Set selectedKeys = selector.selectedKeys();
 2 Iterator keyIterator = selectedKeys.iterator();
 3 while(keyIterator.hasNext()) {
 4     SelectionKey key = keyIterator.next();
 5     if(key.isAcceptable()) {
 6         // a connection was accepted by a ServerSocketChannel.
 7     } else if (key.isConnectable()) {
 8         // a connection was established with a remote server.
 9     } else if (key.isReadable()) {
10         // a channel is ready for reading
11     } else if (key.isWritable()) {
12         // a channel is ready for writing
13     }
14     keyIterator.remove();
15 }

  請注意keyIterator.remove()每次迭代結束時的呼叫。在Selector刪除SelectionKey作為自己選擇的關鍵例項,當你完成處理後,你必須這樣做。這樣的話才能在通道下一次變為“就緒”時,Selector將再次將其新增到所選的鍵集合。

  6.停止選擇

  選擇器執行選擇的過程,系統底層會一次詢問每個通道是否就緒,這個過程可能會造成呼叫執行緒進入阻塞狀態,那麼我們有一下二種方式來喚醒在Select()方法中阻塞的執行緒。

  (1)wakeup()方法:一個執行緒呼叫select()方法的那個物件上呼叫Selector.wakeup()方法。阻塞在select()方法上的執行緒會立馬返回。如果有其它執行緒呼叫了wakeup()方法,

    但當前沒有執行緒阻塞在select()方法上,下個呼叫select()方法的執行緒會立即“醒來(wake up)”。

  (2)close()方法:該方法使得任何一個在選擇操作中阻塞的執行緒都被喚醒,用完Selector後呼叫其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey例項無效。通道本身並不會關閉。

以下是一段典型的NIO程式碼:

public void selector() throws IOException {
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        Selector selector=Selector.open();
        ServerSocketChannel ssc=ServerSocketChannel.open();
        ssc.configureBlocking(false);//設定為非阻塞方式
        ssc.socket().bind(new InetSocketAddress(8080));
        ssc.register(selector, SelectionKey.OP_ACCEPT);//註冊監聽的時間
        while(true) {
            Set selectKeys=selector.selectedKeys();//獲取所有的key集合
            Iterator it=selectKeys.iterator();
            while(it.hasNext()) {
                SelectionKey key=(SelectionKey)it.next();
                if((key.readyOps()&SelectionKey.OP_ACCEPT)==SelectionKey.OP_ACCEPT) {
                    ServerSocketChannel ssChannel=(ServerSocketChannel)key.channel();
                    SocketChannel sc=ssChannel.accept();//接收到服務端的請求
                    sc.configureBlocking(false);
                    sc.register(selector, SelectionKey.OP_READ);
                    it.remove();
                }else if((key.readyOps()&SelectionKey.OP_READ)==SelectionKey.OP_READ) {
                    SocketChannel sc=(SocketChannel)key.channel();
                    while(true) {
                        buffer.clear();
                        int n=sc.read(buffer);
                        if(n<0) {
                            break;
                        }
                        buffer.flip();
                    }
                    it.remove();
                }
            }
        }
        
    }

------------恢復內容結束------------