* При перепечатке материалов ссылка на www.SeoLiga.ru обязательна! RSS



Перекрытый ввод-вывод
19 марта 2009

Прежде чем переходить к рассмотрению перекрытого ввода-вывода, вспомним, какие модели ввода-вывода нам уже известны. Появление разных моделей связано с тем, что операции ввода-вывода не всегда могут быть выполнены немедленно.
Самая простая модель ввода-вывода - блокирующая. В блокирующем режиме, если операция не может быть выполнена немедленно, работа нити приостанавливается до тех пор, пока не возникнут условия для выполнения операции. В неблокирующей модели ввода-вывода операция, которая не может быть выполнена немедленно, завершается с ошибкой. И, наконец, в асинхронной модели ввода-вывода предусмотрена система уведомлений о том, что операция может быть выполнена немедленно.
При использовании перекрытого ввода-вывода операция, которая не может быть выполнена немедленно, формально завершается ошибкой - в этом заключается сходство перекрытого ввода-вывода и неблокирующего режима. Однако, в отличие от неблокирующего режима, при перекрытом вводе-выводе WinSock начинает выполнять операцию в фоновом режиме, и после её завершения начавшая операцию программа получает уведомление об успешно выполненной операции или о возникшей при её выполнении фатальной ошибке. Несколько операций ввода-вывода могут одновременно выполняться в фоновом режиме, как бы перекрывая работу инициировавшей их нити и друг друга. Именно поэтому данная модель получила название модели перекрытого ввода-вывода.
Перекрытый ввод-вывод существовал и в спецификации WinSock 1, но реализовывался только для линии NT. Специальных функций для перекрытого ввода-вывода в WinSock 1 не было, надо было использовать функции ReadFile и WriteFile, в которые вместо дескриптора файла подставлялся дескриптор сокета. В WinSock 2 появилась полноценная поддержка перекрытого ввода-вывода для всех версий Windows, а в спецификацию добавились новые функции для его реализации, избавившие от необходимости использования функций файлового ввода-вывода. В данной статье мы будем рассматривать перекрытый ввод-вывод только в спецификации WinSock 2, т.к. старый вариант из-за своих ограничений уже не имеет практического смысла.
Существуют два варианта уведомления о завершении операции перекрытого ввода-вывода: через событие и через процедуру завершения. Кроме того, программа может не дожидаться уведомления, а проверять состояние запроса перекрытого ввода-вывода с помощью функции WSAGetOverlappedResult (её мы рассмотрим позже).
Чтобы сокет мог использоваться в операциях перекрытого ввода-вывода, при его создании должен быть установлен флаг WSA_Flag_Overlapped (функция Socket неявно устанавливает этот флаг). Для выполнения операций перекрытого ввода-вывода сокет не нужно переводить в какой-либо особый режим, достаточно вместо обычных функций Send и Recv использовать WSARecv и WSASend. Сначала мы рассмотрим функцию WSARecv, которая имеет следующий прототип:
int WSARecv(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);

function WSARecv(
S:TSocket;
lpBuffers:PWSABuf;
dwBufferCount:DWORD;
var NumberOfBytesRecvd:DWORD;
var Flags:DWORD;
lpOverlapped:PWSAOverlapped;
lpCompletionRoutine:TWSAOverlappedCompletionRoutine):Integer;

