NIO模型

超大充電寶發表於2020-12-03


通過BIO+ 多執行緒方式可以解決掉多使用者連線的問題,隨著使用者連線的增多,就會無限的建立新的執行緒處理使用者連線但是執行緒的不能無限建立的,因為

  1. 執行緒建立多時,系統需要排程的執行緒就會多,執行緒的上下文切換會消耗很多的系統效能
  2. 每一個執行緒都有自己的棧空間,假如每一個執行緒佔用1M的棧空間,建立1000個執行緒,需要1000M

怎麼解決這個問題?

可以通過執行緒池來解決、複用執行緒達到減少執行緒建立的目的,假如執行緒池提供4個執行緒來做處理,假如執行緒池中4個執行緒都在read資料,進入到阻塞狀態。這個時候是不能接收新使用者的連線。


一:NIO模型簡介

NIO模型最大的特點是將IO 的系統呼叫設定為非阻塞,進行系統呼叫時 ,如果資料準備好則會立即拿到資料執行,如果資料沒有準備,會立即返回一個狀態碼(返回-1,ERR:),使用者可以做自己事情,在一定時間需要繼續查詢

IO複用模型,是系統提供了(select、poll)方式用來同時監聽多個使用者的請求,一旦有事件完成則將結果通知給使用者執行緒進行處理

NIO模型:說的是系統呼叫會立即返回

IO複用是指複用器select等同時監聽多個使用者的連線

IO複用結合BIO來,常使用的是IO複用+NIO模型來使用,才是真正意義上同步非阻塞模型

NIO中提供了選擇器(Selector 類似底層作業系統提供的IO複用器:select、poll、epoll),也叫做多路複用器,作用是檢查一個或者多個NIO Channel(通道)的狀態是否是可讀、可寫······可以實現單執行緒管理多個channel,也可以管理多個網路請求

Channel:通道,用於IO操作的連線,在Java.nio.channels包下定義的,對原有IO的一種補充,不能直接訪問資料需要和緩衝區Buffer進行互動
通道主要實現類:
SocketChannel:通過TCP讀寫網路中的資料,一般客戶端的實現
ServerSocketChannel:監聽新進來的TCP連線,對每一個連線都需要建立一個SocketChannel,一般是服務端的實現
Buffer:緩衝區

IO流中的資料需要經過緩衝區交給Channel
在這裡插入圖片描述

buffer的作用就是使用者和channel通道進行資料交流的橋樑


二:NIO程式設計

服務端

public class Server {

