【置顶】个人阶段性学习和规划总结(技能树)

  本人专注后台服务端开发一晃已经好久了,自我感觉上不仅经验增加了很多,同时接触到的东西确实不少,在增加见识的同时博文也批量更新了很多。大多人看来内容很多很杂,此处做一阶段性整理和总结吧,给自己也梳理梳理一下。
  其实不仅仅是后台服务端开发这个方向,就整个软件开发来说,其知识构成也是有所层次的。个人毕竟不是正规计算机科班出身,也就是大家所说的半路出道、自学成才的野程序员,优点就是不会被那些所谓正规计算机教育的所束缚禁锢住,但是很多时候感觉自己的知识构成还是有所缺陷。既然励志要靠撸代码吃饭养家,那么长痛不如短痛,晚补不如早补,该学的终究跑不掉!

【置顶】博客资源收录大全

  虽然当下微信公众号席卷自媒体市场之势如火如荼,但是针对文章展示的话个人还是偏向于独立博客的形式,主要是因为个人可控制化的东西比较多。我想,在被条条框框束缚的无以喘息的情况下,这个时代没有什么比个性和自由更为重要的了吧。
  下面是一些网络知名人士的博客,以及平时在搜索资料过程中遇到的好站点,都被一一记录下来了,真心喜欢的话可以用RSS订阅这些站点,其中很多都是全文输出的。因为个人的兴趣取向问题,除了将不感兴趣的前端、移动端外,过于偏向于Java/Nodejs等语言化的博客也被KO了(即使编程思想是独立于语言而存在的),希望平时没事多看看吧!

C++设计中的Handle处理类

  Handle这个内容是在看Andrew和Barbara夫妇的《C++沉思录》中接触到的,被广为翻译为句柄,用他来控制其所管理的类。刚开始看瞄的时候就觉得:握草,这不就是智能指针的原型么,难道是没有智能指针时代的轮子?但是耐着性子看完后,觉得还是收获不少的,起码的话算是对智能指针中引用计数、写时复制的设计实现写的比较清楚了。
  题外话,其实智能指正在我司也早有轮子,并且一直被使用至今了。今天过细看了下代码,发现都是最简单的Scoped非拷贝使用方式,而在需要外传的时候都是使用引用的方式传递出去,所以算是挂羊头卖狗肉吧,Shared类其实根本就没做到Shared的事儿,就是个简单的RAII做的事儿。不过,因为我们的业务逻辑比较简单,所以长久使用起来也没有什么问题……

一、简单实现

1.1 准备工作

  作者过于循循善诱的细节东西就不细说了,Point这个类是我们实际的用户类,跟业务相关的我们不管;Handle类是我们要实现的句柄类,我们的目的是要将Point的对象绑定到Handle对象上,让handle控制他所绑定的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
public:
Point(int x, int y): x_(x), y_(y) {}
private:
int x_; int y_;
};
class Handle {
public:
Handle(int, int);
Handle(const Point& p);
private:
UPoint* up_; // TODO...
};

  为了用户使用的舒适傻瓜,那么Handle就应该自动接手用户类Point资源的创建和销毁,通过Handle的构造函数我们得知申请资源的时候就有两种方式:(1)直接让Handle类的构造函数参数和Point类构造函数签名一致,然后做一个参数转发;(2)用拷贝的方式,创建一个现有Point的对象的副本,而原始对象的资源我们不关心。

Linux下的日志服务Rsyslog

  当前公司的业务系统是自己搞了个日志服务,其实也就是对格式化后的消息使用UDP发送做了一个封装,然后日志服务端根据执行进程名字创建对应的日志文件后,对相同服务的日志内容的聚集和落盘操作,并且日志文件每日进行截断保存。由于一些异常交易的跟踪排查都通过日志文件进行跟踪,所以平时对日志文件的依赖还是比较大的,但使用过程中发现这个自研的日志系统问题多多:
  (1). 日志文件的内容经常会乱序,虽然根据时间戳可以判断日志的先后顺序,但人为跳来跳去总有些别扭。在多线程环境下每发一条日志都会创建一个UDP socket,所以预想在今后随着业务增长和线程数目增加,日志量攀升的情况下这种乱序现象会更为的严重;
  (2). 有时候本该有的日志没有发现,不知道是执行流程改了还是怀疑是日志本身丢失了,毕竟是UDP协议发送的,可靠性是得不到保障的;
  (3). 采用自定义的日志格式,虽然zabbix也能进行一些正则匹配,但是我想后续接入专业的日志分析处理系统可能会比较困难,这里把所有的日志元数据都格式化死到消息体里面的;
  (4). 日志消息没法按照level灵活的分文件存放,查阅日志需要在一大堆的调试信息中寻找,费神费力,所以效率真心堪忧。

  一句话三个字儿:坑爹啊!所以在此强烈建议规模不大研发能力又有限的公司,还是应该选择成熟、大众、主流的后台开发组件:一方面这些开源组件对你遇到的和将要遇到的坑,人家都遇到帮你填了;二来他们通常构架灵活,扩充和修改方便,而且围绕他们做的配套工具和组件周边也比较丰富,很多事情都不必自己再做了。虽然,对程序员来说造一个轮子很简单,通过造轮子对比也是一个很好的学习进步方式,更厉害的是轮子对公来说也是KPI亮点,但是没有严格论证测试就将其推向生产环境是一个极度不负责任的行为,情况严重的话会给后续的开发维护带来不小的负担。

