|
Синхронные
и асинхронные сокеты в Windows
Введение
В данном документе мы изучим модель Windows
Sockets. Мы сравним синхронные и асинхронные Window Sockets
API и разберемся, почему же мы должны использовать
асинхронную модель, и как асинхронное расширение
Window Sockets обеспечивает лучшую работоспособность в
Windows.
Synchronous Winsock
Синхронные Window Sockets API смоделированы на
базе стандартных Berkeley Sockets API. Совместимость
между этими двум программными интерфейсами (API)
очень полезна. Особенно тогда, когда более старые
куски unix-кода должны быть портированы под Windows.
Вообще, синхронные функции Window Sockets API
направлены на процедурное программирование,
используя блокирующие сокеты. Программы читают и
пишут в сокеты в блокирующей манере, целиком
полагаясь на операционную систему. Синхронные API
работают лучше в command-line ориентированных
многозадачных системах типа Unix. Это не лучший
выбор для Windows.
Для решения проблем блокирующих вызовов в
ориентированном на события Windows, Winsock API имитирует
блокирующие вызовы через использование blocking hook,
который содержат цикл обработки событий. К
сожалению, такая эмуляция блокирующих вызовов
Window Sockets имеет некоторые ограничения. В
частности, только один блокирующий вызов может
быть активным в данный момент. Эти ограничения
напрямую ударяют по пользовательскому
интерфейсу программы.
Несмотря на то, что блокированная
программа "отдыхает", пока не произойдет
ожидаемое сетевое событие, поскольку Windows занята,
это в действительности не настоящее
блокирование. Правильнее будет сказать, что это
ожидание в цикле, который так же обрабатывает
сообщения Windows, как приведенный ниже пример:
for(;;) {
/* flush messages for good user response */
while(BlockingHook())
;
/* check for WSACancelBlockingCall() */
if(operation_cancelled())
break;
/* check to see if operation completed */
if(operation_complete())
break; /* normal completion */
}
BOOL DefaultBlockingHook(void) {
MSG msg;
BOOL ret;
/* get the next message if any */
ret = (BOOL)PeekMessage(&msg,NULL,0,0,PM_REMOVE);
/* if we got one, process it */
if (ret) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
/* TRUE if we got a message */
return ret;
}
Если оконное сообщение,
назначенное блокирующей программе, помещается в
очередь сообщений Windows, оно передается оконной
процедуре в программе. Пользовательский
интерфейс, принадлежащий программе, остается
активным. Пользователь может выбирать элементы
меню и нажимать клавиши, которые затем будут
отправлены программе. Проблемы могут возникнуть
в том случае, если пользователь выбирает такой
элемент меню, который вызывает другую сетевую
операцию. Потому, что программа уже находится в
блокирующей сетевой функции, не может быть
вызвана другая сетевая функция, и результатом
попытки выполнить сетевую операцию будет ошибка.
Чтобы предотвратить это событие,
программа Windows, использующая синхронные
блокирующие API, должна убедиться, что следующая
сетевая операция не может быть запущена, пока
программа находится в блокирующем режиме.
Программа должна отключать все элементы меню и
комбинации клавиш, которые могут привести к
запуску другой сетевой операции, а это сложный
процесс, особенно если путей много, или полностью
отключать пользовательский интерфейс от
получения сообщений от мыши и клавиатуры и
показывать курсор - песочные часы.
Первый вариант более предпочтителен с
точки зрения пользователя. Пользователь не
сможет запустить следующую сетевую команду, но,
по крайней мере, оставшееся в системе будет
доступным. Пользователь может перейти к другой
выполняющейся программе и продолжить работу
пока первая программа ожидает завершения
сетевой функции. Однако, выполнить эту задачу
программисту отнюдь не легко.
Во втором случае пользователь
оказывается перед курсором - песочными часами в
ожидании завершения сетевой операции, когда мог
бы в это время выполнять полезную работу.
Многозадачная среда Windows более эффективна, чем
однозадачная, поскольку сеть занята.
В обоих случаях невозможно выполнять две
сетевые операции в одно и то же время в одном
приложении. Пользователь не может одновременно
отправлять письмо и проверять наличие новых
писем на сервере.
Проблема синхронных Window Sockets API может быть
решена двумя способами. Оба с использованием
неблокирующих сокетов. Первый метод заключается
в том, что создаются неблокирующие сокеты и
функцией select() проверяют состояние интересующего
сокета. Затем из сокетов читаются и записываются
доступные данные или данные, которые могут быть
помещены в очередь для передачи. Тем не менее,
чтобы избежать проблем блокирующих вызовов,
функция sеlect() должна быть вызвана с параметром
timeout равным нулю, что приводит к неблокирующей
операции. Проще говоря, сама функция select() должна
быть вызвана так, чтобы не запереть систему,
чтобы поддерживать свою очередь сообщений и в то
же время позволить сообщениям Windows достигать
ядра системы. Для этого должен использоваться
код, напоминающий blocking hook:
struct timeval timeout = {0, 0};
while (!select(..., &timeout)) {
if (PeekMessage(...))
{
TranslateMessage(...);
DispatchMessage(...);
}
}
Несмотря на то, что этот код
работает, это жалкое решение. Будет сжигаться
вхолостую время CPU, в то время как ничего
эффективного выполняться не будет - в цикле будет
выполняться функция select(). Этот способ слишком не
экономно распоряжается ресурсами системы и
непригоден в событие - ориентированной
операционной системе Windows.
Более эффективное решение всех проблем
блокирующих сокетов - это использование
асинхронного расширения Window Sockets API.
Asynchronous Winsock: The Good, The Bad, and The Ugly
Использование асинхронного расширения
Window Socket API - лучшее решения для программирования
сокетов под Windows. Оно позволяет выполнять
одновременно множество сетевых задач без лишней
нагрузки на процессор. Используя асинхронное
расширение пользователь может отправлять письмо
и проверять наличие новых на удаленном сервере в
одно и то же время в одной и той же программе.
Асинхронное расширение Winsock оперирует
отсылкой сообщений окну всякий раз, когда
происходит смена состояния сокета, или когда
асинхронная функция завершает поиск в asynchronous
database (Чтобы понять, о чем идет речь, читайте
приложение "А"). Индикация смены состояния
сокета выполняется использованием функции
WSAAsyncSelect(). Поиск по базе данных Winsock выполняется
функцией типа WSAAsyncGetXByY() (WSAAsyncGetProtoByName(),
WSAAsyncGetServByPort() и т.д.). Каждая из стандартных
функций имеет копию с похожим названием для
асинхронного расширения.
[ WSAAsyncSelect ]
Ниже представлена последовательность событий:
1. Во-первых, программа должна создать
интересующий ее сокет. Сокет привязывается к
локальному адресу, затем выполняется WSAAsyncSelect() и
к сокету привязывается событие, нужное
программе. Например, вызвавав WSAAsyncSelect() можно
указать, что программу интересует событие
"connection". Так же в этой функции указывается,
какое окно должно получать уведомление об
изменениях состояния сокета, а так же
указывается само сообщение, которое будет ему
послано. Функция WSAAsyncSelect() делает сокет
неблокирующим, как будто бы была вызвана функция
ioctlsocket() с командой FIONBIO.
2. Вызывается асинхронная функция. С тех пор как
сокет стал неблокирующим, функция connect()
завершается мгновенно, хотя соединение еще не
было установлено. Программа может дальше
выполнять свои задачи. Так как blocking hook не
используется, программа может вызывать другие
сетевые функции.
3. Немного позже Winsock завершает соединение с
удаленным хостом. Winsock посылает сообщение окну,
дескриптор которого был задан при вызове функции
WSAAsyncSelect(). Параметры сообщения указывают сокет, о
чьем состоянии уведомляется, происшедшее
событие и код возврата, связанный с этим
событием.
4. Оконная процедура получает сообщение и
передает управление отвечающей за это части
программы. Если соединение было установлено без
ошибок, программа посылает или принимает данные
из сокета. Если произошла ошибка, программа
информирует пользователя через диалоговое меню
или другим способом.
[ WSAAsyncGetXByY ]
Последовательность событий:
1. Приложение вызывает асинхронную "database"
функцию из ряда WSAAsyncGetXByY. При вызове функции
указывается окно, которое будет получать
сообщение, и когда поиск по базе данных
завершится, окну будет послано указанное
сообщение. После запуска поиска в базе функция
возвратит управление программе. При этом она
возвратит значение "asynchronous task handle", что
идентифицирует асинхронную задачу, которая
вскоре должна завершиться. Приложение
продолжает выполняться пока будет производиться
поиск. Так как программа не использует blocking hook,
нет никаких ограничений на вызов других функций
Winsock. Приложение может выполнять другие сетевые
функции, пока ожидаемая функция не выполнится.
2. Когда поиск будет завершен, Winsock посылает
сообщение окну, заданному при вызове
используемой асинхронной функции. Параметрами
этого сообщения будут дескриптор сетевой задачи,
что получается после вызова функции и код
возврата.
3. Оконная функция передает это сообщение дальше
в функцию, которая его обработает. Если ошибок не
было, программа может использовать полученную
информацию.
Window Message Processing
Асинхронное расширение Window Sockets API
работает довольно неплохо. Оно соединяет API
сетевого программирования с событием -
ориентированной моделью остальных API.
Однако, вопреки этой выгоде, асинхронная
модель Winsock имеет некоторые проблемы. Чтобы
обработать оконное сообщение, информирующее о
том, что изменилось состояние сокета или
завершен просмотр базы данных, программисты
обычно используют два способа - прямые сообщения
главному окну приложения или создают отдельные
окна для каждого сокета или поиска по базе.
По идее, централизованная обработка
сообщений главным окном это легко, но на практике
это добавляет большие проблемы. Если больше чем
один сокет используется программой в одно и то же
время, или производится более одного поиска по
базе данных, нужно хранить список дескрипторов
сокетов или асинхронных задач. Главная оконная
процедура в списке ищет информацию, связанную с
этим дескриптором сокета или сетевой задачи и
отправляет сообщение об изменении состояния или
о завершении просмотра в предназначенную для
этого процедуру. Этот подход заставляет
обработку сети интегрировать в ядро программы,
что затрудняет создание библиотек сетевых
функций (например, реализация почтовых
протоколов). Каждый раз, когда эти функции
используются, в главный оконный обработчик
приложения нужно добавлять дополнительный код,
чтобы все заработало.
Во втором методе обработки сообщений для
получения сообщений приложение создает скрытое
окно. Оно служит для отделения главной оконной
процедуры приложения от обработки сетевых
сообщений. Этот подход упрощает главное
приложение и упрощает использование сетевого
кода вновь в других программах. Неудачной
стороной этого подхода является чрезмерное
использование Windows' USER heap (памяти) - для каждого
созданного окна резервируется довольно большой
его объем.
Приложение "А". Database Functions.
В системе хранится очень много данных о
состоянии сети. Эти данные позволяют Winsock
программам находить адреса удаленных хостов и
соотносить известные номера портов сервисам.
[ Host Name Lookup ]
Host name database хранит соответствие между
именами хостов и их адресами. Функции gethostbyaddr и
gethostbyname используются для доступа к этой базе
данных и получения информации о хостах.
Извлекаемая информация включает в себя имя
хоста, алиасы и адреса для каждого интерфейса.
Gethostbyaddr использует IP как ключ к базе данных, тогда
как gethostbyname использует имя хоста. Gethostbyname
используется намного чаще. Каждый раз, когда
пользователь вводит имя хоста, эта функция
получает соответствующий IP, необходимый для
использования в функциях сокета.
[ Service Lookup ]
Это вторая по частоте использования часть
database функций. Service database хранит соответствия
известных номеров сервисов и их наименованиями.
Getservbyname and getservbyport используются для получения
этой информации. Например, задав имя сервиса
"SMPT", getservbyname возвратит номер порта,
используемый по умолчанию для этого сервиса.
Getservbyport() возвратит имя сервиса, используя номер
порта как ключ для поиска.
Зачем нужна база данных для хранения
известных номеров портов? Администратор может
зафиксировать номера известных портов в целях
безопасности. В другом случае, база сервисов
может пригодится в том случае, если какой-либо из
них находится на стадии разработки. Сервисам
обычно случайно назначают номера портов на
стадии разработки и тестирования, а официальный
номер порта выдает организация Internet Assigned Numbers
Authority (IANA). Используя такую базу данных, номер
порта может быть легко изменен без
перекомпиляции самой программы. Использование
этой базы не строгое требование, если вы
выполняете реализацию широко распространенных
протоколов, таких как SMTP и NNTP. На практике, этот
номер никогда не меняется. Даже если это и
произойдет, хорошая программа должна позволять
пользователю задать номер порта вручную.
[ Protocol Lookup ]
База данных о протоколах - наименее
известная база в Winsock программировании. В ней
хранятся соответствия между названиями
протоколов ("tcp", "udp") и связанными с
ними целочисленными константами. Они
используются в параметре protocol при вызове функции
socket(). Соответственно, существуют две функции
getprotobyname и getprotobynumber.
Большинство программ никогда не
используют эту базу данных. Функция socket() при
вызове позволяет указать параметр protocol равным
нулю, что означает, что сокет должен использовать
протокол по умолчанию, принятый для данного
семейства протоколов. В нашем случае, это
семейство протоколов Internet, а протоколом по
умолчанию для stream - сокетов является TCP. Для
дэйтаграмных сокетов используется UDP.
Использование в качестве номера протокола
результата выполнения функции getprotobyname является
уже излишеством.
Данная статья является
переводом (версия №1) с дополнениями части
руководства "Developing for the Internet with WinSock" Дэйва
Робертса (Dave Roberts), лежащего на сервере www.itknowledge.com.
Publication Date: 09/01/95
Если Вас интересует пример
программирования прокси сервера в среде Windows с
использованием асинхронного расширения Window Sockets
API, написанный переводчиком, то посетите сервер
www.uinc.ru, где вы так же сможете найти много
интересного для себя.
(c) Copyright 2001. Украина, Запорожье. KMiNT21
(mailto:kmint21@mail.ru).
uinC Member
[c]uinC
Special thanks: NiFi[UInC], Motrichuk Alexander, Dave Roberts.
Статья написана специально для UInC (http://www.uinc.ru/).
Любые комментарии, поправки, пожелания или
дополнения можно посылать сюда: kmint21@mail.ru
Все документы и программы на этом сайте собраны ТОЛЬКО для
образовательных целей, мы не отвечаем ни за какие последствия,
которые имели место как следствие использования этих материалов\программ.
Вы используете все вышеперечисленное на свой страх и риск.
| |