概述
在NIO網絡程式設計中,Selector&Channel&Buffer三者的關系是十分緊密的,Buffer從Channel中讀寫,Channel注冊在Selector中。在以往的網絡程式設計中,通常都是通過建立一個線程來維護一個socket通訊,在業務量較小時,是可以很好的完成工作的,但是一旦用戶端增多,建立的線程也随之增多,對硬體的開銷是非常大的。這時候NIO的Selector就展現出了價值:
Selector在Java NIO中可以檢測到一個或者多個Channel,并能夠知曉通道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的線程可以管理多個Channel,進而管理多個網絡連接配接。這樣的單個線程管理管理多個Channel可以極大的減少線程間切換的開銷。
示例
package com.leolee.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
import java.util.Set;
/**
* @ClassName SelectorTest
* @Description: NIO socket程式設計demo,用于了解Selector
* @Author LeoLee
* @Date 2020/9/22
* @Version V1.0
**/
public class SelectorTest {
//端口數組,用于和多個用戶端建立連接配接後配置設定端口
int[] ports = null;
//起始端口
int tempPort = 5000;
//構造器初始化 端口數組ports,并從起始端口tempPort開始配置設定[size]個端口号
public SelectorTest (int size) {
this.ports = new int[size];
for (int i = 0; i < size; i++) {
this.ports[i] = tempPort + i;
}
}
public void selectorTest () throws IOException {
Selector selector = Selector.open();
//windows系統下是sun.nio.ch.WindowsSelectorProvider,如果是linux系統,則是KQueueSelectorProvider
//由于Selector.open()的源碼涉及 sun 包下的代碼,是非開源代碼,具體實作不得而知
// System.out.println(SelectorProvider.provider().getClass());//sun.nio.ch.WindowsSelectorProvider
// System.out.println(sun.nio.ch.DefaultSelectorProvider.create().getClass());//sun.nio.ch.WindowsSelectorProvider
for (int i = 0; i < ports.length; i++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);//非阻塞模式
ServerSocket serverSocket = serverSocketChannel.socket();
//綁定端口
InetSocketAddress address = new InetSocketAddress("127.0.0.1", ports[i]);
serverSocket.bind(address);
//注冊selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("[step1]監聽端口:" + ports[i]);
}
//阻塞代碼,始終監聽來自用戶端的連接配接請求
while (true) {
//擷取我們“感興趣的時間”已經準備好的通道,上面代碼感興趣的是SelectionKey.OP_ACCEPT,這裡擷取的就是SelectionKey.OP_ACCEPT事情類型準備好的通道
//number為該“感興趣的事件“的通道數量
int number = selector.select();
System.out.println("number:" + number);
if (number > 0) {
//由于selector中會有多個通道同時準備好,是以這裡selector.selectedKeys()傳回的是一個set集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("[step2]selectionKeys:" + selectionKeys);
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//由于我們”感興趣“的是SelectionKey.OP_ACCEPT,是以如下判斷
if (selectionKey.isAcceptable()) {
//selectionKey.channel()傳回是ServerSocketChannel的爺爺類SelectableChannel,是以做強制類型轉換
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);//非阻塞模式
//重點重點重點重點重點重點重點重點重點重點
//将接收到的channel同樣也注冊到Selector上,Selector<--->channel<--->buffer,三者是雙向的
socketChannel.register(selector, SelectionKey.OP_READ);//這時候”感興趣的事件“是讀操作,因為要接收用戶端的資料了
//重點重點重點重點重點重點重點重點重點重點
//當以上代碼執行完畢後,已經建立了服務端與用戶端的socket連接配接,這時候就要移除Set集合中的selectionKey,以免之後重複建立該selectionKey對應的通道
iterator.remove();
System.out.println("[step3]成功擷取用戶端的連接配接:" + socketChannel);
} else if (selectionKey.isReadable()) {//判斷selectionKey可讀狀态
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int byteRead = 0;
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
byteBuffer.clear();
int read = socketChannel.read(byteBuffer);
//判斷資料是否讀完
if (read <= 0) {
socketChannel.register(selector, SelectionKey.OP_READ);
break;
}
//寫回資料,這裡為了簡單:讀取什麼資料,就寫回什麼資料
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteRead += read;
}
System.out.println("[step4]讀取:" + byteRead + ",來自與:" + socketChannel);
//重點重點重點重點重點重點重點重點重點重點
//當以上代碼執行完畢後,已經完成了對某一個已經“讀準備好”通道的讀寫操作,這時候就要移除Set集合中的selectionKey,以免之後重複讀寫該selectionKey對應的通道
iterator.remove();
}
}
}
}
}
/*
* 功能描述: <br> 使用nc指令連接配接服務端:nc 127.0.0.1 5000
* 〈〉
* @Param: [args]
* @Return: void
* @Author: LeoLee
* @Date: 2020/9/23 12:59
*/
public static void main(String[] args) throws IOException {
SelectorTest selectorTest = new SelectorTest(5);
selectorTest.selectorTest();
}
}
基本思路:
- 通過構造方法定義5個監聽端口
- 建立Selector,并将已經初始化完成的ServerSocketChannel注冊在Selector上,Selector開始監聽Channel
- 構造while死循環(阻塞代碼),始終監聽來自用戶端的請求,通過判斷Selector注冊通道之後傳回的SelectionKey集合中每一個SelectionKey狀态,來處理不同的操作(建立連接配接、讀、寫)
”感興趣的事件“是一個需要特别注意的概念:
主要分為四種,在SelectionKey類中定義為了四個常量
- Connect
- Accept
- Read
- Write
Channel向Selector注冊的時候都要給定一個 int 類型的 參數 [ops],代表了監聽“感興趣”的通道類型,說人話就是之後傳回的SelectionKey的狀态。可以是複合狀态:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
這是通過一種事件的形式,當某些事件完成後,辨別了Channel的狀态随之改變,是以SelectionKey所代表的Channel的狀态也發生了改變,通過區分判斷不同的狀态,我們知道應該對這些Channel做對應的操作(建立連接配接、讀、寫)。
運作
運作服務端demo,服務端監聽了5個端口
[step1]監聽端口:5000
[step1]監聽端口:5001
[step1]監聽端口:5002
[step1]監聽端口:5003
[step1]監聽端口:5004
使用nc指令來連接配接服務端

服務端5000端口監聽到用戶端建立連接配接的請求并建立連接配接:
[step2]selectionKeys:[[email protected]]
[step3]成功擷取用戶端的連接配接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]
用戶端發送消息到服務端,當服務端收到消息後,将消息内容原封不動的傳回給了用戶端
[step2]selectionKeys:[[email protected]]
[step4]讀取:13,來自與:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]
PS.
可以嘗試多建立幾個用戶端,連接配接不同的端口來感受一下代碼思路
需要代碼的來這裡拿嗷:demo項目位址