入门vsomeip

本文介绍了SOME/IP协议栈的开源实现vsomeip,包括下载、编译源码的步骤,以及hello_world例程的运行。文章详细解析了vsomeip库中的关键类和函数,如runtime、application、handler.hpp等,帮助读者理解vsomeip的工作原理和使用方法。
摘要由CSDN通过智能技术生成

一、前言

SOME/IP是一套可伸缩的基于IP协议的面向服务的中间件的规范,可以在车载以太网上实现RPC功能(Remote Procedure Call,远程过程调用),这样一个ECU可以通过车载以太网调用另一个ECU上提供的函数。vsomip是由宝马开发的开源SOME/IP协议栈。

SOME/IP官网:Scalable service-Oriented MiddlewarE over IP (SOME/IP)

vsomeip代码仓库:GitHub - COVESA/vsomeip: An implementation of Scalable service-Oriented MiddlewarE over IP

二、下载、编译源码

  1. 检查gcc版本(vsomeip使用到了C++11,需要gcc 4.8及以上版本):

    gcc -v
    
  2. 安装cmake(vsomeip使用到了CMake构建系统):

    sudo apt install cmake
    
  3. 安装boost库到/usr/local路径下(vsomeip使用了Boost库,需要Boost 1.55及以上版本,目前最高支持到Boost 1.76,后面执行vsomeip的cmake命令的时候要留意相关的错误信息):

    tar --bzip2 -xf boost_1_76_0.tar.bz2
    cd boost_1_78_0/
    sudo ./bootstrap.sh --prefix=/usr/local
    sudo ./b2 install --with=all
    
  4. 如果安装的Boost库版本错误,可以执行如下命令删除已安装的boost:

    sudo rm -f /usr/local/lib/libboost*
    sudo rm -rf /usr/local/include/boost
    sudo rm -rf /usr/local/lib/cmake/*-1.78.0*
    
  5. 安装gtest(用于编译vsomeip的单元测试用例):

    git clone https://github.com/google/googletest
    cd googletest
    cmake CMakeLists.txt
    make
    sudo cp ./lib/* /usr/lib
    sudo cp -a googletest/include/* /usr/include/
    sudo cp -a googlemock/include/* /usr/include/
    
  6. 安装编译vsomeip内文档所需要的工具和pkg-config工具:

    sudo apt-get update
    sudo apt-get install asciidoc source-highlight doxygen graphviz pkg-config
    
  7. 测试boost是否安装成功(执行g++ test_boost.cpp -o test_boost命令编译测试程序,执行echo 1 2 3 | ./test_boost命令运行测试程序):

    #include <boost/lambda/lambda.hpp>
    #include <iostream>
    #include <iterator>
    #include <algorithm>
    
    int main()
    {
        using namespace boost::lambda;
        typedef std::istream_iterator<int> in;
    
        std::for_each(
            in(std::cin), in(), std::cout << (_1 * 3) << " " );
    }
    
  8. 测试gtest是否安装成功(执行g++ test_gtest.cpp -lgtest -lpthread -o test_gtest命令编译测试程序,执行./test_gtest命令运行测试程序):

    #include <gtest/gtest.h>
    
    int add(int a,int b){
    	return a+b;
    }
    
    TEST(testCase,test0){
        EXPECT_EQ(add(2,3),5);
    }
    
    int main(int argc,char **argv){
    	testing::InitGoogleTest(&argc,argv);
    	return RUN_ALL_TESTS();
    }
    
  9. 下载、编译、安装vsomeip库(重新编译记得先删除原来的build目录,执行make doc命令会生成vsomeip/build/documentation/vsomeipUserGuide.html文件和vsomeip/build/documentation/html/index.html文件):

    git clone https://github.com/COVESA/vsomeip.git
    cd vsomeip
    mkdir build
    cd build
    export GTEST_ROOT=/home/charming/Downloads/googletest
    cmake ..
    make -j20
    sudo make install
    make doc
    
  10. 可以在编译前通过给cmake命令传参的方式更改vsomeip的默认配置(如单播地址、诊断地址等),具体方法请参考vsomeip顶级目录的README.md文件。

三、编译、运行hello_world例程

vsomeip自带一个hello_world例程,源码位置在examples/hello_world,编译步骤如下:

cd vsomeip/build
cmake --build . --target hello_world
make hello_world_service hello_world_client

先在第一个终端里执行如下命令运行hello_world_service程序(添加LD_LIBRARY_PATH=/home/charming/Documents/vsomeip/build是为了解决这个Configuration module could not be loaded!运行时错误信息的):

cd vsomeip/build/examples/hello_world
LD_LIBRARY_PATH=/home/charming/Documents/vsomeip/build \
VSOMEIP_CONFIGURATION=../../../examples/hello_world/helloworld-local.json \
VSOMEIP_APPLICATION_NAME=hello_world_service \
./hello_world_service
2022-02-28 04:51:07.485420 [info] Parsed vsomeip configuration in 0ms
2022-02-28 04:51:07.486476 [info] Using configuration file: "./helloworld-local.json".
2022-02-28 04:51:07.486564 [info] Configuration module loaded.
2022-02-28 04:51:07.486604 [info] Initializing vsomeip application "hello_world_service".
2022-02-28 04:51:07.487442 [info] Instantiating routing manager [Host].
2022-02-28 04:51:07.489528 [info] create_local_server Routing endpoint at /tmp/vsomeip-0
2022-02-28 04:51:07.491667 [info] Application(hello_world_service, 4444) is initialized (11, 100).
2022-02-28 04:51:07.492815 [info] Starting vsomeip application "hello_world_service" (4444) using 2 threads I/O nice 255
2022-02-28 04:51:07.494518 [info] shutdown thread id from application: 4444 (hello_world_service) is: 7f8dc3dff700 TID: 32258
2022-02-28 04:51:07.494435 [info] main dispatch thread id from application: 4444 (hello_world_service) is: 7f8dc4600700 TID: 32257
2022-02-28 04:51:07.496899 [info] Watchdog is disabled!
2022-02-28 04:51:07.498448 [info] io thread id from application: 4444 (hello_world_service) is: 7f8dc6bd0d00 TID: 32255
2022-02-28 04:51:07.498529 [info] io thread id from application: 4444 (hello_world_service) is: 7f8dc2dfd700 TID: 32260
2022-02-28 04:51:07.498719 [info] OFFER(4444): [1111.2222:0.0] (true)
2022-02-28 04:51:07.499362 [info] vSomeIP 3.1.20.3 | (default)
2022-02-28 04:51:07.502779 [info] Listening at /tmp/vsomeip-4444
2022-02-28 04:51:17.506488 [info] vSomeIP 3.1.20.3 | (default)
2022-02-28 04:51:27.514147 [info] vSomeIP 3.1.20.3 | (default)
2022-02-28 04:51:37.516763 [info] vSomeIP 3.1.20.3 | (default)
2022-02-28 04:51:47.523069 [info] vSomeIP 3.1.20.3 | (default)
2022-02-28 04:51:57.525565 [info] vSomeIP 3.1.20.3 | (default)
2022-02-28 04:52:07.534342 [info] vSomeIP 3.1.20.3 | (default)
2022-02-28 04:52:07.729135 [info] Application/Client 5555 is registering.
2022-02-28 04:52:07.730497 [info] Client [4444] is connecting to [5555] at /tmp/vsomeip-5555
2022-02-28 04:52:07.734012 [info] REGISTERED_ACK(5555)
2022-02-28 04:52:07.845123 [info] REQUEST(5555): [1111.2222:255.4294967295]
2022-02-28 04:52:07.854602 [info] RELEASE(5555): [1111.2222]
2022-02-28 04:52:07.854940 [info] Application/Client 5555 is deregistering.
2022-02-28 04:52:07.856428 [info] Client [4444] is closing connection to [5555]
2022-02-28 04:52:12.855090 [info] STOP OFFER(4444): [1111.2222:0.0] (true)
2022-02-28 04:52:12.856448 [info] Stopping vsomeip application "hello_world_service" (4444).

然后在第二个终端里执行如下命令运行hello_world_client程序:

cd vsomeip/build/examples/hello_world
LD_LIBRARY_PATH=/home/charming/Documents/vsomeip/build \
VSOMEIP_CONFIGURATION=../../../examples/hello_world/helloworld-local.json \
VSOMEIP_APPLICATION_NAME=hello_world_client \
./hello_world_client
2022-02-28 04:52:07.718401 [info] Parsed vsomeip configuration in 0ms
2022-02-28 04:52:07.718915 [info] Using configuration file: "../../../examples/hello_world/helloworld-local.json".
2022-02-28 04:52:07.718995 [info] Configuration module loaded.
2022-02-28 04:52:07.719034 [info] Initializing vsomeip application "hello_world_client".
2022-02-28 04:52:07.719077 [info] Instantiating routing manager [Proxy].
2022-02-28 04:52:07.719553 [info] Client [5555] is connecting to [0] at /tmp/vsomeip-0
2022-02-28 04:52:07.720077 [info] Application(hello_world_client, 5555) is initialized (11, 100).
2022-02-28 04:52:07.720329 [info] Starting vsomeip application "hello_world_client" (5555) using 2 threads I/O nice 255
2022-02-28 04:52:07.721646 [info] main dispatch thread id from application: 5555 (hello_world_client) is: 7fe9b477a700 TID: 32263
2022-02-28 04:52:07.722972 [info] io thread id from application: 5555 (hello_world_client) is: 7fe9b6549d00 TID: 32262
2022-02-28 04:52:07.723214 [info] io thread id from application: 5555 (hello_world_client) is: 7fe9b3778700 TID: 32265
2022-02-28 04:52:07.722003 [info] shutdown thread id from application: 5555 (hello_world_client) is: 7fe9b3f79700 TID: 32264
2022-02-28 04:52:07.727308 [info] Listening at /tmp/vsomeip-5555
2022-02-28 04:52:07.727787 [info] Client 5555 (hello_world_client) successfully connected to routing  ~> registering..
2022-02-28 04:52:07.732502 [info] Application/Client 5555 (hello_world_client) is registered.
2022-02-28 04:52:07.846269 [info] ON_AVAILABLE(5555): [1111.2222:0.0]
Sending: World
2022-02-28 04:52:07.847799 [info] Client [5555] is connecting to [4444] at /tmp/vsomeip-4444
Received: Hello World
2022-02-28 04:52:07.852080 [info] Stopping vsomeip application "hello_world_client" (5555).
2022-02-28 04:52:07.855985 [info] Application/Client 5555 (hello_world_client) is deregistered.
2022-02-28 04:52:07.857304 [info] Client [5555] is closing connection to [4444]

四、vsomeip库中一些常用的类和函数

vsomeip.hpp头文件
  1. 我们使用vsomeip库的时候只需要添加这一个头文件就可以了#include <vsomeip/vsomeip.hpp>,它包含了vsomeip中一些常用类的头文件,以下是文件内容:

    #ifndef VSOMEIP_VSOMEIP_HPP
    #define VSOMEIP_VSOMEIP_HPP
    
    /**
     * \brief The central vsomeip header. Include this to use vsomeip.
     */
    
    #include <vsomeip/constants.hpp>
    #include <vsomeip/defines.hpp>
    #include <vsomeip/application.hpp>
    #include <vsomeip/message.hpp>
    #include <vsomeip/payload.hpp>
    #include <vsomeip/runtime.hpp>
    #include <vsomeip/trace.hpp>
    
    namespace vsomeip = vsomeip_v3;
    
    #endif // VSOMEIP_VSOMEIP_HPP
    
