Netty學習筆記(一)

  • Netty是由 JBOSS 提供的一個 Java 開源框架,現為 Github 上的獨立項目。
  • Netty 是一個異步的、基于事件驅動的網絡應用框架,用以快速開發高性能、高可靠性的網絡 IO 程序。
  • Netty 主要針對在 TCP 協議下,面向 Client 端的高并發應用,或者 Peer-to-Peer 場景下的大量數據持續傳輸的應用。
  • Netty 本質是一個 NIO 框架,適用于服務器通訊相關的多種應用場景。
  • 典型的應用有:①阿里分布式服務框架 Dubbo 的 RPC 框 架使用 Dubbo 協議進行節點間通信,Dubbo 協議默認使用 Netty 作為基礎通信組件,用于實現各進程節點之間的內部通信。②經典的 Hadoop 的高性能通信和序列化組件 Avro 的 RPC 框架,默認采用 Netty 進行跨界點通信。它的 NettyService 基于 Netty 框架二次封裝實現。
  • I/O 模型簡單的理解:就是用什么樣的通道進行數據的發送和接收,很大程度上決定了程序通信的性能。
  • Java 共支持 3 種網絡編程模型 I/O 模式:BIONIOAIO
  • Java BIO同步并阻塞(傳統阻塞型),服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情就會造成不必要的線程開銷。
