NIO-網絡編程

1,NIO簡介

  NIO(Non-blocking I/O,非阻塞I/O,或者稱爲NewIO) 是在jdk1.4後提供的一項重要開發包-因爲有了nio的出現才使得Java底層通信的性能得到大幅度的提升

  因爲在整個java的IO中,大部分操作都屬於阻塞性操作,例如,鍵盤輸入數據,程序必須一直等待輸入數據,否則程序無法向下繼續執行,還有就是網絡中Socket程序必須通過accept()方法一直等待用戶的連接。這樣一來,勢必造成大量系統資源的浪費。所以JAVA在jdk1.4之後增加了新IO,(說新,現在一點都不新了啊),NIO的操作基本上都是使用緩衝區完成的。

既然使用了緩衝區,那麼操作的性能將是最高的。

在這裏插入圖片描述
Java NIO 由以下幾個核心部分組成:

  • Channel
  • Buffer
  • Selector

雖然Java NIO 中除此之外還有很多類和組件,但在我看來,Channel,Buffer 和 Selector 構成了核心的API。其它組件,如Pipe和FileLock,只不過是與三個核心組件共同使用的工具類。因此,在概述中我將集中在這三個組件上。

2,Buffer類

緩衝區(Buffer)是一個線性的,有序的數據集,一個緩衝區只能容納一種數據類型。
在基本IO操作中,所有的數據都是以流的形式操作的,而在NIO中,則都是使用緩衝區,所有的讀寫操作都是使用緩衝區完成的。緩衝區本質上是一塊可以寫入數據,然後可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。

java.nio.Buffer下有七個子類,這些子類緩衝區分別用於存儲不同類型的數據。

分別是:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer ,都是針對基本數據類型,沒有針對布爾型的。

(在ByteBuffer下還有一個MappedByteBuffer,用於表示內存映射文件)

在Buffer中的函數

方法 作用
public final int capacity() //Returns this buffer’s capacity.
public final int position() //Returns this buffer’s position.
public final Buffer position(int newPosition) //Sets this buffer’s position. If the mark is defined and larger than the new position then it is discarded.
public final int limit() //Returns this buffer’s limit.
public final Buffer limit(int newLimit) //Sets this buffer’s limit. If the position is larger than the new limit then it is set to the new limit. If the mark is defined and larger than the new          limit then it is discarded.
public final Buffer flip() //Flips this buffer. The limit is set to the current position and then the position is set to zero.
public final int remaining() //Returns the number of elements between the current position and the limit.

……

capacity的含義總是一樣的,返回的是緩衝區的大小。而position和limit的含義取決於Buffer處在讀模式還是寫模式。

capacity
作爲一個內存塊,Buffer有一個固定的大小值,也叫"capacity".你只能往裏寫capacity個byte、long,char等類型。一旦Buffer滿了,需要將其清空(通過讀數據或者清除數據)才能繼續寫數據往裏寫數據。

position
當你寫數據到Buffer中時,position表示當前可寫入的位置,既此指針永遠放到寫入的最後一個元素之後(例如,你已經寫入四個元素,那麼此指針將指向第五個位置)。初始的position值爲0.當一個byte、long等數據寫到Buffer後, position會向前移動到下一個可插入數據的Buffer單元。position最大可爲capacity – 1.(其實就是一個數組)

當讀取數據時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置爲0. 當從Buffer的position處讀取數據時,position向前移動到下一個可讀的位置。

limit
在寫模式下,Buffer的limit表示你最多能往Buffer裏寫多少數據。 寫模式下,limit等於Buffer的capacity。

當切換Buffer到讀模式時, limit表示你最多能讀到多少數據。因此,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)

flip()方法
flip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,並將limit設置成之前position的值。

換句話說,position現在用於標記讀的位置,limit表示之前寫進了多少個byte、char等 —— 現在能讀取多少個byte、char等。

緩衝區操作細節如下圖所示:

A:開闢緩衝區
在這裏插入圖片描述
B:向緩衝區中增加一個數據
在這裏插入圖片描述
C:向緩衝區中增加一組數據
在這裏插入圖片描述
D:執行flip()方法,limit設置爲position,position設置爲0
在這裏插入圖片描述
注:0 <= position <= limit <= capacity

3,Channel類

通道Channel)可以用來讀取數據和寫入數據,通道類似於之前的輸入輸出流,但是程序不會直接操作通道,所有的內容都是先讀取或者寫入緩衝區中,再通過緩衝區取得或者寫入

通道與傳統的流操作不同,傳統的流操作分爲輸入流和輸出流,而通道本身是雙向操作的,既可以完成輸入也可以完成輸出
在這裏插入圖片描述
Java NIO中最重要的幾個Channel的實現:

  • FileChannel: 用於文件的數據讀寫
  • DatagramChannel: 用於UDP的數據讀寫
  • SocketChannel: 用於TCP的數據讀寫,一般是客戶端實現
  • ServerSocketChannel: 允許我們監聽TCP鏈接請求,每個請求會創建會一個SocketChannel,一般是服務器實現

類層次結構:
下面的UML圖使用Idea生成的。
在這裏插入圖片描述

FileChannel
FileChannel是Channel的子類,可以進行文件的通道讀寫操作

使用FileChannel讀取數據到Buffer(緩衝區)以及利用Buffer(緩衝區)寫入數據到FileChannel,代碼如下

package com.company;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelDemo {
  public static void main(String[] args) throws Exception {
    File file=new File("C:\\Users\\張小飛\\Desktop\\888.sql");
    FileInputStream fileInputStream=new FileInputStream(file);
    FileChannel fileChannel=fileInputStream.getChannel();
    ByteBuffer buffer = ByteBuffer.allocate(20);
    ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
    int count=0;
    while ((count=fileChannel.read(buffer))!=-1){
      buffer.flip();
      while (buffer.hasRemaining()){
        byteArrayOutputStream.write(buffer.get());
      }
      buffer.clear();
    }
    System.out.println(new String(byteArrayOutputStream.toByteArray()));
    fileChannel.close();
    fileInputStream.close();
  }
}

在這裏插入圖片描述

4,Selector類

  NIO推出的的主要目的就是解決io的性能問題,而傳統的io中最大的情況在於它屬於同步阻塞io通信,也就是一個線程在進行操作的時候,其他的線程無法進行處理。如果說現在只是一個單機版的程序,那麼沒有任何問題;而如果該程序用於網絡通信,那麼這個問題就會很大,所以真正的NIO應該在高效的網絡傳輸處理程序中。

  網絡通信就是一個基本的通道連接,在NIO中提供兩個新的通道類:ServerSocketChannel,SocketChannel。爲了方便進行所有通道的管理,NIO提供一個Selector通道管理類,這樣所有的通道都可以直接向Selector進行註冊,並且採用統一的模式進行讀寫操作,這樣的設計被稱爲Reactor模式

Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。

這是在一個單線程中使用一個Selector處理3個Channel的圖示:
在這裏插入圖片描述
要使用Selector,得向Selector註冊Channel,然後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。

5,基於NIO實現一個Echo模型

1,實現服務器端程序
SocketClientChannelThread 類

package com.company;

import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * 客戶端處理線程
 */
public class SocketClientChannelThread implements Runnable {
  private SocketChannel clientChannel;             //客戶端通道
  private boolean flag = true;                       //循環標記

  public SocketClientChannelThread(SocketChannel clientChannel) throws Exception {
    this.clientChannel = clientChannel;              //保存客戶端通道
    System.out.println("【客戶端連接成功】,該客戶端的地址爲:" + clientChannel.getRemoteAddress());

  }

  /**
   * 線程任務
   */

