Как я могу прочитать все доступные данные с помощью boost :: asio's async_read_some (), не дожидаясь поступления новых данных? - PullRequest
0 голосов
/ 06 ноября 2019

Я использую boost :: asio для последовательной связи, и я хотел бы прослушивать входящие данные через определенный порт. Итак, я регистрирую ReadHandler, используя serialport::async_read_some(), а затем создаю отдельный поток для обработки асинхронных обработчиков (вызовы io_service::run()). Мой ReadHandler перерегистрирует себя в своем конце, снова вызывая async_read_some(), что, кажется, является общим шаблоном.

Это все работает, и мой пример может печатать данные на стандартный вывод по мере их получения - за исключением того, что я 'мы заметили, что данные, полученные во время работы ReadHandler, не будут «считываться», пока не будет завершено выполнение ReadHandler и после этого будут получены новые данные. То есть, когда данные получены во время работы ReadHandler, хотя async_read_some вызывается по завершении ReadHandler, он не будет немедленно снова вызывать ReadHandler для этих данных. ReadHandler будет вызываться снова, только если дополнительные данные получены после завершения первоначального ReadHandler. На этом этапе данные, полученные во время работы ReadHandler, будут правильно храниться в буфере вместе с «новыми» данными.

Вот мой пример минимального жизнеспособности - я изначально поместил его в Wandbox, но понял, что он выигралне помогает скомпилировать его в Интернете, потому что для его запуска все равно требуется последовательный порт.

// Include standard libraries
#include <iostream>
#include <string>
#include <memory>
#include <thread>

// Include ASIO networking library
#include <boost/asio.hpp>

class SerialPort
{
public:
    explicit SerialPort(const std::string& portName) :
        m_startTime(std::chrono::system_clock::now()),
        m_readBuf(new char[bufSize]),
        m_ios(),
        m_ser(m_ios)
    {
        m_ser.open(portName);
        m_ser.set_option(boost::asio::serial_port_base::baud_rate(115200));

        auto readHandler = [&](const boost::system::error_code& ec, std::size_t bytesRead)->void
        {
            // Need to pass lambda as an input argument rather than capturing because we're using auto storage class
            // so use trick mentioned here: http://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/
            // and here: https://stackoverflow.com/a/40873505
            auto readHandlerImpl = [&](const boost::system::error_code& ec, std::size_t bytesRead, auto& lambda)->void
            {
                if (!ec)
                {
                    const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - m_startTime);

                    std::cout << elapsed.count() << "ms: " << std::string(m_readBuf.get(), m_readBuf.get() + bytesRead) << std::endl;

                    // Simulate some kind of intensive processing before re-registering our read handler
                    std::this_thread::sleep_for(std::chrono::seconds(5));

                    //m_ser.async_read_some(boost::asio::buffer(m_readBuf.get(), bufSize), lambda);
                    m_ser.async_read_some(boost::asio::buffer(m_readBuf.get(), bufSize), std::bind(lambda, std::placeholders::_1, std::placeholders::_2, lambda));
                }
            };

            readHandlerImpl(ec, bytesRead, readHandlerImpl);
        };

        m_ser.async_read_some(boost::asio::buffer(m_readBuf.get(), bufSize), readHandler);

        m_asioThread = std::make_unique<std::thread>([this]()
            {
                this->m_ios.run();
            });
    }

    ~SerialPort()
    {
        m_ser.cancel();
        m_asioThread->join();
    }

private:
    const std::chrono::system_clock::time_point m_startTime;
    static const std::size_t bufSize = 512u;
    std::unique_ptr<char[]> m_readBuf;
    boost::asio::io_service m_ios;
    boost::asio::serial_port m_ser;
    std::unique_ptr<std::thread> m_asioThread;
};


int main()
{
    std::cout << "Type q and press enter to quit" << std::endl;
    SerialPort port("COM1");

    while (std::cin.get() != 'q')
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }

    return 0;
}

(Не обращайте внимания на происходящие странные лямбда-выражения)

Эта программа просто печатает данные встандартный вывод при получении вместе с отметкой времени (миллисекунды с момента запуска программы). Подключив виртуальное последовательное устройство к паре виртуальных последовательных портов, я могу отправлять данные в программу (правда, просто набрав RealTerm). Я вижу проблему, когда набираю короткую строку.

В этом случае я набрал «hi», и «h» было напечатано немедленно. Вскоре после этого я набрал 'i', но на скорости компьютера это было довольно долго, поэтому он не был частью начальных данных, считанных в буфер. В этот момент выполняется ReadHandler, который занимает 5 секунд. В течение этого времени ОС получила «я». Но 'i' не печатается после истечения 5 секунд - следующий async_read_some игнорирует его, пока я не наберу 't', и в этот момент он неожиданно напечатает 'i' и 't'. Пример вывода программы

Вот более четкое описание этого теста и то, что я хочу:

Тест: Запустите программу, подождите 1 секунду, введите привет, подождите 9 секунд, введитеt
То, что я хочу, (выводится на стандартный вывод этой программой):
1000 мс: ч
6010 мс: i
11020 мс: t

Что на самом деле происходит:
1000 мс: ч
10000 мс: ит

Кажется, очень важно, чтобы программа имела способ распознавать данные, полученные между чтениями. Я знаю, что нет способа проверить, доступны ли данные (в буфере ОС), используя последовательные порты ASIO (в любом случае без использования native_handle). Но мне это и не нужно, пока вызов чтения возвращается. Одним из решений этой проблемы может быть просто убедиться, что ReadHandler завершает работу как можно быстрее - очевидно, что 5-секундная задержка в этом примере надумана. Но это не кажется мне хорошим решением;независимо от того, насколько быстро я делаю ReadHandler, все равно можно будет «пропустить» данные (в том смысле, что они не будут видны, пока не будут получены новые данные позже). Есть ли способ гарантировать , что мой обработчик будет читать все данные в течение короткого времени после их получения, независимо от получения дополнительных данных?

Я сделал многопоиск SO и в других местах, но все, что я нашел до сих пор, это просто обсуждение других ловушек, которые приводят к тому, что система вообще не работает.

В качестве крайней меры, похоже, что мой рабочий поток может вызвать вызов io_service::run_for() с тайм-аутом, а не run(), и затем каждый короткий промежуток времени, пока этот поток каким-то образом вызывает ручное чтение. Я не уверен, какую форму он примет, я думаю, он может просто позвонить serial_port::cancel(), а затем повторно позвонить async_read_some. Но для меня это звучит неприлично, даже если это может сработать - и для загрузки потребуется более новая версия boost.

Я собираюсь с boost 1.65.1 на Windows 10 с VS2019, но я действительнонадеюсь, что это не имеет отношения к этому вопросу.

1 Ответ

1 голос
/ 06 ноября 2019

Отвечая на вопрос в заголовке: Вы не можете. По характеру async_read_some вы запрашиваете частичное чтение и вызов вашего обработчика, как только что-нибудь прочитаете. Затем вы долго спите, прежде чем будет вызван еще один async_read_some.

, независимо от того, насколько быстро я выполняю ReadHandler, все равно будет возможно «пропустить» данные (поскольку они не будутдо тех пор, пока не будут получены новые данные)

Если я правильно понимаю вашу проблему, это не имеет значения - вы ничего не пропустите. Данные все еще ожидают в буфере сокета / порта, пока вы не прочитаете их в следующий раз.

Если вы хотите начать обработку только после завершения чтения, вам нужен один из async_read перегрузки вместо. Это, по существу, будет выполнять несколько read_somes в потоке, пока не будет выполнено какое-то условие. Это может означать все на порте / сокете, или вы можете указать несколько пользовательских CompletionCondition. Он вызывается для каждого read_some до тех пор, пока он не вернет 0, после чего чтение считается завершенным, а затем вызывается ReadHandler.

...