„Telegram“ protokolo ir organizacinių požiūrių kritika. 1 dalis, techninė: patirtis rašant klientą nuo nulio - TL, MT

Pastaruoju metu Habré vis dažniau pasirodo įrašai apie tai, kokia gera yra „Telegram“, kokie genialūs ir patyrę broliai Durovai kuria tinklo sistemas ir pan. Tuo pačiu metu labai mažai žmonių tikrai pasinėrė į techninį įrenginį – daugiausia jie naudoja gana paprastą (ir gana skirtingą nuo MTProto) Bot API, pagrįstą JSON, ir dažniausiai tiesiog sutinka. apie tikėjimą visi pagyrimai ir PR, kurie sukasi apie pasiuntinį. Beveik prieš pusantrų metų mano kolega iš Eshelon nevyriausybinės organizacijos Vasilijus (deja, jo paskyra Habré buvo ištrinta kartu su juodraščiu) pradėjo rašyti savo Telegram klientą nuo nulio Perle, o vėliau prisijungė ir šių eilučių autorius. Kodėl Perlas, kai kurie iš karto paklaus? Nes tokie projektai jau egzistuoja kitomis kalbomis.Tiesą sakant, tai ne esmė, gali būti bet kuri kita kalba, kurioje nėra paruošta biblioteka, ir atitinkamai autorius turi eiti iki galo nuo nulio. Be to, kriptografija yra pasitikėjimo dalykas, tačiau patikrinkite. Naudodami gaminį, skirtą saugumui, negalite tiesiog pasikliauti paruošta gamintojo biblioteka ir aklai ja pasitikėti (tačiau tai antrosios dalies tema). Šiuo metu biblioteka gana gerai veikia „vidutiniu“ lygiu (leidžia pateikti bet kokias API užklausas).

Tačiau šioje pranešimų serijoje nebus daug kriptografijos ar matematikos. Bet bus daug kitų techninių smulkmenų ir architektūrinių ramentų (pravers ir tiems, kurie nerašys nuo nulio, o naudosis biblioteka bet kokia kalba). Taigi, pagrindinis tikslas buvo pabandyti įgyvendinti klientą nuo nulio pagal oficialius dokumentus. Tai yra, tarkime, kad oficialių klientų šaltinio kodas yra uždarytas (vėlgi, antroje dalyje mes išsamiau aptarsime temą, kad tai tiesa tai atsitinka taip), bet, kaip ir senais laikais, pavyzdžiui, yra toks standartas kaip RFC - ar galima parašyti klientą vien pagal specifikaciją, „nežiūrint“ į šaltinio kodą, nesvarbu, ar jis oficialus („Telegram Desktop“, mobilusis) arba neoficialus Telethon?

Turinys:

Dokumentacija... ji egzistuoja, tiesa? Ar tai tiesa?..

Užrašų fragmentai šiam straipsniui pradėti rinkti praėjusią vasarą. Visą šį laiką oficialioje svetainėje https://core.telegram.org Dokumentacija buvo 23 sluoksnio, t.y. įstrigo kažkur 2014 m. (pamenate, tada net nebuvo kanalų?). Žinoma, teoriškai tai turėjo leisti mums 2014 metais įdiegti tuo metu funkcionalų klientą. Tačiau net ir tokioje būsenoje dokumentacija, pirma, buvo neišsami, antra, vietomis prieštaraudavo pati sau. Prieš kiek daugiau nei mėnesį, 2019 m. rugsėjo mėn atsitiktinai Buvo nustatyta, kad svetainėje buvo atnaujinta visiškai naujausio 105 sluoksnio dokumentacija su pastaba, kad dabar viską reikia perskaityti dar kartą. Iš tiesų, daugelis straipsnių buvo pataisyti, tačiau daugelis liko nepakitę. Todėl skaitydami toliau pateiktą kritiką dėl dokumentacijos, turėtumėte nepamiršti, kad kai kurie iš šių dalykų nebėra aktualūs, bet kai kurie vis dar yra gana. Juk 5 metai šiuolaikiniame pasaulyje yra ne šiaip ilgas laikas, bet labai daug. Nuo tų laikų (ypač jei neatsižvelgiate į nuo to laiko išmestas ir atgaivintas geochat svetaines) API metodų skaičius schemoje išaugo nuo šimto iki daugiau nei dviejų šimtų penkiasdešimties!

Nuo ko pradėti jaunam autoriui?

Nesvarbu, ar rašote nuo nulio, ar naudojate, pavyzdžiui, paruoštas bibliotekas Telethon, skirtas Python arba Madeline PHP, bet kuriuo atveju pirmiausia reikės užregistruokite savo prašymą - gauti parametrus api_id и api_hash (tie, kurie dirbo su VKontakte API, iš karto supranta), pagal kurį serveris identifikuos programą. Tai turi tai padaryti dėl teisinių priežasčių, tačiau apie tai, kodėl bibliotekos autoriai negali to paskelbti, plačiau pakalbėsime antroje dalyje. Galite būti patenkinti testo reikšmėmis, nors jos yra labai ribotos – faktas yra tas, kad dabar galite užsiregistruoti tik vienas programa, todėl neskubėkite į ją stačia galva.

Dabar techniniu požiūriu turėtume būti suinteresuoti, kad po registracijos gautume pranešimus iš „Telegram“ apie dokumentacijos, protokolo ir kt. atnaujinimus. Tai yra, galima daryti prielaidą, kad svetainė su dokais buvo tiesiog apleista ir toliau dirbo specialiai su tais, kurie pradėjo kurti klientus, nes tai lengviau. Bet ne, nieko panašaus nepastebėjo, jokios informacijos neatėjo.

Ir jei rašote nuo nulio, tada naudoti gautus parametrus iš tikrųjų dar toli. Nors https://core.telegram.org/ ir apie juos kalbama skiltyje Darbo pradžia, iš tikrųjų pirmiausia turėsite įdiegti MTProto protokolas - bet jei tikėtum išdėstymas pagal OSI modelį puslapio pabaigoje bendram protokolo aprašymui, tada tai visiškai veltui.

Tiesą sakant, tiek prieš, tiek po MTProto, keliuose lygiuose vienu metu (kaip sako užsienio tinklų specialistai, dirbantys OS branduolyje, sluoksnio pažeidimas) užklius didelė, skaudi ir baisi tema...

Dvejetainis serializavimas: TL (tipo kalba) ir jos schema, sluoksniai ir daugelis kitų baisių žodžių

Ši tema iš tikrųjų yra „Telegram“ problemų raktas. Ir baisių žodžių bus daug, jei pabandysite į tai įsigilinti.

Taigi, čia yra diagrama. Jei šis žodis ateina į galvą, pasakykite: JSON schema, teisingai pagalvojai. Tikslas tas pats: tam tikra kalba apibūdinti galimą perduodamų duomenų rinkinį. Čia panašumai baigiasi. Jei iš puslapio MTProto protokolas, arba iš oficialaus kliento šaltinio medžio, pabandysime atidaryti kokią nors schemą, pamatysime kažką panašaus į:

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;

Pirmą kartą tai pamatęs žmogus intuityviai atpažins tik dalį to, kas parašyta – na, tai matyt dariniai (nors kur pavadinimas, kairėje ar dešinėje?), juose yra laukai, po kurio po dvitaškio seka tipas... tikriausiai. Čia kampiniuose skliaustuose tikriausiai yra šablonai, tokie kaip C++ (tiesą sakant, ne visai). O ką reiškia visi kiti simboliai, klaustukai, šauktukai, procentai, maišos ženklai (ir akivaizdu, kad skirtingose ​​vietose jie reiškia skirtingus dalykus), kartais esami, o kartais ne, šešioliktainiai skaičiai – ir svarbiausia, kaip iš to gauti reguliariai (kurio serveris neatmes) baitų srautas? Turėsite perskaityti dokumentus (taip, netoliese yra nuorodų į schemą JSON versijoje, bet dėl ​​to ji nėra aiškesnė).

Atidarykite puslapį Dvejetainių duomenų serializavimas ir pasinerkite į magišką grybų pasaulį ir diskrečią matematiką, kažką panašaus į matan 4-ame kurse. Abėcėlė, tipas, reikšmė, kombinatorius, funkcinis kombinatorius, normalioji forma, sudėtinis tipas, polimorfinis tipas... ir visa tai tik pirmas puslapis! Kitas laukia jūsų TL kalba, kuriame, nors jame jau yra nereikšmingo prašymo ir atsakymo pavyzdys, į tipiškesnius atvejus atsakymas visai nepateikiamas, o tai reiškia, kad teks braidyti per matematikos atpasakojimą, išverstą iš rusų į anglų kalbą dar aštuoniose įterptose. puslapių!

Skaitytojai, susipažinę su funkcinėmis kalbomis ir automatinio tipo išvadomis, žinoma, matys aprašymo kalbą šia kalba, net ir iš pavyzdžio, kaip daug pažįstamesnę ir gali pasakyti, kad tai iš esmės nėra blogai. Prieštaravimai tam yra šie:

  • taip, tikslas skamba gerai, bet deja, ji nepasiekta
  • Išsilavinimas Rusijos universitetuose skiriasi net tarp IT specialybių – ne visi išklausė atitinkamą kursą
  • Galiausiai, kaip matysime, praktiškai taip yra nereikalauja, nes naudojamas tik ribotas net aprašyto TL poaibis

Kaip sakyta Leonerdas kanale #perl FreeNode IRC tinkle, kuris bandė įdiegti vartus iš Telegram į Matrix (citatos vertimas iš atminties netikslus):

Atrodo, kad kažkas pirmą kartą buvo supažindintas su tipo teorija, susijaudino ir pradėjo bandyti su ja žaisti, nelabai rūpindamasis, ar to reikia praktiškai.

Pažiūrėkite patys, ar nekyla klausimų dėl plikų tipų (int, long ir pan.) kaip elementaraus dalyko – galiausiai jie turi būti įgyvendinami rankiniu būdu – pavyzdžiui, pabandykime iš jų išvesti vektorius. Tai iš tikrųjų masyvas, jei gautus daiktus vadinsite tinkamais vardais.

Bet prieš

Trumpas TL sintaksės poaibio aprašymas tiems, kurie neskaito oficialios dokumentacijos

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;

Apibrėžimas visada prasideda dizaineris, po kurio pasirinktinai (praktiškai – visada) per simbolį # turėtų CRC32 iš šio tipo normalizuotos aprašymo eilutės. Toliau pateikiamas laukų aprašymas; jei jie yra, tipas gali būti tuščias. Visa tai baigiasi lygybės ženklu, tipo, kuriam priklauso šis konstruktorius, ty, iš tikrųjų, potipis, pavadinimu. Lygybės ženklo dešinėje esantis vaikinas yra polimorfinis - tai yra, jį gali atitikti keli specifiniai tipai.

Jei apibrėžimas atsiranda po eilutės ---functions---, tada sintaksė išliks ta pati, bet reikšmė bus kitokia: konstruktorius taps RPC funkcijos pavadinimu, laukai – parametrais (na, tai yra išliks lygiai tokia pati duota struktūra, kaip aprašyta toliau , tai bus tiesiog priskirta reikšmė), o „polimorfinis tipas“ - grąžinamo rezultato tipas. Tiesa, jis vis tiek išliks polimorfinis – tik apibrėžtas skyriuje ---types---, tačiau šis konstruktorius „nebus svarstomas“. Iškviestų funkcijų tipų perkrovimas jų argumentais, t.y. Kažkodėl kelios funkcijos tuo pačiu pavadinimu, bet skirtingais parašais, kaip C++, TL nenumatytos.

Kam „konstruktorius“ ir „polimorfinis“, jei tai ne OOP? Na, tiesą sakant, kažkam bus lengviau apie tai galvoti OOP terminais - polimorfinis tipas kaip abstrakti klasė, o konstruktoriai yra tiesioginės jos palikuonių klasės, ir final daugelio kalbų terminologijoje. Tiesą sakant, žinoma, tik čia panašumo su tikrais perkrautais konstruktoriaus metodais OO programavimo kalbose. Kadangi čia tik duomenų struktūros, metodų nėra (nors funkcijų ir metodų aprašymas toliau gali sukelti painiavą, kad jie egzistuoja, bet čia jau kitas reikalas) – konstruktorių galite įsivaizduoti kaip vertę iš kurios yra statomas įveskite, kai skaitote baitų srautą.

Kaip tai atsitinka? Deserializatorius, kuris visada nuskaito 4 baitus, mato reikšmę 0xcrc32 - ir supranta, kas bus toliau field1 su tipu int, t.y. nuskaito lygiai 4 baitus, o šiame lauke yra viršutinis laukas su tipu PolymorType skaityti. Mato 0x2crc32 ir supranta, kad toliau yra du laukai, pirma long, o tai reiškia, kad skaitome 8 baitus. Ir vėl sudėtingas tipas, kuris deserializuojamas tokiu pačiu būdu. Pavyzdžiui, Type3 galėtų būti deklaruojami grandinėje, kai tik atitinkamai du konstruktoriai, tada jie turi susitikti arba 0x12abcd34, po kurio reikia perskaityti dar 4 baitus intArba 0x6789cdef, po kurio nieko nebus. Visa kita – reikia padaryti išimtį. Bet kokiu atveju, po to grįžtame prie 4 baitų skaitymo int maržos field_c в constructorTwo ir tuo baigiame skaityti mūsų PolymorType.

