首頁  >  文章  >  後端開發  >  基於彙編的 C/C++ 協程(用於伺服器)的實現

基於彙編的 C/C++ 協程(用於伺服器)的實現

php是最好的语言
php是最好的语言原創
2018-08-02 16:09:002512瀏覽

本篇文章,是 對C/C 協程的實作。我們需要實現這兩個目標:

  1. 有同步式伺服器程式設計的順序思路,以便於功能設計和程式碼偵錯——我使用了libco 中的協程部分

  2. 有非同步I/O 的效能-我使用了libevent 中的event I/O     apache php mysql

結構上,就是將libco 和libevent 兩者的功能結合起來,所以我把我的工程,命名為libcoevent,意思是「基於libevent 的同步協程伺服器程式設計框架」。名字中 co 的意思並不代表 libco,而是 coroutine。

程式語言上,我選擇的是C ,主要是因為libco 只支援基於x86 或x64 架構的Linux,而這樣的架構,基本上都是PC 機,或是資源不缺、效能也不錯的嵌入式系統,上C 完全沒有問題。本文解釋程式碼實現的原理。

如果要使用該工程,請在連結選項中加入 -lco -levent -lcoevent 三個選項。

類別關係及基本功能

類別關係

類別繼承關係

類別的基本繼承關係圖如下:

基於彙編的 C/C++ 協程(用於伺服器)的實現

#在實際呼叫中,只有處於繼承關係樹的葉子結點上的類別才會被實際使用到,其他類別都被視為虛類別。

類別從屬關係

各類別的實例在程式運行中是有從屬關係的,除了作為頂層的Base 類別之外,其他樹葉類別都需依附於其他的類別所在的運行環境中才能執行。從屬關係圖如下:

基於彙編的 C/C++ 協程(用於伺服器)的實現

  • Base 類別提供最基本的運作環境,並管理Server物件;

  • Procedure 物件管理Client 物件。在圖中體現為 ServerSession 物件均管理 Client 物件。

    • Server 物件由應用程式建立並初始化到 Base 物件中運行。當伺服器結束或當其從屬的 Base 物件銷毀時,可設定自動銷毀 Server 物件。

    • Session 物件由處於會話模式(session mode)的Server 物件自動創建,並呼叫應用程式指定的程式入口執行;當會話結束時(函數呼叫return)或其從屬的Server 物件服務結束時,由Server 物件自動銷毀。

  • Client 物件由應用程式呼叫 Procedure 物件的介面創建,用於與第三方服務互動。應用程式可提前呼叫介面要求銷毀 Client 對象,也可以待 Procedure 服務結束時自動統一銷毀。

Base 和 Event 類別

基於彙編的 C/C++ 協程(用於伺服器)的實現

#Base 類別用於執行 libcoevent 的各個服務。每個 Base 類別的實例應對應著一個線程,所有的服務以協程的方式在 Base 實例中運行。從上圖可知,Base 類別包含一個 libevent 函式庫的 event_base 物件和本協程函式庫的一系列 Event 物件。

基於彙編的 C/C++ 協程(用於伺服器)的實現

Event 類別其實是藉用了libevent 的struct event 名稱,因為每一個Event 類的實例,對應libevent 的一個event 物件。我們需要專注的重點,是 ProcedureClient 類別。

Procedure 類別

Procedure 類別有兩個關鍵特點:

  1. 每個物件都有一個libco 協程,即擁有自己獨立的上下文訊息,可以用於編寫一個獨立的伺服器過程(procedure);

  2. #Procesure 的子類別可以建立Client 物件與第三方伺服器通信和互動。

Procedure 類別擁有兩個子類,分別是 ServerSession

Server 類別

Server 類別由應用程式建立並初始化到 Base 物件中運行。 Server 類別有三個子類別:

  • SubRoutine:實際上不作為任何伺服器程序,但提供了最基本的sleep() 函數,並支援Procedure 類別的建立Client 對象的功能,因此應用程式可以用來作為臨時創建或常駐的內部程式來使用。

  • UDPServer:應用程式建立並初始化 UDPServer 物件後,程式會自動綁定到一個資料封包 socket 介面上。應用程式可以透過在網路介面中收發封包來實現網路服務。 UDPServer 同時提供普通模式會話模式

  • TCPServer:應用程式建立並初始化 TCPPServer 物件後,程式會自動綁定並監聽流 socket。 TCPServer 只支援會話模式

所謂的 “普通模式”,也就是應用程式註冊 Server 物件的入口函數,並且由應用程式操作 Server 物件的行為。

所謂的“會話模式”,指的是UDPServer 或TCPServer 對象,在接收傳入資料後,自動區分客戶端,並單獨建立Session對象進行處理。每個 Session 物件只服務一個客戶端。

Session 類別

Session 物件不能由應用程式主動創建,而是由處於會話模式的 Server 類別自動按需建立。 Session 物件的特點是,只能與單一一個客戶端(相較於 UDPServer 物件而言)進行通信,因此沒有 send() 函數,只有 reply()

在頭檔coevent.h 宣告的Session 類別及其子類別皆為純虛類,目的是防止應用程式明確地建構 Session 物件並隱藏實作細節。

Client 類別

Client 物件由 Procedure 物件創建,並由 Procedure 物件進行回收。 Client 物件的作用是主動向遠端伺服器發起通訊。由於從客戶-服務結構的角度,這個動作屬於客戶端,所以命名為 Client。

DNSClient

Client 的子類別中比較特別的是DNSClient 類,這個類別的存在是為了解決在異步I/O 中的getaddrinfo( ) 阻塞問題。 DNSClient 的實作原則請參考程式碼和我之前的文章《DNS 封包結構與個人 DNS 解析程式碼實作》。

而對於 DNSClient 類別而言,具體實作原理,就是封裝了一個 UDPClient 對象,透過該對象完成 DNS 封包的收發,並在類別中實作封包的解析。

UDPServer-基於 libevent 的協程實作

UDPServer 類別普通模式的原理,就是一個非常典型的基於 libevent 的同步協程伺服器框架。在其程式碼實作中,核心功能就是以下幾個函數:

  • _libco_routine(),協程的入口函數,使用這個函數,轉換成為liboevent 的統一服務入口函數

  • _libevent_callback()libevent 時間回呼函數,在這個函式裡,實作協程上下文的恢復。

  • UDPServer::recv_in_timeval(),資料接收函數,在這個函數中,實作關鍵的資料等待功能,同時實作了協程上下文的保存

上述三個函數的程式碼總量,加上空白行也不超過200 行,我相信還是很容易看懂的。以下具體解釋實作原理:

libco 協程介面

#如前文所說,我使用的是 libco 作為協程函式庫。協程對於應用程式是透明的,但是對於函式庫的實作而言,這才是核心。

下面解釋一下libco 的協程功能所提供的幾個接口(libco 的文檔數量簡直“感人”,這也是網上經常被吐槽的…):

創建和銷毀協程

Libco 使用結構體struct stCoRoutine_t * 儲存協程,透過呼叫co_create() 可以建立協程物件;使用co_release() 銷毀協程資源。

進入協程

建立了協程之後,呼叫 co_resume() 可以從協程函數的開頭開始執行協程。

暫停協程

當協程到了需要交出 CPU 使用權的時候,可以呼叫 co_yield() 釋放協程、切換掉上下文。呼叫之後,上下文會恢復到上一個呼叫 co_resume() 的協程中。呼叫 co_yield() 的位置可以視為一個 “斷點”。

恢復協程

恢復協程和建立協程所用的函數都是co_resume(),呼叫函數,將目前堆疊切換為指定協程的上下文,協程會從上文提到的「斷點」 恢復執行。

協程調度實作

從上一小節可以看到,我們使用到的libco 協程功能函數中,雖然包含了協程的切換函數,但什麼時候切換、切換之後CPU 如何分配,這是我們需要實現並封裝起來的工作。

創建和銷毀協程的時機,自然就是在 UDPServer 類別初始化和析構的時候。下文重點解析進入、暫停和恢復協程的操作:

進入協程

進入/ 恢復協程的程式碼,是在_libevent_callback() 中,有這麼一行:

// handle control to user application
co_resume(arg->coroutine);

如果目前協程還沒有被執行過,那麼執行了這句程式碼之後,程式會切換到建立libco 協程時指定的協程函數開始執行。對於 UDPServer,也就是 _libco_routine() 函式。這個函數非常簡單,只有三行:

static void *_libco_routine(void *libco_arg)
{
    struct _EventArg *arg = (struct _EventArg *)libco_arg;
    (arg->worker_func)(arg->fd, arg->event, arg->user_arg);
    return NULL;
}

透過傳入參數,將 libco 回呼函數轉換為應用程式指定的伺服器函數執行。

但要如何實作第一次的 libevent 回呼呢?這還是很簡單的,只需要在呼叫 libevent 的 event_add()時,將逾時時間設為 0 即可,這會導致 libevent 事件立即逾時。透過這個機制,我們也實現了在 Base 運行之後立即執行各 Procedure 服務函數的目的。

暫停和復原協程

何時呼叫co_yield是是本協程實作的重點,呼叫co_yield 的位置,是一個可能會導致上下文切換的地方,也是將非同步程式框架轉換為同步框架的關鍵技術點。這裡可以參考 UDPServerrecv_in_timeval() 函數。函數的基本邏輯如下:

基於彙編的 C/C++ 協程(用於伺服器)的實現

其中最重要的分支,就是對libevent 事件標誌的判斷;而最重要的邏輯,就是event_add()co_yield() 函數的呼叫。函數片段如下:

struct timeval timeout_copy;
timeout_copy.tv_sec = timeout.tv_sec;
timeout_copy.tv_usec = timeout.tv_usec;
    ...
event_add(_event, &timeout_copy);
co_yield(arg->coroutine);

這裡,我們把co_yield() 函數理解為一個斷點,當程式執行到這裡的時候,CPU 的使用權會被交出,程式回到呼叫co_resume() 的上一層函數手中。這個 「上一級函數」 究竟是哪裡呢?其實就是前文提到的 _libevent_callback() 函式。

_libevent_callback() 的角度來看,程式會從 co_resume() 函數傳回,並且繼續往下執行。此時我們可以這麼理解:協程的調度,其實是藉用了 libevent來進行的。這裡我們要關註一下co_resume() 上方的幾句:

// switch into the coroutine
if (arg->libevent_what_ptr) {
    *(arg->libevent_what_ptr) = (uint32_t)what;
}

這裡將libevent 事件flag 值傳遞給了協程,而這是前文進行事件判斷的重要依據。當時間到來,_libevent_callback() 會在下面呼叫 co_resume() 的位置,將 CPU 使用權交回協程。

銷毀協程

除了ci_yield() 之外,協程函數呼叫return 也會導致從co_resume() 傳回,所以在_libevent_callback() 中,我們還需要判斷協程是否已經結束。如果協程結束,那麼就應當銷毀相關的協程資源了。參見 if (is_coroutine_end(arg->coroutine)) {...} 條件體內的程式碼。

會話模式(Session Mode)

在本工程的實作中,提供了被稱為 「會話模式」 的一個伺服器設計模式。會話模式指的是 UDPServer 或 TCPServer 對象,在接收到傳入資料後,自動區分客戶端,並單獨建立 Session 物件進行處理。每個 Session 物件只服務一個客戶端。

對於TCPServer 而言,實作上述的功能比較簡單,因為監聽一個TCP socket 之後,當有傳入連線的時候,只要呼叫accept() ,就可以取得一個新的檔案描述符,為這個檔案描述符建立一個新的Server 的子類別就行了——這就是TCPSession 類別。

