Telegram'ın protokol ve organizasyonel yaklaşımlarına yönelik eleştiri. Bölüm 1, teknik: sıfırdan müşteri yazma deneyimi - TL, MT

Son zamanlarda Habré'de Telegram'ın ne kadar iyi olduğu, Durov kardeşlerin ağ sistemleri kurma konusunda ne kadar parlak ve deneyimli olduğu vb. konulardaki paylaşımlar daha sık yer almaya başladı. Aynı zamanda, çok az kişi kendisini teknik cihaza gerçekten kaptırmıştır - en fazla, JSON'a dayalı oldukça basit (ve MTProto'dan oldukça farklı) bir Bot API'si kullanırlar ve genellikle sadece kabul ederler. inanç üzerine elçinin etrafında dönen tüm övgüler ve halkla ilişkiler. Neredeyse bir buçuk yıl önce, Eshelon STK'sındaki meslektaşım Vasily (maalesef, Habré hakkındaki hesabı taslakla birlikte silindi) kendi Telegram istemcisini Perl'de sıfırdan yazmaya başladı ve daha sonra bu satırların yazarı da katıldı. Neden Perl, bazıları hemen soracak? Çünkü başka dillerde de bu tür projeler zaten var, aslında mesele bu değil, bunun olmadığı başka bir dil de olabilir. hazır kütüphaneve buna göre yazar sonuna kadar gitmelidir sıfırdan. Üstelik kriptografi bir güven meselesidir ama doğrulayın. Güvenliği amaçlayan bir ürünle, üreticinin hazır kitaplığına güvenip körü körüne güvenemezsiniz (ancak bu ikinci bölümün konusudur). Şu anda kütüphane "ortalama" düzeyde oldukça iyi çalışıyor (herhangi bir API isteğinde bulunmanıza izin veriyor).

Ancak bu yazı dizisinde çok fazla kriptografi veya matematik olmayacak. Ancak başka birçok teknik detay ve mimari destek de olacak (sıfırdan yazmayacak, kütüphaneyi herhangi bir dilde kullanacak olanlar için de faydalı). Yani asıl amaç müşteriyi sıfırdan uygulamaya çalışmaktı resmi belgelere göre. Yani resmi müşterilerin kaynak kodlarının kapalı olduğunu varsayalım (yine ikinci bölümde bunun doğru olup olmadığı konusunu daha detaylı ele alacağız) orada yani), ancak eski günlerde olduğu gibi, örneğin RFC gibi bir standart var - resmi olsun, kaynak koduna "bakmadan" yalnızca spesifikasyona göre bir istemci yazmak mümkün mü (Telegram Masaüstü, mobil) veya resmi olmayan Telethon?

İçindekiler:

Belgeler... var, değil mi? Bu doğru mu?..

Bu yazıya ait notlardan parçalar geçen yaz toplanmaya başlandı. Bunca zamandır resmi web sitesinde https://core.telegram.org Belgeler Katman 23'ten itibarendi, yani. 2014'te bir yere sıkışıp kaldık (hatırlıyor musun, o zamanlar kanallar bile yoktu?). Elbette teorik olarak bu, 2014'te o dönemde işlevselliğe sahip bir istemciyi hayata geçirmemize izin vermeliydi. Ancak bu durumda bile belgeler öncelikle eksikti ve ikinci olarak bazı yerlerde kendisiyle çelişiyordu. Bir ay kadar önce, Eylül 2019'da, kazara Tamamen yeni Katman 105 için sitedeki belgelerde büyük bir güncellemenin olduğu ve artık her şeyin yeniden okunması gerektiğine dair bir notun olduğu keşfedildi. Aslında birçok makale revize edildi, ancak çoğu değişmeden kaldı. Bu nedenle, belgelerle ilgili aşağıdaki eleştirileri okurken, bunlardan bazılarının artık geçerli olmadığını, ancak bazılarının hala geçerli olduğunu aklınızda bulundurmalısınız. Sonuçta modern dünyada 5 yıl sadece uzun bir süre değil, aynı zamanda çok birçok. O zamandan beri (özellikle o zamandan bu yana atılan ve yeniden canlanan geochat sitelerini hesaba katmazsanız), şemadaki API yöntemlerinin sayısı yüzden iki yüz ellinin üzerine çıktı!

Genç bir yazar olarak nereden başlamalı?

Sıfırdan yazmanız veya örneğin hazır kütüphaneleri kullanmanız önemli değil Python için Teleton veya PHP için Madeline, her durumda, önce ihtiyacınız olacak başvurunuzu kaydedin - parametreleri al api_id и api_hash (VKontakte API ile çalışanlar hemen anlayacaktır) sunucunun uygulamayı hangi yöntemle tanımlayacağını. Bu sahip bunu yasal nedenlerden dolayı yapın, ancak kütüphane yazarlarının bunu neden yayınlayamadıklarını ikinci bölümde daha ayrıntılı olarak konuşacağız. Çok sınırlı olmasına rağmen test değerlerinden memnun olabilirsiniz - gerçek şu ki artık kayıt olabilirsiniz sadece bir bu yüzden acele etmeyin.

Şimdi, teknik açıdan bakıldığında, kayıt olduktan sonra Telegram'dan dokümantasyon, protokol vb. güncellemeler hakkında bildirimler almamız gerektiği gerçeğiyle ilgilenmeliyiz. Yani, rıhtımın bulunduğu sitenin basitçe terk edildiği ve özellikle müşteri yapmaya başlayanlarla çalışmaya devam ettiği varsayılabilir, çünkü daha kolay. Ama hayır öyle bir şey görülmedi, herhangi bir bilgi gelmedi.

Ve sıfırdan yazarsanız, elde edilen parametreleri kullanmak aslında hala çok uzaktır. Rağmen https://core.telegram.org/ ve Başlarken'de bunlardan bahsediyor, aslında ilk önce uygulamanız gerekecek MTProto protokolü - ama eğer inanırsan OSI modeline göre düzen protokolün genel bir açıklaması için sayfanın sonunda, o zaman tamamen boşuna.

Aslında, MTProto'nun hem öncesinde hem de sonrasında, aynı anda birkaç düzeyde (İşletim Sistemi çekirdeğinde çalışan yabancı ağcıların dediği gibi, katman ihlali), büyük, acı verici ve korkunç bir konu önümüze çıkacak...

İkili serileştirme: TL (Tip Dili) ve şeması, katmanları ve diğer birçok korkutucu kelime

Bu konu aslında Telegram’ın sorunlarının anahtarıdır. Ve eğer derinlemesine araştırmaya çalışırsanız, pek çok korkunç söz olacak.

İşte diyagram. Bu kelime aklınıza gelirse şunu söyleyin: JSON Şeması, Doğru düşündün. Amaç aynı: iletilen olası veri kümesini tanımlayacak bir dil. Benzerliklerin bittiği yer burasıdır. Eğer sayfadan MTProto protokolü, veya resmi müşterinin kaynak ağacından bir şema açmaya çalışacağız, şöyle bir şey göreceğiz:

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;

Bunu ilk kez gören bir kişi, sezgisel olarak yazılanların yalnızca bir kısmını tanıyabilecektir - bunlar görünüşe göre yapılardır (her ne kadar ad nerede, solda mı sağda mı?), içlerinde alanlar var, bundan sonra iki nokta üst üste işaretinden sonra bir tür gelir... muhtemelen. Burada köşeli parantezlerin içinde muhtemelen C++'daki gibi şablonlar vardır (aslında, gerçekten). Ve tüm diğer semboller ne anlama geliyor, soru işaretleri, ünlem işaretleri, yüzdeler, karma işaretleri (ve tabii ki farklı yerlerde farklı anlamlara geliyorlar), bazen var bazen yok, onaltılık sayılar ve en önemlisi bundan nasıl yararlanılacağı doğru (hangisi sunucu tarafından reddedilmeyecektir) bayt akışı? Belgeleri okumanız gerekecek (evet, yakında JSON sürümünde şemaya bağlantılar var - ancak bu onu daha net hale getirmiyor).

Sayfayı aç İkili Veri Serileştirme ve 4. yılda matan'a benzer bir şey olan mantarların ve ayrık matematiğin büyülü dünyasına dalın. Alfabe, tür, değer, birleştirici, işlevsel birleştirici, normal biçim, bileşik tür, polimorfik tür... ve bunların hepsi sadece ilk sayfa! Sonrası seni bekliyor TL Dili, bu, zaten önemsiz bir istek ve yanıtın bir örneğini içermesine rağmen, daha tipik durumlara hiçbir şekilde bir yanıt sağlamaz; bu, başka bir sekiz yerleşik olarak Rusça'dan İngilizceye çevrilmiş matematiğin yeniden anlatılmasından geçmeniz gerektiği anlamına gelir. sayfalar!

İşlevsel dillere ve otomatik tür çıkarımına aşina olan okuyucular elbette bu dildeki açıklama dilini örnekten bile çok daha tanıdık görecek ve bunun aslında prensip olarak fena olmadığını söyleyebilirler. Buna itirazlar şunlardır:

  • evet, hedef Kulağa hoş geliyor ama ne yazık ki o ulaşılamadı
  • Rus üniversitelerinde eğitim, BT uzmanlıkları arasında bile farklılık göstermektedir - herkes ilgili dersi almamıştır
  • Son olarak göreceğimiz gibi pratikte gerekli değil, açıklanan TL'nin bile yalnızca sınırlı bir alt kümesi kullanıldığı için

Bir deyişle LeoNerd kanalda #perl Telegram'dan Matrix'e bir kapı uygulamaya çalışan FreeNode IRC ağında (alıntının çevirisi bellekten hatalı):

Sanki birisi ilk kez yazı tipi teorisiyle tanışmış, heyecanlanmış ve pratikte gerekli olup olmadığını pek umursamadan onunla oynamaya başlamış gibi geliyor.

Temel bir şey olarak çıplak türlere (int, long vb.) ihtiyaç duyulup sorulmadığını kendiniz görün - sonuçta bunların manuel olarak uygulanması gerekir - örneğin, onlardan türetmeye çalışalım. vektör. Yani aslında dizi, eğer ortaya çıkan şeyleri özel isimleriyle çağırırsanız.

Ama önce

Resmi belgeleri okumayanlar için TL sözdiziminin bir alt kümesinin kısa açıklaması

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;

Tanım her zaman başlar tasarımcı, ardından isteğe bağlı olarak (pratikte - her zaman) sembolü aracılığıyla # gerektiği CRC32 bu türün normalleştirilmiş açıklama dizesinden. Daha sonra alanların açıklaması gelir; eğer mevcutsa tür boş olabilir. Bunların hepsi eşittir işaretiyle, bu kurucunun - yani alt türün - ait olduğu türün adı ile biter. Eşittir işaretinin sağındaki adam polimorfik - yani, birkaç spesifik tür buna karşılık gelebilir.

Tanım satırdan sonra gelirse ---functions---sözdizimi aynı kalacak, ancak anlamı farklı olacaktır: yapıcı RPC işlevinin adı olacak, alanlar parametreler haline gelecektir (yani, aşağıda açıklandığı gibi verilen yapıyla tamamen aynı kalacaktır) , bu yalnızca atanan anlam olacaktır) ve "polimorfik tür " - döndürülen sonucun türü. Doğru, yine de polimorfik kalacaktır - sadece bölümde tanımlanmıştır ---types---, ancak bu kurucu "dikkate alınmayacaktır". Çağrılan işlev türlerinin argümanlarına göre aşırı yüklenmesi, ör. Bazı nedenlerden dolayı, C++'da olduğu gibi aynı ada sahip ancak farklı imzalara sahip birçok işlev TL'de sağlanmamaktadır.

OOP değilse neden "yapıcı" ve "polimorfik"? Aslında birisinin bunu OOP terimleriyle düşünmesi daha kolay olacaktır - soyut bir sınıf olarak polimorfik bir tür ve yapıcılar onun doğrudan soyundan gelen sınıflardır ve final birçok dilin terminolojisinde. Aslında elbette sadece burada benzerlik OO programlama dillerinde gerçek aşırı yüklenmiş yapıcı yöntemleriyle. Burada sadece veri yapıları olduğundan, hiçbir yöntem yoktur (her ne kadar işlevlerin ve yöntemlerin açıklaması kafada onların var olduğu konusunda kafa karışıklığı yaratma kapasitesine sahip olsa da, bu farklı bir konudur) - bir kurucuyu aşağıdaki değerden bir değer olarak düşünebilirsiniz: Hangi inşa ediliyor bir bayt akışını okurken yazın.

Bu nasıl oluyor? Her zaman 4 bayt okuyan seri durumdan çıkarıcı değeri görür 0xcrc32 - ve bundan sonra ne olacağını anlıyor field1 tip ile intyani tam olarak 4 bayt okur, bunun üzerinde türün bulunduğu alan PolymorType Okumak. Görür 0x2crc32 ve daha ileride iki alan olduğunu anlıyor, birincisi longyani 8 bayt okuyoruz. Ve sonra yine aynı şekilde seri durumdan çıkarılan karmaşık bir tür. Örneğin, Type3 Sırasıyla iki kurucu en kısa sürede devrede ilan edilebilirse, o zaman ikisinden birini karşılamaları gerekir. 0x12abcd34, bundan sonra 4 bayt daha okumalısınız intVeya 0x6789cdef, bundan sonra hiçbir şey olmayacak. Başka bir şey varsa - bir istisna atmanız gerekir. Neyse, bundan sonra 4 bayt okumaya geri dönüyoruz int поля field_c в constructorTwo ve bununla birlikte okumayı bitiriyoruz PolymorType.

