Шаг 30 - Масштабируемые сетевые приложения на основе Socket

Это мой вольный перевод или трактовка, не знаю как лучше "Windows Sockets 2.0: Write Scalable Winsock Apps Using Completion Ports" Anthony Jones and Amol Deshpande из MSDN Magazine.

Писать сетевые приложения не сложно, но писать такие чтобы были маштабируемы трудновато. Перекрывающиеся(overlapped) "завершающие"(completion) порты позволяют создавать настоящие масштабируемые приложения для WinNT или Win2000. C помощью этих портов и Winsock 2.0 можно писать приложения которые бы работали с тысячами подключений.

И так, как говорилось выше написание сетевых приложений никогда не считалось лёгким делом. Хотя в действительности создать сокет, принять и послать данные особого труда не составит. Трудность состоит в написание приложений которые бы масштабировались от одного сокета до нескольких тысяч. В этой статье мы поговорим о написании таких приложений.

Механизм перекрывающихся операций I/O(ввода вывода) позволяет начать операцию и получить уведомлении о её окончании позже. Это особенно полезно для тех операций которые требуют много времени на выполнение. Поток, который инициировал операцию свободен и может делать что-то другое, пока выполняется операция. Только модель перекрывающегося I/O которая использует завершающие порты позволяет создавать такие приложения. WSAAsyncSelect и select эти функции пришли либо из Win3.1 или портированы из Unix(а его Microsoft не любят, так он не Windows) по их словам вроде как не созданы для создания масштабируемых приложений.

Завершающие порты это очередь в которую операционная система помещает извещения о окончании перекрывающихся операций I/O. Как только операция заканчивается, извещение посылается рабочему потоку который может обработать его. Сокет может быть связан с портом завершения в любой точке после создания(правда не уточнятся создания чего).

Обычно приложение создает несколько рабочих потоков, которые обрабатывают эти извещения. Число рабочих потоков зависит от потребности приложения. В идеальном случае один на каждый процессор(обработчик), но это не подразумевает, что ни один из потоков не должен выполнить блокирующую операцию ввода/вывода или ожидания. Каждый поток получает определённое количество процессорного времени(квант), если поток выполняет блокирующую операцию, то операционная система не ждёт пока закончится время выделенное нашему потоку, а отдаёт его другим потокам. И так первый поток не использовал свой квант времени, поэтому приложение должно иметь другие потоки готовые использовать оставшееся время. Использование завершающих портов это двух ступенчатый процесс. Для начала нужно создать наш порт, это показано ниже:

HANDLE    hIocp;
hIocp = CreateIoCompletionPort
(
	INVALID_HANDLE_VALUE,
	NULL,
	(ULONG_PTR)0,
	0
);
if (hIocp == NULL) 
{
    // Error
}

После того как порт создан, каждый порт желающий использовать его должен связать сокет с ним. Для того чтобы сделать это нам нужно пять вызвать CreateIoCompletionPort, на этот раз устанавливая первый параметр FileHandle, в хэндл сокета, а ExistingCompletionPort установить в хэндл порта, который мы только, что создали. Это показано ниже:

SOCKET    s;
s = socket(AF_INET, SOCK_STREAM, 0);
if (s == INVALID_SOCKET) 
{
	 // Error
	if (CreateIoCompletionPort((HANDLE)s,hIocp,(ULONG_PTR)0,0) == NULL)
	{
		// Error
	}
	....
}

Здесь сокет связан, с завершающим портом. Любая перекрывающаяся операция с этим сокетом теперь будет использовать порт для извещений. Заметьте, что третий параметр CreateIoCompletionPort задаёт ключ, который будет входить в пакет завершения для нашего сокета. Это информация, которая может быть получена каждый раз когда мы получаем извещение о завершении.

После того как мы создали порт и связали с ним сокет нам нужно один или несколько потоков, для обработки извещения. Каждый поток будет находится в цикле, который вызывает GetQueuedCompletionStatus через определённое время и возвращает извещение завершения.

Когда запрос сделан, указатель на структуру завершения передаётся как параметр. GetQueuedCompletionStatus вернёт тоже указатель на структуру, когда операция выполнится. Но с помощью только этой структуры не возможно, узнать какая операция завершилась. Для того чтобы следить за операциями которые завершились, полезно определить свою OVERLAPPED структуру, которая бы содержала информацию о каждой операции поставленной в очередь. Когда перекрывающаяся операция выполняется OVERLAPPEDPLUS передаётся как lpOverlapped параметр(например в WSASend, WSARecv и т.д). Это позволяет вам узнавать информацию о выполнении каждого перекрывающегося запроса. Когда операция завершится указатель на OVERLAPPED, который мы получим после вызова GetQueuedCompletionStatus , теперь будет указывать на расширенную структуру. Заметьте, что OVERLAPPED поле не обязательно должно быть первым полем. После того как мы получили OVERLAPPED структуру, макрос CONTAINING_RECORD может быть использован для получения указателя на расширенную структуру. Посмотрите на этот код

while (1)
{
	ret = GetQueuedCompletionStatus(hIocp,&dwBytesXfered,
		(PULONG_PTR)&PerHandleKey,&Overlap,INFINITE);		
	{
		if (ret == 0)
		{
			// Operation failed
			continue;
		}
		.....
	}
}

PerHandleKey может быть чем угодно, это тот параметр CompletionKey, который мы передавали для CreateIoCompletionPort, при связывании сокета с портом. Параметр Overlap возвращает указатель на структуру OVERLAPPEDPLUS, которая использовалась для инициации перекрывающей операции. Запомните, если ошибка вернулась сразу(типа SOCKET_ERROR и эта ошибка не WSA_IO_PENDING), то никакое извещение не будет поставлено в очередь. Другими словами, если перекрывающийся вызов произошел успешно или с ошибкой WSA_IO_PENDING, то завершающее извещение всегда будет поставлено в очередь. Дополнительно информацию об этом можете посмотреть здесь http://msdn.microsoft.com/library/en-us/dnpic/html/msdn_servrapp.asp.


Предыдущий Шаг | Следующий Шаг | Оглавление
Автор Leonid Molochniy - 10.12.2001