brpc源碼學習(六)- brpc server 端整體流程

brpc的使用比較容易上手,以官方demo爲例,因爲brpc的數據序列化依賴protobuf,所以首先需要定義個proto

然後繼承EchoService並實現Echo方法

然後是整體流程

啓動還是比較簡單的,定義server,AddService,然後Start即可

首先放一張官網的圖,陷入細節前先有個大概印象

然後看下AddService

然後是AddServiceInternal

首先判斷註冊的service中是否有method,如果沒有則直接返回;然後調用InitializeOnce()只進行一次初始化,該函數實際調用的是GlobalInitializeOrDieImpl

在GlobalInitializeOrDieImpl內首先忽略掉SIGPIPE,然後初始化SSL,註冊NamingService,LoadBalancer,CompressHandler;然後開始註冊協議,brpc server一個端口支持多種協議,這裏的協議都是指的是應用層協議,如baidu_std協議是基於tcp,以註冊baidu_std協議爲例

協議Protocol類是一個包含了多個函數指針的結構體,例如:

Parse負責將消息從source上切割下來,client和server均會使用。

ProcessRequest處理serverparse返回的消息,只有server端會調用。

註冊完協議之後,遍歷協議,將含有process_response的協議加入到clientInputMessengerhandler中。

然後回來到AddServiceInternal,接着判斷當前註冊的service是否註冊過,沒註冊的話則註冊service的所有method

然後在demo中使用默認的ServerOption調用server.Start(),首先還是調用InitializeOnce,這裏會直接返回。然後中間的邏輯在默認的option不會調用,直接到初始化bthread

然後從min_port開始遍歷到max_port直到找到一個可用的port,然後創建Acceptor

AcceptorInputMessenger的子類,主要的工作就是處理指定的port來的消息,然後看下BuildAcceptor,遍歷所有的協議,如果協議指定了process_request,那麼就加到acceptorhandler

然後調用acceptorStartAccept,核心代碼如下,初始化SocketOptions,設置fd爲監聽的端口,user爲當前acceptoron_edge_triggered_eventsOnNewConnectionsOnNewConnections是一個函數指針,表示當前fd發生事件後的回調函數

然後調用SocketCreate,摘一段官網介紹:

和fd相關的數據均在Socket中,是rpc最複雜的結構之一,這個結構的獨特之處在於用64位的SocketId指代Socket對象以方便在多線程環境下使用fd

這裏主要邏輯就是根據SocketOption去初始化Socket,然後ResetFileDescriptor

這裏其實還是對socket的初始化工作

因爲brpc使用的是epoll的邊緣觸發,所以將fd設置爲非阻塞,然後設置socketsendrecv buffer大小,然後將當前fd加入到event_dispatcher

GetGlobalEventDispatcher中,會只進行一次初始化dispatcher的工作,會創建FLAGS_event_dispatcher_numdispatcher,默認爲1,構造函數中會創建epoll fd,然後調用Start

Start中會啓動一個bthread,執行RunThisRunThis會執行dispatcherRun

Run中就是循環進行epoll_wait

然後對判斷事件,如果是EPOLLIN事件,那麼執行StartInputEvent,如果是EPOLLOUT,則執行HandleEpollOut

到現在爲止,dispatcherStart就執行結束了,然後回到SocketResetFileDescriptor,會執行AddConsumer,將socket_id存到epoll_eventdata中,然後註冊EPOLLIN事件,到這裏,server端初始化過程就完成了,接下來就坐等請求到來。

 

然後回到epoll_wait,當有新請求到來時,epoll_wait返回,遍歷每一個事件,執行Socket::StartInputEvent;註冊epoll事件時將socketid註冊在了epoll_datau64中,所以首先通過SocketId addressSocket,爲了保證一個fd上只有一個bthread在處理,這裏引入了一個atomic變量_nevent,通過_nevent判斷當前socket是否有bthread正在處理,如果有的話就什麼都不做,因爲正在處理的線程執行完後會執行新事件,如果沒有的話就使用bthread_start_urgent啓動一個bthread執行ProcessEvent來處理新消息,此時epoll bthread讓出當前worker的處理,worker執行新建的ProcessEvent bthread,而epoll bthread則被steal到另一個worker線程執行。這裏就是官網中所說,brpc不區分io線程和worker線程,epoll bthread不負責數據的讀取,IO線程的問題在於一個線程同時只能讀一個fd,當多個繁忙的fd聚集在一個IO線程中時,一些讀取就被延遲了;另外epoll bthread讓出當前workerProcessEvent bthread,這樣使其有更好的cache locality,可以儘快地讀取fd上的數據。

ProcessEvent則是調用on_edge_triggered_events,在Socket Create的時候將該回調函數設置爲了OnNewConnections

這裏會通過MoreReadEvents循環判斷當前fd上是否還有新的事件,然後執行OnNewConnectionsUntilEAGAIN,這裏判斷是否有新事件的方法就是上文提到的_nevent

OnNewConnectionsUntilEAGAIN中,首先通過accept拿到一個已完成的連接,然後從監聽socket中獲取user,即之前的acceptor,然後創建Socket,回調函數設置爲OnNewMessages,接下來的過程和之前創建監聽Socket過程一樣,執行AddConsumer,將socket_id存到epoll_eventdata中,註冊EPOLLIN事件,然後這個新連接有數據來了之後會在epoll_wait返回,執行ProcessEvent,調用到OnNewMessages

再粘一個官網的介紹:

InputMessenger負責從fd上切割和處理消息,它通過用戶回調函數理解不同的格式。Parse一般是把消息從二進制流上切割下來,運行時間較固定;Process則是進一步解析消息(比如反序列化爲protobuf)後調用用戶回調,時間不確定。若一次從某個fd讀取出n個消息(n > 1)InputMessenger會啓動n-1bthread分別處理前n-1個消息,最後一個消息則會在原地被ProcessInputMessenger會逐一嘗試多種協議,由於一個連接上往往只有一種消息格式,InputMessenger會記錄下上次的選擇,而避免每次都重複嘗試。

可以看到,fd間和fd內的消息都會在brpc中獲得併發,這使brpc非常擅長大消息的讀取,在高負載時仍能及時處理不同來源的消息,減少長尾的存在。

這裏的handler就是一開始初始化註冊的所有server端協議。進入while循環,計算一次讀取數據的長度,DoRead會執行_read_buf.append_from_file_descriptor_read_bufIOPortal類型,如上篇博客所講,這個方法會調用readvfd中的數據讀入到iobufblock中。

因爲這一次數據讀取可能會包含多個消息,因此下面會有另一個while循環,每次調用CutInputMessage嘗試從iobuf中切割一條消息,如上文,server端是支持多協議的,所以這裏第一次會嘗試使用所有的協議進行一次parse,因爲大多數情況下一個連接上只有一種協議,因此嘗試一遍之後會記錄下來執行成功的協議,之後將首先嚐試記錄的協議。

這裏的parse就是上文提到的註冊協議中的parse方法,這裏以協議baidu_std爲例簡單介紹一下,baidu_std官方介紹在這裏https://github.com/apache/incubator-brpc/blob/master/docs/cn/baidu_std.md可以看到判斷是否是baidu_std的方法就是判斷前4 個字節是否爲”prpc”,這裏的copy_to爲顯式拷貝,因爲在判斷是否爲baidu_std協議的過程中不能消費數據,否則可能會影響其他協議解析;如果iobuf中的數據不夠4個字節且是”prpc”的前綴,那麼返回PARSE_ERROR_NOT_ENOUGH_DATA錯誤,這個表示到目前爲止不違反當前協議,但是數據不足一個消息,因此會觸發重新DoRead的過程;如果前4個字節就是”prpc”,那麼滿足baidu_std協議,接下來將解析包體長度和包體中的元數據包長度存入到body_sizemeta_size中,如果iobuf中數據長度不足body_size + 12(4 + 4 + 4),那麼同樣會觸發重讀,否則將iobuf中的meta和其他數據切割到msg中並返回,這裏是零拷貝。

如果一次DoRead讀入了n條消息,那麼前n-1條消息會通過QueueMessage後臺啓動了n-1bthread進行處理,而最後一個消息會被析構函數RunLastMessage執行,原地執行process函數,即協議中的process

還是以baidu_std爲例,首先是從iobuf中解析pb格式的meta

然後創建reqresController,然後進行流控,解析req,如果有attachment也解析出來,最後調用CallMethodCallMethod方法在編譯protobuf時生成,會調用到用戶定義的Echo方法

最後在Echodone_guard析構時會調用doneRun方法,發送responseclient

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章