Вопрос 1
ba::async_write(socket_, ba::buffer(string("OK")), yield[ecw]);
Это вызывает неопределенное поведение, поскольку вы передаете временную строку в качестве буфера, но асинхронная операция (по определению) не завершается до возврата вызова async_write
.
Следовательно, буфер является устаревшей ссылкой на что-то разрушенное в стеке или на то, что сейчас там живет.
Буфер отправки логически будет частью объекта self
, чтобы получить более правильный срок жизни. Или, поскольку вы выполняете сопрограммы и собираетесь в любом случае завершить сеанс, просто используйте write
вместо async_write
.
Вопрос 2
Это потому, что неопределенное поведение Неопределенное поведение . Может произойти все, что угодно .
Unasked
Вместо read_some
используйте read
с transfer_exactly(DATA_LEN_4)
или read_until
ссоответствующее условие завершения.
Вместо buffer(reserved_string)
вы можете dynamic_buffer
.
Вместо броска магических струн вы можете просто поймать system_error
где код указывает, какое возникло условие:
try {
timer_.expires_from_now(std::chrono::seconds(TIMEOUT_LIMIT));
// read data
std::string packet;
auto received_len = ba::async_read(socket_,
ba::dynamic_buffer(packet),
ba::transfer_exactly(DATA_LEN_4), yield);
assert(received_len == DATA_LEN_4); // guaranteed
// write back "OK"
ba::write(socket_, ba::buffer("OK"s));
}
catch (boost::system::system_error const& e) {
if (e.code() == ba::error::operation_aborted)
std::cout << "canceled (timeout)" << std::endl;
else if (e.code() == ba::error::eof)
std::cout << "eof" << std::endl;
else throw std::runtime_error(e.code().message());
}
Итак, теперь вы можете обернуть это своим общим блоком обработки исключений:
try {
// ...
} catch (std::exception const& e) {
std::cout << "exception: " << std::quoted(e.what()) << std::endl;
boost::system::error_code ignore;
ba::async_write(socket_, ba::buffer(std::string(e.what())), yield[ignore]);
socket_.close();
timer_.cancel();
}
Но!
- кажется весьма сомнительным, что информирование вашего клиента полезно или даже мудро
- Если не поймать исключение в coro, это все равно уничтожит экземпляр
self
, поэтомуВы можете просто позволить ему сбежать
Таймеры
Время завершения error_code
уже указывает, истек ли таймер или был отменен:
while (socket_.is_open()) {
boost::system::error_code ec;
timer_.async_wait(yield[ec]);
if (ba::error::operation_aborted != ec) // timer was not canceled
socket_.close();
}
Обратите внимание, однако, что обычные пути возврата из сеанса coro НЕ вызывают .cancel()
на time_
. Это приведет к тому, что сокет останется открытым еще <1 с, пока не истечет таймер. </p>
Исключения
Если вы хотите, чтобы исключения исключались из хранилища (вы можете,и вы должны учитывать, что это происходит), вы должны улучшить циклы потока, обрабатывая исключения: Следует ли перехватывать исключение, генерируемое boost :: asio :: io_service :: run ()?
Предлагаемый код для сервера
Объединение coros и значительное упрощение обработки всех условий:
#include <iostream>
#include <iomanip>
#include <boost/thread/thread.hpp>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/scope_exit.hpp>
using namespace std::literals;
namespace ba = boost::asio;
using ba::ip::tcp;
static constexpr unsigned short SERVER_PORT = 1234;
static constexpr std::size_t DATA_LEN_4 = 4;
static constexpr auto TIMEOUT_LIMIT = 1s;
struct session : public std::enable_shared_from_this<session>
{
tcp::socket socket_;
ba::steady_timer timer_;
ba::strand<ba::io_context::executor_type> strand_;
explicit session(ba::io_context& io_context, tcp::socket socket)
: socket_(std::move(socket)),
timer_(io_context),
strand_(io_context.get_executor())
{ }
void go() {
ba::spawn(strand_, [this, self = shared_from_this()](ba::yield_context yield) {
spawn(yield, [this, self](ba::yield_context yield) {
timer_.expires_from_now(TIMEOUT_LIMIT);
while (socket_.is_open()) {
boost::system::error_code ec;
timer_.async_wait(yield[ec]);
if (ba::error::operation_aborted != ec) // timer was not canceled
socket_.close();
}
});
try {
// read data
std::string packet;
ba::async_read(socket_,
ba::dynamic_buffer(packet),
ba::transfer_exactly(DATA_LEN_4), yield);
// write back "OK"
ba::write(socket_, ba::buffer("OK"s));
}
catch (boost::system::system_error const& e) {
if (e.code() == ba::error::operation_aborted)
std::cout << "canceled (timeout)" << std::endl;
else if (e.code() == ba::error::eof)
std::cout << "eof" << std::endl;
else // throw std::runtime_error(e.code().message());
std::cout << "other: " << e.code().message() << std::endl;
}
socket_.close();
timer_.cancel(); // cancel the other coro so we don't keep the session alive
});
}
};
int main() {
ba::io_context io_context;
ba::spawn(io_context, [&](ba::yield_context yield) {
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), SERVER_PORT));
for (;;) {
boost::system::error_code ec;
tcp::socket socket(io_context);
acceptor.async_accept(socket, yield[ec]);
if (!ec)
std::make_shared<session>(io_context, std::move(socket))->go();
}
});
boost::thread_group tgroup;
for (auto i = 0u; i < std::thread::hardware_concurrency(); ++i)
tgroup.create_thread([&io_context] {
for (;;) {
try { io_context.run(); break; } // exited normally
catch (std::exception const &e) { std::clog << "[eventloop] exception caught " << std::quoted(e.what()) << std::endl; }
catch (...) { std::clog << "[eventloop] unknown exception caught" << std::endl; }
}
});
tgroup.join_all();
}
С рандомизированным клиентом
Изменение режима сна на случайный, так чтоэто иногда работает и иногда время ожидания:
std::mt19937 prng { std::random_device{}() };
for (int i = 0; i < 4; i++) {
ba::write(s, ba::buffer(std::string("A")));
std::this_thread::sleep_for(std::uniform_int_distribution<>(200, 400)(prng) * 1ms);
}
Напечатано на моей системе:
1. received: OK
2. received: OK
3. received: OK
canceled (timeout)
4 exception read_some: End of file
5. received: OK
canceled (timeout)
6 exception read_some: End of file
7. received: OK
8. received: OK
Смотри, Ма, без рук
Еще проще, если исключить специальные сообщения, на самом деле ничего не меняется:
ba::spawn(strand_, [this, self = shared_from_this()](ba::yield_context yield) {
try {
ba::steady_timer timer(strand_, TIMEOUT_LIMIT);
timer.async_wait([this](error_code ec) {
if (ba::error::operation_aborted != ec)
socket_.close();
});
std::string packet;
ba::async_read(socket_,
ba::dynamic_buffer(packet),
ba::transfer_exactly(DATA_LEN_4), yield);
ba::write(socket_, ba::buffer("OK"s));
} catch(std::exception const& e) {
std::clog << "error " << std::quoted(e.what()) << std::endl;
}
});
Обратите внимание, что нам даже не нужен timer_
в качестве члена больше, и егодеструктор автоматически корректно отменяет таймеробласти видимости.
Выходные данные практически не меняются:
1. received: OK
2. received: OK
3. received: OK
error "Operation canceled"
4 exception read_some: End of file
5. received: OK
6. received: OK
7. received: OK
error "Operation canceled"
8 exception read_some: End of file
error "Operation canceled"
9 exception read_some: End of file