Java NIO編程實例

 

 


前言

基於NIO的網絡編程實例


提示:以下是本篇文章正文內容,下面案例可供參考

一、NIO與BIO的比較

NIO與BIO的比較 :

  1. BIO是以流的方式處理的 , NIO是以塊的方式處理的(因爲有Buffer) , 塊I/O效率更高
  2. BIO是基於字節流字符流處理的,NIO是基於Channel和Buffer處理的,單個線程可監聽多個客戶端的通道

NIO三個核心組件之間的關係

  1. 每個Channel都對應一個Buffer , Channel是雙向的,可讀可寫;
  2. Selector對應一個線程 ;
  3. 一個線程對應多個Channel ;
  4. 程序切換到哪個Channel是由時間Event決定的 ;
  5. Selector會根據不同的事件在各個通道上切換 ;
  6. Buffer 就是一個內存塊 , 底層是一個數組 ;
  7. 數據的讀取/寫入是通過Buffer進行的 ,是可以讀也可以寫的 ,但需要flip方法做讀寫切換 ,與BIO有本質區別

二、Buffer的機制及其子類

1.Buffer的使用

Buffer的子類型 : 除了Boolean類型,其餘7個Java子類型Buffer都有

public static void main(String[] args) {
        // buffer的使用
        // 1. 創建一個Buffer
        IntBuffer intBuffer = IntBuffer.allocate(5); // 創建一個容量爲5的Buffer

        // 2. 向Buffer中存放數據
        for (int i = 0; i < 5; i++) {
            intBuffer.put(i*2);
        }
        // 3. 從Buffer中讀取數據
        // 將Buffer做讀寫切換
        intBuffer.flip();

        while (intBuffer.hasRemaining()){
            System.out.println(intBuffer.get()); // get方法裏維護了一個索引
        }

    }
 

2.Buffer的四個基本類型

以InteBuffer爲例 , 真正的數據是存放在 final int[] hb;數組裏的

Buffer中定義了所有緩衝區都具有的4個屬性

	private int mark = -1;  //標記,一般不被修改
    private int position = 0; //下一個要被讀寫的元素的索引
    private int limit; //緩衝區的當前終點  (數組索引的最大值)
    private int capacity; // 最大容量
 

其中 , position不能超過limit , position可以被理解爲一個遊標 , 讀寫的時候是根據position的位置進行的

當調用了 flip()函數反轉過後 , position會被置爲0

同時 , 上述的4個參數都有其對應的函數來修改他們的值

public final Buffer clear()方法能將這4個參數恢復到初始狀態 , 但是數據不會真正的被擦除

三、Channel的使用

1. Channel的特徵

  1. 通道是可以同時讀寫的
  2. 可以實現異步讀寫數據
  3. 可以從Buffer中讀取數據 , 也可以向Buffer寫入數據

2. Channel的子類

Channel的子類 : FileChannel文件數據的讀寫 , DatagramChannel UDP數據的讀寫 , ServerScoketChannel和ScoketChannel用於TCP數據的讀寫
我們用於網絡編程最常用的當然就是ServerScoketChannel和ScoketChannel了

(1) FileChannel實例:

public static void main(String[] args) throws IOException {
        String str = "hello world";
        // 創建一個輸出流
        FileOutputStream fileOutputStream = new FileOutputStream("d://file01.txt");

        // 通過輸出流獲取對應的FileChannel , 其真實類型爲FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();

        //創建一個ButeBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 將str放到byte Buffer
        byteBuffer.put(str.getBytes());

        // filp反轉
        byteBuffer.flip();

        //將byteBuffer數據寫入fileChannel
        fileChannel.write(byteBuffer);
        fileOutputStream.close(); //關閉最底層的流

    }
 

這裏需要注意 : 當需要buffer從讀轉爲寫時,需要調用flip函數做讀寫切換

(2) 拷貝文件

/**
     * 拷貝文件
     * */
    public static void copy() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("1.txt");
        FileChannel channel = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
        FileChannel channel1 = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        while (true){
            // 這裏必須要清空一次數據,將關鍵屬性重置
            /**
             * 如果這裏不做復位,read的值會一直是0,程序會一直讀取數據,進入死循環
             * */
            byteBuffer.clear();
            int read = channel.read(byteBuffer);
            if (read == -1){
                break;
            }
            // 將buffer中的數據寫入到channel1
            byteBuffer.flip();
            channel1.write(byteBuffer);
        }

        fileInputStream.close();
        fileOutputStream.close();
    }
 

四、Buffer類型化和只讀

1. 類型化

所謂的類型化就是指 , 存進去的時什麼數據類型的 . 讀取的就要是什麼數據類型 , 否則會報錯

public static void main(String[] args) {
        // 創建一個buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(64);

        for (int i = 0; i < 64; i++) {
            byteBuffer.put((byte)i);

        }

        //反轉並讀取
        byteBuffer.flip();

        //獲取一個只讀的buffer
        ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();

        while (readOnlyBuffer.hasRemaining()){

            System.out.println(readOnlyBuffer.get());
        }
    }
 

2. Buffer的分散和聚合

Scattering: 將數據寫入到buffer中,可以採用buffer數組,依次寫入

Gathering : 從buffer讀取數據時 , 採用buffer數組以此讀

分散和聚合涉及到同時操作多個Buffer

五、MappedByteBuffer

操作系統級別 , 性能比較高

MappedByteBuffer可以直接在內存(堆外內存)中修改文件 , 操作系統不需要再拷貝一次;

public static void main(String[] args) throws IOException {
        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
        FileChannel channel = randomAccessFile.getChannel();

        /**
         * p1 FileChannel.MapMode.READ_WRITE 使用讀寫模式
         *
         * p2 直接修改的起始位置
         *
         * p3 目標文件將多少個字節映射到內存中
         * p2 p3 表示程序可以直接修改的範圍
         * */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        // 修改對應內容
        mappedByteBuffer.put(0,(byte)'A');
        mappedByteBuffer.put(3,(byte)9);
        randomAccessFile.close();
    }
 

六、Selector

能夠檢測多個註冊上來的通道中是否有時間發生

只有連接/通道上真正有讀寫事件發生時,纔會進行讀寫

避免了多線程上下文切換導致的開銷

1. SelectionKey在NIO體系中的作用

  1. 當客戶端連接時, 會通過ServerSocketChannel得到對應的SocketChannel;
  2. 將SocketChannel註冊到Selector上, 使用的是register(Selector sel, int ops), 一個selector上可以註冊多個SocketChannel;
  3. 註冊後會返回一個SelectionKey , 會和該Selector關聯起來
  4. Selector進行監聽selector方法, 返回有事件發生的channel;
  5. 進一步得到各個有事件發生的SelectionKey , 並通過SelectionKey反向獲取SocketChannel的channel
  6. 根據得到的channel完成業務處理

七、NIO非阻塞網絡編程的快速入門

服務器端

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 創建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //創建一個Selector對象,
        Selector selector = Selector.open();

        // 綁定端口6666, 在服務器端監聽
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        // 設置爲非阻塞
        serverSocketChannel.configureBlocking(false);

        // 把serverSocketChannel註冊到selector
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 循環等待用戶連接
        while (true){
            if (selector.select(1000) == 0){ //等待(阻塞)一秒, 沒有事件發生
//            if (selector.selectNow() == 0){ // 也可以設置成非阻塞的
                System.out.println("服務器等待了一秒,無連接");
                continue;
            }

            // 如果返回的>0 , 就獲取相關的selectionKey集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 返回關注事件的集合

            // 遍歷selectionKeys
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();

            while (keyIterator.hasNext()){
                // 獲取到selectionKey
                SelectionKey key = keyIterator.next();
                //根據key對應的通道獲取事件並做相應處理
                if (key.isAcceptable()){
                    //如果是OP_ACCEPT, 表示有新的客戶端產生
                    //給該客戶端生成SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //將socketChannnel設置爲非阻塞
                    socketChannel.configureBlocking(false);
                    //將socketChannel註冊到selector上, 設置事件爲OP_READ,同時給socketChannel關聯一個buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                if (key.isReadable()){
                    // 發生了OP_READ
                    SocketChannel channel=(SocketChannel)key.channel();
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    System.out.println("from 客戶端"+new String(buffer.array()));
                }

                // 手動從集合中移除當前的selectionKey, 防止多線程情況下的重複操作
                keyIterator.remove();

            }


        }

    }
}
 

客戶端

public class NIOClient {

    public static void main(String[] args) throws IOException {
        // 得到一個網絡通道
        SocketChannel socketChannel = SocketChannel.open();
        // 設置爲非阻塞
        socketChannel.configureBlocking(false);
        //設置服務器端ip和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        if (!socketChannel.connect(inetSocketAddress)){

            while (!socketChannel.finishConnect()){
                //如果沒有連接成功,客戶端是非阻塞的,可以做其它工作
                System.out.println("等待連接...");
            }
        }

        // 如果連接成功,就發送數據
        String str = "hello world";
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        // 發送數據 , 將buffer中的數據寫入到channel中
        socketChannel.write(buffer);
        System.in.read();

    }

}
 

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