Telegrami protokolli ja organisatsiooniliste lähenemisviiside kriitika. 1. osa, tehniline: kliendi nullist kirjutamise kogemus - TL, MT

Viimasel ajal on Habres hakanud sagedamini ilmuma postitusi sellest, kui hea on Telegram, kui geniaalsed ja kogenud on vennad Durovid võrgusüsteemide ehitamisel jne. Samal ajal on väga vähesed inimesed tehnilisse seadmesse tõeliselt süvenenud – maksimaalselt kasutavad nad üsna lihtsat (ja MTProtost üsna erinevat) JSON-il põhinevat Bot API-t ja tavaliselt lihtsalt nõustuvad. usu kohta kõik kiitused ja PR, mis sõnumitooja ümber keerlevad. Peaaegu poolteist aastat tagasi hakkas mu kolleeg Esheloni MTÜst Vassili (kahjuks kustutati tema konto Habres koos mustandiga) oma Telegrami klienti Perlis nullist kirjutama ja hiljem liitus ka nende ridade autor. Miks Perl, küsivad mõned kohe? Kuna selliseid projekte on juba teistes keeltes. valmis raamatukogu, ja vastavalt sellele peab autor minema lõpuni algusest. Veelgi enam, krüptograafia on usalduse küsimus, kuid kontrollige. Turvalisusele suunatud toote puhul ei saa lihtsalt loota tootja valmis teegile ja seda pimesi usaldada (see on aga teise osa teema). Hetkel töötab raamatukogu üsna hästi “keskmisel” tasemel (võimaldab teha mis tahes API päringuid).

Krüptograafiat ega matemaatikat selles postitustes aga palju ei ole. Kuid seal on palju muid tehnilisi detaile ja arhitektuurilisi karkusid (kasulik ka neile, kes ei kirjuta nullist, vaid kasutavad raamatukogu mis tahes keeles). Niisiis, peamine eesmärk oli proovida klienti nullist rakendada ametliku dokumentatsiooni järgi. See tähendab, et oletame, et ametlike klientide lähtekood on suletud (jällegi, teises osas käsitleme üksikasjalikumalt selle tõsiasja, et see on tõsi juhtub nii), kuid nagu vanasti, on näiteks selline standard nagu RFC - kas klienti on võimalik kirjutada ainult spetsifikatsiooni järgi, “ilma vaatamata” lähtekoodi, olgu see siis ametlik (Telegram Desktop, mobiil) või mitteametlik Telethon?

Оглавление

Dokumentatsioon... see on olemas, eks? Kas see on tõsi?..

Selle artikli jaoks hakati märkmete fragmente koguma eelmisel suvel. Kogu selle aja ametlikul veebisaidil https://core.telegram.org Dokumentatsioon oli 23. kihi seisuga, st. jäi kuhugi 2014. aastasse kinni (mäletate, siis polnud isegi kanaleid?). Muidugi oleks see teoreetiliselt pidanud võimaldama meil 2014. aastal tolleaegse funktsionaalsusega kliendi juurutada. Kuid isegi sellises seisus oli dokumentatsioon esiteks puudulik ja teiseks läks see kohati iseendaga vastuollu. Veidi üle kuu tagasi, 2019. aasta septembris, oli see juhuslikult Avastati, et saidil on dokumentatsiooni ulatuslik värskendus, täiesti värske Layer 105 jaoks, märkusega, et nüüd tuleb kõik uuesti läbi lugeda. Tõepoolest, palju artikleid muudeti, kuid paljud jäid muutmata. Seetõttu tuleks allolevat kriitikat dokumentatsiooni kohta lugedes meeles pidada, et osa neist asjadest ei ole enam aktuaalsed, aga osad ikka päris. Lõppude lõpuks pole 5 aastat kaasaegses maailmas lihtsalt pikk aeg, vaid väga palju. Alates nendest aegadest (eriti kui te ei arvesta sellest ajast alates kasutusest kõrvaldatud ja taaselustatud geovestlussaite) on API meetodite arv skeemis kasvanud sajalt enam kui kahesajale viiekümnele!

Millest alustada noore autorina?

Pole tähtis, kas kirjutate nullist või kasutate näiteks valmis teeke nagu Telethon Pythoni jaoks või Madeline PHP jaoks, igal juhul vajate kõigepealt registreeri oma taotlus - saada parameetreid api_id и api_hash (Need, kes on VKontakte API-ga töötanud, saavad kohe aru), mille järgi server rakenduse tuvastab. See peavad tehke seda õiguslikel põhjustel, kuid sellest, miks raamatukogu autorid ei saa seda avaldada, räägime pikemalt teises osas. Võite olla rahul testiväärtustega, kuigi need on väga piiratud - tõsiasi on see, et nüüd saate registreeruda ainult üks rakendus, nii et ärge kiirustage sellega ülepeakaela.

Nüüd peaks meid tehnilisest aspektist huvitama see, et pärast registreerimist peaksime saama Telegramilt teateid dokumentatsiooni, protokolli jms uuenduste kohta. See tähendab, et võib eeldada, et dokkidega sait jäeti lihtsalt maha ja jätkas tööd spetsiaalselt nendega, kes hakkasid kliente tegema, sest see on lihtsam. Aga ei, midagi sellist ei täheldatud, infot ei tulnud.

Ja kui nullist kirjutada, siis tegelikult on saadud parameetrite kasutamine veel kaugel. Kuigi https://core.telegram.org/ ja räägib neist jaotises Alustamine, tegelikult peate kõigepealt juurutama MTProto protokoll - aga kui sa usuksid paigutus OSI mudeli järgi lehe lõpus protokolli üldise kirjelduse jaoks, siis on see täiesti asjata.

Tegelikult jääb nii enne kui ka pärast MTProtot mitmel tasandil korraga (nagu OS-i tuumas töötavad välismaised võrgumehed ütlevad, kihi rikkumine) vahele suur, valus ja kohutav teema...

Binaarne serialiseerimine: TL (Type Language) ja selle skeem ning kihid ja paljud muud hirmutavad sõnad

See teema on tegelikult Telegrami probleemide võti. Ja kui proovite sellesse süveneda, on palju kohutavaid sõnu.

Niisiis, siin on diagramm. Kui see sõna teile meelde tuleb, öelge: JSON-skeem, Sa mõtlesid õigesti. Eesmärk on sama: mingi keel võimaliku edastatavate andmete kogumi kirjeldamiseks. Siin sarnasused lõpevad. Kui lehelt MTProto protokoll, või ametliku kliendi lähtepuust, proovime avada mõne skeemi, näeme midagi sellist:

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;

Inimene, kes seda esimest korda näeb, tunneb intuitiivselt ära vaid osa kirjutatust - noh, need on ilmselt struktuurid (kuigi kus on nimi, kas vasakul või paremal?), neis on väljad, mille järel käärsoole järele järgneb tüüp... ilmselt. Siin on nurksulgudes ilmselt sellised mallid nagu C++-s (tegelikult mitte päris). Ja mida tähendavad kõik muud sümbolid, küsimärgid, hüüumärgid, protsendid, räsimärgid (ja ilmselgelt tähendavad need eri kohtades erinevaid asju), mõnikord esinevad ja mõnikord mitte, kuueteistkümnendarvud - ja mis kõige tähtsam, kuidas sellest saada korrapärane (mida server tagasi ei lükka) baidivoogu? Peate lugema dokumentatsiooni (jah, läheduses on JSON-versiooni skeemi lingid, kuid see ei tee asja selgemaks).

Avage leht Binaarandmete serialiseerimine ja sukelduge seente ja diskreetse matemaatika maagilisse maailma, midagi sarnast matanile 4. kursusel. Tähestik, tüüp, väärtus, kombineerija, funktsionaalne kombineerija, normaalvorm, liittüüp, polümorfne tüüp... ja see on kõik alles esimene leht! Järgmine ootab teid TL keel, mis, kuigi sisaldab juba näidet triviaalsest taotlusest ja vastusest, ei anna tüüpilisematele juhtumitele üldse vastust, mis tähendab, et peate läbima vene keelest inglise keelde tõlgitud matemaatika ümberjutustuse veel kaheksal manustatud seadmel. lehekülge!

Funktsionaalkeelte ja automaatse tüübijäreldamisega tuttavad lugejad näevad selles keeles kirjelduskeelt isegi näite põhjal muidugi palju tuttavamana ja võivad öelda, et see pole põhimõtteliselt halb. Vastuväited sellele on järgmised:

  • jah, eesmärk kõlab hästi, aga paraku ta saavutamata
  • Haridus Venemaa ülikoolides on isegi IT-erialade lõikes erinev – kõik pole vastavat kursust läbinud
  • Lõpuks, nagu näeme, praktikas see nii on pole nõutav, kuna kasutatakse ainult piiratud alamhulka isegi kirjeldatud TL-ist

Nagu öeldud LeoNerd kanalil #perl FreeNode IRC võrgus, kes üritas Telegramist Matrixi väravat juurutada (tsitaadi tõlge on mälu järgi ebatäpne):

Tundub, et keegi tutvustas tüübiteooriat esimest korda, sattus vaimustusse ja hakkas sellega mängima, hoolimata sellest, kas seda praktikas vaja on.

Vaadake ise, kas vajadus paljaste tüüpide (int, long jne) kui millegi elementaarse järele küsimusi ei tekita - lõppkokkuvõttes tuleb need käsitsi realiseerida - näiteks proovime neist tuletada vektor. See on tegelikult massiiv, kui nimetada saadud asju õigete nimedega.

Aga enne

TL süntaksi alamhulga lühikirjeldus neile, kes ametlikku dokumentatsiooni ei loe

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;

Määratlus algab alati disainer, mille järel valikuliselt (praktikas - alati) sümboli kaudu # peab olema CRC32 seda tüüpi normaliseeritud kirjeldusstringist. Järgmisena tuleb väljade kirjeldus; kui need on olemas, võib tüüp olla tühi. See kõik lõpeb võrdusmärgiga, selle tüübi nimega, millesse see konstruktor - see on tegelikult alamtüüp - kuulub. Võrdsusmärgist paremal olev mees on polümorfsed - see tähendab, et sellele võib vastata mitu konkreetset tüüpi.

Kui definitsioon esineb pärast rida ---functions---, siis jääb süntaks samaks, kuid tähendus on erinev: konstruktorist saab RPC funktsiooni nimi, väljadest parameetrid (noh, see jääb täpselt samaks antud struktuuriks, nagu allpool kirjeldatud , on see lihtsalt määratud tähendus) ja "polümorfne tüüp" - tagastatud tulemuse tüüp. Tõsi, see jääb siiski polümorfseks – just jaotises määratletud ---types---, kuid seda konstruktorit "ei võeta arvesse". Kutsutud funktsioonide tüüpide ülekoormamine nende argumentidega, st. Millegipärast ei ole TL-is ette nähtud mitut sama nime, kuid erineva signatuuriga funktsiooni, nagu C++-s.

Miks "konstruktor" ja "polümorfne", kui see pole OOP? Noh, tegelikult on kellelgi lihtsam mõelda sellele OOP terminites - polümorfne tüüp kui abstraktne klass ja konstruktorid on selle otsesed järeltulijad ja final mitmete keelte terminoloogias. Tegelikult muidugi ainult siin sarnasus reaalsete ülekoormatud konstruktorimeetoditega OO programmeerimiskeeltes. Kuna siin on lihtsalt andmestruktuurid, siis meetodeid ei ole (kuigi funktsioonide ja meetodite edasine kirjeldamine on üsna võimeline tekitama peas segadust, et need on olemas, aga see on hoopis teine ​​asi) - konstruktorit kui väärtust võib mõelda mis ehitatakse tüüp baidivoo lugemisel.

Kuidas see juhtub? Deserialiseerija, mis loeb alati 4 baiti, näeb väärtust 0xcrc32 - ja saab aru, mis edasi saab field1 tüübiga int, st. loeb täpselt 4 baiti, sellel peal olev väli koos tüübiga PolymorType lugeda. Näeb 0x2crc32 ja mõistab, et esiteks on kaks välja edasi long, mis tähendab, et loeme 8 baiti. Ja siis jälle komplekstüüp, mis on samamoodi deserialiseeritud. Näiteks, Type3 võiks deklareerida ringkonnas kohe, kui vastavalt kaks konstruktorit, siis peavad nad kohtuma kas 0x12abcd34, mille järel peate lugema veel 4 baiti intVõi 0x6789cdef, mille järel pole enam midagi. Kõik muu - peate tegema erandi. Igatahes läheme pärast seda tagasi 4 baiti lugemise juurde int marginaalid field_c в constructorTwo ja sellega lõpetame oma lugemise PolymorType.

