一薛窥、簡介
1.1胖烛、Java 的 NIO眼姐,用非阻塞的 IO 方式∨宸可以用一個線程众旗,處理多個的客戶端連接,就會使用到Selector(選擇器)
1.2趟畏、Selector 能夠檢測多個注冊的通道上是否有事件發(fā)生(注意:多個Channel以事件的方式可以注冊到同一個Selector)贡歧,如果有事件發(fā)生,便獲取事件然后針對每個事件進(jìn)行相應(yīng)的處理赋秀。這樣就可以只用一個單線程去管理多個通道利朵,也就是管理多個連接和請求。
1.3猎莲、只有在 連接/通道 真正有讀寫事件發(fā)生時绍弟,才會進(jìn)行讀寫,就大大地減少了系統(tǒng)開銷著洼,并且不必為每個連接都創(chuàng)建一個線程樟遣,不用去維護(hù)多個線程。
1.4身笤、避免了多線程之間的上下文切換導(dǎo)致的開銷豹悬。
Selector一般稱為選擇器 ,也稱多路復(fù)用器液荸,多條channel復(fù)用selector瞻佛。channe通過注冊到selector ,使selector對channel進(jìn)行監(jiān)聽娇钱,實現(xiàn)盡可能少的線程管理多個連接伤柄。減少了 線程的使用涡尘,降低了因為線程的切換引起的不必要額資源浪費(fèi)和多余的開銷。
也是網(wǎng)絡(luò)傳輸非堵塞的核心組件响迂。
二考抄、特點(diǎn)
2.1、Netty 的 IO 線程 NioEventLoop 聚合了 Selector(選擇器蔗彤,也叫多路復(fù)用器)川梅,可以同時并發(fā)處理成百上千個客戶端連接。
2.2然遏、當(dāng)線程從某客戶端 Socket 通道進(jìn)行讀寫數(shù)據(jù)時贫途,若沒有數(shù)據(jù)可用時,該線程可以進(jìn)行其他任務(wù)待侵。
2.3丢早、線程通常將非阻塞 IO 的空閑時間用于在其他通道上執(zhí)行 IO 操作,所以單獨(dú)的線程可以管理多個輸入和輸出通道秧倾。
2.4怨酝、由于讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運(yùn)行效率那先,避免由于頻繁 I/O 阻塞導(dǎo)致的線程掛起农猬。
2.5、一個 I/O 線程可以并發(fā)處理 N 個客戶端連接和讀寫操作售淡,這從根本上解決了傳統(tǒng)同步阻塞 I/O 一連接一線程模型斤葱,架構(gòu)的性能、彈性伸縮能力和可靠性都得到了極大的提升揖闸。
三揍堕、Selector的作用
選擇器提供選擇執(zhí)行已經(jīng)就緒的任務(wù)的能力。從底層來看汤纸,Selector提供了詢問通道是否已經(jīng)準(zhǔn)備好執(zhí)行每個I/O操作的能力衩茸。Selector 允許單線程處理多個Channel。僅用單個線程來處理多個Channels的好處是蹲嚣,只需要更少的線程來處理通道递瑰。事實上,可以只用一個線程處理所有的通道隙畜,這樣會大量的減少線程之間上下文切換的開銷抖部。
四、Selector類相關(guān)方法
Selector 類是一個抽象類, 常用方法和說明如下:
public abstract class Selector implements Closeable {
public static Selector open();//得到一個選擇器對象
public abstract int select(long timeout);//監(jiān)控所有注冊的通道议惰,當(dāng)其中有 IO 操作可以進(jìn)行時慎颗,將
//對應(yīng)的 SelectionKey 加入到內(nèi)部集合中并返回,參數(shù)用來設(shè)置超時時間
public abstract Set<SelectionKey> selectedKeys();//從內(nèi)部集合中得到所有的 SelectionKey
}
注意事項:
1、NIO中的 ServerSocketChannel功能類似ServerSocket俯萎,SocketChannel功能類似Socket傲宜。
-
2、selector select()方法詳解
select()方法夫啊,可以查詢出已經(jīng)就緒的通道操作函卒,這些就緒的狀態(tài)集合,保存在一個元素是SelectionKey對象的Set集合中撇眯。- selector.select()//阻塞
- selector.select(1000);//阻塞1000毫秒报嵌,在1000毫秒后返回
- selector.selectNow();//不阻塞,立馬返還
- selector.wakeup();//喚醒selector
select()方法返回的int值熊榛,表示有多少通道已經(jīng)就緒锚国,更準(zhǔn)確的說,是自前一次select方法以來到這一次select方法之間的時間段上玄坦,有多少通道變成就緒狀態(tài)血筑。
NIO 非阻塞 網(wǎng)絡(luò)編程原理分析圖
NIO 非阻塞 網(wǎng)絡(luò)編程相關(guān)的(Selector、SelectionKey煎楣、ServerScoketChannel和SocketChannel) 關(guān)系梳理圖
1豺总、當(dāng)客戶端連接時,會通過ServerSocketChannel 得到 SocketChannel转质。
2园欣、Selector 進(jìn)行監(jiān)聽 select 方法, 返回有事件發(fā)生的通道的個數(shù)帖世。
3休蟹、將socketChannel注冊到Selector上,register(Selector sel日矫,int ops)赂弓,一個selector上可以注冊多個SocketChannel。
4哪轿、注冊后返回一個 SelectionKey, 會和該Selector 關(guān)聯(lián)(集合)盈魁。
5、進(jìn)一步得到各個 SelectionKey (有事件發(fā)生)窃诉。
6杨耙、在通過 SelectionKey 反向獲取 SocketChannel,方法 channel()飘痛。
7珊膜、可以通過 得到的 channel,完成業(yè)務(wù)處理宣脉。
五漆枚、可選擇通道(SelectableChannel)
并不是所有的Channel呻袭,都是可以被Selector 復(fù)用的蚀同。比方說阻课,F(xiàn)ileChannel就不能被選擇器復(fù)用。
判斷一個Channel 能被Selector 復(fù)用习寸,有一個前提:判斷他是否繼承了一個抽象類SelectableChannel。如果繼承了SelectableChannel,則可以被復(fù)用感憾,否則不能。
SelectableChannel類提供了實現(xiàn)通道的可選擇性所需要的公共方法令花。它是所有支持就緒檢查的通道類的父類吹菱。所有socket通道,都繼承了SelectableChannel類都是可選擇的彭则,包括從管道(Pipe)對象的中獲得的通道鳍刷。而FileChannel類,沒有繼承SelectableChannel俯抖,因此是不是可選通道输瓜。
通道和選擇器注冊之后,他們是綁定的關(guān)系嗎芬萍?
不是一對一的關(guān)系尤揣。一個通道可以被注冊到多個選擇器上,但對每個選擇器而言只能被注冊一次柬祠。
通道和選擇器之間的關(guān)系北戏,使用注冊的方式完成。SelectableChannel可以被注冊到Selector對象上漫蛔,在注冊的時候嗜愈,需要指定通道的哪些操作,是Selector感興趣的莽龟。
Channel注冊到Selector
使用Channel.register(Selector sel蠕嫁,int ops)方法,將一個通道注冊到一個選擇器時毯盈。第一個參數(shù)剃毒,指定通道要注冊的選擇器是誰。第二個參數(shù)指定選擇器需要查詢的通道操作搂赋。
可以供選擇器查詢的通道操作赘阀,從類型來分,包括以下四種:
- 1脑奠、可讀 : SelectionKey.OP_READ
- 2基公、可寫 : SelectionKey.OP_WRITE
- 3、連接 : SelectionKey.OP_CONNECT
- 4捺信、接收 : SelectionKey.OP_ACCEPT
如果Selector對通道的多操作類型感興趣酌媒,可以用“位或”操作符來實現(xiàn):int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
注意欠痴,操作一詞,是一個是使用非常泛濫秒咨,也是一個容易混淆的詞喇辽。特別提醒的是,選擇器查詢的不是通道的操作雨席,而是通道的某個操作的一種就緒狀態(tài)菩咨。
一旦通道具備完成某個操作的條件,表示該通道的某個操作已經(jīng)就緒陡厘,就可以被Selector查詢到抽米,程序可以對通道進(jìn)行對應(yīng)的操作。比方說糙置,某個SocketChannel通道可以連接到一個服務(wù)器云茸,則處于“連接就緒”(OP_CONNECT)。再比方說谤饭,一個ServerSocketChannel服務(wù)器通道準(zhǔn)備好接收新進(jìn)入的連接标捺,則處于“接收就緒”(OP_ACCEPT)狀態(tài)。還比方說揉抵,一個有數(shù)據(jù)可讀的通道亡容,可以說是“讀就緒”(OP_READ)。一個等待寫數(shù)據(jù)的通道可以說是“寫就緒”(OP_WRITE)冤今。
六闺兢、選擇鍵(SelectionKey)
Channel和Selector的關(guān)系確定好后,并且一旦通道處于某種就緒的狀態(tài)戏罢,就可以被選擇器查詢到屋谭。這個工作,使用選擇器Selector的select()方法完成帖汞。select方法的作用戴而,對感興趣的通道操作,進(jìn)行就緒狀態(tài)的查詢翩蘸。
Selector可以不斷的查詢Channel中發(fā)生的操作的就緒狀態(tài)。并且挑選感興趣的操作就緒狀態(tài)淮逊。一旦通道有操作的就緒狀態(tài)達(dá)成催首,并且是Selector感興趣的操作,就會被Selector選中泄鹏,放入選擇鍵集合中郎任。
一個選擇鍵,首先是包含了注冊在Selector的通道操作的類型备籽,比方說SelectionKey.OP_READ舶治。也包含了特定的通道與特定的選擇器之間的注冊關(guān)系分井。
開發(fā)應(yīng)用程序是,選擇鍵是編程的關(guān)鍵霉猛。NIO的編程尺锚,就是根據(jù)對應(yīng)的選擇鍵,進(jìn)行不同的業(yè)務(wù)邏輯處理惜浅。
選擇鍵的概念瘫辩,有點(diǎn)兒像事件的概念。
一個選擇鍵有點(diǎn)兒像監(jiān)聽器模式里邊的一個事件坛悉,但是又不是伐厌。由于Selector不是事件觸發(fā)的模式,而是主動去查詢的模式裸影,所以不叫事件Event挣轨,而是叫SelectionKey選擇鍵。
七轩猩、Selector的使用流程
7.1刃唐、創(chuàng)建Selector
Selector對象是通過調(diào)用靜態(tài)工廠方法open()來實例化的,如下:
// 1界轩、獲取Selector選擇器
Selector selector = Selector.open();
Selector的類方法open()內(nèi)部是向SPI發(fā)出請求画饥,通過默認(rèn)的SelectorProvider對象獲取一個新的實例。
7.2浊猾、將Channel注冊到Selector
要實現(xiàn)Selector管理Channel抖甘,需要將channel注冊到相應(yīng)的Selector上,如下:
// 2葫慎、獲取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.設(shè)置為非阻塞
serverSocketChannel.configureBlocking(false);
// 4衔彻、綁定連接
serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));
// 5、將通道注冊到選擇器上,并制定監(jiān)聽事件為:“接收”事件
serverSocketChannel.register(selector偷办,SelectionKey.OP_ACCEPT);
上面通過調(diào)用通道的register()方法會將它注冊到一個選擇器上艰额。
首先需要注意的是:
- 與Selector一起使用時,Channel必須處于非阻塞模式下椒涯,否則將拋出異常IllegalBlockingModeException柄沮。這意味著,F(xiàn)ileChannel不能與Selector一起使用废岂,因為FileChannel不能切換到非阻塞模式祖搓,而套接字相關(guān)的所有的通道都可以。
另外湖苞,還需要注意的是:
- 一個通道拯欧,并沒有一定要支持所有的四種操作。比如服務(wù)器通道ServerSocketChannel支持Accept 接受操作财骨,而SocketChannel客戶端通道則不支持镐作〔亟悖可以通過通道上的validOps()方法,來獲取特定通道下所有支持的操作集合该贾。
7.3羔杨、輪詢查詢就緒操作
通過Selector的select()方法,可以查詢出已經(jīng)就緒的通道操作靶庙,這些就緒的狀態(tài)集合问畅,包存在一個元素是SelectionKey對象的Set集合中。
下面是Selector幾個重載的查詢select()方法:
- 1六荒、select():阻塞到至少有一個通道在你注冊的事件上就緒了护姆。
- 2、select(long timeout):和select()一樣掏击,但最長阻塞事件為timeout毫秒卵皂。
- 3、selectNow():非阻塞砚亭,只要有通道就緒就立刻返回灯变。
select()方法返回的int值,表示有多少通道已經(jīng)就緒捅膘,更準(zhǔn)確的說添祸,是自前一次select方法以來到這一次select方法之間的時間段上,有多少通道變成就緒狀態(tài)寻仗。
一旦調(diào)用select()方法刃泌,并且返回值不為0時,通過調(diào)用Selector的selectedKeys()方法來訪問已選擇鍵集合署尤,然后迭代集合的每一個選擇鍵元素耙替,根據(jù)就緒操作的類型,完成對應(yīng)的操作:
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
處理完成后曹体,直接將選擇鍵俗扇,從這個集合中移除,防止下一次循環(huán)的時候箕别,被重復(fù)的處理铜幽。鍵可以但不能添加。試圖向已選擇的鍵的集合中添加元素將拋出java.lang.UnsupportedOperationException究孕。
八啥酱、一個NIO 編程的簡單實例
8.1、服務(wù)端:
/**
* @Description: 服務(wù)端接收客戶端傳來的數(shù)據(jù)
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
//創(chuàng)建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一個Selecor對象
Selector selector = Selector.open();
//綁定一個端口6666, 在服務(wù)器端監(jiān)聽
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//設(shè)置為非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注冊到 selector 關(guān)心 事件為 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 用輪詢的方式厨诸,查詢獲取“準(zhǔn)備就緒”的注冊過的操作
while (true){
//這里我們等待1秒,如果沒有事件發(fā)生, 返回
if(selector.select(1000) == 0) { //沒有事件發(fā)生
System.out.println("服務(wù)器等待了1秒禾酱,無連接");
continue;
}
//如果返回的>0, 就獲取到相關(guān)的 selectionKey集合
//1.如果返回的>0微酬, 表示已經(jīng)獲取到關(guān)注的事件
//2. selector.selectedKeys() 返回關(guān)注事件的集合
// 通過 selectionKeys 反向獲取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍歷 Set<SelectionKey>, 使用迭代器遍歷
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
//獲取到SelectionKey
SelectionKey key = iterator.next();
//處理key時绘趋,需要從selectionKeys集合中刪除,否則下次處理就會有問題
iterator.remove();
//根據(jù)key 對應(yīng)的通道發(fā)生的事件做相應(yīng)處理
if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客戶端連接
//該該客戶端生成一個 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客戶端連接成功 生成了一個 socketChannel " + socketChannel.hashCode());
//將 SocketChannel 設(shè)置為非阻塞
socketChannel.configureBlocking(false);
//將socketChannel 注冊到selector, 關(guān)注事件為 OP_READ颗管, 同時給socketChannel
//關(guān)聯(lián)一個Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客戶端連接后 陷遮,注冊的selectionkey 數(shù)量=" + selector.keys().size()); //2,3,4..
}
if(key.isReadable()) { //發(fā)生 OP_READ
try {
//通過key 反向獲取到對應(yīng)channel
SocketChannel channel = (SocketChannel)key.channel();
//獲取到該channel關(guān)聯(lián)的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
//如果是正常斷開,read方法返回值是-1
int read = channel.read(buffer);
if(read == -1){
key.cancel();
}else {
System.out.println("form 客戶端 " + new String(buffer.array()));
}
} catch (IOException e) {
e.printStackTrace();
//因為客戶端斷開了,因此需要將key取消(從selector的keys集合中真正刪除key)
key.cancel();
}
}
}
}
}
}
8.2垦江、客戶端:
public class NIOClient2 {
public static void main(String[] args) throws IOException {
//得到一個網(wǎng)絡(luò)通道
SocketChannel socketChannel = SocketChannel.open();
//設(shè)置非阻塞
socketChannel.configureBlocking(false);
//提供服務(wù)器端的ip 和 端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("217.0.0.1", 6666);
//連接服務(wù)器
//socketChannel.connect(inetSocketAddress);
//連接服務(wù)器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因為連接需要時間帽馋,客戶端不會阻塞,可以做其它工作..");
}
}
//...如果連接成功比吭,就發(fā)送數(shù)據(jù)
String str = "hello, world";
//Wraps a byte array into a buffer
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//發(fā)送數(shù)據(jù)绽族,將 buffer 數(shù)據(jù)寫入 channel
socketChannel.write(buffer);
//完畢時,清除緩沖區(qū)內(nèi)容
buffer.clear();
//關(guān)閉相關(guān)流
socketChannel.close();
}
}
九衩藤、SelectionKey
9.1吧慢、SelectionKey,表示 Selector 和網(wǎng)絡(luò)通道的注冊關(guān)系, 共四種:
- int OP_ACCEPT:有新的網(wǎng)絡(luò)連接可以 accept赏表,值為 16
- int OP_CONNECT:代表連接已經(jīng)建立检诗,值為 8
- int OP_READ:代表讀操作,值為 1
- int OP_WRITE:代表寫操作瓢剿,值為 4
源碼中:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
9.2逢慌、SelectionKey相關(guān)方法
public abstract class SelectionKey {
public abstract Selector selector();//得到與之關(guān)聯(lián)的 Selector 對象
public abstract SelectableChannel channel();//得到與之關(guān)聯(lián)的通道
public final Object attachment();//得到與之關(guān)聯(lián)的共享數(shù)據(jù)
public abstract SelectionKey interestOps(int ops);//設(shè)置或改變監(jiān)聽事件
public final boolean isAcceptable();//是否可以 accept
public final boolean isReadable();//是否可以讀
public final boolean isWritable();//是否可以寫
}
十、ServerSocketChannel
10.1间狂、ServerSocketChannel 在服務(wù)器端監(jiān)聽新的客戶端 Socket 連接
10.2攻泼、相關(guān)方法如下
public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{
public static ServerSocketChannel open();得到一個 ServerSocketChannel 通道
public final ServerSocketChannel bind(SocketAddress local);設(shè)置服務(wù)器端端口號
public final SelectableChannel configureBlocking(boolean block);設(shè)置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public SocketChannel accept();接受一個連接前标,返回代表這個連接的通道對象
public final SelectionKey register(Selector sel, int ops);注冊一個選擇器并設(shè)置監(jiān)聽事件
}
十一坠韩、SocketChannel
10.1、SocketChannel炼列,網(wǎng)絡(luò) IO 通道只搁,具體負(fù)責(zé)進(jìn)行讀寫操作。NIO 把緩沖區(qū)的數(shù)據(jù)寫入通道俭尖,或者把通道里的數(shù)據(jù)讀到緩沖區(qū)氢惋。
10.2、相關(guān)方法如下
public abstract class SocketChannel extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{
public static SocketChannel open();//得到一個 SocketChannel 通道
public final SelectableChannel configureBlocking(boolean block);//設(shè)置阻塞或非阻塞模式稽犁,取值 false 表示采用非阻塞模式
public boolean connect(SocketAddress remote);//連接服務(wù)器
public boolean finishConnect();//如果上面的方法連接失敗焰望,接下來就要通過該方法完成連接操作
public int write(ByteBuffer src);//往通道里寫數(shù)據(jù)
public int read(ByteBuffer dst);//從通道里讀數(shù)據(jù)
public final SelectionKey register(Selector sel, int ops, Object att);//注冊一個選擇器并設(shè)置監(jiān)聽事件,最后一個參數(shù)可以設(shè)置共享數(shù)據(jù)
public final void close();//關(guān)閉通道
}
NIO編程小結(jié)
NIO編程的難度比同步阻塞BIO大很多已亥。
請注意以上的代碼中并沒有考慮“半包讀”和“半包寫”熊赖,如果加上這些,代碼將會更加復(fù)雜虑椎。
1震鹉、客戶端發(fā)起的連接操作是異步的俱笛,可以通過在多路復(fù)用器注冊O(shè)P_CONNECT等待后續(xù)結(jié)果,不需要像之前的客戶端那樣被同步阻塞传趾。
2迎膜、SocketChannel的讀寫操作都是異步的,如果沒有可讀寫的數(shù)據(jù)它不會同步等待浆兰,直接返回磕仅,這樣I/O通信線程就可以處理其他的鏈路,不需要同步等待這個鏈路可用簸呈。
3榕订、線程模型的優(yōu)化:由于JDK的Selector在Linux等主流操作系統(tǒng)上通過epoll實現(xiàn),它沒有連接句柄數(shù)的限制(只受限于操作系統(tǒng)的最大句柄數(shù)或者對單個進(jìn)程的句柄限制)蝶棋,這意味著一個Selector線程可以同時處理成千上萬個客戶端連接卸亮,而且性能不會隨著客戶端的增加而線性下降。因此玩裙,它非常適合做高性能兼贸、高負(fù)載的網(wǎng)絡(luò)服務(wù)器。
參考:
https://www.cnblogs.com/crazymakercircle/p/9826906.html
https://www.cnblogs.com/CllOVER/p/13441282.html
https://www.cnblogs.com/snailclimb/p/9086334.html
https://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-selector.html