Kritiek op het protocol en de organisatorische benaderingen van Telegram. Deel 1, technisch: ervaring met het schrijven van een klant vanuit het niets - TL, MT

De laatste tijd verschijnen er steeds vaker berichten op Habré over hoe goed Telegram is, hoe briljant en ervaren de gebroeders Durov zijn in het bouwen van netwerksystemen, enz. Tegelijkertijd hebben heel weinig mensen zich echt verdiept in het technische apparaat - hooguit gebruiken ze een vrij eenvoudige (en heel anders dan MTProto) JSON-gebaseerde Bot API, en accepteren ze meestal gewoon op geloof alle lof en PR die rond de boodschapper draaien. Bijna anderhalf jaar geleden begon mijn collega bij de Eshelon NGO Vasily (helaas werd zijn account op Habré samen met het concept gewist) zijn eigen Telegram-client helemaal opnieuw te schrijven in Perl, en later kwam de auteur van deze regels erbij. Waarom Perl, zullen sommigen meteen vragen? Omdat dergelijke projecten al in andere talen bestaan. In feite is dit niet het punt, er zou elke andere taal kunnen zijn waar er geen kant-en-klare bibliotheek, en dienovereenkomstig moet de auteur tot het uiterste gaan helemaal opnieuw. Bovendien is cryptografie een kwestie van vertrouwen, maar verifiëren. Bij een product gericht op veiligheid kun je niet zomaar vertrouwen op een kant-en-klare bibliotheek van de fabrikant en daar blindelings op vertrouwen (dit is echter een onderwerp voor het tweede deel). Op dit moment werkt de bibliotheek redelijk goed op het “gemiddelde” niveau (hiermee kunt u alle API-verzoeken indienen).

Er zal echter niet veel cryptografie of wiskunde in deze reeks berichten voorkomen. Maar er zullen nog veel andere technische details en architectonische krukken zijn (ook handig voor degenen die niet helemaal opnieuw willen schrijven, maar de bibliotheek in welke taal dan ook willen gebruiken). Het belangrijkste doel was dus om te proberen de klant helemaal opnieuw te implementeren volgens officiële documentatie. Dat wil zeggen, laten we aannemen dat de broncode van officiële clients gesloten is (opnieuw zullen we in het tweede deel gedetailleerder ingaan op het feit dat dit waar is gebeurt dus), maar, zoals vroeger, is er bijvoorbeeld een standaard als RFC - is het mogelijk om een ​​client alleen volgens de specificatie te schrijven, "zonder naar de broncode te kijken", zij het officieel (Telegram Desktop, mobiel), of onofficiële Telethon?

авление:

Documentatie... die bestaat, toch? Is het waar?..

Afgelopen zomer werd begonnen met het verzamelen van fragmenten van aantekeningen voor dit artikel. Al die tijd op de officiële website https://core.telegram.org De documentatie was vanaf Laag 23, d.w.z. ergens in 2014 vastgelopen (weet je nog dat er toen nog niet eens kanalen waren?). Uiteraard hadden we hierdoor in theorie in 2014 een client met functionaliteit op dat moment kunnen implementeren. Maar zelfs in deze staat was de documentatie in de eerste plaats onvolledig en in de tweede plaats sprak zij zichzelf op sommige plaatsen tegen. Iets meer dan een maand geleden, in september 2019, was dat zo toevallig Er werd ontdekt dat er een grote update van de documentatie op de site stond, voor de volledig recente Layer 105, met de opmerking dat nu alles opnieuw moet worden gelezen. Veel artikelen zijn inderdaad herzien, maar veel zijn ongewijzigd gebleven. Daarom moet u er bij het lezen van de onderstaande kritiek op de documentatie rekening mee houden dat sommige van deze dingen niet langer relevant zijn, maar sommige nog steeds behoorlijk. Vijf jaar in de moderne wereld is tenslotte niet alleen een lange tijd, maar zeer veel. Sinds die tijd (vooral als je geen rekening houdt met de afgedankte en nieuw leven ingeblazen geochat-sites sindsdien) is het aantal API-methoden in het schema gegroeid van honderd naar meer dan tweehonderdvijftig!

Waar moet je beginnen als jonge auteur?

Het maakt niet uit of u helemaal opnieuw schrijft of bijvoorbeeld kant-en-klare bibliotheken gebruikt Telethon voor Python of Madeline voor PHP, je hebt het in ieder geval eerst nodig registreer uw aanvraag - parameters verkrijgen api_id и api_hash (degenen die met de VKontakte API hebben gewerkt, begrijpen het meteen) waarmee de server de applicatie zal identificeren. Dit moet doe het om juridische redenen, maar we zullen in het tweede deel meer praten over waarom bibliotheekauteurs het niet kunnen publiceren. U bent misschien tevreden met de testwaarden, ook al zijn deze zeer beperkt: feit is dat u zich nu kunt registreren slechts één app, dus haast je er niet halsoverkop in.

Nu zouden we vanuit technisch oogpunt geïnteresseerd moeten zijn in het feit dat we na registratie meldingen van Telegram zouden moeten ontvangen over updates van documentatie, protocol, enz. Dat wil zeggen, je zou kunnen aannemen dat de site met de dokken eenvoudigweg werd verlaten en specifiek bleef werken met degenen die klanten begonnen te maken, omdat het is makkelijker. Maar nee, zoiets werd niet waargenomen, er kwam geen informatie.

En als je helemaal opnieuw schrijft, is het gebruik van de verkregen parameters eigenlijk nog ver weg. Hoewel https://core.telegram.org/ en praat erover in Aan de slag. In feite zul je ze eerst moeten implementeren MTProto-protocol - maar als je geloofde indeling volgens het OSI-model aan het einde van de pagina voor een algemene beschrijving van het protocol, dan is het volkomen tevergeefs.

In feite zal zowel vóór als na MTProto, op verschillende niveaus tegelijk (zoals buitenlandse netwerkers die in de OS-kernel werken, laagschending) een groot, pijnlijk en verschrikkelijk onderwerp in de weg staan...

Binaire serialisatie: TL (Type Language) en zijn schema, en lagen, en vele andere enge woorden

Dit onderwerp is in feite de sleutel tot de problemen van Telegram. En er zullen veel vreselijke woorden zijn als je je erin probeert te verdiepen.

Hier is het diagram. Als dit woord in je opkomt, zeg dan: JSON-schema, Dat dacht je goed. Het doel is hetzelfde: een taal om een ​​mogelijke reeks verzonden gegevens te beschrijven. Dit is waar de overeenkomsten eindigen. Als van de pagina MTProto-protocol, of vanuit de bronboom van de officiële client, zullen we proberen een schema te openen, we zullen zoiets zien als:

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;

Iemand die dit voor de eerste keer ziet, zal intuïtief slechts een deel kunnen herkennen van wat er geschreven staat - nou ja, dit zijn blijkbaar structuren (hoewel waar is de naam, links of rechts?), er zitten velden in, waarna na een dubbele punt een type volgt... waarschijnlijk. Hier staan ​​tussen punthaken waarschijnlijk sjablonen zoals in C++ (in feite niet echt). En wat betekenen al die andere symbolen, vraagtekens, uitroeptekens, percentages, hekjes (en uiteraard betekenen ze verschillende dingen op verschillende plaatsen), soms aanwezig en soms niet, hexadecimale getallen - en vooral: hoe kom je hieruit? rechts (die niet door de server wordt afgewezen) bytestream? Je zult de documentatie moeten lezen (ja, er zijn links naar het schema in de JSON-versie in de buurt - maar dat maakt het er niet duidelijker op).

Open de pagina Serialisatie van binaire gegevens en duik in de magische wereld van paddenstoelen en discrete wiskunde, iets dat lijkt op matan in het 4e jaar. Alfabet, type, waarde, combinator, functionele combinator, normale vorm, samengesteld type, polymorf type... en dat is allemaal nog maar de eerste pagina! Het volgende wacht op u TL-taal, dat, hoewel het al een voorbeeld bevat van een triviaal verzoek en antwoord, helemaal geen antwoord geeft op meer typische gevallen, wat betekent dat je door een hervertelling van de wiskunde moet waden, vertaald van het Russisch naar het Engels op nog eens acht ingebedde Pagina's!

Lezers die bekend zijn met functionele talen en automatische type-inferentie zullen de beschrijvingstaal in deze taal, zelfs uit het voorbeeld, uiteraard als veel bekender beschouwen en kunnen zeggen dat dit in principe eigenlijk niet slecht is. De bezwaren hiertegen zijn:

  • Ja, doel Klinkt goed, maar helaas, zij niet bereikt
  • Het onderwijs aan Russische universiteiten varieert zelfs tussen IT-specialiteiten - niet iedereen heeft de overeenkomstige cursus gevolgd
  • Tenslotte is dit, zoals we zullen zien, in de praktijk ook het geval Het is niet noodzakelijk, aangezien slechts een beperkte subset van zelfs de beschreven TL wordt gebruikt

Zoals gezegd LeoNerd op het kanaal #perl in het FreeNode IRC-netwerk, die probeerde een poort van Telegram naar Matrix te implementeren (vertaling van het citaat is vanuit het geheugen onnauwkeurig):

Het voelt alsof iemand voor het eerst kennis maakte met de typetheorie, opgewonden raakte en ermee begon te spelen, zonder zich er echt om te bekommeren of het in de praktijk nodig was.

Kijk zelf of de behoefte aan kale typen (int, long, etc.) als iets elementairs geen vragen oproept - uiteindelijk moeten ze handmatig worden geïmplementeerd - laten we bijvoorbeeld een poging ondernemen om hieruit af te leiden vector. Dat is in feite array, als je de resulterende dingen bij hun eigennaam noemt.

Maar voorheen

Een korte beschrijving van een subset van de TL-syntaxis voor degenen die de officiële documentatie niet lezen

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;

Definitie begint altijd ontwerper, waarna eventueel (in de praktijk - altijd) via het symbool # moet CRC32 uit de genormaliseerde beschrijvingsreeks van dit type. Vervolgens volgt een beschrijving van de velden; als ze bestaan, kan het type leeg zijn. Dit alles eindigt met een gelijkteken, de naam van het type waartoe deze constructor - dat wil zeggen in feite het subtype - behoort. De man rechts van het gelijkteken is polymorf - dat wil zeggen dat er verschillende specifieke typen mee kunnen corresponderen.

Als de definitie na de regel voorkomt ---functions---, dan blijft de syntaxis hetzelfde, maar de betekenis zal anders zijn: de constructor wordt de naam van de RPC-functie, de velden worden parameters (nou ja, dat wil zeggen, het blijft precies dezelfde gegeven structuur, zoals hieronder beschreven , dit zal eenvoudigweg de toegewezen betekenis zijn), en het "polymorfe type" - het type van het geretourneerde resultaat. Het is waar dat het nog steeds polymorf blijft - zojuist gedefinieerd in de sectie ---types---, maar deze constructor wordt “niet in aanmerking genomen”. Het overbelasten van de typen aangeroepen functies met hun argumenten, d.w.z. Om de een of andere reden zijn verschillende functies met dezelfde naam maar verschillende handtekeningen, zoals in C++, niet voorzien in de TL.

Waarom "constructor" en "polymorf" als het geen OOP is? In feite zal het voor iemand gemakkelijker zijn om hierover na te denken in OOP-termen - een polymorf type als een abstracte klasse, en constructors zijn de directe afstammelingen ervan, en final in de terminologie van een aantal talen. In feite alleen hier natuurlijk gelijkenis met echte overbelaste constructormethoden in OO-programmeertalen. Omdat het hier alleen om datastructuren gaat, zijn er geen methoden (hoewel de beschrijving van functies en methoden verder behoorlijk in staat is verwarring in het hoofd te creëren dat ze bestaan, maar dat is een andere zaak) - je kunt een constructor beschouwen als een waarde uit welke wordt gebouwd type bij het lezen van een bytestream.

Hoe gebeurde dit? De deserializer, die altijd 4 bytes leest, ziet de waarde 0xcrc32 - en begrijpt wat er daarna zal gebeuren field1 met soort int, d.w.z. leest precies 4 bytes, hierop het bovenliggende veld met het type PolymorType lezen. Ziet 0x2crc32 en begrijpt dat er eerst twee velden verder zijn long, wat betekent dat we 8 bytes lezen. En dan weer een complex type, dat op dezelfde manier wordt gedeserialiseerd. Bijvoorbeeld, Type3 zou in het circuit kunnen worden verklaard zodra twee constructeurs respectievelijk, dan moeten ze elkaar ontmoeten 0x12abcd34, waarna je nog 4 bytes moet lezen intOf 0x6789cdef, waarna er niets meer zal zijn. Al het andere: u moet een uitzondering maken. Hoe dan ook, hierna gaan we terug naar het lezen van 4 bytes int поля field_c в constructorTwo en daarmee zijn we klaar met het lezen van onze PolymorType.

