Sources
Delphi Russian Knowledge Base
DRKB - это самая большая и удобная в использовании база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Обнаружение разъединения

01.01.2007

9. Обнаружение разъединения

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

Имеется событие OnDisconnected, но это не то, о чем вы подумали. Событие OnDisconnected возникает при вызове метода Disconnect. Это событие никак не связано с неожиданным разъединением.

9.1. Скажем прощай

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

Чтобы завершить работу, протоколы используют команду QUIT. Когда клиент готов завершить работу, вместо немедленного разъединения, сначала попробуйте послать команду QUIT на сервер. Затем подождите ответа и только после этого производите отсоединение сокета.

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

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

9.2. А нужно ли вам реально знать это?

Многие программисты ошибаются, считая, что они должны немедленно узнать, что соединение неожиданно прервалось. Вы наверно слышали следующую сентенцию – «если дерево падает в лесу и никто при этом не присутствует, то кто услышит звук?». Если сокет отсоединяется, и он становится не доступным, то в действительно ли сокет закрывается? В большинстве случаев ответ НЕТ. Если сокет физически отсоединен и недоступен, то исключение не будет возбуждено, до тех пор, пока не будет произведена попытка доступа к сокету.

Это может казаться странным, но это также как при работе с файлами. Представим себе, что вы открыли таблицу Экселя с гибкого диска, и затем вынули диск из дисковода. Вы можете продолжать работать с файлом, потому что он кэширован в памяти и Эксель, в данный момент, не обращается к физическому файлу. Иногда в течение этого времени вы забываете, что вы вынули дискету из дисководу. Позже вы продолжаете работу с вашей таблицей, все прекрасно пока вы не захотите сохранить ваши изменения. Только в этот момент вы получите сообщение об ошибке. Эксель не получает "EAnIdiotRemovedTheFloppyDiskException", когда вы вынули дискету, но он имеет открытый файл на этом диске.

9.3. Я должен знать это немедленно!

Повторим, что исключение не возникает немедленно. Например, если вы вытащите сетевой кабель из платы или коммутатора, то может потребоваться минута или более. TCP/IP был разработан военными Америки, чтобы иметь устойчивый сетевой протокол, способный противостоять ядерным атакам. Поэтому он спроектирован так, что сеть ждет некоторое время ответов с другой стороны соединения и пытается делать повторные запросы. Вы можете детектировать разрыв соединения немедленно, но иным путем, чем это реализовано в TCP/IP. Данное детектирование может быть реализовано, но очень важно понять, почему для этого нет стандартных функций. Это понимание поможет вам понять, как это можно реализовать.

В большинстве случаев, если вы думаете об этом, вы поймете, что данное поведение приемлемое и даже желательное.

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

Если вы нуждаетесь в немедленном оповещении о разъединении, то вы должны встроить эту возможность в протокол.

9.3.1. Keep Alives

Keep alive (хранить живым) – это один из методов, детектирования немедленных разъединений. Keep alive реализован так, что одна сторона соединения на основе регулярного интервала посылается сообщение, такое как NOOP (No Operation – нет операции) другой стороне и всегда ожидает ответа. Если ответ не получен в течение указанного времени, то соединение считается проблематичным и уничтожается. Период таймаута достаточно короткий и определятся задачами протокола. Если вы реализуете соединение с сердечным монитором, то вы надеетесь, что указанный таймаут короче, чем таймаут для контроля температуры бочонка с пивом.

Есть и другие преимущества при реализации системы keep alive. Во многих случаях, соединение TCP может оставаться действительным, даже если процесс перестал отвечать и принимать сообщения. В других случаях, процесс может продолжать обслуживать запросы, но скорость может быть очень низкой из-за проблем в сервисе, или из-за уменьшения полосы пропускания сети. Keep alive может определять эти ситуации и отмечать их как непригодные, хотя соединение еще существует.

Keep alive запрос может посылаться на регулярной основе или при необходимости, когда требует определить состояние, в зависимости от требований протокола.

9.3.2. Пинги (Pings)

Пинги – это реализация, подобная keep alive, за исключением, что они не возвращаются в ответе на запрос. Пинги посылаются с одной стороны соединения, на другую на основе регулярного интервала. Одна сторона становится отправителем, а вторая монитором. Если в течение указанного интервала времени на мониторе, ответа не будет, то соединение считается непригодным и уничтожается.

Пинги могут считаться аналогичными биениям сердца. Если они становятся редкими, то это означает проблему.

9.4. Исключение EIdConnClosedGracefully

