本文对应代码:
district10/CrossOS: Cross OS Communication,Qt4 程序,已经在 Windows 和 Linux 上测试通过。
title: 封装示意图(忽略了界面同步部分)
主UI->主Wrapper: 所有 UI 操作\n由 Wrapper 接管
note right of 主Wrapper: 本机执行?\n是
主Wrapper->主Class: 直接调用
主Class->主Wrapper: 捕获结果
主Wrapper->主UI: 在界面上有所反映
note right of 主Wrapper: 本机执行?\n否
主Wrapper->主Socket: 把调用函数和参数\n传递到另一台电脑
主Socket->从Socket: TCP 通讯\n程序运行时\n已经建立连接
从Socket->从Wrapper: 还原函数和参数,\n调用 Wrapper
从Wrapper->从Class: 直接调用
从Class->从Wrapper: 捕捉结果
note left of 从Wrapper: 异地反映?\n是\n(序列图里忽略\n了本地反映)
从Wrapper->从Socket: 回传
从Socket->主Socket:
主Socket->主Wrapper: 解析给 Wrapper
主Wrapper->主UI: 在界面上有所反映
程序只有一个,运行在两个电脑上,一个为 Master,一个为 Slave,TCP 连接建立后。操作 Master 的 UI,执行可能在
对 UI 而言,无所谓执行发生在哪台电脑(实现细节被 Wrappers 屏蔽)。
Wrapper 主要做了两件事:
Wrapper 和 Class 有几乎相同的调用接口,所以原来在单电脑上的程序只要简单修改即可。
对程序的要求:
现在两者连接上后,界面发生变化(一些按钮可以使用,一些按钮不再能使用)。现在点击下方大按钮,可以同步两者的颜色(在红和绿直接切换)。
可以看到,
同时,界面是同步的。下面介绍如何将原有类改善,实现跨主机通讯、UI 同步。
先明确几个概念,
这里的修改指南指导了如何在 Class 和 UI 之间插入一个封装
,在不改变(几乎)原有类的基础上实现跨主机通信。
比如从 ui->pushButton->setText( class->getString() )
改起。
首先,在 QString Class::getString( args )
中 emit 一个信号:
emit wrp_getString( returnValue ) // 要提前定义(signals: void wrp_getString( QString text );)
然后参照 Class 定义一个 Wrapper,提供 void Wrapper::getString( args )
函数(几乎和原有类 Class 一模一样),
把 Class 的信号和 Wrapper 的槽连接:
connect( class, SIGNAL(wrp_getString(QString)),
wrapper, SLOT(onWrp_getString(QString)) );
Wrapper 构造的时候会传入 Class 类实体指针,如果 Class 在本机运行,Wrapper::getString( args )
会直接调用 Class::getString( args )
,如果 Class 在异地运行,经过判断 Wrapper::getString( args )
会把 函数名 ID
和参数 args
传到另一台电脑,由另一台电脑的对应的 Wrapper 的 Class 实体执行 Class::getString( args )
。
不管哪个电脑上执行了 Class 函数,信号都会被那个电脑的 Wrapper 捕获,然后 emit 一个同样的信号,并让另一台电脑 emit 这个信号。即 Wrapper::getString( args )
的大致逻辑为:
// Class 函数对应的 Wrapper 函数
void Wrapper::getString( args )
{
if ( Class registered on THIS_COMPUTER )
{
// 本机执行
this->class->getString();
}
else
{
// 异地执行(把函数名和参数传到另一台电脑)
send `Class::getString' + `args' to the other computer
tell THE_OTHER_COMPUTER to run: class->getString()
}
}
// 信号所连接的槽
void onWrp_getString( QString returnValue )
{
// 本机释放信号
emit wrp_getString( returnValue );
// 让异地电脑释放信号
tell THE_OTHER_COMPUTER to: emit Wrapper::wrp_getString( returnValue )
}
现在,两台电脑上,有一台电脑执行了操作(可能是你通过 UI 交互的那一台,或者是另一台),释放了两个同样的信号,只要把这个信号绑定到各自的 UI,界面就同步了:
connect( wrapper, SIGNAL(wrp_getString(QString)),
ui, SLOT(onWrp_getString(QString)) );
最后 UI 得到这个信号,在界面上有所反映:
void Ui::onWrp_getString( QString returnValue )
{
ui->pushButton->setText( returnValue );
}
现在,UI 不再直接调用 Class,而是调用 Wrapper 的函数,
需把原来的 ui->pushButton->setText( class->getString() )
换成 wrapper->getString()
。
总结一下,原来的一句之间的 setText
,变成了间接的:
Class
函数Class
emit 信号给 Wrapper
Wrapper
在两台电脑 emit 信号给 UI
UI
得到信号,同步更新界面Bundle
所有的 Wrappers 和 UI 都在一个全局的 Bundle 中绑定。(源码位于 Src/Wrappers/Wrappers.h
)
/*
* 获得实例
*/
Bundle::getInstance(); // 获得唯一的、静态的一个实例,包含了所有的大的类实例,UIs,Wrappers(Classes)
/*
* 本机是高性能平台(Master)还是工控机(Slave)?
*/
// who am I?
Bundle::whoAmI(); // 我是谁?MASTER 还是 SLAVE?
// 可以进行判断
if ( Bundle::whoAmI() == MASTER ) {
qDebug() << "我是高性能平台(Master)";
}
/*
* Bundle 的东西有
*/
// 主要模块的“封装”
Bundle::getInstance()->lms // LMS Wrapper
Bundle::getInstance()->lmsAgent // Wrapper 用到的界面
Bundle::getInstance()->server // 网络通信接口
Bundle::getInstance()->client // 网络通信接口
...
// 也可以获得主要模块的“类实体”(不推荐直接调用)
Bundle::getInstance()->lms->kernel // LMSReader 实体,可能为 NULL(如果不在本机注册)
...
/*
* Bundle 的一些函数
*/
// 向另一台电脑发数据
Bundle::getInstance()->send( const QByteArray &msg ); // 发送数据到另一台电脑
// 数据需要满足一定的格式,可通过一个 Moderator(“翻译”)静态类/函数进行转化,如:
// 要在另一台电脑上运行 LMSReader::genNewPath( "D://tmp/" ); (LMS 注册在那台电脑上)
Bundle::getInstance()->send( Moderator::lms_genNewPath( "D://tmp/" ) );
类的注册
很简单,修改 Moderator 的一个 HashMap 即可:
// Master: 高性能平台
// Slave : 工控机
wss.insert( BCD::TYPE_LMS, SLAVE );
wss.insert( BCD::TYPE_MCU, SLAVE );
wss.insert( BCD::TYPE_UR, SLAVE );
wss.insert( BCD::TYPE_ARM, SLAVE );
wss.insert( BCD::TYPE_SP20000C, MASTER );
wss.insert( BCD::TYPE_MULTIPLICATION_ON_SLAVE, SLAVE );
wss.insert( BCD::TYPE_ADDITION_ON_MASTER, MASTER );
How to Wrapping (live example)
UR0 封装了 URController,源码位于 Src/Wrappers/UR0.h
。
class UR0 : public QObject
{
Q_OBJECT
public:
URController *kernel; // kernel 变量为封装的对象
private:
bool doItYourself;
public:
UR0( URController *ur0 = NULL );
构造函数基本如下:
UR0::UR0( URController *ur0 /* = NULL */ )
{
// 类执行实体
kernel = ur0;
// 判断是否在本机执行
doItYourself = Moderator::wss.value( BCD::TYPE_UR ) == Bundle::whoAmI();
/*
* 说明:
* Moderator::wss: 记录了所有模块的注册信息(在高性能执行,还是在工控机),
* 新加的模块也要去注册,源码在 `Src/Utils/moderator.cpp'
* Bundle::whoAmI(): 本机是高性能还是工控机?
*/
// 如挂在本机执行,但执行实体为 NULL,说明没有正确初始化
if ( doItYourself && NULL == kernel ) {
Logger::log() << "Fatal Error";
exit( EXIT_FAILURE );
}
// 连接“实体”原有的信号到“封装”的槽,重新 emit 信号 & 分发到两台电脑
connect ( kernel, SIGNAL(wrp_getLocalAddress(QString)),
this, SLOT(onWrp_getLocalAddress(QString)) );
}
这个 wrp_getLocalAddress(QString)
信号是对 QString URControllor::getLocalAddress()
的封装,在保持原有类返回值的情况下,在函数内部 emit 信号,这里连接后被 UR0 捕捉,重新发射信号(在两台电脑):
void UR0::onWrp_getLocalAddress( QString addr )
{
// 在本机 emit 信号(本机界面可以捕获)
emit wrp_getLocalAddress( addr );
// 在异地 emit 信号(异地界面可以捕获)
Bundle::send( Moderator::sig_ur_wrp_getLocalAddress( addr ) );
}
Bundle::send
会把函数和参数传到另一台电脑,再次发射信号。
所有的 Moderator 的函数(如这里的 Moderator::sig_ur_wrp_getLocalAddress( QString )
)要现在 Moderator 里指定一个函数 ID(用于区分不同函数),ID 命名规则为:模块名__FUNCTION_NAME__TYPE1_TYPE2
举例:
// ur 模块,genNewPath 函数,QString 类型参数
UR__GEN_NEW_PATH__QSTRING
// ur 模块,genNewPath 函数,无类型参数也要显式地表明 VOID
UR__GEN_NEW_PATH__VOID
// 信号的模块名为 SIG_模块名__函_数_名__参_数_名,如
SIG_UR__WRP_GET_LOCAL_ADDRESS__QSTRING
// lms 模块,setFrequencyAngleresolution 函数,参数为 int、double
LMS__SET_FREQUENCY_ANGLERESOLUTION__INT_DOUBLE
// 上面的函数 ID 对应的函数依次为:
//
// static QByteArray ur_genNewPath( const QString &path );
// static QByteArray ur_genNewPath( )
// static QByteArray sigUR_wrp_getLocalAddress( const QString &addr )
// static QByteArray lms_setFrequencyAngleresolution( const int i, const double &d )
指定了函数 ID 后要把对应的函数和参数序列化为一串字节,需要定义一个静态函数,返回值为 QByteArray,比如上面的 UR__GEN_NEW_PATH__QSTRING
,对应的函数为:
// .h 文件中申明
static QByteArray ur_genNewPath( const QString &path );
// .cpp 文件中实现
QByteArray Moderator::ur_genNewPath( const QString &path )
{
TX_OUT << (int)UR__GEN_NEW_PATH__QSTRING // 传入函数 ID,强制转化为 int 型
<< path; // 传入调用的参数
return tx; // 这里的宏 TX_OUT 和 return tx 不必深究
}
这样就能顺利地用 Bundle::send( Moderator::ur_genNewPath( path ) )
把信息传递到另一台电脑。
不过,还得写解析部分。对收到的网络数据,要分析是要调用什么函数,还要还原参数,这部分在 void Moderator::dispatch( QByteArray &msg )
函数中定义,比如上面的 Moderator::ur_genNewPath( const QString &path )
在 dispatch 函数中就是:
// 已经提取了函数 ID 到 flag
// 如果函数 ID 为 UR__GEN_NEW_PATH__QSTRING
else if ( UR__GEN_NEW_PATH__QSTRING == flag )
{
// 参数为 QString,那就定义一个 QString
QString path;
// 从网络中取出 QString 类型的路径
in >> path;
// 让本地的“类执行”实体去调用这个函数
Bundle::getInstance()->ur->genNewPath( path );
/*
* 补充说明:
* 这里也可直接调用 `Bundle::getInstance()->ur->kernel->genNewPath( path )'
* 但并不推荐这样做(Wrapper 为判断调用是否合法,kernel 不会)。
*/
}
然后,把模块加到 Bundle 里,比如 UR,则到 Bundle
类中加入变量:
// 加入变量
class Bundle
{
...
UR0 *ur;
...
};
// 初始化,在
Bundle Bundle::initInstance( )
{
Bundle bundle;
...
bundle.ur = NULL;
...
return bundle;
}
// 在 main.cpp 中初始化(参考 `Src/MasterSlave/main.cpp')
Bundle::getInstance()->ur = Bundle::whoAmI() == Moderator::wss.value( BCD::TYPE_UR )
? new UR0( new URController )
: new UR0;
封装流程完毕。总结,需要做的事情有:
定义函数 ID
、序列化函数
、解析和 dispatch