คำติชมของโปรโตคอลและแนวทางองค์กรของ Telegram ส่วนที่ 1 ด้านเทคนิค: ประสบการณ์การเขียนลูกค้าตั้งแต่เริ่มต้น - TL, MT

เมื่อเร็ว ๆ นี้โพสต์เกี่ยวกับความเก่งกาจของ Telegram พี่น้อง Durov ที่เก่งและมีประสบการณ์ในการสร้างระบบเครือข่าย ฯลฯ เริ่มปรากฏให้เห็นบ่อยขึ้นในHabré ในเวลาเดียวกัน มีเพียงไม่กี่คนที่หมกมุ่นอยู่กับอุปกรณ์ทางเทคนิคจริงๆ โดยส่วนใหญ่แล้วพวกเขาใช้ Bot API ที่ค่อนข้างเรียบง่าย (และค่อนข้างแตกต่างจาก MTProto) ที่ใช้ JSON และมักจะยอมรับ เกี่ยวกับศรัทธา คำสรรเสริญและการประชาสัมพันธ์ทั้งหมดที่หมุนรอบผู้ส่งสาร เกือบหนึ่งปีครึ่งที่แล้ว เพื่อนร่วมงานของฉันที่ Eshelon NGO Vasily (น่าเสียดายที่บัญชีของเขาในHabréถูกลบไปพร้อมกับร่าง) เริ่มเขียนไคลเอนต์ Telegram ของเขาเองตั้งแต่เริ่มต้นในภาษา Perl และต่อมาผู้เขียนบรรทัดเหล่านี้ก็เข้าร่วม ทำไมต้อง Perl บางคนจะถามทันที? เพราะโครงการดังกล่าวมีในภาษาอื่นแล้ว อันที่จริง นี่ไม่ใช่ประเด็น อาจมีภาษาอื่น ๆ ที่ไม่มีอยู่จริงก็ได้ ห้องสมุดสำเร็จรูปและด้วยเหตุนี้ผู้เขียนจึงต้องไปตลอดทาง ตั้งแต่เริ่มต้น. ยิ่งกว่านั้นการเข้ารหัสเป็นเรื่องของความไว้วางใจ แต่ต้องตรวจสอบ ด้วยผลิตภัณฑ์ที่มุ่งเป้าไปที่ความปลอดภัย คุณไม่สามารถพึ่งพาไลบรารี่สำเร็จรูปจากผู้ผลิตและไว้วางใจไลบรารี่นั้นอย่างสุ่มสี่สุ่มห้าได้ (อย่างไรก็ตาม นี่เป็นหัวข้อสำหรับส่วนที่สอง) ในขณะนี้ ไลบรารีทำงานได้ค่อนข้างดีในระดับ "ปานกลาง" (ช่วยให้คุณสามารถส่งคำขอ API ใดก็ได้)

อย่างไรก็ตาม โพสต์ชุดนี้จะไม่มีการเข้ารหัสหรือคณิตศาสตร์มากนัก แต่จะมีรายละเอียดทางเทคนิคและไม้ค้ำยันทางสถาปัตยกรรมอื่น ๆ อีกมากมาย (ยังมีประโยชน์สำหรับผู้ที่จะไม่เขียนตั้งแต่ต้น แต่จะใช้ห้องสมุดในภาษาใดก็ได้) ดังนั้นเป้าหมายหลักคือการพยายามนำลูกค้าไปใช้ตั้งแต่เริ่มต้น ตามเอกสารอย่างเป็นทางการ. นั่นคือ สมมติว่าซอร์สโค้ดของลูกค้าอย่างเป็นทางการถูกปิด (อีกครั้งในส่วนที่สองเราจะกล่าวถึงรายละเอียดเพิ่มเติมเกี่ยวกับหัวข้อข้อเท็จจริงที่ว่าสิ่งนี้เป็นจริง ที่เกิดขึ้น ดังนั้น) แต่ในสมัยก่อนมีมาตรฐานเช่น RFC - เป็นไปได้ไหมที่จะเขียนไคลเอนต์ตามข้อกำหนดเพียงอย่างเดียว "โดยไม่ต้องดู" ที่ซอร์สโค้ดไม่ว่าจะเป็นทางการ (Telegram Desktop, มือถือ) หรือ Telethon อย่างไม่เป็นทางการ?

คำศัพท์:

เอกสาร...มีอยู่จริงใช่ไหม? จริงป้ะ?..

ชิ้นส่วนของบันทึกย่อสำหรับบทความนี้เริ่มรวบรวมเมื่อฤดูร้อนที่แล้ว ตลอดเวลานี้บนเว็บไซต์อย่างเป็นทางการ https://core.telegram.org เอกสารเป็นของเลเยอร์ 23 เช่น ติดอยู่ที่ไหนสักแห่งในปี 2014 (จำได้ว่าสมัยนั้นยังไม่มีช่องด้วยซ้ำ) แน่นอนว่าตามทฤษฎีแล้ว สิ่งนี้น่าจะช่วยให้เราสามารถใช้งานไคลเอนต์ที่มีฟังก์ชันการทำงานในขณะนั้นในปี 2014 แต่ถึงแม้จะอยู่ในสภาพนี้ เอกสารก็ยังประการแรกไม่สมบูรณ์ และประการที่สองอยู่ในสถานที่ที่ขัดแย้งกันในตัวเอง เมื่อเดือนที่แล้วในเดือนกันยายน 2019 มันเป็น โดยไม่ตั้งใจ พบว่ามีการอัปเดตเอกสารจำนวนมากบนเว็บไซต์สำหรับเลเยอร์ 105 ที่ค่อนข้างใหม่ โดยมีหมายเหตุว่าตอนนี้ทุกอย่างจำเป็นต้องอ่านอีกครั้ง จริงๆ แล้วมีการแก้ไขบทความหลายบทความ แต่หลายบทความยังคงไม่เปลี่ยนแปลง ดังนั้น เมื่ออ่านคำวิจารณ์ด้านล่างเกี่ยวกับเอกสารนี้ คุณควรจำไว้ว่าสิ่งเหล่านี้บางอย่างไม่เกี่ยวข้องอีกต่อไป แต่บางส่วนก็ยังค่อนข้างเกี่ยวข้อง ท้ายที่สุดแล้ว 5 ปีในโลกสมัยใหม่ไม่ได้เป็นเพียงเวลาที่ยาวนานเท่านั้น มาก มาก. ตั้งแต่เวลานั้น (โดยเฉพาะอย่างยิ่งถ้าคุณไม่คำนึงถึงไซต์ geochat ที่ถูกทิ้งและฟื้นคืนชีพตั้งแต่นั้นมา) จำนวนวิธี API ในโครงการได้เพิ่มขึ้นจากหนึ่งร้อยเป็นมากกว่าสองร้อยห้าสิบ!

จะเริ่มในฐานะนักเขียนรุ่นเยาว์ได้ที่ไหน?

ไม่สำคัญว่าคุณจะเขียนตั้งแต่เริ่มต้นหรือใช้งาน เช่น ไลบรารีสำเร็จรูป เป็นต้น Telethon สำหรับหลาม หรือ Madeline สำหรับ PHPไม่ว่าในกรณีใด คุณจะต้องมีก่อน ลงทะเบียนใบสมัครของคุณ - รับพารามิเตอร์ api_id и api_hash (ผู้ที่ทำงานกับ VKontakte API จะเข้าใจทันที) ซึ่งเซิร์ฟเวอร์จะระบุแอปพลิเคชัน นี้ ต้อง ทำด้วยเหตุผลทางกฎหมาย แต่เราจะพูดถึงสาเหตุที่ผู้เขียนห้องสมุดไม่สามารถเผยแพร่ได้ในส่วนที่สอง คุณอาจพอใจกับค่าทดสอบแม้ว่าจะมีจำกัดมาก แต่ความจริงก็คือ ตอนนี้คุณสามารถลงทะเบียนได้แล้ว เพียงหนึ่งเดียว แอพดังนั้นอย่ารีบเร่งเข้าไป

จากมุมมองทางเทคนิค เราควรสนใจความจริงที่ว่าหลังจากการลงทะเบียนแล้ว เราควรได้รับการแจ้งเตือนจาก Telegram เกี่ยวกับการอัปเดตเอกสาร โปรโตคอล ฯลฯ นั่นคือใคร ๆ ก็สามารถสันนิษฐานได้ว่าไซต์ที่มีท่าเทียบเรือนั้นถูกทิ้งร้างและยังคงทำงานต่อไปโดยเฉพาะกับผู้ที่เริ่มสร้างลูกค้าเพราะ มันง่ายกว่า แต่ไม่เลย ไม่มีอะไรแบบนั้นเกิดขึ้น ไม่มีข้อมูลมา

และถ้าคุณเขียนตั้งแต่ต้น การใช้พารามิเตอร์ที่ได้รับก็ยังห่างไกลออกไป แม้ว่า https://core.telegram.org/ และพูดถึงสิ่งเหล่านั้นใน Getting Started ก่อนอื่นเลย คุณจะต้องนำไปปฏิบัติก่อน โปรโตคอล MTProto - แต่ถ้าคุณเชื่อ เค้าโครงตามโมเดล OSI ในตอนท้ายของหน้าสำหรับคำอธิบายทั่วไปของโปรโตคอล ก็ไม่มีประโยชน์เลย

ในความเป็นจริง ทั้งก่อนและหลัง MTProto ในหลายระดับพร้อมกัน (ดังที่เครือข่ายต่างประเทศที่ทำงานในเคอร์เนลระบบปฏิบัติการพูดว่า การละเมิดเลเยอร์) หัวข้อใหญ่ เจ็บปวด และแย่มากจะเข้ามาขวางทาง...

การทำให้เป็นอนุกรมแบบไบนารี: TL (ภาษาประเภท) และรูปแบบของมัน และเลเยอร์ และคำที่น่ากลัวอื่นๆ อีกมากมาย

ที่จริงแล้วหัวข้อนี้เป็นกุญแจสำคัญในปัญหาของ Telegram และจะมีคำพูดแย่ ๆ มากมายหากคุณพยายามเจาะลึกลงไป

นี่คือแผนภาพ ถ้าคำนี้เข้ามาในใจคุณ ให้พูดว่า JSON สคีมาคุณคิดถูกแล้ว เป้าหมายก็เหมือนกัน: ใช้ภาษาบางอย่างเพื่ออธิบายชุดข้อมูลที่ส่งที่เป็นไปได้ นี่คือจุดที่ความคล้ายคลึงกันสิ้นสุดลง ถ้าจากเพจ โปรโตคอล MTProtoหรือจากแผนผังต้นทางของลูกค้าอย่างเป็นทางการ เราจะพยายามเปิดสคีมา เราจะเห็นบางอย่างดังนี้:

int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;

vector#1cb5c415 {t:Type} # [ t ] = Vector t;

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

rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;

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

---functions---

set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer;

ping#7abe77ec ping_id:long = Pong;
ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

invokeAfterMsg#cb9f372d msg_id:long query:!X = X;
invokeAfterMsgs#3dc4b4f0 msg_ids:Vector<long> query:!X = X;

account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;
account.sendChangePhoneCode#8e57deb flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode;

คนที่เห็นสิ่งนี้เป็นครั้งแรกจะสามารถรับรู้เพียงส่วนหนึ่งของสิ่งที่เขียนโดยสัญชาตญาณ - เห็นได้ชัดว่าสิ่งเหล่านี้เป็นโครงสร้าง (แม้ว่าชื่อจะอยู่ที่ไหนทางด้านซ้ายหรือทางขวาก็ตาม) มีฟิลด์อยู่ในนั้น หลังจากนั้นประเภทจะตามหลังเครื่องหมายทวิภาค... อาจจะ ในวงเล็บเหลี่ยมนี้ อาจมีเทมเพลตเหมือนใน C++ (อันที่จริง ไม่ได้จริงๆ). และสัญลักษณ์อื่นๆ ทั้งหมดหมายถึงอะไร เครื่องหมายคำถาม เครื่องหมายอัศเจรีย์ เปอร์เซ็นต์ เครื่องหมายแฮช (และเห็นได้ชัดว่าพวกมันหมายถึงสิ่งที่แตกต่างกันในที่ที่แตกต่างกัน) บางครั้งก็ปรากฏและบางครั้งก็ไม่มี เลขฐานสิบหก - และที่สำคัญที่สุดคือ ทำอย่างไรจึงจะได้จากสิ่งนี้ ขวา (ซึ่งจะไม่ถูกปฏิเสธโดยเซิร์ฟเวอร์) สตรีมไบต์? คุณจะต้องอ่านเอกสาร (ใช่ มีลิงก์ไปยังสคีมาในเวอร์ชัน JSON ใกล้เคียง - แต่นั่นไม่ได้ทำให้ชัดเจนขึ้น).

เปิดหน้า การทำให้เป็นอนุกรมข้อมูลไบนารี และดำดิ่งสู่โลกมหัศจรรย์ของเห็ดและคณิตศาสตร์แบบไม่ต่อเนื่องซึ่งคล้ายกับมาทันในปีที่ 4 ตัวอักษร, ประเภท, ค่า, ตัวผสม, ตัวผสมฟังก์ชัน, รูปแบบปกติ, ประเภทคอมโพสิต, ประเภทโพลีมอร์ฟิก... และนั่นเป็นเพียงหน้าแรกเท่านั้น! ต่อไปรอคุณอยู่ ภาษาทีแอลซึ่งแม้ว่าจะมีตัวอย่างคำขอและคำตอบเล็กน้อยอยู่แล้ว แต่ก็ไม่ได้ให้คำตอบเลยสำหรับกรณีทั่วไปซึ่งหมายความว่าคุณจะต้องลุยผ่านการเล่าเรื่องคณิตศาสตร์ที่แปลจากภาษารัสเซียเป็นภาษาอังกฤษในอีกแปดรายการที่ฝังอยู่ หน้า!

ผู้อ่านที่คุ้นเคยกับภาษาที่ใช้งานได้และการอนุมานประเภทอัตโนมัติจะเห็นภาษาคำอธิบายในภาษานี้แม้จะจากตัวอย่างที่คุ้นเคยมากกว่าก็ตามและสามารถพูดได้ว่านี่เป็นหลักการที่ไม่เลวเลย ข้อโต้แย้งคือ:

  • ใช่ เป้าหมาย ฟังดูดี แต่อนิจจาเธอ ไม่ประสบความสำเร็จ
  • การศึกษาในมหาวิทยาลัยในรัสเซียนั้นแตกต่างกันไปแม้แต่ในสาขาไอทีเฉพาะทาง - ไม่ใช่ทุกคนที่เรียนหลักสูตรที่เกี่ยวข้อง
  • สุดท้ายนี้ ดังที่เราจะได้เห็นในทางปฏิบัติ ไม่จำเป็นเนื่องจากมีการใช้เพียงเซตย่อยที่จำกัดแม้แต่ TL ที่อธิบายไว้เท่านั้นที่ถูกนำมาใช้

ดังที่กล่าว ลีโอเนิร์ด ในช่อง #perl ในเครือข่าย FreeNode IRC ซึ่งพยายามใช้เกตจาก Telegram ถึง Matrix (การแปลคำพูดไม่ถูกต้องจากหน่วยความจำ):

รู้สึกเหมือนมีคนรู้จักทฤษฎีการพิมพ์เป็นครั้งแรก รู้สึกตื่นเต้น และเริ่มลองเล่นกับมัน โดยไม่สนใจว่าจำเป็นในทางปฏิบัติหรือไม่

ดูด้วยตัวคุณเองว่าความต้องการแบบเปลือย (int, long ฯลฯ) เป็นสิ่งที่ระดับพื้นฐานไม่ก่อให้เกิดคำถามหรือไม่ - ท้ายที่สุดแล้วจะต้องดำเนินการด้วยตนเอง - ตัวอย่างเช่น ลองใช้ความพยายามเพื่อให้ได้มาจากสิ่งเหล่านี้ เวกเตอร์. นั่นคือในความเป็นจริงแล้ว อาร์เรย์ถ้าคุณเรียกสิ่งที่เป็นผลลัพธ์ด้วยชื่อที่ถูกต้อง

แต่ก่อน

คำอธิบายสั้นๆ เกี่ยวกับชุดย่อยของไวยากรณ์ TL สำหรับผู้ที่ไม่ได้อ่านเอกสารอย่างเป็นทางการ

