Kritiko de la protokolo kaj organizaj aliroj de Telegramo. Parto 1, teknika: sperto verki klienton de nulo - TL, MT

Lastatempe, afiŝoj pri kiom bona estas Telegramo, kiom brilaj kaj spertaj la fratoj Durov estas en konstruado de retaj sistemoj ktp. komencis aperi pli ofte ĉe Habré. Samtempe tre malmultaj homoj vere mergis sin en la teknikan aparaton - maksimume ili uzas sufiĉe simplan (kaj sufiĉe malsaman al MTProto) JSON-bazitan Bot-API, kaj kutime nur akceptas. pri fido ĉiuj laŭdoj kaj PR, kiuj rondiras ĉirkaŭ la mesaĝisto. Antaŭ preskaŭ jaro kaj duono, mia kolego ĉe la NRO Eshelon Vasilij (bedaŭrinde lia konto pri Habré estis forviŝita kune kun la malneto) komencis verki sian propran Telegram-klienton de nulo en Perl, kaj poste la aŭtoro de ĉi tiuj linioj aliĝis. Kial Perl, iuj tuj demandos? Ĉar tiaj projektoj jam ekzistas en aliaj lingvoj.Efektive, tio ne estas la afero, povus ekzisti iu alia lingvo kie ne ekzistas. preta biblioteko, kaj sekve la aŭtoro devas iri la tutan vojon de nulo. Plie, kriptografio estas demando de fido, sed kontrolu. Kun produkto celanta sekurecon, vi ne povas simple fidi je preta biblioteko de la fabrikanto kaj blinde fidi ĝin (tamen ĉi tio estas temo por la dua parto). Nuntempe, la biblioteko funkcias sufiĉe bone ĉe la "averaĝa" nivelo (ebligas al vi fari ajnajn API-petojn).

Tamen, ne estos multe da kripto aŭ matematiko en ĉi tiu serio de afiŝoj. Sed estos multaj aliaj teknikaj detaloj kaj arkitekturaj lambastonoj (utilaj ankaŭ por tiuj, kiuj ne skribos de nulo, sed uzos la bibliotekon en iu ajn lingvo). Do, la ĉefa celo estis provi efektivigi la klienton de nulo laŭ oficiala dokumentaro. Tio estas, ni supozu, ke la fontkodo de oficialaj klientoj estas fermita (denove, en la dua parto ni kovros pli detale la temon pri tio, ke tio estas vera ĝi okazas do), sed, kiel en la malnova tempo, ekzemple, ekzistas normo kiel RFC - ĉu eblas skribi klienton laŭ la specifo sole, "sen rigardi" la fontkodon, ĉu ĝi estas oficiala (Telegram Desktop, poŝtelefono), aŭ neoficiala Teletono?

Enhavtabelo:

Dokumentado... ĝi ekzistas, ĉu ne? Ĉu vere?..

Fragmentoj de notoj por ĉi tiu artikolo komencis esti kolektitaj lastan someron. Dum ĉi tiu tempo en la oficiala retejo https://core.telegram.org La dokumentaro estis ekde Tavolo 23, t.e. algluiĝis ie en 2014 (memoru, tiam eĉ ne estis kanaloj?). Kompreneble, en teorio, ĉi tio devus esti permesinta al ni efektivigi klienton kun funkcieco tiutempe en 2014. Sed eĉ en ĉi tiu stato, la dokumentado estis, unue, nekompleta, kaj due, kelkloke ĝi kontraŭdiris sin. Antaŭ iom pli ol monato, en septembro 2019, ĝi estis hazarde Estis malkovrite, ke estis granda ĝisdatigo de la dokumentaro en la retejo, por la sufiĉe lastatempa Tavolo 105, kun noto, ke nun ĉio devas esti legita denove. Ja multaj artikoloj estis reviziitaj, sed multaj restis senŝanĝaj. Sekve, legante la malsupran kritikon pri la dokumentado, vi devas memori, ke iuj el ĉi tiuj aferoj ne plu rilatas, sed iuj ankoraŭ sufiĉe. Post ĉio, 5 jaroj en la moderna mondo ne estas nur longa tempo, sed tre multe da. Ekde tiuj tempoj (precipe se vi ne konsideras la forĵetitajn kaj revivigitajn geobabilejojn ekde tiam), la nombro da API-metodoj en la skemo kreskis de cent al pli ol ducent kvindek!

Kie komenci kiel juna aŭtoro?

Ne gravas ĉu vi skribas de nulo aŭ uzas, ekzemple, pretajn bibliotekojn kiel Telethon por PythonMadeline por PHP, ĉiukaze, vi bezonos unue registri vian kandidatiĝon - akiri parametrojn api_id и api_hash (tiuj, kiuj laboris kun la VKontakte API tuj komprenas) per kiu la servilo identigos la aplikaĵon. Ĉi tio devas faru ĝin pro juraj kialoj, sed ni parolos pli pri kial bibliotekaŭtoroj ne povas publikigi ĝin en la dua parto. Vi povas esti kontenta pri la testvaloroj, kvankam ili estas tre limigitaj - la fakto estas, ke nun vi povas registriĝi nur unu app, do ne rapidu al ĝi.

Nun, el teknika vidpunkto, ni interesiĝu pri tio, ke post registriĝo ni ricevu sciigojn de Telegram pri ĝisdatigoj de dokumentado, protokolo ktp. Tio estas, oni povus supozi, ke la retejo kun la dokoj estis simple forlasita kaj daŭre laboris specife kun tiuj, kiuj komencis fari klientojn, ĉar estas pli facile. Sed ne, nenio tia estis observita, neniu informo venis.

Kaj se vi skribas de nulo, tiam uzi la akiritajn parametrojn efektive estas ankoraŭ tre malproksima. Kvankam https://core.telegram.org/ kaj parolas pri ili en Komenci antaŭ ĉio, fakte, vi unue devos efektivigi MTProto-protokolo — sed se vi kredus aranĝo laŭ la OSI-modelo ĉe la fino de la paĝo por ĝenerala priskribo de la protokolo, tiam ĝi estas tute vana.

Fakte, kaj antaŭ kaj post MTProto, je pluraj niveloj samtempe (kiel diras eksterlandaj retoj laborantaj en la OS-kerno, tavola malobservo), granda, dolora kaj terura temo enmiksos...

Binara seriigo: TL (Tipa Lingvo) kaj ĝia skemo, kaj tavoloj, kaj multaj aliaj timigaj vortoj

Ĉi tiu temo, fakte, estas la ŝlosilo al la problemoj de Telegramo. Kaj estos multaj teruraj vortoj, se vi provos enprofundiĝi en ĝi.

Do, jen la diagramo. Se ĉi tiu vorto venas al via menso, diru: JSON-Skemo, Vi pensis ĝuste. La celo estas la sama: iu lingvo por priskribi eblan aron de transdonitaj datumoj. Ĉi tie finiĝas la similecoj. Se de la paĝo MTProto-protokolo, aŭ el la fontarbo de la oficiala kliento, ni provos malfermi iun skemon, ni vidos ion kiel:

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;

Persono, kiu vidas tion la unuan fojon, intuicie povos rekoni nur parton de tio, kio estas skribita - nu, ĉi tiuj ŝajne estas strukturoj (kvankam kie estas la nomo, maldekstre aŭ dekstre?), estas kampoj en ili, post kiu tipo sekvas post dupunkto... verŝajne. Ĉi tie en angulaj krampoj verŝajne estas ŝablonoj kiel en C++ (fakte, ne tute). Kaj kion signifas ĉiuj aliaj simboloj, demandosignoj, ekkriaj signoj, procentoj, hashsignoj (kaj evidente ili signifas malsamajn aferojn en diversaj lokoj), foje ĉeestantaj kaj foje ne, deksesumaj nombroj - kaj plej grave, kiel akiri de ĉi tio la ĝusta (kiu ne estos malakceptita de la servilo) bajta fluo? Vi devos legi la dokumentaron (jes, estas ligiloj al la skemo en la JSON-versio proksime - sed tio ne pliklarigas ĝin).

Malfermu la paĝon Seriligo de Binaraj Datumoj kaj plonĝu en la magian mondon de fungoj kaj diskreta matematiko, ion similan al matan en la 4-a jaro. Alfabeto, tipo, valoro, kombinilo, funkcia kombinilo, normala formo, kunmetita tipo, polimorfa tipo... kaj tio estas nur la unua paĝo! Poste atendas vin TL Lingvo, kiu, kvankam ĝi jam enhavas ekzemplon de bagatela peto kaj respondo, tute ne donas respondon al pli tipaj kazoj, kio signifas, ke vi devos vadi tra rerakonto de matematiko tradukita el la rusa en la anglan sur aliaj ok enmetitaj. paĝoj!

Legantoj konataj kun funkciaj lingvoj kaj aŭtomata tipa inferenco kompreneble vidos la priskriban lingvon en ĉi tiu lingvo, eĉ de la ekzemplo, kiel multe pli konata, kaj povas diri, ke tio fakte ne estas malbona principe. La obĵetoj kontraŭ tio estas:

  • jes, la celo sonas bone, sed ve, ŝi ne atingita
  • Eduko en rusaj universitatoj varias eĉ inter IT-specialaĵoj - ne ĉiuj sekvis la respondan kurson
  • Fine, kiel ni vidos, praktike ĝi estas ne postulita, ĉar nur limigita subaro de eĉ la TL kiu estis priskribita estas uzita

Kiel dirite Leonerd sur la kanalo #perl en la FreeNode IRC-reto, kiu provis efektivigi pordegon de Telegram al Matrix (traduko de la citaĵo estas malpreciza de memoro):

Ŝajnas, ke iu unuafoje estis prezentita al tajpa teorio, ekscitiĝis kaj komencis provi ludi kun ĝi, ne vere zorgante ĉu ĝi estas bezonata en la praktiko.

Vidu mem, se la bezono de nudaj tipoj (int, long, ktp.) kiel io elementa ne levas demandojn - finfine ili devas esti efektivigitaj permane - ekzemple, ni provu derivi el ili. vektoro. Tio estas, fakte, tabelo, se vi nomas la rezultajn aferojn per siaj propraj nomoj.

Sed antaŭe

Mallonga priskribo de subaro de TL-sintakso por tiuj, kiuj ne legas la oficialan dokumentaron

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;

Difino ĉiam komenciĝas konstruisto, post kio laŭvole (praktike - ĉiam) per la simbolo # devas esti CRC32 de la normaligita priskriba ĉeno de ĉi tiu tipo. Poste venas priskribo de la kampoj; se ili ekzistas, la tipo povas esti malplena. Ĉi ĉio finiĝas per egala signo, la nomo de la tipo al kiu ĉi tiu konstrukciisto - tio estas, fakte, la subtipo - apartenas. La ulo dekstre de la egalsigno estas polimorfa — tio estas, pluraj specifaj tipoj povas respondi al ĝi.

Se la difino okazas post la linio ---functions---, tiam la sintakso restos la sama, sed la signifo estos malsama: la konstrukciisto fariĝos la nomo de la RPC-funkcio, la kampoj fariĝos parametroj (nu, tio estas, ĝi restos ĝuste la sama donita strukturo, kiel priskribite sube. , ĉi tio simple estos la asignita signifo), kaj la "polimorfa tipo" - la tipo de la redonita rezulto. Vere, ĝi ankoraŭ restos polimorfa - ĵus difinita en la sekcio ---types---, sed ĉi tiu konstrukciisto "ne estos konsiderata". Troŝarĝi la tipojn de nomitaj funkcioj per iliaj argumentoj, t.e. Ial, pluraj funkcioj kun la sama nomo sed malsamaj subskriboj, kiel en C++, ne estas antaŭviditaj en la TL.

Kial "konstruisto" kaj "polimorfa" se ĝi ne estas OOP? Nu, fakte, estos pli facile por iu pensi pri tio en OOP terminoj - polimorfa tipo kiel abstrakta klaso, kaj konstruiloj estas ĝiaj rektaj posteulklasoj, kaj final en la terminologio de kelkaj lingvoj. Fakte, kompreneble, nur ĉi tie simileco kun veraj troŝarĝitaj konstruaĵmetodoj en OO programlingvoj. Ĉar ĉi tie estas nur datumstrukturoj, ne ekzistas metodoj (kvankam la priskribo de funkcioj kaj metodoj plue sufiĉe kapablas krei konfuzon en la kapo, ke ili ekzistas, sed tio estas malsama afero) - vi povas pensi pri konstrukciisto kiel valoro de kiu estas konstruata tajpu dum legado de bajta fluo.

Kiel tio okazas? La deserialigilo, kiu ĉiam legas 4 bajtojn, vidas la valoron 0xcrc32 - kaj komprenas kio okazos poste field1 kun tipo int, t.e. legas ĝuste 4 bajtojn, sur ĉi tiu la supra kampo kun la tipo PolymorType legi. Vidas 0x2crc32 kaj komprenas, ke estas du kampoj plu, unue long, kio signifas, ke ni legas 8 bajtojn. Kaj tiam denove kompleksa tipo, kiu estas deserialigita en la sama maniero. Ekzemple, Type3 povus esti deklarita en la cirkvito tuj kiam du konstrukciistoj, respektive, tiam ili devas renkonti ĉu 0x12abcd34, post kio vi devas legi 4 pliajn bajtojn int0x6789cdef, post kio estos nenio. Io ajn alia - vi devas ĵeti escepton. Ĉiuokaze, post ĉi tio ni reiras al legado de 4 bajtoj int kampoj field_c в constructorTwo kaj kun tio ni finas legi nian PolymorType.

