نقد پروتکل و رویکردهای سازمانی تلگرام. بخش 1، فنی: تجربه نوشتن یک مشتری از ابتدا - TL، MT

اخیراً پست‌هایی در مورد اینکه تلگرام چقدر خوب است، برادران دوروف چقدر درخشان و با تجربه در ساختن سیستم‌های شبکه هستند و غیره بیشتر در هابر منتشر می‌شوند. در عین حال، تعداد بسیار کمی از افراد واقعاً خود را در دستگاه فنی غوطه ور کرده اند - حداکثر از یک Bot API نسبتاً ساده (و کاملاً متفاوت از MTProto) مبتنی بر JSON استفاده می کنند و معمولاً فقط می پذیرند. بر ایمان همه ستایش ها و روابط عمومی که حول محور پیام رسان می چرخد. تقریباً یک سال و نیم پیش، همکار من در سازمان غیردولتی اشلون واسیلی (متاسفانه حساب کاربری او در هابره همراه با پیش نویس پاک شد) شروع به نوشتن کلاینت تلگرام خود از ابتدا در پرل کرد و بعداً نویسنده این خطوط به آن پیوست. برخی بلافاصله خواهند پرسید چرا پرل؟ از آنجا که چنین پروژه هایی در حال حاضر در زبان های دیگر وجود دارد. در واقع، این موضوع نیست، هر زبان دیگری می تواند وجود داشته باشد که در آن وجود نداشته باشد. کتابخانه آماده، و بر این اساس نویسنده باید تمام راه را طی کند از ابتدا. علاوه بر این، رمزنگاری یک موضوع قابل اعتماد است، اما تأیید کنید. با محصولی که امنیت را هدف قرار می دهد، نمی توانید به سادگی به یک کتابخانه آماده از سازنده اعتماد کنید و کورکورانه به آن اعتماد کنید (البته این موضوع برای قسمت دوم است). در حال حاضر، کتابخانه در سطح "متوسط" به خوبی کار می کند (به شما امکان می دهد هر درخواست API را انجام دهید).

با این حال، رمزنگاری یا ریاضی زیادی در این سری از پست ها وجود نخواهد داشت. اما بسیاری دیگر از جزئیات فنی و عصاهای معماری (همچنین برای کسانی که از ابتدا نمی نویسند، اما از کتابخانه به هر زبانی استفاده می کنند مفید است). بنابراین، هدف اصلی تلاش برای پیاده سازی مشتری از ابتدا بود طبق اسناد رسمی. یعنی، فرض کنیم کد منبع مشتریان رسمی بسته است (دوباره، در قسمت دوم، موضوع درست بودن این موضوع را با جزئیات بیشتری پوشش خواهیم داد. اتفاق می افتد بنابراین، اما، به عنوان مثال، مانند روزهای قدیم، استانداردی مانند RFC وجود دارد - آیا می توان یک مشتری را طبق مشخصات به تنهایی نوشت، "بدون نگاه کردن" به کد منبع، خواه رسمی باشد (دسکتاپ تلگرام، تلفن همراه)، یا تله تله غیر رسمی؟

شرح:

مستندات... وجود دارد، درست است؟ درسته؟..

بخش هایی از یادداشت های این مقاله تابستان گذشته جمع آوری شد. تمام این مدت در وب سایت رسمی https://core.telegram.org مستندات مربوط به لایه 23 بود، یعنی. جایی در سال 2014 گیر کرده است (یادتان باشد، در آن زمان حتی کانال هایی وجود نداشت؟). البته، از نظر تئوری، این باید به ما اجازه می داد تا در سال 2014 یک کلاینت با عملکرد آن زمان را پیاده سازی کنیم. اما حتی در این حالت، اسناد اولاً ناقص بود و ثانیاً در جاهایی با خود منافات داشت. درست بیش از یک ماه پیش، در سپتامبر 2019، بود به طور تصادفی مشخص شد که به‌روزرسانی بزرگی از اسناد در سایت، برای لایه کاملاً اخیر 105 وجود دارد، با این نکته که اکنون همه چیز باید دوباره خوانده شود. در واقع، بسیاری از مقالات تجدید نظر شدند، اما بسیاری از آنها بدون تغییر باقی ماندند. بنابراین، هنگام خواندن انتقاد زیر در مورد مستندات، باید در نظر داشته باشید که برخی از این موارد دیگر مرتبط نیستند، اما برخی هنوز کاملاً هستند. به هر حال، 5 سال در دنیای مدرن فقط زمان زیادی نیست، بلکه بسیار بسیاری از. از آن زمان‌ها (مخصوصاً اگر سایت‌های geochat حذف شده و احیا شده از آن زمان را در نظر نگیرید)، تعداد روش‌های API در این طرح از صد به بیش از دویست و پنجاه افزایش یافته است!

به عنوان یک نویسنده جوان از کجا شروع کنیم؟

فرقی نمی کند که از ابتدا بنویسید یا از کتابخانه های آماده استفاده کنید Telethon برای پایتون یا ساخته شده برای PHP، در هر صورت، ابتدا نیاز خواهید داشت درخواست خود را ثبت کنید - پارامترها را دریافت کنید api_id и api_hash (کسانی که با VKontakte API کار کرده اند بلافاصله متوجه می شوند) که سرور برنامه را شناسایی می کند. این باید به این کار را به دلایل قانونی انجام دهید، اما در مورد اینکه چرا نویسندگان کتابخانه نمی توانند آن را در قسمت دوم منتشر کنند، بیشتر صحبت خواهیم کرد. ممکن است از مقادیر آزمون راضی باشید، اگرچه آنها بسیار محدود هستند - واقعیت این است که اکنون می توانید ثبت نام کنید فقط یک برنامه، بنابراین در آن عجله نکنید.

حال از نظر فنی باید به این موضوع علاقه داشت که پس از ثبت نام از تلگرام اطلاعیه هایی در مورد به روز رسانی اسناد، پروتکل و ... دریافت کنیم. یعنی می توان فرض کرد که سایت دارای اسکله ها به سادگی رها شده است و به طور خاص با کسانی که شروع به ایجاد مشتری کرده اند به کار خود ادامه می دهد، زیرا راحت تر است اما نه، چنین چیزی مشاهده نشد، هیچ اطلاعاتی نیامد.

و اگر از ابتدا بنویسید، استفاده از پارامترهای به دست آمده در واقع هنوز راه زیادی دارد. با اينكه https://core.telegram.org/ و اول از همه در Getting Started درباره آنها صحبت می کند، در واقع، ابتدا باید اجرا کنید پروتکل MTProto - اما اگر باور کردی چیدمان طبق مدل OSI در انتهای صفحه برای توضیح کلی پروتکل، آنگاه کاملاً بیهوده است.

در واقع، هم قبل و هم بعد از MTProto، در چندین سطح به طور همزمان (همانطور که شبکه‌های خارجی که در هسته سیستم عامل کار می‌کنند، نقض لایه)، یک موضوع بزرگ، دردناک و وحشتناک مانع خواهد شد...

سریال سازی باینری: TL (زبان نوع) و طرح و لایه ها و بسیاری از کلمات ترسناک دیگر

این موضوع در واقع کلید مشکلات تلگرام است. و اگر سعی کنید در آن عمیق شوید، کلمات وحشتناک زیادی وجود خواهد داشت.

بنابراین، در اینجا نمودار است. اگر این کلمه به ذهن شما رسید، بگویید: طرحواره 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، که اگرچه قبلاً حاوی نمونه ای از یک درخواست و پاسخ بی اهمیت است، اما به هیچ وجه پاسخی به موارد معمولی تر ارائه نمی دهد، به این معنی که شما مجبور خواهید بود از طریق بازگویی ریاضیات ترجمه شده از روسی به انگلیسی در هشت مورد دیگر جاسازی شده بگذرید. صفحات!

خوانندگانی که با زبان های تابعی و استنتاج نوع خودکار آشنا هستند، البته، زبان توصیف را در این زبان، حتی از روی مثال، بسیار آشناتر می بینند و می توانند بگویند که در اصل بد نیست. ایراداتی که در این مورد وجود دارد عبارتند از:

  • آره، هدف خوب به نظر می رسد، اما افسوس، او به دست نیامد
  • تحصیلات در دانشگاه های روسیه حتی در بین تخصص های IT متفاوت است - همه دوره مربوطه را گذرانده اند
  • در نهایت، همانطور که خواهیم دید، در عمل چنین است مورد نیاز نیست، زیرا فقط یک زیر مجموعه محدود از حتی TL که توضیح داده شد استفاده می شود

همانطور که گفته شد LeoNerd در کانال #perl در شبکه FreeNode IRC، که سعی کرد یک گیت از تلگرام به ماتریس را پیاده سازی کند (ترجمه نقل قول از حافظه نادرست است):

به نظر می رسد کسی برای اولین بار برای تایپ تئوری معرفی شده است، هیجان زده شده و شروع به بازی با آن کرده است، بدون اینکه در عمل به آن نیاز باشد یا خیر.

خودتان ببینید، اگر نیاز به نوع های خالی (int، طولانی، و غیره) به عنوان چیزی ابتدایی سؤالی ایجاد نمی کند - در نهایت آنها باید به صورت دستی پیاده سازی شوند - برای مثال، بیایید تلاش کنیم تا از آنها استخراج کنیم. وکتور. یعنی در واقع آرایه، اگر چیزهای به دست آمده را با نام خاص خود صدا کنید.

اما قبل از

شرح کوتاهی از زیرمجموعه ای از نحو 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---، اما این سازنده "در نظر گرفته نمی شود". بارگذاری بیش از حد انواع توابع فراخوانی شده توسط آرگومان های آنها، i.e. به دلایلی، چندین تابع با نام یکسان اما امضاهای متفاوت، مانند C++، در TL ارائه نشده است.

چرا "سازنده" و "چند شکل" اگر OOP نیست؟ خوب، در واقع، برای کسی آسانتر خواهد بود که در مورد این با شرایط OOP فکر کند - یک نوع چند شکلی به عنوان یک کلاس انتزاعی، و سازنده ها کلاس های نوادگان مستقیم آن هستند، و final در اصطلاح تعدادی از زبان ها. در واقع، البته، فقط در اینجا شباهت با متدهای سازنده بارگذاری شده واقعی در زبان های برنامه نویسی OO. از آنجایی که در اینجا فقط ساختارهای داده وجود دارد، هیچ روشی وجود ندارد (اگرچه توضیح بیشتر توابع و روش ها کاملاً قادر به ایجاد سردرگمی در ذهن است که آنها وجود دارند، اما این موضوع متفاوت است) - می توانید سازنده را به عنوان یک مقدار در نظر بگیرید. که در حال ساخت است هنگام خواندن یک جریان بایت تایپ کنید.

چگونه این اتفاق می افتد؟ deserializer که همیشه 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، که در مثال ما دارای 2 سازنده است. یکی خالی است (فقط از شناسه تشکیل شده است)، دیگری دارای یک فیلد است ids با نوع ids:Vector<long>.

ممکن است فکر کنید که هم قالب ها و هم ژنریک ها در حرفه ای یا جاوا هستند. اما نه. تقریبا. این تک تک مورد استفاده از براکت های زاویه ای در مدارهای واقعی است و فقط برای Vector استفاده می شود. در جریان بایت، 4 بایت CRC32 برای خود نوع Vector خواهد بود، همیشه یکسان، سپس 4 بایت - تعداد عناصر آرایه، و سپس خود این عناصر.

به این واقعیت اضافه کنید که سریال سازی همیشه در کلمات 4 بایتی اتفاق می افتد، همه انواع مضرب آن هستند - انواع داخلی نیز توضیح داده شده است. bytes и string با سریال دستی طول و این تراز 4 - خوب به نظر می رسد عادی و حتی نسبتا موثر است؟ اگرچه ادعا می شود TL یک سریال سازی باینری موثر است، اما با گسترش تقریباً هر چیزی، حتی مقادیر Boolean و رشته های تک کاراکتری به 4 بایت، آیا JSON همچنان بسیار ضخیم تر خواهد بود؟ ببینید، حتی فیلدهای غیر ضروری را می توان با پرچم های بیتی نادیده گرفت، همه چیز کاملاً خوب است، و حتی برای آینده قابل توسعه است، پس چرا بعداً فیلدهای اختیاری جدید را به سازنده اضافه نکنید؟...

اما نه، اگر نه شرح مختصر من، بلکه مستندات کامل را بخوانید و به اجرا فکر کنید. اولاً، CRC32 سازنده با توجه به خط نرمال شده توضیحات متن طرح محاسبه می شود (حذف فضای خالی اضافی و غیره) - بنابراین اگر یک فیلد جدید اضافه شود، خط توصیف نوع تغییر می کند، و از این رو CRC32 و آن تغییر می کند. در نتیجه سریال سازی. و اگر مشتری قدیمی یک فیلد با پرچم‌های جدید دریافت کند و نداند در مرحله بعد با آنها چه کند، چه می‌کند؟

ثانیاً یادمان باشد CRC32، که در اینجا اساساً به عنوان استفاده می شود توابع هش برای تعیین منحصر به فرد نوع در حال (غیر) سریال. در اینجا ما با مشکل برخورد روبرو هستیم - و نه، احتمال یک در 232 نیست، بلکه بسیار بیشتر است. چه کسی به یاد آورد که CRC32 برای تشخیص (و تصحیح) خطاها در کانال ارتباطی طراحی شده است و بر این اساس این ویژگی ها را به ضرر دیگران بهبود می بخشد؟ به عنوان مثال، به مرتب کردن مجدد بایت ها اهمیتی نمی دهد: اگر CRC32 را از دو خط محاسبه کنید، در خط دوم، 4 بایت اول را با 4 بایت بعدی عوض کنید - همینطور خواهد بود. وقتی ورودی ما رشته‌های متنی از الفبای لاتین (و کمی نقطه‌گذاری) باشد و این نام‌ها به‌طور خاص تصادفی نباشند، احتمال چنین بازآرایی بسیار افزایش می‌یابد.

به هر حال، چه کسی بررسی کرده است که چه چیزی وجود دارد؟ واقعا CRC32؟ یکی از کدهای منبع اولیه (حتی قبل از والتمن) یک تابع هش داشت که هر کاراکتر را در عدد 239 ضرب می کرد، بسیار محبوب این افراد، ها ها!

در نهایت، بسیار خوب، متوجه شدیم که سازنده هایی با یک نوع فیلد Vector<int> и Vector<PolymorType> CRC32 متفاوتی خواهد داشت. عملکرد آنلاین چطور؟ و از منظر نظری، آیا این بخشی از نوع می شود؟? فرض کنید آرایه ای از ده هزار عدد را پاس می کنیم، خوب با Vector<int> همه چیز واضح است، طول و 40000 بایت دیگر. و اگر این Vector<Type2>، که فقط از یک فیلد تشکیل شده است int و تنها در نوع است - آیا باید 10000xabcdef0 را 34 بار و سپس 4 بایت تکرار کنیم؟ int، یا زبان می تواند آن را از سازنده برای ما مستقل کند fixedVec و بجای 80000 بایت دوباره فقط 40000 انتقال بدم؟

این به هیچ وجه یک سوال تئوری بیکار نیست - تصور کنید لیستی از کاربران گروه را دریافت می کنید که هر کدام دارای شناسه، نام، نام خانوادگی هستند - تفاوت در مقدار داده های منتقل شده از طریق اتصال تلفن همراه می تواند قابل توجه باشد. این دقیقاً اثربخشی سریال سازی تلگرام است که برای ما تبلیغ می شود.

بنابراین…

وکتور که هرگز منتشر نشد

اگر سعی کنید صفحات توضیحات ترکیب‌کننده‌ها و غیره را مرور کنید، می‌بینید که یک بردار (و حتی یک ماتریس) به طور رسمی سعی می‌کند از طریق چند ورق چندتایی به خروجی برسد. اما در نهایت آنها فراموش می کنند، مرحله نهایی حذف می شود و به سادگی تعریفی از یک بردار ارائه می شود که هنوز به یک نوع گره خورده است. موضوع چیه؟ در زبان ها برنامه نويسي، به ویژه موارد کاربردی، توصیف ساختار به صورت بازگشتی کاملاً معمولی است - کامپایلر با ارزیابی تنبل خود همه چیز را می فهمد و خودش انجام می دهد. در زبان سریال سازی داده ها آنچه مورد نیاز است بهره وری است: به سادگی توصیف کنید فهرست، یعنی ساختار دو عنصر - اولی یک عنصر داده است، دومی همان ساختار یا فضای خالی برای دم (بسته) (cons) در Lisp). اما بدیهی است که این نیاز خواهد داشت از هر کدام عنصر 4 بایت اضافی (CRC32 در مورد به TL) برای توصیف نوع خود صرف می کند. یک آرایه نیز به راحتی قابل توصیف است اندازه ثابت، اما در مورد آرایه ای با طول ناشناخته از قبل، ما قطع می کنیم.

بنابراین، از آنجایی که TL اجازه خروجی بردار را نمی دهد، باید آن را در کنار اضافه کرد. در نهایت اسناد می گوید:

سریال سازی همیشه از همان سازنده "بردار" (const 0x1cb5c415 = crc32 ("بردار t: نوع # [ t ] = بردار t") استفاده می کند که به مقدار خاص متغیر نوع t وابسته نیست.

مقدار پارامتر اختیاری t در سریال سازی دخیل نیست زیرا از نوع نتیجه مشتق شده است (همیشه قبل از سریال سازی شناخته شده است).

نگاه دقیقتری بینداز: vector {t:Type} # [ t ] = Vector t - ولی هیچی خود این تعریف نمی گوید که عدد اول باید برابر با طول بردار باشد! و از هیچ جا نمی آید. این نکته ای است که باید در ذهن داشته باشید و با دستان خود اجرا کنید. در جای دیگر، اسناد حتی صادقانه اشاره می کنند که نوع واقعی نیست:

کاذب چندشکلی Vector t یک "نوع" است که مقدار آن دنباله ای از مقادیر از هر نوع t است، اعم از جعبه ای یا خالی.

... اما روی آن تمرکز نمی کند. وقتی که از گذراندن رشته ریاضیات (شاید حتی از یک دوره دانشگاهی برایتان شناخته شده) خسته شده اید، تصمیم می گیرید تسلیم شوید و در واقع به نحوه کار با آن در عمل نگاه کنید، تصوری که در ذهن شما باقی می ماند این است که این موضوع جدی است. ریاضیات در هسته، به وضوح توسط Cool People (دو ریاضیدان - برنده ACM) اختراع شد و نه هر کسی. هدف - خودنمایی - محقق شده است.

اتفاقاً در مورد تعداد. این را به شما یادآوری کنیم # این یک مترادف است nat، عدد طبیعی:

عبارات نوع (type-expr) و عبارات عددی (nat-expr). با این حال، آنها به همین ترتیب تعریف می شوند.

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

اما در دستور زبان به همین صورت توصیف شده اند، یعنی. این تفاوت را باید دوباره به خاطر بسپارید و با دست اجرا کنید.

خوب، بله، انواع قالب (vector<int>, vector<User>دارای یک شناسه مشترک (#1cb5c415) ، یعنی اگر می دانید که تماس به عنوان اعلام شده است

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

پس شما دیگر منتظر یک بردار نیستید، بلکه منتظر یک بردار از کاربران هستید. دقیق تر، باید صبر کنید - در کد واقعی، هر عنصر، اگر یک نوع خالی نباشد، سازنده خواهد داشت، و به خوبی در اجرا باید بررسی شود - اما ما دقیقاً در هر عنصر این بردار ارسال شدیم. آن نوع? اگر نوعی PHP باشد، که در آن یک آرایه می تواند انواع مختلفی را در عناصر مختلف داشته باشد، چه؟

در این مرحله شما شروع به فکر می کنید - آیا چنین TL ضروری است؟ شاید برای سبد خرید بتوان از سریال‌ساز انسانی استفاده کرد، همان پروتوبافی که قبلاً وجود داشت؟ این تئوری بود، بیایید به عمل نگاه کنیم.

پیاده سازی TL موجود در کد

TL در اعماق VKontakte متولد شد حتی قبل از اتفاقات معروف با فروش سهم دوروف و (مسلما، حتی قبل از شروع توسعه تلگرام. و در متن باز کد منبع اولین پیاده سازی می توانید عصاهای خنده دار زیادی پیدا کنید. و خود زبان در آنجا به طور کامل تر از آنچه اکنون در تلگرام وجود دارد پیاده سازی شد. به عنوان مثال، هش ها به هیچ وجه در طرح مورد استفاده قرار نمی گیرند (به معنی یک شبه نمونه داخلی (مانند یک برداری) با رفتار انحرافی). یا

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

این تعریف نوع قالب هشمپ به عنوان بردار جفت‌های int - Type است. در C++ چیزی شبیه به این خواهد بود:

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

بنابراین، alpha - کلمه کلیدی! اما فقط در C++ می توانید T بنویسید، اما باید آلفا، بتا بنویسید... اما نه بیشتر از 8 پارامتر، اینجاست که فانتزی به پایان می رسد. به نظر می رسد که روزی روزگاری در سن پترزبورگ دیالوگ هایی مانند این اتفاق افتاده است:

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

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

اما این در مورد اولین اجرای منتشر شده از TL "به طور کلی" بود. بیایید به بررسی پیاده سازی ها در خود کلاینت های تلگرام بپردازیم.

سخنی به واسیلی:

واسیلی، [09.10.18 17:07] بیشتر از همه، الاغ داغ است، زیرا آنها یک دسته از انتزاعات را ایجاد کردند، و سپس یک پیچ بر روی آنها چکش کردند، و ژنراتور کد را با عصا پوشانیدند.
در نتیجه، ابتدا از dock pilot.jpg
سپس از کد dzhekichan.webp

البته، از افرادی که با الگوریتم ها و ریاضیات آشنا هستند، می توان انتظار داشت که Aho، Ullmann را خوانده باشند و با ابزارهایی که در طول دهه ها عملاً در صنعت استاندارد شده اند برای نوشتن کامپایلرهای DSL خود آشنا هستند، درست است؟

نویسنده telegram-cli ویتالی والتمن است، همانطور که از وقوع قالب TLO در خارج از مرزهای (cli) آن می توان فهمید، یکی از اعضای تیم است - اکنون یک کتابخانه برای تجزیه TL اختصاص داده شده است. به طور جداگانه، چه برداشتی از او دارد تجزیه کننده TL؟ ..

16.12 04:18 واسیلی: فکر می کنم کسی به lex+yacc مسلط نشده است
16.12 04:18 واسیلی: من نمی توانم آن را غیر از این توضیح دهم
16.12 04:18 واسیلی: خوب، یا به آنها برای تعداد خطوط در VK پرداخت شده است
16.12 04:19 واسیلی: 3k+ خطوط و غیره.<censored> به جای تجزیه کننده

شاید استثنا باشد؟ بیایید ببینیم چگونه میکند این مشتری رسمی است - تلگرام دسکتاپ:

    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 خط در پایتون، یک جفت عبارت منظم + حالت های خاص مانند یک برداری که البته طبق دستور TL آنطور که باید در طرح اعلام شده است، اما برای تجزیه آن به این سینتکس تکیه کردند... این سوال پیش می آید که چرا این همه معجزه بود؟иاگر به هر حال کسی قرار نیست آن را طبق مستندات تجزیه کند، لایه لایه تر است؟!

به هر حال... یادتان هست در مورد بررسی CRC32 صحبت کردیم؟ بنابراین، در تولید کننده کد دسکتاپ تلگرام لیستی از استثناها برای انواعی وجود دارد که در آنها CRC32 محاسبه شده است. مطابقت ندارد با چیزی که در نمودار مشخص شده است!

واسیلی، [18.12/22 49:XNUMX] و در اینجا به این فکر می کنم که آیا چنین TL مورد نیاز است یا خیر
اگر می‌خواستم با پیاده‌سازی‌های جایگزین مشکل داشته باشم، شروع به درج شکسته‌های خط می‌کردم، نیمی از تجزیه‌کننده‌ها در تعاریف چند خطی شکست خواهند خورد.
tdesktop، با این حال، بیش از حد

نکته یک لاینر را به خاطر بسپارید، کمی بعد به آن باز خواهیم گشت.

خوب، telegram-cli غیر رسمی است، تلگرام دسکتاپ رسمی است، اما بقیه چطور؟ چه کسی می داند؟... در کد کلاینت اندروید اصلاً تجزیه کننده طرحواره وجود نداشت (که سوالاتی را در مورد منبع باز ایجاد می کند، اما این برای قسمت دوم است)، اما چندین قطعه کد خنده دار دیگر وجود داشت، اما بیشتر در مورد آنها در زیر بخش زیر

سریال سازی در عمل چه سوالات دیگری را ایجاد می کند؟ به عنوان مثال، آنها کارهای زیادی را انجام دادند، البته با فیلدهای بیتی و فیلدهای شرطی:

واسیلی: flags.0? true
به این معنی است که اگر پرچم تنظیم شود، فیلد موجود است و برابر با true است

واسیلی: flags.1? int
به این معنی است که میدان موجود است و نیاز به بی‌سریال کردن دارد

واسیلی: الاغ، نگران کاری نباش!
واسیلی: جایی در سند اشاره شده است که درست است یک نوع بدون طول صفر است، اما نمی توان چیزی از سند آنها جمع کرد.
واسیلی: در پیاده سازی های متن باز هم اینطور نیست، اما دسته ای از عصا و پشتیبانی وجود دارد.

تله تله چطور؟ با نگاهی به موضوع MTProto، یک مثال - در اسناد چنین قطعاتی وجود دارد، اما علامت % آن را فقط به عنوان "مطابق با یک نوع لخت معین" توصیف می کنند، یعنی. در مثال های زیر یا یک خطا یا چیزی غیرمستند وجود دارد:

واسیلی، [22.06.18 18:38] در یک مکان:

msg_container#73f1f8dc messages:vector message = MessageContainer;

در یک متفاوت:

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

و این دو تفاوت بزرگ است، در زندگی واقعی نوعی بردار برهنه می آید

من یک تعریف بردار ساده ندیده ام و به آن برخورد نکرده ام

تجزیه و تحلیل به صورت دستی در تله‌تون نوشته می‌شود

در نمودار او این تعریف توضیح داده شده است msg_container

باز هم سوال در مورد درصد باقی می ماند. شرح داده نشده است.

وادیم گونچاروف، [22.06.18 19:22] و در tdesktop؟

واسیلی، [22.06.18 19:23] اما تجزیه کننده TL آنها در موتورهای معمولی به احتمال زیاد این را هم نمی خورد.

// parsed manually

TL یک انتزاع زیبا است، هیچ کس آن را به طور کامل پیاده سازی نمی کند

و % در نسخه آنها از طرح نیست

اما در اینجا مستندات با خود تناقض دارند، بنابراین idk

این در دستور زبان یافت شد، آنها به سادگی می توانستند فراموش کنند که معناشناسی را توصیف کنند

شما سند را در 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)

این کل lexer است:

    ---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کد تولید شده)، از جمله نوع buns اطلاعات برای درون نگری در هر کلاس. هر نوع چند شکلی به یک کلاس پایه انتزاعی خالی تبدیل می‌شود و سازنده‌ها از آن به ارث می‌برند و روش‌هایی برای سریال‌سازی و سریال‌زدایی دارند.

عدم وجود انواع در زبان نوع

تایپ قوی چیز خوبی است، درست است؟ نه، این یک هولیوار نیست (اگرچه من زبان‌های پویا را ترجیح می‌دهم)، بلکه یک اصل در چارچوب TL است. بر اساس آن، زبان باید همه نوع چک را برای ما فراهم کند. خوب، خوب، شاید خودش نه، اما اجرا، اما او حداقل باید آنها را توصیف کند. و چه نوع فرصت هایی را می خواهیم؟

اول از همه، محدودیت ها. در اینجا ما در اسناد برای آپلود فایل ها می بینیم:

سپس محتوای باینری فایل به بخش‌هایی تقسیم می‌شود. همه قطعات باید یک اندازه داشته باشند ( part_size ) و شرایط زیر باید رعایت شود:

  • part_size % 1024 = 0 (قابل تقسیم بر 1 کیلوبایت)
  • 524288 % part_size = 0 (512 کیلوبایت باید به طور مساوی بر اندازه قسمت تقسیم شود)

لازم نیست قسمت آخر این شرایط را داشته باشد، مشروط بر اینکه اندازه آن کمتر از part_size باشد.

هر قسمت باید یک شماره دنباله داشته باشد، file_part، با مقداری از 0 تا 2,999.

پس از پارتیشن بندی فایل، باید روشی را برای ذخیره آن در سرور انتخاب کنید. استفاده کنید upload.saveBigFilePart در صورتی که حجم کامل فایل بیش از 10 مگابایت باشد و upload.saveFilePart برای فایل های کوچکتر
[…] یکی از خطاهای ورودی داده زیر ممکن است برگردانده شود:

  • FILE_PARTS_INVALID - تعداد قطعات نامعتبر است. ارزش بین نیست 1..3000

آیا هیچ کدام از اینها در نمودار وجود دارد؟ آیا این به نوعی با استفاده از TL قابل بیان است؟ خیر اما ببخشید، حتی توربو پاسکال پدربزرگ نیز توانست انواع مشخص شده را توصیف کند محدوده ها. و او یک چیز دیگر را می دانست که اکنون بیشتر به عنوان شناخته شده است enum - نوعی متشکل از شمارش تعداد ثابت (کم) مقادیر. در زبان هایی مانند C - numeric توجه داشته باشید که تا کنون فقط در مورد انواع صحبت کرده ایم شماره. اما آرایه‌ها، رشته‌ها نیز وجود دارند، برای مثال، بهتر است توضیح دهیم که این رشته فقط می‌تواند شامل یک شماره تلفن باشد، درست است؟

هیچ کدام از اینها در TL نیست. اما برای مثال در JSON Schema وجود دارد. و اگر شخص دیگری ممکن است در مورد تقسیم پذیری 512 کیلوبایت بحث کند، که هنوز باید در کد بررسی شود، پس مطمئن شوید که کلاینت به سادگی نتوانست ارسال شماره خارج از محدوده 1..3000 (و خطای مربوطه ممکن نبود رخ دهد) ممکن بود، درست است؟..

به هر حال، در مورد خطاها و مقادیر بازگشتی. حتی کسانی که با TL کار کرده‌اند چشمان خود را تار می‌کنند - بلافاصله این موضوع متوجه ما نشد هرکدام یک تابع در TL در واقع می تواند نه تنها نوع بازگشت توصیف شده، بلکه یک خطا را نیز برگرداند. اما این را نمی توان به هیچ وجه با استفاده از خود TL استنباط کرد. البته، از قبل مشخص است و در عمل نیازی به هیچ چیز نیست (اگرچه در واقع، 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. یا برعکس. اما کار کرد! یعنی سرور به نوع آن اهمیتی نداد. چگونه می تواند این باشد؟ پاسخ ممکن است با قطعات کد تلگرام-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 نسخه، سرانجام به آنچه در پروتکل های اینترنت در دهه 80 انجام می شد رسیدیم - یک بار در ابتدای اتصال با نسخه به توافق رسیدیم!

پس بعدی چیه؟..

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

اما اکنون هنوز می توانید بخندید. فقط پس از 9 لایه دیگر، بالاخره یک سازنده جهانی با شماره نسخه اضافه شد که در ابتدای اتصال فقط یک بار باید فراخوانی شود و به نظر می رسید معنای لایه ها ناپدید شده است، اکنون فقط یک نسخه شرطی است، مانند هر جای دیگر. مشکل حل شد.

دقیقا؟..

واسیلی، [16.07.18 14:01] حتی روز جمعه فکر کردم:
تله سرور رویدادها را بدون درخواست ارسال می کند. درخواست ها باید در InvokeWithLayer پیچیده شوند. سرور به‌روزرسانی‌ها را جمع نمی‌کند؛ ساختاری برای بسته‌بندی پاسخ‌ها و به‌روزرسانی‌ها وجود ندارد.

آن ها مشتری نمی تواند لایه ای را که در آن به روز رسانی می خواهد مشخص کند

Vadim Goncharov، [16.07.18 14:02] آیا InvokeWithLayer در اصل یک عصا نیست؟

واسیلی، [16.07.18 14:02] این تنها راه است

وادیم گونچاروف، [16.07.18 14:02] که اساساً باید به معنای توافق بر روی لایه در ابتدای جلسه باشد.

به هر حال، نتیجه این است که کاهش رتبه مشتری ارائه نشده است

به روز رسانی ها، یعنی نوع Updates در این طرح، این همان چیزی است که سرور نه در پاسخ به درخواست API، بلکه به طور مستقل زمانی که یک رویداد رخ می دهد برای مشتری ارسال می کند. این موضوع پیچیده ای است که در پست دیگری مورد بحث قرار خواهد گرفت، اما در حال حاضر مهم است که بدانید سرور حتی زمانی که مشتری آفلاین است، به روز رسانی ها را ذخیره می کند.

بنابراین، اگر از بسته بندی امتناع کنید از هر کدام بسته برای نشان دادن نسخه آن، منطقاً منجر به مشکلات احتمالی زیر می شود:

  • سرور حتی قبل از اینکه کلاینت از کدام نسخه پشتیبانی می کند، به روز رسانی ها را برای مشتری ارسال می کند
  • پس از ارتقاء مشتری چه کاری باید انجام دهم؟
  • که ضمانت هاکه نظر سرور در مورد شماره لایه در طول فرآیند تغییر نمی کند؟

آیا به نظر شما این یک گمانه زنی صرفا تئوری است و در عمل نمی تواند این اتفاق بیفتد، زیرا سرور به درستی نوشته شده است (حداقل به خوبی تست شده است)؟ ها! مهم نیست که چگونه است!

این دقیقاً همان چیزی است که در ماه اوت به آن برخورد کردیم. در 14 آگوست پیام هایی مبنی بر آپدیت شدن چیزی در سرورهای تلگرام وجود دارد و سپس در لاگ ها:

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 شما شناسایی نشود، با امضای آن باینری است، پایین‌تر همه می رود، رمزگشایی غیرممکن خواهد شد. در چنین شرایطی چه باید کرد؟

خوب، اولین چیزی که به ذهن هر کسی می رسد این است که اتصال را قطع کند و دوباره تلاش کند. کمکی نکرد. ما CRC32 را در گوگل جستجو می کنیم - معلوم شد که این اشیاء از طرح 73 هستند، اگرچه روی 82 کار کردیم. ما به دقت به گزارش ها نگاه می کنیم - شناسه هایی از دو طرح مختلف وجود دارد!

شاید مشکل صرفا در مشتری غیر رسمی ما باشد؟ نه، تلگرام دسکتاپ 1.2.17 (نسخه ارائه شده در تعدادی از توزیع‌های لینوکس) را راه‌اندازی می‌کنیم، در گزارش Exception می‌نویسد: MTP نوع غیرمنتظره شناسه #b5223b0f خوانده شده در MTPMessageMedia…

نقد پروتکل و رویکردهای سازمانی تلگرام. بخش 1، فنی: تجربه نوشتن یک مشتری از ابتدا - TL، MT

گوگل نشان داد که مشکل مشابهی قبلا برای یکی از مشتریان غیر رسمی اتفاق افتاده بود، اما پس از آن شماره نسخه و بر این اساس، فرضیات متفاوت بود...

خب ما باید چی کار کنیم؟ من و واسیلی از هم جدا شدیم: او سعی کرد مدار را به 91 به روز کند، من تصمیم گرفتم چند روز صبر کنم و 73 را امتحان کنم. هر دو روش کار کردند، اما از آنجایی که آنها تجربی هستند، هیچ درک درستی از چند نسخه بالا یا پایین نیاز ندارید. برای پریدن، یا چقدر باید منتظر بمانید.

بعداً توانستم وضعیت را بازتولید کنم: کلاینت را راه‌اندازی می‌کنیم، آن را خاموش می‌کنیم، مدار را مجدداً به یک لایه دیگر کامپایل می‌کنیم، دوباره راه‌اندازی می‌کنیم، دوباره مشکل را برطرف می‌کنیم، به حالت قبلی برمی‌گردیم - اوه، هیچ مقدار سوئیچینگ مدار وجود ندارد و کلاینت دوباره راه‌اندازی می‌شود. چند دقیقه کمک خواهد کرد ترکیبی از ساختارهای داده را از لایه های مختلف دریافت خواهید کرد.

توضیح؟ همانطور که از علائم غیرمستقیم مختلف می توانید حدس بزنید، سرور از بسیاری از فرآیندهای مختلف در ماشین های مختلف تشکیل شده است. به احتمال زیاد، سروری که مسئول "بافر کردن" است، آنچه را که مافوقش به آن داده است، در صف قرار می دهد و آنها آن را در طرحی که در زمان تولید وجود داشت، ارائه می دهند. و تا زمانی که این صف "پوسیده" نشود، هیچ کاری نمی توان در مورد آن انجام داد.

شاید... اما این یک عصا وحشتناک است؟!.. نه، قبل از اینکه به ایده های دیوانه کننده فکر کنید، اجازه دهید به کد مشتریان رسمی نگاه کنیم. در نسخه اندروید ما هیچ تجزیه‌کننده TL پیدا نمی‌کنیم، اما یک فایل سنگین (GitHub از لمس آن خودداری می‌کند) با (سریال‌زدایی) پیدا می‌کنیم. در اینجا قطعات کد آمده است:

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

بچه ها، حتی نمی توانید تصمیم بگیرید که داخل یک لایه چیست؟! خوب، خوب، بیایید بگوییم "دو" با یک خطا منتشر شد، خوب، این اتفاق می افتد، اما سه؟.. بلافاصله، دوباره همان چنگک؟ این چه نوع پورنوگرافی است، ببخشید؟

به هر حال، در کد منبع تلگرام دسکتاپ، اتفاق مشابهی رخ می دهد - اگر چنین است، چندین commit پشت سر هم به طرح شماره لایه آن را تغییر نمی دهد، اما چیزی را برطرف می کند. در شرایطی که هیچ منبع رسمی داده برای طرح وجود ندارد، به جز منابع مشتری رسمی از کجا می توان آن را دریافت کرد؟ و اگر آن را از آنجا بگیرید، تا زمانی که همه روش‌ها را آزمایش نکنید، نمی‌توانید مطمئن باشید که این طرح کاملاً درست است.

چطور می توان این را حتی آزمایش کرد؟ امیدوارم طرفداران تست های یونیتی، عملکردی و غیره در نظرات به اشتراک بگذارند.

خوب، بیایید به یک کد دیگر نگاه کنیم:

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;

این نظر "به صورت دستی ایجاد شده" نشان می دهد که فقط بخشی از این فایل به صورت دستی نوشته شده است (می توانید کل کابوس تعمیر و نگهداری را تصور کنید؟) و بقیه توسط ماشین تولید شده است. با این حال، پس از آن سوال دیگری مطرح می شود - اینکه منابع در دسترس هستند نه کاملا (یک حباب GPL در هسته لینوکس)، اما این موضوع قبلاً برای قسمت دوم است.

ولی کافیه بیایید به پروتکلی برویم که تمام این سریال سازی در بالای آن اجرا می شود.

ام تی پروتو

بنابراین، بیایید باز کنیم توضیحات کلی и شرح مفصل پروتکل و اولین چیزی که به آن برخورد می کنیم اصطلاحات است. و با فراوانی همه چیز. به طور کلی، به نظر می رسد این ویژگی اختصاصی تلگرام باشد - فراخوانی چیزها در مکان های مختلف، یا چیزهای مختلف با یک کلمه، یا برعکس (به عنوان مثال، در یک API سطح بالا، اگر بسته استیکر را ببینید، اینطور نیست. چی فکر کردی).

به عنوان مثال، "پیام" و "جلسه" در اینجا به معنای چیزی متفاوت از رابط معمول مشتری تلگرام هستند. خوب، همه چیز با پیام واضح است، می توان آن را با اصطلاحات OOP تفسیر کرد، یا به سادگی کلمه "بسته" نامید - این یک سطح انتقال پایین است، پیام های مشابهی در رابط وجود ندارد، پیام های خدمات زیادی وجود دارد. . اما جلسه... اما اول از همه.

لایه حمل و نقل

اولین چیز حمل و نقل است. آنها در مورد 5 گزینه به ما خواهند گفت:

  • TCP
  • جیب وب
  • سوکت وب از طریق HTTPS
  • HTTP
  • HTTPS

واسیلی، [15.06.18 15:04] انتقال UDP نیز وجود دارد، اما مستند نیست

و TCP در سه نوع

اولین مورد مشابه UDP از طریق TCP است، هر بسته شامل یک شماره دنباله و crc است
چرا خواندن اسناد روی گاری اینقدر دردناک است؟

خب، الان هست TCP در حال حاضر در 4 نوع:

  • مختصر
  • حد واسط
  • متوسط ​​روکش دار
  • کامل

خوب، خوب، Padded intermediate برای MTProxy، این بعداً به دلیل رویدادهای شناخته شده اضافه شد. اما چرا دو نسخه دیگر (در مجموع سه نسخه) در حالی که می توانید با یکی از آنها کنار بیایید؟ هر چهار مورد اساساً فقط در نحوه تنظیم طول و بار MTProto اصلی متفاوت هستند که بیشتر مورد بحث قرار خواهد گرفت:

  • در خلاصه شده 1 یا 4 بایت است، اما نه 0xef، سپس بدنه
  • در Intermediate این 4 بایت طول و یک فیلد است و اولین بار مشتری باید ارسال کند 0xeeeeeeee برای نشان دادن متوسط ​​بودن آن
  • به طور کامل اعتیاد آورترین، از نقطه نظر یک نتورکر: طول، شماره دنباله، و نه آن چیزی که عمدتا MTProto، بدن، CRC32 است. بله، همه اینها در بالای TCP است. که انتقال قابل اعتمادی را در قالب یک جریان بایت متوالی برای ما فراهم می کند؛ به هیچ دنباله ای نیاز نیست، به خصوص جمع های چک. بسیار خوب، حالا یکی به من اعتراض می کند که TCP دارای چک جمع 16 بیتی است، بنابراین خرابی داده ها اتفاق می افتد. عالی است، اما ما در واقع یک پروتکل رمزنگاری با هش های بیشتر از 16 بایت داریم، همه این خطاها - و حتی بیشتر - توسط یک عدم تطابق SHA در سطح بالاتر شناسایی می شوند. علاوه بر این هیچ نکته ای در CRC32 وجود ندارد.

بیایید خلاصه شده را که در آن یک بایت طول ممکن است، با Intermediate مقایسه کنیم که «در صورت نیاز به تراز کردن داده‌های 4 بایتی» را توجیه می‌کند، که کاملاً مزخرف است. چه، اعتقاد بر این است که برنامه نویسان تلگرام آنقدر ناتوان هستند که نمی توانند داده ها را از یک سوکت در یک بافر تراز شده بخوانند؟ شما هنوز باید این کار را انجام دهید، زیرا خواندن می تواند هر تعداد بایت را به شما برگرداند (و برای مثال سرورهای پراکسی نیز وجود دارند ...). یا از طرف دیگر، اگر ما همچنان دارای بالشتک سنگین بالای 16 بایت هستیم، چرا Abridged را مسدود کنیم - 3 بایت ذخیره کنید گاهی ?

این تصور به وجود می آید که نیکولای دوروف واقعاً دوست دارد چرخ ها، از جمله پروتکل های شبکه، را بدون نیاز واقعی واقعی اختراع کند.

سایر گزینه های حمل و نقل، از جمله وب و MTProxy در حال حاضر در نظر نخواهیم گرفت، شاید در پست دیگری اگر درخواستی باشد. در مورد همین MTProxy، فقط اکنون به یاد بیاوریم که مدت کوتاهی پس از انتشار آن در سال 2018، ارائه دهندگان به سرعت یاد گرفتند که آن را مسدود کنند، که برای مسدود کردن بای پستوسط اندازه بسته! و همچنین این واقعیت که سرور MTProxy نوشته شده (دوباره توسط والتمن) به زبان C بیش از حد به مشخصات لینوکس گره خورده بود، اگرچه این امر اصلاً مورد نیاز نبود (فیل کولین تأیید خواهد کرد) و سرور مشابه در Go یا Node.js در کمتر از صد خط قرار می گیرد.

اما در مورد سواد فنی این افراد پس از بررسی سایر موارد در پایان قسمت نتیجه گیری خواهیم کرد. در حال حاضر، اجازه دهید به لایه 5 OSI برویم، جلسه - که آنها جلسه MTProto را روی آن قرار دادند.

کلیدها، پیام ها، جلسات، Diffie-Hellman

آنها آن را نه کاملاً درست در آنجا قرار دادند... یک جلسه همان جلسه ای نیست که در رابط تحت Active sessions قابل مشاهده است. اما به ترتیب.

نقد پروتکل و رویکردهای سازمانی تلگرام. بخش 1، فنی: تجربه نوشتن یک مشتری از ابتدا - TL، MT

بنابراین ما یک رشته بایت با طول شناخته شده از لایه انتقال دریافت کردیم. این یا یک پیام رمزگذاری شده یا متن ساده است - اگر هنوز در مرحله توافق کلید هستیم و واقعاً آن را انجام می دهیم. در مورد کدام یک از این دسته از مفاهیم به نام "کلید" صحبت می کنیم؟ بیایید این موضوع را برای خود تیم تلگرام روشن کنیم (از اینکه مستندات خودم را با مغز خسته ساعت 4 صبح از انگلیسی ترجمه کردم عذرخواهی می‌کنم، گذاشتن برخی از عبارات راحت‌تر بود):

دو نهاد به نام وجود دارد جلسه - یکی در رابط کاربری مشتریان رسمی تحت "جلسات جاری"، که در آن هر جلسه مربوط به کل دستگاه / سیستم عامل است.
دومین جلسه MTProto، که شماره دنباله پیام (به معنای سطح پایین) را در خود دارد و کدام ممکن است بین اتصالات TCP مختلف دوام بیاورد. چندین جلسه MTProto را می توان به طور همزمان نصب کرد، به عنوان مثال، برای سرعت بخشیدن به دانلود فایل.

بین این دوتا جلسات یک مفهوم وجود دارد مجوز. در مورد منحط می توان گفت که جلسه رابط کاربری مثل این هست که مجوز، اما افسوس که همه چیز پیچیده است. بیایید نگاه بیندازیم:

  • کاربر در دستگاه جدید ابتدا تولید می کند کلید تایید و آن را به حساب خود محدود می کند، به عنوان مثال از طریق پیامک - به همین دلیل است مجوز
  • در داخل اول اتفاق افتاد جلسه MTProto، که دارای session_id درون خودت
  • در این مرحله، ترکیب مجوز и session_id می تواند نامیده شود نمونه - این کلمه در اسناد و کد برخی از مشتریان ظاهر می شود
  • سپس، مشتری می تواند باز شود برخی از جلسات MTProto تحت همان کلید تایید - به همان DC.
  • سپس، یک روز مشتری باید فایل را از آن درخواست کند دی سی دیگر - و برای این DC یک DC جدید تولید خواهد شد کلید تایید !
  • تا به سامانه اطلاع دهد که کاربر جدیدی نیست که ثبت نام می کند بلکه همان است مجوز (جلسه رابط کاربری، مشتری از تماس های API استفاده می کند auth.exportAuthorization در خانه DC auth.importAuthorization در دی سی جدید
  • همه چیز یکسان است، ممکن است چندین باز باشد جلسات MTProto (هر کدام با خود session_id) به این DC جدید، در زیر خود را کلید تایید.
  • در نهایت، مشتری ممکن است از Prefect Forward Secrecy بخواهد. هر کلید تایید بود دائمي کلید - در هر DC - و مشتری می تواند تماس بگیرد auth.bindTempAuthKey برای استفاده موقت کلید تایید - و دوباره، فقط یک temp_auth_key در هر DC، مشترک برای همه جلسات MTProto به این دی سی

توجه داشته باشید که نمک (و نمک های آینده) نیز یکی است کلید تایید آن ها بین همه به اشتراک گذاشته شده است جلسات MTProto به همان دی سی

"بین اتصالات TCP مختلف" به چه معناست؟ پس این یعنی چیزی مثل کوکی مجوز در یک وب سایت - بسیاری از اتصالات TCP به یک سرور مشخص را حفظ می کند (بقا می کند)، اما یک روز خراب می شود. فقط برخلاف HTTP، در MTProto پیام‌های داخل یک جلسه به‌طور ترتیبی شماره‌گذاری و تأیید می‌شوند؛ اگر وارد تونل شوند، اتصال قطع شده است - پس از برقراری یک اتصال جدید، سرور با مهربانی همه چیزهایی را که در جلسه قبلی ارائه نکرده است ارسال می‌کند. اتصال TCP

با این حال، اطلاعات فوق پس از چندین ماه بررسی خلاصه می شود. در این بین آیا ما کلاینت خود را از ابتدا پیاده سازی می کنیم؟ - برگردیم به اول.

پس بیایید تولید کنیم auth_key بر نسخه های Diffie-Hellman از تلگرام. بیایید سعی کنیم مستندات را درک کنیم ...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(داده) + داده + (هر بایت تصادفی); به طوری که طول آن برابر با 255 بایت باشد.
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] من هنوز به درخواست appid نرسیدم

من این درخواست را برای DH فرستادم

و در داک حمل و نقل می گوید که می تواند با 4 بایت کد خطا پاسخ دهد. همین

خوب به من گفت -404 پس چی؟

بنابراین من به او گفتم: "این مزخرفات خود را رمزگذاری شده با کلید سرور با اثر انگشت مانند این، من DH می خواهم" و او با یک 404 احمقانه پاسخ داد.

نظر شما در مورد این پاسخ سرور چیست؟ چه باید کرد؟ کسی نیست که بپرسد (اما بیشتر در مورد آن در قسمت دوم).

در اینجا تمام علاقه در اسکله انجام می شود

من کار دیگری ندارم، فقط آرزو داشتم اعداد را به عقب و جلو تبدیل کنم

دو عدد 32 بیتی من آنها را مثل بقیه بسته بندی کردم

اما نه، این دو باید ابتدا به عنوان BE به خط اضافه شوند

وادیم گونچاروف، [20.06.18 15:49] و به خاطر این 404؟

واسیلی، [20.06.18 15:49] بله!

وادیم گونچاروف، [20.06.18 15:50] بنابراین من نمی فهمم او چه چیزی را "نیافت"

واسیلی، [20.06.18 15:50] تقریبا

من نتوانستم چنین تجزیه ای را به عوامل اول پیدا کنم٪

ما حتی گزارش خطا را مدیریت نکردیم

واسیلی، [20.06.18 20:18] اوه، MD5 هم هست. در حال حاضر سه هش مختلف

اثر انگشت کلید به صورت زیر محاسبه می شود:

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

SHA1 و sha2

پس بیایید آن را قرار دهیم auth_key ما با استفاده از Diffie-Hellman 2048 بیت در اندازه دریافت کردیم. بعدش چی؟ بعد متوجه می شویم که 1024 بیت پایین این کلید به هیچ وجه استفاده نمی شود ... اما بیایید فعلاً در مورد این فکر کنیم. در این مرحله، ما یک راز مشترک با سرور داریم. یک آنالوگ از جلسه TLS ایجاد شده است که یک روش بسیار گران است. اما سرور هنوز چیزی در مورد اینکه ما کی هستیم نمی داند! هنوز نه، در واقع. مجوز. آن ها اگر به عنوان «گذرواژه ورود» فکر می‌کردید، همانطور که زمانی در ICQ انجام می‌دادید، یا حداقل «کلید ورود»، مانند SSH (به عنوان مثال، در برخی از gitlab/github). یک ناشناس دریافت کردیم. اگر سرور به ما بگوید «این شماره‌های تلفن توسط DC دیگری سرویس می‌شوند» چه می‌شود؟ یا حتی "شماره تلفن شما ممنوع است"؟ بهترین کاری که می توانیم انجام دهیم این است که کلید را نگه داریم به این امید که مفید باشد و تا آن زمان پوسیده نشود.

به هر حال، ما آن را با رزرو "دریافت" کردیم. مثلا آیا ما به سرور اعتماد داریم؟ اگه تقلبی باشه چی؟ بررسی های رمزنگاری مورد نیاز است:

واسیلی، [21.06.18 17:53] آنها به مشتریان تلفن همراه پیشنهاد می‌کنند تا یک عدد 2 کیلوبیتی را برای اولیه بودن٪ بررسی کنند.

اما اصلاً مشخص نیست، نافیجوا

واسیلی، [21.06.18 18:02] سند نمی گوید که اگر معلوم شد ساده نیست چه باید کرد

گفته نشده است. بیایید ببینیم کلاینت رسمی اندروید در این مورد چه می کند؟ آ این چیزی است که (و بله، کل فایل جالب است) - همانطور که می گویند، من فقط این را اینجا می گذارم:

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

نه، البته هنوز آنجاست مقداری تست هایی برای اولیه بودن یک عدد وجود دارد، اما شخصاً دیگر دانش کافی از ریاضیات ندارم.

خوب، کلید اصلی را گرفتیم. برای ورود به سیستم، یعنی درخواست ها را ارسال کنید، باید با استفاده از AES رمزگذاری بیشتری انجام دهید.

کلید پیام به عنوان 128 بیت میانی SHA256 از بدنه پیام (شامل جلسه، شناسه پیام و غیره)، از جمله بایت‌های padding که توسط 32 بایت برگرفته از کلید مجوز اضافه شده است، تعریف می‌شود.

Vasily, [22.06.18 14:08] میانگین، عوضی، بیت

بدست آورد auth_key. همه. فراتر از آنها ... از سند مشخص نیست. با خیال راحت کد منبع باز را مطالعه کنید.

توجه داشته باشید که MTProto 2.0 به 12 تا 1024 بایت نیاز دارد، همچنان مشروط به اینکه طول پیام حاصل بر 16 بایت تقسیم شود.

بنابراین چه مقدار بالشتک باید اضافه کنید؟

و بله، 404 نیز در صورت خطا وجود دارد

اگر کسی نمودار و متن اسناد را به دقت مطالعه کرد، متوجه شد که MAC در آنجا وجود ندارد. و اینکه AES در یک حالت IGE خاص استفاده می شود که در هیچ جای دیگری استفاده نمی شود. آنها، البته، در این مورد در سؤالات متداول خود می نویسند ... در اینجا، مانند، خود کلید پیام نیز هش SHA داده های رمزگشایی شده است که برای بررسی یکپارچگی استفاده می شود - و در صورت عدم تطابق، اسناد به دلایلی توصیه می کند بی سر و صدا آنها را نادیده بگیرید (اما در مورد امنیت چه می شود، اگر آنها ما را بشکنند چه می شود؟).

من رمزنگار نیستم، شاید از نظر تئوری این حالت در این مورد ایرادی نداشته باشد. اما من به وضوح می توانم یک مشکل کاربردی را نام ببرم، از دسکتاپ تلگرام به عنوان مثال استفاده کنم. کش محلی (همه این D877F783D5D3EF8C) را به همان روشی که پیام ها در MTProto (فقط در این مورد نسخه 1.0) رمزگذاری می کند، یعنی. ابتدا کلید پیام، سپس خود داده (و جایی به کناری بزرگ اصلی auth_key 256 بایت، بدون آن msg_key بلا استفاده). بنابراین، مشکل در فایل های بزرگ قابل توجه می شود. یعنی، شما باید دو نسخه از داده ها را نگه دارید - رمزگذاری شده و رمزگشایی شده. و اگر مثلاً مگابایت یا پخش ویدیو وجود دارد؟. طرح‌های کلاسیک با MAC بعد از متن رمز به شما امکان می‌دهد آن را به صورت جریانی بخوانید و بلافاصله آن را انتقال دهید. اما با MTProto مجبور خواهید بود در ابتدا کل پیام را رمزگذاری یا رمزگشایی کنید، تنها سپس آن را به شبکه یا دیسک منتقل کنید. بنابراین، در آخرین نسخه های تلگرام دسکتاپ در کش در user_data فرمت دیگری نیز استفاده می شود - با AES در حالت CTR.

واسیلی، [21.06.18 01:27] اوه، فهمیدم IGE چیست: IGE اولین تلاش برای "حالت رمزگذاری احراز هویت" بود که در اصل برای Kerberos بود. این یک تلاش ناموفق بود (از یکپارچگی محافظت نمی کند)، و باید حذف می شد. این آغاز یک تلاش 20 ساله برای یک حالت رمزگذاری احراز هویت بود که کار می کند، که اخیراً در حالت هایی مانند OCB و GCM به اوج خود رسید.

و حالا استدلال ها از طرف سبد خرید:

تیم پشت تلگرام به رهبری نیکولای دوروف از شش قهرمان ACM تشکیل شده است که نیمی از آنها دکترای ریاضی هستند. حدود دو سال طول کشید تا نسخه فعلی MTProto را عرضه کنند.

جالبه. دو سال در سطح پایین تر

یا فقط می توانید tls را مصرف کنید

خوب، بیایید بگوییم که رمزگذاری و سایر نکات ظریف را انجام داده ایم. آیا در نهایت امکان ارسال درخواست های سریالی در TL و غیر سریالی کردن پاسخ ها وجود دارد؟ پس چه چیزی و چگونه باید بفرستید؟ در اینجا، بیایید بگوییم، روش initConnection، شاید این باشد؟

Vasily، [25.06.18 18:46] اتصال را راه اندازی می کند و اطلاعات را در دستگاه و برنامه کاربر ذخیره می کند.

app_id، device_model، system_version، app_version و lang_code را می پذیرد.

و مقداری پرس و جو

مستندات مثل همیشه با خیال راحت متن باز را مطالعه کنید

اگر همه چیز تقریباً با invokeWithLayer روشن بود، پس اینجا چه مشکلی دارد؟ به نظر می رسد، فرض کنید ما داریم - مشتری قبلاً چیزی برای پرسیدن از سرور داشته است - درخواستی وجود دارد که می خواستیم ارسال کنیم:

Vasily, [25.06.18 19:13] با قضاوت بر اساس کد، اولین تماس در این مزخرف پیچیده می شود، و خود مزخرف در invokewithlayer پیچیده می شود.

چرا initConnection نمی تواند یک تماس جداگانه باشد، اما باید یک wrapper باشد؟ بله، همانطور که مشخص شد، این کار باید هر بار در ابتدای هر جلسه انجام شود و نه یک بار، مانند کلید اصلی. ولی! کاربر غیر مجاز نمی تواند آن را فراخوانی کند! اکنون به مرحله ای رسیده ایم که قابل اجرا است این یکی صفحه مستندات - و به ما می گوید که ...

تنها بخش کوچکی از متدهای API در دسترس کاربران غیرمجاز است:

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

اولین آنها، 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 مقدار سوم است

بله، از آن زمان، البته، اسناد به روز شده است. اگرچه ممکن است به زودی دوباره بی ربط شود. یک توسعه دهنده مبتدی چگونه باید بداند؟ شاید اگر درخواست خود را ثبت کنید به شما اطلاع دهند؟ واسیلی این کار را کرد ، اما افسوس که آنها چیزی برای او ارسال نکردند (دوباره در قسمت دوم در مورد این صحبت خواهیم کرد).

... متوجه شدید که ما قبلاً به نوعی به API منتقل شده ایم. به سطح بعدی بروید، و چیزی را در موضوع MTProto از دست داده اید؟ تعجبی نداره:

Vasily، [28.06.18 02:04] Mm، آنها در حال جستجوی برخی از الگوریتم های موجود در e2e هستند.

Mtproto الگوریتم‌ها و کلیدهای رمزگذاری را برای هر دو دامنه و همچنین کمی ساختار wrapper تعریف می‌کند.

اما آنها به طور مداوم سطوح مختلف پشته را مخلوط می کنند، بنابراین همیشه مشخص نیست که mtproto به کجا خاتمه می یابد و سطح بعدی شروع می شود.

چگونه مخلوط می شوند؟ خب، در اینجا همان کلید موقت برای PFS است (به هر حال، دسکتاپ تلگرام نمی تواند این کار را انجام دهد). توسط یک درخواست 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

به شما یادآوری می کنیم که تنها یک نمک برای کل DC وجود دارد. چرا در مورد او بدانید؟ نه فقط به این دلیل که یک درخواست وجود دارد 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 بیتی تعریف می‌شود که پایین‌ترین بیت‌های آن دوباره دارای جادوی "سرور-نه-سرور" هستند، و بقیه یک مهر زمانی یونیکس است، از جمله بخش کسری، که 32 بیت به سمت چپ منتقل شده است. آن ها مهر زمان فی نفسه (و پیام هایی با زمان های بسیار متفاوت توسط سرور رد می شوند). از اینجا معلوم می شود که به طور کلی این یک شناسه است که برای مشتری جهانی است. با توجه به آن - بیایید به یاد داشته باشیم session_id - ما تضمین می کنیم: تحت هیچ شرایطی نمی توان پیامی را که برای یک جلسه در نظر گرفته شده است به یک جلسه دیگر ارسال کرد. یعنی معلوم می شود که قبلاً وجود دارد سه سطح - جلسه، شماره جلسه، شناسه پیام. چرا چنین پیچیدگی، این رمز و راز بسیار بزرگ است.

بنابراین، msg_id مورد نیاز برای ...

RPC: درخواست ها، پاسخ ها، خطاها. تاییدیه ها

همانطور که ممکن است متوجه شده باشید، هیچ نوع یا تابع خاصی از "make an RPC request" در هیچ کجای نمودار وجود ندارد، اگرچه پاسخ هایی وجود دارد. بالاخره ما پیام های مرتبط با محتوا داریم! به این معنا که، هر پیام می تواند یک درخواست باشد! یا نبودن گذشته از همه اینها، از هر کدام وجود دارد msg_id. اما پاسخ هایی وجود دارد:

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

در اینجا مشخص می شود که این پیام به کدام پیام پاسخ می دهد. بنابراین، در سطح بالای API، باید به خاطر بسپارید که تعداد درخواست شما چقدر بوده است - من فکر می کنم نیازی به توضیح نیست که کار ناهمزمان است و می تواند چندین درخواست همزمان در حال انجام باشد. پاسخ هایی که به هر ترتیبی قابل برگشت هستند؟ در اصل، از این و پیام‌های خطایی مانند هیچ کارگری، معماری پشت آن را می‌توان ردیابی کرد: سروری که اتصال 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_Х_MISSING. خوب، یعنی شما همچنان به این خط نیاز خواهید داشت تجزیه. به عنوان مثال FLOOD_WAIT_3600 به این معنی است که شما باید یک ساعت صبر کنید، و PHONE_MIGRATE_5، که شماره تلفنی با این پیش شماره باید در دی سی پنجم ثبت شود. ما یک زبان نوع داریم، درست است؟ ما نیازی به آرگومان از یک رشته نداریم، آرگومان های معمولی این کار را می کنند، بسیار خوب.

باز هم، این در صفحه پیام های سرویس نیست، اما، همانطور که قبلاً در این پروژه معمول است، می توان اطلاعات را پیدا کرد. در صفحه اسناد دیگر. یا سوء ظن ایجاد کند. اول، نگاه کنید، تایپ / نقض لایه - RpcError می توان تو در تو قرار داد RpcResult. چرا بیرون نه؟ چه چیزهایی را در نظر نگرفتیم؟.. بر این اساس، ضمانت آن کجاست RpcError ممکن است در آن تعبیه نشده باشد RpcResult، اما مستقیم یا تودرتو در نوع دیگری باشد؟.. و اگر نمی تواند، چرا در سطح بالا نیست، i.e. آن گم شده است req_msg_id ؟ ..

اما بیایید در مورد پیام های خدماتی ادامه دهیم. مشتری ممکن است فکر کند که سرور برای مدت طولانی فکر می کند و این درخواست فوق العاده را ارائه می دهد:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

سه پاسخ احتمالی برای این سؤال وجود دارد که باز هم با مکانیسم تأیید تلاقی می‌کند؛ تلاش برای فهمیدن اینکه چه چیزی باید باشد (و لیست کلی انواعی که نیاز به تأیید ندارند) به عنوان تکلیف به خواننده واگذار می‌شود (توجه: اطلاعات موجود در سورس کد دسکتاپ تلگرام کامل نیست).

اعتیاد به مواد مخدر: وضعیت پیام

در کل خیلی جاها توی TL و MTProto و به طور کلی تلگرام حس لجبازی به جا میذارن اما از روی ادب و درایت و غیره مهارت های نرم افزاری ما مودبانه در مورد آن سکوت کردیم و فحاشی های دیالوگ ها را سانسور کردیم. با این حال، این مکانОبیشتر صفحه مربوط به پیام هایی در مورد پیام ها حتی برای من که مدت‌هاست با پروتکل‌های شبکه کار می‌کنم و دوچرخه‌هایی با درجات مختلف کج بودن را دیده‌ام، شوکه‌کننده است.

بی ضرر و با تایید شروع می شود. بعد به ما می گویند

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... بقیه اعداد کجا هستند، تامی؟

در مستندات آمده است:

هدف این است که مقادیر error_code گروه بندی شوند (error_code >> 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;
    پس از دریافت، پیام به گونه‌ای پردازش می‌شود که گویی wrapper آنجا نبوده است. با این حال، اگر به طور قطع مشخص شود که پیام orig_message.msg_id دریافت شده است، پیام جدید پردازش نمی شود (در حالی که در همان زمان، آن و orig_message.msg_id تایید می شوند). مقدار orig_message.msg_id باید کمتر از msg_id ظرف باشد.

حتی در مورد چه چیزی سکوت کنیم msgs_state_info دوباره گوش های TL ناتمام بیرون زده است (ما به یک بردار بایت نیاز داشتیم و در دو بیت پایین یک enum و در دو بیت بالاتر پرچم ها وجود داشت). نکته متفاوت است. آیا کسی می داند که چرا همه اینها در عمل است؟ در یک مشتری واقعی ضروری است؟.. به سختی، اما اگر فردی درگیر اشکال زدایی و در حالت تعاملی باشد، می توان سودی را تصور کرد - از سرور بپرسید که چه چیزی و چگونه. اما در اینجا درخواست ها شرح داده شده است رفت و برگشت.

نتیجه این است که هر یک از طرفین نه تنها باید پیام‌ها را رمزگذاری و ارسال کنند، بلکه باید داده‌های مربوط به خود، در مورد پاسخ‌ها به آنها را برای مدت زمان نامعلومی ذخیره کنند. مستندات زمان‌بندی یا کاربرد عملی این ویژگی‌ها را توصیف نمی‌کند. به هیچ وجه. شگفت انگیزترین چیز این است که آنها در واقع در کد مشتریان رسمی استفاده می شوند! ظاهراً چیزی به آنها گفته شده که در اسناد عمومی درج نشده است. از روی کد بفهمید چرا، دیگر به سادگی مورد TL نیست - این یک بخش منطقی (نسبتا) ایزوله نیست، بلکه قطعه ای است که به معماری برنامه گره خورده است، یعنی. برای درک کد برنامه به زمان قابل توجهی بیشتری نیاز دارد.

پینگ و زمان بندی صف ها

از همه چیز، اگر حدس‌های مربوط به معماری سرور (توزیع درخواست‌ها در پشتیبان‌ها) را به خاطر بیاوریم، یک چیز نسبتاً غم انگیز به دنبال دارد - با وجود تمام تضمین‌های تحویل در TCP (یا داده‌ها تحویل داده می‌شوند یا از شکاف مطلع خواهید شد، اما داده‌ها قبل از بروز مشکل تحویل داده می‌شوند)، که تأییدیه‌ها در خود MTProto - بدون تضمین. سرور به راحتی می تواند پیام شما را از دست بدهد یا از بین ببرد و هیچ کاری نمی توان در مورد آن انجام داد، فقط از انواع مختلف عصا استفاده کنید.

و اول از همه - صف های پیام. خوب، با یک چیز همه چیز از همان ابتدا واضح بود - یک پیام تأیید نشده باید ذخیره شود و دوباره ارسال شود. و بعد از چه زمانی؟ و مسخره او را می شناسد. شاید آن پیام های سرویس معتاد به نوعی این مشکل را با عصا حل کند، مثلاً در دسکتاپ تلگرام حدود 4 صف مربوط به آنها وجود دارد (شاید بیشتر، همانطور که قبلاً ذکر شد، برای این کار باید به طور جدی تری به کد و معماری آن بپردازید؛ در عین حال. زمان، ما می دانیم که نمی توان آن را به عنوان نمونه گرفت؛ تعداد معینی از انواع طرح MTProto در آن استفاده نمی شود).

چرا این اتفاق می افتد؟ احتمالاً برنامه نویسان سرور قادر به اطمینان از قابلیت اطمینان در کلاستر یا حتی بافر در بالانس جلویی نبودند و این مشکل را به مشتری منتقل کردند. از ناامیدی، واسیلی سعی کرد با استفاده از الگوریتم هایی از TCP یک گزینه جایگزین را اجرا کند، تنها با دو صف - اندازه گیری RTT به سرور و تنظیم اندازه "پنجره" (در پیام ها) بسته به تعداد درخواست های تایید نشده. یعنی چنین اکتشافی تقریبی برای ارزیابی بار سرور این است که چه تعداد از درخواست‌های ما را می‌تواند همزمان بجوید و از دست ندهد.

خوب، یعنی می فهمی، درست است؟ اگر مجبور شوید دوباره TCP را در بالای پروتکلی که روی TCP اجرا می شود پیاده سازی کنید، این نشان دهنده یک پروتکل بسیار ضعیف طراحی شده است.

اوه بله، چرا به بیش از یک صف نیاز دارید، و به هر حال این برای شخصی که با یک API سطح بالا کار می کند چه معنایی دارد؟ ببینید، شما یک درخواست می کنید، آن را سریال می کنید، اما اغلب نمی توانید بلافاصله آن را ارسال کنید. چرا؟ زیرا پاسخ خواهد بود msg_id، که موقتی استаمن یک برچسب هستم که بهتر است انتساب آن تا دیرتر به تعویق بیفتد - در صورتی که سرور به دلیل عدم تطابق زمانی بین ما و او آن را رد کند (البته می توانیم عصایی بسازیم که زمان ما را از زمان حال تغییر دهد. با اضافه کردن یک دلتا محاسبه شده از پاسخ های سرور به سرور - مشتریان رسمی این کار را انجام می دهند، اما به دلیل بافر کردن، خام و نادرست است). بنابراین، هنگامی که درخواستی را با یک تابع محلی از کتابخانه انجام می دهید، پیام مراحل زیر را طی می کند:

  1. در یک صف قرار دارد و منتظر رمزگذاری است.
  2. منصوب msg_id و پیام به صف دیگری رفت - امکان ارسال. ارسال به سوکت
  3. الف) سرور به MsgsAck پاسخ داد - پیام تحویل داده شد، ما آن را از "صف دیگر" حذف می کنیم.
    ب) یا برعکس، او چیزی را دوست نداشت، او به badmsg پاسخ داد - ارسال مجدد از "صف دیگر"
    ج) هیچ چیز مشخص نیست، پیام باید از یک صف دیگر ارسال شود - اما دقیقاً مشخص نیست چه زمانی.
  4. سرور بالاخره جواب داد RpcResult - پاسخ (یا خطا) واقعی - نه تنها تحویل داده شده، بلکه پردازش شده است.

شاید، استفاده از ظروف می تواند تا حدی مشکل را حل کند. این زمانی است که یک دسته از پیام ها در یک بسته بندی می شوند و سرور با یک تأیید به همه آنها در یک لحظه پاسخ می دهد. msg_id. اما او همچنین این بسته را، اگر مشکلی پیش بیاید، به طور کامل رد خواهد کرد.

و در این مرحله ملاحظات غیر فنی مطرح می شود. به تجربه، عصای زیر بغل زیادی دیده ایم و علاوه بر این، اکنون نمونه های بیشتری از مشاوره و معماری بد را خواهیم دید - آیا در چنین شرایطی ارزش اعتماد و اتخاذ چنین تصمیماتی را دارد؟ سوال لفاظی است (البته نه).

ما در مورد چه چیزی صحبت می کنیم؟ اگر در مورد موضوع "پیام های دارویی در مورد پیام ها" همچنان می توانید با ایراداتی مانند "تو احمقی، نقشه درخشان ما را درک نکردی!" (بنابراین ابتدا مستندات را بنویسید، همانطور که افراد عادی باید، با منطق و مثال هایی از مبادله بسته، سپس صحبت می کنیم)، سپس زمان بندی/تایم اوت ها یک سوال کاملاً کاربردی و خاص است، همه چیز در اینجا برای مدت طولانی شناخته شده است. مستندات در مورد وقفه های زمانی به ما چه می گوید؟

یک سرور معمولاً با استفاده از یک پاسخ RPC دریافت پیام از یک کلاینت (معمولاً یک پرس و جو RPC) را تأیید می کند. اگر یک پاسخ برای مدت طولانی در دسترس باشد، یک سرور ممکن است ابتدا یک تأیید رسید و کمی بعد، خود پاسخ RPC را ارسال کند.

یک کلاینت معمولاً دریافت یک پیام از یک سرور (معمولاً یک پاسخ RPC) را با افزودن یک تأیید به درخواست RPC بعدی در صورتی که خیلی دیر ارسال نشده باشد تأیید می کند (اگر مثلاً 60-120 ثانیه پس از دریافت ایجاد شود. از یک پیام از سرور). با این حال، اگر برای مدت طولانی دلیلی برای ارسال پیام به سرور وجود نداشته باشد یا اگر تعداد زیادی پیام تأیید نشده از طرف سرور وجود داشته باشد (مثلاً بیش از 16)، مشتری یک تأییدیه مستقل ارسال می کند.

... ترجمه می کنم: ما خودمان نمی دانیم چقدر و چگونه به آن نیاز داریم، پس فرض کنیم که بگذار اینطور باشد.

و در مورد پینگ:

پیام های پینگ (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

یک پاسخ معمولاً به همان اتصال برگردانده می شود:

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

این پیام ها نیازی به تایید ندارند. یک پنگ فقط در پاسخ به یک پینگ منتقل می شود در حالی که یک پینگ می تواند توسط هر دو طرف شروع شود.

بسته شدن اتصال معوق + PING

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

مثل پینگ کار میکنه علاوه بر این، پس از دریافت، سرور یک تایمر را راه اندازی می کند که چند ثانیه بعد اتصال فعلی disconnect_delay را می بندد، مگر اینکه پیام جدیدی از همان نوع دریافت کند که به طور خودکار تمام تایمرهای قبلی را بازنشانی می کند. برای مثال اگر کلاینت هر 60 ثانیه یک بار این پینگ ها را ارسال کند، ممکن است disconnect_delay را برابر با 75 ثانیه قرار دهد.

دیوانه ای؟! در 60 ثانیه، قطار وارد ایستگاه می شود، مسافران را پیاده می کند و دوباره در تونل از دست می دهد. در 120 ثانیه، در حالی که آن را می شنوید، به یکی دیگر می رسد و به احتمال زیاد اتصال قطع می شود. خوب، واضح است که پاها از کجا می آیند - "من صدای زنگ را شنیدم، اما نمی دانم کجاست"، الگوریتم Nagl و گزینه TCP_NODELAY وجود دارد که برای کار تعاملی در نظر گرفته شده است. اما، ببخشید، مقدار پیش فرض آن را نگه دارید - 200 میلیثانیه اگر واقعاً می‌خواهید چیزی مشابه را به تصویر بکشید و روی چند بسته احتمالی ذخیره کنید، آن را به مدت 5 ثانیه به تعویق بیندازید، یا هر چیزی که اکنون زمان پایان پیام «کاربر در حال تایپ کردن است...» است. اما نه بیشتر.

و در نهایت پینگ. یعنی بررسی زنده بودن اتصال TCP. خنده دار است، اما حدود 10 سال پیش من یک متن انتقادی در مورد پیام رسان خوابگاه دانشکده ما نوشتم - نویسندگان آنجا نیز سرور را از مشتری پینگ کردند و نه برعکس. اما دانشجویان سال سوم یک چیز هستند و یک دفتر بین المللی چیز دیگری است، درست است؟

ابتدا یک برنامه آموزشی کوچک. یک اتصال TCP، در غیاب تبادل بسته، می تواند هفته ها زنده بماند. این بسته به هدف، هم خوب است و هم بد. خوب است اگر یک اتصال SSH به سرور باز داشتید، از رایانه بلند شدید، روتر را مجددا راه اندازی کردید، به محل خود برگشتید - جلسه از طریق این سرور پاره نشده بود (شما چیزی تایپ نکردید، هیچ بسته ای وجود نداشت) ، راحت است. اگر هزاران کلاینت روی سرور وجود داشته باشد، که هر کدام منابع را اشغال کنند، بد است (سلام، Postgres!)، و میزبان مشتری ممکن است مدت‌ها پیش راه‌اندازی مجدد شده باشد - اما ما در مورد آن نمی‌دانیم.

سیستم‌های چت/IM به یک دلیل دیگر در مورد دوم قرار می‌گیرند - وضعیت‌های آنلاین. اگر کاربر "سقوط کرد" ، باید به همکار خود در این مورد اطلاع دهید. در غیر این صورت، شما با اشتباهی روبرو خواهید شد که سازندگان جابر مرتکب شدند (و به مدت 20 سال آن را اصلاح کردند) - کاربر ارتباط خود را قطع کرده است، اما آنها همچنان به نوشتن پیام برای او ادامه می دهند و معتقدند که او آنلاین است (که در این موارد نیز کاملاً از بین رفته است. چند دقیقه قبل از کشف قطع ارتباط). خیر، گزینه TCP_KEEPALIVE، که بسیاری از افرادی که نحوه عملکرد تایمرهای TCP را به طور تصادفی (با تنظیم مقادیر وحشی مانند ده ها ثانیه) نمی دانند، در اینجا کمکی نمی کند - باید مطمئن شوید که نه تنها هسته سیستم عامل دستگاه کاربر زنده است، اما به طور معمول کار می کند، قادر به پاسخگویی است، و خود برنامه (آیا فکر می کنید نمی تواند مسدود شود؟ دسکتاپ تلگرام در اوبونتو 18.04 بیش از یک بار برای من مسدود شد).

به همین دلیل باید پینگ کنید سرور مشتری، و نه برعکس - اگر مشتری این کار را انجام دهد، اگر اتصال قطع شود، پینگ تحویل داده نمی شود، هدف محقق نمی شود.

در تلگرام چه می بینیم؟ دقیقا برعکسه! خوب، این است. به طور رسمی، البته، هر دو طرف می توانند یکدیگر را پینگ کنند. در عمل، مشتریان از عصا استفاده می کنند ping_delay_disconnect، که تایمر را روی سرور تنظیم می کند. خب، ببخشید، این به مشتری نیست که تصمیم بگیرد چه مدت می خواهد بدون پینگ در آنجا زندگی کند. سرور بر اساس بار خود، بهتر می داند. اما، البته، اگر به منابع اهمیتی ندهید، پینوکیوی شیطانی خودتان خواهید بود و یک عصا این کار را می کند...

چگونه باید طراحی می شد؟

من معتقدم که حقایق فوق به وضوح نشان می دهد که تیم تلگرام / VKontakte در زمینه حمل و نقل (و سطح پایین تر) شبکه های کامپیوتری و صلاحیت پایین آنها در امور مربوطه چندان صلاحیت ندارد.

چرا اینقدر پیچیده شد و معماران تلگرام چگونه می توانند اعتراض کنند؟ این واقعیت که آنها سعی کردند جلسه ای بسازند که اتصال TCP از بین برود، یعنی چیزی که اکنون تحویل داده نشده است، بعداً ارائه خواهیم داد. آنها احتمالاً سعی کردند یک حمل و نقل UDP نیز بسازند، اما با مشکلاتی مواجه شدند و آن را رها کردند (به همین دلیل است که اسناد خالی است - چیزی برای لاف زدن وجود نداشت). اما به دلیل عدم درک نحوه عملکرد شبکه ها به طور کلی و TCP به طور خاص، جایی که می توانید به آن تکیه کنید، و کجا باید خودتان آن را انجام دهید (و چگونه)، و تلاش برای ترکیب این کار با رمزنگاری «دو پرنده با یک سنگ، این نتیجه است.

چگونه لازم بود؟ بر اساس این واقعیت که msg_id یک مهر زمانی است که از نقطه نظر رمزنگاری برای جلوگیری از حملات تکراری ضروری است، پیوستن یک تابع شناسه منحصر به فرد به آن اشتباه است. بنابراین، بدون تغییر اساسی در معماری فعلی (زمانی که جریان به‌روزرسانی‌ها تولید می‌شود، این یک موضوع API سطح بالا برای بخش دیگری از این سری پست‌ها است)، باید:

  1. سروری که اتصال TCP را به کلاینت نگه می دارد مسئولیت را بر عهده می گیرد - اگر از سوکت خوانده شده است، لطفاً خطا را تأیید کنید، پردازش کنید یا برگردانید، بدون ضرر. سپس تأیید یک بردار شناسه نیست، بلکه به سادگی "آخرین seq_no دریافت شده" است - فقط یک عدد، مانند TCP (دو عدد - دنباله شما و عدد تایید شده). ما همیشه در جلسه هستیم، اینطور نیست؟
  2. مهر زمانی برای جلوگیری از حملات تکراری تبدیل به یک فیلد مجزا می شود. بررسی می شود، اما روی چیز دیگری تأثیر نمی گذارد. به اندازه کافی و uint32 - اگر نمک ما حداقل هر نیم روز تغییر کند، می توانیم 16 بیت را به بیت های مرتبه پایین یک قسمت صحیح از زمان فعلی اختصاص دهیم، بقیه را - به قسمت کسری از ثانیه (مثل الان).
  3. حذف شده است msg_id اصلاً - از نقطه نظر تمایز درخواست ها در باطن ها، اولاً شناسه مشتری وجود دارد و ثانیاً شناسه جلسه وجود دارد که آنها را به هم متصل کنید. بر این اساس، تنها یک چیز به عنوان شناسه درخواست کافی است seq_no.

این نیز موفق ترین گزینه نیست؛ یک تصادفی کامل می تواند به عنوان یک شناسه عمل کند - به هر حال، این کار قبلاً در API سطح بالا هنگام ارسال پیام انجام می شود. بهتر است معماری را به طور کامل از نسبی به مطلق بازسازی کنیم، اما این موضوع برای قسمت دیگری است نه این پست.

API؟

تادام! بنابراین، با تلاش در مسیری پر از درد و عصا، در نهایت توانستیم هر درخواستی را به سرور ارسال کنیم و هر گونه پاسخی را برای آنها دریافت کنیم و همچنین به روز رسانی ها را از سرور دریافت کنیم (نه در پاسخ به یک درخواست، بلکه خودش برای ما ارسال می کند، مانند PUSH، اگر کسی اینطور واضح تر باشد).

توجه، اکنون تنها نمونه در پرل در مقاله وجود خواهد داشت! (برای کسانی که با نحو آشنایی ندارند، اولین آرگومان bless ساختار داده شی است، دومی کلاس آن است):

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

بله، اسپویلر عمدی نیست - اگر هنوز آن را نخوانده‌اید، ادامه دهید و این کار را انجام دهید!

اوه، وای~~... این چه شکلی است؟ چیزی بسیار آشنا... شاید این ساختار داده یک Web API معمولی در JSON باشد، با این تفاوت که کلاس ها نیز به اشیا متصل هستند؟..

پس اینجوری شد... رفقا این همه چیه؟... اینهمه تلاش - و ما توقف کردیم تا جایی که برنامه نویسان وب استراحت کنیم فقط شروع کنآیا فقط JSON از طریق HTTPS ساده تر نیست؟! در مقابل چه چیزی گرفتیم؟ آیا تلاش ارزشش را داشت؟

بیایید ارزیابی کنیم که TL+MTProto چه چیزی به ما داده است و چه جایگزین هایی ممکن است. خوب، HTTP، که بر مدل درخواست-پاسخ تمرکز دارد، مناسب نیست، اما حداقل چیزی بالاتر از TLS است؟

سریال سازی فشرده با دیدن این ساختار داده، مشابه JSON، به یاد می آورم که نسخه های باینری آن وجود دارد. بیایید MsgPack را به‌عنوان غیرقابل توسعه علامت‌گذاری کنیم، اما برای مثال، CBOR وجود دارد - به هر حال، استانداردی که در RFC 7049. قابل توجه به این واقعیت است که آن را تعریف می کند برچسب ها، به عنوان مکانیزم گسترش، و در میان قبلا استاندارد شده است وجود دارد:

  • 25 + 256 - جایگزینی خطوط مکرر با ارجاع به شماره خط، چنین روش فشرده سازی ارزان
  • 26 - شی پرل سریال شده با نام کلاس و آرگومان های سازنده
  • 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

اضافه کردن نظر