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

Недавно су на Хабреу све чешће почели да се појављују постови о томе колико је Телеграм добар, колико су браћа Дуров бриљантна и искусни у изградњи мрежних система итд. Истовремено, врло мало људи се заиста уживило у технички уређај - највише, они користе прилично једноставан (и прилично другачији од МТПрото) Бот АПИ заснован на ЈСОН-у, и обично само прихватају на вери све похвале и ПР који се врте око гласника. Пре скоро годину и по дана, мој колега из невладине организације Есхелон Василиј (нажалост, његов налог на Хабреу је избрисан заједно са нацртом) почео је испочетка да пише свој Телеграм клијент на Перлу, а касније се придружио и аутор ових редова. Зашто Перл, неки ће одмах питати? Јер такви пројекти већ постоје на другим језицима, у ствари, није то поента, може постојати било који други језик на коме нема готова библиотека, и сходно томе аутор мора ићи до краја од нуле. Штавише, криптографија је ствар поверења, али проверите. Са производом који има за циљ безбедност, не можете се једноставно ослонити на готову библиотеку произвођача и слепо јој веровати (међутим, ово је тема за други део). Тренутно библиотека ради прилично добро на „просечном“ нивоу (омогућава вам да постављате било који АПИ захтев).

Међутим, у овој серији постова неће бити много криптографије или математике. Али биће много других техничких детаља и архитектонских штака (корисних и за оне који неће писати од нуле, већ ће користити библиотеку на било ком језику). Дакле, главни циљ је био покушати имплементирати клијента од нуле према званичној документацији. Односно, претпоставимо да је изворни код званичних клијената затворен (опет, у другом делу ћемо детаљније покрити тему чињенице да је то тачно то се догађа тако), али, као у стара времена, на пример, постоји стандард као што је РФЦ - да ли је могуће написати клијента само према спецификацији, „без гледања“ у изворни код, било да је званичан (Телеграм Десктоп, мобилни), или незванични телетон?

Оглашение:

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

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

Одакле почети као млади аутор?

Није важно да ли пишете од нуле или користите, на пример, готове библиотеке попут Телетон за Питхон или Маделине за ПХП, у сваком случају, прво ће вам требати региструјте своју пријаву - добити параметре api_id и api_hash (они који су радили са ВКонтакте АПИ-јем одмах разумеју) по којима ће сервер идентификовати апликацију. Ово морати урадите то из правних разлога, али ћемо више о томе зашто аутори библиотеке не могу да то објаве у другом делу. Можда ћете бити задовољни вредностима теста, иако су оне веома ограничене - чињеница је да сада можете да се региструјете само један апликацију, тако да не журите главом у њу.

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

А ако пишете од нуле, онда је коришћење добијених параметара заправо још увек далеко. Мада https://core.telegram.org/ и говори о њима у Геттинг Стартед пре свега, у ствари, прво ћете морати да примените МТПрото протокол - али ако си веровао распоред према ОСИ моделу на крају странице за општи опис протокола, онда је потпуно узалудно.

У ствари, и пре и после МТПротоа, на неколико нивоа одједном (како кажу страни мрежни радници који раде у језгру ОС-а, кршење слоја), велика, болна и страшна тема ће стати на пут...

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

Ова тема је, у ствари, кључна за проблеме Телеграма. И биће много страшних речи ако покушате да се удубите у то.

Дакле, ево дијаграма. Ако вам ова реч падне на памет, реците, ЈСОН шема, Добро сте мислили. Циљ је исти: неки језик за описивање могућег скупа пренетих података. Ту се сличности завршавају. Ако са странице МТПрото протокол, или из изворног стабла званичног клијента, покушаћемо да отворимо неку шему, видећемо нешто попут:

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;

Особа која ово први пут види интуитивно ће моћи да препозна само део написаног – па, то су очигледно структуре (мада где је име, лево или десно?), у њима има поља, после чега следи врста после двотачке.. вероватно. Овде у угаоним заградама вероватно постоје шаблони као у Ц++ (у ствари, не баш). А шта значе сви остали симболи, упитници, узвичници, проценти, хеш знаци (и очигледно значе различите ствари на различитим местима), понекад присутни, а понекад не, хексадецимални бројеви - и што је најважније, како извући из овога редовно (који сервер неће одбацити) ток бајтова? Мораћете да прочитате документацију (да, постоје везе до шеме у ЈСОН верзији у близини - али то не чини ништа јаснијим).

Отворите страницу Бинарна серијализација података и зароните у магични свет печурака и дискретне математике, нешто слично матану у 4. години. Азбука, тип, вредност, комбинатор, функционални комбинатор, нормална форма, композитни тип, полиморфни тип... и то је све само прва страница! Следеће вас чека ТЛ Лангуаге, који, иако већ садржи пример тривијалног захтева и одговора, уопште не даје одговор на типичније случајеве, што значи да ћете морати да се пробијате кроз препричавање математике преведене са руског на енглески на још осам уграђених странице!

Читаоци који су упознати са функционалним језицима и аутоматским закључивањем типова ће, наравно, видети језик описа на овом језику, чак и из примера, као много познатији, и могу рећи да то заправо није лоше у принципу. Замерке на ово су:

  • Да, циљ звучи добро, али авај, она није постигнуто
  • Образовање на руским универзитетима варира чак и међу ИТ специјалностима - нису сви похађали одговарајући курс
  • Коначно, као што ћемо видети, у пракси је тако није потребно, пошто се користи само ограничени подскуп чак и ТЛ који је описан

Као што је речено ЛеоНерд на каналу #perl у ФрееНоде ИРЦ мрежи, који је покушао да имплементира капију од Телеграма до Матрикса (превод цитата је нетачан из меморије):

Чини се као да је неко први пут уведен у теорију куцања, узбудио се и почео да покушава да се игра са њом, не марећи баш да ли је то потребно у пракси.

Уверите се сами, да ли потреба за голим типовима (инт, лонг, итд.) као нечим елементарним не поставља питања – на крају крајева, они морају да се имплементирају ручно – на пример, хајде да покушамо да изведемо из њих вектор. То је, у ствари, низ, ако настале ствари назовете правим именом.

Али пре

Кратак опис подскупа ТЛ синтаксе за оне који не читају званичну документацију

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;

Дефиниција увек почиње конструктор, након чега опционо (у пракси - увек) кроз симбол # треба ЦРЦКСНУМКС из нормализованог низа описа овог типа. Следи опис поља; ако постоје, тип може бити празан. Ово се све завршава знаком једнакости, именом типа којем овај конструктор – то јест, у ствари, подтип – припада. Момак десно од знака једнакости је полиморфна - односно може му одговарати неколико специфичних типова.

Ако се дефиниција јавља после линије ---functions---, онда ће синтакса остати иста, али ће значење бити другачије: конструктор ће постати име РПЦ функције, поља ће постати параметри (па, то јест, остаће потпуно иста дата структура, као што је описано у наставку , ово ће једноставно бити додељено значење), а „полиморфни тип“ - тип враћеног резултата. Истина, и даље ће остати полиморфна - управо дефинисана у одељку ---types---, али овај конструктор се „неће узети у обзир“. Преоптерећење типова позваних функција њиховим аргументима, тј. Из неког разлога, неколико функција са истим именом, али различитим потписима, као у Ц++, није предвиђено у ТЛ-у.

Зашто "конструктор" и "полиморфни" ако није ООП? Па, у ствари, некоме ће бити лакше да о овоме размишља у ООП терминима – полиморфни тип као апстрактна класа, а конструктори су њени директни потомци, и final у терминологији низа језика. У ствари, наравно, само овде сличност са стварним преоптерећеним методама конструктора у ОО програмским језицима. Пошто су овде само структуре података, не постоје методе (иако је даље опис функција и метода прилично способан да створи забуну у глави да оне постоје, али то је друга ствар) – можете замислити конструктор као вредност из која се гради укуцајте када читате ток бајтова.

Како се ово дешава? Десеријализатор, који увек чита 4 бајта, види вредност 0xcrc32 - и разуме шта ће се следеће десити field1 са типом int, тј. чита тачно 4 бајта, на овом пољу изнад са типом PolymorType читати. Види 0x2crc32 и разуме да постоје још два поља, прво long, што значи да читамо 8 бајтова. И онда опет сложени тип, који је десериализован на исти начин. На пример, Type3 могу бити декларисани у кругу чим два конструктора, респективно, тада се морају састати било који 0x12abcd34, након чега треба да прочитате још 4 бајта intИли 0x6789cdef, након чега неће бити ништа. Било шта друго - потребно је да избаците изузетак. У сваком случају, након овога се враћамо на читање 4 бајта int поља field_c в constructorTwo и тиме завршавамо читање наше PolymorType.

Коначно, ако вас ухвате 0xdeadcrc за constructorThree, онда све постаје компликованије. Наше прво поље је bit_flags_of_what_really_present са типом # - у ствари, ово је само псеудоним за тип nat, што значи "природни број". То јест, у ствари, унсигнед инт је, иначе, једини случај када се неозначени бројеви јављају у реалним колима. Дакле, следећа је конструкција са знаком питања, што значи да ће ово поље – бити присутно на жици само ако је одговарајући бит постављен у поље на које се позива (приближно као тернарни оператор). Дакле, претпоставимо да је овај бит постављен, што значи да даље треба да прочитамо поље попут Type, који у нашем примеру има 2 конструктора. Један је празан (састоји се само од идентификатора), други има поље ids са типом ids:Vector<long>.

Можда мислите да су и шаблони и генерици у професионалцима или Јави. Али не. Скоро. Ово сингл случају коришћења угаоних заграда у реалним колима, а користи се САМО за вектор. У току бајтова, то ће бити 4 ЦРЦ32 бајта за сам тип Вецтор, увек исти, затим 4 бајта - број елемената низа, а затим сами ови елементи.

Додајте овоме чињеницу да се серијализација увек дешава у речима од 4 бајта, сви типови су вишеструки - уграђени типови су такође описани bytes и string са ручном серијализацијом дужине и овим поравнањем за 4 - па, изгледа да звучи нормално, па чак и релативно ефикасно? Иако се за ТЛ тврди да је ефикасна бинарна серијализација, дођавола с њима, са проширењем скоро свега, чак и Булових вредности и низова од једног карактера на 4 бајта, да ли ће ЈСОН и даље бити много дебљи? Видите, чак и непотребна поља могу да се прескоче са битним заставицама, све је сасвим добро, па чак и прошириво за будућност, па зашто не бисте касније додали нова опциона поља у конструктор?..

Али не, ако прочитате не мој кратак опис, већ пуну документацију, и размислите о имплементацији. Прво, ЦРЦ32 конструктора се израчунава према нормализованој линији текстуалног описа шеме (уклони додатни размак, итд.) - тако да ако се дода ново поље, линија описа типа ће се променити, а самим тим и њен ЦРЦ32 и , сходно томе, серијализација. А шта би стари клијент урадио да добије поље са новим постављеним заставама, а не зна шта даље са њима?..

Друго, да се подсетимо ЦРЦКСНУМКС, који се овде користи у суштини као хеш функције да се јединствено одреди који се тип (де)серијализује. Овде смо суочени са проблемом судара - и не, вероватноћа није један према 232, већ много већа. Ко се сетио да је ЦРЦ32 дизајниран да детектује (и исправља) грешке у комуникационом каналу, и сходно томе побољшава ова својства на штету других? На пример, није га брига за преуређивање бајтова: ако израчунате ЦРЦ32 из две линије, у другом замените прва 4 бајта са следећа 4 бајта - биће исто. Када су наш унос текстуални низови са латиничног писма (и мало знакова интерпункције), а ова имена нису нарочито насумична, вероватноћа таквог преуређивања се увелико повећава.

Узгред, ко је проверио шта има? стварно ЦРЦ32? Један од раних извора (чак и пре Волтмана) имао је хеш функцију која је множила сваки знак са бројем 239, тако вољену овим људима, ха ха!

Коначно, у реду, схватили смо да конструктори са типом поља Vector<int> и Vector<PolymorType> имаће другачији ЦРЦ32. Шта је са перформансама на мрежи? И са теоријске тачке гледишта, да ли ово постаје део типа? Рецимо да проследимо низ од десет хиљада бројева, па са Vector<int> све је јасно, дужина и још 40000 бајтова. Шта ако ово Vector<Type2>, који се састоји од само једног поља int и само је у типу - да ли треба да поновимо 10000кабцдеф0 34 пута, а затим 4 бајта int, или је језик у стању да га НЕЗАВИСНИ за нас од конструктора fixedVec и уместо 80000 бајтова пренети поново само 40000?

Ово уопште није празно теоријско питање – замислите да добијете листу корисника групе, од којих сваки има ИД, име, презиме – разлика у количини података пренетих преко мобилне везе може бити значајна. Управо нам се рекламира ефикасност Телеграм серијализације.

Тако…

Вектор, који никада није објављен

Ако покушате да прођете кроз странице описа комбинатора и тако даље, видећете да вектор (па чак и матрица) формално покушава да се избаци кроз торке од неколико листова. Али на крају забораве, последњи корак се прескаче и једноставно се даје дефиниција вектора, који још није везан за тип. Шта је било? У језицима програмирање, посебно функционалних, сасвим је типично да се структура описује рекурзивно - компајлер са својом лењом евалуацијом ће све разумети и урадити сам. У језику серијализација података оно што је потребно је ЕФИКАСНОСТ: довољно је једноставно описати списак, тј. структура два елемента – први је елемент података, други је иста сама структура или празан простор за реп (пак (cons) у Лисп). Али ово ће очигледно захтевати сваки елемент троши додатна 4 бајта (ЦРЦ32 у случају у ТЛ) да опише свој тип. Низ се такође може лако описати фиксна величина, али у случају низа унапред непознате дужине, прекидамо.

Стога, пошто ТЛ не дозвољава излаз вектора, морао је бити додат са стране. На крају, документација каже:

Серијализација увек користи исти конструктор „вектор“ (цонст 0к1цб5ц415 = црц32(„вецтор т:Типе # [ т ] = Вецтор т“) који не зависи од специфичне вредности променљиве типа т.

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

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

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

... али се не фокусира на то. Када, уморни од гажења кроз натезање математике (можда вам је чак познато са универзитетског курса), одлучите да одустанете и заправо погледате како да радите са њом у пракси, у глави вам остаје утисак да је ово озбиљно Математика у сржи, јасно су је измислили Цоол Пеопле (два математичара - АЦМ победник), а не било ко. Циљ – да се покаже – је постигнут.

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

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

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

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

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

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

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

У овом тренутку почињете да размишљате - да ли је такав ТЛ неопходан? Можда би за колица било могуће користити људски серијализатор, исти протобуф који је тада већ постојао? То је била теорија, хајде да погледамо праксу.

Постојеће ТЛ имплементације у коду

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

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>;

Ово је дефиниција типа шаблона хасхмап као вектор парова инт - Типе. У Ц++ би то изгледало отприлике овако:

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

тако, alpha - кључна реч! Али само у Ц++ можете писати Т, али треба писати алфа, бета... Али не више од 8 параметара, ту се фантазија завршава. Изгледа да су се некада у Санкт Петербургу водили овакви дијалози:

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

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

Али ово је била прва објављена имплементација ТЛ-а „уопштено“. Хајде да пређемо на разматрање имплементације у самим клијентима Телеграма.

Реч Василију:

Василиј, [09.10.18 17:07] Највише од свега, гузица је врела јер су направили гомилу апстракција, а онда закуцали шраф на њих, и покрили генератор кода штакама
Као резултат, прво из доцк пилот.јпг
Затим из кода дзхекицхан.вебп

Наравно, од људи који су упознати са алгоритмима и математиком, можемо очекивати да су прочитали Ахо, Уллманна и да су упознати са алатима који су постали де фацто стандард у индустрији током деценија за писање њихових ДСЛ компајлера, зар не?..

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

16.12 04:18 Василиј: Мислим да неко није савладао лек+иацц
16.12 04:18 Василиј: Не могу другачије да објасним
16.12 04:18 Василиј: па, или су плаћени за број редова у ВК
16.12 04:19 Василиј: 3к+ редова итд.<censored> уместо парсера

Можда изузетак? Да видимо како ради Ово је ЗВАНИЧНИ клијент - Телеграм Десктоп:

    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+ редова у Питхон-у, пар регуларних израза + специјални случајеви попут вектора, који је, наравно, декларисан у шеми како треба да буде према ТЛ синтакси, али су се ослањали на ову синтаксу да би га рашчланили... Поставља се питање зашто је све то било чудо?иВише је слојевит ако га ионако нико неће рашчланити према документацији?!

Успут... Сећаш се да смо разговарали о ЦРЦ32 провери? Дакле, у генератору кодова Телеграм Десктоп постоји листа изузетака за оне типове у којима је израчунат ЦРЦ32 не подудара се са оним назначеним на дијаграму!

Василиј, [18.12/22 49:XNUMX] а овде бих размислио да ли је потребан такав ТЛ
ако бих желео да се петљам са алтернативним имплементацијама, почео бих да убацујем преломе редова, половина парсера ће се покварити на дефиницијама са више редова
тдесктоп, међутим, такође

Запамтите тачку о једнолинеру, на то ћемо се вратити мало касније.

Добро, телеграм-цли је незваничан, Телеграм Десктоп је званичан, али шта је са осталима? Ко зна?.. У коду Андроид клијента уопште није било парсера шеме (што поставља питања о отвореном коду, али ово је за други део), али је било још неколико смешних делова кода, али више о њима у пододељак у наставку.

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

Василиј: 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] и на тдесктоп-у?

Васили, [22.06.18 19:23] Али њихов ТЛ парсер на редовним моторима највероватније неће ни ово појести

// parsed manually

ТЛ је лепа апстракција, нико је не примењује у потпуности

А % није у њиховој верзији шеме

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

Пронађено је у граматици, могли су једноставно заборавити да опишу семантику

Видели сте документ на ТЛ, не можете да схватите без пола литра

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

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

    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];

оне. једноставније је благо речено.”

Генерално, као резултат тога, парсер и генератор кода за стварно коришћени подскуп ТЛ-а уклапају се у приближно 100 граматичких линија и ~300 редова генератора (рачунајући све print'с генерисани код), укључујући гомиле информација о типу за интроспекцију у сваком разреду. Сваки полиморфни тип се претвара у празну апстрактну основну класу, а конструктори наслеђују од ње и имају методе за серијализацију и десеријализацију.

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

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

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

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

  • part_size % 1024 = 0 (дељиво са 1 КБ)
  • 524288 % part_size = 0 (512КБ мора бити једнако дељиво са парт_сизе)

Последњи део не мора да задовољи ове услове, под условом да је његова величина мања од парт_сизе.

Сваки део треба да има редни број, филе_парт, са вредношћу у распону од 0 до 2,999.

Након што је датотека партиционирана, потребно је да изаберете метод за њено чување на серверу. Користите уплоад.савеБигФилеПарт у случају да је пуна величина датотеке већа од 10 МБ и уплоад.савеФилеПарт за мање датотеке.
[…] може бити враћена једна од следећих грешака у уносу података:

  • ФИЛЕ_ПАРТС_ИНВАЛИД — Неважећи број делова. Вредност није између 1..3000

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

Ништа од овога није у ТЛ-у. Али постоји, на пример, у ЈСОН шеми. А ако неко други може да расправља о дељивости 512 КБ, да ово још увек треба да се провери у коду, онда се уверите да клијент једноставно није могао послати број ван домета 1..3000 (а одговарајућа грешка није могла настати) било би могуће, зар не?..

Узгред, о грешкама и повратним вредностима. Чак и они који су радили са ТЛ замагљују очи - то нам није одмах синуло сваки функција у ТЛ-у заправо може да врати не само описани тип враћања, већ и грешку. Али ово се ни на који начин не може закључити користећи сам ТЛ. Наравно, то је већ јасно и нема потребе за било чим у пракси (иако се у ствари РПЦ може урадити на различите начине, на ово ћемо се вратити касније) – али шта је са Чистоћом концепата математике апстрактних типова? из небеског света?.. Покупио сам тегљач – па га усклади.

И на крају, шта је са читљивошћу? Па, тамо, уопште, волео бих опис имате га тачно у шеми (у ЈСОН шеми, опет, јесте), али ако сте већ напрегнути са тим, шта је са практичном страном - барем тривијално гледајући разлике током ажурирања? Уверите се сами на прави примери:

-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;

Зависи од свих, али ГитХуб, на пример, одбија да истакне промене унутар тако дугих редова. Игра „нађи 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 типа за све могуће опције. Комбинаторна експлозија. Тако се кристална чистоћа теорије ТЛ још једном разбила о ливено гвожђе сурове реалности серијализације.

Поред тога, на неким местима ови момци сами крше сопствену типологију. На пример, у МТПрото (следеће поглавље) одговор се може компримовати помоћу Гзипа, све је у реду - осим што су слојеви и коло нарушени. Још једном, није пожњео сам РпцРесулт, већ његов садржај. Па, зашто ово?.. Морао сам да сечем у штаку да компресија ради било где.

Или други пример, једном смо открили грешку - послата је InputPeerUser уместо InputUser. Или обрнуто. Али успело је! То јест, сервер није марио за тип. Како то може бити? Одговор нам могу дати фрагменти кода из телеграм-цли:

  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. Документација нам говори о посебној ТЛ особини:

Ако клијент подржава слој 2, онда се мора користити следећи конструктор:

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

У пракси, то значи да пре сваког АПИ позива, инт са вредношћу 0x289dd1f6 мора се додати испред броја методе.

Звучи нормално. Али шта се даље догодило? Онда се појавио

invokeWithLayer3#b7475268 query:!X = X;

Па шта је следеће? Као што можете претпоставити,

invokeWithLayer4#dea0d430 query:!X = X;

Смешно? Не, прерано је за смех, размислите о томе сваки захтев из другог слоја треба да буде умотан у тако посебан тип - ако су сви различити за вас, како другачије можете да их разликујете? А додавање само 4 бајта испред је прилично ефикасан метод. Тако,

invokeWithLayer5#417a57ae query:!X = X;

Али очигледно је да ће то после неког времена постати нека врста вакханалије. И дошло је решење:

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

Ура! Након 9 верзија, коначно смо дошли до онога што је рађено у интернет протоколима још 80-их година - договарање верзије једном на почетку конекције!

Па шта је следеће?..

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

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

Баш тако?..

Василиј, [16.07.18 14:01] И у петак сам мислио:
Телесервер шаље догађаје без захтева. Захтеви морају бити умотани у ИнвокеВитхЛаиер. Сервер не омота ажурирања; не постоји структура за омотавање одговора и ажурирања.

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

Вадим Гончаров, [16.07.18 14:02] зар ИнвокеВитхЛаиер у принципу није штака?

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

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

Узгред, произилази да клијент није обезбеђен

Ажурирања, тј. тип Updates у шеми, ово је оно што сервер шаље клијенту не као одговор на захтев АПИ-ја, већ независно када дође до догађаја. Ово је сложена тема о којој ће бити речи у другом посту, али за сада је важно знати да сервер чува ажурирања чак и када је клијент ван мреже.

Дакле, ако одбијете да умотате сваки пакет да назначи његову верзију, ово логично води до следећих могућих проблема:

  • сервер шаље ажурирања клијенту чак и пре него што је клијент обавестио коју верзију подржава
  • шта да радим након надоградње клијента?
  • који гаранциједа се мишљење сервера о броју слоја неће променити током процеса?

Да ли мислите да је ово чисто теоријска спекулација, а у пракси се то не може десити, јер је сервер исправно написан (барем је добро тестиран)? Ха! Како год да је!

Управо на ово смо наишли у августу. 14. августа појавиле су се поруке да се нешто ажурира на Телеграм серверима... а затим у логовима:

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.

а затим неколико мегабајта трагова стека (па, истовремено је поправљено евидентирање). На крају крајева, ако нешто није препознато у вашем ТЛ-у, то је бинарно по потпису, даље низ линију СВЕ иде, декодирање ће постати немогуће. Шта треба да урадите у таквој ситуацији?

Па, прва ствар која некоме падне на памет је да прекине везу и покуша поново. Није помогло. Гугламо ЦРЦ32 - испоставило се да су то објекти из шеме 73, иако смо радили на 82. Пажљиво гледамо дневнике - постоје идентификатори из две различите шеме!

Можда је проблем искључиво у нашем незваничном клијенту? Не, покрећемо Телеграм Десктоп 1.2.17 (верзија се испоручује у бројним Линук дистрибуцијама), он пише у евиденцију изузетака: МТП Неочекивани ИД типа #б5223б0ф прочитан у МТПМессагеМедиа…

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

Гугл је показао да се сличан проблем већ десио једном од незваничних клијената, али су тада бројеви верзија и, сходно томе, претпоставке били другачији...

Па шта да радимо? Василиј и ја смо се раздвојили: он је покушао да ажурира коло на 91, ја сам одлучио да сачекам неколико дана и испробам 73. Обе методе су функционисале, али пошто су емпиријске, нема разумевања колико верзија горе или доле вам треба да скочите, или колико дуго треба да чекате .

Касније сам успео да поновим ситуацију: покрећемо клијента, искључујемо га, поново компајлирамо коло на други слој, рестартујемо, поново откривамо проблем, враћамо се на претходни - упс, нема промене кола и клијент се поново покреће за неколико минута ће помоћи. Добићете мешавину структура података из различитих слојева.

Објашњење? Као што можете претпоставити из различитих индиректних симптома, сервер се састоји од много процеса различитих типова на различитим машинама. Највероватније је сервер који је одговоран за „баферовање“ ставио у ред оно што су му дали надређени, а они су то дали у шеми која је била на снази у време генерисања. И док овај ред није „труо“, ништа се није могло учинити.

Можда... али ово је страшна штака?!.. Не, пре него што размишљамо о лудим идејама, погледајмо кодекс званичних клијената. У Андроид верзији не налазимо никакав ТЛ парсер, али налазимо велику датотеку (ГитХуб одбија да је доради) са (де)серијализацијом. Ево исечака кода:

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

Момци, зар не можете ни да одлучите шта је унутар једног слоја?! Добро, добро, рецимо „два“ су пуштена са грешком, па, дешава се, али ТРИ?.. Одмах, опет исти раке? Каква је ово порнографија, извини?..

У изворном коду Телеграм Десктоп-а, иначе, дешава се слична ствар - ако јесте, неколико урезивања у низу на шему не мењају њен број слоја, већ нешто поправљају. У условима када не постоји званичан извор података за шему, одакле се могу добити осим изворног кода званичног наручиоца? А ако узмете одатле, не можете бити сигурни да је шема потпуно исправна док не тестирате све методе.

Како се ово уопште може тестирати? Надам се да ће љубитељи јединичних, функционалних и других тестова поделити у коментарима.

У реду, хајде да погледамо још један део кода:

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;

Овај коментар „ручно креиран“ сугерише да је само део ове датотеке написан ручно (можете ли да замислите целу ноћну мору одржавања?), а остатак је машински генерисан. Међутим, онда се поставља друго питање – да ли су извори доступни не потпуно (а ла ГПЛ блобс у Линук кернелу), али ово је већ тема за други део.

Али доста. Пређимо на протокол на коме се покреће сва ова серијализација.

МТПрото

Дакле, отворимо општи опис и детаљан опис протокола а прво на шта наиђемо је терминологија. И са обиљем свега. Уопштено говорећи, изгледа да је ово власничка карактеристика Телеграма – називање ствари другачије на различитим местима, или различите ствари једном речју, или обрнуто (на пример, у АПИ-ју високог нивоа, ако видите пакет налепница, то није шта си мислио).

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

Транспортни слој

Прва ствар је транспорт. Они ће нам рећи о 5 опција:

  • ТЦП
  • Вебсоцкет
  • Вебсоцкет преко ХТТПС-а
  • ХТТП
  • ХТТПС

Василиј, [15.06.18 15:04] Постоји и УДП транспорт, али није документован

И ТЦП у три варијанте

Први је сличан УДП-у преко ТЦП-а, сваки пакет укључује редни број и црц
Зашто је читање докумената на колицима тако болно?

Па, ево га сада ТЦП већ у 4 варијанте:

  • Скраћено
  • Средњи
  • Паддед интермедиате
  • Пун

Па, ок, Паддед интермедиате за МТПроки, ово је касније додато због добро познатих догађаја. Али зашто још две верзије (укупно три) када бисте могли да прођете са једном? Сва четири се суштински разликују само по томе како подесити дужину и носивост главног МТПрото-а, о чему ће даље бити речи:

  • у скраћеном облику је 1 или 4 бајта, али не 0кеф, већ тело
  • у средњем ово је 4 бајта дужине и поље, а први пут клијент мора да пошаље 0xeeeeeeee да укаже да је средњи
  • у Фулл најзависнији, са тачке гледишта мрежног оператера: дужина, редни број, а НЕ ОНАЈ који је углавном МТПрото, тело, ЦРЦ32. Да, све ово је на врху ТЦП-а. Што нам обезбеђује поуздан транспорт у облику секвенцијалног тока бајтова; нису потребне секвенце, посебно контролни суми. Добро, сад ће ми неко приговорити да ТЦП има 16-битну контролну суму, па долази до оштећења података. Одлично, али ми заправо имамо криптографски протокол са хешовима дужим од 16 бајтова, све ове грешке - па чак и више - ће бити ухваћене СХА неусклађеношћу на вишем нивоу. Нема смисла у ЦРЦ32 поврх овога.

Хајде да упоредимо скраћено, у коме је могућ један бајт дужине, са средњим, што оправдава „У случају да је потребно поравнање података од 4 бајта“, што је прилично бесмислица. Шта, верује се да су програмери Телеграма толико неспособни да не могу да читају податке са сокета у усклађени бафер? Ово и даље морате да урадите, јер читање може да вам врати било који број бајтова (а постоје и прокси сервери, на пример...). Или, с друге стране, зашто блокирати скраћено ако ћемо и даље имати позамашан пад на врху од 16 бајтова - уштедите 3 бајта Понекад ?

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

Друге опције транспорта, укљ. Веб и МТПроки, нећемо сада разматрати, можда у неком другом посту, ако постоји захтев. О овом истом МТПроки-у, да се сада само подсетимо да су провајдери убрзо након објављивања 2018. брзо научили да га блокирају, намењен блокирање заобилазницеПо Величина пакета! И такође чињеница да је МТПроки сервер написан (опет од стране Волтмана) у Ц-у био превише везан за Линук специфичности, иако то уопште није било потребно (Пхил Кулин ће потврдити), и да би сличан сервер било у Го или Ноде.јс стане у мање од стотину редова.

Али закључке о техничкој писмености ових људи донећемо на крају одељка, након разматрања других питања. За сада, пређимо на ОСИ слој 5, сесија - на коју су поставили МТПрото сесију.

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

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

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

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

Постоје два ентитета тзв седница - један у корисничком интерфејсу званичних клијената под „тренутним сесијама“, где свака сесија одговара читавом уређају/ОС-у.
Други је МТПрото сесија, који у себи има редни број поруке (у смислу ниског нивоа) и који може трајати између различитих ТЦП веза. Неколико МТПрото сесија се може инсталирати истовремено, на пример, да би се убрзало преузимање датотека.

Између ово двоје сесије постоји концепт овлашћење. У дегенерисаном случају то можемо рећи УИ сесија исто је као овлашћење, али авај, све је компликовано. Погледајмо:

  • Корисник на новом уређају прво генерише аутх_кеи и везује га за налог, на пример путем СМС-а - ето зашто овлашћење
  • Десило се у првом МТПрото сесија, који има session_id унутар себе.
  • У овом кораку, комбинација овлашћење и session_id могао се назвати пример - ова реч се појављује у документацији и коду неких клијената
  • Затим, клијент може да отвори неки МТПрото сесије под истим аутх_кеи - у исти ДЦ.
  • Затим ће једног дана клијент морати да затражи датотеку од други ДЦ - и за овај ДЦ ће бити генерисан нови аутх_кеи !
  • Да обавести систем да се не региструје нови корисник, већ исти овлашћење (УИ сесија), клијент користи АПИ позиве auth.exportAuthorization у кући ДЦ auth.importAuthorization у новом ДЦ.
  • Све је исто, неколико може бити отворено МТПрото сесије (свако са својим session_id) овом новом ДЦ, под његов аутх_кеи.
  • Коначно, клијент може желети савршену тајност унапред. Сваки аутх_кеи био трајан кључ - по ДЦ - и клијент може да позове auth.bindTempAuthKey за употребу привремен аутх_кеи - и опет, само један темп_аутх_кеи по ДЦ, заједнички за све МТПрото сесије овом ДЦ.

Запазите то со (и будуће соли) је такође један на аутх_кеи оне. подељено између свих МТПрото сесије истом ДЦ.

Шта значи „између различитих ТЦП веза“? Дакле, ово значи нешто слично ауторизациони колачић на веб локацији – опстаје (преживљава) многе ТЦП везе са датим сервером, али једног дана се поквари. Само за разлику од ХТТП-а, у МТПрото поруке у оквиру сесије се секвенцијално нумеришу и потврђују; ако су ушле у тунел, веза је прекинута - након успостављања нове везе, сервер ће љубазно послати све у овој сесији што није испоручио у претходној ТЦП веза.

Међутим, горе наведене информације су сажете након вишемесечне истраге. У међувремену, да ли имплементирамо нашег клијента од нуле? - вратимо се на почетак.

Па хајде да генеришемо auth_key на Диффие-Хеллман верзије из Телеграма. Хајде да покушамо да разумемо документацију...

Василиј, [19.06.18 20:05] дата_витх_хасх := СХА1(подаци) + подаци + (било који насумични бајт); тако да је дужина једнака 255 бајтова;
енцриптед_дата := РСА(дата_витх_хасх, сервер_публиц_кеи); број од 255 бајта (биг ендиан) се подиже на потребну снагу преко потребног модула, а резултат се чува као број од 256 бајта.

Имају неку дрогу ДХ

Не личи на ДХ здраве особе
У дк-у не постоје два јавна кључа

Па, на крају је ово решено, али је остао талог - доказ о раду је од стране клијента да је успео да факторише број. Врста заштите од ДоС напада. А РСА кључ се користи само једном у једном правцу, у суштини за шифровање new_nonce. Али док ће ова наизглед једноставна операција успети, са чиме ћете морати да се суочите?

Василиј, [20.06.18/00/26 XNUMX:XNUMX] Још нисам стигао до аппид захтева

Овај захтев сам послао ДХ

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

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

Па сам му рекао: „Ухвати своје срање шифровано серверским кључем са оваквим отиском прста, желим ДХ“, а оно је одговорило са глупим 404

Шта мислите о овом одговору сервера? Шта да радим? Нема ко да пита (али о томе у другом делу).

Овде се све камате обављају на оптуженичкој клупи

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

Два 32-битна броја. Спаковао сам их као и све остале

Али не, ово двоје треба прво додати у ред као БЕ

Вадим Гончаров, [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)

СХА1 и сха2

Па хајде да кажемо auth_key примили смо 2048 бита користећи Диффие-Хеллман. Шта је следеће? Затим откривамо да се нижих 1024 бита овог кључа не користе ни на који начин... али хајде да размислимо о овоме за сада. У овом кораку имамо заједничку тајну са сервером. Успостављен је аналог ТЛС сесије, што је веома скупа процедура. Али сервер још увек не зна ништа о томе ко смо ми! Не још, заправо. овлашћење. Оне. ако сте мислили у терминима „логин-пассворд“, као што сте некада радили у ИЦК-у, или барем „логин-кеи“, као у ССХ-у (на пример, на неком гитлаб/гитхубу). Добили смо анонимну. Шта ако нам сервер каже „ове телефонске бројеве сервисира други ДЦ“? Или чак „ваш број телефона је забрањен“? Најбоље што можемо да урадимо је да задржимо кључ у нади да ће бити користан и да до тада неће покварити.

Иначе, „примили смо” га са резервом. На пример, да ли верујемо серверу? Шта ако је лажно? Криптографске провере би биле потребне:

Василиј, [21.06.18 17:53] Нуде мобилним клијентима да провере 2кбит број за прималност%)

Али то уопште није јасно, нафеијоа

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

Није речено. Да видимо шта званични Андроид клијент ради у овом случају? А Ето шта (и да, цео фајл је занимљив) - како кажу, оставићу само ово овде:

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

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

Ок, имамо главни кључ. За пријаву, тј. слати захтеве, потребно је да извршите даље шифровање, користећи АЕС.

Кључ поруке је дефинисан као 128 средњих битова СХА256 тела поруке (укључујући сесију, ИД поруке, итд.), укључујући бајтове за попуњавање, уз 32 бајта преузета из ауторизационог кључа.

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

Схватио сам auth_key. Све. Иза њих... није јасно из документа. Слободно проучите отворени изворни код.

Имајте на уму да МТПрото 2.0 захтева од 12 до 1024 бајтова допуна, и даље под условом да резултујућа дужина поруке буде дељива са 16 бајтова.

Дакле, колико паддинга треба да додате?

И да, постоји и 404 у случају грешке

Ако је неко пажљиво проучио дијаграм и текст документације, приметио је да тамо нема МАЦ-а. И тај АЕС се користи у одређеном ИГЕ режиму који се нигде другде не користи. Они, наравно, пишу о томе у својим ФАК... Овде, као, сам кључ поруке је такође СХА хеш дешифрованих података, који се користи за проверу интегритета - а у случају неслагања, документација из неког разлога препоручује да их прећутно игноришемо (али шта је са безбедношћу, шта ако нас сломе?).

Нисам криптограф, можда нема ништа лоше у овом режиму у овом случају са теоријске тачке гледишта. Али могу јасно да наведем практичан проблем, користећи Телеграм Десктоп као пример. Шифрује локални кеш (све ове Д877Ф783Д5Д3ЕФ8Ц) на исти начин као и поруке у МТПрото (само у овом случају верзија 1.0), тј. прво кључ поруке, затим сами подаци (и негде поред главног великог auth_key 256 бајтова, без којих msg_key бескорисно). Дакле, проблем постаје приметан на великим датотекама. Наиме, потребно је да чувате две копије података – шифровану и дешифровану. А ако постоје мегабајти, или стриминг видео, на пример?.. Класичне шеме са МАЦ-ом после шифрованог текста омогућавају вам да прочитате његов ток, одмах га преносите. Али са МТПрото ћете морати у почетку шифрујте или дешифрујте целу поруку, па је тек онда пренесите на мрежу или на диск. Стога, у најновијим верзијама Телеграма Десктоп у кешу у user_data Такође се користи други формат - са АЕС-ом у ЦТР режиму.

Василиј, [21.06.18 01:27] Ох, сазнао сам шта је ИГЕ: ИГЕ је био први покушај „режима шифровања аутентификације“, првобитно за Керберос. Био је то неуспешан покушај (не пружа заштиту интегритета) и морао је да буде уклоњен. То је био почетак 20-годишње потраге за начином шифровања који функционише за потврду идентитета, који је недавно кулминирао у режимима као што су ОЦБ и ГЦМ.

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

Тим који стоји иза Телеграма, предвођен Николајем Дуровом, састоји се од шест АЦМ шампиона, од којих половина доктори математике. Требало им је око две године да уведу тренутну верзију МТПрото-а.

То је смешно. Две године на нижем нивоу

Или можете само узети тлс

У реду, рецимо да смо урадили шифровање и друге нијансе. Да ли је коначно могуће слати захтеве серијализоване у ТЛ-у и десериализовати одговоре? Па шта и како треба послати? Ево, рецимо, метода инитЦоннецтион, можда је то то?

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

Прихвата апп_ид, девице_модел, систем_версион, апп_версион и ланг_цоде.

И неки упит

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

Ако је све било приближно јасно са инвокеВитхЛаиер, шта онда овде није у реду? Испоставило се, рецимо да имамо - клијент је већ имао нешто да пита сервер - постоји захтев који смо желели да пошаљемо:

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

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

Само мали део АПИ метода је доступан неовлашћеним корисницима:

  • аутх.сендЦоде
  • аутх.ресендЦоде
  • аццоунт.гетПассворд
  • аутх.цхецкПассворд
  • аутх.цхецкПхоне
  • аутх.сигнУп
  • аутх.сигнИн
  • аутх.импортАутхоризатион
  • хелп.гетЦонфиг
  • хелп.гетНеарестДц
  • хелп.гетАппУпдате
  • хелп.гетЦднЦонфиг
  • лангпацк.гетЛангПацк
  • лангпацк.гетСтрингс
  • лангпацк.гетДифференце
  • лангпацк.гетЛангуагес
  • лангпацк.гетЛангуаге

Први од њих, auth.sendCode, а ту је и онај неговани први захтев у коме шаљемо апи_ид и апи_хасх, а након тога добијамо СМС са кодом. А ако смо у погрешном ДЦ (телефонске бројеве у овој земљи опслужује друга, на пример), онда ћемо добити грешку са бројем жељеног ДЦ. Да бисте сазнали на коју ИП адресу преко ДЦ броја треба да се повежете, помозите нам help.getConfig. Некада је било само 5 уноса, али након познатих догађаја 2018, број се значајно повећао.

Сада да се подсетимо да смо анонимно дошли до ове фазе на серверу. Није ли прескупо само добити ИП адресу? Зашто не урадите ово, и друге операције, у нешифрованом делу МТПрото-а? Чујем примедбу: „како да будемо сигурни да неће РКН одговорити лажним адресама?“ На ово се сећамо да су, генерално, званични клијенти РСА кључеви су уграђени, тј. можеш ли само знак ова информација. Заправо, ово се већ ради за информације о заобилажењу блокирања које клијенти добијају преко других канала (логично, то се не може урадити у самом МТПрото-у; такође морате знати где да се повежете).

ОК. У овој фази ауторизације клијента, још нисмо овлашћени и нисмо регистровали нашу апликацију. За сада само желимо да видимо шта сервер реагује на методе доступне неовлашћеном кориснику. И овде…

Василиј, [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;

У шеми, прво долази друго

У шеми тдесктоп трећа вредност је

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

...Приметили сте да смо већ некако прешли на АПИ, тј. на следећи ниво, а пропустили сте нешто у теми МТПрото? Не изненађује:

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

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

Али они стално мешају различите нивое стека, тако да није увек јасно где се мтпрото завршио и где је почео следећи ниво

Како се мешају? Па, ево истог привременог кључа за ПФС, на пример (успут, Телеграм Десктоп то не може). Извршава се путем АПИ захтева auth.bindTempAuthKey, тј. са највишег нивоа. Али у исто време омета шифровање на нижем нивоу - након тога, на пример, морате то да урадите поново initConnection итд., ово није само нормалан захтев. Оно што је такође посебно је то што можете имати само ЈЕДАН привремени кључ по ДЦ-у, иако поље auth_key_id у свакој поруци вам омогућава да промените кључ бар у свакој поруци, и да сервер има право да "заборави" привремени кључ у било ком тренутку - документација не каже шта да се ради у овом случају... добро, зашто не би Немате неколико кључева, као код сета будућих соли, и?..

Постоји неколико других ствари које вреди напоменути у вези са темом МТПрото.

Поруке порука, мсг_ид, мсг_секно, потврде, пингови у погрешном правцу и друге идиосинкразије

Зашто морате да знате о њима? Зато што „цуре“ на виши ниво и морате их бити свесни када радите са АПИ-јем. Претпоставимо да нас мсг_кеи не занима; доњи ниво је све дешифровао за нас. Али унутар дешифрованих података имамо следећа поља (такође и дужину података, тако да знамо где је паддинг, али то није важно):

  • сол - инт64
  • сессион_ид - инт64
  • мессаге_ид — инт64
  • сек_но - инт32

Подсетимо, за цео ДЦ постоји само једна со. Зашто знати за њу? Не само зато што постоји захтев get_future_salts, који вам говори који ће интервали важити, али и зато што ако је ваша со „трула“, онда ће се порука (захтев) једноставно изгубити. Сервер ће, наравно, пријавити нову сол издавањем new_session_created - али са старим ћете морати некако поново да га пошаљете, нпр. И ово питање утиче на архитектуру апликације.

Серверу је дозвољено да потпуно одустане од сесије и одговори на овај начин из много разлога. У ствари, шта је МТПрото сесија са стране клијента? Ово су два броја session_id и seq_no поруке у оквиру ове сесије. Па, и основна ТЦП веза, наравно. Рецимо да наш клијент још увек не зна како да уради многе ствари, искључио се и поново спојио. Ако се ово догодило брзо - стара сесија се наставила у новој ТЦП вези, повећајте seq_no даље. Ако то потраје, сервер би могао да га избрише, јер је са његове стране и ред, како смо сазнали.

Шта би требало да буде seq_no? Ох, то је зезнуто питање. Покушајте да искрено разумете шта се мислило:

Порука у вези са садржајем

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

Редни број поруке (мсг_секно)

32-битни број једнак двоструком броју порука „у вези са садржајем“ (оних које захтевају потврду, а посебно оних које нису контејнери) које је креирао пошиљалац пре ове поруке и накнадно увећан за један ако је тренутна порука порука у вези са садржајем. Контејнер се увек генерише након целог садржаја; према томе, њен редни број је већи или једнак редним бројевима порука које се налазе у њему.

Какав је ово циркус са повећањем за 1, па још за 2?.. Претпостављам да су у почетку значили „најмањи бит за АЦК, остало је број“, али резултат није сасвим исти - посебно, излази, може се послати неки потврде које имају исто seq_no! Како? Па, на пример, сервер нам нешто пошаље, пошаље, а ми сами ћутимо, само одговарамо сервисним порукама које потврђују пријем његових порука. У овом случају, наше одлазне потврде ће имати исти одлазни број. Ако сте упознати са ТЦП-ом и мислите да ово звучи некако дивље, али не изгледа баш дивље, јер у ТЦП-у seq_no се не мења, али потврда иде на seq_no на другој страни, пожурићу да вас узнемирим. Потврде се налазе у МТПрото НЕ на seq_no, као у ТЦП-у, али по msg_id !

Шта је ово msg_id, најважније од ових поља? Јединствени идентификатор поруке, као што име каже. Дефинише се као 64-битни број, чији најнижи битови опет имају магију „сервер-не-сервер“, а остатак је Уник временска ознака, укључујући разломак, померен за 32 бита улево. Оне. временска ознака сама по себи (а поруке са временима која се превише разликују ће бити одбијене од стране сервера). Из овога се испоставља да је генерално ово идентификатор који је глобалан за клијента. С обзиром на то – да се подсетимо session_id - гарантујемо: Ни под којим условима се порука намењена једној сесији не може послати у другу сесију. Односно, испоставља се да већ постоји три ниво - сесија, број сесије, ид поруке. Зашто таква прекомерна компликација, ова мистерија је веома велика.

Дакле, msg_id потребно за...

РПЦ: захтеви, одговори, грешке. Потврде.

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

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

Овде је назначено на коју поруку је ово одговор. Због тога, на највишем нивоу АПИ-ја, мораћете да запамтите који је био број вашег захтева - мислим да нема потребе да објашњавате да је посао асинхрони, и да може бити неколико захтева у току истовремено, одговори на које се могу вратити било којим редоследом? У принципу, из овога и порука о грешкама као што су без радника, може се пратити архитектура која стоји иза овога: сервер који одржава ТЦП везу са вама је фронт-енд балансер, прослеђује захтеве позадинским странама и прикупља их назад преко message_id. Чини се да је овде све јасно, логично и добро.

Да?.. А ако размислите о томе? Уосталом, и сам РПЦ одговор такође има поље msg_id! Да ли треба да вичемо на сервер „не одговараш на мој одговор!“? И да, шта је било са потврдама? О страници поруке о порукама говори нам шта је

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

и то мора да уради свака страна. Али не увек! Ако сте добили РпцРесулт, он сам по себи служи као потврда. Односно, сервер може да одговори на ваш захтев са МсгсАцк - као, „Примио сам га“. РпцРесулт може одмах да одговори. Могло би бити обоје.

И да, још увек морате да одговорите на одговор! Потврда. У супротном, сервер ће га сматрати недоступним и поново вам га послати. Чак и након поновног повезивања. Али овде се, наравно, поставља питање тајм-аута. Погледајмо их мало касније.

У међувремену, погледајмо могуће грешке у извршавању упита.

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

Ма, неко ће ускликнути, ево хуманијег формата – има ред! Не журите. Ево списак грешака, али наравно не комплетан. Из њега сазнајемо да је код нешто слично ХТТП грешке (па, наравно, семантика одговора се не поштује, на неким местима су распоређени насумично међу кодовима), а линија изгледа као ВЕЛИКА_СЛОВА_АНД_НУМБЕРС. На пример, ПХОНЕ_НУМБЕР_ОЦЦУПИЕД или ФИЛЕ_ПАРТ_Х_МИССИНГ. Па, то јест, и даље ће вам требати ова линија анализирати. На пример, FLOOD_WAIT_3600 значи да морате чекати сат времена, и PHONE_MIGRATE_5, да телефонски број са овим префиксом мора бити регистрован у 5. ДЦ. Имамо језик типа, зар не? Не треба нам аргумент из стринга, обични ће то учинити, у реду.

Опет, ово није на страници сервисних порука, али, као што је већ уобичајено код овог пројекта, информације се могу пронаћи на другој страници документације... Или бацати сумњу. Прво, погледајте, куцање/кршење слоја - RpcError може бити угнежђен у RpcResult. Зашто не напољу? Шта нисмо водили рачуна?.. Сходно томе, где је гаранција да RpcError НЕ могу бити уграђени у RpcResult, али да буде директно или угнежђено у другом типу?.. А ако не може, зашто није на највишем нивоу, тј. недостаје req_msg_id ? ..

Али хајде да наставимо са сервисним порукама. Клијент може помислити да сервер дуго размишља и упутити овај диван захтев:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Постоје три могућа одговора на ово питање, који се опет укрштају са механизмом потврде; покушај да се разуме шта би требало да буду (и шта је општа листа типова који не захтевају потврду) оставља се читаоцу као домаћи задатак (напомена: информације у изворни код Телеграм Десктоп није потпун).

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

Генерално, многа места у ТЛ, МТПрото и Телеграму уопште остављају осећај тврдоглавости, али из учтивости, такта и др. мекане вјештине Учтиво смо о томе ћутали, а безобразлуке у дијалозима цензурисали. Међутим, ово местоОвећи део странице је о поруке о порукама То је шокантно чак и за мене, који већ дуго радим са мрежним протоколима и виђам бицикле различитог степена искривљености.

Почиње безазлено, са потврдама. Затим нам говоре о

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;

Па, свако ко почне да ради са МТПрото-ом мораће да се носи са њима; у циклусу „исправљено – поново компајлирано – покренуто“, добијање грешака у бројевима или соли која је успела да се поквари током уређивања је уобичајена ствар. Међутим, овде постоје две тачке:

  1. То значи да је оригинална порука изгубљена. Морамо да направимо неке редове, то ћемо погледати касније.
  2. Који су то чудни бројеви грешака? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... где су остали бројеви, Томи?

У документацији се наводи:

Намера је да вредности еррор_цоде буду груписане (еррор_цоде >> 4): на пример, кодови 0к40 — 0к4ф одговарају грешкама у декомпозицији контејнера.

али, прво, померање у другом правцу, и друго, није важно, где су остали кодови? У ауторовој глави?.. Ипак, то су ситнице.

Зависност почиње у порукама о статусима порука и копијама порука:

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

    • 1 = ништа се не зна о поруци (мсг_ид је пренизак, друга страна га је можда заборавила)
    • 2 = порука није примљена (мсг_ид спада у опсег сачуваних идентификатора; међутим, друга страна сигурно није примила такву поруку)
    • 3 = порука није примљена (мсг_ид превисок; међутим, друга страна је сигурно још није примила)
    • 4 = порука је примљена (имајте на уму да је овај одговор уједно и потврда о пријему)
    • +8 = порука је већ потврђена
    • +16 = порука не захтева потврду
    • +32 = РПЦ упит садржан у поруци се обрађује или је обрада већ завршена
    • +64 = одговор у вези са садржајем на поруку која је већ генерисана
    • +128 = друга страна поуздано зна да је порука већ примљена
      Овај одговор не захтева потврду. То је само по себи потврда релевантног мсгс_стате_рек.
      Имајте на уму да ако се изненада испостави да друга страна нема поруку која изгледа као да јој је послата, порука се једноставно може поново послати. Чак и ако друга страна прими две копије поруке у исто време, дупликат ће бити игнорисан. (Ако је прошло превише времена, а оригинални мсг_ид више није важећи, порука треба бити умотана у мсг_цопи).
  • Добровољно саопштавање статуса порука
    Свака страна може добровољно да обавести другу страну о статусу порука које је друга страна послала.
    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_copy#e06046b2 orig_message:Message = MessageCopy;
    Једном примљена, порука се обрађује као да омот није ту. Међутим, ако се поуздано зна да је порука ориг_мессаге.мсг_ид примљена, онда се нова порука не обрађује (док се истовремено она и ориг_мессаге.мсг_ид признају). Вредност ориг_мессаге.мсг_ид мора бити нижа од мсг_ид контејнера.

Чак да ћутимо шта msgs_state_info опет вире уши недовршеног ТЛ-а (требао нам је вектор бајтова, а у нижа два бита је био енум, а у виша два бита заставице). Поента је другачија. Да ли неко разуме зашто је све ово у пракси? у стварном клијенту потребно?.. Тешко, али може се замислити нека корист ако се особа бави отклањањем грешака, а у интерактивном режиму - питајте сервер шта и како. Али овде су захтеви описани повратно путовање.

Из тога произилази да свака страна мора не само да шифрује и шаље поруке, већ и да чува податке о себи, о одговорима на њих, непознато време. Документација не описује ни термине ни практичну применљивост ових функција. ни на који начин. Оно што је најневероватније је да се они заправо користе у коду званичних клијената! Очигледно им је речено нешто што није уврштено у јавну документацију. Разумети из кода зашто, више није тако једноставно као у случају ТЛ-а – није (релативно) логички изолован део, већ комад везан за архитектуру апликације, тј. биће потребно знатно више времена за разумевање кода апликације.

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

Из свега, ако се сетимо нагађања о архитектури сервера (дистрибуцији захтева по бацкендовима), следи прилично тужна ствар – упркос свим гаранцијама испоруке у ТЦП-у (или су подаци испоручени, или ћете бити обавештени о празнини, али подаци ће бити испоручени пре него што дође до проблема), да потврде у самом МТПрото-у - нема гаранција. Сервер може лако изгубити или избацити вашу поруку и ништа се не може учинити поводом тога, само користите различите врсте штака.

И пре свега - редови порука. Па, са једном ствари је све било очигледно од самог почетка – непотврђена порука мора да се сачува и да се замери. А после ког времена? И лудак га познаје. Можда те зависне сервисне поруке некако решавају овај проблем са штакама, рецимо, у Телеграм Десктоп-у постоје око 4 реда који им одговарају (можда више, као што је већ поменуто, за ово морате озбиљније да се удубите у његов код и архитектуру; истовремено време, знамо да се не може узети као узорак, у њему се не користи одређени број типова из МТПрото шеме).

Зашто се ово дешава? Вероватно, серверски програмери нису били у стању да обезбеде поузданост унутар кластера, па чак ни баферовање на предњем балансеру, и пренели су овај проблем на клијента. Из очаја, Василиј је покушао да имплементира алтернативну опцију, са само два реда, користећи алгоритме из ТЦП-а - мерење РТТ-а до сервера и подешавање величине „прозора“ (у порукама) у зависности од броја непотврђених захтева. То јест, тако груба хеуристика за процену оптерећења сервера је колико наших захтева може да жваће у исто време и да не изгуби.

Па, то је, разумете, зар не? Ако морате поново да имплементирате ТЦП на протокол који ради преко ТЦП-а, то указује на веома лоше дизајниран протокол.

О да, зашто вам треба више од једног реда, и шта то уопште значи за особу која ради са АПИ-јем високог нивоа? Видите, направите захтев, серијализујте га, али често не можете одмах да га пошаљете. Зашто? Јер ће одговор бити msg_id, што је привременоаЈа сам етикета, чије је додељивање најбоље одложити до што је могуће касније - у случају да је сервер одбије због неслагања времена између нас и њега (наравно, можемо направити штаку која помера наше време из садашњости серверу додавањем делте израчунате из одговора сервера – званични клијенти то раде, али је грубо и нетачно због баферовања). Стога, када упутите захтев са позивом локалне функције из библиотеке, порука пролази кроз следеће фазе:

  1. Лежи у једном реду и чека шифровање.
  2. Именован msg_id а порука је отишла у други ред - могуће прослеђивање; послати у утичницу.
  3. а) Сервер је одговорио на МсгсАцк - порука је испоручена, бришемо је из „другог реда“.
    б) Или обрнуто, нешто му се није допало, одговорио је на бадмсг - поново пошаљи из „другог реда“
    ц) Ништа се не зна, поруку треба поново послати из другог реда - али се не зна тачно када.
  4. Сервер је коначно одговорио RpcResult - стварни одговор (или грешка) - не само испоручен, већ и обрађен.

Можда, употреба контејнера могла би делимично да реши проблем. Ово је када се гомила порука спакује у једну, а сервер је одговорио потврдом на све одједном, у једном msg_id. Али он ће такође одбацити овај чопор, ако нешто пође по злу, у целости.

И у овом тренутку долазе у обзир нетехничка разматрања. Из искуства смо видели многе штаке, а поред тога, сада ћемо видети још примера лоших савета и архитектуре – да ли у таквим условима вреди веровати и доносити такве одлуке? Питање је реторичко (наравно да није).

Шта говоримо? Ако на тему „поруке о дрогама о порукама“ још увек можете да спекулишете са примедбама попут „ти си глуп, ниси разумео наш сјајан план!“ (па прво напишите документацију, како нормални људи треба, са образложењем и примерима размене пакета, па ћемо онда да причамо), онда су тајминги/тајм-аути чисто практично и специфично питање, овде се све одавно зна. Шта нам документација говори о тајм-аутима?

Сервер обично потврђује пријем поруке од клијента (обично, РПЦ упит) користећи РПЦ одговор. Ако се одговор чека дуго, сервер може прво послати потврду о пријему, а нешто касније и сам РПЦ одговор.

Клијент обично потврђује пријем поруке од сервера (обично РПЦ одговор) додавањем потврде следећем РПЦ упиту ако није прекасно послат (ако се генерише, рецимо, 60-120 секунди након пријема поруке са сервера). Међутим, ако у дужем временском периоду нема разлога за слање порука серверу или ако постоји велики број непризнатих порука са сервера (рецимо преко 16), клијент шаље самосталну потврду.

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

И о пинговима:

Пинг поруке (ПИНГ/ПОНГ)

ping#7abe77ec ping_id:long = Pong;

Одговор се обично враћа на исту везу:

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

Ове поруке не захтевају потврде. Понг се преноси само као одговор на пинг, док пинг може бити покренут са било које стране.

Одложено затварање везе + ПИНГ

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

Ради као пинг. Поред тога, након што је ово примљено, сервер покреће тајмер који ће затворити тренутну везу дисцоннецт_делаи секунди касније осим ако не прими нову поруку истог типа која аутоматски ресетује све претходне тајмере. На пример, ако клијент шаље ове пингове сваких 60 секунди, може да постави дисцоннецт_делаи на 75 секунди.

Јеси ли луд?! За 60 секунди, воз ће ући у станицу, спустити и покупити путнике и поново изгубити контакт у тунелу. За 120 секунди, док га чујете, стићи ће до другог и веза ће се највероватније прекинути. Па, јасно је одакле ноге долазе – „Чуо сам звоно, али не знам где је“, ту је Наглов алгоритам и опција ТЦП_НОДЕЛАИ, намењена интерактивном раду. Али, извините, држите се његове подразумеване вредности - 200 Миллисекунди Ако заиста желите да прикажете нешто слично и уштедите на неколико могућих пакета, онда то одложите на 5 секунди, или шта год да је временско ограничење поруке „Корисник куца...“ сада. Али не више.

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

Прво, мали едукативни програм. ТЦП веза, у одсуству размене пакета, може да живи недељама. Ово је и добро и лоше, у зависности од сврхе. Добро је да сте имали отворену ССХ везу са сервером, устали сте са рачунара, рестартовали рутер, вратили се на своје место - сесија преко овог сервера није поцепана (нисте ништа укуцали, није било пакета) , згодно је. Лоше је ако на серверу има хиљаде клијената, од којих сваки заузима ресурсе (здраво, Постгрес!), а клијентов хост се можда одавно поново покренуо - али нећемо знати за то.

Цхат/ИМ системи спадају у други случај из једног додатног разлога - статуса на мрежи. Ако је корисник „отпао“, о томе треба да обавестите његове саговорнике. У супротном, завршићете са грешком коју су креатори Јаббер-а направили (и исправљали је 20 година) – корисник је прекинуо везу, али настављају да му пишу поруке, верујући да је онлајн (које су се такође потпуно изгубиле у овим неколико минута пре него што је откривен прекид). Не, опција ТЦП_КЕЕПАЛИВЕ, коју многи људи који не разумеју како ТЦП тајмери ​​функционишу насумично убацују (постављањем дивљих вредности као што су десетине секунди), овде неће помоћи - морате да се уверите да не само језгро ОС-а корисникова машина је жива, али и нормално функционише, не може да одговори, а и сама апликација (мислите ли да не може да се замрзне? Телеграм Десктоп на Убунту 18.04 ми се замрзавао више пута).

Зато морате да пингујете сервер клијент, а не обрнуто – ако клијент то уради, ако је веза прекинута, пинг неће бити испоручен, циљ неће бити постигнут.

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

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

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

Зашто се испоставило да је тако компликовано и како архитекте Телеграма могу да покушају да приговоре? Чињеница да су покушали да направе сесију која преживи прекиде ТЦП везе, односно оно што није испоручено сада, испоручићемо касније. Вероватно су покушали и да направе УДП транспорт, али су наишли на потешкоће и одустали од тога (зато је документација празна - није се имало чиме хвалити). Али због недостатка разумевања како мреже уопште и ТЦП посебно функционишу, где се можете ослонити на то и где то треба да урадите сами (и како), и покушај да се ово комбинује са криптографијом „две птице са један камен”, ово је резултат.

Како је то било потребно? На основу чињенице да msg_id је временска ознака неопходна са криптографске тачке гледишта да би се спречили напади понављања, грешка је придодати јој јединствену функцију идентификатора. Стога, без суштинске промене тренутне архитектуре (када се генерише ток ажурирања, то је тема високог нивоа АПИ-ја за други део ове серије постова), требало би да:

  1. Сервер који држи ТЦП везу са клијентом преузима одговорност - ако је прочитао са сокета, молимо вас да потврдите, обрадите или вратите грешку, без губитка. Тада потврда није вектор ИД-ова, већ једноставно „последњи примљени сек_но“ - само број, као у ТЦП-у (два броја - ваш сек и потврђени). Увек смо у оквиру сесије, зар не?
  2. Временска ознака за спречавање напада понављања постаје посебно поље, а ла нонце. Проверено је, али не утиче ни на шта друго. Доста и uint32 - ако се наша сол мења барем на сваких пола дана, можемо доделити 16 битова нижим битовима целог дела тренутног времена, остатак - делимичном делу секунде (као сада).
  3. Уклоњено msg_id уопште - са становишта разликовања захтева на позадину, постоји, прво, ид клијента, а друго, ид сесије, који их повезује. Сходно томе, само једна ствар је довољна као идентификатор захтева seq_no.

Ово такође није најуспешнија опција; као идентификатор може послужити потпуни случајни одабир - то се већ ради у АПИ-ју високог нивоа приликом слања поруке, иначе. Било би боље да се архитектура потпуно преправи из релативне у апсолутну, али ово је тема за други део, а не овај пост.

АПИ?

Та-даам! Дакле, прошавши пут пун бола и штака, коначно смо били у могућности да пошаљемо било какве захтеве серверу и добијемо било какве одговоре на њих, као и да примамо ажурирања са сервера (не као одговор на захтев, већ сам шаље нам, као ПУСХ, ако неко тако је јасније).

Пажња, сада ће у чланку бити једини пример у Перлу! (за оне који нису упознати са синтаксом, први аргумент блесс је структура података објекта, други је његова класа):

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

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

Ох, чекај~~... како ово изгледа? Нешто врло познато... можда је ово структура података типичног веб АПИ-ја у ЈСОН-у, осим што су класе такође повезане са објектима?..

Па овако испада... О чему се ради, другови?.. Толико труда - и стали смо да се одморимо тамо где су веб програмери тек почиње?..Зар само ЈСОН преко ХТТПС-а не би био једноставнији?! Шта смо добили у замену? Да ли је труд вредан тога?

Хајде да проценимо шта нам је ТЛ+МТПрото дао и које су алтернативе могуће. Па, ХТТП, који се фокусира на модел захтев-одговор, се лоше уклапа, али бар нешто изнад ТЛС-а?

Компактна серијализација. Видевши ову структуру података, сличну ЈСОН-у, сећам се да постоје њене бинарне верзије. Означимо МсгПацк као недовољно проширив, али постоји, на пример, ЦБОР - иначе, стандард описан у РФЦ КСНУМКС. Значајно је по томе што дефинише ознаке, као механизам експанзије, и међу већ стандардизован постоје:

  • 25 + 256 - замена поновљених линија референцом на број реда, тако јефтина метода компресије
  • 26 - серијализовани Перл објекат са именом класе и аргументима конструктора
  • 27 - серијализовани језик независан објекат са именом типа и аргументима конструктора

Па, покушао сам да серијализујем исте податке у ТЛ-у и у ЦБОР-у са укљученим стринговима и паковањем објеката. Резултат је почео да варира у корист ЦБОР-а негде од мегабајта:

cborlen=1039673 tl_len=1095092

Дакле, закључак: Постоје знатно једноставнији формати који не подлежу проблему неуспеха синхронизације или непознатог идентификатора, са упоредивом ефикасношћу.

Брзо успостављање везе. То значи нула РТТ након поновног повезивања (када је кључ већ једном генерисан) - примењиво од прве МТПрото поруке, али уз неке резерве - погодите исту сол, сесија није покварена, итд. Шта нам уместо тога нуди ТЛС? Цитат на тему:

Када користите ПФС у ТЛС-у, ТЛС сесије (РФЦ КСНУМКС) да бисте наставили шифровану сесију без поновног преговарања о кључевима и без чувања кључних информација на серверу. Приликом отварања прве везе и креирања кључева, сервер шифрује стање везе и преноси га клијенту (у облику тикета за сесију). Сходно томе, када се веза настави, клијент шаље тикет за сесију, укључујући кључ сесије, назад на сервер. Сама тикет је шифрована привременим кључем (сессион тицкет кеи), који се чува на серверу и мора бити дистрибуиран међу свим фронтенд серверима који обрађују ССЛ у кластеризованим решењима.[10] Стога, увођење тикета за сесију може да наруши ПФС ако су привремени серверски кључеви угрожени, на пример, када се чувају дуже време (ОпенССЛ, нгинк, Апацхе их подразумевано чувају током целог трајања програма; популарни сајтови користе кључ неколико сати, до дана).

Овде РТТ није нула, потребно је да размените барем ЦлиентХелло и СерверХелло, након чега клијент може да пошаље податке заједно са Финисхед. Али овде треба да се сетимо да немамо Веб, са гомилом новоотворених веза, већ месинџер, чија је веза често једна и мање-више дуготрајни, релативно кратки захтеви ка Веб страницама – све је мултиплексирано интерно. Односно, сасвим је прихватљиво да нисмо наишли на стварно лош део метроа.

Заборавили сте још нешто? Напишите у коментарима.

Наставиће се!

У другом делу ове серије постова разматраћемо не техничка, већ организациона питања – приступе, идеологију, интерфејс, однос према корисницима итд. Међутим, на основу техничких информација које су овде представљене.

Трећи део ће наставити да анализира техничку компоненту / развојно искуство. Научићете, посебно:

  • наставак пандемонијума са разноликошћу типова ТЛ
  • непознате ствари о каналима и супергрупама
  • зашто су дијалози гори од списка
  • о апсолутном наспрам релативног адресирања порука
  • која је разлика између фотографије и слике
  • како емоји ометају курзив текст

и друге штаке! Будите у току!

Извор: ввв.хабр.цом

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