ПрограммированиеСтатьиСеть

On-line игры: взаимодействие с сервером (на примере игры MOBL от компании K-D LAB).

Автор:


(2game || !2game) ? doGame() : playGame()

Краткое предисловие
Оn-line игры, их типы. Функциональность сервера
Программирование взаимодействия с сервером
  Реализация взаимодействия: игровой клиент
Заключение
Приложение А: кратко о проекте MOBL
Приложение B. Глоссарий.

Краткое предисловие

или небольшое неформальное введение в тему...

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

Из всего множества современных игр мы выделим и подвергнем небольшому анализу on-line игры. Если быть еще более точными, то мы с Вами разберем общие вопросы взаимодействия между однопользовательским игровым клиентом и игровым сервером. В качестве наглядного примера рассмотрим реализацию подобного общения в "симуляторе жизни" MOBL (официальная страница продукта) компании K-D LAB. MOBL представляет собой WEB приложение, в котором серверная часть реализована на JSP, а клиентом выступает апплет.

Автор, избегая приведения <однозначных> рецептов вида: <чтобы получить ЭТО, сделайте ТО>, старался выдержать данную статью в стиле <как это делалось>, рассматривая один из возможных вариантов решения поставленной задачи. В процессе изложения не заостряется особое внимание на вопросах конкретной реализации, однако большинство приводимых примеров потребуют от читателя некоторых знаний языка программирования Java в целом и технологии JSP, в частности.

Оn-line игры, их типы. Функциональность сервера

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

Под on-line игрой мы будем понимать небольшую (порядка нескольких сотен килобайт) игровую программу, которая загружается с WWW-сервера, и требующая наличия соединения (возможно, непостоянного) со своим игровым сервером. Понятно, что чем меньше размер нашей игры, тем лучше, так как это позволяет увеличить количество наших потенциальных игроков за счет тех пользователей, которые являются "счастливыми" обладателями достаточно медленных каналов связи. Игра может быть реализована в виде апплета (Java) или при помощи Flash (Macromedia). Никаких ограничений на жанровую принадлежность не накладывается - наша игра может быть как небольшой обычной аркадой, так и стильной мозгодробильной головоломкой. On-line игры делятся на однопользовательские (игрок может играть только самостоятельно) и многопользовательские (игрок играет совместно с другими пользователями или против других игроков). Обращаю Ваше внимание на то, что подобное разделение игр, в общем случае, никак не влияет на количество одновременно играющих пользователей на одном игровом сервере.

Игровым сервером назовём [url=#webapp]WEB-приложение[/url], которое является программной реализацией следующей функциональности:

1. первичная регистрация пользователя- игрока в системе;
2. проведение проверок регистрационных данных при подключении зарегистрированных игроков;
3. взаимодействие с игровым клиентом (в основном, обмен требуемой для игры информацией, в соответствии с заданным внутренним протоколом);
4. хранение всей необходимой для игроков и игр информации в некоторой БД и предоставление доступа к ней.

На "плечи" сервера могут быть возложены и другие задачи. Реализация сервера, понятное дело, может быть выполнена на любом известном вам языке программирования и с дополнительным применением полезных технологий и будет представлять собою набор серверных сценариев (JSP, ASP, etc.)

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

Программирование взаимодействия с сервером

Приступим же к рассмотрению вопросов реализации (здесь и далее будет использоваться Java/JSP). В общем случае происходит следующее:

1. пользователь регистрируется на сервере;
2. зарегистрированный пользователь авторизуется на специальной страничке доступа к игре;
3. сервер подготавливает клиента-игру для сеанса;
4. игра, с компьютера клиента, время от времени ведет информационный обмен с сервером.

Регистрация пользователя. Проверка регистрационных данных.

Наш MOBL-сервер является обычным WEB-приложением. Было решено сделать регистрацию игроков и проверку регистрационных данных (при открытии пользователем игровой сессии) независимой от самого игрового клиента. Пользователю нужно заполнить регистрационную форму (см. Листинг 1; также см. формы HTML). Проверка корректности вводимых данных осуществляется на клиенте при помощи javascript-a. Такая же проверка осуществляется и при получении данных формы серверным сценарием регистрации. Отмечу, что приводимый в листинге код сильно упрощен и сокращен. Сделано это было для того, чтобы не загромождать изложение деталями JSP-реализации. Серверный сценарий регистрации, в случае корректности полученных регистрационных данных, осуществляет добавление нового игрока в свою базу данных. Кстати, применение СУБД и, как следствие, использование SQL для работы с информацией в БД, не только упрощает процесс разработки, но и повышает переносимость кода.

ЛИСТИНГ 1.
Регистрационная форма игрока

<form name="RegForm" action="reg" method="post" onSubmit="return ParseRegForm();">
   <table align="center" cellpadding="0" cellspacing="0" border="0">
     <tr>
       <td align="right"><font size=3>Ник:</font></td>
       <td><input class="in" type="text" SIZE="10" name="nick" maxlength="20"></td>
     </tr>

     <!-- ... здесь определяются другие поля для регистрации ... -->

     <tr>
       <td></td>
       <td><input class="on" type="submit" value="Зарегистрироваться!"></td>
     </tr>
   </table>
</form>

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

Взаимодействие клиента и сервера. Определение протокола.

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

Начав разработку WEB реализации, было бы логичным рассмотреть взаимодействие между клиентом и сервером, основываясь на применении HTTP-протокола (RFC2616). Это позволяет не задумываться о всевозможных сетевых программно-аппаратных конфигурациях, через которые могут <пролетать> наши пакеты. В результате мы получаем возможность играть с любой машины, на которой есть установленный и нормально настроенный для WEB-сёрфинга браузер.

Исходя из вышесказанного, получаем: (*)
· запросы от игровых клиентов обрабатываются серверными сценариями;
· вся требуемая информация передается сценариям через набор параметров. В частности, т.к. HTTP-протокол является пассивным, каждое обращение нашего игрового клиента должно передавать на сервер данные о текущем игроке;
· серверные ответы формируются в plain-тексте (в заранее определенном нами формате), и разбираются на клиенте. Сервер и клиент "знают" о требуемом формате сообщений и в каждом конкретном случае способны корректно обработать их.

MOBL-клиент является Java-апплетом, требующим для нормального функционирования игры периодического подключения к серверу для загрузки/сохранения игровой информации. Апплет должен знать о своем текущем игроке. Следовательно, сервер должен каким-то образом предоставить данную информацию апплету (желательно побеспокоиться о том, чтобы пароль не <летал> в открытом виде, в противном случае велика вероятность того, что Вам чего-то да <поломают>). К счастью, нам не нужно в данном вопросе изобретать велосипед и воспользуемся стандартным решением - передача параметров в апплет (см. Листинг 2). Кстати, использование элемента APPLET уже устарело, вместо него рекомендуется использовать элемент OBJECT.

Имеем следующее: после удачной регистрации, сервер генерирует <на лету> HTML-страничку, в которой есть информация о самой игре (апплет), и данные текущего игрока, переданные в апплет в качестве параметров. Так как во время создания второй версии MOBL-а была включена поддержка русского языка (первая версия была исключительно английской), в апплет также передается информация о выбранном пользователем языке.

ЛИСТИНГ 2.
Передача регистрационных данных в апплет

<applet name="Mobl" width="640" height="480"
  codebase=".." archive="mobl.jar" code="com.kdlab.mobl.applet.Mobl.class"
>

   <!-- тут были другие параметры -->

   <param name="nick" value="тут должен быть Ник игрока">
   <param name="password" value="понятное дело, что пароль, закрытый">

   <param name="lang" value="или RU, или EN">

</applet>

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

Реализация взаимодействия: серверная часть.

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

Кратко рассмотрим функциональность этих сценариев, на примере кода сценария создания новой игры и генерации стартовых параметров (см. Листинг 3). В начале каждый сценарий должен осуществить проверку правильности переданных ему регистрационных данных (отмечено на листинге при помощи reg-check). Если регистрационные данные корректны, сценарий начинает выполнение запрошенной функциональности, в нашем примере - простая генерация стартовых параметров для новой игры (create). Затем сценарий формирует ответ для клиента (answer). Отметим, что из-за использования HTTP, каждый ответ игрового сервера (выдаваемый обычным plain-текстом) будет дополняться со стороны WEB-контейнера стандартным HTTP-заголовком. Поэтому, для облегчения разбора серверного ответа на клиентской стороне, начало вывода серверного ответа предваряется `init`-тэгом, обозначающим место окончания HTTP-заголовка (init).

Реализация функциональности серверных сценариев для сохранения/загрузки выполняется аналогично рассмотренному выше материалу.

ЛИСТИНГ 3.
Создание новой игры и генерация стартовых параметров

<%

   // init: начнем формирование нашего ответа с `init`-тэга...
   out.print(AppSettings.skstrInitTag);

   // reg-check:
   String
     strNick = request.getParameter("nick"),
     strPswd = request.getParameter("password");

   // осуществим проверку регистрационных данных
   if ( !RegistrationBean.sbCheckLogin(strNick, strPswd) ) {
     Log.svError("MoblInit", "bad login [" + strNick + "]/[" + strPswd + "]");
     out.print(AppSettings.skstrAnswerBadLogin); // "BAD_LOGIN"

     return;
   }
   Log.svLog("MoblInit", "param request by [" + strNick + "]");

   // create:
   int 
     iGameID = -1,
     iRnd = srnd.nextInt(); // генерилка псевдо-случайных чисел !!! ссылка на класс

   if ( -1 == (iGameID = GameBean.siMakeGame("nick"), iRnd)) ) {
     Log.svError("MoblInit", "can't create game...");
     out.print(AppSettings.skstrAnswerFAIL); // "FAIL"

     return;
   }
   Log.svLog("MoblInit", 
             "game#" + iGameID + " has been created; [" + strNick + "], RND = " + iRnd);

   // answer:
   // AppSettings.skstrAnswerOK == "OK", AppSettings.skchTagDelim == ","

   out.println(
       AppSettings.skstrAnswerOK
     + AppSettings.skchTagDelim + AppSettings.siMaxQuant //ограничение по времени
     + AppSettings.skchTagDelim + AppSettings.siMaxRules //ограничение по количеству правил
     + AppSettings.skchTagDelim + iGameID // внутренний идентификатор созданной игры
     + AppSettings.skchTagDelim + iRnd // главный стартовый параметр для игры
   );

%> 

Реализация взаимодействия: игровой клиент

Взаимодействие игрового клиента с сервером, в нашем случае, можно разделить на несколько небольших задач:

1. перед началом любой игры нужно попросить сервер сгенерировать нам стартовые параметры игры;
2. в случае удачного окончания игры, её конфигурация передается на сервер для сохранения;
3. игровой клиент также должен уметь загружать с сервера сохраненные игры (страница "лучшие игры").

Определимся же с тем, КАК мы будет общаться с игровым сервером. Исходя из спецификации Java Security (для желающих - небольшой FAQ), следует, что мы можем установить соединение из апплета только с тем сервером, с которого был вытянут этот самый апплет. Будем использовать JDK 1.х, потому как в браузерах по умолчанию поддерживается именно она, поддержка же более свежих версий появляется после установки дополнительных плагинов. В пакете java.net есть всё, что нам может понадобиться для связи с сервером, а именно: достаточно полезный класс URL, с не менее полезным методом openConnection(). На основании этого класса сделаем свою небольшую обертку для HTTP-соединения (см. Листинг 4).

Конечно же, можно было бы сделать и собственную реализацию работы с сервером через класс Socket, но это порождает дополнительные проблемы.

ЛИСТИНГ 4.
Класс-обертка для установления соединения с определенным хостом,
передачи параметров методом POST и получением результирующего потока-ответа

import java.io.*;
import java.net.*;

public class HTTPConnection {
   private String strServer;
   private String strScript;
   private String strParams;

   public HTTPConnection(String strURL) {
     this(strURL, false);
   }

   public HTTPConnection(String strURL, boolean bIsTrim) {
     int 
       iiSlash = strURL.indexOf('/', "http://".length()) + 1,
       int iiParam = strURL.lastIndexOf('?') + 1;

     strServer = strURL.substring(0, iiSlash);
     strScript = strURL.substring(iiSlash, iiParam - 1);
     strParams = strURL.substring(iiParam);
   }

   public InputStream isOpenStream() throws UnknownHostException, IOException {
     // соединимся
     URLConnection conn = new URL(strServer + strScript).openConnection();
     conn.setDoOutput(true);
     conn.setUseCaches(false);
     conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
     conn.setRequestProperty("Content-Length", "" + strParams.length());

     // посылаем данные
     OutputStream out = conn.getOutputStream();
     out.write(strParams.getBytes());
     out.flush();
     out.close();

     // получим поток с ответом
     return conn.getInputStream();
   }
} 

Оставшаяся работа — сущие пустяки. Продемонстрируем это на примере загрузки с сервера начальных параметров для новой игры (см. Листинг 5). Для начала нам нужно сформировать строку адреса, для подключения к соответствующему серверному сценарию (address). Метод getCodeBase() возвращает реальный базовый путь к апплету, что позволяет нам не задумываться над сохранением адреса сервера в игровом апплете. Нам важно лишь знать имя и параметры нужного нам server-side script (хранится в константах класса AppSettings, в частности, skstrScriptInitURL - имя сценария для создания новой игры на сервере и получения её стартовых параметров). После этого воспользуемся функциональностью класса из Листинга 4 и пошлем запрос серверу и пробуем получить поток с ответом сервера.

Если ошибок в предыдущих наших действиях не было, начнем получать ответ (receive). Сразу после этого проведем отсечение ненужного нам HTTP-заголовка (skip-header), а заодно и проверку полученного ответа (checks), в соответствии с определенными до этого протоколом и допустимыми ответами со стороны сервера.

Дальнейшее же действо - простая загрузка полученных данных и инициализация внутренних переменных игрового энжина (loading).

ЛИСТИНГ 5.
Загрузка с сервера начальных параметров для новой игры

public int iLoadDataFromServer() {
   // address:
   InputStream isInitParams = null;

   try {
     String strURL = 
         getCodeBase()
       + AppSettings.skstrScriptInitURL
       + "?" + AppSettings.skstrScriptParamNick + "=" + username
       + "&" + AppSettings.skstrScriptParamPswd + "=" + password;

     HTTPConnection srv = new HTTPConnection(strURL);
     isInitParams = srv.isOpenStream();
   } catch (Exception ex) {
     return -1;
   }

   // receive: 
   StringBuffer strbuf = new StringBuffer();
   try {
     int iCurByte = 0;
     while (-1 != (iCurByte = isInitParams.read()))
       strbuf.append((char) iCurByte);
   }
   catch (IOException ioex) {
     return -1;
   }
   String strAnswer = strbuf.toString();

   // skip-header:
   strAnswer = strAnswer.substring(
     strAnswer.indexOf(
         AppSettings.skstrInitTag)
       + AppSettings.skstrInitTag.length()
     );

   // checks:
   if (strAnswer.startsWith(AppSettings. ...)) {
     return -1;
   }
   // ...

   // loading:
   StringTokenizer strtok = new StringTokenizer(
     strAnswer, 
     AppSettings.skchTagDelim
   );
   String strCurTok = null;
   strtok.nextToken(); // skip `OK`

   // Quants...
   try {
     strCurTok = strtok.nextToken();
     iMaxQuant = Integer.parseInt(strCurTok);
   }
   catch (NumberFormatException nfex) {
   }
   if (iMaxQuant < skiMaxQuant)
     iMaxQuant = skiMaxQuant;

   // ...

   return 0;
}

Заключение

Из вышесказанного видно, что проектирование протокола взаимодействия между клиентом и сервером, а также соответствующая реализация, не является очень сложным занятием. Резюмируя, повторю основные идеи, изложенные в статье:
· выделите задачи, функциональность которых будет реализована в рамках разрабатываемого сервера; определитесь с нужными клиенту данными для работы с сервером;
· определите протокол взаимодействия между клиентом и сервером как минимально необходимое множество сообщений и ответов на них, а также формат сообщений (используйте для этого информацию, полученную в результате выполнения предыдущего пункта);
· проведите анализ доступных технологий, которые могут быть использованы (не обязательно в полной мере) в проекте. Начинайте разрабатывать что-либо своё "с нуля" только в том случае, если Вы действительно не можете воспользоваться уже существующими разработками;
· приступайте к реализации :) Будьте бдительны - велика вероятность того, что Вы не смогли с самого начала предусмотреть и учесть все возможные нюансы Вашего проекта. Помните, что чем раньше Вы начнете вносить изменения в систему, а не ставить временные закладки, надеясь на будущий "авось!", - тем лучше!

УДАЧИ!

PS:
Конечно же, в рамках данной статьи, можно было более подробно рассмотреть много других тем. Например, такой аспект разработки игр как "запись демок", или, говоря другими словами, ведение подробного журнала событий игры, в котором сохраняются все действия игрока, а также независящие изменения игровой вселенной.

Но это уже совсем другая история...


Приложение А: кратко о проекте MOBL

Чтобы не повторяться, просто приведу эту ссылку, для интересующихся историей MOBL-а. Здесь же я просто сообщу несколько фактов:

· первоначальная реализация была сделана на С++;
· первая on-line версия была сделана на Java (игровой апплет) и C++ (CGI-scripts, Win32 platform);
· текущая (вторая по счету) версия является полностью переписанной первой версией, портированной на Java. В частности, сервер был реализован на JSP. Также, во время портирования, были сделаны многие дополнительные изменения и улучшения.


Приложение B. Глоссарий.

Java Server Pages (JSP) - независимая от платформы технология, предлагаемая в SUN's J2EE. Альтернативная методика разработки приложений, динамически генерирующих ответ по запросам клиента

Login - информация, используемая для однозначной идентификации пользователей системы. Состоит из двух элементов, собственно, логина - уникального имени пользователя и пароля.

SQL - структурированный язык запросов. Используется для работы с БД

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

WEB-приложение - приложение, построенное для работы в сети Internet. Представляет собой набор взаимосвязанных скриптов (сценариев), работающих под управлением WEB-контейнера.

База данных (БД) - множество взаимосвязанной структурированной информации. Так, в роли БД, может выступать и набор текстовых файлов, хранящих нужную информацию)

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

Серверный сценарий (скрипт, страница) - набор инструкций для обработки получаемого запроса с целью генерации ответа.

СУБД - Система Управления Базами Данных. Например, Oracle.


07-19 сентября 2002, г. Симферополь.

#java, #JSP, #веб, #клиент, #онлайн игры, #сервер

16 февраля 2002 (Обновление: 17 июня 2009)