1 заметка с тегом

net РСС

23 августа 2011, 18:40

Как определить, что клиент закрыл соединение

Мне тут на днях потребовалось в клиент-серверном коде моментально определить, закрыл ли клиент соединение или оно всё ещё держится. Решение требовалось сделать как на .NET так и на POSIX с помощью обычного C. Быстрый анализ различных документаций показал, что решения «в лоб» в виде вызова некоторой функции аля «getConnectionState(soket)» просто не существует ни в posix, ни в .NET.

Самое интересное, что по спецификации TCP этот протокол позволяет вполне точно определить когда клиент отсоединился. Речь конечно идёт о корректном завершении соединения, а не о физическом обрыве связи которое нельзя определить штатными средствами tcp протокола.

Требование к задаче следующее:
  1. Должна быть функция которая гарантированно получает N байт из сокета.
  2. Функция должна без промедлений определять отвалившегося клиента.
  3. .NET C# версия должна выбрасывать свой ConnectionLostException в случае разрыва соединения.
  4. POSIX C/C++ версия должна возвращать количество полученных байт или ноль в случае разрыва соединения.
Решение конечно же есть, но для этого требуется задействовать вспомогательную функцию poll(). Эта функция существует как в обычной posix среде, так и в .NET, и имеет одинаковое название в обоих случаях. Разница лишь в том, что в .NET среде она сугубо сокетная, а в posix ею можно любые файловые дискрипторы обрабатывать. Необычность этой функции заключается в том, что определение разрыва соединения это её далеко не основная задача. Этой функцией можно определить разные состояния/события сокета (для .NET) или файлового дискриптора (для posix). По большому счёту эта функция — слуга двух господ, т. к. в разные моменты времени в зависимости от контекста она сигнализирует о совершенно разных событиях в сокете/дискрипторе.

В нашем частном случае после того как соединение было успешно установлено, poll() возвращает true/non zero если сокет имеет данные, либо соединение разорвано (наша задача затем определить что именно произошло: пришли данные или соединение закрыто). В противном случае по таймауту возвращает false/zero (т. е. за время таймаута событий в сокете не случилось). Если к примеру сокет находится только в состоянии установки соединения, то результат возвращённый poll() будет указывать на другие события связанные с сокетом. Это стоит помнить.

Теперь код.

C#:
  1. static public void ReceiveBytes(Socket sock, byte[] data)
  2. {
  3.     int totalReceived = 0;
  4.     int size = data.Length;
  5.  
  6.     // Цикл до тех пор пока не получим все данные
  7.     while (totalReceived < size)
  8.     {
  9.         // Ждём 0.1 секунду события: или новые данные или обрыв
  10.         if (sock.Poll(100000, SelectMode.SelectRead))
  11.         {
  12.             // Проверяем есть ли какие-то данные у сокета для нас
  13.             if (sock.Available == 0)
  14.             {
  15.                 // Данных нет, но раз Poll() вернул true — это обрыв
  16.                 // поэтому бросаем свой ConnectionLostException
  17.                 throw new ConnectionLostException();
  18.             }
  19.             try
  20.             {
  21.                 // Получаем данные
  22.                 totalReceived += sock.Receive(data, totalReceived /* offset */,
  23.                                              size - totalReceived /* size to recieve */,
  24.                                              SocketFlags.None);
  25.             }
  26.             catch (SocketException ex)
  27.             {
  28.                 // В зависимости от того как сконфигурирован сокет
  29.                 // мы можем поймать таймаут или попытку заблокировать сокет
  30.                 if (ex.SocketErrorCode == SocketError.TimedOut ||
  31.                    ex.SocketErrorCode == SocketError.WouldBlock)
  32.                 {
  33.                     // Это не критично и мы продолжаем цикл
  34.                     continue;
  35.                 }
  36.                 else
  37.                 {
  38.                     // Остальные исключения говорят о какой-то ошибке
  39.                     break;
  40.                 }
  41.             }
  42.         }
  43.     }
  44. }
  45.  

С/С++:

  1. int recieveData(int socket, void * buffer, int size)
  2. {
  3.     int totalRecieved = 0, arrived = 0;
  4.     struct pollfd pfd;
  5.  
  6.     // Подготавливаем структуру для вызова функции poll()
  7.     pfd.fd = socket; // Указываем дискриптор сокета который интересует
  8.     pfd.events = POLLIN | POLLHUP | POLLRDNORM;
  9.  
  10.     // Цикл пока не получим все данные
  11.     while(totalRecieved < size)
  12.     {
  13.         // Ожидаем событие в сокете с таймаутом 100 миллисекунд
  14.         if(poll(&pfd, 1100) > 0)
  15.         {
  16.             // Произошло какое-то событие — пытаемся получить данные
  17.             arrived = recv(socket, (char*)buffer + totalRecieved, size - totalRecieved, MSG_DONTWAIT);
  18.  
  19.             // Если данных не пришло после того как poll() вернул не нулевой
  20.             // результат — это означает только обрыв соединения
  21.             if(arrived == -1 || totalRecieved == arrived)
  22.             {
  23.                 // Сбрасываем счётчик полученных данных, что скажет об разрыве соединения
  24.                 totalRecieved = 0;
  25.                 break;
  26.             }
  27.  
  28.             // Продолжаем получать данные
  29.             totalRecieved += arrived;
  30.         }
  31.     }
  32.  
  33.     return totalRecieved;
  34. }
  35.  

Приведенный выше код прекрасно отрабатывает и мгновенно отлавливает ситуации когда клиент закрывает соединение.

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

Happy coding! =)
posix   net   c&cpp   c#