探索ROS中的XML-RPC机制(一)概述和服务端

ROS节点的通信机制是基于XML-RPC协议,这是一个远程调用的协议,说白了也是一种进程间通信的方式,让一个进程A调用进程B上的函数,不可能直接调用,只能是将进程A的参数按一定协议封装传到进程B,进程B调用函数处理后,将返回值再封装成XML返回至进程A.

Xml-RPC是一个远程过程调用的分布式计算协议,通过XML将调用函数封装,并 使用HTTP协议作为传送机制。 RPC采用C/S模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。

XML-RPC不支持在socket里实现用户轮询代码,用户要轮转一个额外的线程去做这部分工作。使用XML-RPC时,所有使用都通过XMLRPSManager组件,所有与master的通信都通过master::execute函数。



XML-RPC的优点:简单、轻量;XML编码,可读性强;支持多语言多平台;

缺点:

  1. 对字符的编码较弱,中文编码可能得需要通过base64,所以ROS话题不能是中文
  2. 浪费带宽,比如就传递两个参数,需要发送一大堆无用的xml节点
  3. 对复杂数据结构支持不够好
  4. 实时性不够好,不如基于protobuf的RPC通信

XmlRpc++是一个基于C++的XML-RPC第三方库,也是ROS所使用的,需要头文件和链接动态库。我找了半天都没找到完整的Linux版本的,只好直接用ROS自带的,CMakeLists这样写:

1
2
3
4
5
include_directories(/opt/ros/kinetic/include/)
link_directories(/opt/ros/kinetic/lib)

add_executable(HelloServer "HelloServer.cpp")
target_link_libraries(HelloServer -lxmlrpcpp ) # 链接库文件

这个库其实很小,头文件如下:

ROS源码自带了一个XmlRpc++的测试程序,就是ros_comm\utilities\xmlrpcpp\test中的HelloClient.cppHelloServer.cpp,我们先看服务端

HelloServer

要想自定义一个远程调用,服务端需要实现一个抽象类XmlRpcServerMethod的子类,其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 // 类定义在命名空间 XmlRpc
// Representation of a parameter or result value
class XmlRpcValue;

//类XmlRpcServer处理客户端的请求以进行远程调用
class XmlRpcServer;
// 宏XMLRPCPP_DECL无定义,仅说明XmlRpc在ROS中做静态库编译
class XMLRPCPP_DECL XmlRpcServerMethod {
public:
XmlRpcServerMethod(std::string const& name, XmlRpcServer* server = 0);
virtual ~XmlRpcServerMethod();

std::string& name() { return _name; } // 远程调用的名称

// 执行method,参数 XmlRpcValue,子类必须实现此纯虚函数
virtual void execute(XmlRpcValue& params, XmlRpcValue& result) = 0;

// 返回method的帮助信息,由子类实现
virtual std::string help() { return std::string(); }

protected:
std::string _name; // method 名称赋值
XmlRpcServer* _server;
};

看来这个类很简单,关键是method名称和实现纯虚函数,服务端程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include "xmlrpcpp/XmlRpc.h"
#include <iostream>
#include <stdlib.h>
using namespace XmlRpc;

// The server
XmlRpcServer s;

// One argument is passed, result is "Hello, " + arg.
class HelloName : public XmlRpcServerMethod
{
public: // HelloName是 method 名称
HelloName(XmlRpcServer* s) : XmlRpcServerMethod("HelloName", s) {}
void execute(XmlRpcValue& params, XmlRpcValue& result)
{
std::string resultString = "Hello, ";
resultString += std::string(params[0]);
result = resultString; // 赋值给 result
}
} helloName(&s); // 类HelloName实例化

// A variable number of arguments are passed, all doubles, result is their sum.
class Sum : public XmlRpcServerMethod
{
public:
Sum(XmlRpcServer* s) : XmlRpcServerMethod("Sum", s) {}
void execute(XmlRpcValue& params, XmlRpcValue& result)
{
int nArgs = params.size();
double sum = 0.0;
for (int i=0; i<nArgs; ++i)
sum += double(params[i]);
result = sum;
}
} sum(&s);

int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage: HelloServer serverPort \n";
return -1;
}
int port = atoi(argv[1]);
// Specify the level of verbosity of informational messages. 0 is no output, 5 is very verbose.
XmlRpc::setVerbosity(5);

// 创建socket,绑定到端口,进入listen模式等待客户端
s.bindAndListen(port);

// Enable introspection
s.enableIntrospection(true);

// 等待一定时间以处理客户端请求,-1表示一直等待或指导exit()被调用
s.work(-1.0);
return 0;
}

服务端程序比较简单,主要是自定义method类后,使用XmlRpcServer的构造函数添加method,另外也可以用XmlRpcServer::addMethod(XmlRpcServerMethod* method);,构造函数中实际也是调用这个函数。

然后是函数setVerbosity:

1
2
3
// Easy API for log verbosity
int XmlRpc::getVerbosity() { return XmlRpcLogHandler::getVerbosity(); }
void XmlRpc::setVerbosity(int level) { XmlRpcLogHandler::setVerbosity(level); }

看来就是设置日志等级,不用再深究了。

bindAndListen其实就是封装了Linux网络编程的服务端常用函数,一看源码就清楚。至于work函数,内容如下:

1
2
3
4
5
6
// 在一定时间内处理客户端请求
void XmlRpcServer::work(double msTime)
{
XmlRpcUtil::log(2, "XmlRpcServer::work: waiting for a connection");
_disp.work(msTime);
}

这里看到日志函数了,等级是2,这里的2要与上面设置的等级相比较,也就是XmlRpcLogHandler::getVerbosity(),如果不大于所设置等级才会有日志。然后是XmlRpcDispatch::work,这个就是通信的核心了, 看源码应该是poll的方式, 懒得深入研究了。

在终端输入./HelloServer 8888运行,开始监听端口8888,运行客户端后收到了HelloName的远程调用

然后服务端回复XML,内容基于Http协议



参考:
XmlRPC简介及使用