Skip to content

RU: Example. Hello.

newenclave edited this page Aug 18, 2017 · 4 revisions

HELLO

Простой пример использования библиотечки.

Protocol

Протокол декларирован в proto-файле (https://github.com/newenclave/vtrc/blob/master/examples/hello/protocol/hello.proto).

package howto; // namespace для С++
option cc_generic_services = true; // говорим протобуферу сгенерировать 
                                   // service hello_service 

message request_message {          // сообщение - запрос
    optional string name = 1;
}

message response_message {         // сообщение - ответ
    optional string hello = 1;
}

service hello_service {            // описание сервиса. 
    // в сервисе всего один вызов, который принимает сообщение request_message
    // и результат помещает в сообщение response_message
    rpc send_hello( request_message ) returns ( response_message );
}

Server

Файлы *.h

#include "vtrc/server/application.h"  // для основного класса server::application
#include "vtrc/server/listener/tcp.h" // слушатель TCP

#include "vtrc/common/connection-iface.h" // информация о соединении
#include "vtrc/common/closure-holder.h"   // RAII класс для замыкания в вызове 
#include "vtrc/common/thread-pool.h"      // управление потоками и сервисом io_service

#include "protocol/hello.pb.h"          // hello protocol. Сгенерированный файл из  hello.proto
#include "google/protobuf/descriptor.h" // для descriptor( )->full_name( ) 
#include "boost/lexical_cast.hpp"       // для приведения номера порта из командной строки. 


using namespace vtrc; /// чтоб не писать лишний раз

Первое, что описано в файле исходнике сервера - это класс-сервис, наследник от сгенерированного howto::hello_service

class  hello_service_impl: public howto::hello_service {
    
    /// будем сохранять информацию о клиенте, владеющим этим классом
    common::connection_iface *cl_;

    /// обработчик вызова, описанного в proto-файле как 
    ///     rpc send_hello( request_message ) returns ( response_message );
    void send_hello(::google::protobuf::RpcController*  /*controller*/, // не нужен в данном примере
            const ::howto::request_message*     request,  // запрос
            ::howto::response_message*          response, // сообщение с ответом
            ::google::protobuf::Closure*        done) /*override*/  // сигнал об исполнении
    {
        common::closure_holder ch( done ); /// RAII. в деструкторе будет вызван done->Run( );
        
        std::ostringstream oss;

        // создадим строчку с приветствием, в которую добавим информацию о клиете,
        // который пользуется нашим сервисом 
        // cl_->name( ) вернет имя транспорнтной точки, которой пользуется клиент
        oss << "Hello " << request->name( )
            << " from hello_service_impl::send_hello!\n"
            << "Your transport name is '"
            << cl_->name( ) << "'.\nHave a nice day.";
        
        /// установим сообщению-ответу строку с результатом
        response->set_hello( oss.str( ) );
    }

public:
    
    // constructor
    hello_service_impl( common::connection_iface *cl )
        :cl_(cl)
    { }
    
    /// просто удобная обертка для получения имени данного сервиса.
    static std::string const &service_name(  )
    {
        // вернет то, что сгенерировал protobuf
        return howto::hello_service::descriptor( )->full_name( );
    }

};

make_service

Второе, что описано в исходнике -- функция-фабрика, для создания нашего сервиса. Она будет вызвана в момент, когда клиент будет обращаться к сервису первый раз.

vtrc::shared_ptr<google::protobuf::Service> make_service(
                                           common::connection_iface* connection,
                                           const std::string &service_name )
{
    if( service_name == hello_service_impl::service_name( ) ) { /// проверим это ли у нас попросили
        std::cout << "Create service " << service_name
                  << " for " << connection->name( )
                  << "\n";
        return vtrc::make_shared<hello_service_impl>( connection ); /// вернем shared_ptr на наш сервис.
    }
    /// или вернем пустой указатель, если не можем отдаьт клиенту сервис.
    return vtrc::shared_ptr<google::protobuf::Service>( ); 
}

Теперь у нас есть все, что нужно для исполнения удаленного вызова для клиентов. Осталось только добавить возможность пользоваться этим всем.

main

Функция main создает server::application, назначает фабрику сервисов и запускает слушателя, который будет принимать клиентов и передавать их на обслуживание приложению;

int main( int argc, const char **argv )
{

    /// значения по умолчанию
    const char *address = "127.0.0.1";
    unsigned short port = 56560;

    /// если что-то передали в командную строку, попробуем это применить в качестве:
    if( argc > 2 ) {
        /// 2 параметра IP PORT
        address = argv[1];
        port = boost::lexical_cast<unsigned short>( argv[2] );
    } else if( argc > 1 ) {
        /// 1 параметр PORT
        port = boost::lexical_cast<unsigned short>( argv[1] );
    }

    /// экземпляр класса для управления потоками и io_service
    /// по-умолчанию он не запускает никаких потоков, а просто создает io_service
    /// который доступен через метод get_io_service( )
    /// можно добавить сразу несколько потоков, передав их количество в конструктор
    /// Например common::thread_pool tp( 2 );
    common::thread_pool tp;
    
    /// экземпляр приложения
    server::application app( tp.get_io_service( ) );

    /// назначим фабрику
    app.assign_service_factory( &make_service );

    /// в случае некорректных данных (неправильная строка с IP)
    /// либо в случае системных ошибок (например невозможность открыть порт, потому что уже занят)
    /// будет сгенерировано исключение
    try {
        
        /// создаем слушателя, передаем ему ссылку на наше приложение
        /// create возвращает shared_ptr<server::listener> !!!
        /// то есть сделать указатель уникальным нельзя
        vtrc::shared_ptr<server::listener>
                tcp( server::listeners::tcp::create( app, address, port ) );
        
        /// start! тут будет открыт порт и начнут приниматься клиенты.
        tcp->start( );

        /// запустим цикл обработки событий в текущем потоке
        /// таким образом наш сервер будет крутиться всего в одном потоке - 
        /// потоке функции main.
        /// НО! 
        /// В системах Windows будет второй поток, который управляет таймерами
        tp.attach( );

        /// Кстати.
        /// При выходе из области видиммости слушатель будет остановлен и уничтожен
        /// поэтому выносить tp.attach( ) за try { } catch не имеет смысла. 
        /// сервер просто не будет работать

    } catch( const std::exception &ex ) {
        /// что-то неполучилось. Скажем в консоль об этом
        std::cerr << "Hello, world failed: " << ex.what( ) << "\n";
    }
    
    /// дождемся завершения всех потоков, которые были созданы через common::thread_pool
    /// в нашем случае это лишнее, потому как у нас дополнительных потоков нет
    /// оставил в качестве примера
    tp.join_all( );

    /// make valgrind happy.
    google::protobuf::ShutdownProtobufLibrary( );

    return 0;
}

Теперь можно запустить сервер

./hello_server               # запустить сервер на 127.0.0.1:56560
./hello_server 12345         # запустить сервер на 127.0.0.1:12345
./hello_server 0.0.0.0 12345 # запустить сервер на 0.0.0.0:12345

С сервером примера всё.

Client

С клиентом всё немного проще. Клиент должен только соединиться и предоставить канал для Stub-классов сервисов.

####Заголовки:

#include "vtrc/client/client.h"       /// client::vtrc_client сам клиент
#include "vtrc/common/thread-pool.h"  /// Управлятор потоками 
#include "vtrc/common/stub-wrapper.h" /// Обертка для использоания Stub-классов
                                           /// без обертки можно вполне обойтись

#include "protocol/hello.pb.h"  /// сгенерированные классы для работы с протоколом
 
#include "boost/lexical_cast.hpp" /// для приведения порта из строки в число

using namespace vtrc; /// чтоб не писать постоянно

Далее объявлены функции, которые есть реакция на события клиента.

namespace {

    /// соединён
    void on_connect( )
    {
        std::cout << "connect...";
    }
    
    /// готов
    void on_ready( )
    {
        std::cout << "ready...";
    }
    
    /// соединение закрыто
    void on_disconnect( )
    {
        std::cout << "disconnect...";
    }
}

Опять же это не обязательный код, который можно удалить.

main

В функции main:

создается пул потоков, 
создается клиент, 
происходит подписка на сигналы клиента, 
клиент соединяется, 
при успешном соединении создается канал, делается удаленный вызов и выводится результат

код:

int main( int argc, const char **argv )
{
    /// управлялка потоками. При старте создадим один поток, 
    /// потому что работа клиента асинхронная, несмотря на то, что 
    /// вызов connect не вернет управление до тех пор, пока клиент не станет готов
    /// либо не произойдет ошибка, которая сгенерирует исключение.
    common::thread_pool tp( 1 );

    /// значения по умолчанию
    const char *address = "127.0.0.1";
    unsigned short port = 56560;

    /// настроим так же как и в случае сервера
    if( argc > 2 ) {
        address = argv[1];
        port = boost::lexical_cast<unsigned short>( argv[2] );
    } else if( argc > 1 ) {
        port = boost::lexical_cast<unsigned short>( argv[1] );
    }

    /// попробуем .... 
    try {

        /// создаем экземпляр клиента 
        /// как и в случае со слушателями сервера мы пользуемся функцией-фабрикой,
        /// которая вернет нам vtrc_client_sptr (shared_ptr<vtrc_client>)
        client::vtrc_client_sptr cl =
                         client::vtrc_client::create( tp.get_io_service( ) );

        /// подпишемся на сигналы
        cl->on_connect_connect( on_connect );
        cl->on_ready_connect( on_ready );
        cl->on_disconnect_connect( on_disconnect );

        std::cout <<  "Connecting..." << std::endl;

        /// попытаемся соединиться
        /// в случае успеха на консоль будет выведено 
        /// Connecting...connect...ready...Ok
        cl->connect( address, port );

        std::cout << "Ok" << std::endl;

        /// Создадим канал, который потом отдадим Stub-классу
        vtrc::unique_ptr<common::rpc_channel> channel(cl->create_channel( ));

        /// просто удобный typedef
        typedef howto::hello_service_Stub stub_type;

        /// обертка для вызовов, которая создаст класс
        common::stub_wrapper<stub_type> hello(channel.get( ));

        /// сообщения запроса и ответа
        howto::request_message  req;
        howto::response_message res;

        /// методы с префиксом set_, либо mutable_ создаются генератором-протобуфером
        /// для установки значений 
        req.set_name( "%USERNAME%" );

        /// делаем удаленный вызов. 
        /// если бы hello был экземпляром Stub-класса, 
        /// то вызов выглядел бы так: 
        ///     stub_type hello(channel.get( ));
        ///     ......
        ///     hello.send_helo( NULL, &req, &res, NULL );
        hello.call( &stub_type::send_hello, &req, &res );

        /// вывод результата, который отдал сервер
        std::cout <<  res.hello( ) << std::endl;

    } catch( const std::exception &ex ) {
        std::cerr << "Hello, world failed: " << ex.what( ) << "\n";
    }

    /// остановим потоки и подождем пока они не прекратят работу
    tp.stop( );
    tp.join_all( );

    /// make valgrind happy.
    google::protobuf::ShutdownProtobufLibrary( );

    return 0;
}

вот и весь клиент

пробуем:

сервер:

[root@virt2real default]# ./hello_server 0.0.0.0 50000 

клиент:

% ./hello_client 192.168.3.1 50000
Connecting...
connect...ready...Ok
Hello %USERNAME% from hello_service_impl::send_hello!
Your transport name is 'tcp://192.168.3.10:53354'.
Have a nice day.

Clone this wiki locally