Пишем телеграм-бот

Telegram – широкоизвестный и популярный мессендждер-соцсеть в узких кругах, и этот круг быстро расширяется благодаря стараниям его конкурентов. Под конкурентами я имею ввиду “традиционные” социальные сети, которые внедрением(ужесточением) цензуры отталкивают многих… Но эту статейку я хочу посвятить не свободе слова(или вседозволенности, как ее некоторый называют), а созданию бот-программы.

О Telegram

Он возник как эксперимент П.Дурова с технологией MTProto – методом шифрования, созданным его братом Н.Дуровым. MTProto – криптографический метод проитокол передачи данных, основанный на симметричном шифровании и ряде других приколюх, предназначенных для безопасной передачи данных. Все это значит, что исходящее сообщение могут прочесть только тот, кто его отправил, и тот, кому(те, кому – если это чат или канал) оно было отправлено. Именно эта защищенность от третьих лиц(не дающая возможности точечной модерации) и дает все новые и новые обороты в популярности этого приложения. Что и говорить – даже Трамп, после его изгнания из Twitter`а и прочих заокеанских сетей, создал Telegram-аккаунт!

Бот это что?

Телега имеет два вида API: Telegram API и Telegram Bot API. Первое служит для постройки своего приложения, использующее внутреннюю инфраструктуру(сервера по-сути) Телеграма, эти методы реализует TDLib. Бот – это также программа, но уже размещенная на своем сервере; еще отличие в том, что он(точнее, его интерфейс) может быть во всех программах использующих сервера телеги(в приложении Telegram точно, может быть реализовано в прогах на TDLib).

Итак,можно говорить было бы о Telegram API, но ты пришол сюда, вероятно, узнать о ботах. Первым делом – его возможности:

  • Получайте индивидуальные уведомления и новости
  • Интегрируйте с другими сервисами
  • Принимайте платежи от пользователей Telegram
  • Создавайте собственные инструменты
  • Создавайте одиночные и многопользовательские игры

Да, это то что перечислено в доках телеги – а что ты ожидал?? Я остановлюсь на пункте 4 и создам форму для оформления оформления заказа на сайте(по-сути, можно сказать пункт 2)

Создание бота. Теория

В этом разделе указана теория потому, что здесь я опишу алгоритм образования бота, так сказать – создать общее понимание процессов.

Прежде всего, как и при любой работе с программными интерфейсами, – аутентификация. Здесь не привычной аутентификации вроде получения открытого и закрытого ключей и, в начале каждой сессии, получение временного токена; вместо этого нужно написать боту, который, на основании предоставленных данных, зарегистрирует нового бота и выдаст тебе постоянный токен. После этого, в “диалоге” с ним же, выкатываем ему ввесь список команд, которые должны быть у бота.

Далее, мы должны сообщить адрес на который будут приходить все обновления – все действия с ботом -это вариант №1. Второй вариант – когда прослушка не установлена – все действия сохраняются на сервере мессенджера и, в течении 24 часов, можно получить этот лог. Эти два варианта – два взаимоисключающие методы: если установлена прослушка – лог на сервере вестись не будет и наоборот, но тогда события никуда не будут отправлены.

Создание бот. Практика

На практике всегда стоит использовать готовые, одобренные разрабами телеграма, библиотеками – об этом говорю не только я, но и документация, иначе может последовать бан. Но, дабы лучше понять, как это устроенно и работает – я реализую это нативно(без использования библиотек).

@botFather

Находим @BotFather, начинаем с ним переписку(отправляем команду “/start”). В ответ он перечислит все команды, которые ему доступны,, жмем “/newbot” и пишем слаг создаваемого бота. Если все прошло успешно(выполняется условие уникальности слага) – создается бот и ты получишь его токен; также, выполнив команду “/setcommands” можно указать список нужных команд(команда “/start” будет создана автоматически).

Перейдем непосредственно к программированию сервера нашего бота! Писать я буду на PHP, но разница невелика. Использую метод curl(), поскольку для задания пути нашего “слушателя” нужно отправить публичный ключ ssl-сертификата сервера с бот-программой. “Программного приветствия” нет, вот код для установки слушателя(его также можно установить из CLI)

<?php
    // инициализируем сеанс связи с нашим ботом
	$ch = curl_init( "https://api.telegram.org/bot{bot-token}/setWebhook" );
	// в заголовках сообщаем, что передаем файл
	curl_setopt( $ch, CURLOPT_HTTPHEADER, [ "Content-type: multipart/form-data" ] );
	curl_setopt( $ch, CURLOPT_POST, true );
	curl_setopt( $ch,
		CURLOPT_POSTFIELDS,
		[
			'url'         => 'https://'.$_SERVER['HTTP_HOST'],//  адрес слушателя
			'certificate' => '@' .realpath(__DIR__ . '/public.pem')// путь к публичному ключу
		] );

	curl_exec( $ch );

	curl_close( $ch );

В ответ этому скрипту, если не возникло ошибок, будет возвращена сторка

{"ok":true,"result":true,"description":"Webhook was set"}

но, на самом деле, это еще не показатель правильной установки адреса прослушки. Чтоб проверить правильность перейди по адресу https://api.telegram.org/bot{bot-token}/getWebhookInto, в случаи если все правильно будет

{"ok": true, "result": {"url": " https://xxx.xxx.xxx.xxx", "has_custom_certificate": true, "pending_update_count": 25, "last_error_date": 1484557151, "last_error_message" ":" Время ожидания соединения "," max_connections ": 40}}

В случаи, если интернет провайдер сервера твоего бота не блокирует Телеграм(намек на РФ), на адрес https://xxx.xxx.xxx.xxx будут приходить уведомления от бота. Например, пользователь сделал команду “/start”, на адрес https://xxx.xxx.xxx.xxx/start будет отправлен запрос.

В ответе на обновление от телеги должен содержатся один из “Допустимых типов”.

Погворим о втором варианте – получение обновлений самостоятельно. Прежде всего, нужно удалить прослушку: перейдите по адресу https://api.telegram.org/bot{bot-token}/setWebhook, при этом ты увидишь

{"ok":true,"result":true,"description":"Webhook was deleted"}

далее, любое действие с ботом будет записано и доступно по адресу https://api.telegram.org/bot{bot-token}/getUpdates в виде обьекта Update.

Как я говорил ранее, не делай этот код нативно используй одобренные разрабами библиотеки… все это лишь для понимания того, что происходит “под капотом” у этих библиотек! Оно пригодится…

Блокчеин, крипта и майнинг – технический взгляд на вопрос

Начало 2021 года… курс биткоина перевалил 40К долларов…. однако здравствуйте! В связи с очередной волной криптохайпа на рынке, решил я начеркать эту статейку – о крипте с технической стороны. С финансовой стороны(точнее мое, обывательское, отношение к ее ценности) – крипта такой же финансовый инструмент, как и доллар США. Объясню: ныне, доллар это цифра, сила которой обеспечена гарантиями правительства(системы) США. Крипта – это такая же цифра, обеспечена гарантиями системы, в которой она хранится – блокчейном. Ценность любого ресурса, в конце-концов, определяют люди. На этом, перейдем к сути статьи…

Содержание

Технический базис

Вся технология блокчейна, как и все в компьютерных сетях, где нет доверительных отношений, строиться вокруг хешей. Они создаются функциями, в алгоритме которых заложены следующие моменты:

  • независимо от длины входящей строки – хеш-код имеет строго определенную длину;
  • имея хеш невозможно восстановить исходную информацию;
  • результат хефирования идентичных строк идентичный;
  • один хеш может иметь только одну исходную строку.

Как, я думаю всем понято, последнее утверждение, теоретически, бессмысленное; поскольку, пул символов ограничен и их количество в хеш-строке также. Но я сказал теоретически – поскольку это число(вариантов комбинаций) велико(для алгоритма SHA-256, в биткоин, их аж 2 в 256-ой степени).

Блокчейн

Блокчейн – это, по большому счету, всего лишь база данных(ну или бухгалтерская книга, коль разговор в контексте криптовалюты). Да, в связи с особенностями его использования – она окружена различными проверками входящих данных. В виду ее безопасности(об этом позже) я б, к примеру, использовал данную схему для проведения электронных выборов; но на данный момент, ничего лучше, чем хранить крипту или свои идентификационные данные, люди не придумали(кста, го в комменты – почему так?).

Предлагаю(а точнее – настаиваю, раз уж ты это читаешь) разобраться, как она устроена и что из себя представляет.

Теоретический взгляд

Данная схема хранения данных предусматривает зависимость каждого следующего блока данных от предыдущего, с английского – “цепочка блоков”. Кроме того, для пущей безопасности, блокчейн делают децентрализованным – т.е. копия БД и сопровождающее ее ПО хранятся у каждого, кто использует данную сеть, и все они равноправны.

Децентрализованный блокчейн могут подменить только >50% пользователей этой системы, с одинаково сфальсифицированными блоками.

Майнинг

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

Теория

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

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

Рассмотрим на примере Биткоина. Первоначально, с полученных транзакций формируется дерево Меркла(о нем – ниже); после, берется корень этого дерева, конкатенируется с хешом предыдущего блока и случайной строкой(nonce-field); из полученной строки создается хеш и проверяется его сложность: если она выше, чем требует блокчейн для нового блока – составляется новый nonce-field и процесс повторяется, в ином случаи – создается новый блок, о чем оповещаются все майнеры.

Биткоин-блокчейн пересчитывает сложность хеша каждые 2016 блоков таким образом, чтоб процесс нахождения nonce-field(а соответственно, и создание блока) был равен 10 минутам.

Дерево Меркла

Дерево Меркла – это способ хранения информации в виде хешей. Оно строится так: каждая транзакция хешируется – таким образом, формируются листья первой очереди. Затем хеши соседних листьев складывают(по 2) и снова получают хеш этой строки. Эту операцию проделывают до тех пор пока не остается 1 строка – это и есть корень дерева Меркла.

дерево Меркла
дерево Меркла

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

Так зачем же оно надо? Дело в том, что каждая транзакция, после попадания в блок, находится в “замороженном” состоянии. Она получает окончательное одобрение системой только после нескольких подтверждений. Это подтверждение блок транзакций получает при постройке следующих блоков, в которые, замороженная транзакция также вносится; к примеру, в биткоине, для признания транзакции вырлаты награды полностью легальной, нужно 100 подтверждений(последующих блоков), а на оплату биткоинами 6. Так вот, каждая проверка подтверждения требует усилий сети, а такая структура их экономит. Кроме экономии вычислительных мощностей, эта структура позволяет строить “легкие” клиенты – хранящие только корни дерева.

Данные подтверждения, как я понял, нужны для предотвращения двойных трат средств.

Сказ о сложности

Дело в том, что хеш, который мы видим – это представление в шестнадцатеричной форме; его можно перевести в десятеричную – и тогда это будет обычное число. Сложность это разница между максимальным значением хеша и текущим хешем. Строка с максимальной сложностью это, по сути, шестнадцатеричная строка, состоящая с самого старшого символа в этом формате и переведенная в десятичный. Допустимыми символами в шестнадцатеричной системе есть десять цифр(0 – 9) и шесть первых букв латинского алфавита(a – f), таким образом – старший символ f. Формула перерасчета выглядит так

где d – порядковый номер символа(0 – 15), n – порядковый номер символа в строке.

Вот почему, когда говорят о сложности и цели в майнинге биткоин, большинство статей упоминают о предшествующих нулях в хеше – ведь они не вносят никакого влияния на величину хеша.

О майнинг-пулах

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

Кошелек и электронная подпись

Каждое перечисление или начисление крипты происходит с/на кошелек. Адрес кошелька это, по сути, хеш от каких-то персональных данных(либо, как у биткоин-кошельков, набор случайных символов) владельца. Однако, ввиду того, что блокчейн(как и любая компьютерная система) построен на недоверии – каждая транзакция требует подписи. Здесь и вступают на сцену публичный и секретный ключи. Работают они так: каждая транзакция содержит подпись(шифровку от хеша транзакции и секретного ключа) и публичный ключ; майнер, прежде чем внести новую информацию в блок, при помощи публичного ключа, дешифрует подпись; если хеш операции равен хешу после дешифрования – все ок.

Ремарка о подписи: в процессе шифрования используют хеш информации по причине экономии вычислительных мощностей.

По поводу баланса кошелька – система не хранит информацию о нем; он результат всех транзакций по адресу.

Что же такое криптовалюта?

Криптовалюта – это вознаграждение майнеру за формирование нового блока, т.е. за поддержание блокчейн-сети в рабочем состоянии. Оно состоит из фиксированной суммы(установленной сетью) и комисии с транзакций(также установленной сетью).

Например, в сети Биткоин, вознаграждение за первые 210 тыс. блоков было по 50 монет; спустя каждые 210 тыс. оно делится на 2. Примерно, к 2033 году эмиссия новых монет прекратится, т.е. майнеры будут получать только комиссию. И-да, ответить – как же их увидеть? – первая транзакция в начале каждого тела блока.

Заключение

Здесь я кратко прошёлся по основным моментам крипты и всем, что с ней связано. В одной из будущих статей реализуем свою валюту; не в виде токена на существующем блокчейне, а создадим с ничего… В общем, to be continue…

WebSocket’ы – что и как? реализация на PHP

Приветствую всех, кто заглянул ко мне на огонек! Сегодня речь пойдет о вебсокетах. Если ты пользовались мессенджерами, чатами, играли в онлайн-игры или смотрели прямые трансляции – ты однозначно были клиентом вебсокет-соединения.

Содержание

Теория

WebSocket — протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени. Протокол управления передачей(TCP) основной протокол передачи данных в интернете; он является подложкой для остальных протоколов, среди которых сравним http/https и ws/wss. Обычно, “общаясь” с сайтом, браузер использует http/https протоколы, которые работают в режиме “вопрос-ответ”, т.е. без запроса от браузера никакой информации от сервера не будет. Вебсокет протокол(ws и wss, защищенный, по аналогии с http/https), в свою очередь, держит “коридор” обмена данными постоянно открытым, т.е. сервер может по “своей” инициативе послать информацию.

“Рукопожатие”

Это, пожалуй, основное событие в вебсокет-общении. И несмотря на это, оно незаметно и никак не зависит от клиента(имеется ввиду – от человека в чате, либо другом приложении). Происходит оно так: клиент отправляет заголовки серверу

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

с предложением сменить протокол на websocket, вместе с этим присылает ключ. На основании ключа, сервер строит свой ключ и отсылает его в виде заголовков клиенту. Если полученный клиентом заголовок

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

содержит код ответа 101 и правильный ключ – ответ воспринимается как “да”. Сэтого момента соединение установлено и общение может быть начато, но если сервер не пришлет заголовков или будет неверен ключ – соединение будет разорвано.

Правила построения ответного ключа:

  • взять строковое значение из заголовка Sec-WebSocket-Key и объединить со строкой 258EAFA5-E914-47DA-95CA-C5AB0DC85B11;
  • вычислить бинарный хеш SHA-1 (бинарная строка из 20 символов) от полученной в первом пункте строки;
  • закодировать хеш в Base64.

Немного о передаваемых данных

Согласно спецификации RFC 6455 обмен данными происходит в виде фреймов. Вот блок-схема каждого фрейма

структура вебсокет-фрейма

С первого взгляда вспомнился мем “Ну нахер…”, ну да ладно! Из этой схемы и ее описания можно сделать заключение:

  • каждая “порция” информации передаётся фреймами, она может быть в одном или нескольких подряд;
  • каждый фрейм начинается с информации о том, как извлечь целевую информацию из него.

Теперь о самих правилах построения фреймов…

“Ингредиенты” фреймов

Каждый фрейм строится по следующих правилах:

  • Первый байт указывает на то, полная ли в нем информация(1), или будет продолжение(0). В случаи если фрейм обладает не полной инфой – нужно ждать закрывающего фрейма, в котором первый бит будет равен 1;
  • следующие 3 бита(обычно по 0) это расширение для протокола;
  • следующие 4 бита определяют тип полезных данных фрейма:
    • 0х1 – текстовые данные;
    • 0х2 – бинарные(файл);
    • 0х3-7- окно возможностей для полезной информации на будущее(сейчас таких данных нет);
    • 0х8 – фрейм с приказом закрыть соединение;
    • 0х9 – фрейм PING на проверку состояния соединения;
    • 0хА – фрейм PONG(ответ на PING, говорящий “все ОК”);
    • 0хB-F – окно возможностей для управления соединением на будущее(сейчас таких данных нет);
    • 0х0 – фрагментированный фрейм, являющийся продолжением предыдущего.
  • следующий бит(маска) указывает замаскирована ли инфомация фрейма;
  • следующие 7 бит или 7 бит + 2 или 8 байта(сейчас объясню) это длинна тела сообщения. Если эти 7 бит перевести в значение, то правила следующие:
    • если значение между 0 и 125 – это и есть длинна тела сообщения;
    • когда значение 126 – на длину тела указывают следующие 2 байта(16 бит);
    • значение строго больше 126 – на длину тела указывают следующие 8 байта(64 бит);
  • если маска установлена, 4 байта после длинны тела будут ее ключом – в ином случаи(маска неустановленная, т.е. 0) – этого слоя в фрейме не будет;
  • и в конце, в оставшемся будет содержатся полезная информация фрейма.

Эту информацию нужно знать для того, чтоб правильно составить функцию кодирования/декодирования информации сокета. Далее, я построю и эти функции.

Немного о битовых масках

Битмаски – это последовательность битов, предназначенных для маскирования целевой информации. Не буду излагать всю теорию о них(если интересно – загуглите или напишите в комментах), меня, в контексте вебсокетов, интересует лишъ одна операция – раз маскирование целевой информации методом xor(именно он указан в спецификации на протокол вебсокет)

Использование битмаски, механизм xor

Пусть верхний ряд цифр это битовая маска, а средний – скрытая информация. Будем рассматривать столбцы цыфр. Следуя операции xor нужно сравнить бит маски с соответствующим битом скрытой инфы, и если они совпадают, то результат false(0 у выражении состояния бита) и наоборот. Проделав эту операцию со всеми битами(длина маски = длине полезной информации) будет получена строка с исходными данными. Ключом маски называется наименьший повторяющийся участок маски – именно его хранит каждый фрейм.

Балабольство о практике

Как следует из ранее сказанного – для соединения нужно иметь сервер и клиент. На сервере будет запущен демон(никакого отношения к мифологии, всего-лишъ – вечно работающий скрипт), а клиент будет будет веб-страницей.

В идеале, сервер должен быть написан на языке программирования, поддерживающем асинхронность, например серверный javascript(node.js). Но, допустим, это пристройка к проекту на PHP, среди доступных разработчиков нет(я о тебе, т.к .полагаю, что ты разраб) обладающих знаниями или не желающий изучить новое(что странно); или еще какая-то ведомая лишъ тебе причина. Как известно, пых пока что полностью синхронен, но для низко нагруженных проектов – этого достаточно – выльется только в задержки работы; для нагруженных проектов есть фреймворки, обеспечивающие асинхронность – reactPHP, amPHP, для вебсокет-серверов с асинхронностью – workerman, ratchet и т.д.

По ходу статьи я планирую реализовать следующее(на нативном php):

  • websocket-сервер на PHP;
  • HTML/JS-клиент для общения с сервером.

На деле и пых может быть клиентом и слушать сокет…

Реализация серверной части

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

Начнем реализацию, а именно под случай, когда нет доступа к серверу по SSH и запустить скрипт можно вызвав его в браузере. Для этого вначале снимем ограничение времени работы скрипта и запретим завершать работу после закрытия браузера

<?php
ignore_user_abort(true);
set_time_limit(0);

далее, создадим вебсокет, к которому и будут присоединятся клиенты

<?php
if (($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP))) {
    echo 'Сокет не создан. Причина: ' . socket_strerror(socket_last_error());
}

здесь сокет создается функцией socket_create(), которая при создании сокета возвращает сокет ресурса, иначе false; конструкция socket_strerror(socket_last_error()) всего получает текст ошибки, параметры это семейство протоколов, тип передачи данных и используемый протокол. Кста, забыл главное – проверь конфигурацию пыха – чтоб  --enable-sockets был true. Сейчас нужно привязать сокет и выставить его на прослушку, а также, сделать его неблокирующим

<?php
if (socket_bind($sock, 'localhost', 8080)) {
    echo 'Сокет не привязан. Причина: ' . socket_strerror(socket_last_error());
}

if (socket_listen($sock, 10)) {
    echo 'Сокет не прослушивается. Причина: ' . socket_strerror(socket_last_error());
}

socket_set_nonblock($sock);

здесь первый – сокет созданный ранее, ‘localhost’ – домен или IP адрес сервера, 8080 порт, через который будет открыт доступ. В socket_listen() второй параметр необязателен, будет указывать максимальное количество подключенных к нему клиентов.

На этой стадии, я имею сокет; чтоб превратить его в вебсокет – нужно запустить его как службу, т.е. в демон(цикл событий). В самом простом случаи сгодится такой код

<?php
while(true){
    // дальнейшая работа
}

Итак, первым, что нужно сделать – проверить наличие новых подключений(дальнейший код в этой главе должен быть расположен в цикле)

<?php
if ($connection = socket_accept($sock)) {
   $headers = socket_read($connection,1024);
}

При их наличии, прочтем их(получив при этом заголовки от клиента) и, исходя из ранее изложенных правил, сформируем серверные заголовки

      $parts = explode('Sec-WebSocket-Key:',$headers); 
      $secWSKey = trim(explode(PHP_EOL,$parts[1])[0]);
      $secWSAccept = base64_encode(sha1($secWSKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true));

      $answer = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        'Sec-WebSocket-Accept: ' . $secWSAccept,
        'Sec-WebSocket-Version: 13'
      ];

      if (stripos($headers,'name=') !== false) {
        $parts = explode('name=',$headers,2);
        $name = explode(' ',$parts[1])[0];
      }
      $name = isset($name) ? $name : 'Anonymous' . count($connections);

      socket_write($connection, implode("rn",$answer) . "rnrn");

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

      socket_write($connection,encodeToFrame('Server: Hello! Welcome to Chat, ' . $name));

      if (!empty($connections)) {
        foreach ($connections as $connect) {
          socket_write($connect->connection,encodeToFrame('Server: New User(' . $name . ') in Chat!'));
        }
      }

      $connections[] = (object) [
        'connection' => $connection,
        'name' => $name
      ];

программа – поприветствуем новенького, оповестим остальных о нем и запишем “контактные данные”(функции кодирования и декодирования рассмотрим отдельно). Малость не забыл, при передачи параметров методом GET, они будут доступны только в заголовке.

После проверки на новых – проверим каждый сокет на наличие фреймов, если они есть – декодируем их

    if (!empty($connections)) {
      foreach ($connections as $connect) {
        $message = frameDecode(socket_read($connect->connection,1024000));

        if ($message === 'break') {
          break 2;
        }

        if (!empty($message)) {
          foreach ($connections as $c) {
            socket_write($c->connection,encodeToFrame($connect->name . ': ' . $message));
          }

          $message = '';
        }
      }
    }

и отправим сообщение всем участникам. Поскольку цикл “живет” пока работает сервер сделаем возможность принудительной его(цикла) остановки. После выхода из цикла событий закроем сокет

socket_close($sock);

Декодирование фрейма

Здесь все просто – просто следуем правилам. Вначале, из полученной строки(в том, что это именно строка можно убедится распечатав ее var_dump-ом) фрейма извлечем первый и второй байты в двоичном виде(побитово)

  $firstByteToBits = sprintf('%08b', ord($frame[0]));
  $secondByteToBits = sprintf('%08b', ord($frame[1]));

извлечем информацию о типе полезной информации и ее длине

  $opcod = bindec(substr($firstByteToBits,4));
  $bodyLenght = bindec(substr($secondByteToBits,1));

  if ($bodyLenght < 126) {
    $bodyLenght = $bodyLenght;
    $maskKey = substr($frame,2,4);
    $body = substr($frame,6,$bodyLenght);
  } elseif ($bodyLenght === 126) {
    $bodyLenght = sprintf('%16b',substr($frame,2,2));
    $maskKey = substr($frame,4,4);
    $body = substr($frame,8,$bodyLenght);
  } else {
    $bodyLenght = sprintf('%64b',substr($frame,2,8));
    $maskKey = substr($frame,10,4);
    $body = substr($frame,14,$bodyLenght);
  }

если ее длинна хранится в следующих 16-ти или 64-х битах – извлечем следующие 2 или 8 байта(8 бит = 1 байт, кто забыл, а 1 байт = 1 символ строки) в битовой форме

Проверим кадр на целостность(фрагментирование), на тип информации и наличие маскировки(я буду пропускать фрагментированные, не маскированные и кары с нетекстовой информацией)

  if ((int)$secondByteToBits[0] === 0 || $firstByteToBits[0] === 0 || $opcod !== 1) {
    return '';
  }

О снятии маски – фрейм содержит 32-битный ключ и имеет смысл разбивать полезные данные данные участками по 32 бита, и уже их демаскировать(склеить конкатенацией)

  $i = 0;
  $unmaskedBody = '';

  while ($i < $bodyLenght/4) {
    $unmaskedBody .= substr($body,4*$i,4) ^ $maskKey;
    $i++;
  }

Если все же нужно обрабатывать и фрагментированный фреймы – стоит взглянуть в сторону глобальных переменных и конкатенации демаскированных строк.

Кодирование в фрейм

Эта часть оказалась немного сложнее, по крайней мере для меня – у меня образование не связанное с ЕОМ, а в документации об этом я ничего не нашел. Дело в том, что для шифрования длины – я должен понимать откуда 125, 126 и 127 ? Тут я сделал предположение

Имеется 7 бит, которые могут иметь 128(27) положений. Первое – это 0 символов(байт), 2 положения(126 и 127) это для информирования о том, какому правилу следовать. Исходя из этого, 126-ое положение свидетельствует о длине сообщения = 125 байт. Из этого можно сделать вывод – максимальная длина сообщения, содержащаяся в 16-ти битах = 216 – 1, в 64 = 264 – 1.

Подытожим все это в коде: определим значение 7-ми последних битов второго байта и, по необходимости, 2-ох или 8-ми последующих байтов

  $opcodInBits = sprintf('%04b', 1);
  $bodyLenght = strlen($content);
  $bodyLenghtInSecondByte = sprintf('%07b',$bodyLenght);
  $extendedLenght = '';

  if ($bodyLenght > 125) {
    if ($bodyLenght < 65536) {
      $bodyLenghtInSecondByte = sprintf('%07b',126);
      $extendedLenght = sprintf('%32b',$bodyLenght);
    } elseif ($bodyLenght > 65535 && $bodyLenght < 4294967296) {
      $bodyLenghtInSecondByte = sprintf('%07b',127);
      $extendedLenght = sprintf('%64b',$bodyLenght);
    } else {
      return '';
    }
  }

  $firstByte = chr(bindec('1000' . $opcodInBits));
  $secondByte = chr(bindec('0' . $bodyLenghtInSecondByte));

Несмотря на то, что протокол говорит о равных правах клиента и сервера, сервер требует маскированных данных, клиент – открытых.

Реализация клиентской части

Создадим простое текстовое поле и поле для сообщений

  <table>
    <tr>
      <td>
        <textarea name="name" rows="8" cols="80" id="input"></textarea><br>
        <button type="button" name="button" id="sendToServer">Send</button>
      </td>
    </tr>
    <tr>
      <td id="messages"></td>
    </tr>
  </table>

а далее, javascript-обьект для общения с серверным сокетом

    var socket = new WebSocket('ws://localhost:8080?name=Bogdan');

он имеет 4 метода, которые срабатывают при разных событиях

    socket.onopen = function() {
      console.log('connected open!');
    }

    socket.onerror = function() {
      console.log('connection error!');
    }

    socket.onclose = function() {
      console.log('connection closed!');
    }

    socket.onmessage = function(e) {
      document.getElementById('messages').innerHTML += '<p>' + e.data + '</p>';
    }

Первый метод сработает при успешном обмене заголовками, второй – ошибка создания объекта связи или ошибка “рукопожатия”. Следующий – это закрытие соединения и последний – принятие сообщения на сокете.

В отличии от PHP javascript-обьект уже содержит методы и автоматически кодирует/декодирует фреймы.

Заключение

Этой статьей я попытался объяснить технологию вебсокетов максимально просто. Увесь код с этой статьи лежит здесь. Данный протокол обмена сообщениями можно использовать не только для real-time приложений, но и как замена ajax(если нужно экономить трафик). Обьяснение простое – вебсокет обменивается “большим” заголовком только при установлении связи, в свою очередь, ajax сопровождает любой свой запрос+ответ двумя “большими” заголовками.

Namespace на пальцах

Всем привет и здравствуйте! Сегодня речь пойдет об пространстве имен(nаmespaсe). Эта фича чисто объектно-ориентированного программирования; несмотря на то, что пространство имен есть во многих языках программирования(C++, python, java и т.д.), код, приведенный в примерах, будет на php.

Содержание

Теория о nаmespase

Пространство имён (англ. namespace) — некоторое множество, под которым подразумевается модель, абстрактное хранилище или окружение, созданное для логической группировки уникальных идентификаторов (то есть имён).

Идентификатор, определённый в пространстве имён, ассоциируется с этим пространством. Один и тот же идентификатор может быть независимо определён в нескольких пространствах. Таким образом, значение, связанное с идентификатором, определённым в одном пространстве имён, может иметь (или не иметь) такое же значение, как и такой же идентификатор, определённый в другом пространстве. Языки с поддержкой пространств имён определяют правила, указывающие, к какому пространству имён принадлежит идентификатор (то есть его определение).

Такое определение дает нам википедия, однако оно, как по мне, достаточно сложное для понимания. Я же определю более просто – это дополнительная координата для обращения к классу. Рассмотрим примеры для большего понимания…

Примеры

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

Теперь применительно к коду: 2 программиста написали по одинаково названному классу

<?php
class ClassName {
    // some methods
}

и отправили третьему. Тот, должен использовать эти классы для сборки приложения – но как к ним обращаться? Вариант №1 – на берегу договориться о наименовании классов – это подойдет для небольшого их количества, но если их десятки и сотни? Вариант №2 -правильный вариант – договориться об использовании пространств имен

<?php
namespace developer1;

class ClassName {
    // some methods
}
<?php
namespace developer2;

class ClassName {
    // some methods
}

вот так это будет выглядеть в php. Далее, третьему нужно использовать эти классы в своем коде. Чтоб обратиться к такому классу, нужно сначала указать пространство имен, а затем имя класса

<?php
$var1 = new developer1ClassName();
$var2 = new developer2ClassName();

это годится при единичном вызове каждого класса, но что если ссылок на каждый класс множество? каждый раз писать пространство имени класса? По меньшей мере, это не удобно. Для этого есть директива use as (для php, в других языках есть аналоги)

<?php
use developer1ClassName as ClassName1;
use developer2ClassName as ClassName2;

$var1 = new ClassName1();
$var2 = new ClassName2();

По сути, это работает как “локальное переименование”, т.е классы ClassName1 и ClassName2 будут существовать только в пределах файла с этими директивами. В случаи если начальные классы имеют разные названия, директиву use as можно сократить до use

<?php
use developer1ClassName;

$var = new ClassName();

Работать это будет так: при обращении к классу ClassName будет проверены все директивы use и если есть та, которая на конце имеет такое же имя класса, будет обращение к пространству этого имени.

Автозагрузка классов

Обычно, чтоб получить доступ к файлу, используют функцию на подобие require(), применяя ее к каждому файлу. Когда проект большой и требует подключения множества файлов, а также написан с соблюдением принципов ООП(что в нынешних реалиях маст хэв,

пьяная плесень - монгольский

т.е. обязательно) это неудобно. В этом случаи удобно использовать “автозагрузку классов”. Дело в том, что при обращении к незнакомому классу, возбуждается функция, которой передается название требуемого класса(если известно его имя пространства – будет передано и оно).

Таким образом, объявив эту функцию раньше вызова неизвестного класса, можно подключить ее файл. Реализую это на php.

Сначала приведем структуру директорий и названий классов в соответствие с namespace и именем класса. Есть две функции для подобной работы – __autoload()(устаревшая с php 7.2) и spl_autoload_register()(появившаяся в php 5.0). Также, с php 5.3 добавлены анонимные функции и код автозагрузчика будет выглядеть так

<?php
spl_autoload_register(function($class)
{
  require($class . '.php');
});

Использование анонимной функции необязательно, можно, как аргумент, указать имя функции-загрузчика.

Заключение

В этой, сравнительно небольшой, статье рассмотрены пространства имен для классов. Наведены 2 способа их использования и, несмотря на то, что код приведен лишь для языка php, для других – эти принципы работы, также, сохраняются.