nio探索

單線程bio

我們首先要解決的問題是:爲什麼要使用nio?

nio除了叫new io之外,還叫做non-blocking io,也就是非阻塞io,可見這一塊的重要性。

阻塞與非阻塞,我們主要講網絡編程。

傳統的BIO(Blocking IO):

寫一個server:

package nio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws IOException {
        byte[] contents = new byte[1024];
        ServerSocket serverSocket = new ServerSocket(8088);
        while (true){
            System.out.println("--------------------split---------------------------");
            System.out.println("waiting connection from client....");
            Socket socket = serverSocket.accept();
            System.out.println("connected!");

            System.out.println("waiting data from client....");
            InputStream inputStream = socket.getInputStream();

            int read = inputStream.read(contents);
            if(read>0){
                System.out.println("data from client has been received!");
                System.out.println("the data is: " + new String(contents));
            }

        }

    }
}

這裏有兩個地方會阻塞:

 Socket socket = serverSocket.accept();

只有當客戶端來連接,阻塞纔會解除。

 InputStream inputStream = socket.getInputStream();
 int read = inputStream.read(contents);

從客戶端讀數據也會阻塞。

我寫兩個客戶端(或者你一個客戶端run兩次也可以):

package nio;

import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1",8088);
        System.out.println("Client tries to connect server...");
      
            Scanner scanner = new Scanner(System.in);
            System.out.println("please write something...");
            String next = scanner.next();
            socket.getOutputStream().write(next.getBytes());
            System.out.println("***data from client has sent!");
        
    }
}

Client.java

package nio;

import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

public class Client2 {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 8088);
        System.out.println("Client2 tries to connect server...");
        Scanner scanner = new Scanner(System.in);
        System.out.println("please write something...");
        String next = scanner.next();
        socket.getOutputStream().write(next.getBytes());
        System.out.println("***data from client has sent!");
    }
}



Client2.java

開啓server,先用client連。

--------------------split---------------------------
waiting connection from client....
--------------------split---------------------------
waiting connection from client....

connected!
waiting data from client....

連上了。

但是卡在等待數據那裏了。

這時候我們開啓client2。

--------------------split---------------------------
waiting connection from client....

connected!
waiting data from client....

server沒有反應,表示沒有連上。

對於client2來說,是卡在accept那裏了。

如果此時client發送數據了:

Client tries to connect server...
please write something...
client
***data from client has sent!

Process finished with exit code 0

那麼client2就會連上:

--------------------split---------------------------
waiting connection from client....

connected!
waiting data from client....
data from client has been received!
the data is: client                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
--------------------split---------------------------
waiting connection from client....
connected!
waiting data from client....

這是一個問題。

一個客戶端不可能只發一條數據,它應該保持發送數據的狀態,所以我們在client上面加上while(true):

package nio;

import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 8088);
        System.out.println("Client tries to connect server...");
        while (true) {
            Scanner scanner = new Scanner(System.in);
            System.out.println("please write something...");
            String next = scanner.next();
            socket.getOutputStream().write(next.getBytes());
            System.out.println("***data from client has sent!");
        }

    }
}

重啓server再次測試:

--------------------split---------------------------
waiting connection from client....
connected!
waiting data from client....

讓client連接上:

Client tries to connect server...
please write something...
hello
***data from client has sent!
please write something...

當client發送一條數據後,它還保持着發送數據的狀態。

--------------------split---------------------------
waiting connection from client....
connected!
waiting data from client....
data from client has been received!
the data is: hello                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
--------------------split---------------------------
waiting connection from client....

但是server接受到一條數據後就斷開與client的連接了,它又在等待一個新的連接,client此時再發數據server是接收不到的。

由此我們得出一個結論:單線程無法解決併發問題。


多線程bio

我們的思路是通過多線程解決。

一個client連過來,服務端就會產生一個socket,此時開一條線程將socket傳進去,以此與client通信。

package nio;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;


public class Server {
    int count = 0;
    public static void main(String[] args) throws IOException {
        Server server = new Server();
        ServerSocket serverSocket = new ServerSocket(8088);
        while (true) {
            System.out.println("--------------------split---------------------------");
            System.out.println("waiting connection from client....");
            final Socket socket = serverSocket.accept();
            server.count++;
            Thread thread = new Thread(new IOWithClient(server.count,socket));
            thread.start();
        }
    }
}

class IOWithClient implements Runnable{

    int count;
    Socket socket;
    byte[] contents = new byte[1024];

    public IOWithClient(int count, Socket socket) {
        this.count = count;
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            System.out.println("client" + (count) + " connected!");
            System.out.println("waiting data from client" + (count) + "....");
            while (true) {

                InputStream inputStream = socket.getInputStream();
                int read = inputStream.read(contents);
                if (read > 0) {
                    System.out.println("data from client"+(count)+" has been received!");
                    System.out.println("the data is: " + new String(contents));
                }
            }
        } catch (IOException e) {
            if(e.getMessage().equals("Connection reset")){
                System.out.println("client" + count + " disconnected...");
            }else{
                e.printStackTrace();
            }
        }
    }
}


我這裏把task拎出來結構會不會清晰一點。

這裏我用了count來保存第幾個客戶端連過來了,目的是爲了控制檯打印的時候清晰一點。

這當然能夠解決所有的問題(併發的問題),但是它太消耗資源了。

new一個thread,可是很耗內存的。況且,成千上萬個thread,最終能有信息通信的又有幾個呢?

