日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

常用 IO 模型图解介绍

發布時間:2025/3/20 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 常用 IO 模型图解介绍 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

很多時候對于不同的IO模型的概念和原理我們可能不是很清楚,有時候可能也會在不同的IO間迷糊,筆者也是有同樣的問題。所以經過系統的學習以后將我們常見的五種IO模型在這里做一下總結,以供大家參考和學習。

1.基本概念

五種IO模型包括:阻塞IO、非阻塞IO、IO多路復用、信號驅動IO、異步IO。為了對后面的內容的一些西域不混淆,首先給大家介紹一下系統調用常用的幾個函數和基本概念。

1.1 系統調用函數

以下幾個系統函數參考了一些書籍和文章,如果有不正確的地方還請大家指出。

函數名稱函數作用
recvfrom提供給用戶用于接收網絡IO的系統接口,從套接字上接收一個消息,可同時應用于面向連接和無連接的套接字如果此系統調用返回值<0,并且 errno為EWOULDBLOCK或EAGAIN(套接字已標記為非阻塞,而接收操作被阻塞或者接收超時 )時,連接正常,阻塞接收數據(這很關鍵,前4種IO模型都設計此系統調用)
select允許程序同時在多個底層文件描述符上,等待輸入的到達或輸出的完成。以數組形式存儲文件描述符,64位機器默認2048個。當有數據準備好時,無法感知具體是哪個流OK了,所以需要一個一個的遍歷,函數的時間復雜度為O(n)
poll以鏈表形式存儲文件描述符,沒有長度限制。本質與select相同,函數的時間復雜度也為O(n)
epoll基于事件驅動。如果某個流準備好了,會以事件通知,知道具體是哪個流,因此不需要遍歷,函數的時間復雜度為O(1)
sigaction用于設置對信號的處理方式,也可檢驗對某信號的預設處理方式。Linux使用SIGIO信號來實現IO異步通知機制

1.2同步與異步

同步和異步是針對應用程序和內核交互而言的。

概念
同步用戶進程觸發IO操作并等待或輪詢的去查看是否就緒
異步用戶進程觸發IO操作以后便開始做自己的事情,當IO操作已經完成的時候會得到IO完成的通知,需要CPU支持

1.3阻塞與非阻塞

阻塞和非阻塞是針對于進程在訪問數據的時候。根據IO操作的就緒狀態來采取的不同的方式。

概念
阻塞阻塞方式下讀取或寫入方法將一直等待
非阻塞非阻塞方式下讀取或寫入方法會立即返回一個狀態值

2.IO模型介紹

2.1 阻塞IO模型

學習過操作系統的伙伴應該知道,不管是網絡IO還是磁盤IO,對于讀操作而言,都是等到網絡的某個數據分組到達后/數據準備好后,將數據拷貝到內核空間的緩沖區中,再從內核空間拷貝到用戶空間的緩沖區。執行流程圖大致如圖:

通過流程圖可以看到,阻塞IO的執行過程是進程進行系統調用,等待內核將數據準備好并復制到用戶態緩沖區后,進程放棄使用CPU并一直阻塞在此,直到數據準備好。

2.2 非阻塞IO模型

每次應用程序詢問內核是否有數據準備好。如果就緒,就進行拷貝操作;如果未就緒,就不阻塞程序,內核直接返回未就緒的返回值,等待用戶程序下一個輪詢。在每一次詢問之前,對于程序來說是非阻塞的,占用CPU資源,可以做其他事情。執行流程圖大致如圖:

主要有兩個階段:

  • 等待數據階段:未阻塞, 用戶進程需要盲等,不停的去輪詢內核;
  • 數據復制階段:阻塞,此時進行數據復制。

在這兩個階段中,用戶進程只有在數據復制階段被阻塞了,而等待數據階段沒有阻塞,但是用戶進程需要盲等,不停地輪詢內核,看數據是否準備好。

2.3 IO多路復用模型

相比于阻塞IO模型,多路復用只是多了一個select/poll/epoll函數。select函數會不斷地輪詢自己所負責的文件描述符/套接字的到達狀態,當某個套接字就緒時,就對這個套接字進行處理。select負責輪詢等待,recvfrom負責拷貝。當用戶進程調用該select,select會監聽所有注冊好的IO,如果所有IO都沒注冊好,調用進程就阻塞。多路復用一般都是用于網絡IO,服務端與多個客戶端的建立連接。執行流程圖大致如圖:

對于客戶端來說,一般感受不到阻塞,因為請求來了,可以用放到線程池里執行;但對于執行select的操作系統而言,是阻塞的,需要阻塞地等待某個套接字變為可讀。IO多路復用其實是阻塞在select,poll,epoll這類系統調用上的,復用的是執行select,poll,epoll的線程。

2.4 信號驅動IO模型

當數據報準備好的時候,內核會向應用程序發送一個信號,進程對信號進行捕捉,并且調用信號處理函數來獲取數據報。執行流程圖大致如圖:

該模型也分為兩個階段:

  • 數據準備階段:未阻塞,當數據準備完成之后,會主動的通知用戶進程數據已經準備完成,對用戶進程做一個回調;
  • 數據拷貝階段:阻塞用戶進程,等待數據拷貝。

2.5 異步IO模型

用戶進程發起系統調用后,立刻就可以開始去做其他的事情,然后直到I/O數據準備好并復制完成后,內核會給用戶進程發送通知,告訴用戶進程操作已經完成了。

異步I/O執行的兩個階段都不會阻塞讀寫操作,由內核完成,完成后內核將數據放到指定的緩沖區,通知應用程序來取。

3.BIO,NIO,AIO介紹

操作系統的IO模型是底層基石,Java對于IO的操作其實就是進一步的封裝。

3.1 BIO

BIO–同步阻塞,JDK1.4之前常用的編程方式,適用于連接數目比較小且固定的架構,對服務器資源要求高,并發局限于應用中。在使用的時候,首先在服務端啟動一個ServerSocket來監聽網絡請求,客戶端啟動Socket發起網絡請求,默認情況下ServerSocket會建立一個線程來處理此請求,如果服務端沒有線程可用,客戶端則會阻塞等待或遭到拒絕,并發效率比較低。
我們使用java來模擬IO模型的連接,首先編寫服務端的代碼:

/*** @Author likangmin* @create 2020/12/02 13:35*/ public class Server {public static void main(String[] args) {int port=genPort(args);ServerSocket server=null;ExecutorService service= Executors.newFixedThreadPool(50);try {server=new ServerSocket(port);System.out.println("server started!");while (true){Socket socket=server.accept();service.execute(new Handler(socket));}} catch (IOException e) {e.printStackTrace();}finally {if(server!=null){try {server.close();} catch (IOException e) {e.printStackTrace();}}server=null;}}private static int genPort(String[] args) {if(args.length>0){return Integer.parseInt(args[0]);}else{return 9999;}}static class Handler implements Runnable{Socket socket=null;public Handler(Socket socket){this.socket=socket;}@Overridepublic void run() {BufferedReader reader=null;PrintWriter writer=null;try {reader=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));writer=new PrintWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8"));String readMassage=null;while (true){System.out.println("server reading...");if((readMassage=reader.readLine())==null){break;}System.out.println(readMassage);writer.println("server recive:"+readMassage);writer.flush();}} catch (IOException e) {e.printStackTrace();}finally {if(socket!=null){try {socket.close();} catch (IOException e) {e.printStackTrace();}}socket=null;if(reader!=null){try {reader.close();} catch (IOException e) {e.printStackTrace();}}reader=null;if(writer!=null){writer.close();}writer=null;}}} }

編寫一個客戶端:

/*** @Author likangmin* @create 2020/12/02 13:35*/ public class Client {public static void main(String[] args) {String host=null;int port=0;if(args.length>2){host=args[0];port=Integer.parseInt(args[1]);}else{host="127.0.0.1";port=9999;}Socket socket=null;BufferedReader reader = null;PrintWriter writer = null;Scanner s = new Scanner(System.in);try{socket = new Socket(host, port);String message = null;reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));writer = new PrintWriter(socket.getOutputStream(), true);while(true){message = s.nextLine();if(message.equals("exit")){break;}writer.println(message);writer.flush();System.out.println(reader.readLine());}}catch(Exception e){e.printStackTrace();}finally{if(socket != null){try {socket.close();} catch (IOException e) {e.printStackTrace();}}socket = null;if(reader != null){try {reader.close();} catch (IOException e) {e.printStackTrace();}}reader = null;if(writer != null){writer.close();}writer = null;}} }

然后啟動,先啟動服務端,再啟動客戶端,然后輸入數據


會發現服務端一直會阻塞在接收數據那里。

3.2 NIO

NIO–同步非阻塞,是基于事件驅動思想來完成的,適用于連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,并發局限于應用中,編程復雜,JDK1.4 開始支持。當 socket 有流可讀或可寫入時,操作系統會相應地通知應用程序進行處理,應用再將流讀取到緩沖區或寫入操作系統。一個有效的請求對應一個線程,當連接沒有數據時,是沒有工作線程來處理的。
服務器實現模式為一個請求一個通道,即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有 I/O 請求時才啟動一個線程進行處理。NIO中有幾個比較重要的角色:緩沖區Buffer,通道Channel,多路復用器Selector。
(1)Buffer
在NIO庫中,所有數據都是用緩沖區(用戶空間緩沖區)處理的。在讀取數據時,它是直接讀到緩沖區中的;在寫入數據時,也是寫入到緩沖區中。任何時候訪問NIO中的數據,都是通過緩沖區進行操作。
緩沖區實際上是一個數組,并提供了對數據的結構化訪問以及維護讀寫位置等信息。
(2)Channel
nio中對數據的讀取和寫入要通過Channel,它就像水管一樣,是一個通道。通道不同于流的地方就是通道是雙向的,可以用于讀、寫和同時讀寫操作。
(3)Selector
多路復用器,用于注冊通道。客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。
這里我們同樣寫一個demo作為例子,來讓大家知道怎么使用和使用NIO的步驟,為了讓大家看的比較清楚,在適當的地方添加了注釋。同樣的先寫服務端的代碼:

/*** @Author likangmin* @create 2020/12/02 13:35*/ public class NIOServer implements Runnable {// 多路復用器, 選擇器。 用于注冊通道的。private Selector selector;// 定義了兩個緩存。分別用于讀和寫。 初始化空間大小單位為字節。private ByteBuffer readBuffer = ByteBuffer.allocate(1024);private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);public static void main(String[] args) {new Thread(new NIOServer(9999)).start();}public NIOServer(int port) {init(port);}private void init(int port){try {System.out.println("server starting at port " + port + " ...");// 開啟多路復用器this.selector = Selector.open();// 開啟服務通道ServerSocketChannel serverChannel = ServerSocketChannel.open();// 非阻塞, 如果傳遞參數true,為阻塞模式。serverChannel.configureBlocking(false);// 綁定端口serverChannel.bind(new InetSocketAddress(port));// 注冊,并標記當前服務通道狀態/** register(Selector, int)* int - 狀態編碼* OP_ACCEPT : 連接成功的標記位。* OP_READ : 可以讀取數據的標記* OP_WRITE : 可以寫入數據的標記* OP_CONNECT : 連接建立后的標記*/serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);System.out.println("server started.");} catch (IOException e) {e.printStackTrace();}}@Overridepublic void run(){while(true){try {// 阻塞方法,當至少一個通道被選中,此方法返回。// 通道是否選擇,由注冊到多路復用器中的通道標記決定。this.selector.select();// 返回以選中的通道標記集合, 集合中保存的是通道的標記。相當于是通道的ID。Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();while(keys.hasNext()){SelectionKey key = keys.next();// 將本次要處理的通道從集合中刪除,下次循環根據新的通道列表再次執行必要的業務邏輯keys.remove();// 通道是否有效if(key.isValid()){// 阻塞狀態try{if(key.isAcceptable()){accept(key);}}catch(CancelledKeyException cke){// 斷開連接。 出現異常。key.cancel();}// 可讀狀態try{if(key.isReadable()){read(key);}}catch(CancelledKeyException cke){key.cancel();}// 可寫狀態try{if(key.isWritable()){write(key);}}catch(CancelledKeyException cke){key.cancel();}}}} catch (IOException e) {e.printStackTrace();}}}private void write(SelectionKey key){this.writeBuffer.clear();SocketChannel channel = (SocketChannel)key.channel();Scanner reader = new Scanner(System.in);try {System.out.print("put message for send to client > ");String line = reader.nextLine();// 將控制臺輸入的字符串寫入Buffer中。 寫入的數據是一個字節數組。writeBuffer.put(line.getBytes("UTF-8"));writeBuffer.flip();channel.write(writeBuffer);channel.register(this.selector, SelectionKey.OP_READ);} catch (IOException e) {e.printStackTrace();}}private void read(SelectionKey key){try {// 清空讀緩存。this.readBuffer.clear();// 獲取通道SocketChannel channel = (SocketChannel)key.channel();// 將通道中的數據讀取到緩存中。通道中的數據,就是客戶端發送給服務器的數據。int readLength = channel.read(readBuffer);// 檢查客戶端是否寫入數據。if(readLength == -1){// 關閉通道key.channel().close();// 關閉連接key.cancel();return;}/** flip, NIO中最復雜的操作就是Buffer的控制。* Buffer中有一個游標。游標信息在操作后不會歸零,如果直接訪問Buffer的話,數據有不一致的可能。* flip是重置游標的方法。NIO編程中,flip方法是常用方法。*/this.readBuffer.flip();// 字節數組,保存具體數據的。 Buffer.remaining() -> 是獲取Buffer中有效數據長度的方法。byte[] datas = new byte[readBuffer.remaining()];// 是將Buffer中的有效數據保存到字節數組中。readBuffer.get(datas);System.out.println("from " + channel.getRemoteAddress() + " client : " + new String(datas, "UTF-8"));// 注冊通道, 標記為寫操作。channel.register(this.selector, SelectionKey.OP_WRITE);} catch (IOException e) {e.printStackTrace();try {key.channel().close();key.cancel();} catch (IOException e1) {e1.printStackTrace();}}}private void accept(SelectionKey key){try {// 此通道為init方法中注冊到Selector上的ServerSocketChannelServerSocketChannel serverChannel = (ServerSocketChannel)key.channel();// 阻塞方法,當客戶端發起請求后返回。 此通道和客戶端一一對應。SocketChannel channel = serverChannel.accept();channel.configureBlocking(false);// 設置對應客戶端的通道標記狀態,此通道為讀取數據使用的。channel.register(this.selector, SelectionKey.OP_READ);} catch (IOException e) {e.printStackTrace();}} }

然后再寫客戶端的代碼:

/*** @Author likangmin* @create 2020/12/02 13:35*/ public class NIOClient {public static void main(String[] args) {// 遠程地址創建InetSocketAddress remote = new InetSocketAddress("localhost", 9999);SocketChannel channel = null;// 定義緩存。ByteBuffer buffer = ByteBuffer.allocate(1024);try {// 開啟通道channel = SocketChannel.open();// 連接遠程服務器。channel.connect(remote);Scanner reader = new Scanner(System.in);while(true){System.out.print("put message for send to server > ");String line = reader.nextLine();if(line.equals("exit")){break;}// 將控制臺輸入的數據寫入到緩存。buffer.put(line.getBytes("UTF-8"));// 重置緩存游標buffer.flip();// 將數據發送給服務器channel.write(buffer);// 清空緩存數據。buffer.clear();// 讀取服務器返回的數據int readLength = channel.read(buffer);if(readLength == -1){break;}// 重置緩存游標buffer.flip();byte[] datas = new byte[buffer.remaining()];// 讀取數據到字節數組。buffer.get(datas);System.out.println("from server : " + new String(datas, "UTF-8"));// 清空緩存。buffer.clear();}} catch (IOException e) {e.printStackTrace();} finally{if(null != channel){try {channel.close();} catch (IOException e) {e.printStackTrace();}}}} }

然后我們先啟動服務端,然后再啟動客戶端

3.3 AIO

AIO–異步非阻塞,進行讀寫操作時,只須直接調用api的read或write方法即可。一個有效請求對應一個線程,客戶端的IO請求都是OS先完成了再通知服務器應用去啟動線程進行處理。這里就不做代碼演示了,大家又需要可以自己去找一些代碼查看,操作起來比較簡單。

4.總結

最后給大家做一下總結,從效率上來說,可以簡單理解為阻塞IO<非阻塞IO<多路復用IO<信號驅動IO<異步IO。從同步和異步來說,只有異步IO模型是異步的,其他均為同步。

總結

以上是生活随笔為你收集整理的常用 IO 模型图解介绍的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。