Netty構(gòu)建NIO的httpClient

先簡單的了解一下BIO與NIO

下圖是幾種常見I/O模型的對比:


圖片.png

傳統(tǒng)的BIO里面socket.read()膝晾,如果TCP RecvBuffer里沒有數(shù)據(jù),函數(shù)會一直阻塞,直到收到數(shù)據(jù),返回讀到的數(shù)據(jù)。

對于NIO朴译,如果TCP RecvBuffer有數(shù)據(jù),就把數(shù)據(jù)從網(wǎng)卡讀到內(nèi)存属铁,并且返回給用戶眠寿;反之則直接返回0,永遠(yuǎn)不會阻塞焦蘑。

最新的AIO(Async I/O)里面會更進(jìn)一步:不但等待就緒是非阻塞的盯拱,就連數(shù)據(jù)從網(wǎng)卡到內(nèi)存的過程也是異步的。

經(jīng)典的BIO模式例嘱,每連接每線程的模型狡逢,之所以使用多線程,主要原因在于socket.accept()拼卵、socket.read()奢浑、socket.write()三個主要函數(shù)都是同步阻塞的,當(dāng)一個連接在處理I/O的時候腋腮,系統(tǒng)是阻塞的雀彼,如果是單線程的話必然就掛死在那里;但CPU是被釋放出來的即寡,開啟多線程徊哑,就可以讓CPU去處理更多的事情。多線程一般都使用線程池聪富,可以讓線程的創(chuàng)建和回收成本相對較低实柠。在活動連接數(shù)不是特別高(小于單機(jī)1000)的情況下,這種模型是比較不錯的善涨,可以讓每一個連接專注于自己的I/O并且編程模型簡單窒盐,也不用過多考慮系統(tǒng)的過載、限流等問題钢拧。線程池本身就是一個天然的漏斗蟹漓,可以緩沖一些系統(tǒng)處理不了的連接或請求。
??不過源内,這個模型最本質(zhì)的問題在于葡粒,嚴(yán)重依賴于線程。線程的創(chuàng)建和銷毀成本很高膜钓,線程本身占用較大內(nèi)存嗽交,線程的切換成本是很高的。

NIO一個重要的特點(diǎn)是:socket主要的讀颂斜、寫夫壁、注冊和接收函數(shù),在等待就緒階段都是非阻塞的沃疮,真正的I/O操作是同步阻塞的盒让。
??NIO的讀寫函數(shù)可以立刻返回,這就給了我們不開線程利用CPU的最好機(jī)會:如果一個連接不能讀寫(socket.read()返回0或者socket.write()返回0)司蔬,我們可以把這件事記下來邑茄,記錄的方式通常是在Selector上注冊標(biāo)記位,然后切換到其它就緒的連接(channel)繼續(xù)進(jìn)行讀寫俊啼。

結(jié)合事件模型使用NIO同步非阻塞特性

NIO的主要事件有幾個:讀就緒肺缕、寫就緒、有新連接到來授帕。
首先需要注冊當(dāng)這幾個事件到來的時候所對應(yīng)的處理器同木。
其次,用一個死循環(huán)選擇就緒的事件豪墅,會執(zhí)行系統(tǒng)調(diào)用(Linux 2.6之前是select泉手、poll,2.6之后是epoll偶器,Windows是IOCP)斩萌,還會阻塞的等待新事件的到來。新事件到來的時候屏轰,會在selector上注冊標(biāo)記位颊郎,標(biāo)示可讀、可寫或者有連接到來霎苗。

interface ChannelHandler{ 
  void channelReadable(Channel channel); 
  void channelWritable(Channel channel); 
}
class Channel{ 
  Socket socket;
  Event event;//讀姆吭,寫或者連接
 }
