Написание игрового интернет сервера на php

Введение

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

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

Описание метода


Итак, опишу то что нам понадобится для реализации:
– Хостинг с поддержкой php;
– Ваша игра с работоспособным сетевым кодом :).
Первый пункт я надеюсь не вызывает больших проблем у начинающих игрописателей, т.к. существует множество сайтов, предоставляющих бесплатный домен с поддержкой php.
А вот со вторым пунктом придется немного попариться, впрочем, это уже тема для отдельной статьи…
Так как в данной статье я использовал PHP, то потребуется знание его основ. Впрочем, при желании, перевод на другой язык написания web страниц не составит большого труда.

  Итак, имеем в Интернете домен на котором размещен наш скрипт арены. Есть игра-клиент, которой нужно узнать кол-во доступных серверов, и при необходимости создать свой.
  Что нам нужно от арены? Всего-навсего получить список серверов в виде IP:Port IP:Port IP:Port… и зарегистрировать новый.
  Как это будет происходить? Да очень просто! Посредством HTTP запросов.
  Так как нет идеальных решений, какие минусы у данного метода?
– Серверы находящиеся за шлюзом не будут видны остальным клиентам, т.к. даже сама игра-сервер без понятия на каком external порту она висит.
– При падении хостера (сайта) арена шлепнется вместе с ним! Но это относится уже к форс-мажорным обстоятельствам… ;).
  А какие же плюсы?
– Относительная простота реализации;
– Легко разместить такую арену в локальной сети;
– Не требует восстановления после различных ЧП :).

Реализация


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

  Всего будет 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): stringconst  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 thenbegin  WSACleanup;  Exit;  end; sock := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if sock = INVALID_SOCKET thenbegin  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 thenbegin  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 thenbegin  ZeroMemory(@buf, 1024);  error := recv(sock, buf, 1024, 0);  while error > 0 dobegin  Result := Result + Copy(buf, 0, error);  error := recv(sock, buf, 1024, 0);  endend;  // Закрываем сокет – завершаем работу с сетью  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:

 = 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 с атрибутами разрешающими его изменение.
Вот собственно и все! Дальше дело стоит за Вашей фантазией…

Удачи!