Kritik terhadap protokol dan pendekatan organisasi Telegram. Bagian 1, teknis: pengalaman menulis klien dari awal - TL, MT

Belakangan ini, postingan tentang betapa bagusnya Telegram, betapa brilian dan berpengalamannya Durov bersaudara dalam membangun sistem jaringan, dll mulai lebih sering muncul di Habré. Pada saat yang sama, sangat sedikit orang yang benar-benar membenamkan diri dalam perangkat teknis - paling banyak, mereka menggunakan Bot API yang cukup sederhana (dan sangat berbeda dari MTProto) berdasarkan JSON, dan biasanya hanya menerima pada iman semua pujian dan PR yang berkisar pada pembawa pesan. Hampir satu setengah tahun yang lalu, rekan saya di LSM Eshelon Vasily (sayangnya, akunnya di Habré terhapus bersama dengan drafnya) mulai menulis klien Telegramnya sendiri dari awal di Perl, dan kemudian penulis baris-baris ini bergabung. Kenapa Perl, ada yang langsung bertanya? Karena proyek semacam itu sudah ada dalam bahasa lain. Sebenarnya bukan itu intinya, mungkin ada bahasa lain yang tidak ada perpustakaan yang sudah jadi, dan oleh karena itu penulis harus melakukan yang terbaik dari awal. Selain itu, kriptografi adalah masalah kepercayaan, tapi verifikasi. Dengan produk yang ditujukan untuk keamanan, Anda tidak bisa hanya mengandalkan perpustakaan yang sudah jadi dari pabrikan dan mempercayainya begitu saja (namun, ini adalah topik untuk bagian kedua). Saat ini, perpustakaan berfungsi cukup baik pada tingkat "rata-rata" (memungkinkan Anda membuat permintaan API apa pun).

Namun, tidak akan banyak kriptografi atau matematika dalam rangkaian postingan ini. Namun akan ada banyak detail teknis dan penopang arsitektur lainnya (juga berguna bagi mereka yang tidak akan menulis dari awal, tetapi akan menggunakan perpustakaan dalam bahasa apa pun). Jadi, tujuan utamanya adalah mencoba mengimplementasikan klien dari awal menurut dokumentasi resmi. Artinya, mari kita asumsikan bahwa kode sumber klien resmi ditutup (sekali lagi, di bagian kedua kita akan membahas lebih detail topik fakta bahwa ini benar. terjadi jadi), tetapi, seperti di masa lalu, misalnya, ada standar seperti RFC - apakah mungkin untuk menulis klien sesuai dengan spesifikasinya saja, "tanpa melihat" kode sumbernya, baik yang resmi (Telegram Desktop, seluler), atau Telethon tidak resmi?

contoh:

Dokumentasi... itu ada kan? Apakah benar?..

Fragmen catatan untuk artikel ini mulai dikumpulkan musim panas lalu. Selama ini di situs resminya https://core.telegram.org Dokumentasinya adalah pada Layer 23, yaitu. terjebak di suatu tempat pada tahun 2014 (ingat, saat itu belum ada saluran?). Tentu saja, secara teori, hal ini seharusnya memungkinkan kami mengimplementasikan klien dengan fungsionalitas pada saat itu di tahun 2014. Namun bahkan di negara bagian ini, dokumentasinya, pertama, tidak lengkap, dan kedua, ada yang bertentangan dengan dirinya sendiri. Lebih dari sebulan yang lalu, pada bulan September 2019, hal itu terjadi tanpa sengaja Ditemukan bahwa ada pembaruan besar dokumentasi di situs tersebut, untuk Layer 105 yang cukup baru, dengan catatan bahwa sekarang semuanya perlu dibaca lagi. Memang banyak artikel yang direvisi, namun banyak pula yang tetap tidak berubah. Oleh karena itu, ketika membaca kritik mengenai dokumentasi di bawah ini, perlu diingat bahwa beberapa hal tersebut sudah tidak relevan lagi, namun ada pula yang masih cukup relevan. Lagipula, 5 tahun di dunia modern bukan hanya waktu yang lama, tapi sangat banyak. Sejak saat itu (terutama jika Anda tidak memperhitungkan situs geochat yang dibuang dan dihidupkan kembali sejak saat itu), jumlah metode API dalam skema ini telah berkembang dari seratus menjadi lebih dari dua ratus lima puluh!

Di mana memulai sebagai penulis muda?

Tidak masalah apakah Anda menulis dari awal atau menggunakan, misalnya, perpustakaan yang sudah jadi Teleton untuk Python или Madeline untuk PHP, bagaimanapun, Anda harus melakukannya terlebih dahulu daftarkan permohonan Anda - mendapatkan parameter api_id и api_hash (mereka yang pernah bekerja dengan API VKontakte segera mengerti) yang digunakan server untuk mengidentifikasi aplikasi. Ini harus melakukannya karena alasan hukum, tetapi kita akan membahas lebih lanjut mengapa penulis perpustakaan tidak dapat mempublikasikannya di bagian kedua. Anda mungkin puas dengan nilai tesnya, meskipun nilainya sangat terbatas - faktanya sekarang Anda bisa mendaftar hanya satu aplikasi, jadi jangan terburu-buru melakukannya.

Sekarang, dari sudut pandang teknis, kita harus tertarik pada kenyataan bahwa setelah pendaftaran kita akan menerima pemberitahuan dari Telegram tentang pembaruan dokumentasi, protokol, dll. Artinya, orang dapat berasumsi bahwa situs dengan dermaga itu ditinggalkan begitu saja dan terus bekerja secara khusus dengan mereka yang mulai membuat klien, karena hal ini lebih mudah. Tapi tidak, tidak ada hal seperti itu yang teramati, tidak ada informasi yang datang.

Dan jika Anda menulis dari awal, menggunakan parameter yang diperoleh sebenarnya masih jauh. Meskipun https://core.telegram.org/ dan membicarakannya di Memulai terlebih dahulu, sebenarnya, Anda harus menerapkannya terlebih dahulu Protokol MTProto - tapi jika kamu percaya tata letak sesuai dengan model OSI di akhir halaman untuk gambaran umum protokol, maka sia-sia belaka.

Faktanya, baik sebelum dan sesudah MTProto, pada beberapa level sekaligus (seperti yang dikatakan oleh penggiat jejaring asing yang bekerja di kernel OS, pelanggaran lapisan), topik yang besar, menyakitkan, dan mengerikan akan menghalangi...

Serialisasi biner: TL (Type Language) dan skemanya, lapisannya, dan banyak kata menakutkan lainnya

Topik inilah yang sebenarnya menjadi kunci permasalahan Telegram. Dan akan ada banyak kata-kata buruk jika Anda mencoba mendalaminya.

Jadi, inilah diagramnya. Jika kata ini terlintas di benak Anda, katakan, Skema JSON, Anda berpikir dengan benar. Tujuannya sama: suatu bahasa untuk mendeskripsikan kemungkinan kumpulan data yang dikirimkan. Di sinilah persamaannya berakhir. Jika dari halaman Protokol MTProto, atau dari pohon sumber klien resmi, kami akan mencoba membuka beberapa skema, kami akan melihat sesuatu seperti:

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;

Seseorang yang melihat ini untuk pertama kalinya secara intuitif akan dapat mengenali hanya sebagian dari apa yang tertulis - yah, ini tampaknya struktur (walaupun di mana namanya, di kiri atau di kanan?), ada bidang di dalamnya, setelah itu suatu tipe mengikuti setelah titik dua... mungkin. Di sini, di tanda kurung sudut mungkin ada templat seperti di C++ (sebenarnya, tidak juga). Dan apa arti semua simbol lainnya, tanda tanya, tanda seru, persentase, tanda pagar (dan jelas artinya berbeda di tempat berbeda), terkadang ada dan terkadang tidak, angka heksadesimal - dan yang paling penting, bagaimana cara mendapatkan dari ini tepat (yang tidak akan ditolak oleh server) aliran byte? Anda harus membaca dokumentasinya (ya, ada tautan ke skema di versi JSON di dekatnya - tetapi itu tidak membuatnya lebih jelas).

Buka halamannya Serialisasi Data Biner dan menyelami dunia magis jamur dan matematika diskrit, mirip dengan matan di tahun ke-4. Alfabet, tipe, nilai, kombinator, kombinator fungsional, bentuk normal, tipe komposit, tipe polimorfik... dan itu semua hanyalah halaman pertama! Berikutnya menanti Anda Bahasa TL, yang meskipun sudah berisi contoh permintaan dan tanggapan sepele, tidak memberikan jawaban sama sekali untuk kasus-kasus yang lebih umum, yang berarti Anda harus mengarungi penceritaan kembali matematika yang diterjemahkan dari bahasa Rusia ke bahasa Inggris pada delapan kasus tertanam lainnya. halaman!

Pembaca yang akrab dengan bahasa fungsional dan inferensi tipe otomatis, tentu saja, akan melihat bahasa deskripsi dalam bahasa ini, bahkan dari contohnya, jauh lebih familiar, dan dapat mengatakan bahwa pada prinsipnya ini sebenarnya tidak buruk. Keberatan terhadap hal ini adalah:

  • ya, tujuannya kedengarannya bagus, tapi sayangnya, dia tidak tercapai
  • Pendidikan di universitas-universitas Rusia bervariasi bahkan di antara spesialisasi TI - tidak semua orang mengambil kursus yang sesuai
  • Akhirnya, seperti yang akan kita lihat, dalam praktiknya memang demikian tidak diperlukan, karena hanya sebagian kecil dari TL yang telah dijelaskan yang digunakan

Seperti Yang Dikatakan Leo Nerd di saluran #perl di jaringan FreeNode IRC, yang mencoba menerapkan gerbang dari Telegram ke Matrix (terjemahan kutipan tidak akurat dari ingatan):

Rasanya seperti seseorang pertama kali diperkenalkan dengan teori mengetik, menjadi bersemangat, dan mulai mencoba bermain-main dengannya, tidak terlalu peduli apakah teori itu diperlukan dalam praktik.

Lihat sendiri, apakah kebutuhan akan tipe bare (int, long, dll.) sebagai sesuatu yang mendasar tidak menimbulkan pertanyaan - pada akhirnya harus diimplementasikan secara manual - misalnya, mari kita coba turunkan darinya vektor. Faktanya, hal tersebut Himpunan, jika Anda menyebut hasil dengan nama aslinya.

Tapi sebelum

Deskripsi singkat tentang subset sintaks TL bagi mereka yang tidak membaca dokumentasi resmi

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;

Definisi selalu dimulai perancang, setelah itu secara opsional (dalam praktiknya - selalu) melalui simbol # harus CRC32 dari string deskripsi yang dinormalisasi jenis ini. Berikutnya adalah deskripsi field; jika ada, tipenya mungkin kosong. Ini semua diakhiri dengan tanda sama dengan, nama tipe yang dimiliki konstruktor ini - yaitu subtipe -. Orang di sebelah kanan tanda sama dengan adalah polimorfik - yaitu, beberapa tipe tertentu dapat berhubungan dengannya.

Jika definisi muncul setelah baris ---functions---, maka sintaksisnya akan tetap sama, tetapi maknanya akan berbeda: konstruktor akan menjadi nama fungsi RPC, bidang akan menjadi parameter (yaitu, strukturnya akan tetap sama persis, seperti dijelaskan di bawah , ini hanya akan menjadi arti yang ditetapkan), dan “tipe polimorfik " - tipe hasil yang dikembalikan. Benar, itu akan tetap polimorfik - baru saja didefinisikan di bagian tersebut ---types---, tetapi konstruktor ini “tidak akan dipertimbangkan”. Membebani tipe fungsi yang dipanggil berdasarkan argumennya, mis. Untuk beberapa alasan, beberapa fungsi dengan nama yang sama tetapi tanda tangan berbeda, seperti di C++, tidak disediakan di TL.

Mengapa "konstruktor" dan "polimorfik" jika bukan OOP? Faktanya, akan lebih mudah bagi seseorang untuk memikirkan hal ini dalam istilah OOP - tipe polimorfik sebagai kelas abstrak, dan konstruktor adalah kelas keturunan langsungnya, dan final dalam terminologi sejumlah bahasa. Faktanya, tentu saja, hanya di sini kesamaan dengan metode konstruktor yang kelebihan beban dalam bahasa pemrograman OO. Karena ini hanya struktur data, tidak ada metode (walaupun deskripsi fungsi dan metode lebih lanjut cukup mampu menimbulkan kebingungan di kepala bahwa mereka ada, tapi itu masalah lain) - Anda dapat menganggap konstruktor sebagai nilai dari yang sedang dibangun ketik saat membaca aliran byte.

Bagaimana ini bisa terjadi? Deserializer, yang selalu membaca 4 byte, melihat nilainya 0xcrc32 - dan memahami apa yang akan terjadi selanjutnya field1 dengan tipe int, yaitu. membaca tepat 4 byte, di atasnya adalah bidang dengan tipe PolymorType membaca. Melihat 0x2crc32 dan memahami bahwa ada dua bidang selanjutnya, pertama long, yang berarti kita membaca 8 byte. Dan sekali lagi tipe kompleks, yang dideserialisasi dengan cara yang sama. Misalnya, Type3 dapat dideklarasikan dalam rangkaian segera setelah dua konstruktor berturut-turut, maka keduanya harus bertemu 0x12abcd34, setelah itu Anda perlu membaca 4 byte lagi intAtau 0x6789cdef, setelah itu tidak akan ada apa-apa. Ada lagi - Anda perlu memberikan pengecualian. Bagaimanapun, setelah ini kita kembali membaca 4 byte int поля field_c в constructorTwo dan dengan itu kami selesai membaca PolymorType.