Galiausiai, jei jus sugaus 0xdeadcrcconstructorThree, tada viskas tampa sudėtingesnė. Mūsų pirmoji sritis yra bit_flags_of_what_really_present su tipu # - Tiesą sakant, tai tik tipo slapyvardis nat, reiškiantis „natūralų skaičių“. Tai yra, beje, beženklis int yra vienintelis atvejis, kai realiose grandinėse atsiranda beženklių skaičių. Taigi, toliau yra konstrukcija su klaustuku, o tai reiškia, kad šis laukas - jis bus laidoje tik tuo atveju, jei nurodytame lauke bus nustatytas atitinkamas bitas (apytiksliai kaip trijų dalių operatorius). Taigi, tarkime, kad šis bitas buvo nustatytas, o tai reiškia, kad toliau turime skaityti tokį lauką kaip Type, kuris mūsų pavyzdyje turi 2 konstruktorius. Vienas yra tuščias (susideda tik iš identifikatoriaus), kitame yra laukas ids su tipu ids:Vector<long>.

Galite pamanyti, kad ir šablonai, ir bendrieji yra privalumai arba Java. Bet ne. Beveik. Tai vienišas kampinių skliaustų naudojimo tikrosiose grandinėse atveju, ir jis naudojamas TIK vektoriui. Baitų sraute tai bus 4 CRC32 baitai pačiam Vector tipui, visada tas pats, tada 4 baitai - masyvo elementų skaičius, o tada patys šie elementai.

Pridėkite tai, kad serializavimas visada vyksta 4 baitų žodžiais, visi tipai yra jo kartotiniai – aprašomi ir įtaisytieji tipai bytes и string su rankiniu ilgio serializavimu ir šiuo lygiavimu 4 - gerai, atrodo, kad tai skamba normaliai ir netgi gana efektyviai? Nors teigiama, kad TL yra efektyvus dvejetainis serializavimas, po velnių, išplėtus beveik viską, net Būlio reikšmes ir vieno simbolio eilutes iki 4 baitų, ar JSON vis tiek bus daug storesnis? Žiūrėk, net ir nereikalingus laukus galima praleisti su bitų vėliavėlėmis, viskas gana gerai ir netgi praplečiama ateičiai, tai kodėl vėliau konstruktoriaus nepridėjus naujų pasirenkamų laukų?..

Bet ne, jei perskaitysite ne mano trumpą aprašymą, o visą dokumentaciją ir pagalvosite apie įgyvendinimą. Pirma, konstruktoriaus CRC32 apskaičiuojamas pagal normalizuotą schemos teksto aprašymo eilutę (pašalinkite papildomą tarpą ir pan.) – taigi, jei bus pridėtas naujas laukas, pasikeis tipo aprašymo eilutė, taigi ir jos CRC32 bei , vadinasi, serializavimas. O ką darytų senasis klientas, jei gautų lauką su naujomis vėliavėlėmis ir nežinotų, ką su jomis daryti toliau?..

Antra, prisiminkime CRC32, kuris čia naudojamas iš esmės kaip maišos funkcijos vienareikšmiškai nustatyti, koks tipas yra (iš)serializuojamas. Čia susiduriame su susidūrimų problema – ir ne, tikimybė yra ne viena iš 232, o daug didesnė. Kas prisiminė, kad CRC32 skirtas aptikti (ir ištaisyti) ryšio kanalo klaidas ir atitinkamai pagerinti šias savybes kitų nenaudai? Pavyzdžiui, jai nerūpi baitų pertvarkymas: jei apskaičiuosite CRC32 iš dviejų eilučių, antroje pirmuosius 4 baitus pakeisite kitais 4 baitais - bus tas pats. Kai mūsų įvestis yra teksto eilutės iš lotyniškos abėcėlės (ir šiek tiek skyrybos ženklų), o šie pavadinimai nėra ypač atsitiktiniai, tokio pertvarkymo tikimybė labai padidėja.

Beje, kas patikrino, kas ten buvo? tikrai CRC32? Vienas iš ankstyvųjų šaltinio kodų (dar prieš Waltmaną) turėjo maišos funkciją, kuri kiekvieną simbolį padaugino iš skaičiaus 239, taip mėgstama šių žmonių, ha ha!

Galiausiai, gerai, supratome, kad konstruktoriai su lauko tipu Vector<int> и Vector<PolymorType> turės kitokį CRC32. Ką apie našumą internete? Ir teoriniu požiūriu, ar tai tampa tipo dalimi? Tarkime, kad perduodame dešimties tūkstančių skaičių masyvą, gerai su Vector<int> viskas aišku, ilgis ir dar 40000 XNUMX baitų. O jeigu ši Vector<Type2>, kurį sudaro tik vienas laukas int ir jis yra tik tipas - ar mums reikia pakartoti 10000xabcdef0 34 4 kartų ir tada XNUMX baitus int, arba kalba sugeba ją NEPRIKLAUSOTI mums nuo konstruktoriaus fixedVec o vietoj 80000 40000 baitų vėl perkelti tik XNUMX XNUMX?

Tai visai ne tuščias teorinis klausimas – įsivaizduokite, kad gaunate grupės vartotojų sąrašą, kurių kiekvienas turi ID, vardą, pavardę – mobiliuoju ryšiu perduodamų duomenų kiekio skirtumas gali būti didelis. Mums reklamuojamas būtent „Telegram“ serializacijos efektyvumas.

Taigi…

Vektorius, kuris niekada nebuvo išleistas

Jei bandysite braidyti po kombinatorių aprašymo puslapius ir pan., pamatysite, kad vektorius (ir net matrica) formaliai bandoma išvesti kelių lapų eilutes. Bet galiausiai jie pamiršta, paskutinis žingsnis praleidžiamas ir tiesiog pateikiamas vektoriaus apibrėžimas, kuris dar nėra susietas su tipu. Kas nutiko? Kalbomis programavimas, ypač funkcines, gana būdinga struktūrą apibūdinti rekursyviai - kompiliatorius su savo atsainiu vertinimu viską supras ir padarys pats. Kalboje duomenų serializavimas reikalingas EFEKTYVUMAS: užtenka tiesiog apibūdinti sąrašas, t.y. dviejų elementų struktūra - pirmasis yra duomenų elementas, antrasis yra ta pati struktūra arba tuščia vieta uodegai (paketui (cons) Lisp). Bet to, aišku, reikės kiekvienas elementas išleidžia papildomus 4 baitus (CRC32 atveju TL), kad apibūdintų savo tipą. Masyvą taip pat galima lengvai apibūdinti fiksuoto dydžio, bet iš anksto nežinomo ilgio masyvo atveju nutraukiame.

Todėl, kadangi TL neleidžia išvesti vektoriaus, jį reikėjo pridėti prie šono. Galiausiai dokumentuose rašoma:

Serializuojant visada naudojamas tas pats konstruktorius „vektorius“ (const 0x1cb5c415 = crc32 („vector t:Type # [t ] = Vector t“), kuris nepriklauso nuo konkrečios t tipo kintamojo reikšmės.

Neprivalomo parametro t reikšmė serializuojant nedalyvauja, nes ji gaunama iš rezultato tipo (visada žinoma prieš serializavimą).

Pažiūrėk atidžiau: vector {t:Type} # [ t ] = Vector t - bet niekur Pats šis apibrėžimas nesako, kad pirmasis skaičius turi būti lygus vektoriaus ilgiui! Ir tai neatsiranda iš niekur. Tai duotybė, kurią reikia turėti omenyje ir įgyvendinti savo rankomis. Kitur dokumentuose net nuoširdžiai paminėta, kad tipas netikras:

Vektoriaus t polimorfinis pseudotipas yra „tipas“, kurio reikšmė yra bet kokio tipo t reikšmių seka, įdėta į dėžutę arba tuščia.

... bet nekreipia dėmesio į tai. Kai pavargęs braidyti per matematikos tempimą (galbūt net iš universiteto kurso), nusprendi pasiduoti ir iš tikrųjų pažiūri, kaip su tuo dirbti praktiškai, galvoje susidaro įspūdis, kad tai rimta. Matematikos esmė, ją aiškiai sugalvojo „Cool People“ (du matematikai – ACM nugalėtojas), o ne bet kas. Tikslas – pasipuikuoti – pasiektas.

Beje, apie skaičių. Leiskite jums tai priminti # tai sinonimas nat, natūralusis skaičius:

Yra tipo išraiškos (tipas-išs) ir skaitines išraiškas (nat-expr). Tačiau jie apibrėžiami taip pat.

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

bet gramatikoje jie aprašomi taip pat, t.y. Šį skirtumą vėl reikia prisiminti ir įgyvendinti ranka.

Na, taip, šablonų tipai (vector<int>, vector<User>) turi bendrą identifikatorių (#1cb5c415), t.y. jei žinote, kad skambutis paskelbtas kaip

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

tada lauki nebe tik vektoriaus, o vartotojų vektoriaus. Tiksliau, turėtų palaukite - realiame kode kiekvienas elementas, jei ne plikas tipas, turės konstruktorių, ir gerąja prasme įgyvendinant reikėtų patikrinti - bet mes buvome išsiųsti tiksliai kiekviename šio vektoriaus elemente to tipo? O kas, jei tai būtų kažkoks PHP, kurio masyve gali būti skirtingų tipų skirtinguose elementuose?

Šiuo metu pradedate galvoti – ar toks TL reikalingas? Gal vežimėliui būtų galima panaudoti žmogaus serializatorių, tą patį protobufą, kuris jau tada egzistavo? Tokia buvo teorija, pažiūrėkime į praktiką.

Esami TL diegimai kode

TL gimė VKontakte gilumoje dar prieš garsiuosius įvykius parduodant Durovo akcijas ir (tikrai), dar prieš pradedant kurti Telegram. Ir atvirojo kodo pirmojo diegimo šaltinio kodas galite rasti daug juokingų ramentų. Ir pati kalba ten buvo įdiegta visapusiškiau nei dabar Telegramoje. Pavyzdžiui, schemoje maišos iš viso nenaudojamos (tai yra įtaisytas pseudotipas (kaip vektorius) su nukrypimu nuo elgesio). Arba

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

bet panagrinėkime, kad būtų išsamumo, galima atsekti, taip sakant, Minties Milžino evoliuciją.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Arba šis gražuolis:

    static const char *reserved_words_polymorhic[] = {

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

      };

Šis fragmentas yra apie tokius šablonus kaip:

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

Tai yra „hashmap“ šablono tipo apibrėžimas kaip int – tipo porų vektorius. C++ tai atrodytų maždaug taip:

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

taigi, alpha - raktažodis! Bet tik C++ galima rašyti T, bet reikėtų rašyti alfa, beta... Bet ne daugiau kaip 8 parametrai, čia ir baigiasi fantazija. Atrodo, kadaise Sankt Peterburge vyko tokie dialogai:

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

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

Bet tai buvo apie pirmąjį paskelbtą TL įgyvendinimą „apskritai“. Pereikime prie diegimo pačiuose „Telegram“ klientuose.

Žodis Vasilijui:

Vasilijus, [09.10.18 17:07] Labiausiai asilas yra karštas, nes jie sukūrė abstrakcijų krūvą, o tada įkalė varžtą ir uždengė kodų generatorių ramentais
Dėl to pirmiausia iš doko pilot.jpg
Tada iš kodo dzhekichan.webp

Žinoma, žmonės, susipažinę su algoritmais ir matematika, galime tikėtis, kad jie perskaitė Aho, Ullmann ir yra susipažinę su įrankiais, kurie per dešimtmečius tapo de facto standartais pramonėje rašydami savo DSL kompiliatorius, tiesa?

Autorius telegrama-kli yra Vitalijus Valtmanas, kaip galima suprasti iš TLO formato atsiradimo už jo (kli) ribų, komandos narys - dabar yra skirta TL analizavimo biblioteka atskirai, koks įspūdis apie ją TL analizatorius? ..

16.12 04:18 Vasilijus: Manau, kad kažkas neįvaldė lex+yacc
16.12 04:18 Vasilijus: Kitaip negaliu paaiškinti
16.12 04:18 Vasilijus: na arba jiems buvo sumokėta už eilučių skaičių VK
16.12 04:19 Vasilijus: 3k+ eilučių ir pan.<censored> vietoj analizatoriaus

Gal išimtis? Pažiūrėkime kaip daro Tai OFICIALUS klientas – „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);

Python programoje 1100+ eilučių, pora įprastų išraiškų + ypatingi atvejai kaip vektorius, kuris, žinoma, schemoje deklaruojamas taip, kaip turi būti pagal TL sintaksę, bet jie pasikliovė šia sintaksė ją analizuodami... Kyla klausimas, kodėl visa tai buvo stebuklas?иTai labiau sluoksniuota, jei niekas vis tiek nesiruošia analizuoti pagal dokumentaciją?!

Beje... Prisimeni, mes kalbėjome apie CRC32 tikrinimą? Taigi „Telegram Desktop“ kodų generatoriuje yra išimčių sąrašas tiems tipams, kuriuose apskaičiuotas CRC32 nesutampa su nurodytu diagramoje!

Vasilijus, [18.12/22 49:XNUMX] o čia pagalvočiau ar reikia tokio TL
jei norėčiau suktis su alternatyviais diegimais, pradėčiau įterpti eilučių lūžius, pusė analizatorių sulaužys kelių eilučių apibrėžimus
tačiau taip pat ir tdesktop

Prisiminkite mintį apie vieno įdėklą, prie jos grįšime šiek tiek vėliau.

Gerai, „Telegram-Cli“ yra neoficialus, „Telegram Desktop“ yra oficialus, bet kaip su kitais? Kas žino?.. Android kliento kode visai nebuvo schemos analizatoriaus (dėl to kyla klausimų dėl atvirojo kodo, bet tai yra antra dalis), tačiau buvo keletas kitų juokingų kodo dalių, bet daugiau apie juos poskyrį žemiau.

Kokius dar klausimus serializavimas kelia praktikoje? Pavyzdžiui, jie padarė daug dalykų, žinoma, naudodami bitų ir sąlyginius laukus:

Vasilijus: flags.0? true
reiškia, kad laukas yra ir yra lygus true, jei vėliavėlė nustatyta

Vasilijus: flags.1? int
reiškia, kad laukas yra ir jį reikia deserializuoti

Vasilijus: Asi, nesijaudink dėl to, ką darai!
Vasilijus: Kažkur dokumente paminėta, kad tiesa yra plikas nulinio ilgio tipas, bet neįmanoma nieko surinkti iš jų dokumento
Vasilijus: Atvirojo kodo diegimuose taip pat nėra, tačiau yra daugybė ramentų ir atramų

O kaip Telethon? Žvelgiant į MTProto temą, pavyzdys - dokumentacijoje yra tokių dalių, bet ženklas % jis apibūdinamas tik kaip „atitinkantis duotą pliką tipą“, t.y. toliau pateiktuose pavyzdžiuose yra klaida arba kažkas nedokumentuota:

Vasilijus, [22.06.18 18:38] Vienoje vietoje:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Kitaip:

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

Ir tai yra du dideli skirtumai, realiame gyvenime atsiranda kažkoks plikas vektorius

Nemačiau pliko vektorinio apibrėžimo ir nesu sutikęs

Analizė parašyta ranka teletonu

Jo diagramoje apibrėžimas komentuojamas msg_container

Vėl lieka klausimas apie %. Tai nėra aprašyta.

Vadimas Gončarovas, [22.06.18 19:22] ir tdesktop?

Vasilijus, [22.06.18 19:23] Bet jų TL analizatorius įprastuose varikliuose greičiausiai irgi to nevalgys

// parsed manually

TL yra graži abstrakcija, niekas jos iki galo neįgyvendina

Ir % nėra jų schemos versijoje

Bet čia dokumentacija prieštarauja pati sau, todėl idk

Tai buvo rasta gramatikoje, jie galėjo tiesiog pamiršti aprašyti semantiką

Pamatėte dokumentą TL, negalite suprasti be pusės litro

„Na, tarkime“, – pasakys kitas skaitytojas, – „tu ką nors kritikuoji, tai parodyk man, kaip tai turėtų būti daroma“.

Vasilijus atsako: „Kalbant apie analizatorių, man patinka tokie dalykai

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

kažkaip labiau patinka nei

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

arba

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

tai VISAS lekseris:

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

tie. švelniai tariant, paprasčiau“.

Apskritai, faktiškai naudojamo TL poaibio analizatorius ir kodų generatorius telpa į maždaug 100 gramatikos eilučių ir ~300 generatoriaus eilučių (skaičiuojant visas print's sugeneruotas kodas), įskaitant tipo informacijos bandeles, skirtas kiekvienos klasės savistabai. Kiekvienas polimorfinis tipas virsta tuščia abstrakčia bazine klase, o konstruktoriai paveldi iš jos ir turi serializacijos ir deserializacijos metodus.

Tipų trūkumas šrifto kalboje

Stiprus spausdinimas yra geras dalykas, tiesa? Ne, tai ne holivaras (nors man labiau patinka dinamiškos kalbos), o postulatas TL rėmuose. Remiantis juo, kalba turėtų pateikti mums visokius patikrinimus. Na, gerai, gal ne jis pats, o įgyvendinimas, bet jis turėtų bent juos aprašyti. O kokių mes norime galimybių?

Visų pirma, suvaržymai. Čia matome failų įkėlimo dokumentaciją:

Tada failo dvejetainis turinys yra padalintas į dalis. Visos dalys turi būti vienodo dydžio ( part_dydis ) ir turi būti įvykdytos šios sąlygos:

  • part_size % 1024 = 0 (dalomas iš 1KB)
  • 524288 % part_size = 0 (512 KB turi būti tolygiai dalijamasi iš dalies dydžio)

Paskutinė dalis neturi atitikti šių sąlygų, jei jos dydis yra mažesnis nei part_size.

Kiekviena dalis turi turėti eilės numerį, failo_ dalis, kurios vertė svyruoja nuo 0 iki 2,999 XNUMX.

Suskirstę failą į skaidinį, turite pasirinkti būdą, kaip jį išsaugoti serveryje. Naudokite upload.saveBigFilePart tuo atveju, kai visas failo dydis yra didesnis nei 10 MB ir upload.saveFilePart mažesniems failams.
[…] gali būti grąžinta viena iš šių duomenų įvesties klaidų:

  • FILE_PARTS_INVALID – neteisingas dalių skaičius. Vertė nėra tarp 1..3000

Ar kas nors iš to yra diagramoje? Ar tai kažkaip išreiškiama naudojant TL? Nr. Bet atsiprašau, net senelio Turbo Pascal sugebėjo apibūdinti nurodytus tipus diapazonus. Ir jis žinojo dar vieną dalyką, dabar geriau žinomą kaip enum - tipas, sudarytas iš fiksuoto (mažo) reikšmių skaičiaus. Tokiomis kalbomis kaip C - skaitinė, atkreipkite dėmesį, kad iki šiol kalbėjome tik apie tipus skaičiai. Bet yra ir masyvai, eilutės... pavyzdžiui, būtų malonu apibūdinti, kad šioje eilutėje gali būti tik telefono numeris, tiesa?

Nė vienas iš to nėra TL. Tačiau yra, pavyzdžiui, JSON schemoje. Ir jei kas nors galėtų ginčytis dėl 512 KB dalijimosi, kad tai vis tiek reikia patikrinti kode, įsitikinkite, kad klientas negalėjo išsiųsti numerį už diapazono ribų 1..3000 (ir atitinkamos klaidos negalėjo atsirasti) būtų buvę įmanoma, tiesa?..

Beje, apie klaidas ir grąžinamas reikšmes. Net tie, kurie dirbo su TL, užlieja akis – mums tai ne iš karto suprato kiekvienas funkcija TL iš tikrųjų gali grąžinti ne tik aprašytą grąžinimo tipą, bet ir klaidą. Bet to jokiu būdu negalima nustatyti naudojant patį TL. Žinoma, tai jau aišku ir praktiškai nieko nereikia (nors iš tiesų RPC galima daryti įvairiai, prie to grįšime vėliau) – bet kaip dėl abstrakčių tipų matematikos sąvokų grynumo iš dangiškojo pasaulio?.. Paėmiau vilkiką – taip derinkite.

Ir galiausiai, kaip dėl skaitomumo? Na, ten, apskritai, norėčiau aprašymas ar jis tinkamas schemoje (JSON schemoje, vėlgi, taip), bet jei jau esate įtemptas, tai kaip dėl praktinės pusės – bent jau nereikšminga pažvelgti į skirtumus atnaujinant? Pažiūrėkite patys adresu tikrų pavyzdžių:

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

arba

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

Tai priklauso nuo kiekvieno, bet, pavyzdžiui, „GitHub“ atsisako pabrėžti pokyčius tokiose ilgose eilutėse. Žaidimas „surask 10 skirtumų“, ir ką smegenys iš karto mato, kad abiejų pavyzdžių pradžia ir pabaiga yra vienodos, reikia nuobodžiai skaityti kažkur per vidurį... Mano nuomone, tai ne tik teoriškai, bet grynai vizualiai purvinas ir apleistas.

Beje, apie teorijos grynumą. Kodėl mums reikia bitų laukų? Ar neatrodo, kad jie kvapas blogai tipo teorijos požiūriu? Paaiškinimą galima pamatyti ankstesnėse diagramos versijose. Iš pradžių taip, taip ir buvo, kiekvienam čiauduliui buvo kuriamas naujas tipas. Šie užuomazgos vis dar egzistuoja tokia forma, pavyzdžiui:

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;

Bet dabar įsivaizduokite, jei jūsų struktūroje yra 5 pasirenkami laukai, jums reikės 32 tipų visoms galimoms parinktims. Kombinacinis sprogimas. Taigi TL teorijos krištolinis grynumas dar kartą sugriuvo prieš atšiaurios serializacijos realybės ketaus užpakalį.

Be to, kai kur šie vaikinai patys pažeidžia jų pačių tipologiją. Pavyzdžiui, MTProto (kitas skyrius) atsakymą gali suspausti Gzip, viskas gerai – išskyrus tai, kad pažeisti sluoksniai ir grandinė. Vėlgi, buvo nupjauta ne pati RpcResult, o jos turinys. Na, kam tai daryti?.. Teko įsirėžti į ramentą, kad suspaudimas veiktų bet kur.

Arba kitas pavyzdys, kartą atradome klaidą – ji buvo išsiųsta InputPeerUser vietoj InputUser. Arba atvirkščiai. Bet pavyko! Tai yra, serveriui nerūpėjo tipas. Kaip tai gali būti? Atsakymą mums gali pateikti kodo fragmentai iš 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);

Kitaip tariant, čia atliekamas serializavimas RANKINIU būdu, nesugeneruotas kodas! Gal serveris realizuotas panašiai?.. Iš principo tai pasiteisins vieną kartą padarius, bet kaip jį palaikyti vėliau atnaujinimų metu? Ar dėl to buvo sugalvota schema? Ir čia pereiname prie kito klausimo.

Versijų kūrimas. Sluoksniai

Kodėl scheminės versijos vadinamos sluoksniais, galima tik spėlioti remiantis paskelbtų schemų istorija. Matyt, iš pradžių autoriai manė, kad pagrindinius dalykus galima atlikti pagal nepakitusią schemą ir tik esant reikalui, esant konkrečiai užklausai, nurodo, kad tai daroma naudojant kitą versiją. Iš principo net gera idėja – ir nauja bus tarsi „sumaišyta“, sluoksniuota ant seno. Bet pažiūrėkime, kaip tai buvo padaryta. Tiesa, aš negalėjau į tai pažvelgti nuo pat pradžių - tai juokinga, bet pagrindinio sluoksnio diagrama tiesiog neegzistuoja. Sluoksniai prasidėjo 2. Dokumentacijoje pasakojama apie specialią TL funkciją:

Jei klientas palaiko 2 sluoksnį, turi būti naudojamas šis konstruktorius:

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

Praktiškai tai reiškia, kad prieš kiekvieną API iškvietimą pateikiamas int su verte 0x289dd1f6 turi būti pridėtas prieš metodo numerį.

Skamba normaliai. Bet kas nutiko toliau? Tada pasirodė

invokeWithLayer3#b7475268 query:!X = X;

Taigi, kas toliau? Kaip galite atspėti,

invokeWithLayer4#dea0d430 query:!X = X;

Juokinga? Ne, per anksti juoktis, pagalvokite apie tai kiekvienas užklausą iš kito sluoksnio reikia įvynioti į tokį specialų tipą - jei tau jie visi skirtingi, kaip kitaip juos atskirsi? Ir tik 4 baitų pridėjimas priekyje yra gana efektyvus būdas. Taigi,

invokeWithLayer5#417a57ae query:!X = X;

Bet akivaizdu, kad po kurio laiko tai taps savotiška bakchanalia. Ir atėjo sprendimas:

Atnaujinimas: pradedant nuo 9 sluoksnio, pagalbinių metodų invokeWithLayerN galima naudoti tik kartu su initConnection

Sveika! Po 9 versijų pagaliau priėjome prie to, kas buvo daroma interneto protokoluose dar devintajame dešimtmetyje – prisijungimo pradžioje susitarėme dėl versijos vieną kartą!

Taigi kas toliau?..

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

Bet dabar vis tiek galite juoktis. Tik po dar 9 sluoksnių pagaliau buvo pridėtas universalus konstruktorius su versijos numeriu, kurį prisijungimo pradžioje reikia iškviesti tik vieną kartą ir lyg dingo sluoksnių prasmė, dabar tai tik sąlyginė versija, pvz. visur kitur. Problema išspręsta.

Tiksliai?..

Vasilijus, [16.07.18 14:01] Dar penktadienį galvojau:
Teleserveris siunčia įvykius be užklausos. Užklausos turi būti suvyniotos į InvokeWithLayer. Serveris neapvynioja naujinimų; nėra atsakymų ir naujinimų apvyniojimo struktūros.

Tie. klientas negali nurodyti sluoksnio, kuriame jis nori atnaujinti

Vadim Goncharov, [16.07.18 14:02] ar InvokeWithLayer iš principo nėra ramentas?

Vasilijus, [16.07.18 14:02] Tai vienintelis būdas

Vadim Goncharov, [16.07.18 14:02], kas iš esmės turėtų reikšti susitarimą dėl sluoksnio sesijos pradžioje

Beje, iš to seka, kad kliento ankstesnė versija nėra teikiama

Atnaujinimai, t.y. tipo Updates schemoje tai yra tai, ką serveris siunčia klientui ne atsakydamas į API užklausą, bet savarankiškai, kai įvyksta įvykis. Tai sudėtinga tema, kuri bus aptariama kitame įraše, tačiau kol kas svarbu žinoti, kad serveris išsaugo atnaujinimus net tada, kai klientas neprisijungęs.

Taigi, jei atsisakote vynioti kiekvienas paketą, kad būtų nurodyta jo versija, logiškai tai sukelia šias galimas problemas:

  • serveris siunčia atnaujinimus klientui dar prieš tai, kai klientas praneša, kurią versiją palaiko
  • ką turėčiau daryti atnaujinus klientą?
  • kas garantijaskad proceso metu serverio nuomonė apie sluoksnio numerį nepasikeis?

Ar manote, kad tai grynai teorinės spekuliacijos, o praktiškai taip negali atsitikti, nes serveris parašytas teisingai (bent jau patikrintas gerai)? Cha! Kad ir kaip būtų!

Būtent su tuo susidūrėme rugpjūčio mėnesį. Rugpjūčio 14 dieną buvo pranešimai, kad kažkas atnaujinama Telegram serveriuose... o tada žurnaluose:

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.

ir tada kelių megabaitų kamino pėdsakų (na, tuo pačiu ir registracija buvo sutvarkyta). Galų gale, jei kažkas neatpažįstamas jūsų TL, tai yra dvejetainis pagal parašą, toliau VISKAS eina, dekoduoti taps neįmanoma. Ką reikėtų daryti tokioje situacijoje?

Na, pirmas dalykas, kuris ateina į galvą, yra atsijungti ir bandyti dar kartą. Nepadėjo. Paieškojome google CRC32 – tai pasirodė objektai iš 73 schemos, nors dirbome su 82. Atidžiai žiūrime į žurnalus – ten yra dviejų skirtingų schemų identifikatoriai!

Galbūt problema yra tik mūsų neoficialiame kliente? Ne, paleidžiame „Telegram Desktop 1.2.17“ (versija tiekiama daugelyje „Linux“ paskirstymų), ji įrašo į išimčių žurnalą: MTP Netikėtas tipo ID #b5223b0f, perskaitytas MTPMessageMedia...

„Telegram“ protokolo ir organizacinių požiūrių kritika. 1 dalis, techninė: patirtis rašant klientą nuo nulio - TL, MT

Google parodė, kad panaši problema jau buvo nutikusi vienam iš neoficialių klientų, tačiau tada versijų numeriai ir atitinkamai prielaidos skyrėsi...

Taigi ką turėtume daryti? Mes su Vasilijumi išsiskyrėme: jis bandė atnaujinti grandinę iki 91, aš nusprendžiau palaukti kelias dienas ir išbandyti 73. Abu metodai veikė, bet kadangi jie yra empiriniai, nėra supratimo, kiek versijų reikia aukštyn ar žemyn. peršokti arba kiek laiko reikia laukti .

Vėliau man pavyko atkurti situaciją: paleidžiame klientą, išjungiame, perkompiliuojame grandinę į kitą sluoksnį, paleidžiame iš naujo, vėl nustatome problemą, grįžtame prie ankstesnės – oi, grandinė neperjungiama ir klientas paleidžiamas iš naujo. kelios minutės padės. Gausite duomenų struktūrų derinį iš skirtingų sluoksnių.

Paaiškinimas? Kaip galite atspėti iš įvairių netiesioginių požymių, serverį sudaro daugybė skirtingų tipų procesų skirtingose ​​mašinose. Labiausiai tikėtina, kad serveris, atsakingas už „buferį“, į eilę įtraukė tai, ką jam davė viršininkai, ir jie tai davė pagal schemą, kuri buvo generavimo metu. Ir kol ši eilė „supuvo“, nieko nebuvo galima padaryti.

Galbūt... bet čia baisus ramentas?!.. Ne, prieš galvodami apie beprotiškas idėjas, pažiūrėkime į oficialių klientų kodą. „Android“ versijoje nerandame jokio TL analizatoriaus, bet randame didelį failą („GitHub“ atsisako jo liesti) su (de)serializavimu. Štai kodo fragmentai:

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;

arba

    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... atrodo laukinė. Bet tikriausiai tai yra sugeneruotas kodas, tada gerai?.. Bet jis tikrai palaiko visas versijas! Tiesa, neaišku, kodėl viskas maišoma, slapti pokalbiai ir visokie _old7 kažkaip neatrodo kaip mašinų karta... Tačiau labiausiai mane sužavėjo

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Vaikinai, ar net negalite nuspręsti, kas yra viename sluoksnyje?! Na, gerai, tarkim, su klaida buvo paleistas „du“, na, būna, bet TRYS?.. Iš karto vėl tas pats grėblys? Kas čia per pornografija, atsiprašau?

Beje, „Telegram Desktop“ šaltinio kode atsitinka panašiai – jei taip, keli schemos įsipareigojimai iš eilės nekeičia jos sluoksnio numerio, o kažką pataiso. Tais atvejais, kai nėra oficialaus schemos duomenų šaltinio, iš kur juos galima gauti, išskyrus oficialaus kliento šaltinio kodą? Ir jei paimsite tai iš ten, negalėsite būti tikri, kad schema yra visiškai teisinga, kol neišbandysite visų metodų.

Kaip tai netgi galima išbandyti? Tikiuosi, kad vienetinių, funkcinių ir kitų testų gerbėjai pasidalins komentaruose.

Gerai, pažiūrėkime į kitą kodo dalį:

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;

Šis komentaras „sukurtas rankiniu būdu“ rodo, kad tik dalis šio failo buvo parašyta rankiniu būdu (ar įsivaizduojate visą priežiūros košmarą?), o likusi dalis buvo sukurta mašina. Tačiau tada iškyla kitas klausimas – ar šaltiniai yra ne iki galo (a la GPL blobs Linux branduolyje), bet tai jau antros dalies tema.

Bet pakankamai. Pereikime prie protokolo, kurio viršuje vyksta visa ši serija.

MT Proto

Taigi, atidarykime Bendras aprašymas и išsamus protokolo aprašymas ir pirmas dalykas, už kurį suklumpame, yra terminija. Ir su visko gausa. Apskritai, atrodo, kad tai yra patentuota „Telegram“ funkcija – vadinti dalykus skirtingose ​​vietose arba kitaip vienu žodžiu, arba atvirkščiai (pavyzdžiui, aukšto lygio API, jei matote lipdukų paketą, tai nėra ką tu galvoji).

Pavyzdžiui, „pranešimas“ ir „sesija“ čia reiškia ką kita nei įprastoje „Telegram“ kliento sąsajoje. Na, su pranešimu viskas aišku, jį galima interpretuoti OOP terminais arba tiesiog pavadinti žodžiu „paketas“ - tai žemas, transportavimo lygis, nėra tų pačių pranešimų kaip sąsajoje, yra daug paslaugų pranešimų . Bet sesija... bet pirmiausiai.

transportavimo sluoksnis

Pirmas dalykas yra transportas. Jie mums papasakos apie 5 variantus:

  • TCP
  • Tinklo lizdas
  • Websocket per HTTPS
  • HTTP
  • HTTPS

Vasilijus, [15.06.18 15:04] Taip pat yra UDP transportas, bet jis nėra dokumentuotas

Ir TCP trimis variantais

Pirmasis yra panašus į UDP per TCP, kiekviename pakete yra eilės numeris ir crc
Kodėl taip skausminga skaityti dokumentus ant vežimėlio?

Na, štai dabar TCP jau 4 variantai:

  • Sutrumpintas
  • Tarpinis
  • Paminkštintas tarpinis
  • Pilnas

Na, gerai, Padded tarpinis MTProxy, tai vėliau buvo pridėta dėl gerai žinomų įvykių. Bet kam dar dvi versijos (iš viso trys), kai galima apsieiti su viena? Visi keturi iš esmės skiriasi tik tuo, kaip nustatyti pagrindinio MTProto ilgį ir naudingą apkrovą, apie kurią bus kalbama toliau:

  • Sutrumpintuose jis yra 1 arba 4 baitai, bet ne 0xef, tada kūnas
  • Intermediate tai yra 4 baitai ir laukas, o pirmą kartą klientas turi siųsti 0xeeeeeeee nurodyti, kad jis yra tarpinis
  • Visiškai labiausiai priklausomybę, tinklo naudotojo požiūriu: ilgis, eilės numeris ir NE TAS, kuris daugiausia yra MTProto, korpusas, CRC32. Taip, visa tai yra TCP viršuje. Tai suteikia mums patikimą transportą nuoseklaus baitų srauto pavidalu; nereikia jokių sekų, ypač kontrolinių sumų. Gerai, dabar kažkas man prieštaraus, kad TCP turi 16 bitų kontrolinę sumą, todėl duomenys sugadinami. Puiku, bet iš tikrųjų turime kriptografinį protokolą, kurio maišos ilgesnės nei 16 baitų, visas šias klaidas – ir dar daugiau – užfiksuos aukštesnio lygio SHA neatitikimas. Be to, CRC32 nėra jokios prasmės.

Palyginkime Sutrumpintą, kuriame galimas vienas baitas, su Intermediate, kuris pateisina „Jei reikia 4 baitų duomenų išlyginimo“, o tai yra gana nesąmonė. Manoma, kad „Telegram“ programuotojai yra tokie nekompetentingi, kad negali nuskaityti duomenų iš lizdo į suderintą buferį? Jūs vis tiek turite tai padaryti, nes skaitymas gali grąžinti bet kokį baitų skaičių (ir, pavyzdžiui, yra ir tarpinių serverių...). Arba, kita vertus, kam blokuoti sutrumpintą funkciją, jei 16 baitų vis tiek turėsime nemenką užpildą – sutaupykite 3 baitus kartais ?

Susidaro įspūdis, kad Nikolajus Durovas labai mėgsta išradinėti ratus, įskaitant tinklo protokolus, be jokio realaus praktinio poreikio.

Kitos transporto galimybės, įskaitant. Web ir MTProxy, dabar nesvarstysime, gal kitame įraše, jei bus užklausa. Apie tą patį MTProxy, prisiminkime tik dabar, kad netrukus po jo išleidimo 2018 m. teikėjai greitai išmoko jį blokuoti, skirtą aplinkkelio blokavimasPagal pakuotės dydis! Taip pat faktas, kad MTProxy serveris, parašytas (vėlgi Waltmano) C, buvo pernelyg susietas su Linux specifika, nors to visai nereikėjo (patvirtins Phil Kulin), ir kad panašus serveris Go arba Node.js telpa į mažiau nei šimtą eilučių.

Bet išvadas apie šių žmonių techninį raštingumą padarysime skyriaus pabaigoje, apsvarstę kitus klausimus. Kol kas pereikime prie OSI 5 sluoksnio, seanso – ant kurio jie įdėjo MTProto sesiją.

Raktai, pranešimai, sesijos, Diffie-Hellman

Jie įdėjo jį ne visai teisingai... Sesija nėra ta pati sesija, kuri matoma sąsajos dalyje Aktyvūs seansai. Bet tvarka.

„Telegram“ protokolo ir organizacinių požiūrių kritika. 1 dalis, techninė: patirtis rašant klientą nuo nulio - TL, MT

Taigi iš transportavimo sluoksnio gavome žinomo ilgio baitų eilutę. Tai yra arba užšifruotas pranešimas, arba paprastas tekstas – jei vis dar esame rakto susitarimo stadijoje ir iš tikrųjų tai darome. Apie kurią iš sąvokų, vadinamų „raktu“, krūvos mes kalbame? Išsiaiškinkime šią problemą pačiai „Telegram“ komandai (atsiprašau, kad 4 val. pavargusiomis smegenimis išverčiau savo dokumentaciją iš anglų kalbos, kai kurias frazes buvo lengviau palikti tokias, kokios yra):

Yra du subjektai, vadinami posėdis - vienas oficialių klientų vartotojo sąsajoje, skiltyje „dabartinės sesijos“, kur kiekviena sesija atitinka visą įrenginį / OS.
Antrasis yra MTProto sesija, kuriame yra pranešimo eilės numeris (žemo lygio prasme) ir kuris gali trukti tarp skirtingų TCP ryšių. Vienu metu galima įdiegti kelias MTProto sesijas, pavyzdžiui, siekiant pagreitinti failų atsisiuntimą.

Tarp šių dviejų sesijos yra koncepcija leidimas. Išsigimusiu atveju galime taip sakyti UI sesija yra toks pat kaip leidimas, bet, deja, viskas sudėtinga. Pažiūrėkime:

  • Naudotojas naujame įrenginyje pirmiausia sukuria auth_key ir susieja ją su sąskaita, pavyzdžiui, SMS žinute – štai kodėl leidimas
  • Tai atsitiko pirmojo viduje MTProto sesija, kuris turi session_id savyje.
  • Šiame etape derinys leidimas и session_id galima būtų vadinti pavyzdys - šis žodis yra kai kurių klientų dokumentuose ir koduose
  • Tada klientas gali atidaryti šiek tiek MTProto sesijos pagal tą patį auth_key - į tą patį DC.
  • Tada vieną dieną klientas turės paprašyti failo kitas DC - ir šiam DC bus sukurtas naujas auth_key !
  • Informuoti sistemą, kad registruojasi ne naujas vartotojas, o tas pats leidimas (UI sesija), klientas naudoja API skambučius auth.exportAuthorization namuose DC auth.importAuthorization naujajame DC.
  • Viskas tas pats, gali būti atviri keli MTProto sesijos (kiekvienas su savo session_id) į šį naują DC, pagal jo auth_key.
  • Galiausiai klientas gali norėti tobulo perdavimo slaptumo. kas auth_key buvo nuolatinis raktas - per DC - ir klientas gali skambinti auth.bindTempAuthKey naudojimui laikinas auth_key - ir vėl tik vienas temp_auth_key per DC, bendras visiems MTProto sesijos į šią DC.

Atkreipkite dėmesį, kad druska (ir būsimos druskos) taip pat yra vienas auth_key tie. pasidalino tarp visų MTProto sesijos į tą patį DC.

Ką reiškia „tarp skirtingų TCP jungčių“? Taigi tai reiškia kažkas kaip autorizacijos slapukas svetainėje – jis išlaiko (išgyvena) daug TCP ryšių su tam tikru serveriu, bet vieną dieną sugenda. Tik skirtingai nei HTTP, MTProto pranešimai seanso metu yra numeruojami ir patvirtinami nuosekliai; jei jie pateko į tunelį, ryšys nutrūko – užmezgus naują ryšį, serveris maloniai išsiųs viską šioje sesijoje, ko nepateikė ankstesnėje TCP ryšys.

Tačiau aukščiau pateikta informacija apibendrinta po daugelio mėnesių tyrimo. Tuo tarpu ar mes įgyvendiname savo klientą nuo nulio? – grįžkime į pradžią.

Taigi generuokime auth_key apie Diffie-Hellman versijos iš Telegram. Pabandykime suprasti dokumentaciją...

Vasilijus, [19.06.18 20:05] data_with_hash := SHA1(duomenys) + duomenys + (bet kokie atsitiktiniai baitai); taip, kad ilgis būtų lygus 255 baitams;
šifruoti_duomenys := RSA(duomenys_su_maiša, serverio_viešasis_raktas); 255 baitų ilgio skaičius (didysis endianas) padidinamas iki reikiamos galios per reikiamą modulį, o rezultatas išsaugomas kaip 256 baitų skaičius.

Jie turi šiek tiek svaiginančio DH

Neatrodo kaip sveiko žmogaus DH
Dx nėra dviejų viešųjų raktų

Na, galų gale tai buvo sutvarkyta, bet liko likutis - kliento atliktas darbo įrodymas, kad jis sugebėjo apskaičiuoti skaičių. Apsaugos nuo DoS atakų tipas. O RSA raktas naudojamas tik vieną kartą viena kryptimi, iš esmės šifravimui new_nonce. Tačiau nors ši iš pažiūros paprasta operacija pavyks, su kuo teks susidurti?

Vasilijus, [20.06.18/00/26 XNUMX:XNUMX] Aš dar negavau programos

Išsiunčiau šį prašymą DH

Ir transporto doke sakoma, kad jis gali atsakyti su 4 baitais klaidos kodu. Tai viskas

Na, jis man pasakė -404, o kas?

Taigi aš jam pasakiau: „Pagauk savo kvailystę, užšifruotą serverio raktu su tokiu piršto atspaudu, aš noriu DH“, ir jis atsakė kvailu 404.

Ką manote apie šį serverio atsakymą? Ką daryti? Nėra kam klausti (bet apie tai plačiau antroje dalyje).

Čia visas susidomėjimas atliekamas prieplaukoje

Neturiu ką veikti, tik svajojau konvertuoti skaičius pirmyn ir atgal

Du 32 bitų skaičiai. Aš juos supakavau kaip ir visi kiti

Bet ne, šiuos du pirmiausia reikia įtraukti į eilutę kaip BE

Vadimas Gončarovas, [20.06.18 15:49] ir dėl to 404?

Vasilijus, [20.06.18 15:49] TAIP!

Vadimas Gončarovas, [20.06.18 15:50] todėl nesuprantu ko jis gali "nerado"

Vasilijus [20.06.18 15:50] apie

Aš negalėjau rasti tokio išskaidymo į pagrindinius veiksnius)

Mes net netvarkėme klaidų pranešimų

Vasilijus, [20.06.18 20:18] O, yra ir MD5. Jau trys skirtingos maišos

Rakto piršto atspaudas apskaičiuojamas taip:

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

SHA1 ir sha2

Taigi įdėkime auth_key mes gavome 2048 bitų dydį naudodami Diffie-Hellman. Kas toliau? Tada sužinome, kad žemesni 1024 šio rakto bitai jokiu būdu nenaudojami... bet dabar pagalvokime apie tai. Šiame žingsnyje mes turime bendrą paslaptį su serveriu. Nustatytas TLS seanso analogas, o tai labai brangi procedūra. Bet serveris vis tiek nieko nežino apie tai, kas mes esame! Tiesą sakant, dar ne. leidimas. Tie. jei galvojote apie „prisijungimo slaptažodį“, kaip kadaise darėte ICQ, arba bent „prisijungimo raktą“, kaip SSH (pavyzdžiui, kai kuriuose „gitlab“ / „github“). Gavome anoniminį. Ką daryti, jei serveris mums pasakys: „šiuos telefono numerius aptarnauja kitas DC“? Ar net „jūsų telefono numeris uždraustas“? Geriausia, ką galime padaryti, tai laikyti raktą tikėdamiesi, kad jis bus naudingas ir iki tol nesuges.

Beje, „gavome“ su išlygomis. Pavyzdžiui, ar pasitikime serveriu? O jei tai netikra? Kriptografiniai patikrinimai būtų reikalingi:

Vasilijus, [21.06.18 17:53] Jie siūlo mobiliesiems klientams patikrinti 2kbit numerio pirmenybę)

