Критика на протокола и организационните подходи на Telegram. Част 1, техническа: опит в писането на клиент от нулата - TL, MT

Напоследък в Habré започнаха да се появяват по-често публикации за това колко добър е Telegram, колко брилянтни и опитни са братята Дурови в изграждането на мрежови системи и т.н. В същото време много малко хора наистина се потопиха в техническото устройство - най-много те използват доста прост (и много различен от MTProto) базиран на JSON Bot API и обикновено просто приемат на вярата всички онези похвали и PR, които се въртят около месинджъра. Преди почти година и половина моят колега от NPO Echelon Василий (за съжаление акаунтът му в Habré беше изтрит заедно с черновата) започна да пише собствен клиент Telegram от нулата в Perl, а по-късно се присъедини и авторът на тези редове. Защо Perl, веднага ще попитат някои? Защото вече има такива проекти на други езици.Всъщност не това е въпросът, може да има всеки друг език готова библиотека, и съответно авторът трябва да стигне докрай от нулата. Освен това криптографията е такова нещо - вярвай, но проверявай. С продукт, фокусиран върху сигурността, не можете просто да разчитате на готовата библиотека на доставчика и да му вярвате сляпо (все пак това е тема за повече във втората част). В момента библиотеката работи доста добре на „средно“ ниво (позволява ви да правите всякакви API заявки).

В тази поредица от публикации обаче няма да има много криптография и математика. Но ще има много други технически подробности и архитектурни патерици (ще бъде полезно и за тези, които няма да пишат от нулата, но ще използват библиотеката на всеки език). И така, основната цел беше да се опитаме да внедрим клиента от нулата според официалната документация. Тоест, да предположим, че изходният код на официалните клиенти е затворен (отново във втората част ще разкрием по-подробно темата какво всъщност е това се случва така), но, както в старите времена, например, има стандарт като RFC - възможно ли е да напишете клиент само според спецификацията, „без да надничате“ в изходния код, дори официален (Telegram Desktop, мобилен ), дори неофициален телетон?

Съдържание:

Документация ... има ли я? Вярно ли е?..

Фрагменти от бележки за тази статия започнаха да се събират миналото лято. През цялото това време на официалния сайт https://core.telegram.org документацията беше от слой 23, т.е. заседнал някъде през 2014 г. (помните ли, че тогава още нямаше дори канали?). Разбира се, на теория това трябваше да направи възможно внедряването на клиент с функционалност по това време през 2014 г. Но дори и в това състояние документацията беше, първо, непълна, и второ, на места си противоречише. Преди малко повече от месец, през септември 2019 г., беше случайно беше установено, че сайтът има голяма актуализация на документацията, за напълно нов слой 105, с бележка, че сега всичко трябва да се прочете отново. Наистина много статии са преработени, но много са останали непроменени. Ето защо, когато четете критиките по-долу относно документацията, трябва да имате предвид, че някои от тези неща вече не са актуални, но някои все още са доста. В крайна сметка 5 години в съвременния свят са не просто много, но много много. Оттогава (особено ако не вземете предвид изхвърлените и възкресени геочатове оттогава), броят на API методите в схемата е нараснал от сто на повече от двеста и петдесет!

Откъде започвате като млад писател?

Няма значение дали пишете от нулата или използвате например готови библиотеки като Telethon за Python или Madeline за PHP, във всеки случай първо ще ви трябва регистрирайте вашето приложение - вземете параметри api_id и api_hash (тези, които са работили с API на VKontakte, веднага разбират), чрез който сървърът ще идентифицира приложението. Това трябва да по правни причини, но ще говорим повече за това защо авторите на библиотеката не могат да го публикуват във втората част. Може би ще бъдете доволни от тестовите стойности, въпреки че са много ограничени - факт е, че сега можете да се регистрирате на вашия номер само един приложение, така че не бързайте стремглаво.

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

И ако пишете от нулата, тогава използването на получените параметри всъщност е все още далеч. Макар че https://core.telegram.org/ и говори за тях първо в Първи стъпки, всъщност първо трябва да внедрите MTProto протокол - но ако вярвате оформление според модела OSI в края на страницата на общото описание на протокола, тогава напълно напразно.

Всъщност както преди MTProto, така и след това, на няколко нива наведнъж (както казват чуждестранни мрежови специалисти, работещи в ядрото на ОС, нарушение на слоя), голяма, болезнена и ужасна тема ще попречи ...

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

Тази тема всъщност е ключът към проблемите на Telegram. И ще има много ужасни думи, ако се опитате да се задълбочите в това.

И така, схема. Ако си спомняте тази дума, кажете: JSON схемаПравилно си помислил. Целта е същата: някакъв език, който да опише възможен набор от предавани данни. Това всъщност е мястото, където приликата свършва. Ако от страницата 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;

Човек, който вижда това за първи път, интуитивно ще разпознае само част от написаното - добре, това са очевидно структури (макар че къде е името, отляво или отдясно?), В тях има полета, след които типът минава през дебелото черво ... вероятно. Тук, в ъглови скоби, вероятно има шаблони като в C ++ (всъщност, всъщност не). И какво означават всички други символи, въпросителни знаци, удивителни знаци, проценти, решетки (и очевидно те означават различни неща на различни места), присъстващи някъде, но не и някъде, шестнадесетични числа - и най-важното, как да получите от това прав (който няма да бъде отхвърлен от сървъра) байтов поток? Трябва да прочетете документацията (Да, има връзки към схемата в JSON версията наблизо - но това не я прави по-ясна).

Отваряне на страницата Сериализация на двоични данни и се потопете в магическия свят на гъбите и дискретната математика, нещо подобно на матан през 4-та година. Азбука, тип, стойност, комбинатор, функционален комбинатор, нормална форма, съставен тип, полиморфен тип... и това е само първата страница! Следващото ви очаква TL език, който, въпреки че вече съдържа пример за тривиална заявка и отговор, изобщо не дава отговор на по-типични случаи, което означава, че ще трябва да преминете през преразказа на математика, преведена от руски на английски на още осем вложени страници!

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

  • да, гол звучи добре, но уви не е постигнато
  • образованието в руските университети варира дори сред ИТ специалностите - не всеки чете съответния курс
  • В крайна сметка, както ще видим, на практика е така не се изисква, тъй като се използва само ограничено подмножество дори от описания TL

Както беше казано Леон Нерд на канала #perl в мрежата FreeNode IRC, опитвайки се да внедри порта от Telegram към Matrix (преводът на цитата е неточен по памет):

Усещането е като някой, който се е запознал с теорията на типа за първи път, развълнувал се е и е започнал да се опитва да си играе с нея, без да се интересува дали е необходимо на практика.

Вижте сами дали необходимостта от голи типове (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 в терминологията на редица езици. Всъщност, разбира се, тук сходство с реални претоварени методи на конструктор в OO езиците за програмиране. Тъй като тук има само структури от данни, няма методи (въпреки че описанието на функциите и методите по-долу е доста способно да създаде объркване в главата относно това какво представляват, но това е нещо друго) - можете да мислите за конструктор като за стойност, от която се изгражда тип при четене на поток от байтове.

как става това Десериализаторът, който винаги чете 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 е единственият случай, между другото, когато неподписаните числа се намират в реални схеми. И така, следващата е конструкция с въпросителен знак, което означава, че това е полето - то ще присъства в кабела само ако съответният бит е зададен в полето, към което се отнася (приблизително като троичен оператор). Така че, да предположим, че този бит е бил включен, тогава трябва да прочетете поле като Type, който в нашия пример има 2 конструктора. Единият е празен (състои се само от идентификатор), другият има поле ids с тип ids:Vector<long>.

Може би си мислите, че и шаблоните, и генериците са добри или Java. Но не. почти. Това единственото случай на ъглови скоби в реални вериги и се използва САМО за Vector. В поток от байтове това ще бъдат 4 CRC32 байта за самия тип Vector, винаги едни и същи, след това 4 байта - броят на елементите на масива и след това самите тези елементи.

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

Но не, ако прочетете не моето кратко описание, а пълната документация и помислете за изпълнението. Първо, CRC32 на конструктора се изчислява от нормализирания описателен низ на текста на схемата (премахване на допълнителни празни интервали и т.н.) - така че ако се добави ново поле, низът за описание на типа ще се промени, а оттам и неговият CRC32 и, следователно, сериализацията. И какво би направил старият клиент, ако получи поле с нови флагове, но не знае какво да прави с тях по-нататък? ..

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

Между другото, кой провери какво има наистина CRC32? В един от ранните източници (дори преди Уолтман) имаше хеш функция, която умножаваше всеки знак по числото 239, толкова обичано от тези хора, ха-ха!

Най-накрая, добре, разбрахме, че конструкторите с тип поле Vector<int> и Vector<PolymorType> ще има различен CRC32. А какво да кажем за презентацията на линия? И по отношение на теорията, става ли част от типа? Да кажем, че предаваме масив от десет хиляди числа, добре, с Vector<int> всичко е ясно, дължината и още 40000 XNUMX байта. И ако това Vector<Type2>, който се състои само от едно поле int и е единственият в типа - трябва ли да повторим 10000xabcdef0 34 пъти и след това 4 байта int, или езикът може да ПОКАЖЕ това за нас от конструктора fixedVec и вместо 80000 40000 байта, прехвърлете отново само XNUMX XNUMX?

Това изобщо не е празен теоретичен въпрос - представете си, че получавате списък с групови потребители, всеки от които има идентификатор, собствено име, фамилия - разликата в количеството данни, прехвърлени през мобилна връзка, може да бъде значителна. Това е ефективността на сериализацията на Telegram, която ни се рекламира.

Така…

Вектор, който не може да бъде изведен

Ако се опитате да преминете през страниците с описание на комбинатори и наоколо, ще видите, че вектор (и дори матрица) формално се опитва да изведе няколко листа чрез кортежи. Но в крайна сметка те се забиват, последната стъпка се пропуска и просто се дава дефиницията на вектор, която също не е обвързана с тип. Какво има тук? На езици програмиране, особено функционални, е доста типично структурата да се опише рекурсивно - компилаторът с неговата мързелива оценка ще разбере всичко и ще го направи. В езика сериализация на данни но е необходима ЕФЕКТИВНОСТ: достатъчно е просто да се опише списък, т.е. структура от два елемента - първият е елемент от данни, вторият е самата същата структура или празно място за опашката (pack (cons) в Lisp). Но това очевидно ще изисква всеки елемент допълнително изразходва 4 байта (CRC32 в случай на TL), за да опише своя тип. Лесно е да се опише масив фиксиран размер, но в случай на масив с неизвестна преди това дължина, прекъсваме.

Тъй като TL не ви позволява да изведете вектор, той трябваше да бъде добавен отстрани. В крайна сметка документацията казва:

Сериализацията винаги използва един и същ конструктор „вектор“ (const 0x1cb5c415 = crc32(„vector t:Type # [ t ] = Vector t“), който не зависи от конкретната стойност на променливата от тип t.

Стойността на незадължителния параметър t не участва в сериализацията, тъй като се извлича от типа резултат (винаги известен преди десериализацията).

Погледни отблизо: vector {t:Type} # [ t ] = Vector t - но никъде в самата дефиниция не пише, че първото число трябва да е равно на дължината на вектора! И не следва отникъде. Това е даденост, която трябва да имате предвид и да прилагате с ръцете си. На друго място в документацията дори честно се споменава, че типът е фалшив:

Полиморфният псевдотип Vector t е „тип“, чиято стойност е поредица от стойности от произволен тип t, или в кутия, или без.

… но не се фокусира върху него. Когато вие, уморени от разтягане на математиката (може би дори позната ви от университетски курс), решите да оцените и да гледате как всъщност да работите с нея на практика, впечатлението остава в главата ви: тук Сериозната математика се основава на , явно Cool People (двама математици - победители в ACM), а не кой да е. Целта - да се изръсим - е постигната.

Между другото, за броя. Припомням си # това е синоним nat, естествено число:

Има типови изрази (typeexpr) и числови изрази (nat-expr). Те обаче се определят по същия начин.

type-expr ::= expr
nat-expr ::= expr

но в граматиката те са описани по същия начин, т.е. тази разлика отново трябва да се запомни и да се въведе в изпълнението на ръка.

Е, да, типове шаблони (vector<int>, vector<User>) имат общ идентификатор (#1cb5c415), т.е. ако знаете, че повикването е декларирано като

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

тогава вие чакате не просто вектор, а вектор от потребители. По-точно, задължително изчакайте - в реалния код всеки елемент, ако не е гол тип, ще има конструктор и в добрия смисъл при изпълнението ще е необходимо да се провери - и ние бяхме изпратени точно във всеки елемент от този вектор този тип? И ако беше някакъв PHP, в който масивът може да съдържа различни типове в различни елементи?

В този момент започвате да се чудите - необходим ли е такъв TL? Може би за количката би било възможно да се използва човешкият сериализатор, същият protobuf, който вече съществуваше тогава? Това беше теория, нека да видим практиката.

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

TL се роди в недрата на VKontakte още преди добре познатите събития с продажбата на дела на Дуров и (сигурно), още преди разработването на 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>;

Това е дефиницията на типа шаблон на hashmap, като вектор от двойки тип int. В C++ ще изглежда нещо подобно:

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

така, alpha - ключова дума! Но само в С++ може да се пише Т, но трябва да се пише алфа, бета... Но не повече от 8 параметъра, фантазията приключи на тета. Така че изглежда, че някога в Санкт Петербург е имало приблизително такива диалози:

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

-- Смотрите, какой я синтаксис придумал для шаблонов и вектора!
-- Ты долбанулся, как мы это парсить будем?
-- Да не ссыте, он там один в схеме, захаркодить -- и ок

Но става дума за първото изложено изпълнение на TL "като цяло". Нека да преминем към разглеждането на имплементациите в действителните клиенти на Telegram.

Дума на Василий:

Василий, [09.10.18 17:07] Най-вече дупето е горещо от факта, че прецакаха куп абстракции, а след това им забиха болт и наложиха кодегератора с патерици
В резултат на това първо от доковете pilot.jpg
След това от кода jekichan.webp

Разбира се, от хора, запознати с алгоритми и математика, можем да очакваме, че са чели Aho, Ullman и са запознати с де факто индустриалните стандартни инструменти за писане на техните DSL компилатори през десетилетията, нали? ..

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

16.12 04:18 Василий: според мен някой не е усвоил lex + yacc
16.12 04:18 Василий: иначе не мога да го обясня
16.12 04:18 Василий: добре, или им е платено за броя на линиите във VK
16.12 04:19 Василий: 3k+ реда на др<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+ реда в Python, няколко регулярни израза + специални случаи на векторния тип, който, разбира се, е деклариран в схемата, както трябва да бъде според синтаксиса на TL, но те го поставят в този синтаксис, анализират го повече ... Въпросът е защо да се занимаваме с цялото това чудоиповече пуф, ако така или иначе никой няма да го анализира според документацията?!

Между другото... Спомняте ли си, че говорихме за проверката CRC32? И така, в генератора на кодове на Telegram Desktop има списък с изключения за тези типове, в които изчисленият CRC32 не съответства както е показано на диаграмата!

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

Запомнете точката за едноредовите, ще се върнем към нея малко по-късно.

Добре, telegram-cli е неофициален, Telegram Desktop е официален, но какво да кажем за другите? И кой знае?.. В кода на клиента на Android изобщо нямаше парсер на схема (което повдига въпроси относно отворения код, но това е за втората част), но имаше няколко други смешни парчета код, но за тях в подраздела по-долу.

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

Василий: flags.0? true
означава, че полето присъства и е вярно, ако флагът е зададен

Василий: flags.1? int
означава, че полето присъства и трябва да бъде десериализирано

Василий: Дупе, не гори, какво правиш!
Василий: Някъде в документа се споменава, че true е гол тип с нулева дължина, но е нереалистично да се събере нещо от техните документи
Василий: В отворените изпълнения също няма такова нещо, но има много патерици и подпори

Какво ще кажете за телетон? Гледайки напред по темата за MTProto, пример - има такива парчета в документацията, но знакът % описва се само като "отговарящ на дадения гол тип", т.е. в примерите по-долу или грешка, или нещо недокументирано:

Василий, [22.06.18/18/38 XNUMX:XNUMX] На едно място:

msg_container#73f1f8dc messages:vector message = MessageContainer;

В различен:

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

И това са две големи разлики, в реалния живот идва някакъв вид гол вектор

Не съм виждал голи векторни дефиниции и не съм срещал

Анализ, написан на ръка в телетон

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

Отново остава въпросът за %. Не е описано.

Вадим Гончаров, [22.06.18/19/22 XNUMX:XNUMX] и в tdesktop?

Василий, [22.06.18/19/23 XNUMX:XNUMX] Но техният 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
                [wd<>#.?!]+  # 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
            ([wd<>#.?]+)     # 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. Въз основа на него езикът трябва да предоставя всякакви проверки за нас. Е, добре, нека не той, а изпълнението, но поне да ги опише. И какви възможности искаме?

На първо място, ограничения. Тук виждаме в документацията за качване на файлове:

След това двоичното съдържание на файла се разделя на части. Всички части трябва да имат еднакъв размер ( part_size ) и трябва да бъдат изпълнени следните условия:

  • part_size % 1024 = 0 (делимо на 1KB)
  • 524288 % part_size = 0 (512KB трябва да се дели равномерно на part_size)

Последната част не трябва да отговаря на тези условия, при условие че нейният размер е по-малък от part_size.

Всяка част трябва да има пореден номер, файл_част, със стойност в диапазона от 0 до 2,999.

След като файлът е разделен, трябва да изберете метод за запазването му на сървъра. използване upload.saveBigFilePart в случай, че пълният размер на файла е повече от 10 MB и upload.saveFilePart за по-малки файлове.
[…] може да бъде върната една от следните грешки при въвеждане на данни:

  • FILE_PARTS_INVALID - Невалиден брой части. Стойността не е между 1..3000

Има ли някои от тях в схемата? Изразимо ли е по някакъв начин чрез TL? Не. Но извинете, дори старомодният Turbo Pascal успя да опише типовете, дадени от диапазони. И можеше да направи още нещо, сега по-известно като enum - тип, състоящ се от изброяване на фиксиран (малък) брой стойности. В езици като C - числови, имайте предвид, говорихме само за типове досега. численост. Но има и масиви, низове ... например би било хубаво да се опише, че този низ може да съдържа само телефонен номер, нали?

Нищо от това не е в TL. Но има, например, в JSON Schema. И ако някой друг може да възрази относно делимостта на 512 KB, че това все още трябва да се провери в кода, тогава се уверете, че клиентът просто не можех изпрати номер извън обхват 1..3000 (и съответната грешка не можеше да възникне) би било възможно, нали? ..

Между другото, относно грешките и връщаните стойности. Окото е замъглено дори за тези, които са работили с TL - това не ни хрумна веднага всеки един функция в TL всъщност може да върне не само описания тип връщане, но и грешка. Но това не може да се изведе чрез самия TL. Разбира се, това е разбираемо и nafig не е необходимо на практика (въпреки че всъщност RPC може да се направи по различни начини, ще се върнем към това) - но какво да кажем за чистотата на понятията на математиката на абстрактните типове от небесния свят? .. Грабна влекача - така мач.

И накрая, какво ще кажете за четливостта? Е, там, като цяло, бих искал описание има го точно в схемата (отново е в 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);

С други думи, тук се извършва сериализацията РЪЧНО, а не генериран код! Може би сървърът е реализиран по подобен начин?.. По принцип това ще работи, ако се направи веднъж, но как да го поддържате после с актуализации? Нали за това беше схемата? И тогава преминаваме към следващия въпрос.

Версиониране. Слоеве

Защо версиите на схемата се наричат ​​слоеве, може да се гадае само въз основа на историята на публикуваните схеми. Очевидно първоначално на авторите им се е струвало, че основните неща могат да се правят по непроменена схема и само когато е необходимо, посочвайте на конкретни заявки, че се извършват според различна версия. По принцип дори добра идея - и новото ще се „смеси“, ще се наслои върху старото. Но нека да видим как е направено. Вярно е, че не беше възможно да се търси от самото начало - смешно е, но схемата на базовия слой просто не съществува. Слоевете започват на 2. Документацията ни казва за специална TL функция:

Ако клиентът поддържа слой 2, тогава трябва да се използва следният конструктор:

invokeWithLayer2#289dd1f6 {X:Type} query:!X = X;

На практика това означава, че преди всяко API извикване, int със стойността 0x289dd1f6 трябва да се добави преди номера на метода.

Звучи добре. Но какво се случи след това? Тогава дойде

invokeWithLayer3#b7475268 query:!X = X;

И така, какво следва? Както е лесно да се досетите

invokeWithLayer4#dea0d430 query:!X = X;

Забавен? Не, рано е да се смееш, помисли за какво всеки заявка от друг слой трябва да бъде увита в такъв специален тип - ако имате всички различни, как иначе да ги различите? И добавянето само на 4 байта отпред е доста ефективен метод. Така

invokeWithLayer5#417a57ae query:!X = X;

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

Актуализация: Започвайки със слой 9, помощни методи invokeWithLayerN може да се използва заедно с initConnection

Ура! След 9 версии най-накрая стигнахме до това, което беше направено в интернет протоколите през 80-те години - договаряне на версия веднъж в началото на връзката!

И така, какво следва?..

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

И сега можете да се смеете. Едва след още 9 слоя най-накрая беше добавен универсален конструктор с номер на версия, който трябва да се извика само веднъж в началото на връзката и смисълът в слоевете изглежда изчезна, сега е само условна версия, като навсякъде другаде. Проблема решен.

нали?..

Василий, [16.07.18/14/01 XNUMX:XNUMX] В петък си помислих:
Телесървърът изпраща събития без заявка. Заявките трябва да бъдат обвити в InvokeWithLayer. Сървърът не обвива актуализации, няма структура за обвиване на отговори и актуализации.

Тези. клиентът не може да посочи слоя, в който иска актуализации

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX PM] InvokeWithLayer не е ли патерица по принцип?

Василий, [16.07.18 14:02] Това е единственият начин

Вадим Гончаров, [16.07.18/14/02 XNUMX:XNUMX], което по същество трябва да означава наслояване в началото на сесията

Между другото, от това следва, че не се предоставя понижаване на клиентска версия

Актуализации, т.е. Тип Updates в схемата това е, което сървърът изпраща на клиента не в отговор на API заявка, а самостоятелно, когато възникне събитие. Това е сложна тема, която ще бъде обсъдена в друга публикация, но засега е важно да знаете, че сървърът натрупва актуализации дори когато клиентът е офлайн.

По този начин при отказ от увиване всеки пакет, за да посочи неговата версия, следователно логично възникват следните възможни проблеми:

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

Смятате ли, че това е чисто теоретично мислене, а на практика това няма как да се случи, защото сървърът е написан правилно (във всички случаи е тестван добре)? ха! Без значение как!

Точно на това се натъкнахме през август. На 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 дистрибуции), той записва в регистъра на изключения: MTP Unexpected type id #b5223b0f прочетено в MTPMessageMedia…

Критика на протокола и организационните подходи на Telegram. Част 1, техническа: опит в писането на клиент от нулата - TL, MT

Google показа, че подобен проблем вече се е случил с един от неофициалните клиенти, но тогава номерата на версиите и съответно предположенията бяха различни ...

И така, какво да правя? Василий и аз се разделихме: той се опита да актуализира схемата до 91, реших да изчакам няколко дни и да опитам до 73. И двата метода проработиха, но тъй като те са емпирични, няма разбиране колко версии трябва да скочите нагоре или надолу, нито колко дълго трябва да чакате.

По-късно успях да възпроизведа ситуацията: стартираме клиента, изключваме го, прекомпилираме схемата на друг слой, рестартираме, хващаме проблема отново, връщаме се към предишния - опа, без превключване на схемата и рестартиране на клиента за няколко минути ще помогнат. Ще получите комбинация от структури от данни от различни слоеве.

Обяснение? Както можете да предположите от различните индиректни симптоми, сървърът се състои от много различни типове процеси на различни машини. Най-вероятно този от сървърите, който отговаря за "буферирането", е поставил в опашката това, което са му дали по-горните и те са го дали в схемата, която е била по време на генерирането. И докато тази опашка не беше „изгнила“, нищо не можеше да се направи по въпроса.

Освен ако... но това е ужасна патерица?!.. Не, преди да мислим за луди идеи, нека погледнем кода на официалните клиенти. Във версията за Android не намираме никакъв TL анализатор, но намираме тежък файл (github отказва да го оцвети) с (де)сериализация. Ето кодовите фрагменти:

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;

Този „ръчно създаден“ коментар тук предполага, че само част от този файл е написан на ръка (можете ли да си представите кошмара за поддръжка?), а останалата част е машинно генерирана. Тогава обаче възниква друг въпрос - че източниците са налични не напълно (a la blobs под GPL в ядрото на Linux), но това вече е тема за втората част.

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

MT Proto

Така че нека отворим общо описание и подробно описание на протокола и първото нещо, в което се спъваме, е терминологията. И с изобилие от всичко. Като цяло изглежда, че това е търговска марка на Telegram - да наричате нещата на различни места по различни начини или различни неща с една дума или обратното (например в API на високо ниво, ако видите пакет със стикери - това не е това, което си мислите).

Например "съобщение" (съобщение) и "сесия" (сесия) - тук те означават нещо различно, отколкото в обичайния интерфейс на клиента на Telegram. Е, всичко е ясно със съобщението, може да се тълкува от гледна точка на OOP или просто да се нарече думата „пакет“ - това е ниско, транспортно ниво, няма същите съобщения като в интерфейса, има много на сервизните. Но сесията ... но първо най-важното.

Транспортен слой

Първото нещо е транспортът. Те ще ни кажат около 5 опции:

  • TCP
  • Уебсокет
  • Websocket през HTTPS
  • HTTP
  • HTTPS

Василий, [15.06.18/15/04 XNUMX:XNUMX] А има и UDP транспорт, но не е документиран

И TCP в три варианта

Първият е подобен на UDP през TCP, всеки пакет включва пореден номер и crc
Защо е толкова болезнено да четеш докове на количка?

Ами там сега TCP вече в 4 варианта:

  • Съкратен
  • Междинен
  • Подплатен междинен
  • Пълен

Добре, подплатен междинен продукт за MTProxy, това беше добавено по-късно поради известни събития. Но защо още две версии (общо три), когато може и една? И четирите по същество се различават само по това как да зададете дължината и полезния товар на действителния основен MTProto, което ще бъде обсъдено по-нататък:

  • в Съкратено това е 1 или 4 байта, но не и 0xef след това тялото
  • в Intermediate това са 4 байта дължина и поле и първият път, когато клиентът трябва да изпрати 0xeeeeeeee за да покаже, че е междинен
  • изцяло, най-пристрастяващото от гледна точка на мрежовия: дължина, пореден номер, а НЕ ТОЗИ, който е основно MTProto, тяло, CRC32. Да, всичко това през TCP. Което ни осигурява надежден транспорт под формата на сериен поток от байтове, не са необходими последователности, особено контролни суми. Добре, сега ще ми възразят, че TCP има 16-битова контролна сума, така че се случва повреда на данните. Страхотно, освен че всъщност имаме криптографски протокол с хешове, по-дълги от 16 байта, всички тези грешки - и дори повече - ще бъдат уловени от несъответствие на SHA на по-високо ниво. В CRC32 няма смисъл от това.

Нека сравним Abridged, където е възможен един байт дължина, с Intermediate, което оправдава „В случай, че е необходимо подравняване на 4-байтови данни“, което е доста глупост. Какво, смята се, че програмистите на Telegram са толкова тромави, че не могат да четат данни от сокета в подравнен буфер? Все още трябва да направите това, защото четенето може да ви върне произволен брой байтове (а има и прокси сървъри, например ...). Или, от друга страна, защо да се занимаваме с Abridged, ако все още имаме солидни подложки от 16 байта отгоре - спестете 3 байта понякога ?

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

Други възможности за транспорт, вкл. Web и MTProxy, няма да разглеждаме сега, може би в друга публикация, ако има заявка. Сега само ще припомним за този MTProxy, че скоро след пускането му през 2018 г. доставчиците бързо се научиха да блокират точно него, предназначен за блок байпасОт размер на пакета! А също и фактът, че MTProxy сървърът, написан (отново от Waltman) на C, беше ненужно обвързан със спецификата на Linux, въпреки че изобщо не беше необходим (Фил Кулин ще потвърди) и че подобен сървър или на Go, или на Node.js побира по-малко от сто реда.

Но ние ще направим изводи за техническата грамотност на тези хора в края на раздела, след като разгледаме други въпроси. Засега да преминем към 5-ти OSI слой, сесия - на който са поставили MTProto сесията.

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

Те го поставят там не съвсем правилно ... Сесията не е същата сесия, която се вижда в интерфейса под Активни сесии. Но по ред.

Критика на протокола и организационните подходи на Telegram. Част 1, техническа: опит в писането на клиент от нулата - TL, MT

Тук получихме низ от байтове с известна дължина от транспортния слой. Това е или криптирано съобщение, или обикновен текст - ако все още сме на етапа на ключови преговори и всъщност го правим. За кое от групата понятия, наречени "ключ", говорим? Нека изясним този въпрос за самия екип на Telegram (извинявам се, че преведох собствената си документация от английски на или уморен мозък в 4 сутринта, беше по-лесно да оставя някои фрази такива, каквито са):

Има две образувания, т.нар сесия - един в потребителския интерфейс на официалните клиенти под "текущи сесии", където всяка сесия съответства на цяло устройство / ОС.
Втората е MTProto сесия, който има пореден номер на съобщение (в смисъл на ниско ниво) в него и който може да продължи между различни TCP връзки. Няколко MTProto сесии могат да бъдат настроени едновременно, например, за да се ускори изтеглянето на файлове.

Между тези двамата сесии е концепцията упълномощаване. В изродения случай може да се каже така UI сесия е същото като упълномощаванеНо уви, това е сложно. Ние гледаме:

  • Потребителят на новото устройство първо генерира auth_key и го обвързва с акаунт, например чрез SMS - затова упълномощаване
  • Това се случи вътре в първия MTProto сесия, който има session_id вътре в себе си.
  • На тази стъпка комбинацията упълномощаване и session_id може да бъде назован инстанция - тази дума се среща в документацията и кода на някои клиенти
  • След това клиентът може да отвори малко MTProto сесии под същите auth_key - към същия DC.
  • Тогава един ден клиентът трябва да поиска файл от друг DC - и за този DC ще бъде генериран нов auth_key !
  • Да каже на системата, че не се регистрира нов потребител, а същият упълномощаване (UI сесия), клиентът използва API извиквания auth.exportAuthorization в дома DC auth.importAuthorization в новия ДК.
  • Все пак може да има няколко отворени MTProto сесии (всеки със своето session_id) към този нов DC, под негов auth_key.
  • И накрая, клиентът може да иска Perfect Forward Secrecy. Всеки auth_key Това беше постоянен ключ - за DC - и клиентът може да се обади auth.bindTempAuthKey за използване временен auth_key - и пак само един temp_auth_key за DC, общ за всички MTProto сесии към този DC.

Имайте предвид, че сол (и бъдещи соли) също един на auth_key тези. споделено между всички MTProto сесии към същия DC.

Какво означава "между различни TCP връзки"? Това означава, че това нещо като бисквитка за оторизация на уебсайт - тя продължава (оцелява) много TCP връзки към този сървър, но един ден ще се повреди. Само за разлика от HTTP, в MTProto, вътре в сесията, съобщенията са последователно номерирани и потвърдени, влезли са в тунела, връзката е прекъсната - след установяване на нова връзка, сървърът любезно ще изпрати всичко в тази сесия, което не е доставил в предишна TCP връзка.

Информацията по-горе обаче е изцедена след многомесечно съдебно дело. Междувременно внедряваме ли нашия клиент от нулата? - да се върнем в началото.

Така че генерираме auth_key на версии на Diffie-Hellman от Telegram. Нека се опитаме да разберем документацията...

Василий, [19.06.18/20/05 1:255] data_with_hash := SHAXNUMX(данни) + данни + (всякакви произволни байтове); такава, че дължината е равна на XNUMX байта;
криптирани_данни := RSA(данни_с_хеш, сървър_публичен_ключ); 255-байтово число (big endian) се повишава до необходимата степен над необходимия модул и резултатът се съхранява като 256-байтово число.

Имат някаква дрога DH

Не прилича на DH на здрав човек
В dx няма два публични ключа

Е, в крайна сметка се разбрахме, но утайката остана - доказателство за свършена работа от клиента, че е успял да разложи числото. Тип защита срещу DoS атаки. И RSA ключът се използва само веднъж в една посока, основно за криптиране new_nonce. Но докато тази на пръв поглед проста операция успее, с какво ще трябва да се сблъскате?

Василий, [20.06.18/00/26 XNUMX:XNUMX] Още не съм стигнал до заявката за приложение

Изпратих запитване до DH

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

Е, той ми каза -404, какво от това?

Ето ме към него: „хванете ефигната си криптирана със сървърния ключ с пръстов отпечатък на такъв и такъв, искам DH“, и той отговаря глупаво 404

Какво бихте мислили за такъв отговор на сървъра? Какво да правя? Няма кого да попитате (но повече за това във втората част).

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

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

Две 32 битови числа. Опаковах ги като всички останали

Но не, точно тези две ви трябват първи в ред като BE

Вадим Гончаров, [20.06.18/15/49 404:XNUMX PM] и поради това XNUMX?

Василий, [20.06.18 15:49] ДА!

Вадим Гончаров, [20.06.18/15/50 XNUMX:XNUMX] така че не разбирам какво може да „не намери“

Василий, [20.06.18 15:50] около

Не намерих такова разлагане на прости делители%)

Дори докладването за грешки не беше овладяно

Василий, [20.06.18/20/18 5:XNUMX] А, има и MDXNUMX. Вече три различни хеша

Ключовият пръстов отпечатък се изчислява, както следва:

digest = md5(key + iv)
fingerprint = substr(digest, 0, 4) XOR substr(digest, 4, 4)

SHA1 и sha2

Така че нека поставим auth_key 2048 бита в размер, който получихме според Diffie-Hellman. Какво следва? След това откриваме, че долните 1024 бита на този ключ не се използват по никакъв начин ... но нека помислим за това засега. На тази стъпка имаме споделена тайна със сървъра. Създаден е аналог на TLS сесия, много скъпа процедура. Но сървърът все още не знае нищо за това кои сме! Всъщност още не оторизация. Тези. ако мислите от гледна точка на „парола за вход“, както беше в ICQ, или поне „ключ за вход“, както в SSH (например в някои gitlab / github). Станахме анонимни. И ако сървърът ни отговори "тези телефонни номера се обслужват от друг DC"? Или дори „телефонният ви номер е забранен“? Най-доброто, което можем да направим, е да запазим ключа с надеждата, че ще бъде все още полезен и няма да се развали дотогава.

Между другото, ние го "приехме" с резерви. Например, имаме ли доверие на сървъра? Той фалшив ли е? Имаме нужда от криптографски проверки:

Василий, [21.06.18/17/53 2:XNUMX] Те предлагат на мобилни клиенти да проверят XNUMXkbit номер за простота%)

Но изобщо не е ясно, нафейхоа

Василий, [21.06.18/18/02 XNUMX:XNUMX] Докът не казва какво да прави, ако се окаже, че не е просто

Не е казано. Да видим какво прави официалният клиент за Android в този случай? А това е което (и да, там целият файл е интересен) - както се казва, просто ще го оставя тук:

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

Не, разбира се там някои има проверки за простота на число, но лично аз вече нямам достатъчно познания по математика.

Добре, имаме главния ключ. За да влезете, т.е. изпращане на заявки, е необходимо да се извърши допълнително криптиране, като вече се използва AES.

Ключът на съобщението се дефинира като 128 средни бита на SHA256 на тялото на съобщението (включително сесия, идентификатор на съобщение и т.н.), включително байтовете за допълване, предварени от 32 байта, взети от ключа за оторизация.

Василий, [22.06.18/14/08 XNUMX:XNUMX PM] Средни кучки

Получил auth_key. Всичко. По-нататък ги ... не става ясно от доковете. Чувствайте се свободни да изучавате кода с отворен код.

Обърнете внимание, че MTProto 2.0 изисква от 12 до 1024 байта запълване, като все още е предмет на условието дължината на полученото съобщение да се дели на 16 байта.

И така, колко подложка да поставите?

И да, тук също 404 в случай на грешка

Ако някой внимателно проучи диаграмата и текста на документацията, забеляза, че там няма MAC. И този AES се използва в някакъв IGE режим, който не се използва никъде другаде. Те, разбира се, пишат за това в техните FAQ... Тук, например, самият ключ на съобщението е в същото време SHA хеш на декриптираните данни, използвани за проверка на целостта - и в случай на несъответствие, документацията за някаква причина препоръчва мълчаливото им игнориране (но какво да кажем за сигурността, внезапно да ни разбие?).

Аз не съм криптограф, може би в този режим в случая няма нищо лошо от теоретична гледна точка. Но определено мога да назова практически проблем, използвайки примера на Telegram Desktop. Той криптира локалния кеш (всички тези D877F783D5D3EF8C) по същия начин като съобщенията в MTProto (само в този случай версия 1.0), т.е. първо ключа на съобщението, след това самите данни (и някъде встрани основния big auth_key 256 байта, без които msg_key безполезен). Така проблемът става забележим при големи файлове. А именно, трябва да пазите две копия на данните – криптирано и декриптирано. И ако има мегабайти или поточно видео, например? .. Класическите схеми с MAC след шифрования текст ви позволяват да го четете поточно, незабавно го прехвърляте. И с MTProto трябва в началото шифровайте или декриптирайте цялото съобщение, едва след това го прехвърлете в мрежата или на диск. Следователно в най-новите версии на Telegram Desktop в кеша в user_data вече се използва друг формат - с AES в CTR режим.

Василий, [21.06.18/01/27 20:XNUMX AM] О, разбрах какво е IGE: IGE беше първият опит за „режим на криптиране за удостоверяване“, първоначално за Kerberos. Това беше неуспешен опит (не осигурява защита на целостта) и трябваше да бъде премахнат. Това беше началото на XNUMX-годишно търсене на работещ режим на криптиране за удостоверяване, което наскоро достигна кулминация в режими като OCB и GCM.

А сега аргументите от страната на количката:

Екипът зад Telegram, ръководен от Николай Дуров, се състои от шестима шампиони на ACM, половината от които доктори по математика. Отне им около две години, за да пуснат текущата версия на MTProto.

Какво е смешно. Две години до по-ниско ниво

Или можете просто да вземете tls

Добре, да кажем, че сме направили криптиране и други нюанси. Можем ли най-накрая да изпратим TL-сериализирани заявки и да десериализираме отговорите? И така, какво трябва да се изпрати и как? Ето метода initConnectionможе би това е?

Василий, [25.06.18/18/46 XNUMX:XNUMX] Инициализира връзката и запазва информацията на устройството и приложението на потребителя.

Приема app_id, device_model, system_version, app_version и lang_code.

И малко запитване

Документация както винаги. Чувствайте се свободни да изучавате отворения код

Ако всичко беше приблизително ясно с invokeWithLayer, тогава какво е това? Оказва се, че да предположим, че имаме - клиентът вече имаше нещо, за което да попита сървъра - има заявка, която искахме да изпратим:

Василий, [25.06.18/19/13 XNUMX:XNUMX] Съдейки по кода, първото обаждане е опаковано в този боклук, а самият боклук е в invokewithlayer

Защо initConnection не може да бъде отделно извикване, а трябва да бъде обвивка? Да, както се оказа, трябва да се прави всеки път в началото на всяка сесия, а не еднократно, както е с основния ключ. Но! Не може да бъде извикан от неоторизиран потребител! Ето, че стигнахме до етапа, в който е приложим този страница с документация - и тя ни казва, че...

Само малка част от методите на API са достъпни за неоторизирани потребители:

  • 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? Чувам възражение: „как можете да сте сигурни, че RKN не отговаря с фалшиви адреси?“ За това припомняме, че всъщност в официалните клиенти вградени RSA ключове, т.е. можете просто абонирам тази информация. Всъщност това вече е направено за информация за заобикаляне на ключалки, които клиентите получават по други канали (логично е това да не може да се направи в самия MTProto, защото все пак трябва да знаете къде да се свържете).

ДОБРЕ. На този етап от оторизацията на клиента ние все още не сме оторизирани и не сме регистрирали нашето приложение. Засега просто искаме да видим какво реагира сървърът на методите, достъпни за неоторизиран потребител. И тук…

Василий, [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? Нищо изненадващо:

Василий, [28.06.18/02/04 2:XNUMX AM] Мм, ровят се из някои от алгоритмите на eXNUMXe

Mtproto дефинира алгоритми за криптиране и ключове за двата домейна, както и малко структура на обвивка

Но те постоянно смесват различни нива на стека, така че не винаги е ясно къде свършва mtproto и започва следващото ниво.

Как се смесват? Е, тук е същият временен ключ за PFS, например (между другото, Telegram Desktop не знае как да го направи). Изпълнява се чрез API заявка auth.bindTempAuthKey, т.е. от най-високото ниво. Но в същото време пречи на криптирането на по-ниско ниво - след него, например, трябва да го направите отново initConnection и т.н., това не е само нормална заявка. Отделно, също така предоставя, че можете да имате само ЕДИН временен ключ на DC, въпреки че полето auth_key_id във всяко съобщение ви позволява да промените ключа поне на всяко съобщение и че сървърът има право да „забрави“ временния ключ по всяко време - какво да правите в този случай, документацията не казва ... добре, защо не би било възможно да има няколко ключа, както при набор от бъдещи соли, но?..

Има няколко други неща, които си струва да се отбележат в темата MTProto.

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

Защо трябва да знаете за тях? Защото те "изтичат" едно ниво по-нагоре и трябва да знаете за тях, когато работите с API. Да предположим, че не се интересуваме от msg_key, по-ниското ниво декриптира всичко вместо нас. Но вътре в дешифрираните данни имаме следните полета (също дължината на данните, за да знаем къде е подложката, но това не е важно):

  • сол-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? О, това е труден въпрос. Опитайте се честно да разберете какво имате предвид:

Съобщение, свързано със съдържание

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

Пореден номер на съобщението (msg_seqno)

32-битово число, равно на два пъти броя на „свързаните със съдържанието“ съобщения (тези, които изискват потвърждение, и по-специално тези, които не са контейнери), създадени от подателя преди това съобщение и впоследствие увеличени с единица, ако текущото съобщение е съобщение, свързано със съдържание. Контейнер винаги се генерира след цялото му съдържание; следователно неговият пореден номер е по-голям или равен на поредните номера на съобщенията, съдържащи се в него.

Какъв цирк е това с увеличение от 1 и след това още 2? .. Подозирам, че първоначалното значение беше „малък бит за ACK, останалото е число“, но резултатът не е съвсем правилен - по-специално, оказва се, че може да се изпрати малко потвърждения, които имат същото seq_no! как? Е, например, сървърът ни изпраща нещо, изпраща, а ние самите мълчим, отговаряме само със съобщения за потвърждение на услугата за получаване на неговите съобщения. В този случай нашите изходящи потвърждения ще имат същия изходящ номер. Ако сте запознати с TCP и смятате, че това звучи някак налудничаво, но изглежда не е много диво, защото в TCP seq_no не се променя и потвърждението отива на seq_no другата страна - тогава бързам да разстроя. Пристигат потвърждения към MTProto НЕ на seq_no, както в TCP, но msg_id !

Какво е това msg_id, най-важното от тези полета? Уникалният идентификатор на съобщението, както подсказва името. Дефинира се като 64-битово число, чиито най-младши битове отново имат магия на сървъра, а не на сървъра, а останалото е времево клеймо на Unix, включително дробната част, изместено с 32 бита наляво. Тези. timestamp per se (и съобщенията с твърде различно време ще бъдат отхвърлени от сървъра). От това излиза, че в общи линии това е идентификатор, който е глобален за клиента. Докато - помнете session_id - ние сме гарантирани: При никакви обстоятелства съобщение, предназначено за една сесия, не може да бъде изпратено в друга сесия. Тоест, оказва се, че вече има три ниво — сесия, номер на сесия, идентификатор на съобщение. Защо такова свръхусложняване, тази мистерия е много голяма.

По този начин, msg_id необходими за…

RPC: заявки, отговори, грешки. Потвърждения.

Както може би сте забелязали, никъде в схемата няма специален тип или функция „направи RPC заявка“, въпреки че има отговори. Все пак имаме съобщения, свързани със съдържание! Това е, който и да е съобщението може да бъде заявка! Или да не бъде. След всичко, всеки има msg_id. А ето и отговорите:

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

Тук се посочва на кое съобщение е отговорът. Следователно на най-високото ниво на API ще трябва да запомните какъв номер е имала вашата заявка - мисля, че не е необходимо да обяснявам, че работата е асинхронна и може да има няколко заявки едновременно, отговорите на които могат да бъдат върнати в произволен ред? По принцип от това и съобщенията за грешка като no works може да се проследи архитектурата зад това: сървърът, който поддържа 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 грешки (е, разбира се, семантиката на отговорите не се спазва, на някои места те се разпределят по кодове на случаен принцип), а низът изглежда така CAPITAL_LETTERS_AND_NUMBERS. Например PHONE_NUMBER_OCCUPIED или FILE_PART_X_MISSING. Е, това е, все още имате тази линия анализирам, Например FLOOD_WAIT_3600 ще означава, че трябва да изчакате един час и PHONE_MIGRATE_5че телефонният номер с този префикс трябва да бъде регистриран в 5-ти ДК. Имаме типов език, нали? Нямаме нужда от аргумент от низа, регулярните изрази ще свършат работа, чо.

Отново, това не е на страницата със служебни съобщения, но, както вече е обичайно с този проект, информация може да бъде намерена на друга страница с документация, или събуди подозрение. Първо, вижте, нарушение на писане/слоеве - RpcError може да се инвестира в RpcResult. Защо не навън? Какво не сме взели предвид?.. Съответно къде е гаранцията, че RpcError може да не се инвестира в RpcResult, но да бъдат директно или вложени в друг тип? липсва req_msg_id ? ..

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

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

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

Пристрастяване: Статуси на съобщения

Като цяло много места в TL, MTProto и Telegram като цяло оставят усещане за инат, но от учтивост, такт и др. меки умения ние учтиво замълчахме за това, а нецензурните думи в диалозите бяха цензурирани. Въпреки това, това мястоОпо-голямата част от страницата за съобщения за съобщения предизвиква шок дори за мен, който отдавна работя с мрежови протоколи и съм виждал велосипеди с различна степен на кривина.

Започва безобидно, с потвърждения. След това ни се казва за

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... къде са останалите числа, Томи?

В документацията се посочва:

Намерението е стойностите на error_code да бъдат групирани (error_code >> 4): например кодовете 0x40 - 0x4f съответстват на грешки в разлагането на контейнера.

но, първо, изместване в другата посока, и второ, няма значение къде са останалите кодове? В главата на автора?.. Това обаче са дреболии.

Пристрастяването започва в съобщения за статус на публикация и копия на публикация:

  • Искане за информация за състоянието на съобщението
    Ако някоя от страните не е получила информация за статуса на своите изходящи съобщения за известно време, тя може изрично да я поиска от другата страна:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Информационно съобщение относно статуса на съобщенията
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Тук info е низ, който съдържа точно един байт от състоянието на съобщението за всяко съобщение от входящия списък msg_ids:

    • 1 = нищо не се знае за съобщението (msg_id е твърде нисък, другата страна може да го е забравила)
    • 2 = съобщението не е получено (msg_id попада в обхвата на съхранените идентификатори; другата страна обаче със сигурност не е получила такова съобщение)
    • 3 = съобщението не е получено (msg_id е твърде висок; обаче другата страна със сигурност все още не го е получила)
    • 4 = съобщението е получено (обърнете внимание, че този отговор е същевременно и потвърждение за получаване)
    • +8 = съобщението вече е потвърдено
    • +16 = съобщение, което не изисква потвърждение
    • +32 = RPC заявка, съдържаща се в съобщение, което се обработва или обработката вече е завършена
    • +64 = вече генериран отговор, свързан със съдържанието
    • +128 = другата страна знае със сигурност, че съобщението вече е получено
      Този отговор не изисква потвърждение. Това е потвърждение на съответния msgs_state_req, само по себе си.
      Имайте предвид, че ако внезапно се окаже, че другата страна няма съобщение, което изглежда като изпратено до нея, съобщението може просто да бъде изпратено отново. Дори другата страна да получи две копия на съобщението едновременно, дубликатът ще бъде игнориран. (Ако е минало твърде много време и оригиналният msg_id вече не е валиден, съобщението трябва да бъде обвито в msg_copy).
  • Доброволно съобщаване на статуса на съобщенията
    Всяка страна може доброволно да информира другата страна за статуса на съобщенията, предадени от другата страна.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Разширено доброволно съобщаване на статуса на едно съобщение
    ...
    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;
  • Изрична молба за повторно изпращане на съобщения
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Отдалечената страна незабавно отговаря чрез повторно изпращане на заявените съобщения […]
  • Изрична молба за повторно изпращане на отговорите
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Отдалечената страна незабавно отговаря чрез повторно изпращане отговори към исканите съобщения […]
  • Копия на съобщения
    В някои ситуации старо съобщение с msg_id, който вече не е валиден, трябва да бъде изпратено отново. След това се увива в контейнер за копиране:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Веднъж получено, съобщението се обработва така, сякаш обвивката не е там. Ако обаче е известно със сигурност, че съобщението orig_message.msg_id е получено, тогава новото съобщение не се обработва (като в същото време то и orig_message.msg_id се потвърждават). Стойността на orig_message.msg_id трябва да е по-ниска от 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. Но той също ще отхвърли тази опаковка, ако нещо се обърка, също и цялата.

И в този момент нетехническите съображения влизат в действие. От опит сме виждали много патерици, а освен това сега ще видим още примери за лоши съвети и архитектура - в такива условия струва ли си да се доверявате и да взимате такива решения? Въпросът е риторичен (не разбира се).

За какво говорим? Ако по темата „съобщения на наркомани за съобщения“ все още можете да спекулирате с възражения като „ти си глупав, не разбра нашата брилянтна идея!“ (така че първо напишете документацията, както трябва на обикновените хора, с обосновка и примери за обмен на пакети, тогава ще говорим), след това времената / таймаутите са чисто практически и специфичен въпрос, тук всичко отдавна е известно. Но какво ни казва документацията за изчакванията?

Сървърът обикновено потвърждава получаването на съобщение от клиент (обикновено RPC заявка), използвайки RPC отговор. Ако отговорът идва дълго време, сървърът може първо да изпрати потвърждение за получаване, а малко по-късно и самия RPC отговор.

Клиентът обикновено потвърждава получаването на съобщение от сървър (обикновено RPC отговор), като добавя потвърждение към следващата RPC заявка, ако тя не е предадена твърде късно (ако е генерирана, да речем, 60-120 секунди след получаването на съобщение от сървъра). Въпреки това, ако за дълъг период от време няма причина да се изпращат съобщения до сървъра или ако има голям брой непотвърдени съобщения от сървъра (да речем, над 16), клиентът предава самостоятелно потвърждение.

... Превеждам: ние самите не знаем колко и как е необходимо, добре, нека преценим, че нека бъде така.

А относно пинговете:

Ping съобщения (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Отговорът обикновено се връща към същата връзка:

pong#347773c5 msg_id:long ping_id:long = Pong;

Тези съобщения не изискват потвърждения. Понг се предава само в отговор на пинг, докато пинг може да бъде иницииран от всяка страна.

Отложено затваряне на връзка + PING

ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

Работи като пинг. В допълнение, след като това бъде получено, сървърът стартира таймер, който ще затвори текущата връзка disconnect_delay секунди по-късно, освен ако не получи ново съобщение от същия тип, което автоматично нулира всички предишни таймери. Ако клиентът изпраща тези ping веднъж на всеки 60 секунди, например, той може да зададе disconnect_delay равно на 75 секунди.

Да не си полудял?! След 60 секунди влакът ще влезе в гарата, ще остави и ще вземе пътници и отново ще загуби комуникация в тунела. След 120 секунди, докато сте навън, той ще дойде при друг и връзката най-вероятно ще прекъсне. Е, ясно е откъде растат краката - „Чух звънене, но не знам къде е“, има алгоритъмът на Nagle и опцията TCP_NODELAY, която беше предназначена за интерактивна работа. Но, съжалявам, забавяне на стойността му по подразбиране - 200 Milliсекунди. Ако наистина искате да изобразите нещо подобно и да спестите възможна двойка пакети - добре, отложете го поне за 5 секунди или каквото и да е времето за изчакване на съобщението „Потребителят пише ...“ сега е равно. Но не повече.

И накрая, пингове. Тоест проверка на жизнеността на TCP връзка. Смешно е, но преди около 10 години написах критичен текст за месинджъра на хостела на нашия факултет - там авторите също пингваха сървъра от клиента, а не обратното. Но студентите трета година са едно, а международният офис е друго, нали? ..

Първо, малка образователна програма. TCP връзката, при липса на обмен на пакети, може да живее седмици. Това е както добро, така и лошо, в зависимост от целта. Е, ако сте имали отворена SSH връзка със сървъра, станали сте от компютъра си, рестартирате захранващия рутер, върнахте се на мястото си - сесията през този сървър не се прекъсна (не въведе нищо, нямаше пакети), удобно. Лошо е, ако има хиляди клиенти на сървъра, всеки един заема ресурси (здравей Postgres!) и клиентският хост може да се е рестартирал преди много време - но ние няма да знаем за това.

Системите за чат/IM спадат към втория случай по друга, допълнителна причина – онлайн статусите. Ако потребителят "падна", е необходимо да информира събеседниците си за това. В противен случай ще има грешка, която създателите на Jabber са направили (и коригирана в продължение на 20 години) - потребителят е прекъснал връзката, но те продължават да му пишат съобщения, вярвайки, че е онлайн (които също бяха напълно изгубени в тези няколко минути преди това прекъсването беше открито). Не, опцията TCP_KEEPALIVE, която много хора, които не разбират как работят TCP таймерите, изскача навсякъде (чрез задаване на диви стойности като десетки секунди), няма да помогне тук - трябва да се уверите, че не само ядрото на ОС на машината на потребителя е жива, но също така функционира нормално, в състояние е да отговори и самото приложение (мислите ли, че не може да замръзне? Telegram Desktop на Ubuntu 18.04 ми се срива многократно).

Ето защо трябва да пингвате сървър клиент, а не обратното - ако клиентът направи това, когато връзката е прекъсната, пингът няма да бъде доставен, целта не е постигната.

И какво виждаме в Telegram? Всичко е точно обратното! Е, т.е. формално, разбира се, и двете страни могат да се пингват взаимно. На практика клиентите използват патерица ping_delay_disconnect, който задейства таймер на сървъра. Е, съжалявам, не е работа на клиента да решава колко дълго иска да живее там без пинг. Сървърът, въз основа на натоварването си, знае по-добре. Но, разбира се, ако не съжалявате за ресурсите, тогава злият Пинокио ​​са самите те и патерицата ще падне ...

Как трябваше да бъде проектиран?

Считам, че горните факти съвсем ясно показват не много високата компетентност на екипа на Telegram / VKontakte в областта на транспортното (и по-ниско) ниво на компютърните мрежи и тяхната ниска квалификация по съответните въпроси.

Защо се оказа толкова сложно и как архитектите на Telegram могат да се опитат да възразят? Фактът, че те се опитаха да направят сесия, която оцелява при прекъсване на TCP връзката, тоест това, което не сме доставили сега, ще доставим по-късно. Вероятно са се опитвали да правят и UDP транспорт, но са се затруднили и са го изоставили (затова документацията е празна - нямаше с какво да се похваля). Но поради липсата на разбиране за това как работят мрежите като цяло и TCP в частност, къде можете да разчитате на него и къде трябва да го направите сами (и как), и опитите да се комбинира това с криптографията „един удар от два птици с един камък” - такъв труп се оказа.

Как трябваше да бъде? Въз основа на факта, че msg_id е клеймо за време, което е криптографски необходимо за предотвратяване на атаки за повторно възпроизвеждане, е грешка да се прикачи функция за уникален идентификатор към него. Следователно, без драстична промяна на текущата архитектура (когато се формира нишката за актуализации, това е тема за API на високо ниво за друга част от тази поредица от публикации), ще трябва да:

  1. Сървърът, поддържащ TCP връзката към клиента, поема отговорност - ако сте извадили от сокета, моля, потвърдете, обработете или върнете грешка, без загуба. Тогава потвърждението не е вектор от идентификатори, а просто "последният получен seq_no" - просто число, както в TCP (две числа - вашето собствено seq и потвърдено). Винаги сме в сесия, нали?
  2. Времевият печат за предотвратяване на атаки с повторение става отделно поле, а la nonce. Проверено, но нищо друго не е засегнато. Достатъчно и uint32 - ако нашата сол се променя поне на всеки половин ден, можем да разпределим 16 бита към по-малките бита от цялата част на текущото време, а останалите - към дробната част от секундата (както е сега).
  3. Отстранява се msg_id изобщо - от гледна точка на разграничаване на заявките в задните части, има, първо, идентификаторът на клиента и второ, идентификаторът на сесията и ги свързва. Съответно като идентификатор на заявка е достатъчен само един seq_no.

Също така не е най-добрият вариант, пълно произволно може да служи като идентификатор - между другото това вече е направено в API на високо ниво при изпращане на съобщение. Би било по-добре да сменим изцяло архитектурата от относителна към абсолютна, но това е тема за друга част, не за тази публикация.

API?

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

Внимание, сега ще има единственият пример за Perl в статията! (за тези, които не са запознати със синтаксиса, първият аргумент за благословение е структурата на данните на обекта, вторият е неговият клас):

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' )
};

Да, особено не под спойлера - ако не сте го чели, отидете и го направете!

О, чакай~~… как изглежда? Нещо много познато… може би това е структурата на данните на типичен уеб API в JSON, освен че може би класовете са прикачени към обекти?..

Така се оказва ... Какво е това, другари? .. Толкова много усилия - и спряхме да си починем там, където уеб програмистите току що започва?.. Няма ли просто JSON през HTTPS да е по-лесно?! И какво получихме в замяна? Заслужаваха ли си тези усилия?

Нека оценим какво ни даде TL+MTProto и какви алтернативи са възможни. Е, HTTP заявка-отговор не пасва добре, но поне нещо над TLS?

компактна сериализация. Виждайки тази структура от данни, подобна на JSON, се помни, че има нейни двоични варианти. Нека отбележим MsgPack като недостатъчно разширим, но има например CBOR - между другото стандартът, описан в RFC 7049. Забележително е с това, че определя тагове, като удължителен механизъм и сред вече стандартизирани има:

  • 25 + 256 - замяна на дублиращи се редове с препратка към номер на ред, такъв евтин метод за компресиране
  • 26 - сериализиран Perl обект с име на клас и аргументи на конструктора
  • 27 - сериализиран независим от езика обект с име на тип и аргументи на конструктора

Е, опитах се да сериализирам същите данни в TL и CBOR с активирано опаковане на низове и обекти. Резултатът започна да се различава в полза на CBOR някъде от мегабайт:

cborlen=1039673 tl_len=1095092

По този начин, заключение: Има значително по-прости формати, които не са обект на грешка при синхронизиране или проблем с неизвестен идентификатор, със сравнима ефективност.

Бързо установяване на връзка. Това означава нулев RTT след повторно свързване (когато ключът вече е генериран веднъж) - приложимо от първото съобщение на MTProto, но с някои уговорки - влязоха в същата сол, сесията не се развали и т.н. Какво ни предлага TLS в замяна? Свързан цитат:

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

Тук RTT не е нула, трябва да обмените поне ClientHello и ServerHello, след което, заедно с Finished, клиентът вече може да изпраща данни. Но тук трябва да се помни, че нямаме мрежата с нейния куп новооткрити връзки, а месинджър, чиято връзка често е една и повече или по-малко дълготрайни, относително кратки заявки за уеб страници - всичко е мултиплексиран вътре. Тоест съвсем приемливо е, ако не сме попаднали на много лош участък на метрото.

Забравихте нещо друго? Пишете в коментарите.

Следва продължение!

Във втората част от тази поредица от публикации ще разгледаме организационни въпроси, а не технически - подходи, идеология, интерфейс, отношение към потребителите и т.н. Въз основа обаче на техническата информация, която беше представена тук.

Третата част ще продължи анализа на техническия компонент / опит в разработката. Ще научите по-специално:

  • продължение на пандиза с разнообразието от TL-типове
  • неизвестни неща за каналите и супергрупите
  • отколкото диалози е по-лошо от списък
  • относно абсолютното срещу относително адресиране на съобщения
  • каква е разликата между снимка и изображение
  • как емотикони пречат на текст в курсив

и други патерици! Останете на линия!

Източник: www.habr.com

Добавяне на нов коментар