對 Telegram 協議和組織方法的批評。 第 1 部分,技術:從頭開始編寫客戶端的經驗 - TL、MT

最近,關於 Telegram 有多牛、Durov 兄弟在構建網絡系統方面有多麼出色和經驗豐富等帖子開始越來越多地出現在 Habré 上。 與此同時,很少有人真正沉浸在技術設備中——他們最多使用一個相當簡單(並且與 MTProto 有很大不同)的基於 JSON 的 Bot API,並且通常只接受 憑信心 所有那些圍繞信使的讚美和公關。 大約一年半前,我在 NPO Echelon Vasily 的同事(不幸的是,他在 Habré 上的帳戶隨草稿被刪除)開始用 Perl 從頭開始編寫他自己的 Telegram 客戶端,後來這些行的作者加入了。 為什麼是 Perl,有些人會立即問? 因為其他語言已經有這樣的項目了。其實這不是重點,其他任何語言都可以 成品庫,因此作者必須一路走下去 從頭開始. 此外,密碼學是這樣一種東西——信任,但要驗證。 對於以安全為中心的產品,您不能只依賴供應商的現成庫並盲目相信它(但是,這是第二部分的更多主題)。 目前,該庫在“中間”級別上運行良好(允許您發出任何 API 請求)。

然而,這一系列的帖子中不會有太多密碼學和數學方面的內容。 但是還會有許多其他技術細節和架構拐杖(它對於那些不會從頭開始編寫但會以任何語言使用該庫的人也很有用)。 所以,主要目標是嘗試從頭開始實現客戶端 根據官方文件. 也就是說,假設官方客戶端的源代碼是封閉的(同樣,在第二部分中,我們將更詳細地揭示這到底是什麼的話題 發生 所以),但是,就像過去一樣,例如,有一個像 RFC 這樣的標準 - 是否可以單獨根據規範編寫客戶端,“不偷看”源代碼,甚至是官方的(Telegram Desktop,mobile) ,甚至是非官方的 Telethon?

目錄:

文檔……在嗎? 是真的嗎?...

這篇文章的筆記片段從去年夏天開始收集。 一直在官方網站上 https://core.telegram.org 文檔是從第 23 層開始的,即停留在 2014 年的某個地方(記住,那時甚至還沒有頻道?)。 當然,理論上,這應該可以在2014年實現一個具有當時功能的客戶端。 但即使在這種情況下,文檔首先是不完整的,其次,有些地方自相矛盾。 一個多月前,也就是 2019 年 XNUMX 月, 偶然 我們發現該站點對文檔進行了大幅更新,更新了一個全新的第 105 層,並指出現在所有內容都需要重新閱讀。 的確,很多文章都修改過,但也有很多沒變。 因此,在閱讀下面關於文檔的批評時,您應該記住,其中一些內容不再相關,但有些內容仍然相關。 畢竟,5 年在現代世界不僅很多,而且 很多。 從那時起(特別是如果你不考慮從那時起丟棄和復活的地理聊天),該方案中的 API 方法數量從一百個增加到兩百五十多個!

作為一名年輕作家,你從哪裡開始?

無論您是從頭開始編寫還是使用例如現成的庫都沒有關係 Python 電視馬拉松瑪德琳 PHP,無論如何,你首先需要 註冊您的應用程序 - 獲取參數 api_id и api_hash (使用過 VKontakte API 的人會立即明白)服務器將通過它來識別應用程序。 這 必須 出於法律原因,但我們將在第二部分詳細討論為什麼圖書館作者不能發布它。 也許您會對測試值感到滿意,儘管它們非常有限 - 事實上,現在您可以註冊您的號碼 只有一個 申請,所以不要急於求成。

現在,從技術的角度來看,我們應該感興趣的是,在註冊後我們應該從 Telegram 收到有關文檔、協議等更新的通知。 也就是說,可以假設帶有碼頭的站點只是簡單地“得分”並繼續專門與那些開始吸引客戶的人合作,因為。 這更容易。 但是沒有,沒有觀察到那樣的情況,沒有任何信息傳來。

而如果你從頭開始寫,那麼接收到的參數的使用其實還很遙遠。 雖然 https://core.telegram.org/ 並首先在入門中討論它們,實際上,您首先必須實施 MTProto協議 - 但如果你相信 根據 OSI 模型佈局 在協議的一般描述頁面的末尾,然後完全是徒勞的。

事實上,無論是在 MTProto 之前還是之後,同時在多個級別上(正如在 OS 內核工作的外國網絡人員所說的那樣,層違規),一個大的、痛苦的和可怕的話題將阻礙......

二進制序列化:TL(Type Language)和它的scheme,還有layers,還有很多嚇人的詞

事實上,這個主題是 Telegram 問題的關鍵。 如果你試圖深入研究它,將會有很多可怕的詞。

所以,計劃。 如果你記得這個詞,說, JSON 架構你想對了。 目標是一樣的:用某種語言來描述一組可能的傳輸數據。 事實上,這就是相似性結束的地方。 如果從頁面 MTProto協議,或者從官方客戶端的源碼樹,我們嘗試打開一些scheme,我們會看到類似這樣的東西:

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

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

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

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

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

---functions---

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

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

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

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

第一次看到這個的人會憑直覺只認出寫的部分——好吧,這些顯然是結構(儘管名字在哪裡,左邊還是右邊?),裡面有字段,之後該類型通過冒號......可能。 在這裡,在尖括號中,可能有 C++ 中的模板(實際上, 不是真的). 所有其他符號是什麼意思,問號,感嘆號,百分比,格子(顯然它們在不同地方表示不同的東西),出現在某個地方,但不在某個地方,十六進制數字 - 最重要的是,如何從中得到 (哪個不會被服務器拒絕)字節流? 你必須閱讀文檔 (是的,附近有指向 JSON 版本中的模式的鏈接——但這並不能使它更清楚).

打開頁面 二進制數據序列化 並沉浸在蘑菇和離散數學的神奇世界中,類似於第 4 年的 matan。 字母表、類型、值、組合子、函數式組合子、範式、複合類型、多態類型……這只是第一頁! 接下來等你 天語,雖然它已經包含了一個簡單的請求和響應的例子,但根本沒有提供對更典型案例的答案,這意味著你將不得不在八個以上的嵌套上費力地複述從俄語翻譯成英語的數學頁!

熟悉函數式語言和自動類型推斷的讀者,當然看到這種語言的描述,即使是從一個例子來看,也熟悉得多,可以說這在原理上大體上是不錯的。 對此的反對意見是:

  • 是的, 目標 聽起來不錯,可惜 沒有達到
  • 俄羅斯大學的教育甚至因 IT 專業而異 - 並非每個人都讀過相應的課程
  • 最後,正如我們將看到的,在實踐中它是 它不需要,因為僅使用了所描述的 TL 的有限子集

如前所述 萊奧書呆子 在頻道上 #perl 在 FreeNode IRC 網絡上,嘗試實現從 Telegram 到 Matrix 的門(根據記憶,引用的翻譯不准確):

感覺就像是第一次接觸類型理論的人,很興奮並開始嘗試使用它,而不是真正關心在實踐中是否有必要。

自己看看是否需要裸類型(int、long 等)作為基本的東西不會引起問題 - 最後它們必須手動實現 - 例如,讓我們嘗試從它們派生 向量. 也就是說,事實上, 大批,如果你用它們的專有名稱來調用結果。

但之前

TL 語法子集的簡要說明,供不了解的人閱讀官方文檔

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

fixed#abcdef34 id:int = Type2;

fixedVec set:Vector<Type2> = FixedVec;

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

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

總是開始定義 設計師,之後,可選地(在實踐中,總是)通過符號 # 必須 CRC32 來自給定類型的規範化描述字符串。 接下來是字段的描述,如果它們是 - 類型可以為空。 這一切都以等號結尾,即給定構造函數(實際上是子類型)所屬類型的名稱。 等號右邊的類型是 多態性 ——也就是可以對應幾種具體的類型。

如果定義出現在該行之後 ---functions---, 那麼語法將保持不變,但含義將不同:構造函數將成為 RPC 函數的名稱,字段將成為參數(好吧,也就是說,它將保持完全相同的給定結構,如下所述,它只是給定的含義),並且“多態類型”是返回結果的類型。 是的,它仍將保持多態性 - 僅在部分中定義 ---types---,並且不會考慮此構造函數。 通過參數輸入被調用函數的重載,即出於某種原因,TL 中沒有提供幾個與 C++ 中相同名稱但不同簽名的函數。

如果不是 OOP,為什麼要使用“構造函數”和“多態”? 好吧,事實上,有人從 OOP 的角度來考慮它會更容易——一個多態類型作為一個抽像類,構造函數是它的直接後代類,而且 final 在許多語言的術語中。 其實當然在這裡 相似 在 OO 編程語言中使用真正重載的構造函數方法。 由於這裡只有數據結構,沒有方法(儘管下面對函數和方法的描述很容易在頭腦中產生關於它們是什麼的混淆,但這是關於其他東西的)——你可以把構造函數想像成從中獲得價值 正在建設中 讀取字節流時輸入。

這是怎麼發生的? 始終讀取 4 個字節的解串器看到值 0xcrc32 - 並了解接下來會發生什麼 field1 與類型 int, IE。 恰好讀取 4 個字節,在這個具有類型的覆蓋字段上 PolymorType 讀。 看見 0x2crc32 並了解到還有兩個領域,首先 long,所以我們讀取了 8 個字節。 然後又是一個複雜類型,它以相同的方式反序列化。 例如, Type3 只要兩個構造函數分別進一步必須滿足其中一個,就可以在模式中聲明 0x12abcd34, 之後你需要再讀 4 個字節 int0x6789cdef,之後什麼都沒有。 任何其他 - 你需要拋出異常。 無論如何,之後我們返回讀取 4 個字節 int 領域 field_c в constructorTwo 然後我們讀完我們的 PolymorType.

最後,如果被抓住 0xdeadcrcconstructorThree,然後事情變得更加複雜。 我們的第一個領域 bit_flags_of_what_really_present 與類型 # - 事實上,這只是類型的別名 nat意為“自然數”。 也就是說,事實上,unsigned int 是唯一的情況,順便說一句,當在實際方案中找到無符號數字時。 因此,接下來是一個帶問號的構造,這意味著這是字段 - 只有在引用的字段中設置了相應的位(大約像三元運算符)時,它才會出現在線路上。 所以,假設這個位是打開的,那麼你需要讀取一個字段,比如 Type,在我們的示例中有 2 個構造函數。 一個是空的(只包含一個標識符),另一個有字段 ids 與類型 ids:Vector<long>.

你可能會認為模板和泛型都很好,或者 Java。 但不是。 幾乎。 這 唯一的 實際電路中尖括號的情況,它僅用於 Vector。 在字節流中,對於 Vector 類型本身,這將是 4 個 CRC32 字節,始終相同,然後是 4 個字節 - 數組元素的數量,然後是這些元素本身。

除此之外,序列化總是以 4 字節的字出現,所有類型都是它的倍數 - 還描述了內置類型 bytes и string 手動序列化長度和對齊 4 - 好吧,這聽起來很正常,甚至相對有效? 雖然 TL 號稱是高效的二進制序列化,但見鬼去吧,隨著任何東西的擴展,甚至布爾值和最大 4 字節的單字符字符串,JSON 還會厚很多嗎? 看,即使不需要的字段也可以通過位標誌跳過,一切都很好,甚至可以為將來擴展,你後來是否在構造函數中添加了新的可選字段?..

但是不,如果你不是閱讀我的簡短描述,而是閱讀完整的文檔,並考慮實施。 首先,構造函數的 CRC32 是通過規範化的模式文本描述字符串計算的(刪除多餘的空格等)——因此,如果添加了新字段,類型描述字符串將發生變化,因此它的 CRC32 和序列化也會發生變化。 如果老客戶收到一個設置了新標誌的字段,但他不知道接下來要做什麼,他會怎麼做?..

其次,讓我們記住 CRC32, 在這里基本上用作 散列函數 唯一確定正在(反)序列化的類型。 在這裡,我們面臨碰撞的問題 - 不,概率不是 232 分之一,而是更多。 誰記得 CRC32 旨在檢測(和糾正)通信通道中的錯誤,並相應地改進這些屬性以損害他人的利益? 例如,她不關心字節的排列:如果你從兩行計算 CRC32,在第二行你將交換前 4 個字節和接下來的 4 個字節——它們將是相同的。 當我們將來自拉丁字母表的文本字符串(和一些標點符號)作為輸入,並且這些名稱不是特別隨機時,這種排列的概率就會大大增加。

順便說一句,誰檢查了那裡的東西 CRC32? 在一個早期的資料中(甚至在 Waltman 之前)有一個哈希函數將每個字符乘以數字 239,這些人非常喜歡,哈哈!

最後,好吧,我們意識到具有字段類型的構造函數 Vector<int> и Vector<PolymorType> 會有不同的CRC32。 線上的演示呢? 而在理論上, 它是否成為類型的一部分? 假設我們傳遞了一個包含一萬個數字的數組,那麼, Vector<int> 一切都很清楚,長度和另外 40000 個字節。 如果這個 Vector<Type2>,它只包含一個字段 int 它是該類型中唯一的一個 - 我們是否需要重複 10000xabcdef0 34 次然後是 4 個字節 int,或者語言能夠從構造函數中為我們顯示這個 fixedVec 而不是 80000 個字節,只傳輸 40000 個字節?

這根本不是一個無意義的理論問題——想像一下你得到一個組用戶列表,每個用戶都有一個 ID、名字、姓氏——通過移動連接傳輸的數據量的差異可能很大。 向我們宣傳的是 Telegram 序列化的有效性。

所以…

無法推導的向量

如果您嘗試仔細閱讀組合子的描述頁面,您會看到一個向量(甚至矩陣)正試圖通過元組推導出幾張表。 但最後他們被錘了,最後一步被跳過,簡單地給出了一個向量的定義,它也沒有綁定到一個類型。 這是怎麼回事? 在語言中 程式設計,尤其是函數式的,遞歸地描述結構是很典型的——具有惰性評估的編譯器將理解一切並做到這一點。 在語言中 數據序列化 但需要效率:簡單描述就足夠了 名單, IE。 兩個元素的結構 - 第一個是數據元素,第二個是相同的結構本身或尾部的空白空間(包 (cons) 在 Lisp 中)。 但這顯然需要 每個 元素額外花費 4 個字節(在 TL 的情況下為 CRC32)來描述其類型。 數組很容易描述 固定尺寸,但在先前未知長度的數組的情況下,我們中斷。

因此,由於 TL 不允許您輸出向量,因此必須在旁邊添加它。 最終文檔說:

序列化始終使用不依賴於類型 t 變量的特定值的相同構造函數“向量”(const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t”)。

可選參數 t 的值不參與序列化,因為它是從結果類型派生的(在反序列化之前總是已知的)。

細看: vector {t:Type} # [ t ] = Vector t - 但 無處 定義本身並沒有說第一個數字必須等於向量的長度! 它不會從任何地方跟隨。 這是給定的,您需要牢記並親自實施。 在其他地方,文檔甚至誠實地提到該類型是假的:

Vector t 多態偽類型是一種“類型”,其值是任意類型 t 的值序列,可以是 boxed 或 bare。

......但不專注於此。 當你厭倦了數學的延伸(你甚至可能從大學課程中知道),決定評分並觀察如何在實踐中實際使用它時,印象會留在你的腦海中:這裡的嚴肅數學是基於,顯然是 Cool People(兩位數學家 - ACM 的獲勝者),而不僅僅是任何人。 目標 - 揮霍 - 已經實現。

順便說一下,關於數量。 記起 # 這是一個同義詞 nat, 自然數:

有類型表達式(類型表達式) 和數值表達式 (自然表達式). 但是,它們的定義方式相同。

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

但在語法中,它們以相同的方式描述,即必須再次記住這種差異並手動投入實施。