Lõpuks, kui vahele jääte 0xdeadcrc eest constructorThree, siis läheb kõik keerulisemaks. Meie esimene valdkond on bit_flags_of_what_really_present tüübiga # - tegelikult on see lihtsalt tüübi varjunimi nat, mis tähendab "loomulikku numbrit". See tähendab, et tegelikult on unsigned int, muide, ainus juhtum, kui reaalsetes ahelates esinevad märgita numbrid. Niisiis, järgmine on küsimärgiga konstruktsioon, mis tähendab, et see väli - see on juhtmes olemas ainult siis, kui viidatud väljale on määratud vastav bitt (umbes nagu kolmikoperaator). Oletame, et see bitt on määratud, mis tähendab, et edasi peame lugema välja nagu Type, millel meie näites on 2 konstruktorit. Üks on tühi (koosneb ainult identifikaatorist), teisel on väli ids tüübiga ids:Vector<long>.

Võib arvata, et nii mallid kui ka geneerilised andmed on plussis või Javas. Kuid mitte. Peaaegu. See üksi nurksulgude kasutamise korral reaalsetes ahelates ja seda kasutatakse AINULT Vectori jaoks. Baitivoos on need 4 CRC32 baiti tüübi Vector enda jaoks, alati samad, seejärel 4 baiti - massiivi elementide arv ja seejärel need elemendid ise.

Lisage siia fakt, et serialiseerimine toimub alati 4-baidiste sõnadega, kõik tüübid on selle kordsed - kirjeldatakse ka sisseehitatud tüüpe bytes и string koos pikkuse käsitsi järjestamisega ja selle joondusega 4-ga - noh, tundub, et see kõlab normaalselt ja isegi suhteliselt tõhusalt? Kuigi väidetavalt on TL tõhus binaarne serialiseerimine, kas JSON on siiski palju paksem, kui peaaegu kõike, isegi Boole'i ​​väärtusi ja ühemärgilisi stringe laiendatakse 4 baidini? Vaata, isegi mittevajalikud väljad saab bitilippudega vahele jätta, kõik on päris hea ja isegi tuleviku jaoks laiendatav, miks mitte hiljem konstruktorisse uusi valikulisi välju lisada?..

Aga ei, kui te ei loe mitte minu lühikirjeldust, vaid täielikku dokumentatsiooni ja mõtlete rakendamisele. Esiteks arvutatakse konstruktori CRC32 skeemi tekstikirjelduse normaliseeritud rea järgi (eemaldage ekstra tühik jne) - seega uue välja lisamisel muutub tüübi kirjeldusrida ja sellest tulenevalt ka selle CRC32 ja , järelikult serialiseerimine. Ja mida teeks vana klient, kui ta saaks uute lippudega välja ja ta ei tea, mida nendega edasi teha?

Teiseks, meenutagem CRC32, mida kasutatakse siin sisuliselt kui räsifunktsioonid üheselt määrata, millist tüüpi (de)serialiseeritakse. Siin seisame silmitsi kokkupõrgete probleemiga – ja ei, tõenäosus ei ole üks 232-st, vaid palju suurem. Kes mäletas, et CRC32 on mõeldud sidekanali vigade tuvastamiseks (ja parandamiseks) ning vastavalt sellele parandab neid omadusi teiste kahjuks? Näiteks ei hooli see baitide ümberkorraldamisest: kui arvutate CRC32 kahest reast, vahetate teises esimesed 4 baiti järgmise 4 baidiga - see on sama. Kui meie sisendiks on ladina tähestiku tekstistringid (ja veidi kirjavahemärke) ja need nimed pole eriti juhuslikud, suureneb sellise ümberpaigutamise tõenäosus oluliselt.

Muide, kes kontrollis, mis seal oli? tegelikult CRC32? Ühel varasel lähtekoodil (isegi enne Waltmanit) oli räsifunktsioon, mis korrutas iga tähemärgi arvuga 239, mis on nende inimeste poolt nii armastatud, ha ha!

Lõpuks, okei, saime aru, et väljatüübiga konstruktorid Vector<int> и Vector<PolymorType> on erinev CRC32. Aga on-line jõudlus? Ja teoreetilisest vaatenurgast, kas see muutub tüübi osaks? Oletame, et edastame kümne tuhande numbri massiivi, hästi Vector<int> kõik on selge, pikkus ja veel 40000 XNUMX baiti. Mis siis, kui see Vector<Type2>, mis koosneb ainult ühest väljast int ja see on tüübis üksi - kas peame kordama 10000xabcdef0 34 4 korda ja seejärel XNUMX baiti int, või on keel võimeline seda meie jaoks konstruktorist SÕLTUMA fixedVec ja 80000 40000 baiti asemel kanda jälle ainult XNUMX XNUMX?

See ei ole üldse tühine teoreetiline küsimus - kujutage ette, et saate grupi kasutajate nimekirja, kellel kõigil on ID, eesnimi, perekonnanimi - mobiilside kaudu edastatava andmemahu erinevus võib olla märkimisväärne. Meile reklaamitakse just Telegrami serialiseerimise tõhusust.

Nii et ...

Vektor, mida kunagi välja ei antud

Kui proovite liikuda läbi kombinaatorite ja muu sellise kirjelduse lehekülgi, näete, et vektorit (ja isegi maatriksit) üritatakse formaalselt väljastada mitme lehe korruste kaudu. Kuid lõpuks nad unustavad, viimane samm jäetakse vahele ja antakse lihtsalt vektori definitsioon, mis pole veel tüübiga seotud. Mis viga? Keeltes programmeerimine, eriti funktsionaalseid, on üsna tüüpiline kirjeldada struktuuri rekursiivselt - kompilaator oma laisa hinnanguga saab kõigest aru ja teeb ise. Keeles andmete serialiseerimine vaja on TÕHUSUST: piisab lihtsalt kirjeldamisest nimekiri, st. kahe elemendi struktuur - esimene on andmeelement, teine ​​on sama struktuur ise või tühi ruum saba jaoks (pakk (cons) Lispi keeles). Kuid see nõuab ilmselgelt iga element kulutab oma tüübi kirjeldamiseks veel 4 baiti (TL-i puhul CRC32). Massiivi saab ka lihtsalt kirjeldada fikseeritud suurus, kuid eelnevalt teadmata pikkusega massiivi puhul katkestame.

Seega, kuna TL ei võimalda vektorit väljastada, tuli see lisada kõrvale. Lõpuks ütleb dokument:

Serialiseerimisel kasutatakse alati sama konstruktorit "vektorit" (const 0x1cb5c415 = crc32 ("vector t:Type # [ t ] = Vector t"), mis ei sõltu t-tüüpi muutuja konkreetsest väärtusest.

Valikulise parameetri t väärtus ei ole serialiseerimisega seotud, kuna see tuletatakse tulemuse tüübist (alati teada enne deserialiseerimist).

Vaata lähemalt: vector {t:Type} # [ t ] = Vector t - aga kusagil pole See definitsioon ise ei ütle, et esimene arv peab olema võrdne vektori pikkusega! Ja see ei tule kuskilt. See on antud, mida tuleb meeles pidada ja oma kätega rakendada. Mujal on dokumentatsioonis isegi ausalt öeldud, et tüüp pole päris:

Vektori t polümorfne pseudotüüp on "tüüp", mille väärtus on mis tahes tüüpi t väärtuste jada, kas karbis või tühi.

... aga ei keskendu sellele. Kui olete väsinud matemaatika venitamisest (võib-olla isegi ülikooli kursuselt teada) otsustate alla anda ja vaatate, kuidas sellega praktikas töötada, jääb pähe mulje, et see on tõsine. Matemaatika keskmes, selle leiutasid selgelt Cool People (kaks matemaatikut – ACM võitja), mitte keegi. Eesmärk – eputada – on täidetud.

Muide, numbri kohta. Tuletame teile seda meelde # see on sünonüüm nat, naturaalarv:

On tüüpväljendeid (tüüp-ekspr) ja numbrilised avaldised (nat-ekspr). Neid määratletakse aga samamoodi.

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

aga grammatikas on neid kirjeldatud samamoodi, s.t. Seda erinevust tuleb jälle meeles pidada ja käsitsi ellu viia.

Jah, mallitüübid (vector<int>, vector<User>) neil on ühine identifikaator (#1cb5c415), st. kui teate, et kõnest teatatakse kui

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

siis ei oota te enam pelgalt vektorit, vaid kasutajate vektorit. Täpsemalt, peaks oota - reaalses koodis on igal elemendil, kui mitte paljatüübil, konstruktor ja heas mõttes oleks juurutamisel vaja kontrollida - aga meile saadeti täpselt selle vektori igas elemendis seda tüüpi? Mis siis, kui see oleks mingi PHP, mille massiiv võib erinevates elementides sisaldada erinevat tüüpi?

Siinkohal hakkad mõtlema – kas selline TL on vajalik? Äkki oleks käru jaoks võimalik kasutada inimserialisaatorit, sama protobufi, mis siis juba olemas oli? See oli teooria, vaatame praktikat.

Olemasolevad TL-rakendused koodis

TL sündis VKontakte sügavuses juba enne kuulsaid sündmusi Durovi aktsia müügiga ja (kindlasti), isegi enne Telegrami arendamise algust. Ja avatud lähtekoodiga esimese teostuse lähtekood võite leida palju naljakaid karkusid. Ja keel ise rakendati seal täielikumalt kui praegu Telegramis. Näiteks räsi ei kasutata skeemis üldse (see tähendab sisseehitatud pseudotüüpi (nagu vektorit), millel on hälbiv käitumine). Või

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

aga kaalugem täielikkuse huvides nii-öelda Mõttehiiglase arengu jälgimist.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Või see ilus:

    static const char *reserved_words_polymorhic[] = {

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

      };

See fragment käsitleb selliseid malle nagu:

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

See on hashmapi malli tüübi määratlus int-tüüpi paaride vektorina. C++ puhul näeks see välja umbes selline:

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

nii, alpha - märksõna! Kuid ainult C++-s saab kirjutada T, aga tuleks kirjutada alfa, beeta... Aga mitte rohkem kui 8 parameetrit, sellega fantaasia lõpeb. Näib, et kunagi ammu Peterburis toimusid sellised dialoogid:

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

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

Kuid see puudutas TL-i esimest avaldatud rakendust "üldiselt". Liigume edasi Telegrami klientide endi rakenduste kaalumisele.

Sõna Vassilile:

Vassili, [09.10.18 17:07] Eelkõige on perse kuum sellepärast, et nad lõid hunniku abstraktsioone, lõid seejärel poldi külge ja katsid koodigeneraatori karkudega
Selle tulemusena kõigepealt doki pilot.jpg
Seejärel koodist dzhekichan.webp

Algoritmide ja matemaatikaga kursis olevatelt inimestelt võime muidugi eeldada, et nad on lugenud Ahot, Ullmanni ja on tuttavad tööriistadega, mis on aastakümnete jooksul muutunud tööstuses de facto standardiks oma DSL-kompilaatorite kirjutamiseks, eks?

Autor telegramm-kli on Vitali Valtman, nagu võib aru saada TLO formaadi esinemisest väljaspool selle (kli) piire, meeskonna liige - nüüd on eraldatud teek TL parsimiseks eraldi, mis mulje temast on jäänud TL parser? ..

16.12 04:18 Vassili: Ma arvan, et keegi ei valdanud lex+yacc
16.12 04:18 Vassili: Ma ei oska seda teisiti seletada
16.12 04:18 Vassili: no või maksti neile VK liinide arvu eest
16.12 04:19 Vassili: 3k+ rida jne.<censored> parseri asemel

Võib-olla erand? Vaatame, kuidas teeb See on AMETLIK klient – ​​Telegrami töölaud:

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

Pythonis 1100+ rida, paar regulaaravaldist + erijuhud nagu vektor, mis on skeemis muidugi deklareeritud nii nagu TL süntaksi järgi olema peab, aga nad toetusid sellele süntaksile selle sõelumisel... Tekib küsimus, miks see kõik oli ime?иSee on kihilisem, kui keegi seda niikuinii dokumentatsiooni järgi parsima ei hakka?!

Muide... Mäletate, et me rääkisime CRC32 kontrollist? Niisiis on Telegrami töölaua koodigeneraatoris loend eranditest nende tüüpide jaoks, milles arvutatud CRC32 ei klapi skeemil näidatuga!

Vassili, [18.12/22 49:XNUMX] ja siin ma mõtleks, kas sellist TL-i on vaja
kui ma tahaksin jamada alternatiivsete rakendustega, hakkaksin sisestama reavahetusi, pooled parserid katkevad mitmerealiste definitsioonide puhul
tdesktop aga ka

Pidage meeles punkt ühe laineri kohta, me pöördume selle juurde veidi hiljem tagasi.

Olgu, telegram-cli on mitteametlik, Telegram Desktop on ametlik, aga kuidas on teistega? Kes teab?.. Androidi kliendikoodis polnud üldse skeemi parserit (mis tekitab küsimusi avatud lähtekoodiga, aga see on teise osa kohta), kuid seal oli mitmeid muid naljakaid koodijuppe, kuid neist rohkem alljaotises.

Milliseid muid küsimusi serialiseerimine praktikas tõstatab? Näiteks tegid nad palju asju, muidugi bitiväljade ja tingimuslike väljadega:

Vassili: flags.0? true
tähendab, et väli on olemas ja on võrdne tõene, kui lipp on seatud

Vassili: flags.1? int
tähendab, et väli on olemas ja seda tuleb deserialiseerida

Vassili: Perse, ära muretse selle pärast, mida teed!
Vassili: Dokumendis on kuskil mainitud, et tõsi on tühi nullpikkusega tüüp, kuid nende dokumendist on võimatu midagi kokku panna
Vassili: Avatud lähtekoodiga rakendustes see samuti nii ei ole, kuid seal on hunnik karkusid ja tugesid

Aga Telethon? Vaadates ette MTProto teemat, näide - dokumentatsioonis on sellised jupid, aga märk % seda kirjeldatakse ainult kui “antud paljastüübile vastavat”, st. allolevates näidetes on viga või midagi dokumenteerimata:

Vassili, [22.06.18 18:38] Ühes kohas:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Teisel kujul:

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

Ja need on kaks suurt erinevust, päriselus tuleb mingi alasti vektor

Ma ei ole näinud paljast vektordefinitsiooni ega ole ka kohanud

Analüüs on kirjutatud käsitsi telethonis

Tema diagrammil on definitsioon välja kommenteeritud msg_container

Jälle jääb küsimus % kohta. Seda ei kirjeldata.

Vadim Gontšarov, [22.06.18 19:22] ja tdesktopis?

Vassili, [22.06.18 19:23] Kuid tõenäoliselt ei söö seda ka nende tavamootorite TL-parser

// parsed manually

TL on ilus abstraktsioon, keegi ei rakenda seda täielikult

Ja % pole nende skeemi versioonis

Aga siin läheb dokumentatsioon iseendaga vastuollu, nii et idk

See leiti grammatikast, nad võisid lihtsalt semantika kirjeldamise unustada

Sa nägid dokumenti TL-is, ilma poole liitrita ei saa sellest aru

"Noh, ütleme," ütleb teine ​​lugeja, "te kritiseerite midagi, nii et näidake mulle, kuidas seda tuleks teha."

Vassili vastab: "Mis puutub parserisse, siis mulle meeldivad sellised asjad nagu

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

kuidagi rohkem meeldib kui

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

või

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

see on KOGU lekser:

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

need. lihtsam on pehmelt öeldes."

Üldiselt mahub tegelikult kasutatud TL-i alamhulga parser ja koodigeneraator ligikaudu 100 grammatikareale ja ~300 generaatori reale (loendades kõiki print'i loodud kood), sealhulgas tüübiteabe kuklid igas klassis enesevaatluseks. Iga polümorfne tüüp muutub tühjaks abstraktseks baasklassiks ja konstruktorid pärivad sellest ning neil on meetodid serialiseerimiseks ja deserialiseerimiseks.

Tüüpide puudumine tüübikeeles

Tugev trükkimine on hea, eks? Ei, see pole holivar (kuigi ma eelistan dünaamilisi keeli), vaid postulaat TL-i raames. Sellest lähtuvalt peaks keel meile igasuguseid tšekke pakkuma. No okei, võib-olla mitte tema ise, vaid teostus, aga ta peaks neid vähemalt kirjeldama. Ja milliseid võimalusi me tahame?

Esiteks piirangud. Siin näeme failide üleslaadimise dokumentatsioonis:

Seejärel jagatakse faili binaarne sisu osadeks. Kõik osad peavad olema sama suurusega ( osa_suurus ) ja peavad olema täidetud järgmised tingimused:

  • part_size % 1024 = 0 (jagutav 1 kB-ga)
  • 524288 % part_size = 0 (512 kB peab olema ühtlaselt jagatav osa suurusega)

Viimane osa ei pea nendele tingimustele vastama, kui selle suurus on väiksem kui osa_suurus.

Igal osal peaks olema järjenumber, faili_osa, mille väärtus jääb vahemikku 0 kuni 2,999.

Pärast faili jaotamist peate valima selle serverisse salvestamise meetodi. Kasutage upload.saveBigFilePart juhul kui faili täissuurus on üle 10 MB ja upload.saveFilePart väiksemate failide jaoks.
[…] võidakse tagastada üks järgmistest andmesisestusvigadest:

  • FILE_PARTS_INVALID — vigane osade arv. Väärtus ei ole vahel 1..3000

Kas see on diagrammil? Kas see on TL-i abil kuidagi väljendatav? Ei. Kuid vabandust, isegi vanaisa Turbo Pascal suutis kirjeldatud tüüpe kirjeldada vahemikud. Ja ta teadis veel üht asja, nüüd paremini tuntud kui enum - tüüp, mis koosneb fikseeritud (väikese) arvu väärtuste loendist. Sellistes keeltes nagu C - numbriline, pange tähele, et seni oleme rääkinud ainult tüüpidest numbrid. Aga on ka massiive, stringe... näiteks oleks tore kirjeldada, et see string tohib sisaldada ainult telefoninumbrit, eks?

Ükski neist pole TL-is. Kuid see on olemas näiteks JSON-skeemis. Ja kui keegi veel vaidleks 512 KB jaguvuse üle, et see tuleb ikka koodis kontrollida, siis veenduge, et klient lihtsalt ei saanud saatke vahemikust väljas number 1..3000 (ja vastavat viga poleks saanud tekkida) oleks ju võimalik ju?..

Muide, vigade ja tagastatavate väärtuste kohta. Isegi need, kes on TL-iga töötanud, ähmastavad silmi – see ei tulnud meile kohe pähe igaüks TL-i funktsioon võib tegelikult tagastada mitte ainult kirjeldatud tagastustüübi, vaid ka vea. Kuid seda ei saa TL-i enda abil kuidagi järeldada. Muidugi on see juba selge ja praktikas pole midagi vaja (kuigi tegelikult saab RPC-d teha erineval viisil, tuleme selle juurde hiljem tagasi) - aga kuidas on abstraktsete tüüpide matemaatika mõistete puhtusega taevasest maailmast?.. võtsin puksiiri üles - nii sobitage.

Ja lõpuks, kuidas on lood loetavusega? Noh, üldiselt ma tahaksin kirjeldus kas see on skeemis õige (JSON-skeemis jällegi), aga kui olete sellega juba kurnatud, siis kuidas on lood praktilise poolega – vähemalt triviaalne värskenduste ajal erinevusi vaadata? Vaadake ise aadressil tõelisi näiteid:

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

või

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

Oleneb igaühest, aga näiteks GitHub keeldub nii pikkade ridade sees toimuvaid muudatusi esile tõstmast. Mäng “leia 10 erinevust” ja mida aju kohe näeb, on see, et mõlema näite algus ja lõpp on samad, tuleb tüütult kuskilt keskelt lugeda... Minu arust pole see ainult teoorias, aga puhtalt visuaalselt räpane ja lohakas.

Muide, teooria puhtusest. Miks me vajame bitivälju? Kas ei tundu, et nemad lõhn tüübiteooria seisukohalt halb? Seletusi võib näha diagrammi varasemates versioonides. Alguses jah, nii see oli, iga aevastamise jaoks tekkis uus tüüp. Need alged on sellisel kujul endiselt olemas, näiteks:

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;

Kuid kujutage nüüd ette, kui teie struktuuris on 5 valikulist välja, siis on kõigi võimalike valikute jaoks vaja 32 tüüpi. Kombinatoorne plahvatus. Seega purunes TL-teooria kristallpuhtus serialiseerimise karmi reaalsuse malmist tagumikku.

Lisaks rikuvad need tüübid mõnes kohas ise oma tüpoloogiat. Näiteks MTProtos (järgmine peatükk) saab vastuse kokku suruda Gzipiga, kõik on korras – välja arvatud see, et kihte ja vooluringi rikutakse. Taaskord ei saagitud mitte RpcResulti ennast, vaid selle sisu. No miks seda teha?.. pidin karku lõikama, et kompressioon töötaks igal pool.

Või teine ​​näide, kord avastasime vea – see saadeti InputPeerUser asemel InputUser. Või vastupidi. Aga see töötas! See tähendab, et server ei hoolinud tüübist. Kuidas see saab olla? Vastuse võivad meile anda telegramm-cli koodifragmendid:

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

Teisisõnu, siin tehakse serialiseerimist KÄSITSI, pole loodud koodi! Võib-olla on server sarnaselt realiseeritud?.. Põhimõtteliselt see toimib, kui seda üks kord teha, aga kuidas seda hiljem uuenduste käigus toetada? Kas see on põhjus, miks see skeem leiutati? Ja siit liigume edasi järgmise küsimuse juurde.

Versioonide koostamine. Kihid

Miks skeemiversioone kihtideks nimetatakse, saab vaid oletada avaldatud skeemide ajaloo põhjal. Ilmselt arvasid autorid algul, et põhilisi asju saab teha muutmata skeemi järgi ja ainult vajaduse korral, konkreetsete taotluste korral, näitavad, et neid tehakse teistsuguse versiooniga. Põhimõtteliselt isegi hea idee - ja uus on justkui “segatud”, kihiti vana peale. Aga vaatame, kuidas seda tehti. Tõsi, ma ei saanud seda algusest peale vaadata - see on naljakas, kuid aluskihi diagrammi lihtsalt pole. Kihid algasid 2-ga. Dokumentatsioon räägib meile TL erifunktsioonist:

Kui klient toetab kihti 2, tuleb kasutada järgmist konstruktorit:

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

Praktikas tähendab see, et enne iga API kõnet int väärtusega 0x289dd1f6 tuleb lisada enne meetodi numbrit.

Kõlab normaalselt. Aga mis edasi sai? Siis ilmus

invokeWithLayer3#b7475268 query:!X = X;

Mis siis edasi saab? Nagu arvata võis,

invokeWithLayer4#dea0d430 query:!X = X;

Naljakas? Ei, naerda on veel vara, mõelge sellele igaüks teise kihi päring tuleb sellisesse erilisse pakkida - kui need kõik on sinu jaoks erinevad, siis kuidas sa neid muidu eristad? Ja ainult 4 baiti ette lisamine on üsna tõhus meetod. Niisiis,

invokeWithLayer5#417a57ae query:!X = X;

Aga on ilmselge, et mõne aja pärast saab sellest mingi bakhhanaalia. Ja lahendus tuli:

Värskendus: alates kihist 9, abimeetoditest invokeWithLayerN saab kasutada ainult koos initConnection

Hurraa! Pärast 9 versiooni jõudsime lõpuks selleni, mida tehti Interneti-protokollides veel 80ndatel – leppisime versioonis kokku ühe korra ühenduse alguses!

Mis siis edasi saab? ..

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

Aga nüüd saab veel naerda. Alles peale veel 9 kihti lisandus lõpuks ka versiooninumbriga universaalne konstruktor, mida tuleb ühenduse alguses kutsuda vaid üks kord ja kihtide tähendus oli justkui kadunud, nüüd on see vaid tingimuslik versioon, nagu Igalpool mujal. Probleem lahendatud.

Täpselt?...

Vassili, [16.07.18 14:01] Isegi reedel mõtlesin:
Teleserver saadab sündmusi ilma päringuta. Taotlused peavad olema ümbritsetud InvokeWithLayeriga. Server ei pakenda värskendusi; vastuste ja värskenduste mähkimiseks puudub struktuur.

Need. klient ei saa määrata kihti, mille värskendusi ta soovib

Vadim Gontšarov, [16.07.18 14:02] kas InvokeWithLayer pole põhimõtteliselt kark?

Vassili, [16.07.18 14:02] See on ainus viis

Vadim Gontšarov, [16.07.18 14:02] mis sisuliselt peaks tähendama kihi kokkuleppimist seansi alguses

Muide, sellest järeldub, et kliendi madalamat versiooni ei pakuta

Uuendused, st. tüüp Updates skeemis saadab server selle kliendile mitte vastusena API päringule, vaid sündmuse toimumisel iseseisvalt. See on keeruline teema, millest räägitakse teises postituses, kuid praegu on oluline teada, et server salvestab värskendused ka siis, kui klient on võrguühenduseta.

Seega, kui keeldute mähkimast iga pakett selle versiooni näitamiseks, põhjustab see loogiliselt järgmisi võimalikke probleeme:

  • server saadab kliendile uuendused juba enne, kui klient on teavitanud, millist versiooni ta toetab
  • mida peaksin tegema pärast kliendi uuendamist?
  • kes garantiidet serveri arvamus kihi numbri kohta protsessi käigus ei muutu?

Kas arvate, et see on puhtalt teoreetiline spekulatsioon ja praktikas seda juhtuda ei saa, kuna server on õigesti kirjutatud (vähemalt on see hästi testitud)? ha! Ükskõik kuidas see ka poleks!

Just sellega me augustis kokku puutusime. 14. augustil tulid teated, et midagi uuendatakse Telegrami serverites... ja siis logides:

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.

ja siis mitu megabaiti virnajälgi (noh, samal ajal sai logimine parandatud). Lõppude lõpuks, kui teie TL-is midagi ei tuvastata, on see allkirja järgi binaarne KÕIK läheb, muutub dekodeerimine võimatuks. Mida peaksite sellises olukorras tegema?

Noh, esimene asi, mis kellelegi meelde tuleb, on katkestada ühendus ja proovida uuesti. Ei aidanud. Googeldame CRC32 - need osutusid objektideks skeemilt 73, kuigi töötasime 82 kallal. Vaatame hoolikalt logisid - seal on kahe erineva skeemi identifikaatorid!

Võib-olla on probleem puhtalt meie mitteametlikus kliendis? Ei, käivitame Telegram Desktopi 1.2.17 (versiooni, mis on saadaval paljudes Linuxi distributsioonides), see kirjutab erandite logisse: MTP Ootamatu tüübi ID #b5223b0f loe MTPMessageMediast…

Telegrami protokolli ja organisatsiooniliste lähenemisviiside kriitika. 1. osa, tehniline: kliendi nullist kirjutamise kogemus - TL, MT

Google näitas, et ühe mitteametliku kliendiga oli sarnane probleem juba juhtunud, kuid siis olid versiooninumbrid ja vastavalt ka eeldused erinevad...

Mida me siis tegema peaksime? Vassili ja mina läksime lahku: ta üritas vooluringi 91-le värskendada, otsustasin paar päeva oodata ja proovida 73-ga. Mõlemad meetodid töötasid, kuid kuna need on empiirilised, siis pole arusaamist, mitut versiooni üles või alla vajate. hüpata või kui kaua peate ootama .

Hiljem suutsin olukorra taasesitada: käivitame kliendi, lülitame selle välja, kompileerime skeemi uuesti teisele kihile, taaskäivitame, tabame probleemi uuesti, naaseme eelmise juurde - oih, vooluringi ei vahetata ja klient taaskäivitab hetkeks paar minutit aitab. Saate erinevate kihtide andmestruktuure.

Selgitus? Nagu erinevate kaudsete sümptomite põhjal võite arvata, koosneb server paljudest erinevat tüüpi protsessidest erinevates masinates. Tõenäoliselt pani "puhverdamise" eest vastutav server järjekorda selle, mille ülemused talle andsid, ja nad andsid selle genereerimise ajal kehtinud skeemi järgi. Ja kuni see järjekord pole "mäda", ei saanud sellega midagi teha.

Võib-olla... aga see on jube kark?!.. Ei, enne kui hulludele ideedele mõelda, vaatame ametlike klientide koodeksit. Androidi versioonis ei leia me ühtegi TL-parserit, küll aga (de)serialiseerimisega kopsakat faili (GitHub keeldub seda puudutamast). Siin on koodilõigud:

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;

või

    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... näeb metsik välja. Aga ilmselt on see genereeritud kood, siis olgu?... Aga kindlasti toetab see kõiki versioone! Tõsi, pole selge, miks kõik on kokku segatud, salajased vestlused ja kõikvõimalikud _old7 kuidagi ei näe välja nagu masinate genereerimine... Siiski, kõige rohkem olin sellest vaimustuses

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Poisid, kas te ei suuda isegi otsustada, mis on ühe kihi sees?! No okei, ütleme, et "kaks" lasti veaga lahti, noh, juhtub, aga KOLM?.. Kohe jälle sama reha? Mis pornograafia see on, vabandust?

Muide, Telegram Desktopi lähtekoodis juhtub sarnane asi - kui jah, siis mitu järjestikust skeemi sisseviimist ei muuda selle kihi numbrit, vaid parandavad midagi. Tingimustes, kus skeemi jaoks puudub ametlik andmeallikas, kust neid saab, välja arvatud ametliku kliendi lähtekood? Ja kui sealt võtta, siis ei saa enne kõigi meetodite katsetamist kindel olla, et skeem on täiesti õige.

Kuidas seda üldse testida saab? Loodan, et üksuse-, funktsionaalsete ja muude testide fännid jagavad seda kommentaarides.

Olgu, vaatame veel ühte koodilõiku:

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;

See "käsitsi loodud" kommentaar viitab sellele, et ainult osa sellest failist kirjutati käsitsi (kas kujutate ette kogu hoolduse õudusunenägu?) ja ülejäänu on masinaga loodud. Siis aga tekib teine ​​küsimus – et allikad on olemas mitte täielikult (a la GPL plekid Linuxi tuumas), aga see on juba teise osa teema.

Aga piisavalt. Liigume edasi protokolli juurde, mille peal kogu see serialiseerimine jookseb.

MT Proto

Niisiis, avame üldkirjeldus и protokolli üksikasjalik kirjeldus ja esimene asi, mille otsa komistame, on terminoloogia. Ja kõige üleküllusega. Üldiselt näib see olevat Telegrami patenteeritud funktsioon – asjade nimetamine erinevates kohtades erinevalt või erinevate asjade nimetamine ühe sõnaga või vastupidi (näiteks kõrgetasemelises API-s, kui näete kleebisepakki, siis see pole nii mida sa arvasid).

Näiteks "sõnum" ja "seanss" tähendavad siin midagi muud kui tavalises Telegrami kliendiliideses. Noh, sõnumiga on kõik selge, seda võib tõlgendada OOP terminites või nimetada lihtsalt sõnaks "pakett" - see on madal transporditase, seal pole samu sõnumeid, mis liideses, teenuseteateid on palju . Aga seanss... aga kõigepealt asjad kõigepealt.

transpordikiht

Esimene asi on transport. Nad räägivad meile 5 võimalusest:

  • TCP
  • Veebipesa
  • Websocket HTTPS-i kaudu
  • HTTP
  • HTTPS

Vassili, [15.06.18 15:04] On olemas ka UDP transport, kuid see pole dokumenteeritud

Ja TCP kolmes variandis

Esimene on sarnane UDP-ga TCP kaudu, iga pakett sisaldab järjenumbrit ja crc-d
Miks on kärus dokumentide lugemine nii valus?

Noh, seal see nüüd on TCP juba 4 variandis:

  • lühendatud
  • Kesktaseme
  • Polsterdatud vaheosa
  • Täis

Noh, ok, polsterdatud vaheaine MTProxy jaoks, see lisati hiljem tuntud sündmuste tõttu. Aga milleks veel kaks versiooni (kokku kolm), kui ühega saaks hakkama? Kõik neli erinevad sisuliselt ainult peamise MTProto pikkuse ja kasuliku koormuse määramise poolest, mida arutatakse edasi:

  • lühendis on see 1 või 4 baiti, kuid mitte 0xef, siis keha
  • Intermediate'is on see 4 baiti pikk ja väli ning esimest korda peab klient saatma 0xeeeeeeee näitamaks, et see on keskmine
  • Täielikult kõige sõltuvust tekitavam, võrgupidaja seisukohast: pikkus, järjenumber ja MITTE SEE, mis on peamiselt MTProto, keha, CRC32. Jah, kõik see on TCP peal. Mis tagab meile usaldusväärse transpordi järjestikuse baitivoo kujul; pole vaja jadasid, eriti kontrollsummasid. Olgu, nüüd vaidleb keegi mulle vastu, et TCP-l on 16-bitine kontrollsumma, nii et andmed rikutakse. Suurepärane, kuid tegelikult on meil krüptograafiline protokoll, mille räsi on pikem kui 16 baiti, kõik need vead – ja veelgi enam – jäävad kinni SHA mittevastavusele kõrgemal tasemel. Peale selle pole CRC32-l mõtet.

Võrrelgem lühendit, mille pikkus on üks bait, Intermediate'iga, mis õigustab "Juhul, kui on vaja 4-baidist andmete joondust", mis on üsna jama. Mis, arvatakse, et Telegrami programmeerijad on nii ebakompetentsed, et ei suuda lugeda andmeid pistikupesast joondatud puhvrisse? Seda pead ikka tegema, sest lugemine võib sulle tagastada suvalise arvu baite (ja on ka näiteks puhverservereid...). Või teisest küljest, miks blokeerida lühendatud funktsioon, kui meil on 16 baidi peale endiselt kopsakas polsterdus – säästke 3 baiti mõnikord ?

Jääb mulje, et Nikolai Durovile meeldib väga ilma tegeliku praktilise vajaduseta rattaid, sealhulgas võrguprotokolle uuesti leiutada.

Muud transpordivõimalused, sh. Veeb ja MTProxy, me praegu ei kaalu, võib-olla mõnes teises postituses, kui on taotlus. Selle sama MTProxy kohta pidagem nüüd vaid meeles, et vahetult pärast selle avaldamist 2018. aastal õppisid pakkujad kiiresti seda blokeerima, mis oli mõeldud möödaviigu blokeeriminePoolt pakendi suurus! Ja ka asjaolu, et C-keeles kirjutatud MTProxy server (taas Waltmani poolt) oli liialt seotud Linuxi spetsiifikaga, kuigi seda ei nõutud üldse (Phil Kulin kinnitab), ja et sarnane server kas Go või Node.js-s mahub vähem kui sajale reale.

Kuid nende inimeste tehnilise kirjaoskuse kohta teeme järeldused osa lõpus, pärast muude küsimuste kaalumist. Praegu liigume edasi OSI kihi 5, seansi juurde – millele nad paigutasid MTProto seansi.

Võtmed, sõnumid, seansid, Diffie-Hellman

Nad paigutasid selle sinna mitte täiesti õigesti... Seanss ei ole sama seanss, mis on nähtav liideses jaotises Aktiivsed seansid. Aga järjekorras.

Telegrami protokolli ja organisatsiooniliste lähenemisviiside kriitika. 1. osa, tehniline: kliendi nullist kirjutamise kogemus - TL, MT

Nii saime transpordikihilt teadaoleva pikkusega baidistringi. See on kas krüpteeritud sõnum või lihttekst – kui oleme veel võtmekokkuleppe staadiumis ja teeme seda ka tegelikult. Millistest mõistetest, mida nimetatakse võtmeks, me räägime? Teeme selle probleemi Telegrami meeskonna jaoks selgeks (vabandan, et kell 4 hommikul väsinud ajuga enda dokumentatsiooni inglise keelest tõlkisin, lihtsam oli jätta mõned fraasid nii nagu nad on):

Nimetatakse kahte olemit istung - üks ametlike klientide kasutajaliideses jaotises "praegused seansid", kus iga seanss vastab tervele seadmele / OS-ile.
Teine on MTProto seanss, milles on kirja järjekorranumber (madala taseme tähenduses) ja mis võib kesta erinevate TCP-ühenduste vahel. Samaaegselt saab installida mitu MTProto seanssi, näiteks failide allalaadimise kiirendamiseks.

Nende kahe vahel istungid on kontseptsioon luba. Mandunud juhul võime seda öelda UI seanss on sama nagu luba, aga paraku on kõik keeruline. Vaatame:

  • Esmalt loob uue seadme kasutaja auth_key ja seob selle kontoga, näiteks SMS-i teel – sellepärast luba
  • See juhtus esimese sees MTProto seanss, millel on session_id enda sees.
  • Selles etapis kombinatsioon luba и session_id võiks nimetada Näiteks - see sõna esineb mõne kliendi dokumentatsioonis ja koodis
  • Seejärel saab klient avada mõned MTProto seansid sama all auth_key - samale alalisvoolule.
  • Seejärel peab klient ühel päeval faili taotlema teine ​​DC - ja selle DC jaoks genereeritakse uus auth_key !
  • Teavitada süsteemi, et see ei ole uus kasutaja, kes registreerub, vaid sama luba (UI seanss), kasutab klient API-kõnesid auth.exportAuthorization kodus DC-s auth.importAuthorization uues DC-s.
  • Kõik on sama, mitu võib olla avatud MTProto seansid (igaühel oma session_id) sellele uuele alalisvoolule, all tema auth_key.
  • Lõpuks võib klient soovida täiuslikku edasisaladust. iga auth_key oli püsiv võti - DC kohta - ja klient saab helistada auth.bindTempAuthKey kasutamiseks ajutine auth_key - ja jälle ainult üks temp_auth_key DC kohta, kõigile ühine MTProto seansid sellele DC-le.

Pange tähele sool (ja tulevased soolad) on samuti üks auth_key need. jagatud kõigi vahel MTProto seansid samale alalisvoolule.

Mida tähendab "erinevate TCP-ühenduste vahel"? Nii et see tähendab midagi sellist autoriseerimisküpsis veebisaidil - see säilitab (elab) palju TCP-ühendusi antud serveriga, kuid ühel päeval läheb see halvasti. Ainult erinevalt HTTP-st nummerdatakse ja kinnitatakse MTProtos seansisisesed sõnumid järjestikku, tunnelisse sisenemisel ühendus katkes – pärast uue ühenduse loomist saadab server lahkesti kõik selles seansis, mida ta eelmisel korral ei edastanud. TCP ühendus.

Ülaltoodud teave on aga kokku võetud pärast mitu kuud kestnud uurimist. Kas vahepeal rakendame oma klienti nullist? - lähme tagasi algusesse.

Nii et genereerime auth_key edasi Diffie-Hellmani versioonid Telegramist. Proovime dokumentatsioonist aru saada...

Vassili, [19.06.18 20:05] data_with_hash := SHA1(andmed) + andmed + (mis tahes juhuslikud baidid); nii, et pikkus on 255 baiti;
krüptitud_andmed := RSA(andmed_koos_räsi, serveri_avalik_võti); 255-baidine arv (big endian) tõstetakse nõutava mooduli üle nõutava võimsuseni ja tulemus salvestatakse 256-baidise arvuna.

Neil on dope DH

Ei näe välja nagu terve inimese DH
Dx-is pole kahte avalikku võtit

Noh, lõpuks sai see asi korda tehtud, aga jääk jäi - töö tõendab klient, et ta suutis numbrit arvestada. DoS-rünnakute vastase kaitse tüüp. Ja RSA-võtit kasutatakse ainult üks kord ühes suunas, peamiselt krüptimiseks new_nonce. Kuid kuigi see pealtnäha lihtne toiming õnnestub, millega peate silmitsi seisma?

Vassili, [20.06.18/00/26 XNUMX:XNUMX] Ma pole veel rakenduse taotluseni jõudnud

Saatsin selle taotluse DH-le

Ja transpordidokis on kirjas, et see suudab vastata 4-baidise veakoodiga. See on kõik

Noh, ta ütles mulle -404, mis siis?

Ütlesin talle: "Võtke oma jama, mis on krüpteeritud serverivõtmega sellise sõrmejäljega, ma tahan DH-d," ja see vastas rumala 404-ga.

Mida arvate sellest serveri vastusest? Mida teha? Küsida pole kelleltki (aga sellest pikemalt teises osas).

Siin tehakse kogu huvi dokil

Mul pole muud teha, unistasin lihtsalt numbrite edasi-tagasi teisendamisest

Kaks 32-bitist numbrit. Pakkisin need nagu kõik teisedki

Aga ei, need kaks tuleb enne reale lisada kui BE

Vadim Gontšarov, [20.06.18 15:49] ja selle tõttu 404?

Vassili, [20.06.18 15:49] JAH!

Vadim Gontšarov, [20.06.18 15:50] nii et ma ei saa aru, mida ta "ei leidnud"

Vassili, [20.06.18 15:50] ligikaudu

Ma ei leidnud sellist jaotust algteguriteks)

Me isegi ei haldanud veateadet

Vassili, [20.06.18 20:18] Oh, seal on ka MD5. Juba kolm erinevat räsi

Võtme sõrmejälg arvutatakse järgmiselt.

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

SHA1 ja sha2

Nii et paneme selle auth_key saime Diffie-Hellmani abil 2048 bitti suuruse. Mis järgmiseks? Järgmisena avastame, et selle võtme madalamaid 1024 bitti ei kasutata mingil moel... aga mõelgem praegu sellele. Selles etapis on meil serveriga jagatud saladus. Loodud on TLS-i seansi analoog, mis on väga kulukas protseduur. Kuid server ei tea ikka veel midagi sellest, kes me oleme! Tegelikult veel mitte. volitus. Need. kui mõtlesite sisselogimisparoolile, nagu kunagi ICQ-s, või vähemalt sisselogimisvõtmele, nagu SSH-s (näiteks mõnes gitlabis/githubis). Saime anonüümse. Mis siis, kui server ütleb meile, et "neid telefoninumbreid teenindab teine ​​DC"? Või isegi "teie telefoninumber on keelatud"? Parim, mida saame teha, on hoida võtit alles lootuses, et sellest on kasu ega lähe selleks ajaks mäda.

Muide, me “saasime” selle reservatsioonidega. Näiteks kas me usaldame serverit? Mis siis, kui see on võlts? Krüptograafiline kontroll oleks vajalik:

Vassili, [21.06.18 17:53] Nad pakuvad mobiiliklientidele võimalust kontrollida 2kbit numbri esmatähtsust%)

Aga see pole üldse selge, nafeijoa

Vassili, [21.06.18 18:02] Dokumendis ei ole kirjas, mida teha, kui see ei ole lihtne

Pole öeldud. Vaatame, mida ametlik Androidi klient sel juhul teeb? A see on mis (ja jah, kogu fail on huvitav) - nagu öeldakse, jätan selle siia:

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

Ei, see on muidugi alles mõned Arvude ürgsuse testid on olemas, aga isiklikult pole mul enam piisavalt teadmisi matemaatikast.

Olgu, meil on põhivõti. Sisselogimiseks, st. päringute saatmiseks peate AES-i abil täiendavalt krüpteerima.

Sõnumivõti on määratletud kui 128 sõnumi keha SHA256 keskmist bitti (sealhulgas seanss, sõnumi ID jne), sealhulgas täitebaidid, millele on lisatud autoriseerimisvõtmest võetud 32 baiti.

Vassili, [22.06.18 14:08] Keskmine, lits

Vastu võetud auth_key. Kõik. Peale nende... see ei selgu dokumendist. Uurige julgelt avatud lähtekoodi.

Pange tähele, et MTProto 2.0 nõuab 12–1024 baiti täitmist, kuid tingimusel, et saadud sõnumi pikkus jagub 16 baidiga.

Niisiis, kui palju polsterdust peaksite lisama?

Ja jah, vea korral on ka 404

Kui keegi uuris hoolikalt dokumentatsiooni skeemi ja teksti, märkas ta, et seal pole MAC-i. Ja et AES-i kasutatakse teatud IGE-režiimis, mida mujal ei kasutata. Nad muidugi kirjutavad sellest oma KKK-s... Siin on näiteks sõnumivõti ise ka dekrüpteeritud andmete SHA-räsi, mida kasutatakse terviklikkuse kontrollimiseks - ja mittevastavuse korral mingil põhjusel dokumentatsiooni soovitab neid vaikselt ignoreerida (aga kuidas on lood turvalisusega, mis siis, kui nad meid lõhuvad?).

Ma ei ole krüptograaf, võib-olla pole sellel režiimil antud juhul teoreetilisest küljest midagi halba. Kuid ma võin selgelt nimetada praktilise probleemi, kasutades näitena Telegram Desktopi. See krüpteerib kohalikku vahemälu (kõik need D877F783D5D3EF8C) samamoodi nagu MTProto (ainult antud juhul versioon 1.0) sõnumid, st. kõigepealt sõnumi võti, seejärel andmed ise (ja kusagil kõrvale peamine suur auth_key 256 baiti, ilma milleta msg_key kasutu). Seega muutub probleem märgatavaks suurte failide puhul. Nimelt tuleb andmetest alles jätta kaks koopiat – krüpteerituna ja dekrüpteerituna. Ja kui on megabaite või näiteks voogedastusvideot?.. Klassikalised skeemid, kus MAC on pärast šifriteksti, võimaldavad teil seda vooge lugeda, kohe edastades. Kuid MTProtoga peate seda tegema algul krüptida või dekrüpteerida kogu sõnum, alles seejärel edastada see võrku või kettale. Seetõttu on Telegram Desktopi uusimates versioonides vahemälus user_data Kasutatakse ka teist vormingut - AES-iga CTR-režiimis.

Vassili, [21.06.18 01:27] Oh, ma sain teada, mis on IGE: IGE oli esimene katse luua "autentiv krüpteerimisrežiim", algselt Kerberose jaoks. See oli ebaõnnestunud katse (see ei paku terviklikkuse kaitset) ja see tuli eemaldada. See oli 20-aastase töötava autentiva krüpteerimisrežiimi otsimise algus, mis hiljuti kulmineerus selliste režiimidega nagu OCB ja GCM.

Ja nüüd argumendid vankri poolelt:

Telegrami meeskond eesotsas Nikolai Duroviga koosneb kuuest ACM-i meistrist, kellest pooled on matemaatika doktorid. MTProto praeguse versiooni kasutuselevõtuks kulus neil umbes kaks aastat.

See on naljakas. Kaks aastat madalamal tasemel

Või võite lihtsalt tls-i võtta

Olgu, oletame, et oleme krüptimise ja muud nüansid teinud. Kas lõpuks on võimalik saata TL-is serialiseeritud päringuid ja vastuseid deserialiseerida? Mida ja kuidas siis saata? Siin on näiteks meetod initConnection, võib-olla see on see?

Vassili, [25.06.18 18:46] Initsialiseerib ühenduse ja salvestab teabe kasutaja seadmesse ja rakendusse.

See aktsepteerib parameetrid app_id, device_model, system_version, app_version ja lang_code.

Ja mõni päring

Dokumentatsioon nagu alati. Uurige julgelt avatud lähtekoodi

Kui invokeWithLayeriga oli kõik ligikaudu selge, siis mis siin valesti on? Selgub, oletame, et meil on - kliendil oli juba midagi, mille kohta serverilt küsida - on päring, mille tahtsime saata:

Vassili, [25.06.18 19:13] Koodi järgi otsustades on esimene kõne selle jama sisse mässitud ja jama ise on mähitud invokewithlayeri

Miks ei võiks initConnection olla eraldi kõne, vaid peab olema ümbris? Jah, nagu selgus, tuleb seda teha iga kord iga seansi alguses ja mitte üks kord, nagu põhiklahvi puhul. Aga! Volitamata kasutaja ei saa sellele helistada! Nüüd oleme jõudnud etappi, kus see on kohaldatav See dokumentatsioonileht – ja see ütleb meile, et...

Volitamata kasutajatele on saadaval ainult väike osa API meetoditest.

  • 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

Kõige esimene neist, auth.sendCode, ja seal on see hinnatud esimene päring, milles saadame api_id ja api_hash ning pärast mida saame SMS-i koodiga. Ja kui oleme vales alalisvoolus (näiteks selle riigi telefoninumbreid teenindab mõni teine), saame veateate soovitud alalisvoolu numbriga. Aidake meid, et teada saada, millise IP-aadressiga DC-numbri järgi peate ühenduse looma help.getConfig. Kui korraga oli sissekandeid vaid 5, siis pärast 2018. aasta kuulsaid sündmusi on see arv oluliselt kasvanud.

Nüüd meenutagem, et jõudsime sellesse etappi serveris anonüümselt. Kas lihtsalt IP-aadressi hankimine pole liiga kallis? Miks mitte teha seda ja muid toiminguid MTProto krüptimata osas? Kuulen vastulauset: "kuidas saame olla kindlad, et valeaadressidega ei vastaks RKN?" Selleks me mäletame, et üldiselt ametlikud kliendid RSA-võtmed on manustatud, st. kas sa saad lihtsalt telli see informatsioon. Tegelikult tehakse seda juba blokeeringust möödahiilimise teabe saamiseks, mida kliendid saavad teiste kanalite kaudu (loogiliselt MTProtos endas seda teha ei saa, samuti on vaja teada, kuhu ühendada).

OKEI. Kliendi autoriseerimise praeguses etapis ei ole me veel volitatud ega ole oma taotlust registreerinud. Tahame praegu lihtsalt näha, mida server vastab volitamata kasutajale saadaolevatele meetoditele. Ja siin…

Vassili, [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;

Skeemis on esimene teine

Tdesktopi skeemis on kolmas väärtus

Jah, sellest ajast alates on dokumentatsiooni loomulikult uuendatud. Kuigi see võib varsti jälle ebaoluliseks muutuda. Kuidas peaks algaja arendaja teadma? Võib-olla teavitavad nad teid, kui registreerite oma avalduse? Vassili tegi seda, kuid paraku ei saatnud nad talle midagi (jällegi, me räägime sellest teises osas).

...Märkasite, et oleme juba kuidagi üle läinud API-le, st. järgmisele tasemele ja jäi MTProto teemas midagi kahe silma vahele? Ei mingit üllatust:

Vassili, [28.06.18 02:04] Mm, nad tuhnivad mõnes e2e algoritmis

Mtproto defineerib mõlema domeeni krüpteerimisalgoritmid ja võtmed, samuti veidi ümbrisstruktuuri

Kuid nad segavad pidevalt virna erinevaid tasemeid, nii et pole alati selge, kus mtproto lõppes ja järgmine tase algas

Kuidas need segunevad? Noh, siin on sama ajutine võti näiteks PFS-i jaoks (muide, Telegram Desktop ei saa seda teha). See täidetakse API päringuga auth.bindTempAuthKey, st. tipptasemelt. Kuid samal ajal segab see krüptimist madalamal tasemel - näiteks pärast seda peate seda uuesti tegema initConnection jne, see pole nii lihtsalt tavaline taotlus. Eriline on ka see, et teil võib olla ainult ÜKS ajutine võti alalisvoolu kohta, kuigi väli auth_key_id igas kirjas lubab võtit muuta vähemalt iga sõnumi puhul ja et serveril on õigus ajutine võti igal ajal “unustada” – dokumentatsioonis ei ole kirjas, mida sellisel juhul teha... no miks võiks Kas teil pole mitut võtit, nagu tulevaste soolade komplekti puhul, ja?...

MTProto teema puhul on veel mõned asjad, mida tasub tähele panna.

Sõnumiteated, msg_id, msg_seqno, kinnitused, pingid vales suunas ja muud eripärad

Miks sa pead neist teadma? Kuna need "lekivad" kõrgemale tasemele ja API-ga töötades peate neist teadlik olema. Oletame, et msg_key meid ei huvita; madalam tase on kõik meie eest dekrüpteerinud. Kuid dekrüpteeritud andmete sees on meil järgmised väljad (ka andmete pikkus, nii et me teame, kus polster asub, kuid see pole oluline):

  • sool - int64
  • seansi_id – int64
  • sõnumi_id – int64
  • seq_no - int32

Tuletame meelde, et kogu alalisvoolu jaoks on ainult üks sool. Miks temast teada? Mitte ainult sellepärast, et on taotlus get_future_salts, mis ütleb teile, millised intervallid kehtivad, aga ka sellepärast, et kui teie sool on "mäda", läheb sõnum (taotlus) lihtsalt kaduma. Server teatab uuest soolast loomulikult väljastades new_session_created - aga vanaga tuleb see näiteks kuidagi uuesti saata. Ja see probleem mõjutab rakenduse arhitektuuri.

Serveril on lubatud seansid üldse katkestada ja sel viisil vastata mitmel põhjusel. Mis on tegelikult MTProto seanss kliendi poolelt? Need on kaks numbrit session_id и seq_no sõnumeid selle seansi jooksul. No ja loomulikult selle aluseks olev TCP-ühendus. Oletame, et meie klient ei tea ikka veel, kuidas paljusid asju teha, ta katkestas ühenduse ja lõi uuesti ühenduse. Kui see juhtus kiiresti - vana seanss jätkus uues TCP-ühenduses, suurendage seq_no edasi. Kui see võtab kaua aega, võib server selle kustutada, sest tema poolel on see ka järjekord, nagu saime teada.

Mis see peaks olema seq_no? Oh, see on keeruline küsimus. Proovige ausalt aru saada, mida silmas peetakse:

Sisuga seotud sõnum

Sõnum, mis nõuab selgesõnalist kinnitust. Need hõlmavad kõiki kasutajateateid ja paljusid teenuseteateid, peaaegu kõik, välja arvatud konteinerid ja kinnitused.

Sõnumi järjekorranumber (msg_seqno)

32-bitine arv, mis võrdub kahekordse "sisuga seotud" sõnumite arvuga (need, mis nõuavad kinnitust, ja eriti need, mis ei ole konteinerid), mille saatja on loonud enne seda sõnumit ja mida suurendatakse seejärel ühe võrra, kui praegune sõnum on sisuga seotud sõnum. Mahuti genereeritakse alati pärast kogu selle sisu; seetõttu on selle järjenumber suurem või võrdne selles sisalduvate teadete järjenumbritega.

Mis tsirkus see on, kus juurdekasv on 1 ja siis teine ​​2?.. Kahtlustan, et algselt mõeldi "ACK-i jaoks kõige vähem olulist bitti, ülejäänu on arv", kuid tulemus pole päris sama - eelkõige tuleb välja, saab saata mõned samad kinnitused seq_no! Kuidas? Noh, näiteks server saadab meile midagi, saadab selle ja me ise vaikime, vastates ainult teenuseteadetega, mis kinnitavad tema sõnumite kättesaamist. Sel juhul on meie väljaminevate kinnituste väljaminevate kinnituste number sama. Kui olete TCP-ga tuttav ja arvate, et see kõlab kuidagi metsikult, kuid see ei tundu väga metsik, sest TCP-s seq_no ei muutu, aga kinnitus läheb seq_no teisel pool ma kiirustan teid häirima. Kinnitused on esitatud MTProtos EI edasi seq_no, nagu TCP-s, kuid poolt msg_id !

Mis see on msg_id, kõige olulisem neist valdkondadest? Unikaalne sõnumi identifikaator, nagu nimigi ütleb. See on määratletud kui 64-bitine arv, mille madalaimatel bittidel on jällegi "server-mitte-server" maagia ja ülejäänud on Unixi ajatempel, sealhulgas murdosa, mis on nihutatud 32 bitti vasakule. Need. ajatempel iseenesest (ja sõnumid, mille aeg on liiga erinev, lükkab server tagasi). Sellest selgub, et üldiselt on tegemist kliendi jaoks globaalse identifikaatoriga. Arvestades seda – meenutagem session_id - meile on garanteeritud: Ühele seansile mõeldud sõnumit ei saa mingil juhul saata teise seansi. St selgub, et juba on kolm tase - seanss, seansi number, sõnumi ID. Miks selline ülekeerutamine, see mõistatus on väga suur.

Niisiis, msg_id vaja selleks...

RPC: päringud, vastused, vead. Kinnitused.

Nagu olete ehk märganud, pole skeemil kuskil spetsiaalset tüüpi "tee RPC päring" ega funktsiooni, kuigi vastused on olemas. Meil on ju sisuga seotud sõnumeid! See on, kõik sõnum võib olla palve! Või mitte olla. Pealegi, iga on msg_id. Kuid vastused on olemas:

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

Siin näidatakse, millisele sõnumile see vastus on. Seetõttu peate API tipptasemel meeles pidama, mis oli teie päringu number - arvan, et pole vaja selgitada, et töö on asünkroonne ja korraga võib pooleli olla mitu päringut, mille vastuseid saab suvalises järjekorras tagastada? Põhimõtteliselt saab sellest ja veateadetest nagu tööliste puudumine jälgida selle taga olevat arhitektuuri: teiega TCP-ühendust hoidev server on esiotsa tasakaalustaja, edastab päringud taustaprogrammidele ja kogub need tagasi message_id. Tundub, et siin on kõik selge, loogiline ja hea.

Jah?.. Ja kui järele mõelda? RPC vastusel endal on ju ka väli msg_id! Kas me peame serverile karjuma "sa ei vasta minu vastusele!"? Ja jah, mis seal kinnitustes oli? Teave lehe kohta sõnumid sõnumite kohta ütleb meile, mis on

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

ja seda peavad tegema mõlemad pooled. Aga mitte alati! Kui saite RpcResulti, toimib see ise kinnitusena. See tähendab, et server saab teie päringule vastata sõnumiga MsgsAck – näiteks "Sain selle kätte." RpcResult saab kohe reageerida. See võib olla mõlemat.

Ja jah, vastusele tuleb ikka vastata! Kinnitamine. Vastasel juhul loeb server selle kättetoimetamatuks ja saadab selle teile uuesti tagasi. Isegi pärast taasühendamist. Aga siin kerkib loomulikult ajavõtete küsimus. Vaatame neid veidi hiljem.

Vahepeal vaatame võimalikke päringu täitmise vigu.

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

Oh, keegi hüüab, siin on inimlikum formaat - seal on rida! Võta aega. Siin vigade loend, kuid loomulikult mitte täielik. Sellest saame teada, et kood on midagi sellist HTTP vead (no muidugi ei peeta kinni vastuste semantikast, kohati jaotuvad need koodide vahel juhuslikult) ja rida näeb välja selline CAPITAL_LETTERS_AND_NUMBERS. Näiteks PHONE_NUMBER_OCCUPIED või FILE_PART_Х_PUUDUB. Noh, see tähendab, et teil on seda rida ikkagi vaja sõeluda. Näiteks FLOOD_WAIT_3600 tähendab, et peate ootama tund aega ja PHONE_MIGRATE_5, et selle eesliitega telefoninumber tuleb registreerida 5. DC-s. Meil on tüübikeel, eks? Me ei vaja stringi argumente, tavalised sobivad, olgu.

Jällegi ei ole seda teenuseteadete lehel, kuid nagu selle projekti puhul juba tavaks, leiate teavet teisel dokumentatsiooni lehel. Or kahtlustama. Esiteks, vaata, trükkimine/kihi rikkumine – RpcError saab sisse pesastada RpcResult. Miks mitte väljas? Millega me ei arvestanud?.. Kus on vastavalt sellele garantii RpcError EI tohi olla sisse manustatud RpcResult, aga olema otse või pesastatud mõnes muus tüübis?.. Ja kui ei saa, siis miks ei ole see tipptasemel, st. see on puudu req_msg_id ? ..

Kuid jätkame teenindussõnumitega. Klient võib arvata, et server mõtleb pikalt ja esitada selle imelise taotluse:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Sellele küsimusele on kolm võimalikku vastust, mis ristuvad taas kinnitusmehhanismiga, püüdes aru saada, millised need peaksid olema (ja millised on üldine loetelu tüüpidest, mis ei vaja kinnitust), jäetakse lugejale kodutööks (märkus: teave Telegrami töölaua lähtekood pole täielik).

Narkomaania: sõnumi staatused

Üldiselt jätavad paljud kohad TL-is, MTProtos ja Telegramis üldiselt kangekaelsuse tunde, aga viisakusest, taktitundest ja muust pehmed oskused Me vaikisime sellest viisakalt ja tsenseerisime dialoogides esinenud roppusi. Samas see kohtОsuurem osa lehest on umbes sõnumid sõnumite kohta See on šokeeriv isegi minu jaoks, kes ma olen pikka aega võrguprotokollidega töötanud ja näinud erineva kõverusega jalgrattaid.

See algab kahjutult, kinnitustega. Järgmisena räägivad nad meile sellest

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;

Eks kõik, kes MTProtoga tööd alustavad, peavad nendega tegelema, tsüklis “parandatud – uuesti kompileeritud – käivitatud” on numbrivigade või toimetuste käigus halvaks läinud soola saamine tavaline asi. Siiski on siin kaks punkti:

  1. See tähendab, et algne sõnum on kadunud. Peame looma mõned järjekorrad, vaatame seda hiljem.
  2. Mis on need kummalised veanumbrid? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... kus on teised numbrid, Tommy?

Dokumentatsioonis on kirjas:

Eesmärk on veakoodi väärtused rühmitada (error_code >> 4): näiteks koodid 0x40 — 0x4f vastavad vigadele konteineri lagunemisel.

aga esiteks nihe teises suunas ja teiseks pole vahet, kus on teised koodid? Autori peas?.. Need on siiski pisiasjad.

Sõltuvus saab alguse sõnumite olekutest ja sõnumikoopiatest:

  • Sõnumi oleku teabe taotlus
    Kui kumbki osapool ei ole mõnda aega saanud teavet oma väljaminevate sõnumite oleku kohta, võib ta seda teiselt poolelt selgesõnaliselt nõuda:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Teabesõnum sõnumite oleku kohta
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Siin info on string, mis sisaldab täpselt ühe baidi sõnumi olekut iga sissetuleva msg_ids loendi kirja kohta:

    • 1 = sõnumi kohta pole midagi teada (msg_id on liiga madal, teine ​​pool võib selle unustada)
    • 2 = sõnumit ei saadud (msg_id jääb salvestatud identifikaatorite vahemikku, kuid teine ​​pool pole kindlasti sellist sõnumit saanud)
    • 3 = sõnumit ei saadud kätte (msg_id on liiga kõrge, kuid teine ​​pool pole seda kindlasti veel kätte saanud)
    • 4 = sõnum vastu võetud (pange tähele, et see vastus on samal ajal ka kättesaamise kinnitus)
    • +8 = teade on juba kinnitatud
    • +16 = teade ei vaja kinnitust
    • +32 = RPC-päring sisaldub sõnumis, mida töödeldakse või töötlemine on juba lõppenud
    • +64 = sisuga seotud vastus juba loodud sõnumile
    • +128 = teine ​​osapool teab kindlalt, et teade on juba vastu võetud
      See vastus ei nõua kinnitust. See on iseenesest asjakohase msgs_state_req kinnitus.
      Pane tähele, et kui ootamatult selgub, et teisel poolel pole sõnumit, mis näib talle saadetud, saab sõnumi lihtsalt uuesti saata. Isegi kui teine ​​pool peaks sõnumist kaks koopiat korraga saama, ignoreeritakse duplikaati. (Kui on möödunud liiga palju aega ja algne msg_id ei kehti enam, tuleb sõnum pakkida kausta msg_copy).
  • Sõnumite oleku vabatahtlik teavitamine
    Kumbki pool võib vabatahtlikult teavitada teist osapoolt teise poole edastatud sõnumite staatusest.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Ühe sõnumi oleku laiendatud vabatahtlik teavitamine
    ...
    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;
  • Selgesõnaline taotlus sõnumite uuesti saatmiseks
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Kaugosaline vastab kohe, saates nõutud sõnumid uuesti […]
  • Selgesõnaline taotlus vastuste uuesti saatmiseks
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Kaugosaline vastab kohe uuesti saatmisega vastuseid soovitud sõnumitele […]
  • Sõnumite koopiad
    Mõnel juhul tuleb vana sõnum msg_id-ga, mis enam ei kehti, uuesti saata. Seejärel pakitakse see koopiamahutisse:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Pärast kättesaamist töödeldakse sõnumit nii, nagu poleks ümbrist seal. Kui aga on kindlalt teada, et kiri orig_message.msg_id saadi, siis uut kirja ei töödelda (samal ajal kinnitatakse see ja orig_message.msg_id). Orig_message.msg_id väärtus peab olema väiksem kui konteineri msg_id.

Olgem isegi vait, millest msgs_state_info jälle paistavad lõpetamata TL kõrvad välja (meil oli vaja baitide vektorit ja kahes alumises bitis oli enum ja kahes kõrgemas bitis lipud). Asi on erinev. Kas keegi saab aru, miks see kõik praktikas on? tõelises kliendis vajalik?.. Raskustega, kuid võib ette kujutada mingit kasu, kui inimene tegeleb silumisega ja interaktiivses režiimis - küsige serverilt, mis ja kuidas. Kuid siin on taotlusi kirjeldatud edasi-tagasi sõit.

Sellest järeldub, et iga osapool ei pea mitte ainult krüpteerima ja sõnumeid saatma, vaid ka enda kohta, neile antud vastuste kohta andmeid talletama teadmata aja jooksul. Dokumentatsioon ei kirjelda nende funktsioonide ajastust ega praktilist rakendatavust. kuidagi. Kõige hämmastavam on see, et neid kasutatakse tegelikult ametlike klientide koodis! Ilmselt räägiti neile midagi, mida avalikus dokumentatsioonis ei olnud. Saage koodist aru miks, pole enam nii lihtne kui TL puhul - see pole (suhteliselt) loogiliselt isoleeritud osa, vaid rakendusarhitektuuriga seotud jupp, st. nõuab rakenduse koodi mõistmiseks oluliselt rohkem aega.

Pingid ja ajastused. Järjekorrad.

Kõigest, kui meenutada oletusi serveri arhitektuuri kohta (päringute jaotus taustaprogrammide vahel), järgneb üsna kurb seik - hoolimata kõigist TCP-s olevatest tarnegarantiidest (kas edastatakse andmed või teavitatakse teid lüngast, kuid andmed edastatakse enne probleemi ilmnemist), et kinnitused MTProtos endas - ei mingeid garantiisid. Server võib teie sõnumi kergesti kaotada või välja visata ja sellega ei saa midagi teha, piisab, kui kasutada erinevat tüüpi karkusid.

Ja esiteks - sõnumijärjekorrad. Noh, ühe asjaga oli kõik algusest peale ilmne – kinnitamata sõnum tuleb salvestada ja edasi saata. Ja mis aja pärast? Ja naljamees tunneb teda. Võib-olla lahendavad need sõltuvuses olevad teenuseteated selle probleemi kuidagi karkudega, näiteks Telegram Desktopis on neile vastavad umbes 4 järjekorda (võib-olla rohkem, nagu juba mainitud, selleks peate selle koodi ja arhitektuuri tõsisemalt süvenema; samal ajal aeg, me Teame, et seda ei saa prooviks võtta; teatud arvu MTProto skeemi tüüpe selles ei kasutata).

Miks see juhtub? Tõenäoliselt ei suutnud serveri programmeerijad tagada klastri töökindlust ega isegi eesmise tasakaalustaja puhverdamist ja kandsid selle probleemi üle kliendile. Meeleheitest proovis Vassili rakendada alternatiivset võimalust, ainult kahe järjekorraga, kasutades TCP algoritme - mõõtes RTT-d serverile ja kohandades akna suurust (sõnumites) sõltuvalt kinnitamata päringute arvust. See tähendab, et selline ligikaudne heuristiline serveri koormuse hindamiseks on see, kui palju meie päringuid suudab see korraga närida ja mitte kaotada.

Noh, see tähendab, saate aru, eks? Kui peate TCP-d uuesti rakendama TCP-põhise protokolli peale, viitab see väga halvasti kavandatud protokollile.

Ah jaa, miks on vaja rohkem kui ühte järjekorda ja mida see ikkagi tähendab kõrgetasemelise API-ga töötava inimese jaoks? Vaata, sa esitad päringu, järjestad selle, aga sageli ei saa sa seda kohe saata. Miks? Sest vastus saab olema msg_id, mis on ajutineаOlen silt, mille loovutamist on parem võimalikult hilja edasi lükata - juhuks kui server selle meie ja tema vahelise aja mittevastavuse tõttu tagasi lükkab (muidugi saame teha karku, mis nihutab meie aega praegusest serverisse, lisades serveri vastustest arvutatud delta – ametlikud kliendid teevad seda, kuid see on puhverdamise tõttu toores ja ebatäpne). Seega, kui teete päringu raamatukogust kohaliku funktsioonikõnega, läbib teade järgmised etapid.

  1. See asub ühes järjekorras ja ootab krüptimist.
  2. Ametisse nimetatud msg_id ja teade läks teise järjekorda – võimalik edasisaatmine; saata pistikupessa.
  3. a) Server vastas MsgsAck - sõnum toimetati kohale, kustutame selle "muust järjekorrast".
    b) Või vastupidi, talle ei meeldinud midagi, ta vastas halvale sõnumile - saatke uuesti "teisest järjekorrast"
    c) Midagi pole teada, kiri tuleb teisest järjekorrast uuesti saata – aga pole täpselt teada, millal.
  4. Server vastas lõpuks RpcResult - tegelik vastus (või viga) - mitte ainult tarnitud, vaid ka töödeldud.

Ehk, võib konteinerite kasutamine probleemi osaliselt lahendada. See on siis, kui hunnik sõnumeid pakitakse ühte ja server vastas neile kõigile korraga kinnitusega msg_id. Kuid ta lükkab ka selle paki täielikult tagasi, kui midagi läks valesti.

Ja siinkohal tulevad mängu mittetehnilised kaalutlused. Kogemuste põhjal oleme näinud palju karkusid ja lisaks näeme nüüd rohkem näiteid halbadest nõuannetest ja arhitektuurist - kas sellistes tingimustes tasub usaldada ja selliseid otsuseid teha? Küsimus on retooriline (muidugi mitte).

Millest me räägime? Kui teemal "uimastiteated sõnumite kohta" võite siiski spekuleerida vastuväidetega, nagu "sa oled loll, sa ei saanud meie hiilgavasest plaanist aru!" (nii et kõigepealt kirjutage dokumentatsioon, nagu normaalsed inimesed peaksid, koos põhjenduste ja paketivahetuse näidetega, siis räägime), siis on ajastused/ajalõpud puhtalt praktiline ja konkreetne küsimus, siin on kõik ammu teada. Mida dokumentatsioon meile ajalõppude kohta ütleb?

Server kinnitab tavaliselt kliendilt sõnumi (tavaliselt RPC-päringu) vastuvõtmist RPC vastuse abil. Kui vastust tuleb kaua, võib server saata esmalt kviitungi ja veidi hiljem RPC vastuse.

Tavaliselt kinnitab klient serverilt sõnumi (tavaliselt RPC vastuse) kättesaamist, lisades kinnituse järgmisele RPC päringule, kui seda ei edastata liiga hilja (kui see genereeritakse näiteks 60–120 sekundit pärast vastuvõtmist serveri sõnumist). Kui aga pikema aja jooksul pole põhjust serverisse sõnumeid saata või kui serverist on palju kinnitamata sõnumeid (näiteks üle 16), edastab klient eraldiseisva kinnituse.

... Tõlgin: me ise ei tea, kui palju ja kuidas me seda vajame, seega oletame, et las see olla nii.

Ja pingide kohta:

Ping-sõnumid (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Vastus tagastatakse tavaliselt samale ühendusele:

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

Need sõnumid ei vaja kinnitust. Pong edastatakse ainult vastusena pingile, samal ajal kui pingi saab algatada kumbki pool.

Edasilükatud ühenduse sulgemine + PING

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

Töötab nagu ping. Lisaks käivitab server pärast selle vastuvõtmist taimeri, mis sulgeb praeguse ühenduse disconnect_delay sekundit hiljem, välja arvatud juhul, kui see saab uut sama tüüpi sõnumit, mis lähtestab automaatselt kõik eelmised taimerid. Kui klient saadab need pingid näiteks kord iga 60 sekundi järel, võib ta seada disconnect_delay väärtuseks 75 sekundit.

Oled sa hull?! 60 sekundi pärast siseneb rong jaama, väljub ja võtab reisijad peale ning kaotab tunnelis taas kontakti. 120 sekundi pärast, kui te seda kuulete, jõuab see teiseni ja ühendus katkeb tõenäoliselt. Noh, on selge, kust jalad tulevad - "Ma kuulsin helinat, aga ei tea, kus see on", seal on Nagli algoritm ja TCP_NODELAY valik, mis on mõeldud interaktiivseks tööks. Kuid vabandust, hoidke kinni selle vaikeväärtusest - 200 Millisekundit Kui soovite tõesti kujutada midagi sarnast ja säästa paar võimalikku paketti, siis lükake see 5 sekundiks edasi või mis iganes sõnumi "Kasutaja kirjutab..." ajalõpp on. Aga mitte rohkem.

Ja lõpuks, pingid. See tähendab, et TCP-ühenduse elavuse kontrollimine. Naljakas, aga umbes 10 aastat tagasi kirjutasin kriitilise teksti meie teaduskonna ühiselamu sõnumitooja kohta – sealsed autorid pingutasid ka serveri kliendilt, mitte vastupidi. Aga 3. kursuse tudengid on üks asi ja rahvusvaheline kontor teine ​​asi, eks?...

Esiteks väike haridusprogramm. TCP-ühendus võib pakettvahetuse puudumisel töötada nädalaid. See on olenevalt eesmärgist nii hea kui ka halb. Hea, kui sul oli serveriga avatud SSH ühendus, tõusid arvutist püsti, taaskäivitasid ruuteri, naasid oma kohale - selle serveri seanss ei katkenud (ei kirjutanud midagi, pakette ei olnud) , see on mugav. On halb, kui serveris on tuhandeid kliente, kes kasutavad ressursse (tere, Postgres!) ja kliendi host võib olla juba ammu taaskäivitatud – aga me ei saa sellest teada.

Vestlus-/IM-süsteemid kuuluvad teisele juhtumile veel ühel põhjusel – võrguolekud. Kui kasutaja "kukkus maha", peate sellest oma vestluspartnereid teavitama. Vastasel juhul saate teha vea, mille Jabberi loojad tegid (ja parandasid 20 aastat) - kasutaja katkestas ühenduse, kuid nad jätkavad talle sõnumite kirjutamist, uskudes, et ta on võrgus (mis ka nendes täielikult kaduma läksid). paar minutit enne ühenduse katkemise avastamist). Ei, valik TCP_KEEPALIVE, mille paljud inimesed, kes ei mõista, kuidas TCP taimerid töötavad, viskavad juhuslikult sisse (määrates metsikväärtusi, näiteks kümneid sekundeid), siin ei aita – peate veenduma, et mitte ainult OS-i kernel kasutaja masinast on elus, kuid töötab ka normaalselt, suudab reageerida ja rakendus ise (kas arvate, et see ei saa külmuda? Ubuntu 18.04 Telegram Desktop hangus minu jaoks rohkem kui korra).

Sellepärast tuleb pingida server klient, mitte vastupidi - kui klient seda teeb, kui ühendus katkeb, siis pingi ei edastata, eesmärki ei saavutata.

Mida me Telegramis näeme? See on täpselt vastupidine! Noh, see on. Formaalselt võivad mõlemad pooled muidugi üksteist pingida. Praktikas kasutavad kliendid karku ping_delay_disconnect, mis määrab serveris taimeri. Vabandage, klient ei saa otsustada, kui kaua ta ilma pingita seal elada soovib. Server teab oma koormuse põhjal paremini. Aga muidugi, kui sa ressursse ei pane pahaks, siis oled sa oma kuri Pinocchio ja kark teeb seda...

Kuidas see oleks pidanud olema kujundatud?

Usun, et ülaltoodud faktid viitavad selgelt sellele, et Telegrami/VKontakte meeskond ei ole eriti pädev arvutivõrkude transpordi (ja madalama taseme) valdkonnas ning nende madal kvalifikatsioon asjakohastes küsimustes.

Miks see nii keeruliseks osutus ja kuidas saavad Telegrami arhitektid vastu vaielda? Asjaolu, et nad üritasid teha seanssi, mis elab üle TCP-ühenduse katkestused, st mida praegu ei edastatud, edastame hiljem. Tõenäoliselt proovisid nad ka UDP-transporti teha, kuid tekkisid raskused ja nad loobusid sellest (sellepärast on dokumentatsioon tühi - polnud millegagi uhkustada). Kuid kuna puudub arusaamine sellest, kuidas võrgud üldiselt ja konkreetselt TCP töötavad, kus saate sellele tugineda ja kus peate seda ise tegema (ja kuidas), ning katse ühendada see krüptograafiaga "kaks kärbest koos". üks kivi”, selline on tulemus.

Kuidas see vajalik oli? Lähtudes sellest, msg_id on krüptograafilisest vaatenurgast vajalik ajatempel kordusrünnakute vältimiseks, sellele unikaalse identifikaatori funktsiooni lisamine on viga. Seega, ilma praegust arhitektuuri põhjalikult muutmata (kui värskenduste voog luuakse, on see kõrgetasemeline API-teema selle postituste seeria teise osa jaoks), tuleks teha järgmist.

  1. Kliendiga TCP ühendust hoidev server võtab vastutuse - kui ta on pesast lugenud, siis palun kinnitage, töötlege või tagastage viga, kaotusi pole. Siis pole kinnitus mitte ID-de vektor, vaid lihtsalt "viimati vastuvõetud seq_no" - lihtsalt number, nagu TCP-s (kaks numbrit - teie jada ja kinnitatud). Oleme alati seansi sees, kas pole?
  2. Ajatempel kordusrünnakute vältimiseks muutub eraldi väljaks, a la nonce. Seda kontrollitakse, kuid see ei mõjuta midagi muud. Piisavalt ja uint32 - kui meie sool muutub vähemalt iga poole päeva tagant, saame 16 bitti eraldada praeguse aja täisarvu madalat järku bittidele, ülejäänud - sekundi murdosale (nagu praegu).
  3. Eemaldatud msg_id üldse - taustaprogrammide päringute eristamise seisukohalt on esiteks kliendi ID ja teiseks seansi ID, ühendage need kokku. Järelikult piisab päringu identifikaatoriks ainult ühest asjast seq_no.

