API сокета Java: Як визначити, чи з'єднання закрито?


92

Я стикаюся з деякими проблемами з API сокета Java. Я намагаюся відобразити кількість гравців, які зараз підключені до моєї гри. Неважко визначити, коли гравець підключився. Однак видається надмірно важким визначити, коли програвач відключився за допомогою API сокета.

isConnected()Здається, дзвінок до розетки, яка була відключена віддалено, завжди повертається true. Подібним чином, isClosed()здається, повернення до розетки, яка була закрита віддалено, завжди повертається false. Я читав, що для того, щоб насправді визначити, чи закрито сокет, дані повинні бути записані у вихідний потік і вилучити виняток. Це здається справді нечистим способом вирішення цієї ситуації. Нам би просто постійно доводилося розсилати по мережі повідомлення про сміття, щоб коли-небудь дізнатися, коли сокет закрився.

Чи є якесь інше рішення?

Відповіді:


183

Немає API TCP, який повідомляє вам про поточний стан з'єднання. isConnected()і isClosed()повідомить вам поточний стан вашого розетки . Не те саме.

  1. isConnected()повідомляє, чи підключали ви цю розетку . У вас є, тож це повертається правдою.

  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, або платформі.


18
Важко повірити, що протокол TCP, який орієнтований на підключення, навіть не може знати статус свого з’єднання ... Хлопці, які придумали ці протоколи, їздять на своїх автомобілях із закритими очима?
PedroD

31
@PedroD Навпаки: це було навмисно. Попередні набори протоколів, такі як SNA, мали "сигнал дзвінка". TCP був розроблений для переживання ядерної війни і, що є більш тривіальним, падінь і злетів маршрутизатора: отже, повна відсутність нічого, як сигнал набору номера, стан з'єднання тощо; і це також те, чому підтримка TCP в RFC описується як суперечлива функція, і чому вона завжди вимкнена за замовчуванням. TCP все ще з нами. СНР? IPX? ISO? Ні. Вони правильно зрозуміли.
Маркіз Лорнський,

3
Я не вважаю, що це хороший привід для приховування цієї інформації від нас. Знання того, що з'єднання було втрачено, не обов'язково означає, що протокол менш стійкий до несправностей, це завжди залежить від того, що ми робимо з цими знаннями ... Для мене метод isBound і isConnected з java - це чисто макетні методи, вони не мають ніякої користі , але висловлюють необхідність у прослуховувачі подій з'єднання ... Але я ще раз повторюю: знання того, що з'єднання було втрачено, не погіршує роботу протоколу. Тепер, якщо ці протоколи, про які ви кажете, перервали зв’язок, як тільки вони виявили, що його було втрачено, це вже інша історія.
PedroD

21
@Pedro Ви не розумієте. Це не "виправдання". Немає інформації, яку можна утримати. Немає тонального сигналу набору. TCP не знає, чи не вдалося з’єднання, поки ви не спробуєте щось з цим зробити. Це був основний критерій проектування.
Маркіз Лорнський,

2
@EJP дякую за вашу відповідь. Ви, мабуть, знаєте, як перевірити, чи не закрилася розетка без "блокування"? Використання read або readLine збирається заблокувати. Чи є спосіб зрозуміти, якщо інший сокет закрився доступним методом, здається, він повертається 0замість -1.
неповносправність

9

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

Крім того, відповідне питання


6
Це «загальна практика» в деяких протоколах. Я зауважую, що HTTP, найпоширеніший протокол програми на планеті, не має операції PING.
Маркіз Лорнський

2
@ user207421, оскільки HTTP / 1 - це один протокол. Ви надсилаєте запит, отримуєте відповідь. І ви закінчили. Розетка закрита. Розширення Websocket має функцію PING, як і HTTP / 2.
Michał Zabielski,

Я рекомендував серцебиття одним байтом, якщо це можливо :)
Стефан Рейх

@ MichałZabielski Це тому, що дизайнери HTTP цього не вказали. Ви не знаєте, чому ні. HTTP / 1.1 - це не один протокол. Сокет не закритий: з'єднання HTTP за замовчуванням постійне. FTP, SMTP, POP3, IMAP, TLS, ... не мають серцебиття.
Маркіз

2

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

Якщо ви хотіли ... Ви можете делегувати відповідальність за "реєстрацію" клієнту. Тобто у вас буде колекція підключених користувачів із позначкою часу на останньому повідомленні, отриманому від кожного ... якщо клієнт закінчиться, ви примусите перереєстрацію клієнта, але це приводить до цитати та ідеї нижче.

Я прочитав, що для того, щоб фактично визначити, чи не закрито сокет, дані повинні бути записані у вихідний потік, а виняток потрібно вловити. Це здається справді нечистим способом вирішення цієї ситуації.

Якби ваш код Java не закривав / не відключав сокет, то як би ви інформували про те, що віддалений хост закрив ваше з’єднання? Зрештою, ваш try / catch робить приблизно те саме, що робив би поллер, який прослуховує події в сокеті ACTUAL. Розглянемо наступне:

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

Я думаю, одна з особливостей абстрактних мов полягає в тому, що ви абстрагуєтесь від дрібниць. Подумайте про ключове слово в C # (спробувати / нарешті) для SqlConnection s або що завгодно ... це просто вартість ведення бізнесу ... Я думаю, що спробувати / зловити / нарешті є прийнятим і необхідним шаблоном для використання Socket.


Ваша локальна система не може "закрити вашу розетку", з повідомленням або без попередження. Ваше питання починається "якщо ваш код Java не закрив / не відключив сокет ...?" теж не має сенсу.
Маркіз Лорнський,

1
Що саме заважає системі (наприклад, Linux) змусити закрити сокет? Чи все-таки це можливо зробити з 'gdb' за допомогою команди 'call close'?
Андрій Лебеденко

@AndreyLebedenko Нічого "точно не заважає", але він цього не робить.
Маркіз Лорнський,

@EJB, хто тоді?
Андрій Лебеденко

@AndreyLebedenko Ніхто цього не робить. Тільки програма може закрити власні сокети, якщо вона не закривається, не роблячи цього, і в цьому випадку ОС очиститься.
Маркіз Лорнський,

1

Я думаю, що це характер з'єднань tcp, оскільки в цих стандартах потрібно приблизно 6 хвилин мовчання для передачі, перш ніж ми дійдемо висновку про те, що зв’язок зник! Тому я не думаю, що ви можете знайти точне рішення цієї проблеми. Можливо, кращий спосіб написати якийсь зручний код, щоб вгадати, коли сервер повинен вважати, що з'єднання користувача закрито.


2
Це може зайняти набагато більше, до двох годин.
Маркіз Лорнський,

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 !");

0

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


-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блоці воно було нульовим .
Маркіз

-4

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


send()Виклик поверне -1 , тільки якщо вихідні дані отримали буферні для досить довго для посилу таймерів закінчуються. Це майже напевно не відбудеться під час першого надсилання після відключення через буферизацію на обох кінцях та асинхронну природу send()під капотом.
Маркіз
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.