2.1.15. Неблокирующий режим

2.1.15. Неблокирующий режим

Ранее мы столкнулись с функциями, которые могут надолго приостановить работу вызвавшей их нити, если действие не может быть выполнено немедленно. Это функции accept, recv, recvfrom, send, sendto и connect (в дальнейшем в этом разделе мы не будем упоминать функции recvfrom и sendto, потому что они в смысле блокирования эквивалентны функциям recv и send соответственно, и все, что будет здесь сказано о recv и send, применимо к recvfrom и sendto). Такое поведение не всегда удобно вызывающей программе, поэтому в библиотеке сокетов предусмотрен особый режим работы сокетов — неблокирующий. Этот режим может быть установлен или отменен дм каждого сокета индивидуально с помощью функции ioctlsocket, имеющей следующий прототип:

function ioctlsocket(s: TSocket; cmd: DWORD; var arg: u_long): Integer;

Данная функция предназначена для выполнения нескольких логически мало связанных между собой действий. Возможно, у разработчиков первых версий библиотеки сокетов были причины экономить на количестве функций, потому что мы и дальше увидим, что иногда непохожие операции выполняются одной функцией. Но вернемся к ioctlsocket. Ее параметр cmd определяет действие, которое выполняет функция, а также смысл параметра arg. Допустимы три значения параметра cmd: SIOCATMARK, FIONREAD и FIONBIO. При задании SIOCATMARK параметр arg рассматривается как выходной: в нем возвращается ноль, если во входном буфере сокета имеются высокоприоритетные данные, и ненулевое значение, если таких данных нет (как уже было оговорено, мы в этой книге не будем касаться передачи высокоприоритетных данных).

При cmd, равном FIONREAD, в параметре arg возвращается размер данных, находящихся во входном буфере сокета, в байтах. При использовании TCP это число равно максимальному количеству информации, которое можно получить на данный момент за один вызов recv. Для UDP это значение равно суммарному размеру всех находящихся в буфере дейтаграмм (напомним, что прочитать несколько дейтаграмм за один вызов recv нельзя). Функция ioctlsocket с параметром FIONREAD может использоваться для проверки наличия данных с целью избежать вызова recv тогда, когда это может привести к блокированию, или для организации вызова recv в цикле до тех пор, пока из буфера не будет извлечена вся информация.

При задании аргумента FIONBIO параметр arg рассматривается как входной. Если его значение равно нулю, сокет будет переведен в блокирующий режим, если не равно нулю — в неблокирующий. Таким образом, чтобы перевести который сокет s в неблокирующий режим, нужно выполнить следующие действия (листинг 2.28).

Листинг 2.28. Перевод сокета в неблокирующий режим

var

 S: TSocket;

 Arg: u_long;

begin

 Arg:= 1;

 ioctlsocket(S, FIONBIO, Arg);

Пока программа использует только стандартные сокеты (а не сокеты Windows), сокет может быть переведен в неблокирующий или обратно в блокирующий режим в любой момент. Неблокирующим может быть сделан любой сокет (серверный или клиентский) независимо от протокола.

Функция ioctlsocket возвращает нулевое значение в случае успеха и ненулевое — при ошибке. В примере, как всегда, проверка результата для краткости опущена.

Итак, по умолчанию сокет работает в блокирующем режиме. С особенностями работы функций accept, connect, recv и send в этом режиме мы уже познакомились. Теперь рассмотрим то, как они ведут себя в неблокирующем режиме. Для этого сначала вспомним, когда эти функции блокируют вызвавшую их нить.

? accept — блокирует нить, если на момент ее вызова очередь подключений пуста.

? connect — в случае TCP блокирует сокет практически всегда, потому что требуется время на установление связи с удаленным сокетом. Без блокирования вызов connect выполняется только в том случае, если какая-либо ошибка не дает возможности приступить к операции установления связи. Также без блокирования функция connect выполняется при использовании UDP, потому что в данном случае она только устанавливает фильтр для адресов.

? recv — блокирует нить, если на момент вызова входной буфер сокета пуст.

? send — блокирует нить, если в выходном буфере сокета недостаточно места, чтобы скопировать туда переданную информацию.

Если условия, при которых эти функции выполняются без блокирования, выполнены, то их поведение в блокирующем и неблокирующем режимах идентично. Если же выполнение операции без блокирования невозможно, функции возвращают результат, указывающий на ошибку. Чтобы понять, произошла ли ошибка из-за необходимости блокирования или из-за чего-либо еще. программа должна вызвать функцию WSAGetLastError. Если она вернет WSAEWOULDBLOCK, значит, никакой ошибки не было, но выполнение операции без блокирования невозможно. Закрывать сокет и создавать новый после WSAEWOULDBLOCK, разумеется, не нужно, т. к. ошибки не было, и связь (в случае TCP) осталась неразорванной.

