Проблема с потоками Java NIO с SocketChannel.write()

Иногда при отправке большого количества данных через SocketChannel.write() базовый буфер TCP заполняется, и мне приходится постоянно повторять попытку записи(), пока все данные не будут отправлены.

Итак, у меня может быть что-то вроде этого:

public void send(ByteBuffer bb, SocketChannel sc){
   sc.write(bb);
   while (bb.remaining()>0){
      Thread.sleep(10);
      sc.write(bb);          
   }
}

Проблема в том, что случайная проблема с большим ByteBuffer и переполнением базового буфера TCP означает, что этот вызов send() будет заблокирован на неожиданное время. В моем проекте одновременно подключены сотни клиентов, и одна задержка, вызванная подключением к одному сокету, может вывести всю систему из строя, пока не будет устранена эта одна задержка с одним SocketChannel. Когда возникает задержка, это может вызвать цепную реакцию замедления в других областях проекта, и важно иметь низкую задержку.

Мне нужно решение, которое позаботится об этой проблеме переполнения буфера TCP прозрачно и не приведет к блокировке всего, когда необходимы несколько вызовов SocketChannel.write(). Я подумал о том, чтобы поместить send() в отдельный класс, расширяющий Thread, чтобы он работал как отдельный поток и не блокировал вызывающий код. Тем не менее, я обеспокоен накладными расходами, необходимыми для создания потока для КАЖДОГО подключения к сокету, которое я поддерживаю, особенно когда в 99% случаев SocketChannel.write() завершается успешно с первой попытки, а это означает, что нет необходимости в потоке. . (Другими словами, размещение send() в отдельном потоке действительно необходимо только в том случае, если используется цикл while() - только в случаях, когда есть проблема с буфером, возможно, в 1% случаев) Если есть проблема с буфером только 1% времени, мне не нужны накладные расходы на поток для остальных 99% вызовов send().

Я надеюсь, что это имеет смысл... Я мог бы действительно использовать некоторые предложения. Спасибо!


person DivideByHero    schedule 18.07.2009    source источник


Ответы (6)


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

См. полную книгу Java NIO и эту Статья о Java для начала работы. Однако обратите внимание, что это сложная тема, и она по-прежнему вызывает некоторые проблемы с многопоточностью в вашем коде. Google «неблокирующий NIO» для получения дополнительной информации.

person AngerClown    schedule 18.07.2009

Чем больше я читаю о Java NIO, тем больше меня это раздражает. Во всяком случае, я думаю, что эта статья решает вашу проблему...

http://weblogs.java.net/blog/2006/05/30/tricks-and-tips-nio-part-i-why-you-must-handle-opwrite

Похоже, у этого парня есть более элегантное решение, чем петля сна.

Также я быстро прихожу к выводу, что использование Java NIO само по себе слишком опасно. Там, где я могу, я думаю, я, вероятно, буду использовать Apache MINA, который обеспечивает хорошую абстракцию над Java NIO и его небольшими «сюрпризами».

person geme_hendrix    schedule 24.02.2010
comment
Ссылка сейчас не работает. - person user207421; 04.09.2019

Вам не нужен sleep(), так как запись либо вернется немедленно, либо заблокируется. У вас может быть исполнитель, которому вы передаете запись, если она не записывается в первый раз. Другой вариант — иметь небольшой пул потоков для выполнения операций записи.

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

person Peter Lawrey    schedule 18.07.2009
comment
Сон имеет какой-то смысл, если сокет не блокируется с полным буфером, оригинальный постер не говорит. - person Stefan L; 23.08.2011
comment
Предполагается, что для очистки буфера всегда требуется не менее 10 мс. Лично, если буфер не очищается почти сразу, либо буфер слишком мал, либо считыватель слишком медленный. Я бы закрыл связь. - person Peter Lawrey; 23.08.2011
comment
Если байтовый буфер в примере больше, чем выходной буфер сокета ядра, код неэффективен, поскольку он, вероятно, может писать быстрее, чем может писать сетевой адаптер, и, таким образом, переполняется и переходит в спящий режим. В спящем режиме 10 мс сетевой адаптер может записать около 1 МБ в сеть 1 Гбит, что намного больше, чем размер типичного выходного буфера сокета. Если байтовый буфер составляет всего несколько КБ, вероятно, считыватель слишком медленный, тогда имеет смысл сделать небольшую паузу, используя OP_WRITE, это более сложно, но позволит избежать перегрузки потока в спящий режим, а также опустошения буфера. - person Stefan L; 02.02.2012
comment
@StefanL Нет, сон не имеет никакого смысла. Для кода бессмысленно делать предположения о том, сколько времени потребуется для очистки буфера, когда select() скажет вам точно. Например, если приемник не читает, для очистки буфера потребуется бесконечность. - person user207421; 01.10.2016

Для сотен подключений вам, вероятно, не нужно возиться с NIO. Вам подойдут старые добрые блокирующие сокеты и потоки.

С помощью NIO вы можете зарегистрировать интерес к OP_WRITE для ключа выбора, и вы получите уведомление, когда появится место для записи дополнительных данных.

person Tom Hawtin - tackline    schedule 18.07.2009

Есть несколько вещей, которые вам нужно сделать, если у вас уже есть цикл, использующий Selector.select(); чтобы определить, какие сокеты готовы для ввода/вывода.

  • Установите неблокирующий канал сокета после того, как вы его создали, sc.configureBlocking(false);
  • Запишите (возможно, части) буфер и проверьте, не осталось ли чего. Буфер сам заботится о текущей позиции и о том, сколько осталось.

Что-то вроде

sc.write(bb);
if(sc.remaining() == 0)
   //we're done with this buffer, remove it from the select set if there's nothing else to send.
else
    //do other stuff/return to select loop
  • Избавьтесь от цикла while, который спит
person nos    schedule 18.07.2009

Я сталкиваюсь с некоторыми из тех же проблем прямо сейчас:
- Если у вас небольшое количество подключений, но с большими передачами, я бы просто создал пул потоков и позволил блокировать запись для потоков записи.
- Если у вас много подключений, вы можете использовать полный Java NIO и зарегистрировать OP_WRITE в своих сокетах accept(), а затем дождаться появления селектора.

Все это есть в книге Orielly Java NIO.
Также: http://www.exampledepot.com/egs/java.nio/NbServer.html?l=rel

Некоторые исследования в Интернете привели меня к мысли, что NIO довольно избыточен, если у вас нет большого количества входящих соединений. В противном случае, если это всего лишь несколько больших передач - просто используйте поток записи. Скорее всего ответ будет быстрее. У многих людей возникают проблемы с тем, что NIO не отвечает так быстро, как им хотелось бы. Поскольку ваш поток записи сам по себе блокируется, это не повредит вам.

person EdH    schedule 18.07.2009