本程序为基于C/S的网络聊天室系统,使用Linux网络编程作为服务器,使用QT编程作为客户端。
客户端通过输入IP地址、端口号、Email、聊天名称、聊天组号连接到服务器,用户通过客户端发送消息,同时接收来自相同组其他客户端发送的消息,获取当前在线用户信息,通知新用户的上线和用户的下线,实现群聊功能。
服务器负责管理用户的连接、发送消息与退出,有新用户建立连接时,记录新用户信息,并向同组其他客户端广播;用户退出时,清除用户信息,并向同组其他客户端广播离开信息;当有用户发送消息时,向同组其他客户端广播。
服务器主要功能有:管理连接的客户端、接收客户端发送数据、向客户端转发数据、向客户端发送数据。
用例图如下所示:
图1服务器用例图
服务器接收客户端数据时序图如下所示:
图3服务器接收客户端数据时序图
服务器非功能需求
高性能
作为C/S架构的聊天程序,首要解决高并发需求,服务器可以同时相应多个客户端,并且不会因为某个客户端而阻塞,具有较高的吞吐量和较低的相应时延,满足实时聊天的需求。
此系统需要选取合适的网络开发模型,网络IO模型,可以满足高并发高性能需求,代码实现方面,需选择高效的数据结构与算法,对系统性能进行全面提升。
可修改性可读性代码实现方面应满足可修改性与可读性,便于以后进行维护及扩展。
系统应满足可读性,代码风格统一,符合编程语言规范,符合编程语言惯用法,命名规范、明了易懂,文档齐全,具有良好的注释,代码结构规整,逻辑清晰,符合“高内聚-低耦合”原则。
系统应满足模块化,系统应是多个模块组合,每个模块具有较高的内聚性,每个模块都便于测试,模块间应具有较低的耦合。
系统应满足可重用性,对于公共的函数、模块、组件将其提出出来进行封装,供未来的重用,节省开发成本,提高开发效率。
系统应满足可维护性,可以高效的对系统进行更改、升级。
可测试性
可以较为简便的对系统进行测试,快速暴露于发现系统的错误,并进行修改。
容易编写桩程序替换底层模块(如数据库等)进行测试。
可以方便的进行白盒测试、黑盒测试、性能测试、安全性测试等。
可扩展性
系统能够满足相应的纵向扩展与横向扩展。
可以通过在一个计算节点中增加资源或更好利用资源进行纵向扩展,如选择合适的并发方式与模型、满足高性能等。
可以通过增加更能多的计算节点进行扩展,如将长连接与短连接分布在不同服务器上,将数据库分离开来,使用RPC进行扩展等。
客户端可跨平台,可方便修改通信协议,增加新功能。
客户端功能需求
客户端用例图如下所示:
图4客户端用例图
事件流:
备选流:
前置条件:
无
后置条件:
进入到聊天界面进行操作
接收消息用例文本:
若JSON数据解析失败,用例结束
客户端成功连接服务器
客户端可跨平台,可方便进行功能扩充。
架构
服务器初始架构
如下图所示,整体为C/S架构,多个客户端与服务器通信,所有客户端仅依赖一个服务器,聊天服务器承担责任过重,具有较大的性能压力。
通信协议采用字符串拼接形式,形如code:name:data,优点是简单,体积小,缺点是可读性差、扩展性差,若需要增加新字段,需要大量修改程序。
图5服务器初始架构
网络通信采用原始的selectI/O多路复用方式,虽较好解决了并发性问题,但实现起来较为臃肿,且与业务代码耦合严重,难以修改维护。
图5服务器初始代码结构
代码没有清晰的模块,一个文件承载了过多功能,没有较多体现分而治之思想,且代码存在注释缺失,对可修改可读性造成了更大的损害。
优化后服务器架构
图6当前服务器架构
支持多种客户端,客户端与服务器使用TCP长连接进行消息推送,获取数据信息时使用TCP短连接,降低性能压力,TCP长连接可和TCP短连接分离为不同服务器,TCP短连接使用HTTP协议,进行横向扩展。
客户端与服务器使用JSON格式传输数据,方便扩展,如可以容易的进行客户端跨平台扩展,增加修改JSON格式字段可很少或不修改现有代码,增加新功能。
数据库使用redis内存数据库,具有很高的性能,用于存储各聊天室基本信息,聊天数据(使用过期策略清理聊天数据),且可独立为一高性能服务器,提供高效的数据访问能力。
网络连接层次使用基于IO多路复用、Reactor模式的异步事件处理库—Libevent.
也即将网络连接层更改为事件驱动架构,Libevent提供一个主事件循环,监听网络IO等事件,在检测到事件时触发具有特定参数的回调函数。通过事件驱动,将网络IO操作与业务层可以较好的解耦。
此外,使用现成的成熟的网络库,可以提高开发效率,提高网络并发性能,编写系统时可专注于业务代码,代码更易于维护、修改。
图7服务器采用分层架构
接入层:使用libevent事件驱动机制,负责管理与客户端的长连接,接收来自客户端数据,向各客户端发送数据。此层保存接入层客户端的标识符sockfd和业务层客户端标识符email的映射,使得下层不用考虑接入层的特有数据。
业务层:根据接入层传来的email和信息,解析JSON,处理具体的业务,对于群聊业务来说,根据emial所在的groupid将消息转发给相应组员。上层接入层通过接口返回值处理相应的接入层操作。
DAO层:用于访问redis数据库,进行具体的数据库访问操作,向上层返回数据访问接口。
redis数据库层:存储用户会话信息,此系统中暂时使用内存中的map代替,体现了分层的好处—可测试性,基本数据结构为:
Key:email,Value:[email,name,groupid]客户端架构
QT提供了一些设计优秀的结构、行为机制,可以使程序具有良好的架构。如QT将界面与业务逻辑相分离,可以独立方便修改界面与业务逻辑,而不用做过多修改;使用信号/槽机制来完成组件间的通信,是事件驱动编程的一种独特实现方式,很好的解除了各个组件间的耦合,使得客户端开发与维护变得简单。
客户端发向服务器:
服务器发向客户端:
代码
服务器—接入层
接入客户端类定义如下所示,对于能使用const的场合,均使用const修饰,作为一种安全保证和标识。传递函数参数时,尽量使用引用,而不是值拷贝,可以得到较好的效率。使用容器时使用STL模板容器,拥有很好的性能,特别是使用map极大加快了查询的速度。
因为网络库使用了基于C语言的Libevent库,因此需要将面向过程的LibeventAPI与面向对象的类结合起来。
使用命名空间来管理类,增加系统的模块化。
务层设置),之后将数据转交给业务层,此次通过引用来是业务层修改数据,业务层处理后返回:处理的是什么类型的数据、应发送给原发送方的数据、应转发给相应用户的数据。接入层再根据返回值调用相应处理函数,进行数据转发等操作。
此处使用了C++11语法—auto关键字,可自动推断变量类型。对于多重条件,使用switch,更加清晰地处理各种情况。程序中未显示出现数字,即魔数,均使用常量来代替。
服务层接口如下所示,仅向上层—接入层提供两个接口,其中data_dispatch接口承担了多个相似功能,首先传入上层发送方的email和接收到的数据,再解析数据,根据数据格式进行相应处理,如果需要返回给上层发送方、多个接收者,则修改引用的参数即可。offline接口负责用户下线的处理。
此接口类使用了虚析构函数,使用实现dao接口的对象来操纵数据库。此外,使用conststatic定义了大量常量便于使用。
业务层具体实现类面向接口编程,满足设计模式中合成复用原则—细节应依赖抽象,抽象不应依赖细节。
DAO层用于与数据库交互,接口如下所示,接口设计使用了C++纯虚函数语法,符合语言规范。
namespacedao{classIOnlineUserInfoDao{public:virtualvoidadd_new_user(std::stringemail,std::stringname,intgroupid)=0;virtualbooldel_user_by_email(std::stringemail)=0;virtualstd::vector
voidOnlineUserInfoDaoImpl::add_new_user(std::stringemail,std::stringname,intgroupid){ClientInfoc;email=email;name=name;groupid=groupid;map_clients_info[email]=c;}boolOnlineUserInfoDaoImpl::del_user_by_email(std::stringemail){autoit=map_clients_info.find(email);if(it!=map_clients_info.end()){map_clients_info.erase(it);returntrue;}returnfalse;std::vector
下面为解析客户端发来JSON数据的类的构造函数,传入JSON数据,将字段数组存在成员变量中。解析JSON库时使用rapidjson开源JSON解析库。
以下为ChatTcpSocket类的两个函数,startRecvMsg函数用于连接接收到数据时的信号,接收到信号时调用transferMsg函数,此时transferMsg再发送接收到数据的信号。
//开始接收消息voidChatTcpSocket::startRecvMsg(){connect(&socket,&QTcpSocket::readyRead,this,&ChatTcpSocket::transferMsg);}//接收到消息时发送信号voidChatTcpSocket::transferMsg(){QByteArraynetdata=socket.readLine();emitrecvMsg(QString(netdata));以下为聊天窗口类ChatWindow的槽函数,当ChatTcpSocket类接收到数据并调用transferMsg发送后,ChatWindow中的recvMsg函数进行接收处理。解析服务器发送来的JSON数据,根据不同CODE字段调用不同处理函数进行处理。
voidChatWindow::recvMsg(constQString&data){serverjson::ParseServerJsonp(data);switch(p.getCode()){caseserverjson::USER_ONLINE_CODE:newUserOnline(p.getEmail(),p.getName());break;caseserverjson::USER_OFFLINE_CODE:if(membersMap.contains(p.getEmail())){userOffline(p.getEmail(),membersMap[p.getEmail()]);}break;caseserverjson::USER_MSG_CODE:if(membersMap.contains(p.getEmail())){newUserMsg(p.getEmail(),membersMap[p.getEmail()],getMsg(),p.getTime());break;caseserverjson::GROUP_MEMBER_LIST_CODE:disGroupMembersList(p.getMembers());break;}以下为客户端发送在线信息和聊天信息函数,将数据封装为JSON数据进行发送。//发送上线信息
voidChatTcpSocket::sendOnline(){clientjson::Onlineon(clientjson::ONLINE_CODE,email,name,groupid);sendMsgToServer(on.toJson());}//用户发送信息voidChatTcpSocket::sendMsg(constQString&email,constQString&msg){clientjson::SendMsgm(clientjson::SENDMSG_CODE,email,msg);sendMsgToServer(m.toJson());}质量评价高性能
此系统中,使用C语言的网络库Libevent,为轻量级、基于异步事件驱动的开源高性能网络库。底层使用主流的并发解决方案—I/O多路复用。因此借助Libevent库可以实现高并发能力的C/S程序。实际测试中本系统具有较好的并发能力,允许多个客户端向服务器同时发送消息,并且具有较低的时延与响应速度。
代码层面,使用高效的算法和数据结构,如在进行大量查询操作时,使用map数据结构替换vector等线性结构。
数据库使用基于内存的Redis数据库存储在线用户信息,Redis数据库具有优秀的性能表现。
可读可修改性
系统具有一定的可读性,代码风格统一,符合编程语言规范,符合编程语言惯用法。对函数、类、模块、文件的命名较为规范,力求明了易懂。对函数、代码关键部分均添加了注释,代码结构规整,逻辑清晰,遵从“高内聚—低耦合原则”。
架构上使用分层,每一层实现较为清晰,相对独立,向上层提供接口,带来了良好的可读与可修改性。
系统使用模块化设计,使用C++的命名空间将类分为各个不同模块。每个模块具有较高的内聚性,便于修改与测试,模块间具有较低的耦合性。
系统尽可能将公共函数、模块提取出来进行封装,以供重用,节省开发成本,提升开发效率。
系统使用标准的分层架构,带来了良好的可测试性。因为各层间通过接口来调用,可以单独测试每一层的接口。而且对于下层模块,可以通过桩程序代替测试,本系统中为测试高层模块,将最底层的数据库使用map变量代替,不用修改高层模块,即完成了高层模块的测试,具有较好的可测试性。
系统使用异步技术来支持,具有良好的性能,也即支持良好的垂直扩展,可以通过在一个计算节点中增加资源或更好利用资源进行纵向扩展。
系统可将长连接与短连接分离在不同服务器,短连接使用HTTP协议,因为采用了分层结构,数据库也可以较为容易进行分离,满足水平扩展。
本系统设计过程中,以设计模式基本思想为指导,如使用接口满足开闭原则和依赖倒转原则,每个类满足单一职责原则,满足高内聚—低耦合原则。设计接口时,尽可能满足接口隔离原则。
本次系统我选择了基于C/S架构的聊天室系统,一是因为C/S作为一个基础性架构,应用广泛,可以很好增强架构设计能力,二是因为之前做过类似系统,可以对其进行重构,增加新功能,认识到架构优化的重要意义。
优化过程中,我深切感受到了一个设计良好的架构带来的诸多好处。
面对着当初设计不友好的代码,借助于软件体系结构这门课程所学的架构知识,我开始重新设计并编写聊天室系统。在众多的架构模式中,我综合选择了最为基础性架构—分层架构,掌握好分层架构才能灵活运用其他架构模式。我参照最著名的分层设计TCP/IP协议,尝试着将我的系统分为四个层次—接入层、服务层、DAO层、数据库。每一层次向上层提供相应的接口,每一层次相对独立,只为紧接着上层服务,使用紧邻的下层提供的服务,而且不存在跨层次调用。这样,整个系统就有了大体上较为清晰的结构,我也体会到了分层带来的好处:当编写、修改某一功能时,我可以专注与该功能所在层次,而不用过多修改其他层次,更不会出现之前牵一发而动全身的现象;当我在进行测试时,我也要使用桩程序,替换掉某一层,单独测试某一层,如我使用内存中的map数据结构替换掉redis数据库,程序也具有相同的效果。
总之,未来在设计系统时,要充分考虑架构思想,设计模式思想,不断在实践中增强自己的架构运用能力。