Telegram protokola un organizatoriskās pieejas kritika. 1. daļa, tehniskā: klienta rakstīšanas pieredze no nulles - TL, MT

Pēdējā laikā vietnē Habré arvien biežāk parādās ieraksti par to, cik laba ir Telegram, cik izcili un pieredzējuši brāļi Durovi ir tīklu sistēmu veidošanā utt. Tajā pašā laikā ļoti maz cilvēku patiešām iedziļinājās tehniskajā ierīcē — parasti viņi izmanto diezgan vienkāršu (un ļoti atšķirīgu no MTProto) uz JSON balstītu Bot API un parasti vienkārši piekrīt. par ticību visas tās uzslavas un PR, kas grozās ap ziņnesi. Gandrīz pirms pusotra gada mans kolēģis NPO Echelon Vasily (diemžēl viņa konts Habré tika dzēsts kopā ar melnrakstu) sāka rakstīt pats savu Telegram klientu no nulles Perlā, un vēlāk pievienojās šo rindu autors. Kāpēc Perl, daži uzreiz jautās? Jo ir jau tādi projekti citās valodās.Patiesībā ne par to ir runa,varētu būt jebkura cita valoda,kur pabeigta bibliotēka, un attiecīgi autoram jāiet līdz galam no nulles. Turklāt kriptogrāfija ir tāda lieta - uzticieties, bet pārbaudiet. Izmantojot uz drošību vērstu produktu, jūs nevarat vienkārši paļauties uz pārdevēja gatavu bibliotēku un akli tam ticēt (tomēr šī tēma ir plašāka otrajā daļā). Šobrīd bibliotēka darbojas diezgan labi “vidējā” līmenī (ļauj veikt jebkādus API pieprasījumus).

Tomēr šajā rakstu sērijā nebūs daudz kriptogrāfijas un matemātikas. Bet būs daudz citu tehnisko detaļu un arhitektūras kruķu (noderēs arī tiem, kas nerakstīs no nulles, bet izmantos bibliotēku jebkurā valodā). Tātad, galvenais mērķis bija mēģināt īstenot klientu no nulles saskaņā ar oficiālo dokumentāciju. Tas ir, pieņemsim, ka oficiālo klientu pirmkods ir aizvērts (atkal otrajā daļā mēs sīkāk atklāsim tēmu par to, kas tas īsti ir tas notiek tātad), bet, piemēram, vecos laikos, ir tāds standarts kā RFC - vai ir iespējams uzrakstīt klientu tikai pēc specifikācijas, “bez ieskatīšanās” avota kodā, pat oficiālā (Telegram Desktop, mobilais ), pat neoficiālais Telethon?

Satura rādītājs:

Dokumentācija ... vai tā ir tur? Tā ir patiesība?..

Piezīmju fragmentus šim rakstam sāka vākt pagājušā gada vasarā. Visu šo laiku oficiālajā vietnē https://core.telegram.org dokumentācija bija uz 23. slāni, t.i. iestrēdzis kaut kur 2014. gadā (atcerieties, toreiz pat kanālu vēl nebija?). Protams, teorētiski tam vajadzēja dot iespēju 2014. gadā ieviest klientu ar tā laika funkcionalitāti. Bet pat šādā stāvoklī dokumentācija, pirmkārt, bija nepilnīga, otrkārt, vietām tā bija pretrunā pati ar sevi. Pirms nedaudz vairāk kā mēneša, 2019. gada septembrī, tas bija nejauši tika konstatēts, ka vietnē ir liels dokumentācijas atjauninājums, pilnīgi svaigam 105. slānim, ar piebildi, ka tagad viss ir jāizlasa vēlreiz. Patiešām, daudzi panti ir pārskatīti, bet daudzi ir palikuši nemainīgi. Tāpēc, lasot zemāk esošo kritiku par dokumentāciju, jāņem vērā, ka dažas no šīm lietām vairs nav aktuālas, bet dažas joprojām ir diezgan. Galu galā 5 gadi mūsdienu pasaulē ir ne tikai daudz, bet ļoti daudz. Kopš tā laika (īpaši, ja neņem vērā kopš tā laika izmestos un augšāmceltos ģeočatus) API metožu skaits shēmā ir pieaudzis no simts līdz vairāk nekā divsimt piecdesmit!

Kur jūs sākat kā jaunais rakstnieks?

Nav svarīgi, vai jūs rakstāt no nulles vai izmantojat, piemēram, gatavas bibliotēkas, piemēram, Teletons Python vai Madeline priekš PHP, jebkurā gadījumā vispirms būs nepieciešams reģistrēt savu pieteikumu Sākot no iegūt parametrus api_id и api_hash (tie, kas strādāja ar VKontakte API, uzreiz saprot), pēc kura serveris identificēs lietojumprogrammu. Šis jāuzņemas juridisku apsvērumu dēļ, bet par to, kāpēc bibliotēku autori to nevar publicēt, vairāk runāsim otrajā daļā. Varbūt jūs būsiet apmierināti ar testa vērtībām, lai gan tās ir ļoti ierobežotas - fakts ir tāds, ka tagad varat reģistrēties uz savu numuru tikai viens pieteikumu, tāpēc nesteidzieties ar galvu.

Tagad no tehniskā viedokļa mums vajadzēja būt ieinteresētiem, lai pēc reģistrācijas mums būtu jāsaņem paziņojumi no Telegram par dokumentācijas, protokola u.c. atjauninājumiem. Tas ir, varētu pieņemt, ka vietne ar dokiem tika vienkārši “novērtēta” un turpināja īpaši strādāt ar tiem, kuri sāka veidot klientus, jo. tas ir vieglāk. Bet nē, nekas tāds netika novērots, informācija nenāca.

Un, ja rakstāt no nulles, tad līdz saņemto parametru izmantošanai patiesībā vēl ir tālu. Lai gan https://core.telegram.org/ un par tiem vispirms tiek runāts sadaļā Darba sākšana, patiesībā jums vispirms ir jāievieš MTProto protokols - bet ja tu tici izkārtojums pēc OSI modeļa protokola vispārīgā apraksta lapas beigās, tad pilnīgi velti.

Faktiski gan pirms MTProto, gan pēc tam vairākos līmeņos vienlaikus (kā saka ārvalstu tīklnieki, kas strādā OS kodolā, slāņa pārkāpums) traucēs liela, sāpīga un briesmīga tēma ...

Binārā serializācija: TL (tipa valoda) un tās shēma, un slāņi un daudzi citi biedējoši vārdi

Šī tēma patiesībā ir Telegram problēmu atslēga. Un būs daudz briesmīgu vārdu, ja mēģināsit tajā iedziļināties.

Tātad, shēma. Ja atceries šo vārdu, saki: JSON shēmaTu pareizi domāji. Mērķis ir tāds pats: kāda valoda, lai aprakstītu iespējamo pārsūtīto datu kopu. Patiesībā ar to līdzība beidzas. Ja no lapas MTProto protokols, vai no oficiālā klienta avota koka, mēs mēģināsim atvērt kādu shēmu, mēs redzēsim kaut ko līdzīgu:

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;

Cilvēks, kurš to redz pirmo reizi, intuitīvi atpazīs tikai daļu no rakstītā - labi, tās ir acīmredzami struktūras (lai gan kur ir nosaukums, pa kreisi vai pa labi?), Tajos ir lauki, pēc kuriem tips iet caur kolu ... droši vien. Šeit, leņķa iekavās, iespējams, ir tādas veidnes kā C ++ (faktiski, ne īsti). Un ko nozīmē visi pārējie simboli, jautājuma zīmes, izsaukuma zīmes, procenti, režģi (un acīmredzot tie dažādās vietās nozīmē dažādas lietas), kaut kur, bet ne kaut kur, heksadecimālie skaitļi - un pats galvenais, kā no tā iegūt regulāri (kuru serveris nenoraidīs) baitu straume? Jums ir jāizlasa dokumentācija (Jā, tuvumā ir saites uz shēmu JSON versijā, taču tas to nepadara skaidrāku).

Lapas atvēršana Bināro datu serializācija un ienirt sēņu un diskrētās matemātikas maģiskajā pasaulē, kaut kas līdzīgs matānam 4. kursā. Alfabēts, tips, vērtība, kombinators, funkcionālais kombinators, normālā forma, saliktais tips, polimorfais tips... un tā ir tikai pirmā lapa! Nākamais jūs gaida TL valoda, kas, lai gan jau satur triviāla pieprasījuma un atbildes piemēru, vispār nesniedz atbildi uz tipiskākiem gadījumiem, kas nozīmē, ka nāksies brist cauri no krievu valodas angļu valodā tulkotās matemātikas pārstāstījumiem vēl astoņos ligzdotajos lapas!

Lasītāji, kas pārzina funkcionālās valodas un automātisko tipu secinājumus, protams, redzēja šīs valodas aprakstus, pat no piemēra, daudz pazīstamākus un var teikt, ka principā tas nav slikti. Iebildumi pret to ir:

  • Jā, mērķis izklausās labi, bet diemžēl nav sasniegts
  • izglītība Krievijas augstskolās atšķiras pat IT specialitātēs - ne visi lasa atbilstošo kursu
  • Visbeidzot, kā mēs redzēsim, praksē tas tā ir Tas neprasa, jo tiek izmantota tikai ierobežota pat aprakstītā TL apakškopa

Kā teica LeoNerds uz kanālu #perl FreeNode IRC tīklā, mēģinot ieviest vārtus no Telegram uz Matrix (citāta tulkojums ir neprecīzs no atmiņas):

Tāda sajūta, it kā kāds, kurš pirmo reizi tika iepazīstināts ar tipu teoriju, aizrāvās un sāka ar to spēlēties, īsti nerūpējoties par to, vai tas ir vajadzīgs praksē.

Paskatieties paši, vai nepieciešamība pēc plikiem tipiem (int, gari utt.) kā kaut kas elementārs nerada jautājumus - galu galā tie jārealizē manuāli - piemēram, mēģināsim no tiem atvasināt vektors. Tas ir, patiesībā, masīvs, ja iegūtās lietas sauc to īstajos vārdos.

Bet pirms tam

Īss TL sintakses apakškopas apraksts tiem, kas to nedara... lasiet oficiālo dokumentāciju

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;

Vienmēr sāk definīciju konstruktori, pēc kura pēc izvēles (praksē vienmēr) caur simbolu # vajadzētu CRC32 no dotā tipa normalizētās apraksta virknes. Tālāk seko lauku apraksts, ja tie ir - tips var būt tukšs. Tas viss beidzas ar vienādības zīmi, tā tipa nosaukumu, kuram pieder dotais konstruktors - tas ir, faktiski, apakštips. Veids pa labi no vienādības zīmes ir polimorfs - tas ir, tas var atbilst vairākiem konkrētiem veidiem.

Ja definīcija notiek pēc rindas ---functions---, tad sintakse paliks nemainīga, bet nozīme būs cita: konstruktors kļūs par RPC funkcijas nosaukumu, lauki kļūs par parametriem (nu, tas ir, tā paliks tieši tāda pati dotajā struktūrā, kā aprakstīts tālāk, tā būs tikai dotā nozīme), un "polimorfs tips" ir atgrieztā rezultāta veids. Tiesa, tas joprojām paliks polimorfs - tikko definēts sadaļā ---types---, un šis konstruktors netiks ņemts vērā. Ierakstiet izsaukto funkciju pārslodzes pēc to argumentiem, t.i. nez kāpēc TL nav paredzētas vairākas funkcijas ar tādu pašu nosaukumu, bet atšķirīgu parakstu, kā C++.

Kāpēc "konstruktors" un "polimorfs", ja tas nav OOP? Nu īstenībā kādam par to būs vieglāk domāt OOP izteiksmē - polimorfs tips kā abstrakta klase, un konstruktori ir tās tiešās pēcteču klases, turklāt final vairāku valodu terminoloģijā. Patiesībā, protams, šeit līdzība ar reālām pārslogotām konstruktora metodēm OO programmēšanas valodās. Tā kā šeit ir tikai datu struktūras, tad metožu nav (lai gan tālāk sniegtais funkciju un metožu apraksts diezgan spēj radīt neizpratni par to, kas tās ir, bet tas ir par ko citu) - konstruktoru var uzskatīt par vērtība, no kuras tiek būvēts ierakstiet, lasot baitu straumi.

Kā tas notiek? Deserializators, kas vienmēr nolasa 4 baitus, redz vērtību 0xcrc32 - un saprot, kas notiks tālāk field1 ar tipu int, t.i. nolasa tieši 4 baitus šajā augšējā laukā ar veidu PolymorType lasīt. Redz 0x2crc32 un saprot, ka ir divi lauki tālāk, pirmkārt long, tāpēc mēs nolasām 8 baitus. Un tad atkal sarežģīts tips, kas tiek deserializēts tādā pašā veidā. Piemēram, Type3 varētu deklarēt shēmā, tiklīdz attiecīgi divi konstruktori, tad tiem jāsatiekas vai nu 0x12abcd34, pēc kura jums jāizlasa vēl 4 baiti intVai 0x6789cdef, pēc kura nekas nebūs. Jebkas cits - vajag mest izņēmumu. Jebkurā gadījumā pēc tam mēs atgriežamies pie 4 baitu nolasīšanas int robežas field_c в constructorTwo un par to mēs pabeidzam lasīt mūsu PolymorType.