//IO線程主循環(huán): 
class IoThread extends Thread{ 
  public void run(){ 
    Channel channel; 
    while(channel=Selector.select()){
      //選擇就緒的事件和對應(yīng)的連接 
      if(channel.event==accept){ 
        registerNewChannelHandler(channel);//如果是新連接,則注冊一個新的讀寫處理器 
      } 
      if(channel.event==write){
       getChannelHandler(channel).channelWritable(channel);//如果可以寫唁盏,則執(zhí)行寫事件 
      } 
      if(channel.event==read){
       getChannelHandler(channel).channelReadable(channel);//如果可以讀内狸,則執(zhí)行讀事件
      }
    }
  } 
  Map<Channel检眯,ChannelHandler> handlerMap;//所有channel的對應(yīng)事件處理器 
}

這個程序很簡短忆矛,也是最簡單的Reactor模式:注冊所有感興趣的事件處理器匾浪,單線程輪詢選擇就緒事件,執(zhí)行事件處理器蹬碧。注意select是阻塞的昂灵,無論是通過操作系統(tǒng)的通知(epoll)還是不停的輪詢(select避凝,poll),這個函數(shù)是阻塞的眨补。所以你可以放心大膽地在一個while(true)里面調(diào)用這個函數(shù)而不用擔(dān)心CPU空轉(zhuǎn)管削。

為解決高并發(fā)時線程數(shù)過多的問題,這里我們使用成熟的NIO框架Netty編寫httpClient撑螺。

Bootstrap是Socket客戶端創(chuàng)建工具類含思,用戶通過Bootstrap可以方便的創(chuàng)建netty的客戶端并發(fā)起異步TCP連接操作。

創(chuàng)建客戶端連接輔助類Bootstrap
        Bootstrap b = new Bootstrap();
        b.group(workerGroup);//NioEventLoopGroup
        b.channel(NioSocketChannel.class);
        b.option(ChannelOption.SO_KEEPALIVE, false);
        b.option(ChannelOption.SO_TIMEOUT, this.timeout);
        b.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
                // 客戶端接收到的是httpResponse響應(yīng)实蓬,所以要使用HttpResponseDecoder進(jìn)行解碼
                ch.pipeline().addLast(new HttpResponseDecoder());
                // 客戶端發(fā)送的是httprequest茸俭,所以要使用HttpRequestEncoder進(jìn)行編碼
                ch.pipeline().addLast(new HttpRequestEncoder());
                ch.pipeline().addLast(handler);
            }
        });

上述代碼就是客戶端創(chuàng)建的流程:
1.用戶線程創(chuàng)建Bootstrap
Bootstrap是Socket客戶端創(chuàng)建工具類,通過API設(shè)置創(chuàng)建客戶端相關(guān)的參數(shù)安皱,異步發(fā)起客戶端連接调鬓。
2.指定處理客戶端連接、IO讀寫的Reactor線程組NioEventLoopGroup酌伊√谖眩可以通過構(gòu)造函數(shù)指定I/O線程的個數(shù),默認(rèn)為CPU內(nèi)核數(shù)的2倍居砖。
3.通過Bootstrap的ChannelFactory和用戶指定的Channel類型創(chuàng)建用于客戶端連接的NioSocketChannel虹脯。此處的NioSocketChannel類似于Java NIO提供的SocketChannel。
4.設(shè)置TCP參數(shù)

主要TCP參數(shù)如下:
(1) SO_TIMEOUT: 控制讀取操作將阻塞多少毫秒奏候,如果返回值為0循集,計時器就被禁止了,該線程將被無限期阻塞蔗草。
(2) SO_SNDBUF: 套接字使用的發(fā)送緩沖區(qū)大小
(3) SO_RCVBUF: 套接字使用的接收緩沖區(qū)大小
(4) SO_REUSEADDR : 是否允許重用端口
(5) CONNECT_TIMEOUT_MILLIS: 客戶端連接超時時間咒彤,原生NIO不提供該功能,Netty使用的是自定義連接超時定時器檢測和超時控制
(6) TCP_NODELAY : 是否使用Nagle算法