constructor = Type;
myVec ids:Vector<long> = Type;

fixed#abcdef34 id:int = Type2;

fixedVec set:Vector<Type2> = FixedVec;

constructorOne#crc32 field1:int = PolymorType;
constructorTwo#2crc32 field_a:long field_b:Type3 field_c:int = PolymorType;
constructorThree#deadcrc bit_flags_of_what_really_present:# optional_field4:bit_flags_of_what_really_present.1?Type = PolymorType;

an_id#12abcd34 id:int = Type3;
a_null#6789cdef = Type3;

คำจำกัดความเริ่มต้นเสมอ นักออกแบบหลังจากนั้นก็เป็นทางเลือก (ในทางปฏิบัติ - เสมอ) ผ่านสัญลักษณ์ # ควร CRC32 จากสตริงคำอธิบายที่ทำให้เป็นมาตรฐานของประเภทนี้ ถัดมาคือคำอธิบายของฟิลด์ต่างๆ หากมีอยู่ ประเภทอาจว่างเปล่า ทั้งหมดนี้ลงท้ายด้วยเครื่องหมายเท่ากับ ชื่อของประเภทที่คอนสตรัคเตอร์นี้ ซึ่งในความเป็นจริงแล้วเป็นประเภทย่อย คนที่อยู่ทางขวาของเครื่องหมายเท่ากับคือ polymorphic - นั่นคือหลายประเภทที่เฉพาะเจาะจงสามารถสอดคล้องกับมันได้

หากคำจำกัดความเกิดขึ้นหลังบรรทัด ---functions---จากนั้นไวยากรณ์จะยังคงเหมือนเดิม แต่ความหมายจะแตกต่าง: ตัวสร้างจะกลายเป็นชื่อของฟังก์ชัน RPC ฟิลด์จะกลายเป็นพารามิเตอร์ (นั่นคือมันจะยังคงเหมือนเดิมทุกประการตามโครงสร้างที่กำหนดตามที่อธิบายไว้ด้านล่าง นี่จะเป็นความหมายที่กำหนด) และ "ประเภทโพลีมอร์ฟิก " - ประเภทของผลลัพธ์ที่ส่งคืน จริงอยู่ มันจะยังคงเป็น polymorphic - เพิ่งกำหนดไว้ในส่วนนี้ ---types---แต่ตัวสร้างนี้จะ "ไม่ได้รับการพิจารณา" การโอเวอร์โหลดประเภทของฟังก์ชันที่ถูกเรียกตามอาร์กิวเมนต์ เช่น ด้วยเหตุผลบางประการ TL จึงไม่ได้จัดเตรียมฟังก์ชันหลายอย่างที่มีชื่อเดียวกันแต่ลายเซ็นต่างกัน เช่นเดียวกับใน C++

ทำไมต้อง "ตัวสร้าง" และ "polymorphic" ถ้าไม่ใช่ OOP ในความเป็นจริง มันจะง่ายกว่าสำหรับใครบางคนที่จะคิดเกี่ยวกับสิ่งนี้ในแง่ของ OOP - ประเภท polymorphic เป็นคลาสนามธรรม และตัวสร้างเป็นคลาสทายาทโดยตรง และ final ในศัพท์เฉพาะของภาษาต่างๆ แน่นอนว่าที่นี่ที่เดียวเท่านั้น ความคล้ายคลึงกัน ด้วยวิธีคอนสตรัคเตอร์ที่โอเวอร์โหลดจริงในภาษาการเขียนโปรแกรม OO เนื่องจากที่นี่เป็นเพียงโครงสร้างข้อมูล จึงไม่มีวิธีการ (แม้ว่าคำอธิบายของฟังก์ชันและวิธีการเพิ่มเติมจะสามารถสร้างความสับสนในหัวที่มีอยู่ได้ แต่นั่นเป็นอีกเรื่องหนึ่ง) - คุณสามารถนึกถึง Constructor เป็นค่าจาก ที่ กำลังก่อสร้าง พิมพ์เมื่ออ่านสตรีมไบต์

สิ่งนี้เกิดขึ้นได้อย่างไร? โปรแกรมดีซีเรียลไลเซอร์จะอ่านค่าขนาด 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>.

คุณอาจคิดว่าทั้งเทมเพลตและข้อมูลทั่วไปอยู่ในข้อดีหรือ Java แต่ไม่มี. เกือบ. นี้ เท่านั้น กรณีการใช้วงเล็บเหลี่ยมในวงจรจริง และใช้กับ Vector เท่านั้น ในสตรีมไบต์ สิ่งเหล่านี้จะเป็น 4 CRC32 ไบต์สำหรับประเภท Vector เอง ซึ่งเท่ากันเสมอ จากนั้น 4 ไบต์ - จำนวนองค์ประกอบอาร์เรย์ และจากนั้นองค์ประกอบเหล่านี้เอง

เพิ่มความจริงที่ว่าการทำให้เป็นอนุกรมมักจะเกิดขึ้นในคำที่มีขนาด 4 ไบต์ทุกประเภทจะมีจำนวนทวีคูณ - มีการอธิบายประเภทในตัวด้วย bytes и string ด้วยการจัดลำดับความยาวด้วยตนเองและการจัดตำแหน่งนี้ด้วย 4 - ดูเหมือนว่าจะฟังดูปกติและค่อนข้างมีประสิทธิภาพด้วยซ้ำ? แม้ว่า TL จะถูกอ้างว่าเป็นอนุกรมไบนารี่ที่มีประสิทธิภาพ แต่ด้วยการขยายตัวของสิ่งใด ๆ แม้แต่ค่าบูลีนและสตริงอักขระเดี่ยวเป็น 4 ไบต์ JSON จะยังคงหนาขึ้นมากหรือไม่ ดูสิ แม้กระทั่งฟิลด์ที่ไม่จำเป็นก็สามารถข้ามได้ด้วยแฟล็กบิต ทุกอย่างค่อนข้างดีและยังสามารถขยายได้ในอนาคต ดังนั้นทำไมไม่เพิ่มฟิลด์ทางเลือกใหม่ให้กับ Constructor ในภายหลังล่ะ?..

แต่ไม่ ถ้าคุณไม่ได้อ่านคำอธิบายสั้น ๆ ของฉัน แต่เป็นเอกสารฉบับเต็ม และคิดถึงการนำไปปฏิบัติ ประการแรก CRC32 ของตัวสร้างจะถูกคำนวณตามบรรทัดปกติของคำอธิบายข้อความของโครงร่าง (ลบช่องว่างเพิ่มเติม ฯลฯ ) - ดังนั้นหากมีการเพิ่มฟิลด์ใหม่ บรรทัดคำอธิบายประเภทจะเปลี่ยนไป และด้วยเหตุนี้ CRC32 และ ดังนั้น การทำให้เป็นอนุกรม แล้วลูกค้าเก่าจะทำอย่างไรถ้าเขาได้รับฟิลด์ที่มีการตั้งค่าธงใหม่และเขาไม่รู้ว่าจะทำอย่างไรต่อไป?..

ประการที่สองมาจำไว้ CRC32ซึ่งใช้ที่นี่เป็นหลักว่า ฟังก์ชันแฮช เพื่อกำหนดประเภทที่ไม่ซ้ำใครที่กำลัง (ยกเลิก) อนุกรม ที่นี่เรากำลังเผชิญกับปัญหาการชนกัน - และไม่ ความน่าจะเป็นไม่ใช่หนึ่งใน 232 แต่มากกว่านั้นมาก ใครจำได้ว่า CRC32 ได้รับการออกแบบมาเพื่อตรวจจับ (และแก้ไข) ข้อผิดพลาดในช่องทางการสื่อสารและปรับปรุงคุณสมบัติเหล่านี้ให้เกิดความเสียหายต่อผู้อื่น ตัวอย่างเช่น ไม่สนใจเรื่องการจัดเรียงไบต์ใหม่ หากคุณคำนวณ CRC32 จากสองบรรทัด ในวินาทีที่คุณสลับ 4 ไบต์แรกกับ 4 ไบต์ถัดไป มันจะเท่ากัน เมื่ออินพุตของเราเป็นสตริงข้อความจากตัวอักษรละติน (และเครื่องหมายวรรคตอนเล็กน้อย) และชื่อเหล่านี้ไม่ได้สุ่มเป็นพิเศษ ความน่าจะเป็นของการจัดเรียงใหม่จะเพิ่มขึ้นอย่างมาก

ว่าแต่ใครตรวจดูว่ามีอะไรอยู่บ้าง? จริงๆ ซีอาร์ซี32? หนึ่งในซอร์สโค้ดยุคแรกๆ (ก่อน Waltman ด้วยซ้ำ) มีฟังก์ชันแฮชที่จะคูณอักขระแต่ละตัวด้วยหมายเลข 239 ซึ่งเป็นที่ชื่นชอบของคนเหล่านี้ ฮ่าฮ่า!

ในที่สุด โอเค เราก็ได้รู้ว่าตัวสร้างที่มีประเภทฟิลด์ Vector<int> и Vector<PolymorType> จะมี CRC32 ที่แตกต่างกัน แล้วประสิทธิภาพออนไลน์ล่ะ? และจากมุมมองทางทฤษฎี สิ่งนี้จะกลายเป็นส่วนหนึ่งของประเภทนี้หรือไม่? สมมติว่าเราผ่านอาร์เรย์จำนวนหนึ่งหมื่นเช่นกัน Vector<int> ทุกอย่างชัดเจน ความยาว และอีก 40000 ไบต์ ถ้าเป็นอย่างนี้ล่ะ Vector<Type2>ซึ่งประกอบด้วยฟิลด์เดียวเท่านั้น int และเป็นประเภทเดียว - เราจำเป็นต้องทำซ้ำ 10000xabcdef0 34 ครั้งแล้ว 4 ไบต์หรือไม่ intหรือภาษาสามารถแยกมันให้เราได้จากตัวสร้าง fixedVec และแทนที่จะเป็น 80000 ไบต์ โอนอีกครั้งเพียง 40000?

นี่ไม่ใช่คำถามเชิงทฤษฎีที่ไม่ได้ใช้งานเลย ลองจินตนาการว่าคุณได้รับรายชื่อผู้ใช้กลุ่ม ซึ่งแต่ละคนมีรหัส ชื่อ นามสกุล - ปริมาณข้อมูลที่ถ่ายโอนผ่านการเชื่อมต่อมือถืออาจมีนัยสำคัญ มันเป็นประสิทธิภาพของการทำให้เป็นอนุกรมของ Telegram ที่โฆษณากับเราอย่างชัดเจน

ดังนั้น…

เวกเตอร์ซึ่งไม่เคยถูกปล่อยออกมา

หากคุณพยายามที่จะดูหน้าคำอธิบายของตัวรวมและอื่น ๆ คุณจะเห็นว่าเวกเตอร์ (และแม้แต่เมทริกซ์) กำลังพยายามส่งออกอย่างเป็นทางการผ่านสิ่งอันดับของหลายแผ่นงาน แต่ในท้ายที่สุดพวกเขาก็ลืมไป ขั้นตอนสุดท้ายจะถูกข้ามไป และให้คำจำกัดความของเวกเตอร์ซึ่งยังไม่เชื่อมโยงกับประเภท เกิดอะไรขึ้น? ในภาษาต่างๆ การเขียนโปรแกรมโดยเฉพาะอย่างยิ่งฟังก์ชันการทำงาน เป็นเรื่องปกติที่จะอธิบายโครงสร้างแบบวนซ้ำ - คอมไพเลอร์ที่มีการประเมินแบบสันหลังยาวจะเข้าใจและทำทุกอย่างด้วยตัวมันเอง ในภาษา การจัดลำดับข้อมูล สิ่งที่จำเป็นคือประสิทธิภาพ: แค่อธิบายก็เพียงพอแล้ว รายการ, เช่น. โครงสร้างของสององค์ประกอบ - อันแรกคือองค์ประกอบข้อมูล ส่วนที่สองคือโครงสร้างเดียวกันเองหรือพื้นที่ว่างสำหรับส่วนท้าย (แพ็ค (cons) ในลิสป์) แต่สิ่งนี้จะต้องชัดเจน แต่ละ องค์ประกอบจะใช้เวลาเพิ่มอีก 4 ไบต์ (CRC32 ในกรณีของ TL) เพื่ออธิบายประเภทของมัน อาร์เรย์สามารถอธิบายได้ง่ายเช่นกัน ขนาดคงที่แต่ในกรณีของอาร์เรย์ที่ไม่ทราบความยาวล่วงหน้า เราจะแยกออก

ดังนั้น เนื่องจาก TL ไม่อนุญาตให้เอาท์พุตเวกเตอร์ จึงจำเป็นต้องเพิ่มมันที่ด้านข้าง ในที่สุดเอกสารก็บอกว่า:

การทำให้เป็นอนุกรมจะใช้ตัวสร้าง "เวกเตอร์" ตัวเดียวกันเสมอ (const 0x1cb5c415 = crc32("vector t:Type # [ t ] = Vector t") ซึ่งไม่ได้ขึ้นอยู่กับค่าเฉพาะของตัวแปรประเภท t

ค่าของพารามิเตอร์เผื่อเลือก t จะไม่เกี่ยวข้องกับการทำให้เป็นอนุกรม เนื่องจากค่าดังกล่าวได้มาจากประเภทผลลัพธ์ (ทราบเสมอก่อนการดีซีเรียลไลซ์)

ลองดูให้ละเอียดยิ่งขึ้น: vector {t:Type} # [ t ] = Vector t - แต่ ไม่มีที่ไหนเลย คำจำกัดความนี้ไม่ได้บอกว่าตัวเลขแรกต้องเท่ากับความยาวของเวกเตอร์! และมันไม่ได้มาจากที่ไหนเลย นี่คือสิ่งที่ต้องคำนึงถึงและนำไปใช้ด้วยมือของคุณ ที่อื่น เอกสารยังระบุโดยสุจริตว่าประเภทนั้นไม่ใช่ของจริง:

นามแฝง Vector t polymorphic คือ "ประเภท" ซึ่งค่าคือลำดับของค่าของประเภท t ใด ๆ ไม่ว่าจะชนิดบรรจุกล่องหรือเปลือย

...แต่ไม่ได้โฟกัสไปที่มัน เมื่อคุณเบื่อหน่ายกับการเรียนวิชาคณิตศาสตร์ที่ยืดเยื้อ (อาจรู้จักจากหลักสูตรมหาวิทยาลัยด้วยซ้ำ) ตัดสินใจยอมแพ้และลองคิดดูว่าจะใช้งานคณิตศาสตร์อย่างไรในทางปฏิบัติ ความรู้สึกที่ค้างอยู่ในหัวของคุณก็คือว่านี่เป็นเรื่องร้ายแรง คณิตศาสตร์ที่เป็นแก่นแท้ มันถูกประดิษฐ์ขึ้นอย่างชัดเจนโดย Cool People (นักคณิตศาสตร์สองคน - ผู้ชนะ ACM) และไม่ใช่แค่ใครก็ตาม เป้าหมาย - เพื่ออวด - สำเร็จแล้ว

โดยวิธีการเกี่ยวกับจำนวน ให้เราเตือนคุณว่า # มันเป็นคำพ้องความหมาย nat, จำนวนธรรมชาติ:

มีนิพจน์ประเภท (ประเภท-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 ก่อนเหตุการณ์สำคัญด้วยการขายหุ้นของ Durov และ (อย่างแน่นอน) ก่อนที่การพัฒนา Telegram จะเริ่มต้นขึ้นด้วยซ้ำ และในโอเพ่นซอร์ส ซอร์สโค้ดของการใช้งานครั้งแรก คุณจะพบไม้ค้ำยันตลกๆ มากมาย และภาษาเองก็ถูกนำไปใช้ที่นั่นอย่างสมบูรณ์มากกว่าใน Telegram ในปัจจุบัน ตัวอย่างเช่น ไม่ได้ใช้แฮชเลยในโครงการ (หมายถึงประเภทเทียมในตัว (เช่น เวกเตอร์) ที่มีพฤติกรรมเบี่ยงเบน) หรือ

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

แต่ให้เราพิจารณาติดตามวิวัฒนาการของยักษ์แห่งความคิดเพื่อความสมบูรณ์

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

หรืออันที่สวยงามนี้:

    static const char *reserved_words_polymorhic[] = {

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

      };

ส่วนนี้เกี่ยวข้องกับเทมเพลตเช่น:

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

นี่คือคำจำกัดความของประเภทเทมเพลตแฮชแมปที่เป็นเวกเตอร์ของ int - คู่ประเภท ใน C++ จะมีลักษณะดังนี้:

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

ดังนั้น, alpha - คำสำคัญ! แต่เฉพาะใน C ++ เท่านั้นที่คุณสามารถเขียน T ได้ แต่คุณควรเขียนอัลฟ่า เบต้า... แต่ไม่เกิน 8 พารามิเตอร์ นั่นคือจุดสิ้นสุดของจินตนาการ ดูเหมือนว่ากาลครั้งหนึ่งในเซนต์ปีเตอร์สเบิร์กมีบทสนทนาเช่นนี้เกิดขึ้น:

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

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

แต่นี่เป็นเรื่องเกี่ยวกับการใช้งาน TL "โดยทั่วไป" ที่เผยแพร่ครั้งแรก มาดูการใช้งานในไคลเอนต์ Telegram กันดีกว่า

คำพูดถึง Vasily:

Vasily, [09.10.18 17:07] เหนือสิ่งอื่นใด ก้นนั้นร้อนแรงเพราะพวกเขาสร้างนามธรรมมากมาย จากนั้นจึงตอกสลักลงบนพวกมัน และปิดเครื่องสร้างโค้ดด้วยไม้ค้ำ
ผลลัพธ์แรกจาก dock Pilot.jpg
จากนั้นจากโค้ด dzhekichan.webp

แน่นอนว่าจากคนที่คุ้นเคยกับอัลกอริธึมและคณิตศาสตร์ เราสามารถคาดหวังได้ว่าพวกเขาได้อ่าน Aho, Ullmann และคุ้นเคยกับเครื่องมือที่กลายเป็นมาตรฐานโดยพฤตินัยในอุตสาหกรรมมานานหลายทศวรรษในการเขียนคอมไพเลอร์ DSL ใช่ไหม..

โดยผู้เขียน โทรเลข-cli คือ Vitaly Valtman ดังที่สามารถเข้าใจได้จากการเกิดรูปแบบ TLO นอกขอบเขต (cli) ซึ่งเป็นสมาชิกของทีม - ขณะนี้มีการจัดสรรไลบรารีสำหรับการแยกวิเคราะห์ TL แล้ว แยกต่างหากความประทับใจของเธอคืออะไร ตัวแยกวิเคราะห์ TL? ..

16.12 04:18 Vasily: ฉันคิดว่ามีคนไม่เข้าใจ lex+yacc
16.12 04:18 วาซิลี: ฉันอธิบายเป็นอย่างอื่นไม่ได้
16.12 04:18 Vasily: หรือพวกเขาได้รับค่าตอบแทนตามจำนวนบรรทัดใน VK
16.12 04:19 วาซิลี่: 3k+ ไลน์ เป็นต้น<censored> แทนที่จะเป็นตัวแยกวิเคราะห์

อาจเป็นข้อยกเว้น? มาดูวิธีการกัน ทำ นี่คือไคลเอนต์อย่างเป็นทางการ - Telegram Desktop:

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

มากกว่า 1100 บรรทัดใน Python นิพจน์ทั่วไปสองสามรายการ + กรณีพิเศษเช่นเวกเตอร์ ซึ่งแน่นอนว่าได้รับการประกาศในรูปแบบตามที่ควรจะเป็นไปตามไวยากรณ์ TL แต่พวกเขาอาศัยไวยากรณ์นี้เพื่อแยกวิเคราะห์... เกิดคำถามว่าเหตุใดจึงเกิดปาฏิหาริย์?иมันจะซับซ้อนกว่านี้ถ้าไม่มีใครแยกวิเคราะห์ตามเอกสารล่ะ?!

ยังไงก็ตาม... จำที่เราคุยกันเรื่องการตรวจสอบ CRC32 ได้ไหม? ดังนั้นในตัวสร้างโค้ด Telegram Desktop จึงมีรายการข้อยกเว้นสำหรับประเภทที่คำนวณ CRC32 ไม่ตรงกัน กับอันที่ระบุในแผนภาพ!

Vasily, [18.12/22 49:XNUMX] และที่นี่ ฉันจะคิดว่าจำเป็นต้องมี TL ดังกล่าวหรือไม่
ถ้าฉันต้องการยุ่งกับการใช้งานทางเลือกอื่น ฉันจะเริ่มแทรกการขึ้นบรรทัดใหม่ ตัวแยกวิเคราะห์ครึ่งหนึ่งจะพังตามคำจำกัดความแบบหลายบรรทัด
tdesktop ด้วยเช่นกัน

จำประเด็นเกี่ยวกับซับเดียวเราจะกลับมาพูดถึงในภายหลัง

โอเค telegram-cli ไม่เป็นทางการ Telegram Desktop เป็นทางการ แต่อย่างอื่นล่ะ? ใครจะรู้?.. ในโค้ดไคลเอนต์ Android ไม่มีตัวแยกวิเคราะห์สคีมาเลย (ซึ่งทำให้เกิดคำถามเกี่ยวกับโอเพ่นซอร์ส แต่นี่เป็นเพียงส่วนที่สอง) แต่มีโค้ดตลก ๆ อีกหลายชิ้น แต่มีมากกว่านั้นใน ส่วนย่อยด้านล่าง

ในทางปฏิบัติแล้วการทำให้เป็นอนุกรมทำให้เกิดคำถามอะไรอีกบ้าง ตัวอย่างเช่น พวกเขาทำสิ่งต่างๆ มากมายกับฟิลด์บิตและฟิลด์แบบมีเงื่อนไข:

วาซิลี: flags.0? true
หมายความว่ามีฟิลด์นี้อยู่และมีค่าเท่ากับจริงหากตั้งค่าสถานะไว้

วาซิลี: flags.1? int
หมายความว่ามีฟิลด์นี้อยู่และจำเป็นต้องดีซีเรียลไลซ์

Vasily: Ass อย่ากังวลกับสิ่งที่คุณกำลังทำอยู่!
Vasily: มีการกล่าวถึงที่ไหนสักแห่งในเอกสารว่า true เป็นแบบเปลือยที่มีความยาวเป็นศูนย์ แต่ไม่สามารถรวบรวมสิ่งใดจากเอกสารของพวกเขาได้
Vasily: ในการใช้งานโอเพ่นซอร์ส ก็ไม่เป็นเช่นนั้นเช่นกัน แต่มีไม้ค้ำยันและการสนับสนุนมากมาย

แล้วเทเลธอนล่ะ? มองไปข้างหน้าในหัวข้อของ MTProto ตัวอย่าง - มีชิ้นส่วนดังกล่าวในเอกสารประกอบ แต่เป็นสัญญาณ % มันถูกอธิบายว่า "สอดคล้องกับประเภทเปลือยที่กำหนด" เท่านั้น กล่าวคือ ในตัวอย่างด้านล่างมีข้อผิดพลาดหรือมีบางอย่างที่ไม่มีเอกสาร:

วาซิลี [22.06.18 18:38] ในที่เดียว:

msg_container#73f1f8dc messages:vector message = MessageContainer;

ในรูปแบบอื่น:

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

และนี่คือความแตกต่างใหญ่สองประการ ในชีวิตจริง มีเวกเตอร์เปล่าๆ เกิดขึ้น

ฉันไม่เคยเห็นคำจำกัดความของเวกเตอร์เปล่าๆ และไม่เคยเจอมาก่อน

การวิเคราะห์เขียนด้วยมือในภาษาเทเลทอน

ในแผนภาพของเขา คำจำกัดความจะถูกใส่ความคิดเห็นไว้ msg_container

อีกครั้ง คำถามยังคงอยู่ประมาณ % มันไม่ได้อธิบาย

Vadim Goncharov, [22.06.18 19:22] และใน tdesktop?

Vasily, [22.06.18 19:23] แต่ตัวแยกวิเคราะห์ TL ของพวกเขาในเครื่องยนต์ปกติมักจะไม่กินสิ่งนี้เช่นกัน

// parsed manually

TL เป็นสิ่งที่เป็นนามธรรมที่สวยงาม ไม่มีใครนำไปใช้ได้อย่างสมบูรณ์

และ % ไม่ได้อยู่ในเวอร์ชันของโครงร่าง

แต่ที่นี่เอกสารขัดแย้งในตัวเอง ไม่ทราบเลย

พบได้ในไวยากรณ์พวกเขาสามารถลืมอธิบายความหมายได้

คุณเห็นเอกสารใน TL คุณไม่สามารถเข้าใจได้หากไม่มีครึ่งลิตร

“เอาล่ะ สมมติว่า” ผู้อ่านอีกคนจะพูดว่า “คุณวิพากษ์วิจารณ์บางสิ่งบางอย่าง ดังนั้นแสดงให้ฉันเห็นว่าควรทำอย่างไร”

Vasily ตอบกลับ:“ ในส่วนของ parser ฉันชอบสิ่งที่ชอบ

    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รหัสที่สร้างขึ้น) รวมถึงขนมปังข้อมูลประเภทสำหรับการวิปัสสนาในแต่ละชั้นเรียน ประเภทโพลีมอร์ฟิกแต่ละประเภทจะกลายเป็นคลาสฐานนามธรรมที่ว่างเปล่า และตัวสร้างจะสืบทอดจากคลาสนั้นและมีวิธีการสำหรับซีเรียลไลซ์เซชันและดีซีเรียลไลซ์เซชัน

ขาดประเภทในภาษาประเภท

การพิมพ์ที่แข็งแกร่งเป็นสิ่งที่ดีใช่ไหม? ไม่ นี่ไม่ใช่โฮลิวาร์ (แม้ว่าฉันจะชอบภาษาไดนามิกมากกว่า) แต่เป็นสมมุติฐานภายในกรอบของ TL จากข้อมูลดังกล่าว ภาษาควรจัดให้มีการตรวจสอบทุกประเภทสำหรับเรา โอเค อาจจะไม่ใช่ตัวเขาเอง แต่เป็นการดำเนินการ แต่อย่างน้อยเขาก็ควรอธิบายสิ่งเหล่านั้น และเราต้องการโอกาสแบบไหน?

ก่อนอื่นเลย ข้อจำกัด ต่อไปนี้เราเห็นในเอกสารประกอบสำหรับการอัปโหลดไฟล์:

เนื้อหาไบนารีของไฟล์จะถูกแบ่งออกเป็นส่วนๆ ทุกชิ้นส่วนจะต้องมีขนาดเท่ากัน ( ส่วน_ขนาด ) และต้องเป็นไปตามเงื่อนไขต่อไปนี้:

  • part_size % 1024 = 0 (หารด้วย 1KB)
  • 524288 % part_size = 0 (512KB ต้องหารด้วย part_size เท่าๆ กัน)

ส่วนสุดท้ายไม่จำเป็นต้องตรงตามเงื่อนไขเหล่านี้ โดยมีเงื่อนไขว่าขนาดจะน้อยกว่า part_size

แต่ละส่วนควรมีหมายเลขลำดับ file_partโดยมีค่าตั้งแต่ 0 ถึง 2,999

หลังจากแบ่งพาร์ติชันไฟล์แล้ว คุณต้องเลือกวิธีการบันทึกไฟล์บนเซิร์ฟเวอร์ ใช้ upload.saveBigFilePart ในกรณีที่ไฟล์มีขนาดเต็มเกิน 10 MB และ upload.saveFilePart สำหรับไฟล์ขนาดเล็ก
[…] ข้อผิดพลาดในการป้อนข้อมูลข้อใดข้อหนึ่งต่อไปนี้อาจถูกส่งคืน:

  • FILE_PARTS_INVALID — จำนวนชิ้นส่วนไม่ถูกต้อง ค่าไม่ได้อยู่ระหว่าง 1..3000

มีสิ่งนี้อยู่ในแผนภาพหรือไม่? สิ่งนี้สามารถแสดงออกได้โดยใช้ TL หรือไม่? เลขที่ แต่ขอโทษด้วย แม้แต่ Turbo Pascal ของคุณปู่ก็สามารถอธิบายประเภทที่ระบุได้ ช่วง. และเขาก็รู้อีกสิ่งหนึ่งที่ตอนนี้รู้จักกันดีในชื่อ enum - ประเภทที่ประกอบด้วยการแจงนับของค่าคงที่ (น้อย) ในภาษาเช่น C - ตัวเลข โปรดทราบว่าจนถึงตอนนี้เราได้พูดถึงเฉพาะประเภทเท่านั้น ตัวเลข. แต่ก็มีอาร์เรย์ สตริงด้วย... เช่น เป็นการดีที่จะอธิบายว่าสตริงนี้มีได้เฉพาะหมายเลขโทรศัพท์ใช่ไหม

สิ่งนี้ไม่มีอยู่ใน TL แต่มีตัวอย่างเช่นใน JSON Schema และหากคนอื่นอาจโต้แย้งเกี่ยวกับการแบ่งขนาด 512 KB ว่ายังต้องมีการตรวจสอบในโค้ด จากนั้นตรวจสอบให้แน่ใจว่าลูกค้าเพียง ไม่สามารถ ส่งเลขออกนอกเขต 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. หรือในทางกลับกัน แต่มันก็ได้ผล! นั่นคือเซิร์ฟเวอร์ไม่สนใจประเภทดังกล่าว เป็นไปได้ยังไง? คำตอบอาจได้รับจากส่วนโค้ดจาก telegram-cli:

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

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

กล่าวอีกนัยหนึ่ง นี่คือจุดที่การทำซีเรียลไลซ์เสร็จสิ้น ด้วยตนเองไม่ได้สร้างโค้ด! บางทีเซิร์ฟเวอร์อาจถูกใช้งานในลักษณะเดียวกัน?.. โดยหลักการแล้ว สิ่งนี้จะได้ผลหากทำเพียงครั้งเดียว แต่จะรองรับในภายหลังในระหว่างการอัพเดตได้อย่างไร? นี่คือสาเหตุว่าทำไมโครงการนี้จึงถูกคิดค้นขึ้นมา? และที่นี่เราจะไปยังคำถามถัดไป

การกำหนดเวอร์ชัน เลเยอร์

เหตุใดเวอร์ชันแผนผังจึงถูกเรียกว่าเลเยอร์จึงสามารถคาดเดาได้โดยอิงตามประวัติของแผนผังที่เผยแพร่เท่านั้น เห็นได้ชัดว่าในตอนแรกผู้เขียนคิดว่าสิ่งพื้นฐานสามารถทำได้โดยใช้รูปแบบที่ไม่เปลี่ยนแปลง และเฉพาะในกรณีที่จำเป็นเท่านั้น สำหรับการร้องขอเฉพาะเท่านั้น ระบุว่าพวกเขากำลังดำเนินการโดยใช้เวอร์ชันอื่น โดยหลักการแล้ว แม้แต่ความคิดที่ดี - และสิ่งใหม่ก็จะ "ผสม" ซ้อนกันอยู่ด้านบนของสิ่งเก่า แต่มาดูกันว่ามันทำอย่างไร จริงอยู่ที่ฉันไม่สามารถดูได้ตั้งแต่แรกเริ่ม - มันตลก แต่ไม่มีไดอะแกรมของเลเยอร์ฐานอยู่เลย เลเยอร์เริ่มต้นด้วย 2 เอกสารประกอบบอกเราเกี่ยวกับคุณสมบัติ TL พิเศษ:

หากไคลเอนต์รองรับเลเยอร์ 2 จะต้องใช้ตัวสร้างต่อไปนี้:

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

ในทางปฏิบัติ นี่หมายความว่าก่อนการเรียก API ทุกครั้ง จะมีค่า int 0x289dd1f6 ต้องเพิ่มก่อนหมายเลขวิธี

ฟังดูปกติ แต่เกิดอะไรขึ้นต่อไป? จากนั้นก็ปรากฏตัวขึ้น

invokeWithLayer3#b7475268 query:!X = X;

แล้วจะเป็นอย่างไรต่อไป? อย่างที่คุณอาจเดาได้ว่า

invokeWithLayer4#dea0d430 query:!X = X;

ตลก? ไม่ ยังเร็วเกินไปที่จะหัวเราะ ลองคิดถึงข้อเท็จจริงนั้นดู แต่ละ คำขอจากเลเยอร์อื่นจะต้องห่อด้วยประเภทพิเศษ - หากคุณมีพวกมันทั้งหมดที่แตกต่างกัน คุณจะแยกแยะได้อย่างไร? และการเพิ่มเพียง 4 ไบต์ข้างหน้าก็เป็นวิธีที่ค่อนข้างมีประสิทธิภาพ ดังนั้น,

invokeWithLayer5#417a57ae query:!X = X;

แต่เห็นได้ชัดว่าหลังจากนั้นไม่นานสิ่งนี้ก็จะกลายเป็นแบคคานาเลีย และวิธีแก้ปัญหาก็เกิดขึ้น:

อัปเดต: เริ่มต้นด้วยเลเยอร์ 9 วิธีการช่วยเหลือ invokeWithLayerN สามารถใช้ร่วมกับ initConnection

ไชโย! หลังจากเวอร์ชัน 9 ในที่สุดเราก็มาถึงสิ่งที่ทำในอินเทอร์เน็ตโปรโตคอลย้อนกลับไปในยุค 80 - ยอมรับเวอร์ชันหนึ่งครั้งเมื่อเริ่มต้นการเชื่อมต่อ!

แล้วจะเป็นอย่างไรต่อไป..

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

แต่ตอนนี้คุณยังสามารถหัวเราะได้ หลังจากผ่านไปอีก 9 เลเยอร์ ในที่สุดตัวสร้างสากลที่มีหมายเลขเวอร์ชันก็ถูกเพิ่มเข้ามา ซึ่งจะต้องถูกเรียกเพียงครั้งเดียวที่จุดเริ่มต้นของการเชื่อมต่อ และความหมายของเลเยอร์ดูเหมือนจะหายไป ตอนนี้มันเป็นเพียงเวอร์ชันที่มีเงื่อนไขเช่น ทุกที่อื่น แก้ไขปัญหา.

อย่างแน่นอน?..

Vasily, [16.07.18 14:01] แม้กระทั่งวันศุกร์ฉันก็คิดว่า:
เซิร์ฟเวอร์โทรคมนาคมส่งกิจกรรมโดยไม่มีการร้องขอ คำขอจะต้องรวมอยู่ใน InvokeWithLayer เซิร์ฟเวอร์ไม่ตัดการอัปเดต ไม่มีโครงสร้างสำหรับห่อคำตอบและอัปเดต

เหล่านั้น. ลูกค้าไม่สามารถระบุเลเยอร์ที่เขาต้องการอัพเดตได้

Vadim Goncharov, [16.07.18 14:02] InvolvedWithLayer ไม่ใช่ไม้ค้ำยันในหลักการใช่ไหม

วาซิลี [16.07.18 14:02] นี่เป็นหนทางเดียวเท่านั้น

Vadim Goncharov, [16.07.18 14:02] ซึ่งโดยพื้นฐานแล้วควรหมายถึงการตกลงกันในเลเยอร์ตอนเริ่มเซสชัน

อย่างไรก็ตาม เป็นไปตามที่ไม่มีการดาวน์เกรดไคลเอ็นต์

การอัปเดตเช่น พิมพ์ Updates ในรูปแบบ นี่คือสิ่งที่เซิร์ฟเวอร์ส่งไปยังไคลเอนต์ซึ่งไม่ตอบสนองต่อคำขอ API แต่เป็นอิสระเมื่อมีเหตุการณ์เกิดขึ้น นี่เป็นหัวข้อที่ซับซ้อนที่จะกล่าวถึงในโพสต์อื่น แต่สำหรับตอนนี้ สิ่งสำคัญคือต้องรู้ว่าเซิร์ฟเวอร์บันทึกการอัปเดตแม้ว่าไคลเอนต์จะออฟไลน์ก็ตาม

ดังนั้นหากคุณปฏิเสธที่จะห่อ แต่ละ เพื่อระบุเวอร์ชัน ซึ่งนำไปสู่ปัญหาที่เป็นไปได้ดังต่อไปนี้:

  • เซิร์ฟเวอร์จะส่งการอัพเดตไปยังไคลเอนต์ก่อนที่ไคลเอนต์จะแจ้งว่ารองรับเวอร์ชันใด
  • ฉันควรทำอย่างไรหลังจากอัพเกรดไคลเอนต์?
  • ใคร ค้ำประกันความคิดเห็นของเซิร์ฟเวอร์เกี่ยวกับหมายเลขเลเยอร์จะไม่เปลี่ยนแปลงในระหว่างกระบวนการหรือไม่

คุณคิดว่านี่เป็นการคาดเดาทางทฤษฎีล้วนๆ และในทางปฏิบัติสิ่งนี้ไม่สามารถเกิดขึ้นได้ เนื่องจากเซิร์ฟเวอร์เขียนอย่างถูกต้อง (อย่างน้อยก็ได้รับการทดสอบอย่างดี) ฮ่า! ไม่ว่ายังไงก็ตาม!

นี่คือสิ่งที่เราพบเจอในเดือนสิงหาคม เมื่อวันที่ 14 สิงหาคม มีข้อความแจ้งว่ามีบางอย่างกำลังอัปเดตบนเซิร์ฟเวอร์ Telegram... จากนั้นในบันทึก:

2019-08-15 09:28:35.880640 MSK warn  main: ANON:87: unknown object type: 0x80d182d1 at TL/Object.pm line 213.
2019-08-15 09:28:35.751899 MSK warn  main: ANON:87: unknown object type: 0xb5223b0f at TL/Object.pm line 213.

จากนั้นติดตามสแต็กหลายเมกะไบต์ (ในเวลาเดียวกันการบันทึกก็ได้รับการแก้ไข) ท้ายที่สุดแล้ว หากบางสิ่งไม่ได้รับการยอมรับใน TL ของคุณ TL ของคุณก็จะเป็นแบบไบนารี่ตามลายเซ็น ถัดลงมาในบรรทัดถัดไป ทั้งหมด ไป การถอดรหัสจะเป็นไปไม่ได้ คุณควรทำอย่างไรในสถานการณ์เช่นนี้?

สิ่งแรกที่ทุกคนนึกถึงคือตัดการเชื่อมต่อแล้วลองอีกครั้ง ไม่ได้ช่วยอะไร เราใช้ Google CRC32 - สิ่งเหล่านี้กลายเป็นวัตถุจากโครงร่าง 73 แม้ว่าเราจะทำงานกับ 82 ก็ตาม เราจะดูบันทึกอย่างละเอียด - มีตัวระบุจากสองโครงร่างที่แตกต่างกัน!

บางทีปัญหาอาจอยู่ที่ไคลเอนต์ที่ไม่เป็นทางการของเราล้วนๆ? ไม่ เราเปิดตัว Telegram Desktop 1.2.17 (เวอร์ชันที่มาพร้อมกับ Linux หลายรุ่น) โดยเขียนลงในบันทึกข้อยกเว้น: MTP Unexpected type id #b5223b0f read in MTPMessageMedia...

คำติชมของโปรโตคอลและแนวทางองค์กรของ Telegram ส่วนที่ 1 ด้านเทคนิค: ประสบการณ์การเขียนลูกค้าตั้งแต่เริ่มต้น - TL, MT

Google แสดงให้เห็นว่าปัญหาที่คล้ายกันได้เกิดขึ้นกับไคลเอนต์ที่ไม่เป็นทางการรายหนึ่งแล้ว แต่หมายเลขเวอร์ชันและด้วยเหตุนี้ สมมติฐานจึงแตกต่าง...

แล้วเราควรทำอย่างไร? ฉันกับ Vasily แยกกัน: เขาพยายามอัปเดตวงจรเป็น 91 ฉันตัดสินใจรอสองสามวันแล้วลองใช้ 73 ทั้งสองวิธีใช้งานได้ แต่เนื่องจากเป็นการทดลองเชิงประจักษ์จึงไม่เข้าใจว่าคุณต้องการขึ้นหรือลงกี่เวอร์ชัน เพื่อกระโดดหรือต้องรอนานแค่ไหน

ต่อมาฉันสามารถสร้างสถานการณ์ขึ้นมาใหม่ได้: เราเปิดใช้งานไคลเอนต์ ปิดมัน คอมไพล์วงจรใหม่เป็นอีกเลเยอร์หนึ่ง รีสตาร์ท จับปัญหาอีกครั้ง กลับไปที่อันก่อนหน้า - โอ๊ะโอ ไม่มีการสลับวงจรจำนวนเท่าใด และไคลเอนต์รีสตาร์ทเป็นเวลา ไม่กี่นาทีจะช่วยได้ คุณจะได้รับโครงสร้างข้อมูลจากชั้นต่างๆ ผสมกัน

คำอธิบาย? เนื่องจากคุณสามารถเดาได้จากอาการทางอ้อมต่างๆ เซิร์ฟเวอร์ประกอบด้วยกระบวนการหลายประเภทในเครื่องที่แตกต่างกัน เป็นไปได้มากว่าเซิร์ฟเวอร์ที่รับผิดชอบในการ "บัฟเฟอร์" จะใส่สิ่งที่ผู้บังคับบัญชามอบให้ไว้ในคิว และพวกเขาก็มอบมันตามแบบแผนที่วางไว้ ณ เวลาที่สร้าง และจนกว่าคิวนี้จะ "เน่า" ก็ทำอะไรไม่ได้

บางที... แต่นี่เป็นไม้ค้ำยันที่แย่มาก!.. ไม่ ก่อนที่จะคิดถึงไอเดียบ้าๆ มาดูโค้ดของลูกค้าอย่างเป็นทางการกันดีกว่า ในเวอร์ชัน Android เราไม่พบตัวแยกวิเคราะห์ TL ใด ๆ แต่เราพบไฟล์ขนาดใหญ่ (GitHub ปฏิเสธที่จะแก้ไขมัน) พร้อมด้วย (de) การทำให้เป็นอนุกรม นี่คือข้อมูลโค้ด:

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

หรือ

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

อืม...ดูดุร้ายนะ แต่บางทีนี่อาจเป็นโค้ดที่สร้างขึ้นแล้วโอเคไหม.. แต่มันรองรับทุกเวอร์ชันอย่างแน่นอน! จริงอยู่ มันไม่ชัดเจนว่าทำไมทุกอย่างจึงปะปนกัน การแชทลับ และอื่นๆ อีกมากมาย _old7 ดูไม่เหมือนการสร้างเครื่องจักรเลย... อย่างไรก็ตาม ที่สำคัญที่สุด ฉันรู้สึกทึ่งมาก

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

พวกคุณตัดสินใจไม่ได้เหรอว่ามีอะไรอยู่ในชั้นเดียว?! โอเค สมมติว่า "สอง" ถูกปล่อยออกมาโดยมีข้อผิดพลาด มันเกิดขึ้น แต่สามล่ะ.. คราดแบบเดิมอีกแล้วเหรอ? นี่มันสื่อลามกประเภทไหน ขอโทษที..

อย่างไรก็ตามในซอร์สโค้ดของ Telegram Desktop สิ่งที่คล้ายกันก็เกิดขึ้น - หากเป็นเช่นนั้น คอมมิตหลายอย่างในแถวกับโครงร่างจะไม่เปลี่ยนหมายเลขเลเยอร์ แต่แก้ไขบางอย่าง ในเงื่อนไขที่ไม่มีแหล่งข้อมูลอย่างเป็นทางการสำหรับโครงการ จะหาได้จากที่ไหน ยกเว้นซอร์สโค้ดของลูกค้าอย่างเป็นทางการ และถ้าคุณนำมาจากจุดนั้น คุณจะไม่สามารถแน่ใจได้ว่ารูปแบบนั้นถูกต้องสมบูรณ์จนกว่าคุณจะทดสอบวิธีการทั้งหมด

สิ่งนี้สามารถทดสอบได้อย่างไร? ฉันหวังว่าแฟน ๆ ของหน่วย การทดสอบการทำงาน และการทดสอบอื่น ๆ จะแบ่งปันในความคิดเห็น

เอาล่ะ มาดูโค้ดอีกชิ้นกัน:

public static class TL_folders_deleteFolder extends TLObject {
    public static int constructor = 0x1c295881;

    public int folder_id;

    public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) {
        return Updates.TLdeserialize(stream, constructor, exception);
    }

    public void serializeToStream(AbstractSerializedData stream) {
        stream.writeInt32(constructor);
        stream.writeInt32(folder_id);
    }
}

//manually created

//RichText start
public static abstract class RichText extends TLObject {
    public String url;
    public long webpage_id;
    public String email;
    public ArrayList<RichText> texts = new ArrayList<>();
    public RichText parentRichText;

    public static RichText TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
        RichText result = null;
        switch (constructor) {
            case 0x1ccb966a:
                result = new TL_textPhone();
                break;
            case 0xc7fb5e01:
                result = new TL_textSuperscript();
                break;

ความคิดเห็นนี้ "สร้างด้วยตนเอง" แสดงให้เห็นว่ามีเพียงบางส่วนของไฟล์นี้เท่านั้นที่เขียนด้วยตนเอง (คุณนึกภาพฝันร้ายในการบำรุงรักษาทั้งหมดออกไหม) และส่วนที่เหลือสร้างขึ้นโดยเครื่อง อย่างไรก็ตาม มีคำถามอีกข้อหนึ่งเกิดขึ้น - ว่ามีแหล่งที่มาหรือไม่ ไม่สมบูรณ์ (a la GPL blobs ในเคอร์เนล Linux) แต่นี่เป็นหัวข้อสำหรับส่วนที่สองอยู่แล้ว

แต่พอแล้ว. เรามาดูโปรโตคอลที่อยู่ด้านบนสุดซึ่งการซีเรียลไลซ์ทั้งหมดนี้ทำงาน

MT โปรโต

งั้นเรามาเปิดกันดีกว่า คำอธิบายทั่วไป и คำอธิบายโดยละเอียดของโปรโตคอล และสิ่งแรกที่เราสะดุดคือคำศัพท์ และด้วยความอุดมสมบูรณ์ของทุกสิ่ง โดยทั่วไป นี่ดูเหมือนจะเป็นคุณสมบัติที่เป็นกรรมสิทธิ์ของ Telegram - การเรียกสิ่งต่าง ๆ ที่แตกต่างกันในที่ต่าง ๆ หรือสิ่งต่าง ๆ ด้วยคำเดียวหรือในทางกลับกัน (เช่นใน API ระดับสูง หากคุณเห็นชุดสติ๊กเกอร์ ก็ไม่เป็นเช่นนั้น สิ่งที่คุณคิด)

ตัวอย่างเช่น "ข้อความ" และ "เซสชัน" หมายถึงสิ่งที่แตกต่างไปจากอินเทอร์เฟซไคลเอ็นต์ Telegram ทั่วไป ข้อความทุกอย่างชัดเจนสามารถตีความได้ในแง่ OOP หรือเรียกง่ายๆว่าคำว่า "แพ็กเก็ต" - นี่เป็นระดับการขนส่งต่ำไม่มีข้อความเหมือนกับในอินเทอร์เฟซมีข้อความบริการมากมาย . แต่เซสชัน... แต่สิ่งแรกสุดก่อน

ชั้นขนส่ง

สิ่งแรกคือการขนส่ง พวกเขาจะบอกเราเกี่ยวกับ 5 ตัวเลือก:

  • TCP
  • เว็บซ็อคเก็ต
  • เว็บซ็อกเก็ตผ่าน HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] นอกจากนี้ยังมีการขนส่ง UDP ด้วย แต่ไม่มีเอกสาร

และ TCP ในสามรูปแบบ

อันแรกคล้ายกับ UDP บน TCP แต่ละแพ็กเก็ตจะมีหมายเลขลำดับและ crc
ทำไมการอ่านเอกสารบนรถเข็นถึงเจ็บปวดขนาดนี้?

ตอนนี้ก็มีแล้ว TCP มีอยู่แล้วใน 4 รูปแบบ:

  • สรุป
  • Intermediate
  • มีเบาะรองกลาง
  • เต็ม

โอเค มีเบาะรองระดับกลางสำหรับ MTProxy ซึ่งถูกเพิ่มในภายหลังเนื่องจากเหตุการณ์ที่รู้จักกันดี แต่ทำไมถึงมีสองเวอร์ชัน (รวมทั้งหมดสามเวอร์ชัน) ในเมื่อคุณสามารถใช้เวอร์ชันเดียวได้? โดยพื้นฐานแล้วทั้งสี่แตกต่างกันเพียงวิธีตั้งค่าความยาวและเพย์โหลดของ MTProto หลัก ซึ่งจะกล่าวถึงต่อไป:

  • ใน Abridged จะเป็น 1 หรือ 4 ไบต์ แต่ไม่ใช่ 0xef ตามด้วยเนื้อหา
  • ในระดับกลาง นี่คือความยาว 4 ไบต์และฟิลด์ และเป็นครั้งแรกที่ไคลเอ็นต์ต้องส่ง 0xeeeeeeee เพื่อระบุว่าเป็นสื่อกลาง
  • ในแบบเต็มเสพติดมากที่สุดจากมุมมองของเครือข่าย: ความยาวหมายเลขลำดับและไม่ใช่อันที่ส่วนใหญ่เป็น MTProto เนื้อหา CRC32 ใช่ ทั้งหมดนี้อยู่เหนือ TCP ซึ่งให้การขนส่งที่เชื่อถือได้แก่เราในรูปแบบของสตรีมไบต์แบบเรียงลำดับ ไม่จำเป็นต้องมีลำดับใดๆ โดยเฉพาะเช็คซัม โอเค ตอนนี้มีคนจะคัดค้านฉันว่า TCP มีเช็คซัม 16 บิต ข้อมูลเสียหายจึงเกิดขึ้น เยี่ยมมาก แต่จริงๆ แล้ว เรามีโปรโตคอลการเข้ารหัสที่มีแฮชยาวกว่า 16 ไบต์ ข้อผิดพลาดเหล่านี้ทั้งหมด และยิ่งกว่านั้นอีก จะถูกตรวจพบโดย SHA ที่ไม่ตรงกันในระดับที่สูงกว่า ไม่มีประเด็นใดใน CRC32 นอกเหนือจากนี้

ลองเปรียบเทียบ Abridged ซึ่งมีความยาวได้หนึ่งไบต์กับ Intermediate ซึ่งจัดชิดขอบ "ในกรณีที่จำเป็นต้องมีการจัดตำแหน่งข้อมูล 4 ไบต์" ซึ่งค่อนข้างไร้สาระ เชื่อกันว่าอะไรคือโปรแกรมเมอร์ Telegram ไร้ความสามารถจนไม่สามารถอ่านข้อมูลจากซ็อกเก็ตลงในบัฟเฟอร์ที่จัดตำแหน่งได้ คุณยังคงต้องทำเช่นนี้ เนื่องจากการอ่านสามารถส่งคืนไบต์เป็นจำนวนเท่าใดก็ได้ (และยังมีพร็อกซีเซิร์ฟเวอร์ด้วย เช่น...) หรือในทางกลับกัน ทำไมต้องบล็อก Abridged ถ้าเรายังมีช่องว่างภายในที่หนักหน่วงอยู่ที่ 16 ไบต์ - ประหยัด 3 ไบต์ บางครั้ง ?

มีคนรู้สึกว่า Nikolai Durov ชอบประดิษฐ์วงล้อใหม่ รวมถึงโปรโตคอลเครือข่าย โดยไม่จำเป็นต้องใช้งานจริง

ตัวเลือกการขนส่งอื่น ๆ ได้แก่ เว็บและ MTProxy เราจะไม่พิจารณาในตอนนี้ อาจจะอยู่ในโพสต์อื่น หากมีการร้องขอ เกี่ยวกับ MTProxy เดียวกันนี้ ขอให้เราจำไว้ว่าหลังจากเปิดตัวในปี 2018 ไม่นาน ผู้ให้บริการก็เรียนรู้อย่างรวดเร็วที่จะบล็อกมัน โดยมีจุดประสงค์เพื่อ บายพาสการปิดกั้นโดย ขนาดบรรจุภัณฑ์! และความจริงที่ว่าเซิร์ฟเวอร์ MTProxy ที่เขียน (อีกครั้งโดย Waltman) ใน C นั้นเชื่อมโยงกับข้อมูลเฉพาะของ Linux มากเกินไปแม้ว่าจะไม่จำเป็นเลยก็ตาม (Phil Kulin จะยืนยัน) และเซิร์ฟเวอร์ที่คล้ายกันใน Go หรือ Node.js พอดีไม่ถึงร้อยบรรทัด

แต่เราจะสรุปเกี่ยวกับความรู้ทางเทคนิคของคนเหล่านี้ในตอนท้ายของส่วนนี้ หลังจากพิจารณาประเด็นอื่นๆ แล้ว สำหรับตอนนี้ เรามาต่อกันที่ OSI เลเยอร์ 5 ซึ่งเป็นเซสชันที่พวกเขาวางเซสชัน MTProto

คีย์ ข้อความ เซสชัน ดิฟฟี่-เฮลล์แมน

พวกเขาวางไว้ไม่ถูกต้องทั้งหมด... เซสชันไม่ใช่เซสชันเดียวกับที่มองเห็นได้ในอินเทอร์เฟซภายใต้เซสชันที่ใช้งานอยู่ แต่ตามลำดับ.

คำติชมของโปรโตคอลและแนวทางองค์กรของ Telegram ส่วนที่ 1 ด้านเทคนิค: ประสบการณ์การเขียนลูกค้าตั้งแต่เริ่มต้น - TL, MT

ดังนั้นเราจึงได้รับสตริงไบต์ที่ทราบความยาวจากเลเยอร์การขนส่ง นี่เป็นข้อความที่เข้ารหัสหรือข้อความธรรมดา หากเรายังอยู่ในขั้นตอนข้อตกลงหลักและกำลังดำเนินการอยู่จริงๆ เรากำลังพูดถึงแนวคิดใดที่เรียกว่า "กุญแจ" มาชี้แจงปัญหานี้ให้กับทีม Telegram กัน (ฉันขอโทษที่แปลเอกสารของฉันเองจากภาษาอังกฤษด้วยสมองที่เหนื่อยล้าตอนตี 4 มันง่ายกว่าที่จะปล่อยบางวลีไว้เหมือนเดิม):

มีอยู่ ๒ ประการ เรียกว่า เซสชั่น - หนึ่งใน UI ของลูกค้าอย่างเป็นทางการภายใต้ "เซสชันปัจจุบัน" โดยที่แต่ละเซสชันสอดคล้องกับอุปกรณ์ / ระบบปฏิบัติการทั้งหมด
ประการที่สองคือ เซสชั่น MTProtoซึ่งมีหมายเลขลำดับของข้อความ (ในความหมายระดับต่ำ) อยู่ในนั้น และอันไหน อาจคงอยู่ระหว่างการเชื่อมต่อ TCP ที่แตกต่างกัน สามารถติดตั้ง MTProto หลายเซสชันพร้อมกันได้ เช่น เพื่อเพิ่มความเร็วในการดาวน์โหลดไฟล์

ระหว่างสองคนนี้ ครั้ง ราคา มีแนวคิดอยู่ การอนุญาต. ในกรณีที่เสื่อมเราก็พูดอย่างนั้นได้ เซสชัน UI ก็เหมือนกับ การอนุญาตแต่อนิจจาทุกอย่างซับซ้อน มาดูกัน:

  • ผู้ใช้บนอุปกรณ์ใหม่สร้างขึ้นก่อน auth_key และผูกเข้ากับบัญชี เช่น ทาง SMS นั่นคือสาเหตุ การอนุญาต
  • มันเกิดขึ้นภายในครั้งแรก เซสชั่น MTProtoซึ่งมี session_id ภายในตัวคุณ
  • ในขั้นตอนนี้ให้ทำการรวมกัน การอนุญาต и session_id สามารถเรียกได้ว่า ตัวอย่าง - คำนี้ปรากฏในเอกสารและรหัสของลูกค้าบางราย
  • จากนั้นลูกค้าก็สามารถเปิดได้ บาง เซสชัน MTProto ภายใต้สิ่งเดียวกัน auth_key - ไปยัง DC เดียวกัน
  • จากนั้นวันหนึ่งลูกค้าจะต้องขอไฟล์จาก ดีซีอีกอัน - และสำหรับ DC นี้ DC ใหม่จะถูกสร้างขึ้น auth_key !
  • เพื่อแจ้งระบบว่าไม่ใช่ผู้ใช้ใหม่ที่กำลังลงทะเบียนแต่เป็นผู้ใช้เดิม การอนุญาต (เซสชัน UI) ไคลเอนต์ใช้การเรียก API auth.exportAuthorization ที่บ้านดีซี auth.importAuthorization ในดีซีใหม่
  • ทุกอย่างเหมือนเดิมหลายอันอาจจะเปิดอยู่ เซสชัน MTProto (แต่ละอย่างมีของตัวเอง session_id) ไปยัง DC ใหม่นี้ภายใต้ ของเขา auth_key.
  • สุดท้าย ลูกค้าอาจต้องการ Perfect Forward Secrecy ทั้งหมด auth_key เป็น ถาวร คีย์ - ต่อ DC - และลูกค้าสามารถโทรได้ auth.bindTempAuthKey สำหรับการใช้งาน ชั่วคราว auth_key - และอีกครั้งเพียงอันเดียวเท่านั้น temp_auth_key ต่อ DC ทั่วไปสำหรับทุกคน เซสชัน MTProto ถึงดีซีแห่งนี้

สังเกตได้ว่า เกลือ (และเกลือแห่งอนาคต) ก็เป็นหนึ่งในนั้นเช่นกัน auth_key เหล่านั้น. แบ่งปันระหว่างทุกคน เซสชัน MTProto ไปยัง DC เดียวกัน

"ระหว่างการเชื่อมต่อ TCP ที่แตกต่างกัน" หมายความว่าอย่างไร ดังนั้นนี่หมายความว่า สิ่งที่ต้องการ คุกกี้การอนุญาตบนเว็บไซต์ - ยังคงมีอยู่ (รอด) การเชื่อมต่อ TCP จำนวนมากไปยังเซิร์ฟเวอร์ที่กำหนด แต่วันหนึ่งมันเกิดข้อผิดพลาด แตกต่างจาก HTTP เท่านั้น ในข้อความ MTProto ภายในเซสชันจะถูกกำหนดหมายเลขและยืนยันตามลำดับ หากพวกเขาเข้าสู่ทันเนล การเชื่อมต่อขาด - หลังจากสร้างการเชื่อมต่อใหม่ เซิร์ฟเวอร์จะส่งทุกอย่างในเซสชันนี้ที่ไม่ได้ส่งในครั้งก่อน การเชื่อมต่อทีพีพี

อย่างไรก็ตาม ข้อมูลข้างต้นได้รับการสรุปหลังจากการสอบสวนหลายเดือน ในระหว่างนี้ เรากำลังดำเนินการกับลูกค้าของเราตั้งแต่เริ่มต้นหรือไม่ - กลับไปที่จุดเริ่มต้นกันเถอะ

เรามาสร้างกัน auth_key บน เวอร์ชัน Diffie-Hellman จาก Telegram. มาลองทำความเข้าใจกับเอกสาร...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (ไบต์สุ่มใด ๆ); โดยมีความยาวเท่ากับ 255 ไบต์
encrypted_data := RSA(data_with_hash, server_public_key); ตัวเลขยาว 255 ไบต์ (big endian) จะถูกยกขึ้นเป็นกำลังที่จำเป็นเหนือโมดูลัสที่จำเป็น และผลลัพธ์จะถูกจัดเก็บเป็นตัวเลข 256 ไบต์

พวกเขามี DH ยาเสพติดอยู่บ้าง

ดูไม่เหมือน DH ของคนที่มีสุขภาพดี
ไม่มีกุญแจสาธารณะสองตัวใน dx

ในท้ายที่สุดสิ่งนี้ก็ถูกแยกออก แต่ยังมีสารตกค้างอยู่ - ลูกค้าพิสูจน์การทำงานว่าเขาสามารถแยกตัวประกอบจำนวนได้ ประเภทของการป้องกันการโจมตี DoS และคีย์ RSA จะใช้เพียงครั้งเดียวในทิศทางเดียวเท่านั้น สำหรับการเข้ารหัสเป็นหลัก new_nonce. แต่ในขณะที่การดำเนินการที่ดูเหมือนง่ายนี้จะประสบความสำเร็จ คุณจะต้องเผชิญอะไร?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] ฉันยังไม่ได้รับคำขอ appid

ฉันส่งคำขอนี้ไปยัง DH

และในท่าเรือขนส่งก็บอกว่าสามารถตอบสนองด้วยรหัสข้อผิดพลาด 4 ไบต์ นั่นคือทั้งหมดที่

เขาบอกฉันว่า -404 แล้วไงล่ะ?

ดังนั้นฉันจึงบอกเขาว่า: "จับเรื่องไร้สาระของคุณที่เข้ารหัสด้วยรหัสเซิร์ฟเวอร์ที่มีลายนิ้วมือแบบนี้ ฉันต้องการ DH" แล้วมันตอบกลับด้วย 404 โง่ ๆ

คุณคิดอย่างไรกับการตอบกลับของเซิร์ฟเวอร์นี้ จะทำอย่างไร? ไม่มีใครถาม (แต่จะเพิ่มเติมในส่วนที่สอง)

ที่นี่ความสนใจทั้งหมดเสร็จสิ้นบนท่าเรือ

ฉันไม่มีอะไรทำ ฉันแค่ฝันว่าจะแปลงตัวเลขกลับไปกลับมา

ตัวเลข 32 บิตสองตัว ฉันบรรจุพวกมันเหมือนคนอื่นๆ

แต่ไม่ จำเป็นต้องเพิ่มทั้งสองนี้ในบรรทัดก่อนเป็น BE

Vadim Goncharov, [20.06.18 15:49] และเพราะเหตุนี้ 404?

วาซิลี [20.06.18 15:49] ใช่แล้ว!

วาดิม กอนชารอฟ [20.06.18 15:50] เลยไม่เข้าใจว่าเขา “หาไม่เจอ” เรื่องอะไร

วาซิลี [20.06.18 15:50] เกี่ยวกับ

ฉันไม่สามารถหาการสลายตัวเป็นปัจจัยสำคัญได้%)

เราไม่ได้จัดการการรายงานข้อผิดพลาดด้วยซ้ำ

Vasily, [20.06.18 20:18] โอ้ มี MD5 ด้วย มีแฮชที่แตกต่างกันสามอันแล้ว

ลายนิ้วมือหลักจะถูกคำนวณดังนี้:

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

SHA1 และ Sha2

เอาเป็นว่า auth_key เราได้รับขนาด 2048 บิตโดยใช้ Diffie-Hellman อะไรต่อไป? ต่อไปเราจะพบว่าคีย์นี้มีขนาด 1024 บิตต่ำกว่าไม่ได้ใช้ในทางใดทางหนึ่ง... แต่ตอนนี้มาคิดเรื่องนี้กันดีกว่า ในขั้นตอนนี้ เรามีความลับที่แชร์กับเซิร์ฟเวอร์ มีการสร้างเซสชัน TLS แบบอะนาล็อกซึ่งเป็นขั้นตอนที่มีราคาแพงมาก แต่เซิร์ฟเวอร์ยังไม่รู้อะไรเลยว่าเราเป็นใคร! ยังไม่จริงเลย การอนุญาต. เหล่านั้น. หากคุณคิดในแง่ของ "รหัสผ่านเข้าสู่ระบบ" เหมือนที่คุณเคยทำใน ICQ หรืออย่างน้อย "รหัสเข้าสู่ระบบ" เช่นเดียวกับใน SSH (ตัวอย่างเช่น บน gitlab/github บางรุ่น) เราได้รับอันที่ไม่ระบุชื่อ จะเกิดอะไรขึ้นหากเซิร์ฟเวอร์แจ้งเราว่า “หมายเลขโทรศัพท์เหล่านี้ให้บริการโดย DC อื่น”? หรือแม้กระทั่ง “หมายเลขโทรศัพท์ของคุณถูกแบน”? สิ่งที่ดีที่สุดที่เราทำได้คือเก็บกุญแจไว้โดยหวังว่ามันจะมีประโยชน์และจะไม่เน่าเสียเมื่อถึงเวลานั้น

อีกอย่างเรา “ได้รับ” ด้วยการจองครับ เช่น เราเชื่อถือเซิร์ฟเวอร์หรือไม่? แล้วถ้าเป็นของปลอมล่ะ? จำเป็นต้องมีการตรวจสอบการเข้ารหัส:

Vasily, [21.06.18 17:53] พวกเขาเสนอไคลเอนต์มือถือเพื่อตรวจสอบหมายเลข 2kbit สำหรับความเป็นมา%)

แต่มันก็ไม่ชัดเจนเลย นาเฟยัว

Vasily, [21.06.18 18:02] เอกสารไม่ได้บอกว่าต้องทำอย่างไรหากกลายเป็นเรื่องไม่ง่าย

ไม่ได้กล่าวไว้. มาดูกันว่าไคลเอนต์ Android อย่างเป็นทางการทำอะไรในกรณีนี้? ก นั่นคือสิ่งที่ (และใช่ ไฟล์ทั้งหมดน่าสนใจ) อย่างที่พวกเขาพูด ฉันจะทิ้งสิ่งนี้ไว้ที่นี่:

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

ไม่ แน่นอนว่ามันยังอยู่ที่นั่น บาง มีการทดสอบความเป็นปฐมภูมิของจำนวนหนึ่ง แต่โดยส่วนตัวแล้ว ฉันไม่มีความรู้ทางคณิตศาสตร์เพียงพออีกต่อไป

โอเค เราได้มาสเตอร์คีย์แล้ว หากต้องการเข้าสู่ระบบเช่น ส่งคำขอ คุณจะต้องทำการเข้ารหัสเพิ่มเติมโดยใช้ AES

คีย์ข้อความถูกกำหนดให้เป็นบิตกลาง 128 บิตของ SHA256 ของเนื้อหาข้อความ (รวมถึงเซสชัน รหัสข้อความ ฯลฯ) รวมถึงไบต์ที่เติม ซึ่งต่อท้ายด้วย 32 ไบต์ที่นำมาจากคีย์การให้สิทธิ์

Vasily, [22.06.18 14:08] ธรรมดา, ผู้หญิงเลว, บิต

ได้รับ auth_key. ทั้งหมด. นอกเหนือจากนั้น...จากเอกสารก็ไม่ชัดเจน อย่าลังเลที่จะศึกษาโค้ดโอเพ่นซอร์ส

โปรดทราบว่า MTProto 2.0 ต้องใช้ช่องว่างภายในตั้งแต่ 12 ถึง 1024 ไบต์ โดยยังคงอยู่ภายใต้เงื่อนไขที่ความยาวของข้อความผลลัพธ์จะหารด้วย 16 ไบต์

แล้วควรเสริมเบาะเท่าไหร่ครับ?

และใช่ มี 404 ในกรณีที่เกิดข้อผิดพลาดด้วย

หากใครศึกษาไดอะแกรมและข้อความในเอกสารอย่างละเอียดจะสังเกตเห็นว่าไม่มี MAC อยู่ที่นั่น และ AES นั้นถูกใช้ในโหมด IGE บางโหมดที่ไม่ได้ใช้ที่อื่น แน่นอนว่าพวกเขาเขียนเกี่ยวกับสิ่งนี้ในคำถามที่พบบ่อย... ที่นี่คีย์ข้อความเองก็เป็นแฮช SHA ของข้อมูลที่ถอดรหัสซึ่งใช้เพื่อตรวจสอบความสมบูรณ์ - และในกรณีที่ไม่ตรงกัน เอกสารประกอบด้วยเหตุผลบางประการ แนะนำให้เพิกเฉยต่อพวกเขา (แต่แล้วเรื่องความปลอดภัยล่ะ ถ้าพวกเขาทำลายเราล่ะ?)

ฉันไม่ใช่นักเข้ารหัส อาจไม่มีอะไรผิดปกติกับโหมดนี้ในกรณีนี้จากมุมมองทางทฤษฎี แต่ฉันสามารถบอกชื่อปัญหาในทางปฏิบัติได้อย่างชัดเจน โดยใช้ Telegram Desktop เป็นตัวอย่าง มันเข้ารหัสแคชในเครื่อง (D877F783D5D3EF8C ทั้งหมดเหล่านี้) ในลักษณะเดียวกับข้อความใน MTProto (เฉพาะในกรณีนี้เวอร์ชัน 1.0) เช่น อันดับแรกคือคีย์ข้อความ จากนั้นจึงตามด้วยข้อมูล (และนอกเหนือจากคีย์หลักที่สำคัญ auth_key 256 ไบต์โดยไม่มี msg_key ไร้ประโยชน์). ดังนั้นปัญหาจะสังเกตเห็นได้ชัดเจนในไฟล์ขนาดใหญ่ กล่าวคือ คุณต้องเก็บข้อมูลไว้สองชุด - เข้ารหัสและถอดรหัส และหากมีเมกะไบต์หรือสตรีมมิ่งวิดีโอเป็นต้น.. รูปแบบคลาสสิกที่มี MAC หลังจากไซเฟอร์เท็กซ์ช่วยให้คุณสามารถอ่านสตรีมและส่งสัญญาณได้ทันที แต่ด้วย MTProto คุณจะต้องทำ แรก เข้ารหัสหรือถอดรหัสข้อความทั้งหมด จากนั้นจึงถ่ายโอนไปยังเครือข่ายหรือดิสก์ ดังนั้นใน Telegram Desktop เวอร์ชันล่าสุดจึงอยู่ในแคช user_data นอกจากนี้ยังใช้รูปแบบอื่นด้วย - โดยมี AES ในโหมด CTR

Vasily, [21.06.18 01:27] โอ้ ฉันพบว่า IGE คืออะไร: IGE เป็นความพยายามครั้งแรกใน "โหมดการเข้ารหัสการรับรองความถูกต้อง" ซึ่งเดิมมีไว้สำหรับ Kerberos เป็นความพยายามที่ล้มเหลว (ไม่ได้ให้การปกป้องความสมบูรณ์) และต้องถูกลบออก นั่นคือจุดเริ่มต้นของภารกิจ 20 ปีสำหรับโหมดการเข้ารหัสรับรองความถูกต้องที่ใช้งานได้ ซึ่งเพิ่งมาถึงจุดสุดยอดในโหมดเช่น OCB และ GCM

และตอนนี้ข้อโต้แย้งจากฝั่งรถเข็น:

ทีมที่อยู่เบื้องหลัง Telegram ซึ่งนำโดย Nikolai Durov ประกอบด้วยแชมป์ ACM หกคน โดยครึ่งหนึ่งเป็นปริญญาเอกสาขาคณิตศาสตร์ พวกเขาใช้เวลาประมาณสองปีในการเปิดตัว MTProto เวอร์ชันปัจจุบัน

มันสนุก. สองปีในระดับล่าง

หรือคุณก็แค่หา tls

เอาล่ะ สมมติว่าเราได้ทำการเข้ารหัสและความแตกต่างอื่นๆ แล้ว ในที่สุดก็เป็นไปได้ไหมที่จะส่งคำขอต่อเนื่องใน TL และดีซีเรียลไลซ์การตอบกลับ แล้วต้องส่งอะไรและอย่างไร? ที่นี่สมมติว่าวิธีการ การเชื่อมต่อเริ่มต้นบางทีนี่อาจจะเป็นเหรอ?

Vasily, [25.06.18 18:46] เริ่มต้นการเชื่อมต่อและบันทึกข้อมูลบนอุปกรณ์และแอปพลิเคชันของผู้ใช้

ยอมรับ app_id, device_model, system_version, app_version และ lang_code

และสอบถามมาบ้าง

เอกสารเช่นเคย. รู้สึกอิสระที่จะศึกษาโอเพ่นซอร์ส

หากทุกอย่างชัดเจนโดยประมาณด้วย involvedWithLayer แล้วเกิดอะไรขึ้นที่นี่ ปรากฎว่า สมมติว่าเรามี - ลูกค้ามีเรื่องที่จะถามเซิร์ฟเวอร์อยู่แล้ว - มีคำขอที่เราต้องการส่ง:

Vasily, [25.06.18 19:13] เมื่อดูจากโค้ดแล้ว การโทรครั้งแรกก็อยู่ในอึนี้ และอึเองก็ถูกห่อด้วยการเรียกใช้ด้วยเลเยอร์

เหตุใด initConnection จึงไม่สามารถแยกการโทรได้ แต่ต้องเป็น wrapper ใช่ ปรากฎว่าจะต้องดำเนินการทุกครั้งในช่วงเริ่มต้นของแต่ละเซสชัน ไม่ใช่เพียงครั้งเดียว เช่นเดียวกับคีย์หลัก แต่! ผู้ใช้ที่ไม่ได้รับอนุญาตไม่สามารถเรียกได้! ตอนนี้เรามาถึงขั้นที่สามารถนำไปใช้ได้แล้ว อันนี้ หน้าเอกสารประกอบ - และมันบอกเราว่า...

วิธีการ API มีเพียงส่วนเล็กๆ เท่านั้นที่พร้อมใช้งานสำหรับผู้ใช้ที่ไม่ได้รับอนุญาต:

  • auth.sendCode
  • auth.resendCode
  • account.getPassword
  • auth.checkPassword
  • auth.checkPhone
  • auth.signUp
  • auth.signIn
  • auth.importการอนุญาต
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

คนแรกของพวกเขา auth.sendCodeและมีคำขอแรกอันเป็นที่รักซึ่งเราจะส่ง api_id และ api_hash และหลังจากนั้นเราได้รับ SMS พร้อมรหัส และหากเราอยู่ใน 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 ค่าที่สามคือ

ใช่ ตั้งแต่นั้นเป็นต้นมา เอกสารก็ได้รับการอัปเดตอย่างแน่นอน แม้ว่าในไม่ช้ามันอาจไม่เกี่ยวข้องอีกต่อไปก็ตาม Developer มือใหม่ควรรู้ได้อย่างไร? บางทีถ้าคุณลงทะเบียนใบสมัครพวกเขาจะแจ้งให้คุณทราบ? Vasily ทำสิ่งนี้ แต่อนิจจาพวกเขาไม่ได้ส่งอะไรเลย (เราจะพูดถึงเรื่องนี้อีกครั้งในส่วนที่สอง)

...คุณสังเกตเห็นว่าเราได้ย้ายไปยัง API แล้ว เช่น ไปอีกระดับและพลาดบางสิ่งบางอย่างในหัวข้อ MTProto? ไม่แปลกใจเลย:

Vasily [28.06.18 02:04] พวกเขากำลังค้นหาอัลกอริธึมบางอย่างใน e2e

Mtproto กำหนดอัลกอริธึมและคีย์การเข้ารหัสสำหรับทั้งสองโดเมน รวมถึงโครงสร้าง wrapper เล็กน้อย

แต่พวกเขาผสมระดับต่าง ๆ ของสแต็กอย่างต่อเนื่อง ดังนั้นจึงไม่ชัดเจนเสมอไปว่า mtproto สิ้นสุดที่ใดและระดับต่อไปเริ่มต้นขึ้น

พวกเขาผสมกันอย่างไร? นี่เป็นคีย์ชั่วคราวเดียวกันสำหรับ PFS (อย่างไรก็ตาม Telegram Desktop ไม่สามารถทำได้) มันถูกดำเนินการโดยคำขอ API auth.bindTempAuthKey, เช่น. จากระดับบนสุด แต่ในขณะเดียวกันก็รบกวนการเข้ารหัสในระดับล่าง - หลังจากนั้นคุณต้องทำอีกครั้ง initConnection ฯลฯ นี่ไม่ใช่ เพียงแค่ คำขอปกติ สิ่งที่พิเศษคือคุณสามารถมีคีย์ชั่วคราวได้เพียงคีย์เดียวต่อ DC แม้ว่าจะเป็นฟิลด์ก็ตาม auth_key_id ในแต่ละข้อความช่วยให้คุณสามารถเปลี่ยนคีย์ได้อย่างน้อยทุกข้อความและเซิร์ฟเวอร์มีสิทธิ์ "ลืม" คีย์ชั่วคราวได้ตลอดเวลา - เอกสารไม่ได้บอกว่าต้องทำอย่างไรในกรณีนี้... แล้วทำไมถึงทำได้ คุณไม่มีกุญแจหลายอันเหมือนกับชุดเกลือแห่งอนาคตและ?..

มีสิ่งอื่นอีกสองสามอย่างที่ควรทราบเกี่ยวกับธีม MTProto

ข้อความ, msg_id, msg_seqno, การยืนยัน, ปิงไปในทิศทางที่ผิด และลักษณะเฉพาะอื่นๆ

ทำไมคุณต้องรู้เกี่ยวกับพวกเขา? เพราะมัน “รั่ว” ไปสู่ระดับที่สูงกว่า และคุณจำเป็นต้องระวังเมื่อทำงานกับ API สมมติว่าเราไม่สนใจ msg_key เพราะระดับล่างได้ถอดรหัสทุกอย่างให้เราแล้ว แต่ภายในข้อมูลที่ถอดรหัสแล้ว เรามีฟิลด์ต่อไปนี้ (รวมถึงความยาวของข้อมูลด้วย ดังนั้นเราจึงรู้ว่าช่องว่างภายในอยู่ที่ไหน แต่นั่นไม่สำคัญ):

  • เกลือ - int64
  • session_id - int64
  • message_id — int64
  • seq_no - int32

เราขอเตือนคุณว่าเกลือ 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 ไม่ บน seq_noเช่นเดียวกับใน TCP แต่โดย msg_id !

นี่คืออะไร msg_idที่สำคัญที่สุดของสาขาเหล่านี้คืออะไร? ตัวระบุข้อความที่ไม่ซ้ำตามชื่อที่แนะนำ มันถูกกำหนดให้เป็นตัวเลข 64 บิต บิตต่ำสุดซึ่งมีเวทย์มนตร์ "เซิร์ฟเวอร์ไม่ใช่เซิร์ฟเวอร์" อีกครั้ง และที่เหลือคือการประทับเวลา Unix รวมถึงส่วนที่เป็นเศษส่วน จะถูกเลื่อนไปทางซ้าย 32 บิต เหล่านั้น. การประทับเวลาต่อตัว (และข้อความที่มีเวลาแตกต่างกันมากเกินไปจะถูกเซิร์ฟเวอร์ปฏิเสธ) จากนี้ปรากฎว่าโดยทั่วไปนี่คือตัวระบุที่เป็นสากลสำหรับไคลเอนต์ เมื่อพิจารณาแล้ว - จำไว้เถอะ session_id - เรารับประกัน: ไม่สามารถส่งข้อความสำหรับเซสชันหนึ่งไปยังเซสชันอื่นได้ไม่ว่าในกรณีใดก็ตาม. นั่นก็คือปรากฎว่ามีอยู่แล้ว สาม ระดับ - เซสชัน หมายเลขเซสชัน รหัสข้อความ เหตุใดจึงมีความซับซ้อนมากเกินไป ความลึกลับนี้จึงยิ่งใหญ่มาก

ดังนั้น msg_id จำเป็นสำหรับ...

RPC: คำขอ การตอบกลับ ข้อผิดพลาด การยืนยัน

ดังที่คุณอาจสังเกตเห็นแล้วว่าไม่มีประเภทหรือฟังก์ชัน "สร้างคำขอ RPC" พิเศษที่ใดก็ได้ในไดอะแกรม แม้ว่าจะมีคำตอบก็ตาม ท้ายที่สุด เรามีข้อความที่เกี่ยวข้องกับเนื้อหา! นั่นคือ, ใด ข้อความอาจเป็นคำขอ! หรือไม่ให้เป็น. หลังจากนั้น, แต่ละ เป็น msg_id. แต่มีคำตอบ:

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

ที่นี่จะระบุว่าข้อความใดเป็นการตอบกลับ ดังนั้นที่ระดับบนสุดของ API คุณจะต้องจำไว้ว่าคำขอของคุณมีจำนวนเท่าใด ฉันคิดว่าไม่จำเป็นต้องอธิบายว่างานเป็นแบบอะซิงโครนัส และอาจมีคำขอหลายรายการที่กำลังดำเนินการในเวลาเดียวกัน คำตอบที่สามารถส่งคืนได้ในลำดับใด ๆ ? โดยหลักการแล้ว จากสิ่งนี้และข้อความแสดงข้อผิดพลาดที่ไม่มีผู้ปฏิบัติงาน สถาปัตยกรรมที่อยู่เบื้องหลังสิ่งนี้สามารถตรวจสอบได้: เซิร์ฟเวอร์ที่ดูแลการเชื่อมต่อ TCP กับคุณคือ Front-End Balancer โดยจะส่งต่อคำขอไปยังแบ็กเอนด์และรวบรวมกลับผ่านทาง message_id. ดูเหมือนว่าทุกอย่างที่นี่ชัดเจน สมเหตุสมผล และดี

ใช่ไหม..แล้วถ้าลองคิดดูล่ะ? ท้ายที่สุดแล้ว การตอบสนองของ RPC ก็มีฟิลด์เช่นกัน msg_id! เราจำเป็นต้องตะโกนใส่เซิร์ฟเวอร์ว่า “คุณไม่ตอบคำตอบของฉัน!” หรือไม่? ใช่แล้ว มีอะไรเกี่ยวกับการยืนยันบ้าง? เกี่ยวกับเพจ ข้อความเกี่ยวกับข้อความ บอกเราว่าคืออะไร

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

และจะต้องทำกันคนละฝ่าย แต่ไม่เสมอไป! หากคุณได้รับ RpcResult RpcResult จะทำหน้าที่เป็นการยืนยัน นั่นคือเซิร์ฟเวอร์สามารถตอบสนองคำขอของคุณด้วย MsgsAck เช่น "ฉันได้รับแล้ว" RpcResult สามารถตอบสนองได้ทันที อาจเป็นได้ทั้งสองอย่าง

และใช่ คุณยังต้องตอบคำตอบ! การยืนยัน มิฉะนั้นเซิร์ฟเวอร์จะถือว่าไม่สามารถจัดส่งได้และส่งกลับมาให้คุณอีกครั้ง ถึงแม้จะเชื่อมต่อใหม่แล้วก็ตาม แต่แน่นอนว่าปัญหาการหมดเวลาก็เกิดขึ้นที่นี่ มาดูพวกเขากันอีกสักหน่อย

ในระหว่างนี้ เรามาดูข้อผิดพลาดในการดำเนินการค้นหาที่อาจเกิดขึ้นกัน

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

โอ้มีคนจะอุทานว่านี่คือรูปแบบที่มีมนุษยธรรมมากขึ้น - มีเส้น! ใช้เวลาของคุณ ที่นี่ รายการข้อผิดพลาดแต่แน่นอนว่ายังไม่สมบูรณ์ จากนั้นเราจะเรียนรู้ว่าโค้ดนั้นคืออะไร สิ่งที่ต้องการ ข้อผิดพลาด HTTP (แน่นอนว่าซีแมนทิกส์ของการตอบสนองไม่ได้รับการเคารพในบางสถานที่จะมีการกระจายแบบสุ่มระหว่างรหัส) และบรรทัดดูเหมือนว่า CAPITAL_LETTERS_AND_NUMBERS. ตัวอย่างเช่น PHONE_NUMBER_OCCUPIED หรือ FILE_PART_H_MISSING นั่นคือคุณยังคงต้องใช้บรรทัดนี้ แยกวิเคราะห์. ตัวอย่างเช่น FLOOD_WAIT_3600 จะหมายความว่าคุณต้องรอหนึ่งชั่วโมงและ PHONE_MIGRATE_5ว่าหมายเลขโทรศัพท์ที่มีคำนำหน้านี้จะต้องลงทะเบียนใน DC ที่ 5 เรามีภาษาพิมพ์ใช่ไหม? เราไม่ต้องการข้อโต้แย้งจากสตริง ข้อโต้แย้งปกติก็เพียงพอแล้ว

ขอย้ำอีกครั้งว่าสิ่งนี้ไม่ได้อยู่ในหน้าข้อความบริการ แต่ตามปกติแล้วสำหรับโปรเจ็กต์นี้ คุณสามารถดูข้อมูลได้ ในหน้าเอกสารอื่น. หรือ ทำให้เกิดความสงสัย. ประการแรก ดู การละเมิดการพิมพ์/เลเยอร์ - RpcError สามารถซ้อนกันได้ RpcResult. ทำไมไม่ออกไปข้างนอก? เราไม่ได้คำนึงถึงอะไร .. แล้วมีที่ไหนรับประกันว่า RpcError ไม่อาจฝังอยู่ใน RpcResultแต่จะโดยตรงหรือซ้อนอยู่ในประเภทอื่น?.. แล้วถ้าทำไม่ได้ทำไมไม่อยู่ระดับบนสุดล่ะนั่นก็คือ มันหายไป req_msg_id ? ..

แต่เรามาพูดถึงข้อความบริการกันต่อ ลูกค้าอาจคิดว่าเซิร์ฟเวอร์คิดมานานแล้วและทำคำขอที่ยอดเยี่ยมนี้:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

มีคำตอบที่เป็นไปได้สามข้อสำหรับคำถามนี้โดยตัดกับกลไกการยืนยันอีกครั้ง พยายามทำความเข้าใจว่าควรเป็นอย่างไร (และรายการประเภททั่วไปที่ไม่ต้องการการยืนยัน) ปล่อยให้ผู้อ่านทำการบ้าน (หมายเหตุ: ข้อมูลใน ซอร์สโค้ด Telegram Desktop ไม่สมบูรณ์)

การติดยา: สถานะข้อความ

โดยทั่วไปแล้ว สถานที่หลายแห่งใน TL, MTProto และ Telegram โดยทั่วไปจะให้ความรู้สึกดื้อรั้น แต่ขาดความสุภาพ ไหวพริบ และอื่นๆ ทักษะอ่อน เราเงียบเกี่ยวกับเรื่องนี้อย่างสุภาพ และเซ็นเซอร์คำหยาบคายในบทสนทนา อย่างไรก็ตามสถานที่แห่งนี้Оหน้าส่วนใหญ่เกี่ยวกับ ข้อความเกี่ยวกับข้อความ เป็นเรื่องที่น่าตกใจสำหรับฉันที่ทำงานเกี่ยวกับโปรโตคอลเครือข่ายมาเป็นเวลานานและเคยเห็นจักรยานที่มีระดับความคดงอต่างกันไป

มันเริ่มต้นอย่างไม่มีอันตรายพร้อมการยืนยัน ต่อไปพวกเขาจะบอกเราเกี่ยวกับ

bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;
bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;

ทุกคนที่เริ่มทำงานกับ MTProto จะต้องจัดการกับพวกเขา ในรอบ "แก้ไข - คอมไพล์ใหม่ - เปิดตัว" การได้รับข้อผิดพลาดด้านตัวเลขหรือเกลือที่จัดการได้ไม่ดีระหว่างการแก้ไขเป็นเรื่องปกติ อย่างไรก็ตาม มีสองประเด็นที่นี่:

  1. ซึ่งหมายความว่าข้อความต้นฉบับจะหายไป เราจำเป็นต้องสร้างคิวขึ้นมา เราจะมาดูกันทีหลัง
  2. หมายเลขข้อผิดพลาดแปลก ๆ เหล่านี้คืออะไร? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... แล้วเลขอื่นอยู่ไหนล่ะทอมมี่?

เอกสารระบุว่า:

จุดประสงค์คือจัดกลุ่มค่า 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;
    เมื่อได้รับแล้ว ข้อความจะถูกประมวลผลเหมือนกับว่าไม่มีกระดาษห่ออยู่ที่นั่น อย่างไรก็ตาม หากทราบแน่ชัดว่าได้รับข้อความ orig_message.msg_id ข้อความใหม่จะไม่ได้รับการประมวลผล (ในขณะเดียวกันก็ยอมรับข้อความนั้นและ orig_message.msg_id) ค่าของ orig_message.msg_id ต้องต่ำกว่า msg_id ของคอนเทนเนอร์

เรามาเงียบๆ กันดีกว่าว่าเรื่องอะไร msgs_state_info อีกครั้งที่หูของ TL ที่ยังสร้างไม่เสร็จยื่นออกมา (เราต้องการเวกเตอร์ไบต์ และในสองบิตล่างก็มี enum และในสองบิตที่สูงกว่าก็มีแฟล็ก) ประเด็นมันแตกต่างออกไป ไม่มีใครเข้าใจว่าทำไมทั้งหมดนี้ถึงเกิดขึ้นจริง? ในลูกค้าจริง จำเป็น?.. ด้วยความยากลำบาก แต่เราสามารถจินตนาการถึงประโยชน์บางอย่างได้หากบุคคลมีส่วนร่วมในการแก้ไขจุดบกพร่องและในโหมดโต้ตอบ - ถามเซิร์ฟเวอร์ว่าอะไรและอย่างไร แต่ที่นี่มีการอธิบายคำขอไว้ ไป - กลับ.

ตามมาว่าแต่ละฝ่ายจะต้องไม่เพียงแต่เข้ารหัสและส่งข้อความ แต่ยังจัดเก็บข้อมูลเกี่ยวกับตนเองเกี่ยวกับการตอบกลับพวกเขาโดยไม่ทราบระยะเวลา เอกสารประกอบไม่ได้อธิบายทั้งกำหนดเวลาหรือการบังคับใช้คุณสมบัติเหล่านี้ในทางปฏิบัติ ในทางไม่. สิ่งที่น่าทึ่งที่สุดคือพวกมันถูกใช้จริงในโค้ดของลูกค้าอย่างเป็นทางการ! เห็นได้ชัดว่าพวกเขาได้รับแจ้งบางอย่างที่ไม่รวมอยู่ในเอกสารสาธารณะ ทำความเข้าใจจากโค้ด ทำไม, นั้นไม่ง่ายเหมือนในกรณีของ TL อีกต่อไป - มันไม่ใช่ส่วนที่แยกออกจากกันทางตรรกะ (ค่อนข้าง) แต่เป็นชิ้นส่วนที่เชื่อมโยงกับสถาปัตยกรรมแอปพลิเคชันเช่น จะต้องใช้เวลามากขึ้นอย่างมากในการทำความเข้าใจรหัสแอปพลิเคชัน

ปิงและการกำหนดเวลา คิว

จากทุกสิ่ง ถ้าเราจำการเดาเกี่ยวกับสถาปัตยกรรมเซิร์ฟเวอร์ (การกระจายคำขอข้ามแบ็กเอนด์) สิ่งที่ค่อนข้างน่าเศร้าตามมา - แม้จะมีการรับประกันการส่งมอบทั้งหมดใน TCP (ข้อมูลจะถูกส่งหรือคุณจะได้รับแจ้งเกี่ยวกับช่องว่าง แต่ ข้อมูลจะถูกส่งก่อนที่ปัญหาจะเกิดขึ้น) การยืนยันใน MTProto นั้นเอง - ไม่มีการรับประกัน. เซิร์ฟเวอร์อาจสูญเสียหรือโยนข้อความของคุณออกไปได้อย่างง่ายดาย และไม่สามารถทำอะไรกับมันได้ เพียงแค่ใช้ไม้ค้ำประเภทต่างๆ

และก่อนอื่น - คิวข้อความ มีสิ่งหนึ่งที่ชัดเจนตั้งแต่เริ่มต้น - ข้อความที่ไม่ได้รับการยืนยันจะต้องถูกจัดเก็บและส่งอีกครั้ง และหลังจากเวลาใด? และตัวตลกก็รู้จักเขา บางทีข้อความบริการที่ติดยาเสพติดเหล่านั้นอาจช่วยแก้ปัญหานี้ได้ด้วยไม้ค้ำเช่นใน Telegram Desktop มีคิวประมาณ 4 คิวที่สอดคล้องกัน (อาจมากกว่านั้นดังที่ได้กล่าวไปแล้วสำหรับสิ่งนี้คุณต้องเจาะลึกโค้ดและสถาปัตยกรรมของมันอย่างจริงจังมากขึ้น ในเวลาเดียวกัน เวลา เรารู้ว่าไม่สามารถนำมาเป็นตัวอย่างได้ ไม่ได้ใช้บางประเภทจากโครงการ MTProto)

ทำไมสิ่งนี้ถึงเกิดขึ้น? อาจเป็นไปได้ว่าโปรแกรมเมอร์เซิร์ฟเวอร์ไม่สามารถรับประกันความน่าเชื่อถือภายในคลัสเตอร์ หรือแม้แต่การบัฟเฟอร์บน Front Balancer และโอนปัญหานี้ไปยังไคลเอนต์ ด้วยความสิ้นหวัง Vasily พยายามใช้ตัวเลือกอื่นโดยมีเพียงสองคิวโดยใช้อัลกอริธึมจาก TCP - การวัด RTT ไปยังเซิร์ฟเวอร์และปรับขนาดของ "หน้าต่าง" (ในข้อความ) ขึ้นอยู่กับจำนวนคำขอที่ไม่ได้รับการยืนยัน นั่นคือ ฮิวริสติกคร่าวๆ ในการประเมินโหลดของเซิร์ฟเวอร์คือจำนวนคำขอของเราที่เซิร์ฟเวอร์สามารถเคี้ยวได้ในเวลาเดียวกันและไม่สูญเสีย

นั่นคือคุณเข้าใจใช่ไหม? หากคุณต้องใช้ TCP อีกครั้งบนโปรโตคอลที่ทำงานบน TCP นี่แสดงว่าโปรโตคอลได้รับการออกแบบมาไม่ดีนัก

โอ้ ใช่แล้ว ทำไมคุณถึงต้องการมากกว่าหนึ่งคิว และนี่หมายความว่าอย่างไรสำหรับคนที่ทำงานกับ API ระดับสูงอยู่แล้ว? ดูสิ คุณส่งคำขอ ทำให้เป็นอนุกรม แต่บ่อยครั้งที่คุณไม่สามารถส่งได้ทันที ทำไม เพราะคำตอบก็จะเป็น. msg_idซึ่งเป็นเรื่องชั่วคราวаฉันเป็นค่ายเพลง ซึ่งการมอบหมายงานควรจะเลื่อนออกไปให้ช้าที่สุดเท่าที่จะทำได้ - ในกรณีที่เซิร์ฟเวอร์ปฏิเสธเนื่องจากเวลาระหว่างเรากับเขาไม่ตรงกัน (แน่นอน เราสามารถสร้างไม้ค้ำยันที่เปลี่ยนเวลาของเราจากปัจจุบันได้ ไปยังเซิร์ฟเวอร์โดยการเพิ่มเดลต้าที่คำนวณจากการตอบสนองของเซิร์ฟเวอร์ - ลูกค้าอย่างเป็นทางการทำเช่นนี้ แต่มันหยาบและไม่ถูกต้องเนื่องจากการบัฟเฟอร์) ดังนั้น เมื่อคุณทำการร้องขอด้วยการเรียกใช้ฟังก์ชันภายในเครื่องจากไลบรารี ข้อความจะผ่านขั้นตอนต่อไปนี้:

  1. มันอยู่ในคิวเดียวและรอการเข้ารหัส
  2. ได้รับการแต่งตั้ง msg_id และข้อความก็ไปที่คิวอื่น - การส่งต่อที่เป็นไปได้ ส่งไปที่ซ็อกเก็ต
  3. ก) เซิร์ฟเวอร์ตอบกลับ MsgsAck - ข้อความถูกส่งแล้ว เราจะลบออกจาก "คิวอื่น"
    b) หรือในทางกลับกัน เขาไม่ชอบอะไรบางอย่าง เขาตอบ badmsg - ส่งอีกครั้งจาก "คิวอื่น"
    c) ไม่ทราบสิ่งใด ข้อความจะต้องถูกส่งอีกครั้งจากคิวอื่น - แต่ไม่ทราบแน่ชัดว่าเมื่อใด
  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 วินาทีในภายหลัง เว้นแต่จะได้รับข้อความใหม่ประเภทเดียวกันซึ่งจะรีเซ็ตตัวจับเวลาก่อนหน้าทั้งหมดโดยอัตโนมัติ หากไคลเอ็นต์ส่ง Ping เหล่านี้หนึ่งครั้งทุกๆ 60 วินาที ก็อาจตั้งค่า Disconnect_delay เท่ากับ 75 วินาที

คุณบ้าหรือเปล่า?! ภายใน 60 วินาที รถไฟจะเข้าสู่สถานี ลงรับผู้โดยสาร และขาดการติดต่อในอุโมงค์อีกครั้ง ภายใน 120 วินาที ขณะที่คุณได้ยิน เสียงจะมาถึงอีกจุดหนึ่ง และการเชื่อมต่อน่าจะขาด ชัดเจนว่าขามาจากไหน - "ฉันได้ยินเสียงดัง แต่ไม่รู้ว่ามันอยู่ที่ไหน" มีอัลกอริทึมของ Nagl และตัวเลือก TCP_NODELAY ซึ่งมีไว้สำหรับการทำงานเชิงโต้ตอบ แต่ขอโทษที ยึดค่าเริ่มต้นไว้ - 200 Milliวินาที หากคุณต้องการพรรณนาถึงสิ่งที่คล้ายกันและบันทึกในแพ็กเก็ตที่เป็นไปได้ ให้เลื่อนออกไป 5 วินาที หรืออะไรก็ตามที่ข้อความ "ผู้ใช้กำลังพิมพ์..." หมดเวลาแล้ว แต่ไม่มีอีกแล้ว

และสุดท้ายก็ปิง นั่นคือการตรวจสอบความสดของการเชื่อมต่อ TCP น่าตลกดี แต่เมื่อประมาณ 10 ปีที่แล้ว ฉันเขียนข้อความวิจารณ์เกี่ยวกับผู้ส่งสารของหอพักคณะของเรา ผู้เขียนที่นั่นส่ง Ping ไปที่เซิร์ฟเวอร์จากไคลเอนต์ด้วย ไม่ใช่ในทางกลับกัน แต่นักศึกษาปี 3 ก็เรื่องหนึ่ง และสำนักงานระหว่างประเทศก็เป็นอีกเรื่องหนึ่งใช่ไหม..

ขั้นแรกให้โปรแกรมการศึกษาเล็กน้อย การเชื่อมต่อ TCP หากไม่มีการแลกเปลี่ยนแพ็กเก็ต สามารถใช้งานได้นานหลายสัปดาห์ มีทั้งดีและไม่ดีขึ้นอยู่กับจุดประสงค์ เป็นการดีถ้าคุณมีการเชื่อมต่อ SSH ที่เปิดอยู่บนเซิร์ฟเวอร์ คุณลุกขึ้นจากคอมพิวเตอร์ รีบูทเราเตอร์ และกลับไปยังที่ของคุณ - เซสชันผ่านเซิร์ฟเวอร์นี้ไม่ขาด (คุณไม่ได้พิมพ์อะไรเลย ไม่มีแพ็กเก็ต) ก็สะดวก เป็นเรื่องไม่ดีหากมีไคลเอนต์หลายพันรายบนเซิร์ฟเวอร์ โดยแต่ละรายใช้ทรัพยากร (สวัสดี Postgres!) และโฮสต์ของไคลเอ็นต์อาจรีบูตมานานแล้ว - แต่เราจะไม่รู้เรื่องนี้

ระบบแชท/IM ตกอยู่ในกรณีที่ 20 ด้วยเหตุผลเพิ่มเติม 18.04 ประการ นั่นก็คือ สถานะออนไลน์ หากผู้ใช้ "ล้มลง" คุณต้องแจ้งคู่สนทนาของเขาเกี่ยวกับเรื่องนี้ มิฉะนั้นคุณจะพบข้อผิดพลาดที่ผู้สร้าง Jabber ทำ (และแก้ไขเป็นเวลา XNUMX ปี) - ผู้ใช้ตัดการเชื่อมต่อ แต่พวกเขายังคงเขียนข้อความถึงเขาต่อไปโดยเชื่อว่าเขาออนไลน์อยู่ (ซึ่งหายไปโดยสิ้นเชิงในสิ่งเหล่านี้ ไม่กี่นาทีก่อนที่จะค้นพบการตัดการเชื่อมต่อ) ไม่ ตัวเลือก TCP_KEEPALIVE ซึ่งหลายคนที่ไม่เข้าใจว่าตัวจับเวลา TCP ทำงานอย่างไรโดยการสุ่ม (โดยการตั้งค่าไวด์เช่นสิบวินาที) จะไม่ช่วยที่นี่ - คุณต้องแน่ใจว่าไม่เพียง แต่เคอร์เนลระบบปฏิบัติการเท่านั้น ของเครื่องของผู้ใช้ยังมีชีวิตอยู่ แต่ยังทำงานได้ตามปกติในการตอบสนองและตัวแอปพลิเคชันเอง (คุณคิดว่ามันไม่สามารถหยุดได้หรือไม่ Telegram Desktop บน Ubuntu XNUMX ค้างสำหรับฉันมากกว่าหนึ่งครั้ง)

นั่นเป็นเหตุผลที่คุณต้องปิง เซิร์ฟเวอร์ ลูกค้า และไม่ใช่ในทางกลับกัน - หากลูกค้าทำเช่นนี้ หากการเชื่อมต่อขาด ping จะไม่ถูกส่ง เป้าหมายจะไม่สำเร็จ

เราเห็นอะไรบนโทรเลข มันตรงกันข้ามเลย! นั่นคือ แน่นอนว่าทั้งสองฝ่ายสามารถ ping กันได้อย่างเป็นทางการ ในทางปฏิบัติ ลูกค้าใช้ไม้ค้ำยัน ping_delay_disconnectซึ่งตั้งเวลาบนเซิร์ฟเวอร์ ขอโทษที มันไม่ได้ขึ้นอยู่กับลูกค้าที่จะตัดสินใจว่าเขาต้องการอยู่ที่นั่นนานแค่ไหนโดยไม่ใช้ Ping เซิร์ฟเวอร์ตามโหลดจะรู้ดีกว่า แต่แน่นอนว่า ถ้าคุณไม่คำนึงถึงทรัพยากร คุณจะกลายเป็นพินอคคิโอผู้ชั่วร้าย และไม้ค้ำยันจะทำ...

ควรได้รับการออกแบบอย่างไร?

ฉันเชื่อว่าข้อเท็จจริงข้างต้นแสดงให้เห็นอย่างชัดเจนว่าทีมงาน Telegram/VKontakte ไม่มีความสามารถมากนักในด้านเครือข่ายคอมพิวเตอร์ระดับการขนส่ง (และต่ำกว่า) และมีคุณสมบัติต่ำในเรื่องที่เกี่ยวข้อง

เหตุใดจึงกลายเป็นเรื่องซับซ้อน และสถาปนิก Telegram จะพยายามคัดค้านได้อย่างไร ข้อเท็จจริงที่ว่าพวกเขาพยายามสร้างเซสชันที่รอดจากการเชื่อมต่อ TCP ขัดข้อง เช่น สิ่งใดที่ไม่ได้จัดส่งในขณะนี้ เราจะจัดส่งในภายหลัง พวกเขาอาจพยายามสร้างการขนส่ง UDP ด้วย แต่ประสบปัญหาและละทิ้งมันไป (นั่นคือสาเหตุที่เอกสารว่างเปล่า - ไม่มีอะไรจะอวดได้) แต่เนื่องจากขาดความเข้าใจว่าเครือข่ายโดยทั่วไปและ TCP ทำงานอย่างไร คุณสามารถพึ่งพาได้ที่ไหน และคุณต้องทำเองที่ไหน (และอย่างไร) และความพยายามที่จะรวมสิ่งนี้เข้ากับการเข้ารหัส "นกสองตัวด้วย หินก้อนเดียว” นี่คือผล

มันจำเป็นแค่ไหน? โดยอาศัยข้อเท็จจริงที่ว่า msg_id เป็นการประทับเวลาที่จำเป็นจากมุมมองของการเข้ารหัสเพื่อป้องกันการโจมตีซ้ำ ถือเป็นความผิดพลาดที่จะแนบฟังก์ชันตัวระบุเฉพาะลงไป ดังนั้น หากไม่มีการเปลี่ยนแปลงสถาปัตยกรรมปัจจุบันโดยพื้นฐาน (เมื่อมีการสร้างสตรีมการอัปเดต นั่นคือหัวข้อ API ระดับสูงสำหรับส่วนอื่นของโพสต์ชุดนี้) เราจะต้อง:

  1. เซิร์ฟเวอร์ที่ถือการเชื่อมต่อ TCP กับไคลเอนต์จะรับผิดชอบ - หากได้อ่านจากซ็อกเก็ตแล้ว โปรดรับทราบ ประมวลผล หรือส่งคืนข้อผิดพลาด ไม่มีการสูญเสีย การยืนยันไม่ใช่เวกเตอร์ของ id แต่เป็นเพียง "seq_no ที่ได้รับล่าสุด" - เป็นเพียงตัวเลขเช่นเดียวกับใน TCP (ตัวเลขสองตัว - seq ของคุณและตัวเลขที่ยืนยัน) เราอยู่ในเซสชันเสมอใช่ไหม?
  2. การประทับเวลาเพื่อป้องกันการโจมตีแบบเล่นซ้ำจะกลายเป็นฟิลด์ที่แยกจากกัน มีการตรวจสอบแล้ว แต่ไม่มีผลกระทบต่อสิ่งอื่นใด พอแล้ว. uint32 - หากเกลือของเราเปลี่ยนแปลงอย่างน้อยทุกครึ่งวัน เราสามารถจัดสรร 16 บิตให้กับบิตลำดับต่ำของส่วนจำนวนเต็มของเวลาปัจจุบัน ส่วนที่เหลือ - ให้เป็นเศษส่วนของวินาที (ณ ตอนนี้)
  3. ลบออก msg_id เลย - จากมุมมองของการแยกคำขอบนแบ็กเอนด์ ประการแรกคือรหัสลูกค้า และประการที่สองคือรหัสเซสชันที่ต่อเข้าด้วยกัน ดังนั้น มีเพียงสิ่งเดียวเท่านั้นที่เพียงพอสำหรับเป็นตัวระบุคำขอ seq_no.

นี่ไม่ใช่ตัวเลือกที่ประสบความสำเร็จมากที่สุด การสุ่มแบบสมบูรณ์อาจทำหน้าที่เป็นตัวระบุได้ - ซึ่งได้ทำไปแล้วใน API ระดับสูงเมื่อส่งข้อความ จะดีกว่าถ้าสร้างสถาปัตยกรรมใหม่ทั้งหมดจากแบบสัมพัทธ์เป็นแบบสัมบูรณ์ แต่นี่เป็นหัวข้อสำหรับส่วนอื่น ไม่ใช่โพสต์นี้

เอพีไอ?

ทาดาม! ดังนั้น จากการที่ต้องดิ้นรนฝ่าเส้นทางที่เต็มไปด้วยความเจ็บปวดและไม้ค้ำ ในที่สุดเราก็สามารถส่งคำขอใด ๆ ไปยังเซิร์ฟเวอร์และรับคำตอบใด ๆ รวมถึงรับการอัปเดตจากเซิร์ฟเวอร์ (ไม่ใช่เพื่อตอบสนองต่อคำขอ แต่เป็นตัวมันเอง ส่งให้เราแบบ PUSH ถ้าใครชัดเจนกว่านี้)

โปรดทราบ ตอนนี้จะมีตัวอย่างเดียวในภาษา Perl ในบทความ! (สำหรับผู้ที่ไม่คุ้นเคยกับไวยากรณ์ อาร์กิวเมนต์แรกของ 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 - วัตถุ Perl ที่ทำให้เป็นอนุกรมพร้อมชื่อคลาสและอาร์กิวเมนต์ตัวสร้าง
  • 27 - อ็อบเจ็กต์ที่ไม่ขึ้นกับภาษาแบบอนุกรมพร้อมชื่อประเภทและอาร์กิวเมนต์ตัวสร้าง

ฉันพยายามทำให้ข้อมูลเดียวกันเป็นอนุกรมใน TL และ CBOR โดยเปิดใช้งานการบรรจุสตริงและอ็อบเจ็กต์ ผลลัพธ์เริ่มแตกต่างกันไปตาม CBOR จากเมกะไบต์:

cborlen=1039673 tl_len=1095092

ดังนั้น เอาท์พุต: มีรูปแบบที่เรียบง่ายกว่ามากซึ่งไม่มีปัญหาความล้มเหลวในการซิงโครไนซ์หรือตัวระบุที่ไม่รู้จัก โดยมีประสิทธิภาพที่เทียบเคียงได้

การสร้างการเชื่อมต่อที่รวดเร็ว. ซึ่งหมายความว่า RTT เป็นศูนย์หลังจากการเชื่อมต่อใหม่ (เมื่อคีย์ได้ถูกสร้างขึ้นแล้วครั้งหนึ่ง) - ใช้ได้กับข้อความ MTProto แรกสุด แต่มีข้อสงวนบางประการ - เข้าสู่สภาวะเดิม เซสชันจะไม่เน่าเสีย ฯลฯ TLS เสนออะไรให้เราแทน? อ้างในหัวข้อ:

เมื่อใช้ PFS ใน TLS, ตั๋วเซสชัน TLS (RFC 5077) เพื่อดำเนินการเซสชันที่เข้ารหัสต่อโดยไม่ต้องเจรจาคีย์ใหม่ และไม่ต้องจัดเก็บข้อมูลคีย์บนเซิร์ฟเวอร์ เมื่อเปิดการเชื่อมต่อครั้งแรกและสร้างคีย์ เซิร์ฟเวอร์จะเข้ารหัสสถานะการเชื่อมต่อและส่งไปยังไคลเอนต์ (ในรูปแบบของตั๋วเซสชัน) ดังนั้น เมื่อการเชื่อมต่อกลับมาทำงานอีกครั้ง ไคลเอนต์จะส่งตั๋วเซสชัน รวมถึงคีย์เซสชัน กลับไปยังเซิร์ฟเวอร์ ตั๋วนั้นถูกเข้ารหัสด้วยคีย์ชั่วคราว (คีย์ตั๋วเซสชัน) ซึ่งถูกเก็บไว้บนเซิร์ฟเวอร์และจะต้องกระจายไปยังเซิร์ฟเวอร์ส่วนหน้าทั้งหมดที่ประมวลผล SSL ในโซลูชันแบบคลัสเตอร์ ดังนั้นการแนะนำตั๋วเซสชันอาจละเมิด PFS หากคีย์เซิร์ฟเวอร์ชั่วคราวถูกบุกรุกเช่นเมื่อเก็บไว้เป็นเวลานาน (OpenSSL, nginx, Apache จัดเก็บไว้ตามค่าเริ่มต้นตลอดระยะเวลาของโปรแกรม ไซต์ยอดนิยมใช้ เป็นเวลาหลายชั่วโมง จนถึงหลายวัน)

ที่นี่ RTT ไม่ใช่ศูนย์ คุณต้องแลกเปลี่ยนอย่างน้อย ClientHello และ ServerHello หลังจากนั้นไคลเอนต์สามารถส่งข้อมูลพร้อมกับ Finished ได้ แต่ที่นี่เราควรจำไว้ว่าเราไม่มีเว็บซึ่งมีการเชื่อมต่อที่เพิ่งเปิดใหม่มากมาย แต่เป็นผู้ส่งสาร ซึ่งการเชื่อมต่อนั้นมักจะเป็นคำขอเดียวที่มีอายุการใช้งานยาวนานและค่อนข้างสั้นไปยังเว็บเพจ - ทุกอย่างเป็นแบบมัลติเพล็กซ์ ภายใน กล่าวคือ ค่อนข้างยอมรับได้หากเราไม่ได้เจอสถานีรถไฟใต้ดินที่แย่จริงๆ

ลืมอย่างอื่นหรือเปล่า? เขียนในความคิดเห็น

จะดำเนินต่อไป!

ในส่วนที่สองของโพสต์ชุดนี้ เราจะพิจารณาไม่ใช่เรื่องทางเทคนิค แต่เป็นประเด็นเกี่ยวกับองค์กร เช่น แนวทาง อุดมการณ์ ส่วนต่อประสาน ทัศนคติต่อผู้ใช้ ฯลฯ อย่างไรก็ตามจากข้อมูลทางเทคนิคที่นำเสนอนี้

ส่วนที่สามจะวิเคราะห์องค์ประกอบทางเทคนิค/ประสบการณ์การพัฒนาต่อไป คุณจะได้เรียนรู้โดยเฉพาะ:

  • ความโกลาหลอย่างต่อเนื่องด้วย TL ประเภทต่างๆ
  • สิ่งที่ไม่รู้เกี่ยวกับช่องและซูเปอร์กรุ๊ป
  • เหตุใดบทสนทนาจึงแย่กว่าบัญชีรายชื่อ
  • เกี่ยวกับการกำหนดที่อยู่ข้อความแบบสัมบูรณ์เทียบกับแบบสัมพันธ์
  • ความแตกต่างระหว่างภาพถ่ายและรูปภาพคืออะไร
  • อิโมจิรบกวนข้อความตัวเอียงอย่างไร

และไม้ค้ำอื่นๆ! คอยติดตาม!

ที่มา: will.com

เพิ่มความคิดเห็น