Чому неможливо без спроби вводу-виводу виявити, що сокет TCP був витончено закритий одноранговою мережею?


92

Як наступне нещодавнє запитання , мені цікаво, чому неможливо в Java, не намагаючись читати / писати на сокеті TCP, виявити, що сокет був витончено закритий одноранговою ланкою? Здається, це має місце незалежно від того, чи використовується хтось до NIO Socketабо NIO SocketChannel.

Коли одноранговий зв'язок витончено закриває TCP-з'єднання, стеки TCP з обох сторін з'єднання знають про факт. Серверна сторона (та, яка ініціює вимкнення) опиняється в стані FIN_WAIT2, тоді як клієнтська сторона (та, яка явно не реагує на вимкнення) опиняється в стані CLOSE_WAIT. Чому в методі немає Socketабо SocketChannelякий може запросити стек TCP, щоб побачити, чи не було розірвано базове з'єднання TCP? Це те, що стек TCP не надає такої інформації про стан? Або це дизайнерське рішення, щоб уникнути дорогого дзвінка в ядро?

За допомогою користувачів, які вже розмістили деякі відповіді на це питання, я думаю, я бачу, звідки проблема може виникнути. Сторона, яка явно не закриває з'єднання, опиняється в стані TCP, що CLOSE_WAITозначає, що з'єднання перебуває в стані відключення і чекає, поки сторона видасть власну CLOSEоперацію. Я гадаю, це досить справедливо, що isConnectedповертається trueі isClosedповертається false, але чому немає чогось подібного isClosing?

Нижче наведені тестові класи, які використовують розетки pre-NIO. Але однакові результати отримують за допомогою NIO.

import java.net.ServerSocket;
import java.net.Socket;

public class MyServer {
  public static void main(String[] args) throws Exception {
    final ServerSocket ss = new ServerSocket(12345);
    final Socket cs = ss.accept();
    System.out.println("Accepted connection");
    Thread.sleep(5000);
    cs.close();
    System.out.println("Closed connection");
    ss.close();
    Thread.sleep(100000);
  }
}


import java.net.Socket;

public class MyClient {
  public static void main(String[] args) throws Exception {
    final Socket s = new Socket("localhost", 12345);
    for (int i = 0; i < 10; i++) {
      System.out.println("connected: " + s.isConnected() + 
        ", closed: " + s.isClosed());
      Thread.sleep(1000);
    }
    Thread.sleep(100000);
  }
}

Коли тестовий клієнт підключається до тестового сервера, вихід залишається незмінним навіть після того, як сервер ініціює вимкнення з'єднання:

connected: true, closed: false
connected: true, closed: false
...

Я хотів би згадати: протокол SCTP не має цієї "проблеми". SCTP не має напівзакритого стану як TCP, іншими словами одна сторона не може продовжувати надсилати дані, тоді як інший кінець закрив свій сокет надсилання. Це має полегшити ситуацію.
Тарней Калман,

2
У нас є дві поштові скриньки (розетки) ........................................... ...... Поштові скриньки надсилають пошту один одному за допомогою RoyalMail (IP) забудьте про TCP ............................. .................... Все гаразд, поштові скриньки можуть надсилати / отримувати пошту одне одному (останнім часом багато затримок) надсилаючи, не отримуючи жодних проблем. ............. Якби одна поштова скринька потрапила під вантажівку і зазнала невдачі .... як би знала інша поштова скринька? Про це довелося б повідомити Royal Mail, який, у свою чергу, не знав би, доки він не спробував відправити / отримати пошту з тієї невдалої поштової скриньки .. ...... erm .....
divinci

Якщо ви не збираєтеся читати з сокета чи писати в сокет, чому вам все одно? І якщо ви збираєтеся читати з сокета або писати в сокет, навіщо робити додаткову перевірку? Який варіант використання?
Девід Шварц,

2
Socket.closeне є витонченим близьким.
user253751

