Малышев_Сетевое программирование 19.12.18
.pdf71
Далее нужно разобраться с параметрами, которые будут передаваться в функцию WndProc при возникновении события. Выглядит эта функция:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
Параметры wParam и lParam содержат вспомогательную информацию (в зависимости от события). Для событий сети в параметре wParam хранится дескриптор сокета, на котором произошло событие. Таким образом, не нужно хранить весь массив созданных сокетов, их всегда можно получить в событии.
Параметр lParam состоит из двух слов: младшее определяет событие, а старшее – код ошибки.
Вот теперь можно переходить к рассмотрению реального примера по обработке быстрых клавиш [21]. Всю функцию _tWinMain можно увидеть в листинге 8.
Листинг 8.
//1.cpp: определяет точку входа для приложения.
#include "stdafx.h" #include "1.h"
#define MAX_LOADSTRING 100
//Глобальные переменные:
HINSTANCE hInst; |
// текущий экземпляр |
|
TCHAR szTitle[MAX_LOADSTRING]; // Текст строки заголовка |
||
TCHAR szWindowClass[MAX_LOADSTRING];// имя класса главного |
||
//окна |
|
|
// Отправить объявления функций, включенных в этот модуль кода: |
||
ATOM |
MyRegisterClass(HINSTANCE hInstance); |
|
BOOL |
InitInstance(HINSTANCE, int); |
|
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM,
LPARAM);
//Основная функция - аналог int main() в консольном приложении: int APIENTRY _tWinMain(HINSTANCE hInstance,
//- дескриптор экземпляра приложения
HINSTANCE hPrevInstance,
//- в Win32 не используется
LPTSTR lpCmdLine,
//нужен для запуска окна в режиме командной строки
int nCmdShow)
// - режим отображения окна
{
//Отмена определения параметров hPrevInstance (предыдущий
//экземпляр приложения) и lpCmdLine (командная строка)
UNREFERENCED_PARAMETER(hPrevInstance);
72
UNREFERENCED_PARAMETER(lpCmdLine);
//TODO: разместите код здесь.
MSG msg;
HACCEL hAccelTable; // дескриптор таблицы быстрых клавиш
//Инициализация глобальных строк
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_MY, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// Выполнить инициализацию приложения: if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_MY));
// - загружает заданную таблицу быстрых клавиш
WSADATA wsd; // структура, в которую помещается детальная //информация о сокете
if (WSAStartup(MAKEWORD(2,2), &wsd) != 0) // заполняет
//переменную типа WSADATA и загружает динамическую библиотеку
//сокетов, интерфейс Windows Socket
{
MessageBox(0, "Can't load WinSock", "Error", 0); // Не удается
//загрузить WinSock 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); // Не удается
//загрузить WinSock return 0;
}
ULONG ulBlock; ulBlock = 1;
73
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) // привязка сокета к порту и
//IP-адресу
{
MessageBox(0, "Can't bind", "Error", 0); // Невозможно привязать return 1;
}
WSAAsyncSelect(sServerListen, hWnd, WM_USER+1, FD_ACCEPT);
//серверный сокет
listen(sServerListen, 4); //режим прослушивания
// Цикл основного сообщения:
while (GetMessage(&msg, NULL, 0, 0))//извлекает сообщения для
//любого окна
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
//обрабатывает быстрые клавиши для команд меню // То есть эта функция на первом этапе производит обработку
//сообщений и создает //сообщения более крупные по группе. Нам уже //можно не анализировать, что нажата //и отпущена клавиша, а просто //можно обрабатывать сообщение "введена буква". //Каждый процесс //должен использовать эту функцию для обработки сообщений.
{
TranslateMessage(&msg);
// - переводит сообщения формата виртуальных клавиш в сообщения //символы
DispatchMessage(&msg);
// - распределяет сообщение оконной процедуре
}
}
closesocket(sServerListen);
WSACleanup(); // завершает использование данного DLL и //прерывает обращение к функциям WinSock. При удачном выполнении //вернется ноль.
74
return (int) msg.wParam;
}
//Здесь должны быть: ФУНКЦИЯ: MyRegisterClass()
//ФУНКЦИЯ: InitInstance(HINSTANCE, int)
//ФУНКЦИЯ: WndProc(HWND, UINT, WPARAM, LPARAM)
Благодаря использованию функции WSAAsyncSelect весь код (без дополнительных потоков) расположен прямо в функции_tWinMain.
Функция _tWinMain – [17] имеет четыре параметра. Первый из них – HINSTANCE hInstance (строка 4). hInstance является дескриптором экземпляра окна (это некий код оконной процедуры, идентификатор, по которому ОС будет отличать её от остальных окон). Через него можно обращаться к окну в процессе работы в других функциях изменять параметры окна. HINSTANCE является одним из многочисленных типов данных определенных в WinAPI, таким же как int, например.
Следующий параметр: HINSTANCE hPrevInstance (строка 5). Как написано в комментариях, в Win32 он не используется (так как он создан для 3.x разрядной системы), из предыдущего понятно, что это дескриптор экземпляра окна.
Далее переменная типа LPSTR (строка 6) с именем lpCmdLine. Она используется в том случае, если окно запускается через командную строку
спрописью параметров.
Ипоследний параметр: целочисленный, определяет способ (режим) отображения окна. Нужен для функции ShowWindow. Например, с помощью него можно развернуть окно на весь экран, сделать его определённой высоты, прозрачным или поверх остальных.
// TODO: разместите код здесь.
MSG msg;
HACCEL hAccelTable; // дескриптор таблицы быстрых клавиш
Две функции с именем LoadString загружают текст из ресурсов строк [18]. В данном случае выполнится код загрузки ресурса.
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_MY, szWindowClass, MAX_LOADSTRING);
Четыре параметра [18].
hInstance – указывает на экземпляр данной программы, потому что нужны ресурсы из данного проекта.
75
IDS_APP_TITLE – имя ресурса, который надо загрузить. Если сейчас дважды щелкнуть в ресурсах на разделе String Table, то откроется таблица строк, в которой в одной колонке будет имя строки, а во второй текст. Вот именно это имя и нужно указывать в этом параметре.
szTitle – переменная, в которую должно поместиться значение. В начале файла у нас объявлено две переменные с именами szTitle и szWindowClass:
TCHAR szTitle[MAX_LOADSTRING]; // Текст заголовка окна TCHAR szWindowClass[MAX_LOADSTRING]; // Имя класса главно-
го окна.
В качестве размера указано MAX_LOADSTRING. MAX_LOADSTRING – последний параметр, который указывает мак-
симальное количество загружаемых символов. Получается, что размер загружаемой строки равен размеру строки в переменной, и нельзя загрузить из ресурсов в переменную больше, чем выделено памяти.
После этого идет вызов функции MyRegisterClass(hInstance). В ней происходит заполнение структуры WNDCLASSEX.
WNDCLASSEX – это структура, которая используется при создании нового класса окна.
Далее выполняем инициализацию приложения, создание сокета и перевод его в неблокирующий режим; привязываем сокет к порту и IP- адресу:
// Выполнить инициализацию приложения: if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_MY));
// - загружает заданную таблицу быстрых клавиш
WSADATA wsd; // структура, в которую помещается детальная информация о сокете
if (WSAStartup(MAKEWORD(2,2), &wsd) != 0) // заполняет
//переменную типа WSADATA и загружает динамическую библиотеку
//сокетов, интерфейс Windows Socket
{
MessageBox(0, "Can't load WinSock", "Error", 0); // Не удается
//загрузить WinSock return 0;
}
SOCKET sServerListen, sClient;
76
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); // Не удается
//загрузить WinSock 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) // привязка сокета к порту и
//IP-адресу
{
MessageBox(0, "Can't bind", "Error", 0); // Невозможно привязать return 1;
}
Перед запуском прослушивания (listen) вызывается функция WSAAsyncSelect, чтобы выбрать созданный сокет и перевести его в неблокирующий (асинхронный) режим.
WSAAsyncSelect(sServerListen, hWnd, WM_USER+1, FD_ACCEPT);
//серверный сокет
listen(sServerListen, 4); //режим прослушивания
ВWSAAsyncSelect указываются следующие параметры:
sServerListen – переменная, которая указывает на созданный серверный сокет;
hWnd – указатель на главное окно программы, и именно ему будут передаваться сообщения;
WM_USER+1 – все пользовательские сообщения должны быть больше константы WM_USER. Меньшие значения могут ис-
77
пользоваться системой и вызвать конфликт. Такая конструкция применяется, чтобы явно показать необходимость использования такого сообщения. В реальных приложениях нужно создавать для этого константу с понятным именем и использовать ее. Это можно сделать следующим образом: #define WM_NETMESSAGE WM_USER+1;
FD_ACCEPT – событие, которое нужно обрабатывать. Серверный сокет готов принимать соединение со стороны клиента.
Далее выполняем основной цикл обработки сообщений:
while (GetMessage(&msg, NULL, 0, 0)) //извлекает сообщения для любого окна
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
//обрабатывает быстрые клавиши для команд меню // То есть эта функция на первом этапе производит обработку
//сообщений и создает сообщения более крупные по группе. Нам уже //можно не анализировать, что нажата и отпущена клавиша, а просто //можно обрабатывать сообщение "введена буква". Каждый процесс //должен использовать эту функцию для обработки сообщений.
{
TranslateMessage(&msg);
// - переводит сообщения формата виртуальных клавиш в //сообщения символы
DispatchMessage(&msg);
// - распределяет сообщение оконной процедуре
}
}
closesocket(sServerListen);
WSACleanup(); // завершает использование данного DLL и //прерывает обращение к функциям WinSock. При удачном выполнении //вернется ноль.
return (int) msg.wParam;
}
Основное будет происходить в функции WndProc. Начало функции, где нужно добавить код, показано в листинге:
//ФУНКЦИЯ: WndProc(HWND, UINT, WPARAM, LPARAM)
//НАЗНАЧЕНИЕ: обрабатывает сообщения в главном окне.
//Обработка сетевых сообщений в функции WndProc
// WM_COMMAND |
- обработка меню приложения |
// WM_PAINT - закрасить главное окно |
|
// WM_DESTROY |
- ввести сообщение о выходе и вернуться. |
78
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;
79
// другие возможности: case WM_COMMAND:
wmId = LOWORD(wParam); wmEvent = HIWORD(wParam);
// Разобрать выбор в меню: switch (wmId)
{
case IDM_ABOUT: DialogBox(hInst,
MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About); break;
case IDM_EXIT: DestroyWindow(hWnd); break;
default:
return DefWindowProc(hWnd, message, wParam,
lParam);
}
break; case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: добавьте любой код отрисовки...
EndPaint(hWnd, &ps); break;
case WM_DESTROY: PostQuitMessage(0); break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
Здесь в самом начале добавлен оператор case, который проверяет, равно ли пойманное сообщение искомому сетевому сообщению WM_USER+1. Если это сетевое событие, то запускается перебор сетевых событий с помощью оператора switch, который сравнивает указанное в скобках значение с поступающими событиями:
switch (WSAGETSELECTEVENT(lParam))
Как известно, в параметре lParam находятся код ошибки и тип события. Чтобы получить событие, используется функция WSAGETSELECTEVENT. А затем проверяются необходимые нам события.
80
Если произошло соединение со стороны клиента, то выполняется следующий код:
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. В
этом случае читаются пришедшие данные (recv), а клиенту посылается текст "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;
По событию FD_WRITE ничего не происходит, а только стоит комментарий. По событию FD_CLOSE закрывается сокет.
Рассмотренный пример с использованием функции select может работать только с одним клиентом. Чтобы добавить возможность одновременной обработки нескольких соединений, необходимо сформировать массив потоков, используемых для приема/передачи данных.
