一步一個腳印,從BIO到NIO

上次寫了一篇BIO筆記 (查看),聊了BIO。這次接着聊NIO。

NIO三大元素

Channel 通道

個人感覺,通道有點類似於BIO的Stream。只不過stream只是單向的要麼input,要麼output。而Channel則是雙向的並且支持非阻塞。具體實現包括 FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel等。本文重點用到後面兩個。

Buffer 緩存

用於保存數據的緩存,包括以下現實: ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer。具有非常靈添的操作。

Selector 多路複用器

可以通過多路複用器來管理多個通道,以及獲取通道觸發事件
通道 通過註冊的方式,添加偵聽事件,通過select()方法獲取可用的事件(比如可連接,可讀,可寫等操作)。

一步一個腳印,實例演示

1:實現多人聊天的工具,包括服務端,客戶端。

引用上一篇的 Soket IO的通信工具的代碼,如下:

服務端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerMain {

    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(4700);
        Socket socket = ss.accept();
        PrintWriter os=new PrintWriter(socket.getOutputStream());
        BufferedReader br =new BufferedReader(new InputStreamReader(System.in) );
        BufferedReader is =new BufferedReader(new InputStreamReader(socket.getInputStream()) );
        new Thread( ()->{
            while (true) {
                try {
                    System.out.println( is.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        while (true) {
            String str=br.readLine();
            os.println(str);
            os.flush();
        }
    }
}

客戶端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class ClientMain {

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

        Socket socket =new Socket("127.0.0.1",4700);
        PrintWriter os=new PrintWriter(socket.getOutputStream());
        BufferedReader br =new BufferedReader(new InputStreamReader(System.in) );
        BufferedReader is =new BufferedReader(new InputStreamReader(socket.getInputStream()) );

        new Thread( ()->{
            while (true) {
                try {
                    System.out.println( is.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        while (true) {
            String str=br.readLine();
            os.println(str);
            os.flush();
        }
    }
}

分碼分析:

1:上面實例的服務端與客戶端都是使用的BIO實現的。
2 :客戶端與服務端是一對一的。
3:服務端在 Socket socket = ss.accept(); 的時候產生blocking,等待連接客戶端連接。
4:服務端、客戶端 在is.readLine() 的時候產生blocking。

總結:

聊天工具那肯定是要支持多個客戶端的,不然跟服務端聊天多沒意思。
服務端可以只有一個,必須支持同時響應多個人,那就必須是非blocking的。

使用NIO改造 :

服務端

import sun.nio.ch.ThreadPool;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;

public class ServerMain {

    static volatile HashMap<String, SocketChannel> clients = new HashMap<>();
    static Queue<Msg> queue = new ArrayBlockingQueue<Msg>(1000);
    static ByteBuffer bfWrite = ByteBuffer.allocate(4000);
    static ByteBuffer bfRead = ByteBuffer.allocate(4000);


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


        new Thread(() -> {
            try {
                connect();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "connection").start();

        new Thread(() -> {
            try {
                read();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "read").start();


        new Thread(() -> {
            try {
                trans();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "trans").start();

    }

    public static void connect() throws IOException, InterruptedException {
        System.out.println("--------鏈接監聽開始-------");
        ServerSocketChannel channel = ServerSocketChannel.open();
        //偵聽   127.0.0.1 :11111
        channel.bind(new InetSocketAddress("127.0.0.1", 11111));
        //設置爲 非阻塞
        channel.configureBlocking(false);

        //循環執行
        while (true) {
            //等待連接,非阻塞,如果沒有連接,clien=null;
            SocketChannel client = channel.accept();
            //連接不爲空時
            if (client != null) {
                //連接設置 非阻塞
                client.configureBlocking(false);
                //把鏈接添加到 鏈接列表
                synchronized (clients) {
                    clients.put(String.valueOf(client.socket().getPort()), client);
                }
                System.out.println("添加鏈接:" + client.socket().getPort());
            }
            Thread.sleep(500);
        }
    }

    //讀取
    public static void read() throws IOException, InterruptedException {
        System.out.println("--------信息監聽開始-------");

        while (true) {
            synchronized (clients) {
                for (Map.Entry<String, SocketChannel> it : clients.entrySet()) {

                    //斷開連接的去掉
                    SocketChannel sc=it.getValue();
                    if (sc.isConnected() == false) {
                        clients.remove(it.getKey());
                        System.out.println("刪除連接:" + sc);
                        continue;
                    }

                    //嘗試讀取數據
                    if (sc.read(bfRead) > 0) {
                        bfRead.flip();
                        byte[] arr = new byte[bfRead.limit()];
                        bfRead.get(arr);
                        String str = new String(arr);
                        String [] msgArr =str.split(":");

                        if (msgArr == null || msgArr.length < 2) {
                            System.out.println("忽略信息:" + str);
                        }
                        else
                        {
                            Msg msg= new Msg( String.valueOf(sc.socket().getPort()),msgArr[0],msgArr[1]);
                            queue.add(msg);
                        }
                        bfRead.clear();
                    }
                }
            }
            java.lang.Thread.sleep(500);
        }
    }

    //信息轉發
    public static void trans() throws IOException, InterruptedException {
        System.out.println("--------信息轉發開始-------");
        while (true) {
            Msg msg = queue.poll();

            if (msg != null) {
                SocketChannel c = clients.get(msg.to);
                bfWrite.clear();
                bfWrite.put( String.format("%s:%s",msg.from,msg.msg) .getBytes());
                bfWrite.flip();
                c.write(bfWrite);
            }
            Thread.sleep(500);
        }

    }

    public static class Msg {
        public String to;
        public String from;
        public String msg;
        public Msg( String from,String to, String msg) {
            this.to = to;
            this.from = from;
            this.msg = msg;
        }
        public String getTo() {
            return to;
        }
        public void setTo(String to) {
            this.to = to;
        }
        public String getFrom() {
            return from;
        }
        public void setFrom(String from) {
            this.from = from;
        }
        public String getMsg() {
            return msg;
        }
        public void setMsg(String msg) {
            this.msg = msg;
        }
    }
}

客戶端

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

public class ClientMain {

    static SocketChannel sc;
    static ByteBuffer bfWrite = ByteBuffer.allocate(4000);
    static ByteBuffer bfRead = ByteBuffer.allocate(4000);

    public static void main(String[] args) throws IOException, InterruptedException {

        sc = SocketChannel.open();
        sc.configureBlocking(false); //設置爲非阻塞
        sc.connect(new InetSocketAddress("127.0.01", 11111));
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        try {
            if (sc.finishConnect()) {
                //啓動讀線程
                new Thread(() -> {
                    try {
                        read();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }, "readThread").start();
                //寫入
                while (true) {
                    String msg = br.readLine();
                    bfWrite.clear();
                    bfWrite.put(msg.getBytes());
                    bfWrite.flip();
                    System.out.println("發送信息:" + msg);
                    sc.write(bfWrite);
                    Thread.sleep(200);
                }
            }
        } finally {
            sc.finishConnect();
        }
    }


    public static void read() throws InterruptedException, IOException {
        while (true) {
            if (sc.read(bfRead) > 0) {
                bfRead.flip();
                byte[] arr = new byte[bfRead.limit()];
                bfRead.get(arr);
                String str = new String(arr);
                String[] msgArr = str.split(":");
                //不符合格式的數據
                if (msgArr == null || msgArr.length < 2) {

                    continue;
                }
                System.out.println(String.format("來自%s的信息:%s", msgArr[0], msgArr[1]));
                bfRead.clear();
            }
            Thread.sleep(500);
        }

    }

}

把它放到服務器上。並運行起來,開啓一個服務器實例,開啓兩個客戶端實例。運行效果如下:

服務器
在這裏插入圖片描述
客戶端1:
在這裏插入圖片描述
額戶端2
在這裏插入圖片描述

代碼分析:

服務端

1:使用了NIO 的channel,並設置爲 非阻塞。

 ServerSocketChannel channel = ServerSocketChannel.open();
        //偵聽   127.0.0.1 :11111
        channel.bind(new InetSocketAddress("127.0.0.1", 11111));
        //設置爲 非阻塞
        channel.configureBlocking(false);

2:因爲是非阻塞的,資源的調用需要考慮多線程。多處引入synchronize

 static volatile HashMap<String, SocketChannel> clients = new HashMap<>();

3:服務端包括分爲三個線程,分別是,連接,讀取信息,轉發信息。
連接:如果有新的連接進來,則把連接放入clients
讀取:循環嘗試去讀取各個連接的數據,如有數據則寫入轉發隊列
轉發:循環讀取隊列的數據,根據 消息to 找到目標連接,並把數據發送。

4:代碼優化,爲了更好的應對業務,引入隊列、消息封裝等。

客戶端

1:同樣使用了NIO 的channel,並設置爲 非阻塞。

  sc = SocketChannel.open();
  sc.configureBlocking(false); //設置爲非阻塞
  sc.connect(new InetSocketAddress("127.0.01", 11111));

總結:

1:聊天的功能基本能實現。
2:但是能效方面偏低,比如服務器的信息讀取。假如說有10000個連接,那就得遍歷10000次。如果能做到哪個連接有信息就讀哪個連接,做到目標精確就好了。

引入多路複用器 selector

服務端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;

public class ServerMain2 {

    static volatile HashMap<String, SocketChannel> clients = new HashMap<>();
    static Queue<Msg> queue = new ArrayBlockingQueue<Msg>(1000);
    static Selector selector;

    public static void main(String[] args) throws IOException, InterruptedException {
        selector = Selector.open();
        Selector selector = Selector.open();
        ServerSocketChannel channel = ServerSocketChannel.open();
        //偵聽   127.0.0.1 :11111
        channel.bind(new InetSocketAddress("127.0.0.1", 11111));
        //設置爲 非阻塞
        channel.configureBlocking(false);
        //通道註冊到selector, 類型爲連接
        channel.register(selector, SelectionKey.OP_ACCEPT);

        new Thread(()->{
            try {
                trans();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"trans").start();

        while (true) {
            if (selector.select(1000) == 0) {
                continue;
            }
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                iter.remove();

                if (key.isAcceptable()) {
                    handleAccept(key);
                }
                if (key.isReadable()) {
                    handleRead(key);
                }
            }
            Thread.sleep(500);
        }
    }

    public static void handleAccept(SelectionKey key) throws IOException {

        System.out.println("-------handleAccept---------");
        ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
        SocketChannel sc = ssChannel.accept();
        sc.configureBlocking(false);
        sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(4000));


        //把鏈接添加到 鏈接列表
        synchronized (clients) {
            clients.put(String.valueOf(sc.socket().getPort()), sc);
        }
        System.out.println("添加鏈接:" + sc.socket().getPort());

    }

    public static void handleRead(SelectionKey key) throws IOException {

        System.out.println("-------handleRead---------");
        SocketChannel sc = (SocketChannel) key.channel();
        ByteBuffer buf = (ByteBuffer) key.attachment();
        long bytesRead = sc.read(buf);
        while (bytesRead > 0) {
            buf.flip();
            while (buf.hasRemaining()) {
                byte[] arr = new byte[buf.limit()];
                buf.get(arr);
                String str = new String(arr);
                String[] msgArr = str.split(":");
                if (msgArr == null || msgArr.length < 2) {
                    System.out.println("忽略信息:" + str);
                } else {
                    Msg msg = new Msg(String.valueOf(sc.socket().getPort()), msgArr[0], msgArr[1]);
                    queue.add(msg);
                }
            }
            System.out.println();
            buf.clear();
            bytesRead = sc.read(buf);
        }
        if (bytesRead == -1) {
            sc.close();
        }
    }
    //信息轉發
    public static void trans() throws IOException, InterruptedException {
        ByteBuffer bfWrite =ByteBuffer.allocate(4000);

        while (true) {
            Msg msg = queue.poll();

            if (msg != null) {
                SocketChannel c = clients.get(msg.to);
                bfWrite.clear();
                bfWrite.put(String.format("%s:%s", msg.from, msg.msg).getBytes());
                bfWrite.flip();
                c.write(bfWrite);
            }
            Thread.sleep(500);
        }

    }

    public static class Msg {
        public String to;
        public String from;
        public String msg;
        
        public Msg(String from, String to, String msg) {
            this.to = to;
            this.from = from;
            this.msg = msg;
        }
        public String getTo() {
            return to;
        }
        public void setTo(String to) {
            this.to = to;
        }
        public String getFrom() {
            return from;
        }
        public void setFrom(String from) {
            this.from = from;
        }
        public String getMsg() {
            return msg;
        }
        public void setMsg(String msg) {
            this.msg = msg;
        }
    }
}

代碼分析:

大概流程如下圖所示:
在這裏插入圖片描述

相對之前的版本,做了以下調整。
1:使用多路複用器。

 selector = Selector.open();

2:通道註冊到多路複用器

        ServerSocketChannel channel = ServerSocketChannel.open();
        //偵聽   127.0.0.1 :11111
        channel.bind(new InetSocketAddress("127.0.0.1", 11111));
        //設置爲 非阻塞
        channel.configureBlocking(false);
        //通道註冊到selector, 類型爲連接
        channel.register(selector, SelectionKey.OP_ACCEPT);

3:偵聽 連接以及可讀事件。取代了原先開始兩個線程(連接線程,讀取線程)遍歷的方式。

while (true) {
            if (selector.select(1000) == 0) {
                continue;
            }
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                iter.remove();
                if (key.isAcceptable()) {
                    handleAccept(key);
                }
               else  if (key.isReadable()) {
                    handleRead(key);
                }
            }
            Thread.sleep(500);
        }

演示效果:
服務端:
在這裏插入圖片描述

客戶端1:
在這裏插入圖片描述

客戶端2:
在這裏插入圖片描述

總結:

1:NIO的三大要素在本例子已經使用到了。
2:再次總結一下使用的好處。
1):channel ,支持not blocking。SeverSocketChanel 支持一個通道連接多個客戶端。
2):Buffer,可以定義固定的一塊內存,支持clear, flip等操作。可以重複利用,減少GC。
3):selector, 管理channel,偵聽channel的事件。減少白忙添,增加能效。從以前的循環嘗試讀數據,到有數據纔去讀。同樣連接也是。減少用戶態/核生態的切換。

寫在最後。
感覺NIO有點複雜,代碼量有點多。。是不是應該有更好的封裝呢?
未完,待續。。。。

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