Akhirnya, jika Anda tertangkap 0xdeadcrc untuk constructorThree, maka segalanya menjadi lebih rumit. Bidang pertama kami adalah bit_flags_of_what_really_present dengan tipe # - sebenarnya, ini hanyalah alias untuk tipe tersebut nat, yang berarti "bilangan asli". Faktanya, unsigned int adalah satu-satunya kasus ketika bilangan unsigned muncul di sirkuit nyata. Jadi, selanjutnya adalah konstruksi dengan tanda tanya, artinya bidang ini - akan ada di kabel hanya jika bit yang sesuai diatur di bidang yang dimaksud (kira-kira seperti operator ternary). Jadi, mari kita asumsikan bahwa bit ini telah disetel, yang berarti selanjutnya kita perlu membaca bidang seperti itu Type, yang dalam contoh kita memiliki 2 konstruktor. Yang satu kosong (hanya terdiri dari pengidentifikasi), yang lain memiliki bidang ids dengan tipe ids:Vector<long>.

Anda mungkin berpikir bahwa templat dan generiknya ada dalam versi pro atau Java. Tapi tidak. Hampir. Ini satu-satunya kasus penggunaan tanda kurung sudut di sirkuit nyata, dan ini HANYA digunakan untuk Vektor. Dalam aliran byte, ini akan menjadi 4 byte CRC32 untuk tipe Vector itu sendiri, selalu sama, kemudian 4 byte - jumlah elemen array, dan kemudian elemen itu sendiri.

Ditambah fakta bahwa serialisasi selalu terjadi dalam kata-kata 4 byte, semua tipe adalah kelipatannya - tipe bawaan juga dijelaskan bytes и string dengan serialisasi manual panjangnya dan penyelarasan ini sebanyak 4 - sepertinya terdengar normal dan bahkan relatif efektif? Meskipun TL diklaim sebagai serialisasi biner yang efektif, persetan dengan perluasan apa saja, bahkan nilai Boolean dan string karakter tunggal hingga 4 byte, apakah JSON akan tetap lebih tebal? Lihat, bahkan bidang yang tidak diperlukan pun dapat dilewati dengan tanda bit, semuanya cukup baik, dan bahkan dapat diperluas untuk masa mendatang, jadi mengapa tidak menambahkan bidang opsional baru ke konstruktor nanti?..

Tapi tidak, jika Anda tidak membaca uraian singkat saya, tetapi dokumentasi lengkapnya, dan memikirkan implementasinya. Pertama, CRC32 konstruktor dihitung berdasarkan baris deskripsi teks skema yang dinormalisasi (hapus spasi tambahan, dll.) - jadi jika bidang baru ditambahkan, baris deskripsi tipe akan berubah, dan karenanya CRC32 dan , akibatnya, serialisasi. Dan apa yang akan dilakukan klien lama jika dia menerima bidang dengan tanda baru, dan dia tidak tahu apa yang harus dilakukan selanjutnya?..

Kedua, mari kita ingat CRC32, yang pada dasarnya digunakan di sini sebagai fungsi hash untuk secara unik menentukan tipe apa yang sedang (dide)serialkan. Di sini kita dihadapkan pada masalah tabrakan - dan tidak, kemungkinannya bukan satu berbanding 232, tetapi jauh lebih besar. Siapa yang ingat bahwa CRC32 dirancang untuk mendeteksi (dan memperbaiki) kesalahan dalam saluran komunikasi, dan karenanya meningkatkan sifat-sifat ini sehingga merugikan orang lain? Misalnya, ia tidak peduli dengan penataan ulang byte: jika Anda menghitung CRC32 dari dua baris, di baris kedua Anda menukar 4 byte pertama dengan 4 byte berikutnya - hasilnya akan sama. Jika masukan kita berupa rangkaian teks dari alfabet Latin (dan sedikit tanda baca), dan nama-nama ini tidak terlalu acak, kemungkinan penataan ulang seperti itu akan sangat meningkat.

Ngomong-ngomong, siapa yang memeriksa apa yang ada di sana? benar-benar CRC32? Salah satu kode sumber awal (bahkan sebelum Waltman) memiliki fungsi hash yang mengalikan setiap karakter dengan angka 239, sangat disukai oleh orang-orang ini, ha ha!

Akhirnya, oke, kami menyadari bahwa konstruktor dengan tipe bidang Vector<int> и Vector<PolymorType> akan memiliki CRC32 yang berbeda. Bagaimana dengan kinerja online? Dan dari sudut pandang teoretis, apakah ini menjadi bagian dari tipe? Katakanlah kita melewatkan array sepuluh ribu angka, ya Vector<int> semuanya jelas, panjangnya dan 40000 byte lainnya. Bagaimana jika ini Vector<Type2>, yang hanya terdiri dari satu bidang int dan itu satu-satunya di tipe - apakah kita perlu mengulangi 10000xabcdef0 34 kali dan kemudian 4 byte int, atau bahasanya dapat MANDIRI untuk kita dari konstruktornya fixedVec dan bukannya 80000 byte, transfer lagi hanya 40000?

Ini sama sekali bukan pertanyaan teoretis yang sia-sia - bayangkan Anda menerima daftar pengguna grup, yang masing-masing memiliki id, nama depan, nama belakang - perbedaan dalam jumlah data yang ditransfer melalui koneksi seluler bisa sangat signifikan. Justru efektivitas serialisasi Telegram yang diiklankan kepada kami.

Begitu…

Vektor, yang tidak pernah dirilis

Jika Anda mencoba menelusuri halaman-halaman deskripsi kombinator dan seterusnya, Anda akan melihat bahwa sebuah vektor (dan bahkan sebuah matriks) secara formal mencoba untuk dikeluarkan melalui tupel dari beberapa lembar. Namun pada akhirnya mereka lupa, langkah terakhir dilewati, dan diberikan definisi vektor saja, yang belum terikat dengan jenisnya. Apa masalahnya? Dalam bahasa pemrograman, terutama yang fungsional, biasanya menggambarkan struktur secara rekursif - kompiler dengan evaluasinya yang malas akan memahami dan melakukan semuanya sendiri. Dalam bahasa serialisasi data yang dibutuhkan adalah EFISIENSI: cukup dijabarkan saja daftar, yaitu. struktur dua elemen - yang pertama adalah elemen data, yang kedua adalah struktur yang sama atau ruang kosong untuk ekornya (paket (cons) di Cadel). Tapi ini jelas membutuhkan dari masing-masing elemen menghabiskan 4 byte tambahan (CRC32 dalam kasus di TL) untuk menjelaskan tipenya. Sebuah array juga dapat dengan mudah dijelaskan ukuran tetap, tetapi dalam kasus array yang panjangnya tidak diketahui sebelumnya, kami memutuskannya.

Oleh karena itu, karena TL tidak mengizinkan keluaran vektor, maka harus ditambahkan di samping. Pada akhirnya dokumentasi mengatakan:

Serialisasi selalu menggunakan konstruktor “vektor” yang sama (const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t”) yang tidak bergantung pada nilai spesifik variabel bertipe t.

Nilai parameter opsional t tidak terlibat dalam serialisasi karena berasal dari tipe hasil (selalu diketahui sebelum deserialisasi).

Lihat lebih dekat: vector {t:Type} # [ t ] = Vector t - tetapi tidak ada tempat Definisi ini sendiri tidak mengatakan bahwa bilangan pertama harus sama dengan panjang vektor! Dan itu tidak datang dari mana pun. Ini adalah hal yang perlu diingat dan diterapkan dengan tangan Anda. Di tempat lain, dokumentasinya bahkan dengan jujur ​​​​menyebutkan bahwa tipe tersebut tidak nyata:

Pseudotipe polimorfik Vektor t adalah “tipe” yang nilainya merupakan urutan nilai jenis t apa pun, baik dalam kotak atau kosong.

... tapi tidak fokus padanya. Ketika Anda, lelah mengarungi hamparan matematika (bahkan mungkin Anda ketahui dari kuliah), memutuskan untuk menyerah dan benar-benar mencari cara untuk mengerjakannya dalam praktik, kesan yang tertinggal di kepala Anda adalah bahwa ini Serius Matematika pada intinya, jelas ditemukan oleh Cool People (dua ahli matematika - pemenang ACM), dan bukan sembarang orang. Tujuannya - untuk pamer - telah tercapai.

Ngomong-ngomong, tentang nomornya. Izinkan kami mengingatkan Anda akan hal itu # itu sinonim nat, bilangan asli:

Ada ekspresi tipe (tipe-expr) dan ekspresi numerik (nat-expr). Namun, keduanya didefinisikan dengan cara yang sama.

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

tetapi dalam tata bahasanya dijelaskan dengan cara yang sama, yaitu. Perbedaan ini sekali lagi harus diingat dan diterapkan secara manual.

