boost::asio 通讯服务器实践
1. 开发环境搭建
1.1. Asio准备
万事开头难。对于一个C++的陌生者,编译一个开源的代码并不是一件轻松愉快的事情。为使大家在审阅和检测本代码可使用性时没有必要花费太多时间和精力去编译Boost::Asio库,在此把我在编译boost库时不愉快的经历记录下来,供参考。使用Asio有两种方式,一种是直接使用Asio库,到网站:
https://sourceforge.net/projects/asio/files/asio/1.11.0%20%28Development%29/可以下载单独的Asio库,目前最新版本为1.11。另一种是使用boost.asio库,该库依赖于Boost.System库,因此需要把下载后的boost源码编译生成相应的库才能使用。Boost下载地址http://www.boost.org/,目前最新版本为1.61.0. 我下载的版本为1.61.0版本。下载后解压到E:\boost1610目录下.
1.2. Qt准备
到Qt官网上下载Qt。我电脑环境是联想TIANYI 100,4G内存,安装的windwos10笔记本开发,因此下载的是windows版本的Qt5.7版本,安装比较简单,按照提示实行“下一步”就可以。
1.3. 编译boost
编译boost比较麻烦一点,使用Qt自带的mingw编译boost步骤如下:
-
配置mingw的系统环境变量PATH。我把QT安装在C盘下,path值配置为:C:\Qt\Qt5.6.1\Tools\mingw492_32\bin。配置后再命令行中输入gcc -v 如果配置成功,则显示为:(配置完后,我试着在命令行中输入gcc -v,居然提示gcc不是内部命令。然后把电脑重起,再输入gcc -v,这次显示成功)。
-
下载boost后,解压到任意目录下我的解压目录为E:\boost1610;
-
打开cmd.exe,切换到E:\boost1610目录下,在命令行中实行bootstrap.bat gcc 生成b2.exe和bjam.exe。Bootstrap.bat 是在Windows下生成自己的bjam.exe 和b2.exe 的脚本。 在Linux下是bootstrap.sh. Bootstrap.bat gcc命令实行完成后,会在QT解压的目录下生成bjam.exe 和b2.exe这两个文件.
-
实行b2.exe开始编译。这个命令会生成一个bin.v2的文件夹,为下一步通过bjam命令提供链接。
-
在命令行窗口下实行bjam "toolset=gcc" install编译boost源代码。表示将编译结果默认保存到C:\boost目录下或者指定目录 bjam.exe “-sTools=mingw” --prefix=d:\boost install 或者 Bjam.exe --prefix=d:\boost toolset=gcc install 来编译boost。编译完成后会看到D:\boost文件夹下面有两个子目录 include 和lib。 实行这个命令过程非常耗时,慢慢等待。
1.4. 使用boost库
方法一:
1、选择将这两个目录拷贝到mingw目录下的include和lib目录中,或者将这两个目录的路径添加到系统路径中。
2、由于在windows下系统默认的命名方式为libboost-iostreams-mgw44-mt-1-61-0.a 可以改为libboost-iostreams.a. Mt 表示多线程,默认。
方法二:
建立Qt项目的时候,都会生成一个XXX.pro的文件,在该文件下增加如下代码:
#DEFINES += BOOST_THREAD_USE_LIB
INCLUDEPATH += E:\Boost\include\boost-1_61
#DEPENDPATH += E:\Boost\include\boost-1_61
LIBS += E:\Boost\lib\libboost_system-mgw49-mt-1_61.a
LIBS += -lpthread libwsock32 libws2_32
其中最后一行
LIBS += -lpthread libwsock32 libws2_32表示要用到windows的完成端口模型下的winsocket库。
建议使用方法二。
到此,开发环境基本搭建完成。下面开始Boost.Asio设计开发服务器软件。
2. Asio通讯服务器设计
2.1. 设计指标
-
跨平台,一套代码在不修改原代码基础上可运行于windows和Linux系统;
-
运行于Linux系统上时采用EPOLL模型,运行于Windows上时采用完成端口模型(IOCP),且并发连接量尽可能接近操作系统IO模型提供的理论并发量;
-
能接入任何通过TCP方式连接的客户端,不受任何具体协议数据包的限制;
-
客户端发送数据的时候,要求服务器应答时,服务器应答客户端时间应尽可能小,最低要求小于100毫秒。
-
有效管理服务器与客户端连接的生命周期
-
基础通讯与业务逻辑解耦。
2.2. 概要设计
由于跨平台要求,目前C++适合跨平台的第三方库有libevent、asio库等,我们决定采用asio库实现。服务程序由通讯服务类、连接类、连接管理类、业务接口模板类等四大类组成。为适应不同的客户端接入,服务程序不解析任何数据包,只接受客户端连接和接收客户端发送过来的数据。客户端连接成功后,调用连接器管理对象保存连接,同时向操作系统投递异步读事件,当客户端发送的数据到达服务器,调用连接器中的读事件读取数据,通过业务接口将数据传递到业务接口,数据的处理由业务接口进一步处理。客户端需要服务器应答的,由业务接口根据客户端与服务器端通讯协议格式构造应答数据包后,由连接器转发到客户端。
通讯服务类(TTcpServer):提供基础的端口监听监听,不处理任何业务逻辑,以达到通讯服务类通用的目的。对用户暴露接口有:
-
SetIpAddress设置监听的IP地址和端口。
-
SetRecycleSoketTime设置回收掉线的Socket时间间隔。
-
Start启动方法。启动方法允许用户使用单线程运行和多线程运行,也可以在启动的同时注册自己的业务对象。线程数量由通讯服务类由运行服务的物理机CPU核心数决定,线程数量最大为CPU*2-1.不允许用户自行开辟线程。
-
RegisterBussiness注册业务对象方法。通过RegisterBussiness方法注册用户通过实现业务接口类完成自己的业务逻辑。
连接器类(TConnector):连接器类是通讯服务程序的核心,客户端与服务端连接成功后,会产生一个连接会话通道,该通道我们称为连接器,客户通过连接器发送数据到服务端,服务端通过连接器发送数据到客户端。每个连接器有一个唯一标识符ConnectorId,通过客户端唯一标识符ClientId建立与客户端的关联。用户可以通过查询连接器ConnectorId或者ClientId获取指定的连接器。连接器的主要作用就是读取数据或者下发数据到客户端,读取到的数据通过业务接口传递给业务处理类,由业务处理类处理数据。
连接器管理类:连接器管理类提供保存连接器的容器,作为通讯内部管理类,提供连接器插入到容器、关闭连接器、删除连接器、更改连接器客户端ID和连接器工作参数。
业务接口类:为用户提供业务处理模板。用户通过实行业务接口实现自己的业务逻辑。
流程描述:
2.3. 详细设计
曾经用Delphi写过windows完成端口模型的通讯代码,而C++,尚未入门,临时抱佛脚采用QT编辑器尝试编写本案例详细设计和具体实现,很多命名规则还保留有一些记忆中的Delphi风格。为方便专业级别的C++程序员审阅本案例,也因本人不喜欢阅读那些带下划线的代码,本详细设计命名规则解释如下:
-
所有的类名均用大写的T字母开头。
-
public类成员变量统一由大写的字母开头。成员变量由两个单词组成的,每个单词的首字母均采用大写。
-
Protect或者Private成员变量m+单词首字母命名。
-
Public方法名,只有一个单词的由单词的第一个字母大写,其他字母小写。 由2个单词组成的,每个单词第一个字母采用大写,其他用小写。
-
Protect或者private方法,均采用首个单词全部小写,其他单词的第一个字母大写。
-
除非不得已,本设计常量定义尽量采用const,不使用宏定义。
通讯服务类、连接器类、连接器管理类、业务逻辑接口,业务逻辑示例TDefeaultBussiness类详细设计及各对象之间的关系如下图所示。
2.3.1. 通讯服务器类(TTcpserver)
一、通讯服务器工作流程:
-
使用TTcpServer::TTcpServer()构造器生成一个TTcpserver对象,如TTcpserver tcpserver; 或者使用带ip地址和端口号参数的构造器TTcpserver::TTcpServer(string ipAddr,int port)生成一个TTcpServer对象。
-
如果还没设置监听的IP地址和端口的,调用SetIpAddress设置监听IP地址和端口。
-
调用 RegisterBussiness方法绑定用户自己实现的业务逻辑对象。
-
调用SetRecycleSocketTime方法设置回收断线的客户端连接,该步骤可选,如果用户不调用该方法,则默认10分钟回收一次。
-
调用Start方法启动服务,参数为True时将根据运行的物理服务器CPU核数产生2*CPU核数-1的线程数量运行通讯服务。
-
功能描述:
-
waitAccept方法:使用异步方式等待远程客户端连接,当有连接请求时。如果连接成功,调用连接成功回调函数来保存连接和投递异步读等待。
-
acceptHandle方法:是waitAccept方法的回调函数,连接成功后该方法被调用,保存连接器和投递异步读事件。 如果有异常,则把异常写到异常日志中。
-
Start方法:启动服务。首先调用waitAccept等待连接,然后调用连接器管理对象中的OpenTimers方法启动连接器回收掉线的连接器,最后调用采用线程方式启动mIoServer.run。
2.3.2. 连接器类
功能描述:
-
DoRead:投递异步读事件等待数据接收,如果读到客户端关闭的错误代码,则关闭连接。
-
ReadHandle:异步读事件回调函数,当读取到数据后,通过业务接口把数据传递到业务处理对象。再次调用DoRead投递读事件
-
DoWrite:投递异步写事件,由于什么时候在连接器上面进行写操作是由客户端或者应用协议决定的,投递写事件由业务逻辑接口调用。
-
writeHandle:异步写回调函数,通过该回调函数将写结果通知给业务层。
2.3.3. 连接器管理类
功能描述:
-
AddConnector方法:为连接器生成连接器ID,并把连接器对象保存到连接器容器中。工作流程:
首先中连接器ID回收站中获取可以利用的ID,如果没有,则通过增长方式建立一个ID。
-
其次调用容器的插入方法保存连接器对象。
-
GetSocket方法:通过连接器ID或者客户端ID获取连接器。
-
UpdataConnector方法:当客户端注册后,更新连接器的ClientId。由业务接口调用。
-
checkConnection方法:定时检查连接器容器中是否有掉线的连接器,并将掉线的连接器中删除掉线的连接器。
源代码:
/***********************************************
* 撰写时间:2016年8月12日
*C++ 等级:初级
*作者:
*/
#ifndef TCPSERVER_H
#define TCPSERVER_H
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/thread.hpp>
#include "connector.h"
#include "connectormanager.h"
#include "userbusiness.h"
#include <string>
using namespacestd;
class TTcpServer
{
public:
//提供默认的构造函数
TTcpServer();
//提供通过IP和端口构造服务类的构造函数
TTcpServer(conststringipAddress,shortport);
//~TTcpServer(){}