Критика протокола и оргподходов Telegram. Часть 1, техническая: опыт написания клиента с нуля — TL, MT

    В последнее время на Хабре стали чаще появляться посты о том, как хорош Telegram, как гениальны и опытны братья Дуровы в построении сетевых систем, и т.п. В то же время, очень мало кто действительно погружался в техническое устройство — как максимум, используют достаточно простой (и весьма отличающийся от MTProto) Bot API на базе JSON, а обычно просто принимают на веру все те дифирамбы и пиар, что крутятся вокруг мессенджера. Почти полтора года назад мой коллега по НПО "Эшелон" Василий (к сожалению, его учетку на Хабре стёрли вместе с черновиком) начал писать свой собственный клиент Telegram с нуля на Perl, позже присоединился и автор этих строк. Почему на Perl, немедленно спросят некоторые? Потому что на других языках такие проекты уже есть На самом деле, суть не в этом, мог быть любой другой язык, где еще нет готовой библиотеки, и соответственно автор должен пройти весь путь с нуля. Тем более, криптография дело такое — доверяй, но проверяй. С продуктом, нацеленным на безопасность, вы не можете просто взять и положиться на готовую библиотеку от производителя, слепо ему поверив (впрочем, это тема более для второй части). На данный момент библиотека вполне работает на "среднем" уровне (позволяет делать любые API-запросы).


    Тем не менее, в данной серии постов будет не так много криптографии и математики. Зато будет много других технических подробностей и архитектурных костылей (пригодится и тем, кто не будет писать с нуля, а будет пользоваться библиотекой на любом языке). Итак, главной целью было — попытаться реализовать клиент с нуля по официальной документации. То есть, предположим, что исходный код официальных клиентов закрыт (опять же во второй части подробнее раскроем тему того, что это и правда бывает так), но, как в старые времена, например, есть стандарт по типу RFC — возможно ли написать клиент по одной лишь спецификации, "не подглядывая" в исходники, хоть официальных (Telegram Desktop, мобильных), хоть неофициальных Telethon?


    Оглавление:



    Документация… она ведь есть? Правда?..


    Фрагменты заметок для этой статьи начали собираться еще прошлым летом. Всё это время на официальном сайте https://core.telegram.org документация была по состоянию на Layer 23, т.е. застряв где-то в 2014 году (помните, тогда даже каналов еще не было?). Конечно, по идее, это должно было позволять реализовать клиент с функциональностью на тот момент 2014 года. Но и в таком состоянии документация была, во-первых, неполна, во-вторых, местами противоречила сама себе. Чуть более месяца назад, в сентябре 2019, было случайно обнаружено, что на сайте большое обновление документации, на вполне свежий Layer 105, с пометкой, что теперь всю надо читать заново. Действительно, многие статьи были переработаны, но многие — так и остались без изменений. Поэтому, читая критику ниже по поводу документации, следует иметь в виду, что некоторые из этих вещей уже неактуальны, но некоторые — всё еще вполне. В конце концов, 5 лет в современном мире — это не просто много, а очень много. С тех времен (особенно если не учитывать выброшенные и заново возрожденные с тех пор геочаты) число API-методов в схеме выросло с сотни до более чем двухсот пятидесяти!


    С чего начать молодому автору?


    Неважно, пишете ли Вы с нуля или используете например готовые библиотеки типа Telethon для Python или Madeline для PHP, в любом случае Вам потребуется сначала зарегистрировать своё приложениеполучить параметры api_id и api_hash (работавшие с API ВКонтакте сразу понимают), по которым сервер будет идентифицировать приложение. Это придется сделать и по юридическим соображениям, но подробнее о том, почему авторы библиотек не могут его публиковать, поговорим во второй части. Возможно, вас удовлетворят тестовые значения, хотя они сильно ограничены — дело в том, что сейчас на свой номер можно зарегистрировать только одно приложение, так что не кидайтесь сразу очертя голову.


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


    И если писать с нуля, то до использования полученных параметров на самом деле еще далеко. Хотя https://core.telegram.org/ и говорит в Getting Started о них первым делом, на самом деле сначала придется реализовать протокол MTProto — но если Вы поверили раскладке по модели OSI в конце страницы общего описания протокола, то совершенно зря.


    На самом деле, и до MTProto, и после, на нескольких уровнях сразу (как говорят зарубежные работающие в ядре ОС сетевики, layer violation) на пути встанет большая, больная и ужасная тема...


    Бинарная сериализация: TL (Type Language) и его схема, и слои, и много других страшных слов


    Эта тема, собственно, в проблемах Telegram — ключевая. И страшных слов, если Вы попытаетесь в неё вникнуть, будет много.


    Итак, схема. Если на это слово Вам вспомнилась, скажем, JSON Schema, Вы подумали правильно. Цель та же: некоторый язык для описания возможного набора передаваемых данных. На этом, собственно, сходство и заканчивается. Если со страницы протокола MTProto, или из дерева исходных текстов официального клиента, мы попытаемся открыть какую-нибудь схему, то увидим нечто вроде:


    int ? = Int;
    long ? = Long;
    double ? = Double;
    string ? = String;
    
    vector#1cb5c415 {t:Type} # [ t ] = Vector t;
    
    rpc_error#2144ca19 error_code:int error_message:string = RpcError;
    
    rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
    rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
    rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;
    
    msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;
    
    ---functions---
    
    set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer;
    
    ping#7abe77ec ping_id:long = Pong;
    ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;
    
    invokeAfterMsg#cb9f372d msg_id:long query:!X = X;
    invokeAfterMsgs#3dc4b4f0 msg_ids:Vector<long> query:!X = X;
    
    account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;
    account.sendChangePhoneCode#8e57deb flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode;

    Человек, видящий это впервые, интуитивно сможет распознать только часть написанного — ну, это видимо структуры (хотя где имя, слева или справа?), вот есть поля в них, после которых через двоеточие идёт тип… наверное. Вот в угловых скобках наверное шаблоны как в Си++ (на самом деле, не совсем). А что значат все остальные символы, знаки вопроса, восклицательные, проценты, решетки (причем явно ведь в разных местах значат разное), где-то присутствующие, а где-то нет, шестнадцатиричные циферки — и самое главное, как из этого получить правильный (который не будет отвергнут сервером) поток байт? Придется читать документацию (да, там рядом бывают ссылки на схему в JSON-версии — но понятнее от этого не становится).


    Открываем страницу Binary Data Serialization и погружаемся в волшебный мир грибов и дискретной математики нечто похожее на матан на 4 курсе. Алфавит, тип, значение, комбинатор, функциональный комбинатор, нормальная форма, композитный тип, полиморфный тип… и всё это только первая страница! Дальше Вас ожидает TL Language, который хоть уже и содержит пример тривиального запроса и ответа, совершенно не дает ответа на более типичные случаи, а значит, придется продираться через пересказ математики в переводе с русского на английский еще на восьми вложенных страницах!


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


    • да, цель звучит хорошо, но увы, она не достигается
    • образование в ВУЗах России варьирует даже среди IT-шных специальностей — соответствующий курс читали не всем
    • наконец, как мы увидим, на практике это не требуется, поскольку используется лишь ограниченное подмножество даже того TL, что был описан

    Как сказал LeoNerd на канале #perl в IRC-сети FreeNode, пытавшийся реализовать гейт из Telegram в Matrix (перевод цитаты неточный по памяти):


    Такое чувство, что кто-то впервые познакомился с теорией типов, пришел в восторг и начал пытаться играться с этим, не особенно заботясь, нужно ли это на практике.

    Смотрите сами, если необходимость bare-типов (int, long и т.д.) как чего-то элементарного вопросов не вызывают — в конечном счете их надо реализовать вручную — для примера возьмем попытку вывести из них вектор. То есть, на самом деле, массив, если называть получившиеся вещи своими именами.


    Но прежде


    Краткое описание подмножества синтаксиса TL для тех, кто не… ет читать официальную документацию


    constructor = Type;
    myVec ids:Vector<long> = Type;
    
    fixed#abcdef34 id:int = Type2;
    
    fixedVec set:Vector<Type2> = FixedVec;
    
    constructorOne#crc32 field1:int = PolymorType;
    constructorTwo#2crc32 field_a:long field_b:Type3 field_c:int = PolymorType;
    constructorThree#deadcrc bit_flags_of_what_really_present:# optional_field4:bit_flags_of_what_really_present.1?Type = PolymorType;
    
    an_id#12abcd34 id:int = Type3;
    a_null#6789cdef = Type3;

    Начинает определение всегда конструктор, после которого опционально (на практике — всегда) через символ # следует CRC32 от нормализованной строки описания данного типа. Дальше идет описание полей, если они есть — тип может быть и пустым. Заканчивает это всё через знак равенства имя типа, которому данный конструктор — то есть, фактически, подтип — принадлежит. Тот тип, что справа от знака равенства, он полиморфный — то есть ему может соответствовать несколько конкретных типов.


    Если же определение встретилось после строки ---functions---, то синтаксис останется таким же, но смысл будет уже другой: конструктор станет именем RPC-функции, поля — параметрами (ну то есть он останется точно такой же структурой данной, как описано ниже, просто таков будет наделяемый смысл), а "полиморфный тип" — типом возвращаемого результата. Он, правда, всё равно останется полиморфным — просто определенным в секции ---types---, а этот вот конструктор будет "не считаться". Перегрузки типов вызываемых функций по их аргументам, т.е. несколько функций с одним и тем же именем, но разной сигнатурой, как в C++, в TL почему-то не предусмотрено.


    Почему "конструктор" и "полиморфный", если это не ООП? Ну, на самом деле, кому-то будет проще будет думать об этом именно в терминах ООП — полиморфный тип как абстрактный класс, а конструкторы — это его прямые классы-наследники, причем final в терминологии ряда языков. На самом деле, конечно, здесь лишь похожесть с реальными перегруженными методами конструкторов в ОО-языках программирования. Поскольку тут — всего лишь структуры данных, никаких методов нет (хотя описание функций и методов далее вполне способно создать путаницу в голове, что они есть, но то речь о другом) — то можно думать о конструкторе как о значении, из которого конструируется тип при чтении потока байт.


    Как это происходит? Десериализатор, который всегда читает по 4 байта, видит значение 0xcrc32 — и понимает, что дальше будет field1 с типом int, т.е. читает ровно 4 байта, на этом вышележащее поле с типом PolymorType прочитано. Видит 0x2crc32 и понимает, что дальше два поля, сначала long, значит читаем 8 байт. А дальше опять сложный тип, который десериализуется аналогично. Например, Type3 мог быть объявлен в схеме как только два конструктора, соответственно, дальше должны встретиться либо 0x12abcd34, после которого надо прочитать еще 4 байта int, либо 0x6789cdef, после которого не будет ничего. Чтоугодно другое — надо выкинуть исключение. В любом случае после этого мы возвращаемся к чтению 4 байт int поля field_c в constructorTwo и на том заканчиваем читать наш PolymorType.


    Наконец, если попался 0xdeadcrc для constructorThree, то всё становится сложнее. Первым у нас поле bit_flags_of_what_really_present с типом # — на самом деле, это всего лишь алиас для типа nat, означающего "натуральное число". То есть, по сути, unsigned int — единственный, кстати, случай, когда в реальных схемах встречаются беззнаковые числа. Итак, дальше конструкция со знаком вопроса, означающая, что вот это поле — оно будет присутствовать on the wire, только если установлен соответствующий бит в поле, на которое сослались (примерно как тернарный оператор). Итак, предположим, что этот бит стоял, значит, дальше надо читать поле типа Type, у которого в нашем примере 2 конструктора. Один пустой (состоит только из идентификатора), в другом есть поле ids с типом ids:Vector<long>.


    Вы можете подумать, что как шаблоны и generic'и в плюсах или Java. Но нет. Ну, почти. Это единственный случай применения угловых скобок в реальных схемах, и он используется ТОЛЬКО для Vector. В потоке байт это будут 4 байт CRC32 для самого типа Vector, всегда одинаковые, потом 4 байта — число элементов массива, и дальше сами эти элементы.


    Добавьте к этому то, что сериализация всегда происходит словами по 4 байта, все типы ей кратны — к встроенным типам описаны еще bytes и string с ручной сериализацией длины и этого выравнивания по 4 — ну, вроде бы звучит нормально и даже сравнительно эффективно? Хотя TL заявляется как эффективная бинарная сериализация, но хрен уж с ними, с расширением чего попало, даже булевых значений и односимвольных строк до 4 байт, всё равно JSON будет куда толще? Вон, даже ненужные поля могут быть пропущены битовыми флагами, всё совсем хорошо, и даже расширяемо на будущее, взял да и досыпал новых опциональных полей в конструктор потом?..


    А вот нет, если читать не моё краткое описание, а полную документацию, и подумать над реализацией. Во-первых, CRC32 конструктора считается по нормализованной строке текстового описания схемы (убрать лишние whitespace и т.д.) — так что если добавляется новое поле, изменится строка описания типа, а значит и её CRC32 и, следовательно, сериализация. Да и что старый клиент делал бы, если бы ему пришло поле с новыми установленными флагами, а он не знает, что с ними делать дальше?..


    Во-вторых, вспомним о CRC32, которая применяется здесь по сути в качестве хэш-функции для уникального определения, что за тип (де)сериализуется. Тут мы сталкиваемся с проблемой коллизий — и нет, вероятность не единица на 232, а значительно больше. Кто вспомнил о том, что CRC32 заточена на обнаружение (и исправление) ошибок в канале связи, и соответственно улучшает эти свойства в ущерб другим? Например, ей плевать на перестановку байт: если Вы посчитаете CRC32 от двух строк, во второй первые 4 байта поменяете местами со следующими 4 байтами — она будет одинакова. Когда у нас на входе текстовые строки из латинского алфавита (и немного пунктуации), и имена эти не особо случайны, вероятность такой перестановки здорово повышается.


    Кстати, а кто проверял, что там действительно CRC32? В одном из ранних исходников (еще до Вальтмана) была хэш-функция, умножавшая каждый символ на так любимое этими людьми число 239, ха-ха!

    Наконец, ладно, мы поняли, что конструкторы с типом поля Vector<int> и Vector<PolymorType> будут иметь разный CRC32. А что насчет представления на линии? И с точки зрения теории, становится ли это частью типа? Допустим, мы передаем массив из десяти тысяч чисел, ну с Vector<int> всё понятно, длина и еще 40000 байт. А если это Vector<Type2>, который состоит только из одного поля int и он один в типе — надо ли нам 10000 раз повторять 0xabcdef34 и затем 4 байта int, или же язык в состоянии ВЫВЕСТИ это за нас из конструктора fixedVec и вместо 80000 байт передать опять только 40000 ?


    Это совсем не праздный теоретический вопрос — представьте, Вы получаете список пользователей группы, каждый из которых имеет id, имя, фамилию — разница в объеме передаваемых данных по мобильному соединению может быть значительной. Именно эффективность сериализации Telegram нам и рекламируют.


    Итак...


    Vector, который так и не смогли вывести


    Если Вы попытаетесь продраться через страницы описания комбинаторов и около, Вы увидите, что вектор (и даже матрицу) формально пытаются вывести через tuples несколько листов. Но в конечном итоге забивают, конечный шаг пропускается, и просто дается определение вектора, который еще и не привязан к типу. В чем тут дело? В языках программирования, особенно функциональных, вполне типично описать структуру рекурсивно — компилятор с его lazy evaluation сам всё поймёт и сделает. В языке сериализации данных же необходима ЭФФЕКТИВНОСТЬ: достаточно просто описать список, т.е. структуру из двух элементов — первым элемент данных, вторым — саму эту же структуру либо пустое место для хвоста (пачка (cons) в Lisp). Но это, очевидно, потребует для каждого элемента дополнительно тратить 4 байта (CRC32 в случае в TL) на описание его типа. Легко можно описать и массив фиксированного размера, но вот в случае массива заранее неизвестной длины — обламываемся.


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


    Serialization always uses the same constructor “vector” (const 0x1cb5c415 = crc32("vector t:Type # [ t ] = Vector t”) that is not dependent on the specific value of the variable of type t.

    The value of the optional parameter t is not involved in the serialization since it is derived from the result type (always known prior to deserialization).

    Присмотритесь: vector {t:Type} # [ t ] = Vector t — но нигде в самом этом определении не сказано, что первое число должно быть равным длине вектора! И ниоткуда это не следует. Это данность, которую нужно держать в голове и реализовывать руками. В других местах документация даже честно упоминает, что тип ненастоящий:


    The Vector t polymorphic pseudotype is a “type” whose value is a sequence of values of any type t, either boxed or bare.

    … но не акцентирует на этом внимание. Когда Вы, устав продираться через натягивание математики (может быть даже известной Вам из университетского курса), решаете забить и смотреть уже собственно как с этим работать на практике, в голове осталось впечатление: тут Серьезная Математика в основе, придумывали явно Крутые Люди (два математика-призера ACM), а не кто попало. Цель — пустить пыль в глаза — достигнута.


    Кстати, о числе. Напомним, # это синоним nat, натурального числа:

    There are type expressions (type-expr) and numeric expressions (nat-expr). However, they are defined the same way.
    type-expr ::= expr
    nat-expr ::= expr


    но в грамматике они описаны одинаково, т.е. эту разницу опять надо помнить и закладывать в реализацию руками.

    Ну и да, шаблонные типы (vector<int>, vector<User>) имеют общий идентификатор (#1cb5c415), т.е. если знаешь, что вызов объявлен как


    users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;

    то ждёшь уже не просто вектор, а вектор юзеров. Точнее, должен ждать — в реальном коде каждый элемент, если не bare-тип, будет иметь конструктор, и по-хорошему в имплементации надо бы проверять — а нам точно в каждом элементе этого вектора прислали того типа? А если это был какой-нибудь PHP, у которого в массиве могут лежать разные типа в разных элементах?


    На этом месте начинаешь задумываться — а нужен ли такой TL? Может, для телеги можно было бы и человеческий сериализатор использовать, тот же protobuf, уже тогда существовавший? Это была теория, давайте посмотрим на практику.


    Существующие реализации TL в коде


    TL родился в недрах ВКонтакте еще до известных событий с продажей доли Дурова и (наверное), еще до начала разработки Telegram. И в выложенных в опенсорс исходниках первой реализации можно найти много весёлых костылей. Да и сам язык там был реализован более полно, чем сейчас в Telegram. Например, хэши в схеме не используются совсем (имеется в виду встроенный псевдотип (как вектор) с девиантным поведением). Или


    Templates are not used now. Instead, the same universal constructors (for example, vector {t:Type} [t] = Vector t) are used w

    но рассмотрим для полноты картины, чтобы проследить, так сказать, эволюцию Гиганта Мысли.


    #define ZHUKOV_BYTES_HACK
    
    #ifdef ZHUKOV_BYTES_HACK
    
    /* dirty hack for Zhukov request */

    Или вот, прекрасное:


        static const char *reserved_words_polymorhic[] = {
    
          "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", NULL
    
          };

    Этот фрагмент — про шаблоны, вида:


    intHash {alpha:Type} vector<coupleInt<alpha>> = IntHash<alpha>;

    Это определение шаблонного типа хэшмэп, как вектора пар int — Type. В C++ это выглядело бы примерно так:


        template <T> class IntHash {
          vector<pair<int,T>> _map;
        }

    так вот, alpha — ключевое слово! Но только в C++ ты можешь писать T, а должен писать alpha, beta… Но не Больше 8 параметров, на тэте фантазия кончилась. Так и представляется, что когда-то в Питере случились примерно такие диалоги:


    -- Надо сделать в TL шаблоны
    -- Бл... Ну пусть параметры зовут альфа, бета,... Какие там ещё буквы есть... О, тэта!
    -- Грамматика? Ну потом напишем
    
    -- Смотрите, какой я синтаксис придумал для шаблонов и вектора!
    -- Ты долбанулся, как мы это парсить будем?
    -- Да не ссыте, он там один в схеме, захаркодить -- и ок

    Но это было про первую выложенную реализацию TL "вообще". Перейдём к рассмотрению реализаций в собственно Telegram-клиентах.


    Слово Василию:


    Vasily, [09.10.18 17:07]
    Больше всего жопа раскаляется от того, что они навертели кучу абстракций, а потом забили на них болт, и обложили кодогегератор костылями
    В результате, сначала от доки лётчик.jpg
    Потом от кода джекичан.webp

    Конечно, от знакомых с алгоритмами и математикой людей мы можем ожидать, что они читали Ахо, Ульмана, и знакомы со ставшими за десятилетия стандартом де-факто в отрасли инструментами для написания компиляторов своих DSL, правда?..


    Автором telegram-cli является Виталий Вальтман, как можно понять по встречаемости формата TLO за его (cli) пределами, член команды — сейчас библиотека для парсинга TL выделена отдельно, какое складывается впечатление о её парсере TL?..


    16.12 04:18 Vasily: по-моему, кто-то не осилил lex+yacc
    16.12 04:18 Vasily: иначе я не могу объяснить это
    16.12 04:18 Vasily: ну или им за количество строк в вк платили
    16.12 04:19 Vasily: 3к+ строк др<censored> вместо парсера

    Может, исключение? Давайте посмотрим, как делает это ОФИЦИАЛЬНЫЙ клиент — Telegram Desktop:


        nametype = re.match(r'([a-zA-Z\.0-9_]+)(#[0-9a-f]+)?([^=]*)=\s*([a-zA-Z\.<>0-9_]+);', line);
        if (not nametype):
          if (not re.match(r'vector#1cb5c415 \{t:Type\} # \[ t \] = Vector t;', line)):
             print('Bad line found: ' + line);

    1100+ строк на Питоне, пара регулярок + особые случаи типа вектора, который, конечно, объявлен в схеме как полагается по синтаксису TL, но клали они на этот синтаксис, парсить его еще… Спрашивается, зачем было городить всё это чудище слоёное, если всё равно никто не собирается это парсить по документации?!


    Кстати… Помните, мы говорили о проверке CRC32? Так вот, в кодогенераторе Telegram Desktop есть список исключений для тех типов, в которых рассчитанный CRC32 не совпадает с указанным в схеме!


    Vasily, [18.12 22:49]
    и тут бы задуматься, а нужен ли такой TL
    если бы я хотел подгадить альтернативным реализациям, я бы начал переносы строк вставлять, половина парсеров сломается на многострочных определениях
    tdesktop, впрочем, тоже

    Запомните момент об однострочности, мы к нему вернемся чуть позже.


    Ладно, telegram-cli — неофициальный, Telegram Desktop — официальный, но что насчет других? А кто знает?.. В коде Android-клиента вообще не нашлось парсера схемы (что вызывает вопросы к опенсорсности, но это для второй части), зато нашлось несколько других весёлых кусков кода, но о них в подразделе ниже.


    Какие еще вопросы на практике поднимает сериализация? Например, наворотили они, конечно, с битовыми полями и условными полями:


    Vasily: flags.0? true
    означает, что поле присутствует и равно true, если флаг выставлен

    Vasily: flags.1? int
    означает, что поле присутствует, и его надо десериализовать

    Vasily: Жопа, не гори, что ты делаешь!
    Vasily: Там где-то в доке есть упоминание, что true — это голый тип нулевой длины, но из их доки что-то собрать нереально
    Vasily: В сорцах открытых реализаций этого тоже нет, зато есть куча костылей и подпорок

    А, допустим, Telethon? Забегая вперёд по теме MTProto, пример — в документации есть вот такие куски, но знак % в ней описан только как "соответствующий данному bare-тип", т.е. в примерах ниже или ошибка, или нечто недокументированное:


    Vasily, [22.06.18 18:38]
    В одном месте:
    msg_container#73f1f8dc messages:vector message = MessageContainer;


    В другом:
    msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;


    И это две большие разницы, в реале приходит какой-то голый вектор

    Я не видел bare определения вектора и не встречал его

    В telethon руками написан разбор

    В его схеме закоментировано определение msg_container

    Опять же, остаётся вопрос про %. Оно не описано.

    Vadim Goncharov, [22.06.18 19:22]
    а в tdesktop?

    Vasily, [22.06.18 19:23]
    Но их парсер TL на регуляиках это тоже скорее всего не съест

    // parsed manually

    TL красивая абстракция, никто его не реализует полностью

    А % в их версии схемы нет

    Но тут документация противоречит сама себе, так что хз

    Оно встречалось в грамматике, они могли просто забыть описать семантику

    Ты ж видел доку на TL, там без поллитры не разберёшься

    "Ну допустим", скажет иной читатель, "что-то вы всё критикуете, так покажите, как надо".


    Василий отвечает: "а что касается парсера, мне штуки вида


        args: /* empty */ { $$ = NULL; }
            | args arg { $$ = g_list_append( $1, $2 ); }
            ;
    
        arg: LC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
                | LC_ID ':' condition '?' type-term { $$ = tl_arg_new_cond( $1, $5, $3 ); free($3); }
                | UC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
                | type-term { $$ = tl_arg_new( "", $1 ); }
                | '[' LC_ID ']' { $$ = tl_arg_new_mult( "", tl_type_new( $2, TYPE_MOD_NONE ) ); }
                ;

    как-то больше нравятся, чем


    struct tree *parse_args4 (void) {
      PARSE_INIT (type_args4);
      struct parse so = save_parse ();
      PARSE_TRY (parse_optional_arg_def);
      if (S) {
        tree_add_child (T, S);
      } else {
        load_parse (so);
      }
      if (LEX_CHAR ('!')) {
        PARSE_ADD (type_exclam);
        EXPECT ("!");
      }
      PARSE_TRY_PES (parse_type_term);
      PARSE_OK;
    }

    или


            # Regex to match the whole line
            match = re.match(r'''
                ^                  # We want to match from the beginning to the end
                ([\w.]+)           # The .tl object can contain alpha_name or namespace.alpha_name
                (?:
                    \#             # After the name, comes the ID of the object
                    ([0-9a-f]+)    # The constructor ID is in hexadecimal form
                )?                 # If no constructor ID was given, CRC32 the 'tl' to determine it
    
                (?:\s              # After that, we want to match its arguments (name:type)
                    {?             # For handling the start of the '{X:Type}' case
                    \w+            # The argument name will always be an alpha-only name
                    :              # Then comes the separator between name:type
                    [\w\d<>#.?!]+  # The type is slightly more complex, since it's alphanumeric and it can
                                   # also have Vector<type>, flags:# and flags.0?default, plus :!X as type
                    }?             # For handling the end of the '{X:Type}' case
                )*                 # Match 0 or more arguments
                \s                 # Leave a space between the arguments and the equal
                =
                \s                 # Leave another space between the equal and the result
                ([\w\d<>#.?]+)     # The result can again be as complex as any argument type
                ;$                 # Finally, the line should always end with ;
                ''', tl, re.IGNORECASE | re.VERBOSE)

    это ВЕСЬ лексер:


        ---functions---         return FUNCTIONS;
        ---types---             return TYPES;
        [a-z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return LC_ID;
        [A-Z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return UC_ID;
        [0-9]+                  yylval.number = atoi(yytext); return NUM;
        #[0-9a-fA-F]{1,8}       yylval.number = strtol(yytext+1, NULL, 16); return ID_HASH;
    
        \n                      /* skip new line */
        [ \t]+                  /* skip spaces */
        \/\/.*$                 /* skip comments */
        \/\*.*\*\/              /* skip comments */
        .                       return (int)yytext[0];

    т.е. попроще — это мягко сказано".


    В общем, в итоге парсер и кодогенератор для реально используемого подмножества TL уложился в примерно 100 строк грамматики и ~300 строк генератора (считая и все print'ы генерируемого кода), включая плюшки типа информацию о типах для интроспекции в каждом классе. Каждый полиморфный тип превращается в пустой абстрактный базовый класс, а конструкторы — наследуются от него и имеют методы для сериализации и десериализации.


    Нехватка типов в языке типов


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


    Прежде всего, constraints. Вот мы видим в документации по закачке файлов:


    The file’s binary content is then split into parts. All parts must have the same size ( part_size ) and the following conditions must be met:
    • part_size % 1024 = 0 (divisible by 1KB)
    • 524288 % part_size = 0 (512KB must be evenly divisible by part_size)


    The last part does not have to satisfy these conditions, provided its size is less than part_size.

    Each part should have a sequence number, file_part, with a value ranging from 0 to 2,999.

    After the file has been partitioned you need to choose a method for saving it on the server. Use upload.saveBigFilePart in case the full size of the file is more than 10 MB and upload.saveFilePart for smaller files.
    [...] one of the following data input errors may be returned:
    • FILE_PARTS_INVALID — Invalid number of parts. The value is not between 1..3000

    Что-нибудь из этого присутствует в схеме? Это как-то выразимо средствами TL? Нет. Но позвольте, ведь даже дедовский Turbo Pascal умел описывать типы, задаваемые диапазонами. И еще одну вещь умел, ныне более известную как enum — тип, состоящий из перечисления фиксированного (небольшого) количества значений. В языках типа Си — числовых, заметьте, мы пока говорили только о типах чисел. А ведь есть еще массивы, строки… например, неплохо было бы описать, что вот эта строка может содержать только номер телефона, да?


    Ничего из этого в TL нет. Зато есть, например, в JSON Schema. И если про делимость 512 Кб кто-то еще может возразить, что такое всё равно надо проверять в коде, то сделать так, чтобы клиент попросту не мог послать номер вне диапазона 1..3000 (и соответствующей ошибки не могло возникнуть) уж можно было бы, да?..


    Кстати, об ошибках и возвращаемых значениях. Глаз замыливается даже у тех, кто поработал с TL — до нас не сразу дошло, что каждая функция в TL на самом деле может вернуть не только описанный тип возврата, но и ошибку. Но это средствами самого TL не выводимо никак. Конечно, оно и так понятно и нафиг не нужно на практике (хотя на самом деле, RPC можно делать по-разному, мы еще вернемся к этому) — но как же Чистота концепций Математики Абстрактных Типов из мира горнего?.. Взялся за гуж — так соответствуй уж.


    И в конце концов, что насчет читабельности? Ну, там, вообще хотелось бы description иметь прямо в схеме (в JSON-схеме опять же есть), но если уж с ним напряг, то как насчет практической стороны — хотя бы банально смотреть диффы при обновлениях? Смотрите сами на реальных примерах:


    -channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;
    +channelFull#1c87a71a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;


    или


    -message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;
    +message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;


    У кого как, но GitHub, например, изменения внутри таких длинных строк подсвечивать отказывается. Игра "найди 10 отличий", причем что мозг сразу видит, это что начала и концы в обоих примерах одинаковы, нужно нудно вчитываться где-то в середине… На мой взгляд, вот это вот не то что в теории, а чисто визуально выглядит грязно и неряшливо.


    Кстати, о чистоте теории. А зачем нужны битовые поля? Не кажется ли, что они пахнут нехорошо с точки зрения теории типов? Объяснение можно увидеть в ранних версиях схемы. Сначала да, так и было, на каждый чих создавался новый тип. Эти рудименты и сейчас есть вот в таком виде например:


    storage.fileUnknown#aa963b05 = storage.FileType;
    storage.filePartial#40bc6f52 = storage.FileType;
    storage.fileJpeg#7efe0e = storage.FileType;
    storage.fileGif#cae1aadf = storage.FileType;
    storage.filePng#a4f63c0 = storage.FileType;
    storage.filePdf#ae1e508d = storage.FileType;
    storage.fileMp3#528a0677 = storage.FileType;
    storage.fileMov#4b09ebbc = storage.FileType;
    storage.fileMp4#b3cea0e4 = storage.FileType;
    storage.fileWebp#1081464c = storage.FileType;

    Но теперь представьте, если у Вас в структуре 5 опциональных полей, то Вам понадобится 32 типа для всех возможных вариантов. Комбинаторный взрыв. Так хрустальная чистота теории TL в очередной раз разбилась о чугунную задницу суровой реальности сериализации.


    Кроме того, местами эти ребята сами нарушают свою же типизацию. Например, в MTProto (следующая глава) ответ может быть пожат Gzip, всё здраво — кроме того, что нарушение слоев и схемы. Раз, и пожали не сам RpcResult, а его содержимое. Ну вот зачем так делать?.. Пришлось впиливать костыль, чтобы сжатие работало где угодно.


    Или еще пример, мы однажды обнаружили ошибку — посылался InputPeerUser вместо InputUser. Или наоборот. Но это работало! То есть серверу было пофиг на тип. Как такое может быть? Ответ, возможно, подскажут нам фрагменты кода из telegram-cli:


      if (tgl_get_peer_type (E->id) != TGL_PEER_CHANNEL || (C && (C->flags & TGLCHF_MEGAGROUP))) {
        out_int (CODE_messages_get_history);
        out_peer_id (TLS, E->id);
      } else {    
        out_int (CODE_channels_get_important_history);
    
        out_int (CODE_input_channel);
        out_int (tgl_get_peer_id (E->id));
        out_long (E->id.access_hash);
      }
      out_int (E->max_id);
      out_int (E->offset);
      out_int (E->limit);
      out_int (0);
      out_int (0);

    Иными словами, здесь сериализация делается ВРУЧНУЮ, а не сгенерированным кодом! Может быть, сервер реализован аналогично?.. В принципе, такое сгодится, если сделать один раз, но как это потом поддерживать при обновлениях? Уж не за этим ли схема была придумана? И тут мы переходим к следующему вопросу.


    Версионность. Слои (layers)


    Почему версии схемы названы слоями, можно делать только предположения, исходя из истории опубликованных схем. По всей видимости, поначалу авторам показалось, что базовые вещи можно делать на неизмененной схеме, и только там, где надо, на конкретные запросы указывать, что они делаются по другой версии. В принципе, даже неплохая идея — и новое будет как бы "подмешиваться", наслаиваться на старое. Но посмотрим, как это было сделано. Правда, посмотреть с самого начала не удалось — забавно, но схемы базового слоя просто не существует. Слои начались с 2. Документация рассказывает нам о специальной фиче TL:


    If a client supports Layer 2, then the following constructor must be used:
    invokeWithLayer2#289dd1f6 {X:Type} query:!X = X;


    In practice, this means that before every API call, an int with the value 0x289dd1f6 must be added before the method number.

    Звучит нормально. Но что было дальше? Дальше появился


    invokeWithLayer3#b7475268 query:!X = X;

    А дальше? Как нетрудно догадаться,


    invokeWithLayer4#dea0d430 query:!X = X;

    Смешно? Нет, еще рано смеяться, подумайте над тем, что каждый запрос с другого слоя нужно оборачивать в такой специальный тип — если они у Вас все разные, как их иначе различать-то? И добавление всего лишь 4 байт перед — довольно эффективный метод. So,


    invokeWithLayer5#417a57ae query:!X = X;

    Но очевидно, что через некоторое время это станет некоторой вакханалией. И пришло решение:


    Update: Starting with Layer 9, helper methods invokeWithLayerN can be used only together with initConnection

    Ура! Через 9 версий мы пришли, наконец, к тому, что в Internet-протоколах делалось еще в 80-е — согласованию версии один раз в начале соединения!


    А дальше?..


    invokeWithLayer10#39620c41 query:!X = X;
    ...
    invokeWithLayer18#1c900537 query:!X = X;

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


    Точно?..


    Vasily, [16.07.18 14:01]
    Ещё в пятницу подумалось:
    События телесервер посылает без запроса. Запросы нужно заворачивать в InvokeWithLayer. Апдейты сервер не заворачивает, нет структуры для оборачивания ответов и апдейтов.

    Т.е. клиент не может указать слой, в котором он хочет апдейты

    Vadim Goncharov, [16.07.18 14:02]
    а InvokeWithLayer разве не костыль в принципе?

    Vasily, [16.07.18 14:02]
    Это единственный способ

    Vadim Goncharov, [16.07.18 14:02]
    который по сути должен значить согласование лэйера в начале сессии

    кстати, из этого следует, что даунгрейд клиента не предусмотрен

    Апдейты, т.е. тип Updates в схеме — это то, что сервер присылает клиенту не в ответ на API-запрос, а самостоятельно по возникновению события. Это сложная тема, которая будет рассмотрена в другом посте, сейчас же важно знать, что сервер копит Updates и во время оффлайна клиента.


    Таким образом, при отказе от оборачивания каждого пакета в указание ему версии, отсюда логически возникают следующие возможные проблемы:


    • сервер посылает клиенту апдейты еще до того, как тот сообщил, какая им поддерживается версия
    • что надо делать после апгрейда клиента?
    • кто гарантирует, что мнение сервера о номере слоя не поменяется в процессе?

    Думаете, это сугубо теоретические умствования, и на практике такого не может возникнуть, ведь сервер написан корректно (во всяком случае, тестируется хорошо)? Ха! Как бы не так!


    Именно на это мы в августе и напоролись. 14 августа мелькали сообщения, что на серверах Telegram что-то обновляеют… а дальше в логах:


    2019-08-15 09:28:35.880640 MSK warn  main: ANON:87: unknown object type: 0x80d182d1 at TL/Object.pm line 213.
    2019-08-15 09:28:35.751899 MSK warn  main: ANON:87: unknown object type: 0xb5223b0f at TL/Object.pm line 213.

    и далее несколько мегабайт стэктрейсов (ну, заодно и логирование пофиксили). Ведь если у Вас в TL что-то не распозналось — он же бинарный по сигнатурам, дальше в потоке ВСЁ поедет, декодирование станет невозможным. Что вообще в такой ситуации делать?


    Ну, первое что любому в голову приходит — отсоединиться и попробовать заново. Не помогло. Гуглим по CRC32 — это оказались объекты с 73 схемы, хотя мы работали на 82. Внимательно смотрим в логи — там идентификаторы с двух разных схем!


    Может, проблема сугубо в нашем неофициальном клиенте? Нет, запускаем Telegram Desktop 1.2.17 (версия, поставляемая в ряде дистрибутивов Linux), он пишет в лог Exception: MTP Unexpected type id #b5223b0f read in MTPMessageMedia...



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


    Так что же делать-то? Мы с Василием разделились: он попробовал обновить схему до 91, я решил подождать несколько дней и попробовать на 73. Оба способа сработали, но поскольку они эмпирические, нет никакого понимания, ни на сколько версий вверх или вниз надо прыгать, ни сколько времени надо ждать.


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


    Объяснение? Как можно догадаться по различным косвенным симптомам, сервер состоит из многих процессов разных типов на различных машинах. Скорее всего, тот из серверов, что отвечает за "буферизацию", положил в очередь то, что ему отдавали вышестоящие, а они отдавали в той схеме, которая была на момент генерации. И пока эта очередь не "протухла", ничего с этим сделать было нельзя.


    Разве что… но ведь это жуткий костыль?!.. Нет, прежде чем думать о безумных идеях, давайте посмотрим в код официальных клиентов. В версии для Android мы не находим никакого TL-парсера, но находим здоровенный файл (гитхаб отказывается его подкрашивать) с (де)сериализацией. Вот фрагменты кода:


    public static class TL_message_layer68 extends TL_message {
        public static int constructor = 0xc09be45f;
    //...
    //еще пачка подобных
    //...
        public static class TL_message_layer47 extends TL_message {
            public static int constructor = 0xc992e15c;
            public static Message TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
                Message result = null;
                switch (constructor) {
                    case 0x1d86f70e:
                        result = new TL_messageService_old2();
                        break;
                    case 0xa7ab1991:
                        result = new TL_message_old3();
                        break;
                    case 0xc3060325:
                        result = new TL_message_old4();
                        break;
                    case 0x555555fa:
                        result = new TL_message_secret();
                        break;
                    case 0x555555f9:
                        result = new TL_message_secret_layer72();
                        break;
                    case 0x90dddc11:
                        result = new TL_message_layer72();
                        break;
                    case 0xc09be45f:
                        result = new TL_message_layer68();
                        break;
                    case 0xc992e15c:
                        result = new TL_message_layer47();
                        break;
                    case 0x5ba66c13:
                        result = new TL_message_old7();
                        break;
                    case 0xc06b9607:
                        result = new TL_messageService_layer48();
                        break;
                    case 0x83e5de54:
                        result = new TL_messageEmpty();
                        break;
                    case 0x2bebfa86:
                        result = new TL_message_old6();
                        break;
                    case 0x44f9b43d:
                        result = new TL_message_layer104();
                        break;
                    case 0x1c9b1027:
                        result = new TL_message_layer104_2();
                        break;
                    case 0xa367e716:
                        result = new TL_messageForwarded_old2(); //custom
                        break;
                    case 0x5f46804:
                        result = new TL_messageForwarded_old(); //custom
                        break;
                    case 0x567699b3:
                        result = new TL_message_old2(); //custom
                        break;
                    case 0x9f8d60bb:
                        result = new TL_messageService_old(); //custom
                        break;
                    case 0x22eb6aba:
                        result = new TL_message_old(); //custom
                        break;
                    case 0x555555F8:
                        result = new TL_message_secret_old(); //custom
                        break;
                    case 0x9789dac4:
                        result = new TL_message_layer104_3();
                        break;

    или


        boolean fixCaption = !TextUtils.isEmpty(message) &&
        (media instanceof TLRPC.TL_messageMediaPhoto_old ||
         media instanceof TLRPC.TL_messageMediaPhoto_layer68 ||
         media instanceof TLRPC.TL_messageMediaPhoto_layer74 ||
         media instanceof TLRPC.TL_messageMediaDocument_old ||
         media instanceof TLRPC.TL_messageMediaDocument_layer68 ||
         media instanceof TLRPC.TL_messageMediaDocument_layer74)
        && message.startsWith("-1");

    Кхм… выглядит дико. Но, наверное, это сгенерированный код, тогда ладно?.. Зато уж точно все версии поддерживает! Правда, непонятно, почему всё намешано в одну кучу, и секретные чаты, и всякие _old7 как-то не похожи на машинную генерацию… Впрочем, больше всего я офигел от


    TL_message_layer104
    TL_message_layer104_2
    TL_message_layer104_3

    Ребята, вы там что, даже внутри одного слоя определиться не можете?! Ну, ладно, "два", допустим, релизнулись с ошибкой, ну бывает, но ТРИ?.. Сходу же еще раз на те же грабли? Что это за порнография, пардон?..


    В исходниках Telegram Desktop, кстати, случается аналогичное — раз так, и несколько коммитов подряд в схему не меняют её номера слоя, а что-то фиксят. В условиях, когда официального источника данных по схеме нет, откуда её брать, кроме исходников официального клиента? А возьмешь оттуда, не можешь быть уверен, что схема целиком правильная, пока не протестируешь все методы.


    А как такое вообще можно тестировать? Надеюсь, любители юнит-, функциональных и прочих тестов поделятся в комментариях.


    Ладно, рассмотрим еще фрагмент кода:


    public static class TL_folders_deleteFolder extends TLObject {
        public static int constructor = 0x1c295881;
    
        public int folder_id;
    
        public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) {
            return Updates.TLdeserialize(stream, constructor, exception);
        }
    
        public void serializeToStream(AbstractSerializedData stream) {
            stream.writeInt32(constructor);
            stream.writeInt32(folder_id);
        }
    }
    
    //manually created
    
    //RichText start
    public static abstract class RichText extends TLObject {
        public String url;
        public long webpage_id;
        public String email;
        public ArrayList<RichText> texts = new ArrayList<>();
        public RichText parentRichText;
    
        public static RichText TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
            RichText result = null;
            switch (constructor) {
                case 0x1ccb966a:
                    result = new TL_textPhone();
                    break;
                case 0xc7fb5e01:
                    result = new TL_textSuperscript();
                    break;

    Вот этот комментарий "manually created" наводит на мысль, что лишь часть этого файла написана вручную (представляете весь кошмар в части maintenance?), а остальное таки сгенерировано машиной. Однако, тогда возникает другой вопрос — о том, что исходники доступны не полностью (а-ля блобы под GPL в ядре Linux), однако это уже тема для второй части.


    Но довольно. Перейдём к протоколу, поверх которого вся эта сериализация гоняется.


    MTProto


    Итак, открываем общее описание и детальное описание протокола и первым делом спотыкаемся о терминологию. И с обилием всего. Вообще, это похоже фирменная фишка Telegram — называть вещи в разных местах по-разному, либо разные вещи одним словом, либо наоборот (например, в высокоуровневом API если увидите sticker pack — это не то, что Вы подумали).


    Например, "сообщение" (message) и "сессия" (session) — здесь значат другое, чем в привычном интерфейсе Telegram-клиента. Ну, с сообщением всё понятно, его можно было бы трактовать в терминах ООП, или же просто называть словом "пакет" — это низкий, транспортный уровень, здесь не те сообщения, что в интерфейсе, много служебных. А вот сессия… но обо всём по порядку.


    Транспортный уровень


    Первым делом — транспорт. Нам расскажут аж про 5 вариантов:


    • TCP
    • Websocket
    • Websocket over HTTPS
    • HTTP
    • HTTPS

    Vasily, [15.06.18 15:04]
    А ещё есть UDP транспорт, но он не документирован

    А TCP в трёх вариантах

    Первый похож на UDP поверх TCP, каждый пакет включает в себя sequence number и crc
    Почему читать доки на тележку так больно?

    Ну, сейчас там TCP уже в 4 вариантах:


    • Abridged
    • Intermediate
    • Padded intermediate
    • Full

    Ну хорошо, Padded intermediate для MTProxy, это позже добавили из-за известных событий. А вот зачем еще две версии (итого три), когда можно было бы обойтись одной? Все четыре по сути отличаются лишь тем, каким образом задать длину и payload собственно того основного MTProto, о котором речь пойдёт дальше:


    • в Abridged это 1 или 4 байта, но не 0xef, затем тело
    • в Intermediate это 4 байта длины и поле, причем первый раз клиент должен послать 0xeeeeeeee для указания, что это Intermediate
    • в Full самая наркомания, с точки зрения сетевика: длина, sequence number, причем НЕ ТОТ, что в основном MTProto, тело, CRC32. Да, всё это поверх TCP. Который предоставляет нам надежный транспорт в виде последовательного потока байт, никакие последовательности не нужны, тем более контрольные суммы. Окей, мне сейчас возразят, что в TCP 16-битная контрольная сумма, так что искажение данных случается. Отлично, только у нас вообще-то криптографический протокол с хэшами длиннее 16 байт, все эти ошибки — и дажее более — будут отловлены на несовпадении SHA уровнем выше. Никакого смысла в CRC32 поверх этого — НЕТ.

    Сравним Abridged, в котором возможен один байт длины, с Intermediate, который обосновывается "In case 4-byte data alignment is needed", что довольно-таки чепуха. Что, считается, что программисты Telegram настолько неумехи, что не могут прочитать данные из сокета в выровненный буфер? Это всё равно придется делать, потому что чтение может вернуть Вам какое попало количество байт (а еще бывают например прокси-сервера...). Или с другой стороны, зачем городить Abridged, если сверху у нас всё равно будут здоровенные padding'и от 16 байт — сэкономить 3 байта иногда ?


    Складывается впечатление, что Николай Дуров очень любит изобретать велосипеды, в том числе сетевые протоколы, без реальной практической надобности.


    Остальные варианты транспорта, в т.ч. Web и MTProxy, мы сейчас рассматривать не будем, может быть, в другом посте, если будет запрос. Про этот самый MTProxy вспомним сейчас лишь, что вскоре после его выпуска в 2018, провайдеры быстренько научились блокировать именно его, предназначенного для обхода блокировок, по размеру пакета! А также тот факт, что написанный (опять же Вальтманом) сервер MTProxy на Си был излишне завязан на линуксовую специфику, хотя это совсем не требовалось (Фил Кулин подтвердит), и что аналогичный сервер то ли на Go, то ли на Node.js уместился менее чем в сотню строк.


    Но делать выводы о технической грамотности этих людей делать будем в конце раздела, после рассмотрения других вопросов. Пока перейдём к 5-му уровню OSI, сессионному — на который они поместили MTProto session.


    Ключи, сообщения, сессии, Diffie-Hellman


    Поместили они его туда не совсем корректно… Сессия — это не та сессия, что видна в интерфейсе под Active sessions. Но по порядку.



    Вот мы получили с транспортного уровня строку байт известной длины. Это либо шифрованное сообщение, либо plaintext — если мы еще на стадии согласования ключа и собственно им и занимаемся. О каком из кучи понятий под названием "ключ" идет речь? Проясним этот вопрос за саму команду Telegram (приношу извинения за перевод с английского собственной документации к либе усталым мозгом в 4 утра, некоторые фразы было проще оставить как есть):


    Есть две сущности под названием session — одна в UI официальных клиентов под "current sessions", где каждой сессии соответствует целое устройство / OS.
    Вторая — MTProto session, у которой есть sequence number сообщения (в низкоуровневом смысле) в ней, и которая может длиться между разными TCP-соединениями. Одновременно могут быть установлены несколько MTProto-сессий, например для ускорения закачки файлов.

    Между этими двумя sessions находится понятие authorization. В вырожденном случае, можно сказать, что UI-сессия есть то же, что authorization, но увы, всё сложно. Смотрим:
    • Пользователь на новом устройстве сначала генерирует auth_key и bounds it to account, например по SMS — потому и authorization
    • Произошло это внутри первой MTProto session, которая имеет session_id внутри себя.
    • На данном шаге, комбинация authorization и session_id могла быть названа instance — это слово встречается в документации и коде некоторых клиентов
    • Затем, клиент может открыть несколько MTProto sessions под одним и тем же auth_key — к одному и тому же DC.
    • Затем, однажды клиенту понадобится запросить файл у другого DC — и для этого DC будет сгенерирован новый auth_key !
    • Чтобы сообщить системе, что это не новый юзер регистрируется, а та же самая authorization (UI-сессия), клиент использует вызовы API auth.exportAuthorization в домашнем DC auth.importAuthorization в новом DC.
    • Всё так же, может быть открыто несколько MTProto sessions (каждая с собственным session_id) к этому новому DC, под его auth_key.
    • Наконец, клиент может захоть Perfect Forward Secrecy. Каждый auth_key был permanent key — per DC — и клиент может вызвать auth.bindTempAuthKey для использования temporary auth_key — и снова, только один temp_auth_key per DC, общий для всех MTProto sessions к этому DC.


    Заметим, что salt (и future salts) тоже одна на auth_key т.е. shared между всеми MTProto sessions к одному и тому же DC.

    Что значит "между разными TCP-соединениями"? Значит, что это нечто вроде авторизации кукой на веб-сайте — она сохраняется (переживает) много TCP-соединений к данному серверу, но однажды протухнет. Только в отличие от HTTP, в MTProto внутри сессии сообщения последовательно нумеруются и подтверждаются, въехали в туннель, разорвалось соединение — после установления нового соединения сервер любезно отправит всё то в этой сессии, что не доставил в прошлом TCP-соединении.


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


    Так что, генерируем auth_key по версии Диффи-Хеллмана от Telegram. Попытаемся понять документацию...


    Vasily, [19.06.18 20:05]
    data_with_hash := SHA1(data) + data + (any random bytes); such that the length equal 255 bytes;
    encrypted_data := RSA (data_with_hash, server_public_key); a 255-byte long number (big endian) is raised to the requisite power over the requisite modulus, and the result is stored as a 256-byte number.

    У них какой-то наркоманский DH

    Не похоже на DH здорового человека
    В дх нет двух публичных ключей

    Ну с этим в итоге разобрались но осадочек остался — делается proof of work клиентом, что он смог факторизовать число. Типа защита от DoS-атак. И RSA-ключ используется только один раз в одном направлении, по сути, для шифрования new_nonce. Но пока эта вроде бы простая операция получится, с чем придется столкнуться?


    Vasily, [20.06.18 00:26]
    Я ещё не дошёл до запроса appid

    Это я запрос на DH отправил

    А, в доке на транспорт написано, что может ответить 4 байтами кода ошибки. И всё

    Ну вот сказал он мне -404, и что?

    Вот я ему: "лови свою ефигню шифрованную ключом сервера с отпечатком таким-то, хочу DH", а оно в ответ тупо 404

    Что бы Вы подумали на такой ответ сервера? Что делать? Спросить-то не у кого (но об этом во второй части).


    Тут весь интерес по доке сделать

    Мне вот больше заняться нечем, только и мечтал числа туда-сюда конвертить

    Два 32 битных числа. Я их и упаковал как все остальные

    Но нет, именно эти два нужно сначала в строку как BE

    Vadim Goncharov, [20.06.18 15:49]
    и из-за этого 404?

    Vasily, [20.06.18 15:49]
    ДА!

    Vadim Goncharov, [20.06.18 15:50]
    вот я и не понимаю, что он может "не нашла"

    Vasily, [20.06.18 15:50]
    примерно

    Не нашла такого разложения на простые делители %)

    Даже error reporting не осилили

    Vasily, [20.06.18 20:18]
    О, там ещё и MD5. Уже три разных хэша

    The key fingerprint is computed as follows:
    digest = md5(key + iv)
    fingerprint = substr(digest, 0, 4) XOR substr(digest, 4, 4)


    SHA1 и sha2

    Итак, положим, auth_key размером 2048 бит мы по Диффи-Хеллману получили. Что дальше? Дальше мы обнаруживаем, что младшие 1024 бита этого ключа никак не используются… но подумаем пока вот о чем. На данном шаге у нас есть с сервером общий секрет. Установлен аналог TLS-сессии, весьма затратной процедурой. Но сервер еще ничего не знает о том, кто мы такие! Еще нет, собственно, авторизации. Т.е. если Вы мыслили в понятиях "логин-пароль", как когда-то в ICQ, или хотя бы "логин-ключ", как в SSH (например на какой-нибудь gitlab/github). Мы получили анонимуса. А если сервер ответит нам "данные телефонные номера обслуживаются другим DC"? Или вообще "ваш телефонный номер забанен"? Лучшее, что мы можем сделать — это сохранить ключ в надежде, что еще пригодится и не протухнет к тому моменту.


    Кстати, "получили" мы его с оговорками. Вот например, мы доверяем серверу? Вдруг он поддельный? Нужны бы криптографические проверки:


    Vasily, [21.06.18 17:53]
    Они предлагают мобильным клиентам проверять 2кбитное число на простоту %)

    Но вообще непонятно, нафейхоа

    Vasily, [21.06.18 18:02]
    В доке не сказано, что делать, если оно не простое оказалось

    Не сказано. Давайте посмотрим, что в этом случае делает официальный клиент под Андроид? А вот что (и да, там весь файл интересный) — как говорится, я просто оставлю это здесь:


    278     static const char *goodPrime = "c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b";
    279   if (!strcasecmp(prime, goodPrime)) {

    Не, там конечно еще какие-то проверки простоты числа есть, но лично я достаточными познаниями в математике уже не обладаю.


    Ладно, мы получили основной ключ. Чтобы авторизоваться, т.е. послать запросы, надо производить дальнейшее шифрование, уже с помощью AES.


    The message key is defined as the 128 middle bits of the SHA256 of the message body (including session, message ID, etc.), including the padding bytes, prepended by 32 bytes taken from the authorization key.

    Vasily, [22.06.18 14:08]
    Средние, сука, биты

    Получил auth_key. Всё. Дальше них… не понятно из доки. Feel free to study the open source code.

    Note that MTProto 2.0 requires from 12 to 1024 bytes of padding, still subject to the condition that the resulting message length be divisible by 16 bytes.

    Так сколько паддинга сыпать?

    И да, тут тоже 404 в случае ошибки

    Если кто внимательно изучил схему и текст документации, обратил внимание, что никакого MAC там нет. И что AES используется в некоем, более нигде не применяющемся режиме IGE. Они, конечно, пишут об этом в своем FAQ… Здесь, типа, сам ключ сообщения заодно и является SHA-хэшом расшифрованных данных, используемым для проверки целостности — причем в случае несовпадения документация почему-то рекомендует silently ignore их (а как же безопасность, вдруг нас ломают?).


    Я не криптограф, может быть, в этом режиме в данном случае и нет ничего плохого с теоретической точки зрения. Но я могу совершенно точно назвать практическую проблему, на примере Telegram Desktop. Он локальный кэш (все вот эти D877F783D5D3EF8C) шифрует тем же способом, что сообщения в MTProto (только в данном случае версии 1.0), т.е. сначала ключ сообщения, потом сами данные (и где-то в стороне основной большой auth_key на 256 байт, без которого msg_key бесполезен). Так вот, проблема становится заметна на больших файлах. А именно, Вам надо держать две копии данных — шифрованную и расшифрованную. А если там мегабайты, или потоковое видео, например?.. Классические схемы с MAC после шифротекста позволяют Вам считать его потоково, сразу передавая. А с MTProto придется сначала зашифровать или расшифровать сообщение целиком, только потом передавать в сеть или на диск. Поэтому в свежих версиях Telegram Desktop в кэше в user_data применяется уже и другой формат — с AES в режиме CTR.


    Vasily, [21.06.18 01:27]
    О, я узнал, что такое IGE: IGE was the first attempt at an "authenticating encryption mode," originally for Kerberos. It was a failed attempt (it does not provide integrity protection), and had to be removed. That was the beginning of a 20 year quest for an authenticating encryption mode that works, which recently culminated in modes like OCB and GCM.

    А теперь аргументы со стороны телеги:

    The team behind Telegram, led by Nikolai Durov, consists of six ACM champions, half of them Ph.Ds in math. It took them about two years to roll out the current version of MTProto.

    Чот смешно. Два года на нижний уровень

    А могли бы просто взять tls

    Ладно, допустиим, шифрование и прочие нюансы мы сделали. Можно, наконец, посылать сериализованные в TL запросы и десериализовывать ответы? Так а что и как слать надо? Вот, допустим, метод initConnection, наверное это оно?


    Vasily, [25.06.18 18:46]
    Initializes connection and save information on the user's device and application.

    Оно принимает app_id, device_model, system_version, app_version и lang_code.

    И некий query

    Документация как всегда. Feel free to study the open source

    Если с invokeWithLayer всё было примерно понятно, то здесь-то что? Оказывается, предположим у нас — клиент уже имел нечто, о чем спросить сервер — имеется запрос, который мы хотели послать:


    Vasily, [25.06.18 19:13]
    Судя по коду, первый вызов заворачивается в эту дрисню, а сама дрисня в invokewithlayer

    Почему initConnection не мог быть отдельным вызовом, а обязательно должен быть оберткой? Да, как оказалось, его надо обязательно каждый раз в начале каждой сессии делать, а не разово, как с основным ключом. Но! Его не может вызвать неавторизованный пользователь! Вот мы добрались до этапа, в котором применима вот эта страница документации — и она сообщает нам, что...


    Only a small portion of the API methods are available to unauthorized users:

    • auth.sendCode
    • auth.resendCode
    • account.getPassword
    • auth.checkPassword
    • auth.checkPhone
    • auth.signUp
    • auth.signIn
    • auth.importAuthorization
    • help.getConfig
    • help.getNearestDc
    • help.getAppUpdate
    • help.getCdnConfig
    • langpack.getLangPack
    • langpack.getStrings
    • langpack.getDifference
    • langpack.getLanguages
    • langpack.getLanguage

    Самый первый из них, auth.sendCode, и есть тот заветный первый запрос, в котором мы отправим api_id и api_hash, и после которого нам приходит SMS с кодом. А если мы попали не в тот DC (телефонные номера этой страны обслуживает другой, например), то нам придёт ошибка с номером нужного DC. Чтобы узнать, на какой IP-адрес по номеру DC надо соединяться, нам поможет help.getConfig. Когда-то там было всего 5 записей, но после известных событий 2018 года число значительно возросло.


    Теперь вспомним то, что мы попали на этом этапе на сервере анонимусом. Не слишком ли затратно для того, чтобы просто получить IP-адрес? Почему было бы не делать это, и другие операции, в нешифрованной части MTProto? Слышу возражение: "а как удостовериться, что это не РКН фальшивыми адресами ответит?". На это мы вспомним, что вообще-то в официальные клиенты вшиты RSA-ключи, т.е. можно просто подписать эту информацию. Собственно, так уже и делается для информации по обходам блокировок, которую клиенты получают по другим каналам (логично, что это нельзя сделать в самом MTProto, еще ведь надо знать, куда соединиться).


    Ну, ладно. На этом этапе авторизации клиента мы еще не авторизованы и не регистрировали своё приложение. Мы хотим просто пока посмотреть, что отвечает сервер на методы, доступные неавторизованному пользователю. И тут...


    Vasily, [10.07.18 14:45]
    https://core.telegram.org/method/help.getConfig
    config#7dae33e0 [...] = Config;
    help.getConfig#c4f9186b = Config;


    https://core.telegram.org/api/datacenter
    config#232d5905 [...] = Config;
    help.getConfig#c4f9186b = Config;


    В схеме первое, приходит второе

    В схеме tdesktop третье значение

    Да, с тех пор, конечно, документацию обновили. Хотя скоро она снова может стать неактуальной. А откуда должен знать начинающий разработчик? Может быть, если зарегистрировать своё приложение, то сообщат? Василий сделал это, но увы — ничего ему не прислали (снова, поговорим об этом во второй части).


    … Вы заметили, что мы уже как-то перешли к API, т.е. к следующему уровню, и что-то пропустили в теме MTProto? Ничего удивительного:


    Vasily, [28.06.18 02:04]
    Мм, они шарят часть алгоритмов на e2e

    Mtproto определяет алгоритмы и ключи шифрования для обоих доменов, а также немного структуру обёртки

    Но они постоянно смешивают разные уровни стека, так что не всегда понятно, где закончился mtproto и начался следующий уровень

    Как смешивают? Ну вот тот же временный ключ для PFS, например (кстати, Telegram Desktop его не умеет). Он выполняется запросом API auth.bindTempAuthKey, т.е. с верхнего уровня. Но при этом вторгается в шифрование на нижнем уровне — после него, например, надо заново делать initConnection и т.п., это не просто обычный запрос. Отдельно доставляет еще и то, что можно иметь только ОДИН временный ключ на DC, хотя поле auth_key_id в каждом сообщении позволяет менять ключ хоть каждое сообщение, и что сервер имеет право в любой момент "забыть" временный ключ — что в этом случае делать, документация не говорит… ну почему нельзя было бы иметь несколько ключей, как с набором future salts, а?..


    Стоит отметить в теме MTProto еще некоторые вещи.


    Сообщения о сообщениях, msg_id, msg_seqno, подтверждения, пинги не в ту сторону и другие идиосинкразии


    Почему о них нужно знать? Потому что они "протекают" на уровень выше, и о них нужно знать, работая с API. Положим, msg_key нас не интересует, нижний уровень расшифровал всё для нас. Но внутри расшифрованных данных у нас такие поля (еще длина данных, чтоб знать, где padding, но это не важно):


    • salt — int64
    • session_id — int64
    • message_id — int64
    • seq_no — int32

    Напомним, соль — одна на весь DC. Зачем о ней знать? Не только потому, что есть запрос get_future_salts, который сообщает, в какие интервалы какие будут валидны, но и потому, что если Ваша соль "протухла", то сообщение (запрос) — просто потеряется. Сервер, конечно, сообщит новую соль, выдав new_session_created — но со старым придется как-то делать перепосылку, например. И этот вопрос влияет на архитектуру приложения.


    Серверу разрешено вообще дропать сессии и отвечать таким образом по многим поводам. Собственно, что такое сессия MTProto со стороны клиента? Это два числа, session_id и seq_no сообщения внутри этой сессии. Ну, и нижележащее TCP-соединение, конечно. Допустим, наш клиент еще много чего не умеет, отсоединился, переподсоединился. Если это произошло быстро — в новом TCP-соединении продолжилась старая сессия, увеличиваем seq_no дальше. Если долго — сервер мог её удалить, потому что на его стороне это еще и очередь, как мы выяснили.


    Каков должен быть seq_no? О, это хитрый вопрос. Попробуйте честно понять, что имелось в виду:


    Content-related Message

    A message requiring an explicit acknowledgment. These include all the user and many service messages, virtually all with the exception of containers and acknowledgments.

    Message Sequence Number (msg_seqno)

    A 32-bit number equal to twice the number of “content-related” messages (those requiring acknowledgment, and in particular those that are not containers) created by the sender prior to this message and subsequently incremented by one if the current message is a content-related message. A container is always generated after its entire contents; therefore, its sequence number is greater than or equal to the sequence numbers of the messages contained in it.

    Что это за цирк с инкрементом на 1, а потом еще на 2?.. Подозреваю, изначально имелось в виду "младший бит для ACK, остальное номер", но в результате получается не совсем так — в частности, выходит, может быть послано несколько подтверждений, имеющих один и тот же seq_no! Как? Ну например сервер нам что-то шлет, шлёт, а мы сами молчим, только отвечаем сервисными сообщениями подтверждений о получении его сообщений. В этом случае наши исходящие подтверждения будут иметь один и тот же исходящий номер. Если Вы знакомы с TCP и подумали, что это звучит как-то дико, но вроде бы не очень и дико, ведь в TCP seq_no не меняется, а подтверждение идёт на seq_no той стороны — то поспешу огорчить. В MTProto подтверждения идут НЕ по seq_no, как в TCP, а по msg_id !


    Что же это за msg_id, самое важное из этих полей? Уникальный идентификатор сообщения, как явствует из названия. Определён он как 64-битное число, самые младшие биты которого опять имеют магию "сервер-не сервер", а остальное — Unix timestamp, включая дробную часть, сдвинутый на 32 бита влево. Т.е. метка времени по сути (и сообщения со слишком отличающимся временем будут отвергнуты сервером). Из этого выходит, что в общем-то это идентификатор, глобальный для клиента. При том, что — вспомним session_id — нам гарантируется: Under no circumstances can a message meant for one session be sent into a different session. То есть, получается, что есть аж три уровня — сессия, номер в сессии, id сообщения. Зачем такое переусложнение, сия тайна есть велика весьма.


    Итак, msg_id нужен для...


    RPC: запросы, ответы, ошибки. Подтверждения.


    Как Вы, может быть, заметили, нигде в схеме нет специального типа или функции "сделать RPC-запрос", хотя есть ответы. Ведь у нас же есть content-related сообщения! То есть, любое сообщение может быть запросом! Или не быть. Ведь у каждого есть msg_id. А вот ответы — есть:


    rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult;

    Вот здесь и указывается, на какое сообщение это ответ. Поэтому Вам, на верхнем уровне API, придется помнить, какой номер был у Вашего запроса — думаю, не надо пояснять, что работа асинхронная, и одновременно в работе может быть несколько запросов, ответы на которые могут вернуться в любом порядке? В принципе, из этого, и сообщений об ошибках типа no workers, прослеживается стоящая за этим архитектура: поддерживающий с Вами TCP-соединение сервер — фронтенд-балансировщик, он направляет запросы на бэкенды и собирает их обратно по message_id. Вроде тут всё понятно, логично и хорошо.


    Да?.. А если задуматься? Ведь у самого RPC-ответа тоже есть поле msg_id! Надо ли нам орать серверу "вы не отвечаете на мой ответ!"? И да, что там было про подтверждения? Страница про сообщения про сообщения говорит нам, что есть


    msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

    и его должна делать каждая сторона. Но не всегда! Если Вы получили RpcResult, он сам служит подтверждением. То есть, на Ваш запрос сервер может ответить MsgsAck — типа, "я получил". Может сразу ответить RpcResult. Может быть и то и другое.


    И да, Вы таки должны ответить на ответ! Подтверждением. Иначе сервер будет считать его недоставленным и вывалит Вам его опять. Даже после переподсоединения. Но тут, конечно, вопрос таймаутов возникнет. Рассмотрим их чуть позже.


    А пока рассмотрим возможные ошибки выполнения запросов.


    rpc_error#2144ca19 error_code:int error_message:string = RpcError;

    О, воскликнет кто-то, здесь более человечный формат — есть строка! Не торопитесь. Вот перечень ошибок, но, конечно, не полный. Из него мы узнаем, что код — нечто вроде HTTP-ошибки (ну разумеется, семантика ответов не соблюдается, местами они распределены по кодам как попало), а строка имеет вид типа БОЛЬШИЕ_БУКВЫ_И_ЦИФРЫ. Например, PHONE_NUMBER_OCCUPIED или FILE_PART_Х_MISSING. Ну то есть, Вам эту строку еще придется пропарсить. Например, FLOOD_WAIT_3600 будет означать, что надо ждать час, а PHONE_MIGRATE_5, что телефонному номеру с этим префиксом надо регистрироваться в 5-м DC. У нас ведь язык типов, да? Аргумент из строки нам не нужен, регулярками обойдутся, чо.


    Опять же, на странице сервисных сообщений этого нет, но, как уже привычно с этим проектом, информация может найтись на другой странице документации. Или навести на подозрение. Во-первых, смотрите, нарушение типизации/слоёв — RpcError может быть вложен в RpcResult. Почему не снаружи? Что мы не учли?.. Соответственно, где гарантия, что RpcError может быть и НЕ вложен в RpcResult, а быть напрямую или вложен в другой тип?.. А если не может, почему он не верхнего уровня, т.е. в нём отсутствует req_msg_id ?..


    Но продолжим о сервисных сообщениях. Клиент может счесть, что сервер долго думает, и сделать вот такой замечательный запрос:


    rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

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


    Наркомания: статусы сообщений о сообщениях


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


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


    bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;
    bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;

    Ну, с ними придется столкнуться каждому начинающему работать с MTProto, в цикле "поправил — перекомпилировал — запустил" получить ошибки номеров или успевшую протухнуть за время правок соль — обычное дело. Однако тут два момента:


    1. Из этого следует, что оригинальное сообщение потеряно. Нужно городить какие-то очереди, рассмотрим это позже.
    2. Что за странные номера ошибок? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64… где остальные номера, Томми?

    Документация утверждает:


    The intention is that error_code values are grouped (error_code >> 4): for example, the codes 0x40 — 0x4f correspond to errors in container decomposition.

    но, во-первых, сдвиг в другую сторону, во-вторых, всё равно, где остальные коды? В голове автора?.. Впрочем, это мелочи.


    Наркомания начинается в сообщениях о статусах сообщений и копиях сообщений:


    • Request for Message Status Information
      If either party has not received information on the status of its outgoing messages for a while, it may explicitly request it from the other party:
      msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
    • Informational Message regarding Status of Messages
      msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
      Here, info is a string that contains exactly one byte of message status for each message from the incoming msg_ids list:
      • 1 = nothing is known about the message (msg_id too low, the other party may have forgotten it)
      • 2 = message not received (msg_id falls within the range of stored identifiers; however, the other party has certainly not received a message like that)
      • 3 = message not received (msg_id too high; however, the other party has certainly not received it yet)
      • 4 = message received (note that this response is also at the same time a receipt acknowledgment)
      • +8 = message already acknowledged
      • +16 = message not requiring acknowledgment
      • +32 = RPC query contained in message being processed or processing already complete
      • +64 = content-related response to message already generated
      • +128 = other party knows for a fact that message is already received
        This response does not require an acknowledgment. It is an acknowledgment of the relevant msgs_state_req, in and of itself.
        Note that if it turns out suddenly that the other party does not have a message that looks like it has been sent to it, the message can simply be re-sent. Even if the other party should receive two copies of the message at the same time, the duplicate will be ignored. (If too much time has passed, and the original msg_id is not longer valid, the message is to be wrapped in msg_copy).
    • Voluntary Communication of Status of Messages
      Either party may voluntarily inform the other party of the status of the messages transmitted by the other party.
      msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
    • Extended Voluntary Communication of Status of One Message

      msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
      msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
    • Explicit Request to Re-Send Messages
      msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
      The remote party immediately responds by re-sending the requested messages [...]
    • Explicit Request to Re-Send Answers
      msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
      The remote party immediately responds by re-sending answers to the requested messages [...]
    • Message Copies
      In some situations, an old message with a msg_id that is no longer valid needs to be re-sent. Then, it is wrapped in a copy container:
      msg_copy#e06046b2 orig_message:Message = MessageCopy;
      Once received, the message is processed as if the wrapper were not there. However, if it is known for certain that the message orig_message.msg_id was received, then the new message is not processed (while at the same time, it and orig_message.msg_id are acknowledged). The value of orig_message.msg_id must be lower than the container’s msg_id.

    Даже помолчим о том, что в msgs_state_info опять торчат уши недоделанного TL (нужен был вектор байт, и в младших двух битах enum, а в старших флаги). Суть в другом. Кто-нибудь понимает, зачем всё это на практике в реальном клиенте нужно?.. С трудом, но можно представить себе какую-то пользу, если человек занимается отладкой, причем в интерактивном режиме — спросить у сервера, что да как. Но здесь описываются запросы в обе стороны.


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


    Пинги и тайминги. Очереди.


    Из всего, если вспомнить догадки об архитектуре сервера (распределение запросов по бэкендам), вытекает довольно унылая вещь — несмотря на все гарантии доставки что в TCP (либо данные доставлены, либо Вам сообщат о разрыве, но данные до момента проблемы будут доставлены), что подтверждения в самом MTProto — гарантий нет. Сервер может запросто прое потерять или выкинуть Ваше сообщение, и ничего с этим сделать нельзя, только городить костыли разных видов.


    И прежде всего — очереди сообщений. Ну, с одной-то всё было очевидно с самого начала — неподтвержденное сообщение надо хранить и перепосылать. А через какое время? А шут его знает. Возможно, вон те наркоманские сервисные сообщение как-то костылями решают эту проблему, скажем, в Telegram Desktop примерно штуки 4 очереди, им соответствующих (может больше, как уже говорилось, для этого надо вникать в его код и архитектуру более серьезно; при этом мы знаем, что за образец его брать нельзя, энное количество типов из схемы MTProto в нём не используется).


    Почему так происходит? Вероятно, программисты сервера не смогли обеспечить надежность внутри кластера, или хотя бы даже буферизацию на фронте-балансировщике, и переложили эту проблему на клиента. От безысходности Василий попытался реализовать альтернативный вариант, с всего двумя очередями, используя алгоритмы из TCP — замеряя RTT до сервера и корректируя размер "окна" (в сообщениях) в зависимости от числа неподтвержденных запросов. То есть, грубая такая эвристика для оценки загруженности сервера — сколько одновременно наших запросов он может жевать и не терять.


    Ну то есть, Вы понимаете, да? Если поверх работающего по TCP протокола приходится реализовывать опять TCP — это говорит об очень плохо спроектированном протоколе.


    Ах да, почему нужно более одной очереди, и вообще, что это значит для человека, работающего с высокоуровневым API? Смотрите, Вы делаете запрос, сериализуете его, но отправить его немедленно зачастую нельзя. Почему? Потому что ответ будет по msg_id, который есть временная метка, назначение которой лучше отложить на как можно более позже — вдруг сервер отвергнет из-за несовпадения времени у нас и у него (конечно, мы можем сделать костыль, сдвигающий наше время от настоящего к серверному прибавлением дельты, вычисленной из ответов сервера — официальные клиенты так и делают, но этот способ груб и неточен из-за буферизации). Поэтому, когда Вы делаете запрос локальным вызовом функции из библиотеки, сообщение проходит такие стадии:


    1. Лежит в одной очереди и ожидает шифрования.
    2. Назначен msg_id и сообщение легло в другую очередь — возможной перепосылки; отправляем в сокет.
    3. а) Сервер ответил MsgsAck — сообщение доставлено, удаляем из "другой очереди".
      б) Или наоборот, что-то ему не понравилось, он ответил badmsg — перепосылаем из "другой очереди"
      в) Ничего неизвестно, надо перепослать сообщение из другой очереди — но неизвестно точно, когда.
    4. Сервер наконец ответил RpcResult — собственно ответом (или ошибкой) — не просто доставлено, но и обработано.

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


    И в этом месте вступают в действие уже не технические соображения. По опыту, мы видели много костылей, а кроме того, сейчас увидим еще примеры плохих советов и архитектуры — в таких условиях, стоит ли доверять и принимать такие решения? Вопрос риторический (конечно, нет).


    О чем речь? Если по теме "наркоманские сообщения о сообщениях" еще можно спекулировать возражениями вида "это вы тупые, не поняли наш гениальный замысел!" (так напишите сначала документацию, как полагается у нормальных людей, с rationale и примерами обмена пакетов, тогда и поговорим), то тайминги/таймауты — вопрос сугубо практический и конкретный, тут всё давно известно. А что же нам говорит документация о таймаутах?


    A server usually acknowledges the receipt of a message from a client (normally, an RPC query) using an RPC response. If a response is a long time coming, a server may first send a receipt acknowledgment, and somewhat later, the RPC response itself.

    A client normally acknowledges the receipt of a message from a server (usually, an RPC response) by adding an acknowledgment to the next RPC query if it is not transmitted too late (if it is generated, say, 60-120 seconds following the receipt of a message from the server). However, if for a long period of time there is no reason to send messages to the server or if there is a large number of unacknowledged messages from the server (say, over 16), the client transmits a stand-alone acknowledgment.

    … Перевожу: мы сами не знаем, сколько и как надо, ну давайте прикинем, что пусть будет вот так.


    И о пингах:


    Ping Messages (PING/PONG)
    ping#7abe77ec ping_id:long = Pong;


    A response is usually returned to the same connection:
    pong#347773c5 msg_id:long ping_id:long = Pong;


    These messages do not require acknowledgments. A pong is transmitted only in response to a ping while a ping can be initiated by either side.

    Deferred Connection Closure + PING
    ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;


    Works like ping. In addition, after this is received, the server starts a timer which will close the current connection disconnect_delay seconds later unless it receives a new message of the same type which automatically resets all previous timers. If the client sends these pings once every 60 seconds, for example, it may set disconnect_delay equal to 75 seconds.

    Да вы с ума сошли?! За 60 секунд поезд въедет на станцию, высадит-возьмет пассажиров, и снова потеряет связь в туннеле. За 120 секунд, пока прочухаетесь, он приедет на другую, и соединение скорее всего порвётся. Ну, понятно откуда ноги растут — "слышал звон, да не знает где он", есть алгоритм Нагла и опция TCP_NODELAY, предназначавшаяся для интерактивной работы. Но, простите, её дефолтное значение задержи — 200 миллисекунд. Если вам так уж хочется изобразить нечто похожее и сэкономить на возможной паре пакетов — ну отложите, накрайняк, на 5 секунд, или чему там сейчас равен таймаут сообщения "User is typing...". Но не больше.


    И наконец, пинги. То бишь, проверка живости TCP-соединения. Забавно, но примерно 10 лет назад я писал критический текст о мессенджере общаги нашего факультета — там авторы тоже пинговали сервер с клиента, а не наоборот. Но одно дело студенты 3 курса, а другое — международная контора, да?..


    Сначала небольшой ликбез. TCP-соединение, при отсутствии обмена пакетами, может жить неделями. Это и хорошо, и плохо, в зависимости от цели. Хорошо, если у Вас было открыто SSH-соединение на сервер, Вы встали из-за компа, перезагрузили роутер по питанию, вернулись на место — сессия через этот сервер не порвалась (ничего не набирали, пакетов не было), удобно. Плохо, если на сервере тысячи клиентов, каждый занимает ресурсы (привет, Постгрес!), и хост клиента, возможно, давно уже перезагрузился — но мы об этом не узнаем.


    Системы чатов / IM относятся ко второму случаю по еще одной, дополнительной причине — онлайн-статусы. Если пользователь "отвалился", надо сообщить об этом его собеседникам. Иначе получится ошибка, которую допустили создатели Jabber (и 20 лет исправляли) — пользователь отсоединился, но ему продолжают писать сообщения, считая, что он online (которые еще и полностью терялись в эти несколько минут до обнаружения разрыва). Нет, опция TCP_KEEPALIVE, которую многие не понимающие, как работают таймеры TCP, суют куда попало (ставя дикие значения типа десятков секунд), здесь не поможет — Вам нужно убедиться, что живо не только ядро ОС машины пользователя, но и нормально функционирует, в состоянии ответить, и само приложение (думаете, оно не может зависнуть? Telegram Desktop на Ubuntu 18.04 у меня зависал неоднократно).


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


    А что же мы видим в Telegram? Всё ровно наоборот! Ну т.е. формально, конечно, обе стороны могут пинговать друг друга. На практике же — клиенты пользуются костылём ping_delay_disconnect, который взводит на сервере таймер. Ну простите, это не дело клиента решать, сколько он там хочет жить без пинга. Серверу, исходя из своей нагрузки, видней. Но, конечно, если ресурсов не жалко, то сами себе злобные буратины, и костыль сойдёт...


    А как надо было проектировать?


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


    Почему же оно такое сложное вышло, и чем архитекторы Telegram могут попытаться возразить? Тем, что они пытались сделать сессию, которая переживает разрывы TCP-соединений, т, е. что не доставили сейчас — доставим позже. Вероятно, еще попытались сделать UDP-транспорт, правда столкнулись со сложностями и забросили (потому и в документации пусто — нечем похвастаться было). Но из-за непонимания того, как работают сети вообще и TCP в частности, где можно на него положиться, а где нужно делать самому (и как), и попытки совместить это с криптографией "одним выстрелом двух зайцев" — получился вот такой кадавр.


    А как надо было? Исходя из того, что msg_id является меткой времени, необходимой с криптографической точки зрения для предотвращения replay-атак, ошибкой является навешивание на него функции уникального идентификатора. Поэтому, без кардинального изменения текущей архитектуры (когда формируется поток Updates, это тема высокоуровневого API для другой части этой серии постов), нужно было бы:


    1. Сервер, держащий TCP-соединение с клиентом, берет на себя ответственность — если вычитал из сокета, изволь подтвердить, обработать или вернуть ошибку, никаких потерь. Тогда подтверждением становится не вектор id'ов, а просто "последний полученный seq_no" — просто число, как в TCP (два числа — свой seq и подтвержденный). Мы ведь в рамках сессии всегда, не так ли?
    2. Метка времени для предотвращения replay-атак становится отдельным полем, а-ля nonce. Проверяется, но больше ни на что другое не влияет. Хватит и uint32 — если у нас соль меняется не реже каждых полсуток, можно отвести 16 бит на младшие биты целой части текущего времени, остальное — на дробную часть секунды (как и сейчас).
    3. Убирается msg_id вовсе — с точки зрения различения запросов на бэкендах есть, во-первых, id клиента, во-вторых, id сессии, их и конкатенируйте. Соответственно, в качестве идентификатора запроса достаточно одно лишь seq_no.

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


    API ?


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


    Внимание, сейчас будет единственный в статье пример на Perl! (для тех, кто не знаком с синтаксисом, первый аргумент bless — структура данных объекта, второй — его класс):


    2019.10.24 12:00:51 $1 = {
      'cb' => 'TeleUpd::__ANON__',
      'out' => bless( {
        'filter' => bless( {}, 'Telegram::ChannelMessagesFilterEmpty' ),
        'channel' => bless( {
          'access_hash' => '-6698103710539760874',
          'channel_id' => '1380524958'
        }, 'Telegram::InputPeerChannel' ),
        'pts' => '158503',
        'flags' => 0,
        'limit' => 0
      }, 'Telegram::Updates::GetChannelDifference' ),
      'req_id' => '6751291954012037292'
    };
    
    2019.10.24 12:00:51 $1 = {
      'in' => bless( {
        'req_msg_id' => '6751291954012037292',
        'result' => bless( {
          'pts' => 158508,
          'flags' => 3,
          'final' => 1,
          'new_messages' => [],
          'users' => [],
          'chats' => [
            bless( {
              'title' => 'Хулиномика',
              'username' => 'hoolinomics',
              'flags' => 8288,
              'id' => 1380524958,
              'access_hash' => '-6698103710539760874',
              'broadcast' => 1,
              'version' => 0,
              'photo' => bless( {
                'photo_small' => bless( {
                  'volume_id' => 246933270,
                  'file_reference' => '
                  'secret' => '1854156056801727328',
                  'local_id' => 228648,
                  'dc_id' => 2
                }, 'Telegram::FileLocation' ),
                'photo_big' => bless( {
                  'dc_id' => 2,
                  'local_id' => 228650,
                  'file_reference' => '
                  'secret' => '1275570353387113110',
                  'volume_id' => 246933270
                }, 'Telegram::FileLocation' )
              }, 'Telegram::ChatPhoto' ),
              'date' => 1531221081
            }, 'Telegram::Channel' )
          ],
          'timeout' => 300,
          'other_updates' => [
            bless( {
              'pts_count' => 0,
              'message' => bless( {
                'post' => 1,
                'id' => 852,
                'flags' => 50368,
                'views' => 8013,
                'entities' => [
                  bless( {
                    'length' => 20,
                    'offset' => 0
                  }, 'Telegram::MessageEntityBold' ),
                  bless( {
                    'length' => 18,
                    'offset' => 480,
                    'url' => 'https://alexeymarkov.livejournal.com/[url_вырезан].html'
                  }, 'Telegram::MessageEntityTextUrl' )
                ],
                'reply_markup' => bless( {
                  'rows' => [
                    bless( {
                      'buttons' => [
                        bless( {
                          'text' => '???? 165',
                          'data' => 'send_reaction_0'
                        }, 'Telegram::KeyboardButtonCallback' ),
                        bless( {
                          'data' => 'send_reaction_1',
                          'text' => '???? 9'
                        }, 'Telegram::KeyboardButtonCallback' )
                      ]
                    }, 'Telegram::KeyboardButtonRow' )
                  ]
                }, 'Telegram::ReplyInlineMarkup' ),
                'message' => 'А вот и новая книга! 
    // [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
     напечатаю.',
                'to_id' => bless( {
                  'channel_id' => 1380524958
                }, 'Telegram::PeerChannel' ),
                'date' => 1571724559,
                'edit_date' => 1571907562
              }, 'Telegram::Message' ),
              'pts' => 158508
            }, 'Telegram::UpdateEditChannelMessage' ),
            bless( {
              'pts' => 158508,
              'message' => bless( {
                'edit_date' => 1571907589,
                'to_id' => bless( {
                  'channel_id' => 1380524958
                }, 'Telegram::PeerChannel' ),
                'date' => 1571807301,
                'message' => 'Почему Вы считаете Facebook плохой компанией? Можете прокомментировать? По-моему, это шикарная компания. Без долгов, с хорошей прибылью, а если решат дивы платить, то и еще могут нехило подорожать.
    
    Для меня ответ совершенно очевиден: потому что Facebook делает ужасный по качеству продукт. Да, у него монопольное положение и да, им пользуется огромное количество людей. Но мир не стоит на месте. Когда-то владельцам Нокии было смешно от первого Айфона. Они думали, что лучше Нокии ничего быть не может и она навсегда останется самым удобным, красивым и твёрдым телефоном - и доля рынка это красноречиво демонстрировала. Теперь им не смешно.
    
    Конечно, рептилоиды сопротивляются напору молодых гениев: так Цукербергом был пожран Whatsapp, потом Instagram. Но всё им не пожрать, Паша Дуров не продаётся!
    
    Так будет и с Фейсбуком. Нельзя всё время делать говно. Кто-то когда-то сделает хороший продукт, куда всё и уйдут.
    
    #соцсети #facebook #акции #рептилоиды',
                'reply_markup' => bless( {
                  'rows' => [
                    bless( {
                      'buttons' => [
                        bless( {
                          'data' => 'send_reaction_0',
                          'text' => '???? 452'
                        }, 'Telegram::KeyboardButtonCallback' ),
                        bless( {
                          'text' => '???? 21',
                          'data' => 'send_reaction_1'
                        }, 'Telegram::KeyboardButtonCallback' )
                      ]
                    }, 'Telegram::KeyboardButtonRow' )
                  ]
                }, 'Telegram::ReplyInlineMarkup' ),
                'entities' => [
                  bless( {
                    'length' => 199,
                    'offset' => 0
                  }, 'Telegram::MessageEntityBold' ),
                  bless( {
                    'length' => 8,
                    'offset' => 919
                  }, 'Telegram::MessageEntityHashtag' ),
                  bless( {
                    'offset' => 928,
                    'length' => 9
                  }, 'Telegram::MessageEntityHashtag' ),
                  bless( {
                    'length' => 6,
                    'offset' => 938
                  }, 'Telegram::MessageEntityHashtag' ),
                  bless( {
                    'length' => 11,
                    'offset' => 945
                  }, 'Telegram::MessageEntityHashtag' )
                ],
                'views' => 6964,
                'flags' => 50368,
                'id' => 854,
                'post' => 1
              }, 'Telegram::Message' ),
              'pts_count' => 0
            }, 'Telegram::UpdateEditChannelMessage' ),
            bless( {
              'message' => bless( {
                'reply_markup' => bless( {
                  'rows' => [
                    bless( {
                      'buttons' => [
                        bless( {
                          'data' => 'send_reaction_0',
                          'text' => '???? 213'
                        }, 'Telegram::KeyboardButtonCallback' ),
                        bless( {
                          'data' => 'send_reaction_1',
                          'text' => '???? 8'
                        }, 'Telegram::KeyboardButtonCallback' )
                      ]
                    }, 'Telegram::KeyboardButtonRow' )
                  ]
                }, 'Telegram::ReplyInlineMarkup' ),
                'views' => 2940,
                'entities' => [
                  bless( {
                    'length' => 609,
                    'offset' => 348
                  }, 'Telegram::MessageEntityItalic' )
                ],
                'flags' => 50368,
                'post' => 1,
                'id' => 857,
                'edit_date' => 1571907636,
                'date' => 1571902479,
                'to_id' => bless( {
                  'channel_id' => 1380524958
                }, 'Telegram::PeerChannel' ),
                'message' => 'Пост про 1С вызвал бурную полемику. Человек 10 (видимо, 1с-программистов) единодушно написали:
    // [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
    
    Я бы добавил, что блестящая у 1С дистрибуция, а маркетинг... ну, такое.'
              }, 'Telegram::Message' ),
              'pts_count' => 0,
              'pts' => 158508
            }, 'Telegram::UpdateEditChannelMessage' ),
            bless( {
              'pts' => 158508,
              'pts_count' => 0,
              'message' => bless( {
                'message' => 'Здравствуйте, расскажите, пожалуйста, чем вредит экономике 1С?
    
    // [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
    #софт #it #экономика',
                'edit_date' => 1571907650,
                'date' => 1571893707,
                'to_id' => bless( {
                  'channel_id' => 1380524958
                }, 'Telegram::PeerChannel' ),
                'flags' => 50368,
                'post' => 1,
                'id' => 856,
                'reply_markup' => bless( {
                  'rows' => [
                    bless( {
                      'buttons' => [
                        bless( {
                          'data' => 'send_reaction_0',
                          'text' => '???? 360'
                        }, 'Telegram::KeyboardButtonCallback' ),
                        bless( {
                          'data' => 'send_reaction_1',
                          'text' => '???? 32'
                        }, 'Telegram::KeyboardButtonCallback' )
                      ]
                    }, 'Telegram::KeyboardButtonRow' )
                  ]
                }, 'Telegram::ReplyInlineMarkup' ),
                'views' => 4416,
                'entities' => [
                  bless( {
                    'offset' => 0,
                    'length' => 64
                  }, 'Telegram::MessageEntityBold' ),
                  bless( {
                    'offset' => 1551,
                    'length' => 5
                  }, 'Telegram::MessageEntityHashtag' ),
                  bless( {
                    'length' => 3,
                    'offset' => 1557
                  }, 'Telegram::MessageEntityHashtag' ),
                  bless( {
                    'offset' => 1561,
                    'length' => 10
                  }, 'Telegram::MessageEntityHashtag' )
                ]
              }, 'Telegram::Message' )
            }, 'Telegram::UpdateEditChannelMessage' )
          ]
        }, 'Telegram::Updates::ChannelDifference' )
      }, 'MTProto::RpcResult' )
    };
    
    2019.10.24 12:00:51 $1 = {
      'in' => bless( {
        'update' => bless( {
          'user_id' => 2507460,
          'status' => bless( {
            'was_online' => 1571907651
          }, 'Telegram::UserStatusOffline' )
        }, 'Telegram::UpdateUserStatus' ),
        'date' => 1571907650
      }, 'Telegram::UpdateShort' )
    };
    
    2019.10.24 12:05:46 $1 = {
      'in' => bless( {
        'chats' => [],
        'date' => 1571907946,
        'seq' => 0,
        'updates' => [
          bless( {
            'max_id' => 141719,
            'channel_id' => 1295963795
          }, 'Telegram::UpdateReadChannelInbox' )
        ],
        'users' => []
      }, 'Telegram::Updates' )
    };
    
    2019.10.24 13:01:23 $1 = {
      'in' => bless( {
        'server_salt' => '4914425622822907323',
        'unique_id' => '5297282355827493819',
        'first_msg_id' => '6751307555044380692'
      }, 'MTProto::NewSessionCreated' )
    };
    
    2019.10.24 13:24:21 $1 = {
      'in' => bless( {
        'chats' => [
          bless( {
            'username' => 'freebsd_ru',
            'version' => 0,
            'flags' => 5440,
            'title' => 'freebsd_ru',
            'min' => 1,
            'photo' => bless( {
              'photo_small' => bless( {
                'local_id' => 328733,
                'volume_id' => 235140688,
                'dc_id' => 2,
                'file_reference' => '
                'secret' => '4426006807282303416'
              }, 'Telegram::FileLocation' ),
              'photo_big' => bless( {
                'dc_id' => 2,
                'file_reference' => '
                'volume_id' => 235140688,
                'local_id' => 328735,
                'secret' => '71251192991540083'
              }, 'Telegram::FileLocation' )
            }, 'Telegram::ChatPhoto' ),
            'date' => 1461248502,
            'id' => 1038300508,
            'democracy' => 1,
            'megagroup' => 1
          }, 'Telegram::Channel' )
        ],
        'users' => [
          bless( {
            'last_name' => 'Panov',
            'flags' => 1048646,
            'min' => 1,
            'id' => 82234609,
            'status' => bless( {}, 'Telegram::UserStatusRecently' ),
            'first_name' => 'Dima'
          }, 'Telegram::User' )
        ],
        'seq' => 0,
        'date' => 1571912647,
        'updates' => [
          bless( {
            'pts' => 137596,
            'message' => bless( {
              'flags' => 256,
              'message' => 'Создать джейл с именем покороче ??',
              'to_id' => bless( {
                'channel_id' => 1038300508
              }, 'Telegram::PeerChannel' ),
              'id' => 119634,
              'date' => 1571912647,
              'from_id' => 82234609
            }, 'Telegram::Message' ),
            'pts_count' => 1
          }, 'Telegram::UpdateNewChannelMessage' )
        ]
      }, 'Telegram::Updates' )
    };

    Да, специально не под спойлером — если Вы не вчитались, идите и сделайте это!


    Oh, wai~~… на что же это похоже? Что-то очень знакомое… может, это структура данных типичного Web API в JSON, только разве что еще к объектам классы прицепили?..


    Так это же получается… Что же это выходит, товарищи?.. Столько усилий — и мы остановились передохнуть там, где Web-программисты только начинают?.. А просто JSON поверх HTTPS был бы не проще?! А что же мы получили в обмен? Стоили ли эти усилия того?


    Давайте оценим, что нам дали TL+MTProto, и какие возможны альтернативы. Ну, HTTP, ориентированный на модель "запрос-ответ", подходит плохо, но хотя бы что-то поверх TLS ?


    Компактная сериализация. Видя вот эту структуру данных, похожую на JSON, вспоминается, что есть его бинарные варианты. Отметем MsgPack как недостаточно расширяемый, но вот есть, например, CBOR — между прочим, стандарт, описанный в RFC 7049. Примечателен он тем, что в нём определены теги, как механизм расширения, и среди уже стандартизированных имеются:


    • 25 + 256 — замена повторяющихся строк на ссылку на номер строки, такой дешевый метод компрессии
    • 26 — сериализованный объект Perl c именем класса и аргументами конструктора
    • 27 — сериализованный языконезависимый объект с именем типа и аргументами конструктора

    Ну что ж, я попробовал одни и те же данные сериализовать в TL и в CBOR со включенной упаковкой строк и объектов. Результат стал различаться в пользу CBOR где-то от мегабайта:


    cborlen=1039673 tl_len=1095092

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


    Быстрое установление соединения. Имеется в виду нулевой RTT после переподключения (когда ключ был уже однажды выработан) — применимо с первого же сообщения MTProto, но при некоторых оговорках — попали в ту же соль, сессия не протухла, etc. Что нам взамен предлагает TLS? Цитата по теме:


    При использовании PFS в TLS могут применяться TLS session tickets (RFC 5077) для возобновления зашифрованной сессии без повторного согласования ключей и без сохранения ключевой информации на сервере. При открытии первого соединения и создания ключей, сервер шифрует состояние соединения и передает его клиенту (в виде session ticket). Соответственно, при возобновлении соединения клиент посылает session ticket, содержащий в том числе сессионный ключ, обратно серверу. Сам ticket шифруется временным ключом (session ticket key), который хранится на сервере и должен распределяться по всем frontend-серверам, обрабатывающим SSL в кластеризованных решениях.[10]. Таким образом, введение session ticket может нарушать PFS в случае компрометации временных серверных ключей, например, при их длительном хранении (OpenSSL, nginx, Apache по умолчанию хранят их в течение всего времени работы программы; популярные сайты используют ключ в течение нескольких часов, вплоть до суток).

    Здесь RTT не нулевой, нужно обменяться как минимум ClientHello и ServerHello, после чего вместе с Finished клиент уже может слать данные. Но тут следует вспомнить, что у нас не Web, с его кучей вновь открываемых соединений, а мессенджер, соединение у которого часто одно и более-менее долгоживущее, относительно коротких запросов на Web-страницы — всё мультиплексируется внутри. То есть, вполне приемлемо, если нам не попался совсем уж плохой перегон метро.


    Что-то еще забыл? Пишите в комментах.


    To be continued!


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


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


    • продолжение свистопляски с многообразием TL-типов
    • неизвестные вещи о каналах и супергруппах
    • чем dialogs хуже roster
    • об абсолютной vs относительной адресации сообщений
    • чем отличается photo от image
    • как эмодзи мешают размечать текст курсивом

    и другие костыли! Stay tuned!

    Support the author
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 236

      –1
      В коде Android-клиента вообще не нашлось парсера схемы (что вызывает вопросы к опенсорсности
      В репозитории F-Droid проприетарные части кода удаляют перед сборкой и собирают клиент из исходников, следовательно, парсер таки должен быть.
        +7

        Никто не утверждал, что оно не собирается. И наличие парсера отсюда никак не следует. Тезис в другом. Представьте, некто написал приложение на Си и выложил в качестве исходников некоего подмодуля вывод промежуточной стадии компилятора в ассемблер. Соберется ли оно? Да. Исходники открыты? Вроде бы тоже да, но те ли это исходники?..

          –1
          Логично, но в F-Droid такое не должны принимать именно из-за проблем с тем, чтобы убедиться, что это те исходники. Поэтому, им и доверяют такие параноики, как я. Таким образом, либо их сборка официального клиента обходится без парсера, либо пишут свой, либо я совершенно напрасно о них так хорошо думаю, либо парсер таки есть в исходниках в виде исходников.
            +10
            либо их сборка официального клиента обходится без парсера

            А зачем бы ей в нём нуждаться? Выкладывают уже сгенерированный отдельно лежащим парсером код, например, компилятор его соберёт; разве F-Droid делает доскональный аудит кода, а уж тем более пишет за авторов не выложенные ими инструменты? Лично для меня звоночек — сообщения типа "исходники клиента на GitHub обновлены до версии ..." — почему разработка не ведется в самом гитхабе?..


            Если парсер есть, следует ткнуть пальцем, где же он.

              0

              Насколько я слышал, Android выкладывают с задержкой для того чтоб форки не набирали излишней популярности

                +3

                И что в форках плохого, спрашивается?

                  0

                  Потребителям, наверное, ничего. Для telegram — отсутствие контроля над ними.

                    +1

                    А зачем им контроль над ними? Они этот выбор — опенсорс и возможность каждому написать свой клиент — выбрали еще в 2013. И подтвердили этой осенью свежей документацией. То бишь, им должно быть достаточно контроля над протоколом и сервером.

                      –1

                      Видимо недостаточно. Или у вас есть другие объяснения?

                        +1

                        Надеюсь, не надо объяснять, почему это плохо?

                          –1
                          Кому плохо?
                            +1

                            (со)Обществу.

                0
                github.com:Telegram-FOSS-Team/Telegram-FOSS → TMessagesProj/jni/tgnet/MTProtoScheme.cpp

                Парсера нет. В исходниках (это те, из которых собирается f-droid версия) лежит уже сгенерированный (или написанный вручную) .cpp файл + его .h.

                + дополнительный бонус, мы видим, как внутри TL может лежать JSON. %)
          +15
          Самое поразительное это то, что у них при всём безумии подхода получается самый быстрый и удобный мессенджер с очень маленьким количеством багов силами крошечной по современным меркам команды.
            –7
            И именно ето больше всего бесит хейтеров
              +12
              Но переехать на TLS на уровне транспорта им бы не помешало.
                0
                В актуальном прокси-протоколе — уже, там верхним слоем обычный TLS.
                  0

                  Прокси протокол не описан. Описан обфусцированный транспорт для нижнего уровня. Если вы про faketls, то это не TLS.

                    +1
                    Выходит вообще дичь. Обернутый в псевдо-TLS MTProto, который является неумелым переизобертением TLS.
                      0

                      это не переизобретение. У него только одна цель — выглядеть как TLS на проводе (для маскировки). Т.к. одно дело если DPI видит поток байт ни на что не похожий (что подозрительно) и другое — валидный http/2 TLS.

                        0
                        Я про то что сам MTProto в чистом в виде без всяких прокси, это по сути попытка Николая сделать TLS. В посте ведь как раз об этом.
                          0

                          Ну как бы… Сейчас уже "всё интернет-сообщество" признало, что TCP+TLS1.2 не очень-то подходит для современных реалий (мобильные устройства, частые подключения-отключения, смена IP). Из за этого сделали TLS1.3 и HTTP/3 + QUIC на подходе.
                          Мессенджеры в этом плане первыми начали с такими проблемами сталкиваться. Делали бы telegram в 2019м — наверное сделали бы на TLS1.3 или QUIC (в статье же писали, что они UDP тоже пытались написать, но не осилили).

                            +2

                            Необходимость UDP vs недостатки TCP и нужность QUIC и прочих поделок Гугла сильно преувеличена хайпом. Гугл здесь тоже изобретает велосипеды вместо SCTP, просто более качественные. Да и проистекает всё это в основном как подпорки других архитектурных проблем современного Веба.

                              0

                              Из того что я читал — основная проблема SCTP в том, что его не понимает сетевое оборудование (в отличие от TCP и UDP).


                              В разработке HTTP/3 + QUIC не только гугл участвует (Mozilla, Cloudflare, Netflix), видимо не у одного него есть к TCP + TLS претензии.

                                +1

                                Он прекрасно оборачивается в UDP, так что эта проблема давно решена.


                                А эти участвуют по чуть другой причине — Google делает это всё сугубо для целей своего бизнеса, а не блага всея Интернета, т.е. если не вмешаться в процесс, можно проворонить что-нибудь плохое уже для своего бизнеса. Вот потому и участвуют.

              +3
              Крошечная, это сколько? Очень маленькое количество багов, это сколько? Удобный, это как считали? Быстрый, это с чем сравнивали? Ну и наконец — а сколько времени эта «крошечная» команда ваяла этот примитивный засланец сообщений?

              Если посчитать метрики, то окажется, что задачу для пятого класса детишки из института решали года два.
                –3
                Крошечная, это сколько?

                Два десятка челобит

                Удобный, это как считали?

                — Product-as-a-Platform, ЕВПОЧЯ.
                — удобная однопальцевость
                — отправка фоточек альбомами
                — уникальные на момент запуска фичи типа секретных чатов, live location, каналов, и live preview
                — поддержка прокси-серверов
                  +4
                  Два десятка челобит

                  Откуда сведения?


                  — удобная однопальцевость

                  Нет. Несколько версий как на андроиде переделали контекстное меню, теперь приходится перемещать пальцы (у меня планшет).


                  — отправка фоточек альбомами
                  — уникальные на момент запуска фичи типа секретных чатов, live location, каналов, и live preview
                  — поддержка прокси-серверов

                  А вот и нет. На момент запуска из этого не было ничего. Каналы появились в 2015, альбомы вообще почти через 5 лет после запуска

                    –1
                    Откуда сведения?

                    В прессе проскакивала инфа.

                    теперь приходится перемещать пальцы (у меня планшет).

                    На планшетах в принципе однопальцевость недостижима, а на телефонах она и не менялась.

                    На момент запуска из этого не было ничего

                    Не на момент запуска мессенджера, а на момент запуска фич. Они были первыми на рынке.
                      +3
                      На планшетах в принципе однопальцевость недостижима, а на телефонах она и не менялась.

                      Достижима на некоторое определенное время (листание одного канала, например).


                      а на момент запуска фич. Они были первыми на рынке.

                      Чушь какая. Многое из этого было задолго до. Например информация о ссылке — боты в IRC делали это еще в досмартфонную эру.

                        –3
                        Например информация о ссылке — боты в IRC делали это еще в досмартфонную эру.

                        Тут ключевое — в досмартфонную. В досмартфонную эру, эти ваши IRC, например, прошли мимо меня — ICQ и вебчатики были наше всё.
                          +3
                          и вебчатики

                          Ну, тут можно только посочувствовать.

                            0

                            Чему посочувствовать? (: тепло, лампово, уютно. И как это сейчас говорится — трендовенько. зеро-клиент, и вот это всё.

                              0

                              Они же ведь убогие даже сейчас, когда Web 2.0 во все щели, а уж тогда-то...

                                0
                                Сейчас они давно мертвы — все ушли на мобилы.
                                А тогда это было очень удобно — порой на рабочую машину нельзя было ставить сторонний софт, флэшек еще не было, а на дискетах таскать неудобно. А «ослик ИЕ» — вот он, под рукой. В 4.0 появился XHR и стало еще более бодро: AJAX-чатики выигрывали у перегружаемых тупо UX-ом, и народ туда валил толпами.
                                  0

                                  Ну не знаю, я в те годы Миранду именно на дискетке по кафедрам таскал.

                        +2
                        Не на момент запуска мессенджера, а на момент запуска фич. Они были первыми на рынке.

                        The OTR protocol was designed by cryptographers Ian Goldberg and Nikita Borisov and released on 26 October 2004.

                        Я OTR впиливал в свой мессенджер году в 2010-м. Чего-чего первыми они сделали?

                          –1
                          Куда вы его впиливали? Первым массовым мессенджером в мобильном сегменте был WhatsApp.
                          А OTR — это шифровальный оверлей для семейства протоколов Jabber.
                            +1
                            Куда вы его впиливали?

                            Вы не хотите знать, правда. Я вырос из коротких штанишек пиара своей недоподелки, теперь только свои скиллы пиарю.


                            А OTR — это шифровальный оверлей для семейства протоколов Jabber.

                            Orly? Я OTR-стримы гонял через вконтактик в том числе (очень смешно выглядела личка в вебне после этого). OTR вообще плевать на конкретный протокол.

                              0

                              Эта штука называлась Leechcraft, и я даже одно время её использовал. Комбайн аля старая опера.

                                0

                                Ну не комбайн же, модули же!

                        –7
                        >> Два десятка челобит

                        Если, как пишет автор, только кривую копию TLS они 2 года ваяли, то…

                        Даже за 2 года — это уже 40 человеко-лет. Сорок!!! И на выходе что? Отправка и приём сообщений. Ну да, с рядом удобств, плюс на нескольких платформах. Но за минимум 40 человеко-лет!!!

                        Пусть 4 платформы (браузер, ведроид, ифон, сервер), пусть по 10 рыл на платформу. Три команды пишут строго одно и то-же, но с перламутровыми пуговицами. Итого — 10 человеко-лет на условную единую ведроид-софтину с весьма скромным функционалом. Или 10 человеко-лет на сервер, который реализует весьма простую очередь с асинхронным доступом. И это минимум. А скорее всего они лет 20-30 пилили свой сервер очередей.

                        За 10 человеко-лет можно винду переписать, плюс ещё браузер с блокнотами всякими до кучи. Правда без миллиона драйверов, понятно. И хотя такая эффективность не для больших контор, но 20-30 лет на просто сервер очередей — это ужас даже по меркам толстой конторы.

                        В общем — эффективность Дурова в плане разработки софта на весьма посредственном уровне. Когда бюджет лошадиный — тогда можно. Но был бы это стартап — однозначно раз в 10 могли бы время сократить, ведь стартапу никто бесконечных миллионов зеленью не отваливает.
                          +1
                          Это эффективность всяких WhatsApp на весьма посредственном уровне. Что они там делают командами в 300 человек я без понятия.
                        0
                        Удобный, это как считали?


                        Я сравнивал Telegram с его ближайшими конкурентами — WhatsApp, Viber, Facebook Messsenger. В чем Telegram удобнее:

                        1. Десктопный клиент. Он не самый лучший, если сравнивать с мультпротокольными Jabber-клиентами, но он хотя бы есть и не требует принудительно включенного телефона в той же сети
                        2. Удобные группы и супергруппы с более-менее приемлемыми возможностями администрирования, не светящие при этом телефоны всех участников
                        3. Боты с человеческим API и отсутствием странных лимитов


                        +4
                        быстрый и удобный мессенджер

                        Это больше похоже на социальную сеть из которой убрали все кроме личных сообщений: каналы, стикеры, эможи, «постоянный онлайн», отсутствие нормального ростера/вкладок, привязка к телефону и пр. Ну и на уровне кода туда из vk утащили несколько кусков.
                          +1
                          С пробуждением. Все эти мессенджеры «нового поколения» и есть социальные сети, упрощённые под управление со смартфонов. Они спят и видят, как повторяют успехи азиатских сетей, привязавших к себе пользователей по рукам и ногам их же собственными действиями.

                          Задаваться тут вопросом, зачем нужны привязка к телефону и доступ к контактам, — всё равно, что удивляться, что в тюрьме шнурки и ремни отбирают.
                            +2
                            С пробуждением. Все эти мессенджеры «нового поколения» и есть социальные сети, упрощённые под управление со смартфонов

                            Да я и не спал. И не пользуюсь подобным софтом. просто до многих это не дошло еще…
                            +1
                            каналы, стикеры, эможи, «постоянный онлайн»

                            С разморозочкой. Всё это есть сто лет как.

                            отсутствие нормального ростера/вкладок

                            Plus Messenger. Ростер не нужен — есть просто список активных чатов, сортированный по last updated
                              +2
                              список активных чатов, сортированный по last updated

                              который пересортировывается пока ты пытаешься попасть в нужный чат пальцем
                                –1
                                Может у вас просто слишком много чатов? Никогда такой проблемы не испытывал.
                                  +1

                                  И в хроме у меня слишком много вкладок, я знаю.

                                    0

                                    Более сотни, а что? Это не от количества чатов зависит, а исключительно от активности в них — достаточно флуда всего в 3-4...

                                      –2
                                      Как человек с пятью флудилками — несогласен )
                                        +1

                                        Значит, у Вас не было потребности форвардить между ними, например, или долгое время не читать одну, но потом всё-таки желать всё это вычитать. "Потребности в колбасе нет"

                                          0

                                          Долгое время не читать — действительно не было, просто отписываюсь и все.

                                            0

                                            Ну вот есть у меня каналы, которые пишут не то чтоб часто, и у меня возникает настроение почитать данную тематику раз в несколько недель, допустим. Зачем мне от них отписываться и терять позицию прочтения, если потенциально все эти статьи будут мне интересны?..

                                  +4
                                  Ростер не нужен — есть просто список активных чатов, сортированный по last updated

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


                                  Впрочем, это тема для второй части.

                                    0
                                    Сейчас они в оф клиенте это немного улучшили — добавили т.н. «архив», где можно прикрепить больше 5 чатов. Поэтому у меня все флудилки сразу уехали в архив, и им были закреплены постоянные места. Но все равно выглядит как костыль. Очень сильно не хватает ростера и групп…
                                      +1

                                      На самом деле, там был задел на "папки" — но появилось всего 2 метода в folders.* т.е. клиент может пользоваться только готовыми предсозданными на сервере папками (на данный момент архив). Почему так недоделано, если вот уже почти было сделано правильно, непонятно.

                                    +2
                                    В результате чего наверху флуд, а важное сообщение, отправленное ночью в личку — внизу? :)
                                      0
                                      Нет.
                                        0

                                        Ну как нет, регулярно с этим сталкиваюсь.

                                          –1
                                          Хз, УМВР )
                                            0

                                            Сколько у Вас диалогов-то?

                                              0

                                              Регулярно — порядка 50, а так за всю историю — несколько сотен.

                                                0

                                                Видимо, Вам везет, у меня примерно такие же цифры.

                                                  0
                                                  Ну, в них не пишут одновременно как правило.
                                                    +2

                                                    Кстати, я не могу утверждать наверняка, пишут или не пишут в мои. Это может быть и кривая работа апдейтов — на десктопе и андроиде задержка в обновлении одного и того же чата могла достигать минут.

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

                                                      И этот лаг было фичей, чтобы разные люди заходили с разным временем старта. То что люди об этом не знали, никого не волновало.
                                      –1
                                      С разморозочкой. Всё это есть сто лет как.

                                      ну так я этим и не польззуюсь 100 лет, жду когда дурная мода пройдет и начнется новая, может она не такая дурная будет.

                                      Ростер не нужен — есть просто список активных чатов, сортированный по last updated

                                      Вот знаешь, хочется взять у*бать за такое. Мало того, что интерфейс не компактен и нет группировок, так еще и чаты скачут как бешеные козлы туда-сюда.
                                        0
                                        ну так я этим и не польззуюсь 100 лет, жду когда дурная мода пройдет и начнется новая, может она не такая дурная будет.

                                        Ждите. Не пройдет, потому что выразительно и эмоционально.

                                        Мало того, что интерфейс не компактен и нет группировок

                                        Берете Plus Messenger — там есть группировки по типам: лички, группы, супергруппы, каналы, боты.
                                          0
                                          Спасибо, не надо. Сами в своих соц. сетях сидите
                                            0

                                            Ну да, из 200 диалогов поделим на 5, получим 40 — всё равно много.


                                            Ждите. Не пройдет, потому что выразительно и эмоционально.

                                            Мы надеемся, что пройдёт. Ибо выразительности там кот наплакал.

                                      +2
                                      Вот да, такой терминальный, загробный Not Invented Here, но ведь мне самому продуктом пользоваться приятнее, чем любым аналогом. Когнитивный диссонанс.
                                        0
                                        силами крошечной по современным меркам команды

                                        Силами оперативки моего мака десктопный клиент у них быстрый. Telegram — Memory: 4.59GB — вот что я вижу прямо сейчас. Да у меня Photoshop и Illustrator вместе взятые меньше памяти жрут. Что конкретно такого быстрого в этом вашем телеграмме по сравнению с iMessage, WhatsApp и Viber? Клиент WhatApp для мака, кстати говоря, занимает всего лишь 300mb оперативки на том же маке, с тем же количеством контактов и с б`ольшим количеством чатов.
                                        Пользуюсь телегой только из-за того, что он выбран основным мессенджером на работе, а так давно бы от него избавился.
                                        Есть необходимость пользоваться еще и Viber, WhatsApp, iMessage, так что не понятно что такое «самый быстрый и удобный»? Сравнивая мобильные приложения вообще не понятно о чем речь. На iOS у всех мессенджеров ± все одинаково. Если сравнивать десктопные клиенты — Telegram, в моем личном рейтинге, самый не удобный, самый забагованный, самый ресурсозатратный, самый тормознутый (из-за блокировок долго загружается, бывает что не присылает push-уведомления на телефон) и самый переоцененный.
                                          0
                                          Прям таки интересно стало, сколько у меня TG Desktop жрет. Под Windows диспетчер задач выдал вполне ожидаемые 118 мегабайт памяти. Под Linux немного веселее все оказалось. Попытка замерить память телеграма командой pmap выдало что-то в районе 3.5 гигабайт. Ирония в том, что другой скрипт, показывающий общую загрузку памяти мне отчитался о загрузке… 0.4 гигабайт занятой памяти. Я так и не понял, что он там считал, но возможно, что всю замапленную память, а не реально используемую. ps уже показал более реалистичную цифру — в районе 250 мегабайт.
                                          Мака у меня, к сожалению, нет, но в правдивости цифры 4.6 гигабайт отожраной оперативки есть повод сомневаться.
                                            +1
                                            Вот у меня на маке прямо сейчас
                                              0
                                              Да, клиент явно течёт. При длительном аптайме клиента в несколько дней сутки или недель реальная память растёт до гигабайта и больше, сжатая – несколько гигов, отчего система начинает свопаться.
                                              0
                                              В Linux измерить потребление памяти очень не просто. Я бы сказал не возможно.

                                              Т.е. измерить потребление виртуальной памяти да, можно. Измерить RSS сейчас этого процесса можно.

                                              Но это все погода и вектор )
                                              +1
                                              > Telegram — Memory: 4.59GB — вот что я вижу прямо сейчас.
                                              Если это та память, что в Unix системах зовётся VSZ, то это показатель тупо ни о чём: процесс может замапить себе всё, что найдёт в файловой системе, и показатель вырастет до петабайт (если процессор позволит), но ничего из этого не будет подгружено. VSZ это вроде самодекларированных «намерений» процесса использовать данный объём в наиболее крайнем случае.
                                              Посчитайте RSS и сумму размеров dirty pages. Вот их уже можно сравнивать. (Я не знаю, почему dirty pages не включается в показ по всяким ps — на практике это даже более полезный показатель, чем RSS.)
                                                –1
                                                Вот после нескольких дней аптайма клиента: жрёт 2,64 Гб физической + 6,25 Гб сжатой памяти. Это больше ядра и больше тяжёлой IDE с открытым проектом. Как я понимаю, сжатая память – это то что при возможности скидывается в своп. Ладно бы просто маппинг фалов, но больше похоже что кроме дискового кэша используется и кэш в памяти. Перезапуск клиента освобождает ресурсы, картинки при этом повторно не перекачиваются. Видимо, отсутствие оптимизации, а может баг.
                                                скрин

                                              +1

                                              Я тут по результатам другого треда таки поставил телегу на десктоп.


                                              1. Версия из исходников тянет штук двадцать лишних зависимостей. Неудобно. Ну да ладно, позволим себе поставить -bin, на один раз сойдёт.
                                              2. Требует телефон. Пришлось вспоминать пароль от sms-reg.com. Неудобно.
                                              3. Как-то подлагивает. По-видимому, когда пытается воспроизводить звук. По-видимому, потому, что у меня нет пульсаудио. Без багов, ага. И быстро.
                                              4. После джойна в чатик с сотней человек жрёт 200 мегабайт памяти. Быстро!
                                              5. Как сделать текст во всю ширину экрана, а не 300 пикселей, не нашёл. Удобно. У меня 3840 пикселей по горизонтали, из которых используется десятая часть, класс.
                                              6. Как настроить уведомления, ключевые слова для хайлайта, вот это всё. что ожидаешь от адекватного мессенджера, не нашёл. Удобно.

                                              Потом ещё придирки были, но то уже мелочи.

                                                0
                                                Нужно понять, что телеграм это, в первую очередь, мобильный мессенджер. И его ближайшие конкуренты — это вайбер и вацап. Пункт два сразу же отпадает.
                                                Пункты 3-4… А вы точно пользовались electron-based мессенджерами? Они и жрут больше и лагают тоже больше. Телега тут не первая, не последняя.
                                                5-6 это вообще вкусовщина.
                                                  +2
                                                  Почему это отпадает? Использую дискорд и матрикс с телефона без номера.

                                                  Телега не электрон, соответственно сравнивать надо с не-электроном.
                                                    +1
                                                    Нужно понять, что телеграм это, в первую очередь, мобильный мессенджер. И его ближайшие конкуренты — это вайбер и вацап. Пункт два сразу же отпадает.

                                                    То есть, с десктопа им пользоваться никак? Какой же это тогда мессенджер для профессионального и тематического™ общения?


                                                    А вы точно пользовались electron-based мессенджерами?

                                                    Только дискордом на игровой машине с виндой, но я там никогда не смотрел на потребление памяти. Но он, э, игровой. Push-to-talk там, многопользовательские голосовые каналы, игровой оверлей. В телеге это всё есть?


                                                    5-6 это вообще вкусовщина.

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

                                                      +1
                                                      5-6 это вообще вкусовщина.

                                                      Ни раз нет. Это — эргономика. Попробуйте использовать на работе разработчиком, вот тогда поймёте и поплюетесь.

                                                        0
                                                        А какой мессенджер позволяет настроить ключевые слова для хайлайта?
                                                          0
                                                          IRC?
                                                            0
                                                            А из тех, которым реально пользуются?
                                                              0

                                                              IRC сейчас реально пользуются несколько сотен тысяч человек в мире, причем не простых негров или домохозяек, как в типичных мессенджерах, а технически продвинутых (например, официальные каналы кучи опенсорс-проектов на Freenode). Поэтому они гораздо весомее цифр других мессенджеров. Смотреть надо на другое — почему эти "современные, которыми пользуются", не могут сделать то, что было в старичке IRC.

                                                                0
                                                                Я рад за этих технически продвинутых, но тут я на стороне «простых негров и домохозяек». Если мессенджером никто из моих контактов не будет пользоваться, то для меня это бесполезное приложение.

                                                                А вопрос нужно ставить по-другому: не «не могут», а «не делают». Телеграм и WhatsApp — это мессенджеры для повседневного общения, а не для работы. Выделять ресурсы на внедрение функций, которыми будут пользоваться одна сотая аудитории, — очень сомнительное решение.
                                                            0

                                                            Почти любой IRC-клиент. Slack (что не удивительно, он вырастал из IRC и имел в него гейты). Discord, если склероз не изменяет...

                                                        0

                                                        А какой клиент? Их, гхм, несколько.
                                                        Если тот, который telegram-desktop, то он, ну, такой.

                                                          0

                                                          Он самый, да. Он разве не официальный?

                                                            0

                                                            Ну, он из тех, на которые стоит ссылка на официальном сайте, да.
                                                            Официальность у телеги понятие относительное, это скорее "рекомендуемый" клиент из разработок коммьюнити — десктоп у них явно на последнем месте в приоритетах.


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

                                                              0

                                                              Да как Вам сказать… Хуже вебморды у них клиента наверное нет. На маке я через пару месяцев не выдержал и поставил telegram-desktop в дополнение к родному. Да, так и пользовался двумя сразу — потому что множества фич и багов у них малопересекающиеся :/

                                                                0

                                                                Вебморда хотя бы не течет. :)


                                                                На маке есть overtake/TelegramSwift, который лучше telegram-desktop на порядок, зачем эти мучения?


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

                                                                  0
                                                                  На линуксе тот же официальный десктопный клиент месяцами работает.
                                                                    0

                                                                    Судя по скриншотам, именно им я вроде бы и пользовался. Всех проблем сейчас не упомню, хреновая прокрутка стикеров была, вроде копипаста сообщений неудобная. Так вот и жил, Instant View в нём, стикеры из десктопа.

                                                                      0

                                                                      С копипастой проблем не вижу, может, версия старая была.
                                                                      На стикеры мне как-то все равно :)

                                                        +3
                                                        Как-то пробовал использовать штуки три готовых библиотеки (под разные языки), и ни одна сходу не заработала. Теперь понятно, почему.

                                                        А проверка на простоту — это вообще шедевр.
                                                        if (!strcasecmp(prime, goodPrime))
                                                          +1

                                                          Я так понимаю это какое-то легаси число захардкодили, там дальше вроде идет нормальная проверка.

                                                            0

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

                                                              0
                                                              то есть оно что, критериям этой проверки — не удовлетворяет?

                                                              удовлетворяет, просто захардкожено, чтобы бедная мобила не тратила много усилий на проверку is prime для него.

                                                          +8
                                                          Ох-ох-ох! Может ну её, эту вторую часть? Я уже от первой устал материться!
                                                          Я даже и не представлял, что там настолько всё из говна и палок собрано! Аж расхотелось им пользоваться :(
                                                            0

                                                            О да.


                                                            Меня периодически просят впилить поддержку протокола телеги в один IM, который я попиливаю, и я всё откладывал, а теперь, ну, короче, это совсем какое-то бессмысленное занятие. Лучше на ring и matrix посмотрю.

                                                            +4
                                                            Что-то еще забыл? Пишите в комментах.

                                                            Возможно не совсем в тему, но как пример из разряда «и так сойдет».

                                                            Http апи для ботов и tdlib (оно тоже умеет авторизоваться как бот из коробки, если кто не в курсе) используют разные диалекты markdown при парсинге сообщений.

                                                            Как хочет tdlib
                                                            **жирный**
                                                            __курсив__
                                                            ~~зачеркнутый~~
                                                            

                                                            Как хочет бот апи
                                                            *жирный*
                                                            _курсив_
                                                            зачеркнутый не умеет 
                                                            

                                                            Одинаковый парсер для tdlib и для сервера не осилили. И как подсказывает подсветка хабра, что первый, что второй вариант не соответствуют общепринятым стандартам.
                                                              +2
                                                              Еще 5 копеек про TDLib и опенсорц:

                                                              На днях обновилась приватная версия TDLib.
                                                              На GitHub странице клиента Unigram появились коммиты, благодаря которым известно, что библиотека уже поддерживает…
                                                                0

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

                                                              +12
                                                              Сломал мозг на середине статьи. Авторам — респект, у вас железные нервы.

                                                              А вообще все пляски с кодом (плюс api ключи) похоже на обфускацию и вендер-лок для усложнения исследования кода и написания альтернативных клиентов (а может и серверов).
                                                                +1

                                                                Проект альтернативного сервера на GitHub как-то видел. Вот только нужен ли он кому, если с основным оно федерацию не сможет?..

                                                                +25
                                                                «В принципе, такое сгодится, если сделать один раз, но как это потом поддерживать при обновлениях?

                                                                Типичное „олимпийское“ программирование, IMHO. Видна рука мастера.
                                                                  0
                                                                  Спасибо за разбор!
                                                                  Нет ли планов изучить блокчейн TON?
                                                                  На первый взгляд там тоже есть «интересные» решения.
                                                                    0

                                                                    А там, на первый взгляд, всё примерно на тех же подходах. Особенно позабавил, видимо, тот же "восторг отрочества", когда ребята открыли для себя Forth и воодушевлённые этим фактом "написали" его специализированный эрзац Fift.

                                                                      0

                                                                      Это еще год потратить на попытки разобраться и писать свой клиент? :) Они ведь еще даже не запустились. Может оказаться, что их сама жизнь на практике, так сказать, сурово научит о применимости тех или иных подходов.

                                                                        +1

                                                                        Вы смерти nuclight хотите? Я пока пролистал whitepaper TON, чуть монитор не разбил. Спецификация TL по сравнению с ним — стихи.

                                                                        +8
                                                                        Лучшая статья за этот год, что я прочитал на швабре. Ребятки, пишите еще. Можно потом про matrix продолжить, например. В сравнении с…
                                                                          +1
                                                                          к сожалению, его учетку на Хабре стёрли вместе с черновиком

                                                                          Учетка нашлась, а вот черновики куда-то сгинули.
                                                                            +1

                                                                            Я теперь понимаю, почему Дуров отказался отдавать ключи шифрования, аргументируя тем, что это технически невозможно)

                                                                              +2

                                                                              А в этой истории вообще слишком много подозрительного.

                                                                                +2
                                                                                Технически невозможно, ибо не смогли найти в дереве исходников? :-)
                                                                                0

                                                                                Ну ура, наконец-то кто-то раскопал эту глинищу. Уважение!


                                                                                А результат трудов вы выложите в общий доступ? И чтобы не в виде предскомпилированного кода?)

                                                                                  0

                                                                                  Уже выложили. Просто правила Хабра вроде запрещают рекламу своих проектов...

                                                                                    0
                                                                                    В хабе «я пиарюсь» вроде можно же?
                                                                                      +2

                                                                                      Часто можно видеть в подобных ситуациях, как автор в комментариях даёт такую ссылку. Вроде как не запрещено, просьбу дать ссылку поддерживаю.

                                                                                        0
                                                                                        Правила не запрещают ссылки на гитхаб.
                                                                                        0
                                                                                        Разработка ведётся открыто на гитхабе.
                                                                                          +2
                                                                                          +13
                                                                                          Спасибо за статью. Тут уже пишут, что мол. ведь работает же быстро и все такое. Ну а чему там не работать? Задача то простая, а все скользкие моменты, большинство которых появились из-за странных решений, переложены на клиентов. Лишняя нагрузка на сервер их не смущает, они просто поднимают тысячи и тысячи нод в облаках. И даже гордятся этим. Главный принцип, если тебе нужно 30млн на запуск, возьми миллиард и не парься.

                                                                                          Как-то я тоже в исходники сунулся, хотелось проникнуться, что пишут очень умные люди. Потом два дня плакал. Интрига в том, что они правда умные и в чем-то конкретном им разобраться легко. Но вот сделать просто и понятно им не круто, нужно чтобы «перло», до конца доводить им тоже не круто.

                                                                                          Эта элитарность растет от П. Дурова, который жуткий сноб. Он гордится своей ограничительной (вегетарианской) диетой, а также отказом от любых медицинских препаратов, что, по его мнению, возвышает над другими. Его кумир Джобс, который тоже весь элита и на диете, а еще менеджер, который умел презентовать статусность продукта вместо самого продукта. Т.е. эти вот «я не понял, что он сделал, но раз многократный чемпион ACM старательно трудился, значит это круто, об этом и поговорим». Еще он ссылается на Че, но идея свободы выражается в «отвалите от меня, я свободный человек».

                                                                                          Что такое айфон? Новая индустрия, выросшая из продукта, который по сути обычные смартфон и ноутбук, с отличным дизайном, странными костылями, головной болью разработчиков приложений и замкнутой экосистемой.
                                                                                          Так что, они будут пилить это год за годом, пока однажды у них не получится свой json over https. И работать с этим придется как с тем же hls, который, при внешней простоте, очень сложно балансируется на клиенте, а на сервере реализуется непонятно как и во всех случаях по разному.
                                                                                            +6
                                                                                            Так что, они будут пилить это год за годом, пока однажды у них не получится свой json over https

                                                                                            Вы сейчас будете смеяться...


                                                                                            jsonObjectValue#c0de1bd9 key:string value:JSONValue = JSONObjectValue;
                                                                                            
                                                                                            jsonNull#3f6d7b68 = JSONValue;
                                                                                            jsonBool#c7345e6a value:Bool = JSONValue;
                                                                                            jsonNumber#2be0dfa4 value:double = JSONValue;
                                                                                            jsonString#b71e767a value:string = JSONValue;
                                                                                            jsonArray#f7444763 value:Vector<JSONValue> = JSONValue;
                                                                                            jsonObject#99c1d49d value:Vector<JSONObjectValue> = JSONValue;

                                                                                            Это уже в 91-й схеме.

                                                                                              +4
                                                                                              Это вы в статье упоминаете про 'json over https', я на него и ссылаюсь. Получается, что вы предвидели даже их дальнейшие шаги. Когда им понадобилось что-то пересылать всерьез (payments), они прикручивают еще один слой, просто чтобы работало.

                                                                                              Меня беспокоит даже не качество продукта, который скоро будет везде, а его влияние на культуру разработки. Если apple повлияли главным образом на культуру среди пользователей, а ios осталась вещью в себе, с замкнутым циклом, то эти ребята нагло лезут со своим бардаком в общую тусовку, заявляя как надо делать и подкупая пафосом элитаризма. Через несколько лет мы будем с грустными лицами слушать молодых и активных про то, что это круто и как все классно изобретено. И ощущения будут как у прочитавших «Пикник на обочине», которым рассказывают про классную идею автора книги про сталкеров, сделанной по игре.
                                                                                                –1
                                                                                                > заявляя как надо делать

                                                                                                Я вот не помню прямых подобных заявлений, как минимум. По-моему, вы обвиняете воображаемого оппонента, в том же комменте выше — не припомню, чтобы Павел писал, как вегетарианство «возвышает его над другими».
                                                                                                  0
                                                                                                  А может это возрастное. Посмотрим к чему со временем всё это причалит.
                                                                                                +2
                                                                                                Но вот сделать просто и понятно им не круто, нужно чтобы «перло», до конца доводить им тоже не круто.

                                                                                                Сложилось ровно такое впечатление. Особенно про тот же TL — уау, мы крутые, мы пересказали в доках учебник статью из вики про завтипы и начали писать маленький кусок тайпчекера, но потом стало чо-т влом, давайте так хреначить, норм будет.

                                                                                                  +2
                                                                                                  Интрига в том, что они правда умные и в чем-то конкретном им разобраться легко. Но вот сделать просто и понятно им не круто, нужно чтобы «перло», до конца доводить им тоже не круто.
                                                                                                  Типичные программисты-олимпиадники. Там весь рабочий процесс на этом построен: разобраться в чем-то конкретном, получить хоть какую-то рабочую версию, а вот отшлифовать до завершенного вида и обеспечить читаемость и поддерживаемость кода задача уже не стоит.
                                                                                                  Отсюда же и изобретение своих велосипедов и игнорирование существующих проверенных временем решений — на олимпиадах 3rd-party код нельзя использовать, да и ЧСВ жмет :)
                                                                                                  +1

                                                                                                  А вот сырой JSON в схеме:


                                                                                                  inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia;
                                                                                                  
                                                                                                  updateBotWebhookJSON#8317c0c3 data:DataJSON = Update;
                                                                                                  updateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Update;
                                                                                                  help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector<MessageEntity> min_age_confirm:flags.1?int = help.TermsOfService;
                                                                                                  
                                                                                                  dataJSON#7d748d04 data:string = DataJSON;

                                                                                                  Используется например в payments.

                                                                                                    +3
                                                                                                    nuclight ждем еще такой же разбор исходников TON, что-то мне кажется, что там будет еще хуже.
                                                                                                    +2
                                                                                                    (На правах параноика.) Давайте спросим себя, зачем нужен такой цирк из велосипедов и костылей уже на уровне протокола? Не для того ли, чтобы даже другие люди в команде, имеющие доступ к коду, не понимали до конца, как он работает, и тем более не могли отрефакторить? А не способ ли это скрыть масштабную закладку?
                                                                                                    +9

                                                                                                    По пунктам:


                                                                                                    TL (Type Language) и его схема

                                                                                                    Для разработки простого клиента не нужно досконально разбираться в TL. До появления флагов можно было вообще за один вечер написать генератор типов и RPC запросов. Когда появились флаги и стали добавляться сложные типы, то да, пришлось немного пореверсить и почитать код других парсеров TL.


                                                                                                    если Ваша соль "протухла", то сообщение (запрос) — просто потеряется. Сервер, конечно, сообщит новую соль, выдав new_session_created — но со старым придется как-то делать перепосылку, например.

                                                                                                    Ротация в криптографии — обычное дело. Сервер отправит BadMsgNotification типа bad_server_salt. Запоминать отправляемые сообщения — это обычная практика. «Как-то делать перепосылку» придётся и в случае обрыва TCP соединения.


                                                                                                    Серверу разрешено вообще дропать сессии и отвечать таким образом по многим поводам.

                                                                                                    Чаще всего, сервер возвращает RPC Error — включая случаи, когда он не может десериализовать сообщение. Если продолжение невозможно (из-за некорректных данных на транспортном уровне), тогда сервер возвращает 4х байтный код ошибки и, действительно, рвёт соединение.


                                                                                                    С трудом, но можно представить себе какую-то пользу, если человек занимается отладкой, причем в интерактивном режиме — спросить у сервера, что да как. Но здесь описываются запросы в обе стороны.

                                                                                                    Для секретных чатов используется вложенное соединение MTProto, защищённое end-to-end шифрованием. В таких случаях оба клиента могут отправлять друг-другу запросы (можно сказать, что протокол используется в режиме peer-to-peer).


                                                                                                    И о пингах: <...> Да вы с ума сошли?! За 60 секунд поезд въедет на станцию… <...> есть алгоритм Нагла <...> дефолтное значение задержи — 200 миллисекунд. Если вам так уж хочется изобразить нечто похожее и сэкономить на возможной паре пакетов — ну отложите, накрайняк, на 5 секунд, или чему там сейчас равен таймаут сообщения "User is typing...". Но не больше.

                                                                                                    Ну и зачем пользователям или серверу знать с точностью до 200 мс или 5 с, что пользователь ушёл в offline? Ну и в чём проблема того, что приложение на телефоне скажет ОС, что его нужно будить каждые 30/60 секунд (пробуждение в ряде случаев приводит к увеличению частоты CPU) и будет именно с таким интервалом сообщать серверу о своей работоспособности? Telegram запарился насчёт энергосбережения — ну и молодец.


                                                                                                    Транспортный уровень. Нам расскажут аж про 5 вариантов

                                                                                                    Никто не заствляет разработчиков поддерживать всё, что сервер. Можно реализовать только Abridged TCP и никаких проблем не будет.


                                                                                                    У меня тоже есть опыт написания клиента с нуля. А потом — опыт переписывания клиента и написания (простого, на данный момент обеспечивающего только одиночные чаты) сервера (можно найти на github по словам telegram и qt; поддерживается только Linux).
                                                                                                    На мой взгляд, при всех его проблемах, Telegram по прежнему является технически наиболее совершенным и продуманным протоколом. Это подтверждается, например, хорошей масштабируемостью — ни один другой IM не поддерживает групповые чаты на 200 000 пользователей. И даже на порядок меньшая планка в 20к участников чата является практически недостижимой.


                                                                                                    Telegram вообще большие молодцы и почти всё делают максимально эффективно. Протокол становится лучше от схемы к схеме. Увы, каким-нибудь XMPP и Matrix (хотя сердцем я именно за них) до подобной эффективности ещё очень и очень далеко. Очень жаль, что к Telegram почти невозможно прикрутить федерализацию не растеряв всю эффективность.

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

                                                                                                      У Matrix все-же больше другая проблема — крайне тормознутая реализация. Когда сервер на go начили писать совсем другое дело стало, но фичи не все реализовали.
                                                                                                        0
                                                                                                        У Matrix все-же больше другая проблема — крайне тормознутая реализация. Когда сервер на go начили писать совсем другое дело стало, но фичи не все реализовали.

                                                                                                        А как там с нужностью альтернативных реализаций? У меня руки чешутся на хаскеле что-нибудь этакое пописать.

                                                                                                          0
                                                                                                          Не вкурсе. Думаю это просто довольно сложно. Пока из альтернативных есть только Ruma (Rust) и Dendrite (go).
                                                                                                        +6
                                                                                                        Для разработки простого клиента не нужно досконально разбираться в TL. До появления флагов можно было вообще за один вечер написать генератор типов и RPC запросов. Когда появились флаги и стали добавляться сложные типы, то да, пришлось немного пореверсить и почитать код других парсеров TL.

                                                                                                        Пореверсить — это плохо. Обязана быть спецификация, по которой можно просто взять и сделать. В статье мысль о документации проносится не один раз. А TL простой тогда, когда всё понято и пишется конечный код; как уже упоминалось, в Teleperl парсер очень короткий вышел. Но сначала нужно продраться через всю эту ненужную (как в итоге оказывается) заумь. Тогда как в действительности хватило бы вон того подраздела в статье.


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


                                                                                                        Ротация в криптографии — обычное дело. Сервер отправит BadMsgNotification типа bad_server_salt.

                                                                                                        Ротация — да. Терять при этом данные — нет.


                                                                                                        Запоминать отправляемые сообщения — это обычная практика. «Как-то делать перепосылку» придётся и в случае обрыва TCP соединения.

                                                                                                        Нет, это не является обычной практикой, уровень TCP плюс некоторые специальные вещи — единственные, где такая буферизация нормально. В прикладном коде такие слои городить нечего.


                                                                                                        Чаще всего, сервер возвращает RPC Error — включая случаи, когда он не может десериализовать сообщение. Если продолжение невозможно (из-за некорректных данных на транспортном уровне), тогда сервер возвращает 4х байтный код ошибки и, действительно, рвёт соединение.

                                                                                                        Да, спасибо, Кэп. Но речь была не об этом уровне, а выше — у нас нет гарантий, и если серверу ВНЕЗАПНО приспичило забыть сессию, то придется повторять наши сообщения.


                                                                                                        Для секретных чатов используется вложенное соединение MTProto, защищённое end-to-end шифрованием. В таких случаях оба клиента могут отправлять друг-другу запросы (можно сказать, что протокол используется в режиме peer-to-peer).

                                                                                                        Нет, это не соединение. Нет, нельзя так сказать. Я конечно понимаю, на что отсылка, но это кривое понимание и изложение — будь я преподом на экзамене, поставил бы 2.


                                                                                                        Ну и зачем пользователям или серверу знать с точностью до 200 мс или 5 с, что пользователь ушёл в offline?

                                                                                                        Неверное понимание работы TCP. Уйдёт он далеко не сразу, а когда сработают таймеры ретрансмитов. И речь в этом месте была о подтверждении получения, т.е. освобождении буферов противоположной стороны. Статус пользователя к ресурсам сервера имеет мало отношения.


                                                                                                        Ну и в чём проблема того, что приложение на телефоне скажет ОС, что его нужно будить каждые 30/60 секунд (пробуждение в ряде случаев приводит к увеличению частоты CPU) и будет именно с таким интервалом сообщать серверу о своей работоспособности? Telegram запарился насчёт энергосбережения — ну и молодец.

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

                                                                                                          +9

                                                                                                          Вот! Отличный ответ! Его-то я и ждал :)


                                                                                                          Обязана быть спецификация

                                                                                                          Знакомы с поговоркой «дарённому коню в зубы не смотрят»? В случае, если вы заказываете/оплачиваете разработку, вы можете выдвигать свои требования. Хоть спецификацию просить, хоть эталонную реализацию («хозяин — барин»).


                                                                                                          Telegram LLC вправе сам решать, какие данные о своих разработках он будет (или не будет) публиковать.
                                                                                                          Вся статья сквозит идеей о том, что Telegram кому-то чем-то обязаны. Вы ещё к WhatsApp и Facebook Messenger с такими требованиями обратитесь :).


                                                                                                          Поймите меня правильно (хотя пока это не удаётся), за время разработки клиента я и сам столкнулся со множеством интересных моментов и могу очень долго рассказывать о проблемах в Telegram. Но перед этим нужно признать, что у Telegram получилось (на практике) лучше, чем у всех остальных. И за документацию, какой бы она ни была, и за открытый код клиентов — нужно в первую очередь сказать спасибо. И только после этого конструктивно (без характеристик, типа «наркомания») рассуждать о технических недостатках протокола. Если бы не Telegram, то в XMPP и Matrix было бы совсем скучно, потому что остальные (Facebook, WhatsApp, Viber и другие) далеко не настолько дружелюбны.


                                                                                                          Продолжу по пунктам:


                                                                                                          Ротация — да. Терять при этом данные — нет.

                                                                                                          Клиент отправил заведомо недостоверные (с точки зрения аутентификации) данные (всё-равно, что накосячил с подписью). С чего бы серверу их сохранять? Сервер предоставляет возможность делать всё эффективно и правильно — можно даже запрашивать соль на несколько часов вперёд.


                                                                                                          Нет, это не является обычной практикой, уровень TCP плюс некоторые специальные вещи — единственные, где такая буферизация нормально. В прикладном коде такие слои городить нечего.

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


                                                                                                          Нет, это не соединение.

                                                                                                          Нет, соединение. :-) Если хотите, можете посмотреть общепринятое значение слова «соединение» (только не нужно додумывать что-то, чтобы это опровергать). Я же не написал, например, «TCP соединение».


                                                                                                          Нет, нельзя так сказать.

                                                                                                          Вы не можете указывать, кому и как можно или нельзя говорить. Я считаю, что выразился достаточно понятно.


                                                                                                          Неверное понимание работы TCP

                                                                                                          Наверно это потому, что речь не о TCP.


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

                                                                                                          Я процитировал абзац про ping_delay_disconnect с большим интервалом и написал комментарий именно к нему.


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

                                                                                                          Да не хотим мы быть offline! Мы хотим быть online (получать сообщения сразу), но не тратить аккумулятор впустую.


                                                                                                          P.S.: С вами трудно поддерживать конструктивный разговор. Пожалуйста, оставьте свои оценки (например — двойки) себе. Я считаю, что статья была бы значительно лучше, если бы техническая часть не была так перемешана с выплеском эмоций.


                                                                                                          P.P.S.: При написании сервера я собрал ещё вторую порцию граблей и я тоже в восторге от документации. Вот пример:


                                                                                                          https://core.telegram.org/api/optimisation#server-salt


                                                                                                          At present, a single salt’s lifespan is 1 hour

                                                                                                          https://core.telegram.org/mtproto/description#server-salt


                                                                                                          {server salt is} periodically (say, every 24 hours) changed (separately for each session)

                                                                                                          https://core.telegram.org/mtproto/service_messages#request-for-several-future-salts


                                                                                                          a server salt is attached to the authorization key rather than being session-specific
                                                                                                            +9
                                                                                                            Вся статья сквозит идеей о том, что Telegram кому-то чем-то обязаны.

                                                                                                            Ну, если б не пошли в опенсорс — не были бы. А так, простите, какая-то голимая эзотерика. Претендуешь — соответствуй.

                                                                                                            Я лично в недоумении — архитектура насквозь велосипедная. TLS+JSON — и погнали. И не нужно писать отдельный IDL (а TL фактически именно его функции выполняет).

                                                                                                            Да не хотим мы быть offline! Мы хотим быть online (получать сообщения сразу), но не тратить аккумулятор впустую.

                                                                                                            Это так не работает. Ты либо тратишь батарейку на поддержание wakelock-ов и keepalive-трафик, либо отваливаешься.

                                                                                                            «Запрос соли у сервера» — вообще за гранью разумного.
                                                                                                              +2
                                                                                                              архитектура насквозь велосипедная

                                                                                                              Что есть — то есть. :) У Telegram куча своих велосипедов, зато ездят быстро.


                                                                                                              Как выглядит подключение приложение после пробуждения в Telegram?
                                                                                                              Клиент: сессия-123456-дай-диалоги
                                                                                                              Сервер: ОК-первый-контакт-556-второй-738-третий-129-кстати-первый-это-вася, второй-дима, третий-саша,-от-васи-пять-непрочитанных-последнее-сообщение-ты-тут?-от-димы-...


                                                                                                              То есть через сетевой пинг до сервера и обратно, буквально с первого пакета, Telegram готов отобразить начало списока диалогов со всеми данными и последними сообщениями. Разве что без аватарок (они обычно есть в кеше). Быстрее просто некуда и больше никто из IM так не умеет.


                                                                                                              TLS

                                                                                                              Достаточно быстрое и безопасное кеширование сессий появилось только в TLS 1.3. См, например, https://blog.cloudflare.com/introducing-0-rtt/


                                                                                                              +JSON — и погнали.

                                                                                                              Telegram экономят каждый байт трафика, так что на json они бы явно не согласились. Может CBOR бы подошёл.

                                                                                                                –1
                                                                                                                Достаточно быстрое и безопасное кеширование сессий появилось только в TLS 1.3. См, например, blog.cloudflare.com/introducing-0-rtt

                                                                                                                По опыту — это все равно занимает несколько секунд (Waiting for network… Connecting to proxy… Updating ...), если не пуш, а открываешь морду приложеньки. Так что — экономия на спичках.

                                                                                                                Telegram экономят каждый байт трафика, так что на json они бы явно не согласились. Может CBOR бы подошёл.

                                                                                                                Или BSON, но суть в другом — уже обкатанный стандарт.
                                                                                                                  +3
                                                                                                                  Как выглядит подключение приложение после пробуждения в Telegram? Клиент: сессия-123456-дай-диалоги

                                                                                                                  А вот и нет. Первым после соединения чаще будет updates.getState или updates.getDifference, и это еще тоже как смотреть, совсем первым может быть например ping, который не жалко потерять, если вдруг случится bad_server_salt.


                                                                                                                  То есть через сетевой пинг до сервера и обратно, буквально с первого пакета, Telegram готов отобразить начало списока диалогов со всеми данными и последними сообщениями. Разве что без аватарок (они обычно есть в кеше). Быстрее просто некуда и больше никто из IM так не умеет.

                                                                                                                  А точно всех проверяли, чтоб вот так безапелляционно утверждать, будто никто больше до такой "гениальной идеи" не додумался? Давайте я NNTP из 80-х вспомню, где тоже можно этот список первым же запросом получить.


                                                                                                                  Telegram экономят каждый байт трафика, так что на json они бы явно не согласились. Может CBOR бы подошёл.

                                                                                                                  … а потом нивелируют это килобайтом padding? Впрочем, и без него TL совсем не оптимален по байтам, вот пост https://m.roem.ru/17-07-2013/115554/durov-ishchet-razrabotchikov-dlya-novogo-protokola-kommentariy-pavla/#comment-115125 еще аж от 2013 года, где человек сравниваниет и аналоги по сериализации, и дополнительная критика по криптографии, которой не было у меня.

                                                                                                                  +1
                                                                                                                  Это так не работает. Ты либо тратишь батарейку на поддержание wakelock-ов и keepalive-трафик, либо отваливаешься.

                                                                                                                  Ну, у меня — работает на Sailfish OS. Настраиваю ping_with_disconnect на 45 секунд и пробуждение приложения на каждые 30 секунд.


                                                                                                                  https://git.merproject.org/mer-core/nemo-keepalive/blob/master/lib/backgroundactivity.h#L54

                                                                                                                    0
                                                                                                                    Ну, у меня — работает на Sailfish OS.

                                                                                                                    Мы щас про андроид так то )
                                                                                                                    Я, когда портировал Orchid — замечательно наелся, что девайс уходит в слип, и никакие write/flush на сокетах не работают. Так у меня развалился tor circuit. Пришлось костылить.
                                                                                                                      +1
                                                                                                                      Мы щас про андроид так то )

                                                                                                                      Статья — про протокол Telegram. Я комментировал и продолжаю комментировать протокол. В частности — функцию ping_with_disconnect. Когда и как разговор стал про Android?

                                                                                                                        0
                                                                                                                        Когда начались разговоры про батарейку.
                                                                                                                    –1
                                                                                                                    > Ну, если б не пошли в опенсорс — не были бы.

                                                                                                                    Ну вот в моем опенсорсном проекте нет ридми, нет доков и описания работы. Они есть, но лежат в моих личных/рабочих местах. А перенести в паблик с кодом — лень. Не обязан.
                                                                                                                      +1
                                                                                                                      А перенести в паблик с кодом — лень. Не обязан.

                                                                                                                      Я обычно на такие репозитории трачу пару секунд времени и никогда не использую.
                                                                                                                      Поэтому в моих — обычно есть. Чтобы человек, который пришел — мог понять: что это, зачем, и как работает.
                                                                                                                        +2
                                                                                                                        Я говорю, что вы обязаны тратить на них время? Тогда почему вы говорите, что их авторы обязаны что-то делать?
                                                                                                                          +1
                                                                                                                          Есть такая штука — best practice.
                                                                                                                          Формально — никто не обязан.
                                                                                                                          Но после этого не удивляйтесь, если вам скажут, что ваш репозиторий — помойка.
                                                                                                                          +3

                                                                                                                          Существуют разные причины опенсорсить код.


                                                                                                                          Например, я так делаю с собственными библиотеками, которые реюзаю для различных заказчиков — просто потому что "по умолчанию" заказчику передаются исключительные права на код. А так — вот, использовал open source библиотеку под BSDL, какая разница, кто ее автор?


                                                                                                                          В том, чтобы кто-то пришел и мог что-то легко понять, я и не заинтересован.

                                                                                                                            0
                                                                                                                            В том, чтобы кто-то пришел и мог что-то легко понять, я и не заинтересован.

                                                                                                                            А зачем тогда опенсорсить?
                                                                                                                            Опенсорс — это про поделиться знаниями.
                                                                                                                              +3

                                                                                                                              На вопрос «зачем» я ответил абзацем выше процитированного вами.


                                                                                                                              И, нет, опенсорс не обязательно про поделиться знаниями. Это юридический инструмент, имеющий самые различные применения.

                                                                                                                                +2
                                                                                                                                Юридический инструмент — это лицензия )
                                                                                                                                А опенсорс — именно что про поделиться.
                                                                                                                                  +1

                                                                                                                                  Опенсорс — это и есть лицензирование ПО по определенным принципам. По определению.

                                                                                                                                    0
                                                                                                                                    Опенсорс — это прежде всего философия и этика. Которой должны соответствовать лицензии, позиционирующие себя как таковые.
                                                                                                                                    Сам по себе опенсорс наличия лицензии не требует — я могу спокойно дать вам пачку своих исходников — и это будет опенсорс.
                                                                                                                                      +1

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


                                                                                                                                      С этикой я не вижу никаких проблем: если кого-то тот код заинтересует, он может всегда сделать свой форк, задокументировать и так далее. Если меня попросит помочь разобраться — помогу по мере наличия времени. А просто так делать что-либо я не обязан, тем более что я очень сомневаюсь, что этот код вообще кому-то интересен :-)

                                                                                                                                        0

                                                                                                                                        Нет, это именно что инструмент: когда наступит идеальный мир и лицензии станут не нужны, сам опенсорс при этом останется


                                                                                                                                        если кого-то тот код заинтересует, он может всегда сделать свой форк, задокументировать и так далее

                                                                                                                                        Вы правда не понимаете, откуда здесь растет этика? Чтобы этот "кто-то" мог "сделать форк, задокументировать" и так далее, он должен потратить время не только на эти вещи, но и на то, чтобы разобраться — т.е. сначала понять, что было в голове автора. Это всё — затраты. При том, что у автора эта информация уже имеется, и для него это заняло бы меньше ресурсов. Перекладывание своей работы на других. короче, что и есть неэтично; причем по сути на неопределенный круг лиц:


                                                                                                                                        • "Как тебе спится, Джон, серийный программист?" (с) известная картинка с расчетами
                                                                                                                                          0

                                                                                                                                          Правда не понимаю.


                                                                                                                                          У меня есть библиотека, которой люди действительно пользуются, там есть вводный readme, все покрыто тестами так, что они прекрасно выполняют роль документации, даже запилил по реквестам в github issues пару фич, которые мне самому не особо и нужны были, но из соображений полноты архитектуры они там очень даже уместны.


                                                                                                                                          А рядом лежат куски сомнительного кода, которые вряд ли нужны кому-то кроме меня, никто ни разу не интересовался, зачем мне на это тратить свое время? Я вроде никому ничего не должен.


                                                                                                                                          Первое и второе — это разный опенсорс, для разных целей: первое — чтобы поделиться, второе — просто для упрощения моей жизни (намного проще брать собственный BSDL-код, чем объяснять заказчикам, почему на "вот это вот" исключительные права не передаются, и прописывать исключения в договор).

                                                                                                                                            0

                                                                                                                                            Ах, ну хотя бы тесты. Они вполне себе частичная замена документации.

                                                                                                                                              0

                                                                                                                                              Из меня фиговый писатель, мне проще кодом показать. :)

                                                                                                                                0

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

                                                                                                                            +2

                                                                                                                            Ну, значит напрашиваетесь на общественное порицание и реакцию "не буду даже разбираться, что это вообще такое, если автор ведет себя так неэтично, вряд ли там будет что-то хорошее".

                                                                                                                              0
                                                                                                                              > Ну, значит напрашиваетесь на общественное порицание

                                                                                                                              Если у вас много лишнего времени для доказательств как весь опенсорс обязан «обществу», то не для всех в обществе это так.
                                                                                                                                –2

                                                                                                                                В обществе хватает отсталых элементов, что есть, то есть, да.

                                                                                                                                  –2
                                                                                                                                  «Адекватный» ответ, ничего не скажешь. Дальнейшая дискуссия с вами действительно бессмысленна.
                                                                                                                        +1

                                                                                                                        (ответ выходит по тематике 2 части, зачем-то лезете вперёд)


                                                                                                                        В случае, если вы заказываете/оплачиваете разработку, вы можете выдвигать свои требования. Хоть спецификацию просить, хоть эталонную реализацию («хозяин — барин»).

                                                                                                                        Есть принятые в среде профессионалов и других участвующих в развитии Интернета нормы. Например, публикации RFC. И требование определенного уровня к оформлению оных.


                                                                                                                        Telegram LLC вправе сам решать, какие данные о своих разработках он будет (или не будет) публиковать.

                                                                                                                        А мы вправе сами решать, называть ли коричневую субстанцию фекалями.


                                                                                                                        Вся статья сквозит идеей о том, что Telegram кому-то чем-то обязаны. Знакомы с поговоркой «дарённому коню в зубы не смотрят»?

                                                                                                                        А с чего это Вы собственно решили, что не обязаны? Знакомы с поговоркой "Со свиным рылом да в калашный ряд"? Или, знакомы с понятием "общественный договор"? Человек (и контора) очень много чего обязан, начиная от безусловных вещей, типа уголовного кодекса, и заканчивая обусловленными областью деятельности. Например, я никому не обязан уметь рисовать, как Пикассо, или петь, как Шаляпин — но если я попытаюсь выдать себя за певца или художника, со мной вполне могут поступить очень неприятным образом, как с Остапом Бендером и Кисой Воробьяниновым в так и не успевшей стать столицей мира деревне. Улавливаете?


                                                                                                                        Вы ещё к WhatsApp и Facebook Messenger с такими требованиями обратитесь :)

                                                                                                                        Что-нибудь слышали о громадных антимонопольных штрафах, которые накладывали сначала на Microsoft, потом на Google? То, что большая бюрократическая машина тормозит, еще не означает, что эти ребята всё делают правильно и с них нельзя потребовать


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

                                                                                                                        Лучше остальных? Это напоминает анекдот "мне не надо бежать быстрее медведя, мне надо бежать быстрее тебя", примерно как сказать, что лето в Москве лучше Мурманска (даже позагорать можно).


                                                                                                                        А с чем сравнивали-то, что вот ну "прям точно лучше всех остальных"? Кругозор-то невелик, похоже? Кто эти остальные? Не считать же таковыми всё это мобильное хипстерское говно последнего десятилетия, типа WhatsApp, Viber, Facebook ?


                                                                                                                        Я к Telegram отношусь так — этот "йогурт с пониженным содержанием говна", настолько, что им уже хоть как-то стало можно пользоваться. То, что индустрия скатилась куда-то в жопу, и у конкурентов на двойку, не делает телегу на пятерку — так, три с плюсом. По второй части опишу и интерфейсные претензии, например, не только протокольные (в отличие от протокола, их хоть попытаюсь исправить в своем клиенте).


                                                                                                                        И за документацию, какой бы она ни была, и за открытый код клиентов — нужно в первую очередь сказать спасибо.

                                                                                                                        И только после этого конструктивно (без характеристик, типа «наркомания») рассуждать о технических недостатках протокола. Если бы не Telegram, то в XMPP и Matrix было бы совсем скучно, потому что остальные (Facebook, WhatsApp, Viber и другие) далеко не настолько дружелюбны.
                                                                                                                        P.S.: С вами трудно поддерживать конструктивный разговор. Пожалуйста, оставьте свои оценки (например — двойки) себе. Я считаю, что статья была бы значительно лучше, если бы техническая часть не была так перемешана с выплеском эмоций.

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


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