JAVA NIO編程介紹

? ? ??傳統(tǒng)的IO操作是同步阻塞IO模式(BIO)乱凿,數(shù)據(jù)的讀取寫入必須阻塞在一個線程內(nèi)等待其完成转培。NIO則是同步非阻塞IO模式。BIO面向流操作添祸,NIO面向緩沖區(qū)操作申尤。

? ? ??NIO主要有三大核心部分:Channel(通道)癌幕,Buffer(緩沖區(qū)), Selector。傳統(tǒng)IO基于字節(jié)流和字符流進行操作昧穿,而NIO基于Channel和Buffer(緩沖區(qū))進行操作勺远,數(shù)據(jù)總是從通道讀取到緩沖區(qū)中,或者從緩沖區(qū)寫入到通道中时鸵。Selector(選擇區(qū))用于監(jiān)聽多個通道的事件(比如:連接打開胶逢,數(shù)據(jù)到達)。因此饰潜,單個線程可以監(jiān)聽多個數(shù)據(jù)通道初坠。

一 Channel(通道)

? ? ? ?Chanel通道相當于IO操作的載體,數(shù)據(jù)通過Channel讀取和寫入彭雾,全雙工模式(雙向)碟刺。Channel類似流,但是又和流不同薯酝,流的讀寫是單向的比如InputStream半沽、OutputStream爽柒。但是Chanel既可以從通道里面讀取數(shù)據(jù)又可以把數(shù)據(jù)寫到通道里面去。通道中的數(shù)據(jù)總是要先讀到一個Buffer者填,或者總是要從一個Buffer中寫入浩村。

1.1 FileChannel

? ? ? ?FileChannel是一個基于文件的通道≌加矗可以通過文件通道讀寫文件心墅。有一點要注意FileChannel無法設置為非阻塞模式。它總是以阻止模式運行榨乎。

FileChannel提供的函數(shù)

方法 解釋
open 打開一個文件怎燥,把文件和通道關聯(lián)起來
read 從當前通道讀取字節(jié)序列到給定的緩沖區(qū)
write 從緩沖區(qū)向該通道寫入字節(jié)序列
position 跳轉到文件的指定位置
size 獲取文件大小
truncate 截取文件
force 將通道里尚未寫入磁盤的數(shù)據(jù)強制寫到磁盤上
transferTo 將字節(jié)從當前通道傳輸?shù)浇o定的可寫字節(jié)通道
transferFrom 將給定的可讀字節(jié)通道上的字節(jié)傳輸?shù)疆斍巴ǖ乐?/td>
map 將當前通道某個區(qū)域直接映射到內(nèi)存中
lock 獲取此通道文件的獨占鎖定
tryLock 嘗試獲取此通道文件的給定區(qū)域的鎖定