Sonunda yakalanırsan 0xdeadcrc için constructorThree, o zaman her şey daha karmaşık hale gelir. İlk alanımız bit_flags_of_what_really_present tip ile # - aslında bu yalnızca türün takma adıdır nat, "doğal sayı" anlamına gelir. Yani aslında imzasız int, gerçek devrelerde imzasız sayıların oluştuğu tek durumdur. Yani, bir sonraki adım soru işaretli bir yapıdır; bu, bu alanın - yalnızca karşılık gelen bitin belirtilen alanda (yaklaşık olarak üçlü bir operatör gibi) ayarlanması durumunda tel üzerinde mevcut olacağı anlamına gelir. Bu bitin ayarlandığını varsayalım, bu da şuna benzer bir alanı okumamız gerektiği anlamına gelir: TypeÖrneğimizde 2 yapıcı var. Biri boş (yalnızca tanımlayıcıdan oluşur), diğerinde bir alan var ids tip ile ids:Vector<long>.

Hem şablonların hem de jeneriklerin profesyonellerde veya Java'da olduğunu düşünebilirsiniz. Ama hayır. Neredeyse. Bu sadece Gerçek devrelerde açılı ayraçların kullanılması durumunda SADECE Vektör için kullanılır. Bayt akışında bunlar, Vector türünün kendisi için 4 CRC32 bayt olacaktır, her zaman aynı, ardından 4 bayt - dizi öğelerinin sayısı ve ardından bu öğelerin kendisi.

Buna serileştirmenin her zaman 4 baytlık sözcüklerle gerçekleştiği gerçeğini de ekleyin, tüm türler bunun katlarıdır - yerleşik türler de açıklanmıştır bytes и string uzunluğun manuel olarak serileştirilmesi ve bu 4'e göre hizalama - peki, kulağa normal ve hatta nispeten etkili görünüyor mu? Her ne kadar TL'nin etkili bir ikili serileştirme olduğu iddia edilse de, hemen hemen her şeyin, hatta Boolean değerlerinin ve tek karakterli dizelerin 4 bayta genişletilmesiyle, JSON hala çok daha kalın olacak mı? Bakın, gereksiz alanlar bile bit bayraklarıyla atlanabilir, her şey oldukça iyidir ve hatta gelecek için genişletilebilir, o zaman neden yapıcıya daha sonra yeni isteğe bağlı alanlar eklemiyorsunuz?..

Ancak hayır, eğer kısa açıklamamı değil, tüm belgeleri okursanız ve uygulamayı düşünürseniz. İlk olarak, yapıcının CRC32'si, şemanın metin açıklamasının normalleştirilmiş satırına göre hesaplanır (fazladan boşlukları kaldırın, vb.) - yani yeni bir alan eklenirse, tür açıklama satırı değişecek ve dolayısıyla CRC32 ve , dolayısıyla serileştirme. Peki eski müşteri, yeni bayrakların yerleştirildiği bir alan alırsa ve onlarla daha sonra ne yapacağını bilmiyorsa ne yapar?..

İkincisi şunu hatırlayalım CRC32burada esas olarak şu şekilde kullanılır: karma işlevleri hangi türün seri hale getirilip kaldırıldığını benzersiz bir şekilde belirlemek için. Burada çarpışma sorunuyla karşı karşıyayız - ve hayır, olasılık 232'de bir değil, çok daha fazla. CRC32'nin iletişim kanalındaki hataları tespit etmek (ve düzeltmek) ve buna göre bu özellikleri başkalarının zararına geliştirmek üzere tasarlandığını kim hatırladı? Örneğin, baytların yeniden düzenlenmesi umrunda değil: CRC32'yi iki satırdan hesaplarsanız, ikincisinde ilk 4 baytı sonraki 4 baytla değiştirirsiniz - aynı olacaktır. Girdimiz Latin alfabesinden metin dizeleri (ve biraz noktalama işareti) olduğunda ve bu adlar özellikle rastgele olmadığında, böyle bir yeniden düzenlemenin olasılığı büyük ölçüde artar.

