Netty雜記2—NIO網路程式設計

冰洋發表於2019-03-01

前言

在上篇文章中對BIO網路程式設計的相關內容進行了講解,通過我們一步一步的優化,雖然我們通過多執行緒解決了併發訪問的問題,但是BIO本身的一些特性造成的問題卻沒有得到解決。

BIO是阻塞IO,我們使用執行緒來進行IO的排程,我們無法確定io是否就緒,但是每個IO操作都會建立執行緒,這個時候如果IO未就緒,那麼建立的執行緒也會處於阻塞狀態。

在之前講解NIO基本知識的時候我們提到過NIO通過通道選擇器可以實現同時對多個通道的管理,實際上就是通過管理多個IO操作,換句話說是單執行緒處理多執行緒併發,有效的防止執行緒因為IO沒有就緒而被掛起。

在使用NIO進行網路程式設計的時候需要用到的就是通道選擇器,所以我們先看一下通道選擇器的相關內容。

NIO

ServerSocketChannel

Java NIO中的 ServerSocketChannel 是一個可以監聽新進來的TCP連線的通道, 就像標準IO中的ServerSocket一樣。

//代開ServerSocketChannel 
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
複製程式碼

通過 ServerSocketChannel.accept() 方法監聽新進來的連線。當 accept()方法返回的時候,它返回一個包含新進來的連線的 SocketChannel。 可以設定成非阻塞模式。在非阻塞模式下,accept() 方法會立刻返回,如果還沒有新進來的連線,返回的將是null。

SocketChannel

Java NIO中的SocketChannel是一個連線到TCP網路套接字的通道。可以通過以下2種方式建立SocketChannel:

//開啟一個SocketChannel並連線到網際網路上的某臺伺服器。
socketChannel.connect(new InetSocketAddress("localhost",8888));
//一個新連線到達ServerSocketChannel時,會建立一個SocketChannel。
SocketChannel socketChannel = SocketChannel.open();
複製程式碼

通道選擇器 Selector

為什麼使用通道選擇器?

在前言中提到過,NIO通過通道選擇器可以實現同時對多個通道的管理,其實就是同時對多個IO操作的管理,也就是實現了單執行緒處理多執行緒併發問題。對於作業系統來說,執行緒之間上下文切換的開銷很大,而且每個執行緒都要佔用系統的一些資源(如記憶體)。因此太多的執行緒會耗費大量的資源,所以使用通道選擇器來對多個通道進行管理。

Selector的使用

1. 建立Selector

//通過呼叫Selector.open()方法建立一個Selector,如下:
Selector selector = Selector.open();
複製程式碼

2.將通道註冊到通道選擇器中

//為了將Channel和Selector配合使用,必須將channel註冊到selector上。通過SelectableChannel.register()方法來實現
//        建立ServerSocketChanner
        ServerSocketChannel ssc = ServerSocketChannel.open();
//        繫結埠號
        ssc.bind(new InetSocketAddress(8888));
//        設定通道非阻塞
        ssc.configureBlocking(false);
//        建立通道選擇器
        Selector selector = Selector.open();
//        將通道註冊到通道選擇器中 要求:通道都必須是非阻塞的  意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式
//        第二個引數是我們需要通道選擇器幫我們管理什麼事件型別  註冊為接受就緒
        ssc.register(selector, SelectionKey.OP_ACCEPT);
複製程式碼

register()方法的第二個引數代表註冊的通道事件型別,一個通道觸發一個事件就意味著該事件準備就緒了,總共有四種事件:Connect(連線就緒 ) Accept(接受就緒) Read(有資料可讀的通道 讀就緒) Write(寫就緒)。對於選擇器而言,可以針對性的找到(監聽)的事件就緒的通道,進行相關的操作。

這四種事件用SelectionKey的四個常量來表示:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

可以用“位或”操作符將常量連線起來

SelectionKey物件

物件中包含很多的屬性,譬如:

  • interest集合(事件集合)
  • ready集合
  • Channel
  • Selector
  • 附加的物件(可選)

3.選擇器的select ()方法

select()方法返回的int值表示有多少通道已經就緒。自上次呼叫select()方法後有多少通道變成就緒狀態。如果呼叫select()方法,因為有一個通道變成就緒狀態,返回了1,若再次呼叫select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法呼叫之間,只會有一個通道就緒。

4.選擇器的selectedKeys()方法