runtime类
  1. vsomeip::runtime类,这是一个单例类,定义了一些静态成员函数和虚成员函数,如create_application函数、create_request函数、create_response函数、create_payload函数等等,负责管理本机vsomeip库中所有的公共资源,定义在<vsomeip/runtime.hpp>文件内:

    /**
     *
     * \brief Singleton class containing all public resource management
     * facilities of vsomeip.
     *
     * The methods of this class shall be used to create instances of all the
     * classes needed to facilitate SOME/IP communication. In particular, it is
     * the entry point to create instances of the @ref application class that
     * contains the main public API of vsomeip.
     *
     */
    class VSOMEIP_IMPORT_EXPORT runtime {
    public:
    
        static std::string get_property(const std::string &_name);
        static void set_property(const std::string &_name, const std::string &_value);
    
        static std::shared_ptr<runtime> get();
    
        virtual ~runtime() {
        }
    
        /**
         *
         * \brief Creates a vsomeip application object.
         *
         * An application object manages service offers and requests as well as
         * event subscriptions. It allows to register user application functions
         * as callbacks that are called on specific events during runtime, e.g
         * to react on incoming SOME/IP messages.
         * An application object is identified by a unique name that is also used
         * in (and therefore has to match) the configuration files of vsomeip. If
         * the name is left empty, the application name is taken from the
         * environment variable "VSOMEIP_APPLICATION_NAME"
         *
         * \param _name Name of the application on the system.
         *
         */
        virtual std::shared_ptr<application> create_application(
                const std::string &_name = "") = 0;
    ...
        /**
         *
         * \brief Constructs an empty request message.
         *
         * The message can then be used to call @ref application::send to send a
         * SOME/IP message. The message type is set to REQUEST after the
         * call and the request identifier is automatically set during the
         * @ref application::send call.
         *
         * The user application is responsible for setting the service instance
         * and the payload.
         *
         * \param _reliable Determines whether this message shall be sent
         * over a reliable connection (TCP) or not (UDP).
         *
         */
        virtual std::shared_ptr<message> create_request(
                bool _reliable = false) const = 0;
    
        /*
         * \brief Constructs an empty response message from a given request
         * message.
         *
         * The message can then be used to call @ref application::send to send a
         * SOME/IP message. The message type is set to RESPONSE after the
         * call and the request identifier is automatically set from the
         * request message.
         *
         * The user application is responsible for setting the service instance
         * and the payload.
         *
         * \param _request The request message that shall be answered by
         * the response message.
         *
         */
        virtual std::shared_ptr<message> create_response(
                const std::shared_ptr<message> &_request) const = 0;
    ...
        /**
         *
         * \brief Creates a payload object filled with the given data.
         *
         * \param _data Bytes to be copied into the payload object.
         *
         */
        virtual std::shared_ptr<payload> create_payload(
                const std::vector<byte_t> &_data) const = 0;
    ...
    };
    
  2. runtime类具体的实现是implementation/runtime/include/runtime_impl.hpp文件中定义的runtime_impl类:

    class runtime_impl: public runtime {
    public:
    
        static std::string get_property(const std::string &_name);
        static void set_property(const std::string &_name, const std::string &_value);
    
        static std::shared_ptr<runtime> get();
    
        virtual ~runtime_impl();
    
        std::shared_ptr<application> create_application(
                const std::string &_name);
    
        std::shared_ptr<message> create_message(bool _reliable) const;
        std::shared_ptr<message> create_request(bool _reliable) const;
        std::shared_ptr<message> create_response(
                const std::shared_ptr<message> &_request) const;
        std::shared_ptr<message> create_notification(bool _reliable) const;
    
        std::shared_ptr<payload> create_payload() const;
        std::shared_ptr<payload> create_payload(const byte_t *_data,
                uint32_t _size) const;
        std::shared_ptr<payload> create_payload(
                const std::vector<byte_t> &_data) const;
    
        std::shared_ptr<application> get_application(
                const std::string &_name) const;
    
        void remove_application( const std::string &_name);
    
    private:
        static std::map<std::string, std::string> properties_;
    
        std::map<std::string, std::weak_ptr<application>> applications_;
    
        mutable std::mutex applications_mutex_;
    };
    
  3. 其中值得注意的是runtime_impl::get和runtime_impl::create_application函数的实现(位于implementation/runtime/src/runtime_impl.cpp文件):

    std::shared_ptr<runtime> runtime_impl::get() {
        static std::shared_ptr<runtime> the_runtime_ = std::make_shared<runtime_impl>();
        return the_runtime_;
    }
    
    std::shared_ptr<application> runtime_impl::create_application(
            const std::string &_name) {
        static std::uint32_t postfix_id = 0;
        std::lock_guard<std::mutex> its_lock(applications_mutex_);
        std::string its_name_ = _name;
        auto found_application = applications_.find(_name);
        if( found_application != applications_.end()) {
            its_name_ += "_" + std::to_string(postfix_id++);
        }
        std::shared_ptr<application> application = std::make_shared<application_impl>(its_name_);
        applications_[its_name_] = application;
        return application;
    }
    