5.創(chuàng)建默認(rèn)的channel Handler pipeline咒精,用于調(diào)度和執(zhí)行網(wǎng)絡(luò)事件镶柱。
Bootstrap為了簡化Handler的編排,提供了ChannelInitializer模叙,當(dāng)TCP鏈路注冊成功后歇拆,調(diào)用initChannel接口。
pipeline維護(hù)著一個或者多個handler 用于異步的處理I/O數(shù)據(jù),如HttpResponseDecoder是一個客戶端接收到的是httpResponse響應(yīng)故觅,所以要使用HttpResponseDecoder進(jìn)行解碼的handle厂庇。

客戶端連接操作

以下內(nèi)容轉(zhuǎn)自netty4源碼分析-connect
http://xw-z1985.iteye.com/blog/1937999

        // Start the client.
        ChannelFuture f = b.connect(host, port);
        f.channel().closeFuture();

1、異步發(fā)起TCP連接 b.connect();
在doConnect方法中調(diào)用initAndRegister方法输吏,創(chuàng)建和初始化NioSocketChannel宋列,并注冊channel對應(yīng)的網(wǎng)絡(luò)監(jiān)聽狀態(tài)位到多路復(fù)用器。
coonect方法并沒有返回一個Channel 而是一個 ChannelFuture评也。 ChannelFutre提供添加一個addListener的方法,使得這個channel真正被打開的時候調(diào)用用戶設(shè)置的回調(diào)灭返。
2盗迟、由多路復(fù)用器在I/O中輪詢個Channel,處理連接結(jié)果
3熙含、如果連接成功罚缕,設(shè)置Future結(jié)果,發(fā)送連接成功事件怎静,觸發(fā)ChannelPipeline執(zhí)行
4邮弹、由ChannelPipeline調(diào)度執(zhí)行系統(tǒng)和用戶的ChannelHandler,執(zhí)行業(yè)務(wù)邏輯

這里主要看一下cennect操作

Channel創(chuàng)建完成后蚓聘,連接操作會異步執(zhí)行腌乡,最終調(diào)用到HeadContext的connect方法.

doConnect三種可能結(jié)果

1.連接成功,然會true;
?2.暫時沒有連接上夜牡,服務(wù)器端沒有返回ACK應(yīng)答与纽,連接結(jié)果不確定,返回false塘装。此種結(jié)果下急迂,需要將NioSocketChannel中的selectionKey設(shè)置為OP_CONNECT,監(jiān)聽連接結(jié)果蹦肴;
?3.接連失敗僚碎,直接拋出I/O異常
??異步返回之后,需要判斷連接結(jié)果阴幌,如果成功勺阐,則觸發(fā)ChannelActive事件。最終會將NioSocketChannel中的selectionKey設(shè)置為SelectionKey.OP_READ裂七,用于監(jiān)聽網(wǎng)絡(luò)讀操作皆看。

異步連接結(jié)果通知

NioEventLoop的Selector輪詢客戶端連接Channel,當(dāng)服務(wù)端返回應(yīng)答后背零,進(jìn)行判斷腰吟。依舊是NioEventLoop中的processSelectedKey方法。
doFinishConnect方法通過調(diào)用SocketChannel的finishConnect方法完成連接的建立,在NioSocketChannel中實(shí)現(xiàn)。此時毛雇,isActive()返回true嫉称,所以觸發(fā)ChannelActive事件,該事件是一個inbound事件灵疮,所以Inbound的處理器可以通過實(shí)現(xiàn)channelActive方法來進(jìn)行相應(yīng)的操作织阅。

總結(jié):從發(fā)起connect請求到請求建立先后共經(jīng)歷了以下幾件事情:
1、創(chuàng)建套接字SocketChannel
2震捣、設(shè)置套接字為非阻塞
3荔棉、設(shè)置channel當(dāng)前感興趣的事件為SelectionKey.OP_READ
4、創(chuàng)建作用于SocketChannel的管道Pipeline蒿赢,該管道中此時的處理器鏈表為:Head(outbound)->tail(inbound)润樱。
5、設(shè)置SocketChannel的options和attrs羡棵。
6壹若、為管道增加一個Inbound處理器ChannelInitializer。經(jīng)過此步驟后皂冰,管道中的處理器鏈表為:head(outbound)->ChannelInitializer(inbound)->tail(inbound)店展。注意ChannelInitializer的實(shí)現(xiàn)方法initChannel,里面會當(dāng)channelRegisgered事件發(fā)生時將EchoClientHandler加入到管道中秃流。
7赂蕴、啟動客戶端線程,并將register0任務(wù)加入到線程的任務(wù)隊(duì)列中剔应。而register0任務(wù)做的事情為:將SocketChannel睡腿、0、注冊到selector中并得到對應(yīng)的selectionkey峻贮。然后通過回調(diào)席怪,將doConnect0任務(wù)加入到線程的任務(wù)隊(duì)列中。線程從啟動到現(xiàn)在這段時間內(nèi)纤控,任務(wù)隊(duì)列的變化如下:register0任務(wù)->register0任務(wù)挂捻,doConnect0任務(wù)-> doConnect0任務(wù)
8、通過channelRegistered事件船万,將EchoClientHandler加入到管道中刻撒,并移除ChannelInitializer,經(jīng)過此步驟后耿导,管道中的處理器鏈表為:head(outbound)-> EchoClientHandler (inbound)->tail(inbound)声怔。管道從創(chuàng)建到現(xiàn)在這段時間內(nèi),處理器鏈表的變化歷史為:head->tail舱呻,head->ChannelInitializer(inbound)->tail醋火,head-> EchoClientHandler (inbound)->tail
9悠汽、doConnect0任務(wù)會觸發(fā)connect事件,connect是一個Outbound事件芥驳,headHandler通過調(diào)用AbstractNioUnsafe的方法向服務(wù)端發(fā)起connect請求柿冲,并設(shè)置ops為SelectionKey.OP_CONNECT
10、客戶端線程N(yùn)ioEventLoop中的select接收到connect事件后兆旬,將SelectionKey.OP_CONNECT從ops中移除假抄,然后調(diào)用finishConnect方法完成連接的建立。到此丽猬,connect就正式建立了宿饱。
11、最后觸發(fā)ChannelActive事件脚祟。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刑棵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子愚铡,更是在濱河造成了極大的恐慌,老刑警劉巖胡陪,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沥寥,死亡現(xiàn)場離奇詭異,居然都是意外死亡柠座,警方通過查閱死者的電腦和手機(jī)邑雅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妈经,“玉大人淮野,你說我怎么就攤上這事〈蹬荩” “怎么了骤星?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長爆哑。 經(jīng)常有香客問我洞难,道長,這世上最難降的妖魔是什么揭朝? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任队贱,我火速辦了婚禮,結(jié)果婚禮上潭袱,老公的妹妹穿的比我還像新娘柱嫌。我一直安慰自己,他們只是感情好屯换,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布编丘。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瘪吏。 梳的紋絲不亂的頭發(fā)上癣防,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機(jī)與錄音掌眠,去河邊找鬼蕾盯。 笑死,一個胖子當(dāng)著我的面吹牛蓝丙,可吹牛的內(nèi)容都是我干的级遭。 我是一名探鬼主播渺尘,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼挫鸽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鸥跟?” 一聲冷哼從身側(cè)響起丢郊,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎医咨,沒想到半個月后枫匾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拟淮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年干茉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片很泊。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡角虫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出委造,到底是詐尸還是另有隱情戳鹅,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布昏兆,位于F島的核電站粉楚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏亮垫。R本人自食惡果不足惜模软,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饮潦。 院中可真熱鬧燃异,春花似錦、人聲如沸继蜡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至仅颇,卻和暖如春单默,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背忘瓦。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工搁廓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耕皮。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓境蜕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親凌停。 傳聞我的和親對象是個殘疾皇子粱年,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容