API сокетов Java: как узнать, закрыто ли соединение?

93

У меня возникли проблемы с API сокетов Java. Я пытаюсь отобразить количество игроков, подключенных к моей игре в данный момент. Легко определить, когда игрок подключился. Однако кажется излишне сложным определить, когда игрок отключился, используя API сокетов.

Вызов isConnected()сокета, который был отключен удаленно, всегда возвращается true. Точно так же вызов isClosed()сокета, который был удаленно закрыт, всегда кажется возвращаемым false. Я читал, что для фактического определения того, был ли сокет закрыт, данные должны быть записаны в выходной поток и должно быть обнаружено исключение. Это кажется действительно нечистым способом справиться с этой ситуацией. Нам просто пришлось бы постоянно рассылать по сети мусорное сообщение, чтобы узнать, когда сокет закрылся.

Есть ли другое решение?

Дэн Брауэр
источник

Ответы:

183

Нет TCP API, который сообщит вам текущее состояние соединения. isConnected()и isClosed()сообщит вам текущее состояние вашего сокета . Не то же самое.

  1. isConnected()сообщает вам, подключили ли вы эту розетку . У вас есть, поэтому он возвращает true.

  2. isClosed()сообщает вам, закрыли ли вы этот сокет. Пока вы этого не сделаете, он вернет false.

  3. Если партнер закрыл соединение надлежащим образом

    • read() возвращает -1
    • readLine() возвращается null
    • readXXX()бросает EOFExceptionза любые другие XXX.

    • Запись вызовет IOException: «соединение сброшено одноранговым узлом», в конечном итоге, с учетом задержек буферизации.

  4. Если соединение разорвано по какой-либо другой причине, запись в IOExceptionконечном итоге вызовет , как указано выше, и чтение может сделать то же самое.

  5. Если одноранговый узел все еще подключен, но не использует соединение, можно использовать тайм-аут чтения.

  6. Вопреки тому, что вы можете прочитать в другом месте, об этом ClosedChannelExceptionне говорится . [Также нет SocketException: socket closed.] Это только говорит вам, что вы закрыли канал, а затем продолжили его использовать. Другими словами, ошибка программирования с вашей стороны. Это не указывает на закрытое соединение.

  7. В результате некоторых экспериментов с Java 7 в Windows XP также выяснилось, что если:

    • вы выбираете OP_READ
    • select() возвращает значение больше нуля
    • связанный SelectionKeyуже недействителен ( key.isValid() == false)

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

Маркиз Лорн
источник
19
Трудно поверить, что протокол TCP, ориентированный на соединение, не может даже знать статус своего соединения ... Неужели ребята, которые придумывают эти протоколы, водят свои машины с закрытыми глазами?
PedroD
33
@PedroD Напротив: это было преднамеренно. В предыдущих пакетах протоколов, таких как SNA, был «гудок». TCP был разработан, чтобы пережить ядерную войну и, что более тривиально, взлеты и падения маршрутизатора: отсюда полное отсутствие чего-либо вроде гудка, состояния соединения и т. Д .; и именно поэтому поддержка активности TCP описана в RFC как спорная функция и почему по умолчанию она всегда отключена. ПТС все еще с нами. СНС? IPX? ISO? Не. Они все поняли правильно.
Маркиз Лорн,
4
Я не верю, что это хороший повод скрывать от нас эту информацию. Знание того, что соединение было потеряно, не обязательно означает, что протокол менее устойчив к сбоям, это всегда зависит от того, что мы делаем с этим знанием ... Для меня методы isBound и isConnected из java являются чистыми фиктивными методами, они бесполезны , но выражают потребность в прослушивателе событий соединения ... Но я повторяю: знание того, что соединение было потеряно, не ухудшает протокол. Теперь, если эти протоколы, о которых вы говорите, уничтожили соединение, как только они обнаружили, что оно было потеряно, это другая история.
PedroD
23
@Pedro Ты не понимаешь. Это не «оправдание». Нет информации, которую следует скрывать. Нет гудка. TCP не знает , не удалось ли установить соединение, пока вы не попытаетесь что-то с ним сделать. Это был основной критерий дизайна.
Маркиз Лорн,
2
@EJP спасибо за ваш ответ. Вы, наверное, знаете, как проверить, закрылся ли сокет без «блокировки»? Использование read или readLine приведет к блокировке. Есть ли способ понять, что если другой сокет был закрыт доступным методом, он, кажется, возвращается 0вместо -1.
insumity
9

Обычной практикой в ​​различных протоколах обмена сообщениями является поддержание пульса друг друга (продолжение отправки пакетов ping), пакет не должен быть очень большим. Механизм зондирования позволит вам обнаружить отключенного клиента даже до того, как TCP выяснит это в целом (таймаут TCP намного больше). Отправьте зонд и подождите, скажем, 5 секунд для ответа, если вы не видите ответа, скажем, 2-3 при последующих проверках ваш плеер отключается.

Также связанный вопрос

