java基礎-netty詳解


nio是net開發中最常被提起的點,而游戲服務器端對這個也是看的比較重。java底層提供了nio但是確實很少見有人直接用他,原因很簡單,看netty或者mina的文章都可以看到原因,就是它比較難用,想實現很穩定的商用需要功底很深。
那么網絡底層框架解決了這些問題,現在最主流的就是netty,最開始解除游戲行業的時候還是用的mina,mina實現的比較簡單易上手,但是功能和靈活度欠缺。改用netty的時候剛好是主程職位,所有的工作都是我來做,所以有更深入了解netty的機會,但是也是被動的,因為公司要用就得研究的比較透啊,不然哪里敢使用。
一轉眼使用了很多年了,也就是最開始轉用netty的時候進行很深入的了解,net底層實現好了以后,幾乎就不會再去管它了。一直都是花時間在游戲業務和團隊管理上了。

接下來從經常被問到的一些主要問題分別講起。
netty的結構?
1.AbstractBootstrap引導程序,無論是服務器端(ServerBootstrap)還是客戶端(Bootstrap)都是用它來進行引導的。實例化一個這個對象、設置好內容、綁定端口,開啟同步就OK了。
2.EventLoopGroup事件線程池組,它是組,這個組里面管理的就是EventLoop。這里需要兩個線程池組,一個是boss一個是worker,boss線程池組個數與綁定的端口數相關,幾個端口就設置幾個,多了也沒有用,它是用來管理外部連接事件的,連接成功創建了channel了就扔到worker里面去管理了。worker線程池組數量默認是核心數*2,它是管理真實連接的地方,當有任何事件的時候就會得到通知在線程池中處理channel的響應。
3.EventLoop事件線程池,線程池大家都可以理解,它里面管理一個執行線程池(但是像epollEventLoop其實是單線程運行的,也就是一個線程池只有一個線程)、一組channel、還有其他的鎖;狀態;任務隊列等。最重要的是各種事件選擇器(selector)是在這里面定義,后面會介紹各種selector的區別。
4.channel通道,簡單的理解為socket連接的一種抽象,所有的連接都單獨抽象成為通道放在EventLoop中管理,當哪個通道有事件了,就生成一個任務執行。worker通道建立是通過ChannelInitializer完成的。
5.ChannelInitializer通道初始化器,每個鏈接建立的時候,調用初始化器初始化一個channel,並且給channel構造后一個channelPipeline。
6.ChannelPipeline管道,一個channel有個管道,管道的作用就是管理一個責任鏈,任何事件的執行都是通過這個責任鏈一層一層執行的。每個責任鏈環節就是一個channelHandler。
7.ChannelHandler通道處理器,就是一個環節的處理,比如黑名單處理、拆包封包、編解碼都是一個處理器。這個處理器是公用的,每個通道的沒個環節都有一個ChannelHandlerContext。
8.ChannelHandlerContext通道處理器環境,就是記錄在某個處理器環節的數據存放的,里面可以存放一些處理過或者待處理的屬性。
所以netty的結構可以看做是
Bootstrap{
EventLoopGroup boss;
EventLoopGroup worker{
EventLoop{
Selector
ThreadPoolExecutor
Channel{
ChannelPipeline{
ChannelHandlerContext
}
}
}
}
}

channel、channelPipeline與channelContext的關系?
boss和work線程池的結構是什么樣的?boss可以設置幾個線程?
其實前面的netty的結構已經回答了這兩個問題。

什么是多路復用,各種selector的區別,存在哪些問題?
多路復用是相對bio來說。bio中每個鏈路都是一個線程阻塞運行,是主動監聽有任何信號就進行處理,而nio中是又一個selector來管理多個鏈路,selector監聽某鏈路有信號才調用執行的被動處理。
selector實現主要有:
select-兼容性好
poll-實現類似select,沒有連接數限制
epoll-沒有連接數限制,效率高,高效率是因為它不只是返回信號,而且返回了哪些channel有什么信號,而不需要再去輪訓一遍channel列表去找哪些channel有信號。
在linux操作系統下,epoll是最長被使用的選擇器,但是它有一個臭名昭著的bug,在某些特殊情況下會導致selector線程喚醒,但是其實沒有任務可以執行,這樣它就一直輪訓查找任務導致CPU到100%。netty解決的辦法就是監聽這種空輪訓,若空輪訓到達一定數量就考試重新建立這個selector。

