国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式

這篇具有很好參考價(jià)值的文章主要介紹了【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式。希望對大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

人其實(shí)很難抵制誘惑,人只能遠(yuǎn)離誘惑,所以千萬不要高看自己的定力。
【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor


一、LT和ET模式

1.理解LT和ET的工作原理

1.
多路轉(zhuǎn)接接口select poll epoll所做的工作其實(shí)都是事件通知,只向上層通知事件到來,處理就緒事件的工作并不由這些API來完成,這些接口在進(jìn)行事件通知時(shí),有沒有自己的策略呢?
其實(shí)是有的,在網(wǎng)絡(luò)編程中,select poll 只支持LT工作模式,而epoll除了LT工作模式外,還支持ET工作模式,不同的工作模式對應(yīng)著不同的就緒事件通知策略,LT模式是這些IO接口的默認(rèn)工作模式,ET模式是epoll的高效工作模式。

2.
下面來舉一個(gè)例子幫助大家理解ET和LT模式的區(qū)別(送快遞的例子)
新上任的快遞員小李要給學(xué)24宿舍樓的張三送快遞,張三買了很多的快遞,估摸著有6-7個(gè)快遞,小李到了學(xué)24的樓底,然后就給樓上的張三打電話,通知張三下來拿快遞,但是張三正在和他的狐朋狗友開黑打游戲呢,于是張三就嘴上答應(yīng)著我馬上下去,但始終就不下去,老實(shí)人小李見張三遲遲不下來拿快遞,又給張三打電話,讓張三下來拿快遞,但張三嘴上又說,我馬上下去拿快遞,真的馬上,但過了一會(huì)兒張三依舊還是不下來,小李又只能給張三打電話,張三啊,你的快遞到了,你趕快下來取快遞吧,終于張三和自己的狐朋狗友推完對面的水晶了,下樓來取快遞了,但是張三一個(gè)人只拿走了3個(gè)快遞,還剩下三個(gè)快遞,張三也沒辦法了,張三一個(gè)人一次只能拿這么多快遞啊,于是張三就拿著他的三個(gè)快遞上樓了,繼續(xù)和他的舍友開黑打游戲。結(jié)果沒一會(huì)兒,小李又給張三打電話,說張三啊,你的快遞沒拿完呢,你買了6樣?xùn)|西,你只拿了3樣,還剩3個(gè)包裹你沒拿呢,張三又嘴上說,好的好的,我馬上下去拿,但其實(shí)又重復(fù)著前面的動(dòng)作,好一會(huì)兒才下樓拿走了剩余的3個(gè)包裹,當(dāng)包裹全部被拿走之后,小李才不會(huì)給張三打電話了。
老油條快遞員小王恰巧也要給學(xué)24宿舍樓的張三送快遞,恰巧的是,張三這次又買了6個(gè)快遞,所以小王也碰巧要給張三送6個(gè)包裹。小王到了張三樓底下,給張三打了一個(gè)電話,說 張三啊,我只給你打一次電話,你現(xiàn)在要是不下來取快遞,我后面是不會(huì)給你打電話的,除非你又買了新的快遞,我手上你的快遞數(shù)量變多的時(shí)候,我才會(huì)稍微好心的再給你打一個(gè)電話,否則其他情況下,我只會(huì)打一次,你要是不下來取快遞,那我就不管你了,我給其他客戶送快遞去了。張三一聽,這不行啊,我要是現(xiàn)在不下來取快遞,這個(gè)快遞員以后就不給我打電話了,那我下樓找不到快遞員,拿不到我的快遞怎么辦,所以張三就立馬下樓取快遞去了。張三一次拿不了這么多快遞啊,但張三又不能漏下一些快遞,因?yàn)樾⊥跸乱淮尾粫?huì)再給張三打電話了,所以張三剛到樓上放下手中的三個(gè)快遞,又立馬返回樓下取走剩余的三個(gè)快遞了。

3.
在上面的這兩個(gè)例子中,其實(shí)小李的工作模式就是水平觸發(fā)Level Triggered模式,簡稱LT模式,小王的工作模式就是邊緣觸發(fā)Edge Triggered模式,簡稱ET模式,也是多路轉(zhuǎn)接接口高效的模式。
LT對應(yīng)epoll的工作方式就是,當(dāng)epoll檢測到sock上有就緒的事件時(shí),epoll_wait會(huì)立馬返回通知程序員事件就緒了,程序員可以選擇只讀取sock緩沖區(qū)的部分?jǐn)?shù)據(jù),剩下的數(shù)據(jù)暫時(shí)不讀了,等下次調(diào)用recv的時(shí)候再讀取sock緩沖區(qū)中的剩余數(shù)據(jù),下次怎么調(diào)用recv呢?當(dāng)然也是通過epoll_wait通知然后再進(jìn)行調(diào)用啦,所以只要sock中的數(shù)據(jù)程序員沒有一次性拿走,那么后續(xù)再調(diào)用epoll_wait時(shí),epoll_wait依舊會(huì)進(jìn)行就緒事件的通知,告訴程序員來讀取sock中的剩余數(shù)據(jù),而這樣的方式就是LT模式,即只要底層有數(shù)據(jù)沒讀完,后續(xù)epoll_wait返回時(shí)就會(huì)一直通知用戶讀取數(shù)據(jù)。
而ET對應(yīng)的工作方式是,如果底層有數(shù)據(jù)沒讀完,后續(xù)epoll_wait不會(huì)通知程序員事件就緒了,只有當(dāng)?shù)讓訑?shù)據(jù)增多的時(shí)候,epoll_wait才會(huì)再通知一次程序員,否則epoll_wait只會(huì)通知一次。

2.通過代碼來觀察LT和ET工作模式的不同

1.
在前一篇文章中我們寫過epoll_server,當(dāng)然epoll_server的默認(rèn)工作模式也是LT模式,在下面的代碼中我將處理就緒事件的接口HandlerEvent( )屏蔽掉了,當(dāng)客戶端連接到來時(shí),服務(wù)器的epoll_wait一定會(huì)檢測到listensock上的讀事件就緒了,所以epoll_wait會(huì)返回,告知程序員要處理數(shù)據(jù)了,但如果程序員一直不處理數(shù)據(jù)的話,那epoll_wait每次都會(huì)告知程序員要處理數(shù)據(jù)了,所以從顯示器的輸出結(jié)果來看,epoll_wait返回后,根據(jù)返回值n,一定是進(jìn)入到了default分支中,并且每次epoll_wait都會(huì)告知程序員事件就緒,所以顯示器會(huì)一直瘋狂打印have events ready,因?yàn)橹灰讓佑惺录途w,對于listensock來說,只要內(nèi)核監(jiān)聽隊(duì)列有就緒的連接,那就是就緒,epoll_wait就會(huì)一直通知程序員事件就緒了,趕快處理吧。(就像小李一樣,只要張三不拿走快遞,小李就會(huì)一直給張三打電話)

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

