Unit Testing a http rest client implemented using boost::beast - c++

I have an HTTP REST client implemented using boost::beast.
RESTClient.hpp
#pragma once
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/strand.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
// Performs an HTTP GET and prints the response
class RESTClient : public std::enable_shared_from_this<RESTClient> {
public:
explicit RESTClient(net::io_context& ioc);
virtual ~RESTClient();
virtual void run(char const* host, char const* port, char const* target, int version);
virtual void onResolve(beast::error_code ec, tcp::resolver::results_type results);
virtual void onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type);
virtual void onWrite(beast::error_code ec, std::size_t bytes_transferred);
virtual void onRead(beast::error_code ec, std::size_t bytes_transferred);
private:
void createGetRequest(char const* host, char const* target, int version);
void createPostRequest(char const* host, char const* target, int version, char const *body);
std::string createBody();
tcp::resolver m_resolver;
beast::tcp_stream m_stream;
beast::flat_buffer m_buffer; // (Must persist between reads)
http::request<http::string_body> m_httpRequest;
http::response<http::string_body> m_httpResponse;
};
RESTClient.cpp
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/strand.hpp>
#include <boost/lexical_cast.hpp>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include "RESTClient.hpp"
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
void fail(beast::error_code ec, char const* what) {
std::cerr << what << ": " << ec.message() << "\n";
}
RESTClient::RESTClient(net::io_context& ioc)
: m_resolver(net::make_strand(ioc)), m_stream(net::make_strand(ioc)) {
}
RESTClient::~RESTClient() = default;
void RESTClient::run(char const* host, char const* port, char const* target, int version) {
createPostRequest(host, target, version, createBody().c_str());
m_resolver.async_resolve(host, port, beast::bind_front_handler(
&RESTClient::onResolve,
shared_from_this()));
}
void RESTClient::onResolve(beast::error_code ec, tcp::resolver::results_type results) {
if (ec) {
return fail(ec, "resolve");
}
std::cout << "onResolve ******" << std::endl;
m_stream.expires_after(std::chrono::seconds(30));
m_stream.async_connect(results, beast::bind_front_handler(
&RESTClient::onConnect,
shared_from_this()));
}
void
RESTClient::onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
if (ec) {
return fail(ec, "connect");
}
std::cout << "onConnect ******" << std::endl;
m_stream.expires_after(std::chrono::seconds(30));
http::async_write(m_stream, m_httpRequest,
beast::bind_front_handler(
&RESTClient::onWrite,
shared_from_this()));
}
void
RESTClient::onWrite(beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
return fail(ec, "write");
}
std::cout << "onWrite ******" << std::endl;
http::async_read(m_stream, m_buffer, m_httpResponse, beast::bind_front_handler(
&RESTClient::onRead,
shared_from_this()));
}
void RESTClient::onRead(beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
return fail(ec, "read");
}
std::cout << "onRead ******" << std::endl;
std::cout << m_httpResponse << std::endl;
m_stream.socket().shutdown(tcp::socket::shutdown_both, ec);
if (ec && ec != beast::errc::not_connected) {
return fail(ec, "shutdown");
}
}
void RESTClient::createGetRequest(char const* host, char const* target, int version) {
m_httpRequest.version(version);
m_httpRequest.method(http::verb::get);
m_httpRequest.target(target);
m_httpRequest.set(http::field::host, host);
m_httpRequest.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
}
void RESTClient::createPostRequest(char const* host, char const* target, int version, char const* body) {
m_httpRequest.version(version);
m_httpRequest.method(http::verb::post);
m_httpRequest.target(target);
m_httpRequest.set(http::field::host, host);
m_httpRequest.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
m_httpRequest.set(http::field::content_length, boost::lexical_cast<std::string>(strlen(body)));
m_httpRequest.set(http::field::body, body);
m_httpRequest.prepare_payload();
}
std::string RESTClient::createBody() {
boost::property_tree::ptree tree;
boost::property_tree::read_json("test.json",tree);
std::basic_stringstream<char> jsonStream;
boost::property_tree::json_parser::write_json(jsonStream, tree, false);
std::cout << "json stream :" << jsonStream.str() << std::endl;
return jsonStream.str();
}
int main(int argc, char** argv) {
// Check command line arguments.
if (argc != 4 && argc != 5) {
std::cerr <<
"Usage: http-client-async <host> <port> <target> [<HTTP version: 1.0 or 1.1(default)>]\n" <<
"Example:\n" <<
" http-client-async www.example.com 80 /\n" <<
" http-client-async www.example.com 80 / 1.0\n";
return EXIT_FAILURE;
}
auto const host = argv[1];
auto const port = argv[2];
auto const target = argv[3];
int version = argc == 5 && !std::strcmp("1.0", argv[4]) ? 10 : 11;
// The io_context is required for all I/O
net::io_context ioc;
std::cout << "version: " << version << std::endl;
// Launch the asynchronous operation
std::make_shared<RESTClient>(ioc)->run(host, port, target, version);
// Run the I/O service. The call will return when
// the get operation is complete.
ioc.run();
return EXIT_SUCCESS;
}
I want to unit test this client. I want to be able to POST a specific JSON from this client, and have the mock server reply with another specific JSON. what is the best way to do this. I am using google test

Related

boost::asio::async_write write ssl::stream succuess but server not get

I code a ssl server and client run a pingpang process, after a little time, client say send data success but server not get it.
client run in multi thread, when single thread, seem normal.
and i try add timer to add shake hand, then server can get all data, but i want it can run rightly with out shake hand
can anyone help figure out whats wrong.
here is my server
#include <cstdlib>
#include <functional>
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/bind/bind.hpp>
using boost::asio::ip::tcp;
class session : public std::enable_shared_from_this<session> {
public:
session(tcp::socket socket, boost::asio::ssl::context &context)
: socket_(std::move(socket), context), m_strand(socket.get_executor()) {
}
void start() {
do_handshake();
}
private:
void do_handshake() {
auto self(shared_from_this());
socket_.async_handshake(boost::asio::ssl::stream_base::server,
[this, self](const boost::system::error_code &error) {
if (!error) {
do_read();
}
});
}
void do_read() {
auto self(shared_from_this());
socket_.async_read_some(boost::asio::buffer(data_),
[this, self](const boost::system::error_code &ec, std::size_t length) {
if (!ec) {
std::cout << "get <";
std::cout.write(data_, length);
std::cout << std::endl;
do_write(length);
}
});
}
void do_write(std::size_t length) {
auto self(shared_from_this());
std::cout << "send <";
std::cout.write(data_, length);
std::cout << std::endl;
boost::asio::async_write(socket_, boost::asio::buffer(data_, length),
[this, self](const boost::system::error_code &ec,
std::size_t /*length*/) {
if (!ec) {
do_read();
}
});
}
boost::asio::ssl::stream<tcp::socket> socket_;
boost::asio::strand<boost::asio::ip::tcp::socket::executor_type> m_strand;
char data_[1024];
};
class server {
public:
server(boost::asio::io_context &io_context, unsigned short port)
: acceptor_(io_context, tcp::endpoint(tcp::v4(), port)),
context_(boost::asio::ssl::context::sslv23) {
context_.set_options(
boost::asio::ssl::context::default_workarounds
| boost::asio::ssl::context::no_sslv2
| boost::asio::ssl::context::single_dh_use);
context_.set_password_callback(std::bind(&server::get_password, this));
context_.use_certificate_chain_file("server.pem");
context_.use_private_key_file("server.pem", boost::asio::ssl::context::pem);
context_.use_tmp_dh_file("dh2048.pem");
do_accept();
}
private:
std::string get_password() const {
return "test";
}
void do_accept() {
acceptor_.async_accept(
[this](const boost::system::error_code &error, tcp::socket socket) {
if (!error) {
std::make_shared<session>(std::move(socket), context_)->start();
}
do_accept();
});
}
tcp::acceptor acceptor_;
boost::asio::ssl::context context_;
};
int main(int argc, char *argv[]) {
try {
if (argc != 2) {
std::cerr << "Usage: server <port>\n";
return 1;
}
boost::asio::io_context io_context;
using namespace std; // For atoi.
server s(io_context, atoi(argv[1]));
io_context.run();
}
catch (std::exception &e) {
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
and next client
#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/thread.hpp>
using boost::asio::ip::tcp;
using std::placeholders::_1;
using std::placeholders::_2;
enum {
max_length = 1024
};
class client {
public:
client(boost::asio::io_context &io_context,
boost::asio::ssl::context &context,
const tcp::resolver::results_type &endpoints)
: socket_(io_context, context), strand_(io_context.get_executor()) {
socket_.set_verify_mode(boost::asio::ssl::verify_peer);
socket_.set_verify_callback(
std::bind(&client::verify_certificate, this, _1, _2));
connect(endpoints);
}
private:
bool verify_certificate(bool preverified,
boost::asio::ssl::verify_context &ctx) {
char subject_name[256];
X509 *cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256);
std::cout << "Verifying " << subject_name << "\n";
return true;
}
void connect(const tcp::resolver::results_type &endpoints) {
boost::asio::async_connect(socket_.lowest_layer(), endpoints,
[this](const boost::system::error_code &error,
const tcp::endpoint & /*endpoint*/) {
if (!error) {
handshake();
} else {
std::cout << "Connect failed: " << error.message() << "\n";
}
});
}
void handshake() {
socket_.async_handshake(boost::asio::ssl::stream_base::client,
[this](const boost::system::error_code &error) {
if (!error) {
send_request("hello ssl");
boost::asio::post(strand_, std::bind(&client::recv, this));
} else {
std::cout << "Handshake failed: " << error.message() << "\n";
}
});
}
void send_request(const std::string &msg) {
boost::asio::async_write(
socket_, boost::asio::buffer(msg),
[this](const boost::system::error_code &error, std::size_t length) {
if (!error) {
std::cout << "send data success, size : " << length << std::endl;
} else {
std::cout << "Write failed: " << error.message() << std::endl;
}
});
}
void recv() {
boost::asio::async_read(
socket_, buffer_, boost::asio::transfer_exactly(9),
boost::asio::bind_executor(
strand_, [this](const boost::system::error_code &error, std::size_t length) {
if (!error) {
std::istream buffer(&buffer_);
std::vector<char> msg(length, 0);
buffer.readsome(msg.data(), length);
std::string recvMsg(msg.begin(), msg.end());
std::cout << recvMsg << std::endl;
send_request(recvMsg);
boost::asio::post(strand_, std::bind(&client::recv, this));
} else {
std::cout << "Read failed: " << error.message() << std::endl;
}
}));
}
boost::asio::ssl::stream<tcp::socket> socket_;
boost::asio::streambuf buffer_;
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
};
int main(int argc, char *argv[]) {
try {
if (argc != 3) {
std::cerr << "Usage: client <host> <port>\n";
return 1;
}
boost::asio::io_context io_context;
tcp::resolver resolver(io_context);
auto endpoints = resolver.resolve(argv[1], argv[2]);
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.load_verify_file("ca.pem");
client c(io_context, ctx, endpoints);
boost::thread_group threadPool;
for (size_t i = 0; i < boost::thread::hardware_concurrency(); ++i) {
threadPool.create_thread(boost::bind(&boost::asio::io_context::run, &io_context));
}
io_context.run();
}
catch (std::exception &e) {
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
when data not send to server,client will print like this
hello ssl
hello ssl
send data success, size : 9
send data success, size : 9
Check this out. If you remove the thread_group (as far as I can tell, it adds no value), everything works. This is a good sign that you have a threading bug.
I'm not in the mood to read the code until I see the problem, so let's circle a bit.
Adding ASAN/UBSAN shows nothing bad immediately, so that's good.
Let me look at the code a little then.
session creates a m_strand - which is never used...
you forgot to join the extra threads
Now that I noticed some potential confusion around the strand, I looked at the client strand use. And see that it is inconsistent:
the socket itself is NOT on the strand
send_request doesn't run on nor bind the completion handler to the strand's executor
the communication is full-duplex (meaning async_write and async_read happen concurrently).
this means that where client::recv is posted to the strand, it doesn't actually synchronize threaded access to socket_ (because the send_request is not tied to the strand in the first place)
If the above is surprising, you're not the first to fall into that:
Why is `net::dispatch` needed when the I/O object already has an executor?. In your example connect() and handshake() can be considered safe because they form a logical strand (sequential flow of execution). The problem arises with the concurrent paths.
By far the simplest way to fix the situation seems to construct socket_ from the strand_. This implies reordering the members so strand_ is initialized first:
client(boost::asio::io_context& io_context, ssl::context& context,
const tcp::resolver::results_type& endpoints)
: strand_(io_context.get_executor())
, socket_(strand_, context)
Next up, all posts to the strand can be dropped because they always happen from a completion handler on that strand.
send_request("hello ssl");
recv(); // already on the strand in this completion handler
The mild irony is that send_request was executed under the implied assumption that it was on the strand.
The cleaned up programs until this point are
File client.cpp
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/thread.hpp>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
using boost::asio::ip::tcp;
using boost::system::error_code;
using std::placeholders::_1;
using std::placeholders::_2;
namespace ssl = boost::asio::ssl;
class client {
public:
client(boost::asio::io_context& io_context, ssl::context& context,
const tcp::resolver::results_type& endpoints)
: strand_(io_context.get_executor())
, socket_(strand_, context)
{
socket_.set_verify_mode(ssl::verify_peer);
socket_.set_verify_callback(
std::bind(&client::verify_certificate, this, _1, _2));
connect(endpoints);
}
private:
bool verify_certificate(bool preverified, ssl::verify_context& ctx)
{
char subject_name[256];
X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256);
std::cout << "Verifying " << subject_name << "\n";
return true;
}
void connect(const tcp::resolver::results_type& endpoints)
{
async_connect( //
socket_.lowest_layer(), endpoints,
bind_executor(
strand_, [this](error_code error, const tcp::endpoint&) {
if (!error) {
handshake();
} else {
std::cout << "Connect failed: " << error.message() << "\n";
}
}));
}
void handshake()
{
socket_.async_handshake(
ssl::stream_base::client,
bind_executor(strand_, [this](error_code error) {
if (!error) {
send_request("hello ssl");
recv(); // already on the strand in this completion handler
} else {
std::cout << "Handshake failed: " << error.message()
<< "\n";
}
}));
}
void send_request(std::string const& msg)
{
msg_ = msg;
async_write(
socket_, boost::asio::buffer(msg_),
bind_executor(
strand_, [/*this*/](error_code error, std::size_t length) {
if (!error) {
std::cout << "send data success, size : " << length << std::endl;
} else {
std::cout << "Write failed: " << error.message() << std::endl;
}
}));
}
void recv()
{
async_read(
socket_, buffer_, boost::asio::transfer_exactly(9),
boost::asio::bind_executor(
strand_, [this](error_code error, std::size_t length) {
if (!error) {
std::istream buffer(&buffer_);
std::vector<char> msg(length, 0);
buffer.readsome(msg.data(), length);
msg_.assign(msg.begin(), msg.end());
std::cout << msg_ << std::endl;
send_request(msg_);
recv(); // already on the strand in this completion handler
} else {
std::cout << "Read failed: " << error.message() << std::endl;
}
}));
}
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
ssl::stream<tcp::socket> socket_;
boost::asio::streambuf buffer_;
std::string msg_;
};
int main(int argc, char* argv[])
{
try {
if (argc != 3) {
std::cerr << "Usage: client <host> <port>\n";
return 1;
}
boost::asio::io_context io_context;
tcp::resolver resolver(io_context);
auto endpoints = resolver.resolve(argv[1], argv[2]);
ssl::context ctx(ssl::context::sslv23);
ctx.load_verify_file("ca.pem");
client c(io_context, ctx, endpoints);
boost::thread_group threadPool;
for (size_t i = 0; i < boost::thread::hardware_concurrency(); ++i) {
threadPool.create_thread(
boost::bind(&boost::asio::io_context::run, &io_context));
}
threadPool.join_all();
//io_context.run();
return 0;
} catch (std::exception const& e) {
std::cerr << "Exception: " << e.what() << "\n";
return 1;
}
}
File server.cpp
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/bind/bind.hpp>
#include <cstdlib>
#include <functional>
#include <iostream>
namespace ssl = boost::asio::ssl;
using boost::asio::ip::tcp;
using boost::system::error_code;
class session : public std::enable_shared_from_this<session> {
public:
session(tcp::socket socket, ssl::context& context)
: socket_(std::move(socket), context)
, m_strand(socket.get_executor())
{
}
void start()
{
do_handshake();
}
private:
void do_handshake()
{
auto self(shared_from_this());
socket_.async_handshake(ssl::stream_base::server,
[this, self](error_code error) {
if (!error) {
do_read();
}
});
}
void do_read()
{
auto self(shared_from_this());
socket_.async_read_some(
boost::asio::buffer(data_),
[this, self](error_code ec, std::size_t length) {
if (!ec) {
std::cout << "get <";
std::cout.write(data_, length);
std::cout << std::endl;
do_write(length);
}
});
}
void do_write(std::size_t length)
{
auto self(shared_from_this());
std::cout << "send <";
std::cout.write(data_, length);
std::cout << std::endl;
boost::asio::async_write(
socket_, boost::asio::buffer(data_, length),
[this, self](error_code ec, std::size_t /*length*/) {
if (!ec) {
do_read();
}
});
}
ssl::stream<tcp::socket> socket_;
boost::asio::strand<tcp::socket::executor_type> m_strand;
char data_[1024];
};
class server {
public:
server(boost::asio::io_context& io_context, unsigned short port)
: acceptor_(io_context, tcp::endpoint(tcp::v4(), port))
, context_(ssl::context::sslv23)
{
context_.set_options(ssl::context::default_workarounds |
ssl::context::no_sslv2 |
ssl::context::single_dh_use);
context_.set_password_callback(std::bind(&server::get_password, this));
context_.use_certificate_chain_file("server.pem");
context_.use_private_key_file("server.pem", ssl::context::pem);
context_.use_tmp_dh_file("dh2048.pem");
do_accept();
}
private:
std::string get_password() const
{
return "test";
}
void do_accept()
{
acceptor_.async_accept([this](error_code error, tcp::socket socket) {
if (!error) {
std::make_shared<session>(std::move(socket), context_)->start();
}
do_accept();
});
}
tcp::acceptor acceptor_;
ssl::context context_;
};
int main(int argc, char* argv[])
{
try {
if (argc != 2) {
std::cerr << "Usage: server <port>\n";
return 1;
}
boost::asio::io_context io_context;
server s(io_context, std::atoi(argv[1]));
io_context.run();
return 0;
} catch (std::exception const& e) {
std::cerr << "Exception: " << e.what() << "\n";
return 1;
}
}
Other Problems
Lifetime Error
UBSAN/ASAN didn't catch it,but this is wrong:
void send_request(const std::string& msg)
{
async_write(
socket_, boost::asio::buffer(msg),
...
The problem is the lifetime of msg, which disappears before the async operation got a chance to run, let alone complete. So, move the buffer so the lifetime is sufficient (e.g. member msg_).
Concurrent Writes
When the client locks up, it shows
send data success, size : 9
hello ssl
hello ssl
send data success, size : 9
send data success, size : 9
This indicates that a second hello ssl is received before send is initiated. This means that a second send is initiated. Under the hood this cancels a duplex synchronization object inside the ssl stream context. You can see this with -DBOOST_ASIO_ENABLE_HANDLER_TRACKING:
#asio|1630155694.209267|51139|deadline_timer#0x7ffc6fa61e48.cancel
Visualizing with the handlerviz.pl script:
The problem is violating the requirements here:
The program must ensure that the stream performs no other write operations (such as async_write, the stream's async_write_some function, or any other composed operations that perform writes) until this operation completes.
Two easy ways to fix it:
change the IO from full duplex to sequential read/write/read/write just like the server
make an output queue that contains messages still to be written in sequence
Fixed Solution
This uses an outbox as in the second solution for overlapping writes above. I've also taken the liberty to
remove the unnecessary intermediate buffer streambuf buffer_ instead reading directly into a string.
replace io_context + thread_group with the more elegant thread_pool
many minor improvements (some mentioned above)
File client.cpp
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/thread.hpp>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
using boost::asio::ip::tcp;
using boost::system::error_code;
using std::placeholders::_1;
using std::placeholders::_2;
namespace ssl = boost::asio::ssl;
using Executor = boost::asio::thread_pool::executor_type;
class client {
public:
client(Executor ex, ssl::context& context,
const tcp::resolver::results_type& endpoints)
: strand_(ex)
, socket_(strand_, context)
{
socket_.set_verify_mode(ssl::verify_peer);
socket_.set_verify_callback(
std::bind(&client::verify_certificate, this, _1, _2));
connect(endpoints);
}
private:
bool verify_certificate(bool preverified, ssl::verify_context& ctx)
{
char subject_name[256];
X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256);
std::cout << "Verifying " << subject_name << "\n";
return true;
}
void connect(const tcp::resolver::results_type& endpoints)
{
async_connect( //
socket_.lowest_layer(), endpoints,
bind_executor(
strand_, [this](error_code error, const tcp::endpoint&) {
if (!error) {
handshake();
} else {
std::cout << "Connect failed: " << error.message() << "\n";
}
}));
}
void handshake()
{
socket_.async_handshake(
ssl::stream_base::client,
bind_executor(strand_, [this](error_code error) {
if (!error) {
send_request("hello ssl");
recv(); // already on the strand in this completion handler
} else {
std::cout << "Handshake failed: " << error.message()
<< "\n";
}
}));
}
void send_request(std::string msg)
{
outbox_.push_back(std::move(msg));
if (outbox_.size() == 1)
{
send_loop();
}
}
void send_loop()
{
async_write( //
socket_, boost::asio::buffer(outbox_.back()),
bind_executor(
strand_, [this](error_code error, std::size_t length) {
if (!error) {
std::cout << "send data success, size : " << length << std::endl;
outbox_.pop_back();
} else {
std::cout << "Write failed: " << error.message() << std::endl;
}
if (!outbox_.empty())
send_loop();
}));
}
void recv()
{
async_read(
socket_, boost::asio::dynamic_buffer(buffer_), boost::asio::transfer_exactly(9),
boost::asio::bind_executor(
strand_, [this](error_code error, std::size_t length) {
if (!error) {
std::cout << buffer_ << std::endl;
send_request(std::move(buffer_));
recv(); // already on the strand in this completion handler
} else {
std::cout << "Read failed: " << error.message() << std::endl;
}
}));
}
boost::asio::strand<Executor> strand_;
ssl::stream<tcp::socket> socket_;
std::string buffer_;
std::deque<std::string> outbox_;
};
int main(int argc, char* argv[])
{
try {
if (argc != 3) {
std::cerr << "Usage: client <host> <port>\n";
return 1;
}
ssl::context ctx(ssl::context::sslv23);
ctx.load_verify_file("ca.pem");
boost::asio::thread_pool io;
tcp::resolver resolver(io);
client c(io.get_executor(), ctx, resolver.resolve(argv[1], argv[2]));
io.join();
return 0;
} catch (std::exception const& e) {
std::cerr << "Exception: " << e.what() << "\n";
return 1;
}
}
File server.cpp
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/bind/bind.hpp>
#include <cstdlib>
#include <functional>
#include <iostream>
namespace ssl = boost::asio::ssl;
using boost::asio::ip::tcp;
using boost::system::error_code;
class session : public std::enable_shared_from_this<session> {
public:
session(tcp::socket socket, ssl::context& context)
: socket_(std::move(socket), context)
{
}
void start()
{
do_handshake();
}
private:
void do_handshake()
{
auto self(shared_from_this());
socket_.async_handshake(ssl::stream_base::server,
[this, self](error_code error) {
if (!error) {
do_read();
}
});
}
void do_read()
{
auto self(shared_from_this());
socket_.async_read_some(
boost::asio::buffer(data_),
[this, self](error_code ec, std::size_t length) {
if (!ec) {
std::cout << "get <";
std::cout.write(data_.data(), length);
std::cout << std::endl;
do_write(length);
}
});
}
void do_write(std::size_t length)
{
auto self(shared_from_this());
std::cout << "send <";
std::cout.write(data_.data(), length);
std::cout << std::endl;
boost::asio::async_write(
socket_, boost::asio::buffer(data_.data(), length),
[this, self](error_code ec, std::size_t /*length*/) {
if (!ec) {
do_read();
}
});
}
ssl::stream<tcp::socket> socket_;
std::array<char, 1024> data_;
};
class server {
public:
server(boost::asio::io_context& io_context, unsigned short port)
: acceptor_(io_context, tcp::endpoint(tcp::v4(), port))
, context_(ssl::context::sslv23)
{
context_.set_options(ssl::context::default_workarounds |
ssl::context::no_sslv2 |
ssl::context::single_dh_use);
context_.set_password_callback(std::bind(&server::get_password, this));
context_.use_certificate_chain_file("server.pem");
context_.use_private_key_file("server.pem", ssl::context::pem);
context_.use_tmp_dh_file("dh2048.pem");
do_accept();
}
private:
std::string get_password() const
{
return "test";
}
void do_accept()
{
acceptor_.async_accept([this](error_code error, tcp::socket socket) {
if (!error) {
std::make_shared<session>(std::move(socket), context_)->start();
}
do_accept();
});
}
tcp::acceptor acceptor_;
ssl::context context_;
};
int main(int argc, char* argv[])
{
try {
if (argc != 2) {
std::cerr << "Usage: server <port>\n";
return 1;
}
boost::asio::io_context io_context;
server s(io_context, std::atoi(argv[1]));
io_context.run();
return 0;
} catch (std::exception const& e) {
std::cerr << "Exception: " << e.what() << "\n";
return 1;
}
}
Live Demo:
As you can see (using a uniq -dc trick to suppress all non-repeating lines) now it happily continues in the case where multiple receives come in before send is initiated.

Boost ASIO - Unable to send message over TCP

I am using Boost ASIO for my TCP network communication. Here is my code:
Server.h:
#ifndef VIBRANIUM_CORE_SERVER_H
#define VIBRANIUM_CORE_SERVER_H
#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include <boost/asio.hpp>
#include <deque>
#include "Logger.h"
#include "Client.h"
#include "Protocol/ServerOpcode.h"
using boost::asio::ip::tcp;
namespace Vibranium {
class Server {
public:
Server(boost::asio::io_service &io_service, short port)
: acceptor_(io_service, tcp::endpoint(tcp::v4(), port)),
socket_(io_service) {
do_accept();
Logger::Log("Server Started! Listening on Port("+std::to_string(port)+")", Logger::Success, true);
}
static std::deque<std::shared_ptr<Client>> Clients;
private:
void do_accept();
int incrementor;
tcp::acceptor acceptor_;
tcp::socket socket_;
};
}
#endif //VIBRANIUM_CORE_SERVER_H
Server.cpp:
#include "Server.h"
#include "Client.h"
using namespace Vibranium;
std::deque<std::shared_ptr<Client>> Server::Clients;
void Server::do_accept()
{
acceptor_.async_accept(socket_,
[this](boost::system::error_code ec)
{
if (!ec)
{
incrementor++;
Logger::Log("New Connection (ID: " + std::to_string(incrementor) + ")",Logger::Success);
std::shared_ptr<Client> c = std::make_shared<Client>(std::move(socket_));
c->start();
c->connectionId = incrementor;
Server::Clients.push_back(c);
}
do_accept();
});
}
Client.h:
#ifndef VIBRANIUM_CORE_CLIENT_H
#define VIBRANIUM_CORE_CLIENT_H
#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include <boost/asio.hpp>
#include "Protocol/ServerOpcode.h"
using boost::asio::ip::tcp;
namespace Vibranium{
class Client: public std::enable_shared_from_this<Client>
{
public:
Client(tcp::socket socket)
: socket(std::move(socket))
{
}
void start();
int connectionId;
tcp::socket socket;
void Send(ServerOpcode serverOpcode, const std::string& message);
private:
void do_read();
void do_write(std::size_t length);
enum { max_length = 1024 };
char data_[max_length];
};
}
#endif //VIBRANIUM_CORE_CLIENT_H
Client.cpp:
#include "Client.h"
#include "Server.h"
void Vibranium::Client::start() {
do_read();
}
void Vibranium::Client::do_read() {
auto self(shared_from_this());
socket.async_read_some(boost::asio::buffer(data_, max_length),
[this, self](boost::system::error_code ec, std::size_t length)
{
if ((boost::asio::error::eof == ec) || (boost::asio::error::connection_reset == ec))
{
Logger::Log("Disconnected ID: " + std::to_string(connectionId),Logger::Error, true);
for (int i = 0; i < Server::Clients.size(); ++i) {
if(Server::Clients[i]->connectionId == connectionId)
Server::Clients.erase(Server::Clients.begin()+i);
}
}
else
{
std::cout.write(data_, length);
std::cout << "\n";
//do_write(length);
Send(ServerOpcode::SMSG_AUTH_CONNECTION_RESPONSE,"How are you, mate?");
}
});
}
void Vibranium::Client::do_write(std::size_t length) {
auto self(shared_from_this());
boost::asio::async_write(socket, boost::asio::buffer(data_, length),
[this, self](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
do_read();
}
});
}
void Vibranium::Client::Send(ServerOpcode serverOpcode, const std::string& message) {
auto self(shared_from_this());
std::cout << "HERE!" << std::endl;
size_t request_length = std::strlen(message.c_str());
boost::asio::async_write(socket, boost::asio::buffer(message.c_str(), request_length),
[this, self](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec)
do_read();
else
Logger::Log("Message sent failed to " + std::to_string(connectionId),Logger::Error);
});
}
Here is how I simulate several connections to the server:
Config config("AuthServer");
std::string defaultIP = "127.0.0.1";
std::string defaultPort = "8080";
int connectionsNumber = CommandQuestion<int>::AskQuestion("How many connections do you want established?");
std::cout << "Initializing " << std::to_string(connectionsNumber) << " connection/s." << std::endl;
std::cout << "Trying to connect to " << defaultIP << " on port: " << config.GetConfigValue("AuthServerPort", defaultPort) << std::endl;
boost::asio::io_context io_context;
std::vector<tcp::socket> sockets;
for (int i = 0; i < connectionsNumber; ++i) {
try
{
sockets.emplace_back(io_context);
tcp::socket& s{sockets.back()};
tcp::resolver resolver(io_context);
boost::asio::connect(s, resolver.resolve( defaultIP,config.GetConfigValue("AuthServerPort", defaultPort)));
std::string message = "I am testing here!!!";
size_t request_length = std::strlen(message.c_str());
boost::asio::write(s, boost::asio::buffer(message, request_length));
char reply[max_length];
size_t reply_length = boost::asio::read(s,boost::asio::buffer(reply, request_length));
Logger::Log(std::to_string(i) + " Connected!",Logger::Success);
std::cout << "Reply is: ";
std::cout.write(reply, reply_length);
std::cout << "\n";
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
}
So basically I am establishing connection and then the clients sends a message to the server saying I am testing here!!! than I would like to use my function Send to send back to the client another message How are you, mate?.
However it does not work. It seems the message can't be seen back from the client. On server side I see the output of HERE! which is located in Send function. That means I reach that point.
On another hand if I uncomment //do_write(length); from function do_read() and comment back send() it seems to return back the message I am testing here!!!. So by my understanding communication works that way.
My question is where is my mistake with Send function? Why I can't make it send another message instead of just replying back with what the client sent in first place.
I found myself where the error was, here is how I solved it:
void Vibranium::Client::Send(ServerOpcode serverOpcode, const std::string& message) {
auto self(shared_from_this());
std::cout << "HERE!" << std::endl;
strcpy(data_, message.c_str());
size_t request_length = sizeof(data_)/sizeof(*data_);
boost::asio::async_write(socket, boost::asio::buffer(data_, request_length),
[this, self](boost::system::error_code ec, std::size_t /*length*/)
{
if (ec)
Logger::Log("Message sent failed to " + std::to_string(connectionId),Logger::Error);
});
}

io_context.run() in a separate thread blocks

I have a RESTServer.hpp implemented using boost.beast as shown below.
#pragma once
#include <boost/property_tree/json_parser.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio.hpp>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <memory>
#include <string>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
class RESTServer : public std::enable_shared_from_this<RESTServer> {
public:
RESTServer(tcp::socket socket)
: m_socket(std::move(socket)) {
}
void start() {
readRequest();
checkDeadline();
}
private:
tcp::socket m_socket;
beast::flat_buffer m_buffer{8192};
http::request<http::dynamic_body> m_request;
http::response<http::dynamic_body> m_response;
net::steady_timer m_deadline{m_socket.get_executor(), std::chrono::seconds(60)};
void readRequest() {
auto self = shared_from_this();
http::async_read(m_socket, m_buffer, m_request, [self](beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (!ec) {
self->processRequest();
}
});
}
void processRequest() {
m_response.version(m_request.version());
m_response.keep_alive(false);
switch (m_request.method()) {
case http::verb::get:
m_response.result(http::status::ok);
m_response.set(http::field::server, "Beast");
createResponse();
break;
case http::verb::post:
m_response.result(http::status::ok);
m_response.set(http::field::server, "Beast");
createResponse();
break;
default:
m_response.result(http::status::bad_request);
m_response.set(http::field::content_type, "text/plain");
beast::ostream(m_response.body())
<< "Invalid request-method '"
<< std::string(m_request.method_string())
<< "'";
break;
}
writeResponse();
}
void createResponse() {
if(request_.target() == "/count")
{
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body())
<< "<html>\n"
<< "<head><title>Request count</title></head>\n"
<< "<body>\n"
<< "<h1>Request count</h1>\n"
<< "<p>There have been "
<< my_program_state::request_count()
<< " requests so far.</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else if(request_.target() == "/time")
{
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body())
<< "<html>\n"
<< "<head><title>Current time</title></head>\n"
<< "<body>\n"
<< "<h1>Current time</h1>\n"
<< "<p>The current time is "
<< my_program_state::now()
<< " seconds since the epoch.</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else
{
response_.result(http::status::not_found);
response_.set(http::field::content_type, "text/plain");
beast::ostream(response_.body()) << "File not found\r\n";
}
}
void writeResponse() {
auto self = shared_from_this();
m_response.set(http::field::content_length, m_response.body().size());
http::async_write(m_socket, m_response,
[self](beast::error_code ec, std::size_t) {
self->m_socket.shutdown(tcp::socket::shutdown_send, ec);
self->m_deadline.cancel();
});
}
void checkDeadline() {
auto self = shared_from_this();
m_deadline.async_wait([self](beast::error_code ec) {
if (!ec) {
self->m_socket.close(ec);
}
});
}
};
void httpServer(tcp::acceptor& acceptor, tcp::socket& socket) {
acceptor.async_accept(socket, [&](beast::error_code ec) {
if (!ec) {
std::make_shared<RESTServer>(std::move(socket))->start();
}
httpServer(acceptor, socket);
});
}
I also have a RESTClient RESTClient.hpp and RESTClient.cpp as shown below.
RESTClient.hpp
#pragma once
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/strand.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
// Performs an HTTP GET and prints the response
class RESTClient : public std::enable_shared_from_this<RESTClient> {
public:
explicit RESTClient(net::io_context& ioc);
virtual ~RESTClient();
virtual void run(char const* host, char const* port, char const* target, int version);
virtual void onResolve(beast::error_code ec, tcp::resolver::results_type results);
virtual void onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type);
virtual void onWrite(beast::error_code ec, std::size_t bytes_transferred);
virtual void onRead(beast::error_code ec, std::size_t bytes_transferred);
private:
void createGetRequest(char const* host, char const* target, int version);
void createPostRequest(char const* host, char const* target, int version, char const *body);
std::string createBody();
tcp::resolver m_resolver;
beast::tcp_stream m_stream;
beast::flat_buffer m_buffer; // (Must persist between reads)
http::request<http::string_body> m_httpRequest;
http::response<http::string_body> m_httpResponse;
};
RESTClient.cpp
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/strand.hpp>
#include <boost/lexical_cast.hpp>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include "RESTClient.hpp"
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
void fail(beast::error_code ec, char const* what) {
std::cerr << what << ": " << ec.message() << "\n";
}
RESTClient::RESTClient(net::io_context& ioc)
: m_resolver(net::make_strand(ioc)), m_stream(net::make_strand(ioc)) {
}
RESTClient::~RESTClient() = default;
void RESTClient::run(char const* host, char const* port, char const* target, int version) {
createPostRequest(host, target, version, createBody().c_str());
m_resolver.async_resolve(host, port, beast::bind_front_handler(
&RESTClient::onResolve,
shared_from_this()));
}
void RESTClient::onResolve(beast::error_code ec, tcp::resolver::results_type results) {
if (ec) {
return fail(ec, "resolve");
}
std::cout << "onResolve ******" << std::endl;
m_stream.expires_after(std::chrono::seconds(30));
m_stream.async_connect(results, beast::bind_front_handler(
&RESTClient::onConnect,
shared_from_this()));
}
void
RESTClient::onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
if (ec) {
return fail(ec, "connect");
}
std::cout << "onConnect ******" << std::endl;
m_stream.expires_after(std::chrono::seconds(30));
http::async_write(m_stream, m_httpRequest,
beast::bind_front_handler(
&RESTClient::onWrite,
shared_from_this()));
}
void
RESTClient::onWrite(beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
return fail(ec, "write");
}
std::cout << "onWrite ******" << std::endl;
http::async_read(m_stream, m_buffer, m_httpResponse, beast::bind_front_handler(
&RESTClient::onRead,
shared_from_this()));
}
void RESTClient::onRead(beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
return fail(ec, "read");
}
std::cout << "onRead ******" << std::endl;
std::cout << m_httpResponse << std::endl;
m_stream.socket().shutdown(tcp::socket::shutdown_both, ec);
if (ec && ec != beast::errc::not_connected) {
return fail(ec, "shutdown");
}
}
void RESTClient::createGetRequest(char const* host, char const* target, int version) {
m_httpRequest.version(version);
m_httpRequest.method(http::verb::get);
m_httpRequest.target(target);
m_httpRequest.set(http::field::host, host);
m_httpRequest.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
}
void RESTClient::createPostRequest(char const* host, char const* target, int version, char const* body) {
m_httpRequest.version(version);
m_httpRequest.method(http::verb::post);
m_httpRequest.target(target);
m_httpRequest.set(http::field::host, host);
m_httpRequest.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
m_httpRequest.set(http::field::content_length, boost::lexical_cast<std::string>(strlen(body)));
m_httpRequest.set(http::field::body, body);
m_httpRequest.prepare_payload();
}
std::string RESTClient::createBody() {
boost::property_tree::ptree tree;
boost::property_tree::read_json("test.json",tree);
std::basic_stringstream<char> jsonStream;
boost::property_tree::json_parser::write_json(jsonStream, tree, false);
std::cout << "json stream :" << jsonStream.str() << std::endl;
return jsonStream.str();
}
int main(int argc, char** argv) {
// Check command line arguments.
if (argc != 4 && argc != 5) {
std::cerr <<
"Usage: http-client-async <host> <port> <target> [<HTTP version: 1.0 or 1.1(default)>]\n" <<
"Example:\n" <<
" http-client-async www.example.com 80 /\n" <<
" http-client-async www.example.com 80 / 1.0\n";
return EXIT_FAILURE;
}
auto const host = argv[1];
auto const port = argv[2];
auto const target = argv[3];
int version = argc == 5 && !std::strcmp("1.0", argv[4]) ? 10 : 11;
// The io_context is required for all I/O
net::io_context ioc;
std::cout << "version: " << version << std::endl;
// Launch the asynchronous operation
std::make_shared<RESTClient>(ioc)->run(host, port, target, version);
// Run the I/O service. The call will return when
// the get operation is complete.
ioc.run();
return EXIT_SUCCESS;
}
Now I want to test my RESTClient using googletest. In the unit test, I want to use the RESTServer to simulate the response to the client. My test class is shown below.
class MyTest : public ::testing::Test{
virtual void SetUp(){
httpServer(m_acceptor, m_socket);
m_threads.emplace_back(boost::bind(&boost::asio::io_context::run, &m_ioc));
m_ioc.run();
}
virtual void TearDown() {
for(auto& thread : m_threads) {
if(thread.joinable()) {
thread.join();
}
}
net::ip::address m_address = net::ip::make_address("0.0.0.0");
unsigned short m_port = static_cast<unsigned short>(8080);
net::io_context m_ioc{1};
tcp::acceptor m_acceptor{m_ioc, {m_address, m_port}};
tcp::socket m_socket{m_ioc};
std::vector<std::thread> m_threads;
};
My question is the following.
When I implement the class MyTest, I start the RESTServer in a separate thread. Please see the code in SetUp(). When I call m_ioc.run(), the server runs, but blocks. I would expect the server to run in a separate thread, and proceed to execute my test case, where I start the client and do some GET and POST operations.
You're calling run in both the thread
m_threads.emplace_back(boost::bind(&boost::asio::io_context::run, &m_ioc));
and the google test thread
m_ioc.run();
which is causing the Setup to block. Try removing m_ioc.run(); since you are already spawning a thread to call io_context::run.

io_context.run() for boost beast server

I have a RESTServer.hpp implemented using boost.beast as shown below.
#pragma once
#include <boost/property_tree/json_parser.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio.hpp>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <memory>
#include <string>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
class RESTServer : public std::enable_shared_from_this<RESTServer> {
public:
RESTServer(tcp::socket socket)
: m_socket(std::move(socket)) {
}
void start() {
readRequest();
checkDeadline();
}
private:
tcp::socket m_socket;
beast::flat_buffer m_buffer{8192};
http::request<http::dynamic_body> m_request;
http::response<http::dynamic_body> m_response;
net::steady_timer m_deadline{m_socket.get_executor(), std::chrono::seconds(60)};
void readRequest() {
auto self = shared_from_this();
http::async_read(m_socket, m_buffer, m_request, [self](beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (!ec) {
self->processRequest();
}
});
}
void processRequest() {
m_response.version(m_request.version());
m_response.keep_alive(false);
switch (m_request.method()) {
case http::verb::get:
m_response.result(http::status::ok);
m_response.set(http::field::server, "Beast");
createResponse();
break;
case http::verb::post:
m_response.result(http::status::ok);
m_response.set(http::field::server, "Beast");
createResponse();
break;
default:
m_response.result(http::status::bad_request);
m_response.set(http::field::content_type, "text/plain");
beast::ostream(m_response.body())
<< "Invalid request-method '"
<< std::string(m_request.method_string())
<< "'";
break;
}
writeResponse();
}
void createResponse() {
if(request_.target() == "/count")
{
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body())
<< "<html>\n"
<< "<head><title>Request count</title></head>\n"
<< "<body>\n"
<< "<h1>Request count</h1>\n"
<< "<p>There have been "
<< my_program_state::request_count()
<< " requests so far.</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else if(request_.target() == "/time")
{
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body())
<< "<html>\n"
<< "<head><title>Current time</title></head>\n"
<< "<body>\n"
<< "<h1>Current time</h1>\n"
<< "<p>The current time is "
<< my_program_state::now()
<< " seconds since the epoch.</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else
{
response_.result(http::status::not_found);
response_.set(http::field::content_type, "text/plain");
beast::ostream(response_.body()) << "File not found\r\n";
}
}
void writeResponse() {
auto self = shared_from_this();
m_response.set(http::field::content_length, m_response.body().size());
http::async_write(m_socket, m_response,
[self](beast::error_code ec, std::size_t) {
self->m_socket.shutdown(tcp::socket::shutdown_send, ec);
self->m_deadline.cancel();
});
}
void checkDeadline() {
auto self = shared_from_this();
m_deadline.async_wait([self](beast::error_code ec) {
if (!ec) {
self->m_socket.close(ec);
}
});
}
};
void httpServer(tcp::acceptor& acceptor, tcp::socket& socket) {
acceptor.async_accept(socket, [&](beast::error_code ec) {
if (!ec) {
std::make_shared<RESTServer>(std::move(socket))->start();
}
httpServer(acceptor, socket);
});
}
I also have a RESTClient RESTClient.hpp and RESTClient.cpp as shown below.
RESTClient.hpp
#pragma once
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/strand.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
// Performs an HTTP GET and prints the response
class RESTClient : public std::enable_shared_from_this<RESTClient> {
public:
explicit RESTClient(net::io_context& ioc);
virtual ~RESTClient();
virtual void run(char const* host, char const* port, char const* target, int version);
virtual void onResolve(beast::error_code ec, tcp::resolver::results_type results);
virtual void onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type);
virtual void onWrite(beast::error_code ec, std::size_t bytes_transferred);
virtual void onRead(beast::error_code ec, std::size_t bytes_transferred);
private:
void createGetRequest(char const* host, char const* target, int version);
void createPostRequest(char const* host, char const* target, int version, char const *body);
std::string createBody();
tcp::resolver m_resolver;
beast::tcp_stream m_stream;
beast::flat_buffer m_buffer; // (Must persist between reads)
http::request<http::string_body> m_httpRequest;
http::response<http::string_body> m_httpResponse;
};
RESTClient.cpp
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/strand.hpp>
#include <boost/lexical_cast.hpp>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include "RESTClient.hpp"
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
void fail(beast::error_code ec, char const* what) {
std::cerr << what << ": " << ec.message() << "\n";
}
RESTClient::RESTClient(net::io_context& ioc)
: m_resolver(net::make_strand(ioc)), m_stream(net::make_strand(ioc)) {
}
RESTClient::~RESTClient() = default;
void RESTClient::run(char const* host, char const* port, char const* target, int version) {
createPostRequest(host, target, version, createBody().c_str());
m_resolver.async_resolve(host, port, beast::bind_front_handler(
&RESTClient::onResolve,
shared_from_this()));
}
void RESTClient::onResolve(beast::error_code ec, tcp::resolver::results_type results) {
if (ec) {
return fail(ec, "resolve");
}
std::cout << "onResolve ******" << std::endl;
m_stream.expires_after(std::chrono::seconds(30));
m_stream.async_connect(results, beast::bind_front_handler(
&RESTClient::onConnect,
shared_from_this()));
}
void
RESTClient::onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
if (ec) {
return fail(ec, "connect");
}
std::cout << "onConnect ******" << std::endl;
m_stream.expires_after(std::chrono::seconds(30));
http::async_write(m_stream, m_httpRequest,
beast::bind_front_handler(
&RESTClient::onWrite,
shared_from_this()));
}
void
RESTClient::onWrite(beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
return fail(ec, "write");
}
std::cout << "onWrite ******" << std::endl;
http::async_read(m_stream, m_buffer, m_httpResponse, beast::bind_front_handler(
&RESTClient::onRead,
shared_from_this()));
}
void RESTClient::onRead(beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
return fail(ec, "read");
}
std::cout << "onRead ******" << std::endl;
std::cout << m_httpResponse << std::endl;
m_stream.socket().shutdown(tcp::socket::shutdown_both, ec);
if (ec && ec != beast::errc::not_connected) {
return fail(ec, "shutdown");
}
}
void RESTClient::createGetRequest(char const* host, char const* target, int version) {
m_httpRequest.version(version);
m_httpRequest.method(http::verb::get);
m_httpRequest.target(target);
m_httpRequest.set(http::field::host, host);
m_httpRequest.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
}
void RESTClient::createPostRequest(char const* host, char const* target, int version, char const* body) {
m_httpRequest.version(version);
m_httpRequest.method(http::verb::post);
m_httpRequest.target(target);
m_httpRequest.set(http::field::host, host);
m_httpRequest.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
m_httpRequest.set(http::field::content_length, boost::lexical_cast<std::string>(strlen(body)));
m_httpRequest.set(http::field::body, body);
m_httpRequest.prepare_payload();
}
std::string RESTClient::createBody() {
boost::property_tree::ptree tree;
boost::property_tree::read_json("test.json",tree);
std::basic_stringstream<char> jsonStream;
boost::property_tree::json_parser::write_json(jsonStream, tree, false);
std::cout << "json stream :" << jsonStream.str() << std::endl;
return jsonStream.str();
}
int main(int argc, char** argv) {
// Check command line arguments.
if (argc != 4 && argc != 5) {
std::cerr <<
"Usage: http-client-async <host> <port> <target> [<HTTP version: 1.0 or 1.1(default)>]\n" <<
"Example:\n" <<
" http-client-async www.example.com 80 /\n" <<
" http-client-async www.example.com 80 / 1.0\n";
return EXIT_FAILURE;
}
auto const host = argv[1];
auto const port = argv[2];
auto const target = argv[3];
int version = argc == 5 && !std::strcmp("1.0", argv[4]) ? 10 : 11;
// The io_context is required for all I/O
net::io_context ioc;
std::cout << "version: " << version << std::endl;
// Launch the asynchronous operation
std::make_shared<RESTClient>(ioc)->run(host, port, target, version);
// Run the I/O service. The call will return when
// the get operation is complete.
ioc.run();
return EXIT_SUCCESS;
}
Now I want to test my RESTClient using googletest. In the unit test, I want to use the RESTServer to simulate the response to the client. My test class is shown below.
class MyTest : public ::testing::Test{
virtual void SetUp(){
httpServer(m_acceptor, m_socket);
}
net::ip::address m_address = net::ip::make_address("0.0.0.0");
unsigned short m_port = static_cast<unsigned short>(8080);
net::io_context m_ioc{1};
tcp::acceptor m_acceptor{m_ioc, {m_address, m_port}};
tcp::socket m_socket{m_ioc};
};
My question is the following.
When I implement the class MyTest, I need to pass an io_context to both the httpServer and RESTClient. Should the same io_context, be passed to both Client and Server? or should the io_context, be different. Can someone throw some light on this? and also explain the reason. I would like to understand what an io_context actually means?
Should the same io_context, be passed to both Client and Server? or should the io_context, be different.
Can someone throw some light on this? and also explain the reason. I would like to understand what an io_context actually means?
That is really up to you: an io_context provides the context in which the asynchronous calls such as async_resolve and async_write run in. Think of io_context::run as your event loop.
Your typically steps involve
creating the io_context
providing io_context with some work to do (i.e. your async_resolve, async_connect, async_reads and async_write, async_wait on timers, etc.)
calling run either in some thread.
The run call blocks until the io_context runs out of work unless you provide it with a work object.
Other points:
You should note that typically your asynchronous handlers add more work to the io_context causing run to not just run out of work and exit.
Whether to create an explicit work object or not depends on your specific application design. Personally I prefer to be in control of every asynchronous operation executed and also to be responsible for a "clean" shutdown i.e. cancelling all outstanding work and letting all started operations finish cleanly. It is also possible to simply stop the io_context but that might be careless. You would need to create a work object if you need to call runio_context. Typically if you write a server, you already have a listening socket and already have work, so there's no need to add a work object. Similarly, you might have a periodic timer, etc.
Other threading models are also possible using the run_one call. You would typically use this when you have some other event loop/library running.
More advanced threading models and scaling work across multiple threads can be accomplished using this sample by the asio author.
Coming back to your question: keep in mind that you still need to provide an execution thread to the io_context in order to execute the IO calls. So one io_context is simpler to manage (one context + one blocking call to run). It's probably more efficient to have one context as you would avoid unnecessary thread context switching.

Passing an additional parameter to a function called using beast::bind_front_handler

I have a boost::beast REST client. the .hpp and .cxx are given below.
#pragma once
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/strand.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
// Performs an HTTP GET and prints the response
class RESTClient : public std::enable_shared_from_this<RESTClient> {
public:
explicit RESTClient(net::io_context& ioc);
virtual ~RESTClient();
virtual void run(char const* host, char const* port, char const* target, int version);
virtual void onResolve(beast::error_code ec, tcp::resolver::results_type results);
virtual void onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type);
virtual void onWrite(beast::error_code ec, std::size_t bytes_transferred);
virtual void onRead(beast::error_code ec, std::size_t bytes_transferred);
private:
void createGetRequest(char const* host, char const* target, int version);
void createPostRequest(char const* host, char const* target, int version, char const *body);
std::string createBody();
tcp::resolver m_resolver;
beast::tcp_stream m_stream;
beast::flat_buffer m_buffer; // (Must persist between reads)
http::request<http::string_body> m_httpRequest;
http::response<http::string_body> m_httpResponse;
};
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/strand.hpp>
#include <boost/lexical_cast.hpp>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include "RESTClient.hpp"
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
void fail(beast::error_code ec, char const* what) {
std::cerr << what << ": " << ec.message() << "\n";
}
RESTClient::RESTClient(net::io_context& ioc)
: m_resolver(net::make_strand(ioc)), m_stream(net::make_strand(ioc)) {
}
RESTClient::~RESTClient() = default;
void RESTClient::run(char const* host, char const* port, char const* target, int version) {
createPostRequest(host, target, version, createBody().c_str());
m_resolver.async_resolve(host, port, beast::bind_front_handler(
&RESTClient::onResolve,
shared_from_this()));
}
void RESTClient::onResolve(beast::error_code ec, tcp::resolver::results_type results) {
if (ec) {
return fail(ec, "resolve");
}
std::cout << "onResolve ******" << std::endl;
m_stream.expires_after(std::chrono::seconds(30));
m_stream.async_connect(results, beast::bind_front_handler(
&RESTClient::onConnect,
shared_from_this()));
}
void
RESTClient::onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
if (ec) {
return fail(ec, "connect");
}
std::cout << "onConnect ******" << std::endl;
m_stream.expires_after(std::chrono::seconds(30));
http::async_write(m_stream, m_httpRequest,
beast::bind_front_handler(
&RESTClient::onWrite,
shared_from_this()));
}
void
RESTClient::onWrite(beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
return fail(ec, "write");
}
std::cout << "onWrite ******" << std::endl;
http::async_read(m_stream, m_buffer, m_httpResponse, beast::bind_front_handler(
&RESTClient::onRead,
shared_from_this()));
}
void RESTClient::onRead(beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
return fail(ec, "read");
}
std::cout << "onRead ******" << std::endl;
std::cout << m_httpResponse << std::endl;
m_stream.socket().shutdown(tcp::socket::shutdown_both, ec);
if (ec && ec != beast::errc::not_connected) {
return fail(ec, "shutdown");
}
}
void RESTClient::createGetRequest(char const* host, char const* target, int version) {
m_httpRequest.version(version);
m_httpRequest.method(http::verb::get);
m_httpRequest.target(target);
m_httpRequest.set(http::field::host, host);
m_httpRequest.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
}
void RESTClient::createPostRequest(char const* host, char const* target, int version, char const* body) {
m_httpRequest.version(version);
m_httpRequest.method(http::verb::post);
m_httpRequest.target(target);
m_httpRequest.set(http::field::host, host);
m_httpRequest.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
m_httpRequest.set(http::field::content_length, boost::lexical_cast<std::string>(strlen(body)));
m_httpRequest.set(http::field::body, body);
m_httpRequest.prepare_payload();
}
std::string RESTClient::createBody() {
boost::property_tree::ptree tree;
boost::property_tree::read_json("test.json",tree);
std::basic_stringstream<char> jsonStream;
boost::property_tree::json_parser::write_json(jsonStream, tree, false);
std::cout << "json stream :" << jsonStream.str() << std::endl;
return jsonStream.str();
}
int main(int argc, char** argv) {
// Check command line arguments.
if (argc != 4 && argc != 5) {
std::cerr <<
"Usage: http-client-async <host> <port> <target> [<HTTP version: 1.0 or 1.1(default)>]\n" <<
"Example:\n" <<
" http-client-async www.example.com 80 /\n" <<
" http-client-async www.example.com 80 / 1.0\n";
return EXIT_FAILURE;
}
auto const host = argv[1];
auto const port = argv[2];
auto const target = argv[3];
int version = argc == 5 && !std::strcmp("1.0", argv[4]) ? 10 : 11;
// The io_context is required for all I/O
net::io_context ioc;
std::cout << "version: " << version << std::endl;
// Launch the asynchronous operation
std::make_shared<RESTClient>(ioc)->run(host, port, target, version);
// Run the I/O service. The call will return when
// the get operation is complete.
ioc.run();
return EXIT_SUCCESS;
}
I want to pass an additional parameter of the type std::function<void(bool)> to onWrite in the line shown below.
http::async_write(m_stream, m_httpRequest,
beast::bind_front_handler(
&RESTClient::onWrite,
shared_from_this()));
how do I do that?
The declaration:
virtual void onWrite(std::function<void(bool)> f, beast::error_code ec, std::size_t bytes_transferred);
The definition:
void
RESTClient::onWrite(std::function<void(bool)> f, beast::error_code ec, std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
return fail(ec, "write");
}
//...
The invocation:
http::async_write(m_stream, m_httpRequest,
beast::bind_front_handler(
&RESTClient::onWrite,
shared_from_this(),
[](bool){ std::cout << "functor" << std::endl;}
));