Chỉ trích về giao thức và cách tiếp cận tổ chức của Telegram. Phần 1, kỹ thuật: kinh nghiệm viết client từ đầu - TL, MT

Gần đây, những bài viết về việc Telegram tốt như thế nào, anh em nhà Durov xuất sắc và giàu kinh nghiệm như thế nào trong việc xây dựng hệ thống mạng,… đã bắt đầu xuất hiện thường xuyên hơn trên Habré. Đồng thời, rất ít người thực sự đắm mình vào thiết bị kỹ thuật - nhiều nhất là họ sử dụng Bot API khá đơn giản (và khá khác biệt so với MTProto) dựa trên JSON và thường chỉ chấp nhận về đức tin tất cả những lời khen ngợi và PR xoay quanh người đưa tin. Gần một năm rưỡi trước, đồng nghiệp của tôi tại tổ chức phi chính phủ Eshelon Vasily (thật không may, tài khoản của anh ấy trên Habré đã bị xóa cùng với bản nháp) bắt đầu viết ứng dụng khách Telegram của riêng mình từ đầu bằng Perl, và sau đó là tác giả của những dòng này đã tham gia. Tại sao lại là Perl, một số người sẽ hỏi ngay? Bởi vì những dự án như vậy đã tồn tại ở các ngôn ngữ khác. Trên thực tế, đây không phải là vấn đề, có thể có bất kỳ ngôn ngữ nào khác không có thư viện làm sẵn, và theo đó tác giả phải cố gắng hết sức từ đầu. Hơn nữa, mật mã là vấn đề của sự tin cậy nhưng phải được xác minh. Với một sản phẩm hướng đến bảo mật, bạn không thể chỉ dựa vào thư viện làm sẵn từ nhà sản xuất và tin tưởng nó một cách mù quáng (tuy nhiên, đây là chủ đề của phần thứ hai). Hiện tại, thư viện hoạt động khá tốt ở mức “trung bình” (cho phép bạn thực hiện bất kỳ yêu cầu API nào).

Tuy nhiên, sẽ không có nhiều mật mã hoặc toán học trong loạt bài viết này. Nhưng sẽ có nhiều chi tiết kỹ thuật và nạng kiến ​​​​trúc khác (cũng hữu ích cho những người không viết từ đầu mà sẽ sử dụng thư viện bằng bất kỳ ngôn ngữ nào). Vì vậy, mục tiêu chính là cố gắng triển khai ứng dụng khách từ đầu theo tài liệu chính thức. Nghĩa là, hãy giả sử rằng mã nguồn của khách hàng chính thức đã bị đóng (một lần nữa, trong phần thứ hai, chúng tôi sẽ đề cập chi tiết hơn về chủ đề thực tế là điều này đúng xảy ra vậy), nhưng, như ngày xưa, chẳng hạn, có một tiêu chuẩn như RFC - liệu có thể viết một ứng dụng khách chỉ theo đặc tả mà “không cần nhìn” vào mã nguồn, dù là mã chính thức (Telegram Desktop, di động) hay Telethon không chính thức?

Mục lục:

Tài liệu... nó tồn tại, phải không? Có thật không?..

Những mảnh ghi chú cho bài viết này đã bắt đầu được thu thập vào mùa hè năm ngoái. Tất cả thời gian này trên trang web chính thức https://core.telegram.org Tài liệu này thuộc Lớp 23, tức là. bị mắc kẹt ở đâu đó vào năm 2014 (bạn có nhớ rằng hồi đó thậm chí còn không có kênh không?). Tất nhiên, về mặt lý thuyết, điều này đáng lẽ phải cho phép chúng tôi triển khai một ứng dụng khách có chức năng vào thời điểm đó trong năm 2014. Nhưng ngay cả ở trạng thái này, thứ nhất, tài liệu vẫn chưa đầy đủ và thứ hai là nó mâu thuẫn với chính nó ở một số chỗ. Chỉ hơn một tháng trước, vào tháng 2019 năm XNUMX, tình cờ Người ta phát hiện ra rằng có một bản cập nhật lớn về tài liệu trên trang web, dành cho Lớp 105 khá gần đây, với lưu ý rằng bây giờ mọi thứ cần phải được đọc lại. Quả thực, nhiều bài viết đã được sửa đổi, nhưng nhiều bài vẫn không thay đổi. Do đó, khi đọc những lời chỉ trích bên dưới về tài liệu, bạn nên nhớ rằng một số thứ trong số này không còn phù hợp nữa, nhưng một số vẫn còn phù hợp. Suy cho cùng, 5 năm ở thế giới hiện đại không chỉ là một khoảng thời gian dài mà còn rất rất nhiều. Kể từ thời điểm đó (đặc biệt nếu bạn không tính đến các trang web trò chuyện địa lý bị loại bỏ và hồi sinh kể từ đó), số lượng phương thức API trong sơ đồ đã tăng từ một trăm lên hơn hai trăm năm mươi!

Bắt đầu từ đâu với tư cách là một tác giả trẻ?

Chẳng hạn, việc bạn viết từ đầu hay sử dụng các thư viện làm sẵn như Telethon cho Python hoặc Madeline cho PHP, trong mọi trường hợp, trước tiên bạn sẽ cần đăng ký ứng dụng của bạn - lấy thông số api_id и api_hash (những người đã làm việc với API VKontakte sẽ hiểu ngay) nhờ đó máy chủ sẽ nhận dạng ứng dụng. Cái này phải làm điều đó vì lý do pháp lý, nhưng chúng ta sẽ nói nhiều hơn về lý do tại sao các tác giả thư viện không thể xuất bản nó trong phần thứ hai. Bạn có thể hài lòng với các giá trị thử nghiệm, mặc dù chúng rất hạn chế - thực tế là bây giờ bạn có thể đăng ký chỉ một ứng dụng, vì vậy đừng vội lao vào nó.

Bây giờ, từ quan điểm kỹ thuật, chúng ta nên quan tâm đến thực tế là sau khi đăng ký, chúng ta sẽ nhận được thông báo từ Telegram về các bản cập nhật tài liệu, giao thức, v.v. Đó là, người ta có thể cho rằng địa điểm có bến cảng chỉ đơn giản là bị bỏ hoang và tiếp tục làm việc cụ thể với những người bắt đầu kiếm khách hàng, bởi vì nó dễ dàng hơn. Nhưng không, không có gì như vậy được quan sát, không có thông tin nào được đưa ra.

Và nếu bạn viết từ đầu, thì việc sử dụng các tham số thu được thực sự vẫn còn một chặng đường dài. Mặc dù https://core.telegram.org/ và nói về chúng trong phần Bắt đầu trước hết, trên thực tế, trước tiên bạn sẽ phải triển khai Giao thức MTProto - nhưng nếu bạn tin bố trí theo mô hình OSI ở cuối trang có mô tả chung về giao thức, thì điều đó hoàn toàn vô ích.

Trên thực tế, cả trước và sau MTProto, ở nhiều cấp độ cùng một lúc (như các nhà mạng nước ngoài làm việc trong nhân hệ điều hành nói, vi phạm lớp), một chủ đề lớn, nhức nhối và khủng khiếp sẽ cản trở...

Tuần tự hóa nhị phân: TL (Ngôn ngữ loại) và lược đồ cũng như các lớp của nó cũng như nhiều từ đáng sợ khác

Trên thực tế, chủ đề này chính là chìa khóa giải quyết các vấn đề của Telegram. Và sẽ có rất nhiều từ ngữ khủng khiếp nếu bạn cố gắng đào sâu vào nó.

Vì vậy, đây là sơ đồ. Nếu từ này xuất hiện trong đầu bạn, hãy nói, Lược đồ JSON, Bạn đã nghĩ đúng. Mục tiêu là như nhau: một số ngôn ngữ để mô tả một tập hợp dữ liệu được truyền có thể. Đây là nơi mà sự tương đồng kết thúc. Nếu từ trang Giao thức MTProto, hoặc từ cây nguồn của máy khách chính thức, chúng ta sẽ thử mở một số lược đồ, chúng ta sẽ thấy một cái gì đó như:

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;

Một người nhìn thấy điều này lần đầu tiên bằng trực giác sẽ chỉ có thể nhận ra một phần của những gì được viết - à, đây là những cấu trúc rõ ràng (mặc dù tên ở đâu, ở bên trái hay bên phải?), có các trường trong đó, sau đó một loại theo sau dấu hai chấm... có lẽ vậy. Ở đây trong dấu ngoặc nhọn có thể có các mẫu giống như trong C++ (trên thực tế, không hẳn). Và tất cả các ký hiệu khác có ý nghĩa gì, dấu hỏi, dấu chấm than, tỷ lệ phần trăm, dấu thăng (và rõ ràng chúng có nghĩa là những thứ khác nhau ở những vị trí khác nhau), đôi khi hiện diện và đôi khi không, số thập lục phân - và quan trọng nhất, làm thế nào để hiểu được điều này thường xuyên (sẽ không bị máy chủ từ chối) luồng byte? Bạn sẽ phải đọc tài liệu (vâng, có các liên kết đến lược đồ trong phiên bản JSON gần đó - nhưng điều đó không làm cho nó rõ ràng hơn chút nào).

Mở trang Tuần tự hóa dữ liệu nhị phân và đi sâu vào thế giới kỳ diệu của nấm và toán học rời rạc, tương tự như matan ở năm thứ 4. Bảng chữ cái, kiểu, giá trị, tổ hợp, tổ hợp chức năng, dạng thông thường, kiểu tổng hợp, kiểu đa hình... và đó chỉ là trang đầu tiên! Tiếp theo đang chờ bạn Ngôn ngữ TL, mặc dù nó đã chứa một ví dụ về một yêu cầu và phản hồi tầm thường, nhưng không cung cấp câu trả lời nào cho các trường hợp điển hình hơn, điều đó có nghĩa là bạn sẽ phải lội qua phần kể lại toán học được dịch từ tiếng Nga sang tiếng Anh trên tám phần được nhúng khác trang!

Tất nhiên, những độc giả quen thuộc với các ngôn ngữ chức năng và suy luận kiểu tự động sẽ thấy ngôn ngữ mô tả trong ngôn ngữ này, thậm chí từ ví dụ, quen thuộc hơn nhiều và có thể nói rằng về nguyên tắc thì điều này thực sự không tệ. Những phản đối về điều này là:

  • Vâng, Mục tiêu nghe có vẻ hay đấy, nhưng than ôi, cô ấy không đạt
  • Giáo dục tại các trường đại học Nga khác nhau ngay cả giữa các chuyên ngành CNTT - không phải ai cũng tham gia khóa học tương ứng
  • Cuối cùng, như chúng ta sẽ thấy, trong thực tế, đó là không yêu cầu, vì chỉ một tập hợp con giới hạn của TL được mô tả mới được sử dụng

Như đã nói Sư TửMọt Sách trên kênh #perl trong mạng IRC FreeNode, người đã cố gắng triển khai một cổng từ Telegram sang Matrix (bản dịch trích dẫn không chính xác từ bộ nhớ):

Có cảm giác như ai đó lần đầu tiên được làm quen với lý thuyết loại, trở nên hào hứng và bắt đầu thử chơi với nó mà không thực sự quan tâm liệu nó có cần thiết trong thực tế hay không.

Hãy tự mình xem, nếu nhu cầu về các loại trần (int, long, v.v.) như một thứ gì đó cơ bản không đặt ra câu hỏi - cuối cùng chúng phải được triển khai theo cách thủ công - ví dụ: chúng ta hãy thử rút ra từ chúng vectơ. Đó là, trên thực tế, mảng, nếu bạn gọi những thứ thu được bằng tên riêng của chúng.

Nhưng trước đó

Mô tả ngắn gọn về tập hợp con cú pháp TL dành cho những người không đọc tài liệu chính thức

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;

Định nghĩa luôn bắt đầu nhà xây dựng, sau đó tùy chọn (trong thực tế - luôn luôn) thông qua ký hiệu # nên CRC32 từ chuỗi mô tả chuẩn hóa của loại này. Tiếp theo là mô tả về các trường; nếu chúng tồn tại thì loại có thể trống. Tất cả điều này kết thúc bằng một dấu bằng, tên của kiểu mà hàm tạo này - trên thực tế, là kiểu con - thuộc về. Người đứng bên phải dấu bằng là đa hình - nghĩa là, một số loại cụ thể có thể tương ứng với nó.

Nếu định nghĩa xảy ra sau dòng ---functions---, thì cú pháp sẽ giữ nguyên, nhưng ý nghĩa sẽ khác: hàm tạo sẽ trở thành tên của hàm RPC, các trường sẽ trở thành tham số (nghĩa là nó sẽ giữ nguyên cấu trúc đã cho, như được mô tả bên dưới , đây đơn giản sẽ là ý nghĩa được gán) và “loại đa hình " - loại kết quả trả về. Đúng, nó vẫn sẽ có tính đa hình - chỉ được xác định trong phần ---types---, nhưng hàm tạo này sẽ “không được xem xét”. Quá tải các loại hàm được gọi bằng đối số của chúng, tức là. Vì lý do nào đó, một số hàm có cùng tên nhưng có chữ ký khác nhau, như trong C++, không được cung cấp trong TL.

Tại sao lại là "hàm tạo" và "đa hình" nếu nó không phải là OOP? Chà, trên thực tế, ai đó sẽ dễ dàng nghĩ về điều này theo thuật ngữ OOP hơn - một kiểu đa hình như một lớp trừu tượng và các hàm tạo là các lớp con cháu trực tiếp của nó, và final trong thuật ngữ của một số ngôn ngữ. Tất nhiên, trên thực tế, ở đây chỉ sự giống nhau với các phương thức xây dựng quá tải thực sự trong các ngôn ngữ lập trình OO. Vì đây chỉ là cấu trúc dữ liệu nên không có phương thức (mặc dù việc mô tả thêm về hàm và phương thức hoàn toàn có khả năng tạo ra sự nhầm lẫn trong đầu rằng chúng tồn tại, nhưng đó lại là một vấn đề khác) - bạn có thể coi hàm tạo là một giá trị từ cái mà đang được xây dựng gõ khi đọc luồng byte.

Làm thế nào điều này xảy ra? Trình giải tuần tự, luôn đọc 4 byte, sẽ thấy giá trị 0xcrc32 - và hiểu điều gì sẽ xảy ra tiếp theo field1 với loại int, I E. đọc chính xác 4 byte, trên trường này có loại nằm trên PolymorType đọc. Nhìn 0x2crc32 và hiểu rằng còn có hai lĩnh vực nữa, thứ nhất long, có nghĩa là chúng tôi đọc 8 byte. Và sau đó lại là một loại phức tạp, được giải tuần tự hóa theo cách tương tự. Ví dụ, Type3 có thể được khai báo trong mạch ngay khi có hai hàm tạo tương ứng, khi đó chúng phải đáp ứng một trong hai 0x12abcd34, sau đó bạn cần đọc thêm 4 byte intHoặc 0x6789cdef, sau đó sẽ không có gì cả. Bất cứ điều gì khác - bạn cần ném một ngoại lệ. Dù sao thì sau này chúng ta quay lại đọc 4 byte int cánh đồng field_c в constructorTwo và cùng với đó chúng ta đọc xong PolymorType.