2.
在添加listensock到epoll底層的紅黑樹中時(shí),不僅僅關(guān)心listensock的讀事件,同時(shí)還讓listensock的工作模式是ET,只要將EPOLLIN和EPOLLET按位或即可。
所以當(dāng)連接到來時(shí),可以看到服務(wù)器只會(huì)打印一次have event ready,只要沒有新連接到來,那么epoll_wait只會(huì)通知程序員一次事件就緒,除非到來了新連接,那就說明內(nèi)核監(jiān)聽隊(duì)列中就緒的連接變多了,換言之就是listensock底層的數(shù)據(jù)變多了,此時(shí)epoll_wait才會(huì)再好心提醒一次程序員,事件就緒了,你趕快處理吧。反過來就是,只要后續(xù)listensock底層的數(shù)據(jù)沒有增多,那么epoll_wait就不會(huì)在通知程序員了。
而由于我們設(shè)置的timeout是阻塞式等待,所以你可以看到,只要沒有新連接到來,服務(wù)器就會(huì)阻塞住,epoll_wait調(diào)用不會(huì)再返回,也就不會(huì)再通知程序員。而反觀LT模式,雖然每次epoll_wait都是阻塞式等待,但epoll_wait每次都會(huì)返回,每次都會(huì)告知程序員,這就是兩者的不同。邊緣觸發(fā)只會(huì)觸發(fā)一次,水平觸發(fā)會(huì)一直觸發(fā)

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

3.ET模式高效的原因(fd必須是非阻塞的)

1.
為什么ET模式是高效的呢?這是非常重要的一個(gè)面試題,許多的面試官在問到網(wǎng)絡(luò)環(huán)節(jié)時(shí),都會(huì)讓我們講一下select poll epoll各自的用法,epoll的底層原理,三個(gè)接口的優(yōu)缺點(diǎn),還有就是epoll的兩種工作模式,以及ET模式高效的原因,ET模式高效的原因也是一個(gè)高頻的問題。

2.
ET模式下,只有底層數(shù)據(jù)從無到有,從有到多的時(shí)候,才會(huì)通知上層一次,通知的機(jī)制就是rbtree+ready_queue+cb,所以ET這種通知機(jī)制就會(huì)倒逼程序員一次將底層的數(shù)據(jù)全部讀走,如果不一次讀走,就可能造成數(shù)據(jù)丟失,你無法保證對方一定會(huì)繼續(xù)給你發(fā)數(shù)據(jù)啊,如果無法保證這點(diǎn),那就無法保證epoll_wait還會(huì)通知你下一次,如果無法保證這一點(diǎn),那就有可能你只讀取了sock的部分?jǐn)?shù)據(jù),但后續(xù)epoll_wait可能不會(huì)再通知你了,從而導(dǎo)致后續(xù)的數(shù)據(jù)你永遠(yuǎn)都讀不上來了,所以你必須一次將底層的數(shù)據(jù)全部讀走。
如何保證一次將底層的數(shù)據(jù)全部讀走呢?那就只能循環(huán)讀取了,如果只調(diào)用recv一次,是無法保證一次將底層的數(shù)據(jù)全部讀走的。所以我們可以打個(gè)while循環(huán)一直讀sock接收緩沖區(qū)中的數(shù)據(jù),直到讀取不上來數(shù)據(jù),但這里其實(shí)就又有一個(gè)問題了,如果sock是阻塞的,循環(huán)讀讀到最后一定會(huì)沒數(shù)據(jù),而此時(shí)由于sock是阻塞的,那么服務(wù)器就會(huì)阻塞在最后一次的recv系統(tǒng)調(diào)用處,直到有數(shù)據(jù)到來,而此時(shí)服務(wù)器就會(huì)被掛起,服務(wù)器一旦被掛起,那就完蛋了~
服務(wù)器被掛起,那就無法運(yùn)行了,無法給客戶提供服務(wù)了,這就很有可能造成很多公司盈利上的損失,所以服務(wù)器一定不能停下來,更不能被掛起,需要一直運(yùn)行,以便給客戶提供服務(wù)。而如果使用非阻塞文件描述符,當(dāng)recv讀取不到數(shù)據(jù)時(shí),recv會(huì)返回-1,同時(shí)錯(cuò)誤碼被設(shè)置為EAGAIN和EWOULDBLOCK,這倆錯(cuò)誤碼的值是一樣的,此時(shí)就可以判斷出,我們一次把底層的數(shù)據(jù)全部都讀走了。
所以在工程實(shí)踐上,epoll以ET模式工作時(shí),文件描述符必須設(shè)置為非阻塞,防止服務(wù)器由于等待某種資源就緒從而被掛起。

3.
解釋完ET模式下fd必須是非阻塞的原因后,那為什么ET模式是高效的呢?可能有人會(huì)說,因?yàn)镋T模式只會(huì)通知一次,倒逼程序員將數(shù)據(jù)一次全部讀走,所以ET模式就是高效的,如果這個(gè)問題滿分100分的話,你這樣的回答只能得到20分,因?yàn)槟愕幕卮鹌鋵?shí)僅僅只是答案的引線,真正最重要的部分你還是沒說出來。
倒逼程序員一次將數(shù)據(jù)全部讀走,那不就是讓上層盡快取走數(shù)據(jù)嗎?盡快取走數(shù)據(jù)后,就可以給對方發(fā)送一個(gè)更大的16位窗口大小,讓對方更新出更大的滑動(dòng)窗口大小,提高底層數(shù)據(jù)發(fā)送的效率,更好的使用TCP延遲應(yīng)答,滑動(dòng)窗口等策略?。。∵@才是ET模式高效的最本質(zhì)的原因?。?!
因?yàn)镋T模式可以更好的利用TCP提高數(shù)據(jù)發(fā)送效率的種種策略,例如延遲應(yīng)答,滑動(dòng)窗口等。
之前在講TCP的時(shí)候,TCP報(bào)頭有個(gè)字段叫做PSH,其實(shí)這個(gè)字段如果被設(shè)置的話,epoll_wait就會(huì)將此字段轉(zhuǎn)換為通知機(jī)制,再通知一次上層,讓其盡快讀走數(shù)據(jù)。