Eindelijk, als je gepakt wordt 0xdeadcrc voor constructorThree, dan wordt alles ingewikkelder. Ons eerste veld is bit_flags_of_what_really_present met soort # - in feite is dit slechts een alias voor het type nat, wat 'natuurlijk getal' betekent. Dat wil zeggen dat niet-ondertekende int trouwens het enige geval is waarin niet-ondertekende getallen voorkomen in echte circuits. Het volgende is dus een constructie met een vraagteken, wat betekent dat dit veld alleen op de draad aanwezig zal zijn als de overeenkomstige bit in het genoemde veld is ingesteld (ongeveer zoals een ternaire operator). Laten we dus aannemen dat dit bit is ingesteld, wat betekent dat we verder een veld als Type, die in ons voorbeeld twee constructors heeft. De ene is leeg (bestaat alleen uit de identificatie), de andere heeft een veld ids met soort ids:Vector<long>.

Je zou kunnen denken dat zowel sjablonen als generieke geneesmiddelen in de profs van Java zitten. Maar nee. Bijna. Dit единственный geval van het gebruik van hoekbeugels in echte circuits, en dit wordt ALLEEN gebruikt voor Vector. In een bytestroom zijn dit 4 CRC32-bytes voor het Vector-type zelf, altijd hetzelfde, dan 4 bytes - het aantal array-elementen, en dan deze elementen zelf.

Voeg daarbij het feit dat serialisatie altijd plaatsvindt in woorden van 4 bytes, alle typen zijn veelvouden daarvan - de ingebouwde typen worden ook beschreven bytes и string met handmatige serialisatie van de lengte en deze uitlijning met 4 - nou, het lijkt normaal en zelfs relatief effectief te klinken? Hoewel wordt beweerd dat TL een effectieve binaire serialisatie is, zal JSON, met de uitbreiding van zo ongeveer alles, zelfs Booleaanse waarden en strings van één teken naar 4 bytes, nog steeds veel dikker zijn? Kijk, zelfs onnodige velden kunnen worden overgeslagen met bitvlaggen, alles is redelijk goed en zelfs uitbreidbaar voor de toekomst, dus waarom zouden we later geen nieuwe optionele velden aan de constructor toevoegen?

Maar nee, als je niet mijn korte beschrijving leest, maar de volledige documentatie, en nadenkt over de implementatie. Ten eerste wordt de CRC32 van de constructor berekend volgens de genormaliseerde regel van de tekstbeschrijving van het schema (verwijder extra witruimte, enz.) - dus als een nieuw veld wordt toegevoegd, zal de typebeschrijvingsregel veranderen, en dus de CRC32 en , dus serialisatie. En wat zou de oude klant doen als hij een veld zou ontvangen met nieuwe vlaggen, en hij weet niet wat hij er vervolgens mee moet doen?

Ten tweede: laten we het onthouden CRC32, die hier hoofdzakelijk wordt gebruikt als hash-functies om op unieke wijze te bepalen welk type wordt ge(de)serialiseerd. Hier worden we geconfronteerd met het probleem van botsingen - en nee, de kans is niet één op 232, maar veel groter. Wie herinnerde zich dat CRC32 is ontworpen om fouten in het communicatiekanaal te detecteren (en te corrigeren) en dienovereenkomstig deze eigenschappen te verbeteren ten nadele van anderen? Het maakt zich bijvoorbeeld niet uit om bytes te herschikken: als je CRC32 uit twee regels berekent, verwissel je in de tweede de eerste 4 bytes met de volgende 4 bytes - het zal hetzelfde zijn. Wanneer onze invoer bestaat uit tekstreeksen uit het Latijnse alfabet (en een beetje leestekens), en deze namen zijn niet bijzonder willekeurig, neemt de kans op een dergelijke herschikking enorm toe.

Trouwens, wie heeft gecontroleerd wat daar was? echt CRC32? Een van de vroege broncodes (zelfs vóór Waltman) had een hash-functie die elk teken vermenigvuldigde met het getal 239, zo geliefd bij deze mensen, ha ha!

Eindelijk, oké, we realiseerden ons dat constructors met een veldtype Vector<int> и Vector<PolymorType> zal verschillende CRC32 hebben. Hoe zit het met de online prestaties? En vanuit theoretisch oogpunt gezien wordt dit onderdeel van het type? Laten we zeggen dat we een reeks van tienduizend nummers doorgeven, nou ja Vector<int> alles is duidelijk, de lengte en nog eens 40000 bytes. Wat als dit Vector<Type2>, dat uit slechts één veld bestaat int en het is alleen in het type - moeten we 10000xabcdef0 34 keer herhalen en dan 4 bytes int, of de taal kan het voor ons ONAFHANKELIJK maken van de constructor fixedVec en in plaats van 80000 bytes weer slechts 40000 bytes overdragen?

Dit is helemaal geen ijdele theoretische vraag - stel je voor dat je een lijst met groepsgebruikers ontvangt, die allemaal een ID, voornaam en achternaam hebben - het verschil in de hoeveelheid gegevens die via een mobiele verbinding wordt overgedragen, kan aanzienlijk zijn. Het is precies de effectiviteit van Telegram-serialisatie die ons wordt geadverteerd.

Zo…

Vector, die nooit is uitgebracht

Als je door de pagina's met beschrijvingen van combinatoren enzovoort probeert te bladeren, zul je zien dat een vector (en zelfs een matrix) formeel probeert te worden uitgevoerd via tupels van verschillende bladen. Maar uiteindelijk vergeten ze het, de laatste stap wordt overgeslagen en er wordt eenvoudigweg een definitie van een vector gegeven, die nog niet aan een type is gebonden. Wat is er aan de hand? In talen programmeren, vooral functionele, is het vrij typerend om de structuur recursief te beschrijven - de compiler met zijn luie evaluatie zal alles zelf begrijpen en doen. In taal serialisatie van gegevens Wat nodig is, is EFFICIËNTIE: het is voldoende om het eenvoudigweg te beschrijven lijst, d.w.z. structuur van twee elementen - het eerste is een data-element, het tweede is dezelfde structuur zelf of een lege ruimte voor de staart (pack (cons) in Lisp). Maar dit zal uiteraard nodig zijn elke element besteedt nog eens 4 bytes (CRC32 in het geval in TL) om zijn type te beschrijven. Een array kan ook eenvoudig worden beschreven vaste maat, maar in het geval van een array van vooraf onbekende lengte breken we af.

Omdat TL het uitvoeren van een vector niet toestaat, moest deze daarom terzijde worden toegevoegd. Uiteindelijk zegt de documentatie:

Serialisatie gebruikt altijd dezelfde constructor “vector” (const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t”) die niet afhankelijk is van de specifieke waarde van de variabele van type t.

De waarde van de optionele parameter t is niet betrokken bij de serialisatie, aangezien deze is afgeleid van het resultaattype (altijd bekend voorafgaand aan deserialisatie).

Bekijk het eens nader: vector {t:Type} # [ t ] = Vector t - Maar nergens Deze definitie zelf zegt niet dat het eerste getal gelijk moet zijn aan de lengte van de vector! En het komt nergens vandaan. Dit is een gegeven dat u in gedachten moet houden en dat u met uw handen moet implementeren. Elders vermeldt de documentatie zelfs eerlijk dat het type niet echt is:

Het vector t polymorfe pseudotype is een “type” waarvan de waarde een reeks waarden is van elk type t, omkaderd of kaal.

... maar concentreert zich er niet op. Wanneer je, moe van het waden door de ingewikkelde wiskunde (die je misschien zelfs kent van een universitaire opleiding), besluit het op te geven en daadwerkelijk te gaan kijken hoe je er in de praktijk mee kunt werken, dan blijft de indruk in je hoofd achter dat dit serieus is. Wiskunde in de kern is duidelijk uitgevonden door Cool People (twee wiskundigen - ACM-winnaar), en niet zomaar iemand. Het doel – pronken – is bereikt.

Trouwens, over het aantal. Laten we u daaraan herinneren # het is een synoniem nat, natuurlijk nummer:

Er zijn type-expressies (type-expr) en numerieke uitdrukkingen (nat-expr). Ze worden echter op dezelfde manier gedefinieerd.

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

maar in de grammatica worden ze op dezelfde manier beschreven, d.w.z. Dit verschil moet opnieuw worden onthouden en met de hand in de praktijk worden gebracht.