Visbeidzot, ja noķer 0xdeadcrc par constructorThree, tad lietas kļūst sarežģītākas. Mūsu pirmais lauks bit_flags_of_what_really_present ar tipu # - patiesībā tas ir tikai tipa pseidonīms natnozīmē "dabiskais skaitlis". Tas ir, patiesībā neparakstīts int ir vienīgais gadījums, starp citu, kad neparakstīti skaitļi tiek atrasti reālās shēmās. Tātad, nākamā ir konstrukcija ar jautājuma zīmi, kas nozīmē, ka šis ir lauks - tas būs vadā tikai tad, ja laukā, uz kuru ir atsauce, ir iestatīts atbilstošais bits (apmēram kā trīskāršs operators). Tātad, pieņemsim, ka šis bits bija ieslēgts, tad jums ir jāizlasa tāds lauks kā Type, kam mūsu piemērā ir 2 konstruktori. Viens ir tukšs (sastāv tikai no identifikatora), otram ir lauks ids ar tipu ids:Vector<long>.

Jūs varētu domāt, ka gan veidnes, gan sugas ir labas vai Java. Bet nē. Gandrīz. Šis vienīgais leņķa kronšteinu gadījumā reālās shēmās, un to izmanto TIKAI vektoram. Baitu straumē tie būs 4 CRC32 baiti pašam Vector tipam, vienmēr vienādi, pēc tam 4 baiti - masīva elementu skaits un pēc tam paši elementi.

Pievienojiet tam faktu, ka serializācija vienmēr notiek 4 baitu vārdos, visi veidi ir tā daudzkārtņi - ir aprakstīti arī iebūvētie veidi bytes и string ar manuālu garuma serializāciju un šo izlīdzināšanu ar 4 - labi, šķiet, ka tas izklausās normāli un pat salīdzinoši efektīvi? Lai gan tiek apgalvots, ka TL ir efektīva binārā serializācija, bet vai JSON joprojām būs daudz biezāks, ja tiek paplašināts jebkas, pat Būla vērtības un vienas rakstzīmes virknes līdz 4 baitiem? Skatieties, pat nevajadzīgos laukus var izlaist ar bitu karodziņiem, viss ir kārtībā un pat paplašināms nākotnei, vai vēlāk pievienojāt konstruktoram jaunus izvēles laukus?..

Bet nē, ja lasāt nevis manu īso aprakstu, bet pilnu dokumentāciju un domājat par ieviešanu. Pirmkārt, konstruktora CRC32 tiek aprēķināts pēc normalizētās shēmas teksta apraksta virknes (noņemt papildu atstarpes utt.) - tātad, ja tiek pievienots jauns lauks, mainīsies tipa apraksta virkne un līdz ar to arī CRC32 un attiecīgi serializācija. Un ko darītu vecais klients, ja saņemtu laukumu ar jauniem karogiem, bet viņš nezinātu, ko ar tiem darīt tālāk? ..

Otrkārt, atcerēsimies CRC32, kas šeit tiek izmantots būtībā kā hash funkcijas lai unikāli noteiktu, kāds veids tiek (de)serializēts. Šeit mēs saskaramies ar sadursmju problēmu - un nē, varbūtība nav viena no 232, bet daudz vairāk. Kurš atcerējās, ka CRC32 ir paredzēts, lai atklātu (un labotu) kļūdas sakaru kanālā un attiecīgi uzlabotu šīs īpašības, kaitējot citiem? Piemēram, viņai nerūp baitu permutācija: ja jūs saskaitāt CRC32 no divām rindām, otrajā jūs apmainīsit pirmos 4 baitus ar nākamajiem 4 baitiem - tas būs tas pats. Ja mums ir teksta virknes no latīņu alfabēta (un nedaudz pieturzīmju) un šie nosaukumi nav īpaši nejauši, šādas permutācijas iespējamība ir ievērojami palielināta.

Starp citu, kurš pārbaudīja, kas tur ir tiešām CRC32? Vienā no agrīnajiem avotiem (pat pirms Valtmana) bija hash funkcija, kas reizināja katru rakstzīmi ar skaitli 239, kas ir ļoti iemīļota šiem cilvēkiem, ha ha!

Visbeidzot, labi, mēs sapratām, ka konstruktori ar lauka tipu Vector<int> и Vector<PolymorType> būs atšķirīgs CRC32. Un kā ar prezentāciju uz līnijas? Un, runājot par teoriju, vai tas kļūst par tipa daļu? Pieņemsim, ka mēs nododam desmit tūkstošu skaitļu masīvu, labi, ar Vector<int> viss skaidrs, garums un vēl 40000 XNUMX baitu. Un ja šis Vector<Type2>, kas sastāv tikai no viena lauka int un tas ir vienīgais tipā - vai mums ir jāatkārto 10000xabcdef0 34 reizes un pēc tam 4 baiti int, vai valoda spēj mums to parādīt no konstruktora fixedVec un 80000 40000 baitu vietā atkal pārsūtīt tikai XNUMX XNUMX?

Šis nepavisam nav tukšs teorētisks jautājums - iedomājieties, ka jūs saņemat grupas lietotāju sarakstu, katram no kuriem ir id, vārds, uzvārds - mobilā savienojuma pārsūtīto datu apjoma atšķirība var būt ievērojama. Tā ir Telegram serializācijas efektivitāte, kas mums tiek reklamēta.

Tātad ...

Vektors, ko nevarēja izsecināt

Mēģinot pārlūkot kombinatoru un par to aprakstu lapas, jūs redzēsit, ka vektors (un pat matrica) formāli mēģina secināt vairākas lapas, izmantojot korteņus. Bet galu galā tie tiek āmuri, pēdējais solis tiek izlaists un vienkārši tiek dota vektora definīcija, kas arī nav saistīta ar tipu. Kas te par lietu? Valodās programmēšana, īpaši funkcionālās, diezgan tipiski ir struktūru aprakstīt rekursīvi - sastādītājs ar savu slinko izvērtējumu visu sapratīs un izdarīs. Valodā datu serializācija bet ir vajadzīga EFEKTIVITĀTE: pietiek vienkārši aprakstīt saraksts, t.i. divu elementu struktūra - pirmais ir datu elements, otrais ir tā pati struktūra vai tukša vieta astei (paka (cons) Lispā). Bet tas acīmredzot prasīs no katra elements papildus iztērē 4 baitus (CRC32 TL gadījumā), lai aprakstītu tā veidu. Ir viegli aprakstīt masīvu fiksēts izmērs, bet iepriekš nezināma garuma masīva gadījumā mēs pārtraucam.

Tā kā TL neļauj izvadīt vektoru, tas bija jāpievieno malā. Galu galā dokumentācijā teikts:

Serializācijā vienmēr tiek izmantots tas pats konstruktors “vector” (const 0x1cb5c415 = crc32 (“vector t:Type # [ t ] = Vector t”), kas nav atkarīgs no t tipa mainīgā konkrētās vērtības.

Izvēles parametra t vērtība nav iesaistīta serializācijā, jo tā ir atvasināta no rezultāta veida (vienmēr zināms pirms deserializācijas).

Apskatiet to tuvāk: vector {t:Type} # [ t ] = Vector t - bet nekur pati definīcija nepasaka, ka pirmajam skaitlim jābūt vienādam ar vektora garumu! Un tas ne no kurienes neizriet. Tas ir dots, kas jums jāpatur prātā un jāīsteno ar savām rokām. Citur dokumentācijā pat godīgi minēts, ka tips ir viltots:

Vektora t polimorfais pseidotips ir “tips”, kura vērtība ir jebkura veida t vērtību secība, kas ir iesaiņota vai tukša.

… bet nekoncentrējas uz to. Kad jūs, noguris no brist matemātikas stiepšanās (varbūt pat zināt no universitātes kursa), nolemjat ieskaitīt punktus un skatīties, kā ar to praktiski strādāt, galvā paliek iespaids: šeit Serious Mathematics ir balstīta uz , acīmredzami Cool People (divi matemātiķi - ACM uzvarētājs), un ne katrs. Mērķis - plātīties - ir sasniegts.

Starp citu, par numuru. Atsaukt # tas ir sinonīms nat, dabiskais skaitlis:

Ir tipa izteiksmes (tipaekspr) un skaitliskās izteiksmes (nat-ekspr). Tomēr tie ir definēti vienādi.

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

bet gramatikā tie ir aprakstīti tāpat, t.i. šī atšķirība atkal ir jāatceras un jāievieš ieviešanā ar roku.

Jā, veidņu veidi (vector<int>, vector<User>) ir kopīgs identifikators (#1cb5c415), t.i. ja zināt, ka zvans ir deklarēts kā

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

tad jūs gaidāt ne tikai vektoru, bet arī lietotāju vektoru. Precīzāk, vajadzētu pagaidiet - reālā kodā katram elementam, ja ne plikam tipam, būs konstruktors, un labā nozīmē realizācijā būtu jāpārbauda - un mums tika nosūtīts precīzi katrā šī vektora elementā tas tips? Un ja tas būtu kaut kāds PHP, kurā masīvs var saturēt dažādus veidus dažādos elementos?

Šajā brīdī sāc aizdomāties – vai tāds TL ir vajadzīgs? Varbūt ratam varētu izmantot cilvēku serializer, to pašu protobufu, kas jau toreiz bija? Tā bija teorija, paskatīsimies uz praksi.

Esošās TL implementācijas kodā

TL dzima VKontakte iekšienē jau pirms labi zināmajiem notikumiem ar Durova daļas pārdošanu un (protams), pat pirms Telegram izstrādes. Un atvērtā avotā pirmās ieviešanas avoti jūs varat atrast daudz smieklīgu kruķu. Un pati valoda tur tika ieviesta pilnīgāk nekā tagad Telegram. Piemēram, shēmā vispār netiek izmantotas jaucējzīmes (tas nozīmē iebūvēto pseidotipu (piemēram, vektoru) ar novirzēm). Or

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

bet pilnības labad apskatīsim attēlu, lai izsekotu, tā sakot, Domas Milža evolūcijai.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Vai arī šis skaistais:

    static const char *reserved_words_polymorhic[] = {

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

      };

Šis fragments ir par veidnēm, piemēram:

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

Šī ir hashmap veidnes tipa definīcija kā int un tipu pāru vektors. Programmā C++ tas izskatītos apmēram šādi:

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

tātad, alpha - atslēgvārds! Bet tikai C++ var rakstīt T, bet jāraksta alfa, beta... Bet ne vairāk par 8 parametriem, fantāzija beidzās uz teta. Tātad šķiet, ka reiz Sanktpēterburgā bija apmēram šādi dialogi:

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

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

Bet runa bija par pirmo izklāstīto TL ieviešanu "vispār". Pāriesim pie ieviešanas apsvērumiem faktiskajos Telegram klientos.

Bazilika vārds:

Vasilijs, [09.10.18 17:07] Visvairāk ēzelis ir karsts no tā, ka viņi uzskrūvēja abstrakciju gūzmu, pēc tam uzšāva skrūvi un pārklāja ar kruķiem kodinātāju.
Rezultātā vispirms no dokiem pilots.jpg
Pēc tam no jekichan.webp koda

Protams, no cilvēkiem, kas pārzina algoritmus un matemātiku, mēs varam sagaidīt, ka viņi ir izlasījuši Aho, Ullman un ir pazīstami ar de facto nozares standarta rīkiem, lai rakstītu savus DSL kompilatorus gadu desmitiem, vai ne? ..

Autors telegramma-kli ir Vitālijs Valtmans, kā var saprast no TLO formāta rašanās ārpus tā (kli) robežām, komandas biedrs - tagad ir piešķirta bibliotēka TL parsēšanai atsevišķikāds iespaids par viņu ir radies TL parsētājs? ..

16.12 04:18 Vasīlijs: manuprāt kāds nav apguvis lex + yacc
16.12 04:18 Vasilijs: citādi es nevaru izskaidrot
16.12 04:18 Vasilijs: nu vai viņiem maksāja par rindu skaitu VK
16.12 04:19 Vasilijs: 3k+ rindas citi<censored> parsētāja vietā

Varbūt izņēmums? Paskatīsimies, kā dara šis ir OFICIĀLAIS klients — 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+ rindiņas Pythonā, pāris regulāras izteiksmes + vektora tipa speciālie gadījumi, kas, protams, shēmā tiek deklarēti kā nākas pēc TL sintakses, bet uzliek uz šīs sintakses, parsē vairāk ... Jautājums ir, kāpēc uztraukties ar visu šo brīnumuиvēl pufs, ja tāpat neviens netaisās parsēt pēc dokumentācijas ?!

Starp citu... Atcerieties, ka mēs runājām par CRC32 pārbaudi? Tātad Telegram Desktop koda ģeneratorā ir izņēmumu saraksts tiem veidiem, kuros aprēķinātais CRC32 nesakrīt kā norādīts diagrammā!

Vasīlij, [18.12 22:49] un te vajadzētu padomāt vai tāds TL ir vajadzīgs
ja es gribētu jaukties ar alternatīvām implementācijām, es sāktu ievietot rindiņu pārtraukumus, puse parsētāju pārtrauks uz vairāku rindu definīcijām
tomēr arī tdesktop

Atcerieties punktu par viena laineriem, mēs atgriezīsimies pie tā nedaudz vēlāk.

Labi, telegram-klis ir neoficiāls, Telegram Desktop ir oficiāls, bet kā ar pārējiem? Un kas zina?.. Android klienta kodā vispār nebija shēmas parsētāja (kas rada jautājumus par atvērto avotu, bet šī ir otrajai daļai), bet bija vēl vairāki smieklīgi koda gabali, bet par tiem apakšsadaļā zemāk.

Kādus citus jautājumus seriālizēšana rada praksē? Piemēram, viņi, protams, sabojāja ar bitu laukiem un nosacījuma laukiem:

vasilijs: flags.0? true
nozīmē, ka lauks ir un ir patiess, ja karogs ir iestatīts

vasilijs: flags.1? int
nozīmē, ka lauks ir klāt un tas ir jādeserializē

Vasīlijs: Dupsi, nededzini, ko tu dari!
Vasīlijs: kaut kur dokumentā ir minēts, ka patiess ir tukšs nulles garuma tips, bet ir nereāli kaut ko savākt no viņu dokumentiem
Vasīlijs: Arī atklātajās implementācijās tāda nav, taču ir daudz kruķu un butaforiju

Kā ar Telethon? Skatoties uz priekšu par tēmu MTProto, piemērs - dokumentācijā ir tādi gabali, bet zīme % tas ir aprakstīts tikai kā "atbilstošs dotajam tukšajam tipam", t.i. zemāk esošajos piemēros ir vai nu kļūda, vai kaut kas nedokumentēts:

Vasilijs, [22.06.18/18/38 XNUMX:XNUMX] Vienā vietā:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Citā veidā:

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

Un tās ir divas lielas atšķirības, reālajā dzīvē nāk kaut kāds pliks vektors

Es neesmu redzējis tukšas vektoru definīcijas un neesmu ar to saskāries

Teletonā ar roku uzrakstīta analīze

Viņa shēma komentēja definīciju msg_container

Atkal paliek jautājums par %. Tas nav aprakstīts.

Vadims Gončarovs, [22.06.18. 19:22] un tdesktopā?

Vasilijs, [22.06.18/19/23 XNUMX:XNUMX] Bet to TL parsētājs uz regulatoriem arī laikam neēdīs

// parsed manually

TL ir skaista abstrakcija, neviens to pilnībā neīsteno

Un viņu shēmas versijā nav %

Bet te dokumentācija ir pretrunā pati ar sevi, tāpēc xs

Tas tika atrasts gramatikā, viņi varēja aizmirst aprakstīt semantiku

Nu tu redzēji doku uz TL, bez puslitra nevar izdomāt

"Nu, pieņemsim," teiks kāds cits lasītājs, "jūs kritizējat visu, tāpēc parādiet to, kā vajadzētu."

Vasilijs atbild: “Kas attiecas uz parsētāju, man ir vajadzīgas tādas lietas kā

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

kaut kā vairāk kā

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

vai

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

šis ir VISS lekseris:

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

tie. vienkāršāk ir maigi izsakoties."

Galu galā faktiski izmantotās TL apakškopas parsētājs un koda ģenerators iekļaujas aptuveni 100 gramatikas rindās un ~ 300 ģeneratora rindās (ieskaitot visas print's ģenerētais kods), tostarp tipveida labumi, tipa informācija ieskatiem katrā klasē. Katrs polimorfais tips tiek pārvērsts par tukšu abstraktu bāzes klasi, un konstruktori no tās manto, un tiem ir serializācijas un deserializācijas metodes.

Tipu trūkums tipa valodā

Spēcīga rakstīšana ir laba, vai ne? Nē, tas nav holivars (lai gan es dodu priekšroku dinamiskām valodām), bet gan postulāts TL ietvaros. Pamatojoties uz to, valodai mums vajadzētu nodrošināt visādas pārbaudes. Nu, labi, lai nevis viņš, bet gan īstenošana, bet viņam tās vismaz jāapraksta. Un kādas iespējas mēs vēlamies?

Pirmkārt, ierobežojumi. Šeit mēs redzam failu augšupielādes dokumentācijā:

Pēc tam faila binārais saturs tiek sadalīts daļās. Visām daļām jābūt vienāda izmēra ( daļas_izmērs ) un ir jāievēro šādi nosacījumi:

  • part_size % 1024 = 0 (dalāms ar 1 KB)
  • 524288 % part_size = 0 (512 KB jābūt vienmērīgi dalāmiem ar daļas_izmēru)

Pēdējai daļai nav jāatbilst šiem nosacījumiem, ja tās izmērs ir mazāks par part_size.

Katrai daļai jābūt kārtas numuram, faila_daļa, ar vērtību diapazonā no 0 līdz 2,999.

Kad fails ir sadalīts, jums jāizvēlas tā saglabāšanas metode serverī. izmantot upload.saveBigFilePart gadījumā, ja faila pilnais izmērs ir lielāks par 10 MB un upload.saveFilePart mazākiem failiem.
[…] var tikt atgriezta viena no šīm datu ievades kļūdām:

  • FILE_PARTS_INVALID — nederīgs daļu skaits. Vērtība nav starp 1..3000

Vai shēmā ir kāds no tiem? Vai tas ir kaut kā izsakāms ar TL palīdzību? Nē. Bet atvainojiet, pat vecmodīgais Turbo Pascal spēja aprakstīt veidus, ko dod diapazonos. Un viņš varēja darīt vēl vienu lietu, kas tagad ir labāk pazīstama kā enum - tips, kas sastāv no fiksēta (maza) vērtību skaita uzskaitījuma. Ņemiet vērā, ka tādās valodās kā C — ciparu, mēs līdz šim esam runājuši tikai par veidiem. skaitļi. Bet ir arī masīvi, virknes ... piemēram, būtu jauki aprakstīt, ka šajā virknē var būt tikai tālruņa numurs, vai ne?

Nekas no tā nav TL. Bet ir, piemēram, JSON shēmā. Un, ja kāds cits var iebilst pret 512 KB dalāmību, ka tas vēl ir jāpārbauda kodā, tad pārliecinieties, ka klients vienkārši nevarētu nosūtīt numuru ārpus diapazona 1..3000 (un atbilstošā kļūda nevarēja rasties) tas būtu iespējams, vai ne? ..

Starp citu, par kļūdām un atgriešanas vērtībām. Acis ir aizmiglotas pat tiem, kas ir strādājuši ar TL - tas mums uzreiz neienāca prātā katrs funkcija TL faktiski var atgriezt ne tikai aprakstīto atgriešanas veidu, bet arī kļūdu. Bet tas nav izsecināms, izmantojot pašu TL. Protams, tas ir saprotams un nafig praksē nav nepieciešams (lai gan patiesībā RPC var veikt dažādos veidos, mēs atgriezīsimies pie šī) - bet kā ir ar abstrakto tipu matemātikas jēdzienu tīrību no debesu pasaules? .. Paķēra velkoni - tātad sader.

Un visbeidzot, kā ar lasāmību? Nu tur, vispār, es gribētu Detalizēta informācija: vai tas ir pareizi shēmā (atkal, tas ir JSON shēmā), bet, ja tā jau ir saspringta, tad kā ar praktisko pusi - vismaz ir banāli skatīties atšķirības atjauninājumu laikā? Skatieties paši plkst reāli piemēri:

Sākot nochannelFull#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;

vai

Sākot nomessage#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;

Kādam tas patīk, bet, piemēram, GitHub atsakās izcelt izmaiņas tik garās rindās. Spēle “atrodi 10 atšķirības”, un smadzenes uzreiz redz, ka sākumi un beigas abos piemēros ir vienādi, jums ir garlaicīgi jālasa kaut kur pa vidu ... Manuprāt, tas nav tikai teorētiski, bet tīri vizuāli izskatās netīrs un nekopts.

Starp citu, par teorijas tīrību. Kāpēc ir nepieciešami bitu lauki? Vai viņiem nešķiet smarža slikti no tipu teorijas viedokļa? Paskaidrojumu var redzēt iepriekšējās shēmas versijās. Sākumā jā, tā bija, katram šķaudīšanai tika izveidots jauns veids. Šie elementi joprojām ir pieejami šādā formā, piemēram:

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;

Bet tagad iedomājieties, ja jūsu struktūrā ir 5 izvēles lauki, tad jums ir nepieciešami 32 veidi visām iespējamām opcijām. kombinatoriskais sprādziens. Tātad TL teorijas kristāla tīrība atkal atdūrās pret čuguna ēzeli, ko raksturo sērijveida realitāte.

Turklāt vietām šie puiši paši pārkāpj savu rakstīto. Piemēram, MTProto (nākamā nodaļa) atbildi var saspiest ar Gzip, viss ir saprātīgs - izņemot slāņu un shēmas pārkāpumu. Vienreiz, un pļāva nevis pašu RpcResult, bet gan tā saturu. Nu, kāpēc to darīt?.. Man vajadzēja iegriezt kruķi, lai kompresija darbotos jebkurā vietā.

Vai cits piemērs, mēs reiz atradām kļūdu - nosūtīts InputPeerUser nevis InputUser. Vai arī otrādi. Bet izdevās! Tas ir, serverim nerūpēja veids. Kā tas var būt? Atbildi, iespējams, pamudinās koda fragmenti no 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);

Citiem vārdiem sakot, šeit ir veikta serializācija MANUĀLI, nav ģenerēts kods! Varbūt serveris ir realizēts līdzīgā veidā?.. Principā tas darbosies, ja to vienreiz izdarītu, bet kā to vēlāk var atbalstīt ar atjauninājumiem? Vai tad shēma nav domāta tam? Un tad mēs pārejam pie nākamā jautājuma.

Versionēšana. Slāņi

Kāpēc shēmu versijas sauc par slāņiem, var tikai uzminēt, pamatojoties uz publicēto shēmu vēsturi. Acīmredzot sākotnēji autoriem šķita, ka elementāras lietas var izdarīt pēc nemainīgas shēmas, un tikai nepieciešamības gadījumā uz konkrētiem pieprasījumiem norādīt, ka tiek darīts pēc citas versijas. Principā pat laba ideja - un jaunais it kā “sajauksies”, uzslānīs veco. Bet paskatīsimies, kā tas tika izdarīts. Tiesa, no paša sākuma nebija iespējams paskatīties - tas ir smieklīgi, bet bāzes slāņa shēma vienkārši neeksistē. Slāņi sākās ar 2. Dokumentācija stāsta par īpašu TL līdzekli:

Ja klients atbalsta 2. slāni, ir jāizmanto šāds konstruktors:

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

Praksē tas nozīmē, ka pirms katra API izsaukuma tiek parādīts int ar vērtību 0x289dd1f6 jāpievieno pirms metodes numura.

Izklausās OK. Bet kas notika tālāk? Tad nāca

invokeWithLayer3#b7475268 query:!X = X;

Tātad, kas ir tālāk? Kā to ir viegli uzminēt

invokeWithLayer4#dea0d430 query:!X = X;

Smieklīgi? Nē, vēl par agru smieties, padomājiet par ko katrs pieprasījums no cita slāņa jāiesaiņo tādā īpašā veidā - ja jums tie visi ir atšķirīgi, kā citādi tos atšķirt? Un tikai 4 baitu pievienošana priekšā ir diezgan efektīva metode. Tātad

invokeWithLayer5#417a57ae query:!X = X;

Bet skaidrs, ka pēc kāda laika tas kļūs par kādu bakhanāliju. Un risinājums nāca:

Atjauninājums: sākot ar 9. slāni, palīgmetodēm invokeWithLayerN var lietot kopā ar initConnection

Urrā! Pēc 9 versijām beidzot nonācām pie tā, kas tika darīts interneta protokolos tālajā 80. gados - versijas sarunas vienu reizi savienojuma sākumā!

Tātad, kas būs tālāk? ..

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

Un tagad jūs varat smieties. Tikai pēc vēl 9 slāņiem beidzot tika pievienots universāls konstruktors ar versijas numuru, kuru savienojuma sākumā vajag izsaukt tikai vienu reizi un slāņos nozīme it kā ir pazudusi, tagad tā ir tikai nosacītā versija, kā visur citur. Problēma atrisināta.

Pa labi?..

Vasilijs, [16.07.18/14/01 XNUMX:XNUMX PM] Piektdien es domāju:
Teleserveris nosūta notikumus bez pieprasījuma. Pieprasījumi ir jāiesaiņo InvokeWithLayer. Serveris neapkopo atjauninājumus, nav struktūras atbilžu un atjauninājumu iesaiņošanai.

Tie. klients nevar norādīt slāni, kurā viņš vēlas atjaunināt

Vadims Gončarovs, [16.07.18. 14:02] Vai InvokeWithLayer principā nav kruķis?

Vasilijs, [16.07.18/14/02 XNUMX:XNUMX PM] Tas ir vienīgais veids

Vadims Gončarovs, [16.07.18/14/02 XNUMX:XNUMX PM], kas būtībā nozīmē slāņošanu sesijas sākumā

Starp citu, no tā izriet, ka klienta pazemināšana netiek nodrošināta

Atjauninājumi, t.i. veids Updates shēmā tas ir tas, ko serveris nosūta klientam nevis atbildot uz API pieprasījumu, bet gan pats par sevi, kad notiek notikums. Šī ir sarežģīta tēma, kas tiks apspriesta citā ierakstā, taču šobrīd ir svarīgi zināt, ka serveris uzkrāj atjauninājumus pat tad, kad klients ir bezsaistē.

Tādējādi, atsakoties ietīt no katra pakotni, lai norādītu tās versiju, tāpēc loģiski rodas šādas iespējamās problēmas:

  • serveris nosūta klientam atjauninājumus, pirms klients ir paziņojis, kuru versiju tas atbalsta
  • kas jādara pēc klienta jaunināšanas?
  • kurš garantijaska servera viedoklis par slāņa numuru procesa gaitā nemainīsies?

Vai jūs domājat, ka tā ir tīri teorētiska domāšana, un praksē tas nevar notikt, jo serveris ir uzrakstīts pareizi (jebkurā gadījumā tas ir labi pārbaudīts)? Ha! Vienalga, kā!

Tieši ar to mēs saskārāmies augustā. 14. augustā parādījās ziņojumi, ka Telegram serveros kaut kas tiek atjaunināts ... un pēc tam žurnālos:

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.

un pēc tam dažus megabaitus steka trases (nu, tajā pašā laikā tika fiksēta reģistrēšana). Galu galā, ja kaut kas netika atpazīts jūsu TL - tas ir binārs pēc parakstiem, tālāk straumē ALL iet, dekodēšana kļūs neiespējama. Ko darīt šādā situācijā?

Pirmā lieta, kas ikvienam ienāk prātā, ir atvienoties un mēģināt vēlreiz. Nepalīdzēja. Pameklējām googlē CRC32 - tie izrādījās objekti no 73. shēmas, lai gan strādājām pie shēmas 82. Uzmanīgi apskatām žurnālus - tur ir identifikatori no divām dažādām shēmām!

Varbūt problēma ir tikai mūsu neoficiālajā klientā? Nē, mēs palaižam Telegram Desktop 1.2.17 (versija, kas tiek piegādāta ar vairākiem Linux izplatījumiem), tā ieraksta izņēmumu žurnālā: MTP Negaidīts tipa ID #b5223b0f, kas lasīts MTPMessageMedia…

Telegram protokola un organizatoriskās pieejas kritika. 1. daļa, tehniskā: klienta rakstīšanas pieredze no nulles - TL, MT

Google parādīja, ka līdzīga problēma jau bija gadījusies vienam no neoficiālajiem klientiem, taču tad versiju numuri un attiecīgi arī pieņēmumi bija atšķirīgi...

Tātad, ko darīt? Mēs ar Vasīliju izšķīrāmies: viņš mēģināja atjaunināt shēmu uz 91, es nolēmu pagaidīt dažas dienas un mēģināt līdz 73. Abas metodes darbojās, bet, tā kā tās ir empīriskas, nav izpratnes par to, cik versiju jums ir nepieciešams uzlēkt uz augšu. vai uz leju, ne arī cik ilgi jāgaida .

Vēlāk izdevās situāciju atveidot: palaižam klientu, izslēdzam, pārkomponējam shēmu citā slānī, restartējam, atkal noķeram problēmu, atgriežamies pie iepriekšējās - ups, nekādas shēmas pārslēgšanas un klienta restartēšanas vairākas reizes minūtes palīdzēs. Jūs saņemsit datu struktūru sajaukumu no dažādiem slāņiem.

Paskaidrojums? Kā jūs varat uzminēt no dažādiem netiešajiem simptomiem, serveris sastāv no daudziem dažādu veidu procesiem dažādās iekārtās. Visticamāk, ka tas no serveriem, kas ir atbildīgs par “buferizāciju”, rindā ielika to, ko iedeva augstākie, un iedeva shēmā, kāda bija ģenerēšanas brīdī. Un kamēr šī rinda nebija “sapuvusi”, ar to neko nevarēja darīt.

Ja vien... bet tas ir baigais kruķis?!.. Nē, pirms domāt par trakām idejām, paskatīsimies oficiālo klientu kodeksā. Android versijā mēs neatrodam nevienu TL parsētāju, bet atrodam dūšīgu failu (github atsakās to krāsot) ar (de)serializāciju. Šeit ir koda fragmenti:

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;

vai

    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... izskatās traki. Bet, iespējams, tas ir ģenerēts kods, tad labi? .. Bet tas noteikti atbalsta visas versijas! Tiesa, nav skaidrs, kāpēc viss ir jaukts vienā kaudzē, un slepenie čati, un visādi _old7 kaut kā nelīdzinās mašīnu ģenerēšanai... Tomēr lielākā daļa no visa man aizgāja rieksti

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Puiši, vai jūs pat nevarat izlemt viena slāņa ietvaros?! Nu labi, "divus", teiksim, izlaida ar kļūdu, nu gadās, bet TRĪS?.. Tūlīt atkal uz tā paša grābekļa? Kas tā par pornogrāfiju, atvainojiet? ..

Starp citu, līdzīga lieta notiek arī Telegram Desktop avotos - ja tā, un vairākas shēmas pēc kārtas nemaina tās slāņa numuru, bet gan kaut ko labo. Apstākļos, kad shēmai nav oficiāla datu avota, kur tos iegūt, izņemot oficiālos klientu avotus? Un, ja jūs to ņemat no turienes, jūs nevarat būt pārliecināts, ka shēma ir pilnīgi pareiza, kamēr nepārbaudīsit visas metodes.

Kā to vispār var pārbaudīt? Ceru, ka vienību, funkcionālo un citu testu cienītāji dalīsies komentāros.

Labi, apskatīsim vēl vienu koda daļu:

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;

Šis “manuāli izveidotais” komentārs liecina, ka tikai daļa no šī faila ir rakstīta ar roku (vai varat iedomāties apkopes murgu?), bet pārējais ir ģenerēts ar mašīnu. Taču tad rodas cits jautājums – ka avoti ir pieejami ne pilnībā (a la blobs zem GPL Linux kodolā), bet tas jau ir otrās daļas tēma.

Bet pietiek. Pārejam pie protokola, kuram virsū visa šī serializācija dzenas.

MT Proto

Tātad atveram vispārīgs apraksts и detalizēts protokola apraksts un pirmais, kam paklupam, ir terminoloģija. Un ar visa pārpilnību. Kopumā šķiet, ka šī ir Telegram preču zīme - saukt lietas dažādās vietās dažādos veidos vai dažādas lietas vienā vārdā, vai otrādi (piemēram, augsta līmeņa API, ja redzat uzlīmju pakotni - šis nav tas, ko jūs domājāt).

Piemēram, "ziņa" (ziņa) un "sesija" (sesija) - šeit tie nozīmē kaut ko citu nekā parastajā Telegram klienta saskarnē. Nu, ar ziņojumu viss ir skaidrs, to varētu interpretēt OOP izteiksmē vai vienkārši saukt par vārdu "paka" - tas ir zems, transporta līmenis, nav tādi paši ziņojumi kā saskarnē, ir daudz pakalpojumu. Bet sesija ... bet vispirms vispirms.

transporta slānis

Pirmā lieta ir transports. Mums tiks pastāstīts par 5 iespējām:

  • TCP
  • Tīmekļa ligzda
  • Websocket, izmantojot HTTPS
  • HTTP
  • HTTPS

Vasilijs, [15.06.18/15/04 XNUMX:XNUMX] Un ir arī UDP transports, bet tas nav dokumentēts

Un TCP trīs variantos

Pirmais ir līdzīgs UDP, izmantojot TCP, katrā paketē ir kārtas numurs un crc
Kāpēc ir tik sāpīgi lasīt dokus uz ratiņiem?

Nu tur tagad TCP jau 4 variantos:

  • saīsināts
  • Starpposma
  • polsterēts starpposms
  • pilns

Labi, polsterēts starpprodukts MTProxy, tas vēlāk tika pievienots zināmu notikumu dēļ. Bet kāpēc vēl divas versijas (kopā trīs), ja varētu darīt vienu? Visi četri būtībā atšķiras tikai ar to, kā iestatīt paša galvenā MTProto garumu un kravnesību, kas tiks apspriests tālāk:

  • Saīsinātajā tas ir 1 vai 4 baiti, bet ne 0xef, tad pamatteksts
  • In Intermediate tas ir 4 baiti garš un lauks, un pirmo reizi klientam ir jānosūta 0xeeeeeeee lai norādītu, ka tas ir vidējs
  • in Full, visvairāk atkarību, no tīkla lietotāja viedokļa: garums, kārtas numurs un NE TAS, kas būtībā ir MTProto, korpuss, CRC32. Jā, tas viss, izmantojot TCP. Kas nodrošina mums uzticamu transportu sērijas baitu straumes veidā, nav vajadzīgas nekādas sekvences, īpaši kontrolsummas. Labi, tagad viņi man iebildīs, ka TCP ir 16 bitu kontrolsumma, tāpēc notiek datu sabojāšana. Lieliski, izņemot to, ka mums faktiski ir kriptogrāfijas protokols ar jaucējkrājumu, kas ir garāks par 16 baitiem, visas šīs kļūdas — un pat vairāk — tiks konstatētas SHA neatbilstības dēļ augstākā līmenī. CRC32 tam nav jēgas.

Salīdzināsim Abridged, kur ir iespējams viens baits garums, ar Intermediate, kas attaisno "Ja ir nepieciešama 4 baitu datu izlīdzināšana", kas ir diezgan absurds. Kas, tiek uzskatīts, ka Telegram programmētāji ir tik neveikli, ka nevar nolasīt datus no ligzdas izlīdzinātā buferī? Jums tas joprojām ir jādara, jo lasīšana var atgriezt jebkuru baitu skaitu (un ir arī starpniekserveri, piemēram ...). Vai, no otras puses, kāpēc uztraukties ar Abridged, ja mums joprojām ir lieli polsterējumi no 16 baitiem augšā — ietaupiet 3 baitus dažreiz ?

Rodas iespaids, ka Nikolajam Durovam ļoti patīk bez reālas praktiskas vajadzības izgudrot velosipēdus, tostarp tīkla protokolus.

Citas transporta iespējas, t.sk. Web un MTProxy, mēs tagad neņemsim vērā, varbūt citā ierakstā, ja būs pieprasījums. Mēs tikai tagad atcerēsimies par šo MTProxy, ka drīz pēc tā izlaišanas 2018. gadā pakalpojumu sniedzēji ātri iemācījās bloķēt tieši to, kas paredzēts bloķēt apvedceļuAr paciņas izmērs! Un arī tas, ka MTProxy serveris (atkal Waltmana) C valodā bija lieki piesaistīts Linux specifikai, lai gan tas nemaz nebija vajadzīgs (Fils Kulins apstiprinās), un ka līdzīgs serveris vai nu Go, vai Node.js atbilst mazāk nekā simts rindiņām.

Bet secinājumus par šo cilvēku tehnisko pratību izdarīsim sadaļas beigās, pārdomājot citus jautājumus. Pagaidām pāriesim pie 5. OSI slāņa, sesijas – uz kuras viņi ievietoja MTProto sesiju.

Atslēgas, ziņas, sesijas, Difijs-Helmans

Viņi to ievietoja ne gluži pareizi... Sesija nav sesija, kas ir redzama saskarnes sadaļā Aktīvās sesijas. Bet kārtībā.

Telegram protokola un organizatoriskās pieejas kritika. 1. daļa, tehniskā: klienta rakstīšanas pieredze no nulles - TL, MT

Šeit mēs esam saņēmuši zināma garuma baitu virkni no transporta slāņa. Tas ir vai nu šifrēts ziņojums, vai vienkāršs teksts — ja mēs joprojām esam atslēgas sarunu stadijā un faktiski to darām. Par kuru no jēdzieniem, ko sauc par "atslēgu", mēs runājam? Noskaidrosim šo jautājumu pašai Telegram komandai (atvainojos par paša dokumentācijas tulkošanu no angļu valodas vai nu nogurušām smadzenēm 4 no rīta, dažas frāzes bija vieglāk atstāt tādas, kādas tās ir):

Ir divas vienības, ko sauc Sesija - viens oficiālo klientu lietotāja saskarnē sadaļā "pašreizējās sesijas", kur katra sesija atbilst visai ierīcei / OS.
Otrais ir MTProto sesija, kurā ir ziņojuma kārtas numurs (zema līmeņa nozīmē) un kurš var ilgt starp dažādiem TCP savienojumiem. Vienlaikus var iestatīt vairākas MTProto sesijas, piemēram, lai paātrinātu failu lejupielādi.

Starp šiem diviem sesijas ir jēdziens atļauja. Deģenerāta gadījumā tā var teikt UI sesija ir tāds pats kā atļaujaBet diemžēl tas ir sarežģīti. Mēs skatāmies:

  • Lietotājs jaunajā ierīcē vispirms ģenerē auth_key un sasaista to ar kontu, piemēram, ar īsziņu starpniecību – tāpēc atļauja
  • Tas notika pirmajā iekšā MTProto sesija, kam ir session_id sevī iekšā.
  • Šajā posmā kombinācija atļauja и session_id varētu nosaukt piemērs - šis vārds ir atrodams dažu klientu dokumentācijā un kodā
  • Pēc tam klients var atvērt daži MTProto sesijas zem tā paša auth_key - uz to pašu DC.
  • Tad kādu dienu klientam ir jāpieprasa fails no vēl viens DC - un šim DC tiks ģenerēts jauns auth_key !
  • Lai pateiktu sistēmai, ka tas nav jauns lietotājs, kas reģistrējas, bet tas pats atļauja (UI sesija), klients izmanto API zvanus auth.exportAuthorization mājas DC auth.importAuthorization jaunajā DC.
  • Tomēr atvērti var būt vairāki MTProto sesijas (katram savs session_id) uz šo jauno DC, zem viņa auth_key.
  • Visbeidzot, klients var vēlēties Perfect Forward Secrecy. Katrs auth_key bija pastāvīgs atslēga - pa DC - un klients var piezvanīt auth.bindTempAuthKey lietošanai pagaidu auth_key - un atkal tikai viens temp_auth_key uz DC, visiem kopīgs MTProto sesijas uz šo DC.

Ņemiet vērā, ka sāls (un nākotnes sāļi) arī viens uz auth_key tie. dalīta starp visiem MTProto sesijas uz to pašu DC.

Ko nozīmē “starp dažādiem TCP savienojumiem”? Tas nozīmē, ka šis kaut kas kā autorizācijas sīkfails vietnē - tas saglabā (izdzīvo) daudzus TCP savienojumus ar šo serveri, taču kādu dienu tas sabojāsies. Tikai atšķirībā no HTTP, MTProto sesijas iekšienē ziņojumi tiek secīgi numurēti un apstiprināti, tie iekļuva tunelī, savienojums pārtrūka - pēc jauna savienojuma izveides serveris laipni nosūtīs visu šajā sesijā, ko tas nepiegādāja iepriekšējais TCP savienojums.

Tomēr iepriekš sniegtā informācija ir saspiesta pēc daudzu mēnešu ilgas tiesvedības. Vai tikmēr mēs ieviešam savu klientu no nulles? - atgriezīsimies sākumā.

Tātad mēs ģenerējam auth_key par Diffie-Hellman versijas no Telegram. Mēģināsim izprast dokumentāciju...

Vasilijs, [19.06.18/20/05 1:255] data_with_hash := SHAXNUMX(data) + dati + (jebkuri nejauši baiti); tā, lai garums būtu vienāds ar XNUMX baitiem;
šifrēti_dati := RSA(dati_ar_hash, servera_publiskā_atslēga); 255 baitu garš skaitlis (lielais endians) tiek palielināts līdz vajadzīgajai jaudai pār nepieciešamo moduli, un rezultāts tiek saglabāts kā 256 baitu skaitlis.

Viņi dabūja drosmīgu DH

Neizskatās pēc veselīga cilvēka DH
dx nav divu publisko atslēgu

Nu beigās izdomājām, bet nogulsnes palika - darba apliecinājums tiek veikts no klienta puses, ka spējis skaitļus faktorizēt. Aizsardzības veids pret DoS uzbrukumiem. Un RSA atslēga tiek izmantota tikai vienu reizi vienā virzienā, galvenokārt šifrēšanai new_nonce. Bet, kamēr šī šķietami vienkāršā darbība izdosies, ar ko jums būs jāsaskaras?

Vasīlij, [20.06.18/00/26 XNUMX:XNUMX] Es vēl neesmu sasniedzis appid pieprasījumu

Nosūtīju pieprasījumu DH

Un transporta dokā rakstīts, ka var atbildēt ar 4 baitiem kļūdas koda. Un viss

Nu, viņš man teica -404, un ko tad?

Šeit es viņam esmu: "noķer savu efigna, kas šifrēta ar servera atslēgu ar tādu un tādu pirkstu nospiedumu, es gribu DH", un tas muļķīgi atbild 404

Ko jūs domājat par šādu servera atbildi? Ko darīt? Nav kam jautāt (bet par to vairāk otrajā daļā).

Šeit visa dokā interese ir jādara

Man nav nekā cita, ko darīt, es tikai sapņoju par skaitļu konvertēšanu uz priekšu un atpakaļ

Divi 32 bitu skaitļi. Es tos iesaiņoju tāpat kā visus citus

Bet nē, tieši šie divi jums ir vajadzīgi vispirms rindā kā BE

Vadims Gončarovs, [20.06.18. 15:49] un šī 404 dēļ?

Vasilijs, [20.06.18. 15:49] JĀ!

Vadims Gončarovs, [20.06.18/15/50 XNUMX:XNUMX PM] tāpēc es nesaprotu, ko viņš var "neatradu"

Vasilijs, [20.06.18 15:50] par

Es neatradu šādu sadalīšanos vienkāršos dalītājos%)

Pat kļūdu ziņošana netika apgūta

Vasilijs, [20.06.18. 20:18] Ak, ir arī MD5. Jau trīs dažādi hash

Atslēgas pirkstu nospiedumu aprēķina šādi:

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

SHA1 un sha2

Tātad liekam auth_key Saskaņā ar Difiju-Helmenu mēs saņēmām 2048 bitu lielumu. Ko tālāk? Tad mēs uzzinām, ka šīs atslēgas apakšējie 1024 biti netiek izmantoti nekādi... bet padomāsim par to pagaidām. Šajā solī mums ir kopīgs noslēpums ar serveri. Ir izveidots TLS sesijas analogs, kas ir ļoti dārga procedūra. Bet serveris vēl neko nezina par to, kas mēs esam! Patiesībā vēl nē autorizācija. Tie. ja domājāt par “pieteikšanās paroli”, kā tas bija ICQ, vai vismaz “pieteikšanās atslēgu”, kā SSH (piemēram, kādā gitlab / github). Mēs kļuvām anonīmi. Un ja serveris mums atbild "šos tālruņu numurus apkalpo cits DC"? Vai pat “jūsu tālruņa numurs ir aizliegts”? Labākais, ko varam darīt, ir saglabāt atslēgu, cerot, ka tā joprojām noderēs un līdz tam laikam nebūs sapuvusi.

Starp citu, mēs to "saņēmām" ar atrunām. Piemēram, vai mēs uzticamies serverim? Vai viņš ir viltots? Mums ir nepieciešamas kriptogrāfijas pārbaudes:

Vasilijs, [21.06.18/17/53 2:XNUMX] Viņi piedāvā mobilajiem klientiem vienkāršības labad pārbaudīt XNUMXkbitu numuru%)

Bet tas vispār nav skaidrs, nafeijoa

Vasilijs, [21.06.18/18/02 XNUMX:XNUMX] Doks nesaka, ko darīt, ja izrādījās, ka tas nav vienkārši

Nav teikts. Apskatīsim, ko šajā gadījumā dara oficiālais Android klients? A tas ir kas (un jā, viss fails tur ir interesants) - kā saka, es to vienkārši atstāšu šeit:

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

Nē, protams, tur daži ir pārbaudes par skaitļa vienkāršību, bet man personīgi vairs nav pietiekamu zināšanu matemātikā.

Labi, mēs saņēmām galveno atslēgu. Lai pieteiktos, t.i. sūtīt pieprasījumus, nepieciešams veikt turpmāku šifrēšanu, jau izmantojot AES.

Ziņojuma atslēga ir definēta kā ziņojuma pamatteksta SHA128 256 vidējie biti (ieskaitot sesiju, ziņojuma ID utt.), ieskaitot aizpildīšanas baitus, kam pievienoti 32 baiti, kas ņemti no autorizācijas atslēgas.

Vasilijs, [22.06.18 14:08] Vidējas kuces

Saņemts auth_key. Visi. Tālāk viņiem ... tas nav skaidrs no dokiem. Jūtieties brīvi izpētīt atvērtā pirmkoda kodu.

Ņemiet vērā, ka MTProto 2.0 ir nepieciešams no 12 līdz 1024 baitiem aizpildījuma, tomēr ar nosacījumu, ka iegūtais ziņojuma garums dalās ar 16 baitiem.

Tātad, cik daudz polsterējuma ievietot?

Un jā, arī šeit 404 kļūdas gadījumā

Ja kāds rūpīgi pētīja diagrammu un dokumentācijas tekstu, viņš pamanīja, ka tur nav MAC. Un ka AES tiek izmantots kādā IGE režīmā, kas netiek izmantots nekur citur. Viņi, protams, raksta par to savos FAQ... Šeit, piemēram, pati ziņojuma atslēga vienlaikus ir atšifrēto datu SHA hash, ko izmanto, lai pārbaudītu integritāti - un neatbilstības gadījumā dokumentāciju kāds iemesls iesaka tos klusībā ignorēt (bet kā ar drošību, pēkšņi mūs salauzt?).

Es neesmu kriptogrāfs, varbūt šajā režīmā šajā gadījumā nav nekā slikta no teorētiskā viedokļa. Bet es noteikti varu nosaukt praktisku problēmu, izmantojot Telegram Desktop piemēru. Tas šifrē lokālo kešatmiņu (visus šos D877F783D5D3EF8C) tāpat kā ziņojumus MTProto (tikai šajā gadījumā versija 1.0), t.i. vispirms ziņojuma atslēga, pēc tam paši dati (un kaut kur malā galvenais lielais auth_key 256 baiti, bez kuriem msg_key bezjēdzīgi). Tātad problēma kļūst pamanāma lielos failos. Proti, jāsaglabā divas datu kopijas – šifrētas un atšifrētas. Un, ja ir megabaiti vai, piemēram, straumēšanas video? .. Klasiskās shēmas ar MAC pēc šifrētā teksta ļauj to lasīt straumējot, nekavējoties pārsūtot. Un ar MTProto jums tas ir jādara sākumā šifrējiet vai atšifrējiet visu ziņojumu, tikai pēc tam pārsūtiet to uz tīklu vai disku. Tāpēc jaunākajās Telegram Desktop versijās kešatmiņā user_data jau tiek izmantots cits formāts - ar AES VKS režīmā.

Vasilijs, [21.06.18/01/27 20:XNUMX] Ak, es uzzināju, kas ir IGE: IGE bija pirmais mēģinājums izmantot "autentifikācijas šifrēšanas režīmu", kas sākotnēji bija paredzēts Kerberos. Tas bija neveiksmīgs mēģinājums (tas nenodrošina integritātes aizsardzību), un tas bija jānoņem. Tas bija sākums XNUMX gadu meklējumiem pēc autentifikācijas šifrēšanas režīma, kas darbojas, kas nesen beidzās ar tādiem režīmiem kā OCB un GCM.

Un tagad argumenti no ratu puses:

Komanda aiz Telegram, kuru vada Nikolajs Durovs, sastāv no sešiem ACM čempioniem, no kuriem puse ir matemātikas doktori. Viņiem bija nepieciešami aptuveni divi gadi, lai ieviestu pašreizējo MTProto versiju.

Kas tur jocīgs. Divi gadi līdz zemākajam līmenim

Vai arī mēs varētu vienkārši ņemt tls

Labi, pieņemsim, ka esam veikuši šifrēšanu un citas nianses. Vai mēs beidzot varam nosūtīt TL serializētus pieprasījumus un deserializēt atbildes? Kas tad jāsūta un kā? Šeit ir metode initConnectionvarbūt tas ir tas?

Vasilijs, [25.06.18/18/46 XNUMX:XNUMX] Inicializē savienojumu un saglabā informāciju lietotāja ierīcē un lietojumprogrammā.

Tā pieņem app_id, device_model, system_version, app_version un lang_code.

Un daži vaicājumi

Dokumentācija kā vienmēr. Jūtieties brīvi izpētīt atvērto avotu

Ja ar invokeWithLayer viss bija aptuveni skaidrs, kas tas ir? Izrādās, ka mums ir - klientam jau bija par ko jautāt serverim - ir pieprasījums, kuru mēs gribējām nosūtīt:

Vasilijs, [25.06.18/19/13 XNUMX:XNUMX] Spriežot pēc koda, pirmais zvans ir ietīts šajā miskastē, un pats atkritums ir invokewithlayer

Kāpēc initConnection nevarētu būt atsevišķs zvans, bet tam ir jābūt iesaiņojumam? Jā, kā izrādījās, tas jādara katru reizi katras sesijas sākumā, nevis vienreiz, kā ar galveno taustiņu. Bet! To nevar izsaukt neautorizēts lietotājs! Šeit mēs esam sasnieguši posmu, kurā tas ir piemērojams Šis dokumentācijas lapa — un tajā ir norādīts, ka...

Neautorizētiem lietotājiem ir pieejama tikai neliela API metožu daļa:

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

Pats pirmais no tiem auth.sendCode, un ir pirmais vērtīgais pieprasījums, kurā mēs nosūtīsim api_id un api_hash, un pēc tam saņemam SMS ar kodu. Un, ja mēs nokļuvām nepareizā DC (šīs valsts tālruņu numurus apkalpo, piemēram, cits), tad mēs saņemsim kļūdu ar vēlamā līdzstrāvas numuru. Lai noskaidrotu, ar kuru IP adresi mums ir jāpieslēdzas pēc līdzstrāvas numura, mums palīdzēs help.getConfig. Kādreiz bija tikai 5 ieraksti, bet pēc labi zināmajiem 2018. gada notikumiem to skaits ir ievērojami pieaudzis.

Tagad atcerēsimies, ka šajā posmā esam nokļuvuši anonīmajā serverī. Vai nav pārāk dārgi iegūt tikai IP adresi? Kāpēc gan neveikt šo un citas darbības nešifrētajā MTProto daļā? Es dzirdu iebildumu: "kā jūs varat pārliecināties, ka tas nav RKN, kas atbildēs ar viltotām adresēm?". Uz to mēs atgādinām, ka faktiski oficiālajos klientiem iegultās RSA atslēgas, t.i. jūs vienkārši varat parakstīt šo informāciju. Faktiski tas jau tiek darīts informācijai par slēdzeņu apiešanu, ko klienti saņem pa citiem kanāliem (loģiski, ka to nevar izdarīt pašā MTProto, jo jums joprojām ir jāzina, kur izveidot savienojumu).

LABI. Šajā klientu autorizācijas posmā mēs vēl neesam pilnvaroti un neesam reģistrējuši savu pieteikumu. Pagaidām tikai vēlamies redzēt, kā serveris reaģē uz metodēm, kas pieejamas neautorizētam lietotājam. Un šeit…

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

Shēmā nāk pirmais, otrais

Tdesktop shēmā trešā vērtība ir

Jā, kopš tā laika, protams, dokumentācija ir atjaunināta. Lai gan drīz tas atkal var kļūt nebūtisks. Un kā tas būtu jāzina iesācējam izstrādātājam? Varbūt, ja reģistrēsi savu pieteikumu, viņi tevi informēs? Vasilijs to izdarīja, bet diemžēl viņam nekas netika nosūtīts (atkal mēs par to runāsim otrajā daļā).

... Jūs pamanījāt, ka mēs jau kaut kā esam pārgājuši uz API, t.i. uz nākamo līmeni un kaut ko palaidu garām MTProto tēmā? Nekas pārsteidzošs:

Vasīlij, [28.06.18/02/04 2:XNUMX] Mm, viņi rakņājas pa dažiem eXNUMXe algoritmiem

Mtproto definē šifrēšanas algoritmus un atslēgas abiem domēniem, kā arī nedaudz iesaiņojuma struktūras

Taču viņi pastāvīgi sajauc dažādus steka līmeņus, tāpēc ne vienmēr ir skaidrs, kur beidzās mtproto un sākās nākamais līmenis.

Kā tie tiek sajaukti? Piemēram, šeit ir tā pati pagaidu atslēga PFS (starp citu, Telegram Desktop nezina, kā to izdarīt). To izpilda API pieprasījums auth.bindTempAuthKey, t.i. no augstākā līmeņa. Bet tajā pašā laikā tas traucē šifrēšanu zemākā līmenī - piemēram, pēc tā jums tas jādara vēlreiz initConnection utt., tas tā nav tikko normāls pieprasījums. Atsevišķi tas arī nodrošina, ka jums var būt tikai VIENA pagaidu atslēga uz DC, lai gan lauks auth_key_id katrā ziņojumā ļauj mainīt atslēgu vismaz katrā ziņojumā un ka serverim ir tiesības jebkurā laikā “aizmirst” pagaidu atslēgu - ko šajā gadījumā darīt, dokumentācijā nav teikts ... nu, kāpēc nevarētu būt vairākas atslēgas, kā ar nākotnes sāļu komplektu, bet?..

MTProto tēmā ir vēl dažas lietas, kuras ir vērts atzīmēt.

Ziņojumu ziņojumi, msg_id, msg_seqno, apstiprinājumi, ping nepareizā virzienā un citas īpatnības

Kāpēc jums par tiem jāzina? Jo tie "noplūst" vienu līmeni augstāk, un jums par tiem jāzina, strādājot ar API. Pieņemsim, ka mūs neinteresē msg_key, zemākais līmenis mums visu atšifrēja. Bet atšifrētajos datos mums ir šādi lauki (arī datu garums, lai uzzinātu, kur atrodas polsterējums, taču tas nav svarīgi):

  • sāls-int64
  • session_id — int64
  • ziņojuma_id — int64
  • seq_no-int32

Atcerieties, ka visam DC ir tikai viens sāls. Kāpēc par to zināt? Ne tikai tāpēc, ka ir pieprasījums get_future_salts, kas norāda, kuri intervāli būs derīgi, bet arī tāpēc, ka, ja jūsu sāls būs “sapuvis”, ziņa (pieprasījums) vienkārši pazudīs. Serveris, protams, ziņos par jauno sāli, izsniedzot new_session_created - bet ar veco būs kaut kā jāpārsūta, piemēram. Un šis jautājums ietekmē lietojumprogrammas arhitektūru.

Serverim ir atļauts pārtraukt sesijas un atbildēt šādā veidā daudzu iemeslu dēļ. Patiesībā, kas ir MTProto sesija no klienta puses? Tie ir divi skaitļi session_id и seq_no ziņas šīs sesijas laikā. Nu, un, protams, pamatā esošais TCP savienojums. Pieņemsim, ka mūsu klients joprojām nezina, kā darīt daudzas lietas, atvienots, atjaunots. Ja tas notika ātri - vecā sesija turpinājās jaunajā TCP savienojumā, palieliniet seq_no tālāk. Ja tas aizņems ilgu laiku, serveris varētu to izdzēst, jo tā pusē tā arī ir rinda, kā noskaidrojām.

Kādam jābūt seq_no? Ak, tas ir grūts jautājums. Mēģiniet godīgi saprast, kas bija domāts:

Ar saturu saistīts ziņojums

Ziņojums, kas prasa skaidru apstiprinājumu. Tie ietver visus lietotāju un daudzus pakalpojumu ziņojumus, praktiski visus, izņemot konteinerus un apstiprinājumus.

Ziņojuma kārtas numurs (msg_seqno)

32 bitu skaitlis, kas vienāds ar divkāršu “ar saturu saistītu” ziņojumu skaitu (tos, kuriem nepieciešams apstiprinājums, un jo īpaši tiem, kas nav konteineri), ko sūtītājs ir izveidojis pirms šī ziņojuma un pēc tam palielina par vienu, ja pašreizējais ziņojums ir ar saturu saistīts ziņojums. Konteiners vienmēr tiek ģenerēts pēc visa tā satura; tādēļ tā kārtas numurs ir lielāks vai vienāds ar tajā ietverto ziņojumu kārtas numuriem.

Kas tas par cirku ar soli 1 un pēc tam vēl 2? .. Man ir aizdomas, ka sākotnējā nozīme bija "zemais bits ACK, pārējais ir skaitlis", bet rezultāts nav īsti pareizs - jo īpaši , izrādās, ka var nosūtīt daži apstiprinājumi, kuriem ir tas pats seq_no! Kā? Nu, piemēram, serveris mums kaut ko sūta, sūta, un mēs paši klusējam, atbildam tikai ar servisa apstiprinājuma ziņām par viņa ziņojumu saņemšanu. Šajā gadījumā mūsu izejošajiem apstiprinājumiem būs tāds pats izejošais numurs. Ja esat iepazinies ar TCP un domājat, ka tas izklausās traki, bet šķiet, ka tas nav pārāk mežonīgs, jo TCP seq_no nemainās, un apstiprinājums iet uz seq_no otra puse - tad es steidzos apbēdināt. Apstiprinājumi nāk uz MTProto NAV par seq_no, tāpat kā TCP, bet msg_id !

Kas tas ir msg_id, vissvarīgākā no šīm jomām? Ziņojuma unikālais ID, kā norāda nosaukums. Tas ir definēts kā 64 bitu skaitlis, kura vismazāk nozīmīgajos bitos atkal ir servera-ne-servera maģija, bet pārējie ir Unix laikspiedols, ieskaitot daļējo daļu, kas pārvietots par 32 bitiem pa kreisi. Tie. laika zīmogs pats par sevi (un serveris noraidīs ziņojumus ar pārāk atšķirīgu laiku). No tā izrādās, ka kopumā tas ir klientam globāls identifikators. Kamēr - atceries session_id - mums tiek garantēts: Vienai sesijai paredzēto ziņojumu nekādā gadījumā nevar nosūtīt uz citu sesiju. Tas ir, izrādās, ka jau ir trīs līmenis — sesija, sesijas numurs, ziņas ID. Kāpēc tik pārlieku sarežģījumi, šis noslēpums ir ļoti liels.

Tātad, msg_id nepieciešams priekš…

RPC: pieprasījumi, atbildes, kļūdas. Apstiprinājumi.

Kā jūs, iespējams, pamanījāt, shēmā nekur nav īpaša veida vai funkcijas "veikt RPC pieprasījumu", lai gan atbildes ir. Galu galā mums ir ar saturu saistīti ziņojumi! Tas ir, jebkurš ziņa var būt pieprasījums! Vai arī nebūt. Galu galā, no katra tur ir msg_id. Un šeit ir atbildes:

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

Šeit ir norādīts, uz kuru ziņojumu šī ir atbilde. Tāpēc API augšējā līmenī jums būs jāatceras, kāds numurs bija jūsu pieprasījumam - manuprāt, nav nepieciešams skaidrot, ka darbs ir asinhrons, un vienlaikus var būt vairāki pieprasījumi, uz kuriem atbildes vai var atgriezt jebkurā secībā? Principā no šī un kļūdas ziņojumiem, piemēram, darbinieku neesamības, var izsekot aiz tā esošās arhitektūras: serveris, kas uztur ar jums TCP savienojumu, ir priekšgala līdzsvarotājs, tas novirza pieprasījumus uz aizmugursistēmām un savāc tos atpakaļ. message_id. Šķiet, ka šeit viss ir skaidrs, loģiski un labi.

Jā?.. Un ja tā padomā? Galu galā arī pašai RPC atbildei ir lauks msg_id! Vai mums ir jākliedz serverim "jūs neatbildat uz manu atbildi!"? Un jā, kas tur bija par apstiprinājumu? Par lapu ziņas par ziņām stāsta mums, kas ir

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

un katrai pusei tas jādara. Bet ne vienmēr! Ja saņemat RpcResult, tas pats par sevi kalpo kā apstiprinājums. Tas nozīmē, ka serveris var atbildēt uz jūsu pieprasījumu ar MsgsAck — piemēram, "Es to saņēmu". Var uzreiz atbildēt uz RpcResult. Tas varētu būt gan.

Un jā, jums joprojām ir jāatbild uz atbildi! Apstiprinājums. Pretējā gadījumā serveris uzskatīs to par nepiegādātu un atkal izstās jums. Pat pēc savienojuma atjaunošanas. Bet šeit, protams, radīsies jautājums par taimautu. Apskatīsim tos nedaudz vēlāk.

Tikmēr apsvērsim iespējamās kļūdas vaicājuma izpildē.

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

Ak, kāds izsauksies, te ir cilvēcīgāks formāts - tur ir rinda! Nesteidzies. Šeit kļūdu sarakstsbet noteikti ne pilnīga. No tā mēs uzzinām, ka kods ir − kaut kas kā HTTP kļūdas (nu, protams, netiek ievērota atbilžu semantika, dažviet tās tiek sadalītas pa kodiem nejauši), un virkne izskatās šādi LIELIE_BURTI_UN_NUMBERS. Piemēram, PHONE_NUMBER_OCCUPIED vai FILE_PART_X_MISSING. Nu, tas ir, jums joprojām ir šī līnija parsēt. Piemēram, FLOOD_WAIT_3600 nozīmēs, ka jāgaida stunda, un PHONE_MIGRATE_5ka tālruņa numurs ar šo prefiksu jāreģistrē 5. DC. Mums ir tipa valoda, vai ne? Mums nav nepieciešams arguments no virknes, regulāras izteiksmes derēs, cho.

Atkal tas nav servisa ziņojumu lapā, bet, kā jau ierasts šim projektam, informāciju var atrast citā dokumentācijas lapā. Vai raisīt aizdomas. Pirmkārt, skatieties, mašīnrakstīšanas/slāņu pārkāpums - RpcError var ieguldīt RpcResult. Kāpēc ne ārā? Ko mēs neesam ņēmuši vērā?.. Attiecīgi, kur ir garantija, ka RpcError nedrīkst ieguldīt RpcResult, bet būt tieši vai ligzdotā citā veidā? tā pietrūkst req_msg_id ? ..

Bet turpināsim par dienesta ziņojumiem. Klients var uzskatīt, ka serveris ilgi domā, un izteikt tik brīnišķīgu pieprasījumu:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Uz to ir trīs iespējamās atbildes, kas atkal krustojas ar apstiprinājuma mehānismu, lai mēģinātu saprast, kādiem tiem jābūt (un kāds ir to veidu saraksts, kuriem vispār nav nepieciešams apstiprinājums), lasītājs tiek atstāts kā mājas darbs (piezīme: informācija Telegram Desktop avotos nav pilnīga).

Atkarība: Ziņojuma pasta statusi

Vispār daudzviet TL, MTProto un Telegram vispār atstāj spītības sajūtu, bet aiz pieklājības, takta u.c. mīkstās prasmes mēs par to pieklājīgi klusējām, un neķītrības dialogos tika cenzētas. Tomēr šī vietaОlielākā daļa lapas par ziņas par ziņām izraisa šoku pat man, kas jau ilgu laiku strādā ar tīkla protokoliem un esmu redzējis dažādas izliekuma pakāpes velosipēdus.

Tas sākas nekaitīgi, ar apstiprinājumiem. Tālāk mums stāsta par

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 ar tiem nāksies saskarties ikvienam, kurš sāks strādāt ar MTProto, ciklā “izlabots - pārkompilēts - palaists” skaitļu kļūdu vai rediģēšanas laikā sapuvušās sāls iegūšana ir ierasta lieta. Tomēr šeit ir divi punkti:

  1. No tā izriet, ka sākotnējais ziņojums tiek pazaudēts. Mums ir jānožogo dažas rindas, mēs to apsvērsim vēlāk.
  2. Kādi ir šie dīvainie kļūdu skaitļi? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64… kur ir pārējie skaitļi, Tomij?

Dokumentācijā teikts:

Ir paredzēts, ka kļūdas_koda vērtības tiek grupētas (error_code >> 4): piemēram, kodi 0x40 - 0x4f atbilst konteinera sadalīšanas kļūdām.

bet, pirmkārt, pārbīde uz otru pusi, otrkārt, nav svarīgi, kur ir pārējie kodi? Autora galvā?.. Tomēr tie ir sīkumi.

Atkarība sākas ar ziņu statusa ziņojumiem un ziņu kopijām:

  • Ziņojuma statusa informācijas pieprasījums
    Ja kāda no pusēm kādu laiku nav saņēmusi informāciju par savu izejošo ziņojumu statusu, tā var to nepārprotami pieprasīt no otras puses:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informatīvs ziņojums par ziņojumu statusu
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Lūk, info ir virkne, kas satur tieši vienu ziņojuma statusa baitu katram ziņojumam no ienākošo msg_ids saraksta:

    • 1 = nekas nav zināms par ziņojumu (msg_id pārāk zems, iespējams, ka otra puse to ir aizmirsusi)
    • 2 = ziņojums nav saņemts (msg_id ietilpst saglabāto identifikatoru diapazonā; tomēr otra puse noteikti nav saņēmusi šādu ziņojumu)
    • 3 = ziņojums nav saņemts (msg_id pārāk augsts; tomēr otra puse to noteikti vēl nav saņēmusi)
    • 4 = ziņojums saņemts (ņemiet vērā, ka šī atbilde vienlaikus ir arī saņemšanas apstiprinājums)
    • +8 = ziņojums jau ir apstiprināts
    • +16 = ziņojums nav jāapstiprina
    • +32 = RPC vaicājums ietverts ziņojumā, kas tiek apstrādāts vai apstrāde jau ir pabeigta
    • +64 = ar saturu saistīta atbilde uz jau ģenerētu ziņojumu
    • +128 = otra puse precīzi zina, ka ziņojums jau ir saņemts
      Šai atbildei nav nepieciešams apstiprinājums. Tas ir apstiprinājums par attiecīgo msgs_state_req, un pats par sevi.
      Ņemiet vērā, ja pēkšņi atklājas, ka otrai pusei nav ziņas, kas izskatās kā tai nosūtītas, ziņu var vienkārši nosūtīt atkārtoti. Pat ja otrai pusei jāsaņem divas ziņojuma kopijas vienlaikus, dublikāts tiks ignorēts. (Ja ir pagājis pārāk daudz laika un sākotnējais msg_id vairs nav derīgs, ziņojums ir jāiesaiņo msg_copy).
  • Brīvprātīga paziņošana par ziņojumu statusu
    Katra puse var brīvprātīgi informēt otru pusi par otras puses nosūtīto ziņojumu statusu.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Paplašināta brīvprātīga viena ziņojuma statusa paziņošana
    ...
    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;
  • Skaidrs pieprasījums atkārtoti nosūtīt ziņojumus
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Attālā puse nekavējoties atbild, atkārtoti nosūtot pieprasītos ziņojumus […]
  • Skaidrs pieprasījums atkārtoti nosūtīt atbildes
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Attālā puse nekavējoties atbild, atkārtoti nosūtot atbildes uz pieprasītajām ziņām […]
  • Ziņojumu kopijas
    Dažās situācijās vecs ziņojums ar msg_id, kas vairs nav derīgs, ir jānosūta atkārtoti. Pēc tam tas tiek iesaiņots kopēšanas konteinerā:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Pēc saņemšanas ziņojums tiek apstrādāts tā, it kā iesaiņojuma tur nebūtu. Tomēr, ja ir droši zināms, ka ziņojums orig_message.msg_id ir saņemts, jaunais ziņojums netiek apstrādāts (vienlaikus tā un orig_message.msg_id tiek apstiprināti). Orig_message.msg_id vērtībai ir jābūt mazākai par konteinera msg_id.

Pat klusēsim par to, ka in msgs_state_info atkal nepabeigtā TL ausis izvirzās (mums bija vajadzīgs baitu vektors, un apakšējos divos enum bitos un vecākos bitos karodziņi). Lieta ir kaut kas cits. Vai kāds saprot, kāpēc tas viss notiek praksē reālā klientā nepieciešams?.. Ar grūtībām, bet var iedomāties kādu labumu, ja cilvēks nodarbojas ar atkļūdošanu, un interaktīvā režīmā - pajautā serverim kas un kā. Bet šeit ir aprakstīti pieprasījumi turp un atpakaļ.

No tā izriet, ka katrai pusei ir ne tikai jāšifrē un jānosūta ziņojumi, bet arī jāglabā dati par tiem, par atbildēm uz tiem un nezināmu laiku. Dokumentācijā nav aprakstīts šo funkciju grafiks vai praktiskā pielietojamība. nekādā veidā. Pārsteidzošākais ir tas, ka tie patiešām tiek izmantoti oficiālo klientu kodā! Acīmredzot viņiem tika pateikts kaut kas tāds, kas nebija iekļauts atklātajā dokumentācijā. Saprast no koda kāpēc, vairs nav tik vienkārši kā TL gadījumā - šī nav (salīdzinoši) loģiski izolēta daļa, bet gan gabals, kas piesaistīts aplikācijas arhitektūrai, t.i. prasīs daudz vairāk laika, lai saprastu lietojumprogrammas kodu.

Ping un laiki. Rindas.

No visa, ja atceraties minējumus par servera arhitektūru (pieprasījumu sadalījums pa aizmugursistēmām), izriet diezgan blāva lieta - neskatoties uz visām piegādes garantijām, ka TCP (vai nu dati ir piegādāti, vai arī tiksiet informēts par pārtraukums, bet dati tiks piegādāti līdz problēmas brīdim), ka apstiprinājumi pašā MTProto - nekādu garantiju. Serveris var viegli pazaudēt vai izmest jūsu ziņojumu, un ar to neko nevar darīt, tikai iežogot dažāda veida kruķus.

Un vispirms – ziņu rindas. Nu, pirmkārt, viss bija acīmredzams jau no paša sākuma - neapstiprināta ziņa ir jāsaglabā un jāpārsūta. Un pēc kāda laika? Un jestrs viņu pazīst. Varbūt tie atkarīgo servisa ziņojumi kaut kā atrisina šo problēmu ar kruķiem, teiksim, Telegram Desktop tām atbilst aptuveni 4 rindas (varbūt vairāk, kā jau minēts, šim nolūkam ir nopietnāk jāiedziļinās tā kodā un arhitektūrā; tajā pašā laikā laika, mēs zinām, ka to nevar ņemt par paraugu, tajā netiek izmantots noteikts skaits tipu no MTProto shēmas).

Kāpēc tas notiek? Iespējams, servera programmētāji nespēja nodrošināt klastera uzticamību vai vismaz pat buferizāciju priekšējā balansētājā un novirzīja šo problēmu uz klientu. No izmisuma Vasilijs mēģināja ieviest alternatīvu iespēju, tikai ar divām rindām, izmantojot TCP algoritmus - mērot RTT serverim un pielāgojot “loga” izmēru (ziņojumos) atkarībā no neapstiprināto pieprasījumu skaita. Tas ir, tāda aptuvena heiristika servera slodzes novērtēšanai - cik daudz no mūsu pieprasījumiem tas var sakošļāt vienlaikus un nezaudēt.

Nu, tas ir, jūs saprotat, vai ne? Ja jums atkal jāievieš TCP papildus protokolam, kas darbojas, izmantojot TCP, tas norāda uz ļoti slikti izstrādātu protokolu.

Ak, jā, kāpēc ir vajadzīgas vairākas rindas, un vispār, ko tas nozīmē cilvēkam, kurš strādā ar augsta līmeņa API? Paskatieties, jūs veicat pieprasījumu, jūs to serializējat, bet bieži vien nav iespējams to nosūtīt uzreiz. Kāpēc? Jo atbilde būs msg_id, kas ir īslaicīgsаEs esmu etiķete, kuras iecelšanu labāk atlikt pēc iespējas vēlāk - pēkšņi serveris to noraidīs, jo starp mums un viņu ir laika nesakritība (protams, mēs varam uztaisīt kruķi, kas novirza mūsu laiku no tagadnes servera laikam, pievienojot delta, kas aprēķināta no servera atbildēm - oficiālie klienti to dara, taču šī metode ir neapstrādāta un neprecīza buferizācijas dēļ). Tātad, kad veicat pieprasījumu, izmantojot vietējās funkcijas zvanu no bibliotēkas, ziņojumam ir šādi posmi:

  1. Atrodas tajā pašā rindā un gaida šifrēšanu.
  2. Iecelts msg_id un ziņojums nonāca citā rindā - iespējama pārsūtīšana; nosūtīt uz kontaktligzdu.
  3. a) Serveris atbildēja MsgsAck - ziņojums tika piegādāts, mēs to izdzēšam no "citas rindas".
    b) vai otrādi, viņam kaut kas nepatika, viņš atbildēja uz sliktu ziņu - mēs sūtām atkārtoti no “citas rindas”
    c) Nekas nav zināms, ir nepieciešams atkārtoti nosūtīt ziņojumu no citas rindas - bet nav precīzi zināms, kad.
  4. Serveris beidzot atbildēja RpcResult - faktiskā atbilde (vai kļūda) - ne tikai piegādāta, bet arī apstrādāta.

Varbūt, konteineru izmantošana varētu daļēji atrisināt problēmu. Tas ir tad, kad ziņojumu kopums tiek iesaiņots vienā un serveris atbildēja ar apstiprinājumu visiem uzreiz, ar vienu msg_id. Bet viņš arī noraidīs šo paku, ja kaut kas nogāja greizi, arī visu.

Un šajā brīdī tiek ņemti vērā netehniski apsvērumi. Pēc pieredzes esam redzējuši daudzus kruķus, turklāt tagad vēl redzēsim sliktu padomu un arhitektūras piemērus – vai šādos apstākļos ir vērts uzticēties un pieņemt šādus lēmumus? Jautājums ir retorisks (protams, nē).

Par ko mēs runājam? Ja par tēmu "atkarīgie ziņojumi par ziņām" joprojām varat spekulēt ar iebildumiem, piemēram, "tu esi stulbs, jūs nesapratāt mūsu izcilo ideju!" (tātad vispirms rakstīt dokumentāciju, kā jau normāliem cilvēkiem pienākas, ar pamatojumu un pakešu apmaiņas piemēriem, tad runāsim), tad laiki / taimauts ir tīri praktisks un specifisks jautājums, te viss jau sen zināms. Bet ko dokumentācija stāsta par taimautiem?