application类
  1. vsomeip::application类,每个使用vsomeip库的应用程序都需要有一个application对象,用来调用vsomeip库提供的一系列API,application类在<vsomeip/application.hpp中>定义:

    /**
     *
     * \brief This class contains the public API of the vsomeip implementation.
     *
     * Due to its heavy resource footprint, it should exist once per client and can
     * be instantiated using the API of @ref runtime. It manages the lifecycle of
     * the vsomeip client and allocates all resources needed to communicate.
     *
     */
    class application {
    public:
    ...
        /**
         *
         * \brief Initializes the application.
         *
         *  The init method must be called first after creating a vsomeip
         *  application and executes the following steps to initialize it:
         * - Loading the configuration from a dynamic module
         *   - Loading the configuration from a .json file or
         *   - Loading the configuration from compiled data (not yet available)
         * - Determining routing configuration and initialization of the routing
         *   itself
         * - Installing signal handlers
         *
         */
        virtual bool init() = 0;
    
        /**
         *
         * \brief Starts message processing.
         *
         * This method must be called after init to start message processing. It
         * will block until the message processing is terminated using the @ref
         * stop method or by receiving signals. It processes messages received
         * via the sockets and uses registered callbacks to pass them to the user
         * application.
         *
         */
        virtual void start() = 0;
    
        /**
         * \brief Stops message processing.
         *
         * This method stops message processing. Thus, @ref start will return
         * after a call to stop.
         *
         */
        virtual void stop() = 0;
    ...
        /**
         *
         * \brief Offers a SOME/IP service instance.
         *
         * The user application must call this method for each service it offers
         * to register it at the vsomeip routing component, which makes the
         * service visible to interested clients. Dependent on the configuration
         * the service is available internally only or internally and externally.
         * To offer a service to the external network, the configuration must
         * contain a port for the offered service instance. If no such port
         * configuration is provided, the service is not visible outside the
         * device.
         *
         * \param _service Service identifier of the offered service interface.
         * \param _instance Instance identifier of the offered service instance.
         * \param _major Major service version (Default: 0).
         * \param _minor Minor service version (Default: 0).
         *
         */
        virtual void offer_service(service_t _service, instance_t _instance,
                major_version_t _major = DEFAULT_MAJOR, minor_version_t _minor =
                        DEFAULT_MINOR) = 0;
    
        /**
         *
         * \brief Stops offering a SOME/IP service instance.
         *
         * The user application must call this method to withdraw a service offer.
         *
         * \param _service Service identifier of the offered service interface.
         * \param _instance Instance identifer of the offered service instance.
         * \param _major Major service version (Default: 0).
         * \param _minor Minor service version (Default: 0).
         *
         */
        virtual void stop_offer_service(service_t _service, instance_t _instance,
                major_version_t _major = DEFAULT_MAJOR, minor_version_t _minor =
                        DEFAULT_MINOR) = 0;
    ...
        /**
         *
         * \brief Registers the application as client of a service instance.
         *
         * A user application must call this method for each service instance it
         * wants to use. The request is stored within the routing component and the
         * application is registered as client for the service as soon as the
         * service instance becomes available.
         *
         * \param _service Service identifier of the requested service interface.
         * \param _instance Instance identifier of the requested service instance.
         * \param _major Major service version (Default: 0xFF).
         * \param _minor Minor service version (Default: 0xFFFFFF).
         * \param _use_exclusive_proxy Create an IP endpoint that is exclusively
         * used for the communication of this application to the service instance.
         *
         */
        virtual void request_service(service_t _service, instance_t _instance,
                major_version_t _major = ANY_MAJOR,
                minor_version_t _minor = ANY_MINOR) = 0;
    
        /**
         *
         * \brief Unregister the application as client of a service instance.
         *
         * A user application should call this method if it does not request to
         * use the service instance any longer. The method unregisters the request
         * a the routing component, which removes the service instance from the
         * list of requested service instances if the call releases the last
         * existing request for the service instance. This is important for
         * external service instances, as the SOME/IP Service Discovery can avoid
         * to send unnecessary Find messages.
         *
         * \param _service Service identifier of the offered service interface.
         * \param _instance Instance identifier of the offered service instance.
         *
         */
        virtual void release_service(service_t _service, instance_t _instance) = 0;
    ...
        /**
         *
         * \brief Sends a message.
         *
         * Serializes the specified message object, determines the taget and sends
         * the message to the target. For requests, the request identifier is
         * automatically built from the client identifier and the session
         * identifier.
         *
         * \param _message Message object.
         *
         */
        virtual void send(std::shared_ptr<message> _message) = 0; 
    ...
        /**
         *
         * \brief Register a state handler with the vsomeip runtime.
         *
         * The state handler tells if this client is successfully [de]registered
         * at the central vsomeip routing component. This is called during the
         * @ref start and @ref stop methods of this class to inform the user
         * application about the registration state.
         *
         * \param _handler Handler function to be called on state change.
         *
         */
        virtual void register_state_handler(state_handler_t _handler) = 0;
    
        /**
         *
         * \brief Unregister the state handler.
         *
         */
        virtual void unregister_state_handler() = 0;
    
        /**
         *
         * \brief Registers a handler for the specified method or event.
         *
         * A user application must call this method to register callbacks for
         * for messages that match the specified service, instance, method/event
         * pattern. It is possible to specify wildcard values for all three
         * identifiers arguments.
         *
         * Notes:
         * - Only a single handler can be registered per service, instance,
         *   method/event combination.
         * - A subsequent call will overwrite an existing registration.
         * - Handler registrations containing wildcards can be active in parallel
         *   to handler registrations for specific service, instance, method/event
         *   combinations.
         *
         * \param _service Service identifier of the service that contains the
         * method or event. Can be set to ANY_SERVICE to register a handler for
         * a message independent from a specific service.
         * \param _instance Instance identifier of the service instance that
         * contains the method or event. Can be set to ANY_INSTANCE to register
         * a handler for a message independent from a specific service.
         * \param _method Method/Event identifier of the method/event that is
         * to be handled. Can be set to ANY_METHOD to register a handler for
         * all methods and events.
         * \param _handler Callback that will be called if a message arrives
         * that matches the specified service, instance and method/event
         * parameters.
         *
         */
        virtual void register_message_handler(service_t _service,
                instance_t _instance, method_t _method,
                message_handler_t _handler) = 0;
        /**
         *
         * \brief Unregisters the message handler for the specified service
         * method/event notification.
         *
         * \param _service Service identifier of the service that contains the
         * method or event. Can be set to ANY_SERVICE to unregister a handler for
         * a message independent from a specific service.
         * \param _instance Instance identifier of the service instance that
         * contains the method or event. Can be set to ANY_INSTANCE to unregister
         * a handler for a message independent from a specific service.
         * \param _method Method/Event identifier of the method/event that is
         * to be handled. Can be set to ANY_METHOD to unregister a handler for
         * all methods and events.
         */
        virtual void unregister_message_handler(service_t _service,
                instance_t _instance, method_t _method) = 0;
        
        /**
         *
         * \brief Register a callback that is called when service instances
         * availability changes.
         *
         * This method allows for the registration of callbacks that are called
         * whenever a service appears or disappears. It is possible to specify
         * wildcards for service, instance and/or version. Additionally, the
         * version specification is optional and defaults to DEFAULT_MAJOR
         * /DEFAULT_MINOR.
         *
         * \param _service Service identifier of the service instance whose
         * availability shall be reported. Can be set to ANY_SERVICE.
         * \param _instance Instance identifier of the service instance whose
         * availability shall be reported. Can be set to ANY_INSTANCE.
         * \param _handler Callback to be called if availability changes.
         * \param _major Major service version. The parameter defaults to
         * DEFAULT_MAJOR and can be set to ANY_MAJOR.
         * \param _minor Minor service version. The parameter defaults to
         * DEFAULT_MINOR and can be set to ANY_MINOR.
         *
         */
        virtual void register_availability_handler(service_t _service,
                instance_t _instance, availability_handler_t _handler,
                major_version_t _major = ANY_MAJOR, minor_version_t _minor = ANY_MINOR) = 0;
    ...
        /**
         *
         * \brief Unregister all registered handlers.
         *
         */
        virtual void clear_all_handler() = 0;
    ...
    };
    
  2. application类具体的实现是implementation/runtime/include/application_impl.hpp文件中定义的application_impl类:

    class application_impl: public application,
            public routing_manager_host,
            public std::enable_shared_from_this<application_impl> {
    public:
        VSOMEIP_EXPORT application_impl(const std::string &_name);
    ...
    };
    