Nou ja, sjabloontypen (vector<int>, vector<User>) hebben een gemeenschappelijke identificatie (#1cb5c415), d.w.z. als u weet dat de oproep wordt aangekondigd als

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

dan wacht je niet langer alleen op een vector, maar op een vector van gebruikers. Preciezer, moet wacht - in echte code zal elk element, zo niet een kaal type, een constructor hebben, en op een goede manier zou het bij de implementatie nodig zijn om dit te controleren - maar we werden precies in elk element van deze vector gestuurd dat soort? Wat als het een soort PHP was, waarin een array verschillende typen in verschillende elementen kan bevatten?

Op dit punt begin je te denken: is zo'n TL nodig? Misschien zou het voor de kar mogelijk zijn om een ​​menselijke serialisator te gebruiken, dezelfde protobuf die toen al bestond? Dat was de theorie, laten we eens naar de praktijk kijken.

Bestaande TL-implementaties in code

TL werd geboren in de diepten van VKontakte, zelfs vóór de beroemde gebeurtenissen met de verkoop van het aandeel Durov en (zeker), zelfs voordat de ontwikkeling van Telegram begon. En opensource broncode van de eerste implementatie je kunt veel grappige krukken vinden. En de taal zelf werd daar vollediger geïmplementeerd dan nu in Telegram. Hashes worden bijvoorbeeld helemaal niet gebruikt in het schema (dat wil zeggen een ingebouwd pseudotype (zoals een vector) met afwijkend gedrag). Of

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

maar laten we voor de volledigheid overwegen om als het ware de evolutie van de Reus van het Denken te traceren.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Of deze mooie:

    static const char *reserved_words_polymorhic[] = {

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

      };

Dit fragment gaat over sjablonen zoals:

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

Dit is de definitie van een hashmap-sjabloontype als een vector van int - Type-paren. In C++ zou het er ongeveer zo uitzien:

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

dus, alpha - trefwoord! Maar alleen in C++ kun je T schrijven, maar je zou alpha, beta moeten schrijven... Maar niet meer dan 8 parameters, daar houdt de fantasie op. Het lijkt erop dat er ooit in St. Petersburg een aantal dialogen als deze plaatsvonden:

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

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

Maar dit ging over de eerste gepubliceerde implementatie van TL “in het algemeen”. Laten we verder gaan met het overwegen van implementaties in de Telegram-clients zelf.

Woord aan Vasily:

Vasily, [09.10.18 17:07] Bovenal is de kont heet omdat ze een aantal abstracties hebben gecreëerd, er vervolgens een bout op hebben gehamerd en de codegenerator hebben bedekt met krukken
Als gevolg hiervan eerst vanaf dock pilot.jpg
Vervolgens uit de code dzhekichan.webp

Van mensen die bekend zijn met algoritmen en wiskunde kunnen we natuurlijk verwachten dat ze Aho, Ullmann hebben gelezen en bekend zijn met de tools die de afgelopen decennia de facto standaard zijn geworden in de industrie voor het schrijven van hun DSL-compilers, toch?

Door de auteur telegram-cli is Vitaly Valtman, zoals kan worden begrepen uit het voorkomen van het TLO-formaat buiten zijn (cli) grenzen, een lid van het team - nu is er een bibliotheek voor TL-parsing toegewezen afzonderlijk, wat is de indruk van haar TL-parser? ..

16.12 04:18 Vasily: Ik denk dat iemand lex+yacc niet beheerst
16.12 04:18 Vasily: Ik kan het niet anders uitleggen
16.12 04:18 Vasily: nou, of ze werden betaald voor het aantal regels in VK
16.12 04:19 Vasily: 3k+ lijnen enz<censored> in plaats van een parser

Misschien een uitzondering? Laten we kijken hoe делает Dit is de OFFICIËLE client - Telegram Desktop:

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

Meer dan 1100 regels in Python, een paar reguliere expressies + speciale gevallen zoals een vector, die uiteraard in het schema wordt gedeclareerd zoals het zou moeten zijn volgens de TL-syntaxis, maar ze vertrouwden op deze syntaxis om het te parseren... De vraag rijst: waarom was dit allemaal een wonder?иHet is meer gelaagd als niemand het toch volgens de documentatie gaat ontleden?!

Trouwens... Weet je nog dat we het hadden over CRC32-controle? In de codegenerator van Telegram Desktop is er dus een lijst met uitzonderingen voor die typen waarin de berekende CRC32 komt niet overeen met degene aangegeven in het diagram!

Vasily, [18.12/22 49:XNUMX] en hier zou ik nadenken of zo'n TL nodig is
als ik zou willen knoeien met alternatieve implementaties, zou ik beginnen met het invoegen van regeleinden, de helft van de parsers zal breken bij definities van meerdere regels
tdesktop echter ook

Onthoud het punt over oneliner, we komen er later op terug.

Oké, telegram-cli is niet officieel, Telegram Desktop is officieel, maar hoe zit het met de anderen? Wie weet?.. In de Android-clientcode zat helemaal geen schema-parser (wat vragen oproept over open source, maar dit is voor het tweede deel), maar er waren verschillende andere grappige stukjes code, maar daarover meer in de onderafdeling hieronder.

Welke andere vragen roept serialisatie in de praktijk op? Ze hebben bijvoorbeeld natuurlijk veel dingen gedaan met bitvelden en voorwaardelijke velden:

Vasili: flags.0? true
betekent dat het veld aanwezig is en gelijk is aan waar als de vlag is ingesteld

Vasili: flags.1? int
betekent dat het veld aanwezig is en moet worden gedeserialiseerd

Vasily: Ass, maak je geen zorgen over wat je doet!
Vasily: Er staat ergens in het document vermeld dat true een type met nullengte is, maar het is onmogelijk om iets uit hun document samen te stellen
Vasily: Bij de open source-implementaties is dit ook niet het geval, maar er zijn wel een hoop krukken en steunen

Hoe zit het met Teleton? Vooruitkijkend naar het onderwerp MTProto, een voorbeeld - in de documentatie staan ​​​​dergelijke stukken, maar het bord % het wordt alleen beschreven als “overeenkomend met een bepaald kaal type”, d.w.z. in de onderstaande voorbeelden is er een fout of is er iets ongedocumenteerds:

Vasily, [22.06.18 18:38] Op één plek:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Op een andere:

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

En dit zijn twee grote verschillen: in het echte leven komt er een soort naakte vector

Ik heb geen kale vectordefinitie gezien en ben er ook geen tegengekomen

De analyse wordt met de hand geschreven in Telethon

In zijn diagram wordt de definitie becommentarieerd msg_container

Opnieuw blijft de vraag over %. Het is niet beschreven.

Vadim Goncharov, [22.06.18 19:22] en in tdesktop?

Vasily, [22.06.18 19:23] Maar hun TL-parser op reguliere motoren zal dit waarschijnlijk ook niet eten

// parsed manually

TL is een prachtige abstractie, niemand implementeert deze volledig

En % staat niet in hun versie van het schema

Maar hier spreekt de documentatie zichzelf tegen, dus ik weet het niet

Het werd gevonden in de grammatica, ze hadden eenvoudigweg kunnen vergeten de semantiek te beschrijven

Je hebt het document op TL gezien, je komt er niet uit zonder een halve liter

“Nou, laten we zeggen”, zal een andere lezer zeggen, “je hebt ergens kritiek op, dus laat me zien hoe het moet.”

Vasily antwoordt: “Wat de parser betreft, ik hou van dingen als

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

vind het op de een of andere manier beter dan

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

of

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

dit is de HELE lexer:

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

die. eenvoudiger is zacht uitgedrukt.”

Over het algemeen passen de parser en codegenerator voor de feitelijk gebruikte subset van TL daardoor in ongeveer 100 regels grammatica en ongeveer 300 regels van de generator (alle print's gegenereerde code), inclusief type-informatiebroodjes voor introspectie in elke klas. Elk polymorf type verandert in een lege abstracte basisklasse, en constructors erven ervan en beschikken over methoden voor serialisatie en deserialisatie.

Gebrek aan typen in de typetaal

Sterk typen is een goede zaak, toch? Nee, dit is geen holivar (hoewel ik de voorkeur geef aan dynamische talen), maar een postulaat in het kader van TL. Op basis daarvan zou de taal ons allerlei controles moeten bieden. Nou ja, misschien niet hij zelf, maar de implementatie, maar hij zou ze op zijn minst moeten beschrijven. En welke mogelijkheden willen we?

Allereerst beperkingen. Hier zien we in de documentatie voor het uploaden van bestanden:

De binaire inhoud van het bestand wordt vervolgens in delen opgesplitst. Alle onderdelen moeten dezelfde maat hebben ( onderdeelgrootte ) en er moet aan de volgende voorwaarden worden voldaan:

  • part_size % 1024 = 0 (deelbaar door 1KB)
  • 524288 % part_size = 0 (512 KB moet gelijkmatig deelbaar zijn door part_size)

Het laatste onderdeel hoeft niet aan deze voorwaarden te voldoen, op voorwaarde dat de omvang ervan kleiner is dan part_size.

Elk onderdeel moet een volgnummer hebben, bestandsdeel, met een waarde variërend van 0 tot 2,999.

Nadat het bestand is gepartitioneerd, moet u een methode kiezen om het op de server op te slaan. Gebruik upload.saveBigFilePart als de volledige grootte van het bestand groter is dan 10 MB en upload.saveFilePart voor kleinere bestanden.
[…] een van de volgende gegevensinvoerfouten kan worden geretourneerd:

  • FILE_PARTS_INVALID — Ongeldig aantal onderdelen. De waarde ligt er niet tussen 1..3000

Staat dit allemaal in het diagram? Is dit op de een of andere manier uit te drukken met behulp van TL? Nee. Maar neem me niet kwalijk, zelfs de Turbo Pascal van grootvader kon de gespecificeerde typen beschrijven bereiken. En hij wist nog één ding, nu beter bekend als enum - een type bestaande uit een opsomming van een vast (klein) aantal waarden. Houd er rekening mee dat we in talen als C - numeriek alleen over typen hebben gesproken cijfers. Maar er zijn ook arrays, strings... Het zou bijvoorbeeld leuk zijn om te beschrijven dat deze string alleen een telefoonnummer kan bevatten, toch?

Niets hiervan staat in de TL. Maar er is bijvoorbeeld in JSON Schema. En als iemand anders misschien ruzie maakt over de deelbaarheid van 512 KB, dat dit nog in code gecontroleerd moet worden, zorg er dan voor dat de client simpelweg kon niet stuur een nummer buiten bereik 1..3000 (en de overeenkomstige fout kon niet zijn ontstaan) het zou mogelijk zijn geweest, toch?

Trouwens, over fouten en retourwaarden. Zelfs degenen die met TL hebben gewerkt, vervagen hun ogen - dat drong niet meteen tot ons door elk een functie in TL kan feitelijk niet alleen het beschreven retourtype retourneren, maar ook een fout. Maar dit kan op geen enkele manier worden afgeleid uit de TL zelf. Het is natuurlijk al duidelijk en er is in de praktijk niets voor nodig (hoewel RPC in feite op verschillende manieren kan worden gedaan, we komen hier later op terug) - maar hoe zit het met de zuiverheid van de concepten van de wiskunde van abstracte typen uit de hemelse wereld?.. Ik heb de sleepboot opgehaald - dus match hem.

En tot slot: hoe zit het met de leesbaarheid? Nou, daar zou ik in het algemeen graag willen beschrijving zorg dat het goed in het schema staat (in het JSON-schema is dat ook zo), maar als je er al moeite mee hebt, hoe zit het dan met de praktische kant - op zijn minst triviaal om naar de verschillen te kijken tijdens updates? Ontdek het zelf op echte voorbeelden:

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

of

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

Het hangt van iedereen af, maar GitHub weigert bijvoorbeeld veranderingen binnen zulke lange rijen onder de aandacht te brengen. Het spel "vind 10 verschillen", en wat de hersenen meteen zien is dat het begin en het einde in beide voorbeelden hetzelfde zijn, je moet ergens in het midden saai lezen... Naar mijn mening is dit niet alleen in theorie, maar puur visueel vies en slordig.

Trouwens, over de zuiverheid van de theorie. Waarom hebben we bitvelden nodig? Lijkt het er niet op dat ze geur slecht vanuit het oogpunt van de typetheorie? De uitleg is te zien in eerdere versies van het diagram. In eerste instantie, ja, zo was het, voor elke niesbui werd een nieuw type gecreëerd. Deze beginselen bestaan ​​nog steeds in deze vorm, bijvoorbeeld:

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;

Maar stel je nu voor dat als je 5 optionele velden in je structuur hebt, je 32 typen nodig hebt voor alle mogelijke opties. Combinatorische explosie. Zo botste de kristalheldere zuiverheid van de TL-theorie opnieuw tegen de ijzeren reet van de harde realiteit van serialisatie.

Bovendien schenden deze jongens op sommige plaatsen zelf hun eigen typologie. In MTProto (volgend hoofdstuk) kan het antwoord bijvoorbeeld worden gecomprimeerd door Gzip, alles is in orde - behalve dat de lagen en het circuit worden geschonden. Opnieuw werd niet RpcResult zelf geoogst, maar de inhoud ervan. Nou, waarom zou ik dit doen? Ik moest in een kruk snijden zodat de compressie overal zou werken.

Of nog een voorbeeld: we ontdekten ooit een fout: deze werd verzonden InputPeerUser in plaats van InputUser. Of vice versa. Maar het werkte! Dat wil zeggen, de server gaf niets om het type. Hoe kan dit? Het antwoord kan ons worden gegeven door codefragmenten uit telegram-cli:

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

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

Met andere woorden: dit is waar serialisatie plaatsvindt HANDMATIG, geen gegenereerde code! Misschien is de server op een vergelijkbare manier geïmplementeerd? In principe werkt dit als het één keer wordt gedaan, maar hoe kan dit later tijdens updates worden ondersteund? Is dit de reden waarom dit systeem is bedacht? En hier gaan we verder met de volgende vraag.

Versiebeheer. Lagen

Waarom de schemaversies lagen worden genoemd, kan alleen worden gespeculeerd op basis van de geschiedenis van gepubliceerde schema's. Kennelijk dachten de auteurs in eerste instantie dat basiszaken gedaan konden worden met het ongewijzigde schema, en gaven ze alleen waar nodig, voor specifieke verzoeken, aan dat ze met een andere versie werden gedaan. In principe zelfs een goed idee - en het nieuwe zal als het ware “gemengd” zijn, gelaagd bovenop het oude. Maar laten we eens kijken hoe het gedaan werd. Toegegeven, ik kon er niet vanaf het begin naar kijken - het is grappig, maar het diagram van de basislaag bestaat simpelweg niet. Lagen begonnen met 2. De documentatie vertelt ons over een speciale TL-functie:

Als een client Laag 2 ondersteunt, moet de volgende constructor worden gebruikt:

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

In de praktijk betekent dit dat vóór elke API-aanroep een int met de waarde wordt geplaatst 0x289dd1f6 moet vóór het methodenummer worden toegevoegd.

Klinkt normaal. Maar wat gebeurde er daarna? Toen verscheen

invokeWithLayer3#b7475268 query:!X = X;

Dus wat is het volgende? Zoals je misschien wel vermoedt,

invokeWithLayer4#dea0d430 query:!X = X;

Grappig? Nee, het is te vroeg om te lachen, denk daar eens over na elk een verzoek uit een andere laag moet in zo'n speciaal type worden verpakt - als ze voor jou allemaal verschillend zijn, hoe kun je ze dan anders onderscheiden? En het toevoegen van slechts 4 bytes aan de voorkant is een behoorlijk efficiënte methode. Dus,

invokeWithLayer5#417a57ae query:!X = X;

Maar het is duidelijk dat dit na een tijdje een soort bacchanaal zal worden. En de oplossing kwam:

Update: te beginnen met Laag 9, helpermethoden invokeWithLayerN kan alleen samen met gebruikt worden initConnection

Hoera! Na 9 versies kwamen we eindelijk bij wat er in de jaren 80 met internetprotocollen werd gedaan: we hadden eenmaal overeenstemming bereikt over de versie aan het begin van de verbinding!

Dus wat is het volgende?..

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

Maar nu kun je nog steeds lachen. Pas na nog eens 9 lagen werd eindelijk een universele constructor met een versienummer toegevoegd, die maar één keer aan het begin van de verbinding hoeft te worden aangeroepen, en de betekenis van de lagen leek te zijn verdwenen, nu is het slechts een voorwaardelijke versie, zoals overal anders. Probleem opgelost.

Precies?..

Vasily, [16.07.18 14:01] Zelfs op vrijdag dacht ik:
De teleserver verzendt gebeurtenissen zonder verzoek. Aanvragen moeten worden verpakt in InvokeWithLayer. De server verpakt geen updates; er is geen structuur voor het verpakken van reacties en updates.

Die. de klant kan niet opgeven in welke laag hij updates wil

Vadim Goncharov, [16.07.18 14:02] is InvokeWithLayer in principe geen steunpilaar?

Vasily, [16.07.18 14:02] Dit is de enige manier

Vadim Goncharov, [16.07.18 14:02] wat in wezen zou moeten betekenen dat we het aan het begin van de sessie eens worden over de laag

Hieruit volgt overigens dat er geen downgrade voor klanten is voorzien

Updates, d.w.z. type Updates in het schema is dit wat de server naar de client stuurt, niet als reactie op een API-verzoek, maar onafhankelijk wanneer er een gebeurtenis plaatsvindt. Dit is een complex onderwerp dat in een ander bericht zal worden besproken, maar voor nu is het belangrijk om te weten dat de server updates opslaat, zelfs als de client offline is.

Dus als u weigert in te pakken elke pakket om de versie ervan aan te geven, leidt dit logischerwijs tot de volgende mogelijke problemen:

  • de server stuurt updates naar de client nog voordat de client heeft laten weten welke versie hij ondersteunt
  • wat moet ik doen nadat ik de client heb geüpgraded?
  • die garantiesdat de mening van de server over het laagnummer tijdens het proces niet verandert?

Denk je dat dit puur theoretische speculatie is, en dat dit in de praktijk niet kan gebeuren, omdat de server correct is geschreven (althans, hij is goed getest)? Ha! Hoe het ook is!

Dit is precies waar we in augustus tegenaan liepen. Op 14 augustus waren er berichten dat er iets werd bijgewerkt op de Telegram-servers... en vervolgens in de logs:

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.

en vervolgens enkele megabytes aan stacktraces (nou ja, tegelijkertijd werd de logboekregistratie opgelost). Als iets niet in uw TL wordt herkend, is het immers binair per handtekening, verderop in de rij ALLEMAAL gaat, zal decoderen onmogelijk worden. Wat moet je doen in zo’n situatie?

Welnu, het eerste dat in iemands gedachten opkomt is de verbinding verbreken en het opnieuw proberen. Heeft niet geholpen. We googlen CRC32 - dit bleken objecten uit schema 73 te zijn, hoewel we aan 82 hebben gewerkt. We kijken zorgvuldig naar de logs - er zijn identificatiegegevens van twee verschillende schema's!

Misschien ligt het probleem puur bij onze onofficiële client? Nee, we lanceren Telegram Desktop 1.2.17 (versie geleverd in een aantal Linux-distributies), het schrijft naar het uitzonderingslogboek: MTP Onverwacht type id #b5223b0f gelezen in MTPMessageMedia…

Kritiek op het protocol en de organisatorische benaderingen van Telegram. Deel 1, technisch: ervaring met het schrijven van een klant vanuit het niets - TL, MT

Google liet zien dat een soortgelijk probleem zich al had voorgedaan bij een van de niet-officiële clients, maar toen waren de versienummers en dus de aannames anders...

Dus, wat zullen we doen? Vasily en ik gingen uit elkaar: hij probeerde het circuit bij te werken naar 91, ik besloot een paar dagen te wachten en 73 te proberen. Beide methoden werkten, maar omdat ze empirisch zijn, is er geen inzicht in hoeveel versies omhoog of omlaag je nodig hebt om te springen, of hoe lang je moet wachten.

Later kon ik de situatie reproduceren: we starten de client, schakelen hem uit, compileren het circuit opnieuw naar een andere laag, herstarten, vangen het probleem opnieuw op, keren terug naar de vorige - oeps, er is geen circuitwisseling geweest en de client is een tijdje opnieuw opgestart een paar minuten zullen helpen. Je krijgt een mix van datastructuren uit verschillende lagen.

Uitleg? Zoals u kunt raden aan de hand van verschillende indirecte symptomen, bestaat de server uit veel processen van verschillende typen op verschillende machines. Hoogstwaarschijnlijk heeft de server die verantwoordelijk is voor het “bufferen” in de wachtrij gezet wat zijn superieuren eraan hebben gegeven, en zij hebben het gegeven in het schema dat bestond op het moment van generatie. En totdat deze wachtrij “rot” was, kon er niets aan worden gedaan.

Misschien... maar dit is een vreselijke kruk?!.. Nee, laten we, voordat we aan gekke ideeën denken, eens kijken naar de code van de officiële klanten. In de Android-versie vinden we geen TL-parser, maar wel een flink bestand (GitHub weigert het te retoucheren) met (de)serialisatie. Hier zijn de codefragmenten:

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;

of

    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... ziet er wild uit. Maar waarschijnlijk is dit gegenereerde code, oké?.. Maar het ondersteunt zeker alle versies! Het is waar dat het niet duidelijk is waarom alles door elkaar loopt, geheime chats en dergelijke _old7 op de een of andere manier lijkt het niet op machinegeneratie... Maar ik werd er vooral door weggeblazen

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Jongens, kunnen jullie niet eens beslissen wat er in één laag zit?! Nou, oké, laten we zeggen dat er “twee” zijn vrijgegeven met een fout, nou, dat gebeurt, maar DRIE?.. Meteen weer dezelfde rake? Wat is dit voor pornografie, sorry?

In de broncode van Telegram Desktop gebeurt trouwens iets soortgelijks - als dat zo is, veranderen verschillende commits op rij in het schema het laagnummer niet, maar repareren ze iets. In omstandigheden waarin er geen officiële gegevensbron voor het schema bestaat, waar kunnen deze dan worden verkregen, behalve de broncode van de officiële klant? En als je verder gaat, kun je er pas zeker van zijn dat het schema volledig correct is als je alle methoden hebt getest.

Hoe kan dit überhaupt getest worden? Ik hoop dat fans van unit-, functionele en andere tests dit in de reacties zullen delen.

Oké, laten we eens naar een ander stukje code kijken:

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;

Deze opmerking “handmatig gemaakt” suggereert dat slechts een deel van dit bestand handmatig is geschreven (kun je je de hele onderhoudsnachtmerrie voorstellen?), en de rest is machinaal gegenereerd. Dan rijst er echter een andere vraag: of de bronnen beschikbaar zijn niet helemaal (a la GPL-blobs in de Linux-kernel), maar dit is al een onderwerp voor het tweede deel.

Maar genoeg. Laten we verder gaan met het protocol waarop al deze serialisatie draait.

MT-proto

Laten we dus opengaan algemene beschrijving и gedetailleerde beschrijving van het protocol en het eerste waar we over struikelen is de terminologie. En met een overvloed aan alles. Over het algemeen lijkt dit een eigen kenmerk van Telegram te zijn: dingen anders noemen op verschillende plaatsen, of verschillende dingen met één woord, of omgekeerd (als je bijvoorbeeld in een API op hoog niveau een stickerpakket ziet, is dat niet het geval). wat jij dacht).

‘bericht’ en ‘sessie’ betekenen hier bijvoorbeeld iets anders dan in de gebruikelijke Telegram-clientinterface. Welnu, alles is duidelijk met de boodschap, het kan worden geïnterpreteerd in OOP-termen, of eenvoudigweg het woord "pakket" worden genoemd - dit is een laag transportniveau, er zijn niet dezelfde berichten als in de interface, er zijn veel serviceberichten . Maar de sessie... maar eerst de dingen eerst.

transport laag

Het eerste is vervoer. Ze zullen ons over 5 opties vertellen:

  • TCP
  • web-socket
  • Websocket via HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Er is ook UDP-transport, maar dit is niet gedocumenteerd

En TCP in drie varianten

De eerste is vergelijkbaar met UDP via TCP, elk pakket bevat een volgnummer en crc
Waarom is het lezen van documenten op een karretje zo pijnlijk?

Nou, daar is het nu TCP al in 4 varianten:

  • verkorte
  • Gemiddeld
  • Gevoerd tussenstuk
  • Vol

Nou, oké, opgevuld tussenproduct voor MTProxy, dit is later toegevoegd vanwege bekende evenementen. Maar waarom nog twee versies (drie in totaal) als je met één ook rond kunt komen? Alle vier verschillen in wezen alleen in de manier waarop de lengte en het laadvermogen van de belangrijkste MTProto worden ingesteld, wat verder zal worden besproken:

  • in verkorte versie is het 1 of 4 bytes, maar niet 0xef, dan de body
  • in Intermediate is dit 4 bytes lang en een veld, en de eerste keer dat de client moet verzenden 0xeeeeeeee om aan te geven dat het gemiddeld is
  • Kortom het meest verslavend, vanuit het oogpunt van een netwerker: lengte, volgnummer, en NIET DE ENE die voornamelijk MTProto is, body, CRC32. Ja, dit alles bevindt zich bovenop TCP. Dit levert ons betrouwbaar transport op in de vorm van een sequentiële bytestroom; er zijn geen reeksen nodig, vooral geen controlesommen. Oké, nu zal iemand tegen mij bezwaar maken dat TCP een 16-bits controlesom heeft, dus gegevensbeschadiging vindt plaats. Geweldig, maar we hebben eigenlijk een cryptografisch protocol met hashes langer dan 16 bytes, al deze fouten – en zelfs meer – zullen worden opgevangen door een SHA-mismatch op een hoger niveau. Daarbovenop heeft CRC32 GEEN zin.

Laten we Abridged, waarin één byte lengte mogelijk is, vergelijken met Intermediate, wat rechtvaardigt "In het geval dat data-uitlijning van 4 bytes nodig is", wat nogal onzin is. Er wordt aangenomen dat Telegram-programmeurs zo incompetent zijn dat ze geen gegevens van een socket in een uitgelijnde buffer kunnen lezen? Je moet dit nog steeds doen, omdat het lezen je een willekeurig aantal bytes kan opleveren (en er zijn bijvoorbeeld ook proxyservers...). Of aan de andere kant, waarom zou je Abridged blokkeren als we nog steeds een flinke opvulling bovenop 16 bytes hebben - bespaar 3 bytes soms ?

Je krijgt de indruk dat Nikolai Durov het heel leuk vindt om wielen opnieuw uit te vinden, inclusief netwerkprotocollen, zonder enige echte praktische noodzaak.

Overige transportmogelijkheden, incl. Web en MTProxy, we zullen er nu niet over nadenken, misschien in een andere post, als er een verzoek is. Wat dezelfde MTProxy betreft, laten we er nu pas aan denken dat providers kort na de release in 2018 snel leerden deze te blokkeren, bedoeld voor blokkering omzeilenDoor Pakketgrootte! En ook het feit dat de MTProxy-server, geschreven (opnieuw door Waltman) in C, overdreven gebonden was aan Linux-specificaties, hoewel dit helemaal niet nodig was (Phil Kulin zal bevestigen), en dat een vergelijkbare server, hetzij in Go of Node.js, passen in minder dan honderd regels.

Maar we zullen aan het einde van dit hoofdstuk conclusies trekken over de technische geletterdheid van deze mensen, nadat we andere kwesties hebben overwogen. Laten we voorlopig verder gaan met OSI-laag 5, sessie - waarop ze de MTProto-sessie hebben geplaatst.

Sleutels, berichten, sessies, Diffie-Hellman

Ze hebben het daar niet helemaal correct geplaatst... Een sessie is niet dezelfde sessie die zichtbaar is in de interface onder Actieve sessies. Maar in volgorde.

Kritiek op het protocol en de organisatorische benaderingen van Telegram. Deel 1, technisch: ervaring met het schrijven van een klant vanuit het niets - TL, MT

We hebben dus een bytereeks met bekende lengte ontvangen van de transportlaag. Dit is óf een gecodeerd bericht, óf platte tekst – als we ons nog in de belangrijkste fase van de overeenkomst bevinden en dit ook daadwerkelijk doen. Over welke van de concepten die “sleutel” worden genoemd, hebben we het? Laten we dit probleem voor het Telegram-team zelf ophelderen (mijn excuses voor het vertalen van mijn eigen documentatie uit het Engels met een vermoeid brein om 4 uur 's ochtends, het was gemakkelijker om sommige zinnen te laten zoals ze zijn):

Er worden twee entiteiten genoemd Sessie - één in de gebruikersinterface van officiële clients onder “huidige sessies”, waarbij elke sessie overeenkomt met een volledig apparaat / besturingssysteem.
De tweede - MTProto-sessie, waarin het volgnummer van het bericht (op een laag niveau) staat, en welke kan duren tussen verschillende TCP-verbindingen. Er kunnen meerdere MTProto-sessies tegelijkertijd worden geïnstalleerd, om bijvoorbeeld het downloaden van bestanden te versnellen.

Tussen deze twee sessies er is een concept machtiging. In het gedegenereerde geval kunnen we dat zeggen UI-sessie is hetzelfde als machtiging, maar helaas is alles ingewikkeld. Laten we kijken:

  • De gebruiker op het nieuwe apparaat genereert eerst inlogcode en koppelt het aan een account, bijvoorbeeld via sms - daarom machtiging
  • Het gebeurde in de eerste MTProto-sessie, welke heeft session_id in jezelf.
  • Bij deze stap de combinatie machtiging и session_id gebeld kon worden instantie - dit woord komt voor in de documentatie en code van sommige clients
  • Vervolgens kan de klant openen verscheidene MTProto-sessies onder hetzelfde inlogcode - naar hetzelfde DC.
  • Dan zal de klant op een dag het bestand moeten opvragen nog een DC - en voor deze DC wordt een nieuwe gegenereerd inlogcode !
  • Om het systeem te informeren dat het geen nieuwe gebruiker is die zich registreert, maar dezelfde machtiging (UI-sessie), gebruikt de client API-aanroepen auth.exportAuthorization in thuis-DC auth.importAuthorization in het nieuwe DC.
  • Alles is hetzelfde, er kunnen er meerdere open zijn MTProto-sessies (elk met zijn eigen session_id) naar deze nieuwe DC, onder zijn inlogcode.
  • Ten slotte wil de klant misschien Perfect Forward Secrecy. Elk inlogcode был blijvend toets - per DC - en de klant kan bellen auth.bindTempAuthKey voor gebruik tijdelijk inlogcode - en nogmaals, slechts één temp_auth_key per DC, gemeenschappelijk voor iedereen MTProto-sessies naar dit gelijkstroom.

Merk op dat zout (en toekomstige zouten) is er ook één aan inlogcode die. gedeeld tussen iedereen MTProto-sessies naar hetzelfde DC.

Wat betekent "tussen verschillende TCP-verbindingen"? Dit betekent dus zoiets als autorisatiecookie op een website - het blijft vele TCP-verbindingen met een bepaalde server behouden (overleeft), maar op een dag gaat het kapot. Alleen in tegenstelling tot HTTP worden berichten binnen een sessie in MTProto opeenvolgend genummerd en bevestigd; als ze de tunnel binnenkwamen, werd de verbinding verbroken - na het tot stand brengen van een nieuwe verbinding zal de server zo vriendelijk zijn alles in deze sessie te verzenden wat hij in de vorige niet heeft afgeleverd TCP-verbinding.

De bovenstaande informatie is echter samengevat na vele maanden onderzoek. Zijn we ondertussen onze klant helemaal opnieuw aan het implementeren? - laten we teruggaan naar het begin.

Dus laten we genereren auth_key op Diffie-Hellman-versies van Telegram. Laten we proberen de documentatie te begrijpen...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (willekeurige willekeurige bytes); zodanig dat de lengte gelijk is aan 255 bytes;
gecodeerde_data:= RSA(data_with_hash, server_public_key); een getal van 255 bytes (big endian) wordt verhoogd tot de vereiste macht over de vereiste modulus, en het resultaat wordt opgeslagen als een getal van 256 bytes.

Ze hebben wat verdomde DH

Lijkt niet op de DH van een gezond persoon
Er zijn geen twee publieke sleutels in dx

Nou, uiteindelijk werd dit opgelost, maar er bleef een residu achter: het bewijs van het werk werd door de klant gedaan dat hij het aantal kon ontbinden. Type bescherming tegen DoS-aanvallen. En de RSA-sleutel wordt slechts één keer in één richting gebruikt, voornamelijk voor codering new_nonce. Maar hoewel deze ogenschijnlijk eenvoudige operatie zal slagen, waar zult u dan mee te maken krijgen?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Ik heb het appid-verzoek nog niet ontvangen

Ik heb dit verzoek naar DH gestuurd

En in het transportdock staat dat het kan reageren met 4 bytes foutcode. Dat is alles

Nou, hij vertelde me -404, dus wat?

Dus ik zei tegen hem: "Vang je onzin gecodeerd met een serversleutel met een vingerafdruk als deze, ik wil DH", en hij reageerde met een stomme 404

Wat zou je van deze serverreactie vinden? Wat moeten we doen? Er is niemand om het te vragen (maar daarover meer in het tweede deel).

Hier wordt alle belangstelling op de kade gedaan

Ik heb niets anders te doen, ik droomde er gewoon van om getallen heen en weer te converteren

Twee 32-bits getallen. Ik heb ze ingepakt, net als iedereen

Maar nee, deze twee moeten eerst als BE aan de regel worden toegevoegd

Vadim Goncharov, [20.06.18 15:49] en daarom 404?

Vasily, [20.06.18 15:49] JA!

Vadim Goncharov, [20.06.18 15:50] dus ik begrijp niet wat hij kan “niet vinden”

Vasili, [20.06.18 15:50] over

Ik kon een dergelijke ontleding in priemfactoren niet vinden%)

We hebben niet eens de foutrapportage beheerd

Vasily, [20.06.18 20:18] Oh, er is ook MD5. Al drie verschillende hashes

De sleutelvingerafdruk wordt als volgt berekend:

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

SHA1 en sha2

Dus laten we het zeggen auth_key we hebben 2048 bits ontvangen met behulp van Diffie-Hellman. Wat is het volgende? Vervolgens ontdekken we dat de onderste 1024 bits van deze sleutel op geen enkele manier worden gebruikt... maar laten we hier even over nadenken. Bij deze stap hebben we een gedeeld geheim met de server. Er is een analoog van de TLS-sessie tot stand gebracht, wat een zeer dure procedure is. Maar de server weet nog steeds niets over wie we zijn! Nog niet, eigenlijk. autorisatie. Die. als je dacht in termen van “login-wachtwoord”, zoals je ooit deed in ICQ, of op zijn minst “login-sleutel”, zoals in SSH (bijvoorbeeld op een of andere gitlab/github). Wij hebben een anonieme ontvangen. Wat als de server ons vertelt dat “deze telefoonnummers worden beheerd door een andere DC”? Of zelfs “uw telefoonnummer is verboden”? Het beste wat we kunnen doen is de sleutel bewaren in de hoop dat hij bruikbaar zal zijn en tegen die tijd niet zal rotten.

We hebben het overigens onder voorbehoud “ontvangen”. Vertrouwen we bijvoorbeeld de server? Wat als het nep is? Er zouden cryptografische controles nodig zijn:

Vasily, [21.06.18 17:53] Ze bieden mobiele clients de mogelijkheid om een ​​2kbit-nummer te controleren op primaliteit%)

