C/C++ 基于Linux的高并发后台服务器-经验小结

1、多线程多进程模式

进程是操作系统资源分配最小的调度单位,意味着进程被暂停后,其下所有线程都会失去调度资源的权限。要充分利用系统资源,最好的形式是多线程多进程模式。
我们最好将一个整体功能,分散到多个进程当中,从而实现资源利用率最大化。否则就只能多个线程在一个进程内进行竞争,没办法充分利用系统资源。

2、进程创建

在linux中,开启进程一般通过exec系列函数或者fork函数来完成。即使是exec函数,也会要使用到fork函数。
所以开启进程,fork函数是无法绕开的。而fork函数会对线程造成影响,所以我们一定要先定好进程结构,然后再开启线程。原因:首先,由于线程无法被复制,所以在子进程中,一些线程会消失(没有被复制过来;其次,如果程序逻辑依赖多线程模式的时候,fork可能在子进程中破坏掉这种模式,进而使得程序出现无法预料的问题。所以一定要先准备好进程结构,再去使用线程!!!

3、进程入口函数的实现

 a)使用无属性的指针参数和固定参数的进程入口函数来实现
 b)使用面向对象的参数和统一的进程入口函数来实现
 c)使用模板函数来实现

这三种方式都可以实现,但是方便程度和安全性不一样。第一种方式技术上最简单,但是类型在转换的时候,可能出现问题。而且可以传入的参数数量是固定的,以后其他项目很难复用此代码。第二种方式比第一种好了不少。参数不是固定的,可移植性强了很多。但是这种方式需要专门写一个参数封装和解析的代码。这种解析代码的复用性会比较差。因为每个进程的任务不一样,参数也不一样,参数的含义也可能大相径庭。第三种方式难度最大,但是使用起来最方便,可以移植性最强。参数可以随时修改,函数也可以是类的成员函数。此外参数无需解析,直接原样转发到目标函数。实现起来也不需要太多代码,stl里面准备好了很多工具,可以直接使用。就是模板编程不太好理解。

*3c 模板函数和模板类

std::bind用于给一个可调用对象绑定参数。可调用对象包括函数对象(仿函数)、函数指针、函数引用、成员函数指针和数据成员指针。
std::forward(u)有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题。
std::move是无条件的转为右值引用,而std::forward是有条件的转为右值引用,更准确的说叫做Perfect forwarding(完美转发),而	std::forward里面蕴含着的条件则是Reference Collapsing(引用折叠)。

对于一个函数:虚函数特性和模板函数特性不能同时存在,但是一个模板类可以有虚函数

4、多进程通信为什么用本地套接字通信(最方便最快速)而不用其他通信方式?

1.文件通信依赖磁盘速度(大量读写容易造成磁盘击穿),且慢于网络传输
2.管道在多线程环境下不太方便(可能会出现多个线程往一个管道内写入数据导致数据错误),而且管道为单向的。(除非是一对一管道通信,否则不建议使用管道通信)
3、信号量的信息容量过小(只能用于通知信号状态变化,大量的日志数据无法通过信号量传递),但是传输速度很快,也不会出现跨进程/线程的的问题
4、内存共享需要反复加锁同步,否则可能会出现问题(加锁需要极力避免,否则会出现卡顿)
5、消息函数(sendmsg、recvmsg)需要创建时确定收发方 (但是也有优势:可以收发大量数据,且不像管道同时有数据插入的情况以及不需要上锁)
6、“网络套接字”通信也可以,但需要额外的IP和端口(指定IP和端口随时都可以连上),但因为需要占用双方的一个IP和端口(占用资源),高并发可能造成挤占效应

*5、关于守护进程/正常进程被杀其子进程的去留?

1、守护进程若设置了prctl(PR_SET_PDEATHSIG,SIGHUP);或者prctl(PR_SET_PDEATHSIG, SIGTERM)这个函数的作用是,当父进程挂掉后,会发送SIGHUP或者SIGTERM信号给子进程。做了测试程序,果然,这次关掉B脚本后,A就退出了。后来又在A的代码中加了个信号处理函数,处理了SIGTERM和SIGHUP,发现kill掉B脚本的时候,A确实接收到了信号。
2、正常情况下(默认),一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。由于孤儿进程会被init进程给收养,所以孤儿进程不会对系统造成危害。
3、一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。一个进程如果只复制fork子进程而不负责对子进程进行wait()或是waitpid()调用来释放其所占有资源的话,那么就会产生很多的僵死进程,如果要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会变成孤儿进程,从而被init所收养,这样init就会释放所有的僵死进程所占有的资源,从而结束僵死进程。
4、一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

6、使用“本地套接字”通信的优势

1、无需IP和端口,不影响服务器对外资源(虚拟本地文件地址,内核文件映射出来的,不占IP和端口,可以放心使用),无挤占效应
2、信息无需加锁,可以多线程并发写(还可以采用epoll多线程并发读)
3、数据传输量巨大,传输速率高(纯内存读写),没有经过磁盘读写
4、本地模式采用“本地套接字”通信,集群模式(日志服务器程序和客户端程序在两台不同的物理机器上)采用“网络套接字”通信
5、使用本地套接字通信后,若后期想改为网络套接字通信,工作量很小,基本上可以完全沿用所有逻辑,只在套接字创建和客户端创建时做小修改(把原来的文件和地址改成网络套接字通信需要的IP和端口即可)
6、A和B同时发100MB的数据包,经网卡驱动排序后逐个发送后日志服务器逐个接收,不会产生数据异常“插入”等。

7、日志服务器处理从其他进程发送的日志操作

1、对每个请求都开大量的线程处理(在资源峰值出现过分挤占)
2、固定若干线程处理其他进程发送的日志(接受日志),这种方式更合适

8、禁止拷贝构造和复制的三种方式*

1、设置拷贝构造与copy assign为私有
2、继承不可拷贝构造与拷贝赋值的基类
因为默认生成的拷贝构造函数会自动调用基类的拷贝构造函数,如果基类的拷贝构造函数是 private,那么它无法访问,也就无法正常生成拷贝构造函数。
3、使用delete

9、进程间通信的实现

1、初始化(服务端:创建套接字、绑定位置、监听;客户端:创建套接字)
2、链接
3、发送/接收
4、关闭(关闭套接字)

10、封装,抽象

1、抽象类用于继承,具有OOP特性,客户端只用上层接口函数,不用关注下层的具体实现;
2、当具体实现需要变化时,上层接口不变,因而不用改变上层具体代码。

11、static修饰类内成员变量和成员函数

1、静态成员变量
    1、静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。
    2、静态成员变量必须在类中声明,在类外全局定义(也可以同时初始化)(在类外全局定义和初始化[无需加static]可行int Data::data = 125;,但若要在外部其他函数内定义初始化静态成员变量(公有和私有都不行)不可行)。
    3、静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间。
    4、静态变量是所有对象共享的可以通过对象名访问。类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,若属于公有成员则可以通过类名直接去访问;私有静态变量在外部不能访问。
2、静态成员函数
    1、静态成员函数的目的 操作静态成员数据。
    2、静态成员函数 不能访问 非静态成员数据。(静态成员函数内部没有this指针)
    3、普通成员函数 可以操作 静态成员数据 非静态成员数据。
    4、静态成员变量和静态成员函数都有权限之分。
3、静态成员函数与非静态成员函数的异同
    1、相同点::无论静态函数还是非静态函数,都是属于类的(这一点与数据成员的静态非静态不同),对象并不拥有函数的拷贝。
    2、区别:
        非静态的函数由类对象(加.或指针加->)调用,这时将向函数传递this指针;而静态函数由类名:: (或对象名.)调用,但静态函数不传递this指针,不识别对象个体,所以通常用来对类的静态数据成员操作。
        非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。

12、Epoll的深入理解

参考链接:深入理解 Linux 的 epoll 机制

1、epoll 的使用非常简单,只有 3 个系统调用:
    1、epollcreate 负责创建一个池子,一个监控和管理句柄 fd 的池子;
    2、epollctl 负责管理这个池子里的 fd 增、删、改;
    3、pollwait 就是负责打盹的,让出 CPU 调度,但是只要有“事”,立马会从这里唤醒;
2、Linux 内核对于 epoll 池的内部实现就是用红黑树的结构体来管理这些注册进程来的句柄 fd。红黑树是一种平衡二叉树,时间复杂度为 O(log n),就算这个池子就算不断的增删改,也能保持非常稳定的查找性能。
3、文件描述符fd:Linux 设计成一切皆是文件的架构,这个不是说说而已,而是随处可见。实现一个文件系统的时候,就要实现这个文件调用,这个结构体用 struct file_operations 来表示。这个结构体有非常多的函数,精简了一些,如下:
struct file_operations {
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    int (*open) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    // ....
};

你看到了 read,write,open,fsync,poll 等等,这些都是对文件的定制处理操作,对于文件的操作其实都是在这个框架内实现逻辑而已,比如 ext2 如果有对 read/write 做定制化,那么就会是 ext2_read,ext2_write,ext4 就会是 ext4_read,ext4_write。在 open 具体“文件”的时候会赋值对应文件系统的 file_operations 给到 file 结构体。那我们很容易知道 read 是文件系统定制 fd 读的行为调用,write 是文件系统定制 fd 写的行为调用,file_operations->poll 呢?

4、poll回调是定制监听事件的机制实现。通过 poll 机制让上层能直接告诉底层,我这个 fd 一旦读写就绪了,请底层硬件(比如网卡)回调的时候自动把这个 fd 相关的结构体放到指定队列中,并且唤醒操作系统。

举个例子:网卡收发包其实走的异步流程,操作系统把数据丢到一个指定地点,网卡不断的从这个指定地点掏数据处理。请求响应通过中断回调来处理,中断一般拆分成两部分:硬中断和软中断。poll 函数就是把这个软中断回来的路上再加点料,只要读写事件触发的时候,就会立马通知到上层,采用这种事件通知的形式,浪费的时间窗就完全消失了。
划重点:这个 poll 事件回调机制则是 epoll 池高效最核心原理。
划重点:epoll 池管理的句柄只能是支持了 file_operations->poll 的文件 fd。换句话说,如果一个“文件”所在的文件系统没有实现 poll 接口,那么就用不了 epoll 机制。

5、poll 怎么设置?

在 epoll_ctl 下来的实现中,有一步是调用 vfs_poll 这个里面就会有个判断,如果 fd 所在的文件系统的 file_operations 实现了 poll ,那么就会直接调用,如果没有,那么就会报告响应的错误码。

static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt)
{
    if (unlikely(!file->f_op->poll))
        return DEFAULT_POLLMASK;
    return file->f_op->poll(file, pt);
}
6、poll 调用里面究竟是实现了什么?

总结概括来说:挂了个钩子,设置了唤醒的回调路径。epoll 跟底层对接的回调函数是:ep_poll_callback,这个函数其实很简单,做两件事情:
1、把事件就绪的 fd 对应的结构体放到一个特定的队列(就绪队列,ready list);
2、唤醒 epoll ,活来啦!
当 fd 满足可读可写的时候就会经过层层回调,最终调用到这个回调函数,把对应 fd 的结构体放入就绪队列中,从而把 epoll 从 epoll_wait 出唤醒。这个对应结构体是什么?结构体叫做 epitem ,每个注册到 epoll 池的 fd 都会对应一个。

7、就绪队列需要用很高级的数据结构吗?

就绪队列就简单了,因为没有查找的需求了呀,只要是在就绪队列中的 epitem ,都是事件就绪的,必须处理的。所以就绪队列就是一个最简单的双指针链表。

8、小结:epoll 之所以做到了高效,最关键的两点:
    1、内部管理 fd 使用了高效的红黑树结构管理,做到了增删改之后性能的优化和平衡;
    2、epoll 池添加 fd 的时候,调用 file_operations->poll ,把这个 fd 就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行;
    3、epoll 池核心的两个数据结构:红黑树和就绪列表。红黑树是为了应对用户的增删改需求,就绪列表是 fd 事件就绪之后放置的特殊地点,epoll 池只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组;

*13、在Linux下编程一定要熟练掌握日志法(读写日志)

子进程和线程崩溃是不会返回错误的,所以一定要及时记录日志以更好地定位问题所在。且一定要搞清楚模块设计(前后环境,模块重要/主要流程),以便定位问题所在模块,更好地回溯。如果不明确设计,当线程崩溃导致问题时也不会报错,如果没有日志法定位会不知道如何排查问题。

14、主模块的设计:

客户端的接入,分发客户端到 客户端处理进程 去处理 
开始——>创建日志服务器——>创建客户端处理进程——>创建网络服务器——>结束

*15、之前的本地日志服务器进程只开了单线程处理Epoll池,而网络服务器面对成千上万的客户端操作,需要创建线程池(每个线程都和网络服务器的线程函数绑定)使用多线程处理Epoll池的多路复用IO(高并发高并行),注意并行只能在多核CPU下生效。每个线程通过waitEvents等待Epoll返回事件,还需Link()-accept操作获得可处理客户端对象(状态、套接字和套接字参数[地址等])后,将其传给客户端处理进程进一步处理,然后继续等待。

16、线程池设计:开始——>构造——>启动——>添加任务——>等待——>关闭——>结束

接口:Start(),AddTask(),Close(),(private:) TaskDispatch()

为什么把构造和启动分开?

  • 若在构造时直接启动,主进程还未分离出专门用于处理这个线程池的子进程,那么线程池会留在主进程的线程内,子进程的线程(在Linux中,子进程可能不能继承主进程的线程)对其无法操作。所以最好还是把启动和构造分离,等准备就绪后再启动。

为什么要设置线程池?

  • 1、新建、启动线程需要时间Time和资源Resource(内存、线程ID),如果内存耗尽就启动不了新的线程;如果任务不紧急的情况下根据需要再启动线程是没关系的,但在高并发的情况下如果这时候再启动可能会导致线程启动失败和延迟过大。因此为了避免这种问题,在刚开始资源充足的情况下把重要的核心线程先分配好时间资源,就能够从容应对后续的高压力状态。
  • 2、因为线程对象属于内核的,因此线程池只是对其进行了封装操作,是无法被复制、赋值的。
  • 3、任务分配:用epoll机制和本地套接字实现线程任务的自动分配,epoll在start函数初始化且epoll需要指定线程个数,在多个线程内同时进行wait,通过本地套接字将任务函数发到每个线程去,哪个线程能拿到由内核自己决定。这样做的好处:1、不依赖锁和队列下能够实现多线程的任务分配(外面添加任务也是多线程的),效率非常高(比起我们自己加锁解锁)。2、多线程同时读写都没有问题(多路复用IO)。

*17、为什么ThreadPool线程池中的vector容器不能直接放线程对象而是放入线程指针?

要放到容器里的对象一定要有默认构造函数和复制构造函数,线程没有复制构造函数,因此不能直接放入容器内,因此只能放入指针。

18、抽象接口(Cbusiness类)最大的作用是解耦:

如果没有这类接口,处理业务的函数大部分都要写进主模块里,而主模块的大部分内容都和业务无关,业务再怎么变化底层逻辑也不会改变。
因此通过设计了接口层我们实现了解耦,把业务层剥离,等到我们需要处理业务的时候再去实现。
客户端处理模块实际上就是业务层的实现。

19、连接层

主模块的epoll池是用来接入客户端的,而业务层的epoll池是用来处理客户端的),因此实际开发中主模块线程池大小应该不会超过客户端处理的线程池大小(一般情况下业务层耗费的时间资源较连接层更多)

20、HTTP模块的设计(HTTP收发数据包和URL的解析)

  • 1、URL:万维网上,每一信息资源都有统一的且在网上的地址,该地址成为统一资源定位符(Uniform Resource Locator),URL由三部分组成:资源类型、存放资源的主机域名、资源文件名。也可认为由4部分组成:协议、主机、端口、路径.
    + 2、HTTP协议发送的包数据解析。
    + 3、封装的作用:a)降低使用成本(若不封装你要在每次使用的时候把http_parser.h所有细节初始化到位,然后一步步调用);b)对外屏蔽细节(低耦合,你想要的数据通过公有函数获取,内部的细节函数你不知道也不能知道);c)增加可移植性(可能牺牲一点点性能);d)与更多同类数据关联(高内聚)

http_parser.h(*.c)是网上现有的轮子,核心思想是面向过程,每次获取到一定数据后即调用对应回调函数(如果回调函数非空),主要给C语言提供方法,而我们C++核心思想应是面向对象,所以也应该封装成我们需要的

21、数据库模块设计(要有一个大纲,明确的目标,方便后面编码实现时不会迷失方向,在设计阶段时不会关注具体的算法实现,而会简单思考联系与局限:什么合适什么不合适)

  • 1、实现基类DataBaseClient(对外统一基本接口),通用封装。
  • 2、对于数据库,要么是非事务型:Connect->Exec->处理结果->close 或者事务型:Connect->StartTransaction->Exec->CommitTransaction->Close,对于有开始事务的数据库,可以在遇到错误时进行回滚,安全性更高。(为什么要开启事务?对于数据库往往有多张表同时进行操作,比如更新了某张表可能会需要进一步更新和这张表有关的其他表,那么就需要分步执行操作,在这些操作处理中不能保证每步都一定执行成功没有错误,也仍然可能失败,因为软件依托于硬件执行,硬件也可能出问题导致失败,所以软件不能保证100%成功。如果第二步失败了第一步成功了,即出现数据不一致(脱节)问题,就应该将第一步进行回滚操作,保证回到执行该任务前的状态)这种机制保证了只要没有进行提交事务(CommitTransaction)结束,数据库一定会回滚使得和之前的状态保证一致。
  • 3、为什么每个线程执行数据库任务都要连接后立即关闭?(一个数据库能够同时接手的SQL事务是有限的,如果有若干线程占用了资源不动,一直卡着其他线程就用不了了)

22、关于类成员函数是否内联?

思考是否会出现交叉引用的情况,若会出现,最好将声明和定义(实现)分离,这样头文件和cpp实现文件分离就可以规避这种问题。

23、关于数据库基类是否能使用复制构造函数和赋值运算符重载函数?

因为客户端和数据库交互通过套接字,套接字不能被直接复制,应重新创建一个新的,所以不能被复制拷贝。

24、为什么mysql和sqlite3要分开定义实现?

因为mysql和sqlite3可能都有对应特定的类型(对方没有我有)要定义和使用,分开定义可以不相互影响,且都是从父类(抽象数据库类)派生的,有共同特征而也可以接受细微区别,不会影响到对方。

25、二级指针解析的注意事项

注意来回传数据时,char*字符指针指向空地址的时候不可以把这个指针转换成Buffer(会报错),例如:若某个二级指针非空,你在二级指针检查是否为空时考虑到了,但你解这个二级指针的时候忘记考虑一级指针是否为空,直接转Buffer类会报错:terminate called after throwing an instance of 'std::logic_error' what():  basic_string: construction from null is not valid 已放弃 (核心已转储)

26、加密模块的设计与实现

设置为工具类的原因:

加密模块一般都是以工具类的模块实现,无需声明对象,都是静态方法。  
每个方法之间没有必要的联系,可以相互独立也可以相互关联(加法是减法的逆运算,但是两者相互独立,有时候也可以相互关联比如一起用[阶乘:加和乘一起用])
随取随用,无需配置或者初始化

加盐和时间字符串的原因

更好地区分传输数据,防止碰撞(把所有的字母数字符号排列组合算出来MD5,当传输的密文数据长度较短时可以很快地查出来明文结果,密文越长破解代价越大,但是越长也意味着消耗的时间和资源越多,所以要平衡)  
密码和私有字符串,不用发给服务器,可以杜绝传输过程中的安全隐患。  
发送时只用传输用户+盐+时间+MD5(MD5计算自【用户+密码+盐+时间字符串+私有字符串】拼接),防止传输时发生安全信息泄露问题  
盐:随机字符串  
私有字符串:定制加密字符串

27、项目测试:测试的越全面,代码越可靠

1. 功能测试:包括单元测试和模块测试,目的是验证项目的功能是否正确实现,和预期一致
2. 性能测试(有些人把可靠性测试和安全测试分开,其实也属于性能测试):包括稳定性测试和压力测试,稳定性测试一般是写固定的脚本或者程序,反复触发被测试程序的功能或接口。触发可以按照次数触发或者按照时间触发。比如接口类的,会按照次数来计算。每千/万/十万/百万次调用,失败的次数。比如时间类的,会按照系统使用多少小时/天,出现错误/崩溃的次数来计算。压力测试是给软件不断加压,强制其在极限的情况下运行,观察它可以运行到何种程度,从而发现性能缺陷,是通过搭建与实际环境相似的测试环境,通过测试程序在同一时间内或某一段时间内,向系统发送预期数量的交易请求、测试系统在不同压力情况下的效率状况,以及系统可以承受的压力情况。(压力测试是测试上限,稳定性测试偏重于测试在某套件下的耐力)
3. 其他的分类方法:黑盒测试、白盒测试、灰盒测试;动态测试、静态测试;动员测试、集成测试。

总结1:每章干了什么

三、进程模块:
    1、进程类封装
    2、进程入口函数模板类封装
    3、进程间的信息传输(文件描述符操作封装,实际采用单向管道读写数据)
    4、守护进程实现
    5、测试
四、日志服务器模块
    1、日志服务器进程类封装
五、Epoll接口封装
    1、EPoll功能模块封装(实现了多路复用IO)
六、主要类的设计与实现
    1、本地套接字和网络套接字的接口类封装
    2、线程功能封装
七、日志模块
    1、日志模块的调用接口扩展与应用实现(接口底层具体化实现记录日志,宏命令定义不同性质日志输出)
    2、测试
八、主模块
    1、线程池模块类封装与实现
    2、主模块类封装与实现(客户端的接入,分发“建立好连接的客户端套接字”给客户端处理进程以进行任务处理)
    3、测试
九、客户端处理模块
    1、客户端处理模块的封装与实现
十、HTPP模块
    1、HTTP解析类封装与实现(HTTP请求解析和URL解析)
    2、测试
十一,十二、数据库模块
    1、通用数据库接口类封装
    2、Sqlite3功能模块类封装与实现
    3、Mysql功能模块类封装与实现
    4、使用宏命令定义表结构(方便使用)
    5、两类数据库的测试
十三、加密模块
    1、MD5数据加密实现
    2、测试
十四、业务模块
    1、业务模块功能封装与实现
十五、项目集成测试
	跨进程实现套接字传递,主进程只负责接入,由子进程的线程负责处理套接字(多进程多线程)
八、线程池测试
十、HTTP地址解析封装、URL解析封装
十一十二、数据库模块封装(增删改查)
十三、加密模块的封装和实现 OPenssl MD5 用户名+时间+盐+私密字符串 =》MD5加密串匹配
十四、业务模块实现
十五、集成测试:整体系统DEBUG

总结2:核心技术

模板类设计:

  1. 函数模板类-CFunction
    • 通过设计可变参数模板类(std::binder解决参数绑定问题+模板类解决不同类别参数问题)传递每个进程的处理和回调函数,解决不同功能需要不同参数的问题(后期就不用为一个新功能重新定制一整个函数模块)。应用:日志服务器和客户端的CProcess类中的进程函数、线程函数、服务器进程的连接成功回调函数和接收成功回调函数。将进程设计为抽象模板类,所有日志进程/业务进程全从中派生。
    • 为什么CFunction要继承一个基类CFunctionBase?我们创建一个CProcess进程对象有一个指针成员*m_func声明为CFunctionBase类型而不是CFunction类型,但我们可以将CFunction函数存储在基类指针中,这样能够防止m_func被传染为模板函数。
  2. 业务层的连接成功回调函数模板类CConnectedFunction: public CFunctionBase和接收成功回调函数模板类CReceivedFunction: public CFunctionBase
    + 为什么不用1.CFunction?(参考我进一步阐述的链接C++模板类作为类指针成员(指向普通函数和类内静态函数,相当于类处理函数)和运算符重载函数以及占位符一起使用产生的参数无法匹配问题解决办法——分家!)
因为这两个函数还需要在对象创建后执行函数时传入额外的参数(连接成功时传入客户端对象,接收成功传入客户端对象和字符串数据)
需要重定义()运算符函数,而对CFunction的()运算符实现多态这样在编译时会报错(因为参数类型根本不匹配),具体就是编译过程中,你在构建这个模板类对象时需要将这个模板类拟定的类型具体化,然后对于所有()重载运算符函数都会通过std::m_binder函数输入1个或两个占位符,
拿两个占位符做例子,但是对于没有参数的()运算符重载函数,无法做到和占位符需要的参数相匹配,导致编译器认为这里存在异常(即使你在使用时只会调用这个带两个参数的重载运算符函数),所以报错无法通过编译。因此重新定义了对应的两个模板类(只改了对应数目参数的()重载运算符函数)

类的模板函数设计:

1. CProcess::SetFunction,为不同进程提供可变函数类型(函数名称,返回值类型,特征标/参数列表)和可变参数;
2. CThread::CThread构造函数(在创建线程对象时直接指定线程执行函数-可变参数和可变类型函数)和CThread::SetThreadFunc 模板函数(用于使用不带参数的构造函数创建的线程对象后面指定线程执行函数-可变参数和可变类型函数)
3. CBusiness::setConnectedCallback和CBusiness:setRecvCallback,为不同业务提供"可变函数和可变参数"的的连接成功回调函数和接收成功回调函数;
4. CThreadPoll::AddTask增加任务模板函数,客户端需要处理任务时,创建指定的任务(可变参数和可变类型模板)指针,让该线程特定的客户端静态套接字对象(注意这里的客户端套接字对象已由服务端添加至Epoll池中等待套接字已处理完毕,转为就绪状态)将函数地址进行发送,然后从Epoll池读到函数地址,将该套接字从Epoll池中删除,执行完函数后释放函数空间。
5. LogInfo::LogInfo<<重载运算符模板函数,提供可变数据输入日志和连续使用<<操作。

你可能感兴趣的:(linux,服务器,c++)