Fine, se oni kaptos vin 0xdeadcrc por constructorThree, tiam ĉio fariĝas pli komplika. Nia unua kampo estas bit_flags_of_what_really_present kun tipo # - fakte, ĉi tio estas nur kaŝnomo por la tipo nat, kun la signifo "natura nombro". Tio estas, fakte, sensigna int estas, cetere, la nura kazo kiam sensignaj nombroj okazas en realaj cirkvitoj. Do, sekvas konstruo kun demandosigno, signifante ke ĉi tiu kampo - ĝi ĉeestos sur la drato nur se la responda bito estas fiksita en la kampo referita (proksimume kiel ternara operatoro). Do, ni supozu, ke ĉi tiu bito estis agordita, kio signifas, ke plu ni devas legi kampon kiel Type, kiu en nia ekzemplo havas 2 konstruilojn. Unu estas malplena (konsistas nur el la identigilo), la alia havas kampon ids kun tipo ids:Vector<long>.

Vi povus pensi, ke kaj ŝablonoj kaj generikoj estas en la avantaĝoj aŭ Java. Sed ne. Preskaŭ. Ĉi tio la sola kazo de uzado de angulaj krampoj en realaj cirkvitoj, kaj ĝi estas uzata NUR por Vektoro. En bajta fluo, ĉi tiuj estos 4 CRC32 bajtoj por la Vektora tipo mem, ĉiam la sama, tiam 4 bajtoj - la nombro da tabelelementoj, kaj tiam ĉi tiuj elementoj mem.

Aldonu al tio, ke seriigo ĉiam okazas en vortoj de 4 bajtoj, ĉiuj tipoj estas multobloj de ĝi - ankaŭ la enkonstruitaj tipoj estas priskribitaj. bytes и string kun mana seriigo de la longo kaj ĉi tiu vicigo je 4 - nu, ŝajnas soni normale kaj eĉ relative efika? Kvankam TL estas asertita esti efika binara seriigo, sed al la diablo kun ili, kun la ekspansio de preskaŭ io ajn, eĉ Buleaj valoroj kaj unu-karakteraj ĉenoj al 4 bajtoj, ĉu JSON ankoraŭ estos multe pli dika? Rigardu, eĉ nenecesaj kampoj povas esti preterlasitaj per bitaj flagoj, ĉio estas sufiĉe bona, kaj eĉ etendebla por la estonteco, do kial ne aldoni poste novajn laŭvolajn kampojn al la konstruilo?..

Sed ne, se vi legas ne mian mallongan priskribon, sed la plenan dokumentadon, kaj pripensu la efektivigon. Unue, la CRC32 de la konstrukciisto estas kalkulita laŭ la normaligita linio de la teksta priskribo de la skemo (forigi ekstran blankspacon, ktp.) - do se nova kampo estas aldonita, la tipa priskribo linio ŝanĝiĝos, kaj tial ĝia CRC32 kaj , sekve, seriigo. Kaj kion farus la malnova kliento se li ricevus kampon kun novaj flagoj starigitaj, kaj li ne scias kion fari kun ili poste?...

Due, ni memoru CRC32, kiu estas uzata ĉi tie esence kiel haŝfunkcioj por unike determini, kia tipo estas (de)seriagata. Ĉi tie ni estas antaŭ la problemo de kolizioj - kaj ne, la probablo ne estas unu el 232, sed multe pli granda. Kiu memoris, ke CRC32 estas desegnita por detekti (kaj korekti) erarojn en la komunika kanalo, kaj sekve plibonigas ĉi tiujn ecojn en malutilo de aliaj? Ekzemple, ĝi ne zorgas pri rearanĝo de bajtoj: se vi kalkulas CRC32 el du linioj, en la dua vi interŝanĝas la unuajn 4 bajtojn kun la sekvaj 4 bajtoj - estos la sama. Kiam nia enigo estas tekstaj ĉenoj el la latina alfabeto (kaj iom da interpunkcio), kaj tiuj nomoj ne estas precipe hazardaj, la probableco de tia rearanĝo multe pliiĝas.

Cetere, kiu kontrolis kio estis tie? vere CRC32? Unu el la fruaj fontkodoj (eĉ antaŭ Waltman) havis hash-funkcion kiu multobligis ĉiun signon per la numero 239, tiel amata de ĉi tiuj homoj, ha ha!

Fine, bone, ni rimarkis, ke konstrukciistoj kun kampotipo Vector<int> и Vector<PolymorType> havos malsaman CRC32. Kio pri enreta agado? Kaj el teoria vidpunkto, ĉu ĉi tio fariĝas parto de la tipo? Ni diru, ke ni pasas tabelon de dek mil nombroj, bone kun Vector<int> ĉio estas klara, la longeco kaj aliaj 40000 bajtoj. Kio se ĉi tio Vector<Type2>, kiu konsistas el nur unu kampo int kaj ĝi estas sola en la tipo - ĉu ni bezonas ripeti 10000xabcdef0 34 4 fojojn kaj poste XNUMX bajtojn int, aŭ la lingvo kapablas SENDEPENDI ĝin por ni de la konstruilo fixedVec kaj anstatau 80000 bajtoj, transigi denove nur 40000?

Ĉi tio tute ne estas senutila teoria demando - imagu, ke vi ricevas liston de grupuzantoj, ĉiu el kiuj havas identigilon, antaŭnomon, familian nomon - la diferenco en la kvanto da datumoj transdonitaj per poŝtelefona konekto povas esti grava. Ĝuste la efikeco de Telegram-serialigo estas reklamita al ni.

Do…

Vektoro, kiu neniam estis liberigita

Se vi provas travadi la paĝojn de priskribo de kombinistoj kaj tiel plu, vi vidos ke vektoro (kaj eĉ matrico) formale provas esti eligita tra opoj de pluraj folioj. Sed finfine ili forgesas, la fina paŝo estas preterlasita, kaj difino de vektoro estas simple donita, kiu ankoraŭ ne estas ligita al tipo. Kio estas la problemo? En lingvoj programado, precipe funkciaj, estas sufiĉe tipe priskribi la strukturon rekursie - la kompililo kun sia maldiligenta taksado komprenos kaj faros ĉion mem. En lingvo serialigo de datumoj kio necesas estas Efikeco: sufiĉas simple priskribi listo, t.e. strukturo de du elementoj - la unua estas datenelemento, la dua estas la sama strukturo mem aŭ malplena spaco por la vosto (pako (cons) en Lisp). Sed ĉi tio evidente postulos ĉiu elemento elspezas pliajn 4 bajtojn (CRC32 en la kazo en TL) por priskribi ĝian tipon. Tabelo ankaŭ povas esti facile priskribita fiksa grandeco, sed en la kazo de tabelo de nekonata longo anticipe, ni derompas.

Tial, ĉar TL ne permesas eligi vektoron, ĝi devis esti aldonita flanke. Finfine la dokumentaro diras:

Seriigo ĉiam uzas la saman konstruilon "vektoro" (const 0x1cb5c415 = crc32 ("vektoro t:Type # [ t ] = Vektora t") kiu ne dependas de la specifa valoro de la variablo de tipo t.

La valoro de la laŭvola parametro t ne estas engaĝita en la seriigo ĉar ĝi estas derivita de la rezultspeco (ĉiam konata antaŭ deseriigo).

Rigardu pli detale: vector {t:Type} # [ t ] = Vector t - sed nenie Ĉi tiu difino mem ne diras, ke la unua nombro devas esti egala al la longo de la vektoro! Kaj ĝi ne venas de ie ajn. Ĉi tio estas donita, kiu devas esti memorita kaj efektivigita per viaj manoj. Aliloke, la dokumentaro eĉ honeste mencias, ke la tipo ne estas reala:

La polimorfa pseŭdotipo de Vektora t estas "tipo" kies valoro estas sekvenco de valoroj de iu ajn tipo t, ĉu boksita aŭ nuda.

... sed ne koncentriĝas pri ĝi. Kiam vi, laca vadi tra la streĉado de matematiko (eble eĉ konata de vi el universitata kurso), decidas rezigni kaj efektive rigardi kiel labori kun ĝi praktike, la impreso restas en via kapo, ke tio estas Grava. Matematiko ĉe la kerno, ĝi estis klare inventita de Cool People (du matematikistoj - ACM-gajninto), kaj ne nur iu ajn. La celo - montri - estas atingita.

Cetere, pri la nombro. Ni memorigu tion al vi # ĝi estas sinonimo nat, natura nombro:

Estas tipesprimoj (tipo-espr) kaj nombraj esprimoj (nat-espr). Tamen, ili estas difinitaj same.

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

sed en la gramatiko ili estas priskribitaj sammaniere, t.e. Ĉi tiu diferenco denove devas esti memorita kaj metita en efektivigon permane.

Nu, jes, ŝablonspecoj (vector<int>, vector<User>) havas komunan identigilon (#1cb5c415), t.e. se vi scias, ke la voko estas anoncita kiel

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

tiam vi ne plu atendas nur vektoron, sed vektoron de uzantoj. Pli precize, devus atendu - en reala kodo, ĉiu elemento, se ne nuda tipo, havos konstruilon, kaj en bona maniero en efektivigo estus necese kontroli - sed ni estis senditaj ĝuste en ĉiu elemento de ĉi tiu vektoro tiu tipo? Kio se ĝi estus ia PHP, en kiu tabelo povas enhavi malsamajn tipojn en malsamaj elementoj?

Je ĉi tiu punkto vi komencas pensi - ĉu tia TL estas necesa? Eble por la ĉaro eblus uzi homan seriigilon, la saman protobuf kiu jam ekzistis tiam? Tio estis la teorio, ni rigardu la praktikon.

Ekzistantaj TL-efektivigoj en kodo

TL naskiĝis en la profundo de VKontakte eĉ antaŭ la famaj eventoj kun la vendo de la parto de Durov kaj (certe), eĉ antaŭ ol la evoluo de Telegram komenciĝis. Kaj en malferma fonto fontkodo de la unua efektivigo vi povas trovi multajn amuzajn lambastonojn. Kaj la lingvo mem estis efektivigita tie pli plene ol ĝi estas nun en Telegramo. Ekzemple, hashoj tute ne estas uzataj en la skemo (signifante enkonstruitan pseŭdotipon (kiel vektoro) kun devia konduto). Aŭ

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

sed ni konsideru, pro kompleteco, spuri, por tiel diri, la evoluon de la Giganto de Penso.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Aŭ ĉi tiu bela:

    static const char *reserved_words_polymorhic[] = {

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

      };

Ĉi tiu fragmento temas pri ŝablonoj kiel:

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

Jen la difino de hashmap-ŝablona tipo kiel vektoro de int - Tipo-paroj. En C++ ĝi aspektus kiel ĉi tio:

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

do, alpha - ŝlosilvorto! Sed nur en C++ oni povas skribi T, sed oni devas skribi alfa, beta... Sed ne pli ol 8 parametroj, tie finiĝas la fantazio. Ŝajnas, ke iam en Peterburgo okazis tiaj dialogoj:

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

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

Sed ĉi tio temis pri la unua publikigita efektivigo de TL "ĝenerale". Ni daŭrigu pripensi efektivigojn en la Telegram-klientoj mem.

Vorto al Vasilij:

Vasilij, [09.10.18 17:07] Plejparte, la azeno estas varma ĉar ili kreis amason da abstraktaĵoj, kaj poste martelis riglilon sur ilin, kaj kovris la kodgeneratoron per lambastonoj.
Rezulte, unue el dock pilot.jpg
Tiam de la kodo dzhekichan.webp

Kompreneble, de homoj familiaraj kun algoritmoj kaj matematiko, ni povas atendi, ke ili legis Aho, Ullmann, kaj konas la ilojn, kiuj fariĝis fakte normaj en la industrio dum la jardekoj por verki siajn DSL-kompililojn, ĉu ne?..

De telegram-cli estas Vitaly Valtman, kiel oni povas kompreni el la apero de la formato TLO ekster ĝiaj (cli) limoj, membro de la teamo - nun biblioteko por TL-analizo estis asignita aparte, kia estas la impreso de ŝi TL-analizilo? ..

16.12 04:18 Vasilij: Mi pensas, ke iu ne regis lex+yacc
16.12 04:18 Vasilij: Mi ne povas klarigi ĝin alie
16.12 04:18 Vasilij: nu, aŭ ili estis pagitaj pro la nombro da linioj en VK
16.12 04:19 Vasilij: 3k+ linioj ktp.<censored> anstataŭ analizilo

Eble escepto? Ni vidu kiel faras Ĉi tiu estas la OFICIALA kliento - 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+ linioj en Python, kelkaj regulaj esprimoj + specialaj kazoj kiel vektoro, kiu, kompreneble, estas deklarita en la skemo kiel ĝi devus esti laŭ la TL-sintakso, sed ili fidis je ĉi tiu sintakso por analizi ĝin... Estiĝas la demando, kial ĉio estis miraklo?иĜi estas pli tavoligita, se neniu analizos ĝin laŭ la dokumentaro ĉiukaze?!

Cetere... Ĉu vi memoras, ke ni parolis pri CRC32-kontrolado? Do, en la Telegram Desktop-kodgeneratoro estas listo de esceptoj por tiuj tipoj, en kiuj la kalkulita CRC32 ne kongruas kun tiu indikita en la diagramo!

Vasilij, [18.12/22 49:XNUMX] kaj ĉi tie mi pensus ĉu tia TL estas bezonata
se mi volus fuŝi kun alternativaj efektivigoj, mi komencus enmeti liniorompojn, duono de la analiziloj rompiĝos sur plurliniaj difinoj.
tdesktop tamen ankaŭ

Memoru la punkton pri unu-ekskurso, ni revenos al ĝi iom poste.

Bone, telegram-cli estas neoficiala, Telegram Desktop estas oficiala, sed kio pri la aliaj? Kiu scias?.. En la Android-klientkodo tute ne estis skem-analizilo (kiu starigas demandojn pri malferma fonto, sed ĉi tio estas por la dua parto), sed estis pluraj aliaj amuzaj kodoj, sed pli pri ili en la subsekcio malsupre.

Kiajn aliajn demandojn la seriigo levas praktike? Ekzemple, ili faris multajn aferojn, kompreneble, kun bitaj kampoj kaj kondiĉaj kampoj:

Vasilij: flags.0? true
signifas ke la kampo ĉeestas kaj egalas vera se la flago estas metita

Vasilij: flags.1? int
signifas ke la kampo ĉeestas kaj devas esti deserialigita

Vasilij: Pugo, ne zorgu pri tio, kion vi faras!
Vasilij: Estas mencio ie en la dokumento, ke vera estas nuda nul-longa tipo, sed estas neeble kunmeti ion ajn el ilia dokumento.
Vasilij: En la malfermkodaj efektivigoj ankaŭ ne estas la kazo, sed estas amaso da lambastonoj kaj subtenoj.

Kio pri Telethon? Antaŭrigardante la temon de MTProto, ekzemplo - en la dokumentado estas tiaj pecoj, sed la signo % ĝi estas priskribita nur kiel "korespondanta al donita nuda-tipo", t.e. en la malsupraj ekzemploj estas aŭ eraro aŭ io nedokumentita:

Vasilij, [22.06.18 18:38] En unu loko:

msg_container#73f1f8dc messages:vector message = MessageContainer;

En malsama:

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

Kaj ĉi tiuj estas du grandaj diferencoj, en la reala vivo venas ia nuda vektoro

Mi ne vidis nudan vektoran difinon kaj ne trovis tian

Analizo estas skribita mane en teletono

En lia diagramo la difino estas komentita msg_container

Denove, la demando restas ĉirkaŭ %. Ĝi ne estas priskribita.

Vadim Gonĉarov, [22.06.18 19:22] kaj en tdesktop?

Vasily, [22.06.18 19:23] Sed ilia TL-analizilo sur regulaj motoroj ankaŭ ne manĝos ĉi tion.

// parsed manually

TL estas bela abstraktaĵo, neniu efektivigas ĝin tute

Kaj % ne estas en sia versio de la skemo

Sed ĉi tie la dokumentado kontraŭdiras sin, do idk

Ĝi troviĝis en la gramatiko, ili simple povus forgesi priskribi la semantikon

Vi vidis la dokumenton sur TL, vi ne povas eltrovi ĝin sen duona litro

"Nu, ni diru," alia leganto diros, "vi kritikas ion, do montru al mi kiel ĝi estu farita."

Vasilij respondas: “Koncerne la analizilon, mi ŝatas aferojn kiel

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

iel ŝatas ĝin pli bone ol

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

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

jen la TUTA leksilo:

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

tiuj. pli simple estas milde diri.”

Ĝenerale, kiel rezulto, la analizilo kaj kodgeneratoro por la efektive uzita subaro de TL konvenas en proksimume 100 liniojn de gramatiko kaj ~300 liniojn de la generatoro (kalkulante ĉiujn print's generita kodo), inkluzive de tipinformbulkoj por introspekto en ĉiu klaso. Ĉiu polimorfa tipo iĝas malplena abstrakta bazklaso, kaj konstrukciistoj heredas de ĝi kaj havas metodojn por seriigo kaj deseriigo.

Manko de tipoj en la tiplingvo

Forta tajpado estas bona afero, ĉu ne? Ne, ĉi tio ne estas holivaro (kvankam mi preferas dinamikajn lingvojn), sed postulato kadre de TL. Surbaze de ĝi, la lingvo devus provizi ĉiajn ĉekojn por ni. Nu, bone, eble ne li mem, sed la efektivigo, sed li almenaŭ priskribu ilin. Kaj kiajn ŝancojn ni volas?

Antaŭ ĉio, limoj. Jen ni vidas en la dokumentado por alŝuti dosierojn:

La binara enhavo de la dosiero tiam estas dividita en partojn. Ĉiuj partoj devas havi la saman grandecon ( part_grandeco ) kaj la sekvaj kondiĉoj devas esti plenumitaj:

  • part_size % 1024 = 0 (dividebla per 1KB)
  • 524288 % part_size = 0 (512KB devas esti egale divideblaj per part_grandeco)

La lasta parto ne devas kontentigi ĉi tiujn kondiĉojn, kondiĉe ke ĝia grandeco estas malpli ol part_size.

Ĉiu parto havu sinsekvon, dosierparto, kun valoro intervalanta de 0 ĝis 2,999.

Post kiam la dosiero estas dividita, vi devas elekti metodon por konservi ĝin en la servilo. Uzu upload.saveBigFilePart se la plena grandeco de la dosiero estas pli ol 10 MB kaj upload.save FilePart por pli malgrandaj dosieroj.
[…] unu el la jenaj datumoj enigo-eraroj povas esti resendita:

  • FILE_PARTS_INVALID — Nevalida nombro da partoj. La valoro ne estas inter 1..3000

Ĉu io el ĉi tio estas en la diagramo? Ĉu tio estas iel esprimebla uzante TL? Ne. Sed pardonu, eĉ Turbo Pascal de avo povis priskribi la specifitajn tipojn intervaloj. Kaj li sciis plian aferon, nun pli konata kiel enum - tipo konsistanta el nombrado de fiksa (malgranda) nombro da valoroj. En lingvoj kiel C - nombra, notu, ke ĝis nun ni parolis nur pri tipoj nombroj. Sed ekzistas ankaŭ tabeloj, ĉenoj... ekzemple, estus bone priskribi, ke tiu ĉi ĉeno povas enhavi nur telefonnumeron, ĉu ne?

Nenio el ĉi tio estas en la TL. Sed ekzistas, ekzemple, en JSON-Skemo. Kaj se iu alia povus argumenti pri la dividebleco de 512 KB, ke ĉi tio ankoraŭ devas esti kontrolita en kodo, tiam certigu, ke la kliento simple ne povis sendi nombron ekster la intervalo 1..3000 (kaj la responda eraro ne povus estiĝi) estus eble, ĉu ne?...

Cetere, pri eraroj kaj revenaj valoroj. Eĉ tiuj, kiuj laboris kun TL, malklarigas siajn okulojn - tio ne tuj ekkomprenis al ni ĉiu funkcio en TL efektive povas resendi ne nur la priskribitan revenspecon, sed ankaŭ eraron. Sed ĉi tio neniel povas esti deduktita uzante la TL mem. Kompreneble, ĝi jam estas klara kaj ne necesas io ajn en la praktiko (kvankam fakte, RPC povas esti farita laŭ malsamaj manieroj, ni revenos al ĉi tio poste) - sed kio pri la Pureco de la konceptoj de Matematiko de Abstraktaj Tipoj el la ĉiela mondo?.. Mi prenis la tiron — do kongruu.

Kaj fine, kio pri legebleco? Nu, tie, ĝenerale, mi ŝatus Priskribo havi ĝin ĝuste en la skemo (en la JSON-skemo, denove, ĝi estas), sed se vi jam estas streĉita kun ĝi, do kio pri la praktika flanko - almenaŭ bagatela rigardi la diferencojn dum ĝisdatigoj? Vidu mem ĉe realaj ekzemploj:

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

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

Ĝi dependas de ĉiuj, sed GitHub, ekzemple, rifuzas reliefigi ŝanĝojn en tiaj longaj linioj. La ludo "trovu 10 diferencojn", kaj tio, kion la cerbo tuj vidas, estas, ke la komencoj kaj finoj en ambaŭ ekzemploj estas la samaj, necesas tede legi ie en la mezo... Miaopinie, ĉi tio ne nur teorie, sed pure videble malpura kaj malzorgema.

Cetere, pri la pureco de la teorio. Kial ni bezonas bitajn kampojn? Ĉu ne ŝajnas, ke ili odori malbona el la vidpunkto de la teorio de tipoj? La klarigo povas esti vidita en pli fruaj versioj de la diagramo. Komence, jes, tiel estis, por ĉiu terno estis kreita nova tipo. Tiuj rudimentoj daŭre ekzistas en tiu formo, ekzemple:

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;

Sed nun imagu, se vi havas 5 laŭvolajn kampojn en via strukturo, tiam vi bezonos 32 tipojn por ĉiuj eblaj opcioj. Kombinita eksplodo. Tiel, la kristala pureco de la TL-teorio denove frakasis kontraŭ la gisfera azeno de la severa realeco de seriigo.

Krome, en kelkaj lokoj ĉi tiuj uloj mem malobservas sian propran tipologion. Ekzemple, en MTProto (sekva ĉapitro) la respondo povas esti kunpremita per Gzip, ĉio estas en ordo - krom ke la tavoloj kaj cirkvito estas malobservitaj. Denove, ne RpcResult mem estis rikoltita, sed ĝia enhavo. Nu, kial fari ĉi tion?.. Mi devis tranĉi en lambastonon, por ke la kunpremado funkciu ie ajn.

Aŭ alia ekzemplo, ni iam malkovris eraron - ĝi estis sendita InputPeerUser anstataŭ InputUser. Aŭ inverse. Sed ĝi funkciis! Tio estas, la servilo ne zorgis pri la tipo. Kiel tio povas esti? La respondo povas esti donita al ni per kodfragmentoj de 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);

Alivorte, ĉi tie okazas seriigo MANUE, ne generita kodo! Eble la servilo estas efektivigita en simila maniero?.. Principe, tio funkcios se farite unufoje, sed kiel ĝi povas esti subtenata poste dum ĝisdatigoj? Ĉu tial la skemo estis inventita? Kaj jen ni transiru al la sekva demando.

Versiado. Tavoloj

Kial la skemaj versioj estas nomitaj tavoloj povas nur esti konjektita surbaze de la historio de publikigitaj skemoj. Ŝajne, komence la aŭtoroj opiniis, ke bazaj aferoj povas esti faritaj per la senŝanĝa skemo, kaj nur kie necese, por specifaj petoj, indikas ke ili estas faritaj per malsama versio. Principe, eĉ bona ideo - kaj la nova estos kvazaŭ "miksita", tavoligita super la malnova. Sed ni vidu kiel ĝi estis farita. Vere, mi ne povis rigardi ĝin de la komenco - ĝi estas amuza, sed la diagramo de la baza tavolo simple ne ekzistas. Tavoloj komenciĝis per 2. La dokumentaro rakontas al ni pri speciala TL-trajto:

Se kliento subtenas Tavolon 2, tiam la sekva konstrukciisto devas esti uzata:

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

En praktiko, tio signifas, ke antaŭ ĉiu API-voko, int kun la valoro 0x289dd1f6 devas esti aldonita antaŭ la metodonumero.

Sonas normale. Sed kio okazis poste? Tiam aperis

invokeWithLayer3#b7475268 query:!X = X;

Kio do sekvas? Kiel vi eble divenos,

invokeWithLayer4#dea0d430 query:!X = X;

Amuza? Ne, estas tro frue por ridi, pensu pri tio ĉiu peto de alia tavolo devas esti envolvita en tia speciala tipo - se vi havas ĉiujn malsamajn, kiel alie vi povas distingi ilin? Kaj aldoni nur 4 bajtojn antaŭe estas sufiĉe efika metodo. Do,

invokeWithLayer5#417a57ae query:!X = X;

Sed estas evidente, ke post iom da tempo ĉi tio fariĝos ia bakanalo. Kaj venis la solvo:

Ĝisdatigo: Komencante per Tavolo 9, helpaj metodoj invokeWithLayerN uzeblas nur kune kun initConnection

Hura! Post 9 versioj, ni finfine venis al tio, kio estis farita en Interretaj protokoloj jam en la 80-aj jaroj - konsentante pri la versio unufoje komence de la konekto!

Kio do sekvas?...

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

Sed nun vi ankoraŭ povas ridi. Nur post pliaj 9 tavoloj oni fine aldonis universalan konstrukcion kun versio-numero, kiun oni devas voki nur unufoje komence de la konekto, kaj la signifo de la tavoloj ŝajnis malaperis, nun ĝi estas nur kondiĉa versio, kiel ĉie aliloke. Problemo solvita.

Ĝuste?..

Vasilij, [16.07.18 14:01] Eĉ vendrede mi pensis:
La teleservilo sendas eventojn sen peto. Petoj devas esti envolvitaj en InvokeWithLayer. La servilo ne envolvas ĝisdatigojn; ekzistas neniu strukturo por envolvi respondojn kaj ĝisdatigojn.

Tiuj. la kliento ne povas specifi la tavolon en kiu li volas ĝisdatigojn

Vadim Gonĉarov, [16.07.18 14:02] ĉu InvokeWithLayer principe ne estas lambastono?

Vasily, [16.07.18 14:02] Tio estas la sola vojo

Vadim Goncharov, [16.07.18 14:02] kiu esence devus signifi konsenti pri la tavolo komence de la sesio

Cetere, ĝi sekvas, ke kliento malaltiĝo ne estas provizita

Ĝisdatigoj, t.e. tajpu Updates en la skemo, ĉi tio estas kion la servilo sendas al la kliento ne en respondo al API-peto, sed sendepende kiam okazaĵo okazas. Ĉi tio estas kompleksa temo, kiu estos diskutita en alia afiŝo, sed nuntempe gravas scii, ke la servilo konservas Ĝisdatigojn eĉ kiam la kliento estas eksterrete.

Tiel, se vi rifuzas envolvi ĉiu pakaĵo por indiki ĝian version, tio logike kondukas al la sekvaj eblaj problemoj:

  • la servilo sendas ĝisdatigojn al la kliento eĉ antaŭ ol la kliento informis kiun version ĝi subtenas
  • kion mi faru post altgradigo de la kliento?
  • kiu garantiojke la opinio de la servilo pri la tavolnombro ne ŝanĝiĝos dum la procezo?

Ĉu vi pensas, ke tio estas pure teoria konjekto, kaj praktike tio ne povas okazi, ĉar la servilo estas ĝuste skribita (almenaŭ, ĝi estas bone provita)? Ha! Ne gravas kiel ĝi estas!

Ĝuste ĉi tion ni renkontis en aŭgusto. La 14-an de aŭgusto, estis mesaĝoj, ke io estas ĝisdatigita en la Telegram-serviloj... kaj poste en la protokoloj:

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.

kaj poste pluraj megabajtoj da stakspuroj (nu, samtempe la protokolo estis riparita). Post ĉio, se io ne estas rekonita en via TL, ĝi estas duuma per subskribo, pli malsupre en la linio ĈIUJ iras, malkodado fariĝos neebla. Kion vi faru en tia situacio?

Nu, la unua afero, kiu venas en la menson de iu ajn, estas malkonekti kaj provi denove. Ne helpis. Ni guglos CRC32 - ĉi tiuj montriĝis objektoj de skemo 73, kvankam ni laboris pri 82. Ni zorge rigardas la protokolojn - estas identigiloj de du malsamaj skemoj!

Eble la problemo estas nur en nia neoficiala kliento? Ne, ni lanĉas Telegram Desktop 1.2.17 (versio liverita en kelkaj Linukso-distribuoj), ĝi skribas al la Escepta protokolo: MTP Neatendita tipo-identigilo #b5223b0f legita en MTPMessageMedia...

Kritiko de la protokolo kaj organizaj aliroj de Telegramo. Parto 1, teknika: sperto verki klienton de nulo - TL, MT

Guglo montris, ke simila problemo jam okazis al unu el la neoficialaj klientoj, sed tiam la versionumeroj kaj, sekve, la supozoj estis malsamaj...

Kion do ni faru? Mi kaj Vasilij disiĝis: li provis ĝisdatigi la cirkviton al 91, mi decidis atendi kelkajn tagojn kaj provi 73. Ambaŭ metodoj funkciis, sed ĉar ili estas empirie, oni ne komprenas kiom da versioj supren aŭ malsupren vi bezonas. salti, aŭ kiom longe vi devas atendi.

Poste mi povis reprodukti la situacion: ni lanĉas la klienton, malŝaltas ĝin, rekompilas la cirkviton al alia tavolo, rekomencas, denove kaptas la problemon, revenas al la antaŭa - ho, neniu kvanto da cirkvitoŝanĝo kaj kliento rekomencas dum unu. kelkaj minutoj helpos. Vi ricevos miksaĵon de datumstrukturoj de malsamaj tavoloj.

Ĉu klarigo? Kiel vi povas supozi el diversaj nerektaj simptomoj, la servilo konsistas el multaj procezoj de malsamaj tipoj sur malsamaj maŝinoj. Plej verŝajne, la servilo, kiu respondecas pri "buffering", metis en la vicon tion, kion ĝiaj superuloj donis al ĝi, kaj ili donis ĝin en la skemo, kiu estis en la tempo de generacio. Kaj ĝis ĉi tiu vico "putra", nenio povus esti farita pri ĝi.

Eble... sed ĉi tio estas terura lambastono?!.. Ne, antaŭ ol pensi pri frenezaj ideoj, ni rigardu la kodon de la oficialaj klientoj. En la Android-versio ni ne trovas ajnan TL-analizilon, sed ni trovas fortan dosieron (GitHub rifuzas tuŝi ĝin) kun (de)serialigo. Jen la kodaj fragmentoj:

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;

    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... aspektas sovaĝa. Sed, verŝajne, ĉi tio estas generita kodo, do bone?.. Sed ĝi certe subtenas ĉiujn versiojn! Vere, ne estas klare kial ĉio estas miksita kune, sekretaj babilejoj kaj ĉiaj _old7 iel ne aspektas kiel maŝingeneracio... Tamen ĉefe mi estis forblovita

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Uloj, ĉu vi eĉ ne povas decidi kio estas ene de unu tavolo?! Nu, bone, ni diru "du" estis liberigitaj kun eraro, nu, okazas, sed TRI?.. Tuj, la sama rastilo denove? Kia pornografio estas ĉi tio, pardonu?...

En la fontkodo de Telegram Desktop, cetere, simila afero okazas - se jes, pluraj sinsekvaj engaĝiĝoj al la skemo ne ŝanĝas ĝian tavolnumeron, sed ion riparas. En kondiĉoj, kie ne ekzistas oficiala fonto de datumoj por la skemo, de kie ĝi povas esti akirita, krom la fontkodo de la oficiala kliento? Kaj se vi prenas ĝin de tie, vi ne povas esti certa, ke la skemo estas tute ĝusta ĝis vi provas ĉiujn metodojn.

Kiel ĉi tio eĉ povas esti provita? Mi esperas, ke ŝatantoj de unuopaj, funkciaj kaj aliaj provoj dividos en la komentoj.

Bone, ni rigardu alian kodon:

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;

Ĉi tiu komento "mane kreita" sugestas, ke nur parto de ĉi tiu dosiero estis skribita permane (ĉu vi povas imagi la tutan prizorgan koŝmaron?), kaj la resto estis maŝin-generita. Tamen, tiam aperas alia demando - ke la fontoj estas haveblaj ne tute (al la GPL-bloboj en la Linukso-kerno), sed ĉi tio jam estas temo por la dua parto.

Sed sufiĉe. Ni transiru al la protokolo, sur kiu funkcias ĉio ĉi tiu seriigo.

MT Proto

Do, ni malfermu Ĝenerala priskribo и detala priskribo de la protokolo kaj la unua afero, pri kiu ni stumblas, estas la terminologio. Kaj kun abundo de ĉio. Ĝenerale, ĉi tio ŝajnas esti proprieta trajto de Telegram - voki aferojn malsame en malsamaj lokoj, aŭ malsamajn aferojn per unu vorto, aŭ inverse (ekzemple, en altnivela API, se vi vidas glubildpakaĵon, ĝi ne estas. kion vi pensis).

Ekzemple, "mesaĝo" kaj "sesio" signifas ion malsaman ĉi tie ol en la kutima Telegram-klienta interfaco. Nu, ĉio estas klara kun la mesaĝo, ĝi povus esti interpretita en OOP terminoj, aŭ simple nomita la vorto "pako" - ĉi tio estas malalta, transportnivelo, ne estas la samaj mesaĝoj kiel en la interfaco, estas multaj servomesaĝoj. . Sed la kunsido... sed unue.

transporta tavolo

La unua afero estas transporto. Ili rakontos al ni pri 5 ebloj:

  • TCP
  • Retejo
  • Websocket per HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Estas ankaŭ UDP-transporto, sed ĝi ne estas dokumentita

Kaj TCP en tri variantoj

La unua estas simila al UDP super TCP, ĉiu pakaĵeto inkluzivas sekvencon kaj crc
Kial legi dokumentojn sur ĉaro estas tiel dolora?

Nu, jen ĝi nun TCP jam en 4 variantoj:

  • Senbrida
  • Intera
  • Remburita meza
  • Plena

Nu, bone, Remburita meza por MTProxy, ĉi tio estis poste aldonita pro konataj eventoj. Sed kial du pliaj versioj (tri entute), kiam vi povus elteni unu? Ĉiuj kvar esence malsamas nur en kiel agordi la longon kaj utilan ŝarĝon de la ĉefa MTProto, kiu estos diskutita plu:

  • en Mallongigita ĝi estas 1 aŭ 4 bajtoj, sed ne 0xef, tiam la korpo
  • en Meza ĉi tio estas 4 bajtoj da longo kaj kampo, kaj la unuan fojon la kliento devas sendi 0xeeeeeeee por indiki ke ĝi estas Meza
  • en Plena la plej toksomaniulo, el la vidpunkto de retumanto: longeco, sinsekvo, kaj NE TIU, kiu estas ĉefe MTProto, korpo, CRC32. Jes, ĉio ĉi estas super TCP. Kiu provizas nin per fidinda transporto en la formo de sinsekva bajta fluo; neniuj sekvencoj estas necesaj, precipe ĉeksumoj. Bone, nun iu kontraŭos min, ke TCP havas 16-bitan kontrolsumon, do okazas korupto de datumoj. Bonege, sed ni efektive havas ĉifrikan protokolon kun haŝiŝoj pli longaj ol 16 bajtoj, ĉiuj ĉi tiuj eraroj - kaj eĉ pli - estos kaptitaj de SHA-malkongruo je pli alta nivelo. Estas NENIU punkto en CRC32 aldone al ĉi tio.

Ni komparu Mallongigitan, en kiu unu bajto de longo eblas, kun Meza, kiu pravigas "En la okazo ke necesas 4-bajta datuma vicigo", kio estas tute sensencaĵo. Kio, oni kredas, ke Telegram-programistoj estas tiel nekompetentaj, ke ili ne povas legi datumojn de ingo en vicigitan bufron? Vi ankoraŭ devas fari tion, ĉar legado povas redoni al vi ajnan nombron da bajtoj (kaj ekzistas ankaŭ prokuraj serviloj, ekzemple...). Aŭ aliflanke, kial bloki Mallongigitan se ni ankoraŭ havos fortan remburaĵon super 16 bajtoj - ŝparu 3 bajtojn kelkfoje ?

Oni havas la impreson, ke Nikolao Durov tre ŝatas reinventi radojn, inkluzive de retaj protokoloj, sen vera praktika bezono.

Aliaj transportopcioj, inkl. Retejo kaj MTProxy, ni ne konsideros nun, eble en alia afiŝo, se estas peto. Pri ĉi tiu sama MTProxy, ni nur memoru nun, ke baldaŭ post ĝia liberigo en 2018, provizantoj rapide lernis bloki ĝin, celita por pretervojo blokadolaŭ pakaĵo grandeco! Kaj ankaŭ la fakto, ke la MTProxy-servilo skribita (denove de Waltman) en C estis tro ligita al Linukso-specifoj, kvankam tio tute ne estis postulata (Phil Kulin konfirmos), kaj ke simila servilo aŭ en Go aŭ Node.js farus konvenas en malpli ol cent linioj.

Sed ni eltiros konkludojn pri la teknika legopovo de tiuj homoj fine de la sekcio, post pripensado de aliaj aferoj. Nuntempe, ni transiru al OSI-tavolo 5, sesio - sur kiu ili metis MTProto-sesion.

Ŝlosiloj, mesaĝoj, kunsidoj, Diffie-Hellman

Ili metis ĝin tien ne tute ĝuste... Sesio ne estas la sama sesio, kiu estas videbla en la interfaco sub Aktivaj sesioj. Sed en ordo.

Kritiko de la protokolo kaj organizaj aliroj de Telegramo. Parto 1, teknika: sperto verki klienton de nulo - TL, MT

Do ni ricevis bajtan ĉenon de konata longo de la transporta tavolo. Ĉi tio estas aŭ ĉifrita mesaĝo aŭ klarteksto - se ni ankoraŭ estas en la ŝlosila interkonsentstadio kaj efektive faras ĝin. Pri kiu el la aro da konceptoj nomataj "ŝlosilo" ni parolas? Ni klarigu ĉi tiun aferon por la Telegram-teamo mem (mi pardonpetas, ke mi tradukis mian propran dokumentadon el la angla kun laca cerbo je la 4-a matene, estis pli facile lasi iujn frazojn tiaj, kia ili estas):

Estas du estaĵoj nomitaj seanco - unu en la UI de oficialaj klientoj sub "aktualaj sesioj", kie ĉiu sesio respondas al tuta aparato / OS.
Due - MTProto-sesio, kiu havas la sinsekvon de la mesaĝo (en malaltnivela signifo) en ĝi, kaj kiu povas daŭri inter malsamaj TCP-konektoj. Pluraj MTProto-sesioj povas esti instalitaj samtempe, ekzemple, por akceli elŝutadon de dosieroj.

Inter ĉi tiuj du kunsidoj estas koncepto rajtigo. En la degenerita kazo, ni povas diri tion UI-sesio estas la sama kiel rajtigo, sed ve, ĉio estas komplika. Ni rigardu:

  • La uzanto sur la nova aparato unue generas aŭth_key kaj ligas ĝin al konto, ekzemple per SMS - tial rajtigo
  • Ĝi okazis interne de la unua MTProto-sesio, kiu havas session_id en vi mem.
  • Je ĉi tiu paŝo, la kombinaĵo rajtigo и session_id povus esti nomita aŭtoritato - ĉi tiu vorto aperas en la dokumentado kaj kodo de iuj klientoj
  • Tiam, la kliento povas malfermi pluraj MTProto-sesioj sub la sama aŭth_key - al la sama DC.
  • Tiam, unu tagon la kliento devos peti la dosieron de alia DC - kaj por tiu ĉi PK estos generita nova aŭth_key !
  • Por informi la sistemon, ke ne nova uzanto registras, sed la sama rajtigo (UI-sesio), la kliento uzas API-vokojn auth.exportAuthorization en hejmo DC auth.importAuthorization en la nova DC.
  • Ĉio estas la sama, pluraj povas esti malfermitaj MTProto-sesioj (ĉiu kun sia propra session_id) al ĉi tiu nova DC, sub lia aŭth_key.
  • Fine, la kliento eble volas Perfect Forward Secrecy. Ĉiu aŭth_key estis Permanenta ŝlosilo - per DC - kaj la kliento povas voki auth.bindTempAuthKey por uzo provizora aŭth_key - kaj denove, nur unu temp_auth_key po DC, komuna al ĉiuj MTProto-sesioj al ĉi tiu DC.

rimarku, tio salo (kaj estontaj saloj) estas ankaŭ unu aŭth_key tiuj. dividita inter ĉiuj MTProto-sesioj al la sama DC.

Kion signifas "inter malsamaj TCP-konektoj"? Do ĉi tio signifas io kiel rajtiga kuketo en retejo - ĝi daŭras (travivas) multajn TCP-ligojn al difinita servilo, sed iutage ĝi malboniĝas. Nur male al HTTP, en MTProto mesaĝoj ene de sesio estas sinsekve numeritaj kaj konfirmitaj; se ili eniris la tunelon, la konekto estis rompita - post establi novan konekton, la servilo afable sendos ĉion en ĉi tiu sesio, kion ĝi ne liveris en la antaŭa TCP-konekto.

Tamen, la supraj informoj estas resumitaj post multaj monatoj da esploro. Dume, ĉu ni efektivigas nian klienton de nulo? — ni reiru al la komenco.

Do ni generu auth_key sur Diffie-Hellman-versioj de Telegramo. Ni provu kompreni la dokumentadon...

Vasilij, [19.06.18 20:05] data_with_hash := SHA1(datumoj) + datumoj + (ajna hazardaj bajtoj); tia ke la longo egalas al 255 bajtoj;
ĉifritaj_datenoj := RSA(datumoj_kun_hash, servilo_publika_ŝlosilo); 255-bajta longa nombro (big endian) estas levita al la bezonata potenco super la bezonata modulo, kaj la rezulto estas stokita kiel 256-bajta nombro.

Ili havas iom da drogo DH

Ne aspektas kiel la DH de sana persono
Ne estas du publikaj ŝlosiloj en dx

Nu, finfine tio estis aranĝita, sed restaĵo restis - pruvo de laboro estas farita de la kliento, ke li povis faktoro la nombron. Tipo de protekto kontraŭ DoS-atakoj. Kaj la RSA-ŝlosilo estas uzata nur unufoje en unu direkto, esence por ĉifrado new_nonce. Sed dum ĉi tiu ŝajne simpla operacio sukcesos, kion vi devos alfronti?

Vasilij, [20.06.18/00/26 XNUMX:XNUMX] Mi ankoraŭ ne atingis la apid-peton

Mi sendis ĉi tiun peton al DH

Kaj, en la transportdoko ĝi diras, ke ĝi povas respondi per 4 bajtoj de erara kodo. Tio estas ĉio

Nu, li diris al mi -404, do kio?

Do mi diris al li: "Kaptu vian aĉaĵon ĉifritan per servila ŝlosilo kun fingrospuro tia, mi volas DH", kaj ĝi respondis per stulta 404

Kion vi pensus pri ĉi tiu servila respondo? Kion fari? Estas neniu por demandi (sed pli pri tio en la dua parto).

Ĉi tie la tuta intereso estas farita sur la doko

Mi havas nenion alian por fari, mi nur revis konverti nombrojn tien kaj reen

Du 32 bitaj nombroj. Mi pakis ilin kiel ĉiuj

Sed ne, ĉi tiuj du devas esti aldonitaj al la linio unue kiel BE

Vadim Gonĉarov, [20.06.18 15:49] kaj pro tio 404?

Vasilij, [20.06.18 15:49] JES!

Vadim Goncharov, [20.06.18 15:50] do mi ne komprenas, kion li povas "ne trovis"

Vasilij, [20.06.18 15:50] proksimume

Mi ne povis trovi tian putriĝon en primajn faktoroj%)

Ni eĉ ne administris erarraportadon

Vasilij, [20.06.18 20:18] Ho, estas ankaŭ MD5. Jam tri malsamaj hashoj

La ŝlosila fingrospuro estas komputita jene:

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

SHA1 kaj sha2

Do ni metu ĝin auth_key ni ricevis 2048 bitojn en grandeco uzante Diffie-Hellman. Kio sekvas? Poste ni malkovras, ke la pli malaltaj 1024 bitoj de ĉi tiu ŝlosilo neniel estas uzataj... sed ni pensu pri tio nuntempe. Je ĉi tiu paŝo, ni havas dividitan sekreton kun la servilo. Analogo de la TLS-sesio estis establita, kio estas tre multekosta proceduro. Sed la servilo ankoraŭ scias nenion pri kiu ni estas! Ankoraŭ ne, fakte. rajtigo. Tiuj. se vi pensis en terminoj de "saluto-pasvorto", kiel vi iam faris en ICQ, aŭ almenaŭ "ensaluto-ŝlosilo", kiel en SSH (ekzemple, ĉe iu gitlab/github). Ni ricevis anoniman. Kio se la servilo diras al ni "ĉi tiuj telefonnumeroj estas servataj de alia DC"? Aŭ eĉ "via telefonnumero estas malpermesita"? La plej bona, kiun ni povas fari, estas konservi la ŝlosilon kun la espero, ke ĝi estos utila kaj tiam ne putros.

Cetere, ni "ricevis" ĝin kun rezervoj. Ekzemple, ĉu ni fidas la servilon? Kio se ĝi estas falsa? Kriptografiaj kontroloj bezonus:

Vasily, [21.06.18 17:53] Ili proponas poŝtelefonajn klientojn kontroli 2kbitan nombron pri primaco%)

