天天看點

NIO 學習筆記NIO學習

NIO學習

NIO 三個重要元件 Buffer Channel Selector

1.Buffer

Buffer,底層數組,通過4個标志維護

NIO 學習筆記NIO學習
  • position表示目前指針位置/數組下标
  • limit表示緩沖區目前最多處理的資料
  • capacity容量
  • mark标志位

重要的api

NIO 學習筆記NIO學習
  • get() 擷取目前位置的資料,指針右移
  • put(資料) 向目前位置填入資料
  • allocate(int n)初識化,建立一個大小為n的緩沖區
//Buffer的使用
    public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(5);

        for(int i = 0;i<intBuffer.capacity();i++){
            intBuffer.put(i*3);//position++
        }
        //Buffer是雙向的,既可以讀,也可以寫
        //讀寫切換 "轉向"
        //limit = position
        //position = 0
        intBuffer.flip();


        //檢查下一個位置是否有資料
        while(intBuffer.hasRemaining()){
            //get()方法擷取該位置的值,并且将向後移動指針
            System.out.println(intBuffer.get());//position++
        }
    }
           

filp()和clear()兩者都會把position改為0,不過flip首先将limit=position,再将position置為0,clear将limit = capacity,position = 0

Buffer的聚合和分散

  • 可以建立一個Buffer數組進行資料的讀取寫入,這樣會更靈活。
  • read() write()接受Buffer[]參數
/*簡單的聚合和分散展示*/
public static void main(String[] args) throws Exception{
        ByteBuffer [] byteBuffers = new ByteBuffer[2];
        byteBuffers[0]=ByteBuffer.allocate(5);
        byteBuffers[1]=ByteBuffer.allocate(5);
	//建立Socket步驟
	//1.打開一個ServerSocketChannel用來建立SocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        InetSocketAddress inetSocketAddress = new InetSocketAddress(30000);
	//2.綁定監聽端口号
        serverSocketChannel.socket().bind(inetSocketAddress);
	//3.等待用戶端連接配接(類似于ServerSocket的accept方法)  阻塞
        SocketChannel accept = serverSocketChannel.accept();
	
        int msgLength = 10;//最大輸入長度

        while(true){
        //記錄輸入長度
            long byteRead = 0;

            while(byteRead < msgLength){
            	//阻塞
                long read = accept.read(byteBuffers);
                byteRead+=read;
                System.out.println("byteRead="+byteRead);
				//列印必要資訊
                Arrays.asList(byteBuffers).stream().map(byteBuffer ->"position="+byteBuffer.position()+" ,limit="+byteBuffer.limit()).forEach(System.out::println);

            }
            Arrays.asList(byteBuffers).forEach(byteBuffer -> {byteBuffer.flip();});

            long byteWrite = 0;

            while (byteWrite<msgLength){
            
                long write = accept.write(byteBuffers);

                byteWrite+=write;

            }

            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());

            System.out.println("byteread="+byteRead+" bytewrite="+byteWrite+" msg length="+msgLength);


        }
    }
           

發送至少10個才調用write

使用telnet進行測試:

  1. 發送4個
    NIO 學習筆記NIO學習
  2. 發送6個
    NIO 學習筆記NIO學習
  3. 發送10個,執行一次read,一次write
NIO 學習筆記NIO學習

發送12個

讀兩次,一次10個,一次2個,同時執行一次write

NIO 學習筆記NIO學習

2.Channel

FileChannel

是FileOutputStream/FileInPutStream的成員變量,即被包裹在io類中

NIO 學習筆記NIO學習

api:

FileChannel寫入檔案

public static void main(String[] args) {
        String str = "hello world!";

        try(FileOutputStream out = new FileOutputStream("myText.txt")){
			//擷取Channel
            FileChannel channel = out.getChannel();

            ByteBuffer buffer = ByteBuffer.allocate(1024);

            buffer.put(str.getBytes("UTF-8"));
			//filp()将指針position置0,修改limit
            buffer.flip();//此方法不要忘記
			//将buffer的資料寫入到myText.txt檔案中
            channel.write(buffer);

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

channel.write()方法将從position所指向的位置進行寫入,如果不執行flip()方法,可以想象write會寫入錯誤的資料

FileChannel讀取檔案

public static void main(String[] args) {
        File f = new File("myText.txt");
        try(FileInputStream input = new FileInputStream(f)){
            FileChannel fileChannel = input.getChannel();

            ByteBuffer byteBuffer = ByteBuffer.allocate((int)f.length());

            fileChannel.read(byteBuffer);

            System.out.println(new String(byteBuffer.array(),"UTF-8"));
        }catch (IOException ie){
            ie.printStackTrace();
        }
    }
           

byteBuffer.array()方法傳回對應類型Buffer的數組,ByteBuffer就傳回 byte[]

FileChannel讀取并寫入檔案 (檔案拷貝)

public static void main(String[] args) {
        try(FileInputStream input = new FileInputStream("text.txt");FileOutputStream output = new FileOutputStream("copy.txt")){
            FileChannel channel = input.getChannel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            FileChannel channel1 = output.getChannel();

            while(true){
            //clear()方法不能忘記寫!!!
                byteBuffer.clear();
                if(channel.read(byteBuffer)==-1){
                    break;
                }
                byteBuffer.flip();
                channel1.write(byteBuffer);
            }
            
        }catch (IOException ie){
            ie.printStackTrace();
        }
    }
           

channel.read(byteBuffer);

方法調用将會傳回讀取的資料數量,有個神奇的地方就是當position==limit時,會傳回0,永遠不會傳回-1。

也就是說,忘記寫clear()或者自作聰明把clear()用flip()代替(比如我),該程式會陷入死循環。

未寫clear,無限循環寫入buffer的資料;把clear()寫成flip(),無限循環,但是因為第一次執行就把position=limit=0,不會寫入任何資料。

FileChannel提供了檔案拷貝的函數

public static void main(String[] args) {
        try(FileInputStream in = new FileInputStream("1.jpeg"); FileOutputStream out = new FileOutputStream("2.jpeg")){
        //來源Channel
            FileChannel inChannel = in.getChannel();
        //寫入Channel
            FileChannel outChannel = out.getChannel();
		//第一個參數是來源Channel,第二個參數是位置,第三個參數是大小
            outChannel.transferFrom(inChannel,0,inChannel.size());
        }catch (IOException e){
            e.printStackTrace();
        }
    }
           

Selector 多路複用器

  • Selector可以檢測多個注冊的通道上是否有事件發生
  • 事件驅動,有事件的時候才會執行連接配接/讀寫
  • 避免線程頻繁切換
  • 1個SelectionKey 對應 1個Channel,可以通過SelectionKey得到監聽的管道。

NIO寫法

NIO服務端基本步驟:

  1. 建立ServerSocketChannel,設定為非阻塞
  2. 将ServerSocketChannel注冊到Selector中監聽事件
  3. 每隔1s調用select()方法監聽有沒有連接配接事件/讀寫事件
  4. 如果有select()!=0,有事件發生,進行判斷,如果是連接配接事件,就把這個Channel注冊,并且開始監聽資料寫入
  5. 當有讀寫事件時擷取SelectionKey并且得到對應Channel,讀取Channel的資料
服務端代碼
//建立ServerSocketChannel
        ServerSocketChannel socketChannel = ServerSocketChannel.open();

        //擷取Selector對象
        Selector selector = Selector.open();

        //綁定端口
        InetSocketAddress i  = new InetSocketAddress(30000);
        socketChannel.bind(i);

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

        //把ServerSocketChannel 注冊到 Selector
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //循環等待用戶端連接配接

        while(true){
            //select監聽哪個注冊的Channel有新的事件 Read/Write/Accept
            if(selector.select(1000)==0){
                //沒有事件發生
                System.out.println("無連接配接");
                continue;
            }

            //擷取到相關的Selection集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while (iterator.hasNext()){
                SelectionKey next = iterator.next();
                if(next.isAcceptable()){
                    //已經有連接配接,accept()是阻塞方法,但是此時一定有連接配接,不會阻塞
                    SocketChannel accept = socketChannel.accept();
                    accept.configureBlocking(false);
                    System.out.println("一個連接配接,"+accept.hashCode());
                    //将新加入的連接配接設定為Read,注冊到selector
                    accept.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));


                }
                if(next.isReadable()){
                    SocketChannel channel = (SocketChannel)next.channel();
                    ByteBuffer attachment = (ByteBuffer)next.attachment();

                    channel.read(attachment);

                    System.out.println("from 用戶端 "+ new String(attachment.array()));
                }
                //防止多線程重複通路
                iterator.remove();
            }

        }
           

所有的Channel都要進行注冊,因為要監聽讀寫事件

ServerSocketChannel主要用來擷取SocketChannel,用于擷取連接配接,SocketChannel主要用于處理讀寫事件(是嗎?)

用戶端代碼
// 連接配接後發送一條hello world!
    public static void main(String[] args) throws Exception{
        SocketChannel socketChannel = SocketChannel.open();
        //socketChannel.configureBlocking(false);

        //伺服器 ip 和 port
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",30000);

        //連接配接伺服器
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()){
                System.out.println("等待...");
            }
        }
        //連接配接成功
        String str = "hello world!";
        //Warps a byte array into a buffer
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //發送資料
        socketChannel.write(buffer);
        System.in.read();
    }

           

(未完待續)

繼續閱讀