Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование в сетях Windows

.pdf
Скачиваний:
538
Добавлен:
11.03.2015
Размер:
3.02 Mб
Скачать

228

ЧАСТЬ II Интерфейс прикладного программирования Winsock

Листинг 8-7. (продолжение)

/I Обработка полученных данных, // содержащихся в OataBuf

 

// Шаг 8

 

1

// Асинхронная отправка нового

запроса WSARecvO на сокет

 

 

л

Flags = О,

 

6 !

ZeroMemoryC&AcceptOverlapped,

 

 

sizeof(WSAOVERLAPPED)),

 

 

AcceptOverlapped hEvent = EventArray[Index -

 

WSA_WAIT_EVENT_O],

 

 

DataBuf len = DATA.BUFSIZE,

 

 

DataBuf buf = Buffer,

 

 

WSARecv(AcceptSocket, &DataBuf,

1,

&ReovBytes, &Flags, &AcceptOverlapped, NULL),

В Windows NT и 2000 модель перекрытого ввода-вывода позволяет приложениям принимать соединения в манере перекрытия, вызывая функцию AcceptEx на слушающем сокете Эта функция — специальное расширение Winsock 1 1 и доступна в файле Mswsock h из библиотеки Mswsock lib Первоначально она предназначалась для перекрытого ввода-вывода в Windows NT и 2000, но работает и с Winsock 2 Функция AcceptEx определена так

BOOL AcceptEx (

 

 

SOCKET sListenSocket,

 

 

SOCKET sAcceptSocket,

 

 

PVOID

IpOutputBuffer,

 

 

DWORD

dwReceiveDataLength,

 

 

DWORD

dwLocalAddressLength,

"

«00

DWORD dwRemoteAddressLength,

'

 

LPDWORD lpdwBytesReceived,

LPOVERLAPPED lpOverlapped

Параметр sListenSocket обозначает слушающий сокет, a sAcceptSocket — сокет, принимающий входящее соединение Функция AcceptEx отличается от accept вы должны передать ей уже готовый принимающий сокет, а не создавать его функцией Для создания сокета вызовите функцию socket или WSASocket Параметр IpOutputBuffer — специальный буфер, принимающий три блока данных локальный адрес сервера, удаленный адрес клиента и первый блок данных на новом соединении Параметр dwReceiveDataLength указывает количество байт в IpOutputBuffer, используемых для приема данных Если

Г Л А ВА 8 Ввод вывод в Winsock

229

он равен 0, при установлении соединения данные не принимаются Параметры dwLocalAddressLength и duRemoteAddressLength указывают, сколько байт в lpOutputBuffer резервируется для хранения локального и удаленного адресов при принятии соединения Размеры буферов должны быть минимум на 16 байт больше, чем максимальная длина адреса в используемом транспортном протоколе Например, в TCP/IP размер буфера равен размеру структуры SOCKADDRJN + 16 байт

По адресу ipdwBytesRecewed записывается количество принятых байт данных, если операция завершилась синхронно Если же функция AcceptEx возвращает ERRORJO PENDING, данные по этому указателю не обновляются и количество принятых байт нужно получать через механизм уведомления о завершении Последний параметр — ipOverlapped, это структура OVERLAPPED, позволяющая использовать AcceptEx для асинхронного ввода-вывода Как упоминалось ранее, данная функция работает с уведомлениями о событиях только в приложениях с перекрытым вводом-выводом, так как в ней нет параметра процедуры завершения

Функция GetAcceptExSockaddrs из расширения Winsock выделяет локальный и удаленный адреса из параметра IpOutputBujfer

VOIDGetAcceptExSockaddrs(

PVOIDlpOutputBuffer,

DWORDdwReceiveDataLength,

ЛDWORD dwLocalAddressLength,

°DWORD dwRemoteAddressLength, "° LPSOCKADDR «LocalSockaddr,

ыь LPINT LocalSockaddrLength, '!< LPSOCKADDR «RemoteSockaddr,

LPINTRemoteSockaddrLength

),

Параметр lpOutputBuffer — указатель lpOutputBuffer, возвращенный функцией AcceptEx Параметры dwReceiveDataLength, dwLocalAddressLength и dwRemoteAddressLength должны иметь те же значения, что и dwReceiveDataLength, dwLocalAddressLength и dwRemoteAddressLength, переданные в вызове AcceptEx Параметры LocalSockaddr и RemoteSockaddr — указатели на структуры SOCKADDR с информацией о локальном и удаленном адресах В них хранится смещение от адреса исходного параметра lpOutputBuffer Это облегчает обращение к элементам структур SOCKADDR, содержащихся в lpOutputBuffer Параметры LocalSockaddrLength и RemoteSockaddrLength задают размер локального и удаленного адресов

Процедуры завершения

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

Для использования процедуры завершения приложение должно указать процедуру завершения, а также структуру WSAOVERLAPPED функции вводавывода Winsock в качестве параметра (как было описано ранее). У процедуры завершения следующий прототип:

void CALLBACK CompletionROUTINE( DWORD dwError,

DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags

);

Когда завершается запрос перекрытого ввода-вывода, параметры функции завершения содержат следующую информацию-.

dwError — статус завершения для перекрытой операции, на которую указываетlpOverlapped;

Ш cbTransferred — количество байт, перемещенных при этой операции;

lpOverlapped — структуруWSAOVERLAPPED, переданную в исходный вызов функции ввода-вывода;

Ш duFlags не используется и равен 0.

Основное отличие перекрытого запроса с процедурой завершения от запроса с объектом события — поле hEvent структуры WSAOVERLAPPED не используется, то есть нельзя связать объект события с запросом ввода-выво- да. Сделав перекрытый запрос с процедурой завершения, вызывающий поток должен обязательно выполнить процедуру завершения по окончании запроса. Для этого нужно перевести поток в состояние ожидания (alertable wait state) и выполнить процедуру завершения позже, по окончании операции ввода-вывода.

Для перевода потока в состояние ожидания используйте функцию WSAWaitForMultipleEvents.Тонкостьвтом,чтофункцииWSAWaitForMultipleEvents

нужен свободный объект события. Если приложение работает только в модели перекрытого ввода-вывода с процедурами завершения, таких событий может и не быть. В качестве альтернативы можно задействовать \С^п32-функ- цию SleepEx для перевода потока в состояние ожидания. Конечно, также можно создать фиктивный, ни с чем не связанный объект события. Если вызывающий поток всегда занят и не находится в состоянии ожидания, процедура завершения никогда не будет вызвана.

ФункцияWSAWaitForMultipleEventsтакжеможетпереводитьпотоквсостояние ожидания и вызывать процедуру завершения ввода-вывода, если параметр fAlertable равен TRUE. Когда запрос ввода-вывода заканчивается через процедурузавершения,возвращатьсябудетWSA_IO_COMPLET1ON,анеиндекс в массиве событий. Функция SleepEx ведет себя так же, как и функция WSAWaitForMultipleEvents, кроме того, что ей не нужны объекты события:

DWORD SleepEx(

 

 

 

DWORD

dwMilliseconds,

 

 

 

BOOL

bAlertable

.

i i

, ,j

ГЛАВА 8 Ввод-вывод в Winsock

231

Параметр dwMilliseconds определяет, сколько миллисекунд функция SleepEx будет ждать. Если он равен INFINITE, SleepEx будет ждать бесконечно. Параметр bAlertable задает, как будет выполняться процедура завершения. Если он равен FALSE и происходит обратный вызов по окончании ввода-вывода, процедура завершения не выполняется и операция не завершится, пока не истечет период ожидания, заданный в dwMilliseconds. Если bAlertable равен TRUE, выполняется процедура завершения и функция SleepEx возвращает

WAITJOJ0OMPLETION.

В листинге 8-8 показана структура простого сервера, управляющего одним сокетом с использованием процедур завершения. Выделим следующие этапы.

1.Создание сокета и прослушивание соединений на заданном порте.

2.Прием входящего соединения.

3.Создание структуры WSAOVERLAPPED для установленного соединения.

4.Отправка асинхронного запроса WSARecv на сокет с заданием структуры WSAOVERLAPPED и процедуры завершения в качестве параметра.

5.Вызов функции WSAWaitForMultipleEvents с параметромfAlertable, равным

TRUE, и ожидание завершения запроса перекрытого ввода-вывода. Когда запрос завершается, автоматически выполняется процедура завершения

ифункция WSAWaitForMultipleEvents возвращает WSAJOjOOMPLETION. В

процедуре завершения нужно отправить новый запрос перекрытого вво- да-вывода WSARecv с процедурой завершения.

6.Проверка, что функция WSAWaitForMultipleEvents возвращает WSAJO_ COMPLETION.

7.Повтор шагов 5 и 6.

Листинг 8-8. Перекрытый ввод-вывод с использованием процедур завершения

SOCKETAcceptSocket;

WSABUF DataBuf;

МШфЬИН

void main(void)

 

{

;(

WSAOVERLAPPED Overlapped;

 

//Шаг 1:

//Запуск Winsock, настройка сокета для прослушивания

//Шаг2:

//

Прием

соединения

 

 

 

AcceptSocket = accept(ListenSocket, NULL, NULL);

Л*

«f

// Шаг 3:

 

»<•

ЩП

 

II

Приняв

соединение, начинаем

 

\в9ы

.ее у

 

 

 

о

yiodatj

\ см.след.смр-

232

ЧАСТЬ II Интерфейс прикладного программирования Winsock

Листинг 8-8. (продолжение)

X

1 Э 1 >

II обработку перекрытого ввода-вывода

//с использованием процедур завершения.

//Для этого в первую очередь

//делаем запрос WSARecv().

Flags = 0;

ZeroMemory(&0verlapped, sizeof(WSAOVERLAPPED));

о

DataBuf.len = DATA_BUFSIZE; DataBuf.buf = Buffer; //Шаг 4:

//Отправка асинхронного запроса WSARecv() на сокет

//со структурой WSAOVERLAPPED и описанной ниже

//процедурой завершения WorkerRoutine в качестве

//параметров.

if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, Overlapped, WorkerRoutine)

== SOCKET_ERROR)

{

if (WSAGetLastErrorO != WSA_IO_PENDING)

{

printf("WSARecv() failed with error Xd\n", WSAGetLastErrorO);

return;

//Так как функция WSAWaitForMultipleEventsO

//ожидает один или несколько объектов "событие",

//нужно создать фиктивный объект.

//В качестве альтернативы можно использовать

//функцию SleepEx().

EventArray[0] = WSACreateEventO;

while(TRUE)

{

// Шаг 5:

Index = WSAWaitForHultipleEvents(1, EventArray, FALSE, WSA_INFINITE, TRUE);

// Шаг 6:

if (Index == WAIT_IO_COMPLETI0N)

{

//Процедура завершения запроса перекрытого

//ввода-вывода закончила работу. Продолжаем

//работу с другими процедурами завершения.

Г Л А ВА 8 Ввод-вывод в Winsock

233

Листинг 8-8. (продолжение) break;

t "gW^HMI else}

//Произошла ошибка, нужно остановить обработку!

//Если мы также работали с объектом "событие",

//это может быть индекс в массиве событий,

return;

}

void CALLBACK WorkerRoutine(DWORD Error,

DWORD BytesTransferred, LPWSAOVERLAPPED Overlapped, DWORD InFlags)

'{

DWORD SendBytes, RecvBytes; DWORD Flags;

if (Error != 0 || BytesTransferred == 0)

{

//Или на сокете произошла ошибка,

//или сокет закрыт партнером closesocket(AcceptSocket); return;

//Запрос перекрытого ввода-вывода WSARecvQ завершился

//успешно. Теперь можно обработать принятые

//данные, содержащиеся в DataBuf. После этого

//нужно отправить другой запрос перекрытого ввода-вывода

//WSARecvO или WSASend(). Для простоты отправим

//запрос WSARecv().

Flags = 0;

ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));

'DataBuf.len = DATA.BUFSIZE;

DataBuf.buf = Buffer;

if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &Overlapped, WorkerRoutine)

== S0CKET_ERR0R)

if (WSAGetLastErrorO != WSA_10_PENDING )

см.след.стр.

2 34

ЧАСТЬ II Интерфейс прикладного программирования Winsock

Листинг 8-8. (продолжение)

{

printf("WSARecv() failed with error Xd\n", WSAGetLastErrorO);

return;

}

< t

kP

Модель портов завершения

Эта самая сложная модель ввода-вывода. Впрочем, она позволяет достичь наивысшего быстродействия, если приложение должно управлять многими сокетами одновременно. К сожалению, эта модель доступна только в Windows NT и 2000. Из-за сложности ее следует использовать, только если приложению необходимо управлять сотнями и тысячами сокетов одновременно и при этом нужно добиться хорошей масштабируемости при добавлении новых процессоров. Модель оптимальна только при высокоэффективном сервере под управлением Windows NT или 2000, обрабатывающем множество запросов ввода-вывода через сокеты (например, Web-сервера).

Модель портов завершения требует создать Win32-o6beKT порта завершения, который будет управлять запросами перекрытого ввода-вывода, используя указанное количество потоков для обработки завершения этих запросов. Заметим, что на самом деле порт завершения — это конструкция ввода-вы- вода из Win32, Windows NT и 2000, способная работать не только с сокетами. Впрочем, здесь мы опишем лишь преимущества этой модели при работе с описателями сокетов. Для начала функцией CreateloCompletionPort создадим объект порта завершения, который будет использоваться для управления запросами ввода-вывода для любого количества сокетов:

HANDLECreateIoCompletionPort( HANDLE FileHandle, HANDLEExistingCompletionPort, DWORDCompletionKey, DWORDNumberOfConcurrentThreads

);

Заметьте, функция в действительности используется в двух разных целях: создания порта завершения и привязки к нему описателя сокета.

При первоначальном создании порта единственный важный параметр — NumberOfConcurrentThreads, первые три параметра игнорируются. Он определяет количество потоков, которые могут одновременно выполняться на порте завершения. В идеале порт должен обслуживаться только одним потоком на каждом процессоре, чтобы избежать переключений контекста. Значение 0 разрешает выделить число потоков, равное числу процессоров в системе. Создать порт завершения можно так:

CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); sot

Г Л А ВА 8 Ввод-вывод в Winsock

235

При этом возвращается описатель порта завершения, используемый при привязке сокета.

Рабочие потоки и порты завершения

Теперь нужно связать с портом завершения описатели сокетов. Но прежде чем начинать собственно привязку, необходимо создать один или несколько рабочих потоков для обслуживания порта, когда на него отправляются запросы ввода-вывода сокетов. Но сколько именно потоков понадобится? Это весьма сложный вопрос, так как требуемое число потоков зависит от структуры приложения.

Важно понимать различие между количеством конкурентных потоков, задаваемых при вызове CreateloCompletionPort, и количеством создаваемых рабочих потоков — это не одно и то же. Ранее мы рекомендовали при вызове функции CreateloCompletionPort задавать один поток на процессор, чтобы предотвратить переключение контекста между потоками. Параметр NumberOfConcurrentThreads функции CreateloCompletionPort явно указывает системе разрешать только п потокам одновременно работать с портом завершения. Даже если будет создано более п рабочих потоков для порта завершения, только п смогут работать одновременно. (На самом деле, это значение может быть превышено на короткий промежуток времени, но система быстро сократит количество потоков до величины, указанной в вызове CreateloCompletionPort^)

Может показаться странным: зачем создавать рабочих потоков больше, чем указано в вызове CreateloCompletionPortl Дело в том, что если один из рабочих потоков приостанавливается (путем вызова функции Sleep или WaitForSingleObjecf), другой сможет работать с портом вместо него. Иными словами, всегда желательно иметь столько выполняемых потоков, сколько задано в вызове CreateloCompletionPort. Поэтому, если вы ожидаете, что какой-то поток будет блокирован, лучше создать больше рабочих потоков, чем указано в параметре CreateloCompletionPort в вызовеNumberOfConcurrentThreads.

Создав достаточное число рабочих потоков, начинайте собственно привязку описателей сокетов к порту. Вызовите функцию CreateloCompletionPort Для существующего порта и передайте в первых трех параметрах (FileHandle, ExistingCompletionPort и CompletionKey) информацию о сокете. Параметр

FileHandle — описатель сокета, который нужно связать с портом завершения,

ExistingCompletionPort определяет порт завершения, a CompletionKey данные описателя (per-handle data), которые можно связать с конкретным описателем сокета. Используя этот параметр, приложение может связать с сокетом любые данные. (Мы называем его данными описателя, потому что он представляет данные, связанные с описателем сокета.) Полезно использовать этот параметр, как указатель на структуру данных, содержащую описатель и Другую информацию о сокете, чтобы процедуры потока, обслуживающие порт завершения, могли ее получать.

Теперь приступим к созданию простого приложения. В листинге 8-9 показано, как разработать приложение эхо-сервера, используя модель портов завершения. Выделим следующие этапы:

236

ЧАСТЬ II Интерфейс прикладного программирования Wmsock

1. Создается порт завершения. Четвертый параметр равен О, что разрешает только одному рабочему потоку на процессор одновременно выполняться на порте завершения.

2 Выясняется, сколько процессоров в системе

3. Создаются рабочие потоки для обслуживания завершившихся запросов ввода-вывода порта завершения с использованием информации о процессорах, полученной на шаге 2 В нашем простом примере мы создаем один рабочий поток на процессор, так как не ожидаем приостановки работы потоков. При вызове функции CreateThread нужно указать рабочую процедуру, которую поток выполняет после создания. Действия, которые в ней должен выполнить поток, мы обсудим далее.

4 Готовится сокет для прослушивания порта 5150

5.Принимается входящее соединение функцией accept.

6.Создается структура данных описателя и в ней сохраняется описатель принятого сокета.

7.Возвращенный функцией accept новый описатель сокета связывается с портом завершения вызовом функции CreateloCompletionPort Структура данных описателя передается в параметре CompletionKey.

8.Начинается обработка ввода-вывода на принятом соединении. Один или несколько асинхронных запросов WSARecv или WSASend отправляются на сокет с использованием механизма перекрытого ввода-вывода. Когда эти запросы завершаются, рабочий поток обслуживает их и продолжает обрабатывать новые ( в рабочей процедуре на шаге 3).

9- Шаги 5-8 повторяются до окончания работы сервера.

Листинг 8-9. Настройка порта завершения

StartWinsock();

// Шаг 1:

 

 

 

// Создается

порт завершения

ввода-вывода

 

 

 

t

>

CompletionPort

= CreateIoCompletionPort(

 

INVALID_HANDLE_VALUE, NULL,

0, 0);

'Цо''

// Шаг 2:

//Выяснение количества процессоров в системе GetSystemlnfo(&SystemInfо);

//Шаг 3:

//Создание рабочих потоков для доступных процессоров в системе.

// В данном простейшем случае, мы создадим один рабочий поток

//для каждого процессора.

for(i = 0; 1 < Systemlnfо.dwNumberOfProcessors;

Г Л А ВА 8 Ввод-вывод в Winsock

237

Листинг 8-9. (продолжение)

{

HANDLE ThreadHandle;

//Создание рабочего потока сервера и передача

//порта завершения в качестве параметра.

//Примечание: процедура ServerWorkerThread

//не определена в этом листинге.

н

!l ThreadHandle = CreateThread(NULL, 0,

• ' ServerWorkerThread, CompletionPort,

0, &ThreadID);

n

. // Закрытие описателя потока CloseHandle(ThreadHandle);

//Шаг 4:

//Создание слушающего сокета

Listen = WSASocket(AF_INET, SOCK.STREAM, 0, NULL, 0,

WSA_FLAG_OVERLAPPED);

InternetAddr.sin_family = AF_INET; InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY); InternetAddr.sin_port = htons(5150);

bind(Listen, (PSOCKADDR) AlnternetAddr, sizeof(InternetAddr));

// Подготовка сокета для прослушивания

listen(Listen, 5);

while(TRUE)

{.

//Шаг 5:

// Прием соединений и их привязка к порту завершения

Accept = WSAAccept(Listen, NULL, NULL, NULL, 0);

//Шаг 6:

//Создание структуры данных описателя,

//которая будет связана с сокетом PerHandleData = (LPPER_HANDLE_DATA)

GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));

printf("Socket number Xd connected\n", Accept); PerHandleData->Socket = Accept;

см.след.стр.