* При перепечатке материалов ссылка на www.SeoLiga.ru обязательна!
Неблокирующий режим
19 марта 2009
Выше мы столкнулись с функциями, которые могут надолго приостановить работу вызвавшей их нити, если действие не может быть выполнено немедленно. Это функции 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 в неблокирующий режим, надо выполнить следующие действия: var S:TSocket; Arg:u_long; begin ... Arg:=1; IOCtlSocket(S,FIONBIO,Arg); Пока программа использует только стандартные сокеты (и не использует Windows Sockets), сокет может быть переведён в неблокирующий или обратно в блокирующий режим в любой момент. Неблокирующим может быть сделан любой сокет независимо от того, какой протокол он использует и является ли он серверным или клиентским. Функция 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. Следующий пример кода иллюстрирует это: var S:Scoket; 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 IsSet(S,SetW) then // Connect выполнен успешно else if IsSet(S,SetE) then // Соединиться не удалось else // Произошла ещё какая-то ошибка Напомню, что сокет, входящий в множество SetW, будет считаться готовым, если он соединён, а в его выходном буфере есть место. Сокет, входящий в множество SetE, будет считаться готовым, если попытка соединения не удалась. До тех пор, пока попытка соединения не завершилась (успехом или неудачей), ни одно из этих условий готовности не будет выполнено. Таким образом, в данном случае Select завершит работу только после того, как будет выполнена попытка соединения, и о результатах этой попытки можно будет судить по тому, в какое из множеств входит сокет. Из приведённого примера не видно, какие преимущества даёт неблокирующий сокет по сравнению с блокирующим. Казалось бы, проще вызвать Connect в блокирующем режиме, дождаться результата и лишь потом переводить сокет в неблокирующий режим. Во многих случаях это действительно может оказаться удобнее. Преимущества соединения в неблокирующем режиме связаны с тем, что между вызовами Connect и Select программа может выполнить какую-либо полезную работу, а в случае блокирующего сокета программа будет вынуждена сначала дождаться завершения работы функции Connect и лишь потом сделать что-то ещё. Функция Send для неблокирующего сокета также имеет некоторые специфические черты поведения. Они проявляются, когда свободное место в выходном буфере есть, но его недостаточно для хранения данных, которые программа пытается отправить с помощью этой функции. В этом случае функция Send, согласно документации, может скопировать в выходной буфер такой объём данных, для которого хватает места. В этом случае она вернёт значение, равное этому объёму (оно будет меньше, чем значение параметра Len, заданного программой). Оставшиеся данные программа должна отправить позже, вызвав ещё раз функцию Send. Такое поведение функции Send возможно только при использовании TCP. В случае UDP дейтаграмма никогда не разделяется на части, и если в выходном буфере не хватает места для всей дейтаграммы, функция Send возвращает ошибку, а WSAGetLastError - WSAEWouldBlock. Сразу отмечу, что, хотя спецификация допускает частичное копирование функцией Send данных в буфер сокета, на мне такое поведение встретить не удалось: мои эксперименты показали, что функция S всегда либо копирует данные целиком, расширяя при необходимости буфер, либо даёт ошибку WSAEWouldBlock. Ниже этот вопрос будет обсуждаться подробнее. Тем не менее, при написании программ следует учитывать возможность частичного копирования, т.к. оно может появиться в тех условиях или в тех реализациях библиотеки сокетов, которые в моих экспериментах не были проверены.