@immibis Це, безсумнівно, витончене закриття, якщо в буфері прийому сокетів немає непрочитаних даних або ви не заплутали SO_LINGER.
Маркіз Лорнський,

Відповіді:


29

Я часто використовую Sockets, здебільшого з селекторами, і, наскільки я розумію, не експерт з мережевої OSI, виклик shutdownOutput()Socket насправді надсилає щось у мережі (FIN), що пробуджує мій Selector з іншого боку (така ж поведінка в C мова). Тут у вас є виявлення : фактично виявлення операції зчитування, яка не вдасться виконати її.

У коді, який ви надаєте, закриття сокета призведе до вимкнення як вхідних, так і вихідних потоків, без можливості зчитування даних, які можуть бути доступними, а тому їх втрата. Метод Java Socket.close()виконує "витончене" відключення (протилежне тому, що я спочатку думав) у тому, що дані, залишені у вихідному потоці, будуть відправлені з наступним FIN для сигналізації про його закриття. FIN буде підтверджено іншою стороною, як і будь-який звичайний пакет 1 .

Якщо вам потрібно зачекати, поки інша сторона закриє розетку, вам потрібно дочекатися її FIN. І для цього потрібно виявити Socket.getInputStream().read() < 0, що означає, що ви не повинні закривати свою розетку, оскільки вона закриває своюInputStream .

З того, що я зробив у C, а тепер і в Java, досягнення такого синхронізованого закриття повинно бути зроблено так:

  1. Вихід сокета для вимкнення (посилає FIN з іншого кінця, це останнє, що коли-небудь буде надіслано цим сокетом). Вхід все ще відкритий, тому ви можете read()виявити пульт дистанційного керуванняclose()
  2. Читайте сокет, InputStreamпоки ми не отримаємо відповідь-FIN з іншого кінця (оскільки він виявить FIN, він пройде той самий витончений процес відключення). Це важливо для деяких ОС, оскільки вони насправді не закривають сокет, поки один із його буферів все ще містить дані. Вони називаються "привидними" сокетами і використовують номери дескрипторів в ОС (це може не бути проблемою для сучасної ОС)
  3. Закрийте сокет (зателефонувавши Socket.close()або закривши його InputStreamабо OutputStream)

Як показано в наступному фрагменті Java:

public void synchronizedClose(Socket sok) {
    InputStream is = sok.getInputStream();
    sok.shutdownOutput(); // Sends the 'FIN' on the network
    while (is.read() > 0) ; // "read()" returns '-1' when the 'FIN' is reached
    sok.close(); // or is.close(); Now we can close the Socket
}

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

Як зазначив @WarrenDew у своєму коментарі, відкидання даних у програмі (прикладний рівень) спричиняє не витончене роз'єднання на рівні програми: хоча всі дані були отримані на рівні TCP ( whileцикл), вони відкидаються.

1 : З " Фундаментальної мережі в Java ": див. Рис. 3.3 с.45, і весь §3.7, с.43-48


Java виконує витончене закриття. Це не "жорстоко".
Маркіз Лорнський

2
@EJP, "витончене відключення" - це специфічний обмін, що відбувається на рівні TCP, де Клієнт повинен сигналізувати про свою волю до відключення від Сервера, який, у свою чергу, надсилає решту даних перед тим, як закрити свою сторону. Частина "надсилати залишені дані" повинна обробляти програма (хоча люди більшу частину часу нічого не надсилатимуть). Виклик socket.close()є "жорстоким", оскільки він не поважає цю сигналізацію Клієнт / Сервер. Сервер буде повідомлений про відключення клієнта лише тоді, коли його власний буфер виведення сокета заповнений (оскільки інша сторона, яка була закрита, не перевіряє дані).
Matthieu

Для отримання додаткової інформації див. MSDN .
Матьє

Java не змушує додатки викликати Socket.close () замість того, щоб спочатку «надсилати [інші] залишені дані», і це не заважає їм спочатку застосувати вимкнення. Socket.close () просто робить те, що робить close (sd). У цьому плані Java нічим не відрізняється від самого API Berkeley Sockets. Дійсно, в більшості аспектів.
Маркіз Лорнський