Перекрытым вводом-выводом управляют два последних параметра функции, но функция WSARecv обладает и другими дополнительными по сравнению с функцией Recv возможностями, не связанными с перекрытым вводом-выводом. Если оба этих параметра равны nil, или сокет создан без указания флага WSA_Flag_Overlapped, функция работает в обычном блокирующем или неблокирующем режиме, который установлен для сокета. При этом её поведение отличается от поведения функции Recv только тремя незначительными аспектами: во-первых, вместо одного буфера ей можно передать несколько буферов, которые заполняются последовательно. Во-вторых, флаги передаются ей не как значение, а как параметр-переменная, и при некоторых условиях функция WSARecv может их изменять (при использовании TCP и UDP флаги никогда не меняются, поэтому мы не будем рассматривать здесь эту возможность). В-третьих, при успешном завершении функция WSARecv возвращает не количество прочитанных байт, а ноль, а количество прочитанных байт возвращается через параметр lpNumberOfBytesRecvd.
Буферы, в которые нужно поместить данные, передаются функции WSARecv через параметр lpBuffers. Он содержит указатель на начало массива структур TWSABuf, а параметр dwBufferCount - количество элементов в этом массиве. Выше мы знакомились со структурой TWSABuf: она содержит указатель на начало буфера и его размер. Соответственно, массив таких структур определяет набор буферов. При чтении данных заполнение буферов начинается с первого буфера в массиве lpBuffers, затем, если в нём не хватает места, используется второй буфер и т.д. Функция не переходит к следующему буферу, пока не заполнит предыдущий до последнего байта. Таким образом, данные, получаемые с помощью функции WSARecv, могут быть помещены в несколько несвязных областей памяти, что иногда бывает удобно, если принимаемые сообщения имеют строго определённый формат с фиксированными размерами компонентов пакета: в этом случае можно каждый компонент поместить в свой независимый буфер.
Теперь переходим непосредственно к рассмотрению перекрытого ввода-вывода с использованием событий. Для использования этого режима при вызове функции WSARecv параметр lpCompletionRoutine должен быть равен nil, а через параметр lpOverlapped передаётся указатель на структуру TWSAOverlapped, которая определена следующим образом:
typedef struct _WSAOVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

type PWSAOverlapped=^TWSAOverlapped;
TWSAOverlapped=packed record
Internal,InternalHigh,Offet,OffsetHigh:DWORD;
hEvent:TWSAEvent;
end;

Поля Internal, InternalHigh, Offset и OffsetHigh предназначены для внутреннего использования системой, программа не должна выполнять никаких действий с ними. Поле hEvent задаёт событие, которое будет взведено при завершении операции перекрытого ввода-вывода. Если на момент вызова функции WSARecv данные в буфере сокета отсутствуют, она вернёт значение Socket_Error, а функция WSAGetLastError - WSA_IO_Pending (997). Это значит, что операция начала выполняться в фоновом режиме. В этом случае функция WSARecv не изменяет значения параметров NumberOfBytesRecvd и Flag. Поля структуры TWSAOverlapped при этом также модифицируются, и эта структура должна быть сохранена программой в неприкосновенности до окончания операции перекрытого ввода-вывода без изменений. После окончания операции будет взведено событие, указанное в поле hEvent параметра lpOverlapped. При необходимости программа может дождаться этого взведения с помощью функции WSAWaitForMultipleEvents.
Как только запрос будет выполнен, в буферах, переданных через параметр lpBuffers, оказываются принятые данные. Но знания одного только факта, что запрос выполнен, недостаточно, чтобы этими данными воспользоваться, потому что, во-первых, неизвестен размер этих данных, а во-вторых, неизвестно, успешно ли завершена операция перекрытого ввода-вывода. Для получения недостающей информации служит функция WSAGetOverlappedResult, имеющая следующий прототип:
BOOL WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags);

function WSAGetOverlappedResult(
S:TSocket;
lpOverlapped:PWSAOverlapped;
var cbTransfer:DWORD;
fWait:BOOL;
var Flags:DWORD):BOOL;

