进程是操作系统资源分配最小的调度单位,意味着进程被暂停后,其下所有线程都会失去调度资源的权限。要充分利用系统资源,最好的形式是多线程多进程模式。
我们最好将一个整体功能,分散到多个进程当中,从而实现资源利用率最大化。否则就只能多个线程在一个进程内进行竞争,没办法充分利用系统资源。
在linux中,开启进程一般通过exec系列函数或者fork函数来完成。即使是exec函数,也会要使用到fork函数。
所以开启进程,fork函数是无法绕开的。而fork函数会对线程造成影响,所以我们一定要先定好进程结构,然后再开启线程。原因:首先,由于线程无法被复制,所以在子进程中,一些线程会消失(没有被复制过来;其次,如果程序逻辑依赖多线程模式的时候,fork可能在子进程中破坏掉这种模式,进而使得程序出现无法预料的问题。所以一定要先准备好进程结构,再去使用线程!!!
a)使用无属性的指针参数和固定参数的进程入口函数来实现
b)使用面向对象的参数和统一的进程入口函数来实现
c)使用模板函数来实现
这三种方式都可以实现,但是方便程度和安全性不一样。第一种方式技术上最简单,但是类型在转换的时候,可能出现问题。而且可以传入的参数数量是固定的,以后其他项目很难复用此代码。第二种方式比第一种好了不少。参数不是固定的,可移植性强了很多。但是这种方式需要专门写一个参数封装和解析的代码。这种解析代码的复用性会比较差。因为每个进程的任务不一样,参数也不一样,参数的含义也可能大相径庭。第三种方式难度最大,但是使用起来最方便,可以移植性最强。参数可以随时修改,函数也可以是类的成员函数。此外参数无需解析,直接原样转发到目标函数。实现起来也不需要太多代码,stl里面准备好了很多工具,可以直接使用。就是模板编程不太好理解。
std::bind用于给一个可调用对象绑定参数。可调用对象包括函数对象(仿函数)、函数指针、函数引用、成员函数指针和数据成员指针。
std::forward(u)有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题。
std::move是无条件的转为右值引用,而std::forward是有条件的转为右值引用,更准确的说叫做Perfect forwarding(完美转发),而 std::forward里面蕴含着的条件则是Reference Collapsing(引用折叠)。
对于一个函数:虚函数特性和模板函数特性不能同时存在,但是一个模板类可以有虚函数
1.文件通信依赖磁盘速度(大量读写容易造成磁盘击穿),且慢于网络传输
2.管道在多线程环境下不太方便(可能会出现多个线程往一个管道内写入数据导致数据错误),而且管道为单向的。(除非是一对一管道通信,否则不建议使用管道通信)
3、信号量的信息容量过小(只能用于通知信号状态变化,大量的日志数据无法通过信号量传递),但是传输速度很快,也不会出现跨进程/线程的的问题
4、内存共享需要反复加锁同步,否则可能会出现问题(加锁需要极力避免,否则会出现卡顿)
5、消息函数(sendmsg、recvmsg)需要创建时确定收发方 (但是也有优势:可以收发大量数据,且不像管道同时有数据插入的情况以及不需要上锁)
6、“网络套接字”通信也可以,但需要额外的IP和端口(指定IP和端口随时都可以连上),但因为需要占用双方的一个IP和端口(占用资源),高并发可能造成挤占效应
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的输出都需要特殊处理。
1、无需IP和端口,不影响服务器对外资源(虚拟本地文件地址,内核文件映射出来的,不占IP和端口,可以放心使用),无挤占效应
2、信息无需加锁,可以多线程并发写(还可以采用epoll多线程并发读)
3、数据传输量巨大,传输速率高(纯内存读写),没有经过磁盘读写
4、本地模式采用“本地套接字”通信,集群模式(日志服务器程序和客户端程序在两台不同的物理机器上)采用“网络套接字”通信
5、使用本地套接字通信后,若后期想改为网络套接字通信,工作量很小,基本上可以完全沿用所有逻辑,只在套接字创建和客户端创建时做小修改(把原来的文件和地址改成网络套接字通信需要的IP和端口即可)
6、A和B同时发100MB的数据包,经网卡驱动排序后逐个发送后日志服务器逐个接收,不会产生数据异常“插入”等。
1、对每个请求都开大量的线程处理(在资源峰值出现过分挤占)
2、固定若干线程处理其他进程发送的日志(接受日志),这种方式更合适
1、设置拷贝构造与copy assign为私有
2、继承不可拷贝构造与拷贝赋值的基类
因为默认生成的拷贝构造函数会自动调用基类的拷贝构造函数,如果基类的拷贝构造函数是 private,那么它无法访问,也就无法正常生成拷贝构造函数。
3、使用delete
1、初始化(服务端:创建套接字、绑定位置、监听;客户端:创建套接字)
2、链接
3、发送/接收
4、关闭(关闭套接字)
1、抽象类用于继承,具有OOP特性,客户端只用上层接口函数,不用关注下层的具体实现;
2、当具体实现需要变化时,上层接口不变,因而不用改变上层具体代码。
1、静态成员变量
1、静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。
2、静态成员变量必须在类中声明,在类外全局定义(也可以同时初始化)(在类外全局定义和初始化[无需加static]可行int Data::data = 125;,但若要在外部其他函数内定义初始化静态成员变量(公有和私有都不行)不可行)。
3、静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间。
4、静态变量是所有对象共享的可以通过对象名访问。类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,若属于公有成员则可以通过类名直接去访问;私有静态变量在外部不能访问。
2、静态成员函数
1、静态成员函数的目的 操作静态成员数据。
2、静态成员函数 不能访问 非静态成员数据。(静态成员函数内部没有this指针)
3、普通成员函数 可以操作 静态成员数据 非静态成员数据。
4、静态成员变量和静态成员函数都有权限之分。
3、静态成员函数与非静态成员函数的异同
1、相同点::无论静态函数还是非静态函数,都是属于类的(这一点与数据成员的静态非静态不同),对象并不拥有函数的拷贝。
2、区别:
非静态的函数由类对象(加.或指针加->)调用,这时将向函数传递this指针;而静态函数由类名:: (或对象名.)调用,但静态函数不传递this指针,不识别对象个体,所以通常用来对类的静态数据成员操作。
非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。
参考链接:深入理解 Linux 的 epoll 机制
1、epollcreate 负责创建一个池子,一个监控和管理句柄 fd 的池子;
2、epollctl 负责管理这个池子里的 fd 增、删、改;
3、pollwait 就是负责打盹的,让出 CPU 调度,但是只要有“事”,立马会从这里唤醒;
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 呢?
举个例子:网卡收发包其实走的异步流程,操作系统把数据丢到一个指定地点,网卡不断的从这个指定地点掏数据处理。请求响应通过中断回调来处理,中断一般拆分成两部分:硬中断和软中断。poll 函数就是把这个软中断回来的路上再加点料,只要读写事件触发的时候,就会立马通知到上层,采用这种事件通知的形式,浪费的时间窗就完全消失了。
划重点:这个 poll 事件回调机制则是 epoll 池高效最核心原理。
划重点:epoll 池管理的句柄只能是支持了 file_operations->poll 的文件 fd。换句话说,如果一个“文件”所在的文件系统没有实现 poll 接口,那么就用不了 epoll 机制。
在 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);
}
总结概括来说:挂了个钩子,设置了唤醒的回调路径。epoll 跟底层对接的回调函数是:ep_poll_callback,这个函数其实很简单,做两件事情:
1、把事件就绪的 fd 对应的结构体放到一个特定的队列(就绪队列,ready list);
2、唤醒 epoll ,活来啦!
当 fd 满足可读可写的时候就会经过层层回调,最终调用到这个回调函数,把对应 fd 的结构体放入就绪队列中,从而把 epoll 从 epoll_wait 出唤醒。这个对应结构体是什么?结构体叫做 epitem ,每个注册到 epoll 池的 fd 都会对应一个。
就绪队列就简单了,因为没有查找的需求了呀,只要是在就绪队列中的 epitem ,都是事件就绪的,必须处理的。所以就绪队列就是一个最简单的双指针链表。
1、内部管理 fd 使用了高效的红黑树结构管理,做到了增删改之后性能的优化和平衡;
2、epoll 池添加 fd 的时候,调用 file_operations->poll ,把这个 fd 就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行;
3、epoll 池核心的两个数据结构:红黑树和就绪列表。红黑树是为了应对用户的增删改需求,就绪列表是 fd 事件就绪之后放置的特殊地点,epoll 池只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组;
子进程和线程崩溃是不会返回错误的,所以一定要及时记录日志以更好地定位问题所在。且一定要搞清楚模块设计(前后环境,模块重要/主要流程),以便定位问题所在模块,更好地回溯。如果不明确设计,当线程崩溃导致问题时也不会报错,如果没有日志法定位会不知道如何排查问题。
客户端的接入,分发客户端到 客户端处理进程 去处理
开始——>创建日志服务器——>创建客户端处理进程——>创建网络服务器——>结束
接口:Start(),AddTask(),Close(),(private:) TaskDispatch()
为什么把构造和启动分开?
为什么要设置线程池?
要放到容器里的对象一定要有默认构造函数和复制构造函数,线程没有复制构造函数,因此不能直接放入容器内,因此只能放入指针。
如果没有这类接口,处理业务的函数大部分都要写进主模块里,而主模块的大部分内容都和业务无关,业务再怎么变化底层逻辑也不会改变。
因此通过设计了接口层我们实现了解耦,把业务层剥离,等到我们需要处理业务的时候再去实现。
客户端处理模块实际上就是业务层的实现。
主模块的epoll池是用来接入客户端的,而业务层的epoll池是用来处理客户端的),因此实际开发中主模块线程池大小应该不会超过客户端处理的线程池大小(一般情况下业务层耗费的时间资源较连接层更多)
http_parser.h(*.c)是网上现有的轮子,核心思想是面向过程,每次获取到一定数据后即调用对应回调函数(如果回调函数非空),主要给C语言提供方法,而我们C++核心思想应是面向对象,所以也应该封装成我们需要的
思考是否会出现交叉引用的情况,若会出现,最好将声明和定义(实现)分离,这样头文件和cpp实现文件分离就可以规避这种问题。
因为客户端和数据库交互通过套接字,套接字不能被直接复制,应重新创建一个新的,所以不能被复制拷贝。
因为mysql和sqlite3可能都有对应特定的类型(对方没有我有)要定义和使用,分开定义可以不相互影响,且都是从父类(抽象数据库类)派生的,有共同特征而也可以接受细微区别,不会影响到对方。
注意来回传数据时,char*字符指针指向空地址的时候不可以把这个指针转换成Buffer(会报错),例如:若某个二级指针非空,你在二级指针检查是否为空时考虑到了,但你解这个二级指针的时候忘记考虑一级指针是否为空,直接转Buffer类会报错:terminate called after throwing an instance of 'std::logic_error' what(): basic_string: construction from null is not valid 已放弃 (核心已转储)
加密模块一般都是以工具类的模块实现,无需声明对象,都是静态方法。
每个方法之间没有必要的联系,可以相互独立也可以相互关联(加法是减法的逆运算,但是两者相互独立,有时候也可以相互关联比如一起用[阶乘:加和乘一起用])
随取随用,无需配置或者初始化
更好地区分传输数据,防止碰撞(把所有的字母数字符号排列组合算出来MD5,当传输的密文数据长度较短时可以很快地查出来明文结果,密文越长破解代价越大,但是越长也意味着消耗的时间和资源越多,所以要平衡)
密码和私有字符串,不用发给服务器,可以杜绝传输过程中的安全隐患。
发送时只用传输用户+盐+时间+MD5(MD5计算自【用户+密码+盐+时间字符串+私有字符串】拼接),防止传输时发生安全信息泄露问题
盐:随机字符串
私有字符串:定制加密字符串
1. 功能测试:包括单元测试和模块测试,目的是验证项目的功能是否正确实现,和预期一致
2. 性能测试(有些人把可靠性测试和安全测试分开,其实也属于性能测试):包括稳定性测试和压力测试,稳定性测试一般是写固定的脚本或者程序,反复触发被测试程序的功能或接口。触发可以按照次数触发或者按照时间触发。比如接口类的,会按照次数来计算。每千/万/十万/百万次调用,失败的次数。比如时间类的,会按照系统使用多少小时/天,出现错误/崩溃的次数来计算。压力测试是给软件不断加压,强制其在极限的情况下运行,观察它可以运行到何种程度,从而发现性能缺陷,是通过搭建与实际环境相似的测试环境,通过测试程序在同一时间内或某一段时间内,向系统发送预期数量的交易请求、测试系统在不同压力情况下的效率状况,以及系统可以承受的压力情况。(压力测试是测试上限,稳定性测试偏重于测试在某套件下的耐力)
3. 其他的分类方法:黑盒测试、白盒测试、灰盒测试;动态测试、静态测试;动员测试、集成测试。
三、进程模块:
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
因为这两个函数还需要在对象创建后执行函数时传入额外的参数(连接成功时传入客户端对象,接收成功传入客户端对象和字符串数据)
需要重定义()运算符函数,而对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<<重载运算符模板函数,提供可变数据输入日志和连续使用<<操作。