انتقاد البروتوكول والمنهج التنظيمي لـ Telegram. الجزء الأول ، تقني: تجربة كتابة عميل من الصفر - TL، MT

في الآونة الأخيرة ، بدأت المنشورات تظهر على Habré في كثير من الأحيان حول مدى جودة Telegram ، ومدى ذكاء الأخوين Durov وخبرتهم في بناء أنظمة الشبكات ، وما إلى ذلك. في الوقت نفسه ، غمر عدد قليل جدًا من الأشخاص أنفسهم حقًا في الجهاز التقني - على الأكثر يستخدمون واجهة برمجة تطبيقات بوت بسيطة إلى حد ما (ومختلفة تمامًا عن MTProto) تعتمد على JSON ، وعادة ما يقبلون فقط على الإيمان كل تلك المديح والعلاقات العامة التي تدور حول الرسول. منذ ما يقرب من عام ونصف ، بدأ زميلي في NPO Echelon Vasily (للأسف ، تم حذف حسابه على Habré مع المسودة) في كتابة عميل Telegram الخاص به من الصفر في Perl ، ولاحقًا انضم مؤلف هذه السطور. لماذا بيرل ، سيسأل البعض على الفور؟ لأن هناك بالفعل مثل هذه المشاريع بلغات أخرى.في الواقع ، ليس هذا هو الهدف ، يمكن أن يكون هناك أي لغة أخرى فيها مكتبة منتهية، وبناءً عليه يجب على المؤلف أن يمضي كل الطريق من الصفر. علاوة على ذلك ، فإن التشفير شيء من هذا القبيل - الثقة ، ولكن التحقق. مع منتج يركز على الأمان ، لا يمكنك الاعتماد فقط على مكتبة البائع الجاهزة والاعتقاد بها بشكل أعمى (ومع ذلك ، هذا موضوع لمزيد من المعلومات في الجزء الثاني). في الوقت الحالي ، تعمل المكتبة بشكل جيد في المستوى "المتوسط" (يسمح لك بعمل أي طلبات API).

ومع ذلك ، لن يكون هناك الكثير من التشفير والرياضيات في هذه السلسلة من المنشورات. ولكن سيكون هناك العديد من التفاصيل الفنية والعكازات المعمارية الأخرى (ستكون مفيدة أيضًا لأولئك الذين لن يكتبوا من الصفر ، لكنهم سيستخدمون المكتبة بأي لغة). لذلك ، كان الهدف الرئيسي هو محاولة تنفيذ العميل من البداية وفقًا للوثائق الرسمية. بمعنى ، لنفترض أن الكود المصدري للعملاء الرسميين مغلق (مرة أخرى ، في الجزء الثاني ، سنكشف بمزيد من التفصيل موضوع ماهية هذا الأمر حقًا هو لذلك) ، ولكن ، كما في الأيام الخوالي ، على سبيل المثال ، هناك معيار مثل RFC - هل من الممكن كتابة عميل وفقًا للمواصفات وحدها ، "بدون اختلاس" في شفرة المصدر ، حتى الرسمية (Telegram Desktop ، mobile ) ، حتى Telethon غير الرسمية؟

главление:

التوثيق ... هل هو موجود؟ هل هذا صحيح؟ ..

بدأ جمع أجزاء من الملاحظات الخاصة بهذه المقالة في الصيف الماضي. كل هذا الوقت على الموقع الرسمي https://core.telegram.org كان التوثيق اعتبارًا من الطبقة 23 ، أي عالق في مكان ما في عام 2014 (تذكر ، في ذلك الوقت لم تكن هناك قنوات حتى الآن؟). بالطبع ، من الناحية النظرية ، كان من المفترض أن يجعل هذا من الممكن تنفيذ عميل لديه وظائف في ذلك الوقت في عام 2014. لكن حتى في هذه الحالة ، كان التوثيق ، أولاً ، غير مكتمل ، وثانيًا ، في مواضع تناقضت مع نفسه. منذ أكثر من شهر بقليل ، في سبتمبر 2019 ، كان كذلك بالصدفة وجد أن الموقع يحتوي على تحديث كبير للوثائق ، لطبقة 105 جديدة تمامًا ، مع ملاحظة أن كل شيء يحتاج الآن إلى القراءة مرة أخرى. في الواقع ، تم مراجعة العديد من المقالات ، لكن العديد منها ظل دون تغيير. لذلك ، عند قراءة النقد أدناه حول التوثيق ، يجب أن تضع في اعتبارك أن بعض هذه الأشياء لم تعد ذات صلة ، لكن بعضها لا يزال صحيحًا. بعد كل شيء ، 5 سنوات في العالم الحديث ليست مجرد الكثير ، ولكن جدا الكثير من. منذ ذلك الحين (خاصةً إذا كنت لا تأخذ في الحسبان geochats المهملة والمُحيتة منذ ذلك الحين) ، فقد نما عدد طرق API في المخطط من مائة إلى أكثر من مائتين وخمسين!

من أين تبدأ ككاتب شاب؟

لا يهم إذا كنت تكتب من الصفر أو تستخدم ، على سبيل المثال ، مكتبات جاهزة مثل Telethon لبايثون أو مادلين لـ PHP، على أي حال ، سوف تحتاج أولاً سجل طلبك - الحصول على المعلمات api_id и api_hash (أولئك الذين عملوا مع VKontakte API يفهمون على الفور) من خلالها سيتعرف الخادم على التطبيق. هذا لديك لأسباب قانونية ، لكننا سنتحدث أكثر عن سبب عدم تمكن مؤلفي المكتبات من نشرها في الجزء الثاني. ربما ستكون راضيًا عن قيم الاختبار ، على الرغم من أنها محدودة للغاية - الحقيقة هي أنه يمكنك الآن التسجيل على رقمك واحد فقط التطبيق ، لذلك لا تتسرع.

الآن ، من وجهة نظر فنية ، كان يجب أن نهتم بحقيقة أنه بعد التسجيل يجب أن نتلقى إشعارات من Telegram حول تحديثات الوثائق والبروتوكول وما إلى ذلك. بمعنى ، يمكن للمرء أن يفترض أن الموقع الذي يحتوي على أرصفة السفن قد تم "تسجيله" ببساطة واستمر في العمل على وجه التحديد مع أولئك الذين بدأوا في تكوين عملاء ، لأن. انها اسهل. لكن لا ، لم يلاحظ شيء من هذا القبيل ، ولم تأت أي معلومات.

وإذا كتبت من الصفر ، فإن استخدام المعلمات المستلمة لا يزال بعيدًا في الواقع. بالرغم من https://core.telegram.org/ ويتحدث عنها أولاً في "الخطوات الأولى" ، في الواقع ، عليك أولاً تنفيذها بروتوكول MTProto - ولكن إذا كنت تؤمن التخطيط وفقًا لنموذج OSI في نهاية صفحة الوصف العام للبروتوكول ، ثم عبثًا تمامًا.

في الواقع ، قبل MTProto وبعده ، على عدة مستويات في وقت واحد (كما يقول المسوقون الأجانب العاملون في نظام التشغيل kernel ، انتهاك الطبقة) ، سيكون هناك موضوع كبير ومؤلم ورهيب في الطريق ...

التسلسل الثنائي: TL (لغة النوع) ومخططها ، وطبقاتها ، والعديد من الكلمات المخيفة الأخرى

هذا الموضوع ، في الواقع ، هو مفتاح مشاكل Telegram. وسيكون هناك الكثير من الكلمات الرهيبة إذا حاولت الخوض فيها.

لذا ، مخطط. إذا كنت تتذكر هذه الكلمة ، فقل ، مخطط JSONفكرت بشكل صحيح. الهدف واحد: لغة ما لوصف مجموعة محتملة من البيانات المرسلة. هذا ، في الواقع ، هو المكان الذي ينتهي فيه التشابه. إذا من الصفحة بروتوكول MTProto، أو من شجرة المصدر للعميل الرسمي ، سنحاول فتح مخطط ما ، سنرى شيئًا مثل:

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;

الشخص الذي يرى هذا لأول مرة سوف يتعرف بشكل حدسي فقط على جزء مما هو مكتوب - حسنًا ، هذه على ما يبدو هياكل (على الرغم من أين الاسم ، على اليسار أو على اليمين؟) ، هناك حقول فيها ، وبعد ذلك النوع يمر عبر القولون ... ربما. هنا ، بين قوسين زاوية ، من المحتمل وجود قوالب كما في C ++ (في الواقع ، ليس حقا). وماذا تعني جميع الرموز الأخرى ، علامات الاستفهام ، وعلامات التعجب ، والنسب المئوية ، والشبكات (ومن الواضح أنها تعني أشياء مختلفة في أماكن مختلفة) ، موجودة في مكان ما ، ولكن ليس في مكان ما ، الأرقام السداسية العشرية - والأهم من ذلك ، كيفية الحصول على من هذا حق (التي لن يرفضها الخادم) دفق البايت؟ عليك أن تقرأ الوثائق (نعم ، هناك روابط إلى المخطط في إصدار JSON القريب - لكن هذا لا يوضحه بشكل أكبر).

فتح الصفحة تسلسل البيانات الثنائية وانغمس في العالم السحري للفطر والرياضيات المنفصلة ، وهو شيء مشابه لمااتان في السنة الرابعة. الأبجدية ، النوع ، القيمة ، المُجمع ، المُجمع الوظيفي ، الشكل العادي ، النوع المركب ، النوع متعدد الأشكال ... وهذه هي الصفحة الأولى فقط! التالي في انتظارك لغة TL، والتي ، على الرغم من أنها تحتوي بالفعل على مثال لطلب واستجابة تافهة ، فإنها لا تقدم إجابة لحالات نموذجية على الإطلاق ، مما يعني أنه سيتعين عليك الخوض في إعادة سرد الرياضيات المترجمة من الروسية إلى الإنجليزية في ثمانية أخرى متداخلة صفحات!

القراء المطلعون على اللغات الوظيفية والاستدلال التلقائي بالنوع ، بالطبع ، رأوا في هذه الأوصاف اللغوية ، حتى من مثال ، أكثر دراية ، ويمكن أن يقولوا أن هذا ليس سيئًا بشكل عام من حيث المبدأ. الاعتراضات على ذلك هي:

  • نعم، هدف يبدو جيدا ، ولكن للأسف غير منجز
  • يختلف التعليم في الجامعات الروسية حتى بين تخصصات تكنولوجيا المعلومات - لا يقرأ الجميع الدورة التدريبية المقابلة
  • أخيرًا ، كما سنرى ، من الناحية العملية هو كذلك غير مطلوب، حيث يتم استخدام مجموعة فرعية محدودة فقط من TL التي تم وصفها

كما قيل ليونرد على القناة #perl على شبكة FreeNode IRC ، في محاولة لتنفيذ بوابة من Telegram إلى Matrix (ترجمة الاقتباس غير دقيقة من الذاكرة):

يبدو الأمر وكأنه شخص تم تقديمه لنظرية الكتابة لأول مرة ، وتحمس وبدأ في محاولة اللعب بها ، ولم يهتم حقًا إذا كان ذلك ضروريًا في الممارسة.

لاحظ بنفسك ما إذا كانت الحاجة إلى أنواع عارية (int ، long ، إلخ) كشيء أساسي لا تثير أسئلة - في النهاية يجب تنفيذها يدويًا - على سبيل المثال ، دعنا نحاول الاشتقاق منها ناقلات. هذا هو ، في الواقع ، مجموعة، إذا كنت تدعو الأشياء الناتجة بأسمائها الصحيحة.

لكن قبل

وصف موجز لمجموعة فرعية من بناء جملة TL لأولئك الذين لا يقرأون الوثائق الرسمية

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;

يبدأ التعريف دائمًا مصمم، وبعد ذلك ، اختياريًا (عمليًا ، دائمًا) من خلال الرمز # ينبغي CRC32 من سلسلة الوصف المقيسة للنوع المحدد. يأتي بعد ذلك وصف الحقول ، إذا كانت كذلك - يمكن أن يكون النوع فارغًا. ينتهي كل شيء بعلامة التساوي ، اسم النوع الذي ينتمي إليه المُنشئ المحدد - أي النوع الفرعي في الواقع -. النوع الموجود على يمين علامة التساوي هو متعدد الأشكال - أي أنه يمكن أن يتوافق مع عدة أنواع محددة.

إذا حدث التعريف بعد السطر ---functions---، ثم سيظل بناء الجملة كما هو ، لكن المعنى سيكون مختلفًا: سيصبح المُنشئ اسم وظيفة RPC ، وستصبح الحقول معلمات (حسنًا ، أي أنها ستبقى بالضبط نفس البنية المحددة كما هو موضح أدناه ، سيكون فقط المعنى المعطى) ، و "النوع متعدد الأشكال" هو نوع النتيجة التي يتم إرجاعها. صحيح أنه سيظل متعدد الأشكال - تم تعريفه للتو في القسم ---types---، ولن يتم النظر في هذا المنشئ. اكتب الكثير من الوظائف التي تم استدعاؤها بواسطة وسيطاتها ، أي لسبب ما ، لا يتم توفير العديد من الوظائف بنفس الاسم ولكن بتوقيع مختلف ، كما هو الحال في C ++ ، في TL.

لماذا "منشئ" و "متعدد الأشكال" إذا لم يكن OOP؟ حسنًا ، في الواقع ، سيكون من الأسهل على شخص ما التفكير في الأمر من منظور OOP - نوع متعدد الأشكال كفئة مجردة ، والمنشئات هم فئات سليلها المباشر ، علاوة على ذلك final في مصطلحات عدد من اللغات. في الواقع ، بالطبع ، هنا تشابه مع طرق الإنشاء المثقلة بالأعباء الحقيقية في لغات البرمجة OO. نظرًا لوجود هياكل بيانات فقط هنا ، لا توجد طرق (على الرغم من أن وصف الوظائف والأساليب أدناه قادر تمامًا على خلق ارتباك في الرأس حول ماهيتها ، ولكن هذا يتعلق بشيء آخر) - يمكنك التفكير في المُنشئ باعتباره قيمة من خلالها يجري بناؤها اكتب عند قراءة دفق بايت.

كيف يحدث هذا؟ يرى جهاز إلغاء التسلسل ، الذي يقرأ دائمًا 4 بايت ، القيمة 0xcrc32 - ويفهم ما سيحدث بعد ذلك field1 مع النوع int، أي. يقرأ 4 بايت بالضبط ، في هذا الحقل العلوي بالنوع PolymorType يقرأ. يرى 0x2crc32 وتدرك أن هناك مجالين إضافيين ، أولاً long، لذلك نقرأ 8 بايت. ثم مرة أخرى نوع معقد ، يتم فك تسلسله بنفس الطريقة. على سبيل المثال، Type3 يمكن الإعلان عنها في المخطط بمجرد أن يلتقي اثنان من المُنشئين ، على التوالي ، أيضًا 0x12abcd34، وبعد ذلك تحتاج إلى قراءة 4 بايت أخرى intأو 0x6789cdefوبعد ذلك لن يكون هناك شيء. أي شيء آخر - تحتاج إلى طرح استثناء. على أي حال ، بعد ذلك نعود إلى قراءة 4 بايت int الحقول field_c в constructorTwo وعليه ننتهي من قراءة PolymorType.

