如何用Java與python程式碼解釋IO模型

tengshe789發表於2019-03-03

前天剛好看了點《UNIX網路程式設計》,比較頭大。現在我來整理一下所學所得,並用於個人備忘。如果有不對,請批評。

想要解鎖更多新姿勢?請訪問https://blog.tengshe789.tech/

IO模型介紹

IO模型是什麼?很多書籍或者百度百度百科,都沒有給出明確的解釋,我也不敢亂下定義。以我愚見,IO模型,是通過根據前人主觀意識的思考而構成客觀闡述IO複雜操作邏輯的物件。

要知道,應用程式使用系統資源的一個過程,程式無法直接操作IO裝置的,因為使用者程式不能直接訪問磁碟,所以要通過核心的系統呼叫讀取,這個核心讀取的過程就是使用者程式等待的過程,等待核心讀取後將資料從核心記憶體複製到程式記憶體。因此作業系統設立一個IO模型進行規範,就非常有必要了。

應用程式使用系統資源

為了更好地瞭解IO模型,我們需要事先回顧下:同步、非同步、阻塞、非阻塞

同步與非同步:描述的是使用者執行緒與核心的互動方式,同步指使用者執行緒發起IO請求後需要等待或者輪詢核心IO操作完成後才能繼續執行;而非同步是指使用者執行緒發起IO請求後仍然繼續執行,當核心IO操作完成後會通知使用者執行緒,或者呼叫使用者執行緒註冊的回撥函式。

阻塞與非阻塞:描述是使用者執行緒呼叫核心IO操作的方式,阻塞是指IO操作需要徹底完成後才返回到使用者空間;而非阻塞是指IO操作被呼叫後立即返回給使用者一個狀態值,無需等到IO操作徹底完成。

IO模型一共有5類:

  • blocking-IO BIO(阻塞IO)

  • non-blocking IO NIO(非阻塞IO)

  • IO multiplexing IO多路複用

  • signal driven IO 訊號驅動IO

  • asynchronous IO AIO(非同步IO)

    由於signal driven IO(訊號驅動IO)在實際中並不常用,所以主要介紹其餘四種IO Model。

BIO(blocking io)

先來看看讀操作流程

1535200521414

從圖中可以看出,使用者程式呼叫了recvfrom這個系統呼叫,kernel就開始了IO的第一個階段:準備資料。

對於network io來說,很多時候資料在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候kernel就要等待足夠的資料到來。

而在使用者程式這邊,整個程式會被阻塞。當kernel一直等到資料準備好了,它就會將資料從kernel中拷貝到使用者記憶體,然後kernel返回結果,使用者程式才解除block的狀態,重新執行起來。

也就是說,blocking IO的特點就是在IO執行的兩個階段(等待資料和拷貝資料兩個階段)都被block了。

JAVA 阻塞 demo

下面的例子主要使用Socket通道進行程式設計。服務端如下:

/**
 * @program: socketTest
 * @description: one thread demo for bio version
 * @author: tEngSHe789
 * @create: 2018-08-26 21:17
 **/
public class Server {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket=new ServerSocket(8888);
            System.out.println("服務端Start....");
            //等待客戶端就緒 -> 堵塞
            while (true){
                Socket socket = serverSocket.accept();
                System.out.println("發現客戶端連線");
                InputStream is=socket.getInputStream();
                byte[] b =new byte[1024];
                //等待客戶端傳送請求 -> 堵塞
                while (true) {
                    int data = is.read(b);
                    String info=null;
                    if (data!=-1){
                        info=new String(b,0,data,"GBK");
                    }
                    System.out.println(info);
                }

            }
        } catch (IOException e) {
        }
    }
}
複製程式碼

客戶端

/**
 * @program: socketTest
 * @description: one thread demo for bio version
 * @author: tEngSHe789
 **/