Калпак Гадре
источник
6
В некоторых протоколах это «общая практика» . Отмечу, что HTTP, наиболее часто используемый протокол приложений на планете, не имеет операции PING.
Marquis of Lorne
2
@ user207421, потому что HTTP / 1 - это одноразовый протокол. Отправляешь запрос, получаешь ответ. И все готово. Розетка закрыта. Расширение Websocket имеет операцию PING, также как и HTTP / 2.
Michał Zabielski
Я рекомендовал сердцебиение с помощью одного байта, если возможно :)
Стефан Райх
@ MichałZabielski Это потому, что разработчики HTTP не указали это. Вы не знаете, почему бы и нет. HTTP / 1.1 не является одноразовым протоколом. Сокет не закрыт: HTTP-соединения по умолчанию постоянны. FTP, SMTP, POP3, IMAP, TLS, ... без пульса.
Marquis of Lorne
2

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

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

Я прочитал это, чтобы фактически определить, закрыт ли сокет, данные должны быть записаны в выходной поток и должно быть обнаружено исключение. Это кажется действительно нечистым способом справиться с этой ситуацией.

Если ваш Java-код не закрыл / не отключил Socket, то как еще вы узнали бы, что удаленный хост закрыл ваше соединение? В конечном счете, ваш try / catch делает примерно то же самое, что и опросчик, ожидающий событий в сокете ACTUAL. Обратите внимание на следующее:

  • ваша локальная система может закрыть ваш сокет без уведомления вас ... это просто реализация Socket (то есть он не опрашивает оборудование / драйвер / прошивку / что-либо для изменения состояния).
  • новый Socket (Proxy p) ... есть несколько сторон (на самом деле 6 конечных точек), которые могут закрыть вам соединение ...

Я думаю, что одна из особенностей абстрактных языков заключается в том, что вы абстрагированы от мелочей. Подумайте о ключевом слове using в C # (try / finally) для SqlConnection s или чего-то еще ... это просто затраты на ведение бизнеса ... Я думаю, что try / catch / finally является принятым и необходимым шаблоном для использования Socket.

Скоттли
источник
Ваша локальная система не может «закрыть ваш сокет» с уведомлением вас или без него. Ваш вопрос начинается с того, что ваш Java-код не закрыл / не отключил сокет ...? тоже не имеет смысла.
Marquis of Lorne,
1
Что именно мешает системе (например, Linux) принудительно закрыть сокет? Можно ли сделать это из gdb с помощью команды call close?
Андрей Лебеденко
@AndreyLebedenko Ничего «точно не мешает», но не делает.
Маркиз Лорн
@EJB, а кто тогда?
Андрей Лебеденко
@AndreyLebedenko Никто этого не делает. Только приложение может закрыть свои собственные сокеты, если оно не завершится без этого, и в этом случае ОС очистится.
Marquis of Lorne
1

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

Эхсан Ходарахми
источник
2
Это может занять намного больше, до двух часов.
Marquis of Lorne,
0

Поскольку @ user207421 говорит, что нет способа узнать текущее состояние соединения из-за модели архитектуры протокола TCP / IP. Таким образом, сервер должен вас заметить, прежде чем закрывать соединение, или вы проверяете это самостоятельно.
Это простой пример, который показывает, как узнать, что сокет закрыт сервером:

sockAdr = new InetSocketAddress(SERVER_HOSTNAME, SERVER_PORT);
socket = new Socket();
timeout = 5000;
socket.connect(sockAdr, timeout);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream());
while ((data = reader.readLine())!=null) 
      log.e(TAG, "received -> " + data);
log.e(TAG, "Socket closed !");
ucMedia
источник
0

Я столкнулся с подобной проблемой. В моем случае клиент должен периодически отправлять данные. Я надеюсь, что у вас такое же требование. Затем я устанавливаю SO_TIMEOUT, socket.setSoTimeout(1000 * 60 * 5);который генерируется по истечении java.net.SocketTimeoutExceptionуказанного времени. Тогда я легко могу обнаружить мертвого клиента.

Hurelhuyag
источник
-2

Вот как я с этим справляюсь

 while(true) {
        if((receiveMessage = receiveRead.readLine()) != null ) {  

        System.out.println("first message same :"+receiveMessage);
        System.out.println(receiveMessage);      

        }
        else if(receiveRead.readLine()==null)
        {

        System.out.println("Client has disconected: "+sock.isClosed()); 
        System.exit(1);
         }    } 

если result.code == null

Петар Сехо
источник
Вам не нужно звонить readLine()дважды. Вы уже знаете, что в elseблоке было null .
Маркиз Лорн
-4

В Linux при записи () в сокет, который другой, неизвестный вам, закрыт, вызовет сигнал / исключение SIGPIPE, как бы вы ни хотели его вызвать. Однако, если вы не хотите быть пойманным SIGPIPE, вы можете использовать send () с флагом MSG_NOSIGNAL. Вызов send () вернется с -1, и в этом случае вы можете проверить errno, который сообщит вам, что вы пытались записать сломанный канал (в данном случае сокет) со значением EPIPE, которое согласно errno.h эквивалентно 32. В качестве реакции на EPIPE вы можете вернуться назад и попытаться снова открыть сокет и попытаться отправить свою информацию еще раз.

MikeK
источник
send()Вызов возвратит -1 , только если исходящие данные получили буферные для достаточно долго для посыла таймеров истекают. Это почти наверняка не произойдет при первой отправке после отключения из-за буферизации на обоих концах и асинхронного характера send()под капотом.
Marquis of Lorne