Введение
Этапы создания приложения Реализация простейшей программы Заключение |
Сейчас в Интернете, книгах, журналах и других разнообразных изданиях очень мало пишут о DirectX, поэтому желающий использовать возможности DirectX в своих программах попросту не может найти информации. Удается найти материалы, посвященные Direct3D и DirectInput, но о других частях DirectX, таких, скажем, как DirectPlay, нет практически ничего.
Цель этой статьи – хоть как-то поправить такое положение и описать основные аспекты создания сетевых приложений, использующих DirectX.
Перечислим основные этапы создания сетевого приложения на основе DirectPlay:
Последний этап – разрыв соединения, очищение памяти, корректное завершение программы.
Для примера, описанного в статье, взята архитектура peer-to-peer, по-другому – клиент-клиент. Она рекомендуется для применения при соединении не более 2 пользователей, однако, в документации по DirectX максимальное количество пользователей определяется в диапазоне от 20 до 30. При создании такого соединения каждый из клиентов должен послать соответствующие сообщения другим клиентам сети, причем каждому лично. Именно эта особенность и сказывается на скорости передачи данных, делая ее сравнительно небольшой.
Как вы уже, наверно, заметили из сказанного выше, для организации соединения архитектуры peer to peer необходимо создать хост. В данном случае этот хост будет отвечать только за добавление новых игроков в уже созданную сессию, то есть хост будет передавать новому клиенту адреса уже подсоединенных к сессии компьютеров, а тем, в свою очередь, адрес нового клиента, создавая тем самым связь между всеми участниками сессии.
СОВЕТ Можно также построить приложение в соответствии с требованиями архитектуры «клиент – сервер» (Client/Server). При этом в сети должен быть компьютер (сервер), к которому будут подключаться клиенты. В отличие от хоста в архитектуре peer to peer, сервер будет выполнять вычисления для всех клиентов и вся информация, передающаяся из клиентского приложения и в клиентское приложение, будет проходить через сервер и обрабатываться им. Главным требованием при использовании такой архитектуры является наличие достаточно мощного для решения этой задачи компьютера, исполняющего задачи сервера. |
Как и в других компонентах DirectX, программирование DirectPlay основывано на использовании COM интерфейсов. Этот подход является очень удобным, так как все действия, производимые в процессе выполнения программы, осуществляются через указатели на определенные интерфейсы. Так, например, для каждой архитектуры, будь то клиент – клиент или клиент – сервер, существует главный интерфейс, через указатель на который и осуществляется работа приложения. Необходимо также отметить и вспомогательные интерфейсы, играющие немаловажную роль.
Главным интерфейсом для приложений, использующих архитектуру клиент – клиент является IDirectPlay8Peer. Естественно, для того чтобы им воспользоваться, необходимо использовать заголовочный файл dplay8.h. (Заметьте, что имя интерфейсов DirectX начинается на «I»). Включим в код программы определение указателя на этот интерфейс:
// Главный интерфейс Peer to Peer(при создании делается равным NULL)
IDirectPlay8Peer* lpPeer = NULL;
|
Именно через функции этого интерфейса и будут происходить основные действия. Но для начала разберем функцию, создающую сам COM объект:
CoCreateInstance (CLSID_DirectPlay8Peer, // Идентификатор класса объекта NULL, // должен быть установлен в NULL CLSCTX_INPROC_SERVER, // Указывает на то, что объект// реализован как DLL. IID_IDirectPlay8Peer, // Идентификатор интерфейса (LPVOID*) &lpPeer); // Ранее объявленный указатель на// интерфейс |
После создания объект необходимо инициализировать, причем функция инициализации должна быть использована первой из всех функций интерфейса
lpPeer ->Initialize(PVOID // Информация о пользователе(обычно делается равной NULL)const pvUserContext, // Имя Функции, получающей сообщения от других клиентовconst PFNDPNMESSAGEHANDLER pfn, // Спецификация соединения (обычно делается равной NULL)const DWORD dwFlags); |
Особенно здесь нужно обратить внимание на второй параметр, так как именно эта функция будет отвечать за обработку всех сообщений после успешного соединения. Прототип этой функции следует объявить в самом начале кода, а сама она должна выглядеть примерно так (имя функции можно выбирать произвольно, главное чтобы тип и число аргументов совпадали бы с описанными ниже):
HRESULT WINAPI DirectPlayMessageHandler(PVOID pvUserContext,
DWORD dwMessageId, PVOID pMsgBuffer)
{
return S_OK;
}
|
Тело функции обычно представляет собой большой switch(), который обрабатывает поступающие в функцию сообщения, идентификатор которых равен dwMessageId. Функция обрабатывает данные, на которые указывает параметр pMsgBuffer.
Теперь необходимо получить список провайдеров и устройств, через которое можно будет создать подключение. Ее можно решить, используя в программе следующую функцию:
lpPeer ->EnumServiceProviders( const GUID *const pguidServiceProvider, const GUID *const pguidApplication, const DPN_SERVICE_PROVIDER_INFO *const pSPInfoBuffer, DWORD *const pcbEnumData, DWORD *const pcReturned, const DWORD dwFlags); |
Первый аргумент является указателем на GUID провайдеров, информацию о которых мы хотим получить.
Ниже приведена таблица, в которой рассмотрены провайдеры и соответствующие им идентификаторы:
Название провайдера | Идентификатор |
---|---|
Протокол TCP/IP | CLSID_DP8SP_TCPIP |
Протокол IPX | CLSID_DP8SP_IPX |
Модем | CLSID_DP8SP_MODEM |
Соединение через Serial кабель | CLSID_DP8SP_SERIAL |
BlueToth (Новинка) | CLSID_DP8SP_BLUETOOTH |
Симулятор сети TCP/IP | CLSID_NETWORKSIMULATOR_DP8SP_TCPIP |
Второй аргумент является указателем на GUID приложения. Если этот параметр указан, то функция будет перечислять только тех провайдеров, которые могут устанавливать соединения с данным приложением. Если же этот аргумент равен NULL, то функция перечислит всех зарегистрированных провайдеров. Третьим параметром является указатель на структуру типа DPN_SERVICE_PROVIDER_INFO. Об этой структуре мы будем говорить ниже, а пока замечу, что после выполнения функции этот указатель будет указывать на первую из массива структур, а в каждую из этих структур будет записана информация, идентифицирующая одного из провайдеров.
Четвёртым параметром является указатель на буфер, в который будет записана вся требуемая информация о каждом из провайдеров. При этом в первом двойном слове этого буфера при вызове функции должен быть записан размер этого буфера. В том случае, если размер буфера слишком мал, функция возвращает значение DPNERR_BUFFERTOOSMALL и в первое двойное слово буфера записывает размер, необходимый для размещения всей информации.
Следующий, пятый параметр указывает на двойное слово, в которое должно быть записано число провайдеров, информацию о которых мы хотим получить. И, наконец, в качестве последнего аргумента может быть указан NULL или флаг DPNENUMSERVICEPROVIDERS_ALL. В последнем случае перечисляться будут в том числе и провайдеры, которые в момент вызова функции недоступны, и устройства для взаимодействия с которыми в системе не установлены. В большинстве случаев этот флаг не используется.
К моменту вызова функции требуемый размер буфера и число провайдеров могут быть неизвестны. Поэтому обычно функцию перечисления провайденров используют следующим образом. При первом вызове функции указывают в первом двойном слове буфера записывают нуль, то есть указывают что размер буфера равен нулю. При этом функция возвращает DPNENUMSERVICEPROVIDERS_ALL и в первое двойное слово буфера записывает необходимый размер буфера. После этого выделяется буфер необходимого размера, и функция вызывается второй раз. После второго вызова функции мы получаем всю необходимую для работы информацию. В MSDN можно найти пример подобного использования функции. Я привожу этот пример ниже:
DPN_SERVICE_PROVIDER_INFO* pdnSPInfo = NULL; DPN_SERVICE_PROVIDER_INFO* pdnSPInfoEnum = NULL; DWORD dwItems = 0; DWORD dwSize = 0; // Determine the required buffer size hr = g_pDP->EnumServiceProviders(NULL, NULL, NULL, &dwSize, &dwItems, 0); pdnSPInfo = (DPN_SERVICE_PROVIDER_INFO*) new BYTE[dwSize]; //Fill the buffer with service provider information hr = g_pDP->EnumServiceProviders(NULL, NULL, pdnSPInfo, &dwSize, &dwItems, 0) // Print the provider descriptions pdnSPInfoEnum = pdnSPInfo; for(DWORD i = 0; i < dwItems; i++) { printf("Found Service Provider: %S\n", pdnSPInfoEnum->pwszName); pdnSPInfoEnum++; } |
Тип DPN_SERVICE_PROVIDER_INFO описан следующим образом:
typedef struct _DPN_SERVICE_PROVIDER_INFO { DWORD dwFlags; // Флаг использования симулятора сети GUID guid; // Идентификатор провайдера WCHAR *pwszName; // Имя провайдера PVOID pvReserved; // Зарезервирован – должен быть NULL DWORD dwReserved; // Зарезервирован – должен быть NULL } DPN_SERVICE_PROVIDER_INFO, *PDPN_SERVICE_PROVIDER_INFO; |
Таким образом, эти данные позволяют однозначно идентифицировать всех провайдеров.
Разобранная нами функция носит универсальный характер, то есть с помощью ее можно выполнять несколько полезных действий, а не только получать список провайдеров. Используя ее по-разному, можно получить список устройств, проверить в рабочем ли состоянии находится провайдер. Подробнее об этом можно узнать из подраздела «Практика».
Итак, появляется вопрос, как создать хост или подключиться к выбранному хосту, используя то или иное устройство. Для этих целей используются два метода, вызываемые через указатель на интерфейс IDirectPlay8Peer. Первый из них, создающий хост, описан следующим образом:
// Создание хоста HRESULT Host( // Специальная структура, описывающая приложениеconst DPN_APPLICATION_DESC *const pdnAppDesc, // Указатель на интерфейс (Адрес устройства) IDirectPlay8Address **const prgpDeviceInfo, // Указывает на количество объектов в rgpDeviceInfoconst DWORD cDeviceInfo, // Зарезервировано - NULLconst DPN_SECURITY_DESC *const pdpSecurity, // Зарезервировано - NULLconst DPN_SECURITY_CREDENTIALS *const pdpCredentials, // Необязательный параметр – увеличивается при присоединение клиента VOID *const pvPlayerContext, // Флагconst DWORD dwFlags); |
Рассмотрим параметры по порядку. Первым аргументом является специальная структура DPN_APPLICATION_DESC, многочисленные поля которой характеризуют создаваемую сессию:
typedef struct _DPN_APPLICATION_DESC { DWORD dwSize; DWORD dwFlags; GUID guidInstance; GUID guidApplication; DWORD dwMaxPlayers; DWORD dwCurrentPlayers; WCHAR *pwszSessionName; WCHAR *pwszPassword; PVOID pvReservedData; DWORD dwReservedDataSize; PVOID pvApplicationReservedData; DWORD dwApplicationReservedDataSize; } DPN_APPLICATION_DESC, *PDPN_APPLICATION_DESC; |
Сразу необходимо отметить, что большинству полей этой структуры редко присваивают значения, отличные от принятых по умолчанию. Итак, первое поле должно содержать размер структуры в байтах, вычисляемый функцией sizeof(). Второе поле указывает, какой именно тип сессии будет создан, чаще всего используются следующие значения:
Третье поле структуры должно содержать GUID, который генерирует DirectPlay при запуске сессии. Однако для метода Host это поле значения не имеет, так как этот метод игнорирует данное поле.
В четвертый параметр нужно передать GUID приложения, который необходимо индивидуально сгенерировать для приложения.
Пятое поле, dwMaxPlayers, содержит максимальное число клиентов.
Следующее за ним поле dwCurrentPlayers содержит текущее количество пользователей в сессии, это поле не нужно заполнять самостоятельно.
Седьмое поле, pwszSessionName, должно содержать имя сессии, например, полученное от пользователя.
Наконец, в восьмое поле pwszPassword передается пароль для доступа к сессии. Нужно отметить, что пароль используется в том случае, если выставлен флаг DPNSESSION_REQUIREPASSWORD.
Остальные же четыре поля этой структуры зарезервированы и не используются.
Со вторым параметром дело обстоит куда сложнее. Как видно, здесь требуется указатель на массив вспомогательных объектов, реализующих интерфейс IDirectPlay8Address. Применительно к методу Host этот массив должен содержать адреса устройств, которые будут использоваться для хостинга приложений. DirectPlay предоставляет стандартную реализацию вспомогательного объекта, реализующего IDirectPlay8Address. Его CLSID – CLSID_DirectPlay8Address.
Третий параметр функции Host() – это двойное слово, в которое необходимо передать количество объектов в массиве cDeviceInfo, то есть количество устройств.
Четвертый и пятый параметр – это указатели на структуры DPN_SECURITY_DESC и DPN_SECURITY_CREDENTIALS, которые зарезервированы для дальнейшего использования.
Шестой параметр функции – флаг. Единственное значение, которое он может принимать – DPNHOST_OKTOQUERYFORADDRESSING. Оно означает, что если пользователь ввел недостаточно информации для создания хоста, появится диалоговое окно, позволяющее завершить настройку.
О вспомогательном интерфейсе IDirectPlay8Address необходимо рассказать подробнее. Перед началом работы с объектом, реализующим этот интерфейс, нужно указать, для какого провайдера будет создан адрес устройствa. Сделать это можно с помощью метода SetSP. При этом провайдер задается с помощью GUID-а.
HRESULT SetSP( // Возможные варианты GUID’ов приведены в таблице 1const GUID *const pguidSP); |
Идентификатор устройства задается методом SetDevice.
HRESULT SetDevice( // Идентификатор устройства, полученный фун-ей EnumServiceProvidersconst GUID *const pguidDevice); |
Стоит разобрать еще один метод рассматриваемого интерфейса, AddComponent. Он требуется для создания адреса хоста. Таким образом, интерфейс IDirectPlay8Address предназначен не только для того, чтобы сообщать адрес устройства, но и для того, чтобы создать адрес хоста, к которому будет происходить подключение. Указание адреса хоста понадобится в функции Connect, но об этом чуть позже. Итак, AddComponent описан таким образом:
// Объявление указателя на интерфейс IDirectPlay8Address – lpHostAdsress // ... // Создание COM объекта, вызов требуемых функций // ... HRESULT AddComponent( // назначаем тип – будь то порт, тел. номер...const WCHAR * const pwszName, // буфер, откуда будет взята информацияconstvoid * const lpvData, // размер буфераconst DWORD dwDataSize, // тип буфераconst DWORD dwDataType); |
В качестве первого параметра должен быть указан тип информации, передаваемой функции. Если, например, пользователь выбрал соединение через модем и хочет соединиться с уже созданным хостом, то, естественно, он должен каким-то образом указать не только тип устанавливаемого соединения, но и телефон, по которому модем должен позвонить на компьютер, выполняющий обязанности хоста. В этом случае нужно в первом аргументе указать DPNA_KEY_PHONENUMBER. Это значение означает, что функции будет передан номер телефона. Так, при соединении по протоколу TCP/IP, нужно передать IP-адрес. Сам же телефон или IP-адрес должен находиться в буфере, на который указывает второй параметр, lpvData. Третий параметр должен содержать размер буфера в байтах. Последний параметр должен содержать тип буфера, при этом чаще всего используются буферы строкового (DPNA_DATATYPE_STRING) и числового (DPNA_DATATYPE_DWORD) типа.
Теперь пришло время ознакомиться с функцией Connect интерфейса IDirectPlay8Peer. Интуитивно понятно, что она предназначена для установления соединения с хостом. Приведем ее (довольно длинное) описание:
HRESULT Connect( // Структура, описывающая сетевое приложение (см. выше)const DPN_APPLICATION_DESC *const pdnAppDesc, // Адрес хоста IDirectPlay8Address *const pHostAddr, // Адрес устройства IDirectPlay8Address *const pDeviceInfo, // Зарезервировано - NULLconst DPN_SECURITY_DESC *const pdnSecurity, // Зарезервировано - NULLconst DPN_SECURITY_CREDENTIALS *const pdnCredentials, // Информация о соединяющемся пользователе (Опционально - NULL)constvoid *const pvUserConnectData, // Размер pvUserConnectDataconst DWORD dwUserConnectDataSize, // Опционально - NULLvoid *const pvPlayerContext, // Опционально - NULLvoid *const pvAsyncContext, // Хендл DPNHANDLE *const phAsyncHandle, // Флагconst DWORD dwFlags ); |
Три первых и последний аргумент совпадают с первыми тремя аргументами метода Host , остальные в большинстве случаев устанавливаются в NULL. Следует обратить внимание на флаг.
Последний из рассматриваемых в этом разделе статьи методов, SetPeerInfo, совершает важное действие, связанное с тем, чтобы передать информацию о пользователе в приложение и соответствующим образом обработать ее:
HRESULT SetPeerInfo( // Структура, характеризующая пользователяconst DPN_PLAYER_INFO * const pdpnPlayerInfo, // Ставится чаще всего в NULL PVOID const pvAsyncContext, // Хендл DPNHANDLE *const phAsyncHandle, // Флагconst DWORD dwFlags ); |
Здесь для начала нужно разобрать первый аргумент, представляющий собой указатель на структуру DPN_PLAYER_INFO, содержащую информацию о пользователе. Она содержит описанные ниже поля:
typedef struct _DPN_PLAYER_INFO { // Размер структуры DWORD dwSize; // Флаг DWORD dwInfoFlags; // Имя пользователя PWSTR pwszName; // Информация, описывающая пользователя(буфер) PVOID pvData; // Размер буфера DWORD dwDataSize; // Дополнительная информация DWORD dwPlayerFlags; } DPN_PLAYER_INFO, *PDPN_PLAYER_INFO; |
В большинстве случаев используются поля dwSize – размер структуры, dwInfoFlags и pwszName – поле, которому следует присвоить имя пользователя. Причем dwInfoFlags может равняться DPNINFO_NAME, тогда информация об имени пользователя будет взята из pwszName, или DPNINFO_DATA, тогда будет использоваться буфер pvData, размер которого нужно записать в поле dwDataSize. Также следует уделить особое внимание флагу dwPlayerFlags, который принимает одно из следующих значений: DPNPLAYER_HOST – указывает что структура содержит информацию о хосте, и DPNPLAYER_LOCAL – структура содержит информацию об обычном пользователе.
Второй параметр метода SetPeerInfo() используется при асинхронном обмене информацией о пользователях. Этот параметр связан с сообщением DPN_MSGID_ASYNC_OP_COMPLETE, о котором пойдет речь во второй части статьи. При создании сессии этому параметру присваивается значение NULL.
Насчет третьего параметра нужно отметить, что это своеобразный указатель на асинхронное действие. В нашем случае при создании сессии в этот параметр передается NULL.
Ну и наконец, четвертый параметр, dwFlags, представляет собой флаг, способный принимать единственное значение DPNSETPEERINFOSET_SYNC, означающее синхронное действие.
ПРЕДУПРЕЖДЕНИЕ Для обработки ошибок, а также в отладочных целях удобно использовать макрос DXTRACE_ERR_MSGBOX(<Текст сообщения>,<Код ошибки>), который выводит диалоговое окно с информацией об ошибке. Этот макрос объявлен в файле dxerr9.h. |
В этом разделе будет по шагам рассмотрен процесс написания программы с использованием функций, описанных выше. Всю программу описывать нет смысла, поэтому будут приведены только ее основные функции. Итак, начнем:
Рисунок 1.
// Идентификатор главного окна HWND hDirPlay; // Основной интерфейс DirectPlay IDirectPlay8Peer * g_pDP = NULL; // Проверка, будет ли создан сервер BOOL bHost = TRUE; // Имя игрока TCHAR chPlayerName[MAX_PATH]; // Имя сессии TCHAR chSessionName[MAX_PATH]; // Идентификатор выбранного провайдера GUID * guidServiceProviderSelect; // Идентификатор выбранного устройства GUID * guidSPDevice; // GUID приложения – сгенерируйте с помощью // инструмента Create GUID в Visual Studiostaticconst GUID ApplicationGuid = ...; |
HRESULT InitDirectPlay(HWND hDirPlay) { // g_pDP – указатель на интерфейс IDirectPlay8Peer. HRESULT hr = S_OK; CoInitialize(NULL); //Создание основного объекта DirectPlay hr = CoCreateInstance( CLSID_DirectPlay8Peer, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Peer, (LPVOID*)&g_pDP); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при создании объекта"),hr); return hr; } //Инициализация DirectPlay hr = g_pDP->Initialize(NULL, DirectPlayMessageHandler, 0); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при инициализации"),hr); return hr; } return S_OK; } |
При завершении приложения необходимо вызвать CoUninitialize() для прекращения работы с COM.
struct ArrayHelper { ArrayHelper(BYTE * array){ m_array = array; } ArrayHelper(BYTE * array) { if(m_array) delete[] m_array; } BYTE * m_array; }; // Передаем в функцию только идентификатор окна HRESULT FindListProvider(HWND hDirPlay) { // bHost и guidSPDevice должны быть определены до вызова функции// g_pDP – указатель на интерфейс IDirectPlay8Peer. HRESULT hr = S_OK; // Переменная цикла DWORD = 0; // Основное хранилище данных о провайдерах DPN_SERVICE_PROVIDER_INFO * pdnSPInfo = NULL; // Резервное хранилище данных о провайдерах DPN_SERVICE_PROVIDER_INFO * pdnSPInfoEnum = NULL; // Количество провайдеров DWORD dwItems = 0; // Размер буфера в байтах DWORD dwSize = 0; // Промежуточная строчка (иия провайдера для вывода) TCHAR strBuf[MAX_PATH]= {0}; // Положение в списке провайдеровint nIndex = 0; // Находим число провайдеров и размер буфера hr = g_pDP->EnumServiceProviders(NULL, NULL, NULL, &dwSize, &dwItems, NULL); // Обрабатываем, если произошла ошибкаif( FAILED(hr) && hr != DPNERR_BUFFERTOOSMALL) { DXTRACE_ERR_MSGBOX(_T("Error FindListProvider"),hr); return hr; } // Создаем массив полученного размера для хранения данных о провайдерах pdnSPInfo = (DPN_SERVICE_PROVIDER_INFO*) new BYTE[dwSize]; // Контроль памяти, занятой под массив ArrayHelper SPInfoHelper(pdnSPInfo); // Записываем в созданный массив данные всех провайдеров hr = g_pDP->EnumServiceProviders(NULL, NULL, pdnSPInfo, &dwSize, &dwItems, NULL); if(FAILED(hr)) { // Обрабатываем возможные ошибки DXTRACE_ERR_MSGBOX(_T("Error FindListProvider"),hr); return hr; } // Цикл, выводящий найденнных провайдеров на экран// Копируем в резервную структуру pdnSPInfoEnum = pdnSPInfo; // Создаем циклfor(DWORD i = 0; i < dwItems; i++) { // Конвертируем в strBuf имя провайдера DXUtil_ConvertWideStringToGenericCch(strBuf, pdnSPInfoEnum->pwszName, MAX_PATH ); // Добавляем в список IDC_LISTSP nIndex = (int)SendDlgItemMessage( hDirPlay, IDC_LISTSP, LB_ADDSTRING, 0, (LPARAM) strBuf ); // Записываем GUID в список с названиями GUID * pGuid = new GUID; if(NULL == pGuid) { DXTRACE_ERR_MSGBOX(_T("Не хватает памяти"), E_OUTOFMEMORY); return E_OUTOFMEMORY; } *pGuid = pdnSPInfoEnum->guid; // Добавляем в IDC_LISTSP SendDlgItemMessage(hDirPlay, IDC_LISTSP, LB_SETITEMDATA, nIndex, (LPARAM)&Guid); //Переходим на следующего найденного провайдера pdnSPInfoEnum++; } // Если провайдеров не найдено...if(0 == dwItems) DXTRACE_ERR_MSGBOX(_T("Провайдеры не найдены"),0); return S_OK; } |
HRESULT EnumAdapterSP(HWND hDlg, const GUID* pGuidSP) { // bHost и guidSPDevice должны быть определены до вызова функции// g_pDP – указатель на интерфейс IDirectPlay8Peer. HRESULT hr = S_OK; // Информация о провайдере DPN_SERVICE_PROVIDER_INFO * pdnSPInfo = NULL; DWORD dwItems = 0; // Количество провайдеров DWORD dwSize = 0; // Размер для создания массиваint nIndex; // Положение в списке//Строковый буфер для хранения имени устройства TCHAR strName[MAX_PATH]; //Обнуляем содержимое элемента SendDlgItemMessage(hDlg, IDC_COMBODEVICE, CB_RESETCONTENT, 0, 0); // Получить размер для массива от провайдера// Заметьте, первый аргумент заполнен(!!!) hr = g_pDP->EnumServiceProviders(pGuidSP, NULL, NULL, &dwSize, &dwItems, 0); // Обрабатываем ошибкиif(FAILED(hr) && hr != DPNERR_BUFFERTOOSMALL) { DXTRACE_ERR_MSGBOX(_T("Failed Enumerating Service Providers"),hr); return hr; } // Создаем массив полученного размера для хранения данных об устройствах pdnSPInfo = (DPN_SERVICE_PROVIDER_INFO*) new BYTE[dwSize]; // Контроль памяти, занятой под массив ArrayHelper SPInfoHelper(pdnSPInfo); // Заполняем массив hr = g_pDP->EnumServiceProviders(pGuidSP, NULL, pdnSPInfo, &dwSize, &dwItems, 0); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Failed Enumerating Service Providers"),hr); return hr; } // Резервные данные DPN_SERVICE_PROVIDER_INFO* pdnSPInfoEnum; pdnSPInfoEnum = pdnSPInfo; for (DWORD i = 0; i < dwItems; i++ ) // Пускаем цикл { // Конвертируем в strName имя устройства DXUtil_ConvertWideStringToGenericCch(strName, pdnSPInfoEnum->pwszName, MAX_PATH ); // Добавляем в список имя устройства nIndex = (int)SendDlgItemMessage(hDlg, IDC_COMBODEVICE, CB_ADDSTRING, 0, (LPARAM) strName ); // Добавляем в список идентификатор устройства GUID * pGuid = new GUID; if(NULL == pGuid) { DXTRACE_ERR_MSGBOX(_T("Не хватает памяти"), E_OUTOFMEMORY); return E_OUTOFMEMORY; } *pGuid = pdnSPInfoEnum->guid; SendDlgItemMessage(hDlg, IDC_COMBODEVICE, CB_SETITEMDATA, nIndex, (LPARAM)&Guid ); // Переходим к следующему элементу массива pdnSPInfoEnum++; } // Устанавливаем положение в списке (в нулевую позицию) SendDlgItemMessage(hDlg, IDC_COMBODEVICE,CB_SETCURSEL, 0, 0); return hr; } |
HRESULT SwitchCreateHost(HWND hDlg, GUID* pGuidSP) { // bHost и guidSPDevice должны быть определены до вызова функции// g_pDP – указатель на интерфейс IDirectPlay8Peer.if(pGuidSP == NULL) return E_POINTER; bHost = IsDlgButtonChecked(hDlg, IDC_CHECKHOST); // IDC_COMBODEVICE – элемент, содержащий список устройств// IDC_CHECKHOST – элемент управления CheckButton// IDC_PHONENAMEHOST –обычное текстовое поле, используется // для ввода телефона, если выбран провайдер MODEM, или имени хоста,// если выбран TCP/IP// IDC_LINE1 – метка, поясняющая IDC_PHONENAMEHOST// IDC_PORT – текстовое поле для ввода порта, в случае выбора TCP/IP// IDC_LINE2 – метка, поясняющая IDC_PORTif(*pGuidSP == CLSID_DP8SP_TCPIP) { EnableWindow(GetDlgItem(hDlg, IDC_COMBODEVICE), TRUE); EnableWindow(GetDlgItem(hDlg, IDOK), TRUE); EnableWindow(GetDlgItem(hDlg, IDC_CHECKHOST), TRUE); SetWindowText(hDlg, _T("TCP/IP - Вы выбрали")); SetDlgItemText(hDlg, IDC_LINE1, _T("Имя хоста:")); EnableWindow(GetDlgItem(hDlg, IDC_LINE1), !bHost); EnableWindow(GetDlgItem(hDlg, IDC_PHONENAMEHOST), !bHost); EnableWindow(GetDlgItem(hDlg, IDC_LINE2), TRUE); SetDlgItemText(hDlg,IDC_LINE2, _T("Порт:")); EnableWindow(GetDlgItem(hDlg, IDC_PORT), TRUE); } elseif(*pGuidSP == CLSID_DP8SP_IPX) { MessageBox( hDlg, _T("Выбраный вами протокол (IPX) НЕ ПОДДЕРЖИВАЕТСЯ"), _T("IPX"), MB_OK | MB_ICONQUESTION); } elseif(*pGuidSP == CLSID_DP8SP_SERIAL) { SetWindowText(hDlg, _T("SERIAL - Вы выбрали")); EnableWindow(GetDlgItem(hDlg,IDC_COMBODEVICE),TRUE); EnableWindow(GetDlgItem(hDlg,IDOK),TRUE); EnableWindow(GetDlgItem(hDlg,IDC_CHECKHOST),TRUE); SetDlgItemText(hDlg,IDC_LINE1,NULL); EnableWindow(GetDlgItem(hDlg,IDC_LINE1),FALSE); EnableWindow(GetDlgItem(hDlg,IDC_PHONENAMEHOST),FALSE); EnableWindow(GetDlgItem(hDlg,IDC_LINE2),FALSE); SetDlgItemText(hDlg,IDC_LINE2,NULL); EnableWindow(GetDlgItem(hDlg,IDC_PORT),FALSE); } elseif(*pGuidSP == CLSID_DP8SP_MODEM) { SetWindowText(hDlg, _T("MODEM - Вы выбрали")); EnableWindow(GetDlgItem(hDlg, IDC_COMBODEVICE), TRUE); EnableWindow(GetDlgItem(hDlg, IDOK), TRUE); EnableWindow(GetDlgItem(hDlg, IDC_CHECKHOST), TRUE); SetDlgItemText(hDlg,IDC_LINE1, _T("Телефон:")); EnableWindow(GetDlgItem(hDlg, IDC_LINE1), TRUE); EnableWindow(GetDlgItem(hDlg, IDC_PHONENAMEHOST), TRUE); EnableWindow(GetDlgItem(hDlg, IDC_LINE2), FALSE); SetDlgItemText(hDlg, IDC_LINE2, NULL); EnableWindow(GetDlgItem(hDlg, IDC_PORT), FALSE); } else { MessageBox( hDlg, _T("Выбранный Вами пункт не поддерживается"), _T("Произошла ошибка"), MB_OK | MB_ICONQUESTION); SetWindowText(hDlg, _T("Произошла ошибка")); SetDlgItemText(hDlg, IDC_LINE1, NULL); EnableWindow(GetDlgItem(hDlg, IDC_LINE1), FALSE); EnableWindow(GetDlgItem(hDlg, IDC_PHONENAMEHOST), FALSE); EnableWindow(GetDlgItem(hDlg, IDC_LINE2), FALSE); SetDlgItemText(hDlg, IDC_LINE2, NULL); EnableWindow(GetDlgItem(hDlg, IDC_PORT), FALSE); SendDlgItemMessage(hDlg, IDC_COMBODEVICE, CB_RESETCONTENT, 0, 0); EnableWindow(GetDlgItem(hDlg, IDC_COMBODEVICE), FALSE); EnableWindow(GetDlgItem(hDlg, IDOK), FALSE); EnableWindow(GetDlgItem(hDlg, IDC_CHECKHOST), FALSE); } return S_OK; } |
HRESULT JoinHostGame(HWND hDlg, GUID * pGuidSP, GUID* guidSPDevice, bool bHost) { // g_pDP – указатель на интерфейс IDirectPlay8Peer. HRESULT hr = S_OK; CComPtr<IDirectPlay8Address> spDeviceAddress; // Адрес устройства CComPtr<IDirectPlay8Address> spHostAddress; // Адрес хоста// Начало инициализации адреса устройства hr = CoCreateInstance(CLSID_DirectPlay8Address, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Address, (void**)&spDeviceAddress); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при инициализации устройства"),hr); return hr; } // Назначение идентификатора провайдера устройству hr = spDeviceAddress->SetSP(pGuidSP); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при назначении провайдера устройству"),hr); return hr; } // Задаем тип устройства hr = spDeviceAddress->SetDevice(guidSPDevice); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при назначении устройства"),hr); return hr; } // Если пользователь хочет подключиться к уже готовому хосту, начинаем// инициализацию подключения к удаленному хосту.if(!bHost) { // Создание адреса хоста hr = CoCreateInstance(CLSID_DirectPlay8Address, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Address, (void**)&spHostAddress); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при инициализации хоста"),hr); return hr; } // Задаем идентификатор провайдера hr = spHostAddress->SetSP(pGuidSP); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при назначении адреса хоста"),hr); return hr; } } // ------------------------------------------------------------------------// Если соединение производится по TCP/IP...if(*pGuidSP == CLSID_DP8SP_TCPIP) { TCHAR strHostname[MAX_PATH]; TCHAR strPort[MAX_PATH]; GetDlgItemText(hDlg, IDC_PHONENAMEHOST, strHostname, MAX_PATH); GetDlgItemText(hDlg, IDC_PORT, strPort, MAX_PATH); // Если пользователь хочет создать новый хост.if(bHost) { if(_tcslen(strPort) > 0) { // Добавляем порт к spDeviceAddress DWORD dwPort = _ttoi(strPort); hr = spDeviceAddress->AddComponent(DPNA_KEY_PORT, &dwPort, sizeof(dwPort), DPNA_DATATYPE_DWORD); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Извините, ошибка при назначении порта"),hr); return hr; } } } else { // Если пользователь хочет подключиться к уже готовому хосту, начинаем// инициализацию подключения к удаленному хосту.// Добавляем имя хоста (то есть его IP-адрес) к spHostAddressif(_tcslen(strHostname) > 0) { WCHAR wstrHostname[MAX_PATH]; DXUtil_ConvertGenericStringToWideCch(wstrHostname, strHostname, MAX_PATH); hr = spHostAddress->AddComponent(DPNA_KEY_HOSTNAME, wstrHostname, (DWORD) (wcslen(wstrHostname) + 1) * sizeof(WCHAR), DPNA_DATATYPE_STRING); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при назначении IP-адреса хоста"),hr); return hr; } } if(_tcslen(strPort) > 0) { // Добавляем порт к spDeviceAddress DWORD dwPort = _ttoi(strPort); hr = spHostAddress->AddComponent(DPNA_KEY_PORT, &dwPort, sizeof(dwPort), DPNA_DATATYPE_DWORD); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Извините, ошибка при назначении порта"),hr); return hr; } } } } // ------------------------------------------------------------------------// Если соединение производится через модем...if(*pGuidSP == CLSID_DP8SP_MODEM) { // Получаем значение телефонного номера TCHAR tchPhoneNumber[MAX_PATH]; GetDlgItemText(hDlg, IDC_PHONENAMEHOST, tchPhoneNumber, MAX_PATH); // Если пользователь хочет подключиться к имеющемуся хостуif(!bHost) { // Если телефонный номер задан...if(_tcslen(tchPhoneNumber) > 0) { WCHAR wchPhoneNumber[MAX_PATH]; // Конвертация из TCHAR в WCHAR DXUtil_ConvertGenericStringToWideCch(wchPhoneNumber, tchPhoneNumber, MAX_PATH); // Добавляем номер телефона к spDeviceAddress hr = spHostAddress->AddComponent(DPNA_KEY_PHONENUMBER, wchPhoneNumber, (DWORD)(wcslen(wchPhoneNumber) + 1) * sizeof(WCHAR), DPNA_DATATYPE_STRING); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при инициализации телефонного номера"),hr); return hr; } } } } // ------------------------------------------------------------------------// Если соединение производится по протоколу SERIAL...if(*pGuidSP == CLSID_DP8SP_SERIAL) { MessageBox(hDlg, _T("Извините, данный протокол не обрабатывается"), _T("DirectSendPlay"), MB_OK | MB_ICONQUESTION); } // ------------------------------------------------------------------------// Если соединение производится по протоколу IPX...if(*pGuidSP == CLSID_DP8SP_IPX) { MessageBox(hDlg, _T("Извините, данный протокол не обрабатывается"), _T("DirectSendPlay"), MB_OK | MB_ICONQUESTION); } // ------------------------------------------------------------------------// Заполнение стуктур DPN_PLAYER_INFO, DPN_APPLICATION_DESC GetDlgItemText(hDlg, IDC_EDIT1, chPlayerName, MAX_PATH); WCHAR wchNamePlayer[MAX_PATH]; // Преобразование из TCHAR в WCHAR DXUtil_ConvertGenericStringToWideCch(wchNamePlayer, chPlayerName, MAX_PATH); // Структура, описывающая игрока DPN_PLAYER_INFO dpPlayerInfo; // Обнуляем содержимое структуры ZeroMemory(&dpPlayerInfo, sizeof(DPN_PLAYER_INFO)); // Задаем размер структуры dpPlayerInfo.dwSize = sizeof(DPN_PLAYER_INFO); // Задаем дополнительные флаги dpPlayerInfo.dwInfoFlags = DPNINFO_NAME; // Задаем имя игрока dpPlayerInfo.pwszName = wchNamePlayer; // Задаем информацию об игроке hr = g_pDP->SetPeerInfo(&dpPlayerInfo, NULL, NULL, DPNOP_SYNC); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при передаче информации о игроке"),hr); return hr; } // Структура, описывающая сетевое приложение DPN_APPLICATION_DESC dpnAppDesc; ZeroMemory(&dpnAppDesc, sizeof(DPN_APPLICATION_DESC)); dpnAppDesc.dwSize = sizeof(DPN_APPLICATION_DESC); dpnAppDesc.dwFlags = DPNSESSION_NODPNSVR; // Задаем GUID приложения dpnAppDesc.guidApplication = ApplicationGuid; dpnAppDesc.guidInstance = GUID_NULL; // Имя сессии пока инициализируется значением NULL dpnAppDesc.pwszSessionName = NULL; // -------------------------------------------------------------------------// Создание хостаif(bHost) { DWORD dwHostFlags = 0; // Считываем имя сессии GetDlgItemText(hDlg, IDC_NAMESESSION, chSessionName, MAX_PATH); // Если строка не пустаif(_tcslen(chSessionName) > 0) { WCHAR wchSessionName[MAX_PATH]; DXUtil_ConvertGenericStringToWideCch(wchSessionName, chSessionName, MAX_PATH); // Задаем имя сессии dpnAppDesc.pwszSessionName = wchSessionName; } // Назначаем хост сессии hr = g_pDP->Host( // Задаем описание приложения &dpnAppDesc, // Задаем адрес устройства &spDeviceAddress, // Указываем, что устройство одно 1, NULL, NULL, NULL, // Указываем, что не нужно выводить стандартное окно.// Если нужно, чтобы это окно показывалось, // нужно указать флаг DPNHOST_OKTOQUERYFORADDRESSING. 0); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при назначении хоста"),hr); return hr; } } // ------------------------------------------------------------------------// Соединение с уже созданным хостомelse { DWORD dwConnectFlags = 0; // Установка соединения hr = g_pDP->Connect(&dpnAppDesc, // Адрес хоста spHostAddress, // Адрес устройства, через которое производится соединение spDeviceAddress, NULL, NULL, NULL, // указываем, что в параметре pvUserConnectData данные отсутствуют 0, NULL, NULL, NULL, dwConnectFlags); if(FAILED(hr)) { DXTRACE_ERR_MSGBOX(_T("Ошибка при назначении соединения"),hr); return hr; } } return(hr); } |
HRESULT WINAPI DirectPlayMessageHandler( PVOID pvUserContext, DWORD dwMessageId, PVOID pMsgBuffer ) { switch(dwMessageId) { case DPN_MSGID_CONNECT_COMPLETE: { PDPNMSG_CONNECT_COMPLETE pConnectCompleteMsg; pConnectCompleteMsg=(PDPNMSG_CONNECT_COMPLETE)pMsgBuffer; if(FAILED(pConnectCompleteMsg->hResultCode)) { DXTRACE_ERR_MSGBOX(_T("DPN_MSGID_CONNECT_COMPLETE"), pConnectCompleteMsg->hResultCode ); break; } else MessageBox(hDirPlay,_T("Подключение прошло удачно"),_T("Проверка"),MB_OK); break; } } return S_OK; } |
Произведя все описанные выше действия, вы должны получить сетевое приложение, работающее с провайдерами TCP/IP и MODEM. Думаю, что написать функции обработки и создания соединения для других провайдеров не составит труда, если приложить немного усилий и прочитать DirectX Help for C++.
Вот и все, на этом первая часть статьи закончена. В заключение могу лишь отметить, что разработчики DirectPlay ставили перед собой задачу создать методы, позволяющие унифицировано работать с различными провайдерами, то есть создавать соединения с помощью различных средств по единому алгоритму. А теперь замечу, что мы рассмотрели только одну часть проблемы. Мы написали только ту часть программы, которая отвечает за инициализацию и установку соединения. Ту часть программы, которая будет отвечать за передачу данных, мы рассмотрим в следующей части статьи.