嗯,是的,模板類型(vector<int>, vector<User>) 有一個共同的標識符 (#1cb5c415), IE。 如果您知道調用被聲明為

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

那麼你等待的不僅僅是一個向量,而是一個用戶向量。 更確切地說, 必須 等等 - 在實際代碼中,每個元素,如果不是裸類型,都會有一個構造函數,並且在實現中以一種好的方式,有必要檢查 - 我們被準確地發送到這個向量的每個元素中 那種? 如果它是某種 PHP,其中數組可以包含不同元素中的不同類型?

此時,你開始懷疑——需要這樣的 TL 嗎? 也許對於購物車來說,可以使用人工序列化器,即當時已經存在的同一個 protobuf? 這是理論,讓我們看看實踐。

代碼中現有的 TL 實現

TL 誕生於 VKontakte 的內部,甚至在出售 Durov 的股份和(),甚至在 Telegram 開發之前。 並且在開源中 第一次實施的來源 你可以找到很多有趣的拐杖。 而且語言本身在那裡的實現比現在在 Telegram 中的實現更全面。 例如,該方案中根本沒有使用哈希(意味著具有異常行為的內置偽類型(如向量))。 或者

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

但為了完整起見,讓我們考慮一下這幅圖畫,以便追溯,可以這麼說,思想巨人的演變。

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

或者這個美麗的:

    static const char *reserved_words_polymorhic[] = {

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

      };

這個片段是關於模板的,比如:

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

這是 hashmap 模板類型的定義,作為 int 類型對的向量。 在 C++ 中,它看起來像這樣:

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

所以, alpha - 關鍵字! 但是只有在C++中你可以寫T,但是你必須寫alpha,beta……但是不能超過8個參數,幻想在theta上結束了。 因此,似乎在聖彼得堡曾經有過這樣的對話:

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

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

但這是關於“一般”的 TL 的第一個佈局實施。 讓我們繼續考慮實際 Telegram 客戶端中的實現。

羅勒的話:

Vasily,[09.10.18/17/07 XNUMX:XNUMX] 最重要的是,屁股很熱,因為他們搞砸了一堆抽象,然後他們在上面敲了一個螺栓,把拐杖放在了 codegeger 上
結果,首先從碼頭 pilot.jpg
然後從 jekichan.webp 代碼

當然,對於熟悉算法和數學的人來說,我們可以預期他們讀過 Aho、Ullman,並且熟悉幾十年來編寫 DSL 編譯器的事實上的行業標準工具,對吧?..

作者: 電報客戶端 是 Vitaliy Valtman,從 TLO 格式超出其 (cli) 限制的出現可以理解,團隊成員 - 現在分配了用於解析 TL 的庫 對她的印像如何 TL解析器?..

16.12 04:18 Vasily:在我看來,有人沒有掌握 lex + yacc
16.12 04:18 Vasily:不然我沒法解釋
16.12 04:18 Vasily:好吧,或者他們為 VK 中的行數付費
16.12 04:19 Vasily:其他 3k+ 行<censored> 而不是解析器

也許是個例外? 讓我們看看如何 品牌 這是官方客戶端 — Telegram Desktop:

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

Python 中的 1100 多行,一些正則表達式 + 向量類型的特殊情況,當然,根據 TL 語法在方案中聲明它應該是這樣的,但是他們把它放在這個語法上,更多地解析它... 問題是,為什麼要為所有這些奇蹟而煩惱и更多的粉撲,如果沒有人會根據文檔解析它?!

順便說一下...還記得我們討論過 CRC32 校驗嗎? 因此,在 Telegram Desktop 代碼生成器中,有一個例外列表,用於計算 CRC32 的那些類型 不匹配 如圖所示!

Vasily,[18.12 22:49] 在這裡你應該考慮是否需要這樣的 TL
如果我想搞亂替代實現,我會開始插入換行符,一半的解析器會中斷多行定義
但是,tdesktop 也是

記住關於單行的要點,我們稍後會回到它。

好的,telegram-cli 是非官方的,Telegram Desktop 是官方的,但是其他的呢? 誰知道呢?.. 在 Android 客戶端代碼中,根本沒有模式解析器(這引發了關於開源的問題,但這是第二部分),但還有其他幾個有趣的代碼片段,但關於它們下面的小節。

序列化在實踐中還提出了哪些其他問題? 例如,他們當然搞砸了位字段和條件字段:

輕率地: flags.0? true
表示該字段存在並且如果設置了標誌則為真

輕率地: flags.1? int
表示該字段存在,需要反序列化

瓦西里:屁股,別燒了,你在幹什麼!
Vasily:文檔中某處提到 true 是零長度的裸類型,但從他們的文檔中收集一些東西是不現實的
Vasily:在開放實現中也沒有這樣的東西,但是有很多拐杖和道具

電視馬拉松怎麼樣? 展望 MTProto 的主題,一個例子 - 文檔中有這樣的部分,但標誌 % 它僅被描述為“對應於給定的裸類型”,即在下面的示例中,要么是錯誤,要么是未記錄的內容:

瓦西里,[22.06.18/18/38 XNUMX:XNUMX] 在一個地方:

msg_container#73f1f8dc messages:vector message = MessageContainer;

在不同的:

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

這是兩個很大的區別,在現實生活中,出現了某種裸向量

我還沒有看到裸矢量定義,也沒有遇到過它

手寫在電視節目中的分析

他的模式註釋掉了定義 msg_container

同樣,問題仍然是 about%。 它沒有被描述。

Vadim Goncharov,[22.06.18/19/22 XNUMX:XNUMX PM] 在 tdesktop 中?

Vasily,[22.06.18/19/23 XNUMX:XNUMX] 但是他們在監管機構上的 TL 解析器可能也不會吃掉它

// parsed manually

TL 是一個美麗的抽象,沒有人完全實現它

他們的計劃版本中沒有 %

但是這裡的文檔自相矛盾,所以 xs

它是在語法中發現的,他們可能只是忘記描述語義

好吧,您在 TL 上看到了碼頭,沒有半升就無法弄清楚

“好吧,比方說,”另一位讀者會說,“你批評一切,所以就應該表現出來。”

Vasily 回答:“至於解析器,我需要像

    args: /* empty */ { $$ = NULL; }
        | args arg { $$ = g_list_append( $1, $2 ); }
        ;

    arg: LC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | LC_ID ':' condition '?' type-term { $$ = tl_arg_new_cond( $1, $5, $3 ); free($3); }
            | UC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | type-term { $$ = tl_arg_new( "", $1 ); }
            | '[' LC_ID ']' { $$ = tl_arg_new_mult( "", tl_type_new( $2, TYPE_MOD_NONE ) ); }
            ;

不知何故比

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

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

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

這是整個詞法分析器:

    ---functions---         return FUNCTIONS;
    ---types---             return TYPES;
    [a-z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return LC_ID;
    [A-Z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return UC_ID;
    [0-9]+                  yylval.number = atoi(yytext); return NUM;
    #[0-9a-fA-F]{1,8}       yylval.number = strtol(yytext+1, NULL, 16); return ID_HASH;

    n                      /* skip new line */
    [ t]+                  /* skip spaces */
    //.*$                 /* skip comments */
    /*.**/              /* skip comments */
    .                       return (int)yytext[0];

那些。 更簡單的是委婉地說。”

一般來說,最終,實際使用的 TL 子集的解析器和代碼生成器適合大約 100 行語法和 ~ 300 行生成器(包括所有 print的生成代碼),包括類型好東西,每個類中用於自省的類型信息。 每個多態類型都變成一個空的抽象基類,構造函數繼承自它並具有序列化和反序列化的方法。

類型語言中缺少類型

強類型很好,對吧? 不,這不是 holivar(儘管我更喜歡動態語言),而是 TL 中的假設。 基於它,語言應該為我們提供各種檢查。 好吧,好吧,不是讓他,而是實施,但他至少應該描述一下。 我們想要什麼機會?

首先,約束。 這裡我們在上傳文件的文檔中看到:

然後文件的二進制內容被分成幾部分。 所有零件必須具有相同的尺寸( 零件尺寸 ) 並且必須滿足以下條件:

  • part_size % 1024 = 0 (能被1KB整除)
  • 524288 % part_size = 0 (512KB 必須能被 part_size 整除)

最後一部分不必滿足這些條件,只要它的大小小於 part_size。

每個部分都應該有一個序號, 文件部分, 取值範圍為 0 到 2,999。

文件分區後,您需要選擇一種方法將其保存在服務器上。 使用 上傳.saveBigFilePart 如果文件的完整大小超過 10 MB 並且 上傳.saveFilePart 對於較小的文件。
[…] 可能會返回以下數據輸入錯誤之一:

  • FILE_PARTS_INVALID - 零件數無效。 該值不介於 1..3000

模式中是否存在這些? 它是否可以通過 TL 以某種方式表達? 不。 但是對不起,即使是老式的 Turbo Pascal 也能夠描述由 範圍. 他還可以做一件事,現在更廣為人知的是 enum - 由固定(少量)值的枚舉組成的類型。 請注意,在 C 之類的語言中 - 數字,到目前為止我們只討論了類型。 數字. 但是也有數組,字符串……比如,描述這個字符串只能包含一個電話號碼就好了,對吧?

這些都不在 TL 中。 但是,例如,在 JSON Schema 中。 如果其他人可以反對 512 KB 的可分割性,這仍然需要在代碼中檢查,那麼請確保客戶端簡單地 不能 發送超出範圍的號碼 1..3000 (並且不會出現相應的錯誤)這是可能的,對吧?..

順便說一下錯誤和返回值。 即使對於那些與 TL 合作過的人來說,眼睛也模糊了——我們並沒有立即意識到這一點 每個 TL 中的函數實際上不僅可以返回所描述的返回類型,還可以返回錯誤。 但這不能通過 TL 本身推導出來。 當然,這是可以理解的,nafig 在實踐中是不必要的(雖然實際上,RPC 可以通過不同的方式來完成,我們會回到這個)——但是來自天堂世界的抽像類型數學概念的純度呢? .. 抓住了拖船 - 所以匹配。

最後,可讀性如何? 好吧,總的來說,我想 描述 讓它在模式中正確(同樣,它在 JSON 模式中),但如果它已經用它緊張,那麼實際方面呢 - 至少在更新期間觀察差異是老生常談? 親自看看 真實的例子:

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

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

有人喜歡它,但是 GitHub,例如,拒絕在這麼長的行中突出顯示更改。 遊戲“找10個不同點”,大腦第一眼看到的是兩個例子的開頭和結尾是一樣的,你需要在中間的某個地方繁瑣地閱讀……在我看來,這不僅僅是理論上的,但純粹從視覺上看 又髒又亂.

順便說一句,關於理論的純度。 為什麼需要位域? 他們似乎不 從類型論的角度來看不好嗎? 在架構的早期版本中可以看到解釋。 起初,是的,是這樣的,每次打噴嚏都會產生一種新的類型。 這些雛形仍然以這種形式存在,例如:

storage.fileUnknown#aa963b05 = storage.FileType;
storage.filePartial#40bc6f52 = storage.FileType;
storage.fileJpeg#7efe0e = storage.FileType;
storage.fileGif#cae1aadf = storage.FileType;
storage.filePng#a4f63c0 = storage.FileType;
storage.filePdf#ae1e508d = storage.FileType;
storage.fileMp3#528a0677 = storage.FileType;
storage.fileMov#4b09ebbc = storage.FileType;
storage.fileMp4#b3cea0e4 = storage.FileType;
storage.fileWebp#1081464c = storage.FileType;

但是現在想像一下,如果您的結構中有 5 個可選字段,那麼所有可能的選項都需要 32 種類型。 組合爆炸。 因此,TL理論的水晶純度再次撞上了連載的嚴酷現實的鑄鐵屁股。

此外,在某些地方,這些人自己也違反了自己的打字規定。 例如,在 MTProto(下一章)中,響應可以通過 Gzip 壓縮,一切都是合理的——除了違反層和模式。 有一次,並沒有收穫 RpcResult 本身,而是收穫了它的內容。 好吧,為什麼要這樣做?..我不得不砍掉一根拐杖,這樣壓縮就可以在任何地方發揮作用。

或者另一個例子,我們曾經發現一個錯誤——發送 InputPeerUser 而不是 InputUser. 或相反亦然。 但它奏效了! 也就是說,服務器不關心類型。 怎麼會這樣? 答案可能會由來自 telegram-cli 的代碼片段提示:

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

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

也就是說,到這裡序列化就完成了 手動,不是生成的代碼! 也許服務器是以類似的方式實現的?.. 原則上,如果完成一次就可以工作,但是以後如何通過更新來支持它? 這不是計劃的目的嗎? 然後我們繼續下一個問題。

版本控制。 圖層

為什麼模式版本被稱為層只能根據已發布模式的歷史來猜測。 顯然,起初在作者看來,基本的事情可以在一個不變的方案上完成,並且只有在必要時,才向特定請求表明它們是根據不同的版本完成的。 原則上,即使是一個好主意 - 新的也會“混合”在舊的之上。 但是,讓我們看看它是如何完成的。 沒錯,從一開始就不可能看到 - 這很有趣,但基礎層方案根本不存在。 層從 2 開始。文檔告訴我們一個特殊的 TL 功能:

如果客戶端支持第 2 層,則必須使用以下構造函數:

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

實際上,這意味著在每次 API 調用之前,一個具有值的 int 0x289dd1f6 必須在方法編號之前添加。

聽起來不錯。 但接下來發生了什麼? 然後來了

invokeWithLayer3#b7475268 query:!X = X;

那麼下一步是什麼? 因為很容易猜到

invokeWithLayer4#dea0d430 query:!X = X;

有趣的? 不,現在笑還早,想想什麼 來自另一層的請求需要包裝在這樣一個特殊的類型中——如果它們都不同,還能如何區分它們? 在前面僅添加 4 個字節是一種非常有效的方法。 所以

invokeWithLayer5#417a57ae query:!X = X;

但很明顯,過一段時間就會變成一些酒神。 解決方案來了:

更新:從第 9 層開始,輔助方法 invokeWithLayerN 可以一起使用 initConnection

萬歲! 在 9 個版本之後,我們終於來到了 80 年代互聯網協議所做的事情——在連接開始時進行一次版本協商!

那麼接下來呢?...

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

現在你可以笑了。 又過了9層,終於加了一個帶版本號的通用構造函數,只需要在連接開始時調用一次,層中的意思好像沒了,現在只是一個條件版本,比如其他地方。 問題解決了。

正確的?..

瓦西里,[16.07.18/14/01 XNUMX:XNUMX PM] 星期五我想:
遠程服務器無需請求即可發送事件。 請求需要包裝在 InvokeWithLayer 中。 服務器不包裝更新,沒有包裝響應和更新的結構。

那些。 客戶端無法指定他想要更新的層

Vadim Goncharov,[16.07.18/14/02 XNUMX:XNUMX PM] InvokeWithLayer 原則上不是拐杖嗎?

瓦西里,[16.07.18/14/02 XNUMX:XNUMX PM] 這是唯一的方法

Vadim Goncharov,[16.07.18/14/02 XNUMX:XNUMX PM] 本質上應該意味著在會議開始時分層

順便說一句,由此可見,不提供客戶端降級

更新,即類型 Updates 在該方案中,這是服務器發送給客戶端的內容,不是響應 API 請求,而是在事件發生時自行發送。 這是一個複雜的話題,將在另一篇文章中討論,但現在重要的是要知道服務器即使在客戶端離線時也會累積更新。

因此,當拒絕包裹時 每個 包來指示其版本,因此邏輯上可能會出現以下問題:

  • 服務器在客戶端告知它支持哪個版本之前向客戶端發送更新
  • 升級客戶端后應該做什麼?
  • 擔保服務器對層數的看法不會在此過程中改變?

你認為這是純粹的理論思考,而在實踐中這不可能發生,因為服務器寫得正確(無論如何,它被測試得很好)? 哈! 不管怎樣!

這正是我們在 14 月遇到的情況。 XNUMX 月 XNUMX 日,消息閃現,Telegram 服務器正在更新某些內容……然後在日誌中:

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

然後是幾兆字節的堆棧跟踪(好吧,與此同時,日誌記錄已修復)。 畢竟,如果你的 TL 中沒有識別出某些東西 - 它是二進制的簽名,在流中更遠 全部 去,解碼將變得不可能。 遇到這種情況怎麼辦?

好吧,任何人想到的第一件事就是斷開連接並重試。 沒有幫助。 我們用谷歌搜索了 CRC32——這些結果是方案 73 中的對象,儘管我們在方案 82 上工作。我們仔細查看日誌——有來自兩個不同方案的標識符!

也許問題純粹出在我們的非官方客戶端? 不,我們運行 Telegram Desktop 1.2.17(許多 Linux 發行版提供的版本),它寫入異常日誌:MTP Unexpected type id #b5223b0f read in MTPMessageMedia ...

對 Telegram 協議和組織方法的批評。 第 1 部分,技術:從頭開始編寫客戶端的經驗 - TL、MT

谷歌表明,一個非官方客戶已經發生了類似的問題,但是版本號和相應的假設是不同的......

那麼該怎麼辦? Vasily和我分道揚鑣:他試過把scheme更新到91,我決定等幾天試試73。兩種方法都管用,但由於是經驗,不知道你需要跳多少個版本或下來,也不是你要等多久。

後來,我設法重現了這種情況:我們啟動客戶端,將其關閉,將方案重新編譯到另一層,重新啟動,再次發現問題,返回到之前的 - 糟糕,沒有切換方案並重新啟動客戶端幾次分鐘會有所幫助。 您將收到來自不同層的混合數據結構。

解釋? 正如您可以從各種間接症狀中猜測的那樣,服務器由不同機器上的許多不同類型的進程組成。 最有可能的是,負責“緩衝”的服務器之一將較高級服務器提供給它的內容放入隊列中,並按照生成時的方案提供。 在這個隊列“爛掉”之前,我們無能為力。

除非……但這是一個可怕的拐杖?!……不,在思考瘋狂的想法之前,讓我們先看看官方客戶端的代碼。 在 Android 版本中,我們沒有找到任何 TL 解析器,但是我們找到了一個帶有(反)序列化的大文件(github 拒絕給它著色)。 以下是代碼片段:

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

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

嗯……看起來很瘋狂。 但是,大概,這是一個生成的代碼,那好嗎?..但它肯定支持所有版本! 沒錯,目前尚不清楚為什麼所有東西都混合在一堆,秘密聊天以及各種 _old7 不知何故與機器生成不相似......然而,最重要的是我從

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

伙計們,你們連一層都不能決定嗎?! 好吧,好吧,比方說,“兩個”被錯誤地釋放了,好吧,它發生了,但是三個?.. 立即再次在同一個耙子上? 抱歉,這是什麼色情內容?...

順便說一句,類似的事情發生在 Telegram Desktop 源代碼中——如果是這樣的話,並且連續多次提交到該方案並沒有改變它的層數,而是修復了一些東西。 在方案沒有官方數據來源的情況下,除了官方客戶來源外,我可以從哪裡獲得數據? 從那裡開始,在測試所有方法之前,您無法確定該方案是否完全正確。

這怎麼能被測試呢? 希望單元測試、功能測試和其他測試的愛好者在評論中分享。

好,我們再看一段代碼:

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

    public int folder_id;

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

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

//manually created

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

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

這裡的“手動創建”註釋表明該文件只有一部分是手寫的(您能想像維護的噩夢嗎?),其餘部分是機器生成的。 然而,另一個問題出現了——資源是可用的 不完全的 (Linux 內核中 GPL 下的 la blob),但這已經是第二部分的主題了。

但足夠了。 讓我們繼續討論所有這些序列化所遵循的協議。

MT 原型

所以讓我們打開 一般描述 и 協議的詳細描述 我們絆倒的第一件事是術語。 並擁有豐富的一切。 一般來說,這似乎是 Telegram 的商標——以不同的方式在不同的地方調用事物,或者用一個詞調用不同的事物,反之亦然(例如,在高級 API 中,如果你看到一個貼紙包——這個不是你想的那樣)。

例如,“消息”(message) 和“會話”(session)——在這裡它們的含義與 Telegram 客戶端通常的界面不同。 好吧,消息的一切都很清楚,可以用 OOP 來解釋,或者簡稱為“包”這個詞——這是一個低級的傳輸層,沒有與界面中相同的消息,有很多服務的。 但是會議……但要事第一。

傳輸層

首先是交通。 我們將被告知 5 個選項:

  • TCP
  • 網絡套接字
  • 通過 HTTPS 的網絡套接字
  • HTTP
  • HTTPS

Vasily,[15.06.18/15/04 XNUMX:XNUMX PM] 還有 UDP 傳輸,但沒有記錄

和 TCP 的三種變體

第一種類似於 UDP over TCP,每個數據包都包含一個序列號和一個 crc
為什麼在推車上閱讀碼頭如此痛苦?

現在好了 TCP 已有 4 種變體:

  • 簡略
  • 中級
  • 填充中間體
  • 飾品

好的,填充了 MTProxy 的中間體,這是後來由於已知事件而添加的。 但是,為什麼還有兩個版本(總共三個),而一個可以做到呢? 這四個本質上的區別僅在於如何設置主 MTProto 本身的長度和有效負載,這將在後面進一步討論:

  • 在 Abridged 中它是 1 或 4 個字節但不是 0xef 那麼 body
  • 在 Intermediate 中,這是 4 個字節的長度和一個字段,客戶端第一次必鬚髮送 0xeeeeeeee 表明它是中級
  • 從網絡工作者的角度來看,完整版是最令人上癮的:長度、序列號,而不是基本上是 MTProto、正文、CRC32 的那個。 是的,所有這些都通過 TCP。 它以串行字節流的形式為我們提供了可靠的傳輸,不需要序列,尤其是校驗和。 好的,現在我將反對 TCP 有一個 16 位校驗和,所以數據損壞發生了。 很好,除了我們實際上有一個哈希值超過 16 字節的加密協議,所有這些錯誤 - 甚至更多 - 都將在更高級別的 SHA 不匹配上被捕獲。 CRC32 對此毫無意義。

讓我們比較一下 Abridged,其中一個字節的長度是可能的,Intermediate 證明“以防需要 4 字節數據對齊”,這是非常無稽之談。 什麼,據信 Telegram 程序員笨到無法將數據從套接字讀取到對齊的緩衝區中? 您仍然必須這樣做,因為讀取可以返回任意數量的字節(並且還有代理服務器,例如......)。 或者,另一方面,如果頂部的 16 個字節仍有大量填充,為什麼還要使用 Abridged - 節省 3 個字節 有時 ?

給人的印像是,尼古拉杜羅夫非常喜歡發明自行車,包括網絡協議,但沒有真正的實際需要。

其他交通選擇,包括。 Web 和 MTProxy,我們現在不考慮,如果有要求,可能在另一篇文章中考慮。 我們現在只會回憶起這個 MTProxy,它在 2018 年發布後不久,供應商很快學會了完全阻止它,旨在 塊旁路, 經過 數據包大小! 還有一個事實,即 MTProxy 服務器(同樣由 Waltman)用 C 編寫,儘管根本不需要(Phil Kulin 會確認),但與 Linux 細節不必要地聯繫在一起,而且在 Go 或 Node.js 上也有類似的服務器適合不到一百行。

但在考慮其他問題後,我們將在本節末尾對這些人的技術素養得出結論。 現在,讓我們繼續討論第 5 個 OSI 層,會話——他們在上面放置了 MTProto 會話。

密鑰、消息、會話、Diffie-Hellman

他們把它放在那裡並不完全正確......會話不是活動會話下界面中可見的會話。 但秩序井然。

對 Telegram 協議和組織方法的批評。 第 1 部分,技術:從頭開始編寫客戶端的經驗 - TL、MT

在這裡,我們從傳輸層接收到一串已知長度的字節。 這要么是加密消息,要么是明文——如果我們仍處於密鑰協商階段並且實際上正在這樣做的話。 我們在談論哪些稱為“密鑰”的概念? 讓我們為 Telegram 團隊本身澄清這個問題(我很抱歉將我自己的文檔從英語翻譯成早上 4 點的大腦疲倦,保留一些短語更容易):

有兩個實體叫做 會議 - 一個在“當前會話”下的官方客戶端用戶界面中,其中每個會話對應於整個設備/操作系統。
第二個 - MTProto會話,其中有一個消息序列號(在低級意義上),並且 可能在不同的 TCP 連接之間持續。 可以同時設置多個 MTProto 會話,例如,以加快文件下載速度。

在這兩者之間 會議 是概念 授權. 在退化的情況下,可以說 界面會話 是相同的 授權但是,唉,這很複雜。 我們看:

  • 新設備上的用戶首先生成 授權鍵 並將其綁定到帳戶,例如,通過 SMS - 這就是為什麼 授權
  • 它發生在第一個 MTProto會話, 其中有 session_id 在你自己裡面。
  • 在這一步,組合 授權 и session_id 可以稱為 - 這個詞出現在一些客戶的文檔和代碼中
  • 然後,客戶端可以打開 幾個 MTProto 會話 在同一個 授權鍵 - 到同一個 DC。
  • 然後有一天客戶需要從 另一個DC - 對於這個 DC,將生成一個新的 授權鍵 !
  • 告訴系統這不是新用戶註冊,而是同一個 授權 (界面會話), 客戶端使用 API 調用 auth.exportAuthorization 在家用直流 auth.importAuthorization 在新的DC中。
  • 都一樣,可能有幾個開 MTProto 會話 (每個都有自己的 session_id) 到這個新的 DC,在 授權鍵.
  • 最後,客戶可能想要完全前向保密。 每一個 授權鍵 這是 永久 密鑰 - 每個 DC - 客戶端可以調用 auth.bindTempAuthKey 用來 臨時 授權鍵 - 再一次,只有一個 暫時驗證金鑰 每個 DC,對所有人通用 MTProto 會話 到這個DC。

注意 (和未來的鹽)也是一個 授權鍵 那些。 所有人共享 MTProto 會話 到同一個DC。

“在不同的 TCP 連接之間”是什麼意思? 這意味著這個 就像是 網站上的授權 cookie - 它持續(存活)到該服務器的許多 TCP 連接,但總有一天它會變壞。 與 HTTP 不同的是,在 MTProto 中,消息在會話內部按順序編號和確認,它們進入隧道,連接斷開 - 在建立新連接後,服務器將在本次會話中發送它在之前未傳遞的所有內容TCP 連接。

然而,上述信息是經過數月的訴訟後才得到的。 與此同時,我們是否從頭開始實施我們的客戶? -讓我們回到開頭。

所以我們生成 auth_key來自 Telegram 的 Diffie-Hellman 版本. 讓我們試著理解文檔......

Vasily,[19.06.18/20/05 1:255] data_with_hash := SHAXNUMX(data) + data + (any random bytes); 這樣長度等於XNUMX字節;
encrypted_data := RSA(data_with_hash, server_public_key); 一個 255 字節長的數字(大端)被提高到必要模數的必要冪,結果存儲為 256 字節數字。

他們得到了一些毒品 DH

看起來不像健康人的DH
dx 中沒有兩個公鑰

好吧,最後,我們弄明白了,但沉積物仍然存在——客戶完成了工作證明,證明他能夠對數字進行因式分解。 針對 DoS 攻擊的保護類型。 而RSA密鑰只在一個方向上使用一次,本質上是為了加密 new_nonce. 但是,當這個看似簡單的操作成功時,您將不得不面對什麼?

Vasily,[20.06.18/00/26 XNUMX:XNUMX] 我還沒有達到 appid 請求

我向 DH 發送了請求

並且,在運輸工具的碼頭上寫著它可以用 4 個字節的錯誤代碼來回答。 就是這樣

好吧,他告訴我-404,那又怎樣?

我對他說:“抓住你的 efigna 用服務器密鑰加密,我想要 DH”,它愚蠢地響應 404

您會如何看待這樣的服務器響應? 該怎麼辦? 沒有人要問(但在第二部分中有更多內容)。

這裡所有對dock的興趣都在做

無事可做,只夢想著把數字來迴轉換

兩個 32 位數字。 我像其他人一樣打包它們

但是不,你首先需要的是這兩個作為 BE

Vadim Goncharov,[20.06.18/15/49 404:XNUMX PM] 因為這個 XNUMX?

瓦西里,[20.06.18/15/49 XNUMX:XNUMX PM] 是的!

Vadim Goncharov,[20.06.18/15/50 XNUMX:XNUMX PM] 所以我不明白他能“找不到”什麼

瓦西里,[20.06.18 15:50] 關於

我沒有找到這樣的分解成簡單的除數)

連報錯都沒掌握

Vasily,[20.06.18/20/18 5:XNUMX PM] 哦,還有 MDXNUMX。 已經有三個不同的哈希值

密鑰指紋計算如下:

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

SHA1 和 sha2

所以讓我們把 auth_key 根據 Diffie-Hellman,我們得到了 2048 位的大小。 下一步是什麼? 然後我們發現這個密鑰的低 1024 位沒有以任何方式使用......但是讓我們現在考慮一下。 在這一步,我們與服務器有一個共享的秘密。 已經建立了一個 TLS 會話的模擬,這是一個非常昂貴的過程。 但是服務器還不知道我們是誰! 還沒有,實際上 授權. 那些。 如果您考慮的是“登錄密碼”,就像它曾經在 ICQ 中一樣,或者至少是“登錄密鑰”,就像在 SSH 中一樣(例如,在某些 gitlab / github 上)。 我們得到了匿名。 如果服務器回答我們“這些電話號碼由另一個 DC 提供服務”? 甚至“您的電話號碼被禁止”? 我們能做的最好的事情就是保存密鑰,希望到那時它仍然有用並且不會腐爛。

順便說一句,我們有保留地“收到”它。 例如,我們信任服務器嗎? 他是假的嗎? 我們需要密碼檢查:

Vasily,[21.06.18/17/53 2:XNUMX PM] 為了簡單起見,他們提供移動客戶端來檢查 XNUMXkbit 數字)

但是一點都不清楚,nafeijoa

Vasily, [21.06.18/18/02 XNUMX:XNUMX] 碼頭沒有說如果結果不簡單怎麼辦

沒說。 讓我們看看Android 的官方客戶端在這種情況下做了什麼? A 就是這樣 (是的,整個文件在那裡都很有趣)——正如他們所說,我就把它留在這裡:

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

不,當然有 一些 有檢查數字的簡單性,但就我個人而言,我不再擁有足夠的數學知識。

好的,我們得到了萬能鑰匙。 登錄,即發送請求,有必要執行進一步的加密,已經使用 AES。

消息密鑰定義為消息正文(包括會話、消息 ID 等)的 SHA128 的中間 256 位,包括填充字節,前面加上取自授權密鑰的 32 個字節。

瓦西里,[22.06.18/14/08 XNUMX:XNUMX PM] 平均母狗

已收到 auth_key. 全部。 此外,他們……從碼頭上還不清楚。 隨意研究開源代碼。

請注意,MTProto 2.0 需要 12 到 1024 字節的填充,但仍需滿足生成的消息長度可被 16 字節整除的條件。

那麼要放入多少填充物?

是的,這裡也是 404,以防出現錯誤

如果有人仔細研究了圖表和文檔的文本,他會注意到那裡沒有 MAC。 並且該 AES 在其他任何地方都沒有使用的某些 IGE 模式中使用。 當然,他們在他們的常見問題解答中寫下了它......在這裡,消息密鑰本身同時是用於檢查完整性的解密數據的 SHA 哈希 - 如果不匹配,文檔出於某種原因建議默默地忽略它們(但是安全性如何,突然破壞我們?)。

我不是密碼學家,也許在這種情況下,從理論上的角度來看,在這種模式下並沒有錯。 但我絕對可以使用 Telegram Desktop 的示例來命名一個實際問題。 它以與 MTProto 中的消息(僅在本例中為 877 版)相同的方式加密本地緩存(所有這些 D783F5D3D8EF1.0C),即首先是消息密鑰,然後是數據本身(以及主要的大 auth_key 256 字節,沒有它 msg_key 無用)。 因此,這個問題在大文件上變得很明顯。 即,您需要保留數據的兩個副本 - 加密和解密。 如果有兆字節或流媒體視頻,例如?..密文後帶有 MAC 的經典方案允許您以流媒體方式讀取它,並立即傳輸它。 使用 MTProto 你必須 起初 加密或解密整個消息,然後才將其傳輸到網絡或磁盤。 因此,在最新版本的 Telegram Desktop 中的緩存中 user_data 另一種格式已經被使用 - 在 CTR 模式下使用 AES。

Vasily,[21.06.18/01/27 20:XNUMX AM] 哦,我發現了 IGE 是什麼:IGE 是“驗證加密模式”的首次嘗試,最初用於 Kerberos。 這是一次失敗的嘗試(它不提供完整性保護),必須被刪除。 這是 XNUMX 年來尋求有效的身份驗證加密模式的開始,最近以 OCB 和 GCM 等模式達到頂峰。

現在來自購物車方面的論點:

Telegram 背後的團隊由 Nikolai Durov 領導,由六名 ACM 冠軍組成,其中一半是數學博士。 他們花了大約兩年的時間才推出當前版本的 MTProto。

有什麼好笑的。 兩年到較低水平

或者我們可以只接受 tls

好的,假設我們已經完成了加密和其他細微差別。 我們最終可以發送 TL 序列化請求並反序列化響應嗎? 那麼應該發送什麼以及如何發送? 這是方法 初始化連接也許就是這樣?

Vasily,[25.06.18/18/46 XNUMX:XNUMX PM] 初始化連接並保存有關用戶設備和應用程序的信息。

它接受 app_id、device_model、system_version、app_version 和 lang_code。

還有一些查詢

一如既往的文檔。 隨意研究開源

如果使用 invokeWithLayer 一切都大致清楚,那麼它是什麼? 事實證明,假設我們有——客戶端已經有一些東西要詢問服務器——有一個我們想要發送的請求:

Vasily, [25.06.18/19/13 XNUMX:XNUMX] 從代碼來看,第一個調用被包裝在這個垃圾中,垃圾本身在 invokewithlayer

為什麼 initConnection 不能是一個單獨的調用,而必須是一個包裝器? 是的,事實證明,它必須在每次會話開始時每次都完成,而不是像主鍵那樣一次性完成。 但! 未經授權的用戶不能調用它! 在這裡我們已經到了適用的階段 這個 文檔頁面 - 它告訴我們......

只有一小部分 API 方法可供未經授權的用戶使用:

  • 驗證發送代碼
  • auth.重發程式碼
  • account.get密碼
  • 驗證.checkPassword
  • auth.check電話
  • 授權註冊
  • 授權登錄
  • auth.import授權
  • 幫助.getConfig
  • 幫助.getNearestDc
  • 幫助.getAppUpdate
  • 幫助.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.get差異
  • langpack.getLanguages
  • langpack.get語言

他們中的第一個 auth.sendCode,還有那個珍貴的第一個請求,我們將在其中發送 api_id 和 api_hash,然後我們會收到一條帶有代碼的短信。 如果我們到達了錯誤的 DC(例如,這個國家的電話號碼由另一個國家提供服務),那麼我們將收到一個錯誤消息,其中包含所需 DC 的號碼。 要通過 DC 號碼找出我們需要連接到哪個 IP 地址,我們將幫助 help.getConfig. 曾經只有5個參賽作品,但在經歷了2018年的知名事件後,數量大幅增加。

現在讓我們記住我們在匿名服務器上的這個階段。 僅僅獲得一個IP地址會不會太貴了? 為什麼不在 MTProto 的未加密部分執行此操作和其他操作? 我聽到一個反對意見:“你怎麼能確定不是 RKN 會用假地址回應?”。 對此我們記得,事實上,在官方客戶中 嵌入式 RSA 密鑰, IE。 你可以 符號 此信息。 實際上,關於繞過客戶端通過其他渠道收到的鎖的信息,這已經完成了(這是合乎邏輯的,這不能在 MTProto 本身中完成,因為您仍然需要知道連接到哪裡)。

好的。 在客戶授權的這個階段,我們還沒有被授權,也沒有註冊我們的申請。 我們現在只想看看服務器對未授權用戶可用的方法有何響應。 和這裡…

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

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

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

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

在計劃中,第一個,第二個來了

在 tdesktop 模式中,第三個值是

是的,從那時起,當然,文檔已經更新。 儘管很快它可能會再次變得無關緊要。 新手開發人員應該如何知道? 也許如果您註冊您的應用程序,他們會通知您? Vasily 這樣做了,但是很遺憾,沒有任何東西發送給他(同樣,我們將在第二部分中討論這個)。

...您注意到我們已經以某種方式轉移到 API,即到下一個級別並錯過了 MTProto 主題中的某些內容? 沒什麼奇怪的:

Vasily,[28.06.18/02/04 2:XNUMX AM] 嗯,他們正在翻找 eXNUMXe 上的一些算法

Mtproto 為兩個域定義了加密算法和密鑰,以及一些包裝器結構

但是他們不斷地混合不同的堆棧級別,所以並不總是很清楚 mtproto 在哪裡結束以及下一個級別從哪裡開始。

它們是如何混合的? 好吧,這裡是 PFS 的相同臨時密鑰,例如(順便說一下,Telegram Desktop 不知道該怎麼做)。 它由 API 請求執行 auth.bindTempAuthKey, IE。 從頂層。 但與此同時,它會干擾較低級別的加密 - 例如,在它之後,您需要再次進行 initConnection 等等,這不是 只是 正常要求。 另外,它還規定您在 DC 上只能擁有一個臨時密鑰,儘管字段 auth_key_id 在每條消息中至少允許您更改每條消息的密鑰,並且服務器有權隨時“忘記”臨時密鑰 - 在這種情況下該怎麼辦,文檔沒有說明......好吧,為什麼不可能有多個密鑰,就像一組未來的鹽一樣,但是?..

MTProto 主題中還有其他一些值得注意的事情。

消息消息、msg_id、msg_seqno、確認、錯誤方向的 ping 和其他特性

為什麼您需要了解它們? 因為它們“洩漏”了更高一層,您在使用 API 時需要了解它們。 假設我們對 msg_key 不感興趣,下層為我們解密了一切。 但是在解密後的數據裡面,我們有以下字段(也是數據的長度,可以知道padding在哪裡,不過這個不重要):

  • 鹽-int64
  • session_id - int64
  • message_id - int64
  • seq_no-int32

回想一下,整個 DC 只有一種鹽。 為什麼知道這件事? 不僅因為有要求 get_future_salts,它告訴哪些間隔是有效的,但也因為如果你的鹽是“爛的”,那麼消息(請求)就會丟失。 服務器當然會通過發出來報告新鹽 new_session_created - 但是對於舊的,您將不得不以某種方式重新發送,例如。 這個問題會影響應用程序的架構。

出於多種原因,允許服務器完全丟棄會話並以這種方式響應。 實際上,客戶端的 MTProto 會話是什麼? 這是兩個數字 session_id и seq_no 此會話中的消息。 好吧,當然還有底層的 TCP 連接。 假設我們的客戶仍然不知道如何做很多事情,斷開連接,重新連接。 如果這發生得很快——舊會話在新的 TCP 連接中繼續,增加 seq_no 更遠。 如果花費很長時間,服務器可能會刪除它,因為正如我們發現的那樣,它在它的一側也是一個隊列。

應該是什麼 seq_no? 哦,這是一個棘手的問題。 嘗試誠實地理解其含義:

內容相關消息

需要明確確認的消息。 這些包括所有用戶和許多服務消息,幾乎所有的容器和確認除外。

消息序列號 (msg_seqno)

一個 32 位數字,等於發送者在此消息之前創建的“內容相關”消息(需要確認的消息,特別是那些不是容器的消息)的兩倍,如果當前消息是內容相關的消息。 容器總是在其全部內容之後生成; 因此,它的序號大於或等於其中包含的消息的序號。

這是什麼馬戲團,增量為 1,然後又是 2?..我懷疑原來的意思是“低位為 ACK,其餘為數字”,但結果不太正確 - 特別是,原來可以發 幾個 具有相同的確認 seq_no! 如何? 好吧,比如服務器給我們發了東西,發了,我們自己就沉默了,我們只回复服務確認信息,關於收到他的信息。 在這種情況下,我們的傳出確認將具有相同的傳出號碼。 如果你熟悉 TCP 並認為這聽起來有點瘋狂,但它似乎不是很瘋狂,因為在 TCP seq_no 沒有改變,確認去 seq_no 對方——那我趕緊不高興。 MTProto 即將收到確認 seq_no,就像在 TCP 中一樣,但是 msg_id !

這是什麼 msg_id,這些領域中最重要的? 顧名思義,消息的唯一 ID。 它被定義為一個 64 位數字,其中最低有效位再次具有服務器非服務器的魔力,其餘是 Unix 時間戳,包括小數部分,向左移動 32 位。 那些。 timestamp 本身(並且時間差異太大的消息將被服務器拒絕)。 從這裡可以看出,一般來說,這是一個對客戶端來說是全局的標識符。 雖然 - 記住 session_id - 我們保證: 在任何情況下都不能將用於一個會話的消息發送到另一個會話. 也就是原來已經有 級別——會話、會話號、消息 ID。 為什麼這麼複雜,這個謎團很大。

因此, msg_id 需要……

RPC:請求、響應、錯誤。 確認。

您可能已經註意到,雖然有答案,但模式中的任何地方都沒有特殊類型或函數“發出 RPC 請求”。 畢竟,我們有內容相關的消息! 那是, 任何 留言可以是請求! 或者不是。 畢竟, 每個msg_id. 以下是答案:

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

這是指示這是對哪條消息的響應的地方。 因此,在 API 的頂層,你將不得不記住你的請求有多少 - 我認為沒有必要解釋工作是異步的,並且可以同時有多個請求,回答哪個可以按任意順序退貨嗎? 原則上,從這個,以及類似 no worker 的錯誤消息,可以追踪到這背後的架構:與你保持 TCP 連接的服務器是一個前端平衡器,它將請求定向到後端並收集它們回來 message_id. 這裡的一切似乎都清晰、合乎邏輯且良好。

是嗎?.. 如果你考慮一下? 畢竟,RPC 響應本身也有一個字段 msg_id! 我們是否需要對服務器大喊“你沒有回應我的回答!”? 是的,確認有什麼用? 關於頁面 關於消息的消息 告訴我們什麼是

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

每一方都必須這樣做。 但不總是! 如果您收到 RpcResult,它本身就是一種確認。 也就是說,服務器可以用 MsgsAck 響應您的請求——比如“我收到了”。 可以立即回答 RpcResult。 可能兩者都是。

是的,您仍然必須回答答案! 確認。 否則服務器會認為未送達,再次丟給你。 即使在重新連接之後。 但是這里當然會出現超時問題。 讓我們稍後再看。

同時,讓我們考慮查詢執行中可能出現的錯誤。

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

哦,有人會驚呼,這是一種更人性化的格式——有一條線! 慢慢來。 這裡 錯誤列表但肯定不完整。 從中我們了解到代碼是 - 就像是 HTTP 錯誤(嗯,當然,響應的語義沒有得到尊重,在某些地方它們是由代碼隨機分佈的),字符串看起來像 CAPITAL_LETTERS_AND_NUMBERS 大寫. 例如,PHONE_NUMBER_OCCUPIED 或 FILE_PART_X_MISSING。 好吧,就是你還是要這條線 解析。 例如, FLOOD_WAIT_3600 將意味著你必須等待一個小時,並且 PHONE_MIGRATE_5具有此前綴的電話號碼應在 5th DC 中註冊。 我們有一種類型語言,對嗎? 我們不需要字符串中的參數,正則表達式就可以了,cho。

同樣,這不在服務消息頁面上,但是,正如這個項目已經習慣的那樣,可以找到信息 在另一個文檔頁面上。 或者 引起懷疑. 首先,看,違反打字/層 - RpcError 可以投資 RpcResult. 為什麼不在外面? 我們沒有考慮到什麼?..因此,保證在哪裡 RpcError 不能投資 RpcResult,而是直接或嵌套在另一種類型中? 它缺乏 req_msg_id ?..

但是讓我們繼續服務消息。 客戶端可能認為服務端想了很久,提出了這麼奇葩的請求:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

它有三種可能的答案,再次與確認機制相交,試圖理解它們應該是什麼(以及一般不需要確認的類型列表是什麼),留給讀者作為作業(注意: Telegram 桌面資源中的信息不完整)。

成癮:消息發布狀態

總的來說,TL、MTProto、Telegram很多地方總的來說給人一種固執的感覺,但出於禮貌、圓滑等 軟技能 我們禮貌地對此保持沉默,對話中的髒話被審查了。 然而,這個地方О大部分關於頁面 關於消息的消息 連我這個長期接觸網絡協議,見識過各種彎曲度數的自行車的人都感到震驚。

它以無害的方式開始,需要確認。 接下來,我們被告知

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

好吧,每個開始使用 MTProto 的人都將不得不面對它們,在“更正 - 重新編譯 - 啟動”循環中,在編輯過程中出現數字錯誤或鹽分變質是很常見的事情。 但是,這裡有兩點:

  1. 由此可見,原始消息丟失了。 我們需要隔離一些隊列,我們稍後會考慮這個。
  2. 那些奇怪的錯誤號是什麼? 16、17、18、19、20、32、33、34、35、48、64……湯米,剩下的數字在哪裡?

文件指出:

意圖是將error_code值分組(error_code >> 4):例如代碼0x40 - 0x4f對應容器分解中的錯誤。

但是,首先,向另一個方向轉變,其次,其餘代碼在哪裡並不重要? 在作者的腦海中?.. 但是,這些都是小事。

上癮始於發布狀態消息和發布副本:

  • 消息狀態信息請求
    如果任何一方在一段時間內沒有收到關於其傳出消息狀態的信息,它可以明確地向另一方請求:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • 有關消息狀態的信息性消息
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    在這裡, info 是一個字符串,它包含傳入 msg_ids 列表中每條消息的消息狀態的一個字節:

    • 1 = 消息一無所知(msg_id 太低,對方可能忘記了)
    • 2 = message not received(msg_id在存儲的標識符範圍內,但對方肯定沒有收到過這樣的消息)
    • 3 = message not received(msg_id太高,但是對方肯定還沒有收到)
    • 4 = 消息已收到(請注意,此響應同時也是接收確認)
    • +8 = 消息已經確認
    • +16 = 消息不需要確認
    • +32 = 正在處理或處理已完成的消息中包含的 RPC 查詢
    • +64 = 對消息的內容相關響應已經生成
    • +128 = 對方知道消息已經收到
      此響應不需要確認。 它本身就是對相關 msgs_state_req 的確認。
      請注意,如果突然發現對方沒有看起來像是已發送給它的消息,則可以簡單地重新發送該消息。 即使對方同時收到消息的兩份副本,副本也會被忽略。 (如果時間過長,原來的 msg_id 不再有效,消息將被包裝在 msg_copy 中)。
  • 消息狀態的自願通信
    任何一方都可以自願將另一方發送的消息的狀態告知另一方。
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • 一條消息狀態的擴展自願通信
    ...
    msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
    msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
  • 明確請求重新發送消息
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    遠程方通過重新發送請求的消息立即響應 [...]
  • 明確要求重新發送答案
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    遠程方立即通過重新發送來響應 答案 請求的消息 […]
  • 郵件副本
    在某些情況下,需要重新發送 msg_id 不再有效的舊消息。 然後,它被包裹在一個副本容器中:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    一旦接收到消息,就好像包裝器不存在一樣進行處理。 但是,如果確定收到消息 orig_message.msg_id,則不會處理新消息(同時確認它和 orig_message.msg_id)。 orig_message.msg_id 的值必須小於容器的 msg_id。

讓我們甚至對以下事實保持沉默 msgs_state_info 再次,未完成的 TL 的耳朵伸出來(我們需要一個字節向量,在枚舉的低兩位中,在較舊的位標誌中)。 重點是另一回事。 有誰知道為什麼這一切在實踐中 在真實客戶端 有必要嗎?.. 很難,但是你可以想像如果一個人在交互模式下進行調試會有一些好處 - 詢問服務器是什麼以及如何。 但是這裡描述了請求 往返.

由此可見,每一方不僅必須加密和發送消息,還必須存儲關於它們的數據、關於它們的答案,並且存儲時間未知。 該文檔未描述這些功能的時間或實際適用性。 決不. 最令人驚訝的是,它們竟然被用在了官方客戶端的代碼中! 顯然,他們被告知了一些未包含在公開文檔中的內容。 從代碼中理解 為什麼,不再像 TL 那樣簡單——這不是(相對)邏輯上孤立的部分,而是與應用程序架構相關的部分,即將需要更多時間來理解應用程序代碼。

ping 和計時。 隊列。

從所有方面來看,如果您還記得關於服務器架構的猜測(跨後端的請求分佈),接下來會發生一件相當乏味的事情 - 儘管 TCP 中的所有交付保證(數據已經交付,或者您將被告知中斷,但數據會一直傳送到問題出現的那一刻),即 MTProto 本身的確認 - 沒有保證. 服務器很容易丟失或丟棄您的消息,對此無能為力,只能圍堵各種類型的拐杖。

首先是消息隊列。 好吧,一方面,一切從一開始就很明顯 - 必須存儲並重新發送未經確認的消息。 什麼時候之後? 小丑認識他。 或許那些癮君子服務消息以某種方式用拐杖解決了這個問題,比如說,在 Telegram Desktop 中大約有 4 個隊列與之對應(可能更多,如前所述,為此你需要更認真地研究它的代碼和架構;同時時間,我們知道它不能作為樣本,MTProto 方案中的一定數量的類型沒有在其中使用)。

為什麼會這樣? 可能是服務器程序員無法確保集群內的可靠性,或者至少無法在前端平衡器上進行緩衝,並將此問題轉移到客戶端。 出於絕望,Vasily 嘗試實現一個替代選項,只有兩個隊列,使用 TCP 算法 - 測量到服務器的 RTT 並根據未確認請求的數量調整“窗口”大小(在消息中)。 也就是說,這種用於估計服務器負載的粗略啟發式方法 - 它可以同時處理我們的多少請求而不丟失。

嗯,也就是說,你明白了吧? 如果您必須在基於 TCP 的協議之上再次實現 TCP,則表明協議設計非常糟糕。

哦,是的,為什麼需要多個隊列,一般來說,這對於使用高級 API 的人來說意味著什麼? 你看,你提出一個請求,你序列化它,但通常不可能立即發送。 為什麼? 因為答案會是 msg_id, 這是暫時的а我是一個廠牌,約會最好推遲到越晚越好——突然服務器會因為我們和它的時間不匹配而拒絕它(當然,我們可以做一個拐杖,把我們的時間從現在轉移通過添加從服務器響應計算的增量到服務器時間 - 官方客戶端這樣做,但由於緩衝,這種方法是粗糙和不准確的)。 因此,當您使用庫中的本地函數調用發出請求時,消息會經歷以下階段:

  1. 位於同一個隊列中,正在等待加密。
  2. 任命 msg_id 並且消息進入另一個隊列 - 可能轉發; 發送到套接字。
  3. a) 服務器回复 MsgsAck - 消息已發送,我們將其從“其他隊列”中刪除。
    b) 反之亦然,他不喜歡某些東西,他回答了 badmsg - 我們從“其他隊列”重新發送
    c) 什麼都不知道,有必要從另一個隊列重新發送消息——但不知道確切的時間。
  4. 服務器終於回答了 RpcResult - 實際響應(或錯誤)- 不僅已交付,而且已處理。

