Kritik av Telegrams protokoll och organisatoriska tillvägagångssätt. Del 1, teknisk: erfarenhet av att skriva en klient från grunden - TL, MT

På senare tid har det börjat dyka upp inlägg oftare på Habré om hur bra Telegram är, hur briljanta och erfarna Durov-bröderna är i att bygga nätverkssystem osv. Samtidigt var det väldigt få som verkligen fördjupade sig i den tekniska enheten - på sin höjd använder de en ganska enkel (och väldigt annorlunda från MTProto) JSON-baserad Bot API, och accepterar vanligtvis bara på tro alla dessa lovord och PR som kretsar kring budbäraren. För nästan ett och ett halvt år sedan började min kollega på NPO Echelon Vasily (tyvärr togs hans konto på Habré bort tillsammans med utkastet) att skriva sin egen Telegram-klient från grunden i Perl, och senare anslöt författaren till dessa rader. Varför Perl, kommer vissa genast att fråga? Eftersom det redan finns sådana projekt på andra språk. Det är faktiskt inte meningen, det kan finnas något annat språk där färdigt bibliotek, och därför måste författaren gå hela vägen från början. Dessutom är kryptografi en sådan sak - lita på, men verifiera. Med en säkerhetsfokuserad produkt kan du inte bara lita på en leverantörs färdiga bibliotek och blint tro på det (dock är detta ett ämne för mer i den andra delen). För tillfället fungerar biblioteket ganska bra på "mellannivån" (låter dig göra alla API-förfrågningar).

Det blir dock inte mycket kryptografi och matematik i denna serie av inlägg. Men det kommer att finnas många andra tekniska detaljer och arkitektoniska kryckor (det kommer också att vara användbart för dem som inte kommer att skriva från grunden, utan kommer att använda biblioteket på vilket språk som helst). Så det huvudsakliga målet var att försöka implementera klienten från grunden enligt officiell dokumentation. Det vill säga, anta att källkoden för officiella klienter är stängd (igen, i den andra delen kommer vi att avslöja mer detaljerat ämnet för vad detta egentligen är händer så), men som i gamla dagar, till exempel, finns det en standard som RFC - är det möjligt att skriva en klient enligt specifikationen ensam, "utan att kika" in i källkoden, till och med officiell (Telegram Desktop, mobil ), även inofficiell Telethon?

Innehållsförteckning:

Dokumentation ... finns den där? Är det sant?..

Fragment av anteckningar till denna artikel började samlas in förra sommaren. Hela tiden på den officiella webbplatsen https://core.telegram.org dokumentationen var från och med skikt 23, dvs. fastnat någonstans 2014 (kom ihåg, då fanns det inte ens kanaler än?). Naturligtvis borde detta i teorin ha gjort det möjligt att implementera en klient med funktionalitet vid den tiden 2014. Men även i detta tillstånd var dokumentationen för det första ofullständig och för det andra på sina ställen motsäger den sig själv. För en dryg månad sedan, i september 2019, var det så av misstag det konstaterades att sajten har en stor uppdatering av dokumentationen, för ett helt färskt Layer 105, med en notering att nu måste allt läsas igen. Visserligen har många artiklar reviderats, men många har förblivit oförändrade. När du läser kritiken nedan om dokumentationen bör du därför ha i åtanke att vissa av dessa saker inte längre är relevanta, men vissa är fortfarande ganska. När allt kommer omkring är 5 år i den moderna världen inte bara mycket, utan mycket massor. Sedan dess (särskilt om du inte tar hänsyn till de kasserade och återuppståndna geochatsna sedan dess), har antalet API-metoder i systemet vuxit från hundra till mer än tvåhundrafemtio!

Var ska man börja som ung författare?

Det spelar ingen roll om du skriver från grunden eller använder till exempel färdiga bibliotek som Telethon för Python eller Madeline för PHP, i alla fall behöver du först registrera din ansökan - hämta parametrar api_id и api_hash (de som arbetade med VKontakte API förstår omedelbart) genom vilken servern kommer att identifiera applikationen. Detta måste av juridiska skäl, men vi kommer att prata mer om varför biblioteksförfattare inte kan publicera den i andra delen. Kanske kommer du att vara nöjd med testvärdena, även om de är mycket begränsade - faktum är att nu kan du registrera dig på ditt nummer bara en applikation, så skynda inte huvudstupa.

Nu borde vi ur teknisk synvinkel ha varit intresserade av att vi efter registrering ska få meddelanden från Telegram om uppdateringar av dokumentation, protokoll osv. Det vill säga, man kunde anta att platsen med bryggorna helt enkelt "poängdes" och fortsatte att arbeta specifikt med dem som började skapa kunder, eftersom. det är lättare. Men nej, inget sådant observerades, ingen information kom.

Och om du skriver från början, är användningen av de mottagna parametrarna faktiskt fortfarande långt borta. Fastän https://core.telegram.org/ och pratar om dem först i Komma igång, faktiskt måste du först implementera MTProto-protokoll - men om du tror layout enligt OSI-modellen i slutet av sidan av den allmänna beskrivningen av protokollet, då helt förgäves.

Faktum är att både före MTProto och efter, på flera nivåer samtidigt (som utländska nätverkare som arbetar i OS-kärnan säger, lageröverträdelse), kommer ett stort, smärtsamt och hemskt ämne att komma i vägen ...

Binär serialisering: TL (Type Language) och dess schema, och lager, och många andra skrämmande ord

Detta ämne är faktiskt nyckeln till Telegrams problem. Och det blir många hemska ord om du försöker fördjupa dig i det.

Alltså schema. Om du kommer ihåg detta ord, säg JSON-schemaDu trodde rätt. Målet är detsamma: något språk för att beskriva en möjlig uppsättning överförda data. Det är faktiskt där likheten slutar. Om från sidan MTProto-protokoll, eller från källträdet för den officiella klienten, kommer vi att försöka öppna något schema, vi kommer att se något i stil med:

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;

En person som ser detta för första gången kommer intuitivt bara att känna igen en del av det som skrivs - ja, det här är tydligen strukturer (även om var är namnet, till vänster eller till höger?), Det finns fält i dem, varefter typen går igenom tjocktarmen ... förmodligen. Här, inom vinkelparenteser, finns det förmodligen mallar som i C ++ (faktiskt, inte riktigt). Och vad betyder alla andra symboler, frågetecken, utropstecken, procentsatser, gitter (och uppenbarligen betyder de olika saker på olika ställen), finns någonstans, men inte någonstans, hexadecimala tal - och viktigast av allt, hur man kommer från detta höger (som inte kommer att avvisas av servern) byte stream? Du måste läsa dokumentationen (Ja, det finns länkar till schemat i JSON-versionen i närheten - men detta gör det inte tydligare).

Öppnar sidan Binär data serialisering och kasta dig in i svampens och diskreta matematikens magiska värld, något som liknar matan på 4:e året. Alfabet, typ, värde, kombinator, funktionell kombinator, normal form, sammansatt typ, polymorf typ... och det är bara första sidan! Nästa väntar på dig TL Språk, som, även om den redan innehåller ett exempel på en trivial begäran och svar, inte ger något svar på mer typiska fall alls, vilket innebär att du kommer att behöva vada genom återberättelsen av matematik översatt från ryska till engelska på åtta mer kapslade sidor!

Läsare som är bekanta med funktionella språk och automatisk typslutledning såg naturligtvis i detta språkbeskrivningar, även från ett exempel, mycket mer bekanta, och kan säga att detta i princip inte är dåligt. Invändningarna mot detta är:

  • Ja, mål låter bra, men tyvärr inte uppnått
  • utbildning vid ryska universitet varierar även mellan IT-specialiteter - inte alla läser motsvarande kurs
  • Slutligen, som vi ska se, är det i praktiken det inte nödvändigteftersom endast en begränsad delmängd av även den TL som beskrevs används

Som sagt Leonörd på kanalen #perl på FreeNode IRC-nätverket, försöker implementera en gate från Telegram till Matrix (översättningen av citatet är felaktig från minnet):

Det känns som att någon som introducerades för typteori för första gången, blev upphetsad och började försöka leka med den, inte riktigt brydde sig om om det var nödvändigt i praktiken.

Se själv om behovet av kala typer (int, long, etc.) som något elementärt inte väcker frågor - i slutändan måste de implementeras manuellt - låt oss till exempel ta ett försök att härleda från dem vektor. Det är faktiskt array, om du kallar de resulterande sakerna vid deras rätta namn.

Men först

Kort beskrivning av en delmängd av TL-syntaxen för de som inte... läs den officiella dokumentationen

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;

Börjar alltid definition designern, varefter valfritt (i praktiken alltid) genom symbolen # måste vara CRC32 från den normaliserade beskrivningssträngen av den givna typen. Därefter kommer beskrivningen av fälten, om de är det - typen kan vara tom. Det hela slutar med ett likhetstecken, namnet på den typ som den givna konstruktören - det vill säga undertypen - tillhör. Typen till höger om likhetstecknet är polymorf – det vill säga det kan motsvara flera specifika typer.

Om definitionen förekommer efter raden ---functions---, då kommer syntaxen att förbli densamma, men innebörden kommer att vara annorlunda: konstruktorn kommer att bli namnet på RPC-funktionen, fälten kommer att bli parametrar (det vill säga, det kommer att förbli exakt samma givna struktur som beskrivs nedan, det kommer bara att vara den angivna betydelsen), och "polymorf typ" är typen av det returnerade resultatet. Det är sant att det fortfarande förblir polymorft - precis definierat i avsnittet ---types---, och denna konstruktör kommer inte att beaktas. Skriv överbelastningar av anropade funktioner efter deras argument, dvs. av någon anledning tillhandahålls inte flera funktioner med samma namn men en annan signatur, som i C++, i TL.

Varför "konstruktor" och "polymorf" om det inte är OOP? Tja, faktiskt, det kommer att vara lättare för någon att tänka på det i termer av OOP - en polymorf typ som en abstrakt klass, och konstruktörer är dess direkta ättlingklasser, dessutom final i ett antal språks terminologi. Faktiskt såklart här likhet med verkligt överbelastade konstruktormetoder i OO-programmeringsspråk. Eftersom det bara finns datastrukturer här så finns det inga metoder (även om beskrivningen av funktioner och metoder nedan är ganska kapabel att skapa förvirring i huvudet om vad de är, men det handlar om något annat) - man kan tänka sig en konstruktör som en värde från vilket håller på att byggas typ när du läser en ström av byte.

Hur går det till? Deserializern, som alltid läser 4 byte, ser värdet 0xcrc32 - och förstår vad som kommer att hända härnäst field1 med typ int, dvs. läser exakt 4 byte, på detta överliggande fält med typ PolymorType läsa. Ser 0x2crc32 och förstår att det finns två fält längre, först long, så vi läser 8 byte. Och så igen en komplex typ, som deserialiseras på samma sätt. Till exempel, Type3 skulle kunna deklareras i schemat så snart två konstruktörer, respektive, ytterligare måste mötas antingen 0x12abcd34, varefter du behöver läsa ytterligare 4 byte intEller 0x6789cdef, varefter det inte blir något. Något annat - du måste kasta ett undantag. I alla fall, efter det återgår vi till att läsa 4 byte int fält field_c в constructorTwo och på det har vi läst klart vår PolymorType.

Slutligen, om den blir fångad 0xdeadcrc för constructorThree, då blir det mer komplicerat. Vårt första fält bit_flags_of_what_really_present med typ # – i själva verket är detta bara ett alias för typen natbetyder "naturligt tal". Det vill säga, i själva verket är osignerad int det enda fallet, förresten, när osignerade nummer finns i verkliga kretsar. Så, nästa är en konstruktion med ett frågetecken, vilket betyder att detta är fältet - det kommer att finnas på tråden endast om motsvarande bit är inställd i det refererade fältet (ungefär som en ternär operator). Så anta att den här biten var på, då måste du läsa ett fält som Type, som i vårt exempel har 2 konstruktörer. Den ena är tom (består endast av en identifierare), den andra har ett fält ids med typ ids:Vector<long>.

