A client, part 2

104

You've already learned what's the difference between a client and a server, how to resolve a hostname into IP address, and how to connect to a given endpoint. Before that, you've learned how to send and receive data via established TCP connection. In this lesson we will bring all these together and write a very simple client application.

Our client application will do the following:

  1. Get the hostname from the commandline argument;
  2. Resolve the given hostname into IP address;
  3. Connect to the IP address resolved to HTTP port (80);
  4. Send HTTP GET request;
  5. Receive HTTP response and output it into stdout;
  6. Do everything in an asynchronous manner.

To keep things simple I've omitted error handling. Here is the complete example:

#include <iostream>
#include <boost/asio.hpp>

namespace io = boost::asio;
namespace ip = io::ip;
using tcp = ip::tcp;
using error_code = boost::system::error_code;
using namespace std::placeholders;

class application
{
public:

    application(io::io_context& io_context, std::string const& hostname)
    : resolver(io_context)
    , socket  (io_context)
    {
        request = "GET / HTTP/1.1\n"
                  "Host: " + hostname + "\n"
                  "Connection: close\n\n";

        resolver.async_resolve(hostname, "http", std::bind(&application::on_resolve, this, _1, _2));
    }

private:

    void on_resolve(error_code error, tcp::resolver::results_type results)
    {
        std::cout << "Resolve: " << error.message() << "\n";
        io::async_connect(socket, results, std::bind(&application::on_connect, this, _1, _2));
    }

    void on_connect(error_code error, tcp::endpoint const& endpoint)
    {
        std::cout << "Connect: " << error.message() << ", endpoint: " << endpoint << "\n";
        io::async_write(socket, io::buffer(request), std::bind(&application::on_write, this, _1, _2));
    }

    void on_write(error_code error, std::size_t bytes_transferred)
    {
        std::cout << "Write: " << error.message() << ", bytes transferred: " << bytes_transferred << "\n";
        io::async_read(socket, response, std::bind(&application::on_read, this, _1, _2));
    }

    void on_read(error_code error, std::size_t bytes_transferred)
    {
        std::cout << "Read: " << error.message() << ", bytes transferred: " << bytes_transferred << "\n\n"
                  << std::istream(&response).rdbuf() << "\n";
    }

    tcp::resolver resolver;
    tcp::socket socket;
    std::string request;
    io::streambuf response;
};

int main(int argc, char* argv[])
{
    io::io_context io_context;
    application app(io_context, argv[0]);
    io_context.run();
    return 0;
}

Compile it, run in the commandline and pass google.com as an argument. You should see something like this:

./application google.com

Resolve: The operation completed successfully
Connect: The operation completed successfully, endpoint: 64.233.161.138:80
Write: The operation completed successfully, bytes transferred: 51
Read: End of file, bytes transferred: 547

HTTP/1.1 301 Moved Permanently
Location: http://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Wed, 25 Mar 2020 19:25:28 GMT
Expires: Fri, 24 Apr 2020 19:25:28 GMT
Cache-Control: public, max-age=2592000
Server: gws
Content-Length: 219
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
Connection: close

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

So, just 60+ lines of code! There are several things which we should mention.

In this example I've composed HTTP GET request manually:

request = "GET / HTTP/1.1\n"
          "Host: " + hostname + "\n"
          "Connection: close\n\n";

This is freakishly low-level approach which you shouldn't use in your production code. Instead, you should use Boost.Beast which provides HTTP composer and parser. We will learn how to use Boost.Beast a bit later.

Now, let's look at the connect function call:

io::async_connect(socket, results, std::bind(&application::on_connect, this, _1, _2));

That's something new. io::async_connect is a free-function and it is designed to work with a range of endpoints. It iterates over the given endpoints and tries to connect to each of them until success. Upon success, it stops and passes successfully connected endpoint into the completion handler. It is convenient to use this function when you deal with resolver::resolve result.

Next thing we should look at is reading of the response:

io::async_read(socket, response, std::bind(&application::on_read, this, _1, _2));

boost::asio::async_read function will read the data until one of the following:

  1. The given buffer is full;
  2. The connection has been closed;
  3. An error has occurred.

If none of these has happend, and the server doesn't send anything but just keep the connection alive, the function won't call its completion handler. That's why I've put Connection: close header into the request. In case of Connection: keep-alive you've could use boost::asio::async_read_until function to handle the data properly. We will discuss how to use this function later. Still, in the example above we've asked server to close the connection and that's why we see this in the output window:

Read: End of file, bytes transferred: 547

And the last thing, is the response status:

HTTP/1.1 301 Moved Permanently
Location: http://www.google.com/

We've asked for http://google.com/ and the server replied that we should navigate to http://www.google.com/ instead. Which means that in terms of TCP our client works perfectly.

Share this page:

Learning plan

14. Timers
Working with asynchronous timers within io_context polling loop
What's the difference between a client and a server, and what do they have in common
Resolving hostnames into IP addresses before connect
17. A client, part 2
Writing a very simple client application in C++ with Boost.Asio
How to deal with completion handlers manually to combine Boost.Asio with other APIs
Let's take a break and briefly look across everything we've learned so far
A closer look on how to pass data views into Boost.Asio functions