Netty雜記2—NIO網絡編程

前言

在上篇文章中對BIO網絡編程的相關內容進行了講解,經過咱們一步一步的優化,雖然咱們經過多線程解決了併發訪問的問題,可是BIO自己的一些特性形成的問題卻沒有獲得解決。java

BIO是阻塞IO,咱們使用線程來進行IO的調度,咱們沒法肯定io是否就緒,可是每一個IO操做都會建立線程,這個時候若是IO未就緒,那麼建立的線程也會處於阻塞狀態。git

在以前講解NIO基本知識的時候咱們提到過NIO經過通道選擇器能夠實現同時對多個通道的管理,實際上就是經過管理多個IO操做,換句話說是單線程處理多線程併發,有效的防止線程由於IO沒有就緒而被掛起。github

在使用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. 建立Selectorsocket

//經過調用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();
    }
}
複製代碼

我不能保證每個地方都是對的,可是能夠保證每一句話,每一行代碼都是通過推敲和斟酌的。但願每一篇文章背後都是本身追求純粹技術人生的態度。

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

相關文章
相關標籤/搜索