比如你上淘寶,淘寶爲每一個用戶都new一個thread來處理,真正發生數據交互的(比如買東西)會有多少?大多數人只是隨便逛逛。

就算是用線程池來重用線程,還是多線程。我們需要用一個單線程來解決問題!

這時候,nio就登場了。

(難道bio就沒用了嗎?如果客戶端和服務端的io很頻繁,100個連接有99個一直在傳輸數據,當然是可以用bio的)。


nio的模型

我們已經學過FileChannelByteBuffer了,在網絡通信中,數據的傳遞同樣是建立channel

package nio;

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

public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1",9090));
        System.out.println("Client tries to connect server...");
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        while (true) {
            Scanner scanner = new Scanner(System.in);
            System.out.println("please write something...");
            String next = scanner.next();
            byteBuffer.put(next.getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
            System.out.println("***data from client has sent!");
        }

    }
}

Client.java

package nio;

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

public class Client2 {

    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1",9090));
        System.out.println("Client2 tries to connect server...");
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        while (true) {
            Scanner scanner = new Scanner(System.in);
            System.out.println("please write something...");
            String next = scanner.next();
            byteBuffer.put(next.getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
            System.out.println("***data from client2 has sent!");
        }
    }
}



Client2.java

客戶端這裏我們通過SocketChannel去連接遠程機器。

連上以後會卡在scanner那裏。

然後是server部分:

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * non-blocking server
 */
public class TomcatServer {
    static ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) {
        SocketChannel socketChannel = null;
        ServerSocketChannel serverSocketChannel = null;
        try {
            serverSocketChannel = ServerSocketChannel.open();
            SocketAddress socketAddress = new InetSocketAddress(9090);
            serverSocketChannel.bind(socketAddress);

            serverSocketChannel.configureBlocking(false);

            while(true){
                //check if there's any information from the connected client
                //if there is, print them.
                //else,do nothing,and continue checking
                Iterator<SocketChannel> iterator = channelList.iterator();
                int read = 0;
                while(iterator.hasNext()) {

                    try {
                        read = iterator.next().read(byteBuffer);
                    } catch (IOException e) {
                        System.out.println(e.getMessage());
                        iterator.remove();
                        System.out.println("the number of the clients are: " + channelList.size());
                    }
                    if (read > 0) {

                        String data = new String(byteBuffer.array(),0,read);
                        System.out.println("the data from client is : " + data);
                        byteBuffer.clear();
                    }
                }
                //this is non-block
                //whether there's client trying to connect the server, following codes will be executed
                socketChannel = serverSocketChannel.accept();

                if(socketChannel != null){
                    System.out.println("connected!");
                    //make socket non-block
                    //thus IO between server and client has no blocking.
                    socketChannel.configureBlocking(false);
                    //for further checking
                    channelList.add(socketChannel);

                    System.out.println(channelList.size() + " clients have connected the server.");
                }
            }

        } catch (IOException e) {
           e.printStackTrace();
        }finally {
            if(socketChannel != null){
                try {
                    socketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if(serverSocketChannel != null){
                try {
                    serverSocketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

首先代碼裏面除了main線程沒有其他thread,所以是單線程的。

每來一個連接,都會建立一條channel,所以我們用一個channelList來存儲所有連上的客戶端。

 serverSocketChannel.configureBlocking(false);

首先我們確保serverSocketChannel的非阻塞,這樣一個client連上後,另一個也能連。換句話說,socketChannel = serverSocketChannel.accept();不會阻塞。

然後進入while true

邏輯是這樣的:取出所有已經連上的channel,一個個遍歷,裏面要是有信息,就打印出來,要是沒有就算了。

這裏的while true保證服務端一直監聽客戶端消息的狀態。

我們可以想想,如果不這麼設計的話,如果是accept之後再read,這麼做會有問題的:


代碼實現是:

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * non-blocking server
 */
public class TomcatServer {
    static ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) {
        SocketChannel socketChannel = null;
        ServerSocketChannel serverSocketChannel = null;
        try {
            serverSocketChannel = ServerSocketChannel.open();
            SocketAddress socketAddress = new InetSocketAddress(9090);
            serverSocketChannel.bind(socketAddress);

            serverSocketChannel.configureBlocking(false);

            while (true) {

                socketChannel = serverSocketChannel.accept();

                if (socketChannel != null) {
                    System.out.println("connected!");
                    //make socket non-block
                    //thus IO between server and client has no blocking.
                    socketChannel.configureBlocking(false);
                    //for further checking
                    channelList.add(socketChannel);

                    System.out.println(channelList.size() + " clients have connected the server.");

                    int read = socketChannel.read(byteBuffer);

                    if (read > 0) {
                        System.out.println("content from client is: " + new String(byteBuffer.array()));
                    }

                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socketChannel != null) {
                try {
                    socketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (serverSocketChannel != null) {
                try {
                    serverSocketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

就算有一個客戶端連上了,也不能收到客戶端發來的消息,因爲一直while循環,所以我們要把讀取客戶端消息的代碼提到前面去。

 socketChannel.configureBlocking(false);

保證了與客戶端io的非阻塞,即read = next.read(byteBuffer);是非阻塞的。


遺留的問題

以上,用單線程實現了併發,即是所謂的nio。然而,這依然是有問題的,因爲死循環裏面遍歷channelList並檢查裏面是否有內容很消耗資源,我們希望,是不是能夠把這個遍歷的任務交給操作系統函數去做,這樣會不會更快?

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