UDPServer 就比較麻煩了,因為 UDP 不能這麼做。我們只能自行實作所謂的 session。

UDPSession 實作

設計目標

我們需要實作UDPSession 類別的以下效果:

  • 類當呼叫recv 函數時,只會接收到對應的遠端客戶端發送的資料

  • 類別呼叫send 函數(實際實作是reply())時,可以使用UDPServer 的連接埠來回覆

recv()

在工程中,UDPSession 是抽象類,實際實作是 UDPItnlSession。但準確而言,UDPItnlSession 的實現,密切依賴 UDPServer。這一部分,可以參考 UDPServer_session_mode_worker() 函數中的 do-while() 迴圈體程式碼。程式想法如下:

  • UDPServer 維護一個 UDPSession 字典,以遠端 IP 連接埠名稱的組合作為 key。

  • 當資料到來時,判斷遠端IP 連接埠的組合是否在字典中,如果在,那麼就把資料複製給對應的session;如果不存在,則建立session

複製資料的程式碼,請參閱UDPItnlSession 類別的forward_incoming_data() 函數實作。

reply()

發送資料其實就很簡單,直接對 UDPServer 的 fd 進行 sendto() 就可以了。

quit

對於session mode 的Server 對象,程式碼中提供了一個可以由其session 呼叫的、要求server 退出並銷毀資源的函數: quit_session_mode_server()。實作原理是向 server 觸發一個 EV_SIGNAL 事件。對於普通的 I/O 事件而言,這是不該出現的,我們這裡活用來作為退出訊號。如果 server 發現了這個訊號,則觸發退出邏輯。

應用範例

本工程的範例程式碼分成server 和client 兩部分,其中server 用到了libcoevent,而client 只是使用Python#寫的簡單程式。本文就不說明 client 部分的程式碼了。

Server 的程式碼,分別針對 Server 類別的三個子類別做了應用範例。使用了包括空白行、偵錯語句、錯誤判斷等的邏輯,只使用不到 300 行,就實作了一個流程和兩個服務。應該說,邏輯還是很清楚的,而且也節省了大量程式碼。

SubRoutine

透過函數 _simple_test_routine(),展示了一個一次性的線性網路邏輯。程式中,routine 首先建立了一個 DNSClient 對象,向預設域名伺服器請求了一個域名,然後 connect() 該伺服器的 80 埠。成功後,直接返回。

這個函數展示了 SubRoutine 的使用場景,以及 Client 物件的使用方法,特別是 DNSClient 的簡易使用方法。

UDPServer

UDPServer 的入口函數是 _udp_session_routine(),功能是為客戶端提供網域查詢服務。 Clients 傳送一段字串作為待查詢域名,然後 server 透過 DNSClient 物件請求後,將查詢結果傳回給客戶端。

這個函數展示了 UDPSession 物件和 DNSClient 的(比較複雜和完整的)使用方法。

TCPServer

入口函數是 _tcp_session_routine(),邏輯比較簡單,主要是展示 TCPSession 的用法。

後記

原理上,libcoevent 已經開發了,實作了必須的功能,完全可以用來寫伺服器程式。當然由於這是初版,所以很多程式碼看起來還是有點亂。這個函式庫的意義在於,可以從教學角度,仔細地說明 C/C 協程更為本源的實作原理,也可以作為一個可用的協程伺服器函式庫來使用。

歡迎讀者針對這個函式庫多多批判,也歡迎讀者提出新需求-例如我就決定加幾個需求,算是TODO 吧:

  1. 實作HTTPServer,作為TCPServer 的子類,提供HTTP fcgi 服務;

  2. 實作SSLClient 的類,處理對外的SSL請求。

相關文章:

C#網頁程式設計系列文章(八)之UdpClient實作同步UDP伺服器

C語言實作php伺服器

相關影片:

C# 教學

以上是基於彙編的 C/C++ 協程(用於伺服器)的實現的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn