溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

C/C++ 協(xié)程用于服務(wù)器的實現(xiàn)方法

發(fā)布時間:2020-10-23 15:10:23 來源:億速云 閱讀:292 作者:小新 欄目:編程語言

C/C++ 協(xié)程用于服務(wù)器的實現(xiàn)方法?這個問題可能是我們?nèi)粘W(xué)習(xí)或工作經(jīng)常見到的。希望通過這個問題能讓你收獲頗深。下面是小編給大家?guī)淼膮⒖純?nèi)容,讓我們一起來看看吧!

  1. 有同步式服務(wù)器編程的順序思路,便于功能設(shè)計和代碼調(diào)試——我使用了 libco 中的協(xié)程部分

  2. 有異步 I/O 的性能——我使用了 libevent 中的 event I/O     apache php mysql

結(jié)構(gòu)上,就是將 libco 和 libevent 兩者的功能結(jié)合起來,所以我把我的工程,命名為 libcoevent,意為 “基于 libevent 的同步協(xié)程服務(wù)器編程框架”。名字中 co 的意思并不代表 libco,而是 coroutine。

編程語言上,我選擇的是 C++,主要是因為 libco 只支持基于 x86 或 x64 架構(gòu)的 Linux,而這樣的架構(gòu),基本上都是 PC 機,或者是資源不缺、性能也不錯的嵌入式系統(tǒng),上 C++ 完全沒有問題。本文解釋代碼實現(xiàn)的原理。

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

類關(guān)系及基本功能

類關(guān)系

類繼承關(guān)系

類的基本繼承關(guān)系圖如下:

C/C++ 協(xié)程用于服務(wù)器的實現(xiàn)方法

在實際調(diào)用中,只有處于繼承關(guān)系樹的葉子結(jié)點上的類才會被實際使用到,其他類均視為虛類。

類從屬關(guān)系

各類的實例在程序運行中是有從屬關(guān)系的,除了作為頂層的 Base 類之外,其他樹葉類都需依附于其他的類所在的運行環(huán)境中才能執(zhí)行。從屬關(guān)系圖如下:

C/C++ 協(xié)程用于服務(wù)器的實現(xiàn)方法

  • Base 類提供最基本的運行環(huán)境,并管理 Server 對象;

  • Procedure 對象管理 Client 對象。在圖中體現(xiàn)為 ServerSession 對象均管理 Client 對象。

    • Server 對象由應(yīng)用程序創(chuàng)建并初始化到 Base 對象中運行。當(dāng)服務(wù)器結(jié)束或當(dāng)其從屬的 Base 對象銷毀時,可配置自動銷毀 Server 對象。

    • Session 對象由處于會話模式(session mode)的 Server 對象自動創(chuàng)建,并調(diào)用應(yīng)用程序指定的程序入口運行;當(dāng)會話結(jié)束時(函數(shù)調(diào)用 return)或其從屬的 Server 對象服務(wù)結(jié)束時,由 Server 對象自動銷毀。

  • Client 對象由應(yīng)用程序調(diào)用 Procedure 對象的接口創(chuàng)建,用于與第三方服務(wù)交互。應(yīng)用程序可提前調(diào)用接口要求銷毀 Client 對象,也可以待 Procedure 服務(wù)結(jié)束時自動統(tǒng)一銷毀。

Base 和 Event 類

C/C++ 協(xié)程用于服務(wù)器的實現(xiàn)方法

Base 類用于運行 libcoevent 的各個服務(wù)。每個 Base 類的實例應(yīng)對應(yīng)著一個線程,所有的服務(wù)以協(xié)程的方式在 Base 實例中運行。從上圖可知,Base 類包含一個 libevent 庫的 event_base 對象和本協(xié)程庫的一系列 Event 對象。

Event 類其實是借用了 libevent 的 struct event 名稱,因為每一個 Event 類的實例,對應(yīng)著 libevent 的一個 event 對象。我們需要關(guān)注的重點,是 ProcedureClient 類。

Procedure 類

Procedure 類有兩個關(guān)鍵特點:

  1. 每個對象都擁有一個 libco 協(xié)程,即擁有自己獨立的上下文信息,可以用于編寫一個獨立的服務(wù)器過程(procedure);

  2. Procesure 的子類可以創(chuàng)建 Client 對象與第三方服務(wù)器通信和交互。

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

Server 類