أخيرًا ، إذا تم القبض عليه 0xdeadcrc إلى constructorThree، ثم تصبح الأمور أكثر تعقيدًا. مجالنا الأول bit_flags_of_what_really_present مع النوع # - في الواقع ، هذا مجرد اسم مستعار للنوع natتعني "العدد الطبيعي". هذا هو ، في الواقع ، int غير الموقعة هي الحالة الوحيدة ، بالمناسبة ، عندما توجد أرقام غير موقعة في مخططات حقيقية. إذن ، التالي هو البناء بعلامة استفهام ، مما يعني أن هذا هو الحقل - سيكون موجودًا على السلك فقط إذا تم تعيين البتة المقابلة في الحقل المشار إليه (تقريبًا مثل المشغل الثلاثي). لذا ، افترض أن هذا الجزء كان قيد التشغيل ، فأنت بحاجة إلى قراءة حقل مثل Type، والتي تحتوي في مثالنا على مُنشئين. أحدهما فارغ (يتكون فقط من معرف) ، والآخر به حقل ids مع النوع ids:Vector<long>.

قد تعتقد أن كلا من القوالب والأدوية جيدة أو جافا. لكن لا. بالكاد. هذا الوحيد حالة الأقواس الزاوية في الدوائر الحقيقية ، وهي تستخدم فقط لـ Vector. في دفق البايت ، سيكون هذا 4 بايت CRC32 لنوع Vector نفسه ، دائمًا نفس الشيء ، ثم 4 بايت - عدد عناصر المصفوفة ، ثم هذه العناصر نفسها.

أضف إلى ذلك حقيقة أن التسلسل يحدث دائمًا بكلمات 4 بايت ، وجميع الأنواع مضاعفات منها - كما تم وصف الأنواع المضمنة bytes и string مع التسلسل اليدوي للطول وهذه المحاذاة بمقدار 4 - حسنًا ، يبدو أنه يبدو طبيعيًا وحتى فعالًا نسبيًا؟ على الرغم من أنه يُزعم أن TL هو تسلسل ثنائي فعال ، ولكن مع توسع أي شيء ، حتى القيم المنطقية والسلاسل أحادية الحرف حتى 4 بايت ، هل ستظل JSON أكثر سمكًا؟ انظر ، حتى الحقول غير الضرورية يمكن تخطيها بعلامات البت ، كل شيء على ما يرام ، وحتى قابل للتوسيع للمستقبل ، هل أضفت حقولًا اختيارية جديدة إلى المُنشئ لاحقًا؟ ..

لكن لا ، إذا لم تقرأ وصفي الموجز ، ولكن التوثيق الكامل ، وفكرت في التنفيذ. أولاً ، يتم حساب CRC32 المُنشئ بواسطة سلسلة وصف نص المخطط المعياري (إزالة المسافة البيضاء الزائدة ، وما إلى ذلك) - لذلك إذا تمت إضافة حقل جديد ، ستتغير سلسلة وصف النوع ، وبالتالي CRC32 ، وبالتالي ، التسلسل. وماذا سيفعل العميل القديم إذا حصل على حقل به مجموعة أعلام جديدة ، لكنه لم يعرف ماذا يفعل بها بعد ذلك؟ ..

ثانيًا ، دعنا نتذكر CRC32، والذي يستخدم هنا بشكل أساسي كـ وظائف التجزئة لتحديد نوع التسلسل الذي يتم (de) بشكل فريد. نحن هنا نواجه مشكلة الاصطدامات - ولا ، الاحتمال ليس واحدًا في 232 ، بل أكثر من ذلك بكثير. من تذكر أن CRC32 مصمم لاكتشاف (وتصحيح) الأخطاء في قناة الاتصال ، وبالتالي تحسين هذه الخصائص على حساب الآخرين؟ على سبيل المثال ، لا تهتم بتبديل البايتات: إذا عدت CRC32 من سطرين ، في الثانية سوف تستبدل أول 4 بايت بالبايتات الأربعة التالية - ستكون هي نفسها. عندما يكون لدينا سلاسل نصية من الأبجدية اللاتينية (وعلامات ترقيم صغيرة) كمدخلات ، وهذه الأسماء ليست عشوائية بشكل خاص ، يزداد احتمال حدوث مثل هذا التقليب بشكل كبير.

بالمناسبة ، من فحص ما كان هناك حقا CRC32؟ في أحد المصادر المبكرة (حتى قبل والتمان) كانت هناك دالة تجزئة ضربت كل حرف بالرقم 239 ، وهو محبوب جدًا من قبل هؤلاء الناس ، ها ها!

أخيرًا ، حسنًا ، أدركنا أن المُنشئين لديهم نوع حقل Vector<int> и Vector<PolymorType> سيكون لها CRC32 مختلفة. وماذا عن العرض على الخط؟ ومن الناحية النظرية ، هل تصبح جزءًا من النوع؟ لنفترض أننا مررنا مصفوفة من عشرة آلاف رقم ، حسنًا ، مع Vector<int> كل شيء واضح ، الطول و 40000 بايت أخرى. وإذا كان هذا Vector<Type2>، والتي تتكون من حقل واحد فقط int وهو الوحيد من النوع - هل نحتاج إلى تكرار 10000xabcdef0 34 مرة ثم 4 بايت int، أو أن اللغة قادرة على عرض هذا لنا من المُنشئ fixedVec وبدلاً من 80000 بايت ، نقل 40000 بايت فقط مرة أخرى؟

هذا ليس سؤالًا نظريًا خاملًا على الإطلاق - تخيل أنك تحصل على قائمة بمستخدمي المجموعة ، كل منهم له معرف ، واسم أول ، واسم عائلة - يمكن أن يكون الاختلاف في كمية البيانات المنقولة عبر اتصال المحمول كبيرًا. يتم الإعلان عن فعالية تسلسل Telegram لنا.

لذلك ...

المتجه الذي لا يمكن استنتاجه

إذا حاولت الخوض في صفحات وصف المجمعات وما حولها ، فسترى أن المتجه (وحتى المصفوفة) يحاول رسميًا استنتاج عدة أوراق من خلال المجموعات. ولكن في النهاية يتم التوصل إليها ، ويتم تخطي الخطوة الأخيرة ، ويتم تقديم تعريف المتجه ببساطة ، وهو أيضًا غير مرتبط بنوع. ما الأمر هنا؟ في اللغات برمجة، خاصة الوظيفية ، من المعتاد وصف البنية بشكل متكرر - فالمجمع بتقييمه البطيء سيفهم كل شيء ويفعله. في اللغة تسلسل البيانات لكن الكفاءة مطلوبة: يكفي وصفها ببساطة قائمة، أي. هيكل من عنصرين - الأول هو عنصر بيانات ، والثاني هو نفس الهيكل نفسه أو مساحة فارغة للذيل (حزمة (cons) في Lisp). لكن من الواضح أن هذا سيتطلب كل بالإضافة إلى ذلك ، ينفق العنصر 4 بايت (CRC32 في حالة TL) لوصف نوعه. من السهل وصف المصفوفة حجم ثابت، ولكن في حالة وجود مصفوفة ذات طول غير معروف سابقًا ، فإننا نفصل.

لذلك نظرًا لأن TL لا تسمح لك بإخراج متجه ، فيجب إضافته على الجانب. في النهاية تقول الوثائق:

يستخدم التسلسل دائمًا نفس المُنشئ "المتجه" (const 0x1cb5c415 = crc32 ("vector t: Type # [t] = Vector t") التي لا تعتمد على القيمة المحددة للمتغير من النوع t.

لا يتم تضمين قيمة المعلمة الاختيارية t في التسلسل نظرًا لأنها مشتقة من نوع النتيجة (المعروفة دائمًا قبل إلغاء التسلسل).

ألق نظرة فاحصة: vector {t:Type} # [ t ] = Vector t - لكن لا مكان التعريف نفسه لا يقول أن الرقم الأول يجب أن يكون مساويًا لطول المتجه! ولا يتبع ذلك من أي مكان. هذا معطى يجب أن تضعه في اعتبارك وتنفيذه بيديك. في مكان آخر ، تشير الوثائق بصراحة إلى أن النوع مزيف:

النمط الزائف المتجه t متعدد الأشكال هو "نوع" قيمته عبارة عن سلسلة من القيم من أي نوع t ، إما محاصر أو مكشوف.

... لكنها لا تركز عليها. عندما تتعب من الخوض في تمدد الرياضيات (ربما تعرفك حتى من دورة جامعية) ، تقرر أن تسجل وتراقب كيف تعمل معها فعليًا في الممارسة العملية ، يبقى الانطباع في رأسك: هنا تستند الرياضيات الجادة ، من الواضح أنهم رائعون (اثنان من علماء الرياضيات - ربحا من ACM) ، وليس أي شخص فقط. الهدف - التفاخر - قد تحقق.

بالمناسبة ، حول الرقم. يتذكر # إنه مرادف nat، عدد طبيعي:

توجد تعابير كتابة (اكتب) والتعبيرات الرقمية (نات إكسبر). ومع ذلك ، يتم تعريفهم بنفس الطريقة.

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

ولكن في القواعد يتم وصفها بنفس الطريقة ، أي يجب تذكر هذا الاختلاف مرة أخرى ووضعه في التنفيذ يدويًا.

حسنًا ، نعم ، أنواع القوالب (vector<int>, vector<User>) لها معرّف مشترك (#1cb5c415)، أي. إذا كنت تعلم أن المكالمة معلنة باسم

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

فأنت لا تنتظر مجرد ناقل ، بل متجه للمستخدمين. أكثر دقة، ينبغي انتظر - في الكود الحقيقي ، سيكون لكل عنصر ، إن لم يكن نوعًا مكشوفًا ، مُنشئًا ، وبطريقة جيدة في التنفيذ ، سيكون من الضروري التحقق - وقد تم إرسالنا بالضبط في كل عنصر من عناصر هذا المتجه هذا النوع؟ وإذا كان نوعًا من PHP ، حيث يمكن أن تحتوي المصفوفة على أنواع مختلفة في عناصر مختلفة؟

في هذه المرحلة ، تبدأ في التساؤل - هل هناك حاجة إلى TL؟ ربما بالنسبة للعربة ، سيكون من الممكن استخدام المسلسل البشري ، وهو نفس الجهاز الذي كان موجودًا بالفعل في ذلك الوقت؟ كانت نظرية ، دعنا نلقي نظرة على الممارسة.

عمليات تنفيذ TL الموجودة في التعليمات البرمجية

وُلد TL في أحشاء فكونتاكتي حتى قبل الأحداث المعروفة ببيع حصة Durov و (بالتأكيد) ، حتى قبل تطوير Telegram. وفي المصدر المفتوح مصادر التنفيذ الأول يمكنك أن تجد الكثير من العكازات المضحكة. وتم تطبيق اللغة نفسها هناك بشكل كامل أكثر مما هي عليه الآن في Telegram. على سبيل المثال ، لا يتم استخدام التجزئة على الإطلاق في المخطط (بمعنى النمط الزائف المدمج (مثل المتجه) مع السلوك المنحرف). أو

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

لكن دعونا نفكر في الصورة من أجل اكتمالها ، من أجل تتبع ، إذا جاز التعبير ، تطور عملاق الفكر.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

أو هذا الجميل:

    static const char *reserved_words_polymorhic[] = {

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

      };

يتعلق هذا الجزء بالقوالب ، مثل:

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

هذا هو تعريف نوع قالب hashmap ، كمتجه لأزواج int - Type. في C ++ سيبدو شيئًا كالتالي:

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

لذا، alpha - كلمة رئيسية! لكن في C ++ فقط ، يمكنك كتابة T ، ولكن عليك كتابة alpha و beta ... ولكن ليس أكثر من 8 معلمات ، انتهى الخيال في ثيتا. لذلك يبدو أنه في يوم من الأيام في سانت بطرسبرغ كان هناك تقريبًا مثل هذه الحوارات:

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

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

ولكن كان الأمر يتعلق بالتنفيذ الأول للغة الهدف "بشكل عام". دعنا ننتقل إلى النظر في عمليات التنفيذ في عملاء Telegram الفعليين.

كلمة باسيل:

فاسيلي ، [09.10.18 17:07] الأهم من ذلك كله ، أن الحمار ساخن من حقيقة أنهم قاموا بفشل مجموعة من التجريدات ، ثم قاموا بدق برغي عليها ، ووضعوا عكازات على أداة الترميز
نتيجة لذلك ، أولاً من أحواض السفن pilot.jpg
ثم من كود jekichan.webp

بالطبع ، من الأشخاص المطلعين على الخوارزميات والرياضيات ، يمكننا أن نتوقع أنهم قد قرأوا Aho ، Ullman ، وهم على دراية بأدوات الصناعة المعيارية الفعلية لكتابة مترجمي DSL على مدى العقود ، أليس كذلك؟ ..

من قبل المؤلف برقية CLI هو فيتالي فالتمان ، كما يمكن فهمه من حدوث تنسيق TLO خارج حدوده (CLI) ، عضو في الفريق - الآن يتم تخصيص مكتبة تحليل TL على حدةما هو انطباعها محلل TL؟ ..

16.12 04:18 فاسيلي: في رأيي ، شخص ما لم يتقن lex + yacc
16.12 04:18 فاسيلي: وإلا لا يمكنني شرح ذلك
16.12 04:18 فاسيلي: حسنًا ، أو تم الدفع لهم مقابل عدد الأسطر في VK
16.12 04:19 فاسيلي: 3 آلاف + سطور أخرى<censored> بدلا من المحلل اللغوي

ربما استثناء؟ دعونا نرى كيف يجعل هذا هو العميل الرسمي - Telegram Desktop:

    nametype = re.match(r'([a-zA-Z.0-9_]+)(#[0-9a-f]+)?([^=]*)=s*([a-zA-Z.<>0-9_]+);', line);
    if (not nametype):
      if (not re.match(r'vector#1cb5c415 {t:Type} # [ t ] = Vector t;', line)):
         print('Bad line found: ' + line);

أكثر من 1100 سطر في Python ، زوجان من التعبيرات العادية + حالات خاصة من نوع المتجه ، والتي ، بالطبع ، يتم الإعلان عنها في المخطط كما ينبغي أن تكون وفقًا لبناء جملة TL ، لكنهم وضعوها على هذا النحو ، قم بتحليلها أكثر ... والسؤال هو لماذا تهتم بكل هذه المعجزةиمزيد من النفخة ، إذا لم يكن أحد سيحللها وفقًا للوثائق على أي حال؟!

بالمناسبة ... تذكر أننا تحدثنا عن الشيك CRC32؟ لذلك ، في منشئ أكواد Telegram Desktop ، توجد قائمة استثناءات لتلك الأنواع التي يتم فيها حساب CRC32 غير مطابق كما هو مبين في الرسم البياني!

فاسيلي ، [18.12 22:49] وهنا يجب أن تفكر فيما إذا كانت هناك حاجة إلى TL
إذا كنت أرغب في العبث بالتطبيقات البديلة ، فسأبدأ في إدخال فواصل الأسطر ، وسيتوقف نصف المحللين عن التعريفات متعددة الأسطر
tdesktop أيضًا

تذكر النقطة المتعلقة بالخط الواحد ، سنعود إليها بعد قليل.

حسنًا ، Telegram-cli غير رسمي ، Telegram Desktop رسمي ، لكن ماذا عن الآخرين؟ ومن يدري؟ .. في كود عميل Android ، لم يكن هناك محلل مخطط على الإطلاق (مما يثير تساؤلات حول المصدر المفتوح ، ولكن هذا للجزء الثاني) ، ولكن كان هناك العديد من الأجزاء المضحكة الأخرى من التعليمات البرمجية ، ولكن عنها في القسم الفرعي أدناه.

ما هي الأسئلة الأخرى التي يثيرها التسلسل في الممارسة؟ على سبيل المثال ، أفسدوا ، بالطبع ، حقول البت والحقول الشرطية:

فاسيلي: flags.0? true
يعني أن الحقل موجود وصحيح إذا تم تعيين العلم

فاسيلي: flags.1? int
يعني أن الحقل موجود ويحتاج إلى إلغاء التسلسل

فاسيلي: الحمار ، لا تحترق ، ماذا تفعل!
فاسيلي: في مكان ما في المستند ، هناك إشارة إلى أن الصواب هو نوع خالٍ من الطول الصفري ، لكن من غير الواقعي جمع شيء من مستنداتهم
فاسيلي: لا يوجد شيء من هذا القبيل في التطبيقات المفتوحة أيضًا ، ولكن هناك الكثير من العكازات والدعامات

ماذا عن Telethon؟ بالنظر إلى موضوع MTProto ، على سبيل المثال - هناك مثل هذه القطع في الوثائق ، ولكن العلامة % يوصف فقط بأنه "مطابق للنوع المكشوف" ، أي في الأمثلة أدناه ، إما خطأ أو شيء غير موثق:

فاسيلي ، [22.06.18/18/38 XNUMX:XNUMX] في مكان واحد:

msg_container#73f1f8dc messages:vector message = MessageContainer;

بشكل مختلف:

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

وهذان اختلافان كبيران ، في الحياة الواقعية يأتي نوع من المتجه العاري

لم أر تعريفات ناقلات عارية ولم أجدها

تحليل مكتوب بخط اليد

علق مخططه خارج التعريف msg_container

مرة أخرى ، يبقى السؤال حول٪. لم يتم وصفه.

فاديم جونشاروف ، [22.06.18/19/22 XNUMX:XNUMX مساءً] وفي tdesktop؟

فاسيلي ، [22.06.18/19/23 XNUMX:XNUMX] لكن المحلل اللغوي TL على المنظمين ربما لن يأكله أيضًا

// parsed manually

TL هو تجريد جميل ، لا أحد ينفذه بالكامل

ولا يوجد٪ في نسختهم من المخطط

لكن هنا الوثائق تناقض نفسها ، لذا فإن xs

تم العثور عليه في القواعد ، يمكنهم فقط نسيان وصف الدلالات

حسنًا ، لقد رأيت قفص الاتهام على TL ، لا يمكنك اكتشافه بدون نصف لتر

"حسنًا ، دعنا نقول ،" سيقول قارئ آخر ، "أنت تنتقد كل شيء ، لذا أظهره كما ينبغي."

يجيب فاسيلي: "أما بالنسبة للمحلل اللغوي ، فأنا بحاجة إلى أشياء مثل

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

بطريقة أو بأخرى أكثر من

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

أو

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

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

هذا هو المعجم الكامل:

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

أولئك. أبسط هو وضعه بشكل معتدل ".

بشكل عام ، في النهاية ، يتناسب المحلل اللغوي ومولد الشفرة للمجموعة الفرعية المستخدمة بالفعل من TL في حوالي 100 سطر من القواعد و 300 سطر تقريبًا من المولد (بما في ذلك جميع printرمز تم إنشاؤه) ، بما في ذلك أنواع الأشياء الجيدة ، واكتب معلومات الاستبطان في كل فئة. يتم تحويل كل نوع متعدد الأشكال إلى فئة أساسية مجردة فارغة ، وترث المنشئات منه ولديها طرق للتسلسل وإلغاء التسلسل.

عدم وجود أنواع في لغة الكتابة

الكتابة القوية جيدة ، أليس كذلك؟ لا ، هذا ليس هوليفار (على الرغم من أنني أفضل اللغات الديناميكية) ، ولكنه افتراض داخل TL. بناءً على ذلك ، يجب أن توفر لنا اللغة جميع أنواع الشيكات. حسنًا ، حسنًا ، لا تدعه ، بل التنفيذ ، لكن يجب أن يصفها على الأقل. وما هي الفرص التي نريدها؟

بادئ ذي بدء ، القيود. هنا نرى في وثائق تحميل الملفات:

ثم يتم تقسيم المحتوى الثنائي للملف إلى أجزاء. يجب أن يكون لجميع الأجزاء نفس الحجم ( Part_size ) ويجب استيفاء الشروط التالية:

  • part_size % 1024 = 0 (قابلة للقسمة على 1 كيلوبايت)
  • 524288 % part_size = 0 (512 كيلو بايت يجب أن يكون قابلاً للقسمة بالتساوي على الحجم الجزئي)

لا يجب أن يستوفي الجزء الأخير هذه الشروط ، بشرط أن يكون حجمه أقل من حجم_جزء.

يجب أن يكون لكل جزء رقم تسلسلي ، ملف_جزء، بقيمة تتراوح من 0 إلى 2,999.

بعد تقسيم الملف ، تحتاج إلى اختيار طريقة لحفظه على الخادم. يستخدم upload.saveBigFilePart في حال كان الحجم الكامل للملف أكبر من 10 ميغا بايت و upload.saveFilePart للملفات الأصغر.
[...] قد يتم إرجاع أحد أخطاء إدخال البيانات التالية:

  • FILE_PARTS_INVALID - عدد الأجزاء غير صالح. القيمة ليست بين 1..3000

هل أي من هذه موجودة في المخطط؟ هل يمكن التعبير عنها بطريقة ما عن طريق TL؟ لا. لكن معذرةً ، حتى توربو باسكال القديم كان قادرًا على وصف الأنواع التي قدمها نطاقات. ويمكنه أن يفعل شيئًا آخر ، يُعرف الآن باسم enum - نوع يتكون من تعداد لعدد ثابت (صغير) من القيم. في لغات مثل C - رقمية ، ضع في اعتبارك أننا تحدثنا فقط عن الأنواع حتى الآن. أعداد. ولكن هناك أيضًا مصفوفات وسلاسل ... على سبيل المثال ، سيكون من الجيد وصف أن هذه السلسلة لا يمكن أن تحتوي إلا على رقم هاتف ، أليس كذلك؟

لا شيء من هذا في TL. ولكن هناك ، على سبيل المثال ، في مخطط JSON. وإذا كان بإمكان شخص آخر الاعتراض على قابلية 512 كيلوبايت للقسمة ، فإن هذا لا يزال بحاجة إلى التحقق في الكود ، فتأكد من أن العميل ببساطة لا يمكن إرسال الرقم خارج النطاق 1..3000 (والخطأ المقابل لا يمكن أن ينشأ) سيكون من الممكن ، أليس كذلك؟ ..

بالمناسبة ، حول الأخطاء وإرجاع القيم. العين غير واضحة حتى بالنسبة لأولئك الذين عملوا مع TL - لم يشرعنا على الفور كل يمكن لوظيفة في TL أن ترجع ليس فقط نوع الإرجاع الموصوف ، ولكن أيضًا خطأ. لكن هذا لا يمكن استنتاجه عن طريق TL نفسه. بالطبع ، هذا مفهوم ولا يعد nafig ضروريًا في الممارسة (على الرغم من أنه في الواقع ، يمكن إجراء RPC بطرق مختلفة ، سنعود إلى هذا) - ولكن ماذا عن نقاء مفاهيم رياضيات الأنواع المجردة من العالم السماوي؟ .. أمسك الساحبة - حتى تطابق.

وأخيرًا ، ماذا عن سهولة القراءة؟ حسنًا ، هناك ، بشكل عام ، أود وصف هل لديك الحق في المخطط (مرة أخرى ، إنه موجود في مخطط JSON) ، ولكن إذا كان متوترًا بالفعل ، فماذا عن الجانب العملي - على الأقل من المبتذل مشاهدة الاختلافات أثناء التحديثات؟ انظر بنفسك في أمثلة حقيقية:

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

أو

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

شخص ما يعجبه ، لكن GitHub ، على سبيل المثال ، يرفض إبراز التغييرات داخل هذه الخطوط الطويلة. لعبة "اعثر على 10 اختلافات" ، وما يراه الدماغ على الفور هو أن البدايات والنهايات هي نفسها في كلا المثالين ، تحتاج إلى قراءة مملة في مكان ما في الوسط ... في رأيي ، هذا ليس من الناحية النظرية فقط ، لكنها تبدو بصرية بحتة قذرة وغير مهذبة.

بالمناسبة ، حول نقاء النظرية. لماذا تحتاج حقول البت؟ لا يبدو أنهم يشم سيئة من وجهة نظر نظرية النوع؟ يمكن رؤية تفسير في الإصدارات السابقة من المخطط. في البداية ، نعم ، كان الأمر كذلك ، تم إنشاء نوع جديد لكل عطسة. هذه الأساسيات لا تزال موجودة في هذا الشكل ، على سبيل المثال:

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;

لكن تخيل الآن إذا كان لديك 5 حقول اختيارية في هيكلك ، فأنت بحاجة إلى 32 نوعًا لجميع الخيارات الممكنة. انفجار اندماجي. لذلك تحطمت نقاء الكريستال لنظرية TL مرة أخرى ضد الحمار المصبوب للحقيقة القاسية للتسلسل.

بالإضافة إلى ذلك ، ينتهك هؤلاء الأشخاص أنفسهم كتاباتهم في الأماكن. على سبيل المثال ، في MTProto (الفصل التالي) يمكن ضغط الاستجابة بواسطة Gzip ، كل شيء معقول - باستثناء انتهاك الطبقات والمخطط. مرة واحدة ، ولم تحصد RpcResult نفسها ، بل محتوياتها. حسنًا ، لماذا تفعل هذا؟ .. اضطررت إلى قطع عكاز حتى يعمل الضغط في أي مكان.

أو مثال آخر ، وجدنا ذات مرة خطأ - تم الإرسال InputPeerUser بدلا من InputUser. أو العكس. لكنها نجحت! أي أن الخادم لم يهتم بالنوع. كيف يمكن أن يكون هذا؟ الإجابة ، ربما ، ستتم المطالبة بها من خلال أجزاء التعليمات البرمجية من telegram-cli:

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

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

بمعنى آخر ، هنا يتم إجراء التسلسل يدويا، لم يتم إنشاء رمز! ربما يتم تنفيذ الخادم بطريقة مماثلة؟ .. من حيث المبدأ ، سيعمل هذا إذا تم تنفيذه مرة واحدة ، ولكن كيف يمكنك دعمه لاحقًا بالتحديثات؟ أليس هذا ما كان من أجله المخطط؟ ثم ننتقل إلى السؤال التالي.

الإصدار. طبقات

لا يمكن تخمين سبب تسمية إصدارات المخطط للطبقات إلا بناءً على محفوظات المخططات المنشورة. على ما يبدو ، بدا للمؤلفين في البداية أنه يمكن القيام بالأشياء الأساسية وفقًا لمخطط غير متغير ، وعند الضرورة فقط ، قم بالإشارة إلى طلبات محددة يتم إجراؤها وفقًا لإصدار مختلف. من حيث المبدأ ، حتى الفكرة الجيدة - والإرادة الجديدة ، كما كانت ، "تندمج" ، تطبّق على القديم. لكن دعونا نرى كيف تم ذلك. صحيح ، لم يكن من الممكن النظر من البداية - إنه أمر مضحك ، لكن مخطط الطبقة الأساسية ببساطة غير موجود. بدأت الطبقات في 2. تخبرنا الوثائق عن ميزة TL الخاصة:

إذا كان العميل يدعم الطبقة 2 ، فيجب استخدام المُنشئ التالي:

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

في الممارسة العملية ، هذا يعني أنه قبل كل استدعاء API ، int مع القيمة 0x289dd1f6 يجب إضافته قبل رقم الطريقة.

يبدو موافق. لكن ماذا حدث بعد ذلك؟ ثم جاء

invokeWithLayer3#b7475268 query:!X = X;

إذن ما هي الخطوة التالية؟ كما هو سهل التخمين

invokeWithLayer4#dea0d430 query:!X = X;

مضحك؟ لا ، من السابق لأوانه الضحك ، فكر فيما كل يجب تغليف طلب من طبقة أخرى بنوع خاص من هذا القبيل - إذا كان لديك كل منهم مختلف ، فكيف تميزهم؟ وتعد إضافة 4 بايت فقط في المقدمة طريقة فعالة جدًا. لذا

invokeWithLayer5#417a57ae query:!X = X;

لكن من الواضح أنه بعد فترة من الوقت سيصبح بعض السكريات. وجاء الحل:

التحديث: بدءًا من الطبقة 9 ، الطرق المساعدة invokeWithLayerN يمكن استخدامها مع ملفات initConnection

الصيحة! بعد 9 إصدارات ، توصلنا أخيرًا إلى ما تم إنجازه في بروتوكولات الإنترنت في الثمانينيات - التفاوض على الإصدار مرة واحدة في بداية الاتصال!

فماذا بعد؟ ..

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

والآن يمكنك الضحك. فقط بعد 9 طبقات أخرى ، تمت إضافة مُنشئ عام برقم إصدار أخيرًا ، والذي يجب أن يتم استدعاؤه مرة واحدة فقط في بداية الاتصال ، ويبدو أن المعنى في الطبقات قد اختفى ، والآن أصبح مجرد إصدار شرطي ، مثل فى اى مكان اخر. تم حل المشكلة.

يمين؟..

فاسيلي ، [16.07.18/14/01 XNUMX:XNUMX مساءً] يوم الجمعة فكرت:
يرسل الخادم عن بُعد الأحداث دون طلب. يجب تغليف الطلبات في InvokeWithLayer. لا يقوم الخادم بالتفاف التحديثات ، ولا توجد بنية لتغليف الاستجابات والتحديثات.

أولئك. لا يمكن للعميل تحديد الطبقة التي يريد التحديثات فيها

فاديم غونشاروف ، [16.07.18/14/02 XNUMX:XNUMX مساءً] Isn't InvokeWithLayer عكاز من حيث المبدأ؟

فاسيلي ، [16.07.18/14/02 XNUMX:XNUMX مساءً] هذه هي الطريقة الوحيدة

فاديم غونشاروف ، [16.07.18/14/02 XNUMX:XNUMX مساءً] والذي يعني في جوهره وضع طبقات في بداية الجلسة

بالمناسبة ، يترتب على ذلك عدم توفير الرجوع إلى إصدار أقدم للعميل

التحديثات ، أي يكتب Updates في المخطط ، هذا هو ما يرسله الخادم إلى العميل ليس استجابةً لطلب واجهة برمجة التطبيقات ، ولكن من تلقاء نفسه عند حدوث حدث. هذا موضوع معقد سيتم مناقشته في منشور آخر ، ولكن في الوقت الحالي من المهم معرفة أن الخادم يقوم بتجميع التحديثات حتى عندما يكون العميل غير متصل بالإنترنت.

وهكذا ، عند رفض الالتفاف كل الحزمة للإشارة إلى نسختها ، ومن ثم تنشأ المشاكل المحتملة التالية منطقيًا:

  • يرسل الخادم تحديثات إلى العميل قبل أن يخبر العميل الإصدار الذي يدعمه
  • ما الذي يجب عمله بعد ترقية العميل؟
  • الذي ضماناتأن رأي الخادم حول رقم الطبقة لن يتغير في العملية؟

هل تعتقد أن هذا تفكير نظري بحت ، وعمليًا لا يمكن أن يحدث هذا ، لأن الخادم مكتوب بشكل صحيح (على أي حال ، تم اختباره جيدًا)؟ ها! لا يهم كيف!

هذا بالضبط ما واجهناه في أغسطس. في 14 أغسطس ، ظهرت رسائل تفيد بأنه تم تحديث شيء ما على خوادم Telegram ... ثم في السجلات:

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.

ثم بضعة ميغا بايت من آثار المكدس (حسنًا ، في نفس الوقت ، تم إصلاح التسجيل). بعد كل شيء ، إذا لم يتم التعرف على شيء ما في TL الخاص بك - فهو ثنائي بالتوقيعات ، وكذلك في الدفق الجميع يذهب ، سيصبح فك التشفير مستحيلاً. ماذا تفعل في مثل هذه الحالة؟

حسنًا ، أول ما يتبادر إلى ذهن أي شخص هو قطع الاتصال والمحاولة مرة أخرى. لم يساعد. بحثنا على googled CRC32 - تبين أنها كائنات من المخطط 73 ، على الرغم من أننا عملنا على المخطط 82. نحن ننظر بعناية إلى السجلات - هناك معرفات من نظامين مختلفين!

ربما المشكلة محضة في عميلنا غير الرسمي؟ لا ، نحن نشغل Telegram Desktop 1.2.17 (الإصدار المزود بعدد من توزيعات Linux) ، يكتب في سجل الاستثناءات: MTP Unlimited type id # b5223b0f يُقرأ في MTPMessageMedia ...

انتقاد البروتوكول والمنهج التنظيمي لـ Telegram. الجزء الأول ، تقني: تجربة كتابة عميل من الصفر - TL، MT

أظهرت Google أن مشكلة مماثلة حدثت بالفعل لأحد العملاء غير الرسميين ، ولكن بعد ذلك كانت أرقام الإصدارات ، وبالتالي ، كانت الافتراضات مختلفة ...

اذا مالعمل؟ انقسمت أنا وفاسيلي: لقد حاول تحديث المخطط إلى 91 ، قررت الانتظار بضعة أيام ومحاولة تنفيذ 73. نجحت كلتا الطريقتين ، ولكن نظرًا لأنهما تجريبية ، فلا يوجد فهم لعدد الإصدارات التي تحتاجها أو أسفل ، ولا كم من الوقت عليك الانتظار.

في وقت لاحق ، تمكنت من إعادة إنتاج الموقف: بدأنا العميل ، وأوقفنا تشغيله ، وأعدنا ترجمة المخطط إلى طبقة أخرى ، وأعد تشغيله ، واكتشف المشكلة مرة أخرى ، والعودة إلى السابقة - عفوًا ، لا تبديل المخطط وإعادة تشغيل العميل لعدة مرات دقيقة سوف تساعد. ستتلقى مزيجًا من هياكل البيانات من طبقات مختلفة.

توضيح؟ كما يمكنك التخمين من خلال الأعراض غير المباشرة المختلفة ، يتكون الخادم من العديد من أنواع العمليات المختلفة على أجهزة مختلفة. على الأرجح ، وضع أحد الخوادم المسؤولة عن "التخزين المؤقت" في قائمة الانتظار ما أعطاه الأعلىون ، وقاموا بإعطائه في المخطط الذي كان في وقت التوليد. وحتى تصبح قائمة الانتظار "فاسدة" ، لا يمكن فعل أي شيء حيال ذلك.

ما لم ... ولكن هذا عكاز رهيب؟! .. لا ، قبل التفكير في الأفكار المجنونة ، لنلق نظرة على كود العملاء الرسميين. في إصدار Android ، لم نجد أي محلل TL ، لكننا نجد ملفًا ضخمًا (يرفض جيثب تلوينه) مع (de) التسلسل. فيما يلي مقتطفات التعليمات البرمجية:

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

أو

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

حسنًا ... يبدو الأمر مجنونًا. لكن ، على الأرجح ، هذا رمز تم إنشاؤه ، إذن حسنًا؟ .. لكنه بالتأكيد يدعم جميع الإصدارات! صحيح ، ليس من الواضح سبب اختلاط كل شيء في كومة واحدة ، والمحادثات السرية ، وكل أنواع المحادثات _old7 إلى حد ما لا يشبه جيل الآلة ... ومع ذلك ، فقد ذهبت من المكسرات

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

يا رفاق ، ألا يمكنك حتى أن تقرر داخل طبقة واحدة؟! حسنًا ، حسنًا ، "اثنان" ، دعنا نقول ، تم إطلاق سراحهما مع وجود خطأ ، حسنًا ، هذا يحدث ، ولكن ثلاثة؟ .. على الفور مرة أخرى على نفس أشعل النار؟ أي نوع من المواد الإباحية هذا ، آسف؟ ..

بالمناسبة ، يحدث شيء مشابه في مصادر Telegram Desktop - إذا كان الأمر كذلك ، فإن العديد من الالتزامات المتتالية للمخطط لا تغير رقم طبقتها ، بل تصلح شيئًا ما. في حالة عدم وجود مصدر بيانات رسمي للنظام ، من أين يمكنني الحصول عليه ، باستثناء مصادر العملاء الرسمية؟ وأنت تأخذها من هناك ، لا يمكنك التأكد من أن المخطط صحيح تمامًا حتى تختبر جميع الطرق.

كيف يمكن حتى اختبار هذا؟ آمل أن يشارك عشاق اختبارات الوحدة والوظيفية وغيرها في التعليقات.

حسنًا ، لنلقِ نظرة على جزء آخر من التعليمات البرمجية:

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;

يشير هذا التعليق "الذي تم إنشاؤه يدويًا" هنا إلى أن جزءًا فقط من هذا الملف مكتوب يدويًا (هل يمكنك تخيل كابوس الصيانة؟) والباقي يتم إنشاؤه آليًا. ومع ذلك ، هناك سؤال آخر يطرح نفسه - أن المصادر متاحة ليس تماما (a la blobs under GPL في Linux kernel) ، لكن هذا موضوع بالفعل للجزء الثاني.

لكن يكفي. دعنا ننتقل إلى البروتوكول الذي يتم على رأسه كل هذا التسلسل.

MTProto

لذلك دعونا نفتح وصف عام и وصف مفصل للبروتوكول وأول شيء نتعثر فيه هو المصطلحات. ومع وفرة من كل شيء. بشكل عام ، يبدو أن هذه علامة تجارية لـ Telegram - لاستدعاء الأشياء في أماكن مختلفة بطرق مختلفة ، أو أشياء مختلفة في كلمة واحدة ، أو العكس (على سبيل المثال ، في واجهة برمجة تطبيقات عالية المستوى إذا رأيت حزمة ملصقات - هذا ليس كما كنت تعتقد).

على سبيل المثال ، "message" (رسالة) و "جلسة" (جلسة) - يعنيان هنا شيئًا مختلفًا عن الواجهة المعتادة لعميل Telegram. حسنًا ، كل شيء واضح مع الرسالة ، يمكن تفسيره من حيث OOP ، أو ببساطة يطلق عليه كلمة "package" - هذا مستوى نقل منخفض ، لا توجد نفس الرسائل كما في الواجهة ، هناك الكثير من تلك الخدمة. لكن الجلسة ... لكن أول الأشياء أولاً.

طبقة النقل

أول شيء هو النقل. سيتم إخبارنا عن 5 خيارات:

  • TCP
  • مقبس ويب
  • Websocket عبر HTTPS
  • HTTP
  • HTTPS

فاسيلي ، [15.06.18/15/04 XNUMX:XNUMX مساءً] وهناك أيضًا نقل UDP ، لكن لم يتم توثيقه

و TCP في ثلاثة متغيرات

الأول مشابه لـ UDP عبر TCP ، كل حزمة تتضمن رقم تسلسلي و crc
لماذا هو مؤلم جدا قراءة الأرصفة على عربة؟

حسنا هناك الآن TCP بالفعل في 4 متغيرات:

  • موجز
  • متوسط
  • وسيط مبطن
  • طويل

حسنًا ، وسيط مبطن لـ MTProxy ، تمت إضافة هذا لاحقًا بسبب الأحداث المعروفة. ولكن لماذا نسختين أخريين (ثلاثة في المجموع) ، في حين أن أحدهما يمكن أن يفعل؟ تختلف العناصر الأربعة بشكل أساسي فقط في كيفية تعيين الطول والحمولة الصافية لـ MTProto الرئيسي نفسه ، والذي سيتم مناقشته بمزيد من التفصيل:

  • باختصار ، يكون 1 أو 4 بايت ولكن ليس 0xef ثم الجسم
  • في المتوسط ​​، هذا طوله 4 بايت وحقل ، وأول مرة يجب على العميل إرسالها 0xeeeeeeee للإشارة إلى أنه متوسط
  • بشكل كامل ، الأكثر إدمانًا ، من وجهة نظر المسوق الشبكي: الطول ، رقم التسلسل ، وليس الشيء الذي هو أساسًا MTProto ، الجسم ، CRC32. نعم ، كل هذا عبر TCP. مما يوفر لنا نقلًا موثوقًا به في شكل دفق تسلسلي من البايتات ، لا نحتاج إلى تسلسلات ، خاصةً المجاميع الاختبارية. حسنًا ، سأعترض الآن على أن TCP لديه مجموع اختباري 16 بت ، لذلك يحدث تلف في البيانات. رائع ، باستثناء أن لدينا بالفعل بروتوكول تشفير به تجزئة أطول من 16 بايت ، كل هذه الأخطاء - وأكثر من ذلك - سيتم اكتشافها عند عدم تطابق SHA على مستوى أعلى. ليس هناك فائدة في CRC32 من هذا.

دعنا نقارن الموجز ، حيث يكون بايت واحد من الطول ممكنًا ، مع الوسيط ، والذي يبرر "في حالة الحاجة إلى محاذاة البيانات 4 بايت" ، وهذا هراء إلى حد ما. ما الذي يُعتقد أن مبرمجي Telegram أخرقون جدًا لدرجة أنهم لا يستطيعون قراءة البيانات من المقبس إلى مخزن مؤقت محاذي؟ لا يزال يتعين عليك القيام بذلك ، لأن القراءة يمكن أن تعيد لك أي عدد من البايتات (وهناك أيضًا خوادم بروكسي ، على سبيل المثال ...). أو ، من ناحية أخرى ، لماذا تهتم بـ Abridged إذا كان لا يزال لدينا حشوات ضخمة من 16 بايت في الأعلى - وفر 3 بايت أحيانا ?

لدى المرء انطباع بأن نيكولاي دوروف مغرم جدًا باختراع الدراجات ، بما في ذلك بروتوكولات الشبكة ، دون الحاجة العملية الحقيقية.

خيارات النقل الأخرى ، بما في ذلك. الويب و MTProxy ، لن نفكر الآن ، ربما في منشور آخر ، إذا كان هناك طلب. سنتذكر الآن فقط عن MTProxy هذا أنه بعد وقت قصير من إطلاقه في 2018 ، سرعان ما تعلم مقدمو الخدمة حظره بالضبط ، المقصود به تجاوز كتلةبواسطة حجم الحزمة! وأيضًا حقيقة أن خادم MTProxy المكتوب (مرة أخرى بواسطة Waltman) في C كان مرتبطًا بشكل غير ضروري بتفاصيل Linux ، على الرغم من أنه لم يكن مطلوبًا على الإطلاق (سيؤكد Phil Kulin) ، وأن خادمًا مشابهًا سواء على Go أو على Node.js تناسب أقل من مائة سطر.

لكننا سنستخلص استنتاجات حول محو الأمية التقنية لهؤلاء الأشخاص في نهاية القسم ، بعد النظر في قضايا أخرى. في الوقت الحالي ، دعنا ننتقل إلى طبقة OSI الخامسة ، الجلسة - التي وضعوا فيها جلسة MTProto.

مفاتيح ، رسائل ، جلسات ، ديفي هيلمان

لقد وضعوا ذلك بشكل غير صحيح تمامًا ... الجلسة ليست هي نفس الجلسة التي تظهر في الواجهة ضمن الجلسات النشطة. لكن بالترتيب.

انتقاد البروتوكول والمنهج التنظيمي لـ Telegram. الجزء الأول ، تقني: تجربة كتابة عميل من الصفر - TL، MT

هنا تلقينا سلسلة من البايتات ذات الطول المعروف من طبقة النقل. هذه إما رسالة مشفرة أو نص عادي - إذا كنا لا نزال في مرحلة التفاوض الرئيسية ونقوم بذلك بالفعل. أي مجموعة من المفاهيم تسمى "مفتاح" نتحدث عنها؟ دعنا نوضح هذه المشكلة لفريق Telegram نفسه (أعتذر عن ترجمة وثائقي الخاص من الإنجليزية إلى دماغ متعب في الرابعة صباحًا ، كان من الأسهل ترك بعض العبارات كما هي):

هناك نوعان من الكيانات تسمى الجلسة - واحد في واجهة المستخدم للعملاء الرسميين ضمن "الجلسات الحالية" ، حيث تتوافق كل جلسة مع جهاز / نظام تشغيل كامل.
الثاني - جلسة MTProto، الذي يحتوي على رقم تسلسل رسالة (بمعنى منخفض المستوى) فيه ، وأي قد تدوم بين اتصالات TCP المختلفة. يمكن إعداد عدة جلسات MTProto في نفس الوقت ، على سبيل المثال ، لتسريع تنزيلات الملفات.

بين هذين دورات هو المفهوم ترخيص. في حالة الانحطاط ، يمكن للمرء أن يقول ذلك جلسة واجهة المستخدم بالضبط مثل ترخيصلكن للأسف ، الأمر معقد. نحن ننظر:

  • ينشئ المستخدم على الجهاز الجديد أولاً مفتاح المصادقة وتقيدها بالحساب ، على سبيل المثال ، عن طريق الرسائل القصيرة - لهذا السبب ترخيص
  • حدث ذلك داخل الأول جلسة MTProtoالتي لديها session_id داخل نفسك.
  • في هذه الخطوة ، يتم الجمع ترخيص и session_id يمكن أن يسمى مثل - تم العثور على هذه الكلمة في وثائق ورموز بعض العملاء
  • بعد ذلك ، يمكن للعميل فتح ملفات بعض جلسات MTProto تحت نفس الشيء مفتاح المصادقة - لنفس العاصمة.
  • ثم في يوم من الأيام يحتاج العميل إلى طلب ملف من آخر DC - وسيتم إنشاء واحدة جديدة لهذا العاصمة مفتاح المصادقة !
  • لإخبار النظام أن هذا ليس مستخدمًا جديدًا يقوم بالتسجيل ، بل هو نفسه ترخيص (جلسة واجهة المستخدم) ، يستخدم العميل مكالمات API auth.exportAuthorization في المنزل DC auth.importAuthorization في العاصمة الجديدة.
  • كل نفس ، قد يكون هناك عدة مفتوحة جلسات MTProto (لكل منها ما يخصها session_id) إلى هذا DC الجديد ، تحت له مفتاح المصادقة.
  • أخيرًا ، قد يرغب العميل في Perfect Forward Secrecy. كل مفتاح المصادقة كان دائم key - لكل DC - ويمكن للعميل الاتصال auth.bindTempAuthKey للاستخدام مؤقت مفتاح المصادقة - ومرة ​​أخرى ، واحد فقط temp_auth_key لكل DC ، مشترك للجميع جلسات MTProto إلى هذا DC.

نلاحظ أن ملح (وأملاح المستقبل) أيضًا مفتاح المصادقة أولئك. مشتركة بين الجميع جلسات MTProto لنفس العاصمة.

ماذا تعني عبارة "بين اتصالات TCP المختلفة"؟ هذا يعني أن هذا شيء مثل ملف تعريف ارتباط الترخيص على موقع ويب - يستمر (ينجو) من العديد من اتصالات TCP إلى هذا الخادم ، ولكنه سيصبح سيئًا في يوم من الأيام. فقط على عكس HTTP ، في MTProto ، داخل الجلسة ، يتم ترقيم الرسائل وتأكيدها بالتسلسل ، ودخلوا النفق ، وانقطع الاتصال - بعد إنشاء اتصال جديد ، سيرسل الخادم كل شيء في هذه الجلسة لم يسلمه في اتصال TCP السابق.

ومع ذلك ، فإن المعلومات الواردة أعلاه هي ضغوط بعد عدة أشهر من التقاضي. في غضون ذلك ، هل نقوم بتنفيذ عملائنا من الصفر؟ - لنعد إلى البداية.

لذلك نحن نولد auth_key في إصدارات Diffie-Hellman من Telegram. دعنا نحاول فهم التوثيق ...

فاسيلي ، [19.06.18/20/05 1:255] data_with_hash: = SHAXNUMX (بيانات) + بيانات + (أي بايت عشوائي) ؛ بحيث يكون الطول XNUMX بايت ؛
encrypted_data: = RSA (data_with_hash، server_public_key) ؛ يتم رفع رقم طويل 255 بايت (كبير endian) إلى القوة المطلوبة على المعامل المطلوب ، ويتم تخزين النتيجة كرقم 256 بايت.

لقد حصلوا على بعض المنشطات DH

لا يبدو وكأنه شخص سليم DH
لا يوجد مفتاحان عامان في dx

حسنًا ، في النهاية ، اكتشفنا ذلك ، لكن الرواسب بقيت - دليلًا على العمل قام به العميل أنه كان قادرًا على تحليل الرقم. نوع الحماية ضد هجمات DoS. ويتم استخدام مفتاح RSA مرة واحدة فقط في اتجاه واحد ، بشكل أساسي للتشفير new_nonce. ولكن بينما تنجح هذه العملية التي تبدو بسيطة ، ما الذي عليك مواجهته؟

فاسيلي ، [20.06.18/00/26 XNUMX:XNUMX] لم أصل إلى طلب تقديم الطلب بعد

لقد أرسلت طلبًا إلى DH

وفي قفص الاتهام على النقل ، تمت كتابة أنه يمكنه الإجابة بـ 4 بايت من رمز الخطأ. وهذا كل شيء

حسنًا ، قال لي -404 ، فماذا في ذلك؟

ها أنا ذا: "امسك efigna الخاص بك المشفر بمفتاح الخادم ببصمة كذا وكذا ، أريد DH" ، ويستجيب بغباء 404

ما رأيك في استجابة الخادم هذه؟ ما يجب القيام به؟ ليس هناك من يسأل (ولكن المزيد عن ذلك في الجزء الثاني).

هنا كل الاهتمام في قفص الاتهام هو القيام به

ليس لدي أي شيء آخر أفعله ، لقد حلمت فقط بتحويل الأرقام ذهابًا وإيابًا

رقمان 32 بت. حزمتهم مثل أي شخص آخر

لكن لا ، هذان هما ما تحتاجه أولاً في السطر كـ BE

فاديم جونشاروف ، [20.06.18/15/49 404:XNUMX مساءً] وبسبب هذا XNUMX؟

فاسيلي ، [20.06.18/15/49 XNUMX:XNUMX مساءً] نعم!

فاديم غونشاروف ، [20.06.18/15/50 XNUMX:XNUMX مساءً] لذلك لا أفهم ما يمكنه "لم يجد"

فاسيلي ، [20.06.18 15:50] حول

لم أجد مثل هذا التحلل إلى قواسم بسيطة٪)

حتى الإبلاغ عن الخطأ لم يتقن

فاسيلي ، [20.06.18/20/18 5:XNUMX مساءً] أوه ، هناك أيضًا MDXNUMX. بالفعل ثلاث تجزئات مختلفة

يتم حساب بصمة المفتاح على النحو التالي:

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

SHA1 و sha2

لذلك دعونا نضع auth_key حصلنا على حجم 2048 بت وفقًا لـ Diffie-Hellman. ماذا بعد؟ ثم اكتشفنا أن أقل 1024 بتًا من هذا المفتاح لا يتم استخدامها بأي شكل من الأشكال ... ولكن دعنا نفكر في هذا الآن. في هذه الخطوة ، لدينا سر مشترك مع الخادم. تم إنشاء تناظرية لجلسة TLS ، وهو إجراء مكلف للغاية. لكن الخادم لا يعرف شيئًا عن هويتنا حتى الآن! ليس بعد ، في الواقع تفويض. أولئك. إذا كنت تفكر من حيث "كلمة مرور تسجيل الدخول" ، كما اعتادت أن تكون في ICQ ، أو على الأقل "مفتاح تسجيل الدخول" ، كما هو الحال في SSH (على سبيل المثال ، في بعض gitlab / github). حصلنا على مجهول. وإذا أجابنا الخادم "يتم تقديم أرقام الهواتف هذه من قبل DC آخر"؟ أو حتى "رقم هاتفك ممنوع"؟ أفضل شيء يمكننا القيام به هو حفظ المفتاح على أمل أن يظل مفيدًا ولن يفسد بحلول ذلك الوقت.

بالمناسبة ، "تلقيناها" مع بعض التحفظات. على سبيل المثال ، هل نثق في الخادم؟ هل هو مزيف؟ نحتاج إلى فحوصات تشفير:

فاسيلي ، [21.06.18/17/53 2:XNUMX مساءً] يقدمون لعملاء الهاتف المحمول التحقق من رقم XNUMX كيلو بت من أجل البساطة ٪)