又双叒一个HTTP服务端轮子

  现在看来,自己已经撸了好几个HTTP服务端的轮子了,就像是做前端的都爱做博客主题一样,估计做HTTP服务端也是很多服务端开发最爱干的事情了。事由是公司测试环境需要一个虚拟银行端来匹配打款系统做测试使用,而且后面可能也会用来匹配做压力测试和系统调优工作,虽说主要达到需求为目的,这东西可做简单可做复杂,但是本着进益求精(其实是不折腾不死心)的态度,自己还是想把这个功能能够模拟的真实一点。刚好自己之前做了很多的小组件,然后发现很多可以整理一下拿来直接使用,由此不禁感叹:工作年限多了,虽说感到技术和能力没啥长进,手头倒是积累了一大批的轮子、工具和脚手架,也能算得上是一笔小积累和小财富吧……
  总体来说,东西还是向着更好的、更成熟的方向发展的。
  经过前几次HTTP服务端的尝试,现在总算感觉把boost.asio和HTTP/1.x的GET/POST较好的融合起来了。通过GET方式返回文件系统内容基本很容易就能做一个静态Web服务器,而且之前用过FastCGI接口让其支持php动态语言也不是难事,但是总体开发HTTP服务端的应该都是作为接口开发的,将特定uri路由到特定接口的处理函数中去,所以开发中提供向指定uri注册回调函数的机制,将解析后的HTTP头部、uri和参数、POST数据体透传给这些函数,并约定处理结果的返回内容和格式,就算是形成了一个通用的HTTP服务器框架了,后面各种服务都可以注册进来,将请求路由到各个响应函数中,这个库把HTTP服务需要考虑的大多问题都帮忙解决了。后续的工作可以对更多的头属性做支持,HTTP/2暂时就不考虑了,不得不说这玩意儿实在是太复杂了……

Boost.Chrono时间库的使用

  时钟这个东西在程序中扮演者重要的角色,在系统编程的时候睡眠、带超时的等待、带超时的条件变量、带超时的锁都会用到,但是往往对特定系统依赖性很大,感觉即使不考虑系统的跨平台性,如果能使用一个稳定的接口,同时如果能够方便的对时刻、时段等进行相关的操作和运算,将是再好不过的了。
  在boost库中和时间相关的库有Boost.DateTime和Boost.Chrono,前者专注于时间时刻以及本地化相关的内容,而后者主要是时刻、时长和时间的计算等内容。当然,C++11标准已经支持std::chrono了,但是为了兼容老编译系统现在很多C++库和程序都使用boost.chrono作为时间类库(还有的原因就是std::chrono没有收录boost.chrono的所有功能,比如统计CPU使用时间、自定义时间输出格式等),不过比较可惜的是即便使用boost::chrono作为权宜之计,也需要boost-1.47版本之上才行,而现在比较旧的发行版需要升级boost库才可以使用。想想现在RHEL-6.x仍然被大规模的部署,而且RedHat要为这货提供长达十年的技术支持,真不知道啥时候才能顺顺利利的享受C++11……
  Boost.Chrono的时间类型分为duration和time_point,也就是时长和时刻两类,很多概念和接口都是围绕这两个维度去定义和实现的。

一、Clock

  clock是Boost.Chrono中的重要概念,而且这些clock都包含一个now()的成员函数,用于返回当前的time_point。Boost.Chrono包含的clock类型有:
  (1) chrono::system_clock 代表系统时间,比如电脑上显示的当前时间,其特点是这个时间可以被用户手动设置更新,所以这个时钟是可以和外部时钟源同步的。这个时钟还有一个to_time_t()成员函数,用于返回自1970.1.1开始到某个时间点所经过的秒数,数据类型是std::time_t。这种时钟通常用来转换成日历时间使用。
  (2) chrono::steady_clock 其特点是时间是单调增长的,后一个时刻访问得到的时间点肯定比之前时刻得到的时间点要晚,即使我们手动将系统时间向前调整了也不会改变这个时钟稳步向前推行累计,其也被称为monotonic time,该时钟是均匀增长且不能被调整,其特性对于很多不允许时间错乱的系统是十分重要的。chrono::steady_clock通常是基于系统启动时间来计时的,而且常常用来进行耗时、等待等工作使用。
  (3) chrono::high_resolution_clock 依赖于系统实现,通常是上面两种时钟的某个宏定义,取决于哪个时钟源更为的精确,所以其输出也决定于取决于上面哪个clock来实现的。
  (4) chrono::process_real_cpu_clock 表示自进程启动以来使用的CPU时间,而这个数据也可以通过使用std::clock()来获得。chrono::process_user_cpu_clockboost::chrono::process_system_cpu_clock表示自进程启动以来,在用户态、内核态所花费的时间,而所有的这些事件可以通过chrono::process_cpu_clock来获得,他返回上面所有时间组成的一个tuple结构。
  (5) chrono::thread_clock 返回基于线程统计的花费时间,而且不区分用户态、内核态的时间。

C++之虚函数的访问性

  在上次的一篇文章中,提到了private virtual函数,说实话直到当前自己所写的所有的虚函数都是public的,毕竟成员数据总应当被设置为private已经深入人心了,但是对成员函数的访问性貌似强调的不够多。后面网上搜了一下,虚函数的访问性还是挺有讲究的,顿时Sutter的两篇历史博文让自己醍醐灌顶,可见经典永流传啊。
  总体而言,涉及到虚函数应该秉持Non-Virtual Interface Idiom,相似的说法是Template Method涉及模式。

一、接口类型是non-virtual public,实现类型是virtual private

  如果一个成员还是是virtual public,那么这个函数就需要完成两个任务:定义调用接口、提供实现细节,而很多请看下这两个目标是对立竞争关系,因为接口要尽可能保持稳定,而实现要尽可能的方便修改更新。
  模板方法就是将接口定义为稳定的non-virtual,然后将实现和定制化的工作代理给private virtual成员函数,这样继承类就可以直接继承public函数作为稳定接口,同时override基类的private virtual进行定制化的实现。这样去做的话其好处有:
  (1) 在基类的公有接口中可以做很多pre-conditions和post-conditions的工作、插入度量性代码、写入调试跟踪日志等,跟一般的说是在调用之前设定好相关场景,而在调用之后清理相关场景,而不需要在每个派生类override的时候重复这一任务。
  (2) 接口和实现分类后,两者就不用像原本public virtual要实现一一对应的关系,比如在一个公共接口中可以按照一定的顺序、一定的条件可选择性的调用多个private virtual实现函数,派生类选择性的override某些或者全部虚函数,处理起来就更加灵活了。
  (3) 这样实现后的类后续修改和维护更加的方便,可以快捷的在public non-virtual接口中添加检查、调试等任何操作,派生类也可以按需独立的override业务部分,接口的使用者不受任何影响。
  (4) 关键的是这种手法几乎没有副作用,即使公有接口类没有额外的工作而仅仅当做一个函数wrapper,也可以使用inline进行可能的调用开销的优化。

C++面向对象设计的访问性问题

  最近在看Scott Meyers大神的《Effective C++》和《More Effective C++》,虽然这两本书都是古董级的教参了(当然针对C++11/C++14作者所更新的《Modern Effective C++》英文已经发售了,不过还没中文翻译版本),但是现在看来仍然收益匪浅,而且随着对这个复杂语言了解的深入和实践项目经验的增加,很多东西和作者产生了一种共鸣,以前种种疑惑突然有种拨云雾而见天日、豁然开朗的感觉,也难怪被列为合格C++程序员之必读书目。其实C++确实是个可怕的语言,于是市面上针对这个语言的教参也是聆郎满目层出不穷,当然水平也是参差不齐,像上面所说的Meyers三部曲能够历久弥新,也凸显了这些经典教参的真正价值。
  至于最近回归C++本质,主要是觉得现在后台开发的RPC、MQ、分布式系统虽然被称的神乎其神的,但是作为成熟的组件绝大多数公司都可以是直接拿来主义,当然也不可否认其使用经验的可贵,因为最近线上使用这些组件还是遇到或多或少不少问题的,以后可以少走些坑,然而这种东西也是可遇难求的;反而C++语言本身的使用占用了程序员绝大多数的工作内容,从而直接影响到项目的质量和后续的可维护性。在此,侯捷老师的 勿在浮沙筑高台 仍如警世名言响彻在耳,一个合格的程序员其扎实的基本功是多么重要。
  C++面向对象的东西太多了:public、protected、private访问和继承,virtual和多态、多继承,外加const、缺省参数、名字查找等,光这些元素的排列组合就可以导出很多种情况,看似灵活多变,但不是每种情况都值得去尝试的。

