Kritiek op die protokol en organisatoriese benaderings van Telegram. Deel 1, tegnies: ervaring van die skryf van 'n kliënt van nuuts af - TL, MT

Onlangs het plasings meer gereeld op Habré begin verskyn oor hoe goed Telegram is, hoe briljant en ervare die Durov-broers is in die bou van netwerkstelsels, ens. Terselfdertyd het baie min mense hulself regtig verdiep in die tegniese toestel - hulle gebruik hoogstens 'n redelik eenvoudige (en baie anders as MTProto) JSON-gebaseerde Bot API, en aanvaar gewoonlik net op geloof al daardie lofprysinge en PR wat om die boodskapper draai. Byna 'n jaar en 'n half gelede het my kollega by NPO Echelon Vasily (ongelukkig is sy rekening op Habré saam met die konsep uitgevee) sy eie Telegram-kliënt van voor af in Perl begin skryf, en later het die skrywer van hierdie reëls aangesluit. Hoekom Perl, sal sommige dadelik vra? Want daar is reeds sulke projekte in ander tale. Trouens, dit is nie die punt nie, daar kan enige ander taal wees waar voltooide biblioteek, en dienooreenkomstig moet die skrywer al die pad gaan van die begin af. Boonop is kriptografie so iets - vertrou, maar verifieer. Met 'n sekuriteit-gefokusde produk kan jy nie net staatmaak op 'n verkoper se klaargemaakte biblioteek en dit blindelings glo nie (dit is egter 'n onderwerp vir meer in die tweede deel). Op die oomblik werk die biblioteek redelik goed op die "middelste" vlak (laat jou toe om enige API-versoeke te maak).

Daar sal egter nie veel kriptografie en wiskunde in hierdie reeks plasings wees nie. Maar daar sal baie ander tegniese besonderhede en argitektoniese krukke wees (dit sal ook nuttig wees vir diegene wat nie van nuuts af sal skryf nie, maar die biblioteek in enige taal sal gebruik). Dus, die hoofdoel was om die kliënt van nuuts af te probeer implementeer volgens amptelike dokumentasie. Dit wil sê, veronderstel dat die bronkode van amptelike kliënte gesluit is (weereens, in die tweede deel, sal ons die onderwerp van wat dit werklik is in meer besonderhede openbaar gebeur so), maar soos in die ou dae, byvoorbeeld, is daar 'n standaard soos RFC - is dit moontlik om 'n kliënt volgens die spesifikasie alleen te skryf, "sonder om in die bronkode te loer", selfs amptelik (Telegram Desktop, selfoon) ), selfs nie-amptelike Telethon?

INHOUDSOPGAWE:

Dokumentasie ... is dit daar? Is dit waar?..

Fragmente van notas vir hierdie artikel het verlede somer begin versamel word. Al hierdie tyd op die amptelike webwerf https://core.telegram.org die dokumentasie was vanaf Laag 23, d.w.s. iewers in 2014 vasgesit (onthou jy, destyds was daar nog nie eens kanale nie?). Natuurlik, in teorie, moes dit dit moontlik gemaak het om 'n kliënt met funksionaliteit op daardie tydstip in 2014 te implementeer. Maar selfs in hierdie toestand was die dokumentasie eerstens onvolledig, en tweedens het dit op plekke homself weerspreek. 'n Bietjie meer as 'n maand gelede, in September 2019, was dit ongeluk daar is gevind dat die webwerf 'n groot opdatering van die dokumentasie het, vir 'n heeltemal vars Layer 105, met 'n nota dat alles nou weer gelees moet word. Inderdaad, baie artikels is hersien, maar baie het onveranderd gebly. Daarom, wanneer u die kritiek hieronder oor die dokumentasie lees, moet u in gedagte hou dat sommige van hierdie dinge nie meer relevant is nie, maar sommige nog redelik. Na alles, 5 jaar in die moderne wêreld is nie net baie nie, maar baie baie van. Sedertdien het die aantal API-metodes in die skema van honderd tot meer as tweehonderd-en-vyftig sedertdien (veral as jy nie die weggooide en opgestane geochats sedertdien in ag neem nie!

Waar begin jy as jong skrywer?

Dit maak nie saak of jy van nuuts af skryf of byvoorbeeld klaargemaakte biblioteke soos Telethon vir Python of Madeline vir PHP, in elk geval, sal jy eers nodig het registreer jou aansoek - kry parameters api_id и api_hash (diegene wat met die VKontakte API gewerk het, verstaan ​​dadelik) waarmee die bediener die toepassing sal identifiseer. Hierdie sal moet om wetlike redes, maar ons sal in die tweede deel meer praat oor hoekom biblioteekskrywers dit nie kan publiseer nie. Miskien sal jy tevrede wees met die toetswaardes, hoewel dit baie beperk is - die feit is dat jy nou op jou nommer kan registreer net een aansoek, so moenie kophou jaag nie.

Nou, uit 'n tegniese oogpunt, moes ons daarin belanggestel het dat ons na registrasie kennisgewings van Telegram moes ontvang oor opdaterings aan die dokumentasie, protokol, ens. Dit wil sê, 'n mens sou kon aanvaar dat die terrein met die dokke eenvoudig "gepunt" is en spesifiek voortgegaan het om te werk met diegene wat begin om kliënte te maak, want. dis makliker. Maar nee, niks so is waargeneem nie, geen inligting het gekom nie.

En as jy van nuuts af skryf, dan is die gebruik van die ontvangde parameters eintlik nog ver weg. Alhoewel https://core.telegram.org/ en praat eers oor hulle in Getting Started, eintlik moet jy eers implementeer MTProto protokol - maar as jy glo uitleg volgens die OSI-model aan die einde van die bladsy van die algemene beskrywing van die protokol, dan heeltemal tevergeefs.

Trouens, beide voor MTProto en daarna, op verskeie vlakke gelyktydig (soos buitelandse netwerkers wat in die OS-kern werk, sê laagoortreding), sal 'n groot, pynlike en verskriklike onderwerp in die pad kom ...

Binêre serialisering: TL (Type Taal) en sy skema, en lae, en baie ander eng woorde

Hierdie onderwerp is in werklikheid die sleutel tot Telegram se probleme. En daar sal baie verskriklike woorde wees as jy daarin probeer delf.

So, skema. As jy hierdie woord onthou, sê, JSON-skemaJy het reg gedink. Die doel is dieselfde: een of ander taal om 'n moontlike stel oorgedrade data te beskryf. Dit is in werklikheid waar die ooreenkoms eindig. As van die bladsy MTProto protokol, of uit die bronboom van die amptelike kliënt, sal ons probeer om 'n skema oop te maak, ons sal iets sien soos:

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;

'n Persoon wat dit vir die eerste keer sien sal intuïtief net 'n deel van wat geskryf is herken - wel, dit is blykbaar strukture (alhoewel waar is die naam, links of regs?), Daar is velde daarin, waarna die tipe gaan deur die kolon ... waarskynlik. Hier, tussen hakies, is daar waarskynlik sjablone soos in C ++ (in werklikheid, nie heeltemaal nie). En wat beteken al die ander simbole, vraagtekens, uitroeptekens, persentasies, roosters (en natuurlik beteken dit verskillende dinge op verskillende plekke), êrens, maar nie iewers nie, heksadesimale getalle - en die belangrikste, hoe om hieruit te kom правильный (wat nie deur die bediener verwerp sal word nie) greepstroom? Jy moet die dokumentasie lees (Ja, daar is skakels na die skema in die JSON-weergawe naby - maar dit maak dit nie duideliker nie).

Die oopmaak van die bladsy Binêre data serialisering en duik in die magiese wêreld van sampioene en diskrete wiskunde, iets soortgelyk aan matan in die 4de jaar. Alfabet, tipe, waarde, kombinator, funksionele kombinator, normale vorm, saamgestelde tipe, polimorfiese tipe... en dit is net die eerste bladsy! Volgende wag vir jou TL Taal, wat, hoewel dit reeds 'n voorbeeld van 'n onbenullige versoek en reaksie bevat, glad nie 'n antwoord op meer tipiese gevalle bied nie, wat beteken dat jy deur die hervertelling van wiskunde wat uit Russies in Engels vertaal is op agt meer geneste sal moet waad. bladsye!

Lesers wat vertroud is met funksionele tale en outomatiese tipe afleiding, het natuurlik in hierdie taal beskrywings gesien, selfs uit 'n voorbeeld, baie meer bekend, en kan sê dat dit in beginsel nie sleg is nie. Die besware hierteen is:

  • Ja, die doel klink goed, maar helaas nie bereik nie
  • onderwys in Russiese universiteite wissel selfs tussen IT-spesialiteite - nie almal lees die ooreenstemmende kursus nie
  • Ten slotte, soos ons sal sien, is dit in die praktyk nie nodig nie, aangesien slegs 'n beperkte subset van selfs die TL wat beskryf is, gebruik word

Soos gesê Leonerd op die kanaal #perl op die FreeNode IRC-netwerk, probeer om 'n hek van Telegram na Matrix te implementeer (die vertaling van die aanhaling is onakkuraat uit die geheue):

Dit voel asof iemand wat vir die eerste keer aan tikteorie bekend gestel is, opgewonde geraak het en daarmee begin speel het, nie regtig omgee of dit in die praktyk nodig is nie.

Kyk self of die behoefte aan kaal tipes (int, long, ens.) as iets elementêrs nie vrae laat ontstaan ​​nie - op die ou end moet dit handmatig geïmplementeer word - kom ons neem byvoorbeeld 'n poging om daaruit af te lei vektor. Dit is in werklikheid, skikking, as jy die gevolglike dinge by hul eie name noem.

Maar voor

Kort beskrywing van 'n subset van die TL-sintaksis vir diegene wat dit nie doen nie ... lees die amptelike dokumentasie

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;

Begin altyd definisie ontwerper, waarna, opsioneel (in die praktyk, altyd) deur die simbool # behoort CRC32 van die genormaliseerde beskrywingstring van die gegewe tipe. Volgende kom die beskrywing van die velde, as hulle is - die tipe kan leeg wees. Dit eindig alles met 'n gelyke-teken, die naam van die tipe waaraan die gegewe konstruktor - dit is in werklikheid die subtipe - behoort. Die tipe regs van die gelykheidsteken is polimorfies - dit wil sê, dit kan ooreenstem met verskeie spesifieke tipes.

As die definisie na die lyn voorkom ---functions---, dan sal die sintaksis dieselfde bly, maar die betekenis sal anders wees: die konstruktor sal die naam van die RPC-funksie word, die velde sal parameters word (wel, dit wil sê, dit sal presies dieselfde gegewe struktuur bly soos hieronder beskryf, dit sal net die betekenis wees wat gegee word), en "polimorfiese tipe ' is die tipe van die teruggestuurde resultaat. Dit sal weliswaar steeds polimorfies bly – net in die afdeling gedefinieer ---types---, en hierdie konstruktor sal nie oorweeg word nie. Tik oorladings van geroepe funksies volgens hul argumente, d.w.s. om een ​​of ander rede word verskeie funksies met dieselfde naam maar 'n ander handtekening, soos in C++, nie in TL verskaf nie.

Hoekom "konstruktor" en "polimorf" as dit nie OOP is nie? Wel, trouens, dit sal makliker wees vir iemand om daaroor te dink in terme van OOP - 'n polimorfiese tipe as 'n abstrakte klas, en konstrukteurs is sy direkte afstammelinge klasse, bowendien final in die terminologie van 'n aantal tale. Trouens, natuurlik, hier ooreenkoms met werklike oorlaaide konstruktormetodes in OO-programmeertale. Aangesien daar net datastrukture hier is, is daar geen metodes nie (alhoewel die beskrywing van funksies en metodes hieronder redelik in staat is om verwarring in die kop te skep oor wat dit is, maar dit gaan oor iets anders) - jy kan aan 'n konstruktor dink as 'n waarde waaruit gebou word tik wanneer 'n stroom grepe gelees word.

Hoe gebeur dit? Die deserializer, wat altyd 4 grepe lees, sien die waarde 0xcrc32 - en verstaan ​​wat volgende gaan gebeur field1 met tipe int, d.w.s. lees presies 4 grepe, op hierdie oorliggende veld met tipe PolymorType lees. Sien 0x2crc32 en verstaan ​​dat daar twee velde verder is, eerstens long, so ons lees 8 grepe. En dan weer 'n komplekse tipe, wat op dieselfde manier gedeserialiseer word. Byvoorbeeld, Type3 kan in die skema verklaar word sodra twee konstruktors, onderskeidelik, verder óf moet ontmoet 0x12abcd34, waarna jy nog 4 grepe moet lees intOf 0x6789cdef, waarna daar niks sal wees nie. Enigiets anders - jy moet 'n uitsondering gooi. In elk geval, daarna keer ons terug na die lees van 4 grepe int velde field_c в constructorTwo en daarop lees ons ons klaar PolymorType.