public class Client {
    public static void main(String[] args) {
        try {
            Socket socket=new Socket("127.0.0.1",8888);
            OutputStream os = socket.getOutputStream();
            System.out.println("正在傳送資料");
            os.write("這是來自客戶端的資訊".getBytes());
            os.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

PY 阻塞 demo

服務端

import socket

s = socket.socket()
s.bind((`127.0.0.1`,8888))
print(`服務端啟動....`)
# 等待客戶端就緒 -> 堵塞
s.listen()
# 等待客戶端傳送請求 -> 堵塞
conn,addr = s.accept()
msg = conn.recv(1024).decode(`utf-8`)
print(msg)
conn.close()
s.close()

複製程式碼

客戶端

import socket

s = socket.socket()
s.connect((`127.0.0.1`,8888))
print(`客戶端已啟動....`)
s.send(`正在傳送資料`.encode(`utf-8`))
s.close()
複製程式碼

NIO(non blocking io)

NIO就不一樣了,recvform系統呼叫呼叫之後,程式並沒有被阻塞,核心馬上返回給程式,如果資料還沒準備好,此時會返回一個error。程式在返回之後,可以乾點別的事情,然後再發起recvform系統呼叫。重複上面的過程,迴圈往復的進行recvform系統呼叫。這個過程通常被稱之為輪詢。

輪詢檢查核心資料,直到資料準備好,再拷貝資料到程式,進行資料處理。需要注意,拷貝資料整個過程,程式仍然是屬於阻塞的狀態。

1535201598609

JAVA 與NIO

Java NIO(New IO)是一個可以替代標準Java IO API的IO API(從Java 1.4開始),Java NIO提供了與標準IO不同的IO工作方式。

在java中,標準的IO基於位元組流和字元流進行操作的,而NIO是基於通道(Channel)和緩衝區(Buffer)進行操作,資料總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。

我們先看看Buffer類

Buffer類

Java NIO中的Buffer主要用於與NIO通道進行互動,資料是從通道讀入到緩衝區,從緩衝區寫入通道中的。概念上,緩衝區可以看成包在一個物件內的陣列,下面看一個圖

新建立的ByteBuffer

這是一個新建立的容量為10的ByteBuffer邏輯圖,他有四個屬性來提供關於其包含的資料元素資訊,分別是:

1)容量(capacity):表示Buffer最大資料容量,緩衝區容量不能為負,並且建立後不能修改。

2)限制(limit):也叫上界。第一個不應該讀取或者寫入的資料的索引,即位於limit後的資料不可以讀寫。緩衝區的限制不能為負,並且不能大於其容量(capacity)。

3)位置(position):下一個要讀取或寫入的資料的索引。緩衝區的位置不能為負,並且不能大於其限制(limit)。

4)標記(mark)與重置(reset):標記是一個索引,通過Buffer中的mark()方法指定Buffer中一個特定的position,之後可以通過呼叫reset()方法恢復到這個position。

從這幅圖可以看到,他的容量(capacity)和限制(limit)設定為10,位置設定為0,每個緩衝區容量是固定的,標記是未定義的,其他三個屬性可以通過使用緩衝區解決。

緩衝區儲存資料支援的資料型別

支援七種資料型別,他們是:
1.byteBuffer
2.charBuffer
3.shortBuffer
4.IntBuffer
5.LongBuffer
6.FloatBuffer
7.DubooBuffer

基本用法

使用Buffer讀寫資料一般遵循以下四個步驟:

(1) 寫入資料到Buffer,一般有可以從Channel讀取到緩衝區中,也可以呼叫put方法寫入。

(2) 呼叫flip()方法,切換資料模式。

(3) 從Buffer中讀取資料,一般從緩衝區讀取資料寫入到通道中,也可以呼叫get方法讀取。

(4) 呼叫clear()方法或者compact()方法。

緩衝區API

首先,用allocate 指定緩衝區大小1024

ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
複製程式碼
儲存或填充

我們可以用put 存入資料到緩衝區

byteBuffer.put("tengshe789".getBytes());
複製程式碼

當呼叫put時,會指出下一個元素應當被插入的位置,位置(position)指向的是下一個元素。如果指向的位置超過限制(limit),則丟擲BufferOverFlowException異常。

翻轉

Flip將一個能夠繼續新增資料元素的填充狀態的緩衝區翻轉成一個準備讀出元素的釋放狀態

byteBuffer.flip();
複製程式碼

具體有什麼用呢?

對於已經寫滿了緩衝區,如果將緩衝區內容傳遞給一個通道,以使內容能被全部寫出。

但如果通道現在在緩衝區上執行get,那麼它將從我們剛剛插入的有用資料之外取出未定義資料。通過翻轉將位置值重新設為 0,通道就會從正確位置開始獲取。