Bet tai visai neaišku, nafeijoa

Vasilijus, [21.06.18 18:02] Dokumente nenurodyta, ką daryti, jei paaiškėja, kad tai nėra paprasta

Nepasakyta. Pažiūrėkime, ką šiuo atveju daro oficialus „Android“ klientas? A štai ką (ir taip, visas failas įdomus) – kaip sakoma, paliksiu čia:

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

Ne, žinoma, jis vis dar yra kai kurie Yra testų dėl skaičiaus pirmumo, bet aš asmeniškai nebeturiu pakankamai matematikos žinių.

Gerai, mes turime pagrindinį raktą. Norėdami prisijungti, t.y. siųsti užklausas, turite atlikti tolesnį šifravimą naudojant AES.

Pranešimo raktas apibrėžiamas kaip 128 viduriniai pranešimo teksto SHA256 bitai (įskaitant seansą, pranešimo ID ir kt.), įskaitant užpildymo baitus, prie kurių pridedami 32 baitai, paimti iš autorizacijos rakto.

Vasilijus, [22.06.18 14:08] Vidutinis, kalė, bitai

Gauta auth_key. Visi. Už jų... iš dokumento neaišku. Nedvejodami studijuokite atvirojo kodo kodą.

Atminkite, kad MTProto 2.0 reikalauja nuo 12 iki 1024 baitų užpildymo, vis tiek su sąlyga, kad gautas pranešimo ilgis dalijasi iš 16 baitų.