See pole ka kõige edukam variant, identifikaatoriks võiks olla täielik juhuslik - seda tehakse muide juba kõrgetasemelises API-s sõnumi saatmisel. Parem oleks arhitektuur täielikult suhtelisest absoluutseks ümber teha, kuid see on teise osa, mitte selle postituse teema.

API?

Ta-daam! Seega, olles võidelnud läbi valu ja karkudega tulvil tee, saime lõpuks serverile saata mis tahes päringuid ja neile vastuseid saada, samuti serverilt värskendusi saada (mitte vastusena päringule, vaid see ise saadab meile, näiteks PUSH, kui keegi on nii selgem).

Tähelepanu, nüüd on artiklis ainus näide Perlis! (neile, kes süntaksit ei tunne, on õnnistamise esimene argument objekti andmestruktuur, teine ​​selle klass):

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

Jah, mitte meelega spoiler – kui te pole seda veel lugenud, tehke seda!

Oh, oot~~... kuidas see välja näeb? Midagi väga tuttavat... võib-olla on see JSON-i tüüpilise veebi API andmestruktuur, välja arvatud see, et klassid on lisatud ka objektidele?..

Nii see siis välja tuleb... Milles see on, seltsimehed?.. Nii palju vaeva – ja me peatusime puhkama seal, kus veebiprogrammeerijad alles algamas?..Kas lihtsalt JSON HTTPS-i kaudu poleks lihtsam?! Mida me vastutasuks saime? Kas pingutus oli seda väärt?

Hindame, mida TL+MTProto meile andis ja millised alternatiivid on võimalikud. Noh, HTTP, mis keskendub päringu-vastuse mudelile, sobib halvasti, kuid vähemalt midagi TLS-i peal?

Kompaktne serialiseerimine. Seda JSON-ile sarnast andmestruktuuri nähes meenub, et sellest on olemas binaarversioonid. MsgPacki märgime ebapiisavalt laiendatavaks, kuid seal on näiteks CBOR - muide, standard, mida kirjeldatakse RFC 7049. See on tähelepanuväärne selle poolest, et see määratleb silte, laienemismehhanismina ja muu hulgas juba standarditud saadaval:

  • 25 + 256 - korduvate ridade asendamine viitega rea ​​numbrile, selline odav tihendusmeetod
  • 26 - serialiseeritud Perli objekt klassi nime ja konstruktori argumentidega
  • 27 - serialiseeritud keelest sõltumatu objekt tüübinime ja konstruktori argumentidega

Noh, proovisin samu andmeid TL-is ja CBOR-is järjestada, kui stringide ja objektide pakkimine oli lubatud. Tulemus hakkas CBOR-i kasuks varieeruma kuskil megabaidist:

cborlen=1039673 tl_len=1095092

Niisiis, järeldus: on võrreldava tõhususega oluliselt lihtsamaid vorminguid, mille puhul ei esine sünkroonimistõrkeid või tundmatu identifikaatorit.

Kiire ühenduse loomine. See tähendab null RTT-d peale taasühendamist (kui võti on juba korra genereeritud) – rakendub juba esimesest MTProto sõnumist, kuid teatud reservatsioonidega – tabab sama soola, seanss pole mäda jne. Mida TLS meile selle asemel pakub? Tsitaat teemal:

PFS-i kasutamisel TLS-is on TLS-i seansipiletid (RFC 5077), et jätkata krüpteeritud seanssi ilma võtmete uuesti läbirääkimisteta ja võtmeteavet serverisse salvestamata. Esimese ühenduse avamisel ja võtmete loomisel krüpteerib server ühenduse oleku ja edastab selle kliendile (seansipileti kujul). Vastavalt sellele saadab klient ühenduse taastamisel seansipileti koos seansivõtmega serverisse tagasi. Pilet ise on krüptitud ajutise võtmega (seansipileti võti), mis salvestatakse serverisse ja tuleb jaotada kõigi SSL-i klastrilahendustes töötlevate esiserverite vahel.[10] Seega võib seansipileti kasutuselevõtt rikkuda PFS-i, kui ajutised serverivõtmed on rikutud, näiteks kui neid hoitakse pikka aega (OpenSSL, nginx, Apache salvestavad need vaikimisi kogu programmi kestuse jooksul; populaarsed saidid kasutavad võti mitu tundi, kuni päevani).

Siin ei ole RTT null, peate vahetama vähemalt ClientHello ja ServerHello, misjärel saab klient saata andmeid koos Valmis. Siinkohal tuleks aga meeles pidada, et meil pole mitte Veeb oma äsja avatud ühendustega, vaid messenger, mille ühendus on sageli üks ja enam-vähem pikaealine, suhteliselt lühike päring veebilehtedele - kõik on multipleksitud. sisemiselt. See tähendab, et see on täiesti vastuvõetav, kui me ei kohanud väga halba metrooosa.

Kas unustasite midagi muud? Kirjutage kommentaaridesse.

Jätkub!

Selle postituste sarja teises osas käsitleme mitte tehnilisi, vaid korralduslikke küsimusi - lähenemisviise, ideoloogiat, liidest, suhtumist kasutajatesse jne. Põhineb siiski siin esitatud tehnilisel teabel.

Kolmandas osas jätkatakse tehnilise komponendi/arenduskogemuse analüüsimist. Õpid eelkõige:

  • pandemooniumi jätkumine mitmesuguste TL tüüpidega
  • tundmatuid asju kanalite ja supergruppide kohta
  • miks on dialoogid kehvemad kui nimekirjad
  • absoluutse ja suhtelise sõnumi adresseerimise kohta
  • mis vahe on fotol ja pildil
  • kuidas emotikonid kaldkirjas teksti segavad

ja muud kargud! Püsige lainel!

Allikas: www.habr.com

Lisa kommentaar