Java IO學習筆記五:BIO到NIO

Grey Zeng 發表於 2021-06-15
Java

作者:Grey

原文地址: Java IO學習筆記五:BIO到NIO

準備環境

準備一個CentOS7的Linux例項:
例項的IP:
192.168.205.138

我們這次實驗的目的就是直觀感受一下BIO和NIO的效能差異

BIO

準備服務端程式碼:

import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * BIO Socket Server
 */
public class SocketServerBIOTest {
    private static final int PORT = 9090;
    private static final int BACK_LOG = 2;

    public static void main(String[] args) {
        ServerSocket server = null;
        try {
            server = new ServerSocket();
            server.bind(new InetSocketAddress(PORT), BACK_LOG);
            System.out.println("server started , port : " + PORT);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            // 接受客戶端連線
            while (true) {
                // 先阻塞,這樣客戶端暫時無法連線進來
//                System.in.read();

                // 這個方法也是阻塞的,如果沒有客戶端連線進來,會一直阻塞在這裡,除非設定了超時時間
                Socket client = server.accept();

                System.out.println("client " + client.getPort() + " connected!!!");
                // 客戶端連線進來後,開闢一個新的執行緒去接收並處理
                new Thread(() -> {
                    try {
                        InputStream inputStream = client.getInputStream();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                        char[] data = new char[1024];
                        while (true) {
                            int num = reader.read(data);
                            if (num > 0) {
                                System.out.println("client read some data is :" + num + " val :" + new String(data, 0, num));
                            } else if (num == 0) {
                                System.out.println("client read nothing!");
                                continue;
                            } else {
                                System.out.println("client read -1...");
                                System.in.read();
                                client.close();
                                break;
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

並且在Linux例項上執行這個程式碼,然後在自己本地的機器上準備客戶端的程式碼:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;


public class C10Kclient {

    public static void main(String[] args) {
        LinkedList<SocketChannel> clients = new LinkedList<>();
        InetSocketAddress serverAddr = new InetSocketAddress("192.168.205.138", 9090);
        for (int i = 10000,j = 10001; i < 65000; i+=2,j+=2) {
            try {
                SocketChannel client1 = SocketChannel.open();
                SocketChannel client2 = SocketChannel.open();
                client1.bind(new InetSocketAddress("192.168.205.1", i )); 
                client1.connect(serverAddr);
                clients.add(client1);

                client2.bind(new InetSocketAddress("192.168.205.1", j));
                
                client2.connect(serverAddr);
                clients.add(client2);

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("clients "+ clients.size());
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服務端每次用兩埠迴圈多次去和服務端建立連線,我們可以觀察服務端建立連線的速度,通過服務端列印的資訊可以感知到連線的速度。

[[email protected] socket]# javac SocketServerBIOTest.java && java SocketServerBIOTest
server started , port : 9090
client 10000 connected!!!
client 10001 connected!!!
client 10002 connected!!!
client 10003 connected!!!
client 10006 connected!!!
client 10007 connected!!!
client 10008 connected!!!
client 10004 connected!!!
client 10009 connected!!!
client 10010 connected!!!
client 10011 connected!!!
...

NIO

我們把服務端的BIO切換成NIO,服務端的程式碼改成如下:

import java.net.InetSocketAddress; 
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

public class SocketServerNIOTest {


    public static void main(String[] args) throws Exception {

        LinkedList<SocketChannel> clients = new LinkedList<>();

        ServerSocketChannel ss = ServerSocketChannel.open();  //服務端開啟監聽:接受客戶端
        ss.bind(new InetSocketAddress(9090));
        ss.configureBlocking(false); 

        while (true) {
        	
            SocketChannel client = ss.accept();
            if (client == null) {
             
            } else {
                client.configureBlocking(false); //重點  socket(服務端的listen socket<連線請求三次握手後,往我這裡扔,我去通過accept 得到  連線的socket>,連線socket<連線後的資料讀寫使用的> )
                int port = client.socket().getPort();
                System.out.println("client..port: " + port);
                clients.add(client);
            }

            ByteBuffer buffer = ByteBuffer.allocateDirect(4096); 
            //遍歷已經連結進來的客戶端能不能讀寫資料
            for (SocketChannel c : clients) {  
                int num = c.read(buffer);  
                if (num > 0) {
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);

                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }
            }
        }
    }
}

其中

ss.configureBlocking(false); 

即把服務端設定為非阻塞的,由於非阻塞,所以死迴圈中,程式碼不會卡在

SocketChannel client = ss.accept();

這裡一直不執行,而且也無須丟擲一個新的執行緒去接收客戶端。
當得到

client != null

的時候,即有新的客戶端連線進來,我們把這個clients加入到列表中,然後遍歷clients,去消費客戶端的請求。

同時,我們可以在服務端設定客戶端的非阻塞,即:

client.configureBlocking(false);

再次執行客戶端,並切換到服務端檢視列印日誌,速度比前面的BIO快了非常多。

[[email protected] socket]# javac SocketServerNIOTest.java && java SocketServerNIOTest
... 速度快很多...
client..port: 10000
client..port: 10001
client..port: 10002
client..port: 10003
client..port: 10004
client..port: 10005
client..port: 10006
client..port: 10007
client..port: 10008
client..port: 10009
client..port: 10010
client..port: 10011
...

為什麼BIO慢

因為每次連線都會發生兩次系統呼叫,一次是通過accept建立socket,另一次是呼叫clone方法丟擲一個執行緒。
而NIO只有一個執行緒(執行緒克隆的耗時就不存在了),而且作業系統也提供了對應的支援。所以要比BIO快很多。

原始碼

Github