Taigi, kiek paminkštinimo reikėtų pridėti?

Ir taip, klaidos atveju yra ir 404

Jei kas atidžiai išstudijavo dokumentacijos schemą ir tekstą, pastebėjo, kad ten nėra MAC. Ir kad AES naudojamas tam tikru IGE režimu, kuris niekur kitur nenaudojamas. Jie, žinoma, rašo apie tai savo DUK... Čia, pavyzdžiui, pats pranešimo raktas taip pat yra iššifruotų duomenų SHA maiša, naudojama patikrinti vientisumą, o jei neatitikimas, dokumentacija dėl tam tikrų priežasčių rekomenduoja tyliai juos ignoruoti (bet kaip su saugumu, o jei jie mus sulaužys?).

Nesu kriptografas, gal šiuo atveju teoriškai nieko blogo šiame režime nėra. Bet galiu aiškiai įvardinti praktinę problemą, kaip pavyzdį naudodamas „Telegram Desktop“. Jis užšifruoja vietinę talpyklą (visos šios D877F783D5D3EF8C) taip pat, kaip ir pranešimus MTProto (tik šiuo atveju 1.0 versija), t.y. pirmiausia pranešimo raktas, tada patys duomenys (ir kažkur nuo pagrindinio didelio auth_key 256 baitai, be kurių msg_key nenaudingas). Taigi, problema tampa pastebima dideliuose failuose. Būtent, jums reikia saugoti dvi duomenų kopijas – užšifruotas ir iššifruotas. O jei yra megabaitų, ar, pavyzdžiui, vaizdo transliacija?.. Klasikinės schemos su MAC po šifruoto teksto leidžia skaityti srautu, iškart perduodant. Bet su MTProto turėsite iš pradžių užšifruoti arba iššifruoti visą pranešimą, tik tada perkelti jį į tinklą arba į diską. Todėl naujausiose „Telegram Desktop“ versijose talpykloje user_data Taip pat naudojamas kitas formatas – su AES CTR režimu.

Vasilijus, [21.06.18 01:27] O, aš sužinojau, kas yra IGE: IGE buvo pirmasis „autentifikavimo šifravimo režimo“ bandymas, iš pradžių skirtas „Kerberos“. Tai buvo nesėkmingas bandymas (ji neužtikrina vientisumo apsaugos) ir turėjo būti pašalinta. Tai buvo 20 metų trukusio veiksmingo autentifikavimo šifravimo režimo, kuris neseniai baigėsi tokiais režimais kaip OCB ir GCM, pradžia.

O dabar argumentai iš krepšelio pusės:

„Telegram“ komandą, vadovaujamą Nikolajaus Durovo, sudaro šeši ACM čempionai, iš kurių pusė yra matematikos mokslų daktarai. Jiems prireikė maždaug dvejų metų, kol jie išleido dabartinę MTProto versiją.

Tai juokinga. Dveji metai žemesniame lygyje

Arba galite tiesiog paimti tls

Gerai, tarkime, kad atlikome šifravimą ir kitus niuansus. Ar pagaliau galima siųsti užklausas suserializuotas TL ir deserializuoti atsakymus? Taigi, ką ir kaip turėtumėte siųsti? Čia, tarkime, metodas initConnection, gal tai yra?

Vasilijus, [25.06.18 18:46] Inicijuoja ryšį ir išsaugo informaciją vartotojo įrenginyje ir programoje.

Jis priima „app_id“, „device_model“, „system_version“, „app_version“ ir „lang_code“.

Ir šiek tiek užklausa

Dokumentacija kaip visada. Nesivaržykite studijuoti atvirojo kodo

Jei viskas buvo maždaug aišku naudojant invokeWithLayer, tai kas čia ne taip? Pasirodo, tarkime, turime – klientas jau turėjo ko paklausti serverio – yra užklausa, kurią norėjome išsiųsti:

Vasilijus, [25.06.18 19:13] Sprendžiant iš kodo, pirmas skambutis įvyniotas į šitą šūdą, o pats šūdas įvyniotas į invokewithlayer

Kodėl „initConnection“ negali būti atskiras iškvietimas, bet turi būti paketas? Taip, kaip paaiškėjo, tai turi būti daroma kiekvieną kartą kiekvienos sesijos pradžioje, o ne vieną kartą, kaip naudojant pagrindinį klavišą. Bet! Neleistinas vartotojas jo negali iškviesti! Dabar mes pasiekėme etapą, kai jis taikomas Šitas dokumentacijos puslapis – ir jame nurodoma, kad...

