DirectX 9. Использование DirectPlay

Начало знакомства. Часть 1.

Автор: Михаил Новиков
Источник: RSDN Magazine #2-2003
Опубликовано: 12.07.2003
Версия текста: 1.0.1
Введение
Этапы создания приложения
Реализация простейшей программы
Об архитектуре
Описание интерфейсов и функций.
IDirectPlay8Address
Практика.
Заключение

Введение

Сейчас в Интернете, книгах, журналах и других разнообразных изданиях очень мало пишут о 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

О вспомогательном интерфейсе 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.

Листинг функции FindListProvider ()
        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;
}
Листинг функции EnumAdapterSP
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;
}
Функция JoinHostGame() – главная функция создания и инициализации хоста и коннекта.
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 ставили перед собой задачу создать методы, позволяющие унифицировано работать с различными провайдерами, то есть создавать соединения с помощью различных средств по единому алгоритму. А теперь замечу, что мы рассмотрели только одну часть проблемы. Мы написали только ту часть программы, которая отвечает за инициализацию и установку соединения. Ту часть программы, которая будет отвечать за передачу данных, мы рассмотрим в следующей части статьи.


Эта статья опубликована в журнале RSDN Magazine #2-2003. Информацию о журнале можно найти здесь