    public static void main(String[] args) {
        //建立服務端ServerSocketChannel例項
        ServerSocketChannel serverSocketChannel;
        {
            try {
                serverSocketChannel = ServerSocketChannel.open();

                //繫結埠
                serverSocketChannel.bind(new InetSocketAddress(6666));
                System.out.println("服務端啟動啦");

                //監聽

                //設定serverSocketChannel為非阻塞
                serverSocketChannel.configureBlocking(false);

                //建立selector複用器
                Selector selector = Selector.open();

                //將監聽事件註冊到複用器上
                serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

                //等待系統返回已完成的事件
                //select()本身是會阻塞,等系統告訴使用者空間那些事件已經準備就緒,返回結果表示已準備完成的事件個數

                while (selector.select() > 0) {
                    //感興趣事件集合(指的就是註冊到selector中的事件)
                    Set <SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator <SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey selectionKey = iterator.next();
                        //刪除掉已經完成的事件
                        iterator.remove();
                        if (selectionKey.isAcceptable()) {
                            //當前是可接收事件已經準備就緒
                            ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();

                            //接收客戶端連線,返回一個SocketChannel例項表示是客戶端的連線
                            SocketChannel socketChannel = serverSocketChannel1.accept();
                            System.out.println("客戶端連線上");

                            //設定SocketChannel例項為非阻塞
                            socketChannel.configureBlocking(false);

                            //將SocketChannel註冊到複用器上,並關注讀事件
                            socketChannel.register(selector,SelectionKey.OP_READ);
                        }

                        if (selectionKey.isReadable()) {
                            //當前有可讀事件發生
                            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

                            //讀資料
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            //往Buffer中寫資料
                            int read = socketChannel.read(buffer);
                            if (read == -1) {
                                //客戶端已經關閉
                                socketChannel.close();
                                continue;
                            }
                            //進行讀寫模式的切換
                            buffer.flip();

                            byte[] bytes = new byte[buffer.remaining()];
                            //讀取buff資料
                            buffer.get(bytes);
                            //接收資料
                            String msg = new String(bytes);

                            System.out.println("客戶端:"+socketChannel.getRemoteAddress()+" 傳送資料"+msg);


                            String recv = "[echo]:"+msg;
                            //回覆訊息

                            //先將Buffer清空
                            buffer.clear();

                            //往Buffer寫資料
                            buffer.put(recv.getBytes());

                            //讀寫模式切換
                            buffer.flip();
                            //將Buffer資料讀到channel通道
                            socketChannel.write(buffer);

                            //業務斷開
                            if ("exit".equals(msg)) {
                                socketChannel.close();
                            }
                        }
                    }
                }

                //來輪序是什麼事件完成


            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

1、建立ServerSocketChannel例項
2、對通道serverSocketChannel進行埠繫結bind
3、將通道設定為非阻塞configureBlocking設定為false
4、建立複用器例項Selector(Selector.open())
5、將serverSocketChannel註冊到複用器上,並關注ACCEPT事件
6、等待系統返回已完成事件集合(select)
7、通過遍歷感興趣事件集合
8、如果是accept事件完成,則進行accept操作,接收客戶端連線,將客戶端連線channel設定為非阻塞,並關注read事件
9、迴圈第6步,
10、如果是read事件完成,則進行讀資料操作

客戶端

public class Client {
    public static void main(String[] args) {
        try {
        //建立SocketChannel通道
            SocketChannel socketChannel = SocketChannel.open();

            //設定socketChannel為非阻塞
            socketChannel.configureBlocking(false);

            //例項化複用器
            Selector selector = Selector.open();

            //連線服務端,該connect不會阻塞,會立即返回 boolean true:連線成功  false:表示還未連線成功
            if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 6666))){
                //表示連線不成功 當前正在連線,將當前的可連線事件交給核心幫助監聽
                socketChannel.register(selector,SelectionKey.OP_CONNECT);

                //等待連線完成
                selector.select();
                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();

                    if (selectionKey.isConnectable()) socketChannel.finishConnect();
                }
            }

            //要麼是連線成功
            //給服務端傳送資料
            Scanner scanner = new Scanner(System.in);
            String msg = null;

            //註冊讀事件
            socketChannel.register(selector,SelectionKey.OP_READ);

            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while ((msg =scanner.nextLine()) != null) {
                //重複性讀操作
                buffer.clear();

                msg +="\n";
                //將資料寫入到Buffer中
                buffer.put(msg.getBytes());
                //讀寫模式切換
                buffer.flip();

                //將資料從 Buffer中寫入channel通道,對於Buffer而言,是讀取資料
                socketChannel.write(buffer);



                //等核心資料準備完成
                selector.select();

                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();

                    if (selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();

                        ByteBuffer allocate = ByteBuffer.allocate(1024);
                        //將資料從通道寫入Buffer
                        channel.read(allocate);

                        //讀寫模式切換
                        allocate.flip();

                        //remaining 實際讀取的資料長度
                        byte[] bytes = new byte[allocate.remaining()];


                        //將Buffer資料讀到byte陣列中
                        allocate.get(bytes);


                        String recv = new String(bytes);

                        System.out.println(recv);

                    }
                }

                //判斷是否結束
                if ("exit".equals(msg)) {
                    break;
                }


            }



            //關閉資源
            selector.close();
            socketChannel.close();

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

1、建立SocketChannel的通道例項
2、將通道SocketChannel設定為非阻塞
3、例項化複用器(Selector.open)
4、主動連線服務端(connect),當前會立即返回,返回結果為true,則表示連線成功
5、如果返回為false,將通道socketChannel註冊到複用器中,等待系統返回連線成功。呼叫finishConnect
6、和服務端進行讀寫操作(通過通道進行讀寫,需要藉助Buffer)


問題1:客戶端為什麼主動connect連線

在這裡插入圖片描述
在BIO中connect操作是一個阻塞方法,在NIO中設定為非阻塞,交給IO複用器來進行監聽關注的事件是否完成(CONNECT事件),核心來幫助監聽感興趣事件是否完成,前提是事件必須觸發,發生之後核心才能夠監聽(connect),在NIO中SocketChannel是設定為非阻塞,當前操作會立即返回,當呼叫connect之後,會立即返回一個Boolean型別的結果,表示是連線成功還是正在連線重,當前的connect事件才觸發,觸發之後核心才能幫助監聽,當然前提是將connect事件註冊到複用器上,核心才能關注到該事件。


問題2:客戶端斷開連線之後,服務端一致迴圈介面有可讀事件,且為空

在這裡插入圖片描述
客戶端斷開連線,服務端會接收到-1,佔用空間,服務端認為是有資料可以讀取,就會一直有可讀事件發生需要服務端處理,判斷通道接收是否為-1,是則結束接收


問題3:為什麼寫沒有註冊到複用器上

寫操作在NIO中也是一個事件,注意:寫事件是需要主動發起寫操作,一般寫完之後立即write操作不會進行阻塞,即通常寫操作並不需要註冊。


三:NIO模型詳解

1、channel(通道)

channel是和使用者的操作IO相連,但是通道不能直接使用(需要使用Buffer)

讀操作:從channel裡讀取的資料通過Buffer交給使用者
寫操作:將使用者要傳送的資料通過Buffer交給channel

在這裡插入圖片描述
channel和流區別時什麼?

  1. channel的資料讀寫操作必須要藉助Buffer緩衝,流是不需要的
  2. channel是即可以讀資料也可以寫資料,流是單向的,要麼讀,要麼寫

使用示例:讀操作

在這裡插入圖片描述
channel的主要實現類

  • FileChannel:使用者讀取,寫入檔案的通道
  • SocketChannel:通過TCP讀寫網路中的資料通道,一般客戶端的實現,主要用來連線服務端(connect)
  • ServerSocketChannel:通過TCP來讀寫網路中的資料通道,一般是服務端實現,用來監聽(accept)客戶端通道的連線
  • DatagramChannel:通過UDP來讀寫網路中的資料通道

2、Buffer 緩衝區

Java NIO 的 Buffer 用於和 **NIO Channel(通道)**互動。資料是從通道讀入緩衝區,從緩衝區寫入到通道中。緩衝區本質上是塊可以寫入資料,再從中讀資料的記憶體。該記憶體被包裝成 NIO 的 Buffer 物件,並提供了一系列方法,方便開發者訪問該塊記憶體

(1)基本用法

使用Buffer讀寫資料一般四步走:

  1. 寫資料到 Buffer
  2. 呼叫 buffer.flip()
  3. 從 Buffer 中讀取資料
  4. 呼叫 clear() 或 compact()

當向 buffer 寫資料時,buffer 會記錄寫了多少資料。一旦要讀取資料,需通過 flip() 將 Buffer 從寫模式切到讀模式。在讀模式下,可讀之前寫到 buffer 的所有資料。一旦讀完資料,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:呼叫 clear()compact() 方法。
5. clear() 會清空整個緩衝區
6. compact() 只會清除已經讀過的資料。任何未讀資料都被移到緩衝區的起始處,新寫入的資料將放到緩衝區未讀資料的後面。

使用示例

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();

(2)Buffer實現

Buffer的實現底層是通過特定型別(byte、long…)陣列來儲存資料
陣列中資料的操作需要藉助4個指標來操作:

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;   //標記
private int position = 0; //位置
private int limit; //限制
private int capacity; //容量

標記、位置、限制和容量值遵守以下不變式

0<=標記<=位置<=限制<=容量

新建立的緩衝區總有一個0位置和一個未定義的標記。初始限制可以為0,也可以為其他值,這取決於緩衝區型別及其構建方式。一般情況下,緩衝區的初始內容是未定義的。

在這裡插入圖片描述
capacity:
作為一個記憶體塊,Buffer有個固定大小,即capacity。你只能往裡寫capacity個byte、long,char等。一旦Buffer滿,需將其清空(通過讀或清除資料)才能繼續往裡寫資料。

position:
取決於Buffer處在讀還是寫模式:

  • 寫資料到Buffer時,position表示當前位置。初始的position值為0,當一個byte、long等資料寫到Buffer後, position會向前移動到下一個可插入資料的Buffer單元。所以position最大可為capacity–1。
  • 讀資料時,也是從某特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置為0。當從Buffer的position處讀取資料時,position向前移動到下一個可讀的位置。

limit:

  • 寫模式下,表示最多能往Buffer寫多少資料。所以此時limit=capacity。
  • 讀模式時, limit表示最多能讀到多少資料。

因此,當切換Buffer到讀模式時,limit會被設定成寫模式下的position值。即你能讀到之前寫入的所有資料(limit被設定成已寫資料的數量,這個值在寫模式下就是position)。


讀寫實現過程指標變化
在這裡插入圖片描述


(3)Buffer型別

Buffer是一個抽象類,其實現的子類有ByteBuffer(位元組緩衝區)、CharBuffer(字元緩衝區) ·······

Java NIO中Buffer有如下型別:
在這裡插入圖片描述
這些Buffer型別代表了不同的資料型別,即可通過這些型別來操作緩衝區中的位元組。

緩衝區有兩種:堆上開闢的空間,堆外開闢的空間


(4)Buffer的分配

要想獲得一個Buffer物件首先要進行分配。每個Buffer類都有一個allocate方法。

Buffer的建立:

ByteBuffer為例:

  1. ByteBuffer allocate(int capacity):在堆上建立指定大小的緩衝
  2. ByteBuffer allocateDirect(int capacity):在堆外空間建立指定大小的緩衝
  3. ByteBuffer wrap(byte[] array):通過byte陣列例項建立一個緩衝區
  4. ByteBuffer wrap(byte[] array, int offset, int length) :指定byte資料中的內容寫入到一個新的緩衝區

在這裡插入圖片描述


向Buffer寫資料

寫資料到Buffer有兩種方式:

  1. 從Channel寫到Buffer
    int bytesRead = inChannel.read(buf);
  2. 通過Buffer的put()方法寫到Buffer裡
    buf.put(127);

從Buffer讀資料

從Buffer讀資料有兩種方式:

  1. 從Buffer讀取資料到Channel
    int bytesWritten = inChannel.write(buf);
  2. 通過Buffer的get()方法從Buffer讀取資料
    buf.get();

flip()方法:

flip()方法將Buffer從寫模式切換到讀模式。呼叫flip()方法會將position設回0,並將limit設定成之前position的值。換句話說,position現在用於標記讀的位置,limit表示之前寫進了多少個byte、char等 —— 現在能讀取多少個byte、char等。


3、Selector(複用器)

選擇器或者叫做IO複用器,作用是用來檢查一個或者是多個NIO Channel的狀態是否是可讀,可寫…,可以實現單執行緒管理多個channel,也可以實現多執行緒管理channel

(1)selector的使用

  1. 建立一個selector的例項
    通過呼叫selector的open方法例項出物件
    Selector selector = Selector.open();
  2. 註冊channel上事件
    註冊關注事件,channel提供的register方法,第一個引數傳遞的是selector例項,第二個引數傳遞的是感興趣事件
    serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
  3. 使用selector監聽器來監聽事件是否完成
    等待核心返回就緒事件
    selector.select();
    阻塞等待核心返回結果,告訴使用者那些關注的事件完成
  4. 遍歷感興趣事件集合
    Set <SelectionKey> selectionKeys = selector.selectedKeys();
    返回的是Set型別集合,通過迭代器進行遍歷
    感興趣集合就是已註冊並且已準備就緒的事件
  5. 如果還有關注事件,則跳轉到第三步繼續監聽
  6. 最終關閉複用器

(2)SelectableChannel

關於通過SelectionKey的channel()方法返回的型別可以強轉為不同的channel型別,如下:

在這裡插入圖片描述
channel()方法的宣告如下:

public abstract SelectableChannel channel();

返回的是SelectableChannel型別

其中ServerSocketChannelSocketChannel型別都是SelectableChannel的子類,SelectableChannel是一個抽象類
在NIO中使用的configureBlockingregister都是定義在SelectableChannel類中的方法,ServerSocketChannelSocketChannel都是直接繼承SelectableChannel中的方法


register方法是需要接受兩個引數

SelectionKey register(Selector sel, int ops)

第二個引數是“感興趣的集合”,這個集合是selector需要監聽channel對什麼事件感興趣,可以監聽的事件型別有四種:connect、read、write、accept事件

(3)SelectionKey

SelectionKey中提供了感興趣事件,總共有四種

    public static final int OP_READ = 1 << 0;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_ACCEPT = 1 << 4;

佔用了int的4個bit位

提供的一些方法介紹

boolean isValid() :當前感興趣事件是否有效
void cancel():取消事件:告訴核心,註冊的某一個事件不需要在關注了
boolean isAcceptable() :判斷當前是否是可接受事件

selectionKey.channel():返回的是該selectionKey對應的channel
selectionKey.selector();返回該selectionKey對應的selector例項
int readyOps():返回需要監控的可讀事件

(4)selector選擇過程

三種鍵集合
已註冊鍵的集合、已選擇鍵的集合、已取消鍵的集合

  • 已註冊鍵的集合:
    所有的註冊到選擇器上的事件都會放到已註冊事件集合上,包含已經失效的鍵,通過keys()方法獲取
    selector.keys();//已註冊事件集合

  • 已選擇鍵的集合:
    已選擇鍵的集合是已註冊集合的子集,主要是選擇器已經監聽準備就緒的鍵的集合
    通過selectedKeys()方法拿到結果(可能為空)

  • 已取消鍵的集合:
    是已註冊鍵的集合的子集,包含的是cancel()方法呼叫多的鍵,不會是呼叫cancel方法立即取消,需要先加入到該已取消鍵的集合


//select() 一直阻塞直至有事件準備就緒才返回 .selectedKeys()不會為空
//select(long timeout) :在指定時間內阻塞,selectedKeys()可能為空
//int selectNow() 不會阻塞,會立即返回 selectedKeys()可能會空

在這裡插入圖片描述
Java的selector選擇過程依賴本地作業系統所提供的IO複用模型
呼叫select()方法執行過程
在這裡插入圖片描述

在這裡插入圖片描述
一個socketchannel的通道,可以立即為對應選擇過程中的鍵,一旦通道註冊,鍵存在已註冊鍵集合,可能存在已選擇鍵的集合和已取消鍵的集合

一個通道上又可以有讀事件、可以有寫事件、可連線事件和可接收事件,對應選擇過程當中interset集合和read集合

(5)select()方法介紹

//select() :一直阻塞直至有事件準備就緒才返回 .selectedKeys()不會為空
//select(long timeout) :在指定時間內阻塞,selectedKeys()可能為空
//int selectNow() :不會阻塞,會立即返回 selectedKeys()可能會空

返回就緒事件的個數

返回值都是int型別表示的是有多少通道已經就緒,自上一次呼叫select()方法後有多少個通道變成就緒狀態,在之前select()呼叫時就進入就緒狀態的通道不會被基礎本次的呼叫中,在前一次select()呼叫進入就緒但現在已經不處於就緒的通道也不會被進入,一旦呼叫select()方法,並且返回值不為0時,則可以通過SelectionKeys()方法來訪問已選擇鍵的集合

(6)停止選擇方法