Ya, tipe templat (vector<int>, vector<User>) memiliki pengenal umum (#1cb5c415), yaitu jika Anda tahu bahwa panggilan itu diumumkan sebagai

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

maka Anda tidak lagi menunggu hanya sebuah vektor, tetapi vektor pengguna. Lebih tepatnya, seharusnya tunggu - dalam kode nyata, setiap elemen, jika bukan tipe kosong, akan memiliki konstruktor, dan dalam implementasi yang baik perlu dilakukan pemeriksaan - tetapi kami dikirim tepat di setiap elemen vektor ini tipe itu? Bagaimana jika itu adalah sejenis PHP, di mana sebuah array dapat berisi tipe berbeda dalam elemen berbeda?

Pada titik ini Anda mulai berpikir - apakah TL seperti itu diperlukan? Mungkin untuk cart bisa menggunakan human serializer, protobuf yang sama yang sudah ada saat itu? Itu teorinya, mari kita lihat praktiknya.

Implementasi TL yang ada dalam kode

TL lahir di kedalaman VKontakte bahkan sebelum peristiwa terkenal dengan penjualan saham Durov dan (pasti), bahkan sebelum pengembangan Telegram dimulai. Dan dalam sumber terbuka kode sumber implementasi pertama Anda dapat menemukan banyak kruk lucu. Dan bahasanya sendiri diterapkan di sana lebih lengkap daripada yang sekarang di Telegram. Misalnya, hash tidak digunakan sama sekali dalam skema (artinya pseudotipe bawaan (seperti vektor) dengan perilaku menyimpang). Atau

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

tapi mari kita pertimbangkan, demi kelengkapan, untuk menelusuri, bisa dikatakan, evolusi Raksasa Pemikiran.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Atau yang cantik ini:

    static const char *reserved_words_polymorhic[] = {

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

      };

Fragmen ini tentang template seperti:

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

Ini adalah definisi tipe template hashmap sebagai vektor pasangan int - Type. Di C++ akan terlihat seperti ini:

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

jadi, alpha - kata kunci! Tapi hanya di C++ Anda bisa menulis T, tapi Anda harus menulis alpha, beta... Tapi tidak lebih dari 8 parameter, disitulah fantasi berakhir. Tampaknya pada suatu ketika di Sankt Peterburg terjadi beberapa dialog seperti ini:

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

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

Tapi ini tentang implementasi TL yang pertama kali dipublikasikan “secara umum”. Mari beralih ke penerapan pada klien Telegram itu sendiri.

Kata untuk Vasily:

Vasily, [09.10.18 17:07] Yang terpenting, pantatnya panas karena mereka membuat banyak abstraksi, lalu memasang baut pada mereka, dan menutupi pembuat kode dengan kruk
Hasilnya, pertama dari dock pilot.jpg
Kemudian dari kode dzhekichan.webp

Tentu saja, dari orang-orang yang akrab dengan algoritma dan matematika, kita dapat berharap bahwa mereka telah membaca Aho, Ullmann, dan akrab dengan alat-alat yang telah menjadi standar de facto di industri selama beberapa dekade untuk menulis kompiler DSL mereka, bukan?..

Oleh penulis telegram-cli adalah Vitaly Valtman, seperti yang dapat dipahami dari terjadinya format TLO di luar batas (cli), anggota tim - sekarang perpustakaan untuk penguraian TL telah dialokasikan terpisah, apa kesannya padanya pengurai TL? ..

16.12 04:18 Vasily: Saya pikir seseorang tidak menguasai lex+yacc
16.12 04:18 Vasily: Saya tidak bisa menjelaskan sebaliknya
16.12 04:18 Vasily: baiklah, atau mereka dibayar untuk jumlah baris di VK
16.12 04:19 Vasily: 3k+ garis dll<censored> bukannya parser

Mungkin pengecualian? Mari kita lihat caranya tidak Ini adalah klien RESMI - 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+ baris dengan Python, beberapa ekspresi reguler + kasus khusus seperti vektor, yang, tentu saja, dideklarasikan dalam skema sebagaimana mestinya sesuai dengan sintaks TL, tetapi mereka mengandalkan sintaks ini untuk menguraikannya... Timbul pertanyaan, mengapa itu semua merupakan keajaiban?иLebih berlapis jika tidak ada yang mau menguraikannya sesuai dengan dokumentasinya?!

Ngomong-ngomong... Ingat kita pernah membicarakan tentang pemeriksaan CRC32? Jadi, di pembuat kode Telegram Desktop terdapat daftar pengecualian untuk tipe yang menghitung CRC32 tidak cocok dengan yang ditunjukkan pada diagram!

Vasily, [18.12/22 49:XNUMX] dan di sini saya akan memikirkan apakah TL seperti itu diperlukan
jika saya ingin mengacaukan implementasi alternatif, saya akan mulai memasukkan jeda baris, setengah dari parser akan merusak definisi multi-baris
Namun, tdesktop juga

Ingat poin tentang one-liner, kita akan membahasnya kembali nanti.

Oke, telegram-cli tidak resmi, Telegram Desktop resmi, tapi bagaimana dengan yang lain? Siapa tahu?.. Pada kode klien Android tidak ada pengurai skema sama sekali (yang menimbulkan pertanyaan tentang open source, tapi ini untuk bagian kedua), tetapi ada beberapa potongan kode lucu lainnya, tetapi lebih banyak tentangnya di bagian subbagian di bawah ini.

Pertanyaan lain apa yang muncul dalam praktik serialisasi? Misalnya, mereka melakukan banyak hal, tentu saja, dengan bidang bit dan bidang bersyarat:

dengan mudah: flags.0? true
berarti bidang tersebut ada dan sama dengan benar jika bendera disetel

dengan mudah: flags.1? int
berarti bidang tersebut ada dan perlu dideserialisasi

Vasily: Ass, jangan khawatir dengan apa yang kamu lakukan!
Vasily: Ada yang menyebutkan di suatu tempat di dokumen bahwa true adalah tipe dengan panjang nol, tetapi tidak mungkin untuk mengumpulkan apa pun dari dokumen mereka
Mudah: Dalam implementasi open source hal ini juga tidak terjadi, tetapi ada banyak penopang dan pendukung

Bagaimana dengan Telethon? Ke depan ke topik MTProto, contohnya - di dokumentasi ada bagian seperti itu, tetapi tandanya % itu digambarkan hanya sebagai "sesuai dengan tipe telanjang tertentu", yaitu. pada contoh di bawah ini ada kesalahan atau sesuatu yang tidak terdokumentasi:

Vasily, [22.06.18 18:38] Di satu tempat:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Dalam cara yang berbeda:

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

Dan ini adalah dua perbedaan besar, dalam kehidupan nyata ada semacam vektor telanjang

Saya belum melihat definisi vektor secara langsung dan belum menemukannya

Analisis ditulis dengan tangan di telethon

Dalam diagramnya, definisi tersebut dikomentari msg_container

Sekali lagi, pertanyaannya tetap tentang%. Hal ini tidak dijelaskan.

Vadim Goncharov, [22.06.18 19:22] dan di tdesktop?

Vasily, [22.06.18 19:23] Tapi parser TL mereka di mesin biasa kemungkinan besar juga tidak akan memakan ini

// parsed manually

TL adalah abstraksi yang indah, tidak ada yang mengimplementasikannya sepenuhnya

Dan % tidak ada dalam skema versi mereka

Tapi di sini dokumentasinya bertentangan, jadi entahlah

Itu ditemukan dalam tata bahasa, mereka mungkin saja lupa menjelaskan semantiknya

Anda melihat dokumen di TL, Anda tidak dapat memahaminya tanpa setengah liter

“Baiklah, katakanlah,” pembaca lain akan berkata, “Anda mengkritik sesuatu, jadi tunjukkan kepada saya bagaimana hal itu harus dilakukan.”

Vasily menjawab: “Untuk parser, saya suka hal-hal seperti

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

entah bagaimana lebih menyukainya daripada

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

или

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

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

ini adalah lexer SELURUHnya:

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

itu. lebih sederhana secara halus.”

Secara umum, sebagai hasilnya, parser dan pembuat kode untuk subset TL yang sebenarnya digunakan cocok dengan sekitar 100 baris tata bahasa dan ~300 baris generator (termasuk semua printkode yang dihasilkan), termasuk informasi tipe roti untuk introspeksi di setiap kelas. Setiap tipe polimorfik berubah menjadi kelas dasar abstrak yang kosong, dan konstruktor mewarisinya serta memiliki metode untuk serialisasi dan deserialisasi.

Kurangnya tipe dalam bahasa tipe

Mengetik dengan kuat adalah hal yang baik, bukan? Tidak, ini bukan holivar (meskipun saya lebih suka bahasa dinamis), tetapi sebuah postulat dalam kerangka TL. Berdasarkan hal tersebut, bahasa tersebut seharusnya memberikan segala macam pemeriksaan untuk kita. Baiklah, mungkin bukan dia sendiri, tapi implementasinya, tapi setidaknya dia harus menjelaskannya. Dan peluang seperti apa yang kita inginkan?

Pertama-tama, kendala. Di sini kita melihat dokumentasi untuk mengunggah file:

Konten biner file kemudian dipecah menjadi beberapa bagian. Semua bagian harus memiliki ukuran yang sama ( bagian_ukuran ) dan ketentuan berikut harus dipenuhi:

  • part_size % 1024 = 0 (dapat dibagi 1KB)
  • 524288 % part_size = 0 (512KB harus habis dibagi part_size)

Bagian terakhir tidak harus memenuhi ketentuan ini, asalkan ukurannya kurang dari part_size.

Setiap bagian harus memiliki nomor urut, file_part, dengan nilai berkisar antara 0 hingga 2,999.

Setelah file dipartisi, Anda perlu memilih metode untuk menyimpannya di server. Menggunakan unggah.saveBigFilePart jika ukuran penuh file lebih dari 10 MB dan unggah.saveFilePart untuk file yang lebih kecil.
[…] Salah satu kesalahan input data berikut mungkin muncul:

  • FILE_PARTS_INVALID — Jumlah bagian tidak valid. Nilainya bukan di antara 1..3000

Apakah semua ini ada dalam diagram? Apakah ini dapat diungkapkan menggunakan TL? TIDAK. Tapi maaf, bahkan Turbo Pascal milik kakek pun mampu menjelaskan tipe-tipe yang ditentukan rentang. Dan dia mengetahui satu hal lagi, yang sekarang lebih dikenal sebagai enum - tipe yang terdiri dari enumerasi sejumlah nilai tetap (kecil). Dalam bahasa seperti C - numerik, perhatikan bahwa sejauh ini kita hanya membicarakan tipe angka. Tapi ada juga array, string... misalnya, alangkah baiknya jika dijelaskan bahwa string ini hanya bisa berisi nomor telepon, bukan?

Semua ini tidak ada di TL. Tapi ada, misalnya, di Skema JSON. Dan jika orang lain mungkin berpendapat tentang pembagian 512 KB, bahwa ini masih perlu diperiksa dalam kode, maka pastikan bahwa klien cukup tidak dapat mengirim nomor di luar jangkauan 1..3000 (dan kesalahan terkait tidak mungkin muncul) itu mungkin saja terjadi, bukan?..

Ngomong-ngomong, tentang kesalahan dan nilai kembalian. Bahkan mereka yang pernah bekerja dengan TL mengaburkan pandangan mereka - kami tidak langsung menyadarinya setiap suatu fungsi di TL sebenarnya tidak hanya dapat mengembalikan tipe pengembalian yang dijelaskan, tetapi juga kesalahan. Tapi ini tidak dapat disimpulkan dengan cara apapun menggunakan TL itu sendiri. Tentu saja sudah jelas dan tidak perlu praktek apa-apa (walaupun sebenarnya RPC bisa dilakukan dengan cara yang berbeda-beda, kita akan membahasnya lagi nanti) - tapi bagaimana dengan Kemurnian konsep Matematika Tipe Abstrak dari dunia surgawi?.. Saya mengambil kapal tunda - jadi cocokkan.

Dan terakhir, bagaimana dengan keterbacaan? Ya, secara umum, saya ingin deskripsi memilikinya dengan benar dalam skema (dalam skema JSON, sekali lagi, memang demikian), tetapi jika Anda sudah merasa tegang dengan hal itu, lalu bagaimana dengan sisi praktisnya - setidaknya melihat perbedaan selama pembaruan? Lihat sendiri di contoh nyata:

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

или

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

Itu tergantung pada semua orang, tetapi GitHub, misalnya, menolak untuk menyoroti perubahan dalam antrean panjang tersebut. Permainan “temukan 10 perbedaan”, dan yang langsung dilihat otak adalah bahwa awal dan akhir kedua contoh itu sama, Anda perlu membaca dengan cermat di tengah-tengah... Menurut saya, ini bukan hanya teori, tapi murni secara visual kotor dan ceroboh.

Berbicara tentang kemurnian teori. Mengapa kita memerlukan bidang bit? Bukankah itu terlihat seperti itu? bau buruk dari sudut pandang teori tipe? Penjelasannya dapat dilihat pada diagram versi sebelumnya. Awalnya ya begitulah, setiap bersin tercipta tipe baru. Dasar-dasar ini masih ada dalam bentuk ini, misalnya:

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;

Tapi sekarang bayangkan, jika Anda memiliki 5 bidang opsional dalam struktur Anda, maka Anda memerlukan 32 tipe untuk semua opsi yang memungkinkan. Ledakan kombinatorial. Dengan demikian, kemurnian kristal dari teori TL sekali lagi hancur berkeping-keping dari kenyataan pahit serialisasi.

Selain itu, di beberapa tempat orang-orang ini sendiri melanggar tipologinya sendiri. Misalnya, di MTProto (bab berikutnya), responsnya dapat dikompresi dengan Gzip, semuanya baik-baik saja - kecuali lapisan dan sirkuitnya dilanggar. Sekali lagi, yang dipetik bukanlah RpcResult itu sendiri, melainkan isinya. Nah, kenapa melakukan ini?.. Saya harus memotong kruk agar kompresi bisa bekerja di mana saja.

Atau contoh lain, kami pernah menemukan kesalahan - kesalahan itu terkirim InputPeerUser daripada InputUser. Atau sebaliknya. Tapi itu berhasil! Artinya, server tidak peduli dengan jenisnya. Bagaimana ini bisa terjadi? Jawabannya mungkin diberikan kepada kita melalui potongan kode dari 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);

Dengan kata lain, di sinilah serialisasi dilakukan SECARA MANUAL, bukan kode yang dihasilkan! Mungkin server diimplementasikan dengan cara yang sama?.. Pada prinsipnya, ini akan berhasil jika dilakukan sekali, tetapi bagaimana cara mendukungnya nanti selama pembaruan? Apakah ini sebabnya skema ini diciptakan? Dan di sini kita beralih ke pertanyaan berikutnya.

Pembuatan versi. Lapisan

Mengapa versi skema disebut lapisan hanya dapat dispekulasikan berdasarkan sejarah skema yang diterbitkan. Rupanya, pada awalnya penulis berpikir bahwa hal-hal dasar dapat dilakukan dengan menggunakan skema yang tidak berubah, dan hanya jika diperlukan, untuk permintaan khusus, menunjukkan bahwa hal tersebut dilakukan dengan menggunakan versi yang berbeda. Pada prinsipnya, bahkan ide yang bagus - dan yang baru akan seolah-olah “bercampur”, berlapis di atas yang lama. Tapi mari kita lihat bagaimana hal itu dilakukan. Benar, saya tidak dapat melihatnya dari awal - ini lucu, tetapi diagram lapisan dasar tidak ada. Lapisan dimulai dengan 2. Dokumentasi memberi tahu kita tentang fitur TL khusus:

Jika klien mendukung Layer 2, maka konstruktor berikut harus digunakan:

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

Dalam praktiknya, ini berarti bahwa sebelum setiap panggilan API, sebuah int dengan nilai 0x289dd1f6 harus ditambahkan sebelum nomor metode.

Kedengarannya normal. Tapi apa yang terjadi selanjutnya? Lalu muncul

invokeWithLayer3#b7475268 query:!X = X;

Jadi apa selanjutnya? Seperti yang Anda duga,

invokeWithLayer4#dea0d430 query:!X = X;

Lucu? Tidak, masih terlalu dini untuk tertawa, pikirkan fakta itu masing-masing permintaan dari lapisan lain perlu dibungkus dengan tipe khusus - jika semuanya berbeda untuk Anda, bagaimana lagi Anda bisa membedakannya? Dan menambahkan hanya 4 byte di depan adalah metode yang cukup efisien. Jadi,

invokeWithLayer5#417a57ae query:!X = X;

Tapi jelas bahwa setelah beberapa saat ini akan menjadi semacam bacchanalia. Dan solusinya pun muncul:

Pembaruan: Dimulai dengan Layer 9, metode pembantu invokeWithLayerN hanya dapat digunakan bersama dengan initConnection

Hore! Setelah 9 versi, kami akhirnya sampai pada apa yang dilakukan dalam protokol Internet di tahun 80an - menyetujui versi tersebut satu kali di awal koneksi!

Lalu apa selanjutnya?..

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