2
@LeonidUsov Це просто неправильно. Java read()повертає -1 в кінці потоку і продовжує це робити, скільки разів ви це не називаєте. AC read()або recv()повертає нуль в кінці потоку, і продовжує це робити, скільки разів ви це називаєте.
Маркіз Лорнський

18

Я думаю, що це більше питання програмування сокетів. Java просто дотримується традицій програмування сокетів.

З Вікіпедії :

TCP забезпечує надійну, упорядковану доставку потоку байтів від однієї програми на одному комп'ютері до іншої програми на іншому комп'ютері.

Після завершення рукостискання TCP не робить жодної різниці між двома кінцевими точками (клієнтом та сервером). Термін "клієнт" та "сервер" в основному для зручності. Отже, "сервер" може надсилати дані, а "клієнт" може надсилати деякі інші дані одночасно один одному.

Термін "Закрити" також вводить в оману. Існує лише декларація FIN, що означає "Я більше не збираюся надсилати вам речі". Але це не означає, що в польоті немає пакетів, або інший більше не має чого сказати. Якщо ви використовуєте поштову пошту як рівень зв'язку або якщо ваш пакет проїжджав різні маршрути, можливо, одержувач отримує пакети в неправильному порядку. TCP знає, як це виправити.

Крім того, ви, як програма, можете не встигати перевіряти, що в буфері. Отже, за вашої зручності ви можете перевірити, що в буфері. Загалом, поточна реалізація сокета не така вже й погана. Якщо насправді був isPeerClosed (), це додатковий дзвінок, який потрібно робити щоразу, коли ви хочете зателефонувати прочитаним.


1
Я не думаю, що ви можете перевірити стан у коді C як на Windows, так і на Linux !!! Java з якихось причин може не виставляти деякі речі, як було б непогано виставити функції getsockopt, які є у Windows та Linux. Насправді, відповіді нижче містять деякий код C для Linux на стороні Linux.
Дін Хіллер,

Я не думаю, що наявність методу 'isPeerClosed ()' якось зобов'язує вас викликати його перед кожною спробою читання. Ви можете просто зателефонувати йому лише тоді, коли це вам явно потрібно. Я погоджуюся, що поточна реалізація сокету не така вже й погана, хоча для цього потрібно писати у вихідний потік, якщо ви хочете з’ясувати, закрита віддалена частина сокета чи ні. Тому що якщо цього немає, вам доведеться обробляти свої письмові дані і з іншого боку, і це просто така ж велика кількість задоволення, як сидіти на вертикально орієнтованому цвяху;)
Ян Хруби,

1
Це робить середнім "більше немає пакетів в польоті. FIN отримується після будь-яких даних у польоті. Однак це не означає, що одноранговий пристрій закрив сокет для введення. Вам потрібно ** надіслати щось * і отримати `` скидання з'єднання '', щоб це виявити. FIN міг просто означати зупинку виробництва.
Маркіз Лорнський,

11

Базовий API сокетів не має такого сповіщення.

