Малышев_Сетевое программирование 19.12.18
.pdf61
гда удобно вызывающей программе, поэтому в библиотеке сокетов предусмотрен неблокирующий режим работы сокетов. Этот режим может быть установлен или отменен для каждого сокета индивидуально с помощью функции ioctlsocket, имеющей следующий прототип:
function ioctlsocket(s: TSocket; cmd: DWORD; var arg: u_long): Integer;
Данная функция предназначена для выполнения нескольких логически мало связанных между собой действий.
Параметр cmd определяет действие, которое выполняет функция, а также определяет смысл параметра arg. Допустимы три значения пара-
метра cmd: SIOCATMARK, FIONREAD и FIONBIO.
При задании SIOCATMARK параметр arg рассматривается как выходной: в нем возвращается ноль, если во входном буфере сокета имеются высокоприоритетные данные, и ненулевое значение, если таких данных нет.
При cmd, равном FIONREAD, в параметре arg возвращается размер данных, находящихся во входном буфере сокета, в байтах. При использовании TCP это число равно максимальному количеству информации, которое можно получить на данный момент за один вызов recv. Для UDP это значение равно суммарному размеру всех находящихся в буфере дейтаграмм (напомним, что прочитать несколько дейтаграмм за один вызов recv нельзя). Функция ioctlsocket с параметром FIONREAD может использоваться для проверки наличия данных с целью избежать вызова recv тогда, когда это может привести к блокированию (нет данных во входном буфере), или для организации вызова recv в цикле до тех пор, пока из буфера не будет извлечена вся информация.
При задании аргумента FIONBIO параметр arg рассматривается как входной. Если его значение равно нулю, то сокет будет в блокирующем режиме, если не равно нулю будет переведен в неблокирующий. Таким образом, чтобы перевести сокет s в неблокирующий режим, нужно выполнить следующие действия.
Следующий листинг показывет как создать сокет и переключить его в неблокирующий режим [13]:
Листинг 6 SOCKET sock; unsigned long nb = 1; int err;
sock = socket(AF_INET, SOCK_STREAM, 0);
err = ioctlsocket(sock, FIONBIO, (unsigned long *) &nb); if (err == SOCKET_ERROR)
62
{
//ошибка при переключении сокета в неблокирующий режим
}
или [14]
var
S: TSocket;
Arg: u_long; begin
...
Arg := 1;
ioctlsocket(S, FIONBIO, Arg);
…
end;
Пока программа использует только стандартные сокеты (а не сокеты Windows), сокет может быть переведен в неблокирующий или обратно в блокирующий режим в любой момент. Неблокирующим может быть сделан любой сокет (серверный или клиентский) независимо от протокола.
Функция ioctlsocket возвращает нулевое значение в случае успеха и ненулевое – при ошибке. (Во втором примере проверка результата для краткости опущена.)
Итак, по умолчанию сокет работает в блокирующем режиме.
После переключения сокета в неблокирующий режим вызовы Winsock API, связанные с приемом, передачей данных или управлением соединений, будут сразу возвращать управление приложению, не ожидая завершения текущей операции. В большинстве случаев данные вызовы возвращают ошибку типа WSAEWOULDBLOCK, что означает, что операция не имела времени закончится в период вызова функции. К примеру функция recv вернет WSAEWOULDBLOCK, если нет ожидающих данных в системном буфере для данного сокета. Часто нужны дополнительные вызовы функции пока она не вернет сообщение об удачном завершение операции. Следующая таблица описывает значение WSAEWOULDBLOCK при вызове разных Winsock API-функций [13, 14]:
63
Таблица 3 Описание WSAEWOULDBLOCK ошибки для неблокирующих
сокетов
Имя функции |
Описание |
|
accept и WSAAccept |
Нет запросов на установление связи, вызовите опять для провеки |
|
наличия запросов (очередь пуста) |
||
|
||
|
|
|
Closesocket |
В большинстве случаев, это означает, что setsockopt была вызвана |
|
с опцией SO_LINGER отличной от нуля, тайм-аут был установлен. |
||
|
||
|
Установка связи начата, вызовите снова, чтобы проверить завер- |
|
|
шение операции. В случае TCP блокирует сокет практически все- |
|
|
гда, потому что требуется время на установление связи с удален- |
|
connect и |
ным сокетом. Без блокирования вызов connect выполняется толь- |
|
ко в том случае, если какая-либо ошибка не дает возможности |
||
WSAConnect |
||
приступить к операции установления связи. Также без блокирова- |
||
|
ния функция connect выполняется при использовании UDP, пото- |
|
|
му что в данном случае она только устанавливает фильтр для ад- |
|
|
ресов |
|
recv, WSARecv, |
Данные не были получены (входной буфер пуст). Проверьте снова |
|
recvfrom и |
позже. |
|
WSARecvFrom |
||
|
||
send, WSASend, |
Нет места в выходном системном буфере отсылаемых данных, |
|
sendto и |
чтоб скопировать туда переданные данные. Пробуйте снова поз- |
|
WSASendTo |
же. |
Если условия, при которых эти функции выполняются без блокирования, выполнены, то их поведение в блокирующем и неблокирующем режимах идентично. Если же выполнение операции без блокирования невозможно, функции возвращают результат, указывающий на ошибку. Чтобы понять, произошла ли ошибка из-за необходимости блокирования или из-за чего-либо еще, программа должна вызвать функцию
WSAGetLastError. Если она вернет WSAEWOULDBLOCK, значит, никакой ошибки не было, но выполнение операции без блокирования невозможно. Закрывать сокет и создавать новый после WSAEWOULDBLOCK не нужно, т. к. ошибки не было, и связь (в случае TCP) остается неразорванной.
Следует отметить, что при нулевом выходном буфере сокета (т. е. когда функция send передаст данные напрямую в сеть) и при большом объеме информации функция send может выполняться достаточно долго, т. к. эти данные отправляются по частям, и на каждую часть в рамках протокола TCP получаются подтверждения. Однако, эта задержка не считается блокированием, и в данном случае send будет одинаково себя вести с бло-
64
кирующими и неблокирующими сокетами, т. е. вернет управление программе лишь после того, как все данные окажутся в сети [13].
Когда большинство неблокирующих вызовов функции терпят неудачу с ошибкой WSAEWOULDBLOCK, необходимо проверять все коды возвратов и быть готовым к неудачному вызову в любое время. Многие программисты совершают большую ошибку, постоянно вызывая функцию, пока она не вернет удачный код возврата. Например, постоянный вызов recv в цикле в ожидании прочтения 100 байт данных ничем не лучше чем вызов recv в блокирующем режиме с параметром MSG_PEEK. Winsock- модели ввода-вывода могут помочь приложению определить, когда сокет готов к чтению, либо передаче данных.
Каждый из режимов – блокирующий и неблокирующий – имеют свои недостатки и преимущества. Блокирующие сокеты более легки в использовании с концептуальной точки зрения, но есть затруднения в управлении большого количества соединений или, когда передаются данные разных объемов и в разные периоды времени. С другой стороны неблокирующие сокеты более сложны, так как существует необходимость в написание более сложного кода для управления возможностью приема кодов возврата типа WSAEWOULDBLOCK при каждом вызове Winsock API- функций. Сокетные модели ввода-вывода помогают приложению справится с управлением передачей данных на одном или более соединений одновременно асинхронным способом [14].
7.4.SELECT (ВЫБОР)
Эта модель обеспечивает более контролируемый способ блокирования. Хотя она позволяет работать и с блокирующими socket’ами, но рассмотрим неблокирующий режим [12]. Принцип этой модели станет понятным, если взглянуть на иллюстрацию (рис. 14):
65
Рис. 14. Select
Диалог программы и WinSock будет следующим:
Программа: «Отправь-ка эти данные» WinSock: «Не могу сделать это сейчас»
Программа: «Хорошо, сообщи, когда будет наилучший момент, чтоб повторить попытку»
WinSock: «Конечно, подожди немного»
…
…
«Пробуй снова!» Программа: «Отправь-ка эти данные»
WinSock: «Сделано!»
Эта модель выглядит подобно блокирующему socket’у, потому что select действительно блокирует. Первый вызов пытается выполнить WinSock-операцию, которая заблокировала бы выполнение основного процесса, но функция не может быть выполнена и завершается неудачей. При этом управление передается основному потоку программы, который, в свою очередь, вызывает метод select, т. е. программа обращается к модели, чтобы определить подходящее время для повторной попытки. Она будет ждать наилучшего момента, чтобы повторить WinSock-функцию.
Хотя данная модель блокирует, но используют ее для неблокирующих socket’ов. Это связано с тем, что этот способ может «ждать» при многократных событиях.
Ниже приведен прототип метода select:
select (nfds:DWORD, readfds:DWORD, writefds:DWORD, exceptfds:DWORD, timeout:DWORD)
66
Select определяет статус одного или нескольких socket’ов, предоставляя синхронизацию ввода/вывода, если это необходимо. Первый параметр (nfds) игнорируется, последний параметр (timeout) используется для определения оптимального времени «ожидания» функции. Остальные параметры определяют набор socket’ов:
readfds – набор socket’ов, которые будут проверены на возможность чтения;
writefds – набор socket’ов, которые будут проверены на возможность записи;
exceptfds – набор socket’ов, которые будут проверены на наличие ошибок.
«Возможность чтения» значит, что данные прибыли на socket, и чтение после select’а аналогично получению данных. «Возможность записи» значит, что сейчас подходящее время для передачи данных, т. к. получатель, возможно, готов принять их. Exceptfds используется, чтобы поймать ошибки из неблокирующих соединений.
Для функций accept, recv и send ошибка WSAEWOULDBLOCK озна-
чает, что операцию следует повторить через некоторое время, и, возможно, в следующий раз она не потребует блокирования и будет выполнена. Функция connect в этом случае начинает фоновую работу по установлению соединения. О завершении этой работы можно судить по готовности сокета, которая проверяется с помощью функции select [14].
Листинг 7.
Установление связи при использовании неблокирующего сокета 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 ... //создаем объект исключения
67
end; //Полезная работа
…
…
FD_ZERO(SetW); //инициализация множества сокетов, очистка от мусора
FD_SET(S, SetW); //добавляем в множество сокет S FD_ZERO(SetE);
FD_SET(S, SetE);
select(0, nil, @SetW, @SetE, nil);
if FD_ISSET(S, SetW) then //есть ли сокет S в множестве SetW?
//Готов. Connect выполнен успешно else if FD_ISSET(S, SetE) then
//Готов. Соединиться не удалось else
//Произошла еще какая-то ошибка
Сокет, входящий в множество SetW (возможность записи), будет считаться готовым, если он соединен, а в его выходном буфере есть место. Сокет, входящий в множество SetE (наличие ошибки), будет считаться готовым, если попытка соединения не удалась. До тех пор, пока попытка соединения не завершилась (успехом или неудачей), ни одно из этих условий готовности не будет выполнено. В данном случае select завершит работу только после того, как будет выполнена попытка соединения, и о результатах этой попытки можно будет судить по тому, в какое из множеств попал сокет S: SetW – готов и соединен, SetE – готов, но не соединен.
Из приведенного примера не видно, какие преимущества дает неблокирующий сокет по сравнению с блокирующим. Кажется, что проще вызвать connect в блокирующем режиме, дождаться результата и только потом перевести сокет в неблокирующий режим. Во многих случаях это действительно может оказаться удобнее. Преимущества же соединения в неблокирующем режиме (как в листинге) связаны с тем, что между вызовами connect и select программа может выполнить какую-либо полезную работу, а в случае блокирующего сокета программа будет вынуждена сначала дождаться завершения работы connect и только потом сделать что-то полезное.
Функция send для неблокирующего сокета также имеет некоторое специфическое поведение. Оно проявляются, когда свободное место в выходном буфере есть, но его недостаточно для хранения данных, которые программа пытается отправить с помощью этой функции. В этом случае
68
функция send может скопировать в выходной буфер только объем данных, для которого хватает места. При этом она вернет значение, равное этому объему (оно будет меньше, чем значение параметра len, заданного программой). Оставшиеся данные программа должна отправить позже, вызвав еще раз функцию send. Такое поведение функции send характерно только при использовании TCP. В случае UDP дейтаграммы никогда не разделяются на части, и если в выходном буфере не хватает места для всей дейтаграммы, то функция send возвращает ошибку, a WSAGetLastError – WSAEWOULDBLOCK.
Хотя спецификация допускает частичное копирование функцией send данных в буфер сокета, на практике такое поведение пока не зафиксировано: все эксперименты показали, что функция send всегда либо копирует данные целиком, расширяя при необходимости буфер, либо дает ошибку WSAEWOULDBLOCK. Тем не менее, при написании программ следует учитывать возможность частичного копирования, т. к. оно может появиться в тех условиях или в тех реализациях библиотеки сокетов, которые не были проверены.
7.5.WSAASYNCSELECT
Большинство оконных программ используют специальные диалоговые окна, что бы получить информацию от пользователя или отправить ему. WinSock обеспечивает способ уведомления о сетевых событиях с обработкой сообщений Windows. Функция WSAAsyncSelect позволяет зарегистрировать уведомление для определенного сетевого события в виде привычного сообщения Windows [12].
Рис. 15. WSAAsyncSelect
69
Допустим, что первое сообщение хочет отправить какие-то данные socket’у, используя send. Так как socket неблокирующий, WinSock-функция будет завершена мгновенно. Функция может завершиться успешно, но тут это не будем рассматривать. Необходимо настроить WSAAsyncSelect таким образом, чтоб функция сообщила о событии FD_WRITE (готовность к записи). В итоге будет получено сообщение от WinSock, о том, что данное событие произошло: это означает что-то типа «Я готово, попробуй переслать свои данные». После этого в обработчике сообщения программа пытается переслать данные, и эта попытка успешно завершается.
Диалог между программой и WinSock подобен модели select, различие лишь в методе уведомления: оконное сообщение вместо синхронного вызова select’а. В то время как select блокирует основной процесс, ожидая пока произойдет событие, программа, использующая WSAAsyncSelect, может продолжить обработку сообщений Windows до тех пор, пока не происходит никаких событий:
Программа настраивается для уведомления о сетевых событиях через оконные сообщения
Программа: «Отправь-ка эти данные» WinSock: «Не могу сделать это сейчас» Программа обрабатывает некоторое сообщение Программа обрабатывает другое сообщение
Программа получает уведомляющее сообщение от WinSock Программа: «Отправь-ка эти данные»
WinSock: «Сделано! »
WSAAsyncSelect обеспечивает более «Windows’овский» способ уведомления, довольно простой в использовании. Для серверов с низкой пропускной способностью (меньше 1000 соединений) этот способ вполне эффективен. Недостатком является то, что оконные сообщения, сами по себе, не очень быстрые, а так же в том, что для использования этой модели требуются окна (т. е. программа должна быть GUI (graphical user interface)).
Функция select введена в библиотеку WinSock для совместимости с аналогичными библиотеками других платформ. Для программирования в Windows более мощной является функция WSAAsyncSelect.
Функция выглядит следующим образом:
int WSAAsyncSelect (SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);
Рассмотрим каждый параметр:
s – сокет, события которого необходимо ловить;
hWnd – окно, которому будут посылаться события при возникновении сетевых сообщений; именно у этого окна (или родительского) должна
70
быть функция в следующнем ниже примере WndProc, которая будет получать сообщения;
wMsg – сообщение, которое будет отсылаться окну, по его типу можно определить, что это событие сети;
lEvent – битовая маска сетевых событий, которые нас интересуют, этот параметр может принимать любую комбинацию из следующих значений:
FD_READ — готовность к чтению;
FD_WRITE — готовность к записи; FD_OOB — получение срочных данных; FD_ACCEPT — подключение клиентов; FD_CONNECT — соединение с сервером; FD_CLOSE — закрытие соединения;
FD_QOS — изменения сервиса QoS (Quality of Service);
FD_GROUP_QOS — изменение группы QoS.
Если функция отработала успешно, то она вернет значение больше нуля, если произошла ошибка – SOCKET_ERROR.
Функция автоматически переводит сокет в неблокирующий режим, и нет смысла вызывать функцию ioctlsocket.
Вот простой пример вызова WSAAsyncSelect: WSAAsyncSelect(s, hWnd, wMsg, FD_READ|FD_WRITE);
После выполнения этой строчки кода окно hWnd будет получать событие wMsg каждый раз, когда сокет s будет готов принимать и отправлять данные.
Чтобы отменить работу события, необходимо вызвать эту же функцию, правильно указав первые два параметра, но в качестве четвертого параметра указать 0 (содержимое третьего параметра не имеет значения, потому что событие не будет отправляться, и можно также указать ноль):
WSAAsyncSelect(s, hWnd, 0, 0).
Если нужно просто изменить типы событий, то можно вызвать функцию с новыми значениями четвертого параметра, таким образом, нет смысла сначала обнулять, а потом устанавливать заново.
Для каждого сокета можно назначить только одно сообщение (wMsg) на разные события 4го параметра. Т. е. нельзя по событию FD_READ окну посылать одно сообщение, а по FD_WRITE – другое.
Windows будет получать сообщения (в примере ниже – в функции WndProc) без необходимости замораживать работу программы для ожидания доступности сокетов.