Параметры S и lpOverlapped функции WSAGetOverlappedResult определяют сокет и операцию перекрытого ввода-вывода, информацию о которой требуется получить. Их значения должны совпадать со значениями соответствующих параметров, переданных функции WSARecv. Через параметр cbTransfer возвращается количество полученных байт, а через параметр Flags возвращаются флаги (напомню, что при использовании TCP и UDP флаги не модифицируются, и выходное значение параметра Flags будет равно входному значению параметра Flags функции WSARecv).
Допускается вызов функции WSAGetOverlappedResult до того, как операция перекрытого ввода-вывода будет завершена. В этом случае поведение функции зависит от параметра fWait. Если он равен True, функция переводит нить в состояние ожидания до тех пор, пока операция не будет завершена. Если он равен False, функция завершается немедленно с ошибкой WSA_IO_Incomplete (996).
Функция WSAGetOverlappedResult возвращает True, если операция перекрытого ввода-вывода успешно завершена, и False, если произошли какие-то ошибки. Ошибка может возникнуть в одном из трёх случаев:
1. Операция перекрытого ввода-вывода ещё не завершена, а параметр fWait равен False.
2. Операция перекрытого ввода-вывода завершилась с ошибкой (например, из-за разрыва связи).
3. Параметры, переданные функции WSAGetOverlappedResult, имеют некорректные значения.
Точную причину, по которой функция вернула False, можно установить стандартным образом - по коду ошибки, возвращаемому функцией WSAGetLastError.
В принципе, программа может вообще не использовать события для отслеживания завершения операции ввода-вывода, а вызывать вместо этого время от времени функцию WSAGetOverlappedResul в удобные для себя моменты. При этом при вызове функции WSARecv можно указать нулевое значение события hEvent. Но следует иметь ввиду, что при вызове функции WSAGetOverlappedResult с параметром fWait, равным True, указанное событие используется для ожидания завершения операции, и если событие не задано, возникнет ошибка. Таким образом, если событие не используется, функция WSAGetOverlappedResult не может вызываться в режиме ожидания.
Отдельно рассмотрим ситуацию, когда на момент вызова функции WSARecv с ненулевым параметром lpOverlapped во входном буфере сокета есть данные. В этом случае функция отработает так же, как и в неперекрытом режиме, т.е. изменит значения параметров NumberOfBytesRecvd и Flags и вернёт ноль, свидетельствующий об успешном выполнении функции. Но при этом событие будет взведено, а в структуру lpOverlapped будет внесена вся необходимая информация. Благодаря этому последующие вызовы функций WSAWaitForMultipleEvents и WSAGetOverlappedResult будут выполняться корректно, т.е. таким образом, как если бы функция WSARecv завершилась с ошибкой WSA_IO_Pending, и сразу после этого в буфер сокета поступили данные. Это позволяет использовать один и тот же код для обработки результатов операций перекрытого ввода-вывода независимо от того, были ли в буфере сокета данные на момент начала операции или нет.
Новая операция перекрытого ввода-вывода может быть начата до того, как закончится предыдущая. Это может быть удобно при работе с несколькими сокетами: можно выполнять операции с ними параллельно в фоновом режиме, получая уведомления о завершении каждой из операций.
В MSDN'е мне не удалось найти информацию о том, что будет, если вызвать для сокета WSARecv повторно, до того как будет завершена предыдущая операция перекрытого чтения. Мои эксперименты показали, что в этом случае операции перекрытого чтения встают в очередь, т.е. первый полученный сокетом пакет приводит к завершению операции, начатой первой, второй пакет - операции, начатой второй, и т.д. Но я не знаю, документировано ли такое поведение операций перекрытого ввода-вывода где-либо, и поэтому рекомендую не предполагать, что они всегда будут выполняться таким образом.
В качестве примера использования перекрытого ввода-вывода рассмотрим ситуацию, когда программа начинает операцию чтения данных из сокета, время от времени проверяя статус операции. События в этом примере не используются, проверка осуществляется с помощью функции WSAGetOverlappedResult.
var S:TSocket;
Overlapped:TWSAOverlapped;
BufPtr:TWSABuf;
RecvBuf:array[1..100] of Char;
Cnt,Flags:Cardinal;
begin
// Инициализация WinSock, создание сокета S, привязка его к адресу
......
// Подготовка структуры, задающей буфер
BufPtr.Buf:=@RBuf;
BufPtr.Len:=SizeOf(RBuf);
// Подготовка структуры TWSAOverlapped
// Поля Internal, InternalHigh, Offset, OffsetHigh программа не устанавливает
Overlapped.hEvent:=0;
Flags:=0;
// Начало операции перекрытого получения данных
WSARecv(S,@BufPtr,1,Cnt,Flags,@Overlapped,nil);
while True do
begin
if WSAGetOverlappedResult(S,@Overlapped,Cnt,False,Flags) then
begin
// Данные получены, находятся в RecvBuf, обрабатываем
......
// Выходим из цикла
Break
end
else if WSAGetLastError<>WSA_IO_Incomplete then
begin
// Произошла ошибка, анализируем её
......
// Выходим из цикла
Break
end
else
begin
// Операция чтения не завершена
// Занимаемся другими действиями
end
end;

