Ранее мы уже говорили, что в системах с несколькими сетевыми картами привязка сокета к адресу в том случае, когда его выбор доверен системе, может осуществляться не во время выполнения функции
bind
, а позже, когда системе станет понятно, зачем используется этот сокет. Например, когда TCP-клиент осуществляет подключение к серверу, система по адресу этого сервера определяет, через какую карту должен идти обмен, и выбирает соответствующий адрес. То же самое происходит с UDP-клиентом: когда он отправляет первую дейтаграмму, система по адресу получателя определяет, к какой карте следует привязать сокет. Поэтому клиент и в данном случае может оставить выбор адреса на усмотрение системы. С серверами все несколько сложнее. Система привязывает сокет UDP-сервера к адресу, он ожидает получения пакета. В этот момент система не имеет никакой информации о том, с какими узлами будет вестись обмен через данный сокет, и может выбрать не тот адрес, который нужен. Поэтому сокеты UDP-серверов, работающих
в подобных системах, должны явно привязываться к требуемому адресу. Сокеты TCP-серверов, находящиеся в режиме ожидания и имеющие адрес
INADDR_ANY
, допускают подключение к ним по любому сетевому интерфейсу, который имеется в системе. Сокет, который создается таким сервером при подключении клиента, будет автоматически привязан к IP-адресу того сетевого интерфейса, через который осуществляется взаимодействие с подключившимся клиентом. Таким образом, сокеты, созданные для взаимодействия с разными клиентами, могут оказаться привязанными к разным адресам.
После успешного завершения функций
socket
и
bind
сокет создан и готов к работе. Дальнейшие действия с ним зависят от того, какой протокол он реализует и для какой роли предназначен. Мы разберем эти операции в разделах, посвященных соответствующим протоколам. Там же мы увидим, что в некоторых случаях можно обойтись без вызова функции
bind
, поскольку она будет неявно вызвана при вызове других функций библиотеки сокетов.
Когда сокет больше не нужен, следует освободить связанные с ним ресурсы. Это выполняется в два этапа: сначала сокет "выключается", а потом закрывается.
Для выключения сокета предусмотрена функция
shutdown
, имеющая следующий прототип:
function shutdown(s: TSocket; how: Integer): Integer;
Параметр
s
определяет сокет, который необходимо выключить, параметр
how
может принимать значения
SD_RECEIVE
,
SD_SEND
или
SD_BOTH
. Функция возвращает ноль при успешном выполнении и
SOCKET_ERROR
— в случае ошибки. Вызов функции с параметром
SD_RECEIVE
запрещает чтение данных из входного буфера сокета. Однако на у ровне протокола вызов этой функции игнорируется: дейтаграммы UDP и пакеты TCP, посланные данному сокету, продолжают помещаться в буфер, хотя программа уже не может их оттуда забрать.
При указании значения
SD_SEND
функция запрещает отправку данных через сокет. В случае протокола TCP при этом удаленный сокет получает специальный сигнал, предусмотренный данным протоколом, уведомляющий о том, что больше данные посылаться не будут. Если на момент вызова
shutdown
в буфере для исходящих остаются данные, сначала посылаются они. а потом только сигнал о завершении. Поскольку протокол UDP подобных сигналов не предусматривает, то в этом случае
shutdown
просто запрещает библиотеке сокетов использовать указанный сокет для отправки данных.
Параметр
SD_BOTH
позволяет одновременно запретить и прием, и передачу данных через сокет.
Примечание
Модуль
WinSock
до пятой версии Delphi включительно содержит ошибку: в нем не определены константы
SD_XXX
. Чтобы использовать их в своей программе, нужно объявить их так, как показано в листинге 2.5.
Листинг 2.5. Объявление констант SD_XXX для Delphi 5 и более ранних версий
const
SD_RECEIVE = 0;
SD_SEND = 1;
SD_BOTH = 2;
Для освобождения ресурсов, связанных с сокетом, служит функция
closesocket
, которая освобождает память, выделенную для буферов, и порт. Ее единственный параметр задает сокет, который требуется закрыть, а возвращаемое значение — ноль или
SOCKET_ERROR
. После вызова этой функции соответствующий дескриптор сокета перестает иметь смысл, и использовать его больше нельзя.
По умолчанию функция
closesocket
немедленно возвращает управление вызвавшей ее программе, а процесс закрытия сокета начинает выполняться в фоновом режиме. Под закрытием подразумевается не только освобождение ресурсов, но и отправка данных, которые остались в выходном буфере сокета. Вопрос о том, как изменить поведение функции
closesocket
, будет обсуждаться в разд. 2.1.17. Если сокет закрывается одной нитью в тот момент, когда другая нить пытается выполнить какую-либо операцию с этим сокетом, то эта операция завершается с ошибкой.
Функция
shutdown
нужна в первую очередь для того, чтобы заранее сообщить партнеру по связи о намерении завершить связь, причем это имеет смысл только для протоколов, поддерживающих соединение. В
случае UDP функцию shutdown вызывать практически бессмысленно, можно сразу вызывать
closesocket
. При использовании TCP удаленная сторона получает сигнал о выключении партнера, но стандартная библиотека сокетов не позволяет программе обнаружить его получение (такие функции есть в сокетах Windows, о чем мы будем говорить далее). Но этот сигнал может быть важен для внутрисистемных функций, реализующих сокеты. Windows-версия библиотеки сокетов относится к отсутствию данного сигнала достаточно либерально, поэтому вызов shutdown в том случае, когда и клиент, и сервер работают под управлением Windows, не обязателен. Но реализации TCP в других системах не всегда столь же снисходительно относятся к подобной небрежности. Результатом может стать долгое (до двух часов) "подвешенное" состояние сокета в той системе, когда с ним и работать уже нельзя, и информации об ошибке программа не получает. Поэтому в случае TCP лучше не пренебрегать вызовом
shutdown
, чтобы сокет на другой стороне не имел проблем.
MSDN рекомендует следующий порядок закрытия TCP-сокета. Во-первых, сервер не должен закрывать свой сокет по собственной инициативе, он может это делать только после того, как был закрыт связанный с ним клиентский сокет. Клиент начинает закрытие сокета с вызова
shutdown
с параметром
SD_SEND
. Сервер после этого сначала получает все данные, которые оставались в буфере сокета клиента, а затем получает от клиента сигнал о завершении передачи. Тем не менее сокет клиента продолжает работать на прием, поэтому сервер при необходимости может на этом этапе послать клиенту какие-либо данные, если это необходимо. Затем сервер вызывает
shutdown
с параметром
SD_SEND
, и сразу после этого —
closesocket
. Клиент продолжает читать данные из входящего буфера сокета до тех пор, пока не будет получен сигнал о завершении передачи сервером. После этого клиент также вызывает
closesocket
. Такая последовательность гарантирует, что данные не будут потеряны, но, как мы уже обсуждали ранее, она не может быть реализована в рамках стандартных сокетов из-за невозможности получить сигнал о завершении передачи, посланный удаленной стороной. Поэтому на практике следует реализовывать упрощенный способ завершения связи: клиент вызывает
shutdown
с параметром
SD_SEND
или
SD_BOTH
и сразу после этого —
closesocket
. Сервер при попытке выполнить операцию с сокетом получает ошибку, после которой также вызывает
closesocket
. Вызов
shutdown
на стороне сервера при этом не нужен, т.к. в этот момент соединение уже потеряно, и высылать данные из буфера вместе с сигналом завершения уже некуда.
2.1.9. Передача данных при использовании UDP
Мы наконец-то добрались до изучения того, ради чего сокеты и создавались: как передавать и получать с их помощью данные. По традиции начнем рассмотрение с более простого протокола UDP. Функции, которые рассматриваются в этом разделе, могут работать и с другими протоколами, и от этого их поведение может меняться. Мы здесь описываем только их поведение при использовании UDP.
Для передачи данных удалённому сокету предусмотрена функция
sendto
, описанная следующим образом:
function sendto(s: TSocket; var Buf; len, flags: Integer; var addrto: TSockAddr; tolen: Integer): Integer;
Первый параметр данной функции задаёт сокет, который служит для передачи данных. Здесь нужно указать значение, полученное ранее от функции
socket
. Параметр
Buf
задаёт буфер, в котором хранятся данные для отправки, а параметр
len
— размер этих данных в байтах. Параметр
flags
позволяет указать некоторые дополнительные опции, которых мы здесь касаться не будем, т.к. в большинстве случаев они не нужны. Пока следует запомнить, что параметр
flags
в функции
sendto
, а также в других функциях, где он встречается, должен быть равен нулю. Параметр
addrto
задает адрес (состоящий из IP-адреса и порта) удаленного сокета, который должен получить эти данные. Значение параметра
addrto
должно формироваться по тем же правилам, что значение аналогичного параметра функции bind, за исключением того, что IP-адрес и порт должны быть заданы явно (т.е. не допускаются значения
INADDR_ANY
и нулевой номера порта). Параметр
tolen
задает длину буфера, отведенного для адреса, и должен быть равен
SizeOf(TSockAddr)
. Один вызов функции
sendto
приводит к отправке одной дейтаграммы. Данные, переданные в
sendto
, никогда не разбиваются на несколько дейтаграмм, и данные, переданные последовательными вызовами