Skip to content

Lightweight C++ HTTP client and server library based on Asio for embedding purpose.

License

Notifications You must be signed in to change notification settings

sprinfall/webcc

Repository files navigation

Webcc - C++ HTTP Library

中文版 README

Lightweight C++ HTTP client and server library based on Boost Asio for embedding purpose.

=> Build Instructions (中文版)

Git repo: https://github.com/sprinfall/webcc. Please check this one instead of the forked for the latest features.

Contents

Overview

  • Cross-platform: Windows, Linux and MacOS
  • Easy-to-use client API inspired by Python requests
  • IPv6 support
  • SSL/HTTPS support with OpenSSL
  • GZip compression support with Zlib (optional)
  • Persistent (Keep-Alive) connections
  • Data streaming
    • for uploading and downloading large files on client
    • for serving and receiving large files on server
  • Basic & Token authorization
  • Timeout control
  • Source code follows Google C++ Style
  • Automation tests and unit tests included

Client API

A Complete Example

Let's start from a complete client example:

#include <iostream>

#include "webcc/client_session.h"
#include "webcc/logger.h"

int main() {
  // Configure logger to print only to console.
  WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
  
  // Session provides convenient request APIs, stores request configurations
  // and manages persistent connenctions.
  webcc::ClientSession session;

  // Catch exceptions for error handling.
  try {
    // Send a HTTP GET request.
    auto r =
        session.Send(webcc::RequestBuilder{}.Get("http://httpbin.org/get")());

    // Print the response data.
    std::cout << r->data() << std::endl;

  } catch (const webcc::Error& error) {
    std::cerr << error << std::endl;
  }

  return 0;
}

Request Builder

As you can see, a helper class named RequestBuilder is used to chain the parameters and finally build a request object. Please pay attention to the () operator.

URL query parameters can be easily added through Query() method:

session.Send(webcc::RequestBuilder{}
                 .Get("http://httpbin.org/get")
                 .Query("key1", "value1")
                 .Query("key2", "value2")());

Adding additional headers is also easy:

session.Send(webcc::RequestBuilder{}
                 .Get("http://httpbin.org/get")
                 .Header("Accept", "application/json")());

HTTPS

Accessing HTTPS has no difference from HTTP:

session.Send(webcc::RequestBuilder{}.Get("https://httpbin.org/get")());

NOTE: The HTTPS/SSL support requires the build option WEBCC_ENABLE_SSL to be enabled.

Invoking GitHub REST API

Listing GitHub public events is not a big deal:

auto r = session.Send(
    webcc::RequestBuilder{}.Get("https://api.github.com/events")());

You can then parse r->data() to JSON object with your favorite JSON library. My choice for the examples is jsoncpp. But Webcc itself doesn't understand JSON nor require one. It's up to you to choose the most appropriate JSON library.

RequestBuilder provides a lot of functions for you to customize the request. Let's see more examples.

Authorization

In order to list the followers of an authorized GitHub user, you need either Basic Authorization:

session.Send(webcc::RequestBuilder{}.
             Get("https://api.github.com/user/followers").
             AuthBasic(<login>, <password>)
             ());

Or Token Authorization:

session.Send(webcc::RequestBuilder{}
                 .Get("https://api.github.com/user/followers")
                 .AuthToken(token)());

Keep-Alive

Though Keep-Alive (i.e., Persistent Connection) is a good feature and enabled by default, you can turn it off:

auto r = session.Send(
    webcc::RequestBuilder{}.Get("http://httpbin.org/get").KeepAlive(false)());

The API for other HTTP requests is no different from GET.

POST Request

A POST request needs a body which is normally a JSON string for REST API. Let's post a small UTF-8 encoded JSON string:

session.Send(webcc::RequestBuilder{}
                 .Post("http://httpbin.org/post")
                 .Body("{'name'='Adam', 'age'=20}")
                 .Json()
                 .Utf8()());

The body of a POST request could be any content other than JSON string.

It could be the binary data of a file. See Uploading Files.

It could be a form urlencoded string:

webcc::ClientSession session;
session.SetContentType("application/x-www-form-urlencoded", "utf8");

// Use UrlQuery to compose the urlencoded string.
// Don't use RequestBuilder::Query() which is dedicated to GET.
webcc::UrlQuery query;
query.Add("key1", "value1");
query.Add("key2", "value2");
// ...

auto r = session.Send(webcc::RequestBuilder{}
                          .Post("http://httpbin.org/post")
                          .Body(query.ToString())());

Please see examples/form_urlencoded_client.cc for more details.

Downloading Files

Webcc has the ability to stream large response data to a file. This is especially useful when downloading files.

// stream = true
auto r = session.Send(
    webcc::RequestBuilder{}.Get("http://httpbin.org/image/jpeg")(), true);

// Move the streamed file to your destination.
r->file_body()->Move("./wolf.jpeg");

Uploading Files

Streaming is also available for uploading:

auto r = session.Send(
    webcc::RequestBuilder{}.Post("http://httpbin.org/post").File(path)());

The file will not be loaded into the memory all at once, instead, it will be read and sent piece by piece.

Please note that Content-Length header will still be set to the true size of the file, this is different from the handling of chunked data (Transfer-Encoding: chunked).

Client API and Threads

A ClientSession shouldn't be used by multiple threads to send requests.

The state functions, Start(), Stop() and Cancel(), are thread safe. E.g., you can call Send() in thread A and call Stop() in thread B. Please see examples/heartbeat_client for more details.

Example:

void ThreadedClient() {
  std::vector<std::thread> threads;

  for (int i = 0; i < 3; ++i) {
    threads.emplace_back([]() {
      webcc::ClientSession session;

      try {
        auto r = session.Send(
            webcc::RequestBuilder{}.Get("http://httpbin.org/get")());

        std::cout << r->data() << std::endl;

      } catch (const webcc::Error&) {
      }
    });
  }

  for (auto& t : threads) {
    t.join();
  }
}

Server API

A Minimal Server

The following example is a minimal yet complete HTTP server.

Start it, open a browser with localhost:8080, you will see Hello, World! as response.

#include "webcc/logger.h"
#include "webcc/response_builder.h"
#include "webcc/server.h"

class HelloView : public webcc::View {
public:
  webcc::ResponsePtr Handle(webcc::RequestPtr request) override {
    if (request->method() == "GET") {
      return webcc::ResponseBuilder{}.OK().Body("Hello, World!")();
    }

    return {};
  }
};

int main() {
  try {
    webcc::Server server{ boost::asio::ip::tcp::v4(), 8080 };

    server.Route("/", std::make_shared<HelloView>());

    server.Run();

  } catch (const std::exception&) {
    return 1;
  }

  return 0;
}

URL Route

The Route() method routes different URLs to different views.

You can route different URLs to the same view:

server.Route("/", std::make_shared<HelloView>());
server.Route("/hello", std::make_shared<HelloView>());

Or even the same view object:

auto view = std::make_shared<HelloView>();
server.Route("/", view);
server.Route("/hello", view);

But normally a view only handles a specific URL (see the Book Server example).

The URL could be regular expressions. The Book Server example uses a regex URL to match against book IDs.

Finally, it's always suggested to explicitly specify the HTTP methods allowed for a route:

server.Route("/", std::make_shared<HelloView>(), { "GET" });

Running A Server

The last thing about server is Run():

void Run(std::size_t workers = 1, std::size_t loops = 1);

Workers are threads which will be waken to process the HTTP requests once they arrive. Theoretically, the more workers you have, the more concurrency you gain. In practice, you have to take the number of CPU cores into account and allocate a reasonable number for it.

The loops means the number of threads running the IO Context of Asio. Normally, one thread is good enough, but it could be more than that.

Response Builder

The server API provides a helper class ResponseBuilder for the views to chain the parameters and finally build a response object. This is exactly the same strategy as RequestBuilder.

REST Book Server

Suppose you want to create a book server and provide the following operations with RESTful API:

  • Query books based on some criterias.
  • Add a new book.
  • Get the detailed information of a book.
  • Update the information of a book.
  • Delete a book.

The first two operations are implemented by BookListView deriving from webcc::View:

class BookListView : public webcc::View {
public:
  webcc::ResponsePtr Handle(webcc::RequestPtr request) override {
    if (request->method() == "GET") {
      return Get(request);
    }
    if (request->method() == "POST") {
      return Post(request);
    }
    return {};
  }
  
private:
  // Get a list of books based on query parameters.
  webcc::ResponsePtr Get(webcc::RequestPtr request);

  // Create a new book.
  // The new book's data is attached as request data in JSON format.
  webcc::ResponsePtr Post(webcc::RequestPtr request);
};

Other operations are implemented by BookDetailView:

class BookDetailView : public webcc::View {
public:
  webcc::ResponsePtr Handle(webcc::RequestPtr request) override {
    if (request->method() == "GET") {
      return Get(request);
    }
    if (request->method() == "PUT") {
      return Put(request);
    }
    if (request->method() == "DELETE") {
      return Delete(request);
    }
    return {};
  }
  
protected:
  // Get the detailed information of a book.
  webcc::ResponsePtr Get(webcc::RequestPtr request);

  // Update a book.
  webcc::ResponsePtr Put(webcc::RequestPtr request);

  // Delete a book.
  webcc::ResponsePtr Delete(webcc::RequestPtr request);
};

The detailed implementation is out of the scope of this README, but here is an example:

webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) {
  if (request->args().size() != 1) {
    // NotFound means the resource specified by the URL cannot be found.
    // BadRequest could be another choice.
    return webcc::ResponseBuilder{}.NotFound()();
  }

  const std::string& book_id = request->args()[0];

  // Get the book by ID from, e.g., the database.
  // ...

  if (<NotFound>) {
    // There's no such book with the given ID. 
    return webcc::ResponseBuilder{}.NotFound()();
  }

  // Convert the book to JSON string and set as response data.
  return webcc::ResponseBuilder{}.OK().Data(<JsonStringOfTheBook>).
      Json().Utf8()();
}

Last step, route URLs to the proper views and run the server:

int main(int argc, char* argv[]) {
  // ...

  try {
    webcc::Server server{ boost::asio::ip::tcp::v4(), 8080 };

    server.Route("/books",
                 std::make_shared<BookListView>(),
                 { "GET", "POST" });

    server.Route(webcc::R("/books/(\\d+)"),
                 std::make_shared<BookDetailView>(),
                 { "GET", "PUT", "DELETE" });

    server.Run();

  } catch (const std::exception& e) {
    std::cerr << e.what() << std::endl;
    return 1;
  }

  return 0;

Please see examples/book_server for more details.

IPv6 Support

IPv6 Server

Only need to change the protocol to boost::asio::ip::tcp::v6():

webcc::Server server{ boost::asio::ip::tcp::v6(), 8080 };

IPv6 Client

Only need to specify an IPv6 address:

session.Send(webcc::RequestBuilder{}.Get("http://[::1]:8080/books")());