Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
5 семестр / OSiSP_LR_7.doc
Скачиваний:
4
Добавлен:
18.02.2023
Размер:
214.53 Кб
Скачать

3 Использование сокетов через события 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);

В данном случае необходимо правильно указать первые два параметра и обнулить последний. Содержимое третьего параметра не имеет значения, потому что событие не будет отправляться, и можно указать ноль. Если вам нужно просто изменить типы событий, то можете вызвать функцию с новыми значениями четвертого параметра. Нет смысла сначала обнулять, а потом устанавливать заново.

Для каждого сокета можно назначить только одно сообщение на разные события. Это означает, что нельзя по событию FD_READ окну посылать одно сообщение, а по FD_WRITE — другое.

Прежде чем приступать к рассмотрению примера, надо разобраться с параметрами, которые будут передаваться в функцию WndProc при возникновении события. Вспомним, как выглядит эта функция:

LRESULT CALLBACK WndProc(

HWND hWnd,

UINT message,

WPARAM wParam,

LPARAM lParam)

Параметры wParam и lParam содержат вспомогательную информацию (в зависимости от события). Для событий сети в параметре wParam хранится дескриптор сокета, на котором произошло событие. Таким образом, вам не надо хранить массив созданных сокетов, их всегда можно получить в событии.

Параметр lParam состоит из двух слов: младшее определяет событие, а старшее — код ошибки. Вот теперь можно переходить к рассмотрению реального примера. Создайте новое приложение Win32 Application, назовите проект WSASel. Откройте файл WSASel.cpp и подкорректируйте функцию _tWinMain. Как всегда, весь код нужно добавить до цикла обработки сообщений. Всю функцию вы можете увидеть в следующем листинге:

int APIENTRY _tWinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPTSTR lpCmdLine,

int nCmdShow)

{

// TODO: Place code here.

MSG msg;

HACCEL hAccelTable;

// Initialize global strings

LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);

LoadString(hInstance, IDC_WSASEL, szWindowClass, MAX_LOADSTRING);

MyRegisterClass(hInstance);

// Perform application initialization:

if (!InitInstance (hInstance, nCmdShow))

{

return FALSE;

}

hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_WSASEL);

WSADATA wsd;

if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)

{

MessageBox(0, "Can't load WinSock", "Error", 0);

return 0;

}

SOCKET sServerListen, sClient;

struct sockaddr_in localaddr, clientaddr;

HANDLE hThread;

DWORD dwThreadId;

int iSize;

sServerListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

if (sServerListen == SOCKET_ERROR)

{

MessageBox(0, "Can't load WinSock", "Error", 0);

return 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. Единственное, перед запуском прослушивания (listen) вызывается функция WSAAsyncSelect, чтобы выбрать созданный сокет и перевести его в асинхронный режим. Здесь указываются следующие параметры:

  • sServerListen — переменная, которая указывает на созданный серверный сокет;

  • hWnd — указатель на главное окно программы, и именно ему будут передаваться сообщения;

  • WM_USER+1 — все пользовательские сообщения должны быть больше константы WM_USER. Меньшие значения могут использоваться системой и вызвать конфликт. Я использовал такую конструкцию, чтобы явно показать необходимость использования такого сообщения. В реальных приложениях я советую создавать для этого константу с понятным именем и использовать ее. Это можно сделать следующим образом: #define WM_NETMESSAGE WM_USER+1;

  • FD_ACCEPT — событие, которое нужно обрабатывать. Что может делать серверный сокет? Принимать соединения со стороны клиента. Именно это событие нас интересует.

Самое главное будет происходить в функции WndProc. Начало функции, где нужно добавить код, показано в следующем листинге:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message,

WPARAM wParam, LPARAM lParam)

{

int wmId, wmEvent;

PAINTSTRUCT ps;

HDC hdc;

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.

Хочу обратить ваше внимание, что обмен информацией происходит асинхронно. Отправку и прием большого количества данных нужно будет делать порциями.

Допустим, что клиент должен передать серверу 1 Мбайт данных. Конечно же, за один прием это сделать нереально. Поэтому на стороне сервера вы должны действовать следующим образом:

  • сервер должен узнать (клиент должен сообщить серверу) количество передаваемых данных;

  • сервер должен выделить необходимый объем памяти или, если количество данных слишком большое, создать временный файл;

  • при получении события FD_READ сохранять принятые данные в буфере или в файле. Обрабатывать событие, пока данные не будут получены полностью, и клиент не пришлет определенную последовательность байт, определяющую завершение передачи данных.

Подобным способом должна происходить отправка с клиента:

  • сообщить серверу количество отправляемых данных;

  • открыть файл, из которого будут читаться данные;

  • послать первую порцию данных, остальные данные — по событию FD_WRITE;

  • по завершению отправки послать серверу последовательность байт, определяющую завершение передачи данных.

Использование сообщений Windows очень удобно, но вы теряете совместимость с UNIX-системами, где сообщения реализованы по-другому и нет функции WSAAsyncSelect. Поэтому при переносе такой программы на другую платформу возникнут большие проблемы и придется переписать слишком много кода. Но если перенос не планируется, то я всегда использую WSAAsyncSelect, что позволяет добиться максимальной производительности и удобства программирования.

Соседние файлы в папке 5 семестр