{{notification.text}}

MirGames

Наверняка, Вы играли в такую игру как «Warcraft 3». И было бы просто прекрасно, если Вы играли по интернету, ибо в этом случае Вы бы могли созерцать и испытать в действии то, что называется «Battle.net». В любом случае я поясню. Это некий “портал” благодаря которому игроки всего интернета могут запросто найти работающие игровые сервера не выходя из игры. Что значительно облегчает им жизнь, т.к. отпадает необходимость заранее договариваться с соперниками при помощи чатов и подобных средств…

То о чём я буду говорить в этой статье, поможет Вам создать подобное для своей игрушки. Сам метод достаточно прост и почти не имеет отрицательных моментов. Из-за отсутствия информации по данной теме мне пришлось самому, методом проб и ошибок, писать подобный портал (далее “арена”) для своего проекта TFK).

Описание метода
Итак, опишу то что нам понадобится для реализации.

  1. Хостинг с поддержкой php
  2. Ваша игра с работоспособным сетевым кодом

Первый пункт я надеюсь не вызывает больших проблем у начинающих “игрописателей”, т.к. существует множество сайтов, предоставляющих бесплатный домен с поддержкой php. К примеру, для TFK он был предоставлен сайтом mirgames.ru

А вот со вторым пунктом придётся немного попариться, впрочем, это уже тема для отдельной статьи…

Так как в данной статье я использовал PHP, то потребуется знание его основ. Впрочем, при желании, перевод на другой язык написания web страниц не составит большого труда.

Итак, имеем в интернете домен на котором размещён наш скрипт “арены”. Есть игра-клиент, которой нужно узнать кол-во доступных серверов, и при необходимости создать свой.

Что нам нужно от “арены”? Всего-навсего получить список серверов в виде “IP:Port IP:Port IP:Port...” и зарегистрировать новый.

Как это будет происходить? Да очень просто! Посредством HTTP запросов.

Так как нет идеальных решений, какие минусы у данного метода?
  1. Серверы находящиеся за шлюзом не будут видны остальным клиентам, т.к. даже сама игра-сервер без понятия на каком external порту она висит.
  2. При падении хостера (сайта) арена шлёпнется вместе с ним! Но это относится уже к форс-мажорным обстоятельствам… ;)

А какие же плюсы?
  1. Относительная простота реализации
  2. Легко разместить такую арену в локальной сети
  3. Не требует восстановления после различных ЧП :)

Реализация
В этом разделе описаны основные процедуры необходимые для воплощения нашей мечты в реальность. Работа с ареной делится на 2 части:
  • Подача HTTP запросов и обработка ответов игрой
  • Обработка запроса скриптом на арене

Всего будет 2 вида запросов: view и ping.

VIEW необходим для получения списка серверов. Будет выглядеть следующим образом:

Запрос:
http://host/?action=arena&mode=view

Ответ: 212.100.15.45:25666 192.10.38.212:25666

Т.е. в ответе мы видим, что на данный момент на арене находятся 2 сервера на портах 25666

PING для оповещения арены о том что сервер жив и удалять его из списка пока нет никакой необходимости. Вы могли заметить то, что нет запроса на регистрацию сервера на арене, т.к. в качестве регистрации выступает постоянный “ping” посылаемый им. Сам же запрос “ping” следует посылать раз в несколько десятков секунд (20-40)…

Запрос:
http://host/?action=arena&mode=ping&port=25666

Ответ нам абсолютно не нужен :)

Реализация на стороне игры
Соответственно нам теперь необходимо знать как отправить HTTP запрос и получить на него ответ. Всё проще чем может показаться. Приведу всего одну процедуру использующую возможности WinSock:
function Arena(const mode: string; get: boolean): string;
  const
   host = 'host.ru';
   port = 25666;
  var
   wData : WSADATA;
   addr  : sockaddr_in;
   sock  : integer;
   error : integer;
   buf   : array [0..1023] of Char;
   str   : string;
   phe   : PHostEnt;
  begin
  //Инициализация сокета
  Result := '';
  WSAStartup($0101, wData);
  phe := gethostbyname(PChar(string(host)));
  if phe = nil then
   begin
   WSACleanup;
   Exit;
   end;

  sock := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

  if sock = INVALID_SOCKET then
   begin
   WSACleanup;
   Exit;
   end;

  addr.sin_family := AF_INET;
  addr.sin_port   := htons(80);
  addr.sin_addr   := PInAddr(phe.h_addr_list^)^;

  error := connect(sock, addr, sizeof(addr));
  if error = SOCKET_ERROR then
   begin
   closesocket(sock);
   WSACleanup;
   Exit;
   end;

  // Составляем строку запроса
  str := 'GET http://' + host + '/?action=arena&mode=' + mode;
  if mode = 'ping' then
   str := str + '&port=' + IntToStr(port);
  str := str + ' HTTP/1.0'#13#10#13#10;

  // отправляем
  send(sock, str[1], Length(str), 0);

  // Если нужен ответ то принимаем
  if get then
   begin
   ZeroMemory(@buf, 1024);
   error := recv(sock, buf, 1024, 0);
   while error > 0 do
    begin
    Result := Result + Copy(buf, 0, error);
    error  := recv(sock, buf, 1024, 0);
    end;
   end;
  // Закрываем сокет – завершаем работу с сетью
  closesocket(sock);
  WSACleanup;
     
  // Вырезаем из ответа то что нам нужно, т.е. отрезаем HTTP заголовки
  if get and Result <> '' then
   Result := Copy(Result, pos(#13#10#13#10, Result) + 4, Length(Result));
  end;

В функцию передаётся всего 2 параметра mode и get.

Первый является именем запроса, а второй означает нужен ли нам результат обработки запроса. Соответственно вызов этой функции для наших двух запросов будет выглядеть следующим образом:
Str := Arena('view', true); // для получения списка серверов
  Arena('ping', false);       // сообщить арене что наш сервер живее живых

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

Приведу пример:
procedure Arena_PingThread;
  begin
  Arena('ping', false);
  end;
     
  procedure Arena_Ping;
  var
   id : DWORD;
  begin
  CreateThread(nil, 128, @Arena_PingThread, nil, 0, id);
  end;

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

Реализация на стороне интернет сервера
Итак, с игрой разобрались, теперь осталось написать скрипт!

В запросах мы посылаем ключевое слово “action=arena” благодаря чему помимо арены на данном домене может висеть полноценный сайт.

Для того, чтобы определить адресуется ли данный запрос арене, в index.php необходимо (желательно в самом начале) написать следующее:
if ($action == 'arena') {
   include 'arena.php';
   die();
  }

Это означает, что в случае того, когда захотят “пообщаться” с ареной, будет запущен скрипт арены для обработки запроса и дальнейшее выполнение скрипта index.php прекратится.

А вот и сам код arena.php:
<?php
   //В этом файле будет храниться список активных серверов
   $list_file = 'db/arena_list.txt';
   // Узнаём IP адрес отправителя запроса
   $ip = $_SERVER['REMOTE_ADDR'];
   // Читаем номер порта из запроса
   $port = intval($_REQUEST['port']);
   // Это от хитрых кулхацкеров ;)
   if (!($port >= 1024 && $port <= 65500))
    $port = 25666;
   // Читаем файл-список
   $lst = file($list_file);
   // В переменной $time теперь хранится текущее время
   $time = time();
   $j = -1;
   $i = 0;
   // Удаляем “мертвецов” и попутно ищем адрес отправителя в этом списке
   while ($i < count($lst)) {
    $lst[$i] = trim($lst[$i]);
    list($l_ip, $l_port, $l_time) = explode(":", $lst[$i]);
    // Если время с предыдущего пинга превысило 45 секунд – его явно уже нет
    if ($l_time < ($time - 45)) {
     for ($t = $i; $t < count($lst) - 1; $t++)
      $lst[$t] = $lst[$t + 1];
     unset($lst[count($lst) - 1]);
     continue;
    }
    if ($l_ip == $ip) $j = $i;
    $i++;
   }

   // Обработка запроса
   switch ($mode) {
    case 'view':
     for ($i = 0; $i < Count($lst); $i++) {
      // Вывод очередного IP:Port из списка
      list($l_ip, $l_port, $l_time) = explode(":", $lst[$i]);
      echo $l_ip.':'.$l_port.' ';
     }
     break;
    case 'ping':
     if ($j == -1)
      // Если пингуется впервые, значит новый сервер - добавляем
      array_push($lst, $ip.':'.$port.':'.$time);
     else {
      // Обновляем информацию для сервера
      // Заметьте, что при смене порта на сервере на арене он тоже изменится
      list($l_ip, $l_port, $l_time) = explode(":", $lst[$j]);
      $lst[$j] = $l_ip.':'.$port.':'.$time;
      }
     break;
   }

   // Обновляем список серверов в файле-списке
   $f = fopen($list_file, "a+");
   flock($f, LOCK_EX);
   ftruncate($f, 0);
   for ($i = 0; $i < count($lst); $i++)
    fwrite($f, $lst[$i]."n");
   fflush($f);
   flock($f, LOCK_UN);
   fclose($f);
  ?>;

Файл со списком серверов должен находиться в “db/arena_list.txt” с атрибутами разрешающими его изменение.

Вот собственно и всё! Дальше дело стоит за Вашей фантазией…

Если заметите какие-либо ошибки или недоработки данной реализации – буду рад Вас выслушать.

Удачи!

Ссылки
www.gamedev.ru/articles/?sect=8
12.06.05 23:21