sdbus-c++中文版使用说明(二)——基础层API


  本章节通过一个用指定分隔符,将整型数组拼接为字符串的连接器dbus实现示例,详细介绍sdbus-c++库基础层API的使用流程。

1. sdbus-c++创建连接

  在sdbus-c++中创建D-Bus连接有多种方法,可以选择给定一个name,将被作为一个连接的Bus Name:

  • createBusConnection():在用户上下文中打开与会话总线的连接,否则打开与系统总线的连接。
  • createBusConnection(const sdbus::ServiceName& name):按照指定的公共名(well-known name),在用户上下文中就打开会话总线连接,否则打开系统总线连接。
  • createSystemBusConnection():打开与系统总线的连接;
  • createSystemBusConnection(const sdbus::ServiceName& name):按照指定名称,打开与系统总线的连接。
  • createSessionBusConnection():打开与会话总线的连接。
  • createSessionBusConnection(const sdbus::ServiceName& name):按照指定名称,打开与会话总线的连接。
  • createSessionBusConnectionWithAddress(const std::string& address):打开与自定义地址的会话总线的连接
  • createRemoteSystemBusConnection(const std::string& host):使用 ssh 打开与远程主机上的系统总线的连接
  • createDirectBusConnection(const std::string& address):在自定义地址打开直接 D-Bus 连接(请参阅点对点直连(待发布)
  • createDirectBusConnection(int fd):在给定的文件描述符处打开直接 D-Bus 连接(请参阅点对点直连(待发布)
  • createServerBus(int fd):作为服务器在给定的文件描述符上打开直接 D-Bus 连接(请参阅点对点直连(待发布)
  • createBusConnection(sd_bus *bus):直接从底层 sd_bus 连接实例创建连接(该实例已直接通过 sd-bus API 创建并预先设置)。

2. sdbus-c++连接的使用

  在sdbus-c++连接的设计中,每个连接都是一个IConnection实例,每个实例都会有一个线程负责运行事件循环,来处理所有传入和传出消息以及与 D-Bus 守护进程的所有通信。简单来说,sdbus-c++的这一事件循环线程,对于同步Method调用是阻塞的,对于异步Method是非阻塞的。
  那么,从技术角度来说,我们怎样从服务器和客户端的角度来使用连接?

2.1 在服务器端使用 D-Bus 连接

  在服务器端,自己手动创建 D-Bus 连接,在其上请求总线名称,然后结合需要选择手动启动事件循环:

  • 用于内部事件循环
    • 阻塞方式:enterEventLoop()
    • 非阻塞异步方式:enterEventLoopAsync()
  • 用于外部事件循环:
      如果在应用程序中使用自定义的事件循环实现(例如,sd-event、GLib Event Loop、boost::asio 等),并且想将sdbus-c++的D-Bus连接与该事件循环关联,可使用这种方法。有关更多信息请参阅在外部事件循环中使用 sdbus-c++(待发布)

  在事件循环创建的前后,都可以创建、“挂钩”(hook)、删除该连接上的对象和代理。对象的构造函数中将 D-Bus 连接作为引用,从而实现连接和对象互相绑定,当使用B-Bus对象时,必须确保它的连接存在。

注意:对象和代理可能都挂到一个连接上。一个D-Bus 服务器也可能是另一个D-Bus服务器的客户端,并与其导出的 D-Bus 接口以及面向其他 D-Bus 接口的代理共享一个 D-Bus 连接。

2.2 在客户端使用 D-Bus 连接

  在客户端,我们同样需要一个连接——只是与服务器端不同,我们不需要在其上请求唯一的总线名称。创建代理时,我们有更多选项:

  • 将已经存在的连接作为引用传递:当应用程序已经维护 D-Bus 连接时(可能它在其上提供 D-Bus API,或者它已经挂接了一些代理),这是典型的方法。代理将与其他人共享连接。使用这种方法,我们必须确保只要代理存在,连接就存在。有关在该连接上运行事件循环的选项的讨论,请参阅上面的2.1 在服务器端使用 D-Bus 连接部分。
  • 让代理维护自己的连接(对于简单的D-Bus客户端来说,这是更简单的方法),我们有两个选择:
    • 通过连接构造代理:自己创建连接,然后用std::move构造代理,让代理成为此连接的所有者。这样做的好处是我们可以选择连接类型(system, session, remote)。此外构造函数存在一个传入sdbus::dont_run_event_loop_thread_t的重载,区别在于:
      • 当创建时没有 dont_run_event_loop_thread_t,代理将在该连接上启动专用的事件循环线程;
      • 当传入dont_run_event_loop_thread_t创建时,代理将不会在该连接上启动事件循环线程。
    • 不通过连接构造代理:在底层,代理会创建自己的连接,连接到会话总线(在用户上下文中时)或系统总线,例如5.2 同步Client端中的代理实现。此外:
      • 当创建时没有dont_run_event_loop_thread_t,代理将在该连接上启动专用的事件循环线程;
      • 或者,当传入dont_run_event_loop_thread_t创建时,代理将不会在该连接上启动事件循环线程。

3. sdbus-c++中代理的使用

  如果代理是监听传入消息(如信号、异步调用回复等)的“长寿命”代理,才需要事件循环,在代理中启动专用的事件循环很简单,但每次创建/销毁代理都会带来性能和资源成本,并且会损害可扩展性。
  如果我们只是偶尔需要执行一个或几个 D-Bus 调用而已,则应该使用“短寿命、轻量级”代理:通过启用dont_run_event_loop_thread_t标签来立即创建代理、执行调用、然后立即销毁代理。这样的代理不会产生事件循环线程,它仅支持同步 D-Bus 调用(无信号、无异步调用……)。

4. 优雅地停止内部 I/O 事件循环

  具有异步事件循环的连接(即通过enterEventLoopAsync()启动的连接)将停止并在其析构函数中自动join其事件循环线程。如果在enterEventLoop()启动的同步事件循环中发生阻塞,可以通过从不同线程或系统信号的handler,在相应的总线连接上调用leaveEventLoop()来解除阻塞。

5. 连接器的实现示例

5.1 同步Server端

  在基础层API中,我们已经得到了D-Bus总线连接、对象、代理的抽象,我们可以通过它们的接口类(IConnectionIObjectIProxy)与他们进行交互,但与底层 sd-bus C 库类似,我们仍在 D-Bus 消息级别上工作。我们需要create、对参数进行序列化/反序列化、send。基于基本 sdbus-c++ API 实现的简单 Concatenator 服务如下所示:

#include <sdbus-c++/sdbus-c++.h>
#include <vector>
#include <string>

// Yeah, global variable is ugly, but this is just an example and we want to access
// the concatenator instance from within the concatenate method handler to be able
// to emit signals.
sdbus::IObject* g_concatenator{};

void concatenate(sdbus::MethodCall call)
{
    // 从Message中获得反序列化的numbers集合
    std::vector<int> numbers;
    call >> numbers;

    // 从Message中获得反序列化的分隔符
    std::string separator;
    call >> separator;

    // 整型数组为空时抛出异常
    if (numbers.empty())
        throw sdbus::Error(sdbus::Error::Name{"org.sdbuscpp.Concatenator.Error"}, "No numbers provided");

    std::string result;
    for (auto number : numbers)
    {
        result += (result.empty() ? std::string() : separator) + std::to_string(number);
    }

    // 序列化的结果字符串赋值给reply,并将其发给caller
    auto reply = call.createReply();
    reply << result;
    reply.send();

    // 声明该连接上的接口名称 org.sdbuscpp.Concatenator
    sdbus::InterfaceName interfaceName{"org.sdbuscpp.Concatenator"};
    // 创建信号'concatenated'
    sdbus::SignalName signalName{"concatenated"};
    auto signal = g_concatenator->createSignal(interfaceName, signalName);
    signal << result;
    // 发出信号'concatenated'
    g_concatenator->emitSignal(signal);
}

int main(int argc, char *argv[])
{
    // 创建D-Bus连接并定义了公共名称
    sdbus::ServiceName serviceName{"org.sdbuscpp.concatenator"};
    auto connection = sdbus::createBusConnection(serviceName);

    // 创建concatenator的D-Bus对象
    sdbus::ObjectPath objectPath{"/org/sdbuscpp/concatenator"};
    auto concatenator = sdbus::createObject(*connection, std::move(objectPath));

    g_concatenator = concatenator.get();

    // 注册D-Bus方法和信号到concatenator对象, 并输出对象.
    sdbus::InterfaceName interfaceName{"org.sdbuscpp.Concatenator"};
    concatenator->addVTable( sdbus::MethodVTableItem{sdbus::MethodName{"concatenate"}, sdbus::Signature{"ais"}, {}, sdbus::Signature{"s"}, {}, &concatenate, {}}
                           , sdbus::SignalVTableItem{sdbus::MethodName{"concatenated"}, sdbus::Signature{"s"}, {}, {}} )
                           .forInterface(interfaceName);

    // 在当前总线连接上执行 I/O 事件循环.
    connection->enterEventLoop();
}

Server端的代码解读:
  我们建立一个 D-Bus 系统连接并在其上请求 D-Bus 名称为org.sdbuscpp.concatenator,D-Bus 客户端将使用此名称来查找服务。然后,我们在此连接上创建一个具有路径(/org/sdbuscpp/concatenator)的对象。我们添加一个所谓的对象 vtable,在其中声明和描述其 D-Bus API,即对象提供的接口、方法、信号、属性(如果有的话)。然后,我们需要确保在连接上运行事件循环,该循环处理所有传入、传出和其他请求。

提示:还有一个带参数return_slot_tSlotaddVTable()的重载方法,会返回一个Slot对象(插槽对象)。该插槽是与 vtable 注册相关的简单句柄(基于 RAII )。只要我们保留插槽对象,vtable 注册就处于活动状态。当释放插槽时,vtable 会自动从 D-Bus 对象中删除。这使我们能够实现“动态”D-Bus 对象 API,该 API 在对象生命周期内可随时添加和移除。(在sdbus-c++中vtable相关插槽均符合该定义,后文出现时仅作提示,将不再赘述。)

注意: D-Bus 对象可以附加任意数量的 vtable。甚至对象的 D-Bus 接口也可以附加多个 vtable。

  这一层上任何 D-Bus 对象方法的回调都是类型为void(sdbus::MethodCall call)的可回调函数。参数call是传入的方法调用消息。我们需要从中反序列化我们的方法输入参数,然后可以调用方法的逻辑并获取结果,其次对于给定的call,我们创建一条reply消息,将结果打包进去并通过send()将其发回给调用者。(如果我们有一个返回空的方法,我们只会发回一个空的reply。)我们还会用结果发出一个信号,为此,需要通过对象的createSignal()来创建一个信号消息,将序列化结果放到其中,然后通过调用对象的emitSignal()将其发送给订阅者。
  请注意,我们可以在运行时的任何时间动态地在连接上创建和销毁 D-Bus 对象,即使连接上有活动的事件循环。因此,管理 D-Bus 对象的生命周期(创建、导出和销毁 D-Bus 对象)是完全线程安全的。

5.2 同步Client端

#include <sdbus-c++/sdbus-c++.h>
#include <vector>
#include <string>
#include <iostream>
#include <unistd.h>

void onConcatenated(sdbus::Signal signal)
{
    std::string concatenatedString;
    signal >> concatenatedString;

    std::cout << "Received signal with concatenated string " << concatenatedString << std::endl;
}

int main(int argc, char *argv[])
{
    // 创建concatenator对象的Server端代理. 由于这里我们在创建代理实例时没有向其传递连接,该代理 
    // 将自动创建自己的连接(连接到系统总线或会话总线, 具体取决于上下文)。
    sdbus::ServiceName destination{"org.sdbuscpp.concatenator"};
    sdbus::ObjectPath objectPath{"/org/sdbuscpp/concatenator"};
    auto concatenatorProxy = sdbus::createProxy(std::move(destination), std::move(objectPath));

    // 声明该对象上存在的一个接口名 org.sdbuscpp.Concatenator
    sdbus::InterfaceName interfaceName{"org.sdbuscpp.Concatenator"};
    // 订阅名为'concatenated'的信号
    sdbus::SignalName signalName{"concatenated"};
    concatenatorProxy->registerSignalHandler(interfaceName, signalName, &onConcatenated);

    std::vector<int> numbers = {1, 2, 3};
    std::string separator = ":";

    sdbus::MethodName concatenate{"concatenate"};
    // 在对象的给定接口上调用concatenate
    {
        auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
        method << numbers << separator;
        auto reply = concatenatorProxy->callMethod(method);
        std::string result;
        reply >> result;
        assert(result == "1:2:3");
    }

    // 再次调用,这次我们传入空的整型数组,这会导致触发异常 sdbus::Error
    {
        auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
        method << std::vector<int>() << separator;
        try
        {
            auto reply = concatenatorProxy->callMethod(method);
            assert(false);
        }
        catch(const sdbus::Error& e)
        {
            std::cerr << "Got concatenate error " << e.getName() << " with message " << e.getMessage() << std::endl;
        }
    }

    // sleep一会,等待第一次调用时产生的'concatenated' 信号
    sleep(1);

    return 0;
}

Client端的代码解读:
  以上示例中,我们在总线org.sdbuscpp.concatenator上给/org/sdbuscpp/concatenator对象创建了一个代理,并给感兴趣的信号注册了处理程序onConcatenated
  此级别的 D-Bus 信号处理程序的回调类型为void(sdbus::Signal signal),参数signal是传入的信号消息。我们需要从中反序列化参数,然后用来执行业务逻辑。

提示:还有一个带参数return_slot_tregisterSignalHandler()重载,它也会返回一个Slot插槽对象。这里只要保留槽对象,信号订阅就处于活动状态,当释放对象时,信号处理程序会自动取消注册,以此更好地控制信号订阅的生命周期。(sdbus-c++中与信号注册相关插槽均符合该定义,后文出现该类插槽时仅作提示,不再赘述。)

  随后,我们对对象的concatenate()方法进行了两次 RPC 调用。通过调用代理的createMethodCall()方法来创建调用消息,将方法输入参数序列化到其中,并通过代理的callMethod()进行同步调用,即可获得返回值。第二次RPC 调用concatenate()使用无效参数完成,因此我们从服务收到 D-Bus 错误回复,我们可以看到,这是通过抛出sdbus::Error类型的异常来表现的。

5.3 异步Server端

  使用 sdbus-c++ 的基本层 API,以下是concatenate 方法的异步实现:

void concatenate(sdbus::MethodCall call)
{
    // Deserialize the collection of numbers from the message
    std::vector<int> numbers;
    call >> numbers;

    // Deserialize separator from the message
    std::string separator;
    call >> separator;

    // 启用一个线程实现异步执行...
    std::thread([numbers = std::move(numbers), separator = std::move(separator), call = std::move(call)]()
    {
        // Return error if there are no numbers in the collection
        if (numbers.empty())
        {
            // Let's send the error reply message back to the client
            auto reply = call.createErrorReply({"org.sdbuscpp.Concatenator.Error", "No numbers provided"});
            reply.send();
            return;
        }

        std::string result;
        for (auto number : numbers)
        {
            result += (result.empty() ? std::string() : separator) + std::to_string(number);
        }

        // Let's send the reply message back to the client
        auto reply = call.createReply();
        reply << result;
        reply.send();

        // Emit 'concatenated' signal (creating and emitting signals is thread-safe)
        sdbus::InterfaceName interfaceName{"org.sdbuscpp.Concatenator"};
        sdbus::SignalName signalName{"concatenated"};
        auto signal = g_concatenator->createSignal(interfaceName, signalName);
        signal << result;
        g_concatenator->emitSignal(signal);
    }).detach();
}

异步Server端代码解读:
  与同步版本相比,有一些细微的差别。请注意,我们使用std::move将当前消息call发送给工作线程(顺便说一句,我们也可以选择在工作线程中执行输入参数反序列化,而不是先反序列化后移动到工作线程…)。在这个异步执行的工作线程中,我们可以选择在传入的消息call上执行以下方法(创建和发送回复、创建和发射信号在设计上是线程安全的):

  • createReply():当我们得到执行结果result,可以调用当前消息call的该方法创建回复消息;
  • createErrorReply():当我们执行期间出现异常,异步情况下将无法像同步那样抛出Error类型消息,只能通过调用该方法,创建一个Error类型的回复(这是因为现在处于工作线程的上下文中,而不是 D-Bus 调度程序线程的上下文中,这种发回错误的方式,实际上也可以在同步 D-Bus 方法中使用)。

  方法回调参数类型在同步和异步版本中是相同的(均为sdbus::MethodCall call),这意味着 sdbus-c++ 并不关心我们如何执行 D-Bus 方法,只是在运行时决定是否同步执行它,或者是否要将执行过程(如耗时更长、运行更复杂的情况)移至工作线程。

5.4 异步Client端

  基于sdbus-c++基本层API实现Client端的异步调用,我们可以选择以下两种方法:
1. 基于回调(callback-based):在发出调用时将回调函数传给代理,收到回复时会自动调用该回调函数

int main(int argc, char *argv[])
{
    /* ...  */

    auto callback = [](MethodReply reply, std::optional<sdbus::Error> error)
    {
        if (!error) // No error
        {
            std::string result;
            reply >> result;
            std::cout << "Got concatenate result: " << result << std::endl;
        }
        else // We've got a D-Bus error...
        {
            std::cerr << "Got concatenate error " << error->getName() << " with message " << error->getMessage() << std::endl;
        }
    }

    // Invoke concatenate on given interface of the object
    {
        auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
        method << numbers << separator;
        concatenatorProxy->callMethod(method, callback);
        // When the reply comes, we shall get "Got concatenate result 1:2:3" on the standard output
    }

    // Invoke concatenate again, this time with no numbers and we shall get an error
    {
        auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
        method << std::vector<int>() << separator;
        concatenatorProxy->callMethod(method, callback);
        // When the reply comes, we shall get concatenation error message on the standard error output
    }

    /* ... */

    return 0;
}

方法1-异步Client端的代码解读:
  回调是一个返回空值的函数,它接受两个参数:一个MethodReply类型表示回复消息的引用,以及一个指向预期sdbus::Error实例的指针,该指针的用法如下:

  • 可选参数error为空:表示在调用时没有发生 D-Bus 错误,并且回复消息包含有效回复。
  • 可选参数error非空:表示在调用期间发生了错误,我们可以从参数error访问错误名称和消息内容。

  此外,函数IProxy::callMethod()还有一个重载方法,允许客户端异步执行时,设置超时参数timeout

2. 基于std::future:使用函数IProxy::callMethod()基于std::future的重载

    ...
    // Invoke concatenate on given interface of the object
    {
        auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
        method << numbers << separator;
        auto future = concatenatorProxy->callMethod(method, sdbus::with_future);
        try
        {
            auto reply = future.get(); // This will throw if call ends with an error
            std::string result;
            reply >> result;
            std::cout << "Got concatenate result: " << result << std::endl;
        }
        catch (const sdbus::Error& e)
        {
            std::cerr << "Got concatenate error " << e.getName() << " with message " << e.getMessage() << std::endl;
        }
    }

方法2-异步Client端的代码解读:
  该重载函数调用后返回一个std::future对象,当方法异步执行后,在没有异常发生时该对象将包含返回值对应的回复消息,在调用发生异常时返回Error(由std::future::get()抛出sdbus::Error)。

DBus专栏导航:
D-Bus理论基础
Linux系统DBus工具的使用
sdbus-c++中文版使用说明(一)——概括介绍与编译
sdbus-c++中文版使用说明(二)——基础层API
sdbus-c++中文版使用说明(三)——便捷层API
sdbus-c++中文版使用说明(四)——C++绑定层API

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值