Cuối cùng, nếu bạn bị bắt 0xdeadcrc cho constructorThree, thì mọi thứ trở nên phức tạp hơn. Lĩnh vực đầu tiên của chúng tôi là bit_flags_of_what_really_present với loại # - thực ra đây chỉ là bí danh của loại nat, có nghĩa là "số tự nhiên". Trên thực tế, unsigned int là trường hợp duy nhất khi các số không dấu xuất hiện trong các mạch thực. Vì vậy, tiếp theo là cấu trúc có dấu chấm hỏi, nghĩa là trường này - nó sẽ chỉ xuất hiện trên dây nếu bit tương ứng được đặt trong trường được tham chiếu (gần giống như toán tử ternary). Vì vậy, hãy giả sử rằng bit này đã được đặt, điều đó có nghĩa là chúng ta cần đọc thêm một trường như Type, trong ví dụ của chúng tôi có 2 hàm tạo. Một cái trống (chỉ bao gồm mã định danh), cái còn lại có một trường ids với loại ids:Vector<long>.

Bạn có thể nghĩ rằng cả mẫu và mẫu chung đều là ưu điểm hoặc Java. Nhưng không. Hầu hết. Cái này đơn trường hợp sử dụng dấu ngoặc nhọn trong mạch thực và CHỈ được sử dụng cho Vector. Trong luồng byte, đây sẽ là 4 CRC32 byte cho chính loại Vector, luôn giống nhau, sau đó là 4 byte - số phần tử mảng và sau đó là chính các phần tử này.

Thêm vào đó, thực tế là việc tuần tự hóa luôn xảy ra ở các từ có 4 byte, tất cả các loại đều là bội số của nó - các loại tích hợp cũng được mô tả bytes и string với việc xê-ri hóa độ dài theo cách thủ công và căn chỉnh theo 4 - à, nghe có vẻ bình thường và thậm chí tương đối hiệu quả? Mặc dù TL được khẳng định là một hệ thống tuần tự hóa nhị phân hiệu quả, nhưng với việc mở rộng gần như bất kỳ thứ gì, ngay cả các giá trị Boolean và chuỗi ký tự đơn thành 4 byte, liệu JSON có còn dày hơn nhiều không? Hãy nhìn xem, ngay cả những trường không cần thiết cũng có thể được bỏ qua bằng cờ bit, mọi thứ đều khá tốt và thậm chí có thể mở rộng cho tương lai, vậy tại sao không thêm các trường tùy chọn mới vào hàm tạo sau này?..

Nhưng không, nếu bạn không đọc mô tả ngắn gọn của tôi mà đọc tài liệu đầy đủ và suy nghĩ về việc triển khai. Đầu tiên, CRC32 của hàm tạo được tính theo dòng chuẩn hóa của mô tả văn bản của lược đồ (xóa khoảng trắng thừa, v.v.) - vì vậy nếu một trường mới được thêm vào, dòng mô tả loại sẽ thay đổi, và do đó CRC32 của nó và , do đó, tuần tự hóa. Và khách hàng cũ sẽ làm gì nếu anh ta nhận được một trường có gắn cờ mới và anh ta không biết phải làm gì với chúng tiếp theo?..

Thứ hai, hãy nhớ CRC32, được sử dụng ở đây về cơ bản như hàm băm để xác định duy nhất loại nào đang được (hủy) tuần tự hóa. Ở đây chúng ta phải đối mặt với vấn đề va chạm - và không, xác suất không phải là một trên 232, mà lớn hơn nhiều. Ai còn nhớ rằng CRC32 được thiết kế để phát hiện (và sửa) lỗi trong kênh liên lạc và theo đó cải thiện các thuộc tính này để gây bất lợi cho người khác? Ví dụ: nó không quan tâm đến việc sắp xếp lại các byte: nếu bạn tính CRC32 từ hai dòng, trong dòng thứ hai, bạn hoán đổi 4 byte đầu tiên với 4 byte tiếp theo - nó sẽ giống nhau. Khi đầu vào của chúng tôi là các chuỗi văn bản từ bảng chữ cái Latinh (và một chút dấu câu) và những tên này không đặc biệt ngẫu nhiên, khả năng sắp xếp lại như vậy sẽ tăng lên rất nhiều.

Nhân tiện, ai đã kiểm tra những gì ở đó? thực sự CRC32? Một trong những mã nguồn đầu tiên (ngay cả trước Waltman) có hàm băm nhân mỗi ký tự với số 239, rất được những người này yêu thích, ha ha!

Cuối cùng, được rồi, chúng tôi nhận ra rằng các hàm tạo có loại trường Vector<int> и Vector<PolymorType> sẽ có CRC32 khác nhau. Còn hiệu suất trực tuyến thì sao? Và từ quan điểm lý thuyết, điều này có trở thành một phần của loại? Giả sử chúng ta chuyển một mảng gồm mười nghìn số, với Vector<int> mọi thứ đều rõ ràng, độ dài và 40000 byte khác. Điều gì sẽ xảy ra nếu điều này Vector<Type2>, chỉ bao gồm một trường int và nó chỉ có trong loại - chúng ta có cần lặp lại 10000xabcdef0 34 lần rồi 4 byte không inthoặc ngôn ngữ có thể ĐỘC LẬP nó cho chúng ta khỏi hàm tạo fixedVec và thay vì 80000 byte, lại chỉ chuyển 40000?

Đây hoàn toàn không phải là một câu hỏi lý thuyết suông - hãy tưởng tượng bạn nhận được danh sách người dùng nhóm, mỗi người trong số họ có id, họ, tên - sự khác biệt về lượng dữ liệu được truyền qua kết nối di động có thể rất đáng kể. Đó chính xác là hiệu quả của việc đăng nhiều kỳ trên Telegram đã được quảng cáo cho chúng tôi.

Vì thế…

Vector, chưa bao giờ được phát hành

Nếu bạn cố gắng lướt qua các trang mô tả các tổ hợp, v.v., bạn sẽ thấy rằng một vectơ (và thậm chí cả một ma trận) chính thức đang cố gắng xuất ra thông qua các bộ dữ liệu của một số trang tính. Nhưng cuối cùng họ quên mất, bước cuối cùng bị bỏ qua và định nghĩa về một vectơ chỉ được đưa ra một cách đơn giản, chưa gắn với một kiểu nào. Có chuyện gì vậy? Bằng các ngôn ngữ lập trình, đặc biệt là các hàm chức năng, việc mô tả cấu trúc theo cách đệ quy là khá điển hình - trình biên dịch với khả năng đánh giá lười biếng của nó sẽ tự hiểu và thực hiện mọi thứ. Bằng ngôn ngữ tuần tự hóa dữ liệu điều cần thiết là HIỆU QUẢ: chỉ cần mô tả là đủ danh sách, I E. cấu trúc của hai phần tử - phần tử đầu tiên là phần tử dữ liệu, phần tử thứ hai là chính cấu trúc đó hoặc một khoảng trống cho phần đuôi (gói (cons) bằng Lisp). Nhưng điều này rõ ràng sẽ đòi hỏi môi phần tử dành thêm 4 byte (CRC32 trong trường hợp trong TL) để mô tả loại của nó. Một mảng cũng có thể được mô tả dễ dàng kích thước cố định, nhưng trong trường hợp mảng có độ dài không xác định trước thì chúng ta sẽ tách ra.

Do đó, vì TL không cho phép xuất ra một vectơ nên nó phải được thêm vào bên cạnh. Cuối cùng tài liệu nói:

Quá trình tuần tự hóa luôn sử dụng cùng một hàm tạo “vector” (const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t”) không phụ thuộc vào giá trị cụ thể của biến loại t.

Giá trị của tham số tùy chọn t không liên quan đến việc tuần tự hóa vì nó được lấy từ loại kết quả (luôn được biết trước khi khử tuần tự).

Hãy xem xét kỹ hơn: vector {t:Type} # [ t ] = Vector t - nhưng hư không Bản thân định nghĩa này không nói rằng số đầu tiên phải bằng độ dài của vectơ! Và nó không đến từ đâu cả. Đây là điều cần được ghi nhớ và thực hiện bằng chính đôi tay của bạn. Ở những nơi khác, tài liệu thậm chí còn đề cập một cách trung thực rằng loại này không có thật:

Kiểu giả đa hình Vector t là một “loại” có giá trị là một chuỗi các giá trị thuộc bất kỳ loại t nào, được đóng hộp hoặc trần.

... nhưng không tập trung vào nó. Khi bạn cảm thấy mệt mỏi với việc phải trải qua quá trình trải dài của toán học (thậm chí có thể bạn đã biết từ một khóa học đại học), quyết định từ bỏ và thực sự xem xét cách làm việc với nó trong thực tế, ấn tượng để lại trong đầu bạn là đây là một điều nghiêm trọng. Cốt lõi của toán học, rõ ràng là do Cool People (hai nhà toán học - người chiến thắng ACM) phát minh ra chứ không phải của riêng ai. Mục tiêu - thể hiện - đã đạt được.

Nhân tiện, về con số. Hãy để chúng tôi nhắc nhở bạn rằng # đó là một từ đồng nghĩa nat, số tự nhiên:

Có các biểu thức kiểu (gõ-expr) và biểu thức số (nat-expr). Tuy nhiên, chúng được định nghĩa theo cùng một cách.

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

nhưng trong ngữ pháp chúng được mô tả theo cùng một cách, tức là Sự khác biệt này một lần nữa phải được ghi nhớ và thực hiện bằng tay.

Vâng, vâng, các loại mẫu (vector<int>, vector<User>) có một định danh chung (#1cb5c415), I E. nếu bạn biết rằng cuộc gọi được thông báo là

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

thì bạn không còn chờ đợi chỉ một vectơ nữa mà là vectơ người dùng. Chính xác hơn, nên chờ đã - trong mã thực, mọi phần tử, nếu không phải là kiểu trần, sẽ có một hàm tạo và theo cách tốt trong quá trình triển khai thì cần phải kiểm tra - nhưng chúng tôi đã được gửi chính xác trong mọi phần tử của vectơ này kiểu đó? Điều gì sẽ xảy ra nếu đó là một loại PHP nào đó, trong đó một mảng có thể chứa các kiểu khác nhau trong các phần tử khác nhau?

Tại thời điểm này, bạn bắt đầu nghĩ - TL như vậy có cần thiết không? Có lẽ đối với xe đẩy, có thể sử dụng bộ nối tiếp của con người, cùng một protobuf đã tồn tại vào thời điểm đó? Đó là lý thuyết, hãy nhìn vào thực tế.

Việc triển khai TL hiện có trong mã

TL được sinh ra ở sâu trong VKontakte ngay cả trước những sự kiện nổi tiếng với việc bán cổ phần của Durov và (chắc chắn), ngay cả trước khi sự phát triển của Telegram bắt đầu. Và trong nguồn mở mã nguồn của lần triển khai đầu tiên bạn có thể tìm thấy rất nhiều chiếc nạng ngộ nghĩnh. Và bản thân ngôn ngữ đã được triển khai ở đó đầy đủ hơn so với hiện tại trên Telegram. Ví dụ: các hàm băm hoàn toàn không được sử dụng trong lược đồ (có nghĩa là một kiểu giả tích hợp (như vectơ) có hành vi sai lệch). Hoặc

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

nhưng chúng ta hãy xem xét, để có được sự đầy đủ, có thể nói là theo dõi sự tiến hóa của Người khổng lồ về Tư tưởng.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Hoặc cái đẹp này:

    static const char *reserved_words_polymorhic[] = {

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

      };

Đoạn này nói về các mẫu như:

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

Đây là định nghĩa về loại mẫu hashmap dưới dạng vectơ của các cặp int - Type. Trong C++ nó sẽ trông giống như thế này:

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

vì thế, alpha - từ khóa! Nhưng chỉ trong C++ bạn mới có thể viết T, nhưng bạn nên viết alpha, beta... Nhưng không quá 8 tham số, đó là nơi mà sự tưởng tượng kết thúc. Dường như ngày xửa ngày xưa ở St. Petersburg đã xảy ra một số cuộc đối thoại như thế này:

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

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

Nhưng đây là về việc triển khai TL “nói chung” được công bố lần đầu tiên. Hãy chuyển sang xem xét việc triển khai trong chính ứng dụng khách Telegram.

Lời gửi Vasily:

Vasily, [09.10.18 17:07] Hơn hết, cái mông nóng hổi vì họ tạo ra một đống trừu tượng, sau đó đóng đinh vào chúng và dùng nạng che bộ tạo mã
Kết quả là, đầu tiên là từ dock Pilot.jpg
Sau đó từ mã dzhekichan.webp

Tất nhiên, từ những người quen thuộc với các thuật toán và toán học, chúng ta có thể mong đợi rằng họ đã đọc Aho, Ullmann và quen thuộc với các công cụ đã trở thành tiêu chuẩn thực tế trong ngành trong nhiều thập kỷ để viết trình biên dịch DSL của họ, phải không?..

Tác giả điện tín-cli là Vitaly Valtman, có thể hiểu từ sự xuất hiện của định dạng TLO bên ngoài ranh giới (cli) của nó, một thành viên của nhóm - hiện một thư viện để phân tích cú pháp TL đã được phân bổ riêng, ấn tượng của cô ấy là gì Trình phân tích cú pháp TL? ..

16.12 04:18 Vasily: Tôi nghĩ ai đó đã không thành thạo lex+yacc
16.12 04:18 Vasily: Tôi không thể giải thích khác được
16.12 04:18 Vasily: à, hoặc họ đã được trả tiền cho số dòng trong VK
16.12 04:19 Vasily: hơn 3k dòng, v.v.<censored> thay vì một trình phân tích cú pháp

Có lẽ là một ngoại lệ? Hãy xem làm thế nào làm Đây là ứng dụng khách CHÍNH THỨC - 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);

Hơn 1100 dòng trong Python, một vài biểu thức chính quy + các trường hợp đặc biệt như vectơ, tất nhiên, được khai báo trong lược đồ theo cú pháp TL, nhưng họ đã dựa vào cú pháp này để phân tích cú pháp nó... Câu hỏi được đặt ra, tại sao tất cả lại là một phép lạ?иNó sẽ nhiều lớp hơn nếu không ai phân tích nó theo tài liệu?!

Nhân tiện... Bạn có nhớ chúng ta đã nói về việc kiểm tra CRC32 không? Vì vậy, trong trình tạo mã Telegram Desktop có một danh sách các trường hợp ngoại lệ đối với những loại mà CRC32 được tính toán không phù hợp với với cái được chỉ ra trong sơ đồ!