Ten slotte, as gevang 0xdeadcrc vir constructorThree, dan raak dinge meer ingewikkeld. Ons eerste veld bit_flags_of_what_really_present met tipe # - in werklikheid is dit net 'n alias vir die tipe natwat "natuurlike getal" beteken. Dit is, in werklikheid, ongetekende int is die enigste geval, terloops, wanneer ongetekende nommers in werklike skemas gevind word. Dus, volgende is 'n konstruksie met 'n vraagteken, wat beteken dat dit die veld is - dit sal slegs op die draad teenwoordig wees as die ooreenstemmende bis in die veld waarna verwys word gestel is (ongeveer soos 'n drieledige operateur). So, veronderstel dat hierdie bietjie aan was, dan moet jy 'n veld lees soos Type, wat in ons voorbeeld 2 konstruktors het. Een is leeg (bestaan ​​slegs uit 'n identifiseerder), die ander het 'n veld ids met tipe ids:Vector<long>.

Jy mag dalk dink dat beide sjablone en generieke goed is of Java. Maar nee. Amper. Hierdie die enigste geval van hoekhakies in werklike stroombane, en dit word SLEGS vir Vector gebruik. In 'n greepstroom sal dit 4 CRC32 grepe vir die Vector-tipe self wees, altyd dieselfde, dan 4 grepe - die aantal skikkingselemente, en dan hierdie elemente self.

Voeg hierby die feit dat serialisering altyd in woorde van 4 grepe plaasvind, alle tipes is veelvoude daarvan - ingeboude tipes word ook beskryf bytes и string met die hand serialisering van die lengte en hierdie belyning met 4 - wel, dit klink normaal en selfs relatief doeltreffend? Alhoewel daar beweer word dat TL doeltreffende binêre serialisering is, maar te hel met hulle, met die uitbreiding van enigiets, selfs Boolese waardes en enkelkarakterstringe tot 4 grepe, sal JSON steeds baie dikker wees? Kyk, selfs onnodige velde kan deur bietjie-vlae oorgeslaan word, alles is net goed, en selfs uitbreidbaar vir die toekoms, het jy later nuwe opsionele velde by die konstruktor gevoeg?

Maar nee, as jy nie my kort beskrywing lees nie, maar die volledige dokumentasie, en dink oor die implementering. Eerstens, die konstruktor se CRC32 word bereken deur die genormaliseerde skema teks beskrywing string (verwyder ekstra wit spasie, ens.) - so as 'n nuwe veld bygevoeg word, sal die tipe beskrywing string verander, en dus sy CRC32 en, gevolglik, serialisering. En wat sou die ou kliënt doen as hy 'n veld met nuwe vlae gestel kry, maar hy weet nie wat om volgende daarmee te doen nie? ..

Tweedens, laat ons onthou CRC32, wat hier in wese gebruik word as hash funksies om uniek te bepaal watter tipe (gede)serialiseer word. Hier sit ons voor die probleem van botsings – en nee, die waarskynlikheid is nie een uit 232 nie, maar veel meer. Wie het onthou dat CRC32 ontwerp is om foute in die kommunikasiekanaal op te spoor (en reg te stel) en dienooreenkomstig hierdie eienskappe te verbeter tot nadeel van ander? Sy gee byvoorbeeld nie om oor die permutasie van grepe nie: as jy CRC32 vanaf twee reëls tel, sal jy in die tweede die eerste 4 grepe omruil met die volgende 4 grepe - dit sal dieselfde wees. Wanneer ons teksstringe uit die Latynse alfabet (en 'n bietjie leestekens) as invoer het, en hierdie name is nie besonder toevallig nie, word die waarskynlikheid van so 'n permutasie aansienlik verhoog.

Terloops, wie het nagegaan wat daar was werklik CRC32? In een van die vroeë bronne (selfs voor Waltman) was daar 'n hash-funksie wat elke karakter vermenigvuldig met die getal 239, so geliefd deur hierdie mense, ha ha!

Uiteindelik, goed, ons het besef dat konstrukteurs met 'n veldtipe Vector<int> и Vector<PolymorType> sal verskillende CRC32 hê. En wat van die aanbieding op die lyn? En in terme van teorie, word dit deel van die tipe? Kom ons sê ons slaag 'n skikking van tienduisend getalle, wel, met Vector<int> alles is duidelik, die lengte en nog 40000 XNUMX grepe. En as dit Vector<Type2>, wat slegs uit een veld bestaan int en dit is die enigste een in die tipe - moet ons 10000xabcdef0 34 keer herhaal en dan 4 grepe int, of die taal kan dit vir ons vanaf die konstruktor WYS fixedVec en in plaas van 80000 40000 grepe, dra weer net XNUMX XNUMX oor?

Dit is glad nie 'n ledige teoretiese vraag nie - stel jou voor jy kry 'n lys van groepgebruikers, wat elkeen 'n id, voornaam, van het - die verskil in die hoeveelheid data wat oor 'n mobiele verbinding oorgedra word, kan beduidend wees. Dit is die doeltreffendheid van Telegram-serialisering wat aan ons geadverteer word.

So…

Vektor, wat nie afgelei kon word nie

As jy probeer om deur die beskrywingsbladsye van kombinatore en omtrent te blaai, sal jy sien dat 'n vektor (en selfs 'n matriks) formeel probeer om verskeie velle deur tupels af te lei. Maar op die ou end word hulle gehamer, die laaste stap word oorgeslaan, en die definisie van 'n vektor word eenvoudig gegee, wat ook nie aan 'n tipe gebonde is nie. Wat is die saak hier? In tale Programmering, veral funksionele, is dit nogal tipies om die struktuur rekursief te beskryf - die samesteller met sy lui evaluasie sal alles verstaan ​​en dit doen. In taal data serialisering maar DOELTREFFENDHEID is nodig: dit is genoeg om eenvoudig te beskryf lys, d.w.s. 'n struktuur van twee elemente - die eerste is 'n data-element, die tweede is dieselfde struktuur self of 'n leë spasie vir die stert (pak (cons) in Lisp). Maar dit sal natuurlik vereis elkeen element spandeer addisioneel 4 grepe (CRC32 in die geval van TL) om sy tipe te beskryf. Dit is maklik om 'n skikking te beskryf vaste grootte, maar in die geval van 'n skikking van 'n voorheen onbekende lengte, breek ons ​​af.

Dus, aangesien TL jou nie toelaat om 'n vektor uit te voer nie, moes dit aan die kant bygevoeg word. Uiteindelik sê die dokumentasie:

Serialisering gebruik altyd dieselfde konstruktor “vektor” (const 0x1cb5c415 = crc32(“vector t:Type # [t ] = Vector t”) wat nie afhanklik is van die spesifieke waarde van die veranderlike van tipe t nie.

Die waarde van die opsionele parameter t is nie betrokke by die serialisering nie, aangesien dit afgelei is van die resultaattipe (altyd bekend voor deserialisering).

Kyk van naderby: vector {t:Type} # [ t ] = Vector t - maar nêrens die definisie self sê nie dat die eerste getal gelyk moet wees aan die lengte van die vektor nie! En dit volg nêrens nie. Dit is 'n gegewe wat jy in gedagte moet hou en met jou hande moet implementeer. Elders noem die dokumentasie selfs eerlik dat die tipe vals is:

Die Vector t polimorfiese pseudotipe is 'n "tipe" waarvan die waarde 'n reeks waardes van enige tipe t is, hetsy in boks of kaal.

… maar fokus nie daarop nie. Wanneer jy, moeg daarvoor om deur die strek van wiskunde (dalk selfs aan jou bekend van 'n universiteitskursus) te waag, besluit om te score en te kyk hoe om werklik daarmee te werk in die praktyk, bly die indruk in jou kop: hier is Serious Mathematics gebaseer op , natuurlik Cool People (twee wiskundiges -wenner van die ACM), en nie sommer enigiemand nie. Die doelwit – om te spog – is bereik.

Terloops, oor die nommer. Herroep # dit is 'n sinoniem nat, natuurlike getal:

Daar is tipe uitdrukkings (tipeuitdr) en numeriese uitdrukkings (nat-uitdr). Hulle word egter op dieselfde manier gedefinieer.

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

maar in grammatika word hulle op dieselfde manier beskryf, d.w.s. hierdie verskil moet weer onthou word en met die hand in die implementering ingesit word.

Wel, ja, sjabloontipes (vector<int>, vector<User>) het 'n gemeenskaplike identifiseerder (#1cb5c415), d.w.s. as jy weet die oproep is verklaar as

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

dan wag jy nie net vir 'n vektor nie, maar 'n vektor van gebruikers. Meer presies, behoort wag - in regte kode sal elke element, indien nie 'n blote tipe nie, 'n konstruktor hê, en op 'n goeie manier in die implementering sal dit nodig wees om na te gaan - en ons is presies in elke element van hierdie vektor gestuur daardie tipe? En as dit 'n soort PHP was, waarin die skikking verskillende tipes in verskillende elemente kan bevat?

Op hierdie stadium begin jy wonder - is so 'n TL nodig? Miskien sou dit vir die kar moontlik wees om die menslike serializer te gebruik, dieselfde protobuf wat toe reeds bestaan ​​het? Dit was teorie, kom ons kyk na die praktyk.

Bestaande TL-implementerings in kode

TL is in die ingewande van VKontakte gebore selfs voor die bekende gebeure met die verkoop van Durov se aandeel en (sekerlik), selfs voor die ontwikkeling van Telegram. En in oopbron bronne van die eerste implementering jy kan baie snaakse krukke vind. En die taal self is meer volledig daar geïmplementeer as wat dit nou in Telegram is. Hashes word byvoorbeeld glad nie in die skema gebruik nie (wat beteken die ingeboude pseudotipe (soos 'n vektor) met afwykende gedrag). Of

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

maar laat ons volledigheidshalwe die prentjie oorweeg, om so te sê die evolusie van die Reus van Denke na te spoor.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Of hierdie pragtige een:

    static const char *reserved_words_polymorhic[] = {

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

      };

Hierdie fragment handel oor sjablone, soos:

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

Dit is die definisie van die hashmap-sjabloontipe, as 'n vektor van int - Tipe-pare. In C++ sal dit iets soos volg lyk:

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

so, alpha - sleutelwoord! Maar net in C++ kan jy T skryf, maar jy moet alfa, beta skryf... Maar nie meer as 8 parameters nie, die fantasie het op theta geëindig. Dit blyk dus dat daar een keer in St. Petersburg ongeveer sulke dialoë was:

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

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

Maar dit was oor die eerste uitgelê implementering van TL "in die algemeen". Kom ons gaan oor na die oorweging van implementerings in die werklike Telegram-kliënte.

Basil se woord:

Vasily, [09.10.18 17:07] Bowenal, die esel is warm van die feit dat hulle 'n klomp abstraksies opgeskroef het, en toe 'n bout daarop gehamer het en die codegegerator met krukke oorgetrek het
As gevolg hiervan, eers vanaf die dokke die pilot.jpg
Dan van die jekichan.webp-kode

Natuurlik, van mense wat vertroud is met algoritmes en wiskunde, kan ons verwag dat hulle Aho, Ullman gelees het en vertroud is met die de facto industriestandaardgereedskap om hul DSL-samestellers oor die dekades heen te skryf, reg? ..

Skrywer telegram-cli is Vitaliy Valtman, soos verstaan ​​kan word uit die voorkoms van die TLO-formaat buite sy (cli) perke, 'n lid van die span - nou is die biblioteek vir die ontleding van TL toegeken afsonderlikwat is die indruk van haar TL ontleder? ..

16.12 04:18 Vasily: na my mening het iemand nie lex + yacc bemeester nie
16.12 04:18 Vasily: anders kan ek dit nie verduidelik nie
16.12 04:18 Vasily: wel, of hulle is betaal vir die aantal reëls in VK
16.12 04:19 Vasily: 3k+ lyne van ander<censored> in plaas van 'n ontleder

Miskien 'n uitsondering? Kom ons kyk hoe doen dit is die AMPTELIKE 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+ reëls in Python, 'n paar gereelde uitdrukkings + spesiale gevalle van die vektortipe, wat natuurlik in die skema verklaar word soos dit volgens die TL-sintaksis moet wees, maar hulle sit dit op hierdie sintaksis, ontleed dit meer ... Die vraag is, hoekom die moeite doen met al hierdie wonderwerkиmeer puff, as niemand dit in elk geval volgens die dokumentasie gaan ontleed nie?!

Terloops... Onthou ons het gepraat oor die CRC32-tjek? Dus, in die Telegram Desktop-kodegenerator is daar 'n lys uitsonderings vir die tipes waarin die berekende CRC32 stem nie ooreen nie soos in die diagram aangedui!

Vasily, [18.12 22:49] en hier moet jy dink of so 'n TL nodig is
as ek met alternatiewe implementerings wil mors, sal ek reëlbreuke begin invoeg, die helfte van die ontleders sal op multi-lyn definisies breek
tdesktop egter ook

Onthou die punt oor one-liners, ons sal 'n bietjie later terugkom.

Goed, telegram-cli is nie-amptelik, Telegram Desktop is amptelik, maar wat van die ander? En wie weet? .. In die Android-kliëntkode was daar glad geen skema-ontleder nie (wat vrae laat ontstaan ​​oor oopbron, maar dit is vir die tweede deel), maar daar was verskeie ander snaakse stukke kode, maar daaroor in die onderafdeling hieronder.

Watter ander vrae laat serialisering in die praktyk ontstaan? Byvoorbeeld, hulle het natuurlik met bietjie velde en voorwaardelike velde opgeskroef:

vasily: flags.0? true
beteken die veld is teenwoordig en waar as die vlag gestel is

vasily: flags.1? int
beteken dat die veld teenwoordig is en gedeserialiseer moet word

Vasily: Ass, moenie brand nie, wat doen jy!
Vasily: Iewers in die dokument is daar 'n melding dat waar 'n kaal tipe van nul lengte is, maar dit is onrealisties om iets uit hul dokumente te versamel
Vasily: Daar is ook nie so iets in oop implementerings nie, maar daar is baie krukke en stutte

Wat van 'n Telethon? As ons vorentoe kyk oor die onderwerp van MTProto, 'n voorbeeld - daar is sulke stukke in die dokumentasie, maar die teken % dit word slegs beskryf as "ooreenstemmende met die gegewe kaal-tipe", d.w.s. in die voorbeelde hieronder, óf 'n fout óf iets wat nie gedokumenteer is nie:

Vasily, [22.06.18/18/38 XNUMX:XNUMX] Op een plek:

msg_container#73f1f8dc messages:vector message = MessageContainer;

In 'n ander:

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

En dit is twee groot verskille, in die werklike lewe kom 'n soort naakte vektor

Ek het nie blote vektordefinisies gesien nie en het dit nog nie teëgekom nie

Analise geskryf in teleton met die hand

Sy skema het die definisie kommentaar gelewer msg_container

Weereens bly die vraag omtrent%. Dit word nie beskryf nie.

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

Vasily, [22.06.18/19/23 XNUMX:XNUMX] Maar hulle TL-parser op die reguleerders sal dit waarskynlik ook nie eet nie

// parsed manually

TL is 'n pragtige abstraksie, niemand implementeer dit heeltemal nie

En daar is geen % in hul weergawe van die skema nie

Maar hier weerspreek die dokumentasie homself, so xs

Dit is in grammatika gevind, hulle kon net vergeet om die semantiek te beskryf

Wel, jy het die beskuldigdebank op TL gesien, jy kan dit nie uitvind sonder 'n halwe liter nie

"Wel, kom ons sê," sal 'n ander leser sê, "jy kritiseer alles, so wys dit soos dit moet."

Vasily antwoord: “wat die ontleder betref, ek het dinge nodig soos

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

een of ander manier meer soos 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 die 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];

dié. eenvoudiger is om dit sag te stel."

Oor die algemeen pas die ontleder en kodegenerator vir die werklik gebruikte subset van TL in ongeveer 100 reëls grammatika en ~ 300 reëls van die kragopwekker (insluitend alle printse gegenereerde kode), insluitend tipe goedjies en tipe inligting vir introspeksie in elke klas. Elke polimorfiese tipe word in 'n leë abstrakte basisklas verander, en konstrukteurs erf dit en het metodes vir serialisering en deserialisering.

Gebrek aan tipes in tiktaal

Sterk tik is goed, reg? Nee, dit is nie 'n holivar nie (hoewel ek dinamiese tale verkies), maar 'n postulaat binne TL. Op grond daarvan behoort die taal allerhande tjeks vir ons te verskaf. Wel, goed, laat hom nie, maar die implementering, maar hy moet hulle ten minste beskryf. En watter geleenthede wil ons hê?

Eerstens, beperkings. Hier sien ons in die dokumentasie vir die oplaai van lêers:

Die lêer se binêre inhoud word dan in dele verdeel. Alle dele moet dieselfde grootte hê ( deel_grootte ) en die volgende voorwaardes moet nagekom word:

  • part_size % 1024 = 0 (deelbaar deur 1KB)
  • 524288 % part_size = 0 (512KB moet eweredig deelbaar wees deur deelgrootte)

Die laaste deel hoef nie aan hierdie voorwaardes te voldoen nie, mits sy grootte kleiner as deel_grootte is.

Elke deel moet 'n rynommer hê, lêer_deel, met 'n waarde wat wissel van 0 tot 2,999.

Nadat die lêer gepartisioneer is, moet u 'n metode kies om dit op die bediener te stoor. gebruik upload.saveBigFilePart ingeval die volle grootte van die lêer meer as 10 MB is en upload.saveFilePart vir kleiner lêers.
[…] een van die volgende data-invoerfoute kan teruggestuur word:

  • FILE_PARTS_INVALID - Ongeldige aantal dele. Die waarde is nie tussen nie 1..3000

Is enige van hierdie teenwoordig in die skema? Is dit op een of ander manier uitdrukbaar deur middel van TL? Geen. Maar verskoon my, selfs die outydse Turbo Pascal kon die tipes beskryf wat deur gegee word reekse. En hy kon nog een ding doen, nou beter bekend as enum - 'n tipe wat bestaan ​​uit 'n opsomming van 'n vaste (klein) aantal waardes. In tale soos C - numeries, let wel, ons het tot dusver net oor tipes gepraat. getalle. Maar daar is ook skikkings, stringe ... dit sal byvoorbeeld lekker wees om te beskryf dat hierdie string net 'n telefoonnommer kan bevat, nie waar nie?

Niks hiervan is in TL nie. Maar daar is byvoorbeeld in JSON-skema. En as iemand anders beswaar kan maak oor die deelbaarheid van 512 KB dat dit nog in die kode gekontroleer moet word, maak dan seker dat die kliënt eenvoudig kon nie stuur nommer buite bereik 1..3000 (en die ooreenstemmende fout kon nie ontstaan ​​het nie) dit sou moontlik wees, reg? ..

Terloops, oor foute en terugkeerwaardes. Die oog is vaag selfs vir diegene wat met TL gewerk het – dit het nie dadelik tot ons deurgedring nie elkeen 'n funksie in TL kan eintlik nie net die beskryf tipe terugkeer terugstuur nie, maar ook 'n fout. Maar dit is nie afleibaar deur middel van die TL self nie. Natuurlik is dit verstaanbaar en nafig is nie in die praktyk nodig nie (alhoewel RPC eintlik op verskillende maniere gedoen kan word, ons sal hierna terugkeer) - maar wat van die Suiwerheid van die konsepte van Wiskunde van Abstrakte Tipes uit die hemelse wêreld? .. Die sleepboot gegryp - so pas.

En laastens, wat van leesbaarheid? Wel, daar, in die algemeen, wil ek graag beskrywing het dit reg in die skema (weereens, dit is in die JSON-skema), maar as dit reeds daarmee gespanne is, wat dan van die praktiese kant - dit is ten minste belaglik om verskille tydens opdaterings te kyk? Sien self by werklike voorbeelde:

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

Iemand hou daarvan, maar GitHub, byvoorbeeld, weier om veranderinge binne sulke lang lyne uit te lig. Die speletjie "vind 10 verskille", en wat die brein dadelik sien is dat die begin en einde dieselfde is in beide voorbeelde, jy moet vervelig iewers in die middel lees ... Na my mening is dit nie net in teorie nie, maar suiwer visueel lyk vuil en onversorgd.

Terloops, oor die suiwerheid van teorie. Hoekom is bietjie velde nodig? Lyk hulle nie reuk sleg uit die oogpunt van tipe teorie? 'n Verduideliking kan in vroeëre weergawes van die skema gesien word. Aanvanklik, ja, dit was so, 'n nuwe tipe is vir elke nies geskep. Hierdie beginsels is nog steeds daar in hierdie vorm, byvoorbeeld:

storage.fileUnknown#aa963b05 = storage.FileType;
storage.filePartial#40bc6f52 = storage.FileType;
storage.fileJpeg#7efe0e = storage.FileType;
storage.fileGif#cae1aadf = storage.FileType;
storage.filePng#a4f63c0 = storage.FileType;
storage.filePdf#ae1e508d = storage.FileType;
storage.fileMp3#528a0677 = storage.FileType;
storage.fileMov#4b09ebbc = storage.FileType;
storage.fileMp4#b3cea0e4 = storage.FileType;
storage.fileWebp#1081464c = storage.FileType;

Maar stel jou nou voor as jy 5 opsionele velde in jou struktuur het, dan benodig jy 32 tipes vir alle moontlike opsies. kombinatoriese ontploffing. So het die kristalsuiwerheid van die TL-teorie weereens teen die gietystergat van die harde werklikheid van serialisering neergestort.

Boonop oortree hierdie ouens op plekke self hul eie tik. Byvoorbeeld, in MTProto (volgende hoofstuk) kan die reaksie deur Gzip saamgepers word, alles is sinvol - behalwe vir die oortreding van lae en skema. Een keer, en het nie die RpcResult self geoes nie, maar die inhoud daarvan. Wel, hoekom doen dit? .. Ek moes 'n kruk insny sodat kompressie enige plek sou werk.

Of 'n ander voorbeeld, ons het een keer 'n fout gevind - gestuur InputPeerUser in plaas van InputUser. Of andersom. Maar dit het gewerk! Dit wil sê, die bediener het nie omgegee oor die tipe nie. Hoe kan dit wees? Die antwoord sal miskien gevra word deur kodefragmente van telegram-cli:

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

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

Met ander woorde, hier word die serialisering gedoen HANDMATIG, nie gegenereerde kode nie! Is die bediener dalk op 'n soortgelyke manier geïmplementeer?.. In beginsel sal dit werk as dit een keer gedoen word, maar hoe om dit dan tydens opdaterings te ondersteun? Is dit nie waarvoor die skema was nie? En dan gaan ons oor na die volgende vraag.

Weergawe. Lae

Waarom skemaweergawes lae genoem word, kan slegs geraai word op grond van die geskiedenis van gepubliseerde skemas. Blykbaar het dit aanvanklik vir die skrywers gelyk of basiese dinge op 'n onveranderde skema gedoen kan word, en slegs waar nodig, aan spesifieke versoeke aandui dat dit volgens 'n ander weergawe gedoen word. In beginsel, selfs 'n goeie idee - en die nuwe sal as 't ware "inmeng", 'n laag op die oue. Maar kom ons kyk hoe dit gedoen is. Dit was weliswaar nie moontlik om van die begin af te kyk nie - dit is snaaks, maar die basislaagskema bestaan ​​eenvoudig nie. Lae het by 2 begin. Die dokumentasie vertel ons van 'n spesiale TL-kenmerk:

As 'n kliënt Laag 2 ondersteun, moet die volgende konstruktor gebruik word:

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

In die praktyk beteken dit dat voor elke API-oproep 'n int met die waarde 0x289dd1f6 moet voor die metodenommer bygevoeg word.

Klink OK. Maar wat het volgende gebeur? Toe kom

invokeWithLayer3#b7475268 query:!X = X;

So wat is volgende? Soos dit maklik is om te raai

invokeWithLayer4#dea0d430 query:!X = X;

Snaaks? Nee, dis te vroeg om te lag, dink oor wat elkeen 'n versoek van 'n ander laag moet in so 'n spesiale tipe toegedraai word - as jy hulle almal anders het, hoe anders om hulle te onderskei? En om net 4 grepe voor by te voeg, is 'n redelik doeltreffende metode. Dus

invokeWithLayer5#417a57ae query:!X = X;

Maar dit is duidelik dat dit na 'n rukkie 'n bietjie bacchanalia sal word. En die oplossing het gekom:

Opdatering: Begin met Laag 9, helpermetodes invokeWithLayerN kan saam gebruik word initConnection

Hoera! Na 9 weergawes het ons uiteindelik gekom by wat in die 80's in die internetprotokolle gedoen is - weergawe onderhandeling een keer aan die begin van die verbinding!

So wat is volgende? ..

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

En nou kan jy lag. Eers na nog 9 lae is uiteindelik 'n universele konstruktor met 'n weergawenommer bygevoeg, wat slegs een keer aan die begin van die verbinding genoem hoef te word, en die betekenis in die lae blyk te verdwyn, nou is dit net 'n voorwaardelike weergawe, soos oral anders. Probleem opgelos.

Reg?..

Vasily, [16.07.18/14/01 XNUMX:XNUMX PM] Vrydag het ek gedink:
Die telebediener stuur gebeurtenisse sonder 'n versoek. Versoeke moet in InvokeWithLayer toegedraai word. Die bediener omvou nie opdaterings nie, daar is geen struktuur om antwoorde en opdaterings om te draai nie.

Dié. die kliënt kan nie die laag spesifiseer waarin hy opdaterings wil hê nie

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX PM] Is InvokeWithLayer nie in beginsel 'n kruk nie?

Vasily, [16.07.18/14/02 XNUMX:XNUMX PM] Dit is die enigste manier

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX PM] wat in wese lae aan die begin van die sessie moet beteken

Terloops, dit volg hieruit dat 'n kliënt-afgradering nie verskaf word nie

Opdaterings, m.a.w. tipe Updates in die skema is dit wat die bediener na die kliënt stuur, nie in reaksie op 'n API-versoek nie, maar op sy eie wanneer 'n gebeurtenis plaasvind. Dit is 'n komplekse onderwerp wat in 'n ander pos bespreek sal word, maar vir eers is dit belangrik om te weet dat die bediener opdaterings ophoop selfs wanneer die kliënt vanlyn is.

Dus, wanneer jy weier om te wikkel elkeen pakket om sy weergawe aan te dui, vandaar dat die volgende moontlike probleme logies ontstaan:

  • die bediener stuur opdaterings na die kliënt voordat die kliënt gesê het watter weergawe dit ondersteun
  • wat moet gedoen word nadat die kliënt opgegradeer is?
  • wat waarborgedat die bediener se mening oor die laagnommer nie in die proses sal verander nie?

Dink jy dit is suiwer teoretiese denke, en in die praktyk kan dit nie gebeur nie, want die bediener is reg geskryf (dit is in elk geval goed getoets)? Ha! Maak nie saak hoe nie!

Dit is presies wat ons in Augustus raakgeloop het. Op 14 Augustus het boodskappe geflits dat iets op die Telegram-bedieners opgedateer word ... en toe in die 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 'n paar megagrepe stapelspore (wel, terselfdertyd is logging reggestel). Na alles, as iets nie in jou TL herken is nie - dit is binêr deur handtekeninge, verder in die stroom ALMAL gaan, sal dekodering onmoontlik word. Wat om te doen in so 'n situasie?

Wel, die eerste ding wat in iemand se gedagtes opkom, is om te ontkoppel en weer te probeer. Het nie gehelp nie. Ons het CRC32 gegoogle - dit blyk voorwerpe uit skema 73 te wees, alhoewel ons aan skema 82 gewerk het. Ons kyk noukeurig na die logs - daar is identifiseerders van twee verskillende skemas!

Is die probleem dalk bloot in ons nie-amptelike kliënt? Nee, ons loop Telegram Desktop 1.2.17 (die weergawe wat by 'n aantal Linux-verspreidings voorsien word), dit skryf aan die Uitsonderingslogboek: MTP Onverwagse tipe id #b5223b0f gelees in MTPMessageMedia ...

Kritiek op die protokol en organisatoriese benaderings van Telegram. Deel 1, tegnies: ervaring van die skryf van 'n kliënt van nuuts af - TL, MT

Google het gewys dat 'n soortgelyke probleem reeds met een van die nie-amptelike kliënte gebeur het, maar toe was die weergawenommers en dienooreenkomstig die aannames anders ...

So wat om te doen? Vasily en ek is uitmekaar: hy het probeer om die skema op te dateer na 91, ek het besluit om 'n paar dae te wag en te probeer om 73. Beide metodes het gewerk, maar aangesien hulle empiries is, is daar geen begrip van hoeveel weergawes jy nodig het om op te spring of af, ook nie hoe lank jy moet wag nie.

Later het ek daarin geslaag om die situasie te reproduseer: ons begin die kliënt, skakel dit af, hersaamstel die skema na 'n ander laag, herbegin, vang die probleem weer op, keer terug na die vorige een - oeps, geen verandering van die skema en herbegin die kliënt vir verskeie minute sal help. Jy sal 'n mengsel van datastrukture van verskillende lae ontvang.

Verduideliking? Soos u uit die verskillende indirekte simptome kan raai, bestaan ​​die bediener uit baie verskillende soorte prosesse op verskillende masjiene. Waarskynlik, die een van die bedieners wat verantwoordelik is om te "buffer" het in die tou geplaas wat die hoëres dit gegee het, en hulle het dit gegee in die skema wat ten tyde van generasie was. En totdat hierdie tou “vrot” was, kon niks daaraan gedoen word nie.

Tensy ... maar dit is 'n verskriklike kruk?!.. Nee, voor ons dink aan mal idees, kom ons kyk na die kode van amptelike kliënte. In die Android-weergawe vind ons geen TL-ontleder nie, maar ons vind 'n stewige lêer (github weier om dit in te kleur) met (de)serialisering. Hier is die kodebrokkies:

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... dit lyk mal. Maar dit is waarskynlik 'n gegenereerde kode, dan is dit goed? .. Maar dit ondersteun beslis alle weergawes! Dit is waar, dit is nie duidelik hoekom alles in een hoop gemeng is nie, en geheime geselsies, en allerhande soorte _old7 een of ander manier nie soortgelyk aan masjiengenerasie nie ... Maar die meeste van alles het ek mal geword

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Ouens, kan julle nie eers binne een laag besluit nie?! Wel, goed, "twee", kom ons sê, is vrygestel met 'n fout, wel, dit gebeur, maar DRIE? .. Dadelik weer op dieselfde hark? Watse soort pornografie is dit, jammer? ..

Terloops, 'n soortgelyke ding gebeur in die Telegram Desktop-bronne - indien wel, en verskeie commits in 'n ry na die skema verander nie sy laagnommer nie, maar maak iets reg. In toestande wanneer daar geen amptelike databron vir die skema is nie, waar kan ek dit vandaan kry, behalwe vir die amptelike kliëntbronne? En jy neem dit van daar af, jy kan nie seker wees dat die skema heeltemal korrek is totdat jy al die metodes toets nie.

Hoe kan dit selfs getoets word? Ek hoop dat aanhangers van eenheid-, funksionele en ander toetse in die kommentaar sal deel.

Goed, kom ons kyk na 'n ander stukkie kode:

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;

Daardie "handgemaakte" opmerking hier dui daarop dat slegs 'n deel van hierdie lêer met die hand geskryf is (kan jy jou die onderhoudsnagmerrie voorstel?), en die res word deur die masjien gegenereer. Dan ontstaan ​​egter 'n ander vraag - dat die bronne beskikbaar is nie heeltemal nie (a la blobs onder die GPL in die Linux-kern), maar dit is reeds 'n onderwerp vir die tweede deel.

Maar genoeg. Kom ons gaan aan na die protokol waarop al hierdie serialisering jaag.

MT Proto

So kom ons maak oop algemene beskrywing и gedetailleerde beskrywing van die protokol en die eerste ding waaroor ons struikel, is terminologie. En met 'n oorvloed van alles. Oor die algemeen blyk dit 'n handelsmerk van Telegram te wees - om dinge op verskillende plekke op verskillende maniere te noem, of verskillende dinge in een woord, of andersom (byvoorbeeld in 'n hoëvlak API as jy 'n plakkerpak sien - dit is nie wat jy gedink het nie).

Byvoorbeeld, "boodskap" (boodskap) en "sessie" (sessie) - hier beteken hulle iets anders as in die gewone koppelvlak van die Telegram-kliënt. Wel, alles is duidelik met die boodskap, dit kan geïnterpreteer word in terme van OOP, of bloot die woord "pakket" genoem word - dit is 'n lae vervoervlak, daar is nie dieselfde boodskappe as in die koppelvlak nie, daar is baie van diens. Maar die sessie ... maar eerste dinge eerste.

Vervoerlaag

Die eerste ding is vervoer. Ons sal oor 5 opsies vertel word:

  • TCP
  • Webwerf
  • Websocket oor HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18/15/04 XNUMX:XNUMX PM] En daar is ook UDP-vervoer, maar dit is nie gedokumenteer nie

En TCP in drie variante

Die eerste is soortgelyk aan UDP oor TCP, elke pakkie bevat 'n volgnommer en 'n crc
Hoekom is dit so pynlik om dokke op 'n kar te lees?

Wel daar nou TCP reeds in 4 variante:

  • verkorte
  • Intermediêre
  • opgestopte intermediêre
  • Volle

Ok, opgestopte intermediêre vir MTProxy, dit is later bygevoeg as gevolg van bekende gebeure. Maar hoekom nog twee weergawes (drie in totaal), wanneer een kan doen? Al vier verskil in wese slegs in hoe om die lengte en loonvrag van die werklike hoof-MTProto in te stel, wat verder bespreek sal word:

  • in Afgekort is dit 1 of 4 grepe maar nie 0xef dan liggaam nie
  • in Intermediate is dit 4 grepe lengte en 'n veld, en die eerste keer wat die kliënt moet stuur 0xeeeeeeee om aan te dui dat dit Intermediêr is
  • ten volle, die mees verslawende, uit die oogpunt van 'n netwerker: lengte, volgordenommer, en NIE DIE EEN wat basies MTProto, liggaam, CRC32 is nie. Ja, dit alles oor TCP. Wat ons voorsien van betroubare vervoer in die vorm van 'n reeks stroom grepe, geen rye is nodig nie, veral kontrolesomme. Goed, nou sal hulle teen my beswaar maak dat TCP 'n 16-bis kontrolesom het, so datakorrupsie vind plaas. Wonderlik, behalwe dat ons eintlik 'n kriptografiese protokol het met hashes langer as 16 grepe, sal al hierdie foute - en selfs meer - op 'n SHA-wanpassing op 'n hoër vlak vasgevang word. Daar is GEEN punt in CRC32 hieroor nie.

Kom ons vergelyk Abridged, waar een greep lengte moontlik is, met Intermediate, wat "In die geval dat 4-grepe data-belyning nodig is" regverdig, wat redelik onsin is. Wat, daar word geglo dat Telegram-programmeerders so lomp is dat hulle nie data van die sok in 'n belynde buffer kan lees nie? Jy moet dit steeds doen, want lees kan jou enige aantal grepe teruggee (en daar is ook instaanbedieners, byvoorbeeld ...). Of, aan die ander kant, hoekom die moeite doen met Abridged as ons nog stewige opvullings van 16 grepe bo-op het - spaar 3 grepe soms ?

Mens kry die indruk dat Nikolai Durov baie lief is daarvoor om fietse uit te vind, insluitend netwerkprotokolle, sonder werklike praktiese behoefte.

Ander vervoeropsies, inkl. Web en MTProxy, ons sal nie nou oorweeg nie, miskien in 'n ander pos, as daar 'n versoek is. Ons sal nou eers oor hierdie einste MTProxy onthou dat verskaffers kort na sy vrystelling in 2018 vinnig geleer het om presies dit te blokkeer, bedoel vir blokkeer verbypadDeur pakkie grootte! En ook die feit dat die MTProxy-bediener wat (weereens deur Waltman) in C geskryf is, onnodig aan Linux besonderhede gekoppel was, hoewel dit glad nie nodig was nie (Phil Kulin sal bevestig), en dat 'n soortgelyke bediener óf op Go óf op Node.js pas minder as honderd lyne.

Maar ons sal gevolgtrekkings maak oor die tegniese geletterdheid van hierdie mense aan die einde van die afdeling, nadat ons ander kwessies oorweeg het. Kom ons gaan vir eers aan na die 5de OSI-laag, sessie - waarop hulle die MTProto-sessie geplaas het.

Sleutels, boodskappe, sessies, Diffie-Hellman

Hulle het dit nie heeltemal korrek daar gestel nie ... Sessie is nie die sessie wat in die koppelvlak onder Aktiewe sessies sigbaar is nie. Maar in volgorde.

Kritiek op die protokol en organisatoriese benaderings van Telegram. Deel 1, tegnies: ervaring van die skryf van 'n kliënt van nuuts af - TL, MT

Hier het ons 'n string grepe van bekende lengte van die vervoerlaag ontvang. Dit is óf 'n geënkripteerde boodskap óf gewone teks - as ons nog in die sleutelonderhandelingstadium is en dit eintlik doen. Van watter van die klomp konsepte genaamd "sleutel" praat ons? Kom ons verduidelik hierdie kwessie vir die Telegram-span self (ek vra om verskoning dat ek my eie dokumentasie van Engels vertaal het na óf 'n moeë brein om 4 in die oggend, dit was makliker om 'n paar frases te laat soos hulle is):

Daar is twee entiteite genoem Sessie - een in die UI van amptelike kliënte onder "huidige sessies", waar elke sessie ooreenstem met 'n hele toestel / OS.
Tweedens - MTProto sessie, wat 'n boodskapvolgnommer (in 'n lae-vlak sin) daarin het, en watter kan tussen verskillende TCP-verbindings duur. Verskeie MTProto-sessies kan terselfdertyd opgestel word, byvoorbeeld om lêeraflaaie te bespoedig.

Tussen hierdie twee sessies is die konsep magtiging. In die ontaarde geval kan mens dit sê UI sessie is dieselfde as magtigingMaar helaas, dit is ingewikkeld. Ons kyk:

  • Die gebruiker op die nuwe toestel genereer eers auth_key en bind dit aan rekening, byvoorbeeld, per SMS - dit is hoekom magtiging
  • Dit het binne die eerste gebeur MTProto sessie, wat het session_id binne jouself.
  • By hierdie stap, die kombinasie magtiging и session_id genoem kon word byvoorbeeld - hierdie woord word gevind in die dokumentasie en kode van sommige kliënte
  • Dan kan die kliënt oopmaak sommige MTProto sessies onder dieselfde auth_key - na dieselfde DC.
  • Dan moet die kliënt eendag 'n lêer by versoek 'n ander DC - en vir hierdie DC sal 'n nuwe een gegenereer word auth_key !
  • Om die stelsel te vertel dat dit nie 'n nuwe gebruiker is wat registreer nie, maar dieselfde magtiging (UI sessie), die kliënt gebruik API-oproepe auth.exportAuthorization in die huis DC auth.importAuthorization in die nuwe DC.
  • Tog kan daar verskeie oop wees MTProto sessies (elkeen met sy eie session_id) na hierdie nuwe DC, onder sy auth_key.
  • Laastens wil die kliënt Perfect Forward Secrecy hê. Elke auth_key dit was permanente sleutel - per DC - en die kliënt kan bel auth.bindTempAuthKey vir gebruik tydelike auth_key - en weer net een temp_auth_key per DC, algemeen vir almal MTProto sessies na hierdie DC.

neem waar dat sout (en toekomstige soute) ook een op auth_key dié. onder almal gedeel MTProto sessies na dieselfde DC.

Wat beteken "tussen verskillende TCP-verbindings"? Dit beteken dat hierdie iets soos magtigingskoekie op 'n webwerf - dit hou (oorleef) baie TCP-verbindings na hierdie bediener, maar eendag sal dit sleg gaan. Slegs anders as HTTP, in MTProto, binne die sessie, word boodskappe opeenvolgend genommer en bevestig, hulle het die tonnel binnegegaan, die verbinding is verbreek - nadat 'n nuwe verbinding tot stand gebring is, sal die bediener vriendelik alles in hierdie sessie stuur wat dit nie in die vorige TCP-verbinding.

Die inligting hierbo is egter 'n knyp na baie maande se litigasie. Implementeer ons intussen ons kliënt van nuuts af? - kom ons gaan terug na die begin.

So genereer ons auth_key op weergawes van Diffie-Hellman van Telegram. Kom ons probeer om die dokumentasie te verstaan ​​...

Vasily, [19.06.18/20/05 1:255] data_with_hash := SHAXNUMX(data) + data + (enige ewekansige grepe); sodanig dat die lengte gelyk is aan XNUMX grepe;
geïnkripteer_data := RSA(data_met_hash, bediener_publieke_sleutel); 'n 255-grepe lange getal (groot endian) word verhoog tot die vereiste krag oor die vereiste modulus, en die resultaat word as 'n 256-grepe getal gestoor.

Hulle het 'n dope DH gekry

Lyk nie na 'n gesonde persoon se DH nie
Daar is nie twee publieke sleutels in dx nie

Wel, op die ou end het ons dit uitgepluis, maar die sediment het gebly - 'n bewys van werk word deur die kliënt gedoen dat hy die getal kon faktoriseer. Tipe beskerming teen DoS-aanvalle. En die RSA-sleutel word net een keer in een rigting gebruik, in wese vir enkripsie new_nonce. Maar terwyl hierdie oënskynlik eenvoudige operasie slaag, wat sal jy in die gesig staar?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Ek het nog nie die toepaslike versoek bereik nie

Ek het 'n versoek aan DH gestuur

En in die beskuldigdebank op die vervoer is geskryf dat dit kan antwoord met 4 grepe van die foutkode. En dit is dit

Wel, hy het vir my gesê -404, so wat?

Hier is ek vir hom: "vang jou efigna geïnkripteer met die bedienersleutel met 'n vingerafdruk van so en so, ek wil DH hê", en dit reageer dom 404

Wat sou jy dink van so 'n bedienerreaksie? Wat om te doen? Daar is niemand om te vra nie (maar meer daaroor in die tweede deel).

Hier is al die belangstelling in die beskuldigdebank om te doen

Ek het niks anders om te doen nie, ek het net daarvan gedroom om getalle heen en weer om te skakel

Twee 32-bis-nommers. Ek het hulle gepak soos almal

Maar nee, dit is hierdie twee wat jy eerste in 'n ry as BE nodig het

Vadim Goncharov, [20.06.18/15/49 404:XNUMX PM] en as gevolg hiervan XNUMX?

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

Vadim Goncharov, [20.06.18/15/50 XNUMX:XNUMX PM] so ek verstaan ​​nie wat hy kan "nie gevind het nie"

Vasily, [20.06.18 15:50] oor

Ek het nie so 'n ontbinding in eenvoudige delers gevind nie%)

Selfs foutrapportering is nie bemeester nie

Vasily, [20.06.18/20/18 5:XNUMX PM] O, daar is ook MDXNUMX. Reeds drie verskillende hashes

Die sleutelvingerafdruk word soos volg bereken:

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

SHA1 en sha2

So kom ons stel auth_key 2048 stukkies in grootte het ons volgens Diffie-Hellman gekry. Wat is volgende? Dan vind ons uit dat die onderste 1024 bisse van hierdie sleutel nie op enige manier gebruik word nie ... maar kom ons dink solank hieroor. By hierdie stap het ons 'n gedeelde geheim met die bediener. 'n Analoog van 'n TLS-sessie is gevestig, 'n baie duur prosedure. Maar die bediener weet nog niks van wie ons is nie! Nog nie, eintlik magtiging. Dié. as jy gedink het in terme van "login-wagwoord", soos dit vroeër in ICQ was, of ten minste "login-key", soos in SSH (byvoorbeeld op een of ander gitlab / github). Ons het anoniem geword. En as die bediener ons antwoord "hierdie telefoonnommers word deur 'n ander DC bedien"? Of selfs “jou telefoonnommer is verbied”? Die beste ding wat ons kan doen, is om die sleutel te stoor in die hoop dat dit teen daardie tyd nog nuttig sal wees en nie vrot nie.

Terloops, ons het dit met voorbehoud “ontvang”. Vertrou ons byvoorbeeld die bediener? Is hy vals? Ons benodig kriptografiese tjeks:

Vasily, [21.06.18/17/53 2:XNUMX PM] Hulle bied mobiele kliënte aan om 'n XNUMXkbit-nommer na te gaan vir eenvoud%)

Maar dis glad nie duidelik nie, nafeijoa

Vasily, [21.06.18/18/02 XNUMX:XNUMX] Die beskuldigdebank sê nie wat om te doen as dit nie eenvoudig blyk te wees nie

Nie gesê nie. Kom ons kyk wat die amptelike kliënt vir Android in hierdie geval doen? A Dis wat (en ja, die hele lêer is interessant daar) - soos hulle sê, ek los dit net hier:

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

Nee, natuurlik daar sommige daar word gekontroleer vir die eenvoud van 'n getal, maar persoonlik het ek nie meer voldoende kennis in wiskunde nie.

Goed, ons het die hoofsleutel. Om aan te meld, m.a.w. versoeke stuur, is dit nodig om verdere enkripsie uit te voer, wat reeds AES gebruik.

Die boodskapsleutel word gedefinieer as die 128 middelste stukkies van die SHA256 van die boodskapliggaam (insluitend sessie, boodskap-ID, ens.), insluitend die opvulgrepe, voorafgegaan deur 32 grepe wat van die magtigingsleutel geneem is.

Vasily, [22.06.18/14/08 XNUMX:XNUMX PM] Gemiddelde tewe

Ontvang auth_key. Almal. Verder hulle ... dit is nie duidelik uit die dokke nie. Bestudeer gerus die oopbronkode.

Let daarop dat MTProto 2.0 van 12 tot 1024 grepe opvulling vereis, steeds onderhewig aan die voorwaarde dat die resulterende boodskaplengte deelbaar is deur 16 grepe.

So hoeveel vulling om in te sit?

En ja, ook hier, 404 in geval van 'n fout

As iemand die diagram en die teks van die dokumentasie noukeurig bestudeer het, het hy opgemerk dat daar geen MAC daar is nie. En daardie AES word in een of ander IGE-modus gebruik wat nêrens anders gebruik word nie. Hulle skryf natuurlik daaroor in hul FAQ... Hier, soos, die boodskapsleutel self is terselfdertyd die SHA-hash van die ontsyferde data wat gebruik word om die integriteit na te gaan - en in die geval van 'n wanpassing, die dokumentasie vir een of ander rede beveel aan om hulle stilweg te ignoreer (maar wat van sekuriteit, breek ons ​​skielik?).

Ek is nie 'n kriptograaf nie, miskien is daar in hierdie modus in hierdie geval niks verkeerd uit 'n teoretiese oogpunt nie. Maar ek kan beslis 'n praktiese probleem noem deur die voorbeeld van Telegram Desktop te gebruik. Dit enkripteer die plaaslike kas (al hierdie D877F783D5D3EF8C) op dieselfde manier as boodskappe in MTProto (slegs in hierdie geval, weergawe 1.0), d.w.s. eers die boodskapsleutel, dan die data self (en iewers eenkant die hoofgroot auth_key 256 grepe, waarsonder msg_key nutteloos). So, die probleem word opvallend op groot lêers. Jy moet naamlik twee kopieë van die data hou – geïnkripteer en gedekripteer. En as daar megagrepe is, of streaming video, byvoorbeeld? .. Klassieke skemas met MAC na die syferteks laat jou toe om dit streaming te lees en dit onmiddellik oor te dra. En met MTProto moet jy op die eerste enkripteer of dekripteer die hele boodskap, en dra dit dan eers na die netwerk of na skyf oor. Daarom, in die nuutste weergawes van Telegram Desktop in die kas in user_data 'n ander formaat word reeds gebruik - met AES in CTR-modus.

Vasily, [21.06.18/01/27 20:XNUMX AM] O, ek het uitgevind wat IGE is: IGE was die eerste poging tot 'n "verifiërende enkripsiemodus," oorspronklik vir Kerberos. Dit was 'n mislukte poging (dit bied nie integriteitbeskerming nie), en moes verwyder word. Dit was die begin van 'n XNUMX jaar lange soeke na 'n verifikasie enkripsiemodus wat werk, wat onlangs uitgeloop het op modusse soos OCB en GCM.

En nou die argumente van die kar se kant af:

Die span agter Telegram, onder leiding van Nikolai Durov, bestaan ​​uit ses ACM-kampioene, waarvan die helfte Ph.D's in wiskunde. Dit het hulle ongeveer twee jaar geneem om die huidige weergawe van MTProto uit te voer.

Wat is snaaks. Twee jaar na die laer vlak

Of ons kan net tls neem

Goed, kom ons sê ons het enkripsie en ander nuanses gedoen. Kan ons uiteindelik TL-vervolgversoeke stuur en antwoorde deserialiseer? So wat moet gestuur word en hoe? Hier is die metode initConnectiondalk is dit dit?

Vasily, [25.06.18/18/46 XNUMX:XNUMX PM] Inisialiseer verbinding en stoor inligting op die gebruiker se toestel en toepassing.

Dit aanvaar app_id, device_model, system_version, app_version en lang_code.

En 'n bietjie navraag

Dokumentasie soos altyd. Bestudeer gerus die oopbron

As alles min of meer duidelik was met invokeWithLayer, wat is dit dan? Dit blyk dat gestel ons het - die kliënt het reeds iets gehad om die bediener oor te vra - daar is 'n versoek wat ons wou stuur:

Vasily, [25.06.18/19/13 XNUMX:XNUMX] Te oordeel aan die kode, is die eerste oproep toegedraai in hierdie vullis, en die vullis self is in invokewithlayer

Hoekom kon initConnection nie 'n aparte oproep wees nie, maar moet dit 'n omhulsel wees? Ja, soos dit geblyk het, moet dit elke keer aan die begin van elke sessie gedoen word, en nie eenmalig, soos met die hoofsleutel nie. Maar! Dit kan nie deur 'n ongemagtigde gebruiker geroep word nie! Hier het ons die stadium bereik waarin dit van toepassing is Hierdie een dokumentasie bladsy - en dit vertel ons dat ...

Slegs 'n klein gedeelte van die API-metodes is beskikbaar vir ongemagtigde gebruikers:

  • auth.sendCode
  • auth.resendCode
  • rekening.kryWagwoord
  • 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

Die heel eerste van hulle auth.sendCode, en daar is daardie kosbare eerste versoek waarin ons api_id en api_hash sal stuur, en waarna ons 'n SMS met 'n kode ontvang. En as ons by die verkeerde DC gekom het (telefoonnommers van hierdie land word byvoorbeeld deur 'n ander bedien), dan sal ons 'n fout met die nommer van die verlangde DC ontvang. Om uit te vind met watter IP-adres ons moet koppel deur die DC-nommer, sal ons gehelp word help.getConfig. Eens was daar net 5 inskrywings, maar na die bekende gebeure van 2018 het die getal aansienlik toegeneem.

Laat ons nou onthou dat ons op hierdie stadium op die anonieme bediener gekom het. Is dit nie te duur om net 'n IP-adres te kry nie? Hoekom doen dit nie, en ander bedrywighede, in die ongeënkripteerde deel van MTProto nie? Ek hoor ’n beswaar: “hoe kan jy seker maak dat dit nie die RKN is wat met vals adresse gaan reageer nie?”. Om hierdie ons onthou dat, in werklikheid, in amptelike kliënte ingebedde RSA-sleutels, d.w.s. jy kan maar teken hierdie inligting. Eintlik word dit reeds gedoen vir inligting oor die omseil van slotte wat kliënte deur ander kanale ontvang (dit is logies dat dit nie in MTProto self gedoen kan word nie, want jy moet nog weet waar om te koppel).

OK. Op hierdie stadium van kliëntmagtiging is ons nog nie gemagtig nie en het ons nie ons aansoek geregistreer nie. Ons wil net vir eers sien wat die bediener reageer op die metodes wat beskikbaar is vir 'n ongemagtigde gebruiker. En hier…

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;

In die skema kom die eerste, die tweede

In die tdesktop-skema is die derde waarde

Ja, sedertdien is die dokumentasie natuurlik bygewerk. Alhoewel dit binnekort weer irrelevant kan word. En hoe moet 'n beginner ontwikkelaar weet? Miskien as jy jou aansoek registreer, sal hulle jou inlig? Vasily het dit gedoen, maar helaas, niks is aan hom gestuur nie (weereens, ons sal hieroor in die tweede deel praat).

... Jy het opgemerk dat ons reeds op een of ander manier na die API verskuif het, d.w.s. na die volgende vlak en het iets in die MTProto-tema gemis? Niks verbasend nie:

Vasily, [28.06.18/02/04 2:XNUMX AM] Mm, hulle vroetel deur sommige van die algoritmes op eXNUMXe

Mtproto definieer enkripsie-algoritmes en sleutels vir beide domeine, sowel as 'n bietjie van 'n omhulselstruktuur

Maar hulle meng voortdurend verskillende stapelvlakke, so dit is nie altyd duidelik waar mtproto geëindig het en die volgende vlak begin het nie.

Hoe word hulle gemeng? Wel, hier is dieselfde tydelike sleutel vir PFS, byvoorbeeld (terloops, Telegram Desktop weet nie hoe om dit te doen nie). Dit word uitgevoer deur 'n API-versoek auth.bindTempAuthKey, d.w.s. vanaf die boonste vlak. Maar terselfdertyd meng dit in met enkripsie op die laer vlak - daarna moet jy dit byvoorbeeld weer doen initConnection ens., dit is nie net normale versoek. Afsonderlik lewer dit ook dat jy net EEN tydelike sleutel op die DC kan hê, hoewel die veld auth_key_id in elke boodskap laat jou toe om die sleutel ten minste elke boodskap te verander, en dat die bediener die reg het om die tydelike sleutel te eniger tyd te "vergeet" - wat om te doen in hierdie geval, die dokumentasie sê nie ... wel, hoekom dit sou nie moontlik wees om verskeie sleutels te hê, soos met 'n stel toekomstige soute nie, maar ?..

Daar is 'n paar ander dinge wat die moeite werd is om op te let in die MTProto-tema.

Boodskapboodskappe, msg_id, msg_seqno, erkennings, pings in die verkeerde rigting en ander eienaardighede

Hoekom moet jy van hulle weet? Omdat hulle een vlak hoër "lek", en jy moet van hulle weet wanneer jy met die API werk. Gestel ons stel nie belang in msg_key nie, die laer vlak het alles vir ons gedekripteer. Maar binne die ontsyferde data het ons die volgende velde (ook die lengte van die data om te weet waar die opvulling is, maar dit is nie belangrik nie):

  • sout-int64
  • session_id - int64
  • boodskap_id - int64
  • seq_no-int32

Onthou dat sout een is vir die hele DC. Hoekom weet jy van haar? Nie net omdat daar 'n versoek is nie get_future_salts, wat vertel watter intervalle geldig sal wees, maar ook omdat as jou sout “vrot” is, dan gaan die boodskap (versoek) eenvoudig verlore. Die bediener sal natuurlik die nuwe sout rapporteer deur uit te reik new_session_created - maar met die ou een sal jy byvoorbeeld op een of ander manier weer moet stuur. En hierdie vraag beïnvloed die argitektuur van die toepassing.

Die bediener word om baie redes toegelaat om sessies heeltemal te laat vaar en op hierdie manier te reageer. Eintlik, wat is 'n MTProto-sessie van die kliënt se kant af? Dit is twee nommers session_id и seq_no boodskappe binne hierdie sessie. Wel, en die onderliggende TCP-verbinding, natuurlik. Kom ons sê ons kliënt weet steeds nie hoe om baie dinge te doen nie, ontkoppel, weer verbind. As dit vinnig gebeur het - die ou sessie het voortgegaan in die nuwe TCP-verbinding, verhoog seq_no verder. As dit lank neem, kan die bediener dit uitvee, want aan sy kant is dit ook 'n tou, soos ons uitgevind het.

Wat moet wees seq_no? O, dis 'n moeilike vraag. Probeer om eerlik te verstaan ​​wat bedoel is:

Inhoudverwante boodskap

'n Boodskap wat 'n eksplisiete erkenning vereis. Dit sluit al die gebruiker- en baie diensboodskappe in, feitlik almal met die uitsondering van houers en erkennings.

Boodskapvolgordenommer (msg_seqno)

'n 32-bis-getal gelykstaande aan twee keer die aantal "inhoudverwante" boodskappe (diegene wat erkenning vereis, en veral dié wat nie houers is nie) wat deur die sender voor hierdie boodskap geskep is en daarna met een verhoog is as die huidige boodskap 'n inhoudverwante boodskap. 'n Houer word altyd na sy hele inhoud gegenereer; daarom is sy rynommer groter as of gelyk aan die rynommers van die boodskappe wat daarin vervat is.

Watter soort sirkus is dit met 'n verhoging van 1, en dan nog 2? .. Ek vermoed dat die oorspronklike betekenis was "lae bietjie vir ACK, die res is 'n nommer", maar die resultaat is nie heeltemal reg nie - veral, dit blyk dat dit gestuur kan word sommige bevestigings wat dieselfde het seq_no! Hoe? Wel, byvoorbeeld, die bediener stuur vir ons iets, stuur, en ons self is stil, ons antwoord net met diensbevestigingsboodskappe oor die ontvangs van sy boodskappe. In hierdie geval sal ons uitgaande bevestigings dieselfde uitgaande nommer hê. As jy vertroud is met TCP en gedink het dat dit nogal mal klink, maar dit blyk nie baie wild te wees nie, want in TCP seq_no verander nie, en bevestiging gaan na seq_no die ander kant - dan haas ek my om te ontstel. Bevestigings kom na MTProto NIE op seq_no, soos in TCP, maar msg_id !

Wat is hierdie msg_id, die belangrikste van hierdie velde? Die unieke ID van die boodskap, soos die naam aandui. Dit word gedefinieer as 'n 64-bis-getal, waarvan die minste beduidende stukkies weer bediener-nie-bediener-magie het, en die res is 'n Unix-tydstempel, insluitend die breukdeel, wat 32 bisse na links verskuif is. Dié. tydstempel per se (en boodskappe met te verskillende tye sal deur die bediener verwerp word). Hieruit blyk dit dat dit oor die algemeen 'n identifiseerder is wat globaal vir die kliënt is. Terwyl - onthou session_id - ons is gewaarborg: Onder geen omstandighede kan 'n boodskap wat vir een sessie bedoel is, na 'n ander sessie gestuur word nie. Dit wil sê, dit blyk dat daar reeds is 3 vlak — sessie, sessienommer, boodskap-ID. Hoekom so 'n oorkomplikasie, hierdie raaisel is baie groot.

So, msg_id benodig vir...

RPC: versoeke, antwoorde, foute. Bevestigings.

Soos jy dalk opgemerk het, is daar geen spesiale tipe of funksie "maak 'n RPC-versoek" enige plek in die skema nie, alhoewel daar antwoorde is. Ons het immers inhoudverwante boodskappe! Dit wil sê, enige boodskap kan 'n versoek wees! Of nie wees nie. Na alles, elkeen daar is msg_id. En hier is die antwoorde:

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

Dit is hier waar aangedui word op watter boodskap dit 'n reaksie is. Daarom, op die boonste vlak van die API, sal jy moet onthou watter nommer jou versoek gehad het - ek dink dit is nie nodig om te verduidelik dat die werk asynchronies is nie, en daar kan verskeie versoeke op dieselfde tyd wees, die antwoorde waarop kan in enige volgorde terugbesorg word? In beginsel, hieruit, en foutboodskappe soos geen werkers nie, kan die argitektuur hieragter opgespoor word: die bediener wat 'n TCP-verbinding met jou onderhou, is 'n front-end-balanseerder, dit rig versoeke na backends en versamel dit saam message_id. Alles blyk duidelik, logies en goed hier te wees.

Ja?.. En as jy daaroor dink? Die RPC-reaksie self het immers ook 'n veld msg_id! Moet ons vir die bediener skree "jy reageer nie op my antwoord nie!"? En ja, wat was daar oor bevestiging? Oor bladsy boodskappe oor boodskappe vertel ons wat is

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

en elke kant moet dit doen. Maar nie altyd nie! As jy 'n RpcResult ontvang, dien dit op sigself as 'n erkenning. Dit wil sê, die bediener kan reageer op jou versoek met MsgsAck - soos, "Ek het dit ontvang." Kan dadelik RpcResult antwoord. Dit kan albei wees.

En ja, jy moet nog die antwoord beantwoord! Bevestiging. Andersins sal die bediener dit as onafgelewer beskou en dit weer vir jou uitgooi. Selfs na heraansluiting. Maar hier sal die kwessie van time-outs natuurlik ontstaan. Kom ons kyk 'n bietjie later na hulle.

Kom ons oorweeg intussen moontlike foute in die uitvoering van navraag.

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

O, sal iemand uitroep, hier is 'n meer menslike formaat - daar is 'n lyn! Vat jou tyd. Hier lys van foutemaar beslis nie volledig nie. Daaruit leer ons dat die kode − is iets soos HTTP-foute (wel, natuurlik, die semantiek van die antwoorde word nie gerespekteer nie, op sommige plekke word hulle ewekansig deur kodes versprei), en die string lyk soos CAPITAL_LETTERS_AND_NUMBERS. Byvoorbeeld, PHONE_NUMBER_OCCUPIED of FILE_PART_X_MISSING. Wel, dit wil sê, jy moet nog steeds hierdie lyn ontleed. Byvoorbeeld, FLOOD_WAIT_3600 sal beteken dat jy 'n uur moet wag, en PHONE_MIGRATE_5dat die telefoonnommer met hierdie voorvoegsel in die 5de DC geregistreer moet word. Ons het 'n tipe taal, reg? Ons het nie 'n argument van die string nodig nie, gereelde uitdrukkings sal doen, cho.

Weereens, dit is nie op die diensboodskap-bladsy nie, maar, soos reeds gebruiklik met hierdie projek, kan inligting gevind word op 'n ander dokumentasiebladsy. of agterdog wek. Eerstens, kyk, oortreding van tik/lae - RpcError belê kan word RpcResult. Hoekom nie buite nie? Wat het ons nie in ag geneem nie?.. Gevolglik, waar is die waarborg dat RpcError mag nie in belê word nie RpcResult, maar direk of geneste in 'n ander tipe wees? dit ontbreek req_msg_id ? ..

Maar kom ons gaan voort oor diensboodskappe. Die kliënt mag dink dat die bediener lank dink en so 'n wonderlike versoek rig:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Daar is drie moontlike antwoorde daarop, wat weereens met die bevestigingsmeganisme sny, om te probeer verstaan ​​wat dit moet wees (en wat is die lys tipes wat nie bevestiging in die algemeen vereis nie), word die leser as huiswerk gelaat (let wel: die inligting in die Telegram Desktop-bronne is nie volledig nie).

Verslawing: Boodskapposstatusse

Oor die algemeen laat baie plekke in TL, MTProto en Telegram in die algemeen 'n gevoel van koppigheid, maar uit beleefdheid, takt en ander sagte vaardighede ons het beleefd daaroor geswyg, en die obseniteite in die dialoë is gesensor. Hierdie plek egterОmeeste van die bladsy oor boodskappe oor boodskappe veroorsaak skok selfs vir my, wat al lank met netwerkprotokolle werk en fietse van verskillende grade van kromming gesien het.

Dit begin skadeloos, met bevestigings. Vervolgens word ons vertel van

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;

Wel, almal wat met MTProto begin werk, sal hulle moet in die gesig staar, in die “korrigeer - hersaamgestel - geloods” siklus, om nommerfoute of sout wat vrot geword het tydens wysigings, 'n algemene ding te kry. Daar is egter twee punte hier:

  1. Dit volg dat die oorspronklike boodskap verlore is. Ons moet 'n paar toue omhein, ons sal dit later oorweeg.
  2. Wat is daardie vreemde foutnommers? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64 … waar is die res van die nommers, Tommie?

Die dokumentasie lui:

Die bedoeling is dat error_code waardes gegroepeer word (error_code >> 4): byvoorbeeld, die kodes 0x40 - 0x4f stem ooreen met foute in houer ontbinding.

maar, eerstens, 'n verskuiwing in die ander rigting, en tweedens maak dit nie saak waar die res van die kodes is nie? In die skrywer se kop?.. Dit is egter onbenullighede.

Verslawing begin in posstatusboodskappe en poskopieë:

  • Versoek vir Boodskapstatusinligting
    As enige van die partye vir 'n rukkie nie inligting oor die status van sy uitgaande boodskappe ontvang het nie, kan dit uitdruklik van die ander party versoek word:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Inligtingsboodskap aangaande status van boodskappe
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Hier, info is 'n string wat presies een greep boodskapstatus vir elke boodskap van die inkomende msg_ids-lys bevat:

    • 1 = niks is bekend oor die boodskap nie (msg_id te laag, die ander party het dit dalk vergeet)
    • 2 = boodskap nie ontvang nie (msg_id val binne die reeks gestoorde identifiseerders; die ander party het egter beslis nie so 'n boodskap ontvang nie)
    • 3 = boodskap nie ontvang nie (msg_id te hoog; die ander party het dit egter beslis nog nie ontvang nie)
    • 4 = boodskap ontvang (let daarop dat hierdie antwoord ook terselfdertyd 'n ontvangserkenning is)
    • +8 = boodskap reeds erken
    • +16 = boodskap wat nie erkenning vereis nie
    • +32 = RPC-navraag vervat in boodskap wat verwerk word of verwerking reeds voltooi
    • +64 = inhoudverwante reaksie op boodskap wat reeds gegenereer is
    • +128 = ander party weet vir 'n feit dat die boodskap reeds ontvang is
      Hierdie antwoord vereis nie 'n erkenning nie. Dit is 'n erkenning van die relevante msgs_state_req, op sigself.
      Let daarop dat as dit skielik blyk dat die ander party nie 'n boodskap het wat lyk of dit na hom gestuur is nie, kan die boodskap eenvoudig weer gestuur word. Selfs al sou die ander party twee kopieë van die boodskap op dieselfde tyd ontvang, sal die duplikaat geïgnoreer word. (As te veel tyd verloop het, en die oorspronklike msg_id is nie meer geldig nie, moet die boodskap in msg_copy toegedraai word).
  • Vrywillige Kommunikasie van Status van Boodskappe
    Enige party kan die ander party vrywillig inlig oor die status van die boodskappe wat deur die ander party versend is.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Uitgebreide vrywillige kommunikasie van status van een boodskap
    ...
    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;
  • Eksplisiete versoek om boodskappe weer te stuur
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Die afgeleë party reageer onmiddellik deur die versoekte boodskappe weer te stuur […]
  • Eksplisiete versoek om antwoorde weer te stuur
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Die afgeleë party reageer dadelik deur weer te stuur antwoorde na die versoekte boodskappe […]
  • Boodskap kopieë
    In sommige situasies moet 'n ou boodskap met 'n msg_id wat nie meer geldig is nie, weer gestuur word. Dan word dit in 'n kopiehouer toegedraai:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Sodra dit ontvang is, word die boodskap verwerk asof die omhulsel nie daar was nie. As dit egter vir seker bekend is dat die boodskap orig_message.msg_id ontvang is, dan word die nuwe boodskap nie verwerk nie (terwyl dit en orig_message.msg_id terselfdertyd erken word). Die waarde van orig_message.msg_id moet laer wees as die houer se msg_id.

Laat ons selfs stilbly oor die feit dat in msgs_state_info weer, die ore van die onvoltooide TL steek uit (ons het 'n vektor van grepe nodig gehad, en in die onderste twee stukkies enum, en in die ouer bisse vlae). Die punt is iets anders. Verstaan ​​iemand hoekom dit alles in die praktyk is in regte kliënt nodig? .. Met moeite, maar jy kan jou 'n voordeel voorstel as 'n persoon besig is met ontfouting, en in 'n interaktiewe modus - vra die bediener wat en hoe. Maar versoeke word hier beskryf heen en weer.

Dit volg hieruit dat elke kant nie net boodskappe moet enkripteer en stuur nie, maar ook data daaroor, oor die antwoorde daarop, en vir 'n onbekende hoeveelheid tyd moet stoor. Die dokumentasie beskryf nie die tydsberekeninge of die praktiese toepaslikheid van hierdie kenmerke nie. op geen manier. Wat die verbasendste is, is dat hulle eintlik in die kode van amptelike kliënte gebruik word! Hulle is glo iets vertel wat nie in die oop dokumentasie ingesluit is nie. Verstaan ​​uit die kode hoekom, is nie meer so eenvoudig soos in die geval van TL nie - dit is nie 'n (vergelykende) logies geïsoleerde deel nie, maar 'n stuk wat aan die toepassingsargitektuur gekoppel is, d.w.s. sal baie meer tyd verg om die toepassingskode te verstaan.

Pings en tydsberekeninge. Toue.

Uit alles, as jy die raaiskote oor die bedienerargitektuur onthou (verspreiding van versoeke oor backends), volg 'n taamlik dowwe ding - ten spyte van al die waarborge van aflewering wat in TCP (óf die data is afgelewer, of jy sal ingelig word oor die breek, maar die data sal afgelewer word tot die oomblik van die probleem), dat bevestigings in MTProto self - geen waarborge nie. Die bediener kan maklik jou boodskap verloor of weggooi, en niks kan daaraan gedoen word nie, net om krukke van verskillende soorte omhein te maak.

En eerstens - boodskaprye. Wel, vir een ding, alles was duidelik van die begin af - 'n onbevestigde boodskap moet gestoor en gegrief word. En na watter tyd? En die nar ken hom. Miskien los daardie verslaafde diensboodskappe op een of ander manier hierdie probleem op met krukke, sê maar, in Telegram Desktop is daar ongeveer 4 toue wat daarmee ooreenstem (miskien meer, soos reeds genoem, hiervoor moet jy meer ernstig in die kode en argitektuur daarvan delf; terselfdertyd tyd, ons weet dat dit nie as 'n monster geneem kan word nie, 'n sekere aantal tipes van die MTProto-skema word nie daarin gebruik nie).

Hoekom gebeur dit? Waarskynlik kon die bedienerprogrammeerders nie betroubaarheid binne die groepering verseker nie, of ten minste selfs buffer op die voorste balanseerder, en het hierdie probleem na die kliënt verskuif. Vasily het uit desperaatheid probeer om 'n alternatiewe opsie te implementeer, met slegs twee toue, met behulp van algoritmes van TCP - meet RTT na die bediener en pas die "venster" grootte (in boodskappe) aan na gelang van die aantal nie-erkende versoeke. Dit wil sê, so 'n rowwe heuristiek om bedienerlading te skat - hoeveel van ons versoeke dit terselfdertyd kan kou en nie verloor nie.

Wel, dit is, jy verstaan, reg? As jy weer TCP moet implementeer bo-op 'n protokol wat oor TCP werk, dui dit op 'n baie swak ontwerpte protokol.

O ja, hoekom is meer as een tou nodig, en in die algemeen, wat beteken dit vir 'n persoon wat met 'n hoëvlak API werk? Kyk, jy rig 'n versoek, jy maak dit serialiseer, maar dit is dikwels onmoontlik om dit dadelik te stuur. Hoekom? Want die antwoord sal wees msg_id, wat tydelik isаEk is 'n etiket, waarvan die afspraak beter is om so laat as moontlik uit te stel - skielik sal die bediener dit verwerp as gevolg van 'n tydsverskil tussen ons en dit (natuurlik kan ons 'n kruk maak wat ons tyd van die hede verskuif by die bedienertyd deur 'n delta by te voeg wat uit die bedienerantwoorde bereken is - amptelike kliënte doen dit, maar hierdie metode is kru en onakkuraat as gevolg van buffering). So wanneer jy 'n versoek met 'n plaaslike funksie oproep van die biblioteek maak, gaan die boodskap deur die volgende stadiums:

  1. Lê in dieselfde tou en wag vir enkripsie.
  2. Aangestel msg_id en die boodskap het na 'n ander tou gegaan - moontlike aanstuur; stuur na sok.
  3. a) Die bediener het MsgsAck geantwoord - die boodskap is afgelewer, ons verwyder dit uit die "ander tou".
    b) Of andersom, hy het nie van iets gehou nie, antwoord hy badmsg - ons stuur weer uit die "ander tou"
    c) Niks is bekend nie, dit is nodig om die boodskap van 'n ander tou weer te stuur - maar dit is nie presies bekend wanneer nie.
  4. Bediener het uiteindelik geantwoord RpcResult - die werklike reaksie (of fout) - nie net afgelewer nie, maar ook verwerk.

Miskien, kan die gebruik van houers die probleem gedeeltelik oplos. Dit is wanneer 'n klomp boodskappe in een gepak word, en die bediener het geantwoord met 'n erkenning aan almal op een slag, met een msg_id. Maar hy sal ook hierdie pak verwerp, as iets verkeerd geloop het, ook die hele ding.

En op hierdie punt kom nie-tegniese oorwegings ter sprake. Uit ondervinding het ons baie krukke gesien, en boonop sal ons nou meer voorbeelde van slegte raad en argitektuur sien - is dit in sulke toestande die moeite werd om te vertrou en sulke besluite te neem? Die vraag is retories (natuurlik nie).

Waaroor praat ons? As jy oor die onderwerp "verslaafde boodskappe oor boodskappe" nog steeds kan spekuleer met besware soos "jy is dom, jy het nie ons briljante idee verstaan ​​nie!" (Skryf dus eers die dokumentasie, soos normale mense moet, met rasionaal en pakkie-uitruil voorbeelde, dan gesels ons), dan is tydsberekeninge / timeouts 'n suiwer praktiese en spesifieke kwessie, alles is lankal bekend hier. Maar wat sê die dokumentasie vir ons oor uitteltyd?

'n Bediener erken gewoonlik die ontvangs van 'n boodskap van 'n kliënt (gewoonlik 'n RPC-navraag) deur 'n RPC-antwoord te gebruik. As 'n antwoord nog lank kom, kan 'n bediener eers 'n ontvangserkenning stuur, en 'n bietjie later, die RPC-antwoord self.

'n Kliënt erken gewoonlik die ontvangs van 'n boodskap vanaf 'n bediener (gewoonlik 'n RPC-reaksie) deur 'n erkenning by die volgende RPC-navraag te voeg as dit nie te laat versend word nie (as dit byvoorbeeld 60-120 sekondes na die ontvangs gegenereer word van 'n boodskap vanaf die bediener). As daar egter vir 'n lang tydperk geen rede is om boodskappe na die bediener te stuur nie of as daar 'n groot aantal nie-erkende boodskappe vanaf die bediener is (sê meer as 16), stuur die kliënt 'n alleenstaande erkenning.

