netlib.narod.ru< Назад | Оглавление | Далее >

Работа с сервером

Наконец-то начинается настоящее развлечение! Первый этап реальной работы с сетью — это создание сервера. Сервер действует как центральный процессор вашей сетевой игры. Все игроки подключаются к серверу через клиентское приложение и начинаемтся передача данных туда-сюда.

Сервер поддерживает синхронизацию данных и оповещает игроков о текущем состоянии игры. Хотя для небольших сетевых игр это, возможно, не самый быстрый метод, для крупномасштабных игр он является наилучшим, и поэтому я использую его в главе 15.

Вот завершенная функция, которая создает объект сервера и инициализирует его, устанавливая функцию обработки сообщений (пока очень простую), создает компоненты адреса и структуру данных сессии, а затем выполняет специальный вызов для открытия игровой сессии. В результате вызова этой функции вы получете указатель на сетевой объект сервера.

// GUID сервера
GUID AppGUID = { 0xede9493e, 0x6ac8, 0x4f15,
               { 0x8d, 0x1, 0x8b, 0x16, 0x32, 0x0, 0xb9, 0x66 } };

// Прототип обработчика сообщений
HRESULT WINAPI ServerMsgHandler(PVOID pvUserContext,
                                DWORD dwMessageId, PVOID pMsgBuffer);

IDirectPlay8Server *StartNetworkServer(
                  char  *szSessionName, // Имя сессии (в ANSI)
                  char  *szPassword,    // Используемый пароль
                                        // (NULL если нет)
                  DWORD dwPort,         // Используемый порт
                  DWORD dwMaxPlayers)   // Максимальное количество
                                        //игроков
{
    IDirectPlay8Server  *pDPServer;
    IDirectPlay8Address *pDPAddress;

    DPN_APPLICATION_DESC dpad;

    WCHAR wszSessionName[256];
    WCHAR wszPassword[256];

    // Создание и инициализация объекта сервера
    if(FAILED(DirectPlay8Create(&IID_IDirectPlay8Server,
                                (void**)&pDPServer, NULL)))
        return NULL;
    if(FAILED(pDPServer->Initialize(pDPServer,
                                    ServerMsgHandler, 0))) {
        pDPServer->Release();
        return NULL;
    }

    // Создание объекта адреса, установка поставщика услуг
    // и порта
    if(FAILED(DirectPlay8AddressCreate(
          &IID_IDirectPlay8Address, (void**)&pDPAddress, NULL))) {
        pDPServer->Release();
        return NULL;
    }
    pDPAddress->SetSP(&CLDID_DP8SP_TCPIP);
    pDPAddress->AddComponent(DPNA_KEY_PORT, &dwPort,
                             sizeof(DWORD), DPNA_DATATYPE_DWORD);

    // Установка данных сессии
    ZeroMemory(&dpad, sizeof(DPNA_APPLICATION_DESC));
    dpad.dwSize  = sizeof(DPNA_APPLICATION_DESC);
    dpad.dwFlags = DPNSESSION_CLIENT_SERVER;

    // Установка имени сессии с преобразованием ANSI в Unicode
    mbstowcs(wszSessionName, szSessionName,
             strlen(szSessionName)+1);
    dpad.pwszSessionName = wszSessionName;

    // Установка пароля (если надо)
    if(szPassword != NULL) {
        mbstowcs(wszPassword, szPassword, strlen(szPassword)+1);
        dpad.pwszPassword = wszPassword;
        dpad.dwFlags |= DPNSESSION_REQUIREPASSWORD;
    }

    // Установка максимальногоколичества игроков
    dpad.dwMaxPlayers = dwMaxPlayers;

Здесь я на секунду остановлюсь и познакомлю вас со специальной функцией узла, которая открывает сетевую сессию объекта сервера:

HRESULT IDirectPlay8Server::Host(
     const DPN_APPLICATION_DESC *const pdnAppDesc,         // Данные сессии
     IDirectPlay8Address **const prgpDeviceInfo,           // Объект адреса
     const DWORD cDeviceInfo,                              // 1
     const DPN_SECURITY_DESC *const pdpSecurity,           // NULL
     const DPN_SECURITY_CREDENTIALS *const pdpCredentials, // NULL
     VOID *const dwPlayerContext,                          // NULL
     const DWORD dwFlags);                                 // 0

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

    if(FAILED(pDPServer->Host(&dpad, &pDPAddress, 1,
                              NULL, NULL, NULL, 0))) {
        pDPAddress->Release();
        pDPServer->Release();
        return NULL;
    }
    // Освобождаем объект адреса - он больше не требуется
    pDPAddress->Release();

    // Возвращаем объект сервера
    return pDPServer;
}

// Функция серверного обработчика сообщений с размещенной на месте
// и готовой к употреблению конструкцией switch...case для сообщений
HRESULT WINAPI ServerMsgHandler(PVOID pvUserContext,
                                DWORD dwMessageId, PVOID pMsgBuffer)
{
    // Определите здесь структуры сообщений DPNMSG_* 

    // Указатель на вызывающий объект сервера передается через
    // пользовательский указатель при вызове Initialize.
    IDirectPlay8Server *pDPServer;
    pDPServer = (IDirectPlay8Server*)pvUserContext;

    switch(dwMessageId) {
        // Добавьте здесь инструкции case для различных
        // типов сообщений, например:
        // case DPN_MSGID_CREATE_PLAYER:
        // DPNMSG_CREATE_PLAYER dpcp;
        // dpcp = (DPNMSG_CREATE_PLAYER*)pMsgBuffer;
        // Делайте, что вам надо с этими данными и, когда закончите,
        // верните флаг успеха.
        // return S_OK;
    }
    return E_FAIL;
}

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

Поддержка игроков

При запуске сервера одним из первых вы получите сообщение о создании игрока. Первый игрок всегда создается для главного узла. Другие игроки могут приходить и уходить, а игрок главного узла остается в течение всей сессии.

Работа с сообщениями о создании игрока

Сообщение о создании игрока определено как DPN_MSGID_CREATE_PLAYER, и буфер сообщения приводится к типу структуры DPNMSG_CREATE_PLAYER, объявление которой выглядит так:

typedef struct _DPNMSG_CREATE_PLAYER {
     DWORD dwSize;          // Размер структуры
     DWORD dpnidPlayer;     // Идентификатор игрока
     PVOID pvPlayerContext; // Указатель на данные контекста игрока
} DPNMSG_CREATE_PLAYER;

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

ПРИМЕЧАНИЕ
Контекст игрока (player context) — это информация, которую вы используете для описания игрока в вашем приложении. Это может быть указатель на структуру, экземпляр класса или буфер данных, который содержит имя игрока, уровень здоровья, возраст, текущее оружие, броню и т.д.
Предоставляя DirectPlay указатель на этот контекст игрока, вы обеспечиваете DirectPlay возможность в дальнейшем предоставить вам способ быстрого доступа к данным. Это избавляет от необходимости перебирать весь список игроков, ища требуемый идентификатор, когда время поджимает!

Как видите, в структуре DPNMSG_CREATE_PLAYER ряд данных отсутствует, в частности, имя игрока. Это работа отдельной функции, с которой вы познакомитесь в следующем разделе, «Получение имени игрока». Сейчас вы устанавливаете контекст игрока, что осуществляется простым приведением типа указателя:

DPNMSG_CREATE_PLAYER pCreatePlayer;
pCreatePlayer->pvPlayerContext = (PVOID)ContextDataPtr;

Конечно, ContextDataPtr — это указатель на что-то, что вы используете для хранения данных игрока. Чтобы связать единственную структуру из массива структур, содержащих внутриигровую информацию об игроках, передайте в виде контекста указатель на структуру, как показано в следующем примере:

typedef struct {
    char  szPlayerName[32]; // Имя игрока
    DWORD dwXPos, dwYPos;   // Координаты игрока
} sPlayerInfo;
sPlayerInfo Players[100]; // Место для 100 игроков

// Внутри обрабатывающей сообщения конструкции
// switch...case в секции создания игрока:
pCreatePlayer->pvPlayerContext = (PVOID)&sPlayerInfo[1];

Получение имени игрока

У игрока есть связанное с ним имя и вы должны суметь получить эту информацию, чтобы использовать в игре (кто хочет, чтобы его называли по номеру?). Это работа функции IDirectPlayer8Server::GetClientInfo:

HRESULT IDirectPlay8Server::GetClientInfo(
     const DPNID dpnid,                     // Идентификатор игрока
     DPN_PLAYER_INFO *const pdpnPlayerInfo, // Структура данных игрока
     DWORD *const pdwSize,                  // Размер предыдущей структуры
     const DWORD dFlags);                   // 0

И снова вы имеете дело со структурой данных, которая может быть любого размера, поэтому вам сперва надо запросить правильный размер, выделить буфер, а затем получить структуру. Буфер данных — это форма структуры DPN_PLAYER_INFO, показанной ниже:

typedef struct _DPN_PLAYER_INFO {
     DWORD dwSize;        // Размер структуры
     DWORD dwInfoFlags;   // DPNINFO_NAME | DPNINFO_DATA
     PWSTR pwszName;      // Имя игрока (в Unicode)
     PVOID pvData;        // Указатель на данные игрока
     DWORD dwDataSize;    // Размер данных игрока
     DWORD dwPlayerFlags; // DPNPLAYER_LOCAL для локального игрока
                          // или DPNPLAYER_HOST для игрока главного узла
} DPN_PLAYER_INFO;

Вы посмотрели на магические параметры, а теперь давайте пойдем дальше и взглянем на процесс обработки сообщения о создании игрока и извлечения связанного с игроком имени:

HRESULT WINAPI ServerMsgHandler(PVOID pvUserContext,
                                DWORD dwMessageId, PVOID pMsgBuffer)
{
    IDirectPlay8Server *pDPServer;
    HRESULT hr;
    DPNMSG_CREATE_PLAYER *pCreatePlayer;
    DPN_PLAYER_INFO *dppi;
    DWORD dwSize;

    if((pDPServer = (IDirectPlay8Server*)pvUserContext)) == NULL)
        return E_FAIL;

    switch(dwMessageId) {
      case DPN_MSGID_CREATE_PLAYER:
        pCreatePlayer = (DPNMSG_CREATE_PLAYER*)pMsgBuffer;
        dwSize = 0;
        dppi = NULL;

        // Запрашиваем размер буфера данных
        hr = pDPServer->GetClientInfo(
                    pCreatePlayer->dpnidPlayer, dppi, &dwSize, 0);

        // Проверка ошибок - если это недопустимый игрок,
        // значит добавляется игрок главного узла (пропускаем его)
        if(FAILED(hr) && hr != DPNERR_BUFFERTOOSMALL) {
            if(hr == DPNERR_INVALIDPLAYER)
                break;
        }

        // Выделяем буфер данных и получаем информацию
        dppi = (DPN_PLAYER_INFO*)new BYTE[dwSize];
        ZeroMemory(dppi, sizeof(DPN_PLAYER_INFO);
        dppi.dwSize = sizeof(DPN_PLAYER_INFO);
        if(FAILED(pDPServer->GetClientInfo( \
                    pCreatePlayer->dpnidPlayer, dppi, &dwSize, 0))) {
            delete[] dppi;
            break;
        }

        // Теперь у нас есть информация игрока в структуре dppi.
        // Чтобы получить имя игрока в кодировке ANSI, обратитесь
        // к функции wcstombs.

        // Сейчас мы просто отображаем имя в окне сообщений.
        char szName[32];
        wcstombs(szName, dppi->pwszName, 32);
        MessageBox(NULL, szName, "Player Joined", MB_OK);

        // Избавляемся от буфера с данными игрока
        delete[] dppi;
        return S_OK;
    }
    return E_FAIL;
}

 

ВНИМАНИЕ!
Обратите внимание, что запрос получения данных игрока может закончиться неудачей не по вашей вине. Клиент может не установить структуру с информацией клиента и, следовательно, сервер не сможет получить ее.

Уничтожение игроков

Нет, вы не убиваете их в игре, но когда игрок разрывает соединение, вы получаете сообщение об этом. Создать сообщение также просто, как и в случае создания игрока. Буфер сообщения необходимо привести к типу структуры DPNMSG_DESTROY_PLAYER:

typedef struct _DPNMSG_DESTROY_PLAYER {
     DWORD dwSize;          // Размер структуры
     DPNID dpnidPlayer;     // Идентификатор удаляемого игрока
     PVOID pvPlayerContext; // Указатель на контекст игрока
     DWORD dwReason;        // Причина отключения
} DPNMSG_DESTROY_PLAYER;

Вы снова используете идентификатор игрока и указатель на его контекст, которые уже видели ранее. Вопросы вызывает только последнее поле, dwReason. Почему игрок покинул игру? Было ли это обычное завершение игры, или было внезапно разорвано соединение, может была прервана сессия или игрок был принудительно отключен? Каждой из этих причин соответствует макрос из перечисленных в таблице 5.8. Вы можете использовать значение в поле dwReason как считаете нужным.


Таблица 5.8. Причины отключения



Макрос Описание

DPNDESTROYPLAYERREASON_NORMAL Обычное отключение игрока.
DPNDESTROYPLAYERREASON_CONNECTIONLOST Отключение игрока из-за разрыва соединения.
DPNDESTROYPLAYERREASON_SESSIONTERMINATED Удаление игрока из-за завершения сессии.
DPNDESTROYPLAYERREASON_HOSTDESTROYPLAYER Принудительное удаление игрока сервером.


Для принудительного отключения игрока вы используете функцию IDirectPlay8Server::DestroyClient:

HRESULT IDirectPlay8Server::DestroyClient(
     const DPNID pdnidClient,        // Идентификатор игрока
     const void *const pDestroyInfo, // NULL
     const DWORD dwDestroyInfoSize,  // 0
     const DWORD dwFlags);           // 0

Здесь нет ничего нового — просто укажите идентификатор игрока. Вот как выполняется отключение:

pDPServer->DestroyClient(dpnidPlayerID, NULL, 0, 0);

Другой способ отключения игрока — завершение сессии. Об этом мы поговорми чуть позже в разделе «Завершение сессии на главном узле».

Получение данных

Игровые данные передаются в виде зависящих от приложения сообщений, но они всегда заключены в сообщения типа DPN_MSGID_RECEIVE, использующих структуры данных DPNMSG_RECEIVE:

typedef struct _DPNMSG_RECEIVE {
     DWORD dwSize;             // Размер структуры
     DPNID dpnidSender;        // Идентификатор отправителя
     PVOID pvPlayerContext;    // Указатель на контекст игрока
     PBYTE pReceiveData;       // Буфер принятых данных
     DWORD dwReceivedDataSize; // Размер принятых данных
     DPNHANDLE hBufferHandle;  // Дескриптор буфера данных
} DPNMSG_RECEIVE;

Чтобы обработать данные, обращайтесь к ним через указатель pReceiveData, используя, если необходимо, дескриптор памяти Windows hBufferHandle. Сперва это кажется бессмысленным, но в действительности гарантирует, что сообщение будет храниться в памяти, пока вы не будете готовы работать с данными.

В качестве примера предположим, что будет принято 16 байт данных. Эти данные представляют состояние игрока в игре. Для доступа к данным выполните приведение типа к указателю на структуру данных и работайте с данными, как показано в следующем примере:

#define MSG_PLAYERSTATE 0x101

typedef struct {
    DWORD dwType;
    DWORD dwXPos, dwYPos, dwHealth;
} sPlayerState;