Bu arada, orada ne olduğunu kim kontrol etti? gerçekten CRC32 mi? İlk kaynak kodlarından birinde (Waltman'dan bile önce), her karakteri 239 sayısıyla çarpan bir karma işlevi vardı; bu insanlar tarafından çok sevilirdi, ha ha!

Sonunda, alan tipine sahip kurucuların Vector<int> и Vector<PolymorType> farklı CRC32'ye sahip olacaktır. Çevrimiçi performans ne durumda? Ve teorik açıdan bakıldığında, bu türün bir parçası mı oluyor? Diyelim ki on bin sayıdan oluşan bir diziyi geçtik. Vector<int> her şey açık, uzunluk ve 40000 bayt daha. Peki ya bu Vector<Type2>tek bir alandan oluşan int ve türünde tek başına - 10000xabcdef0'ü 34 kez ve ardından 4 bayt tekrarlamamız gerekiyor mu? intveya dil onu bizim için yapıcıdan BAĞIMSIZ hale getirebilir fixedVec ve 80000 bayt yerine yalnızca 40000 bayt mı aktarılsın?

Bu hiç de boş bir teorik soru değil - her birinin bir kimliği, adı, soyadı olan grup kullanıcılarının bir listesini aldığınızı hayal edin - mobil bağlantı üzerinden aktarılan veri miktarındaki fark önemli olabilir. Bize duyurulan tam olarak Telegram serileştirmesinin etkinliğidir.

Yani ...

Hiçbir zaman yayınlanmayan vektör

Birleştiricilerin ve benzerlerinin açıklamalarını içeren sayfalar arasında gezinmeye çalışırsanız, bir vektörün (ve hatta bir matrisin) resmi olarak birkaç sayfadan oluşan demetler aracılığıyla çıktılanmaya çalıştığını göreceksiniz. Ancak sonunda unuturlar, son adım atlanır ve henüz bir türe bağlı olmayan bir vektörün tanımı basitçe verilir. Sorun ne? Dillerde programlama, özellikle işlevsel olanları, yapıyı yinelemeli olarak tanımlamak oldukça tipiktir - derleyici, tembel değerlendirmesiyle her şeyi kendisi anlayacak ve yapacaktır. dilde veri serileştirme ihtiyaç duyulan şey VERİMLİLİKtir: basitçe açıklamak yeterlidir listeyani iki öğenin yapısı - birincisi bir veri öğesi, ikincisi aynı yapının kendisi veya kuyruk için boş bir alan (paket) (cons) Lisp'te). Ama bu açıkça gerektirecek her öğesi, türünü tanımlamak için ek 4 bayt (TL durumunda CRC32) harcar. Bir dizi aynı zamanda kolaylıkla tanımlanabilir sabit boyut, ancak önceden uzunluğu bilinmeyen bir dizi olması durumunda kopuyoruz.

Bu nedenle TL bir vektörün çıktısına izin vermediğinden yandan eklenmesi gerekiyordu. Sonuçta belgeler şunu söylüyor:

Serileştirme her zaman aynı yapıcı "vektör"ü kullanır (const 0x1cb5c415 = crc32("vector t:Type # [ t ] = Vector t") ve bu, t tipindeki değişkenin spesifik değerine bağlı değildir.

İsteğe bağlı parametre t'nin değeri, sonuç türünden türetildiği için serileştirmeye dahil değildir (her zaman seri durumdan çıkarmadan önce bilinir).

Daha yakından bak: vector {t:Type} # [ t ] = Vector t - ancak hiçbir yerde Bu tanımın kendisi, ilk sayının vektörün uzunluğuna eşit olması gerektiğini söylemez! Ve hiçbir yerden gelmiyor. Bu akılda tutulması ve elle uygulanması gereken bir veridir. Başka yerlerde, belgeler dürüstçe türün gerçek olmadığını belirtiyor:

Vector t polimorfik psödotipi, değeri kutulu veya çıplak herhangi bir t tipindeki değer dizisi olan bir "tiptir".

... ama ona odaklanmıyor. Matematiğin esnetilmesinden yorulduğunuzda (hatta belki bir üniversite dersinden biliyorsunuzdur), pes etmeye karar verdiğinizde ve onunla pratikte nasıl çalışacağınıza gerçekten baktığınızda, kafanızda bunun ciddi bir şey olduğu izlenimi oluşur. Matematiğin özünde, herhangi biri tarafından değil, Cool People (iki matematikçi - ACM kazananı) tarafından icat edildiği açıktır. Gösteriş yapma hedefine ulaşıldı.

Bu arada, sayı hakkında. şunu hatırlatalım # eşanlamlı nat, doğal sayı:

Tür ifadeleri var (tür-ifadesi) ve sayısal ifadeler (doğal ifade). Ancak aynı şekilde tanımlanırlar.

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

ancak gramerde aynı şekilde tanımlanırlar, yani. Bu farkın bir kez daha hatırlanması ve elle hayata geçirilmesi gerekiyor.

Evet, şablon türleri (vector<int>, vector<User>) ortak bir tanımlayıcıya sahip (#1cb5c415), yani. çağrının şu şekilde duyurulduğunu biliyorsanız:

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

o zaman artık sadece bir vektörü değil, kullanıcıların vektörünü bekliyorsunuz. Daha kesin, gerektiği bekleyin - gerçek kodda, çıplak tür olmasa da her öğenin bir yapıcısı olacaktır ve uygulamada iyi bir şekilde kontrol edilmesi gerekecektir - ancak biz bu vektörün her öğesinde tam olarak gönderildik bu tip? Ya bir dizinin farklı öğelerde farklı türleri içerebildiği bir tür PHP olsaydı?

Bu noktada şunu düşünmeye başlıyorsunuz: Böyle bir TL gerekli mi? Belki araba için o zamanlar zaten var olan aynı protobuf olan bir insan serileştirici kullanmak mümkün olabilir? Teori buydu, pratiğe bakalım.

Koddaki mevcut TL uygulamaları

TL, Durov'un hissesinin satışıyla ünlü olaylardan önce bile VKontakte'nin derinliklerinde doğdu ve (elbette), Telegram'ın gelişimi başlamadan önce bile. Ve açık kaynakta ilk uygulamanın kaynak kodu pek çok komik koltuk değneği bulabilirsiniz. Ve dilin kendisi de orada şu anda Telegram'da olduğundan daha eksiksiz bir şekilde uygulandı. Örneğin, şemada karmalar hiç kullanılmaz (sapkın davranışa sahip yerleşik bir sözde tip (bir vektör gibi) anlamına gelir). Veya

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

ama bütünlük adına, deyim yerindeyse Düşünce Devi'nin evriminin izini sürmeyi düşünelim.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Ya da şu güzel:

    static const char *reserved_words_polymorhic[] = {

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

      };

Bu parça aşağıdaki gibi şablonlarla ilgilidir:

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

Bu, hashmap şablon türünün int - Type çiftlerinin bir vektörü olarak tanımıdır. C++'da şöyle görünecektir:

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

yani burada alpha - anahtar kelime! Ama sadece C++'ta T yazabilirsin ama alfa, beta yazmalısın... Ama 8 parametreden fazla olmasın, fantezi burada bitiyor. Öyle görünüyor ki, bir zamanlar St. Petersburg'da buna benzer diyaloglar yaşanmıştı:

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

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

Ancak bu, TL'nin “genel olarak” yayınlanan ilk uygulamasıyla ilgiliydi. Telegram istemcilerindeki uygulamaları değerlendirmeye geçelim.

Vasily'e:

Vasily, [09.10.18 17:07] En önemlisi, kıç ateşli çünkü bir sürü soyutlama yarattılar, sonra üzerlerine bir cıvata çaktılar ve kod oluşturucuyu koltuk değnekleriyle kapattılar
Sonuç olarak, ilk olarak dock pilot.jpg'den
Daha sonra dzhekichan.webp kodundan

Elbette algoritmalara ve matematiğe aşina olan insanlardan Aho, Ullmann'ı okumuş olmalarını ve DSL derleyicilerini yazmak için onlarca yıldır sektörde fiili standart haline gelen araçlara aşina olmalarını bekleyebiliriz, değil mi?..

Yazar telgraf-cli TLO formatının (cli) sınırları dışında ortaya çıkmasından da anlaşılacağı üzere Vitaly Valtman, ekibin bir üyesi - artık TL ayrıştırma için bir kütüphane tahsis edildi ayrı ayrı, onun hakkındaki izlenimi nedir? TL ayrıştırıcı? ..

16.12 04:18 Vasily: Sanırım birisi lex+yacc konusunda ustalaşmamış
16.12 04:18 Vasily: Başka türlü açıklayamam
16.12 04:18 Vasily: peki ya da VK'daki hat sayısı için ödeme yapıldı
16.12 04:19 Vasily: 3k+ satır vb.<censored> ayrıştırıcı yerine

Belki bir istisna? Bakalım nasıl делает Bu RESMİ istemci - Telegram Masaüstü:

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

Python'da 1100'den fazla satır, birkaç düzenli ifade + bir vektör gibi özel durumlar, ki bu elbette şemada TL sözdizimine göre olması gerektiği gibi bildirildi, ancak onu ayrıştırmak için bu sözdizimine güvendiler... Soru ortaya çıkıyor: neden bunların hepsi bir mucizeydi?иZaten belgelere göre kimse onu ayrıştırmayacaksa daha katmanlı mı?

Bu arada... CRC32 kontrolünden bahsettiğimizi hatırlıyor musunuz? Dolayısıyla, Telegram Masaüstü kod oluşturucusunda, hesaplanan CRC32'nin geçerli olduğu türler için bir istisna listesi vardır. eşleşmiyor şemada gösterilenle!

Vasily, [18.12/22 49:XNUMX] ve burada böyle bir TL'ye ihtiyaç olup olmadığını düşünürdüm
alternatif uygulamalarla uğraşmak isteseydim satır sonları eklemeye başlardım, ayrıştırıcıların yarısı çok satırlı tanımlarda bozulur
ancak tdesktop da

Tek satırlık noktayı unutmayın, buna biraz sonra döneceğiz.

Tamam, telegram-cli resmi değil, Telegram Masaüstü resmi, peki ya diğerleri? Kim bilir?.. Android istemci kodunda hiçbir şema ayrıştırıcı yoktu (bu da açık kaynakla ilgili soruları gündeme getiriyor, ancak bu ikinci bölüm için), ancak birkaç komik kod parçası daha vardı, ancak bunlar hakkında daha fazlası için bkz. aşağıdaki alt bölüm.

Serileştirme pratikte başka hangi soruları gündeme getiriyor? Örneğin bit alanları ve koşullu alanlarla elbette pek çok şey yaptılar:

Vasily: flags.0? true
alanın mevcut olduğu ve bayrak ayarlanmışsa doğruya eşit olduğu anlamına gelir

Vasily: flags.1? int
alanın mevcut olduğu ve seri durumdan çıkarılması gerektiği anlamına gelir

Vasily: Göt, ne yaptığın konusunda endişelenme!
Vasily: Belgenin bir yerinde true'nun çıplak sıfır uzunluklu bir tür olduğundan bahsediliyor, ancak belgelerinden herhangi bir şeyi bir araya getirmek imkansız
Vasily: Açık uygulamalarda da böyle bir şey yok ama çok fazla koltuk değneği ve destek var

Peki Telethon'a ne dersiniz? MTProto konusuna bir örnek olarak bakalım - belgelerde bu tür parçalar var, ancak işaret % yalnızca "belirli bir çıplak tipe karşılık gelen" olarak tanımlanır, yani. aşağıdaki örneklerde ya bir hata var ya da belgelenmemiş bir şey var:

Vasily, [22.06.18 18:38] Tek bir yerde:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Farklı bir şekilde:

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

Ve bunlar iki büyük fark, gerçek hayatta bir çeşit çıplak vektör geliyor

Çıplak bir vektör tanımı görmedim ve rastlamadım

Analiz, teletonda elle yazılır

Diyagramında tanım yorumlanmıştır msg_container

Yine % ile ilgili soru kalıyor. Açıklanmadı.

Vadim Goncharov, [22.06.18 19:22] ve masaüstünde mi?

Vasily, [22.06.18 19:23] Ancak normal motorlardaki TL ayrıştırıcıları da büyük olasılıkla bunu yemeyecek

// parsed manually

TL güzel bir soyutlamadır, kimse onu tam olarak uygulayamaz

Ve %, şemanın kendi versiyonunda değil

Ancak burada belgeler kendisiyle çelişiyor, bu yüzden bilmiyorum

Dilbilgisinde bulundu, anlambilimi açıklamayı unutmuş olabilirler

TL’de belgeyi gördün, yarım litre olmadan anlayamazsın

Başka bir okuyucu, "Diyelim ki, bir şeyi eleştiriyorsunuz, o halde bana bunun nasıl yapılması gerektiğini gösterin" diyecektir.

Vasily şöyle yanıtlıyor: “Ayrıştırıcıya gelince, şunun gibi şeyleri severim:

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

bir şekilde bundan daha çok hoşlanıyorum

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

veya

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

bu BÜTÜN sözlüktür:

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

onlar. daha basit olanı, hafifçe söylemektir.

Genel olarak, sonuç olarak, TL'nin fiilen kullanılan alt kümesi için ayrıştırıcı ve kod üreteci yaklaşık 100 gramer satırına ve üretecin ~300 satırına sığar (hepsini sayarsak) printHer sınıfta iç gözlem için tür bilgisi çörekleri de dahil olmak üzere, oluşturulan kod). Her polimorfik tür boş bir soyut temel sınıfa dönüşür ve yapıcılar bundan miras alır ve serileştirme ve seri durumdan çıkarma yöntemlerine sahiptir.

Yazı dilinde tür eksikliği

Güçlü yazmak iyi bir şeydir, değil mi? Hayır, bu bir holivar değil (her ne kadar dinamik dilleri tercih etsem de), TL çerçevesinde bir varsayımdır. Buna dayanarak dil bizim için her türlü kontrolü sağlamalıdır. Tamam, belki kendisi değil ama uygulaması ama en azından bunları tanımlaması gerekiyor. Peki ne tür fırsatlar istiyoruz?

Her şeyden önce kısıtlamalar. Dosya yüklemeye ilişkin belgelerde şunu görüyoruz:

Dosyanın ikili içeriği daha sonra parçalara bölünür. Tüm parçalar aynı boyutta olmalıdır ( parça_boyutu ) ve aşağıdaki koşulların karşılanması gerekir:

  • part_size % 1024 = 0 (1 KB'a bölünebilir)
  • 524288 % part_size = 0 (512 KB, part_size'ye eşit olarak bölünebilir olmalıdır)

Boyutunun part_size'den küçük olması koşuluyla, son parçanın bu koşulları karşılaması gerekmez.

Her parçanın bir sıra numarası olmalı, dosya_bölümü0 ile 2,999 arasında değişen bir değere sahiptir.

Dosya bölümlendikten sonra onu sunucuya kaydetmek için bir yöntem seçmeniz gerekir. Kullanmak upload.saveBigFilePart dosyanın tam boyutunun 10 MB'tan fazla olması durumunda ve upload.saveFilePart daha küçük dosyalar için.
[…] aşağıdaki veri giriş hatalarından biri döndürülebilir:

  • FILE_PARTS_INVALID — Geçersiz parça sayısı. Değer arasında değil 1..3000

Diyagramda bunlardan herhangi biri var mı? Bu bir şekilde TL kullanılarak ifade edilebilir mi? HAYIR. Ama kusura bakmayın, büyükbabamın Turbo Pascal'ı bile belirtilen türleri tanımlayabilmişti. Aralıklar. Ve şimdi daha iyi bilinen bir şeyi daha biliyordu: enum - sabit (küçük) sayıda değerin numaralandırılmasından oluşan bir tür. C - sayısal gibi dillerde şu ana kadar yalnızca türlerden bahsettiğimizi unutmayın. sayılar. Ama bir de diziler, diziler var... Mesela bu dizinin sadece telefon numarası içerebileceğini anlatmak güzel olurdu değil mi?

Bunların hiçbiri TL'de yok. Ancak örneğin JSON Şeması'nda var. Ve eğer bir başkası 512 KB'nin bölünebilirliği hakkında tartışırsa, bunun hala kodda kontrol edilmesi gerekiyor, o zaman istemcinin basitçe yapamadı aralık dışında bir numara gönder 1..3000 (ve buna karşılık gelen hata ortaya çıkamazdı) mümkün olabilirdi, değil mi?..

Bu arada, hatalar ve dönüş değerleri hakkında. TL ile çalışmış olanlar bile gözlerini bulanıklaştırıyor; bunu hemen anlamadık. her TL'deki bir işlev aslında yalnızca açıklanan dönüş türünü değil aynı zamanda bir hatayı da döndürebilir. Ancak TL'nin kendisinden hiçbir şekilde bu sonuç çıkarılamaz. Tabii ki, zaten açık ve pratikte hiçbir şeye gerek yok (aslında RPC farklı şekillerde yapılabilir, buna daha sonra geri döneceğiz) - peki ya Soyut Türlerin Matematiği kavramlarının Saflığı? göksel dünyadan mı?.. Römorkörü aldım - eşleştirin.

Ve son olarak okunabilirlik ne olacak? Peki, genel olarak şunu isterim tanım şemada doğru şekilde var (JSON şemasında yine öyle), ancak zaten bu konuda zorlanıyorsanız, o zaman pratik tarafa ne dersiniz - en azından güncellemeler sırasında farklara önemsiz bir şekilde bakmak? Kendiniz görün gerçek örnekler:

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

veya

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

Bu herkese bağlıdır, ancak örneğin GitHub, bu kadar uzun satırlar içindeki değişiklikleri vurgulamayı reddediyor. “10 fark bul” oyunu ve beynin hemen gördüğü şey, her iki örnekte de başlangıç ​​ve bitişin aynı olduğu, sıkıcı bir şekilde ortada bir yerde okumak gerekiyor... Bana göre bu sadece teoride değil, ama tamamen görsel olarak kirli ve özensiz.

Bu arada, teorinin saflığı hakkında. Neden bit alanlarına ihtiyacımız var? Öyle görünmüyor mu koku tip teorisi açısından kötü mü? Açıklama diyagramın önceki versiyonlarında görülebilir. İlk başta evet öyleydi, her hapşırığın yeni bir türü yaratılıyordu. Bu temeller hala bu biçimde mevcuttur, örneğin:

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;

Ancak şimdi, yapınızda 5 isteğe bağlı alan varsa, o zaman tüm olası seçenekler için 32 türe ihtiyacınız olacağını hayal edin. Kombinatoryal patlama. Böylece, TL teorisinin kristal saflığı, serileştirmenin sert gerçekliğinin dökme demirden kıçına karşı bir kez daha paramparça oldu.

Ayrıca bazı yerlerde bu adamlar kendi tipolojilerini kendileri ihlal ediyorlar. Örneğin, MTProto'da (sonraki bölüm) yanıt Gzip tarafından sıkıştırılabilir, katmanların ve devrenin ihlal edilmesi dışında her şey yolundadır. Bir kez daha, RpcResult'un kendisi değil içeriği elde edildi. Peki bunu neden yapayım?.. Sıkıştırmanın herhangi bir yerde işe yaraması için koltuk değneğini kesmem gerekiyordu.

Veya başka bir örnek, bir zamanlar bir hata keşfettik - gönderildi InputPeerUser yerine InputUser. Ya da tam tersi. Ama işe yaradı! Yani sunucu türü umursamadı. Bu nasıl olabilir? Cevap bize telegram-cli'deki kod parçalarıyla verilebilir:

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

Başka bir deyişle serileştirmenin yapıldığı yer burasıdır MANUEL, kod oluşturulmadı! Belki sunucu da benzer şekilde uygulanmıştır?.. Prensip olarak bu bir kez yapılırsa işe yarayacaktır, ancak daha sonra güncellemeler sırasında nasıl desteklenebilir? Plan bu yüzden mi icat edildi? Ve burada bir sonraki soruya geçiyoruz.

Sürüm oluşturma. Katmanlar

Şematik versiyonlara neden katman denildiği, yalnızca yayınlanan şemaların geçmişine dayanarak tahmin edilebilir. Görünüşe göre, ilk başta yazarlar temel şeylerin değişmeyen şema kullanılarak yapılabileceğini düşündüler ve yalnızca gerekli olduğunda, belirli talepler için bunların farklı bir versiyon kullanılarak yapıldığını belirttiler. Prensip olarak, iyi bir fikir bile - ve yeni, eskisinin üzerine katmanlar halinde "karışık" olacak. Ama nasıl yapıldığını görelim. Doğru, en başından beri ona bakamadım - komik, ancak temel katmanın şeması mevcut değil. Katmanlar 2 ile başladı. Belgeler bize özel bir TL özelliğinden bahsediyor:

Bir istemci Katman 2'yi destekliyorsa aşağıdaki kurucunun kullanılması gerekir:

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

Pratikte bu, her API çağrısından önce değeri içeren bir int'nin olması anlamına gelir. 0x289dd1f6 yöntem numarasının önüne eklenmelidir.

Kulağa normal geliyor. Peki sonra ne oldu? Sonra ortaya çıktı

invokeWithLayer3#b7475268 query:!X = X;

Peki sırada ne var? Tahmin edebileceğiniz gibi,

invokeWithLayer4#dea0d430 query:!X = X;

Eğlenceli? Hayır, gülmek için henüz çok erken, bir düşünün her başka bir katmandan gelen bir isteğin bu kadar özel bir türe sarılması gerekiyor - eğer hepsi sizin için farklıysa, onları başka nasıl ayırt edebilirsiniz? Önüne yalnızca 4 bayt eklemek oldukça etkili bir yöntemdir. Bu yüzden,

invokeWithLayer5#417a57ae query:!X = X;

Ancak bir süre sonra bunun bir nevi baklavalığa dönüşeceği aşikar. Ve çözüm geldi:

Güncelleme: Katman 9'dan başlayarak yardımcı yöntemler invokeWithLayerN yalnızca birlikte kullanılabilir initConnection

Yaşasın! 9 sürümün ardından nihayet 80'lerde İnternet protokollerinde yapılanlara geldik - bağlantının başında sürüm üzerinde anlaşmaya vardık!

Peki sırada ne var?..

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

Ama şimdi hâlâ gülebilirsin. Ancak başka bir 9 katmandan sonra, bağlantının başlangıcında yalnızca bir kez çağrılması gereken sürüm numarasına sahip evrensel bir kurucu nihayet eklendi ve katmanların anlamı kaybolmuş gibi görünüyordu, şimdi bu sadece koşullu bir sürüm, gibi başka heryer. Sorun çözüldü.

Kesinlikle?..

Vasily, [16.07.18 14:01] Cuma günü bile şunu düşündüm:
Telesunucu olayları istek olmadan gönderir. İstekler InvokeWithLayer'a sarılmalıdır. Sunucu güncellemeleri sarmaz; yanıtları ve güncellemeleri sarmaya yönelik bir yapı yoktur.

Onlar. istemci güncellemeleri istediği katmanı belirleyemez

Vadim Goncharov, [16.07.18 14:02] InvokeWithLayer prensipte bir koltuk değneği değil mi?

Vasily, [16.07.18 14:02] Tek yol bu

Vadim Goncharov, [16.07.18 14:02] bu aslında oturumun başında katman üzerinde anlaşmaya varmak anlamına gelmeli

Bu arada, istemci sürüm düşürmenin sağlanmadığı sonucu çıkıyor

Güncellemeler, ör. tip Updates şemada bu, sunucunun istemciye bir API isteğine yanıt olarak değil, bir olay meydana geldiğinde bağımsız olarak gönderdiği şeydir. Bu, başka bir gönderide ele alınacak karmaşık bir konudur, ancak şimdilik sunucunun, istemci çevrimdışıyken bile Güncellemeleri kaydettiğini bilmek önemlidir.

Bu nedenle, sarmayı reddederseniz her Paketin sürümünü belirtmesi mantıksal olarak aşağıdaki olası sorunlara yol açar:

  • sunucu, istemci hangi sürümü desteklediğini bildirmeden önce bile istemciye güncellemeler gönderir
  • İstemciyi yükselttikten sonra ne yapmalıyım?
  • kim garantiişlem sırasında sunucunun katman numarası hakkındaki görüşünün değişmeyeceğini mi düşünüyorsunuz?

Bunun tamamen teorik bir spekülasyon olduğunu ve sunucu doğru yazıldığı için (en azından iyi bir şekilde test edildiği için) pratikte bunun olamayacağını mı düşünüyorsunuz? Ha! Nasıl olursa olsun!

Ağustos ayında karşılaştığımız durum tam olarak buydu. 14 Ağustos'ta Telegram sunucularında ve ardından günlüklerde bir şeylerin güncellendiğine dair mesajlar vardı:

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.

ve ardından birkaç megabaytlık yığın izi (aynı zamanda günlük kaydı da düzeltildi). Sonuçta, eğer TL'nizde bir şey tanınmıyorsa, bu imza yoluyla ikili bir sistemdir, daha da aşağısında TÜM giderse kod çözme imkansız hale gelecektir. Böyle bir durumda ne yapmalısınız?

Herkesin aklına gelen ilk şey bağlantıyı kesip tekrar denemektir. Yardım etmedi. CRC32'yi Google'da araştırdık - bunların şema 73'teki nesneler olduğu ortaya çıktı, ancak 82 üzerinde çalıştık. Günlüklere dikkatlice bakıyoruz - iki farklı şemadan tanımlayıcılar var!

Belki sorun tamamen resmi olmayan müşterimizdedir? Hayır, Telegram Desktop 1.2.17'yi (bir dizi Linux dağıtımında sağlanan sürüm) başlatıyoruz, İstisna günlüğüne yazıyor: MTP Beklenmeyen tür kimliği #b5223b0f, MTPMessageMedia'da okundu…

Telegram'ın protokol ve organizasyonel yaklaşımlarına yönelik eleştiri. Bölüm 1, teknik: sıfırdan müşteri yazma deneyimi - TL, MT

Google, benzer bir sorunun resmi olmayan istemcilerden birinin başına da geldiğini gösterdi ancak sürüm numaraları ve buna bağlı olarak varsayımlar farklıydı...

Yani ne yapmalıyız? Vasily ve ben ayrıldık: devreyi 91'e güncellemeye çalıştı, ben birkaç gün bekleyip 73'ü denemeye karar verdim. Her iki yöntem de işe yaradı, ancak ampirik oldukları için kaç sürüm yukarı veya aşağı ihtiyacınız olduğuna dair bir anlayış yok atlamak için veya ne kadar beklemeniz gerektiği.

Daha sonra durumu yeniden oluşturabildim: istemciyi başlatıyoruz, kapatıyoruz, devreyi başka bir katmana yeniden derliyoruz, yeniden başlatıyoruz, sorunu tekrar yakalıyoruz, öncekine dönüyoruz - ah, hiçbir devre değişimi yok ve istemci bir süre yeniden başlatılıyor birkaç dakika yardımcı olacaktır. Farklı katmanlardan veri yapılarının bir karışımını alacaksınız.

Açıklama? Çeşitli dolaylı belirtilerden de tahmin edebileceğiniz gibi sunucu, farklı makinelerde farklı türde birçok işlemden oluşur. Büyük olasılıkla, "ara belleğe alma" işleminden sorumlu olan sunucu, üstlerinin ona verdiği şeyi sıraya koymuş ve bunu üretim sırasında mevcut olan şemaya göre vermiştir. Ve bu kuyruk "çürüyene" kadar bu konuda hiçbir şey yapılamazdı.

Belki... ama bu berbat bir koltuk değneği mi?!.. Hayır, çılgın fikirleri düşünmeden önce resmi müşterilerin kodlarına bakalım. Android sürümünde herhangi bir TL ayrıştırıcı bulamıyoruz, ancak serileştirme (de) kaldırma özelliğine sahip ağır bir dosya buluyoruz (GitHub bu dosyaya rötuş yapmayı reddediyor). İşte kod parçacıkları:

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;

veya

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

Hımm... vahşi görünüyor. Ama muhtemelen bu oluşturulmuş koddur, o halde tamam mı?.. Ama kesinlikle tüm sürümleri destekliyor! Doğru, neden her şeyin birbirine karıştığı, gizli sohbetler ve her türlü şey açık değil. _old7 bir şekilde makine üretimine benzemiyor... Ancak en çok da beni şaşırttı

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Beyler, bir katmanın içinde ne olduğuna bile karar veremiyor musunuz?! Peki, tamam diyelim ki “iki” hatayla çıktı, yani oluyor ama ÜÇ?.. Hemen yine aynı tırmık mı? Bu nasıl bir pornografi, pardon?..

Bu arada, Telegram Desktop'ın kaynak kodunda da benzer bir şey oluyor - eğer öyleyse, şemaya arka arkaya yapılan birkaç taahhüt, katman numarasını değiştirmiyor, ancak bir şeyi düzeltiyor. Program için resmi bir veri kaynağının bulunmadığı durumlarda, resmi müşterinin kaynak kodu dışında nereden elde edilebilir? Ve eğer oradan alırsanız, tüm yöntemleri test etmeden şemanın tamamen doğru olduğundan emin olamazsınız.

Bu nasıl test edilebilir? Ünite, fonksiyonel ve diğer test hayranlarının yorumlarını paylaşacağını umuyorum.

Tamam, başka bir kod parçasına bakalım:

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;

Bu "manuel olarak oluşturulmuş" yorumu, bu dosyanın yalnızca bir kısmının manuel olarak yazıldığını (tüm bakım kabusunu hayal edebiliyor musunuz?) ve geri kalanının makine tarafından oluşturulduğunu gösteriyor. Ancak o zaman başka bir soru ortaya çıkıyor: Kaynakların mevcut olup olmadığı tam değil (Linux çekirdeğindeki bir GPL blob'u), ancak bu zaten ikinci bölümün konusu.

Ama yeterli. Tüm bu serileştirmenin üzerinde çalıştığı protokole geçelim.

MT Protokolü

Öyleyse açalım Genel açıklama и protokolün ayrıntılı açıklaması ve ilk tökezlediğimiz şey terminolojidir. Ve her şeyin bolluğuyla. Genel olarak, bu Telegram'ın tescilli bir özelliği gibi görünüyor - farklı yerlerdeki şeyleri farklı şekilde çağırmak veya tek kelimeyle farklı şeyleri çağırmak veya bunun tersi (örneğin, üst düzey bir API'de, bir çıkartma paketi görürseniz, bu doğru değildir) ne düşündün).

Örneğin, "mesaj" ve "oturum" burada normal Telegram istemci arayüzünden farklı bir anlama gelir. Mesajla ilgili her şey açık, OOP terimleriyle yorumlanabilir veya basitçe "paket" kelimesi olarak adlandırılabilir - bu düşük bir taşıma seviyesidir, arayüzdekiyle aynı mesajlar yoktur, çok sayıda servis mesajı vardır . Ama seans... ama ilk önce şeyler.

Taşıma katmanı

İlk şey ulaşımdır. Bize 5 seçenekten bahsedecekler:

  • TCP
  • Web yuvası
  • HTTPS üzerinden Websocket
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] UDP aktarımı da var ama belgelenmemiş

Ve TCP'nin üç çeşidi

Birincisi TCP üzerinden UDP'ye benzer; her paket bir sıra numarası ve crc içerir.
Arabadaki belgeleri okumak neden bu kadar acı verici?

İşte şimdi burada TCP'nin zaten 4 çeşidi var:

  • Kısaltılmış
  • Orta seviye
  • Yastıklı ara ürün
  • Her Şey Dahil

Tamam, MTProxy için yastıklı ara ürün, bu daha sonra iyi bilinen olaylar nedeniyle eklendi. Peki bir taneyle idare edebilecekken neden iki versiyon daha (toplamda üç)? Dördü de temel olarak yalnızca ana MTProto'nun uzunluğunun ve yükünün nasıl ayarlanacağı konusunda farklılık gösterir; bu daha sonra tartışılacaktır:

  • Kısaltılmış'ta 1 veya 4 bayttır, ancak 0xef değildir, o zaman gövde
  • Orta düzeyde bu 4 bayt uzunluğunda ve bir alandır ve istemcinin ilk kez göndermesi gereken 0xeeeeeeee Orta düzey olduğunu belirtmek için
  • Tam olarak bir ağ oluşturucunun bakış açısından en bağımlılık yapıcı olan: uzunluk, sıra numarası ve esas olarak MTProto, gövde, CRC32 olan BİR DEĞİL. Evet, bunların hepsi TCP'nin üstünde. Bu bize sıralı bayt akışı biçiminde güvenilir aktarım sağlar; hiçbir diziye, özellikle de sağlama toplamlarına ihtiyaç yoktur. Tamam, şimdi birisi bana TCP'nin 16 bitlik bir sağlama toplamına sahip olduğu konusunda itiraz edecek, bu nedenle veri bozulması meydana gelecektir. Harika, ancak aslında 16 bayttan daha uzun hash'lere sahip bir şifreleme protokolümüz var; tüm bu hatalar ve hatta daha fazlası, daha yüksek düzeydeki bir SHA uyumsuzluğu tarafından yakalanacaktır. Bunun üzerine CRC32'de HİÇBİR nokta yoktur.

Bir baytlık uzunluğun mümkün olduğu Kısaltılmış'ı, "4 baytlık veri hizalamanın gerekli olması durumunda" ifadesini haklı çıkaran Orta ile karşılaştıralım ki bu oldukça saçma. Ne, Telegram programcılarının bir soketten hizalanmış bir ara belleğe veri okuyamayacak kadar beceriksiz olduklarına mı inanılıyor? Yine de bunu yapmak zorundasınız, çünkü okuma size istediğiniz sayıda bayt döndürebilir (ve örneğin proxy sunucular da vardır...). Veya diğer taraftan, 16 baytın üzerinde hala büyük bir dolguya sahip olacaksak, neden Kısaltılmış'ı engelleyelim - 3 bayttan tasarruf edin bazen ?

Nikolai Durov'un, ağ protokolleri de dahil olmak üzere, gerçek bir pratik ihtiyaç olmadan tekerlekleri yeniden icat etmeyi gerçekten sevdiği izlenimi ediniliyor.

Diğer ulaşım seçenekleri dahil. Web ve MTProxy'yi talep olursa şimdi, belki başka bir yazıda ele almayacağız. Aynı MTProxy hakkında, 2018'de piyasaya sürülmesinden kısa bir süre sonra sağlayıcıların onu engellemeyi hızlı bir şekilde öğrendiğini şimdi hatırlayalım. baypas engellemeTarafından Paket Boyutu! Ve ayrıca (yine Waltman tarafından) C dilinde yazılan MTProxy sunucusunun Linux özelliklerine aşırı derecede bağlı olduğu gerçeği, buna hiç gerek olmamasına rağmen (Phil Kulin onaylayacak) ve Go veya Node.js'deki benzer bir sunucunun yüzden fazla satıra sığmaz.

Ancak bölümün sonunda diğer konuları da ele alarak bu kişilerin teknik okuryazarlığı hakkında sonuçlara varacağız. Şimdilik, MTProto oturumunu yerleştirdikleri OSI katman 5 oturumuna geçelim.

Anahtarlar, mesajlar, oturumlar, Diffie-Hellman

Onu oraya tamamen doğru şekilde yerleştirmediler... Bir oturum, arayüzde Etkin oturumlar altında görünen oturumla aynı değildir. Ama sırayla.

Telegram'ın protokol ve organizasyonel yaklaşımlarına yönelik eleştiri. Bölüm 1, teknik: sıfırdan müşteri yazma deneyimi - TL, MT

Böylece taşıma katmanından bilinen uzunlukta bir bayt dizisi aldık. Bu ya şifrelenmiş bir mesajdır ya da düz metindir; eğer hâlâ temel anlaşma aşamasındaysak ve bunu gerçekten yapıyorsak. “Anahtar” denilen bir sürü kavramdan hangisinden bahsediyoruz? Bu konuyu Telegram ekibi adına açıklığa kavuşturalım (Saat 4'te yorgun bir beyinle kendi belgelerimi İngilizce'den çevirdiğim için özür dilerim, bazı cümleleri olduğu gibi bırakmak daha kolay oldu):

adı verilen iki varlık vardır. Oturum - her oturumun tüm cihaza / işletim sistemine karşılık geldiği "geçerli oturumlar" altındaki resmi istemcilerin kullanıcı arayüzünde bir tane.
İkincisi MTProto oturumuİçinde mesajın sıra numarasını (düşük seviyeli anlamda) bulunduran ve farklı TCP bağlantıları arasında sürebilir. Örneğin dosya indirme işlemlerini hızlandırmak için aynı anda birkaç MTProto oturumu kurulabilir.

Bu ikisi arasında oturumları bir konsept var yetki. Dejenere durumda şunu söyleyebiliriz. Kullanıcı arayüzü oturumu aynıdır yetkiama ne yazık ki her şey karmaşık. Haydi bakalım:

  • Yeni cihazdaki kullanıcı ilk olarak şunu oluşturur: kimlik_anahtarı ve bunu örneğin SMS yoluyla hesaba bağlar - işte bu yüzden yetki
  • İlk içeride oldu MTProto oturumu, hangisi session_id kendi içinde.
  • Bu adımda kombinasyon yetki и session_id çağrılabilir örnek - bu kelime bazı istemcilerin belgelerinde ve kodlarında görünür
  • Daha sonra müşteri açılabilir bazı MTProto oturumları aynı şeyin altında kimlik_anahtarı - aynı DC'ye.
  • Daha sonra bir gün müşterinin dosyayı şu adresten istemesi gerekecektir: başka bir DC - ve bu DC için yeni bir tane oluşturulacak kimlik_anahtarı !
  • Kayıt olanın yeni bir kullanıcı olmadığını, aynı kullanıcı olduğunu sisteme bildirmek yetki (Kullanıcı arayüzü oturumu), istemci API çağrılarını kullanır auth.exportAuthorization ev DC'sinde auth.importAuthorization yeni DC'de.
  • Her şey aynı, birkaçı açık olabilir MTProto oturumları (her biri kendi session_id) bu yeni DC'ye, altında onun kimlik_anahtarı.
  • Son olarak müşteri Mükemmel İletim Gizliliği isteyebilir. Her kimlik_anahtarı oldu kalıcı anahtar - DC başına - ve müşteri arayabilir auth.bindTempAuthKey kullanmak için geçici kimlik_anahtarı - ve yine sadece bir tane temp_auth_key DC başına, herkes için ortak MTProto oturumları bu DC'ye.

Unutmayın tuz (ve gelecekteki tuzlar) da bir tanesidir kimlik_anahtarı onlar. herkes arasında paylaşılan MTProto oturumları aynı DC'ye.

"Farklı TCP bağlantıları arasında" ne anlama geliyor? Yani bu şu anlama geliyor gibi bir şey bir web sitesindeki yetkilendirme çerezi - belirli bir sunucuya yapılan birçok TCP bağlantısını sürdürür (hayatta kalır), ancak bir gün bozulur. Yalnızca HTTP'den farklı olarak, MTProto'da bir oturum içindeki mesajlar sırayla numaralandırılır ve onaylanır; tünele girdilerse bağlantı koptu - yeni bir bağlantı kurulduktan sonra sunucu, önceki oturumda teslim etmediği her şeyi nazikçe bu oturumda gönderecektir. TCP bağlantısı.

Ancak yukarıdaki bilgiler aylarca süren araştırmaların ardından özetlenmiştir. Bu arada müşterimizi sıfırdan mı uyguluyoruz? - hadi başlangıca geri dönelim.

O halde hadi üretelim auth_key üzerinde Telegram'dan Diffie-Hellman versiyonları. Belgeleri anlamaya çalışalım...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(veri) + veri + (herhangi bir rastgele bayt); uzunluk 255 bayta eşit olacak şekilde;
şifrelenmiş_veri := RSA(data_with_hash, sunucu_genel_anahtar); 255 baytlık uzun bir sayı (big endian), gerekli modül üzerinde gerekli güce yükseltilir ve sonuç, 256 baytlık bir sayı olarak saklanır.

Biraz uyuşturucuları var DH

Sağlıklı bir insanın DH'sine benzemiyor
Dx'te iki ortak anahtar yoktur

Sonunda bu çözüldü, ancak bir kalıntı kaldı - müşteri tarafından sayıyı çarpanlara ayırabildiğine dair iş kanıtı yapıldı. DoS saldırılarına karşı koruma türü. Ve RSA anahtarı yalnızca bir kez tek yönde, esasen şifreleme için kullanılır new_nonce. Ancak görünüşte basit olan bu operasyon başarılı olsa da neyle yüzleşmek zorunda kalacaksınız?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Başvuru isteğine henüz ulaşmadım

Bu isteği DH'ye gönderdim

Ve taşıma iskelesinde 4 baytlık bir hata koduyla yanıt verebileceğini söylüyor. Bu kadar

Bana -404 dedi, ne olmuş yani?

Ben de ona şöyle dedim: "Bunun gibi parmak izine sahip bir sunucu anahtarıyla şifrelenmiş saçmalıklarını yakala, DH'yi istiyorum" ve aptalca bir 404 ile yanıt verdi.

Bu sunucu yanıtı hakkında ne düşünüyorsunuz? Ne yapalım? Sorulacak kimse yok (fakat bu konuda daha fazlası ikinci bölümde).

Burada tüm ilgi iskelede yapılıyor

Yapacak başka bir şeyim yok, sadece sayıları ileri geri çevirmeyi hayal ettim

İki adet 32 ​​bitlik sayı. Herkes gibi onları da paketledim

Ama hayır, BE olarak önce bu ikisinin satıra eklenmesi gerekiyor

Vadim Goncharov, [20.06.18 15:49] ve bu nedenle 404?

Vasily, [20.06.18 15:49] EVET!

Vadim Goncharov, [20.06.18 15:50] bu yüzden ne "bulamadığını" anlamıyorum

Vasily, [20.06.18 15:50] hakkında

Asal faktörlere böyle bir ayrıştırma bulamadım")

Hata raporlamayı bile yönetemedik

Vasily, [20.06.18 20:18] Ah, bir de MD5 var. Zaten üç farklı karma

Anahtar parmak izi şu şekilde hesaplanır:

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

SHA1 ve sha2

Öyleyse koyalım auth_key Diffie-Hellman'ı kullanarak 2048 bit boyutunda aldık. Sıradaki ne? Daha sonra bu anahtarın alt 1024 bitinin hiçbir şekilde kullanılmadığını keşfediyoruz... ama şimdilik bunu düşünelim. Bu adımda sunucuyla ortak bir sırrımız var. Çok pahalı bir prosedür olan TLS oturumunun bir analogu oluşturuldu. Ancak sunucu hala kim olduğumuz hakkında hiçbir şey bilmiyor! Aslında henüz değil. yetki. Onlar. Bir zamanlar ICQ'da yaptığınız gibi "giriş şifresi" veya en azından SSH'de olduğu gibi (örneğin, bazı gitlab/github'da) "giriş anahtarı" açısından düşündüyseniz. İsimsiz bir tane aldık. Sunucu bize "bu telefon numaralarına başka bir DC tarafından hizmet veriliyor" derse ne olur? Veya “telefon numaranız yasaklandı” mı? Yapabileceğimiz en iyi şey, işe yarayacağını ve o zamana kadar çürümeyeceğini umarak anahtarı saklamaktır.

Bu arada, bunu rezervasyonlarla “aldık”. Örneğin sunucuya güveniyor muyuz? Ya sahteyse? Şifreleme kontrolleri gerekli olacaktır:

Vasily, [21.06.18 17:53] Mobil istemcilere asallık için 2kbit'lik bir sayıyı kontrol etmelerini sunuyorlar")

Ama hiç de net değil, nafeijoa

Vasily, [21.06.18 18:02] Belge, işin basit olmadığı ortaya çıkarsa ne yapılacağını söylemiyor

Söylenmedi. Bakalım bu durumda resmi Android istemcisi ne yapacak? A bu ne (ve evet, dosyanın tamamı ilginç) - dedikleri gibi, şunu buraya bırakacağım:

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

Hayır elbette hala orada bazı Bir sayının asallığıyla ilgili testler var ama kişisel olarak artık yeterli matematik bilgisine sahip değilim.

Tamam, ana anahtarı aldık. Oturum açmak için, yani. İstek göndermek için AES kullanarak daha fazla şifreleme yapmanız gerekir.

Mesaj anahtarı, yetkilendirme anahtarından alınan 128 baytın başına eklenen doldurma baytları da dahil olmak üzere, mesaj gövdesinin (oturum, mesaj kimliği vb. dahil) SHA256'sının 32 orta biti olarak tanımlanır.

Vasily, [22.06.18 14:08] Ortalama, kaltak, bitler

got a auth_key. Tüm. Bunların ötesinde... belgede açık değil. Açık kaynak kodunu incelemekten çekinmeyin.

MTProto 2.0'ın 12 ila 1024 bayt arasında dolgu gerektirdiğini ve yine de elde edilen mesaj uzunluğunun 16 bayta bölünebilmesi koşuluna bağlı olduğunu unutmayın.

Peki ne kadar dolgu eklemelisiniz?

Ve evet, hata durumunda 404 de var

Herhangi biri dokümantasyonun şemasını ve metnini dikkatlice incelemişse, orada MAC olmadığını fark etmiştir. Ve bu AES, başka hiçbir yerde kullanılmayan belirli bir IGE modunda kullanılır. Elbette bunu SSS'lerinde yazıyorlar... Burada, mesaj anahtarının kendisi aynı zamanda şifresi çözülmüş verilerin SHA karma değeridir ve bütünlüğü kontrol etmek için kullanılır - ve bir uyumsuzluk durumunda, herhangi bir nedenden dolayı belgeler. onları sessizce görmezden gelmenizi tavsiye ediyor (peki ya güvenlik, ya bizi kırarlarsa?).

Ben bir kriptograf değilim, belki de bu durumda teorik açıdan bu modda yanlış bir şey yoktur. Ancak Telegram Desktop'ı örnek olarak kullanarak pratik bir sorunu açıkça adlandırabilirim. Yerel önbelleği (tüm bu D877F783D5D3EF8C) MTProto'daki mesajlarla aynı şekilde şifreler (yalnızca bu durumda sürüm 1.0), yani. önce mesaj anahtarı, sonra verinin kendisi (ve ana büyük anahtar dışında bir yerde) auth_key 256 bayt, bu olmadan msg_key kullanışsız). Böylece sorun büyük dosyalarda fark edilir hale gelir. Yani, verilerin iki kopyasını (şifrelenmiş ve şifresi çözülmüş) saklamanız gerekir. Ve örneğin megabaytlar veya video akışı varsa?.. Şifreli metinden sonra MAC içeren klasik şemalar, onu hemen ileterek akışını okumanıza olanak tanır. Ancak MTProto ile yapmanız gerekenler ilk mesajın tamamını şifreleyin veya şifresini çözün, ancak daha sonra onu ağa veya diske aktarın. Bu nedenle, Telegram Desktop'ın en son sürümlerinde önbellekte user_data Başka bir format da kullanılır - TO modunda AES ile.

Vasily, [21.06.18 01:27] Ah, IGE'nin ne olduğunu öğrendim: IGE, orijinal olarak Kerberos için "kimlik doğrulama şifreleme modu"na yönelik ilk girişimdi. Başarısız bir girişimdi (bütünlük koruması sağlamaz) ve kaldırılması gerekiyordu. Bu, işe yarayan bir kimlik doğrulama şifreleme modu için 20 yıllık bir arayışın başlangıcıydı ve yakın zamanda OCB ve GCM gibi modlarla sonuçlandı.

Ve şimdi araba tarafındaki argümanlar:

Nikolai Durov liderliğindeki Telegram'ın arkasındaki ekip, yarısı matematik alanında doktora derecesine sahip altı ACM şampiyonundan oluşuyor. MTProto'nun mevcut sürümünü kullanıma sunmaları yaklaşık iki yıl sürdü.

Çok komik. Alt seviyede iki yıl

Ya da sadece tl alabilirsin

Tamam, diyelim ki şifrelemeyi ve diğer nüansları yaptık. Sonunda TL'de serileştirilmiş istekler göndermek ve yanıtları seri durumdan çıkarmak mümkün mü? Peki ne ve nasıl göndermelisiniz? Burada yöntem diyelim initConnectionbelki de budur?

Vasily, [25.06.18 18:46] Bağlantıyı başlatır ve kullanıcının cihazına ve uygulamasına ilişkin bilgileri kaydeder.

App_id, devices_model, system_version, app_version ve lang_code'u kabul eder.

Ve bazı sorgular

Her zamanki gibi belgeler. Açık kaynağı incelemekten çekinmeyin

invokeWithLayer ile ilgili her şey yaklaşık olarak açıksa, burada sorun ne? Diyelim ki istemcinin zaten sunucuya soracağı bir şey vardı - göndermek istediğimiz bir istek var:

Vasily, [25.06.18 19:13] Koda bakılırsa, ilk çağrı bu saçmalığa sarılmış ve saçmalığın kendisi de invokewithlayer'a sarılmış

Neden initConnection ayrı bir çağrı olamaz da bir sarmalayıcı olmalıdır? Evet, ortaya çıktığı gibi, ana anahtarda olduğu gibi bir kez değil, her oturumun başında her seferinde yapılması gerekiyor. Ancak! Yetkisiz bir kullanıcı tarafından çağrılamaz! Artık uygulanabilir aşamaya geldik Bu dokümantasyon sayfası - ve bize şunu söylüyor...

API yöntemlerinin yalnızca küçük bir kısmı yetkisiz kullanıcıların kullanımına açıktır:

  • auth.sendCode
  • auth.resendCode
  • account.getPassword
  • auth.checkŞifre
  • auth.checkTelefon
  • kimlik doğrulama.kayıt
  • auth.signIn
  • auth.importYetkilendirme
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

Bunlardan ilki, auth.sendCodeve api_id ve api_hash'ı gönderdiğimiz ve ardından kod içeren bir SMS aldığımız o değerli ilk istek var. Ve eğer yanlış DC'deysek (örneğin, bu ülkedeki telefon numaraları başka bir ülke tarafından servis ediliyor), o zaman istenen DC'nin numarasıyla ilgili bir hata alacağız. DC numarasına göre hangi IP adresine bağlanmanız gerektiğini öğrenmek için bize yardımcı olun help.getConfig. Bir zamanlar sadece 5 katılım vardı, ancak 2018'deki ünlü olayların ardından bu sayı önemli ölçüde arttı.

Şimdi sunucuda bu aşamaya anonim olarak geldiğimizi hatırlayalım. Sadece bir IP adresi almak çok pahalı değil mi? Neden bunu ve diğer işlemleri MTProto'nun şifrelenmemiş kısmında yapmıyorsunuz? İtirazı duyuyorum: "Yanlış adreslerle yanıt verecek olanın RKN olmadığından nasıl emin olabiliriz?" Bunun için genel olarak resmi müşterilerin olduğunu hatırlıyoruz RSA anahtarları gömülüdüryani sadece yapabilir misin imza bu bilgi. Aslında bu, istemcilerin diğer kanallar aracılığıyla aldığı engellemenin atlanmasına ilişkin bilgi için zaten yapılıyor (mantıksal olarak bu, MTProto'nun kendisinde yapılamaz; ayrıca nereye bağlanacağınızı da bilmeniz gerekir).

TAMAM. Müşteri yetkilendirmesinin bu aşamasında henüz yetkilendirilmedik ve başvurumuzu kaydetmedik. Şimdilik sunucunun yetkisiz bir kullanıcıya sunulan yöntemlere nasıl yanıt verdiğini görmek istiyoruz. Ve burada…

Vasily, [10.07.18 14:45] https://core.telegram.org/method/help.getConfig

config#7dae33e0 [...] = Config;
help.getConfig#c4f9186b = Config;

https://core.telegram.org/api/datacenter

config#232d5905 [...] = Config;
help.getConfig#c4f9186b = Config;

Şemada birinci ikinci olur

Tdesktop şemasında üçüncü değer:

Evet, o zamandan beri elbette belgeler güncellendi. Her ne kadar yakında tekrar alakasız hale gelebilir. Acemi bir geliştirici bunu nasıl bilmelidir? Belki başvurunuzu kaydederseniz sizi bilgilendirirler? Bunu Vasily yaptı, ama ne yazık ki ona hiçbir şey göndermediler (yine bunun hakkında ikinci bölümde konuşacağız).

...zaten bir şekilde API'ye geçtiğimizi fark ettiniz, yani. bir sonraki seviyeye geçtiniz ve MTProto konusunda bir şeyi mi kaçırdınız? Sürpriz değil:

Vasily, [28.06.18 02:04] Mm, e2e'deki bazı algoritmaları karıştırıyorlar

Mtproto, her iki alan için de şifreleme algoritmalarını ve anahtarlarını ve ayrıca bir miktar sarmalayıcı yapısını tanımlar

Ancak sürekli olarak yığının farklı seviyelerini karıştırırlar, bu nedenle mtproto'nun nerede bittiği ve bir sonraki seviyenin nerede başladığı her zaman net değildir.

Nasıl karışıyorlar? İşte PFS için aynı geçici anahtar (bu arada, Telegram Desktop bunu yapamaz). Bir API isteği tarafından yürütülür auth.bindTempAuthKeyyani en üst seviyeden. Ancak aynı zamanda daha düşük seviyedeki şifrelemeye de müdahale eder - örneğin ondan sonra bunu tekrar yapmanız gerekir initConnection vb. bu değil sadece normal istek. Ayrıca özel olan şey, alan olmasına rağmen DC başına yalnızca BİR geçici anahtara sahip olmanızdır. auth_key_id her mesajda anahtarı en azından her mesajda değiştirmenize izin verir ve sunucunun herhangi bir zamanda geçici anahtarı "unutma" hakkına sahip olması - belgeler bu durumda ne yapılacağını söylemez... peki neden yapasınız ki? Gelecekteki tuzlarda olduğu gibi birkaç anahtarınız yok ve ?..

MTProto temasıyla ilgili dikkat edilmesi gereken birkaç şey daha var.

Mesaj mesajları, msg_id, msg_seqno, onaylar, yanlış yöndeki pingler ve diğer tuhaflıklar

Neden onlar hakkında bilgi sahibi olmanız gerekiyor? Çünkü daha yüksek bir seviyeye “sızıyorlar” ve API ile çalışırken bunların farkında olmanız gerekiyor. Diyelim ki msg_key ile ilgilenmiyoruz; alt seviye bizim için her şeyin şifresini çözmüştür. Ancak şifresi çözülmüş verilerin içinde aşağıdaki alanlar bulunur (ayrıca verinin uzunluğu, dolayısıyla dolgunun nerede olduğunu biliyoruz, ancak bu önemli değil):

  • tuz - int64
  • oturum_kimliği - int64
  • message_id — int64
  • sıra_no - int32

DC'nin tamamı için tek bir tuz bulunduğunu hatırlatalım. Neden ondan haberin var? Sadece bir talep olduğu için değil get_future_saltsBu size hangi aralıkların geçerli olacağını söyler, ancak aynı zamanda tuzunuz "çürük" ise mesaj (istek) kaybolacaktır. Sunucu elbette yeni tuzu yayınlayarak rapor edecektir. new_session_created - ama eskisini örneğin bir şekilde yeniden göndermeniz gerekecek. Bu sorun da uygulama mimarisini etkiliyor.

Sunucunun birçok nedenden dolayı oturumları tamamen bırakmasına ve bu şekilde yanıt vermesine izin verilir. Aslında istemci tarafından MTProto oturumu nedir? Bunlar iki sayı session_id и seq_no Bu oturumdaki mesajlar. Ve elbette temeldeki TCP bağlantısı. Diyelim ki müşterimiz hala birçok şeyin nasıl yapılacağını bilmiyor, bağlantıyı kesip tekrar bağlandı. Bu hızlı bir şekilde gerçekleştiyse - eski oturum yeni TCP bağlantısında devam ettiyse, artırın seq_no daha öte. Uzun zaman alırsa, sunucu onu silebilir, çünkü öğrendiğimiz gibi onun tarafında da bir kuyruk var.

Ne olmalı seq_no? Ah, bu zor bir soru. Ne demek istediğini dürüstçe anlamaya çalışın:

İçerikle İlgili Mesaj

Açık bir onay gerektiren bir mesaj. Bunlar, kapsayıcılar ve bildirimler hariç olmak üzere, neredeyse tamamı tüm kullanıcı ve birçok hizmet mesajını içerir.

Mesaj Sıra Numarası (msg_seqno)

Gönderen tarafından bu mesajdan önce oluşturulan ve daha sonra mevcut mesaj bir mesaj ise bir artırılan "içerikle ilgili" mesajların (bildirim gerektirenler ve özellikle kapsayıcı olmayanlar) iki katı sayısının iki katına eşit 32 bitlik bir sayı. İçerikle ilgili mesaj. Bir kapsayıcı her zaman tüm içeriğinden sonra oluşturulur; bu nedenle sıra numarası, içerdiği mesajların sıra numaralarından büyük veya eşittir.

Bu nasıl bir sirk, 1'er artımlar ve ardından 2'şer artışlar?.. Başlangıçta "ACK için en az anlamlı olan bit, geri kalanı bir sayı" demek istediklerini sanıyorum ama sonuç tam olarak aynı değil - özellikle çıkıyor, gönderilebilir bazı onaylar aynı seq_no! Nasıl? Örneğin, sunucu bize bir şey gönderir, gönderir ve biz de sessiz kalırız, yalnızca mesajlarının alındığını onaylayan servis mesajlarıyla yanıt veririz. Bu durumda giden onaylarımız aynı giden numaraya sahip olacaktır. TCP'ye aşinaysanız ve bunun kulağa çılgınca geldiğini düşünüyorsanız, ancak çok da çılgın görünmüyor çünkü TCP'de seq_no değişmez ancak onay şuraya gider: seq_no diğer yandan seni üzmek için acele edeceğim. Onaylar MTProto'da sağlanır DEĞİL üzerinde seq_noTCP'de olduğu gibi, ancak msg_id !

Bu nedir msg_idBu alanların en önemlisi? Adından da anlaşılacağı gibi benzersiz bir mesaj tanımlayıcıdır. En düşük bitleri yine "sunucu-sunucu değil" büyüsüne sahip olan 64 bitlik bir sayı olarak tanımlanır ve geri kalanı, kesirli kısım da dahil olmak üzere 32 bit sola kaydırılmış bir Unix zaman damgasıdır. Onlar. zaman damgası (ve zamanları çok farklı olan mesajlar sunucu tarafından reddedilecektir). Buradan genel olarak bunun müşteri için küresel bir tanımlayıcı olduğu ortaya çıkıyor. Bunu göz önünde bulundurarak - hatırlayalım session_id - garanti ediyoruz: Hiçbir durumda bir oturuma gönderilen mesaj farklı bir oturuma gönderilemez.. Yani, zaten var olduğu ortaya çıkıyor üç seviye - oturum, oturum numarası, mesaj kimliği. Neden bu kadar karmaşıklık, bu gizem çok büyük.

Bu durumda, msg_id ihtiyaç var...

RPC: istekler, yanıtlar, hatalar. Onaylar.

Fark etmiş olabileceğiniz gibi, diyagramın hiçbir yerinde özel bir "RPC isteği yap" türü veya işlevi yoktur, ancak yanıtlar vardır. Sonuçta içerikle ilgili mesajlarımız var! Yani, herhangi mesaj bir istek olabilir! Yada olmamak. Nihayet, her var msg_id. Ama yanıtlar var:

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

Bunun hangi mesaja yanıt olduğu burada belirtilir. Bu nedenle, API'nin en üst seviyesinde isteğinizin sayısının ne olduğunu hatırlamanız gerekecek - işin eşzamansız olduğunu ve aynı anda devam eden birkaç isteğin olabileceğini açıklamaya gerek olmadığını düşünüyorum. hangi sırayla döndürülebilecek cevaplar? Prensip olarak, bundan ve çalışan yok gibi hata mesajlarından, bunun arkasındaki mimari izlenebilir: Sizinle TCP bağlantısını sürdüren sunucu bir ön uç dengeleyicidir, istekleri arka uçlara iletir ve bunları aracılığıyla geri toplar. message_id. Görünüşe göre burada her şey açık, mantıklı ve iyi.

Evet?.. Peki ya düşünürseniz? Sonuçta RPC yanıtının kendisinin de bir alanı var msg_id! Sunucuya “cevabıma cevap vermiyorsun!” diye bağırmamız mı gerekiyor? Ve evet, onaylarla ilgili ne vardı? Hakkında sayfası mesajlarla ilgili mesajlar bize ne olduğunu söyler

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

ve bu her iki tarafça da yapılmalıdır. Ama her zaman değil! Bir RpcResult aldıysanız, kendisi bir onay görevi görür. Yani sunucu, isteğinize "Aldım" gibi MsgsAck ile yanıt verebilir. RpcResult hemen yanıt verebilir. Her ikisi de olabilir.

Ve evet, hala cevabı cevaplamanız gerekiyor! Onayla. Aksi halde sunucu bunu teslim edilemez olarak değerlendirecek ve size tekrar gönderecektir. Yeniden bağlandıktan sonra bile. Ancak burada elbette zaman aşımı sorunu ortaya çıkıyor. Biraz sonra bunlara bakalım.

Bu arada olası sorgu yürütme hatalarına da bakalım.

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

Ah, birisi haykıracak, işte daha insani bir format - bir çizgi var! Acele etmeyin. Burada hataların listesiama elbette tamamlanmadı. Ondan kodun olduğunu öğreniyoruz gibi bir şey HTTP hataları (elbette yanıtların anlambilimine uyulmuyor, bazı yerlerde kodlar arasında rastgele dağıtılıyorlar) ve satır şöyle görünüyor CAPITAL_LETTERS_AND_NUMBERS. Örneğin, PHONE_NUMBER_OCCUPIED veya FILE_PART_Х_MISSING. Yani, yine de bu çizgiye ihtiyacın olacak ayrıştırmak. Örneğin, FLOOD_WAIT_3600 bir saat beklemeniz gerektiği anlamına gelir ve PHONE_MIGRATE_5, bu öneke sahip bir telefon numarasının 5. DC'ye kaydedilmesi gerektiğini. Bir yazı dilimiz var değil mi? Bir dizeden gelen bir argümana ihtiyacımız yok, normal olanlar işe yarayacaktır, tamam.

Tekrar belirtmek isterim ki bu, servis mesajları sayfasında yer almaz, ancak bu projede zaten olağan olduğu gibi, bilgiler bulunabilir. başka bir dokümantasyon sayfasında. veya şüphe uyandırmak. Öncelikle bakın, yazım/katman ihlali - RpcError yuvalanabilir RpcResult. Neden dışarıda değil? Neyi hesaba katmadık?.. Buna göre bunun garantisi nerede? RpcError içine gömülü OLMAYABİLİR RpcResult, ama doğrudan mı yoksa başka bir türe yuvalanmış mı?.. Ve eğer yapamıyorsa neden en üst seviyede olmasın, yani. O kayıp req_msg_id ? ..

Ancak servis mesajlarına devam edelim. İstemci, sunucunun uzun süredir düşündüğünü düşünebilir ve şu harika istekte bulunabilir:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Bu sorunun yine doğrulama mekanizmasıyla kesişen üç olası yanıtı vardır; bunların ne olması gerektiğini (ve onay gerektirmeyen türlerin genel listesinin neler olduğunu) anlamaya çalışmak okuyucuya ödev olarak bırakılmıştır (not: aşağıdaki bilgilerde yer almaktadır). Telegram Masaüstü kaynak kodu tamamlanmadı).

Uyuşturucu bağımlılığı: mesaj durumları

Genel olarak TL, MTProto ve Telegram'daki birçok yer genel olarak inatçılık hissi bırakıyor, ancak nezaket, incelik ve diğerlerinden dolayı hassas Yetenek Kibarca bu konuda sessiz kaldık, diyaloglardaki müstehcenlikleri sansürledik. Ancak burasıОsayfanın çoğu bununla ilgili mesajlarla ilgili mesajlar Uzun süredir ağ protokolleriyle çalışan ve çeşitli derecelerde çarpık bisikletler görmüş olan benim için bile şok edici.

Onaylamalarla zararsız bir şekilde başlar. Sonra bize şunu anlatıyorlar

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;

Peki, MTProto ile çalışmaya başlayan herkes bunlarla uğraşmak zorunda kalacak; "düzeltildi - yeniden derlendi - başlatıldı" döngüsünde sayı hataları almak veya düzenlemeler sırasında bozulmayı başaran tuz yaygın bir şeydir. Ancak burada iki nokta var:

  1. Bu, orijinal mesajın kaybolduğu anlamına gelir. Bazı kuyruklar oluşturmamız gerekiyor, buna daha sonra bakacağız.
  2. Bu garip hata numaraları nelerdir? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... diğer sayılar nerede Tommy?

Belgelerde şunlar belirtiliyor:

Amaç, error_code değerlerinin gruplandırılmasıdır (error_code >> 4): örneğin, 0x40 – 0x4f kodları konteyner ayrıştırmasındaki hatalara karşılık gelir.

ama birincisi, diğer yöne bir kayma ve ikincisi, önemli değil, diğer kodlar nerede? Yazarın kafasında mı?.. Ancak bunlar önemsiz şeyler.

Bağımlılık, mesaj durumları ve mesaj kopyalarıyla ilgili mesajlarda başlıyor:

  • Mesaj Durum Bilgisi Talebi
    Taraflardan biri, giden mesajlarının durumu hakkında bir süre bilgi alamadıysa, bunu diğer taraftan açıkça talep edebilir:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Mesajların Durumuna İlişkin Bilgilendirme Mesajı
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Burada, info gelen msg_ids listesindeki her mesaj için tam olarak bir baytlık mesaj durumu içeren bir dizedir:

    • 1 = mesaj hakkında hiçbir şey bilinmiyor (msg_id çok düşük, karşı taraf bunu unutmuş olabilir)
    • 2 = mesaj alınmadı (msg_id saklanan tanımlayıcıların aralığına giriyor; ancak karşı taraf kesinlikle böyle bir mesaj almadı)
    • 3 = mesaj alınmadı (msg_id çok yüksek; ancak karşı taraf mesajı henüz almamış)
    • 4 = mesaj alındı ​​(bu yanıtın aynı zamanda bir alındı ​​onayı olduğunu unutmayın)
    • +8 = mesaj zaten onaylandı
    • +16 = onay gerektirmeyen mesaj
    • +32 = İşlenmekte olan veya işleme zaten tamamlanmış mesajda bulunan RPC sorgusu
    • +64 = zaten oluşturulmuş mesaja içerikle ilgili yanıt
    • +128 = karşı taraf mesajın zaten alındığını biliyor
      Bu yanıt bir onay gerektirmez. Bu, ilgili msgs_state_req'in başlı başına bir onayıdır.
      Aniden karşı tarafın kendisine gönderilmiş gibi görünen bir mesajı olmadığı ortaya çıkarsa mesajın yeniden gönderilebileceğini unutmayın. Karşı taraf aynı anda mesajın iki kopyasını alsa bile kopya dikkate alınmayacaktır. (Çok fazla zaman geçmişse ve orijinal msg_id artık geçerli değilse, mesaj msg_copy içine sarılmalıdır).
  • Mesajların Durumunun Gönüllü İletişimi
    Taraflardan herhangi biri, diğer tarafın gönderdiği mesajların durumu hakkında diğer tarafa gönüllü olarak bilgi verebilir.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Bir Mesajın Durumunun Genişletilmiş Gönüllü İletişimi
    ...
    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;
  • Mesajların Yeniden Gönderilmesine İlişkin Açık Talep
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Uzaktaki taraf istenen mesajları yeniden göndererek hemen yanıt verir […]
  • Yanıtların Yeniden Gönderilmesine İlişkin Açık Talep
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Uzaktaki taraf, yeniden göndererek hemen yanıt verir cevaplar istenen mesajlara […]
  • Mesaj Kopyaları
    Bazı durumlarda, artık geçerli olmayan msg_id'li eski bir mesajın yeniden gönderilmesi gerekir. Daha sonra bir kopya kabına sarılır:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Mesaj alındıktan sonra ambalajlayıcı orada yokmuş gibi işlenir. Ancak orig_message.msg_id mesajının alındığı kesin olarak biliniyorsa, yeni mesaj işlenmez (aynı zamanda hem orig_message.msg_id hem de onaylanır). orig_message.msg_id değeri kapsayıcının msg_id değerinden düşük olmalıdır.

Hatta ne olduğu konusunda sessiz kalalım msgs_state_info yine bitmemiş TL'nin kulakları dışarı çıkıyor (bir bayt vektörüne ihtiyacımız vardı ve alt iki bitte bir numaralandırma vardı ve yüksek iki bitte bayraklar vardı). Mesele farklı. Bütün bunların neden pratikte olduğunu anlayan var mı? gerçek bir müşteride gerekli mi?.. Zor, ancak bir kişi hata ayıklamayla meşgulse ve etkileşimli modda ise bir miktar fayda hayal edilebilir - sunucuya ne ve nasıl olduğunu sorun. Ancak burada istekler açıklanıyor her iki yönde.

Bu, her bir tarafın yalnızca mesajları şifrelemesi ve göndermesi değil, aynı zamanda kendileri ve onlara verilen yanıtlar hakkındaki verileri bilinmeyen bir süre boyunca saklaması gerektiği anlamına gelir. Belgelerde bu özelliklerin zamanlamaları veya pratik uygulanabilirliği açıklanmamaktadır. hiçbir şekilde. En şaşırtıcı olanı ise bunların aslında resmi müşterilerin kodlarında kullanılıyor olmasıdır! Görünüşe göre onlara kamuya açık belgelerde yer almayan bir şey söylendi. Koddan anlayın neden, artık TL'deki kadar basit değil - (nispeten) mantıksal olarak izole edilmiş bir parça değil, uygulama mimarisine bağlı bir parça, yani. uygulama kodunu anlamak önemli ölçüde daha fazla zaman gerektirecektir.

Pingler ve zamanlamalar. Kuyruklar.

Her şeyden önce, sunucu mimarisi (isteklerin arka uçlar arasında dağılımı) hakkındaki tahminleri hatırlarsak, oldukça üzücü bir şey ortaya çıkar - TCP'deki tüm teslimat garantilerine rağmen (ya veriler teslim edilir ya da boşluk hakkında bilgilendirileceksiniz, ancak Veriler sorun oluşmadan önce teslim edilecektir), bu onaylar MTProto'nun kendisindedir - garanti yok. Sunucu mesajınızı kolayca kaybedebilir veya atabilir ve bu konuda hiçbir şey yapılamaz, sadece farklı türde koltuk değnekleri kullanın.

Ve her şeyden önce mesaj kuyrukları. Her şeyin en başından beri açık olduğu bir şey vardı; onaylanmamış bir mesajın saklanması ve yeniden gönderilmesi gerekiyordu. Peki ne zaman sonra? Ve soytarı onu tanıyor. Belki de bu bağımlı hizmet mesajları bir şekilde bu sorunu koltuk değnekleriyle çözüyor, örneğin Telegram Masaüstünde bunlara karşılık gelen yaklaşık 4 kuyruk var (belki daha önce de belirtildiği gibi daha fazlası, bunun için kodunu ve mimarisini daha ciddiye almanız gerekir; aynı zamanda) Örnek olarak alınamayacağını biliyoruz; MTProto şemasından belirli sayıda tür kullanılmamaktadır).

Bu neden oluyor? Muhtemelen sunucu programcıları küme içinde güvenilirliği sağlayamadılar, hatta ön dengeleyicide ara belleğe almayı bile başaramadılar ve bu sorunu istemciye aktardılar. Vasily, umutsuzluk içinde, yalnızca iki kuyruklu, TCP'den gelen algoritmaları kullanarak alternatif bir seçenek uygulamaya çalıştı - sunucuya giden RTT'yi ölçmek ve onaylanmamış isteklerin sayısına bağlı olarak "pencerenin" (mesajlarda) boyutunu ayarlamak. Yani, sunucunun yükünü değerlendirmek için böylesine kaba bir buluşsal yöntem, isteklerimizden kaç tanesini aynı anda çiğneyebileceği ve kaybetmeyeceğidir.

Yani anlıyorsun değil mi? TCP üzerinden çalışan bir protokolün üzerine TCP'yi tekrar uygulamak zorunda kalırsanız, bu çok kötü tasarlanmış bir protokole işaret eder.

Ah evet, neden birden fazla kuyruğa ihtiyacınız var ve bu, üst düzey bir API ile çalışan bir kişi için ne anlama geliyor? Bakın, talepte bulunuyorsunuz, seri hale getiriyorsunuz ama çoğu zaman hemen gönderemiyorsunuz. Neden? Çünkü cevap şu olacak msg_idgeçici olanаBen bir plak şirketiyim ve görevi mümkün olduğu kadar geç bir tarihe ertelenirse iyi olur - sunucunun bizimle onun arasındaki zaman uyumsuzluğu nedeniyle bunu reddetmesi durumunda (tabii ki zamanımızı şimdiki zamandan kaydıracak bir koltuk değneği yapabiliriz) sunucunun yanıtlarından hesaplanan bir delta ekleyerek sunucuya ekleyin - resmi müşteriler bunu yapar, ancak ara belleğe alma nedeniyle kaba ve hatalıdır). Bu nedenle kütüphaneden yerel bir işlev çağrısı ile istekte bulunduğunuzda mesaj aşağıdaki aşamalardan geçer:

  1. Tek bir kuyrukta yatıyor ve şifrelenmeyi bekliyor.
  2. Görevlendirilmiş msg_id ve mesaj başka bir kuyruğa gitti - olası yönlendirme; sokete gönderin.
  3. a) Sunucu MsgsAck'e yanıt verdi - mesaj teslim edildi, onu "diğer kuyruktan" sildik.
    b) Veya tam tersi, bir şeyi beğenmedi, badmsg'e cevap verdi - "başka bir sıradan" tekrar gönder
    c) Hiçbir şey bilinmiyor, mesajın başka bir sıradan yeniden gönderilmesi gerekiyor - ancak tam olarak ne zaman olduğu bilinmiyor.
  4. Sunucu sonunda yanıt verdi RpcResult - gerçek yanıt (veya hata) - yalnızca iletilmekle kalmaz, aynı zamanda işlenir.

Belkikonteynerlerin kullanılması sorunu kısmen çözebilir. Bu, bir grup mesajın tek bir mesajda paketlendiği ve sunucunun hepsine tek seferde bir onayla yanıt verdiği zamandır. msg_id. Ancak bir şeyler ters giderse bu paketi de tamamen reddedecektir.

Ve bu noktada teknik olmayan hususlar devreye giriyor. Deneyimlerimize dayanarak pek çok koltuk değneği gördük ve ayrıca artık daha fazla kötü tavsiye ve mimari örneği göreceğiz - bu tür koşullarda güvenmeye ve bu tür kararlar almaya değer mi? Soru retoriktir (elbette değil).

Ne hakkında konuşuyoruz? Eğer “mesajlarla ilgili uyuşturucu mesajları” konusunda hala “aptalsın, harika planımızı anlamadın!” (bu yüzden önce normal insanların yapması gerektiği gibi, paket değişiminin gerekçeleri ve örnekleriyle birlikte belgeleri yazın, sonra konuşuruz), sonra zamanlamalar/zaman aşımları tamamen pratik ve spesifik bir sorudur, buradaki her şey uzun zamandır bilinmektedir. Belgeler bize zaman aşımları hakkında ne söylüyor?

Sunucu genellikle istemciden bir mesajın (normalde bir RPC sorgusu) alındığını bir RPC yanıtı kullanarak bildirir. Yanıtın gelmesi uzun sürüyorsa, sunucu önce bir alındı ​​bildirimi gönderebilir, daha sonra da RPC yanıtının kendisini gönderebilir.

Bir istemci normalde bir sunucudan bir mesajın (genellikle bir RPC yanıtı) alındığını, eğer mesaj çok geç iletilmediyse (örneğin, alındıktan 60-120 saniye sonra oluşturulduysa) bir sonraki RPC sorgusuna bir onay ekleyerek onaylar. sunucudan gelen bir mesaj). Bununla birlikte, uzun bir süre boyunca sunucuya mesaj göndermek için bir neden yoksa veya sunucudan çok sayıda onaylanmamış mesaj varsa (örneğin 16'dan fazla), istemci tek başına bir alındı ​​bildirimi iletir.

... Çeviriyorum: Buna ne kadar ve nasıl ihtiyacımız olduğunu kendimiz bilmiyoruz, o yüzden öyle olduğunu varsayalım.

Ve ping'ler hakkında:

Ping Mesajları (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Genellikle aynı bağlantıya bir yanıt döndürülür:

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

Bu mesajlar teşekkür gerektirmez. Bir pong yalnızca bir ping'e yanıt olarak iletilirken, ping her iki tarafça da başlatılabilir.

Ertelenmiş Bağlantı Kapatma + PING

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

Ping gibi çalışır. Ek olarak, bu alındıktan sonra sunucu, önceki tüm zamanlayıcıları otomatik olarak sıfırlayan aynı türde yeni bir mesaj almadığı sürece mevcut bağlantıyı kes_delay saniyeler sonra kapatacak bir zamanlayıcı başlatır. İstemci bu ping'leri örneğin her 60 saniyede bir gönderirse, bağlantı kesme_delay'ini 75 saniyeye eşitleyebilir.

Sen deli misin?! 60 saniye sonra tren istasyona girecek, yolcu indirip alacak ve tüneldeki iletişim yine kesilecek. 120 saniye sonra siz onu dinlerken o başka bir sese gelecek ve büyük ihtimalle bağlantı kopacak. Bacakların nereden geldiği açık - "Bir zil sesi duydum ama nerede olduğunu bilmiyorum", Nagl'ın algoritması ve etkileşimli çalışmaya yönelik TCP_NODELAY seçeneği var. Ama kusura bakmayın, varsayılan değerini koruyun - 200 Millisaniye Gerçekten benzer bir şeyi tasvir etmek ve olası birkaç paketten tasarruf etmek istiyorsanız, bunu 5 saniye erteleyin veya "Kullanıcı yazıyor..." mesaj zaman aşımı ne kadar olursa olsun. Ama artık yok.

Ve son olarak pingler. Yani TCP bağlantısının canlılığını kontrol etmek. Komik, ancak yaklaşık 10 yıl önce fakültemizin yurdunun elçisi hakkında eleştirel bir metin yazdım - oradaki yazarlar da istemciden sunucuya ping attılar, tersi değil. Ama 3. sınıf öğrencileri başka bir şey, uluslararası ofis başka bir şey, değil mi?..

İlk olarak küçük bir eğitim programı. Paket değişimi olmadığında bir TCP bağlantısı haftalarca yaşayabilir. Bu, amaca bağlı olarak hem iyi hem de kötüdür. Sunucuya açık bir SSH bağlantınız olması, bilgisayardan kalkmanız, yönlendiriciyi yeniden başlatmanız, yerinize dönmeniz iyi olur - bu sunucu üzerinden oturum kesilmedi (hiçbir şey yazmadınız, paket yoktu) , Bu kullanışlı. Sunucuda her biri kaynakları kaplayan binlerce istemci varsa (merhaba, Postgres!) ve istemcinin ana bilgisayarı uzun zaman önce yeniden başlatılmış olabilir - ancak bunu bilmeyeceğiz.

Sohbet/IM sistemleri ek bir nedenden ötürü ikinci duruma girer: çevrimiçi durumlar. Kullanıcı "düştüyse" muhataplarını bu konuda bilgilendirmeniz gerekir. Aksi takdirde, Jabber'in yaratıcılarının yaptığı (ve 20 yıl boyunca düzelttiği) bir hatayla karşı karşıya kalacaksınız - kullanıcının bağlantısı kesildi, ancak çevrimiçi olduğuna inanarak ona mesaj yazmaya devam ediyorlar (ki bu da bunda tamamen kaybolmuş) bağlantı kopukluğunun fark edilmesinden birkaç dakika önce). Hayır, TCP zamanlayıcılarının nasıl çalıştığını anlamayan birçok kişinin rastgele attığı (onlarca saniye gibi vahşi değerler ayarlayarak) TCP_KEEPALIVE seçeneği burada yardımcı olmayacaktır - yalnızca işletim sistemi çekirdeğinin değil, emin olmanız gerekir. Kullanıcının makinesinin tamamı canlı, ancak aynı zamanda normal çalışıyor, yanıt veremiyor ve uygulamanın kendisi (donamayacağını mı düşünüyorsunuz? Ubuntu 18.04'teki Telegram Masaüstü benim için birden fazla kez dondu).

Bu yüzden ping atmanız gerekiyor sunucu istemci ve bunun tersi geçerli değildir - eğer müşteri bunu yaparsa, bağlantı kesilirse ping iletilmez, hedefe ulaşılamaz.

Telegram'da ne görüyoruz? Tam tersi! Yani öyle. Resmi olarak elbette her iki taraf da birbirine ping atabilir. Uygulamada müşteriler koltuk değneği kullanıyor ping_delay_disconnect, sunucudaki zamanlayıcıyı ayarlar. Kusura bakmayın, orada ne kadar süre ping olmadan yaşamak istediğine karar vermek müşteriye bağlı değil. Sunucu, yüküne bağlı olarak daha iyisini bilir. Ama elbette, kaynaklara aldırış etmezseniz, o zaman kendinizin şeytani Pinokyo'su olursunuz ve bir koltuk değneği de işe yarar...

Nasıl tasarlanmalıydı?

Yukarıdaki gerçeklerin, Telegram/VKontakte ekibinin bilgisayar ağlarının taşıma (ve daha düşük) seviyesi alanında çok yetkin olmadığını ve ilgili konulardaki niteliklerinin düşük olduğunu açıkça gösterdiğine inanıyorum.

Neden bu kadar karmaşık hale geldi ve Telegram mimarları nasıl itiraz etmeye çalışabilir? TCP bağlantısı kopan bir oturum oluşturmaya çalışmışlar, yani şu anda teslim edilmeyenleri daha sonra teslim edeceğiz. Muhtemelen bir UDP aktarımı da yapmayı denediler, ancak zorluklarla karşılaştılar ve bundan vazgeçtiler (bu nedenle belgeler boştu - övünecek bir şey yoktu). Ancak genel olarak ağların ve özel olarak TCP'nin nasıl çalıştığını, ona nerede güvenebileceğinizi ve bunu kendi başınıza nerede (ve nasıl) yapmanız gerektiğini anlama eksikliği ve bunu kriptografiyle birleştirme girişimi nedeniyle "iki kuş ve tek taş”, sonuç bu.

Nasıl gerekliydi? Şu gerçeğe dayanarak msg_id tekrar saldırılarını önlemek için kriptografik açıdan gerekli bir zaman damgasıdır, buna benzersiz bir tanımlayıcı işlevi eklemek bir hatadır. Bu nedenle, mevcut mimariyi temelden değiştirmeden (Güncellemeler akışı oluşturulduğunda, bu, bu yazı dizisinin başka bir bölümü için üst düzey bir API konusudur), birinin şunları yapması gerekir:

  1. İstemciyle TCP bağlantısını tutan sunucu sorumluluğu üstlenir - eğer soketten okuduysa, lütfen onaylayın, işleyin veya bir hata döndürün, kayıp yok. O zaman onay bir kimlik vektörü değil, yalnızca "son alınan sıra_no"dur - TCP'de olduğu gibi yalnızca bir sayıdır (iki sayı - sıranız ve onaylanan sayı). Her zaman seansın içindeyiz değil mi?
  2. Tekrar saldırılarını önleyen zaman damgası, bir kerede ayrı bir alan haline gelir. Kontrol edilir, ancak başka hiçbir şeyi etkilemez. Yeterince ve uint32 - tuzumuz en az yarım günde bir değişirse, 16 biti mevcut zamanın tam sayı kısmının düşük dereceli bitlerine, geri kalanını ise saniyenin kesirli kısmına (şimdiki gibi) ayırabiliriz.
  3. Kaldırıldı msg_id hiç - arka uçlardaki istekleri ayırt etme açısından bakıldığında, öncelikle müşteri kimliği ve ikinci olarak oturum kimliği vardır ve bunları birleştirir. Buna göre istek tanımlayıcı olarak tek bir şey yeterlidir seq_no.

Bu aynı zamanda en başarılı seçenek de değil; tam bir rastgele, tanımlayıcı görevi görebilir - bu arada, bu zaten üst düzey API'de bir mesaj gönderilirken yapılıyor. Mimariyi göreceliden mutlaklığa tamamen yeniden yapmak daha iyi olurdu, ancak bu başka bir bölümün konusu, bu yazının değil.

API?

Ta-daam! Böylece, acı ve koltuk değnekleriyle dolu bir yolda mücadele ettikten sonra, sonunda sunucuya herhangi bir istek gönderebildik ve bunlara herhangi bir yanıt alabildik, ayrıca sunucudan güncellemeler (bir isteğe yanıt olarak değil, kendisi) alabildik. Birisi varsa bize PUSH gibi gönderir - bu şekilde daha açıktır).

Dikkat, artık yazıda Perl'deki tek örnek olacak! (sözdizimine aşina olmayanlar için, korusun ilk argümanı nesnenin veri yapısı, ikincisi ise sınıfıdır):

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

Evet, bilerek spoiler vermek istemiyorum; eğer henüz okumadıysanız, devam edin ve okuyun!

Oh, bekle~~... bu neye benziyor? Çok tanıdık bir şey... belki de bu, JSON'daki tipik bir Web API'sinin veri yapısıdır, ancak sınıfların da nesnelere eklenmesi dışında?..

İşte böyle ortaya çıkıyor... Bütün bunlar ne anlama geliyor, yoldaşlar?.. Bu kadar çaba - ve biz de Web programcılarının olduğu yerde dinlenmek için durduk. yeni başlıyorum?..HTTPS üzerinden JSON daha basit olmaz mıydı?! Karşılığında ne aldık? Bu çabaya değer miydi?

TL+MTProto'nun bize neler sunduğunu ve hangi alternatiflerin mümkün olduğunu değerlendirelim. İstek-yanıt modeline odaklanan HTTP pek uygun değil ama en azından TLS'nin üstünde bir şey mi var?

Kompakt serileştirme. JSON'a benzeyen bu veri yapısını görünce bunun ikili versiyonlarının olduğunu hatırlıyorum. MsgPack'i yeterince genişletilemez olarak işaretleyelim, ancak örneğin CBOR var - bu arada, burada açıklanan bir standart RFC 7049. tanımlaması nedeniyle dikkat çekicidir. etiketlerbir genişleme mekanizması olarak ve zaten standartlaştırılmış var:

  • 25 + 256 - tekrarlanan satırları satır numarasına referansla değiştirmek, çok ucuz bir sıkıştırma yöntemi
  • 26 - sınıf adı ve yapıcı argümanlarıyla serileştirilmiş Perl nesnesi
  • 27 - tür adı ve yapıcı argümanları ile serileştirilmiş dilden bağımsız nesne

Aynı verileri TL'de ve CBOR'da dize ve nesne paketleme etkinken serileştirmeye çalıştım. Sonuç bir megabayttan CBOR lehine değişmeye başladı:

cborlen=1039673 tl_len=1095092

Bu durumda, çıktı: Senkronizasyon hatası veya bilinmeyen tanımlayıcı sorununa maruz kalmayan, karşılaştırılabilir verimlilikte, önemli ölçüde daha basit formatlar vardır.

Hızlı bağlantı kurulumu. Bu, yeniden bağlanmanın ardından sıfır RTT anlamına gelir (anahtar zaten bir kez oluşturulduğu zaman) - ilk MTProto mesajından itibaren geçerlidir, ancak bazı çekincelerle - aynı tuzağa düşerseniz, oturum çürük olmaz, vb. Bunun yerine TLS bize ne sunuyor? Konuyla ilgili alıntı:

TLS'de PFS kullanırken, TLS oturum biletleri (RFC 5077) anahtarları yeniden müzakere etmeden ve anahtar bilgilerini sunucuda saklamadan şifrelenmiş bir oturumu sürdürmek için. İlk bağlantıyı açarken ve anahtarlar oluştururken, sunucu bağlantı durumunu şifreler ve bunu istemciye (oturum bileti biçiminde) iletir. Buna göre, bağlantı devam ettirildiğinde istemci, oturum anahtarını da içeren bir oturum biletini sunucuya geri gönderir. Biletin kendisi, sunucuda depolanan ve kümelenmiş çözümlerde SSL işleyen tüm ön uç sunucular arasında dağıtılması gereken geçici bir anahtarla (oturum bileti anahtarı) şifrelenir.[10] Bu nedenle, örneğin uzun süre saklandıklarında geçici sunucu anahtarlarının güvenliği ihlal edilirse, bir oturum biletinin eklenmesi PFS'yi ihlal edebilir (OpenSSL, nginx, Apache bunları programın tüm süresi boyunca varsayılan olarak saklar; popüler siteler bunu kullanır). birkaç saatten birkaç güne kadar anahtar).

Burada RTT sıfır değildir, en azından ClientHello ve ServerHello'yu değiştirmeniz gerekir, ardından istemci Finished ile birlikte veri gönderebilir. Ancak burada, yeni açılan bağlantılarla dolu bir Web'e sahip olmadığımızı, ancak bağlantısı genellikle bir veya daha fazla veya daha az uzun ömürlü, Web sayfalarına nispeten kısa istekler içeren bir haberciye sahip olduğumuzu hatırlamalıyız - her şey çoğullanmıştır dahili olarak. Yani gerçekten kötü bir metro bölümüne rastlamamışsak oldukça kabul edilebilir.

Başka bir şeyi mi unuttun? Yorumlara yazın.

Devam edecek!

Bu yazı dizisinin ikinci bölümünde teknik değil organizasyonel konuları - yaklaşımlar, ideoloji, arayüz, kullanıcılara karşı tutum vb. - ele alacağız. Ancak burada sunulan teknik bilgilere dayanmaktadır.

Üçüncü bölümde teknik bileşen/geliştirme deneyimi analiz edilmeye devam edilecektir. Özellikle şunları öğreneceksiniz:

  • TL türlerinin çeşitliliği ile kargaşanın devamı
  • Kanallar ve süper gruplar hakkında bilinmeyen şeyler
  • Diyaloglar neden listeden daha kötü?
  • mutlak ve göreceli mesaj adresleme hakkında
  • fotoğraf ve resim arasındaki fark nedir
  • emoji italik metne nasıl müdahale ediyor

ve diğer koltuk değnekleri! Bizi izlemeye devam edin!

Kaynak: habr.com

Yorum ekle