[关闭]
@gnat-xj 2015-11-17T02:17:16.000000Z 字数 7618 阅读 1937

封装说明(已经备份)

notes docs bcd

read online:


UI -- Wrapper(Class) -- TCP/IP -- Wrapper(Class)
Created with Raphaël 2.1.2封装示意图(忽略了界面同步部分)主UI主UI主Wrapper主Wrapper主Class主Class主Socket主Socket从Socket从Socket从Wrapper从Wrapper从Class从Class所有 UI 操作由 Wrapper 接管本机执行?直接调用捕获结果在界面上有所反映本机执行?把调用函数和参数传递到另一台电脑TCP 通讯程序运行时已经建立连接还原函数和参数,调用 Wrapper直接调用捕捉结果异地反映?(序列图里忽略了本地反映)回传解析给 Wrapper在界面上有所反映

程序只有一个,运行在两个电脑上,一个为 Master,一个为 Slave,TCP 连接建立后。操作 Master 的 UI,执行可能在

对 UI 而言,无所谓执行发生在哪台电脑(实现细节被 Wrappers 屏蔽)。

Wrapper 主要做了两件事:

  1. 判断执行应该在本机还是异地,并调用本地或异地的类执行实体
  2. 接受 Class 或者 Socket 传来的结果,并 emit 相应的信号(二传手)

Wrapper 和 Class 有几乎相同的调用接口,
所以原来在单电脑上的程序只要简单修改即可。

对程序的要求:

  1. 要避免直接获取 Class 的返回值(返回值难以跨电脑),而是采用 emit 信号的方式
  2. Class 的信号由 Wrapper 完全接管,重新发射信号(信号名称保持不变)。
  3. UI 的变化,都是通过接受 Wrapper 的信号。

新的预操作流程

主程序运行后,先要选择自己是 Master(高性能平台)还是 Slave(工控机):