Теперь перейдём к рассмотрению перекрытого ввода-вывода с использованием процедур завершения. Для этого при вызове функции WSARecv нужно задать указатель на процедуру завершения, описанную в программе. Процедура завершения должна иметь следующий прототип:
void CALLBACK CompletionROUTINE(
DWORD dwError,
DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped,
DWORD dwFlags);

type TWSAOverlappedCompletionRoutine=procedure(
dwError:DWORD;
cdTransferred:DWORD;
lpOverlapped:PWSAOverlapped;
dwFlags:DWORD);stdcall;

При использовании процедур завершения в функцию WSARecv также нужно передавать указатель на структуру TWSAOverlapped через параметр lpOverlapped, но значение поля hEvent этой структуры игнорируется. Вместо взведения события при завершении операции будет вызвана процедура, указанная в качестве параметра функции WSARecv. Указатель структуру, заданный при вызове WSARecv, передаётся в процедуру завершения через параметр lpOverlapped. Смысл остальных параметров очевиден: dwError - это код ошибки (или ноль, если операция завершена успешно), cbTransferred - количество полученных байт (само полученное сообщение копируется в буфер, указанный функции WSARecv), а dwFlags - флаги.
Процедура завершения всегда выполняется в той нити, которая инициировала начало операции перекрытого ввода-вывода. Но система не может прерывать нить для выполнения процедуры завершения в любой удобный ей момент - нить должна перейти в состояние ожидания. В это состояние нить можно перевести, например, с помощью функции SleepEx, имеющей следующий прототип:
function SleepEx(dwMilliseconds:DWORD;bAlertable:BOOL):DWORD;

Функция SleepEx является частью стандартного API системы и импортируется модулем Windows. Она переводит нить в состояние ожидания. Параметр dwMilliseconds задаёт время ожидания в миллисекундах (или значение Infinite для бесконечного ожидания). Параметр bAlertable указывает, допустимо ли прерывание состояния ожидания для выполнения процедуры завершения. Если bAlertable равен False, функция SleepEx ведёт себя так же, как функция Sleep, т.е. просто приостанавливает работу нити на заданное время. Если bAlertable равен True, нить может быть выедена системой из состояния ожидания раньше, чем истечёт заданное время, если возникнет необходимость выполнить процедуру завершения. О причине завершения ожидания программа может судить по результату, возвращаемому функцией SleepEx: ноль в случае завершения по таймауту и Wait_IO_Completion в случае завершения из-за выполнения процедуры завершения (в последнем случае сначала выполняется процедура завершения, а потом только происходит возврат из функции SleepEx). Если завершилось несколько операций перекрытого ввода-вывода, в результате выполнения SleepEx будут вызваны процедуры завершения для всех этих операций.
Существует также возможность ожидать выполнения процедуры завершения одновременно с ожиданием взведения событий с помощью функции WSAWaitForMultuipleEvents. Напомню, что у этой функции также есть параметр fAlertable. Если задать его равным True, то при необходимости выполнения процедуры завершения функция WSAWaitForMultipleEvents, подобно функции SleepEx, выполняет эту процедуру и возвращает Wait_IO_Completion.
Если программа выполняет одновременно несколько операций перекрытого ввода-вывода, встаёт вопрос, как при вызове процедуры завершения определить, какая из них завершилась. Для каждой такой операции должен быть создан уникальный экземпляр структуры TWSAOverlapped. Процедура завершения получает указатель на тот экземпляр, который использовался для начала завершившейся операции. Можно сравнить указатель с теми, которые использовались для запуска операций перекрытого ввода-вывода, и определить, какая из них завершилась. Это не всегда бывает удобно из-за необходимости где-то хранить список указателей, использовавшихся для операций перекрытого ввода-вывода. Существуют ещё два варианта решения этой проблемы. Первый вариант заключается в создании своей процедуры завершения для каждой из выполняющихся параллельно операций. Этот вариант приводит к получению громоздкого кода и может быть неудобен, если число одновременно выполняющихся операций заранее неизвестно. Его следует использовать только при одновременном выполнении разнородных опреаций, требующих разных алгоритмов при обработке их завершения. Другой вариант предлагается в MSDN'e. Так как при использовании процедур завершения значение поля hEvent структуры TWSAOverlapped игнорируется системой, программа может записать туда любое 32-битное значение и с его помощью определить, какая из операций завершена. В строго типизированном языке, каким является Паскаль, подобное смешение типа дескриптора и целого выглядит весьма непривлекательно, но, к сожалению, это лучшее из того, что нам предлагают разработчики WinSock API.
При использовании процедур завершения допустимо использование функции WSAGetOverlappedResult для определения статуса операции, но её параметр fWait обязательно должен быть равен False, потому что события, необходимые для выполнения ожидания, не взводятся, и попытка дождаться окончания операции может привести к блокировке работы нити.
В процедуре завершения допускается вызывать функции, начинающие новую операцию перекрытого ввода-вывода, в том числе и такую же операцию, которая только что завершена. Эта возможность используется в примере, приведённом ниже. Пример иллюстрирует работу клиента, который подключается к серверу и получает от него данные в режиме перекрытого ввода-вывода, выполняя параллельно какие-то другие действия.
var S:TSocket;
Overlapped:TWSAOverlapped;
BufPtr:TWSABuf;
RecvBuf:array[1..100] of Char;
Cnt,Flags:Cardinal;
Connected:Boolean;

procedure GetData(Err,Cnt:DWORD;OvPtr:PWSAOverlapped;Flags:DWORD);stdcall;
begin
if Err<>0 then
begin
// Произошла ошибка. Соединение надо устанавливать заново
CloseSocket(S);
Connected:=False
end
else
begin
// Получены данные, обрабатываем
......
// Запускаем новую операцию перекрытого чтения
Flags:=0;
WSARecv(S,@BufPtr,1,Cnt,Flags,OvPtr,GetData)
end
end;

procedure ProcessConnection;
begin
// Устанавливаем начальное состояние - сокет не соединён
Connected:=False;
// Задаём буфер
BufPtr.Buf:=@RecvBuf;
BufPtr.Len:=SizeOf(RecvBuf);
while True do
begin
if not Connected then
begin
Connected:=True;
// Создаём и подключаем сокет
S:=Socket(AF_Inet,Sock_Stream,0);
Connect(S,...);
// Запускаем первую для данного сокета операцию чтения
Flags:=0;
WSARecv(S,@BufPtr,1,Cnt,Flags,@Overlapped,GetData)
end;
// Позволяем системе выполнить процедуру завершения,
// если это необходимо
SleepEx(0,True);
// Выполняем какие-либо дополнительные действия
......
end
end;