Tapi sekarang kamu masih bisa tertawa. Hanya setelah 9 lapisan berikutnya, konstruktor universal dengan nomor versi akhirnya ditambahkan, yang hanya perlu dipanggil sekali di awal koneksi, dan arti dari lapisan tersebut sepertinya telah hilang, sekarang hanya versi bersyarat, seperti di tempat lain. Masalah terpecahkan.

Tepat?..

Vasily, [16.07.18 14:01] Bahkan pada hari Jumat saya berpikir:
Teleserver mengirimkan acara tanpa permintaan. Permintaan harus dibungkus dalam InvokeWithLayer. Server tidak membungkus pembaruan; tidak ada struktur untuk membungkus tanggapan dan pembaruan.

Itu. klien tidak dapat menentukan lapisan di mana dia ingin pembaruan

Vadim Goncharov, [16.07.18 14:02] bukankah InvokeWithLayer pada prinsipnya merupakan penopang?

Vasily, [16.07.18 14:02] Ini adalah satu-satunya cara

Vadim Goncharov, [16.07.18 14:02] yang pada dasarnya berarti menyetujui lapisan di awal sesi

Omong-omong, penurunan versi klien tidak disediakan

Pembaruan, mis. jenis Updates dalam skema, ini adalah apa yang dikirimkan server ke klien bukan sebagai respons terhadap permintaan API, tetapi secara independen ketika suatu peristiwa terjadi. Ini adalah topik kompleks yang akan dibahas di postingan lain, tetapi untuk saat ini penting untuk mengetahui bahwa server menyimpan Pembaruan bahkan ketika klien sedang offline.

Jadi, jika Anda menolak untuk membungkusnya dari masing-masing paket untuk menunjukkan versinya, ini secara logis mengarah ke kemungkinan masalah berikut:

  • server mengirimkan pembaruan ke klien bahkan sebelum klien menginformasikan versi mana yang didukungnya
  • apa yang harus saya lakukan setelah memutakhirkan klien?
  • siapa jaminanbahwa pendapat server tentang nomor lapisan tidak akan berubah selama proses berlangsung?

Apakah menurut Anda ini murni spekulasi teoretis, dan dalam praktiknya hal ini tidak dapat terjadi, karena server ditulis dengan benar (setidaknya, telah diuji dengan baik)? Ha! Tidak peduli bagaimana keadaannya!

Inilah yang kami temui pada bulan Agustus. Pada tanggal 14 Agustus, ada pesan bahwa ada sesuatu yang sedang diperbarui di server Telegram... dan kemudian di log:

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.

dan kemudian beberapa megabyte jejak tumpukan (yah, pada saat yang sama logging telah diperbaiki). Lagi pula, jika ada sesuatu yang tidak dikenali di TL Anda, itu adalah biner berdasarkan tanda tangan, di bagian selanjutnya SEMUA pergi, decoding menjadi tidak mungkin. Apa yang harus Anda lakukan dalam situasi seperti ini?

Ya, hal pertama yang terlintas di benak siapa pun adalah memutuskan sambungan dan mencoba lagi. Tidak membantu. Kami Google CRC32 - ini ternyata adalah objek dari skema 73, meskipun kami mengerjakan 82. Kami melihat log dengan cermat - ada pengidentifikasi dari dua skema berbeda!

Mungkin masalahnya murni di klien tidak resmi kami? Tidak, kami meluncurkan Telegram Desktop 1.2.17 (versi yang disediakan di sejumlah distribusi Linux), ia menulis ke log Pengecualian: MTP Tipe id tak terduga #b5223b0f dibaca di MTPMessageMedia…

Kritik terhadap protokol dan pendekatan organisasi Telegram. Bagian 1, teknis: pengalaman menulis klien dari awal - TL, MT

Google menunjukkan bahwa masalah serupa telah terjadi pada salah satu klien tidak resmi, tetapi nomor versi dan, karenanya, asumsinya berbeda...

Jadi apa yang harus kita lakukan? Vasily dan saya berpisah: dia mencoba memperbarui sirkuit ke 91, saya memutuskan untuk menunggu beberapa hari dan mencoba pada 73. Kedua metode berhasil, tetapi karena bersifat empiris, tidak ada pemahaman tentang berapa banyak versi naik atau turun yang Anda perlukan untuk melompat, atau berapa lama Anda harus menunggu.

Kemudian saya dapat mereproduksi situasinya: kami meluncurkan klien, mematikannya, mengkompilasi ulang sirkuit ke lapisan lain, memulai ulang, menangkap masalahnya lagi, kembali ke yang sebelumnya - ups, tidak ada jumlah peralihan sirkuit dan klien memulai ulang untuk a beberapa menit akan membantu. Anda akan menerima campuran struktur data dari berbagai lapisan.

Penjelasan? Seperti yang dapat Anda tebak dari berbagai gejala tidak langsung, server terdiri dari banyak proses dengan tipe berbeda pada mesin berbeda. Kemungkinan besar, server yang bertanggung jawab untuk "buffering" memasukkan ke dalam antrian apa yang diberikan oleh atasannya, dan mereka memberikannya dalam skema yang ada pada saat pembuatan. Dan sampai antrian ini “busuk”, tidak ada yang bisa dilakukan untuk mengatasinya.

Mungkin... tapi ini penopang yang buruk?!.. Tidak, sebelum memikirkan ide-ide gila, mari kita lihat kode klien resminya. Dalam versi Android kami tidak menemukan parser TL apa pun, tetapi kami menemukan file yang besar dan kuat (GitHub menolak untuk memperbaikinya) dengan (de)serialisasi. Berikut cuplikan kodenya:

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

или

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

Hmm... terlihat liar. Tapi, mungkin, ini adalah kode yang dihasilkan, oke?.. Tapi yang pasti mendukung semua versi! Benar, tidak jelas mengapa semuanya tercampur, obrolan rahasia, dan segala macamnya _old7 entah bagaimana tidak terlihat seperti generasi mesin... Namun, yang terpenting, saya terpesona olehnya

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Teman-teman, tidak bisakah kalian memutuskan apa yang ada di dalam satu lapisan?! Baiklah, katakanlah "dua" dirilis dengan kesalahan, ya, itu terjadi, tapi TIGA?.. Segera, penggaruk yang sama lagi? Pornografi macam apa ini, maaf?..

Omong-omong, dalam kode sumber Telegram Desktop, hal serupa terjadi - jika demikian, beberapa komitmen berturut-turut pada skema tidak mengubah nomor lapisannya, tetapi memperbaiki sesuatu. Dalam kondisi di mana tidak ada sumber data resmi untuk skema tersebut, dari mana data tersebut dapat diperoleh, kecuali kode sumber dari klien resmi? Dan jika Anda mengambilnya dari sana, Anda tidak dapat memastikan bahwa skema tersebut sepenuhnya benar sampai Anda menguji semua metode.

Bagaimana ini bisa diuji? Saya harap penggemar tes unit, fungsional, dan lainnya akan berbagi di komentar.

Oke, mari kita lihat potongan kode lainnya:

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;

Komentar “dibuat secara manual” ini menunjukkan bahwa hanya sebagian dari file ini yang ditulis secara manual (dapatkah Anda membayangkan seluruh mimpi buruk pemeliharaan?), dan sisanya dibuat oleh mesin. Namun, kemudian muncul pertanyaan lain – apakah sumbernya tersedia tidak sepenuhnya (ala gumpalan GPL di kernel Linux), tetapi ini sudah menjadi topik untuk bagian kedua.

Tapi cukup. Mari beralih ke protokol yang menjalankan semua serialisasi ini.

MT Proto

Jadi, mari kita buka gambaran umum и penjelasan rinci tentang protokol dan hal pertama yang membuat kita tersandung adalah terminologinya. Dan dengan segalanya yang berlimpah. Secara umum, ini tampaknya merupakan fitur eksklusif Telegram - menyebut sesuatu secara berbeda di tempat berbeda, atau berbeda dengan satu kata, atau sebaliknya (misalnya, di API tingkat tinggi, jika Anda melihat paket stiker, itu tidak benar. apa yang kamu pikirkan).

Misalnya, “pesan” dan “sesi” memiliki arti yang berbeda di sini dibandingkan antarmuka klien Telegram biasa. Nah, dengan pesannya semuanya jelas, bisa diartikan dalam istilah OOP, atau cukup disebut kata "paket" - ini adalah level transport yang rendah, tidak ada pesan yang sama seperti di antarmuka, ada banyak pesan layanan . Tapi sesinya... tapi yang terpenting adalah yang utama.

lapisan transportasi

Hal pertama adalah transportasi. Mereka akan memberi tahu kami tentang 5 opsi:

  • TCP
  • soket web
  • Soket web melalui HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Ada juga transport UDP, tapi tidak didokumentasikan

Dan TCP dalam tiga varian

Yang pertama mirip dengan UDP melalui TCP, setiap paket menyertakan nomor urut dan crc
Mengapa membaca dokumen di kereta begitu menyakitkan?

Nah, itu dia sekarang TCP sudah dalam 4 varian:

  • Ringkas
  • Menengah
  • Menengah empuk
  • Penuh

Baiklah, perantara empuk untuk MTProxy, ini kemudian ditambahkan karena acara terkenal. Tetapi mengapa harus ada dua versi lagi (total tiga) jika Anda bisa bertahan dengan satu versi? Keempatnya pada dasarnya berbeda hanya pada cara mengatur panjang dan payload MTProto utama, yang akan dibahas lebih lanjut:

  • di Ringkasnya adalah 1 atau 4 byte, tetapi bukan 0xef, lalu isi
  • di Menengah ini panjangnya 4 byte dan satu bidang, dan pertama kali klien harus mengirim 0xeeeeeeee untuk menunjukkan bahwa itu adalah Intermediate
  • secara Lengkap yang paling membuat ketagihan, dari sudut pandang seorang networker: panjang, nomor urut, dan BUKAN YANG terutama MTProto, body, CRC32. Ya, semua ini di atas TCP. Yang memberi kita transportasi yang andal dalam bentuk aliran byte berurutan; tidak diperlukan urutan, terutama checksum. Oke, sekarang ada yang keberatan dengan saya karena TCP memiliki checksum 16-bit, sehingga terjadi kerusakan data. Hebat, tetapi kami sebenarnya memiliki protokol kriptografi dengan hash yang lebih panjang dari 16 byte, semua kesalahan ini - dan bahkan lebih banyak lagi - akan ditangkap oleh ketidakcocokan SHA di tingkat yang lebih tinggi. TIDAK ada gunanya CRC32 selain ini.

Mari kita bandingkan Abridged, yang memungkinkan panjangnya satu byte, dengan Intermediate, yang membenarkan “Jika diperlukan penyelarasan data 4-byte,” yang merupakan hal yang tidak masuk akal. Apa yang diyakini bahwa pemrogram Telegram sangat tidak kompeten sehingga mereka tidak dapat membaca data dari soket ke dalam buffer yang selaras? Anda masih harus melakukan ini, karena membaca dapat mengembalikan sejumlah byte (dan ada juga server proxy, misalnya...). Atau di sisi lain, mengapa memblokir Abridged jika kita masih memiliki padding yang besar dan kuat di atas 16 byte - hemat 3 byte kadang-kadang ?

Ada kesan bahwa Nikolai Durov sangat suka menemukan kembali roda, termasuk protokol jaringan, tanpa kebutuhan praktis yang nyata.

Pilihan transportasi lainnya, termasuk. Web dan MTProxy tidak akan kami pertimbangkan sekarang, mungkin di postingan lain jika ada request. Tentang MTProxy yang sama, mari kita ingat sekarang bahwa tak lama setelah dirilis pada tahun 2018, penyedia dengan cepat belajar untuk memblokirnya, yang dimaksudkan untuk melewati pemblokiranOleh Ukuran paket! Dan juga fakta bahwa server MTProxy yang ditulis (sekali lagi oleh Waltman) di C terlalu terikat dengan spesifikasi Linux, meskipun ini tidak diperlukan sama sekali (Phil Kulin akan mengonfirmasi), dan bahwa server serupa baik di Go atau Node.js akan melakukannya muat dalam kurang dari seratus baris.

Namun kami akan menarik kesimpulan tentang literasi teknis orang-orang ini di akhir bagian ini, setelah mempertimbangkan masalah lainnya. Untuk saat ini, mari beralih ke OSI layer 5, sesi - tempat mereka menempatkan sesi MTProto.

Kunci, pesan, sesi, Diffie-Hellman

Mereka menempatkannya di sana tidak sepenuhnya benar... Sesi bukanlah sesi yang sama yang terlihat di antarmuka di bawah Sesi aktif. Tapi secara berurutan.

Kritik terhadap protokol dan pendekatan organisasi Telegram. Bagian 1, teknis: pengalaman menulis klien dari awal - TL, MT

Jadi kami menerima string byte yang panjangnya diketahui dari lapisan transport. Ini bisa berupa pesan terenkripsi atau teks biasa - jika kita masih dalam tahap perjanjian kunci dan benar-benar melakukannya. Di antara sekumpulan konsep yang disebut “kunci” manakah yang sedang kita bicarakan? Mari kita perjelas masalah ini untuk tim Telegram sendiri (Saya minta maaf karena menerjemahkan dokumentasi saya sendiri dari bahasa Inggris dengan otak yang lelah pada jam 4 pagi, lebih mudah untuk membiarkan beberapa frasa apa adanya):