Server 類由應(yīng)用程序創(chuàng)建并初始化到 Base 對象中運行。Server 類有三個子類:

  • SubRoutine:實際上不作為任何服務(wù)器程序,但提供了最基本的 sleep() 函數(shù),并支持 Procedure 類的創(chuàng)建 Client 對象的功能,因此應(yīng)用程序可以用來作為臨時創(chuàng)建或常駐的內(nèi)部程序來使用。

  • UDPServer:應(yīng)用程序創(chuàng)建并初始化 UDPServer 對象后,程序會自動綁定到一個數(shù)據(jù)報 socket 接口上。應(yīng)用可以通過在網(wǎng)絡(luò)接口中收發(fā)數(shù)據(jù)包來實現(xiàn)網(wǎng)絡(luò)服務(wù)。UDPServer 同時提供普通模式會話模式。

  • TCPServer:應(yīng)用程序創(chuàng)建并初始化 TCPPServer 對象后,程序會自動綁定并監(jiān)聽流 socket。TCPServer 只支持會話模式

所謂的 “普通模式”,也就是應(yīng)用程序注冊 Server 對象的入口函數(shù),并且由應(yīng)用程序操作 Server 對象的行為。

所謂的 “會話模式”,指的是 UDPServer 或 TCPServer 對象,在接收到傳入數(shù)據(jù)后,自動區(qū)分客戶端,并單獨創(chuàng)建 Session 對象進(jìn)行處理。每個 Session 對象只服務(wù)于一個客戶端。

Session 類

Session 對象不能由應(yīng)用主動創(chuàng)建,而是由處于會話模式的 Server 類自動按需創(chuàng)建。Session 對象的特點是,只能與單一一個客戶端(相比起 UDPServer 對象而言)進(jìn)行通信,因此沒有 send() 函數(shù),只有 reply() 。

在頭文件 coevent.h 聲明的 Session 類及其子類均為純虛類,目的是防止應(yīng)用程序顯式地構(gòu)建 Session 對象并隱藏實現(xiàn)細(xì)節(jié)。

Client 類

Client 對象由 Procedure 對象創(chuàng)建,并且由 Procedure 對象進(jìn)行回收。Client 對象的作用是主動向遠(yuǎn)程服務(wù)器發(fā)起通信。由于從客戶-服務(wù)結(jié)構(gòu)的角度,這個動作屬于客戶端,所以命名為 Client。

DNSClient

Client 的子類中比較特別的是 DNSClient 類,這個類的存在是為了解決在異步 I/O 中的 getaddrinfo() 阻塞問題。DNSClient 的實現(xiàn)原理請參見代碼和我之前的文章《DNS 報文結(jié)構(gòu)和個人 DNS 解析代碼實現(xiàn)》。

而對于 DNSClient 類而言,具體實現(xiàn)原理,就是封裝了一個 UDPClient 對象,通過該對象完成 DNS 報文的收發(fā),并在類中實現(xiàn)報文的解析。

UDPServer——基于 libevent 的協(xié)程實現(xiàn)

UDPServer 類普通模式的原理,就是一個非常典型的基于 libevent 的同步協(xié)程服務(wù)器框架。其代碼實現(xiàn)中,核心功能就是以下幾個函數(shù):

  • _libco_routine(),協(xié)程的入口函數(shù),使用這個函數(shù),轉(zhuǎn)化成為 liboevent 的統(tǒng)一服務(wù)入口函數(shù)

  • _libevent_callback(),libevent 時間回調(diào)函數(shù),在這個函數(shù)里,實現(xiàn)協(xié)程上下文的恢復(fù)。

  • UDPServer::recv_in_timeval(),數(shù)據(jù)接收函數(shù),在這個函數(shù)中,實現(xiàn)關(guān)鍵的數(shù)據(jù)等待功能,同時實現(xiàn)了協(xié)程上下文的保存

上述三個函數(shù)的代碼總量,加上空行也不超過 200 行,我相信還是很容易看明白的。以下具體解釋實現(xiàn)原理:

libco 協(xié)程接口

正如前文所說,我使用的是 libco 作為協(xié)程庫。協(xié)程對于應(yīng)用程序是透明的,但是對于庫的實現(xiàn)而言,這才是核心。

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

創(chuàng)建和銷毀協(xié)程

Libco 使用結(jié)構(gòu)體 struct stCoRoutine_t * 保存協(xié)程,通過調(diào)用 co_create() 可以創(chuàng)建協(xié)程對象;使用 co_release() 銷毀協(xié)程資源。

進(jìn)入?yún)f(xié)程

創(chuàng)建了協(xié)程之后,調(diào)用 co_resume() 可以從協(xié)程函數(shù)的開頭開始執(zhí)行協(xié)程。

暫停協(xié)程