... Ek vertaal: ons self weet nie hoeveel en hoe dit nodig is nie, wel, kom ons skat dat laat dit so wees.

En oor pings:

Ping-boodskappe (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

'n Antwoord word gewoonlik na dieselfde verbinding teruggestuur:

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

Hierdie boodskappe vereis nie erkennings nie. 'n Pong word slegs in reaksie op 'n ping oorgedra, terwyl 'n ping deur weerskante geïnisieer kan word.

Uitgestelde verbindingsluiting + PING

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

Werk soos ping. Daarbenewens, nadat dit ontvang is, begin die bediener 'n timer wat die huidige verbinding disconnect_delay sekondes later sal sluit, tensy dit 'n nuwe boodskap van dieselfde tipe ontvang wat outomaties alle vorige timers terugstel. As die kliënt hierdie pings een keer elke 60 sekondes stuur, kan dit byvoorbeeld disconnect_delay gelyk stel aan 75 sekondes.

Is jy mal?! Binne 60 sekondes sal die trein die stasie binnegaan, passasiers aflaai en oplaai, en weer verbinding in die tonnel verloor. Oor 120 sekondes, terwyl jy rondkyk, sal hy by 'n ander een aankom, en die verbinding sal heel waarskynlik verbreek. Wel, dit is duidelik waar die bene vandaan groei - "Ek het 'n lui gehoor, maar ek weet nie waar dit is nie", daar is die Nagle-algoritme en die TCP_NODELAY-opsie, wat bedoel was vir interaktiewe werk. Maar, jammer, vertraag die verstekwaarde daarvan - 200 Millisekondes. As jy regtig iets soortgelyks wil uitbeeld en op 'n moontlike paar pakkies wil stoor - wel, stel dit af, ten minste vir 5 sekondes, of wat ook al die uitteltyd van die boodskap "Gebruiker tik ..." is nou gelyk aan. Maar nie meer nie.

En uiteindelik, pings. Dit wil sê om die lewendheid van 'n TCP-verbinding na te gaan. Dis snaaks, maar ek het so 10 jaar gelede 'n kritiese teks geskryf oor die boodskapper van die koshuis van ons fakulteit - daar het die skrywers ook die bediener van die kliënt geping, en nie andersom nie. Maar derdejaarstudente is een ding, en 'n internasionale kantoor is 'n ander, reg? ..

Eerstens 'n klein opvoedkundige program. 'n TCP-verbinding, in die afwesigheid van pakkieuitruiling, kan weke lank leef. Dit is beide goed en sleg, afhangende van die doel. Wel, as jy 'n SSH-verbinding met die bediener oop gehad het, het jy van jou rekenaar af opgestaan, die kragroeteerder herlaai, na jou plek teruggekeer - die sessie deur hierdie bediener het nie gebreek nie (het niks getik nie, daar was geen pakkies nie), gerieflik. Dit is sleg as daar duisende kliënte op die bediener is, elkeen neem hulpbronne op (hallo Postgres!), en die kliëntgasheer het dalk lankal herlaai - maar ons sal nie daarvan weet nie.

Klets/IM-stelsels behoort tot die tweede geval vir 'n ander, bykomende rede - aanlynstatusse. As die gebruiker "afgeval" het, is dit nodig om sy gespreksgenote daaroor in te lig. Andersins sal daar 'n fout wees wat die skeppers van Jabber gemaak het (en vir 20 jaar reggestel het) - die gebruiker het ontkoppel, maar hulle gaan voort om boodskappe aan hom te skryf en glo dat hy aanlyn is (wat ook heeltemal verlore was in hierdie paar minute tevore die breek is ontdek). Nee, die TCP_KEEPALIVE-opsie, wat baie mense wat nie verstaan ​​hoe TCP-timers werk nie, oral verskyn (deur wilde waardes soos tientalle sekondes in te stel), sal nie hier help nie - jy moet seker maak dat nie net die OS-kern van die gebruiker se masjien is lewendig, maar funksioneer ook normaal, in staat om te antwoord, en die toepassing self (dink jy dit kan nie vries nie? Telegram Desktop op Ubuntu 18.04 het herhaaldelik vir my neergestort).

Dit is hoekom jy moet ping bediener kliënt, en nie omgekeerd nie - as die kliënt dit doen, wanneer die verbinding verbreek word, sal die ping nie afgelewer word nie, die doelwit word nie bereik nie.

En wat sien ons in Telegram? Alles is presies die teenoorgestelde! Wel, d.w.s. formeel kan albei kante mekaar natuurlik ping. In die praktyk gebruik kliënte 'n kruk ping_delay_disconnect, wat 'n timer op die bediener aanskakel. Wel, jammer, dit is nie die kliënt se saak om te besluit hoe lank hy sonder ping daar wil bly nie. Die bediener, gebaseer op sy lading, weet beter. Maar, natuurlik, as jy nie jammer voel vir die hulpbronne nie, dan is die bose Pinocchio hulself, en die kruk sal afkom ...

Hoe moes dit ontwerp gewees het?

Ek glo dat die bogenoemde feite baie duidelik dui op die nie baie hoë bevoegdheid van die Telegram / VKontakte-span op die gebied van die vervoer (en laer) vlak van rekenaarnetwerke en hul lae kwalifikasie in relevante sake.

Hoekom het dit so ingewikkeld geword, en hoe kan Telegram-argitekte probeer om beswaar te maak? Die feit dat hulle probeer het om 'n sessie te maak wat TCP-verbinding oorleef, breek, dit wil sê wat ons nie nou gelewer het nie, sal ons later lewer. Hulle het waarskynlik ook probeer om UDP-vervoer te maak, al het hulle probleme ondervind en dit laat vaar (dis hoekom die dokumentasie leeg is - daar was niks om mee te spog nie). Maar as gevolg van 'n gebrek aan begrip van hoe netwerke in die algemeen en TCP in die besonder werk, waar jy daarop kan staatmaak, en waar jy dit self moet doen (en hoe), en probeer om dit met kriptografie te kombineer "een skoot van twee voëls met een klip” - so 'n kadawer het geblyk.

Hoe moes dit gewees het? Gebaseer op die feit dat msg_id is 'n tydstempel wat kriptografies nodig is om herhalingsaanvalle te voorkom, is dit 'n fout om 'n unieke identifiseerderfunksie daaraan te heg. Daarom, sonder om die huidige argitektuur drasties te verander (wanneer die Opdaterings-draad gevorm word, is dit 'n hoëvlak API-onderwerp vir 'n ander deel van hierdie reeks plasings), sal 'n mens moet:

  1. Die bediener wat die TCP-verbinding met die kliënt hou, neem verantwoordelikheid - as jy van die sok afgetrek het, erken, verwerk of stuur 'n fout asseblief, geen verlies nie. Dan is die bevestiging nie 'n vektor van id's nie, maar bloot "the last receive seq_no" - net 'n nommer, soos in TCP (twee nommers - jou eie volg en bevestig). Ons is altyd in sessie, is ons nie?
  2. Die tydstempel om herhalingsaanvalle te voorkom, word 'n aparte veld, a la nonce. Gekontroleer, maar niks anders word geraak nie. Genoeg en uint32 - as ons sout ten minste elke halwe dag verander, kan ons 16 bisse toewys aan die onderste stukkies van die heelgetaldeel van die huidige tyd, die res - aan die breukdeel van 'n sekonde (soos dit nou is).
  3. Teruggetrek msg_id enigsins - vanuit die oogpunt van onderskeidende versoeke op die backends, is daar eerstens die kliënt-ID, en tweedens die sessie-ID, en koppel hulle aan. Gevolglik, as 'n versoekidentifiseerder, is slegs een genoeg seq_no.

Ook nie die beste opsie nie, 'n volledige ewekansige kan as 'n identifiseerder dien - dit word terloops reeds in die hoëvlak-API gedoen wanneer 'n boodskap gestuur word. Dit sal beter wees om die argitektuur heeltemal te verander van relatief na absoluut, maar dit is 'n onderwerp vir 'n ander deel, nie hierdie pos nie.

API?

Ta-daam! Dus, nadat ons deur 'n pad vol pyn en krukke gebaan het, kon ons uiteindelik enige versoeke na die bediener stuur en enige antwoorde daarop ontvang, asook opdaterings van die bediener ontvang (nie in reaksie op 'n versoek nie, maar dit stuur vir ons self, soos PUSH, as iemand soveel duideliker).

Aandag, nou sal daar die enigste Perl-voorbeeld in die artikel wees! (vir diegene wat nie vertroud is met die sintaksis nie, is die eerste argument om te seën die objek se datastruktuur, die tweede is sy klas):

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

Ja, veral nie onder die bederf nie – as jy dit nie gelees het nie, gaan doen dit!

O, wag~~... hoe lyk dit? Iets baie bekend ... miskien is dit die datastruktuur van 'n tipiese Web API in JSON, behalwe dat klasse dalk aan voorwerpe geheg is? ..

So dit blyk ... Wat is dit, kamerade? .. Soveel moeite - en ons het gestop om te rus waar die webprogrammeerders net begin?.. Sou net JSON oor HTTPS nie makliker wees nie?! En wat het ons in ruil gekry? Was hierdie pogings die moeite werd?

Kom ons evalueer wat TL+MTProto ons gegee het en watter alternatiewe moontlik is. Wel, HTTP-versoekreaksie pas nie goed nie, maar ten minste iets bo-op TLS?

kompakte serialisering. As u hierdie datastruktuur sien, soortgelyk aan JSON, word onthou dat daar sy binêre variante is. Kom ons merk MsgPack as onvoldoende uitbreidbaar, maar daar is byvoorbeeld CBOR - terloops, die standaard beskryf in RFC 7049. Dit is opmerklik vir die feit dat dit definieer etikette, as 'n verlengingsmeganisme, en onder reeds gestandaardiseer daar is:

  • 25 + 256 - vervanging van duplikaatlyne met 'n lynnommerverwysing, so 'n goedkoop kompressiemetode
  • 26 - serialized Perl voorwerp met klas naam en konstruktor argumente
  • 27 - serialized taal-onafhanklike objek met tipe naam en konstruktor argumente

Wel, ek het probeer om dieselfde data in TL en CBOR te serialiseer met die verpakking van stringe en voorwerpe geaktiveer. Die resultaat het iewers van 'n megagreep ten gunste van CBOR begin verskil:

cborlen=1039673 tl_len=1095092

So, gevolgtrekking: Daar is aansienlik eenvoudiger formate wat nie onderhewig is aan die sinkroniseringsfout of onbekende identifiseerderprobleem nie, met vergelykbare doeltreffendheid.

Vinnige verbinding vestiging. Dit beteken nul RTT na herverbinding (wanneer die sleutel reeds een keer gegenereer is) - van toepassing vanaf die heel eerste MTProto-boodskap, maar met 'n paar voorbehoude - hulle het in dieselfde sout beland, die sessie het nie vrot geraak nie, ens. Wat bied TLS ons in ruil daarvoor? Verwante aanhaling:

Wanneer PFS in TLS gebruik word, TLS-sessiekaartjies (RFC 5077) om die geënkripteerde sessie te hervat sonder om die sleutels te heronderhandel en sonder om die sleutelinligting op die bediener te stoor. Wanneer die eerste verbinding oopgemaak word en sleutels gegenereer word, enkripteer die bediener die toestand van die verbinding en stuur dit na die kliënt (in die vorm van 'n sessiekaartjie). Gevolglik, wanneer die verbinding hervat word, stuur die kliënt 'n sessiekaartjie wat onder andere die sessiesleutel bevat, terug na die bediener. Die kaartjie self word geïnkripteer met 'n tydelike sleutel (sessiekaartjiesleutel), wat op die bediener gestoor word en moet versprei word na alle frontend-bedieners wat SSL in gegroepeerde oplossings hanteer.[10]. Dus, die bekendstelling van 'n sessiekaartjie kan PFS oortree as tydelike bedienersleutels gekompromitteer word, byvoorbeeld wanneer dit vir 'n lang tyd gestoor word (OpenSSL, nginx, Apache stoor dit by verstek vir die hele tyd wat die program loop; gewilde werwe gebruik die sleutel vir 'n paar uur, tot dae).