Следует отметить, что при нулевом выходном буфере сокета (т. е. когда функция send передаст данные напрямую в сеть) и большом объеме информации функция send может выполняться достаточно долго, т. к. эти данные отправляются по частям, и на каждую часть в рамках протокола TCP получаются подтверждения. Но эта задержка не считается блокированием, и в данном случае send будет одинаково вести себя с блокирующими и неблокирующими сокетами, т. е. вернет управление программе лишь после того, как все данные окажутся в сети.

Для функций accept, recv и send WSAEWOULDBLOCK означает, что операцию следует повторить через некоторое время, и, может быть, в следующий раз она не потребует блокирования и будет выполнена. Функция connect в этом случае начинает фоновую работу по установлению соединения. О завершении этой работы можно судить по готовности сокета, которая проверяется с помощью функции select. Листинг 2.29 иллюстрирует это.

Листинг 2.29. Установление связи при использовании неблокирующего сокета

var

 S: TSocket;

 Block: u_long;

 SetW, SetE: TFDSet;

begin

 S:=socket(AF_INET, SOCK_STREAM, 0);

 Block:= 1;

 ioctlsocket(S, FIONBIO, Block);

 connect(S…);

 if WSAGetLastError <> WSAEWOULDBLOCK then

 begin

// Произошла ошибка

 raise…

 end;

 FD_ZERO(SetW);

 FD_SET(S, SetW);

 FD_ZERO(SetE);

 FD_SET(S, SetE);

 select(0, nil, @SetW, @SetE, nil);

 if FD_ISSET(S, SetW) then

// Connect выполнен успешно

 else if FD_ISSET(S, SetE) then

// Соединиться не удалось

 else

// Произошла еще какая-то ошибка

Напомним, что сокет, входящий в множество SetW, будет считаться готовым, если он соединен, а в его выходном буфере есть место. Сокет, входящий в множество SetE, будет считаться готовым, если попытка соединения не удалась. До тех пор, пока попытка соединения не завершилась (успехом или неудачей), ни одно из этих условий готовности не будет выполнено. Таким образом, в данном случае select завершит работу только после того, как будет выполнена попытка соединения, и о результатах этой попытки можно будет судить по тому, в какое из множеств входит сокет.

Из приведенного примера не видно, какие преимущества дает неблокирующий сокет по сравнению с блокирующим. Казалось бы, проще вызвать connect в блокирующем режиме, дождаться результата и лишь потом переводить сокет в неблокирующий режим. Во многих случаях это действительно может оказаться удобнее. Преимущества соединения в неблокирующем режиме связаны с тем, что между вызовами connect и select программа может выполнить какую-либо полезную работу, а в случае блокирующего сокета программа будет вынуждена сначала дождаться завершения работы функции connect и лишь потом сделать что-то еще.

Функция send для неблокирующего сокета также имеет некоторые специфические черты поведения. Они проявляются, когда свободное место в выходном буфере есть, но его недостаточно для хранения данных, которые программа пытается отправить с помощью этой функции. В этом случае функция send, согласно документации, может скопировать в выходной буфер такой объем данных, для которого хватает места. При этом она вернет значение, равное этому объему (оно будет меньше, чем значение параметра len, заданного программой). Оставшиеся данные программа должна отправить позже, вызвав еще раз функцию send. Такое поведение функции send возможно только при использовании TCP. В случае UDP дейтаграмма никогда не разделяется на части, и если в выходном буфере не хватает места для всей дейтаграммы, то функция send возвращает ошибку, a WSAGetLastError — WSAEWOULDBLOCK.

Сразу отметим, что, хотя спецификация допускает частичное копирование функцией send данных в буфер сокета, на практике такое поведение наблюдать пока не удалось: все эксперименты показали, что функция send всегда либо копирует данные целиком, расширяя при необходимости буфер, либо дает ошибку WSAEWOULDBLOCK. Далее этот вопрос будет обсуждаться подробнее. Тем не менее при написании программ следует учитывать возможность частичного копирования, т. к. оно может появиться в тех условиях или в тех реализациях библиотеки сокетов, которые в наших экспериментах не были проверены.

Данный текст является ознакомительным фрагментом.