لكن الأمر غير واضح على الإطلاق يا نافيجوا

فاسيلي ، [21.06.18/18/02 XNUMX:XNUMX] الرصيف لا يقول ما يجب فعله إذا اتضح أنه ليس بسيطًا

لم يقل. دعونا نرى ما يفعله العميل الرسمي لنظام Android في هذه الحالة؟ أ وهذا ما (ونعم ، الملف بأكمله مثير للاهتمام هناك) - كما يقولون ، سأتركه هنا:

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

لا ، بالطبع هناك بعض هناك عمليات تدقيق لبساطة الرقم ، لكنني شخصياً لم أعد أمتلك معرفة كافية في الرياضيات.

حسنًا ، حصلنا على المفتاح الرئيسي. لتسجيل الدخول ، أي إرسال الطلبات ، من الضروري إجراء مزيد من التشفير ، باستخدام AES بالفعل.

يتم تعريف مفتاح الرسالة على أنه 128 بت وسطى من SHA256 من نص الرسالة (بما في ذلك الجلسة ، ومعرف الرسالة ، وما إلى ذلك) ، بما في ذلك وحدات البايت المتروكة ، والمقدمة مسبقًا بـ 32 بايت مأخوذة من مفتاح التفويض.

فاسيلي ، [22.06.18/14/08 XNUMX:XNUMX مساءً] فتيات متوسطات