Sed tute ne klaras, nafeijoa

Vasilij, [21.06.18 18:02] La dokumento ne diras kion fari, se ĝi montriĝos ne simpla.

Ne dirite. Ni vidu, kion faras la oficiala Android-kliento en ĉi tiu kazo? A jen kio (kaj jes, la tuta dosiero estas interesa) - kiel oni diras, mi nur lasos ĉi tion ĉi tie:

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

Ne, kompreneble ĝi ankoraŭ estas tie iuj Estas testoj pri la unuaviceco de nombro, sed persone mi ne plu havas sufiĉajn sciojn pri matematiko.

Bone, ni ricevis la ĉefŝlosilon. Por ensaluti, t.e. sendu petojn, vi devas fari plian ĉifradon, uzante AES.

La mesaĝŝlosilo estas difinita kiel la 128 mezaj bitoj de la SHA256 de la mesaĝkorpo (inkluzive de sesio, mesaĝidentigilo, ktp.), inkluzive de la remburaĵaj bajtoj, antaŭmetitaj per 32 bajtoj prenitaj de la rajtigoŝlosilo.

Vasilij, [22.06.18 14:08] Average, bitch, bits

Ricevite auth_key. Ĉiuj. Preter ili... ĝi ne klaras el la dokumento. Bonvolu studi la malfermfontan kodon.