Maar het is helemaal niet duidelijk, nafeijoa

Vasily, [21.06.18 18:02] Het document zegt niet wat te doen als het niet eenvoudig blijkt te zijn

Niet gezegd. Laten we eens kijken wat de officiële Android-client in dit geval doet? A dat is wat (en ja, het hele bestand is interessant) - zoals ze zeggen, laat ik dit hier achter:

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

Nee, natuurlijk is die er nog sommige Er zijn tests voor de primairheid van een getal, maar persoonlijk heb ik niet meer voldoende kennis van wiskunde.

Oké, we hebben de hoofdsleutel. Om in te loggen, bijv. verzoeken verzendt, moet u verdere codering uitvoeren met behulp van AES.

De berichtsleutel wordt gedefinieerd als de 128 middelste bits van de SHA256 van de berichttekst (inclusief sessie, bericht-ID, enz.), inclusief de opvulbytes, voorafgegaan door 32 bytes uit de autorisatiesleutel.

Vasily, [22.06.18 14:08] Gemiddeld, teef, bits

Ontvangen auth_key. Alle. Buiten hen... het blijkt niet duidelijk uit het document. Voel je vrij om de open source-code te bestuderen.

Merk op dat MTProto 2.0 12 tot 1024 bytes aan opvulling vereist, nog steeds onder de voorwaarde dat de resulterende berichtlengte deelbaar is door 16 bytes.

Dus hoeveel vulling moet je toevoegen?

En ja, er is ook een 404 bij een fout

Als iemand het diagram en de tekst van de documentatie zorgvuldig bestudeerde, merkte hij dat er geen MAC is. En dat AES wordt gebruikt in een bepaalde IGE-modus die nergens anders wordt gebruikt. Ze schrijven hierover uiteraard in hun FAQ... Hier is de berichtsleutel zelf ook de SHA-hash van de gedecodeerde gegevens, die wordt gebruikt om de integriteit te controleren - en in het geval van een mismatch, de documentatie om de een of andere reden raadt aan om ze stilzwijgend te negeren (maar hoe zit het met de veiligheid, wat als ze ons kapot maken?).

Ik ben geen cryptograaf, misschien is er in dit geval vanuit theoretisch oogpunt niets mis met deze modus. Maar ik kan duidelijk een praktisch probleem benoemen, met Telegram Desktop als voorbeeld. Het codeert de lokale cache (al deze D877F783D5D3EF8C) op dezelfde manier als berichten in MTProto (alleen in dit geval versie 1.0), d.w.z. eerst de berichtsleutel, dan de gegevens zelf (en ergens naast de belangrijkste big auth_key 256 bytes, zonder welke msg_key nutteloos). Het probleem wordt dus merkbaar bij grote bestanden. U moet namelijk twee kopieën van de gegevens bewaren: gecodeerd en gedecodeerd. En als er bijvoorbeeld megabytes zijn, of streaming video?.. Klassieke schema's met MAC achter de cijfertekst zorgen ervoor dat je deze stream kunt lezen en deze onmiddellijk kunt verzenden. Maar met MTProto zal het wel moeten in eerste instantie versleutel of decodeer het volledige bericht en breng het vervolgens over naar het netwerk of naar de schijf. Daarom worden in de nieuwste versies van Telegram Desktop de cache in user_data Er wordt ook een ander formaat gebruikt: met AES in CTR-modus.

Vasily, [21.06.18 01:27] Oh, ik heb ontdekt wat IGE is: IGE was de eerste poging tot een “authenticerende encryptiemodus”, oorspronkelijk voor Kerberos. Het was een mislukte poging (het biedt geen integriteitsbescherming) en moest worden verwijderd. Dat was het begin van een twintig jaar durende zoektocht naar een authenticerende encryptiemodus die werkt, die onlangs culmineerde in modi als OCB en GCM.

En nu de argumenten van de karkant:

Het team achter Telegram, onder leiding van Nikolai Durov, bestaat uit zes ACM-kampioenen, waarvan de helft een doctoraat in wiskunde heeft. Het kostte hen ongeveer twee jaar om de huidige versie van MTProto uit te rollen.

Dat is grappig. Twee jaar op het lagere niveau

Of je kunt gewoon tls nemen

Oké, laten we zeggen dat we de codering en andere nuances hebben gedaan. Is het eindelijk mogelijk om verzoeken geserialiseerd in TL te verzenden en de antwoorden te deserialiseren? Dus wat en hoe moet je verzenden? Hier, laten we zeggen, de methode initVerbinding, misschien is dit het?

Vasily, [25.06.18 18:46] Initialiseert de verbinding en slaat informatie op het apparaat en de applicatie van de gebruiker op.

Het accepteert app_id, device_model, system_version, app_version en lang_code.

En wat vragen

Documentatie zoals altijd. Voel je vrij om de open source te bestuderen

Als alles ongeveer duidelijk was met invokeWithLayer, wat is er dan mis? Het blijkt dat, laten we zeggen dat we - de client had al iets om de server over te vragen - er een verzoek is dat we wilden verzenden:

Vasily, [25.06.18 19:13] Afgaande op de code is de eerste oproep verpakt in deze onzin, en de onzin zelf is verpakt in invokewithlayer

Waarom kan initConnection geen afzonderlijke aanroep zijn, maar een wrapper? Ja, het bleek dat dit elke keer aan het begin van elke sessie moet worden gedaan, en niet één keer, zoals bij de hoofdsleutel. Maar! Het kan niet worden opgeroepen door een onbevoegde gebruiker! Nu hebben we het stadium bereikt waarin het toepasbaar is deze documentatiepagina - en het vertelt ons dat...

Slechts een klein deel van de API-methoden is beschikbaar voor ongeautoriseerde gebruikers:

  • auth.sendCode
  • auth.resendCode
  • account.getPassword
  • auth.checkWachtwoord
  • auth.checkTelefoon
  • auth.aanmelden
  • auth.aanmelden
  • auth.importAuthorisatie
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getVerschil
  • langpack.getLanguages
  • langpack.getLanguage

De allereerste daarvan, auth.sendCode, en er is dat gekoesterde eerste verzoek waarin we api_id en api_hash sturen, en waarna we een sms met een code ontvangen. En zitten we in het verkeerde DC (telefoonnummers in dit land worden bijvoorbeeld bediend door een ander DC), dan krijgen we een foutmelding met het nummer van het gewenste DC. Help ons om erachter te komen met welk IP-adres per DC-nummer u verbinding moet maken help.getConfig. Ooit waren er slechts 5 inzendingen, maar na de beroemde evenementen van 2018 is het aantal aanzienlijk toegenomen.

Laten we nu niet vergeten dat we anoniem in deze fase op de server zijn beland. Is het niet te duur om gewoon een IP-adres te krijgen? Waarom zou je dit, en andere bewerkingen, niet doen in het niet-versleutelde deel van MTProto? Ik hoor het bezwaar: “hoe kunnen we ervoor zorgen dat het niet RKN is die met valse adressen gaat reageren?” Hieraan herinneren we ons dat het over het algemeen officiële klanten zijn RSA-sleutels zijn ingebed, d.w.z. kun je gewoon teken deze informatie. Eigenlijk wordt dit al gedaan voor informatie over het omzeilen van blokkeringen die clients via andere kanalen ontvangen (logischerwijs kan dit niet in MTProto zelf; je moet ook weten waar je verbinding moet maken).

OK. In deze fase van de autorisatie van de klant zijn we nog niet geautoriseerd en hebben we onze aanvraag niet geregistreerd. We willen voorlopig alleen zien wat de server reageert op methoden die beschikbaar zijn voor een ongeautoriseerde gebruiker. En hier…

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

In het schema komt de eerste op de tweede plaats

In het tdesktop-schema is de derde waarde

Ja, sindsdien is de documentatie uiteraard bijgewerkt. Hoewel het binnenkort misschien weer irrelevant wordt. Hoe moet een beginnende ontwikkelaar dit weten? Misschien zullen zij u informeren als u uw aanvraag registreert? Vasily deed dit, maar helaas stuurden ze hem niets (nogmaals, we zullen hierover in het tweede deel praten).

...Je hebt gemerkt dat we op de een of andere manier al naar de API zijn overgestapt, d.w.z. naar het volgende niveau en iets gemist in het MTProto-onderwerp? Geen verrassing:

Vasily, [28.06.18 02:04] Mm, ze snuffelen door enkele algoritmen op e2e

Mtproto definieert versleutelingsalgoritmen en sleutels voor beide domeinen, evenals een beetje een wrapper-structuur

Maar ze mixen voortdurend verschillende niveaus van de stapel, dus het is niet altijd duidelijk waar mtproto eindigde en het volgende niveau begon

Hoe mixen ze? Welnu, hier is bijvoorbeeld dezelfde tijdelijke sleutel voor PFS (Telegram Desktop kan dit trouwens niet). Het wordt uitgevoerd door een API-verzoek auth.bindTempAuthKey, d.w.z. vanaf het hoogste niveau. Maar tegelijkertijd interfereert het met de codering op een lager niveau - daarna moet je het bijvoorbeeld opnieuw doen initConnection enz., dit is niet het geval gewoon normaal verzoek. Bijzonder is ook dat je per DC maar ÉÉN tijdelijke sleutel kunt hebben, ondanks het veld auth_key_id in elk bericht kunt u de sleutel van ten minste elk bericht wijzigen, en dat de server het recht heeft om de tijdelijke sleutel op elk moment te “vergeten” - de documentatie zegt niet wat u in dit geval moet doen... nou ja, waarom zou dat kunnen 'Heb je niet meerdere sleutels, zoals bij een set toekomstige zouten, en?..

Er zijn nog een paar andere dingen die het vermelden waard zijn over het MTProto-thema.

Berichtberichten, msg_id, msg_seqno, bevestigingen, pings in de verkeerde richting en andere eigenaardigheden

Waarom moet je meer over hen weten? Omdat ze naar een hoger niveau ‘lekken’ en je er rekening mee moet houden als je met de API werkt. Laten we aannemen dat we niet geïnteresseerd zijn in msg_key; het lagere niveau heeft alles voor ons gedecodeerd. Maar binnen de gedecodeerde gegevens hebben we de volgende velden (ook de lengte van de gegevens, dus we weten waar de opvulling is, maar dat is niet belangrijk):

  • zout - int64
  • sessie_id - int64
  • bericht_id — int64
  • seq_no - int32

Laten we u eraan herinneren dat er slechts één zout is voor het hele DC. Waarom weet je iets over haar? Niet alleen omdat er een verzoek is get_future_salts, die u vertelt welke intervallen geldig zijn, maar ook omdat als uw zout “rot” is, het bericht (verzoek) gewoon verloren gaat. De server zal het nieuwe zout uiteraard melden door het uit te geven new_session_created - maar bij de oude zul je hem bijvoorbeeld op de een of andere manier opnieuw moeten verzenden. En dit probleem heeft invloed op de applicatiearchitectuur.

De server mag om vele redenen sessies helemaal beëindigen en op deze manier reageren. Wat is eigenlijk een MTProto-sessie vanaf de clientkant? Dit zijn twee cijfers session_id и seq_no berichten binnen deze sessie. Nou ja, en de onderliggende TCP-verbinding natuurlijk. Laten we zeggen dat onze cliënt nog steeds niet weet hoe hij veel dingen moet doen, hij heeft de verbinding verbroken en opnieuw verbonden. Als dit snel gebeurde, werd de oude sessie voortgezet in de nieuwe TCP-verbinding seq_no verder. Als het lang duurt, kan de server het verwijderen, omdat het aan zijn kant ook een wachtrij is, zoals we ontdekten.

Wat zou het moeten zijn seq_no? O, dat is een lastige vraag. Probeer eerlijk te begrijpen wat er bedoeld werd:

Inhoudsgerelateerd bericht

Een bericht dat een expliciete bevestiging vereist. Deze omvatten alle gebruikers- en veel serviceberichten, vrijwel allemaal met uitzondering van containers en bevestigingen.

Berichtvolgordenummer (msg_seqno)

Een 32-bits getal dat gelijk is aan tweemaal het aantal ‘inhoudsgerelateerde’ berichten (de berichten die bevestiging vereisen, en met name de berichten die geen containers zijn) die door de afzender vóór dit bericht zijn aangemaakt en vervolgens met één worden verhoogd als het huidige bericht een inhoudelijke boodschap. Een container wordt altijd gegenereerd na de volledige inhoud; daarom is het volgnummer groter dan of gelijk aan de volgnummers van de berichten die het bevat.

Wat is dit voor circus met een verhoging van 1, en dan nog eens van 2?.. Ik vermoed dat ze in eerste instantie bedoelden “het minst significante bit voor ACK, de rest is een getal”, maar het resultaat is niet helemaal hetzelfde - in het bijzonder, het komt eruit, kan worden verzonden verscheidene bevestigingen met hetzelfde seq_no! Hoe? Welnu, de server stuurt ons bijvoorbeeld iets, verzendt het, en wij zwijgen zelf en reageren alleen met serviceberichten die de ontvangst van zijn berichten bevestigen. In dit geval hebben onze uitgaande bevestigingen hetzelfde uitgaande nummer. Als je bekend bent met TCP en dacht dat dit op de een of andere manier wild klinkt, maar het lijkt niet erg wild, want in TCP seq_no verandert niet, maar de bevestiging gaat naar seq_no aan de andere kant zal ik me haasten om je van streek te maken. Bevestigingen worden verstrekt in MTProto NIET op seq_no, zoals in TCP, maar door msg_id !

Wat is dit msg_id, de belangrijkste van deze velden? Een unieke bericht-ID, zoals de naam al doet vermoeden. Het wordt gedefinieerd als een 64-bits getal, waarvan de laagste bits opnieuw de ‘server-niet-server’-magie hebben, en de rest is een Unix-tijdstempel, inclusief het fractionele deel, 32 bits naar links verschoven. Die. tijdstempel op zichzelf (en berichten met tijden die te veel verschillen, worden door de server afgewezen). Hieruit blijkt dat het in het algemeen om een ​​identifier gaat die globaal is voor de cliënt. Laten we het daarom onthouden session_id - wij zijn gegarandeerd: Een bericht dat voor de ene sessie bedoeld is, kan in geen geval naar een andere sessie worden verzonden. Dat wil zeggen, het blijkt dat er al is drie niveau - sessie, sessienummer, bericht-ID. Waarom zo'n overcomplicatie, dit mysterie is erg groot.

aldus msg_id nodig voor...

RPC: verzoeken, antwoorden, fouten. Bevestigingen.

Zoals u wellicht heeft gemerkt, is er nergens in het diagram een ​​speciaal type "maak een RPC-verzoek" of functie aanwezig, hoewel er wel antwoorden zijn. Wij hebben immers inhoudelijke berichten! Dat is, elk de boodschap kan een verzoek zijn! Of niet te zijn. Ten slotte, elke er is msg_id. Maar er zijn antwoorden:

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

Hier wordt aangegeven op welk bericht dit een reactie is. Daarom moet u op het hoogste niveau van de API onthouden wat het nummer van uw verzoek was - ik denk dat het niet nodig is uit te leggen dat het werk asynchroon is en dat er tegelijkertijd meerdere verzoeken in uitvoering kunnen zijn, waarvan de antwoorden in willekeurige volgorde kunnen worden geretourneerd? In principe is hieruit en foutmeldingen like noworkers de architectuur hierachter te herleiden: de server die een TCP-verbinding met je onderhoudt is een front-end balancer, hij stuurt verzoeken door naar de backends en verzamelt ze terug via message_id. Het lijkt erop dat alles hier duidelijk, logisch en goed is.

Ja?.. En als je erover nadenkt? Het RPC-antwoord zelf heeft immers ook een veld msg_id! Moeten we tegen de server schreeuwen “je beantwoordt mijn antwoord niet!”? En ja, hoe zat het met bevestigingen? Over pagina berichten over berichten vertelt ons wat is

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

en het moet door elke kant gedaan worden. Maar niet altijd! Als u een RpcResult heeft ontvangen, geldt dit zelf als bevestiging. Dat wil zeggen dat de server op uw verzoek kan reageren met MsgsAck, bijvoorbeeld: "Ik heb het ontvangen." RpcResult kan direct reageren. Het kan allebei zijn.

En ja, je moet het antwoord nog beantwoorden! Bevestiging. Anders zal de server het als onbestelbaar beschouwen en het opnieuw naar u terugsturen. Zelfs na opnieuw verbinden. Maar hier doet zich natuurlijk de kwestie van time-outs voor. Laten we ze later eens bekijken.

Laten we in de tussentijd eens kijken naar mogelijke fouten bij het uitvoeren van query's.

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

Oh, iemand zal uitroepen, hier is een menselijker formaat - er is een regel! Neem de tijd. Hier lijst met fouten, maar uiteraard niet compleet. Hieruit leren we dat de code dat is zoiets als HTTP-fouten (nou ja, natuurlijk wordt de semantiek van de antwoorden niet gerespecteerd, op sommige plaatsen worden ze willekeurig verdeeld over de codes), en de regel ziet eruit als CAPITAL_LETTERS_AND_NUMBERS. Bijvoorbeeld PHONE_NUMBER_OCCUPIED of FILE_PART_Х_MISSING. Dat wil zeggen, je hebt deze lijn nog steeds nodig ontleden. Bijvoorbeeld FLOOD_WAIT_3600 betekent dat je een uur moet wachten, en PHONE_MIGRATE_5, dat een telefoonnummer met dit voorvoegsel in het 5e DC moet worden geregistreerd. We hebben een typetaal, toch? We hebben geen argument uit een string nodig, gewone argumenten zijn voldoende, oké.

Nogmaals, dit staat niet op de serviceberichtenpagina, maar zoals al gebruikelijk bij dit project is de informatie wel te vinden op een andere documentatiepagina. Of argwaan wekken. Ten eerste, kijk, type-/laagschending - RpcError kan worden ingebed RpcResult. Waarom niet buiten? Waar hebben we geen rekening mee gehouden? Waar is dus de garantie dat RpcError mag NIET worden ingebed RpcResult, maar direct zijn of genest in een ander type?.. En als dat niet kan, waarom bevindt het zich dan niet op het hoogste niveau, d.w.z. het ontbreekt req_msg_id ? ..

Maar laten we verder gaan over serviceberichten. De client denkt misschien dat de server lang nadenkt en doet dit prachtige verzoek:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Er zijn drie mogelijke antwoorden op deze vraag, die opnieuw kruisen met het bevestigingsmechanisme; proberen te begrijpen wat ze zouden moeten zijn (en wat de algemene lijst van typen is die geen bevestiging behoeven) wordt als huiswerk aan de lezer overgelaten (let op: de informatie in de broncode van Telegram Desktop is niet compleet).