  @Override
  public void run() {
    ByteBuffer buffer = ByteBuffer.allocate(50);            //創建緩衝區
    try {
      while (this.flag) {
        //可能會重複使用一個buffer,所以是使用之前做清空處理
        buffer.clear();
        int readCount = this.clientChannel.read(buffer);     //接收客戶端發送數據
        String readMessage = new String(buffer.array(), 0, readCount).trim();
        System.out.println("【服務器接收到消息】" + readMessage);
        String writeMessage = "【echo】" + readMessage + "\n";
        if ("exit".equals(readMessage)) {
          writeMessage = "【exit】拜拜,下次再見";
          this.flag = false;                     //修改標記
        }
        buffer.clear();
        buffer.put(writeMessage.getBytes()); //緩衝區保存數據
        buffer.flip();                       //重置緩衝區
        this.clientChannel.write(buffer);    //迴應信息
      }
      this.clientChannel.close();           //關閉通道
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

EchoServer類 --服務器端啓動類

package com.company;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class EchoServer {
  public static final int  PORT=9999;       //綁定的端口

  public static void main(String[] args) throws Exception {
    //考慮到性能的優化,最多允許五個用戶進行訪問
    ExecutorService executorService= Executors.newFixedThreadPool(5);            //聲明一個固定大小的線程池
    ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
    serverSocketChannel.configureBlocking(false);                                          //設置非阻塞模式
    serverSocketChannel.bind(new InetSocketAddress(PORT));                                 //服務綁定端口
    //打開一個選擇器,隨後所有的channel通道都要再次註冊
    Selector selector=Selector.open();
    //將當前的serverSocketChannel統一註冊到Selector中,接受統一的管理
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    System.out.println("服務器端啓動程序,該程序在"+PORT+"端口上監聽,等待客戶端連接。。");
    //所有的連接處理都需要被selector管理,也就是說,只要有新的用戶連接,那麼就是通過selector處理
    int keySelect=0;
    while ((keySelect=selector.select())>0){
      Set<SelectionKey> selectionKeys = selector.selectedKeys();
      Iterator<SelectionKey> iterator = selectionKeys.iterator();
      while (iterator.hasNext()) {
        SelectionKey selectionKey =  iterator.next();
        if (selectionKey.isAcceptable()){                    //模式爲接收連接模式
          SocketChannel clientChannel=serverSocketChannel.accept(); //等待接收
          if (clientChannel!=null){     //已經有了連接
            executorService.submit(new SocketClientChannelThread(clientChannel));
          }
        }
        iterator.remove();                     //移除此通道
      }
    }
    executorService.shutdown();     //關閉線程池
    serverSocketChannel.close();      //關閉服務器通道
  }

}

2,定義一個輸入工具類,實現鍵盤數據接收
InputUtil類

package com.company;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class InputUtil {
  public static final BufferedReader KEYBOARD_INPUT=new BufferedReader(new InputStreamReader(System.in));   //鍵盤緩衝輸入流
  private InputUtil(){}
  public static String getString(String prompt) throws IOException {
    boolean flag=true;
    String str=null;
    while (flag){
      System.out.println(prompt);
      str=KEYBOARD_INPUT.readLine();
      if(str==null||"".equals(str)){
        System.out.println("數據輸入流錯誤,請重新輸入!");

      }
      else {
        flag=false;
      }
    }
    return  str;
  }
}

3,實現客戶端Socket
EchoClient類

package com.company;



import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class EchoClient {
  public static final String HOST="localhost";       //連接主機
  public static final int PORT=9999;                 //綁定端口

  public static void main(String[] args) throws IOException {
    SocketChannel clientChannel = SocketChannel.open();
    clientChannel.connect(new InetSocketAddress(HOST,PORT));       //連接服務器端
    ByteBuffer buffer = ByteBuffer.allocate(50);                   //開闢緩存
    boolean flag=true;
    while (flag){
      buffer.clear();
      String msg = InputUtil.getString("請輸入要發送的信息:");
      buffer.put(msg.getBytes());
      buffer.flip();
      clientChannel.write(buffer);
      buffer.clear();
      int readCount = clientChannel.read(buffer);                //讀取服務器端響應
      buffer.flip();
      System.err.println(new String(buffer.array(),0,readCount));
      if ("exit".equals(msg)){                                   //結束指令
        flag=false;                                              //結束循環
      }
    }
   clientChannel.close();
  }
}

最後的運行效果如下:

在這裏插入圖片描述

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