Ada dua entitas yang disebut Sidang - satu di UI klien resmi di bawah "sesi saat ini", di mana setiap sesi berhubungan dengan seluruh perangkat / OS.
Yang kedua adalah Sesi MTProto, yang memiliki nomor urut pesan (dalam arti tingkat rendah) di dalamnya, dan yang mana dapat bertahan di antara koneksi TCP yang berbeda. Beberapa sesi MTProto dapat diinstal secara bersamaan, misalnya untuk mempercepat pengunduhan file.

Di antara keduanya sesi ada sebuah konsep otorisasi. Dalam kasus yang merosot, kita dapat mengatakan demikian sesi UI sama dengan otorisasi, tapi sayangnya, semuanya rumit. Mari lihat:

  • Pengguna di perangkat baru pertama kali membuat kunci_autentikasi dan mengikatnya ke akun, misalnya melalui SMS - itulah alasannya otorisasi
  • Itu terjadi di bagian dalam yang pertama Sesi MTProto, yang memiliki session_id di dalam dirimu.
  • Pada langkah ini, kombinasinya otorisasi и session_id bisa dipanggil contoh - kata ini muncul di dokumentasi dan kode beberapa klien
  • Kemudian, klien dapat membuka beberapa Sesi MTProto di bawah yang sama kunci_autentikasi - ke DC yang sama.
  • Kemudian, suatu hari klien perlu meminta file tersebut DC lainnya - dan untuk DC ini yang baru akan dibuat kunci_autentikasi !
  • Untuk memberi tahu sistem bahwa yang mendaftar bukanlah pengguna baru, melainkan sama otorisasi (sesi UI), klien menggunakan panggilan API auth.exportAuthorization di rumah DC auth.importAuthorization di DC baru.
  • Semuanya sama, mungkin ada beberapa yang terbuka Sesi MTProto (masing-masing dengan miliknya sendiri session_id) ke DC baru ini, di bawah -nya kunci_autentikasi.
  • Terakhir, klien mungkin menginginkan Kerahasiaan Penerusan Sempurna. Setiap kunci_autentikasi itu permanen kunci - per DC - dan klien dapat menelepon auth.bindTempAuthKey untuk digunakan sementara kunci_autentikasi - dan sekali lagi, hanya satu temp_auth_key per DC, umum untuk semua Sesi MTProto ke DC ini.

Catat itu garam (dan garam masa depan) juga aktif kunci_autentikasi itu. dibagikan di antara semua orang Sesi MTProto ke DC yang sama.

Apa yang dimaksud dengan "antara koneksi TCP yang berbeda"? Jadi ini artinya sesuatu seperti cookie otorisasi di situs web - cookie ini bertahan (bertahan) pada banyak koneksi TCP ke server tertentu, tetapi suatu hari menjadi buruk. Hanya tidak seperti HTTP, di MTProto, pesan-pesan dalam suatu sesi diberi nomor urut dan dikonfirmasi; jika mereka memasuki terowongan, koneksi terputus - setelah membuat koneksi baru, server akan dengan baik hati mengirimkan semua yang ada di sesi ini yang tidak terkirim di sesi sebelumnya koneksi TCP.

Namun, informasi di atas dirangkum setelah penyelidikan berbulan-bulan. Sementara itu, apakah kami menerapkan klien kami dari awal? - ayo kembali ke awal.

Jadi mari kita menghasilkan auth_key pada Versi Diffie-Hellman dari Telegram. Mari kita coba memahami dokumentasinya...

Mudah, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (byte acak apa pun); sedemikian rupa sehingga panjangnya sama dengan 255 byte;
data_terenkripsi := RSA(data_with_hash, server_public_key); bilangan sepanjang 255 byte (big endian) dipangkatkan ke modulus yang diperlukan, dan hasilnya disimpan sebagai bilangan 256 byte.

Mereka punya obat bius DH

Tidak terlihat seperti DH orang sehat
Tidak ada dua kunci publik di dx

Nah, pada akhirnya hal ini telah terselesaikan, namun masih ada sisa - bukti kerja telah dilakukan oleh klien bahwa ia mampu memfaktorkan bilangan tersebut. Jenis perlindungan terhadap serangan DoS. Dan kunci RSA hanya digunakan sekali dalam satu arah, pada dasarnya untuk enkripsi new_nonce. Namun meskipun operasi yang tampaknya sederhana ini berhasil, apa yang harus Anda hadapi?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Saya belum menerima permintaan appid

Saya mengirimkan permintaan ini ke DH

Dan, di dok transportasi dikatakan dapat merespons dengan kode kesalahan 4 byte. Itu saja

Yah, dia memberitahuku -404, lalu kenapa?

Jadi saya mengatakan kepadanya: “Tangkap omong kosong Anda yang dienkripsi dengan kunci server dengan sidik jari seperti ini, saya ingin DH,” dan dia merespons dengan 404 yang bodoh

Apa pendapat Anda tentang respons server ini? Apa yang harus dilakukan? Tidak ada yang bertanya (tetapi lebih dari itu di bagian kedua).

Di sini semua kepentingan dilakukan di dermaga

Saya tidak punya pekerjaan lain, saya hanya bermimpi mengonversi angka bolak-balik

Dua nomor 32 bit. Saya mengemasnya seperti orang lain

Tapi tidak, keduanya perlu ditambahkan ke baris terlebih dahulu sebagai BE

Vadim Goncharov, [20.06.18 15:49] dan karena ini 404?

Mudah, [20.06.18 15:49] YA!

Vadim Goncharov, [20.06.18 15:50] jadi saya tidak mengerti apa yang "tidak dia temukan"

Mudah, [20.06.18 15:50] tentang

Saya tidak dapat menemukan dekomposisi menjadi faktor prima%)

Kami bahkan tidak mengelola pelaporan kesalahan

Vasily, [20.06.18 20:18] Oh, ada MD5 juga. Sudah tiga hash berbeda

Sidik jari kunci dihitung sebagai berikut:

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

SHA1 dan sha2

Jadi mari kita jelaskan auth_key kami menerima ukuran 2048 bit menggunakan Diffie-Hellman. Apa berikutnya? Selanjutnya kita menemukan bahwa 1024 bit yang lebih rendah dari kunci ini tidak digunakan dengan cara apa pun... tapi mari kita pikirkan hal ini sekarang. Pada langkah ini, kami memiliki rahasia bersama dengan server. Analog dari sesi TLS telah dibuat, yang merupakan prosedur yang sangat mahal. Tapi server masih belum tahu apa-apa tentang siapa kita! Sebenarnya belum. otorisasi. Itu. jika Anda berpikir dalam istilah "kata sandi masuk", seperti yang pernah Anda lakukan di ICQ, atau setidaknya "kunci masuk", seperti di SSH (misalnya, di beberapa gitlab/github). Kami menerima yang anonim. Bagaimana jika server memberi tahu kita “nomor telepon ini dilayani oleh DC lain”? Atau bahkan “nomor telepon Anda diblokir”? Hal terbaik yang bisa kita lakukan adalah menyimpan kunci tersebut dengan harapan dapat berguna dan tidak membusuk saat itu juga.

Ngomong-ngomong, kami “menerimanya” dengan reservasi. Misalnya, apakah kita mempercayai servernya? Bagaimana jika itu palsu? Pemeriksaan kriptografi diperlukan:

Vasily, [21.06.18 17:53] Mereka menawarkan klien seluler untuk memeriksa nomor 2kbit untuk mengetahui primalitas%)

Tapi tidak jelas sama sekali, nafeijoa

Vasily, [21.06.18 18:02] Dokumen tersebut tidak mengatakan apa yang harus dilakukan jika ternyata tidak sederhana

Tidak dikatakan. Mari kita lihat apa yang dilakukan klien Android resmi dalam kasus ini? A s apa (dan ya, keseluruhan file menarik) - seperti yang mereka katakan, saya akan meninggalkan ini di sini:

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

Tidak, tentu saja masih ada beberapa Ada tes untuk primalitas suatu bilangan, tetapi secara pribadi saya tidak lagi memiliki pengetahuan matematika yang memadai.

Oke, kita sudah mendapatkan kunci masternya. Untuk masuk, mis. mengirim permintaan, Anda perlu melakukan enkripsi lebih lanjut, menggunakan AES.

Kunci pesan didefinisikan sebagai 128 bit tengah SHA256 isi pesan (termasuk sesi, ID pesan, dll.), termasuk byte padding, diawali dengan 32 byte yang diambil dari kunci otorisasi.

Vasily, [22.06.18 14:08] Rata-rata, jalang, bit

Diterima auth_key. Semua. Di luar mereka... tidak jelas dari dokumen. Jangan ragu untuk mempelajari kode sumber terbuka.

Perhatikan bahwa MTProto 2.0 memerlukan padding 12 hingga 1024 byte, namun tetap tunduk pada ketentuan bahwa panjang pesan yang dihasilkan dapat dibagi 16 byte.

Jadi berapa banyak padding yang harus Anda tambahkan?

Dan ya, ada juga 404 jika terjadi kesalahan

Jika ada yang mempelajari diagram dan teks dokumentasi dengan cermat, mereka akan memperhatikan bahwa tidak ada MAC di sana. Dan AES tersebut digunakan dalam mode IGE tertentu yang tidak digunakan di tempat lain. Mereka, tentu saja, menulis tentang ini di FAQ mereka... Di sini, seperti, kunci pesan itu sendiri juga merupakan hash SHA dari data yang didekripsi, digunakan untuk memeriksa integritas - dan jika terjadi ketidakcocokan, dokumentasi untuk beberapa alasan merekomendasikan untuk mengabaikannya secara diam-diam (tetapi bagaimana dengan keamanan, bagaimana jika mereka menghancurkan kita?).

Saya bukan seorang kriptografer, mungkin tidak ada yang salah dengan mode ini dalam hal ini dari sudut pandang teoritis. Namun saya dapat dengan jelas menyebutkan masalah praktisnya, dengan menggunakan Telegram Desktop sebagai contoh. Ini mengenkripsi cache lokal (semua D877F783D5D3EF8C) dengan cara yang sama seperti pesan di MTProto (hanya dalam kasus ini versi 1.0), yaitu. pertama kunci pesannya, lalu datanya sendiri (dan di samping kunci utama auth_key 256 byte, tanpanya msg_key tidak berguna). Jadi, masalahnya menjadi terlihat pada file besar. Yaitu, Anda perlu menyimpan dua salinan data - terenkripsi dan didekripsi. Dan jika ada megabyte, atau streaming video, misalnya?.. Skema klasik dengan MAC setelah ciphertext memungkinkan Anda membacanya secara streaming, segera mentransmisikannya. Namun dengan MTProto Anda harus melakukannya pada awalnya mengenkripsi atau mendekripsi seluruh pesan, baru kemudian mentransfernya ke jaringan atau ke disk. Oleh karena itu, di Telegram Desktop versi terbaru, cache masuk user_data Format lain juga digunakan - dengan AES dalam mode CTR.

Vasily, [21.06.18 01:27] Oh, saya mengetahui apa itu IGE: IGE adalah upaya pertama pada "mode enkripsi autentikasi", awalnya untuk Kerberos. Ini adalah upaya yang gagal (tidak memberikan perlindungan integritas), dan harus dihapus. Itu adalah awal dari pencarian 20 tahun untuk mode enkripsi autentikasi yang berhasil, yang baru-baru ini mencapai puncaknya pada mode seperti OCB dan GCM.

Dan sekarang argumen dari sisi kereta:

Tim di belakang Telegram, dipimpin oleh Nikolai Durov, terdiri dari enam juara ACM, setengah dari mereka adalah Ph.D di bidang matematika. Mereka membutuhkan waktu sekitar dua tahun untuk meluncurkan versi MTProto saat ini.

Itu lucu. Dua tahun di level bawah

Atau Anda bisa mengambil tls

Oke, katakanlah kita sudah melakukan enkripsi dan nuansa lainnya. Apakah akhirnya mungkin untuk mengirim permintaan berseri dalam TL dan membatalkan serialisasi tanggapannya? Jadi apa dan bagaimana sebaiknya Anda mengirim? Katakanlah, inilah metodenya initConnection, mungkin ini dia?

Vasily, [25.06.18 18:46] Menginisialisasi koneksi dan menyimpan informasi pada perangkat dan aplikasi pengguna.

Ia menerima app_id, device_model, system_version, app_version dan lang_code.

Dan beberapa pertanyaan

Dokumentasi seperti biasa. Jangan ragu untuk mempelajari open source

Jika semuanya sudah jelas dengan invokeWithLayer, lalu apa yang salah di sini? Ternyata, misalkan kita punya - klien sudah punya sesuatu untuk ditanyakan ke server - ada permintaan yang ingin kita kirim:

Vasily, [25.06.18 19:13] Dilihat dari kodenya, panggilan pertama dibungkus dengan omong kosong ini, dan omong kosong itu sendiri dibungkus dengan invokewithlayer

Mengapa initConnection tidak bisa menjadi panggilan terpisah, tetapi harus menjadi pembungkus? Ya, ternyata, hal itu harus dilakukan setiap saat di awal setiap sesi, dan tidak hanya sekali, seperti pada kunci utama. Tetapi! Itu tidak dapat dipanggil oleh pengguna yang tidak sah! Sekarang kita telah mencapai tahap penerapannya Yang ini halaman dokumentasi - dan ini memberi tahu kita bahwa...

Hanya sebagian kecil dari metode API yang tersedia untuk pengguna tidak sah:

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

Yang pertama dari mereka, auth.sendCode, dan ada permintaan pertama yang kami hargai di mana kami mengirim api_id dan api_hash, dan setelah itu kami menerima SMS dengan kode. Dan jika kita salah DC (nomor telepon di negara ini dilayani oleh negara lain misalnya), maka kita akan mendapat error dengan nomor DC yang diinginkan. Untuk mengetahui alamat IP mana berdasarkan nomor DC yang perlu Anda sambungkan, bantu kami help.getConfig. Dulunya hanya ada 5 entri, namun setelah peristiwa terkenal tahun 2018, jumlahnya meningkat secara signifikan.

Sekarang mari kita ingat bahwa kita sampai pada tahap ini di server secara anonim. Bukankah terlalu mahal untuk sekedar mendapatkan alamat IP? Mengapa tidak melakukan ini, dan operasi lainnya, di bagian MTProto yang tidak terenkripsi? Saya mendengar keberatan: “bagaimana kita bisa memastikan bahwa bukan RKN yang akan membalas dengan alamat palsu?” Untuk ini kita ingat bahwa, secara umum, klien resmi Kunci RSA tertanam, yaitu. bisakah kamu saja langganan informasi ini. Sebenarnya hal ini sudah dilakukan untuk informasi cara melewati pemblokiran yang diterima klien melalui saluran lain (secara logika, hal ini tidak dapat dilakukan di MTProto itu sendiri; Anda juga perlu tahu di mana harus terhubung).

OKE. Pada tahap otorisasi klien ini, kami belum berwenang dan belum mendaftarkan aplikasi kami. Kami hanya ingin melihat untuk saat ini respons server terhadap metode yang tersedia untuk pengguna yang tidak sah. Dan di sini…

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

Dalam skema tersebut, yang pertama datang yang kedua

Dalam skema tdesktop nilai ketiga adalah

Ya, sejak itu tentu saja dokumentasinya telah diperbarui. Meskipun mungkin akan segera menjadi tidak relevan lagi. Bagaimana seharusnya pengembang pemula mengetahuinya? Mungkin jika Anda mendaftarkan aplikasi Anda, mereka akan memberi tahu Anda? Vasily melakukan ini, tetapi sayangnya, mereka tidak mengiriminya apa pun (sekali lagi, kita akan membicarakannya di bagian kedua).

...Anda memperhatikan bahwa kami telah berpindah ke API, mis. ke level berikutnya, dan melewatkan sesuatu di topik MTProto? Tidak ada kejutan:

Vasily, [28.06.18 02:04] Mm, mereka mengobrak-abrik beberapa algoritma di e2e

Mtproto mendefinisikan algoritma enkripsi dan kunci untuk kedua domain, serta sedikit struktur pembungkus

Namun mereka terus-menerus mencampur level tumpukan yang berbeda, jadi tidak selalu jelas di mana mtproto berakhir dan level berikutnya dimulai

Bagaimana cara mereka bercampur? Nah, ini kunci sementara yang sama untuk PFS, misalnya (omong-omong, Telegram Desktop tidak bisa melakukannya). Itu dijalankan oleh permintaan API auth.bindTempAuthKey, yaitu. dari tingkat atas. Tetapi pada saat yang sama, ini mengganggu enkripsi di tingkat yang lebih rendah - setelah itu, misalnya, Anda perlu melakukannya lagi initConnection dll., ini bukan hanya permintaan biasa. Yang juga istimewa adalah Anda hanya dapat memiliki SATU kunci sementara per DC, meskipun di lapangan auth_key_id di setiap pesan memungkinkan Anda untuk mengubah kunci setidaknya setiap pesan, dan bahwa server memiliki hak untuk "melupakan" kunci sementara kapan saja - dokumentasi tidak mengatakan apa yang harus dilakukan dalam kasus ini... yah, mengapa tidak bisa bukankah Anda memiliki beberapa kunci, seperti sekumpulan garam masa depan, dan?..

Ada beberapa hal lain yang perlu diperhatikan tentang tema MTProto.

Pesan pesan, msg_id, msg_seqno, konfirmasi, ping ke arah yang salah dan keanehan lainnya

Mengapa Anda perlu mengetahuinya? Karena mereka “bocor” ke tingkat yang lebih tinggi, dan Anda perlu mewaspadainya saat bekerja dengan API. Anggaplah kita tidak tertarik pada msg_key; level yang lebih rendah telah mendekripsi semuanya untuk kita. Namun di dalam data yang didekripsi kita memiliki kolom berikut (juga panjang datanya, jadi kita tahu di mana paddingnya, tapi itu tidak penting):

  • garam - int64
  • sesi_id - int64
  • pesan_id — int64
  • seq_no - int32

Izinkan kami mengingatkan Anda bahwa hanya ada satu garam untuk seluruh DC. Mengapa tahu tentang dia? Bukan hanya karena ada permintaan get_future_salts, yang memberi tahu Anda interval mana yang valid, tetapi juga karena jika garam Anda “busuk”, maka pesan (permintaan) akan hilang begitu saja. Server tentu saja akan melaporkan garam baru tersebut dengan mengeluarkannya new_session_created - tetapi dengan yang lama Anda harus mengirimnya ulang, misalnya. Dan masalah ini mempengaruhi arsitektur aplikasi.

Server diperbolehkan untuk menghentikan sesi sama sekali dan merespons dengan cara ini karena berbagai alasan. Sebenarnya apa itu sesi MTProto dari sisi klien? Ini adalah dua angka session_id и seq_no pesan dalam sesi ini. Ya, dan koneksi TCP yang mendasarinya, tentu saja. Katakanlah klien kita masih belum tahu bagaimana melakukan banyak hal, dia terputus dan terhubung kembali. Jika ini terjadi dengan cepat - sesi lama dilanjutkan di koneksi TCP baru, tingkatkan seq_no lebih jauh. Kalau lama, server bisa menghapusnya, karena di sisinya juga ada antrian, seperti yang kita tahu.

Apa yang seharusnya terjadi seq_no? Oh, itu pertanyaan yang rumit. Cobalah untuk memahami dengan jujur ​​apa yang dimaksud:

Pesan terkait Konten

Sebuah pesan yang membutuhkan pengakuan eksplisit. Ini mencakup semua pesan pengguna dan banyak layanan, hampir semuanya kecuali container dan ucapan terima kasih.

Nomor Urutan Pesan (msg_seqno)

Nomor 32-bit yang sama dengan dua kali jumlah pesan “yang berhubungan dengan konten” (yang memerlukan pengakuan, dan khususnya yang bukan merupakan wadah) yang dibuat oleh pengirim sebelum pesan ini dan selanjutnya bertambah satu jika pesan saat ini adalah pesan pesan terkait konten. Sebuah wadah selalu dibuat setelah seluruh isinya; oleh karena itu, nomor urutnya lebih besar atau sama dengan nomor urut pesan yang terkandung di dalamnya.

Sirkus macam apa ini dengan kenaikan 1, lalu kenaikan 2?.. Saya curiga awalnya yang mereka maksud adalah "bit paling tidak signifikan untuk ACK, sisanya adalah angka", tetapi hasilnya tidak persis sama - khususnya, keluar, dapat dikirim beberapa konfirmasi memiliki hal yang sama seq_no! Bagaimana? Misalnya, server mengirimi kami sesuatu, mengirimkannya, dan kami sendiri tetap diam, hanya merespons dengan pesan layanan yang mengonfirmasi penerimaan pesannya. Dalam hal ini, konfirmasi keluar kami akan memiliki nomor keluar yang sama. Jika Anda familiar dengan TCP dan berpikir ini terdengar liar, tapi sepertinya tidak terlalu liar, karena di TCP seq_no tidak berubah, tapi konfirmasi masuk seq_no di sisi lain, aku akan segera membuatmu kesal. Konfirmasi disediakan di MTProto TIDAK pada seq_no, seperti di TCP, tetapi oleh msg_id !

Apa ini msg_id, bidang yang paling penting? Pengidentifikasi pesan unik, seperti namanya. Ini didefinisikan sebagai angka 64-bit, bit terendah lagi-lagi memiliki keajaiban "server-bukan-server", dan sisanya adalah stempel waktu Unix, termasuk bagian pecahan, digeser 32 bit ke kiri. Itu. stempel waktu itu sendiri (dan pesan dengan waktu yang terlalu berbeda akan ditolak oleh server). Dari sini ternyata secara umum ini merupakan identifier yang bersifat global untuk client. Mengingat itu - mari kita ingat session_id - kami dijamin: Dalam situasi apa pun, pesan yang dimaksudkan untuk satu sesi tidak dapat dikirim ke sesi lain. Artinya, ternyata sudah ada tiga level - sesi, nomor sesi, id pesan. Mengapa begitu rumit, misteri ini sangat besar.

Dengan demikian, msg_id dibutuhkan untuk...

RPC: permintaan, tanggapan, kesalahan. Konfirmasi.

Seperti yang mungkin Anda ketahui, tidak ada jenis atau fungsi khusus "buat permintaan RPC" di mana pun dalam diagram, meskipun ada jawabannya. Bagaimanapun, kami memiliki pesan terkait konten! Itu adalah, apapun pesannya bisa berupa permintaan! Atau tidak menjadi. Lagipula, dari masing-masing ada msg_id. Tapi ada jawabannya:

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

Di sinilah ditunjukkan pesan mana yang ditanggapi. Oleh karena itu, di tingkat atas API, Anda harus mengingat berapa nomor permintaan Anda - menurut saya tidak perlu menjelaskan bahwa pekerjaannya tidak sinkron, dan mungkin ada beberapa permintaan yang sedang berjalan pada saat yang bersamaan, jawaban yang dapat dikembalikan dalam urutan apa pun? Pada prinsipnya, dari ini dan pesan kesalahan seperti tidak ada pekerja, arsitektur di balik ini dapat ditelusuri: server yang memelihara koneksi TCP dengan Anda adalah penyeimbang front-end, meneruskan permintaan ke backend dan mengumpulkannya kembali melalui message_id. Tampaknya semuanya jelas, logis, dan bagus di sini.

Ya?.. Dan jika Anda memikirkannya? Lagipula, respon RPC sendiri juga punya lapangan msg_id! Apakah kita perlu berteriak ke server “kamu tidak menjawab jawaban saya!”? Dan ya, ada apa dengan konfirmasi? Tentang halaman pesan tentang pesan memberitahu kita apa adanya

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

dan itu harus dilakukan oleh masing-masing pihak. Tapi tidak selalu! Jika Anda menerima RpcResult, itu sendiri berfungsi sebagai konfirmasi. Artinya, server dapat merespons permintaan Anda dengan MsgsAck - seperti, “Saya menerimanya.” RpcResult dapat merespons dengan segera. Bisa jadi keduanya.

Dan ya, Anda masih harus menjawab jawabannya! Konfirmasi. Jika tidak, server akan menganggapnya tidak terkirim dan mengirimkannya kembali kepada Anda. Bahkan setelah koneksi kembali. Namun di sini, tentu saja, muncul masalah timeout. Mari kita lihat nanti.

Sementara itu, mari kita lihat kemungkinan kesalahan eksekusi kueri.

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

Oh, seseorang akan berseru, ini format yang lebih manusiawi - ada garis! Tidak usah buru-buru. Di Sini daftar kesalahan, tapi tentu saja belum lengkap. Dari situ kita mengetahui apa itu kode sesuatu seperti Kesalahan HTTP (tentu saja, semantik tanggapan tidak dihormati, di beberapa tempat mereka didistribusikan secara acak di antara kode), dan garisnya terlihat seperti CAPITAL_LETTERS_AND_NUMBER. Misalnya, PHONE_NUMBER_OCCUPIED atau FILE_PART_Х_MISSING. Artinya, Anda masih membutuhkan baris ini menguraikan. Misalnya, FLOOD_WAIT_3600 berarti Anda harus menunggu satu jam, dan PHONE_MIGRATE_5, bahwa nomor telepon dengan awalan ini harus didaftarkan di DC ke-5. Kami memiliki bahasa tipe, bukan? Kita tidak perlu argumen dari string, argumen biasa sudah cukup, oke.

Sekali lagi, ini tidak ada di halaman pesan layanan, tetapi, seperti biasa pada proyek ini, informasinya dapat ditemukan di halaman dokumentasi lain. Atau menimbulkan kecurigaan. Pertama, lihat, pelanggaran pengetikan/lapisan - RpcError dapat disarangkan RpcResult. Kenapa tidak di luar? Apa yang tidak kami perhitungkan?.. Oleh karena itu, di mana jaminannya RpcError TIDAK boleh tertanam di dalamnya RpcResult, tapi langsung atau bersarang di tipe lain?.. Dan kalau tidak bisa, kenapa tidak di level atas, yaitu. telah hilang req_msg_id ? ..