تلقى auth_key. الجميع. مزيد منهم ... ليس واضحا من الاحواض. لا تتردد في دراسة الكود مفتوح المصدر.

لاحظ أن MTProto 2.0 يتطلب مساحة من 12 إلى 1024 بايت ، ولا يزال يخضع لشرط أن يكون طول الرسالة الناتجة قابلاً للقسمة على 16 بايت.

إذن ما مقدار الحشو لوضعه؟

ونعم هنا أيضًا 404 في حالة حدوث خطأ

إذا درس شخص ما الرسم التخطيطي ونص الوثائق بعناية ، فقد لاحظ أنه لا يوجد MAC هناك. ويتم استخدام AES في بعض أوضاع IGE التي لا يتم استخدامها في أي مكان آخر. إنهم ، بالطبع ، يكتبون عنها في الأسئلة الشائعة الخاصة بهم ... هنا ، على سبيل المثال ، مفتاح الرسالة نفسه هو في نفس الوقت تجزئة SHA للبيانات التي تم فك تشفيرها المستخدمة للتحقق من التكامل - وفي حالة عدم التطابق ، يتم توثيق سبب ما يوصي بتجاهلهم بصمت (لكن ماذا عن الأمن ، يكسرنا فجأة؟).

أنا لست خبير تشفير ، ربما في هذا الوضع في هذه الحالة لا يوجد شيء خطأ من الناحية النظرية. لكن يمكنني بالتأكيد تسمية مشكلة عملية ، باستخدام مثال Telegram Desktop. يقوم بتشفير ذاكرة التخزين المؤقت المحلية (كل هذه D877F783D5D3EF8C) بنفس طريقة الرسائل في MTProto (فقط في هذه الحالة ، الإصدار 1.0) ، أي أولاً ، مفتاح الرسالة ، ثم البيانات نفسها (وفي مكان ما بعيدًا عن المفتاح الكبير auth_key 256 بايت ، بدونها msg_key عديم الفائدة). لذلك ، تصبح المشكلة ملحوظة في الملفات الكبيرة. أي أنك تحتاج إلى الاحتفاظ بنسختين من البيانات - مشفرة ومفككة. وإذا كان هناك ميغا بايت ، أو دفق الفيديو ، على سبيل المثال؟ .. المخططات الكلاسيكية مع MAC بعد النص المشفر تسمح لك بقراءته متدفقة ، ونقله على الفور. ومع MTProto عليك ذلك في البداية تشفير الرسالة بأكملها أو فك تشفيرها ، وعندها فقط قم بنقلها إلى الشبكة أو إلى القرص. لذلك ، في أحدث إصدارات Telegram Desktop في ذاكرة التخزين المؤقت بتنسيق user_data تم استخدام تنسيق آخر بالفعل - مع AES في وضع CTR.

فاسيلي ، [21.06.18/01/27 20:XNUMX صباحًا] أوه ، لقد اكتشفت ما هو IGE: كانت IGE أول محاولة في "وضع تشفير للمصادقة" ، أصلاً لـ Kerberos. لقد كانت محاولة فاشلة (لا توفر حماية السلامة) ، وكان لا بد من إزالتها. كانت تلك بداية بحث لمدة XNUMX عامًا عن وضع تشفير للمصادقة يعمل ، والذي بلغ ذروته مؤخرًا في أوضاع مثل OCB و GCM.

والآن الحجج من جانب العربة:

يتكون الفريق وراء Telegram ، بقيادة نيكولاي دوروف ، من ستة أبطال ACM ، نصفهم دكتوراه في الرياضيات. استغرق الأمر منهم حوالي عامين لإطلاق الإصدار الحالي من MTProto.

ما هو مضحك. سنتان على المستوى الأدنى

أو يمكننا فقط أخذ tls

حسنًا ، لنفترض أننا قمنا بالتشفير والفروق الدقيقة الأخرى. هل يمكننا أخيرًا إرسال طلبات تسلسلية TL وإلغاء تسلسل الردود؟ إذن ما الذي يجب إرساله وكيف؟ ها هي الطريقة initConnectionربما هذا هو؟

Vasily ، [25.06.18/18/46 XNUMX:XNUMX مساءً] يقوم بتهيئة الاتصال وحفظ المعلومات على جهاز المستخدم والتطبيق.

يقبل app_id و device_model و system_version و app_version و lang_code.

وبعض الاستفسار

التوثيق كالعادة. لا تتردد في دراسة المصدر المفتوح

إذا كان كل شيء واضحًا تقريبًا مع InvokeWithLayer ، فما هو؟ اتضح أنه لنفترض أن لدينا - كان لدى العميل بالفعل شيء يسأل الخادم عنه - هناك طلب أردنا إرساله:

فاسيلي ، [25.06.18/19/13 XNUMX:XNUMX] استنادًا إلى الكود ، يتم تغليف المكالمة الأولى في هذه القمامة ، والقمامة نفسها في حالة الطرد

لماذا لا يمكن أن تكون initConnection اتصالاً منفصلاً ، ولكن يجب أن تكون مجمِّعًا؟ نعم ، كما اتضح فيما بعد ، يجب أن يتم ذلك في كل مرة في بداية كل جلسة ، وليس مرة واحدة ، كما هو الحال مع المفتاح الرئيسي. لكن! لا يمكن استدعاؤه من قبل مستخدم غير مصرح له! لقد وصلنا هنا إلى المرحلة التي يكون فيها قابلاً للتطبيق هذا واحد صفحة التوثيق - وتخبرنا أن ...

يتوفر جزء صغير فقط من طرق واجهة برمجة التطبيقات للمستخدمين غير المصرح لهم:

  • المصادقة
  • Author.resendCode
  • account.getPassword
  • المصادقة
  • المصادقة
  • المصادقة
  • المصادقة
  • المصادقة ، الاستيراد ، الإذن
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