4.LT和ET模式使用時(shí)的讀取方式

1.
在LT模式下,如果fd是阻塞的,那么上次只能讀一次,這是出于工程需求,因?yàn)槲覀儾荒茏尫?wù)器阻塞掛起,而在文件描述符是阻塞的情況下,如果我們進(jìn)行循環(huán)讀,則最后一次肯定會(huì)讀取不到數(shù)據(jù),那么此時(shí)服務(wù)器進(jìn)程就會(huì)阻塞住,等待fd的skbuff中到來數(shù)據(jù),但服務(wù)器是不能被阻塞掛起的,所以我們只能讀取一行。
如果fd是非阻塞的,那其實(shí)就不用擔(dān)心了,我們進(jìn)行循環(huán)讀就可以,這樣是比較高效的,因?yàn)樵诜亲枞沂荓T工作模式的情況下,無論我們是一行讀還是循環(huán)讀服務(wù)器都是不會(huì)被阻塞掛起的。對于讀一次來說,在LT模式下也是不會(huì)出問題的,因?yàn)橹灰猻kbuff中有數(shù)據(jù),那么epoll_wait就會(huì)一直通知程序員來盡快取走數(shù)據(jù),我們不用擔(dān)心丟失數(shù)據(jù)的情況發(fā)生。
在ET模式下,fd必須是非阻塞的,因?yàn)槌鲇诠こ虒?shí)踐的角度考慮,為了讓數(shù)據(jù)被程序員完整的拿到,我們只能進(jìn)行循環(huán)讀,而只要你進(jìn)行循環(huán)讀,fd萬萬就不能是阻塞的,因?yàn)檠h(huán)讀的最后一次讀取一定會(huì)讀不到數(shù)據(jù),只要讀不到數(shù)據(jù),且fd是阻塞的,那么服務(wù)器就被掛起了,這并不是我們想要看到的結(jié)果,所以在ET模式下,沒得商量,fd必須是非阻塞的,同時(shí)程序員在應(yīng)用層讀取數(shù)據(jù)的方式也必須是循環(huán)讀,不可以讀一行。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

二、Reactor

1.tcpServer.hpp

1.1 連接結(jié)構(gòu)體

1.
我們知道socket套接字在通信的時(shí)候,每個(gè)sock在內(nèi)核都會(huì)創(chuàng)建接收緩沖區(qū)和發(fā)送緩沖區(qū),這樣的緩沖區(qū)常常開辟在堆上,不會(huì)像臨時(shí)變量char buffer[1024]隨著棧幀空間的銷毀而銷毀,這能更好的存儲(chǔ)網(wǎng)絡(luò)中收到的數(shù)據(jù) 和 即將要發(fā)送到網(wǎng)絡(luò)中的數(shù)據(jù),如果用棧上的空間來存儲(chǔ)網(wǎng)絡(luò)收發(fā)的數(shù)據(jù),則數(shù)據(jù)極有可能被銷毀掉,因?yàn)橹灰兞克跅N毀,則變量中的數(shù)據(jù)在下次變量重新開辟時(shí),就會(huì)由原來存儲(chǔ)的網(wǎng)絡(luò)數(shù)據(jù)變?yōu)槲闯跏蓟^的隨機(jī)數(shù)據(jù)了。
所以為了讓每個(gè)sock都有自己的收發(fā)緩沖區(qū),我們不再使用原來編寫服務(wù)器時(shí),用一個(gè)char buffer[1024]來存儲(chǔ)sock上的網(wǎng)絡(luò)數(shù)據(jù),而是改用一個(gè)Connection結(jié)構(gòu)體來代表一個(gè)通信的sock,這個(gè)結(jié)構(gòu)體內(nèi)部包含通信的套接字描述符_sock,以及sock所對應(yīng)的_inbuffer和_outbuffer。
除此之外,該結(jié)構(gòu)體還包括了三個(gè)回調(diào)方法_recver,_sender,_excepter,分別表示sock對應(yīng)的讀方法,寫方法,異常方法,func_t是一個(gè)包裝器類型,包裝內(nèi)容為函數(shù)指針,返回值是void,參數(shù)是Connection指針類型,這三個(gè)參數(shù)其實(shí)就是Reactor反應(yīng)堆模式的神來之筆所在,后面總結(jié)Reactor時(shí),就知道為什么要這么設(shè)計(jì)Connection了,同時(shí)也知道為什么Reactor叫反應(yīng)堆模式了。實(shí)現(xiàn)Reactor網(wǎng)絡(luò)庫,這個(gè)Connection是關(guān)鍵所在。
該結(jié)構(gòu)體還包括了一個(gè)額外的服務(wù)器類型的指針,在某些場景下,比如Connection結(jié)構(gòu)體和TcpServer服務(wù)器兩個(gè)類是分文件的,此時(shí)如果在Connection的回調(diào)方法中,想要調(diào)用一下TcpServer類中的方法時(shí),這個(gè)回指指針會(huì)幫我們拿到TcpServer中的方法,今天我們是不需要的,因?yàn)榻裉靸蓚€(gè)類都放到了tcpServer.hpp中
Connection還實(shí)現(xiàn)了兩個(gè)函數(shù),一個(gè)是注冊函數(shù),一個(gè)是關(guān)閉sock的函數(shù),注冊函數(shù)用于將外部實(shí)現(xiàn)的sock對應(yīng)的讀方法,寫方法,異常方法,注冊到sock所在的結(jié)構(gòu)體Connection中。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

1.2 初始化服務(wù)器

1.
initServer接口還是先將listensock創(chuàng)建出來,將服務(wù)器的ip地址和port端口號(hào)都bind綁定好,然后設(shè)置服務(wù)器為監(jiān)聽狀態(tài),既然是Reactor網(wǎng)絡(luò)庫,則使用的多路轉(zhuǎn)接接口一定是epoll,所以還需要調(diào)用epoll_create創(chuàng)建epoll模型,與sock相同的是,今天的epoll所對應(yīng)的接口,我們也做了封裝,將其單獨(dú)實(shí)現(xiàn)到epoller.hpp中,作為一個(gè)組件來使用。
當(dāng)服務(wù)器開始運(yùn)行時(shí),一定會(huì)有大量的Connection結(jié)構(gòu)體對象需要被new出來,那么這些結(jié)構(gòu)體對象需不需要被管理呢?當(dāng)然是需要的,所以在服務(wù)器類里面,定義了一個(gè)哈希表_connections,用sock來作為哈希表的鍵值,sock對應(yīng)的結(jié)構(gòu)體connection和sock一起作為鍵值對,也就是哈希桶中存儲(chǔ)的值(存儲(chǔ)鍵值對<sock, connection>),今天是不會(huì)出現(xiàn)哈希沖突的,所以每個(gè)鍵值下面的哈希桶只會(huì)掛一個(gè)鍵值對,即一個(gè)<sock, connection>.
初始化服務(wù)器時(shí),第一個(gè)需要被添加到哈希表中的sock,一定是listensock,所以在initServer方法中,先把listensock添加到哈希表里面,添加的同時(shí)還要傳該listensock所對應(yīng)的關(guān)心事件的方法,對于listensock來說,只需要關(guān)注讀方法即可,其他兩個(gè)方法設(shè)為nullptr即可。

