文章目录
前言
各位C友们,好久不见,最近一个月在搞项目,算是半摆半学的状态吧,博客断更了一段时间,现在项目搞完了,博客之后也会慢慢更新的,最近的几篇文章会将最近一个月写的项目总结出来分享给大家,希望对大家有所帮助!话不多说,先来对项目做一个简要的总结,即对系统的接口使用C++进行再度封装,实现一个高并发服务器的组件,用户可以根据切换具体的应用层协议和快速搭建服务器。下文进行项目的详细介绍。
一、效果演示
说明:效果是基于Http协议搭建的服务器的主页和错误界面的响应,只是为了简单的验证项目能否正常的运作,并不能说明服务器的性能,在项目最后的测试部分,将会进行压力测试,看所搭建服务器的性能如何。
主页
错误
经过如上实验可以简单的得出,所编写的最终服务器还是没有大毛病的,可以进行正常的文件读写。至于其它的实际的功能性接口,即一些网站实际业务的处理接口,不在此项目的能力范围之内。如要进行验证,则要搭建对应的前端的服务并实现后端的处理接口,有兴趣的同学可根据需要自行实现并验证。
二、模块
1. 介绍
- 命令
find . \( -name "*.cc" -o -name "*.h" -o -name "*.hpp" \) -type f -exec wc -l {} +
使用此命令,查看一下本项目的总代码量大概在3500行左右,总的来说还是有不少的代码的,但也可以学习到不少的技术。
2. 服务器模块
本模块将从对连接和处理两个角度进行简要介绍,具体的实现还要落实到代码实现部分,连接主要是对系统的接口使用C++进行封装成类,便于上层进行调用;处理是通过多线程进行实现的,主要采用的是主从Reactor模式实现,即将连接的监听和初始化放在主Reactor中,而连接的处理操作则放在其它的Reactor中,从而提升服务器的性能。
关于连接实现的子模块:
套接字,关于连接的套接字
图解:
- 网络套接字,即文件描述符,根据不同的用途可分为客户端发起连接的套接字,服务端用于监听的套接字,以及从全连接队列中拿上来的套接字三种,根据不同的需要此处对套接字运用继承的思想,进一步的具象化实现,方便进行使用。
- 服务端监听套接字,继承普通套接字的功能外,还需要进行对连接的绑定,监听,接收功能。
- 服务端接收套接字,只需要接收服务端通过监听获取接收的套接字,从而进行业务的处理
- 客户端套接字,除基本的接收和发送数据爱,还需额外实现对服务端发送建立连接的请求。
连接事件的监听模块,即对epoll系列接口的封装
图解:
- Linux操作系统内核中,对套接字事件的管理已经有成熟的epoll系列的接口供我们使用,我们只需对现成的接口进行再度封装,更加简便的使用即可,除此之外如果对这一部分有疑惑的,可见博主之前写的一篇文章:【Linux进阶之路】高级IO,里面系统的介绍了IO模型。
- 构造函数,将会调用epoll_create创建epoll_fd用于管理就绪和监听事件,内核中采用的是红黑树进行的管理。
- 事件的更新,涉及三个部分,即事件的添加,修改,删除,涉及三个选项,EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL,是对内核结构struct epoll_event的修改。
- 事件的添加和删除,除了对内核结构的修改外,还要对哈希表中的结构,即用户对事件的监控的结构,进行修改。
- 事件的等待,即阻塞等待就绪的文件描述符,然后方便对其执行业务处理。
连接事件的处理模块,即对就绪事件设置回调函数
图解:
- 总的来说,主要是读,写,刷新,错误四种事件的设置,取消,打开和处理,事件的更新和移除操作,事件属性信息的获取。
- 回调函数的设置是在连接初始化时进行的,取消是在连接异常或者关闭时进行的。刷新回调与项目的非活跃销毁有关(之后会提及),错误回调指的是对连接进行释放操作。
- 就绪事件的设置是在Poller的Wait接口进行设置的,处理是在多线程中的任务池执行的,根据就绪和监控事件进行执行对应的回调方法。
- 监控事件的打开是在连接初始化完毕进行打开的,关闭是在连接异常或者不需要关心时进行。
- 更新和移除事件,指的是通过调用Poller中的更新和移除的接口进行实现的,此处只做简要介绍即可。
连接事件的监听模块,即对连接进行监听和接收
图解:
- Acceptor模块是对监听套接字SSocket的进一步封装,在构造完成对套接字的初始化,绑定,监听;设置连接处理回调函数。
- SSocket在构造函数自动完成绑定与监听工作,通过Channel来完成读事件的处理和监控,Channel会调用Poller中的UpDate进行监控。
- 连接处理回调的设置,回调函数由服务器Server对象进行设置,主要完成的是对连接初始化工作,设置完即可启动读监控获取连接了。
连接事件的定时模块,即设置定时任务以及非活跃销毁连接
图解1:
- TimerWheel主要完成定时任务的添加,刷新,取消,管理功能。主要采用时间轮和timer_fd文件描述符,智能指针的技术进行实现。
- 初始化工作主要完成timerfd的创建,时间轮的初始化,使用Channe设置与打开读回调,监控与执行timerfd的读事件。
- 定时任务的刷新在被Channel管理连接事件就绪执行,添加则是将定时任务的信息添加到vector和unordered_map中,取消则是调用定时任务(TimerTask)的取消定时销毁接口或者从unordered_map移除定时任务对象。
图解2:
- TimerTask则是对定时任务的设置,执行,取消,资源销毁,对定时任务属性信息的管理,辅助TimerWheel的实现。
- 初始化功能主要是通过TimerWheel进行添加时用来设置对应任务的属性信息,和回调函数。
- TimerTask实际上由shared_ptr进行管理,并当TimerWheel刷新其引用计数为0时调用析构执行定时任务和资源销毁。
- 设置资源销毁,其实就是从TimerWheel的unordered_map中移除对应的管理信息。
连接事件的存储模块,即缓存区,一个连接有一对缓存区,即读缓冲区和写缓存区,读缓存区存放客户端发来的数据,写缓存区存放服务器处理后的信息,用于对客户端进行响应。
图解:
- Buffer的设计思想是采用双指针或者叫滑动窗口更为合适,即vector容器的下标[读偏移,写偏移) 的中间存放着可读数据块,在读写过程中读偏移和写偏移会动态的移动,因此叫滑动窗口更为贴切。其它的接口都是根据此实现的,在后续文章的实现代码即可深刻体会。
连接事件的上下文模块,因为服务器可能采用的应用层协议使用是不同的上下文,因此使用的是任意类,即一个可以存放任意元素的类进行实现,方便应用层丝滑地切换协议。
图解:
- Any类的设计思想还是很值得学习的,即如何设计一个类存放任意元素(内置和自定义类),上述主要是通过内置父类,以及模版子类,Any通过存放一个指向父类指针的成员变量实现,总的来说将内置类,多态,继承运用的十分巧妙,Any外部采用模版取值函数,构造进行获取和初始化进行再度封装。
连接事件的管理模块,整合之前的存储模块,上下文模块,定时模块完成对连接的统一处理
图解:
Connection算的上一个比较综合的模块了,起的 “承上启下” 的作用,即向上为用户提供消息处理,任意事件,初始化连接,上下文的记录回调,及其设置和切换。向下整合Buffer,TimeWheel,Any模块,从而完善连接的功能。
从回调来看,用户只需要关心消息是如何处理的,并不需要关心如何将消息读取和发送;连接的初始化工作,也需要通过用户来完成,比如说上下文的类型就需要通过设置Any类来完成;上下文记录和任意事件处理,用户可关心可不关心。
从读写来看,读取数据需要一个缓存区将数据从内核的缓存区中读取出来,便于之后的数据接收,写数据则是将数据快速的发送,并当内核的发送缓冲区
从上下文来看,设置上下文是通过连接初始化时进行调用的,而切换则是将用户的回调进行全部的切换,进而切换应用层协议。
从非活跃来看,即在TimerWheel中添加一个定时销毁连接的任务,当连接有事件就绪时就进行刷新,直到超过指定时间内没有事件就绪进行销毁处理,从而释放一些服务器资源,避免闲置资源。
关于事件处理实现的子模块:
连接事件的线程与任务模块,使用多线程,完成对连接处理任务的分配,使用任务池完成对连接的处理等功能。
图解:
- 此模型主要采用的是Reactor中的主从模式进行实现,即一个线程,通常为主线程完成对连接的监听和初始化,而对于连接的处理工作,则交由线程池中的线程来完成,其中每一个线程绑定一个EventLoop,拥有自己的任务池,用于执行任务。这样连接的接收和处理进行解耦,增加了任务执行效率。
- 从LoopThread看,创建线程对象,并绑定自定义实现的入口函数,入口函数创建并绑定EventLoop,并开启事件循环。
- 从LoopThreadPool看,创建出一定数量的线程,并用容器存储对应的LoopThread对象和EventLoop对象的地址,用于给连接分配线程。
- 从EventLoop看,主要功能是通过Poller对象,即事件监控对象完成对就绪事件的获取,再通过事件对应的Channel对象将就绪任务统一放到任务池中,最后统一执行任务池中的所有任务。同时可以通过添加定时任务完成对Connection的非活跃销毁功能。而互斥锁则是为了保证异常状态下任务池的同步执行。
综合所有模块的服务器模块,即整合连接的实现和处理两大模块中的所有模块向上提供的封装模块。
图解:
- TcpServer模块主要连接的监听,分配,管理的功能,即子线程完成对连接的处理工作,主线程完成对连接的监听和初始化工作,包括对创建Connection连接管理对象,分配事件循环,设置回调等,通过哈希表完成对连接的管理和查询功能;
此处整合前面的模块的,画出线程关于连接分配和处理的流程:
3. 应用层模块
连接的处理和分配的关于服务器性能方面的事情已经介绍完毕了,下面介绍一下本项目采用的上层的应用层协议——Http协议及其相关的功能模块。
应用层协议的功能模块,由于Http协议的报文的分析的是对字符串解析的工作,以及在进行业务处理的过程中可能会从服务器获取文件,因此关于这些功能的接口将被放在此模块当中。
图解:
应用层协议的解析模块,网络的协议格式一般都是报头 + 报文的形式,Http的协议的请求大致组成为请求行 + 消息头 + 消息体,响应的组成为状态行 + 消息头 + 消息体。通过对信息解析成一些变量,或者放到特定的数据结构中,便可方便进行获取和管理。
请求模块——
响应模块——
上下文模块——
- 通过HttpContext完成对请求的解析并将结果存放在HttpRequest中,通过服务器的业务处理,完成对HttpRespond的填充,最后通过对HttpRespond的组装完成对客户端的应答。
应用层协议的服务器模块,主要是根据之前的服务器模块和当前模块的请求方法,针对的进行业务处理,最终将响应返回给客户端。
图解:
- HttpServer主要包含初始化,更新动态业务处理函数,消息处理三大功能,详细涉及到之前的HttpContext,HttpResponse,Connection,Buffer等模块。
- 初始化主要是通过TcpServer完成对消息处理函数以及对连接初始化时对上下文的绑定,启动非活跃销毁连接的功能,以及设置静态处理时获取文件的资源路径。
- 更新动态业务处理函数,主要是通过请求方法,字符串,函数指针回调,将相应的处理方法设置到哈希表当中,便于在客户端进行请求时查找对应的处理方法,以及动态地进行更新和删除。
- 消息处理,主要涉及获取上下文,即HttpContext接着上一回没有处理完毕的情况接着处理,如果获取到了完整的上下文,就进行业务处理,否则就进行错误页面的填充,最后组织Http协议正文响应发送给客户端。
尾序
本项目基本的功能就介绍完毕了,主要包含服务器和应用层两大模块,详细的子模块上述内容都有图解,希望对大家有所帮助,若有误请在评论区详细说明,我是舜华,期待与你的下一次相遇!