网络框架重构之路plain2.0(c++23 without module) 综述

最近互联网行业一片哀叹,这是受到三年影响的后遗症,许多的公司也未能挺过寒冬,一些外资也开始撤出市场,因此许多的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详解 卷一》:许多人都推荐这个,如果你想了解网络编程的方方面面,那么这本书应该读一读

    《算法导论》:如果希望对算法有个深入一点的了解,这本书适合不过,我也正准备看这本书

  3、网站

    github:https://github.com/ 应该算最大的开源社区了吧

    cppreference:https://en.cppreference.com/ 如果想要得知C++23新增的特性,这个网站比较全,你也可以在里面查看标准库的接口和相应的示例

    coding-interview-university:https://github.com/jwasham/coding-interview-university 如果你想进入谷歌、微软等等顶流大厂,这个上面有一个外国朋友分享的心得,怎样进行系统的学习和面试的方方面面,本人是不行了,就靠各位后浪的朋友

    QQ:348477824(plain交流群),这个群目前就如那个头像一样十分死寂,但是不妨碍新的朋友加入过来进行一些技术探讨,真诚欢迎各位朋友的加入,当然有人能提供一个好的plain图标就再好不过了