Tik nedidelė API metodų dalis yra prieinama neįgaliotiems vartotojams:

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

Pats pirmasis iš jų, auth.sendCode, ir yra ta branginama pirmoji užklausa, kurioje siunčiame api_id ir api_hash, o po to gauname SMS su kodu. Ir jei mes esame ne toje DC (telefono numerius šioje šalyje aptarnauja, pavyzdžiui, kitas), tada gausime klaidą su norimos DC numeriu. Norėdami sužinoti, prie kurio IP adreso pagal DC numerį turite prisijungti, padėkite mums help.getConfig. Vienu metu buvo tik 5 įrašai, tačiau po garsių 2018 metų įvykių skaičius gerokai išaugo.

Dabar prisiminkime, kad šį etapą serveryje pasiekėme anonimiškai. Ar ne per brangu gauti tik IP adresą? Kodėl to ir kitų operacijų neatlikus nešifruotoje MTProto dalyje? Girdžiu prieštaravimą: „kaip galime įsitikinti, kad klaidingais adresais atsakys ne RKN? Tam mes prisimename, kad apskritai oficialūs klientai RSA raktai yra įterpti, t.y. ar gali tiesiog ženklas Ši informacija. Tiesą sakant, tai jau daroma dėl informacijos apie blokavimo apeitį, kurią klientai gauna kitais kanalais (logiška, to negalima padaryti pačiame MTProto; taip pat reikia žinoti, kur prisijungti).

GERAI. Šiame kliento įgaliojimo etape mes dar nesame įgalioti ir neužregistravome savo paraiškos. Tik dabar norime pamatyti, kaip serveris reaguoja į metodus, prieinamus neteisėtam vartotojui. Ir čia…

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

Schemoje pirmas yra antras

tdesktop schemoje trečioji reikšmė yra

Taip, nuo tada, žinoma, dokumentacija buvo atnaujinta. Nors netrukus tai vėl gali tapti nereikšminga. Kaip pradedantysis kūrėjas turėtų žinoti? Gal jei užregistruosite savo prašymą, jie jus informuos? Vasilijus tai padarė, bet, deja, jie jam nieko neatsiuntė (vėlgi, apie tai kalbėsime antroje dalyje).

...Pastebėjote, kad mes jau kažkaip persikėlėme į API, t.y. į kitą lygį, ir kažką praleidote MTProto temoje? Jokio siurprizo:

Vasilijus, [28.06.18 02:04] Mm, jie knisa po kai kuriuos e2e algoritmus

Mtproto apibrėžia šifravimo algoritmus ir raktus abiem domenams, taip pat šiek tiek įpakavimo struktūros

Tačiau jie nuolat maišo skirtingus krūvos lygius, todėl ne visada aišku, kur baigėsi mtproto ir prasidėjo kitas lygis

Kaip jie maišosi? Na, štai, pavyzdžiui, tas pats laikinas PFS raktas (beje, „Telegram Desktop“ to padaryti negali). Jis vykdomas pagal API užklausą auth.bindTempAuthKey, t.y. nuo aukščiausio lygio. Bet tuo pat metu tai trukdo šifruoti žemesniu lygiu - pavyzdžiui, po jo reikia tai padaryti dar kartą initConnection ir tt, tai ne tiesiog normalus prašymas. Ypatinga ir tai, kad viename DC galite turėti tik VIENĄ laikiną raktą, nors lauke auth_key_id kiekviename laiške leidžia pakeisti raktą bent kiekvienu pranešimu, o serveris turi teisę bet kada „pamiršti“ laikinąjį raktą – dokumentacijoje nepasakoma, ką tokiu atveju daryti... na kodėl galėjo Ar jūs neturite kelių raktų, kaip su būsimų druskų rinkiniu, ir?..

Yra keletas kitų dalykų, kuriuos verta atkreipti dėmesį į MTProto temą.

Pranešimų pranešimai, msg_id, msg_seqno, patvirtinimai, ping ne ta kryptimi ir kitos ypatybės

Kodėl apie juos reikia žinoti? Kadangi jie „nuteka“ į aukštesnį lygį, ir jūs turite juos žinoti dirbdami su API. Tarkime, kad mūsų nedomina msg_key; žemesnis lygis viską iššifravo už mus. Tačiau iššifruotų duomenų viduje turime šiuos laukus (taip pat duomenų ilgį, todėl žinome, kur yra užpildas, bet tai nėra svarbu):

  • druska - int64
  • session_id – int64
  • pranešimo_id – int64
  • seq_no - int32

Priminsime, kad visai DC yra tik viena druska. Kodėl apie ją žinoti? Ne tik todėl, kad yra prašymas get_future_salts, kuris nurodo, kurie intervalai galios, bet ir todėl, kad jei jūsų druska bus „supuvusi“, žinutė (užklausa) tiesiog bus prarasta. Žinoma, serveris praneš apie naują druską išduodamas new_session_created - bet su senu teks kažkaip persiųsti pvz. Ir ši problema turi įtakos programos architektūrai.

Serveriui dėl daugelio priežasčių leidžiama visiškai nutraukti seansus ir tokiu būdu atsakyti. Tiesą sakant, kas yra MTProto sesija iš kliento pusės? Tai du skaičiai session_id и seq_no pranešimus šios sesijos metu. Na, ir, žinoma, pagrindinis TCP ryšys. Tarkime, mūsų klientas vis dar nežino, kaip padaryti daug dalykų, jis atsijungė ir vėl prisijungė. Jei tai įvyko greitai - senoji sesija tęsėsi naujame TCP ryšyje, padidinkite seq_no toliau. Jei tai užtruks ilgai, serveris gali jį ištrinti, nes jo pusėje tai taip pat yra eilė, kaip išsiaiškinome.

Koks jis turėtų būti seq_no? Oi, keblus klausimas. Pabandykite nuoširdžiai suprasti, kas buvo galvoje:

Su turiniu susijęs pranešimas

Pranešimas, reikalaujantis aiškaus patvirtinimo. Tai apima visus naudotojo ir daugelio paslaugų pranešimus, beveik visus, išskyrus konteinerius ir patvirtinimus.

Pranešimo sekos numeris (msg_seqno)

32 bitų skaičius, lygus dvigubam „su turiniu susijusių“ pranešimų (kurių reikia patvirtinti, ypač tų, kurie nėra talpyklos), kuriuos siuntėjas sukūrė prieš šį pranešimą ir vėliau padidintas vienu, jei dabartinis pranešimas yra su turiniu susijusį pranešimą. Konteineris visada generuojamas po viso jo turinio; todėl jo eilės numeris yra didesnis arba lygus jame esančių pranešimų eilės numeriams.

Koks čia cirkas su prieaugiu 1, o po to dar 2?.. Įtariu, kad iš pradžių jie turėjo omenyje „mažiausiai reikšmingą ACK bitą, o likusi dalis yra skaičius“, bet rezultatas nėra visiškai tas pats - ypač išeina, galima siųsti šiek tiek patvirtinimai, turintys tą patį seq_no! Kaip? Na, pavyzdžiui, serveris mums kažką siunčia, siunčia, o mes patys tylime, tik atsakome serviso pranešimais, patvirtinančiais jo pranešimų gavimą. Tokiu atveju mūsų siunčiamų patvirtinimų numeris bus toks pat. Jei esate susipažinę su TCP ir manote, kad tai skamba kažkaip laukiškai, bet atrodo nelabai laukinė, nes TCP seq_no nesikeičia, bet eina patvirtinimas seq_no iš kitos pusės, aš skubėsiu jus nuliūdinti. Patvirtinimai pateikiami MTProto NĖRA apie seq_no, kaip ir TCP, bet pagal msg_id !

Kas čia msg_id, svarbiausia iš šių sričių? Unikalus pranešimo identifikatorius, kaip rodo pavadinimas. Jis apibrėžiamas kaip 64 bitų skaičius, kurio žemiausi bitai vėl turi magiją „serveris-ne-serveris“, o likusieji yra Unix laiko žyma, įskaitant trupmeninę dalį, perkeltą 32 bitais į kairę. Tie. laiko žyma per se (ir pranešimus, kurių laikas per daug skiriasi, serveris atmes). Iš to paaiškėja, kad tai yra visuotinis kliento identifikatorius. Atsižvelgiant į tai – prisiminkime session_id - Mes garantuojame: Jokiu būdu vienai sesijai skirtas pranešimas negali būti siunčiamas į kitą sesiją. Tai yra, pasirodo, kad jau yra trys lygis - sesija, sesijos numeris, žinutės ID. Kodėl toks pernelyg sudėtingas, ši paslaptis yra labai didelė.

tokiu būdu, msg_id reikalingas...

RPC: užklausos, atsakymai, klaidos. Patvirtinimai.

Kaip tikriausiai pastebėjote, niekur diagramoje nėra specialaus tipo ar funkcijos „padaryti RPC užklausą“, nors atsakymai yra. Juk turime su turiniu susijusių žinučių! Tai yra, bet koks žinutė gali būti prašymas! Arba nebūti. Po visko, kiekvienas yra msg_id. Bet yra atsakymų:

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

Čia nurodoma, į kurį pranešimą tai yra atsakymas. Todėl aukščiausiame API lygyje turėsite atsiminti, koks buvo jūsų užklausos numeris – manau, nereikia aiškinti, kad darbas yra asinchroninis, o vienu metu gali būti vykdomos kelios užklausos, į kuriuos atsakymus galima grąžinti bet kokia tvarka? Iš esmės iš šio ir klaidų pranešimų, pvz., darbuotojų nėra, galima atsekti už tai slypinčią architektūrą: serveris, palaikantis TCP ryšį su jumis, yra priekinės dalies balansuotojas, jis persiunčia užklausas užpakalinėms programoms ir renka jas atgal per message_id. Atrodo, kad čia viskas aišku, logiška ir gerai.

Taip?.. O jei pagalvoji? Juk ir pats RPC atsakymas turi lauką msg_id! Ar mums reikia šaukti serveriui „tu neatsakai į mano atsakymą! Ir taip, kas ten buvo apie patvirtinimus? Apie puslapį pranešimai apie žinutes mums pasako, kas yra

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

ir tai turi daryti kiekviena pusė. Bet ne visada! Jei gavote RpcResult, jis pats tarnauja kaip patvirtinimas. Tai yra, serveris gali atsakyti į jūsų užklausą su „MsgsAck“, pvz., „Gavau“. RpcResult gali reaguoti nedelsiant. Tai gali būti abu.

Ir taip, jūs vis tiek turite atsakyti į atsakymą! Patvirtinimas. Priešingu atveju serveris laikys, kad jis nepristatomas, ir vėl atsiųs jums. Net ir po prisijungimo. Bet čia, žinoma, iškyla laiko praleidimo klausimas. Pažvelkime į juos šiek tiek vėliau.

Tuo tarpu pažvelkime į galimas užklausos vykdymo klaidas.

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

O, kas nors sušuks, čia humaniškesnis formatas – yra linija! Neskubėk. Čia klaidų sąrašas, bet, žinoma, ne visiškai. Iš jo sužinome, kad kodas yra kažkas kaip HTTP klaidos (na, aišku, atsakymų semantikos nepaisoma, kai kur atsitiktinai paskirstomos tarp kodų), o eilutė atrodo taip CAPITAL_LETTERS_AND_NUMBERS. Pavyzdžiui, PHONE_NUMBER_OCCUPIED arba FILE_PART_Х_MISSING. Na, tai yra, jums vis tiek reikės šios eilutės analizuoti. Pavyzdžiui, FLOOD_WAIT_3600 reikš, kad turėsite palaukti valandą ir PHONE_MIGRATE_5, kad telefono numeris su šiuo prefiksu turi būti užregistruotas 5-oje DC. Mes turime tipinę kalbą, tiesa? Mums nereikia argumentų iš eilutės, tiks įprasti, gerai.

Vėlgi, tai nėra paslaugų pranešimų puslapyje, bet, kaip jau įprasta šiame projekte, informaciją galite rasti kitame dokumentacijos puslapyje. Arba mesti įtarimą. Pirma, pažiūrėkite, spausdinimo / sluoksnio pažeidimas - RpcError gali būti įdėtas RpcResult. Kodėl ne lauke? Į ką neatsižvelgėme?.. Atitinkamai, kur garantija, kad RpcError NEGALIMA įterpti RpcResult, bet būti tiesiogiai ar įdėtas į kitą tipą?.. O jei negali, kodėl jis nėra aukščiausio lygio, t.y. jo trūksta req_msg_id ? ..

Bet tęskime apie paslaugų pranešimus. Klientas gali manyti, kad serveris ilgai galvoja ir pateikti šį nuostabų prašymą:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Yra trys galimi atsakymai į šį klausimą, vėlgi susikertantys su patvirtinimo mechanizmu; bandymas suprasti, kokie jie turėtų būti (ir koks yra bendras tipų, kuriems nereikia patvirtinimo) sąrašas, paliekamas skaitytojui kaip namų darbas (pastaba: informacija Telegram Desktop šaltinio kodas nėra baigtas).

