Извините, много букаф
Позволю себе процитировать замечательную, на мой взгляд, книженцию "Эффективное программирование TCP/IP" Йона Снейдера. Ее легко найти в сети в сканированном виде. Изумительно легко написана, очень подробно, а главное с примерами программирования и очень подробным разбором типичных источников ошибок и заблуждений, в том числе самой распространенной - о гарантированной доставке...
Потенциальные ошибки.
Вы уже видели одну из потенциальных ошибок при работе с TCP: не исключено, что
подтвержденные TCP данные не дошли до приложения-получателя. Как и большинство других
таких ошибок, это довольно редкая ситуация, и, даже если она встречается, последствия
могут быть не очень печальными. Важно, чтобы программист знал об этой неприятности и
предусматривал защиту при возможном нежелательном результате. Не думайте, что TCP обо
всем позаботится сам, следует подумать об устойчивости приложения.
Защита от упомянутой ошибки очевидна. Если приложению-отправителю важно иметь
информацию, что сообщение дошло до приложения-получателя, то получатель должен сам
подтвердить факт приема. Часто такое подтверждение присутствует неявно. Например, если
клиент запрашивает у сервера некоторые данные и сервер отвечает, то сам ответ - это
подтверждение получения запроса.
Более сложный для клиента вопрос - что делать, если сервер не подтверждает приема?
Это в основном зависит от конкретного приложения, поэтому готового Решения не существует.
Однако стоит отметить, что повторная посылка запроса не всегда годится.
Рассмотрим некоторые типичные ошибки. Пока между двумя хостами существует связь,
TCP гарантирует доставку данных по порядку и без искажений. Ошибка может произойти только
при разрыве связи. Из-за чего же связь может разорваться? Есть три причины:
- постоянный или временный сбой в сети;
- отказ принимающего приложения;
- аварийный сбой самого хоста на принимающем конце.
Каждое из этих событий по-разному отражается на приложении-отправителе.
- Сбой в сети.
Сбои в сети происходят по разным причинам: от потери связи с маршрутизатором или отрезком
опорной сети до выдергивания из разъема кабеля локальной Ethernet-сети.
При наличии у TCP ожидающего запроса на чтение операция возвращает ошибку, и переменная
errno устанавливается в ETIMEDOUT. Если ожидающего запроса на чтение нет, то следующая
операция записи завершится ошибкой. При этом либо будет послан сигнал SIGPIPE, либо (если
этот сигнал перехвачен или игнорируется) в переменную errno записано значение EPIPE.
Если промежуточный маршрутизатор не может переправить далее IР-датаграмму, содержащую
некоторый сегмент, то он посылает хосту - отправителю ICMP-сообшение о том, что сеть или
хост назначения недоступны. В этом случае некоторые реализации возвращают в качестве кода
ошибки значение ENETUNREACH или EHOSTUNREACH
- Отказ приложения.
А теперь разберемся, что происходит, когда аварийно или как-либо иначе завершается
приложение на другом конце соединения. Прежде всего следует понимать, что с точки зрения
вашего приложения аварийное завершение другого конца отличается от ситуации, когда
приложение на том конце вызывает функцию close (или closesocket, если речь идет о Windows),
а затем exit. В обоих случаях TCP на другом конце посылает вашему TCP сегмент FIN. FIN
выступает в роли признака конца файла и означает, что у отправившего его приложения нет
больше данных для вас. Это не значит, что приложение на другом конце завершилось или не
хочет принимать данные. Подробнее это рассмотрено в совете 16. Как приложение уведомляется
о приходе FIN (и уведомляется ли вообще), зависит от его действий в этот момент.
Правильно спроектированное приложение, конечно, не игнорирует ошибки, такая ситуация может
иметь место и в корректно написанных программах. Предположим, что приложение выполняет
подряд несколько операций записи без промежуточного чтения- Типичный пример - FTP. Если
приложение на другом конце «падает», то TCP посылает сегмент FIN. Поскольку данная программа
только пишет, но не читает, в ней не содержится информация о получении этого FIN. При отправке
следующего сегмента TCP на другом конце вернет RST. А в программе опять не будет никаких
сведений об этом, так как ожидающей операции чтения нет. При второй попытке записи после
краха отвечающего конца программа получит сигнал SIGPIPE, если этот сигнал перехвачен или
игнорируется - код ошибки EPIPE.
Такое поведение вполне типично для приложений, выполняющих многократную запись без чтения,
поэтому надо отчетливо представлять себе последствия. Приложение уведомляется только после
второй операции отправки данных завершившемуся партнеру. Но, так как предшествующая операция
привела к сбросу соединения, посланные ей данные были потеряны.
- Kрax хоста на другом конце соединения.
Последняя ошибка, которую следует рассмотреть, - это аварийный останов хоста на другом
конце. Ситуация отличается от краха хоста, поскольку TCP на другом конце не может с помощью
сегмента FIN проинформировать программу о то, что ее партнер уже не работает.
Пока хост на другом конце не перезагрузят, ситуация будет выглядеть как сбой в сети - TCP
удаленного хоста не отвечает. Как и при сбое в сети, TCP продолжает повторно передавать
неподтвержденные сегменты. Но в конце концов, если удаленный хост так и не перезагрузится,
то TCP вернет приложению код ошибки ETIMEDOUT.
А что произойдет, если удаленный хост перезагрузится до того, как TCP Прекратит попытки и
разорвет соединение? Тогда повторно передаваемые вами сегменты дойдут до перезагрузившегося
хоста, в котором нет никакой информации о старых соединениях. В таком случае спецификация
TCP [Postel 1981b] требует, чтобы принимающий хост послал отправителю RST. В результате
отправитель оборвет соединение, и приложение либо получит код ошибки ECONNRESET (если есть
ожидающее чтение), либо следующая операция записи закончится сигналов SIGPIPE или ошибкой
EPIPE.
Для обнаружения потери связи с клиентом необязательно реализовывать пульсацию,
как это делалось в совете 10. Нужно всего лишь установить тайм-аут для операции чтения.
Тогда, если от клиента в течение определенного времени не поступает запросов, то сервер
может предположить, что клиента больше нет, и разорвать соединение. Так поступают многие
FTP-серверы. Это легко сделать, либо явно установив таймер, либо воспользовавшись
возможностями системного вызова select, как было сделано при реализации пульсации.
Операция записи данных в TCP.
В общем случае операция записи не блокирует процесс, если только буфер передачи TCP
не полон. Это означает, что после записи управление почти всегда быстро возвращается
программе. После получения управления нельзя ничего гарантировать относительно
местонахождения «записанных» данных. Как упоминается в совете 9, это имеет значение
для надежности передачи данных.
С точки зрения приложения данные записаны. Поэтому, помня о гарантиях доставки,
предлагаемых TCP, можно считать, что информация дошла до другого конца.
В действительности, некоторые (или все) эти данные в момент возврата из операции записи
могут все еще стоять в очереди на передачу. И если хост или приложение на другом конце
постигнет крах, то информация будет потеряна.
Если отправляющее приложение завершает сеанс аварийно, то TCP все равно будет пытаться
доставить данные.
Еще один важный момент, который нужно иметь в виду, - это обработка ошибки записи.
Если при записи на диск вызов write не вернул код ошибки, то точно известно, что запись
была успешной.
При работе с TCP получение кода ошибки от операции записи - очень редкое явление.
Поскольку операция записи возвращает управление до фактической от правки данных, обычно
ошибки выявляются при последующих операциях. Так как следующей операцией чаще всего
бывает чтение, предполагается, что ошибки записи обнаруживаются при чтении. Операция
записи возвращает лишь ошибки, очевидные в момент вызова, а именно:
- неверный дескриптор сокета;
- файловый дескриптор указывает не на сокет (в случае вызова send и родственных
функций);
- указанный при вызове сокет не существует или не подсоединен;
- в качестве адреса буфера указан недопустимый адрес.
Причина большинства этих проблем - ошибка в программе. После завершения стадии
разработки они почти не встречаются. Исключение составляет код ошибки EPIPE (или сигнал
SIGPIPE), который свидетельствует о сбросе соединения хостом на другом конце. Условия,
при которых такая ошибка возникает, обсуждались в совете 9 при рассмотрении краха
приложения-партнера.
Применительно к TCP соединениям операцию записи лучше представлять себе как копирование
в очередь для передачи, сопровождаемое извещением TCP о появлении новых данных. Понятно,
какую работу TCP произведет дальше, но эти действия будут асинхронны по отношению к самой
записи.
Аккуратное размыкание соединений.
Например, клиент может соединиться с сервером, отправить серию запросов, а затем закрыть
свою половину соединения, предоставив тем самым серверу информацию, что больше запросов
не будет. Серверу для ответа клиенту, возможно, понадобится выполнить большой объем работы
и даже связаться с другими серверами, так что он продолжает посылать данные уже после того,
как клиент прекратил отправлять запросы. С другой стороны, сервер может послать в ответ
сколько угодно данных, так что клиент не определяет заранее, когда ответ закончится.
Поэтому сервер, вероятно, как и клиент, закроет свой конец соединения, сигнализируя о
конце передачи.
После того как ответ на последний запрос клиента отправлен и сервер закрыл свой конец
соединения, TCP завершает фазу разрыва. Обратите внимание, что закрытие соединения
рассматривается как естественный способ известить партнера о прекращении передачи данных.
По сути, посылается признак конца файла EOF.
Теперь, когда вы познакомились с вызовом shutdown, посмотрите, как его можно использовать
для аккуратного размыкания соединения. Цель этой операции гарантировать, что обе стороны
получат все предназначенные им данные до того, соединение будет разорвано.
Просто закрыть соединение в некоторых случаях недостаточно, поскольку могут быть потеряны
еще не принятые данные. Помните, что, когда приложение закрывает соединение, недоставленные
данные отбрасываются.
Когда TCP получает от хоста на другом конце сегмент FIN, он сообщает об этом приложению,
возвращая нуль из операции чтения. Примеры приводятся в строке 45 листинга 3.1 и в строке
20 листинга 3.2, где путем сравнения кода возврата recv с нулем проверяется, получен
ли EOF. Часто возникает путаница, когда в ситуации, подобной той, что показана в листинге
3.1, используется системный вызов select. Когда приложение на другом конце закрывает
отправляющую сторону соединения, вызывая close или shutdown либо просто завершая работу,
select возвращает управление, сообщая, что в сокете есть данные для чтения. Если приложение
при этом не проверяет EOF, то оно может попытаться обработать сегмент нулевой длины или
зациклиться, переключаясь между вызовами read и select.
В сетевых конференциях часто отмечают, что «select свидетельствует о наличии информации
для чтения, но в действительности ничего не оказывается». В действительности хост на другом
конце просто закрыл, как минимум, отправляющую сторону соединения, и данные, о присутствии
которых говорит select, -это всего лишь признак конца файла.