例如我們定義了一個容量是10的buffer,並填入hello,如下圖所示

1535636182144

翻轉後如下圖所示

1535636206735
重讀

Rewind與 flip相似,但不影響上界屬性。它只是將位置值設回 0。可以使用 rewind()後退,重讀已經被翻轉的緩衝區中的資料。

byteBuffer.rewind();
複製程式碼
獲取

翻轉完了,就可以用get獲取緩衝區資料了

byte[] b= new byte[byteBuffer.limit()];
byteBuffer.get(b);
複製程式碼

當呼叫get時,會指出下一個元素應當被索引的位置,位置(position)返回時會+1s。如果指向的位置超過限制(limit),則丟擲BufferUnderFlowException異常。如果提供的索引超過範圍,也會丟擲IndexOutOfBoundsException異常

釋放

remaining可以告訴你從當前位置(position)到限制(limit)還剩的元素數目

int count = byteBuffer.remaining();
複製程式碼

clear將緩衝區重置為空狀態

byteBuffer.clear();
複製程式碼
壓縮

如果我們只想從緩衝區中釋放一部分資料,而不是全部,然後重新填充。為了實現這一點,未讀的資料元素需要下移以使第一個元素索引為 0。儘管重複這樣做會效率低下,但這有時非常必要,而 API 對此為您提供了一個 compact()函式。

byteBuffer.compact();
複製程式碼
標記與重置

標記是一個索引,通過Buffer中的mark()方法指定Buffer中一個特定的position,之後可以通過呼叫reset()方法恢復到這個position。要知道緩衝區的標記在mark()函式被呼叫前時未定義的,如果標記未定義,呼叫reset()會導致InvalidMarkException異常

byteBuffer.position(2).mark().position(4).reset();
複製程式碼

要注意,java.nio中的類特意被設計為支援級聯呼叫,優雅的使用級聯呼叫,可以產生優美易讀的程式碼。

直接緩衝區與非直接緩衝區

非直接緩衝區

上面我們說了ByteBuffer,也就是緩衝區的用法,譬如用allocate() 方法指定緩衝區大小,然後進行填充或翻轉操作等等等。我們所建立的緩衝區,都屬於直接緩衝區。他們都是在JVM記憶體中建立,在每次呼叫基礎作業系統的一個本機IO之前或者之後,虛擬機器都會將緩衝區的內容複製到中間緩衝區(或者從中間緩衝區複製內容),緩衝區的內容駐留在JVM內,因此銷燬容易,但是佔用JVM記憶體開銷,處理過程中有複製操作。

非直接緩衝區寫入步驟:

1.建立一個臨時的直接ByteBuffer物件。
2.將非直接緩衝區的內容複製到臨時緩衝中。
3.使用臨時緩衝區執行低層次I/O操作。
4.臨時緩衝區物件離開作用域,並最終成為被回收的無用資料。

/**
 * @program: UndirectBuffer
 * @description: 利用通道完成檔案的複製(非直接緩衝區)
 * @author: tEngSHe789
 **/
public class UndirectBuffer {
    public static void main(String[] args) throws IOException {
        // 建立流
        FileInputStream fis = new FileInputStream("d://blog.md");
        FileOutputStream fos = new FileOutputStream("d://blog.md");
        //獲取管道
        FileChannel in = fis.getChannel();
        FileChannel out = fos.getChannel();
        // 分配指定大小的緩衝區
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (in.read(buffer) !=-1){
            buffer.flip();// 準備讀資料了
            out.write(buffer);
            buffer.clear();
        }
        out.close();
        in.close();
        fis.close();
        fos.close();
    }
}
複製程式碼
直接緩衝區

直接緩衝區,是通過 allocateDirect() 方法在JVM記憶體開闢記憶體,在每次呼叫基礎作業系統的一個本機IO之前或者之後,虛擬機器都會避免將緩衝區的內容複製到中間緩衝區(或者從中間緩衝區複製內容),緩衝區的內容駐留在實體記憶體內,會少一次複製過程,如果需要迴圈使用緩衝區,用直接緩衝區可以很大地提高效能。

雖然直接緩衝區使JVM可以進行高效的I/O操作,但它使用的記憶體是作業系統分配的,繞過了JVM堆疊,建立和銷燬比堆疊上的緩衝區要更大的開銷。

