講講muduo的channel
對比一下,epoll在監聽fd及其發生事件的時候,需要透過epoll_ctl將fd和該fd感興趣的事件註冊到epoll多路複用模組中。當epoll監聽到事件發生後,會將發生事件的fd和fd對應的事件返回(透過event結構體)。
channel類採用面對物件的思想,對fd,fd感興趣的事件以及監聽到具體發生的事件封裝起來。同時channel儲存了不同事件對應的處理函式。
channel提供了修改感興趣事件的方法,設定處理函式的方法,以及發生不同事件時呼叫不同處理函式的方法。
channel包裝了事件,那麼事件的分發交給誰?
channel的分發由eventloop負責,每個channel都會屬於一個eventloop。channel類中會有一個eventloop指標。
channel提供了對感興趣事件的修改,那修改後是如何通知到多路複用元件的?
channel提供了update方法,該方法會透過eventloop呼叫到poller,poller來更新
channel會和一個具體的tcp connection繫結,是怎麼判斷tcp connection是不是存在的呢?
channel會用一個weak_ptr指向一個物件,在呼叫自己處理事件的函式時,會使用weak_ptr.lock方法嘗試轉化為一個shared_ptr方法。如果成功說明物件存在,不成功說明物件失敗。
muduo庫支援什麼樣的多路複用元件?
muduo庫支援Poller和EPollPoller,我只實現了EPollPoller。
EPollPoller類提供了什麼樣的功能?
對比一下,epoll在監聽fd及其發生事件的時候,需要透過epoll_ctl將fd和該fd感興趣的事件註冊到epoll多路複用模組中。當epoll監聽到事件發生後,會將發生事件的fd和fd對應的事件返回(透過event結構體)。
EpollPoller採用面對物件的思想將epoll封裝了起來,將epoll_create封裝在EpollPoller的建構函式中,epoll_ctl封裝在update中,epoll_wait封裝在poll中。
epoll函式需要一個epoll_event陣列儲存返回的事件,你在封裝中使用的是什麼?為什麼?
我在封裝中使用的vector,vector具備動態擴容的機制。當返回活躍事件的個數等於vector長度的時候,動態擴容為原來的兩倍。該步封裝在了epoll_ctl中。
poller怎麼更新channel的?
poller根據透過index來。
channel中會儲存一個index,該index表示channel所處的狀態。-1表示新的channel,1新增,2刪除。
如果當前channel,是沒有被新增過或者是刪除狀態,則將其用add新增。並修改狀態位新增。
否則,判斷有沒有感興趣的事件型別,沒有則直接設定為刪除狀態。有則,使用mod修改,不改變狀態。
注意index的設定是透過poller來的。
Epollpoller怎麼返回感興趣的事件?
在註冊到epoll中的時候,event_data中的ptr會攜帶對應的channel指標。從而在返回感興趣的事件中,直接用指標能得到channel物件。
為什麼更新需要index?
判斷channel的狀態,邏輯呼叫清晰。
epollPoller刪除channel怎麼刪除的?
從poller map中儲存的刪除對應的channel,根據狀態在多路複用元件上刪除對應的channel。將channel狀態設定為new。
muduo庫eventloop類是怎麼設計的?用來幹什麼?
採用面對物件的設計思想,設計了channel和poller兩個元件。還需要一個類將它們排程起來,eventloop類是網路伺服器中負責迴圈的重要模組,它能做到持續監聽、持續獲取監聽結果、持續處理監聽結果對應的事件。
eventLoop經常向其他loop註冊事件執行,是透過哪些成員達成這一目的的?
eventLoop中有兩個比較的成員,一個是wakeupFd,一個是pendingFunctors。在eventLoop初始化的時候,wakeupFd就被註冊到了eventLoop自己所屬的poller中,並關注epollin讀事件。此後想要喚醒eventLoop,則向該wakeupFd寫入資料。
同時,會向loop的pendingFunctors中添入想要執行的回撥函式。
eventLoop迴圈被喚醒後,會去執行pendingFunctors中的所有回撥函式。
這裡的wakeupFd使用的是eventFd。
eventloop的loop迴圈中執行了哪些方法?
執行了兩種方法,一種是channel發生並註冊的對應事件,一種是其餘loop新增的回撥函式。
eventloop採用了one loop per thread的方法,如何保證新增的回撥loop在對應的執行緒中執行呢?
muduo庫儲存了一個執行緒區域性變數threadId,並將該值儲存在了eventloop物件中。判斷的時候讀取當前threadId和eventloop中的threadId進行比較,如果一致就執行。不一致,加入到pendingFunctors中,並喚醒對應的eventloop執行。
muduo的one loop per thread設計是怎麼實現的?
muduo有一個EventLoopThread,其中儲存了loop物件和thread的物件,它們是一對一的。
具體的,EventLoopThread的stratLoop方法,會啟動該thread物件的執行。thread物件繫結的執行緒方法會在棧上建立一個EventLoop物件並將其loop方法開啟,之後返回EventLoop物件的執行。兩者之間透過鎖和訊號量同步。
該loop物件是放線上程的棧中的,會隨著執行緒消亡而消亡,其所對應的EventLoop物件也會消亡。
怎麼防止一個執行緒建立多個eventloop?
eventloop中設定一個執行緒 thread local變數。是指標。 指標指向該執行緒建立好的eventloop變數,如果不為null,則說明已經建立好了。
muduo庫會建立無數個執行緒嗎?
不會,muduo提供了一個EventLoopThreadPool類。該類是一個EventLoopThread執行緒池。這個類在開始的時候會指定執行緒的個數。並透過start方法,將所有執行緒啟動,並儲存每個執行緒的eventloop指標。
EventLoopThreadPool作用在哪裡?
它有一個getNextLoop方法,透過輪詢的方式,返回一個loop,共baseloop使用。
Acceptor類做了什麼事情?
類比我們在開發一個普通server的時候,需要建立一個socket,將該socket和ip地址+port繫結,並開啟為監聽狀態。當有新連線來臨的時候,呼叫accept接受連線。
因此Acceptor類中有一個socket用來監聽連線,該socket被封裝為channel並註冊在poller中。於是,有一個acceptChannel和一個eventloop。
該eventloop執行在baseloop中。
當acceptChannel被註冊在poller中時,會關注它的讀事件,讀回撥是建立方為它設定的。其實際作用是,選擇一個subloop,將新連線新增到他的poller中。
TcpServer類做了什麼事情?
TcpServer中有一個loop物件,該loop為baseloop,tcpserver的acceptor物件執行在baseloop上。當有新的連線透過acceptor返回後,會被TcpServer打包成一個tcpconnection物件。
之後TcpServer從自己所擁有的執行緒池執行緒中選擇一個執行緒,使用其中的loop,稱為subloop來服務該tcpconnection。當其套接字發生相應的事件,執行相應的回撥。
所以,TcpServer中有loop物件,acceptor物件,EventLoopThreadPool執行緒池和tcpconnection連線。
Acceptor是在tcpserver中的,它會接受新的連線,這個回撥是TcpServer設定的,具體講講。
這個回撥函式是TcpServer中的newConnection。該函式將本端(呼叫系統函式)和對端(傳入)ip+port得到,之後從執行緒池裡面輪詢得到一個eventloop。之後,構造一個tcpconnection物件。將該物件的各種資訊回撥設定給tcpconnection。注意,這裡的回撥比較有意思,其傳遞鏈為使用者設定給TcpServer=>TcpConnection=>Channel=>Poller=>notify channel呼叫回撥。
並透過得到的eventloop,非同步執行TcpConnection的connectEstablished。該方法是將tcpconnection放到subloop中執行。
TcpServer有個啟動過程,啟動的時候他幹了什麼?
啟動的時候,先啟動subloop執行緒池,再將acceptor的accept fd開啟listen,並在loop的epoll中使能讀事件。
你說了建立,tcpconnection的關閉回撥的連線是客戶給的嗎?
不是,是tcpserver自己設定的。它會在自己記錄的map中去掉tcpconnection,同時在ioloop中喚醒,並呼叫TcpConnection::connectDestroyed方法。
說說TcpConnection類
類比我們在開發一個普通server的時候,當有一個新的連線到來的時候,我們會接受連線並將其註冊到epoll中。TcpConnection對新來的連線進行了封裝。
它擁有的物件為連線的socket,將socket封裝的channel,以及server設定的連線,讀寫等等回撥函式。
TcpConnection傳送資料的方法怎麼呼叫的?在哪裡呼叫?
send方法可以使用在定義server onMessage的時候。
send中有sendInLoop的函式,該函式在同步/非同步的狀態下執行傳送資訊的操作。
進入sendInLoop的邏輯中,若第一次傳送資料,則使用write系統函式向channel的fd中寫資料。
如果傳送完成,則在loop中註冊一個寫完成的回撥。
如果沒有傳送完成,則將讓channel關注寫操作,並喚醒epoll,執行寫操作。
(注意:這裡會對緩衝區的資料長度做判斷(原有的+新來的),如果超過了水位線,則觸發高水位回撥。)
channel寫操作,它將buffer中的資料僅可能的寫,如果寫完了,則關閉寫事件,並執行一個寫結束回撥。
TcpConnection呼叫shutdown是什麼時候?它是怎麼執行的?
shutdown方法可以使用在定義server onMessage的時候,傳送完訊息。
在自己的執行緒執行shutDownInLoop。
如果channel沒有寫事件,則說明channel裡面沒有資料。關閉socket的寫端,會觸發epoll中的EPOLLUP事件,呼叫closeback回撥。該closeback是tcp的handelclose(置為關閉狀態,取消所有感興趣的事件)方法。
這裡有一點比較有趣,如果還有在寫的操作,shutDownInLoop不會執行。只是在這裡設定為了kDisconnecting。在write寫完資料之後,會判斷狀態,如果是kDisconnecting再去執行shutdownInLoop。
TcpConnection呼叫connectEstablished是什麼時候?它是怎麼執行的?
Tcpserver呼叫了這個方法,當連線到來後,在初始化該資源後,server會強制執行該方法。
連線成功,將connecting改編為connected。
將channel與tcpconnection繫結,防止conn被銷燬後,還在channel中呼叫。
註冊channel讀事件。呼叫連線建立的回撥。
TcpConnection呼叫connectDestroyed是什麼時候?它是怎麼執行的?
該方法透過tcpserver的removeConnection最終被註冊到了TcpConnection的closeCallBack中。
設定狀態為斷開連線。將channel所有感興趣的事件關閉。將channel從poller中刪除。
TcpConnection有不同的註冊事件,分別的作用是什麼講講?
handleRead讀資料讀到buffer中,呼叫設定好的messageCallback_。如果讀到資料為0,說明觸發讀事件但是沒有發過來的了,則呼叫handelclose關閉。
handleWrite從buffer將資料寫到fd中。寫之後,判斷有沒有都寫完,寫完呼叫一個全部完成的回撥。如果狀態正在等待關閉連線,再去執行shutdownInLoop。
handclose設定為關閉狀態。
TcpConnection繼承自enable_shared_from_this,為什麼?
TcpConnection是唯一一個繼承自enable_shared_from_this的類。
TcpConnection的生命週期比較模糊,庫使用方會看到TcpConnection,同時由於回撥函式的鏈條為:TcpServer=>TcpConnection=>Channel=>Poller=>notify channel。庫使用方在構造TcpServer的時候,會寫好連線建立/斷開,可讀寫事件的回撥,該回撥中的事件需要使用TcpConnection。為了防止TcpConnection物件銷燬帶來的函式呼叫錯誤,使用shared_from_this()方法將智慧指標傳遞到這些事件回撥中,從而實現合理的生存期控制。
TcpConnection定義了不同的狀態,你能講講在什麼時候設定的嗎?
建立的時候設定成了kConnecting
在將channel與tcpconnection繫結,並將其設定為關注讀事件後,設定為kConnected。
在呼叫shutdown後,變為kDisconnecting狀態。
在觸發closeback後,變為kDisconnected狀態。
設定這些狀態是有用的,比如在寫訊息的時候,如果狀態正在等待關閉連線,說明之前想要關閉連線的時候,因為寫操作沒有關閉,現在再去執行shutdownInLoop。
channel有個buffer類,說說這個類。
Buffer類封裝了使用者緩衝區,以及向其中讀寫資料等方法。
初始化狀態。CheapPrepend記錄整塊資料的長度。
一段時間後,向buffer中寫入資料。資料分割槽為read(可讀),write(可寫)。其中,readIdx標誌可讀區域起始位置,writeIdx標誌可寫區域起始位置。
從可讀區域中讀取一部分資料後,會有部分buffer區域空閒。
繼續向資料區寫入資料,可讀區域變大,但是超出了buffer的大小。但是算上空白區域,buffer還可以容納整個資料。
整體資料向前調整。
繼續寫入資料,但是這次算上空白區域也無法滿足資料要求,則擴容。