Відправляючий стек TCP все одно не надсилає біт FIN до останнього пакета, тому може бути багато даних, які буферизуються, коли програма, що надсилає, логічно закрила свій сокет до того, як ці дані навіть будуть відправлені. Аналогічно, дані, які буферизуються, оскільки мережа швидше, ніж програма, що отримує (я не знаю, можливо, ви передаєте її через повільніше з'єднання), можуть бути значущими для одержувача, і ви не хотіли б, щоб програма, яка отримує, відкинула її просто тому, що стек отримав біт FIN.


У моєму тестовому прикладі (можливо, я маю надати його тут ...) немає спеціально надісланих / отриманих даних через з'єднання. Отже, я майже впевнений, що стек отримує FIN (витончений) або RST (у деяких не витончених сценаріях). Це також підтверджує netstat.
Олександр

1
Звичайно - якщо нічого не буферизовано, FIN буде негайно надіслано на інший порожній пакет (без корисного навантаження). Однак після FIN до цього кінця з'єднання більше не надсилаються пакети даних (він все одно буде АКТИРУВАТИ все, що йому надіслано).
Mike Dimmick

1
Що відбувається, так це те, що сторони з'єднання опиняються в <code> CLOSE_WAIT </code> і <code> FIN_WAIT_2 </code>, і саме в цьому стані <code> isConcected () </code> та <code> isClosed () </code> все ще не бачить, що з'єднання було розірвано.
Олександр

Дякуємо за ваші пропозиції! Думаю, зараз я краще розумію проблему. Я зробив запитання більш конкретним (див. Третій абзац): чому немає "Socket.isClosing" для тестування напівзакритих з'єднань?
Олександр

8

Оскільки жодна з відповідей поки що повністю не відповідає на питання, я підсумовую своє поточне розуміння проблеми.

Коли встановлюється TCP-з'єднання і один виклик close()або shutdownOutput()його сокет телефонує , сокет на іншій стороні з'єднання переходить у CLOSE_WAITстан. В принципі, з стеку TCP можна дізнатися, чи знаходиться сокет у CLOSE_WAITстані, не викликаючи read/recv(наприклад, getsockopt()у Linux: http://www.developerweb.net/forum/showthread.php?t=4395 ), але це не так портативний.

SocketКлас Java, здається, розроблений для забезпечення абстракції, порівнянної із сокетом BSD TCP, ймовірно, тому, що це рівень абстракції, до якого люди звикли при програмуванні додатків TCP / IP. BSD-сокети - це узагальнюючі підтримуючі сокети, крім просто INET (наприклад, TCP), тому вони не забезпечують портативний спосіб виявлення стану TCP сокета.

Не існує такого методу, як isCloseWait()тому, що люди звикли програмувати TCP-додатки на рівні абстракції, запропонованому сокетами BSD, не очікують, що Java надасть додаткові методи.


3
Також Java не може забезпечити будь-які додаткові методи, портативно. Можливо, вони могли б зробити метод isCloseWait (), який повертав би false, якщо платформа його не підтримувала, але скільки програмістів вкусила б ця помилка, якби вони тестували лише на підтримуваних платформах?
ефемієнт

1
здається, це може бути для мене портативним ... у Windows є msdn.microsoft.com/en-us/library/windows/desktop/… та linux this pubs.opengroup.org/onlinepubs/009695399/functions/…
Dean Hiller

1
Справа не в тому, що програмісти до цього звикли; це те, що інтерфейс сокета - це те, що корисно для програмістів. Майте на увазі, що абстракція сокетів використовується більше, ніж протокол TCP.
Уоррен Дью,

У Java немає такого методу, як, isCloseWait()оскільки він не підтримується на всіх платформах.
Маркіз Лорнський,

Протокол ident (RFC 1413) дозволяє серверу або тримати з'єднання відкритим після надсилання відповіді, або закривати його, не надсилаючи більше даних. Клієнт Java-ідентифікатора може вирішити тримати з'єднання відкритим, щоб уникнути тристороннього рукостискання при наступному пошуку, але як він знає, що з'єднання все ще відкрите? Чи слід просто спробувати відповісти на будь-яку помилку, повторно відкривши з'єднання? Або це помилка проектування протоколу?
Девід

7

Визначити, чи закрито віддалену сторону підключення сокета (TCP), можна за допомогою методу java.net.Socket.sendUrgentData (int) та ловити викид IOException, який віддалено. Це перевірено між Java-Java та Java-C.

Це дозволяє уникнути проблеми проектування комунікаційного протоколу для використання певного механізму пінгування. Відключивши OOBInline на сокеті (setOOBInline (false), будь-які отримані дані OOB мовчки відкидаються, але дані OOB все одно можуть бути надіслані. Якщо віддалена сторона закрита, спроба скидання з'єднання робиться невдалою і спричиняє скидання IOException .

Якщо ви фактично використовуєте дані OOB у своєму протоколі, тоді ваш пробіг може відрізнятися.


4

стек Java IO однозначно надсилає FIN, коли він руйнується під час різкого відключення. Просто немає сенсу, що ви не можете це виявити, б / к більшість клієнтів надсилають FIN лише в тому випадку, якщо вони відключають з'єднання.

... ще одна причина, через яку я справді починаю ненавидіти класи NIO Java. Здається, все трохи напівзадниця.


1
також, схоже, я отримую і кінець потоку при читанні (-1 повернення), коли присутній FIN. Отже, це єдиний спосіб, який я бачу, щоб виявити відключення на стороні читання.

3
Ви можете це виявити. Ви отримуєте EOS під час читання. І Java не надсилає FIN. TCP робить це. Java не реалізує TCP / IP, вона просто використовує реалізацію платформи.
Маркіз Лорнський,

4

Це цікава тема. Зараз я перекопав код Java, щоб перевірити. З мого висновку, є дві різні проблеми: перша - це сам TCP RFC, який дозволяє віддалено закритому сокету передавати дані в напівдуплексі, тому віддалено закритий сокет все ще залишається напіввідкритим. Відповідно до RFC, RST не розриває з'єднання, вам потрібно надіслати явну команду ABORT; тому Java дозволяє передавати дані через напівзакритий сокет

(Існує два методи зчитування статусу закриття в обох кінцевих точках.)

Інша проблема полягає в тому, що реалізація каже, що така поведінка є необов’язковою. Оскільки Java прагне бути портативною, вони реалізували найкращу загальну функцію. Здається, підтримка карти (ОС, реалізація напівдуплексу) була б проблемою.


1
Припускаю, ви говорите про RFC 793 ( faqs.org/rfcs/rfc793.html ), Розділ 3.5 Закриття з’єднання. Я не впевнений, що це пояснює проблему, тому що обидві сторони завершують витончене відключення з'єднання і потрапляють до штатів, де вони не повинні надсилати / отримувати будь-які дані.
Олександр

Залежить. скільки FIN ви бачите на розетці? Крім того, це може бути специфічною проблемою для платформи: можливо, вікна відповідають на кожну FIN з FIN, а зв’язок закрито на обох кінцях, але інші операційні системи можуть не поводитися так, і ось тут виникає проблема 2
Lorenzo Boccaccia

1
Ні, на жаль, це не так. isOutputShutdown і isInputShutdown - це перше, що кожен намагається зіткнутися з цим "відкриттям", але обидва методи повертають false. Я щойно тестував його на Windows XP та Linux 2.6. Повернені значення всіх 4 методів залишаються однаковими навіть після спроби зчитування
Олександр

1
Для запису, це не напівдуплекс. Напівдуплекс означає, що лише одна сторона може надсилати одночасно; обидві сторони все ще можуть надіслати.
rbp

1
isInputShutdown і isOutputShutdown перевіряють локальний кінець з'єднання - це тести, щоб з'ясувати, чи викликали ви на цьому сокеті shutdownInput або shutdownOutput. Вони не говорять вам нічого про віддалений кінець з'єднання.
Mike Dimmick

3

Це недолік класів сокетів OO (і всіх інших, які я розглядав) - відсутність доступу до вибраного системного виклику.

Правильна відповідь на С:

struct timeval tp;  
fd_set in;  
fd_set out;  
fd_set err;  

FD_ZERO (in);  
FD_ZERO (out);  
FD_ZERO (err);  

FD_SET(socket_handle, err);  

tp.tv_sec = 0; /* or however long you want to wait */  
tp.tv_usec = 0;  
select(socket_handle + 1, in, out, err, &tp);  

if (FD_ISSET(socket_handle, err) {  
   /* handle closed socket */  
}  

Ви можете зробити те ж саме з getsocketop(... SOL_SOCKET, SO_ERROR, ...).
Девід Шварц,

1
Набір дескрипторів файлів помилок не буде означати закрите з'єднання. Будь ласка, прочитайте інструкцію з вибору: 'Osimfds - Цей набір спостерігається за "виняткових умов". На практиці поширена лише одна така виняткова умова: наявність позасмугових даних (OOB) для зчитування з сокета TCP. ' FIN - це не дані OOB.
Ян Вробель,

1
Ви можете використовувати клас 'Selector' для доступу до syscall 'select ()'. Однак він використовує NIO.
Matthieu

1
Немає нічого виняткового у зв'язку, яке було відключено іншою стороною.
Девід Шварц,

1
Багато платформи, в тому числі Java, зробити забезпечують доступ до вибраного () системному виклику.
Маркіз Лорнський,

2

Ось кульгаве рішення. Використовуйте SSL;) і SSL виконує рукостискання при розірванні, тому ви отримуєте сповіщення про закриття сокета (більшість реалізацій, схоже, роблять належне розірвання рукостискання).


2
Як отримувати "повідомлення" про закриття сокета при використанні SSL у Java?
Dave B

2

Причиною такої поведінки (яка не є специфічною для Java) є той факт, що ви не отримуєте ніякої інформації про статус із стеку TCP. Зрештою, сокет - це лише черговий дескриптор файлу, і ви не можете дізнатись, чи є з нього фактичні дані для читання, не намагаючись насправді ( select(2)це не допоможе, це лише сигнал про те, що ви можете спробувати, не блокуючи).

Для отримання додаткової інформації дивіться поширені запитання про сокет Unix .


2
Сокети REALbasic (в Mac OS X і Linux) базуються на BSD-сокетах, але RB вдається видавати приємну помилку 102, коли з'єднання розривається іншим кінцем. Тож я погоджуюсь з оригінальним плакатом, це повинно бути можливим, і це кульгаво, що Java (і Какао) цього не надають.
Joe Strout

1
@JoeStrout RB може робити це лише тоді, коли ви виконуєте певні операції вводу-виводу. Не існує API, який надаватиме вам статус з'єднання без введення-виведення. Період. Це не недолік Java. Насправді це пов’язано з відсутністю «тону набору номера» в TCP, що є навмисною особливістю дизайну.
Маркіз Лорнський,

select() повідомляє, чи є дані або EOS доступні для читання без блокування. "Сигнали, які можна спробувати, не блокуючи", безглуздо. Якщо ви перебуваєте в режимі неблокування, ви завжди можете спробувати, не блокуючи. select()керується даними в буфері прийому сокета або очікуваному FIN або просторі в буфері передачі сокета.
Маркіз Лорнський

@EJP Що про getsockopt(SO_ERROR)? Насправді навіть getpeernameскаже вам, чи все ще розетка підключена.
Девід Шварц

0

Тільки записи вимагають обміну пакетами, що дозволяє визначити втрату з'єднання. Поширеним методом є використання опції KEEP ALIVE.


Я думаю, що кінцевій точці дозволено ініціювати витончене відключення з'єднання, відправивши пакет із набором FIN, не записуючи жодного корисного навантаження.
Олександр

@Alexander Звичайно, так, але це не стосується цієї відповіді, яка стосується виявлення меншої кількості зв’язку.
Маркіз Лорнський

-3

Що стосується роботи з напіввідкритими сокетами Java, можна поглянути на isInputShutdown () та isOutputShutdown () .


Ні. Це говорить вам лише те, що ви телефонували, а не те, що викликав колега.
Маркіз Лорнський

Хочете поділитися своїм джерелом для цього твердження?
JimmyB

3
Хочете поділитися своїм джерелом із протилежним? Це ваше твердження. Якщо у вас є доказ, давайте. Я стверджую, що ви помиляєтесь. Зробіть експеримент і доведіть, що я не правий.
Маркіз Лорнський,

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