C++服务器设计(零):总体设计
这个系列把毕业论文的部分贴了出来,以作保存留念。整个系列分为三大部分,其中第一章到第三章是介绍服务器的系统层设计,设计思路参考了libevent和muduo等开源代码的实现;第四章到第六章是介绍服务器的服务层设计,设计思路参考了自己的Khala实现;第七章介绍了如何利用该服务器框架实现一款类似于QQ的聊天系统。全文主要参考了陈硕的《Linux多线程服务端编程》、《Unix网络编程卷1》。
系统简介
本系统是用C++设计实现的TCP网络服务器框架。该系统底层I/O部分采用基于Reactor模式的非阻塞模型,线程部分采用event loop+ 线程池模型。系统的服务层具有超时检测管理、多类型设备和多事件消息管理,以及连接生命周期管理的功能。同时整个系统通过封装实现了网络实现与业务逻辑相分离的多线程网络框架。
该框架面向网络业务开发者,对整个网络服务端的开发过程提供了更高层的封装,能够大大降低业务开发难度。通过该框架,开发者无需关心底层网络I/O及多线程下并发连接的实现,只需根据具体业务所属设备类型编写业务逻辑作为消息响应事件,并可以选择使用服务层提供的相关服务作为业务支持。
该框架适用于局域网内基于TCP长连接的业务场景。比如智能家居下的多设备管理,局域网内的多用户聊天工具等。在这些场景中,通过使用本系统框架,开发者只需编写少数业务相关的代码即可完成服务端功能的实现。
系统需求分析
本服务器系统主要从高效和易用两个方面作为设计的核心。
高效是指合理设计系统底层的I/O和线程模型,充分利用CPU和内存资源,达到尽可能的高连接和高并发的要求。此处只考虑服务器硬件性能的利用,并不考虑以太网带宽的限制。
易用是从两个方面出发进行考虑。第一是将网络细节与业务逻辑相分离,使该框架的开发者从底层琐碎细节中解脱出来,只需专注于业务逻辑的编写;第二是提供一层服务层,能为对于连接超时、多设备及生命周期支持等特定场合下有特殊需求的开发者提供服务支持。
本服务器系统的基本需求分析如下:
- l 环境需求:本系统只考虑在安全可控的局域网内部使用,网络数据采用明文传输,局域网内客户机可能会因为网络故障或死机等异常导致与服务器断开。但是并不考虑服务器可能被某些用户恶意攻击的场景。因此本系统并不为安全性做特别的增强。在公网环境上使用本服务器系统是不安全的。
- l 操作系统需求:由于服务器系统涉及大量与底层操作系统交互的过程,如I/O及网络的处理,而不同操作系统的系统调用并不完全相同。比如在I/O复用上,虽然大多数系统均提供了select作为系统调用,但是该调用有着O(n)的时间复杂度,效率低下。因此不同系统各自实现了不同的类似系统调用作为优化,比如Linux系统下提供了epoll,BSD系统下提供了kqueue,Windows下提供了IOCP。虽然可以通过适配器模式对这些系统特性进行封装,抽象出一层跨平台的统一接口,但这将大大增加整个设计实现的工作量,同时这也不是本系统设计的重点。因此本服务器系统只支持X86-64下的Linux,不考虑可移植性,不支持Windows等其他平台。
- l 网络需求:由于服务器系统涉及基于TCP/IP协议的网络编程过程,而在TCP/IP中又存在多种选择。在传输层中,存在TCP和UDP两种协议,其中TCP是可靠的字节流协议,而UDP是不可靠的数据报协议。由于本服务器系统大多运用于局域网内,且原生业务中并无类似音频和多媒体应用等大数据量的传输要求,同时由于对于连接的生命周期的设计将是系统设计的重点。因此我们优先采用基于可靠性和有序性、面向连接的TCP协议,并不支持UDP协议。同时由于开发时间及成本的关系,本系统只目前支持IPv4,不考虑兼容IPv6。
- l 性能需求:作为服务器系统应该能够充分利用CPU和内存资源。同时能够发挥多核处理器的效能,原生支持多核多线程,并保证线程安全,而不像libevent本身只支持单线程,需要后期进行多线程修改。
- l 数据传输需求:TCP协议是一个无边界的字节流协议,但读写数据处理是以一条完整消息为单位的。因此服务器需要能够区分消息边界,能够解决“目前收到的数据暂时不能构成一条完整消息”和“一次收到多条消息的数据”等情况。同时在多线程的环境下需要保证消息的正确和有序,即不能出现一条消息内混入另一条消息的数据,并且每条消息按照接收顺序等待被处理。同时以上细节不能暴露给用户,用户应该只需关注收到一条完整消息的处理,及每次发送出去一条完整的消息。
同时服务器框架提供了一些特定服务,可以为某些特殊业务的开发提供便利。开发者可以选择使用其中某些框架服务支持,也可以选择自己另行实现。这些服务相关的需求分析如下:
- l 超时检测:服务器应该能够检测长期未发送有效数据的空闲连接。这些连接很有可能是由于机器断电、网线故障或防火墙导致的的僵死连接。大量的僵死连接将占用服务器资源,影响服务器性能。因此当服务器检测到这些可能的僵死连接后,应该尝试通知这些连接,并最终断开这些僵死连接,释放它们占用的系统资源。
- l 多设备管理:不同于传统服务器仅对消息进行解析回复,而不对消息事件进行归类管理。本服务器系统应该能够管理不同连接客户的设备类型,并能够对来自不同类型设备的不同消息消息进行权限管理。即某个设备类型的连接无权限请求属于另一个设备类型的消息事件。而如果某个消息事件同时属于多个设备类型,每种设备类型的连接请求该消息事件的消息处理机制也不尽相同。用户可以为系统创建新的设备类型,为该设备类型注册不同的消息事件,并针对每种消息事件编写具体的业务处理代码。
- l 连接生命周期管理:每个连接的过程存在多个阶段,比如连接的建立,登录的验证,退出前的处理等。服务器为每个连接的不同阶段中预留了不同的接口,并保证这些接口内代码的线程安全。这些接口由不同设备类型进行管理,用户可以根据不同的设备类型重写这些接口,完成对不同设备类型的每个连接在不同阶段的下的操作管理。
系统总体架构
如图2-1所示,整个服务器框架由系统层、服务层和用户层组成,运行于Linux操作系统之上。其中系统层和服务层构成框架主体,并通过相关接口提供给用户层使用。用户层由框架的使用者依据具体业务进行实现。每层的具体介绍如下。
图2-1 服务框架软件架构图
系统层是整个服务器系统框架的核心。它与操作系统交互,通过I/O和线程等机制,保证了整个系统的正常高效运行,并提供了对底层系统及网络等细节的封装,保证了与上层具体业务逻辑的分离。
系统层由Reactor模式、多线程模型、连接对象和应用层I/O缓冲四个部分组成。在Reactor模式中,通过Linux下的epoll系统调用实现的非阻塞I/O机制,实现了对不同连接的I/O事件的管理,构成了系统层的主体。同时在多线程模型中,采用了event loop + 线程池机制[39],同时线程间通过任务队列的形式进行通信,并且保证对于每个连接的整个连接周期中都在一个线程中进行管理。在连接对象部分,对每个连接的描述符等信息和连接相关操作进行了封装处理。在应用层I/O缓冲中,通过缓冲机制,封装了非阻塞I/O下的数据收发处理,保证了数据收发下的完整性。
服务层是在系统层的基础上进行了进一步封装,并提供了更多的服务功能。理论上整个框架可以剥离服务层而单独在系统层上运行,但框架的易用性将大打折扣,同时业务逻辑也将很容易侵入到系统层的代码中。使用者可以根据具体业务需求选择使用服务层的某些功能,对不需要的服务进行关闭。因此在实际使用中应该基于服务层提供的接口进行开发工作。
服务层主要由连接超时管理、多设备类型管理和连接生命周期管理三个部分组成。
在连接超时管理中,服务层通过心跳机制对每个连接进行计时,客户端需要每隔一段时间向服务器发送有效数据,通知服务器连接仍然在线。当服务器检测到某个连接超时时,将会执行超时接口中实现的处理机制,并强制将该连接断开连接并移除系统。用户可以设置超时时间,并通过超时接口为不同设备制定不同的超时处理机制。
在多设备类型管理中,服务层提供了以设备类型为单位制定不同的消息事件,并为不同消息事件实现具体处理的机制。服务层默认实现了临时设备类型和登录设备类型这两种设备类型,并为其制定了和登录相关的消息事件及处理。
连接的生命周期管理主要是提供一组和连接的生命周期状态变化和错误处理相关的接口,并以设备类型为单位进行管理。这些生命周期状态包括连接的建立、登录、超时及退出等。用户可以为不同的设备类型的连接制定不同的生命周期接口实现,比如统计每一个新建连接信息,或对不同类型的登录连接进行验证等。
图2-2 用户创建新的设备类型
用户层位于整个服务器系统框架之上,由该框架的使用者根据具体的业务需求进行实现。使用者主要工作为根据连接类型创建新的设备类型对象,如图2-2所示,并为新的设备类型注册业务相关的消息事件和处理机制,并根据需求实现该设备类型对应连接的生命周期接口。
整个服务器框架设计中并不涉及磁盘文件I/O和数据库操作,而在具体服务端业务编程中通常会涉及数据库操作。如果需要连接使用数据库,服务端开发者应该在系统用户层自行实现或导入第三方的数据库操作类库,比如Mysql++库等,并通过该库的API实现与后台数据库的交互操作。