Priklausomybė nuo narkotikų: pranešimų būsenos

Apskritai daug kur TL, MTProto ir apskritai Telegram palieka užsispyrimo jausmą, bet iš mandagumo, taktiškumo ir kt. minkšti įgūdžiai Mes mandagiai apie tai tylėjome, o dialoguose esančias nešvankybes cenzūravome. Tačiau ši vietaОdidžioji puslapio dalis yra apie pranešimai apie žinutes Tai šokiruoja net mane, ilgą laiką dirbančią su tinklo protokolais ir mačiusią įvairaus kreivumo dviračius.

Prasideda nepavojingai, patvirtinimais. Toliau jie mums pasakoja apie

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;

Na, o su jais teks susidurti kiekvienam, kuris pradės dirbti su MTProto, cikle „pataisyta – perkompiliuota – paleista“ dažnas dalykas yra gauti skaičių klaidų ar redagavimų metu sugedusią druską. Tačiau čia yra du punktai:

  1. Tai reiškia, kad pradinis pranešimas yra prarastas. Turime sukurti keletą eilių, tai pažiūrėsime vėliau.
  2. Kokie yra šie keisti klaidų skaičiai? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... kur kiti skaičiai, Tomi?

Dokumentuose nurodyta:

Siekiama, kad error_code reikšmės būtų sugrupuotos (error_code >> 4): pavyzdžiui, kodai 0x40 – 0x4f atitinka konteinerio skaidymo klaidas.

bet, pirma, poslinkis kita kryptimi, antra, nesvarbu, kur kiti kodai? Autoriaus galvoje?.. Tačiau tai smulkmenos.

Priklausomybė prasideda pranešimuose apie pranešimų būsenas ir pranešimų kopijas:

  • Pranešimo būsenos informacijos užklausa
    Jei kuri nors šalis kurį laiką negavo informacijos apie savo siunčiamų pranešimų būseną, ji gali jos aiškiai paprašyti kitos šalies:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informacinis pranešimas apie pranešimų būseną
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Čia info yra eilutė, kurioje yra tiksliai vienas pranešimo būsenos baitas kiekvienam pranešimui iš gaunamų msg_ids sąrašo:

    • 1 = nieko nežinoma apie pranešimą (msg_id per mažas, kita šalis galėjo jį pamiršti)
    • 2 = pranešimas negautas (msg_id patenka į saugomų identifikatorių diapazoną; tačiau kita šalis tokio pranešimo tikrai negavo)
    • 3 = pranešimas negautas (msg_id per didelis; tačiau kita šalis jo tikrai dar negavo)
    • 4 = pranešimas gautas (atkreipkite dėmesį, kad šis atsakymas kartu yra ir gavimo patvirtinimas)
    • +8 = pranešimas jau patvirtintas
    • +16 = pranešimas, kurio nereikia patvirtinti
    • +32 = RPC užklausa yra apdorojamame pranešime arba apdorojimas jau baigtas
    • +64 = su turiniu susijęs atsakymas į jau sugeneruotą pranešimą
    • +128 = kita šalis tikrai žino, kad pranešimas jau gautas
      Šis atsakymas nereikalauja patvirtinimo. Tai yra atitinkamo msgs_state_req patvirtinimas.
      Atkreipkite dėmesį, kad jei staiga paaiškėja, kad kita šalis neturi žinutės, kuri atrodo kaip jai išsiųsta, žinutę galima tiesiog išsiųsti iš naujo. Net jei kita šalis turėtų gauti dvi pranešimo kopijas tuo pačiu metu, dublikatas bus ignoruojamas. (Jei praėjo per daug laiko ir pradinis msg_id nebegalioja, pranešimas turi būti supakuotas į msg_copy).
  • Savanoriškas pranešimų būsenos perdavimas
    Bet kuri šalis gali savanoriškai informuoti kitą šalį apie kitos šalies perduodamų pranešimų būseną.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Išplėstas savanoriškas vieno pranešimo būsenos perdavimas
    ...
    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;
  • Aiškus prašymas pakartotinai siųsti pranešimus
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Nutolusi šalis nedelsdama atsako iš naujo išsiųsdama prašomus pranešimus […]
  • Aiškus prašymas pakartotinai siųsti atsakymus
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Nutolusi šalis iš karto atsako siųsdama dar kartą Atsakymai į prašomas žinutes […]
  • Pranešimų kopijos
    Kai kuriais atvejais seną pranešimą su msg_id, kuris nebegalioja, reikia išsiųsti iš naujo. Tada jis suvyniotas į kopijavimo konteinerį:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Gavus pranešimą, jis apdorojamas taip, tarsi įvynioklio ten nebūtų. Tačiau jei tikrai žinoma, kad pranešimas orig_message.msg_id buvo gautas, tada naujas pranešimas nėra apdorojamas (tuo pačiu metu jis ir orig_message.msg_id yra patvirtinami). Orig_message.msg_id reikšmė turi būti mažesnė nei sudėtinio rodinio msg_id.

Net patylėkime ką msgs_state_info vėl kyšo nebaigto TL ausys (mums reikėjo baitų vektoriaus, o apatiniuose dviejuose bituose buvo enum, o aukštesniuose - vėliavėlės). Esmė kitokia. Ar kas nors supranta, kodėl visa tai yra praktikoje? tikrame kliente reikia?.. Sunkiai, bet galima įsivaizduoti kažkokią naudą, jei žmogus užsiima derinimu, o interaktyviu režimu - paklausk serverio kas ir kaip. Bet čia aprašomi prašymai kelionė pirmyn ir atgal.

Iš to seka, kad kiekviena šalis turi ne tik šifruoti ir siųsti žinutes, bet ir saugoti duomenis apie save, apie atsakymus į juos, nežinomą laiką. Dokumentuose neaprašomas nei šių funkcijų laikas, nei praktinis pritaikymas. jokiu būdu. Nuostabiausia tai, kad jie iš tikrųjų naudojami oficialių klientų kode! Matyt, jiems buvo pasakyta tai, kas nebuvo įtraukta į viešą dokumentaciją. Suprask iš kodo kodėl, nebėra toks paprastas kaip TL atveju - tai ne (santykinai) logiškai izoliuota dalis, o gabalas, susietas su aplikacijos architektūra, t.y. reikės žymiai daugiau laiko suprasti programos kodą.

Pingai ir laikai. Eilės.

Iš visko, jei prisimename spėliones apie serverio architektūrą (užklausų pasiskirstymas per backends), seka gana liūdnas dalykas – nepaisant visų pristatymo garantijų TCP (arba duomenys pristatomi, arba būsite informuoti apie spragą, bet duomenys bus pateikti prieš iškilus problemai), kad patvirtinimai pačiame MTProto - jokių garantijų. Serveris gali lengvai pamesti arba išmesti jūsų pranešimą, ir nieko negalima padaryti, tereikia naudoti įvairių tipų ramentus.

Ir pirmiausia – žinučių eilės. Na, o su vienu dalyku viskas buvo akivaizdu nuo pat pradžių – nepatvirtintą žinutę reikia saugoti ir persiųsti. Ir po kurio laiko? Ir juokdarys jį pažįsta. Galbūt tie priklausomi paslaugų pranešimai kažkaip išsprendžia šią problemą su ramentais, tarkime, Telegram Desktop yra apie 4 juos atitinkančios eilės (galbūt daugiau, kaip jau minėta, tam reikia rimčiau įsigilinti į jo kodą ir architektūrą; tuo pačiu Mes žinome, kad jis negali būti paimtas kaip pavyzdys; tam tikras skaičius tipų iš MTProto schemos joje nenaudojamas).

Kodėl tai vyksta? Tikriausiai serverio programuotojai nesugebėjo užtikrinti patikimumo klasterio viduje ar net buferio priekiniame balansuotoje ir perkėlė šią problemą klientui. Iš nevilties Vasilijus bandė įgyvendinti alternatyvų variantą, turėdamas tik dvi eiles, naudodamas TCP algoritmus - išmatuodamas RTT į serverį ir koreguodamas „lango“ dydį (pranešimuose) priklausomai nuo nepatvirtintų užklausų skaičiaus. Tai yra, tokia apytikslė serverio apkrovos įvertinimo euristika yra tai, kiek mūsų užklausų jis gali sukramtyti vienu metu ir neprarasti.

Na, tai yra, jūs suprantate, tiesa? Jei turite dar kartą įdiegti TCP prie protokolo, veikiančio per TCP, tai rodo, kad protokolas yra labai prastai.

O taip, kam reikia daugiau nei vienos eilės ir ką tai vis dėlto reiškia žmogui, dirbančiam su aukšto lygio API? Žiūrėk, jūs pateikiate užklausą, serializuojate ją, bet dažnai negalite jos išsiųsti iš karto. Kodėl? Nes atsakymas bus msg_id, kuri yra laikinaаAš esu etiketė, kurios skyrimą geriausia atidėti kuo vėlesniam laikui – tuo atveju, jei serveris jį atmes dėl laiko tarp mūsų ir jo nesutapimo (žinoma, galime padaryti ramentą, kuris perkelia mūsų laiką nuo dabarties prie serverio pridedant deltą, apskaičiuotą iš serverio atsakymų – oficialūs klientai tai daro, bet tai yra neapdorota ir netiksli dėl buferio). Todėl, kai pateikiate užklausą vietinės funkcijos iškvietimu iš bibliotekos, pranešimas pereina šiuos etapus:

  1. Jis guli vienoje eilėje ir laukia šifravimo.
  2. Paskirtas msg_id ir žinutė pateko į kitą eilę – galimas persiuntimas; siųsti į lizdą.
  3. a) Serveris atsakė MsgsAck - pranešimas buvo pristatytas, mes jį ištriname iš „kitos eilės“.
    b) Arba atvirkščiai, jam kažkas nepatiko, jis atsakė į blogus pranešimus - iš naujo siųsti iš „kitos eilės“
    c) Nieko nežinoma, žinutę reikia išsiųsti iš kitos eilės – bet tiksliai nežinoma kada.
  4. Pagaliau serveris atsakė RpcResult - faktinis atsakymas (arba klaida) - ne tik pristatytas, bet ir apdorotas.

gal, konteinerių naudojimas galėtų iš dalies išspręsti problemą. Tai yra tada, kai krūva pranešimų yra supakuota į vieną, o serveris atsakė patvirtindamas juos visus iš karto, viename msg_id. Tačiau jis taip pat atmes visą šią pakuotę, jei kas nors nutiktų.

Ir šiuo metu atsiranda netechniniai sumetimai. Iš patirties matėme daug ramentų, be to, dabar matysime ir daugiau blogų patarimų ir architektūros pavyzdžių – ar tokiomis sąlygomis verta pasitikėti ir priimti tokius sprendimus? Klausimas retorinis (žinoma, ne).

apie ką mes kalbame? Jei temoje „narkotikų pranešimai apie žinutes“ vis tiek galite spėlioti prieštaravimais, tokiais kaip „tu kvailas, nesupratai mūsų puikaus plano! (tad pirma rašykit dokumentaciją, kaip turėtų normalūs žmonės, su pagrindimu ir paketų keitimo pavyzdžiais, tada kalbėsime), tada laikai/laikai yra grynai praktinis ir konkretus klausimas, viskas čia jau seniai žinoma. Ką dokumentai mums sako apie skirtąjį laiką?

Serveris paprastai patvirtina pranešimo iš kliento gavimą (paprastai RPC užklausą), naudodamas RPC atsakymą. Jei atsakymas laukia ilgai, serveris pirmiausia gali išsiųsti gavimo patvirtinimą, o kiek vėliau – patį RPC atsakymą.

Klientas paprastai patvirtina pranešimo iš serverio gavimą (paprastai RPC atsakymą), pridėdamas patvirtinimą prie kitos RPC užklausos, jei jis nepersiunčiamas per vėlai (jei jis sugeneruojamas, tarkime, praėjus 60–120 sekundžių nuo gavimo). pranešimo iš serverio). Tačiau jei ilgą laiką nėra jokios priežasties siųsti pranešimus į serverį arba yra daug nepatvirtintų pranešimų iš serverio (tarkime, daugiau nei 16), klientas perduoda atskirą patvirtinimą.

... Verčiu: mes patys nežinome, kiek ir kaip mums to reikia, todėl tarkime, kad tebūnie taip.

O apie pingus:

Ping žinutės (PING / PONG)

ping#7abe77ec ping_id:long = Pong;

Atsakymas paprastai grąžinamas į tą patį ryšį:

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

Šiems pranešimams patvirtinti nereikia. Pong yra perduodamas tik atsakant į ping, o ping gali būti inicijuotas bet kurios pusės.

Atidėtas ryšio uždarymas + PING

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