Hier is RTT nie nul nie, jy moet ten minste ClientHello en ServerHello uitruil, waarna, saam met Finished, die kliënt reeds data kan stuur. Maar hier moet onthou word dat ons nie die Web, met sy klomp nuut oopgemaakte verbindings het nie, maar 'n boodskapper, waarvan die verbinding dikwels een en min of meer langlewende, relatief kort versoeke vir webblaaie is - alles is binne vermenigvuldig. Dit wil sê, dit is heel aanvaarbaar, as ons nie op 'n baie slegte moltrein-gedeelte afgekom het nie.

Iets anders vergeet? Skryf in die kommentaar.

Vervolg!

In die tweede deel van hierdie reeks plasings sal ons organisatoriese kwessies eerder as tegniese kwessies oorweeg – benaderings, ideologie, koppelvlak, houding teenoor gebruikers, ens. Gebaseer egter op die tegniese inligting wat hier aangebied is.

Die derde deel sal voortgaan met die ontleding van die tegniese komponent / ontwikkelingservaring. Jy sal veral leer:

  • voortsetting van die pandemonium met die verskeidenheid TL-tipes
  • onbekende dinge oor kanale en supergroepe
  • as dialoog is erger as rooster
  • oor absolute vs relatiewe boodskapadressering
  • wat is die verskil tussen foto en beeld
  • hoe emoji inmeng met skuinsgedrukte teks

en ander krukke! Bly ingeskakel!

Bron: will.com

Voeg 'n opmerking