當(dāng)協(xié)程到了需要交出 CPU 使用權(quán)的時候,可以調(diào)用 co_yield() 釋放協(xié)程、切換掉上下文。調(diào)用之后,上下文會恢復(fù)到上一個調(diào)用 co_resume() 的協(xié)程中。調(diào)用 co_yield() 的位置可以視為一個 “斷點”。

恢復(fù)協(xié)程

恢復(fù)協(xié)程和創(chuàng)建協(xié)程所用的函數(shù)都是 co_resume(),調(diào)用該函數(shù),將當(dāng)前堆棧切換為指定協(xié)程的上下文,協(xié)程會從上文提到的 “斷點” 恢復(fù)執(zhí)行。

協(xié)程調(diào)度實現(xiàn)

從上一小節(jié)可以看到,我們使用到的 libco 協(xié)程功能函數(shù)中,雖然包含了協(xié)程的切換函數(shù),但什么時候切換、切換之后 CPU 如何分配,這是我們需要實現(xiàn)并封裝起來的工作。

創(chuàng)建和銷毀協(xié)程的時機,自然就是在 UDPServer 類初始化和析構(gòu)的時候。下文重點解析進(jìn)入、暫停和恢復(fù)協(xié)程的操作:

進(jìn)入?yún)f(xié)程

進(jìn)入 / 恢復(fù)協(xié)程的代碼,是在 _libevent_callback() 中,有這么一行:

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

如果當(dāng)前協(xié)程還沒有被執(zhí)行過,那么執(zhí)行了這句代碼之后,程序會切換到創(chuàng)建 libco 協(xié)程時指定的協(xié)程函數(shù)開始執(zhí)行。對于 UDPServer,也就是 _libco_routine() 函數(shù)。這個函數(shù)非常簡單,只有三行:

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;
}

通過傳入?yún)?shù),將 libco 回調(diào)函數(shù)轉(zhuǎn)換為應(yīng)用程序指定的服務(wù)器函數(shù)執(zhí)行。

但是如何實現(xiàn)第一次的 libevent 回調(diào)呢?這還是很簡單的,只需要在調(diào)用 libevent 的 event_add()時,將超時時間設(shè)置為 0 即可,這會導(dǎo)致 libevent 事件立即超時。通過這個機制,我們也就實現(xiàn)了在 Base 運行之后立即執(zhí)行各 Procedure 服務(wù)函數(shù)的目的。

暫停和恢復(fù)協(xié)程

在什么時候調(diào)用 co_yield是本協(xié)程實現(xiàn)的重點,調(diào)用 co_yield 的位置,是一個可能會導(dǎo)致上下文切換的地方,也是將異步編程框架轉(zhuǎn)換為同步框架的關(guān)鍵技術(shù)點。這里可以參照 UDPServerrecv_in_timeval() 函數(shù)。函數(shù)的基本邏輯如下:

C/C++ 協(xié)程用于服務(wù)器的實現(xiàn)方法

其中最重要的分支,就是對 libevent 事件標(biāo)志的判斷;而最重要的邏輯,就是 event_add()co_yield() 函數(shù)的調(diào)用。函數(shù)片段如下:

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() 函數(shù)理解為一個斷點,當(dāng)程序執(zhí)行到這里的時候,CPU 的使用權(quán)會被交出,程序回到調(diào)用 co_resume() 的上一級函數(shù)手中。這個 “上一級函數(shù)” 究竟是哪里呢?實際上就是前文提到的 _libevent_callback() 函數(shù)。

_libevent_callback() 的角度來看,程序會從 co_resume() 函數(shù)返回,并且繼續(xù)往下執(zhí)行。此時我們可以這么理解:協(xié)程的調(diào)度,實際上是借用了 libevent來進(jìn)行的。這里我們要關(guān)注一下 co_resume() 上方的幾句:

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

這里將 libevent 事件 flag 值傳遞給了協(xié)程,而這是前文進(jìn)行事件判斷的重要依據(jù)。當(dāng)時間到來,_libevent_callback() 會在下面調(diào)用 co_resume() 的位置,將 CPU 使用權(quán)交回給協(xié)程。

銷毀協(xié)程

除了 ci_yield() 之外,協(xié)程函數(shù)調(diào)用 return 也會導(dǎo)致從 co_resume() 返回,所以在 _libevent_callback() 中,我們還需要判斷協(xié)程是否已經(jīng)結(jié)束。如果協(xié)程結(jié)束,那么就應(yīng)當(dāng)銷毀相關(guān)的協(xié)程資源了。參見 if (is_coroutine_end(arg->coroutine)) {...} 條件體內(nèi)的代碼。

