тут блогhttps://blog.gelin.ru/2024-01-29T14:21:04+06:00Общественные обязательства интроверта.<br>Сообщения на ИТ тематику, но не обязательно.Об ORM2024-01-29T00:00:00+06:002024-01-29T14:21:04+06:00Денис Нелюбинtag:blog.gelin.ru,2024-01-29:/2024/01/orm-jpa.html<p>Как известно,
в базах данных,
как правило, реляционных,
у нас таблицы.
С колонками.
А в документо-ориентированных БД лежат документы,
в формате, например, JSON.
С полями.
Объединённые в коллекции.</p>
<p>Самое интересное,
что таблицы могут ссылаться на другие таблицы.
Все эти внешние ключи и тому подобное.
Собственно,
одинокие таблицы, ни с чем …</p><p>Как известно,
в базах данных,
как правило, реляционных,
у нас таблицы.
С колонками.
А в документо-ориентированных БД лежат документы,
в формате, например, JSON.
С полями.
Объединённые в коллекции.</p>
<p>Самое интересное,
что таблицы могут ссылаться на другие таблицы.
Все эти внешние ключи и тому подобное.
Собственно,
одинокие таблицы, ни с чем не связанные,
это довольно бесполезно.
В документных БД документы из одних коллекций
тоже могут ссылаться на документы других коллекций.
Но и сами документы могут
содержать вложенные документы.
И даже целые коллекции/списки/массивы вложенных документов.
Документы — они не обязательно совсем уж плоские.</p>
<p><a href="https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5">ООП</a> учит нас,
что программа оперирует объектами.
Структурами в памяти.
У которых есть какие-то методы.
И какое-то состояние.
И объекты могут ссылаться на другие объекты.
Собственно,
граф объектов в памяти —
это и есть то,
с чем работает ООП программа.</p>
<p>Получается,
чтобы работать с данными в базе данных,
нам нужно уметь загружать данные из БД в память,
и сохранять данные из памяти в БД.
В динамически типизированных языках
(вроде Python, или JavaScript, или PHP)
обычно сильно не заморачиваются,
и представляют отдельные строки таблицы реляционной БД
как универсальную структуру данных
<a href="https://ru.wikipedia.org/wiki/%D0%90%D1%81%D1%81%D0%BE%D1%86%D0%B8%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2">ассоциативный массив</a>.
Это dictionary в Python,
object в JavaScript,
array в PHP
или <code>Map</code> в Java.</p>
<p>Но в статически типизированных языках
работать с <code>Map</code> как минимум менее удобно,
чем с нормальными объектами.
Поэтому нам нужен <a href="https://ru.wikipedia.org/wiki/ORM">ORM</a> — Object-Relational Mapping.
То есть способ получать объекты из таблицы БД
и сохранять объекты в таблицу БД.
К сожалению,
многие реализации ORM берут на себя слишком много всего дополнительного.</p>
<p>Можно пойти в лоб.
Сделать простейшее решение.
Полностью разделить собственно объекты,
с которыми мы работаем.
И то, как мы их извлекаем и пишем из/в БД.
Пусть объекты остаются объектами.
Пусть SQL остаётся SQL,
и мы продолжим его использовать.
Но нам понадобятся явные узкоспецифичные методы
извлечения и сохранения данных.
То, что называют «репозиторий».</p>
<p>Таковы приёмы работы,
например, с <a href="https://spring.io/guides/gs/relational-data-access/"><code>JdbcTemplate</code></a> в Spring.
Явный SQL.
Явное преобразование колонок таблицы в поля объектов и наоборот.</p>
<div class="highlight"><pre><span></span><code><span class="kd">class</span> <span class="nc">H2GetRfidTagRepository</span><span class="p">(</span>
<span class="kd">private</span> <span class="kd">val</span> <span class="nv">jdbc</span><span class="p">:</span> <span class="n">JdbcOperations</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="n">GetRfidTagRepository</span> <span class="p">{</span>
<span class="kd">override</span> <span class="kd">fun</span> <span class="nf">getRfidTagByNumber</span><span class="p">(</span><span class="n">tagNumber</span><span class="p">:</span> <span class="kt">String</span><span class="p">):</span> <span class="n">RfidTagModel? </span><span class="p">{</span>
<span class="kd">val</span> <span class="p">(</span><span class="nv">sql</span><span class="p">,</span> <span class="nv">params</span><span class="p">)</span> <span class="o">=</span> <span class="n">buildQuery</span><span class="p">(</span><span class="n">tagNumber</span><span class="p">)</span> <span class="c1">// строим запрос с одним параметром</span>
<span class="kd">val</span> <span class="nv">result</span> <span class="o">=</span> <span class="n">jdbc</span><span class="p">.</span><span class="na">query</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">ResultSetExtractor</span> <span class="p">{</span> <span class="n">rs</span> <span class="o">-></span> <span class="c1">// выполняем запрос</span>
<span class="n">extractResult</span><span class="p">(</span><span class="n">rs</span><span class="p">)</span> <span class="c1">// извлекаем объект из ResultSet</span>
<span class="p">},</span> <span class="o">*</span><span class="n">params</span><span class="p">.</span><span class="na">toTypedArray</span><span class="p">())</span> <span class="c1">// передаём параметры запроса</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
<span class="kd">internal</span> <span class="kd">fun</span> <span class="nf">buildQuery</span><span class="p">(</span><span class="n">tagNumber</span><span class="p">:</span> <span class="kt">String</span><span class="p">):</span> <span class="n">Pair</span><span class="o"><</span><span class="kt">String</span><span class="p">,</span> <span class="n">List</span><span class="o"><</span><span class="kt">Any</span><span class="o">>></span> <span class="p">{</span>
<span class="kd">val</span> <span class="nv">sql</span> <span class="o">=</span> <span class="s">"""</span>
<span class="s"> SELECT</span>
<span class="s"> t.id AS tagId,</span>
<span class="s"> t.name AS tagName,</span>
<span class="s"> t.number AS tagNumber,</span>
<span class="s"> t.owner_id AS tagOwnerId,</span>
<span class="s"> t.vehicle_id AS tagVehicleId</span>
<span class="s"> FROM rfid_tag t</span>
<span class="s"> WHERE t.number = ?;</span>
<span class="s"> """</span><span class="p">.</span><span class="na">trimIndent</span><span class="p">()</span> <span class="c1">// обыкновенный SQL с параметром </span>
<span class="k">return</span> <span class="n">sql</span> <span class="n">to</span> <span class="n">listOf</span><span class="p">(</span><span class="n">tagNumber</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">internal</span> <span class="kd">fun</span> <span class="nf">extractResult</span><span class="p">(</span><span class="n">rs</span><span class="p">:</span> <span class="n">ResultSet</span><span class="p">):</span> <span class="n">RfidTagModel? </span><span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">rs</span><span class="p">.</span><span class="na">next</span><span class="p">())</span> <span class="p">{</span> <span class="c1">// из одной (в данном случае единственной) строки результата</span>
<span class="k">return</span> <span class="n">RfidTagModel</span><span class="p">(</span> <span class="c1">// создаётся объект</span>
<span class="n">id</span> <span class="o">=</span> <span class="n">rs</span><span class="p">.</span><span class="na">getString</span><span class="p">(</span><span class="s">"tagId"</span><span class="p">),</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">rs</span><span class="p">.</span><span class="na">getString</span><span class="p">(</span><span class="s">"tagName"</span><span class="p">),</span>
<span class="n">number</span> <span class="o">=</span> <span class="n">rs</span><span class="p">.</span><span class="na">getString</span><span class="p">(</span><span class="s">"tagNumber"</span><span class="p">),</span>
<span class="n">ownerId</span> <span class="o">=</span> <span class="n">rs</span><span class="p">.</span><span class="na">getString</span><span class="p">(</span><span class="s">"tagOwnerId"</span><span class="p">),</span>
<span class="n">vehicleId</span> <span class="o">=</span> <span class="n">rs</span><span class="p">.</span><span class="na">getString</span><span class="p">(</span><span class="s">"tagVehicleId"</span><span class="p">)</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="kc">null</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<div class="highlight"><pre><span></span><code><span class="kd">class</span> <span class="nc">H2CreateChargingSessionRepository</span><span class="p">(</span>
<span class="kd">private</span> <span class="kd">val</span> <span class="nv">jdbc</span><span class="p">:</span> <span class="n">JdbcOperations</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="n">CreateChargingSessionRepository</span> <span class="p">{</span>
<span class="kd">override</span> <span class="kd">fun</span> <span class="nf">createNewChargingSession</span><span class="p">(</span><span class="n">connectorId</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="n">rfidTagId</span><span class="p">:</span> <span class="kt">String</span><span class="p">):</span> <span class="kt">String</span> <span class="p">{</span>
<span class="kd">val</span> <span class="nv">id</span> <span class="o">=</span> <span class="n">UUID</span><span class="p">.</span><span class="na">randomUUID</span><span class="p">().</span><span class="na">toString</span><span class="p">()</span> <span class="c1">// уникальный случайный ID новой записи</span>
<span class="kd">val</span> <span class="p">(</span><span class="nv">sql</span><span class="p">,</span> <span class="nv">params</span><span class="p">)</span> <span class="o">=</span> <span class="n">buildQuery</span><span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="n">connectorId</span><span class="p">,</span> <span class="n">rfidTagId</span><span class="p">)</span> <span class="c1">// строим запрос с тремя параметрами</span>
<span class="n">jdbc</span><span class="p">.</span><span class="na">update</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="o">*</span><span class="n">params</span><span class="p">.</span><span class="na">toTypedArray</span><span class="p">())</span> <span class="c1">// выполняем запрос</span>
<span class="k">return</span> <span class="n">id</span>
<span class="p">}</span>
<span class="kd">internal</span> <span class="kd">fun</span> <span class="nf">buildQuery</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="n">connectorId</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="n">rfidTagId</span><span class="p">:</span> <span class="kt">String</span><span class="p">):</span> <span class="n">Pair</span><span class="o"><</span><span class="kt">String</span><span class="p">,</span> <span class="n">List</span><span class="o"><</span><span class="kt">Any</span><span class="o">>></span> <span class="p">{</span>
<span class="kd">val</span> <span class="nv">sql</span> <span class="o">=</span> <span class="s">"""</span>
<span class="s"> INSERT INTO charging_session (id, connector_id, rfid_tag_id)</span>
<span class="s"> VALUES (?, ?, ?);</span>
<span class="s"> """</span><span class="p">.</span><span class="na">trimIndent</span><span class="p">()</span> <span class="c1">// обыкновенный SQL с параметрами</span>
<span class="k">return</span> <span class="n">sql</span> <span class="n">to</span> <span class="n">listOf</span><span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="n">connectorId</span><span class="p">,</span> <span class="n">rfidTagId</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>В принципе,
ничего плохого тут нет.
Кроме того,
что репозитории и SQL запросы существуют явно.
И нет красивого способа прозрачно загрузить связанные объекты из БД
по мере необходимости.
Загрузили, обработали, выгрузили.
Если в процессе обработки нужно загрузить что-то ещё,
нужно загрузить это что-то явно,
и явно связать с уже имеющимися объектами.
Ничего плохого,
это прекрасно ложится на цикл работы веб-приложений.
Всё равно для обработки пользовательского запроса
нужно загрузить, обработать и сохранить данные.</p>
<p>Но под ORM обычно понимают несколько большее.
Типичный ORM делает ещё целую кучу вещей:</p>
<ul>
<li>преобразование строк-колонок в объекты-поля</li>
<li>преобразование объектов-полей в строки-колонки (собственно, ORM)</li>
<li>безопасное построение SQL запросов (через какое-то API)</li>
<li>сокрытие SQL, получение объектов через API ORM</li>
<li>прозрачное получение зависимых объектов и данных</li>
<li>сокрытие персистентности объектов (вы, пользуясь полученными из БД объектами, можете не знать, что они хранились в БД и можете не контролировать момент, когда их надо сохранить в БД)</li>
<li>управление кэшем объектов в памяти (следствие сокрытия персистентности)</li>
<li>управление жизненным циклом объекта (следствие сокрытия персистентности, это больше не ваши объекты, их контролирует ORM)</li>
<li>контроль схемы БД по форме описанных объектов (вы можете не контролировать таблицы в БД, ORM сделает это за вас)</li>
<li>создание классов объектов из схемы БД (противоположное предыдущему)</li>
</ul>
<p>Лично у меня всё,
что идёт дальше первых трёх пунктов в этом списке,
вызывает большие опасения.
Неконтролируемый кэш?
Неконтролируемые объекты в памяти?
Я не знаю в точности,
когда мои изменения будут сохранены в БД?
Как это вообще?</p>
<p>Насколько сильно ORM скрывает свою магию
сильно зависит от API.
Скажем,
давным-давно популярным паттерном было
<a href="https://ru.wikipedia.org/wiki/ActiveRecord">ActiveRecord</a>.
Единственно популярный способ работы с БД
в <a href="https://guides.rubyonrails.org/active_record_basics.html">Ruby on Rails</a>.
В ActiveRecord все операции работы с данными,
включая метод <code>save()</code>,
это методы самого класса,
представляющего таблицу.
За это ActiveRecord и критикуют.
Мол, объекты с данными
явно выпячивают,
что они объекты с персистентными данными.</p>
<div class="highlight"><pre><span></span><code><span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"David"</span><span class="p">,</span> <span class="ss">occupation</span><span class="p">:</span> <span class="s2">"Code Artist"</span><span class="p">)</span>
<span class="n">users</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s1">'David'</span><span class="p">,</span> <span class="ss">occupation</span><span class="p">:</span> <span class="s1">'Code Artist'</span><span class="p">)</span><span class="o">.</span><span class="n">order</span><span class="p">(</span><span class="ss">created_at</span><span class="p">:</span> <span class="ss">:desc</span><span class="p">)</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">find_by</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s1">'David'</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="s1">'Dave'</span>
<span class="n">user</span><span class="o">.</span><span class="n">save</span>
</code></pre></div>
<p>В мире Java придумали свой API для ORM
— <a href="https://en.wikipedia.org/wiki/Jakarta_Persistence">JPA</a>.
Java (или Jakarta) Persistence API.
Самая популярная его реализация: <a href="https://en.wikipedia.org/wiki/Hibernate_(framework)">Hibernate</a>.
А ещё есть <a href="https://docs.spring.io/spring-data/jpa/reference/index.html">Spring Data JPA</a>,
обёртка над тем же Hibernate.</p>
<p>Получение данных в JPA начинается с репозитория.
Который вроде как всего лишь интерфейс.
А его реализация генерируется на лету исходя из известной схемы данных
(да, Hibernate анализирует ваши таблицы,
и даже может сам их создавать)
и описанных сущностей.</p>
<div class="highlight"><pre><span></span><code><span class="nd">@NoRepositoryBean</span>
<span class="kd">interface</span> <span class="nc">MyBaseRepository</span><span class="o"><</span><span class="n">T</span><span class="p">,</span> <span class="n">ID</span><span class="o">></span> <span class="kd">extends</span> <span class="n">Repository</span><span class="o"><</span><span class="n">T</span><span class="p">,</span> <span class="n">ID</span><span class="o">></span> <span class="p">{</span>
<span class="n">Optional</span><span class="o"><</span><span class="n">T</span><span class="o">></span> <span class="nf">findById</span><span class="p">(</span><span class="n">ID</span> <span class="n">id</span><span class="p">);</span>
<span class="o"><</span><span class="n">S</span> <span class="kd">extends</span> <span class="n">T</span><span class="o">></span> <span class="n">S</span> <span class="nf">save</span><span class="p">(</span><span class="n">S</span> <span class="n">entity</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">interface</span> <span class="nc">UserRepository</span> <span class="kd">extends</span> <span class="n">MyBaseRepository</span><span class="o"><</span><span class="n">User</span><span class="p">,</span> <span class="n">Long</span><span class="o">></span> <span class="p">{</span>
<span class="n">User</span> <span class="nf">findByEmailAddress</span><span class="p">(</span><span class="n">EmailAddress</span> <span class="n">emailAddress</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>А сами данные — это обычные <a href="https://en.wikipedia.org/wiki/JavaBeans">java beans</a>,
помеченные специальными аннотациями,
чтобы указать,
кто здесь первичный ключ,
а где внешние ключи,
и вообще,
какие тут имеются связи между таблицами/объектами.</p>
<div class="highlight"><pre><span></span><code><span class="nd">@Entity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Customer</span> <span class="p">{</span>
<span class="nd">@Id</span>
<span class="nd">@GeneratedValue</span><span class="p">(</span><span class="n">strategy</span> <span class="o">=</span> <span class="n">GenerationType</span><span class="p">.</span><span class="na">AUTO</span><span class="p">)</span>
<span class="kd">private</span> <span class="n">Long</span> <span class="n">id</span><span class="p">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">firstName</span><span class="p">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">lastName</span><span class="p">;</span>
<span class="c1">//...</span>
<span class="p">}</span>
</code></pre></div>
<p>Как получать данные, более-менее понятно.
Есть хитрые методы репозитория.
Есть магия превращения хитрого названия метода в запрос к БД.
При большом желании и крайней необходимости можно и SQL свой написать.
Только это будет не SQL,
а специальный язык запросов Hibernate.
Там вместо имён таблиц используются имена классов сущностей.
И условия выборки и джойнов описываются в более «объектном» стиле.
Мне такое не нравится.
Такой запрос не выполнишь так просто в консоли БД,
потому что это не SQL,
он лишь очень похож на SQL.</p>
<p>Как добавлять данные, тоже более-менее понятно.
Мы создаём объект сущности как-то минуя репозиторий.
Это же всего лишь приправленный магией обычный java bean.
Можно использовать <code>new</code>.
А потом вызываем метод <code>save()</code> репозитория.</p>
<p>А как обновлять данные?
И вот тут вылезает необходимость знать кучу подкапотной магии JPA и Hibernate.
Оказывается объекты-сущности помнят,
с какой именно строчкой таблицы в БД они связаны
(ну, по первичному ключу).
Объект, полученный из репозитория,
и объект, созданный через <code>new</code>,
различаются.
Второй знает,
что он не соответствует ни одной строке в таблице,
он новый.
И только вызов <code>save()</code> у репозитория выдаст ему первичный ключ
и сделает объект managed,
теперь hibernate заботится о его жизненном цикле.</p>
<p>Так вот,
update — это просто вызов сеттера у managed сущности.
Hibernate запомнит,
что сущность изменилась.
И будет сохранять её изменившееся состояние.
Когда?
Когда будет завершаться транзакция.
Да, у нас ещё появляются транзакции.
В простейшем случае нам не нужно особо заботиться о них.
Тот же Spring MVC будет обрамлять в транзакцию
обработку каждого HTTP запроса.
Но если мы хотим точно контролировать,
когда изменённые данные попадут в БД,
нам нужно явно позаботиться о транзакциях.</p>
<p>С одной стороны хорошо.
Есть репозиторий для получения данных.
Далее с этими данными можно работать как с обычными объектами.
Только не совсем как с обычными.
Их нельзя так просто клонировать.
Их нельзя так просто получить как результат десериализации.
Ну и надо всегда помнить,
что где-то в памяти есть невидимый кэш всех произведённых изменений,
который нужно рано или поздно,
явно или неявно сохранить в БД.</p>
<p>Мне не нравится.
Слишком много скрытой магии,
которая может драматично повлиять на ход вещей.
Что-нибудь может потеряться и не сохраниться.</p>
<p>В одном старом проекте для Андроида я попробовал другой подход.
Вся бизнес-логика,
все сущности и операции над ними,
описаны интерфейсами.
Каждый метод интерфейса — это атомарное и консистентное изменение.
Для изменений,
которые невозможно или не хочется выразить одним вызовом метода,
есть <code>Editor</code>,
который по сути является <a href="https://en.wikipedia.org/wiki/Unit_of_work">Unit of work</a>
и <a href="https://en.wikipedia.org/wiki/Builder_pattern">Builder</a>.
То есть несколько изменений объединяются в транзакцию,
которая должна быть завершена явно.</p>
<div class="highlight"><pre><span></span><code><span class="kd">interface</span> <span class="nc">Entity</span><span class="o"><</span><span class="n">EditorType</span> <span class="kd">extends</span> <span class="n">Entity</span><span class="p">.</span><span class="na">Editor</span><span class="o">></span> <span class="p">{</span>
<span class="n">EditorType</span> <span class="nf">edit</span><span class="p">();</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">Editor</span> <span class="p">{</span>
<span class="kt">void</span> <span class="nf">commit</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">Action</span> <span class="kd">extends</span> <span class="n">Entity</span><span class="o"><</span><span class="n">Action</span><span class="p">.</span><span class="na">Editor</span><span class="o">></span> <span class="p">{</span>
<span class="n">Set</span><span class="o"><</span><span class="n">Folder</span><span class="o">></span> <span class="nf">getFolders</span><span class="p">();</span>
<span class="n">String</span> <span class="nf">getHead</span><span class="p">();</span>
<span class="n">String</span> <span class="nf">getBody</span><span class="p">();</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">Editor</span> <span class="kd">extends</span> <span class="n">Entity</span><span class="p">.</span><span class="na">Editor</span> <span class="p">{</span>
<span class="n">Editor</span> <span class="nf">setHead</span><span class="p">(</span><span class="n">String</span> <span class="n">head</span><span class="p">);</span>
<span class="n">Editor</span> <span class="nf">setBody</span><span class="p">(</span><span class="n">String</span> <span class="n">body</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>И есть реализация всех этих интерфейсов,
где все данные хранятся в SQLLite базе данных,
как оно принято в Андроиде.
Каждый геттер делает одну транзакцию.
<code>Editor.commit()</code> делает свою транзакцию.</p>
<div class="highlight"><pre><span></span><code><span class="kd">class</span> <span class="nc">SQLiteAction</span> <span class="kd">implements</span> <span class="n">Action</span> <span class="p">{</span>
<span class="kd">transient</span> <span class="n">SQLiteDatabase</span> <span class="n">db</span><span class="p">;</span>
<span class="kd">final</span> <span class="kt">long</span> <span class="n">id</span><span class="p">;</span>
<span class="n">String</span> <span class="n">head</span><span class="p">;</span>
<span class="n">String</span> <span class="n">body</span><span class="p">;</span>
<span class="n">SQLiteFolderSet</span> <span class="n">folders</span><span class="p">;</span>
<span class="c1">//...</span>
<span class="kd">public</span> <span class="n">Set</span><span class="o"><</span><span class="n">Folder</span><span class="o">></span> <span class="nf">getFolders</span><span class="p">()</span> <span class="p">{</span>
<span class="k">assert</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="na">id</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">checkDb</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="na">db</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="na">db</span><span class="p">.</span><span class="na">beginTransaction</span><span class="p">();</span>
<span class="k">try</span> <span class="p">{</span>
<span class="n">Cursor</span> <span class="n">cursor</span> <span class="o">=</span> <span class="n">ActionDao</span><span class="p">.</span><span class="na">selectFolders</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="na">db</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="na">id</span><span class="p">);</span>
<span class="n">List</span><span class="o"><</span><span class="n">SQLiteFolder</span><span class="o">></span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ArrayList</span><span class="o"><</span><span class="n">SQLiteFolder</span><span class="o">></span><span class="p">();</span>
<span class="k">while</span> <span class="p">(</span><span class="n">cursor</span><span class="p">.</span><span class="na">moveToNext</span><span class="p">())</span> <span class="p">{</span>
<span class="n">result</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">FolderDao</span><span class="p">.</span><span class="na">getFolder</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="na">model</span><span class="p">,</span> <span class="n">cursor</span><span class="p">));</span>
<span class="p">}</span>
<span class="n">Collections</span><span class="p">.</span><span class="na">sort</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="k">new</span> <span class="n">SQLiteFolderComparator</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="na">model</span><span class="p">));</span>
<span class="k">this</span><span class="p">.</span><span class="na">folders</span><span class="p">.</span><span class="na">setFolders</span><span class="p">(</span><span class="n">result</span><span class="p">);</span>
<span class="n">cursor</span><span class="p">.</span><span class="na">close</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="na">db</span><span class="p">.</span><span class="na">setTransactionSuccessful</span><span class="p">();</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="na">db</span><span class="p">.</span><span class="na">endTransaction</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="na">folders</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="kd">private</span> <span class="kd">class</span> <span class="nc">SQLiteActionEditor</span> <span class="kd">implements</span> <span class="n">Action</span><span class="p">.</span><span class="na">Editor</span> <span class="p">{</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">head</span> <span class="o">=</span> <span class="n">SQLiteAction</span><span class="p">.</span><span class="na">this</span><span class="p">.</span><span class="na">head</span><span class="p">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">body</span> <span class="o">=</span> <span class="n">SQLiteAction</span><span class="p">.</span><span class="na">this</span><span class="p">.</span><span class="na">body</span><span class="p">;</span>
<span class="c1">//...</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">commit</span><span class="p">()</span> <span class="p">{</span>
<span class="n">checkDb</span><span class="p">(</span><span class="n">db</span><span class="p">);</span>
<span class="n">db</span><span class="p">.</span><span class="na">beginTransaction</span><span class="p">();</span>
<span class="k">try</span> <span class="p">{</span>
<span class="n">ActionDao</span><span class="p">.</span><span class="na">updateAction</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">SQLiteAction</span><span class="p">.</span><span class="na">this</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="na">head</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="na">body</span><span class="p">);</span>
<span class="n">db</span><span class="p">.</span><span class="na">setTransactionSuccessful</span><span class="p">();</span>
<span class="n">SQLiteAction</span><span class="p">.</span><span class="na">this</span><span class="p">.</span><span class="na">head</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="na">head</span><span class="p">;</span>
<span class="n">SQLiteAction</span><span class="p">.</span><span class="na">this</span><span class="p">.</span><span class="na">body</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="na">body</span><span class="p">;</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="n">db</span><span class="p">.</span><span class="na">endTransaction</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>Очень хитрыми получились <code>List</code> и <code>Set</code>.
Они же модифицируемые (с немутабельными коллекциями получилось бы, пожалуй, проще, тот же <code>Editor</code> для коллекций добавить).
И добавление или удаление элементов в/из списка
тоже немедленно отражается на данных в БД.</p>
<div class="highlight"><pre><span></span><code><span class="kd">class</span> <span class="nc">SQLiteFolderSet</span> <span class="kd">extends</span> <span class="n">SQLiteModelEntity</span> <span class="kd">implements</span> <span class="n">Set</span><span class="o"><</span><span class="n">Folder</span><span class="o">></span> <span class="p">{</span>
<span class="n">List</span><span class="o"><</span><span class="n">SQLiteFolder</span><span class="o">></span> <span class="n">folders</span><span class="p">;</span>
<span class="c1">//...</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">add</span><span class="p">(</span><span class="n">Folder</span> <span class="n">folder</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">folder</span> <span class="k">instanceof</span> <span class="n">SQLiteFolder</span><span class="p">))</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="n">UnsupportedOperationException</span><span class="p">(</span><span class="s">"cannot add not-SQLite folder"</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">SQLiteFolder</span> <span class="n">sqlFolder</span> <span class="o">=</span> <span class="p">(</span><span class="n">SQLiteFolder</span><span class="p">)</span><span class="n">folder</span><span class="p">;</span>
<span class="n">checkDb</span><span class="p">(</span><span class="n">db</span><span class="p">);</span>
<span class="n">db</span><span class="p">.</span><span class="na">beginTransaction</span><span class="p">();</span>
<span class="k">try</span> <span class="p">{</span>
<span class="n">addFolder</span><span class="p">(</span><span class="n">sqlFolder</span><span class="p">);</span>
<span class="n">updateOrder</span><span class="p">();</span>
<span class="n">db</span><span class="p">.</span><span class="na">setTransactionSuccessful</span><span class="p">();</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="n">db</span><span class="p">.</span><span class="na">endTransaction</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="p">}</span>
</code></pre></div>
<p>В итоге происходит работа с объектами через интерфейсы.
Немного нетривиально изначальное получение объектов.
Но дальше все операции определены и ограничены интерфейсами.
Да, на каждый чих происходит обращение к БД
(и сохранение изменений,
это привязано к жизненному циклу активностей,
то есть пользовательских экранов в Андроиде).
Но таких чихов на каждое действие пользователя — лишь единицы.
В общем-то, как и в вебе.
И в целом производительность не страдает,
как могло бы показаться.</p>
<div class="highlight"><pre><span></span><code><span class="c1">// разные методы разных Activity в иерархии наследования...</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="n">Model</span> <span class="nf">openModel</span><span class="p">(</span><span class="n">Context</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="n">SQLiteModel</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">DATABASE_NAME</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">onResume</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">//...</span>
<span class="n">path</span> <span class="o">=</span> <span class="n">intent</span><span class="p">.</span><span class="na">getStringExtra</span><span class="p">(</span><span class="n">EXTRA_FOLDER_PATH</span><span class="p">);</span>
<span class="n">folder</span> <span class="o">=</span> <span class="n">getModel</span><span class="p">().</span><span class="na">getFolder</span><span class="p">(</span><span class="k">new</span> <span class="n">SimplePath</span><span class="p">(</span><span class="n">path</span><span class="p">));</span>
<span class="c1">//...</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">getActionFromIntent</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">//...</span>
<span class="kt">int</span> <span class="n">position</span> <span class="o">=</span> <span class="n">intent</span><span class="p">.</span><span class="na">getIntExtra</span><span class="p">(</span><span class="n">EXTRA_ACTION_POSITION</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">setAction</span><span class="p">(</span><span class="n">getFolder</span><span class="p">().</span><span class="na">getActions</span><span class="p">().</span><span class="na">get</span><span class="p">(</span><span class="n">position</span><span class="p">));</span>
<span class="c1">//...</span>
<span class="p">}</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">onOkClick</span><span class="p">()</span> <span class="p">{</span>
<span class="n">EditText</span> <span class="n">body</span> <span class="o">=</span> <span class="p">(</span><span class="n">EditText</span><span class="p">)</span><span class="n">findViewById</span><span class="p">(</span><span class="n">R</span><span class="p">.</span><span class="na">id</span><span class="p">.</span><span class="na">action_body</span><span class="p">);</span>
<span class="n">String</span> <span class="n">actionBody</span> <span class="o">=</span> <span class="n">body</span><span class="p">.</span><span class="na">getText</span><span class="p">().</span><span class="na">toString</span><span class="p">();</span>
<span class="n">String</span> <span class="n">actionHead</span> <span class="o">=</span> <span class="n">ActionHelper</span><span class="p">.</span><span class="na">getHeadFromBody</span><span class="p">(</span><span class="n">actionBody</span><span class="p">);</span>
<span class="n">getAction</span><span class="p">().</span><span class="na">edit</span><span class="p">().</span><span class="na">setHead</span><span class="p">(</span><span class="n">actionHead</span><span class="p">).</span><span class="na">setBody</span><span class="p">(</span><span class="n">actionBody</span><span class="p">).</span><span class="na">commit</span><span class="p">();</span>
<span class="n">finish</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div>
<p>В моём случае я специально сделал интерфейсы ядра минималистичными,
а набор операций ограниченным.
И получилось упихнуть все тонкости работы с БД внутрь реализаций объектов.
В более реальных системах
внесение изменений в эти интерфейсы, их методы,
и, соответственно, в реализации
выглядит трудоёмким.</p>
<p>Но мне всё же кажется, что это и есть правильный ORM.
Когда действительно можно работать с объектами как с объектами.
А их реализация уже будет ответственна за то,
чтобы правильно сохранить нужное в БД.
Без протекания абстракций в виде необходимости управлять транзакциями
или учитывать то,
были ли эти объекты получены из БД
или другим путём.</p>Об организации2024-01-06T00:00:00+06:002024-01-06T22:09:49+06:00Денис Нелюбинtag:blog.gelin.ru,2024-01-06:/2024/01/code.html<p>Об организации кода. Code layout.</p>
<p>Попадается тебе проект, которому лет 10.
Открываешь его.
И что ты видишь?
<code>config</code>, <code>dto</code>, <code>integration</code>, <code>model</code>, <code>rest</code>, вездесущий <code>util</code>.
Какие-то части веб приложения?
Ну так мы и до этого знали, что это веб приложение.
Что оно делает-то?</p>
<p>Если копнуть глубже, появляется какое-то понимание:</p>
<ul>
<li><code>dto</code><ul>
<li><code>audit …</code></li></ul></li></ul><p>Об организации кода. Code layout.</p>
<p>Попадается тебе проект, которому лет 10.
Открываешь его.
И что ты видишь?
<code>config</code>, <code>dto</code>, <code>integration</code>, <code>model</code>, <code>rest</code>, вездесущий <code>util</code>.
Какие-то части веб приложения?
Ну так мы и до этого знали, что это веб приложение.
Что оно делает-то?</p>
<p>Если копнуть глубже, появляется какое-то понимание:</p>
<ul>
<li><code>dto</code><ul>
<li><code>audit</code></li>
<li><code>campaign</code></li>
<li><code>fee</code></li>
<li><code>invoice</code></li>
<li>...</li>
</ul>
</li>
<li><code>model</code><ul>
<li><code>audit</code></li>
<li><code>campaign</code></li>
<li><code>invoice</code></li>
<li>...</li>
</ul>
</li>
<li><code>rest</code><ul>
<li><code>audit</code></li>
<li><code>campaign</code></li>
<li><code>invoice</code></li>
<li>...</li>
</ul>
</li>
</ul>
<p>И тут, допустим, прилетает задача.
Отчёты по аудиту стали слишком тяжёлыми.
А нужны они только аудиторам.
Надо бы вынести их на другой сервер,
чтобы не мешали остальным пользователям пользоваться приложением.</p>
<p>Но ведь у нас монолит.
Как это, вынести на другой сервер?</p>
<p>Наверное, стоит выделить наши <code>dto.audit</code>, <code>model.audit</code>, <code>rest.audit</code>
и, наверняка, еще с десяток разных <code>*.audit</code>
в отдельное приложение.
Наверняка окажется, что оно всё зависит от пары-тройки различных <code>util</code>.
И ещё от чего-нибудь.</p>
<p>Если вы хорошо писали код, стараясь минимизировать зависимости
и вообще старались следовать <a href="https://ru.wikipedia.org/wiki/SOLID_(%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)">SOLID</a>,
то, скорее всего, у вас получится.
Но было бы значительно проще,
если бы весь код, относящийся к аудиту,
не был бы размазан ровным слоем по всему проекту.
А был бы в своём пакете, в своей отдельной папочке.</p>
<p>А если у вас монолит действительно сильно монолитный,
то, возможно,
вам придётся просто взять ваш монолит как есть,
и просто запустить ещё одну копию на другом сервере.
Пусть там используется только <code>audit</code> часть.
Грязно? А то!</p>
<p>Откуда берётся эта мода группировать файлы исходного кода
по выполняемым им техническим функциям,
а не по бизнес функциям?</p>
<p>Полагаю,
потому что слишком многие туториалы для начинающих
либо вообще не подымают тему организации исходного кода,
либо предлагают группировку по архитектурным слоям
как единственный возможный вариант.
Сходу нагуглил: <a href="https://dev.to/jazzybruno/spring-boot-project-folder-structure-12oe">1</a>,
<a href="https://medium.com/the-resonant-web/spring-boot-2-0-project-structure-and-best-practices-part-2-7137bdcba7d3">2</a>.
И слишком часто это называют Best Practices.</p>
<p><img alt="by layer layout" src="https://blog.gelin.ru/2024/01/by-layer-layout.png"></p>
<p>Популярные веб фреймворки на других языках,
например, <a href="https://medium.com/django-unleashed/django-project-structure-a-comprehensive-guide-4b2ddbf2b6b8">Django</a>
или <a href="https://laravel.com/docs/10.x/structure">Laravel</a>,
или даже руководства по <a href="https://ru.wikipedia.org/wiki/Model-View-Controller">MVC</a>
для <a href="https://www.calhoun.io/using-mvc-to-structure-go-web-applications/">Go</a>,
тоже напирают на различия в архитектурной роли
различных компонентов.
И предлагают в обязательном порядке группировать их по этому признаку.
И либо игнорируют бизнес роль компонентов.
Либо предлагают группировать по смыслу
лишь как второй вариант размещения файлов исходного кода. </p>
<p>Я призываю группировать по фичам,
бизнес функциям.
Всё равно делить (на микросервисы, ага)
придётся поперёк слоёв,
чтобы в каждом (микро)сервисе остались все слои.
Так давайте же сразу складывать всё так,
чтобы потом удобнее было бы пилить.
А пилить потом всё равно придётся.
Рано или поздно, так или иначе.</p>
<p><img alt="как группировать" src="https://blog.gelin.ru/2024/01/group-by.png"></p>
<p>Делайте вот так:</p>
<ul>
<li><code>audit</code><ul>
<li><code>dto</code></li>
<li><code>model</code></li>
<li><code>rest</code></li>
</ul>
</li>
<li><code>campaign</code><ul>
<li><code>dto</code></li>
<li><code>model</code></li>
<li><code>rest</code></li>
</ul>
</li>
<li><code>fee</code><ul>
<li><code>dto</code></li>
</ul>
</li>
<li><code>invoice</code><ul>
<li><code>dto</code></li>
<li><code>model</code></li>
<li><code>rest</code></li>
</ul>
</li>
<li><code>util</code></li>
<li>...</li>
</ul>
<p>Тогда и разделить будет проще.
В том числе и потому, что вы, возможно,
инстинктивно постараетесь уменьшить зависимости
между разными частями приложения.</p>
<p>В идеале зависимостей между разными фичами вообще быть не должно.
Могут быть зависимости от <code>util</code>.
Ну что ж, на то он и утиль, чтобы предоставлять какие-то мелкие сервисы всем.
Для начала пусть будет отдельным пакетом.
А потом подумайте выделить его в отдельную библиотеку или модуль.
Один приватный модуль с дурацким названием <code>shared</code> всё же лучше,
чем дублирование кода.</p>
<p>В чём-то это противопоставление похоже на размещение софта в файловой системе.</p>
<p>Есть Filesystem Hierarchy Standard (<a href="https://ru.wikipedia.org/wiki/FHS">FHS</a>)
для Unix систем.
Где исполняемые файлы кладутся в <code>/bin</code>, настройки в <code>/etc</code>, а изменяемые файлы в <code>/var</code>
(логи в <code>/var/log</code>).
Соображение тут в том,
что разные типы файлов требуют разного места на диске, разной скорости доступа, разной надёжности хранения.
И удобнее примонтировать разные виды файловых систем поближе к корню,
и совместно использовать их разными приложениями.
Но, ручной контроль того, какая программа куда что положила,
становится почти невозможным.
Контролируют это с помощью различных пакетных менеджеров.
Которые следят, что куда складывается,
и подчищают нужные файлы при удалении софта.</p>
<p>С другой стороны, в Windows принято,
чтобы каждая программа всё тащила с собой.
А в Mac каждая программа — это вообще маленький образ диска.
В этих ОС программы
свои настройки хранят централизованно (реестр Windows),
и логи тоже пишут централизованно.
Не через файлы, а через сервисы операционной системы.</p>
<p>P.S. <a href="https://www.geeksforgeeks.org/spring-boot-code-structure/">Не я один так считаю</a>.
Что исходный код надо организовывать по фичам (by feature),
а не по слоям (by layer).</p>О SAML2023-09-18T00:00:00+06:002023-09-18T21:32:35+06:00Денис Нелюбинtag:blog.gelin.ru,2023-09-18:/2023/09/saml.html<p>Вспомним базу.</p>
<p>Есть <a href="https://ru.wikipedia.org/wiki/%D0%90%D1%83%D1%82%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D1%8F">аутентификация</a>
— проверка того,
что пользователь является действительно тем,
за кого себя выдаёт.
Есть <a href="https://ru.wikipedia.org/wiki/%D0%90%D0%B2%D1%82%D0%BE%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F">авторизация</a>
— проверка того,
что пользователь (про которого мы уже точно знаем, что это он)
действительно может делать то,
что он собирается сделать.</p>
<p>Есть фреймворк авторизации <a href="https://blog.gelin.ru/2017/02/oauth.html">OAuth</a> 2.0.
Фреймворк он потому,
что описывает лишь …</p><p>Вспомним базу.</p>
<p>Есть <a href="https://ru.wikipedia.org/wiki/%D0%90%D1%83%D1%82%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D1%8F">аутентификация</a>
— проверка того,
что пользователь является действительно тем,
за кого себя выдаёт.
Есть <a href="https://ru.wikipedia.org/wiki/%D0%90%D0%B2%D1%82%D0%BE%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F">авторизация</a>
— проверка того,
что пользователь (про которого мы уже точно знаем, что это он)
действительно может делать то,
что он собирается сделать.</p>
<p>Есть фреймворк авторизации <a href="https://blog.gelin.ru/2017/02/oauth.html">OAuth</a> 2.0.
Фреймворк он потому,
что описывает лишь саму концепцию,
не сильно вдаваясь в конкретные детали.
Авторизации потому,
что речь в нём идет о предоставлении прав некоторому пользователю
к ресурсам на некотором сервере.
Самое популярное приложение OAuth 2.0
— это «логин» через социальные сети.
В данном случае клиент (наш сайт) запрашивает у сервера (социальной сети)
доступ на получение ресурса,
которым является идентификатор или емейл пользователя.
Если мы смогли получить емейл,
значит, кто-то там (сервер авторизации) проверил,
что это именно тот пользователь,
и что нам можно показать его емейл.
Значит, так уж и быть,
поверим и мы.</p>
<p>Есть стандарт авторизации <a href="https://en.wikipedia.org/wiki/OpenID_Connect">OpenID connect</a>,
на основе OAuth 2.0,
реализованный, например, в <a href="https://blog.gelin.ru/2019/01/keycloak.html">Keycloak</a>.
Стандарт,
потому что в нём прописаны все детали,
что отсутствуют в OAuth 2.0.
Точные адреса эндпоинтов,
правила передачи параметров,
форматы токенов.</p>
<p>А ещё есть <a href="https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language">SAML</a>
— Security Assertion Markup Language.
Такое странное название,
потому что в этом протоколе осуществляется обмен сообщениями
в формате <a href="https://ru.wikipedia.org/wiki/XML">XML</a>,
который тоже Markup Language.
Протокол существует тоже в нескольких версиях,
актуальная на текущий момент — версия 2.0.
Удивительно, но Майкрософт вроде не приложили руку к SAML,
это изначально детище <a href="https://en.wikipedia.org/wiki/OASIS_(organization)">OASIS</a>.</p>
<p>SAML называют протоколом аутентификации.
Его задача — сообщить заинтересованным сущностям,
что личность этого пользователя установлена.
А эти сущности уже сами решат,
что с этим делать,
и куда пользователя пускать.
Ну и с помощью SAML делают <a href="https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%85%D0%BD%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%8F_%D0%B5%D0%B4%D0%B8%D0%BD%D0%BE%D0%B3%D0%BE_%D0%B2%D1%85%D0%BE%D0%B4%D0%B0">SSO</a>
(Single Sign-On).
Когда паролями и прочими штуками аутентификации занимается какой-то один
(тот самый Single) компонент системы.
А остальные компоненты лишь делегируют эти вещи,
и доверяют этому компоненту.</p>
<p>Суть SAML сводится к обмену сообщениями.
Между Service Provider (SP) —
вашим сайтом,
которому нужно аутентифицировать пользователя,
чтобы предоставить ему какой-то сервис.
И Identity Provider (IdP) —
тем самым центральным компонентом,
который берёт на себя аутентификацию пользователей.</p>
<p><img alt="SAML entities" src="https://blog.gelin.ru/2023/09/saml-entities.png"></p>
<p>На уровне сообщений всё просто.</p>
<p>Service Provider хочет понять,
что за юзер к нему пришёл.
И он отправляет так называемый <code>AuthnRequest</code>
к Identity Provider.</p>
<p>AuthnRequest может выглядеть так:</p>
<div class="highlight"><pre><span></span><code><span class="nt"><samlp:AuthnRequest</span>
<span class="na">xmlns:samlp=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:protocol"</span>
<span class="na">xmlns:saml=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span>
<span class="na">ID=</span><span class="s">"ONELOGIN_d4cb296ba14031e6ca4a3ecc122b0ef076e96985"</span>
<span class="na">Version=</span><span class="s">"2.0"</span>
<span class="na">IssueInstant=</span><span class="s">"2023-09-17T06:22:00Z"</span>
<span class="na">Destination=</span><span class="s">"https://idp.saml.example.net/idp/endpoint/HttpRedirect"</span>
<span class="na">ProtocolBinding=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"</span>
<span class="na">AssertionConsumerServiceURL=</span><span class="s">"https://my.cool.site.example.net/saml/acs"</span><span class="nt">></span>
<span class="nt"><saml:Issuer></span>
https://my.cool.site.example.net/saml/metadata
<span class="nt"></saml:Issuer></span>
<span class="nt"><samlp:NameIDPolicy</span>
<span class="na">Format=</span><span class="s">"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"</span>
<span class="na">AllowCreate=</span><span class="s">"true"</span><span class="nt">/></span>
<span class="nt"><samlp:RequestedAuthnContext</span> <span class="na">Comparison=</span><span class="s">"exact"</span><span class="nt">></span>
<span class="nt"><saml:AuthnContextClassRef></span>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
<span class="nt"></saml:AuthnContextClassRef></span>
<span class="nt"></samlp:RequestedAuthnContext></span>
<span class="nt"></samlp:AuthnRequest></span>
</code></pre></div>
<p><code>xmlns:*</code> — это <a href="https://en.wikipedia.org/wiki/XML_namespace">неймспейсы</a> в XML. Они придают «смысла» последующим тегам.<br>
<code>ID</code> — это уникальный идентификатор данного запроса.<br>
<code>Version</code> — это версия SAML.<br>
<code>IssueInstant</code> — дата-время данного запроса.<br>
<code>Destination</code> — URL, куда этот запрос направляется, некий эндпоинт IdP.<br>
<code>ProtocolBinding</code> — это enum, обозначающий, как «забиндить» ответ. Об этом чуть попозже...<br>
<code>AssertionConsumerServiceURL</code> — URL эндпоинта под названием Assertion Consumer Service (ACS) у нашего Service Provider, адрес, куда направлять ответ.<br>
<code>Issuer</code> — в данном случае идентификатор отправителя запроса. Обычно это URL, по которому можно получить некоторые метаданные,
которые описывают нашего отправителя: URL эндпоинтов и т.п. В виде XML документа, конечно же.<br>
<code>RequestedAuthnContext</code> — в данном случае значение <code>PasswordProtectedTransport</code> означает,
что мы просим Identity Provider спросить у пользователя пароль.</p>
<p>Здесь запрос не подписан.
Но Service Provider может добавить ещё и криптографическую подпись.
Чтобы Identity Provider мог удостовериться,
что запрос пришёл не абы от кого.
Хотя обычно IdP воспринимает запросы только с заранее оговорёнными/настроенными ACS URL.
Грубо говоря,
можно считать,
что Issuer соответствует Client ID из OAuth 2.0,
а ACS URL — Redirect URI.</p>
<p>Identity Provider проверяет,
каким-то способом,
например, запрашивая пароль,
идентичность пользователя.
И после успешной проверки отправляет ответ.</p>
<p>Ответ может выглядеть так:</p>
<div class="highlight"><pre><span></span><code><span class="nt"><saml2p:Response</span>
<span class="na">xmlns:saml2p=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:protocol"</span>
<span class="na">Destination=</span><span class="s">"https://my.cool.site.example.net/saml/acs"</span>
<span class="na">ID=</span><span class="s">"_14c658df9e3c277f21113d5a3c1d40ef1694580083159"</span>
<span class="na">InResponseTo=</span><span class="s">"ONELOGIN_d4cb296ba14031e6ca4a3ecc122b0ef076e96985"</span>
<span class="na">IssueInstant=</span><span class="s">"22023-09-17T06:22:05Z"</span>
<span class="na">Version=</span><span class="s">"2.0"</span>
<span class="na">xmlns:xsd=</span><span class="s">"http://www.w3.org/2001/XMLSchema"</span><span class="nt">></span>
<span class="nt"><saml2:Issuer</span>
<span class="na">xmlns:saml2=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span>
<span class="na">Format=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:nameid-format:entity"</span><span class="nt">></span>
https://idp.saml.example.net
<span class="nt"></saml2:Issuer></span>
<span class="nt"><ds:Signature</span> <span class="na">xmlns:ds=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#"</span><span class="nt">></span>
<span class="nt"><ds:SignedInfo></span>
<span class="nt"><ds:CanonicalizationMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2001/10/xml-exc-c14n#"</span><span class="nt">/></span>
<span class="nt"><ds:SignatureMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#rsa-sha1"</span><span class="nt">/></span>
<span class="nt"><ds:Reference</span> <span class="na">URI=</span><span class="s">"#_14c658df9e3c277f21113d5a3c1d40ef1694580083159"</span><span class="nt">></span>
<span class="nt"><ds:Transforms></span>
<span class="nt"><ds:Transform</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#enveloped-signature"</span><span class="nt">/></span>
<span class="nt"><ds:Transform</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2001/10/xml-exc-c14n#"</span><span class="nt">></span>
<span class="nt"><ec:InclusiveNamespaces</span> <span class="na">xmlns:ec=</span><span class="s">"http://www.w3.org/2001/10/xml-exc-c14n#"</span> <span class="na">PrefixList=</span><span class="s">"xsd"</span><span class="nt">/></span>
<span class="nt"></ds:Transform></span>
<span class="nt"></ds:Transforms></span>
<span class="nt"><ds:DigestMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#sha1"</span><span class="nt">/></span>
<span class="nt"><ds:DigestValue></span>
...
<span class="nt"></ds:DigestValue></span>
<span class="nt"></ds:Reference></span>
<span class="nt"></ds:SignedInfo></span>
<span class="nt"><ds:SignatureValue></span>
...
<span class="nt"></ds:SignatureValue></span>
<span class="nt"><ds:KeyInfo></span>
<span class="nt"><ds:X509Data></span>
<span class="nt"><ds:X509Certificate></span>
...
<span class="nt"></ds:X509Certificate></span>
<span class="nt"></ds:X509Data></span>
<span class="nt"></ds:KeyInfo></span>
<span class="nt"></ds:Signature></span>
<span class="nt"><saml2p:Status></span>
<span class="nt"><saml2p:StatusCode</span> <span class="na">Value=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:status:Success"</span><span class="nt">/></span>
<span class="nt"></saml2p:Status></span>
<span class="nt"><saml2:Assertion</span>
<span class="na">xmlns:saml2=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:assertion"</span>
<span class="na">ID=</span><span class="s">"_e3ac8fc176e96fb1d61c3c225016013f1694580083159"</span>
<span class="na">IssueInstant=</span><span class="s">"22023-09-17T06:22:05Z"</span>
<span class="na">Version=</span><span class="s">"2.0"</span>
<span class="na">xmlns:xsd=</span><span class="s">"http://www.w3.org/2001/XMLSchema"</span><span class="nt">></span>
<span class="nt"><saml2:Issuer</span> <span class="na">Format=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:nameid-format:entity"</span><span class="nt">></span>
https://idp.saml.example.net
<span class="nt"></saml2:Issuer></span>
<span class="nt"><ds:Signature</span> <span class="na">xmlns:ds=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#"</span><span class="nt">></span>
<span class="nt"><ds:SignedInfo></span>
<span class="nt"><ds:CanonicalizationMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2001/10/xml-exc-c14n#"</span><span class="nt">/></span>
<span class="nt"><ds:SignatureMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#rsa-sha1"</span><span class="nt">/></span>
<span class="nt"><ds:Reference</span> <span class="na">URI=</span><span class="s">"#_e3ac8fc176e96fb1d61c3c225016013f1694580083159"</span><span class="nt">></span>
<span class="nt"><ds:Transforms></span>
<span class="nt"><ds:Transform</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#enveloped-signature"</span><span class="nt">/></span>
<span class="nt"><ds:Transform</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2001/10/xml-exc-c14n#"</span><span class="nt">></span>
<span class="nt"><ec:InclusiveNamespaces</span> <span class="na">xmlns:ec=</span><span class="s">"http://www.w3.org/2001/10/xml-exc-c14n#"</span> <span class="na">PrefixList=</span><span class="s">"xsd"</span><span class="nt">/></span>
<span class="nt"></ds:Transform></span>
<span class="nt"></ds:Transforms></span>
<span class="nt"><ds:DigestMethod</span> <span class="na">Algorithm=</span><span class="s">"http://www.w3.org/2000/09/xmldsig#sha1"</span><span class="nt">/></span>
<span class="nt"><ds:DigestValue></span>
...
<span class="nt"></ds:DigestValue></span>
<span class="nt"></ds:Reference></span>
<span class="nt"></ds:SignedInfo></span>
<span class="nt"><ds:SignatureValue></span>
...
<span class="nt"></ds:SignatureValue></span>
<span class="nt"><ds:KeyInfo></span>
<span class="nt"><ds:X509Data></span>
<span class="nt"><ds:X509Certificate></span>
...
<span class="nt"></ds:X509Certificate></span>
<span class="nt"></ds:X509Data></span>
<span class="nt"></ds:KeyInfo></span>
<span class="nt"></ds:Signature></span>
<span class="nt"><saml2:Subject></span>
<span class="nt"><saml2:NameID</span> <span class="na">Format=</span><span class="s">"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"</span><span class="nt">></span>
123456
<span class="nt"></saml2:NameID></span>
<span class="nt"><saml2:SubjectConfirmation</span> <span class="na">Method=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:cm:bearer"</span><span class="nt">></span>
<span class="nt"><saml2:SubjectConfirmationData</span>
<span class="na">InResponseTo=</span><span class="s">"ONELOGIN_d4cb296ba14031e6ca4a3ecc122b0ef076e96985"</span>
<span class="na">NotOnOrAfter=</span><span class="s">"22023-09-17T06:22:10Z"</span>
<span class="na">Recipient=</span><span class="s">"https://my.cool.site.example.net/saml/acs"</span><span class="nt">/></span>
<span class="nt"></saml2:SubjectConfirmation></span>
<span class="nt"></saml2:Subject></span>
<span class="nt"><saml2:Conditions</span> <span class="na">NotBefore=</span><span class="s">"22023-09-17T06:22:05Z"</span> <span class="na">NotOnOrAfter=</span><span class="s">"22023-09-17T06:22:10Z"</span><span class="nt">></span>
<span class="nt"><saml2:AudienceRestriction></span>
<span class="nt"><saml2:Audience></span>
https://my.cool.site.example.net/saml/metadata
<span class="nt"></saml2:Audience></span>
<span class="nt"></saml2:AudienceRestriction></span>
<span class="nt"></saml2:Conditions></span>
<span class="nt"><saml2:AuthnStatement</span> <span class="na">AuthnInstant=</span><span class="s">"22023-09-17T06:22:05Z"</span><span class="nt">></span>
<span class="nt"><saml2:AuthnContext></span>
<span class="nt"><saml2:AuthnContextClassRef></span>
urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified
<span class="nt"></saml2:AuthnContextClassRef></span>
<span class="nt"></saml2:AuthnContext></span>
<span class="nt"></saml2:AuthnStatement></span>
<span class="nt"><saml2:AttributeStatement></span>
<span class="nt"><saml2:Attribute</span> <span class="na">Name=</span><span class="s">"userId"</span> <span class="na">NameFormat=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"</span><span class="nt">></span>
<span class="nt"><saml2:AttributeValue</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:type=</span><span class="s">"xsd:anyType"</span><span class="nt">></span>
0050xxxxxxxx
<span class="nt"></saml2:AttributeValue></span>
<span class="nt"></saml2:Attribute></span>
<span class="nt"><saml2:Attribute</span> <span class="na">Name=</span><span class="s">"username"</span> <span class="na">NameFormat=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"</span><span class="nt">></span>
<span class="nt"><saml2:AttributeValue</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:type=</span><span class="s">"xsd:anyType"</span><span class="nt">></span>
vasil.pupkin@saml.example.net
<span class="nt"></saml2:AttributeValue></span>
<span class="nt"></saml2:Attribute></span>
<span class="nt"><saml2:Attribute</span> <span class="na">Name=</span><span class="s">"email"</span> <span class="na">NameFormat=</span><span class="s">"urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"</span><span class="nt">></span>
<span class="nt"><saml2:AttributeValue</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:type=</span><span class="s">"xsd:anyType"</span><span class="nt">></span>
vasil.pupkin@example.com
<span class="nt"></saml2:AttributeValue></span>
<span class="nt"></saml2:Attribute></span>
<span class="nt"></saml2:AttributeStatement></span>
<span class="nt"></saml2:Assertion></span>
<span class="nt"></saml2p:Response></span>
</code></pre></div>
<p>Выглядит сложно,
как оно обычно и бывает с XML.<br>
<code>Destination</code> — это кому направлен ответ, в данном случае это ACS URL нашего Service Provider.<br>
<code>ID</code> — это ID ответа.<br>
<code>InResponseTo</code> — это ID запроса, на который дан этот ответ.<br>
<code>Issuer</code> — это Entity ID отправителя запроса (и того, кто поставил подпись).<br>
<code>Signature</code> — это цифровая подпись. В формате <a href="https://en.wikipedia.org/wiki/XML_Signature">XML Signature</a>. SAML сообщение, кроме непосредственно подписи, может также включать публичный ключ для проверки этой подписи. Соответственно, проверяющая сторона должна держать список доверенных ключей. Подписывается как всё сообщение в целом, так и отдельно Assertion.<br>
<code>Status</code> — статус ответа. В данном случае <code>StatusCode</code> содержит значение <code>Success</code>. Может быть и ошибка. </p>
<p><code>Assertion</code> — самая важная часть ответа. В принципе, может быть запрошена отдельно, через отдельный специальный эндпоинт. Но здесь возвращается сразу в ответе.<br>
У Assertion есть свой <code>Issuer</code> и своя <code>Signature</code>.<br>
<code>NameID</code> — это, по сути, идентификатор пользователя в IdP.<br>
Атрибут <code>NotOnOrAfter</code> указывает срок действия данного Assertion. В данном случае лишь пять минут.<br>
Присутствуют всякие ограничения, кому именно и на какой срок выдано это Assertion.</p>
<p><code>AttributeStatement</code> — это набор атрибутов пользователя.
Ключ-значение.
Какие атрибуты есть зависит исключительно от IdP.
В SAML 2.0 атрибуты можно получить отдельным запросом.</p>
<p>Эти запросы и ответы — типичный XML.
Куча всяких штуковин и разнообразных атрибутов.
Часто дублирующих друг друга.
Чтобы покрыть все возможные случаи,
даже не встречающиеся в дикой природе.
Просто смиритесь с тем,
что вот так вот оно устроено.</p>
<p>Интереснее другое.
Каким образом Service Provider и Identity Provider
обмениваются этими XML сообщениями?</p>
<p>В первых версиях SAML обмен осуществлялся по протоколу
<a href="https://en.wikipedia.org/wiki/SOAP">SOAP</a>.
Это <a href="https://en.wikipedia.org/wiki/Remote_procedure_call">RPC</a> протокол
с сериализацией данных в XML.
Ещё больше XML.</p>
<p>Но, для нашего Веба придумали и другие способы.
Их в SAML называют Binding.
В Вебе чаще всего используют два: <code>HTTP-Redirect</code> и <code>HTTP-POST</code>.
В обоих случаях обмен данными происходит
через браузер,
SP и IdP не взаимодействуют друг с другом напрямую.</p>
<p><img alt="SAML login sequence diagram" src="https://blog.gelin.ru/2023/09/saml-login.png"></p>
<!---
https://planttext.com/api/plantuml/png/TP9FRy8m3CNl-HGMTzYTmoIs7OOqH4KvxX8bfbYKf7FS8hxzqhH5BN-kyVDxzY_MHZp31_jBG5QLPY53bNO2inov8OEGiM_88iz01yYpgMXjqGd9TQfQVsLAvHdCv--3DooXqWN2XUG8fI_8GKdkfBGHQhHWwwm6RMoBtSknhdNhNol6E0F2gfQZs-5VZK4Uqxxt-so-GqkCHdvToirNcv--8Kx3-gmilXeWxX1Tk5UBXvnSKM9EXTkwdirBsvjfIw9rkXaskYIRucBJO3-Lc2EQ4zJj746qrA4hJxRhoBH47sTEjRTAXt3nRE_YaUbmJURJmW2r7ojKtWLy4fomPjjy3mVFvmc0hWSEXAKTWqtNSF-roM94TnLYLSYxpsXIdv6V8PuaZ3zkT1ttWFjDHeeGJnuxlf5Xm11Yh5ei5keV
-->
<p>Пользователь тыкает на ссылку или кнопку Login.
И браузер переходит на некий эндпоинт нашего Service Provider.
Провайдер формирует <code>AuthnRequest</code>,
сжимает его с помощью <a href="https://en.wikipedia.org/wiki/Deflate">Deflate</a>,
кодирует его с помощью <a href="https://en.wikipedia.org/wiki/Base64">Base64</a>
и получает некоторую не сильно длинную строчку.
Берёт URL эндпоинта Identity Provider,
куда положено направлять наш запрос.
Добавляет к этому URL query parameter <code>SAMLRequest</code>,
где в качестве значения берёт тот самый закодированный AuthnRequest.
И редиректит туда браузер.
То есть возвращает в браузер статус HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302">302 Found</a>
и заголовок <code>Location</code> с нашим новым URL.
Это и есть <code>HTTP-Redirect</code>.</p>
<p>Часто добавляют ещё параметр <code>RelayState</code>.
Он вернётся неизменным от IdP.
Что может пригодиться,
например,
чтобы передать ID текущей сессии
или URL, куда нужно вернуть пользователя после логина.</p>
<p>Identity Provider получает (HTTP GET) запрос из пользовательского браузера.
Где есть параметр <code>SAMLRequest</code>,
в котором закодирован <code>AuthnRequest</code>.
IdP может обнаружить,
например, по кукам,
что этот пользователь/браузер
уже залогинен.
Тогда последующие шаги можно пропустить
и сразу перейти к отправке ответа.
Либо же пользователю нужно показать форму логина,
проверить его пароль.
И убедиться,
что такой пользователь всё ещё активен.</p>
<p>Если с пользователем всё хорошо,
и Identity Provider уверен,
что это тот самый пользователь,
то можно формировать <code>Response</code>.
Провайдер пишет в <code>Assertion</code> то, что известно о пользователе.
Выставляет в несильно далёкое будущее <code>NotOnOrAfter</code>.
Подписывает своим приватным ключом.
Прикладывает свой публичный ключ.
Сжимает всё это с Deflate,
кодирует с Base64.
Получает уже довольно длинную строку.</p>
<p>Отправляет в браузер пользователю HTML форму
со скрытым полем <code>SAMLResponse</code>,
куда помещает закодированный ответ.
Добавляет ещё поле <code>RelayState</code>.
Навешивает немного JavaScript,
чтобы форма сразу же засабмитилась на URL Service Provider
под названием AssertionConsumerService (ACS).
Это и есть <code>HTTP-POST</code>.</p>
<p>Service Provider получает HTTP POST запрос,
где в полях формы есть закодированный <code>SAMLResponse</code>.
Провайдер проверяет формат ответа,
подписи,
и что они сделаны доверенным сертификатом.
Сохраняет Assertion куда-нибудь до лучших времён.
О пользователе известны его ID на IdP,
имя, емейл.
Возможно, и назначенные роли.
Этого достаточно,
чтобы начать работать с этим пользователем.</p>
<p>Ну и с <code>RelayState</code> можно что-то сделать,
как минимум проверить,
что он тот же самый,
что был отправлен.
И редиректнуть пользователя туда.</p>
<p>Все эти передачки отлично видны в девелоперской консоли браузера.
А для раскодирования всех этих Base64 сообщений
есть <a href="https://samltool.io/">samltool.io</a></p>
<p>Вот, вкратце, и весь SAML.
Его часть про логин.
Очень похоже на OAuth.
Так же перенаправляем пользователя на страницы Identity Provider,
где он должен залогиниться.
А обратно получаем какие-то утверждённые сведения о пользователе.
Ну не через редирект (то есть GET запрос),
а через POST.</p>
<p>Более глубинная разница в том,
что в случае OAuth у вас остаётся на руках токен.
С этим токеном можно делать какие-то запросы на API,
получать дополнительные сведения о пользователе,
совершать какие-то действия
там, где этот токен смогут принять и проверить.
В случае SAML у вас остаётся лишь Assertion.
Это полезные сведения о пользователе.
Но больше с ними ничего сделать нельзя.
Вроде как можно послать запрос для получения дополнительных атрибутов,
но про это я не расскажу.
Но нельзя что-либо сделать с какой-нибудь третьей сущностью,
SAML подразумевает взаимодействие только с Identity Provider.</p>
<p>Но зато в SAML есть забавная фишка с логаутом.
Если пользователь вылогинивается из Service Provider,
тот может сообщить об этом в Identity Provider.
А Identity Provider, в свою очередь,
может сообщить об этом всем известным ему Service Provider.
Получается такой глобальный логаут.
Это может быть полезно.</p>
<p>Что-то мне больше нравится OpenID Connect
(реализация OAuth 2.0).
Всё чётко и понятно.
И компактно.
И функционально.
А SAML выглядит слишком жирным
для той кучки возможностей,
что он предоставляет.</p>О провайдерах2023-02-05T00:00:00+06:002023-02-06T07:21:29+06:00Денис Нелюбинtag:blog.gelin.ru,2023-02-05:/2023/02/providers.html<p>Подключился я тут, не в Омске,
к новому для меня провайдеру,
чьё название начинается на «Б» и заканчивается на «йн».
И, кажется, понял,
почему его, не только не в Омске,
ругают.
Подключения к интернетам ведь бывают разные...</p>
<p>Давным-давно,
в начале века,
работал я в компании,
которая подключала домашние сети к …</p><p>Подключился я тут, не в Омске,
к новому для меня провайдеру,
чьё название начинается на «Б» и заканчивается на «йн».
И, кажется, понял,
почему его, не только не в Омске,
ругают.
Подключения к интернетам ведь бывают разные...</p>
<p>Давным-давно,
в начале века,
работал я в компании,
которая подключала домашние сети к Интернету.
И не только домашние сети,
но и общаги универов,
и индивидуальных абонентов.
То есть,
сети по домам тоже протягивали.
Воздушки кидали.
До сих пор помню незабываемые впечатления
от лазания по засранному голубями чердаку.</p>
<p>Да, существовал тогда такой феномен,
как домашние сети.
Это когда энтузиасты кидали кабель к соседям.
Ну, чтобы в игрушки сетевые поиграть.
И мемасиками меняться.
А те кидали кабель ещё к соседям.
И таким образом получалась локальная сеть
в пределах одного или нескольких соседних домов.
Но без Интернета.
Вот мы Интернет в такие сети и проводили.</p>
<p>Выдавали Интернет максимально честным способом.
Мы покупали в Америке бэушные управляемые свичи <a href="https://ru.wikipedia.org/wiki/3Com">3Com</a>.
На <a href="https://ru.wikipedia.org/wiki/EBay">eBay</a> покупали.
Тогда это был гораздо более простой челлендж,
чем сейчас.
Свичи с дюжиной 10-мегабитных портов,
и парой «аплинков» на 100-мегабит.
В 10-мегабитные порты втыкали компьютеры клиентов.
А 100-мегабитные шли к нам,
или к соседнему свичу.
Да, 10-мегабитный интернет тогда было очень круто.</p>
<p>Управляемый свич — это такой свич,
у которого есть свой IP адрес
и несколько интерфейсов управления.
Веб морда, <a href="https://ru.wikipedia.org/wiki/Telnet">telnet</a>
и чудесный <a href="https://ru.wikipedia.org/wiki/SNMP">SNMP</a>.
Как минимум,
можно увидеть, а есть ли вообще линк на порту клиента,
не заглядывая в ящик на чердаке,
где свич стоит.
Как максимум, можно свичом как-то порулить.
Помню, мы в админку добавляли отображение статуса порта свича у клиента,
получая данные с самого свича по SNMP.</p>
<p>Управляемый свич был нужен ради одной важной функции:
автоматической блокировки портов.
Свич, как любой нормальный свич, ведёт у себя таблицу <a href="https://ru.wikipedia.org/wiki/MAC-%D0%B0%D0%B4%D1%80%D0%B5%D1%81">MAC адресов</a>.
На каком порту какой MAC адрес наблюдается.
И, соответственно,
на какой порт отправлять <a href="https://ru.wikipedia.org/wiki/Ethernet">Ethernet</a> фреймы,
отправленные этому MAC адресу.
А в управляемых свичах можно ограничить количество MAC адресов на порту.
И даже сделать так,
чтобы порт отключался,
если на нём появляется слишком много MAC-адресов
или какие-то адреса, не запомненные заранее.</p>
<p>Схема такая.
Пользователь получает свой статический <a href="https://ru.wikipedia.org/wiki/%D0%A7%D0%B0%D1%81%D1%82%D0%BD%D1%8B%D0%B9_IP-%D0%B0%D0%B4%D1%80%D0%B5%D1%81">серый</a> IP адрес.
Ручками прописывает его на своей сетевой карте.
<a href="https://ru.wikipedia.org/wiki/DHCP">DHCP</a> мы не использовали,
так как в домашних сетях было очень легко, даже случайно,
поднять свой DHCP сервер.
А так как до нашего, провайдерского, DHCP сервера
было сильно дальше, чем до соседей,
то нехороший сосед легко мог навыдавать IP адресов через DHCP всем своим соседям.
И лишить их Интернета, понятно.</p>
<p>Имеем статический IP адрес.
И MAC адрес сетевухи пользователя.
MAC адрес прибивается к порту свича через ту самую фишку с блокировкой портов.
А на сервере заводится статическая <a href="https://ru.wikipedia.org/wiki/ARP">ARP</a> таблица,
жёстко задающая соответствие MAC и IP адреса каждого клиента.
Доступ к Интернету и учёт трафика происходит по IP адресу.
Вуаля.</p>
<p>Весь учёт идёт по IP адресу.
Но пользователь не может поменять IP адрес или взять IP адрес другого пользователя,
тогда Интернет перестанет работать,
потому что на маршрутизаторе этот IP адрес связан с другим MAC адресом.
Пользователь также не может взять MAC адрес другого пользователя,
потому что в этом случае его порт на умном свиче будет заблокирован.</p>
<p>С точки зрения пользователя всё просто и удобно.
Обычный статический IP адрес.
Пользователи домашних сетей умели такое настраивать.
И <a href="https://ru.wikipedia.org/wiki/Maximum_transmission_unit">MTU</a> полноценный в 1500 байт.</p>
<p>С точки зрения провайдера всё немного непросто.
Нужно и свичи управляемые, и настроить их.
Нужна статическая ARP таблица.
Зато потом простой учёт и блокировка пользователей просто по IP адресам.
Ну и <a href="https://ru.wikipedia.org/wiki/NAT">NAT</a> нужен, один раз.
Зато, если есть более одного аплинка,
можно выпускать пользователя куда удобнее,
там, где дешевле.</p>
<p>Почти что лучшее подключение к интернетам ever.
Лучше только полновесные <a href="https://blog.gelin.ru/2018/11/ipv6.html">IPv6</a> каждому пользователю.
Но IPv6 тогда, можно сказать, не было.
А белых IPv4 никто юзерам,
конечно же, не раздавал.</p>
<!--
<div class="highlight"><pre><span></span><code><span class="nv">cloud</span> <span class="nv">Internet</span>
<span class="nv">component</span> <span class="nv">Router</span>
<span class="nv">component</span> <span class="nv">Switch</span>
<span class="nv">actor</span> <span class="nv">User</span>
<span class="nv">Internet</span> <span class="o">--</span> <span class="nv">Router</span>
<span class="nv">Router</span> <span class="o">--</span> <span class="nv">Switch</span>
<span class="nv">Switch</span> <span class="o">--</span> <span class="nv">User</span>
<span class="nv">note</span> <span class="nv">right</span> <span class="nv">of</span> <span class="nv">Router</span>
<span class="nv">static</span> <span class="nv">ARP</span>
<span class="nv">NAT</span>
<span class="nv">AAA</span> <span class="nv">by</span> <span class="nv">IP</span>
<span class="k">end</span> <span class="nv">note</span>
<span class="nv">note</span> <span class="nv">right</span> <span class="nv">of</span> <span class="nv">Switch</span>
<span class="nv">static</span> <span class="nv">MAC</span> <span class="nv">on</span> <span class="nv">ports</span>
<span class="nv">auto</span><span class="o">-</span><span class="nv">block</span> <span class="nv">ports</span>
<span class="k">end</span> <span class="nv">note</span>
<span class="nv">note</span> <span class="nv">right</span> <span class="nv">of</span> <span class="nv">User</span>
<span class="nv">static</span> <span class="nv">IP</span>
<span class="k">end</span> <span class="nv">note</span>
</code></pre></div>
-->
<p><img alt="простое подключение к интернетам" src="https://blog.gelin.ru/2023/02/simple.png"></p>
<p>Важно идентифицировать пользователя,
отличить его от других,
и подсчитать именно его трафик.
А также не дать нехорошим пользователям качать трафик
от имени хороших пользователей.</p>
<p>С управляемыми свичами это можно без дополнительных ухищрений.
Но управляемые свичи — дорогое удовольствие.
Они у нас таки кончились.
А когда у тебя на одном управляемом порту висит десяток пользователей,
подключенных через неуправляемые свичи,
ты уже не можешь их достоверно различить.
Поэтому позднее мы водрузили VPN.
Не помню, какой именно.
Кажется, тот,
который работал из коробки в Windows.
К VPN мы ещё вернёмся...</p>
<p>Получается, если вы не можете
воткнуть каждого пользователя в управляемый порт,
вам надо как-то выкручиваться.
Всё ещё любимый мною ЭР-Телеком (ака Дом.ру)
выкручивается с помощью <a href="https://ru.wikipedia.org/wiki/PPPoE">PPPoE</a>.</p>
<p>Немного вернёмся к основам.
<a href="https://ru.wikipedia.org/wiki/PPP_(%D1%81%D0%B5%D1%82%D0%B5%D0%B2%D0%BE%D0%B9_%D0%BF%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB)">PPP</a>, Point-to-Point Protocol — это такой специальный протокол,
который позволяет (в частном случае)
передавать IP пакеты от одного узла к другому
(поэтому Point-to-Point).
Этот протокол отвечает за установления соединения
(существует понятие коннекта),
за аутентификацию (есть механизмы передачи и проверки пароля),
за передачу сетевого трафика (не только IP, но нас интересует IP).
Ещё через PPP эти самые point получают IP адреса
в рамках PPP соединения (отдельный DHCP не нужен).</p>
<p>PPP используется,
когда вы выходите в интернеты по <a href="https://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%B4%D0%B5%D0%BC">модему</a>.
Сначала вы дозваниваетесь,
модемные протоколы устанавливают канал передачи данных,
а последующая аутентификация и передача IP трафика
уже происходит именно по протоколу PPP.</p>
<p><img alt="PPP через модем" src="https://blog.gelin.ru/2023/02/ppp-modem.png"></p>
<p>PPP используется во многих VPNах.
Например,
в <a href="https://ru.wikipedia.org/wiki/OpenVPN">OpenVPN</a> именно PPP запускается после того,
как установлен туннель между клиентом и сервером,
и дальше весь обмен трафиком идёт через PPP.
Именно поэтому в OpenVPN нужно явно подключаться к серверу,
и отключаться.
А вот в других,
более «сетевых» «VPN»,
вроде <a href="https://blog.gelin.ru/2018/05/tinc.html">Tinc</a> или <a href="https://blog.gelin.ru/2020/11/wireguard.html">WireGuard</a>,
PPP не используется,
и нет понятия «установки соединения».</p>
<p><img alt="PPP в VPN" src="https://blog.gelin.ru/2023/02/ppp-vpns.png"></p>
<p>Так вот, PPPoE — это PPP over Ethernet.
Не поверх различных IP туннелей, как в VPN.
А прямо поверх Ethernet.
То есть PPP инкапсулируется в Ethernet фреймы.</p>
<p><img alt="PPPoE" src="https://blog.gelin.ru/2023/02/pppoe.png"></p>
<p>Пользователь ставит у себя маршрутизатор,
втыкает в него Ethernet кабель.
Этот кабель идёт прямо к оборудованию провайдера.
И настраивает PPPoE.
PPPoE сервер провайдера доступен в той же локальной сети
по MAC адресу.
Маршрутизатор пользователя подключается к PPPoE серверу провайдера,
аутентифицируется там,
и начинает гонять IP трафик.
Все довольны.
Но MTU чуток урезается.</p>
<p>Тут есть нюанс безопасности.
Другой пользователь в этой же сети
может тоже поднять у себя PPPoE сервер.
И тут, как и с DHCP,
кто раньше ответит,
того и тапки.
Конечно, раздавать интернеты через свой PPPoE
злобному пользователю довольно бессмысленно.
Но вот стащить пароли он может.
Поэтому нужно запрещать <a href="https://ru.wikipedia.org/wiki/Password_Authentication_Protocol">PAP</a>
(где пароль передаётся открытым текстом)
и использовать <a href="https://ru.wikipedia.org/wiki/CHAP">CHAP</a>
(где передаётся солёный дайджест пароля).</p>
<p>PPP удобен для провайдера тем,
что тут есть сеанс работы пользователя.
Который начинается с подключения и аутентификации.
И заканчивается разрывом соединения
(<a href="https://neolurk.org/wiki/%D0%9D%D0%B8_%D0%B5%D0%B4%D0%B8%D0%BD%D0%BE%D0%B3%D0%BE_%D1%80%D0%B0%D0%B7%D1%80%D1%8B%D0%B2%D0%B0">не было разрывов</a>!)
В течение всего этого сеанса
за пользователем закреплён не только IP адрес,
но и целый сетевой интерфейс
(того самого PPP).
И на этом интерфейсе очень удобно считать трафик
(правда, чаще всего данные снимаются лишь при завершении соединения,
отсюда и разрывы раз в сутки).
Есть тонны учётного софта,
которые заточены именно на учёт PPP сеансов.</p>
<p>PPPoE,
по сравнению с более другими настоящими VPN,
хорош минимальными издержками.
У нас нет ещё одного слоя IP.
И мы не гоняем IP поверх IP.
Всё просто и логично.</p>
<p>Дом.ру прекрасен вот ещё чем.
Если вы захотите
(отключите NAT у провайдера в личном кабинете),
то через PPPoE он будет выдавать белый IP.
То есть на вашем маршрутизаторе будет реальный
доступный из интернетов IP адрес.
Идеально,
если вы хотите держать дома сервер.
Ну, правда, адрес каждый раз будет разным,
при каждом переподключении.
Но это более-менее лечится с помощью <a href="https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_DNS">dynamic DNS</a>.
А за дополнительные деньги вам выдадут и статический адрес.
Ну и IPv6 тоже можно,
правда, с кривоватыми префиксами.</p>
<p>В таком варианте,
когда на ваш домашний маршрутизатор приходит настоящий IP адрес,
пусть и через PPPoE,
необходим лишь один NAT.
Адресов вашей домашней сети в интернеты,
на вашем домашнем маршрутизаторе.
Или вообще без NAT,
если IPv6.
Это очень удобно и хорошо.
<a href="https://ru.wikipedia.org/wiki/UPnP">UPnP</a> и раздача торрентов работают идеально.</p>
<p>Но это всё в Омске.
А в Астане Дом.ру нет.
Тут вообще подозрительно часто встречается ADSL.</p>
<p><a href="https://ru.wikipedia.org/wiki/XDSL">xDSL</a> (x Digital Subscriber Line)
— так часто называют семейство протоколов
для передачи трафика по паре медных проводов.
Тех самых,
что используются в телефонии.
Только в обычных модемах используется частотный диапазон
человеческой речи,
сигналы модема передаются вместо голоса.
В результате максимальная скорость получается лишь 56 кбит/с
(я помню, да).
Зато с точки зрения телефонного оборудования (АТС),
ничего не меняется.
Дозвонились до абонента,
и затем говорите человеческим голосом или шипите модемом,
без разницы.</p>
<p>Но те же самые телефонные провода
вполне можно пробросить из точки А в точку Б.
Например, арендовать пару проводов
в телефонных кабелях,
которые протянуты в колодцах по всему городу.
Это называется «выделенная линия».
И вот на обоих концах этой линии можно подключить
<a href="https://ru.wikipedia.org/wiki/SDSL">SDSL</a> модемы.
Symmetric DSL.
Скорость там получается уже порядка двух мегабит в секунду,
на расстояниях в несколько километров.</p>
<p>Но для подключения обычных частных абонентов
используется <a href="https://ru.wikipedia.org/wiki/ADSL">ADSL</a>.
Asymmetric DSL.
Есть ATC.
От неё к абонентам уже протянут телефонный кабель.
А у абонентов уже стоят телефонные аппараты.
Когда по телефону никто не разговаривает,
эти провода,
по сути,
лежат без дела.
Ну давайте воткнём на АТС специальное сетевое оборудование
(так называемый DSLAM, DSL Access Multiplexer).
А у абонента поставим ADSL модем.
И смотрите,
теперь по этим проводам можно гонять сетевой трафик
на приличной скорости.
И даже одновременно с телефонными разговорами
(частотное разделение сигналов).</p>
<p><img alt="ADSL" src="https://blog.gelin.ru/2023/02/adsl.png"></p>
<p>ADSL — это целое семейство очень хитрых протоколов.
Там есть оптимизация,
чтобы сигналы в соседних парах одного кабеля не мешали друг другу.
Там учитывается то,
что кабели расходятся от АТС к абонентам,
а значит,
наибольшие взаимные помехи наблюдаются
ближе к АТС.
Отсюда проистекает ассиметричность (буква A в названии)
протоколов.
Ассиметричность в скорости.
От АТС к абоненту (download) получается добиться аж 24 Мбит/с.
А вот от абонента к АТС (upload) — лишь 3.5 Мбит/с максимум.
Ну, ADSL делали уже с оглядкой на структуру трафика этого нашего Веба.
Так что тут всё правильно,
что даунлоад быстрее.</p>
<p>Меня всегда удивляло,
что внутри ADSL,
по какой-то неведомой причине,
прижился протокол <a href="https://ru.wikipedia.org/wiki/ATM">ATM</a>.
Когда-то ATM был перспективным крутым протоколом,
который собирался соперничать с Ethernet.
Но Ethernet не сдавался и наращивал скорость.
И ATM как-то тихо помер.
Но остался жить, вот, в ADSL.
Там очень много слоёв протоколов.
Те же фреймы Ethernet могут передаваться поверх ячеек ATM.
И таки где-то сверху снова возникает наш вездесущий PPP.</p>
<p>24 Мбит/с (и это только при правильной фазе Луны)
по нынешним временам
— ну, не очень быстро.
Поэтому, если можно получить интернеты
не по телефонной линии,
нужно получать не по телефонной линии.</p>
<p>Как правило, к дому тянут оптоволоконный кабель.
А дальше по дому либо обычный <a href="https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D1%82%D0%B0%D1%8F_%D0%BF%D0%B0%D1%80%D0%B0">UTP</a>
до каждой квартиры,
либо тоже оптику.
В любом случае в квартире появляется маршрутизатор,
либо с <a href="https://ru.wikipedia.org/wiki/8P8C">RJ45</a>,
либо с каким-то оптическим портом (или внешним конвертером).
Так как я оптику у себя в квартире так и не видел,
мне сложно понять,
какой из вариантов <a href="https://ru.wikipedia.org/wiki/Fiber_to_the_x">FTTx</a> в реальности используют.
Маршрутизатор таки всё равно получает Ethernet.</p>
<p>Возвращаемся к нашим Билайнам.
Тут нет PPPoE.
А как же происходит аутентификация и учёт трафика?
А через <a href="https://ru.wikipedia.org/wiki/L2TP">L2TP</a>.
Нормальный такой VPN.
Не хуже OpenVPN.
Работает поверх <a href="https://ru.wikipedia.org/wiki/IPsec">IPsec</a>.</p>
<p>Что тут плохого?
Во-первых,
полноценный IPsec требует заметных вычислительных ресурсов на маршрутизаторе.
Говорят, когда Билайн только начал подключать домашний интернет,
пользователи бегали, искали
маршрутизаторы помощнее,
чтобы L2TP не тормозил.
Во-вторых, вот как выглядит подключение на маршрутизаторе:</p>
<p><img alt="L2TP на маршрутизаторе" src="https://blog.gelin.ru/2023/02/l2tp.png"></p>
<p>Сначала поверх Ethernet
мы получаем по DHCP некий серый адрес
(<code>10.160.х.х</code> в данном случае).
TP-Link называет это вторичным подключением.
Интернета тут быть не должно.
Но должен работать DNS,
чтобы отрезолвить имя L2TP сервера
(<code>l2tp.internet.beeline.kz</code>).
Причём это должны быть какие-то приватные DNS сервера.
Ведь у этого L2TP сервера должен быть приватный адрес.
Или же нужно проковырять из этой серой сети доступ к L2TP серверу.
Во всяком случае,
в публичных интернетах вы адрес этого сервера не отрезолвите.
Слоожно (с точки зрения организации всего этого).</p>
<p>А теперь,
получив адрес и доступ к L2TP серверу,
мы к нему подключаемся.
Аутентифицируемся логином-паролем,
тут же снова PPP у нас.</p>
<p>И, сюрприз-сюрприз,
через L2TP/PPP мы получаем ещё один серый адрес
(<code>10.215.x.x</code>).
И настроек, чтобы это как-то поменять, я не нашёл.
Точнее, есть опция со статическим IP адресом,
+70% к стоимости тарифа.
Дороговато.</p>
<p>Что значит ещё один серый адрес?
А это значит,
что провайдер будет делать NAT.
Всего получается два NATа.
Один на нашем домашнем маршрутизаторе,
нашу внутреннюю сеть в серый адрес,
выданный провайдером.
Второй на маршрутизаторе провайдера,
серый адрес в настоящий IP.</p>
<p>Чем плох двойной NAT?
Во-первых,
узнать свой реальный IP можно только извне.
Например, через <code>curl ifconfig.me</code>.</p>
<p>Во-вторых,
нужно помнить,
как работает NAT.
Это очень stateful штука.
Он отслеживает все IP/TCP/UDP соединения.
Помнит, какой клиентский IP/порт
каким публичным IP/портом он выставил наружу.
Если какие-то хитрые протоколы NAT не может отследить,
они через NAT работать не будут.
Чтобы помнить все соединения нужна память.
И, если домашний маршрутизатор
уж как-нибудь справится со всеми пятью домашними устройствами.
То маршрутизатору провайдера нужно работать
с тысячами клиентов
и десятками тысяч соединений.</p>
<p><img alt="NAT" src="https://blog.gelin.ru/2023/02/nat.png"></p>
<p>Когда пакеты долго не ходят,
или когда банально маршрутизатору не хватает места
для хранения новых соединений,
он будет удалять записи.
Это приводит к тому,
что старые соединения просто отваливаются.
У меня, например,
стали отваливаться неактивные <a href="https://blog.gelin.ru/2018/10/ssh_14.html">ssh</a> подключения.
Подключился,
потом на что-то отвлёкся на полчаса,
возвращаешься,
а терминал уже ни на что не реагирует,
соединение зависло.
Решение: добавить в <code>~/.ssh/config</code> строку <code>ServerAliveInterval 60</code>.
Чтобы ssh раз в минуту посылал пустой пакет по сети.
Собственно, всякие keep alive штуки
во всяких разных протоколах,
включая даже VPN,
и были добавлены,
чтобы бороться с протухающим NAT.</p>
<p>Кстати,
залипшее ssh подключение можно принудительно закрыть
с помощью комбо: <code>Enter</code>, <code>~</code>, <code>.</code> (нажать последовательно).
Есть и другие <a href="https://lonesysadmin.net/2011/11/08/ssh-escape-sequences-aka-kill-dead-ssh-sessions/">последовательности</a>.</p>
<p>Чем Билайн хуже Дом.ру?
Относительно «тяжёлый» L2TP вместо «простого» PPPoE.
В два раза больше серых IP адресов.
Двойной NAT,
без покупки статического IP я не могу поднять дома сервер.
Отсутствие IPv6.
Мне, как айтишнику,
обидно.</p>
<p>Зато за те же деньги
я получаю ещё и мобильную связь.
А при хорошем и грамотно настроенном маршрутизаторе
(не экономьте на CPU)
разницы при просмотре ютубов,
в общем-то,
и не будет.</p>О Флиппере2022-09-18T00:00:00+06:002022-09-19T03:40:06+06:00Денис Нелюбинtag:blog.gelin.ru,2022-09-18:/2022/09/flipper.html<p>Наконец-то я добрался до своего Flipper Zero.
Его зовут L4b4tle (Лабатл? Лфобфотл?).
Я уже прокачал его до второго лвла.</p>
<p><img alt="знакомство" src="https://blog.gelin.ru/2022/09/intro.jpg"></p>
<p><img alt="лвл 2" src="https://blog.gelin.ru/2022/09/level2.png"></p>
<p><a href="https://en.wikipedia.org/wiki/Flipper_Zero">Flipper Zero</a>
— это широко известный в узких кругах
сверхуспешный кикстартерный долгострой.
В далёком 2020 русские ребята открыли
<a href="https://www.kickstarter.com/projects/flipper-devices/flipper-zero-tamagochi-for-hackers">кампанию на Kickstarter</a>.
Запросили $60k.
Получили $4.8M.
Решили,
что это достаточно много,
чтобы …</p><p>Наконец-то я добрался до своего Flipper Zero.
Его зовут L4b4tle (Лабатл? Лфобфотл?).
Я уже прокачал его до второго лвла.</p>
<p><img alt="знакомство" src="https://blog.gelin.ru/2022/09/intro.jpg"></p>
<p><img alt="лвл 2" src="https://blog.gelin.ru/2022/09/level2.png"></p>
<p><a href="https://en.wikipedia.org/wiki/Flipper_Zero">Flipper Zero</a>
— это широко известный в узких кругах
сверхуспешный кикстартерный долгострой.
В далёком 2020 русские ребята открыли
<a href="https://www.kickstarter.com/projects/flipper-devices/flipper-zero-tamagochi-for-hackers">кампанию на Kickstarter</a>.
Запросили $60k.
Получили $4.8M.
Решили,
что это достаточно много,
чтобы сделать всё по-взрослому.
Поэтому теперь у Flipper Devices Inc. штаб-квартира в США,
а производство налажено в Китае.
И поставляют Флипперы по всему миру.
Многие нелёгкие детали
этого процесса подробно изложены
<a href="https://habr.com/ru/company/flipperdevices/blog/">на Хабре</a>.</p>
<p><a href="https://flipperzero.one/">Flipper Zero</a>
— это хакерский мультитул.
Как плоскогубцы, пилочки, ножи и кусачки.
Только для софтверных хакеров.
Перехват, чтение и воспроизведение
радиосигналов, карточек и ключей доступа.
В одном маленьком карманном устройстве.
Про это уже немного <a href="https://vas3k.ru/notes/flipperzero/">написал Вастрик</a>.
Я чуток дополню.</p>
<p><img alt="мультитул" src="https://blog.gelin.ru/2022/09/multitool.jpg"></p>
<p>Я пропустил кампанию на Кикстартере.
Не потому, что не заметил.
А потому, что не знал, нужен ли мне такой девайс.
А потом решил,
что таки нужен (зачем, об этом чуть позднее).
Но Кикстартер тогда уже закончился.
И я оказался в числе тех,
кто оформил предзаказ уже на официальном сайте.
В августе 2021 заплатил $10 за предзаказ.
В декабре 2021 оплатил уже полную стоимость заказа:
сам Флиппер со скидкой в $50,
чехольчик и доставка.
И вот,
в августе 2022, мы встретились.
Я уже говорил, что это долгострой? :)</p>
<p>В комплект поставки входит сам Flipper Zero,
USB type-A – type-C шнурок,
и небольшая инструкция.
Плюс мой чехольчик
(очень полезен,
если таскать Флиппер в кармане).
Нужно ещё купить правильную <a href="https://docs.flipperzero.one/basics/sd-card">SD карту</a>.
Флиппер хранит на карте почти всё,
что можно хранить на карте.
Базы данных,
сохранённые сигналы и последовательности,
всё.
Поэтому SD карта обязательно нужна.</p>
<p>А потом нужно поставить <a href="https://flipperzero.one/update">официальное приложение</a>,
и обновить прошивку.
Приложение очень симпатичное.
Под Linux поставляется в виде <a href="https://ru.wikipedia.org/wiki/AppImage">AppImage</a>,
скачал и сразу запускаешь.
Кроме обновления прошивки можно делать «скриншоты»
(да и вообще полностью управлять Флиппером)
и скачивать/заливать файлы
во внутреннее хранилище Флиппера
и на SD карту.
Учтите только,
что в качестве кард-ридера Флиппер очень плох,
там очень небыстрый доступ к карте.</p>
<p><img alt="приложение" src="https://blog.gelin.ru/2022/09/desktop-app.jpg"></p>
<p>Есть и <a href="https://play.google.com/store/apps/details?id=com.flipperdevices.app">мобильное приложение</a>.
Подключается к Флипперу через блутуф.
Через него тоже можно обновлять прошивку
(начиная с какой-то недавней версии прошивки,
то, что идёт с завода,
нужно будет в первый раз обновить через USB)
и синхронизировать содержимое SD карты.
А ещё можно делиться записанными ключами.</p>
<p><img alt="мобильное приложение" src="https://blog.gelin.ru/2022/09/mobile-app.jpg"></p>
<p>Так.
Что может Флиппер и зачем он мне нужен?</p>
<p>Пройдёмся по главному меню.</p>
<p><img alt="главное меню" src="https://blog.gelin.ru/2022/09/main-menu.jpg"></p>
<p><a href="https://docs.flipperzero.one/sub-ghz">Sub-GHz</a> — это приложение
(каждый пункт меню во Флиппере запускает приложение)
для чтения, записи и воспроизведения
цифровых радиосигналов
на частотах 433 или 868 мегагерц (в Европе и СНГ).
На 433 мегагерцах работают автомобильные сигнализации,
пульты шлагбаумов,
беспроводные звонки.
Почти всё бытовое радио,
которое не вайфай или блутуф.</p>
<p>Если у вас, допустим,
есть пультик открывания шлагбаума,
то всё просто.
Сначала запускаете на Флиппере Frequency Analyzer.
И жмёте кнопочку пультика.
Так вы узнаете точную частоту.
Потом запускаете чтение сигнала на нужной частоте.
Тут вам нужно будет перебрать четыре возможных варианта модуляции сигнала,
которые поддерживаются чипом <a href="https://www.ti.com/lit/ds/symlink/cc1101.pdf">CC1101</a>,
что стоит во Флиппере.
Если повезёт,
Флиппер расшифрует сигнал
и вы сможете его сохранить.
А потом повторить.
Так у вас появится дубликат ключа во Флиппере.</p>
<p>У меня пультика нет.
Зато есть шлагбаум,
который я очень хочу научиться открывать.
Въезд на парковку с задней стороны офиса.
Не то,
чтобы я собираюсь там парковаться каждый день,
но на случай экстренной необходимости очень даже пригодится.
Поймать сигнал из окон офиса в 100 метрах от шлагбаума не получается.
Нужно организовывать патрулирование ближе :)</p>
<p>Пытался поймать сигнал своей автосигнализации.
Безуспешно.
Флиппер не обучен его пониманию.
Там, похоже,
даже частота разных посылов немного разная.
Это хорошо.
Зато ловил сигналы чьих-то других сигнализаций.
Флиппер не даёт их сохранять,
потому что там используются <a href="https://en.wikipedia.org/wiki/Rolling_code">rolling code</a>.
Рисует замочки и не даёт сохранять.
Потому что повторять эти сигналы бессмысленно,
не сработают.</p>
<p>Тем не менее
зарядные лючки Теслы Флиппер открывать умеет.
Для этого нужно <a href="https://github.com/UberGuidoZ/Flipper/tree/main/Sub-GHz/Vehicles/Tesla">скачать</a>
уже заранее записанную последовательность
и положить её на SD карту.
Осталось только найти Теслу :)</p>
<p><a href="https://docs.flipperzero.one/rfid">125 kHz RFID</a> —
это приложение для чтения и эмуляции
так называемых низкочастотных (LF) RFID меток.
Чаще всего это такие белые карточки-пропуска
или кругленькие ключи от домофонов в пластиковом корпусе,
которые прикладываются.
Таких меток вокруг оказалось очень много.
Ключ от офиса,
ключи от домофонов,
карточка-пропуск в фитнесс.
Так что теперь у меня есть бэкапы всех этих карточек.
Один раз это даже пригодилось,
когда два настоящих ключа от переговорки
оказались заперты в самой переговорке.
Учтите,
что копии ключей от домофона будут просто работать,
а вот ключ от фитнесса будет работать
только если у владельца ключа есть деньги на счету :)
Ещё Флиппер умеет записывать RFID карты,
если найдёте «болванку»,
на которую можно записать.</p>
<p><img alt="RFID" src="https://blog.gelin.ru/2022/09/rfid.jpg"></p>
<p><a href="https://docs.flipperzero.one/nfc">NFC</a> —
это тот самый NFC, который нынче имеется в телефонах.
Чем он отличается от RFID,
<a href="https://habr.com/ru/company/flipperdevices/blog/571838/">описано в блоге</a>.
Возможности Флиппера
мало чем отличается от продвинутого NFC приложения,
установленного в телефоне.
Можно читать, сохранять и воспроизводить
простые NFC карты,
в которых хранится лишь их идентификатор.
Можно читать публичную информацию (номер карты, срок действия)
с банковских карт.
Ещё Флиппер может выдавать техническую информацию
об NFC ридере при эмуляции карты.
Кому-то это может быть полезно.</p>
<p><a href="https://docs.flipperzero.one/infrared">Infrared</a> —
ИК пультик.
В современных телефонах ИК порт встречается крайне редко.
Вот вам теперь Флиппер,
чтобы не забывать,
как это удобно,
иметь под рукой программируемый ИК передатчик.</p>
<p>В комплекте идёт «универсальный» ИК пульт для телевизоров.
Кнопка включения/выключения,
кнопка мути,
регулировка громкости и переключение каналов.
А в файлике на SD карте записаны по 100500 кодов
на каждую кнопочку.
Их так много,
что при передаче их всех отображается индикатор прогресса.
И можно остановить передачу,
если код уже сработал.
Однако,
для попавшихся на пути умных телевизоров,
срабатывало только включение,
и больше ничего.
Но это не страшно,
можно записать нужные коды и ручками добавить в файлик.</p>
<p><img alt="TВ пультик" src="https://blog.gelin.ru/2022/09/tv-remote.png"></p>
<p>ИК пультики во Флиппере отображаются «на боку»,
потому что при их использовании удобнее держать Флиппер вертикально.</p>
<p>Можно создавать свои пультики,
записывая сигналы от отдельных кнопок.
Правда, интерфейс будет уже не такой красивый,
много кнопок перебирать будет неудобно.
Но работать будет.</p>
<p><img alt="TB Philips" src="https://blog.gelin.ru/2022/09/philips-tv.png"></p>
<p>С кондиционерами
(и, вероятно, со всеми другими пультами с дисплеем,
например, у меня у робота-пылесоса такой)
всё сложнее.
Об этом тоже было <a href="https://habr.com/ru/company/flipperdevices/blog/566148/">написано в блоге</a>.
Пульт от кондиционера передаёт не код нажатой кнопки.
Он передаёт всё состояние пульта:
режим работы, температура, влючен ли swing и всё такое.
Всё одним большим пакетом при нажатии любой кнопки.
Флиппер может лишь записать весь этот пакет целиком.
Но это же и хорошо.
Наконец-то во Флиппере у меня есть правильная кнопка
«Включить охлаждение до 25 градусов с вентилятором на автомате
и включенным swing».
Одна.
Кнопка.
Правда, на работе оказалось аж три разновидности
несовместимых друг с другом кондиционеров.
И, наверное,
показания часов на пульте тоже передаются в этом ИК пакете,
а значит,
включение кондиционера с Флиппера сбрасывает на нём часы.
Если это важно.</p>
<p><img alt="правильный пульт кондиционера" src="https://blog.gelin.ru/2022/09/ac-remote.png"></p>
<p><a href="https://docs.flipperzero.one/gpio-and-modules">GPIO</a> —
General Purpose Input/Output.
Некоторые ножки микроконтроллера Флиппера
любезно выведены наружу.
К Флипперу можно подключать хардварные модули.
Как минимум есть <a href="https://docs.flipperzero.one/development/hardware/wifi-debugger-module">Wi-Fi карточка</a>,
официально предназначенная только для удалённого дебага.
В меню можно подёргать пины
и включить/отключить питание на выводах.</p>
<p><img alt="GPIO меню" src="https://blog.gelin.ru/2022/09/gpio-menu.png"></p>
<p><a href="https://docs.flipperzero.one/ibutton">iButton</a> —
это чтение и эмуляция тех самых ключей-таблеток для домофонов.
Всё очень подробно, опять-таки, <a href="https://habr.com/ru/company/flipperdevices/blog/561792/">описано в блоге</a>.
Некоторые виды ключей можно и писать на болванку
(примерно это и делают, когда вы копируете ключ от домофона
в мастерской изготовления ключей).
Если вы знаете ID мастер-ключа,
можно и его сэмулировать,
ID можно ввести вручную.
У меня во Флиппере теперь хранятся бэкапы
всех ключей от домофонов,
которые у меня дома завалялись.
Некоторые я даже не помню,
от какой квартиры :)</p>
<p><a href="https://docs.flipperzero.one/bad-usb">Bad USB</a>.
Флиппер умеет представляться любым USB устройством.
И слать любые USB последовательности.
Чтобы сделать что-нибудь плохое.
Или хорошее.
Вот в этом меню можно выбрать на выполнение какой-нибудь из заранее заготовленных сценариев.
В штатной поставке есть два сценария с USB клавиатурой для атаки на MacOS или Windows.
Если посмотреть на <a href="https://github.com/UberGuidoZ/Flipper/tree/main/BadUSB">другие сценарии</a>,
то видно,
что всё, в основном,
сводится к запуску чего-нибудь в терминале.
Хотя вот подбор пин-кода на Android выглядит более полезным :)
Ну почему бы и нет,
если можно подключить USB клавиатуру и есть длинный сценарий,
который тупо и механично вводит
все доступные четырёхциферные комбинации...</p>
<p><a href="https://ru.wikipedia.org/wiki/%D0%A3%D0%BD%D0%B8%D0%B2%D0%B5%D1%80%D1%81%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D0%B4%D0%B2%D1%83%D1%85%D1%84%D0%B0%D0%BA%D1%82%D0%BE%D1%80%D0%BD%D0%B0%D1%8F_%D0%B0%D1%83%D1%82%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D1%8F">U2F</a>
— это Universal 2nd Factor.
Вообще не знал про эту штуку,
пока не заполучил Флиппер.
Это такой стандарт для второго фактора аутентификации,
где второй фактор представлен физическим устройством.
Это устройство чаще всего называют Security Key (или Token).
Это USB брелок с кнопочкой.
Он хранит в себе приватный ключ
и осуществляет обработку challenge запросов
от приложения,
которое запрашивает аутентификацию.
А кнопочка нужна,
чтобы в момент аутентификации убедиться,
что тут рядом находится живой человек,
вероятно,
владелец брелка.
Вот Флиппер и работает таким брелком.
<a href="https://github.com/flipperdevices/flipperzero-firmware/issues/1565">Пока</a> только через USB,
хотя в стандарте предусмотрена работа через Блутуф и NFC,
чтобы и в мобильных приложениях тоже можно было бы так авторизоваться.
И кнопочку на Флиппере тоже нужно нажимать.
Я проверил,
в Гугле, на ГитХабе, в Cloudflare и Twitter
такой способ двухфакторной аутентификации есть и работает,
как минимум в Chrome.</p>
<p>Plugins.
В этот пункт меню попали приложения,
которые по каким-то причинам не удостоились отдельных пунктов меню.
Возможно,
потому что недостаточно хакерские.
Можно,
кстати,
<a href="https://habr.com/ru/post/594895/">написать и своё приложение</a>.
Однако <a href="https://github.com/flipperdevices/flipperzero-firmware/issues/73">пока</a> Флиппер не умеет выполнять файлы с флэшки.
Поэтому для запуска своего приложения
придётся собрать свою прошивку целиком,
с вашим новым приложением в комплекте.</p>
<p>Bluetooth Remote.
Флиппер работает как блутуф клавиатура или мышь.
Да, через блутуф он тоже может представляться разными устройствами.
Клавиатура, конечно,
условной степени удобности.
Но вот её вариант с клавишами управления воспроизведением
могут помочь в переключении треков в телефоне.
А вариант Keynote может быть полезным
для презентаций.</p>
<p><img alt="клавиатура для презентаций" src="https://blog.gelin.ru/2022/09/bt-keynote.png"></p>
<p><img alt="блутуф клавиатура" src="https://blog.gelin.ru/2022/09/bt-keyboard.png"></p>
<p><img alt="клавиатура для плеера" src="https://blog.gelin.ru/2022/09/bt-media.png"></p>
<p><img alt="блутуф мышка" src="https://blog.gelin.ru/2022/09/bt-mouse.png"></p>
<p>Music Player.
Во Флиппере есть пищалка.
Довольно приличная пищалка.
Она даже может играть монофоническую мелодию.
И можно <a href="https://github.com/neverfa11ing/FlipperMusicRTTTL">качать рингтоны</a>.
Чтобы пищало,
как телефоны двадцать лет назад.</p>
<p><img alt="плеер" src="https://blog.gelin.ru/2022/09/music-player.png"></p>
<p>Snake Game.
Смотрите,
экранчик с янтарной подсветкой,
почти как на моём <a href="https://ru.wikipedia.org/wiki/Siemens_ME45">Siemens ME45</a>
2001 года выпуска.
Ну ладно, в Сименсе экран был 101x80 пикселей,
а во Флиппере — 128x64.
Рингтоны есть.
Конечно же тут должна быть «Змейка».
Чтобы олскулы окончательно свело.
И она есть!</p>
<p><img alt="Змейка" src="https://blog.gelin.ru/2022/09/snake.png"></p>
<p>Итого.
У меня во Флиппере забэкаплены все ключи от офисов,
от домофонов.
Забиты команды включения сразу в правильный режим
для всех кондиционеров дома и в офисе.
И, на всякий случай,
самые важные кнопки пультов телевизора и роботопылесоса.
Я использую Флиппер как U2F токен.
Всё ещё не теряю надежды поймать сигнал
открывания шлагбаума возле офиса.
И вообще, есть ещё парочка шлагбаумов,
которые меня раздражают.</p>
<p>Под Флиппер можно пописать софт.
<a href="https://github.com/DroomOne/flipperzero-firmware/tree/dev/applications%2Fflappy_bird">Флаппи Бёрд</a>
и <a href="https://github.com/jeffplang/flipperzero-firmware/tree/tetris_game/applications/tetris_game">Тетрис</a>
уже портировали.
А оффлайнового динозавра — пока нет.
Надо заняться.
Вообще, большой список всяких аддонов — <a href="https://github.com/djsime1/awesome-flipperzero">здесь</a>.</p>
<p>Штатная прошивка Флиппера,
пожалуй,
недостаточно хакерская.
Её достаточно для тех случаев,
когда нужные ключи и пульты
у вас уже есть,
и вам нужна лишь ещё одна цифровая копия.
Скопировать тот же ключ от шлагбаума можно за пару минут.
Но вот для настоящего взлома,
когда у вас нет ключа,
она не годится.
И это неспроста.
Создатели ведь озаботились сертификацией,
чтобы легально поставлять во все страны.
Поэтому по дефолту всё легально.
Но есть альтернативные прошивки...</p>
<p>Кстати, то, что написано на чехольчике,
на коробочке,
и что иногда выкрикивает сам Флиппер —
<code>フリッパー</code>
это действительно слово «Флиппер»,
написанное японской <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%82%D0%B0%D0%BA%D0%B0%D0%BD%D0%B0">катаканой</a>.
Ну и Флиппер — это герой-дельфин
из фильмов и <a href="https://ru.wikipedia.org/wiki/%D0%A4%D0%BB%D0%B8%D0%BF%D0%BF%D0%B5%D1%80_(%D1%82%D0%B5%D0%BB%D0%B5%D1%81%D0%B5%D1%80%D0%B8%D0%B0%D0%BB,_1964)">сериалов</a> из шестидесятых.
Умный дельфин.</p>
<p><img alt="フリッパー" src="https://blog.gelin.ru/2022/09/flipper.png"></p>
<p>Флиппер-устройство — это ведь ещё и немного
<a href="https://ru.wikipedia.org/wiki/%D0%A2%D0%B0%D0%BC%D0%B0%D0%B3%D0%BE%D1%87%D0%B8">тамагочи</a>.
Те самые виртуальные зверушки в яйцах-брелках,
которых надо было кормить.
Флиппера надо кормить новыми ключами,
и использованием ключей.
На первом лвле он читал книжки «Программирование на Си для чайников».
Теперь,
на втором лвле,
пишет код и что-то паяет,
и просит его не беспокоить.
Это помимо обычного сна, просмотра телевизора
и прочих активностей.</p>
<p>Обзаведётесь Флиппером,
обязательно почитайте
<a href="https://docs.flipperzero.one/basics/control">начальные разделы документации</a>.
Там описаны неочевидные способы навигации.
Оказывается,
во Флиппере есть встроенный браузер по сохранённым файлам.
И ссылки на самые востребованные файлы
можно поместить в Favorites меню,
доступное сразу по клавише «вниз».
Таким образом,
«воспроизвести» нужные ключи
возможно действительно быстро.</p>
<p><img alt="навигация" src="https://blog.gelin.ru/2022/09/navigation.jpg"></p>Об Актау2022-09-11T00:00:00+06:002022-09-11T19:52:34+06:00Денис Нелюбинtag:blog.gelin.ru,2022-09-11:/2022/09/aktau.html<p>Девочки сказали: «Хотим на море!»
Какое море?
Ближайшее!
Какое ближайшее?
Каспийское!
Чтобы в Европу (часть света) даже не соваться.
Чтобы было тепло,
чтобы пожарить косточки в конце августа.
Рядом Казахстан,
значит, в Казахстане.
Вроде в Актау есть море и какие-то пляжи.
Полетели в Актау.
Через Нур-Султан.
Час самолётом из Омска …</p><p>Девочки сказали: «Хотим на море!»
Какое море?
Ближайшее!
Какое ближайшее?
Каспийское!
Чтобы в Европу (часть света) даже не соваться.
Чтобы было тепло,
чтобы пожарить косточки в конце августа.
Рядом Казахстан,
значит, в Казахстане.
Вроде в Актау есть море и какие-то пляжи.
Полетели в Актау.
Через Нур-Султан.
Час самолётом из Омска в Нур-Султан.
Два с половиной часа из Нур-Султана в Актау.
По деньгам за билеты получилось почти столько же,
сколько было слетать в Европу (Евросоюз)
три года назад.</p>
<p><img alt="в августе" src="https://blog.gelin.ru/2022/09/august-map.png"></p>
<p><a href="https://ru.wikipedia.org/wiki/%D0%90%D0%BA%D1%82%D0%B0%D1%83">Актау</a>.
До 1991 года назывался Шевченко.
Ух, что пишет Википедия.
Город был построен в начале 60-х в безводной пустыне
(да, тут море и пустыня)
для добычи урана.
Да, да, ядерное оружие и ядерная энергетика.
А нефть в нём начали добывать лишь после распада СССР.
То есть да,
маленький промышленный городок.
Был.
А ещё там нет пресной воды,
город живёт на опреснённой морской воде.</p>
<p>Это действительно пустыня.
Не песчаная, но пустыня.
С самолёта открывается вид без единого деревца.
Чётко виден рельеф местности.
Каменистая равнина самых разных оттенков рыжего, красного и серого.
Перед самой посадкой становится видно,
что что-то на этих камнях всё же растёт.
Маленькие одинокие кустики <a href="https://ru.wikipedia.org/wiki/%D0%A1%D0%B0%D0%BA%D1%81%D0%B0%D1%83%D0%BB">саксаула</a>.</p>
<p>Аэропорт маленький, чистенький.
В 20 километрах от, собственно, города.
Так что пришлось познакомиться с местным такси.
Такси как такси.
Через Яндекс.Go вызывается и приезжает.
Довольно дёшево.
Уровни «Комфорт» и выше просто отсутствуют,
есть только «Эконом».
Самая популярная машина у местных таксистов: <a href="https://ru.wikipedia.org/wiki/LADA_Granta">Lada Granta</a>.
Почти все машины на газе.
Многие машины — откровенные развалюхи.
Один раз развалюхой оказался даже шестисотый мерседес нежно-голубого цвета,
чуть ли не старше меня.
Но для «Эконом» это нормально.
Таксисты гоняют, как и везде,
что, учитывая местные штрафы,
выглядит странно.
Впрочем, дорожную полицию за три с половиной дня я видел лишь один раз.
Видимо, камер контроля скорости нет.</p>
<p>Был маленький промышленный городок.
Последние несколько лет,
похоже, в регион вливают хорошие деньги.
Все дороги ровные, с новенькими бордюрами.
Очень много новостроек,
разных форматов,
и девятиэтажки,
и частные коттеджи.
Свеженькие торговые центры,
и в центре города.
Правда, там ничего толкового не продают.
Аквапарк, парочка парков с аттракционами,
на вид тоже недавно построенные.
Только они почему-то не работают.
Парки и аттракционы в них.</p>
<p>Строек много.
И всё строят, похоже, из местного камня.
Несущие конструкции многоэтажек отливают из бетона.
А вот все стены,
где в Сибири кладут кирпич или шлакоблок,
тут кладут камень.
Характерного песчаного цвета.
Одинаковыми брусками,
которые выглядят выпиленными из скалы.
Видели на одной стройке вблизи.</p>
<p><img alt="каменный кирпич" src="https://blog.gelin.ru/2022/09/block.jpg"></p>
<p>Архитектура,
дома в центре,
что сохранились с советских времён,
тут интересные.
Как, обычно, строят, допустим,
общежития?
Лестницы и коридоры спрятаны где-то в толще здания.
А тут всё наоборот.
Лестницы-то тоже в здании,
но с одной стороны прикрыты от внешней среды
лишь узорными решётками.
С северной стороны.
И с этой же стороны вдоль всей длины домов
на каждом этаже проходит галерея.
Как длинный балкон,
открытая на улицу.
На эту галерею выходят лестницы.
Это — «подъезд».
Снаружи.
И на эту галерею уже выходят двери квартир.
Практически,
сразу на улицу.
Крайние квартиры эти галереи перегораживают
и сооружают уже полноценные лоджии.
Ну а что,
через них же никто не проходит.
На этих галереях,
бывает,
сушится бельё.</p>
<p><img alt="архитектура" src="https://blog.gelin.ru/2022/09/building.jpg"></p>
<p>На крыше одного из этих домов стоит маяк.
Просто пристройка на ещё этажа три на крыше,
небольшая башенка.
Из нашей квартиры вечерами было его хорошо видно.
Как он работает.
С моря,
полагаю,
тоже хорошо видно,
маяк же.
Очень яркий белый луч.
А за ним менее яркий красный.
Не знаю, что это значит.</p>
<p><img alt="маяк" src="https://blog.gelin.ru/2022/09/lighthouse.jpg"></p>
<p>Любопытные тут адреса.
Хоть есть и улицы
(заглавный проспект конечно же
носит имя Нурсултана Назарбаева),
дома нумеруются по микрорайонам.
Один микрорайон — это большущий квартал,
ограниченный крупными дорогами.
Вот внутри этого квартала дома и пронумерованы
почти хаотично.
А сами микрорайоны имеют только номера.
Так что вполне нормально в районе с новостройками
дому иметь адрес: 35-й микрорайон, дом 35.</p>
<p>А в Приморском районе — наоборот.
Нормальные адреса.
И чудесные названия улиц: Жемчужная,
Весенняя, Урожайная, Душистая, Майская,
Каспийская, Пляжная, Прохладная,
Целинная, Лазурная,
Дачная, Тенистая,
Полевая, Прибрежная, Степная.</p>
<p><img alt="улицы" src="https://blog.gelin.ru/2022/09/streets.png"></p>
<p>Что-то странное тут с людьми на улицах.
Их там просто нет.
Почти.
Мы, конечно, были в будни.
Но и выходной застали,
ничего не изменилось.
Климат тут что ли такой?
В разгар дня никто нос на улицу не высовывает?
Вечером люди появляются.</p>
<p>Люди интересные.
Само собой, много казахов.
Русские похожи на ирландцев,
загорелые до красноты,
волосы выцветшие.
На пляже народ может прийти в одиночку,
бросить сумку и одежду
и спокойно пойти плескаться в море.
Никому не приходит в голову как-то следить за вещами.
Не воруют, видимо.
И дети.
Детей много.
Они сами себе играют и носятся целыми днями.
Без присмотра взрослых.
Прям как в моём советском детстве.
И никто не беспокоится,
если ребёнок самостоятельно полез в море.</p>
<p>Потому что море тут у берега очень мелкое.
На пляжах в Приморском районе
мы честно пытались добраться туда,
где можно нормально поплавать.
Но нет,
и в трёхстах метрах от берега
в лучшем случае будет по пояс.
А дальше вода уже становится холодной.
А у берега — очень тепло.</p>
<p>Море и берег тут больше всего похожи
на венецианский <a href="https://blog.gelin.ru/2019/06/venezia.html">Лидо</a>.
Красивый жёлтый песок сюда,
кажется,
регулярно привозят.
Но его сдувает на край пляжа.
И оголяется местный грунт,
который в сухом виде больше всего похож
на серую пыль.
Но на нём вполне комфортно лежать.
На коврике.
Но это всё же грунт, а не песок.
Поэтому парни,
предлагающие пляжные зонтики,
ходят с сапёрной лопаткой.</p>
<p>На пляже все дни вечерами проходили какие-то корпоративные игры.
Любопытно,
что громогласный ведущий
совершенно свободно
вставлял в свои безумные реплики
слова на казахском,
русском и английском.
Вот три языка совершенно на равных.</p>
<p>Кроме лежания на пляже и загорания на солнышке
мы ещё немного поразвлекались.
Побегали по банкам,
чтобы разменять валюту.
В этот раз мы поступили умнее,
и посмотрели в интернетах,
где курс обмена выгоднее.
Приходим,
а нам говорят: «Тенге будут после двух, приходите через час».
Мы: «Так уже 14:05».
Нам: «Нет, сейчас 13:05, приходите через час».
Так мы узнали,
что в Казахстане два часовых пояса:
на западе: UTC+5,
в центре и на востоке: UTC+6.</p>
<p>Можно покататься по морю на лодках.
Или, скорее,
катерах.
Как договоритесь с местными.
Тут всё решается по-телефону.
Мы напросились на маленький красно-белый милый катерок.
Видимо, когда-то он был заточен для дайвинга.
Сзади была лесенка для спуска в воду.
А в днище даже были окна.
Тем не менее
это оказался настоящий <a href="https://ru.wikipedia.org/wiki/%D0%93%D0%BB%D0%B8%D1%81%D1%81%D0%B5%D1%80">глиссер</a>.
Часовую прогулку <s>по Иртышу</s> вдоль всего городского побережья
нам устроил сын брата хозяина лодки.
Пацан, похоже,
всё лето тут подрабатывал.
И ему позволили рано утром самостоятельно покатать туристов.
Ну он и оторвался.
Рев двигателя.
Пенящиеся волны за кормой.
Ветер в лицо.
Прыг-прыг по волнам.
Страшно.
Круто.</p>
<p><img alt="вид с катера" src="https://blog.gelin.ru/2022/09/from-boat.jpg"></p>
<p>Самая главная туристическая достопримечательность города
— скальная тропа.
Берег-то действительно каменистый,
и центр города стоит на скале метрах в двадцати над водой.
Ну действительно «ак тау» — «белая гора».
И вот у подножья этой скалы,
у кромки моря,
где волны и камни,
проложили тропу.
Точнее, лестницы, пандусы и эстакады.
Деревянные.
Чем-то похоже на велодорожку вокруг озера <a href="https://blog.gelin.ru/2022/07/almaty.html">Борового</a>.
Но гораздо более симпатично.
И только для пешего передвижения.
У основания скалы,
оказывается,
есть куча родников.
В некоторых местах тропы их даже можно увидеть и пощупать.
И там растёт камыш.</p>
<p><img alt="тропа" src="https://blog.gelin.ru/2022/09/pathway.jpg"></p>
<p>Умаявшись ходить по жаре вдоль кромки моря,
мы таки поднялись в город.
От чего умаялись ещё больше.
И там наткнулись на прекрасную кофейню
<a href="https://go.2gis.com/rlw25t">Coffee Corner</a>.
Прохлада кондиционера
и вкуснючие лимонады.
Рекомендую.</p>
<p><img alt="кофе" src="https://blog.gelin.ru/2022/09/coffee.jpg"></p>
<p>Отдыхать в Актау можно и нужно.
На море.
На пляже.
Сильно приятнее, чем, допустим, в <a href="https://blog.gelin.ru/2018/09/blog-post_2.html">Сочи</a>.
Пляжи громадные, и не каменистые.
Море тёплое и мелкое.
Сервис ненавязчивый.
Потому что почти отсутствует :)
Вдоль пляжей есть кафешки,
но еда там нам не понравилась.
Туристов немного,
но у меня сложилось впечатление,
что в ближайшие годы ситуация поменяется.
Возможно, тут сделают более полноценные курорты.
Первые большие отели уже имеются.</p>
<p>Уже на обратном пути,
уже на заходе на посадку в Нур-Султан,
мужик на соседнем сиденье что-то разговорился.
Он оказался уроженцем Актау
и нехило так порекламировал местные достопримечательности
(на которых мы так и не побывали).
В маяк,
оказывается,
можно было сходить на экскурсию,
посмотреть, как оно там всё устроено.
В пятидесяти километрах на север от города
есть крутые песчаные пляжи
<a href="https://go.2gis.com/h38hci">Голубая бухта</a>.
В пятидесяти километрах на юг,
на <a href="https://go.2gis.com/pk9qp">мысе Песчаном</a>,
есть и другие пляжи,
где голыми руками можно ловить раков.
А ещё где-то в окрестностях есть
<a href="https://go.2gis.com/07qtjl">радоновые источники</a>
(помните про добычу урана?),
и <a href="https://go.2gis.com/qxsni">форт Шевченко</a>,
и <a href="https://go.2gis.com/jt5ffq">подземная мечеть Бекет-Ата</a>.
Можно вернуться.</p>
<p><a href="https://photos.app.goo.gl/VWSij2tLsDW5Wb1X7">Фоточки</a></p>О ThinkPad2022-08-21T00:00:00+06:002022-09-10T19:03:51+06:00Денис Нелюбинtag:blog.gelin.ru,2022-08-21:/2022/08/thinkpad.html<p>Я изменил.
Asus Zenbookу изменил.
Раньше я покупал только <a href="https://blog.gelin.ru/2017/08/blog-post.html">Зенбуки</a>.
Очень их любил.
А тут купил Lenovo ThinkPad.
Теперь люблю ФинкПады :)</p>
<p>Как началась фигня,
я вдруг вспомнил,
что давненько уже хотел новый ноутбук.
На предыдущем Зенбуке Intel Core <a href="https://ark.intel.com/ru/products/95443/Intel-Core-i5-7200U-Processor-3M-Cache-up-to-3_10-GHz">i5-7200U</a> Kaby Lake
уже как-то перестало хватать.
Ох уж этот новый интернет …</p><p>Я изменил.
Asus Zenbookу изменил.
Раньше я покупал только <a href="https://blog.gelin.ru/2017/08/blog-post.html">Зенбуки</a>.
Очень их любил.
А тут купил Lenovo ThinkPad.
Теперь люблю ФинкПады :)</p>
<p>Как началась фигня,
я вдруг вспомнил,
что давненько уже хотел новый ноутбук.
На предыдущем Зенбуке Intel Core <a href="https://ark.intel.com/ru/products/95443/Intel-Core-i5-7200U-Processor-3M-Cache-up-to-3_10-GHz">i5-7200U</a> Kaby Lake
уже как-то перестало хватать.
Ох уж этот новый интернет.
Когда в одной вкладке запущен Google Meet,
он сжирает столько CPU,
что гуглодокумент в соседней вкладке
открывается реально пару минут.
Неудобно.
Раздражает.
Надо больше CPU.</p>
<p><img alt="мой финкпад" src="https://blog.gelin.ru/2022/08/thinkpad.jpg"></p>
<p>А тут как раз Intel выпускает новое,
<a href="https://ark.intel.com/content/www/ru/ru/ark/products/series/217839/12th-generation-intel-core-i9-processors.html#@Mobile">двенадцатое поколение</a> своих процессоров.
Вдруг получится найти ноутбук с ним.
Правда,
позднее оказалось,
что не самое лучшее поколение получилось...</p>
<p>А ещё я привык к HiDPI экрану.
И обнаружил, что в этих моих любимых габаритах
тринадцатидюймовых ультрабуков
стали вставлять четырнадцатидюймовые экраны.
Просто рамки стали тоньше.
Так что хотелось четырнадцатидюймого экрана
с разрешением минимум 3200x1800.
Желательно OLED.</p>
<p>Ну и памяти, <a href="https://blog.gelin.ru/2020/08/memory.html">памяти</a> побольше.
В предыдущий Зенбук удалось впихнуть 24 гигабайта.
Вроде нормально с таким объемом.
А вот 16 — маловато,
чтобы одновременно вкладки хрома, проекты в IDEA и пачку Докеров запускать.
Так что 32 или хотя бы 24 гигабайта нужно обязательно.</p>
<p>Да, и ещё хотелось с полнофункциональным USB Type-C.
Чтобы можно было к <a href="https://blog.gelin.ru/2020/04/monitor.html">монитору</a> по USB подключать,
и чтобы ноутбук при этом заряжался.</p>
<p>Сначала присмотрелся к <a href="https://www.asus.com/ru/Laptops/For-Home/Zenbook/Zenbook-14X-OLED-UX5400-11th-Gen-Intel/">Zenbook 14X</a>.
Есть и на 11-м поколении Intel,
и на 12-м (тогда только в виде анонсов).
Есть и с OLED, и с IPS.
И FullHD, и 4K.
Но вот зараза,
в продаже были только модели с 16 гигами памяти.
А мне нужно больше.
И, как я быстро выяснил,
добавить память нельзя,
нет слотов.
Ужас.</p>
<p>Начал глядеть на Lenovo <a href="https://www.lenovo.com/ru/ru/laptops/yoga/yoga-slim-series/Yoga-Slim-7i-Pro-Gen-7-14%E2%80%B3-Intel/p/LEN101Y0015">Yoga Slim</a>.
Красивые.
Классные.
Но, чорт побери,
опять 16 гигабайт памяти распаяны,
и слотов воткнуть памяти побольше нет.
Они что, сговорились?</p>
<p>Пошукал по критерию наличия 32 гигабайт памяти и процессора Intel
в местных интернет магазинах.
Нашёл только громадные шестнадцатидюймовые дорогущие ThinkPad, Dell и Asus.</p>
<p>У тут Дима (спасибо!) кидает ссылку на очередной ThinkPad.
В Яндекс.Маркете.
И с AMD <a href="https://ru.wikipedia.org/wiki/Ryzen">Ryzen</a>.
Оказывается,
в мобильных процессорах в последние годы рулит AMD.
Ryzen лучше Core по производительности, тепловыделению и энергопотреблению.
Не знал.
Теперь знаю.</p>
<p>В общем, купил <a href="https://www.lenovo.com/ru/ru/laptops/thinkpad/p-series/P14s-AMD-G2/p/22WSP144SA2">Lenovo ThinkPad P14s (2nd Gen, AMD)</a>.
А на следующий день Lenovo объявило,
что уходит из России.
Правда, кажется, так и не ушли.
Я неделю боялся,
доедет ли мой оплаченный ноутбук из Москвы или нет.
Доехал.</p>
<p>Итак.
Имеем на борту <a href="https://www.amd.com/en/products/apu/amd-ryzen-7-pro-5850u">AMD Ryzen™ 7 PRO 5850U</a>.
Восемь ядер, шестнадцать потоков.
На 2 гигагерцах в штатном режиме.
При простое до 400 мегагерц скидывают.
Разгоняться может до 4.5 гигагерц.</p>
<p>Машина — зверь.
Я такой красивый htop раньше видел только на серверах.
Шестнадцать потоков,
и все чего-то делают.
Всякие Гугломиты и Гуглодокументы вообще перестали быть проблемой.
Можно перекодировать видосы в h264 со скоростью больше единицы
(с сетевого диска),
то бишь, сразу же можно смотреть результат перекодирования.
И ноутбук при этом не пытается взлететь или прожечь стол.
Красота.
С процом я угадал.</p>
<p><img alt="htop" src="https://blog.gelin.ru/2022/08/htop.png"></p>
<p>Памяти взял 32 гигабайта.
16 гигабайт распаяно, 16 стоит в слоте.
Можно будет воткнуть и побольше.
Но пока не надо.</p>
<p>Экран взял 4K UHD (3840x2160).
Я бы сказал,
многовато DPI для 14 дюймов.
Это ж где-то 314 DPI получается.
Имхо, достаточно 165 DPI для полноценного HiDPI и суперкрасивеньких шрифтов.
Это как бы FullHD получается в 14 дюймах.
Переборщил?</p>
<p>Что там ещё?</p>
<p>Видюха — встроенная в проц.
Судя по тестам и обзорам — тоже лучшая из мобильных встроенных.
Мне хватит.
В игрушки ещё не играл.</p>
<p>Диск — <a href="https://ru.wikipedia.org/wiki/NVM_Express">NVMe</a> накопитель от <a href="https://ru.wikipedia.org/wiki/Hynix">SK hynix</a>
на терабайт.
Капец какой быстрый,
после обычных SSD.
Линукс супер быстро поставился.</p>
<p>Для NVMe диска в биосе можно задать пароль.
Даже несколько разных паролей.
Я всё никак не мог найти ответ на вопрос:
если я задам пароль,
будет ли это означать,
что содержимое диска зашифровано
(а мне это надо).
В документации упоминалось
<a href="https://en.wikipedia.org/wiki/Opal_Storage_Specification">TCG Opal</a>,
что установка пароля в биосе не работает,
если активирована TCG Opal management software program.
Но было совершенно непонятно,
поддерживает ли мой конкретный диск этот Opal.</p>
<p>Наконец в той же самой документации я нашёл раздел про то,
что случится, если вы забудете пароль.
Оказывается,
в этом случае никто не в состоянии будет восстановить содержимое диска,
поддержка Lenovo может только поставить новый диск.
Ну, то, что надо.
На деле пароль запрашивается только при включении
или «жёстком» ребуте.
Обычный ребут пароля не требует.
Так что не забывайте настраивать блокировку экрана.</p>
<p><img alt="nvme password" src="https://blog.gelin.ru/2022/08/nvme-password.png"></p>
<p>USB Type-C по левому борту.
Как я хотел,
чтобы от монитора заряжался.
Заряжается.
И даже штатная зарядка втыкается в этот порт.
Телефоны от этой зарядки тоже можно заряжать :)</p>
<p>Это у меня получается идеальная конфигурация.
Один USB Type-C шнурочек идёт к монитору.
По этому шнурочку ноутбук заряжается.
И передаёт картинку в монитор.
И передаёт звук в монитор.
И передаёт звук в USB наушники, которые воткнуты в монитор.
Класс.</p>
<p>Ещё слева:
ещё один Type-C вместе с разъёмом для докстанции
(не выяснял, для какой),
USB Type-A,
полноразмерный HDMI,
разъём для наушников,
дырочка для MicroSD карт.</p>
<p><img alt="слева" src="https://blog.gelin.ru/2022/08/left.jpg"></p>
<p>Справа:
Ethernet,
полноразмерый Ethernet порт,
(да, толщины ноутбука для него хватает,
это же «квадратный» и «брутальный» ThinkPad).
Он даже пригодился,
чтобы быстренько скопировать данные со старого ноутбука,
пару сотен гигабайт
(не пытайтесь сделать это через вайфай).
Выхлоп системы охлаждения,
справа.
И ещё один USB Type-A.
И дырка для чтения смарт-карт.
Не знаю,
зачем мне это,
но факт: в этот ноубук можно засовывать карты с чипом,
например, банковские.</p>
<p><img alt="справа" src="https://blog.gelin.ru/2022/08/right.jpg"></p>
<p>А сзади ещё есть дырочка для SIM карты.
Тут есть GSM модем.
Не пробовал пока.</p>
<p>Чувствуете, да?
Машинка явно для работы.
Это — ThinkPad.</p>
<p>ThinkPad — не такой красивый алюминиевый, как Zenbook.
Зато корпус и не нагревается.
Нормальный такой soft touch пластик.
Немного марается,
но очень приятный на ощупь.
Очень брутально он выглядит только на фотографиях.
На деле же он такой же маленький и миленький,
как и Зенбук.
Ну, чуть потолще
(зато полноразмерный Ethernet влез).</p>
<p>Это ThinkPad.
У него есть «сосочек» <a href="https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BD%D0%B7%D0%BE%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D0%B4%D0%B6%D0%BE%D0%B9%D1%81%D1%82%D0%B8%D0%BA">TrackPoint</a>.
Красненькая пипка посередине клавиатуры.
Ещё один способ управлять курсором мыши,
помимо тачпада.
Странный, непривычный.
Тачпадом или мышой как-то быстрее получается точно прицеливаться,
переходить от быстрого перемещения к медленному.
Зато этим трекпоинтом весьма удобно прокручивать
(если зажать среднюю клавишу)
сразу в двух направлениях.</p>
<p><img alt="сосочек" src="https://blog.gelin.ru/2022/08/track-point.jpg"></p>
<p>Это ThinkPad.
Это обычный ноутбук,
не трансформер.
Но экран открывается на 180 градусов.
Да, его можно раскрыть в одну плоскость.
Понятия не имею, зачем мне это надо.
Зато радует возможность раскрыть его на любой угол,
не боясь сломать.
Хотя видел подобные ноутбуки
на наклонных стойках для докладчиков,
там эта фича с экраном в одной плоскости с клавиатурой
была уместна.</p>
<p><img alt="крышка открыта" src="https://blog.gelin.ru/2022/08/lid-opened.jpg"></p>
<p>Это ThinkPad.
Тут не нужно разбирать весь ноутбук,
чтобы заменить клавиатуру.
Она откручивается и снимается сверху.
И это описано в официальной документации.
А снизу есть «дренажное отверстие клавиатуры».
Куда будет вытекать кофе (лучше без сахара),
которое вы прольёте на клавиатуру
(не проверял).</p>
<p><img alt="снятие клавиатуры" src="https://blog.gelin.ru/2022/08/keyboard.png"></p>
<p>Есть сканер отпечатков пальца.
Он под Linux работает.
Но, в KDE логин
(точнее, только разблокировка,
потому что при логине нужен пароль,
чтобы всякие <a href="https://wiki.archlinux.org/title/KDE_Wallet">KWallet</a> активировать)
по отпечатку уж очень сильно глючит.
Отключил.</p>
<p>А вот кнопочки <code>Fn</code> и <code>Ctrl</code> тут перепутаны.
Кажется, их можно «переставить» через BIOS.
Но я уже так привык.
А так клавиатура ничем не хуже зенбуковой.
Даже чуток поприятнее и помягче.
Есть полноценные <code>Home</code>, <code>End</code>, <code>Insert</code>, <code>Delete</code>.
(Каждый раз пускаю слюнки на обсуждения или обзоры
всяческих механических кастомных клавиатур
с хитрыми щёлкающими свичами,
но потом вспоминаю,
что у меня везде ноутбуки,
и мои варианты их использования не подразумевают
подключения внешней клавиатуры.)</p>
<p>Ryzen оказался довольно жарким процессором.
Или тут система охлаждения так устроена.
Дует он тёпленьким на правую руку,
что лежит справа на мышке.
Ощутимо тёпленьким.
Обогрев руки такой.
Зато корпус никогда не нагревается.
Вот чем нравилась мне система охлаждения прежнего Зенбука.
Никаких дырок снизу.
Дырки только сзади, в щели между корпусом и экраном,
на забор и на выхлоп воздуха.
Поэтому, если и дует,
это не чувствуется.
У ФинкПада дырок снизу навалом,
полкорпуса сеточкой забрана.
И дует только направо.
Зато,
видимо,
может нормально охлаждаться
с закрытой крышкой.</p>
<p><img alt="днище" src="https://blog.gelin.ru/2022/08/bottom.jpg"></p>
<p>Время жизни от батареи тоже не гигантское.
Чтобы прожить заявленные 12 часов,
видимо,
нужно его включить,
но ничего не запускать.
И яркость экрана выкрутить на минимум.
Тогда,
может,
и проживёт.
А так,
за полтора часа постоянно включенного экрана,
использования только браузера
(без Google Meet) и просмотра PDF
выжирает примерно 20% батареи.
Ну, покодить немного в кафе хватит.
Возможно,
дело в 4К экране,
говорят, он много жрёт.</p>
<p>Чтобы продлить время жизни батарейки,
я ведь большую часть времени заряжаюсь от монитора,
поставил заряжать батарею только до 95%,
а не до 100%.
Есть такая опция в KDE.
Кажется, это тоже фишка ФинкПадов.</p>
<p>С энергопотреблением и жаркостью процессора можно поиграть.
Не знаю,
что там есть в Windows.
А в Linux есть <a href="https://wiki.archlinux.org/title/TLP">TLP</a>,
демон для мониторинга и оптимизации энергопотребления.
А для Ryzen ещё есть <a href="https://github.com/FlyGoat/RyzenAdj">RyzenAdj</a>
и гуй к нему: <a href="https://ryzencontroller.com/">Ryzen Controller</a>.
Эта штука позволяет ограничить энергопотребление и тепловыделение процессора.
Правда,
для этого вам придётся отключить
<a href="https://ru.wikipedia.org/wiki/Secure_Boot">Secure Boot</a>,
Linux почему-то не даёт нужный доступ к железу
под Secure Boot.</p>
<p>Например,
ограничить энергопотребление процессора 8 ваттами в среднем
и 16 ваттами в пике,
а температуру 60 градусами,
можно так:</p>
<div class="highlight"><pre><span></span><code><span class="gp">$ </span>sudo ryzenadj --stapm-limit<span class="o">=</span><span class="m">8000</span> --fast-limit<span class="o">=</span><span class="m">16000</span> --slow-limit<span class="o">=</span><span class="m">8000</span> --tctl-temp<span class="o">=</span><span class="m">60</span>
</code></pre></div>
<p>Я не замечал особенно,
чтобы ryzenadj на что-то существенно влиял.
Эффективнее искать вкладки Хрома,
которые жрут лишнее CPU.
Ну, разве что,
если ограничить температуру ещё сильнее,
то мышка начинает спотыкаться.</p>
<p>Пока я (неделю) писал эту статью,
мой теперь уже любимый ThinkPad что-то взглюкнул.
Сначала не захотел засыпать.
Потом после ребута стал жёстко перезагружаться.
А потом вообще перестал включаться.
Но потом таки включился и работает стабильно.
Может быть, RyzenAdj требует более аккуратного обращения.
Может, какой-нибудь <a href="https://wiki.archlinux.org/title/Ryzen#Troubleshooting">баг</a>
в работе Linux с Ryzen,
вылез.</p>
<p>Оказалось,
что в биосе Lenovo затаилась нехилая такая утилита диагностики.
За пять часов тестов CPU, памяти и прочего
она нашла только,
что рапортуемая ёмкость батареи больше заводской ёмкости.
Других ошибок нет.
Буду надеяться,
что всё хорошо.
Пока остерегусь пользоваться RyzenAdj.</p>
<p><img alt="встроенная диагностика" src="https://blog.gelin.ru/2022/08/diagnostics.jpg"></p>
<p>Ах, да,
точка над <code>i</code> в надписи ThinkPad на крышке ноутбука
является индикатором питания :)</p>
<p><img alt="лампочка" src="https://blog.gelin.ru/2022/08/power-led.jpg"></p>Об Алматы2022-07-03T00:00:00+06:002022-07-11T23:29:13+06:00Денис Нелюбинtag:blog.gelin.ru,2022-07-03:/2022/07/almaty.html<p>Случился у нас семейный отпуск.
Кажется,
самый дикий отпуск за последнее время.
Решили мы доехать на <a href="https://blog.gelin.ru/2020/07/cars.html">машинке</a>
до <a href="https://ru.wikipedia.org/wiki/%D0%9D%D1%83%D1%80-%D0%A1%D1%83%D0%BB%D1%82%D0%B0%D0%BD">Нур-Султана</a>,
а потом ещё сгонять в <a href="https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%BC%D0%B0-%D0%90%D1%82%D0%B0">Алматы</a> на недельку.
Ну а что,
дедушка же в советские времена
добирался из Омска в Целиноград
на своей «копейке».
Чем мы хуже?
А в Алматы (или …</p><p>Случился у нас семейный отпуск.
Кажется,
самый дикий отпуск за последнее время.
Решили мы доехать на <a href="https://blog.gelin.ru/2020/07/cars.html">машинке</a>
до <a href="https://ru.wikipedia.org/wiki/%D0%9D%D1%83%D1%80-%D0%A1%D1%83%D0%BB%D1%82%D0%B0%D0%BD">Нур-Султана</a>,
а потом ещё сгонять в <a href="https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%BC%D0%B0-%D0%90%D1%82%D0%B0">Алматы</a> на недельку.
Ну а что,
дедушка же в советские времена
добирался из Омска в Целиноград
на своей «копейке».
Чем мы хуже?
А в Алматы (или всё-таки Алма-Ате?)
мы просто никогда ещё не были,
но давно хотели.
А тут целая кучка родственников и знакомых образовалась,
поспособствовать и за компанию.</p>
<p>Так как так далеко на машине мы ехали впервые,
решили не торопиться,
и задержаться на денёк в Боровом.
Который нынче называется <a href="https://ru.wikipedia.org/wiki/%D0%91%D1%83%D1%80%D0%B0%D0%B1%D0%B0%D0%B9_(%D0%BF%D0%BE%D1%81%D1%91%D0%BB%D0%BE%D0%BA)">Бурабай</a>.
Озеро,
кажется, всё ещё <a href="https://ru.wikipedia.org/wiki/%D0%91%D0%BE%D1%80%D0%BE%D0%B2%D0%BE%D0%B5_(%D0%BE%D0%B7%D0%B5%D1%80%D0%BE)">Боровое</a>,
а посёлок на его берегу — уже Бурабай.</p>
<p>Знающие люди посоветовали
не ехать на юг,
через <a href="https://ru.wikipedia.org/wiki/%D0%9E%D0%B4%D0%B5%D1%81%D1%81%D0%BA%D0%BE%D0%B5">Одесское</a>.
Там, говорят,
дорога разбитая.
А поехать на запад,
через <a href="https://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%82%D1%80%D0%BE%D0%BF%D0%B0%D0%B2%D0%BB%D0%BE%D0%B2%D1%81%D0%BA">Петропавловск</a>.
В принципе,
не сильно большой крюк.
Можно ещё уехать через <a href="https://ru.wikipedia.org/wiki/%D0%A7%D0%B5%D1%80%D0%BB%D0%B0%D0%BA_(%D0%9E%D0%BC%D1%81%D0%BA%D0%B0%D1%8F_%D0%BE%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C)">Черлак</a>
в <a href="https://ru.wikipedia.org/wiki/%D0%9F%D0%B0%D0%B2%D0%BB%D0%BE%D0%B4%D0%B0%D1%80">Павлодар</a>.
Но это уже совсем мимо Борового получается.</p>
<p>Маршрут получился таким.
Омск — Петропавловск: 280+ километров,
4-5 часов времени.
Ибо на таможне минимум час потеряете.
Петропавловск — <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BA%D1%88%D0%B5%D1%82%D0%B0%D1%83">Кокшетау</a> (Кокчетав): 180+ километров.
Кокчетав — <a href="https://ru.wikipedia.org/wiki/%D0%A9%D1%83%D1%87%D0%B8%D0%BD%D1%81%D0%BA">Щучинск</a>: 70 километров.
Щучинск — Боровое: 20+ километров.</p>
<p><img alt="карта" src="https://blog.gelin.ru/2022/07/map.png"></p>
<p>Поздно утром мы выехали из Омска.
Поздно вечером мы приехали в Боровое.
По пути купили страховку на машину для езды в Казахстане,
за рубли,
домик стоит прямо перед российской таможней.</p>
<p>Наземная таможня оказалась гораздо гуманнее авиационной.
Паспорта смотрят, штампики ставят.
Спрашивают, куда и зачем едете.
Деньги и валюту не пересчитывают.
Машину досматривают,
просят открыть все двери, бардачки, багажник, капот.
Смотрят под сиденьями.
Но в чемоданы
(а в багажник Пиканто влезает ровно один большой чемодан)
не заглядывают.
Старайтесь ехать в будни,
чтобы не пересечься с какими-нибудь автобусами.
А то можете потерять ещё больше времени.</p>
<p>Дорога от Омска до Петропавловска нормальная.
Обычная однополосная дорога.
В России можно ехать 90,
в Казахстане, кажется, можно 100.
На территории России дорога немного ровнее.
Каждые 20-30 километров встречаются места для отдыха.
В Казахстане ещё и оборудованные туалетом,
иногда даже не конструкции «дырка в полу».</p>
<p>В Казахстане на обочине дороги встречаются «картонные» силуэты
лошадок и коров,
видимо,
чтобы водители не расслаблялись.
Потому что настоящие коровки, лошадки и овечки
тоже часто встречаются.
Стадами.</p>
<p>Попадаются целые вымершие берёзовые рощи.
Стоят голые без листвы.
Говорят, их пожрали какие-то гусеницы.
Выглядит страшновато.
Потом, в горах Алматы, мы видели
аналогичные остовы елей,
затопленных озёрами.</p>
<p>Петропавловск — маленький городишко.
Но там есть вся нужная цивилизация.
Мы там обменяли рубли на тенге.
Будьте внимательнее с курсом,
он нынче скачет.
В обменниках курс может быть выгоднее,
чем в банках,
но там могут согласиться менять только большие суммы.</p>
<p>Дорога до Кокчетава такая же,
однополосная.
Вот только за 50 километров до Кокчетава они устроили ремонт.
Реально содрали верхний слой асфальта на 50 километрах дороги,
и начали класть новый,
несколько километров положили.
Ехать по этим процарапанным канавкам как патефонная игла
быстрее 80 мне не захотелось.
Но ничо, доехали.</p>
<p>Кокчетав запомнился отсутствием публичных туалетов
в так называемых «торговых центрах».
И внезапно перекрытым проездом через мост.
Реально, стояла местная патрульная полиция
(так называются казахские гаишники)
и разворачивала всех желающих проехать.
Пришлось немного повоевать с навигатором
и объехать Кокчетав где-то в стороне,
мимо аэропорта.</p>
<p>Дорога до Щучинска оказалась прекрасной.
Уже по две полосы в каждом направлении,
с отбойником или газончиком посередине.
А самое классное,
что дорога ведёт в горы.
Те самые невысокие <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BA%D1%88%D0%B5%D1%82%D0%B0%D1%83_(%D0%B2%D0%BE%D0%B7%D0%B2%D1%8B%D1%88%D0%B5%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D1%8C)">горы</a>,
где расположено озеро Боровое.
И под вечер эти горы
просто прекрасно освещались заходящим солнцем.</p>
<p>Щучинск — совсем маленький городок.
Мы его проехали без остановок.</p>
<p>А из Щучинска в Боровое ведёт
прекрасная узенькая дорожка через горы,
сквозь сосновый лес.
С подъёмами, спусками, поворотами.
И сосны.
И закатное солнце.
Очень красиво.</p>
<p>Бурабай — малюсенький курортный посёлок.
Тут всё подчинено туризму.
Все квартиры сдаются.
Куча гостиниц.
Велосипеды, обычные и водные, в аренду.
Парк с аттракционами, террариумом и колесом обозрения.
Рынок.
Типа торговый центр.
С колеса обозрения хорошо видно,
что главной достопримечательностью посёлка,
помимо озёр,
является заброшенная недостроенная девятиэтажка
на возвышенности в самом центре.
Половина дорог — без асфальта,
щебёнкой засыпаны.
Будьте аккуратны в построении маршрутов,
иногда лучше сделать крюк в несколько километров
и подъехать к цели с другой стороны,
зато по асфальту.</p>
<p>Озеро Боровое,
конечно же,
прекрасно.
Пляж, можно купаться.
Вода чистая, в июне ещё несколько прохладная.
В ветреную погоду есть ну почти морской прибой.
Отличный вид.</p>
<p>Вокруг всего озера идёт пешеходная и велодорожка.
Мы взяли велосипеды на пару часов и успели обогнуть пол озера,
и полазать немного по скалам.
Будьте готовы к крутым подъёмам и спускам.
И к пересечению автодорог.</p>
<p>На северо-западе озера находятся те самые скалы,
которые изображены на всех открытках.
Туда можно доехать на машине,
заплатив на шлагбауме мзду за проезд в заповедную зону.
Или бесплатно пешком или на велосипеде.
Там можно взять водный велосипед и подплыть к скале «Сфинксу».
Место популярное,
поэтому вся скала расписана граффити,
и усыпана водными велосипедистами в ярких спасательных жилетах.
Вид с середины озера едва ли не красивее,
чем с пляжа.</p>
<p><img alt="популярные скалы" src="https://blog.gelin.ru/2022/07/burabay.jpg"></p>
<p>В принципе,
понятно,
почему Боровое рекламируют как
всеказахстанский курорт.
После этих бесконечных степей
эти внезапные горы и чистые озёра — хорошо.
Рекомендую.</p>
<p>После пары ночёвок и дня поджаривания на пляже Борового
мы двинулись дальше в Нур-Султан.</p>
<p>От Щучинска до Нур-Султана имеется платная автомагистраль.
Аж по три полосы в каждую сторону.
Оплата на выезде.
На въезде тоже нужно медленно проехать через пункт контроля,
вашу машину запомнят и подсчитают.
А на выезде нужно будет оплатить перед шлагбаумом.
Лучше наличкой,
автомат выдаёт сдачу,
а карта казахского банка почему-то не сработала.
За нашу маленькую машинку взяли чисто символические
450 тенге.
Будьте внимательны на подъезде к пунктам контроля,
там стоят знаки ограничения скорости,
а штрафы на превышение в Казахстане сильно больше российских.
Сэкономить денег не получится,
альтернативной бесплатной дороги до Нур-Султана нет.</p>
<p>Эта автомагистраль — самый скучный участок пути.
Можете понаслаждаться видом плоских степей до горизонта.
Некое разнообразие вносят только развязки и пересечение мелких речек
(половина из которых пересохли),
тут можно поймать неприятные неровности дороги,
снижайте скорость.
Официально тут можно гнать 120,
но стык на плитах на развязке может быть очень неприятным.</p>
<p>Появление самого Нур-Султана на горизонте —
весьма величественное зрелище.
Километров за 30 на горизонте появляются высоченные небоскрёбы.
Не «вырастают» из земли,
а проявляются тёмно-голубыми силуэтами на фоне ярко-голубого неба.</p>
<p>Вокруг города проложена окружная дорога.
Будьте внимательны,
северо-западная часть этого кольца ещё не до конца достроена,
движение там перекрыто.
Мы ехали по восточной половинке кольца.
Странная трасса.
То одна полоса,
то две.
Местами явные знаки,
разрешающие 90.
Местами 70.
Местами 60.
Но на знаках,
как и в России,
экономят.
Поэтому понять,
с какой скоростью можно ехать на данном участке дороги,
почти невозможно.
Навигатор тоже врёт.
Слишком часто получалось,
что я еду на «максимальных без штрафа» 68,
а местные ребята позади явно хотят ехать быстрее,
а обгон запрещён.</p>
<p>Бог с ним, с Нур-Султаном.
Город стал большим.
В жару на улицах находиться невозможно,
тени нет.
Пешком перемещаться затруднительно.
Типичный размер кварталов в новой части города: полкилометра.
Мы лишь наелись, засунули машинку в гараж
и отоспались.</p>
<p>А на следующий день отчалили на самолёте в Алматы.</p>
<p><a href="https://ru.wikipedia.org/wiki/Qazaq_Air">Qazaq Air</a>
летает замечательными самолётиками
<a href="https://ru.wikipedia.org/wiki/Bombardier_Q_Series">De Havilland Dash-8-Q400NG</a>.
Он же Bombardier DHC-8.
Турбовинтовой маленький летающий автобус,
очень похожий на <a href="https://ru.wikipedia.org/wiki/%D0%90%D0%BD-24">Ан-24</a>.
Только потише и побыстрее.
Приятный самолётик.</p>
<p><img alt="пропеллер Дэша" src="https://blog.gelin.ru/2022/07/propeller.jpg"></p>
<p>Итак, Алматы.</p>
<p>В Алма-Ате — горы.
Горы.
ГОРЫ.
Большущие.
Со снежными шапками.
Они видны отовсюду.
Очень впечатляют,
когда смотришь на них с самолёта.
Да и откуда угодно.
Я таких больших красивых гор
ещё ни разу не видел.
А за горами,
если верить карте,
<a href="https://ru.wikipedia.org/wiki/%D0%98%D1%81%D1%81%D1%8B%D0%BA-%D0%9A%D1%83%D0%BB%D1%8C">Иссык-Куль</a>
и <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%B8%D1%80%D0%B3%D0%B8%D0%B7%D0%B8%D1%8F">Кыргызстан</a>.
А дальше ещё горы.
<a href="https://ru.wikipedia.org/wiki/%D0%A2%D1%8F%D0%BD%D1%8C-%D0%A8%D0%B0%D0%BD%D1%8C">Тянь-Шань</a>, ведь.
Из Алма-Аты виден лишь самый краешек.</p>
<p>Любой город <a href="https://newsomsk.ru/news/53322-fantaziya_na_temu_omsk_v_gorax_nabiraet_populyarno/">становится лучше</a>,
если из него видны горы.</p>
<p><img alt="видны горы" src="https://blog.gelin.ru/2022/07/almaty.jpg"></p>
<p>Сам город расположен у подножия гор.
Поэтому он весь под наклоном.
Город почти весь прямоугольный,
<a href="https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D1%81%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5_%D0%B3%D0%BE%D1%80%D0%BE%D0%B4%D1%81%D0%BA%D0%B8%D1%85_%D0%BA%D0%B2%D0%B0%D1%80%D1%82%D0%B0%D0%BB%D0%BE%D0%B2">манхеттенская метрика</a> работает.
С севера на юг все улицы идут вверх,
к горам.
Буквально вверх,
под небольшим уклоном,
к горам.
Этот уклон чувствуется везде,
даже когда гор не видно.
С юга на север улицы идут вниз.
Идти так, конечно, легче,
но не всегда.
С запада на восток и с востока на запад
можно двигаться почти без уклона,
если повезёт.
В общем,
заблудиться невозможно.
Но меня хождение вверх по уклону
к концу недели стало сильно раздражать.</p>
<p>В первый же день нас потащили гулять по городу.
<a href="https://ru.wikipedia.org/wiki/%D0%9F%D0%B0%D1%80%D0%BA_%D0%B8%D0%BC%D0%B5%D0%BD%D0%B8_28_%D0%B3%D0%B2%D0%B0%D1%80%D0%B4%D0%B5%D0%B9%D1%86%D0%B5%D0%B2-%D0%BF%D0%B0%D0%BD%D1%84%D0%B8%D0%BB%D0%BE%D0%B2%D1%86%D0%B5%D0%B2">Парк 28 панфиловцев</a>.
<a href="https://ru.wikipedia.org/wiki/%D0%97%D0%B5%D0%BB%D1%91%D0%BD%D1%8B%D0%B9_%D0%B1%D0%B0%D0%B7%D0%B0%D1%80_(%D0%90%D0%BB%D0%BC%D0%B0-%D0%90%D1%82%D0%B0)">Зелёный базар</a>,
большущий страшный (для меня) крытый рынок.
Прошли (вверх) вдоль <a href="https://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D0%BB%D0%B0%D1%8F_%D0%90%D0%BB%D0%BC%D0%B0%D1%82%D0%B8%D0%BD%D0%BA%D0%B0">Малой Алматинки</a>.
Поднялись (пешком!) на <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BA-%D0%A2%D0%BE%D0%B1%D0%B5">Коктобе</a>.
И спустились обратно!
Коктобе, это небольшая гора,
где установлена телебашня,
которую печатают на открытках.
Сейчас там находится ещё и парк развлечений
с аттракционами, колесом обозрения
и небольшим зоопарком.</p>
<p><img alt="вид с Коктобе" src="https://blog.gelin.ru/2022/07/koktobe.jpg"></p>
<p>Алма-Ата — очень милый и уютный город.
Широкие тротуары.
Местами велодорожки.
Чуток пешеходных улиц.
Очень очень много деревьев и зелени.
Настолько много,
что даже фонари ночью не освещают улицы ярко,
сквозь листву свет не пробивается,
а создают приятный полумрак.
Куча фонтанов.
Мы нашли даже паровой фонтан.
Это когда оно брызгает не струями воды,
а распыляет воду в туман.
И можно ходить в этом тумане
как ёжик.</p>
<p>А на следующий день нас повезли в горы.
Не прямо в горы,
а сначала 200 километров на восток,
в сторону Китая,
а потом 70 километров на юго-запад,
к границе с Кыргызстаном,
уже в настоящие горы.</p>
<p>По пути останавливались в <a href="https://ru.wikipedia.org/wiki/%D0%A3%D0%B9%D0%B3%D1%83%D1%80%D1%8B">уйгурской</a> деревушке
<a href="https://ru.wikipedia.org/wiki/%D0%91%D0%B0%D0%B9%D1%81%D0%B5%D0%B8%D1%82">Байсеит</a>.
Позавтракать.
Там не то что по-русски,
даже по-казахски не очень разговаривают.
И, по традиции,
оставляют двери в дом открытыми.
Местный чай с молоком (или даже, скорее, молоко с чаем)
— очень даже ничего.
<a href="https://ru.wikipedia.org/wiki/%D0%A1%D0%B0%D0%BC%D1%81%D0%B0">Самса</a> и шашлыки — даже очень чего.</p>
<p>Ехали мы прогуляться в <a href="https://ru.wikipedia.org/wiki/%D0%A7%D0%B0%D1%80%D1%8B%D0%BD%D1%81%D0%BA%D0%B8%D0%B9_%D0%BA%D0%B0%D0%BD%D1%8C%D0%BE%D0%BD">Чарынском каньоне</a>.
Пару километров по каньону до реки.
Там отдохнули и перекусили.
И обратно, дорога вверх.
Вскарабкались по склону и посмотрели на каньон сверху.
Тут я понял,
что горы — это серьёзно.
Меня научили ходить с палками :)</p>
<p>Каньон — чертовски красивый.
Раньше такое видел только на картинках.
Дикая пустынная красота,
где от человека только дорога по дну каньона,
места отдыха
и группки туристов.
Но это мы были в будни.
Говорят,
по выходным это всё превращается
в толпы туристов и большой базар.</p>
<p><img alt="каньон" src="https://blog.gelin.ru/2022/07/charyn.jpg"></p>
<p>Дальше через перевалы,
обширные цветущие
(совершенно дикими цветами до горизонта)
долины,
снова перевалы,
снова цветущие долины...
Очень красивая дорога.
Добрались до села <a href="https://ru.wikipedia.org/wiki/%D0%A1%D0%B0%D1%82%D1%8B_(%D0%90%D0%BB%D0%BC%D0%B0%D1%82%D0%B8%D0%BD%D1%81%D0%BA%D0%B0%D1%8F_%D0%BE%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C)">Саты</a>.
Где-то чёрт-те где в горах.</p>
<p>Село маленькое.
Расположено в живописнейшей долине,
где пасут местных коров и стада овец.
И горная речка, конечно же.
В горах всегда где-нибудь рядом будет речка.</p>
<p><img alt="село Саты" src="https://blog.gelin.ru/2022/07/saty.jpg"></p>
<p>Радом есть несколько озёр,
на которые мы,
собственно,
и приехали.
Поэтому почти вся деревня состоит из «гостевых домов».
Жители реально пристраивают целые пристройки со спальнями к своим домам,
и оборудуют тёплые туалеты,
чтобы туристы платили за ночлег и питание.
Ну и нормальная сотовая связь присутствует,
если есть электричество,
станция торчит посреди деревни.</p>
<p>Озеро <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D0%B8%D0%BD%D0%B4%D1%8B_(%D0%BE%D0%B7%D0%B5%D1%80%D0%BE)">Каинды</a>.
Доехать туда от Саты можно только на «специально оборудованном» уазике.
Потому что туда ведёт дорога из щебня и булыжников,
плюс нужно переезжать вброд несколько речек.
А потом ещё нужно идти пешком,
несколько подъёмов и спусков,
и несколько речек по брёвнышку перейти.
Или ехать на лошадках.
За деньги, естественно.
Девочкам езда на лошадке понравилась.
Хотя, говорят,
было страшновато,
особенно на спусках.</p>
<p><img alt="озеро Каинды" src="https://blog.gelin.ru/2022/07/kaindy.jpg"></p>
<p>Я думал,
такой цвет воды получается только после манипуляций в Фотошопе.
Нифига.
Он на самом деле такой и есть.
Насыщенный бирюзовый.
Но прозрачная при этом.
На иголках еловых,
что ли настояна?
При образовании этого озера после
<a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%B5%D0%BC%D0%B8%D0%BD%D1%81%D0%BA%D0%BE%D0%B5_%D0%B7%D0%B5%D0%BC%D0%BB%D0%B5%D1%82%D1%80%D1%8F%D1%81%D0%B5%D0%BD%D0%B8%D0%B5">землетрясения</a> в 1911 году
были затоплены еловые леса.
Получается,
в Боровом были сосны,
а тут ели.</p>
<p>После ночёвки в Саты мы поехали на <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D1%8C%D1%81%D0%B0%D0%B9%D1%81%D0%BA%D0%B8%D0%B5_%D0%BE%D0%B7%D1%91%D1%80%D0%B0">озеро Кольсай</a>.
Нижнее озеро.
Одно из трёх Кольсайских озёр.
Тут есть цивилизация.
Можно доехать на обычной машине.
Есть гостиницы, парковка и прочие удобства.
По озеру можно прокатиться на водном велосипеде или на лодке.
Очень красиво.
А до следующего озера — четыре часа по горным тропам.
Мы уже не стали.</p>
<p><img alt="озеро Кольсай" src="https://blog.gelin.ru/2022/07/kolsay.jpg"></p>
<p>Здесь мы повстречали киношников.
Даже, наверное,
когда катались в лодке,
попали на общий план озера,
снятый с коптера.
Оказалось,
это какой-то совместный корейско-казахский фильм.
Мы даже задержались на полтора часика,
чтобы дождаться актёров,
посмотреть,
кто такие будут сниматься в какой-то романтичной сцене
с пледом и фруктами на фоне озера.
Дождались.
Какой-то корейский мальчик в ярком костюме
и какая-то казахская девочка в платье.
Я их не знаю :)
Скучная работа у киношников,
полдня собираться, всё готовить,
потом ждать актёров,
потом снять сцену максимум минут на пять,
потом полдня собираться, всё убирать.
Зато помощник режиссёра устроила
нам небольшой мастер-класс по правильной съёмке с телефона
и построению кадра :)</p>
<p>Снова Саты.
Снова обед.
Кормят вкусно.
Мы впервые попробовали
настоящий <a href="https://ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%80%D1%83%D1%82">курт</a>
и <a href="https://ru.wikipedia.org/wiki/%D0%96%D0%B5%D0%BD%D1%82">жент</a>.
Кажется, даже домашнего приготовления.
И обратно, в Алма-Ату.</p>
<p>На следующий день большие девочки пошли в горы.
В Алма-Ате можно доехать до гор на автобусе.
И там начинаются уже пешие маршруты.
Потрясающей красоты,
как оно всё там, в горах, выглядит
в июне,
когда всё цветёт.
Несколько часов можно пешком побродить по горным тропам.
Вверх и вниз.
А потом на другом автобусе уехать обратно в город.</p>
<p><img alt="горные тропы" src="https://blog.gelin.ru/2022/07/mountains.jpg"></p>
<p>Ну а мы с дочей осваивали городской электросамокатный транспорт.
Тут есть <a href="https://whoosh-bike.ru/">Whoosh</a> и <a href="https://jetshr.com/ru/">Jet</a>.
У Whoosh самокаты менее ушатанные и приложение чуток поприятнее.
У Jet цены чуть пониже.
Whoosh у меня стоял ещё с Омска.
Но в Казахстане к нему пришлось привязать другую, казахскую, карту.
Так же как и в <a href="https://ru.wikipedia.org/wiki/%D0%AF%D0%BD%D0%B4%D0%B5%D0%BA%D1%81.Go">Яндекс.Go</a>,
приложение то же самое,
а методы оплаты зависят от того,
где ты находишься.
С Whoosh всё хорошо,
только мы проклинали проспект Абая,
который оказался чертовски недружелюбным к пешеходам и самокатам.
И проклинали сам Whoosh,
который потребовал перетащить в конце поездки
самокаты через переходный мост на другую сторону дороги.
А этот мост был весьма крутым и лишь с полозьями для детских колясок.
Ну и поездка на Whoosh получилась одноразовой,
они почему-то смогли списать с меня её стоимость только когда я уже вернулся в Россию.
А пока на мне висел долг,
арендовать самокаты я не мог.</p>
<p>Проехали мы на самокатах почти всю доступную для самокатов Алма-Ату.
За часок.
Доехали до <a href="https://ru.wikipedia.org/wiki/%D0%9F%D0%B0%D1%80%D0%BA_%D0%B8%D0%BC%D0%B5%D0%BD%D0%B8_%D0%9F%D0%B5%D1%80%D0%B2%D0%BE%D0%B3%D0%BE_%D0%9F%D1%80%D0%B5%D0%B7%D0%B8%D0%B4%D0%B5%D0%BD%D1%82%D0%B0_%D0%A0%D0%B5%D1%81%D0%BF%D1%83%D0%B1%D0%BB%D0%B8%D0%BA%D0%B8_%D0%9A%D0%B0%D0%B7%D0%B0%D1%85%D1%81%D1%82%D0%B0%D0%BD_(%D0%90%D0%BB%D0%BC%D0%B0-%D0%90%D1%82%D0%B0)">парка имени Первого Президента</a>.
Которого нельзя называть, видимо.
Хех, а ведь там есть ещё и парк фонда Первого Президента.</p>
<p>Парк большой.
Недостаточно тенистый.
Там где-то есть обзорная площадка с отличным видом на горы.
До которой мы не дошли.</p>
<p>Куда ещё дошли наши ножки в последующие дни?</p>
<p><a href="https://ru.wikipedia.org/wiki/%D0%A6%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BF%D0%B0%D1%80%D0%BA_%D0%BA%D1%83%D0%BB%D1%8C%D1%82%D1%83%D1%80%D1%8B_%D0%B8_%D0%BE%D1%82%D0%B4%D1%8B%D1%85%D0%B0_%D0%B8%D0%BC%D0%B5%D0%BD%D0%B8_%D0%93%D0%BE%D1%80%D1%8C%D0%BA%D0%BE%D0%B3%D0%BE_(%D0%90%D0%BB%D0%BC%D0%B0-%D0%90%D1%82%D0%B0)">Парк Горького</a>.
Он же центральный парк культуры и отдыха.
Хороший парк.
Много деревьев.
Много тропинок.
Велодорожка тоже есть.
Нормально аттракционов,
для детей и их детей.
Есть даже своё маленькое озеро с катамаранами.
И всякие новомодные площадки для скейтборда и прочих опасных видов спорта.</p>
<p><img alt="крепость в парке" src="https://blog.gelin.ru/2022/07/central-park.jpg"></p>
<p>А за парком сразу — <a href="https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%BC%D0%B0%D1%82%D0%B8%D0%BD%D1%81%D0%BA%D0%B8%D0%B9_%D0%B7%D0%BE%D0%BE%D0%BF%D0%B0%D1%80%D0%BA">зоопарк</a>.
Хороший зоопарк.
Нам здорово повезло.
Видели, как ползает удав,
как плавает мурена,
как загорает большущий крокодил,
как бурый медведь купается и играет с покрышкой
(ну такую игрушку ему подсунули),
как спят летучие мыши,
как ест волк и слон.
Мне нравится эта тенденция в зоопарках
вместо решёток устанавливать стёкла.
Можно даже пофантазировать,
что ты этому тигрику пузико чешешь.</p>
<p>Прогулялись по местному <a href="https://ru.wikipedia.org/wiki/%D0%A3%D0%BB%D0%B8%D1%86%D0%B0_%D0%96%D0%B8%D0%B1%D0%B5%D0%BA_%D0%96%D0%BE%D0%BB%D1%8B_(%D0%90%D0%BB%D0%BC%D0%B0-%D0%90%D1%82%D0%B0)">Арбату</a>.
Проехали пару станций на метро.
Метро тут классное.
Поезда-гусеницы,
с гармошками между вагонами,
непрерывным пространством внутри.</p>
<p>В последний день поехали на автобусе до <a href="https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D0%B4%D0%B5%D1%83">Медеу</a>.
Тот самый знаменитый каток в горах.
Чтобы прокатиться на канатной дороге до <a href="https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D0%BC%D0%B1%D1%83%D0%BB%D0%B0%D0%BA">Шымбулака</a>.
Автобус проезжал через пару укреплений против <a href="https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D0%BB%D1%8C">селей</a>.
Страшновато, впечатляюще.
Сам Медеу видел только из кабинки <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D0%BD%D0%B0%D1%82%D0%BD%D0%B0%D1%8F_%D0%B4%D0%BE%D1%80%D0%BE%D0%B3%D0%B0_%D0%9C%D0%B5%D0%B4%D0%B5%D1%83_%E2%80%94_%D0%A7%D0%B8%D0%BC%D0%B1%D1%83%D0%BB%D0%B0%D0%BA">канатной дороги</a>.
Ну каток и каток.
Не сильно-то и большой.
Летом там, похоже,
на роликах катались.</p>
<p>А канатная дорога офигенная.
Это ж почти как колесо обозрения.
Только не три минуты, а двадцать.
И дороже.
И кабинки раскачиваются,
вместе с тросами,
на длинных промежутках между опорами тросы ходят вверх-вниз.
А иногда это всё может застрять на несколько минут,
если с электричеством перебои.
Это вам не в лифте застрять.
Это в стеклянной кабинке на единственном тросе
где-то между небом и землёй.
<a href="https://ru.wikipedia.org/wiki/%D0%90%D0%BA%D1%80%D0%BE%D1%84%D0%BE%D0%B1%D0%B8%D1%8F">Акро-</a> и <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BB%D0%B0%D1%83%D1%81%D1%82%D1%80%D0%BE%D1%84%D0%BE%D0%B1%D0%B8%D1%8F">клаустрофобам</a> не рекомендую.</p>
<p><img alt="канатная дорога" src="https://blog.gelin.ru/2022/07/shymbulak.jpg"></p>
<p>Вот там, на Шымбулаке,
наконец-то почувствовалось,
что в горах холодно.
Там даже нашлось немного снега.
В июне.
Наползали тучки.
Спотыкались о вершины гор.
И мы сбежали от дождя.</p>
<p>Дождь потом пошёл.
Когда мы уже вернулись в город.
Сильный дождь в Алматы — это когда сплошной поток воды по щиколотку,
от которого не спрячешься и не перешагнёшь.
Этот дождь потом нас догонял в Бурабае и окончательно догнал уже в Омске.</p>
<p>Да,
хорошо покушать в Алматы можно в
кафе <a href="https://go.2gis.com/g5b1y">Medovic</a>,
кафе <a href="https://go.2gis.com/b0j1k">Nedelka</a>,
кафе грузинской кухни <a href="https://go.2gis.com/oerte">Дадиани</a>.
А квартиры для проживания можно искать на
<a href="https://apartamenty.kz/">apartamenty.kz</a>
и <a href="https://krisha.kz/">krisha.kz</a>.</p>
<p>Самолётом обратно в Нур-Султан.
И на машине обратно в Омск.
В этот раз мы решили ещё больше не торопиться и поехать с двумя ночёвками.
В Бурабае и в Петропавловске.
Не более четырёх часов за рулём каждый день.
Очень комфортный график.</p>
<p>Снова искупались в Боровом.
Пока не согнал дождик.</p>
<p>Зато был вечер погулять в Петропавловске.
Милый городок.
Тут есть большая центральная площадь.
С большим центральным флагом.
И большой центральной пешеходной улицей.
Которая идёт через площадь в парк
снова Первого Президента.
А в парке есть
и памятник <a href="https://ru.wikipedia.org/wiki/%D0%90%D0%B1%D0%B0%D0%B9_%D0%9A%D1%83%D0%BD%D0%B0%D0%BD%D0%B1%D0%B0%D0%B5%D0%B2">Абаю</a> и Пушкину,
и аттракционы,
и монументы памяти Великой Отечественной войны.</p>
<p><img alt="Абай-Пушкин" src="https://blog.gelin.ru/2022/07/abay-pushkin.jpg"></p>
<p>Петропавловск — это меньше 300 километров от Омска.
И уже другая страна.
С кокаколой, айфонами и кинотеатрами.
И банками.
А это уже лайфхак.</p>
<p>В Петропавловске улицу,
на которой находилась квартира,
явно ремонтировали.
Середина проезжей части была раскопана и наспех засыпана.
Проезд затруднён,
но никаких запрещающих знаков не было,
когда мы заезжали.
А на следующее утро въезд и выезд
из этого квартала был перекрыт бетонными блоками.
С обоих концов улицы.
Замуровали!
С третьего раза удалось найти
выезд на соседнюю улицу через дворы.</p>
<p><a href="https://ru.wikipedia.org/wiki/2%D0%93%D0%98%D0%A1">2ГИС</a>
показал себя неплохим навигатором.
Дорогу по всем городам прокладывал правильно.
Не сильно врал о разрешённой скорости.
Все дороги и тропинки в алматинских горах знал.
А вот дорожек в алматинском зоопарке не знал.
Единственно,
не умел прокладывать маршрут между разными городами.
Поддержка ответила,
что эта фича пока работает только в России.
Но, скоро будет и везде.</p>
<p>В Казахстан на машинке ездить вполне можно.
Это не страшно.
И даже комфортно.
И уж точно дешевле, чем на самолёте.
Пикантик за 280 километров до Петропавловска сожрал меньше 16 литров бензина.
Это меньше шести литров на сотню.
Очень хорошо.
А в Казахстане бензин даже дешевле,
чем в России:
около 211 тенге за литр 95-го.</p>
<p>Алматы — прекрасный город.
Очень милый, уютный и красивый.
Уютнее и красивее,
чем Нур-Султан.
Жильё, правда, дорогое...</p>
<p><a href="https://photos.app.goo.gl/Tuz8NKiTBeLife5C6">Фоточки</a></p>О CodeFest2022-06-11T00:00:00+06:002022-06-12T05:21:42+06:00Денис Нелюбинtag:blog.gelin.ru,2022-06-11:/2022/06/codefest.html<p>Вот и состоялся <a href="https://12.codefest.ru/">двенадцатый по счёту CodeFest</a>.
И я там был,
пиво и коктейли пил.
На доклады ходил.
На афтепати немного общался.</p>
<p><a href="https://11.codefest.ru/">Прошлый CodeFest</a> в 2021 году
я пропустил.
Ковид гораздо интенсивнее шагал по планете.
Да и просто решил послать все конференции к чёрту.
Ошибался.</p>
<p>Конференции — это хорошо.
Крайне полезно …</p><p>Вот и состоялся <a href="https://12.codefest.ru/">двенадцатый по счёту CodeFest</a>.
И я там был,
пиво и коктейли пил.
На доклады ходил.
На афтепати немного общался.</p>
<p><a href="https://11.codefest.ru/">Прошлый CodeFest</a> в 2021 году
я пропустил.
Ковид гораздо интенсивнее шагал по планете.
Да и просто решил послать все конференции к чёрту.
Ошибался.</p>
<p>Конференции — это хорошо.
Крайне полезно
хотя бы раз в год
опыляться новыми знаниями и идеями.
И CodeFest для этого весьма выгоден.
Рядом с Омском
(от Омска до Новосибирска лишь ночь на поезде).
Относительно дёшев.
Не является узко тематической конференцией,
а значит,
можно нахвататься всего околоайтишного сразу везде.</p>
<p>И CodeFest оправдал ожидания.
Конференция нисколько не испортилась.
Местами даже стала лучше.
Отсутствие привязки треков к залам.
И, соответственно,
пометка одних и тех же докладов сразу несколькими треками.
Отличный тайминг:
40 минут на доклад +
20 минут на перерыв.
Афтепати на барной улочке,
где можно свободно перемещаться из одного бара в другой,
от одной компании к другой,
или даже просто застрять посередине
и послушать-поговорить.</p>
<p>Мы, по старой традиции,
поехали в Новосибирск на день раньше.
И посвятили пятницу очередной экскурсии по Академгородку.
Оказалось,
что Новосибирск в целом,
и Академгородок в частности,
в мае на 250% красивее,
чем в марте.
Сосны, берёзы,
Обское море.
Так что пусть CodeFest и дальше продолжает
случаться в мае.
А Новосибирский зоопарк
я ребёнку снова задолжал.</p>
<p><img alt="logo" src="https://blog.gelin.ru/2022/06/codefest-logo.svg"></p>
<p>Ладно.
Поехали по докладам.
Из тех,
что я успел посетить.</p>
<p>Андрей Себрант,
известный человек из Яндекса,
<a href="https://12.codefest.ru/lecture/2086">выступил</a>
с философским докладом про Метавселенную.
<a href="https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%B0%D0%B2%D1%81%D0%B5%D0%BB%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F">Metaverse</a>.
Это не обязательно виртуальная реальность.
То есть, она, конечно, виртуальная.
Но для взаимодействия с ней вовсе не нужны очки виртуальной реальности.
Это — виртуальное, цифровое отражение нашего реального мира.
Те же деньги — это часть Метавселенной,
у них почти и нет физического воплощения в настоящей реальной Вселенной.
А каждое наше действие в реальном мире
всё чаще и чаще оставляет цифровой след в Метавселенной.
И даже такая вроде простая очевидная вещь,
как ползунок времени в Яндекс.Погоде
стирает грань между настоящим и виртуальным.
В прошлом и настоящем это — настоящие наблюдения за погодой.
В будущем — это прогноз, поведение цифрового двойника погоды
в ещё не наступившем в реальности будущем.</p>
<p><img alt="Яндекс.Погода в будущем" src="https://blog.gelin.ru/2022/06/ya-weather.png"></p>
<p>Пример доклада,
которого не должно было быть.
Совсем.
<a href="https://12.codefest.ru/lecture/1975">Рассуждения</a>
Дениса Цветких про чистую архитектуру и DDD.
Не нужно из работ
<a href="https://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D1%80%D1%82%D0%B8%D0%BD,_%D0%A0%D0%BE%D0%B1%D0%B5%D1%80%D1%82_(%D0%B8%D0%BD%D0%B6%D0%B5%D0%BD%D0%B5%D1%80)">Роберта Мартина</a>
и
Эрика Эванса,
которые про архитектуру,
то есть,
довольно абстрактную вещь,
делать слишком конкретные выводы,
а потом ещё и жаловаться,
что эти выводы недостаточно конкретны.
Это ведь всего лишь рекомендации
(и таки да, повод для дальнейших консультаций).
А каждый понимает и применяет рекомендации
лишь в меру собственной испорченности.
И это не беда самих рекомендаций.</p>
<p>Александр Воронков
совершенно офигенно <a href="https://12.codefest.ru/lecture/2070">рассказал и показал</a>
про Music-as-a-Code.
К сожалению,
лишь в LiveChannel
(онлайн и в отдельном кинозале трансляция,
плюс маленькие доклады в перерывах).
Я попал лишь на самый конец,
во второй перерыв.
И очень пожалел, что не смотрел с самого начала.
Краткая теория <a href="https://blog.gelin.ru/2015/02/blog-post_22.html">музыки</a>,
равномерно темперированный строй,
и даже из девятнадцати ступеней (не знал).
Факт,
что октава на гитаре — это середина струны
(очевидно, но я не осознавал).
Ух.
И как закодить эти ноты,
и эффекты,
на Go.
И какой получается шикарный DSL,
где прямо в Vim можно диджеить на полную,
просто печатая код.
Демо было очуменным.
Если буду на <a href="https://ru.wikipedia.org/wiki/Ludum_Dare">Ludum Dare</a>,
буду так писать музыку :)</p>
<p><img alt="Live Channel" src="https://blog.gelin.ru/2022/06/live-channel.jpg"></p>
<p>Алексей Мерсон <a href="https://12.codefest.ru/lecture/1972">рассказал</a>
про проблемы масштабирования и нагрузки.
Меня, в плохом смысле,
порадовало,
что даже в проектах на 100+ человек,
где, вроде всё продумали про мониторинг и поддержку,
всё равно случаются косяки,
которые расследуются и исправляются месяцами.
И основным затыком являются даже не технические проблемы,
а, как оно часто бывает,
люди и их взаимоотношения.</p>
<p>Марат Сибгатулин из Яндекса
<a href="https://12.codefest.ru/lecture/2010">поделился болью</a>
управления сетями.
Как менеджерить эти сотни и тысячи коммутаторов,
в данном случае внутри Яндекса.
Я помню, что 20 лет назад всё было тоже больно.
Тогда для управления свичами были
telnet, веб интерфейс
и <a href="https://ru.wikipedia.org/wiki/SNMP">SNMP</a>.
Каждый из интерфейсов
предлагал доступ лишь к некоторому подмножеству фичей устройств.
Самым многообещающим выглядел SNMP.
Но у каждого производителя или даже у каждого устройства
был свой набор параметров,
с которым тоже надо было разбираться
и импортировать в скрипты.
За 20 лет мало что изменилось.
Появились новые многообещающие протоколы.
Но они по-прежнему не дают полного и совместимого контроля
девайсов разных производителей.
В результате инженеры Яндекса вынуждены
клепать свои костыли и строить свою систему управления.
Где-то там внутри работает Annushka,
штука, похожая на <a href="https://blog.gelin.ru/2018/06/terraform_12.html">Terraform</a>,
но больше заточенная на управление конфигурацией устройств,
а не облаков.
Пока закрытая разработка Яндекса.</p>
<p>Рене ван Беверн,
крутой учёный,
работающий на Хуавей,
единственный докладчик-иностранец,
но докладывавший на русском языке 👍,
<a href="https://12.codefest.ru/lecture/2061">рассказал про алгоритмы</a>.
«Мы тут придумываем алгоритмы,
так что для нас ваш бэкенд —
как фронтенд.»
На примере одного алгоритма
он показал,
какие они могут быть хитрыми.
И как доказывается,
что ещё хитрее их сделать нельзя.
Началась математика на конференции.
А алгоритм,
о котором шла речь,
— это <a href="https://en.wikipedia.org/wiki/Count%E2%80%93min_sketch">Count-min sketch</a>.
Крайне интересная штука,
родственная <a href="https://ru.wikipedia.org/wiki/%D0%A4%D0%B8%D0%BB%D1%8C%D1%82%D1%80_%D0%91%D0%BB%D1%83%D0%BC%D0%B0">фильтру Блума</a>.
Этот скетч позволяет
приблизительно подсчитать количество самых часто встречающихся
элементов в последовательности.
Причём размер структуры не зависит от размера последовательности,
но зависит от требуемой точности.</p>
<p><img alt="Count-min sketch" src="https://blog.gelin.ru/2022/06/count-min-sketch.jpg"></p>
<p>Ирина Степанова
<a href="https://12.codefest.ru/lecture/2003">рассказала</a>
про диалоговые интерфейсы.
Не голосовые,
а диалоговые.
Потому что боты должны и в текстовом чатике
достаточно умно общаться.
И таких ботов всё больше.
И каждая большая приличная компания хочет подобного бота себе.
И это уже сейчас возможно.
Воспитать и научить бота для каждого.</p>
<p>Второй день конференции
начался с <a href="https://12.codefest.ru/lecture/2004">выступления</a>
широко известного в узких кругах математика
Алексея Савватеева.
Популяризатора математики.
А вы знаете, что такое число?
А вы знаете, что такое <a href="https://ru.wikipedia.org/wiki/%D0%98%D1%80%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D1%87%D0%B8%D1%81%D0%BB%D0%BE">иррациональные числа</a>?
А показать сможете?
А <a href="https://ru.wikipedia.org/wiki/%D0%A2%D1%80%D0%B0%D0%BD%D1%81%D1%86%D0%B5%D0%BD%D0%B4%D0%B5%D0%BD%D1%82%D0%BD%D0%BE%D0%B5_%D1%87%D0%B8%D1%81%D0%BB%D0%BE">трансцендентные числа</a>?
А всегда ли будут встречаться пары простых чисел,
отличающиеся на два?
А вы знаете,
что в игре <a href="https://ru.wikipedia.org/wiki/%D0%93%D0%B5%D0%BA%D1%81">Гекс</a>,
во-первых, всегда выигрывает один из игроков,
не существует ничьи,
во-вторых, существует выигрышная стратегия
для того, кто ходит первым,
вот только никто не знает,
в чём именно заключается эта стратегия?
Математика — рулит.</p>
<p>Григорий Петров
<a href="https://12.codefest.ru/lecture/1982">рассказал</a> про Python в 2022 году.
Питон питонит нормально.
Кому сильно нужно,
есть экспериментальная версия без <a href="https://ru.wikipedia.org/wiki/%D0%93%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D0%B1%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0_%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%BF%D1%80%D0%B5%D1%82%D0%B0%D1%82%D0%BE%D1%80%D0%B0">GIL</a>.
Включённые батарейки —
одновременно и преимущество,
и недостаток Питона.
Документация у Питона действительно одна из самых лучших.
А с разрешением зависимостей всё ещё по-прежнему плохо,
потому что уйма зависимостей устанавливается
всего лишь запуском <code>setup.py</code> из пакета,
который делает что хочет.</p>
<p>Даниил Терентьев
неплохо <a href="https://12.codefest.ru/lecture/2047">раскрыл</a>
тему проблемных коммуникаций.
Идиот — не тот,
кто задаёт много уточняющих вопросов,
чтобы выяснить,
что же всё-таки имелось в виду,
а тот, кто этого не делает.
А ещё лучше сразу выражаться ясно и логично.
Особенно,
если вы — начальник.
Ну и логика,
включайте её.
<a href="https://4brain.ru/blog/4-%D0%B7%D0%B0%D0%BA%D0%BE%D0%BD%D0%B0-%D0%BB%D0%BE%D0%B3%D0%B8%D0%BA%D0%B8/">Четыре закона логики</a>,
упомянутые в докладе, —
это <a href="https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD_%D1%82%D0%BE%D0%B6%D0%B4%D0%B5%D1%81%D1%82%D0%B2%D0%B0">закон тождества</a>,
<a href="https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D1%82%D0%B8%D0%B2%D0%BE%D1%80%D0%B5%D1%87%D0%B8%D1%8F">закон (не)противоречия</a>,
<a href="https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD_%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D1%91%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE_%D1%82%D1%80%D0%B5%D1%82%D1%8C%D0%B5%D0%B3%D0%BE">закон исключённого третьего</a>
и <a href="https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD_%D0%B4%D0%BE%D1%81%D1%82%D0%B0%D1%82%D0%BE%D1%87%D0%BD%D0%BE%D0%B3%D0%BE_%D0%BE%D1%81%D0%BD%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F">закон достаточного основания</a>.</p>
<p><img alt="Как повысить уровень понимания" src="https://blog.gelin.ru/2022/06/understanding.jpg"></p>
<p>Антон Черноусов
из Яндекса
<a href="https://12.codefest.ru/lecture/1997">провёл</a> неплохое введение
в <a href="https://blog.gelin.ru/2018/03/lambda.html">Serverless</a>
на примере облаков Яндекса.
Мы с Антоном немного пообщались накануне на афтепати.
Developer Advocate, оказывается,
интересная профессия.
А Serverless в Яндекс.Cloud уже поддерживается.
Есть функции, триггеры, API Gateway,
бесплатные тарифы на немножко запусков.
Всё как у людей.
Можно пользоваться.</p>
<p>Мой любимый регулярный докладчик КодеФеста,
Виктор Грищенко,
снова <a href="https://12.codefest.ru/lecture/1998">порадовал</a>.
На этот раз объяснением концепции нового протокола консенсуса
на гномиках.
Не того консенсуса,
которого иногда пытаются добиться политики.
А <a href="https://en.wikipedia.org/wiki/Consensus_(computer_science)">консенсуса</a>,
когда узлы распределённой системы
хотят договориться об общей картине мира.
Например,
узлы в блокчейне хотят прийти
к общему согласию
насчёт последовательности блоков
и включённых в них транзакций.
Proof-of-Work в этих наших биткойнах
— это большая дорогостоящая лотерея,
кто победил — того и блок.
Здесь же Виктор предлагает иную концепцию.
За количество шагов,
равное удвоенному диаметру сети,
все узлы сети могут быть уверены,
что их ближайшие соседи
приняли и согласны с транзакцией.
Как гномики из <a href="https://ru.wikipedia.org/wiki/%D0%93%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B8_%D0%A4%D0%BE%D0%BB%D0%B7">Gravity Falls</a>.</p>
<p><img alt="мастер-гном" src="https://blog.gelin.ru/2022/06/gnome.png"></p>
<p>Сергей Хованов
<a href="https://12.codefest.ru/lecture/2072">рассказал</a> про ТРИЗ.
<a href="https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BE%D1%80%D0%B8%D1%8F_%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D1%8F_%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B5%D1%82%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B8%D1%85_%D0%B7%D0%B0%D0%B4%D0%B0%D1%87">ТРИЗ</a>
— это теория решения изобретательских задач.
Набор эмпирических методов,
позволяющих разрешать проблемы,
возникающие перед инженерами
(и изобретателями).
Сергей весьма весомо показал,
что эти методы могут применяться
и при решении задач в информационных технологиях.
Хоть последователи ТРИЗ и ведут себя
как какие-то сектанты,
существенное рациональное зерно
в этой теории есть.
Полезно начать применять.</p>
<p><a href="https://12.codefest.ru/lecture/2078">Закрыл</a> конференцию
Дмитрий Иванов.
Попытался дать прогнозы нашей программистской жизни
на десять лет вперёд.
Про IDE согласен.
В слиянии с инструментами совместной работы вроде GitHub
есть смысл
и видны движения.
Будет прикольно.
Логическое программирование — прекрасная штука.
Буду рад,
если оно возродится.
Привет, <a href="https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D0%BB%D0%BE%D0%B3_(%D1%8F%D0%B7%D1%8B%D0%BA_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)">Пролог</a>.
Синтезирование программ?
Ну, как-то сильно фантастично.
А как же P≠NP?</p>
<p>CodeFest по-прежнему хорош.
CodeFest за эти годы не испортился.
В этом году не было иностранных докладчиков.
За исключением Рене ван Беверна,
который, вообще-то,
изъяснялся на чистейшем русском языке,
хоть и с акцентом.
Мои опасения,
что будет много разговоров и намёков
на последние события,
не оправдались.
Хотя было подозрительно много участников
с местом работы <code>¯\_(□)_/¯</code>.
Как выяснилось,
скрипт так заменял пустое место работы
или фразы вроде «безработный» и «самозанятый».
Только при печати один символ в каомодзи
не пропечатался.
<code>¯\_(ツ)_/¯</code></p>
<p><img alt="кофий" src="https://blog.gelin.ru/2022/06/coffee.jpg"></p>О Глории Му2022-02-13T00:00:00+06:002022-02-14T06:34:01+06:00Денис Нелюбинtag:blog.gelin.ru,2022-02-13:/2022/02/gloria.html<p>Я, как всякий уважающий себя читатель художественной литературы на русском языке,
прочитал почти всего <a href="https://ru.wikipedia.org/wiki/%D0%91%D0%BE%D1%80%D0%B8%D1%81_%D0%90%D0%BA%D1%83%D0%BD%D0%B8%D0%BD">Бориса Акунина</a>.
Есть у него проект «Жанры».
С книгами соответствующих направлений: «Детская книга», «Шпионский роман», «Фантастика», «Квест».
Странное дело,
но «Детская книга» получилась гораздо более фантастичной,
чем «Фантастика».
И вообще, имхо, гораздо более добротной,
чем остальные …</p><p>Я, как всякий уважающий себя читатель художественной литературы на русском языке,
прочитал почти всего <a href="https://ru.wikipedia.org/wiki/%D0%91%D0%BE%D1%80%D0%B8%D1%81_%D0%90%D0%BA%D1%83%D0%BD%D0%B8%D0%BD">Бориса Акунина</a>.
Есть у него проект «Жанры».
С книгами соответствующих направлений: «Детская книга», «Шпионский роман», «Фантастика», «Квест».
Странное дело,
но «Детская книга» получилась гораздо более фантастичной,
чем «Фантастика».
И вообще, имхо, гораздо более добротной,
чем остальные книги серии.
Главным героем, там,
напоминаю,
является Эраст «Ластик» Фандорин,
сын Николаса Фандорина, героя серии «Приключения магистра»,
и правнук того самого <a href="https://ru.wikipedia.org/wiki/%D0%AD%D1%80%D0%B0%D1%81%D1%82_%D0%A4%D0%B0%D0%BD%D0%B4%D0%BE%D1%80%D0%B8%D0%BD">Эраста Петровича Фандорина</a>,
которого уж все знают.
Все эти персонажи выдуманы,
напоминаю.</p>
<p>«Детская книга» хороша.
Тут и приключения во времени,
при участии сумасшедшего учёного, конечно же,
и попадание в реальные исторические события,
как это умеет Акунин,
и даже залёт в весьма своеобразное будущее.
Гораздо лучше «Фантастики»
(которую я даже не помню, о чём).
Но это детская книга как бы для мальчиков,
в основном.</p>
<p>И издатель настаивал, что нужна ещё одна детская книга.
Для девочек.
А писать для девочек у Акунина как-то не получалось.
И он <a href="https://borisakunin.livejournal.com/85682.html">нашёл</a> соавтора.
Некую барышню с псевдонимом Глория Му.
Акунин придумал сюжет.
А Глория Му написала всё остальное.</p>
<p>Так появилась «Детская книга для девочек».
А старая «Детская книга» переименовалась в «Детскую книгу для мальчиков».
Здесь главной героиней является,
конечно же,
девочка.
Ангелина «Геля» Фандорина,
сестра Ластика и, соответственно,
тоже правнучка Эраста Петровича.</p>
<p>Девочки тоже умеют путешествовать в прошлое.
Но своими, женскими, путями.
И тоже неплохо вляпываются в исторические реалии.
Интересно.
Захватывающе.</p>
<p>Так я впервые познакомился с творчеством
нового для меня автора:
<a href="https://ru.wikipedia.org/wiki/%D0%93%D0%BB%D0%BE%D1%80%D0%B8%D1%8F_%D0%9C%D1%83">Глорией Му</a>.</p>
<p><img alt="Вернуться по следам" src="https://blog.gelin.ru/2022/02/footsteps.jpg"></p>
<p>Или всё было не так.
Возможно,
всё началось с этой цитаты:</p>
<blockquote>
<p>– Ну как тебе объяснить… – Папа по своей привычке начинал мерить шагами комнату. – Вот если я начну всем рассказывать, что ты никакая не маленькая девочка, а большой зеленый крокодил…<br>
Тут я начинала смеяться, а папа продолжал:<br>
– Вот-вот, на первый раз меня поднимут на смех. На второй задумаются, а на третий начнут к тебе присматриваться и говорить, что да, какая-то ты зеленоватая, и слишком много времени проводишь на болоте, и наверняка ешь других детей.<br>
– Неужели люди такие глупые? – не могла поверить я.<br>
– Люди всякие, – вздыхал папа, – и, к сожалению, довольно часто позволяют себе не думать, а только повторять чужие мысли – пусть и дурацкие.<br>
– И что же делать?<br>
– Ничего тут не поделаешь, – папа разводил руками, – против клеветы и мелочных придирок оружия еще не придумали. </p>
</blockquote>
<p>Где-то она мне попалась на глаза.
И я подумал,
что книгу про такого замечательного папу
обязательно нужно прочитать.
И прочитал.</p>
<p>Это — <a href="https://www.litres.ru/gloria-mu/vernutsya-po-sledam-58828384/">«Вернуться по следам»</a> Глории Му.
И там не только про папу.
Но и про маму, не менее,
но совсем по-другому замечательную.
И про собак.
И про весёлое советское деревенское детство.
И про невесёлые моменты этого детства.
Про смерть.
Про взросление.
И снова про собак.
И про лошадей.
И про друзей.
Про хорошо и про плохо.
Про жизнь.</p>
<p>Я не знаю,
как называется этот жанр.
Верю,
что во многом это автобиография.
Вроде как книга собрана из отдельных кусочков,
публиковавшихся в <a href="https://gloria-mu.livejournal.com/">ЖЖ автора</a>.
Но также допускаю,
что некоторые части там выдуманы.
В любом случае это надо читать.
Ибо давно я так не наслаждался русским языком.</p>
<p>А моя мама говорит,
что это первая книга за много-много лет,
над которой она рыдала.
Я, кстати, тоже рыдал.
Есть там такие моменты,
когда нельзя не рыдать.</p>
<p>Вот такая вам моя рекомендация.</p>
<p>Сильно надеюсь,
что книга зацепила не только меня.
Всё же переиздавалась она не раз,
с <a href="https://twitter.com/gloriamuauthor/status/1319215731909734400">разными обложками</a> :)
Такие книги делают нас, человеков,
а значит, и весь мир, лучше.</p>
<p><img alt="Четрые разные обложки" src="https://blog.gelin.ru/2022/02/covers.jpg"></p>
<p>А недавно,
пока лишь в электронном виде,
вышла <a href="https://www.litres.ru/gloria-mu/igra-v-dzhart/">«Игра в Джарт»</a>.
Автор утверждает,
что это фэнтези.
Даже про драконов.
Наверное, это так.
Правда, спойлер, драконов не будет.</p>
<p>Я бы сказал,
что «Игра в Джарт» — это литературный эксперимент.
На грани, не бейте меня, графомании.
Но нет.
Получилось прекрасно.</p>
<p>В книге три почти независимые части.</p>
<p>Первая, «Аятори», — чисто сказка.
Довольно короткая.
Но чрезвычайно прекрасная.
Тут прекрасно и волшебно каждое слово.
Настолько,
что я не удержался,
и несколько вечеров читал её вслух
своим девочкам.
Эта сказка просто просится быть прочитанной вслух.
Не зря выпустили отдельную аудиокнигу.</p>
<p>Ещё раз повторю.
Это самое волшебное и прекрасное на русском языке,
что я читал.
Не сюжет, сюжет тут не главное.
Язык.
Слова.
Звучание.
Ритм.
Не могу этого передать.
Читайте сами.</p>
<p>Ладно. Вот вам кусочек:</p>
<blockquote>
<p>Как-то раз Кочевник сидел на красивом холме (а если сесть на красивом холме, поворотившись спиной к ближним и дальним селеньям, и смотреть только вперед, то степь казалась бескрайней как прежде), и увидел необыкновенно красивого отрока лет пятнадцати. Очень красивым он был. Краше города Джидды, краше сестрицы Башалай. Шел так легко, что цветы и травы за ним поднимались, а глаза его сияли как солнце и луна.<br>
Остановившись перед Кочевником, отрок молвил:<br>
– Отправляйся в Поднебесную страну, ту, что между небом и землей, найди там девушку, прекрасную, сильную, стройную, высоко подпоясанную, дочь воды и ветра, и возьми ее в жены. Девушка захочет испытать тебя, обернется бобром и ринется в небесную реку, тогда ты…<br>
– Иди отсюда, мальчик, – сказал Кочевник.<br>
Отрок выпучил сияющие глаза, и вдруг исчез, рассеялся как утренний туман над морем.</p>
</blockquote>
<p><a href="https://youtu.be/nthmZSUN1Fc">Сидя на красивом холме</a>, ага.</p>
<p>Вторая история: «Последнее солнце».
Связана с первой лишь некоторыми пересекающимися деталями.
Ну и общей вселенной,
где всё это происходит.
Эта часть подлиннее.
Это серьёзная драма,
пусть и в фэнтезийном сеттинге.
Сложно.
Местами мрачно.
Хэппиэнда не будет.</p>
<p>Третья история: «Дорога до мечты».
Ещё длиннее, чем вторая.
Полноценный рыцарский роман.
То ли приквел,
то ли сиквел,
а, скорее, спин-офф
к предыдущим историям.
Действительно рыцарский роман,
правда-правда,
с девами озера,
заколдованными мечами
и всем прочим.
Хэппиэнд тут есть.
Хотя, что для кого-то хэппи,
для другого может быть просто энд.</p>
<p>Собственно,
«игра в Джарт» — это настольная игра,
существующая в этой фэнтезийной вселенной.
Отличается отсутствием правил.
Точнее, играющие в неё
выдумывают правила по ходу дела.
И таки в неё можно выиграть.
Или проиграть.
Лучше не проигрывать.</p>
<p>А <a href="https://aminoapps.com/c/japandeg/page/blog/aiatori/xpwW_7GrC2u3VM5Djro3ewrDo0jPEkNk0ED">«аятори»</a>
— это настоящая игра.
Японская.
Но я помню,
мы тоже во что-то подобное играли в детстве.
Накидывать верёвочку на пальцы и делать узоры.</p>
<p><img alt="Аятори" src="https://blog.gelin.ru/2022/02/ayatori.jpg"></p>
<p>Кроме «Вернуться по следам»,
«Детской книги для девочек»
и «Игры в Джарт»
Глория Му успела,
оказывается,
написать несколько рассказов и одну повесть.
Ранее они выходили в сборниках,
совместно с другими авторами.
А недавно появились отдельно
в виде электронных книг.</p>
<p>Пока что я прочитал лишь повесть
<a href="https://www.litres.ru/gloria-mu/zhonglery-59618898/">«Жонглёры»</a>.
Это как бы продолжение «Вернуться по следам».
Как бы,
потому что некоторые детали не сходятся.
Хотя героиня вроде та же Гло.</p>
<p>Повесть.
Небольшая.
Более взрослая.
Больше матов,
крепче слог.
Автор всё так же запутывает читателя
в лабиринтах собственных воспоминаний.
Весело.
Задорно.
Иногда завидуешь герою.
Иногда жалеешь.</p>
<p>А потом.
За пять страниц до конца...
Ох, гадкая Глория,
как же ты это со мной делаешь...
Начинаешь рыдать.
Прерываешь чтение.
Успокаиваешься.
Переходишь к пред-пред-предпоследней странице.
И снова рыдаешь.
И так все пять страниц до конца.</p>
<p>Потом,
конечно,
понимаешь,
что вся книга вела тебя к тому,
чтобы порыдать в конце.
Что всё это ради памяти одного человека.
Что ж, мы поплакали по нему.
Не жалко.</p>
<p>Люблю читать Глорию.
Даже завёл ей страничку в Википедии.
Почему-то раньше её завести у фанатов не получалось.
Удаляли из-за незначительности.
Почитайте её книги.
Нет там никакой незначительности!
Больше читателей — больше тиражи — больше значимости,
и страницу с Википедии не удалят! :)</p>