Pavel P
Pavel P

Reputation: 16843

simple networking tcp communication with a server using Boost.ASIO

I've used ASIO around 10 years ago (I recall back then there was another boost.netwrok lib for brief amount of time), since then I've been using my own networking code or some other implementations or languages (e.g. ACE, nodejs etc). Now I want to write simple code using ASIO and I cannot have enough words to describe how much I hate it.

First of all, it's simple sync code that I'm after, it's only a few lines of BSD/winsock code and it's complete mess using asio. Not only all the code looks like mouthful of messy stuff that masks real logic, it also forces me to use some things that I would really like to avoid like plague.

So, at first, I describe what I need to do: 1) connect to server at mymagichost.com : 1234 In ASIO this part even though looks like mouthful of namespace talk is more or less ok:

#include <boost/asio.hpp>
// I'm scared what the code would look like without this one.
using boost::asio::ip::tcp; 
int main()
{
    boost::asio::io_service io_service;
    tcp::socket sock(io_service);
    tcp::resolver resolver(io_service);
    boost::asio::connect(sock, resolver.resolve(
        tcp::resolver::query(tcp::v4(), "mymagichost.com", "1234")));
}

I simply don't get how is that even possible that I cannot pass 1234 as an int, does that mean that passing a string here pulls in entire huge list of service names into my binary just to be able to figure out that I need port 1234?! WTF?

When connected, I need to do this simple talk with the server:

// when I start talking I say PING\n to the server
"PING\n"

// it replies back with 64-bit unsigned int id
"PONG <connection id>\n"

// I ask server to execute command, where <cmd id>
// is an int and connection id is that number that
// server sent to me in PONG.
"COMMAND <cmd id>:<connection id>\n"

// server executes my command
"OK <message id>\n<binary data until remote closes socket>"

With blocking bsd socks it's like 10-20 lines of code:

void talk(int sock)  // consider it pseudo-code
{
    char buf[1024];
    int cmd_id = 1234;
    send(sock, "PING\n", 5, 0);
    unsigned long long connection_id, msg_id;
    recv(sock, buf, sizeof(buffer), 0);
    sscanf(buf, "PONG %llu\n", &connection_id);
    int n = sprintf(buf, "COMMAND %u:llu\n", cmd_id, connection_id);
    send(sock, buf, n, 0);
    vector<char> data;
    for (; (n = recv(sock, buf, sizeof(buf), 0)) > 0;)
       data.insert(data.end(), buf, buf+n);
    sscanf(buf, "OK %llu\n%n", &msg_id, &n);
    char *binary_data = buf.data()+n;
    int binary_data_size = data.size()-n;
}

So, how this can be done in sync asio? In my BSD code I have lots of shortcomings: when receiving replies I might have received partial reply, e.g. I'd need to loop on receiving until trailing '\n' is received (perhaps that wouldn't happen is this simple example). Among many functions to receive data there is one that does exactly that:

boost::asio::read_until(sock, buf, '\n');

However, unlike boost::asio::read, read_until accepts only streambuf (which I really want to avoid like plague). I just don't get, why it wouldn't allow me to use my own fixed buffer, is it because asio dev decided to prevent me from shooting in my foot? I still can deference null pointer is I want to. Anyways, not a big deal, I can use streambuf, but here things get real ugly. According to the docs, after I read_until last '\n' in server OK reply buffer might actually contain more data, here's what the docs say: "After a successful read_until operation, the streambuf may contain additional data beyond the delimiter. An application will typically leave that data in the streambuf for a subsequent read_until operation to examine. ". Now after having handled the last OK reply from the server, I really have no choice but to keep using dreaded streambuf to read-in all remaining data (megs of it), even though I'd prefer to loop and append to my final buffer (a string or a vector). When I'm finished reading in the data all methods to get it from stream buf I pretty bad: copy data multiple times or use istream_iterator/istreambuf_iterator to append it to my container. Both of the are extremely slow in my opinion, my visual studio literally hangs for a couple of seconds (I don't receive gigs of data by the way).

So, what's the proper way to handle it with asio?

Upvotes: 1

Views: 659

Answers (1)

sehe
sehe

Reputation: 392999

Firstly, the address can not be assumed to be numerical, because you're asking for it to be resolved. There's nothing that says the resolver cannot avoid loading the services definitions if it sees the port is numeric, AFAICT.

Secondly, there are a few high-level primitives in ASIO. You could use it:

void talk(std::iostream& sock)  // consider it pseudo-code
{
    int cmd_id = 1234;
    sock << "PING" << std::endl;

    uint64_t connection_id;

    if (sock >> connection_id) {
        sock << "COMMAND " << cmd_id << ":" << connection_id << std::endl;

        char buf[1024];
        std::vector<char> data;
        // read till eof
        while (sock.read(buf, sizeof(buf))
            data.insert(data.end(), buf, buf+sizeof(buf));
        data.insert(data.end(), buf, buf+sock.gcount());
    }

    uint64_t msg_id;
    int n;
    if (sock >> qi::phrase_match("OK" >> qi::auto_ >> '\n' >> qi::auto_, qi::blank, msg_id, n)) {
        // do something with n and msg_id
    }
}

int main() {
    boost::asio::ip::tcp::iostream s { "127.0.0.1", "1234" };
    talk(s);
}

See it Live On Coliru

If you wanted to do without spirit, you would likely hack around things with a getline and similar sscanf contraptions.

Upvotes: 1

Related Questions