Du kanske tycker att både mallar och generika är bra eller Java. Men nej. Nästan. Detta bara fallet med vinkelfästen i riktiga kretsar, och det används ENDAST för Vector. I en byteström blir detta 4 CRC32 byte för själva vektortypen, alltid densamma, sedan 4 byte - antalet arrayelement och sedan själva elementen.

Lägg till detta faktum att serialisering alltid sker i ord på 4 byte, alla typer är multiplar av det - inbyggda typer beskrivs också bytes и string med manuell serialisering av längden och denna justering med 4 - ja, det verkar låta normalt och till och med relativt effektivt? Även om TL påstås vara effektiv binär serialisering, men åt helvete med dem, med expansionen av vad som helst, även booleska värden och enkaraktärssträngar upp till 4 byte, kommer JSON fortfarande att vara mycket tjockare? Titta, även onödiga fält kan hoppas över av bitflaggor, allt är bara bra, och till och med utökbart för framtiden, la du till nya valfria fält till konstruktorn senare?

Men nej, om du inte läser min korta beskrivning, utan hela dokumentationen, och tänker på genomförandet. För det första beräknas konstruktorns CRC32 av den normaliserade schematextbeskrivningssträngen (ta bort extra blanksteg, etc.) - så om ett nytt fält läggs till kommer typbeskrivningssträngen att ändras, och därmed dess CRC32 och följaktligen serialisering. Och vad skulle den gamla klienten göra om han fick ett fält med nya flaggor, men han visste inte vad han skulle göra med dem härnäst? ..

För det andra, låt oss komma ihåg CRC32, som här används i huvudsak som hashfunktioner för att unikt bestämma vilken typ som (av)serialiseras. Här står vi inför problemet med kollisioner – och nej, sannolikheten är inte en på 232, utan mycket mer. Vem kom ihåg att CRC32 är designad för att upptäcka (och korrigera) fel i kommunikationskanalen, och följaktligen förbättra dessa egenskaper till nackdel för andra? Till exempel bryr hon sig inte om permutationen av bytes: om du räknar CRC32 från två rader, i den andra kommer du att byta ut de första 4 byten med de nästa 4 byten - det blir detsamma. När vi har textsträngar från det latinska alfabetet (och lite skiljetecken) som input, och dessa namn inte är särskilt slumpmässiga, ökar sannolikheten för en sådan permutation kraftigt.

Förresten, vem kollade vad som fanns där verkligen CRC32? I en av de tidiga källorna (även före Waltman) fanns det en hashfunktion som multiplicerade varje tecken med siffran 239, så älskad av dessa människor, ha ha!

Slutligen, okej, vi insåg att konstruktörer med en fälttyp Vector<int> и Vector<PolymorType> kommer att ha olika CRC32. Och hur är det med presentationen på linjen? Och i termer av teori, blir det en del av typen? Låt oss säga att vi passerar en array med tiotusen nummer, ja, med Vector<int> allt är klart, längden och ytterligare 40000 XNUMX byte. Och om detta Vector<Type2>, som endast består av ett fält int och det är den enda i typen - behöver vi upprepa 10000xabcdef0 34 4 gånger och sedan XNUMX byte int, eller så kan språket VISA detta för oss från konstruktorn fixedVec och istället för 80000 40000 byte, överföra igen bara XNUMX XNUMX?

Det här är inte en tom teoretisk fråga alls - tänk dig att du får en lista över gruppanvändare, som var och en har ett id, förnamn, efternamn - skillnaden i mängden data som överförs över en mobilanslutning kan vara betydande. Det är effektiviteten av Telegram-serialisering som annonseras för oss.

Så…

Vektor, som inte kunde härledas

Om du försöker vada igenom beskrivningssidorna för kombinatorer och om, kommer du att se att en vektor (och till och med en matris) formellt försöker härleda flera ark genom tupler. Men i slutändan blir de hamrade, det sista steget hoppas över, och definitionen av en vektor ges helt enkelt, som inte heller är bunden till en typ. Vad är det här? På språk programmering, särskilt funktionella, är det ganska typiskt att beskriva strukturen rekursivt - kompilatorn med sin lata utvärdering kommer att förstå allt och göra det. På språket data serialisering men EFFEKTIVITET behövs: det räcker med att helt enkelt beskriva lista, dvs. en struktur av två element - det första är ett dataelement, det andra är samma struktur i sig eller ett tomt utrymme för svansen (pack (cons) i Lisp). Men detta kommer uppenbarligen att kräva av varje elementet spenderar dessutom 4 byte (CRC32 i fallet med TL) för att beskriva dess typ. Det är lätt att beskriva en array fixad storlek, men i fallet med en array med en tidigare okänd längd bryter vi av.

Så eftersom TL inte tillåter dig att mata ut en vektor, måste den läggas till på sidan. I slutändan säger dokumentationen:

Serialisering använder alltid samma konstruktor "vektor" (const 0x1cb5c415 = crc32("vektor t:Typ # [ t ] = vektor t") som inte är beroende av det specifika värdet för variabeln av typen t.

Värdet på den valfria parametern t är inte involverat i serialiseringen eftersom det härleds från resultattypen (alltid känt före avserialiseringen).

Ta en närmare titt: vector {t:Type} # [ t ] = Vector t - men ingenstans definitionen i sig säger inte att det första talet måste vara lika med vektorns längd! Och det följer inte från någonstans. Detta är givet som du måste ha i åtanke och implementera med händerna. På andra håll nämner dokumentationen till och med ärligt att typen är falsk:

Vector t polymorfa pseudotyp är en "typ" vars värde är en sekvens av värden av vilken typ som helst t, antingen inrutad eller blottad.

… men fokuserar inte på det. När du, trött på att vada genom matematikens stretching (kanske till och med känd för dig från en universitetskurs), bestämmer dig för att göra mål och titta på hur du faktiskt arbetar med det i praktiken, finns intrycket kvar i ditt huvud: här är Serious Mathematics baserad på , uppenbarligen Cool People (två matematiker -vinnare av ACM), och inte vem som helst. Målet - att sprätta - har uppnåtts.

Förresten, om antalet. Återkallelse # det är en synonym nat, naturligt nummer:

Det finns typuttryck (typexpr) och numeriska uttryck (nat-expr). Men de definieras på samma sätt.

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

men i grammatiken beskrivs de på samma sätt, d.v.s. denna skillnad återigen måste komma ihåg och införas i genomförandet för hand.

Jo, ja, malltyper (vector<int>, vector<User>) har en gemensam identifierare (#1cb5c415), dvs. om du vet att samtalet deklareras som

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

då väntar du inte bara på en vektor, utan en vektor av användare. Mer exakt, måste vänta - i verklig kod kommer varje element, om inte en bar typ, att ha en konstruktor, och på ett bra sätt i implementeringen skulle det vara nödvändigt att kontrollera - och vi skickades exakt i varje element i denna vektor den typen? Och om det var någon form av PHP, där arrayen kan innehålla olika typer i olika element?

Vid det här laget börjar du undra - behövs en sådan TL? Kanske för vagnen skulle det vara möjligt att använda den mänskliga serializern, samma protobuf som redan fanns då? Det var teori, låt oss titta på praktiken.

Befintliga TL-implementationer i kod

TL föddes i tarmen av VKontakte redan före de välkända händelserna med försäljningen av Durovs andel och (säkert), även före utvecklingen av Telegram. Och i öppen källkod källor till den första implementeringen du kan hitta många roliga kryckor. Och själva språket implementerades där mer fullständigt än det är nu i Telegram. Till exempel används inte hash alls i schemat (vilket betyder den inbyggda pseudotypen (som en vektor) med avvikande beteende). Eller

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

men låt oss för fullständighetens skull betrakta bilden, för att så att säga spåra tankejättens utveckling.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Eller denna vackra:

    static const char *reserved_words_polymorhic[] = {

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

      };

Det här fragmentet handlar om mallar, som:

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

Detta är definitionen av hashmap-malltypen, som en vektor av int - Typ-par. I C++ skulle det se ut ungefär så här:

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

så, alpha - nyckelord! Men bara i C++ kan du skriva T, men du måste skriva alfa, beta... Men inte mer än 8 parametrar, fantasin slutade på theta. Så det verkar som att det en gång i St. Petersburg förekom ungefär sådana dialoger:

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

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

Men det handlade om den först utlagda implementeringen av TL "i allmänhet". Låt oss gå vidare till övervägandet av implementeringar i de faktiska Telegram-klienterna.

Basilikas ord:

Vasily, [09.10.18 17:07] Mest av allt är röven het av det faktum att de skruvade ihop ett gäng abstraktioner, och sedan slog de en bult på dem och överlagrade kodgegeratorn med kryckor
Som ett resultat, först från hamnen pilot.jpg
Sedan från jekichan.webp-koden

Naturligtvis, från personer som är bekanta med algoritmer och matematik kan vi förvänta oss att de har läst Aho, Ullman och är bekanta med de verktyg som har blivit de facto-standarden i branschen under decennierna för att skriva kompilatorer för sina DSL:er, eller hur? ..

Av författaren telegram-cli är Vitaliy Valtman, som kan förstås av förekomsten av TLO-formatet utanför dess (cli) gränser, en medlem av teamet - nu är biblioteket för att analysera TL tilldelat separatvad är intrycket av henne TL-parser? ..

16.12 04:18 Vasily: enligt min åsikt har någon inte behärskat lex + yacc
16.12 04:18 Vasily: annars kan jag inte förklara det
16.12 04:18 Vasily: ja, eller så fick de betalt för antalet rader i VK
16.12 04:19 Vasily: 3k+ rader av andra<censored> istället för en parser

Kanske ett undantag? Låt oss se hur gör detta är den OFFICIELLA klienten — Telegram Desktop:

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

1100+ rader i Python, ett par reguljära uttryck + specialfall av vektortypen, som naturligtvis deklareras i schemat som det ska vara enligt TL-syntaxen, men de sätter det på denna syntax, analyserar det mer ... Frågan är, varför bry sig om allt detta mirakelиmer puff, om ingen ska tolka det enligt dokumentationen ändå ?!

Förresten... Kommer du ihåg att vi pratade om CRC32-kontrollen? Så i Telegram Desktop-kodgeneratorn finns en lista över undantag för de typer där den beräknade CRC32 matchar inte som visas i diagrammet!

Vasily, [18.12 22:49] och här bör du fundera på om en sådan TL behövs
om jag ville bråka med alternativa implementeringar skulle jag börja infoga radbrytningar, hälften av tolkarna kommer att gå sönder på flerradsdefinitioner
tdesktop dock också

Kom ihåg poängen med one-liners, vi återkommer till det lite senare.

Okej, telegram-cli är inofficiellt, Telegram Desktop är officiellt, men hur är det med de andra? Och vem vet?.. I Android-klientkoden fanns det ingen schemaparser alls (vilket väcker frågor om öppen källkod, men det här är för den andra delen), men det fanns flera andra roliga bitar av kod, men om dem i underavsnittet nedan.

Vilka andra frågor väcker serialisering i praktiken? Till exempel skruvade de förstås ihop med bitfält och villkorade fält:

vasily: flags.0? true
betyder att fältet är närvarande och sant om flaggan är inställd

vasily: flags.1? int
betyder att fältet är närvarande och måste deserialiseras

Vasily: Ass, bränn dig inte, vad gör du!
Vasily: Någonstans i dokumentet finns det ett omnämnande att sant är en ren typ av noll längd, men det är orealistiskt att samla in något från sina dokument
Vasily: Det finns inget sådant i öppna implementeringar heller, men det finns många kryckor och rekvisita

Vad sägs om en Telethon? Om man ser framåt på ämnet MTProto, ett exempel - det finns sådana bitar i dokumentationen, men tecknet % den beskrivs bara som "motsvarande den givna kala typen", d.v.s. i exemplen nedan, antingen ett fel eller något odokumenterat:

Vasily, [22.06.18/18/38 XNUMX:XNUMX] På ett ställe:

msg_container#73f1f8dc messages:vector message = MessageContainer;

I en annan:

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

Och det är två stora skillnader, i verkligheten kommer någon form av naken vektor

Jag har inte sett nakna vektordefinitioner och har inte stött på det

Analys skriven i teleton för hand

Hans schema kommenterade definitionen msg_container

Återigen är frågan fortfarande om %. Det beskrivs inte.

Vadim Goncharov, [22.06.18/19/22 XNUMX:XNUMX] och i tdesktop?

Vasily, [22.06.18/19/23 XNUMX:XNUMX] Men deras TL-parser på regulatorerna kommer förmodligen inte att äta det heller

// parsed manually

TL är en vacker abstraktion, ingen implementerar den helt

Och det finns ingen % i deras version av schemat

Men här motsäger dokumentationen sig själv, så xs

Det fanns i grammatiken, de kunde bara glömma att beskriva semantiken

Jo, du såg dockan på TL, du kan inte räkna ut det utan en halv liter

"Tja, låt oss säga," kommer en annan läsare att säga, "du kritiserar allt, så visa det som det ska."

Vasily svarar: "När det gäller tolken behöver jag saker som

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

på något sätt mer som än

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

eller

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

detta är HELA lexern:

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

de där. enklare är att uttrycka det milt."

I allmänhet, i slutändan, passar parsern och kodgeneratorn för den faktiskt använda delmängden av TL in i cirka 100 rader grammatik och ~ 300 rader i generatorn (inklusive alla prints genererade kod), inklusive typgodis, typinformation för introspektion i varje klass. Varje polymorf typ förvandlas till en tom abstrakt basklass, och konstruktörer ärver från den och har metoder för serialisering och deserialisering.

Brist på typer i typspråk

Starkt skrivande är bra, eller hur? Nej, detta är inte en holivar (även om jag föredrar dynamiska språk), utan ett postulat inom TL. Utifrån det borde språket ge oss alla möjliga kontroller. Tja, okej, låt inte honom, utan genomförandet, men han borde åtminstone beskriva dem. Och vilka möjligheter vill vi ha?

Först av allt, begränsningar. Här ser vi i dokumentationen för uppladdning av filer:

Filens binära innehåll delas sedan upp i delar. Alla delar måste ha samma storlek ( del_storlek ) och följande villkor måste vara uppfyllda:

  • part_size % 1024 = 0 (delbart med 1KB)
  • 524288 % part_size = 0 (512KB måste vara jämnt delbart med part_size)

Den sista delen behöver inte uppfylla dessa villkor, förutsatt att dess storlek är mindre än part_size.

Varje del ska ha ett sekvensnummer, fil_del, med ett värde från 0 till 2,999.

Efter att filen har partitionerats måste du välja en metod för att spara den på servern. använda sig av upload.saveBigFilePart om filens fulla storlek är mer än 10 MB och upload.saveFilePart för mindre filer.
[…] ett av följande datainmatningsfel kan returneras:

  • FILE_PARTS_INVALID - Ogiltigt antal delar. Värdet är inte mellan 1..3000

Finns något av dessa i schemat? Kan det på något sätt uttryckas med hjälp av TL? Nej. Men ursäkta mig, även den gammaldags Turbo Pascal kunde beskriva de typer som ges av intervall. Och han kunde göra en sak till, nu mer känd som enum - en typ som består av en uppräkning av ett fast (litet) antal värden. På språk som C - numeriskt, märk väl, vi har bara pratat om typer hittills. tal. Men det finns också arrayer, strängar ... till exempel skulle det vara trevligt att beskriva att denna sträng bara kan innehålla ett telefonnummer, eller hur?

Inget av detta finns i TL. Men det finns till exempel i JSON Schema. Och om någon annan kan invända mot delbarheten av 512 KB att detta fortfarande måste kontrolleras i koden, se till att klienten helt enkelt kunde inte skicka nummer utanför intervallet 1..3000 (och motsvarande fel kunde inte ha uppstått) det skulle vara möjligt, eller hur? ..

Förresten, om fel och returvärden. Ögat är suddigt även för de som jobbat med TL – det gick det inte direkt upp för oss varenda en en funktion i TL kan faktiskt returnera inte bara den beskrivna returtypen, utan också ett fel. Men detta kan inte härledas med hjälp av själva TL. Naturligtvis är det förståeligt ändå och nafig är inte nödvändigt i praktiken (även om RPC faktiskt kan göras på olika sätt, vi återkommer till detta) - men hur är det med renheten i begreppen Mathematics of Abstract Types from the heavenly världen? .. Tog tag i draget - så matcha.

Och slutligen, hur är det med läsbarheten? Tja, där, i allmänhet, skulle jag vilja beskrivning ha det rätt i schemat (igen, det är i JSON-schemat), men om det redan är ansträngt med det, hur är det då med den praktiska sidan - det är åtminstone banalt att titta på skillnader under uppdateringar? Se själv på verkliga exempel:

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

eller

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

Någon gillar det, men GitHub, till exempel, vägrar att lyfta fram förändringar i så långa rader. Spelet "hitta 10 skillnader", och vad hjärnan omedelbart ser är att början och slut är desamma i båda exemplen, du måste tröttsamt läsa någonstans i mitten ... Enligt min mening är detta inte bara i teorin, men rent visuellt ser ut smutsigt och ovårdat.

Förresten, om teorins renhet. Varför behövs bitfält? Verkar de inte göra det lukt dåligt ur typteoretisk synvinkel? En förklaring kan ses i tidigare versioner av schemat. Till en början, ja, det var så, en ny typ skapades för varje nysning. Dessa rudiment finns fortfarande kvar i denna form, till exempel:

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;

Men tänk dig nu om du har 5 valfria fält i din struktur, då behöver du 32 typer för alla möjliga alternativ. kombinatorisk explosion. Så kristallrenheten i TL-teorin kraschade än en gång mot gjutjärnsröven i den hårda verkligheten med serialisering.

Dessutom, på sina ställen bryter dessa killar själva mot sitt eget skrivande. Till exempel, i MTProto (nästa kapitel) kan svaret komprimeras av Gzip, allt är vettigt - förutom brott mot lager och schema. En gång, och skördade inte själva RpcResult, utan dess innehåll. Tja, varför göra det här? .. Jag var tvungen att skära in en krycka för att kompressionen skulle fungera var som helst.

Eller ett annat exempel, vi hittade en gång ett fel - skickat InputPeerUser istället för InputUser. Eller tvärtom. Men det funkade! Det vill säga, servern brydde sig inte om typen. Hur kan det vara såhär? Svaret kommer kanske att fås av kodfragment från 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);

Med andra ord, här görs serialiseringen MANUELLT, inte genererad kod! Kanske är servern implementerad på liknande sätt?.. I princip fungerar detta om det görs en gång, men hur kan man stödja det senare med uppdateringar? Var det inte det upplägget var till för? Och så går vi vidare till nästa fråga.

Versionering. Skikten

Varför schemaversioner kallas lager kan bara gissas utifrån historiken för publicerade scheman. Tydligen verkade det först för författarna som om grundläggande saker kan göras på ett oförändrat schema, och endast när det är nödvändigt, indikerar specifika förfrågningar att de görs enligt en annan version. I princip till och med en bra idé - och den nya kommer så att säga "blandas in", lägga på den gamla. Men låt oss se hur det gjordes. Det var sant att det inte var möjligt att titta från första början - det är roligt, men basskiktsschemat existerar helt enkelt inte. Lager började vid 2. Dokumentationen berättar om en speciell TL-funktion:

Om en klient stöder Layer 2, måste följande konstruktor användas:

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

I praktiken betyder detta att före varje API-anrop, en int med värdet 0x289dd1f6 måste läggas till före metodnumret.

Låter OK. Men vad hände sedan? Sedan kom

invokeWithLayer3#b7475268 query:!X = X;

Så vad är nästa? Som det är lätt att gissa

invokeWithLayer4#dea0d430 query:!X = X;

Rolig? Nej, det är för tidigt att skratta, fundera på vad каждый en begäran från ett annat lager måste lindas in i en sådan speciell typ - om du har alla olika, hur ska man annars skilja dem åt? Och att lägga till bara 4 byte framför är en ganska effektiv metod. Så

invokeWithLayer5#417a57ae query:!X = X;

Men det är uppenbart att det efter ett tag kommer att bli lite bacchanalia. Och lösningen kom:

Uppdatering: Börjar med Layer 9, hjälpmetoder invokeWithLayerN kan användas tillsammans med initConnection

Hurra! Efter 9 versioner kom vi äntligen fram till vad som gjordes i Internetprotokollen redan på 80-talet - versionsförhandling en gång i början av anslutningen!

Så vad är nästa?..

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

Och nu kan du skratta. Först efter ytterligare 9 lager lades äntligen till en universell konstruktor med ett versionsnummer, som bara behöver anropas en gång i början av anslutningen, och meningen i lagren verkar ha försvunnit, nu är det bara en villkorad version, som överallt annars. Problemet löst.

Höger?..

Vasily, [16.07.18/14/01 XNUMX:XNUMX PM] I fredags tänkte jag:
Teleservern skickar händelser utan förfrågan. Förfrågningar måste packas in i InvokeWithLayer. Servern slår inte in uppdateringar, det finns ingen struktur för att radbryta svar och uppdateringar.

De där. klienten kan inte ange i vilket lager han vill ha uppdateringar

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX PM] Är inte InvokeWithLayer en krycka i princip?

Vasily, [16.07.18/14/02 XNUMX:XNUMX PM] Detta är det enda sättet

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX PM] vilket i huvudsak borde betyda skiktning i början av sessionen

Av detta följer förresten att en klientnedgradering inte tillhandahålls

Uppdateringar, d.v.s. typ Updates i schemat är detta vad servern skickar till klienten inte som svar på en API-förfrågan, utan på egen hand när en händelse inträffar. Detta är ett komplext ämne som kommer att diskuteras i ett annat inlägg, men för nu är det viktigt att veta att servern ackumulerar uppdateringar även när klienten är offline.

Alltså när man vägrar att slå in av varje paketet för att indikera dess version, därför uppstår följande möjliga problem logiskt:

  • servern skickar uppdateringar till klienten innan klienten har berättat vilken version den stöder
  • vad ska göras efter att ha uppgraderat klienten?
  • som garantieratt serverns åsikt om lagernumret inte kommer att förändras i processen?

Tror du att detta är rent teoretiskt tänkande, och i praktiken kan det inte hända, eftersom servern är rätt skriven (den är i alla fall testad bra)? ha! Spelar ingen roll hur!

Det är precis vad vi stötte på i augusti. Den 14 augusti blinkade meddelanden om att något uppdaterades på Telegram-servrarna ... och sedan i loggarna:

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.

och sedan några megabyte stackspår (nåja, samtidigt fixades loggningen). När allt kommer omkring, om något inte kändes igen i din TL - är det binärt genom signaturer, längre fram i strömmen ALLT går, kommer avkodning att bli omöjlig. Vad ska man göra i en sådan situation?

Nåväl, det första man tänker på är att koppla bort och försöka igen. Hjälpte inte. Vi googlade CRC32 - det visade sig vara objekt från schema 73, även om vi arbetade på schema 82. Vi tittar noga på loggarna - det finns identifierare från två olika scheman!

Kanske ligger problemet rent i vår inofficiella klient? Nej, vi kör Telegram Desktop 1.2.17 (versionen som levereras med ett antal Linux-distributioner), den skriver till undantagsloggen: MTP Unexpected type id #b5223b0f read in MTPMessageMedia...

Kritik av Telegrams protokoll och organisatoriska tillvägagångssätt. Del 1, teknisk: erfarenhet av att skriva en klient från grunden - TL, MT

Google visade att ett liknande problem redan hade hänt med en av de inofficiella klienterna, men sedan var versionsnumren och följaktligen antagandena annorlunda ...

