NIO的網絡IO操作

一、網絡IO

1.1 概述

文件IO用到的FileChannel並不支持非阻塞操作,學習NIO主要就是進行網絡IO,JavaNIO中的網絡通道是非阻塞IO的實現,基於事件驅動,非常適用於服務器需要維持大量連接,但是數據交換量不大的情況,例如一些即時通信的服務等…

在Java中編寫Socket服務器,通常有以下幾種模式:

  1. 一個客戶 端連接用一個線程,優點:程序編寫簡單;缺點:如果連接非常多,分配的線程也會非常多,服務器可能會因爲資源耗盡而崩潰。
  2. 把每一個客戶端連接交給一個擁有固定數量線程的連接池,優點:程序編寫相對簡單,可以處理大量的連接。線程的開銷非常大,連接如果非常多,排隊現象會比較嚴重。
  3. 使用 Java的NIO,用非阻塞的IO方式處理。這種模式可以用一個線程,處理大量的客戶端連接。

1.2 核心API

1.2.1 Selector選擇器

Selector選擇器能夠檢測多個註冊的通道上是否有事件發生,如果有事件發生,便獲取事件然後針對每個事件進行相應的響應處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接。這樣使得只有在連接真正有讀寫事件發生時,纔會調用函數來進行讀寫,就大大地減少了系統開銷,並且不必爲每個連接都創建一個線程,不用去維護多個線程,並且避免了多線程之間的上下文切換導致的開銷。

該類的常用方法如下所示:

1. public static Selector open(),得到一個選擇器對象
2. public int select(long timeout),監控所有註冊的channel,當其中有註冊的IO操作可以進行時,將對應的SelectionKey 加入到內部集合中並返回,參數用來設置超時時間
3. public Set<SelectionKey> selectedKeys(),從內部集合中得到所有的SelectionKey
4. SelectionKey, 代表了Selector 和serverSocketChannel 的註冊關係,一共四種:
5. int OP_ACCEPT:有新的網絡連接可以accept,值爲16
6. int OP_CONNECT:代表連接已經建立,值爲8

1.2.2 SelectionKey

SelectionKey代表了Selector 和serverSocketChannel 的註冊關係,一共四種:

  1. int OP_ACCEPT:有新的網絡連接可以accept,值爲16
  2. int OP_CONNECT:代表連接已經建立,值爲8
  3. int OP_READ和int OP_WRITE:代表了讀、寫操作,值爲1和4

該類的常用方法如下所示:

* public abstract Selector selector(),得到與之,關聯的Selector對象
 * public abstract SelectableChannel channel(),得到與之關聯的通道
* public final Object attachment(),得到與之關聯的共享數據
* public abstract SelectionKey interestOps(int ops),設置或改變監聽事件
*  public final boolean isAcceptable(), 是否可以accept
* public final boolean isReadable(),是否可以讀
* public final boolean isWritable(),是否可以寫

1.2.3 ServerSocketChannel

用來在服務器端監聽新的客戶端Socket連接,常用方法如下所示:

* public static ServerSocketChannel open(),得到- - 個ServerSocketChannel通道
* public final ServerSocketChannel bind(SocketAddress local),設置服務器端端口號
* public final SelectableChannel configureBlocking(boolean block),設置阻塞或非阻塞模式,取值false 表示採用非阻塞模式
* public SocketChannel accept(),接受-一個連接,返回代表這個連接的通道對象
* public final SelectionKey register(Selector sel, int ops),註冊一個選擇器並設置監聽事件

1.2.4 SocketChannel 網絡IO通道

具體負責進行讀寫操作。NIO總是把緩衝區的數據寫入通道,或者把通道里的數據讀出到緩衝區( buffer)。常用方法如下所示:

* public static SocketChannel open(),得到一個SocketChannel通道
* public final SelectableChannel configureBlocking(boolean block),設置阻塞或非阻塞模式,取值false表示採用非阻塞模式
* public boolean connect(SocketAddress remote),連接服務器
* public boolean finishConnect(),如果上面的方法連接失敗,接下來就要通過該方法完成連接操作
* public int write(ByteBuffer src),往通道里寫數據
* public int read(ByteBuffer dst),從通道里讀數據
* public final SelectionKey register(Selector sel, int ops, Object att),註冊一一個選擇 器並設置監聽事件,最後一個參數可以設置共享數據
* public final void close(),關閉通道

二、簡單的網絡IO的例子(客戶端向服務端發送消息)

2.1 客戶端代碼

package com.example.demo;

import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @author : pengweiwei
 * @date : 2020/1/28 4:54 下午
 */
public class NIOClient {

    public static void main(String[] args) throws Exception{
        //1.先得到一個網絡通道
        SocketChannel socketChannel = SocketChannel.open();

        //2.設置阻塞方式爲非阻塞
        socketChannel.configureBlocking(false);

        //3.設置連接的服務器的IP和端口號
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8888);

        //4.連接服務器
        if(!socketChannel.connect(inetSocketAddress)){
            //如果沒連接上,在等待連接的時候還可以做其他事
            while (!socketChannel.finishConnect()){
                System.out.println(" do something...");
            }
        }

        //5.得到一個緩衝區並存入數據
        String msg = "hello server";
        ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());

        //6.發送數據
        socketChannel.write(byteBuffer);

        //7.不能關閉連接,讓客戶端保持連接
        System.in.read();
    }
}

2.2 服務端代碼

package com.example.demo;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @author : pengweiwei
 * @date : 2020/1/28 5:17 下午
 */
public class NIOServer {

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

        //1.得到一個ServerSocketChannel對象
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //2.得到一個選擇器Selector對象
        Selector selector = Selector.open();

        //3.綁定客戶端的端口號
        serverSocketChannel.bind(new InetSocketAddress(8888));

        //4.設置阻塞方式
        serverSocketChannel.configureBlocking(false);

        //5.把ServerSocketChannel對象註冊給Selector
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //6.監控客戶端
        while (true){
            //表示沒有客戶端嘗試連接
            if(selector.select(2000) == 0){
                System.out.println("沒有客戶端嘗試連接");
                continue;
            }
            //如果有的話,得到所有的SelectionKey,判斷通道里的事件類型
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();

                //判斷selectionKey的事件類型
                 if(selectionKey.isAcceptable()){
                     //客戶端連接事件
                     System.out.println("OP_ACCEPT");
                     SocketChannel socketChannel = serverSocketChannel.accept();
                     socketChannel.configureBlocking(false);
                     socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                 }

                 if(selectionKey.isReadable()){
                     //讀取客戶端數據事件
                     SocketChannel channel = (SocketChannel)selectionKey.channel();
                     ByteBuffer buffer = (ByteBuffer)selectionKey.attachment();
                     channel.read(buffer);
                     System.out.println("客戶端發來的數據"+new String(buffer.array()));
                 }

                iterator.remove();

            }

        }
    }
}

運行結果:
在這裏插入圖片描述
後面的空格是由於緩衝區設置了1024的大小,而發送的消息只有hello,server 後面都是空格,所以,發送消息之前調用字符串的trim去掉空格就行。

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