2.
在AddConnection中,要判斷events是否有EPOLLET,如果有,則文件描述符必須是非阻塞,所以要將sock設(shè)置為非阻塞,設(shè)置的方式也簡單,只要通過fcntl來實(shí)現(xiàn)即可,同樣的,我們把fcntl也封裝成了一個(gè)SetNonBlock()方法來使用。Reactor中epoll的工作模式是ET,這也是Reactor網(wǎng)絡(luò)庫高效的原因。
接下來就是new一個(gè)連接結(jié)構(gòu)體,然后將結(jié)構(gòu)體的字段填充好,比如設(shè)置好回調(diào)方法的值,結(jié)構(gòu)體中的文件描述符值等等。連接結(jié)構(gòu)體創(chuàng)建好后,我們還需要調(diào)用封裝好的AddEvent接口,將sock及其關(guān)心的事件交給epoll來監(jiān)視,最后別忘了把new好的結(jié)構(gòu)體交給哈希表來管理。

3.
在代碼實(shí)現(xiàn)上,給AddConnection傳參時(shí),用到了一個(gè)C++11的知識(shí),就是bind綁定的使用,一般情況下,如果你將包裝器包裝的函數(shù)指針類型傳參給包裝器類型時(shí),是沒有任何問題的,因?yàn)榘b器本質(zhì)就是一個(gè)仿函數(shù),內(nèi)部調(diào)用了被包裝的對象的方法,所以傳參是沒有任何問題的。
但如果你要是在類內(nèi)傳參,那就有問題了,會(huì)出現(xiàn)類型不匹配的問題,這個(gè)問題真的很惡心,而且這個(gè)問題一報(bào)錯(cuò)就劈里啪啦的報(bào)一大堆錯(cuò),因?yàn)閒unction是模板,C++報(bào)錯(cuò)最惡心的就是模板報(bào)錯(cuò),一報(bào)錯(cuò)人都要炸了。話說回來,為什么是類型不匹配呢?因?yàn)樵陬悆?nèi)調(diào)用類內(nèi)方法時(shí),其實(shí)是通過this指針來調(diào)用的,如果你直接將Accepter方法傳給AddConnection,兩者類型是不匹配的,因?yàn)锳ccepter的第一個(gè)參數(shù)是this指針,正確的做法是利用包裝器的適配器bind來進(jìn)行傳參,bind將Accepter進(jìn)行綁定,前兩個(gè)參數(shù)為綁定的對象類型 和 給綁定的對象所傳的參數(shù),因?yàn)锳ccepter第一個(gè)參數(shù)是this指針,所以第一個(gè)參數(shù)就可以固定傳this,后面的一個(gè)參數(shù)不應(yīng)該是現(xiàn)在傳,而應(yīng)該是調(diào)用Accepter方法的時(shí)候再傳,只有這樣才能在類內(nèi)將類成員函數(shù)指針傳給包裝器類型。
不過吧還有一種不常用的方法,就是利用lambda表達(dá)式來進(jìn)行傳參,lambda可以捕捉上下文的this指針,然后再把lambda類型傳給包裝器類型,這種方式不常用,用起來也怪別扭的,function和bind是適配模式,兩者搭配在一起用還是更順眼一些,lambda這種方式了解一下就好。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

1.3 事件派發(fā)器

1.
事件派發(fā)器是真正服務(wù)器要開始運(yùn)行了,服務(wù)器會(huì)將就緒的每個(gè)連接都進(jìn)行處理,首先如果連接不在哈希表中,那就說明這個(gè)連接中的sock還沒有被添加到epoll模型中的紅黑樹,不能直接進(jìn)行處理,需要先添加到紅黑樹中,然后讓epoll_wait來拿取就緒的連接再告知程序員,這個(gè)時(shí)候再進(jìn)行處理,這樣才不會(huì)等待,而是直接進(jìn)行數(shù)據(jù)拷貝。
Loop中處理就緒的事件的方法非常非常的簡單,如果該就緒的fd關(guān)心的是讀事件,那就直接調(diào)用該sock所在連接結(jié)構(gòu)體內(nèi)部的讀方法即可,如果是寫事件那就調(diào)用寫方法即可。有人說那如果fd關(guān)心異常事件呢?其實(shí)異常事件大部分也都是讀事件,不過也有寫事件,所以處理異常的邏輯我們直接放到讀方法和寫方法里面即可,當(dāng)有異常事件到來時(shí),直接去對應(yīng)的讀方法或?qū)懛椒ɡ锩鎴?zhí)行對應(yīng)的邏輯即可。
假設(shè)某個(gè)異常事件發(fā)生了,那么這個(gè)異常事件會(huì)自動(dòng)被內(nèi)核設(shè)置到epoll_wait返回的事件集中,這個(gè)異常事件一定會(huì)和一個(gè)sock關(guān)聯(lián),比如客戶端和服務(wù)器用sock通信著,突然客戶端關(guān)閉連接,那么服務(wù)器的sock上原本關(guān)心著讀事件,此時(shí)內(nèi)核會(huì)自動(dòng)將異常事件設(shè)置到該sock關(guān)心的事件集合里,在處理sock關(guān)心的讀事件時(shí),讀方法會(huì)捎帶處理掉這個(gè)異常事件,處理方式為服務(wù)器關(guān)閉通信的sock,因?yàn)榭蛻舳艘呀?jīng)把連接斷開了,服務(wù)器沒必要維護(hù)和這個(gè)客戶端的連接了,服務(wù)器也斷開就好,這樣的邏輯在讀方法里面就可以實(shí)現(xiàn)。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

