目錄
一、阻塞 & 非阻塞
1、阻塞
2、非阻塞
二、selector
1、連接和讀取
2、處理客戶端斷開
3、處理消息的邊界
4、ByteBuffer大小分配
三、多線程優(yōu)化
四、NIO vs BIO
1、stream vs channnel
2、IO模型
阻塞IO
非阻塞IO
多路復用
異步IO模型
一、阻塞 & 非阻塞
1、阻塞
服務器端的代碼
然后創(chuàng)建客戶端,直接連服務器端的代碼就可以了
運行發(fā)現服務端線程執(zhí)行到accept這個方法后就停止運行了,這個方法是阻塞的,要等連接建立完成之后才能繼續(xù)運行;建立完連接后走到read方法又會阻塞等待讀入數據,讀到數據之后才能恢復運行。
當有多個客戶端的時候,服務端在執(zhí)行到一個客戶端的堵塞方法的時候會一直等待,其他客戶端就不會被處理。?
2、非阻塞
我們可以把ServerSocoketChannel和SocketChannel改成非阻塞模式,這樣如果沒有連接直接返回null,沒有讀取到數據就返回0,不會去堵塞等待了。
但是這樣也會有缺點,因為線程是一直循環(huán)檢查有沒有鏈接和數據的,如果一直沒有連接和數據來,線程依然會不斷循環(huán),太過繁忙了,所以這種非阻塞模式在開發(fā)中也不常用。
二、selector
為了解決上面非堵塞的不斷循環(huán)問題,我們就引入selector,接下里看看selector的使用
1、連接和讀取
用的時候我們要先調用靜態(tài)open方法創(chuàng)建Selector對象,然后來管理這些channel,channel調用register方法注冊到selector,返回值是個SelectionKey對象,?SelectionKey就是將來事件發(fā)生后,通過它可以知道事件是哪個channel的事件,然后調用interstOps設置對哪個事件感興趣
channel事件有4中類型:
- accept-有連接請求的時候觸發(fā)
- connect-客戶端連接建立后觸發(fā)
- read-數據可讀時
- write-可寫事件
然后監(jiān)聽的時候用select()方法,沒有時間發(fā)生的時候線程堵塞,有事件才執(zhí)行,然后遍歷集合調用selectedKey方法處理事件。
如果我們遍歷出來不對事件處理,那么下次遍歷的時候還會有,也就是說如果有未處理事件,那么是不會阻塞的,如果不想處理的話,我們可以用cannel()方法來取消處理
socket處理的時候要想不去不斷循環(huán)讀取,那么也要交給selector管理,所以這個socketChannel也要調用register方法注冊到selector上,還有感興趣關注的事件
這里要把監(jiān)聽到的事件類型分開處理
這個代碼在運行的時候缺空指針了,為什么呢?
selector的集合和selectedKeys集合中每次監(jiān)聽事件中的內容執(zhí)行完是不會自己刪除的,所以我們第一次執(zhí)行完第二次遍歷到有元素執(zhí)行,建立連接的時候卻沒有需要建立的返回null,這個時候就拋出空指針了,我們每次執(zhí)行完一個事件就得手動從容器刪除
2、處理客戶端斷開
我們客戶端和服務器連接之后,如果客戶端退出會拋出異常,如果我們直接try catch去抓的話會循環(huán),因為客戶端關閉會引發(fā)read事假,這個事假一直不處理就會一直執(zhí)行,所以我們必須在catch里面去把key調用cancel()方法取消處理事件
但是如果是正常斷開呢,還是會循環(huán),因為正常斷開執(zhí)行不到catch語句不會cancel,就會一直循環(huán)處理close這個read事件,所以我們要用read的返回值來區(qū)分是不是close指令,是就取消處理掉
3、處理消息的邊界
看看下面這種邊界問題:
我們用分割符的方法來解決邊界的問題,遇到分隔符就說明讀取結束了,我們就分割成一個消息
如果我們實際的內容超過了byteBuffer,一次沒有讀取完不會報錯,第二個會把剩下的部分發(fā)過來會當成一個完整的消息處理,所以當空間不夠的時候,我們需要擴容,而且我們不能把byteBuffer做為局部變量,要當共享的第一個和第二次共用一個byteBuffer。擴容我們是重新搞個更大的buffer2,然后第一次復制過去,第二次的直接讀過去。
1、我們先修正ByteBuffer不能為局部變量,但是我們不能直接放最外層,因為如果放最外層就變成多個socketChannel公用一個ByteBuffer,這樣就亂了,應該每個channel都有自己的byteBuffer,這里就要用到attachment附件,我們可以在注冊的時候把buffer當成附件(Object)一起注冊為SelectionKey里面。后面要用的時候直接用key調用attachment方法就能獲取到buffer
?2、擴容的時候就是比較如果壓縮后的長度和限制最大的長度相等,說明這個消息超過了要擴容,壓縮就是每次讀取完一個分割符后都要進行的,這個壓縮方法就是把因為讀取的去除
4、ByteBuffer大小分配
每個channel都需要記錄可能被分割的消息,因為byteBuffer不能被多個channel共同使用,因此需要每個channel維護一個獨立的byteBuffer
ByteBuffer不能太大,太大的話很多連接就需要很大內存了,因此需要設計大小可變的byteBuffer。一種思路是首先分配一個小buffer,比如4k,如果不夠再擴容為8k,用新的替換掉舊的,優(yōu)點是消息連續(xù)容易處理,缺點是數據拷貝耗性能。另一個思路是多個數組組成buffer,一個數組不夠,把多出來的內容寫入新的數組,與前面的區(qū)別是消息存儲不連續(xù)解析復雜,優(yōu)點是避免了拷貝引起的性能損耗。
三、多線程優(yōu)化
現在的處理都是單線程的,沒有充分利用多核cpu,
我們可以向下圖這樣優(yōu)化,分為boss好worker,boss負責建立連接,而worker負責讀寫操作,如果連接了,可以直接去worker讀,worker里面有一個selector,線程數可以很多很多,不可能一直建立很多worker,所以一個worker可以管理多個channel,多個thread,相當于把任務分擔了。
問題:?
執(zhí)行我們現在是worker-0不斷循環(huán)執(zhí)行select方法和boss的register可能會有問題,因為他們是同一個selector管理,又是不同線程的,如果上面執(zhí)行的selector執(zhí)行select方法會阻塞,所以下面register就不能執(zhí)行。
我們可以把上面的worker.registery方法移動到sc.register上一行,這樣就有可能會先執(zhí)行到register,再執(zhí)行worker.registery()里面的select方法,register不會阻塞直接執(zhí)行完這樣就不會有問題了,因為他們是兩個線程,所以可能是先執(zhí)行的register,執(zhí)行完又會停留到select方法上阻塞,這個時候如果來了個新的客戶端,所以肯定阻塞又出現了那個問題
解決:
我們要想一個辦法,參考netty的解決辦法,讓他們在同一個線程內執(zhí)行,這樣就可以管理執(zhí)行順序了。
我們在搞個隊列,然后注冊的方法里像隊列中添加一個注冊的任務
?
這樣我們就能保證是worker-0在執(zhí)行任務的時候,就會去注冊了,就能保證在worker-0來執(zhí)行這兩個事件了,實現兩個線程的事件用一個線程執(zhí)行,這樣就可以控制順序。最后要注意,添加完事件之后要喚醒selector因為那邊的selector是一直阻塞狀態(tài)的
回顧下整體流程:
我們先接收連接,調用worker進行register去初始化,如果是第一次就會把selector線程都創(chuàng)建好,如果是第二次,都會向隊列加入任務,這個worker-0的run進來是select就是阻塞住如果沒有的話,然后隊列加入完任務就會喚醒這個阻塞的worker執(zhí)行任務,繼續(xù)他執(zhí)行完又select沒任務阻塞了。
四、NIO vs BIO
1、stream vs channnel
- stream不會自動緩沖數據,channel會利用系統(tǒng)提供的發(fā)送緩沖區(qū),接收緩沖區(qū)(更底層)
- stream僅支持阻塞API,channel同時支持阻塞、非阻塞API,網絡channel可配合selector實現多路復用
- 二者均為全雙工,即讀寫可以同時進行
2、IO模型
當調用channel的read方法或stream的read后,會由用戶態(tài)切換至操作系統(tǒng)內核態(tài)來完成真正的數據讀取,而讀取又分為兩個階段:等待數據階段 和 復制階段
阻塞IO
當調用read切換內核態(tài)后,如果沒有數據過來,就會等待數據,等數據到了之后處理好了,復制數據完成之后才切換回用戶態(tài),這個整個過程用戶線程是阻塞的
非阻塞IO
用戶線程會一直循環(huán)切換內核態(tài)看看有沒有數據,沒有就立即返回,然后繼續(xù)轉化,用戶線程始終在運行,這就算非阻塞IO,但是正在有數據復制數據的時候還是阻塞的(這種切換太頻繁了,可能會影響系統(tǒng)性能)
多路復用
多路復用一上來不是調用read,而是用select等待事件(這個是阻塞的),如果有內核態(tài)會告訴他,這個時候再切換過去復制數據(復制的時候還是需要阻塞)?
他們都是阻塞,那比阻塞的優(yōu)勢在哪里呢?看看下面的圖就知道了,阻塞IO的時候,如果當read在阻塞等待的時候,其他還有線程要執(zhí)行別的操作,他必須等待這個read阻塞完成全部完成才執(zhí)行;而多路復用selector可以檢測多個事件,什么事件都可以觸發(fā)喚醒他運行,也就是他阻塞的時候等的是一批事件不是一個事件。
異步IO模型
- 同步:線程自己去獲取結果(一個線程)
- 異步:線程自己不去獲取結果,而是由其他線程送結果(至少兩個線程)
我們先分析一下上面三種是同步還是異步
- 阻塞IO就是同步的,是用戶線程自己發(fā)起read,也是自己阻塞等待接受accept的,所以是同步的,所以也叫同步阻塞IO
- 非阻塞IO也是同步的,他也是線程自己發(fā)送自己接受,只不過他是不斷循環(huán)去發(fā)送,同步非阻塞
- 多路復用也是同步的,他也是自己發(fā)送select之類的等待阻塞,有事件了喚醒自己去執(zhí)行,還是自己發(fā)自己阻塞自己做,也是同步的
異步阻塞是怎么樣的呢?
他是這樣的,客戶端線程read后,直接返回一個回調函數和參數,然后會異步啟動線程2去等待數據和復制數據,復制完后用回調方法通知結果。?所以也這種就是非阻塞的,請求完直接返回了,所以也叫異步非阻塞文章來源:http://www.zghlxwxcb.cn/news/detail-521342.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-521342.html
到了這里,關于NIO-Selector 網絡編程的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!