Veikia kaip ping. Be to, gavęs tai, serveris paleidžia laikmatį, kuris po kelių sekundžių uždarys esamą ryšį disconnect_delay, nebent gaus naują to paties tipo pranešimą, kuris automatiškai iš naujo nustato visus ankstesnius laikmačius. Pavyzdžiui, jei klientas šiuos pingus siunčia kartą per 60 sekundžių, jis gali nustatyti disconnect_delay 75 sekundžių.

Ar tu išprotėjęs?! Po 60 sekundžių traukinys įvažiuos į stotį, išlaipins ir pasiims keleivius ir vėl praras ryšį tunelyje. Po 120 sekundžių, kol išgirsite, jis ateis į kitą ir ryšys greičiausiai nutrūks. Na, aišku, iš kur kyla kojos - „girdėjau skambėjimą, bet nežinau, kur“, yra Nagl algoritmas ir TCP_NODELAY parinktis, skirta interaktyviam darbui. Bet, atsiprašau, laikykitės numatytosios vertės - 200 Millisekundžių Jei tikrai norite pavaizduoti kažką panašaus ir sutaupyti galimų poros paketų, atidėkite tai 5 sekundėms arba bet kokiam kitam pranešimo „Vartotojas rašo...“ skirtasis laikas. Bet ne daugiau.

Ir galiausiai, ping. Tai yra, TCP ryšio gyvumo patikrinimas. Juokinga, bet maždaug prieš 10 metų parašiau kritišką tekstą apie mūsų fakulteto bendrabučio pasiuntinį – ten autoriai taip pat atjungė serverį iš kliento, o ne atvirkščiai. Bet 3 kurso studentai yra vienas dalykas, o tarptautinis biuras yra kitas dalykas, tiesa?...

Pirma, nedidelė edukacinė programa. TCP ryšys, jei nėra paketų mainų, gali veikti kelias savaites. Tai ir gerai, ir blogai, priklausomai nuo tikslo. Gerai, jei buvo atidarytas SSH ryšys su serveriu, atsikėlėte nuo kompiuterio, perkrovėte maršrutizatorių, grįžote į savo vietą - seansas per šį serverį nebuvo sugadintas (nieko neįvedėte, nebuvo paketų) , tai patogu. Blogai, jei serveryje yra tūkstančiai klientų, kurių kiekvienas naudoja išteklius (sveiki, Postgres!), o kliento priegloba seniai galėjo būti paleista iš naujo, bet mes apie tai nesužinosime.

Pokalbių/IM sistemos patenka į antrąjį atvejį dėl vienos papildomos priežasties – prisijungimo būsenų. Jei vartotojas „nukrito“, turite apie tai informuoti jo pašnekovus. Priešingu atveju susidursite su klaida, kurią padarė „Jabber“ kūrėjai (ir taisė 20 metų) – vartotojas atsijungė, bet ir toliau rašo jam žinutes, manydami, kad jis yra prisijungęs (kurie taip pat buvo visiškai pasimetę šiuose kelios minutės iki atjungimo aptikimo). Ne, TCP_KEEPALIVE parinktis, kurią daugelis žmonių, kurie nesupranta, kaip veikia TCP laikmačiai, įmeta atsitiktinai (nustatydami laukines reikšmes, pvz., dešimtis sekundžių), čia nepadės – reikia įsitikinti, kad ne tik OS branduolys vartotojo kompiuterio yra gyvas, bet taip pat veikia normaliai, gali reaguoti, ir pati programa (ar manote, kad ji negali užšalti? Telegram Desktop Ubuntu 18.04 man užstojo daugiau nei vieną kartą).

Štai kodėl jūs turite ping serverio klientas, o ne atvirkščiai - jei klientas tai padarys, jei ryšys nutrūks, ping nebus pristatytas, tikslas nebus pasiektas.

Ką matome telegramoje? Tai yra visiškai priešingai! Na, tai yra. Formaliai, žinoma, abi pusės gali pinguoti viena kitai. Praktiškai klientai naudoja ramentus ping_delay_disconnect, kuris nustato laikmatį serveryje. Na, atsiprašau, ne klientas turi nuspręsti, kiek laiko jis nori ten gyventi be pingo. Serveris, atsižvelgdamas į jo apkrovą, žino geriau. Bet, žinoma, jei nesirūpinate ištekliais, būsite sau piktas Pinokis, o ramentas tiks...

Kaip jis turėjo būti suprojektuotas?

Manau, kad minėti faktai aiškiai rodo, kad Telegram/VKontakte komanda nėra labai kompetentinga transporto (ir žemesnio) kompiuterinių tinklų srityje ir jų žema kvalifikacija aktualiais klausimais.

Kodėl tai pasirodė taip sudėtinga ir kaip „Telegram“ architektai gali bandyti prieštarauti? Tai, kad jie bandė padaryti seansą, išgyvenančią TCP ryšio nutrūkimus, t. y. tai, kas nebuvo pristatyta dabar, pristatysime vėliau. Tikriausiai jie taip pat bandė padaryti UDP transportą, bet susidūrė su sunkumais ir jo atsisakė (todėl dokumentacija tuščia - nebuvo kuo girtis). Tačiau dėl to, kad trūksta supratimo apie tai, kaip veikia tinklai apskritai ir konkrečiai TCP, kur galite tuo pasikliauti ir kur reikia tai padaryti patiems (ir kaip), ir dėl bandymo tai derinti su kriptografija „du paukščiai su vienas akmuo“, toks rezultatas.

Kaip reikėjo? Remiantis tuo, msg_id yra laiko žyma, būtina kriptografiniu požiūriu, kad būtų išvengta pakartojimo atakų, yra klaida prie jos pridėti unikalaus identifikatoriaus funkciją. Todėl iš esmės nekeičiant dabartinės architektūros (kai generuojamas naujinimų srautas, tai yra aukšto lygio API tema kitai šios įrašų serijos daliai), reikėtų:

  1. TCP ryšį su klientu turintis serveris prisiima atsakomybę – jei nuskaitė iš lizdo, patvirtinkite, apdorokite arba grąžinkite klaidą, be nuostolių. Tada patvirtinimas nėra ID vektorius, o tiesiog „paskutinis gautas sek_no“ - tik skaičius, kaip ir TCP (du skaičiai - jūsų seka ir patvirtintas). Mes visada dalyvaujame sesijoje, ar ne?
  2. Laiko žyma, skirta užkirsti kelią pakartotinėms atakoms, tampa atskiru lauku, a la nonce. Jis patikrintas, bet nieko kito neturi. Pakankamai ir uint32 - jei mūsų druska keičiasi bent kas pusę dienos, 16 bitų galime skirti žemos eilės sveikosios esamo laiko dalies bitams, likusius - trupmeninei sekundės daliai (kaip ir dabar).
  3. Pašalinta msg_id iš viso - užklausų atskyrimo foninėse sistemose požiūriu, pirma, yra kliento ID, antra, seanso ID, jas sujunkite. Atitinkamai, užklausos identifikatoriaus pakanka tik vieno dalyko seq_no.

Tai taip pat nėra pats sėkmingiausias variantas, kaip identifikatorius galėtų pasitarnauti visiškas atsitiktinumas – beje, tai jau daroma aukšto lygio API siunčiant žinutę. Geriau būtų visiškai perdaryti architektūrą iš santykinės į absoliučią, bet tai jau kitos dalies, o ne šio įrašo tema.

API?

Ta-daam! Taigi, įveikę skausmo ir ramentų kupiną kelią, pagaliau galėjome siųsti bet kokias užklausas į serverį ir gauti bet kokius atsakymus į juos, taip pat gauti atnaujinimus iš serverio (ne pagal užklausą, o jį patį) siunčia mums, pvz., PUSH, jei kas nors yra aiškiau).

Dėmesio, dabar straipsnyje bus vienintelis „Perl“ pavyzdys! (Tiems, kurie nėra susipažinę su sintaksė, pirmasis palaiminimo argumentas yra objekto duomenų struktūra, antrasis yra jo klasė):

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

Taip, ne tyčia spoileris – jei dar neskaitėte, pirmyn ir padarykite!

O, wai~~... kaip tai atrodo? Kažkas labai pažįstamo... gal tai yra tipiškos žiniatinklio API duomenų struktūra JSON, išskyrus tai, kad klasės taip pat pridedamos prie objektų?..

Taigi, taip išeina... Kas tai per, bendražygiai?.. Tiek pastangų – ir sustojome pailsėti ten, kur žiniatinklio programuotojai tik prasideda?..Ar tik JSON per HTTPS nebūtų paprastesnis?! Ką gavome mainais? Ar buvo verta dėti pastangas?

Įvertinkime, ką mums davė TL+MTProto ir kokios galimos alternatyvos. Na, HTTP, kuriame pagrindinis dėmesys skiriamas užklausos ir atsakymo modeliui, netinkamas, bet bent jau kažkas virš TLS?

Kompaktiškas serializavimas. Matydamas šią duomenų struktūrą, panašią į JSON, prisimenu, kad yra dvejetainių jos versijų. Pažymėkime MsgPack kaip nepakankamai išplečiamą, bet yra, pavyzdžiui, CBOR – beje, standartas aprašytas RFC 7049. Jis išsiskiria tuo, kad apibrėžia žymes, kaip išsiplėtimo mechanizmas ir tarp jau standartizuotas galima:

  • 25 + 256 - pasikartojančių eilučių pakeitimas nuoroda į eilutės numerį, toks pigus suspaudimo būdas
  • 26 - suskirstytas Perl objektas su klasės pavadinimu ir konstruktoriaus argumentais
  • 27 - serializuotas nuo kalbos nepriklausomas objektas su tipo pavadinimu ir konstruktoriaus argumentais

Na, aš pabandžiau suskirstyti tuos pačius duomenis TL ir CBOR su įjungtu eilučių ir objektų pakavimu. Rezultatas pradėjo skirtis CBOR naudai kažkur nuo megabaito:

cborlen=1039673 tl_len=1095092

tokiu būdu, išvada: yra daug paprastesnių formatų, kuriems netaikoma sinchronizavimo gedimo ar nežinomo identifikatoriaus problema ir kurių efektyvumas yra panašus.

Greitas ryšio užmezgimas. Tai reiškia nulinį RTT po pakartotinio prisijungimo (kai raktas jau buvo sugeneruotas vieną kartą) - taikoma nuo pat pirmo MTProto pranešimo, bet su tam tikromis išlygomis - pataikyti ta pačia druska, seansas nesupuvęs ir pan. Ką vietoj to mums siūlo TLS? Citata temoje:

Naudojant PFS TLS, TLS seanso bilietai (RFC 5077), kad atnaujintumėte šifruotą seansą iš naujo nesuderindami raktų ir nesaugodami rakto informacijos serveryje. Atidarydamas pirmąjį ryšį ir kurdamas raktus, serveris užšifruoja ryšio būseną ir perduoda ją klientui (seanso bilieto pavidalu). Atitinkamai, kai ryšys atnaujinamas, klientas siunčia seanso bilietą, įskaitant seanso raktą, atgal į serverį. Pats bilietas yra užšifruotas laikinuoju raktu (seanso bilieto raktu), kuris saugomas serveryje ir turi būti paskirstytas tarp visų frontend serverių, apdorojančių SSL grupiniuose sprendimuose.[10] Taigi seanso bilieto įvedimas gali pažeisti PFS, jei pažeidžiami laikinieji serverio raktai, pavyzdžiui, kai jie saugomi ilgą laiką („OpenSSL“, „nginx“, „Apache“ pagal numatytuosius nustatymus juos saugo visą programos veikimo laiką; populiarios svetainės naudoja raktas kelias valandas, iki dienų).

Čia RTT nėra nulis, reikia apsikeisti bent ClientHello ir ServerHello, po kurio klientas gali siųsti duomenis kartu su Finished. Bet čia turėtume prisiminti, kad mes turime ne internetą su naujai atidarytų jungčių krūva, o pasiuntinį, kurio prisijungimas dažnai yra vienas ir daugiau ar mažiau ilgalaikis, palyginti trumpas užklausas į tinklalapius - viskas yra multipleksuota. viduje. Tai yra, visai priimtina, jei nesusidurtume su tikrai bloga metro atkarpa.

Pamiršai dar ką nors? Rašyk komentaruose.

Tęsti!

Antroje šios serijos įrašų dalyje nagrinėsime ne techninius, o organizacinius klausimus – požiūrius, ideologiją, sąsają, požiūrį į vartotojus ir kt. Tačiau remiantis čia pateikta technine informacija.

Trečioje dalyje bus toliau analizuojamas techninis komponentas / kūrimo patirtis. Visų pirma išmoksite:

  • pandemonijos tęsinys su TL tipų įvairove
  • nežinomų dalykų apie kanalus ir supergrupes
  • kodėl dialogai yra blogesni už sąrašą
  • apie absoliutų ir santykinį pranešimų adresavimą
  • kuo skiriasi nuotrauka ir vaizdas
  • kaip jaustukai trukdo rašyti kursyvą

ir kiti ramentai! Sekite naujienas!

Šaltinis: www.habr.com

Добавить комментарий