Четверг, 16 Августа 2018, 13:58

Приветствую Вас Гость

Меню сайта
Категории каталога
Создание игр [309]
Статьи об общих понятиях связанных с созданием игр.
Программирование [69]
Гайды по программированию на разных ЯП.
Движки и Гейммейкеры [123]
Статьи о программах для создания игр, уроки и описания.
Софт [27]
Различные программы, в том числе в помощь игроделам.
2D-графика [11]
Уроки по рисованию, растр, пиксель-арт, создание спрайтов и пр.
3D-графика [10]
Уроки по моделированию, ландшафт, модели, текстурирование и пр.
Моддинг игр [4]
Модификация компьютерных игр, создание дополнений, перевод, хакинг.
Игры [81]
Статьи об играх, в том числе и сделанных на гейммейкерах.
Разное [63]
Статьи, которые не вошли в определённые разделы.
Наш опрос
Какие жанры компьютерных игр вы предпочитаете?
Всего ответов: 1737
Главная » Статьи » Создание игр

MMORPG на PHP: веб-сокеты
Вы до сих пор используете long-polling для постоянного взаимодействия клиента и сервера? Тогда мы идём к вам!

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

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

Код
<?php
  if(extension_loaded('sockets')) echo "Веб-сокеты поддерживаются";
  else echo "Веб-сокеты не поддерживаются. Грусть.";
?>


Если вы получили сообщение о поддержке веб-сокетов, можете продолжать читать дальше, всё отлично. В противном случае придётся установить php_sockets.dll расширение. Не буду вас учить, как его устанавливать, поскольку более или менее опытный кодер умеет это делать.
Примечание: Denwer часто ругается на это расширение. Рекомендуем в таком случае качать его с официального сайта php

Вернёмся к нашим сокетам. Для начала давайте различим эти понятия - сокет и веб-сокет. Сокеты известны людям из древних времён - каждая приличная многопользовательская игра использовала именно такую технологию. Постепенно она перекочевала и в веб. Сейчас технология лишь на стадии развития, но у неё большие перспективы. Несмотря на это, не все спешат отказывать flash/ajax.



При разработке игры рекомендуется всё же задуматься над этим: постоянное взаимодействие клиента и сервера, сервер на консольном php, отсутствие необходимости в куче http запросов. Заманчиво? Приступим тогда к написанию самого скрипта. Создайте его и назовите как угодно (например, sockets.php). Кроме этого, подготовьте файлы socket.html и socket.js. Откройте новый html-документ и напишите там макет:

Код

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>тест сокет-сервера</title>
</head>
<body>
<br /><br />

<script src="socket.js" type="text/javascript"></script>

Server address:
<input id="sock-addr" type="text" value="ws://echo.websocket.org"><br />
Message:
<input id="sock-msg" type="text">

<input id="sock-send-butt" type="button" value="send">
<br />
<br />
<input id="sock-recon-butt" type="button" value="reconnect"><input id="sock-disc-butt" type="button" value="disconnect">
<br />
<br />

Консоль клиента:
<div id="sock-info" style="border: 1px solid"> </div>

</body>
</html>


В данном примере мы будем использовать форму для контроля соединения, чтобы вы могли самостоятельно ощупать все аспекты работы с сервером. В форме по умолчанию будет стоять адрес ws://echo.websocket.org. Это - официальный сокет-сервер, служащий для проверки работы браузера с веб-сокетами. Его суть проста - он просто принимает запрос и отдаёт такой же ответ (эхо-сервер).

Однако это лишь макет и форма. Сам контроль клиента будет находиться в файле socket.js, который вы создавали ранее. Давайте откроем его и введём следующее:

Код

"use strict";

(function () {
  // переменная, которая хранит соединение
  var socket;

  ////////////////////////////////////////////////////////////////////////////
  var init = function () { // функция инициализации соединения
   
  socket = new WebSocket(document.getElementById("sock-addr").value); // получаем ip и порт с формы

  socket.onopen = connectionOpen; // назначанием обработчик соединения
  socket.onmessage = messageReceived; // назначаем обработчик получения сообщения от сервера
  //socket.onerror = errorOccurred;  
  //socket.onopen = connectionClosed;

  document.getElementById("sock-send-butt").onclick = function () { // при нажатии кнопки отправить...
  socket.send(document.getElementById("sock-msg").value); // отправляем сообщение активному серверу
  };

  document.getElementById("sock-disc-butt").onclick = function () { // когда нажал на кнопку отсоединиться...
  connectionClose(); // отсоединился, заметка от кэпа
  };

  document.getElementById("sock-recon-butt").onclick = function () { // при кнопке соединиться...
  socket = new WebSocket(document.getElementById("sock-addr").value); // соединяемся по IP и порту в форме
  socket.onopen = connectionOpen;
  socket.onmessage = messageReceived;
  };

  };

  function connectionOpen() {
  socket.send("Connection with \""+document.getElementById("sock-addr").value+"\" Подключение установлено обоюдно, отлично!"); // при открытии отправляем соединение серверу
  }

  function messageReceived(e) {
  console.log("Ответ сервера: " + e.data);
  document.getElementById("sock-info").innerHTML += (e.data+"<br />"); // добавляем ответ в консоль  
  }

  function connectionClose() {
  socket.close();
  document.getElementById("sock-info").innerHTML += "Соединение закрыто <br />"; // добавляем сообщение о закрытии в консоль

  }

  return {
  ////////////////////////////////////////////////////////////////////////////
  // ---- onload event ----
  load : function () {
  window.addEventListener('load', function () {
  init();
  }, false);
  }
  }
})().load();


Поскольку код прокомментирован, нет нужны объяснять принцип работы полностью. Загрузилась страница - устанавливаем соединение, и мы готовы к приёму-отдаче сообщений. Видите, как просто? Не надо устанавливать множество соединений, как в случае с AJAX (setInterval, $.ajax).

Итак, мы написали нормальный клиент. Давайте откроем socket.html в интернет-браузере. Если вы всё сделали правильно, соединение немедленно установится и вы сможете отправлять сообщения. Ответы будут полностью аналогичны запросам. Отлично, мы создали хороший клиент. Давайте возьмёмся за самое сложное - сервер.

Что такое сервер? Это обычный php-скрипт, но запускается он из-под консоли ОС, а не из браузера. Нужно, чтобы он был запущен, и не закрывался. Для этого установим set_time_limit(0) и ignore_user_abort(true) (это всё пишется прямо в скрипте).

Для запуска скрипта в UNIX системах надо использовать такую команду:
Код
$ php -q scriptfile.php &

Знак & в конце обязателен, иначе при выходе из терминала скрипт перестанет работать. В Windows системах также можно запускать php-скрипты, однако необходимо знать, где размещён php.exe:
Код
> w:\usr\local\php5\php.exe -q w:\home\localhost\www\scriptfile.php


Для начала скрипт должен развёртывать ws-сервер при помощи простой функции:
Код
socket_bind($socket, '127.0.0.1', 889);//привязываем его к указанным ip и порту


Но мы будем делать дело немножко не так. Давайте откроем socket.php и начнём писать:

Код

</php
error_reporting(E_ALL); //Выводим все ошибки и предупреждения
set_time_limit(180); //Время выполнения скрипта ограничено 180 секундами
ob_implicit_flush(); //Включаем вывод без буферизации  
$starttime = round(microtime(true),2); // записываем время старта


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

Код

echo "try to start...<br />";
$socket = stream_socket_server("tcp://127.0.0.1:8889", $errno, $errstr);

if (!$socket) {
  echo "socket unavailable<br />";
  die($errstr. "(" .$errno. ")\n");
}


Дальше идёт самая главная часть скрипта - мы должны настроить приём и обработку соединений. Для удобства в главном цикле при получении сообщения и прочих событиях вызывается отдельная функция. То есть мы разделим сам приём соединений и обработчик. Код понять несложно:

Код

$connects = array();
while (true) {
  echo "main while...<br />";
  //формируем массив прослушиваемых сокетов:
  $read = $connects;
  $read []= $socket;
  $write = $except = null;

  if (!stream_select($read, $write, $except, null)) {//ожидаем сокеты доступные для чтения (без таймаута)
  break;
  }

  if (in_array($socket, $read)) {//есть новое соединение то обязательно делаем handshake
  //принимаем новое соединение и производим рукопожатие:
  if (($connect = stream_socket_accept($socket, -1)) && $info = handshake($connect)) {
  echo "new connection...<br />";  
  echo "connect=".$connect.", info=".$info."<br />OK<br />";  
  //echo "info<br />";  
  //var_dump($info);  

  $connects[] = $connect;//добавляем его в список необходимых для обработки
  onOpen($connect, $info);//вызываем пользовательский сценарий
  }
  unset($read[ array_search($socket, $read) ]);
  }

  foreach($read as $connect) {//обрабатываем все соединения
  $data = fread($connect, 100000);

  if (!$data) { //соединение было закрыто
  echo "connection closed...<br />";  
  fclose($connect);
  unset($connects[ array_search($connect, $connects) ]);
  onClose($connect);//вызываем пользовательский сценарий
  continue;
  }

  onMessage($connect, $data);//вызываем пользовательский сценарий
  }
}


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

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

Код
fclose($socket);


Как оказалось, сокет-сервер в php и каждое соединение - переменная типа Ресурс, поэтому с ним можно работать, как с обычным ресурсом файла - записывать, читать и т.п.

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

function handshake($connect) { //Функция рукопожатия
  $info = array();

  $line = fgets($connect);
  $header = explode(' ', $line);
  $info['method'] = $header[0];
  $info['uri'] = $header[1];

  //считываем заголовки из соединения
  while ($line = rtrim(fgets($connect))) {
  if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
  $info[$matches[1]] = $matches[2];
  } else {
  break;
  }
  }

  $address = explode(':', stream_socket_get_name($connect, true)); //получаем адрес клиента
  $info['ip'] = $address[0];
  $info['port'] = $address[1];

  if (empty($info['Sec-WebSocket-Key'])) {
  return false;
  }

  //отправляем заголовок согласно протоколу вебсокета
  $SecWebSocketAccept = base64_encode(pack('H*', sha1($info['Sec-WebSocket-Key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
  $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
  "Upgrade: websocket\r\n" .
  "Connection: Upgrade\r\n" .
  "Sec-WebSocket-Accept:".$SecWebSocketAccept."\r\n\r\n";
  fwrite($connect, $upgrade);

  return $info;
}


Любое сообщение сервера или клиента должно быть зашифровано, поскольку прямой текст передавать нельзя. Это логично, ведь не каждый символ может нормально быть разобран, поэтому используется base64. Алгоритм видим ниже:

Код

function encode($payload, $type = 'text', $masked = false)  
{
  $frameHead = array();
  $payloadLength = strlen($payload);

  switch ($type) {
  case 'text':
  // first byte indicates FIN, Text-Frame (10000001):
  $frameHead[0] = 129;
  break;

  case 'close':
  // first byte indicates FIN, Close Frame(10001000):
  $frameHead[0] = 136;
  break;

  case 'ping':
  // first byte indicates FIN, Ping frame (10001001):
  $frameHead[0] = 137;
  break;

  case 'pong':
  // first byte indicates FIN, Pong frame (10001010):
  $frameHead[0] = 138;
  break;
  }

  // set mask and payload length (using 1, 3 or 9 bytes)
  if ($payloadLength > 65535) {
  $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8);
  $frameHead[1] = ($masked === true) ? 255 : 127;
  for ($i = 0; $i < 8; $i++) {
  $frameHead[$i + 2] = bindec($payloadLengthBin[$i]);
  }
  // most significant bit MUST be 0
  if ($frameHead[2] > 127) {
  return array('type' => '', 'payload' => '', 'error' => 'frame too large (1004)');
  }
  } elseif ($payloadLength > 125) {
  $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8);
  $frameHead[1] = ($masked === true) ? 254 : 126;
  $frameHead[2] = bindec($payloadLengthBin[0]);
  $frameHead[3] = bindec($payloadLengthBin[1]);
  } else {
  $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength;
  }

  // convert frame-head to string:
  foreach (array_keys($frameHead) as $i) {
  $frameHead[$i] = chr($frameHead[$i]);
  }
  if ($masked === true) {
  // generate a random mask:
  $mask = array();
  for ($i = 0; $i < 4; $i++) {
  $mask[$i] = chr(rand(0, 255));
  }

  $frameHead = array_merge($frameHead, $mask);
  }
  $frame = implode('', $frameHead);

  // append payload to frame:
  for ($i = 0; $i < $payloadLength; $i++) {
  $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
  }

  return $frame;
}


Аналогично, только наоборот делается раскодирование.

Код

function decode($data)
{
  $unmaskedPayload = '';
  $decodedData = array();

  // estimate frame type:
  $firstByteBinary = sprintf('%08b', ord($data[0]));
  $secondByteBinary = sprintf('%08b', ord($data[1]));
  $opcode = bindec(substr($firstByteBinary, 4, 4));
  $isMasked = ($secondByteBinary[0] == '1') ? true : false;
  $payloadLength = ord($data[1]) & 127;

  // unmasked frame is received:
  if (!$isMasked) {
  return array('type' => '', 'payload' => '', 'error' => 'protocol error (1002)');
  }

  switch ($opcode) {
  // text frame:
  case 1:
  $decodedData['type'] = 'text';
  break;

  case 2:
  $decodedData['type'] = 'binary';
  break;

  // connection close frame:
  case 8:
  $decodedData['type'] = 'close';
  break;

  // ping frame:
  case 9:
  $decodedData['type'] = 'ping';
  break;

  // pong frame:
  case 10:
  $decodedData['type'] = 'pong';
  break;

  default:
  return array('type' => '', 'payload' => '', 'error' => 'unknown opcode (1003)');
  }

  if ($payloadLength === 126) {
  $mask = substr($data, 4, 4);
  $payloadOffset = 8;
  $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset;
  } elseif ($payloadLength === 127) {
  $mask = substr($data, 10, 4);
  $payloadOffset = 14;
  $tmp = '';
  for ($i = 0; $i < 8; $i++) {
  $tmp .= sprintf('%08b', ord($data[$i + 2]));
  }
  $dataLength = bindec($tmp) + $payloadOffset;
  unset($tmp);
  } else {
  $mask = substr($data, 2, 4);
  $payloadOffset = 6;
  $dataLength = $payloadLength + $payloadOffset;
  }

  /**
  * We have to check for large frames here. socket_recv cuts at 1024 bytes
  * so if websocket-frame is > 1024 bytes we have to wait until whole
  * data is transferd.
  */
  if (strlen($data) < $dataLength) {
  return false;
  }

  if ($isMasked) {
  for ($i = $payloadOffset; $i < $dataLength; $i++) {
  $j = $i - $payloadOffset;
  if (isset($data[$i])) {
  $unmaskedPayload .= $data[$i] ^ $mask[$j % 4];
  }
  }
  $decodedData['payload'] = $unmaskedPayload;
  } else {
  $payloadOffset = $payloadOffset - 4;
  $decodedData['payload'] = substr($data, $payloadOffset);
  }

  return $decodedData;
}


Можете успокоиться, самое сложное уже позади. Теперь настало время творчества. Добавьте в конец файла:

Код

function onOpen($connect, $info) {
  echo "open OK<br />\n";
  //fwrite($connect, encode('Привет, мы соеденены'));
}

function onClose($connect) {
  echo "close OK<br />\n";
}

function onMessage($connect, $data) {
  $f = decode($data);
  echo "Message:";
  echo $f['payload'] . "<br />\n";
  fwrite($connect, encode($f['payload']));
}


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

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

Удачи вам и всего хорошего. Если нашли недочёты или замечания - пишите в комментарии. Предупреждаю сразу - это были веб-сокеты с нуля. Я знаю, что есть Daemon.io и прочие готовые сервера, но чем хуже собственная разработка?
Категория: Создание игр | Добавил: JackNazaryan (30 Июля 2015) | Автор: Дмитрий
Просмотров: 4023 | Комментарии: 7 | Рейтинг: 5.0/4 |
Теги: base64, нагрузка, высокая, Сокеты, Web, веб-сокеты, PHP, оптимизация, sockets, WebSockets
Дополнительные опции:
Также если вы считаете, что данный материал мог быть интересен и полезен кому-то из ваших друзей, то вы бы могли посоветовать его, отправив сообщение на e-mail друга:

Игровые объявления и предложения:
Если вас заинтересовал материал «MMORPG на PHP: веб-сокеты», и вы бы хотели прочесть что-то на эту же тему, то вы можете воспользоваться списком схожих материалов ниже. Данный список сформирован автоматически по тематическим меткам раздела. Предлагаются такие схожие материалы: Если вы ведёте свой блог, микроблог, либо участвуете в какой-то популярной социальной сети, то вы можете быстро поделиться данной заметкой со своими друзьями и посетителями.

Всего комментариев: 7
+0-
6 Screpka   (10 Ноября 2015 11:02)
Еще статей! Очень познавательно.

+0-
5 AIDeveloper   (10 Октября 2015 12:57)
AIDeveloperКомикс классный +1 )))

+0-
1 dima9595   (05 Августа 2015 10:48)
dima9595Давно хотел сделать socket сервер, но нормальных мануалов не нашёл. Жду продолжение!

+0-
2 k0fe   (07 Августа 2015 12:20)
k0feДобрый день!
Нет, уроков ждать не стоит!
Подробнее в ЛС.

+0-
3 JackNazaryan   (09 Октября 2015 01:04)
JackNazaryanстоит

+1-
4 k0fe   (09 Октября 2015 21:57)
k0feХорошо, что Вы вернулись.

+0-
7 dima9595   (05 Января 2016 16:19)
dima9595Скоро будут ли новые уроки?

Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Поиск по сайту
10 случ. движков
  • TURBU
  • Tombstone Engine
  • RGM
  • PureBasic
  • Java Script WebGL Games Creator
  • vbGORE
  • Irrlicht Engine
  • Jet3D
  • Point&Click Dev Kit
  • Mokoi Gaming
  • Друзья сайта
    Игровой форум GFAQ.ru Перевод консольных игр
    Все права сохранены. GcUp.ru © 2008-2018 Рейтинг