2

I have a remote server that constantly sends a message like this to my pc:

{Heartbeat}

Furthermore, the remote server listens to my command messages that I send from my pc connected to the remote server, using a string in json format, for example:

{'H': '1', 'N': 3, 'D1': 3, 'D2': 150}

The remote server then sends me the response like, for example:

{1:ok}

Sometimes, however, it happens that the answer is drowned out by the various heartbeats, for example:

{Heartbeat}{Heartbeat}{Heartbeat}{1:ok}{Heartbeat}{Heartbeat}

How can I filter this long sequence {...} extracting only the useful information {1:ok} using C++17 ? As library for TCP/IP communication I use boost ASIO 1.81.0.

For TCP/IP communication in C++17 I make use of a TCPClient class. In main I send this command to the server:

client->Send_cmd("{\'H\': \'1\', \'N\': 3, \'D1\': 3, \'D2\': 150}");

As seen in the TCPClient class I use as a member function to send a message to the server, the following:

void TCPClient::Send_cmd(std::string data)
{
    // search for substring
    std::string ricerca = "";
    std::string response = "";
    std::string response_tmp = "";
    std::string::iterator i1; // Declaration of an iterator to store the returned pointer

    boost::system::error_code error;

    // result indicates the number of data bytes transmitted
    auto result = boost::asio::write(*m_socket, boost::asio::buffer(data), error);
    if (!error) {
        esito_cmd_al_server = "Client sent the command";
    }
    else {
        esito_cmd_al_server = "send failed: " + error.message();
    }

    // We just have to read the response from the server
    for (;;)
    {
        // We use a boost::array to hold the received data.
        // Instead of a boost::array, we could have used a char [] or std::vector.
        //boost::array<char, 128> buf;
        boost::asio::streambuf buf2;
        boost::system::error_code error;

        // The boost::asio::buffer() function automatically determines the size 
        // of the array to help prevent buffer overflow.
        //size_t len = m_socket->read_some(boost::asio::buffer(buf), error);
        size_t len2 = boost::asio::read_until(*m_socket, buf2, "}");

        // for correctness I keep the response on the first {...} and if there are other {...} I delete them
        ricerca = "}";
        response = boost::asio::buffer_cast<const char*>(buf2.data());
        // look for the first occurrence of "}"
        i1 = std::search(response.begin(), response.end(), ricerca.begin(), ricerca.end());
        // if it encountered "}", then delete the rest of the response
        if (i1 != response.end())
        {
            std::string finale2 = response.substr(0, (i1 - response.begin()) + 1);
            response = finale2;
        }

        // check that the incoming message from the server-robot is not a "{Heartbeat}"
        std::string search = "{Hea";
        i1 = std::search(response.begin(), response.end(), search.begin(), search.end());
        // if it encountered "{Heartbeat}", then repeat the for loop
        if (i1 != response.end())
        {
            response = "";
            continue;
        }

        response_tmp = response;

        // When the server closes the connection, the function boost::asio::ip::tcp::socket::read_some()
        // will exit with error boost::asio::error::eof 
        if (error == boost::asio::error::eof)
            break; // Connection closed cleanly by peer.
        else if (error)
            throw boost::system::system_error(error); // Some other error.

        //std::string tmp_str(buf.data(), len);
        //std::string tmp_str2 = boost::asio::buffer_cast<const char*>(buf2.data());
        //esito_cmd_al_server = tmp_str2;
        esito_cmd_al_server = response_tmp;
        if ("{Heartbeat}" == esito_cmd_al_server)
            continue;
        else
            break;
    }
}

Is there a more correct way (mine seems a bit cumbersome) to carry out the filtering I wrote ?

malloy
  • 21
  • 3

1 Answers1

0

I'd use the istream interface, since you're already using the streambuf.

Note that you need to keep the buffer as a member variable, as read_until may read beyond the delimiter and you usually don't want to loose that data.

On the other hand, I would not store the response/result in members, instead return them (either by return value or exception, as you already partly implemented):

read_until(*m_socket, m_readbuf, "}");

std::string msg;
getline(std::istream(&m_readbuf), msg, '}'); // excl '}'


if (msg == "{Heartbeat")
    continue;

return msg;

If you insist, you can do the parsing manually but really consider using string views for efficiency as well as elegance (see e.g. "Everybody hates the string/string_view interface, but it is quite powerful if you find the sweet spot.").

Of course, since it looks like almost JSON, consider using that with e.g. boost::json::stream_parser

Live Demo

Live On Coliru

#include <boost/asio.hpp>
#include <iomanip>
#include <iostream>
namespace asio = boost::asio;
using asio::ip::tcp;

struct TCPClient {
    std::string Send_cmd(std::string const&);

    // private:
    asio::io_context             m_io;
    std::unique_ptr<tcp::socket> m_socket{new tcp::socket(m_io)};
    asio::streambuf              m_readbuf;
};

std::string TCPClient::Send_cmd(std::string const& data) try {
    write(*m_socket, asio::buffer(data));
    for (;;) { // wait for response
        read_until(*m_socket, m_readbuf, "}");

        std::string msg;
        getline(std::istream(&m_readbuf), msg, '}');
        msg += '}'; // getline excludes delimiter

        std::cerr << "Debug: " << quoted(msg) << std::endl;
        if (msg != "{Heartbeat}")
            return msg;
    }
} catch (boost::system::system_error const& se) {
    if (se.code() != asio::error::eof)
        throw;
    return ""; // closed by peer
}

int main() {
    TCPClient demo;
    demo.m_socket->connect({{}, 7878});

    std::string response = demo.Send_cmd("{'H': '1', 'N': 3, 'D1': 3, 'D2': 150}");
    std::cout << "Response: " << quoted(response) << std::endl;
}

Testing with a fake server like

(for a in {1..3}; do sleep 2; echo -n '{Heartbeat}'; done; echo -n "{'42': 'ok'}") | netcat -lp 7878

enter image description here

BONUS: Asynchronous

A heartbeat protocol implies asynchronous/full duplex IO. You're using synchronous IO, which creates the "problem". Consider using a separate read-loop and send-loop and code that matches responses to requests. An example of a protocol that uses that is here: Chrome DevTools Protocol using Boost Beast

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Thank you for your explanations sehe. I would be interested in being able to write the TCPClient class as asynchronous. Could you tell me where to find an example of asynchronous client using boost asio ? I couldn't find any, they are always synchronous. – malloy Jun 14 '23 at 17:52
  • The majority are asynchronous, as that's the purpose of the Asio library (it's in the name!). All examples [are in both flavours where relevant](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/examples.html). Mind you, some advanced examples are only in asynchronous. Besides, my answer linked to a complete example. – sehe Jun 14 '23 at 19:44
  • What you can also do is describe the task / show the current code and I could help you get started. You can also just do that, and post a new question if you have any – sehe Jun 14 '23 at 19:45
  • 1
    I follow your advice sehe. I go to the boost site and take the examples of clients and servers present: -example_cpp03_echo_async_tcp_echo_server.cpp - 1.81.0 -example_cpp03_echo_blocking_tcp_echo_client.cpp - 1.81.0 -example_cpp03_timeouts_async_tcp_client.cpp - 1.81.0 to see how things should be set up and then I try to write one in which the client sends a message and the server replies by sending another message. – malloy Jun 15 '23 at 16:28