Vasily, [18.12/22 49:XNUMX] và ở đây tôi sẽ suy nghĩ xem liệu có cần một TL như vậy không
nếu tôi muốn gặp rắc rối với việc triển khai thay thế, tôi sẽ bắt đầu chèn ngắt dòng, một nửa số trình phân tích cú pháp sẽ ngắt các định nghĩa nhiều dòng
Tuy nhiên, tdesktop cũng vậy

Hãy nhớ quan điểm về một lớp lót, chúng ta sẽ quay lại vấn đề này sau.

Được rồi, telegram-cli là không chính thức, Telegram Desktop là chính thức, nhưng còn những cái khác thì sao? Ai biết được?.. Trong mã máy khách Android hoàn toàn không có trình phân tích cú pháp lược đồ (điều này đặt ra câu hỏi về nguồn mở, nhưng đây là phần thứ hai), nhưng có một số đoạn mã thú vị khác, nhưng có nhiều hơn về chúng trong phần tiểu mục dưới đây.

Việc xê-ri hóa đặt ra những câu hỏi nào khác trong thực tế? Ví dụ, tất nhiên, họ đã làm rất nhiều thứ với các trường bit và trường có điều kiện:

Vasily: flags.0? true
có nghĩa là trường này hiện diện và bằng true nếu cờ được đặt

Vasily: flags.1? int
có nghĩa là trường này hiện diện và cần được giải tuần tự hóa

Vasily: Thằng khốn, đừng lo lắng về việc mình đang làm!
Vasily: Có đề cập ở đâu đó trong tài liệu rằng true là loại có độ dài bằng XNUMX, nhưng không thể tập hợp bất cứ thứ gì từ tài liệu của họ
Vasily: Trong triển khai nguồn mở, điều này cũng không xảy ra, nhưng có rất nhiều điểm tựa và hỗ trợ

Còn Teleton thì sao? Nhìn về chủ đề MTProto, một ví dụ - trong tài liệu có những phần như vậy, nhưng dấu hiệu % nó chỉ được mô tả là “tương ứng với một loại trần nhất định”, tức là trong các ví dụ bên dưới có lỗi hoặc nội dung nào đó không được ghi chép:

Vasily, [22.06.18 18:38] Ở một nơi:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Trong một cách khác:

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

Và đây là hai điểm khác biệt lớn, trong đời thực có một loại vectơ trần trụi nào đó xuất hiện

Tôi chưa thấy định nghĩa vectơ trần và chưa tìm thấy định nghĩa nào

Phân tích được viết bằng tay trên telethon

Trong sơ đồ của anh ấy, định nghĩa được nhận xét msg_container

Một lần nữa, câu hỏi vẫn là về%. Nó không được mô tả.

Vadim Goncharov, [22.06.18 19:22] và trong tdesktop?

Vasily, [22.06.18 19:23] Nhưng trình phân tích cú pháp TL của họ trên các công cụ thông thường rất có thể sẽ không ăn được thứ này

// parsed manually

TL là một sự trừu tượng đẹp đẽ, không ai thực hiện nó một cách trọn vẹn

Và % không có trong phiên bản chương trình của họ

Nhưng ở đây tài liệu lại mâu thuẫn với chính nó, nên tôi không biết

Nó được tìm thấy trong ngữ pháp, họ có thể đơn giản là quên mô tả ngữ nghĩa

Bạn xem tài liệu trên TL, không có nửa lít thì không thể hiểu được

“Ồ, giả sử,” một độc giả khác sẽ nói, “bạn chỉ trích điều gì đó, vậy hãy chỉ cho tôi cách thực hiện nó.”

Vasily trả lời: “Đối với trình phân tích cú pháp, tôi thích những thứ như

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

bằng cách nào đó thích nó hơn

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

hoặc

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

đây là từ vựng TOÀN BỘ:

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

những thứ kia. đơn giản hơn là nói một cách nhẹ nhàng.”

Nói chung, kết quả là trình phân tích cú pháp và trình tạo mã cho tập hợp con được sử dụng thực sự của TL phù hợp với khoảng 100 dòng ngữ pháp và ~300 dòng của trình tạo (tính tất cả printmã được tạo của), bao gồm cả các tập tin thông tin loại để xem xét nội tâm trong mỗi lớp. Mỗi kiểu đa hình biến thành một lớp cơ sở trừu tượng trống và các hàm tạo kế thừa từ nó và có các phương thức để tuần tự hóa và giải tuần tự hóa.

Thiếu các loại trong ngôn ngữ loại

Gõ mạnh là một điều tốt, phải không? Không, đây không phải là holivar (mặc dù tôi thích ngôn ngữ động hơn), mà là một định đề trong khuôn khổ TL. Dựa vào đó, ngôn ngữ sẽ cung cấp tất cả các loại kiểm tra cho chúng ta. Được rồi, có lẽ không phải bản thân anh ấy mà là cách thực hiện, nhưng ít nhất anh ấy cũng nên mô tả chúng. Và chúng ta muốn những loại cơ hội nào?

Trước hết, những hạn chế. Ở đây chúng tôi thấy trong tài liệu để tải lên các tập tin:

Nội dung nhị phân của tệp sau đó được chia thành nhiều phần. Tất cả các bộ phận phải có cùng kích thước ( kích thước phần ) và phải thỏa mãn các điều kiện sau:

  • part_size % 1024 = 0 (chia hết cho 1KB)
  • 524288 % part_size = 0 (512KB phải chia hết cho part_size)

Phần cuối cùng không phải đáp ứng các điều kiện này, miễn là kích thước của nó nhỏ hơn part_size.

Mỗi phần phải có số thứ tự, phần tập tin, với giá trị nằm trong khoảng từ 0 đến 2,999.

Sau khi tệp được phân vùng, bạn cần chọn phương thức lưu tệp trên máy chủ. Sử dụng upload.saveBigFilePart trong trường hợp kích thước đầy đủ của tệp lớn hơn 10 MB và tải lên.saveFilePart cho các tập tin nhỏ hơn.
[…] Một trong những lỗi nhập dữ liệu sau có thể được trả về:

  • FILE_PARTS_INVALID - Số phần không hợp lệ. Giá trị không nằm giữa 1..3000

Có bất kỳ điều này trong sơ đồ? Điều này có thể diễn đạt bằng cách nào đó bằng cách sử dụng TL không? KHÔNG. Nhưng xin lỗi, ngay cả Turbo Pascal của ông nội cũng có thể mô tả các loại được chỉ định các dãy. Và anh ấy còn biết một điều nữa, bây giờ được biết đến nhiều hơn với cái tên enum - một loại bao gồm một bảng liệt kê một số giá trị cố định (nhỏ). Trong các ngôn ngữ như C - số, lưu ý rằng cho đến nay chúng ta chỉ nói về kiểu con số. Nhưng cũng có mảng, chuỗi... ví dụ, sẽ rất hay nếu diễn tả rằng chuỗi này chỉ có thể chứa một số điện thoại phải không?

Không có điều nào trong số này có trong TL. Nhưng có, ví dụ, trong Lược đồ JSON. Và nếu người khác có thể tranh luận về khả năng chia hết của 512 KB, thì điều này vẫn cần được kiểm tra trong mã, thì hãy đảm bảo rằng khách hàng chỉ cần không thể gửi một số ngoài phạm vi 1..3000 (và lỗi tương ứng không thể phát sinh) điều đó có thể xảy ra, phải không?..

Nhân tiện, về lỗi và giá trị trả về. Ngay cả những người đã từng làm việc với TL cũng mờ mắt - chúng tôi không nhận ra ngay điều đó mỗi cái một hàm trong TL thực sự có thể trả về không chỉ kiểu trả về được mô tả mà còn có thể trả về lỗi. Nhưng điều này không thể được suy luận bằng bất kỳ cách nào bằng cách sử dụng chính TL. Tất nhiên, điều đó đã rõ ràng và không cần bất cứ điều gì trong thực tế (mặc dù trên thực tế, RPC có thể được thực hiện theo nhiều cách khác nhau, chúng ta sẽ quay lại vấn đề này sau) - nhưng còn Độ tinh khiết của các khái niệm Toán học về các Loại Trừu tượng thì sao? từ thiên giới?.. Tôi nhặt được cái kéo - vậy hãy khớp với nó.

Và cuối cùng, còn khả năng đọc thì sao? Vâng, nói chung, tôi muốn Mô tả nó có ngay trong lược đồ (một lần nữa, đúng như vậy trong lược đồ JSON), nhưng nếu bạn đã cảm thấy căng thẳng với nó, thì còn về mặt thực tế thì sao - ít nhất là nhìn một cách tầm thường vào những khác biệt trong quá trình cập nhật? Xem cho chính mình tại ví dụ thực tế:

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

hoặc

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

Nó phụ thuộc vào tất cả mọi người, nhưng GitHub chẳng hạn, từ chối làm nổi bật những thay đổi bên trong những dòng dài như vậy. Trò chơi “tìm 10 điểm khác biệt”, và điều mà bộ não nhìn thấy ngay lập tức là phần đầu và phần cuối trong cả hai ví dụ đều giống nhau, bạn cần phải đọc một cách tẻ nhạt ở đâu đó ở giữa... Theo tôi, điều này không chỉ là trên lý thuyết, nhưng hoàn toàn trực quan bẩn thỉu và cẩu thả.

Nhân tiện, về sự thuần khiết của lý thuyết. Tại sao chúng ta cần các trường bit? Chẳng phải có vẻ như họ mùi xấu từ quan điểm của lý thuyết loại? Lời giải thích có thể được nhìn thấy trong các phiên bản trước của sơ đồ. Lúc đầu thì đúng vậy, cứ mỗi cái hắt hơi là một kiểu mới được tạo ra. Những điều thô sơ này vẫn tồn tại ở dạng này, ví dụ:

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;

Nhưng bây giờ hãy tưởng tượng, nếu bạn có 5 trường tùy chọn trong cấu trúc của mình thì bạn sẽ cần 32 loại cho tất cả các tùy chọn có thể có. Vụ nổ tổ hợp. Do đó, sự thuần khiết như pha lê của lý thuyết TL một lần nữa tan vỡ trước thực tế khắc nghiệt của việc xuất bản nhiều kỳ.

Ngoài ra, ở một số nơi, chính những kẻ này cũng vi phạm kiểu chữ của chính họ. Ví dụ: trong MTProto (chương tiếp theo), phản hồi có thể được nén bằng Gzip, mọi thứ đều ổn - ngoại trừ các lớp và mạch bị vi phạm. Một lần nữa, không phải bản thân RpcResult được thu thập mà là nội dung của nó. Chà, tại sao lại làm thế này?.. Tôi phải dùng nạng cắt vào để lực nén có tác dụng ở mọi nơi.

Hoặc một ví dụ khác, chúng tôi đã từng phát hiện ra lỗi - nó đã được gửi InputPeerUser thay vì InputUser. Hoặc ngược lại. Nhưng nó đã hoạt động! Tức là máy chủ không quan tâm đến loại này. Làm sao có thể? Câu trả lời có thể được cung cấp cho chúng ta bằng các đoạn mã từ 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);

Nói cách khác, đây là nơi thực hiện việc tuần tự hóa THỦ CÔNG, không tạo mã! Có lẽ máy chủ được triển khai theo cách tương tự?.. Về nguyên tắc, điều này sẽ hoạt động nếu được thực hiện một lần, nhưng làm thế nào nó có thể được hỗ trợ sau này trong quá trình cập nhật? Đây có phải là lý do tại sao kế hoạch này được phát minh? Và ở đây chúng ta chuyển sang câu hỏi tiếp theo.

Phiên bản. Lớp

Tại sao các phiên bản sơ đồ được gọi là lớp chỉ có thể được suy đoán dựa trên lịch sử của sơ đồ đã xuất bản. Rõ ràng, lúc đầu, các tác giả nghĩ rằng những điều cơ bản có thể được thực hiện bằng cách sử dụng sơ đồ không thay đổi và chỉ khi cần thiết, đối với các yêu cầu cụ thể, hãy chỉ ra rằng chúng đang được thực hiện bằng cách sử dụng một phiên bản khác. Về nguyên tắc, ngay cả một ý tưởng hay - và cái mới sẽ có vẻ như “hỗn hợp”, xếp chồng lên trên cái cũ. Nhưng hãy xem nó được thực hiện như thế nào. Đúng là tôi đã không thể nhìn nó ngay từ đầu - thật buồn cười, nhưng sơ đồ của lớp cơ sở đơn giản là không tồn tại. Các lớp bắt đầu bằng 2. Tài liệu cho chúng tôi biết về tính năng TL đặc biệt:

Nếu máy khách hỗ trợ Lớp 2 thì phải sử dụng hàm tạo sau:

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

Trong thực tế, điều này có nghĩa là trước mỗi lệnh gọi API, một int có giá trị 0x289dd1f6 phải được thêm vào trước số phương thức.

Nghe có vẻ bình thường. Nhưng chuyện gì đã xảy ra tiếp theo? Sau đó xuất hiện

invokeWithLayer3#b7475268 query:!X = X;

Vậy tiếp theo là gì? Như bạn có thể đoán,

invokeWithLayer4#dea0d430 query:!X = X;

Buồn cười? Không, còn quá sớm để cười, hãy nghĩ về sự thật rằng mỗi yêu cầu từ lớp khác cần được gói trong một loại đặc biệt như vậy - nếu tất cả chúng đều khác nhau, bạn có thể phân biệt chúng bằng cách nào khác? Và chỉ thêm 4 byte vào phía trước là một phương pháp khá hiệu quả. Vì thế,

invokeWithLayer5#417a57ae query:!X = X;

Nhưng rõ ràng là sau một thời gian điều này sẽ trở thành một loại bệnh bacchanalia nào đó. Và giải pháp đã đến:

Cập nhật: Bắt đầu với Lớp 9, các phương thức trợ giúp invokeWithLayerN chỉ có thể được sử dụng cùng với initConnection

Hoan hô! Sau 9 phiên bản, cuối cùng chúng tôi đã đạt được những gì đã được thực hiện trong các giao thức Internet vào những năm 80 - đồng ý về phiên bản một lần khi bắt đầu kết nối!

Vậy tiếp theo là gì?..

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

Nhưng bây giờ bạn vẫn có thể cười được. Chỉ sau 9 lớp nữa, một hàm tạo phổ quát có số phiên bản cuối cùng đã được thêm vào, nó chỉ cần được gọi một lần khi bắt đầu kết nối và ý nghĩa của các lớp dường như đã biến mất, giờ đây nó chỉ là một phiên bản có điều kiện, như mọi nơi khác. Vấn đề đã được giải quyết.

Chính xác?..

