BITERP / PinkRabbitMQ

Внешняя Native API компонента для взаимодействия с RabbitMQ из 1С
MIT License
264 stars 107 forks source link

Добавить отправку внешних событий в 1С (ExternalEvent) #23

Open antonwantstosleep opened 4 years ago

antonwantstosleep commented 4 years ago

Например, для задач телефонии Asterisk AMI + RabbitMQ + компонента + 1С:

В 1С нельзя вызвать клиентский метод с сервера. Но компонента может работать на клиенте и на сервере.

В частности, на сервере можно:

На клиенте можно:

Идея для использования на клиенте:

Насколько я знаю, это будет работать только на клиенте - на сервере "внешние события не обрабатываются".

antonwantstosleep commented 4 years ago

Я вообще два дня как вижу C++, и написал следующее в AddInNative.h:

public:
    ...
    enum Methods
    {
        eMethGetLastError = 0,
        eMethConnect = 1,
        eMethDeclareQueue = 2,
        eMethBasicPublish = 3,
        eMethBasicConsume = 4,
        eMethBasicConsumeMessage = 5,
        eMethBasicCancel = 6,
        eMethBasicAck = 7,
        eMethDeleteQueue = 8,
        eMethBindQueue = 9,
        eMethBasicReject = 10,
        eMethDeclareExchange = 11,
        eMethDeleteExchange = 12,
        eMethUnbindQueue = 13,
        eMethSetPriority = 14,
        eMethGetPriority = 15,
        eMethStartConsumingMessages = 16, // aws: Запуск функции для постоянного чтения сообщений в петле
        eMethStopConsumingMessages = 17, // aws: Остановка функции для постоянного чтения сообщений в петле
        eMethLast = 18      // Always last
    };
private:
    ....
    const wchar_t* m_eventSourceName = L"PinkRabbitMQ"; // aws: Источник по умолчанию для ОбработкаВнешнегоСобытия
    const wchar_t* m_eventMessageName = L"AMQP Message"; // aws: Событие по умолчанию для ОбработкаВнешнегоСобытия
    bool asyncMode = false; // aws: Асинхронный режим
    long bufferDepth{100}; // aws: Длина очереди событий
    std::thread loopConsumeMessagesThread; // aws: Поток выполнения для функции постоянного чтения сообщений в петле
    void loopConsumeMessages(); // aws: Функция для постоянного чтения сообщений в петле. void - ничего не возвращает.
    bool sendEvent(const wchar_t* source, const wchar_t* message, const wchar_t* data);
};
antonwantstosleep commented 4 years ago

В AddInNative.cpp:

static const wchar_t *g_MethodNames[] =
{
    L"GetLastError",
    L"Connect",
    L"DeclareQueue",
    L"BasicPublish",
    L"BasicConsume",
    L"BasicConsumeMessage",
    L"BasicCancel",
    L"BasicAck",
    L"DeleteQueue",
    L"BindQueue",
    L"BasicReject",
    L"DeclareExchange",
    L"DeleteExchange",
    L"UnbindQueue",
    L"SetPriority",
    L"GetPriority",
    L"StartConsumingMessages", // aws: Запуск функции для постоянного чтения сообщений в петле
    L"StopConsumingMessages", // aws: Остановка функции для постоянного чтения сообщений в петле
};

static const wchar_t *g_MethodNamesRu[] =
{
    L"GetLastError",
    L"Connect",
    L"DeclareQueue",
    L"BasicPublish",
    L"BasicConsume",
    L"BasicConsumeMessage",
    L"BasicCancel",
    L"BasicAck",
    L"DeleteQueue",
    L"BindQueue",
    L"BasicReject",
    L"DeclareExchange",
    L"DeleteExchange",
    L"UnbindQueue",
    L"SetPriority",
    L"GetPriority",
    L"StartConsumingMessages", // aws: Запуск функции для постоянного чтения сообщений в петле
    L"StopConsumingMessages", // aws: Остановка функции для постоянного чтения сообщений в петле
};
bool AddInNative::CallAsProc(const long lMethodNum, tVariant* paParams, const long lSizeArray)
{
...
// aws
        case eMethStartConsumingMessages: // aws: Запуск функции для постоянного чтения сообщений в петле
        {
            // aws: Устанавливаем асинхронный режим
            asyncMode = true;
            // aws: Проверяем длину очереди сообщений и устанавливаем ее при необходимости
            if (m_iConnect && m_iConnect->GetEventBufferDepth() < bufferDepth)
            {
                m_iConnect->SetEventBufferDepth(bufferDepth);
            }
            // aws: Запускаем отдельный поток с функцией, передаем в него указатель на область памяти
            // с функцией и этот объект (класс)
            loopConsumeMessagesThread = std::thread(&AddInNative::loopConsumeMessages, this);
            // aws: Отсоединяем этот поток от основного (этого)
            loopConsumeMessagesThread.detach();

            return true;
        }
        case eMethStopConsumingMessages: // aws: Остановка функции для постоянного чтения сообщений в петле
        {
            // aws: Отключим асинхронный режим
            asyncMode = false;
            // aws: Отдельный поток сам это поймет и остановится

            return true;
        }
// aws
// aws:----------------------------------------------------------------------//
void AddInNative::loopConsumeMessages() {

    std::string outdata;
    std::uint64_t outMessageTag;
    std::uint16_t timeout = 3000;

    while (asyncMode)
    {
        bool hasMessage = client.basicConsumeMessage(
            outdata,
            outMessageTag,
            timeout
        );

        if (client.getLastError().length() != 0)
        {
            asyncMode = false;
            return;
        }

        if (hasMessage)
        {
            client.basicAck(outMessageTag);
            bool eventSent = sendEvent(m_eventSourceName, m_eventMessageName, Utils::stringToWs(outdata).c_str());

            if (!eventSent)
            {
                return;
            }

        }

    }

}
// aws: ---------------------------------------------------------------------//
bool AddInNative::sendEvent(const wchar_t* source, const wchar_t* message, const wchar_t* data)
{
    bool result = false;

    if (m_iConnect)
    {

        WCHAR_T *wsSource = 0;
        WCHAR_T *wsMessage = 0;
        WCHAR_T *wsData = 0;

        convToShortWchar(&wsSource, source);
        convToShortWchar(&wsMessage, message);
        convToShortWchar(&wsData, data);

        result = m_iConnect->ExternalEvent(wsSource, wsMessage, wsData);

        delete[] wsSource;
        delete[] wsMessage;
        delete[] wsData;

    }

    return result;

}
antonwantstosleep commented 4 years ago

Прошу помощи:

antonwantstosleep commented 4 years ago

Я пробовал скомпилировать на Linux для Linux так.

  1. Внес изменения в PinkRabbitMQLinux/src/AddInNative.h и PinkRabbitMQLinux/src/AddInNative.cpp.
  2. Сделал папку PinkRabbitMQLinux/build.
  3. Перешёл в нее и:
    $ cmake ..
    $ cmake --build . --config Release
  4. Были ошибки, поэтому я поменял файл PinkRabbitMQLinux/CMakeLists.txt:
    
    # Cmake script for background building 64 bit release project library PinkRabbitMQLinux

CMAKE_MINIMUM_REQUIRED(VERSION 2.6 FATAL_ERROR)

PROJECT(PinkRabbitMQLinux)

set (ROOT_DIR ${CMAKE_SOURCE_DIR}) set (CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/src) set(AddInNative_SRC ${CMAKE_SOURCE_DIR}/src)

SET(AddInNative_SRC ${CMAKE_SOURCE_DIR}/AddInNative.cpp ${CMAKE_SOURCE_DIR}/AddInNative.h ${CMAKE_SOURCE_DIR}/dllmain.cpp ${CMAKE_SOURCE_DIR}/stdafx.cpp ${CMAKE_SOURCE_DIR}/stdafx.h )

include_directories(${CMAKE_SOURCE_DIR}/include) include_directories(${CMAKE_SOURCE_DIR}/libevent/include) # aws: Ругань. include_directories(${CMAKE_SOURCE_DIR}/amqp/include) # aws: Ругань.

SET(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${CMAKE_SOURCE_DIR}) SET(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT")

SET(AddInDef_SRC ${CMAKE_SOURCE_DIR}/AddInNative.def)

SET(CMAKE_LINK_DEF_FILE_FLAG AddInNative.def) # aws: Ругань.

add_definitions(-DUNICODE -DWIN32 -DNOMINMAX)

add_library(${PROJECT_NAME} SHARED ${AddInNative_SRC} ${AddInDef_SRC})

set_target_properties( ${PROJECT_NAME} PROPERTIES CLEAN_DIRECT_OUTPUT 1 OUTPUT_NAME ${PROJECT_NAME}Lin64 # aws: Ошибка. )

5. Компонента собралась в libPinkRabbitMQLinuxLin64.so, но весит всего 131,5 кБ против вашей 3 Мб. Где-то что-то не подключилось / не слинковалось?
6. Вот выхлоп ldd.

user@pc:~/Projects/PinkRabbitMQ_test/PinkRabbitMQLinux/build$ ldd PinkRabbitMQLinuxLin64.so linux-vdso.so.1 (0x00007ffeecdf6000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f4b8ec72000) libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f4b8e8e9000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f4b8e54b000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f4b8e333000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f4b8e114000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4b8dd23000) /lib64/ld-linux-x86-64.so.2 (0x00007f4b8f214000)

user@pc:~/Projects/PinkRabbitMQ_test/PinkRabbitMQLinux/build$ ldd PinkRabbitMQLinuxLin64_new.so linux-vdso.so.1 (0x00007fffb3f4e000) libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f9368fa0000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f9368d88000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9368997000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f93685f9000) /lib64/ld-linux-x86-64.so.2 (0x00007f9369540000)


7. Я упаковал ее в архив рядом с манифестом, но при подключении в 1С не работает.
ripreal commented 4 years ago

В каталоге Linux проекта есть sln файл для запуска из под VisualStudio. Его и нужно использовать для разработки и компиляции линуксовой версии. Это полностью подготовленный проект. В этом случае развернуть окружение разработки будет относительно просто. В ридми есть краткая инструкция. Разрабатывать из-под блокнота на линуксе скорее всего не получится, я уже пробовал.

Подход с бесконечным циклом некорректный. Внутренняя либа для работы с rabbitMQ - AMQPСPP поддерживает возможность "подписаться" на очередь. Эту возможность и следуюет использовать. https://github.com/CopernicaMarketingSoftware/AMQP-CPP См раздел. CONSUMING MESSAGES

ripreal commented 4 years ago

channel.consume("my-queue") .onReceived(messageCb) // Подписываемся на очередь

ripreal commented 4 years ago

Прежде чем писать код рекомендую разобраться отладкой. Она вполне возможна и работает. Есть даже консольное приложение https://github.com/BITERP/PinkRabbitMQ/blob/master/PinkRabbitMQLinux/main.cpp, и юнит тесты который автоматически выполняться при запуске консольног оприложения

antonwantstosleep commented 4 years ago

В каталоге Linux проекта есть sln файл для запуска из под VisualStudio. Его и нужно использовать для разработки и компиляции линуксовой версии.

Это да, и у меня есть Windows 10 под Virtual box, но VS 2019 со всеми плюшками (я не знаю, что конкретно надо) весит под 10 Гб, места на диске нету :(

А Вы бы не могли как-нибудь сгенерировать средствами VS 2019 make файл для линукса? Извините, если глупый вопрос (возможно я до конца не понимаю, о чем прошу).

Если нет - ок, попробую найти место.

Подход с бесконечным циклом некорректный. Внутренняя либа для работы с rabbitMQ - AMQPСPP поддерживает возможность "подписаться" на очередь.

Я читал исходники этой либы, но что-то этого сразу не увидел. Сейчас вижу, спасибо. Попробую покурить, как это реализовать в виде метода компоненты. Нужно ведь еще, чтобы он вызывался в 1C "асинхронно", а-ля .НачатьВызов<ИмяМетодаКомпоненты>, верно?

Подскажите, в правильную ли сторону я двигаюсь, исходя из моей задачи? Или есть вариант проще?

antonwantstosleep commented 4 years ago

Немного почитал. Подскажите, правильно ли я понимаю.

  1. Когда мы вызываем из 1С ваш метод basicConsume, в конечном итоге вызывается метод RabbitMQClient::basicConsume, где:

    • вызывается метод библиотеки amqpcpp consume;
    • устанавливается коллбек .onMessage, в котором вы создаете сообщение msgOb на основе полученных данных и записываете его в некую внутреннюю readQueue;
    • и еще создается отдельный поток для постоянного чтения;
    • то есть внешняя компонента уже начинает получать и хранить как бы копии сообщений с сервера (поскольку никто их на сервере не "акает", они все еще доступны для прямого подключения к серверу).
  2. Когда мы вызываем метод basicConsumeMessage, то мы:

    • разово читаем одно сообщение из вашей внутренней очереди readQueue;
    • очередь уменьшается на это сообщение.
  3. Когда мы вызываем метод basicAck - мы просто "акаем" сообщение на сервере, и оно там исчезает.

  4. Вы бы могли сразу в коллбеке .onMessage создать для клиента 1С ExternalEvent, но поскольку внешние события не обрабатываются в 1С на сервере (а вам возможно нужно было это делать именно там), вы реализовали внутреннюю очередь сообщений?

  5. В https://github.com/CopernicaMarketingSoftware/AMQP-CPP/blob/master/include/amqpcpp/deferredget.h и здесь https://github.com/CopernicaMarketingSoftware/AMQP-CPP/issues/156 автор библиотеки пишет, что вроде как разницы между событиями .onMessage, .onReceived и .onSuccess нет.

  6. Следовательно, для реализации фичи нам нужен еще один параметр для вашего метода basicConsume. Что-то типа bool enableExternalEvents:

    • если true, то шлем тело сообщения и тег сразу в 1С (и там уже разбираемся, акать или не акать);
    • если false, то пишем сообщения в память компоненты, чтобы затем мочь их читать на сервере.

Так?

ripreal commented 4 years ago

Примерно вусе так. Только не стоит нагружать метод RabbitMQClient::basicConsume дополниетлньой логикой. ЛУчше сделать отдельный метод для инициализации чтения сообщений во внешние события

antonwantstosleep commented 4 years ago

Никак не могу понять (я тупой + c++ слишком сложный).

Подвешивать коллбек на .onMessage нужно в файле RabbitMQClient.cpp (например, в методе RabbitMQClient::basicConsume ну или в ином, отдельном RabbitMQClient::basicConsumeToExternalEvents, который такой же, только другой).

А Метод RabbitMQClient::basicConsumeToExternalEvents я вызываю из метода AddInNative::basicConsumeToExternalEvents из файла AddInNative.cpp. В том же файле для удобства и красоты я написал метод AddInNative::sendEvent, который дергает метод ExternalEvent живущего в том же файле m_iConnect.

Как мне поработать с AddInNative::sendEvent в коллбеке на .onMessage?

Нужно передать весь класс AddInNative (ссылку &this ?) в параметре при вызове RabbitMQClient::basicConsumeToExternalEvents? И сделать #include "AddInNative.h" в RabbitMQClient.h?

Или передавать только m_iConnect (и делать include IAddInDefBaseEx)?

Или можно как-то одну функцию AddInNative::sendEvent туда передать? Да еще и с живым m_iConnect?

Спасибо, что тратите на меня время.

ripreal commented 4 years ago

Ничего не понял. Мне проще видеть код и ревьюить готовый пулл-реквест. Так я смогу сказать что правильно, а что не очень. Коллбеки делайте по аналогии как в других методах. Есть настроена отладка достаточно просто методом тыка будет найти оптимальный вариант

antonwantstosleep commented 4 years ago

@ripreal , было сложно, но я настроил отладку под Windows в VS2019. Теперь могу ставить точки останова и в 1С, и в VS. Но свою проблему я не решил.

Пока мы находимся в файле СAddInNative.cpp (я тестировал проект для Windows для простоты), у нас видно this (объект класса CAddInNative) и m_iConnect (я так понимаю, это указатель на переменную, где хранится объект класса IAddInDefBase).

Когда мы вызываем метод RabbitMQClient, то переходим в файл RabbitMQClient.cpp, где ничего "старого" уже нет - this хранит объект класса RabbitMQClient.

И как в этот момент получить доступ:

ripreal commented 4 years ago

Это можно сделать. Для передачи указателя на m_iConnect в класс RabbitMQClient используются каллбеки. С ними придется повозиться https://stackoverflow.com/questions/2298242/callback-functions-in-c. Каллбеки могут быт ьнаписаны по-разному. Рекомендую использовать С++ 2011 стиля.