/**
 * @program: DirectBuffer
 * @description: 使用直接緩衝區完成檔案的複製(記憶體對映檔案)
 * @author: tEngSHe789
 **/
public class DirectBuffer {
    public static void main(String[] args) throws IOException {
        //建立管道
        FileChannel in=FileChannel.open(Paths.get("d://blog.md"),StandardOpenOption.READ);
        FileChannel out=FileChannel.open(Paths.get("d://blog.md"),StandardOpenOption.WRITE
                        ,StandardOpenOption.READ,StandardOpenOption.CREATE);
        // 拿到將管道內容對映到記憶體的直接緩衝區對映檔案(一個位置在硬碟的基於記憶體的緩衝區)
        MappedByteBuffer inMappedByteBuffer = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
        MappedByteBuffer outMappedByteBuffer = out.map(FileChannel.MapMode.READ_WRITE, 0, in.size());
        // 對直接緩衝區進行資料讀寫操作
        byte[] bytes=new byte[inMappedByteBuffer.limit()];
        inMappedByteBuffer.get(bytes);
        outMappedByteBuffer.put(bytes);
        in.close();
        out.close();
    }
}
複製程式碼
直接緩衝區與非直接緩衝區的區別
  1. 位元組緩衝區要麼是直接的,要麼是非直接的。如果為直接位元組緩衝區,則 Java 虛擬機器會盡最大努力直接在此緩衝區上執行本機 I/O 操作。也就是說,在每次呼叫基礎作業系統的一個本機 I/O 操作之前(或之後),虛擬機器都會盡量避免將緩衝區的內容複製到中間緩衝區中(或從中間緩衝區中複製內容)。
  2. 直接位元組緩衝區可以通過呼叫此類的 allocateDirect() 工廠方法來建立。此方法返回的緩衝區進行分配和取消分配所需成本通常高於非直接緩衝區。直接緩衝區的內容可以駐留在常規的垃圾回收堆之外,因此,它們對應用程式的記憶體需求量造成的影響可能並不明顯。所以,建議將直接緩衝區主要分配給那些易受基礎系統的本機 I/O 操作影響的大型、持久的緩衝區。一般情況下,最好僅在直接緩衝區能在程式效能方面帶來明顯好處時分配它們。
  3. 直接位元組緩衝區還可以通過 FileChannelmap() 方法 將檔案區域直接對映到記憶體中來建立。該方法返回MappedByteBuffer 。 Java 平臺的實現有助於通過 JNI 從本機程式碼建立直接位元組緩衝區。如果以上這些緩衝區中的某個緩衝區例項指的是不可訪問的記憶體區域,則試圖訪問該區域不會更改該緩衝區的內容,並且將會在訪問期間或稍後的某個時間導致丟擲不確定的異常。
  4. 位元組緩衝區是直接緩衝區還是非直接緩衝區可通過呼叫其 isDirect() 方法來確定。提供此方法是為了能夠在效能關鍵型程式碼中執行顯式緩衝區管理。

Channel

通道是java.nio的第二個創新,表示提供 IO 裝置(例如:檔案、套接字)的直接連線。

若需要使用 NIO 系統,需要獲取用於連線 IO 裝置的通道以及用於容納資料的緩衝區。然後操作緩衝區,對資料進行處理。這其中,Channel負責傳輸, Buffer 負責儲存。

通道是由java.nio.channels 包定義的,Channel 表示 IO 源與目標開啟的連線。Channel 類似於傳統的“流”。只不過 Channel本身不能直接訪問資料, Channel 只能與Buffer 進行互動

介面

java.nio.channels.Channel 介面:

  • FileChannel
  • SocketChannel
  • ServerSocketChannel
  • DatagramChannel

與緩衝區不同,通道API主要由介面指定,不同作業系統上通道的實現會不一樣

實現

直接緩衝區與非直接緩衝區的栗子

分散讀取與聚集寫入

通道可以有選擇地實現兩個新的介面: ScatteringByteChannelGatheringByteChannel

1535700194601

ScatteringByteChannel 有2個read方法,我們都叫她分散讀取(scattering Reads),分散讀取中,通道依次填充每個緩衝區。填滿一個緩衝區後,它就開始填充下一個。在某種意義上,緩衝區陣列就像一個大緩衝區。

