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

前天剛好看了點《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()+"\n"+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,部分覺得比較用心的會同步到掘金,簡書,謝謝你那麼可愛,還一直關注着我~❤��

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