أولهم auth.sendCode، وهناك هذا الطلب الأول العزيز الذي سنرسل فيه api_id و api_hash ، وبعد ذلك نتلقى رسالة نصية قصيرة تحتوي على رمز. وإذا وصلنا إلى DC الخطأ (يتم تقديم أرقام هواتف هذه الدولة من قبل دولة أخرى ، على سبيل المثال) ، فسنحصل على خطأ في رقم DC المطلوب. لمعرفة عنوان IP الذي نحتاج إلى الاتصال به عن طريق رقم DC ، سنساعدنا help.getConfig. بمجرد أن كان هناك 5 إدخالات فقط ، ولكن بعد الأحداث المعروفة لعام 2018 ، زاد العدد بشكل كبير.

الآن دعونا نتذكر أننا وصلنا في هذه المرحلة إلى الخادم المجهول. أليس الحصول على عنوان IP مكلف للغاية؟ لماذا لا تفعل هذا ، وعمليات أخرى ، في الجزء غير المشفر من MTProto؟ أسمع اعتراضًا: "كيف يمكنك التأكد من أنه ليس RKN هو الذي سيستجيب بعناوين مزيفة؟". لهذا نذكر أنه ، في الواقع ، في العملاء الرسميين مفاتيح RSA المضمنة، أي. يمكنك فقط لافتة هذه المعلومة. في الواقع ، تم إجراء ذلك بالفعل للحصول على معلومات حول تجاوز الأقفال التي يتلقاها العملاء من خلال قنوات أخرى (من المنطقي ألا يتم ذلك في MTProto نفسه ، لأنك لا تزال بحاجة إلى معرفة مكان الاتصال).

نعم. في هذه المرحلة من تفويض العميل ، لم يتم التصريح لنا ولم نسجل طلبنا. نريد فقط أن نرى الآن ما يستجيب الخادم للطرق المتاحة لمستخدم غير مصرح له. و هنا…

فاسيلي ، [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;

في المخطط ، يأتي الأول والثاني

في مخطط tdesktop ، القيمة الثالثة هي

نعم ، منذ ذلك الحين وبالطبع تم تحديث الوثائق. على الرغم من أنه قد يصبح غير ذي صلة قريبًا مرة أخرى. وكيف يجب أن يعرف المطور المبتدئ؟ ربما إذا قمت بتسجيل طلبك ، فسيقومون بإبلاغك؟ فعل فاسيلي هذا ، لكن للأسف ، لم يتم إرسال أي شيء إليه (مرة أخرى ، سنتحدث عن هذا في الجزء الثاني).

... لقد لاحظت أننا انتقلنا بالفعل بطريقة ما إلى واجهة برمجة التطبيقات ، أي إلى المستوى التالي وغاب عن شيء في موضوع MTProto؟ لا شيء يثير الدهشة:

فاسيلي ، [28.06.18/02/04 2:XNUMX صباحًا] مم ، إنهم يبحثون في بعض الخوارزميات على eXNUMXe

يحدد Mtproto خوارزميات ومفاتيح التشفير لكلا المجالين ، بالإضافة إلى جزء من بنية الغلاف

لكنهم يخلطون باستمرار مستويات تكديس مختلفة ، لذلك ليس من الواضح دائمًا أين انتهت mtproto وبدأ المستوى التالي.

كيف يختلطون؟ حسنًا ، إليك نفس المفتاح المؤقت لـ PFS ، على سبيل المثال (بالمناسبة ، لا يعرف Telegram Desktop كيفية القيام بذلك). يتم تنفيذه بواسطة طلب API auth.bindTempAuthKey، أي. من أعلى مستوى. لكن في الوقت نفسه ، يتداخل مع التشفير في المستوى الأدنى - بعد ذلك ، على سبيل المثال ، تحتاج إلى القيام بذلك مرة أخرى initConnection إلخ ، هذا ليس كذلك فقط طلب عادي. بشكل منفصل ، يسلم أيضًا أنه لا يمكنك الحصول إلا على مفتاح مؤقت واحد على DC ، على الرغم من الحقل auth_key_id في كل رسالة يسمح لك بتغيير المفتاح على الأقل في كل رسالة ، وأن الخادم لديه الحق في "نسيان" المفتاح المؤقت في أي وقت - ما يجب القيام به في هذه الحالة ، لا تذكر الوثائق ... حسنًا ، لماذا لن يكون من الممكن أن يكون لديك عدة مفاتيح ، كما هو الحال مع مجموعة أملاح المستقبل ، ولكن؟ ..

هناك بعض الأشياء الأخرى الجديرة بالملاحظة في موضوع MTProto.

رسائل الرسائل ، msg_id ، msg_seqno ، إقرارات ، أصوات في الاتجاه الخاطئ وخصوصيات أخرى

لماذا تريد أن تعرف عنهم؟ لأنهم "تسربوا" مستوى أعلى ، وتحتاج إلى معرفة المزيد عنها عند العمل مع API. لنفترض أننا لسنا مهتمين بـ msg_key ، فإن المستوى الأدنى قام بفك تشفير كل شيء بالنسبة لنا. لكن داخل البيانات التي تم فك تشفيرها ، لدينا الحقول التالية (أيضًا طول البيانات لمعرفة مكان الحشو ، لكن هذا ليس مهمًا):

  • ملح int64
  • session_id - int64
  • message_id - int64
  • seq_no-int32

تذكر أنه لا يوجد سوى ملح واحد لكامل العاصمة. لماذا تعرف عنها؟ ليس فقط بسبب وجود طلب get_future_salts، والذي يخبرنا عن الفترات الزمنية التي ستكون صالحة ، ولكن أيضًا لأنه إذا كان ملحك "فاسدًا" ، فستفقد الرسالة (الطلب) ببساطة. سيقوم الخادم بالطبع بالإبلاغ عن الملح الجديد عن طريق الإصدار new_session_created - ولكن مع القديم ، سيتعين عليك إعادة الإرسال بطريقة ما ، على سبيل المثال. وهذا السؤال يؤثر على معمارية التطبيق.

يُسمح للخادم بإسقاط الجلسات تمامًا والاستجابة بهذه الطريقة لأسباب عديدة. في الواقع ، ما هي جلسة MTProto من جانب العميل؟ هذان رقمان session_id и seq_no الرسائل في هذه الجلسة. حسنًا ، واتصال TCP الأساسي ، بالطبع. لنفترض أن عميلنا ما زال لا يعرف كيفية القيام بالكثير من الأشياء ، غير متصل ، وإعادة الاتصال. إذا حدث هذا بسرعة - استمرت الجلسة القديمة في اتصال TCP الجديد ، فقم بالزيادة seq_no إضافي. إذا استغرق الأمر وقتًا طويلاً ، يمكن للخادم حذفه ، لأنه من جانبه يوجد أيضًا قائمة انتظار ، كما اكتشفنا.

ما ينبغي أن يكون seq_no؟ أوه ، هذا سؤال صعب. حاول أن تفهم بصدق ما هو المقصود:

رسالة متعلقة بالمحتوى

رسالة تتطلب إقرارًا صريحًا. وتشمل هذه جميع رسائل المستخدم والعديد من رسائل الخدمة ، جميعها تقريبًا باستثناء الحاويات وإقرارات الاستلام.

رقم تسلسل الرسالة (msg_seqno)

رقم 32 بت يساوي ضعف عدد الرسائل "المتعلقة بالمحتوى" (تلك التي تتطلب إقرارًا ، ولا سيما تلك التي ليست حاويات) التي أنشأها المرسل قبل هذه الرسالة وزادها لاحقًا برقم واحد إذا كانت الرسالة الحالية رسالة متعلقة بالمحتوى. يتم إنشاء الحاوية دائمًا بعد محتوياتها بالكامل ؛ لذلك ، رقم التسلسل الخاص به أكبر من أو يساوي الأرقام التسلسلية للرسائل الموجودة فيه.

أي نوع من السيرك هذا مع زيادة 1 ، ثم 2 أخرى؟ .. أظن أن المعنى الأصلي كان "بت منخفض لـ ACK ، والباقي رقم" ، لكن النتيجة ليست صحيحة تمامًا - على وجه الخصوص ، اتضح أنه يمكن إرسالها بعض التأكيدات التي لها نفس الشيء seq_no! كيف؟ حسنًا ، على سبيل المثال ، يرسل لنا الخادم شيئًا ما ، ويرسله ، ونحن أنفسنا صامتون ، نجيب فقط برسائل تأكيد الخدمة حول تلقي رسائله. في هذه الحالة ، سيكون لتأكيداتنا الصادرة نفس الرقم الصادر. إذا كنت معتادًا على TCP وتعتقد أن هذا يبدو نوعًا من الجنون ، لكن يبدو أنه ليس شديد الوحشية ، لأنه في TCP seq_no لا يتغير ، والتأكيد يذهب إلى seq_no على الجانب الآخر - ثم أسرع في الانزعاج. التأكيدات قادمة إلى MTProto NOT في seq_no، كما في TCP ، لكن msg_id !

ما هذا msg_id، أهم هذه المجالات؟ المعرف الفريد للرسالة ، كما يوحي الاسم. يتم تعريفه على أنه رقم 64 بت ، وأقل البتات أهمية والتي تحتوي مرة أخرى على سحر خادم وليس خادم ، والباقي هو طابع زمني Unix ، بما في ذلك الجزء الكسري ، تم إزاحته 32 بت إلى اليسار. أولئك. الطابع الزمني في حد ذاته (وسيرفض الخادم الرسائل ذات الأوقات المختلفة جدًا). من هذا اتضح أنه ، بشكل عام ، هذا معرف عالمي للعميل. بينما - تذكر session_id - نحن نضمن: لا يمكن بأي حال من الأحوال إرسال رسالة مخصصة لجلسة واحدة إلى جلسة مختلفة. وهذا هو ، اتضح أن هناك بالفعل ثلاثة المستوى - الجلسة ، رقم الجلسة ، معرف الرسالة. لماذا هذا التعقيد المفرط ، هذا اللغز عظيم جدا.

وهكذا، msg_id اللازمة ل…

RPC: الطلبات والاستجابات والأخطاء. التأكيدات.

كما لاحظت ، لا يوجد نوع أو وظيفة خاصة "تقديم طلب RPC" في أي مكان في المخطط ، على الرغم من وجود إجابات. بعد كل شيء ، لدينا رسائل متعلقة بالمحتوى! إنه، أي يمكن أن تكون الرسالة طلب! أو لا تكون. بعد كل ذلك، كل غير msg_id. وإليك الإجابات:

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

هذا هو المكان الذي يشار فيه إلى الرسالة التي يتم الرد عليها. لذلك ، في المستوى العلوي لواجهة برمجة التطبيقات ، سيتعين عليك تذكر الرقم الذي يحتويه طلبك - أعتقد أنه ليس من الضروري توضيح أن العمل غير متزامن ، ويمكن أن يكون هناك عدة طلبات في نفس الوقت ، والإجابات عليها يمكن إرجاعها بأي ترتيب؟ من حيث المبدأ ، من هذا ، ورسائل الخطأ مثل عدم وجود عمال ، يمكن تتبع البنية وراء ذلك: الخادم الذي يحافظ على اتصال TCP معك هو موازن أمامي ، ويوجه الطلبات إلى الخلفيات ويجمعها مرة أخرى. message_id. يبدو أن كل شيء واضح ومنطقي وجيد هنا.

نعم؟ .. وإذا فكرت في الأمر؟ بعد كل شيء ، فإن استجابة RPC نفسها لها حقل أيضًا msg_id! هل نحتاج إلى الصراخ على الخادم "أنت لا ترد على إجابتي!"؟ ونعم ، ماذا كان هناك عن التأكيد؟ حول الصفحة رسائل عن الرسائل يخبرنا ما هو

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

ويجب على كل جانب أن يفعل ذلك. لكن ليس دائما! إذا تلقيت RpcResult ، فسيكون بمثابة إقرار بحد ذاته. أي ، يمكن للخادم الاستجابة لطلبك مع MsgsAck - مثل ، "لقد تلقيته." يمكن الإجابة على الفور RpcResult. يمكن أن يكون كلاهما.

ونعم ، لا يزال يتعين عليك الإجابة على الجواب! تأكيد. خلاف ذلك ، سيعتبر الخادم أنه لم يتم تسليمه ويرميها إليك مرة أخرى. حتى بعد إعادة الاتصال. لكن هنا ، بالطبع ، ستظهر مسألة المهلات. دعونا ننظر إليهم بعد قليل.

في غضون ذلك ، دعنا نفكر في الأخطاء المحتملة في تنفيذ الاستعلام.

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

أوه ، سيصيح أحدهم ، هنا شكل أكثر إنسانية - هناك سطر! خذ وقتك. هنا قائمة الأخطاءلكنها بالتأكيد ليست كاملة. منه نتعلم أن الكود هو - شيء مثل أخطاء HTTP (حسنًا ، بالطبع ، لا يتم احترام دلالات الاستجابات ، في بعض الأماكن يتم توزيعها حسب الرموز بشكل عشوائي) ، وتبدو السلسلة مثل CAPITAL_LETTERS_AND_NUMBERS. على سبيل المثال ، PHONE_NUMBER_OCCUPIED أو FILE_PART_X_MISSING. حسنًا ، هذا هو ، لا يزال يتعين عليك اتباع هذا الخط تحليل. على سبيل المثال FLOOD_WAIT_3600 سيعني أنه عليك الانتظار لمدة ساعة ، و PHONE_MIGRATE_5أنه يجب تسجيل رقم الهاتف الذي يحتوي على هذه البادئة في الخامس DC. لدينا لغة كتابة ، أليس كذلك؟ لا نحتاج إلى وسيط من السلسلة ، فالتعبيرات النمطية ستفعل ، cho.

مرة أخرى ، هذا ليس في صفحة رسائل الخدمة ، ولكن كما هو معتاد بالفعل في هذا المشروع ، يمكن العثور على المعلومات في صفحة وثائق أخرى. أو يثير الشبهة. أولاً ، انظر ، انتهاك للكتابة / الطبقات - RpcError يمكن الاستثمار فيها RpcResult. لماذا ليس بالخارج؟ ما الذي لم نأخذه بعين الاعتبار؟ .. وعليه فأين الضمان ذلك RpcError لا يجوز الاستثمار فيها RpcResult، ولكن تكون مباشرة أو متداخلة في نوع آخر؟ ينقصها req_msg_id ؟ ..

لكن دعنا نستمر في رسائل الخدمة. قد يعتبر العميل أن الخادم يفكر منذ فترة طويلة ، ويقدم مثل هذا الطلب الرائع:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

هناك ثلاث إجابات محتملة لها ، تتقاطع مرة أخرى مع آلية التأكيد ، لمحاولة فهم ما يجب أن تكون (وما هي قائمة الأنواع التي لا تتطلب تأكيدًا بشكل عام) ، يُترك القارئ كواجب منزلي (ملاحظة: المعلومات الموجودة في مصادر Telegram Desktop ليست كاملة).

الإدمان: حالات مشاركة الرسائل

بشكل عام ، تترك العديد من الأماكن في TL و MTProto و Telegram بشكل عام شعورًا بالعناد ، ولكن بسبب الأدب واللباقة وغيرها. مهارات بسيطة التزمنا الصمت بأدب ، وتم حظر البذاءات في الحوارات. ومع ذلك ، هذا المكانОمعظم الصفحة حول رسائل عن الرسائل يسبب الصدمة حتى بالنسبة لي ، الذي كان يعمل مع بروتوكولات الشبكة لفترة طويلة وشهد دراجات بدرجات متفاوتة من الانحناء.

يبدأ بشكل غير ضار ، مع تأكيدات. بعد ذلك ، قيل لنا عن

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;

حسنًا ، سيتعين على كل شخص يبدأ العمل مع MTProto مواجهتها ، في دورة "المصححة - المُعاد تجميعها - إطلاقها" ، يعد الحصول على أخطاء في الأرقام أو الملح الذي تعفن أثناء عمليات التحرير أمرًا شائعًا. ومع ذلك ، هناك نقطتان هنا:

  1. ويترتب على ذلك أن الرسالة الأصلية فقدت. نحتاج إلى سياج بعض قوائم الانتظار ، وسننظر في هذا لاحقًا.
  2. ما هي أرقام الخطأ الغريبة تلك؟ 16 ، 17 ، 18 ، 19 ، 20 ، 32 ، 33 ، 34 ، 35 ، 48 ، 64 ... أين باقي الأرقام يا تومي؟

تنص الوثائق:

القصد من ذلك هو تجميع قيم رمز الخطأ (رمز الخطأ >> 4): على سبيل المثال ، تتوافق الرموز 0x40 - 0x4f مع الأخطاء في تحلل الحاوية.

لكن ، أولاً ، تحول في الاتجاه الآخر ، وثانيًا ، لا يهم أين توجد بقية الرموز؟ في رأس المؤلف؟ .. إلا أن هذه تفاهات.

يبدأ الإدمان في رسائل حالة النشر ونسخ النشر:

  • طلب معلومات حالة الرسالة
    إذا لم يتلق أي من الطرفين معلومات عن حالة رسائله الصادرة لفترة من الوقت ، فيجوز له أن يطلبها صراحةً من الطرف الآخر:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • رسالة إعلامية بشأن حالة الرسائل
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    هنا، info عبارة عن سلسلة تحتوي بالضبط على بايت واحد من حالة الرسالة لكل رسالة من قائمة msg_ids الواردة:

    • 1 = لا يوجد شيء معروف عن الرسالة (msg_id منخفض جدًا ، ربما يكون الطرف الآخر قد نسيها)
    • 2 = لم يتم استلام الرسالة (يقع msg_id ضمن نطاق المعرفات المخزنة ؛ ومع ذلك ، فإن الطرف الآخر بالتأكيد لم يتلق رسالة من هذا القبيل)
    • 3 = لم يتم استلام الرسالة (msg_id مرتفع جدًا ؛ ومع ذلك ، من المؤكد أن الطرف الآخر لم يستلمها بعد)
    • 4 = تم استلام الرسالة (لاحظ أن هذه الاستجابة هي أيضًا في نفس الوقت إقرار استلام)
    • +8 = تم الاعتراف بالرسالة بالفعل
    • +16 = الرسالة لا تتطلب الإقرار
    • +32 = استعلام RPC المضمن في الرسالة التي تتم معالجتها أو معالجتها قد اكتمل بالفعل
    • +64 = تم إنشاء استجابة متعلقة بالمحتوى للرسالة بالفعل
    • +128 = يعرف الطرف الآخر حقيقة أن الرسالة قد تم استلامها بالفعل
      هذا الرد لا يتطلب إقرارا. إنه إقرار بالمادة msgs_state_req ذات الصلة ، في حد ذاتها.
      لاحظ أنه إذا تبين فجأة أن الطرف الآخر ليس لديه رسالة تبدو أنه قد تم إرسالها إليه ، فيمكن ببساطة إعادة إرسال الرسالة. حتى إذا تلقى الطرف الآخر نسختين من الرسالة في نفس الوقت ، فسيتم تجاهل النسخة المكررة. (إذا مر وقت طويل ، ولم يعد معرّف msg_id الأصلي صالحًا ، يجب تغليف الرسالة بتنسيق msg_copy).
  • التواصل الطوعي لحالة الرسائل
    يجوز لأي من الطرفين إبلاغ الطرف الآخر طواعية بحالة الرسائل المرسلة من قبل الطرف الآخر.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • تمديد الاتصال الطوعي لحالة رسالة واحدة
    ...
    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;
  • طلب صريح لإعادة إرسال الرسائل
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    يستجيب الطرف البعيد على الفور بإعادة إرسال الرسائل المطلوبة [...]
  • طلب صريح لإعادة إرسال الإجابات
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    يستجيب الطرف البعيد على الفور بإعادة الإرسال الأجوبة إلى الرسائل المطلوبة [...]
  • نُسخ الرسائل
    في بعض الحالات ، يجب إعادة إرسال رسالة قديمة ذات معرّف msg_id لم تعد صالحة. ثم يتم تغليفها في حاوية نسخ:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    بمجرد استلام الرسالة ، تتم معالجتها كما لو لم يكن الغلاف موجودًا. ومع ذلك ، إذا كان معروفًا على وجه اليقين أن الرسالة Orig_message.msg_id قد تم استلامها ، فلن تتم معالجة الرسالة الجديدة (بينما في نفس الوقت ، يتم الاعتراف بها و Orig_message.msg_id). يجب أن تكون قيمة Orig_message.msg_id أقل من قيمة msg_id الخاصة بالحاوية.

دعونا حتى نلتزم الصمت بشأن حقيقة أن في msgs_state_info مرة أخرى ، تلتصق آذان TL غير المكتملة (كنا بحاجة إلى متجه من البايت ، وفي البتتين السفليتين من التعداد ، وفي أعلام البتات الأقدم). النقطة هي شيء آخر. لا أحد يفهم لماذا كل هذا في الممارسة في عميل حقيقي ضروري؟ .. بصعوبة ، لكن يمكنك تخيل بعض الفوائد إذا كان الشخص منخرطًا في تصحيح الأخطاء ، وفي وضع تفاعلي - اسأل الخادم ماذا وكيف. لكن الطلبات موصوفة هنا ذهابا وإيابا.

ويترتب على ذلك أنه لا يتعين على كل جانب تشفير الرسائل وإرسالها فحسب ، بل يجب عليه أيضًا تخزين البيانات المتعلقة بها ، وحول الإجابات عليها ، ولفترة زمنية غير معروفة. لا يصف التوثيق التوقيت أو التطبيق العملي لهذه الميزات. بأي حال من الأحوال. الأمر الأكثر إثارة للدهشة هو أنها تُستخدم بالفعل في مدونة العملاء الرسميين! على ما يبدو ، تم إخبارهم بشيء لم يتم تضمينه في الوثائق المفتوحة. افهم من الكود لماذا، لم يعد بسيطًا كما في حالة TL - هذا ليس جزءًا معزولًا منطقيًا (نسبيًا) ، ولكنه جزء مرتبط بهندسة التطبيق ، أي سيتطلب مزيدًا من الوقت لفهم رمز التطبيق.

الأصوات والتوقيتات. قوائم الانتظار.

من كل شيء ، إذا كنت تتذكر التخمينات حول بنية الخادم (توزيع الطلبات عبر الخلفيات) ، فسيتبع ذلك أمر ممل إلى حد ما - على الرغم من كل ضمانات التسليم التي في TCP (إما تم تسليم البيانات ، أو سيتم إعلامك عن كسر ، ولكن سيتم تسليم البيانات حتى لحظة حدوث المشكلة) ، تلك التأكيدات في MTProto نفسها - لا توجد ضمانات. يمكن للخادم أن يفقد رسالتك أو يرميها بسهولة ، ولا يمكن فعل أي شيء حيال ذلك ، فقط لتسييج العكازات من مختلف الأنواع.

وقبل كل شيء - قوائم انتظار الرسائل. حسنًا ، لسبب واحد ، كان كل شيء واضحًا منذ البداية - يجب تخزين رسالة غير مؤكدة والاستياء. وبعد اي وقت؟ والمهرج يعرفه. ربما تحل رسائل خدمة المدمنين هذه المشكلة بطريقة ما باستخدام العكازات ، على سبيل المثال ، في Telegram Desktop ، هناك حوالي 4 قوائم انتظار تتوافق معها (ربما أكثر ، كما ذكرنا سابقًا ، لهذا تحتاج إلى الخوض في كودها وبنيتها بشكل أكثر جدية ؛ في نفس الوقت الوقت ، نحن نعلم أنه لا يمكن أخذها كعينة ، ولا يتم استخدام عدد معين من الأنواع من مخطط MTProto فيه).

لماذا يحدث هذا؟ ربما ، لم يتمكن مبرمجو الخادم من ضمان الموثوقية داخل الكتلة ، أو على الأقل حتى التخزين المؤقت على الموازن الأمامي ، وقاموا بتحويل هذه المشكلة إلى العميل. بدافع اليأس ، حاول فاسيلي تنفيذ خيار بديل ، بقائمتين فقط من قوائم الانتظار ، باستخدام خوارزميات من TCP - قياس RTT إلى الخادم وضبط حجم "النافذة" (في الرسائل) اعتمادًا على عدد الطلبات غير المعترف بها. هذا هو ، مثل هذا الاستدلال التقريبي لتقدير تحميل الخادم - كم من طلباتنا يمكنه مضغها في نفس الوقت وعدم خسارتها.

حسنًا ، هذا ، فهمت ، أليس كذلك؟ إذا كان عليك تنفيذ TCP مرة أخرى فوق بروتوكول يعمل عبر TCP ، فهذا يشير إلى بروتوكول مصمم بشكل سيئ للغاية.

أوه نعم ، لماذا هناك حاجة إلى أكثر من قائمة انتظار واحدة ، وبشكل عام ، ماذا يعني هذا بالنسبة لشخص يعمل مع واجهة برمجة تطبيقات عالية المستوى؟ انظر ، أنت تقدم طلبًا ، تقوم بتسلسله ، لكن غالبًا ما يكون من المستحيل إرساله على الفور. لماذا؟ لأن الجواب سيكون msg_id، وهو مؤقتаأنا تسمية ، من الأفضل تأجيل التعيين في وقت متأخر قدر الإمكان - فجأة سيرفض الخادم ذلك بسبب عدم تطابق الوقت بيننا وبينه (بالطبع ، يمكننا صنع عكاز يغير وقتنا من الوقت الحاضر إلى وقت الخادم عن طريق إضافة دلتا محسوبة من استجابات الخادم - يقوم العملاء الرسميون بذلك ، لكن هذه الطريقة غير دقيقة وغير دقيقة بسبب التخزين المؤقت). لذلك عند تقديم طلب باستدعاء وظيفة محلية من المكتبة ، تمر الرسالة بالمراحل التالية:

  1. يقع في نفس قائمة الانتظار وينتظر التشفير.
  2. عين msg_id وذهبت الرسالة إلى قائمة انتظار أخرى - إمكانية إعادة التوجيه ؛ أرسل إلى المقبس.
  3. أ) قام الخادم بالرد على MsgsAck - تم تسليم الرسالة ، نقوم بحذفها من "قائمة الانتظار الأخرى".
    ب) أو العكس ، لم يعجبه شيئًا ، أجاب على badmsg - نعيد الإرسال من "قائمة الانتظار الأخرى"
    ج) لا يوجد شيء معروف ، من الضروري إعادة إرسال الرسالة من قائمة انتظار أخرى - ولكن لا يُعرف متى بالضبط.
  4. أجاب الخادم أخيرًا RpcResult - الاستجابة الفعلية (أو الخطأ) - لم يتم تسليمها فحسب ، بل تمت معالجتها أيضًا.