Основной процедурой здесь является ProcessConnection. Эта процедура в бесконечном цикле устанавливает соединение, если оно не установлено, даёт системе выполнить процедуру завершения, если это требуется, и выполняет какие-либо иные действия, не связанные с получением данных от сокета. Процедура завершения GetData получает и обрабатывает данные, а если произошла ошибка, закрывает сокет и сбрасывает флаг Connected, что служит для процедуры ProcessConnection сигналом о необходимости установить соединение заново.
Достоинства и недостатки процедур завершения хорошо видны из этого примера. Получение и обработка данных выносится в отдельную процедуру, и это, с одной стороны, позволяет разгрузить основную процедуру, но, с другой стороны, заставляет использовать глобальные переменные для буфера и сокета.
Для протоколов, не поддерживающих соединение, существует другая функция для перекрытого получения данных - WSARecvFrom. Из названия очевидно, что она позволяет узнать адрес отправителя. Прототип функции WSARecvFrom таков:
int WSARecvFrom(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
struct sockaddr FAR *lpFrom,
LPINT lpFromlen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);

function WSARecvFrom(
S:TSocket;
lpBuffers:PWSABuf;
dwBufferCount:DWORD;
var NumberOfBytesRecvd:DWORD;
var Flags:DWORD;
lpFrom:PSockAddr;
lpFromLen:PInteger;
lpOverlapped:PWSAOverlapped;
lpCompletionRoutine:TWSAOverlappedCompletionRoutine):Integer;

Параметры lpFrom и lpFromLen этой функции, служащие для получения адреса отправителя, эквиваленты соответствующим параметрам функции RecvFrom, которую мы разбирали в предыдущей статье. В остальном WSARecvFrom ведёт себя так же, как WSARecv, поэтому мы не будем останавливаться на ней.
Для отправки данных в режиме перекрытого ввода-вывода существуют функции WSASend и WSASendTo, имеющие следующие прототипы:
int WSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);

function WSASend(
S:TSocket;
lpBuffers:PWSABuf;
dwBufferCount:DWORD;
var NumberOfBytesRecvd:DWORD;
Flags:DWORD;
lpOverlapped:PWSAOverlapped;
lpCompletionRoutine:TWSAOverlappedCompletionRoutine):Integer;

int WSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
const struct sockaddr FAR *lpTo,
int iToLen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);

function WSASendTo(
S:TSocket;
lpBuffers:PWSABuf;
dwBufferCount:DWORD;
var NumberOfBytesRecvd:DWORD;
Flags:DWORD;
var AddrTo:TSockAddr;
ToLen:Integer;
lpOverlapped:PWSAOverlapped;
lpCompletionRoutine:TWSAOverlappedCompletionRoutine):Integer;

Если вы разобрались с функциями WSARecv, Send и SendTo, то смысл параметров функций WSASend и WSASendTo должен быть вам очевиден, поэтому подробно разбирать мы их не будем. Но отметим, что флаги передаются по значению, и функции не могут изменять их.
Потребность в использовании перекрытого ввода-вывода при отправке данных возникает достаточно редко. Но функции WSASend/WSASendTo могут оказаться удобны при подготовке многокомпонентных пакетов, которые, например, имеют фиксированный заголовок и финальную часть. Для таких пакетов можно один раз подготовить буферы с заголовком и с финальной частью и, пользуясь возможностью отправки данных из несвязных буферов, при отправке каждого пакета менять только его среднюю часть.


Теги: asus socket, IP, borland delphi, User Datagram Protocol, Unix, SQL-сервер, socket, Создание сокета, сокет, sockets Borland Delphi

Статьи по теме:

Добавление текста и полей данных
Проектирование структуры реляционного хранилища данных
Упрощение интерфейса
OnLocaleError
Панель Spacing
Чтение сообщений
Свойство PrintIfEmpty
Компонент TQRSysData
Базы данных, СУБД, дифференциальные файлы
Фильтрация и сортировка данных
Разрешение и запрет
Свойство CurrentColumn
OpenMode
Использование QRPrinter
Панель Debug
| Borland Delphi | Alex |
 


Пн Вт Ср Чт Пт Сб Вс
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31


     



Rambler's Top100

Данный сайт или домен продается ICQ: 403-353-727

© 2009 Seoliga.ru | Borland Delphi | Перекрытый ввод-вывод. Регион сайта: Москва и Санкт-Петербург