Så vad ska man göra? Vasily och jag delade upp: han försökte uppdatera schemat till 91, jag bestämde mig för att vänta några dagar och försöka till 73. Båda metoderna fungerade, men eftersom de är empiriska finns det ingen förståelse för hur många versioner du behöver för att hoppa upp eller ner, inte heller hur länge du måste vänta .

Senare lyckades jag återskapa situationen: vi startar klienten, stänger av den, kompilerar om schemat till ett annat lager, startar om, fångar problemet igen, återgår till det föregående - oj, ingen byte av schemat och startar om klienten i flera minuter kommer att hjälpa. Du kommer att få en blandning av datastrukturer från olika lager.

Förklaring? Som du kan gissa av de olika indirekta symtomen består servern av många olika typer av processer på olika maskiner. Troligtvis satte den av servrarna som är ansvarig för att "buffra" i kön vad de högre gav den, och de gav det i det schema som var vid tidpunkten för genereringen. Och tills den här kön var "rutten" kunde ingenting göras åt det.

Om inte... men det här är en fruktansvärd krycka?!... Nej, innan vi tänker på galna idéer, låt oss titta på koden för officiella kunder. I Android-versionen hittar vi ingen TL-parser, men vi hittar en rejäl fil (github vägrar färga den) med (av)serialisering. Här är kodavsnitten:

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;

eller

    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... det ser galet ut. Men förmodligen är det här en genererad kod, då okej? .. Men den stöder verkligen alla versioner! Det är sant, det är inte klart varför allt blandas i en hög, och hemliga chattar och alla möjliga _old7 på något sätt inte liknar maskingenerering ... Men mest av allt blev jag galen av

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Killar, kan ni inte ens bestämma er inom ett lager?! Tja, okej, "två", låt oss säga, släpptes med ett fel, ja, det händer, men TRE? .. Omedelbart igen på samma rake? Vad är detta för pornografi, förlåt? ..

Förresten, en liknande sak händer i Telegram Desktop-källorna - om så är fallet, och flera commits i rad till schemat ändrar inte dess lagernummer, utan fixar något. Under förhållanden när det inte finns någon officiell datakälla för systemet, var kan jag få det ifrån, förutom de officiella klientkällorna? Och du tar det därifrån, du kan inte vara säker på att schemat är helt korrekt förrän du testar alla metoder.

Hur kan detta ens testas? Jag hoppas att fans av enhet, funktionella och andra tester kommer att dela i kommentarerna.

Okej, låt oss titta på en annan kodbit:

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;

Den "manuellt skapade" kommentaren här antyder att bara en del av den här filen är skriven för hand (kan du föreställa dig underhållsmardrömmen?), och resten är maskingenererat. Men då uppstår en annan fråga - att källorna finns tillgängliga inte fullständigt (a la blobs under GPL i Linux-kärnan), men detta är redan ett ämne för den andra delen.

Men nog. Låt oss gå vidare till protokollet ovanpå som all denna serialisering jagar.

MT Proto

Så låt oss öppna allmän beskrivning и detaljerad beskrivning av protokollet och det första vi snubblar över är terminologi. Och med ett överflöd av allt. I allmänhet verkar detta vara ett varumärke som tillhör Telegram - att kalla saker på olika platser på olika sätt, eller olika saker i ett ord, eller vice versa (till exempel i ett högnivå-API om du ser ett klistermärkepaket - detta är inte vad du trodde).

Till exempel "meddelande" (meddelande) och "session" (session) - här betyder de något annat än i det vanliga gränssnittet för Telegram-klienten. Tja, allt är klart med meddelandet, det kan tolkas i termer av OOP, eller helt enkelt kallas ordet "paket" - det här är en låg transportnivå, det finns inte samma meddelanden som i gränssnittet, det finns många av service. Men sessionen ... men först till kvarn.

Transportlager

Det första är transporter. Vi kommer att få veta om 5 alternativ:

  • TCP
  • websocket
  • Websocket över HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18/15/04 XNUMX:XNUMX PM] Och det finns också UDP-transport, men det är inte dokumenterat

Och TCP i tre varianter

Den första liknar UDP över TCP, varje paket innehåller ett sekvensnummer och en crc
Varför är det så smärtsamt att läsa dockor på en vagn?

Nåväl där nu TCP redan i 4 varianter:

  • förkortad
  • Mellanliggande
  • vadderad mellanliggande
  • full

Okej, vadderad mellanliggande för MTProxy, detta lades senare till på grund av kända händelser. Men varför två versioner till (tre totalt), när man kunde göra det? Alla fyra skiljer sig i huvudsak endast i hur man ställer in längden och nyttolasten för den faktiska huvud-MTProto, som kommer att diskuteras vidare:

  • i Abridged är det 1 eller 4 byte men inte 0xef sedan body
  • i Intermediate är detta 4 bytes längd och ett fält, och första gången klienten måste skicka 0xeeeeeeee för att indikera att det är mellanliggande
  • i sin helhet, det mest beroendeframkallande, ur en nätverkares synvinkel: längd, sekvensnummer och INTE DEN som i grunden är MTProto, body, CRC32. Ja, allt detta över TCP. Vilket ger oss tillförlitlig transport i form av en seriell ström av bytes, inga sekvenser behövs, speciellt kontrollsummor. Okej, nu kommer jag att invändas att TCP har en 16-bitars kontrollsumma, så datakorruption inträffar. Bra, förutom att vi faktiskt har ett kryptografiskt protokoll med hash längre än 16 byte, alla dessa fel – och ännu fler – kommer att fångas på en SHA-felmatchning på en högre nivå. Det finns INGEN mening med CRC32 över detta.

Låt oss jämföra Abridged, där en byte med längd är möjlig, med Intermediate, som motiverar "I fall 4-byte data alignment behövs", vilket är ganska nonsens. Vad tror man att Telegram-programmerare är så klumpiga att de inte kan läsa data från sockeln till en justerad buffert? Du måste fortfarande göra detta, eftersom läsning kan returnera dig hur många byte som helst (och det finns även proxyservrar, till exempel ...). Eller, å andra sidan, varför bry sig om Abridged om vi fortfarande har rejäla stoppningar från 16 byte på toppen - spara 3 byte ibland ?

Man får intrycket att Nikolai Durov är mycket förtjust i att uppfinna cyklar, inklusive nätverksprotokoll, utan egentliga praktiska behov.

Övriga transportmöjligheter, inkl. Webb och MTProxy kommer vi inte att överväga nu, kanske i ett annat inlägg, om det finns en förfrågan. Vi kommer först nu att minnas om just denna MTProxy som snart efter lanseringen 2018 lärde sig leverantörer snabbt att blockera exakt den, avsedd för blockera förbikopplingGenom paketstorlek! Och även det faktum att MTProxy-servern skriven (igen av Waltman) i C var onödigt bunden till Linux-specifikationer, även om det inte var nödvändigt alls (Phil Kulin kommer att bekräfta), och att en liknande server antingen på Go eller på Node.js passar mindre än hundra linjer.

Men vi kommer att dra slutsatser om dessa personers tekniska läskunnighet i slutet av avsnittet, efter att ha övervägt andra frågor. För nu, låt oss gå vidare till det 5:e OSI-lagret, session - där de placerade MTProto-sessionen.

Nycklar, meddelanden, sessioner, Diffie-Hellman

De satte det där inte helt korrekt ... En session är inte samma session som syns i gränssnittet under Aktiva sessioner. Men i ordning.

Kritik av Telegrams protokoll och organisatoriska tillvägagångssätt. Del 1, teknisk: erfarenhet av att skriva en klient från grunden - TL, MT

Här har vi fått en sträng av bytes med känd längd från transportlagret. Detta är antingen ett krypterat meddelande eller klartext - om vi fortfarande befinner oss i nyckelförhandlingsstadiet och faktiskt gör det. Vilket av de begrepp som kallas "nyckel" pratar vi om? Låt oss klargöra denna fråga för Telegram-teamet självt (jag ber om ursäkt för att jag översatte min egen dokumentation från engelska till antingen en trött hjärna klockan 4 på morgonen, det var lättare att lämna några fraser som de är):

Det finns två enheter som kallas session - en i användargränssnittet för officiella klienter under "aktuella sessioner", där varje session motsvarar en hel enhet/OS.
Den andra är MTProto-session, som har ett meddelandesekvensnummer (i låg nivå) i sig, och som kan pågå mellan olika TCP-anslutningar. Flera MTProto-sessioner kan ställas in samtidigt, till exempel för att påskynda nedladdningar av filer.

Mellan dessa två sessioner är konceptet tillstånd. I det degenererade fallet kan man säga så UI-session är det samma som tillståndMen tyvärr är det komplicerat. Vi kollar:

  • Användaren på den nya enheten genererar först auth_key och binder det till konto, till exempel via SMS - det är därför tillstånd
  • Det hände inuti den första MTProto-session, som har session_id inuti dig själv.
  • I detta steg, kombinationen tillstånd и session_id kunde kallas exempel - detta ord finns i dokumentationen och koden för vissa klienter
  • Sedan kan klienten öppna flera MTProto-sessioner under densamma auth_key - till samma DC.
  • Sedan en dag behöver klienten begära en fil från en annan DC - och för denna DC kommer en ny att genereras auth_key !
  • Att tala om för systemet att detta inte är en ny användare som registrerar sig, utan samma sak tillstånd (UI-session), använder klienten API-anrop auth.exportAuthorization hemma DC auth.importAuthorization i nya DC.
  • Samtidigt kan det finnas flera öppna MTProto-sessioner (var och en med sin egen session_id) till denna nya DC, under hans auth_key.
  • Slutligen kan klienten vilja ha Perfect Forward Secrecy. Varje auth_key det var permanenta nyckel - per DC - och klienten kan ringa auth.bindTempAuthKey för användning temporär auth_key - och återigen bara en temp_auth_key per DC, gemensamt för alla MTProto-sessioner till denna DC.

Observera att salt (och framtida salter) också en på auth_key de där. delas mellan alla MTProto-sessioner till samma DC.

Vad betyder "mellan olika TCP-anslutningar"? Det betyder att detta något liknande auktoriseringscookie på en webbplats - den kvarstår (överlever) många TCP-anslutningar till denna server, men en dag kommer den att gå dåligt. Bara till skillnad från HTTP, i MTProto, i sessionen, är meddelanden sekventiellt numrerade och bekräftade, de kom in i tunneln, anslutningen bröts - efter att ha upprättat en ny anslutning kommer servern vänligen att skicka allt i denna session som den inte levererade i tidigare TCP-anslutning.

Informationen ovan är dock en kläm efter många månaders rättstvister. Under tiden, implementerar vi vår kund från grunden? - låt oss gå tillbaka till början.

Så vi genererar auth_keyversioner av Diffie-Hellman från Telegram. Låt oss försöka förstå dokumentationen...

Vasily, [19.06.18/20/05 1:255] data_with_hash := SHAXNUMX(data) + data + (alla slumpmässiga byte); så att längden är lika med XNUMX byte;
krypterad_data := RSA(data_med_hash, server_public_key); ett 255-byte långt tal (big endian) höjs till den erforderliga makten över den erforderliga modulen, och resultatet lagras som ett 256-bytetal.

De fick lite dope DH

Ser inte ut som en frisk persons DH
Det finns inga två publika nycklar i dx

Tja, till slut kom vi på det, men sedimentet fanns kvar - ett bevis på arbete utförs av klienten att han kunde faktorisera antalet. Typ av skydd mot DoS-attacker. Och RSA-nyckeln används bara en gång i en riktning, huvudsakligen för kryptering new_nonce. Men även om denna till synes enkla operation lyckas, vad kommer du att behöva möta?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Jag har inte nått den appid-förfrågan än

Jag skickade en förfrågan till DH

Och i dockan på transporten står det att den kan svara med 4 byte av felkoden. Och det är allt

Tja, han sa till mig -404, så vad?

Här är jag till honom: "fånga din efigna krypterad med servernyckeln med ett fingeravtryck av sådant och sådant, jag vill ha DH", och den svarar dumt 404

Vad skulle du tycka om ett sådant serversvar? Vad ska man göra? Det finns ingen att fråga (men mer om det i andra delen).