handler.hpp头文件
  1. 可以注意到使用application类的register_state_handler成员函数、register_message_handler成员函数、register_availability_handler成员函数传入回调函数时分别使用到了state_handler_t、message_handler_t、availability_handler_t这三个类型,使用vsomeip库需要用到的回调函数类型都定义在<vsomeip/handler.hpp>头文件内:

    #ifndef VSOMEIP_V3_HANDLER_HPP_
    #define VSOMEIP_V3_HANDLER_HPP_
    
    #include <functional>
    #include <memory>
    #include <tuple>
    
    #include <vsomeip/primitive_types.hpp>
    
    namespace vsomeip_v3 {
    
    class message;
    
    typedef std::function< void (state_type_e) > state_handler_t;
    typedef std::function< void (const std::shared_ptr< message > &) > message_handler_t;
    typedef std::function< void (service_t, instance_t, bool) > availability_handler_t;
    ...
    } // namespace vsomeip_v3
    
    #endif // VSOMEIP_V3_HANDLER_HPP_
    
primitive_types.hpp头文件
  1. vsomeip中用到的其它一些特殊变量类型都定义在<vsomeip/primitive_types.hpp>头文件中:

    ...
    namespace vsomeip_v3 {
    
    typedef uint32_t message_t;
    typedef uint16_t service_t;
    typedef uint16_t method_t;
    typedef uint16_t event_t;
    
    typedef uint16_t instance_t;
    typedef uint16_t eventgroup_t;
    
    typedef uint8_t major_version_t;
    typedef uint32_t minor_version_t;
    
    typedef uint32_t ttl_t;
    
    typedef uint32_t request_t;
    typedef uint16_t client_t;
    typedef uint16_t session_t;
    
    typedef uint32_t length_t;
    
    typedef uint8_t protocol_version_t;
    typedef uint8_t interface_version_t;
    
    typedef uint8_t byte_t;
    ...
    } // namespace vsomeip_v3
    ...
    