Tapi mari kita lanjutkan tentang pesan layanan. Klien mungkin berpikir bahwa server berpikir lama dan mengajukan permintaan yang luar biasa ini:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Ada tiga kemungkinan jawaban terhadap pertanyaan ini, sekali lagi bersinggungan dengan mekanisme konfirmasi; mencoba memahami apa yang seharusnya (dan daftar umum jenis apa yang tidak memerlukan konfirmasi) diserahkan kepada pembaca sebagai pekerjaan rumah (catatan: informasi di kode sumber Telegram Desktop tidak lengkap).

Kecanduan narkoba: status pesan

Secara umum banyak tempat di TL, MTProto dan Telegram pada umumnya meninggalkan rasa keras kepala, namun karena kesopanan, kebijaksanaan dan lain-lain. soft skill Kami dengan sopan tetap bungkam tentang hal itu, dan menyensor kata-kata kotor dalam dialog. Namun, tempat iniОsebagian besar halamannya berisi tentang pesan tentang pesan Ini mengejutkan bahkan bagi saya, yang telah lama bekerja dengan protokol jaringan dan telah melihat sepeda dengan berbagai tingkat kebengkokan.

Ini dimulai dengan tidak berbahaya, dengan konfirmasi. Selanjutnya mereka menceritakannya kepada kita

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;

Ya, setiap orang yang mulai bekerja dengan MTProto harus menghadapinya; dalam siklus “diperbaiki - dikompilasi ulang - diluncurkan”, mendapatkan kesalahan angka atau garam yang menjadi buruk selama pengeditan adalah hal yang umum. Namun, ada dua poin di sini:

  1. Artinya pesan aslinya hilang. Kita perlu membuat beberapa antrian, kita akan melihatnya nanti.
  2. Berapa angka kesalahan aneh ini? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64...di mana nomor lainnya, Tommy?

Dokumentasi menyatakan:

Tujuannya adalah agar nilai error_code dikelompokkan (error_code >> 4): misalnya, kode 0x40 — 0x4f sesuai dengan kesalahan dalam dekomposisi container.

tapi, pertama, pergeseran ke arah lain, dan kedua, tidak masalah, di mana kode-kode lainnya? Di kepala penulis?.. Namun, ini sepele.

Kecanduan dimulai pada pesan tentang status pesan dan salinan pesan:

  • Permintaan Informasi Status Pesan
    Jika salah satu pihak belum menerima informasi mengenai status pesan keluarnya selama beberapa waktu, pihak tersebut dapat secara eksplisit memintanya dari pihak lainnya:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Pesan Informasi mengenai Status Pesan
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Di sini, info adalah string yang berisi tepat satu byte status pesan untuk setiap pesan dari daftar msg_ids yang masuk:

    • 1 = tidak ada yang diketahui tentang pesan tersebut (msg_id terlalu rendah, pihak lain mungkin lupa)
    • 2 = pesan tidak diterima (msg_id berada dalam rentang pengenal yang disimpan; namun pihak lain pasti belum menerima pesan seperti itu)
    • 3 = pesan tidak diterima (msg_id terlalu tinggi; namun pihak lain pasti belum menerimanya)
    • 4 = pesan diterima (perhatikan bahwa respons ini juga sekaligus merupakan tanda terima)
    • +8 = pesan sudah diterima
    • +16 = pesan tidak memerlukan pengakuan
    • +32 = Permintaan RPC yang terdapat dalam pesan sedang diproses atau pemrosesan sudah selesai
    • +64 = respons terkait konten terhadap pesan yang sudah dibuat
    • +128 = pihak lain mengetahui fakta bahwa pesan telah diterima
      Tanggapan ini tidak memerlukan pengakuan. Ini adalah pengakuan atas msgs_state_req yang relevan, dengan sendirinya.
      Perhatikan bahwa jika tiba-tiba pihak lain tidak menerima pesan yang sepertinya telah dikirim, pesan tersebut dapat dikirim ulang. Bahkan jika pihak lain menerima dua salinan pesan pada saat yang sama, duplikatnya akan diabaikan. (Jika terlalu banyak waktu berlalu, dan msg_id asli tidak valid lagi, pesan harus dibungkus dengan msg_copy).
  • Komunikasi Sukarela tentang Status Pesan
    Salah satu pihak dapat secara sukarela memberi tahu pihak lainnya tentang status pesan yang dikirimkan oleh pihak lainnya.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Komunikasi Sukarela yang Diperluas tentang Status Satu Pesan
    ...
    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;
  • Permintaan Eksplisit untuk Mengirim Ulang Pesan
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Pihak jarak jauh segera merespon dengan mengirimkan kembali pesan yang diminta […]
  • Permintaan Eksplisit untuk Mengirim Ulang Jawaban
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Pihak jarak jauh langsung merespon dengan mengirimkan ulang jawaban ke pesan yang diminta…
  • Salinan Pesan
    Dalam beberapa situasi, pesan lama dengan msg_id yang tidak valid lagi perlu dikirim ulang. Kemudian dibungkus dalam wadah fotokopi:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Setelah diterima, pesan diproses seolah-olah bungkusnya tidak ada. Namun jika diketahui secara pasti bahwa pesan orig_message.msg_id telah diterima, maka pesan baru tersebut tidak diproses (sementara pada saat yang sama, pesan tersebut dan orig_message.msg_id diakui). Nilai orig_message.msg_id harus lebih rendah dari msg_id kontainer.

Mari kita diam tentang apa msgs_state_info lagi-lagi telinga TL yang belum selesai mencuat (kami membutuhkan vektor byte, dan di dua bit terbawah ada enum, dan di dua bit lebih tinggi ada bendera). Intinya berbeda. Adakah yang mengerti mengapa semua ini dilakukan? dalam klien nyata perlu?.. Dengan susah payah, tetapi orang dapat membayangkan beberapa manfaat jika seseorang terlibat dalam debugging, dan dalam mode interaktif - tanyakan pada server apa dan bagaimana. Tapi di sini permintaannya dijelaskan perjalanan pulang pergi.

Oleh karena itu, masing-masing pihak tidak hanya harus mengenkripsi dan mengirim pesan, tetapi juga menyimpan data tentang diri mereka sendiri, tentang tanggapan terhadap mereka, untuk jangka waktu yang tidak diketahui. Dokumentasi tidak menjelaskan waktu atau penerapan praktis fitur-fitur ini. sama sekali tidak. Yang paling menakjubkan adalah mereka benar-benar digunakan dalam kode klien resmi! Rupanya mereka diberitahu sesuatu yang tidak termasuk dalam dokumentasi publik. Pahami dari kodenya kenapa, tidak lagi sesederhana dalam kasus TL - ini bukan bagian (yang relatif) terisolasi secara logis, tetapi bagian yang terkait dengan arsitektur aplikasi, yaitu. akan membutuhkan lebih banyak waktu secara signifikan untuk memahami kode aplikasi.

Ping dan pengaturan waktu. Antrian.

Dari semuanya, jika kita mengingat tebakan tentang arsitektur server (distribusi permintaan di seluruh backend), hal yang agak menyedihkan terjadi - terlepas dari semua jaminan pengiriman di TCP (baik data terkirim, atau Anda akan diberitahu tentang kesenjangannya, tetapi data akan dikirimkan sebelum masalah terjadi), itu konfirmasi di MTProto sendiri - tidak ada jaminan. Server dapat dengan mudah kehilangan atau membuang pesan Anda, dan tidak ada yang dapat dilakukan untuk mengatasinya, cukup gunakan berbagai jenis kruk.

Dan yang pertama adalah antrian pesan. Ya, dengan satu hal semuanya sudah jelas sejak awal - pesan yang belum dikonfirmasi harus disimpan dan dikirim ulang. Dan setelah jam berapa? Dan badut itu mengenalnya. Mungkin pesan layanan yang kecanduan itu entah bagaimana menyelesaikan masalah ini dengan kruk, katakanlah, di Telegram Desktop ada sekitar 4 antrian yang sesuai (mungkin lebih, seperti yang telah disebutkan, untuk ini Anda perlu mempelajari kode dan arsitekturnya dengan lebih serius; pada saat yang sama waktu, kami Kami tahu bahwa ini tidak dapat diambil sebagai sampel; sejumlah jenis skema MTProto tidak digunakan di dalamnya).

Mengapa ini terjadi? Mungkin, pemrogram server tidak dapat memastikan keandalan dalam cluster, atau bahkan buffering pada penyeimbang depan, dan memindahkan masalah ini ke klien. Karena putus asa, Vasily mencoba menerapkan opsi alternatif, dengan hanya dua antrian, menggunakan algoritma dari TCP - mengukur RTT ke server dan menyesuaikan ukuran "jendela" (dalam pesan) tergantung pada jumlah permintaan yang belum dikonfirmasi. Artinya, heuristik kasar untuk menilai beban server adalah berapa banyak permintaan kita yang dapat dikunyahnya pada saat yang sama dan tidak hilang.

Nah, itu kamu paham kan? Jika Anda harus mengimplementasikan TCP lagi di atas protokol yang berjalan di atas TCP, ini menunjukkan protokol yang dirancang dengan sangat buruk.

Oh ya, mengapa Anda memerlukan lebih dari satu antrian, dan apa artinya ini bagi seseorang yang bekerja dengan API tingkat tinggi? Begini, Anda membuat permintaan, membuat serial, tetapi sering kali Anda tidak dapat mengirimkannya dengan segera. Mengapa? Karena jawabannya adalah msg_id, yang bersifat sementaraаSaya adalah sebuah label, yang penugasannya sebaiknya ditunda hingga selambat-lambatnya - jika server menolaknya karena ketidaksesuaian waktu antara kita dan dia (tentu saja, kita dapat membuat penopang yang menggeser waktu kita dari sekarang ke server dengan menambahkan delta yang dihitung dari respons server - klien resmi melakukan ini, tetapi ini kasar dan tidak akurat karena buffering). Oleh karena itu, saat Anda membuat permintaan dengan panggilan fungsi lokal dari perpustakaan, pesan melewati tahapan berikut:

  1. Itu terletak dalam satu antrian dan menunggu enkripsi.
  2. Diangkat msg_id dan pesan masuk ke antrian lain - kemungkinan penerusan; kirim ke soket.
  3. a) Server merespons MsgsAck - pesan terkirim, kami menghapusnya dari "antrian lain".
    b) Atau sebaliknya, dia tidak menyukai sesuatu, dia menjawab badmsg - kirim ulang dari “antrian lain”
    c) Tidak ada yang diketahui, pesan perlu dikirim ulang dari antrian lain - tetapi tidak diketahui kapan tepatnya.
  4. Server akhirnya merespons RpcResult - respons sebenarnya (atau kesalahan) - tidak hanya dikirimkan, tetapi juga diproses.

Mungkin, penggunaan kontainer sebagian dapat memecahkan masalah tersebut. Ini terjadi ketika sekumpulan pesan dikemas menjadi satu, dan server merespons dengan konfirmasi ke semuanya sekaligus, dalam satu pesan. msg_id. Tapi dia juga akan menolak paket ini, jika terjadi kesalahan, secara keseluruhan.

Dan pada titik ini pertimbangan non-teknis ikut berperan. Dari pengalaman, kita telah melihat banyak kruk, dan sebagai tambahan, sekarang kita akan melihat lebih banyak contoh nasihat dan arsitektur yang buruk - dalam kondisi seperti itu, apakah layak untuk memercayai dan membuat keputusan seperti itu? Pertanyaannya retoris (tentu saja tidak).

Apa yang kita bicarakan? Kalau pada topik “pesan narkoba tentang pesan” Anda masih bisa berspekulasi dengan keberatan seperti “kamu bodoh, kamu tidak mengerti rencana brilian kami!” (jadi tulis dokumentasinya dulu, seperti yang seharusnya dilakukan orang normal, dengan alasan dan contoh pertukaran paket, lalu kita akan bicara), lalu timing/timeout adalah pertanyaan yang murni praktis dan spesifik, semua yang ada di sini sudah diketahui sejak lama. Apa yang dokumentasi katakan kepada kita tentang batas waktu?

Server biasanya mengakui penerimaan pesan dari klien (biasanya, permintaan RPC) menggunakan respons RPC. Jika respons datang dalam waktu lama, server mungkin terlebih dahulu mengirimkan pengakuan tanda terima, dan kemudian, respons RPC itu sendiri.

Klien biasanya mengakui penerimaan pesan dari server (biasanya, respons RPC) dengan menambahkan pengakuan ke permintaan RPC berikutnya jika pesan tersebut tidak dikirimkan terlambat (jika dihasilkan, katakanlah, 60-120 detik setelah penerimaan pesan dari server). Namun, jika untuk jangka waktu yang lama tidak ada alasan untuk mengirim pesan ke server atau jika ada sejumlah besar pesan yang tidak diakui dari server (katakanlah, lebih dari 16), klien mengirimkan pengakuan yang berdiri sendiri.

... Saya terjemahkan: kita sendiri tidak tahu seberapa banyak dan bagaimana kita membutuhkannya, jadi anggap saja biarlah seperti ini.

Dan tentang ping:

Pesan Ping (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Respons biasanya dikembalikan ke koneksi yang sama:

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

Pesan-pesan ini tidak memerlukan pengakuan. Pong ditransmisikan hanya sebagai respons terhadap ping sementara ping dapat dimulai oleh kedua sisi.

Penutupan Koneksi yang Ditunda + PING

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

Bekerja seperti ping. Selain itu, setelah ini diterima, server memulai pengatur waktu yang akan menutup koneksi saat ini diskon_penundaan beberapa detik kemudian kecuali server menerima pesan baru dengan jenis yang sama yang secara otomatis mengatur ulang semua pengatur waktu sebelumnya. Misalnya, jika klien mengirimkan ping ini setiap 60 detik sekali, klien mungkin menyetel putuskan_delay sama dengan 75 detik.

Kamu gila?! Dalam 60 detik, kereta akan memasuki stasiun, menurunkan dan mengambil penumpang, dan kembali kehilangan kontak di dalam terowongan. Dalam 120 detik, saat Anda mendengarnya, pesan itu akan tiba di pesan lain, dan kemungkinan besar koneksi akan terputus. Yah, jelas dari mana kaki itu berasal - “Saya mendengar dering, tapi tidak tahu di mana itu”, ada algoritma Nagl dan opsi TCP_NODELAY, yang dirancang untuk pekerjaan interaktif. Tapi, permisi, pertahankan nilai defaultnya - 200 milidetik Jika Anda benar-benar ingin menggambarkan sesuatu yang serupa dan menghemat beberapa paket, tunda selama 5 detik, atau berapa pun batas waktu pesan "Pengguna sedang mengetik..." sekarang. Tapi tidak lagi.

Dan akhirnya, ping. Artinya, memeriksa keaktifan koneksi TCP. Ini lucu, tetapi sekitar 10 tahun yang lalu saya menulis teks kritis tentang utusan asrama fakultas kami - penulis di sana juga melakukan ping ke server dari klien, dan bukan sebaliknya. Tapi mahasiswa tahun ke-3 adalah satu hal, dan kantor internasional adalah hal lain, bukan?..

Pertama, sedikit program pendidikan. Koneksi TCP, jika tidak ada pertukaran paket, dapat bertahan selama berminggu-minggu. Ini baik dan buruk, tergantung tujuannya. Ada baiknya jika Anda membuka koneksi SSH ke server, Anda bangun dari komputer, me-reboot router, kembali ke tempat Anda - sesi melalui server ini tidak terputus (Anda tidak mengetik apa pun, tidak ada paket) , itu nyaman. Sangat buruk jika ada ribuan klien di server, masing-masing menggunakan sumber daya (halo, Postgres!), dan host klien mungkin sudah lama di-boot ulang - tetapi kita tidak akan mengetahuinya.

Sistem obrolan/IM termasuk dalam kasus kedua karena satu alasan tambahan – status online. Jika pengguna “jatuh”, Anda perlu memberi tahu lawan bicaranya tentang hal ini. Jika tidak, Anda akan mendapatkan kesalahan yang dibuat oleh pembuat Jabber (dan diperbaiki selama 20 tahun) - pengguna telah terputus, tetapi mereka terus menulis pesan kepadanya, percaya bahwa dia sedang online (yang juga benar-benar hilang dalam hal ini beberapa menit sebelum pemutusan hubungan ditemukan). Tidak, opsi TCP_KEEPALIVE, yang dimasukkan secara acak oleh banyak orang yang tidak memahami cara kerja pengatur waktu TCP (dengan menyetel nilai liar seperti puluhan detik), tidak akan membantu di sini - Anda perlu memastikan bahwa tidak hanya kernel OS mesin pengguna masih hidup, tetapi juga berfungsi normal, mampu merespons, dan aplikasi itu sendiri (apakah menurut Anda aplikasi itu tidak dapat dibekukan? Telegram Desktop di Ubuntu 18.04 membeku untuk saya lebih dari sekali).

Itu sebabnya Anda harus melakukan ping server klien, dan bukan sebaliknya - jika klien melakukan ini, jika koneksi terputus, ping tidak akan terkirim, tujuannya tidak akan tercapai.

Apa yang kita lihat di Telegram? Justru sebaliknya! Ya, itu. Secara formal, tentu saja, kedua belah pihak bisa saling melakukan ping. Dalam praktiknya, klien menggunakan kruk ping_delay_disconnect, yang menyetel pengatur waktu di server. Maaf, klien tidak berhak memutuskan berapa lama dia ingin tinggal di sana tanpa ping. Server, berdasarkan bebannya, lebih tahu. Namun, tentu saja, jika Anda tidak mempermasalahkan sumber dayanya, maka Anda akan menjadi Pinokio jahat Anda sendiri, dan tongkat penyangga sudah cukup...

Bagaimana seharusnya hal itu dirancang?

Saya yakin fakta di atas dengan jelas menunjukkan bahwa tim Telegram/VKontakte tidak terlalu kompeten di bidang jaringan komputer tingkat transportasi (dan lebih rendah) dan kualifikasi mereka yang rendah dalam hal-hal yang relevan.

Mengapa hal ini menjadi begitu rumit, dan bagaimana arsitek Telegram dapat menolaknya? Fakta bahwa mereka mencoba membuat sesi yang bertahan dari putusnya koneksi TCP, yaitu apa yang tidak terkirim sekarang, akan kami sampaikan nanti. Mereka mungkin juga mencoba membuat transportasi UDP, tetapi mereka menemui kesulitan dan meninggalkannya (itulah sebabnya dokumentasinya kosong - tidak ada yang bisa dibanggakan). Namun karena kurangnya pemahaman tentang cara kerja jaringan secara umum dan TCP pada khususnya, di mana Anda dapat mengandalkannya, dan di mana Anda perlu melakukannya sendiri (dan bagaimana caranya), serta upaya untuk menggabungkannya dengan kriptografi “dua burung dengan satu batu”, inilah hasilnya.

Bagaimana hal itu perlu? Berdasarkan fakta itu msg_id adalah stempel waktu yang diperlukan dari sudut pandang kriptografi untuk mencegah serangan replay, merupakan kesalahan jika melampirkan fungsi pengidentifikasi unik ke dalamnya. Oleh karena itu, tanpa mengubah arsitektur saat ini secara mendasar (saat aliran Pembaruan dibuat, itu adalah topik API tingkat tinggi untuk bagian lain dari rangkaian postingan ini), seseorang perlu:

  1. Server yang memegang koneksi TCP ke klien bertanggung jawab - jika sudah membaca dari soket, harap akui, proses atau kembalikan kesalahan, tidak ada kerugian. Maka konfirmasinya bukanlah vektor id, tetapi hanya “seq_no yang terakhir diterima” - hanya sebuah angka, seperti dalam TCP (dua angka - seq Anda dan yang dikonfirmasi). Kita selalu berada dalam sesi tersebut, bukan?
  2. Stempel waktu untuk mencegah serangan replay menjadi kolom terpisah, ala nonce. Itu sudah dicentang, tetapi tidak mempengaruhi apa pun. Cukup dan uint32 - jika garam kita berubah setidaknya setiap setengah hari, kita dapat mengalokasikan 16 bit ke bit tingkat rendah dari bilangan bulat waktu saat ini, sisanya - ke pecahan detik (seperti sekarang).
  3. DIHAPUS msg_id sama sekali - dari sudut pandang membedakan permintaan di backend, pertama, ada id klien, dan kedua, id sesi, gabungkan. Oleh karena itu, hanya satu hal yang cukup sebagai pengidentifikasi permintaan seq_no.

Ini juga bukan opsi yang paling sukses; acak lengkap bisa berfungsi sebagai pengidentifikasi - ini sudah dilakukan di API tingkat tinggi saat mengirim pesan. Akan lebih baik jika merombak total arsitektur dari relatif menjadi absolut, tetapi ini adalah topik untuk bagian lain, bukan postingan ini.

API?

Ta-daam! Jadi, setelah berjuang melalui jalan yang penuh dengan rasa sakit dan penopang, kami akhirnya dapat mengirim permintaan apa pun ke server dan menerima jawaban apa pun, serta menerima pembaruan dari server (bukan sebagai tanggapan terhadap permintaan, tetapi server itu sendiri). mengirimkan kami, seperti PUSH, jika ada yang lebih jelas seperti itu).

Perhatian, sekarang akan ada satu-satunya contoh di Perl di artikel! (bagi mereka yang belum familiar dengan sintaksisnya, argumen pertama dari berkah adalah struktur data objek, yang kedua adalah kelasnya):

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

Ya, bukan spoiler yang sengaja dibuat - jika Anda belum membacanya, silakan lakukan!

Oh, tunggu~~... seperti apa ini? Sesuatu yang sangat familiar... mungkin ini adalah struktur data API Web khas di JSON, kecuali kelasnya juga dilampirkan ke objek?..

Jadi begini ternyata... Ada apa kawan?.. Begitu banyak usaha - dan kami berhenti untuk beristirahat di tempat para pemrogram Web baru saja dimulai?..Bukankah JSON melalui HTTPS lebih sederhana?! Apa yang kami dapatkan sebagai imbalannya? Apakah upaya tersebut sepadan?

Mari kita evaluasi apa yang TL+MTProto berikan kepada kita dan alternatif apa yang mungkin. Ya, HTTP, yang berfokus pada model permintaan-respons, tidak cocok, tapi setidaknya ada sesuatu di atas TLS?

Serialisasi ringkas. Melihat struktur data ini, mirip dengan JSON, saya ingat ada versi binernya. Mari kita tandai MsgPack sebagai tidak cukup dapat diperluas, tetapi ada, misalnya, CBOR - omong-omong, standar yang dijelaskan dalam RFC 7049. Hal ini penting karena mendefinisikannya tag, sebagai mekanisme ekspansi, dan di antaranya sudah terstandarisasi Ada:

  • 25 + 256 - mengganti baris berulang dengan referensi ke nomor baris, metode kompresi yang murah
  • 26 - objek Perl berseri dengan nama kelas dan argumen konstruktor
  • 27 - objek serial yang tidak bergantung pada bahasa dengan nama tipe dan argumen konstruktor

Ya, saya mencoba membuat serial data yang sama di TL dan CBOR dengan pengepakan string dan objek diaktifkan. Hasilnya mulai bervariasi mendukung CBOR mulai dari satu megabita:

cborlen=1039673 tl_len=1095092

Dengan demikian, kesimpulan: Ada format yang jauh lebih sederhana yang tidak mengalami masalah kegagalan sinkronisasi atau pengidentifikasi yang tidak diketahui, dengan efisiensi yang sebanding.

Pembentukan koneksi cepat. Ini berarti nol RTT setelah penyambungan ulang (ketika kunci telah dibuat satu kali) - berlaku dari pesan MTProto pertama, tetapi dengan beberapa syarat - mendapatkan masalah yang sama, sesi tidak busuk, dll. Apa yang ditawarkan TLS kepada kita? Kutipan pada topik:

Saat menggunakan PFS di TLS, tiket sesi TLS (RFC 5077) untuk melanjutkan sesi terenkripsi tanpa menegosiasikan ulang kunci dan tanpa menyimpan informasi penting di server. Saat membuka koneksi pertama dan membuat kunci, server mengenkripsi status koneksi dan mengirimkannya ke klien (dalam bentuk tiket sesi). Oleh karena itu, ketika koneksi dilanjutkan, klien mengirimkan tiket sesi, termasuk kunci sesi, kembali ke server. Tiket itu sendiri dienkripsi dengan kunci sementara (kunci tiket sesi), yang disimpan di server dan harus didistribusikan di antara semua server frontend yang memproses SSL dalam solusi berkerumun.[10]. Jadi, pengenalan tiket sesi mungkin melanggar PFS jika kunci server sementara disusupi, misalnya, ketika disimpan untuk waktu yang lama (OpenSSL, nginx, Apache menyimpannya secara default selama seluruh durasi program; situs populer menggunakan kunci selama beberapa jam, hingga berhari-hari).

Di sini RTTnya bukan nol, Anda perlu menukar setidaknya ClientHello dan ServerHello, setelah itu klien dapat mengirim data bersama dengan Selesai. Namun di sini kita harus ingat bahwa kita tidak memiliki Web, dengan sekumpulan koneksi yang baru dibuka, tetapi sebuah messenger, yang koneksinya sering kali berumur panjang, permintaan yang relatif singkat ke halaman Web - semuanya dimultipleks secara internal. Artinya, cukup bisa diterima jika kita tidak menemukan bagian kereta bawah tanah yang sangat buruk.

Lupa hal lain? Tulis di komentar.

Bersambung!

Di bagian kedua dari rangkaian postingan ini kami tidak akan mempertimbangkan masalah teknis, tetapi masalah organisasi - pendekatan, ideologi, antarmuka, sikap terhadap pengguna, dll. Namun berdasarkan informasi teknis yang disajikan di sini.

Bagian ketiga akan melanjutkan analisis komponen teknis/pengalaman pengembangan. Anda akan belajar, khususnya:

  • kelanjutan dari kekacauan dengan beragamnya tipe TL
  • hal yang tidak diketahui tentang saluran dan supergrup
  • mengapa dialog lebih buruk daripada daftar
  • tentang pengalamatan pesan absolut vs relatif
  • apa perbedaan antara foto dan gambar
  • bagaimana emoji mengganggu teks miring

dan kruk lainnya! Pantau terus!

Sumber: www.habr.com

Tambah komentar