前言
在了解一個事物之前,最好能對它的基本屬性和相關(guān)概念有個基本的認知,所以學(xué)習(xí)Netty之前,也有必要了解與Netty相關(guān)的基礎(chǔ)概念知識;本篇將對Netty做一個基礎(chǔ)性的介紹,主要包括Netty的適用場景,特色以及基礎(chǔ)的IO知識,如果你已經(jīng)了解這些知識,也可以跳過本篇,直接進入下一篇:Netty剖析 - 2. 實現(xiàn)
Netty是什么?
首先我們來看Netty是什么,關(guān)于這個問題,其官網(wǎng)有一段闡述:
Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients
這段翻譯過來意思就是:
Netty是一個基于NIO的異步網(wǎng)絡(luò)編程框架,基于Netty能快速的搭建高性能易擴展的網(wǎng)絡(luò)應(yīng)用(包括客戶端與服務(wù)端)
具體來說Netty就是對于Java NIO的封裝,NIO又是什么呢?NIO是Java 1.4后引入的基于事件模型的非阻塞IO框架,在NIO之前,對于數(shù)據(jù)的處理都是基于BIO(blocking IO),從名字上就知道BIO是以阻塞的形式對數(shù)據(jù)進行處理,這種處理形式比較簡單,但是既然阻塞的,那么就不可避免會涉及到線程的操作,熟悉并發(fā)的小伙伴應(yīng)該都知道,線程是一種昂貴的資源,無論是創(chuàng)建,銷毀,還是切換,這就導(dǎo)致BIO在面對一些特定場景如高并發(fā)等束手無策,而這些場景在互聯(lián)網(wǎng)應(yīng)用中卻又很常見;對應(yīng)的,NIO能較好的應(yīng)對這些場景,遺憾的是,Java在剛推出NIO時,由于各種原因,致使其使用復(fù)雜,且經(jīng)常會出現(xiàn)Bug,結(jié)果就是:廣大開發(fā)者有需求,但解決需求的工具就是不好用這樣尷尬的局面,怎么辦呢? -- 自己動手,豐衣食足!大不了再造個"輪子",所以就出現(xiàn)了一系列解決NIO問題的框架,而Netty就是其中最著名的那一個(當然Java發(fā)展到現(xiàn)在,其NIO庫原本的很多問題都得到了解決,不過很多解決方案借鑒的也是Netty的思想)
另外,Netty并不止于解決NIO的問題,它更進一步,還提供了一系列特色功能
Netty的特色
自己的孩子自己最了解,下面試Netty官網(wǎng)對于Netty特色的說明:
It greatly simplifies and streamlines network programming such as TCP and UDP socket server
'Quick and easy' doesn't mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences earned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise
這段話大概的意思就是:
首先,Netty能極大的簡化你的網(wǎng)絡(luò)編程;并且,簡單好用還不需要以復(fù)雜的管理和低效的性能為代價,Netty通過優(yōu)秀的設(shè)計,在易部署,高性能,穩(wěn)定性,擴展性之間找到了一個較好的平衡點
我們把這句話提煉出來,大概就可以得到Netty的幾大特色:
- 針對基本的需求提供了簡單易用的接口,直接上手!
- 針對復(fù)雜的場景提供了很強的擴展性,輕松應(yīng)對業(yè)務(wù)發(fā)展!
- 在上面兩點的基礎(chǔ)上,性能不打折!
而如果對這些特點進行細化,則可以得出:
- 基于事件機制(Pipeline - Handler)達成關(guān)注點分離(消息編解碼,協(xié)議編解碼,業(yè)務(wù)處理)
- 可定制的線程處理模型,單線程,多線程池等
- 屏蔽NIO本身的bug
- 性能上的優(yōu)化
- 相較于NIO接口功能更豐富
- 對外提供統(tǒng)一的接口,底層支持BIO與NIO兩種方式自由切換
這些特性將在本系列第二篇里做詳細分析;既然Netty的本質(zhì)還是一個基于NIO的網(wǎng)絡(luò)框架,那么想要掌握Netty的精髓,對于NIO的了解就不可或缺
NIO處理模型介紹
在介紹NIO之前,最好了解一下BIO,還沒有學(xué)習(xí)過的小伙伴可以閱讀我另外一篇介紹BIO的文章:Java IO使用入門 -- IO其實很簡單
NIO是Java 1.4引入的一種同步非阻塞的I/O模型,也是I/O多路復(fù)用的基礎(chǔ);相對于Java BIO(OIO)提供的基于面向流的阻塞式編程模型,NIO提供的是面向緩沖區(qū)的響應(yīng)式事件編程模型

讀到這里可能有些人會覺得迷糊,什么阻塞?非阻塞?基于流?基于緩沖區(qū)?這里有必要介紹一下Linux下的5中IO模型:

- 阻塞I/O模型:
最常用的I/O模型就是阻塞I/O模型,當用戶進程調(diào)用了recvfrom這個系統(tǒng)調(diào)用,kernel就開始了IO的第一個階段:準備數(shù)據(jù)(對于網(wǎng)絡(luò)IO來說,很多時候數(shù)據(jù)在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的數(shù)據(jù)到來)。這個過程需要等待,也就是說數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞。當kernel一直等到數(shù)據(jù)準備好了,它就會將數(shù)據(jù)從kernel中拷貝到用戶內(nèi)存,然后kernel返回結(jié)果,用戶進程才解除block的狀態(tài),重新運行起來。所以,blocking IO的特點就是在IO執(zhí)行的兩個階段都被block了
image.png
-
非阻塞IO模型:
linux下,可以通過設(shè)置socket使其變?yōu)閚on-blocking。當對一個non-blocking socket執(zhí)行讀操作時,流程是這個樣子:
image.png
當用戶進程發(fā)出read操作時,如果kernel中的數(shù)據(jù)還沒有準備好,那么它并不會block用戶進程,而是立刻返回一個error。從用戶進程角度講 ,它發(fā)起一個read操作后,并不需要等待,而是馬上就得到了一個結(jié)果。用戶進程判斷結(jié)果是一個error時,它就知道數(shù)據(jù)還沒有準備好,于是它可以再次發(fā)送read操作。一旦kernel中的數(shù)據(jù)準備好了,并且又再次收到了用戶進程的system call,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存,然后返回;所以,nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數(shù)據(jù)好了沒有。 -
IO復(fù)用模型:
IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在于單個process就可以同時處理多個網(wǎng)絡(luò)連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責(zé)的所有socket,當某個socket有數(shù)據(jù)到達了,就通知用戶進程
image.png
當用戶進程調(diào)用了select,那么整個進程會被block,而同時,kernel會監(jiān)視所有select負責(zé)的socket,當任何一個socket中的數(shù)據(jù)準備好了,select就會返回。這個時候用戶進程再調(diào)用read操作,內(nèi)核負責(zé)將數(shù)據(jù)從kernel拷貝到用戶進程;所以,I/O 多路復(fù)用的特點是通過一種機制使得一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態(tài),select()函數(shù)就可以返回,所以說它最大的優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不需要創(chuàng)建或維護新的進程/線程。另外,從上面比較IO復(fù)用流程圖和阻塞IO的圖可以發(fā)現(xiàn),多路復(fù)用本身也是阻塞的,事實上,其效率可能還更差一些。因為這里需要使用兩個system call (select 和 recvfrom),而阻塞IO只調(diào)用了一個system call (recvfrom)。但是,用select的優(yōu)勢在于它可以同時處理多個connection。所以,如果處理的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用阻塞IO的web server性能更好,可能延遲還更大。select/epoll的優(yōu)勢并不是對于單個連接能處理得更快,而是在于能處理更多的連接。)在IO復(fù)用模型中,對于每一個socket,一般都設(shè)置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數(shù)block,而不是被socket IO給block 信號驅(qū)動IO模型:
首先開啟套接口信號驅(qū)動I/O功能,并通過系統(tǒng)調(diào)用sigaction執(zhí)行一個信號處理函數(shù)(此系統(tǒng)調(diào)用立即返回,進程繼續(xù)工作,它是非阻塞的)。當數(shù)據(jù)準備就緒時,就為該進程生成一個SIGIO信號。隨即可以在信號處理程序中調(diào)用recvfrom來讀數(shù)據(jù),井通知主循環(huán)函數(shù)處理數(shù)據(jù);一般用的較少-
異步IO:
在異步IO模型下,用戶進程發(fā)起read操作之后,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它收到一個asynchronous read之后,首先它會立刻返回,所以不會對用戶進程產(chǎn)生任何block。然后,kernel會等待數(shù)據(jù)準備完成,然后依然由它將數(shù)據(jù)拷貝到用戶內(nèi)存,當這一切都完成之后,kernel會給用戶進程發(fā)送一個signal,告訴它read操作完成了
image.png
介紹完這5種IO模型后,我們回到NIO,NIO基于的是IO復(fù)用模型(就是上面的第三種IO模型),正如在介紹IO復(fù)用模型時已提到,而在linux下,有三種針對該模型的實現(xiàn),分別為:select,poll,epoll;select和poll的實現(xiàn)機制類似,主要區(qū)別在于描述fd集合的方式不同,poll使用pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu);epoll是linux 2.6后才有的,它主要是對select和poll的缺陷做了一些改進,這兩種實現(xiàn)方式有幾個比較大的缺點:
1) 每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在fd很多時會很大
2) 每次調(diào)用select都需要在內(nèi)核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
3) select支持的文件描述符數(shù)量太小了,默認是1024(當然可以手動改,但改大了不一定效果好,以為前面的1,2兩點)
對于第一個缺點,epoll在每次注冊新的事件到epoll句柄中時,會把所有的fd拷貝進內(nèi)核,而不是在epoll_wait的時候重復(fù)拷貝。這樣就保證了每個fd在整個過程中只會拷貝一次。
對于第二個缺點,epoll的解決思路是每當一個fd準備就緒,就調(diào)用對應(yīng)的回調(diào)函數(shù)將其加入一個就緒鏈表,然后只需要遍歷這個就緒鏈表即可,不需要遍歷所有fd
對于第三個缺點,epoll沒有這個限制,它所支持的fd上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠大于1024,舉個例子,在1GB內(nèi)存的機器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大
另外順便提一下Windows下的異步IO實現(xiàn)機制:I/O Completion Ports,或簡稱IOCP,個人覺得它的設(shè)計比較好,極大的減少線程切換對性能的影響,同時又能保證CPU保持在較高的利用率,有興趣可以閱讀一下這篇文章
總結(jié)
本篇主要介紹了Netty相關(guān)的基礎(chǔ)知識,核心在于各種IO模型,特別是異步IO模型,作用在于為本系列的第二篇Netty剖析 - 2. 實現(xiàn)做準備,如果需要對IO模型進行更深入的了解,可以參考下面幾篇文章:



