Вэб-сокеты и как ими управлять в PHP
В то время, когда интернет получил широкое распространение по всему миру, он представлял собой четко выстроенную модель, состоящую из аппаратной и программной части. Такой глобальная сеть остается и по сегодняшний день. Однако семиуровневая модель OSI пополнилась новым протоколом «ws», который стал частью прикладного уровня, в котором находится и его близкий родственник — протокол http.
Разработка приложений, работающих в реальном времени требует несколько другого поведения от процедуры запроса/ответа между клиентом и сервером. К вэб-приложениям, работающим в реальном времени относятся: чаты, мессенджеры, онлайн-игры.
Если кратко, то те ресурсы, функционирование которых основано на событиях. Пользователь не просто читает или просматривает контент, но постоянно обращается к серверу. Таких обращений может быть десятки тысяч и миллионы в секунду — все зависит от того, сколько клиентов в данный момент используют приложение.
По протоколу HTTP, во время сессии приложения в реальном времени, постоянно происходит обращение клиента к серверу. Это сопровождается объемными заголовками для пакетов данных и значительных нагрузках на сервер и трафик.
При использовании сокета клиентская машина задает вопрос по типу: есть сообщения? (клиент), нет (ответ от сервера). Это происходит в фоновом режиме, незаметном для пользователя. То есть с определенной частотой в секунду клиент спрашивает сервер о наличии данных, а серверная машина отправляет ответ «нет» до тех пор, кока другой клиент не отправит сообщение.
Такая модель поведения подходит для создания приложений в реальном времени и сервер в этом случае не испытывает серьезную нагрузку. Поэтому, через некоторое время после утверждения HTTP были придуманы вэб-сокеты, которые значительно снизили нагрузку на сервер. Ведь интернет стал доступен всем, а вэб приложения постоянно совершенствовались. Нужна была технология, которая снижала себестоимость поддержки приложения. На ее место стал протокол WS.
Что такое вэб-сокеты
Вэб-сокет — это протокол прикладного уровня стека TCP/IP семиуровневой модели OSI. Основные протоколы прикладного уровня до появления сокетов:
- HTTP,
- HTTPS,
- DNS,
- FTP.
Протокол сокетов имеет аббревиатуру WS и WSS для защищенного соединения по аналогии с HTTPS. Для передачи данных на транспортном уровне сокеты пользуются протоколом TCP.
Сокеты (протокол WS) являются собратьями HTTP, так как выполняют такую же функцию — передача данных от клиента к серверу. Однако эти два протокола отличаются принципом взаимодействия клиент/сервер. Технология функционирования сокетов как раз и нашла свое применение в приложениях реального времени. До их существования разработчики находили пути решений с помощью HTTP. Но данный протокол оказался не эффективным для разработки подобных приложений.
Недостатки вэб-сокетов
Для того, чтобы разработчики могли управлять и настраивать работу протокола WS, в браузере и на сервере вэб-сокеты являются объектами со своими свойствами и методами. В PHP есть ряд функций, позволяющих управлять поведением сокетов. Это же относится и к JavaScript — клиентский язык также позволяет манипулировать с серверной технологией.
Основными недостатками сокетов является тот факт, что объект Web-socket поддерживается не всеми браузерами. Устаревшие браузеры не снабжены в DOM данным объектом, а также устаревшие версии серверного ПО, также не поддерживают возможность отправлять запросы и ответы по WS.
Детальнее о работе сокетов
В спецификации для вэб-сокетов определены отдельные API для возможности соединения клиента с сервером. Во время сессии соединение происходит один раз и длится постоянно до окончания сессии. Клиент и сервер находятся в постоянном ожидании, при этом, они подключены друг к другу. Клиент и сервер могут отправлять данные друг другу в любое время — для этого не нужно осуществлять новое соединение, как при использовании HTTP.
Клиент устанавливает соединение с сервером, который отвечает взаимностью — выразить это можно в рукопожатии:
- Клиент отправляет HTTP запрос с дополнительным заголовком upgrade, который сообщает, что соединение должно установиться по сокету.
- Сервер принимает запрос и отправляет ответ по HTTP с таким же дополнительным заголовком.
- Рукопожатие осуществлено и теперь между клиентом и сервером сохраняется постоянное соединение на время сессии по сокету.
- Основой для такого соединения выступает TCP/IP.
- Клиент и сервер могут обмениваться данными в обход HTTP.
- Само собой, обмен может осуществляться и по HTTP.
- Суть в том, что один функционал приложения работает с сокетами, другой с HTTP.
Заголовок запроса от клиента выглядит следующим образом:
- GET/POST ws://mediacontent.buisness.com/ HTTP/1.1 — стандартный HTTP заголовок, который формируется через URL запрос;
- Origin: http://buisness.com — Описывает протокол, домен и порт, куда отправляется запрос;
- Connection: Upgrade — указывает, что браузер хочет установить соединение через сокет;
- Host: mediacontent.buisness.com — указывает имя сервера;
- Upgrade: websocket — протокол соединения.
- Sec-WebSocket-Key: Iv8op/8s+lYFgNBcXmNP8Q= — ключ безопасного соединения, который позволяет браузеру убедиться, что ответ предназначен для него;
Заголовок ответа, отправляемый сервером:
- HTTP/1.1 101 Switching Protocols;
- Date: Fri, 12 Oct 2020 10:37:20 GMT;
- Connection: Upgrade;
- Upgrade: WebSocket;
- Sec-WebSocket-Accept: hhFluiDokk24srzEOFBUlVSlC2g=== — ключ безопасного соединения, который позволяет браузеру убедиться, что ответ предназначен для него;
- Sec-WebSocket-Version: 13 — версия протокола.
После того, как соединение между клиентом и сервером установлено, передача данных осуществляется по протоколу WS, в обход HTTP. При этом, соединение поддерживается постоянно до закрытия окна (окончания сессии).
В зависимости от версии сервера, браузера или сложности скрипта, могут отправляться и дополнительные заголовки:
Sec-WebSocket-Extensions: deflate-frame. Говорит о том, что браузером поддерживается сжатие для данных. Если объект WebSocket модифицирован, что относится к последним версиям браузеров, то и заголовки также расширены. Этот заголовок отправляет информацию о том, какие расширения поддерживает браузер.
Sec-WebSocket-Protocol: soap, wamp. Браузер предупреждает сервер о том, что будут отправляться не только стандартные данные, но и данные, которые поддерживают протоколы SOAP и WAMP. Заголовок описывает формат данных, которые клиент и сервер будут отправлять друг другу. Значения данных заголовков устанавливаются не автоматически, а передаются вызов конструктора:
new WebSocket(«wss://localhost», [«soap», «wamp»]).
Передача данных через сокет-соединение
В HTTP данные передаются пакетами. После запроса от клиента, HTTP высылает текстовые или графические данные посредством маленьких кусочков. Когда на компьютере у клиента все кусочки собраны, пользователь видит полноценную вэб-страницу, текст или изображение. Каждый такой пакет снабжается заголовком на подобие тех, которые мы рассматривали ранее. То есть: сколько пакетов — столько и заголовков.
В сокет-соединении технология передачи данных немного другая. Данные передаются не пакетами, а фреймами (frames). Каждый такой фрейм снабжается заголовком, но другого формата. При этом, фрейм может быть снабжен как текстовыми данными, так и бинарными.
Некоторые особенности и преимущества фреймов:
- Заголовки отличаются от HTTP и гораздо меньшего формата.
- Накладные расходы для транспортировки данных ниже, чем посредством HTТР.
- Малый объем данных помещается в один фрейм.
- Большой объем данных фрагментируется на несколько фреймов.
Фреймы передают три типа информации: текстовые данные (UTF-8), бинарные данные, управляющие фреймы для открытия, поддержания и закрытия соединения.
Протокол WS поддерживает всю структуру глобальной сети, которую поддерживает HTTP. Поэтому, для подключения через WS не нужно менять какие либо настройки, или устанавливать дополнительное ПО. Однако необходимо убедиться, что сервер и браузер поддерживают сокеты.
Как проверить поддержку сокетов браузером
В любом браузере есть консоль JavaScript. Через консоль и следует проверять наличие нужных объектов ядра JavaScript, а также DOM.
Делается это в следующем порядке:
- Открыть браузер;
- Открыть инструменты разработчика;
- Выбрать консоль (Console);
- Ввести команду WebSocket и нажать Enter;
- Если отобразится функция-конструктор WebSocket, то такой объект поддерживается браузером;
- Если вместо конструктора отобразится Undefined, то такой объект отсутствует.
Создание сокет-сервера на PHP
Первое, что необходимо сделать, это настроить сервер на работу с вэб-сокетами. Для этого нужно найти файл php.ini, снять комментарии со строки «extension = php_sockets.dll» и перезапустить сервер. То-же самое нужно сделать и на хостинге, если такой имеется.
Для того, чтобы можно было использовать сокеты вэб-приложением, на сервере необходимо создать специальный скрипт на PHP. Данный скрипт позволяет создать сокет-сервер, который будет создавать соединение на стороне сервера при необходимости.
Пример: Создаем скрипт сокет-сервера и запишем его в файл «socet.php»
function LoadSocketServer($limit = 0) { // Функция, которая запускает сервер, работающий определенное время «limit»
$time_to_start = time(); // Время для запуска сервера
echo ‘SERVER START TO WORK’; // Вывод в консоль уведомление о начале работы сервераecho ‘Socket was created’;
$socketObject = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // Создаем объект сокета:
if (!$socketObject) {
die(‘Error: ‘ . socket_strerror(socket_last_error())); // Если не удалось создать сокет, то появится сообщение об ошибке
}echo ‘Socket was bind to IP’;
$bind_to_ip = socket_bind($socketObject, ‘127.0.0.1’, 7777); // привязываем сокет к ip и нужному порту
if (!$bind) {
die(‘Error: ‘ . socket_strerror(socket_last_error())); // Если привязка не удалась, вывести сообщение об ошибке
}echo ‘Set diffrent options’;
// разделяем работу одного порта для нескольких соединений
$set_option = socket_set_option($socketObject, SOL_SOCKET, SO_REUSEADDR, 1);
if (!$option) {
die(‘Error: ‘ . socket_strerror(socket_last_error()));
}echo ‘Socket Listening’;
$listen = socket_listen($socketObject); // прослушиваем сокет через установленный порт
if (!$listen) {
die(‘Error: ‘ . socket_strerror(socket_last_error()));
}while (true) { // бесконечный цикл, обеспечивающий ожидание подключений
echo ‘Waiting for any connections’;
$connect_sucsess = socket_accept($socketObject); // пауза для ожидания получения ответа
if ($connect_sucsess !== false) {
echo ‘Client connection sucsessful’;
echo ‘Send data to client’;
socket_write($connect_sucsess, ‘Hello World!’);
} else {
echo ‘Error: ‘ . socket_strerror(socket_last_error());
$waiting = 1000;
usleep($waiting);
}// прекращаем работу сервера по истчению $limit секунд
if ($limit && (time() — $time_to_start > $limit)) {
echo ‘Closing connection’;
socket_close($socketObject);
echo ‘Server finish connection’;
return;
}
}
}error_reporting(E_ALL); // выводим все ошибки и предупреждения, которые могут возникнуть при запуске сервера и соединении
set_time_limit(0); // бесконечное время работы скрипта, обеспечивающее постоянное соединение// Запускаем сервер на время, завершение работы которого осуществится через 180 секунд
SocketServer(180);
Теперь нужно открыть командную строку PHP и ввести в ней путь к нашему файлу:
D:\project\localhost\www>php -f socket.php.
Если запуск успешен, то в консоли выведутся следующие строки:
- SERVER START TO WORK;
- Socket was created;
- Socket was bind to IP;
- Set diffrent options;
- Socket Listening;
- Waiting for any connections.
Если отключить сервер, то будет написано Closing connection. Если объект WebSocket создан на стороне клиента и создается соединение, то будет написано Client connection sucsessful. Если сервер отправляет данные по сокету, то будет написано Send data to client.
Однако данной операции не достаточно для того, чтобы приложение могло омениваться данными с сервером посредством WS. Для того, чтобы обеспечить полноценную работу, требуется создать скрипт подключения со стороны клиента. Для этого конечно придется воспользоваться JavaScript. Технология чем-то похожа на AJAX.
Передача данных посредством сокетов осуществляется в фоновом режиме без перезагрузки страницы. Обычно соединение привязывают к какому либо событию JavaScript, к примеру — window.onload. Отправка или получение данных также основано на событии. К примеру функция отправки данных осуществляется при нажатии на какую-либо кнопку.
Если кратко, то при загрузке страницы клиент подключается через сокет к серверу; при нажатии на кнопку отправляются данные по протоколу WS без перезагрузки страницы.
Пример: Реализация функции подключения к серверу на JavaScript со стороны клиента.
var socketObject = new WebSocket(«ws://localhost»);
socketObject.onopen = function() { // Если соединение установлено с хостом, то браузер об это сообщит, а также будет выслан текст «Hello World» с сервера
alert(«Соединение было успешно установлено»);
alert(«Отправляем управляющие фреймы данных на сервер»);
socket.send(«Hello World»);
};socketObject.onmessage = function() { // Сообщение будет выведено если с сервера придет какое либо сообщение
alert(«Данные были получены с сервера»);
};socketObject.onclose = function() {
alert(» Соединение остановлено без проблем»);
}
};socket.onerror = function(error) { // Сообщение выводится в случае возникновения ошибки соединения
alert(«error.message»);
};
Данный код будет работать до окончания сессии. После того, как приложение или страница будут снова загружены, соединение установится автоматически. Далее для передачи данных нужно выполнение какого-либо события: нажатие на кнопку и тому подобное. Так будет происходить постоянно, пока сервер запущен.
За постоянное соединение отвечает зацикленный скрипт на стороне сервера, посредством PHP. Этот скрипт будет постоянно спрашивать браузер о наличии данных об отправки. Если события не происходит, то браузер будет отвечать «нет». При нажатии на кнопку будут отправлены данные после того, как сервер снова запросит о наличии фреймов.
Нельзя использовать для данных операций объект XMLHttpRequest. JavaScript не может управлять заголовками передачи фреймов и пакетов. Только WebSocets! И только при наличии скрипта соединения, расположенного на сервере.
Для низкого соединения с интернетом, отправка больших данных может осуществляться с задержками. Свойство socket.bufferedAmount хранит количество байт, находящихся в буфере и ожидающих отправки. Чтобы увеличить скорость передачи буферизированных данных, следует воспользоваться условной конструкцией:
setInterval(() => {
if (socketObject.bufferedAmount == 0) {
socketObject.send(moreData());
}
}, 100);
Здесь каждые 100 миллисекунд будет отправляться больше данных, при условии, что все текущие байты уже отосланы.
Ошибки сокетов
Сокеты восприимчивы к сетевым сбоям, как и многие другие вэб-технологии. Сервер создается из множества функций и каждая из них может вернуть значение false при каком-либо сбое. Когда происходит сбой, то можно получить информацию об ошибки с помощью двух функций:
- socket_strerror($error_code). Здесь $error_code — это строковое значение, которое возвращено функцией socket_last_error(). Эта функция возвращает описание ошибки.
- socket_last_error($socketObject). Здесь $socket — это объект сокета, созданного функцией-конструктором socket_create(). Функция возвращает информацию об последней ошибке создания сокета. Эта ошибка представлена в виде целого числа.
Полезные функции для манипуляций с сокетами в PHP
- socket_accept(socketObject) — после запроса принимает соединение на стороне сервера для текущего сокета, который вставляется в параметр;
- socket_create_listen(port, connect_count) — принимает соединение на указанном порту первого параметра, а второй параметр указывает сколько максимум может быть в очереди ожидающих соединений;
- socket_get_option(socketObject, level_of_protocol, option) — получает параметры потока при соединении, где второй параметр указывается уровень протокола, на котором расположена опция из третьего параметра. Третий параметр использует одну из предопределенных констант, хранящих значения опций, ознакомиться с которыми можно в документации к PHP. Функция возвращает значение константы опции, при ошибке возвращает false;
- socket_last_error(socketObject) — возвращает последнюю возникшую ошибку при сбое соединения в числовом формате;
- socket_listen(socketObject, connect_count) — прослушивает соединения с сервером, после того как сокет был создан, а клиент присоединен к серверу. Второй параметр указывает максимальное количество ожидаемых соединений, которых может находиться в очереди. В случае успеха возвращает true, или false при ошибке;
- socket_send(socketObject, sending_data, length) — отправляет данные клиенту, который подсоединился к сокету. Третий параметр указывает максимальное количество байт, которые будут отправлены на сервер. Возвращает количество отправленных байтов или false;
- socket_sendto(socketObject, sending_data, length, adress, port) — отправка сообщения длиной length по адресу address через порт port посредством сокет-протокола даже при отсутствии подключения;
- socket_set_block(socketObject) — включает режим блокировки отправки данных до тех пор, пока сокет не получит сигнал на отправку данных – работа скрипта на сервере будет приостановлена. В случае ошибки блокировки соединения будет возвращено false;
- socket_set_option(socketObject, level_of_protocol, option) — настраивает параметры сокета. Последний параметр принимает только одну из предусмотренных в PHP констант;
- socket_strerror(code_of_error) — возвращает ошибку в формате строки. В параметр записывается код ошибки после выполнения функции socket_last_error();
- socket_write(socketObject, data, length) – осуществляет запись данных в сокет. Необязательный параметр length позволяет установить ограничение на размер записываемых данных, который указывается в количестве байт. Возвращает количество байт, записанных в сокет или false, в случае возникновения ошибки.
Закрытие сокета
Когда необходимо преднамеренное закрытие сокета клиентом, то используют функцию socket.close([code], [reason]). Данное мероприятие не делается вручную, а автоматизируется за счет скрипта с клиентской стороны. Пользователям приложения не нужно знать что такое сокет и не следует отводить для его закрытия какой-либо элемент интерфейса. Данная функция инкапсулируется исключительно в скрипте и выполняется автоматически в зависимости от условий.
Параметр code может быть:
1000 – нормальное закрытие, данное значение стоит по умолчанию;
1006 – указывает на потерю соединения при отсутствии закрывающего фрейма;
1001 – отключение сервера или пользователя, подсоединенного по сокету;
1009 – размер сообщения больше придела;
1011 – возникновение непредвиденной ошибки на сервере.
reason – второй параметр, является необязательной строкой, описывающей причины закрытия соединения. Этот параметр можно опустить или использовать как пояснительной записки для пользователей приложения.
Пример1: Само простое создание сокета
$address = ‘mail.ru’; // Записываем в переменную имя хоста
$port = 80;
$socketObject = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // Создаем объект сокета и добавляем в него нужные параметры, состоящие из предопределенных константsocket_connect($socketObject, $address, $port); // Присоединяемся к клиенту в случае отправки запроса с его стороны
socket_write($socketObject, «GET / HTTP/1.0\r\n\r\n»); //Делаем начальную запись в сокет$result = «»; // Переменная для хранения данных, передаваемых по сети
while($read = socket_read($socketObject, 1024)) //Получаем результат передачи данных по сокету от клиента
{
$result .= $read;
}
socket_close($socketObject); //Закрываем соединениеecho «Полученный результат: $result\r\n»; // Отображаем текст сообщения на экране
Одно, но значительное отличие WebSocket от XMLHttpRequest
Есть одно существенное отличие, которое не позволяет эти два объект считать собратьями. Дело в том, что эти технологии осуществляют передачу данных в фоновом режиме без перезагрузки страницы. Однако, XMLHttpRequest работает только с HTTP протоколом и не позволяет создать постоянное соединение как WS.
Каждый раз, когда XMLHttpRequest обращается к серверу, осуществляется новое соединение. Поэтому, данный объект не подходит для создания чатов и других приложений в реальном времени.
Есть ли альтернативный способ создания сокетов на сервере?
Безусловно не только на РНР можно создавать сокет-серверы. Это могут и другие языки, которые обладают нужными классами, а также пользуются специально разработанными библиотеками для создания вэб приложений. Это Java, C#, Python, Ruby.
Чтобы пользоваться сокет-сервером, его нужно создать. Конечно есть готовые скрипты. Однако создание вручную подразумевает неограниченные возможности в настройке сервера.
Со стороны клиента также необходимо писать скрипт для индивидуальных ситуаций. Но можно создать ссылку на JavaScript файл, который будет создавать нужное соединение. Однако события для отправки данных придется создавать самому программисту. Ведь для подключения к серверу всегда можно подобрать один работающий скрипт. Для элементов интерфейса придется создавать отдельные скрипты событий.
Итоговое резюме по вэб-сокетам
Сокеты – это протокол передачи данных, который позволяет передавать текст, изображения и другой контент без необходимости перезагрузки страницы.
Сокеты являются частью сеиуровневой модели OSI. Они расположены на прикладном уровне на ряду с HTTP. Транспортировка данных подчиняется протоколу TCP. Для использования сокетов не нужно осуществлять какие-либо настройки в рамках сети. Для этого достаточно создать файл со скриптом PHP и поместить его на сервер.
Сокет обеспечивает одно, но постоянное соединение клиента с серверов на всю сессию, которое осуществляется сразу после загрузки вэб-страницы. Для возможности соединения клиента с сервером и обмена данными, в обоих точках должны быть скрипты, реализующие возможность общения клиента и сервера.
Благодаря богатому набору функций и констант, передачу данных по сокету можно организовать разными способами. В PHP есть огромное количество функций для манипуляций с сокетами. Однако чаще всего используют лишь несколько из них.
Малым числом функций обладает объект WebSocket в браузере. Однако он единственный объект, который обеспечивает соединение по протоколу WS. WebSocket не является аналогией XMLHttpRequest. Эти объекты обеспечивают передачу данных в фоновом режиме, но подчиняются разным протоколам. Сокеты можно применять в любом приложении или сайте, не зависимо от типа приложения.
Недостаток сокетов – не поддерживаются в старых браузерах (отсутствует объект WebSocket) Чтобы проверить наличие такого объекта, достаточно в консоли браузера ввести его название и нажать Enter. Сокеты используются в онлайн-играх для передачи большого количества данных за короткий промежуток времени. Благодаря этому обеспечивается высокий показатель ping в играх (на fps не влияет, так как это зависит от железа компьютера).
Сокеты могут быть созданы как для создания одного соединения, так и одновременно нескольких – часто эта технология применяется в различных чатах, где участвует большое количество пользователей.
Простые скрипты для создания сокет-соеднения и поддержики передачи данных можно найти в интернете. Для того, чтобы усложнить, но придать гибкости сокету, следует знать все функции и константы, отведенные для сокетов в PHP.
Сокет работает только во время сессии. При закрытии приложения или окна браузера, связь обрывается. Автоматически создается новое соединение при повторной загрузке сайта или приложения. На хостинге сокет-соединение создается аналогичным образом с помощью скрипта. Там уже не придется снимать комментарии с поля «extension = php_sockets.dll».
Очень хорошая статья, правда столько непреднамеренных опечаток (или преднамеренных:)