Использование сокетов через события Windows
Функция select введена в библиотеку WinSock для совместимости с аналогичными библиотеками других платформ. Для программирования в Windows более мощной является функция WSAAsyncSelect, которая позволяет отслеживать состояние сокетов с помощью сообщений Windows. Таким образом, вы сможете получать сообщения в функции WndProc, и нет необходимости замораживать работу программы для ожидания доступности сокетов.
Функция выглядит следующим образом:
int WSAAsyncSelect ( SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent );
Рассмотрим каждый параметр:
s — сокет, события которого необходимо ловить;
hWnd — окно, которому будут посылаться события при возникновении сетевых сообщений. Именно у этого окна (или родительского) должна быть функция WndProc, которая будет получать сообщения;
wMsg — сообщение, которое будет отсылаться окну. По его типу можно определить, что это событие сети;
lEvents — битовая маска сетевых событий, которые нас интересуют. Этот параметр может принимать любую комбинацию из следующих значений:
FD_READ — готовность к чтению;
FD_WRITE — готовность к записи;
FD_OOB — получение срочных данных;
FD_ACCEPT — подключение клиентов;
FD_CONNECT — соединение с сервером;
FD_CLOSE — закрытие соединения;
FD_QOS — изменения сервиса QoS (Quality of Service);
FD_GROUP_QOS — изменение группы QoS.
Если функция отработала успешно, то она вернет значение больше нуля, если произошла ошибка — SOCKET_ERROR.
Функция автоматически переводит сокет в неблокирующий режим, и нет смысла вызывать функцию ioctlsocket.
Вот простой пример использования WSAAsyncSelect:
WSAAsyncSelect(s, hWnd, wMsg, FD_READ|FD_WRITE);
После выполнения этой строчки кода окно hWnd будет получать событие wMsg каждый раз, когда сокет s будет готов принимать и отправлять данные. Чтобы отменить работу события, необходимо вызвать эту же функцию, но в качестве четвертого параметра указать 0:
WSAAsyncSelect(s, hWnd, 0, 0);
В данном случае необходимо правильно указать первые два параметра и обнулить последний. Содержимое третьего параметра не имеет значения, потому что событие не будет отправляться, и можно указать ноль. Если вам нужно просто изменить типы событий, то можете вызвать функцию с новыми значениями четвертого параметра. Нет смысла сначала обнулять, а потом устанавливать заново.
ULONG ulBlock; ulBlock = 1; if (ioctlsocket(sServerListen, FIONBIO, ulBlock) == SOCKET_ERROR) { return 0; }
localaddr.sin_addr.s_addr = htonl(INADDR_ANY); localaddr.sin_family = AF_INET; localaddr.sin_port = htons(5050);
if (bind(sServerListen, (struct sockaddr *)localaddr, sizeof(localaddr)) == SOCKET_ERROR) { MessageBox(0, "Can't bind", "Error", 0); return 1; }
WSAAsyncSelect(sServerListen, hWnd, WM_USER+1, FD_ACCEPT); listen(sServerListen, 4);
// Main message loop: while (GetMessage(msg, NULL, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, msg)) { TranslateMessage(msg); DispatchMessage(msg); } }
closesocket(sServerListen); WSACleanup();
return (int) msg.wParam; }
Благодаря использованию функции WSAAsyncSelect весь код (без дополнительных потоков) можно расположить прямо в функции _tWinMain.
Код практически ничем не отличается от того, что был в проекте TCPServer (см. разд. 4.7.1). Единственное, перед запуском прослушивания (listen) вызывается функция WSAAsyncSelect, чтобы выбрать созданный сокет и перевести его в асинхронный режим. Здесь указываются следующие параметры:
sServerListen — переменная, которая указывает на созданный серверный сокет;
hWnd — указатель на главное окно программы, и именно ему будут передаваться сообщения;
WM_USER+1 — все пользовательские сообщения должны быть больше константы WM_USER. Меньшие значения могут использоваться системой и вызвать конфликт. Я использовал такую конструкцию, чтобы явно показать необходимость использования такого сообщения. В реальных приложениях я советую создавать для этого константу с понятным именем и использовать ее. Это можно сделать следующим образом: #define WM_NETMESSAGE WM_USER+1;
FD_ACCEPT — событие, которое нужно обрабатывать. Что может делать серверный сокет? Принимать соединения со стороны клиента. Именно это событие нас интересует.
Самое главное будет происходить в функции WndProc. Начало функции, где нужно добавить код, показано в листинге 4.20.
Листинг 4.20. Обработка сетевых сообщений в функции WndProc |
SOCKET ClientSocket; int ret; char szRecvBuff[1024], szSendBuff[1024];
switch (message) { case WM_USER+1: switch (WSAGETSELECTEVENT(lParam)) { case FD_ACCEPT: ClientSocket = accept(wParam, 0, 0); WSAAsyncSelect(ClientSocket, hWnd, WM_USER+1, FD_READ | FD_WRITE | FD_CLOSE); break;
case FD_READ: ret = recv(wParam, szRecvBuff, 1024, 0); if (ret == 0) break; else if (ret == SOCKET_ERROR) { MessageBox(0, "Recive data filed", "Error", 0); break; } szRecvBuff[ret] = '\0';
strcpy(szSendBuff, "Command get OK");
ret = send(wParam, szSendBuff, sizeof(szSendBuff), 0); break;
case FD_WRITE: //Ready to send data break;
case FD_CLOSE: closesocket(wParam); break; } case WM_COMMAND: ...
Здесь в самом начале добавлен новый оператор case , который проверяет, равно ли пойманное сообщение искомому сетевому сообщению WM_USER+1. Если это сетевое событие, то запускается перебор сетевых событий. Для этого используется оператор switch, который сравнивает указанное в скобках значение с поступающими событиями:
switch (WSAGETSELECTEVENT(lParam))
Как известно, в параметре lParam находятся код ошибки и тип события. Чтобы получить событие, используется функция WSAGETSELECTEVENT. А затем проверяются необходимые нам события. Если произошло соединение со стороны клиента, то выполняется следующий код:
case FD_ACCEPT: ClientSocket = accept(wParam, 0, 0); WSAAsyncSelect(ClientSocket, hWnd, WM_USER+1, FD_READ | FD_WRITE | FD_CLOSE); break;
Сначала принимается соединение с помощью функции accept. Результатом будет сокет, с помощью которого можно работать с клиентом. С этого сокета тоже нужно ловить события, поэтому вызываем функцию WSAAsyncSelect. Чтобы не плодить сообщения, используем в качестве третьего параметра значение WM_USER+1. Это не вызовет конфликтов, потому что серверный сокет обрабатывает только событие FD_ACCEPT, а у клиентского нас интересуют события чтения, записи данных и закрытия сокета.
Когда к серверу придут данные, поступит сообщение WM_USER+1, а функция WSAGETSELECTEVENT(lParam) вернет значение FD_READ. В этом случае читаются пришедшие данные, а клиенту посылается текст "Command get OK":
case FD_READ: ret = recv(wParam, szRecvBuff, 1024, 0); if (ret == 0) break; else if (ret == SOCKET_ERROR) { MessageBox(0, "Recive data filed", "Error", 0); break; } szRecvBuff[ret] = '\0'; strcpy(szSendBuff, "Command get OK"); ret = send(wParam, szSendBuff, sizeof(szSendBuff), 0); break;
Это тот же самый код, который использовался в приложении TCPServer для обмена данными между клиентом и сервером. Я намеренно не вносил изменений, чтобы сервер можно было протестировать программой TCPClient.
По событию FD_WRITE ничего не происходит, а только стоит комментарий. По событию FD_CLOSE закрывается сокет.
Рассмотренный пример с использованием функции select может работать только с одним клиентом. Чтобы добавить возможность одновременной обработки нескольких соединений, необходимо сформировать массив потоков, используемых для приема/передачи данных. В главе 6 я приведу пример с использованием функции select, который и без массивов потоков будет избавлен от этих недостатков.
Функция WSAAsyncSelect проще в программировании и изначально позволяет обрабатывать множество клиентов. Ну, а самое главное — нет ни одного дополнительного потока.
Чтобы протестировать пример, сначала запустите программу-сервер WSASel, а потом — программу-клиент TCPClient.
Примечание |
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\WSASel. |
Допустим, что клиент должен передать серверу 1 Мбайт данных. Конечно же, за один прием это сделать нереально. Поэтому на стороне сервера вы должны действовать следующим образом:
сервер должен узнать (клиент должен сообщить серверу) количество передаваемых данных;
сервер должен выделить необходимый объем памяти или, если количество данных слишком большое, создать временный файл;
при получении события FD_READ сохранять принятые данные в буфере или в файле. Обрабатывать событие, пока данные не будут получены полностью, и клиент не пришлет определенную последовательность байт, определяющую завершение передачи данных.
Подобным способом должна происходить отправка с клиента:
сообщить серверу количество отправляемых данных;
открыть файл, из которого будут читаться данные;
послать первую порцию данных, остальные данные — по событию FD_WRITE;
по завершению отправки послать серверу последовательность байт, определяющую завершение передачи данных.
Использование сообщений Windows очень удобно, но вы теряете совместимость с UNIX-системами, где сообщения реализованы по-другому и нет функции WSAAsyncSelect. Поэтому при переносе такой программы на другую платформу возникнут большие проблемы и придется переписать слишком много кода. Но если перенос не планируется, то я всегда использую WSAAsyncSelect, что позволяет добиться максимальной производительности и удобства программирования.