通過呼叫selector的selectedKeys()方法,可以得到就緒通道的集合。遍歷集合可以找到自己需要的通道進行相關的操作。

Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();

            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                //處理nio事件
                if(key.isAcceptable()){
//                    獲取通道
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();

//                    註冊讀事件型別
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);


                }

                if(key.isReadable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();

//                    讀取資料
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    int len = -1;
                    while (true){
                        byteBuffer.clear();
                        len = socketChannel.read(byteBuffer);
                        if(len == -1){
                            break;
                        }

                        byteBuffer.flip();
                        while (byteBuffer.hasRemaining()){
                            bos.write(byteBuffer.get());
                        }
                    }
//                    列印讀取到的資料
                    System.out.println(bos.toString());
                    //寫資料給客戶端 註冊事件型別 寫事件
                    socketChannel.register(selector,SelectionKey.OP_WRITE);
                }
                if(key.isWritable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    String msg = "你好,我是伺服器";
                    ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
                    byteBuffer.put(msg.getBytes());
                    byteBuffer.flip();
                    socketChannel.write(byteBuffer);
                    socketChannel.close();
                }
                iterator.remove();
            }
複製程式碼

NIO網路程式設計例項

伺服器程式設計

基本步驟
  1. 開啟ServerSocketChanner
  2. 繫結埠號
  3. 設定通道非阻塞
  4. 開啟選擇器,把通道註冊到選擇器中
  5. 使用Selector輪詢所有的key
    1. 獲取socketChannel
    2. 設定非阻塞 註冊讀事件
    3. 讀取操作
    4. 註冊寫事件
    5. 寫操作
NIO網路程式設計
程式碼
import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws Exception{
//        建立ServerSocketChanner
        ServerSocketChannel ssc = ServerSocketChannel.open();

//        繫結埠號
        ssc.bind(new InetSocketAddress(8888));

//        設定通道非阻塞
        ssc.configureBlocking(false);

//        建立通道選擇器
        Selector selector = Selector.open();

//        將通道註冊到通道選擇器中 要求:通道都必須是非阻塞的
//        第二個引數是我們需要通道選擇器幫我們管理什麼事件型別  註冊為接受就緒
        ssc.register(selector, SelectionKey.OP_ACCEPT);

//        遍歷通道選擇器
        while (true){
            System.out.println("我在8888等你......");
//            返回準備就緒的通道數量
            int nums = selector.select();
//            如果數量小於1說明沒有通道準備就緒 跳過本次迴圈
            if(nums<1) {continue;}

//            獲取所有的keys(通道 事件型別)
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();

            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                //處理nio事件
                if(key.isAcceptable()){
//                    獲取通道
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();

//                    註冊讀事件型別
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                if(key.isReadable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();

//                    讀取資料
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    int len = -1;
                    while (true){
                        byteBuffer.clear();
                        len = socketChannel.read(byteBuffer);
                        if(len == -1){
                            break;
                        }

                        byteBuffer.flip();
                        while (byteBuffer.hasRemaining()){
                            bos.write(byteBuffer.get());
                        }
                    }
//                    列印讀取到的資料
                    System.out.println(bos.toString());
                    //寫資料給客戶端 註冊事件型別 寫事件
                    socketChannel.register(selector,SelectionKey.OP_WRITE);
                }
                if(key.isWritable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    String msg = "你好,我是伺服器";
                    ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
                    byteBuffer.put(msg.getBytes());
                    byteBuffer.flip();
                    socketChannel.write(byteBuffer);
                    socketChannel.close();
                }
                iterator.remove();
            }
        }
    }
}
複製程式碼

客戶端程式設計

基本步驟
  1. 開啟SocketChannel
  2. 連線伺服器
  3. 寫資料給伺服器
  4. 讀取資料
  5. 關閉SocketChannel
程式碼
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOCleint {
    public static void main(String[] args) throws IOException {
//        建立sc
        SocketChannel socketChannel = SocketChannel.open();
//        連線伺服器
        socketChannel.connect(new InetSocketAddress("localhost",8888));

//        寫資料給伺服器
        String msg = "你好,我是客戶端";
        ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
        byteBuffer.put(msg.getBytes());
        byteBuffer.flip();
        socketChannel.write(byteBuffer);
//        關閉輸出流
        socketChannel.shutdownOutput();

//        讀取資料
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        ByteArrayOutputStream bosread = new ByteArrayOutputStream();
        int len = -1;
        while (true){
            readBuffer.clear();
            len = socketChannel.read(readBuffer);
            if(len == -1){
                break;
            }
            readBuffer.flip();
            while (readBuffer.hasRemaining()){

                bosread.write(readBuffer.get());
            }
        }

        System.out.println("我收到:"+bosread.toString());
        socketChannel.close();
    }
}
複製程式碼

我不能保證每一個地方都是對的,但是可以保證每一句話,每一行程式碼都是經過推敲和斟酌的。希望每一篇文章背後都是自己追求純粹技術人生的態度。

永遠相信美好的事情即將發生。

相關文章