Многих Indy пользователей беспокоит исключение EIdConnClosedGracefully, которое часто возбуждается серверами, особенно HTTP и другими серверами. Исключение EIdConnClosedGracefully сигнализирует, что другая сторона умышленно закрыла соединение. Это не тоже самое, как потерянное соединение, которое может появляется в случае ошибки соединения. Если другая сторона закрыла соединение и сокет пытается писать или читать из него, то возбуждается исключениеEIdConnClosedGracefully. Это подобно попытке чтения или записи в файл, который был закрыт без вашего оповещения.

В некоторых случаях – это подлинное исключение и ваш код должен его обработать. В других случаях (обычно в серверах) это нормальное функционирование протокола и Indy обработает это за вас. Даже если  Indy поймала его, когда вы работали в отладчике, то оно возбуждается в нем. Вы просто должны нажать F9 и продолжать и Indy обслужить это исключение, но  отладчик постоянно  надоедает вам. В том случае когда Indy ловит подобное исключение, ваши пользователи никогда не видят его в ваших программах, если только программа не запущена под отладчиком IDE.

9.4.1. Введение

9.4.2. Почему случаются исключения на серверах?

Когда клиент подсоединяется к серверу, то имеются два пути к разъединению соединения:

1.Взаимное соглашение – обе стороны согласны взаимно разъединиться, если одна из сторон попросит  об этом и обе стороны явно разъединяются.
2.Одностороннее разъединение – разъединение и оповещение другой стороны об этом.
С помощью взаимного соглашения обе стороны знают когда разъединяться и обе явно производят разъединение. Большинство протоколов обмена, такие как Mail, News и другие разъединяются подобным образом.

Когда клиент готов разъединиться, то он посылает команду серверу, оповещая его о своем желании. Сервер отвечает подтверждением и затем обе стороны закрывают соединение. В этих случаях исключение EIdConnClosedGracefully не возникает и если это случается, то это означает ошибку, которая должна быть соответственно обработана.

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

При одностороннем разъединении одна сторона просто отсоединяется. Другая сторона продолжает следить и как только определяет разрыв, то прекращает сессию. В протоколах, которые используют это, вы получите исключение EIdConnClosedGracefully и это нормальное поведение для них. Это исключение, но Indy в курсе и обработает это за вас. Протокол whois пример данного поведения. Клиент подсоединяется к серверу и посылает строку запроса. Сервер затем посылает ответ и отсоединяется. Ни каких других сигналов клиенту не посылается, просто производится разъединение.

Протокол HTTP позволяет оба вида – разъединение по взаимному соглашению и одностороннее разъединение и поэтому понятно, почему вы видите иногда исключение EIdConnClosedGracefully с HTTP серверами. Протокол HTTP 1.0 работает подобно протоколу whois, в том смысле, что сервер не посылает сигнала о разъединении и клиент должен просто разъединиться после получения ответа. Клиент должен использовать новое соединение для каждого запроса.

Протокол HTTP 1.1 позволяет в одном соединение запрашивать несколько документов. Тем не менее нет команды разъединения. В любое время, или клиент, или сервер могут разъединиться после получения ответов. Когда клиент разъединяется, но сервер еще продолжает воспринимать запросы, то возникает исключение EIdConnClosedGracefully. В большинстве случаев в HTTP 1.1, клиент единственный, кто разъединяется. А сервер разъединяется как сделано в части реализации протокола HTTP 1.1, если не используется установка keep alive.

9.4.3. Почему это исключение?

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

Исключение EIdConnClosedGracefully возбуждается из ядра, глубоко внутри, в нескольких методах. Исключение EIdConnClosedGracefully в действительности исключение и в большинстве случаев должно быть поймано самым верхним уровнем для обработки. Исключение  - это правильный путь для этого.

9.4.4. Это ошибка?

Не все исключения – это ошибки. Многие разработчики считают и уверены, что все исключения это ошибки. Это не так. Именно поэтому они названы исключениями, а не ошибками.

Delphi и C++ Builder используют исключения, для обработки ошибок, элегантным путем. Тем не менее исключения могут использоваться  не только в случае ошибок. Примером этого является EAbort. Исключения подобного рода используются для изменения потока исполнения и для связи с более высоким уровнем в программе, где они будут пойманы. Indy использует исключения подобным образом.

9.4.5. А когда это ошибка?

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

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

9.4.6. Простое решение

Исключение EIdConnClosedGracefully это базовый класс исключения, особенно для некоторых серверов, оно унаследовано от EIdSilentException. На закладке Language Exceptions tab of Debugger Options (Tools Menu) вы может добавить исключение EIdSilentException в список игнорируемых исключении. После этого добавленное исключение, если оно случится в коде будет обработано в программе, но отладчик не будет прерывать программу для обработки в среде.