Netty學(xué)習(xí)之理解selector原理示例
BIO的弊端
BIO既是Blocking IO,也叫同步阻塞模型,BIO模型如下
如果所示,多個(gè)客戶端連接一個(gè)服務(wù)端, 每出現(xiàn)一個(gè)客戶端就開一個(gè)handler(一般對(duì)應(yīng)一個(gè)線程)處理
對(duì)應(yīng)的服務(wù)端代碼如下
public class BioServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9000); // 任務(wù)處理線程池 ExecutorService pool = Executors.newFixedThreadPool(10); while (true) { System.out.println("server start"); //阻塞方法等待客戶端連接 Socket clientSocket = serverSocket.accept(); System.out.println("new client"); pool.execute(()->{ handler(clientSocket); }); } } private static void handler(Socket clientSocket) { try { byte[] bytes = new byte[1024]; System.out.println("reading"); //接收客戶端的數(shù)據(jù),阻塞方法,沒(méi)有數(shù)據(jù)可讀時(shí)就阻塞 int read = clientSocket.getInputStream().read(bytes); System.out.println("read over"); if (read != -1) { System.out.println("received data:" + new String(bytes, 0, read)); } clientSocket.getOutputStream().write("HelloClient".getBytes()); clientSocket.getOutputStream().flush(); } catch (Exception e) { } } }
上面的代碼使用了線程池來(lái)處理客戶端業(yè)務(wù),整個(gè)代碼有兩步阻塞
- 一步主線程阻塞等待客戶端連接
- 一步線程池的線程阻塞等待客戶端傳輸數(shù)據(jù)準(zhǔn)備好
其實(shí)第一步阻塞到?jīng)]什么,作為一個(gè)服務(wù)沒(méi)有客戶的情況下不阻塞也無(wú)事可做,主要是第二個(gè)阻塞,上面代碼線程池size是10,假如當(dāng)前有10個(gè)客戶端連接都不發(fā)送數(shù)據(jù),那么10個(gè)線程都阻塞,此時(shí)再來(lái)一個(gè)客戶端發(fā)送完數(shù)據(jù)也無(wú)法及時(shí)處理
假設(shè)現(xiàn)在有個(gè)澡堂子,一共雇傭10個(gè)搓澡工,BIO就好比來(lái)一個(gè)客人就分配一個(gè)搓澡師傅,這個(gè)師傅就死死盯著客人洗澡,啥時(shí)候洗澡完了就開始安排搓澡,如果十個(gè)客戶在池子里洗澡洗不完,這時(shí)有11號(hào)客戶來(lái)了,沒(méi)有空閑的搓澡師傅(都在等待客戶洗完澡),那這個(gè)客戶只能傻傻的等著,實(shí)際上十個(gè)搓澡工都沒(méi)有實(shí)際干活,但卻處于傻傻的等待不可用狀態(tài)
解決思路
通過(guò)上面的例子可以看出,BIO的模型明顯是反人類的,合理的工作流程是這樣
10個(gè)搓澡師傅都待著,什么時(shí)候有人洗完澡了才分配搓澡師傅進(jìn)行搓澡,這樣搓澡師傅的工作更加合理,充分的壓榨了搓澡師傅
客戶只會(huì)因?yàn)榇暝鑾煾刀荚诖暝瓒却?,而不是因?yàn)榇暝鑾煾刀荚谏档榷却?/p>
這樣設(shè)計(jì)顯然更加合理,也更加符合現(xiàn)實(shí)的工作流程,相當(dāng)于事件驅(qū)動(dòng),當(dāng)發(fā)生洗完澡
(接收到數(shù)據(jù))事件后,再安排搓澡工(線程)進(jìn)行處理
回到代碼怎么才能做到以事件驅(qū)動(dòng)吶?更確切的說(shuō),作為java代碼,如何得知數(shù)據(jù)傳輸?shù)氖录l(fā)生吶?
顯然java本身肯定是做不到的,我們可以想一下數(shù)據(jù)的來(lái)源,一個(gè)網(wǎng)絡(luò)數(shù)據(jù)的傳輸,一臺(tái)機(jī)器通過(guò)網(wǎng)線或無(wú)線等形式發(fā)送二進(jìn)制數(shù)據(jù)到一臺(tái)電腦,硬件的驅(qū)動(dòng)必然能感知到,那么操作系統(tǒng)也一定能感知到,所以一個(gè)鏈接是否有數(shù)據(jù)傳輸,操作系統(tǒng)最知道!
epoll
linux 提供了epoll系列函數(shù)
上層代碼可以通過(guò)調(diào)用該系列函數(shù)訂閱感興趣的事件,后續(xù)即可感知到注冊(cè)事件的發(fā)生,主要方法如下
- epoll_create: 創(chuàng)建一個(gè)epoll實(shí)例
- epoll_ctl: 訂閱事件
- epoll_wait: 阻塞等待訂閱事件的發(fā)生
有個(gè)這系列函數(shù),java代碼就可以:
- 調(diào)用epoll_create創(chuàng)建一個(gè)epoll實(shí)例
- 調(diào)用epoll_ctl訂閱可讀事件(相當(dāng)于有數(shù)據(jù)傳輸事件)
- 調(diào)用epoll_wait阻塞并等待時(shí)間發(fā)生
當(dāng)然不能直接調(diào)用,而是調(diào)用底層C++再調(diào)用linux函數(shù)
NIO
有了操作系統(tǒng)的支持,java就可以實(shí)現(xiàn)一個(gè)不阻塞的IO服務(wù),這就是NIO模型(Non Blocking IO)
JDK1.4開始引入java.nio包,在linux系統(tǒng)中底層就是使用epoll實(shí)現(xiàn)的(windows中基于winsock2,不開源)
先看一下NIO實(shí)現(xiàn)服務(wù)的代碼
public class NioServer { public static void main(String[] args) throws IOException, InterruptedException { // 創(chuàng)建NIO ServerSocketChannel ServerSocketChannel serverSocket = ServerSocketChannel.open(); serverSocket.socket().bind(new InetSocketAddress(9000)); serverSocket.configureBlocking(false); // 打開Selector處理Channel,底層調(diào)用epoll_create Selector selector = Selector.open(); // 把ServerSocketChannel注冊(cè)到selector上,并且selector對(duì)客戶端accept連接操作感興趣,底層調(diào)用epoll_ctl serverSocket.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服務(wù)啟動(dòng)成功"); while (true) { // 阻塞等待需要處理的事件發(fā)生,即調(diào)用epoll_wait selector.select(); // 獲取selector中注冊(cè)的全部事件的 SelectionKey 實(shí)例 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); // 遍歷SelectionKey對(duì)事件進(jìn)行處理 while (iterator.hasNext()) { SelectionKey key = iterator.next(); // 如果是OP_ACCEPT事件,則進(jìn)行連接獲取和事件注冊(cè) if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = server.accept(); socketChannel.configureBlocking(false); // 這里只注冊(cè)了讀事件,如果需要給客戶端發(fā)送數(shù)據(jù)可以注冊(cè)寫事件,底層調(diào)用epoll_ctl socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("客戶端連接成功"); } else if (key.isReadable()) { // 如果是OP_READ事件,則進(jìn)行讀取和打印 SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(128); int len = socketChannel.read(byteBuffer); // 如果有數(shù)據(jù),把數(shù)據(jù)打印出來(lái) if (len > 0) { System.out.println("接收到消息:" + new String(byteBuffer.array())); } else if (len == -1) { // 如果客戶端斷開連接,關(guān)閉Socket System.out.println("客戶端斷開連接"); socketChannel.close(); } } //從事件集合里刪除本次處理的key,防止下次select重復(fù)處理 iterator.remove(); } } } }
上面及是一個(gè)典型的NIO代碼,其中(liunx中):
- Selector.open() 底層調(diào)用liunx的
epoll_create
- socketChannel.register(selector, SelectionKey.OP_READ)相當(dāng)于調(diào)用liunx的
epoll_ctl
(實(shí)際上只是緩存起來(lái),下一步再真正執(zhí)行epoll_ctl) - selector.select() 底層調(diào)用
epoll_wait
,阻塞并等待訂閱事件發(fā)生
總結(jié)
selector 是java.nio包中一個(gè)nio解決方案,主要可以實(shí)現(xiàn)
- 訂閱IO事件,如連接事件和有數(shù)據(jù)傳輸事件
- 阻塞并等待任何訂閱事件的發(fā)生
在linux內(nèi)核中,selector就是對(duì)epoll函數(shù)的一種封裝,或者說(shuō)epoll是linux針對(duì)java selector的實(shí)現(xiàn)
以上就是Netty學(xué)習(xí)之理解selector原理示例的詳細(xì)內(nèi)容,更多關(guān)于Netty selector原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java開發(fā)學(xué)習(xí) Eclipse項(xiàng)目有紅感嘆號(hào)解決之道
這篇文章主要為大家詳細(xì)介紹了完美解決Eclipse項(xiàng)目有紅感嘆號(hào)問(wèn)題的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04為什么Spring官方推薦的@Transational還能導(dǎo)致生產(chǎn)事故
在Spring中進(jìn)行事務(wù)管理非常簡(jiǎn)單,只需要在方法上加上注解@Transactional,那么為什么Spring官方推薦的@Transational還能導(dǎo)致生產(chǎn)事故,本文就詳細(xì)的介紹一下2021-11-11Java實(shí)現(xiàn)從jar包中讀取指定文件的方法
這篇文章主要介紹了Java實(shí)現(xiàn)從jar包中讀取指定文件的方法,涉及java針對(duì)jar文件的讀取及查找相關(guān)操作技巧,需要的朋友可以參考下2017-08-08Java接口操作(繼承父類并實(shí)現(xiàn)多個(gè)接口)
這篇文章主要介紹了Java接口操作(繼承父類并實(shí)現(xiàn)多個(gè)接口),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10Sharding Jdbc批量操作引發(fā)fullGC解決
這篇文章主要為大家介紹了Sharding Jdbc批量操作引發(fā)fullGC解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11