Kritika e protokollit dhe qasjeve organizative të Telegramit. Pjesa 1, teknike: përvoja e të shkruarit të një klienti nga e para - TL, MT

Kohët e fundit, postimet se sa i mirë është Telegrami, sa të shkëlqyer dhe me përvojë janë vëllezërit Durov në ndërtimin e sistemeve të rrjeteve, etj., kanë filluar të shfaqen më shpesh në Habré. Në të njëjtën kohë, shumë pak njerëz janë zhytur me të vërtetë në pajisjen teknike - më së shumti, ata përdorin një API Bot të bazuar në JSON mjaft të thjeshtë (dhe mjaft të ndryshëm nga MTProto) dhe zakonisht thjesht pranojnë mbi besimin të gjitha lavdërimet dhe PR që rrotullohen rreth mesazherit. Pothuajse një vit e gjysmë më parë, kolegu im në OJQ-në Eshelon Vasily (për fat të keq, llogaria e tij në Habré u fshi së bashku me draftin) filloi të shkruante klientin e tij Telegram nga e para në Perl, dhe më vonë autori i këtyre rreshtave u bashkua. Pse Perl, disa do të pyesin menjëherë? Sepse projekte të tilla tashmë ekzistojnë në gjuhë të tjera.Në fakt, nuk është kjo gjëja, mund të ketë ndonjë gjuhë tjetër ku nuk ka bibliotekë e gatshme, dhe në përputhje me rrethanat autori duhet të shkojë deri në fund nga e para. Për më tepër, kriptografia është një çështje besimi, por verifikojeni. Me një produkt që synon sigurinë, nuk mund të mbështeteni thjesht në një bibliotekë të gatshme nga prodhuesi dhe t'i besoni verbërisht asaj (megjithatë, kjo është një temë për pjesën e dytë). Për momentin, biblioteka funksionon mjaft mirë në nivelin "mesatar" (ju lejon të bëni çdo kërkesë API).

Sidoqoftë, nuk do të ketë shumë kriptografi ose matematikë në këtë seri postimesh. Por do të ketë shumë detaje të tjera teknike dhe paterica arkitekturore (gjithashtu të dobishme për ata që nuk do të shkruajnë nga e para, por do të përdorin bibliotekën në çdo gjuhë). Pra, qëllimi kryesor ishte të përpiqeshim të zbatonim klientin nga e para sipas dokumentacionit zyrtar. Kjo do të thotë, le të supozojmë se kodi burimor i klientëve zyrtarë është i mbyllur (përsëri, në pjesën e dytë do të trajtojmë më në detaje temën e faktit që kjo është e vërtetë Ndodh kështu), por, si në kohët e vjetra, për shembull, ekziston një standard si RFC - a është e mundur të shkruani një klient vetëm sipas specifikimeve, "pa shikuar" në kodin burimor, qoftë zyrtar (Telegram Desktop, celular), apo Telethon jozyrtar?

Përmbajtja:

Dokumentacioni... ekziston, apo jo? A është e vërtetë?..

Fragmentet e shënimeve për këtë artikull filluan të mblidhen verën e kaluar. Gjatë gjithë kësaj kohe në faqen zyrtare https://core.telegram.org Dokumentacioni ishte i Shtresës 23, d.m.th. mbërthyer diku në vitin 2014 (kujtoni, nuk kishte as kanale në atë kohë?). Sigurisht, teorikisht, kjo duhet të na kishte lejuar të zbatonim një klient me funksionalitet në atë kohë në 2014. Por edhe në këtë gjendje dokumentacioni, së pari, ishte i paplotë dhe së dyti, në disa vende binte në kundërshtim me vetveten. Pak më shumë se një muaj më parë, në shtator 2019, ishte rastësisht U zbulua se kishte një përditësim të madh të dokumentacionit në faqe, për Layer 105 plotësisht të fundit, me një shënim që tani gjithçka duhet të lexohet përsëri. Në të vërtetë, shumë nene u rishikuan, por shumë mbetën të pandryshuar. Prandaj, kur lexoni kritikat më poshtë për dokumentacionin, duhet të keni parasysh se disa nga këto gjëra nuk janë më relevante, por disa janë ende mjaft. Në fund të fundit, 5 vjet në botën moderne nuk është vetëm një kohë e gjatë, por очень shumë. Që nga ato kohëra (veçanërisht nëse nuk merrni parasysh faqet gjeochat të hedhura dhe të ringjallura që atëherë), numri i metodave API në skemë është rritur nga njëqind në më shumë se dyqind e pesëdhjetë!

Ku të filloni si një autor i ri?

Nuk ka rëndësi nëse shkruani nga e para apo përdorni, për shembull, biblioteka të gatshme si Telethon për Python ose Madeline për PHP, në çdo rast, do t'ju duhet së pari regjistroni aplikimin tuaj - merrni parametrat api_id и api_hash (ata që kanë punuar me VKontakte API e kuptojnë menjëherë) me të cilin serveri do të identifikojë aplikacionin. Kjo duhet të bëjeni për arsye ligjore, por ne do të flasim më shumë se pse autorët e bibliotekës nuk mund ta publikojnë atë në pjesën e dytë. Ju mund të jeni të kënaqur me vlerat e testit, megjithëse ato janë shumë të kufizuara - fakti është se tani mund të regjistroheni vetem nje aplikacioni, kështu që mos nxitoni në të.

Tani nga pikëpamja teknike duhet të na interesojë fakti që pas regjistrimit të marrim njoftime nga Telegram për përditësime të dokumentacionit, protokollit etj. Kjo do të thotë, mund të supozohet se siti me doke thjesht u braktis dhe vazhdoi të punojë posaçërisht me ata që filluan të bënin klientë, sepse është më e lehtë. Por jo, asgjë e tillë nuk është vërejtur, nuk ka ardhur asnjë informacion.

Dhe nëse shkruani nga e para, atëherë përdorimi i parametrave të marrë është në të vërtetë ende shumë larg. Edhe pse https://core.telegram.org/ dhe flet për to në Fillimi para së gjithash, në fakt, së pari do t'ju duhet të zbatoni Protokolli MTProto - por nëse besonit faqosja sipas modelit OSI në fund të faqes për një përshkrim të përgjithshëm të protokollit, atëherë është plotësisht e kotë.

Në fakt, si para dhe pas MTProto, në disa nivele njëherësh (siç thonë rrjetuesit e huaj që punojnë në kernelin OS, shkelje e shtresës), një temë e madhe, e dhimbshme dhe e tmerrshme do të pengojë ...

Serializimi binar: TL (Type Language) dhe skema e tij, dhe shtresat, dhe shumë fjalë të tjera të frikshme

Kjo temë, në fakt, është çelësi i problemeve të Telegram. Dhe do të ketë shumë fjalë të tmerrshme nëse përpiqeni të thelloheni në të.

Итак, схема. Если на это слово Вам вспомнилась, скажем, Skema JSON, menduat drejt. Qëllimi është i njëjtë: një gjuhë për të përshkruar një grup të mundshëm të dhënash të transmetuara. Këtu mbarojnë ngjashmëritë. Nëse nga faqja Protokolli MTProto, ose nga pema burimore e klientit zyrtar, do të përpiqemi të hapim një skemë, do të shohim diçka të tillë:

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;

Një person që e sheh këtë për herë të parë do të jetë në gjendje të njohë në mënyrë intuitive vetëm një pjesë të asaj që është shkruar - mirë, këto janë struktura në dukje (edhe pse ku është emri, në të majtë apo në të djathtë?), ka fusha në to, pas së cilës pas një dy pika pason një tip... ndoshta. Këtu në kllapa këndore ka ndoshta shabllone si në C++ (në fakt, jo krejt). Dhe çfarë kuptimi kanë të gjitha simbolet e tjera, pikëpyetjet, pikëçuditjet, përqindjet, shenjat hash (dhe padyshim që nënkuptojnë gjëra të ndryshme në vende të ndryshme), ndonjëherë të pranishëm dhe ndonjëherë jo, numra heksadecimal - dhe më e rëndësishmja, si të arrihet nga kjo i duhuri (që nuk do të refuzohet nga serveri) rrjedhë bajte? Do të duhet të lexoni dokumentacionin (po, ka lidhje me skemën në versionin JSON afër - por kjo nuk e bën më të qartë).

Hapni faqen Serializimi i të dhënave binare dhe zhyteni në botën magjike të kërpudhave dhe matematikës diskrete, diçka e ngjashme me matanin në vitin e 4-të. Alfabeti, lloji, vlera, kombinatori, kombinatori funksional, forma normale, tipi i përbërë, tipi polimorfik... dhe kjo është vetëm faqja e parë! Tjetra ju pret Gjuha TL, i cili, megjithëse përmban tashmë një shembull të një kërkese dhe përgjigjeje të parëndësishme, nuk jep fare përgjigje për rastet më tipike, që do të thotë se do t'ju duhet të kaloni nëpër një ritregim të matematikës të përkthyer nga rusishtja në anglisht në tetë të tjera të ngulitura. faqe!

Lexuesit e njohur me gjuhët funksionale dhe konkluzionet automatike të tipit, natyrisht, do ta shohin gjuhën e përshkrimit në këtë gjuhë, madje edhe nga shembulli, si shumë më të njohur, dhe mund të thonë se kjo në fakt nuk është e keqe në parim. Kundërshtimet për këtë janë:

  • Po, qëllimi tingëllon mirë, por mjerisht, ajo nuk arrihet
  • Arsimi në universitetet ruse ndryshon edhe midis specialiteteve të IT - jo të gjithë kanë ndjekur kursin përkatës
  • Më në fund, siç do të shohim, në praktikë është nuk kërkohet, pasi përdoret vetëm një nëngrup i kufizuar i TL-së që u përshkrua

Siç u tha LeoNerd në kanal #perl në rrjetin FreeNode IRC, i cili u përpoq të zbatonte një portë nga Telegram në Matrix (përkthimi i citatit është i pasaktë nga kujtesa):

Duket sikur dikush iu prezantua teorisë së tipit për herë të parë, u emocionua dhe filloi të përpiqej të luante me të, duke mos u kujdesur vërtet nëse ishte e nevojshme në praktikë.

Shihni vetë, nëse nevoja për tipe të zhveshura (int, të gjata, etj.) si diçka elementare nuk ngre pyetje - në fund të fundit ato duhet të zbatohen manualisht - për shembull, le të bëjmë një përpjekje për të nxjerrë prej tyre vektoriale. Kjo është, në fakt, grup, nëse i quani gjërat që rezultojnë me emrat e tyre të duhur.

Por më parë

Një përshkrim i shkurtër i një nëngrupi të sintaksës TL për ata që nuk e lexojnë dokumentacionin zyrtar

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;

Përkufizimi gjithmonë fillon projektues, pas së cilës opsionalisht (në praktikë - gjithmonë) përmes simbolit # duhet të CRC32 nga vargu i përshkrimit të normalizuar të këtij lloji. Më pas vjen një përshkrim i fushave; nëse ato ekzistojnë, lloji mund të jetë bosh. E gjithë kjo përfundon me një shenjë të barabartë, emri i llojit të cilit i përket ky konstruktor - që është, në fakt, nëntipi. Djali në të djathtë të shenjës së barazimit është polimorfike - domethënë, disa lloje specifike mund t'i korrespondojnë asaj.

Nëse përkufizimi ndodh pas rreshtit ---functions---, atëherë sintaksa do të mbetet e njëjtë, por kuptimi do të jetë i ndryshëm: konstruktori do të bëhet emri i funksionit RPC, fushat do të bëhen parametra (mirë, domethënë, do të mbetet saktësisht e njëjta strukturë e dhënë, siç përshkruhet më poshtë , ky do të jetë thjesht kuptimi i caktuar), dhe "lloji polimorfik " - lloji i rezultatit të kthyer. Vërtetë, ajo do të mbetet ende polimorfike - e përcaktuar vetëm në seksion ---types---, por ky konstruktor "nuk do të merret parasysh". Mbingarkimi i llojeve të funksioneve të thirrura nga argumentet e tyre, d.m.th. Për disa arsye, disa funksione me të njëjtin emër, por nënshkrime të ndryshme, si në C++, nuk parashikohen në TL.

Pse "konstruktor" dhe "polimorfik" nëse nuk është OOP? Epo, në fakt, do të jetë më e lehtë për dikë që të mendojë për këtë në terma OOP - një lloj polimorfik si një klasë abstrakte, dhe konstruktorët janë klasat e tij të drejtpërdrejta pasardhëse, dhe final në terminologjinë e një sërë gjuhësh. Në fakt, sigurisht, vetëm këtu ngjashmëri me metoda konstruktori të mbingarkuar reale në gjuhët e programimit OO. Meqenëse këtu janë vetëm struktura të dhënash, nuk ka metoda (megjithëse përshkrimi i funksioneve dhe metodave më tej është mjaft i aftë të krijojë konfuzion në kokë se ato ekzistojnë, por kjo është një çështje tjetër) - mund të mendoni për një konstruktor si një vlerë nga e cila është duke u ndërtuar shkruani kur lexoni një rrjedhë bajt.

Si ndodh kjo? Deserializuesi, i cili lexon gjithmonë 4 bajt, e sheh vlerën 0xcrc32 - dhe kupton se çfarë do të ndodhë më pas field1 me llojin int, d.m.th. lexon saktësisht 4 bajt, në këtë fushë mbivendosëse me llojin PolymorType lexoni. Sheh 0x2crc32 dhe kupton se ka dy fusha më tej, së pari long, që do të thotë se lexojmë 8 bajt. Dhe pastaj përsëri një lloj kompleks, i cili deserializohet në të njëjtën mënyrë. Për shembull, Type3 mund të deklarohen në qark sa më shpejt që dy konstruktorë, përkatësisht, atëherë ata duhet të takohen 0x12abcd34, pas së cilës duhet të lexoni edhe 4 bajt të tjerë intOse 0x6789cdef, pas së cilës nuk do të ketë asgjë. Çdo gjë tjetër - duhet të bëni një përjashtim. Gjithsesi, pas kësaj i kthehemi leximit të 4 bajteve int fushat field_c в constructorTwo dhe me këtë përfundojmë leximin tonë PolymorType.

Më në fund, nëse kapeni 0xdeadcrc për constructorThree, atëherë gjithçka bëhet më e ndërlikuar. Fusha jonë e parë është bit_flags_of_what_really_present me llojin # - në fakt, ky është vetëm një pseudonim për llojin nat, që do të thotë "numër natyror". Kjo është, në fakt, int i panënshkruar është, meqë ra fjala, i vetmi rast kur numrat e panënshkruar ndodhin në qarqe reale. Pra, më pas është një ndërtim me një pikëpyetje, që do të thotë se kjo fushë - do të jetë e pranishme në tela vetëm nëse biti përkatës është vendosur në fushën e përmendur (përafërsisht si një operator tresh). Pra, le të supozojmë se ky bit ishte vendosur, që do të thotë se më tej ne duhet të lexojmë një fushë si Type, e cila në shembullin tonë ka 2 konstruktorë. Njëra është bosh (përbëhet vetëm nga identifikuesi), tjetra ka një fushë ids me llojin ids:Vector<long>.

Ju mund të mendoni se të dy shabllonet dhe gjenerikët janë në avantazhet ose Java. Por jo. Pothuajse. Kjo единственный rasti i përdorimit të kllapave këndore në qarqe reale, dhe përdoret VETËM për Vektor. Në rrjedhën e bajtit, këto do të jenë 4 bajt CRC32 për vetë llojin Vector, gjithmonë të njëjtë, pastaj 4 bajt - numri i elementeve të grupit dhe më pas vetë këta elementë.

Shtojini kësaj faktin se serializimi ndodh gjithmonë me fjalë 4 bajt, të gjitha llojet janë shumëfish të tij - përshkruhen edhe llojet e integruara. bytes и string me serializimin manual të gjatësisë dhe këtë shtrirje me 4 - mirë, duket se tingëllon normale dhe madje relativisht efektive? Edhe pse TL pretendohet të jetë një serial binar efektiv, dreqin me ta, me zgjerimin e pothuajse çdo gjëje, madje edhe vlerat Boolean dhe vargjet me një karakter në 4 bajt, a do të jetë ende shumë më i trashë JSON? Shikoni, edhe fushat e panevojshme mund të kapërcehen me flamuj bit, gjithçka është mjaft e mirë, madje edhe e zgjerueshme për të ardhmen, kështu që pse të mos shtoni më vonë fusha të reja opsionale në konstruktor?..

Por jo, nëse nuk lexoni përshkrimin tim të shkurtër, por dokumentacionin e plotë dhe mendoni për zbatimin. Së pari, CRC32 i konstruktorit llogaritet sipas vijës së normalizuar të përshkrimit të tekstit të skemës (hiq hapësirën shtesë të bardhë, etj.) - kështu që nëse shtohet një fushë e re, linja e përshkrimit të llojit do të ndryshojë, dhe si rrjedhim CRC32 dhe , rrjedhimisht, serializimi. Dhe çfarë do të bënte klienti i vjetër nëse do të merrte një fushë me flamuj të rinj të vendosur dhe ai nuk e di se çfarë të bëjë me ta më pas?..

Së dyti, le të kujtojmë CRC32, e cila përdoret këtu në thelb si funksionet hash për të përcaktuar në mënyrë unike se çfarë lloji po (de)serializohet. Këtu përballemi me problemin e përplasjeve - dhe jo, probabiliteti nuk është një në 232, por shumë më i madh. Kush e kujtoi se CRC32 është krijuar për të zbuluar (dhe korrigjuar) gabimet në kanalin e komunikimit, dhe në përputhje me rrethanat i përmirëson këto veti në dëm të të tjerëve? Për shembull, nuk i intereson riorganizimi i bajteve: nëse llogaritni CRC32 nga dy rreshta, në të dytën ndërroni 4 bajtët e parë me 4 bajtët e ardhshëm - do të jetë njësoj. Kur hyrja jonë janë vargje teksti nga alfabeti latin (dhe pak shenja pikësimi), dhe këta emra nuk janë veçanërisht të rastësishëm, gjasat për një rirregullim të tillë rriten shumë.

Meqë ra fjala, kush kontrolloi se çfarë kishte? vërtet CRC32? Një nga kodet e hershme burimore (madje edhe përpara Waltman) kishte një funksion hash që shumëzonte çdo karakter me numrin 239, aq i dashur nga këta njerëz, ha ha!

Më në fund, në rregull, kuptuam se konstruktorët me një lloj fushe Vector<int> и Vector<PolymorType> do të ketë CRC32 të ndryshme. Po në lidhje me performancën në internet? Dhe nga pikëpamja teorike, a behet kjo pjese e tipit? Le të themi se kalojmë një grup prej dhjetë mijë numrash, mirë me Vector<int> gjithçka është e qartë, gjatësia dhe 40000 bajt të tjerë. Dhe nëse kjo Vector<Type2>, e cila përbëhet nga vetëm një fushë int dhe është vetëm në llojin - a duhet të përsërisim 10000xabcdef0 34 herë dhe pastaj 4 bajt int, ose gjuha është në gjendje ta PAVAROJ atë për ne nga konstruktori fixedVec dhe në vend të 80000 bajt, transferoni përsëri vetëm 40000?

Kjo nuk është aspak një pyetje teorike boshe - imagjinoni të merrni një listë të përdoruesve të grupit, secili prej të cilëve ka një ID, emrin, mbiemrin - ndryshimi në sasinë e të dhënave të transferuara përmes një lidhjeje celulare mund të jetë i rëndësishëm. Është pikërisht efektiviteti i serializimit të Telegramit që na reklamohet.

Kështu që…

Vektor, i cili nuk u lëshua kurrë

Nëse përpiqeni të kaloni nëpër faqet e përshkrimit të kombinatorëve e kështu me radhë, do të shihni se një vektor (dhe madje edhe një matricë) po përpiqet zyrtarisht të dalë përmes tupave prej disa fletësh. Por në fund ata harrojnë, hapi i fundit është anashkaluar dhe thjesht jepet një përkufizim i një vektori, i cili ende nuk është i lidhur me një lloj. Per Cfarë bëhet fjalë? Në gjuhë programimi, veçanërisht ato funksionale, është mjaft tipike të përshkruhet struktura në mënyrë rekursive - përpiluesi me vlerësimin e tij dembel do të kuptojë dhe do të bëjë gjithçka vetë. Në gjuhë serializimi i të dhënave ajo që nevojitet është EFIKESIA: mjafton thjesht të përshkruhet listë, d.m.th. struktura e dy elementeve - i pari është një element i të dhënave, i dyti është vetë e njëjta strukturë ose një hapësirë ​​boshe për bishtin (paketë (cons) në Lisp). Por kjo padyshim do të kërkojë i secilit elementi shpenzon 4 bajt shtesë (CRC32 në rastin në TL) për të përshkruar llojin e tij. Një grup gjithashtu mund të përshkruhet lehtësisht madhësi fikse, por në rastin e një vargu me gjatësi të panjohur paraprakisht, ne shkëputemi.

Prandaj, meqenëse TL nuk lejon nxjerrjen e një vektori, ai duhej të shtohej në anë. Në fund të fundit dokumentacioni thotë:

Serializimi përdor gjithmonë të njëjtin konstruktor "vektor" (const 0x1cb5c415 = crc32 ("vector t:Type # [ t ] = Vector t") që nuk varet nga vlera specifike e ndryshores së tipit t.

Vlera e parametrit opsional t nuk përfshihet në serializimin pasi rrjedh nga lloji i rezultatit (i njohur gjithmonë përpara deserializimit).

Hidhni një vështrim më të afërt: vector {t:Type} # [ t ] = Vector t - por askund Vetë ky përkufizim nuk thotë se numri i parë duhet të jetë i barabartë me gjatësinë e vektorit! Dhe nuk vjen nga askund. Kjo është një e dhënë që duhet mbajtur parasysh dhe zbatuar me duart tuaja. Diku tjetër, dokumentacioni përmend sinqerisht se lloji nuk është real:

Pseudotipi polimorfik i Vector t është një "lloj" vlera e të cilit është një sekuencë vlerash të çdo lloji t, qofshin me kuti ose të zhveshur.

... por nuk fokusohet në të. Kur ju, i lodhur duke ecur nëpër shtrirjen e matematikës (ndoshta edhe i njohur për ju nga një kurs universitar), vendosni të hiqni dorë dhe të shikoni në fakt se si të punoni me të në praktikë, përshtypja që ju ka mbetur në kokë është se kjo është serioze. Matematika në thelb, ajo u shpik qartë nga Cool People (dy matematikanë - fitues ACM), dhe jo kushdo. Qëllimi - për t'u dukur - është arritur.

Nga rruga, për numrin. Le t'ju kujtojmë se # është sinonim nat, numri natyror:

Ka shprehje të tipit (tip-expr) dhe shprehjet numerike (nat-expr). Sidoqoftë, ato përcaktohen në të njëjtën mënyrë.

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

por në gramatikë përshkruhen në të njëjtën mënyrë, d.m.th. Ky ndryshim duhet të mbahet mend përsëri dhe të vihet në zbatim me dorë.

Epo, po, llojet e shablloneve (vector<int>, vector<User>) kanë një identifikues të përbashkët (#1cb5c415), d.m.th. nëse e dini se thirrja shpallet si

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

atëherë nuk jeni më duke pritur vetëm për një vektor, por një vektor përdoruesish. Më saktë, do të prisni - në kodin real, çdo element, nëse jo një tip i zhveshur, do të ketë një konstruktor, dhe në një mënyrë të mirë në zbatim do të ishte e nevojshme të kontrollohej - por ne u dërguam saktësisht në çdo element të këtij vektori atij lloji? Po sikur të ishte një lloj PHP, në të cilin një grup mund të përmbajë lloje të ndryshme në elementë të ndryshëm?

Në këtë pikë ju filloni të mendoni - a është e nevojshme një TL e tillë? Ndoshta për karrocën do të ishte e mundur të përdorej një serializues njerëzor, i njëjti protobuf që ekzistonte atëherë? Kjo ishte teoria, le të shohim praktikën.

Implementimet ekzistuese të TL në kod

TL lindi në thellësi të VKontakte edhe para ngjarjeve të famshme me shitjen e aksioneve të Durov dhe (Ndoshta), edhe para se të fillonte zhvillimi i Telegram. Dhe në burim të hapur kodi burim i zbatimit të parë ju mund të gjeni shumë paterica qesharake. Dhe vetë gjuha u zbatua atje më plotësisht sesa tani në Telegram. Për shembull, hash-et nuk përdoren fare në skemë (që do të thotë një pseudotip i integruar (si një vektor) me sjellje devijuese). Ose

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

por le të shqyrtojmë, për hir të plotësimit, të gjurmojmë, si të thuash, evolucionin e Gjigantit të Mendimit.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Ose kjo e bukura:

    static const char *reserved_words_polymorhic[] = {

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

      };

Ky fragment ka të bëjë me shabllone si:

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

Ky është përkufizimi i një lloji shabllon hashmap si një vektor i çifteve int - Type. Në C++ do të dukej diçka si kjo:

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

kështu që, alpha - fjalë kyçe! Por vetëm në C++ mund të shkruash T, por duhet të shkruash alfa, beta... Por jo më shumë se 8 parametra, këtu mbaron fantazia. Duket se një herë e një kohë në Shën Petersburg u zhvilluan disa dialogë të tillë:

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

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

Por bëhej fjalë për zbatimin e parë të publikuar të TL-së “në përgjithësi”. Le të kalojmë në shqyrtimin e zbatimeve në vetë klientët e Telegram.

Fjalë për Vasily:

Vasily, [09.10.18 17:07] Mbi të gjitha, gomari është i nxehtë sepse ata krijuan një tufë abstraksionesh, dhe më pas goditën një rrufe mbi to dhe mbuluan gjeneratorin e kodit me paterica
Si rezultat, së pari nga dock pilot.jpg
Pastaj nga kodi dzhekichan.webp

Sigurisht, nga njerëzit e njohur me algoritmet dhe matematikën, mund të presim që ata të kenë lexuar Aho, Ullmann dhe janë njohur me mjetet që janë bërë de facto standarde në industri gjatë dekadave për të shkruar përpiluesit e tyre DSL, apo jo?..

Nga autori telegram-cli është Vitaly Valtman, siç mund të kuptohet nga shfaqja e formatit TLO jashtë kufijve të tij (cli), një anëtar i ekipit - tani është ndarë një bibliotekë për analizimin e TL veçmas, cila është përshtypja për të analizues TL? ..

16.12 04:18 Vasily: Unë mendoj se dikush nuk e ka zotëruar lex+yacc
16.12 04:18 Vasily: Nuk mund ta shpjegoj ndryshe
16.12 04:18 Vasily: mirë, ose ata u paguan për numrin e linjave në VK
16.12 04:19 Vasily: 3k+ rreshta etj.<censored> në vend të një analizuesi

Ndoshta një përjashtim? Le të shohim se si bën Ky është klienti ZYRTARE - Telegram Desktop:

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

1100+ rreshta në Python, disa shprehje të rregullta + raste të veçanta si një vektor, i cili, natyrisht, është deklaruar në skemë siç duhet të jetë sipas sintaksës TL, por ata u mbështetën në këtë sintaksë për ta analizuar atë ... Lind pyetja, pse e gjithë kjo ishte një mrekulli?иËshtë më e shtresuar nëse askush nuk do ta analizojë gjithsesi sipas dokumentacionit?!

Meqë ra fjala... Mbani mend që folëm për kontrollin e CRC32? Pra, në gjeneratorin e kodit Telegram Desktop ekziston një listë e përjashtimeve për ato lloje në të cilat llogaritet CRC32 nuk perputhet me atë të treguar në diagram!

Vasily, [18.12/22 49:XNUMX] dhe këtu do të mendoja nëse nevojitet një TL e tillë
nëse do të doja të ngatërroja implementimet alternative, do të filloja të fusja ndërprerje rreshtash, gjysma e analizuesve do të prishen në përkufizimet me shumë rreshta
tdesktop, megjithatë, gjithashtu

Mbani mend pikën për një rresht, ne do t'i kthehemi pak më vonë.

Mirë, telegram-cli është jozyrtar, Telegram Desktop është zyrtar, por çfarë ndodh me të tjerët? Kush e di?.. Në kodin e klientit Android nuk kishte fare analizues skemash (që ngre pyetje në lidhje me burimin e hapur, por kjo është për pjesën e dytë), por kishte disa pjesë të tjera qesharake kodi, por më shumë për to në nënseksioni më poshtë.

Çfarë pyetjesh të tjera ngre në praktikë serializimi? Për shembull, ata bënë shumë gjëra, natyrisht, me fusha bit dhe fusha të kushtëzuara:

Vasily: flags.0? true
do të thotë që fusha është e pranishme dhe është e barabartë nëse flamuri është vendosur

Vasily: flags.1? int
do të thotë se fusha është e pranishme dhe duhet të deserializohet

Vasily: Gomar, mos u shqetëso për atë që po bën!
Vasily: Ka një përmendje diku në dokument që e vërtetë është një lloj i zhveshur me gjatësi zero, por është e pamundur të mblidhet ndonjë gjë nga dokumenti i tyre
Vasily: As në zbatimet me burim të hapur nuk është kështu, por ka një mori patericash dhe mbështetëse

Po Telethoni? Duke parë përpara temën e MTProto, një shembull - në dokumentacion ka pjesë të tilla, por shenja % përshkruhet vetëm si "që i përgjigjet një lloji të caktuar të zhveshur", d.m.th. në shembujt e mëposhtëm ka ose një gabim ose diçka të padokumentuar:

Vasily, [22.06.18 18:38] Në një vend:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Në një tjetër:

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

Dhe këto janë dy dallime të mëdha, në jetën reale vjen një lloj vektori lakuriq

Unë nuk kam parë një përkufizim të zhveshur të vektorit dhe nuk kam hasur në një të tillë

Analiza shkruhet me dorë në telethon

Në diagramin e tij komentohet përkufizimi msg_container

Përsëri, pyetja mbetet rreth %. Nuk përshkruhet.

Vadim Goncharov, [22.06.18 19:22] dhe në tdesktop?

Vasily, [22.06.18 19:23] Por analizuesi i tyre TL në motorët e rregullt me ​​shumë mundësi nuk do ta hajë as këtë

// parsed manually

TL është një abstraksion i bukur, askush nuk e zbaton plotësisht

Dhe % nuk ​​është në versionin e tyre të skemës

Por këtu dokumentacioni bie ndesh me vetveten, pra idk

U gjet në gramatikë, ata thjesht mund të kishin harruar të përshkruanin semantikën

E keni parë dokumentin në TL, nuk e kuptoni dot pa gjysmë litër

"Epo, le të themi," do të thotë një lexues tjetër, "ti kritikon diçka, prandaj më trego se si duhet bërë".

Vasily përgjigjet: "Sa për analizuesin, më pëlqejnë gjëra të tilla

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

disi më pëlqen më mirë se

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

ose

        # 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)

ky është I GJITHË lexeri:

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

ato. më e thjeshtë është e thënë butë.”

Në përgjithësi, si rezultat, analizuesi dhe gjeneratori i kodit për nëngrupin e përdorur aktualisht të TL përshtaten në afërsisht 100 rreshta gramatikore dhe ~ 300 rreshta të gjeneratorit (duke numëruar të gjitha printkodi i gjeneruar i 's), duke përfshirë tipet e informacionit për introspeksion në secilën klasë. Çdo lloj polimorfik kthehet në një klasë bazë abstrakte boshe, dhe konstruktorët trashëgojnë prej saj dhe kanë metoda për serializimin dhe deserializimin.

Mungesa e llojeve në gjuhën e tipit

Shkrimi i fortë është një gjë e mirë, apo jo? Jo, ky nuk është një holivar (edhe pse unë preferoj gjuhët dinamike), por një postulat brenda kornizës së TL. Në bazë të saj, gjuha duhet të na ofrojë lloj-lloj kontrollesh. Epo, në rregull, ndoshta jo ai vetë, por zbatimi, por ai duhet të paktën t'i përshkruajë ato. Dhe çfarë lloj mundësish duam?

Para së gjithash, kufizimet. Këtu shohim në dokumentacionin për ngarkimin e skedarëve:

Përmbajtja binare e skedarit më pas ndahet në pjesë. Të gjitha pjesët duhet të kenë të njëjtën madhësi ( madhësia e pjesës ) dhe duhet të plotësohen kushtet e mëposhtme:

  • part_size % 1024 = 0 (i ndashëm me 1 KB)
  • 524288 % part_size = 0 (512 KB duhet të ndahet në mënyrë të barabartë me madhësinë e pjesës)

Pjesa e fundit nuk duhet të plotësojë këto kushte, me kusht që madhësia e saj të jetë më e vogël se pjesa_size.

Çdo pjesë duhet të ketë një numër sekuence, file_pjesa, me një vlerë që varion nga 0 në 2,999.

Pasi skedari të jetë ndarë, duhet të zgjidhni një metodë për ruajtjen e tij në server. Përdorni upload.saveBigFilePart në rast se madhësia e plotë e skedarit është më shumë se 10 MB dhe ngarko.saveFilePart për skedarë më të vegjël.
[…] mund të kthehet një nga gabimet e mëposhtme të futjes së të dhënave:

  • FILE_PARTS_INVALID — Numri i pavlefshëm i pjesëve. Vlera nuk është ndërmjet 1..3000

A është ndonjë nga këto në diagram? A është kjo disi e shprehshme duke përdorur TL? Nr. Por më falni, edhe Turbo Pascal i gjyshit ishte në gjendje të përshkruante llojet e specifikuara vargjet. Dhe ai dinte një gjë tjetër, tani më i njohur si enum - një lloj që përbëhet nga një numërim i një numri fiks (të vogël) vlerash. Në gjuhë si C - numerike, vini re se deri më tani kemi folur vetëm për lloje numrat. Por ka edhe vargje, vargje... për shembull, do të ishte mirë të përshkruhet se ky varg mund të përmbajë vetëm një numër telefoni, apo jo?

Asnjë nga këto nuk është në TL. Por ka, për shembull, në skemën JSON. Dhe nëse dikush tjetër mund të argumentojë për pjesëtueshmërinë e 512 KB, se kjo ende duhet të kontrollohet në kod, atëherë sigurohuni që klienti thjesht Nuk munda dërgoni një numër jashtë rrezes 1..3000 (dhe gabimi përkatës nuk mund të kishte lindur) do të ishte e mundur, apo jo?..

Nga rruga, në lidhje me gabimet dhe vlerat e kthimit. Edhe ata që kanë punuar me TL turbullojnë sytë - nuk na e kuptoi menjëherë këtë secili një funksion në TL në fakt mund të kthejë jo vetëm llojin e kthimit të përshkruar, por edhe një gabim. Por kjo nuk mund të konkludohet në asnjë mënyrë duke përdorur vetë TL. Sigurisht, tashmë është e qartë dhe nuk ka nevojë për asgjë në praktikë (edhe pse në fakt, RPC mund të bëhet në mënyra të ndryshme, ne do t'i kthehemi kësaj më vonë) - por ç'të themi për Pastërtinë e koncepteve të matematikës së llojeve abstrakte nga bota qiellore?.. E mora tërheqjen - prandaj përputhet.

Dhe së fundi, po në lidhje me lexueshmërinë? Epo, atje, në përgjithësi, do të doja përshkrim e keni të drejtë në skemë (në skemën JSON, përsëri, është), por nëse tashmë jeni të sforcuar me të, atëherë ç'të themi për anën praktike - të paktën e parëndësishme për të parë ndryshimet gjatë përditësimeve? Shihni vetë në shembuj realë:

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

ose

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

Kjo varet nga të gjithë, por GitHub, për shembull, refuzon të nxjerrë në pah ndryshimet brenda linjave kaq të gjata. Loja "gjeni 10 dallime", dhe ajo që truri sheh menjëherë është se fillimet dhe mbarimet në të dy shembujt janë të njëjtë, duhet të lexoni me lodhje diku në mes... Për mendimin tim, kjo nuk është vetëm në teori, por thjesht vizualisht të pista dhe të lëmuara.

Nga rruga, për pastërtinë e teorisë. Pse na duhen fusha bit? A nuk duket se ata erë keq nga pikepamja e teorise se tipit? Shpjegimi mund të shihet në versionet e mëparshme të diagramit. Në fillim, po, kështu ishte, për çdo teshtitje krijohej një lloj i ri. Këto elemente ekzistojnë ende në këtë formë, për shembull:

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;

Por tani imagjinoni, nëse keni 5 fusha opsionale në strukturën tuaj, atëherë do t'ju nevojiten 32 lloje për të gjitha opsionet e mundshme. Shpërthim kombinues. Kështu, pastërtia kristalore e teorisë së TL u thye edhe një herë kundër gomarit prej gize të realitetit të ashpër të serializimit.

Për më tepër, në disa vende këta djem vetë shkelin tipologjinë e tyre. Për shembull, në MTProto (kapitulli tjetër) përgjigja mund të kompresohet nga Gzip, gjithçka është në rregull - përveç se shtresat dhe qarku janë shkelur. Edhe një herë, nuk ishte vetë RpcResult që u korr, por përmbajtja e tij. Epo, pse ta bëj këtë?.. Më duhej të prisja në një paterica në mënyrë që kompresimi të funksiononte kudo.

Ose një shembull tjetër, një herë zbuluam një gabim - ai u dërgua InputPeerUser në vend të InputUser. Ose anasjelltas. Por funksionoi! Kjo do të thotë, serverit nuk u interesua për llojin. Si mund të jetë kjo? Përgjigja mund të na jepet nga fragmente kodi nga telegram-cli:

  if (tgl_get_peer_type (E->id) != TGL_PEER_CHANNEL || (C && (C->flags & TGLCHF_MEGAGROUP))) {
    out_int (CODE_messages_get_history);
    out_peer_id (TLS, E->id);
  } else {    
    out_int (CODE_channels_get_important_history);

    out_int (CODE_input_channel);
    out_int (tgl_get_peer_id (E->id));
    out_long (E->id.access_hash);
  }
  out_int (E->max_id);
  out_int (E->offset);
  out_int (E->limit);
  out_int (0);
  out_int (0);

Me fjalë të tjera, këtu bëhet serializimi ME DORË, nuk është krijuar kod! Ndoshta serveri është implementuar në një mënyrë të ngjashme?.. Në parim, kjo do të funksionojë nëse bëhet një herë, por si mund të mbështetet më vonë gjatë përditësimeve? Kjo është arsyeja pse u shpik skema? Dhe këtu kalojmë në pyetjen tjetër.

Versionimi. Shtresat

Pse versionet skematike quhen shtresa mund të spekulohet vetëm në bazë të historisë së skemave të publikuara. Me sa duket, në fillim autorët menduan se gjërat themelore mund të bëheshin duke përdorur skemën e pandryshuar dhe vetëm aty ku është e nevojshme, për kërkesa specifike, tregojnë se ato po kryheshin duke përdorur një version tjetër. Në parim, edhe një ide e mirë - dhe e reja do të jetë, si të thuash, "e përzier", e shtresuar mbi të vjetrën. Por le të shohim se si u bë. Vërtetë, nuk isha në gjendje ta shikoja që në fillim - është qesharake, por diagrami i shtresës bazë thjesht nuk ekziston. Shtresat filluan me 2. Dokumentacioni na tregon për një veçori të veçantë TL:

Nëse një klient mbështet Layer 2, atëherë duhet të përdoret konstruktori i mëposhtëm:

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

Në praktikë, kjo do të thotë që përpara çdo thirrjeje API, një int me vlerën 0x289dd1f6 duhet të shtohet para numrit të metodës.

Tingëllon normale. Por çfarë ndodhi më pas? Më pas u shfaq

invokeWithLayer3#b7475268 query:!X = X;

Pra, çfarë është më pas? Siç mund ta merrni me mend,

invokeWithLayer4#dea0d430 query:!X = X;

Qesharake? Jo, është shumë herët për të qeshur, mendoni për këtë çdo një kërkesë nga një shtresë tjetër duhet të mbështillet në një lloj kaq të veçantë - nëse të gjitha janë të ndryshme për ju, si mund t'i dalloni ndryshe? Dhe shtimi i vetëm 4 bajt para është një metodë mjaft efikase. Kështu që,

invokeWithLayer5#417a57ae query:!X = X;

Por është e qartë se pas një kohe kjo do të bëhet një lloj bacchanalia. Dhe zgjidhja erdhi:

Përditësimi: Duke filluar me Layer 9, metoda ndihmëse invokeWithLayerN mund të përdoret vetëm së bashku me initConnection

Hora! Pas 9 versioneve, më në fund arritëm te ajo që bëhej në protokollet e Internetit në vitet '80 - duke rënë dakord për versionin një herë në fillim të lidhjes!

Pra, çfarë është më pas?..

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

Por tani ju ende mund të qeshni. Vetëm pas 9 shtresave të tjera, më në fund u shtua një konstruktor universal me një numër versioni, i cili duhet të thirret vetëm një herë në fillim të lidhjes, dhe kuptimi i shtresave dukej se ishte zhdukur, tani është thjesht një version i kushtëzuar, si p.sh. kudo tjetër. Problemi u zgjidh.

Pikërisht?..

Vasily, [16.07.18 14:01] Edhe të premten mendova:
Teleserver dërgon ngjarje pa kërkesë. Kërkesat duhet të mbështillen në InvokeWithLayer. Serveri nuk mbështjell përditësimet; nuk ka strukturë për mbështjelljen e përgjigjeve dhe përditësimeve.

Ato. klienti nuk mund të specifikojë shtresën në të cilën ai dëshiron përditësime

Vadim Goncharov, [16.07.18 14:02] a nuk është InvokeWithLayer një patericë në parim?

Vasily, [16.07.18 14:02] Kjo është mënyra e vetme

Vadim Goncharov, [16.07.18 14:02] që në thelb duhet të nënkuptojë marrëveshjen për shtresën në fillim të seancës

Nga rruga, rrjedh se zbritja e klientit nuk ofrohet

Përditësimet, d.m.th. lloji Updates në skemë, kjo është ajo që serveri i dërgon klientit jo në përgjigje të një kërkese API, por në mënyrë të pavarur kur ndodh një ngjarje. Kjo është një temë komplekse që do të diskutohet në një postim tjetër, por tani për tani është e rëndësishme të dini se serveri ruan Përditësimet edhe kur klienti është jashtë linje.

Kështu, nëse refuzoni të mbështillni i secilit paketë për të treguar versionin e saj, kjo logjikisht çon në problemet e mëposhtme të mundshme:

  • serveri i dërgon klientit përditësime edhe para se klienti të ketë informuar se cilin version e mbështet
  • çfarë duhet të bëj pas përmirësimit të klientit?
  • garancitëse mendimi i serverit për numrin e shtresës nuk do të ndryshojë gjatë procesit?

A mendoni se ky është një spekulim thjesht teorik dhe në praktikë kjo nuk mund të ndodhë, sepse serveri është shkruar saktë (të paktën është testuar mirë)? Ha! Pavarësisht se si është!

Kjo është pikërisht ajo që kemi hasur në gusht. Më 14 gusht, kishte mesazhe se diçka po përditësohej në serverët e Telegram-it... dhe më pas në regjistrat:

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.

dhe më pas disa megabajt gjurmë rafte (mirë, në të njëjtën kohë prerjet u rregulluan). Në fund të fundit, nëse diçka nuk njihet në TL-në tuaj, është binare nga nënshkrimi, më poshtë TE GJITHA shkon, deshifrimi do të bëhet i pamundur. Çfarë duhet të bëni në një situatë të tillë?

Epo, gjëja e parë që i vjen në mendje dikujt është të shkëputet dhe të provojë përsëri. Nuk ndihmoi. Ne kërkojmë në google CRC32 - këto rezultuan të ishin objekte nga skema 73, megjithëse kemi punuar në 82. Ne shikojmë me kujdes regjistrat - ka identifikues nga dy skema të ndryshme!

Ndoshta problemi është thjesht tek klienti ynë jozyrtar? Jo, ne lëshojmë Telegram Desktop 1.2.17 (versioni i ofruar në një numër shpërndarjesh Linux), ai shkruan në regjistrin e përjashtimeve: MTP Lloji i papritur id #b5223b0f lexuar në MTPMessageMedia…

Kritika e protokollit dhe qasjeve organizative të Telegramit. Pjesa 1, teknike: përvoja e të shkruarit të një klienti nga e para - TL, MT

Google tregoi se një problem i ngjashëm kishte ndodhur tashmë me një nga klientët jozyrtar, por më pas numrat e versionit dhe, në përputhje me rrethanat, supozimet ishin të ndryshme ...

Pra, çfarë duhet të bëjmë? Unë dhe Vasily u ndamë: ai u përpoq të përditësonte qarkun në 91, vendosa të prisja disa ditë dhe të provoja 73. Të dyja metodat funksionuan, por duke qenë se janë empirike, nuk mund të kuptojmë se sa versione lart ose poshtë ju nevojiten. për të kërcyer, ose sa kohë duhet të prisni.

Më vonë isha në gjendje të riprodhoja situatën: ne lançojmë klientin, e fikim atë, ripërpilojmë qarkun në një shtresë tjetër, rinisim, kapim përsëri problemin, kthehemi te ai i mëparshmi - oops, nuk ka sasi të ndërrimit të qarkut dhe klienti rinis për një disa minuta do të ndihmojnë. Ju do të merrni një përzierje të strukturave të të dhënave nga shtresa të ndryshme.

Shpjegim? Siç mund ta merrni me mend nga simptoma të ndryshme indirekte, serveri përbëhet nga shumë procese të llojeve të ndryshme në makina të ndryshme. Me shumë mundësi, serveri që është përgjegjës për "buffering" vendosi në radhë atë që eprorët e tij i dhanë, dhe ata e dhanë atë në skemën që ishte në fuqi në kohën e gjenerimit. Dhe derisa kjo radhë të "kalbet", asgjë nuk mund të bëhej për këtë.

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

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

ose

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

Hmm... duket e egër. Por, me siguri, ky është kod i krijuar, atëherë në rregull?.. Por sigurisht që i mbështet të gjitha versionet! Vërtetë, nuk është e qartë pse gjithçka është e përzier së bashku, bisedat sekrete dhe të gjitha llojet e tyre _old7 disi nuk duken si gjenerata e makinerive... Megjithatë, mbi të gjitha më ka mahnitur

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Djema, nuk mund të vendosni se çfarë ka brenda një shtrese?! Epo, mirë, le të themi se "dy" u lëshuan me një gabim, mirë, ndodh, por TRE?.. Menjëherë, përsëri i njëjti grabujë? Çfarë lloj pornografie është kjo, më fal?..

Në kodin burimor të Telegram Desktop, nga rruga, një gjë e ngjashme ndodh - nëse po, disa kryerje me radhë në skemë nuk e ndryshojnë numrin e shtresës së saj, por rregullojnë diçka. Në kushtet kur nuk ka burim zyrtar të të dhënave për skemën, nga mund të merren ato, përveç kodit burimor të klientit zyrtar? Dhe nëse e merrni nga atje, nuk mund të jeni i sigurt që skema është plotësisht e saktë derisa të provoni të gjitha metodat.

Si mund të testohet kjo? Shpresoj që fansat e testeve të njësisë, funksionale dhe të tjera të ndajnë në komente.

Mirë, le të shohim një pjesë tjetër të kodit:

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;

Ky koment "i krijuar me dorë" sugjeron që vetëm një pjesë e këtij skedari është shkruar me dorë (a mund ta imagjinoni të gjithë makthin e mirëmbajtjes?), dhe pjesa tjetër u krijua nga makineri. Megjithatë, atëherë lind një pyetje tjetër - që burimet janë të disponueshme jo plotësisht (a la GPL blobs në kernel Linux), por kjo është tashmë një temë për pjesën e dytë.

Por mjaft. Le të kalojmë te protokolli në krye të të cilit funksionon i gjithë ky serializimi.

MT Proto

Pra, le të hapim общее описание и përshkrim i detajuar i protokollit dhe gjëja e parë që ne pengojmë është terminologjia. Dhe me një bollëk të gjithçkaje. Në përgjithësi, kjo duket të jetë një veçori pronësore e Telegram - duke i quajtur gjërat ndryshe në vende të ndryshme, ose gjëra të ndryshme me një fjalë, ose anasjelltas (për shembull, në një API të nivelit të lartë, nëse shihni një paketë ngjitëse, nuk është çfarë keni menduar).

Për shembull, "mesazh" dhe "sesion" nënkuptojnë diçka ndryshe këtu sesa në ndërfaqen e zakonshme të klientit Telegram. Epo, gjithçka është e qartë me mesazhin, mund të interpretohet në terma OOP, ose thjesht të quhet fjala "paketë" - ky është një nivel i ulët transporti, nuk ka të njëjtat mesazhe si në ndërfaqe, ka shumë mesazhe shërbimi . Por seanca... por gjërat e para së pari.

shtresa e transportit

Gjëja e parë është transporti. Ata do të na tregojnë për 5 opsione:

  • TCP
  • Xhep
  • Mbajtja e internetit mbi HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Ka edhe transport UDP, por nuk është i dokumentuar.

Dhe TCP në tre variante

E para është e ngjashme me UDP mbi TCP, çdo paketë përfshin një numër sekuence dhe crc
Pse është kaq e dhimbshme leximi i dokumenteve në një karrocë?

Epo, ja ku është tani TCP tashmë në 4 variante:

  • shkurtuar
  • I ndërmjetëm
  • E ndërmjetme e mbushur
  • Plot

Epo, ok, ndërmjetëse e mbushur për MTProxy, kjo u shtua më vonë për shkak të ngjarjeve të njohura. Por pse dy versione të tjera (tre gjithsej) kur mund të arrini me një? Të katërt në thelb ndryshojnë vetëm në mënyrën e përcaktimit të gjatësisë dhe ngarkesës së MTProto kryesore, e cila do të diskutohet më tej:

  • në shkurtuar është 1 ose 4 bajt, por jo 0xef, pastaj trupi
  • në Intermediate kjo është 4 bajt gjatësi dhe një fushë, dhe hera e parë që klienti duhet të dërgojë 0xeeeeeeee për të treguar se është e ndërmjetme
  • Në Plot më të varurit, nga këndvështrimi i një rrjeti: gjatësia, numri i sekuencës dhe JO AI që është kryesisht MTProto, trupi, CRC32. Po, e gjithë kjo është në krye të TCP. E cila na siguron transport të besueshëm në formën e një rryme sekuenciale bajtësh; nuk nevojiten sekuenca, veçanërisht shumat e kontrollit. Në rregull, tani dikush do të më kundërshtojë se TCP ka një kontroll 16-bitësh, kështu që ndodh prishja e të dhënave. E shkëlqyeshme, por ne në fakt kemi një protokoll kriptografik me hash më të gjatë se 16 bajt, të gjitha këto gabime - dhe madje edhe më shumë - do të kapen nga një mospërputhje SHA në një nivel më të lartë. Nuk ka asnjë pikë në CRC32 mbi këtë.

Le të krahasojmë Shkurtimin, në të cilin është i mundur një bajt i gjatësisë, me Intermediate, që justifikon "Në rast se nevojitet përafrimi i të dhënave 4-bajtë", gjë që është krejt e pakuptimtë. Çfarë, besohet se programuesit e Telegram janë aq të paaftë sa nuk mund të lexojnë të dhëna nga një prizë në një tampon të rreshtuar? Ju ende duhet ta bëni këtë, sepse leximi mund t'ju kthejë çdo numër bajtësh (dhe ka edhe serverë proxy, për shembull...). Ose nga ana tjetër, pse të bllokojmë Shkurtimin nëse do të kemi akoma një mbushje të madhe mbi 16 bajt - kurseni 3 bajt иногда ?

Të krijohet përshtypja se Nikolai Durovit i pëlqen vërtet të rishpik rrotat, duke përfshirë protokollet e rrjetit, pa ndonjë nevojë reale praktike.

Opsione të tjera transporti, përfshirë. Web dhe MTProxy, ne nuk do ta shqyrtojmë tani, ndoshta në një postim tjetër, nëse ka një kërkesë. Për të njëjtin MTProxy, le të kujtojmë vetëm tani që menjëherë pas lëshimit të tij në 2018, ofruesit mësuan shpejt ta bllokonin atë, të destinuar për bllokimi i anashkalimitNga madhësia e paketës! Dhe gjithashtu fakti që serveri MTProxy i shkruar (përsëri nga Waltman) në C ishte tepër i lidhur me specifikat e Linux, megjithëse kjo nuk kërkohej fare (Phil Kulin do të konfirmojë), dhe se një server i ngjashëm qoftë në Go ose Node.js do të përshtaten në më pak se njëqind rreshta.

Por ne do të nxjerrim përfundime për njohuritë teknike të këtyre njerëzve në fund të seksionit, pasi të shqyrtojmë çështje të tjera. Tani për tani, le të kalojmë në shtresën 5 të OSI, sesioni - në të cilin vendosën sesionin MTProto.

Çelësat, mesazhet, sesionet, Diffie-Hellman

Ata e vendosën atje jo plotësisht saktë... Një seancë nuk është e njëjta seancë që është e dukshme në ndërfaqen nën Sesionet aktive. Por në rregull.

Kritika e protokollit dhe qasjeve organizative të Telegramit. Pjesa 1, teknike: përvoja e të shkruarit të një klienti nga e para - TL, MT

Pra, ne morëm një varg bajt me gjatësi të njohur nga shtresa e transportit. Ky është ose një mesazh i koduar ose tekst i thjeshtë - nëse jemi ende në fazën e marrëveshjes kyçe dhe në fakt po e bëjmë atë. Për cilin prej koncepteve të quajtura "çelës" po flasim? Le ta sqarojmë këtë çështje për vetë ekipin e Telegram (kërkoj falje që përktheva dokumentacionin tim nga anglishtja me një tru të lodhur në orën 4 të mëngjesit, ishte më e lehtë të lija disa fraza ashtu siç janë):

Janë dy entitete të quajtura seancë - një në UI-në e klientëve zyrtarë nën "sesionet aktuale", ku çdo seancë korrespondon me një pajisje / OS të tërë.
E dyta - Sesioni MTProto, i cili ka numrin e sekuencës së mesazhit (në kuptimin e nivelit të ulët) në të dhe cili mund të zgjasë ndërmjet lidhjeve të ndryshme TCP. Disa seanca MTProto mund të instalohen në të njëjtën kohë, për shembull, për të shpejtuar shkarkimin e skedarëve.

Mes këtyre dyve seanca ka një koncept autorizim. Në rastin e degjeneruar, mund të themi se Sesioni i UI është e njëjtë si autorizim, por mjerisht, gjithçka është e ndërlikuar. Le të shohim:

  • Përdoruesi në pajisjen e re gjeneron fillimisht auth_key dhe e lidh atë me llogarinë, për shembull përmes SMS - kjo është arsyeja pse autorizim
  • Ndodhi brenda të parës Sesioni MTProto, e cila ka session_id brenda vetes.
  • Në këtë hap, kombinimi autorizim и session_id mund të quhet shembull - kjo fjalë shfaqet në dokumentacionin dhe kodin e disa klientëve
  • Pastaj klienti mund të hapet несколько Seancat MTProto nën të njëjtën auth_key - në të njëjtën DC.
  • Pastaj, një ditë klienti do të duhet të kërkojë skedarin nga një tjetër DC - dhe për këtë DC do të gjenerohet një e re auth_key !
  • Për të informuar sistemin se nuk është një përdorues i ri që regjistrohet, por i njëjti autorizim (Sesioni i UI), klienti përdor thirrjet API auth.exportAuthorization në shtëpi DC auth.importAuthorization në DC-në e re.
  • Gjithçka është e njëjtë, disa mund të jenë të hapura Seancat MTProto (secila me të vetat session_id) në këtë DC të re, nën e tij auth_key.
  • Së fundi, klienti mund të dëshirojë fshehtësi të përsosur përpara. Çdo auth_key был i përhershëm kyç - për DC - dhe klienti mund të telefonojë auth.bindTempAuthKey per perdorim i përkohshëm auth_key - dhe përsëri, vetëm një temp_auth_key për DC, e përbashkët për të gjithë Seancat MTProto në këtë DC.

vini re, se kripë (dhe kripërat e ardhshme) është gjithashtu një në auth_key ato. ndahet mes të gjithëve Seancat MTProto në të njëjtën DC.

Çfarë do të thotë "midis lidhjeve të ndryshme TCP"? Pra kjo do të thotë diçka si cookie autorizimi në një faqe interneti - vazhdon (mbijeton) shumë lidhje TCP me një server të caktuar, por një ditë shkon keq. Vetëm ndryshe nga HTTP, në MTProto mesazhet brenda një sesioni numërohen dhe konfirmohen në mënyrë sekuenciale; nëse ato hynë në tunel, lidhja u prish - pas vendosjes së një lidhjeje të re, serveri me mirësi do të dërgojë gjithçka në këtë seancë që nuk ka dorëzuar në të kaluarën. Lidhja TCP.

Megjithatë, informacioni i mësipërm është përmbledhur pas shumë muajsh hetimi. Ndërkohë, a po e zbatojmë klientin tonë nga e para? - le të kthehemi në fillim.

Pra, le të gjenerojmë auth_key mbi Versionet e Diffie-Hellman nga Telegram. Le të përpiqemi të kuptojmë dokumentacionin ...

Vasily, [19.06.18 20:05] data_me_hash := SHA1(të dhëna) + të dhëna + (ndonjë bajt të rastit); e tillë që gjatësia është e barabartë me 255 bajt;
të dhënat e_enkriptuara := RSA(të dhënat_me_hash, server_çelës_publik); një numër 255-bajtë i gjatë (endian i madh) ngrihet në fuqinë e nevojshme mbi modulin e nevojshëm dhe rezultati ruhet si një numër 256 bajt.

Ata kanë disa drogë DH

Nuk duket si DH e një personi të shëndetshëm
Nuk ka dy çelësa publikë në dx

Epo, në fund kjo u zgjidh, por mbeti një mbetje - klienti bën prova të punës se ai ishte në gjendje të faktorizonte numrin. Lloji i mbrojtjes kundër sulmeve DoS. Dhe çelësi RSA përdoret vetëm një herë në një drejtim, në thelb për enkriptim new_nonce. Por ndërsa ky operacion në dukje i thjeshtë do të ketë sukses, me çfarë do të duhet të përballeni?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Nuk kam arritur ende në kërkesën e aplikacionit

Këtë kërkesë ia dërgova DH

Dhe, në dok transporti thotë se mund të përgjigjet me 4 bajt të një kodi gabimi. Kjo eshte e gjitha

Epo, ai më tha -404, pra çfarë?

Kështu që unë i thashë: "Kap budallallëkun tënd të koduar me një çelës serveri me një gjurmë gishti si ky, dua DH," dhe ai u përgjigj me një 404 budallaqe.

Çfarë do të mendonit për këtë përgjigje të serverit? Çfarë duhet bërë? Nuk ka kush të pyesë (por më shumë për këtë në pjesën e dytë).

Këtu i gjithë interesi bëhet në bankën e të akuzuarve

Nuk kam asgjë tjetër për të bërë, thjesht kam ëndërruar të konvertoj numrat përpara dhe mbrapa

Dy numra 32 bit. I kam paketuar si gjithë të tjerët

Por jo, këto të dyja duhet të shtohen në rresht së pari si BE

Vadim Goncharov, [20.06.18 15:49] dhe për shkak të kësaj 404?

Vasily, [20.06.18 15:49] ДА!

Vadim Goncharov, [20.06.18 15:50] kështu që nuk e kuptoj se çfarë mund "nuk gjeti"

Vasily, [20.06.18 15:50] për

Unë nuk mund të gjeja një zbërthim të tillë në faktorët kryesorë%)

Ne nuk e menaxhuam as raportimin e gabimeve

Vasily, [20.06.18 20:18] Oh, ka edhe MD5. Tashmë tre hase të ndryshme

Gjurma e gishtit kyç llogaritet si më poshtë:

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

SHA1 dhe sha2

Pra, le të vënë atë auth_key kemi marrë 2048 bit në madhësi duke përdorur Diffie-Hellman. Ç'pritet më tej? Më pas zbulojmë se 1024 bitet më të ulëta të këtij çelësi nuk përdoren në asnjë mënyrë... por le të mendojmë për këtë tani për tani. Në këtë hap, ne kemi një sekret të përbashkët me serverin. Është krijuar një analog i seancës TLS, që është një procedurë shumë e shtrenjtë. Por serveri ende nuk di asgjë se kush jemi ne! Ende jo, në fakt. autorizimi. Ato. nëse keni menduar në termat e "login-password", siç keni bërë dikur në ICQ, ose të paktën "login-key", si në SSH (për shembull, në disa gitlab/github). Ne morëm një anonim. Po sikur serveri të na thotë "këta numra telefoni shërbehen nga një DC tjetër"? Apo edhe "numri juaj i telefonit është i ndaluar"? Më e mira që mund të bëjmë është ta mbajmë çelësin me shpresën se do të jetë i dobishëm dhe nuk do të kalbet deri atëherë.

Meqë ra fjala, ne e “pritëm” me rezerva. Për shembull, a i besojmë serverit? Po sikur të jetë false? Do të nevojiteshin kontrolle kriptografike:

Vasily, [21.06.18 17:53] Ata u ofrojnë klientëve celularë të kontrollojnë një numër 2 kbit për primitet%)

Por nuk është aspak e qartë, nafeijoa

Vasily, [21.06.18 18:02] Dokumenti nuk thotë se çfarë të bëni nëse rezulton se nuk është e thjeshtë

Nuk është thënë. Le të shohim se çfarë bën klienti zyrtar Android në këtë rast? A Ja cfarë (dhe po, i gjithë skedari është interesant) - siç thonë ata, unë thjesht do ta lë këtë këtu:

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

Jo, sigurisht që është ende atje disa Ka teste për parësinë e një numri, por personalisht nuk kam më njohuri të mjaftueshme për matematikën.

Mirë, morëm çelësin kryesor. Për të hyrë, d.m.th. dërgoni kërkesa, duhet të kryeni kriptim të mëtejshëm, duke përdorur AES.

Tasti i mesazhit përkufizohet si 128 bitët e mesit të SHA256 të trupit të mesazhit (përfshirë sesionin, ID-në e mesazhit, etj.), duke përfshirë bajtet e mbushjes, të paraprirë nga 32 bajt të marra nga çelësi i autorizimit.

Vasily, [22.06.18 14:08] Mesatar, kurvë, bit

Marrë auth_key. Të gjitha. Përtej tyre... nuk është e qartë nga dokumenti. Mos ngurroni të studioni kodin me burim të hapur.

Vini re se MTProto 2.0 kërkon nga 12 deri në 1024 byte mbushje, ende me kusht që gjatësia e mesazhit që rezulton të jetë e pjestueshme me 16 bajt.

Pra, sa mbushje duhet të shtoni?

Dhe po, ekziston edhe një 404 në rast gabimi

Nëse dikush studionte me kujdes diagramin dhe tekstin e dokumentacionit, ai vuri re se atje nuk kishte MAC. Dhe se AES përdoret në një mënyrë të caktuar IGE që nuk përdoret askund tjetër. Ata, natyrisht, shkruajnë për këtë në FAQ-të e tyre... Këtu, si p.sh., vetë çelësi i mesazhit është gjithashtu hash-i SHA i të dhënave të deshifruara, i përdorur për të kontrolluar integritetin - dhe në rast mospërputhjeje, dokumentacioni për ndonjë arsye rekomandon injorimin e tyre në heshtje (por po me sigurinë, po sikur të na thyejnë?).

Unë nuk jam një kriptograf, ndoshta nuk ka asgjë të keqe me këtë mënyrë në këtë rast nga pikëpamja teorike. Por unë mund të përmend qartë një problem praktik, duke përdorur Telegram Desktop si shembull. Ai kodon cache-në lokale (të gjitha këto D877F783D5D3EF8C) në të njëjtën mënyrë si mesazhet në MTProto (vetëm në këtë rast versioni 1.0), d.m.th. fillimisht çelësi i mesazhit, pastaj vetë të dhënat (dhe diku mënjanë kryesorja e madhe auth_key 256 bajt, pa të cilat msg_key të padobishme). Pra, problemi bëhet i dukshëm në skedarë të mëdhenj. Përkatësisht, ju duhet të mbani dy kopje të të dhënave - të koduara dhe të deshifruara. Dhe nëse ka megabajt, ose video streaming, për shembull?.. Skemat klasike me MAC pas tekstit të shifruar ju lejojnë ta lexoni atë në transmetim, duke e transmetuar menjëherë. Por me MTProto do t'ju duhet në fillim enkriptoni ose deshifroni të gjithë mesazhin, vetëm atëherë transferojeni atë në rrjet ose në disk. Prandaj, në versionet më të fundit të Telegram Desktop në cache in user_data Përdoret gjithashtu një format tjetër - me AES në modalitetin CTR.

Vasily, [21.06.18 01:27] Oh, kuptova se çfarë është IGE: IGE ishte përpjekja e parë në një "modalitet kriptimi vërtetues", fillimisht për Kerberos. Ishte një përpjekje e dështuar (nuk ofron mbrojtje të integritetit) dhe duhej të hiqej. Ky ishte fillimi i një kërkimi 20-vjeçar për një mënyrë kriptimi vërtetues që funksionon, i cili kohët e fundit arriti kulmin në mënyra si OCB dhe GCM.

Dhe tani argumentet nga ana e karrocës:

Ekipi që qëndron pas Telegram-it, i udhëhequr nga Nikolai Durov, përbëhet nga gjashtë kampionë të ACM-së, gjysma e tyre doktorante në matematikë. Atyre iu deshën rreth dy vjet për të nxjerrë versionin aktual të MTProto.

Kjo është qesharake. Dy vjet në nivelin më të ulët

Ose thjesht mund të marrësh tls

Në rregull, le të themi se kemi bërë enkriptimin dhe nuancat e tjera. A është më në fund e mundur dërgimi i kërkesave të serializuara në TL dhe deserializimi i përgjigjeve? Pra, çfarë dhe si duhet të dërgoni? Këtu, le të themi, metoda initConnection, ndoshta kjo është ajo?

Vasily, [25.06.18 18:46] Inicializon lidhjen dhe ruan informacionin në pajisjen dhe aplikacionin e përdoruesit.

Ai pranon app_id, pajisje_model, system_version, app_version dhe lang_code.

Dhe disa pyetje

Dokumentacioni si gjithmonë. Mos ngurroni të studioni burimin e hapur

Nëse gjithçka ishte afërsisht e qartë me invokeWithLayer, atëherë çfarë nuk shkon këtu? Rezulton, le të themi se kemi - klienti tashmë kishte diçka për të pyetur serverin - ekziston një kërkesë që ne donim të dërgonim:

Vasily, [25.06.18 19:13] Duke gjykuar nga kodi, thirrja e parë është e mbështjellë në këtë katrahurë, dhe vetë katrahura është e mbështjellë në invokewithlayer

Pse initConnection nuk mund të jetë një telefonatë më vete, por duhet të jetë një mbështjellës? Po, siç doli, duhet të bëhet çdo herë në fillim të çdo seance, dhe jo një herë, si me çelësin kryesor. Por! Nuk mund të thirret nga një përdorues i paautorizuar! Tani kemi arritur në fazën ku është e zbatueshme Këtë faqja e dokumentacionit - dhe na tregon se...

Vetëm një pjesë e vogël e metodave API janë në dispozicion për përdoruesit e paautorizuar:

  • auth.sendCode
  • auth.ridërgo Kod
  • account.getPassword
  • auth.kontrolloni fjalëkalimin
  • auth.kontrollo Telefonin
  • auth.regjistrimi
  • auth.hyrja
  • autorizimi.importAutorizimi
  • help.getConfig
  • ndihmë.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getGjuhët
  • langpack.getLanguage

E para prej tyre, auth.sendCode, dhe ekziston ajo kërkesa e parë e dashur në të cilën dërgojmë api_id dhe api_hash, dhe pas së cilës marrim një SMS me një kod. Dhe nëse jemi në DC të gabuar (numrat e telefonit në këtë vend shërbehen nga një tjetër, për shembull), atëherë do të marrim një gabim me numrin e DC-së së dëshiruar. Për të zbuluar se me cilën adresë IP sipas numrit DC duhet të lidheni, na ndihmoni help.getConfig. Dikur kishte vetëm 5 hyrje, por pas ngjarjeve të famshme të vitit 2018, numri është rritur ndjeshëm.

Tani le të kujtojmë se arritëm në këtë fazë në server në mënyrë anonime. A nuk është shumë e shtrenjtë të marrësh vetëm një adresë IP? Pse të mos e bëni këtë dhe operacione të tjera në pjesën e pakriptuar të MTProto? Dëgjoj kundërshtimin: "Si mund të sigurohemi që nuk është RKN ajo që do të përgjigjet me adresa false?" Për këtë ne kujtojmë se, në përgjithësi, klientët zyrtarë Çelësat RSA janë të ngulitur, d.m.th. a mundesh vetem shenjë ky informacion. Në fakt, kjo tashmë po bëhet për informacion mbi anashkalimin e bllokimit që klientët marrin përmes kanaleve të tjera (logjikisht, kjo nuk mund të bëhet në vetë MTProto; gjithashtu duhet të dini se ku të lidheni).

NE RREGULL. Në këtë fazë të autorizimit të klientit, ne nuk jemi ende të autorizuar dhe nuk e kemi regjistruar aplikimin tonë. Ne vetëm duam të shohim tani për tani se çfarë përgjigjet serveri ndaj metodave të disponueshme për një përdorues të paautorizuar. Dhe këtu…

Vasily, [10.07.18 14:45] https://core.telegram.org/method/help.getConfig

config#7dae33e0 [...] = Config;
help.getConfig#c4f9186b = Config;

https://core.telegram.org/api/datacenter

config#232d5905 [...] = Config;
help.getConfig#c4f9186b = Config;

Në skemë, i pari vjen i dyti

Në skemën tdesktop vlera e tretë është

Po, që atëherë, natyrisht, dokumentacioni është përditësuar. Edhe pse së shpejti mund të bëhet përsëri i parëndësishëm. Si duhet të dijë një zhvillues fillestar? Ndoshta nëse regjistroni aplikimin tuaj, ata do t'ju informojnë? Vasily e bëri këtë, por mjerisht, ata nuk i dërguan asgjë (përsëri, ne do të flasim për këtë në pjesën e dytë).

...Ju keni vënë re se ne kemi kaluar disi në API, d.m.th. në nivelin tjetër, dhe keni humbur diçka në temën MTProto? Asnjë çudi:

Vasily, [28.06.18 02:04] Mm, ata po gërmojnë disa nga algoritmet në e2e

Mtproto përcakton algoritmet dhe çelësat e enkriptimit për të dy domenet, si dhe pak një strukturë mbështjellëse

Por ata vazhdimisht përziejnë nivele të ndryshme të stivit, kështu që nuk është gjithmonë e qartë se ku mbaroi mtproto dhe filloi niveli tjetër

Si përzihen ato? Epo, këtu është i njëjti çelës i përkohshëm për PFS, për shembull (nga rruga, Telegram Desktop nuk mund ta bëjë këtë). Ai ekzekutohet nga një kërkesë API auth.bindTempAuthKey, d.m.th. nga niveli i lartë. Por në të njëjtën kohë ajo ndërhyn me enkriptimin në nivelin më të ulët - pas tij, për shembull, duhet ta bëni përsëri initConnection etj., kjo nuk është просто kërkesë normale. Ajo që është gjithashtu e veçantë është se mund të keni vetëm NJË çelës të përkohshëm për DC, edhe pse në fushë auth_key_id në çdo mesazh ju lejon të ndryshoni çelësin të paktën çdo mesazh, dhe se serveri ka të drejtë të "harrojë" çelësin e përkohshëm në çdo kohë - dokumentacioni nuk thotë se çfarë të bëni në këtë rast ... mirë, pse mund të Nuk keni disa çelësa, si me një grup kripërash të ardhshme, dhe ?..

Ka disa gjëra të tjera që ia vlen të përmenden në lidhje me temën MTProto.

Mesazhe mesazhesh, msg_id, msg_seqno, konfirmime, ping në drejtimin e gabuar dhe veçori të tjera

Pse duhet të dini rreth tyre? Sepse ato "rrjedhin" në një nivel më të lartë, dhe ju duhet të jeni të vetëdijshëm për to kur punoni me API. Le të supozojmë se nuk jemi të interesuar për msg_key; niveli më i ulët ka deshifruar gjithçka për ne. Por brenda të dhënave të deshifruara kemi fushat e mëposhtme (gjithashtu gjatësinë e të dhënave, kështu që ne e dimë se ku është mbushja, por kjo nuk është e rëndësishme):

  • kripë - int64
  • sesion_id - int64
  • mesazh_id - int64
  • seq_no - int32

Ju kujtojmë se ka vetëm një kripë për të gjithë DC. Pse di për të? Jo vetëm sepse ka një kërkesë get_future_salts, i cili ju tregon se cilat intervale do të jenë të vlefshme, por edhe sepse nëse kripa juaj është e “kalbur”, atëherë mesazhi (kërkesa) thjesht do të humbasë. Serveri, sigurisht, do të raportojë kripën e re duke e lëshuar new_session_created - por me të vjetrën do të duhet ta ridërgoni disi, për shembull. Dhe kjo çështje ndikon në arkitekturën e aplikacionit.

Serverit i lejohet të heqë seancat fare dhe të përgjigjet në këtë mënyrë për shumë arsye. Në fakt, çfarë është një seancë MTProto nga ana e klientit? Këto janë dy numra session_id и seq_no mesazhet brenda këtij sesioni. Epo, dhe lidhja themelore TCP, natyrisht. Le të themi se klienti ynë ende nuk di të bëjë shumë gjëra, ai u shkëput dhe u rilidh. Nëse kjo ndodhi shpejt - sesioni i vjetër vazhdoi në lidhjen e re TCP, rriteni seq_no me tutje. Nëse kërkon një kohë të gjatë, serveri mund ta fshijë atë, sepse në anën e tij është gjithashtu një radhë, siç e morëm vesh.

Çfarë duhet të jetë seq_no? Oh, kjo është një pyetje e ndërlikuar. Mundohuni të kuptoni sinqerisht se çfarë do të thoshte:

Mesazh i lidhur me përmbajtjen

Një mesazh që kërkon një njohje të qartë. Këto përfshijnë të gjithë përdoruesit dhe shumë mesazhe shërbimi, praktikisht të gjitha me përjashtim të kontejnerëve dhe mirënjohjeve.

Numri i sekuencës së mesazheve (msg_seqno)

Një numër 32-bit i barabartë me dyfishin e numrit të mesazheve "të lidhura me përmbajtjen" (ato që kërkojnë njohje, dhe veçanërisht ato që nuk janë kontejnerë) të krijuar nga dërguesi para këtij mesazhi dhe më pas të shtuara me një nëse mesazhi aktual është një mesazh i lidhur me përmbajtjen. Një kontejner gjenerohet gjithmonë pas gjithë përmbajtjes së tij; prandaj, numri i tij i sekuencës është më i madh ose i barabartë me numrat e sekuencës së mesazheve që përmbahen në të.

Çfarë lloj cirku është ky me një rritje me 1, dhe më pas një tjetër me 2?.. Dyshoj se fillimisht ata nënkuptuan "gjënë më pak të rëndësishme për ACK, pjesa tjetër është një numër", por rezultati nuk është plotësisht i njëjtë - në veçanti, del, mund të dërgohet несколько konfirmimet që kanë të njëjtën gjë seq_no! Si? Epo, për shembull, serveri na dërgon diçka, e dërgon atë, dhe ne vetë heshtim, duke u përgjigjur vetëm me mesazhe shërbimi që konfirmojnë marrjen e mesazheve të tij. Në këtë rast, konfirmimet tona dalëse do të kenë të njëjtin numër dalës. Nëse jeni njohur me TCP dhe mendoni se kjo tingëllon disi e egër, por duket jo shumë e egër, sepse në TCP seq_no nuk ndryshon, por konfirmimi shkon tek seq_no nga ana tjetër, do të nxitoj t'ju mërzit. Konfirmimet jepen në MTProto NUK mbi seq_no, si në TCP, por nga msg_id !

Çfarë është kjo msg_id, më e rëndësishmja nga këto fusha? Një identifikues unik i mesazhit, siç sugjeron emri. Përkufizohet si një numër 64-bitësh, bitet më të ulëta të të cilit përsëri kanë magjinë "server-jo-server", dhe pjesa tjetër është një stamp kohor Unix, duke përfshirë pjesën fraksionale, të zhvendosur 32 bit në të majtë. Ato. vula kohore në vetvete (dhe mesazhet me kohë që ndryshojnë shumë do të refuzohen nga serveri). Nga kjo rezulton se në përgjithësi ky është një identifikues që është global për klientin. Duke pasur parasysh këtë - le të kujtojmë session_id - jemi të garantuar: Në asnjë rrethanë nuk mund të dërgohet një mesazh i destinuar për një seancë në një seancë tjetër. Kjo është, rezulton se ka tashmë tre niveli - sesioni, numri i seancës, ID-ja e mesazhit. Pse një ndërlikim i tillë, ky mister është shumë i madh.

Pra, msg_id nevojiten për...

RPC: kërkesa, përgjigje, gabime. Konfirmimet.

Siç mund ta keni vënë re, nuk ka asnjë lloj ose funksion të veçantë "bëni një kërkesë RPC" askund në diagram, megjithëse ka përgjigje. Në fund të fundit, ne kemi mesazhe të lidhura me përmbajtjen! Kjo eshte, ndonjë mesazhi mund të jetë një kërkesë! Ose të mos jesh. Pas te gjithave, i secilit ka msg_id. Por ka përgjigje:

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

Këtu tregohet se cilit mesazh i përgjigjet. Prandaj, në nivelin më të lartë të API-së, do të duhet të mbani mend se cili ishte numri i kërkesës suaj - mendoj se nuk ka nevojë të shpjegoni se puna është asinkrone dhe mund të ketë disa kërkesa në vazhdim në të njëjtën kohë, përgjigjet për të cilat mund të kthehen në çdo mënyrë? Në parim, nga ky dhe mesazhet e gabimit si asnjë punonjës, mund të gjurmohet arkitektura që qëndron pas kësaj: serveri që mban një lidhje TCP me ju është një balancues i pjesës së përparme, ai i përcjell kërkesat në prapavijë dhe i mbledh ato përsëri nëpërmjet message_id. Duket se gjithçka këtu është e qartë, logjike dhe e mirë.

Po?.. Dhe nëse mendoni për këtë? Në fund të fundit, vetë përgjigja RPC gjithashtu ka një fushë msg_id! A duhet t'i bërtasim serverit "nuk po i përgjigjesh përgjigjes sime!"? Dhe po, çfarë kishte në lidhje me konfirmimet? Rreth faqes mesazhe rreth mesazheve na tregon se çfarë është

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

dhe duhet të bëhet nga secila anë. Por jo gjithmonë! Nëse keni marrë një RpcResult, ai vetë shërben si një konfirmim. Kjo do të thotë, serveri mund t'i përgjigjet kërkesës tuaj me MsgsAck - si, "Unë e mora atë". RpcResult mund të përgjigjet menjëherë. Mund të jenë të dyja.

Dhe po, ju ende duhet t'i përgjigjeni përgjigjes! Konfirmimi. Përndryshe, serveri do ta konsiderojë atë të papërdorshëm dhe do t'jua dërgojë përsëri. Edhe pas rilidhjes. Por këtu, natyrisht, lind çështja e afateve. Le t'i shohim ato pak më vonë.

Ndërkohë, le të shohim gabimet e mundshme të ekzekutimit të pyetjes.

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

Oh, dikush do të bërtasë, këtu është një format më human - ka një rresht! Merrni kohën tuaj. Këtu lista e gabimeve, por sigurisht jo e plotë. Prej tij mësojmë se kodi është diçka si Gabimet HTTP (epo, sigurisht, semantika e përgjigjeve nuk respektohet, në disa vende ato shpërndahen rastësisht midis kodeve), dhe linja duket si KAPITALE_LETTERS_AND_NUMRA. Për shembull, PHONE_NUMBER_OCCUPIED ose FILE_PART_Х_MISSING. Epo, domethënë, do t'ju duhet ende kjo linjë analizoj. Për shembull, FLOOD_WAIT_3600 do të thotë që ju duhet të prisni një orë, dhe PHONE_MIGRATE_5, që një numër telefoni me këtë prefiks duhet të jetë i regjistruar në QK 5. Ne kemi një gjuhë tip, apo jo? Ne nuk kemi nevojë për një argument nga një varg, ato të rregullta do të bëjnë, në rregull.

Përsëri, kjo nuk është në faqen e mesazheve të shërbimit, por, siç është tashmë e zakonshme me këtë projekt, informacioni mund të gjendet në një faqe tjetër dokumentacioni. ose hedhin dyshime. Së pari, shikoni, shkelja e shtypjes/shtresës - RpcError mund të futet në fole RpcResult. Pse jo jashtë? Çfarë nuk kemi marrë parasysh?.. Prandaj, ku është garancia që RpcError NUK mund të përfshihet në RpcResult, por të jetë direkt apo i folezuar në një lloj tjetër?.. Dhe nëse nuk mundet, pse nuk është në nivelin e lartë, d.m.th. mungon req_msg_id ? ..

Por le të vazhdojmë me mesazhet e shërbimit. Klienti mund të mendojë se serveri po mendon për një kohë të gjatë dhe të bëjë këtë kërkesë të mrekullueshme:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Ekzistojnë tre përgjigje të mundshme për këtë pyetje, që përsëri kryqëzohen me mekanizmin e konfirmimit; përpjekja për të kuptuar se cilat duhet të jenë ato (dhe cila është lista e përgjithshme e llojeve që nuk kërkojnë konfirmim) i lihet lexuesit si detyrë shtëpie (shënim: informacioni në kodi burimor i Telegram Desktop nuk është i plotë).

Varësia nga droga: statuset e mesazheve

Në përgjithësi, shumë vende në TL, MTProto dhe Telegram në përgjithësi lënë një ndjenjë kokëfortësie, por nga mirësjellja, takti dhe të tjera. aftësi të buta Ne heshtim me mirësjellje për këtë dhe censuruam turpin në dialog. Megjithatë, ky vendОpjesa më e madhe e faqes është rreth mesazhe rreth mesazheve Është tronditëse edhe për mua, që kam punuar me protokollet e rrjetit për një kohë të gjatë dhe kam parë biçikleta me shkallë të ndryshme shtrembërimi.

Fillon në mënyrë të padëmshme, me konfirmime. Më pas na tregojnë për

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;

Epo, të gjithë ata që fillojnë të punojnë me MTProto do të duhet të merren me to; në ciklin "korrigjuar - ripërpiluar - lëshuar", marrja e gabimeve në numra ose kripa që ka arritur të shkojë keq gjatë redaktimit është një gjë e zakonshme. Sidoqoftë, këtu ka dy pika:

  1. Kjo do të thotë që mesazhi origjinal ka humbur. Duhet të krijojmë disa radhë, do ta shohim më vonë.
  2. Cilat janë këto numra të çuditshëm gabimesh? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... ku janë numrat e tjerë, Tommy?

Në dokumentacion thuhet:

Synimi është që vlerat e kodit të gabimit të grupohen (kodi_gabim >> 4): për shembull, kodet 0x40 — 0x4f korrespondojnë me gabimet në dekompozimin e kontejnerit.

por, së pari, një zhvendosje në drejtimin tjetër, dhe së dyti, nuk ka rëndësi, ku janë kodet e tjera? Në kokën e autorit?.. Megjithatë, këto janë vogëlsi.

Varësia fillon në mesazhet në lidhje me statuset e mesazheve dhe kopjet e mesazheve:

  • Kërkesë për informacion mbi statusin e mesazhit
    Nëse njëra palë nuk ka marrë informacion mbi statusin e mesazheve të saj dalëse për një kohë, ajo mund ta kërkojë në mënyrë eksplicite nga pala tjetër:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Mesazh informues në lidhje me statusin e mesazheve
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Ketu, info është një varg që përmban saktësisht një bajt të statusit të mesazhit për çdo mesazh nga lista hyrëse e msg_ids:

    • 1 = asgjë nuk dihet për mesazhin (msg_id shumë i ulët, pala tjetër mund ta ketë harruar)
    • 2 = mesazhi nuk u mor (msg_id bie brenda gamës së identifikuesve të ruajtur; megjithatë, pala tjetër sigurisht që nuk ka marrë një mesazh të tillë)
    • 3 = mesazhi nuk është marrë (msg_id shumë i lartë; megjithatë, pala tjetër sigurisht që nuk e ka marrë ende)
    • 4 = mesazhi i marrë (vini re se kjo përgjigje është në të njëjtën kohë një konfirmim marrjeje)
    • +8 = mesazhi tashmë është pranuar
    • +16 = mesazh që nuk kërkon njohje
    • +32 = Kërkesa RPC që përmbahet në mesazhin që përpunohet ose përpunimi tashmë ka përfunduar
    • +64 = përgjigje e lidhur me përmbajtjen ndaj mesazhit të krijuar tashmë
    • +128 = pala tjetër e di me siguri se mesazhi është marrë tashmë
      Kjo përgjigje nuk kërkon një mirënjohje. Është një njohje e msgs_state_req përkatës, në vetvete.
      Vini re se nëse papritmas rezulton se pala tjetër nuk ka një mesazh që duket sikur i është dërguar, mesazhi thjesht mund të ridërgohet. Edhe nëse pala tjetër duhet të marrë dy kopje të mesazhit në të njëjtën kohë, kopjimi do të shpërfillet. (Nëse ka kaluar shumë kohë dhe msg_id origjinale nuk është më e vlefshme, mesazhi duhet të mbështillet në msg_copy).
  • Komunikimi vullnetar i statusit të mesazheve
    Secila palë mund të informojë vullnetarisht palën tjetër për statusin e mesazheve të transmetuara nga pala tjetër.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Komunikimi vullnetar i zgjeruar i statusit të një mesazhi
    ...
    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;
  • Kërkesë e qartë për të ridërguar mesazhe
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Pala në distancë përgjigjet menjëherë duke ri-dërguar mesazhet e kërkuara […]
  • Kërkesë e qartë për të ridërguar përgjigjet
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Pala në distancë përgjigjet menjëherë duke e ridërguar Përgjigjet në mesazhet e kërkuara […]
  • Kopjet e mesazheve
    Në disa situata, një mesazh i vjetër me një msg_id që nuk është më i vlefshëm duhet të ridërgohet. Më pas, mbështillet në një enë kopje:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Pasi të merret, mesazhi përpunohet sikur mbështjellësi të mos ishte aty. Megjithatë, nëse dihet me siguri se mesazhi orig_message.msg_id është marrë, atëherë mesazhi i ri nuk përpunohet (ndërsa në të njëjtën kohë, ai dhe orig_message.msg_id pranohen). Vlera e orig_message.msg_id duhet të jetë më e ulët se msg_id e kontejnerit.

Le të heshtim edhe për çfarë msgs_state_info përsëri veshët e TL-së së papërfunduar po dalin jashtë (na duhej një vektor bajtash, dhe në dy bitet e poshtme kishte një enum, dhe në dy bitet më të larta kishte flamuj). Çështja është e ndryshme. A e kupton dikush pse e gjithë kjo është në praktikë? në një klient të vërtetë e nevojshme?.. Me vështirësi, por mund të imagjinohet ndonjë përfitim nëse një person është i angazhuar në korrigjimin e gabimeve, dhe në një mënyrë interaktive - pyesni serverin se çfarë dhe si. Por këtu përshkruhen kërkesat Udhëtim.

Nga kjo rrjedh se secila palë jo vetëm që duhet të kodojë dhe të dërgojë mesazhe, por edhe të ruajë të dhëna për veten e tyre, për përgjigjet ndaj tyre, për një kohë të panjohur. Dokumentacioni nuk përshkruan as kohën dhe as zbatueshmërinë praktike të këtyre veçorive. asnjë mënyrë. Ajo që është më e mahnitshme është se ato përdoren në të vërtetë në kodin e klientëve zyrtarë! Me sa duket atyre u është thënë diçka që nuk është përfshirë në dokumentacionin publik. Kuptoni nga kodi pse, nuk është më aq e thjeshtë sa në rastin e TL - nuk është një pjesë (relativisht) e izoluar logjikisht, por një pjesë e lidhur me arkitekturën e aplikacionit, d.m.th. do të kërkojë shumë më shumë kohë për të kuptuar kodin e aplikacionit.

Ping dhe kohëzgjatje. Radhët.

Nga gjithçka, nëse kujtojmë supozimet për arkitekturën e serverit (shpërndarja e kërkesave nëpër backend), pason një gjë mjaft e trishtueshme - pavarësisht nga të gjitha garancitë e dorëzimit në TCP (ose të dhënat dorëzohen, ose do të informoheni për prishjen, por të dhënat do të dorëzohen derisa të shfaqet problemi), që konfirmimet në vetë MTProto - asnjë garanci. Serveri lehtë mund të humbasë ose të hedhë mesazhin tuaj dhe asgjë nuk mund të bëhet për këtë, thjesht përdorni lloje të ndryshme patericash.

Dhe para së gjithash - radhët e mesazheve. Epo, me një gjë gjithçka ishte e qartë që në fillim - një mesazh i pakonfirmuar duhet të ruhet dhe të dërgohet përsëri. Dhe pas çfarë kohe? Dhe shakaja e njeh atë. Ndoshta ato mesazhe shërbimi të varur e zgjidhin disi këtë problem me paterica, të themi, në Telegram Desktop ka rreth 4 radhë që korrespondojnë me to (ndoshta më shumë, siç u përmend tashmë, për këtë ju duhet të gërmoni më seriozisht në kodin dhe arkitekturën e tij; në të njëjtën kohë kohë, ne e dimë se nuk mund të merret si mostër, një numër i caktuar i llojeve nga skema MTProto nuk janë përdorur në të).

Pse po ndodh kjo? Ndoshta, programuesit e serverëve nuk ishin në gjendje të siguronin besueshmëri brenda grupit, apo edhe buffering në balancuesin e përparmë, dhe e transferuan këtë problem te klienti. Nga dëshpërimi, Vasily u përpoq të zbatonte një opsion alternativ, me vetëm dy radhë, duke përdorur algoritme nga TCP - duke matur RTT në server dhe duke rregulluar madhësinë e "dritares" (në mesazhe) në varësi të numrit të kërkesave të pakonfirmuara. Kjo do të thotë, një heuristikë e tillë e përafërt për vlerësimin e ngarkesës së serverit është se sa nga kërkesat tona mund të përtypë në të njëjtën kohë dhe të mos humbasë.

Epo, kjo është, e kuptoni, apo jo? Nëse duhet të zbatoni TCP përsëri në krye të një protokolli që funksionon mbi TCP, kjo tregon një protokoll të dizajnuar shumë keq.

Oh po, pse keni nevojë për më shumë se një radhë dhe çfarë do të thotë kjo për një person që punon me një API të nivelit të lartë gjithsesi? Shiko, ju bëni një kërkesë, e serializoni, por shpesh nuk mund ta dërgoni menjëherë. Pse? Sepse përgjigja do të jetë msg_id, e cila është e përkohshmeаUnë jam një etiketë, caktimi i të cilit është më mirë të shtyhet sa më vonë të jetë e mundur - në rast se serveri e refuzon atë për shkak të një mospërputhjeje kohore midis nesh dhe tij (natyrisht, ne mund të bëjmë një patericë që e zhvendos kohën tonë nga e tashmja në server duke shtuar një delta të llogaritur nga përgjigjet e serverit - klientët zyrtarë e bëjnë këtë, por është e papërpunuar dhe e pasaktë për shkak të bufferimit). Prandaj, kur bëni një kërkesë me një thirrje funksioni lokal nga biblioteka, mesazhi kalon nëpër fazat e mëposhtme:

  1. Shtrihet në një radhë dhe pret enkriptimin.
  2. I emëruar msg_id dhe mesazhi shkoi në një radhë tjetër - përcjellje e mundshme; dërgoni në prizë.
  3. a) Serveri iu përgjigj MsgsAck - mesazhi u dorëzua, ne e fshijmë atë nga "radha tjetër".
    b) Ose anasjelltas, ai nuk i pëlqeu diçka, ai u përgjigj keq - ridërgo nga "një radhë tjetër"
    c) Asgjë nuk dihet, mesazhi duhet të dërgohet nga një radhë tjetër - por nuk dihet saktësisht se kur.
  4. Serveri më në fund u përgjigj RpcResult - përgjigja aktuale (ose gabimi) - jo vetëm e dorëzuar, por edhe e përpunuar.

Ndoshta, përdorimi i kontejnerëve mund të zgjidhë pjesërisht problemin. Kjo është kur një grup mesazhesh paketohen në një dhe serveri u përgjigj me një konfirmim për të gjitha menjëherë, në një msg_id. Por ai gjithashtu do ta refuzojë këtë paketë, nëse diçka nuk shkon mirë, në tërësi.

Dhe në këtë pikë hyjnë në lojë konsideratat jo-teknike. Nga përvoja, ne kemi parë shumë paterica, dhe përveç kësaj, tani do të shohim më shumë shembuj të këshillave dhe arkitekturës së keqe - në kushte të tilla, a ia vlen të besohet dhe të merren vendime të tilla? Pyetja është retorike (sigurisht jo).

Për çfarë po flasim? Nëse në temën e "mesazheve të drogës për mesazhet" mund të spekuloni akoma me kundërshtime si "je budalla, nuk e kuptove planin tonë të shkëlqyer!" (kështu që fillimisht shkruani dokumentacionin, siç duhet njerëzit normalë, me arsyetimin dhe shembujt e shkëmbimit të paketave, pastaj do të flasim), pastaj oraret/përfundimet janë një pyetje thjesht praktike dhe specifike, gjithçka këtu dihet prej kohësh. Çfarë na thotë dokumentacioni për ndërprerjet kohore?

Një server zakonisht pranon marrjen e një mesazhi nga një klient (normalisht, një pyetje RPC) duke përdorur një përgjigje RPC. Nëse një përgjigje vjen për një kohë të gjatë, një server mund të dërgojë fillimisht një konfirmim të marrjes, dhe disi më vonë, vetë përgjigjen RPC.

Një klient normalisht pranon marrjen e një mesazhi nga një server (zakonisht, një përgjigje RPC) duke shtuar një konfirmim në pyetjen tjetër RPC nëse nuk transmetohet shumë vonë (nëse gjenerohet, të themi, 60-120 sekonda pas marrjes të një mesazhi nga serveri). Megjithatë, nëse për një periudhë të gjatë kohore nuk ka arsye për të dërguar mesazhe në server ose nëse ka një numër të madh mesazhesh të papranuara nga serveri (të themi, mbi 16), klienti transmeton një konfirmim të pavarur.

... Unë përkthej: ne vetë nuk e dimë sa dhe si na duhet, prandaj le të supozojmë se le të jetë kështu.

Dhe në lidhje me ping:

Mesazhe Ping (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Një përgjigje zakonisht kthehet në të njëjtën lidhje:

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

Këto mesazhe nuk kërkojnë mirënjohje. Një pong transmetohet vetëm në përgjigje të një ping ndërsa një ping mund të inicohet nga secila palë.

Mbyllja e shtyrë e lidhjes + PING

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

Punon si ping. Përveç kësaj, pasi të merret kjo, serveri nis një kohëmatës i cili do të mbyllë lidhjen aktuale disconnect_delay sekonda më vonë, përveç nëse merr një mesazh të ri të të njëjtit lloj që rivendos automatikisht të gjithë kohëmatësit e mëparshëm. Nëse klienti i dërgon këto ping një herë në 60 sekonda, për shembull, ai mund të vendosë disconnect_delay të barabartë me 75 sekonda.

A je i cmendur?! Në 60 sekonda, treni do të hyjë në stacion, do të zbresë dhe do të marrë pasagjerët dhe do të humbasë përsëri kontaktin në tunel. Në 120 sekonda, ndërsa e dëgjoni, do të arrijë në një tjetër dhe lidhja ka shumë të ngjarë të prishet. Epo, është e qartë se nga vijnë këmbët - "Kam dëgjuar një zile, por nuk e di se ku është", ekziston algoritmi i Nagl dhe opsioni TCP_NODELAY, i destinuar për punë interaktive. Por, më falni, mbajeni vlerën e tij të paracaktuar - 200 Millisekonda Nëse vërtet dëshironi të përshkruani diçka të ngjashme dhe të kurseni në disa paketa të mundshme, atëherë shtyjeni atë për 5 sekonda, ose çfarëdo që të jetë skadimi i mesazhit "Përdoruesi po shkruan...". Por jo më shumë.

Dhe së fundi, ping. Kjo do të thotë, kontrollimi i gjallërisë së lidhjes TCP. Është qesharake, por rreth 10 vjet më parë shkrova një tekst kritik në lidhje me lajmëtarin e konviktit të fakultetit tonë - autorët atje gjithashtu kërkuan serverin nga klienti, dhe jo anasjelltas. Por studentët e vitit të tretë janë një gjë, dhe një zyrë ndërkombëtare është një gjë tjetër, apo jo?..

Së pari, një program i vogël arsimor. Një lidhje TCP, në mungesë të shkëmbimit të paketave, mund të jetojë për javë të tëra. Kjo është edhe e mirë edhe e keqe, në varësi të qëllimit. Shtë mirë nëse keni pasur një lidhje SSH të hapur me serverin, jeni ngritur nga kompjuteri, keni rindezur ruterin, jeni kthyer në vendin tuaj - sesioni përmes këtij serveri nuk është grisur (nuk keni shkruar asgjë, nuk ka pasur pako) , është i përshtatshëm. Është keq nëse ka mijëra klientë në server, secili duke marrë burime (përshëndetje, Postgres!), dhe hosti i klientit mund të jetë rindezur shumë kohë më parë - por ne nuk do të dimë për këtë.

Sistemet e bisedave/IM bien në rastin e dytë për një arsye shtesë - statuset në internet. Nëse përdoruesi "ra", ju duhet të informoni bashkëbiseduesit e tij për këtë. Përndryshe, do të përfundoni me një gabim që kanë bërë krijuesit e Jabber (dhe e kanë korrigjuar për 20 vjet) - përdoruesi është shkëputur, por ata vazhdojnë t'i shkruajnë mesazhe, duke besuar se ai është në internet (të cilat gjithashtu humbën plotësisht në këto disa minuta para se të zbulohej shkëputja). Jo, opsioni TCP_KEEPALIVE, të cilin shumë njerëz që nuk e kuptojnë se si funksionojnë kohëmatësit TCP e hedhin në mënyrë të rastësishme (duke vendosur vlera të egra si dhjetëra sekonda), nuk do të ndihmojë këtu - duhet të siguroheni që jo vetëm kerneli i OS i makinës së përdoruesit është i gjallë, por gjithashtu funksionon normalisht, në gjendje të përgjigjet, dhe vetë aplikacioni (a mendoni se nuk mund të ngrijë? Telegram Desktop në Ubuntu 18.04 ngriu për mua më shumë se një herë).

Kjo është arsyeja pse ju duhet të bëni ping server klienti, dhe jo anasjelltas - nëse klienti e bën këtë, nëse lidhja prishet, ping nuk do të dorëzohet, qëllimi nuk do të arrihet.

Çfarë shohim në Telegram? Është pikërisht e kundërta! Epo, kjo është. Formalisht, natyrisht, të dyja palët mund të bëjnë ping me njëra-tjetrën. Në praktikë, klientët përdorin një paterica ping_delay_disconnect, i cili vendos kohëmatësin në server. Epo, më falni, nuk i takon klientit të vendosë se sa kohë dëshiron të jetojë atje pa ping. Serveri, bazuar në ngarkesën e tij, e di më mirë. Por, sigurisht, nëse nuk ju shqetësojnë burimet, atëherë do të jeni vetë Pinoku i keq dhe një patericë do ta bëjë...

Si duhet të ishte projektuar?

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

Pse doli të ishte kaq e ndërlikuar dhe si mund të përpiqen të kundërshtojnë arkitektët e Telegram? Fakti që ata u përpoqën të bënin një seancë që i mbijeton lidhjes TCP prishet, d.m.th., atë që nuk u dorëzua tani, ne do ta dorëzojmë më vonë. Ata ndoshta u përpoqën gjithashtu të bënin një transport UDP, por hasën në vështirësi dhe e braktisën atë (kjo është arsyeja pse dokumentacioni është bosh - nuk kishte asgjë për t'u mburrur). Por për shkak të një keqkuptimi se si funksionojnë rrjetet në përgjithësi dhe TCP në veçanti, ku mund të mbështeteni në të dhe ku duhet ta bëni vetë (dhe si), dhe një përpjekje për ta kombinuar këtë me kriptografinë "dy zogj me një gur, Rezultati ishte një kufomë e tillë.

Si ishte e nevojshme? Nisur nga fakti se msg_id është një vulë kohore e nevojshme nga pikëpamja kriptografike për të parandaluar sulmet e përsëritura, është gabim t'i bashkëngjitni një funksion identifikues unik. Prandaj, pa ndryshuar rrënjësisht arkitekturën aktuale (kur gjenerohet rryma e Përditësimeve, kjo është një temë e nivelit të lartë API për një pjesë tjetër të kësaj serie postimesh), duhet:

  1. Serveri që mban lidhjen TCP me klientin merr përgjegjësinë - nëse ka lexuar nga priza, ju lutemi pranoni, përpunoni ose ktheni një gabim, pa humbje. Atëherë konfirmimi nuk është një vektor i ID-ve, por thjesht "seq_no i marrë i fundit" - vetëm një numër, si në TCP (dy numra - seku juaj dhe ai i konfirmuar). Ne jemi gjithmonë brenda seancës, apo jo?
  2. Vula kohore për të parandaluar sulmet përsëritëse bëhet një fushë më vete, a la nonce. Është kontrolluar, por nuk ndikon në asgjë tjetër. Mjaft dhe uint32 - nëse kripa jonë ndryshon të paktën çdo gjysmë dite, ne mund të ndajmë 16 bit në pjesët e rendit të ulët të një pjese të plotë të kohës aktuale, pjesa tjetër - në një pjesë të pjesshme të sekondës (si tani).
  3. U hoq msg_id fare - nga pikëpamja e dallimit të kërkesave në backend, ekziston, së pari, id-ja e klientit, dhe së dyti, id-ja e sesionit, lidh ato. Prandaj, vetëm një gjë mjafton si identifikues i kërkesës seq_no.

Ky nuk është gjithashtu opsioni më i suksesshëm; një rastësi e plotë mund të shërbejë si një identifikues - kjo është bërë tashmë në API të nivelit të lartë kur dërgoni një mesazh, meqë ra fjala. Do të ishte më mirë të ribëhej plotësisht arkitektura nga relative në absolute, por kjo është një temë për një pjesë tjetër, jo këtë postim.

API?

Ta-daam! Pra, duke luftuar në një rrugë plot dhimbje dhe paterica, më në fund ne ishim në gjendje të dërgonim çdo kërkesë në server dhe të merrnim çdo përgjigje për to, si dhe të merrnim përditësime nga serveri (jo në përgjigje të një kërkese, por ai vetë na dërgon, si PUSH, nëse dikush është më e qartë kështu).

Kujdes, tani do të ketë shembullin e vetëm në Perl në artikull! (për ata që nuk e njohin sintaksën, argumenti i parë i Bless është struktura e të dhënave të objektit, i dyti është klasa e tij):

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

Po, jo një spoiler me qëllim - nëse nuk e keni lexuar ende, vazhdoni dhe bëjeni!

Oh, vai~~... si duket kjo? Diçka shumë e njohur... ndoshta kjo është struktura e të dhënave të një API tipike Web në JSON, përveç që klasat janë bashkangjitur edhe me objekte?..

Pra, kështu rezulton... Për çfarë bëhet fjalë, shokë?.. Kaq shumë përpjekje - dhe ne u ndalëm për të pushuar atje ku programuesit e ueb-it sapo fillon?..A nuk do të ishte më e thjeshtë vetëm JSON mbi HTTPS?! Çfarë morëm në këmbim? A ia vlente përpjekja?

Le të vlerësojmë se çfarë na dha TL+MTProto dhe cilat alternativa janë të mundshme. Epo, HTTP, i cili fokusohet në modelin e përgjigjes së kërkesës, është një përshtatje e keqe, por të paktën diçka në krye të TLS?

Serializimi kompakt. Duke parë këtë strukturë të dhënash, të ngjashme me JSON, më kujtohet se ka versione binare të saj. Le ta shënojmë MsgPack si të pamjaftueshëm të zgjeruar, por ekziston, për shembull, CBOR - meqë ra fjala, një standard i përshkruar në RFC7049. Shquhet për faktin se përcakton etiketa, si mekanizëm zgjerimi, dhe ndër tashmë të standardizuar në dispozicion:

  • 25 + 256 - zëvendësimi i linjave të përsëritura me një referencë në numrin e linjës, një metodë kaq e lirë e kompresimit
  • 26 - objekt i serializuar Perl me emrin e klasës dhe argumentet e konstruktorit
  • 27 - objekt i serializuar i pavarur nga gjuha me emrin e tipit dhe argumentet e konstruktorit

Epo, unë u përpoqa të serializoja të njëjtat të dhëna në TL dhe në CBOR me paketimin e vargjeve dhe objekteve të aktivizuara. Rezultati filloi të ndryshojë në favor të CBOR diku nga një megabajt:

cborlen=1039673 tl_len=1095092

Pra, вывод: Ka formate thelbësisht më të thjeshta që nuk i nënshtrohen problemit të dështimit të sinkronizimit ose identifikuesit të panjohur, me efikasitet të krahasueshëm.

Vendosja e shpejtë e lidhjes. Kjo do të thotë zero RTT pas rilidhjes (kur çelësi tashmë është gjeneruar një herë) - i zbatueshëm që nga mesazhi i parë MTProto, por me disa rezerva - goditi të njëjtën kripë, seanca nuk është e kalbur, etj. Çfarë na ofron TLS në vend të kësaj? Citim në temë:

Kur përdorni PFS në TLS, biletat e sesionit TLS (RFC5077) për të rifilluar një sesion të koduar pa ri-negociuar çelësat dhe pa ruajtur informacionin kryesor në server. Kur hapni lidhjen e parë dhe krijoni çelësat, serveri kodon gjendjen e lidhjes dhe ia transmeton atë klientit (në formën e një bilete sesioni). Prandaj, kur lidhja rifillon, klienti dërgon një biletë sesioni, duke përfshirë çelësin e sesionit, përsëri në server. Vetë bileta është e koduar me një çelës të përkohshëm (çelësi i biletës së sesionit), i cili ruhet në server dhe duhet të shpërndahet midis të gjithë serverëve frontend që përpunojnë SSL në zgjidhje të grupuara.[10]. Kështu, futja e një bilete sesioni mund të shkelë PFS nëse çelësat e përkohshëm të serverit rrezikohen, për shembull, kur ato ruhen për një kohë të gjatë (OpenSSL, nginx, Apache i ruajnë ato si parazgjedhje për të gjithë kohëzgjatjen e programit; faqet e njohura përdorin çelësi për disa orë, deri në ditë).

Këtu RTT nuk është zero, ju duhet të shkëmbeni të paktën ClientHello dhe ServerHello, pas së cilës klienti mund të dërgojë të dhëna së bashku me Finished. Por këtu duhet të kujtojmë se ne nuk kemi Web, me një mori lidhjesh të sapohapura, por një lajmëtar, lidhja e të cilit është shpesh një dhe pak a shumë jetëgjatë, kërkesa relativisht të shkurtra për faqet e internetit - gjithçka është e shumëfishuar. nga brenda. Kjo do të thotë, është mjaft e pranueshme nëse nuk hasim në një seksion vërtet të keq të metrosë.

Keni harruar diçka tjetër? Shkruani në komente.

Vazhdon!

Në pjesën e dytë të kësaj serie postimesh do të shqyrtojmë jo çështjet teknike, por ato organizative - qasjet, ideologjinë, ndërfaqen, qëndrimin ndaj përdoruesve, etj. Bazuar, megjithatë, në informacionin teknik që u prezantua këtu.

Pjesa e tretë do të vazhdojë të analizojë komponentin teknik / përvojën e zhvillimit. Në veçanti do të mësoni:

  • vazhdimi i pandemoniumit me shumëllojshmërinë e llojeve të TL
  • gjëra të panjohura rreth kanaleve dhe supergrupeve
  • pse dialogët janë më keq se lista
  • rreth adresimit të mesazhit absolut kundrejt atij relativ
  • cili është ndryshimi midis fotos dhe imazhit
  • как эмодзи мешают размечать текст курсивом

dhe paterica të tjera! Qëndroni të sintonizuar!

Burimi: www.habr.com

Shto një koment