LINUX.ORG.RU

Странное поведение asio::async_read и asio::async_read_until

 


0

3

Столкнулся с такой особенностью.

Есть сокет и стиримбуфер:

asio::ip::tcp::socket m_socket;
asio::streambuf m_readbuf;

Сначала я читаю заголовок функцией async_read_until():

asio::async_read_until(m_socket, m_readbuf, delim,
	[this](const asio::error_code& e, std::size_t bytes)
	{
	//...
	});
Все корректно читается и мы попадаем в хэндлер, количество прочитанных байт соответствует длине заголовка. Далее, запускаем на чтение async_read() в надежде считать заданное количество байт nBytes:
asio::async_read(m_socket, m_readbuf, asio::transfer_exactly(nBytes),
	[this](const asio::error_code& e, std::size_t bytes)
	{
	//...
	});
Однако когда попадаем в хэндлер, узнаем, что e == asio::error::eof и bytes == 0. Было выяснено, что часть пакета, следующая после заголовка, уже находится в буфере. Судя по всему она была прочитана ранее.

Если последовательно запустить async_read_until(), то получим прочитанные данные корректно, то есть хэндлеры будут вызваны и значения bytes будут правильными. Пробовал запускать async_read() первой, в этом случае поведение ожидаемое: вызов хэндлера с запрошенным количеством прочитанных байт.

Для реализации ожидаемого поведения, то есть возвращения запрошенного количества байт в хэндлере, сделал я свою функцию read() следующим образом:

void Connection::read(unsigned int nBytes, std::function<OnReadCompleted> handler)
{
asio::async_read(m_socket, m_readbuf, asio::transfer_exactly(nBytes),
	[nBytes, handler, this](const asio::error_code& e, std::size_t bytes)
	{
		std::vector<char> data;
		if (e == asio::error::eof)
		{
			std::size_t size = (nBytes <= m_readbuf.size() ) ? nBytes : m_readbuf.size();
			if (size > 0)
			{
				std::istream in(&m_readbuf);
				data.resize(size, 0);
				if (in.read( (char*)&data[0], size) )
				{
					asio::error_code ec;
					handler(ec, data);
				}
				else
				{
					handler(asio::error::fault, data);
				}
			}
			else
			{
				handler(asio::error::eof, data);
			}
		}
		else
		{
			if (bytes > 0)
			{
				std::istream in(&m_readbuf);
				data.resize(bytes, 0);
				if ( !in.read( (char*)&data[0], bytes) )
				{
					data.clear();
				}
			}

			handler(e, data);
		}
	});
}
Интересно, баг это или фича? Использую asio 1.11.0 под Windows.


это фича.

This function is used to asynchronously read data into the specified streambuf until the streambuf's get area contains the specified delimiter

...

If the streambuf's get area already contains the delimiter, this asynchronous operation completes immediately.

...

void handler(
  // Result of operation.
  const boost::system::error_code& error,

  // The number of bytes in the streambuf's get
  // area up to and including the delimiter.
  // 0 if an error occurred.
  std::size_t bytes_transferred

Т.е. она гарантирует что по приходу в хендлер в streambuf'е будет лежать как минимум один разделитель, но не гарантрует, что после разделителя ничего нет. И в хендлер передается не размер прочитанного, а количество байт до первого разделителя.

Vinick ★★
()

п..ц код. убиват. просто убиват.

anonymous
()

http://www.boost.org/doc/libs/1_62_0/doc/html/boost_asio/reference/async_read... вот взгляни. Там написано как раз то поведение, которое ты описываешь. Это не баг. Кстати, в их примере http клиента после async_read_until читают так:

boost::asio::async_read(socket_, response_,
          boost::asio::transfer_at_least(1),
          boost::bind(&client::handle_read_content, this,
            boost::asio::placeholders::error));
Попробуй и ты так. Я имею ввиду boost::asio::transfer_at_least(1) вместо того, что у тебя. Если будешь делать, отпишись о результате.

rumgot ★★★★★
()
Последнее исправление: rumgot (всего исправлений: 1)
Ответ на: комментарий от rumgot

В том примере, о котором ты говоришь, чтение происходит побайтно и результат выдается на std::cout. Это не совсем то что мне нужно. Я хотел чтобы читалось заданное количество байт и после прочтения вызывался хэндлер с указанием прочитанного количества. Но вчера видимо был мой косяк когда я отлаживал сервер и мне внезапно приходил asio::error::eof. Сегодня я переделал функцию чтения и вроде все работает на мелких и крупных файлах. (Я пересылаю файлы)

typedef void OnReadCompleted(const asio::error_code& e, const std::vector<char>& data);

void Connection::read(unsigned int nBytes, std::function<OnReadCompleted> handler)
{
asio::async_read(m_socket, m_readbuf, asio::transfer_exactly(nBytes),
	[this, handler](const asio::error_code& e, std::size_t bytes)
	{
		std::vector<char> data;
		if (!e or e == asio::error::eof)
		{
			if (bytes > 0)
			{
				std::istream in(&m_readbuf);
				data.resize(bytes, 0);
				if ( !in.read( (char*)&data[0], bytes) )
				{
					data.clear();
				}
			}
		}
		handler(e, data);
	});
}

silart
() автор топика
Ответ на: комментарий от silart

Еще небольшое уточнение тебе.
При использовании transfer_exactly(nBytes), будет попытка прочитать именно количество nBytes. Если в конце будет меньше байт, то функция чтения повиснет. И тогда либо придет нужное недостающее количество байт, либо сервер закроет соединение и ты получишь EOF, либо (если сервер закрывать соединение не будет) функция чтения так и будет висеть(тогда нужно применять deadline_timer)(в HTTP 1.1 по умолчанию сервер не закрывает соединение например).
Поэтому я бы советовал тебе читать как показано в примерах. Далее сохранять прочитанное в какой-нибудь переменной, а далее уже брать нужными тебе порциями из этой переменной.

rumgot ★★★★★
()
Ответ на: комментарий от rumgot

Я был не прав. Функцию чтения я переделал так:

void Connection::read(unsigned int nBytes, std::function<OnReadCompleted> handler)
{
int32_t nNeedToRead = nBytes - m_readbuf.size();
asio::async_read(m_socket, m_readbuf, asio::transfer_at_least(abs(nNeedToRead) ),
	[this, handler, nBytes](const asio::error_code& e, std::size_t)
	{
		std::vector<char> data;
		if (!e or e == asio::error::eof)
		{
			std::istream in(&m_readbuf);
			data.resize(nBytes, 0);
			if ( !in.read( (char*)&data[0], nBytes) )
			{
				data.clear();
				handler(asio::error::fault, data);
				return;
			}
			asio::error_code ok;
			handler(ok, data);
		}
		else
		{
			handler(e, data);
		}
	});
}

Сначала мы проверяем что есть в буфере, то есть уже прочитано. Далее мы читаем именно недостающее количество байт (либо 0 байт, если в буфере уже находится нужное их количество). Попадая в хендлер, мы берем от туда только требуемое количество байт, все что лишнее мы оставляем до следующего раза.

silart
() автор топика
Ответ на: комментарий от silart

Я пока не проверял, но опять вопрос: если сервер не закроет соединение после передачи тебе данных и вместе с тем размер передаваемых тебе данных будет меньше чем размер запрашиваемых в transfer_at_least байт, то не будет ли функция чтения висеть и ждать недостающих байт? По этой причине transfer_at_least(1) дает универсальное поведение, при необходимости чтение можно повторить несколько раз подряд.

rumgot ★★★★★
()
Последнее исправление: rumgot (всего исправлений: 3)
Ответ на: комментарий от silart

Проверил. Подтверждаю: при постоянном соединении (в HTTP 1.1 по умолчанию) если использовать transfer_at_least с параметром > 1 (я словил такую ситуацию при значении параметра 1000), может возникнуть ситуация, когда функция чтения будет висеть и ждать недостающее количество байт. Если интересно, могу отписать код для проверки. Поэтому считаю, что transfer_at_least(1) наиболее подходящее решение(при этом ЧТЕНИЕ ПРОИСХОДИТ НЕ ПОБАЙТНО).

rumgot ★★★★★
()
Последнее исправление: rumgot (всего исправлений: 1)
Ответ на: комментарий от rumgot

Ты должен быть уверен, что сервер тебе отправит запрашиваемое количество байт. Если запросишь больше, будет висеть. А разве в случае transfer_at_least(1) зависания не будет? Ты же все равно ждешь что поступит хотя бы байт. А если он не поступит? Скорее всего ты так и не попадешь в хендлер. Для борьбы с зависанием, как мне кажется, следует применять таймеры. Моя функция сделана без них. Идея в том, что ты заводишь таймер, перед операцией чтения. А в хендлере ты его сбрасываешь. Если за период таймаута так и не удалось прочитать нужное количество байт, вызывается хендлер таймера, который отменяет операцию чтения (функция cancel() у сокета). После этого ты попадаешь в хендлер функции чтения и получает код ошибки asio::error::operation_aborted.

silart
() автор топика
Ответ на: комментарий от silart

Ты прав. Но рассуждаем дальше: в случае transfer_at_least(1) зависание при чтении возможно только! когда сервер не прислал данных вообще. С другой стороны при использовании больших значений аргумента transfer_at_least(minBytesRead), вероятность зависания будет в случаях когда количество поступивших байт находится в промежутке от нуля(сервер ничего не прислал) до minBytesRead - 1. Т.е. как видишь вероятность зависания и срабатывания таймера явно выше во втором случае. Также, в случае http, при использовании transfer_at_least(1), в конце чтения, когда сервер ничего не пришлет, можно обойтись без таймаута анализируюя полученные данные и не запуская следующее чтение, если все данные уже приняты. Лучше стараться избегать срабатывания таймаутов, ведь это лишнее ожидание.

rumgot ★★★★★
()
Последнее исправление: rumgot (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.