Serveris parasti apstiprina ziņojuma saņemšanu no klienta (parasti RPC vaicājums), izmantojot RPC atbildi. Ja atbilde tiek gaidīta ilgu laiku, serveris vispirms var nosūtīt saņemšanas apstiprinājumu un nedaudz vēlāk arī pašu RPC atbildi.

Klients parasti apstiprina ziņojuma saņemšanu no servera (parasti RPC atbildi), pievienojot apstiprinājumu nākamajam RPC vaicājumam, ja tas netiek pārsūtīts pārāk vēlu (ja tas tiek ģenerēts, piemēram, 60–120 sekundes pēc saņemšanas ziņojumu no servera). Tomēr, ja ilgstoši nav iemesla sūtīt ziņojumus uz serveri vai ja no servera ir liels skaits neapstiprinātu ziņojumu (piemēram, vairāk nekā 16), klients nosūta atsevišķu apstiprinājumu.

... Es tulkoju: mēs paši nezinām, cik un kā vajag, nu, lēšam, lai tā ir.

Un par pingiem:

Ping ziņojumi (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Atbilde parasti tiek atgriezta tajā pašā savienojumā:

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

Šiem ziņojumiem nav nepieciešami apstiprinājumi. Pongs tiek pārraidīts tikai, reaģējot uz ping, savukārt ping var ierosināt abas puses.

Atliktā savienojuma slēgšana + PING

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

Darbojas kā ping. Turklāt pēc tā saņemšanas serveris iedarbina taimeri, kas slēgs pašreizējo savienojumu disconnect_delay sekundes vēlāk, ja vien tas nesaņems jaunu tāda paša veida ziņojumu, kas automātiski atiestata visus iepriekšējos taimerus. Piemēram, ja klients nosūta šos ping vienu reizi ik pēc 60 sekundēm, tas var iestatīt disconnect_delay 75 sekundes.

Vai tu esi izkritis no prāta?! Pēc 60 sekundēm vilciens ieies stacijā, izsēdinās un uzņems pasažierus un atkal zaudēs saziņu tunelī. Pēc 120 sekundēm, kamēr tu čauksies, viņš nonāks pie cita, un savienojums, visticamāk, pārtrūks. Ir skaidrs, no kurienes kājas aug - "Es dzirdēju zvana signālu, bet es nezinu, kur tas ir", ir Nagle algoritms un opcija TCP_NODELAY, kas bija paredzēta interaktīvam darbam. Bet, atvainojiet, aizkavējiet tās noklusējuma vērtību - 200 Millisekundes. Ja jūs patiešām vēlaties attēlot kaut ko līdzīgu un ietaupīt uz iespējamo pakešu pāri - labi, atlieciet to vismaz uz 5 sekundēm vai neatkarīgi no tā, ar ko ziņojuma “Lietotājs raksta ...” taimauts tagad ir vienāds. Bet ne vairāk.

Un visbeidzot, ping. Tas ir, pārbaudot TCP savienojuma dzīvīgumu. Smieklīgi, bet pirms kādiem 10 gadiem uzrakstīju kritisku tekstu par mūsu fakultātes hosteļa ziņnesi - tur arī autori no klienta nopingoja serveri, nevis otrādi. Bet trešā kursa studenti ir viena lieta, un starptautisks birojs ir cita lieta, vai ne? ..

Pirmkārt, neliela izglītības programma. TCP savienojums, ja nav pakešu apmaiņas, var darboties vairākas nedēļas. Tas ir gan labi, gan slikti, atkarībā no mērķa. Nu, ja jums bija atvērts SSH savienojums ar serveri, jūs piecēlāties no datora, pārstartējāt barošanas maršrutētāju, atgriezāties savā vietā - sesija caur šo serveri nepārtrūka (neko neierakstījāt, nebija pakešu), ērti. Ir slikti, ja serverī ir tūkstošiem klientu, katrs no tiem aizņem resursus (sveicināti, Postgres!), un klienta resursdators, iespējams, jau sen ir restartēts, taču mēs par to neuzzināsim.

Tērzēšanas/tērzēšanas sistēmas pieder otrajam gadījumam cita, papildu iemesla dēļ - tiešsaistes statusiem. Ja lietotājs "nokrita", par to ir jāinformē viņa sarunu biedri. Pretējā gadījumā būs kļūda, ko pieļāva Jabber veidotāji (un laboja 20 gadus) - lietotājs atslēdzās, bet viņi turpina rakstīt viņam ziņas, uzskatot, ka viņš ir tiešsaistē (kas arī tika pilnībā pazaudēta šajās pāris minūtēs pirms pārtraukums tika atklāts). Nē, opcija TCP_KEEPALIVE, kas daudziem cilvēkiem, kuri nesaprot, kā darbojas TCP taimeri, parādās jebkurā vietā (iestatot savvaļas vērtības, piemēram, desmitiem sekunžu), šeit nepalīdzēs - jums ir jāpārliecinās, ka ne tikai OS kodols lietotāja mašīna ir dzīva, bet arī darbojas normāli, spēj atbildēt, un pati lietojumprogramma (jūs domājat, ka tā nevar sastingt? Telegram Desktop uz Ubuntu 18.04 man ir vairākkārt avarējusi).

Tāpēc jums vajadzētu ping serveri klients, nevis otrādi - ja klients to dara, savienojumam pārtrūkstot, ping netiks piegādāts, mērķis nav sasniegts.

Un ko mēs redzam telegrammā? Viss ir tieši otrādi! Nu, t.i. formāli, protams, abas puses var ping viena otrai. Praksē klienti izmanto kruķi ping_delay_disconnect, kas serverī iedarbina taimeri. Nu, atvainojiet, tas nav klienta darīšana, lai izlemtu, cik ilgi viņš vēlas tur dzīvot bez ping. Serveris, pamatojoties uz tā slodzi, zina labāk. Bet, protams, ja jums nav žēl resursu, tad ļaunie Pinokio ir paši, un kruķis nolaidīsies ...

Kā to vajadzēja veidot?

Es uzskatu, ka iepriekš minētie fakti diezgan skaidri norāda uz Telegram / VKontakte komandas ne pārāk augsto kompetenci datortīklu transporta (un zemāka) līmeņa jomā un zemo kvalifikāciju attiecīgajos jautājumos.

Kāpēc tas izrādījās tik sarežģīti, un kā Telegram arhitekti var mēģināt iebilst? To, ka viņi mēģināja izveidot sesiju, kas pārdzīvo TCP savienojuma pārtraukumus, tas ir, to, ko mēs tagad nepiegādājām, mēs piegādāsim vēlāk. Droši vien arī mēģināja taisīt UDP transportu, lai gan saskārās ar grūtībām un to pameta (tāpēc arī dokumentācija tukša - nebija ar ko lielīties). Bet tāpēc, ka trūkst izpratnes par to, kā darbojas tīkli kopumā un jo īpaši TCP, kur uz to var paļauties un kur tas jādara pašam (un kā), un mēģinājumi to apvienot ar kriptogrāfiju “viens šāviens no diviem putni ar vienu akmeni” - tāds līķis izrādījās.

Kā tam vajadzēja būt? Pamatojoties uz to, msg_id ir laikspiedols, kas ir kriptogrāfiski nepieciešams, lai novērstu atkārtošanas uzbrukumus, tā ir kļūda, pievienojot tam unikālu identifikatora funkciju. Tāpēc, krasi nemainot pašreizējo arhitektūru (kad tiek izveidots atjauninājumu pavediens, šī ir augsta līmeņa API tēma citai šīs ziņu sērijas daļai), būtu:

  1. Serveris, kas nodrošina TCP savienojumu ar klientu, uzņemas atbildību - ja jūs atņēmāt no ligzdas, lūdzu, apstipriniet, apstrādājiet vai atgrieziet kļūdu, bez zaudējumiem. Tad apstiprinājums nav id vektors, bet vienkārši "pēdējais saņemtais seq_no" - tikai skaitlis, kā TCP (divi cipari - savs sekv un apstiprināts). Mēs vienmēr esam sesijā, vai ne?
  2. Laika zīmogs, lai novērstu atkārtotus uzbrukumus, kļūst par atsevišķu lauku, a la nonce. Pārbaudīts, bet nekas cits netiek ietekmēts. Pietiekami un uint32 - ja mūsu sāls mainās vismaz ik pēc pusdienas, mēs varam atvēlēt 16 bitus pašreizējā laika veselās daļas apakšējiem bitiem, pārējos - sekundes daļējai daļai (kā tas ir tagad).
  3. Ievilkts msg_id vispār - no aizmugursistēmu pieprasījumu atšķiršanas viedokļa, pirmkārt, ir klienta ID, otrkārt, sesijas ID, un tos savieno. Attiecīgi pieprasījuma identifikatoram pietiek tikai ar vienu seq_no.

Arī ne tas labākais variants, par identifikatoru varētu kalpot pilnīgs nejaušs - tas, starp citu, jau tiek darīts augsta līmeņa API, sūtot ziņojumu. Būtu labāk mainīt arhitektūru no relatīvā uz absolūto vispār, bet šī ir tēma citai daļai, nevis šim ierakstam.

API?

Ta-daam! Tātad, izgājuši ceļu cauri sāpju un kruķu pilnajam ceļam, mēs beidzot varējām nosūtīt serverim jebkurus pieprasījumus un saņemt atbildes uz tiem, kā arī saņemt atjauninājumus no servera (nevis atbildot uz pieprasījumu, bet tas nosūta mums sevi, piemēram, PUSH, ja kāds tik daudz skaidrāk).

Uzmanību, tagad rakstā būs vienīgais Perla piemērs! (tiem, kas nezina sintaksi, pirmais svētības arguments ir objekta datu struktūra, otrais ir tā klase):

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

Jā, īpaši ne zem spoilera - ja neesi izlasījis, ej un dari!

Ak, wai~~… kā tas izskatās? Kaut kas ļoti pazīstams… varbūt šī ir JSON tipiskas tīmekļa API datu struktūra, izņemot varbūt, ka objektiem tika pievienotas klases?

Tātad izrādās ... Kas tas ir, biedri? .. Tik daudz pūļu - un mēs apstājāmies atpūsties, kur Web programmētāji tikai sākas?.. Vai tikai JSON, izmantojot HTTPS, nebūtu vieglāk?! Un ko mēs saņēmām apmaiņā? Vai šie centieni bija tā vērti?

Izvērtēsim, ko TL+MTProto mums ir devis un kādas ir iespējamas alternatīvas. Nu, HTTP pieprasījums-atbilde ir slikti piemērota, bet vismaz kaut kas papildus TLS?

kompakta serializācija. Redzot šo datu struktūru, līdzīgi kā JSON, atceramies, ka ir tās binārie varianti. Atzīmēsim MsgPack kā nepietiekami paplašināmu, bet ir, piemēram, CBOR - starp citu, standarts aprakstīts RFC 7049. Tas ir ievērojams ar to, ka tas definē tagus, kā pagarinājuma mehānismu un starp jau standartizēts tur ir:

  • 25 + 256 - dublikātu rindu aizstāšana ar rindas numura atsauci, tik lēta saspiešanas metode
  • 26 - serializēts Perl objekts ar klases nosaukumu un konstruktora argumentiem
  • 27 - serializēts no valodas neatkarīgs objekts ar tipa nosaukumu un konstruktora argumentiem

Es mēģināju serializēt tos pašus datus TL un CBOR ar iespējotu virkņu un objektu iesaiņošanu. Rezultāts sāka atšķirties par labu CBOR kaut kur no megabaita:

cborlen=1039673 tl_len=1095092

Tātad, secinājums: ir ievērojami vienkāršāki formāti, kas nav pakļauti sinhronizācijas kļūmei vai nezināma identifikatora problēmai, un ir salīdzināma efektivitāte.

Ātra savienojuma izveide. Tas nozīmē nulles RTT pēc atkārtotas pieslēgšanas (kad atslēga jau ir ģenerēta vienu reizi) - piemērojams no paša pirmā MTProto ziņojuma, bet ar dažām atrunām - viņi nokļuva vienā sālī, sesija nesapūta utt. Ko TLS mums piedāvā pretī? Saistīts citāts:

Izmantojot PFS TLS, TLS sesijas biļetes (RFC 5077), lai atsāktu šifrēto sesiju, nepārrunājot atslēgas un nesaglabājot atslēgas informāciju serverī. Atverot pirmo savienojumu un ģenerējot atslēgas, serveris šifrē savienojuma stāvokli un nosūta to klientam (sesijas biļetes veidā). Attiecīgi, kad savienojums tiek atsākts, klients nosūta atpakaļ serverim sesijas biļeti, kas cita starpā satur sesijas atslēgu. Pati biļete tiek šifrēta ar pagaidu atslēgu (sesijas biļetes atslēgu), kas tiek glabāta serverī un ir jāizplata visiem frontend serveriem, kas apstrādā SSL klasterizētos risinājumos.[10] Tādējādi sesijas biļetes ieviešana var pārkāpt PFS, ja pagaidu servera atslēgas tiek apdraudētas, piemēram, ja tās tiek glabātas ilgu laiku (OpenSSL, nginx, Apache pēc noklusējuma tās glabā visu programmas darbības laiku; populāras vietnes izmantojiet atslēgu vairākas stundas, līdz pat dienām).

Šeit RTT nav nulle, jāapmainās vismaz ClientHello un ServerHello, pēc tam kopā ar Finished klients jau var nosūtīt datus. Bet šeit jāatceras, ka mums ir nevis Web, ar savu jaunatvērto savienojumu gūzmu, bet gan ziņnesis, kura savienojums bieži vien ir viens un vairāk vai mazāk ilgstošs, salīdzinoši īss Web lapu pieprasījums - viss ir multipleksēts iekšpusē. Tas ir, tas ir diezgan pieņemami, ja mēs nesastaptos ar ļoti sliktu metro posmu.

Aizmirsāt kaut ko citu? Raksti komentāros.

Turpinājums sekos!

Šīs ierakstu sērijas otrajā daļā mēs aplūkosim organizatoriskus, nevis tehniskus jautājumus - pieejas, ideoloģiju, saskarni, attieksmi pret lietotājiem utt. Tomēr, pamatojoties uz šeit sniegto tehnisko informāciju.

Trešajā daļā tiks turpināta tehniskās sastāvdaļas/izstrādes pieredzes analīze. Jo īpaši jūs uzzināsit:

  • pandemonijas turpinājums ar dažādiem TL veidiem
  • nezināmas lietas par kanāliem un supergrupām
  • nekā dialogi ir sliktāks par sarakstu
  • par absolūto un relatīvo ziņojumu adresēšanu
  • kāda ir atšķirība starp fotoattēlu un attēlu
  • kā emocijzīmes traucē tekstam slīprakstā

un citi kruķi! Sekojiet līdzi!

Avots: www.habr.com

Pievieno komentāru