2.
像下面這樣的事件派發(fā)器就是典型的Reactor反應(yīng)堆模式,當(dāng)連接到來時(shí),直接調(diào)用對應(yīng)的sock所在Connection中的回調(diào)方法來進(jìn)行處理即可,這就像是化學(xué)反應(yīng)一樣,當(dāng)連接請求或通信的網(wǎng)絡(luò)數(shù)據(jù)到來時(shí),代碼就像產(chǎn)生化學(xué)反應(yīng)一樣,自動(dòng)調(diào)用連接對應(yīng)的listensock 或 通信對應(yīng)的sock所在的方法進(jìn)行處理即可,就像是一個(gè)化學(xué)反應(yīng)堆,這也是為什么這樣的網(wǎng)絡(luò)庫叫Reactor的原因,因?yàn)槊總€(gè)sock都有自己對應(yīng)的讀 寫 異常方法。
listensock對應(yīng)的_recver方法就是Accepter函數(shù),通信sock對應(yīng)的_recver方法就是Recver函數(shù),通信sock對應(yīng)的_sender方法就是Sender函數(shù)。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

1.4 回調(diào)函數(shù)

1.
當(dāng)listensock底層有連接到來時(shí),epoll_wait告知程序員有事件到來后,則應(yīng)該調(diào)用listensock對應(yīng)的_recver回調(diào)方法,這個(gè)回調(diào)方法,在將listensock添加到連接結(jié)構(gòu)體時(shí),我們就已經(jīng)將Accepter綁定給listensock的_recver回調(diào)方法了。
進(jìn)入Accepter之后就開始讀取listensock的底層連接了,但你能保證一次就把listensock底層的數(shù)據(jù)全部讀取上來嗎?你accept系統(tǒng)調(diào)用,一次最多就只能拿取一個(gè)連接,萬一listensock底層有很多連接呢?今天epoll是ET模式,如果你只讀取一次的話,且恰好后面沒有新連接到來呢?那沒有被拿取上來的連接所對應(yīng)的客戶端就無法和服務(wù)器通信了,這個(gè)問題就是你服務(wù)器產(chǎn)生的,我客戶端和你好好的通信著,結(jié)果你服務(wù)器不受理我的連接請求,那就說明你服務(wù)器代碼有bug。
所以在Accepter中必須循環(huán)讀取listensock底層的數(shù)據(jù),確保一次將listensock底層的數(shù)據(jù)全部讀走,所以Accepter中必須得打死循環(huán)進(jìn)行讀取,循環(huán)讀我們也不怕服務(wù)器被掛起,因?yàn)镋T模式下所有的文件描述符都被我們設(shè)置成了非阻塞,當(dāng)accept拿上來通信的連接后,下一步要做的就是將這個(gè)連接添加到_connections哈希表中,在AddConnection中,會(huì)構(gòu)建sock對應(yīng)的connection結(jié)構(gòu)體,然后將結(jié)構(gòu)體中的字段填充好,將回調(diào)方法設(shè)置到結(jié)構(gòu)體的成員變量里面,另外AddConnection中還會(huì)將sock和其關(guān)心的事件設(shè)置到epoll模型的紅黑樹當(dāng)中,讓epoll幫忙監(jiān)視程序員所關(guān)心的fd的就緒情況。
對于listensock來講,只關(guān)心讀事件,所以在給AddConnection傳參的時(shí)候,后兩個(gè)方法就不傳了,但對于通信的sock來講,后兩個(gè)方法將來也是要調(diào)用的,所以也要傳,這里在傳參的時(shí)候,由于參數(shù)是成員函數(shù),所以也要使用bind固定參數(shù)的方式來進(jìn)行傳參。
當(dāng)accept系統(tǒng)調(diào)用返回值小于0,同時(shí)錯(cuò)誤碼被設(shè)置為EAGAIN或EWOULDBLOCK時(shí),則說明accept已經(jīng)將本輪listensock下就緒的數(shù)據(jù)全部讀完了,此時(shí)就可以break跳出死循環(huán)了。如果錯(cuò)誤碼被設(shè)置為EINTR,則說明進(jìn)程可能由于執(zhí)行某種到來信號(hào)對應(yīng)的handler方法,導(dǎo)致這里的accept系統(tǒng)調(diào)用被中斷,則此時(shí)應(yīng)該繼續(xù)循環(huán)讀取listensock底層的數(shù)據(jù),所以直接continue即可,還有另一種可能,就是accept系統(tǒng)調(diào)用真的出錯(cuò)了,此時(shí)的做法也break跳出循環(huán)即可。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

2.
Recver這里還是和之前一樣的問題,也是前面在寫三個(gè)多路轉(zhuǎn)接接口服務(wù)器時(shí),一直沒有處理的問題,你怎么保證你一次就把所有數(shù)據(jù)全部都讀上來了呢?如果不能保證,那就和Accepter一樣,必須打死循環(huán)來進(jìn)行讀取,當(dāng)recv返回值大于0,那我們就把讀取到的數(shù)據(jù)先放入緩沖區(qū),緩沖區(qū)在哪里呢?其實(shí)就在conn參數(shù)所指向的結(jié)構(gòu)體里面,結(jié)構(gòu)體里會(huì)有sock所對應(yīng)的收發(fā)緩沖區(qū)。然后就調(diào)用外部傳入的回調(diào)函數(shù)_service,對服務(wù)器收到的數(shù)據(jù)進(jìn)行應(yīng)用層的業(yè)務(wù)邏輯處理。
當(dāng)recv讀到0時(shí),說明客戶端把連接關(guān)了,那這就算異常事件,直接回調(diào)sock對應(yīng)的異常處理方法即可。
當(dāng)recv的返回值小于0,同時(shí)錯(cuò)誤碼被設(shè)置為EAGAIN或EWOULDBLOCK時(shí),則說明recv已經(jīng)把sock底層的數(shù)據(jù)全部讀走了,則此時(shí)直接break跳出循環(huán)即可,也有可能是被信號(hào)給中斷了,則此時(shí)應(yīng)該繼續(xù)執(zhí)行循環(huán),另外一種情況就是recv系統(tǒng)調(diào)用真的出錯(cuò)了,則此時(shí)也調(diào)用sock的異常方法進(jìn)行處理即可。
業(yè)務(wù)邏輯處理方法應(yīng)該在本次循環(huán)讀取到所有的數(shù)據(jù)之后再進(jìn)行處理。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