Notu ke MTProto 2.0 postulas de 12 ĝis 1024 bajtoj da kompletigo, daŭre kondiĉigita de la kondiĉo ke la rezulta mesaĝolongo estu dividebla per 16 bajtoj.

Do kiom da kompletigo vi aldonu?

Kaj jes, ekzistas ankaŭ 404 en kazo de eraro

Se iu zorge studis la diagramon kaj tekston de la dokumentaro, ili rimarkis, ke tie ne ekzistas MAC. Kaj tiu AES estas uzata en certa IGE-reĝimo, kiu ne estas uzata aliloke. Ili, kompreneble, skribas pri tio en siaj Oftaj Demandoj... Ĉi tie, kiel, la mesaĝŝlosilo mem estas ankaŭ la SHA hash de la deĉifritaj datumoj, uzata por kontroli la integrecon - kaj en kazo de miskongruo, la dokumentado ial. rekomendas silente ignori ilin (sed kio pri sekureco, kaj se ili rompas nin?).

Mi ne estas kriptografo, eble estas nenio malbona kun ĉi tiu reĝimo ĉi-kaze el teoria vidpunkto. Sed mi povas klare nomi praktikan problemon, uzante Telegram Desktop kiel ekzemplon. Ĝi ĉifras la lokan kaŝmemoron (ĉiuj ĉi D877F783D5D3EF8C) sammaniere kiel mesaĝoj en MTProto (nur ĉi-kaze versio 1.0), t.e. unue la mesaĝŝlosilo, poste la datumoj mem (kaj ie flanken la ĉefa granda auth_key 256 bajtoj, sen kiuj msg_key senutila). Do, la problemo fariĝas videbla sur grandaj dosieroj. Nome, vi devas konservi du kopiojn de la datumoj - ĉifrita kaj malĉifrita. Kaj se estas megabajtoj, aŭ streaming video, ekzemple?.. Klasikaj skemoj kun MAC post la ĉifroteksto permesas vin legi ĝin rivereto, tuj transdonante ĝin. Sed kun MTProto vi devos unue ĉifri aŭ malĉifri la tutan mesaĝon, nur tiam translokigu ĝin al la reto aŭ al disko. Sekve, en la lastaj versioj de Telegram Desktop en la kaŝmemoro en user_data Alia formato ankaŭ estas uzata - kun AES en CTR-reĝimo.

Vasilij, [21.06.18 01:27] Ho, mi eksciis, kio estas IGE: IGE estis la unua provo pri "aŭtentikiga ĉifrareĝimo", origine por Kerberos. Ĝi estis malsukcesa provo (ĝi ne disponigas integrecprotekton), kaj devis esti forigita. Tio estis la komenco de 20-jara serĉado de aŭtentikiga ĉifrada reĝimo, kiu funkcias, kiu lastatempe kulminis per reĝimoj kiel OCB kaj GCM.

Kaj nun la argumentoj de la ĉarflanko:

La teamo malantaŭ Telegram, gvidita de Nikolaj Durov, konsistas el ses ACM-ĉampionoj, duono el ili Ph.Ds en matematiko. Ili bezonis ĉirkaŭ du jarojn por lanĉi la nunan version de MTProto.

Tio estas amuza. Du jarojn ĉe la pli malalta nivelo

Aŭ vi povus simple preni tls

Bone, ni diru, ke ni faris la ĉifradon kaj aliajn nuancojn. Ĉu finfine eblas sendi petojn seriigitajn en TL kaj deseriigi la respondojn? Do kion kaj kiel vi sendu? Jen, ni diru, la metodo initConnection, eble ĉi tio estas?

Vasilij, [25.06.18 18:46] Inicialigas konekton kaj konservas informojn pri la aparato kaj aplikaĵo de la uzanto.

Ĝi akceptas app_id, device_model, system_version, app_version kaj lang_code.

Kaj iu demando

Dokumentado kiel ĉiam. Bonvolu studi la malferman fonton

Se ĉio estis proksimume klara kun invokeWithLayer, do kio malbonas ĉi tie? Rezultas, ni diru, ke ni havas - la kliento jam havis ion por demandi la servilon pri - estas peto, kiun ni volis sendi:

Vasilij, [25.06.18 19:13] Juĝante laŭ la kodo, la unua voko estas envolvita en ĉi tiu aĉaĵo, kaj la aĉaĵo mem estas envolvita en invokewithlayer

Kial initConnection ne povus esti aparta voko, sed devas esti envolvaĵo? Jes, kiel evidentiĝis, ĝi devas esti farita ĉiufoje komence de ĉiu sesio, kaj ne unufoje, kiel ĉe la ĉefa ŝlosilo. Sed! Ĝi ne povas esti vokita de neaŭtorizita uzanto! Nun ni atingis la stadion kie ĝi estas aplikebla Ĉi tiun dokumenta paĝo - kaj ĝi diras al ni, ke...

Nur malgranda parto de la API-metodoj estas haveblaj al neaŭtorizitaj uzantoj:

  • aŭth.sendCode
  • aŭth.resendCode
  • konto.getPasvorto
  • auth.checkPasvorto
  • aŭth.checkPhone
  • aŭth.signUp
  • aŭth.signIn
  • auth.importAuthorization
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

La unua el ili, auth.sendCode, kaj estas tiu ŝatata unua peto, en kiu ni sendas api_id kaj api_hash, kaj post kiu ni ricevas SMS kun kodo. Kaj se ni estas en la malĝusta DC (telefonnumeroj en ĉi tiu lando estas servataj de alia, ekzemple), tiam ni ricevos eraron kun la numero de la dezirata DC. Por ekscii al kiu IP-adreso per DC-numero vi bezonas konektiĝi, helpu nin help.getConfig. Iam estis nur 5 enskriboj, sed post la famaj eventoj de 2018, la nombro signife pliiĝis.

Nun ni memoru, ke ni alvenis al ĉi tiu etapo sur la servilo anonime. Ĉu ne estas tro multekoste nur ricevi IP-adreson? Kial ne fari ĉi tion, kaj aliajn operaciojn, en la neĉifrita parto de MTProto? Mi aŭdas la obĵeton: "kiel ni povas certigi, ke ne RKN respondos per falsaj adresoj?" Al ĉi tio ni memoras ke, ĝenerale, oficialaj klientoj RSA-ŝlosiloj estas enigitaj, t.e. ĉu vi povas simple aboni ĉi tiun informon. Efektive, ĉi tio jam estas farita por informoj pri preterpasi blokadon, kiun klientoj ricevas per aliaj kanaloj (logike, tio ne povas esti farita en MTProto mem; vi ankaŭ bezonas scii kie konekti).

BONE. En ĉi tiu etapo de klienta rajtigo, ni ankoraŭ ne estas rajtigitaj kaj ne registris nian aplikaĵon. Ni nur volas vidi nuntempe, kion la servilo respondas al metodoj disponeblaj por neaŭtorizita uzanto. Kaj ĉi tie…

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

En la skemo, unue venas dua

En la tdesktop-skemo la tria valoro estas

Jes, ekde tiam, kompreneble, la dokumentaro estas ĝisdatigita. Kvankam ĝi baldaŭ povas iĝi sensignifa denove. Kiel devus scii komencanta programisto? Eble se vi registras vian kandidatiĝon, ili informos vin? Vasilij faris tion, sed ve, ili nenion sendis al li (denove, pri tio ni parolos en la dua parto).

...Vi rimarkis, ke ni jam iel transiris al la API, t.e. al la sekva nivelo, kaj maltrafis ion en la temo MTProto? Neniu surprizo:

Vasilij, [28.06.18 02:04] Mm, ili traserĉas kelkajn el la algoritmoj sur e2e

Mtproto difinas ĉifrajn algoritmojn kaj ŝlosilojn por ambaŭ domajnoj, same kiel iom da envolvaĵstrukturo

Sed ili konstante miksas malsamajn nivelojn de la stako, do ne ĉiam estas klare kie mtproto finiĝis kaj la sekva nivelo komenciĝis.

Kiel ili miksas? Nu, jen la sama provizora ŝlosilo por PFS, ekzemple (cetere, Telegram Desktop ne povas fari ĝin). Ĝi estas efektivigita per API-peto auth.bindTempAuthKey, t.e. de la supera nivelo. Sed samtempe ĝi malhelpas ĉifradon ĉe la pli malalta nivelo - post ĝi, ekzemple, vi devas fari ĝin denove. initConnection ktp., ĉi tio ne estas nur normala peto. Kio ankaŭ estas speciala estas, ke vi povas havi nur UNU provizoran ŝlosilon per DC, kvankam la kampo auth_key_id en ĉiu mesaĝo ebligas al vi ŝanĝi la ŝlosilon almenaŭ ĉiun mesaĝon, kaj ke la servilo rajtas "forgesi" la provizoran ŝlosilon iam ajn - la dokumentado ne diras kion fari en ĉi tiu kazo... nu, kial ne povus. vi ne havas plurajn ŝlosilojn, kiel kun aro da estontaj saloj, kaj?...

Estas kelkaj aliaj aferoj rimarkindaj pri la MTProto-temo.

Mesaĝoj, msg_id, msg_seqno, konfirmoj, pings en la malĝusta direkto kaj aliaj idiosinkrazioj

Kial vi bezonas scii pri ili? Ĉar ili "fluas" al pli alta nivelo, kaj vi devas esti konscia pri ili kiam vi laboras kun la API. Ni supozu, ke ni ne interesiĝas pri msg_key; la pli malalta nivelo deĉifris ĉion por ni. Sed ene de la deĉifritaj datumoj ni havas la sekvajn kampojn (ankaŭ la longecon de la datumoj, do ni scias kie estas la kompletigo, sed tio ne gravas):

  • salo - int64
  • session_id - int64
  • mesaĝo_id - int64
  • seq_no - int32

Ni memorigu vin, ke ekzistas nur unu salo por la tuta DC. Kial scii pri ŝi? Ne nur ĉar estas peto get_future_salts, kiu diras al vi kiuj intervaloj validos, sed ankaŭ ĉar se via salo estas "putra", tiam la mesaĝo (peto) simple perdiĝos. La servilo, kompreneble, raportos la novan salon per elsendo new_session_created — sed kun la malnova vi devos resendi ĝin iel, ekzemple. Kaj ĉi tiu afero influas la aplikaĵarkitekturon.

La servilo rajtas tute faligi sesiojn kaj respondi tiamaniere pro multaj kialoj. Efektive, kio estas MTProto-sesio de la klienta flanko? Ĉi tiuj estas du nombroj session_id и seq_no mesaĝojn ene de ĉi tiu sesio. Nu, kaj la subesta TCP-konekto, kompreneble. Ni diru, ke nia kliento ankoraŭ ne scias kiel fari multajn aferojn, li malkonektis kaj rekonektis. Se ĉi tio okazis rapide - la malnova sesio daŭris en la nova TCP-konekto, pliigu seq_no plue. Se ĝi daŭros longan tempon, la servilo povus forigi ĝin, ĉar siaflanke ĝi estas ankaŭ vico, kiel ni eksciis.

Kio devus esti seq_no? Ho, tio estas malfacila demando. Provu honeste kompreni, kio estis signifita:

Enhavo-rilata Mesaĝo

Mesaĝo postulanta eksplicitan agnoskon. Ĉi tiuj inkluzivas ĉiujn uzantojn kaj multajn servomesaĝojn, preskaŭ ĉiuj escepte de ujoj kaj agnoskoj.

Mesaĝa Sekvenca Nombro (msg_seqno)

32-bita nombro egala al duoble la nombro da "enhav-rilataj" mesaĝoj (tiuj postulantaj agnoskon, kaj precipe tiuj kiuj ne estas ujoj) kreitaj de la sendinto antaŭ ĉi tiu mesaĝo kaj poste pliigita je unu se la nuna mesaĝo estas enhav-rilata mesaĝo. Ujo ĉiam estas generita post ĝia tuta enhavo; tial, ĝia sinsekvo estas pli granda ol aŭ egala al la sinsekvonumeroj de la mesaĝoj enhavitaj en ĝi.

Kia cirko estas ĉi tio kun pliigo je 1, kaj poste alia je 2?... Mi suspektas, ke komence ili signifis "la malplej signifa bito por ACK, la resto estas nombro", sed la rezulto ne estas tute sama - precipe, ĝi eliras, povas esti sendita pluraj konfirmoj havantaj la saman seq_no! Kiel? Nu, ekzemple, la servilo sendas al ni ion, sendas ĝin, kaj ni mem restas silentaj, nur respondante per servaj mesaĝoj konfirmante la ricevon de ĝiaj mesaĝoj. En ĉi tiu kazo, niaj eksiĝintaj konfirmoj havos la saman eksiĝintan nombron. Se vi konas TCP kaj pensis, ke ĉi tio sonas iel sovaĝa, sed ĝi ŝajnas ne tre sovaĝa, ĉar en TCP seq_no ne ŝanĝiĝas, sed konfirmo iras al seq_no aliflanke, mi rapidos ĉagreni vin. Konfirmoj estas disponigitaj en MTProto NE sur seq_no, kiel en TCP, sed per msg_id !

Kio estas ĉi tio msg_id, la plej grava el ĉi tiuj kampoj? Unika mesaĝo-identigilo, kiel la nomo sugestas. Ĝi estas difinita kiel 64-bita nombro, kies plej malaltaj bitoj denove havas la "servilo-ne-servilo" magion, kaj la resto estas Unikso-simila tempomarko, inkluzive de la frakcia parto, ŝovita 32 bitojn maldekstren. Tiuj. tempomarko en si mem (kaj mesaĝoj kun tempoj kiuj tro diferencas estos malakceptitaj de la servilo). El tio rezultas, ke ĝenerale ĉi tio estas identigilo, kiu estas tutmonda por la kliento. Konsiderante tion — ni memoru session_id - ni estas garantiitaj: Sub neniuj cirkonstancoj mesaĝo destinita por unu sesio povas esti sendita al malsama sesio. Tio estas, rezultas, ke jam ekzistas tri nivelo - sesio, numero de sesio, mesaĝo-identigilo. Kial tia trokompliko, ĉi tiu mistero estas tre granda.

Kaj tiel, msg_id necesa por...

RPC: petoj, respondoj, eraroj. Konfirmoj.

Kiel vi eble rimarkis, ne ekzistas speciala "faru RPC-peton" tipo aŭ funkcio ie ajn en la diagramo, kvankam ekzistas respondoj. Ja ni havas enhav-rilatajn mesaĝojn! Tio estas, ajn la mesaĝo povus esti peto! Aŭ ne esti. Finfine, ĉiu estas msg_id. Sed estas respondoj:

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

Jen kie estas indikite, al kiu mesaĝo ĉi tio estas respondo. Tial, ĉe la plej alta nivelo de la API, vi devos memori, kia estis la nombro de via peto - mi pensas, ke ne necesas klarigi, ke la laboro estas nesinkrona, kaj povas esti pluraj petoj en progreso samtempe, la respondoj al kiuj povas esti resenditaj en ajna ordo? Principe, el ĉi tio kaj erarmesaĝoj kiel neniuj laboristoj, la arkitekturo malantaŭ ĉi tio povas esti spurita: la servilo, kiu konservas TCP-konekton kun vi, estas front-end-balancilo, ĝi plusendas petojn al la backends kaj kolektas ilin reen per message_id. Ŝajnas, ke ĉio ĉi tie estas klara, logika kaj bona.

Jes?.. Kaj se vi pensas pri tio? Post ĉio, la RPC-respondo mem ankaŭ havas kampon msg_id! Ĉu ni bezonas krii al la servilo "vi ne respondas mian respondon!"? Kaj jes, kio estis pri konfirmoj? Pri paĝo mesaĝoj pri mesaĝoj diras al ni kio estas

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

kaj ĝi devas esti farita de ĉiu flanko. Sed ne ĉiam! Se vi ricevis RpcResult, ĝi mem servas kiel konfirmo. Tio estas, la servilo povas respondi al via peto per MsgsAck - kiel, "Mi ricevis ĝin." RpcResult povas respondi tuj. Ĝi povus esti ambaŭ.

Kaj jes, vi ankoraŭ devas respondi la respondon! Konfirmo. Alie, la servilo konsideros ĝin neliverebla kaj resendos ĝin al vi denove. Eĉ post rekonekto. Sed ĉi tie, kompreneble, ŝprucas la temo de tempoforpasoj. Ni rigardu ilin iom poste.

Intertempe, ni rigardu eblajn demandajn ekzekut-erarojn.

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

Ho, iu ekkrios, jen pli humana formato - estas linio! Prenu vian tempon. Jen listo de eraroj, sed kompreneble ne kompleta. De ĝi ni lernas ke la kodo estas io kiel HTTP-eraroj (nu, kompreneble, la semantiko de la respondoj ne estas respektata, kelkloke ili estas distribuitaj hazarde inter la kodoj), kaj la linio aspektas kiel CAPITAL_LETTERS_AND_NUMBERS. Ekzemple, PHONE_NUMBER_OCCUPIED aŭ FILE_PART_Х_MISSING. Nu, tio estas, vi ankoraŭ bezonos ĉi tiun linion analizi. Ekzemple FLOOD_WAIT_3600 signifos, ke vi devas atendi horon, kaj PHONE_MIGRATE_5, ke telefonnumero kun tiu ĉi prefikso devas esti registrita en la 5-a PK. Ni havas tiplingvon, ĉu ne? Ni ne bezonas argumenton el ŝnuro, regulaj utilos, bone.

Denove, ĉi tio ne estas en la paĝo de servo mesaĝoj, sed, kiel jam kutime ĉe ĉi tiu projekto, la informoj troveblas sur alia dokumenta paĝo. Aŭ ĵeti suspekton. Unue, rigardu, tajpado/tavola malobservo - RpcError povas esti nestita enen RpcResult. Kial ne ekstere? Kion ni ne enkalkulis?.. Sekve, kie estas la garantio, ke RpcError eble NE estas enigita en RpcResult, sed esti rekte aŭ nestitaj en alia tipo?.. Kaj se ĝi ne povas, kial ĝi ne estas en la plej alta nivelo, t.e. ĝi mankas req_msg_id ? ..

Sed ni daŭrigu pri servomesaĝoj. La kliento eble pensas, ke la servilo pensas dum longa tempo kaj faras ĉi tiun mirindan peton:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Estas tri eblaj respondoj al ĉi tiu demando, denove intersekciĝantaj kun la konfirmmekanismo; provi kompreni kio ili devus esti (kaj kia la ĝenerala listo de tipoj kiuj ne postulas konfirmon) estas lasita al la leganto kiel hejmtasko (noto: la informoj en la fontkodo de Telegram Desktop ne estas kompleta).

Drogodependeco: mesaĝaj statoj

Ĝenerale multaj lokoj en TL, MTProto kaj Telegram ĝenerale lasas senton de obstineco, sed pro ĝentileco, takto kaj aliaj molaj kapabloj Ni ĝentile silentis pri ĝi, kaj cenzuris la obscenaĵojn en la dialogoj. Tamen, ĉi tiu lokoОplejparto de la paĝo temas pri mesaĝoj pri mesaĝoj Estas ŝoke eĉ por mi, kiu jam delonge laboras kun retaj protokoloj kaj vidis biciklojn de diversaj gradoj de malrekteco.

Ĝi komenciĝas senkulpe, kun konfirmoj. Poste ili rakontas al ni pri

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;

Nu, ĉiuj, kiuj komencas labori kun MTProto, devos trakti ilin; en la ciklo "korektita - rekompilita - lanĉita" ricevi numero-erarojn aŭ salon, kiu sukcesis malboniĝi dum redaktoj, estas kutima afero. Tamen, estas du punktoj ĉi tie:

  1. Ĉi tio signifas, ke la originala mesaĝo estas perdita. Ni devas krei kelkajn atendovicojn, ni rigardos tion poste.
  2. Kio estas ĉi tiuj strangaj eraraj nombroj? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... kie estas la aliaj nombroj, Tomi?

La dokumentaro deklaras:

La intenco estas, ke la valoroj de eraro_kodo estu grupigitaj (eraro_kodo >> 4): ekzemple, la kodoj 0x40 — 0x4f respondas al eraroj en ujo-malkomponado.

sed, unue, ŝanĝo en la alia direkto, kaj due, ne gravas, kie estas la aliaj kodoj? En la kapo de la aŭtoro?.. Tamen tio estas bagateloj.

Dependeco komenciĝas en mesaĝoj pri mesaĝaj statoj kaj mesaĝkopioj:

  • Peto pri Mesaĝa Statuso-Informo
    Se iu partio ne ricevis informojn pri la stato de siaj eksiĝintaj mesaĝoj dum kelka tempo, ĝi povas eksplicite peti ĝin de la alia partio:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informa Mesaĝo pri Statuso de Mesaĝoj
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    ĉi tie, info estas ĉeno kiu enhavas ekzakte unu bajton de mesaĝo-stato por ĉiu mesaĝo el la envenanta msg_ids listo:

    • 1 = nenio estas konata pri la mesaĝo (msg_id tro malalta, la alia partio eble forgesis ĝin)
    • 2 = mesaĝo ne ricevita (msg_id apartenas al la gamo de konservitaj identigiloj; tamen la alia partio certe ne ricevis tian mesaĝon)
    • 3 = mesaĝo ne ricevita (msg_id tro alta; tamen la alia partio certe ankoraŭ ne ricevis ĝin)
    • 4 = mesaĝo ricevita (notu, ke ĉi tiu respondo ankaŭ estas samtempe kvitanco)
    • +8 = mesaĝo jam agnoskita
    • +16 = mesaĝo ne postulanta agnoskon
    • +32 = RPC-demando enhavita en mesaĝo estanta prilaborita aŭ prilaborado jam finiĝis
    • +64 = enhavo-rilata respondo al mesaĝo jam generita
    • +128 = alia partio certe scias, ke mesaĝo jam estas ricevita
      Ĉi tiu respondo ne postulas agnoskon. Ĝi estas agnosko de la koncerna msgs_state_req, en si mem.
      Notu, ke se subite evidentiĝas, ke la alia partio ne havas mesaĝon, kiu aspektas kiel ĝi estis sendita al ĝi, la mesaĝo povas simple esti resendita. Eĉ se la alia partio ricevus du kopiojn de la mesaĝo samtempe, la duplikato estos ignorita. (Se tro da tempo pasis, kaj la originala msg_id ne plu validas, la mesaĝo devas esti envolvita en msg_copy).
  • Volontula Komunikado de Statuso de Mesaĝoj
    Ĉiu partio povas libervole informi la alian partion pri la stato de la mesaĝoj elsenditaj de la alia partio.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Plilongigita Volontula Komunikado de Statuso de Unu Mesaĝo
    ...
    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;
  • Eksplicita Peto por Re-Sendi Mesaĝojn
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    La fora partio tuj respondas resendante la petitajn mesaĝojn [...]
  • Eksplicita Peto por Re-Sendi Respondojn
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    La fora partio tuj respondas per resendo respondoj al la petitaj mesaĝoj […]
  • Mesaĝaj Kopioj
    En iuj situacioj, malnova mesaĝo kun msg_id, kiu ne plu validas, devas esti resendita. Poste, ĝi estas enpakita en kopiujo:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Post kiam ricevita, la mesaĝo estas procesita kvazaŭ la envolvaĵo ne estus tie. Tamen, se estas certe ke la mesaĝo orig_message.msg_id estis ricevita, tiam la nova mesaĝo ne estas prilaborita (dum samtempe ĝi kaj orig_message.msg_id estas agnoskitaj). La valoro de orig_message.msg_id devas esti pli malalta ol msg_id de la ujo.

Ni eĉ silentu pri kio msgs_state_info denove elstariĝas la oreloj de la nefinita TL (ni bezonis vektoron de bajtoj, kaj en la malsuperaj du bitoj estis enum, kaj en la pli altaj du bitoj estis flagoj). La punkto estas malsama. Ĉu iu komprenas kial ĉio ĉi estas en la praktiko? en vera kliento necesas?.. Malfacile, sed oni povas imagi ian utilon, se homo okupiĝas pri sencimigo, kaj en interaga reĝimo - demandu la servilon kio kaj kiel. Sed ĉi tie la petoj estas priskribitaj rondveturo.

Sekvas, ke ĉiu partio devas ne nur ĉifri kaj sendi mesaĝojn, sed ankaŭ konservi datumojn pri si mem, pri la respondoj al ili, dum nekonata tempodaŭro. La dokumentaro ne priskribas nek la tempojn nek la praktikan aplikeblecon de ĉi tiuj trajtoj. neniu vojo. Plej mirinda estas, ke ili efektive estas uzataj en la kodo de oficialaj klientoj! Ŝajne oni diris al ili ion, kio ne estis inkluzivita en la publika dokumentaro. Komprenu el la kodo por kio, ne plu estas tiel simpla kiel ĉe TL - ĝi ne estas (relative) logike izolita parto, sed peco ligita al la aplika arkitekturo, t.e. postulos signife pli da tempo por kompreni la aplikan kodon.

Pingoj kaj tempoj. Vicoj.

El ĉio, se ni memoras la divenojn pri la servila arkitekturo (distribuo de petoj tra backends), iom malĝoja afero sekvas - malgraŭ ĉiuj liveraj garantioj en TCP (aŭ la datumoj estas liveritaj, aŭ vi estos informita pri la breĉo, sed la datumoj estos liveritaj antaŭ ol la problemo okazas), ke konfirmoj en MTProto mem - neniuj garantioj. La servilo povas facile perdi aŭ forĵeti vian mesaĝon, kaj nenio povas esti farita pri ĝi, nur uzu malsamajn specojn de lambastonoj.

Kaj antaŭ ĉio - mesaĝvostoj. Nu, per unu afero ĉio estis evidenta ekde la komenco - nekonfirmita mesaĝo devas esti konservita kaj resendita. Kaj post kiu tempo? Kaj la pajaco konas lin. Eble tiuj dependaj servaj mesaĝoj iel solvas ĉi tiun problemon per lambastonoj, ekzemple, en Telegram Desktop estas ĉirkaŭ 4 vostoj respondaj al ili (eble pli, kiel jam menciis, por tio necesas pli serioze enprofundiĝi en ĝian kodon kaj arkitekturon; samtempe; tempo, ni Ni scias, ke ĝi ne povas esti prenita kiel specimeno; certa nombro da tipoj el la MTProto-skemo ne estas uzataj en ĝi).

Kial ĉi tio okazas? Verŝajne, la servilprogramistoj estis nekapablaj certigi fidindecon ene de la areto, aŭ eĉ bufro sur la antaŭa balancilo, kaj transdonis ĉi tiun problemon al la kliento. Pro malespero, Vasilij provis efektivigi alternativan opcion, kun nur du atendovicoj, uzante algoritmojn de TCP - mezuri la RTT al la servilo kaj ĝustigi la grandecon de la "fenestro" (en mesaĝoj) depende de la nombro da nekonfirmitaj petoj. Tio estas, tia malglata heŭristiko por taksi la ŝarĝon de la servilo estas kiom da niaj petoj ĝi povas maĉi samtempe kaj ne perdi.

Nu, tio estas, vi komprenas, ĉu ne? Se vi devas efektivigi TCP denove super protokolo super TCP, tio indikas tre malbone desegnitan protokolon.

Ho jes, kial vi bezonas pli ol unu vicon, kaj kion tio signifas por persono laboranta kun altnivela API ĉiuokaze? Rigardu, vi faras peton, seriigas ĝin, sed ofte vi ne povas sendi ĝin tuj. Kial? Ĉar la respondo estos msg_id, kiu estas provizoraаMi estas etikedo, kies tasko estas plej bone prokrastita ĝis kiel eble plej malfrue - en la okazo ke la servilo ĝin malakceptas pro malkongruo de tempo inter ni kaj li (kompreneble, ni povas fari lambastonon, kiu movas nian tempon de la nuntempo. al la servilo aldonante delton kalkulitan el la respondoj de la servilo - oficialaj klientoj faras tion, sed ĝi estas kruda kaj malpreciza pro bufro). Tial, kiam vi faras peton per loka funkciovoko de la biblioteko, la mesaĝo pasas tra la sekvaj etapoj:

  1. Ĝi kuŝas en unu atendovico kaj atendas ĉifradon.
  2. Nomumita msg_id and the message went to another queue - ebla plusendado; sendu al la ingo.
  3. a) La servilo respondis MsgsAck - la mesaĝo estis transdonita, ni forigas ĝin el la "alia vico".
    b) Aŭ inverse, li ne ŝatis ion, li respondis badmsg - resendi el "alia vico"
    c) Nenio estas konata, la mesaĝo devas esti resendita el alia vico - sed oni ne scias precize kiam.
  4. La servilo finfine respondis RpcResult - la efektiva respondo (aŭ eraro) - ne nur liverita, sed ankaŭ prilaborita.

Probable, la uzo de ujoj povus parte solvi la problemon. Jen kiam amaso da mesaĝoj estas pakitaj en unu, kaj la servilo respondis per konfirmo al ĉiuj ili samtempe, en unu msg_id. Sed li ankaŭ malakceptos ĉi tiun pakaĵon, se io misfunkciis, en ĝia tuteco.

Kaj ĉe ĉi tiu punkto ne-teknikaj konsideroj ekludas. Laŭ sperto, ni vidis multajn lambastonojn, kaj krome, ni nun vidos pli da ekzemploj de malbonaj konsiloj kaj arkitekturo - en tiaj kondiĉoj, ĉu indas fidi kaj fari tiajn decidojn? La demando estas retorika (kompreneble ne).

Pri kio ni parolas? Se pri la temo "drogmesaĝoj pri mesaĝoj" vi ankoraŭ povas konjekti kun obĵetoj kiel "vi estas stulta, vi ne komprenis nian brilan planon!" (do skribu la dokumentaron unue, kiel normalaj homoj devus, kun raciaĵo kaj ekzemploj de paka interŝanĝo, poste ni parolos), tiam tempoj/tempotempoj estas pure praktika kaj specifa demando, ĉio ĉi tie estas konata delonge. Kion la dokumentaro diras al ni pri tempo-tempo?

Servilo kutime agnoskas la ricevon de mesaĝo de kliento (normale, RPC-demando) uzante RPC-respondon. Se respondo longe venas, servilo povas unue sendi kvitancon, kaj iom poste, la RPC-respondon mem.

Kliento normale agnoskas la ricevon de mesaĝo de servilo (kutime, RPC-respondo) aldonante agnoskon al la venonta RPC-demando se ĝi ne estas elsendita tro malfrue (se ĝi estas generita, ekzemple, 60-120 sekundojn post la kvitanco). de mesaĝo de la servilo). Tamen, se dum longa tempo ne ekzistas kialo por sendi mesaĝojn al la servilo aŭ se ekzistas granda nombro da neagnoskitaj mesaĝoj de la servilo (diru, pli ol 16), la kliento transdonas memstaran agnoskon.

... Mi tradukas: ni mem ne scias kiom kaj kiel ni bezonas ĝin, do ni supozu, ke estu tiel.

Kaj pri pingoj:

Ping Mesaĝoj (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Respondo estas kutime resendita al la sama konekto:

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

Ĉi tiuj mesaĝoj ne postulas agnoskojn. Pong estas elsendita nur en respondo al ping-o dum ping povas esti iniciatita de ambaŭ flankoj.

Prokrastita Konekto Fermo + PING

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

Funkcias kiel ping. Krome, post kiam ĉi tio estas ricevita, la servilo startas tempigilon kiu fermos la nunan konekton disconnect_delay sekundojn poste krom se ĝi ricevas novan mesaĝon de la sama tipo kiu aŭtomate rekomencigas ĉiujn antaŭajn tempigilojn. Se la kliento sendas ĉi tiujn ping-ojn unufoje ĉiujn 60 sekundojn, ekzemple, ĝi povas agordi disconnect_delay egalan al 75 sekundoj.

Ĉu vi estas freneza?! Post 60 sekundoj, la trajno eniros la stacidomon, forigos kaj prenos pasaĝerojn, kaj denove perdos kontakton en la tunelo. Post 120 sekundoj, dum vi aŭdas ĝin, ĝi alvenos al alia, kaj la konekto plej verŝajne rompiĝos. Nu, estas klare, de kie venas la kruroj - "Mi aŭdis sonoradon, sed ne scias kie ĝi estas", ekzistas la algoritmo de Nagl kaj la opcio TCP_NODELAY, destinita por interaga laboro. Sed, pardonu, tenu ĝian defaŭltan valoron - 200 Millisekundoj Se vi vere volas bildigi ion similan kaj ŝpari sur ebla paro da pakaĵetoj, tiam prokrastu ĝin dum 5 sekundoj, aŭ kio ajn estas la "Uzanto tajpas..." mesaĝotempotempo estas nun. Sed ne plu.

Kaj fine, pings. Tio estas, kontrolante la vivecon de la TCP-konekto. Estas amuza, sed antaŭ ĉirkaŭ 10 jaroj mi skribis kritikan tekston pri la mesaĝisto de la dormejo de nia fakultato - la aŭtoroj tie ankaŭ pingis la servilon de la kliento, kaj ne inverse. Sed studentoj de 3-a jaro estas unu afero, kaj internacia oficejo estas alia, ĉu ne?...

Unue, eta eduka programo. TCP-konekto, en foresto de paka interŝanĝo, povas vivi dum semajnoj. Ĉi tio estas kaj bona kaj malbona, depende de la celo. Estas bone, se vi havis SSH-konekton malfermitan al la servilo, vi leviĝis de la komputilo, rekomencis la enkursigilon, revenis al via loko - la kunsido per ĉi tiu servilo ne estis disŝirita (vi tajpis nenion, ne estis pakoj) , ĝi estas oportuna. Estas malbone se estas miloj da klientoj sur la servilo, ĉiu okupante rimedojn (saluton, Postgres!), kaj la gastiganto de la kliento eble rekomencis antaŭ longe - sed ni ne scios pri tio.

Babilado/IM-sistemoj falas en la duan kazon pro unu kroma kialo - retaj statusoj. Se la uzanto "falis", vi devas informi siajn interparolantojn pri tio. Alie, vi finos kun eraro, kiun la kreintoj de Jabber faris (kaj korektis dum 20 jaroj) - la uzanto malkonektis, sed ili daŭre skribas mesaĝojn al li, kredante, ke li estas enreta (kiuj ankaŭ estis tute perditaj en ĉi tiuj). kelkajn minutojn antaŭ ol la malkonekto estis malkovrita). Ne, la opcio TCP_KEEPALIVE, kiun multaj homoj, kiuj ne komprenas kiel funkcias TCP-tempigiloj, enĵetas hazarde (agordante sovaĝajn valorojn kiel dekoj da sekundoj), ne helpos ĉi tie - vi devas certigi, ke ne nur la OS-kerno. de la maŝino de la uzanto vivas, sed ankaŭ funkcias normale, kapabla respondi, kaj la aplikaĵo mem (ĉu vi pensas, ke ĝi ne povas frostigi? Telegram Desktop sur Ubuntu 18.04 frostiĝis por mi pli ol unufoje).

Tial vi devas pingi servilo kliento, kaj ne inverse - se la kliento faras tion, se la konekto estas rompita, la ping ne estos liverita, la celo ne estos atingita.

Kion ni vidas ĉe Telegramo? Estas ĝuste la malo! Nu, tio estas. Formale, kompreneble, ambaŭ flankoj povas pingsi unu la alian. En praktiko, klientoj uzas lambastonon ping_delay_disconnect, kiu fiksas la tempigilon sur la servilo. Nu, pardonu, ne dependas de la kliento decidi kiom longe li volas vivi tie sen ping. La servilo, surbaze de sia ŝarĝo, scias pli bone. Sed, kompreneble, se vi ne ĝenas la rimedojn, tiam vi estos via propra malbona Pinokjo, kaj lambastono faros...

Kiel ĝi devus esti desegnita?

Mi kredas, ke la supraj faktoj klare indikas, ke la teamo de Telegramo/VKontakte ne estas tre kompetenta en la kampo de transporta (kaj pli malalta) nivelo de komputilaj retoj kaj iliaj malaltaj kvalifikoj en koncernaj aferoj.

Kial ĝi rezultis tiel komplika, kaj kiel Telegram-arkitektoj povas provi kontraŭi? La fakto, ke ili provis fari sesion, kiu postvivas TCP-konekto-rompojn, t.e., kio ne estis liverita nun, ni liveros poste. Ili verŝajne ankaŭ provis fari UDP-transporton, sed ili renkontis malfacilaĵojn kaj forlasis ĝin (tial la dokumentaro estas malplena - estis nenio pri kio fanfaroni). Sed pro manko de kompreno pri kiel funkcias retoj ĝenerale kaj TCP precipe, kie vi povas fidi ĝin, kaj kie vi devas fari ĝin mem (kaj kiel), kaj provo kombini ĉi tion kun kriptografio "du birdoj kun unu ŝtonon”, jen la rezulto.

Kiel ĝi estis necesa? Surbaze de la fakto ke msg_id estas tempomarko necesa de kripta vidpunkto por malhelpi ripetajn atakojn, estas eraro alligi al ĝi unikan identigilan funkcion. Tial, sen esence ŝanĝi la nunan arkitekturon (kiam la Ĝisdatigoj estas generita, tio estas altnivela API-temo por alia parto de ĉi tiu serio de afiŝoj), oni bezonus:

  1. La servilo tenanta la TCP-konekton al la kliento prenas respondecon - se ĝi legis el la ingo, bonvolu agnoski, procesi aŭ redoni eraron, neniu perdo. Tiam la konfirmo ne estas vektoro de id-oj, sed simple "la lasta ricevita seq_no" - nur nombro, kiel en TCP (du nombroj - via sekvo kaj la konfirmita). Ni ĉiam estas ene de la kunsido, ĉu ne?
  2. La tempomarko por malhelpi ripetajn atakojn fariĝas aparta kampo, al la nonce. Ĝi estas kontrolita, sed ne influas ion alian. Sufiĉe kaj uint32 - se nia salo ŝanĝiĝas almenaŭ ĉiun duonan tagon, ni povas asigni 16 bitojn al la malalt-ordaj bitoj de entjera parto de la nuna tempo, la reston - al frakcia parto de sekundo (kiel nun).
  3. Forigita msg_id entute - de la vidpunkto de distingi petojn sur la backends, ekzistas, unue, la kliento-id, kaj due, la seanca id, kunligi ilin. Sekve, nur unu afero sufiĉas kiel peto-identigilo seq_no.

Ĉi tio ankaŭ ne estas la plej sukcesa opcio; kompleta hazarda povus servi kiel identigilo - tio jam estas farita en la altnivela API dum sendado de mesaĝo, cetere. Pli bone estus tute refari la arkitekturon de relativa al absoluta, sed ĉi tio estas temo por alia parto, ne ĉi tiu afiŝo.

API?

Ta-daam! Do, luktinte tra vojo plena de doloro kaj lambastonoj, ni finfine povis sendi ajnajn petojn al la servilo kaj ricevi ajnajn respondojn al ili, kaj ankaŭ ricevi ĝisdatigojn de la servilo (ne responde al peto, sed ĝi mem). sendas nin, kiel PUSH, se iu estas pli klare tiel).

Atentu, nun estos la sola ekzemplo en Perl en la artikolo! (por tiuj, kiuj ne konas la sintakson, la unua argumento de bless estas la datumstrukturo de la objekto, la dua estas ĝia klaso):

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

Jes, ne spoiler intence - se vi ankoraŭ ne legis ĝin, daŭrigu kaj faru ĝin!

Ho, wai~~... kiel tio aspektas? Io tre konata... eble ĉi tio estas la datumstrukturo de tipa TTT-API en JSON, krom ke klasoj ankaŭ estas ligitaj al objektoj?..

Do jen kiel ĝi rezultas... Pri kio temas, kamaradoj?.. Tiom da penado - kaj ni haltis por ripozi kie la Retaj programistoj. ĵus komencante?..Ĉu nur JSON super HTTPS ne estus pli simpla?! Kion ni ricevis interŝanĝe? Ĉu la peno valoris?

Ni taksu, kion donis al ni TL+MTProto kaj kiajn alternativojn eblas. Nu, HTTP, kiu fokusiĝas al la peto-responda modelo, estas malbone taŭga, sed almenaŭ io krom TLS?

Kompakta seriigo. Vidante ĉi tiun datuman strukturon, similan al JSON, mi memoras, ke ekzistas binaraj versioj de ĝi. Ni marku MsgPack kiel nesufiĉe etendebla, sed ekzistas, ekzemple, CBOR - cetere, normo priskribita en RFC 7049. Ĝi estas rimarkinda pro tio, ke ĝi difinas etikedoj, kiel ekspansia mekanismo, kaj inter jam normigitaj disponebla:

  • 25 + 256 - anstataŭigante ripetajn liniojn per referenco al la linionumero, tia malmultekosta kunprema metodo
  • 26 - seriigita Perl-objekto kun klasnomo kaj argumentoj de konstrukciisto
  • 27 - seriigita lingvo-sendependa objekto kun tipnomo kaj argumentoj de konstrukciisto

Nu, mi provis seriigi la samajn datumojn en TL kaj en CBOR kun ŝnuro kaj objektopakado ebligita. La rezulto komencis varii en favoro de CBOR ie de megabajto:

cborlen=1039673 tl_len=1095092

Kaj tiel, konkludo: Estas sufiĉe pli simplaj formatoj, kiuj ne estas submetataj al la problemo de malsukceso de sinkronigado aŭ nekonata identigilo, kun komparebla efikeco.

Rapida konekto establado. Ĉi tio signifas nul RTT post rekonekto (kiam la ŝlosilo jam estis generita unufoje) - aplikebla de la plej unua MTProto-mesaĝo, sed kun iuj rezervoj - trafu la saman salon, la sesio ne putras, ktp. Kion TLS proponas al ni anstataŭe? Citaĵo pri temo:

Kiam vi uzas PFS en TLS, TLS-sesiobiletoj (RFC 5077) por rekomenci ĉifritan sesion sen re-negocado de ŝlosiloj kaj sen stoki ŝlosilinformojn en la servilo. Kiam oni malfermas la unuan konekton kaj kreas ŝlosilojn, la servilo ĉifras la konektan staton kaj transdonas ĝin al la kliento (en formo de sesiobileto). Sekve, kiam la konekto estas rekomencita, la kliento sendas sean bileton, inkluzive de la sesioŝlosilo, reen al la servilo. La bileto mem estas ĉifrita per provizora ŝlosilo (sesiobiletoŝlosilo), kiu estas stokita sur la servilo kaj devas esti distribuita inter ĉiuj fasaj serviloj prilaboranta SSL en amasigitaj solvoj.[10]. Tiel, la enkonduko de sesiobileto povas malobservi PFS se provizoraj servilaj ŝlosiloj estas endanĝerigitaj, ekzemple, kiam ili estas konservitaj dum longa tempo (OpenSSL, nginx, Apache stokas ilin defaŭlte dum la tuta daŭro de la programo; popularaj retejoj uzas la ŝlosilo dum pluraj horoj, ĝis tagoj).

Ĉi tie la RTT ne estas nulo, vi devas interŝanĝi almenaŭ ClientHello kaj ServerHello, post kio la kliento povas sendi datumojn kune kun Finita. Sed ĉi tie ni memoru, ke ni ne havas la Reton, kun ĝia aro da nove malfermitaj konektoj, sed mesaĝiston, kies konekto estas ofte unu kaj pli-malpli longedaŭraj, relative mallongaj petoj al Retaj paĝoj - ĉio estas multipleksita. interne. Tio estas, estas tute akcepteble, se ni ne renkontis vere malbonan metroan sekcion.

Ĉu vi forgesis ion alian? Skribu en la komentoj.

Daŭrigota!

En la dua parto de ĉi tiu serio de afiŝoj ni konsideros ne teknikajn, sed organizajn aferojn - alirojn, ideologion, interfacon, sintenon al uzantoj ktp. Surbaze, tamen, de la teknikaj informoj, kiuj estis prezentitaj ĉi tie.

La tria parto daŭre analizos la teknikan komponan/disvolvan sperton. Vi lernos, precipe:

  • daŭrigo de la pandemonio kun la vario de TL-tipoj
  • nekonataj aferoj pri kanaloj kaj supergrupoj
  • kial dialogoj estas pli malbonaj ol listo
  • pri absoluta vs relativa mesaĝo-adresado
  • kio estas la diferenco inter foto kaj bildo
  • kiel emoji interferas kun kursiva teksto

kaj aliaj lambastonoj! Restu agordita!

fonto: www.habr.com

Aldoni komenton