RabbitMQ的Mirror Queue集群高可用性

  RabbitMQ算是历史悠久的消息队列了,所以也算得上是在工业界久经考验的长者了。这些东西平常让他跑着一般不会出问题,但是生产环境真是不怕一万就怕万一,为了高可用高可靠而言最好还是使用其mirror特性建立集群做备份。其实RabbitMQ用起来在我们公司运维也没有什么经验,虽然数据库我们玩的很溜了,但是RabbitMQ遇到问题还是一脸懵逼,主要这东西是非主流Erlang所做,而且也涉及到什么Node的概念,上一次血泪的教训就是没事千万不要改节点的名字。这段时间闲着把RabbitMQ官方的文档看了一遍,对其mirror集群的知识翻译下来。
  我们说RabbitMQ的服务端通常都沿用broker术语,其表示一个或者多个Erlang Node,每个都运行着RabbitMQ的程序,并且他们之间共享users、virtual hosts、queues、exchanges、bindings和运行时参数,不过默认情况下queue是不会再每个节点上都存在的(这也是后面要说的mirror queue之所在),通常也把他们打包叫做cluster。
  当多态RabbitMQ运行的时候,他们之间默认没有联系,都运行在以自己为中心的小集群当中,创建一个大集群的时候可以选取其中的一个小集群,然后让其他的主机加入到这个集群当中。

1
2
3
rabbit2$ rabbitmqctl stop_app
rabbit2$ rabbitmqctl join_cluster rabbit@rabbit1
rabbit2$ rabbitmqctl start_app

  在集群运行的过程中,node可以随时加入和撤离开集群,不过当整个集群都停止之后,则必须由最后那个关闭的node开始重启集群,而此时如果先启其他的节点,则该节点将会等待30s那个最后关闭的节点启动,否则的话将会启动失败。这样设计是考虑到最后关闭的node可能保留最多的消息,如果想要强制启动非最后关闭的node,则需要添加forget_cluster_node参数才可以,此时新启动的node将直接被提升为master。

C++的pimpl用法

  C++的pImpl可以说是最常见的惯用手法了,在很多的C++项目和C++开发库中都有所见。plmp的缩写就是Pointer to Implementor,顾名思义就是将真正的实现细节的Implementor从类定义的头文件中分离出去,公有类通过一个私有指针指向隐藏的实现类,是促进接口和实现分离的重要机制。
  在C++语言中,要定义某个类型的变量或者使用类型的某个成员,就必须知道这个类的完整定义,其例外情况是:如果定义这个类型的指针,或者该类型是函数的参数或者返回类型(即使是传值类型的),那么就可以通过前置声明引入这个类型的名字,而不需要提供暴露其完整的类型定义,从而类型的完整定义可以被隐藏在其他hpp头文件或者cpp实现文件中,而这个指针也被称为不透明指针(opaque pointer)。通常的pImp的手法是在API的头文件中提供接口类的定义以及实现类的前置声明,实现类的本身定义和成员函数的实现都隐藏在cpp文件中去,同时为了避免实现类的符号污染外部名字空间,实现类大多作为接口类的内部嵌套类的形式。

一、pImpl手法的优势和目的

1.1 信息隐蔽

  私有成员完全可以隐藏在共有接口之外,尤其对于闭源API的设计尤其的适合。同时,很多代码会应用平台依赖相关的宏控制,这些琐碎的东西也完全可以隐藏在实现类当中,给用户一个间接明了的使用接口再好不过了。

1.2 加速编译

  这通常是用pImpl手法的最重要的收益,称之为编译防火墙(compilation firewall),主要是阻断了类的实现和类的实现两者的编译依赖性。这样,类用户不需要额外include不必要的头文件,同时实现类的成员可以随意变更,而公有类的使用者不需要重新编译。
compilation firewall

1.3 更好的二进制兼容性

  承接上面说的,通常对一个类的修改,会影响到类的大小、对象的表示和布局等信息,那么任何该类的用户都需要重新编译才行。而且即使更新的是外部不可访问的private部分,虽然从访问性来说此时只有类成员和友元能否访问类的私有部分,但是由于C++的特性是名字查找先于名字查找和重载解析的(即使不可访问也会返回调用失败,而不是视而不见),私有部分的修改也会影响到类使用者的行为,这也迫使类的使用者需要重新编译。而对于使用pImpl手法,如果实现变更被限制在实现类中,那公有类只持有一个实现类的指针,所以实现做出重大变更的情况下,pImpl也能够保证良好的二进制兼容性。
  因此,独立和自由是pImpl的精髓所在。

1.4 惰性分配

  实现类可以做到按需分配或者实际使用时候再分配,从而节省资源提高响应。如果你意识到这点了,那是很不错的。