Java BIO
  • Java NIO同步非阻塞,服務器實現模式為一個線程處理多個請求(連接),即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有 I/O 請求就進行處理。
  • Java AIO(NIO.2)異步非阻塞,AIO 引入異步通道的概念,采用了 Proactor模式,簡化了程序編寫,有效的請求才啟動線程,它的特點是先由操作系統完成后才通知服務端程序啟動線程去處理,一般適用于連接數較多且連接時間較長的應用。
  • BIO方式適用于連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,并發局限于應用中,JDK1.4 以前的唯一選擇,但程序簡單易理解。
  • NIO方式適用于連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,彈幕系統,服務器間通訊等。編程比較復雜,JDK1.4 開始支持。
  • AIO方式使用于連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用 OS 參與并發操作,編程比較復雜,JDK7 開始支持。
  • Java BIO就是傳統的Java I/O編程,其相關的類和接口在java.io
  • BIO(BlockingI/O)同步阻塞,服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情就會造成不必要的線程開銷,可以通過線程池機制改善(實現多個客戶連接服務器)。
  • Java BIO 編程流程的梳理:
    • 服務器端啟動一個 ServerSocket;
    • 客戶端啟動 Socket 對服務器進行通信,默認情況下服務器端需要對每個客戶建立一個線程與之通訊;
    • 客戶端發出請求后,先咨詢服務器是否有線程響應,若沒有則等待,或者被拒絕;
    • 若有響應,則客戶端線程會等待請求結束后,再繼續執行。
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
    public static void main(String[] args) throws Exception {
        //采用線程池機制
        //思路:
        //1、創建一個線程池
        //2、如果有客戶端連接,就創建一個線程,與之通訊(單獨寫一個方法)
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        //創建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服務器啟動了...");
        while (true) {
            System.out.println("線程信息id=" + Thread.currentThread().getId() + ",名字=" + Thread.currentThread().getName());
            //監聽,等待客戶端連接
            System.out.println("等待連接....");
            final Socket socket = serverSocket.accept();
            System.out.println("連接到一個客戶端...");
            //就創建一個線程,與之通訊(單獨寫一個方法)
            newCachedThreadPool.execute(new Runnable() {
                public void run() {
                    //可以和客戶端通訊
                    handler(socket);
                }
            });
        }
    }
    //編寫一個handler方法,和客戶端通訊
    public static void handler(Socket socket) {
        try {
            System.out.println("線程信息id=" + Thread.currentThread().getId() + ",名字=" + Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            //通過socket獲取輸入流
            InputStream inputStream = socket.getInputStream();
            //循環的讀取客戶端發送的數據
            while (true) {
                System.out.println("線程信息id=" + Thread.currentThread().getId() + ",名字=" + Thread.currentThread().getName());
                System.out.println("read....");
                int read = inputStream.read(bytes);
                if (read != -1) {
                    //輸出客戶端發送的數據
                    System.out.println(new String(bytes, 0, read));
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("關閉和client的連接...");
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
  • Java BIO問題分析:
    • 每個請求都需要創建獨立的線程,與對應的客戶端進行數據Read,業務處理和Write數據。
    • 當并發數較大時,需要創建大量線程來處理連接,系統資源占用較大。
    • 連接建立后,若當前線程暫時沒有數據可讀,則線程就阻塞在 Read 操作上,造成線程資源浪費。
  • Java NIO(Java non-blocking IO):從 JDK1.4 開始,Java 提供了一系列改進的輸入/輸出的新特性,被統稱為 NIO(即 NewIO),是同步非阻塞的。
  • Java NIO相關類都被放在java.nio包及子包下,并且對原java.io包中的很多類進行改寫。
  • Java NIO有三大核心部分:Channel(通道)、Buffer(緩沖區)、Selector(選擇器)。
  • Java NIO面向緩沖區面向塊編程的。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞式的高伸縮性網絡。
  • Java NIO的非阻塞模式,使一個線程從某個通道發送請求或讀取數據,但是它僅能得到目前可用的數據,若目前沒有數據可用時,則什么都不會獲取,而不是保持線程阻塞,所以直至數據變化之前,該線程可以繼續做其他的事情。
  • 通俗理解:Java NIO是可以做到用一個線程來處理多個操作的。假設有 10000 個請求過來,根據實際情況,可以分配 50 或者 100 個線程來處理,不像之前的阻塞 IO 那樣,非得分配 10000 個。
  • HTTP 2.0使用了多路復用的技術,做到同一個連接并發處理多個請求,而且并發請求的數量比 HTTP 1.1 大了好幾個數量級。
import java.nio.IntBuffer;
public class BasicBuffer {
    public static void main(String[] args) {
        //舉例說明 Buffer 的使用(簡單說明)
        //創建一個 Buffer,大小為 5,即可以存放 5 個 int
        IntBuffer intBuffer = IntBuffer.allocate(5);
        //向buffer存放數據
        for (int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put(i * 2);
        }
        //如何從 buffer 讀取數據
        //將 buffer 轉換,注意讀寫切換(!!!)
        intBuffer.flip();
        //若緩沖區還有剩余數據,則繼續讀取
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}
  • Java NIO與BIO的比較:
    • BIO 以流的方式處理數據,而 NIO 以塊的方式處理數據,塊 I/O 的效率比流 I/O 高很多。
    • BIO 是阻塞的,NIO 則是非阻塞的。
    • BIO 基于字節流和字符流進行操作,而 NIO 基于 Channel(通道)和 Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇器)用于監聽多個通道的事件(比如:連接請求,數據到達等),因此使用單個線程就可以監聽多個客戶端通道。
三大核心部分的關系圖
  • Selector、Channel和Buffer之間的關系如下:
    • 每個 Channel 都會對應一個 Buffer;
    • Selector 對應一個線程,一個線程對應多個 Channel(連接);
    • 該圖反應了有三個 Channel 注冊到該 Selector;
    • 程序切換到哪個 Channel 是由事件(Event)決定的;
    • Selector 會根據不同的事件,在各個通道上切換;
    • Buffer 就是一個內存塊,底層是有一個數組;
    • 通過 Buffer來讀取或寫入數據,BIO 中要么是輸入流或是輸出流,不能雙向,但 NIO 的 Buffer 是可讀可寫的,需要 flip 方法切;
    • Channel 是雙向的,可以返回底層操作系統的情況,比如 Linux,底層的操作系統通道就是雙向的。
  • 緩沖區(Buffer):本質上是一個可以讀寫數據的內存塊,可以理解成是一個容器對象(含數組),其內置了一些機制,能夠跟蹤和記錄緩沖區的狀態變化情況。Channel 提供從文件、網絡讀取數據的渠道,但是必須經過 Buffer才能讀取和寫入數據。
  • 在 NIO 中,Buffer 是一個頂層父類,它是一個抽象類,類的層級關系圖如下所示:
  • Buffer 類定義了所有的緩沖區都具有的四個屬性來提供關于其所包含數據元素的信息:
Buffer類相關方法一覽
ByteBuffer類
  • Java NIO 的通道類似于流,但有些區別如下:
    • 通道可以同時進行讀寫,而流只能讀或者只能寫;
    • 通道可以實現異步讀寫數據;
    • 通道可以從緩沖區中讀數據,也可以寫數據到緩沖區中。
  • Java BIO 中的 Stream 是單向的,例如FileInputStream對象只能讀數據,而 Java NIO 中的通道(Channel)是雙向的,可以進行讀寫操作。
  • Channel 在 NIO 中是一個接口:public interface Channel extends Closeable{}
  • 常用的 Channel 類有:FileChannelDatagramChannelServerSocketChannelSocketChannel。(ServerSocketChannel類似于ServerSocketSocketChannel類似于Socket
  • FileChannel用于文件的數據讀寫;DatagramChannel用于UDP的數據讀寫;ServerSocketChannelSocketChannel用于TCP的數據讀寫。
  • FileChannel 主要用來對本地文件進行 IO 操作,常見的方法有:
    • public int read(ByteBuffer dst):從通道中讀取數據并放到緩沖區中;
    • public int write(ByteBuffer src):把緩沖區中的數據寫入到通道中;
    • public long transferFrom(ReadableByteChannel src, long position, long count):從目標通道中復制數據到當前通道中;
    • public long transferTo(long position, long count, WritableByteChannel target):從當前通道中把數據復制給目標通道中。
  • 寫入數據到文件的案例:
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel01 {
    public static void main(String[] args) throws Exception {
        String str = "hello,張三";
        //創建一個輸出流 -> channel
        FileOutputStream fileOutputStream = new FileOutputStream("D:\\opt\\file01.txt");
        //通過 fileOutputStream 獲取對應的 FileChannel
        //這個 fileChannel 真實類型是 FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();
        //創建一個緩沖區 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //將 str 放入 byteBuffer
        byteBuffer.put(str.getBytes());
        //對 byteBuffer 進行翻轉 flip
        byteBuffer.flip();
        //將 byteBuffer 數據寫入到 fileChannel
        fileChannel.write(byteBuffer);
        fileOutputStream.close();
    }
}
  • 讀取文件中數據的案例:
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel02 {
    public static void main(String[] args) throws Exception {
        //創建文件的輸入流
        File file = new File("D:\\opt\\file01.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        //通過 fileInputStream 獲取對應的 FileChannel -> 實際類型為 FileChannelImpl
        FileChannel fileChannel = fileInputStream.getChannel();
        //創建緩沖區
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
        //將通道的數據讀入到 Buffer
        fileChannel.read(byteBuffer);
        //將 byteBuffer 的字節數據轉成 String
        System.out.println(new String(byteBuffer.array()));
        fileInputStream.close();
    }
}
  • 使用一個緩沖區完成文件的讀取和寫入:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel03 {
    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("D:\\opt\\file01.txt");
        FileChannel fileChannel01 = fileInputStream.getChannel();
        FileOutputStream fileOutputStream = new FileOutputStream("D:\\opt\\file02.txt");
        FileChannel fileChannel02 = fileOutputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        //循環讀取
        while (true) {
            //這里有一個重要的操作,一定不要忘了
            /*
            public final Buffer clear() {
                position = 0;
                limit = capacity;
                mark = -1;
                return this;
            }
            */
            //清空 buffer
            byteBuffer.clear();
            int read = fileChannel01.read(byteBuffer);
            //輸出讀到的字節數
            System.out.println("read = " + read);
            //表示讀完
            if (read == -1) {
                break;
            }
            //將 buffer 中的數據寫入到 fileChannel02 -- file02.txt
            byteBuffer.flip();
            fileChannel02.write(byteBuffer);
        }
        //關閉相關的流
        fileInputStream.close();
        fileOutputStream.close();
    }
}
  • 使用transferFrom方法拷貝文件:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
public class NIOFileChannel04 {
    public static void main(String[] args) throws Exception {
        //創建相關流
        FileInputStream fileInputStream = new FileInputStream("D:\\opt\\avatar1.jpg");
        FileOutputStream fileOutputStream = new FileOutputStream("D:\\opt\\avatar2.jpg");
        //獲取各個流對應的 FileChannel
        FileChannel srcCh = fileInputStream.getChannel();
        FileChannel destCh = fileOutputStream.getChannel();
        //使用 transferForm 完成拷貝
        destCh.transferFrom(srcCh, 0, srcCh.size());
        //關閉相關通道和流
        srcCh.close();
        destCh.close();
        fileInputStream.close();
        fileOutputStream.close();
    }
}
  • ByteBuffer 支持類型化的 put 和 get,put 放入的是什么數據類型,get 就應該使用相應的數據類型來取出,否則可能有 BufferUnderflowException 異常。
import java.nio.ByteBuffer;
public class NIOByteBufferPutGet {
    public static void main(String[] args) {
        //創建一個 Buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);
        //類型化方式放入數據
        buffer.putInt(100);
        buffer.putLong(9);
        buffer.putChar('尚');
        buffer.putShort((short) 4);
        //按類型取出對應類型的數據
        buffer.flip();
        System.out.println(buffer.getInt());
        System.out.println(buffer.getLong());
        System.out.println(buffer.getChar());
        System.out.println(buffer.getShort());
    }
}
  • 可以將一個普通 Buffer 轉成只讀 Buffer:
import java.nio.ByteBuffer;
public class ReadOnlyBuffer {
    public static void main(String[] args) {
        //創建一個 buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);
        for (int i = 0; i < 64; i++) {
            buffer.put((byte) i);
        }
        //讀取
        buffer.flip();
        //得到一個只讀的 Buffer
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass());
        //讀取
        while (readOnlyBuffer.hasRemaining()) {
            System.out.println(readOnlyBuffer.get());
        }
        //不能往只讀的緩沖區中再寫入數據,否則會出現這個異常:ReadOnlyBufferException
        readOnlyBuffer.put((byte) 100);
    }
}
  • NIO 還提供了 MappedByteBuffer,可以讓文件直接在內存(堆外的內存)中進行修改,而如何同步到文件由 NIO 來完成。
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
 * 說明 MappedByteBuffer 可讓文件直接在內存(堆外內存)修改,操作系統不需要拷貝一次
 */
public class MappedByteBufferTest {
    public static void main(String[] args) throws Exception {
        RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\opt\\file01.txt", "rw");
        //獲取對應的文件通道
        FileChannel channel = randomAccessFile.getChannel();
        /**
         * 參數 1:FileChannel.MapMode.READ_WRITE 使用的讀寫模式
         * 參數 2:0:可以直接修改的起始位置
         * 參數 3:5: 是映射到內存的大小(不是索引位置),即將 file01.txt 的多少個字節映射到內存,表示可以直接修改的數據范圍為 0-5 個字節
         * MappedByteBuffer 實際類型為 DirectByteBuffer
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        mappedByteBuffer.put(0, (byte) 'H');
        mappedByteBuffer.put(3, (byte) '9');
        //越界異常:IndexOutOfBoundsException
        //mappedByteBuffer.put(5, (byte) 'Y');
        randomAccessFile.close();
        System.out.println("修改成功~~~");
    }
}
  • NIO 還支持通過多個 Buffer(即 Buffer數組)完成讀寫操作,即 Scattering 和 Gathering。
import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
/**
 * Scattering:將數據寫入到 buffer 時,可以采用 buffer 數組,依次寫入 [分散]
 * Gathering:從 buffer 讀取數據時,可以采用 buffer 數組,依次讀 [聚集]
 */
public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws Exception {
        //使用 ServerSocketChannel 和 SocketChannel 網絡
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
        //綁定端口到 socket,并啟動
        serverSocketChannel.socket().bind(inetSocketAddress);
        //創建 buffer 數組
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(5);
        byteBuffers[1] = ByteBuffer.allocate(3);
        //等客戶端連接 (telnet)
        SocketChannel socketChannel = serverSocketChannel.accept();
        //假定從客戶端接收 8 個字節
        int messageLength = 8;
        //循環的讀取
        while (true) {
            int byteRead = 0;
            while (byteRead < messageLength) {
                long l = socketChannel.read(byteBuffers);
                //累計讀取的字節數
                byteRead += l;
                System.out.println("byteRead=" + byteRead);
                //使用流打印,查看當前的這個 buffer 的 position 和 limit
                Arrays.stream(byteBuffers).map(buffer -> "position=" + buffer.position() + ",limit=" + buffer.limit()).forEach(System.out::println);
            }
            //將所有的 buffer 進行 flip
            Arrays.asList(byteBuffers).forEach(Buffer::flip);
            //將數據讀出顯示到客戶端
            long byteWrite = 0;
            while (byteWrite < messageLength) {
                long l = socketChannel.write(byteBuffers);
                byteWrite += l;
            }
            //將所有的buffer進行clear
            Arrays.asList(byteBuffers).forEach(Buffer::clear);
            System.out.println("byteRead=" + byteRead + ",byteWrite=" + byteWrite + ",messageLength=" + messageLength);
        }
    }
}
  • Java NIO采用非阻塞的 IO 方式,使用Selector(選擇器)可以用一個線程處理多個客戶端連接。Selector 能夠檢測多個注冊在通道上是否有事件發生(注意:多個 Channel 以事件的方式可以注冊到同一個 Selector) 只有在(連接/通道)真正有讀寫事件發生時,才會進行讀寫,這就大大地減少了系統開銷,不必為每個連接都創建一個線程,即不用去維護多個線程,避免了多線程之間的上下文切換導致的開銷。
  • Netty 的 IO 線程 NioEventLoop 聚合了 Selector(選擇器,也叫多路復用器),可以并發處理成百上千個客戶端連接。當線程從某客戶端 Socket 通道進行讀寫數據時,若沒有數據可用時,則該線程可以進行其他任務。線程通常將非阻塞 IO 的空閑時間用于在其他通道上執行 IO 操作,所以單個線程可以管理多個輸入和輸出通道。由于讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運行效率,避免由于頻繁 I/O 阻塞導致的線程掛起。
  • 一個 I/O 線程可以并發處理 N 個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞 I/O 一連接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。
  • Selector 相關方法說明:
    • selector.select():阻塞;
    • selector.select(1000):阻塞 1000 毫秒,在 1000 毫秒后返回;
    • selector.wakeup():喚醒 selector;
    • selector.selectNow():不阻塞,立馬返還。
NIO 非阻塞網絡編程關系圖
  • 對上圖的說明:
    • 當客戶端連接時,會通過 ServerSocketChannel 得到 SocketChannel;
    • Selector 進行監聽 select 方法,返回有事件發生的通道個數;
    • 將 socketChannel 注冊(register(Selector sel, int ops))到 Selector 上,一個 Selector 上可以注冊多個 SocketChannel,注冊后返回一個 SelectionKey,會和該 Selector 關聯(用集合管理),進一步得到各個 SelectionKey(有事件發生),再通過 SelectionKey 反向獲取(通過 channel()方法) SocketChannel,最后通過得到的 channel來完成業務處理。
  • 實現服務器和客戶端的簡單通訊(NIO入門案例):
  • 服務端代碼:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
    public static void main(String[] args) throws IOException {
        //創建ServerSocketChannel -> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //得到一個Selector對象
        Selector selector = Selector.open();
        //綁定一個端口6666
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //設置通道為非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //把 serverSocketChannel 注冊到 selector ,關心事件為:OP_ACCEPT,有新的客戶端連接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //循環等待客戶端連接
        while (true) {
            //等待1秒,如果沒有事件發生,就返回
            if (selector.select(1000) == 0) {
                System.out.println("服務器等待了1秒,無連接");
                continue;
            }
            //如果返回值大于,表示已經獲取到關注的事件,就獲取到相關的 selectionKey 集合,反向獲取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //遍歷 Set<SelectionKey>,使用迭代器遍歷
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()) {
                //獲取到SelectionKey
                SelectionKey key = keyIterator.next();
                //根據 key 對應的通道發生的事件,做相應的處理
                //若是 OP_ACCEPT,則表示有新的客戶端連接
                if (key.isAcceptable()) {
                    //該客戶端生成一個 SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客戶端連接成功,生成了一個SocketChannel:" + socketChannel.hashCode());
                    //將SocketChannel設置為非阻塞模式
                    socketChannel.configureBlocking(false);
                    //將socketChannel注冊到selector,關注事件為 OP_READ,同時給SocketChannel關聯一個Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (key.isReadable()) {
                    //通過key,反向獲取到對應的Channel
                    SocketChannel channel = (SocketChannel) key.channel();
                    //獲取到該channel關聯的Buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    channel.read(buffer);
                    System.out.println("from 客戶端:" + new String(buffer.array()));
                }
                //手動從集合中移除當前的 selectionKey,防止重復操作
                keyIterator.remove();
            }
        }
    }
}
  • 客戶端代碼:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
    public static void main(String[] args) throws IOException {
        //得到一個網絡通道
        SocketChannel socketChannel = SocketChannel.open();
        //設置通道為非阻塞模式
        socketChannel.configureBlocking(false);
        //提供服務器端的IP和端口
        InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6666);
        //連接服務器
        //若連接不成功
        if (!socketChannel.connect(socketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("因為連接需要時間,客戶端不會阻塞,可以做其他工作。。。");
            }
        }
        //如果連接成功,就發送數據
        String str = "hello, 張三";
        //包裹字節數組,分配緩沖區的容量由消息的長度決定
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        //發送數據,實際上就是將buffer數據寫入到channel
        socketChannel.write(byteBuffer);
        System.in.read();
    }
}
  • SelectionKey,表示 Selector 和網絡通道的注冊關系,共四種:
    • int OP_READ:代表讀操作,值為 1;
    • int OP_WRITE:代表寫操作,值為 4;
    • int OP_CONNECT:代表連接已經建立,值為 8;
    • int OP_ACCEPT:有新的網絡連接可以 accept,值為 16。
SelectionKey相關方法
  • ServerSocketChannel:在服務器端監聽新的客戶端 Socket 連接,相關方法如下:
  • SocketChannel:網絡 IO 通道,具體負責進行讀寫操作。NIO 把緩沖區的數據寫入通道,或者把通道里的數據讀到緩沖區,相關方法如下:
  • NIO網絡編程應用實例-群聊系統:
    • 編寫一個 NIO 群聊系統,實現服務器端和客戶端之間的數據簡單通訊(非阻塞),實現多人群聊;
    • 服務器端:可以監測用戶上線,離線,并實現消息轉發功能;
    • 客戶端:通過 Channel 可以無阻塞發送消息給其它所有用戶,同時可以接受其它用戶發送的消息(有服務器轉發得到)
  • 服務端代碼:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class GroupChatServer {
    //定義屬性
    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int PORT = 6667;
    //構造器
    //初始化工作
    public GroupChatServer() {
        try {
            //得到選擇器
            selector = Selector.open();
            //ServerSocketChannel
            listenChannel = ServerSocketChannel.open();
            //綁定端口
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            //設置通道為非阻塞模式
            listenChannel.configureBlocking(false);
            //將該 listenChannel 注冊到 selector
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //監聽
    public void listen() {
        try {
            //循環處理
            while (true) {
                int count = selector.select();
                //有事件處理
                if (count > 0) {
                    //遍歷得到 selectionKey 集合
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        //取出 selectionKey
                        SelectionKey key = iterator.next();
                        //監聽到 accept
                        if (key.isAcceptable()) {
                            SocketChannel sc = listenChannel.accept();
                            sc.configureBlocking(false);
                            //將該 sc 注冊到 selector
                            sc.register(selector, SelectionKey.OP_READ);
                            //提示上線
                            System.out.println(sc.getRemoteAddress() + " 上線 ");
                        }
                        //通道發送read事件,即通道是可讀的狀態
                        if (key.isReadable()) {
                            // 處理讀
                            readData(key);
                        }
                        //當前的 key 刪除,防止重復處理
                        iterator.remove();
                    }
                } else {
                    System.out.println("等待....");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //發生異常處理....
        }
    }
    //讀取客戶端消息
    public void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            //得到 channel
            channel = (SocketChannel) key.channel();
            //創建 buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);
            //根據 count 的值做處理
            if (count > 0) {
                //把緩存區的數據轉成字符串
                String msg = new String(buffer.array());
                //輸出該消息
                System.out.println("form 客戶端:" + msg);
                //向其它的客戶端轉發消息(除了自己),專門寫一個方法來處理
                sendInfoToOtherClients(msg, channel);
            }
        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 離線了..");
                //取消注冊
                key.cancel();
                //關閉通道
                channel.close();
            } catch (IOException e2) {
                e2.printStackTrace();
            }
        }
    }
    //轉發消息給其它客戶(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
        System.out.println("服務器轉發消息中...");
        //遍歷所有注冊到 selector 上的 SocketChannel,并排除 self
        for (SelectionKey key : selector.keys()) {
            //通過 key 取出對應的 SocketChannel
            Channel targetChannel = key.channel();
            //排除自己
            if (targetChannel instanceof SocketChannel && targetChannel != self) {
                //轉型
                SocketChannel dest = (SocketChannel) targetChannel;
                //將 msg 存儲到 buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //將 buffer 的數據寫入通道
                dest.write(buffer);
            }
        }
    }
    public static void main(String[] args) {
        //創建服務器對象
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }
}
  • 客戶端代碼:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
public class GroupChatClient {
    //定義相關的屬性
    private final String HOST = "127.0.0.1";//服務器的ip
    private final int PORT = 6667;//服務器端口
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;
    //構造器,完成初始化工作
    public GroupChatClient() throws IOException {
        selector = Selector.open();
        //連接服務器
        socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
        //設置通道為非阻塞模式
        socketChannel.configureBlocking(false);
        //將 channel 注冊到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        //得到 username
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");
    }
    //向服務器發送消息
    public void sendInfo(String info) {
        info = username + " 說:" + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //讀取從服務器端回復的消息
    public void readInfo() {
        try {
            int readChannels = selector.select();
            //有可以用的通道
            if (readChannels > 0) {
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if (key.isReadable()) {
                        //得到相關的通道
                        SocketChannel sc = (SocketChannel) key.channel();
                        //得到一個 Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        //讀取
                        sc.read(buffer);
                        //把讀到的緩沖區的數據轉成字符串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                }
                //刪除當前的 selectionKey,防止重復操作
                iterator.remove();
            } else {
                //System.out.println("沒有可以用的通道...");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws Exception {
        //啟動我們客戶端
        GroupChatClient chatClient = new GroupChatClient();
        //啟動一個線程,每隔 3 秒,讀取從服務器發送數據
        new Thread(() -> {
            while (true) {
                chatClient.readInfo();
                try {
                    Thread.currentThread().sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        //發送數據給服務器端
        Scanner scanner = new Scanner(System.in);
        //按行讀取內容
        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            chatClient.sendInfo(s);
        }
    }
}
  • 在 Java 程序中,常用的零拷貝有 mmap(內存映射)和 sendFile。
  • Java 傳統 IO 和網絡編程的一段代碼:
File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
傳統IO模型
  • DMA:direct memory access 直接內存拷貝(不使用 CPU)
  • mmap:通過內存映射,將文件映射到內核緩沖區,并且用戶空間可以共享內核空間的數據,這樣在進行網絡傳輸時就可以減少內核空間到用戶空間的拷貝次數。
mmap
  • Linux2.1 版本提供了sendFile函數,其基本原理為:數據根本不經過用戶態,直接從內核緩沖區進入Socket Buffer,由于和用戶態完全無關,就減少了一次上下文切換。零拷貝從操作系統角度,是沒有 cpu 拷貝
  • Linux在2.4 版本中,做了一些修改,避免了從內核緩沖區拷貝到 Socketbuffer 的操作,直接拷貝到協議棧,又一次減少了數據拷貝。
  • 其實還是有一次 cpu 拷貝(kernel buffer -> socket buffer),但是拷貝的信息很少,比如 length、offset 消耗低,可以忽略。
  • 我們說零拷貝,是從操作系統的角度來說的。因為內核緩沖區之間,沒有數據是重復的(只有 kernel buffer 有一份數據)。零拷貝不僅僅帶來更少的數據復制,還能帶來其它的性能優勢,例如更少的上下文切換,更少的 CPU 緩存偽共享以及無 CPU 校驗和計算。
  • mmapsendFile的區別:
    • mmap 適合小數據量讀寫,sendFile 適合大文件傳輸。
    • mmap 需要 4 次上下文切換,3 次數據拷貝;sendFile 需要 3 次上下文切換,最少 2 次數據拷貝。
    • sendFile 可以利用 DMA 方式,減少 CPU 拷貝,mmap 則不能(必須從內核拷貝到 Socket緩沖區)。
  • Java NIO零拷貝案例:
  • 服務端代碼:
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NewIOServer {
    public static void main(String[] args) throws Exception {
        InetSocketAddress address = new InetSocketAddress(7001);
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.bind(address);
        //創建buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            int readcount = 0;
            while (-1 != readcount) {
                try {
                    readcount = socketChannel.read(byteBuffer);
                } catch (Exception ex) {
                    // ex.printStackTrace();
                    break;
                }
                //倒帶:position = 0,mark=-1
                byteBuffer.rewind();
            }
        }
    }
}
  • 客戶端代碼:
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class NewIOClient {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 7001));
        String filename = "D:\\opt\\kafka_2.12-2.5.0.tgz";
        //得到一個文件channel
        FileChannel fileChannel = new FileInputStream(filename).getChannel();
        //準備發送
        long startTime = System.currentTimeMillis();
        //在 linux 下一個 transferTo 方法就可以完成傳輸
        //在 windows 下一次調用 transferTo 方法只能發送 8m的內容,這就需要分段傳輸文件,而且要注意傳輸時的位置
        //transferTo 底層使用到零拷貝
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        System.out.println("發送的總的字節數=" + transferCount + " 耗時: " + (System.currentTimeMillis() - startTime));
        //關閉通道
        fileChannel.close();
    }
}
  • JDK7 引入了AsynchronousI/O,即AIO。在進行 I/O 編程中,常用到兩種模式:ReactorProactor。Java 的 NIO 就是 Reactor,當有事件觸發時,服務器端得到通知,進行相應的處理。
  • AIO(NIO2.0):異步不阻塞的 IO。AIO 引入異步通道的概念,采用了 Proactor 模式,簡化了程序編寫,有效的請求才啟動線程,它的特點是先由操作系統完成后才通知服務端程序啟動線程去處理,一般適用于連接數較多且連接時間較長的應用。
BIO NIO AIO
IO模型 同步阻塞 同步非阻塞(多路復用) 異步非阻塞
編程難道 簡單 復雜 復雜
可靠性
吞吐量
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容