I have following code:
class Connection
{
public:
Connection(boost::asio::io_context& ctx)
: context(ctx),
timer(ctx),
socket(ctx)
{
buf.resize(10000);
}
void connect(const std::string& ip, uint16_t port)
{
boost::system::error_code code;
boost::asio::ip::tcp::endpoint ep(ip::address::from_string(ip), port);
socket.connect(ep, code);
std::cout << "Connection status " << code.message() << std::endl;
}
void write()
{
std::string request;
request = "GET / HTTP/1.1\r\n";
request = "Host: hostname.com\r\n\r\n";
std::cout << "Trying to write..." << std::endl;
boost::asio::async_write(socket, boost::asio::buffer(request), transfer_all(),
[=](const boost::system::error_code& e, std::size_t len)
{
std::cout << "request size is " << request.size() << std::endl;
std::cout << "written size is " << len << std::endl;
});
}
void read()
{
timer.expires_from_now(boost::posix_time::milliseconds(4000));
timer.async_wait([this](const boost::system::error_code& e)
{
if (!e) {
std::cout << "Socket closed by timer" << std::endl;
socket.close();
}
});
async_read(socket, boost::asio::buffer(buf), transfer_at_least(1),
[this](const boost::system::error_code& e, std::size_t len)
{
std::cout << "read callback" << std::endl;
sleep(1);
std::cout << buf.substr(0, len);
if (!e)
read();
});
}
private:
std::string buf;
boost::asio::io_context& context;
boost::asio::deadline_timer timer;
boost::asio::ip::tcp::socket socket;
};
int main(int argc, char** argv)
{
boost::asio::io_context ctx;
Connection connection(ctx);
/*std::thread runThread([&ctx]()
{
});*/
//sleep(2);
connection.connect("ip_address", 80);
connection.write();
boost::shared_ptr<io_context::work> work(new io_context::work(ctx));
std::cout << "Waiting for read..." << std::endl;
connection.read();
ctx.run();
}
I expect it to work like this: When the read function is called, we update the timer each time. If the timer expires before the data is available for reading, we will close the socket and thus cancel the asynchronous read operation. I read on the Internet that there may be a spurious wakeup on the timer, but I did not fully figure it out. How reliable is this code?
Socket and timer uses same io_context
CodePudding user response:
Your write
function has big problems. You're initiating an async_write on a temporary (local) request
(You even access request
from the completion handler). This leads to undefined behaviour.
Your time callback looks fine, except it could be more specific about the error code. Most likely "spurious wake-up" would refer to completion with error::operation_aborted
when the timer is canceled. This will also happen when you reset it (with expires_from_now
).
I would probably write it more like
timer.expires_from_now(4000ms);
timer.async_wait([this](error_code ec) {
if (ec != boost::asio::error::operation_aborted)
return;
std::cout << "Socket closed by timer" << std::endl;
socket.cancel();
});
Note the cancel()
which should suffice.
Here's a redo using some simplifications and good practices:
//#define BOOST_ASIO_ENABLE_HANDLER_TRACKING 1
#include <boost/asio.hpp>
#include <iostream>
using boost::asio::ip::tcp;
using boost::system::error_code;
using std::this_thread::sleep_for;
using namespace std::chrono_literals;
class Connection {
public:
template <typename Executor>
Connection(Executor ex) : timer(ex)
, socket(ex) {
buf.resize(10000);
}
void connect(const std::string& ip, uint16_t port) {
std::cout << "Connecting" << std::endl;
auto addr = boost::asio::ip::address::from_string(ip);
socket.connect({addr, port});
std::cout << "Connected to " << socket.remote_endpoint() << std::endl;
}
void write() {
std::cout << "Trying to write..." << std::endl;
async_write( //
socket, boost::asio::buffer(request),
// boost::asio::transfer_all(),
[this](error_code ec, size_t transferred) {
std::cout << "request size is " << request.size() << "\n"
<< "written size is " << transferred << " ("
<< ec.message() << ")" << std::endl;
});
}
void read() {
timer.expires_from_now(4000ms);
timer.async_wait([this](error_code ec) {
if (ec != boost::asio::error::operation_aborted)
return;
socket.cancel();
});
async_read( //
socket, boost::asio::buffer(buf), boost::asio::transfer_at_least(1),
[this](error_code ec, size_t transferred) {
if (ec == boost::asio::error::operation_aborted) {
std::cout << "Socket closed by timer" << std::endl;
return;
}
std::cout << "read callback (" << ec.message() << ", "
<< transferred << " bytes)" << std::endl;
sleep_for(1s);
//std::cout << buf.substr(0, transferred);
if (!ec)
read();
});
}
private:
std::string request = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n";
std::string buf;
boost::asio::steady_timer timer;
tcp::socket socket;
};
int main() {
boost::asio::io_context ctx;
// strand not required unless multi-threading:
Connection connection(make_strand(ctx.get_executor()));
sleep_for(2s);
connection.connect("93.184.216.34", 80);
connection.write();
connection.read();
//auto work = make_work_guard(ctx); // not required
std::cout << "Waiting for read..." << std::endl;
ctx.run();
}
Prints
Connecting
Connected to 93.184.216.34:80
Trying to write...
Waiting for read...
request size is 41
written size is 41 (Success)
read callback (Success, 1591 bytes)
Socket closed by timer
Now, I wouldn't bother with transfer_at_least
and friends. If you want, use async_read_until
with a completion condition or boundary like "\r\n\r\n"
. I would probably also just receive into a dynamic buffer:
async_read_until( //
socket, boost::asio::dynamic_buffer(buf), "\r\n\r\n",
[this](error_code ec, size_t transferred) {
if (ec == boost::asio::error::operation_aborted) {
std::cout << "Socket closed by timer" << std::endl;
return;
}
std::cout << "read callback (" << ec.message() << ", "
<< transferred << " bytes)\n"
<< "Headers only: "
<< std::string_view(buf).substr(0, transferred)
<< "Remaining in buffer (start of body): "
<< buf.size() - transferred << std::endl;
});
Now you can expect to see: Live
Connecting
Connected to 93.184.216.34:80
Trying to write...
Waiting for read...
request size is 41
written size is 41 (Success)
read callback (Success, 335 bytes)
Headers only: HTTP/1.1 200 OK
Age: 550281
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sat, 13 Aug 2022 11:52:28 GMT
Etag: "3147526947 ident"
Expires: Sat, 20 Aug 2022 11:52:28 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (chb/0286)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1256
Remaining in buffer (start of body): 177
Bonus: Use a Library!
In reality I'd be using Boost Beast: Live On Coliru
#include <boost/beast.hpp>
#include <iostream>
using boost::asio::ip::tcp;
using namespace std::chrono_literals;
namespace beast = boost::beast;
namespace http = beast::http;
int main() {
boost::asio::io_context ctx;
beast::tcp_stream conn(ctx.get_executor());
conn.connect({boost::asio::ip::address::from_string("93.184.216.34"), 80});
conn.expires_after(1000ms);
auto req = http::request<http::empty_body>(http::verb::get, "/", 11);
req.set(http::field::host, "www.example.com");
write(conn, req);
conn.expires_after(4000ms);
http::response<http::string_body> res;
beast::flat_buffer buf;
read(conn, buf, res);
std::cout << "Headers: " << res.base() << "\n";
std::cout << "Body received: " << res.body().length() << "\n";
}
Prints
Headers: HTTP/1.1 200 OK
Age: 551081
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sat, 13 Aug 2022 12:05:48 GMT
Etag: "3147526947 ident"
Expires: Sat, 20 Aug 2022 12:05:48 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (chb/0286)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1256
Body received: 1256