網路框架重構之路plain2.0(c++23 without module) 綜述

戀月發表於2023-04-11

最近網際網路行業一片哀嘆,這是受到三年影響的後遺症,許多的公司也未能挺過寒冬,一些外資也開始撤出市場,因此許多的IT從業人員加入失業的行列,而且由於公司較少導致許多人求職進度緩慢,很不幸本人也是其中之一。自從參加工作以來,一直都是忙忙碌碌,開始總認為工作只是為了更好的生活,但是一旦工作停下來後自己就覺得失去了一點什麼,所以很少有像最近這兩個月左右空閒的時光。人一旦空閒下來就未免有些空虛,空虛的原因大部分還是因為對未來的焦躁不安,比如要擔心口袋餘糧問題、擔心兢兢業業的別人會投以不屑的目光。於是在這期間我也嘗試應聘一些崗位,許多面試官問到plain framework是如何實現的時候,說實話一時間我沒法準確的把他們想知道的內容表述清楚,一方面是自己的口才欠缺,一方面確實是因為時隔太久對許多的東西產生了一定的遺忘。大家不要奇怪為何自己會忘了自己寫的東西,拿一件事來說就是我相信大多數人在工作之後對於自己曾經思慮再三覺得十分念念不忘的優美作文或者做題解法都說不出個所以然。人的記憶力是有限的,不然也就不會有那句好記性不如爛筆頭的名言了。重構plain的計劃實際上是從2019年C++準備釋出C++20標準的時候產生的,其module模組概念的引入讓我對於庫開發產生了一種十分美好的期盼。特別是去年年初我就一直在關注C++23,因此在去年就決定在今年內準備完成對plain的重構,其版本號直接提升到了plain2.0,其原因是相比現在的plain1.1rc產生了比較多的變化。我想說的是重構這個框架的目的之一是重拾自己的記憶,完成既定目標的一部分,同時也是增強自己在modern C++方面的技術,希望在這裡與朋友們一起學習和探討設計中遇到的一些問題。同時也順便提一句,大環境雖然是不好但是心中還是應當存有希望,寒冬過後一定會有春天,絕境之地也自然會有生機,盲目自怨自艾不如放鬆心情轉換思維提升自己(當然這裡不是說卷)。我相信未來總是美好的,祝願那些失業或者正要失業的朋友們有個好的工作和生活,也祝福正在工作或者無需工作的活得快樂。同時也希望這系列文章(可能會寫幾篇),給大家能夠帶來一定的收穫。順道說一句,這一些列文章不是C++學習的基礎,可能需要大家對該語言有一定程度的瞭解,該系列文章主要分享的是我在開發和設計中的一些心得體會,如果有不正確的地方希望大家在評論區留言,或者加入群聯絡我。我以前沒有詳細地寫過這方面的文章,主要還是因為太懶,而現在寫這些是因為人到了一定歲數是想要總結什麼的時候了,再說如果中年危機得不到緩解,網際網路或許不再是我的棲身之處,也希望在這裡留下一些有用的知識能夠讓大家對網路程式設計有一定的理解。(這篇文章幾乎花了一天時間,看來是因為自己反應力衰退了,如果大家認為有所幫助希望可以關注一下,後續我將繼續分享)

 原因

 plain1.1rc的不足

  1、名稱空間問題

    如果看過或者接觸過plain的朋友,不難發現名稱空間都是以pf_*開頭。說起這個的時候,還是要從plain的前身plainserver(https://github.com/viticm/plainserver)說起。我看了看時間,emm...,時間大概是九年前,那個時候我還深處在南方的沿海城市。從名字上來說,讓人很容易聯想到伺服器。這個專案有著非常強的針對性,是實現了一個伺服器的常用模型,一個多程式多執行緒模式下的遊戲伺服器的實現。這套模型是許多現今仍舊有許多老的廠商(搜狐、金山等)可能仍在應用的模式。其應用的原因大致是這些公司成立時間早,在這方面應用該模式已經得心應手(成熟的相對是可靠的,企業不願意冒風險去嘗試,畢竟嘗試是需要代價的)。有興趣的朋友們不妨可以看一下其中的實現,我在以前的文章裡提到過該構架並且用圖畫簡單的畫出了其模型。

    大家不妨看看plainserver的common目錄,這個目錄下的結構,再來對比下plain下的那些目錄,是不是感覺到有很多相似。沒有錯,plain就是在common上面衍生出來的,一個更精細、抽象和共用的庫,本來common就是作為客戶端和伺服器的公用庫而存在。然後重點問題來了,開啟檔案看看比如common/src/common/base/log.cc這個檔案,看看其名稱空間,不難發現namespace ps_common_base {,沒有錯這就是之前的命名規則,這是我看了google C++編碼規範之後,自己想的一個命名規則,不難看出這裡的規則是ps(專案名稱)_common(父目錄)_base(子目錄)。

    從上面的名稱空間的規則,不難發現plain現在的名稱空間實際上是在此基礎上做了一點略微調整。自從接觸名稱空間後,我就儘量想要讓函式或者物件被包裹起來,這樣是為了減少跨模組之間的衝突。但是如果以純粹用名稱空間和目錄結構來劃分的話,那麼目錄的層次結構必然會巢狀很深。於是在plain的時候規定目錄的巢狀最好少於三個層次,這樣也約束了名稱空間的巢狀。這樣就形成了pf_basic、pf_sys等一些列的名稱空間,同時也有二級巢狀pf_sys::thread這種。忘了提一點pf其實是plain framework的縮寫,說實話這樣命名本來沒有任何問題。可是在使用中會發現一些名稱空間和方法產生了重名,比如檔案(file)模組,這裡面封裝了一些跨平臺的檔案操作介面,如下:

 

namespace pf_file {                                                                                            
                                                                                                               
namespace api {                                                                                                
                                                                                                               
PF_API int32_t openex(const char *filename, int32_t flag);                      
PF_API int32_t openmode_ex(const char *filename, int32_t flag, int32_t mode);    
PF_API void closeex(int32_t fd);                                                                               
PF_API uint32_t readex(int32_t fd, void *buffer, uint32_t length);               
PF_API uint32_t writeex(int32_t fd, const void *buffer, uint32_t length);       
PF_API int32_t fcntlex(int32_t fd, int32_t cmd);                                                               
PF_API int32_t fcntlarg_ex(int32_t fd, int32_t cmd, int32_t arg);               
PF_API bool get_nonblocking_ex(int32_t socketid);                               
PF_API void set_nonblocking_ex(int32_t socketid, bool on);                       
PF_API void ioctlex(int32_t fd, int32_t request, void *argp);                    
PF_API uint32_t availableex(int32_t fd);                                                                       
PF_API int32_t dupex(int32_t fd);                                                                              
PF_API int64_t lseekex(int32_t fd, uint64_t offset, int32_t whence);            
PF_API int64_t tellex(int32_t fd);                                                                             
PF_API bool truncate(const char *filename);                                                                    
PF_API inline bool exists(const std::string &filename) {                        
  auto fp = fopen(filename.c_str(), "r");                                                                      
  if (is_null(fp)) return false;                                                                               
  fclose(fp); fp = nullptr;                                                                                    
  return true;                                                                                                 
}                                                                                                              
                                                                                                               
} //namespace api                                                                                              
                                                                                                               
} //namespace pf_file

 

    總體上來說這個命名規則沒有問題,將內容巢狀起來(將介面包裹的十分好),到去年的時候我仍舊是這樣想的,儘量用名稱空間來減少這些衝突,我認為這個出發點是沒有任何問題的。可是在使用的時候,我們會這樣pf_file::api::func這種方式進行呼叫,實際上也沒有多大的問題。可是最近我發現這樣呼叫起來不是十分方便,其實這幾個介面就是對於標準介面的封裝,在後面加上了ex就是防止命名衝突,從C的介面來說這裡根本沒有名稱空間一說,我總覺得自己對於這樣的包裹確實有點過了頭。我認為目錄應該只是為了更好的分類讓使用者能夠快速定位,也不必非得和名稱空間扯上什麼關係,在呼叫的時候不需要包裹那麼深,我覺得從使用者的角度來講plain::func這種呼叫即可。其實這個規則的改變,是我對於std的一點理解,在標準庫中並不是所有東西都包裹的那麼強,因此使用起來比較方便。

  2、多餘的程式碼

    由於歷史的原因,因為plain源自於老的專案,其中參考了許多之前的設計,所以我在框架內部保留了許多之前封裝的方法,儘管這些方法可能在框架內部使用不上,但是想到使用者可能會在專案中使用因此還是保留了下來,因此這就造成了許多的冗餘。在看了一些開源的專案之後,我發現大多的開源專案中程式碼的冗餘是十分低的,除了框架需要的實現和外部介面以外任何多餘的方法和類都不考慮。減少程式碼並不意味著功能的減少,而且減少了自己的維護成本,以及使用者的困惑。畢竟當使用者面對那麼一大堆介面的時候,大多時候都表現的手足無措,這個其實和《Effective C++》(這兩月才在看,下面我會推薦一兩本)Item 18: 使介面易於正確使用,而難以錯誤使)的道理一致,我認為介面越少越精緻則使用者就越加能夠正確使用,這也大概就是大道至簡的實現方式吧。

    對於多餘的程式碼,plain2.0的辦法就是進行清除和保留,清除一些無關緊要的介面,留下來一些真的有用的東西。因為到了C++23後標準庫裡面多了許多有意思的東西,對於其已經實現的東西,比如本來想在C++11的plain1.1中想要使用google的StringPiece進行一定的最佳化,不過好在標準的提升,C++17裡面有了string_view,因此這個就不需要額外封裝了。也是因為這個原因,plain2.0d帶著實驗性的目的大刀闊斧的進行大版本的升級,去除裡面多餘的程式碼。特別是.tcc這種檔案,我準備全部移除,將模板的實現也放到.h中去。

    使用標準庫可以讓跨平臺的問題得到更好的解決,因為標準庫幫我們實現了許多介面,這些介面在C++98或者C++03都是沒有的,比如C++11中封裝成的std::thread,以前寫跨平臺的時候你需要熟悉pthread還有windows等相應執行緒API,plain1.0的時候執行緒建立就是呼叫系統的API,這讓自己或者使用者閱讀到相關的原始碼時一頭霧水。時代在進步,標準也在更新,在去年底開始火爆的ChatGTP問世以後,就會發現我們這個時代已經處於技術工業革命快速發展的時期,我覺得有必要跟上時代的步伐,需要嘗試或者去了解一些前沿的新技術,哪怕這項技術不太成熟,但是可以讓自己得到一定的提升。

 

  3、設計模式

    還有由於基於老專案的原因,由於plain基於common庫,因此這個庫之前有許多的針對性。所以設計模式上,還是停留在它的思維之上,要從設計模式的缺陷說起的話,不妨看看下面程式碼的實現:

#include "pf/engine/config.h"                                                   
#include "pf/db/config.h"                                                       
#include "pf/script/config.h"                                                   
#include "pf/net/connection/manager/config.h"                                   
#include "pf/net/protocol/config.h"                                             
#include "pf/cache/manager.h"                                                   
#include "pf/basic/type/variable.h"                                                                            
#include "pf/console/config.h"                                                                                 
                                                                                                               
namespace pf_engine {                                                                                          
                                                                                                               
class PF_API Kernel : public pf_basic::Singleton< Kernel > {                    
                                                                                                               
 public:                                                                                                       
   Kernel();                                                                                                   
   virtual ~Kernel();                                                                                          
                                                                                                               
 public:                                                                                                       
   static Kernel &getsingleton();                                                                              
   static Kernel *getsingleton_pointer();                                                                      
                                                                                                               
 public:                                                                                                       
   virtual bool init();                                                                                        
   virtual void run();                                                                                         
   virtual void stop();                                                                                        
                                                                                                               
 public:                                                                                                       
   pf_net::connection::manager::Basic *get_net() {                               
     return net_.get();                                                                                        
   };                                                                                                          
   pf_db::Interface *get_db();                                                                                 
   pf_cache::Manager *get_cache() {                                                                            
     return cache_.get();                                                       
   };                                                                           
   pf_script::Interface *get_script();                                          
   pf_db::Factory *get_db_factory() { return db_factory_.get(); }               
   pf_script::Factory *get_script_factory() { return script_factory_.get(); }   
   pf_console::Application *get_console() { return console_.get(); }  
...

    以前在使用虛擬函式之後,就會發現虛擬函式有很多函式,因為這是實現C++物件導向之一多型的方式之一,因此在框架之中只要有可能需要的繼承,我就將該類的析構設定為虛擬函式,可是卻完全沒有考慮過這樣的繼承是否應該存在,我以往想的是使用者可以自定義,其實這裡可以使用NVI(None virtual interface)或者std::function的Strategy(策略)以及經典的Pimpl手法,這樣暴露過多的需要繼承的東西其實是增加了使用者的負擔。在設計類時,plain1.1rc中設計模式都是以希望使用者重寫虛擬函式來實現自定義的實現,甚至在類中充斥了大量protected成員變數,這樣的成員變數會造成封裝的破壞還有很可能造成庫升級過程中ABI(Application binary interface)相容問題。

    為了更好的封裝庫,讓我想到的使用C++20開始支援的module裡的export、import實現,至於為何使用C++23 without module,我會在下面專門來講這個問題。這裡我就再嘮叨一下在設計模式的應用中,與此似乎不太相關的一個問題,那就是plain1.1rc中設計了script和db相關封裝的介面,以前為了讓plain1.1rc做到能夠除了依賴系統標準庫以外不再有任何依賴,我將指令碼和資料庫的封裝單獨羅列出去做成了外掛模組,外部需要使用這些介面的時候只需要將外掛以自定義型別的方式進行載入,然後就可以使用plain提供的建立和操作等介面了,這一點上本身沒有什麼問題,不過現在看起來單獨為這兩個留一個目錄有點浪費了,因為現在裡面的實現最多一兩個檔案,所以我打算將它們移到同一個目錄中作為外部的一些介面標準使用。

    

 改良

 plain2.0d的目標

  1、簡單

    簡單是首要目標,就是介面儘量簡單,暴露給使用者的東西儘量少,因此對於封裝產生了比較高的需求,類可能大部分都需要重新設計。正因為大量的類需要重新設計,加上引入了新的標準,所以才有了大版本的提升,至於C++11的版本,後期可能就會逐漸很少維護了。如果大家希望和我一起學習新的標準,那麼就可以切換到開發分支dev進行。(這裡將簡單列為首要目標,可能有人說plain以前的理念是安全、快速、簡單,難道安全就不重要了嗎?答案是否定的,因為2.0就是在1.1基礎上的改進,安全仍舊作為底線,雖然可能提供不了太多強烈安全保證,但是基本的安全保證肯定是需要的)

    plain的英文翻譯叫做平的/簡單的,也是希望該庫能夠提供一些質樸的介面,讓使用者能夠不用產生太多的困惑。其實要將介面設計的簡單並不是一件容易的事情,畢竟很多時候應用面對的場景是比較複雜的,如何能夠將複雜的問題簡單化始終不是一件容易的事情。在這裡我得說說風雲的skynet框架,這個框架從一開始設計的目的就是處理訊息包的傳送和接收問題,在使用者方面只需要關注skynet.callback和skynet.send等少量介面,但是該框架效能是達標的,他使用的Reactor+執行緒池模式實現了高併發,也就是接下來我要提到的快速其中之一(快速不止是效能)。一旦涉及到多執行緒,程式設計就會面臨複雜的環境,不可避免的就會面臨一些race condition(資源競爭)。面對資源競爭,我們就要祭出祖傳大法lock(鎖)。下一節我會簡單提及一些鎖的描述,這裡舉的例子只是說明看起來簡單的東西實現起來不一定那樣簡單。所以在設計2.0的時候,我還是做了一定的考慮,導致有些時候程式碼可能會反覆重寫,幸好在dev可以隨便折騰。

  2、快速

    這裡的快速有兩層含義,一是快速上手,二是高效能。快速上手的依賴就是介面的簡單清晰,還有就是提供一些特需的一些應用場景,比如建立一個伺服器的監聽,只需要引入頭(未來可能直接import)可能只需要三下五除二幾句程式碼就快速實現。

    既然作為網路庫的框架,那麼它要實現高併發,首先我要在這裡申明該網路庫暫時不考慮分散式的那種設計,據我所知分散式有一種訊息實現叫nsq,大家可以在github上搜尋下。高併發並不能增加網路的吞吐量,但是可以充分利用CPU的多核優勢增加反應速度,所以突破硬體的限制這種事在plain中根本不作考慮,還有就是plain使用傳統的socket的介面來實現網路部分(協議為TCP),並沒有使用intel的DPDK這種基於網路卡的技術(據我所知它是所謂的zero copy)。提到TCP/IP,不得不多說一句,我在大學的時候教材裡的一本就是TCP/IP 詳解(卷1),現在不知道扔到哪裡去了,如果看了這本書的話就會對當今的網路構建有個全面的瞭解,畢竟網際網路現在幾乎都是基於這種技術。面試的時候可能會問三次握手(SYN、SYN+ACK、ACK)和四次揮手的問題,甚至會問網路OSI的七層模型(物理層、資料鏈路層、網路層、傳輸層、會話層、表示層、應用層),更有甚者有些人還會問IP(網路層)/TCP(傳輸層)協議分別處於哪一層,還有需要你知道IP協議是基於鏈路、TCP是基於端對端相關的問題。實際上這些問題不復雜,可是你一旦不去看,都會遺忘,要不是最近我在看到相關的書籍,問到這些我只能搖頭。看,在這裡大家是不是也能夠獲得一些面試的答案,如果對於大家有幫助的話就請點個贊吧(開句玩笑,不點贊也沒關係)。

    作為高併發核心之一的網路模組,是plain的核心。其實plain1.1的實現就是基於Reactor模式的,網路框架其實不應該對使用者的消費場景進行過多的考慮和干涉,就是隻需要做好自己應該做的事,這樣的話網路模組的設計複雜度會降低不少。最近看了陳碩的muduo開源庫後,才瞭解到了一種模式叫做one loop per thread,這種模式考慮了併發效能和執行緒安全,我覺得可以利用該模式來改造網路模組,而且那個非同步日誌(AsyncLogger)的實現就幾乎參考了該開源庫的實現,我想後面可以做一些最佳化,得力於新的標準應該有更多提升效能的方法。

    併發程式設計是C++和其他語言中都會面臨到的難點,避免不了的就是使用鎖,當然也有一種lock free的實現,不過在開發中鎖使用還是相對多一些,提到鎖的話大概有mutex(互斥鎖)、rwlock(讀/寫鎖 共享/獨佔鎖)、spinlock(自旋鎖),在muduo這個網路開源框架中陳碩的主張是使用mutex就已經足夠,而且其效能並不差,我也贊同這個觀點,畢竟std::mutex是作為C++標準的一部分而其他的鎖則沒有。陳碩在muduo處理鎖的時候,用到了copy on write(寫的時候複製)縮小臨界區,這對提高併發和安全做出了強烈保證,我認為這個思想其實是源自《Effective C++》Item 29: 爭取異常安全(exception-safe)的程式碼的思想,Scott Meyers在這裡提供了一種強烈異常安全的保證實現,那就是copy and swap(複製和交換),有興趣的可以去看看具體的章節。

    在實現快速的時候,就是儘量隱藏複雜的細節,也就是不要將複雜蔓延到使用者身上,畢竟使用者只需要關心邏輯計算的處理即可,這也是雲風在設計skynet時所說的那樣,使用者只需要處理包到來的處理,然後看情況返回訊息。

    其次是底層需要在實現上加快程式執行的效率,除了多執行緒之外,就是減少執行時的開銷,需要做到執行開銷降低我覺得有兩點比較重要:complie time(某些確定計算儘量放在編譯期)、algorithm(演算法)。

    complie time:編譯期,如果計算放在編譯器,那麼執行期就不必要為了計算產生額外的開銷。這個做法在TMP(Template metaprogramming 模板超程式設計)以及Generic programming(泛型程式設計)中運用的比較多,舉個面試中的例子,如何求解1到N所有數字相加之和,不能使用乘除法、for、while、if、else、switch、case以及三元運算子?:,看這裡有如此之多的限制,普通的遞迴能夠解決這個問題嗎?答案是普通遞迴需要用到上面的條件語句以及運算子,無法使用。那麼這道題其實可以從幾個方面來求解,如利用建構函式和類的靜態變數、利用虛擬函式、利用函式指標,有些人如果看過劍指offer,就知道這些我就是從這本書上看到的。其中還有個比較常見的解法,那就是利用模板,我們來看看這道解法的程式碼(因為短我才放到這裡的):

template <unsigned int N>                                                           
class CalcNum {                                                                    
                                                                                    
 public:                                                                            
   enum {n = CalcNum<N - 1>::n + N };                                                  
                                                                                    
};                                                                                  
                                                                                    
template <>                                                                         
class CalcNum<1> {                                                                 
  public:                                                                           
    enum {n = 1};                                                                   
};
  int main() {                                                                    
    std::cout << "n: " << CalcNum<100>().n << std::endl;                          
  }

    你沒有看錯,n的值並不會在執行期被計算,而是在編譯器具現化模板的時候遞迴被計算出來的,也就是該計算發生在編譯期,如果N的數字越大所耗費的時間會增多,這時候體現在編譯的時間變長了,但是執行期時沒有計算的消耗。得力於C++11過後增加的關鍵字constexpr,特別是C++14強化過後,編譯期能夠計算的東西就越來越多,而且標準庫裡越來越多的介面都是constexpr,可見制定標準的人和業界都認為編譯期程式設計是值得考慮的。

     algorithm:數學是人類偉大的智慧之一,由數學產生的演算法更是能夠解決生活中許多的問題,常見的有機率學、金融學等等,利用演算法可以解決很多難題,而且往往花費很少的時間。當下AI(人工智慧)再次被推向風口浪尖的時候,演算法相應的崗位變得炙手可熱,剛剛走出校門的能夠獲得的報酬也比十年多的程式還要多,這怎能不令人羨慕不已。但是演算法是有門檻的,特別是一些高階演算法,你需要非常紮實的數學功底,很抱歉我在數學方面就屬於有些欠缺的,雖然說我家裡有一本C/C++函式演算法速查手冊,但是仍舊對於演算法一知半解,最近接觸演算法也是劍指offer中看到的這些,我覺得那些很有代表性,不只是為了面試,更加有助於自己對於自身技能的一種提高和增強,我覺得有興趣的朋友不妨瞭解一下。

    演算法能夠加快執行期的速度的原因,我舉一兩個例子大家就應該會明白,例如一個經典面試題,這個也是13年的時候我在進行網易電話面試時未能正確回答出來的問題,其原因就是我根本沒有看這方面的知識,既然沒有看自然就只能一問三不知了。這個問題就是如何O(1)時間刪除單向連結串列中的一個節點,如果沒有接觸相關的知識,除非數學功底好以及對連結串列資料結構十分熟悉,否則這個問題也很難回答上來。其實該題目並不複雜,比起用那些公式才能解決的問題簡單太多了,首先要的是你需要熟悉連結串列的資料結構。單向連結串列就是每個節點都有指向下一個節點的指標,連結串列末尾的節點下一個節點的指標為空。有了對連結串列這種資料結構的瞭解,加上一點發散思維,刪除節點就是將這個節點從連結串列中移除,傳統能想到的是遍歷連結串列,直到該節點將它上一個指標的下一個指標指向需要刪除指標的下一個指標,那麼這將花費O(n)的時間,時間效率上是達不到題目需求的。那麼利用發散思維,我們不妨使用置換的技巧,交換自己和下一個節點的資料,然後讓待刪除的節點指向自己下一個節點的下一個節點,那麼該操作的時間效率就是O(1),當然為了程式碼的robust(魯棒性),你需要考慮該節點位於頭尾節點,程式碼如下:

  void del_list_node(list_node_t** head, list_node_t* node) {                    
    if (!head || !node) return;                                                   
    if (node->next) {                                                             
      auto next = node->next;                                                     
      node->value = node->value;                                                  
      node->next = next->next;                                                    
      delete next;                                                                
      next = nullptr;                                                             
    } else if (*head == node) {                                                   
      *head = nullptr;                                                            
      delete node;                                                                
      node = nullptr;                                                             
    } else {                                                                      
      auto temp = *head;                                                          
      while (temp && temp->next) {                                                
        temp = temp->next;                                                        
      }                                                                           
      if (!temp || temp != node) return;                                          
      temp->next = nullptr;                                                       
      delete node;                                                                
      node = nullptr;                                                             
    }                                                                             
  }                                                                               

    關於這個演算法的應用,我在閱讀muduo的原始碼裡見到了將list中待刪除節點和尾節點交換後刪除尾結點的辦法,該技巧就減少了一次遍歷的消耗,提高了程式執行的效率。

    關於演算法的相應程式碼,大家可以在這個倉庫找到,該庫裡也有C++一些新特性的示例(需等待整理後我才會上傳):https://github.com/viticm/cpp_misc

  3、設計

    這一部分其實都是為了簡單和快速而做的,就是選擇一兩種設計模式,我準備在plain2.0中使用impl手法以及std::function實現Stratege模式,先看看下面的程式碼:

#include "plain/basic/config.h"
#include <string_view>

namespace plain {

class PLAIN_API AsyncLogger : noncopyable {

 public:
  AsyncLogger(const std::string &name,
              std::size_t roll_size,
              int32_t flush_interval = 3);
  ~AsyncLogger();

 public:
  void append(const std::string_view& log);
  void start();
  void stop();

 private:
  struct Impl;
  std::unique_ptr<Impl> impl_;

};

} // namespace plain

    本來是想用logger.h作為示例,不過那個內容較多,害怕大家看了產生排斥,所以這裡選擇了簡單的宣告,這裡看起來是不是十分清新爽朗,我個人認為是的,這個介面一目瞭然,多虧了Impl手法隱藏了實現細節。而對於Strategy的實現,其實plain1.1後面的網路相關都已經在做了,不過做的不夠徹底,還保留了一些多型封裝的十字軍問題。後面具體的實現,我會在後面網路設計方面詳細進行說明,如果有興趣的朋友不妨關注後面的文章。

  4、相容

    這裡的相容並不只是升級後ABI的相容,還有就是跨平臺。

    plain是一個跨平臺的網路庫,也是有歷史原因的,因為這套原來的實現的客戶端是在windows上執行的,而在微軟的PC上執行的遊戲通常稱為端遊,所以在plain1.1rc中跨平臺支援linux和windows。而在plain2.0的時候,我打算加入一個新的平臺,那就是MAC,以前自己沒有MAC的時候也不想去折騰這個平臺,但現在有了之後就乾脆將它也考慮在內。因此在跨平臺的宏定義檔案include/plain/basic/macros/platform.h定義了三個宏:OS_UNIX(類LINUX平臺)OS_WIN(微軟PC)OS_MAC(蘋果PC)

    為了實現更好的跨平臺,我解決的辦法就是儘量使用標準庫,就像陳碩說的將髒活、累活留給別人,而這裡的別人正是制定標準的組委會(編譯器的作者表示不服)。除非要實現一些標準庫裡沒有的功能,比如epoll這種具有平臺屬性的介面時才會幹一些平臺性相關的處理,說實話這確實是一件累活。其次一點就是儘量減少依賴,比如我就沒有使用標準指定的委員會那群人(聰明人)弄出來的boost庫,boost庫有很多高效能的東西,還有一些即將進入標準庫的東西,的確是有學習的必要,Scott Meyers在《Effective C++》Item 55: 讓自己熟悉Boost強烈推薦熟悉該庫。我在想如果使用了該庫,我何不直接使用asio而需要大張旗鼓地自己實現網路庫?而且多了一個依賴,使用者必然就會需要安裝它,說實話我不太喜歡boost那種龐大的容量,除非有一種包管理工具,實現golang那種我在程式碼中申明依賴了後,初始化可以幫助我自動安裝只需要的模組,這樣的話我覺得使用它是挺好的。

    為了實現ABI的相容,我將要把大部分介面去掉虛擬函式的設計,改為使用Pimpl(Pointer implementation)的設計方式,對於使用者不可見的部分,將盡量隱藏。還有對於使用者來說,不需要重寫類,而是隻需要設定需要關心的處理方法,這個正是使用std::function來實現的Strategy模式。

 

  5、測試和示例

    在plain1.0的時候單元測試時沒有的,當版本到了1.1的時候才有,沒有單元測試當時我就只能依賴與一個簡單的示例來找出程式碼中的漏洞,每次除錯說實話有點累人。因此在後面寫的模組,比如因為見到在PHP一個框架laravel的ORM(Object relational mapping)模組後,我見到了其中有大量的單元測試,於是我仿照其實現寫了C++相應的模組放到了plain中,在db目錄下https://github.com/viticm/plain/tree/master/framework/core/src/db,在2.0中這個模組將被簡化,ORM的實現我會單獨做成一個工程或者放入外掛來使用。說到這裡有點稍微跑題,我想說的是單元測試在這裡才開始使用,可以看到裡面的測試都是不全的,plain1.1rc的單元測試:https://github.com/viticm/plain/tree/master/framework/unit_tests/core_test。由於2.0是真正的大改造,因此就可以實現健全的單元測試用例,這個可以用來稍微檢測一下程式的健壯性,但是我這裡並沒有做那種可以複用的自動迴歸的測試,我覺得先實現一種傳統的測試即可,後續有需要加上有時間可以再增添,那部分不過只是添磚加瓦而已。

    至於示例,由於懶的緣故,plain-simple裡只有一個簡單的應用,在2.0中我準備實現以下幾種或者更多的伺服器的示例:echo、discard、chat等。這樣可以測試一下plain作為伺服器使用時併發的能力,類似libevent中的效能測試。當然跑出來的資料可以稍微做一個對比,這個可以作為一個讓自己覺得有成就感的事情來做(手動狗頭,炫耀欺騙一下自己)。

 標準

 C++23(2023)

  1、偽命題

    c++23真的釋出了嗎?

    確實C++23似乎還沒有正式釋出,但是組委會透過的那些新特性已經凍結,其實可以認為C++23已經算是釋出了,新增的標頭檔案和特性已經定型。但是這裡存在一個問題,那就是主流的編譯器GCC(目前最新版本12.1),以及VS(Microsoft Visual Studio),其實對於C++23很多特性都還沒有支援,也就是編譯器的作者還正在填坑,不過我覺得C++23其實不算那麼著急,先把C++20的std::format功能實現了再說,不然許多人都要面對那充滿議論和詬病的C的printf了。也就是說現在C++標準支援的比較充足的是在C++17,應該是C++17所有的功能都是可以使用的,不過C++20的許多功能也能夠使用。

    從上面不難看出,我這裡說plain使用C++23是一個偽命題,因為C++23特性沒被支援,怎麼能夠寫出基於C++23的庫?我將標準定在這裡的原因,就是提醒自己plain使用的是基於最新的C++標準,這確實是一種嘗試。我覺得嘗試是有必要的,一個人沒有必要固步自封,嘗試一些新事物是有用的。在這方面,我認為只有主動擁抱未來的人,才會被未來所擁抱。因此如果想要入C++坑的學生和其他朋友,不妨就以最新的標準開始學習,因為十年之後你會發現這些技術就會真的被普及起來了。

  2、modules

    C++20引入了module,這個已經被其他語言如python、javascript早已引入的功能,這時候來的確實有點遲了。說實話這也是沒有辦法,因此C++語言的設計者一開始就是以C with object oriented(C和麵向物件)來設計的,也就是C++是C的衍生,而C呢標頭檔案和實現本來就是分開的,如果早點引入這個的話,你就會發現C++和C區別實在太大了,估計這會造成C的忠實擁躉有些不適應(C畢竟還是老大哥,許多的高階語言其實現幾乎都是使用C語言)。
    可惜的是C++20裡編譯器並沒有對標準庫提供可以使用,如語句import std;就無法使用,其實C++23標準裡就是需要讓標準庫得以支援,那麼這個就還是需要編譯器的諸位努力,說實話你們是真的辛苦,為了大家的福祉你們就加把勁吧。還有就是module的使用有個痛點,就是模組之間的相互引用的編譯順序問題,畢竟需要編譯出*.gcm才能夠使用import。考慮到了複雜度,plain暫時不考慮使用module。
    由於module特性的吸引力,個人認為以後module還是應當支援,特別對於庫開發者來說這的確是一大利器。

雜項

 寫在最後

  1、園子

    不知不覺我在部落格園已經有了十多年,說句話十年之間世界發生了很大的變化,特別是隔壁的部落格有許多的限制,如你檢視的時候需要登入、複製也需要登入,雖然我理解那種做法,是為了吸引更多的使用者,但是並不贊同那樣的做法,說實話那樣讓人有點不舒服。但是園子給我感覺十分清爽,雖然我明顯感覺園子確實變得有點冷清,閱讀量有點變少了。但我覺得作為一個純技術分享的地方,這樣的園子是值得讓人稱讚的,不忘初心終得始終,希望園子能夠堅持下去並且越做越好。在園子裡我也在一些文章裡獲取了不少知識,因此我十分樂意來分享自己的知識,希望這些知識能夠幫助大家。

    在許多年前閉源還十分盛行的時候,人們想不到今天開源會如此的大行其道,人類想要發展的話我認為首先就需要有這種分享的精神,要是不懂得分享的話,就等於閉關鎖國,除非你的技術是需要保密的話可以另說。將自己的技術和想法分享出來也能給自己帶來快樂,我個人是如此感覺的。

  2、推薦書籍

    《C++程式設計思想》:可以帶你全面瞭解C++的語言設計的方方面面

    《C++ Primer Plus 第五版》:入門可以選擇這本

    《Effective C++ 第三版》:高效的C++程式設計,看看高手如何使用一些技巧(一共55條)來寫作的

    《Effective Modern C++》:同上面那本的作者是Scott Meyers,不過是對於C++11/14上做了一些介紹,還有一些特性的總結,同樣的也是有條款性的建議(42條)

    《C++20 高階程式設計 第五版》:去年底出了中文版,現在我看的是英文版的,沒辦法為了熟悉C++20我只能依靠我憋足的英文來閱讀,看久了就發現似乎英語也不成問題(有種文言文看久了成了古人的錯覺),這本書對C++20的新增特性做了明顯的標註

    《UNIX 環境高階程式設計》:這本書如果開發POSIX的網路,你需要了解一下,這個似乎被稱為UNIX程式設計的”聖經“,對作業系統有詳細地介紹,目前我還在看第一章(等於沒看)

    《TPC/IP詳解 卷一》:許多人都推薦這個,如果你想了解網路程式設計的方方面面,那麼這本書應該讀一讀

    《演算法導論》:如果希望對演算法有個深入一點的瞭解,這本書適合不過,我也正準備看這本書

    《劍指offer》:本來不打算在這裡推薦的,但是如果你要去國內大廠,我覺得它有幫助(不面試的話也可以看看其中的思想,可以對程式設計有所幫助)

  3、網站

    github:https://github.com/ 應該算最大的開源社群了吧

    cppreference:https://en.cppreference.com/ 如果想要得知C++23新增的特性,這個網站比較全,你也可以在裡面檢視標準庫的介面和相應的示例

    coding-interview-university:https://github.com/jwasham/coding-interview-university 如果你想進入谷歌、微軟等等頂流大廠,這個上面有一個外國朋友分享的心得,怎樣進行系統的學習和麵試的方方面面,本人是不行了,就靠各位後浪的朋友(裡面有中文的文件,在目錄translations下,不過你估計要學會科  學上網,其中影片是放在油管上的 個人特別喜歡裡面的一句話:不要覺得自己不夠聰明 這句話格局一下就開啟了

    QQ:348477824(plain交流群),這個群目前就如那個頭像一樣十分死寂,但是不妨礙新的朋友加入過來進行一些技術探討,真誠歡迎各位朋友的加入,當然有人能提供一個好的plain圖示就再好不過了

 

相關文章