3.
之前寫服務(wù)器時(shí),我們從來沒處理過寫事件,寫事件和讀事件不太一樣,關(guān)心讀事件是要常設(shè)置的,但寫事件一般都是就緒的,因?yàn)閮?nèi)核發(fā)送緩沖區(qū)大概率都是有空間的,如果每次都要讓epoll幫我們關(guān)心讀事件,這其實(shí)是一種資源的浪費(fèi),因?yàn)榇蟛糠智闆r下,你send數(shù)據(jù),都是會(huì)直接將應(yīng)用層數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)的,不會(huì)出現(xiàn)等待的情況,而recv就不太一樣,recv在讀取的時(shí)候,有可能數(shù)據(jù)還在網(wǎng)絡(luò)里面,所以recv要等待的概率是比較高的,所以對于讀事件來說,常常都要將其設(shè)置到sock所關(guān)心的事件集合中。
但寫事件并不是這樣的,寫事件應(yīng)該是偶爾設(shè)置到關(guān)心集合中,比如你這次沒把數(shù)據(jù)一次性發(fā)完,但你又沒設(shè)置該sock關(guān)心寫事件,當(dāng)下次寫事件就緒了,也就是內(nèi)核發(fā)送緩沖區(qū)有空間了,epoll_wait也不會(huì)通知你,那你還怎么發(fā)送剩余數(shù)據(jù)啊,所以這個(gè)時(shí)候你就應(yīng)該設(shè)置寫事件關(guān)心了,讓epoll_wait幫你監(jiān)視sock上的寫事件,以便于下次epoll_wait通知你時(shí),你還能夠繼續(xù)發(fā)送上次沒發(fā)完的數(shù)據(jù)。
這個(gè)時(shí)候可能有人會(huì)問,ET模式不是只會(huì)通知一次嗎?如果我這次設(shè)置了寫關(guān)心,但下次發(fā)送數(shù)據(jù)的時(shí)候,還是沒發(fā)送完畢(因?yàn)閮?nèi)核發(fā)送緩沖區(qū)可能沒有剩余空間了),那后面ET模式是不是就不會(huì)通知我了呀,那我還怎么繼續(xù)發(fā)送剩余的數(shù)據(jù)呢?ET模式在底層就緒的事件狀態(tài)發(fā)生變化時(shí),還會(huì)再通知上層一次的,對于讀事件來說,當(dāng)數(shù)據(jù)從無到有,從有到多狀態(tài)發(fā)生變化時(shí),ET就還會(huì)通知上層一次,對于寫事件來說,當(dāng)內(nèi)核發(fā)送緩沖區(qū)剩余空間從無到有,從有到多狀態(tài)發(fā)生變化時(shí),ET也還會(huì)通知上層一次,所以不用擔(dān)心數(shù)據(jù)發(fā)送不完的問題產(chǎn)生,因?yàn)镋T是會(huì)通知我們的。
在循環(huán)外,我們只需要通過判斷outbuffer是否為空的情況,來決定是否要設(shè)置寫事件關(guān)心,當(dāng)數(shù)據(jù)發(fā)送完了那我們就取消對于寫事件的關(guān)心,不占用epoll的資源,如果數(shù)據(jù)沒發(fā)送完,那就設(shè)置對于寫事件的關(guān)心,因?yàn)槲覀円WC下次寫事件就緒時(shí),epoll_wait能夠通知我們對寫事件進(jìn)行處理。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

4.
下面是異常事件的處理方法,我們統(tǒng)一對所有異常事件,都先將其從epoll模型中移除,然后關(guān)閉文件描述符,最后將conn從哈希表_connecions中移除。
值得注意的是,conn指針指向的連接結(jié)構(gòu)體空間,必須由我們自己釋放,有人說,為什么???你哈希表不是都已經(jīng)erase了么?為什么還要程序員自己再delete連接結(jié)構(gòu)體空間呢?
這里要給大家說明一點(diǎn)的是,所有的容器在erase的時(shí)候,都只釋放容器自己所new出來的空間,像哈希表這樣的容器,它會(huì)new一個(gè)存儲(chǔ)鍵值對的節(jié)點(diǎn)空間,節(jié)點(diǎn)里面存儲(chǔ)著conn指針和sockfd,當(dāng)調(diào)用哈希表的erase時(shí),哈希表只會(huì)釋放它自己new出來的節(jié)點(diǎn)空間,至于這個(gè)節(jié)點(diǎn)空間里面存儲(chǔ)了一個(gè)Connection類型的指針,并且這個(gè)指針變量指向一個(gè)結(jié)構(gòu)體空間,這些事情哈希表才不會(huì)管你呢,容器只會(huì)釋放他自己開辟的空間,哈希表是vector掛單鏈表的方式來實(shí)現(xiàn)的。
所以我們要自己手動(dòng)釋放conn指向的空間,如果你不想自己手動(dòng)釋放conn指向的堆空間資源,則可以存儲(chǔ)智能指針對象,這樣在哈希表erase時(shí),就會(huì)釋放智能指針對象的空間,從而自動(dòng)調(diào)用Connection類的析構(gòu)函數(shù)。
這樣搞起來其實(shí)還是很麻煩的,所以我們就自己手動(dòng)釋放就好了,如果不手動(dòng)釋放那就會(huì)造成內(nèi)存泄露。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor
5.
下面這篇文章的第五部分的第二個(gè)標(biāo)題,講述了編譯器默認(rèn)生成的析構(gòu)函數(shù)對于對象的成員變量的處理策略,對于內(nèi)置類型不處理,對于自定義類型會(huì)調(diào)用該類的析構(gòu)函數(shù)。
值得注意的是,就算是自定義類型的指針,他其實(shí)也是被編譯器當(dāng)作內(nèi)置類型了,并不會(huì)調(diào)用指針類型的析構(gòu)函數(shù)。
當(dāng)析構(gòu)函數(shù)被調(diào)用完畢之后,delete的目標(biāo)堆空間就會(huì)被釋放,歸還給操作系統(tǒng)。

【C++】類和對象核心總結(jié)

下面是當(dāng)指針為自定義類型時(shí),編譯器默認(rèn)生成的析構(gòu)函數(shù)不會(huì)調(diào)用對應(yīng)的析構(gòu)函數(shù),和內(nèi)置類型處理策略一樣的證明過程。
【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

1.5 epoller.hpp

下面是封裝的epoll的各個(gè)接口,沒什么難度,我這里也不會(huì)贅述,因?yàn)橹拔覀円呀?jīng)寫過簡單版的LT模式epoll服務(wù)器了,對于epoll接口的使用肯定沒什么難度了,所以屏幕前的老鐵們可以簡單看一下代碼實(shí)現(xiàn),今天的重點(diǎn)是前面Reactor的實(shí)現(xiàn),不是這些小組件是怎么實(shí)現(xiàn)的。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

2.protocol.hpp

