天天看點

Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

一、什麼是NIO

1.概念

NIO是java1.4中引入的,被稱為new I/O,也有說是non-blocking I/O,NIO被成為同步非阻塞的IO。

Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image.png

2.跟BIO流的差別

  1. BIO是面向流的,NIO是面向塊(緩沖區)的。
  2. BIO的流都是同步阻塞的,而NIO是同步非阻塞的。
  3. NIO會等待資料全部傳輸過來再讓線程處理,BIO是直接讓線程等待。
  4. NIO有選擇器,而BIO沒有。
  5. NIO是采用管道和緩存區的形式來處理資料的,而BIO是采用輸入輸出流來處理的。
  6. NIO是可以雙向的,BIO隻能夠單向。
Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image

二、NIO常用元件Channel和Buffer的使用

1.代碼

這裡以檔案複制為例

public class test {
    public static void main(String[] args){
        try{
            //存在的照片
            File inFile=new File("C:\\Users\\Administrator\\Desktop\\study.PNG");
            //複制後要存放照片的位址
            File outFile=new File("C:\\Users\\Administrator\\Desktop\\study1.PNG");
            //打開流
            FileInputStream fileInputStream=new FileInputStream(inFile);
            FileOutputStream fileOutputStream=new FileOutputStream(outFile);
            /**
             * RandomAccessFile accessFile=new RandomAccessFile(inFile,"wr");
             *  FileChannel inFileChannel=accessFile.getChannel();
             *  和下面兩行代碼是一樣的,都是可以拿到FileChannel
             */
            //擷取Channel
            FileChannel inFileChannel=fileInputStream.getChannel();
            FileChannel outFileChannel=fileOutputStream.getChannel();
           //建立buffer
            ByteBuffer buffer=ByteBuffer.allocate(1024*1024);
            //讀取到buffer中
            while (inFileChannel.read(buffer)!=-1){
                //翻轉一下,就可以讀取到全部資料了
                buffer.flip();
                outFileChannel.write(buffer);
                //讀取完後要clear
                buffer.clear();
            }
            //關閉
            inFileChannel.close();
            outFileChannel.close();
            fileInputStream.close();
            fileOutputStream.close();
        }catch (Exception e){}

    }
}      

我的桌面上的确多了一張一模一樣的圖檔

2.解釋

使用NIO的話,需要注意幾個步驟:

  1. 打開流
  2. 擷取通道
  3. 建立Buffer
  4. 切換到讀模式 buffer.flip()
  5. 切換到寫模式 buffer.clear(); 其實這裡也看不出來它是怎麼使用緩沖區的,上面這段代碼中的while循環的作用和下面的代碼是一樣的
while ((i=fileInputStream.read())!=-1){
                fileOutputStream.write(i);
          }      
Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image.png

讓我們趕緊開始NIO的程式設計

三、BIO和NIO的差別

學習了Channel和Buffer的使用,我們就可以正式進入NIO的開發了

代碼

NIO

NIO服務端:隻是接受用戶端發送過來的資料,然後列印在控制台

/**
 * NIO
 * @author xuxiaobai
 */
public class NIOTest {
    private final static int port = 8080;

    public static void main(String[] args) throws IOException {
        //啟動服務端
        TCPServer();
    } 
    /**
     * TCP服務端
     * 接受TCP
     *
     * @throws IOException
     */
    public static void TCPServer() throws IOException {
        //建立服務端多路複用選擇器
        Selector selector = Selector.open();
        //建立服務端SocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //定義位址
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        //綁定位址
        serverSocketChannel.bind(inetSocketAddress);
        System.out.println("綁定成功:" + inetSocketAddress);
        //設定為非阻塞
        serverSocketChannel.configureBlocking(false);
        //注冊服務端選擇端,隻接受accept事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            //加上延時,什麼原理我忘記了,隻知道是為了防止死鎖
            selector.select(500);
            //周遊服務端選擇器的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey next = iterator.next();
                if (!next.isValid()) {
                    //該key無效直接跳過
                    continue;
                }
                //注意
                if (next.isAcceptable()) {
                    //1\. accept事件
                    //接收到accept事件,拿到channel,這個是服務端SocketChannel
                    ServerSocketChannel channel = (ServerSocketChannel) next.channel();
                    //accept得到連接配接用戶端的channel
                    SocketChannel accept = channel.accept();
                    accept.configureBlocking(false);
                    //注冊write事件
                    accept.register(selector, SelectionKey.OP_READ);
                    iterator.remove();
                } else if (next.isReadable()) {
                    //2\. read事件
                    //開啟一個新的線程
                    Thread thread = new Thread(() -> {
                        SocketChannel channel = (SocketChannel) next.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        byteBuffer.clear();
                        try {
                            channel.read(byteBuffer);
                            //開始處理資料
                            byteBuffer.flip();
                            byte[] bytes = new byte[byteBuffer.remaining()];
                            byteBuffer.get(bytes);
                            String x = new String(bytes);
                            if(x.equals("")){
                                //老是會莫名其妙地列印一些空行,打個更新檔
                                return;
                            }
                            System.out.println(x);
                            if ("exit".equals(x)) {
                                //關閉通道
                                try {
                                    channel.close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                                next.cancel();
                            }
                        } catch (IOException e) {
                            //出現異常的處理
                            e.printStackTrace();
                            try {
                                channel.close();
                            } catch (IOException ioe) {
                                ioe.printStackTrace();
                            }
                            next.cancel();
                        }

                    });
                    iterator.remove();
                    thread.start();
                }
            }
        }
    }
}      

BIO

BIO服務端:接受用戶端的資料,然後列印在控制台

BIO用戶端:向服務端發送資料。NIO的測試中也使用這個用戶端進行測試

/**
 * BIO
 * @author xuxiaobai
 */
public class BIOTest {
    private final static int port = 8080;

    public static void main(String[] args) throws IOException {
        TCPClient();
//        TCPServer();
    }

    /**
     * TCP用戶端
     * 發送TCP
     * @throws IOException
     */
    private static void TCPClient() throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        //定義位址
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        //連接配接
        socketChannel.connect(inetSocketAddress);
        System.out.println("連接配接成功:"+inetSocketAddress);
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String next = scanner.next();
            //直接包裝一個buffer
            ByteBuffer wrap = ByteBuffer.wrap(next.getBytes());
            //寫入
            socketChannel.write(wrap);
            if ("exit".equals(next)) {
                //等于exit時關閉channel
                socketChannel.close();
                break;
            }
        }
    }

    /**
     * TCP服務端
     * 接受TCP
     * @throws IOException
     */
    private static void TCPServer() throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //定義位址
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        //綁定
        serverSocketChannel.bind(inetSocketAddress);
        System.out.println("綁定成功:"+inetSocketAddress);
        while (true) {
            //接受連接配接
            SocketChannel accept = serverSocketChannel.accept();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //定義一個緩沖區,讀出來的資料超出緩沖區的大小時會被丢棄
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    while (true) {
                        try {
                            //每次使用前都要清空,但這裡沒有真的區clear資料,隻是移動了buffer裡面的下标
                            byteBuffer.clear();
                            //讀取資料到緩沖區
                            accept.read(byteBuffer);
                            //每次讀取資料前都要flip一下,這裡都移動下标
                            byteBuffer.flip();
                            byte[] bytes = new byte[byteBuffer.remaining()];
                            //擷取資料
                            byteBuffer.get(bytes);
                            String x = new String(bytes);
                            System.out.println(x);
                            if (x.equals("exit")) {
                                //當讀出來的資料等于exit時退出
                                accept.close();
                                break;
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
            //啟動該線程
        }
    }
}      
Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image.png

搞完了代碼,讓我們來看看代碼的示範效果————從用戶端發送資料到服務端,下面展示一下效果:

先後啟動BIO的TCPServer和TCPClient方法;

TCPClient:

Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image

TCPServer:

Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image

步驟

Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image

畫了個圖來表示,這是關于selector的配置流程,在循環中根據不同key值所進行的操作,跟上面檔案複制的例子差不多了,隻不過這裡的Channel是通過 key.channel()獲得的。

Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image

差别

我們來看看一下BIO和NIO的差别。

BIO

我們用IDEA的debug啟動BIO的服務端,然後在啟動多個用戶端。

Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image

我這裡啟動了三個用戶端,可以看到有三個線程已經建立好了,然而我這時還沒有發送資料到服務端。

NIO

我們用IDEA的debug啟動NIO的服務端,然後在啟動多個BIO用戶端。

Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image

這裡啟動了多個用戶端,伺服器上沒有多餘的幾個線程。

修改BIO的TCPClient方法

private static void TCPClient() throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        //定義位址
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        //連接配接
        socketChannel.connect(inetSocketAddress);
        System.out.println("連接配接成功:" + inetSocketAddress);
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String next = scanner.next();
            //直接包裝一個buffer
//            ByteBuffer wrap = ByteBuffer.wrap(next.getBytes());
            //寫入
            while (true) {
                try {
                    //休眠
                    //注意,休眠時間建議調高一點
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                socketChannel.write(ByteBuffer.wrap(next.getBytes()));
            }
//            if ("exit".equals(next)) {
//                //等于exit時關閉channel
//                socketChannel.close();
//                break;
//            }
        }
    }      

休眠時間記得調高點!!!當機警告!

Java:什麼是NIO?什麼是BIO?NIO 和 BIO 有什麼差別?

image.png

這樣用戶端就會在讀取到第一次時,一直發送這個資料,可以看到一些線程,也是隻有在收到資料之後才會建立這個線程去列印這個資料。如果休眠時間調高一點的話,就會看到有時候這裡會一閃一閃的,調低後就會出現一閃而過的很多線程,如下圖。

四、總結