Java NIO: как узнать, когда SocketChannel read() завершен с неблокирующим вводом-выводом

В настоящее время я использую неблокирующий SocketChannel (Java 1.6) в качестве клиента для сервера Redis. Redis принимает текстовые команды непосредственно через сокет, завершается CRLF и отвечает аналогичным образом, быстрый пример:

ОТПРАВИТЬ: 'PING\r\n'

RECV: '+PONG\r\n'

Redis также может возвращать огромные ответы (в зависимости от того, что вы запрашиваете) со многими разделами \r\n-терминированных данных как часть одного ответа.

Я использую стандартный цикл while(socket.read() › 0) {//append bytes} для чтения байтов из сокета и повторной сборки их на стороне клиента в ответ.

ПРИМЕЧАНИЕ. Я не использую селектор, а использую несколько каналов SocketChannel на стороне клиента, подключенных к серверу и ожидающих обработки команд отправки/получения.

Что меня смущает, так это контракт метода SocketChannel.read() в неблокирующем режиме, в частности, как узнать, когда сервер закончил отправку, и я есть все сообщение.

У меня есть несколько способов защиты от слишком быстрого возврата и предоставления серверу возможности ответить, но единственное, на чем я застрял, это:

  1. Возможно ли когда-нибудь, чтобы read() возвращал байты, а затем при последующем вызове не возвращал байтов, но при другом последующем вызове снова возвращал несколько байтов?

В принципе, могу ли я доверять тому, что сервер ответил мне, если я получил хотя бы 1 байт и в конечном итоге read() возвращает 0, тогда я знаю, что закончил, или, возможно, сервер был просто занят и может выдать еще несколько байтов, если я подожду и продолжу попытки?

Если он может продолжать отправлять байты даже после того, как read() вернул 0 байтов (после предыдущих успешных операций чтения), то я понятия не имею, как определить, когда сервер закончил говорить со мной, и на самом деле я сбит с толку, как связь в стиле java.io. * даже узнает, когда сервер готов.

Как вы, ребята, знаете, чтение никогда не возвращает -1, если только соединение не разорвано, а это стандартные долгоживущие соединения с БД, поэтому я не буду закрывать и открывать их при каждом запросе.

Я знаю, что популярный ответ (по крайней мере, на эти вопросы NIO) заключался в том, чтобы взглянуть на Grizzly, MINA или Netty - если возможно, я действительно хотел бы узнать, как все это работает в исходном состоянии, прежде чем принимать некоторые сторонние зависимости.

Спасибо.

Дополнительный вопрос:

Первоначально я думал, что блокирующий SocketChannel будет подходящим способом, поскольку я действительно не хочу, чтобы вызывающий абонент что-либо делал, пока я не обработаю его команду и не верну ему ответ.

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

Я не совсем понимаю, как правильно использовать метод блокировки, поскольку он всегда зависает при чтении.


person Riyad Kalla    schedule 07.02.2011    source источник
comment
Это не связано с NIO, но протокол должен либо использовать сообщения фиксированной длины, сначала отправлять длину сообщения, либо заканчивать каждое сообщение уникальным разделителем.   -  person biziclop    schedule 08.02.2011
comment
Итак, в целом вы говорите, что если у меня нет разделителей или длин для работы, я SOL? Протокол Redis на самом деле определяет эти вещи, но мне пришлось бы преобразовывать байты в символы, по частям, на лету, чтобы проверять эти значения по мере чтения, что делало мой сетевой код чертовски сложным. Я надеялся, что смогу этого избежать.   -  person Riyad Kalla    schedule 08.02.2011
comment
Боюсь, это единственный верный способ, но не все так плохо. Насколько я могу судить, протокол Redis заканчивает каждый элемент команды/ответа CR/LF, поэтому вы можете читать и накапливать байты при проверке CR/LF и преобразовывать буфер в символы только в конце каждой строки.   -  person biziclop    schedule 08.02.2011
comment
biziclop - отличное предложение, я на самом деле не думал группировать по границам \r\n, но это позволит избежать одного случая преобразования байт-символ, о котором я беспокоился (попытка преобразовать символ юникода до того, как все байты будут доступны). Спасибо!   -  person Riyad Kalla    schedule 08.02.2011


Ответы (4)


Если он может продолжать отправлять байты даже после того, как read() вернул 0 байтов (после предыдущих успешных чтений), то я понятия не имею, как определить, когда сервер закончил говорить со мной, и на самом деле я запутался, как java.io. * сообщения в стиле будут даже знать, когда сервер будет готов.

Прочтите и следуйте протоколу:

http://redis.io/topics/protocol

Спецификация описывает возможные типы ответов и способы их распознавания. Некоторые завершаются строкой, а многострочные ответы включают счетчик префиксов.

Ответы

Redis будет отвечать на команды разными типами ответов. Можно проверить тип ответа по первому байту, отправленному сервером:

  • При однострочном ответе первый байт ответа будет +
  • При сообщении об ошибке первым байтом ответа будет -
  • С целым числом первый байт ответа будет:
  • При массовом ответе первым байтом ответа будет $
  • При многомассовом ответе первый байт ответа будет *

Ответ в одну строку

Однострочный ответ представляет собой однострочную строку , начинающуюся с + и заканчивающуюся \r\n. ...

...

Массовые ответы

Такие команды, как LRANGE, должны возвращать несколько значений (каждый элемент списка является значением, а LRANGE должен возвращать более одного элемента). Это достигается с помощью нескольких массовых операций записи, с начальной строкой, указывающей, сколько последовательных операций массовой записи будет выполнено.


Возможно ли когда-либо, чтобы read() возвращал байты, а затем при последующем вызове не возвращал байтов, но при другом последующем вызове снова возвращал несколько байтов? В принципе, могу ли я доверять тому, что сервер отвечает мне, если я получил хотя бы 1 байт и в конечном итоге read() вернет 0, тогда я знаю, что я закончил, или возможно, что сервер был просто занят и может выдать что-то больше байтов, если я подожду и продолжу попытки?

Да, это возможно. Это происходит не только из-за занятости сервера, но и из-за перегрузки сети и неработающих маршрутов, которые могут привести к приостановке передачи данных. Данные представляют собой поток, который может приостанавливаться в любом месте потока независимо от протокола приложения.

Продолжайте читать поток в буфер. Взгляните на первый символ, чтобы определить, какой тип ответа ожидать. Проверяйте буфер после каждого успешного чтения, пока буфер не будет содержать полное сообщение в соответствии со спецификацией.


Первоначально я думал, что блокирующий SocketChannel будет подходящим способом, поскольку я действительно не хочу, чтобы вызывающий абонент что-либо делал, пока я не обработаю его команду и не верну ему ответ.

Я думаю ты прав. Основываясь на моем беглом взгляде на спецификацию, блокировка чтения не будет работать для этого протокола. Поскольку он выглядит построчно, BufferedReader может помочь, но вам все равно нужно знать, как распознать, когда ответ завершен.

person Bert F    schedule 07.02.2011
comment
Берт, совершенно верно; по какой-то причине у меня засело в голове, что я буду читать, пока не закончу, а затем преобразовать весь беспорядок в char затем попытаться определить тип ответа на основе протокола (у меня это напечатано здесь перед меня все разметили). Только после ответа Эрика выше я понял, что мой код должен быть осведомлен о байтах, которые он читает, и ограничивающих значениях, что приводит меня именно к тому, что вы сказали: заглянуть в байты и следовать спецификации. Спасибо. - person Riyad Kalla; 08.02.2011

Для этого ответа обратитесь к спецификациям Redis.

Это не противоречит правилам, когда вызов .read() возвращает 0 байтов при одном вызове и 1 или более байтов при последующем вызове. Это совершенно законно. Если что-то могло вызвать задержку доставки из-за задержки в сети или медленности сервера Redis, это могло произойти.

Ответ, который вы ищете, — это тот же ответ на вопрос: «Если я вручную подключился к серверу Redis и отправил команду, как я могу узнать, когда это было сделано, отправив мне ответ, чтобы я мог отправить другую команду?»

Ответ нужно искать в спецификации Redis. Если нет глобального маркера, который сервер отправляет после выполнения вашей команды, это может быть реализовано на основе команды за командой. Если спецификации Redis не позволяют этого, то это ошибка в спецификациях Redis. Они должны сказать вам, как узнать, когда они отправили все свои данные. Вот почему оболочки имеют командные строки. Redis должен иметь эквивалент.

В случае, если Redis не имеет этого в своих спецификациях, я бы предложил добавить какую-то функциональность таймера. Закодируйте свой поток, обрабатывающий сокет, чтобы сигнализировать о завершении команды после того, как данные не были получены в течение заданного периода времени, например пяти секунд. Выберите период времени, который значительно больше, чем самая длинная команда, которая требуется для выполнения на сервере.

person Erick Robertson    schedule 07.02.2011
comment
Поскольку протокол Redis на самом деле указывает разделители или длину сообщений, вы действительно должны их использовать. Это лучший способ создать надежное решение. Если вы ищете быстрый хак, вам может сойти с рук функция таймера, о которой я упоминал. Однако я бы не рекомендовал это делать, поскольку ответ содержится в протоколе Regis. - person Erick Robertson; 08.02.2011
comment
Эрик, ты спас то немногое, что у меня осталось. Похоже, я неправильно понимал обязанности в этом коде; приписывание поведения несуществующему SocketChannel. Я попытаюсь найти разделяющую информацию в ответе и заставить соединение реагировать соответствующим образом. Спасибо вам за помощь. - person Riyad Kalla; 08.02.2011

Я использую стандартный цикл while(socket.read() > 0) {//append bytes}

Это не стандартная техника в NIO. Вы должны сохранить результат чтения в переменной и проверить его на:

  1. -1, что указывает на EOS, что означает, что вы должны закрыть канал
  2. ноль, что означает отсутствие данных для чтения, что означает, что вы должны вернуться к циклу select(), и
  3. положительное значение, означающее, что вы прочитали такое количество байтов, которое вы должны затем извлечь и удалить из ByteBuffer (get()/compact()), прежде чем продолжить.
person user207421    schedule 15.04.2011

Прошло много времени, но. . .

В настоящее время я использую неблокирующий SocketChannel

Чтобы было ясно, SocketChannels блокируются по умолчанию; чтобы сделать их неблокирующими, нужно явно вызвать SocketChannel#configureBlocking(false)

Я предполагаю, что вы сделали это

Я не использую селектор

Вау; Это проблема; если вы собираетесь использовать неблокирующие каналы, то вам всегда следует использовать селектор (по крайней мере, для чтения); в противном случае вы столкнетесь с путаницей, которую вы описали, а именно. read(ByteBuffer) == 0 ничего не значит (ну, это означает, что в буфере приема tcp на данный момент нет байтов).

Это аналогично проверке вашего почтового ящика, и он пуст; значит ли это, что письмо никогда не придет? ни разу не отправили?

Что меня смущает, так это контракт метода SocketChannel.read() в неблокирующем режиме, в частности, как узнать, когда сервер завершил отправку и у меня есть все сообщение.

Существует контракт -> если селектор выбрал канал для операции чтения, то следующий вызов SocketChannel#read(ByteBuffer) гарантированно вернет> 0 (при условии, что в аргументе ByteBuffer есть место)

Вот почему вы используете селектор, и потому что он может в одном вызове select «выбрать» 1 КБ SocketChannels, у которых есть байты, готовые для чтения

Теперь нет ничего плохого в использовании SocketChannels в режиме блокировки по умолчанию; и, учитывая ваше описание (клиент или два), вероятно, нет причин, поскольку это проще; но если вы хотите использовать неблокирующие каналы, используйте селектор

person igaz    schedule 07.09.2015
comment
Зашел только для того, чтобы проголосовать за это - прошло много времени, но это был отличный ответ. Еще до того, как я по-настоящему понял API-интерфейсы NIO, я все еще придерживался мышления «потокового ввода-вывода» и специально не видел необходимости использовать селектор с комментарием о неблокирующем чтении, изложенным ранее — это очень быстро все кристаллизовало — надеюсь, поможет другие. - person Riyad Kalla; 09.09.2015