C++多程序併發框架FFLIB
三年來一直從事伺服器程式開發,一直都是忙忙碌碌,不久前結束了職業生涯的第一份工作,有了一個禮拜的休息時間,終於可以寫寫總結了。於是把以前的開原始碼做了整理和優化,這就是FFLIB。雖然這邊總結看起來像日記,有很多廢話,但是此文仍然是有很大針對性的。針對伺服器開發中常見的問題,如多執行緒併發、訊息轉發、非同步、效能優化、單元測試,提出自己的見解。
面對的問題
從事開發工程中,遇到過不少問題,很多時候由於時間緊迫,沒有使用優雅的方案。在跟業內的一些朋友交流過程中,我也意識到有些問題是大家都存在的。簡單列舉如下:
- 多執行緒與併發
- 非同步訊息/介面呼叫
- 訊息的序列化與Reflection
- 效能優化
- 單元測試
多執行緒與併發
現在是多核時代,併發才能實現更高的吞吐量、更快的響應,但也是把雙刃劍。總結如下幾個用法:
- 多執行緒+顯示鎖;介面是被多執行緒呼叫的,當被呼叫時,顯示加鎖,再操作實體資料。悲劇的是,工程師為了優化會設計多個鎖,以減少鎖的粒度,甚至有些地方使用了原子操作。這些都為領域邏輯增加了額外的設計負擔。最壞的情況是會出現死鎖。
- 多執行緒+任務佇列;介面被多執行緒呼叫,但請求會被暫存到任務佇列,而任務佇列會被單執行緒不斷執行,典型生產者消費者模式。它的併發在於不同的介面可以使用不同的任務佇列。這也是我最常用的併發方式。
這是兩種最常見的多執行緒併發,它們有個天生的缺陷——Scalability。一個機器的效能總是有瓶頸的。兩個場景的邏輯雖然由多個執行緒實現了併發,但是運算量十分有可能是一臺機器無法承載的。如果是多程序併發,那麼可以分散式把其部署到其他機器(也可部署在一臺機器)。所以多程序併發比多執行緒併發更加Scalability。另外採用多程序後,每個程序單執行緒設計,這樣的程式更加Simplicity。多程序的其他優點如解耦、模組化、方便除錯、方便重用等就不贅言了。
非同步訊息/介面呼叫
提到分散式,就要說一下分散式的通訊技術。常用的方式如下:
- 類RPC;包括WebService、RPC、ICE等,特點是遠端同步呼叫。遠端的介面和本地的介面非常相似。但是遊戲伺服器程式一般非常在意延遲和吞吐量,所以這些阻塞執行緒的同步遠端呼叫方式並不常用。但是我們必須意識到他的優點,就是非常利於呼叫和測試。
- 全非同步訊息;當呼叫遠端介面的時候,非同步傳送請求訊息,介面響應後返回一個結果訊息,呼叫方的回撥函式處理結果訊息繼續邏輯操作。所以有些邏輯就會被切割成ServiceStart和ServiceCallback兩段。有時非同步會講領域邏輯變得支離破碎。另外訊息處理函式中一般會寫一坨的switch/case 處理不同的訊息。最大的問題在於單元測試,這種情況傳統單元測試根本束手無策。
訊息的序列化與Reflection
實現訊息的序列化和反序列化的方式有很多,常見的有Struct、json、Protobuff等都有很成功的應用。我個人傾向於使用輕量級的二進位制序列化,優點是比較透明和高效,一切在掌握之中。在FFLIB 中實現了bin_encoder_t 和 bin_decoder_t 輕量級的訊息序列化,幾十行程式碼而已。
效能優化
已經寫過關於效能方面的總結,參見
有的網友提到profiler、cpuprofiler、callgrind等工具。這些工具我都使用過,說實話,對於我來說,我太認同它有很高的價值。第一他們只能用於開發測試階段,可以初步得到一些效能上參考資料。第二它們如何實現跟蹤人們無從得知。執行其會使程式變慢,不能反映真實資料。第三重要的是,開發測試階段效能和上線後的能一樣嗎?Impossible !
關於效能,原則就是資料說話,詳見博文,不在贅述。
單元測試
關於單元測試,前邊已經談論了一些。遊戲伺服器程式一般都比較龐大,但是不可思議的是,鄙人從來沒見有專案(c++ 後臺架構的)有完整單元測試的。由於存在著非同步和多執行緒,傳統的單元測試框架無法勝任,而開發支援非同步的測試框架又是不現實的。我們必須看到的是,傳統的單元測試框架已經取得了非常大的成功。據我瞭解,使用web 架構的遊戲後臺已經對於單元測試的使用已經非常成熟,取得了極其好的效果。所以我的思路是利用現有的單元測試框架,將非同步訊息、多執行緒的架構做出調整。
已經多次談論單元測試了。其實在開發FFLIB的思路很大程度來源於此,否則可能只是一個c++ 網路庫而已。我決定嘗試去解決這個問題的時候,把FFLIB 定位於框架。
先來看一段非常簡單的單元測試的程式碼 :
Assert(2 == Add(1, 1));
請允許我對這行程式碼做些解釋,對Add函式輸入引數,驗證返回值是否是預期的結果。這不就是單元測試的本質嗎?在想一下我們非同步傳送訊息的過程,如果每個輸入訊息約定一個結果訊息包,每次傳送請求時都繫結一個回撥函式接收和驗證結果訊息包。這樣的話就恰恰滿足了傳統單元測試的步驟了。最後還需解決一個問題,Assert是不能處理非同步的返回值的。幸運的是,future機制可以化非同步為同步。不瞭解future 模式的可以參考這裡:
來看一下在FFLIB框架下遠端呼叫echo 服務的示例:
struct lambda_t { static void callback(echo_t::out_t& msg_) { echo_t::in_t in; in.value = "XXX_echo_test_XXX"; singleton_t<msg_bus_t>::instance() .get_service_group("echo") ->get_service(1)->async_call(in, &lambda_t::callback); } }; echo_t::in_t in;
in.value = "XXX_echo_test_XXX"; singleton_t<msg_bus_t>::instance().get_service_group("echo")->get_service(1)->async_call(in, &lambda_t::callback);
當需要呼叫遠端介面時,async_call(in, &lambda_t::callback); 非同步呼叫必須繫結一個回撥函式,回撥函式接收結果訊息,可以觸發後續操作。這樣的話,如果對echo 的遠端介面做單元測試,可以這樣做:
rpc_future_t< echo_t::out_t> rpc_future; echo_t::in_t in; in.value = "XXX_echo_test_XXX"; const echo_t::out_t& out = rpc_future.call( singleton_t<msg_bus_t>::instance() .get_service_group("echo")->get_service(1), in); Assert(in.value == out.value);
這樣所有的遠端介面都可以被單元測試覆蓋。
FFLIB 介紹
FFLIB 結構圖
程序間通訊採用TPC,而不是多執行緒使用的共享記憶體方式。Service 一般是單執行緒架構的,通過啟動多程序實現相對於多執行緒的併發。由於Broker模式天生石分散式的,所以有很好的Scalability。
訊息時序圖
如何註冊服務和介面
來看一下Echo 服務的實現:
struct echo_service_t { public: void echo(echo_t::in_t& in_msg_, rpc_callcack_t<echo_t::out_t>& cb_) { logtrace((FF, "echo_service_t::echo done value<%s>", in_msg_.value.c_str())); echo_t::out_t out; out.value = in_msg_.value; cb_(out); } }; int main(int argc, char* argv[]) { int g_index = 1; if (argc > 1) { g_index = atoi(argv[1]); } char buff[128]; snprintf(buff, sizeof(buff), "tcp://%s:%s", "127.0.0.1", "10241"); msg_bus_t msg_bus; assert(0 == singleton_t<msg_bus_t>::instance().open("tcp://127.0.0.1:10241") && "can't connnect to broker"); echo_service_t f; singleton_t<msg_bus_t>::instance().create_service_group("echo"); singleton_t<msg_bus_t>::instance().create_service("echo", g_index) .bind_service(&f) .reg(&echo_service_t::echo); signal_helper_t::wait(); singleton_t<msg_bus_t>::instance().close(); //usleep(1000); cout <<"\noh end\n"; return 0; }
- create_service_group 建立一個服務group,一個服務組可能有多個並行的例項
- create_service 以特定的id 建立一個服務例項
- reg 為該服務註冊介面
- 介面的定義規範為void echo(echo_t::in_t& in_msg_, rpc_callcack_t<echo_t::out_t>& cb_),第一個引數為輸入的訊息struct,第二個引數為回撥函式的模板特例,模板引數為返回訊息的struct 型別。介面無需知道傳送訊息等細節,只需將結果callback 即可。
- 註冊到Broker 後,所有Client都可獲取該服務
訊息定義的規範
我們約定每個介面(遠端或本地都應滿足)都包含一個輸入訊息和一個結果訊息。來看一下echo 服務的訊息定義:
struct echo_t { struct in_t: public msg_i { in_t(): msg_i("echo_t::in_t") {} virtual string encode() { return (init_encoder() << value).get_buff(); } virtual void decode(const string& src_buff_) { init_decoder(src_buff_) >> value; } string value; }; struct out_t: public msg_i { out_t(): msg_i("echo_t::out_t") {} virtual string encode() { return (init_encoder() << value).get_buff(); } virtual void decode(const string& src_buff_) { init_decoder(src_buff_) >> value; } string value; }; };
- 每個介面必須包含in_t訊息和out_t訊息,並且他們定義在介面名(如echo _t)的內部
- 所有訊息都繼承於msg_i, 其封裝了二進位制的序列化、反序列化等。構造時賦予型別名作為訊息的名稱。
- 每個訊息必須實現encode 和 decode 函式
這裡需要指出的是,FFLIB 中不需要為每個訊息定義對應的CMD。當介面如echo向Broker 註冊時,reg介面通過C++ 模板的型別推斷會自動將該msg name 註冊給Broker, Broker為每個msg name 分配唯一的msg_id。Msg_bus 中自動維護了msg_name 和msg_id 的對映。Msg_i 的定義如下:
struct msg_i : public codec_i { msg_i(const char* msg_name_): cmd(0), uuid(0), service_group_id(0), service_id(0), msg_id(0), msg_name(msg_name_) {} void set(uint16_t group_id, uint16_t id_, uint32_t uuid_, uint16_t msg_id_) { service_group_id = group_id; service_id = id_; uuid = uuid_; msg_id = msg_id_; } uint16_t cmd; uint16_t get_group_id() const{ return service_group_id; } uint16_t get_service_id() const{ return service_id; } uint32_t get_uuid() const{ return uuid; } uint16_t get_msg_id() const{ return msg_id; } const string& get_name() const { if (msg_name.empty() == false) { return msg_name; } return singleton_t<msg_name_store_t>::instance().id_to_name(this->get_msg_id()); } void set_uuid(uint32_t id_) { uuid = id_; } void set_msg_id(uint16_t id_) { msg_id = id_;} void set_sgid(uint16_t sgid_) { service_group_id = sgid_;} void set_sid(uint16_t sid_) { service_id = sid_; } uint32_t uuid; uint16_t service_group_id; uint16_t service_id; uint16_t msg_id; string msg_name; virtual string encode(uint16_t cmd_) { this->cmd = cmd_; return encode(); } virtual string encode() = 0; bin_encoder_t& init_encoder() { return encoder.init(cmd) << uuid << service_group_id << service_id<< msg_id; } bin_encoder_t& init_encoder(uint16_t cmd_) { return encoder.init(cmd_) << uuid << service_group_id << service_id << msg_id; } bin_decoder_t& init_decoder(const string& buff_) { return decoder.init(buff_) >> uuid >> service_group_id >> service_id >> msg_id; } bin_decoder_t decoder; bin_encoder_t encoder; };
關於效能
由於遠端介面的呼叫必須通過Broker, Broker會為每個介面自動生成效能統計資料,並每10分鐘輸出到perf.txt 檔案中。檔案格式為CSV,參見:
總結
FFLIB框架擁有如下的特點:
- 使用多程序併發。Broker 把Client 和Service 的位置透明化
- Service 的介面要註冊到Broker, 所有連線Broker的Client 都可以呼叫(publisher/ subscriber)
- 遠端呼叫必須繫結回撥函式
- 利用future 模式實現同步,從而支援單元測試
- 訊息定義規範簡單直接高效
- 所有service的介面效能監控資料自動生成,免費的午餐
- Service 單執行緒話,更simplicity
原始碼:
執行示例:
- Cd example/broker && make && ./app_broker –l http://127.0.0.1:10241
- Cd example/echo_server && make && ./app_echo_server
- Cd example/echo_client && make && ./app_echo_client
相關連線
相關推薦
C++多程序併發框架FFLIB
三年來一直從事伺服器程式開發,一直都是忙忙碌碌,不久前結束了職業生涯的第一份工作,有了一個禮拜的休息時間,終於可以寫寫總結了。於是把以前的開原始碼做了整理和優化,這就是FFLIB。雖然這邊總結看起來像日記,有很多廢話,但是此文仍然是有很大針對性的。針對伺服器開發中常
C++多程序併發框架
轉自:http://blogread.cn/it/article/5630 三年來一直從事伺服器程式開發,一直都是忙忙碌碌,不久前結束了職業生涯的第一份工作,有了一個禮拜的休息時間,終於可以寫寫總結了。於是把以前的開原始碼做了整理和優化,這就是FFLIB。雖然這邊總結看起來
SimpleFork php多程序併發框架
SimpleFork 基於PCNTL擴充套件的多程序程序併發框架,介面類似與Java的Thread和Runnable 為什麼要寫SimpleFork 多程序程式的編寫相比較多執行緒編寫更加複雜,需要考慮程序回收、同步、互斥、通訊等問題。對於初學者來說,處理上述問題
多程序併發C/S通訊基本模型及實現
本例實現如下功能: 服務端接收來自客戶端傳送過來的字串,將小寫轉換為大寫後傳送回客戶端。 其中,每一個新客戶端連線後,服務端主程序為此客戶端建立一個子程序進行資料的處理。 多程序併發服務端程式
linux fork多程序併發伺服器模型之C/C++程式碼實戰
在很早的文章中, 我們一起聊過伺服器如何與多個客戶端進行通訊, 那時, 我們要麼用select, 要麼用多執行緒, 卻沒有用多程序。 其實, 多程序也可以實現與多個客戶端進行通訊。 如果是在while中迴圈accept, 然後迴圈處理事情
Python學習多程序併發寫入同一檔案
最近學習了Python的多程序,想到我的高德API爬蟲那個爬取讀寫速度我就心累,實在是慢,看到多程序可以充分利用CPU核數我就開始完善我的程式碼,不過過程是艱辛的,在此之中出現了很多問題,其中最大的問題是爬取的資料是正確的,但是讀寫到Excel中卻開啟是空,想了半天也沒解決,腦子笨沒辦法,不過我
Python多程序併發操作程序池Pool
目錄: multiprocessing模組 Pool類 apply apply_async map close terminate join 程序例項 multiprocessing模組 如果你打算編寫多程序的服務程式,Unix/
Linux多程序併發伺服器(TCP)
Linux多程序併發伺服器(TCP) 前言:在Linux環境下多程序的應用很多,其中最主要的就是網路/客戶伺服器。多程序伺服器是當客戶有請求時 ,伺服器用一個子程序來處理客戶請求。父程序繼續等待其它客戶的請求。這種方法的優點是當客戶有請求時 ,伺服器能及時處理客戶 ,特別是在客戶伺服
Linux學習之網路程式設計(多程序併發伺服器)
言之者無罪,聞之者足以戒。 - “詩序” 上面我們所說過的通訊都是一個伺服器一個客戶端之間的通訊,下面我們來交流一下多程序併發伺服器的相關知識 邏輯上就是這個樣子的,就是一個伺服器多個客戶端進行資料的傳輸。 1、傳送資料的函式: ssize_t send(int sockfd,
Python多程序 併發 multiprocessing庫
庫:multipprocessing 例項: #!/usr/bin/env python # -*- coding:utf-8 -*- import multiprocessing import time def action(sub_process_name): f
linux網路程式設計之多程序併發伺服器
1)使用多程序併發伺服器考慮的因素: (1)父程序描述最大檔案描述符的個數(父程序需要關閉accept返回的新檔案描述符) (2)系統內可建立程序的個數(與記憶體大小相關) (3)程序建立過多是否降低整體服務效能 2)多程序建立併發
嵌入式linux-sqlite3資料庫,多程序併發伺服器,線上詞典
文章目錄 1,簡介: 2,框架圖 2.1,客戶端框架 2.1,伺服器端框架 3,程式碼 3.1,客戶端程式碼 3.2,伺服器端程式碼 1,簡介: 1,線上詞典
嵌入式Linux網路程式設計,TCP多併發伺服器,TCP多執行緒併發伺服器,TCP多程序併發伺服器
文章目錄 1,TCP多執行緒併發伺服器 1.1,標頭檔案net.h 1.2,客戶端client.c 1.3,伺服器端server.c 2,TCP多程序併發伺服器 2.1,標頭檔案net.h 2.2,客
python之多執行緒多程序併發通訊
1.獨立的程序記憶體空間與共享的伺服器程序空間 程序之間是:互不干擾的獨立記憶體空間 #!/usr/bin/env python # -*- coding: utf-8 -*- # @Time :
一個跨平臺的多程序合作框架(一)基本原理
在上一篇文章中,我們探討了一種通過程序交換的合作框架。經過同學們半年多的討論、實驗,目前實現了這個想法。本篇首先介紹基本原理,後續將詳細講解開發過程。 本文的GitHub連結為 這裡 1. 什麼是Taskbus Taskbus 是一種面向非專業開發者的跨平臺多
C++ 多執行緒框架 (2):Mutex 互斥和 Sem 訊號量
互斥和訊號量是多執行緒程式設計的兩個基礎,其原理就不詳細說了,大家去看看作業系統的書或者網上查查吧。 對於互斥的實現,無論什麼作業系統都離不開三個步驟 1.初始化互斥鎖 2.鎖操作 3.解鎖操作 對於不同的系統只是實現的函式有一些不同而已,但是功能其實都大同小異,在
C++ 多執行緒框架(1):new 一下就啟動一個執行緒
幾年前寫過一個C++的多執行緒框架,雖然寫完了,但是人一懶做了一次說明以後就沒影了,最近把程式碼整理了一下,準備發到github上,在這裡,再把這個框架總結一下吧。 多執行緒一直是程式設計中常見的問題,特別是在Linux的c++上,多執行緒的封裝一直不是很好,當然,
C++ 多執行緒框架(3):訊息佇列
之前,多執行緒一些基本的東西,包括執行緒建立,互斥鎖,訊號量,我們都已經封裝,下面來看看訊息佇列 我們儘量少用系統自帶的訊息佇列(比如Linux的sys/msgqueue),那樣移植性不是很強,我們希望的訊息佇列,在訊息打包和提取都是用的標準的C++資料結構,當然,
python3實現多程序併發任務
在python開發中,有時候會有這樣的需求,比如說我後很多個任務,需要並行執行,也就是說有一個任務佇列,大家都知道,在python中的多執行緒,它其實從嚴格意義上來講,並不是真正的多執行緒。所以用多執行緒我們還不如使用多程序。使用多程序的有什麼好處了,它可以實現分散式多機並行。多個客戶端共享一個
Python多程序併發(multiprocessing)
由於Python下呼叫Linux的Shell命令都需要等待返回,所以常常我們設定的多執行緒都達不到效果, 因此在呼叫shell命令不需要返回時,使用threading模組並不是最好的方法。 Python提供了非常好用的多程序包multipro