Här är allt intresse för kajen att göra

Jag har inget annat att göra, jag drömde bara om att konvertera siffror fram och tillbaka

Två 32-bitars nummer. Jag packade dem som alla andra

Men nej, det är dessa två som du behöver först i rad som BE

Vadim Goncharov, [20.06.18/15/49 404:XNUMX] och på grund av detta XNUMX?

Vasily, [20.06.18/15/49 XNUMX:XNUMX PM] JA!

Vadim Goncharov, [20.06.18/15/50 XNUMX:XNUMX PM] så jag förstår inte vad han "inte hittade"

Vasily, [20.06.18 15:50] om

Jag hittade inte en sådan uppdelning i enkla divisorer%)

Inte ens felrapportering bemästrades

Vasily, [20.06.18/20/18 5:XNUMX PM] Åh, det finns också MDXNUMX. Redan tre olika hash

Nyckelfingeravtrycket beräknas enligt följande:

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

SHA1 och sha2

Så låt oss sätta auth_key 2048 bitar i storlek fick vi enligt Diffie-Hellman. Vad kommer härnäst? Sedan får vi reda på att de lägre 1024 bitarna av denna nyckel inte används på något sätt ... men låt oss fundera på detta för nu. I det här steget har vi en delad hemlighet med servern. En analog till en TLS-session har etablerats, en mycket kostsam procedur. Men servern vet inget om vilka vi är än! Inte än, faktiskt tillstånd. De där. om du tänkte i termer av "inloggningslösenord", som det brukade vara i ICQ, eller åtminstone "inloggningsnyckel", som i SSH (till exempel på någon gitlab / github). Vi blev anonyma. Och om servern svarar oss "dessa telefonnummer betjänas av en annan DC"? Eller till och med "ditt telefonnummer är förbjudet"? Det bästa vi kan göra är att spara nyckeln i hopp om att den fortfarande ska vara användbar och inte ruttna då.

Förresten, vi "fick" det med reservationer. Till exempel, litar vi på servern? Är han falsk? Vi behöver kryptografiska kontroller:

Vasily, [21.06.18/17/53 2:XNUMX PM] De erbjuder mobila klienter att kontrollera ett XNUMXkbit-nummer för enkelhetens skull%)

Men det är inte alls klart, nafeijoa

Vasily, [21.06.18/18/02 XNUMX:XNUMX] Kajen säger inte vad man ska göra om det visade sig inte vara enkelt

Inte sagt. Låt oss se vad den officiella klienten för Android gör i det här fallet? A det är vad (och ja, hela filen är intressant där) - som de säger, jag lämnar den bara här:

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

Nej, självklart där några det finns kontroller för enkelheten i ett tal, men personligen har jag inte längre tillräckliga kunskaper i matematik.

Okej, vi har huvudnyckeln. För att logga in, d.v.s. skicka förfrågningar, är det nödvändigt att utföra ytterligare kryptering, redan med AES.

Meddelandenyckeln definieras som de 128 mittersta bitarna av SHA256 i meddelandekroppen (inklusive session, meddelande-ID, etc.), inklusive utfyllnadsbytes, förekommande av 32 byte hämtade från auktoriseringsnyckeln.

Vasily, [22.06.18/14/08 XNUMX:XNUMX PM] Genomsnittliga tikar

Mottagen auth_key. Allt. Vidare dem ... det framgår inte från hamnen. Studera gärna den öppna källkoden.

Observera att MTProto 2.0 kräver 12 till 1024 byte utfyllnad, fortfarande under förutsättning att den resulterande meddelandelängden är delbar med 16 byte.

Så hur mycket stoppning ska man lägga i?

Och ja, även här, 404 vid fel

Om någon noggrant studerade diagrammet och texten i dokumentationen märkte han att det inte finns någon MAC där. Och att AES används i något IGE-läge som inte används någon annanstans. De skriver så klart om det i sin FAQ... Här är liksom själva meddelandenyckeln samtidigt SHA-hash för de dekrypterade data som används för att kontrollera integriteten - och vid en missmatchning, dokumentationen för någon anledning rekommenderar att tyst ignorera dem (men hur är det med säkerheten, plötsligt bryta oss?).

Jag är ingen kryptograf, kanske i det här läget i det här fallet är det inget fel ur en teoretisk synvinkel. Men jag kan definitivt nämna ett praktiskt problem, med exemplet Telegram Desktop. Den krypterar den lokala cachen (alla dessa D877F783D5D3EF8C) på samma sätt som meddelanden i MTProto (endast i detta fall, version 1.0), d.v.s. först meddelandenyckeln, sedan själva datan (och någonstans åt sidan det stora auth_key 256 byte, utan vilken msg_key onyttig). Så problemet blir märkbart på stora filer. Du behöver nämligen behålla två kopior av datan - krypterade och dekrypterade. Och om det finns megabyte, eller strömmande video, till exempel? .. Klassiska scheman med MAC efter chiffertexten låter dig läsa den strömmande, omedelbart överföra den. Och med MTProto måste du först kryptera eller dekryptera hela meddelandet, först sedan överföra det till nätverket eller till disken. Därför, i de senaste versionerna av Telegram Desktop i cachen i user_data ett annat format används redan - med AES i CTR-läge.

Vasily, [21.06.18/01/27 20:XNUMX AM] Åh, jag fick reda på vad IGE är: IGE var det första försöket med ett "autentiseringskrypteringsläge", ursprungligen för Kerberos. Det var ett misslyckat försök (det ger inget integritetsskydd) och måste tas bort. Det var början på en XNUMX år lång strävan efter ett autentiserande krypteringsläge som fungerar, vilket nyligen kulminerade i lägen som OCB och GCM.

Och nu argumenten från vagnens sida:

Teamet bakom Telegram, ledd av Nikolai Durov, består av sex ACM-mästare, hälften av dem doktorer i matematik. Det tog dem ungefär två år att rulla ut den nuvarande versionen av MTProto.

Vad är roligt. Två år till lägre nivå

Eller så kan vi bara ta tls

Okej, låt oss säga att vi har gjort kryptering och andra nyanser. Kan vi äntligen skicka TL-serialiserade förfrågningar och deserialisera svar? Så vad ska skickas och hur? Här är metoden initConnectionkanske det här är det?

Vasily, [25.06.18/18/46 XNUMX:XNUMX PM] Initierar anslutning och sparar information på användarens enhet och applikation.

Den accepterar app_id, device_model, system_version, app_version och lang_code.

Och några frågor

Dokumentation som alltid. Studera gärna öppen källkod

Om allt var ungefär klart med invokeWithLayer, vad är det då? Det visar sig att om vi har - klienten redan hade något att fråga servern om - det finns en förfrågan som vi ville skicka:

Vasily, [25.06.18/19/13 XNUMX:XNUMX] Av koden att döma är det första samtalet insvept i detta skräp, och själva skräpet är i invokewithlayer

Varför kunde inte initConnection vara ett separat samtal, utan måste vara ett omslag? Ja, som det visade sig, måste det göras varje gång i början av varje session, och inte en gång, som med huvudnyckeln. Men! Den kan inte anropas av en obehörig användare! Här har vi nått det stadium där det är tillämpligt den här dokumentationssida - och den berättar att...

Endast en liten del av API-metoderna är tillgängliga för obehöriga användare:

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

Den allra första av dem auth.sendCode, och det finns den där uppskattade första förfrågan där vi skickar api_id och api_hash, och därefter får vi ett SMS med en kod. Och om vi kom till fel DC (telefonnummer i det här landet betjänas till exempel av ett annat), kommer vi att få ett felmeddelande med numret på den önskade DC. För att ta reda på vilken IP-adress vi behöver ansluta till med DC-numret så får vi hjälp av help.getConfig. En gång var det bara 5 anmälningar, men efter de välkända händelserna 2018 har antalet ökat rejält.

Låt oss nu komma ihåg att vi i detta skede hamnade på den anonyma servern. Är det inte för dyrt att bara skaffa en IP-adress? Varför inte göra detta och andra operationer i den okrypterade delen av MTProto? Jag hör en invändning: ”hur kan du se till att det inte är RKN som kommer att svara med falska adresser?”. Till detta minns vi att i själva verket i officiella kunder inbäddade RSA-nycklar, dvs. du kan bara skylt denna informationen. Egentligen görs detta redan för information om förbikoppling av lås, som klienter får via andra kanaler (det är logiskt att detta inte kan göras i själva MTProto, eftersom du fortfarande behöver veta var du ska ansluta).

OK. I detta skede av klientauktorisering är vi ännu inte auktoriserade och har inte registrerat vår ansökan. Vi vill bara se tills vidare vad servern svarar på de metoder som är tillgängliga för en obehörig användare. Och här…

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

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

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

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

I schemat kommer den första, den andra

I tdesktop-schemat är det tredje värdet

Ja, sedan dess har såklart dokumentationen uppdaterats. Även om det snart kan bli irrelevant igen. Och hur ska en nybörjare veta det? Kanske kommer de att informera dig om du registrerar din ansökan? Vasily gjorde detta, men tyvärr skickades ingenting till honom (igen, vi kommer att prata om detta i den andra delen).

... Du märkte att vi redan på något sätt har flyttat till API, d.v.s. till nästa nivå och missat något i MTProto-temat? Inget förvånande:

Vasily, [28.06.18/02/04 2:XNUMX AM] Mm, de rotar igenom några av algoritmerna på eXNUMXe

Mtproto definierar krypteringsalgoritmer och nycklar för båda domänerna, samt lite av en omslagsstruktur

Men de blandar hela tiden olika stacknivåer, så det är inte alltid klart var mtproto slutade och nästa nivå började.

Hur blandas de? Tja, här är samma temporära nyckel för PFS, till exempel (förresten, Telegram Desktop vet inte hur man gör det). Den exekveras av en API-begäran auth.bindTempAuthKey, dvs. från översta nivån. Men samtidigt stör det kryptering på lägre nivå - efter det måste du till exempel göra det igen initConnection etc., så är det inte bara normal begäran. Separat levererar det också att du bara kan ha EN tillfällig nyckel på DC, även om fältet auth_key_id i varje meddelande kan du ändra nyckeln åtminstone varje meddelande, och att servern har rätt att "glömma" den tillfälliga nyckeln när som helst - vad du ska göra i det här fallet, dokumentationen säger inte ... ja, varför det skulle inte vara möjligt att ha flera nycklar, som med en uppsättning framtida salter, men ?..

Det finns några andra saker värda att notera i MTProto-temat.

Meddelandemeddelanden, msg_id, msg_seqno, bekräftelser, pingar i fel riktning och andra egenheter

Varför behöver du veta om dem? Eftersom de "läcker" en nivå högre, och du behöver känna till dem när du arbetar med API:et. Anta att vi inte är intresserade av msg_key, den lägre nivån dekrypterade allt åt oss. Men inuti den dekrypterade datan har vi följande fält (även längden på data för att veta var utfyllnaden är, men detta är inte viktigt):

  • salt-int64
  • session_id - int64
  • meddelande_id - int64
  • seq_no-int32

Kom ihåg att det bara finns ett salt för hela DC. Varför veta om det? Inte bara för att det finns en förfrågan get_future_salts, som talar om vilka intervall som kommer att vara giltiga, men också för att om ditt salt är "ruttet", så kommer meddelandet (förfrågan) helt enkelt att gå förlorat. Servern kommer naturligtvis att rapportera det nya saltet genom att utfärda new_session_created - men med den gamla måste du på något sätt skicka om till exempel. Och denna fråga påverkar applikationens arkitektur.

Servern tillåts släppa sessioner helt och hållet och svara på detta sätt av många anledningar. Vad är egentligen en MTProto-session från klientsidan? Det är två siffror session_id и seq_no meddelanden under denna session. Tja, och den underliggande TCP-anslutningen, förstås. Låt oss säga att vår klient fortfarande inte vet hur man gör en massa saker, kopplad bort, återansluten. Om detta hände snabbt - den gamla sessionen fortsatte i den nya TCP-anslutningen, öka seq_no ytterligare. Om det tar lång tid kan servern ta bort det, för på sin sida är det också en kö, som vi fick reda på.

Vad borde vara seq_no? Åh, det är en knepig fråga. Försök att ärligt förstå vad som menades:

Innehållsrelaterat meddelande

Ett meddelande som kräver en uttrycklig bekräftelse. Dessa inkluderar alla användarmeddelanden och många tjänstemeddelanden, praktiskt taget alla med undantag för behållare och bekräftelser.

Meddelandesekvensnummer (msg_seqno)

Ett 32-bitars nummer lika med två gånger antalet "innehållsrelaterade" meddelanden (de som kräver bekräftelse, och i synnerhet de som inte är behållare) som skapats av avsändaren före detta meddelande och därefter ökat med ett om det aktuella meddelandet är ett innehållsrelaterat meddelande. En behållare genereras alltid efter hela innehållet; därför är dess sekvensnummer större än eller lika med sekvensnumren för meddelandena som finns i den.

Vilken typ av cirkus är det här med en ökning på 1, och sedan ytterligare 2? .. Jag misstänker att den ursprungliga betydelsen var "låg bit för ACK, resten är ett nummer", men resultatet är inte helt rätt - i synnerhet, det visar sig att det går att skicka flera bekräftelser som har samma seq_no! Hur? Tja, till exempel, servern skickar oss något, skickar, och vi själva är tysta, vi svarar bara med servicebekräftelsemeddelanden om att ta emot hans meddelanden. I det här fallet kommer våra utgående bekräftelser att ha samma utgående nummer. Om du är bekant med TCP och tyckte att det här låter lite galet, men det verkar inte vara särskilt vilt, för i TCP seq_no ändras inte, och bekräftelsen går till seq_no andra sidan - då skyndar jag mig att uppröra. Bekräftelser kommer till MTProto INTEseq_no, som i TCP, men msg_id !

Vad är detta msg_id, det viktigaste av dessa fält? Meddelandets unika ID, som namnet antyder. Det definieras som ett 64-bitars nummer, vars minst signifikanta bitar återigen har server-inte-server-magi, och resten är en Unix-tidsstämpel, inklusive bråkdelen, förskjuten 32 bitar till vänster. De där. tidsstämpel i sig (och meddelanden med för olika tider kommer att avvisas av servern). Av detta visar det sig att detta i allmänhet är en identifierare som är global för klienten. Medan - kom ihåg session_id - vi är garanterade: Under inga omständigheter kan ett meddelande avsett för en session skickas till en annan session. Det vill säga, det visar sig att det redan finns tre nivå — session, sessionsnummer, meddelande-id. Varför en sådan överkomplikation, detta mysterium är mycket stort.

Så, msg_id behövs för…

RPC: förfrågningar, svar, fel. Bekräftelser.

Som du kanske har märkt finns det ingen speciell typ eller funktion "gör en RPC-förfrågan" någonstans i schemat, även om det finns svar. Vi har trots allt innehållsrelaterade meddelanden! Det är, några meddelande kan vara en begäran! Eller inte vara. Trots allt, av varje finns msg_id. Och här är svaren:

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

Det är här det indikeras vilket meddelande detta är ett svar. Därför måste du på toppnivån i API:t komma ihåg vilket nummer din förfrågan hade - jag tror att det inte är nödvändigt att förklara att arbetet är asynkront, och det kan finnas flera förfrågningar samtidigt, svaren på vilka kan returneras i valfri ordning? I princip, från detta, och felmeddelanden som inga arbetare, kan arkitekturen bakom detta spåras: servern som upprätthåller en TCP-anslutning med dig är en front-end-balanserare, den riktar förfrågningar till backends och samlar dem tillbaka längs message_id. Allt verkar vara klart, logiskt och bra här.

Ja?.. Och om du tänker efter? När allt kommer omkring har RPC-svaret i sig också ett fält msg_id! Behöver vi skrika åt servern "du svarar inte på mitt svar!"? Och ja, vad stod det om konfirmation? Om sidan meddelanden om meddelanden berättar vad som är

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

och varje sida måste göra det. Men inte alltid! Om du får ett RpcResult fungerar det som en bekräftelse i sig själv. Det vill säga, servern kan svara på din förfrågan med MsgsAck - som "Jag fick den." Kan genast svara på RpcResult. Det kan vara både och.

Och ja, du måste fortfarande svara på svaret! Bekräftelse. I annat fall kommer servern att anse det som olevererat och kasta ut det till dig igen. Även efter återuppkoppling. Men här kommer naturligtvis frågan om timeouts att dyka upp. Låt oss titta på dem lite senare.

Under tiden, låt oss överväga möjliga fel i sökexekveringen.

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

Åh, kommer någon att utbrista, här är ett mer mänskligt format – det finns en linje! Ta din tid. Här lista över felmen absolut inte komplett. Av den lär vi oss att koden är − något liknande HTTP-fel (ja, naturligtvis, svarens semantik respekteras inte, på vissa ställen distribueras de av koder slumpmässigt), och strängen ser ut som CAPITAL_LETTERS_AND_NUMBERS. Till exempel PHONE_NUMBER_OCCUPIED eller FILE_PART_X_MISSING. Tja, det vill säga, du måste fortfarande den här linjen analysera. Till exempel FLOOD_WAIT_3600 kommer att innebära att du måste vänta en timme, och PHONE_MIGRATE_5att telefonnumret med detta prefix ska registreras i 5:e DC. Vi har ett typspråk, eller hur? Vi behöver inte ett argument från strängen, reguljära uttryck duger, cho.

Återigen, detta finns inte på sidan för servicemeddelanden, men som redan är brukligt med detta projekt kan information hittas på en annan dokumentationssida. Eller väcka misstankar. Först, titta, brott mot skrivning/lager - RpcError kan investeras i RpcResult. Varför inte utanför? Vad har vi inte tagit hänsyn till?.. Följaktligen, var är garantin att RpcError får inte investeras i RpcResult, men vara direkt eller kapslad i en annan typ? den saknar req_msg_id ? ..

Men låt oss fortsätta om servicemeddelanden. Klienten kan tänka sig att servern tänker länge och göra en sådan underbar begäran:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Det finns tre möjliga svar på det, återigen korsar bekräftelsemekanismen, för att försöka förstå vad de borde vara (och vad är listan över typer som inte kräver bekräftelse i allmänhet), läsaren lämnas som hemläxa (obs: informationen i Telegram Desktop-källorna är inte fullständig).

Beroende: Meddelande Poststatus

I allmänhet lämnar många platser i TL, MTProto och Telegram i allmänhet en känsla av envishet, men av artighet, takt och andra mjuka färdigheter vi höll artigt tyst om det, och obsceniteterna i dialogerna censurerades. Men denna platsОstörre delen av sidan om meddelanden om meddelanden orsakar chock även för mig som har jobbat med nätverksprotokoll länge och sett cyklar med varierande krökningsgrad.

Det börjar ofarligt, med bekräftelser. Nästa, vi berättas om

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;

Tja, alla som börjar arbeta med MTProto kommer att behöva möta dem, i den "korrigerade - omkompilerade - lanserade" cykeln, att få nummerfel eller salt som har blivit ruttet under redigeringar är en vanlig sak. Det finns dock två punkter här:

  1. Det följer att det ursprungliga meddelandet har gått förlorat. Vi behöver inhägna lite köer, det ska vi överväga senare.
  2. Vad är det för konstiga felsiffror? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... var är resten av siffrorna, Tommy?

I dokumentationen står det:

Avsikten är att error_code-värden ska grupperas (error_code >> 4): till exempel, koderna 0x40 - 0x4f motsvarar fel i containernedbrytning.

men för det första en förskjutning åt andra hållet, och för det andra spelar det ingen roll var resten av koderna finns? I författarens huvud?.. Det är dock bagateller.

Beroendet börjar i poststatusmeddelanden och postkopior:

  • Begäran om information om meddelandestatus
    Om någon av parterna inte har fått information om statusen för sina utgående meddelanden på ett tag, kan den uttryckligen begära det från den andra parten:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informationsmeddelande om status för meddelanden
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Här, info är en sträng som innehåller exakt en byte av meddelandestatus för varje meddelande från listan över inkommande msg_ids:

    • 1 = inget är känt om meddelandet (msg_id för lågt, den andra parten kan ha glömt det)
    • 2 = meddelande inte mottaget (msg_id faller inom intervallet för lagrade identifierare, men den andra parten har verkligen inte tagit emot ett sådant meddelande)
    • 3 = meddelande inte mottaget (msg_id för högt, men den andra parten har verkligen inte fått det ännu)
    • 4 = meddelande mottaget (observera att detta svar samtidigt är en mottagningsbekräftelse)
    • +8 = meddelande redan bekräftat
    • +16 = meddelande som inte kräver bekräftelse
    • +32 = RPC-fråga i meddelandet som bearbetas eller bearbetningen är redan klar
    • +64 = innehållsrelaterat svar på meddelande som redan har genererats
    • +128 = den andra parten vet att meddelandet redan har tagits emot
      Detta svar kräver ingen bekräftelse. Det är en bekräftelse av relevant msgs_state_req, i och för sig.
      Observera att om det plötsligt visar sig att den andra parten inte har ett meddelande som ser ut att ha skickats till sig, kan meddelandet helt enkelt skickas på nytt. Även om den andra parten skulle få två kopior av meddelandet samtidigt, kommer dubbletten att ignoreras. (Om det har gått för lång tid och det ursprungliga msg_id inte längre är giltigt, ska meddelandet packas in i msg_copy).
  • Frivillig kommunikation av status för meddelanden
    Var och en av parterna kan frivilligt informera den andra parten om statusen för de meddelanden som sänts av den andra parten.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Utökad frivillig kommunikation av status för ett meddelande
    .
    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;
  • Explicit begäran om att skicka meddelanden på nytt
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Fjärrparten svarar omedelbart genom att skicka om de begärda meddelandena […]
  • Uttrycklig begäran om att återsända svar
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Fjärrparten svarar omedelbart genom att skicka om svar till de begärda meddelandena […]
  • Meddelandekopior
    I vissa situationer behöver ett gammalt meddelande med ett msg_id som inte längre är giltigt skickas om. Sedan är den inslagen i en kopiabehållare:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    När meddelandet väl mottagits behandlas det som om omslaget inte fanns där. Men om det är känt med säkerhet att meddelandet orig_message.msg_id togs emot, behandlas inte det nya meddelandet (samtidigt som det och orig_message.msg_id bekräftas). Värdet för orig_message.msg_id måste vara lägre än containerns msg_id.

Låt oss till och med tiga om det faktum att i msgs_state_info återigen, öronen på den ofärdiga TL sticker ut (vi behövde en vektor av byte, och i de två nedre bitarna av enum, och i de äldre bitarna flaggor). Poängen är något annat. Är det någon som förstår varför allt detta är i praktiken i verklig klient nödvändigt?.. Med svårighet, men du kan föreställa dig någon fördel om en person är engagerad i felsökning och i ett interaktivt läge - fråga servern vad och hur. Men förfrågningar beskrivs här rundresa.

Det följer av detta att varje sida inte bara måste kryptera och skicka meddelanden, utan också lagra data om dem, om svaren på dem och under en okänd tid. Dokumentationen beskriver inte tidpunkterna eller den praktiska användbarheten av dessa funktioner. inte på något sätt. Det som är mest förvånande är att de faktiskt används i koden för officiella kunder! Tydligen fick de veta något som inte fanns med i den öppna dokumentationen. Förstå från koden varför, är inte längre lika enkelt som i fallet med TL - detta är inte en (jämförelsevis) logiskt isolerad del, utan en bit knuten till applikationsarkitekturen, d.v.s. kommer att kräva mycket mer tid för att förstå applikationskoden.

Pings och timings. Köer.

Från allt, om du kommer ihåg gissningarna om serverarkitekturen (fördelning av förfrågningar över backends), följer en ganska tråkig sak - trots alla garantier för leverans som i TCP (antingen har data levererats, eller så kommer du att bli informerad om bryta, men data kommer att levereras tills problemet uppstår), att bekräftelser i själva MTProto - inga garantier. Servern kan lätt tappa eller kasta ut ditt meddelande, och ingenting kan göras åt det, bara för att inhägna kryckor av olika slag.

Och först av allt – meddelandeköer. Nåväl, för det första var allt självklart från första början - ett obekräftat meddelande måste lagras och skickas upp. Och efter vilken tid? Och narren känner honom. Kanske löser dessa missbrukartjänstmeddelanden på något sätt detta problem med kryckor, säg, i Telegram Desktop finns det ungefär 4 köer som motsvarar dem (kanske fler, som redan nämnts, för detta måste du fördjupa dig i dess kod och arkitektur mer seriöst; samtidigt tid, vi vet att det inte kan tas som ett prov, ett visst antal typer från MTProto-schemat används inte i det).

Varför händer det här? Förmodligen kunde inte serverprogrammerarna säkerställa tillförlitligheten inom klustret, eller åtminstone ens buffring på frontbalancern, och flyttade detta problem till klienten. Av desperation försökte Vasily implementera ett alternativt alternativ, med endast två köer, med hjälp av algoritmer från TCP - mätning av RTT till servern och justering av "fönsterstorleken" (i meddelanden) beroende på antalet obekräftade förfrågningar. Det vill säga en sådan grov heuristik för att uppskatta serverbelastning - hur många av våra förfrågningar den kan tugga samtidigt och inte förlora.

Tja, det vill säga, du förstår, eller hur? Om du måste implementera TCP igen ovanpå ett protokoll som fungerar över TCP, indikerar detta ett mycket dåligt utformat protokoll.

Åh ja, varför behövs mer än en kö, och i allmänhet, vad betyder detta för en person som arbetar med ett högnivå-API? Titta, du gör en förfrågan, du serialiserar den, men det är ofta omöjligt att skicka det direkt. Varför? För svaret kommer att bli msg_id, vilket är tillfälligtаJag är en etikett, vars utnämning är bättre att skjuta upp så sent som möjligt - plötsligt kommer servern att avvisa det på grund av en tidsobalans mellan oss och den (naturligtvis kan vi göra en krycka som flyttar vår tid från nuet till servertiden genom att lägga till ett delta beräknat från serversvaren - officiella klienter gör detta, men denna metod är grov och felaktig på grund av buffring). Så när du gör en förfrågan med ett lokalt funktionssamtal från biblioteket går meddelandet igenom följande steg:

  1. Ligger i samma kö och väntar på kryptering.
  2. Utsedd msg_id och meddelandet gick till en annan kö - möjlig vidarebefordran; skicka till uttaget.
  3. a) Servern svarade MsgsAck - meddelandet levererades, vi tar bort det från "andra kön".
    b) Eller vice versa, han gillade inte något, svarade han badmsg - vi skickar om från "andra kön"
    c) Inget är känt, det är nödvändigt att skicka om meddelandet från en annan kö - men det är inte känt exakt när.
  4. Servern svarade äntligen RpcResult - det faktiska svaret (eller felet) - inte bara levererat, utan också bearbetat.

Kanske, kan användningen av behållare delvis lösa problemet. Detta är när ett gäng meddelanden packas i ett och servern svarade med en bekräftelse till alla på en gång, med en msg_id. Men han kommer också att avvisa denna packning, om något gick fel, också det hela.

Och vid denna punkt spelar icke-tekniska överväganden in. Erfarenhetsmässigt har vi sett många kryckor, och dessutom kommer vi nu att se fler exempel på dåliga råd och arkitektur – är det i sådana förhållanden värt att lita på och fatta sådana beslut? Frågan är retorisk (naturligtvis inte).

Vad pratar vi om? Om du på ämnet "beroendemeddelanden om meddelanden" fortfarande kan spekulera med invändningar som "du är dum, du förstod inte vår briljanta idé!" (så skriv först dokumentationen, som vanliga människor ska, med motivering och exempel på paketutbyte, så pratar vi), sedan är timings/timeouts en rent praktisk och specifik fråga, här har allt varit känt sedan länge. Men vad säger dokumentationen om timeouts?

En server bekräftar vanligtvis mottagandet av ett meddelande från en klient (normalt en RPC-fråga) med hjälp av ett RPC-svar. Om ett svar väntar länge kan en server först skicka en mottagningsbekräftelse, och något senare, själva RPC-svaret.

En klient bekräftar normalt mottagandet av ett meddelande från en server (vanligtvis ett RPC-svar) genom att lägga till en bekräftelse till nästa RPC-fråga om den inte sänds för sent (om den genereras t.ex. 60-120 sekunder efter mottagandet av ett meddelande från servern). Men om det under en längre tid inte finns någon anledning att skicka meddelanden till servern eller om det finns ett stort antal obekräftade meddelanden från servern (säg över 16), sänder klienten en fristående bekräftelse.

... Jag översätter: vi själva vet inte hur mycket och hur det är nödvändigt, ja, låt oss uppskatta att låt det vara så här.

Och om pingar:

Pingmeddelanden (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Ett svar returneras vanligtvis till samma anslutning:

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

Dessa meddelanden kräver inga bekräftelser. En pong sänds endast som svar på ett ping medan en ping kan initieras av båda sidor.

Uppskjuten anslutningsstängning + PING

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

Fungerar som ping. Dessutom, efter att detta har tagits emot, startar servern en timer som kommer att stänga den aktuella anslutningen disconnect_delay sekunder senare om den inte tar emot ett nytt meddelande av samma typ som automatiskt återställer alla tidigare timers. Om klienten skickar dessa pingar en gång var 60:e sekund, till exempel, kan den ställa in disconnect_delay lika med 75 sekunder.

Är du inte klok?! Om 60 sekunder kommer tåget att gå in på stationen, släppa av och plocka upp passagerare och återigen förlora kommunikationen i tunneln. Om 120 sekunder, medan du petar runt, kommer han fram till en annan, och anslutningen kommer med största sannolikhet att bryta. Tja, det är tydligt var benen växer ifrån - "Jag hörde en ringning, men jag vet inte var den är", det finns Nagle-algoritmen och alternativet TCP_NODELAY, som var avsett för interaktivt arbete. Men förlåt, fördröja dess standardvärde - 200 Millisekunder. Om du verkligen vill skildra något liknande och spara på ett eventuellt paketpar - tja, skjut upp det, åtminstone i 5 sekunder, eller vad timeouten för meddelandet "Användaren skriver ..." nu är lika med. Men inte längre.

Och slutligen, pingar. Det vill säga att kontrollera att en TCP-anslutning är livlig. Det är roligt, men för ungefär 10 år sedan skrev jag en kritisk text om budbäraren för vår fakultets vandrarhem - där pingade författarna också servern från klienten, och inte tvärtom. Men tredjeårsstudenter är en sak, och ett internationellt kontor är en annan, eller hur? ..

Först ett litet utbildningsprogram. En TCP-anslutning, i avsaknad av paketutbyte, kan leva i veckor. Detta är både bra och dåligt, beroende på syftet. Tja, om du hade en SSH-anslutning till servern öppen, reste du dig från din dator, startade om strömroutern, återvände till din plats - sessionen genom den här servern bröts inte (skrev inget, det fanns inga paket), bekväm. Det är dåligt om det finns tusentals klienter på servern, var och en tar upp resurser (hej Postgres!), och klientvärden kan ha startat om för länge sedan - men vi kommer inte att veta om det.

Chatt/IM-system hör till det andra fallet av en annan anledning - onlinestatus. Om användaren "föll av" är det nödvändigt att informera sina samtalspartner om det. Annars kommer det att bli ett misstag som skaparna av Jabber gjorde (och korrigerade i 20 år) - användaren kopplade ur, men de fortsätter att skriva meddelanden till honom och tror att han är online (som också var helt förlorad under dessa några minuter innan pausen upptäcktes). Nej, alternativet TCP_KEEPALIVE, som många människor som inte förstår hur TCP-timers fungerar, dyker upp var som helst (genom att ställa in vilda värden som tiotals sekunder), kommer inte att hjälpa här - du måste se till att inte bara OS-kärnan i användarens maskin är vid liv, men fungerar också normalt, för att kunna svara, och själva applikationen (tror du att den inte kan frysa? Telegram Desktop på Ubuntu 18.04 har kraschat för mig upprepade gånger).

Det är därför du bör pinga server klient, och inte vice versa - om klienten gör detta, när anslutningen bryts, kommer ping inte att levereras, målet uppnås inte.

Och vad ser vi i Telegram? Allt är precis tvärtom! Jo, d.v.s. formellt kan naturligtvis båda sidor pinga varandra. I praktiken använder kunderna en krycka ping_delay_disconnect, som aktiverar en timer på servern. Tja, förlåt, det är inte kundens sak att bestämma hur länge han vill bo där utan ping. Servern, baserat på dess belastning, vet bättre. Men, naturligtvis, om du inte tycker synd om resurserna, då är den onda Pinocchio sig själva, och kryckan kommer att falla ...

Hur borde den ha utformats?

Jag tror att ovanstående fakta helt klart indikerar den inte särskilt höga kompetensen hos Telegram / VKontakte-teamet inom området för transport (och lägre) nivå av datanätverk och deras låga kvalifikationer i relevanta frågor.

Varför blev det så komplicerat, och hur kan Telegram-arkitekter försöka invända? Det faktum att de försökte göra en session som överlever TCP-anslutningen avbryts, det vill säga det vi inte levererade nu kommer vi att leverera senare. De försökte förmodligen också göra UDP-transport, även om de stötte på svårigheter och övergav det (det är därför dokumentationen är tom - det fanns inget att skryta med). Men på grund av en bristande förståelse för hur nätverk i allmänhet och TCP i synnerhet fungerar, var du kan lita på det, och var du behöver göra det själv (och hur), och försöker kombinera detta med kryptografi "ett skott av två fåglar i en smäll” - ett sådant kadaver visade sig.

Hur skulle det ha varit? Baserat på det faktum att msg_id är en tidsstämpel som är kryptografiskt nödvändig för att förhindra replay-attacker, är det ett fel att bifoga en unik identifierarfunktion till den. Därför, utan att drastiskt ändra den nuvarande arkitekturen (när uppdateringstråden bildas är detta ett API-ämne på hög nivå för en annan del av denna serie av inlägg), skulle man behöva:

  1. Servern som håller TCP-anslutningen till klienten tar ansvar - om du subtraherar från uttaget, vänligen bekräfta, bearbeta eller returnera ett fel, ingen förlust. Då är bekräftelsen inte en vektor av id, utan helt enkelt "det senast mottagna seq_no" - bara ett nummer, som i TCP (två nummer - din egen seq och bekräftad). Vi är alltid i session, eller hur?
  2. Tidsstämpeln för att förhindra reprisattacker blir ett separat fält, a la nonce. Kontrolleras, men inget annat påverkas. Nog och uint32 - om vårt salt ändras minst varje halv dag, kan vi allokera 16 bitar till de nedre bitarna av heltalsdelen av den aktuella tiden, resten - till bråkdelen av en sekund (som det är nu).
  3. Indragen msg_id överhuvudtaget - ur synvinkeln att särskilja förfrågningar på backends, finns det för det första klient-id, och för det andra sessions-id, och sammanfoga dem. Följaktligen, som en begäranidentifierare, räcker endast en seq_no.

Inte heller det bästa alternativet, en fullständig slump kan fungera som en identifierare - detta görs förresten redan i API:et på hög nivå när du skickar ett meddelande. Det skulle vara bättre att göra om arkitekturen från relativ till absolut helt och hållet, men det här är ett ämne för en annan del, inte det här inlägget.

API?

Ta-daam! Så, efter att ha tagit oss igenom en väg full av smärta och kryckor, kunde vi äntligen skicka alla förfrågningar till servern och ta emot alla svar på dem, samt ta emot uppdateringar från servern (inte som svar på en förfrågan, men det skickar oss själv, såsom PUSH, om någon så mycket tydligare).

Observera, nu kommer det att finnas det enda Perl-exemplet i artikeln! (för de som inte är bekanta med syntaxen är det första argumentet att välsigna objektets datastruktur, det andra är dess klass):

2019.10.24 12:00:51 $1 = {
'cb' => 'TeleUpd::__ANON__',
'out' => bless( {
'filter' => bless( {}, 'Telegram::ChannelMessagesFilterEmpty' ),
'channel' => bless( {
'access_hash' => '-6698103710539760874',
'channel_id' => '1380524958'
}, 'Telegram::InputPeerChannel' ),
'pts' => '158503',
'flags' => 0,
'limit' => 0
}, 'Telegram::Updates::GetChannelDifference' ),
'req_id' => '6751291954012037292'
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'req_msg_id' => '6751291954012037292',
'result' => bless( {
'pts' => 158508,
'flags' => 3,
'final' => 1,
'new_messages' => [],
'users' => [],
'chats' => [
bless( {
'title' => 'Хулиномика',
'username' => 'hoolinomics',
'flags' => 8288,
'id' => 1380524958,
'access_hash' => '-6698103710539760874',
'broadcast' => 1,
'version' => 0,
'photo' => bless( {
'photo_small' => bless( {
'volume_id' => 246933270,
'file_reference' => '
'secret' => '1854156056801727328',
'local_id' => 228648,
'dc_id' => 2
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'local_id' => 228650,
'file_reference' => '
'secret' => '1275570353387113110',
'volume_id' => 246933270
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1531221081
}, 'Telegram::Channel' )
],
'timeout' => 300,
'other_updates' => [
bless( {
'pts_count' => 0,
'message' => bless( {
'post' => 1,
'id' => 852,
'flags' => 50368,
'views' => 8013,
'entities' => [
bless( {
'length' => 20,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 18,
'offset' => 480,
'url' => 'https://alexeymarkov.livejournal.com/[url_вырезан].html'
}, 'Telegram::MessageEntityTextUrl' )
],
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'text' => '???? 165',
'data' => 'send_reaction_0'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 9'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'message' => 'А вот и новая книга! 
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
напечатаю.',
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571724559,
'edit_date' => 1571907562
}, 'Telegram::Message' ),
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'message' => bless( {
'edit_date' => 1571907589,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571807301,
'message' => 'Почему Вы считаете Facebook плохой компанией? Можете прокомментировать? По-моему, это шикарная компания. Без долгов, с хорошей прибылью, а если решат дивы платить, то и еще могут нехило подорожать.
Для меня ответ совершенно очевиден: потому что Facebook делает ужасный по качеству продукт. Да, у него монопольное положение и да, им пользуется огромное количество людей. Но мир не стоит на месте. Когда-то владельцам Нокии было смешно от первого Айфона. Они думали, что лучше Нокии ничего быть не может и она навсегда останется самым удобным, красивым и твёрдым телефоном - и доля рынка это красноречиво демонстрировала. Теперь им не смешно.
Конечно, рептилоиды сопротивляются напору молодых гениев: так Цукербергом был пожран Whatsapp, потом Instagram. Но всё им не пожрать, Паша Дуров не продаётся!
Так будет и с Фейсбуком. Нельзя всё время делать говно. Кто-то когда-то сделает хороший продукт, куда всё и уйдут.
#соцсети #facebook #акции #рептилоиды',
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 452'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'text' => '???? 21',
'data' => 'send_reaction_1'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'entities' => [
bless( {
'length' => 199,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 8,
'offset' => 919
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 928,
'length' => 9
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 6,
'offset' => 938
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 11,
'offset' => 945
}, 'Telegram::MessageEntityHashtag' )
],
'views' => 6964,
'flags' => 50368,
'id' => 854,
'post' => 1
}, 'Telegram::Message' ),
'pts_count' => 0
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'message' => bless( {
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 213'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 8'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 2940,
'entities' => [
bless( {
'length' => 609,
'offset' => 348
}, 'Telegram::MessageEntityItalic' )
],
'flags' => 50368,
'post' => 1,
'id' => 857,
'edit_date' => 1571907636,
'date' => 1571902479,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'message' => 'Пост про 1С вызвал бурную полемику. Человек 10 (видимо, 1с-программистов) единодушно написали:
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
Я бы добавил, что блестящая у 1С дистрибуция, а маркетинг... ну, такое.'
}, 'Telegram::Message' ),
'pts_count' => 0,
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'pts_count' => 0,
'message' => bless( {
'message' => 'Здравствуйте, расскажите, пожалуйста, чем вредит экономике 1С?
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
#софт #it #экономика',
'edit_date' => 1571907650,
'date' => 1571893707,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'flags' => 50368,
'post' => 1,
'id' => 856,
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 360'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 32'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 4416,
'entities' => [
bless( {
'offset' => 0,
'length' => 64
}, 'Telegram::MessageEntityBold' ),
bless( {
'offset' => 1551,
'length' => 5
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 3,
'offset' => 1557
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 1561,
'length' => 10
}, 'Telegram::MessageEntityHashtag' )
]
}, 'Telegram::Message' )
}, 'Telegram::UpdateEditChannelMessage' )
]
}, 'Telegram::Updates::ChannelDifference' )
}, 'MTProto::RpcResult' )
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'update' => bless( {
'user_id' => 2507460,
'status' => bless( {
'was_online' => 1571907651
}, 'Telegram::UserStatusOffline' )
}, 'Telegram::UpdateUserStatus' ),
'date' => 1571907650
}, 'Telegram::UpdateShort' )
};
2019.10.24 12:05:46 $1 = {
'in' => bless( {
'chats' => [],
'date' => 1571907946,
'seq' => 0,
'updates' => [
bless( {
'max_id' => 141719,
'channel_id' => 1295963795
}, 'Telegram::UpdateReadChannelInbox' )
],
'users' => []
}, 'Telegram::Updates' )
};
2019.10.24 13:01:23 $1 = {
'in' => bless( {
'server_salt' => '4914425622822907323',
'unique_id' => '5297282355827493819',
'first_msg_id' => '6751307555044380692'
}, 'MTProto::NewSessionCreated' )
};
2019.10.24 13:24:21 $1 = {
'in' => bless( {
'chats' => [
bless( {
'username' => 'freebsd_ru',
'version' => 0,
'flags' => 5440,
'title' => 'freebsd_ru',
'min' => 1,
'photo' => bless( {
'photo_small' => bless( {
'local_id' => 328733,
'volume_id' => 235140688,
'dc_id' => 2,
'file_reference' => '
'secret' => '4426006807282303416'
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'file_reference' => '
'volume_id' => 235140688,
'local_id' => 328735,
'secret' => '71251192991540083'
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1461248502,
'id' => 1038300508,
'democracy' => 1,
'megagroup' => 1
}, 'Telegram::Channel' )
],
'users' => [
bless( {
'last_name' => 'Panov',
'flags' => 1048646,
'min' => 1,
'id' => 82234609,
'status' => bless( {}, 'Telegram::UserStatusRecently' ),
'first_name' => 'Dima'
}, 'Telegram::User' )
],
'seq' => 0,
'date' => 1571912647,
'updates' => [
bless( {
'pts' => 137596,
'message' => bless( {
'flags' => 256,
'message' => 'Создать джейл с именем покороче ??',
'to_id' => bless( {
'channel_id' => 1038300508
}, 'Telegram::PeerChannel' ),
'id' => 119634,
'date' => 1571912647,
'from_id' => 82234609
}, 'Telegram::Message' ),
'pts_count' => 1
}, 'Telegram::UpdateNewChannelMessage' )
]
}, 'Telegram::Updates' )
};

Ja, speciellt inte under spoilern - om du inte har läst den, gå och gör det!

Oh, wai~~... hur ser det ut? Något mycket bekant... kanske är detta datastrukturen för ett typiskt webb-API i JSON, förutom att klasser kanske var kopplade till objekt?

Så det visar sig ... Vad är det, kamrater? .. Så mycket ansträngning - och vi stannade för att vila där webbprogrammerarna precis börjat?.. Skulle inte bara JSON över HTTPS vara enklare?! Och vad fick vi i utbyte? Var dessa ansträngningar värda det?

Låt oss utvärdera vad TL+MTProto har gett oss och vilka alternativ som är möjliga. Tja, HTTP-förfrågan-svar passar dåligt, men åtminstone något utöver TLS?

kompakt serialisering. När man ser denna datastruktur, liknande JSON, kommer man ihåg att det finns dess binära varianter. Låt oss markera MsgPack som otillräckligt töjbart, men det finns till exempel CBOR - förresten standarden som beskrivs i RFC 7049. Det är anmärkningsvärt för det faktum att det definierar taggar, som en förlängningsmekanism, och bland redan standardiserat det finns:

  • 25 + 256 - att ersätta dubbletter av linjer med en radnummerreferens, en sådan billig komprimeringsmetod
  • 26 - serialiserat Perl-objekt med klassnamn och konstruktorargument
  • 27 - serialiserat språkoberoende objekt med typnamn och konstruktorargument

Tja, jag försökte serialisera samma data i TL och CBOR med packning av strängar och objekt aktiverat. Resultatet började skilja sig till förmån för CBOR någonstans från en megabyte:

cborlen=1039673 tl_len=1095092

Så, slutsats: Det finns betydligt enklare format som inte är föremål för synkroniseringsfel eller okända identifieringsproblem, med jämförbar effektivitet.

Snabb uppkoppling. Detta betyder noll RTT efter återanslutning (när nyckeln redan har genererats en gång) - tillämpligt från det allra första MTProto-meddelandet, men med vissa reservationer - de hamnade i samma salt, sessionen blev inte ruttet osv. Vad erbjuder TLS oss i gengäld? Relaterat citat:

När du använder PFS i TLS, TLS sessionsbiljetter (RFC 5077) för att återuppta den krypterade sessionen utan att omförhandla nycklarna och utan att lagra nyckelinformationen på servern. När den första anslutningen öppnas och nycklar genereras, krypterar servern anslutningens tillstånd och skickar det till klienten (i form av en sessionsbiljett). Följaktligen, när anslutningen återupptas, skickar klienten en sessionsbiljett innehållande bland annat sessionsnyckeln tillbaka till servern. Själva biljetten är krypterad med en temporär nyckel (session ticket key), som lagras på servern och ska distribueras till alla frontendservrar som hanterar SSL i klustrade lösningar.[10]. Således kan införandet av en sessionsbiljett bryta mot PFS om temporära servernycklar äventyras, till exempel när de lagras under lång tid (OpenSSL, nginx, Apache lagrar dem som standard under hela tiden programmet körs; populära webbplatser använd nyckeln i flera timmar, upp till dagar).

Här är RTT inte noll, du behöver byta åtminstone ClientHello och ServerHello, varefter klienten tillsammans med Finished redan kan skicka data. Men här ska man komma ihåg att vi inte har webben, med dess gäng nyöppnade förbindelser, utan en budbärare, vars anslutning ofta är en och mer eller mindre långlivade, relativt korta förfrågningar om webbsidor - allt är multiplexerad inuti. Det vill säga, det är helt acceptabelt, om vi inte stötte på en mycket dålig tunnelbanesektion.

Glömt något annat? Skriv i kommentarerna.

Fortsättning följer!

I den andra delen av denna serie av inlägg kommer vi att överväga organisatoriska frågor snarare än tekniska - tillvägagångssätt, ideologi, gränssnitt, attityd till användare, etc. Baserat dock på den tekniska information som presenterades här.

Den tredje delen kommer att fortsätta analysen av den tekniska komponenten / utvecklingsupplevelsen. Du kommer att lära dig särskilt:

  • fortsättning på pandemonium med olika TL-typer
  • okända saker om kanaler och supergrupper
  • än dialoger är värre än listor
  • om absolut vs relativ meddelandeadressering
  • vad är skillnaden mellan foto och bild
  • hur emoji stör kursiverad text

och andra kryckor! Håll ögonen öppna!

Källa: will.com

Lägg en kommentar