什么是0拷貝?可能存在的問題。
0拷貝是指bytebuffer類型是直接應用的采用0拷貝特性。它的0拷貝是指操作系統的內核空間到用戶空間之間不需要拷貝。
首先,分清哪些操作在用戶空間,例如jvm里面的對象分配、復制、銷毀都是用戶空間,而所有io操作其實jvm只是給操作系統內核一些指令然后就等着操作系統完成,例如磁盤io操作、網絡io操作,那么這就切換到內核態操作內核空間。
然后,減少拷貝操作就是減少時間,常規io操作都是內核讀完復制信息到jvm的內存中等待jvm處理,處理完再往回寫內核空間等待內核去寫磁盤寫網絡,一來一回多兩次內存復制。而0拷貝就是減少這兩次內存復制的。
再然后,僅僅是減少了復制了么?當然不是,內核分配和讀寫這些內存數據其實遠比用戶態讀寫的更快。
最后,java的nio是如何實現0拷貝的。它是用過unsafe類來直接操作主內存的讀寫實現的這部分內存叫做堆外內存,而jvm內使用一個虛引對象用來對應這塊直接內存進行管理。所以內核直接把網絡緩沖區的內容直接寫到這塊內存或者從這個塊內存寫到網絡緩沖區就可以,這樣的速度非常塊,而jvm內部使用這個虛引用對象直接讀寫內外內存的數據。
存在的問題:前面說了0拷貝是依靠對外內存實現的,那么問題就在於堆外內存的管理,若管理不好就會產生內存泄露,而之所以使用虛引用就是為了在虛引用被回收的時候可以通過回收方法對對堆外內存進行回收。而虛引用回收時是將這個回收方法放在一個隊里里面等待jvm調用,而這個調用不一定被執行或者不一定被完全執行完。就會導致有部分堆外內存無法釋放。所以netty的bytebuf對象外面會包裹一個探針,會有監控機制來隨機檢查探針是否正常來判斷對象回收是否正常,一般出現問題都是因為沒有調用回收犯法。

PS:注意,多路復用和0拷貝不是netty的特性,而是java的nio就已經提供了。

封包拆包怎么做的?
netty提供了三種封包拆包解碼器來進行封包拆包,一般就夠用了,也可以自己編程實現。
LengthFieldBasedFrameDecoder 頭長度解碼器
DelimiterBasedFrameDecoder 分隔符解碼器
FixedLengthFrameDecoder 定長解碼器

bytebuf池結構是什么樣的?
池結構分為:arean、chunkList、chunk、page、subpage
PoolArena{
PoolChunkList{
PoolChunk{
PoolSubpage
}
}
}

如何做校驗或者加密來保證不被修改,如何防止惡意重復發包?
可以在字節流的頭部增加兩個內容:1.整個協議內容的校驗碼放在協議頭,可以對協議進行hash計算得到的結果放在這里,若內部被修改了則這個會對不上。2.協議頭部放一個協議順序號,每個客戶端的序號自增,服務器端可以緩存最近10個序號,若重復發送就丟棄。

netty中tcp參數優化?
TCP_NODELAY,是否啟用Nagle算法,該算法防止過多小數據包的發送,將小包合並后一起發送,看似挺有用,但是這會延遲協議的響應,所以游戲服務器一般都禁用。
SO_SNDBUF、SO_RCVBUF,tcp收發緩沖區大小,這個大小一般默認系統即可,若在網絡層遇到瓶頸的時候可以適當放大,這個一般做壓力測試的時候可以進行調優。
SO_BACKLOG,連接請求隊列,當有瞬時有大量連接請求時,可以調整該隊列,避免客戶端連接直接被拒絕導致一直斷線重連。

擴展TCP協議
創建連接,三次握手。客戶端發送創建連接請求,服務器應答請求的同時發送創建連接請求,客戶端應答請求,連接創建完畢。
斷開連接,四次揮手。主動斷開方發送斷開請求並且保證不再發送協議,被斷開方應答請求但是可能還有未發送完成的緩沖內容,被斷開方緩沖區內容都發送完成后發送斷開請求,主動段開方應答請求,自此斷開連接。
之所以斷開連接需要四步是因為被斷開方不能同時就斷開,因為還有一部分發送到一半,他發送完才能進行斷開連接的請求。
弱網情況tcp的優點可能就真的成為短板,tcp協議的實現保證了收發的順序和可靠性,這兩個實現的原理就是將長協議拆分成有序的數據包,按照數據包的順序進行發送,並且發送每一個數據包的時候都需要客戶端進行確認應答,若超時未應答這進行重傳,直到收到確認應答才發送下一個數據包,而這個超時時間會隨着重試次數而遞增。所以弱網的情況經常會發生多次超時,而如果協議比較大,分成多個數據包,每個數據包都超時重發多次,導致一條協議在網絡傳輸上的超時就無法忍受了。解決辦法:
1.盡量減小協議,比如大量的定義數據放在客戶端。
2.減少阻塞協議的實現,有些協議必須等待收發完成而阻塞界面無法操作。但是大多數協議是不需要這個過程的。
3.有條件使用或者及其注重同步效率的,可以考慮使用udp協議。

