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

前天恰好看了點《UNIX網絡編程》,比較頭大。如今我來整理一下所學所得,並用於我的備忘。若是有不對,請批評。html

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

IO模型介紹

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

要知道,應用程序使用系統資源的一個過程,進程沒法直接操做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)

先來看看讀操做流程app

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/...,部分以爲比較用心的會同步到掘金,簡書,謝謝你那麼可愛,還一直關注着我~❤😝

相關文章
相關標籤/搜索