1535700216940

GatheringByteChannel中有2個wirte方法,我們都叫她聚集寫入(gathering Writes),他可以將多個緩衝區的資料聚集到通道中

分散讀取與聚集寫入的應用

分散讀取/聚集寫入對於將資料劃分為幾個部分很有用。例如,您可能在編寫一個使用訊息物件的網路應用程式,每一個訊息被劃分為固定長度的頭部和固定長度的正文。您可以建立一個剛好可以容納頭部的緩衝區和另一個剛好可以容難正文的緩衝區。當您將它們放入一個陣列中並使用分散讀取來向它們讀入訊息時,頭部和正文將整齊地劃分到這兩個緩衝區中。

我們從緩衝區所得到的方便性對於緩衝區陣列同樣有效。因為每一個緩衝區都跟蹤自己還可以接受多少資料,所以分散讀取會自動找到有空間接受資料的第一個緩衝區。在這個緩衝區填滿後,它就會移動到下一個緩衝區。

Python與NIO

服務端(具體見註釋)

from socket import *
import time
s=socket(AF_INET,SOCK_STREAM)
s.bind((`127.0.0.1`,8888))
s.listen(5)
s.setblocking(False) #設定socket的介面為非阻塞
conn_l=[] # 儲存和server的連線 的 連線
del_l=[] # 儲存和和server的斷開 的 連線
while True:
    try:
        # 這個過程是不阻塞的
        conn,addr=s.accept() # 當沒人連線的時候會報錯,走exception(<- py中是except)
        conn_l.append(conn)
    except BlockingIOError:
        print(conn_l)
        for conn in conn_l:
            try:
                data=conn.recv(1024)
                if not data:
                    del_l.append(conn)
                # 這個過程是不阻塞的
                data=conn.recv(1024) # 不阻塞
                if not data: # 如果拿不到data
                    del_l.append(conn) # 在廢棄列表中新增conn
                    continue
                conn.send(data.upper())
            except BlockingIOError:
                pass
            except ConnectionResetError:
                del_l.append(conn)

        for conn in del_l:
            conn_l.remove(conn)
            conn.close()
        del_l=[]
複製程式碼

客戶端

from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect((`127.0.0.1`,8888))

while True:
    msg=input(`>>: `)
    if not msg:continue
    c.send(msg.encode(`utf-8`))
    data=c.recv(1024)
    print(data.decode(`utf-8`))
複製程式碼

IO複用(IO multiplexing)

I/O多路複用實際上就是用select, poll, epoll監聽多個io物件,當io物件有變化(有資料)的時候就通知使用者程式。有些地方也稱這種IO方式為事件驅動IO(event driven IO)。與多程式和多執行緒技術相比,I/O多路複用技術的最大優勢是系統開銷小,系統不必建立程式/執行緒,也不必維護這些程式/執行緒,從而大大減小了系統的開銷。當然具體的可以看看這篇部落格,現在先來看下I/O多路複用的流程:

IO複用

(1)當使用者程式呼叫了select,那麼整個程式會被block;

(2)而同時,kernel會“監視”所有select負責的socket;

(3)當任何一個socket中的資料準備好了,select就會返回;

(4)這個時候使用者程式再呼叫read操作,將資料從kernel拷貝到使用者程式。

這個圖和BIO的圖其實並沒有太大的不同,事實上還更差一些。因為這裡需要使用兩個系統呼叫(select和recvfrom),而BIO只呼叫了一個系統呼叫(recvfrom)。但是,用select的優勢在於它可以同時處理多個connection

JAVA實現IO複用

這裡我們使用的是java.nio下模組來完成I/O多路複用的例子。我用到的Selector(選擇器),是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的執行緒可以管理多個channel,從而管理多個網路連線。

Selector的使用

Selector的建立

Selector selector = Selector.open();
複製程式碼

向Selector註冊通道

為了將Channel和Selector配合使用,必須將channel註冊到selector上。通過SelectableChannel.register()方法來實現,如下:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
複製程式碼

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

