基於彙編的 C/C++ 協程(用於服務器),我以前已經在下面兩篇文章中詳細闡述了原理:git
而這篇文章,就終因而 C/C++ 協程的實現了。正如上面兩篇文章所說的,咱們須要實現的目標有兩個:github
結構上,就是將 libco 和 libevent 二者的功能糅合起來,因此我把個人工程,命名爲 libcoevent,意爲 「基於 libevent 的同步協程服務器編程框架」。名字中 co 的意思並不表明 libco,而是 coroutine。編程
編程語言上,我選擇的是 C++,主要是由於 libco 只支持基於 x86 或 x64 架構的 Linux,而這樣的架構,基本上都是 PC 機,或者是資源不缺、性能也不錯的嵌入式系統,上 C++ 徹底沒有問題。本文解釋代碼實現的原理。segmentfault
若是要使用該工程,請在連接選項中加入 -lco -levent -lcoevent
三個選項。詳情參考 test
目錄。設計模式
本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。服務器
原文發佈於:https://cloud.tencent.com/developer/article/1171032,也是本人的專欄。網絡
類的基本繼承關係圖以下:session
在實際調用中,只有處於繼承關係樹的葉子結點上的類纔會被實際使用到,其餘類均視爲虛類。架構
各種的實例在程序運行中是有從屬關係的,除了做爲頂層的 Base 類以外,其餘樹葉類都需依附於其餘的類所在的運行環境中才能執行。從屬關係圖以下:app
Procedure 對象管理 Client 對象。在圖中體現爲 Server 和 Session 對象均管理 Client 對象。
return
)或其從屬的 Server 對象服務結束時,由 Server 對象自動銷燬。Base 類用於運行 libcoevent 的各個服務。每一個 Base 類的實例應對應着一個線程,全部的服務以協程的方式在 Base 實例中運行。從上圖可知,Base 類包含一個 libevent 庫的 event_base
對象和本協程庫的一系列 Event 對象。
Event 類實際上是借用了 libevent 的 struct event
名稱,由於每個 Event 類的實例,對應着 libevent 的一個 event
對象。咱們須要關注的重點,是 Procedure 和 Client 類。
Procedure 類有兩個關鍵特色:
Procedure 類擁有兩個子類,分別是 Server 和 Session。
Server 類由應用程序建立並初始化到 Base 對象中運行。Server 類有三個子類:
sleep()
函數,並支持 Procedure 類的建立 Client 對象的功能,所以應用程序能夠用來做爲臨時建立或常駐的內部程序來使用。所謂的 「普通模式」,也就是應用程序註冊 Server 對象的入口函數,而且由應用程序操做 Server 對象的行爲。
所謂的 「會話模式」,指的是 UDPServer 或 TCPServer 對象,在接收到傳入數據後,自動區分客戶端,並單首創建 Session 對象進行處理。每一個 Session 對象只服務於一個客戶端。
Session 對象不能由應用主動建立,而是由處於會話模式的 Server 類自動按需建立。Session 對象的特色是,只能與單一一個客戶端(相比起 UDPServer 對象而言)進行通訊,所以沒有 send()
函數,只有 reply()
。
在頭文件 coevent.h
聲明的 Session 類及其子類均爲純虛類,目的是防止應用程序顯式地構建 Session 對象並隱藏實現細節。
Client 對象由 Procedure 對象建立,而且由 Procedure 對象進行回收。Client 對象的做用是主動向遠程服務器發起通訊。因爲從客戶-服務結構的角度,這個動做屬於客戶端,因此命名爲 Client。
Client 的子類中比較特別的是 DNSClient 類,這個類的存在是爲了解決在異步 I/O 中的 getaddrinfo()
阻塞問題。DNSClient 的實現原理請參見代碼和我以前的文章《DNS 報文結構和我的 DNS 解析代碼實現》。
而對於 DNSClient 類而言,具體實現原理,就是封裝了一個 UDPClient 對象,經過該對象完成 DNS 報文的收發,並在類中實現報文的解析。
UDPServer 類普通模式的原理,就是一個很是典型的基於 libevent 的同步協程服務器框架。其代碼實現中,核心功能就是如下幾個函數:
_libco_routine()
,協程的入口函數,使用這個函數,轉化成爲 liboevent 的統一服務入口函數_libevent_callback()
,libevent 時間回調函數,在這個函數裏,實現協程上下文的恢復。UDPServer::recv_in_timeval()
,數據接收函數,在這個函數中,實現關鍵的數據等待功能,同時實現了協程上下文的保存上述三個函數的代碼總量,加上空行也不超過 200 行,我相信仍是很容易看明白的。如下具體解釋實現原理:
正如前文所說,我使用的是 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
的位置,是一個可能會致使上下文切換的地方,也是將異步編程框架轉換爲同步框架的關鍵技術點。這裏能夠參照 UDPServer 的 recv_in_timeval()
函數。函數的基本邏輯以下:
其中最重要的分支,就是對 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)) {...}
條件體內的代碼。
在本工程的實現中,提供了被稱爲 「會話模式」 的一個服務器設計模式。會話模式指的是 UDPServer 或 TCPServer 對象,在接收到傳入數據後,自動區分客戶端,並單首創建 Session 對象進行處理。每一個 Session 對象只服務於一個客戶端。
對於 TCPServer 而言,實現上述的功能比較簡單,由於監聽一個 TCP socket 以後,當有傳入鏈接的時候,只要調用 accept()
,就能夠得到一個新的文件描述符,爲這個文件描述符建立一個新的 Server 的子類就好了——這就是 TCPSession 類。
可是 UDPServer 就比較麻煩了,由於 UDP 不能這麼作。咱們只能自行實現所謂的 session。
咱們須要實現 UDPSession 類的以下效果:
reply()
)時,可使用 UDPServer 的端口進行回覆在工程中,UDPSession 是抽象類,實際實現是 UDPItnlSession。可是準確而言,UDPItnlSession 的實現,密切依賴於 UDPServer。這一部分,能夠參照 UDPServer 的 _session_mode_worker()
函數中的 do-while()
循環體代碼。程序思路以下:
複製數據的代碼,參見 UDPItnlSession 類的 forward_incoming_data()
函數實現。
發送數據其實就很簡單,直接對 UDPServer 的 fd 進行 sendto()
就能夠了。
對於 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 行,就實現了一個過程和兩個服務。應該說,邏輯仍是很清晰的,並且也節省了大量代碼。
經過函數 _simple_test_routine()
,展現了一個一次性的線性網絡邏輯。程序中,routine 首先建立了一個 DNSClient 對象,向默認域名服務器請求了一個域名,而後 connect()
該服務器的 80 端口。成功後,直接返回。
這個函數展現了 SubRoutine 的使用場景,以及 Client 對象的使用方法,特別是 DNSClient 的簡易使用方法。
UDPServer 的入口函數是 _udp_session_routine()
,功能是爲客戶端提供域名查詢服務。Clients 發送一段字符串做爲待查詢域名,而後 server 經過 DNSClient 對象請求後,將查詢結果返回給客戶端。
這個函數展現了 UDPSession 對象和 DNSClient 的(比較複雜和完整的)使用方法。
入口函數是 _tcp_session_routine()
,邏輯比較簡單,主要是展現 TCPSession 的用法。
原理上,libcoevent 已經開發完了,實現了必須的功能,徹底能夠用來編寫服務器程序。固然因爲這是第一版,因此不少代碼看起來仍是有點亂。這個庫的意義在於,能夠從教學角度,仔細地說明 C/C++ 協程更爲本源的實現原理,也能夠做爲一個可用的協程服務器庫來使用。
歡迎讀者針對這個庫多多批判,也歡迎讀者提出新需求——好比我就決定加幾個需求,算是 TODO 吧:
至此,《基於彙編的 C/C++ 協程》三大內容,也就完成了。後續若是有時間,我會再寫一篇產品使用文檔(readme),不過估計就不發佈在這裏了,而是直接發佈在個人 GitHub 上。請多關照~~
最後回顧一下本系列的三篇文章列表:
本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。
原文發佈於:https://cloud.tencent.com/developer/article/1171032,也是本人的專欄。