2.1 解析出一個(gè)完整的報(bào)文

1.
其實(shí)在tcpServer.hpp講解完畢之后,Reactor網(wǎng)絡(luò)庫的重點(diǎn)就已經(jīng)實(shí)現(xiàn)完畢了,也就是網(wǎng)絡(luò)IO層面上的處理連接到來,處理網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)墓ぷ?,已?jīng)大功告成了。
下面的protocol.hpp只是在Reactor網(wǎng)絡(luò)庫的基礎(chǔ)上接入了服務(wù)器的應(yīng)用層,比如如何處理黏包問題,應(yīng)用層如何定制協(xié)議,添加或去掉應(yīng)用層協(xié)議報(bào)頭,對報(bào)文的序列化和反序列化等等工作全部都屬于應(yīng)用層的事情。
但其實(shí)早在以前我們講協(xié)議定制和序列化反序列化的時(shí)候,也就是實(shí)現(xiàn)網(wǎng)絡(luò)版本計(jì)算器的時(shí)候,我們就已經(jīng)實(shí)現(xiàn)過這些工作了,所以protocol.hpp就是從當(dāng)時(shí)的代碼直接拷貝過來的,僅僅只是對解析報(bào)文這個(gè)代碼作了修改。
所以如果有老鐵忘記了當(dāng)時(shí)怎么定制的協(xié)議,那就可以回頭再重新看看當(dāng)時(shí)的文章。

協(xié)議定制+序列化和反序列化

2.
下面的接口是用來解析sock在應(yīng)用層的_inbuffer數(shù)據(jù)的,由于TCP是面向字節(jié)流的,所以如何解析出一個(gè)完整報(bào)文的問題,就必須由應(yīng)用層來做。
我們當(dāng)時(shí)定過協(xié)議,協(xié)議報(bào)頭和有效載荷之間有LINE_SEP也就是\r\n,有效載荷的尾部也\r\n,協(xié)議報(bào)頭表示有效載荷的字節(jié)大小,所以在字節(jié)流的_inbuffer中,解析出一個(gè)完整報(bào)文的邏輯就可以是這樣的:
為了安全起見,先把輸出型參數(shù)text設(shè)置為空串,然后在inbuffer中找LINE_SEP的迭代器位置,找到之后,將報(bào)頭部分substr截取出來,再將其stoi轉(zhuǎn)換為整數(shù),這樣就得到了有效載荷的大小,然后再將截取出來的報(bào)頭,調(diào)用其類內(nèi)函數(shù)size(),得到報(bào)頭的字節(jié)大小,最后再加上兩個(gè)LINE_SEP的大小,這些字節(jié)大小作和之后,就可以得到一個(gè)完整報(bào)文的字節(jié)大小了。
最后一步就是直接從0開始截取total_len大小個(gè)字節(jié),將截取到的字串放到輸出型參數(shù)text里面即可。然后再將0到total_len字節(jié)的數(shù)據(jù)從inbuffer中刪除即可,其實(shí)就是覆蓋數(shù)據(jù)。這樣我們就從大批的字節(jié)流數(shù)據(jù)中截取出了一個(gè)完整的報(bào)文。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

2.2 應(yīng)用層協(xié)議定制

其實(shí)關(guān)于應(yīng)用層協(xié)議的這些代碼在之前網(wǎng)絡(luò)版本計(jì)算器都已經(jīng)講過了,這里我就言簡意賅的說一下,如果有懵逼的老鐵,請移步我原來寫的那篇文章。

移步:協(xié)議定制+序列化和反序列化

1.
下面是應(yīng)用層報(bào)頭的添加與去除,報(bào)頭添加時(shí),其實(shí)只要在報(bào)頭和有效載荷間,以及有效載荷尾部,都加上LINE_SEP,報(bào)頭的內(nèi)容就是有效載荷的長度。
報(bào)頭去除時(shí),先以第一個(gè)LINE_SEP作為分隔符,截取頭部字符串,這樣就得到了有效載荷的長度大小,然后從第一個(gè)LINE_SEP位置開始截取子串,截取長度為有效載荷的長度即可,這樣就得到了完整的有效載荷。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

2.3 序列化和反序列化

1.
下面是序列化反序列化的工作,主要用到的就是我們自己寫的方案和json的方案,企業(yè)內(nèi)部自己一般會(huì)使用protobuf,對外使用json。json我也不會(huì),只能簡單的使用一下,沒有系統(tǒng)的學(xué)過,所以下面我只能說說我們自己的序列化和反序列化方案,不過值得注意的是,實(shí)際在公司使用中,對于序列化和反序列化是有現(xiàn)成的解決方案的,程序員絕對不會(huì)自己去寫!但今天我們作為學(xué)習(xí)者自己寫肯定更能理解序列化和反序列化究竟是作的一個(gè)什么樣的工作,對學(xué)習(xí)者肯定是大有好處的。

2.
對于請求報(bào)文的序列化,其實(shí)就是將結(jié)構(gòu)體Request中的_x _op _y等字段都拼接成一個(gè)字符串,這樣就完成了序列化的工作,但不是僅僅序列化就完了,能序列化就一定得能反序列化,所以,在拼接字符串時(shí),_x和_op,_op和_y之間都得有一個(gè)SEP作為分隔符,方便對到來的請求報(bào)文進(jìn)行反序列化。(結(jié)構(gòu)化數(shù)據(jù) → 字節(jié)流數(shù)據(jù))
反序列化其實(shí)主要就是字符串操作,將字符串中的_x _y _op截取出來,分別轉(zhuǎn)換為int int char類型,賦值給結(jié)構(gòu)體Request的三個(gè)成員變量里面,這樣就完成了反序列化的工作。(字節(jié)流數(shù)據(jù) → 結(jié)構(gòu)化數(shù)據(jù))

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

3.
對于響應(yīng)報(bào)文的序列化,只要將int類型的退出碼和計(jì)算結(jié)果轉(zhuǎn)換為string類型,中間在拼接一個(gè)SEP字段,這樣就從結(jié)構(gòu)化轉(zhuǎn)為了序列化的數(shù)據(jù)。
反序列化的工作也很簡單,只要將字符串中的退出碼和結(jié)果部分截取子串出來,然后再將其轉(zhuǎn)為int類型,這樣就從序列化轉(zhuǎn)為了結(jié)構(gòu)化的數(shù)據(jù)。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

3.main.cc

3.1 業(yè)務(wù)邏輯處理

1.
下面是整個(gè)Reactor服務(wù)器的調(diào)用邏輯,先初始化服務(wù)器,然后執(zhí)行事件派發(fā)接口Dispatcher。
服務(wù)器的應(yīng)用層提供的服務(wù)是計(jì)算服務(wù),所以在構(gòu)建服務(wù)器對象時(shí),要將上層的處理邏輯函數(shù)calculate也傳到服務(wù)器對象內(nèi)部。
在服務(wù)器執(zhí)行Recver方法時(shí),收到數(shù)據(jù)后,會(huì)調(diào)用回調(diào)函數(shù),執(zhí)行流就會(huì)執(zhí)行calculate方法,進(jìn)行讀到的數(shù)據(jù)的業(yè)務(wù)處理。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

2.
calculate就是業(yè)務(wù)邏輯處理方法,在方法內(nèi)部打一個(gè)while循環(huán),只要能夠解析出一個(gè)完整的報(bào)文,那就可以進(jìn)入循環(huán),對拿到的報(bào)文作應(yīng)用層的邏輯處理,當(dāng)_inbuffer中的數(shù)據(jù)被拿的導(dǎo)致剩余數(shù)據(jù)無法構(gòu)成一個(gè)完整報(bào)文時(shí),也就是ParseOnePackage出錯(cuò)時(shí),我們此時(shí)選擇退出循環(huán),將已經(jīng)處理好的請求報(bào)文,也就是構(gòu)建出了響應(yīng)報(bào)文全部發(fā)送到客戶端,有人說怎么把全部的響應(yīng)報(bào)文發(fā)送給客戶端呢?其實(shí)很簡單,在ParseOnePackage內(nèi)部每次處理好一個(gè)請求報(bào)文后,相對應(yīng)的響應(yīng)報(bào)文會(huì)被放到conn內(nèi)部的發(fā)送緩沖區(qū)_outbuffer中,所以當(dāng)跳出循環(huán)時(shí),_outbuffer中已經(jīng)存放了很多就緒的響應(yīng)報(bào)文了,此時(shí)只要在調(diào)用conn內(nèi)部的sender方法進(jìn)行發(fā)送即可。所以這么來看的話,這個(gè)conn是不是很有用呢?整個(gè)貫穿了Reactor代碼實(shí)現(xiàn)的所有模塊。
在ParseOnePackage內(nèi)部也很簡單,因?yàn)槲覀冊趐rotocol.hpp內(nèi)部已經(jīng)將請求/響應(yīng)報(bào)文的序列化反序列化,應(yīng)用層報(bào)文的報(bào)頭和有效載荷分離,添加報(bào)頭等工作全部做好了,所以在ParseOnePackage內(nèi)部只需要調(diào)用對應(yīng)protocol.hpp內(nèi)部實(shí)現(xiàn)的方法即可。比如先去掉報(bào)頭,然后調(diào)用反序列化接口得到一個(gè)結(jié)構(gòu)化的請求,將結(jié)構(gòu)化的請求和一個(gè)未初始化的結(jié)構(gòu)化響應(yīng)進(jìn)行cal處理,在cal處理內(nèi)部其實(shí)就是作相應(yīng)的計(jì)算工作,計(jì)算工作完成后,將結(jié)果填充到結(jié)構(gòu)化的響應(yīng)報(bào)文中即可,然后對響應(yīng)報(bào)文作序列化,添加報(bào)頭等工作,最后只要將完整的響應(yīng)報(bào)文放到outbuffer中即可,等到循環(huán)結(jié)束時(shí),統(tǒng)一將所有的響應(yīng)報(bào)文發(fā)送給對方。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

3.2 Reactor服務(wù)器運(yùn)行結(jié)果

1.
客戶端我們自己也就不寫了,之前講協(xié)議定制的時(shí)候,我們自己已經(jīng)實(shí)現(xiàn)過calclient和calserver了,所以這里直接拿calclient作為客戶端來使用。
從運(yùn)行結(jié)果可以看出,正常的數(shù)據(jù)計(jì)算請求,服務(wù)器是能夠給我們返回對應(yīng)的計(jì)算結(jié)果的,并且當(dāng)客戶端發(fā)生異常時(shí),比如ctrl+c斷開TCP連接,服務(wù)器也能夠?qū)Ξ惓J录龀鱿鄬?yīng)的處理,比如服務(wù)器也關(guān)閉對應(yīng)的tcp連接,同時(shí)釋放sock對應(yīng)的所有資源,例如sock對應(yīng)的connection結(jié)構(gòu)體,將sock移除epoll模型,移除哈希表,關(guān)閉sock文件描述符等處理。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor

4.總結(jié)Reactor模式

1.
我個(gè)人對于Reactor的理解,Reactor主要圍繞事件派發(fā)和自動(dòng)反應(yīng)展開的,就比如連接請求到來,epoll_wait提醒程序員就緒的事件到來,應(yīng)該盡快處理,則與就緒事件相關(guān)聯(lián)的sock會(huì)對應(yīng)著一個(gè)connection結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體我覺得就是反應(yīng)堆模式的精華所在,無論是什么樣就緒的事件,每個(gè)sock都會(huì)有對應(yīng)的回調(diào)方法,所以處理就緒的事件很容易,直接回調(diào)connection內(nèi)的對應(yīng)方法即可,是讀事件就調(diào)用讀方法,是寫事件就調(diào)用寫方法,是異常事件,則在讀方法或?qū)懛椒ㄖ刑幚鞩O的同時(shí),順便處理掉異常事件。
所以我感覺Reactor就像一個(gè)化學(xué)反應(yīng)堆,你向這個(gè)反應(yīng)堆里面扔一些連接請求,或者網(wǎng)絡(luò)數(shù)據(jù),則這個(gè)反應(yīng)堆會(huì)自動(dòng)匹配相對應(yīng)的處理機(jī)制來處理到來的事件,很方便,同時(shí)由于ET模式和EPOLL,這就讓Reactor在處理高并發(fā)連接時(shí),展現(xiàn)出了不俗的實(shí)力。

2.
我們今天所實(shí)現(xiàn)的服務(wù)器是半同步半異步的,半同步是說Reactor既保證了就緒事件的通知,同時(shí)又負(fù)責(zé)了IO,半異步指的是,今天的服務(wù)器還實(shí)現(xiàn)了業(yè)務(wù)處理。

【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式,Linux,設(shè)計(jì)模式,后端,服務(wù)器,Reactor文章來源地址http://www.zghlxwxcb.cn/news/detail-699802.html

到了這里,關(guān)于【Linux】高級IO --- Reactor網(wǎng)絡(luò)IO設(shè)計(jì)模式的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包