ربما، يمكن أن يؤدي استخدام الحاويات إلى حل المشكلة جزئيًا. يحدث هذا عندما يتم حزم مجموعة من الرسائل في واحدة ، ويستجيب الخادم بإقرار للجميع مرة واحدة ، بواحد msg_id. لكنه سيرفض أيضًا هذه الحزمة ، إذا حدث خطأ ما ، كل شيء أيضًا.

وفي هذه المرحلة ، تلعب الاعتبارات غير الفنية دورًا. من خلال التجربة ، رأينا العديد من العكازات ، وبالإضافة إلى ذلك ، سنرى الآن المزيد من الأمثلة على النصائح والهندسة المعمارية السيئة - في مثل هذه الظروف ، هل يستحق الأمر الثقة واتخاذ مثل هذه القرارات؟ السؤال بلاغي (بالطبع لا).

عن ماذا نتحدث؟ إذا كنت في موضوع "رسائل المدمنين حول الرسائل" ، فلا يزال بإمكانك التكهن بالاعتراضات مثل "أنت غبي ، لم تفهم فكرتنا الرائعة!" (لذا اكتب التوثيق أولاً ، كما يجب على الأشخاص العاديين ، مع أمثلة المبررات وتبادل الحزم ، ثم سنتحدث) ، ثم التوقيت / المهلات هي مسألة عملية ومحددة بحتة ، كل شيء معروف هنا منذ فترة طويلة. ولكن ماذا تخبرنا الوثائق عن المهلات؟

يقر الخادم عادةً باستلام رسالة من العميل (عادةً ، استعلام RPC) باستخدام استجابة RPC. إذا كانت الاستجابة قادمة لفترة طويلة ، فقد يرسل الخادم أولاً إشعار استلام ، وبعد ذلك إلى حد ما ، استجابة RPC نفسها.

يقر العميل عادةً باستلام رسالة من الخادم (عادةً ، استجابة RPC) عن طريق إضافة إقرار إلى استعلام RPC التالي إذا لم يتم إرساله متأخرًا (إذا تم إنشاؤه ، على سبيل المثال ، 60-120 ثانية بعد الإيصال لرسالة من الخادم). ومع ذلك ، إذا لم يكن هناك سبب لإرسال رسائل إلى الخادم لفترة طويلة أو إذا كان هناك عدد كبير من الرسائل غير المعترف بها من الخادم (على سبيل المثال ، أكثر من 16) ، يرسل العميل إقرارًا منفردًا.

... أترجم: نحن أنفسنا لا نعرف كم وكيف هو ضروري ، حسنًا ، دعنا نقدر أن يكون الأمر على هذا النحو.

وحول الأصوات:

رسائل Ping (PING / PONG)

ping#7abe77ec ping_id:long = Pong;

عادة ما يتم إرجاع الرد إلى نفس الاتصال:

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

هذه الرسائل لا تتطلب إقرارات. يتم إرسال كرة الطاولة فقط ردًا على ping بينما يمكن بدء ping بواسطة أي من الجانبين.

إغلاق الاتصال المؤجل + PING

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

يعمل مثل ping. بالإضافة إلى ذلك ، بعد استلام هذا ، يبدأ الخادم مؤقتًا يغلق الاتصال الحالي disconnect_delay بعد ثوانٍ ما لم يتلق رسالة جديدة من نفس النوع والتي تعيد ضبط جميع أجهزة ضبط الوقت السابقة تلقائيًا. إذا أرسل العميل هذه الأصوات مرة كل 60 ثانية ، على سبيل المثال ، فقد يقوم بتعيين disconnect_delay بما يعادل 75 ثانية.

هل جننت؟! في غضون 60 ثانية ، سيدخل القطار إلى المحطة وينزل وينقل الركاب ، ويفقد الاتصال مرة أخرى في النفق. خلال 120 ثانية ، بينما تتجول ، سيصل إلى واحدة أخرى ، ومن المرجح أن ينقطع الاتصال. حسنًا ، من الواضح من أين تنمو الأرجل - "سمعت رنينًا ، لكنني لا أعرف مكانه" ، هناك خوارزمية Nagle وخيار TCP_NODELAY ، والذي كان مخصصًا للعمل التفاعلي. لكن ، آسف ، قم بتأخير القيمة الافتراضية - 200 مليثواني. إذا كنت تريد حقًا تصوير شيء مشابه وحفظه على زوج محتمل من الحزم - حسنًا ، قم بإيقاف تشغيله ، على الأقل لمدة 5 ثوانٍ ، أو أيًا كانت مهلة الرسالة "المستخدم يكتب ..." تساوي الآن. ولكن ليس أكثر.

وأخيرًا ، الأصوات. أي التحقق من حيوية اتصال TCP. إنه أمر مضحك ، لكن منذ حوالي 10 سنوات كتبت نصًا نقديًا حول رسول بيت الشباب التابع لأعضاء هيئة التدريس لدينا - هناك أيضًا قام المؤلفون باختبار الخادم من العميل ، وليس العكس. لكن طلاب السنة الثالثة شيء ، والمكتب الدولي شيء آخر ، أليس كذلك؟ ..

أولاً ، برنامج تعليمي صغير. يمكن أن يعيش اتصال TCP ، في حالة عدم وجود تبادل حزم ، لأسابيع. هذا جيد وسيئ على حد سواء ، اعتمادًا على الغرض. حسنًا ، إذا كان لديك اتصال SSH بالخادم مفتوحًا ، فقد نهضت من جهاز الكمبيوتر الخاص بك ، وأعدت تشغيل جهاز التوجيه ، وعادت إلى مكانك - لم تنقطع الجلسة من خلال هذا الخادم (لم تكتب أي شيء ، ولم تكن هناك حزم) ، مريح. إنه أمر سيئ إذا كان هناك الآلاف من العملاء على الخادم ، كل واحد يأخذ الموارد (مرحبًا Postgres!) ، وربما يكون مضيف العميل قد أعيد تشغيله منذ وقت طويل - لكننا لن نعرف ذلك.

تنتمي أنظمة الدردشة / المراسلة الفورية إلى الحالة الثانية لسبب إضافي آخر - حالات الاتصال بالإنترنت. إذا "سقط" المستخدم ، فمن الضروري إبلاغ محاوره بذلك. خلاف ذلك ، سيكون هناك خطأ ارتكبه منشئو Jabber (وتم تصحيحه لمدة 20 عامًا) - المستخدم مفصول ، لكنهم يواصلون كتابة الرسائل إليه ، معتقدين أنه متصل بالإنترنت (والذي فقد تمامًا أيضًا في هذه الدقائق القليلة السابقة) تم اكتشاف الكسر). لا ، لن يساعد خيار TCP_KEEPALIVE ، الذي لا يفهمه الكثير من الأشخاص الذين لا يفهمون كيفية عمل مؤقتات TCP ، في أي مكان (من خلال تعيين قيم برية مثل عشرات الثواني) ، هنا - تحتاج إلى التأكد من أنه ليس فقط نواة نظام التشغيل الخاصة بـ OS جهاز المستخدم على قيد الحياة ، ولكنه يعمل أيضًا بشكل طبيعي ، ويمكنه الإجابة ، والتطبيق نفسه (هل تعتقد أنه لا يمكن تجميده؟ لقد تعطل Telegram Desktop على Ubuntu 18.04 بشكل متكرر).

لهذا السبب يجب عليك بينج الخادم العميل ، وليس العكس - إذا قام العميل بذلك ، فعند انقطاع الاتصال ، لن يتم تسليم اختبار الاتصال ، ولا يتم تحقيق الهدف.

وماذا نرى في Telegram؟ كل شيء هو عكس ذلك تماما! حسنًا ، أي رسميًا ، بالطبع ، يمكن للطرفين الاتصال ببعضهما البعض. في الممارسة العملية ، يستخدم العملاء عكازًا ping_delay_disconnect، والذي يعمل على تشغيل جهاز ضبط الوقت على الخادم. حسنًا ، آسف ، ليس من عمل العميل أن يقرر المدة التي يريد أن يعيش فيها دون اختبار ping. الخادم ، بناءً على حمله ، يعرف بشكل أفضل. لكن ، بالطبع ، إذا لم تشعر بالأسف على الموارد ، فإن بينوكيو الشرير هم أنفسهم ، وسوف ينزل العكاز ...

كيف كان ينبغي تصميمه؟

أعتقد أن الحقائق المذكورة أعلاه تشير بوضوح إلى الكفاءة غير العالية لفريق Telegram / VKontakte في مجال النقل (والمستوى الأدنى) لشبكات الكمبيوتر ومؤهلاتهم المنخفضة في الأمور ذات الصلة.

لماذا اتضح أن الأمر معقد للغاية ، وكيف يمكن لمهندسي Telegram أن يحاولوا الاعتراض؟ حقيقة أنهم حاولوا إجراء جلسة تنجو من انقطاع اتصال TCP ، وهذا هو ، ما لم نقدمه الآن ، سنقوم بتقديمه لاحقًا. ربما حاولوا أيضًا إجراء نقل UDP ، على الرغم من أنهم واجهوا صعوبات وتخلوا عنها (لهذا السبب كانت الوثائق فارغة - لم يكن هناك ما يدعو للتباهي). ولكن نظرًا لعدم فهم كيفية عمل الشبكات بشكل عام و TCP بشكل خاص ، حيث يمكنك الاعتماد عليها ، وأين تحتاج إلى القيام بذلك بنفسك (وكيف) ، ومحاولات دمج هذا مع التشفير "لقطة واحدة من اثنين طيور بحجر واحد "- تحولت هذه الجثة.

كيف كان ينبغي أن يكون؟ بناء على حقيقة أن msg_id هو طابع زمني ضروري من الناحية المشفرة لمنع هجمات الإعادة ، فمن الخطأ إرفاق وظيفة معرف فريد به. لذلك ، دون تغيير البنية الحالية بشكل جذري (عند تشكيل سلسلة التحديثات ، يعد هذا موضوع واجهة برمجة تطبيقات عالي المستوى لجزء آخر من هذه السلسلة من المنشورات) ، يجب على المرء أن:

  1. يتحمل الخادم الذي يحمل اتصال TCP بالعميل المسؤولية - إذا طرحت من المقبس ، فيرجى الإقرار بالخطأ أو معالجته أو إرجاعه ، بدون خسارة. إذن ، التأكيد ليس متجهًا للمعرف ، ولكنه ببساطة "آخر seq_no تم استلامه" - مجرد رقم ، كما هو الحال في TCP (رقمان - التسلسل الخاص بك والتأكيد). نحن دائما في جلسة ، أليس كذلك؟
  2. يصبح الطابع الزمني لمنع هجمات الإعادة حقلاً منفصلاً ، وهو أمر غير مرغوب فيه. تم الفحص ، ولكن لم يتأثر أي شيء آخر. كفى و uint32 - إذا تغير ملحنا كل نصف يوم على الأقل ، فيمكننا تخصيص 16 بتًا للبتات السفلية للجزء الصحيح من الوقت الحالي ، والباقي - للجزء الكسري من الثانية (كما هو الحال الآن).
  3. إزالة msg_id على الإطلاق - من وجهة نظر تمييز الطلبات على الخلفيات ، هناك ، أولاً ، معرف العميل ، وثانيًا ، معرف الجلسة ، وسلسلها. وفقًا لذلك ، كمعرف طلب ، يكفي واحد فقط seq_no.

أيضًا ليس الخيار الأفضل ، يمكن أن تكون عشوائية كاملة بمثابة معرف - يتم ذلك بالفعل في واجهة برمجة التطبيقات عالية المستوى عند إرسال رسالة ، بالمناسبة. سيكون من الأفضل تغيير البنية من النسبي إلى المطلق تمامًا ، لكن هذا موضوع لجزء آخر ، وليس هذا المنشور.

واجهة برمجة التطبيقات؟

Ta-daam! لذلك ، بعد أن شقنا طريقنا عبر طريق مليء بالألم والعكازات ، تمكنا أخيرًا من إرسال أي طلبات إلى الخادم وتلقي أي إجابات عليها ، وكذلك تلقي التحديثات من الخادم (ليس استجابة لطلب ، ولكن إنه يرسل لنا نفسه ، مثل PUSH ، إذا كان أي شخص أكثر وضوحًا).

انتبه ، الآن سيكون هناك مثال Perl الوحيد في المقالة! (بالنسبة لأولئك الذين ليسوا على دراية بالصياغة ، فإن الحجة الأولى التي يجب أن تبارك هي بنية بيانات الكائن ، والثانية هي صنفها):

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

نعم ، خاصةً ليس أسفل الجناح - إذا لم تكن قد قرأته ، فاذهب وافعله!

أوه ، واي ~~… كيف تبدو؟ شيء مألوف جدًا ... ربما يكون هذا هو بنية البيانات لواجهة برمجة تطبيقات ويب نموذجية في JSON ، باستثناء ربما تم إرفاق الفئات بالكائنات؟ ..

هكذا اتضح .. ما هو يا رفاق .. بذل الكثير من الجهد - وتوقفنا للراحة حيث مبرمجو الويب بدأت للتو؟ .. ألن يكون مجرد JSON عبر HTTPS أسهل ؟! وماذا حصلنا في المقابل؟ هل كانت هذه الجهود تستحق العناء؟

دعنا نقيم ما قدمته لنا TL + MTProto وما هي البدائل الممكنة. حسنًا ، استجابة طلب HTTP غير مناسبة ، ولكن على الأقل شيء فوق TLS؟

التسلسل المضغوط. عند رؤية بنية البيانات هذه ، على غرار JSON ، نتذكر أن هناك متغيراتها الثنائية. دعنا نضع علامة على MsgPack على أنه غير قابل للتوسعة بشكل كافٍ ، ولكن هناك ، على سبيل المثال ، CBOR - بالمناسبة ، المعيار الموصوف في RFC 7049. إنه جدير بالملاحظة لأنه يحدد العلامات، كآلية امتداد ، وبين موحدة بالفعل متاح:

  • 25 + 256 - استبدال الأسطر المكررة بمرجع رقم السطر ، مثل طريقة الضغط الرخيصة
  • 26 - كائن Perl متسلسل مع اسم الصنف ووسائط المُنشئ
  • 27 - كائن مستقل عن اللغة متسلسل مع اسم النوع ووسيطات المُنشئ

حسنًا ، حاولت إجراء تسلسل للبيانات نفسها في TL و CBOR مع تمكين تعبئة السلاسل والكائنات. بدأت النتيجة تختلف لصالح CBOR في مكان ما من الميجابايت:

cborlen=1039673 tl_len=1095092

وهكذا، إنتاج: هناك تنسيقات أبسط إلى حد كبير لا تخضع لفشل المزامنة أو مشكلة المعرف غير المعروفة ، بكفاءة مماثلة.

إنشاء اتصال سريع. هذا يعني عدم وجود وقت RTT بعد إعادة الاتصال (عندما يكون المفتاح قد تم إنشاؤه مرة واحدة بالفعل) - قابل للتطبيق من أول رسالة MTProto ، ولكن مع بعض الحجوزات - دخلوا في نفس الملح ، ولم تتعفن الجلسة ، وما إلى ذلك. ماذا تقدم لنا TLS في المقابل؟ اقتباس ذات صلة:

عند استخدام PFS في TLS ، تذاكر جلسة TLS (RFC 5077) لاستئناف الجلسة المشفرة دون إعادة التفاوض على المفاتيح ودون تخزين المعلومات الأساسية على الخادم. عند فتح الاتصال الأول وإنشاء المفاتيح ، يقوم الخادم بتشفير حالة الاتصال وإرسالها إلى العميل (في شكل بطاقة جلسة). وفقًا لذلك ، عند استئناف الاتصال ، يرسل العميل بطاقة جلسة تحتوي ، من بين أشياء أخرى ، على مفتاح الجلسة مرة أخرى إلى الخادم. البطاقة نفسها مشفرة بمفتاح مؤقت (مفتاح تذكرة الجلسة) ، والذي يتم تخزينه على الخادم ويجب توزيعه على جميع خوادم الواجهة الأمامية التي تتعامل مع SSL في حلول مجمعة. [10]. وبالتالي ، فإن إدخال بطاقة جلسة يمكن أن ينتهك PFS إذا تم اختراق مفاتيح الخادم المؤقتة ، على سبيل المثال ، عندما يتم تخزينها لفترة طويلة (OpenSSL ، nginx ، Apache ، قم بتخزينها افتراضيًا طوال فترة تشغيل البرنامج ؛ المواقع الشهيرة استخدم المفتاح لعدة ساعات ، حتى أيام).

هنا RTT ليست صفرية ، فأنت بحاجة إلى تبادل ClientHello و ServerHello على الأقل ، وبعد ذلك ، مع Finished ، يمكن للعميل بالفعل إرسال البيانات. ولكن هنا يجب أن نتذكر أنه ليس لدينا الويب ، مع مجموعة الاتصالات المفتوحة حديثًا ، ولكن ماسنجر ، والذي غالبًا ما يكون الاتصال واحدًا أو أكثر أو أقل ، طلبات قصيرة نسبيًا لصفحات الويب - كل شيء مضاعف في الداخل. هذا ، هذا مقبول تمامًا ، إذا لم نواجه قسم مترو أنفاق سيئ للغاية.

هل نسيت شيئاً آخر؟ اكتب في التعليقات.

أن يستمر!

في الجزء الثاني من هذه السلسلة من المنشورات ، سننظر في القضايا التنظيمية بدلاً من القضايا التقنية - الأساليب ، والأيديولوجيا ، والواجهة ، والموقف تجاه المستخدمين ، إلخ. بناءً على المعلومات الفنية التي تم تقديمها هنا.

سيستمر الجزء الثالث في تحليل المكون الفني / تجربة التطوير. سوف تتعلم على وجه الخصوص:

  • استمرار الصخب مع مجموعة متنوعة من أنواع TL
  • أشياء غير معروفة عن القنوات والمجموعات الفائقة
  • من الحوارات أسوأ من القائمة
  • حول معالجة الرسالة المطلقة مقابل النسبية
  • ما هو الفرق بين الصورة والصورة
  • كيف تتداخل الرموز التعبيرية مع النص المائل

والعكازات الأخرى! ابقوا متابعين!

المصدر: www.habr.com

إضافة تعليق