高性能平台要先打开,并打开 TCP 网络服务器:(点击 Serve

再运行程序,不过这次选择 Slave,然后将工控机连接到高性能平台:(点击 Connect

现在两者连接上后,界面发生变化(一些按钮可以使用,一些按钮不再能使用)。现在点击下方大按钮,可以同步两者的颜色(在红和绿直接切换)。

关闭这两个界面会弹出一个 demo 界面,展示的如何同步两者的 UI 以及运算如何分布在两台电脑上,如图:

可以看到,

同时,界面是同步的。下面介绍如何将原有类改善,实现跨主机通讯、UI 同步。


具体的操作指南(如何修改类和 UI)

先明确几个概念,

类(Class)
实际的执行实体
包装(Wrapper)
封装了类,提供几乎一致的接口,屏蔽了跨主机的一些操作。
界面类(UI)
界面上,调用的虽然是 Wrappers,但和调用 Classes 的方式一致。

这里的修改指南指导了如何在 Class 和 UI 之间插入一个封装,在不改变(几乎)原有类的基础上实现跨主机通信。

--------------------------------H--O--W------------------T--O-------------------?--?--?-----------------------------

比如从 ui->pushButton->setText( class->getString() ) 改起。

首先,在 QString Class::getString( args ) 中 emit 一个信号:

  1. emit wrp_getString( returnValue ) // 要提前定义(signals: void wrp_getString( QString text );)

然后参照 Class 定义一个 Wrapper,提供 void Wrapper::getString( args ) 函数(几乎和原有类 Class 一模一样),

把 Class 的信号和 Wrapper 的槽连接:

  1. connect( class, SIGNAL(wrp_getString(QString)),
  2. 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 ) 的大致逻辑为:

  1. // Class 函数对应的 Wrapper 函数
  2. void Wrapper::getString( args )
  3. {
  4. if ( Class registered on THIS_COMPUTER )
  5. {
  6. // 本机执行
  7. this->class->getString();
  8. }
  9. else
  10. {
  11. // 异地执行(把函数名和参数传到另一台电脑)
  12. send `Class::getString' + `args' to the other computer
  13. tell THE_OTHER_COMPUTER to run: class->getString()
  14. }
  15. }
  16. // 信号所连接的槽
  17. void onWrp_getString( QString returnValue )
  18. {
  19. // 本机释放信号
  20. emit wrp_getString( returnValue );
  21. // 让异地电脑释放信号
  22. tell THE_OTHER_COMPUTER to: emit Wrapper::wrp_getString( returnValue )
  23. }

现在,两台电脑上,有一台电脑执行了操作(可能是你通过 UI 交互的那一台,或者是另一台),
释放了两个同样的信号,只要把这个信号绑定到各自的 UI,界面就同步了:

  1. connect( wrapper, SIGNAL(wrp_getString(QString)),
  2. ui, SLOT(onWrp_getString(QString)) );

最后 UI 得到这个信号,在界面上有所反映:

  1. void Ui::onWrp_getString( QString returnValue )
  2. {
  3. ui->pushButton->setText( returnValue );
  4. }

现在,

UI 不再直接调用 Class,而是调用 Wrapper 的函数,

需把原来的 ui->pushButton->setText( class->getString() ) 换成 wrapper->getString()

总结一下,原来的一句之间的 setText,变成了间接的:

注意:

直接调用 private 成员也不可以了。
--------------------------------E-----N-----D-------------------------------------------------------

其他 1:Bundle

所有的 Wrappers 和 UI 都在一个全局的 Bundle 中绑定。(源码位于 Src/Wrappers/Wrappers.h

  1. /*
  2. * 获得实例
  3. */
  4. Bundle::getInstance(); // 获得唯一的、静态的一个实例,包含了所有的大的类实例,UIs,Wrappers(Classes)
  5. /*
  6. * 本机是高性能平台(Master)还是工控机(Slave)?
  7. */
  8. // who am I?
  9. Bundle::whoAmI(); // 我是谁?MASTER 还是 SLAVE?
  10. // 可以进行判断
  11. if ( Bundle::whoAmI() == MASTER ) {
  12. qDebug() << "我是高性能平台(Master)";
  13. }
  14. /*
  15. * Bundle 的东西有
  16. */
  17. // 主要模块的“封装”
  18. Bundle::getInstance()->lms // LMS Wrapper
  19. Bundle::getInstance()->lmsAgent // Wrapper 用到的界面
  20. Bundle::getInstance()->server // 网络通信接口
  21. Bundle::getInstance()->client // 网络通信接口
  22. ...
  23. // 也可以获得主要模块的“类实体”(不推荐直接调用)
  24. Bundle::getInstance()->lms->kernel // LMSReader 实体,可能为 NULL(如果不在本机注册)
  25. ...
  26. /*
  27. * Bundle 的一些函数
  28. */
  29. // 向另一台电脑发数据
  30. Bundle::getInstance()->send( const QByteArray &msg ); // 发送数据到另一台电脑
  31. // 数据需要满足一定的格式,可通过一个 Moderator(“翻译”)静态类/函数进行转化,如:
  32. // 要在另一台电脑上运行 LMSReader::genNewPath( "D://tmp/" ); (LMS 注册在那台电脑上)
  33. Bundle::getInstance()->send( Moderator::lms_genNewPath( "D://tmp/" ) );

其他 2:TODO

其他 3: Bug fixes

其他 4: 类的注册

很简单,修改 Moderator 的一个 HashMap 即可:

  1. // Master: 高性能平台
  2. // Slave : 工控机
  3. wss.insert( BCD::TYPE_LMS, SLAVE );
  4. wss.insert( BCD::TYPE_MCU, SLAVE );
  5. wss.insert( BCD::TYPE_UR, SLAVE );
  6. wss.insert( BCD::TYPE_ARM, SLAVE );
  7. wss.insert( BCD::TYPE_SP20000C, MASTER );
  8. wss.insert( BCD::TYPE_MULTIPLICATION_ON_SLAVE, SLAVE );
  9. wss.insert( BCD::TYPE_ADDITION_ON_MASTER, MASTER );

How to Wrapping (live example)

加了一些注释,精简了代码。

UR0 封装了 URController,源码位于 Src/Wrappers/UR0.h

  1. class UR0 : public QObject
  2. {
  3. Q_OBJECT
  4. public:
  5. URController *kernel; // kernel 变量为封装的对象
  6. private:
  7. bool doItYourself;
  8. public:
  9. UR0( URController *ur0 = NULL );

构造函数基本如下:

  1. UR0::UR0( URController *ur0 /* = NULL */ )
  2. {
  3. // 类执行实体
  4. kernel = ur0;
  5. // 判断是否在本机执行
  6. doItYourself = Moderator::wss.value( BCD::TYPE_UR ) == Bundle::whoAmI();
  7. /*
  8. * 说明:
  9. * Moderator::wss: 记录了所有模块的注册信息(在高性能执行,还是在工控机),
  10. * 新加的模块也要去注册,源码在 `Src/Utils/moderator.cpp'
  11. * Bundle::whoAmI(): 本机是高性能还是工控机?
  12. */
  13. // 如挂在本机执行,但执行实体为 NULL,说明没有正确初始化
  14. if ( doItYourself && NULL == kernel ) {
  15. Logger::log() << "Fatal Error";
  16. exit( EXIT_FAILURE );
  17. }
  18. // 连接“实体”原有的信号到“封装”的槽,重新 emit 信号 & 分发到两台电脑
  19. connect ( kernel, SIGNAL(wrp_getLocalAddress(QString)),
  20. this, SLOT(onWrp_getLocalAddress(QString)) );
  21. }

这个 wrp_getLocalAddress(QString) 信号是对 QString URControllor::getLocalAddress() 的封装,在保持原有类返回值的情况下,在函数内部 emit 信号,这里连接后被 UR0 捕捉,重新发射信号(在两台电脑):

  1. void UR0::onWrp_getLocalAddress( QString addr )
  2. {
  3. // 在本机 emit 信号(本机界面可以捕获)
  4. emit wrp_getLocalAddress( addr );
  5. // 在异地 emit 信号(异地界面可以捕获)
  6. Bundle::send( Moderator::sig_ur_wrp_getLocalAddress( addr ) );
  7. }

Bundle::send 会把函数和参数传到另一台电脑,再次发射信号。

所有的 Moderator 的函数(如这里的 Moderator::sig_ur_wrp_getLocalAddress( QString )
要现在 Moderator 里指定一个函数 ID(用于区分不同函数),ID 命名规则为:模块名__FUNCTION_NAME__TYPE1_TYPE2

举例:

  1. // ur 模块,genNewPath 函数,QString 类型参数
  2. UR__GEN_NEW_PATH__QSTRING
  3. // ur 模块,genNewPath 函数,无类型参数也要显式地表明 VOID
  4. UR__GEN_NEW_PATH__VOID
  5. // 信号的模块名为 SIG_模块名__函_数_名__参_数_名,如
  6. SIG_UR__WRP_GET_LOCAL_ADDRESS__QSTRING
  7. // lms 模块,setFrequencyAngleresolution 函数,参数为 int、double
  8. LMS__SET_FREQUENCY_ANGLERESOLUTION__INT_DOUBLE
  9. // 上面的函数 ID 对应的函数依次为:
  10. //
  11. // static QByteArray ur_genNewPath( const QString &path );
  12. // static QByteArray ur_genNewPath( )
  13. // static QByteArray sigUR_wrp_getLocalAddress( const QString &addr )
  14. // static QByteArray lms_setFrequencyAngleresolution( const int i, const double &d )

指定了函数 ID 后要把对应的函数和参数序列化为一串字节,需要定义一个静态函数,返回值为 QByteArray,
比如上面的 UR__GEN_NEW_PATH__QSTRING,对应的函数为:

  1. // .h 文件中申明
  2. static QByteArray ur_genNewPath( const QString &path );
  3. // .cpp 文件中实现
  4. QByteArray Moderator::ur_genNewPath( const QString &path )
  5. {
  6. TX_OUT << (int)UR__GEN_NEW_PATH__QSTRING // 传入函数 ID,强制转化为 int 型
  7. << path; // 传入调用的参数
  8. return tx; // 这里的宏 TX_OUT 和 return tx 不必深究
  9. }

这样就能顺利地用 Bundle::send( Moderator::ur_genNewPath( path ) ) 把信息传递到另一台电脑。

不过,还得写解析部分。对收到的网络数据,要分析是要调用什么函数,还要还原参数,这部分在 void Moderator::dispatch( QByteArray &msg ) 函数中定义,
比如上面的 Moderator::ur_genNewPath( const QString &path ) 在 dispatch 函数中就是:

  1. // 已经提取了函数 ID 到 flag
  2. // 如果函数 ID 为 UR__GEN_NEW_PATH__QSTRING
  3. else if ( UR__GEN_NEW_PATH__QSTRING == flag )
  4. {
  5. // 参数为 QString,那就定义一个 QString
  6. QString path;
  7. // 从网络中取出 QString 类型的路径
  8. in >> path;
  9. // 让本地的“类执行”实体去调用这个函数
  10. Bundle::getInstance()->ur->genNewPath( path );
  11. /*
  12. * 补充说明:
  13. * 这里也可直接调用 `Bundle::getInstance()->ur->kernel->genNewPath( path )'
  14. * 但并不推荐这样做(Wrapper 为判断调用是否合法,kernel 不会)。
  15. */
  16. }

然后,把模块加到 Bundle 里,比如 UR,则到 Bundle 类中加入变量:

  1. // 加入变量
  2. class Bundle
  3. {
  4. ...
  5. UR0 *ur;
  6. ...
  7. };
  8. // 初始化,在
  9. Bundle Bundle::initInstance( )
  10. {
  11. Bundle bundle;
  12. ...
  13. bundle.ur = NULL;
  14. ...
  15. return bundle;
  16. }
  17. // 在 main.cpp 中初始化(参考 `Src/MasterSlave/main.cpp')
  18. Bundle::getInstance()->ur = Bundle::whoAmI() == Moderator::wss.value( BCD::TYPE_UR )
  19. ? new UR0( new URController )
  20. : new UR0;

封装流程完毕。

需要做的事情有:

  1. 注册 Class 的执行(在高性能平台还是公共机?)
  2. 对 Class 封装一个 Wrapper,把原来带返回值的函数变成 emit 信号的方式
  3. 把 Wrapper 所有的函数,都在 Moderator 中定义函数 ID序列化函数解析和 dispatch
  4. 把 Wrapper 型指针加到 Bundle 中,并写好初始化函数。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注