Малышев_Сетевое программирование 19.12.18
.pdf81
Функция WSAAsyncSelect проще в программировании и изначально позволяет обрабатывать множество клиентов. Ну, а самое главное — нет ни одного дополнительного потока.
Обратите внимание, что обмен информацией происходит асинхронно. Отправку и прием большого количества данных нужно будет делать порциями.
Допустим, что клиент должен передать серверу 1 Мбайт данных. Конечно же, за один прием это сделать нереально. Поэтому на стороне сервера нужно действовать следующим образом:
сервер должен узнать (клиент должен сообщить серверу) количество передаваемых данных;
сервер должен выделить необходимый объем памяти или, если количество данных слишком большое, создать временный файл;
при получении события FD_READ сохранять принятые данные в буфере или в файле. Обрабатывать событие, пока данные не будут получены полностью, и клиент не пришлет определенную последовательность байт, определяющую завершение передачи данных.
Отправка от клиента должна происходить следующим способом:
-сообщить серверу количество отправляемых данных;
-открыть файл, из которого будут читаться данные;
-послать первую порцию данных, остальные данные – по событию
FD_WRITE;
-по завершению отправки послать серверу последовательность байт, определяющую завершение передачи данных.
Использование сообщений Windows очень удобно, но теряется совместимость с UNIX-системами, где сообщения реализованы по-другому и нет функции WSAAsyncSelect. Поэтому при переносе такой программы на другую платформу возникнут большие проблемы и придется переписать слишком много кода. Но если перенос не планируется, то удобно использовать WSAAsyncSelect, что позволяет добиться максимальной производительности и удобства программирования.
Дополнительные функции [16]:
//ФУНКЦИЯ: MyRegisterClass()
//НАЗНАЧЕНИЕ: регистрирует класс окна.
//КОММЕНТАРИИ:
//Эта функция и ее использование необходимы только в случае, //если нужно, чтобы данный код был совместим с системами Win32, не //имеющими функции RegisterClassEx', которая была добавлена в //Windows 95. Вызов этой функции важен для того, чтобы приложение //получило "качественные" мелкие значки и установило связь с ними.
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
|
82 |
wcex.cbSize = sizeof(WNDCLASSEX); |
|
wcex.style |
= CS_HREDRAW | CS_VREDRAW; |
wcex.lpfnWndProc |
= WndProc; |
wcex.cbClsExtra |
= 0; |
wcex.cbWndExtra |
= 0; |
wcex.hInstance |
= hInstance; |
wcex.hIcon |
= LoadIcon(hInstance, |
MAKEINTRESOURCE(IDI_MY)); |
|
wcex.hCursor |
= LoadCursor(NULL, IDC_ARROW); |
wcex.hbrBackground |
= (HBRUSH)(COLOR_WINDOW+1); |
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_MY); |
|
wcex.lpszClassName |
= szWindowClass; |
wcex.hIconSm |
= LoadIcon(wcex.hInstance, |
MAKEINTRESOURCE(IDI_SMALL)); return RegisterClassEx(&wcex);
}
//ФУНКЦИЯ: InitInstance(HINSTANCE, int)
//НАЗНАЧЕНИЕ: сохраняет обработку экземпляра и создает //главное окно.
//КОММЕНТАРИИ:
//В данной функции дескриптор экземпляра сохраняется в //глобальной переменной, а также создается и выводится на экран главное //окно программы.
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance; // Сохранить дескриптор экземпляра в //глобальной переменной
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd);
return TRUE;
}
83
7.6.WSAEVENTSELECT
Примечание: под «объектом события» далее будет пониматься какоето определенное сетевое событие. Дело в том, что тут событие рассматривается, как класс [12].
WSAEventSelect работает похожим способом с WSAAsyncSelect, но вместо оконных сообщений использует объекты событий. Преимуществом этого является эффективность (объекты событий работают быстрее оконных сообщений). Графическая интерпретация этой модели выглядит немного сложнее, чем предыдущей, но на самом деле это не так:
Рис. 16. WSAEventSelect
Программа регистрируется для уведомления о сетевых событиях через объекты событий
Программа: «Отправь-ка эти данные» WinSock: «Не могу сделать это сейчас»
Программа ждет события, чтобы сигнализировать о нем Программа: «Отправь-ка эти данные»
WinSock: «Сделано!»
Эта модель похожа на блокирующую, т. к. программа ждет событие, о котором ей будет сообщено, но в тоже самое время можно создать свой объект события. Все объекты события являются частью WinAPI, которую использует WinSock. В WinSock есть некоторые функции для создания объектов, но фактически это API-функции в WinSock-упаковке.
Все, что WinSock делает в этой модели, это сигнализирует объект события, когда это событие должно произойти.
Функция, с помощью которой регистрируется сетевое событие -
WSAEventSelect:
84
WSAAsyncSelect отправит сообщение о произошедшем сетевом собы-
тии (FD_READ, FD_WRITE, и т. д.), а у WSAEventSelect есть только один способ уведомления: сигнализирование объекта событий. Это позволяет использовать данную модель как в GUI приложениях, так и в консольных. Какие события произошли можно узнать с помощью функции
WSAEnumNetworkEvents.
// Описание на C++
int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);
// описание на Delphi
function WSAEventSelect(S: TSocket; hEventObject: TWSAEvent; lNetworkEvents: LongInt): Integer;
Эта функция очень похожа на функцию WSAAsyncSelect, за исключением того, что события FD_XXX привязываются не к оконным сообщениям, а к сокетным событиям. Параметр S определяет сокет, события которого отслеживаются, параметр hEventObject – событие, которое должно взводиться в сигнализированное состояние при наступлении отслеживаемых событий, lNetworkEvents – комбинация констант FD_XXX, определяющая, с какими событиями на сокете связывается событие hSocketEvent.
Функция WSAEventSelect возвращает ноль, если операция прошла успешно, и SOCKET_ERROR при возникновении ошибки.
Событие, связанное с сокетом функцией WSAEventSelect, взводится при тех же условиях, при которых в очередь окна помещается сообщение при использовании WSAAsyncSelect. Так, например, функция recv взводит событие, если после ее вызова в буфере сокета еще остаются данные. Но, с другой стороны, функция recv не сбрасывает событие, если данных в буфере сокета нет. А поскольку сокетные события не сбрасываются автоматически функцией WSAWaitForMultipleEvents, программа всегда должна сбрасывать события сама. Так, при обработке FD_READ наиболее типична ситуация, когда сначала сбрасывается событие, а потом вызывается функция recv, которая при необходимости снова взводит событие. Здесь имеется проблема ложных срабатываний в тех случаях, когда данные извлекаются из буфера по частям с помощью нескольких вызовов recv. В данном случае проблему решить легче: не нужно отменять регистрацию событий, достаточно просто сбросить событие непосредственно перед последним вызовом recv.
В принципе, события FD_XXX разных сокетов можно привязать к одному сокетному событию, но этой возможностью обычно не пользуются, т. к. в WinSock2 отсутствуют средства, позволяющие определить, на
85
каком из сокетов событие привело к взведению сокетного события. Поэтому приходится для каждого сокета создавать отдельное событие.
При вызове WSAEventSelect сокет переводится в неблокирующий режим (как и в случае с WSAAsyncSelect). Повторный вызов WSAEventSelect для данного сокета отменяет результаты предыдущего вызова (т. е. невозможно связать разные события FD_XXX одного сокета с разными сокетными событиями).
Сокет, созданный в результате вызова accept или WSAAccept, наследует связь с сокетными событиями, установленную для слушающего сокета.
Существует важное различие между использованием оконных сообщений и сокетных событий для оповещения о том, что происходит на сокете.
Предположим, с помощью функции WSAAsyncSelect события
FD_READ, FD_WRITE и FD_CONNECT связаны с некоторым оконным сообщением. Допустим происходит событие FD_CONNECT. В очередь окна помещается соответствующее сообщение. Затем, до обработки предыдущего сообщения, происходит FD_WRITE. В очередь окна помещается еще одно сообщение, которое информирует об этом. И наконец, при возникновении FD_READ в очередь будет помещено третье сообщение. Затем оконная процедура получит их по очереди и обработает.
Теперь рассмотрим ситуацию, когда те же события связаны с сокетным событием (WSAEventSelect). Когда происходит FD_CONNECT, сокетное событие взводится. Теперь, если FD_WRITE и FD_READ произойдут до того, как сокетное событие будет сброшено, то оно уже не изменит своего состояния. Таким образом, программа, работающая с асинхронными сокетами, основанными на событиях, должна, во-первых, учитывать, что взведенное событие может означать несколько событий FD_XXX, а вовторых, иметь возможность узнать, какие именно события произошли с момента последней проверки. Для получения этой информации преду-
смотрена функция WSAEnumNetworkEvents [19]
Функция WSAEnumNetworkEvents
// Описание на C++
int WSAEnumNetworkEvents(SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents);
// Описание на Delphi
function WSAEnumNetworkEvents(S: TSocket; hEventObject: TWSAEvent; var lpNetworkEvents: TWSANetworkEvents): Integer;
86
Функция WSAEnumNetworkEvents через параметр lpNetworkEvents
возвращает информацию о том, какие события произошли на сокете S с момента последнего вызова этой функции для данного сокета (или с момента запуска программы, если функция вызывается в первый раз). Параметр hEventObject необязательный, он определяет сокетное событие, которое нужно сбросить. Использование этого параметра позволяет обойтись без явного вызова функции WSAResetEvent для сброса события. Как и большинство функций WinSock, функция WSAEnumNetworkEvents возвращает ноль в случае успеха и ненулевое значение при возникновении ошибки [19].
7.7. ИСПОЛЬЗОВАНИЕ ПОТОКОВ
При использовании потоков некоторые из рассмотренных моделей могут показывать различное поведение. Например, блокирующие socket’ы в однопоточных приложениях заблокируют все приложение. Однако, когда блокирующие socket’ы используются в многопоточных приложениях, причем в разных потоках, то главный поток продолжит выполняться, в то время как вспомогательный поток будет заблокирован. Для серверов с очень малой пропускной способностью (около 10 – 50 соединений) легко и целесообразно реализовать модель select в однопоточном соединении для каждого клиента. При этом каждый запущенный поток будет связан с определенным соединением [12].
Существуют также и другие способы использования потоков, такие как обработка многократных соединений в потоке для ограничения количества потоков (это целесообразно для серверов с большим количеством соединений) или использование главного потока для обработки вводимых пользователем данных, а второстепенного (рабочего) потока – для работы с вводом/выводом socket’а.
Аналогично и для других моделей, хотя некоторые из них лучше сочетать с потоками, а другие нет. Например, в WSAAsyncSelect, используещего оконные сообщения, можно использовать потоки, но нужно как-то передавать полученные оконные сообщения в рабочий поток. Более легкой моделью для использования является WSAEventSelect: так как потоки могут ожидать события (даже многократные), то уведомления об этих событиях напрямую действуют с потоком (т. е. не надо использовать два потока как в WSAAsyncSelect).
Так же могут быть использованы чисто блокирующие socket’ы, но при этом трудно будет контролировать поток, т. к. его блокирует WinSock-
87
функция. Аналогичная проблема наблюдается и у модели select. При использовании событий можно создавать пользовательские события (не связанные с WinSock) и использовать их для уведомления потоку о том, чего не требуется делать с socket’ом.
Потоки являются очень мощным инструментом и могут радикально изменить принцип работы той или иной модели ввода/вывода. Большинству серверов необходимо обрабатывать множественные запросы одновременно, поэтому использование потоков является отличным вариантом для решения этой проблемы.
7.8.ВВЕДЕНИЕ В ПЕРЕКРЫТЫЙ ВВОД/ВЫВОД (OVERLAPPED I/O)
Перекрытый ввод/вывод является очень эффективным, особенно при хорошей реализации, и позволяет обрабатывать множество соединений. Это особенно касается сочетания перекрытого ввода/вывода с портами завершения. Однако в большинстве случаев использование перекрытого ввода/вывода излишне [12].
В рассмотренных ранее моделях посылались некоторые уведомления (такие как «данные доступны» или «готов попробовать переслать данные» и т. д.) при возникновении определенных сетевых событий. Перекрывающие модели также посылают уведомления, но не о наступлении сетевых событий, а об их завершении. При вызове WinSock-функция может либо успешно завершиться, либо завершиться неудачей с кодом ошибки WSA_IO_PENDING. При использовании перекрывающих моделей будет уведомление о завершении операции. Т. е. нужно просто подождать, пока операция не завершится.
Ценой этого эффективного подхода является трудная реализация. Если не требуется действительно хорошая эффективность, то лучше воспользоваться ранее рассмотренными моделями. Кроме того, операционные системы Windows не полностью поддерживали перекрытые модели ввода/вывода.
Как и модели с уведомлением о сетевых событиях, перекрытые модели также могут быть реализованы по-разному. Они отличаются способом уведомления: блокирование, polling, процедуры завершения и порты завершения.
7.9. ПЕРЕКРЫТЫЙ ВВОД/ВЫВОД: БЛОКИРОВАНИЕ
Первая модель перекрытого ввода/вывода использует объект события для сигнализирования о завершении. Она многим похожа на
88
WSAEventSelect, но отличается тем, что объект устанавливается в сигнализированное состояние при завершении WinSock-операции, а не при наступлении какого-то сетевого события [12].
Рис. 17. Перекрытый ввода/вывод: блокирование
Программа: «Отправь-ка эти данные»
WinSock: «Хорошо, но я не могу отправить их прямо сейчас» Программа ждет сигнала от объекта события, указывающего на
то, что функция завершена
WinSock-функция выполняется одновременно с работой главного потока программы (главный поток ожидает сигнала от события). Когда событие получает сигнал от WinSock-функции (пунктирная линия на рисунке) оно переходит в сигнализированное состояние и отправляет сигнал главному потоку о том, что функция завершена, и главный поток выполняет обработку полученного сигнала и переходит к выполнению следующей команды.
7.10. ПЕРЕКРЫТЫЙ ВВОД/ВЫВОД: POLLING
Так же как и в ранее рассмотренной модели polling, в этой модели так же можно запросить статус выполнения операции (хотя в polling’e не запрашивался статус выполнения, а просто получались данные о неудачном завершении функции [12]; однако основной поток программы знал, когда и как функция завершилась: неудачно или удачно). С помощью функции WSAGetOverlappedResult можно узнать статус выполняемой операции. Графическая интерпретация перекрытого polling’а очень похожа на интерпретацию обычного polling’а, за исключением того, что WinSock-
89
функция выполняется в то же время, что и опрос программы о выполнении функции.
Рис. 18. Перекрытый ввода/вывод: polling
Программа: «Отправь-ка эти данные»
WinSock: «Хорошо, но я не могу отправить их сейчас» Программа: «Уже отправил?»
WinSock: «Нет»
Программа: «Уже отправил?» WinSock: «Нет»
Программа: «Уже отправил?» WinSock: «Нет»
Программа: «Уже отправил?» WinSock: «Да! »
Эта модель не очень эффективна, так как сильно загружает процессор. Поэтому не рекомендуется использовать эту модель, если только не занизить приоритет главного потока.
7.11. ПЕРЕКРЫТЫЙ ВВОД/ВЫВОД: ПРОЦЕДУРЫ ЗАВЕРШЕНИЯ
Процедуры завершения – процедуры обратного вызова или процедуры отзыва вызываются в ответ на определенное действие при завершении операции. Эти процедуры вызываются в контексте потока, который начал операцию [12]. Допустим есть поток, который запросил перекрытую операцию записи. WinSock выполняет эту операцию в собственном потоке, в то время как основной поток тоже выполняется. Когда операция закончится, WinSock должен вызвать процедуру отзыва, которая будет выполняться в контексте потока WinSock. Таким образом, поток, вызвавший операцию записи, будет выполняться одновременно с процедурой вызова. Проблема состоит в том, что синхронизация с вызывающим потоком отсутствует, и
90
он не знает, завершена ли операция (только если ему не сообщат об этом из параллельного потока).
Что бы избежать этого, WinSock удостоверяется в том, что процедура отзыва протекает в том же потоке, из которого происходил запрос (т. е. в потоке WinSock). Осуществляется это с помощью APC (Asynchronous Procedure Call или Асинхронный Вызов Процедуры), механизма, встроенного в Windows. Это можно представить как «внедрение» процедуры в основной поток выполнения программы. Таким образом, поток сначала выполнит процедуру, а потом продолжит основную работу. Однако, система не может приказать потоку прекратить делать основную работу и обработать сначала эту процедуру. Для обеспечения «внедрения» в нужное место механизм APC требует, что бы поток находился в, так называемом, извещающем состоянии ожидания.
Каждый поток имеет свою собственную APC-очередь, в которой процедуры ждут своего вызова. Когда поток входит в извещающее состояние ожидания, это указывает на то, что он готов обслуживать очередь APC. Перекрытый ввод/вывод с процедурами завершения использует APC для уведомления о завершении операции.
Рис. 19. Перекрытый ввода/вывод: процедуры завершения
Программа: «Отправь-ка эти данные»
WinSock: «Хорошо, но я не могу отправить их сейчас» Программа входит в извещающее состояние ожидания Функция WinSock завершилась
Состояние ожидания получает сигнал, о том, что функция завер-
шена
Выполняется функция отзыва и управление переходит к программе