Vasily, [16.07.18 14:01] Ngay cả vào thứ Sáu, tôi đã nghĩ:
Máy chủ từ xa gửi các sự kiện mà không có yêu cầu. Yêu cầu phải được gói trong InvokeWithLayer. Máy chủ không gói các bản cập nhật; không có cấu trúc để gói các phản hồi và cập nhật.

Những thứ kia. khách hàng không thể chỉ định lớp mà anh ta muốn cập nhật

Vadim Goncharov, [16.07.18 14:02] Về nguyên tắc, InvokeWithLayer không phải là một cái nạng sao?

Vasily, [16.07.18 14:02] Đây là cách duy nhất

Vadim Goncharov, [16.07.18 14:02] về cơ bản có nghĩa là đồng ý về lớp ở đầu phiên

Nhân tiện, theo đó việc hạ cấp ứng dụng khách không được cung cấp

Cập nhật, tức là kiểu Updates trong sơ đồ, đây là những gì máy chủ gửi đến máy khách không phải để đáp lại yêu cầu API mà độc lập khi một sự kiện xảy ra. Đây là một chủ đề phức tạp sẽ được thảo luận trong một bài đăng khác, nhưng bây giờ điều quan trọng cần biết là máy chủ lưu Cập nhật ngay cả khi máy khách ngoại tuyến.

Vì vậy, nếu bạn từ chối bọc môi gói để chỉ ra phiên bản của nó, điều này về mặt logic sẽ dẫn đến các vấn đề có thể xảy ra sau đây:

  • máy chủ gửi các bản cập nhật cho máy khách ngay cả trước khi máy khách thông báo phiên bản nào nó hỗ trợ
  • Tôi nên làm gì sau khi nâng cấp máy khách?
  • ai đảm bảoý kiến ​​của máy chủ về số lớp sẽ không thay đổi trong quá trình này?

Bạn có nghĩ rằng đây hoàn toàn là suy đoán lý thuyết và trên thực tế, điều này không thể xảy ra vì máy chủ được viết chính xác (ít nhất là nó đã được kiểm tra tốt)? Hà! Cho dù nó thế nào đi chăng nữa!

Đây chính xác là những gì chúng tôi đã gặp phải vào tháng 14. Vào ngày XNUMX tháng XNUMX, có thông báo cho biết có điều gì đó đang được cập nhật trên máy chủ Telegram... và sau đó trong nhật ký:

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.

và sau đó là vài megabyte dấu vết ngăn xếp (à, đồng thời việc ghi nhật ký đã được sửa). Xét cho cùng, nếu nội dung nào đó không được nhận dạng trong TL của bạn, thì đó là mã nhị phân theo chữ ký, tiếp theo ở dòng tiếp theo TẤT CẢ CÁC đi, việc giải mã sẽ trở nên không thể. Bạn nên làm gì trong tình huống như vậy?

Chà, điều đầu tiên mà mọi người nghĩ đến là ngắt kết nối và thử lại. Đã không giúp được gì. Chúng tôi google CRC32 - những thứ này hóa ra là các đối tượng từ sơ đồ 73, mặc dù chúng tôi đã làm việc trên sơ đồ 82. Chúng tôi xem xét kỹ nhật ký - có các số nhận dạng từ hai sơ đồ khác nhau!

Có lẽ vấn đề hoàn toàn nằm ở ứng dụng khách không chính thức của chúng tôi? Không, chúng tôi khởi chạy Telegram Desktop 1.2.17 (phiên bản được cung cấp trong một số bản phân phối Linux), nó ghi vào Nhật ký ngoại lệ: MTP Unexpected type id #b5223b0f read in MTPMessageMedia…

Chỉ trích về giao thức và cách tiếp cận tổ chức của Telegram. Phần 1, kỹ thuật: kinh nghiệm viết client từ đầu - TL, MT

Google cho thấy rằng một vấn đề tương tự đã xảy ra với một trong những ứng dụng khách không chính thức, nhưng sau đó số phiên bản và theo đó, các giả định lại khác...

Vậy chúng ta nên làm gì? Vasily và tôi chia tay: anh ấy cố cập nhật mạch lên 91, tôi quyết định đợi vài ngày và thử lên 73. Cả hai phương pháp đều hiệu quả, nhưng vì chúng là theo kinh nghiệm nên không biết bạn cần bao nhiêu phiên bản lên hay xuống để nhảy hoặc bạn phải đợi bao lâu .

Sau đó, tôi đã có thể tái tạo tình huống: chúng tôi khởi chạy máy khách, tắt nó đi, biên dịch lại mạch sang lớp khác, khởi động lại, xử lý lại sự cố, quay lại lớp trước đó - rất tiếc, không có chuyển đổi mạch nào và máy khách khởi động lại trong một vài phút sẽ giúp ích. Bạn sẽ nhận được sự kết hợp của các cấu trúc dữ liệu từ các lớp khác nhau.

Giải trình? Như bạn có thể đoán từ các triệu chứng gián tiếp khác nhau, máy chủ bao gồm nhiều quy trình thuộc các loại khác nhau trên các máy khác nhau. Rất có thể, máy chủ chịu trách nhiệm “lưu vào bộ đệm” đã đưa vào hàng đợi những gì cấp trên của nó đưa cho nó và họ đưa nó theo sơ đồ đã có sẵn tại thời điểm tạo ra. Và cho đến khi hàng đợi này “thối nát”, không thể làm gì được.

Có lẽ... nhưng đây là một cái nạng khủng khiếp?!.. Không, trước khi nghĩ đến những ý tưởng điên rồ, hãy xem mã của các khách hàng chính thức. Trong phiên bản Android, chúng tôi không tìm thấy bất kỳ trình phân tích cú pháp TL nào, nhưng chúng tôi tìm thấy một tệp nặng (GitHub từ chối chỉnh sửa nó) với tính năng tuần tự hóa (hủy). Dưới đây là đoạn mã:

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;

hoặc

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

Hmm...trông hoang dã quá. Nhưng, có lẽ, đây là mã được tạo ra, được chứ?.. Nhưng nó chắc chắn hỗ trợ tất cả các phiên bản! Đúng, không rõ tại sao mọi thứ lại được trộn lẫn với nhau, các cuộc trò chuyện bí mật và đủ thứ _old7 bằng cách nào đó trông không giống thế hệ máy móc... Tuy nhiên, trên hết, tôi bị ấn tượng bởi

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Các bạn ơi, bạn thậm chí không thể quyết định được bên trong một lớp có gì sao?! Chà, được thôi, giả sử “hai” được phát hành có lỗi, à, điều đó xảy ra, nhưng BA?.. Ngay lập tức, lại cùng một cái cào? Xin lỗi, đây là loại nội dung khiêu dâm gì vậy?..

Nhân tiện, trong mã nguồn của Telegram Desktop, điều tương tự cũng xảy ra - nếu vậy, một số cam kết liên tiếp đối với lược đồ không thay đổi số lớp của nó mà sửa một số thứ. Trong điều kiện không có nguồn dữ liệu chính thức cho chương trình thì có thể lấy dữ liệu đó từ đâu, ngoại trừ mã nguồn của khách hàng chính thức? Và nếu bạn bắt đầu từ đó, bạn không thể chắc chắn rằng sơ đồ này hoàn toàn chính xác cho đến khi bạn kiểm tra tất cả các phương pháp.

Làm thế nào điều này thậm chí có thể được kiểm tra? Tôi hy vọng những người hâm mộ đơn vị, chức năng và các bài kiểm tra khác sẽ chia sẻ trong phần bình luận.

Được rồi, hãy xem một đoạn mã khác:

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;

Nhận xét "được tạo thủ công" này gợi ý rằng chỉ một phần của tệp này được viết thủ công (bạn có thể tưởng tượng toàn bộ cơn ác mộng bảo trì không?) và phần còn lại được tạo bằng máy. Tuy nhiên, sau đó một câu hỏi khác được đặt ra - đó là các nguồn có sẵn không hoàn toàn (giống như các đốm màu GPL trong nhân Linux), nhưng đây đã là chủ đề cho phần thứ hai.

Nhưng đủ rồi. Hãy chuyển sang giao thức mà tất cả quá trình tuần tự hóa này chạy trên đó.

MT Proto

Vì vậy, hãy mở mô tả chung и mô tả chi tiết của giao thức và điều đầu tiên chúng tôi vấp phải là thuật ngữ. Và với sự phong phú của mọi thứ. Nhìn chung, đây có vẻ là một tính năng độc quyền của Telegram - gọi mọi thứ khác nhau ở những nơi khác nhau hoặc những thứ khác nhau bằng một từ hoặc ngược lại (ví dụ: trong API cấp cao, nếu bạn thấy gói nhãn dán thì không phải vậy). bạn nghĩ gì).

Ví dụ: “tin nhắn” và “phiên” ở đây có ý nghĩa khác với giao diện máy khách Telegram thông thường. Chà, mọi thứ đều rõ ràng với thông báo, nó có thể được hiểu theo thuật ngữ OOP hoặc gọi đơn giản là từ “gói” - đây là mức truyền tải thấp, không có thông báo giống như trong giao diện, có nhiều thông báo dịch vụ . Nhưng phiên họp... nhưng điều đầu tiên phải làm trước.

lớp vận chuyển

Việc đầu tiên là vận chuyển. Họ sẽ cho chúng tôi biết về 5 lựa chọn:

  • TCP
  • ổ cắm web
  • Websocket qua HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Ngoài ra còn có vận chuyển UDP, nhưng nó không được ghi lại

Và TCP trong ba biến thể

Cái đầu tiên tương tự như UDP qua TCP, mỗi gói bao gồm một số thứ tự và crc.
Tại sao việc đọc tài liệu trên xe đẩy lại đau đớn đến vậy?

Vâng, bây giờ nó ở đó TCP đã có 4 biến thể:

  • Abridged
  • Trung cấp
  • Đệm trung gian
  • Full

Được rồi, Độ đệm trung gian cho MTProxy, điều này sau đó đã được thêm vào do các sự kiện nổi tiếng. Nhưng tại sao lại có thêm hai phiên bản (tổng cộng là ba) khi bạn có thể sử dụng một phiên bản? Về cơ bản, cả bốn đều khác nhau chỉ ở cách đặt độ dài và tải trọng của MTProto chính, điều này sẽ được thảo luận thêm:

  • trong Rút gọn nó là 1 hoặc 4 byte, nhưng không phải 0xef, thì phần thân
  • ở mức Trung gian, đây là độ dài 4 byte và một trường và lần đầu tiên khách hàng phải gửi 0xeeeeeeee để chỉ ra rằng nó là Trung cấp
  • trong Full gây nghiện nhất, theo quan điểm của một nhà mạng: độ dài, số thứ tự, và KHÔNG PHẢI MỘT mà chủ yếu là MTProto, nội dung, CRC32. Có, tất cả điều này đều nằm trên TCP. Cung cấp cho chúng ta khả năng vận chuyển đáng tin cậy dưới dạng luồng byte tuần tự; không cần trình tự, đặc biệt là tổng kiểm tra. Được rồi, bây giờ sẽ có người phản đối tôi rằng TCP có tổng kiểm tra 16 bit, do đó dữ liệu sẽ bị hỏng. Tuyệt vời, nhưng thực tế chúng ta có một giao thức mật mã có hàm băm dài hơn 16 byte, tất cả các lỗi này - và thậm chí nhiều hơn thế - sẽ bị phát hiện do SHA không khớp ở cấp độ cao hơn. KHÔNG có điểm nào trong CRC32 trên hết.

Hãy so sánh Tóm tắt, trong đó có thể có độ dài một byte, với Trung cấp, điều này biện minh cho “Trong trường hợp cần căn chỉnh dữ liệu 4 byte”, điều này khá vô nghĩa. Điều gì, người ta tin rằng các lập trình viên Telegram kém cỏi đến mức họ không thể đọc dữ liệu từ ổ cắm vào bộ đệm được căn chỉnh? Bạn vẫn phải thực hiện việc này vì việc đọc có thể trả về cho bạn bất kỳ số byte nào (và cũng có các máy chủ proxy chẳng hạn...). Hoặc mặt khác, tại sao lại chặn Abridged nếu chúng ta vẫn có phần đệm khổng lồ trên 16 byte - tiết kiệm 3 byte đôi khi ?

Người ta có ấn tượng rằng Nikolai Durov thực sự thích phát minh lại các bánh xe, bao gồm cả các giao thức mạng mà không có bất kỳ nhu cầu thực tế thực sự nào.

Các lựa chọn vận chuyển khác, bao gồm. Web và MTProxy, chúng tôi sẽ không xem xét bây giờ, có thể ở một bài viết khác nếu có yêu cầu. Về cùng MTProxy này, chúng ta hãy nhớ rằng ngay sau khi phát hành vào năm 2018, các nhà cung cấp đã nhanh chóng học cách chặn nó, nhằm mục đích chặn bỏ quaQua kích cỡ gói! Và thực tế là máy chủ MTProxy được viết (lại bởi Waltman) bằng C bị ràng buộc quá mức với các thông số cụ thể của Linux, mặc dù điều này hoàn toàn không bắt buộc (Phil Kulin sẽ xác nhận) và rằng một máy chủ tương tự trong Go hoặc Node.js sẽ chỉ gói gọn trong chưa đầy một trăm dòng.

Nhưng chúng tôi sẽ đưa ra kết luận về trình độ kỹ thuật của những người này ở cuối phần này, sau khi xem xét các vấn đề khác. Bây giờ, chúng ta hãy chuyển sang lớp 5 của OSI, phiên - trên đó họ đã đặt phiên MTProto.

Khóa, tin nhắn, phiên, Diffie-Hellman

Họ đặt nó ở đó không hoàn toàn chính xác... Một phiên không giống với phiên hiển thị trong giao diện trong Phiên hoạt động. Nhưng theo thứ tự.

Chỉ trích về giao thức và cách tiếp cận tổ chức của Telegram. Phần 1, kỹ thuật: kinh nghiệm viết client từ đầu - TL, MT

Vì vậy, chúng tôi đã nhận được một chuỗi byte có độ dài đã biết từ lớp vận chuyển. Đây có thể là tin nhắn được mã hóa hoặc văn bản gốc - nếu chúng tôi vẫn đang ở giai đoạn thỏa thuận chính và đang thực sự thực hiện việc đó. Chúng ta đang nói về khái niệm nào trong số những khái niệm được gọi là “chìa khóa”? Hãy cùng nhóm Telegram làm rõ vấn đề này (tôi xin lỗi vì đã dịch tài liệu của riêng tôi từ tiếng Anh với đầu óc mệt mỏi vào lúc 4 giờ sáng, việc để lại một số cụm từ như hiện tại sẽ dễ dàng hơn):

Có hai thực thể được gọi là Phiên - một trong giao diện người dùng của khách hàng chính thức trong “phiên hiện tại”, trong đó mỗi phiên tương ứng với toàn bộ thiết bị/HĐH.
Thứ hai là Phiên MTProto, có số thứ tự của thông báo (theo nghĩa cấp thấp) trong đó và có thể kéo dài giữa các kết nối TCP khác nhau. Ví dụ: có thể cài đặt một số phiên MTProto cùng lúc để tăng tốc độ tải xuống tệp.

Giữa hai điều này phiên có một khái niệm ủy quyền. Trong trường hợp suy biến, ta có thể nói rằng phiên giao diện người dùng cũng giống như ủy quyền, nhưng than ôi, mọi thứ đều phức tạp. Hãy xem:

  • Người dùng trên thiết bị mới trước tiên tạo ra auth_key và giới hạn nó vào tài khoản, ví dụ như qua SMS - đó là lý do tại sao ủy quyền
  • Nó đã xảy ra bên trong lần đầu tiên Phiên MTProto, trong đó có session_id bên trong chính bạn.
  • Ở bước này, sự kết hợp ủy quyền и session_id có thể được gọi ví dụ - từ này xuất hiện trong tài liệu và mã của một số khách hàng
  • Sau đó, khách hàng có thể mở một số Phiên MTProto dưới cùng auth_key - đến cùng một DC.
  • Sau đó, một ngày nào đó khách hàng sẽ cần yêu cầu tệp từ một DC khác - và đối với DC này, một DC mới sẽ được tạo auth_key !
  • Để thông báo cho hệ thống rằng không phải người dùng mới đang đăng ký mà là người dùng tương tự ủy quyền (phiên giao diện người dùng), khách hàng sử dụng lệnh gọi API auth.exportAuthorization ở nhà DC auth.importAuthorization ở DC mới.
  • Mọi thứ đều giống nhau, một số có thể được mở Phiên MTProto (mỗi cái có cái riêng của nó session_id) cho DC mới này, dưới của mình auth_key.
  • Cuối cùng, khách hàng có thể muốn Bí mật chuyển tiếp hoàn hảo. Mọi auth_key đã vĩnh viễn key - trên mỗi DC - và khách hàng có thể gọi auth.bindTempAuthKey để sử dụng tạm thời auth_key - và một lần nữa, chỉ có một temp_auth_key mỗi DC, chung cho tất cả Phiên MTProto tới DC này.

Lưu ý rằng muối (và muối trong tương lai) cũng là một trên auth_key những thứ kia. được chia sẻ giữa mọi người Phiên MTProto đến cùng một DC.

"Giữa các kết nối TCP khác nhau" nghĩa là gì? Vậy điều này có nghĩa cái gì đó như cookie ủy quyền trên một trang web - nó vẫn tồn tại (tồn tại) nhiều kết nối TCP đến một máy chủ nhất định, nhưng một ngày nào đó nó bị hỏng. Không giống như HTTP, các tin nhắn trong MTProto trong một phiên được đánh số và xác nhận tuần tự; nếu chúng vào đường hầm, kết nối sẽ bị hỏng - sau khi thiết lập kết nối mới, máy chủ sẽ vui lòng gửi mọi thứ trong phiên này mà nó không gửi trong phiên trước đó. Kết nối TCP.

Tuy nhiên, thông tin trên được tóm tắt sau nhiều tháng điều tra. Trong thời gian chờ đợi, chúng ta có đang triển khai ứng dụng khách của mình từ đầu không? - chúng ta hãy quay lại từ đầu.

Vì vậy hãy tạo ra auth_key trên Phiên bản Diffie-Hellman từ Telegram. Chúng ta hãy cố gắng hiểu tài liệu...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (bất kỳ byte ngẫu nhiên nào); sao cho độ dài bằng 255 byte;
mã hóa_data := RSA(data_with_hash, server_public_key); một số dài 255 byte (big endian) được nâng lên mức cần thiết theo mô đun cần thiết và kết quả được lưu trữ dưới dạng số 256 byte.

Họ có một ít thuốc phiện DH

Trông không giống DH của người khỏe mạnh
Không có hai khóa công khai trong dx

Chà, cuối cùng thì việc này đã được giải quyết, nhưng dư lượng vẫn còn - bằng chứng về công việc đã được khách hàng thực hiện rằng anh ta có thể phân tích con số. Loại bảo vệ chống lại các cuộc tấn công DoS. Và khóa RSA chỉ được sử dụng một lần theo một hướng, về cơ bản là để mã hóa new_nonce. Nhưng trong khi hoạt động tưởng chừng đơn giản này sẽ thành công, bạn sẽ phải đối mặt với điều gì?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Tôi vẫn chưa nhận được yêu cầu appid

Tôi đã gửi yêu cầu này tới DH

Và, trong dock vận chuyển, nó nói rằng nó có thể phản hồi với 4 byte mã lỗi. Đó là tất cả

Chà, anh ấy nói với tôi -404, vậy thì sao?

Vì vậy, tôi đã nói với anh ấy: “Hãy bắt cái thứ được mã hóa nhảm nhí của bạn bằng khóa máy chủ có dấu vân tay như thế này, tôi muốn DH,” và nó phản hồi bằng một mã 404 ngu ngốc

Bạn nghĩ gì về phản hồi của máy chủ này? Phải làm gì? Không có ai để hỏi (nhưng sẽ nói thêm về điều đó trong phần thứ hai).

Ở đây tất cả sự quan tâm được thực hiện trên bến tàu

Tôi không còn việc gì để làm, tôi chỉ mơ ước chuyển đổi số qua lại

Hai số 32 bit. Tôi đóng gói chúng như những người khác

Nhưng không, XNUMX cái này cần được thêm vào dòng trước là BE

Vadim Goncharov, [20.06.18 15:49] và vì 404 này?

Vasily, [20.06.18 15:49] CÓ!

Vadim Goncharov, [20.06.18 15:50] nên tôi không hiểu anh ấy có thể “không tìm thấy” điều gì

Vasily, [20.06.18 15:50] về

Tôi không thể tìm thấy sự phân tách như vậy thành thừa số nguyên tố%)

Chúng tôi thậm chí còn không quản lý việc báo cáo lỗi

Vasily, [20.06.18 20:18] Ồ, còn có MD5 nữa. Đã có ba giá trị băm khác nhau

Dấu vân tay của chìa khóa được tính như sau:

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

SHA1 và sha2

Vậy hãy đặt nó auth_key chúng tôi đã nhận được kích thước 2048 bit bằng Diffie-Hellman. Cái gì tiếp theo? Tiếp theo, chúng tôi phát hiện ra rằng 1024 bit thấp hơn của khóa này không được sử dụng theo bất kỳ cách nào... nhưng bây giờ chúng ta hãy nghĩ về điều này. Ở bước này, chúng ta đã chia sẻ bí mật với máy chủ. Một phiên tương tự của phiên TLS đã được thiết lập, đây là một thủ tục rất tốn kém. Nhưng máy chủ vẫn không biết chúng tôi là ai! Thực ra là chưa. ủy quyền. Những thứ kia. nếu bạn nghĩ về “mật khẩu đăng nhập”, như bạn đã từng làm trong ICQ, hoặc ít nhất là “khóa đăng nhập”, như trong SSH (ví dụ: trên một số gitlab/github). Chúng tôi có một người ẩn danh. Điều gì sẽ xảy ra nếu máy chủ cho chúng tôi biết “những số điện thoại này được phục vụ bởi một DC khác”? Hay thậm chí “số điện thoại của bạn bị cấm”? Điều tốt nhất chúng ta có thể làm là giữ lại chiếc chìa khóa với hy vọng rằng đến lúc đó nó sẽ hữu ích và không bị mục nát.

Nhân tiện, chúng tôi đã “nhận” nó với sự đặt trước. Ví dụ: chúng ta có tin tưởng máy chủ không? Nếu nó là giả thì sao? Kiểm tra mật mã sẽ là cần thiết:

Vasily, [21.06.18 17:53] Họ cung cấp cho khách hàng di động để kiểm tra tính nguyên thủy của số 2kbit%)

Nhưng nó không rõ ràng chút nào cả, nafeijoa

Vasily, [21.06.18 18:02] Tài liệu không nói phải làm gì nếu nó không đơn giản

Không nói. Hãy xem client Android chính thức làm gì trong trường hợp này? MỘT đó là những gì (và vâng, toàn bộ tập tin này rất thú vị) - như họ nói, tôi sẽ để nó ở đây:

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

Không, tất nhiên là nó vẫn ở đó một vài Có những bài kiểm tra tính nguyên tố của một số, nhưng cá nhân tôi không còn đủ kiến ​​thức về toán học nữa.

Được rồi, chúng ta đã có chìa khóa chính. Để đăng nhập, tức là. gửi yêu cầu, bạn cần thực hiện mã hóa thêm bằng AES.

Khóa thông báo được định nghĩa là 128 bit ở giữa của SHA256 của nội dung thư (bao gồm phiên, ID thông báo, v.v.), bao gồm các byte đệm, được thêm vào trước 32 byte lấy từ khóa ủy quyền.

Vasily, [22.06.18 14:08] Trung bình, chó cái, bit

Đã nhận auth_key. Tất cả. Ngoài chúng... tài liệu không nói rõ. Hãy thoải mái nghiên cứu mã nguồn mở.

Lưu ý rằng MTProto 2.0 yêu cầu khoảng đệm từ 12 đến 1024 byte, vẫn phải tuân theo điều kiện độ dài tin nhắn thu được phải chia hết cho 16 byte.

Vậy bạn nên thêm bao nhiêu phần đệm?

Và vâng, cũng có lỗi 404 trong trường hợp có lỗi

Nếu ai đó nghiên cứu kỹ sơ đồ và văn bản của tài liệu, họ sẽ nhận thấy rằng không có MAC ở đó. Và AES đó được sử dụng ở một chế độ IGE nhất định không được sử dụng ở bất kỳ nơi nào khác. Tất nhiên, họ viết về điều này trong Câu hỏi thường gặp của họ... Ở đây, giống như, bản thân khóa thông báo cũng là hàm băm SHA của dữ liệu được giải mã, được sử dụng để kiểm tra tính toàn vẹn - và trong trường hợp không khớp, tài liệu vì lý do nào đó khuyên bạn nên âm thầm bỏ qua chúng (nhưng còn vấn đề bảo mật thì sao, nếu chúng phá vỡ chúng ta thì sao?).

Tôi không phải là nhà mật mã học, có lẽ không có gì sai với chế độ này trong trường hợp này theo quan điểm lý thuyết. Nhưng tôi có thể nêu rõ một vấn đề thực tế, lấy Telegram Desktop làm ví dụ. Nó mã hóa bộ đệm cục bộ (tất cả các D877F783D5D3EF8C này) theo cách tương tự như các tin nhắn trong MTProto (chỉ trong trường hợp này là phiên bản 1.0), tức là. đầu tiên là khóa thông báo, sau đó là dữ liệu (và ở đâu đó ngoài phần chính lớn auth_key 256 byte, không có msg_key vô ích). Vì vậy, vấn đề trở nên đáng chú ý trên các tệp lớn. Cụ thể, bạn cần giữ hai bản sao dữ liệu - được mã hóa và giải mã. Và nếu có megabyte hoặc video phát trực tuyến chẳng hạn?.. Các sơ đồ cổ điển với MAC sau văn bản mã hóa cho phép bạn đọc luồng đó, truyền nó ngay lập tức. Nhưng với MTProto bạn sẽ phải lúc đầu mã hóa hoặc giải mã toàn bộ tin nhắn, chỉ sau đó chuyển nó vào mạng hoặc vào đĩa. Do đó, trong các phiên bản mới nhất của Telegram Desktop trong bộ đệm trong user_data Một định dạng khác cũng được sử dụng - với AES ở chế độ CTR.

Vasily, [21.06.18 01:27] Ồ, tôi đã tìm ra IGE là gì: IGE là nỗ lực đầu tiên ở “chế độ mã hóa xác thực”, ban đầu dành cho Kerberos. Đó là một nỗ lực thất bại (nó không cung cấp khả năng bảo vệ tính toàn vẹn) và phải bị xóa. Đó là sự khởi đầu của hành trình 20 năm tìm kiếm một chế độ mã hóa xác thực hoạt động mà gần đây đã lên đến đỉnh điểm ở các chế độ như OCB và GCM.

Và bây giờ là các đối số từ phía giỏ hàng:

Nhóm đằng sau Telegram, do Nikolai Durov lãnh đạo, bao gồm sáu nhà vô địch ACM, một nửa trong số họ là tiến sĩ toán học. Họ mất khoảng hai năm để tung ra phiên bản MTProto hiện tại.

Điều đó thật buồn cười. Hai năm ở cấp độ thấp hơn

Hoặc bạn chỉ có thể lấy tls

Được rồi, giả sử chúng ta đã thực hiện mã hóa và các sắc thái khác. Cuối cùng có thể gửi các yêu cầu được tuần tự hóa trong TL và giải tuần tự hóa các phản hồi không? Vậy bạn nên gửi những gì và bằng cách nào? Ở đây, giả sử, phương pháp kết nối init, có lẽ là thế này?

Vasily, [25.06.18 18:46] Khởi tạo kết nối và lưu thông tin trên thiết bị, ứng dụng của người dùng.

Nó chấp nhận app_id, device_model, system_version, app_version và lang_code.

Và một số truy vấn

Tài liệu như mọi khi. Hãy thoải mái nghiên cứu nguồn mở

Nếu mọi thứ gần như rõ ràng với InvovWithLayer thì có gì sai ở đây? Hóa ra, giả sử chúng tôi có - khách hàng đã có điều gì đó muốn hỏi máy chủ - có một yêu cầu mà chúng tôi muốn gửi:

Vasily, [25.06.18 19:13] Đánh giá theo mã, cuộc gọi đầu tiên được gói trong thứ tào lao này, và bản thân thứ tào lao đó cũng được gói trong Invokewithlayer

Tại sao initConnection không thể là một cuộc gọi riêng biệt mà phải là một trình bao bọc? Có, hóa ra, nó phải được thực hiện mọi lúc vào đầu mỗi phiên chứ không phải một lần như với khóa chính. Nhưng! Nó không thể được gọi bởi người dùng trái phép! Bây giờ chúng ta đã đạt đến giai đoạn áp dụng được nó Cái này trang tài liệu - và nó cho chúng ta biết rằng...

Chỉ một phần nhỏ các phương thức API được cung cấp cho người dùng trái phép:

  • auth.sendCode
  • auth.resendCode
  • tài khoản.getPassword
  • auth.checkMật khẩu
  • auth.checkPhone
  • auth.signUp
  • auth.signIn
  • auth.importỦy quyền
  • trợ giúp.getConfig
  • help.getGần nhấtDc
  • help.getAppUpdate
  • trợ giúp.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getSự khác biệt
  • langpack.getLanguages
  • langpack.getLanguage

Người đầu tiên trong số họ, auth.sendCode, và có yêu cầu ấp ủ đầu tiên trong đó chúng tôi gửi api_id và api_hash, sau đó chúng tôi nhận được SMS có mã. Và nếu chúng ta ở sai DC (ví dụ: số điện thoại ở quốc gia này được phục vụ bởi quốc gia khác), thì chúng ta sẽ nhận được lỗi với số DC mong muốn. Để tìm ra địa chỉ IP nào theo số DC bạn cần kết nối, hãy giúp chúng tôi help.getConfig. Có thời điểm chỉ có 5 bài dự thi nhưng sau sự kiện nổi tiếng năm 2018, con số đã tăng lên đáng kể.

Bây giờ hãy nhớ rằng chúng ta đã đến giai đoạn này trên máy chủ một cách ẩn danh. Không phải là quá tốn kém để có được một địa chỉ IP sao? Tại sao không thực hiện việc này và các hoạt động khác trong phần không được mã hóa của MTProto? Tôi nghe thấy sự phản đối: “làm sao chúng tôi có thể chắc chắn rằng không phải RKN sẽ phản hồi bằng địa chỉ sai?” Về vấn đề này, chúng tôi nhớ rằng, nói chung, khách hàng chính thức Khóa RSA được nhúng, I E. bạn có thể chỉ dấu hiệu thông tin này. Trên thực tế, điều này đã được thực hiện đối với thông tin về việc bỏ qua việc chặn mà khách hàng nhận được thông qua các kênh khác (về mặt logic, điều này không thể thực hiện được trong chính MTProto; bạn cũng cần biết nơi để kết nối).

ĐƯỢC RỒI. Ở giai đoạn ủy quyền khách hàng này, chúng tôi chưa được ủy quyền và chưa đăng ký đơn đăng ký của mình. Hiện tại, chúng tôi chỉ muốn xem máy chủ phản hồi những gì với các phương thức có sẵn cho người dùng trái phép. Và đây…

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

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

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

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

Trong sơ đồ, thứ nhất đến thứ hai

Trong lược đồ tdesktop, giá trị thứ ba là

Có, tất nhiên kể từ đó, tài liệu đã được cập nhật. Mặc dù nó có thể sớm trở nên không liên quan nữa. Một nhà phát triển mới vào nghề nên biết như thế nào? Có lẽ nếu bạn đăng ký ứng dụng của bạn, họ sẽ thông báo cho bạn? Vasily đã làm điều này, nhưng than ôi, họ không gửi cho anh ấy bất cứ thứ gì (một lần nữa, chúng ta sẽ nói về điều này trong phần thứ hai).

...Bạn nhận thấy rằng bằng cách nào đó chúng tôi đã chuyển sang API, tức là. lên cấp độ tiếp theo và bỏ lỡ điều gì đó trong chủ đề MTProto? Không bất ngờ:

Vasily, [28.06.18 02:04] Mm, họ đang lục lọi một số thuật toán trên e2e

Mtproto xác định các thuật toán và khóa mã hóa cho cả hai miền, cũng như một chút cấu trúc trình bao bọc

Nhưng họ liên tục trộn lẫn các cấp độ khác nhau của ngăn xếp, vì vậy không phải lúc nào cũng rõ mtproto kết thúc ở đâu và cấp độ tiếp theo bắt đầu

Họ pha trộn như thế nào? Chà, đây là khóa tạm thời tương tự cho PFS chẳng hạn (nhân tiện, Telegram Desktop không thể làm điều đó). Nó được thực thi bởi một yêu cầu API auth.bindTempAuthKey, I E. từ cấp cao nhất. Nhưng đồng thời, nó cản trở việc mã hóa ở cấp độ thấp hơn - chẳng hạn, sau đó, bạn cần phải thực hiện lại initConnection v.v., đây không phải là chỉ yêu cầu bình thường. Điều đặc biệt nữa là bạn chỉ có thể có MỘT khóa tạm thời cho mỗi DC, mặc dù trường auth_key_id trong mỗi tin nhắn cho phép bạn thay đổi khóa ít nhất ở mọi tin nhắn và máy chủ có quyền “quên” khóa tạm thời bất cứ lúc nào - tài liệu không nói phải làm gì trong trường hợp này... à, tại sao không thể Bạn không có một số chìa khóa, như với một bộ muối trong tương lai, và ?..

Có một số điều đáng chú ý khác về chủ đề MTProto.

Tin nhắn, msg_id, msg_seqno, xác nhận, ping sai hướng và các đặc điểm riêng khác

Tại sao bạn cần biết về họ? Vì chúng “rò rỉ” lên cấp độ cao hơn và bạn cần lưu ý đến chúng khi làm việc với API. Giả sử chúng ta không quan tâm đến msg_key; cấp thấp hơn đã giải mã mọi thứ cho chúng ta. Nhưng bên trong dữ liệu được giải mã, chúng tôi có các trường sau (cũng là độ dài của dữ liệu, vì vậy chúng tôi biết phần đệm ở đâu, nhưng điều đó không quan trọng):

  • muối - int64
  • phiên_id - int64
  • tin nhắn_id - int64
  • seq_no - int32

Hãy để chúng tôi nhắc bạn rằng chỉ có một muối cho toàn bộ DC. Tại sao lại biết về cô ấy? Không chỉ vì có yêu cầu get_future_salts, cho bạn biết khoảng thời gian nào sẽ hợp lệ, nhưng cũng bởi vì nếu muối của bạn bị "thối", thì thông báo (yêu cầu) sẽ bị mất. Tất nhiên, máy chủ sẽ báo cáo muối mới bằng cách đưa ra new_session_created - nhưng với cái cũ, bạn sẽ phải gửi lại nó bằng cách nào đó chẳng hạn. Và vấn đề này ảnh hưởng đến kiến ​​trúc ứng dụng.

Máy chủ được phép loại bỏ hoàn toàn các phiên và phản hồi theo cách này vì nhiều lý do. Trên thực tế, phiên MTProto từ phía khách hàng là gì? Đây là hai số session_id и seq_no tin nhắn trong phiên này. Vâng, tất nhiên là cả kết nối TCP cơ bản. Giả sử khách hàng của chúng tôi vẫn chưa biết cách làm nhiều việc, anh ấy đã ngắt kết nối và kết nối lại. Nếu điều này xảy ra nhanh chóng - phiên cũ vẫn tiếp tục trong kết nối TCP mới, hãy tăng seq_no hơn nữa. Nếu mất nhiều thời gian, máy chủ có thể xóa nó đi, vì về phía nó cũng là một hàng đợi, như chúng tôi đã tìm hiểu.

Nó nên là gì seq_no? Ồ, đó là một câu hỏi khó. Cố gắng thành thật hiểu ý nghĩa của nó:

Tin nhắn liên quan đến nội dung

Một thông báo yêu cầu một sự thừa nhận rõ ràng. Chúng bao gồm tất cả người dùng và nhiều thông báo dịch vụ, hầu như tất cả đều ngoại trừ các thùng chứa và xác nhận.

Số thứ tự tin nhắn (msg_seqno)

Một số 32 bit bằng gấp đôi số lượng tin nhắn "liên quan đến nội dung" (những tin nhắn yêu cầu xác nhận và đặc biệt là những tin nhắn không phải là vùng chứa) được người gửi tạo trước tin nhắn này và sau đó tăng thêm một nếu tin nhắn hiện tại là một tin nhắn liên quan đến nội dung. Vùng chứa luôn được tạo sau toàn bộ nội dung của nó; do đó, số thứ tự của nó lớn hơn hoặc bằng số thứ tự của các thông điệp chứa trong nó.

Đây là loại rạp xiếc gì với mức tăng thêm 1, rồi lại tăng thêm 2?.. Tôi nghi ngờ rằng ban đầu họ có nghĩa là "bit ít quan trọng nhất đối với ACK, phần còn lại là một con số", nhưng kết quả không hoàn toàn giống nhau - đặc biệt, nó xuất hiện, có thể được gửi một số xác nhận có cùng seq_no! Làm sao? Chà, ví dụ, máy chủ gửi cho chúng tôi một cái gì đó, gửi nó và bản thân chúng tôi giữ im lặng, chỉ trả lời bằng các tin nhắn dịch vụ xác nhận việc nhận được tin nhắn của nó. Trong trường hợp này, các xác nhận gửi đi của chúng tôi sẽ có cùng số gửi đi. Nếu bạn đã quen thuộc với TCP và nghĩ rằng điều này nghe có vẻ hoang đường, nhưng nó có vẻ không hoang đường lắm, bởi vì trong TCP seq_no không thay đổi, nhưng xác nhận đi đến seq_no ở phía bên kia, tôi sẽ vội vàng làm bạn khó chịu. Xác nhận được cung cấp trong MTProto KHÔNG trên seq_no, như trong TCP, nhưng bằng msg_id !

Cái này là cái gì msg_id, điều quan trọng nhất trong các lĩnh vực này? Một mã định danh tin nhắn duy nhất, như tên cho thấy. Nó được định nghĩa là một số 64 bit, các bit thấp nhất lại có phép thuật “máy chủ không phải máy chủ” và phần còn lại là dấu thời gian Unix, bao gồm phần phân số, được dịch chuyển 32 bit sang trái. Những thứ kia. dấu thời gian (và các tin nhắn có thời gian khác nhau quá nhiều sẽ bị máy chủ từ chối). Từ đó, hóa ra nói chung đây là một mã định danh mang tính toàn cầu cho khách hàng. Vì điều đó - hãy nhớ session_id - chúng tôi được đảm bảo: Trong mọi trường hợp, một tin nhắn dành cho một phiên không thể được gửi sang một phiên khác. Tức là hóa ra đã có rồi ba cấp độ - phiên, số phiên, id tin nhắn. Tại sao lại phức tạp như vậy, bí ẩn này rất lớn.

Vì vậy, msg_id cần thiết cho...

RPC: yêu cầu, phản hồi, lỗi. Xác nhận.

Như bạn có thể nhận thấy, không có loại hoặc chức năng "thực hiện yêu cầu RPC" đặc biệt nào trong sơ đồ, mặc dù có câu trả lời. Rốt cuộc, chúng tôi có tin nhắn liên quan đến nội dung! Đó là, bất kỳ tin nhắn có thể là một yêu cầu! Hay không được. Rốt cuộc, môimsg_id. Nhưng có câu trả lời:

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

Đây là nơi chỉ ra thông báo nào được phản hồi. Do đó, ở cấp cao nhất của API, bạn sẽ phải nhớ số lượng yêu cầu của mình là bao nhiêu - Tôi nghĩ không cần phải giải thích rằng công việc không đồng bộ và có thể có một số yêu cầu đang được xử lý cùng một lúc, câu trả lời có thể được trả về theo thứ tự nào? Về nguyên tắc, từ thông báo này và các thông báo lỗi như không có công nhân, có thể tìm ra kiến ​​trúc đằng sau điều này: máy chủ duy trì kết nối TCP với bạn là một bộ cân bằng giao diện người dùng, nó chuyển tiếp các yêu cầu đến các chương trình phụ trợ và thu thập chúng trở lại thông qua message_id. Có vẻ như mọi thứ ở đây đều rõ ràng, logic và tốt.

Vâng?.. Và nếu bạn nghĩ về nó? Suy cho cùng, bản thân phản hồi RPC cũng có một trường msg_id! Chúng ta có cần phải hét vào mặt người phục vụ “bạn chưa trả lời câu trả lời của tôi!” không? Và vâng, có gì về việc xác nhận? Giới thiệu về trang tin nhắn về tin nhắn cho chúng ta biết cái gì

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

và việc đó phải do mỗi bên thực hiện. Nhưng không phải lúc nào cũng vậy! Nếu bạn nhận được RpcResult thì bản thân nó sẽ đóng vai trò xác nhận. Nghĩa là, máy chủ có thể phản hồi yêu cầu của bạn bằng MsgsAck - chẳng hạn như “Tôi đã nhận được nó”. RpcResult có thể phản hồi ngay lập tức. Nó có thể là cả hai.

Và vâng, bạn vẫn phải trả lời câu trả lời! Xác nhận. Nếu không, máy chủ sẽ coi như không thể gửi được và gửi lại cho bạn. Ngay cả sau khi kết nối lại. Nhưng tất nhiên ở đây nảy sinh vấn đề về thời gian chờ. Chúng ta hãy nhìn vào chúng một lát sau.

Trong lúc chờ đợi, hãy xem xét các lỗi thực thi truy vấn có thể xảy ra.

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

Ồ, có người sẽ thốt lên, đây là một hình thức nhân văn hơn - có một dòng! Hãy dành thời gian của bạn. Đây danh sách lỗi, nhưng tất nhiên là không đầy đủ. Từ đó chúng ta biết rằng mã này là cái gì đó như Lỗi HTTP (tất nhiên, ngữ nghĩa của các phản hồi không được tôn trọng, ở một số nơi chúng được phân phối ngẫu nhiên giữa các mã) và dòng trông giống như CAPITAL_LETTERS_AND_NUMBERS. Ví dụ: PHONE_NUMBER_OCCUPIED hoặc FILE_PART_Х_MISSING. Vâng, tức là bạn vẫn sẽ cần dòng này phân tích cú pháp. Ví dụ, FLOOD_WAIT_3600 có nghĩa là bạn phải đợi một giờ và PHONE_MIGRATE_5, rằng số điện thoại có tiền tố này phải được đăng ký tại DC thứ 5. Chúng ta có một loại ngôn ngữ, phải không? Chúng ta không cần một đối số từ một chuỗi, những đối số thông thường sẽ làm được, được thôi.

Một lần nữa, thông tin này không có trên trang thông báo dịch vụ, nhưng, như thường lệ với dự án này, bạn có thể tìm thấy thông tin trên một trang tài liệu khác. Hoặc nghi ngờ. Đầu tiên, hãy nhìn, gõ/vi phạm lớp - RpcError có thể được lồng vào RpcResult. Tại sao không ở bên ngoài? Chúng ta đã không tính đến điều gì?.. Theo đó, đâu là sự đảm bảo rằng RpcError KHÔNG thể được nhúng vào RpcResult, nhưng được trực tiếp hoặc lồng trong một loại khác?.. Và nếu không thể, tại sao nó không ở cấp cao nhất, tức là. nó bị thiếu req_msg_id ? ..

Nhưng hãy tiếp tục về tin nhắn dịch vụ. Khách hàng có thể nghĩ rằng máy chủ đã suy nghĩ từ lâu và đưa ra yêu cầu tuyệt vời này:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Có ba câu trả lời có thể có cho câu hỏi này, một lần nữa giao nhau với cơ chế xác nhận; cố gắng hiểu chúng nên là gì (và danh sách chung các loại không yêu cầu xác nhận) được để lại cho người đọc làm bài tập về nhà (lưu ý: thông tin trong mã nguồn Telegram Desktop chưa hoàn chỉnh).

Nghiện ma túy: trạng thái tin nhắn

Nói chung nhiều chỗ ở TL, MTProto và Telegram nói chung để lại cảm giác bướng bỉnh nhưng lại mất lịch sự, khéo léo và những thứ khác các kĩ năng mềm Chúng tôi lịch sự giữ im lặng về điều đó và kiểm duyệt những lời tục tĩu trong các cuộc đối thoại. Tuy nhiên, nơi nàyОhầu hết trang này là về tin nhắn về tin nhắn Điều đó gây sốc ngay cả đối với tôi, người đã làm việc với các giao thức mạng trong một thời gian dài và đã nhìn thấy những chiếc xe đạp có độ cong khác nhau.

Nó bắt đầu một cách vô thưởng vô phạt, với những xác nhận. Tiếp theo họ kể cho chúng tôi về

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;

Chà, tất cả những người bắt đầu làm việc với MTProto sẽ phải đối mặt với chúng; trong chu trình “sửa - biên dịch lại - khởi chạy”, việc gặp lỗi số hoặc muối bị lỗi trong quá trình chỉnh sửa là chuyện thường tình. Tuy nhiên ở đây có hai điểm:

  1. Điều này có nghĩa là tin nhắn ban đầu bị mất. Chúng ta cần tạo một số hàng đợi, chúng ta sẽ xem xét việc đó sau.
  2. Những con số lỗi lạ này là gì? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... những con số khác đâu rồi, Tommy?

Tài liệu nêu rõ:

Mục đích là các giá trị error_code được nhóm lại (error_code >> 4): ví dụ: các mã 0x40 — 0x4f tương ứng với các lỗi trong quá trình phân tách vùng chứa.

nhưng, thứ nhất, một sự thay đổi theo hướng khác, và thứ hai, không thành vấn đề, những mã khác ở đâu? Trong đầu tác giả?.. Tuy nhiên, đây chỉ là những chuyện vặt vãnh.

Cơn nghiện bắt đầu từ những tin nhắn về trạng thái tin nhắn và bản sao tin nhắn:

  • Yêu cầu thông tin trạng thái tin nhắn
    Nếu một trong hai bên không nhận được thông tin về trạng thái tin nhắn gửi đi của mình trong một thời gian, thì bên đó có thể yêu cầu bên kia cung cấp thông tin một cách rõ ràng:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Thông báo thông tin về trạng thái của tin nhắn
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Ở đây, info là một chuỗi chứa chính xác một byte trạng thái tin nhắn cho mỗi tin nhắn từ danh sách msg_ids đến:

    • 1 = không biết gì về tin nhắn (msg_id quá thấp, bên kia có thể đã quên)
    • 2 = không nhận được tin nhắn (msg_id nằm trong phạm vi định danh được lưu trữ; tuy nhiên, bên kia chắc chắn chưa nhận được tin nhắn như vậy)
    • 3 = chưa nhận được tin nhắn (msg_id quá cao; tuy nhiên, bên kia chắc chắn chưa nhận được)
    • 4 = đã nhận được tin nhắn (lưu ý rằng phản hồi này đồng thời là xác nhận đã nhận)
    • +8 = tin nhắn đã được xác nhận
    • +16 = tin nhắn không yêu cầu xác nhận
    • +32 = Truy vấn RPC chứa trong tin nhắn đang được xử lý hoặc quá trình xử lý đã hoàn tất
    • +64 = phản hồi liên quan đến nội dung cho tin nhắn đã được tạo
    • +128 = bên kia biết chắc chắn rằng tin nhắn đã được nhận
      Phản hồi này không yêu cầu xác nhận. Đó là sự thừa nhận về các thông điệp_state_req có liên quan.
      Lưu ý rằng nếu đột nhiên bên kia không có tin nhắn nào giống như đã được gửi đến họ thì bạn có thể chỉ cần gửi lại tin nhắn đó. Ngay cả khi bên kia nhận được hai bản sao của tin nhắn cùng một lúc, bản sao sẽ bị bỏ qua. (Nếu đã quá lâu và msg_id ban đầu không còn hợp lệ thì tin nhắn sẽ được gói trong msg_copy).
  • Tự nguyện truyền đạt trạng thái tin nhắn
    Một trong hai bên có thể tự nguyện thông báo cho bên kia về trạng thái tin nhắn do bên kia truyền đi.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Truyền thông tự nguyện mở rộng về trạng thái của một tin nhắn
    ...
    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;
  • Yêu cầu rõ ràng để gửi lại tin nhắn
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Bên ở xa sẽ phản hồi ngay lập tức bằng cách gửi lại tin nhắn được yêu cầu […]
  • Yêu cầu rõ ràng để gửi lại câu trả lời
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Bên ở xa sẽ phản hồi ngay lập tức bằng cách gửi lại câu trả lời tới các tin nhắn được yêu cầu […]
  • Bản sao tin nhắn
    Trong một số trường hợp, một tin nhắn cũ có msg_id không còn hợp lệ cần được gửi lại. Sau đó, nó được gói trong một thùng chứa bản sao:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Sau khi nhận được, tin nhắn sẽ được xử lý như thể không có trình bao bọc ở đó. Tuy nhiên, nếu biết chắc chắn rằng tin nhắn orig_message.msg_id đã được nhận thì tin nhắn mới sẽ không được xử lý (đồng thời, nó và orig_message.msg_id đều được xác nhận). Giá trị của orig_message.msg_id phải thấp hơn msg_id của vùng chứa.

Chúng ta hãy giữ im lặng về những gì msgs_state_info một lần nữa, tai của TL chưa hoàn thiện lại thò ra ngoài (chúng tôi cần một vectơ byte và ở hai bit thấp hơn có một enum và ở hai bit cao hơn có cờ). Vấn đề là khác nhau. Có ai hiểu tại sao tất cả điều này là trong thực tế? trong một khách hàng thực sự cần thiết?.. Với khó khăn, nhưng người ta có thể tưởng tượng một số lợi ích nếu một người tham gia vào việc gỡ lỗi và ở chế độ tương tác - hãy hỏi máy chủ cái gì và như thế nào. Nhưng ở đây các yêu cầu được mô tả chuyến đi khứ hồi.

Theo đó, mỗi bên không chỉ phải mã hóa và gửi tin nhắn mà còn phải lưu trữ dữ liệu về bản thân họ, về các phản hồi gửi cho họ trong một khoảng thời gian không xác định. Tài liệu không mô tả thời gian hoặc khả năng ứng dụng thực tế của các tính năng này. không có cách nào. Điều tuyệt vời nhất là chúng thực sự được sử dụng trong mã của khách hàng chính thức! Rõ ràng họ đã được cho biết điều gì đó không có trong tài liệu công khai. Hiểu từ mã tại sao, không còn đơn giản như trong trường hợp của TL - nó không phải là một phần (tương đối) biệt lập về mặt logic, mà là một phần gắn liền với kiến ​​trúc ứng dụng, tức là. sẽ cần nhiều thời gian hơn đáng kể để hiểu mã ứng dụng.

Ping và thời gian. Hàng đợi.

Từ mọi thứ, nếu chúng ta nhớ lại những phỏng đoán về kiến ​​trúc máy chủ (phân phối yêu cầu trên các chương trình phụ trợ), thì sẽ có một điều khá đáng buồn xảy ra - bất chấp tất cả các đảm bảo phân phối trong TCP (dữ liệu được gửi hoặc bạn sẽ được thông báo về khoảng trống, nhưng dữ liệu sẽ được gửi trước khi sự cố xảy ra), xác nhận đó trong chính MTProto - không có gì đảm bảo. Máy chủ có thể dễ dàng làm mất hoặc loại bỏ tin nhắn của bạn và không thể làm gì được, chỉ cần sử dụng các loại nạng khác nhau.

Và trước hết - hàng đợi tin nhắn. Chà, có một điều mọi thứ đều rõ ràng ngay từ đầu - một tin nhắn chưa được xác nhận phải được lưu trữ và gửi lại. Và sau thời gian nào? Và gã hề biết anh ta. Có lẽ những tin nhắn dịch vụ gây nghiện đó bằng cách nào đó giải quyết được vấn đề này bằng nạng, chẳng hạn như trong Telegram Desktop có khoảng 4 hàng đợi tương ứng với chúng (có thể nhiều hơn, như đã đề cập, để làm được điều này, bạn cần phải nghiên cứu kỹ hơn về mã và kiến ​​​​trúc của nó; đồng thời theo thời gian, chúng tôi biết rằng nó không thể được lấy làm mẫu; một số loại nhất định từ sơ đồ MTProto không được sử dụng trong đó).

Tại sao chuyện này đang xảy ra? Có lẽ, các lập trình viên máy chủ đã không thể đảm bảo độ tin cậy trong cụm hoặc thậm chí là đệm trên bộ cân bằng phía trước và đã chuyển vấn đề này sang máy khách. Vì tuyệt vọng, Vasily đã cố gắng triển khai một phương án thay thế, chỉ với hai hàng đợi, sử dụng thuật toán từ TCP - đo RTT đến máy chủ và điều chỉnh kích thước của “cửa sổ” (trong tin nhắn) tùy thuộc vào số lượng yêu cầu chưa được xác nhận. Nghĩa là, một phương pháp phỏng đoán sơ bộ để đánh giá tải của máy chủ là xem nó có thể xử lý bao nhiêu yêu cầu của chúng ta cùng một lúc và không bị mất.

Ờ, vậy là bạn hiểu rồi phải không? Nếu bạn phải triển khai lại TCP trên một giao thức chạy trên TCP, điều này cho thấy giao thức được thiết kế rất kém.

Ồ vâng, tại sao bạn cần nhiều hơn một hàng đợi và điều này có ý nghĩa gì đối với một người làm việc với API cấp cao? Hãy nhìn xem, bạn đưa ra một yêu cầu, tuần tự hóa nó, nhưng thường thì bạn không thể gửi nó ngay lập tức. Tại sao? Vì câu trả lời sẽ là msg_id, đó là tạm thờiаTôi là một nhãn hiệu, việc phân công tốt nhất nên được hoãn lại càng muộn càng tốt - trong trường hợp người phục vụ từ chối nó do sự chênh lệch về thời gian giữa chúng tôi và anh ấy (tất nhiên, chúng tôi có thể tạo ra một chiếc nạng để chuyển thời gian của chúng tôi so với hiện tại vào máy chủ bằng cách thêm một delta được tính toán từ phản hồi của máy chủ - khách hàng chính thức thực hiện việc này, nhưng nó thô và không chính xác do lưu vào bộ đệm). Do đó, khi bạn thực hiện một yêu cầu bằng lệnh gọi hàm cục bộ từ thư viện, thông báo sẽ trải qua các giai đoạn sau:

  1. Nó nằm trong một hàng đợi và chờ mã hóa.
  2. Được bổ nhiệm msg_id và tin nhắn đã chuyển sang hàng đợi khác - có thể chuyển tiếp; gửi đến ổ cắm.
  3. a) Máy chủ phản hồi MsgsAck - tin nhắn đã được gửi, chúng tôi xóa nó khỏi “hàng đợi khác”.
    b) Hoặc ngược lại, anh ấy không thích điều gì đó, anh ấy trả lời badmsg - gửi lại từ “hàng đợi khác”
    c) Không có gì được biết, tin nhắn cần được gửi lại từ hàng đợi khác - nhưng không biết chính xác khi nào.
  4. Máy chủ cuối cùng đã phản hồi RpcResult - phản hồi thực tế (hoặc lỗi) - không chỉ được gửi mà còn được xử lý.

Có lẽ, việc sử dụng container có thể giải quyết được phần nào vấn đề. Đây là khi một loạt tin nhắn được gói thành một và máy chủ phản hồi bằng cách xác nhận tất cả chúng cùng một lúc, trong một msg_id. Nhưng anh ấy cũng sẽ từ chối toàn bộ gói này nếu có sự cố xảy ra.

Và tại thời điểm này, những cân nhắc phi kỹ thuật sẽ phát huy tác dụng. Từ kinh nghiệm, chúng ta đã thấy nhiều chiếc nạng, ngoài ra, bây giờ chúng ta sẽ thấy thêm nhiều ví dụ về lời khuyên và kiến ​​trúc tồi - trong điều kiện như vậy, liệu có đáng để tin tưởng và đưa ra những quyết định như vậy không? Câu hỏi mang tính tu từ (tất nhiên là không).

Chúng ta đang nói về điều gì vậy? Nếu về chủ đề “tin nhắn thuốc về tin nhắn”, bạn vẫn có thể suy đoán với những phản đối như “bạn thật ngu ngốc, bạn đã không hiểu kế hoạch tuyệt vời của chúng tôi!” (vì vậy hãy viết tài liệu trước, như những người bình thường nên làm, với lý do và ví dụ về trao đổi gói, sau đó chúng ta sẽ nói), sau đó thời gian/thời gian chờ là một câu hỏi hoàn toàn thực tế và cụ thể, mọi thứ ở đây đã được biết đến từ lâu. Tài liệu cho chúng tôi biết gì về thời gian chờ?

Máy chủ thường xác nhận việc nhận tin nhắn từ máy khách (thông thường là truy vấn RPC) bằng phản hồi RPC. Nếu phải mất một thời gian dài mới có phản hồi, trước tiên, máy chủ có thể gửi xác nhận đã nhận và sau đó là chính phản hồi RPC.

Máy khách thường xác nhận việc nhận tin nhắn từ máy chủ (thường là phản hồi RPC) bằng cách thêm xác nhận vào truy vấn RPC tiếp theo nếu nó không được truyền quá muộn (nếu nó được tạo, chẳng hạn, 60-120 giây sau khi nhận của một tin nhắn từ máy chủ). Tuy nhiên, nếu trong một thời gian dài không có lý do gì để gửi tin nhắn đến máy chủ hoặc nếu có một số lượng lớn tin nhắn chưa được xác nhận từ máy chủ (ví dụ trên 16), máy khách sẽ truyền một xác nhận độc lập.

... Tôi dịch: bản thân chúng tôi cũng không biết mình cần bao nhiêu và như thế nào, vì vậy hãy giả định rằng hãy để nó như thế này.

Và về ping:

Tin nhắn Ping (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Một phản hồi thường được trả về cùng một kết nối:

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

Những tin nhắn này không yêu cầu xác nhận. Một quả bóng chỉ được truyền đi để đáp lại một lệnh ping trong khi một trong hai bên có thể bắt đầu một lệnh ping.

Đóng kết nối hoãn lại + PING

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

Hoạt động giống như ping. Ngoài ra, sau khi nhận được thông tin này, máy chủ sẽ khởi động bộ hẹn giờ sẽ đóng kết nối hiện tại ngắt kết nối_delay giây sau trừ khi nó nhận được thông báo mới cùng loại tự động đặt lại tất cả bộ hẹn giờ trước đó. Ví dụ: nếu máy khách gửi các ping này cứ sau 60 giây một lần, nó có thể đặt ngắt kết nối_delay bằng 75 giây.

Bạn điên à?! Trong 60 giây, tàu sẽ vào ga, trả khách và lại mất liên lạc trong đường hầm. Trong 120 giây, khi bạn nghe thấy nó, nó sẽ đến một thiết bị khác và rất có thể kết nối sẽ bị đứt. Chà, rõ ràng đôi chân đến từ đâu - “Tôi nghe thấy tiếng chuông, nhưng không biết nó ở đâu”, có thuật toán của Nagl và tùy chọn TCP_NODELAY, dành cho công việc tương tác. Nhưng xin lỗi, hãy giữ nguyên giá trị mặc định của nó - 200 Milligiây Nếu bạn thực sự muốn mô tả một cái gì đó tương tự và tiết kiệm một vài gói có thể, thì hãy tắt nó trong 5 giây hoặc bất cứ điều gì hiện tại đã hết thời gian chờ thông báo “Người dùng đang gõ…”. Nhưng không còn nữa.

Và cuối cùng là ping. Tức là kiểm tra tính tồn tại của kết nối TCP. Thật buồn cười, nhưng khoảng 10 năm trước, tôi đã viết một văn bản phê bình về người đưa tin của ký túc xá khoa chúng tôi - các tác giả ở đó cũng đã ping máy chủ từ máy khách chứ không phải ngược lại. Nhưng sinh viên năm 3 là một chuyện, văn phòng quốc tế lại là chuyện khác phải không?..

Đầu tiên, một chương trình giáo dục nhỏ. Một kết nối TCP, nếu không có trao đổi gói, có thể tồn tại trong nhiều tuần. Điều này vừa tốt vừa xấu, tùy thuộc vào mục đích. Thật tốt nếu bạn có kết nối SSH mở với máy chủ, bạn đứng dậy khỏi máy tính, khởi động lại bộ định tuyến, quay lại vị trí của mình - phiên qua máy chủ này không bị rách (bạn không gõ bất cứ thứ gì, không có gói nào) , thật tiện lợi. Thật tệ nếu có hàng nghìn máy khách trên máy chủ, mỗi máy đều chiếm tài nguyên (xin chào, Postgres!), và máy chủ của máy khách đó có thể đã khởi động lại từ lâu - nhưng chúng ta sẽ không biết về điều đó.

Hệ thống trò chuyện/IM rơi vào trường hợp thứ hai vì một lý do bổ sung - trạng thái trực tuyến. Nếu người dùng "bị ngã", bạn cần thông báo cho người đối thoại của họ về điều này. Nếu không, bạn sẽ mắc phải một sai lầm mà những người tạo ra Jabber đã mắc phải (và đã sửa trong 20 năm) - người dùng đã ngắt kết nối, nhưng họ vẫn tiếp tục viết tin nhắn cho anh ta vì tin rằng anh ta đang trực tuyến (điều này cũng hoàn toàn bị mất trong những vài phút trước khi phát hiện ra sự ngắt kết nối). Không, tùy chọn TCP_KEEPALIVE, tùy chọn mà nhiều người không hiểu cách thức hoạt động của bộ định thời TCP được đưa vào ngẫu nhiên (bằng cách đặt các giá trị hoang dã như hàng chục giây), sẽ không hữu ích ở đây - bạn cần đảm bảo rằng không chỉ nhân hệ điều hành Máy của người dùng vẫn hoạt động nhưng cũng hoạt động bình thường, có thể phản hồi và bản thân ứng dụng (bạn có nghĩ rằng nó không thể đóng băng không? Telegram Desktop trên Ubuntu 18.04 đã bị treo nhiều lần đối với tôi).

Đó là lý do tại sao bạn phải ping máy chủ máy khách chứ không phải ngược lại - nếu máy khách làm điều này, nếu kết nối bị hỏng, ping sẽ không được gửi, mục tiêu sẽ không đạt được.

Chúng ta thấy gì trên Telegram? Hoàn toàn ngược lại! Vâng, đó là. Tất nhiên, về mặt hình thức, cả hai bên đều có thể ping nhau. Trong thực tế, khách hàng sử dụng nạng ping_delay_disconnect, đặt bộ hẹn giờ trên máy chủ. Chà, xin lỗi, khách hàng không có quyền quyết định mình muốn sống ở đó bao lâu mà không cần ping. Máy chủ, dựa trên tải của nó, sẽ biết rõ hơn. Nhưng, tất nhiên, nếu bạn không quan tâm đến tài nguyên, thì bạn sẽ trở thành Pinocchio độc ác của chính mình và một chiếc nạng sẽ làm được...

Lẽ ra nó phải được thiết kế như thế nào?

Tôi tin rằng những sự thật trên cho thấy rõ ràng rằng nhóm Telegram/VKontakte không đủ năng lực trong lĩnh vực mạng máy tính ở cấp độ truyền tải (và thấp hơn) cũng như trình độ chuyên môn thấp của họ trong các vấn đề liên quan.

Tại sao nó lại trở nên phức tạp như vậy và làm thế nào các kiến ​​trúc sư của Telegram có thể phản đối? Thực tế là họ đã cố gắng tạo một phiên tồn tại sau khi kết nối TCP bị ngắt, tức là những gì chưa được phân phối bây giờ, chúng tôi sẽ phân phối sau. Có lẽ họ cũng đã cố gắng thực hiện việc vận chuyển UDP, nhưng họ gặp khó khăn và từ bỏ nó (đó là lý do tại sao tài liệu trống rỗng - không có gì để khoe khoang cả). Nhưng do thiếu hiểu biết về cách thức hoạt động của các mạng nói chung và TCP nói riêng, bạn có thể dựa vào nó ở đâu và bạn cần tự mình làm điều đó (và bằng cách nào), cũng như nỗ lực kết hợp điều này với mật mã “hai con chim với một hòn đá”, đây là kết quả.

Nó cần thiết như thế nào? Dựa trên thực tế rằng msg_id là dấu thời gian cần thiết theo quan điểm mật mã để ngăn chặn các cuộc tấn công lặp lại, việc gắn chức năng nhận dạng duy nhất vào nó là một sai lầm. Do đó, nếu không thay đổi cơ bản kiến ​​trúc hiện tại (khi luồng Cập nhật được tạo, đó là chủ đề API cấp cao cho một phần khác của loạt bài đăng này), người ta sẽ cần phải:

  1. Máy chủ giữ kết nối TCP tới máy khách sẽ chịu trách nhiệm - nếu nó đã đọc từ socket, vui lòng xác nhận, xử lý hoặc trả về lỗi, không bị mất. Sau đó, xác nhận không phải là một vectơ id, mà chỉ đơn giản là “seq_no” nhận được lần cuối cùng - chỉ là một số, như trong TCP (hai số - seq của bạn và số được xác nhận). Chúng ta luôn ở trong phiên, phải không?
  2. Dấu thời gian để ngăn chặn các cuộc tấn công lặp lại sẽ trở thành một trường riêng biệt, không cần thiết. Nó được kiểm tra, nhưng không ảnh hưởng đến bất cứ điều gì khác. Đủ và uint32 - nếu muối của chúng tôi thay đổi ít nhất nửa ngày một lần, chúng tôi có thể phân bổ 16 bit cho các bit thứ tự thấp của phần nguyên của thời gian hiện tại, phần còn lại - cho một phần phân số của giây (như bây giờ).
  3. LOẠI BỎ msg_id hoàn toàn không - từ quan điểm phân biệt các yêu cầu trên phần phụ trợ, trước tiên, có id khách hàng và thứ hai là id phiên, nối chúng lại. Theo đó, chỉ có một thứ là đủ làm định danh yêu cầu seq_no.

Đây cũng không phải là lựa chọn thành công nhất; một sự ngẫu nhiên hoàn toàn có thể đóng vai trò như một mã định danh - nhân tiện, điều này đã được thực hiện trong API cấp cao khi gửi tin nhắn. Sẽ tốt hơn nếu làm lại hoàn toàn kiến ​​trúc từ tương đối đến tuyệt đối, nhưng đây là chủ đề dành cho một phần khác, không phải bài đăng này.

API?

Ta-daam! Vì vậy, sau khi vật lộn qua một con đường đầy đau đớn và chống nạng, cuối cùng chúng tôi đã có thể gửi bất kỳ yêu cầu nào đến máy chủ và nhận bất kỳ câu trả lời nào cho chúng, cũng như nhận được thông tin cập nhật từ máy chủ (không phải để đáp lại yêu cầu mà chính nó gửi cho chúng tôi, như PUSH, nếu có ai đó thì rõ ràng hơn theo cách đó).

Chú ý, bây giờ sẽ có ví dụ duy nhất về Perl trong bài viết! (đối với những người không quen với cú pháp, đối số đầu tiên của Phước là cấu trúc dữ liệu của đối tượng, đối số thứ hai là lớp của đối tượng):

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

Có, tôi không cố ý tiết lộ nội dung - nếu bạn chưa đọc nó, hãy tiếp tục và làm điều đó!

Ồ, chờ đã~~... cái này trông như thế nào nhỉ? Một điều gì đó rất quen thuộc... có lẽ đây là cấu trúc dữ liệu của một API Web điển hình trong JSON, ngoại trừ việc các lớp đó cũng được gắn vào các đối tượng?..

Hóa ra là thế này... Chuyện này là sao vậy các đồng chí?.. Quá nhiều nỗ lực - và chúng tôi dừng lại để nghỉ ngơi tại nơi các lập trình viên Web chỉ mới bắt đầu?..Không phải chỉ JSON qua HTTPS sẽ đơn giản hơn sao?! Đổi lại chúng ta đã nhận được gì? Nỗ lực đó có xứng đáng không?

Hãy đánh giá những gì TL+MTProto đã mang lại cho chúng ta và những lựa chọn thay thế nào có thể thực hiện được. Chà, HTTP, tập trung vào mô hình phản hồi yêu cầu, không phù hợp lắm, nhưng ít nhất có thứ gì đó vượt trội hơn TLS?

Tuần tự hóa nhỏ gọn. Nhìn cấu trúc dữ liệu này, tương tự như JSON, tôi nhớ rằng nó có phiên bản nhị phân. Hãy đánh dấu MsgPack là không đủ khả năng mở rộng, nhưng chẳng hạn, có CBOR - nhân tiện, một tiêu chuẩn được mô tả trong RFC 7049. Điều đáng chú ý là nó định nghĩa thẻ, như một cơ chế mở rộng, và trong số đã được chuẩn hóa rồi có:

  • 25 + 256 - thay thế các dòng lặp lại bằng tham chiếu đến số dòng, một phương pháp nén rẻ tiền
  • 26 - đối tượng Perl được tuần tự hóa với tên lớp và đối số hàm tạo
  • 27 - đối tượng độc lập với ngôn ngữ được tuần tự hóa với các đối số tên kiểu và hàm tạo

Chà, tôi đã cố gắng tuần tự hóa cùng một dữ liệu trong TL và CBOR với tính năng đóng gói chuỗi và đối tượng được bật. Kết quả bắt đầu thay đổi theo hướng có lợi cho CBOR từ một megabyte:

cborlen=1039673 tl_len=1095092

Vì vậy, đầu ra: Có những định dạng đơn giản hơn về cơ bản không gặp phải vấn đề về lỗi đồng bộ hóa hoặc mã định danh không xác định, với hiệu quả tương đương.

Thiết lập kết nối nhanh. Điều này có nghĩa là không có RTT sau khi kết nối lại (khi khóa đã được tạo một lần) - áp dụng từ tin nhắn MTProto đầu tiên, nhưng với một số đặt trước - nhấn cùng một muối, phiên không bị hỏng, v.v. Thay vào đó, TLS cung cấp cho chúng tôi những gì? Trích dẫn về chủ đề:

Khi sử dụng PFS trong TLS, vé phiên TLS (RFC 5077) để tiếp tục phiên được mã hóa mà không cần thương lượng lại khóa và không lưu trữ thông tin khóa trên máy chủ. Khi mở kết nối đầu tiên và tạo khóa, máy chủ sẽ mã hóa trạng thái kết nối và truyền nó đến máy khách (dưới dạng vé phiên). Theo đó, khi kết nối được nối lại, máy khách sẽ gửi một vé phiên, bao gồm cả khóa phiên, quay lại máy chủ. Bản thân vé được mã hóa bằng khóa tạm thời (khóa vé phiên), được lưu trữ trên máy chủ và phải được phân phối giữa tất cả các máy chủ giao diện người dùng xử lý SSL trong các giải pháp phân cụm.[10]. Do đó, việc giới thiệu phiếu phiên có thể vi phạm PFS nếu khóa máy chủ tạm thời bị xâm phạm, chẳng hạn như khi chúng được lưu trữ trong thời gian dài (OpenSSL, nginx, Apache lưu trữ chúng theo mặc định trong toàn bộ thời gian của chương trình; các trang web phổ biến sử dụng chìa khóa trong vài giờ, tối đa vài ngày).

Ở đây RTT không bằng XNUMX, bạn cần trao đổi ít nhất ClientHello và ServerHello, sau đó khách hàng có thể gửi dữ liệu cùng với Finished. Nhưng ở đây chúng ta nên nhớ rằng chúng ta không có Web, với vô số kết nối mới được mở, mà là một trình nhắn tin, kết nối của nó thường là một và ít nhiều tồn tại lâu dài, các yêu cầu tương đối ngắn tới các trang Web - mọi thứ đều được ghép kênh nội bộ. Nghĩa là, hoàn toàn có thể chấp nhận được nếu chúng tôi không đi qua một đoạn tàu điện ngầm thực sự tồi tệ.

Quên cái gì khác? Viết trong các ý kiến.

Để được tiếp tục!

Trong phần thứ hai của loạt bài đăng này, chúng tôi sẽ xem xét không phải các vấn đề về mặt kỹ thuật mà là các vấn đề về tổ chức - cách tiếp cận, hệ tư tưởng, giao diện, thái độ đối với người dùng, v.v. Tuy nhiên, dựa trên thông tin kỹ thuật đã được trình bày ở đây.

Phần thứ ba sẽ tiếp tục phân tích thành phần kỹ thuật/kinh nghiệm phát triển. Bạn sẽ học, đặc biệt:

  • tiếp tục đại dịch với sự đa dạng của các loại TL
  • những điều chưa biết về kênh và siêu nhóm
  • tại sao hộp thoại lại tệ hơn bảng phân công
  • về địa chỉ tin nhắn tuyệt đối và tương đối
  • sự khác biệt giữa hình ảnh và hình ảnh là gì
  • biểu tượng cảm xúc can thiệp vào văn bản in nghiêng như thế nào

và những chiếc nạng khác! Giữ nguyên!

Nguồn: www.habr.com

Thêm một lời nhận xét