เมื่อเร็ว ๆ นี้โพสต์เกี่ยวกับความเก่งกาจของ Telegram พี่น้อง Durov ที่เก่งและมีประสบการณ์ในการสร้างระบบเครือข่าย ฯลฯ เริ่มปรากฏให้เห็นบ่อยขึ้นในHabré ในเวลาเดียวกัน มีเพียงไม่กี่คนที่หมกมุ่นอยู่กับอุปกรณ์ทางเทคนิคจริงๆ โดยส่วนใหญ่แล้วพวกเขาใช้ Bot API ที่ค่อนข้างเรียบง่าย (และค่อนข้างแตกต่างจาก MTProto) ที่ใช้ JSON และมักจะยอมรับ เกี่ยวกับศรัทธา คำสรรเสริญและการประชาสัมพันธ์ทั้งหมดที่หมุนรอบผู้ส่งสาร เกือบหนึ่งปีครึ่งที่แล้ว เพื่อนร่วมงานของฉันที่ Eshelon NGO Vasily (น่าเสียดายที่บัญชีของเขาในHabréถูกลบไปพร้อมกับร่าง) เริ่มเขียนไคลเอนต์ Telegram ของเขาเองตั้งแต่เริ่มต้นในภาษา Perl และต่อมาผู้เขียนบรรทัดเหล่านี้ก็เข้าร่วม ทำไมต้อง Perl บางคนจะถามทันที? เพราะโครงการดังกล่าวมีในภาษาอื่นแล้ว อันที่จริง นี่ไม่ใช่ประเด็น อาจมีภาษาอื่น ๆ ที่ไม่มีอยู่จริงก็ได้ ห้องสมุดสำเร็จรูปและด้วยเหตุนี้ผู้เขียนจึงต้องไปตลอดทาง ตั้งแต่เริ่มต้น. ยิ่งกว่านั้นการเข้ารหัสเป็นเรื่องของความไว้วางใจ แต่ต้องตรวจสอบ ด้วยผลิตภัณฑ์ที่มุ่งเป้าไปที่ความปลอดภัย คุณไม่สามารถพึ่งพาไลบรารี่สำเร็จรูปจากผู้ผลิตและไว้วางใจไลบรารี่นั้นอย่างสุ่มสี่สุ่มห้าได้ (อย่างไรก็ตาม นี่เป็นหัวข้อสำหรับส่วนที่สอง) ในขณะนี้ ไลบรารีทำงานได้ค่อนข้างดีในระดับ "ปานกลาง" (ช่วยให้คุณสามารถส่งคำขอ API ใดก็ได้)
อย่างไรก็ตาม โพสต์ชุดนี้จะไม่มีการเข้ารหัสหรือคณิตศาสตร์มากนัก แต่จะมีรายละเอียดทางเทคนิคและไม้ค้ำยันทางสถาปัตยกรรมอื่น ๆ อีกมากมาย (ยังมีประโยชน์สำหรับผู้ที่จะไม่เขียนตั้งแต่ต้น แต่จะใช้ห้องสมุดในภาษาใดก็ได้) ดังนั้นเป้าหมายหลักคือการพยายามนำลูกค้าไปใช้ตั้งแต่เริ่มต้น ตามเอกสารอย่างเป็นทางการ. นั่นคือ สมมติว่าซอร์สโค้ดของลูกค้าอย่างเป็นทางการถูกปิด (อีกครั้งในส่วนที่สองเราจะกล่าวถึงรายละเอียดเพิ่มเติมเกี่ยวกับหัวข้อข้อเท็จจริงที่ว่าสิ่งนี้เป็นจริง ที่เกิดขึ้น ดังนั้น) แต่ในสมัยก่อนมีมาตรฐานเช่น RFC - เป็นไปได้ไหมที่จะเขียนไคลเอนต์ตามข้อกำหนดเพียงอย่างเดียว "โดยไม่ต้องดู" ที่ซอร์สโค้ดไม่ว่าจะเป็นทางการ (Telegram Desktop, มือถือ) หรือ Telethon อย่างไม่เป็นทางการ?
คำศัพท์:
เอกสาร...มีอยู่จริงใช่ไหม? จริงป้ะ?.. จะเริ่มในฐานะนักเขียนรุ่นเยาว์ได้ที่ไหน? การทำให้เป็นอนุกรมแบบไบนารี: TL (ภาษาประเภท) และรูปแบบของมัน และเลเยอร์ และคำที่น่ากลัวอื่นๆ อีกมากมาย MT โปรโต เอพีไอ? จะดำเนินต่อไป!
เอกสาร...มีอยู่จริงใช่ไหม? จริงป้ะ?..
ชิ้นส่วนของบันทึกย่อสำหรับบทความนี้เริ่มรวบรวมเมื่อฤดูร้อนที่แล้ว ตลอดเวลานี้บนเว็บไซต์อย่างเป็นทางการ
จะเริ่มในฐานะนักเขียนรุ่นเยาว์ได้ที่ไหน?
ไม่สำคัญว่าคุณจะเขียนตั้งแต่เริ่มต้นหรือใช้งาน เช่น ไลบรารีสำเร็จรูป เป็นต้น api_id
и api_hash
(ผู้ที่ทำงานกับ VKontakte API จะเข้าใจทันที) ซึ่งเซิร์ฟเวอร์จะระบุแอปพลิเคชัน นี้ ต้อง ทำด้วยเหตุผลทางกฎหมาย แต่เราจะพูดถึงสาเหตุที่ผู้เขียนห้องสมุดไม่สามารถเผยแพร่ได้ในส่วนที่สอง คุณอาจพอใจกับค่าทดสอบแม้ว่าจะมีจำกัดมาก แต่ความจริงก็คือ ตอนนี้คุณสามารถลงทะเบียนได้แล้ว เพียงหนึ่งเดียว แอพดังนั้นอย่ารีบเร่งเข้าไป
จากมุมมองทางเทคนิค เราควรสนใจความจริงที่ว่าหลังจากการลงทะเบียนแล้ว เราควรได้รับการแจ้งเตือนจาก Telegram เกี่ยวกับการอัปเดตเอกสาร โปรโตคอล ฯลฯ นั่นคือใคร ๆ ก็สามารถสันนิษฐานได้ว่าไซต์ที่มีท่าเทียบเรือนั้นถูกทิ้งร้างและยังคงทำงานต่อไปโดยเฉพาะกับผู้ที่เริ่มสร้างลูกค้าเพราะ มันง่ายกว่า แต่ไม่เลย ไม่มีอะไรแบบนั้นเกิดขึ้น ไม่มีข้อมูลมา
และถ้าคุณเขียนตั้งแต่ต้น การใช้พารามิเตอร์ที่ได้รับก็ยังห่างไกลออกไป แม้ว่า
ในความเป็นจริง ทั้งก่อนและหลัง MTProto ในหลายระดับพร้อมกัน (ดังที่เครือข่ายต่างประเทศที่ทำงานในเคอร์เนลระบบปฏิบัติการพูดว่า การละเมิดเลเยอร์) หัวข้อใหญ่ เจ็บปวด และแย่มากจะเข้ามาขวางทาง...
การทำให้เป็นอนุกรมแบบไบนารี: TL (ภาษาประเภท) และรูปแบบของมัน และเลเยอร์ และคำที่น่ากลัวอื่นๆ อีกมากมาย
ที่จริงแล้วหัวข้อนี้เป็นกุญแจสำคัญในปัญหาของ Telegram และจะมีคำพูดแย่ ๆ มากมายหากคุณพยายามเจาะลึกลงไป
นี่คือแผนภาพ ถ้าคำนี้เข้ามาในใจคุณ ให้พูดว่า
int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;
vector#1cb5c415 {t:Type} # [ t ] = Vector t;
rpc_error#2144ca19 error_code:int error_message:string = RpcError;
rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;
msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;
---functions---
set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer;
ping#7abe77ec ping_id:long = Pong;
ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;
invokeAfterMsg#cb9f372d msg_id:long query:!X = X;
invokeAfterMsgs#3dc4b4f0 msg_ids:Vector<long> query:!X = X;
account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;
account.sendChangePhoneCode#8e57deb flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode;
คนที่เห็นสิ่งนี้เป็นครั้งแรกจะสามารถรับรู้เพียงส่วนหนึ่งของสิ่งที่เขียนโดยสัญชาตญาณ - เห็นได้ชัดว่าสิ่งเหล่านี้เป็นโครงสร้าง (แม้ว่าชื่อจะอยู่ที่ไหนทางด้านซ้ายหรือทางขวาก็ตาม) มีฟิลด์อยู่ในนั้น หลังจากนั้นประเภทจะตามหลังเครื่องหมายทวิภาค... อาจจะ ในวงเล็บเหลี่ยมนี้ อาจมีเทมเพลตเหมือนใน C++ (อันที่จริง ไม่ได้จริงๆ). และสัญลักษณ์อื่นๆ ทั้งหมดหมายถึงอะไร เครื่องหมายคำถาม เครื่องหมายอัศเจรีย์ เปอร์เซ็นต์ เครื่องหมายแฮช (และเห็นได้ชัดว่าพวกมันหมายถึงสิ่งที่แตกต่างกันในที่ที่แตกต่างกัน) บางครั้งก็ปรากฏและบางครั้งก็ไม่มี เลขฐานสิบหก - และที่สำคัญที่สุดคือ ทำอย่างไรจึงจะได้จากสิ่งนี้ ขวา (ซึ่งจะไม่ถูกปฏิเสธโดยเซิร์ฟเวอร์) สตรีมไบต์? คุณจะต้องอ่านเอกสาร (ใช่ มีลิงก์ไปยังสคีมาในเวอร์ชัน JSON ใกล้เคียง - แต่นั่นไม่ได้ทำให้ชัดเจนขึ้น).
เปิดหน้า
ผู้อ่านที่คุ้นเคยกับภาษาที่ใช้งานได้และการอนุมานประเภทอัตโนมัติจะเห็นภาษาคำอธิบายในภาษานี้แม้จะจากตัวอย่างที่คุ้นเคยมากกว่าก็ตามและสามารถพูดได้ว่านี่เป็นหลักการที่ไม่เลวเลย ข้อโต้แย้งคือ:
- ใช่ เป้าหมาย ฟังดูดี แต่อนิจจาเธอ ไม่ประสบความสำเร็จ
- การศึกษาในมหาวิทยาลัยในรัสเซียนั้นแตกต่างกันไปแม้แต่ในสาขาไอทีเฉพาะทาง - ไม่ใช่ทุกคนที่เรียนหลักสูตรที่เกี่ยวข้อง
- สุดท้ายนี้ ดังที่เราจะได้เห็นในทางปฏิบัติ ไม่จำเป็นเนื่องจากมีการใช้เพียงเซตย่อยที่จำกัดแม้แต่ TL ที่อธิบายไว้เท่านั้นที่ถูกนำมาใช้
ดังที่กล่าว #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 จะเริ่มต้นขึ้นด้วยซ้ำ และในโอเพ่นซอร์ส
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 ใช่ไหม..
โดยผู้เขียน
16.12 04:18 Vasily: ฉันคิดว่ามีคนไม่เข้าใจ lex+yacc
16.12 04:18 วาซิลี: ฉันอธิบายเป็นอย่างอื่นไม่ได้
16.12 04:18 Vasily: หรือพวกเขาได้รับค่าตอบแทนตามจำนวนบรรทัดใน VK
16.12 04:19 วาซิลี่: 3k+ ไลน์ เป็นต้น<censored>
แทนที่จะเป็นตัวแยกวิเคราะห์
อาจเป็นข้อยกเว้น? มาดูวิธีการกัน
nametype = re.match(r'([a-zA-Z.0-9_]+)(#[0-9a-f]+)?([^=]*)=s*([a-zA-Z.<>0-9_]+);', line);
if (not nametype):
if (not re.match(r'vector#1cb5c415 {t:Type} # [ t ] = Vector t;', line)):
print('Bad line found: ' + line);
มากกว่า 1100 บรรทัดใน 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...
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 ทั่วไป ข้อความทุกอย่างชัดเจนสามารถตีความได้ในแง่ OOP หรือเรียกง่ายๆว่าคำว่า "แพ็กเก็ต" - นี่เป็นระดับการขนส่งต่ำไม่มีข้อความเหมือนกับในอินเทอร์เฟซมีข้อความบริการมากมาย . แต่เซสชัน... แต่สิ่งแรกสุดก่อน
ชั้นขนส่ง
สิ่งแรกคือการขนส่ง พวกเขาจะบอกเราเกี่ยวกับ 5 ตัวเลือก:
- TCP
- เว็บซ็อคเก็ต
- เว็บซ็อกเก็ตผ่าน HTTPS
- HTTP
- HTTPS
Vasily, [15.06.18 15:04] นอกจากนี้ยังมีการขนส่ง UDP ด้วย แต่ไม่มีเอกสาร
และ TCP ในสามรูปแบบ
อันแรกคล้ายกับ UDP บน TCP แต่ละแพ็กเก็ตจะมีหมายเลขลำดับและ crc
ทำไมการอ่านเอกสารบนรถเข็นถึงเจ็บปวดขนาดนี้?
ตอนนี้ก็มีแล้ว
- สรุป
- 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 กัน (ฉันขอโทษที่แปลเอกสารของฉันเองจากภาษาอังกฤษด้วยสมองที่เหนื่อยล้าตอนตี 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
บน
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;
โอ้มีคนจะอุทานว่านี่คือรูปแบบที่มีมนุษยธรรมมากขึ้น - มีเส้น! ใช้เวลาของคุณ ที่นี่ 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 จะต้องจัดการกับพวกเขา ในรอบ "แก้ไข - คอมไพล์ใหม่ - เปิดตัว" การได้รับข้อผิดพลาดด้านตัวเลขหรือเกลือที่จัดการได้ไม่ดีระหว่างการแก้ไขเป็นเรื่องปกติ อย่างไรก็ตาม มีสองประเด็นที่นี่:
- ซึ่งหมายความว่าข้อความต้นฉบับจะหายไป เราจำเป็นต้องสร้างคิวขึ้นมา เราจะมาดูกันทีหลัง
- หมายเลขข้อผิดพลาดแปลก ๆ เหล่านี้คืออะไร? 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
ซึ่งเป็นเรื่องชั่วคราวаฉันเป็นค่ายเพลง ซึ่งการมอบหมายงานควรจะเลื่อนออกไปให้ช้าที่สุดเท่าที่จะทำได้ - ในกรณีที่เซิร์ฟเวอร์ปฏิเสธเนื่องจากเวลาระหว่างเรากับเขาไม่ตรงกัน (แน่นอน เราสามารถสร้างไม้ค้ำยันที่เปลี่ยนเวลาของเราจากปัจจุบันได้ ไปยังเซิร์ฟเวอร์โดยการเพิ่มเดลต้าที่คำนวณจากการตอบสนองของเซิร์ฟเวอร์ - ลูกค้าอย่างเป็นทางการทำเช่นนี้ แต่มันหยาบและไม่ถูกต้องเนื่องจากการบัฟเฟอร์) ดังนั้น เมื่อคุณทำการร้องขอด้วยการเรียกใช้ฟังก์ชันภายในเครื่องจากไลบรารี ข้อความจะผ่านขั้นตอนต่อไปนี้:
- มันอยู่ในคิวเดียวและรอการเข้ารหัส
- ได้รับการแต่งตั้ง
msg_id
และข้อความก็ไปที่คิวอื่น - การส่งต่อที่เป็นไปได้ ส่งไปที่ซ็อกเก็ต - ก) เซิร์ฟเวอร์ตอบกลับ MsgsAck - ข้อความถูกส่งแล้ว เราจะลบออกจาก "คิวอื่น"
b) หรือในทางกลับกัน เขาไม่ชอบอะไรบางอย่าง เขาตอบ badmsg - ส่งอีกครั้งจาก "คิวอื่น"
c) ไม่ทราบสิ่งใด ข้อความจะต้องถูกส่งอีกครั้งจากคิวอื่น - แต่ไม่ทราบแน่ชัดว่าเมื่อใด - ในที่สุดเซิร์ฟเวอร์ก็ตอบกลับ
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 ระดับสูงสำหรับส่วนอื่นของโพสต์ชุดนี้) เราจะต้อง:
- เซิร์ฟเวอร์ที่ถือการเชื่อมต่อ TCP กับไคลเอนต์จะรับผิดชอบ - หากได้อ่านจากซ็อกเก็ตแล้ว โปรดรับทราบ ประมวลผล หรือส่งคืนข้อผิดพลาด ไม่มีการสูญเสีย การยืนยันไม่ใช่เวกเตอร์ของ id แต่เป็นเพียง "seq_no ที่ได้รับล่าสุด" - เป็นเพียงตัวเลขเช่นเดียวกับใน TCP (ตัวเลขสองตัว - seq ของคุณและตัวเลขที่ยืนยัน) เราอยู่ในเซสชันเสมอใช่ไหม?
- การประทับเวลาเพื่อป้องกันการโจมตีแบบเล่นซ้ำจะกลายเป็นฟิลด์ที่แยกจากกัน มีการตรวจสอบแล้ว แต่ไม่มีผลกระทบต่อสิ่งอื่นใด พอแล้ว.
uint32
- หากเกลือของเราเปลี่ยนแปลงอย่างน้อยทุกครึ่งวัน เราสามารถจัดสรร 16 บิตให้กับบิตลำดับต่ำของส่วนจำนวนเต็มของเวลาปัจจุบัน ส่วนที่เหลือ - ให้เป็นเศษส่วนของวินาที (ณ ตอนนี้) - ลบออก
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 - อย่างไรก็ตามเป็นมาตรฐานที่อธิบายไว้ใน
- 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