Drugsverslaving: berichtstatussen

Over het algemeen laten veel plaatsen in TL, MTProto en Telegram in het algemeen een gevoel van koppigheid achter, maar uit beleefdheid, tact en anderen soft skills We zwegen er beleefd over en censureerden de obsceniteiten in de dialogen. Echter, deze plekОhet grootste deel van de pagina gaat over berichten over berichten Het is zelfs schokkend voor mij, die al heel lang met netwerkprotocollen werkt en fietsen in verschillende mate krom heeft gezien.

Het begint onschuldig, met bevestigingen. Vervolgens vertellen ze ons over

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;

Welnu, iedereen die met MTProto gaat werken, zal ermee te maken krijgen; in de cyclus "gecorrigeerd - opnieuw gecompileerd - gelanceerd" is het gebruikelijk om getalfouten of salt te krijgen die tijdens bewerkingen slecht zijn geworden. Er zijn hier echter twee punten:

  1. Dit betekent dat het oorspronkelijke bericht verloren gaat. We moeten een aantal wachtrijen creëren, daar zullen we later naar kijken.
  2. Wat zijn deze vreemde foutnummers? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... waar zijn de andere nummers, Tommy?

In de documentatie staat:

De bedoeling is dat error_code-waarden worden gegroepeerd (error_code >> 4): de codes 0x40 — 0x4f komen bijvoorbeeld overeen met fouten bij de ontleding van containers.

maar ten eerste een verschuiving in de andere richting, en ten tweede maakt het niet uit: waar zijn de andere codes? In het hoofd van de auteur?.. Dit zijn echter kleinigheden.

Verslaving begint in berichten over berichtstatussen en berichtkopieën:

  • Verzoek om berichtstatusinformatie
    Als een van beide partijen al een tijdje geen informatie heeft ontvangen over de status van zijn uitgaande berichten, kan hij deze expliciet opvragen bij de andere partij:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informatief bericht over de status van berichten
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Hier info is een tekenreeks die precies één byte aan berichtstatus bevat voor elk bericht uit de inkomende msg_ids-lijst:

    • 1 = er is niets bekend over het bericht (msg_id te laag, de andere partij is het misschien vergeten)
    • 2 = bericht niet ontvangen (msg_id valt binnen het bereik van opgeslagen identificatiegegevens; de andere partij heeft echter zeker niet zo'n bericht ontvangen)
    • 3 = bericht niet ontvangen (msg_id te hoog; de andere partij heeft het echter zeker nog niet ontvangen)
    • 4 = bericht ontvangen (merk op dat dit antwoord tegelijkertijd ook een ontvangstbevestiging is)
    • +8 = bericht al bevestigd
    • +16 = bericht waarvoor geen bevestiging vereist is
    • +32 = RPC-query opgenomen in bericht dat wordt verwerkt of dat de verwerking al is voltooid
    • +64 = inhoudelijke reactie op reeds gegenereerd bericht
    • +128 = andere partij weet zeker dat het bericht al is ontvangen
      Dit antwoord vereist geen bevestiging. Het is een erkenning van de relevante msgs_state_req, op zichzelf.
      Houd er rekening mee dat als plotseling blijkt dat de andere partij geen bericht heeft dat naar hem lijkt te zijn verzonden, het bericht eenvoudigweg opnieuw kan worden verzonden. Zelfs als de andere partij tegelijkertijd twee exemplaren van het bericht ontvangt, wordt het duplicaat genegeerd. (Als er te veel tijd is verstreken en de oorspronkelijke msg_id niet langer geldig is, moet het bericht in msg_copy worden verpakt).
  • Vrijwillige mededeling van de status van berichten
    Elke partij kan de andere partij vrijwillig informeren over de status van de door de andere partij verzonden berichten.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Uitgebreide vrijwillige mededeling van de status van één bericht
    ...
    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;
  • Expliciet verzoek om berichten opnieuw te verzenden
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    De externe partij reageert onmiddellijk door de gevraagde berichten opnieuw te verzenden […]
  • Expliciet verzoek om antwoorden opnieuw te verzenden
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    De externe partij reageert onmiddellijk door opnieuw te verzenden antwoorden naar de gevraagde berichten […]
  • Berichtkopieën
    In sommige situaties moet een oud bericht met een msg_id die niet langer geldig is, opnieuw worden verzonden. Vervolgens wordt het verpakt in een kopieercontainer:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Eenmaal ontvangen wordt het bericht verwerkt alsof de verpakking er niet is. Als echter zeker bekend is dat het bericht orig_message.msg_id is ontvangen, wordt het nieuwe bericht niet verwerkt (terwijl het tegelijkertijd met orig_message.msg_id wordt bevestigd). De waarde van orig_message.msg_id moet lager zijn dan de msg_id van de container.

Laten we zelfs zwijgen over wat msgs_state_info opnieuw steken de oren van de onvoltooide TL uit (we hadden een vector van bytes nodig, en in de onderste twee bits was er een opsomming, en in de hogere twee bits waren er vlaggen). Het punt is anders. Begrijpt iemand waarom dit allemaal in de praktijk gebeurt? bij een echte klant nodig?.. Met moeite, maar je kunt je enig voordeel voorstellen als een persoon bezig is met foutopsporing en in een interactieve modus - vraag de server wat en hoe. Maar hier worden de verzoeken beschreven rondvaart.

Hieruit volgt dat elke partij niet alleen berichten moet versleutelen en verzenden, maar ook gegevens over zichzelf en de reacties daarop moet opslaan, voor een onbekende tijdsduur. De documentatie beschrijft noch de timing, noch de praktische toepasbaarheid van deze functies. geenszins. Het meest verbazingwekkende is dat ze daadwerkelijk worden gebruikt in de code van officiële klanten! Blijkbaar werd hen iets verteld dat niet in de openbare documentatie stond. Begrijp het uit de code waarom, is niet langer zo eenvoudig als in het geval van TL - het is geen (relatief) logisch geïsoleerd onderdeel, maar een onderdeel dat verband houdt met de applicatiearchitectuur, d.w.z. zal aanzienlijk meer tijd nodig hebben om de applicatiecode te begrijpen.

Pingen en timings. Wachtrijen.

Als we ons de gissingen over de serverarchitectuur (verdeling van verzoeken over backends) herinneren, volgt uit alles iets triests - ondanks alle leveringsgaranties in TCP (de gegevens worden afgeleverd, of u wordt geïnformeerd over de kloof, maar de gegevens worden geleverd voordat het probleem zich voordoet), dat bevestigingen in MTProto zelf - geen garanties. De server kan uw bericht gemakkelijk kwijtraken of weggooien, en er kan niets aan worden gedaan, gebruik gewoon verschillende soorten krukken.

En allereerst: berichtenwachtrijen. Welnu, met één ding was alles vanaf het begin duidelijk: een onbevestigd bericht moet worden opgeslagen en opnieuw worden verzonden. En na hoe laat? En de nar kent hem. Misschien lossen die verslaafde serviceberichten dit probleem op de een of andere manier op met krukken, bijvoorbeeld in Telegram Desktop zijn er ongeveer 4 wachtrijen die ermee corresponderen (misschien meer, zoals al vermeld, hiervoor moet je je serieuzer in de code en architectuur ervan verdiepen; tegelijkertijd We weten dat het niet als voorbeeld kan worden genomen; een bepaald aantal typen uit het MTProto-schema worden er niet in gebruikt).

Waarom gebeurt dit? Waarschijnlijk waren de serverprogrammeurs niet in staat de betrouwbaarheid binnen het cluster te garanderen, of zelfs maar te bufferen op de frontbalancer, en hebben ze dit probleem overgedragen aan de client. Uit wanhoop probeerde Vasily een alternatieve optie te implementeren, met slechts twee wachtrijen, met behulp van algoritmen van TCP - het meten van de RTT naar de server en het aanpassen van de grootte van het "venster" (in berichten) afhankelijk van het aantal onbevestigde verzoeken. Dat wil zeggen, een ruwe heuristiek voor het beoordelen van de belasting van de server is hoeveel van onze verzoeken hij tegelijkertijd kan kauwen en niet verliest.

Nou, dat wil zeggen, je begrijpt het toch? Als je TCP opnieuw moet implementeren bovenop een protocol dat over TCP loopt, duidt dit op een zeer slecht ontworpen protocol.

Oh ja, waarom heb je meer dan één wachtrij nodig, en wat betekent dit eigenlijk voor iemand die met een API op hoog niveau werkt? Kijk, je doet een verzoek, serialiseert het, maar vaak kun je het niet meteen verzenden. Waarom? Want het antwoord zal zijn msg_id, wat tijdelijk isаIk ben een label waarvan de opdracht het beste kan worden uitgesteld tot zo laat mogelijk - voor het geval de server deze afwijst vanwege een tijdsverschil tussen ons en hem (we kunnen natuurlijk een kruk maken die onze tijd verschuift van het heden naar de server door een delta toe te voegen die is berekend op basis van de antwoorden van de server (officiële clients doen dit, maar het is grof en onnauwkeurig vanwege buffering). Wanneer u daarom een ​​verzoek indient met een lokale functieaanroep vanuit de bibliotheek, doorloopt het bericht de volgende fasen:

  1. Het ligt in één wachtrij en wacht op codering.
  2. Benoemd msg_id en het bericht ging naar een andere wachtrij - mogelijk doorgestuurd; naar het stopcontact sturen.
  3. a) De server antwoordde MsgsAck - het bericht is afgeleverd, we verwijderen het uit de "andere wachtrij".
    b) Of andersom, hij vond iets niet leuk, hij antwoordde badmsg - opnieuw verzenden vanuit "een andere wachtrij"
    c) Er is niets bekend, het bericht moet opnieuw worden verzonden vanuit een andere wachtrij, maar het is niet precies bekend wanneer.
  4. De server reageerde uiteindelijk RpcResult - de daadwerkelijke respons (of fout) - niet alleen geleverd, maar ook verwerkt.

Misschienzou het gebruik van containers het probleem gedeeltelijk kunnen oplossen. Dit is wanneer een aantal berichten in één keer zijn verpakt en de server heeft gereageerd met een bevestiging op alle berichten tegelijk, in één keer msg_id. Maar hij zal ook dit pakket, als er iets misgaat, in zijn geheel afwijzen.

En op dit punt spelen niet-technische overwegingen een rol. Uit ervaring hebben we veel krukken gezien, en bovendien zullen we nu meer voorbeelden zien van slecht advies en slechte architectuur - is het in dergelijke omstandigheden de moeite waard om te vertrouwen en dergelijke beslissingen te nemen? De vraag is retorisch (natuurlijk niet).

Waar hebben we het over? Als je over het onderwerp ‘drugsberichten over berichten’ nog steeds kunt speculeren met bezwaren als ‘je bent stom, je hebt ons briljante plan niet begrepen!’ (schrijf dus eerst de documentatie, zoals normale mensen zouden moeten doen, met de grondgedachte en voorbeelden van pakketuitwisseling, dan zullen we praten), daarna zijn timings/time-outs een puur praktische en specifieke vraag, alles hier is al lang bekend. Wat vertelt de documentatie ons over time-outs?

Een server bevestigt doorgaans de ontvangst van een bericht van een client (normaal gesproken een RPC-query) met behulp van een RPC-antwoord. Als het antwoord lang op zich laat wachten, kan een server eerst een ontvangstbevestiging sturen en iets later het RPC-antwoord zelf.

Normaal gesproken bevestigt een client de ontvangst van een bericht van een server (meestal een RPC-antwoord) door een bevestiging toe te voegen aan de volgende RPC-query als deze niet te laat wordt verzonden (als deze bijvoorbeeld 60-120 seconden na de ontvangst wordt gegenereerd). van een bericht van de server). Als er echter gedurende langere tijd geen reden is om berichten naar de server te sturen of als er een groot aantal niet-bevestigde berichten van de server is (bijvoorbeeld meer dan 16), verzendt de client een zelfstandige bevestiging.

... ik vertaal: we weten zelf niet hoeveel en hoe we het nodig hebben, dus laten we aannemen dat het zo is.

En over pingelen:

Ping-berichten (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Een antwoord wordt meestal teruggestuurd naar dezelfde verbinding:

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

Deze berichten vereisen geen bevestiging. Een pong wordt alleen verzonden als reactie op een ping, terwijl een ping door beide kanten kan worden geïnitieerd.

Uitgestelde verbindingssluiting + PING

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

Werkt als ping. Bovendien start de server, nadat dit is ontvangen, een timer die de huidige verbinding disconnect_delay seconden later zal sluiten, tenzij hij een nieuw bericht van hetzelfde type ontvangt, waardoor alle voorgaande timers automatisch worden gereset. Als de client deze pings bijvoorbeeld elke 60 seconden verzendt, kan hij disconnect_delay op 75 seconden instellen.

Ben je gek?! Binnen 60 seconden rijdt de trein het station binnen, zet passagiers af en haalt ze op en verliest opnieuw het contact in de tunnel. Binnen 120 seconden, terwijl je het hoort, komt het bij een ander aan en zal de verbinding hoogstwaarschijnlijk worden verbroken. Welnu, het is duidelijk waar de benen vandaan komen - "Ik hoorde een bel, maar weet niet waar het is", er is het algoritme van Nagl en de TCP_NODELAY-optie, bedoeld voor interactief werk. Maar neem me niet kwalijk, houd vast aan de standaardwaarde: 200 Milliseconden Als je echt iets soortgelijks wilt weergeven en een mogelijk aantal pakketten wilt opslaan, stel het dan 5 seconden uit, of wat de time-out van het bericht "Gebruiker is aan het typen..." nu ook is. Maar niet meer.

En tot slot: ping. Dat wil zeggen, het controleren van de liveheid van de TCP-verbinding. Het is grappig, maar ongeveer 10 jaar geleden schreef ik een kritische tekst over de boodschapper van het studentenhuis van onze faculteit - de auteurs daar pingden ook de server vanaf de client, en niet andersom. Maar derdejaarsstudenten zijn één ding, en een internationaal kantoor is iets anders, toch?

Eerst een klein educatief programma. Een TCP-verbinding kan, als er geen pakketuitwisseling plaatsvindt, wekenlang meegaan. Dit is zowel goed als slecht, afhankelijk van het doel. Het is goed als je een SSH-verbinding met de server open had, je opstond van de computer, de router opnieuw opstartte en terugkeerde naar je plaats - de sessie via deze server was niet gescheurd (je hebt niets getypt, er waren geen pakketten) , het is handig. Het is erg als er duizenden clients op de server staan, die allemaal bronnen in beslag nemen (hallo, Postgres!), En de host van de client is misschien al lang geleden opnieuw opgestart - maar we zullen er niets van weten.

Chat-/IM-systemen vallen om nog een reden in het tweede geval: onlinestatussen. Als de gebruiker "eraf viel", moet u zijn gesprekspartners hierover informeren. Anders krijg je een fout die de makers van Jabber hebben gemaakt (en gedurende 20 jaar gecorrigeerd) - de gebruiker heeft de verbinding verbroken, maar ze blijven berichten naar hem schrijven, in de overtuiging dat hij online is (die ook volledig verloren zijn gegaan in deze enkele minuten voordat de verbinding werd ontdekt). Nee, de TCP_KEEPALIVE-optie, die veel mensen die niet begrijpen hoe TCP-timers werken willekeurig invoeren (door wilde waarden in te stellen zoals tientallen seconden), zal hier niet helpen - je moet ervoor zorgen dat niet alleen de OS-kernel van de machine van de gebruiker leeft, maar functioneert ook normaal, kan reageren, en de applicatie zelf (denk je dat deze niet kan vastlopen? Telegram Desktop op Ubuntu 18.04 is voor mij meer dan eens vastgelopen).

Daarom moet je pingen server client, en niet andersom - als de client dit doet en de verbinding wordt verbroken, wordt de ping niet geleverd en wordt het doel niet bereikt.

Wat zien we op Telegram? Het is precies het tegenovergestelde! Nou ja, dat is het. Formeel kunnen beide partijen elkaar natuurlijk pingen. In de praktijk gebruiken cliënten een kruk ping_delay_disconnect, waarmee de timer op de server wordt ingesteld. Excuseer mij, het is niet aan de cliënt om te beslissen hoe lang hij daar wil blijven wonen zonder ping. De server weet, op basis van zijn belasting, beter. Maar als je de middelen niet erg vindt, dan ben je natuurlijk je eigen kwaadaardige Pinocchio, en een kruk is ook voldoende...

Hoe had het ontworpen moeten worden?

Ik ben van mening dat de bovenstaande feiten duidelijk aangeven dat het Telegram/VKontakte-team niet erg bekwaam is op het gebied van transport (en lager) niveau van computernetwerken en hun lage kwalificaties in relevante zaken.

Waarom bleek het zo ingewikkeld te zijn, en hoe kunnen Telegram-architecten proberen bezwaar te maken? Het feit dat ze probeerden een sessie te maken die de TCP-verbindingsbreuken overleeft, d.w.z. wat nu niet werd afgeleverd, zullen we later leveren. Ze hebben waarschijnlijk ook geprobeerd een UDP-transport te maken, maar ze ondervonden problemen en hebben het opgegeven (daarom is de documentatie leeg - er was niets om over op te scheppen). Maar door een gebrek aan begrip van hoe netwerken in het algemeen en TCP in het bijzonder werken, waar je erop kunt vertrouwen, en waar je dat zelf moet doen (en hoe), en een poging om dit te combineren met cryptografie “twee vogels met één steen”, is dit het resultaat.

Hoe was het nodig? Gebaseerd op het feit dat msg_id Als een tijdstempel vanuit cryptografisch oogpunt noodzakelijk is om replay-aanvallen te voorkomen, is het een vergissing om er een unieke identificatiefunctie aan te koppelen. Daarom zou men, zonder de huidige architectuur fundamenteel te veranderen (wanneer de Updates-stream wordt gegenereerd, dat is een API-onderwerp op hoog niveau voor een ander deel van deze reeks berichten), het volgende moeten doen:

  1. De server die de TCP-verbinding met de client verzorgt, neemt de verantwoordelijkheid. Als deze uit de socket heeft gelezen, bevestig, verwerk of retourneer dan een fout, geen verlies. Dan is de bevestiging geen vector van ID's, maar eenvoudigweg "de laatst ontvangen seq_no" - slechts een getal, zoals in TCP (twee getallen - uw seq en de bevestigde). We zijn altijd binnen de sessie, nietwaar?
  2. De tijdstempel om replay-aanvallen te voorkomen wordt a la nonce een apart veld. Het wordt gecontroleerd, maar heeft verder geen invloed. Genoeg en uint32 - als ons zout minstens elke halve dag verandert, kunnen we 16 bits toewijzen aan de bits van lage orde van een geheel getal van de huidige tijd, de rest - aan een fractioneel deel van een seconde (zoals nu).
  3. VERWIJDERD msg_id helemaal niet - vanuit het oogpunt van het onderscheiden van verzoeken op de backends is er in de eerste plaats de client-ID en ten tweede de sessie-ID, die deze samenvoegt. Dienovereenkomstig is slechts één ding voldoende als verzoekidentificatie seq_no.

Dit is ook niet de meest succesvolle optie; een volledige willekeurige zou als identificatie kunnen dienen - dit gebeurt overigens al in de high-level API bij het verzenden van een bericht. Het zou beter zijn om de architectuur volledig opnieuw te maken van relatief naar absoluut, maar dit is een onderwerp voor een ander deel, niet voor dit bericht.

API?

Ta-daam! Dus nadat we ons door een pad vol pijn en krukken hadden geworsteld, waren we eindelijk in staat om verzoeken naar de server te sturen en antwoorden daarop te ontvangen, en om updates van de server te ontvangen (niet als reactie op een verzoek, maar het zelf stuurt ons, zoals PUSH, als iemand het op die manier duidelijker is).

Let op, nu staat er het enige voorbeeld in Perl in het artikel! (voor degenen die niet bekend zijn met de syntaxis: het eerste argument van zegen is de datastructuur van het object, het tweede is de klasse):

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

Ja, geen spoiler met opzet - als je het nog niet hebt gelezen, ga je gang en doe het!

Oh, wai~~... hoe ziet dit eruit? Iets heel bekends... misschien is dit de datastructuur van een typische web-API in JSON, behalve dat klassen ook aan objecten zijn gekoppeld?

Dus dit is hoe het blijkt... Waar gaat het allemaal over, kameraden?... Zoveel moeite - en we stopten om uit te rusten waar de webprogrammeurs net begonnen?..Zou alleen JSON via HTTPS niet eenvoudiger zijn?! Wat kregen we in ruil? Was de moeite het waard?

Laten we evalueren wat TL+MTProto ons heeft gegeven en welke alternatieven mogelijk zijn. Welnu, HTTP, dat zich richt op het verzoek-antwoordmodel, past niet goed bij elkaar, maar in ieder geval iets bovenop TLS?

Compacte serialisatie. Toen ik deze datastructuur zag, vergelijkbaar met JSON, herinner ik me dat er binaire versies van zijn. Laten we MsgPack markeren als onvoldoende uitbreidbaar, maar er is bijvoorbeeld CBOR - trouwens, een standaard beschreven in RFC 7049. Het valt op door het feit dat het definieert tags, als expansiemechanisme, en onder al gestandaardiseerd beschikbaar:

  • 25 + 256 - herhaalde regels vervangen door een verwijzing naar het regelnummer, zo'n goedkope compressiemethode
  • 26 - geserialiseerd Perl-object met klassenaam en constructorargumenten
  • 27 - geserialiseerd taalonafhankelijk object met typenaam en constructorargumenten

Welnu, ik heb geprobeerd dezelfde gegevens in TL en in CBOR te serialiseren met string- en objectverpakking ingeschakeld. Het resultaat begon ergens vanaf een megabyte in het voordeel van CBOR te variëren:

cborlen=1039673 tl_len=1095092

aldus conclusie: Er zijn aanzienlijk eenvoudiger formaten die niet onderhevig zijn aan het probleem van synchronisatiefouten of onbekende identificatie, met vergelijkbare efficiëntie.

Snelle verbindingsopbouw. Dit betekent nul RTT na opnieuw verbinden (wanneer de sleutel al één keer is gegenereerd) - toepasbaar vanaf het allereerste MTProto-bericht, maar met enig voorbehoud - op hetzelfde zout slaan, de sessie is niet verrot, enz. Wat biedt TLS ons in plaats daarvan? Citaat over onderwerp:

Bij gebruik van PFS in TLS, TLS-sessietickets (RFC 5077) om een ​​gecodeerde sessie te hervatten zonder opnieuw over sleutels te onderhandelen en zonder sleutelinformatie op de server op te slaan. Bij het openen van de eerste verbinding en het aanmaken van sleutels codeert de server de verbindingsstatus en verzendt deze naar de client (in de vorm van een sessieticket). Dienovereenkomstig stuurt de client, wanneer de verbinding wordt hervat, een sessieticket, inclusief de sessiesleutel, terug naar de server. Het ticket zelf is gecodeerd met een tijdelijke sleutel (sessieticketsleutel), die op de server wordt opgeslagen en moet worden gedistribueerd onder alle frontendservers die SSL verwerken in geclusterde oplossingen.[10]. De introductie van een sessieticket kan dus in strijd zijn met de PFS als tijdelijke serversleutels in gevaar komen, bijvoorbeeld wanneer ze lange tijd worden opgeslagen (OpenSSL, nginx, Apache slaan ze standaard op voor de gehele duur van het programma; populaire sites gebruiken de sleutel voor enkele uren, tot dagen).

Hier is de RTT niet nul, je moet minimaal ClientHello en ServerHello uitwisselen, waarna de client gegevens mee kan sturen met Finished. Maar hier moeten we niet vergeten dat we niet het web hebben, met zijn reeks nieuw geopende verbindingen, maar een boodschapper, waarvan de verbinding vaak bestaat uit één en min of meer langlevende, relatief korte verzoeken aan webpagina's - alles is gemultiplext intern. Dat wil zeggen, het is heel acceptabel als we niet een heel slecht metrogedeelte tegenkomen.

Nog iets vergeten? Schrijf in de reacties.

Wordt vervolgd!

In het tweede deel van deze reeks berichten zullen we niet ingaan op technische, maar op organisatorische kwesties: benaderingen, ideologie, interface, houding ten opzichte van gebruikers, enz. Gebaseerd echter op de technische informatie die hier werd gepresenteerd.

In het derde deel wordt de technische component/ontwikkelingservaring verder geanalyseerd. Je leert met name:

  • voortzetting van het pandemonium met de verscheidenheid aan TL-typen
  • onbekende dingen over kanalen en supergroepen
  • waarom dialogen slechter zijn dan roosters
  • over absolute versus relatieve berichtadressering
  • wat is het verschil tussen foto en afbeelding
  • hoe emoji de cursieve tekst verstoren

en andere krukken! Blijf kijken!

Bron: www.habr.com

Voeg een reactie