通道觸發了一個事件意思是該事件已經就緒。所以,某個channel成功連線到另一個伺服器稱為“連線就緒”。一個server socket channel準備好接收新進入的連線稱為“接收就緒”。一個有資料可讀的通道可以說是“讀就緒”。等待寫資料的通道可以說是“寫就緒”。

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

  1. SelectionKey.OP_CONNECT可連線
  2. SelectionKey.OP_ACCEPT可接受連線
  3. SelectionKey.OP_READ可讀
  4. SelectionKey.OP_WRITE可寫

SelectionKey

當向Selector註冊Channel時,register()方法會返回一個SelectionKey物件。它包含了:

  • interest集合
  • ready集合
  • Channel
  • Selector
  • 附加的物件(可選)
interest集合

interest集合是你所選擇的感興趣的事件集合。可以通過SelectionKey讀寫interest集合,像這樣:

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;
複製程式碼

可以看到,用“位與”操作interest 集合和給定的SelectionKey常量,可以確定某個確定的事件是否在interest 集合中。

ready集合

ready 集合是通道已經準備就緒的操作的集合。在一次選擇(Selection)之後,你會首先訪問這個ready set。Selection將在下一小節進行解釋。可以這樣訪問ready集合:

int readySet = selectionKey.readyOps();
複製程式碼

可以用像檢測interest集合那樣的方法,來檢測channel中什麼事件或操作已經就緒。但是,也可以使用以下四個方法,它們都會返回一個布林型別:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
複製程式碼

從SelectionKey訪問Channel和Selector

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();
複製程式碼

java程式碼

/**
 * @program: NIOServer
 * @description: 服務端
 * @author: tEngSHe789
 **/
public class NIOServer {
    public static void main(String[] args) throws IOException {
        System.out.println("服務端Start....");
        // 建立通道
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        // 設定非阻塞
        serverSocketChannel.configureBlocking(false);
        // 繫結連線
        serverSocketChannel.bind(new InetSocketAddress(8888));
        // 獲取選擇器
        Selector selector=Selector.open();
        // 將通道註冊到選擇器
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 輪調式獲取選擇“已經準備就緒”的事件
        while (selector.select() > 0){
            // 獲取當前選擇器的左右已經準備就緒的監聽事件(選擇key)
            Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                // 獲取準備就緒事件
                SelectionKey selectionKey=iterator.next();
                // 判斷具體是什麼事件
                if (selectionKey.isAcceptable()){//如果是“接受就緒”
                    SocketChannel socketChannel=serverSocketChannel.accept();// 獲取連線
                    socketChannel.configureBlocking(false); // 設定非阻塞
                    //將該通道註冊到伺服器上
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }else if (selectionKey.isReadable()){//如是“已經就緒”
                    SocketChannel socketChannel= (SocketChannel) selectionKey.channel();//獲取連線
                    //讀資料
                    ByteBuffer buffer=ByteBuffer.allocate(1024);
                    int len = 0;
                    //分散讀取
                    len=socketChannel.read(buffer);
                    while (len > 0){
                        buffer.flip();
                        System.out.println(new String(buffer.array(),0,len));
                        buffer.clear();
                    }
                }
                iterator.remove();
            }
        }
    }
}
複製程式碼

客戶端:

/**
 * @program: NIOClient
 * @description: 客戶端
 * @author: tEngSHe789
 **/
public class NIOClient {
    public static void main(String[] args) throws IOException {
        System.out.println("客戶端Start....");
        // 建立通道
        SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",8888));
        // 設定SocketChannel介面為非阻塞
        socketChannel.configureBlocking(false);
        //指定緩衝區大小
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        Scanner scanner=new Scanner(System.in);
        while (scanner.hasNext()){
            String msg = scanner.next();
            // 儲存
            buffer.put((new Date().toString()+"
"+msg).getBytes());
            // 翻轉
            buffer.flip();
            // 聚集寫入
            socketChannel.write(buffer);
            // 釋放
            buffer.clear();
        }
        socketChannel.close();
    }
}
複製程式碼

python實現IO複用

對比java用的是Selector,可以幫我們在預設作業系統下選擇最合適的select, poll, epoll這三種多路複合模型,python是通過一種機制一個程式能同時等待多個檔案描述符,而這些檔案描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函式就可以返回

服務端

from socket import *
import select

s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind((`127.0.0.1`,8888))
s.listen(5)
s.setblocking(False) #設定socket的介面為非阻塞
read_l=[s,] # 資料可讀通道的列表
while True:
    # 監聽的read_l中的socket物件內部如果有變化,那麼這個物件就會在r_l
    # 第二個引數裡有什麼物件,w_l中就有什麼物件
    # 第三個引數 如果這裡的物件內部出錯,那會把這些物件加到x_l中
    # 1 是超時時間
    r_l,w_l,x_l=select.select(read_l,[],[],1)
    print(r_l)
    for ready_obj in r_l:
        if ready_obj == s:
            conn,addr=ready_obj.accept() #此時的ready_obj等於s
            read_l.append(conn)
        else:
            try:
                data=ready_obj.recv(1024) #此時的ready_obj等於conn
                if not data:
                    ready_obj.close()
                    read_l.remove(ready_obj)
                    raise Exception(`連線斷開`)
                ready_obj.send(data.upper())
            except ConnectionResetError:
                ready_obj.close()
                read_l.remove(ready_obj)
複製程式碼

客戶端

from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect((`127.0.0.1`,8888))

while True:
    msg=input(`>>>: `)
    if not msg:continue
    c.send(msg.encode(`utf-8`))
    data=c.recv(1024)
    print(data.decode(`utf-8`))
複製程式碼

AIO(asynchronous io)

真正的非同步I/O很牛逼,流程大概如下:

非同步I/O

(1)使用者程式發起read操作之後,立刻就可以開始去做其它的事。

(2)而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對使用者程式產生任何block。

(3)然後,kernel會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切都完成之後,kernel會給使用者程式傳送一個signal,告訴它read操作完成了。

Java

Java中使用AIO需要用到java.nio.channels.AsynchronousChannelGroup和java.nio.channels.AsynchronousServerSocketChannel的包,由於實際專案鮮有人用,就不演示了

總結

回顧一下各個IO Model的比較,如圖所示:

各個IO Model的比較
  • blocking io :阻塞型io,再熟悉不過,處理accept、read、write都會阻塞使用者程式
  • non blocking io:當通過系統呼叫的時候,如果沒有連線或者資料到達就直接返回一個錯誤,使用者程式不阻塞但是不斷的輪詢。注意這個不是java nio框架中對應的網路模型
  • io multiplexing:io多路複用才是nio對應的網路io模型。該模型對於使用者程式也是阻塞的,優點是可以同時支援多個connetciotn。前三種都屬於同步模式,既然都是同步的,如果要做到看似非阻塞,那麼就需要輪詢機制。相對於上一種模型,這種只是將輪詢從使用者程式轉移到了作業系統核心,通過呼叫select函式,不斷輪詢多個connection是否ready,如果有一種ready好的,就通過事件通知使用者程式,使用者程式再通過事件來處理。所以在java的nio中會看到一大堆事件處理。這種模型的阻塞不是在socket層面的阻塞,而是在調動select函式的阻塞。而且相對於blocking io,還多了一次select的系統呼叫,其實效能會更低,所以在低吞吐量下,這種io不見得比bio+執行緒池的模型優越。
  • sign driven:極少使用,不知道
  • async io :java7時候開始升級,也成為nio2。實現了非同步的io。前三種都是通過使用者程式在主動獲取(bio的阻塞,nbio的輪詢和iomult的按事件獲取),而aio互動很簡單,使用者程式呼叫後立即返回,使用者程式不阻塞,核心當完成網路io和資料複製後,主動通知使用者程式。前面說到的系統核心做的操作,除了等待網路io就緒資料到達核心,還有從系統核心複製使用者空間去的過程,非同步io這兩者對於使用者程式而言都是非阻塞的,而前三種,在資料從核心複製到使用者空間這個過程,都是阻塞的。

參考資料

前言說的那本書

Ron Hitchens於2002年 著的《java nio》

findumars

冬瓜蔡

彼岸船伕

NIO的/分散讀取和聚集寫入

併發程式設計網

感謝

續1s時間

全片結束,覺得我寫的不錯?想要了解更多精彩新姿勢?趕快開啟我的?個人部落格 ?吧!

本文地址https://blog.tengshe789.tech/2018/08/25/IO%E6%A8%A1%E5%9E%8B/#more,部分覺得比較用心的會同步到掘金,簡書,謝謝你那麼可愛,還一直關注著我~❤?

相關文章