? ? ? ?下面我們通過一個簡單的實例來看FileChannel怎么使用。

    @Test
    public void fileChannelRead() {

        try {
            // 開啟FileChannel
            RandomAccessFile aFile = new RandomAccessFile("D:\\job\\git\\java-study\\nio\\src\\main\\resources\\fileChanel.txt", "rw");
            FileChannel inChannel = aFile.getChannel();
            ByteBuffer buf = ByteBuffer.allocate(48);
            // 從FileChannel通道讀取數(shù)據(jù)到緩沖區(qū)ByteBuffer
            int bytesRead = inChannel.read(buf);
            while (bytesRead != -1) {
                System.out.println("讀取到的數(shù)據(jù)長度 " + bytesRead);
                buf.flip();
                while (buf.hasRemaining()) {
                    System.out.print((char) buf.get());
                }
                buf.clear();
                // 繼續(xù)讀取文件信息
                bytesRead = inChannel.read(buf);
            }
            aFile.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    @Test
    public void fileChannelWrite() {

        try {
            // 開啟FileChannel
            RandomAccessFile aFile = new RandomAccessFile("D:\\job\\git\\java-study\\nio\\src\\main\\resources\\fileChanelWrite.txt", "rw");
            FileChannel inChannel = aFile.getChannel();
            ByteBuffer buf = ByteBuffer.allocate(48);
            byte[] forWrite = "需要寫入的字符串谬哀。".getBytes(StandardCharsets.UTF_8);
            buf.put(forWrite, 0, forWrite.length);
            buf.flip();
            // 寫入數(shù)據(jù)
            while (buf.hasRemaining()) {
                inChannel.write(buf);
            }
            aFile.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

1.2 DatagramChannel

? ? ? ?DatagramChannel主要是用來基于UDP通信的通道刺覆。

DatagramChannel方法介紹

DatagramChannel方法 返回值 解釋
open() DatagramChannel 創(chuàng)建通道
bind(SocketAddress local) DatagramChannel 綁定端口
validOps() int 只支持OP_READ/OP_WRITE兩種操作
socket() DatagramSocket 獲取與其關聯(lián)的底層DatagramSocket
isConnected() boolean 檢測是否已經(jīng)建立了Socket鏈接
connect(SocketAddress remote) DatagramChannel 鏈接remote端
disconnect() DatagramChannel 斷開通道連接
getRemoteAddress() SocketAddress 獲取遠程地址
receive(ByteBuffer dst) SocketAddress 接收數(shù)據(jù)
send() int 發(fā)送數(shù)據(jù)严肪,向指定的地址發(fā)送數(shù)據(jù)
read() int 必須在connect()之后調(diào)用史煎,接收數(shù)據(jù)
write() int 必須在connect()之后調(diào)用,發(fā)送數(shù)據(jù)
getLocalAddress() SocketAddress 獲取本地地址

注意驳糯,connect()篇梭、send()、read() 三個函數(shù)是配套使用的酝枢。

? ? ? ?接下來我們通過三個例子來說明DatagramChannel的用法恬偷,下面的例子都是阻塞模式的,等講到Selector的時候我們在講非阻塞的用法帘睦。

1.2.1 UDP服務端

? ? ? ?UDP服務端需要調(diào)用bind()函數(shù)綁定本地端口袍患。

    /**
     * UDP 服務端
     */
    @Test
    public void datagramChannelService() {
        try {
            // 獲取通道
            DatagramChannel datagramChannel = DatagramChannel.open();
            // 綁定端口8989,作為UDP服務端
            datagramChannel.bind(new InetSocketAddress(8989));
            // 分配Buffer,用于收發(fā)數(shù)據(jù)
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (true) {
                buffer.clear();
                // 等待接受客戶端發(fā)送數(shù)據(jù)
                SocketAddress socketAddress = datagramChannel.receive(buffer);
                if (socketAddress != null) {
                    buffer.flip();
                    byte[] b = new byte[buffer.limit()];
                    int bufferReceiveIndex = 0;
                    while (buffer.hasRemaining()) {
                        b[bufferReceiveIndex++] = buffer.get();
                    }
                    System.out.println("收到客戶端消息 " + socketAddress.toString() + ":" + new String(b, StandardCharsets.UTF_8));
                    // 接收到消息后給發(fā)送方回應
                    sendDataBack(socketAddress, datagramChannel);
                }
            }

        } catch (IOException e) {
            // ignore
        }
    }

    /**
     * 給socketAddress地址發(fā)送消息
     */
    private void sendDataBack(SocketAddress socketAddress, DatagramChannel datagramChannel) throws IOException {
        String message = "send back";
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(message.getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        datagramChannel.send(buffer, socketAddress);
    }

1.2.2 UDP客戶端

? ? ? ?如果UDP作為客戶端的話,可以直接往UDP服務端發(fā)送消息竣付,服務端接收到消息的時候同時獲取到了對應客戶端的地址信息诡延。又可以把消息發(fā)送回來。

    // UDP客戶端
    @Test
    public void datagramChannelClient() {
        try {
            final DatagramChannel channel = DatagramChannel.open();
            // 開一個線程一直接收UDP服務端發(fā)送過來的消息
            new Thread(() -> {
                try {
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    while (true) {
                        buffer.clear();
                        SocketAddress socketAddress = channel.receive(buffer);
                        if (socketAddress != null) {
                            buffer.flip();
                            byte[] b = new byte[buffer.limit()];
                            int bufferReceiveIndex = 0;
                            while (buffer.hasRemaining()) {
                                b[bufferReceiveIndex++] = buffer.get();
                            }
                            System.out.println("收到消息 " + socketAddress.toString() + ":" + new String(b, StandardCharsets.UTF_8));
                        }
                    }
                } catch (Exception e) {
                    // ignore
                }

            }).start();

            int messageIndex = 0;
            // 控制臺輸入數(shù)據(jù)古胆,然后發(fā)送給指定的地址
            while (true) {
                // 5S發(fā)送一次數(shù)據(jù)
                Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
                sendMessage(channel, new InetSocketAddress("192.168.5.14", 8989), String.valueOf(messageIndex++));
            }

        } catch (IOException e) {
            // ignore
        }
    }


    private void sendMessage(DatagramChannel channel, InetSocketAddress address, String mes) throws IOException {
        if (mes == null || mes.isEmpty()) {
            return;
        }
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.clear();
        buffer.put(mes.getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        channel.send(buffer, address);
    }

1.2.3 connect用法

? ? ? ?DatagramChannel的connect()方法可以和指定的地址綁定起來肆良,配合write()、read()函數(shù)在兩者之間收發(fā)消息逸绎。比如下面的實例我們和time-a.nist.gov建立連接獲取時間惹恃。

    /**
     * UDP connect() 在特定的地址上收發(fā)消息
     */
    @Test
    public void datagramChannelConnect() {
        try {
            // 獲取通道
            DatagramChannel datagramChannel = DatagramChannel.open();
            // 連接到特定的地址,time-a.nist.gov 獲取時間棺牧。只在這個地址間收發(fā)消息 write,read 方法
            datagramChannel.connect(new InetSocketAddress("time-a.nist.gov", 37));
            ByteBuffer buffer = ByteBuffer.allocate(8);
            buffer.order(ByteOrder.BIG_ENDIAN);
            buffer.put((byte) 0);
            buffer.flip();
            // 發(fā)送數(shù)據(jù)到 time-a.nist.gov
            datagramChannel.write(buffer);
            buffer.clear();
            // 前四個字節(jié)補0
            buffer.putInt(0);
            // 從 time-a.nist.gov 讀取數(shù)據(jù)
            datagramChannel.read(buffer);
            buffer.flip();
            // convert seconds since 1900 to a java.util.Date
            long secondsSince1900 = buffer.getLong();
            long differenceBetweenEpochs = 2208988800L;
            long secondsSince1970 = secondsSince1900 - differenceBetweenEpochs;
            long msSince1970 = secondsSince1970 * 1000;
            Date time = new Date(msSince1970);
            // 打印時間
            System.out.println(time);
        } catch (Exception e) {
            // ignore
        }
    }

再次強調(diào)下巫糙,上面實例代碼我們都是用的阻塞模式實現(xiàn)的。等下面講到Selector的時候我們在講怎么用非阻塞的方式實現(xiàn)颊乘。

1.3 SocketChannel

? ? ? ?SocketChannel主要是用來基于TCP通信的通道曲秉,它一般用來作為客戶端的套接字采蚀,它有點類似于java中的Socket類。

SocketChannel方法介紹

SocketChannel方法 返回值 解釋
open() SocketChannel 創(chuàng)建SocketChannel通道
validOps() int 通道支持的操作承二,OP_READ榆鼠、OP_WRITE、OP_CONNECT
bind(SocketAddress local) SocketChannel 地址綁定
setOption(SocketOption<T> name, T value) SocketChannel Socket的選項配置亥鸠,StandardSocketOptions.SO_KEEPALIVE等
shutdownInput() SocketChannel 在沒有關閉通道的情況下妆够,關閉讀操作連接
shutdownOutput() SocketChannel 在沒有關閉通道的情況下,關閉到通道的寫操作連接
socket() Socket 獲取與通道關聯(lián)的socket
isConnected() boolean 判斷通道的網(wǎng)絡socket是否連接
isConnectionPending() boolean 判斷通道是否正在進行操作連接.只有當尚未finishConnection且已經(jīng)調(diào)用connect時,返回true
connect(SocketAddress remote) boolean 連接通道的socket
finishConnect() boolean 完成到socket通道的連接任務负蚊,一般在非阻塞的情況下用到神妹,
getRemoteAddress() SocketAddress 返回通道socket連接的遠端地址
read() int or long 接收數(shù)據(jù)
write int or long 發(fā)送數(shù)據(jù)
getLocalAddress() SocketAddress 返回通道socket連接的本地地址

? ? ? ?setOption():用于給socket設置一些選項配置,比如keep alive家妆。socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);等等鸵荠。具體可以看看StandardSocketOptions里面的一些標準選項。

? ? ? ?關于connect()伤极、isConnectionPending()蛹找、finishConnect()三個函數(shù)的關系我們稍微屢一下。分兩種情況來考慮:

  • 阻塞模式:connect()是阻塞的哨坪,這個時候isConnectionPending()庸疾、finishConnect()兩個函數(shù)我覺得意義不大,因為你本來就是阻塞狀態(tài)的当编,connect()函數(shù)成功了届慈,這兩個函數(shù)值也就確定了:isConnectionPending()->false、finishConnect()->true(如果連接失敗他就是false)忿偷。
  • 非阻塞模式:調(diào)用connect()方法底層socket建立連接的時候金顿。因為是非阻塞的如果連接立即建立成功,則返回true鲤桥,否則返回false揍拆。則此后一旦成功建立連接就必須通過調(diào)用finishConnect()方法來完成鏈接。在沒有調(diào)用finishConnect()之前isConnectionPending()->true芜壁,調(diào)用之后isConnectionPending()->false礁凡。

1.3.1 SocketChannel使用

    /**
     * TCP客戶端,阻塞模式
     */
    @Test
    public void socketChannelClient() {
        try {
            SocketChannel channel = SocketChannel.open();
            // 這里使用的是阻塞模式
            channel.connect(new InetSocketAddress("192.168.5.14", 6800));
            // KEEP ALIVE setOption()函數(shù)的使用,一定要在連接成功之后設置
            channel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (true) {
                buffer.clear();
                int readLength = channel.read(buffer);
                if (readLength >= 0) {
                    buffer.flip();
                    byte[] b = new byte[buffer.limit()];
                    int bufferReceiveIndex = 0;
                    while (buffer.hasRemaining()) {
                        b[bufferReceiveIndex++] = buffer.get();
                    }
                    System.out.println("收到消息 " + ":" + new String(b, StandardCharsets.UTF_8));
                    // 把收到的消息又發(fā)送回去
                    buffer.clear();
                    buffer.put(b);
                    buffer.flip();
                    channel.write(buffer);
                }

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

1.4 ServerSocketChannel

? ? ? ?ServerSocketChannel也是用來基于TCP通信的通道,它一般用來作為服務端的套接字慧妄,它有點類似于java中的ServerSocket類顷牌。一般用來接收客戶端的連接,在客戶端連接的的基礎之上做一些收發(fā)消息的處理塞淹。

ServerSocketChannel主要方法

ServerSocketChannel方法 返回值 解釋
open() ServerSocketChannel 建立通道
validOps() int 當前通道支持的操作窟蓝,OP_ACCEPT
bind(SocketAddress local) ServerSocketChannel 綁定到指定的端口,還可以指定最多多少個連接
setOption(SocketOption<T> name, T value) ServerSocketChannel Socket的選項配置饱普,StandardSocketOptions.SO_KEEPALIVE等
socket() ServerSocket 獲取與通道關聯(lián)的socket
accept() SocketChannel 接收客戶端的連接
getLocalAddress() SocketAddress 返回通道socket連接的本地地址

? ? ? ?ServerSocketChannel是用于接收客戶端連接的运挫,在接收到(accept函數(shù))客戶端連接之后會拿到基于客戶端連接的SocketChannel状共。和每個客戶端的操作都是通過SocketChannel實現(xiàn)的。

1.4.1 ServerSocketChannel的使用

    /**
     * TCP服務端 -- 阻塞模式
     */
    @Test
    public void socketChannelServer() {
        try {
            ServerSocketChannel  channel = ServerSocketChannel.open();
            channel.bind(new InetSocketAddress("192.168.5.14", 6800));
            while (true) {
                // 接收客戶端的連接,之后拿到的就是SocketChannel了谁帕,之后都是基于SocketChannel做相應的操作
                SocketChannel clientSocketChannel = channel.accept();
                clientSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                buffer.clear();
                buffer.put("hello".getBytes(StandardCharsets.UTF_8));
                buffer.flip();
                // 給客戶端發(fā)送消息
                clientSocketChannel.write(buffer);
                // 在收下客戶端的消息
                buffer.clear();
               int readLength = clientSocketChannel.read(buffer);
                if (readLength >= 0) {
                    buffer.flip();
                    byte[] b = new byte[buffer.limit()];
                    int bufferReceiveIndex = 0;
                    while (buffer.hasRemaining()) {
                        b[bufferReceiveIndex++] = buffer.get();
                    }
                    System.out.println("收到消息 " + ":" + new String(b, StandardCharsets.UTF_8));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

二 Buffer(緩沖區(qū))

? ? ? ?Buffer用于和Channel通道進行交互峡继。如你所知,數(shù)據(jù)是從通道讀入緩沖區(qū)匈挖,從緩沖區(qū)寫入到通道中的碾牌。緩沖區(qū)本質(zhì)上是一塊可以寫入數(shù)據(jù),然后可以從中讀取數(shù)據(jù)的內(nèi)存儡循。這塊內(nèi)存被包裝成了NIO Buffer對象舶吗。

? ? ? ?我們先介紹先Buffer里面的三個屬性,接著在介紹下Buffer里面主要的方法择膝。

Buffer里面三個屬性:capacity誓琼、position、limit肴捉。

  • capacity:作為一個內(nèi)存塊腹侣,Buffer有一個固定的大小值,也叫“capacity”.你只能往里寫capacity個byte每庆、long筐带,char等類型今穿。一旦Buffer滿了缤灵,需要將其清空(通過讀數(shù)據(jù)或者清除數(shù)據(jù))才能繼續(xù)往里寫數(shù)據(jù)。
  • position:當你寫數(shù)據(jù)到Buffer中時蓝晒,position表示當前的位置腮出。初始的position值為0。當一個byte芝薇、long等數(shù)據(jù)寫到Buffer后胚嘲,position會向前移動到下一個可插入數(shù)據(jù)的Buffer單元。position最大可為capacity–1洛二;當讀取數(shù)據(jù)時馋劈,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式晾嘶,position會被重置為0妓雾。當從Buffer的position處讀取數(shù)據(jù)時,position向前移動到下一個可讀的位置垒迂。position簡單來說就相當于游標的作用械姻。
  • limit:在寫模式下,Buffer的limit表示你最多能往Buffer里寫多少數(shù)據(jù)机断。寫模式下楷拳,limit等于Buffer的capacity绣夺;當切換Buffer到讀模式時,limit表示你最多能讀到多少數(shù)據(jù)欢揖。因此陶耍,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值她混。換句話說物臂,你能讀到之前寫入的所有數(shù)據(jù)(limit被設置成已寫數(shù)據(jù)的數(shù)量,這個值在寫模式下就是position)产上。

一定要注意position棵磷、limit在讀模式和謝模式下代表的含義,以及兩個模式之間切換的時候position晋涣、limit做了那些變化仪媒。

Buffer.png

Buffer里面常用函數(shù)。

public abstract class Buffer {
    
    /**
     * 獲取當前緩沖區(qū)的容量 -- capacity
     */
    public final int capacity();

    /**
     * 獲取當前緩沖區(qū)的位置 -- position
     */
    public final int position();

    /**
     * 設置當前緩沖區(qū)的位置 -- position
     */
    public final Buffer position(int newPosition);

    /**
     * 獲取當前緩沖區(qū)的限制 -- limit
     */
    public final int limit();

    /**
     * 設置當前緩沖區(qū)的限制 -- limit
     */
    public final Buffer limit(int newLimit);

    /**
     * mark(), reset()函數(shù)是配對使用的谢鹊,將當前緩沖區(qū)的標記(mark)設置在當前位置(position) -- mark
     */
    public final Buffer mark();

    /**
     * 通過調(diào)用mark()方法算吩,可以標記Buffer中的一個特定position。
     * 之后可以通過調(diào)用Buffer.reset()方法恢復到這個position
     */
    public final Buffer reset();

    /**
     *  清除此緩存區(qū)佃扼。將position = 0;limit = capacity;mark = -1;一般在寫入數(shù)據(jù)之前調(diào)用
     */
    public final Buffer clear();

    /**
     * flip()方法可以把Buffer從寫模式切換到讀模式偎巢。調(diào)用flip方法會把position歸零,
     * 并設置limit為之前的position的值兼耀。
     * 也就是說压昼,現(xiàn)在position代表的是讀取位置,limit標示的是已寫入的數(shù)據(jù)位置瘤运。
     */
    public final Buffer flip();

    /**
     * 將position設回0窍霞,這個時候你可以重讀Buffer中的所有數(shù)據(jù)。
     * limit保持不變拯坟,仍然表示能從Buffer中讀取多少個元素
     */
    public final Buffer rewind();

    /**
     * return limit - position; 返回limit和position之間相對位置差
     */
    public final int remaining();

    /**
     * return position < limit但金,返回是否還有未讀內(nèi)容
     */
    public final boolean hasRemaining();

    /**
     * 判斷此緩沖區(qū)是否為只讀
     */
    public abstract boolean isReadOnly();

    /**
     * 判斷此緩沖區(qū)是否由可訪問的數(shù)組支持
     */
    public abstract boolean hasArray();

    /**
     * 返回支持此緩沖區(qū)的數(shù)組
     */
    public abstract Object array();

    /**
     * 返回該緩沖區(qū)的緩沖區(qū)的第一個元素的背襯數(shù)組中的偏移量
     */
    public abstract int arrayOffset();

    /**
     * 判斷個緩沖區(qū)是否為 direct
     */
    public abstract boolean isDirect();

}

flip()、hasRemaining()郁季、clear()冷溃、rewind()、mark()梦裂、reset()幾個函數(shù)要著重理解下似枕。

? ? ? ?要想使用Buffer來讀寫數(shù)據(jù)一般遵循以下四個步驟:

  • 寫數(shù)據(jù)到Buffer里面∪恚可以從Channel讀取出來寫入到緩沖區(qū)中菠净,也可以調(diào)用put方法寫入到緩沖區(qū)中。
  • 調(diào)用flip()方法,切換到讀數(shù)據(jù)模式毅往。這個時候position指向第一個位置牵咙,limit指向?qū)懭霐?shù)據(jù)的最后位置。
  • 從Buffer中讀取數(shù)據(jù)攀唯。一般從緩沖區(qū)讀取數(shù)據(jù)寫入到通道中洁桌,也可以調(diào)用get方法讀取到Buffer里面的數(shù)據(jù)。
  • 調(diào)用clear()或者compact()方法清空緩沖區(qū)侯嘀。

? ? ? ?當向buffer寫入數(shù)據(jù)時另凌,buffer會記錄下寫了多少數(shù)據(jù)。一旦要讀取數(shù)據(jù)戒幔,需要通過flip()方法將Buffer從寫模式切換到讀模式吠谢。

? ? ? ?在讀模式下,可以讀取之前寫入到buffer的所有數(shù)據(jù)诗茎。一旦讀完了所有的數(shù)據(jù)工坊,就需要清空緩沖區(qū),讓它可以再次被寫入敢订。

? ? ? ?有兩種方式能清空緩沖區(qū):

  • clear():方法會清空整個緩沖區(qū)王污。
  • compact():這個方法在ByteBuffer、CharBuffer楚午、ShortBuffer等方法里面提供昭齐。 該方法只會清除已經(jīng)讀過的數(shù)據(jù)。任何未讀的數(shù)據(jù)都被移到緩沖區(qū)的起始處矾柜,新寫入的數(shù)據(jù)將放到緩沖區(qū)阱驾。

? ? ? ?我們使用Buffer緩沖區(qū)。一般使用的都是他的子類:ByteBuffer把沼、CharBuffer啊易、ShortBuffer吁伺、LongBuffer饮睬、FloatBuffer、DoubleBuffer這些篮奄。Buffer的這些子類都是基于JAVA一些基本數(shù)據(jù)類型實現(xiàn)的一個Buffer緩沖區(qū)捆愁。我們先看下這些子類一般都會有的一些方法(Buffer里面的方法他們都會有,Buffer里面的方法我們就不重復介紹了)窟却。

出Buffer提供的放方法之外昼丑,子類里面額外的方法。

Buffer子類方法 描述
allocate(int capacity) Buffer實例化方法 從堆空間中分配一個容量大小為capacity的對應類型的數(shù)組作為緩沖區(qū)的數(shù)據(jù)存儲器
allocateDirect(int capacity) Buffer實例化方法 不使用JVM堆棧而是通過操作系統(tǒng)來創(chuàng)建內(nèi)存塊用作緩沖區(qū)夸赫,它與當前操作系統(tǒng)能夠更好的耦合菩帝,因此能進一步提高I/O操作速度。但是分配直接緩沖區(qū)的系統(tǒng)開銷很大,因此只有在緩沖區(qū)較大并長期存在呼奢,或者需要經(jīng)常重用時宜雀,才使用這種緩沖區(qū)
wrap(T[] array) Buffer實例化方法 這個緩沖區(qū)的數(shù)據(jù)會存放在對應數(shù)組中,對應數(shù)組或buff緩沖區(qū)任何一方中數(shù)據(jù)的改動都會影響另一方握础。其實Buffer底層本來就有一個對應數(shù)組負責來保存buffer緩沖區(qū)中的數(shù)據(jù)辐董,通過allocate方法系統(tǒng)會幫你構造一個對應類型組
wrap(T[] array, int offset,intlength) Buffer實例化方法 在上一個方法的基礎上可以指定偏移量和長度,這個offset也就是包裝后byteBuffer的position禀综,而length呢就是limit-position的大小简烘,從而我們可以得到limit的位置為length+position(offset)
slice() 常規(guī)方法 創(chuàng)建新的緩沖區(qū),其內(nèi)容是此緩沖區(qū)內(nèi)容的共享子序列
duplicate() 常規(guī)方法 創(chuàng)建共享此緩沖區(qū)內(nèi)容的新的字節(jié)緩沖區(qū)
asReadOnlyBuffer() 常規(guī)方法 創(chuàng)建共享此緩沖區(qū)內(nèi)容的新的只讀字節(jié)緩沖區(qū)
get() 常規(guī)方法 從緩沖區(qū)獲取數(shù)據(jù)
put() 常規(guī)方法 把數(shù)據(jù)放入到緩沖區(qū)中
compact() 常規(guī)方法 清除已經(jīng)讀過的數(shù)據(jù)定枷。任何未讀的數(shù)據(jù)都被移到緩沖區(qū)的起始處

? ? ? ?關于Buffer的使用孤澎,我們以ByteBuffer和CharBuffer來舉例說明。其他的Buffer子類適應也都是很簡單的欠窒。

ByteBuffer的使用

    @Test
    public void byteBufferTest() {
        // 創(chuàng)建一個ByteBuffer實例
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 清空
        buffer.clear();
        // 寫入數(shù)據(jù)
        byte[] putByteArray = "hello word!".getBytes(StandardCharsets.UTF_8);
        buffer.put(putByteArray);
        // 切換到讀模式
        buffer.flip();
        // 把數(shù)據(jù)讀取出來
        buffer.slice();
        while (buffer.hasRemaining()) {
            System.out.print((char) buffer.get());
        }
        System.out.println();
        // 重新讀
        buffer.rewind();
        // ps: 這個時候buffer.limit()就是數(shù)組元素的個數(shù)
        byte[] retByte = new byte[buffer.limit()];
        buffer.get(retByte);
        System.out.println(new String(retByte, StandardCharsets.UTF_8));
    }

CharBuffer使用距離

    @Test
    public void charBufferTest() {
        // 創(chuàng)建一個ByteBuffer實例
        CharBuffer buffer = CharBuffer.allocate(1024);
        // 清空
        buffer.clear();
        // 寫入數(shù)據(jù)
        char[] putArray = "hello word!".toCharArray();
        buffer.put(putArray);
        // 切換到讀模式
        buffer.flip();
        // 把數(shù)據(jù)讀取出來
        buffer.slice();
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get());
        }
        System.out.println();
        // 重新讀
        buffer.rewind();
        // ps: 這個時候buffer.limit()就是數(shù)組元素的個數(shù)
        char[] retByte = new char[buffer.limit()];
        buffer.get(retByte);
        System.out.println(String.valueOf(retByte));
    }

三 Selector(多路復用器)

? ? ? ?Selector提供了選擇已就緒任務的能力亥至,Selector會不斷輪詢注冊在上面的Channel,某個Channel發(fā)生讀或?qū)懯录伲瑒t該Channel就處于就緒狀態(tài)姐扮,會被Selector輪詢出來衣吠。然后通過SelectionKey獲取就緒Channel的集合茶敏,進行后續(xù)IO操作。Selector允許單線程處理多個Channel缚俏。如果你的應用打開了多個連接(通道)惊搏,但每個連接的流量都很低,使用Selector就會很方便忧换。

Selector.png

Selector方法介紹

Selector方法 返回值 解釋
open() Selector Selector的創(chuàng)建
isOpen() boolean 判斷此選擇器是否已打開
provider() SelectorProvider 返回創(chuàng)建此通道的提供程序
keys() Set<SelectionKey> 返回所有的SelectionKey
selectedKeys() Set<SelectionKey> 返回已選擇的SelectionKey集合恬惯,要在select()之后調(diào)用
selectNow() int 非阻塞,返回有多少通道就緒
select(long timeout) int 阻塞到至少有一個通道在你注冊的事件上就緒了
select() int 阻塞到至少有一個通道在你注冊的事件上就緒了,返回值表示有多少通道就緒
wakeup() Selector Selector的喚醒

? ? ? ?wakeup() 函數(shù)稍微講下亚茬。Selector的選擇方式有三種:select()酪耳、select(timeout)、selectNow()刹缝。selectNow的選擇過程是非阻塞的碗暗,與wakeup沒有太大關系。select(timeout)和select()的選擇過程是阻塞的梢夯,其他線程如果想終止這個過程言疗,就可以調(diào)用wakeup來喚醒select()。

3.1 Selector創(chuàng)建

? ? ? ?通過調(diào)用Selector.open()方法創(chuàng)建一個Selector對象颂砸。

Selector selector = Selector.open();

3.2 把Channel注冊到Selector

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

? ? ? ?注冊到Selector的Channel是有前提添加的噪奄。Channel必須是非阻塞的死姚,必須是SelectableChannel的子類。所以FileChannel不適用Selector勤篮,因為FileChannel不能切換為非阻塞模式知允,更準確的來說是因為FileChannel沒有繼承SelectableChannel。

? ? ? ?把Channel注冊到Selector的時候還得指定監(jiān)聽事件叙谨。就是告訴Selector我這個Channel對什么事件感興趣温鸽。當Channel上有這個事件發(fā)送的時候這個Channel就會被輪詢出來。NIO提供了四個事件:

Channel注冊事件 解釋
Selectionkey.OP_READ 讀就緒
Selectionkey.OP_WRITE 寫就緒
Selectionkey.OP_CONNECT 連接就緒
Selectionkey.OP_ACCEPT 接收就緒

? ? ? ?我們有兩種方式來設置Selector對Channel的哪些事件感興趣手负。一個是在把Channel注冊到Selector的時候設置涤垫。我們上面已經(jīng)講了這種情況。另一個是調(diào)用SelectionKey的interestOps()函數(shù)來修改Selector對Channel感興趣的事件竟终。

3.2.1 SelectionKey

? ? ? ?每個Channel向Selector注冊時,都將會創(chuàng)建一個SelectionKey對象蝠猬。一個SelectionKey鍵表示了一個特定的通道對象和一個特定的選擇器對象之間的注冊關系。并維護了Channel事件统捶。

SelectionKey方法介紹

方法 返回值 解釋
channel() SelectableChannel 返回此選擇鍵所關聯(lián)的通道
selector() Selector 返回此選擇鍵所關聯(lián)的選擇器
isValid() boolean 檢測此key是否有效
cancel() void 請求將此鍵取消注冊.一旦返回成功,那么該鍵就是無效的
interestOps() int 判斷Selector對Channel的哪些事件感興趣榆芦,OP_READ、OP_WRITE等事件
interestOps(int ops) SelectionKey 設置Selector對Channel的哪些事件感興趣
readyOps() int 獲取此鍵上ready操作集合.即在當前通道上已經(jīng)就緒的事件
isReadable() boolean 檢測此鍵是否為"read"事件.等效于:k.readyOps() & OP_READ != 0
isWritable() boolean 檢測此鍵是否為"write"事件
isConnectable() boolean 檢測此鍵是否為"connect"事件
isAcceptable() boolean 檢測此鍵是否為"accept"事件
attach(Object ob) Object 將給定的對象作為附件添加到此key上.在key有效期間,附件可以在多個ops事件中傳遞
attachment() Object 獲取附件.一個channel的附件,可以再當前Channel(或者說是SelectionKey)生命周期中共享,但是attachment數(shù)據(jù)不會作為socket數(shù)據(jù)在網(wǎng)絡中傳輸

3.3 從Selector中選擇就緒的Channel

? ? ? ?從Selector中選擇就緒的Channel喘鸟,其實是去選擇SelectionKey匆绣,然后通過SelectionKey拿到對應的Channel。通過Channel做相應的操作什黑。

? ? ? ?從Selector中選擇就緒的Channel也很簡單崎淳。先調(diào)用Selecotor的select()方法選擇出已經(jīng)就緒的通道,Selector會幫助我們把這些就緒的通道放到一個就緒列表里目前愕把。然后我們在調(diào)用Selector的selectedKeys()方法把這些通道都拿出來拣凹。

3.4 Selecotr完整實例

    @Test
    public void tcpClient() {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            // 連接
            socketChannel.connect(new InetSocketAddress("192.168.5.14", 6800));
            ByteBuffer writeBuffer = ByteBuffer.allocate(32);
            ByteBuffer readBuffer = ByteBuffer.allocate(32);
            writeBuffer.put("hello".getBytes());
            writeBuffer.flip();
            while (true) {
                writeBuffer.rewind();
                socketChannel.write(writeBuffer);
                readBuffer.clear();
                socketChannel.read(readBuffer);
                readBuffer.flip();
                byte[] b = new byte[readBuffer.limit()];
                int bufferReceiveIndex = 0;
                while (readBuffer.hasRemaining()) {
                    b[bufferReceiveIndex++] = readBuffer.get();
                }
                System.out.println("received : " + new String(b));
            }
        } catch (Exception e) {
            // ignore
        }
    }


    @Test
    public void tcpServer() {

        try {
            // 創(chuàng)建一個ServerSocketChannel通道
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            // 綁定6800端口
            serverChannel.bind(new InetSocketAddress("192.168.5.14", 6800));
            // 設置非阻塞
            serverChannel.configureBlocking(false);
            // Selector創(chuàng)建
            Selector selector = Selector.open();
            // 注冊 channel,并且指定感興趣的事件是 Accept
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            ByteBuffer readBuff = ByteBuffer.allocate(1024);
            ByteBuffer writeBuff = ByteBuffer.allocate(1024);
            writeBuff.put("received".getBytes());
            writeBuff.flip();
            while (true) {
                if (selector.select() > 0) {
                    Set<SelectionKey> readyKeys = selector.selectedKeys();
                    Iterator<SelectionKey> readyKeyIterator = readyKeys.iterator();
                    while (readyKeyIterator.hasNext()) {
                        SelectionKey key = readyKeyIterator.next();
                        readyKeyIterator.remove();

                        if (key.isAcceptable()) {
                            // 連接
                            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            socketChannel.configureBlocking(false);
                            // 我們又給注冊到Selector里面去了恨豁,聲明這個channel只對讀操作感興趣嚣镜。
                            socketChannel.register(selector, SelectionKey.OP_READ);
                        } else if (key.isReadable()) {
                            // 讀
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            readBuff.clear();
                            socketChannel.read(readBuff);
                            readBuff.flip();
                            byte[] b = new byte[readBuff.limit()];
                            int bufferReceiveIndex = 0;
                            while (readBuff.hasRemaining()) {
                                b[bufferReceiveIndex++] = readBuff.get();
                            }
                            System.out.println("received : " + new String(b));
                            // 修改selector對channel感興趣的事件
                            key.interestOps(SelectionKey.OP_WRITE);
                        } else if (key.isWritable()) {
                            // 寫
                            writeBuff.rewind();
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            socketChannel.write(writeBuff);
                            // 修改selector對channel感興趣的事件
                            key.interestOps(SelectionKey.OP_READ);
                        }
                    }
                }
            }

        } catch (IOException e) {
            // ignore
        }

    }

? ? ? ?以上,就是我們對JAVA NIO編程的一個簡單介紹橘蜜。最后我們用一個圖來做一個總結菊匿。

JAVA NIO.png
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市扮匠,隨后出現(xiàn)的幾起案子捧请,更是在濱河造成了極大的恐慌,老刑警劉巖棒搜,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異活箕,居然都是意外死亡力麸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來克蚂,“玉大人闺鲸,你說我怎么就攤上這事“0龋” “怎么了摸恍?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長赤屋。 經(jīng)常有香客問我立镶,道長,這世上最難降的妖魔是什么类早? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任媚媒,我火速辦了婚禮,結果婚禮上涩僻,老公的妹妹穿的比我還像新娘缭召。我一直安慰自己,他們只是感情好逆日,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布嵌巷。 她就那樣靜靜地躺著,像睡著了一般室抽。 火紅的嫁衣襯著肌膚如雪晴竞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天狠半,我揣著相機與錄音噩死,去河邊找鬼。 笑死神年,一個胖子當著我的面吹牛已维,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播已日,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼垛耳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了飘千?” 一聲冷哼從身側響起堂鲜,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎护奈,沒想到半個月后缔莲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡霉旗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年痴奏,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛀骇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡读拆,死狀恐怖擅憔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情檐晕,我是刑警寧澤暑诸,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站辟灰,受9級特大地震影響个榕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伞矩,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一笛洛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乃坤,春花似錦苛让、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至厅须,卻和暖如春仿畸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背朗和。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工错沽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人眶拉。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓千埃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親忆植。 傳聞我的和親對象是個殘疾皇子放可,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Java NIO(New IO)是從Java 1.4版本開始引入的一個新的IO API,可以替代標準的Java I...
    JackChen1024閱讀 7,549評論 1 143
  • # Java NIO # Java NIO屬于非阻塞IO朝刊,這是與傳統(tǒng)IO最本質(zhì)的區(qū)別耀里。傳統(tǒng)IO包括socket和文...
    Teddy_b閱讀 585評論 0 0
  • (轉載說明:本文非原創(chuàng),轉載自http://ifeve.com/java-nio-all/) Java NIO: ...
    數(shù)獨題閱讀 801評論 0 3
  • 轉自 http://www.ibm.com/developerworks/cn/education/java/j-...
    抓兔子的貓閱讀 2,290評論 0 22
  • NIO概述 Java NIO全稱為Non-blocking IO或者New IO拾氓,從名字我們知道NIO是非阻塞的I...
    zhong0316閱讀 595評論 0 7