constants.hpp头文件
  1. vsomeip中用到的其中一些特殊变量类型对应的常数值都定义在<vsomeip/constants.hpp>头文件中:

    ...
    namespace vsomeip_v3 {
    
    const major_version_t DEFAULT_MAJOR = 0x00;
    const minor_version_t DEFAULT_MINOR = 0x00000000;
    const ttl_t DEFAULT_TTL = 0xFFFFFF; // "until next reboot"
    
    const std::string DEFAULT_MULTICAST = "224.0.0.0";
    const uint16_t DEFAULT_PORT = 30500;
    const uint16_t ILLEGAL_PORT = 0;
    const uint16_t ANY_PORT = 0;
    
    const uint16_t NO_TRACE_FILTER_EXPRESSION = 0x0000;
    
    const service_t ANY_SERVICE = 0xFFFF;
    const instance_t ANY_INSTANCE = 0xFFFF;
    const method_t ANY_METHOD = 0xFFFF;
    const major_version_t ANY_MAJOR = 0xFF;
    const minor_version_t ANY_MINOR = 0xFFFFFFFF;
    ...
    } // namespace vsomeip_v3
    ...
    
message类
  1. vsomeip::message类,该类实现了SOME/IP的标准消息类型,message类定义在interface/vsomeip/message.hpp头文件中:

    ...
    /**
     * \brief Implements regular SOME/IP messages.
     *
     * This class extends @ref message_base by an unstructured payload. Except
     * SOME/IP Service Discovery messages, all SOME/IP messages within vsomeip
     * are represented by message objects.
     */
    
    class message: virtual public message_base {
    public:
        virtual ~message() {}
    
        /**
         * \brief Returns a pointer to the message payload.
         */
        virtual std::shared_ptr<payload> get_payload() const = 0;
    
        /**
         * \brief Set the message payload.
         */
        virtual void set_payload(std::shared_ptr<payload> _payload) = 0;
    ...
    };
    ...
    
  2. message类继承了message_base基类,该基类实现了SOME/IP消息类型的头部,并且连接了序列化/反序列化的功能,message_base类定义在<vsomeip/message_base.hpp>头文件中:

    ...
    /**
     * \brief Base class to implement SOME/IP messages.
     *
     * This class implements the SOME/IP message header and connects to the
     * serialzing/deserializing functionalities. The class is inherited by
     * the message classes within ::vsomeip and vsomeip::sd that add the
     * payload representations for regular and Service Discovery messages.
     */
    class message_base
            : public serializable,
              public deserializable {
    public:
    ...
        /**
         * \brief Set the service identifier in the message header.
         */
        VSOMEIP_EXPORT virtual void set_service(service_t _service) = 0;
    ...
        /**
         * \brief Set the instance identifier in the message header.
         *
         * To address the correct service instance, vsomeip uses the instance
         * identifier. For external services it is mapped to a IP address and port
         * combination before the message is sent. For internal messages is
         * transferred as additional data appended to the SOME/IP messages.
         * Therefore, before sending a message, a user application must set the
         * instance identifier.
         */
        VSOMEIP_EXPORT virtual void set_instance(instance_t _instance) = 0;
    ...
        /**
         * \brief Set the method/event identifier in the message header.
         */
        VSOMEIP_EXPORT virtual void set_method(method_t _method) = 0;
    ...
    };
    ...
    
payload类
  1. vsomeip::payload类,message类中get_payload成员函数和set_payload成员函数分别使用了payload类的智能指针作为返回类型和输入类型,payload类定义在interface/vsomeip/payload.hpp文件内:

    ...
    /**
     *
     * \brief This class implements an array of bytes to be used as
     * payload for SOME/IP messages.
     *
    */
    class payload: public serializable, public deserializable {
    public:
        VSOMEIP_EXPORT virtual ~payload() {}
    
        /**
         * \brief Returns true if the given payload is equal to this one.
         *
         * \param _other Payload that shall be compared to this payload.
         */
        VSOMEIP_EXPORT virtual bool operator ==(const payload &_other) = 0;
    
        /**
         * \brief Returns pointer to the payload content
         */
        VSOMEIP_EXPORT virtual byte_t * get_data() = 0;
    
        /**
         * \brief Returns constant pointer to the payload content
         */
        VSOMEIP_EXPORT virtual const byte_t * get_data() const = 0;
    
        /**
         * \brief Copies the given data array to the payload object.
         *
         * The current payload content is replaced by the data provided.
         * The given buffer remains untouched.
         *
         * \param _data Pointer to a data buffer.
         * \param _length Length of the data buffer.
         */
        VSOMEIP_EXPORT virtual void set_data(const byte_t *_data,
                length_t _length) = 0;
    
        /**
         * \brief Copies the given data array to the payload object.
         *
         * The current payload content is replaced by the data provided.
         * The given buffer remains untouched.
         *
         * \param _data Vector containing the data
         */
        VSOMEIP_EXPORT virtual void set_data(
                const std::vector<byte_t> &_data) = 0;
    
        /**
         * \brief Returns the length of the payload content.
         */
        VSOMEIP_EXPORT virtual length_t get_length() const = 0;
    
        /**
         * \brief Set the maximum length of the payload content.
         *
         * This function must be called before directly copying data using the
         * pointer to the internal buffer.
         */
        VSOMEIP_EXPORT virtual void set_capacity(length_t _length) = 0;
    
        /**
         * \brief Moves the given data array to the payload object.
         *
         * The current payload content is replaced by the data provided.
         * The given buffer is owned by the payload object afterwards.
         *
         * \param _data Vector containing the data
         */
        VSOMEIP_EXPORT virtual void set_data(
                std::vector<byte_t> &&_data) = 0;    
    };
    ...
    

五、解析hello_world_service源码

hello_world_service程序包含两个文件:hello_world_service.hpp和hello_world_service_main.cpp,实现的功能是监听从hello_world_client发来的消息,然后在消息内容前面添加"Hello "后再发回给hello_world_client,最后程序会自动退出。

  1. hello_world_service.hpp文件中定义了一个hello_world_service类,这个类有如下几个私有成员:

    private:
        std::shared_ptr<vsomeip::runtime> rtm_;
        std::shared_ptr<vsomeip::application> app_;
        bool stop_;
        std::mutex mutex_;
        std::condition_variable condition_;
        std::thread stop_thread_;
    

    其中rtm_app_这两个指针会在hello_world_service类的构造函数内被初始化,hello_world_service类正是通过这两个指针来调用vsomeip库提供的函数的。其中stop_thread_是用来执行stop成员函数的线程,在hello_world_service类的构造函数内被初始化,在析构函数内被注销。其中stop_mutex_condition_这三个变量用于stop线程和main线程之间的同步。需要特别说明的是on_state_cbk成员函数,它会在start成员函数被调用后被vsomeip库回调,作用是告诉hello_world_service类vsomeip库的路由组件已经成功注册或注销了,其它成员函数代码中的注释都有详细说明。

  2. hello_world_service_main.cpp文件中实例化了一个名为hw_srv的hello_world_service类对象(该类的构造函数会新建一个线程执行stop成员函数,stop成员函数会在on_message_cbk函数执行完5秒后注销vsomeip服务,或者控制台按Ctrl+C或Ctrl+Z也会调用terminate函数使stop成员函数注销vsomeip服务),然后调用了hw_srv.init函数和hw_srv.start函数,vsomeip服务被注销后hw_srv.start函数才退出,最后main函数退出之前会自动调用hw_srv对象的析构函数注销stop线程。

  3. 这是hello_world_service.hpp的源码:

    // Copyright (C) 2015-2017 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
    // This Source Code Form is subject to the terms of the Mozilla Public
    // License, v. 2.0. If a copy of the MPL was not distributed with this
    // file, You can obtain one at http://mozilla.org/MPL/2.0/.
    #include <vsomeip/vsomeip.hpp>
    #include <chrono>
    #include <thread>
    #include <condition_variable>
    #include <mutex>
    
    #if defined ANDROID || defined __ANDROID__
    #include "android/log.h"
    #define LOG_TAG "hello_world_service"
    #define LOG_INF(...) fprintf(stdout, __VA_ARGS__), fprintf(stdout, "\n"), (void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, ##__VA_ARGS__)
    #define LOG_ERR(...) fprintf(stderr, __VA_ARGS__), fprintf(stderr, "\n"), (void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, ##__VA_ARGS__)
    #else
    #include <cstdio>
    #define LOG_INF(...) fprintf(stdout, __VA_ARGS__), fprintf(stdout, "\n")
    #define LOG_ERR(...) fprintf(stderr, __VA_ARGS__), fprintf(stderr, "\n")
    #endif
    
    static vsomeip::service_t service_id = 0x1111;
    static vsomeip::instance_t service_instance_id = 0x2222;
    static vsomeip::method_t service_method_id = 0x3333;
    
    class hello_world_service {
    public:
        // Get the vSomeIP runtime and
        // create a application via the runtime, we could pass the application name
        // here otherwise the name supplied via the VSOMEIP_APPLICATION_NAME
        // environment variable is used
        hello_world_service() :
                        rtm_(vsomeip::runtime::get()),
                        app_(rtm_->create_application()),
                        stop_(false),
                        stop_thread_(std::bind(&hello_world_service::stop, this))
        {
        }
    
        ~hello_world_service()
        {
            stop_thread_.join();
        }
    
        bool init()
        {
            // init the application
            if (!app_->init()) {
                LOG_ERR("Couldn't initialize application");
                return false;
            }
    
            // register a message handler callback for messages sent to our service
            app_->register_message_handler(service_id, service_instance_id,
                    service_method_id,
                    std::bind(&hello_world_service::on_message_cbk, this,
                            std::placeholders::_1));
    
            // register a state handler to get called back after registration at the
            // runtime was successful
            app_->register_state_handler(
                    std::bind(&hello_world_service::on_state_cbk, this,
                            std::placeholders::_1));
            return true;
        }
    
        void start()
        {
            // start the application and wait for the on_event callback to be called
            // this method only returns when app_->stop() is called
            app_->start();
        }
    
        void stop()
        {
            std::unique_lock<std::mutex> its_lock(mutex_);
            while(!stop_) {
                condition_.wait(its_lock);
            }
            std::this_thread::sleep_for(std::chrono::seconds(5));
            // Stop offering the service
            app_->stop_offer_service(service_id, service_instance_id);
            // unregister the state handler
            app_->unregister_state_handler();
            // unregister the message handler
            app_->unregister_message_handler(service_id, service_instance_id,
                    service_method_id);
            // shutdown the application
            app_->stop();
        }
    
        void terminate() {
            std::lock_guard<std::mutex> its_lock(mutex_);
            stop_ = true;
            condition_.notify_one();
        }
    
        void on_state_cbk(vsomeip::state_type_e _state)
        {
            if(_state == vsomeip::state_type_e::ST_REGISTERED)
            {
                // we are registered at the runtime and can offer our service
                app_->offer_service(service_id, service_instance_id);
            }
        }
    
        void on_message_cbk(const std::shared_ptr<vsomeip::message> &_request)
        {
            // Create a response based upon the request
            std::shared_ptr<vsomeip::message> resp = rtm_->create_response(_request);
    
            // Construct string to send back
            std::string str("Hello ");
            str.append(
                    reinterpret_cast<const char*>(_request->get_payload()->get_data()),
                    0, _request->get_payload()->get_length());
    
            // Create a payload which will be sent back to the client
            std::shared_ptr<vsomeip::payload> resp_pl = rtm_->create_payload();
            std::vector<vsomeip::byte_t> pl_data(str.begin(), str.end());
            resp_pl->set_data(pl_data);
            resp->set_payload(resp_pl);
    
            // Send the response back
            app_->send(resp);
            // we have finished
            terminate();
        }
    
    private:
        std::shared_ptr<vsomeip::runtime> rtm_;
        std::shared_ptr<vsomeip::application> app_;
        bool stop_;
        std::mutex mutex_;
        std::condition_variable condition_;
        std::thread stop_thread_;
    };
    
  4. 这是hello_world_service_main.cpp的源码:

    // Copyright (C) 2015-2017 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
    // This Source Code Form is subject to the terms of the Mozilla Public
    // License, v. 2.0. If a copy of the MPL was not distributed with this
    // file, You can obtain one at http://mozilla.org/MPL/2.0/.
    #ifndef VSOMEIP_ENABLE_SIGNAL_HANDLING
    #include <csignal>
    #endif
    #include <vsomeip/vsomeip.hpp>
    #include "hello_world_service.hpp"
    
    #ifndef VSOMEIP_ENABLE_SIGNAL_HANDLING
    hello_world_service *hw_srv_ptr(nullptr);
        void handle_signal(int _signal) {
            if (hw_srv_ptr != nullptr &&
                    (_signal == SIGINT || _signal == SIGTERM))
                hw_srv_ptr->terminate();
        }
    #endif
    
    int main(int argc, char **argv)
    {
        (void)argc;
        (void)argv;
    
        hello_world_service hw_srv;
    #ifndef VSOMEIP_ENABLE_SIGNAL_HANDLING
        hw_srv_ptr = &hw_srv;
        signal(SIGINT, handle_signal);
        signal(SIGTERM, handle_signal);
    #endif
        if (hw_srv.init()) {
            hw_srv.start();
            return 0;
        } else {
            return 1;
        }
    }
    

六、解析hello_world_client源码

hello_world_client程序包含两个文件:hello_world_client.hpp和hello_world_client_main.cpp,实现的功能是等待hello_world_service提供的服务变得可用,然后向其发送内容为"World"的消息,收到hello_world_service的回复消息后将内容输出到控制台并退出程序。

  1. hello_world_client.hpp文件中定义了一个hello_world_client类,不同之处在于多了一个名为on_availability_cbk的回调函数,on_availability_cbk函数会在指定的服务变得可用或者变得不可用的时候被vsomeip库回调。

  2. 这是hello_world_client.hpp的源码:

    // Copyright (C) 2015-2017 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
    // This Source Code Form is subject to the terms of the Mozilla Public
    // License, v. 2.0. If a copy of the MPL was not distributed with this
    // file, You can obtain one at http://mozilla.org/MPL/2.0/.
    #include <vsomeip/vsomeip.hpp>
    
    #if defined ANDROID || defined __ANDROID__
    #include "android/log.h"
    #define LOG_TAG "hello_world_client"
    #define LOG_INF(...) fprintf(stdout, __VA_ARGS__), fprintf(stdout, "\n"), (void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, ##__VA_ARGS__)
    #define LOG_ERR(...) fprintf(stderr, __VA_ARGS__), fprintf(stderr, "\n"), (void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, ##__VA_ARGS__)
    #else
    #include <cstdio>
    #define LOG_INF(...) fprintf(stdout, __VA_ARGS__), fprintf(stdout, "\n")
    #define LOG_ERR(...) fprintf(stderr, __VA_ARGS__), fprintf(stderr, "\n")
    #endif
    
    static vsomeip::service_t service_id = 0x1111;
    static vsomeip::instance_t service_instance_id = 0x2222;
    static vsomeip::method_t service_method_id = 0x3333;
    
    class hello_world_client {
    public:
        // Get the vSomeIP runtime and
        // create a application via the runtime, we could pass the application name
        // here otherwise the name supplied via the VSOMEIP_APPLICATION_NAME
        // environment variable is used
        hello_world_client() :
                        rtm_(vsomeip::runtime::get()),
                        app_(rtm_->create_application())
        {
        }
    
        bool init(){
            // init the application
            if (!app_->init()) {
                LOG_ERR ("Couldn't initialize application");
                return false;
            }
    
            // register a state handler to get called back after registration at the
            // runtime was successful
            app_->register_state_handler(
                    std::bind(&hello_world_client::on_state_cbk, this,
                            std::placeholders::_1));
    
            // register a callback for responses from the service
            app_->register_message_handler(vsomeip::ANY_SERVICE,
                    service_instance_id, vsomeip::ANY_METHOD,
                    std::bind(&hello_world_client::on_message_cbk, this,
                            std::placeholders::_1));
    
            // register a callback which is called as soon as the service is available
            app_->register_availability_handler(service_id, service_instance_id,
                    std::bind(&hello_world_client::on_availability_cbk, this,
                            std::placeholders::_1, std::placeholders::_2,
                            std::placeholders::_3));
            return true;
        }
    
        void start()
        {
            // start the application and wait for the on_event callback to be called
            // this method only returns when app_->stop() is called
            app_->start();
        }
    
        void on_state_cbk(vsomeip::state_type_e _state)
        {
            if(_state == vsomeip::state_type_e::ST_REGISTERED)
            {
                // we are registered at the runtime now we can request the service
                // and wait for the on_availability callback to be called
                app_->request_service(service_id, service_instance_id);
            }
        }
    
        void on_availability_cbk(vsomeip::service_t _service,
                vsomeip::instance_t _instance, bool _is_available)
        {
            // Check if the available service is the the hello world service
            if(service_id == _service && service_instance_id == _instance
                    && _is_available)
            {
                // The service is available then we send the request
                // Create a new request
                std::shared_ptr<vsomeip::message> rq = rtm_->create_request();
                // Set the hello world service as target of the request
                rq->set_service(service_id);
                rq->set_instance(service_instance_id);
                rq->set_method(service_method_id);
    
                // Create a payload which will be sent to the service
                std::shared_ptr<vsomeip::payload> pl = rtm_->create_payload();
                std::string str("World");
                std::vector<vsomeip::byte_t> pl_data(std::begin(str), std::end(str));
    
                pl->set_data(pl_data);
                rq->set_payload(pl);
                // Send the request to the service. Response will be delivered to the
                // registered message handler
                LOG_INF("Sending: %s", str.c_str());
                app_->send(rq);
            }
        }
    
        void on_message_cbk(const std::shared_ptr<vsomeip::message> &_response)
        {
            if(service_id == _response->get_service()
                    && service_instance_id == _response->get_instance()
                    && vsomeip::message_type_e::MT_RESPONSE
                            == _response->get_message_type()
                    && vsomeip::return_code_e::E_OK == _response->get_return_code())
            {
                // Get the payload and print it
                std::shared_ptr<vsomeip::payload> pl = _response->get_payload();
                std::string resp = std::string(
                        reinterpret_cast<const char*>(pl->get_data()), 0,
                        pl->get_length());
                LOG_INF("Received: %s", resp.c_str());
                stop();
            }
        }
    
        void stop()
        {
            // unregister the state handler
            app_->unregister_state_handler();
            // unregister the message handler
            app_->unregister_message_handler(vsomeip::ANY_SERVICE,
                    service_instance_id, vsomeip::ANY_METHOD);
            // alternatively unregister all registered handlers at once
            app_->clear_all_handler();
            // release the service
            app_->release_service(service_id, service_instance_id);
            // shutdown the application
            app_->stop();
        }
    
    private:
        std::shared_ptr<vsomeip::runtime> rtm_;
        std::shared_ptr<vsomeip::application> app_;
    };
    
  3. 这是hello_world_client_main.cpp的源码:

    // Copyright (C) 2015-2017 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
    // This Source Code Form is subject to the terms of the Mozilla Public
    // License, v. 2.0. If a copy of the MPL was not distributed with this
    // file, You can obtain one at http://mozilla.org/MPL/2.0/.
    #ifndef VSOMEIP_ENABLE_SIGNAL_HANDLING
    #include <csignal>
    #endif
    #include <vsomeip/vsomeip.hpp>
    #include "hello_world_client.hpp"
    
    #ifndef VSOMEIP_ENABLE_SIGNAL_HANDLING
    hello_world_client *hw_cl_ptr(nullptr);
        void handle_signal(int _signal) {
            if (hw_cl_ptr != nullptr &&
                    (_signal == SIGINT || _signal == SIGTERM))
                hw_cl_ptr->stop();
        }
    #endif
    
    int main(int argc, char **argv)
    {
        (void)argc;
        (void)argv;
    
        hello_world_client hw_cl;
    #ifndef VSOMEIP_ENABLE_SIGNAL_HANDLING
        hw_cl_ptr = &hw_cl;
        signal(SIGINT, handle_signal);
        signal(SIGTERM, handle_signal);
    #endif
        if (hw_cl.init()) {
            hw_cl.start();
            return 0;
        } else {
            return 1;
        }
    }
    

七、vsomeip配置文件的格式

  1. hello_world例程使用的配置文件名为helloworld-local.json,内容如下:

    {
        "unicast":"134.86.56.94",
        "logging":
        {
            "level":"debug",
            "console":"true"
        },
    
        "applications":
        [
            {
                "name":"hello_world_service",
                "id":"0x4444"
            },
    
            {
                "name":"hello_world_client",
                "id":"0x5555"
            }
        ],
    
        "services":
        [
            {
                "service":"0x1111",
                "instance":"0x2222",
                "unreliable":"30509"
            }
        ],
    
        "routing":"hello_world_service",
        "service-discovery":
        {
            "enable":"false"
        }
    }
    
  2. vsomeip库的配置文件是json格式的,在application类的init成员函数内读取配置文件并使之生效,如果VSOMEIP_CONFIGURATION环境变量没有指定配置文件的路径的话,那么vsomeip将会使用/etc/vsomeip.json这个文件或者/etc/vsomeip目录下的json文件作为配置文件;

  3. 配置文件的每个元素名及其值的具体含义可以参考vsomeip/build/documentation/vsomeipUserGuide.html文件的第四章Configuration File Structure,接下来只介绍几个hello_world例程所用到的几个配置项;

  4. unicast:主机的IP地址;

  5. logging->level:日志等级,可取的值有trace、debug、info、warning、error、fatal;

  6. logging->console:指定日志是否从控制台输出,可取的值有true或false;

  7. applications数组:包含了主机上使用这个配置文件的一系列程序,name指定了程序的名字,id就是一个任意且唯一的16位数字(高位与诊断地址相同时,低位不能为0);

  8. services数组:service指定了service的id,instance指定了service instance的id,unreliable指定了UDP的端口(也可以替换为reliable,用port这个子元素指定TCP使用的端口,用enable-magic-cookies这个子元素指定是否使用magic cookies,可取值为true或false),注意端口不要与其它程序冲突;

  9. 注意在hello_world_service.hpp文件和hello_world_client.hpp文件中都定义了如下三个相同的常量:

    static vsomeip::service_t service_id = 0x1111;
    static vsomeip::instance_t service_instance_id = 0x2222;
    static vsomeip::method_t service_method_id = 0x3333;
    
  10. routing:负责路由的程序的名字;

  11. service-discovery:包含了主程序中与服务发现相关的设置,enable指定是否开启服务发现(可取值有true和false,默认值是true)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值