Как работать с WebSocket на простом PHP хостинге
В хостинге некоторые решения могут показаться вам нестандартными.
Поэтому перед началом работы мы бы рекомендовали ознакомиться с вводной статьей.
Введение
В этой статье мы расскажем как можно реализовать хостинг Веб Сокетов на вашем сайте, который размещается на хостинге.
Если ваш сайт написан на языке PHP, то мы не рекомендуем реализовывать поддержку вебсокетов в рамках языка PHP, а рекомендуем воспользоваться внешним решением.
В нашем случае внешним решением будет сайт на nodejs, в котором будет реализован хостинг Web Socket’ов.
В качестве примера мы будем использовать готовый проект на github Example-chatroom-with-NodeJS-PHP. Этот проект позволит нам создать простой сайт, предлагающий посетителям вести онлайн переписку (онлайн чат).
Сайт на хостинг PHP (mydomain.ru) будет выступать основой нашего проекта. При открытии страницы mydomain.ru все сообщения в чате будут загружаться с ws.mydomain.ru — с сайта с поддержкой вебсокетов. На ws.mydomain.ru мы могли бы разместить любую реализацию вебсокетов, не обязательно созданную на node.js.
Установка и настройка сайта на хостинг PHP
Для начала в панели управления нам потребуется создать два сайта:
- сайт на хостинге PHP, например mydomain.ru
- сайт с поддержкой вебсокетов на node.js, например ws.mydomain.ru
Разумеется, mydomain.ru и ws.mydomain.ru лишь примеры доменов, и вы можете использовать любые домены.
Перейдем в каталог с сайтом на хостинг php
Удалим содержимое тестового сайта
Клонируем репозиторий в каталог www
git clone https://github.com/ethaizone/Example-chatroom-with-NodeJS-PHP www
Откроем для редактирования файл chat.php, который находится в каталоге pages
Будет открыт файл на строке №185.
Сейчас нам необходимо указать, чтобы посетители сайта mydomain.ru при загрузке сообщений из чата обращались к сайту, где включена поддержка вебсокетов. Кроме этого, вместо 3421 порта будет использоваться порт 80.
var socket = io('ws.mydomain.ru:80');
Для сохранения нажмите на клавиатуре F2.
Установка и настройка сайта на node.js для работы с вебсокетами
Теперь осталось настроить сайт ws.mydomain.ru.
Переместим файлы chat.js и package.json в каталог с сайтом ws.mydomain.ru
mv -t ~/ws.mydomain.ru/app chat.js package.json
Перейдем в каталог с сайтом ws.mydomain.ru
Сейчас нам нужно, чтобы сайт на node.js был виден извне. Особенность размещения node.js приложений на хостинге заключается в том, что приложению нужно прикрепиться (bind) не на порт 80 или 8080, а на определенные IP и порт. Эти параметры мы передаем в переменных окружения APP_IP и APP_PORT.
Откроем на 5й строке файл chat.js
var port = process.env.APP_PORT;
Далее в самом низу файла нам необходимо изменить строку
http.listen(process.env.APP_PORT, process.env.APP_IP, function()< console.log('listening on *:'+port); >);
После этого в панели управления укажите файл app/chat.js в качестве файла для запуска приложения (параметр “APP_PATH”)
Проверьте работу сайта открыв mydomain.ru в браузере.
Вы должны увидеть что-то аналогичное следующему:
Размещение тестового сайта можно считать успешным, если при открытии инструментов разработчика в браузере (обычно F12 или CTRL + SHIFT + i) отображается соединение с типом “websocket” и со статусом “101”.
Также можно открыть mydomain.ru в двух разных браузерах, чтобы проверить функциональность самого чата. Однако, из-за особенностей проекта Example-chatroom-with-NodeJS-PHP, при открытии сайта в двух браузерах необходимо ввести один и тот же никнейм.
Готово! Мы успешно разместили сайт на хостинг PHP.
PHP-мониторинг приложений в реальном времени с помощью websockets и node.js
Проверка журналов ошибок является распространенным способом обнаружения ошибок и ошибок. Мы также можем отображать ошибки на экране нашего сервера разработки, или мы даже можем использовать отличные инструменты, такие как
firePHP, для отображения наших ошибок PHP и предупреждений в нашей консоли firebug. Это круто, но мы можем видеть только наши ошибки / предупреждения сеанса. Если мы хотим увидеть чужие ошибки, нам нужно проверить журнал ошибок.
tail -f — наш друг, но нам нужно просмотреть все предупреждения всех сессий, чтобы увидеть наши. Поэтому я хочу создать инструмент для мониторинга моих PHP-приложений в режиме реального времени. Давайте начнем:
В чем идея? Идея заключается в том, чтобы перехватывать все ошибки и предупреждения PHP во время выполнения и отправлять их на HTTP-сервер node.js. Этот сервер будет работать аналогично серверу чата, но наши клиенты смогут только читать журналы сервера. В основном приложения состоят из трех частей: сервер node.js, веб-клиент (html5) и серверная часть (PHP). Позвольте мне немного объяснить каждую часть:
Узел Сервер
По сути, он состоит из двух частей: http-сервер для обработки ошибок / предупреждений PHP и веб-сокет- сервер для управления связью в реальном времени с браузером. Когда я говорю, что использую веб-сокеты, это означает, что веб-клиент будет работать только с браузером с поддержкой веб-сокетов, таким как Chrome. В любом случае это довольно простой обмен с сервера websocket на сервер socket.io, чтобы использовать его с любым браузером. Но веб-сокеты, кажется, будущее, поэтому я буду использовать веб-сокеты в этом примере.
http.createServer(function (req, res) < var remoteAdrress = req.socket.remoteAddress; if (allowedIP.indexOf(remoteAdrress) >= 0) < res.writeHead(200, < 'Content-Type': 'text/plain' >); res.end('Ok\n'); try < var parsedUrl = url.parse(req.url, true); var type = parsedUrl.query.type; var logString = parsedUrl.query.logString; var ip = eval(parsedUrl.query.logString)[0]; if (inspectingUrl == "" || inspectingUrl == ip) < clients.forEach(function(client) < client.write(logString); >); > > catch(err) < console.log("500 to " + remoteAdrress); res.writeHead(500, < 'Content-Type': 'text/plain' >); res.end('System Error\n'); > > else < console.log("401 to " + remoteAdrress); res.writeHead(401, < 'Content-Type': 'text/plain' >); res.end('Not Authorized\n'); > >).listen(httpConf.port, httpConf.host);
var inspectingUrl = undefined; ws.createServer(function(websocket) < websocket.on('connect', function(resource) < var parsedUrl = url.parse(resource, true); inspectingUrl = parsedUrl.query.ip; clients.push(websocket); >); websocket.on('close', function() < var pos = clients.indexOf(websocket); if (pos >= 0) < clients.splice(pos, 1); >>); >).listen(wsConf.port, wsConf.host);
Если вы хотите узнать больше о node.js и увидеть больше примеров, загляните на отличный сайт: http://nodetuts.com/ . На этом сайте Педро Тейшейра покажет примеры и руководства по node.js. На самом деле мой http.ces сервер node.js представляет собой сочетание двух руководств с этого сайта.
Веб-клиент.
Веб-клиент представляет собой простое приложение веб-сокетов. Мы будем обрабатывать соединение через websockets, переподключаться, если оно умирает, и немного больше. Я основан на демо чата node.js
И волшебство JavaScript
var timeout = 5000; var wsServer = '192.168.2.2:8880'; var unread = 0; var focus = false; var count = 0; function updateCount() < count++; $("#count").text(count); >function cleanString(string) < return string.replace(/&/g,"&").replace(//g,">"); > function updateUptime () < var now = new Date(); $("#uptime").text(now.toRelativeTime()); >function updateTitle() < if (unread) < document.title = "(" + unread.toString() + ") Real time " + selectedIp + " monitor"; >else < document.title = "Real time " + selectedIp + " monitor"; >> function pad(n) < return ("0" + n).slice(-2); >function startWs(ip) < try < ws = new WebSocket("ws://" + wsServer + "?ip=" + ip); $('#toolbar').css('background', '#65A33F'); $('#socketStatus').html('Connected to ' + wsServer); //console.log("startWs:" + ip); //listen for browser events so we know to update the document title $(window).bind("blur", function() < focus = false; updateTitle(); >); $(window).bind("focus", function() < focus = true; unread = 0; updateTitle(); >); > catch (err) < //console.log(err); setTimeout(startWs, timeout); >ws.onmessage = function(event) < unread++; updateTitle(); var now = new Date(); var hh = pad(now.getHours()); var mm = pad(now.getMinutes()); var ss = pad(now.getSeconds()); var timeMark = '[' + hh + ':' + mm + ':' + ss + '] '; logString = eval(event.data); var host = logString[0]; var line = ""; if (logString[2]) < line += " " + timeMark + " " + host + " "; line += "" + logString[1]; + " "; > $('#log').append(line); updateCount(); window.scrollBy(0, 100000000000000000); >; ws.onclose = function()< //console.log("ws.onclose"); $('#toolbar').css('background', '#933'); $('#socketStatus').html('Disconected'); setTimeout(function() , timeout); > > $(document).ready(function() < startWs(selectedIp); >); " + logString[2] + "
Серверная часть:
Серверная часть будет молча обрабатывать все предупреждения и ошибки PHP и отправлять их на сервер узла. Идея состоит в том, чтобы разместить минимальную строку кода PHP в начале приложения, которое мы хотим отслеживать. Представьте себе следующий фрагмент кода PHP
$a = $var[1]; $a = 1/0; class Dummy < static function err() < throw new Exception("error"); >> Dummy1::err();
будет выдано:
Уведомление: неопределенная переменная: var
Предупреждение: Деление на ноль
Неполученное исключение «Исключение» с сообщением «ошибка»
Поэтому мы добавим нашу маленькую библиотеку, чтобы перехватывать эти ошибки и отправлять их на сервер узла.
include('client/NodeLog.php'); NodeLog::init('192.168.2.2'); $a = $var[1]; $a = 1/0; class Dummy < static function err() < throw new Exception("error"); >> Dummy1::err();
Скрипт будет работать так же, как и первая версия, но если мы запустим наш сервер node.js в консоли:
$ node server.js HTTP server started at 192.168.2.2::5672 Web Socket server started at 192.168.2.2::8880
Мы увидим эти ошибки / предупреждения в режиме реального времени при запуске нашего браузера
Здесь мы видим небольшой скринкаст с работающим приложением:
Это библиотека на стороне сервера:
class NodeLog < const NODE_DEF_HOST = '127.0.0.1'; const NODE_DEF_PORT = 5672; private $_host; private $_port; /** * @param String $host * @param Integer $port * @return NodeLog */ static function connect($host = null, $port = null) < return new self(is_null($host) ? self::$_defHost : $host, is_null($port) ? self::$_defPort : $port); >function __construct($host, $port) < $this->_host = $host; $this->_port = $port; > /** * @param String $log * @return Array array($status, $response) */ public function log($log) < list($status, $response) = $this->send(json_encode($log)); return array($status, $response); > private function send($log) < $url = "http://_host>:_port>?logString=" . urlencode($log); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_NOBODY, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return array($status, $response); > static function getip() < $realip = '0.0.0.0'; if ($_SERVER) < if ( isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR'] ) < $realip = $_SERVER["HTTP_X_FORWARDED_FOR"]; >elseif ( isset($_SERVER['HTTP_CLIENT_IP']) && $_SERVER["HTTP_CLIENT_IP"] ) < $realip = $_SERVER["HTTP_CLIENT_IP"]; >else < $realip = $_SERVER["REMOTE_ADDR"]; >> else < if ( getenv('HTTP_X_FORWARDED_FOR') ) < $realip = getenv('HTTP_X_FORWARDED_FOR'); >elseif ( getenv('HTTP_CLIENT_IP') ) < $realip = getenv('HTTP_CLIENT_IP'); >else < $realip = getenv('REMOTE_ADDR'); >> return $realip; > public static function getErrorName($err) < $errors = array( E_ERROR =>'ERROR', E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR', E_WARNING => 'WARNING', E_PARSE => 'PARSE', E_NOTICE => 'NOTICE', E_STRICT => 'STRICT', E_DEPRECATED => 'DEPRECATED', E_CORE_ERROR => 'CORE_ERROR', E_CORE_WARNING => 'CORE_WARNING', E_COMPILE_ERROR => 'COMPILE_ERROR', E_COMPILE_WARNING => 'COMPILE_WARNING', E_USER_ERROR => 'USER_ERROR', E_USER_WARNING => 'USER_WARNING', E_USER_NOTICE => 'USER_NOTICE', E_USER_DEPRECATED => 'USER_DEPRECATED', ); return $errors[$err]; > private static function set_error_handler($nodeHost, $nodePort) < set_error_handler(function ($errno, $errstr, $errfile, $errline) use($nodeHost, $nodePort) < $err = NodeLog::getErrorName($errno); /* if (!(error_reporting() & $errno)) < // This error code is not included in error_reporting return; >*/ $log = array( NodeLog::getip(), " :", nl2br($errstr) ); NodeLog::connect($nodeHost, $nodePort)->log($log); return false; >); > private static function register_exceptionHandler($nodeHost, $nodePort) < set_exception_handler(function($exception) use($nodeHost, $nodePort) < $exceptionName = get_class($exception); $message = $exception->getMessage(); $file = $exception->getFile(); $line = $exception->getLine(); $trace = $exception->getTraceAsString(); $msg = count($trace) > 0 ? "Stack trace:\n" : null; $log = array( NodeLog::getip(), nl2br("Uncaught exception '' with message '' in :"), nl2br($msg) ); NodeLog::connect($nodeHost, $nodePort)->log($log); return false; >); > private static function register_shutdown_function($nodeHost, $nodePort) < register_shutdown_function(function() use($nodeHost, $nodePort) < $error = error_get_last(); if ($error['type'] == E_ERROR) < $err = NodeLog::getErrorName($error['type']); $log = array( NodeLog::getip(), ":", nl2br($error['message']) ); NodeLog::connect($nodeHost, $nodePort)->log($log); > echo NodeLog::connect($nodeHost, $nodePort)->end(); >); > private static $_defHost = self::NODE_DEF_HOST; private static $_defPort = self::NODE_DEF_PORT; /** * @param String $host * @param Integer $port * @return NodeLog */ public static function init($host = self::NODE_DEF_HOST, $port = self::NODE_DEF_PORT) < self::$_defHost = $host; self::$_defPort = $port; self::register_exceptionHandler($host, $port); self::set_error_handler($host, $port); self::register_shutdown_function($host, $port); $node = self::connect($host, $port); $node->start(); return $node; > private static $time; private static $mem; public function start() < self::$time = microtime(TRUE); self::$mem = memory_get_usage(); $log = array(NodeLog::getip(), "Start >>>> "); $this->log($log); > public function end() < $mem = (memory_get_usage() - self::$mem) / (1024 * 1024); $time = microtime(TRUE) - self::$time; $log = array(NodeLog::getip(), "End time "); $this->log($log); > >
И конечно полный код на gitHub:
RealTimeMonitor