Критика на протоколот и организациски пристапи на Телеграм. Дел 1, технички: искуство во пишување клиент од нула - TL, MT

Неодамна на Хабре почнаа почесто да се појавуваат постови за тоа колку е добар Телеграм, колку се брилијантни и искусни браќата Дуров во градењето мрежни системи итн. Во исто време, многу малку луѓе навистина се нурнати во техничкиот уред - најмногу, тие користат прилично едноставен (и сосема различен од MTProto) Bot API базиран на JSON и обично само прифаќаат на верата сите пофалби и ПР кои се вртат околу гласникот. Пред речиси една и пол година, мојот колега од невладината организација Ешелон Василиј (за жал, неговата сметка на Хабре беше избришана заедно со нацртот) почна да пишува свој клиент Телеграма од нула во Перл, а подоцна се приклучи и авторот на овие редови. Зошто Перл, некои веднаш ќе прашаат? Бидејќи такви проекти веќе постојат на други јазици Всушност, не е поентата, може да има кој било друг јазик каде што нема готова библиотека, и соодветно на тоа авторот мора да оди до крај од нула. Покрај тоа, криптографијата е прашање на доверба, но проверете. Со производ насочен кон безбедноста, не можете едноставно да се потпрете на готова библиотека од производителот и слепо да и верувате (сепак, ова е тема за вториот дел). Во моментов, библиотеката работи доста добро на „просечно“ ниво (ви овозможува да направите какви било барања за API).

Сепак, нема да има многу криптографија или математика во оваа серија на објави. Но, ќе има многу други технички детали и архитектонски патерици (исто така корисни за оние кои нема да пишуваат од нула, туку ќе ја користат библиотеката на кој било јазик). Значи, главната цел беше да се обидеме да го имплементираме клиентот од нула според официјалната документација. Односно, да претпоставиме дека изворниот код на официјални клиенти е затворен (повторно, во вториот дел подетално ќе ја покриеме темата за фактот дека тоа е точно се случува така), но, како во старите денови, на пример, постои стандард како RFC - дали е можно да се напише клиент само според спецификацијата, „без да се гледа“ во изворниот код, било да е официјален (Telegram Desktop, мобилен), или неофицијален Телетон?

Содржина:

Документација... постои нели? Дали е вистина?..

Фрагменти од белешки за оваа статија почнаа да се собираат минатото лето. Сето ова време на официјалната веб-страница https://core.telegram.org Документацијата беше од Слој 23, т.е. заглавени некаде во 2014 година (се сеќавате, тогаш немаше ни канали?). Се разбира, во теорија, ова требаше да ни овозможи да имплементираме клиент со функционалност во тоа време во 2014 година. Но, и во оваа состојба, документацијата беше, прво, нецелосна, а второ, на места и самата противречи. Пред нешто повеќе од еден месец, во септември 2019 година, тоа беше случајно Откриено е дека има големо ажурирање на документацијата на страницата, за целосно неодамнешниот Layer 105, со напомена дека сега сè треба повторно да се прочита. Навистина, многу членови беа ревидирани, но многумина останаа непроменети. Затоа, кога ја читате критиката подолу за документацијата, треба да имате на ум дека некои од овие работи повеќе не се релевантни, но некои се сè уште прилично. На крајот на краиштата, 5 години во современиот свет не се само долго време, туку многу многу. Оттогаш (особено ако не ги земете предвид отфрлените и оживеаните локации за геочат од тогаш), бројот на API методи во шемата порасна од сто на повеќе од двесте и педесет!

Од каде да започнете како млад автор?

Не е важно дали пишувате од нула или користите, на пример, готови библиотеки како Телетон за Пајтон или Madeline за PHP, во секој случај, прво ќе ви треба регистрирајте ја вашата апликација - добие параметри api_id и api_hash (оние кои работеле со VKontakte API веднаш разбираат) со што серверот ќе ја идентификува апликацијата. Ова ќе мора направете го тоа од правни причини, но ќе зборуваме повеќе за тоа зошто авторите на библиотеката не можат да го објават во вториот дел. Можеби ќе бидете задоволни со вредностите на тестот, иако тие се многу ограничени - факт е дека сега можете да се регистрирате само еден апликација, затоа не брзајте со главата во неа.

Сега, од техничка гледна точка, треба да не интересира фактот дека по регистрацијата треба да добиваме известувања од Telegram за ажурирања на документација, протокол итн. Односно, може да се претпостави дека локацијата со доковите едноставно била напуштена и продолжила да работи конкретно со оние кои почнале да прават клиенти, бидејќи полесно е. Но, не, ништо такво не беше забележано, не дојде информација.

И ако пишувате од нула, тогаш користењето на добиените параметри е всушност сè уште далеку. Иако https://core.telegram.org/ и зборува за нив во Започнување пред сè, всушност, прво ќе треба да ги имплементирате MTProto протокол - но ако верувавте распоред според OSI моделот на крајот на страницата за општ опис на протоколот, тогаш тоа е сосема залудно.

Всушност, и пред и по MTProto, на неколку нивоа одеднаш (како што велат странските вмрежувачи кои работат во кернелот на оперативниот систем, прекршување на слојот), голема, болна и страшна тема ќе ви пречи...

Бинарна серијализација: TL (Јазик на тип) и неговата шема, и слоеви и многу други страшни зборови

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

Зошто „конструктор“ и „полиморфен“ ако не е ООП? Па, всушност, на некој ќе му биде полесно да размислува за ова во термини OOP - полиморфен тип како апстрактна класа, а конструкторите се неговите директни класи на потомци, и 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, што значи „природен број“. Тоа е, всушност, неозначеното int е, патем, единствениот случај кога неозначените броеви се појавуваат во реални кола. Значи, следната е конструкција со прашалник, што значи дека ова поле - ќе биде присутно на жицата само ако соодветниот бит е поставен во полето наведено (приближно како троен оператор). Значи, да претпоставиме дека овој бит е поставен, што значи дека понатаму треба да читаме поле како Type, кој во нашиот пример има 2 конструктори. Едниот е празен (се состои само од идентификаторот), другиот има поле ids со тип ids:Vector<long>.

Можеби мислите дека и шаблоните и генериките се во добри или Java. Но не. За малку. Ова единствениот случај на употреба на аголни загради во реални кола, а се користи САМО за Вектор. Во тек на бајти, овие ќе бидат 4 CRC32 бајти за самиот тип Вектор, секогаш исти, потоа 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 бајти. И ако ова Vector<Type2>, кој се состои од само едно поле int и сам е во типот - дали треба да повториме 10000xabcdef0 34 пати па 4 бајти int, или јазикот може да ни го НЕЗАВИСИ од конструкторот fixedVec и наместо 80000 бајти, пак префрли само 40000?

Ова воопшто не е празно теоретско прашање - замислете дека добивате листа на групни корисници, од кои секој има ид, име, презиме - разликата во количината на податоци пренесени преку мобилна врска може да биде значајна. Нам ни се рекламира токму ефективноста на серијализацијата на Telegram.

Значи

Вектор, кој никогаш не беше објавен

Ако се обидете да поминете низ страниците со опис на комбинаторите и така натаму, ќе видите дека вектор (па дури и матрица) формално се обидува да излезе низ множества од неколку листови. Но, на крајот забораваат, се прескокнува последниот чекор и едноставно се дава дефиниција за вектор, кој сè уште не е врзан за тип. Што е проблемот? Во јазиците програмирање, особено функционалните, сосема е типично да се опише структурата рекурзивно - компајлерот со својата мрзлива евалуација ќе разбере и ќе направи сè сам. Во јазикот серијализација на податоци она што е потребно е ЕФИКАСНОСТ: доволно е едноставно да се опише листа, т.е. структура на два елементи - првиот е податочен елемент, вториот е истата структура или празен простор за опашката (пакет (cons) во Лисп). Но, ова очигледно ќе бара од секоја елементот троши дополнителни 4 бајти (CRC32 во случајот во TL) за да го опише својот тип. Низа исто така може лесно да се опише фиксна големина, но во случај на низа со непозната должина однапред, ние се откинуваме.

Затоа, бидејќи TL не дозволува излез на вектор, мораше да се додаде на страна. На крајот, документацијата вели:

Сериизацијата секогаш го користи истиот конструктор „вектор“ (const 0x1cb5c415 = crc32 („вектор t:Type # [ t ] = Vector t“) кој не зависи од специфичната вредност на променливата од типот t.

Вредноста на опционалниот параметар t не е вклучена во серијализацијата бидејќи е изведена од типот на резултатот (секогаш познат пред десериализацијата).

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

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

... но не се фокусира на тоа. Кога вие, уморни од истегнувањето на математиката (можеби дури и ви е познато од универзитетски курс), ќе одлучите да се откажете и всушност да погледнете како да работите со тоа во пракса, впечатокот во вашата глава е дека ова е сериозно. Математиката во основата, јасно е дека е измислена од Cool People (двајца математичари - победник на ACM), а не кој било. Целта - да се покажеме - е постигната.

Патем, за бројот. Да ве потсетиме дека # тоа е синоним nat, природен број:

Постојат изрази на типот (тип-експр) и нумерички изрази (nat-expr). Сепак, тие се дефинирани на ист начин.

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

но во граматиката се опишани на ист начин, т.е. Оваа разлика мора повторно да се запомни и рачно да се стави во имплементација.

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

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

тогаш веќе не чекате само вектор, туку вектор на корисници. Поточно, треба чекај - во реален код, секој елемент, ако не и гол тип, ќе има конструктор, и на добар начин во имплементацијата ќе треба да се провери - но бевме испратени точно во секој елемент од овој вектор тој тип? Што ако тоа беше некој вид PHP, во кој низата може да содржи различни типови во различни елементи?

Во овој момент почнувате да размислувате - дали е потребен таков TL? Можеби за количката би можело да се користи човечки серијализатор, истиот протобуф што веќе постоел тогаш? Тоа беше теоријата, ајде да погледнеме во пракса.

Постоечки имплементации на TL во код

ТЛ е роден во длабочините на ВКонтакте уште пред познатите настани со продажбата на уделот на Дуров и (Можеби), уште пред да започне развојот на Telegram. И во отворен код изворниот код на првата имплементација можете да најдете многу смешни патерици. И самиот јазик таму беше имплементиран поцелосно отколку што е сега во Телеграма. На пример, хашовите воопшто не се користат во шемата (што значи вграден псевдотип (како вектор) со девијантно однесување). Или

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

но да размислиме, заради комплетноста, да ја следиме, така да се каже, еволуцијата на џинот на мислата.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Или оваа убава:

    static const char *reserved_words_polymorhic[] = {

      "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", NULL

      };

Овој фрагмент е за шаблони како што се:

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

Ова е дефиниција за тип на шаблон за хашмапи како вектор на парови int - Type. Во C++ би изгледало вака:

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

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

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

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

Но, ова беше за првата објавена имплементација на TL „општо“. Ајде да продолжиме со разгледување на имплементации во самите клиенти на Telegram.

Збор до Василиј:

Василиј, [09.10.18 17:07] Најмногу од сè, газот е жежок затоа што создадоа еден куп апстракции, а потоа зачукуваа болт на нив и го покриваа генераторот на кодови со патерици
Како резултат на тоа, прво од dock pilot.jpg
Потоа од кодот dzhekichan.webp

Се разбира, од луѓе запознаени со алгоритми и математика, можеме да очекуваме дека ги прочитале Aho, Ullmann и се запознаени со алатките што станаа де факто стандардни во индустријата во текот на децениите за пишување на нивните 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:XNUMX] и тука би размислил дали е потребен таков ТЛ
ако сакав да се плеткам со алтернативните имплементации, би почнал да вметнувам прекини на линии, половина од парсерите ќе се скршат на повеќелиниски дефиниции
tdesktop, сепак, исто така

Запомнете ја поентата за еднослојна, ќе се вратиме на неа малку подоцна.

Добро, telegram-cli е неофицијален, Telegram Desktop е официјален, но што е со другите? Кој знае?.. Во кодот на клиентот Андроид воопшто немаше анализатор на шема (што покренува прашања за отворен код, но ова е за вториот дел), но имаше уште неколку смешни парчиња код, но повеќе за нив во потсекција подолу.

Кои други прашања ги поставува серијалирањето во пракса? На пример, тие направија многу работи, се разбира, со бит полиња и условни полиња:

Василиј: flags.0? true
значи дека полето е присутно и е еднакво точно ако знамето е поставено

Василиј: flags.1? int
значи дека теренот е присутен и треба да се десерилизира

Василиј: Магаре, не се секирај што правиш!
Василиј: Некаде во документот се споменува дека точно е тип со гола нулта должина, но невозможно е да се состави нешто од нивниот документ
Василиј: И кај имплементациите со отворен код не е така, но има еден куп патерици и потпори

Што е со Телетон? Гледајќи напред кон темата за МТПрото, пример - во документацијата има такви парчиња, но знакот % се опишува само како „што одговара на даден гол тип“, т.е. во примерите подолу има или грешка или нешто недокументирано:

Василиј, [22.06.18 18:38] На едно место:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Во различно:

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

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

Не сум видел гола векторска дефиниција и не наидов на таква

Анализата се пишува рачно во телетон

Во неговиот дијаграм се коментира дефиницијата msg_container

Повторно, прашањето останува околу %. Не е опишано.

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

Василиј, [22.06.18 19:23] Но нивниот TL парсер на обични мотори најверојатно нема да го јаде ниту ова

// parsed manually

TL е убава апстракција, никој не ја спроведува целосно

И % не е во нивната верзија на шемата

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

Се најде во граматиката, едноставно можеа да заборават да ја опишат семантиката

Го видовте документот на TL, не можете да го сфатите без половина литар

„Па, да речеме“, ќе рече друг читател, „ти критикуваш нешто, па покажи ми како треба да се направи тоа“.

Василиј одговара: „Што се однесува до парсерот, ми се допаѓаат работи како

    args: /* empty */ { $$ = NULL; }
        | args arg { $$ = g_list_append( $1, $2 ); }
        ;

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

некако подобро од

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

или

        # Regex to match the whole line
        match = re.match(r'''
            ^                  # We want to match from the beginning to the end
            ([w.]+)           # The .tl object can contain alpha_name or namespace.alpha_name
            (?:
                #             # After the name, comes the ID of the object
                ([0-9a-f]+)    # The constructor ID is in hexadecimal form
            )?                 # If no constructor ID was given, CRC32 the 'tl' to determine it

            (?:s              # After that, we want to match its arguments (name:type)
                {?             # For handling the start of the '{X:Type}' case
                w+            # The argument name will always be an alpha-only name
                :              # Then comes the separator between name:type
                [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генериран код), вклучително и типови зајачиња со информации за интроспекција во секоја класа. Секој полиморфен тип се претвора во празна апстрактна основна класа, а конструкторите наследуваат од неа и имаат методи за серијализација и десериализација.

Недостаток на типови во јазикот на типот

Силното пишување е добра работа, нели? Не, ова не е холивар (иако преферирам динамични јазици), туку постулат во рамките на ТЛ. Врз основа на него, јазикот треба да обезбеди секакви проверки за нас. Па, во ред, можеби не тој самиот, туку имплементацијата, но барем треба да ги опише. И какви можности сакаме?

Прво на сите, ограничувања. Овде гледаме во документацијата за поставување датотеки:

Бинарната содржина на датотеката потоа се дели на делови. Сите делови мора да имаат иста големина ( дел_големина ) и мора да бидат исполнети следните услови:

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

Последниот дел не мора да ги задоволува овие услови, под услов неговата големина да биде помала од part_size.

Секој дел треба да има низа број, датотека_дел, со вредност која се движи од 0 до 2,999.

Откако датотеката е поделена, треба да изберете метод за зачувување на серверот. Користете upload.saveBigFilePart во случај целосната големина на датотеката да биде поголема од 10 MB и upload.saveFilePart за помали датотеки.
[…] може да се врати една од следните грешки за внесување податоци:

  • FILE_PARTS_INVALID — Неважечки број на делови. Вредноста не е помеѓу 1..3000

Дали нешто од ова е на дијаграмот? Дали е ова некако изразливо со користење на TL? бр. Но, извинете, дури и Турбо Паскал на дедо можеше да ги опише наведените типови се движи. И знаеше уште една работа, сега попозната како enum - тип кој се состои од набројување на фиксен (мал) број вредности. На јазици како C - нумерички, забележете дека досега зборувавме само за типови броеви. Но, има и низи, низи... на пример, би било убаво да се опише дека оваа низа може да содржи само телефонски број, нели?

Ништо од ова не е во TL. Но, постои, на пример, во шемата JSON. И ако некој друг би можел да се расправа за деливоста на 512 KB, дека ова сепак треба да се провери во кодот, тогаш проверете дали клиентот едноставно не можеше испрати број надвор од опсегот 1..3000 (а не можеше да се појави соодветната грешка) ќе беше можно, нели?..

Патем, за грешки и повратни вредности. Дури и оние кои работеле со TL ги замаглуваат очите - тоа не ни го разбравме веднаш секој функцијата во TL всушност може да го врати не само опишаниот тип на враќање, туку и грешка. Но, ова не може да се заклучи на кој било начин користејќи го самиот TL. Се разбира, веќе е јасно и нема потреба од ништо во пракса (иако всушност, 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;

Но, очигледно е дека по некое време ова ќе стане некој вид бакханалија. И дојде решението:

Ажурирање: почнувајќи од Layer 9, помошни методи invokeWithLayerN може да се користи само заедно со initConnection

Ура! По 9 верзии, конечно дојдовме до она што беше направено во Интернет протоколите уште во 80-тите - се договоривме за верзијата еднаш на почетокот на врската!

Па што е следно?..

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

Но, сега сеуште можете да се смеете. Само по уште 9 слоеви, конечно беше додаден универзален конструктор со број на верзија, кој треба да се повика само еднаш на почетокот на врската, а значењето на слоевите изгледаше исчезна, сега тоа е само условна верзија, како секаде на друго место. Проблемот е решен.

Точно?..

Василиј, [16.07.18 14:01] Уште во петокот помислив:
Телесерверот испраќа настани без барање. Барањата мора да бидат завиткани во InvokeWithLayer. Серверот не ги обвиткува ажурирањата, нема структура за завиткување одговори и ажурирања.

Оние. клиентот не може да го одреди слојот во кој сака ажурирања

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

Василиј, [16.07.18 14:02] Ова е единствениот начин

Вадим Гончаров, [16.07.18 14:02] што во суштина треба да значи договарање на слојот на почетокот на седницата

Патем, произлегува дека деградирањето на клиентот не е обезбедено

Ажурирања, т.е. тип 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 Неочекуван тип на ID #b5223b0f прочитан во MTPMessageMedia…

Критика на протоколот и организациски пристапи на Телеграм. Дел 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 GPL blobs во кернелот на Linux), но ова е веќе тема за вториот дел.

Но, доволно. Ајде да преминеме на протоколот над кој работи целата оваа серијализација.

МТПрото

Значи, да отвориме општ опис и детален опис на протоколот а првото нешто на што се сопнуваме е терминологијата. И со изобилство од се. Општо земено, се чини дека ова е комерцијална карактеристика на Telegram - нарекувајќи ги работите поинаку на различни места, или различни работи со еден збор или обратно (на пример, во API на високо ниво, ако видите пакет налепници, тоа не е што мислевте).

На пример, „порака“ и „сесија“ овде значат нешто поинакво отколку во вообичаениот интерфејс на клиентот на Telegram. Па, сè е јасно со пораката, може да се толкува во термини OOP, или едноставно да се нарече зборот „пакет“ - ова е ниско ниво на транспорт, нема исти пораки како во интерфејсот, има многу пораки за услуга . Но сесијата... но прво.

транспортен слој

Првата работа е транспортот. Тие ќе ни кажат за 5 опции:

  • TCP
  • Веб-сокет
  • Веб-сокет преку HTTPS
  • HTTP
  • HTTPS

Василиј, [15.06.18 15:04] Има и UDP транспорт, но не е документиран

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

Првиот е сличен на UDP преку TCP, секој пакет вклучува секвенциски број и crc
Зошто е толку болно читањето документи на количка?

Па, еве го сега TCP веќе во 4 варијанти:

  • Скратена
  • Средно
  • Поместено средно
  • Целосна

Па, во ред, поместено средно за MTProxy, ова беше подоцна додадено поради добро познати настани. Но, зошто уште две верзии (вкупно три) кога би можеле да поминете со една? Сите четири суштински се разликуваат само во начинот на поставување на должината и носивоста на главниот MTProto, за што ќе се дискутира понатаму:

  • во Скратено е 1 или 4 бајти, но не 0xef, па телото
  • во Средно ова е 4 бајти должина и поле, и прв пат клиентот мора да испрати 0xeeeeeeee да означи дека е Средно
  • во Целосна најзависноста, од гледна точка на вмрежувач: должина, секвенционен број и НЕ ОНОЈ што е главно MTProto, тело, CRC32. Да, сето ова е на врвот на TCP. Што ни обезбедува сигурен транспорт во форма на секвенцијален бајт поток не се потребни секвенци, особено контролни суми; Добро, сега некој ќе ми приговори дека TCP има 16-битна контролна сума, па се случува оштетување на податоците. Одлично, но ние всушност имаме криптографски протокол со хаш подолги од 16 бајти, сите овие грешки - па дури и повеќе - ќе бидат фатени од SHA несовпаѓање на повисоко ниво. Згора на ова, НЕМА поента во CRC32.

Да го споредиме Скратениот, во кој е можен еден бајт со должина, со Интермедиа, што оправдува „Во случај да е потребно усогласување на податоци од 4 бајти“, што е прилично бесмислено. Што, се верува дека програмерите на Telegram се толку некомпетентни што не можат да читаат податоци од штекер во подреден тампон? Сè уште треба да го направите ова, бидејќи читањето може да ви врати било кој број бајти (а има и прокси-сервери, на пример...). Или од друга страна, зошто да го блокираме Скратениот ако сè уште ќе имаме големо полнење над 16 бајти - заштедете 3 бајти понекогаш ?

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

Други опции за транспорт, вкл. Веб и MTProxy, нема да ги разгледуваме сега, можеби во друг пост, ако има барање. За истиот овој MTProxy, само сега да се потсетиме дека кратко време по неговото објавување во 2018 година, провајдерите брзо научија да го блокираат, наменет за блокирање на бајпасОд големина на пакувањето! И, исто така, фактот дека MTProxy серверот напишан (повторно од Waltman) на C беше премногу поврзан со спецификите на Linux, иако тоа воопшто не беше потребно (Фил Кулин ќе потврди), и дека сличен сервер или во Go или Node.js би се вклопуваат во помалку од сто линии.

Но, ќе извлечеме заклучоци за техничката писменост на овие луѓе на крајот од делот, откако ќе разгледаме други прашања. Засега, да преминеме на OSI слој 5, сесија - на која поставија MTProto сесија.

Клучеви, пораки, сесии, Дифи-Хелман

Тие го поставија таму не сосема правилно... Сесијата не е истата сесија што е видлива во интерфејсот под Активни сесии. Но, по ред.

Критика на протоколот и организациски пристапи на Телеграм. Дел 1, технички: искуство во пишување клиент од нула - TL, MT

Значи, добивме бајт низа со позната должина од транспортниот слој. Ова е или шифрирана порака или обичен текст - ако сè уште сме во фазата на клучен договор и всушност го правиме тоа. За кој од купот концепти наречени „клуч“ зборуваме? Ајде да го разјасниме ова прашање за самиот тим на Telegram (се извинувам што ја преведов мојата сопствена документација од англиски со уморен мозок во 4 часот наутро, беше полесно да оставам некои фрази такви какви што се):

Има два ентитети наречени сесија - еден во интерфејсот на официјални клиенти под „тековни сесии“, каде што секоја сесија одговара на цел уред / оперативен систем.
Вториот е MTProto сесија, кој во себе го има секвенцискиот број на пораката (во смисла на ниско ниво) и кој може да трае помеѓу различни TCP конекции. Неколку MTProto сесии може да се инсталираат истовремено, на пример, за да се забрза преземањето на датотеки.

Помеѓу овие две сесии постои концепт овластување. Во случајот дегенериран, тоа можеме да го кажеме Сесија на UI е исто како овластување, но за жал, сè е комплицирано. Ајде да видиме:

  • Корисникот на новиот уред прво генерира авт_клуч и го врзува за сметка, на пример преку СМС - затоа овластување
  • Тоа се случи внатре во првиот MTProto сесија, кој има session_id внатре во себе.
  • На овој чекор, комбинацијата овластување и session_id може да се нарече пример - овој збор се појавува во документацијата и кодот на некои клиенти
  • Потоа, клиентот може да се отвори некои MTProto сесии под истиот авт_клуч - на истиот DC.
  • Потоа, еден ден клиентот ќе треба да ја побара датотеката од друг DC - и за овој DC ќе се генерира нов авт_клуч !
  • Да се ​​информира системот дека не се регистрира нов корисник, туку истиот овластување (Сесија на UI), клиентот користи API повици auth.exportAuthorization во домот DC auth.importAuthorization во новиот DC.
  • Сè е исто, неколку може да се отворени MTProto сесии (секој со своето session_id) на овој нов DC, под нејзините авт_клуч.
  • Конечно, клиентот може да сака Perfect Forward Secrecy. Секој авт_клуч беше постојан клуч - по DC - и клиентот може да се јави auth.bindTempAuthKey за употреба привремена авт_клуч - и повторно, само еден temp_auth_key по DC, заеднички за сите MTProto сесии на овој DC.

забележи, тоа сол (и идните соли) е исто така еден на авт_клуч тие. споделени меѓу сите MTProto сесии на истиот DC.

Што значи „помеѓу различни TCP врски“? Значи ова значи нешто како колаче за овластување на веб-локација - опстојува (преживува) многу TCP конекции со даден сервер, но еден ден се влошува. Само за разлика од HTTP, во MTProto пораките во рамките на сесијата се последователно нумерирани и потврдени ако влегле во тунелот, врската била прекината - по воспоставувањето нова врска, серверот љубезно ќе испрати сè што не испорачало во претходната сесија; TCP конекција.

Сепак, горенаведените информации се сумирани по повеќемесечна истрага. Во меѓувреме, дали го имплементираме нашиот клиент од нула? - да се вратиме на почетокот.

Па ајде да генерираме auth_key на Верзии на Diffie-Hellman од Telegram. Ајде да се обидеме да ја разбереме документацијата...

Василиј, [19.06.18 20:05] data_with_hash := SHA1(податоци) + податоци + (било случајни бајти); така што должината е еднаква на 255 бајти;
шифрирани_податоци := RSA(податоци_со_хаш, сервер_јавен_клуч); долг број од 255 бајти (голем ендијан) се зголемува до потребната моќност над потребниот модул, а резултатот се чува како број од 256 бајти.

Имаат наркотик ДХ

Не изгледа како DH на здрава личност
Нема два јавни клуча во dx

Па, на крајот ова беше средено, но остана остаток - доказ за работа од страна на клиентот дека тој можел да го факторизира бројот. Вид на заштита од DoS напади. А клучот RSA се користи само еднаш во една насока, во суштина за шифрирање new_nonce. Но, иако оваа навидум едноставна операција ќе успее, со што ќе треба да се соочите?

Василиј, [20.06.18/00/26 XNUMX:XNUMX] Сè уште не сум стигнал до соодветното барање

Ова барање го испратив до ДХ

А, во транспортниот док пишува дека може да одговори со 4 бајти код за грешка. Тоа е се

Па, ми рече -404, па што?

Па јас му реков: „Фати си ги глупостите шифрирани со серверски клуч со отпечаток од прст вака, сакам DH“, и ми одговори со глупава 404

Што би помислиле за одговорот на овој сервер? Што да се прави? Нема кој да праша (но повеќе за тоа во вториот дел).

Тука целиот интерес се врши на обвинителна клупа

Немам што друго да правам, само сонував да ги претворам броевите напред-назад

Два 32-битни броја. Ги спакував како и сите други

Но не, овие две треба прво да се додадат на линијата како BE

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

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

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

Василиј, [20.06.18 15:50 часот] приближно

Не можев да најдам такво распаѓање на прости фактори%)

Ние дури и не управувавме со известување за грешки

Василиј, [20.06.18 20:18] О, има и МД5. Веќе три различни хаши

Клучниот отпечаток од прст се пресметува на следниов начин:

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 kbit за примарност%)

Но, воопшто не е јасно, нафеихоа

Василиј, [21.06.18 18:02] Документот не кажува што да прави ако се покаже дека не е едноставно

Не е кажано. Ајде да видиме што прави официјалниот клиент на Android во овој случај? А тоа е што (и да, целата датотека е интересна) - како што велат, само ќе го оставам ова овде:

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

Не, се разбира дека сè уште е таму некои Има тестови за примарност на некој број, но јас лично веќе немам доволно познавање од математика.

Добро, го добивме главниот клуч. За да се најавите, т.е. испраќајте барања, треба да извршите дополнително шифрирање, користејќи AES.

Копчето за порака е дефинирано како 128 средни битови на SHA256 на телото на пораката (вклучувајќи сесија, ID на порака, итн.), вклучувајќи ги бајтите за полнење, предложени од 32 бајти земени од клучот за авторизација.

Василиј, [22.06.18 14:08] Просечна, кучка, битови

Примено auth_key. Сите. Покрај нив... не е јасно од документот. Слободно проучете го кодот со отворен код.

Забележете дека MTProto 2.0 бара од 12 до 1024 бајти полнење, сè уште под услов должината на пораката да биде делива со 16 бајти.

Значи, колку баласт треба да додадете?

И да, има и 404 во случај на грешка

Ако некој внимателно ги проучувал дијаграмот и текстот на документацијата, забележал дека таму нема MAC. И дека AES се користи во одреден IGE режим кој не се користи никаде на друго место. Тие, се разбира, пишуваат за ова во нивните Најчесто поставувани прашања... Овде, на пример, самиот клуч за пораки е исто така SHA хашот на дешифрираните податоци, кој се користи за проверка на интегритетот - и во случај на неусогласеност, документацијата поради некоја причина препорачува тивко да ги игнорираме (но што е со безбедноста, што ако не скршат?).

Јас не сум криптограф, можеби нема ништо лошо со овој режим во овој случај од теоретска гледна точка. Но, јасно можам да наведам практичен проблем, користејќи го Telegram Desktop како пример. Го шифрира локалниот кеш (сите овие D877F783D5D3EF8C) на ист начин како пораките во MTProto (само во овој случај верзија 1.0), т.е. прво клучот за порака, потоа самите податоци (и некаде настрана главната голема auth_key 256 бајти, без кои msg_key бескорисни). Значи, проблемот станува забележлив на големи датотеки. Имено, треба да чувате две копии од податоците - шифрирани и дешифрирани. И ако има мегабајти, или стриминг видео, на пример?.. Класичните шеми со MAC по шифрениот текст ви дозволуваат да го читате стрим, веднаш пренесувајќи го. Но со MTProto ќе мора во прво време шифрирајте или дешифрирајте ја целата порака, само потоа префрлете ја на мрежата или на дискот. Затоа, во најновите верзии на Telegram Desktop во кешот во user_data Се користи и друг формат - со AES во CTR режим.

Василиј, [21.06.18 01:27] О, дознав што е IGE: IGE беше првиот обид за „режим за автентикација на шифрирање“, првично за Kerberos. Тоа беше неуспешен обид (не обезбедува заштита на интегритетот) и мораше да се отстрани. Тоа беше почеток на 20-годишната потрага по автентициран режим на шифрирање што функционира, кој неодамна кулминираше со режими како OCB и GCM.

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

Тимот зад Telegram, предводен од Николај Дуров, се состои од шест шампиони на ACM, од кои половината докторирале по математика. Им требаа околу две години да ја пуштат тековната верзија на MTProto.

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

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

Во ред, да речеме дека го направивме шифрирањето и другите нијанси. Дали е можно конечно да се испратат барања серијализирани во TL и да се десеријализираат одговорите? Значи, што и како треба да испратите? Еве, да речеме, методот initConnection, можеби ова е тоа?

Vasily, [25.06.18 18:46] Иницијализира поврзување и зачувување информации на уредот и апликацијата на корисникот.

Прифаќа app_id, device_model, system_version, app_version и lang_code.

И некое прашање

Документација како и секогаш. Слободно проучете го софтверот со отворен код

Ако сè беше приближно јасно со invokeWithLayer, тогаш што не е во ред овде? Излегува, да речеме дека имаме - клиентот веќе имал за што да го праша серверот - има барање што сакавме да го испратиме:

Василиј, [25.06.18 19:13] Судејќи според кодот, првиот повик е завиткан во ова глупости, а самиот глупост е завиткан во invokewithlayer

Зошто initConnection не може да биде посебен повик, но мора да биде обвивка? Да, како што се испостави, тоа мора да се прави секој пат на почетокот на секоја сесија, а не еднаш, како со главниот клуч. Но! Не може да се повика од неовластен корисник! Сега дојдовме до фаза каде што е применливо Оваа страница со документација - и ни кажува дека ...

Само мал дел од методите на API се достапни за неовластени корисници:

  • auth.sendCode
  • auth.reendCode
  • account.getPassword
  • auth.checkPassword
  • auth.checkPhone
  • auth.signUp
  • auth.signIn
  • авторизација.увозОвластување
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

Првиот од нив, auth.sendCode, и тука е она негуваното прво барање во кое испраќаме api_id и api_hash, а после тоа добиваме СМС со код. И ако сме во погрешен DC (телефонските броеви во оваа земја ги опслужува друга, на пример), тогаш ќе добиеме грешка со бројот на саканиот DC. За да дознаете на која IP адреса по DC број треба да се поврзете, помогнете ни help.getConfig. Едно време имаше само 5 пријавени, но по познатите настани од 2018 година, бројката значително се зголеми.

Сега да се потсетиме дека до оваа фаза стигнавме на серверот анонимно. Зарем не е прескапо само да се добие IP адреса? Зошто да не го направите ова и другите операции во нешифрираниот дел на MTProto? Го слушам приговорот: „Како можеме да се осигураме дека РКН не е тој што ќе одговори со лажни адреси? На ова се сеќаваме дека, воопшто, официјални клиенти 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, т.е. до следното ниво, а пропушти нешто во темата МТПрото? Нема изненадување:

Василиј, [28.06.18 02:04] Мм, тие претураат по некои од алгоритмите на e2e

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

Но, тие постојано мешаат различни нивоа на магацинот, така што не е секогаш јасно каде заврши mtproto и каде започна следното ниво

Како се мешаат? Па, еве го истиот привремен клуч за PFS, на пример (патем, Telegram Desktop не може да го направи тоа). Се извршува со барање API auth.bindTempAuthKey, т.е. од највисокото ниво. Но, во исто време се меша со шифрирањето на пониско ниво - по него, на пример, треба да го направите повторно initConnection итн., ова не е само нормално барање. Она што е исто така посебно е што можете да имате само ЕДЕН привремен клуч по DC, иако полето auth_key_id во секоја порака ви овозможува да го промените клучот барем секоја порака, и дека серверот има право да го „заборави“ привремениот клуч во секое време - документацијата не кажува што да правите во овој случај... добро, зошто можеше немате неколку клучеви, како со сет на идни соли, и ?..

Има уште неколку работи што вреди да се забележат во врска со темата MTProto.

Пораки, msg_id, msg_seqno, потврди, пингови во погрешна насока и други идиосинкразии

Зошто треба да знаете за нив? Бидејќи тие „протекуваат“ на повисоко ниво и треба да бидете свесни за нив кога работите со API. Да претпоставиме дека не сме заинтересирани за msg_key, пониското ниво ни дешифрирало сè. Но, внатре во дешифрираните податоци ги имаме следните полиња (исто така и должината на податоците, за да знаеме каде е полнењето, но тоа не е важно):

  • сол - инт64
  • 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 од другата страна ќе побрзам да те вознемирувам. Потврдите се дадени во МТПрото НЕ на seq_no, како во TCP, но од msg_id !

Што е ова msg_id, најважното од овие полиња? Единствен идентификатор на пораката, како што сугерира името. Тој е дефиниран како 64-битен број, од кои најниските битови повторно ја имаат магијата „сервер-не-сервер“, а остатокот е временски печат на Unix, вклучувајќи го и фракциониот дел, поместен 32 бита налево. Оние. временскиот печат сам по себе (и пораките со времиња кои премногу се разликуваат ќе бидат отфрлени од серверот). Од ова излегува дека генерално ова е идентификатор кој е глобален за клиентот. Со оглед на тоа - да се потсетиме session_id - гарантираме: Под никакви околности не може да се испрати порака наменета за една сесија на друга сесија. Тоа е, излегува дека веќе има три ниво - сесија, број на сесија, ид на порака. Зошто таква прекумерна компликација, оваа мистерија е многу голема.

Значи, msg_id потребни за...

RPC: барања, одговори, грешки. Потврди.

Како што можеби забележавте, никаде на дијаграмот нема посебен тип или функција „направи барање RPC“, иако има одговори. На крајот на краиштата, имаме пораки поврзани со содржината! Тоа е, кој било пораката може да биде барање! Или да не биде. После се, од секоја постои msg_id. Но, има одговори:

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

Овде се означува на која порака е одговор. Затоа, на највисокото ниво на API, ќе мора да запомните колкав е бројот на вашето барање - мислам дека нема потреба да објаснувате дека работата е асинхрона и може да има неколку барања во исто време, одговорите на кои може да се вратат по кој било редослед? Во принцип, од ова и од пораките за грешка како нема работници, може да се следи архитектурата зад ова: серверот што одржува 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_Х_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 не е целосен).

Зависност од дрога: статуси на пораки

Во принцип, многу места во 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 - не е (релативно) логички изолиран дел, туку парче врзано за архитектурата на апликацијата, т.е. ќе биде потребно значително повеќе време за да се разбере кодот на апликацијата.

Пингови и тајминзи. Редици.

Од сè, ако се потсетиме на нагаѓањата за архитектурата на серверот (распределба на барањата низ бекендовите), следи прилично тажна работа - и покрај сите гаранции за испорака во TCP (или податоците се испорачуваат, или ќе бидете информирани за празнината, но податоците ќе бидат доставени пред да се појави проблемот), дека потврдите во самиот МТПрото - нема гаранции. Серверот лесно може да ја изгуби или исфрли вашата порака, и ништо не може да се направи за тоа, само користете различни типови на патерици.

И пред сè - редици за пораки. Па, со едно сè беше очигледно од самиот почеток - непотврдена порака мора да се складира и повторно да се испрати. И после колку време? И шегата го знае. Можеби тие пораки за услуга зависници некако го решаваат овој проблем со патерици, да речеме, на 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#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 секунди подоцна, освен ако не прими нова порака од ист тип што автоматски ги ресетира сите претходни тајмери. Ако клиентот ги испраќа овие пингови еднаш на 60 секунди, на пример, може да постави disconnect_delay еднакво на 75 секунди.

Дали си луд?! За 60 секунди возот ќе влезе во станицата, ќе спушти и ќе земе патници и повторно ќе изгуби контакт во тунелот. За 120 секунди, додека го слушнете, ќе пристигне на уште една, а врската најверојатно ќе се прекине. Па, јасно е од каде доаѓаат нозете - „Слушнав ѕвонење, но не знам каде е“, тука е алгоритамот на Nagl и опцијата TCP_NODELAY, наменета за интерактивна работа. Но, извинете, држете се до неговата стандардна вредност - 200 Милисекунди Ако навистина сакате да прикажете нешто слично и да заштедите на неколку можни пакети, тогаш одложете го за 5 секунди или што и да е истекот на пораката „Корисникот пишува...“ сега. Но, не повеќе.

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

Прво, малку едукативна програма. TCP врската, во отсуство на размена на пакети, може да живее со недели. Ова е и добро и лошо, во зависност од целта. Добро е ако имавте отворена SSH конекција со серверот, станавте од компјутерот, го рестартиравте рутерот, се вративте на вашето место - сесијата преку овој сервер не беше скината (ништо не сте напишале, немаше пакети) , тоа е погодно. Лошо е ако има илјадници клиенти на серверот, секој зафаќа ресурси (здраво, Postgres!), а домаќинот на клиентот можеби се рестартирал одамна - но ние нема да знаеме за тоа.

Системите за разговор/IM спаѓаат во вториот случај поради една дополнителна причина - онлајн статуси. Ако корисникот „падна“, треба да ги информирате неговите соговорници за ова. Во спротивно, ќе завршите со грешка што ја направиле креаторите на Jabber (и ја коригирале 20 години) - корисникот се исклучил, но продолжуваат да му пишуваат пораки, верувајќи дека е онлајн (кои исто така беа целосно изгубени во овие неколку минути пред да се открие исклучувањето). Не, опцијата TCP_KEEPALIVE, која многу луѓе кои не разбираат како работат TCP тајмерите ја фрлаат по случаен избор (со поставување диви вредности како десетици секунди), нема да помогне тука - треба да бидете сигурни дека не само кернелот на ОС на машината на корисникот е жива, но исто така функционира нормално, не може да одговори, и самата апликација (дали мислите дека не може да се замрзне? Telegram Desktop на Ubuntu 18.04 ми замрзна повеќе од еднаш).

Затоа треба да пингувате сервер клиент, а не обратно - ако клиентот го направи ова, ако врската е прекината, пингот нема да се испорача, целта нема да се постигне.

Што гледаме на Телеграма? Токму спротивното е! Па, тоа е. Формално, се разбира, двете страни можат да се пингираат една со друга. Во пракса, клиентите користат патерица ping_delay_disconnect, кој го поставува тајмерот на серверот. Па, извинете, не е на клиентот да одлучи колку долго сака да живее таму без пинг. Серверот, врз основа на неговото оптоварување, знае подобро. Но, се разбира, ако не ви пречат ресурсите, тогаш ќе бидете вашиот злобен Пинокио, а патерица ќе направи...

Како требаше да биде дизајниран?

Верувам дека горенаведените факти јасно укажуваат дека тимот на Telegram/VKontakte не е многу компетентен во областа на транспортното (и пониско) ниво на компјутерски мрежи и нивните ниски квалификации за релевантни прашања.

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

Како беше потребно? Врз основа на фактот дека msg_id е временски печат неопходен од криптографска гледна точка за да се спречат нападите за повторување, погрешно е да се прикачи единствена функција за идентификатор на неа. Затоа, без фундаментално менување на тековната архитектура (кога ќе се генерира протокот Ажурирања, тоа е тема на API на високо ниво за друг дел од оваа серија на објави), ќе треба:

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

Ова исто така не е најуспешната опција која може да послужи како идентификатор - ова е веќе направено во API на високо ниво кога се испраќа порака; Подобро би било целосно да се преправи архитектурата од релативно во апсолутна, но ова е тема за друг дел, а не за овој пост.

API?

Та-дам! Така, откако се боревме низ патека полна со болка и патерици, конечно успеавме да испратиме какви било барања до серверот и да добиваме какви било одговори на нив, како и да добиваме ажурирања од серверот (не како одговор на барање, туку тој самиот ни испраќа, како PUSH, ако некој е појасно така).

Внимание, сега ќе го има единствениот пример во Перл во статијата! (за оние кои не се запознаени со синтаксата, првиот аргумент на Bless е структурата на податоците на објектот, вториот е неговата класа):

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

Да, не е спојлер намерно - ако сè уште не сте го прочитале, повелете и направете го тоа!

О, вај~~... како изгледа ова? Нешто многу познато... можеби ова е структурата на податоци на типично Web 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

Додадете коментар