// Обработчик сообщений:
HRESULT WINAPI ServerMsgHandler(PVOID pvUserContext,
                        DWORD dwMessageId, PVOID pMsgBuffer)
{
    IDirectPlay8Server *pDPServer;
    HRESULT hr;
    DPNMSG_RECEIVE *pReceive;
    sPlayerState *pState;

    if((pDPServer = (IDirectPlay8Server*)pvUserContext)) == NULL)
        return E_FAIL;

    switch(dwMessageId) {
      case DPN_MSGID_RECEIVE:
        pReceive = (DPNMSG_RECEIVE*)pMsgBuffer;

        // Приведение буфера данных к типу сообщения
        pState = (sPlayerState*)pReceive->pReceivedData;
        if(pState->dwType == MSG_PLAYERSTATE) {

            // Делаем что нам надо со структурой данных

        }
        return S_OK;
    }
    return E_FAIL;
}

Иногда поступает очень много сообщений и вы не можете обработать их все при получении. В таком случае можно поместить их в очередь. Когда вы закончите работать с данными в памяти, передайте дескриптор функции IDirectPlay8Server::ReturnBuffer:

HRESULT IDirectPlay8Server::ReturnBuffer(
     const DPNHANDLE hBufferHandle, // Дескриптор буфера
     const DWORD dwFlags);          // 0

Чтобы DirectPlay знал, что освобождать память не надо, после завершения обработки сообщения верните значение DPNSUCCESS_PENDING, вместо S_OK или E_FAIL.

Отправка сообщений сервера

Что хорошего в сети, которая не может передавать данные? Чтобы сервер отправил данные подключенному клиенту вам надо использовать функцию SendTo, которая отправляет данные отдельному игроку, всем игрокам сразу или игрокам, относящимся к указанной группе. Взгляните на прототип функции SendTo:

HRESULT IDirectPlay8Server::SendTo(
     const DPNID dpnid,              // Идентификатор игрока или группы,
                                     // которым отправляется сообщение.
                                     // Для отправки сообщения всем игрокам
                                     // используйте DPNID_ALL_PLAYERS_GROUP.
     const DPN_BUFFER_DESC *const pBufferDesc, // См. описание
     const DWORD cBufferDesc,        // 1
     const DWORD dwTimeOut,          // Время ожидания отправки сообщения
                                     // (в миллисекундах). 0 - если время
                                     // ожидания не задано.
     void *const pvAsyncContext,     // Предоставленный пользователем контекст
     DPNHANDLE *const phAsyncHandle, // NULL для синхронной операции
     const DWORD dwFlags);           // См. описание

Функция SendTo выглядит подавляюще. Вам необходимо учесть безопасность, метод доставки и регулировку потока. Вы должны указать идентификатор игрока, которому хотите отправить сообщение, а также указатель на структуру DPN_BUFFER_DESC. Определение этой простой структуры выглядит так:

typedef struct _DPN_BUFFER_DESC {
     DWORD dwBufferSize; // Размер передаваемых данных
     BYTE *pBufferData;  // Указатель на передаваемые данные
} DPN_BUFFER_DESC;

Здесь вы задаете размер и указатель на данные, которые хотите отправить. Затем в функции SendTo идет параметр cBufferDesc, которому присваивается значение 1. Далее в dwTimeOut устанавливается значение, определяющее период времени (в миллисекундах), который функция будет ждать, прежде чем вернет ошибку (со времени отправки данных). Если вы не хотите использовать эту возможность, просто укажите в данном параметре 0.

Аргумент pvAsyncContext — это задаваемый пользователем контекст, используемый для указания на информацию, которую вы хотите получить, когда операция отправки будет завершена. Он похож на контекст игрока, поскольку упрощает доступ к информации.

Для использования асинхронной отправки вы предоставляете аргумент phAsyncHandle, куда будет записан дескриптор, который позже можно использовать для отмены операции отправки. Осталось рассмотреть dwFlags. Взгляните на таблицу 5.9, где приведен список макросов, которые можно использовать для конструирования этого значения, с их описанием.


Таблица 5.9. Флаги поведения SendTo



Макрос Описание

DPNSEND_SYNC Синхронная отправка данных. Не возвращаем управление, пока данные не отправлены.
DPNSEND_NOCOPY Заставляет DirectPlay не делать внутреннюю копию отправляемых данных. Это самый эффективный метод отправки данных; однако отложенные данные могут быть изменены прежде чем DirectPlay получит шанс отправить их.
DPNSEND_NOCOMPLETE Указывает DirectPlay, что не надо уведомлять сервер, когда операция отправки завершена.
DPSEND_COMPLETEONPROCESS Заставляет DirectPlay отправлять сообщение DPN_MSGID_SEND_COMPLETE, когда данные отправлены и проверены системой на месте назначения. Это замедляет работу, но позволяет гарантировать, что данные доставлены. Вместе с этим флагом вы должны указывать флаг DPSEND_GUARANTEED.
DPSEND_GUARANTEED Использование гарантированной доставки.
DPNSEND_PRIORITY_HIGH Задает высокий приоритет для сообщения. Используется для отметки важных сообщений, которые должны быть пропущены через механизм фильтрации.
DPNSEND_PRIORITY_LOW Задает низкий приоритет для сообщения. Используйте этот флаг для отметки не слишком важных сообщений, которые могут быть отброшены механизмом фильтрации.
DPNSEND_NOLOOPBACK Подавляет сообщение DPN_MSGID_RECEIVE на сервере, если вы отправляете данные группе в которую входит игрок сервера.
DPNSEND_NONSEQUENCIAL Заставляет систему назначения принимать сообщения в том порядке, в котором они были отправлены (а не быть перепутанными из-за задержек в сети). Непоследовательный прием может замедлить работу удаленной стороны, поскольку потребуется отслеживать, буферизовать и переупорядочивать сетевые сообщения, чтобы они были доставлены в том порядке в котором их отправляли.


Типичная комбинация флагов — DPNSEND_NOLOOPBACK и DPNSEND_NOCOPY обеспечивает оптимальную производительность без вовлечения игрока сервера в отправляемые группам сообщения.

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

Вот снова инструкция switch...case, но теперь с отправкой информации:

DPNHANDLE g_hSendTo; // Асинхронный дескриптор для данных

switch(dwMessageId) {
  case DPN_MSGID_RECEIVE:
    pReceive = (DPNMSG_RECEIVE*)pMsgBuffer;

    // Приведение буфера данных к типу сообщения
    pState = (sPlayerState*)pReceive->pReceivedData;

    if(pState->dwType == MSG_PLAYERSTATE) {
        // Изменение данных
        pState->dwHealth += 10; // Увеличиваем здоровье

        // Приведение типа для отправки
        DPN_BUFFER_DESC dpbd;
        dpbd.dwSize = sizeof(sPlayerState);
        dpbd.pBufferData = pState;

        // Отправка с использованием внутреннего
        // метода копирования и без отправки уведомления.
        // Возвращает асинхронный дескриптор для остановки
        // операции отправки
        pDPServer->SendTo(pReceive->dpnidSender, &dpbd,
                1, 0, NULL, &g_hSendTo, DPNSEND_NOCOMPLETE);
    }
    return S_OK;
}

Чтобы удалить информацию до ее отправки (пока она ожидает в очереди отправки), используйте глобальный дескриптор следующим образом:

// Для отмены одной операции отправки используйте:
pDPServer->CancelAsyncOperation(g_hSendTo, 0);

// Для отмены всех отложенных операций используйте:
pDPServer->CancelAsyncOperation(NULL, DPNCANCEL_ALL_OPERATIONS);

Завершение сессии на главном узле

Когда сервер завершает работу, наступает время для остановки сессии, что прекращает все передачи и уничтожает всех игроков. Это делается с помощью следующей функции:

HRESULT IDirectPlay8Server::Close(
     const DWORD dwFlags);         // 0

 

ПРИМЕЧАНИЕ
Функция Close работает с сетевыми объектами всех моделей, так что вы не ограничены только сервером.

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

И, наконец, освободите все используемые COM-объекты; в данном случае это только объект сервера:

pDPServer->Release();

netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz