Krityk op it protokol en organisatoaryske oanpak fan Telegram. Diel 1, technysk: ûnderfining fan it skriuwen fan in klant fanôf it begjin - TL, MT

Koartlyn binne berjochten oer hoe goed Telegram is, hoe briljant en erfaren de bruorren Durov binne yn it bouwen fan netwurksystemen, ensfh. Tagelyk hawwe heul pear minsken har wirklik ûnderdompele yn it technyske apparaat - op syn meast brûke se in frij ienfâldige (en hiel oars as MTProto) Bot API basearre op JSON, en akseptearje gewoanlik gewoan op leauwe alle lof en PR dy't draaie om de boadskipper. Hast in jier en in heal lyn begon myn kollega by de Eshelon NGO Vasily (spitigernôch, syn akkount op Habré waard wiske tegearre mei it ûntwerp) begon syn eigen Telegram-kliïnt fanôf it begjin te skriuwen yn Perl, en letter kaam de skriuwer fan dizze rigels by. Wêrom Perl, sille guon daliks freegje? Om't sokke projekten al yn oare talen besteane, dêr giet it trouwens net om, der kin in oare taal wêze dêr't gjin klearmakke biblioteek, en dêrtroch moat de skriuwer de hiele wei gean út it neat wei. Boppedat is kryptografy in kwestje fan fertrouwen, mar ferifiearje. Mei in produkt rjochte op feiligens, kinne jo net gewoan fertrouwe op in klearmakke bibleteek fan de fabrikant en blyn fertrouwe op it (lykwols, dit is in ûnderwerp foar it twadde diel). Op it stuit wurket de bibleteek frij goed op it "gemiddelde" nivo (kinne jo alle API-oanfragen meitsje).

D'r sil lykwols net folle kryptografy of wiskunde wêze yn dizze searje berjochten. Mar d'r sille in protte oare technyske details en arsjitektoanyske krukken wêze (ek nuttich foar dyjingen dy't net fanôf it begjin skriuwe, mar de bibleteek yn elke taal brûke). Dat, it haaddoel wie om te besykjen om de klant fanôf it begjin út te fieren neffens offisjele dokumintaasje. Dat is, lit ús oannimme dat de boarnekoade fan offisjele kliïnten is sletten (wer, yn it twadde diel sille wy it ûnderwerp fan it feit dat dit wier is mear detaillearre wurde it bart dus), mar, lykas yn 'e âlde dagen, bygelyks, is d'r in standert lykas RFC - is it mooglik om in kliïnt te skriuwen neffens de spesifikaasje allinich, "sûnder te sjen" nei de boarnekoade, of it no offisjeel is (Telegram Desktop, mobyl), of net-offisjele Telethon?

Ynhâldsopjefte:

Dokumintaasje ... it bestiet, krekt? Is it wier?..

Fragminten fan notysjes foar dit artikel begon ferline simmer te sammeljen. Al dy tiid op 'e offisjele webside https://core.telegram.org De dokumintaasje wie fan Laach 23, d.w.s. fêst te sitten earne yn 2014 (tink, der wiene doe noch net iens kanalen?). Fansels, yn teory, soe dit ús tastean moatte om in klant mei funksjonaliteit op dat stuit yn 2014 te ymplementearjen. Mar sels yn dizze steat wie de dokumintaasje, yn it foarste plak, ûnfolslein, en twadde, op plakken dy't harsels tsjinsprekt. Krekt mear as in moanne lyn, yn septimber 2019, wie it tafallich It waard ûntdutsen dat der in grutte update fan 'e dokumintaasje op' e side wie, foar de folslein resinte Layer 105, mei in opmerking dat no alles opnij lêzen wurde moat. Ja, in protte artikels waarden herzien, mar in protte bleaunen net feroare. Dêrom, by it lêzen fan de krityk hjirûnder oer de dokumintaasje, moatte jo yn 'e rekken hâlde dat guon fan dizze dingen net mear relevant binne, mar guon binne noch altyd. Ommers, 5 jier yn 'e moderne wrâld is net allinnich in lange tiid, mar tige in soad. Sûnt dy tiden (benammen as jo net rekken holden mei de ôfset en wer libben geochat sites sûnt doe), is it oantal API metoaden yn it skema groeid út hûndert nei mear as twahûndert en fyftich!

Wêr te begjinnen as jonge skriuwer?

It makket neat út oft jo skriuwe fanôf it begjin of brûke, bygelyks, klearmakke biblioteken lykas Telethon foar Python of Madeline foar PHP, yn alle gefallen, do silst nedich earst registrearje jo applikaasje - krije parameters api_id и api_hash (dejingen dy't hawwe wurke mei de VKontakte API fuortendaliks begripe) wêrmei de tsjinner sil identifisearje de applikaasje. Dit sil moatte doch it om juridyske redenen, mar wy sille mear prate oer wêrom't biblioteekskriuwers it net kinne publisearje yn it twadde diel. Jo kinne tefreden wêze mei de testwearden, hoewol se heul beheind binne - it feit is dat jo no kinne registrearje mar ien app, dus haast der net yn.

No, út in technysk eachpunt, moatte wy ynteressearre wêze yn it feit dat wy nei registraasje notifikaasjes moatte ûntfange fan Telegram oer updates foar dokumintaasje, protokol, ensfh. Dat is, men koe oannimme dat de side mei de docks gewoan ferlitten waard en trochgie spesifyk te wurkjen mei dyjingen dy't begon te meitsjen fan kliïnten, om't it is makliker. Mar nee, der waard neat waarnommen, der kaam gjin ynformaasje.

En as jo fanôf it begjin skriuwe, dan is it brûken fan de krigen parameters eins noch in lange wei. Alhoewol https://core.telegram.org/ en praat oer harren yn Getting Started earst fan alle, yn feite, jo sille earst moatte útfiere MTProto protokol - mar as jo leaude yndieling neffens it OSI-model oan 'e ein fan 'e side foar in algemiene beskriuwing fan it protokol, dan is it folslein om 'e nocht.

Yn feite, sawol foar as nei MTProto, op ferskate nivo's tagelyk (lykas bûtenlânske netwurkers dy't wurkje yn 'e OS kernel sizze, laach violation), sil in grut, pynlik en skriklik ûnderwerp yn' e wei komme ...

Binêre serialisaasje: TL (Type Language) en syn skema, en lagen, en in protte oare enge wurden

Dit ûnderwerp is feitlik de kaai foar de problemen fan Telegram. En d'r sille in protte ferskriklike wurden wêze as jo besykje yn te dûken.

Dus, hjir is it diagram. As dit wurd yn jo tinken komt, sis dan, JSON-skema, Do tochtst goed. It doel is itselde: wat taal om in mooglike set fan oerdroegen gegevens te beskriuwen. Dit is wêr't de oerienkomsten einigje. As fan 'e side MTProto protokol, of út 'e boarnebeam fan' e offisjele kliïnt, sille wy besykje wat skema te iepenjen, wy sille wat sjen as:

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;

In persoan dy't dit foar de earste kear sjocht, sil yntuïtyf mar in part fan wat skreaun is werkenne kinne - no, dit binne blykber struktueren (hoewol wêr is de namme, links of rjochts?), d'r binne fjilden yn, wêrnei't in type folget nei in kolon ... wierskynlik. Hjir yn hoeke heakjes binne d'r wierskynlik sjabloanen lykas yn C++ (feitlik, net krekt). En wat betsjutte alle oare symboalen, fraachtekens, útroptekens, persintaazjes, hashtekens (en fansels betsjutte se ferskillende dingen op ferskate plakken), soms oanwêzich en soms net, heksadesimale sifers - en it wichtichste, hoe te krijen fan dit regelmjittich (dat sil net wurde ôfwiisd troch de tsjinner) byte stream? Jo moatte de dokumintaasje lêze (ja, d'r binne keppelings nei it skema yn 'e JSON-ferzje tichtby - mar dat makket it net dúdliker).

Iepenje de side Binary Data Serialization en dûk yn 'e magyske wrâld fan paddestoelen en diskrete wiskunde, wat fergelykber mei matan yn it 4e jier. Alfabet, type, wearde, combinator, funksjonele combinator, normale foarm, gearstalde type, polymorphic type ... en dat is allegear mar de earste side! Folgjende wachtet dy TL Taal, dy't, hoewol't it al in foarbyld fan in triviale fersyk en antwurd befettet, gjin antwurd jout op mear typyske gefallen, wat betsjut dat jo troch in wertelling fan wiskunde oerset út it Russysk yn it Ingelsk op nochris acht ynbêde moatte waad siden!

Lêzers dy't fertroud binne mei funksjonele talen en automatyske typekonferinsje sille de beskriuwingstaal yn dizze taal fansels, sels út it foarbyld, as folle fertrouder sjen, en kinne sizze dat dit yn prinsipe net min is. De beswieren dêre binne:

  • ja, it doel klinkt goed, mar och, sy net berikt
  • Underwiis oan Russyske universiteiten ferskilt sels tusken IT spesjaliteiten - net elkenien hat nommen de oerienkommende kursus
  • Uteinlik, sa't wy sille sjen, yn 'e praktyk is it net nedich, om't mar in beheinde subset fan sels de beskreaune TL wurdt brûkt

Lykas sein Leonerd op it kanaal #perl yn it FreeNode IRC-netwurk, dy't besocht in poarte fan Telegram nei Matrix út te fieren (oersetting fan it sitaat is net krekt út it ûnthâld):

It fielt as wie ien foar it earst yntrodusearre yn typeteory, waard optein en begon te besykjen om dermei te boartsjen, net echt te skele oft it yn 'e praktyk nedich wie.

Sjoch foar josels, as de needsaak foar bleate typen (int, lang, ensfh.) as wat elemintêrs gjin fragen opropt - úteinlik moatte se mei de hân ymplementearre wurde - litte wy bygelyks in besykjen nimme om der fan ôf te kommen vector. Dat is yn feite, array, as jo de resultearjende dingen by har eigen namme neame.

Mar earder

In koarte beskriuwing fan in subset fan TL-syntaksis foar dyjingen dy't de offisjele dokumintaasje net lêze

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;

Definysje begjint altyd konstruktor, wêrnei't opsjoneel (yn 'e praktyk - altyd) troch it symboal # moat wêze CRC32 út de normalisearre beskriuwingsstring fan dit type. Folgjende komt in beskriuwing fan de fjilden; as se besteane, kin it type leech wêze. Dit einiget allegear mei in lykweardich teken, de namme fan it type dêr't dizze konstruktor - dat is trouwens it subtype - ta heart. De man rjochts fan it lykweardich teken is polymorfysk - dat is, ferskate spesifike soarten kinne dêrmei oerienkomme.

As de definysje komt nei de line ---functions---, dan sil de syntaksis itselde bliuwe, mar de betsjutting sil oars wêze: de konstruktor sil de namme wurde fan 'e RPC-funksje, de fjilden wurde parameters (goed, dat is, it sil krekt deselde opjûne struktuer bliuwe, lykas hjirûnder beskreaun , dit sil gewoan de tawiisde betsjutting wêze), en it "polymorfyske type" - it type fan it weromjûne resultaat. Wier, it sil noch polymorfysk bliuwe - krekt definieare yn 'e seksje ---types---, mar dizze constructor sil "net wurde beskôge". It oerladen fan de soarten neamde funksjes troch har arguminten, d.w.s. Om ien of oare reden binne ferskate funksjes mei deselde namme, mar ferskillende hantekeningen, lykas yn C ++, net foarsjoen yn 'e TL.

Wêrom "constructor" en "polymorphic" as it is net OOP? No, yn feite sil it makliker wêze foar ien om hjir oer te tinken yn OOP-termen - in polymorfysk type as in abstrakte klasse, en konstruktors binne syn direkte neiteam, en final yn de terminology fan in tal talen. Yn feite, fansels, allinnich hjir oerienkomst mei echte oerladen konstruktormetoaden yn OO-programmearringstalen. Om't hjir gewoan gegevensstruktueren binne, binne d'r gjin metoaden (hoewol't de beskriuwing fan funksjes en metoaden fierders frijwat by steat is om betizing yn 'e holle te meitsjen dat se bestean, mar dat is in oare saak) - kinne jo tinke oan in konstruktor as in wearde út hokker wurdt oanlein type by it lêzen fan in bytestream.

Hoe komt dit? De deserializer, dy't altyd 4 bytes lêst, sjocht de wearde 0xcrc32 - en begrypt wat der dan barre sil field1 mei type int, d.w.s. lêst presys 4 bytes, op dit it oerlizzende fjild mei it type PolymorType lêze. Sjocht 0x2crc32 en begrypt dat der twa fjilden fierder, earst long, wat betsjut dat wy 8 bytes lêze. En dan wer in kompleks type, dat op deselde wize deserialisearre wurdt. Bygelyks, Type3 koe wurde ferklearre yn it circuit sa gau as twa constructors, respektivelik, dan moatte se moetsje beide 0x12abcd34, wêrnei't jo noch 4 bytes lêze moatte int, of 0x6789cdef, wêrnei't der neat komt. Al it oare - jo moatte in útsûndering smite. Hoe dan ek, hjirnei geane wy ​​werom nei it lêzen fan 4 bytes int fjilden field_c в constructorTwo en dêrmei binne wy ​​klear mei it lêzen fan ús PolymorType.

As lêste, as jo krije fongen 0xdeadcrc foar constructorThree, dan wurdt alles yngewikkelder. Us earste fjild is bit_flags_of_what_really_present mei type # - yn feite, dit is gewoan in alias foar it type nat, wat "natuerlik nûmer" betsjut. Dat is, yn feite, unsigned int is, troch de wei, it ienige gefal as net-ûndertekene nûmers foarkomme yn echte circuits. Dus, folgjende is in konstruksje mei in fraachteken, wat betsjuttet dat dit fjild - it sil allinich oanwêzich wêze op 'e draad as it oerienkommende bit is ynsteld yn it fjild neamd (sawat as in ternêre operator). Dus, lit ús oannimme dat dit bit waard ynsteld, wat betsjut dat wy fierder moatte lêze in fjild lykas Type, dy't yn ús foarbyld 2 constructors hat. Ien is leech (bestiet allinich út de identifier), de oare hat in fjild ids mei type ids:Vector<long>.

Jo kinne tinke dat sawol sjabloanen as generika binne yn 'e profs as Java. Mar nee. Hast. Dit allinnich gefal fan in gebrûk hoek heakjes yn echte circuits, en it wurdt brûkt ONLY foar Vector. Yn in bytestream sille dit 4 CRC32 bytes wêze foar it Vectortype sels, altyd itselde, dan 4 bytes - it oantal array-eleminten, en dan dizze eleminten sels.

Foegje hjirby it feit ta dat serialisaasje altyd foarkomt yn wurden fan 4 bytes, alle soarten binne multiplen dêrfan - de ynboude typen wurde ek beskreaun bytes и string mei manuele serialisaasje fan 'e lingte en dizze ôfstimming mei 4 - goed, it liket normaal en sels relatyf effektyf te klinken? Hoewol TL wurdt beweard in effektive binêre serialisaasje te wêzen, sil JSON mei har, mei de útwreiding fan sawat alles, sels Booleaanske wearden en ienkarakterstrings nei 4 bytes, noch folle dikker wêze? Sjoch, sels ûnnedige fjilden kinne wurde oerslein mei bitflaggen, alles is frij goed, en sels útbreidber foar de takomst, dus wêrom net letter nije opsjonele fjilden tafoegje oan de constructor?

Mar nee, as jo lêze net myn koarte beskriuwing, mar de folsleine dokumintaasje, en tink oer de útfiering. As earste wurdt de CRC32 fan 'e konstruktor berekkene neffens de normalisearre line fan' e tekstbeskriuwing fan it skema (ekstra wite romte fuortsmite, ensfh.) - dus as in nij fjild wurdt tafoege, sil de typebeskriuwingsrigel feroarje, en dêrtroch syn CRC32 en , dêrtroch, serialization. En wat soe de âlde klant dwaan as hy in fjild krige mei nije flaggen set, en hy wit net wat hy der fierder mei dwaan moat?

Twads, lit ús ûnthâlde CRC32, dy't hjir yn wêzen brûkt wurdt as hash funksjes om unyk te bepalen hokker type wurdt (de) serialisearre. Hjir steane wy ​​foar it probleem fan botsingen - en nee, de kâns is net ien yn 232, mar folle grutter. Wa ûnthâlde dat CRC32 is ûntwurpen foar in detect (en korrizjearje) flaters yn de kommunikaasje kanaal, en dêrmei ferbettert dizze eigenskippen yn it neidiel fan oaren? Bygelyks, it makket it net út om bytes opnij te regeljen: as jo CRC32 berekkenje fan twa rigels, yn 'e twadde wikselje jo de earste 4 bytes mei de folgjende 4 bytes - it sil itselde wêze. As ús ynfier tekststrings út it Latynske alfabet is (en in bytsje ynterpunksje), en dizze nammen net bysûnder willekeurich binne, nimt de kâns op sa'n weryndieling gâns ta.

Trouwens, wa hat kontrolearre wat der wie? echt CRC32? Ien fan 'e iere boarne koades (sels foar Waltman) hie in hash funksje dy't fermannichfâldige elk karakter mei it nûmer 239, sa leafste troch dizze minsken, ha ha!

Ta beslút, oke, wy realisearre dat constructors mei in fjild type Vector<int> и Vector<PolymorType> sil hawwe ferskillende CRC32. Hoe sit it mei online prestaasjes? En út in teoretysk eachpunt, wurdt dit diel fan it type? Litte we sizze dat wy passe in rige fan tsien tûzen nûmers, goed mei Vector<int> alles is dúdlik, de lingte en oare 40000 bytes. Wat as dit Vector<Type2>, dat bestiet út mar ien fjild int en it is allinich yn it type - moatte wy 10000xabcdef0 34 kear werhelje en dan 4 bytes int, of de taal is yn steat om it foar ús fan 'e konstruktor INDEPEND fixedVec en ynstee fan 80000 bytes, oermeitsje wer allinnich 40000?

Dit is hielendal gjin idle teoretyske fraach - stel jo foar dat jo in list krije mei groepbrûkers, elk fan wa't in id, foarnamme, efternamme hat - it ferskil yn 'e hoemannichte gegevens oer in mobile ferbining kin signifikant wêze. It is krekt de effektiviteit fan Telegram-serialisaasje dy't ús wurdt advertearre.

Sa…

Vector, dat waard nea útbrocht

As jo ​​besykje te wade troch de siden fan beskriuwing fan combinators ensafuorthinne, do silst sjen dat in vector (en sels in matrix) formeel besiket te wurde útfier troch tuples fan ferskate blêden. Mar op it lêst ferjitte se, de lêste stap wurdt oerslein, en in definysje fan in fektor wurdt gewoan jûn, dy't noch net bûn is oan in type. Wat is der oan de hân? Yn talen programmearring, benammen funksjonele, is it hiel typysk om de struktuer rekursyf te beskriuwen - de kompilator mei syn luie evaluaasje sil alles sels begripe en dwaan. Yn taal data serialization wat nedich is effisjinsje: it is genôch om gewoan te beskriuwen list, d.w.s. struktuer fan twa eleminten - de earste is in gegevens elemint, de twadde is deselde struktuer sels as in lege romte foar de sturt (pack (cons) yn Lisp). Mar dit sil fansels fereaskje fan elk elemint besteget in ekstra 4 bytes (CRC32 yn it gefal yn TL) foar in beskriuwe syn type. In array kin ek maklik beskreaun wurde fêste grutte, mar yn it gefal fan in array fan ûnbekende lingte fan tefoaren, wy brekke ôf.

Dêrom, om't TL gjin fektor útfiert, moast it oan 'e kant tafoege wurde. Uteinlik seit de dokumintaasje:

Serialisaasje brûkt altyd deselde constructor "vektor" (const 0x1cb5c415 = crc32 ("vector t: Type # [t] = Vector t") dat is net ôfhinklik fan de spesifike wearde fan de fariabele fan type t.

De wearde fan de opsjonele parameter t is net belutsen by de serialisaasje, om't it is ôflaat fan it resultaattype (altyd bekend foarôfgeand oan deserialisaasje).

Sjoch in tichterby: vector {t:Type} # [ t ] = Vector t - mar noat Dizze definysje sels seit net dat it earste nûmer gelyk wêze moat oan de lingte fan de fektor! En it komt nearne wei. Dit is in opjûne dy't yn gedachten moat wurde hâlden en mei jo hannen ymplementearre. Op oare plakken neamt de dokumintaasje sels earlik dat it type net echt is:

De Vector t polymorphic pseudotype is in "type" waans wearde is in folchoarder fan wearden fan elk type t, of boxed of bleat.

... mar rjochtet him der net op. As jo, wurch fan it trochswaaien fan 'e wiskunde (miskien sels bekend fan in universitêre kursus), beslute om op te jaan en eins te sjen hoe't jo der yn 'e praktyk mei wurkje kinne, is de yndruk dy't yn jo holle bliuwt dat dit Serious is Wiskunde yn 'e kearn, it waard dúdlik útfûn troch Cool People (twa wiskundigen - ACM winner), en net allinne immen. It doel - om te pronken - is berikt.

Trouwens, oer it oantal. Lit ús jo dat herinnerje # it is in synonym nat, natuerlik nûmer:

Der binne type útdrukkingen (type-expr) en numerike útdrukkingen (nat-expr). Se wurde lykwols op deselde manier definieare.

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

mar yn de grammatika wurde se op deselde wize beskreaun, d.w.s. Dit ferskil moat wer betocht wurde en mei de hân yn útfiering brocht wurde.

No, ja, sjabloantypen (vector<int>, vector<User>) hawwe in mienskiplike identifier (#1cb5c415), d.w.s. as jo witte dat de oprop wurdt oankundige as

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

dan wachtsje jo net mear op gewoan in vector, mar in vector fan brûkers. Krekter, soe moatte wachtsje - yn echte koade sil elk elemint, as net in bleate type, in konstruktor hawwe, en op in goede manier yn ymplemintaasje soe it nedich wêze om te kontrolearjen - mar wy waarden presys stjoerd yn elk elemint fan dizze fektor dat type? Wat as it wie in soarte fan PHP, wêryn in array kin befetsje ferskillende soarten yn ferskillende eleminten?

Op dit punt begjinne jo te tinken - is sa'n TL nedich? Miskien soe it foar de karre mooglik wêze om in minsklike serializer te brûken, deselde protobuf dy't doe al bestie? Dat wie de teory, lit ús nei de praktyk sjen.

Besteande TL-ymplemintaasjes yn koade

TL waard berne yn 'e djipten fan VKontakte sels foar de ferneamde eveneminten mei de ferkeap fan Durov syn oandiel en (wiswier,), sels foardat de ûntwikkeling fan Telegram begon. En yn iepen boarne boarnekoade fan de earste ymplemintaasje kinne jo fine in soad grappige krukken. En de taal sels waard dêr folsleiner ymplementearre as no yn Telegram. Bygelyks, hashes wurde hielendal net brûkt yn it skema (dat betsjut in ynboude pseudotype (lykas in vector) mei ôfwikend gedrach). Of

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

mar lit ús foar de folsleinens beskôgje om, om sa te sizzen, de evolúsje fan 'e Giant of Thought te folgjen.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Of dizze moaie:

    static const char *reserved_words_polymorhic[] = {

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

      };

Dit fragmint giet oer sjabloanen lykas:

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

Dit is de definysje fan in hashmap-sjabloantype as in fektor fan int - Type-pearen. Yn C++ soe it der sa útsjen:

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

sa, alpha - kaaiwurd! Mar allinnich yn C ++ kinne jo skriuwe T, mar jo moatte skriuwe alfa, beta ... Mar net mear as 8 parameters, dat is wêr't de fantasy einiget. It liket derop dat der eartiids yn Sint-Petersburch guon dialogen lykas dit plakfûnen:

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

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

Mar dit gie oer de earste publisearre ymplemintaasje fan TL "yn 't algemien". Litte wy trochgean mei it beskôgjen fan ymplemintaasjes yn 'e Telegram-kliïnten sels.

Wurd oan Vasily:

Vasily, [09.10.18 17:07] Meast fan alles, de ezel is hyt omdat se makke in boskje abstraksjes, en dan hammere in bout op harren, en bedekke de koade generator mei krukken
As gefolch, earst út dock pilot.jpg
Dan út de koade dzhekichan.webp

Fansels, fan minsken dy't bekend binne mei algoritmen en wiskunde, kinne wy ​​​​ferwachtsje dat se Aho, Ullmann hawwe lêzen en bekend binne mei de ark dy't de facto standert binne wurden yn 'e sektor yn' e desennia foar it skriuwen fan har DSL-kompilers, toch? ..

Troch telegram-cli is Vitaly Valtman, lykas kin wurde begrepen út it foarkommen fan it TLO-formaat bûten har (cli) grinzen, lid fan it team - no is in biblioteek foar TL-parsing tawiisd apart, wat is de yndruk fan har TL parser? ..

16.12 04:18 Vasily: Ik tink dat immen lex+yacc net behearske
16.12 04:18 Vasily: Ik kin it net oars útlizze
16.12 04:18 Vasily: goed, of se waarden betelle foar it oantal rigels yn VK
16.12 04:19 Vasily: 3k+ rigels etc<censored> ynstee fan in parser

Miskien in útsûndering? Litte wy sjen hoe docht Dit is de OFFISIËLE kliïnt - 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+ rigels yn Python, in pear reguliere útdrukkingen + spesjale gefallen lykas in fektor, dy't, fansels, wurdt ferklearre yn it skema lykas it moat wêze neffens de TL-syntaksis, mar se fertrouden op dizze syntaksis om it te parsearjen ... De fraach ûntstiet, wêrom wie it allegear in wûnder?иIt is mear lagen as gjinien it dochs neffens de dokumintaasje sil parse?!

Troch de wei ... Unthâld dat wy praat oer CRC32 kontrolearjen? Dat, yn 'e Telegram Desktop-koadegenerator is d'r in list mei útsûnderings foar dy soarten wêryn de berekkene CRC32 komt net oerien mei de iene oanjûn yn it diagram!

Vasily, [18.12/22 49:XNUMX] en hjir soe ik tinke oft sa'n TL nedich is
as ik mei alternative ymplemintaasjes rommele woe, soe ik begjinne mei it ynfoegjen fan line breaks, de helte fan 'e parsers sil brekke op multi-line definysjes
tdesktop, lykwols, te

Unthâld it punt oer one-liner, wy komme der in bytsje letter op werom.

Okee, telegram-cli is net-offisjeel, Telegram Desktop is offisjeel, mar hoe sit it mei de oaren? Wa wit. subseksje hjirûnder.

Hokker oare fragen ropt serialisaasje yn 'e praktyk op? Se diene bygelyks in protte dingen, fansels, mei bitfjilden en betingstfjilden:

Vasily: flags.0? true
betsjut dat it fjild oanwêzich is en is gelyk oan wier as de flagge is ynsteld

Vasily: flags.1? int
betsjut dat it fjild oanwêzich is en moat wurde deserialisearre

Vasily: Ezel, meitsje jo gjin soargen oer wat jo dogge!
Vasily: D'r is earne in fermelding yn it dokumint dat wier in keale nul-lingte type is, mar it is ûnmooglik om wat fan har dokumint te sammeljen
Vasily: D'r is ek net sa'n ding yn iepen ymplemintaasjes, mar d'r binne in protte krukken en stipet

Hoe sit it mei Telethon? Foarútsjen nei it ûnderwerp fan MTProto, in foarbyld - yn 'e dokumintaasje binne sokke stikken, mar it teken % it wurdt allinnich omskreaun as "oerienkomme mei in opjûne bleate-type", d.w.s. yn 'e foarbylden hjirûnder is d'r in flater of wat net dokumintearre:

Vasily, [22.06.18 18:38] Op ien plak:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Yn in oar:

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

En dit binne twa grutte ferskillen, yn it echte libben komt in soarte fan neakene vector

Ik haw net sjoen in bleate vector definysje en haw net komme oer in

Analyse wurdt skreaun mei de hân yn teleton

Yn syn diagram wurdt de definysje kommentearre msg_container

Nochris bliuwt de fraach oer %. It wurdt net beskreaun.

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

Vasily, [22.06.18 19:23] Mar har TL-parser op reguliere motoren sil dit nei alle gedachten ek net ite

// parsed manually

TL is in prachtige abstraksje, gjinien ymplementearret it folslein

En % is net yn har ferzje fan it skema

Mar hjir sprekt de dokumintaasje himsels tsjin, dus idk

It waard fûn yn 'e grammatika, se koene gewoan fergetten wêze om de semantyk te beskriuwen

Jo seagen it dokumint op TL, jo kinne it net útfine sûnder in heale liter

"No, lit ús sizze," sil in oare lêzer sizze, "jo bekritisearje wat, dus lit my sjen hoe't it dien wurde moat."

Vasily antwurdet: "Wat de parser oanbelanget, ik hâld fan dingen lykas

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

ien of oare manier like it better as

struct tree *parse_args4 (void) {
  PARSE_INIT (type_args4);
  struct parse so = save_parse ();
  PARSE_TRY (parse_optional_arg_def);
  if (S) {
    tree_add_child (T, S);
  } else {
    load_parse (so);
  }
  if (LEX_CHAR ('!')) {
    PARSE_ADD (type_exclam);
    EXPECT ("!");
  }
  PARSE_TRY_PES (parse_type_term);
  PARSE_OK;
}

of

        # Regex to match the whole line
        match = re.match(r'''
            ^                  # We want to match from the beginning to the end
            ([w.]+)           # The .tl object can contain alpha_name or namespace.alpha_name
            (?:
                #             # After the name, comes the ID of the object
                ([0-9a-f]+)    # The constructor ID is in hexadecimal form
            )?                 # If no constructor ID was given, CRC32 the 'tl' to determine it

            (?:s              # After that, we want to match its arguments (name:type)
                {?             # For handling the start of the '{X:Type}' case
                w+            # The argument name will always be an alpha-only name
                :              # Then comes the separator between name:type
                [wd<>#.?!]+  # The type is slightly more complex, since it's alphanumeric and it can
                               # also have Vector<type>, flags:# and flags.0?default, plus :!X as type
                }?             # For handling the end of the '{X:Type}' case
            )*                 # Match 0 or more arguments
            s                 # Leave a space between the arguments and the equal
            =
            s                 # Leave another space between the equal and the result
            ([wd<>#.?]+)     # The result can again be as complex as any argument type
            ;$                 # Finally, the line should always end with ;
            ''', tl, re.IGNORECASE | re.VERBOSE)

dit is de HELE lexer:

    ---functions---         return FUNCTIONS;
    ---types---             return TYPES;
    [a-z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return LC_ID;
    [A-Z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return UC_ID;
    [0-9]+                  yylval.number = atoi(yytext); return NUM;
    #[0-9a-fA-F]{1,8}       yylval.number = strtol(yytext+1, NULL, 16); return ID_HASH;

    n                      /* skip new line */
    [ t]+                  /* skip spaces */
    //.*$                 /* skip comments */
    /*.**/              /* skip comments */
    .                       return (int)yytext[0];

dy. ienfâldiger is it myld te sizzen."

As resultaat passe de parser en koadegenerator foar de werklik brûkte subset fan TL yn likernôch 100 rigels grammatika en ~300 rigels fan 'e generator (alles telle print's oanmakke koade), ynklusyf type ynformaasje buns foar yntrospeksje yn elke klasse. Elts polymorphic type feroaret yn in lege abstrakte basis klasse, en constructors ervje derfan en hawwe metoaden foar serialization en deserialization.

Gebrek oan typen yn de type taal

Sterk typen is in goede saak, toch? Nee, dit is gjin holivar (hoewol't ik leaver dynamyske talen), mar in postulaat yn it ramt fan TL. Op grûn dêrfan moat de taal ús allerhanne kontrôles leverje. No, goed, miskien net hy sels, mar de útfiering, mar hy moat se op syn minst beskriuwe. En wat foar kânsen wolle wy?

Earst fan alle, beheiningen. Hjir sjogge wy yn 'e dokumintaasje foar it uploaden fan bestannen:

De binêre ynhâld fan it bestân wurdt dan opdield yn dielen. Alle dielen moatte deselde grutte hawwe ( diel_grutte ) en de folgjende betingsten moatte foldien wurde:

  • part_size % 1024 = 0 (dielber troch 1KB)
  • 524288 % part_size = 0 (512KB moat lykwichtich dield wurde troch part_size)

It lêste diel hoecht net te foldwaan oan dizze betingsten, mits de grutte is minder as part_size.

Elk diel moat in folchoardernûmer hawwe, file_part, mei in wearde fariearjend fan 0 oant 2,999.

Neidat it bestân is partitionearre, moatte jo in metoade kieze foar it bewarjen fan it op 'e tsjinner. Brûke upload.saveBigFilePart yn gefal de folsleine grutte fan de triem is mear as 10 MB en upload.saveFilePart foar lytsere triemmen.
[…] ien fan 'e folgjende gegevensynfierflaters kin weromjûn wurde:

  • FILE_PARTS_INVALID - Unjildich oantal dielen. De wearde is net tusken 1..3000

Is ien fan dit yn it diagram? Is dit op ien of oare manier útdruklik mei TL? Nee. Mar ekskús, sels pake syn Turbo Pascal koe de oantsjutte soarten beskriuwe ranges. En hy wist noch ien ding, no better bekend as enum - in type besteande út in opsomming fan in fêst (lyts) oantal wearden. Yn talen lykas C - numerike, tink derom dat wy oant no ta allinich oer typen hawwe praat sifers. Mar der binne ek arrays, strings... it soe bygelyks moai wêze om te beskriuwen dat dizze tekenrige mar in telefoannûmer befetsje kin, net?

Gjin fan dit is yn 'e TL. Mar d'r is bygelyks yn JSON Schema. En as immen oars kin argumearje oer de dielberens fan 512 KB, dat dit noch moat wurde kontrolearre yn koade, soargje dan derfoar dat de kliïnt gewoanwei koe net stjoer in oantal bûten berik 1..3000 (en de korrespondearjende flater koe net ûntstean) it soe mooglik west hawwe, toch? ..

Troch de wei, oer flaters en werom wearden. Sels dejingen dy't mei TL wurke hawwe, meitsje har eagen wazig - dat kaam ús net fuortendaliks troch elkenien in funksje yn TL kin eins werom net allinnich de beskreaune return type, mar ek in flater. Mar dit kin net op ien of oare manier ôflaat wurde mei de TL sels. Fansels is it al dúdlik en is d'r neat nedich yn 'e praktyk (hoewol yn feite, RPC kin dien wurde op ferskate manieren, wy komme hjir letter op werom) - mar hoe sit it mei de suverens fan 'e begripen fan wiskunde fan abstrakte typen fan 'e himelske wrâld?

En as lêste, hoe sit it mei lêsberens? No, dêr soe ik yn 't algemien wol graach wolle beskriuwing hawwe it rjocht yn it skema (yn it JSON-skema, wer, it is it), mar as jo der al ynspannen binne, hoe sit it dan mei de praktyske kant - teminsten triviaal te sjen nei ferskillen by updates? Sjoch sels by echte foarbylden:

-channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;
+channelFull#1c87a71a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;

of

-message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;
+message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;

It hinget fan elkenien ôf, mar GitHub, bygelyks, wegeret feroaringen yn sokke lange rigels te markearjen. It spultsje "fine 10 ferskillen", en wat it brein daliks sjocht is dat it begjin en de ein yn beide foarbylden binne itselde, jo moatte ferfeelsum lêze earne yn 'e midden ... Yn myn miening, dit is net allinnich yn teory, mar suver fisueel smoarch en sloppich.

Trouwens, oer de suverens fan 'e teory. Wêrom hawwe wy bitfjilden nedich? Liket it net dat se rûke min út it eachpunt fan type teory? De útlis kin sjoen wurde yn eardere ferzjes fan it diagram. Yn it earstoan, ja, sa wie it, foar elke snee waard in nij type makke. Dizze rudiminten besteane noch yn dizze foarm, bygelyks:

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;

Mar stel jo no foar, as jo 5 opsjonele fjilden yn jo struktuer hawwe, dan sille jo 32 soarten nedich hawwe foar alle mooglike opsjes. Kombinatoryske eksploazje. Sa, de kristal suverens fan 'e TL teory nochris ferpletterd tsjin' e getten izer ezel fan 'e hurde realiteit fan serialization.

Dêrnjonken skeine dizze jonges sels op guon plakken har eigen typology. Bygelyks, yn MTProto (folgjende haadstik) kin it antwurd wurde komprimearre troch Gzip, alles is goed - útsein dat de lagen en it circuit wurde skeind. Noch ien kear wie it net RpcResult sels dat waard rispje, mar de ynhâld. No, wêrom dit dwaan?.. Ik moast yn in kruk snije, sadat de kompresje oeral wurkje soe.

Of in oar foarbyld, wy ûntdutsen ienris in flater - it waard ferstjoerd InputPeerUser вместо InputUser. Of oarsom. Mar it wurke! Dat is, de tsjinner joech net oer it type. Hoe kin dit? It antwurd kin ús wurde jûn troch koadefragminten fan 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);

Mei oare wurden, dit is wêr serialisaasje wurdt dien MANUEL, net oanmakke koade! Miskien is de tsjinner op in fergelykbere wize ymplementearre?.. Yn prinsipe sil dit wurkje as ien kear dien wurdt, mar hoe kin it letter by fernijings stipe wurde? Is dit wêrom it skema útfûn is? En hjir geane wy ​​nei de folgjende fraach.

Ferzjefoarming. Lagen

Wêrom't de skematyske ferzjes lagen wurde neamd, kin allinich spekulearre wurde op basis fan 'e skiednis fan publisearre skema's. Blykber tochten de auteurs earst dat basis dingen koenen wurde dien mei it net feroare skema, en allinich wêr't nedich, foar spesifike oanfragen, jouwe oan dat se waarden dien mei in oare ferzje. Yn prinsipe, sels in goed idee - en it nije sil wêze, as it wie, "mingd", laach boppe op 'e âlde. Mar litte wy sjen hoe't it dien is. Wier, ik koe it net fan it begjin ôf sjen - it is grappich, mar it diagram fan 'e basislaach bestiet gewoan net. Lagen begon mei 2. De dokumintaasje fertelt ús oer in spesjale TL-funksje:

As in kliïnt Layer 2 stipet, dan moat de folgjende konstruktor brûkt wurde:

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

Yn 'e praktyk betsjut dit dat foar elke API-oprop in int mei de wearde 0x289dd1f6 moat tafoege wurde foar de metoade nûmer.

Klinkt normaal. Mar wat barde dêrnei? Doe ferskynde

invokeWithLayer3#b7475268 query:!X = X;

Dus wat is de folgjende? Sa't jo miskien riede,

invokeWithLayer4#dea0d430 query:!X = X;

Grappich? Nee, it is te betiid om te laitsjen, tink der oer nei elk in fersyk fan in oare laach moat ferpakt wurde yn sa'n spesjaal type - as se allegear oars binne foar jo, hoe kinne jo se dan oars ûnderskiede? En it tafoegjen fan mar 4 bytes foaroan is in frij effisjinte metoade. Sa,

invokeWithLayer5#417a57ae query:!X = X;

Mar it is dúdlik dat dit nei in skoftke in soarte fan bacchanalia wurde sil. En de oplossing kaam:

Update: Begjinnend mei Laach 9, helpmetoaden invokeWithLayerN kin brûkt wurde allinnich tegearre mei initConnection

Hoera! Nei 9 ferzjes kamen wy einlings ta wat der yn 'e jierren '80 yn ynternetprotokollen dien waard - ien kear oerienkomme oer de ferzje oan it begjin fan 'e ferbining!

Dus wat is de folgjende? ..

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

Mar no kinst noch laitsje. Pas nei nochris 9 lagen waard úteinlik in universele konstruktor mei in ferzjenûmer tafoege, dy't mar ien kear oan it begjin fan 'e ferbining moat wurde neamd, en de betsjutting fan' e lagen like ferdwûn te wêzen, no is it gewoan in betingste ferzje, lykas oeral oars. Probleem oplost.

Krekt?..

Vasily, [16.07.18 14:01] Sels op freed tocht ik:
De teleserver stjoert eveneminten sûnder fersyk. Fersiken moatte wurde ferpakt yn InvokeWithLayer. De tsjinner ferpakt gjin updates; d'r is gjin struktuer foar it ynpakken fan antwurden en updates.

Dy. de klant kin net oantsjutte de laach dêr't er wol updates

Vadim Goncharov, [16.07.18 14:02] is InvokeWithLayer yn prinsipe net in kruk?

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

Vadim Goncharov, [16.07.18 14:02] wat yn essinsje soe betsjutte moatte oerienkomme oer de laach oan it begjin fan 'e sesje

Trouwens, it folget dat klant downgrade wurdt net levere

Updates, d.w.s. type Updates yn it skema, dit is wat de tsjinner stjoert nei de kliïnt net as antwurd op in API fersyk, mar ûnôfhinklik as in evenemint bart. Dit is in kompleks ûnderwerp dat sil wurde besprutsen yn in oare post, mar foar no is it wichtich om te witten dat de tsjinner bewarret Updates sels as de klant is offline.

Sa, as jo wegerje te wrap fan elk pakket om syn ferzje oan te jaan, dit liedt logysk ta de folgjende mooglike problemen:

  • de tsjinner stjoert updates nei de klant noch foardat de klant hat ynformearre hokker ferzje it stipet
  • wat moat ik dwaan nei it opwurdearjen fan de klant?
  • wa garânsjesdat de tsjinner syn miening oer it laach nûmer sil net feroarje tidens it proses?

Tinksto dat dit suver teoretyske spekulaasjes is, en yn 'e praktyk kin dit net barre, om't de tsjinner goed skreaun is (op syn minst is it goed hifke)? Ha! Gjin saak hoe it is!

Dit is krekt wat wy rûnen yn augustus. Op 14 augustus wiene d'r berjochten dat der wat waard bywurke op 'e Telegram-tsjinners ... en dan yn' e logs:

2019-08-15 09:28:35.880640 MSK warn  main: ANON:87: unknown object type: 0x80d182d1 at TL/Object.pm line 213.
2019-08-15 09:28:35.751899 MSK warn  main: ANON:87: unknown object type: 0xb5223b0f at TL/Object.pm line 213.

en dan ferskate megabytes fan stapelspoaren (goed, tagelyk waard de logging fêststeld). Ommers, as iets net erkend wurdt yn jo TL, is it binêr troch hantekening, fierderop yn 'e line ALLE giet, dekodearjen sil wurden ûnmooglik. Wat moatte jo dwaan yn sa'n situaasje?

No, it earste ding dat elkenien yn 't sin komt is de ferbining los te meitsjen en nochris te besykjen. Hat net holpen. Wy googleje CRC32 - dit bliken objekten te wêzen fan skema 73, hoewol wy wurke oan 82. Wy sjogge foarsichtich nei de logs - der binne identifiers fan twa ferskillende skema's!

Miskien is it probleem suver yn ús net-offisjele klant? Nee, wy starte Telegram Desktop 1.2.17 (ferzje levere yn in oantal Linux-distribúsjes), it skriuwt nei it útsûnderingslog: MTP Unexpected type id #b5223b0f lêzen yn MTPMessageMedia ...

Krityk op it protokol en organisatoaryske oanpak fan Telegram. Diel 1, technysk: ûnderfining fan it skriuwen fan in klant fanôf it begjin - TL, MT

Google liet sjen dat in ferlykber probleem al bard wie mei ien fan 'e net-offisjele kliïnten, mar doe wiene de ferzjenûmers en, dus, de oannames oars ...

Dus wat moatte wy dwaan? Vasily en ik ferdiele: hy besocht it circuit te aktualisearjen nei 91, ik besleat in pear dagen te wachtsjen en te besykjen op 73. Beide metoaden wurken, mar om't se empirysk binne, is der gjin begryp fan hoefolle ferzjes omheech of omleech jo nedich hawwe om te springen, of hoe lang moatte jo wachtsje.

Letter koe ik de situaasje reprodusearje: wy lansearje de kliïnt, skeakelje it út, kompilearje it circuit opnij nei in oare laach, opnij starte, it probleem opnij fange, werom nei de foarige - oeps, gjin hoemannichte sirkwywikseling en client opnij starte foar in pear minuten sil helpe. Jo krije in miks fan gegevensstruktueren út ferskate lagen.

Ferklearring? Lykas jo kinne riede fan ferskate yndirekte symptomen, bestiet de tsjinner út in protte prosessen fan ferskate soarten op ferskate masines. Meast wierskynlik, de tsjinner dy't ferantwurdlik is foar "bufferjen" sette yn 'e wachtrige wat syn superieuren it joegen, en se joegen it yn it skema dat yn plak wie op it momint fan generaasje. En oant dizze wachtrige "rotte" wie, koe der neat oan dien wurde.

Miskien... mar dit is in skriklike kruk?!.. Nee, foardat wy tinke oer gekke ideeën, litte wy nei de koade fan 'e offisjele kliïnten sjen. Yn 'e Android-ferzje fine wy ​​gjin TL-parser, mar wy fine in fikse triem (GitHub wegeret it oan te reitsjen) mei (de)serialisaasje. Hjir binne de koade snippets:

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

of

    boolean fixCaption = !TextUtils.isEmpty(message) &&
    (media instanceof TLRPC.TL_messageMediaPhoto_old ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer68 ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer74 ||
     media instanceof TLRPC.TL_messageMediaDocument_old ||
     media instanceof TLRPC.TL_messageMediaDocument_layer68 ||
     media instanceof TLRPC.TL_messageMediaDocument_layer74)
    && message.startsWith("-1");

Hmm... sjocht der wyld út. Mar, wierskynlik, dit is oanmakke koade, dan goed? .. Mar it stipet grif alle ferzjes! Wier, it is net dúdlik wêrom't alles meiinoar mingd is, geheime petearen, en allerhanne _old7 ien of oare manier net lykje masine generaasje ... Lykwols, it measte fan alle ik waard blaasd fuort troch

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Jongens, kinne jo net iens beslute wat der binnen ien laach sit?! No, goed, lit ús sizze "twa" waarden frijlitten mei in flater, no, it bart, mar TREJE?.. Daliks wer deselde rake? Wat foar pornografy is dit, sorry?

Yn 'e boarnekoade fan Telegram Desktop, trouwens, bart in ferlykber ding - as dat sa is, feroarje ferskate commits op in rige oan it skema har laachnûmer net, mar reparearje wat. Yn betingsten wêr't gjin offisjele boarne fan gegevens foar it skema is, wêr kin it fan krije, útsein de boarnekoade fan 'e offisjele kliïnt? En as jo it dêrwei nimme, kinne jo der net wis fan wêze dat it skema folslein korrekt is oant jo alle metoaden testen.

Hoe kin dit sels wurde hifke? Ik hoopje dat fans fan ienheid, funksjonele en oare tests sille diele yn 'e kommentaren.

Okee, litte wy nei in oar stikje koade sjen:

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;

Dizze opmerking "mei de hân oanmakke" suggerearret dat mar in part fan dizze triem is skreaun mei de hân (kinne jo yntinke de hiele ûnderhâld nachtmerje?), En de rest wie masine-generearre. Mar dan komt der in oare fraach op - dat de boarnen beskikber binne net folslein (a la GPL blobs yn de Linux kernel), mar dit is al in ûnderwerp foar it twadde diel.

Mar genôch. Litte wy trochgean nei it protokol wêrop al dizze serialisaasje rint.

MT Proto

Dus, litte wy iepenje Algemiene beskriuwing и detaillearre beskriuwing fan it protokol en it earste dat wy stroffelje oer is de terminology. En mei in oerfloed fan alles. Yn 't algemien liket dit in proprietêre funksje fan Telegram te wêzen - dingen oars op ferskate plakken neame, of ferskate dingen mei ien wurd, of oarsom (bygelyks yn in API op heech nivo, as jo in stickerpakket sjogge, is it net wat jo tocht).

Bygelyks, "berjocht" en "sesje" betsjutte hjir wat oars as yn 'e gewoane Telegram-kliïnt-ynterface. No, alles is dúdlik mei it berjocht, it koe wurde ynterpretearre yn OOP-termen, of gewoan it wurd "pakket" neamd - dit is in leech, ferfiernivo, d'r binne net deselde berjochten as yn 'e ynterface, d'r binne in protte tsjinstberjochten . Mar de sesje ... mar earst dingen earst.

ferfier laach

It earste ding is ferfier. Se sille ús fertelle oer 5 opsjes:

  • TCP
  • Websocket
  • Websocket oer HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] D'r is ek UDP-ferfier, mar it is net dokumintearre

En TCP yn trije farianten

De earste is gelyk oan UDP oer TCP, elk pakket omfettet in folchoardernûmer en crc
Wêrom is it lêzen fan dokuminten op in karre sa pynlik?

No, dêr is it no TCP al yn 4 farianten:

  • Ofbrutsen
  • Intermediate
  • Padded tuskenlizzende
  • Fol

No, ok, Padded tuskenlizzende foar MTProxy, dit waard letter tafoege fanwege bekende eveneminten. Mar wêrom twa mear ferzjes (trije yn totaal) as jo koenen krije troch mei ien? Alle fjouwer ferskille yn essinsje allinich yn hoe't jo de lingte en lading fan 'e haad MTProto kinne ynstelle, dy't fierder sille wurde besprutsen:

  • yn Abridged it is 1 of 4 bytes, mar net 0xef, dan it lichem
  • yn Intermediate dit is 4 bytes fan lingte en in fjild, en de earste kear dat de kliïnt moat stjoere 0xeeeeeeee om oan te jaan dat it Intermediate is
  • yn Folslein de meast ferslaavjend, út it eachpunt fan in netwurker: lingte, folchoarder nûmer, en NET DE IENE dat is benammen MTProto, lichem, CRC32. Ja, dit alles is boppe op TCP. Wat ús betrouber ferfier leveret yn 'e foarm fan in sekwinsjele bytestream; gjin sekwinsjes binne nedich, foaral kontrôlesummen. Okee, no sil immen tsjin my beswier meitsje dat TCP in 16-bit kontrôlesum hat, dus datakorrupsje bart. Geweldich, mar wy hawwe eins in kryptografysk protokol mei hashes langer dan 16 bytes, al dizze flaters - en noch mear - sille wurde fongen troch in SHA-mismatch op in heger nivo. D'r is GEEN punt yn CRC32 boppe op dit.

Litte wy Abridged fergelykje, wêryn ien byte fan lingte mooglik is, mei Intermediate, dy't rjochtfeardiget "In gefal dat 4-byte data-ôfstimming nedich is," wat nochal ûnsin is. Wat, it wurdt leaud dat Telegram-programmeurs sa ynkompetint binne dat se gegevens net kinne lêze fan in socket yn in ôfstimd buffer? Jo moatte dit noch dwaan, want it lêzen kin jo elk oantal bytes werombringe (en d'r binne bygelyks ek proxy-tsjinners...). Of oan 'e oare kant, wêrom Abridged blokkearje as wy noch flinke padding sille hawwe boppe op 16 bytes - bewarje 3 bytes soms ?

Men krijt de yndruk dat Nikolai Durov wirklik graach tsjillen opnij útfine, ynklusyf netwurkprotokollen, sûnder echte praktyske need.

Oare ferfier opsjes, incl. Web en MTProxy sille wy no net beskôgje, miskien yn in oare post, as der in fersyk is. Oer deselde MTProxy, lit ús no allinich ûnthâlde dat koart nei syn frijlitting yn 2018, providers gau learden om it te blokkearjen, bedoeld foar bypass blockingtroch pakket grutte! En ek it feit dat de MTProxy-tsjinner skreaun (wer troch Waltman) yn C te folle bûn wie oan Linux-spesifisiteit, hoewol dit hielendal net fereaske wie (Phil Kulin sil befêstigje), en dat in ferlykbere server yn Go of Node.js soe passe yn minder as hûndert rigels.

Mar wy sille konklúzjes lûke oer de technyske geletterdheid fan dizze minsken oan 'e ein fan' e seksje, nei it besjen fan oare problemen. Litte wy no trochgean nei OSI-laach 5, sesje - wêrop se MTProto-sesje pleatsten.

Kaaien, berjochten, sesjes, Diffie-Hellman

Se pleatste it dêr net hielendal goed... In sesje is net deselde sesje dy't sichtber is yn de ynterface ûnder Aktive sesjes. Mar yn oarder.

Krityk op it protokol en organisatoaryske oanpak fan Telegram. Diel 1, technysk: ûnderfining fan it skriuwen fan in klant fanôf it begjin - TL, MT

Sa krigen wy in bytestring fan bekende lingte fan 'e transportlaach. Dit is of in fersifere berjocht of platte tekst - as wy noch yn it poadium fan 'e kaaioerienkomst binne en it eins dogge. Hokker fan 'e bondel begripen neamd "kaai" hawwe wy it oer? Litte wy dit probleem ferdúdlikje foar it Telegram-team sels (ik ferûntskuldigje my foar it oersetten fan myn eigen dokumintaasje út it Ingelsk mei in wurch brein om 4 oere, it wie makliker om guon útdrukkingen sa't se binne te litten):

D'r binne twa entiteiten neamd sitting - ien yn 'e UI fan offisjele kliïnten ûnder "aktuele sesjes", wêr't elke sesje oerienkomt mei in folslein apparaat / OS.
De twadde is MTProto sesje, dêr't it folchoardernûmer fan it berjocht (yn in leech-nivo sin) yn stiet, en hokker kin duorje tusken ferskate TCP-ferbiningen. Ferskate MTProto-sesjes kinne tagelyk wurde ynstalleare, bygelyks om it downloaden fan bestân te fersnellen.

Tusken dizze twa sesjes der is in konsept machtiging. Yn it degenerearre gefal kinne wy ​​dat sizze UI sesje is itselde as machtiging, mar och, alles is yngewikkeld. Litte wy sjen:

  • De brûker op it nije apparaat genereart earst auth_key en bound it to account, bygelyks fia SMS - dat is wêrom machtiging
  • It barde binnen de earste MTProto sesje, dy't hat session_id yn dysels.
  • Op dizze stap, de kombinaasje machtiging и session_id neamd wurde koe autoriteit - dit wurd ferskynt yn 'e dokumintaasje en koade fan guon kliïnten
  • Dan kin de klant iepenje ferskate MTProto sesjes ûnder itselde auth_key - oan deselde DC.
  • Dan, op in dei sil de klant it bestân oanfreegje moatte fan oar dc - en foar dizze DC sil in nije generearre wurde auth_key !
  • Om it systeem te ynformearjen dat it net in nije brûker is dy't registrearret, mar itselde machtiging (UI sesje), brûkt de kliïnt API-oproppen auth.exportAuthorization yn hûs DC auth.importAuthorization yn de nije DC.
  • Alles is itselde, ferskate kinne iepen wêze MTProto sesjes (elk mei syn eigen session_id) nei dizze nije DC, ûnder syn auth_key.
  • Uteinlik kin de klant Perfect Forward Secrecy wolle. Elk auth_key wie permanint kaai ​​- per DC - en de klant kin belje auth.bindTempAuthKey foar gebrûk tydlik auth_key - en wer, mar ien temp_auth_key per DC, mienskiplik foar alle MTProto sesjes oan dizze DC.

Tink derom sâlt (en takomstige sâlten) is ek ien op auth_key dy. dield tusken elkenien MTProto sesjes oan deselde DC.

Wat betsjut "tusken ferskate TCP-ferbiningen"? Dat betsjut dus sokssawat as autorisaasjekoekje op in webside - it bliuwt (oerlibbet) in protte TCP-ferbiningen nei in opjûne tsjinner, mar op in dei giet it min. Allinnich oars as HTTP, yn MTProto wurde berjochten binnen in sesje sequentieel nûmere en befêstige; as se yn 'e tunnel kamen, waard de ferbining ferbrutsen - nei it oprjochtsjen fan in nije ferbining sil de tsjinner freonlik alles stjoere yn dizze sesje dat it net levere yn 'e foarige TCP ferbining.

De boppesteande ynformaasje wurdt lykwols gearfette nei in protte moannen fan ûndersyk. Yn 'e tuskentiid implementearje wy ús klant fanôf it begjin? - lit ús weromgean nei it begjin.

Dus litte wy generearje auth_key on Diffie-Hellman ferzjes fan Telegram. Litte wy besykje de dokumintaasje te begripen ...

Vasily, [19.06.18 20:05] data_with_hash := SHA1 (data) + data + (elke willekeurige bytes); sa dat de lingte gelyk is oan 255 bytes;
fersifere_data:= RSA(data_mei_hash, server_public_key); in 255-byte lang getal (grutte endian) wurdt ferhege nei de nedige macht oer de fereaske modulus, en it resultaat wurdt opslein as in 256-byte getal.

Se hawwe wat dope DH

It liket net op de DH fan in sûne persoan
D'r binne gjin twa iepenbiere kaaien yn dx

No ja, dit waard úteinlik útsocht, mar der bleau in oerbliuwsel oer - bewiis fan wurk wurdt dien troch de opdrachtjouwer dat hy it oantal faktorearje koe. Soart beskerming tsjin DoS-oanfallen. En de RSA-kaai wurdt mar ien kear brûkt yn ien rjochting, yn essinsje foar fersifering new_nonce. Mar wylst dizze skynber ienfâldige operaasje sil slagje, wat sille jo te krijen hawwe?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Ik bin noch net oan it appid fersyk kommen

Ik stjoerde dit fersyk nei DH

En yn it transportdok stiet dat it kin reagearje mei 4 bytes fan in flaterkoade. Da's alles

No, hy fertelde my -404, wat dan?

Dat ik fertelde him: "Fang jo bullshit fersifere mei in serverkaai mei in fingerprint lykas dizze, ik wol DH," en it antwurde mei in domme 404

Wat soene jo tinke fan dit serverantwurd? Wat te dwaan? Der is gjinien te freegjen (mar dêroer yn it twadde diel mear).

Hjir wurdt alle belangstelling dien op it dok

Ik haw neat oars te dwaan, ik dreamde gewoan fan it konvertearjen fan nûmers hinne en wer

Twa 32 bit nûmers. Ik pakte se lykas alle oaren

Mar nee, dizze twa moatte earst oan de rigel tafoege wurde as BE

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

Vasily, [20.06.18 15:49] JA!

Vadim Goncharov, [20.06.18 15:50] dus ik begryp net wat hy kin "net fûn"

Vasily, [20.06.18 15:50] sawat

Ik koe sa'n ûntbining yn prime faktoaren net fine%)

Wy hawwe net iens beheare flatermelding

Vasily, [20.06.18 20:18] Och, d'r is ek MD5. Al trije ferskillende hashes

De kaaifingerprint wurdt as folget berekkene:

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

SHA1 en Sha2

Sa litte wy it sizze auth_key wy krigen 2048 bits yn grutte mei help fan Diffie-Hellman. Wat komt hjirnei? Folgjende ûntdekke wy dat de legere 1024 bits fan dizze kaai net op ien of oare manier brûkt wurde ... mar litte wy dit no tinke. Op dizze stap hawwe wy in dield geheim mei de tsjinner. In analoog fan 'e TLS-sesje is fêststeld, wat in heul djoere proseduere is. Mar de tsjinner wit noch neat oer wa't wy binne! Noch net, eins. machtiging. Dy. as jo tocht yn termen fan "oanmelde-wachtwurd", lykas jo ienris dien hawwe yn ICQ, of op syn minst "oanmelde-kaai", lykas yn SSH (bygelyks op guon gitlab / github). Wy krigen in anonime. Wat as de tsjinner fertelt ús "dizze telefoannûmers wurde betsjinne troch in oare DC"? Of sels "jo telefoannûmer is ferbean"? It bêste dat wy kinne dwaan is de kaai te hâlden yn 'e hope dat it nuttich sil wêze en dan net ferrot wurde sil.

Troch de wei, wy "ûntfongen" it mei reservearrings. Bygelyks, fertrouwe wy de tsjinner? Wat as it fake is? Kryptografyske kontrôles soene nedich wêze:

Vasily, [21.06.18 17:53] Se biede mobile kliïnten oan om in 2kbit nûmer te kontrolearjen op primaliteit%)

Mar it is hielendal net dúdlik, nafeijoa

Vasily, [21.06.18 18:02] It dokumint seit net wat te dwaan as it blykt te wêzen net ienfâldich

Net sein. Litte wy sjen wat de offisjele Android-kliïnt yn dit gefal docht? IN dat is wat (en ja, it hiele bestân is nijsgjirrich) - sa't se sizze, lit ik dit hjir mar litte:

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

Nee, it is der fansels noch guon Der binne toetsen foar de oerheid fan in getal, mar persoanlik haw ik net mear genôch kennis fan de wiskunde.

Okee, wy hawwe de masterkaai. Om yn te loggen, d.w.s. fersiken ferstjoere, moatte jo fierdere fersifering útfiere, mei AES.

De berjochtkaai wurdt definiearre as de 128 middelste bits fan 'e SHA256 fan it berjochtlichem (ynklusyf sesje, berjocht-ID, ensfh.), ynklusyf de padding-bytes, foarôfgeand troch 32 bytes nommen fan 'e autorisaasjekaai.

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

Untfongen auth_key. Alle. Beyond harren ... it is net dúdlik út it dokumint. Fiel jo frij om de iepen boarne koade te studearjen.

Tink derom dat MTProto 2.0 fereasket fan 12 oant 1024 bytes fan padding, noch ûnder betingst dat de resultearjende berjochtlingte dielber is troch 16 bytes.

Dus hoefolle padding moatte jo tafoegje?

En ja, der is ek in 404 yn gefal fan in flater

As immen it diagram en de tekst fan 'e dokumintaasje soarchfâldich studearre, seach se dat d'r gjin MAC is. En dat AES wurdt brûkt yn in bepaalde IGE modus dat wurdt net brûkt earne oars. Se skriuwe hjir fansels oer yn har FAQ... Hjir, lykas, de berjochtkaai sels is ek de SHA-hash fan 'e ûntsifere gegevens, brûkt om de yntegriteit te kontrolearjen - en yn gefal fan in mismatch, de dokumintaasje om ien of oare reden advisearret se stil te negearjen (mar hoe sit it mei feiligens, wat as se ús brekke?).

Ik bin gjin kryptograaf, miskien is der neat mis mei dizze modus yn dit gefal út in teoretysk eachpunt. Mar ik kin dúdlik in praktysk probleem neame, mei Telegram Desktop as foarbyld. It fersiferet de lokale cache (al dizze D877F783D5D3EF8C) op deselde wize as berjochten yn MTProto (allinich yn dit gefal ferzje 1.0), d.w.s. earst de berjochtkaai, dan de gegevens sels (en earne ôfsjoen fan de wichtichste grutte auth_key 256 bytes, sûnder hokker msg_key nutteloos). Dat, it probleem wurdt merkber op grutte bestannen. Jo moatte nammentlik twa kopyen fan 'e gegevens hâlde - fersifere en ûntsifere. En as der megabytes, of streaming fideo, bygelyks? Mar mei MTProto sille jo moatte earst fersiferje of ûntsiferje it hiele berjocht, pas dan oer nei it netwurk of op skiif. Dêrom, yn 'e lêste ferzjes fan Telegram Desktop yn' e cache yn user_data In oar formaat wurdt ek brûkt - mei AES yn CTR-modus.

Vasily, [21.06.18 01:27] Oh, ik fûn út wat IGE is: IGE wie de earste poging ta in "authentisearjende fersiferingsmodus," oarspronklik foar Kerberos. It wie in mislearre besykjen (it jout gjin yntegriteit beskerming), en moast fuortsmiten wurde. Dat wie it begjin fan in 20 jier syktocht nei in autentisearjende fersiferingsmodus dy't wurket, dy't koartlyn kulminearre yn modi lykas OCB en GCM.

En no de arguminten fan 'e karrekant:

It team efter Telegram, ûnder lieding fan Nikolai Durov, bestiet út seis ACM-kampioenen, de helte fan harren Ph.D's yn wiskunde. It duorre har sawat twa jier om de hjoeddeistige ferzje fan MTProto út te rollen.

Dat is grappich. Twa jier op it legere nivo

Of jo koene gewoan tls nimme

Okee, lit ús sizze dat wy de fersifering en oare nuânses hawwe dien. Is it einlings mooglik om fersiken te stjoeren serialisearre yn TL en de antwurden te deserialisearjen? Dus wat en hoe moatte jo stjoere? Hjir, lit ús sizze, de metoade initConnection, miskien is dit it?

Vasily, [25.06.18 18:46] Inisjalisearret ferbining en bewarje ynformaasje oer it apparaat en de applikaasje fan de brûker.

It akseptearret app_id, device_model, system_version, app_version en lang_code.

En wat fraach

Dokumintaasje lykas altyd. Fiel jo frij om de iepen boarne te studearjen

As alles sawat dúdlik wie mei invokeWithLayer, wat is hjir dan mis? It docht bliken, lit ús sizze dat wy hawwe - de kliïnt hie al wat om de tsjinner oer te freegjen - d'r in fersyk is dat wy stjoere woene:

Vasily, [25.06.18 19:13] Neffens de koade is de earste oprop ferpakt yn dizze stront, en de stront sels is ferpakt yn invokewithlayer

Wêrom koe initConnection gjin aparte oprop wêze, mar moat in wrapper wêze? Ja, sa die bliken, moat it elke kear dien wurde oan it begjin fan elke sesje, en net ien kear, lykas mei de haadkaai. Mar! It kin net neamd wurde troch in net autorisearre brûker! No hawwe wy it stadium berikt wêr't it fan tapassing is Dizze dokumintaasje side - en it fertelt ús dat ...

Allinich in lyts part fan 'e API-metoaden is beskikber foar net autorisearre brûkers:

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

De alderearste fan harren, auth.sendCode, en der is dat koestere earste fersyk wêryn wy stjoere api_id en api_hash, en wêrnei't wy krije in SMS mei in koade. En as wy yn 'e ferkearde DC binne (tillefoannûmers yn dit lân wurde bygelyks troch in oar betsjinne), dan krije wy in flater mei it nûmer fan' e winske DC. Om út te finen hokker IP-adres troch DC-nûmer jo moatte ferbine mei, help ús help.getConfig. Op in stuit wiene d'r mar 5 ynstjoerings, mar nei de ferneamde eveneminten fan 2018 is it oantal signifikant tanommen.

Litte wy no betinke dat wy anonym op dit poadium kamen op 'e server. Is it net te djoer om gewoan in IP-adres te krijen? Wêrom net dwaan dit, en oare operaasjes, yn it net fersifere diel fan MTProto? Ik hear it beswier: "hoe kinne wy ​​der wis fan wêze dat it net RKN is dy't reagearret mei falske adressen?" Om dit wy ûnthâlde dat, yn it algemien, offisjele kliïnten RSA-kaaien binne ynbêde, d.w.s. kinne jo gewoan abonnearje dizze ynformaasje. Eins wurdt dit al dien foar ynformaasje oer it omgean fan blokkearjen dy't kliïnten krije fia oare kanalen (logysk kin dit net dien wurde yn MTProto sels; jo moatte ek witte wêr't jo ferbine moatte).

OK. Op dit stadium fan klantautorisaasje binne wy ​​noch net autorisearre en hawwe ús applikaasje net registrearre. Wy wolle gewoan foar no sjen wat de tsjinner reagearret op metoaden dy't beskikber binne foar in net autorisearre brûker. En hjir…

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;

Yn it skema komt de earste twadde

Yn it tdesktop-skema is de tredde wearde

Ja, sûnt dy tiid is de dokumintaasje fansels bywurke. Hoewol't it meikoarten wer irrelevant wurde kin. Hoe moat in begjinnende ûntwikkelder witte? Miskien as jo jo oanfraach registrearje, sille se jo ynformearje? Vasily die dit, mar helaas, se stjoerde him neat (wer, wy sille prate oer dit yn it twadde diel).

...Jo hawwe opfallen dat wy al op ien of oare manier nei de API binne ferhuze, d.w.s. nei it folgjende nivo, en miste wat yn it MTProto-ûnderwerp? Gjin ferrassing:

Vasily, [28.06.18 02:04] Mm, se rommelje troch guon fan 'e algoritmen op e2e

Mtproto definiearret fersiferingsalgoritmen en kaaien foar beide domeinen, lykas ek in bytsje in wrapperstruktuer

Mar se mingje konstant ferskate nivo's fan 'e stapel, dus it is net altyd dúdlik wêr't mtproto einige en it folgjende nivo begon

Hoe mingje se? No, hjir is deselde tydlike kaai foar PFS, bygelyks (troch de manier, Telegram Desktop kin it net dwaan). It wurdt útfierd troch in API-fersyk auth.bindTempAuthKey, d.w.s. fan it boppeste nivo. Mar tagelyk ynterfereart it mei fersifering op it legere nivo - dêrnei moatte jo it bygelyks nochris dwaan initConnection ensfh., dit is net just normaal fersyk. Wat ek spesjaal is dat jo mar ien tydlike kaai hawwe kinne per DC, hoewol it fjild auth_key_id yn elk berjocht kinne jo de kaai feroarje op syn minst elk berjocht, en dat de tsjinner hat it rjocht om "ferjitte" de tydlike kaai op elk momint - de dokumintaasje seit net wat te dwaan yn dit gefal ... goed, wêrom koe 't hawwe jo ferskate kaaien, lykas mei in set fan takomstige sâlten, en ?..

D'r binne in pear oare dingen dy't it wurdich binne op te merken oer it MTProto-tema.

Berjochtberjochten, msg_id, msg_seqno, befêstigings, pings yn 'e ferkearde rjochting en oare eigensinnigens

Wêrom moatte jo witte oer harren? Om't se "lekken" nei in heger nivo, en jo moatte har bewust wêze as jo wurkje mei de API. Litte wy oannimme dat wy net ynteressearre binne yn msg_key; it legere nivo hat alles foar ús ûntsifere. Mar binnen de dekodearre gegevens hawwe wy de folgjende fjilden (ek de lingte fan 'e gegevens, dus wy witte wêr't de padding is, mar dat is net wichtich):

  • sâlt - int64
  • session_id - int64
  • message_id - int64
  • seq_no - int32

Lit ús jo herinnerje dat d'r mar ien sâlt is foar de heule DC. Wêrom witte oer har? Net allinnich omdat der in fersyk is get_future_salts, dy't jo fertelt hokker yntervallen jildich sille wêze, mar ek om't as jo sâlt "rot" is, dan sil it berjocht (fersyk) gewoan ferlern gean. De tsjinner sil, fansels, rapportearje it nije sâlt troch it útjaan new_session_created - mar mei de âlde sille jo it bygelyks op ien of oare manier opnij ferstjoere moatte. En dit probleem hat ynfloed op de applikaasje-arsjitektuer.

De tsjinner is tastien om sesjes hielendal ôf te fallen en op dizze manier om in protte redenen te reagearjen. Eigentlik, wat is in MTProto-sesje fan 'e kliïntkant? Dit binne twa nûmers session_id и seq_no berjochten binnen dizze sesje. No, en de ûnderlizzende TCP-ferbining, fansels. Litte wy sizze dat ús kliïnt noch altyd net wit hoe't hy in protte dingen moat dwaan, hy hat de ferbining losmakke en opnij ferbûn. As dit rap barde - de âlde sesje gie troch yn 'e nije TCP-ferbining, ferheegje seq_no fierder. As it lang duorret, kin de tsjinner it wiskje, want oan 'e kant is it ek in wachtrige, lykas wy fûnen.

Wat moat it wêze seq_no? Oh, dat is in lestige fraach. Besykje earlik te begripen wat wie bedoeld:

Ynhâld-relatearre berjocht

In berjocht dat in eksplisite erkenning fereasket. Dizze omfetsje alle brûkers- en in protte tsjinstberjochten, frijwol allegear mei útsûndering fan konteners en erkenningen.

Berjocht folchoarder nûmer (msg_seqno)

In 32-bit nûmer gelyk oan twa kear it oantal "ynhâld-relatearre" berjochten (dyjingen dy't befestiging nedich binne, en benammen dyjingen dy't gjin konteners binne) makke troch de stjoerder foarôfgeand oan dit berjocht en dêrnei ferhege mei ien as it hjoeddeistige berjocht in ynhâld-relatearre berjocht. In kontener wurdt altyd generearre nei syn hiele ynhâld; dêrom, syn folchoarder nûmer is grutter as of gelyk oan de folchoarder nûmers fan de berjochten befette yn it.

Wat foar sirkus is dit mei in tanimming fan 1, en dan in oar mei 2? .. Ik tink dat se yn 't earstoan "de minste signifikante bit foar ACK betsjutte, de rest is in nûmer", mar it resultaat is net hielendal itselde - benammen, it komt út, kin ferstjoerd wurde ferskate befêstigings hawwende itselde seq_no! Hoe? No, bygelyks, de tsjinner stjoert ús wat, stjoert it, en wy sels bliuwe stil, allinich reagearje mei tsjinstberjochten dy't de ûntfangst fan har berjochten befêstigje. Yn dit gefal sille ús útgeande befêstigings itselde útgeande nûmer hawwe. As jo ​​binne bekend mei TCP en tocht dat dit klinkt ien of oare wize wyld, mar it liket net hiel wyld, want yn TCP seq_no feroaret net, mar befêstiging giet nei seq_no oan 'e oare kant, ik sil hastich om dy opstekke. Befêstigings wurde levere yn MTProto NOT on seq_no, lykas yn TCP, mar troch msg_id !

Wat is dit msg_id, de wichtichste fan dizze fjilden? In unike berjochtidentifikaasje, lykas de namme al fermoeden docht. It wurdt definiearre as in 64-bit getal, wêrfan de leechste bits wer de magy "tsjinner-net-tsjinner" hawwe, en de rest is in Unix-tiidstempel, ynklusyf it fraksjonele diel, ferskood 32 bits nei lofts. Dy. timestamp per se (en berjochten mei tiden dy't te folle ferskille sille wurde ôfwiisd troch de tsjinner). Dêrút docht bliken dat dit yn 't algemien in identifier is dy't globaal is foar de klant. Sjoen dat - lit ús ûnthâlde session_id - wy binne garandearre: Under gjin omstannichheden kin in berjocht bedoeld foar ien sesje nei in oare sesje stjoerd wurde. Dat is, it docht bliken dat der al is trije nivo - sesje, sesjenûmer, berjocht-id. Wêrom sa'n oerkomplikaasje, dit mystearje is heul grut.

En sa, msg_id nedich foar...

RPC: fersiken, antwurden, flaters. Befêstigings.

As jo ​​miskien hawwe opfallen, der is gjin spesjale "meitsje in RPC fersyk" type of funksje oeral yn it diagram, hoewol't der binne antwurden. Ommers, wy hawwe ynhâld-relatearre berjochten! Dat is, eltse it berjocht kin in fersyk wêze! Of net te wêzen. Einsluten, fan elk is msg_id. Mar d'r binne antwurden:

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

Hjir wurdt oanjûn op hokker berjocht dit in reaksje is. Dêrom sille jo op it boppeste nivo fan 'e API moatte ûnthâlde wat it nûmer fan jo oanfraach wie - ik tink dat d'r gjin ferlet is om út te lizzen dat it wurk asyngroan is, en d'r kinne tagelyk ferskate oanfragen yngean, de antwurden wêrop kin wurde weromjûn yn eltse folchoarder? Yn prinsipe, út dit en flater berjochten lykas gjin arbeiders, de arsjitektuer efter dit kin wurde traceed: de tsjinner dy't ûnderhâldt in TCP ferbining mei jo is in front-end balancer, it stjoert fersiken nei de backends en sammelet se werom fia message_id. It liket derop dat alles hjir dúdlik, logysk en goed is.

Ja?.. En as jo der oer tinke? Ommers, it RPC antwurd sels hat ek in fjild msg_id! Moatte wy tsjin de tsjinner roppe "jo antwurde myn antwurd net!"? En ja, wat wie der oer befêstigings? Oer side berjochten oer berjochten fertelt ús wat is

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

en it moat dien wurde troch eltse kant. Mar net altyd! As jo ​​​​in RpcResult hawwe krigen, tsjinnet it sels as befêstiging. Dat is, de tsjinner kin reagearje op jo fersyk mei MsgsAck - lykas, "Ik haw it ûntfongen." RpcResult kin fuortendaliks reagearje. It kin beide wêze.

En ja, jo moatte it antwurd noch beäntwurdzje! Befêstiging. Oars sil de tsjinner it as net te leverjen beskôgje en it wer nei jo stjoere. Ek nei reconnection. Mar hjir komt fansels de kwestje fan timeouts op. Litte wy se letter efkes sjen.

Litte wy yn 'e tuskentiid sjen nei mooglike query-útfierflaters.

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

Och, sil immen útroppe, hjir is in minskliker formaat - der is in rigel! Nim de tiid. Hjir list fan flaters, mar fansels net folslein. Dêrút leare wy dat de koade is sokssawat as HTTP-flaters (goed, fansels, de semantyk fan 'e antwurden wurdt net respektearre, op guon plakken wurde se willekeurich ferdield ûnder de koades), en de line liket as CAPITAL_LETTERS_AND_NUMBERS. Bygelyks PHONE_NUMBER_OCCUPIED of FILE_PART_Х_MISSING. No, dat is, jo sille dizze line noch nedich hawwe parse. Bygelyks FLOOD_WAIT_3600 sil betsjutte dat jo moatte wachtsje in oere, en PHONE_MIGRATE_5, dat in telefoannûmer mei dit foarheaksel registrearre wurde moat yn it 5e DC. Wy hawwe in type taal, krekt? Wy hoege gjin argumint fan in tekenrige, reguliere sil dwaan, okee.

Nochris, dit is net op 'e side fan' e tsjinstberjochten, mar, lykas al gewoanlik is mei dit projekt, is de ynformaasje te finen op in oare dokumintaasje side. Or cast fertinking. Earst, sjoch, typen / laach oertreding - RpcError kin ynsletten wurde RpcResult. Wêrom net bûten? Wat hawwe wy net rekken holden? .. Accordingly, wêr is de garânsje dat RpcError meie NET ynbêde wurde RpcResult, mar wurde direkt of nêst yn in oar type? .. En as it kin net, wêrom is it net op it boppeste nivo, d.w.s. it mist req_msg_id ? ..

Mar litte wy trochgean oer tsjinstberjochten. De kliïnt kin tinke dat de tsjinner in lange tiid tinkt en dit prachtige fersyk docht:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

D'r binne trije mooglike antwurden op dizze fraach, wer krusend mei it befêstigingsmeganisme; besykje te begripen wat se moatte wêze (en wat de algemiene list fan soarten dy't gjin befestiging nedich binne) wurdt oerlitten oan de lêzer as húswurk (notysje: de ynformaasje yn de boarnekoade fan Telegram Desktop is net kompleet).

Drugsferslaving: berjochtstatussen

Yn 't algemien litte in protte plakken yn TL, MTProto en Telegram yn' t algemien in gefoel fan eigensinnigens litte, mar út beleefdheid, takt en oaren soft skills Wy hawwe der hoflik oer swijen, en de obsceniteiten yn de dialogen sensurearre. Lykwols, dit plakОit grutste part fan 'e side giet oer berjochten oer berjochten It is sels skokkend foar my, dy’t al lang mei netwurkprotokollen wurket en fytsen sjoen hat fan ferskillende graad fan krom.

It begjint ûnskuldich, mei befêstigings. Folgjende se fertelle ús oer

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;

No, elkenien dy't begjint te wurkjen mei MTProto sil mei har omgean moatte; yn 'e syklus "korrigearre - opnij kompilearre - lansearre" is it krijen fan nûmerflaters as sâlt dat slagge is om min te gean tidens bewurkingen in gewoan ding. D'r binne lykwols twa punten hjir:

  1. Dit betsjut dat it orizjinele berjocht ferlern is. Wy moatte wat wachtrijen meitsje, dêr sille wy letter nei sjen.
  2. Wat binne dizze frjemde flaternûmers? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... wêr binne de oare nûmers, Tommy?

De dokumintaasje stelt:

De bedoeling is dat error_code-wearden wurde groepeare (error_code >> 4): bygelyks de koades 0x40 - 0x4f oerienkomme mei flaters yn kontenerûntbining.

mar, foarearst, in ferskowing yn 'e oare rjochting, en twadde, it makket neat út, wêr binne de oare koades? Yn 'e holle fan 'e auteur?.. Dat binne lykwols lytse dingen.

Ferslaving begjint yn berjochten oer berjochtstatussen en berjochtkopyen:

  • Fersyk foar Berjochtstatus ynformaasje
    As ien fan 'e partijen in skoft gjin ynformaasje hat krigen oer de status fan har útgeande berjochten, kin se it eksplisyt oanfreegje by de oare partij:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Ynformaasje Berjocht oangeande Status fan Berjochten
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Hjir, info is in tekenrige dy't presys ien byte fan berjochtstatus befettet foar elk berjocht fan 'e ynkommende msg_ids-list:

    • 1 = neat is bekend oer it berjocht (msg_id te leech, de oare partij is it miskien fergetten)
    • 2 = berjocht net ûntfongen (msg_id falt binnen it berik fan opsleine identifiers; de oare partij hat lykwols wis net sa'n berjocht ûntfongen)
    • 3 = berjocht net ûntfongen (msg_id te heech; de oare partij hat it lykwols noch net ûntfongen)
    • 4 = berjocht ûntfongen (tink derop dat dit antwurd ek tagelyk in ûntfangstbefêstiging is)
    • +8 = berjocht al befêstige
    • +16 = berjocht dat gjin erkenning fereasket
    • +32 = RPC-fraach befette yn berjocht dat wurdt ferwurke of ferwurking al foltôge
    • +64 = ynhâld-relatearre antwurd op al oanmakke berjocht
    • +128 = oare partij wit foar in feit dat berjocht is al ûntfongen
      Dit antwurd fereasket gjin erkenning. It is in erkenning fan de oanbelangjende msgs_state_req, yn en fan himsels.
      Tink derom dat as it ynienen bliken docht dat de oare partij gjin berjocht hat dat der nei út liket te wêzen, it berjocht gewoan opnij ferstjoerd wurde kin. Sels as de oare partij twa kopyen fan it berjocht tagelyk krije soe, sil it duplikaat negearre wurde. (As der tefolle tiid foarby is, en it orizjinele msg_id is net mear jildich, dan moat it berjocht ferpakt wurde yn msg_copy).
  • Frijwillige kommunikaasje fan status fan berjochten
    Elke partij kin de oare partij frijwillich ynformearje oer de status fan de berjochten dy't troch de oare partij ferstjoerd binne.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Utwreide frijwillige kommunikaasje fan status fan ien berjocht
    ...
    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;
  • Eksplisite fersyk om berjochten opnij te ferstjoeren
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    De partij op ôfstân reagearret fuortendaliks troch de frege berjochten opnij te ferstjoeren […]
  • Eksplisite fersyk om antwurden opnij te ferstjoeren
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    De partij op ôfstân reagearret fuortendaliks troch opnij te ferstjoeren antwurden nei de frege berjochten […]
  • Berjocht kopyen
    Yn guon situaasjes moat in âld berjocht mei in msg_id dat net mear jildich is, opnij ferstjoerd wurde. Dan wurdt it yn in kopycontainer ferpakt:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Ien kear ûntfongen, wurdt it berjocht ferwurke as wie de wrapper der net. As it lykwols wis is dat it berjocht orig_message.msg_id ûntfongen is, dan wurdt it nije berjocht net ferwurke (wylst it en orig_message.msg_id tagelyk wurde erkend). De wearde fan orig_message.msg_id moat leger wêze as de msg_id fan de kontener.

Lit ús sels stil hâlde oer wat msgs_state_info wer stekke de earen fan 'e ûnfoltôge TL út (wy hienen in fektor fan bytes nedich, en yn 'e legere twa bits wie der in enum, en yn 'e hegere twa bits wiene flaggen). It punt is oars. Begrypt immen wêrom dit alles yn 'e praktyk is? yn in echte klant nedich?.. Mei muoite, mar men kin foarstelle wat foardiel as in persoan is dwaande mei debuggen, en yn in ynteraktive modus - freegje de tsjinner wat en hoe. Mar hjir wurde de oanfragen beskreaun rûnreis.

It folget dat elke partij net allinich berjochten fersiferje en ferstjoere moat, mar ek gegevens oer harsels, oer de antwurden op har, foar in ûnbekende tiid opslaan. De dokumintaasje beskriuwt net de timings of de praktyske tapasberens fan dizze funksjes. gjin manier. Wat it meast verbazingwekkend is, is dat se eins wurde brûkt yn 'e koade fan offisjele kliïnten! Blykber waarden se wat ferteld dat net yn 'e publike dokumintaasje stie. Begryp út de koade wêrom, is net mear sa ienfâldich as yn it gefal fan TL - it is net in (relatyf) logysk isolearre diel, mar in stik ferbûn oan de applikaasje-arsjitektuer, d.w.s. sil signifikant mear tiid fereaskje om de applikaasjekoade te begripen.

Pings en timings. Wachtrige.

Fan alles, as wy de gissingen oer de serverarsjitektuer ûnthâlde (ferdieling fan oanfragen oer backends), folget in nochal tryst ding - nettsjinsteande alle leveringsgarânsjes yn TCP (of de gegevens wurde levere, of jo wurde ynformearre oer it gat, mar de gegevens sille wurde levere foardat it probleem optreedt), dat befêstigingen yn MTProto sels - gjin garânsjes. De tsjinner kin maklik ferlieze of smyt út jo berjocht, en neat kin dien wurde oer it, gewoan brûke ferskillende soarten krukken.

En earst fan alle - berjochtenwachtrigen. No, mei ien ding wie alles fanselssprekkend fan it begjin ôf - in net-befêstige berjocht moat wurde opslein en fergriemd. En nei hokker tiid? En de nar ken him. Miskien losse dy ferslaafde tsjinstberjochten dit probleem op ien of oare manier op mei krukken, sis, yn Telegram Desktop binne d'r sa'n 4 wachtrijen dy't oerienkomme mei har (miskien mear, lykas al neamd, hjirfoar moatte jo har koade en arsjitektuer serieuzer ferdjipje; tagelyk tiid, wy Wy witte dat it kin net nommen wurde as in stekproef; in bepaald oantal typen út de MTProto skema wurde net brûkt yn it).

Wêrom bart dit? Wierskynlik wiene de serverprogrammeurs net yn steat om betrouberens binnen it kluster te garandearjen, of sels buffering op 'e front balancer, en hawwe dit probleem oerbrocht nei de kliïnt. Ut wanhoop besocht Vasily in alternative opsje út te fieren, mei mar twa wachtrijen, mei algoritmen fan TCP - it mjitten fan de RTT nei de tsjinner en oanpasse de grutte fan it "finster" (yn berjochten) ôfhinklik fan it oantal net-befêstige oanfragen. Dat is, sa'n rûge heuristyk foar it beoardieljen fan de lading fan 'e server is hoefolle fan ús oanfragen it tagelyk kin kauwen en net ferlieze.

No, dat is, jo begripe, net? As jo ​​​​TCP opnij moatte ymplementearje boppe op in protokol dat rint oer TCP, jout dit in heul min ûntworpen protokol oan.

Oh ja, wêrom hawwe jo mear as ien wachtrige nedich, en wat betsjut dit foar in persoan dy't wurket mei in API op heech nivo? Sjoch, jo meitsje in fersyk, serialisearje it, mar faaks kinne jo it net fuortendaliks stjoere. Wêrom? Want it antwurd sil wêze msg_id, dat is tydlikаIk bin in label, wêrfan de opdracht it bêste útsteld wurdt oant sa let mooglik - yn it gefal dat de tsjinner it ôfwiist fanwegen in mismatch fan tiid tusken ús en him (fansels kinne wy ​​in kruk meitsje dy't ús tiid ferpleatst fan it hjoeddeiske oan 'e tsjinner troch in delta ta te foegjen berekkene út' e antwurden fan 'e tsjinner - offisjele kliïnten dogge dit, mar it is rûch en net krekt troch buffering). Dêrom, as jo in fersyk meitsje mei in lokale funksje-oprop fan 'e bibleteek, giet it berjocht troch de folgjende stadia:

  1. It leit yn ien wachtrige en wachtet op fersifering.
  2. Oansteld msg_id en it berjocht gie nei in oare wachtrige - mooglik trochstjoere; stjoere nei de socket.
  3. a) De tsjinner antwurde MsgsAck - it berjocht waard levere, wy wiskje it út 'e "oare wachtrige".
    b) Of oarsom, hy mocht wat net leuk, hy antwurde badmsg - opnij ferstjoere fan "in oare wachtrige"
    c) Der is neat bekend, it berjocht moat wer út in oare wachtrige ferstjoerd wurde - mar it is net krekt bekend wannear.
  4. De tsjinner reagearre úteinlik RpcResult - de eigentlike antwurd (as flater) - net allinnich levere, mar ek ferwurke.

Wierskynlik, it brûken fan konteners koe it probleem foar in part oplosse. Dit is as in boskje berjochten yn ien binne ferpakt, en de tsjinner antwurde mei in befêstiging op se allegear tagelyk, yn ien msg_id. Mar hy sil ek dit pak, as der wat mis gie, yn syn gehiel ôfwize.

En op dit punt komme net-technyske oerwagings yn spiel. Ut ûnderfining hawwe wy in protte krukken sjoen, en boppedat sille wy no mear foarbylden sjen fan minne advys en arsjitektuer - is it yn sokke omstannichheden it wurdich te fertrouwen en sokke besluten te nimmen? De fraach is retorysk (fansels net).

Wêr hawwe wy it oer? As jo ​​oer it ûnderwerp "drugsberjochten oer berjochten" noch kinne spekulearje mei beswieren lykas "do bist dom, jo ​​hawwe ús briljante plan net begrepen!" (skriuw dus earst de dokumintaasje, lykas gewoane minsken moatte, mei rationale en foarbylden fan pakketútwikseling, dan prate wy), dan binne timings/timeouts in suver praktyske en spesifike fraach, hjir is alles al lang bekend. Wat fertelt de dokumintaasje ús oer timeouts?

In tsjinner erkent normaal de ûntfangst fan in berjocht fan in kliïnt (normaal in RPC-fraach) mei in RPC-antwurd. As in antwurd in lange tiid komt, kin in tsjinner earst in ûntfangstbefêstiging stjoere, en wat letter, it RPC-antwurd sels.

In kliïnt erkent normaal de ûntfangst fan in berjocht fan in tsjinner (meastentiids in RPC-antwurd) troch in befêstiging ta te foegjen oan 'e folgjende RPC-fraach as it net te let wurdt ferstjoerd (as it wurdt generearre, bygelyks, 60-120 sekonden nei de ûntfangst fan in berjocht fan de tsjinner). As d'r lykwols foar in lange perioade gjin reden is om berjochten nei de tsjinner te stjoeren of as d'r in grut oantal net-befêstige berjochten fan 'e tsjinner is (bygelyks mear as 16), stjoert de kliïnt in stand-alone erkenning.

... ik fertaal: wy sels witte net hoefolle en hoe't wy it nedich hawwe, dus lit ús oannimme dat lit it sa wêze.

En oer pings:

Ping-berjochten (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

In antwurd wurdt normaal weromjûn nei deselde ferbining:

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

Dizze berjochten hawwe gjin erkenning nedich. In pong wurdt allinich oerdroegen as antwurd op in ping, wylst in ping kin wurde inisjearre troch beide kanten.

Utstelde ferbining slute + PING

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

Wurket as ping. Derneist, nei't dit is ûntfongen, begjint de tsjinner in timer dy't de aktuele ferbining disconnect_delay sekonden letter sil slute, útsein as it in nij berjocht fan itselde type ûntfangt dat alle foarige timers automatysk weromset. As de kliïnt dizze pings ien kear elke 60 sekonden stjoert, kin it bygelyks disconnect_delay ynstelle as 75 sekonden.

Binne jo gek?! Yn 60 sekonden sil de trein it stasjon yngean, passazjiers delsette en ophelje, en wer kontakt ferlieze yn 'e tunnel. Yn 120 sekonden, wylst jo it hearre, sil it by in oare oankomme, en de ferbining sil nei alle gedachten brekke. No, it is dúdlik wêr't de skonken weikomme - "Ik hearde in rinkeljen, mar wit net wêr't it is", d'r is Nagl's algoritme en de opsje TCP_NODELAY, bedoeld foar ynteraktyf wurk. Mar, ekskús my, hâld fêst oan syn standertwearde - 200 Millisekonden As jo ​​wirklik wat ferlykber wolle ôfbyldzje en op in mooglik pear pakketten besparje, set it dan foar 5 sekonden ôf, of wat dan ek de "Brûker typt ..." berjochttime-out is no. Mar net mear.

En as lêste, pings. Dat is, it kontrolearjen fan de leefberens fan 'e TCP-ferbining. It is grappich, mar sa'n 10 jier lyn skreau ik in krityske tekst oer de boadskipper fan 'e dorm fan ús fakulteit - de auteurs pingelen dêr ek de tsjinner fan 'e kliïnt, en net oarsom. Mar studinten fan 3e jier binne ien ding, en in ynternasjonaal kantoar is in oar, toch? ..

Earst, in bytsje edukatyf programma. In TCP-ferbining, by it ûntbrekken fan pakketútwikseling, kin wiken libje. Dit is sawol goed as min, ôfhinklik fan it doel. It is goed as jo in SSH-ferbining iepene hawwe mei de tsjinner, jo binne opstien fan 'e kompjûter, de router opnij starte, werom nei jo plak - de sesje fia dizze tsjinner wie net skuord (jo hawwe neat typt, d'r wiene gjin pakketten) , it is handich. It is slim as d'r tûzenen kliïnten op 'e tsjinner binne, dy't elk boarnen opnimme (hallo, Postgres!), En de host fan' e kliïnt kin in lange tiid lyn opnij opstarte - mar wy sille it net witte.

Chat / IM-systemen falle yn it twadde gefal foar ien ekstra reden - online statusen. As de brûker "falt ôf", jo moatte ynformearje syn petearpartners oer dit. Oars sille jo einigje mei in flater dy't de makkers fan Jabber makken (en foar 20 jier korrizjearre) - de brûker hat loskeppele, mar se bliuwe berjochten nei him skriuwe, yn 't leauwe dat hy online is (dy't ek folslein ferlern gien yn dizze pear minuten foardat de ûntsluting waard ûntdutsen). Nee, de opsje TCP_KEEPALIVE, dy't in protte minsken dy't net begripe hoe't TCP-timers wurkje willekeurich ynbringe (troch wylde wearden yn te stellen lykas tsientallen sekonden), sil hjir net helpe - jo moatte derfoar soargje dat net allinich de OS-kern fan de brûker syn masine libbet, mar ek funksjonearret normaal, yn steat om te reagearjen, en de applikaasje sels (tinke jo dat it kin net befrieze? Telegram Desktop op Ubuntu 18.04 beferzen foar my mear as ien kear).

Dêrom moatte jo pingelen server klant, en net oarsom - as de klant dit docht, as de ferbining ferbrutsen is, sil de ping net wurde levere, it doel sil net wurde berikt.

Wat sjogge wy op Telegram? It is krekt oarsom! No, dat wol. Formeel, fansels, beide kanten kinne ping inoar. Yn 'e praktyk brûke kliïnten in kruk ping_delay_disconnect, dy't de timer op 'e tsjinner ynstelt. No, ekskús, it is net oan de klant om te besluten hoe lang hy dêr sûnder ping wenje wol. De tsjinner, basearre op syn lading, wit better. Mar, fansels, as jo de boarnen net tinke, dan sille jo jo eigen kweade Pinocchio wêze, en in kruk sil dwaan ...

Hoe soe it moatte wurde ûntwurpen?

Ik leau dat de boppesteande feiten dúdlik oanjaan dat it Telegram / VKontakte team is net hiel kompetinte op it mêd fan ferfier (en leger) nivo fan kompjûter netwurken en harren lege kwalifikaasjes yn relevante saken.

Wêrom die it sa yngewikkeld te wêzen, en hoe kinne Telegram-arsjitekten besykje beswier te meitsjen? It feit dat se besochten in sesje te meitsjen dy't TCP-ferbining oerlibbet brekt, dus wat no net levere waard, sille wy letter leverje. Se hawwe wierskynlik ek besocht om in UDP-ferfier te meitsjen, mar se tsjinkamen swierrichheden en ferlieten it (dêrom is de dokumintaasje leech - d'r wie neat om oer te opskeppen). Mar troch in gebrek oan begryp fan hoe't netwurken yn it algemien en TCP yn it bysûnder wurkje, wêr kinne jo fertrouwe op it, en wêr't jo moatte dwaan it sels (en hoe), en in besykjen om te kombinearjen dit mei kryptografy "twa fûgels mei ien stien”, dit is it resultaat.

Hoe wie it nedich? Op grûn fan it feit dat msg_id is in tiidstempel dy't nedich is út in kryptografysk eachpunt om opnij oanfallen te foarkommen, it is in flater om der in unike identifierfunksje oan te heakjen. Dêrom, sûnder de hjoeddeistige arsjitektuer fûneminteel te feroarjen (as de Updates-stream wurdt generearre, dat is in API-ûnderwerp op heech nivo foar in oar diel fan dizze searje berjochten), soe men moatte:

  1. De tsjinner dy't de TCP-ferbining hâldt mei de kliïnt nimt ferantwurdlikens - as it hat lêzen fan 'e socket, erken dan, ferwurkje of werom in flater, gjin ferlies. Dan is de befêstiging net in fektor fan ids, mar gewoan "de lêste ûntfongen seq_no" - gewoan in nûmer, lykas yn TCP (twa nûmers - jo seq en de befêstige). Wy binne altyd binnen de sesje, binne wy ​​net?
  2. It tiidstempel om opnij oanfallen te foarkommen wurdt in apart fjild, a la nonce. It wurdt kontrolearre, mar hat gjin ynfloed op wat oars. Genôch en uint32 - as ús sâlt op syn minst elke heale dei feroaret, kinne wy ​​16 bits tawize oan 'e leechsteande bits fan in folslein diel fan' e hjoeddeistige tiid, de rest - oan in fraksje fan in sekonde (lykas no).
  3. Fuorthelle msg_id überhaupt - út it eachpunt fan ûnderskiedende oanfragen op 'e backends, is d'r as earste de klant-id, en as twadde, de sesje-id, ferbine se. Dêrtroch is mar ien ding genôch as fersykidentifikaasje seq_no.

Dit is ek net de meast súksesfolle opsje; in folsleine willekeur kin as identifier tsjinje - dit wurdt trouwens al dien yn 'e API op hege nivo by it ferstjoeren fan in berjocht. It soe better wêze om de arsjitektuer folslein opnij te meitsjen fan relatyf nei absolút, mar dit is in ûnderwerp foar in oar diel, net dizze post.

API?

Ta-daam! Dat, nei't wy troch in paad fol pine en krukken hawwe striden, koene wy ​​einlings alle oanfragen nei de tsjinner stjoere en alle antwurden derop ûntfange, en ek updates fan 'e tsjinner ûntfange (net as antwurd op in fersyk, mar it sels stjoert ús, lykas PUSH, as immen is it dúdliker op dy manier).

Oandacht, no sil d'r it ienige foarbyld wêze yn Perl yn it artikel! (foar dyjingen dy't net bekend binne mei de syntaksis, it earste argumint fan segen is de gegevensstruktuer fan it objekt, de twadde is de klasse):

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

Ja, net opsetsin in spoiler - as jo it noch net hawwe lêzen, gean dan troch en doch it!

Oh, wai~~... hoe sjocht dit der út? Iets heul bekend ... miskien is dit de gegevensstruktuer fan in typyske Web API yn JSON, útsein dat klassen ek oan objekten hechte binne? ..

Dus dit is hoe't it blykt ... Wat is it allegear oer, kameraden? .. Safolle muoite - en wy stoppe om te rêstjen wêr't de webprogrammeurs gewoan begjinne?..Soe net gewoan JSON oer HTTPS ienfâldiger wêze?! Wat krigen wy yn ruil? Wie de muoite wurdich?

Litte wy evaluearje wat TL + MTProto ús joech en hokker alternativen mooglik binne. No, HTTP, dy't him rjochtet op it model foar fersyk-antwurd, is in minne fit, mar teminsten wat boppe op TLS?

Kompakte serialisaasje. Sjoen dizze gegevensstruktuer, fergelykber mei JSON, tink ik dat d'r binêre ferzjes binne. Litte wy MsgPack markearje as net genôch útwreiber, mar d'r is bygelyks CBOR - trouwens, in standert beskreaun yn RFC 7049. It is opmerklik foar it feit dat it definiearret tags, as in útwreiding meganisme, en ûnder al standerdisearre beskikber:

  • 25 + 256 - it ferfangen fan werhelle rigels mei in ferwizing nei it linenûmer, sa'n goedkeap kompresjemetoade
  • 26 - serialisearre Perl-objekt mei klasse namme en konstruktor arguminten
  • 27 - serialisearre taalûnôfhinklik objekt mei type namme en konstruktor arguminten

No, ik besocht te serialisearjen deselde gegevens yn TL en yn CBOR mei string en foarwerp packing ynskeakele. It resultaat begon te fariearjen yn it foardiel fan CBOR earne fan in megabyte:

cborlen=1039673 tl_len=1095092

En sa, РІС <РІРѕРґ: Der binne substansjeel ienfâldiger formaten dy't net ûnderwurpen binne oan it probleem fan syngronisaasjefout of ûnbekende identifier, mei fergelykbere effisjinsje.

Fast ferbining fêstiging. Dit betsjut nul RTT nei opnij ferbining (as de kaai is al generearre ien kear) - fan tapassing fan de alderearste MTProto berjocht, mar mei guon reservearrings - hit itselde sâlt, de sesje is net rot, ensfh. Wat biedt TLS ús ynstee? Sitaat oer ûnderwerp:

By it brûken fan PFS yn TLS, TLS sesje tickets (RFC 5077) om in fersifere sesje te hervatten sûnder toetsen opnij te ûnderhanneljen en sûnder kaaiynformaasje op 'e tsjinner op te slaan. By it iepenjen fan de earste ferbining en it meitsjen fan kaaien, de tsjinner fersiferet de ferbining steat en stjoert it nei de kliïnt (yn 'e foarm fan in sesje ticket). Sadwaande, as de ferbining opnij wurdt, stjoert de kliïnt in sesjekaartsje, ynklusyf de sesjekaai, werom nei de tsjinner. It kaartsje sels is fersifere mei in tydlike kaai (session ticket key), dy't opslein wurdt op 'e tsjinner en moat ferdield wurde ûnder alle frontend-tsjinners dy't SSL ferwurkje yn klustere oplossingen.[10]. Sa kin de ynfiering fan in sesjekaartsje PFS skeine as tydlike serverkaaien kompromitteare wurde, bygelyks as se foar in lange tiid wurde opslein (OpenSSL, nginx, Apache bewarje se standert foar de hiele doer fan it programma; populêre siden brûke de kaai foar ferskate oeren, oant dagen).

Hjir is de RTT net nul, jo moatte op syn minst ClientHello en ServerHello útwikselje, wêrnei't de kliïnt gegevens tegearre mei Finished kin stjoere. Mar hjir moatte wy betinke dat wy net it web hawwe, mei syn boskje nij iepene ferbiningen, mar in boadskipper, wêrfan de ferbining faaks ien en mear of minder langlibbene, relatyf koarte fersiken nei websiden is - alles is multiplexed yntern. Dat is, it is hiel akseptabel as wy net tsjinkomme in echt min subway seksje.

Wat oars fergetten? Skriuw yn 'e kommentaren.

Wurdt ferfolge!

Yn it twadde diel fan dizze searje berjochten sille wy net technyske, mar organisatoaryske problemen beskôgje - oanpakken, ideology, ynterface, hâlding foar brûkers, ensfh. Op grûn fan 'e technyske ynformaasje dy't hjir waard presintearre.

It tredde diel sil trochgean mei it analysearjen fan 'e technyske komponint / ûntwikkelingsûnderfining. Jo sille leare, benammen:

  • fuortsetting fan it pandemonium mei it ferskaat oan TL-soarten
  • ûnbekende dingen oer kanalen en supergroepen
  • wêrom dialogen binne slimmer as roster
  • oer absolute vs relative berjochtadressering
  • wat is it ferskil tusken foto en ôfbylding
  • hoe't emoji ynterferearje mei kursive tekst

en oare krukken! Bliuw op 'e hichte!

Boarne: www.habr.com

Add a comment