TCP擁塞控制
網絡io也是一種資源,資源也都不是無限度的,比如公司或者家里裝網都會根據帶寬收費的,帶寬就是一種限制,鏈路上的路由、交換機的緩存大小也是限制。所以網絡傳輸並不是可以無限量的發送(這里說的是同一時刻發送量,時間拉的足夠長確實可以無限量),所以簡單理解擁塞控制就是調整網絡流量大小。
控制網絡流量大小首先就是要知道控制的量,由於這個不只是本方網絡狀況的因素,也要看對方網絡狀況(我方帶寬100M,對方帶寬可能100M也可能是1M,而且現在移動網絡應用的很多,所以這個值是不定的,一會高一會低)。所以這個控制量是不停的嘗試和調整出來的。而確定這個值TCP的實現提供4個算法來結合使用(最初只有2種方法)。
首先明確,要確定這個值叫做擁塞窗口值 cwnd,它就是同時可以發送多少個數據包。另外有一個值ssthresh (slow start threshold)慢啟動閾值,是協助cwnd使用的,這個值是一個轉折點,即cwnd到達ssthresh的時候就要改變算法了。
阻塞控制的四個算法使用如下:
1.最開始,使用慢開始算法,最開始定義cwnd為1,即每次發送一個數據包,收到一個ack應答,cwnd加1即為2,則下次發送兩個數據報收到兩個ack應答,cwnd加2即為4,這樣指數級增長。
2.cwnd達到ssthresh的時候,改用阻塞避免算法,該時猜測為可能要達到阻塞上限,cwnd增加速度從指數級改為線性的每次得到應答后增加為1,一直到有數據包超時重傳再次進入到慢開始算法。
3.當有三個重復確認的時候,使用快速重傳算法,為了避免數據包達到超時狀態添加了快速重傳算法,tcp發送數據包是有序的,當客戶端收到數據包序號不連續的時候會重復發送ack應答不連續數據包前一個數據包的序號,當服務器方連續收到三個這樣的重復確認則重發丟失的數據包,這樣就在該數據包超時之前重傳了,避免了超時的出現進入慢開始算法。
4.當用三個重復確認的時候,使用快速恢復算法調整cwnd和ssthresh值,三次重復確認可能網絡就是有問題的,但是避免進入慢開始降低傳輸速度,所以這里進行調整cwnd和ssthresh都調整為當前cwnd的一半(有的也會調整成一半再加3其實就是加上三次重復親確認的值),然后繼續使用避免阻塞算法。
5.當有超時重傳的包的時候,回到第一步使用慢開始算法重新來。
PS:其實整個過程就是一直在調整cwnd的大小,一直到維持比較穩定范圍內,這個范圍就是雙方傳輸時的穩定值,小了網絡利用不足,大了會產生大量丟包重傳反倒影響TCP傳輸速率。一般在游戲中用到它的並不太多,要是游戲能把網絡打滿了,流量很客觀,一般都是網絡視頻或者下載類應用考慮的多。

java中常見的網絡異常
SocketTimeoutException 創建socket連接超時,如若設置了超時時間。
BindException:Address already in use: JVM_Bind 綁定的端口已經被使用,這個比較常見,例如服務重復啟動,另外以前也碰到過一個問題就是創建socket連接的時候本地也要隨機給一個端口,但是這個端口可能已經被占用(當時創建的是與MySQL的連接,而隨機得到的本地端口號是綁定的游戲端口)所以也會報錯,這個就是在操作系統層設置隨機端口范圍,避開常用端口和服務使用端口(一般都是用非常大的端口號)。
ConnectException: Connection refused: connect 創建socket連接的時候,IP地址或者端口號不能使用。原因多種,配置錯了、網絡不可達、有防火牆。
SocketException: Socket is closed 連接socket已經關閉了,但是在此對連接進行操作的異常,一般在操作的時候應該進行判斷socket是否已關閉。
SocketException:Connection reset或者Connect reset by peer:Socket write error,連接socket被關閉(一般是指異常關閉對方檢查連接沒有關閉的情況)進行讀或寫的時候發生的異常。

參考資料:
TCP的擁塞控制(詳解)
TCP 擁塞控制
Java網絡編程五個常見的異常發生及對應的解決方案


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM