騰訊開源框架TarsCpp-rpc設計分析-server(三)

3 收發包管理

收發包的管理在整個RPC中佔據了十分重要的地位,如何保證在各種網絡狀況下內容不丟失,同時內容還能被高效、正確解析,是一件比較有意思的事情。

Tars RPC在收發包管理上基本思路是利用緩存,下面的文章從主要邏輯上對如何使用緩存進行說明。

3.1 服務端收包管理

Tars服務端在接收請求時,為了兼顧效率、嚴謹,對收到的字符進行了“分層”處理,如下圖:

騰訊開源框架TarsCpp-rpc設計分析-server(三)

服務端收包管理.png

  • 第一層,從套接字裡讀取請求,放入char類型的buffer中,長度為32 * 1024
  • 第二層,因為請求的長度很可能大於32 * 1024,程序會從一直從套接字裡循環讀取,直到讀取的字節數小於等於零,所以需要將buffer內容追加到string類型的_recvbuffer中。這便出現了第一次字符串的拷貝過程,即將buffer內容拷貝到_recvbuffer中
  • 第三層,對_recvbuffer進行“分包”,確保每個包是一個完整的請求。這裡的包是由“內容長度+內容“(iHeaderLen+ro)組成的,所以分包的邏輯也比較簡單,先讀取前4個字節內容轉為整形長度iHeaderLen,根據這個長度截取後面的內容ro即可。分包後_recvbuffer可能有剩餘不夠組成一個完整包,這時候會繼續等待數據。這裡出現了第二次字符串拷貝,將_recvbuffer中的部分內容拷貝到ro中
  • 第四層,將第三層分好的包體內容ro轉移(std::move)到結構體tagRecvData.buffer中,注意這裡為了提高效率,沒有采用字符串拷貝,而是使用了std::move語義
  • 第五層,對第四層中的tagRecvData.buffer內容進行反序列化,如果想深入瞭解Tars協議序列化和反序列化,請參考Tars-C++ 揭秘篇:Tars協議解析。注意這裡使用的是char*的方式,同樣避免了字符串的拷貝

3.2 服務端發包管理

相比服務端收包流程,發包稍稍複雜,為了簡化,我們從ResponsePacket序列化完的字符串說起。即說明TC_EpollServer::Handle::sendResponse後面的流程。

  • 簡單情況:發送的數據量比較小,原生::send函數能夠完整發送所有數據
  • 複雜情況:::send一次發送不完數據,需要保存在std::vector< TC_Slice > _sendbuffer中進行後續處理。

具體處理邏輯如下所示:

騰訊開源框架TarsCpp-rpc設計分析-server(三)

服務端發包管理.png

簡單說明下圖中變量。buffer為string類型,是要發送的序列化完的內容;tcpSend調用的是原生的::send方法;bytes是tcpSend後實際發送的數據長度;TC_Slice是用來分割buffer的數據結構; _sendbuffer是vector類型的TC_Slice,用來管理buffer數據;kChunkSize是分割大小。 圖裡一共有三個NetThread::Connection::send函數,processPipe裡有一個,processNet裡有兩個,注意他們是重載函數,是不同的函數,我在後面標註了他們在文件(tc_epoll_server.cpp)中的行數

  • 第一種場景: 在processPipe(第一個虛線框圖)中數據通過tcpSend一次就全部發送成功。走綠色箭頭,bytes < buffer.size() 為N的路徑
  • 第二種場景:假設服務剛啟動,進來第一筆請求,開始發送結果,具體處理流程如下:
  • 在processPipe中走綠色箭頭,bytes < buffer.size() 為Y,表示buffer的數據沒有發送完,剩餘的內容按照kChunkSize大小分割為TC_Slice,然後依次放入到_sendbuffer中。注意,這時processPipe就結束了,_sendbuffer的發送放在了第二筆請求的processNet(第二個虛線框圖)中處理
  • 如第一條所說,第二次請求到達時,會觸發processNet函數,走紅色箭頭,除了處理本次請求內容,還會處理上次沒發送完的response數據,即_sendbuffer。這時會把_sendbuffer再次分割成一組組的vector< iovec > vecs,最後通過tcpWriteV中的::writev函數發送
  • 通過tcpWriteV依然有可能會有剩餘數據,這時候就需要根據發送的數據量去調整_sendbuffer,走的是粉紅色的虛線箭頭
  • 第二次請求處理後,也會產生response,會再次進入processPipe中,這時候會判斷_sendbuffer是否為空,為空走綠色箭頭邏輯,不為空,走藍色箭頭邏輯,直接分割為TC_Slice,放入_sendbuffer中,等待第三次請求到達時候處理

3.3 客戶端發包管理

上面在整理服務端流程時,因為把不同場景放在一張圖裡,看起來比較複雜。在客戶端這兒,我們嘗試按場景進行說明。先從客戶端發送第一筆請求開始說起。如下圖所示。

騰訊開源框架TarsCpp-rpc設計分析-server(三)

客戶端發包管理-第一次發送請求.png

  • 在ET_C_NOTIFY == pFDInfo->iType(第一個虛線框圖)中,客戶端發送第一筆請求,經歷了將RequestPacket進行序列化的過程,序列化完的結構是“iHeaderLen + 字符串內容 ”, 放在了msg->sReqData中。
  • msg->sReqData通過TcpTransceiver::send進行發送,iRet是實際發送的字節數。iSize是msg->sReqData的長度。
  • 當只發送了一部分sReqData,即0 < iRet < iSize時,剩餘的數據會放到TC_Buffer _sendBuffer中
  • 當整個請求發送失敗,即iRet < 0時,會把這個請求msg整體放到_timeoutQueue中
  • 這時請求過程就結束了,剩餘的數據不論是_sendBuffer,或者_timeQueue中的msg都會在處理response時一併處理
  • 假設現在客戶端已經接收了服務端的response,然後會繼續進入handleOutputImp(第二個虛線框圖)函數處理剩下的請求數據。
  • 如果_sendBuffer不為空,先從_sendBuffer中取出數據再次發送給服務端,如果還剩下數據會調整_sendBuffer大小,然後再繼續處理_timeQueue中未發送的請求
  • 如果_sendBuffer為空,直接去處理_timeQueue中未發送的請求

下面是客戶端開始發送第二次請求。

騰訊開源框架TarsCpp-rpc設計分析-server(三)

客戶端發包管理-第二次發送請求.png

  • 跟第一次請求類似,經過序列化後得到sReqData,這時候會判斷_sendBuffer是否為空。(其實在代碼邏輯中,每次請求都要判斷_sendBuffer是否為空。因為第一次請求_sendBuffer肯定是空的,為了簡單就沒有在“10.3客戶端發包管理-第一次發送請求.png”中畫這部分邏輯)
  • 如果_sendBuffer為空,繼續進入到TcpTransceiver::send環節,邏輯與第一次發送請求相同
  • 如果_sendBuffer不為空,直接把msg消息體放入未發送隊列_timeoutQueue中,等待第二次response到達時在CommunicatorEpoll::handleOutputImp中處理

3.4 客戶端收包管理

客戶端的收包管理比較簡單,使用一個TC_Buffer類型_recvBuffer作為緩存。如下圖所示:

騰訊開源框架TarsCpp-rpc設計分析-server(三)

客戶端收包管理.png

  • 第一層,從套接字裡接收到的數據都放在_recvBuffer中
  • 第二層,將_recvBuffer按照"iHeaderLen + ResponsePacket rsp"方式分割為一個個獨立的包,並完成rsp的反序列化工作。注意rsp的內容與_recvBuffer是拷貝關係
  • 第三層,將一個個獨立包(ResponsePacket rsp)放到從_timeoutQueue中取出的msg中,這裡也是一個拷貝過程,就完成了客戶端的收包管理


分享到:


相關文章: