How to create a boost ssl iostream? - c++

I'm adding HTTPS support to code that does input and output using boost tcp::iostream (acting as an HTTP server).
I've found examples (and have a working toy HTTPS server) that do SSL input/output using boost::asio::read/boost::asio::write, but none that use iostreams and the << >> operators. How do I turn an ssl::stream into an iostream?
Working code:
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/foreach.hpp>
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
using namespace boost;
using boost::asio::ip::tcp;
typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_stream;
string HTTPReply(int nStatus, const string& strMsg)
{
string strStatus;
if (nStatus == 200) strStatus = "OK";
else if (nStatus == 400) strStatus = "Bad Request";
else if (nStatus == 404) strStatus = "Not Found";
else if (nStatus == 500) strStatus = "Internal Server Error";
ostringstream s;
s << "HTTP/1.1 " << nStatus << " " << strStatus << "\r\n"
<< "Connection: close\r\n"
<< "Content-Length: " << strMsg.size() << "\r\n"
<< "Content-Type: application/json\r\n"
<< "Date: Sat, 09 Jul 2009 12:04:08 GMT\r\n"
<< "Server: json-rpc/1.0\r\n"
<< "\r\n"
<< strMsg;
return s.str();
}
int main()
{
// Bind to loopback 127.0.0.1 so the socket can only be accessed locally
boost::asio::io_service io_service;
tcp::endpoint endpoint(boost::asio::ip::address_v4::loopback(), 1111);
tcp::acceptor acceptor(io_service, endpoint);
boost::asio::ssl::context context(io_service, boost::asio::ssl::context::sslv23);
context.set_options(
boost::asio::ssl::context::default_workarounds
| boost::asio::ssl::context::no_sslv2);
context.use_certificate_chain_file("server.cert");
context.use_private_key_file("server.pem", boost::asio::ssl::context::pem);
for(;;)
{
// Accept connection
ssl_stream stream(io_service, context);
tcp::endpoint peer_endpoint;
acceptor.accept(stream.lowest_layer(), peer_endpoint);
boost::system::error_code ec;
stream.handshake(boost::asio::ssl::stream_base::server, ec);
if (!ec) {
boost::asio::write(stream, boost::asio::buffer(HTTPReply(200, "Okely-Dokely\n")));
// I really want to write:
// iostream_object << HTTPReply(200, "Okely-Dokely\n") << std::flush;
}
}
}
It seems like the ssl::stream_service would be the answer, but that is a dead end.
Using boost::iostreams (as suggested by accepted answer) is the right approach; here's the working code I've ended up with:
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/iostreams/concepts.hpp>
#include <boost/iostreams/stream.hpp>
#include <sstream>
#include <string>
#include <iostream>
using namespace boost::asio;
typedef ssl::stream<ip::tcp::socket> ssl_stream;
//
// IOStream device that speaks SSL but can also speak non-SSL
//
class ssl_iostream_device : public boost::iostreams::device<boost::iostreams::bidirectional> {
public:
ssl_iostream_device(ssl_stream &_stream, bool _use_ssl ) : stream(_stream)
{
use_ssl = _use_ssl;
need_handshake = _use_ssl;
}
void handshake(ssl::stream_base::handshake_type role)
{
if (!need_handshake) return;
need_handshake = false;
stream.handshake(role);
}
std::streamsize read(char* s, std::streamsize n)
{
handshake(ssl::stream_base::server); // HTTPS servers read first
if (use_ssl) return stream.read_some(boost::asio::buffer(s, n));
return stream.next_layer().read_some(boost::asio::buffer(s, n));
}
std::streamsize write(const char* s, std::streamsize n)
{
handshake(ssl::stream_base::client); // HTTPS clients write first
if (use_ssl) return boost::asio::write(stream, boost::asio::buffer(s, n));
return boost::asio::write(stream.next_layer(), boost::asio::buffer(s, n));
}
private:
bool need_handshake;
bool use_ssl;
ssl_stream& stream;
};
std::string HTTPReply(int nStatus, const std::string& strMsg)
{
std::string strStatus;
if (nStatus == 200) strStatus = "OK";
else if (nStatus == 400) strStatus = "Bad Request";
else if (nStatus == 404) strStatus = "Not Found";
else if (nStatus == 500) strStatus = "Internal Server Error";
std::ostringstream s;
s << "HTTP/1.1 " << nStatus << " " << strStatus << "\r\n"
<< "Connection: close\r\n"
<< "Content-Length: " << strMsg.size() << "\r\n"
<< "Content-Type: application/json\r\n"
<< "Date: Sat, 09 Jul 2009 12:04:08 GMT\r\n"
<< "Server: json-rpc/1.0\r\n"
<< "\r\n"
<< strMsg;
return s.str();
}
void handle_request(std::iostream& s)
{
s << HTTPReply(200, "Okely-Dokely\n") << std::flush;
}
int main(int argc, char* argv[])
{
bool use_ssl = (argc <= 1);
// Bind to loopback 127.0.0.1 so the socket can only be accessed locally
io_service io_service;
ip::tcp::endpoint endpoint(ip::address_v4::loopback(), 1111);
ip::tcp::acceptor acceptor(io_service, endpoint);
ssl::context context(io_service, ssl::context::sslv23);
context.set_options(
ssl::context::default_workarounds
| ssl::context::no_sslv2);
context.use_certificate_chain_file("server.cert");
context.use_private_key_file("server.pem", ssl::context::pem);
for(;;)
{
ip::tcp::endpoint peer_endpoint;
ssl_stream _ssl_stream(io_service, context);
ssl_iostream_device d(_ssl_stream, use_ssl);
boost::iostreams::stream<ssl_iostream_device> ssl_iostream(d);
// Accept connection
acceptor.accept(_ssl_stream.lowest_layer(), peer_endpoint);
std::string method;
std::string path;
ssl_iostream >> method >> path;
handle_request(ssl_iostream);
}
}

#Guy's suggestion (using boost::asio::streambuf) should work, and it's probably the easiest to implement. The main drawback to that approach is that everything you write to the iostream will be buffered in memory until the end, when the call to boost::asio::write() will dump the entire contents of the buffer onto the ssl stream at once. (I should note that this kind of buffering can actually be desirable in many cases, and in your case it probably makes no difference at all since you've said it's a low-volume application).
If this is just a "one-off" I would probably implement it using #Guy's approach.
That being said -- there are a number of good reasons that you might rather have a solution that allows you to use iostream calls to write directly into your ssl_stream. If you find that this is the case, then you'll need to build your own wrapper class that extends std::streambuf, overriding overflow(), and sync() (and maybe others depending on your needs).
Fortunately, boost::iostreams provides a relatively easy way to do this without having to mess around with the std classes directly. You just build your own class that implements the appropriate Device contract. In this case that's Sink, and the boost::iostreams::sink class is provided as a convenient way to get most of the way there. Once you have a new Sink class that encapsulates the process of writing to your underlying ssl_stream, all you have to do is create a boost::iostreams::stream that is templated to your new device type, and off you go.
It will look something like the following (this example is adapted from here, see also this related stackoverflow post):
//---this should be considered to be "pseudo-code",
//---it has not been tested, and probably won't even compile
//---
#include <boost/iostreams/concepts.hpp>
// other includes omitted for brevity ...
typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_stream;
class ssl_iostream_sink : public sink {
public:
ssl_iostream_sink( ssl_stream *theStream )
{
stream = theStream;
}
std::streamsize write(const char* s, std::streamsize n)
{
// Write up to n characters to the underlying
// data sink into the buffer s, returning the
// number of characters written
boost::asio::write(*stream, boost::asio::buffer(s, n));
}
private:
ssl_stream *stream;
};
Now, your accept loop might change to look something like this:
for(;;)
{
// Accept connection
ssl_stream stream(io_service, context);
tcp::endpoint peer_endpoint;
acceptor.accept(stream.lowest_layer(), peer_endpoint);
boost::system::error_code ec;
stream.handshake(boost::asio::ssl::stream_base::server, ec);
if (!ec) {
// wrap the ssl stream with iostream
ssl_iostream_sink my_sink(&stream);
boost::iostream::stream<ssl_iostream_sink> iostream_object(my_sink);
// Now it works the way you want...
iostream_object << HTTPReply(200, "Okely-Dokely\n") << std::flush;
}
}
That approach hooks the ssl stream into the iostream framework. So now you should be able to do anything to iostream_object in the above example, that you would normally do with any other std::ostream (like stdout). And the stuff that you write to it will get written into the ssl_stream behind the scenes. Iostreams has built-in buffering, so some degree of buffering will take place internally -- but this is a good thing -- it will buffer until it has accumulated some reasonable amount of data, then it will dump it on the ssl stream, and go back to buffering. The final std::flush, should force it to empty the buffer out to the ssl_stream.
If you need more control over internal buffering (or any other advanced stuff), have a look at the other cool stuff available in boost::iostreams. Specifically, you might start by looking at stream_buffer.
Good luck!

I think what you want to do is use stream buffers (asio::streambuf)
Then you can do something like (untested code written on the fly follows):
boost::asio::streambuf msg;
std::ostream msg_stream(&msg);
msg_stream << "hello world";
msg_stream.flush();
boost::asio::write(stream, msg);
Similarly your read/receive side can read into a stream buffer in conjunction with std::istream so you can process your input using various stream functions/operators.
Asio reference for streambuf
Another note is I think you should check out the asio tutorials/examples. Once you do you'll probably want to change your code to work asynchronously rather than the synchronous example you're showing above.

ssl::stream could be wrapped with boost::iostreams / bidirectional to mimic similar behaviours as tcp::iostream. flushing output before further reading seems cannot be avoided.
#include <regex>
#include <string>
#include <iostream>
#include <boost/iostreams/stream.hpp>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
namespace bios = boost::iostreams;
namespace asio = boost::asio;
namespace ssl = boost::asio::ssl;
using std::string;
using boost::asio::ip::tcp;
using boost::system::system_error;
using boost::system::error_code;
int parse_url(const std::string &s,
std::string& proto, std::string& host, std::string& path)
{
std::smatch m;
bool found = regex_search(s, m, std::regex("^(http[s]?)://([^/]*)(.*)$"));
if (m.size() != 4)
return -1;
proto = m[1].str();
host = m[2].str();
path = m[3].str();
return 0;
}
void get_page(std::iostream& s, const string& host, const string& path)
{
s << "GET " << path << " HTTP/1.0\r\n"
<< "Host: " << host << "\r\n"
<< "Accept: */*\r\n"
<< "Connection: close\r\n\r\n" << std::flush;
std::cout << s.rdbuf() << std::endl;;
}
typedef ssl::stream<tcp::socket> ssl_socket;
class ssl_wrapper : public bios::device<bios::bidirectional>
{
ssl_socket& sock;
public:
typedef char char_type;
ssl_wrapper(ssl_socket& sock) : sock(sock) {}
std::streamsize read(char_type* s, std::streamsize n) {
error_code ec;
auto rc = asio::read(sock, asio::buffer(s,n), ec);
return rc;
}
std::streamsize write(const char_type* s, std::streamsize n) {
return asio::write(sock, asio::buffer(s,n));
}
};
int main(int argc, char* argv[])
{
std::string proto, host, path;
if (argc!= 2 || parse_url(argv[1], proto, host, path)!=0)
return EXIT_FAILURE;
try {
if (proto != "https") {
tcp::iostream s(host, proto);
s.expires_from_now(boost::posix_time::seconds(60));
get_page(s, host, path);
} else {
asio::io_service ios;
tcp::resolver resolver(ios);
tcp::resolver::query query(host, "https");
tcp::resolver::iterator endpoint_iterator =
resolver.resolve(query);
ssl::context ctx(ssl::context::sslv23);
ctx.set_default_verify_paths();
ssl_socket socket(ios, ctx);
asio::connect(socket.lowest_layer(), endpoint_iterator);
socket.set_verify_mode(ssl::verify_none);
socket.set_verify_callback(ssl::rfc2818_verification(host));
socket.handshake(ssl_socket::client);
bios::stream<ssl_wrapper> ss(socket);
get_page(ss, host, path);
}
} catch (const std::exception& e) {
std::cout << "Exception: " << e.what() << "\n";
}
}

Related

Boost Beast Read Conent By Portions