會話模式(Session Mode)

在本工程的實現(xiàn)中,提供了被稱為 “會話模式” 的一個服務(wù)器設(shè)計模式。會話模式指的是 UDPServer 或 TCPServer 對象,在接收到傳入數(shù)據(jù)后,自動區(qū)分客戶端,并單獨創(chuàng)建 Session 對象進(jìn)行處理。每個 Session 對象只服務(wù)于一個客戶端。

對于 TCPServer 而言,實現(xiàn)上述的功能比較簡單,因為監(jiān)聽一個 TCP socket 之后,當(dāng)有傳入連接的時候,只要調(diào)用 accept(),就可以獲得一個新的文件描述符,為這個文件描述符創(chuàng)建一個新的 Server 的子類就行了——這就是 TCPSession 類。

但是 UDPServer 就比較麻煩了,因為 UDP 不能這么做。我們只能自行實現(xiàn)所謂的 session。

UDPSession 實現(xiàn)

設(shè)計目標(biāo)

我們需要實現(xiàn) UDPSession 類的如下效果:

  • 類調(diào)用 recv 函數(shù)時,只會接收到對應(yīng)的遠(yuǎn)程客戶端發(fā)來的數(shù)據(jù)

  • 類調(diào)用 send 函數(shù)(實際實現(xiàn)是 reply())時,可以使用 UDPServer 的端口進(jìn)行回復(fù)

recv()

在工程中,UDPSession 是抽象類,實際實現(xiàn)是 UDPItnlSession。但是準(zhǔn)確而言,UDPItnlSession 的實現(xiàn),密切依賴于 UDPServer。這一部分,可以參照 UDPServer_session_mode_worker() 函數(shù)中的 do-while() 循環(huán)體代碼。程序思路如下:

  • UDPServer 維護(hù)一個 UDPSession 字典,以遠(yuǎn)程 IP + 端口名的組合作為 key。

  • 當(dāng)數(shù)據(jù)到來時,判斷遠(yuǎn)程 IP + 端口的組合是否在字典中,如果在,那么就把數(shù)據(jù)復(fù)制給對應(yīng)的 session;如果不存在,則創(chuàng)建 session

復(fù)制數(shù)據(jù)的代碼,參見 UDPItnlSession 類的 forward_incoming_data() 函數(shù)實現(xiàn)。

reply()

發(fā)送數(shù)據(jù)其實就很簡單,直接對 UDPServer 的 fd 進(jìn)行 sendto() 就可以了。

quit

對于 session mode 的 Server 對象,代碼中提供了一個可以由其 session 調(diào)用的、要求 server 退出并銷毀資源的函數(shù):quit_session_mode_server()。實現(xiàn)原理是向 server 觸發(fā)一個 EV_SIGNAL 事件。對于普通的 I/O 事件而言,這是不應(yīng)當(dāng)出現(xiàn)的,我們這里活用來作為退出信號。如果 server 發(fā)現(xiàn)了這個信號,則觸發(fā)退出邏輯。

應(yīng)用示例

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

Server 的代碼,分別針對 Server 類的三個子類做了應(yīng)用示例。使用了包括空行、調(diào)試語句、錯誤判斷等在內(nèi)的邏輯,僅使用不到 300 行,就實現(xiàn)了一個過程和兩個服務(wù)。應(yīng)該說,邏輯還是很清晰的,而且也節(jié)省了大量代碼。

SubRoutine

通過函數(shù) _simple_test_routine(),展示了一個一次性的線性網(wǎng)絡(luò)邏輯。程序中,routine 首先創(chuàng)建了一個 DNSClient 對象,向默認(rèn)域名服務(wù)器請求了一個域名,然后 connect() 該服務(wù)器的 80 端口。成功后,直接返回。

這個函數(shù)展示了 SubRoutine 的使用場景,以及 Client 對象的使用方法,特別是 DNSClient 的簡易使用方法。

UDPServer

UDPServer 的入口函數(shù)是 _udp_session_routine(),功能是為客戶端提供域名查詢服務(wù)。Clients 發(fā)送一段字符串作為待查詢域名,然后 server 通過 DNSClient 對象請求后,將查詢結(jié)果返回給客戶端。

這個函數(shù)展示了 UDPSession 對象和 DNSClient 的(比較復(fù)雜和完整的)使用方法。

TCPServer

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

感謝各位的閱讀!看完上述內(nèi)容,你們對C/C++ 協(xié)程用于服務(wù)器的實現(xiàn)方法大概了解了嗎?希望文章內(nèi)容對大家有所幫助。如果想了解更多相關(guān)文章內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI