有興趣的同學可以移步筆者的個人博客 更多博客
爲什麼使用netty
Netty是一個網絡通信框架,其出現的原因主要是爲了解決NIO的不足。如:
- NIO的類庫和API繁雜,使用麻煩,你需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;
- 需要具備其它的額外技能做鋪墊,例如熟悉Java多線程編程,因爲NIO編程涉及到Reactor模式,你必須對多線程和網路編程非常熟悉,才能編寫出高質量的NIO程序;
- 可靠性能力補齊,工作量和難度都非常大。例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等等,NIO編程的特點是功能開發相對容易,但是可靠性能力補齊工作量和難度都非常大;
NIO(Non-blocking I/O,在Java領域,也稱爲New I/O),是一種同步非阻塞的I/O模型,也是I/O多路複用的基礎,已經被越來越多地應用到大型應用服務器,成爲解決高併發與大量連接、I/O處理問題的有效方式。
本文會從傳統的阻塞I/O和線程池模型面臨的問題講起,然後對比幾種常見I/O模型,一步步分析NIO怎麼利用事件模型處理I/O,解決線程池瓶頸處理海量連接,包括利用面向事件的方式編寫服務端/客戶端程序。
傳統BIO模型分析
{
ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//線程池
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(8088);
while(!Thread.currentThread.isInturrupted()){//主線程死循環等待新連接到來
Socket socket = serverSocket.accept();
executor.submit(new ConnectIOnHandler(socket));//爲新的連接創建新的線程
}
class ConnectIOnHandler extends Thread{
private Socket socket;
public ConnectIOnHandler(Socket socket){
this.socket = socket;
}
public void run(){
while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循環處理讀寫事件
String someThing = socket.read()//讀取數據
if(someThing!=null){
//處理數據
socket.write()//寫數據
}
}
}
}
上面的代碼就是典型的傳統BIO服務端監聽代碼,由於accept()、read()、write() 這三個方法是阻塞的、耗時的。如果是單線程的話,在執行阻塞代碼是會使cpu空等,並且最重要的是,如果有10000個併發的話,那麼等待時間是不能夠接受的。
使用多線程的本質就是:
- 利用多核。
- 當I/O阻塞系統,但CPU空閒的時候,可以利用多線程使用CPU資源。
但是傳統IO在使用多線程解決上面的問題時會嚴重依賴於線程,意味着每個請求都要建立一個線程,當客戶端數量達到萬級時,就需要建立萬級的線程。線程佔用的內存,切換線程資源昂貴,最終導致這種方案無法保證系統的伸縮性。
傳統NIO模型分析
所有的系統I/O都分爲兩個階段:等待就緒和操作。舉例來說,讀函數,分爲等待系統可讀和真正的讀;同理,寫函數分爲等待網卡可以寫和真正的寫。
需要說明的是等待就緒的阻塞是不使用CPU的,是在“空等”;而真正的讀寫操作的阻塞是使用CPU的,真正在"幹活",而且這個過程非常快,屬於memory copy,帶寬通常在1GB/s級別以上,可以理解爲基本不耗時。
interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//讀,寫或者連接
}
//IO線程主循環:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//選擇就緒的事件和對應的連接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新連接,則註冊一個新的讀寫處理器
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);//如果可以寫,則執行寫事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);//如果可以讀,則執行讀事件
}
}
}
Map<Channel,ChannelHandler> handlerMap;//所有channel的對應事件處理器
}
NIO由原來的阻塞讀寫(佔用線程)變成了單線程輪詢事件,找到可以進行讀寫的網絡描述符進行讀寫。除了事件的輪詢是阻塞的(沒有可乾的事情必須要阻塞),剩餘的I/O操作都是純CPU操作,沒有必要開啓多線程。