I am trying to understand how can I limit the amount of data that is read from the internet by calling 'read_some' function in boost beast.
The starting point is the incremental read example in the beast's docs.
From the docs I understood that the really read data is stored in the flat_buffer.
I make the following experiment:
Set max flat_buffer's size to 1024
Connect to a relatively large (several KB) html page
Call read_some one time
Turn the internet off
Try to read the page to the end
Since buffer's capacity is not large enough to store the entire page, my experiment should fail - I should not be able to read the entire page. Nevertheless, it finishes successfully. That means that there exists some additional buffer where the read data is stored. But what is it made for and how can I limit its size?
UPD
Here is my source code:
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/strand.hpp>
#include <cstdlib>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using namespace http;
template<
bool isRequest,
class SyncReadStream,
class DynamicBuffer>
void
read_and_print_body(
std::ostream& os,
SyncReadStream& stream,
DynamicBuffer& buffer,
boost::beast::error_code& ec ) {
parser<isRequest, buffer_body> p;
read_header( stream, buffer, p, ec );
if ( ec )
return;
while ( !p.is_done()) {
char buf[512];
p.get().body().data = buf;
p.get().body().size = sizeof( buf );
read_some( stream, buffer, p, ec );
if ( ec == error::need_buffer )
ec = {};
if ( ec )
return;
os.write( buf, sizeof( buf ) - p.get().body().size );
}
}
int main(int argc, char** argv)
{
try
{
// Check command line arguments.
if(argc != 4 && argc != 5)
{
std::cerr <<
"Usage: http-client-sync <host> <port> <target> [<HTTP version: 1.0 or 1.1(default)>]\n" <<
"Example:\n" <<
" http-client-sync www.example.com 80 /\n" <<
" http-client-sync 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;
// These objects perform our I/O
boost::asio::ip::tcp::resolver resolver(ioc);
beast::tcp_stream stream(ioc);
// Look up the domain name
auto const results = resolver.resolve(host, port);
// Make the connection on the IP address we get from a lookup
stream.connect(results);
// Set up an HTTP GET request message
http::request<http::string_body> req{http::verb::get, target, version};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
// Send the HTTP request to the remote host
http::write(stream, req);
// This buffer is used for reading and must be persisted
beast::flat_buffer buffer;
boost::beast::error_code ec;
read_and_print_body<false>(std::cout, stream, buffer, ec);
}
catch(std::exception const& e)
{
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
The operating system's TCP IP stack obviously needs to buffer data, so that's likely where it gets buffered.
The way to test your desired scenario:
Live On Coliru
#include <boost/beast.hpp>
#include <iostream>
#include <thread>
namespace net = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;
using net::ip::tcp;
void server()
{
net::io_context ioc;
tcp::acceptor acc{ioc, {{}, 8989}};
acc.listen();
auto conn = acc.accept();
http::request<http::string_body> msg(
http::verb::get, "/", 11, std::string(20ull << 10, '*'));
msg.prepare_payload();
http::request_serializer<http::string_body> ser(msg);
size_t hbytes = write_header(conn, ser);
// size_t bbytes = write_some(conn, ser);
size_t bbytes = write(conn, net::buffer(msg.body(), 1024));
std::cout << "sent " << hbytes << " header and " << bbytes << "/"
<< msg.body().length() << " of body" << std::endl;
// closes connection
}
namespace {
template<bool isRequest, class SyncReadStream, class DynamicBuffer>
auto
read_and_print_body(
std::ostream& /*os*/,
SyncReadStream& stream,
DynamicBuffer& buffer,
boost::beast::error_code& ec)
{
struct { size_t hbytes = 0, bbytes = 0; } ret;
http::parser<isRequest, http::buffer_body> p;
//p.header_limit(8192);
//p.body_limit(1024);
ret.hbytes = read_header(stream, buffer, p, ec);
if(ec)
return ret;
while(! p.is_done())
{
char buf[512];
p.get().body().data = buf;
p.get().body().size = sizeof(buf);
ret.bbytes += http::read_some(stream, buffer, p, ec);
if(ec == http::error::need_buffer)
ec = {};
if(ec)
break;
//os.write(buf, sizeof(buf) - p.get().body().size);
}
return ret;
}
}
void client()
{
net::io_context ioc;
tcp::socket conn{ioc};
conn.connect({{}, 8989});
beast::error_code ec;
beast::flat_buffer buf;
auto [hbytes, bbytes] = read_and_print_body<true>(std::cout, conn, buf, ec);
std::cout << "received hbytes:" << hbytes << " bbytes:" << bbytes
<< " (" << ec.message() << ")" << std::endl;
}
int main()
{
std::jthread s(server);
std::this_thread::sleep_for(std::chrono::seconds(1));
std::jthread c(client);
}
Prints
sent 41 header and 1024/20480 of body
received 1065 bytes of message (partial message)
Side Notes
You start your question with:
I am trying to understand how can I limit the amount of data that is read from the internet
That's built in to Beast
by calling 'read_some' function in boost beast.
To just limit the total amount of data read, you don't have to use read_some in a loop (http::read by definition already does exactly that).
E.g. with the above example, if you replace 20ull<<10 (20 KiB) with 20ull<<20 (20 MiB) you will exceed the default size limit:
http::request<http::string_body> msg(http::verb::get, "/", 11,
std::string(20ull << 20, '*'));
Prints Live On Coliru
sent 44 header and 1024/20971520 of body
received hbytes:44 bbytes:0 (body limit exceeded)
You can also set your own parser limits:
http::parser<isRequest, http::buffer_body> p;
p.header_limit(8192);
p.body_limit(1024);
Which prints Live On Coliru:
sent 41 header and 1024/20480 of body
received hbytes:41 bbytes:0 (body limit exceeded)
As you can see it even knows to reject the request after just reading the headers, using the content-length information from the headers.

Reading a serialized struct at the receiver end boost asio

I am new to boost and networking ;). I am making a client server application with boost::asio, I need to pass structs as messages so used boost::asio::serialization for it :
test.h
#pragma once
#include <boost/archive/binary_oarchive.hpp>
#include <boost/serialization/serialization.hpp>
struct Test
{
public:
int a;
int b;
template<typename archive> void serialize(archive& ar, const unsigned version) {
ar & a;
ar & b;
}
};
client side sending:
void send_asynchronously(tcp::socket& socket) {
Test info;
info.a = 1;
info.b = 2;
{
std::ostream os(&buf);
boost::archive::binary_oarchive out_archive(os);
out_archive << info;
}
async_write(socket, buf, on_send_completed);
}
On the receiver side, I read the data into a boost::asio::buffer, I want to know a way to parse this buffer and extract the object on server side. Please help.
You don't show enough code to know how you declared buf or managed the lifetime.
I'm assuming you used boost::asio::streambuf buf; and it has static storage duration (namespace scope) or is a class member (but you didn't show a class).
Either way, whatever you have you can do "the same" in reverse to receive.
Here's a shortened version (that leaves out the async so we don't have to make guesses about the lifetimes of things like I mentioned above);
Connect
Let's connect to an imaginary server (we can make one below) at port 3001 on localhost:
asio::io_context ioc;
asio::streambuf buf;
tcp::socket s(ioc, tcp::v4());
s.connect({{}, 3001});
Serialize
Basically what you had:
{
std::ostream os(&buf);
boost::archive::binary_oarchive oa(os);
Test req {13,31};
oa << req;
}
Note the {} scope around the stream/archive make sure the archive is completed before sending.
Send
/*auto bytes_sent =*/ asio::write(s, buf);
Receive
Let's assume our server sends back another Test object serialized in the same way¹.
Reading into the buffer, assuming no framing we'll just "read until the end of the stream":
boost::system::error_code ec;
/*auto bytes_received =*/ asio::read(s, buf, ec);
if (ec && ec != asio::error::eof) {
std::cout << "Read error: " << ec.message() << "\n";
return 1;
}
In real life you want timeouts and limits to the amount of data read. Often your protocol will add framing where you know what amount of data to read or what boundary marker to expect.
Deserialize
Test response; // uninitialized
{
std::istream is(&buf);
boost::archive::binary_iarchive ia(is);
ia >> response;
}
Full Demo
Live On Coliru
#include <boost/asio.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/serialization/serialization.hpp>
#include <iostream>
namespace asio = boost::asio;
using tcp = boost::asio::ip::tcp;
struct Test {
int a,b;
template<typename Ar> void serialize(Ar& ar, unsigned) { ar & a & b; }
};
int main() {
asio::io_context ioc;
asio::streambuf buf;
tcp::socket s(ioc, tcp::v4());
s.connect({{}, 3001});
///////////////////
// send a "request"
///////////////////
{
std::ostream os(&buf);
boost::archive::binary_oarchive oa(os);
Test req {13,31};
oa << req;
}
/*auto bytes_sent =*/ asio::write(s, buf);
/////////////////////
// receive "response"
/////////////////////
boost::system::error_code ec;
/*auto bytes_received =*/ asio::read(s, buf, ec);
if (ec && ec != asio::error::eof) {
std::cout << "Read error: " << ec.message() << "\n";
return 1;
}
Test response; // uninitialized
{
std::istream is(&buf);
boost::archive::binary_iarchive ia(is);
ia >> response;
}
std::cout << "Response: {" << response.a << ", " << response.b << "}\n";
}
Using netcat to mock a server with a previously generated response Test{42,99} (base64 encoded here):
base64 -d <<<"FgAAAAAAAABzZXJpYWxpemF0aW9uOjphcmNoaXZlEgAECAQIAQAAAAAAAAAAKgAAAGMAAAA=" | nc -N -l -p 3001
It prints:
Response: {42, 99}
¹ on the same architecture and compiled with the same version of boost, because Boost's binary archives are not portable. The live demo is good demonstration of this

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.

Boost asio async operation bad file descriptor

I'm usig boost asio for an IRC bot, and one of my async operation results in a bad file descriptor. I tried to put the socket in a shared_ptr, but I still got the "Bad File Descriptor" error. I don't know whats wrong in it.
Here are the files, I omitted some of the functions from the cpp file. But I you want to read the full file, it's here on my Github.
The error happends in the _read function.
Thanks!
irc.hpp
#ifndef H_IRC
#define H_IRC
#include <vector>
#include <boost/asio.hpp>
#include <boost/tokenizer.hpp>
#include <boost/shared_ptr.hpp>
class Irc
{
public:
Irc(const std::string &server, const std::string &port, const std::function<void()> onConnect);
void connect();
void close();
void user(const std::string &username);
void user(const std::string &username, const std::string &hostname, const std::string &server, const std::string &realname);
void nick(std::string &nickname);
void join(const std::string &chan);
void part(const std::string &chan);
void privmsg(const std::string &to, const std::string &msg);
void command(const std::string &cmd, const std::string &msg);
void command(const std::string &cmd, const std::string &to, const std::string &msg);
void run();
private:
void _read(const boost::system::error_code &error);
void _send(std::string &message);
void _readHandler(const boost::tokenizer<boost::char_separator<char> > &tokenizer);
void _connectHandler(const boost::system::error_code &error);
void _pong(const std::string &ping);
std::string _server;
std::string _port;
std::string _chan;
std::vector<std::function<void (const boost::tokenizer<boost::char_separator<char> >&)>> _readHandlers;
std::function<void()> _onConnect;
boost::asio::streambuf _buffer;
boost::asio::io_service _ios;
boost::shared_ptr<boost::asio::ip::tcp::socket> _socket;
};
#endif
irc.cpp
#include "irc.hpp"
#include <iostream>
#include <boost/bind.hpp>
#include <boost/make_shared.hpp>
Irc::Irc(const std::string &server, const std::string &port, const std::function<void()> onConnect)
: _server(server), _port(port), _onConnect(onConnect),
_socket(boost::make_shared<boost::asio::ip::tcp::socket>(boost::ref(_ios)))
{
// Ping back handler
_readHandlers.push_back([this](const boost::tokenizer<boost::char_separator<char> > &tokenizer) {
std::vector<std::string> tokens(begin(tokenizer), end(tokenizer));
if(tokens[0].compare("PING") == 0)
_pong(tokens[1]);
});
}
void Irc::connect()
{
boost::asio::ip::tcp::resolver resolver(_ios);
boost::asio::ip::tcp::resolver::query query(_server, _port);
boost::asio::ip::tcp::resolver::iterator it = resolver.resolve(query);
boost::asio::ip::tcp::resolver::iterator end;
boost::system::error_code error = boost::asio::error::host_not_found;
while(it != end)
{
if(!error)
break;
std::cout << "Connecting to " << _server << " " << _port << std::endl;
boost::asio::async_connect(*_socket, it,
boost::bind(&Irc::_connectHandler, this, error)
);
it++;
if(error)
std::cout << "Error : " << error.message() << std::endl;
}
if(error)
std::cout << "Error connectinf to " << _server << " " << error.message() << std::endl;
else
std::cout << "Connection success" << std::endl;
}
void Irc::close()
{
_socket->close();
_ios.stop();
}
void Irc::run()
{
boost::asio::async_read_until(*_socket, _buffer, "\r\n",
boost::bind(&Irc::_read, this,
boost::asio::placeholders::error
)
);
_ios.run();
}
/*
* Private
*/
void Irc::_read(const boost::system::error_code &error)
{
if(error)
{
std::cerr << "Error in read : " << error.message() << std::endl;
}
else
{
std::string data(buffers_begin(_buffer.data()), buffers_begin(_buffer.data()) + _buffer.size());
std::cout << data << std::endl;
boost::char_separator<char> sep("!#:; ");
boost::tokenizer<boost::char_separator<char> > tokenizer(data, sep);
_readHandler(tokenizer);
boost::asio::async_read_until(*_socket, _buffer, "\r\n",
boost::bind(&Irc::_read, this,
boost::asio::placeholders::error
)
);
}
}
inline void Irc::_send(std::string &message)
{
boost::asio::write(*_socket, boost::asio::buffer(message + "\r\n"));
}
void Irc::_readHandler(const boost::tokenizer<boost::char_separator<char> > &tokenizer)
{
for(auto it : _readHandlers)
it(tokenizer);
}
void Irc::_connectHandler(const boost::system::error_code &error)
{
if(!error)
{
_onConnect();
}
}
connect is never called.
This causes the "bad file handle" error
Further Notes
Suddenly, _send uses synchronous asio::write. Why?
Error handling should probably be added there, too (catch or pass error_code& argument).
There's only one socket which never gets re-initialized or assigned. Embedding it into a shared pointer isn't changing anything¹.
This however is strange:
std::cout << "Connecting to " << _server << " " << _port << std::endl;
boost::asio::async_connect(*_socket, it,
boost::bind(&Irc::_connectHandler, this, error)
);
This does potentially many asynchronous connect operations on the same socket simultaneously. This is a data race and therefore Undefined Behaviour, see: documentation.
So you need to fix it to use several sockets or sequential.
Turns out, this is very simple: you're already using the free function version of async_connect:
http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/async_connect/overload1.html
This function attempts to connect a socket to one of a sequence of endpoints. It does this by repeated calls to the socket's async_connect member function, once for each endpoint in the sequence, until a connection is successfully established.
So the fix is to just call it once.
Your bind doesn't use a placeholder, instead uses a hardcoded error!
boost::system::error_code error = boost::asio::error::host_not_found;
boost::asio::async_connect(_socket, it, boost::bind(&Irc::_connectHandler, this, error));
Needs to be more like
boost::asio::async_connect(_socket, it, boost::bind(&Irc::_connectHandler, this, boost::asio::placeholders::error()));
after handling incoming traffic, you need to consume the buffer contents, or it will infinitely repeat the same:
_readHandler(tokenizer);
_buffer.consume(_buffer.size());
Pull Request
https://github.com/Bl4ckb0ne/irc-boost/pull/1
Adds:
869c225 Use shared_ptr
9042c6d Add function level trace
50dee1b Revert shared_ptr and rename _onConnect(ed)
20475b9 Fixing the async_connect debacle
c6d8a2e Fixed channel handling and consistency join/part
6fd9242 Initiate `connect()` instead of read from `run()`
06a6c06 Do **not** assume contiguous buffer storage (UB)
090fe8c Consume handled input
68e5e8a Comment
All the above changed, I have successfully connected to an IRC channel and receiving mesages.
¹ (especially not unless you make sure something hangs on to an instance of the shared_ptr)

boost::asio separate array for each client

I learned C++ and now I would like to move on and learn some network programming. I decided to use boost::asio because it's multiplatform. I wrote this simple program:
client:
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;
enum { max_length = 1000000 };
int main(int argc, char* argv[])
{
while(1)
{
try
{
if (argc != 3)
{
std::cerr << "Usage: blocking_tcp_echo_client <host> <port>\n";
return 1;
}
boost::asio::io_service io_service;
tcp::resolver resolver(io_service);
tcp::resolver::query query(tcp::v4(), argv[1], argv[2]);
tcp::resolver::iterator iterator = resolver.resolve(query);
tcp::socket s(io_service);
s.connect(*iterator);
using namespace std; // For strlen.
std::cout << "Enter message: ";
char request[max_length];
std::cin.getline(request, max_length);
if (request == "\n")
continue;
size_t request_length = strlen(request);
boost::asio::write(s, boost::asio::buffer(request, request_length));
char reply[max_length];
boost::system::error_code error;
size_t reply_length = s.read_some(boost::asio::buffer(reply), error);
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::cout << "Reply is: ";
std::cout.write(reply, reply_length);
std::cout << "\n";
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
exit(1);
}
}
return 0;
}
server:
#include <cstdlib>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/smart_ptr.hpp>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/regex.hpp>
#include <boost/lexical_cast.hpp>
#include <string>
using boost::asio::ip::tcp;
const int max_length = 1000000;
std::string user_array[100];
typedef boost::shared_ptr<tcp::socket> socket_ptr;
unsigned short analyze_user_request(std::string& user_request, short unsigned* ID, std::string* request_value)
{
// function returns:
// 0: if user request is incorrect
// 1: if user requests "PUT" operation
// 2: if user requests "GET" operation
// Furthermore, if request is correct, its value (i.e. ID number and/or string) is saved to short unsigned and string values passed by pointers.
boost::regex exp("^[[:space:]]*(PUT|GET)[[:space:]]+([[:digit:]]{1,2})(?:[[:space:]]+(.*))?$");
boost::smatch what;
if (regex_match(user_request, what, exp, boost::match_extra))
{
short unsigned id_number = boost::lexical_cast<short unsigned>(what[2]);
if (what[1] == "PUT")
{
boost::regex exp1("^[a-zA-Z0-9]+$");
std::string value = boost::lexical_cast<std::string>(what[3]);
if (value.length() > 4095)
return 0;
if (!regex_match(value, exp1))
return 0;
else
{
*request_value = value;
*ID = id_number;
return 1;
}
}
if (what[1] == "GET")
{
*ID = id_number;
return 2;
}
}
if (!regex_match(user_request, what, exp, boost::match_extra))
return 0;
}
void session(socket_ptr sock)
{
try
{
for (;;)
{
char data[max_length];
boost::system::error_code error;
size_t length = sock->read_some(boost::asio::buffer(data), error);
if (error == boost::asio::error::eof)
break; // Connection closed cleanly by peer.
else if (error)
throw boost::system::system_error(error); // Some other error.
// convert buffer data to string for further procession
std::string line(boost::asio::buffers_begin(boost::asio::buffer(data)), boost::asio::buffers_begin(boost::asio::buffer(data)) + length);
std::string reply; // will be "QK", "INVALID", or "OK <value>"
unsigned short vID;
unsigned short* ID = &vID;
std::string vrequest_value;
std::string* request_value = &vrequest_value;
unsigned short output = analyze_user_request(line, ID, request_value);
if (output == 1)
{
// PUT
reply = "OK";
user_array[*ID] = *request_value;
}
else if (output == 2)
{
// GET
reply = user_array[*ID];
if (reply == "")
reply = "EMPTY";
}
else
reply = "INVALID";
boost::system::error_code ignored_error;
size_t ans_len=reply.length();
boost::asio::write(*sock, boost::asio::buffer(reply));
}
}
catch (std::exception& e)
{
std::cerr << "Exception in thread: " << e.what() << "\n";
}
}
void server(boost::asio::io_service& io_service, short port)
{
tcp::acceptor a(io_service, tcp::endpoint(tcp::v4(), port));
for (;;)
{
socket_ptr sock(new tcp::socket(io_service));
a.accept(*sock);
boost::thread t(boost::bind(session, sock));
}
}
int main(int argc, char* argv[])
{
try
{
if (argc != 2)
{
std::cerr << "Usage: blocking_tcp_echo_server <port>\n";
return 1;
}
boost::asio::io_service io_service;
using namespace std; // For atoi.
server(io_service, atoi(argv[1]));
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
Basically, it's an application that allows user to store data on server. User can insert new data using PUT command followed by ID number and data value, and retrieve data using GET command followed by ID. User requests are processed in analyze_user_request function and are subsequently written to or read from array. The problem is that now all clients are using the same global arry. That means that if one client saves something under particular ID all other clients can read it, because they access the same array. I wonder, how can I associate array with different clients, and create a new array when a new client connects?
What about encapsulating session data into a class and create separate session object for each connection. Approximately it can look like this:
Session class definition:
class Session {
public:
// logic from your session function
void handleRequests(socket_ptr sock);
private:
// session data here
}
typedef boost::shared_ptr<Session> SessionPtr;
In "server" function in accept loop create new object and pass it to new thread:
SessionPtr newSession(new Session());
boost::thread acceptThread(boost::bind(&Session::handleRequests, newSession, sock));
Sorry for possible mistakes in code, I am far from my development environment it can not test it.
For more elegant solution for handling several connections separatly see boost::asio example "Chat server": http://www.boost.org/doc/libs/1_47_0/doc/html/boost_asio/example/chat/chat_server.cpp