也許,使用容器可以部分解決這個問題。 這是當一堆消息被打包成一個時,服務器立即響應所有消息,一個 msg_id. 但他也會拒絕這個包,萬一出了什麼差錯,也是整件事情。

在這一點上,非技術因素開始發揮作用。 從經驗來看,我們已經看到了很多拐杖,此外,現在我們將看到更多糟糕的建議和架構的例子——在這種情況下,是否值得信任並做出這樣的決定? 這個問題是反問的(當然不是)。

我們在說啥啊? 如果在“關於消息的沉迷消息”這個話題上,你仍然可以用“你是愚蠢的,你不明白我們的好主意!”之類的反對意見來推測。 (所以先寫文檔,正常人應該,有基本原理和數據包交換示例,然後我們再說),那麼計時/超時是一個純粹實際和具體的問題,這裡早就知道了。 但是文檔告訴我們關於超時的內容是什麼?

服務器通常使用 RPC 響應確認收到來自客戶端的消息(通常是 RPC 查詢)。 如果響應很長時間才到來,服務器可能首先發送一個接收確認,稍後再發送 RPC 響應本身。

客戶端通常通過向下一個 RPC 查詢添加確認來確認收到來自服務器的消息(通常是 RPC 響應),如果它沒有傳輸得太晚(如果它是在接收後 60-120 秒生成的)來自服務器的消息)。 但是,如果在很長一段時間內都沒有理由向服務器發送消息,或者如果有大量來自服務器的未確認消息(例如,超過 16 個),客戶端將發送一個獨立的確認。

... 我翻譯:我們自己不知道需要多少和多少,好吧,讓我們估計一下就這樣吧。

關於 ping:

Ping 消息 (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

響應通常返回到相同的連接:

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

這些消息不需要確認。 pong 僅在響應 ping 時傳輸,而 ping 可以由任何一方發起。

延遲連接關閉 + PING

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

像 ping 一樣工作。 此外,在收到此消息後,服務器啟動一個計時器,該計時器將在 disconnect_delay 秒後關閉當前連接,除非它收到一條自動重置所有先前計時器的相同類型的新消息。 例如,如果客戶端每 60 秒發送一次這些 ping,它可以將 disconnect_delay 設置為 75 秒。

你是不是瘋了?! 60秒後,列車將進站、上下客,再次在隧道內失去聯繫。 在 120 秒內,當你不在的時候,他會來另一個,連接很可能會中斷。 好吧,很明顯腿是從哪里長出來的——“我聽到了鈴聲,但我不知道它在哪裡”,有 Nagle 的算法和 TCP_NODELAY 選項,它是為交互式工作而設計的。 但是,對不起,延遲它的默認值 - 200 米利秒。 如果你真的想描繪類似的東西並保存在一對可能的數據包上 - 好吧,推遲至少 5 秒,或者任何消息“用戶正在輸入......”的超時現在等於。 但沒有了。

最後,ping。 也就是說,檢查 TCP 連接的活躍度。 這很有趣,但大約 10 年前,我寫了一篇關於我們教員宿舍信使的批評性文本 - 作者還從客戶端對服務器進行了 ping 操作,反之亦然。 但是三年級學生是一回事,國際辦公室又是另一回事,對吧?..

首先,一個小型的教育計劃。 在沒有數據包交換的情況下,TCP 連接可以存在數週。 這有好有壞,取決於目的。 好吧,如果你打開了與服務器的 SSH 連接,你從計算機上站起來,重新啟動電源路由器,返回到你的位置 - 通過該服務器的會話沒有中斷(沒有輸入任何內容,沒有數據包),方便的。 如果服務器上有數千個客戶端,每個客戶端都會佔用資源(你好 Postgres!),這很糟糕,客戶端主機可能很久以前就重新啟動了——但我們不會知道。

聊天/IM 系統屬於第二種情況,還有一個額外的原因——在線狀態。 如果用戶“摔倒”,則有必要將其告知對話者。 否則,將會出現 Jabber 的創建者所犯的錯誤(並糾正了 20 年)——用戶斷開連接,但他們繼續向他寫消息,認為他在線(這在之前的幾分鐘內也完全丟失了)破口被發現)。 不,TCP_KEEPALIVE 選項,許多不了解 TCP 計時器如何工作的人,在任何地方彈出(通過設置野生值,如幾十秒),在這裡無濟於事 - 你需要確保不僅是操作系統內核用戶的機器還活著,但也能正常運行,能夠回答,以及應用程序本身(你認為它不能凍結嗎?Ubuntu 18.04 上的 Telegram Desktop 已經多次崩潰)。

這就是為什麼你應該 ping 服務器 客戶端,反之亦然 - 如果客戶端這樣做,當連接斷開時,ping 將不會被傳遞,目標就不會實現。

我們在電報中看到了什麼? 一切恰恰相反! 好吧,即當然,正式地,雙方可以互相ping通。 在實踐中,客戶使用拐杖 ping_delay_disconnect,它會在服務器上啟動一個計時器。 好吧,對不起,決定他想在沒有 ping 的情況下在那裡住多久不是客戶的事。 服務器根據其負載了解得更多。 但是,當然,如果您不為這些資源感到難過,那麼它們本身就是邪惡的木偶奇遇記,拐杖會掉下來...

它應該如何設計?

我相信上述事實非常清楚地表明 Telegram / VKontakte 團隊在計算機網絡傳輸(和較低)級別領域的能力不是很高,而且他們在相關事務上的資質也很低。

為什麼它會變得如此復雜,Telegram 的架構師又該如何反對呢? 事實上,他們試圖建立一個在 TCP 連接中斷後仍然存在的會話,也就是說,我們現在沒有提供的內容,我們稍後會提供。 他們可能還嘗試過進行 UDP 傳輸,儘管他們遇到了困難並放棄了它(這就是為什麼文檔是空的——沒有什麼值得吹噓的)。 但是由於缺乏對一般網絡和特定 TCP 工作原理的理解,您可以在哪裡依賴它,以及您需要自己做(以及如何)的地方,並試圖將其與密碼學結合起來“一次兩次”一塊石頭的鳥”-結果是這樣的屍體。

應該如何? 基於這樣的事實 msg_id 是防止重放攻擊的加密必要時間戳,將唯一標識符函數附加到它是錯誤的。 因此,在不徹底改變當前架構的情況下(當更新線程形成時,這是本系列帖子另一部分的高級 API 主題),必須:

  1. 保持與客戶端的 TCP 連接的服務器負責 - 如果您從套接字中減去,請確認,處理或返回錯誤,沒有損失。 然後確認不是 id 的向量,而只是“最後收到的 seq_no”——只是一個數字,就像在 TCP 中一樣(兩個數字——你自己的 seq 和確認)。 我們一直在開會,不是嗎?
  2. 防止重放攻擊的時間戳成為一個單獨的字段,la nonce。 檢查,但沒有其他影響。 足夠和 uint32 - 如果我們的 salt 至少每半天更改一次,我們可以將 16 位分配給當前時間整數部分的低位,其餘部分分配給秒的小數部分(就像現在一樣)。
  3. 縮回 msg_id 完全-從後端區分請求的角度來看,首先是客戶端ID,其次是會話ID,然後將它們連接起來。 相應地,作為請求標識,一個就夠了 seq_no.

也不是最好的選擇,一個完整的隨機數可以作為一個標識符——順便說一下,這在發送消息時已經在高級 API 中完成了。 最好完全從相對到絕對重新構建架構,但這是另一部分的主題,而不是這篇文章。

API?

噠噠噠! 因此,在經歷了充滿痛苦和拐杖的道路後,我們終於能夠向服務器發送任何請求並接收對它們的任何答复,以及從服務器接收更新(不是響應請求,而是它發送給我們自己,例如 PUSH,如果有人更清楚的話)。

注意,現在文章中將出現唯一的 Perl 示例! (對於那些不熟悉語法的人,bless 的第一個參數是對象的數據結構,第二個是它的類):

2019.10.24 12:00:51 $1 = {
'cb' => 'TeleUpd::__ANON__',
'out' => bless( {
'filter' => bless( {}, 'Telegram::ChannelMessagesFilterEmpty' ),
'channel' => bless( {
'access_hash' => '-6698103710539760874',
'channel_id' => '1380524958'
}, 'Telegram::InputPeerChannel' ),
'pts' => '158503',
'flags' => 0,
'limit' => 0
}, 'Telegram::Updates::GetChannelDifference' ),
'req_id' => '6751291954012037292'
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'req_msg_id' => '6751291954012037292',
'result' => bless( {
'pts' => 158508,
'flags' => 3,
'final' => 1,
'new_messages' => [],
'users' => [],
'chats' => [
bless( {
'title' => 'Хулиномика',
'username' => 'hoolinomics',
'flags' => 8288,
'id' => 1380524958,
'access_hash' => '-6698103710539760874',
'broadcast' => 1,
'version' => 0,
'photo' => bless( {
'photo_small' => bless( {
'volume_id' => 246933270,
'file_reference' => '
'secret' => '1854156056801727328',
'local_id' => 228648,
'dc_id' => 2
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'local_id' => 228650,
'file_reference' => '
'secret' => '1275570353387113110',
'volume_id' => 246933270
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1531221081
}, 'Telegram::Channel' )
],
'timeout' => 300,
'other_updates' => [
bless( {
'pts_count' => 0,
'message' => bless( {
'post' => 1,
'id' => 852,
'flags' => 50368,
'views' => 8013,
'entities' => [
bless( {
'length' => 20,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 18,
'offset' => 480,
'url' => 'https://alexeymarkov.livejournal.com/[url_вырезан].html'
}, 'Telegram::MessageEntityTextUrl' )
],
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'text' => '???? 165',
'data' => 'send_reaction_0'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 9'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'message' => 'А вот и новая книга! 
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
напечатаю.',
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571724559,
'edit_date' => 1571907562
}, 'Telegram::Message' ),
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'message' => bless( {
'edit_date' => 1571907589,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571807301,
'message' => 'Почему Вы считаете Facebook плохой компанией? Можете прокомментировать? По-моему, это шикарная компания. Без долгов, с хорошей прибылью, а если решат дивы платить, то и еще могут нехило подорожать.
Для меня ответ совершенно очевиден: потому что Facebook делает ужасный по качеству продукт. Да, у него монопольное положение и да, им пользуется огромное количество людей. Но мир не стоит на месте. Когда-то владельцам Нокии было смешно от первого Айфона. Они думали, что лучше Нокии ничего быть не может и она навсегда останется самым удобным, красивым и твёрдым телефоном - и доля рынка это красноречиво демонстрировала. Теперь им не смешно.
Конечно, рептилоиды сопротивляются напору молодых гениев: так Цукербергом был пожран Whatsapp, потом Instagram. Но всё им не пожрать, Паша Дуров не продаётся!
Так будет и с Фейсбуком. Нельзя всё время делать говно. Кто-то когда-то сделает хороший продукт, куда всё и уйдут.
#соцсети #facebook #акции #рептилоиды',
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 452'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'text' => '???? 21',
'data' => 'send_reaction_1'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'entities' => [
bless( {
'length' => 199,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 8,
'offset' => 919
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 928,
'length' => 9
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 6,
'offset' => 938
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 11,
'offset' => 945
}, 'Telegram::MessageEntityHashtag' )
],
'views' => 6964,
'flags' => 50368,
'id' => 854,
'post' => 1
}, 'Telegram::Message' ),
'pts_count' => 0
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'message' => bless( {
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 213'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 8'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 2940,
'entities' => [
bless( {
'length' => 609,
'offset' => 348
}, 'Telegram::MessageEntityItalic' )
],
'flags' => 50368,
'post' => 1,
'id' => 857,
'edit_date' => 1571907636,
'date' => 1571902479,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'message' => 'Пост про 1С вызвал бурную полемику. Человек 10 (видимо, 1с-программистов) единодушно написали:
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
Я бы добавил, что блестящая у 1С дистрибуция, а маркетинг... ну, такое.'
}, 'Telegram::Message' ),
'pts_count' => 0,
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'pts_count' => 0,
'message' => bless( {
'message' => 'Здравствуйте, расскажите, пожалуйста, чем вредит экономике 1С?
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
#софт #it #экономика',
'edit_date' => 1571907650,
'date' => 1571893707,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'flags' => 50368,
'post' => 1,
'id' => 856,
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 360'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 32'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 4416,
'entities' => [
bless( {
'offset' => 0,
'length' => 64
}, 'Telegram::MessageEntityBold' ),
bless( {
'offset' => 1551,
'length' => 5
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 3,
'offset' => 1557
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 1561,
'length' => 10
}, 'Telegram::MessageEntityHashtag' )
]
}, 'Telegram::Message' )
}, 'Telegram::UpdateEditChannelMessage' )
]
}, 'Telegram::Updates::ChannelDifference' )
}, 'MTProto::RpcResult' )
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'update' => bless( {
'user_id' => 2507460,
'status' => bless( {
'was_online' => 1571907651
}, 'Telegram::UserStatusOffline' )
}, 'Telegram::UpdateUserStatus' ),
'date' => 1571907650
}, 'Telegram::UpdateShort' )
};
2019.10.24 12:05:46 $1 = {
'in' => bless( {
'chats' => [],
'date' => 1571907946,
'seq' => 0,
'updates' => [
bless( {
'max_id' => 141719,
'channel_id' => 1295963795
}, 'Telegram::UpdateReadChannelInbox' )
],
'users' => []
}, 'Telegram::Updates' )
};
2019.10.24 13:01:23 $1 = {
'in' => bless( {
'server_salt' => '4914425622822907323',
'unique_id' => '5297282355827493819',
'first_msg_id' => '6751307555044380692'
}, 'MTProto::NewSessionCreated' )
};
2019.10.24 13:24:21 $1 = {
'in' => bless( {
'chats' => [
bless( {
'username' => 'freebsd_ru',
'version' => 0,
'flags' => 5440,
'title' => 'freebsd_ru',
'min' => 1,
'photo' => bless( {
'photo_small' => bless( {
'local_id' => 328733,
'volume_id' => 235140688,
'dc_id' => 2,
'file_reference' => '
'secret' => '4426006807282303416'
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'file_reference' => '
'volume_id' => 235140688,
'local_id' => 328735,
'secret' => '71251192991540083'
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1461248502,
'id' => 1038300508,
'democracy' => 1,
'megagroup' => 1
}, 'Telegram::Channel' )
],
'users' => [
bless( {
'last_name' => 'Panov',
'flags' => 1048646,
'min' => 1,
'id' => 82234609,
'status' => bless( {}, 'Telegram::UserStatusRecently' ),
'first_name' => 'Dima'
}, 'Telegram::User' )
],
'seq' => 0,
'date' => 1571912647,
'updates' => [
bless( {
'pts' => 137596,
'message' => bless( {
'flags' => 256,
'message' => 'Создать джейл с именем покороче ??',
'to_id' => bless( {
'channel_id' => 1038300508
}, 'Telegram::PeerChannel' ),
'id' => 119634,
'date' => 1571912647,
'from_id' => 82234609
}, 'Telegram::Message' ),
'pts_count' => 1
}, 'Telegram::UpdateNewChannelMessage' )
]
}, 'Telegram::Updates' )
};

是的,特別是不劇透——如果你還沒有讀過,那就去做吧!

哦,哇~~......它看起來像什麼? 一些非常熟悉的東西……也許這是 JSON 中典型 Web API 的數據結構,除了類可能附加到對象之外?..

事實證明......這是什麼,同志們?..付出了很多努力 - 我們停下來休息 Web 程序員的地方 剛剛開始?.. JSON over HTTPS 會不會更簡單?! 我們換來了什麼? 這些努力值得嗎?

讓我們評估一下 TL+MTProto 為我們提供了什麼以及可能的替代方案。 好吧,HTTP 請求-響應不太合適,但至少是在 TLS 之上?

緊湊的序列化。 看到這個數據結構,類似JSON,想起來還有它的二進制變體。 讓我們將 MsgPack 標記為可擴展性不足,但是有,例如,CBOR - 順便說一下,標準中描述的 RFC 7049. 值得注意的是它定義了 標籤,作為一種擴展機制,並且在 已經標準化 有:

  • 25 + 256 - 用行號引用替換重複行,這種廉價的壓縮方法
  • 26 - 帶有類名和構造函數參數的序列化 Perl 對象
  • 27 - 具有類型名稱和構造函數參數的序列化語言無關對象

好吧,我嘗試在啟用字符串和對像打包的情況下在 TL 和 CBOR 中序列化相同的數據。 結果開始從 XNUMX 兆字節開始有利於 CBOR:

cborlen=1039673 tl_len=1095092

因此, 產量: 有很多更簡單的格式,它們不會出現同步失敗或未知標識符問題,而且效率相當。

快速連接建立. 這意味著重新連接後的 RTT 為零(當密鑰已經生成一次時)——適用於第一個 MTProto 消息,但有一些保留——他們進入了相同的鹽,會話沒有變質,等等。 TLS 為我們提供了什麼回報? 相關報價:

在 TLS 中使用 PFS 時,TLS 會話票證 (RFC 5077) 在不重新協商密鑰且不在服務器上存儲密鑰信息的情況下恢復加密會話。 當打開第一個連接並生成密鑰時,服務器會加密連接狀態並將其發送給客戶端(以會話票證的形式)。 因此,當連接恢復時,客戶端將包含會話密鑰的會話票證發送回服務器。 票證本身使用臨時密鑰(會話票證密鑰)加密,該密鑰存儲在服務器上,必須分發給集群解決方案中處理 SSL 的所有前端服務器。[10]。 因此,如果臨時服務器密鑰被洩露,例如,當它們被長期存儲時(OpenSSL、nginx、Apache 默認情況下在程序運行的整個時間存儲它們;流行站點),會話票證的引入可能違反 PFS使用密鑰幾個小時,最多幾天)。

這裡RTT不為零,至少需要交換ClientHello和ServerHello,然後加上Finished,客戶端就已經可以發送數據了。 但在這裡應該記住,我們沒有 Web,它有一堆新打開的連接,而是一個信使,它的連接通常是一個或多或少長期存在的、相對較短的網頁請求——一切都是裡面多路復用。 也就是說,如果我們沒有遇到非常糟糕的地鐵部分,這是完全可以接受的。

忘了別的東西? 寫在評論裡。

待續!

在本系列文章的第二部分,我們將考慮組織問題而不是技術問題——方法、意識形態、界面、對用戶的態度等。 但是,基於此處提供的技術信息。

第三部分將繼續分析技術組件/開發經驗。 您將特別學到:

  • 各種 TL 類型的混亂的延續
  • 關於通道和超組的未知事物
  • 比對話比花名冊更糟糕
  • 關於絕對與相對消息尋址
  • photo 和 image 有什麼區別
  • 表情符號如何干擾斜體文本

和其他拐杖! 敬請關注!

來源: www.habr.com

添加評論