  • wakeup():wakeup()方法的呼叫會使處於阻塞狀態的select()方法返回,選擇器上第一個還沒有返回的選擇操作立即返回,如果當前還沒有進行中的選擇操作,下一次的select()方法的一次呼叫會立即返回
  • close():通過close關閉seletor操作,使任何一個在選擇操作中的阻塞的執行緒都被喚醒,同時使得註冊到selector上的所有的channel被登出,所有的鍵被取消,但是channel本身不會關閉

四:NIO+多執行緒


思路:
主執行緒主要是進行accept事件的關注,子執行緒完成socketchannel的讀寫事件的關注


服務端

public class MutilThreadServer {
    public static void main(String[] args) {
        //獲取ServerSocketChannel例項
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();


            serverSocketChannel.bind(new InetSocketAddress(9999));
            System.out.println("主執行緒已啟動");

            serverSocketChannel.configureBlocking(false);

            //複用器
            Selector selector = Selector.open();

            serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);


            //迴圈監聽
            while (selector.select() > 0) {
                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    if (!selectionKey.isValid()) continue;

                    if (selectionKey.isAcceptable()) {

                        ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel socketChannel = serverSocketChannel1.accept();
                        System.out.println("Thead:"+Thread.currentThread().getName()+",新使用者連線:"+socketChannel.getRemoteAddress());

                        //將socketChannel交給子執行緒進行讀寫處理
                        new SubThread(socketChannel).start();
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

public class SubThread extends Thread {
    private SocketChannel socketChannel;
    private Selector selector;
    private ByteBuffer buffer;

    public SubThread(SocketChannel socketChannel) {
        this.socketChannel = socketChannel;
        try {
            this.selector = Selector.open();
            this.socketChannel.configureBlocking(false);
            this.socketChannel.register(selector, SelectionKey.OP_READ);
            buffer = ByteBuffer.allocate(1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            //關注讀事件
            while (selector.select() > 0) {
                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();

                    if (!selectionKey.isValid()) continue;
                    if (selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();

                        //讀資料
                        buffer.clear();
                        int read = channel.read(buffer);
                        if (read == -1) {
                            //表示客戶端傳送完資料
                            channel.close();
                            break;
                        }

                        //讀寫切換
                        buffer.flip();

                        byte[] bytes = new byte[buffer.remaining()];

                        buffer.get(bytes);

                        String msg = new String(bytes);
                        System.out.println("Thread:"+Thread.currentThread().getName()+"處理使用者:"+channel.getRemoteAddress()+",資料:"+msg);

                        //給客戶端傳送資料
                        buffer.clear();

                        String recv = "[echo]:"+msg+"\n";
                        buffer.put(recv.getBytes());

                        //讀寫模式切換
                        buffer.flip();

                        channel.write(buffer);

                        if ("exit".equals(msg)) {
                            channel.close();
                            break;
                        }

                    }
                }
            }

        } catch (Exception e) {

        }
    }
}

客戶端

public class Client {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            Selector selector = Selector.open();

            if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999))){
                socketChannel.register(selector,SelectionKey.OP_CONNECT);

                selector.select();
                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();

                    if (selectionKey.isConnectable()) socketChannel.finishConnect();
                }
            }
            Scanner scanner = new Scanner(System.in);
            String msg = null;
            socketChannel.register(selector,SelectionKey.OP_READ);

            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while ((msg =scanner.nextLine()) != null) {
                buffer.clear();

                msg +="\n";
                buffer.put(msg.getBytes());
                buffer.flip();

                socketChannel.write(buffer);
                selector.select();

                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();

                    if (selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();

                        ByteBuffer allocate = ByteBuffer.allocate(1024);
                        channel.read(allocate);

                        allocate.flip();

                        byte[] bytes = new byte[allocate.remaining()];

                        allocate.get(bytes);
                        String recv = new String(bytes);
                        System.out.println(recv);
                    }
                }
                if ("exit".equals(msg)) {
                    break;
                }


            }
            selector.close();
            socketChannel.close();

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

相關文章